Branching for 2.5.0-update1

git-svn-id: https://svn.apache.org/repos/asf/shindig/branches/2.5.0-update1@1513693 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/trunk/.gitignore b/trunk/.gitignore
new file mode 100644
index 0000000..f1875cd
--- /dev/null
+++ b/trunk/.gitignore
@@ -0,0 +1,375 @@
+*~
+*.swp
+.idea
+
+# output of git svn show-ignore below
+# /
+/target
+/work
+/dojo
+/*.iws
+/*.ipr
+/*.iml
+/derby.log
+/maven.log
+/build.xml
+/build-dependency.xml
+/velocity.log*
+/junit*.properties
+/surefire*.properties
+/.project
+/.classpath
+/.settings
+/.deployables
+/.wtpmodules
+/.externalToolBuilders
+/create.sql
+/drop.sql
+/derby.log
+
+# /assembly/
+/assembly/.classpath
+/assembly/.project
+/assembly/.settings
+/assembly/shindig.iml
+/assembly/shindig.ipr
+/assembly/shindig.iws
+/assembly/target
+
+# /extras/
+/extras/target
+/extras/work
+/extras/dojo
+/extras/*.iws
+/extras/*.ipr
+/extras/*.iml
+/extras/derby.log
+/extras/maven.log
+/extras/build.xml
+/extras/build-dependency.xml
+/extras/velocity.log*
+/extras/junit*.properties
+/extras/surefire*.properties
+/extras/.project
+/extras/.classpath
+/extras/.settings
+/extras/.deployables
+/extras/.wtpmodules
+/extras/.externalToolBuilders
+/extras/create.sql
+/extras/drop.sql
+/extras/derby.log
+
+# /extras/src/main/java/
+/extras/src/main/java/META-INF
+
+# /features/
+/features/*.iml
+/features/*.ipr
+/features/*.iws
+/features/.classpath
+/features/.deployables
+/features/.externalToolBuilders
+/features/.project
+/features/.settings
+/features/.wtpmodules
+/features/build-dependency.xml
+/features/build.xml
+/features/create.sql
+/features/derby.log
+/features/dojo
+/features/drop.sql
+/features/junit*.properties
+/features/maven-eclipse.xml
+/features/maven.log
+/features/surefire*.properties
+/features/target
+/features/velocity.log*
+/features/work
+
+# /features/src/main/javascript/features/
+/features/src/main/javascript/features/META-INF
+
+# /features/src/main/javascript/features/dynamic-size.util/
+
+# /features/src/main/javascript/features/dynamic-width/
+
+# /java/
+/java/target
+/java/work
+/java/dojo
+/java/*.iws
+/java/*.ipr
+/java/*.iml
+/java/derby.log
+/java/maven.log
+/java/build.xml
+/java/build-dependency.xml
+/java/velocity.log*
+/java/junit*.properties
+/java/surefire*.properties
+/java/.project
+/java/.classpath
+/java/.settings
+/java/.deployables
+/java/.wtpmodules
+/java/.externalToolBuilders
+/java/create.sql
+/java/drop.sql
+/java/derby.log
+
+# /java/common/
+/java/common/target
+/java/common/work
+/java/common/dojo
+/java/common/*.iws
+/java/common/*.ipr
+/java/common/*.iml
+/java/common/derby.log
+/java/common/maven.log
+/java/common/build.xml
+/java/common/build-dependency.xml
+/java/common/velocity.log*
+/java/common/junit*.properties
+/java/common/surefire*.properties
+/java/common/.project
+/java/common/.classpath
+/java/common/.settings
+/java/common/.deployables
+/java/common/.wtpmodules
+/java/common/.externalToolBuilders
+/java/common/create.sql
+/java/common/drop.sql
+/java/common/derby.log
+/java/common/maven-eclipse.xml
+
+# /java/common/src/main/java/
+/java/common/src/main/java/META-INF
+
+# /java/gadgets/
+/java/gadgets/target
+/java/gadgets/work
+/java/gadgets/dojo
+/java/gadgets/*.iws
+/java/gadgets/*.ipr
+/java/gadgets/*.iml
+/java/gadgets/derby.log
+/java/gadgets/maven.log
+/java/gadgets/build.xml
+/java/gadgets/build-dependency.xml
+/java/gadgets/velocity.log*
+/java/gadgets/junit*.properties
+/java/gadgets/surefire*.properties
+/java/gadgets/.project
+/java/gadgets/.classpath
+/java/gadgets/.settings
+/java/gadgets/.deployables
+/java/gadgets/.wtpmodules
+/java/gadgets/.externalToolBuilders
+/java/gadgets/create.sql
+/java/gadgets/drop.sql
+/java/gadgets/derby.log
+/java/gadgets/maven-eclipse.xml
+
+# /java/gadgets/src/main/java/
+/java/gadgets/src/main/java/META-INF
+
+# /java/sample-container/
+/java/sample-container/target
+/java/sample-container/work
+/java/sample-container/dojo
+/java/sample-container/*.iws
+/java/sample-container/*.ipr
+/java/sample-container/*.iml
+/java/sample-container/derby.log
+/java/sample-container/maven.log
+/java/sample-container/build.xml
+/java/sample-container/build-dependency.xml
+/java/sample-container/velocity.log*
+/java/sample-container/junit*.properties
+/java/sample-container/surefire*.properties
+/java/sample-container/.project
+/java/sample-container/.classpath
+/java/sample-container/.settings
+/java/sample-container/.deployables
+/java/sample-container/.wtpmodules
+/java/sample-container/.externalToolBuilders
+/java/sample-container/create.sql
+/java/sample-container/drop.sql
+/java/sample-container/derby.log
+
+# /java/samples/
+/java/samples/target
+/java/samples/work
+/java/samples/dojo
+/java/samples/*.iws
+/java/samples/*.ipr
+/java/samples/*.iml
+/java/samples/derby.log
+/java/samples/maven.log
+/java/samples/build.xml
+/java/samples/build-dependency.xml
+/java/samples/velocity.log*
+/java/samples/junit*.properties
+/java/samples/surefire*.properties
+/java/samples/.project
+/java/samples/.classpath
+/java/samples/.settings
+/java/samples/.deployables
+/java/samples/.wtpmodules
+/java/samples/.externalToolBuilders
+/java/samples/create.sql
+/java/samples/drop.sql
+/java/samples/derby.log
+
+# /java/server/
+/java/server/target
+/java/server/work
+/java/server/dojo
+/java/server/*.iws
+/java/server/*.ipr
+/java/server/*.iml
+/java/server/derby.log
+/java/server/maven.log
+/java/server/build.xml
+/java/server/build-dependency.xml
+/java/server/velocity.log*
+/java/server/junit*.properties
+/java/server/surefire*.properties
+/java/server/.project
+/java/server/.classpath
+/java/server/.settings
+/java/server/.deployables
+/java/server/.wtpmodules
+/java/server/.externalToolBuilders
+/java/server/create.sql
+/java/server/drop.sql
+/java/server/derby.log
+/java/server/maven-eclipse.xml
+
+# /java/server-dependencies/
+/java/server-dependencies/target
+/java/server-dependencies/work
+/java/server-dependencies/dojo
+/java/server-dependencies/*.iws
+/java/server-dependencies/*.ipr
+/java/server-dependencies/*.iml
+/java/server-dependencies/derby.log
+/java/server-dependencies/maven.log
+/java/server-dependencies/build.xml
+/java/server-dependencies/build-dependency.xml
+/java/server-dependencies/velocity.log*
+/java/server-dependencies/junit*.properties
+/java/server-dependencies/surefire*.properties
+/java/server-dependencies/.project
+/java/server-dependencies/.classpath
+/java/server-dependencies/.settings
+/java/server-dependencies/.deployables
+/java/server-dependencies/.wtpmodules
+/java/server-dependencies/.externalToolBuilders
+/java/server-dependencies/create.sql
+/java/server-dependencies/drop.sql
+/java/server-dependencies/derby.log
+
+# /java/server-resources/
+/java/server-resources/target
+/java/server-resources/work
+/java/server-resources/dojo
+/java/server-resources/*.iws
+/java/server-resources/*.ipr
+/java/server-resources/*.iml
+/java/server-resources/derby.log
+/java/server-resources/maven.log
+/java/server-resources/build.xml
+/java/server-resources/build-dependency.xml
+/java/server-resources/velocity.log*
+/java/server-resources/junit*.properties
+/java/server-resources/surefire*.properties
+/java/server-resources/.project
+/java/server-resources/.classpath
+/java/server-resources/.settings
+/java/server-resources/.deployables
+/java/server-resources/.wtpmodules
+/java/server-resources/.externalToolBuilders
+/java/server-resources/create.sql
+/java/server-resources/drop.sql
+/java/server-resources/derby.log
+
+# /java/server-resources/src/main/webapp/
+/java/server-resources/src/main/webapp/META-INF
+
+# /java/social-api/
+/java/social-api/target
+/java/social-api/work
+/java/social-api/dojo
+/java/social-api/*.iws
+/java/social-api/*.ipr
+/java/social-api/*.iml
+/java/social-api/derby.log
+/java/social-api/maven.log
+/java/social-api/build.xml
+/java/social-api/build-dependency.xml
+/java/social-api/velocity.log*
+/java/social-api/junit*.properties
+/java/social-api/surefire*.properties
+/java/social-api/.project
+/java/social-api/.classpath
+/java/social-api/.settings
+/java/social-api/.deployables
+/java/social-api/.wtpmodules
+/java/social-api/.externalToolBuilders
+/java/social-api/create.sql
+/java/social-api/drop.sql
+/java/social-api/derby.log
+/java/social-api/maven-eclipse.xml
+
+# /java/social-api/src/main/java/
+/java/social-api/src/main/java/META-INF
+
+# /java/uber/
+/java/uber/target
+/java/uber/work
+/java/uber/dojo
+/java/uber/*.iws
+/java/uber/*.ipr
+/java/uber/*.iml
+/java/uber/derby.log
+/java/uber/maven.log
+/java/uber/build.xml
+/java/uber/build-dependency.xml
+/java/uber/velocity.log*
+/java/uber/junit*.properties
+/java/uber/surefire*.properties
+/java/uber/.project
+/java/uber/.classpath
+/java/uber/.settings
+/java/uber/.deployables
+/java/uber/.wtpmodules
+/java/uber/.externalToolBuilders
+/java/uber/create.sql
+/java/uber/drop.sql
+/java/uber/derby.log
+
+# /php/
+/php/target
+/php/work
+/php/dojo
+/php/*.iws
+/php/*.ipr
+/php/*.iml
+/php/derby.log
+/php/maven.log
+/php/build.xml
+/php/build-dependency.xml
+/php/velocity.log*
+/php/junit*.properties
+/php/surefire*.properties
+/php/.project
+/php/.classpath
+/php/.settings
+/php/.deployables
+/php/.wtpmodules
+/php/.externalToolBuilders
+/php/create.sql
+/php/drop.sql
+/php/derby.log
diff --git a/trunk/.reviewboardrc b/trunk/.reviewboardrc
new file mode 100644
index 0000000..597d867
--- /dev/null
+++ b/trunk/.reviewboardrc
@@ -0,0 +1 @@
+REVIEWBOARD_URL = "https://reviews.apache.org"
diff --git a/trunk/BUILD-JAVA b/trunk/BUILD-JAVA
new file mode 100644
index 0000000..29a5668
--- /dev/null
+++ b/trunk/BUILD-JAVA
@@ -0,0 +1,138 @@
+Checking out the source code from SVN
+============================================
+
+1) A Subversion client installed in order to checkout the code.
+  * Instructions for downloading and installing Subversion can be found here: http://subversion.tigris.org/
+2) Create a subdirectory and checkout the Apache Shindig code from its Subversion repository
+  * mkdir ~/src/shindig (or wherever you'd like to put it)
+  * cd ~/src/shindig
+3) svn co http://svn.apache.org/repos/asf/shindig/trunk/ .
+
+
+Installing and running the various java servers
+============================================
+
+1) Install Maven 2.0.8 or higher (see http://maven.apache.org)
+
+2) Make sure the JAVA_HOME environment variable is set to the location of your
+   JDK/JRE, and that the maven executable is in your PATH.
+
+3) From the base source directory ( eg cd .. )
+  * mvn - Cleans the source tree and then builds all the java classes, packages
+    them into jars and installs them in your local repository also adds source
+    jars ( by default ~/.m2/repository on Unix/OSX)
+  * mvn install - does the above but does not clean first
+  * mvn -Psocial - builds only the social parts
+  * mvn -Pgadgets - builds only the gadget parts
+
+  You must perform at least a "mvn" to place build all the artifacts and place
+  them in you local maven repository.
+
+4) To Run, using a embedded Jetty Webapp container, in the base project
+       directory (eg cd .. )
+   * First do a full build as in step 3
+   * mvn -Prun - to run Jetty with both social and gadgets
+   * mvn -Prun -DrunType=gadgets - to run Jetty with only the gadgets server
+   * mvn -Prun -DrunType=social - to run Jetty with only the social server
+
+5) To Run with a different port
+   * cd java/server
+   * mvn clean install jetty:run -DrunType=<full|gadgets|social> -Djetty.port=<port>
+
+6) Once running, you can test the gadget rendering server by hitting this url:
+  http://localhost:8080/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
+  Or you can take a look at the sample container here:
+  http://localhost:8080/samplecontainer/samplecontainer.html
+
+
+Editing the Shindig code with Eclipse
+---------------------------------------------
+
+  * Install the Maven Eclipse plugin
+  * Create and import the Eclipse Project files
+  * Clean up some Build Path errors.
+
+1 - Install the Maven Eclipse plugin from
+    http://m2eclipse.sonatype.org/update/
+  
+  * There is an optional dependency on Sublipse which you can get from
+    http://subclipse.tigris.org/
+    If not installing this dependency then select only the Maven Integration plug-in.
+
+2 - Creating and Importing the Eclipse Projects
+
+  * In the project base directory (same level as this file) run
+
+     mvn eclipse:m2eclipse
+
+  * In Eclipse, import the new projects in the File->Import->Existing Project
+    menu.
+  
+    Choose "Select Root Directory" and select the project base directory that this
+    file is in. This should find five Eclipse projects (common, features, gadgets,
+    server and social-api). Click "Finish".
+
+  * In each project, you will have a section called "Maven Dependencies", where
+    you can find all your jars and sources. Use options in the plugin for
+    downloading or source jars and updating dependencies.
+
+3 - A Necessary Clean-up Step
+
+    You should have 6 errors when you start Eclipse until you clean up:
+
+    At the moment, you will have to edit the source dependencies to remove
+    nested folders. On each project, edit properties > Build Path > Source and
+    remove missing Source folders of "config", and "javascript". Do this for
+    each of "gadgets", and "server" projects.
+
+    There is a maven-eclipse-plugin bug tracking this issue in MECLIPSE-444.
+
+To remove all eclipse-related metadata from your shindig source tree, run:
+
+     mvn eclipse:clean
+
+     
+Generating Code Coverage in Eclipse
+----------------------------------
+
+To generate code coverage statistics inside of Eclipse, install the EclEmma plugin
+  * http://www.eclemma.org/
+  * Open org.apache.shindig.gadgets.AllTests
+  * Right-click in the class, and select Coverage as -> JUnit Test
+
+
+Building a Maven Site with Reports
+----------------------------------
+
+To build a Maven based site with reports
+
+Run:
+     mvn clean install site:site site:deploy -Dsite.localurl=file:///Users/ieb/public_html/shindig -Dproject.url=/~ieb/shindig
+
+Where
+    file:///Users/ieb/public_html/shindig is the final directory where you want to deploy the site to
+    /~ieb/shindig is absolute site URL where you want the site to be hosted from. 
+
+
+Running with Caja
+----------------------------------
+
+Caja is an important part of OpenSocial that greatly enhances JavaScript security.
+Caja is managed in a separate open source project hosted by Google code projects.
+For more information on Caja, see: http://code.google.com/p/google-caja/wiki/GettingStarted
+
+   1) Load this page: http://localhost:8080/samplecontainer/samplecontainer.html
+   2) Point it to this gadget: http://localhost:8080/gadgets/SocialHelloWorld.xml
+
+To see the cajoled code (Firefox only), right-click inside the iframe and do "This Frame -> View Frame Source"    
+    
+    
+Additional Reading
+----------------------------------
+
+For more information, see http://shindig.apache.org/
+
+Read javascript/README for instructions for using the Apache Shindig Gadget Container JavaScript
+to enable your page to render Gadgets using gmodules.com or a server started up as described above.
+
diff --git a/trunk/COMMITTERS b/trunk/COMMITTERS
new file mode 100644
index 0000000..adc9578
--- /dev/null
+++ b/trunk/COMMITTERS
@@ -0,0 +1,46 @@
+The following people have commit access to the Shindig sources.
+Note that this is not a full list of Shindig's authors, however --
+for that, you'd need to look over the log messages to see all the
+patch contributors.
+
+If you have a question or comment, it's probably best to mail
+dev@shindig.apache.org, rather than mailing any of these
+people directly.
+
+Blanket commit access:
+
+	   lindner	Paul Lindner        lindner@apache.org       PMC Chair 
+	   agektmr	Eiji Kitamura       agektmr@apache.org       PMC Member
+	  bhofmann	Bastian Hofmann     bhofmann@apache.org      PMC Member
+	    brianm	Brian McCallister   brianm@apache.org        PMC Member Mentor
+	   chabotc	Chris Chabot        chabotc@apache.org       PMC Member
+	  dbentley	Daniel Bentley      dbentley@apache.org      PMC Member
+	      doll	Cassie Doll         doll@apache.org          PMC Member
+	 dpeterson	Dan Peterson        dpeterson@apache.org     PMC Member
+	      etnu	Kevin Brown         etnu@apache.org          PMC Member
+	       ieb	Ian Boston          ieb@apache.org           PMC Member
+	      evan	Evan Gilbert        evan@apache.org          PMC Member
+	    jasvir	Jasvir Nagra        jasvir@apache.org        PMC Member
+	     johnh	John Hjelmstad      johnh@apache.org         PMC Member
+	     jyang	Jun Yang            jyang@apache.org         PMC Member
+	     sgala	Santiago Gala       sgala@hisitech.com       Emeritus Mentor
+	     lryan	Louis Ryan          lryan@apache.org         PMC Member
+	   martint	Martin Traverso     martint@apache.org       PMC Member
+	  vsiveton	Vincent Siveton     vsiveton@apache.org      PMC Member
+	      zhen	Zhen Wang           zhen@apache.org          PMC Member
+	    awiner	Adam Winer          awiner@apache.org        PMC Member
+	     chico	Chico Charlesworth  chico@apache.org         PMC Member
+	    chirag	Chirag Shah         chirag@apache.org        PMC Member
+	  hsaputra	Henry Saputra       hsaputra@apache.org      PMC Member
+	  chaowang	Jacky Wang          chaowang@apache.org      PMC Member
+	   zhoresh	Ziv Horesh          zhoresh@apache.org       PMC Member
+	     gagan	Gagandeep Singh     gagan@apache.org         PMC Member
+	     hnguy	Han Nguyen          hnguy@apache.org         PMC Member
+	      fitz	Brian Fitzpatrick   fitz@apache.org          Mentor    
+	    gstein	Greg Stein          gstein@apache.org        Emeritus Mentor
+	   sylvain	Sylvain Wallez      sylvain@apache.org       Mentor    
+	     tomdz	Thomas Dudziak      tomdz@apache.org         Mentor    
+	 upayavira	Upayavira           upayavira@apache.org     Mentor    
+	 dharkness	David Harkness      dharkness@apache.org     Emeritus  
+	   henning	Henning Schmiedehausenhenning@apache.org     Emeritus
+	   woodser	Eric Woods			woodser@apache.org		 PMC Member
\ No newline at end of file
diff --git a/trunk/KEYS b/trunk/KEYS
new file mode 100644
index 0000000..184917c
--- /dev/null
+++ b/trunk/KEYS
@@ -0,0 +1,277 @@
+This file contains the PGP keys of various Apache developers.
+Please don't use them for email unless you have to. Their main
+purpose is code signing.
+
+Apache users: pgp < KEYS
+Apache developers:
+        (pgpk -ll <your name> && pgpk -xa <your name>) >> this file.
+      or
+        (gpg --fingerprint --list-sigs <your name>
+             && gpg --armor --export <your name>) >> this file.
+
+Apache developers: please ensure that your key is also available via the
+PGP keyservers (such as pgpkeys.mit.edu).
+
+
+----------------------------------------------------------------
+
+pub   1024D/40E47E14 2008-07-19 [expires: 2009-07-18]
+uid                  Ian Boston <ieb@apache.org>
+sig 3        40E47E14 2008-07-19  Ian Boston <ieb@apache.org>
+sig          51047D66 2009-04-30  Tony Stevenson <pctony@apache.org>
+sub   4096g/1B6E03E8 2008-07-19 [expires: 2009-07-18]
+sig          40E47E14 2008-07-19  Ian Boston <ieb@apache.org>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.8 (Darwin)
+
+mQGiBEiBwKARBADwHzaGyqRANC+sG8WaPQXX7WBKpKpDSFuYB8IcZM/EEXVvAwO5
+vJR2xTs3Vqjz6VJPDkUaNeYANKjuEbMT7dCfNgTe2ZTU2zAuC5+SMwtfESwTceUl
+5XB2vueHxiVHHutXAJR9FBt96oO/+mvBSeyjAymDPF4uQ2+fyS/xvqzDgwCglcIz
+A3oCJvKjU10QOIjNfdTcQnsD/iwimsGjnwvnfNYmjxAIg/MKZBTuoDpatIbw7kmr
+73I0M2JYWqwwfHCoFZmbMQ47IMW3LNQLecQpP9wJ0g60hbB/KFfxfabMVKJzexnp
+uPqmkpVblIc7yFPYknr74LVzgPfpKBqi+RhwNax3w1DQAwauwPWaC+VbiM8KG4v8
+G0uhBACc6vKTLTtxDP2qD82JE1IoWr6riDVRJ9CLQ6+DxiQmbNQtu4DuVq7yGpzb
+EYox3Q1v6zobJSyaW4Sd+8Zlcb+6e4R9PPmWOHJpVYp67UrGTEgyzvrgskkOSTzr
+x1q1OnhtyFxLqBG51niM7ZjucxqZGyPUFJ9BdkxwpZLtrFnxaLQbSWFuIEJvc3Rv
+biA8aWViQGFwYWNoZS5vcmc+iGYEExECACYFAkiBwKACGyMFCQHgk1AGCwkIBwMC
+BBUCCAMEFgIDAQIeAQIXgAAKCRCyDRE5QOR+FFCIAJ9tFipKFKY/dXSBxKhBSfIL
+lHLxVgCdFxNeaM+GC0Rp+LN0Xr6C3SyZzSGIRgQQEQIABgUCSfl7NAAKCRDJx5JO
+UQR9ZusBAJkBPA5P9O3uprXKQWakTASsGT9xqQCghi5VvXT5aUpbSLSTz9cpa8BE
+TxG5BA0ESIHAoBAQAOPkWKIWQGcPPHcgRk5wRNjt8msaGC9s+8SMjBvKQe0nY9Wj
+m91Flv/JwGCUlPdiIHRb61KH+ERo/SCkDbR9jI6F6Wck68j1Qd4R1+4cYTFSo+YW
+wdeeT29D94erZAshUKY4ISSQV02tYSm1di1sxB+GaOC9m097hd1/X9Upu1sbL7te
+P7f23gbxrBvPGMIVmZybUdIxb+DOCnfYbgJnkjywxbeaJAHA64FeeRsSc+g6yE+X
+fd54Auz+heCS01CGdWzbNo5JKjqQxn113tFupzEJaPNyZSHoUe0XND//vBxIUzzp
+cl7rbzrYUuxMLqrKLLGZVztbLLRpme5lh/sQLEjWH76hgikKt9oI9xXRFFJox9bJ
+NJC4V22XzxT6syWVGI2Yh8xkUw96Y4GGUv8ppKSbNLikV7ghVeJ7RIhkp85x9a8i
+5JmNXD5luUqjK6SHsWb9CtVcS9LvwzFrh6h8aGbY+7UeACkd8MKGu/h+WOR+vUWX
+s0/uNqaH9wnI8TJ0HjKiLolip3KfJbUXs1jtxTZOPK/Ye7cM5QvgE+sUX7ysf8H1
+yWswtdQXrrbcp8YEPpK9hx9mObir+K2kh5Hi08fTZ3vowks8kjUlXiJ/O7L2ZD98
+gole4b7jPSSgxqe4hJ4cli38GUiJf5uwyd7fVKIoBNd0mxonr1B3RRuJasfLAAMG
+D/9GsPmkLaBWpNYCJ/umPfCh748TbBG+2zHuYIryB9+Ap+w2Z/V6pkEtm71GFOWG
+3dp6HmuimJDFrYukzwwaeDGAyFA9ef05att0xh6h1gY4eKhDfQwr6nYXkDoI/EIq
+cPl+uJ5FbP+fX9LbsqXp96yBuYPGVANb0pojb8XLAzMwO2Bp2w/jEZMkJ3zrLPac
+FlOJquWpyd6MTbNmSgclybge7YyD72F4OtItkrFCj6QoMuKzXC9cFV+1oTUSxvZN
+Qtf/21paohyZKzNNWzuzRReXlB5Gk47t2bzIEWCHRLi7JbmzIax90QvgFvfApeg9
+7StF59miTFs7E62pQRpMFy6ee4yROUld81HdHHhY+ooYaYnnbyeWk67d4y1E+l1Y
+h5+xBNPKchST80j1qdO7zJ8gP4+4WR7KnbrM4Gpr/7gdr9SYvERgITdqBKsJzRDN
+HZo+qEStUpn1TU5aZiAIfWKKo6xLA6Vv2T4pqnuzUDZR1JUqGSH2CiO6nIDeHDBe
+jDLYmVvq7aav+YUGvykim+2R+r1tLuHflEhZVVsYocHf2nJup6/9S4wBUKUl5/7f
+gy/xT2C6exHLcj7RjkkJH0nWw7SDS5l30ceEUB3n5txdwd6KlM5cBcWYWL7eOmiu
+V5xJ1nVvH/toSWxLNcq6MfZPt1ORwUUYO7Uu4QKpefFyBYhPBBgRAgAPBQJIgcCg
+AhsMBQkB4JNQAAoJELINETlA5H4UgSoAnjmsj20WnZuH7lS96qpoYO7B6yvQAJwJ
+2CXltU6lxdMF6RTkDQ73ESqNbg==
+=T/rN
+-----END PGP PUBLIC KEY BLOCK-----
+
+
+pub   1024D/73F58054 2001-11-30
+      Key fingerprint = 5383 6DAC BD1E 18D3 4D15  E98F F859 2852 73F5 8054
+uid                  Paul Lindner <lindner@inuus.com>
+sig 3        73F58054 2009-10-01  Paul Lindner <lindner@inuus.com>
+sig 3        73F58054 2001-11-30  Paul Lindner <lindner@inuus.com>
+sig          29C90CD8 2009-10-16  Paul Lindner (CODE SIGNING KEY) <lindner@apache.org>
+uid                  Paul Lindner <lindner@apache.org>
+sig 3        73F58054 2009-10-01  Paul Lindner <lindner@inuus.com>
+sig          29C90CD8 2009-10-16  Paul Lindner (CODE SIGNING KEY) <lindner@apache.org>
+sub   1024g/9F1830C5 2001-11-30
+sig          73F58054 2001-11-30  Paul Lindner <lindner@inuus.com>
+
+pub   4096R/29C90CD8 2009-10-16
+      Key fingerprint = 5613 D0E7 F428 DF12 AA79  0C9E 5C91 2E98 29C9 0CD8
+uid                  Paul Lindner (CODE SIGNING KEY) <lindner@apache.org>
+sig 3        29C90CD8 2009-10-16  Paul Lindner (CODE SIGNING KEY) <lindner@apache.org>
+sig          F8EA2967 2009-10-29  Brian McCallister <brianm@apache.org>
+uid                  Paul Lindner <lindner@inuus.com>
+sig 3        29C90CD8 2009-10-16  Paul Lindner (CODE SIGNING KEY) <lindner@apache.org>
+sig          F8EA2967 2009-10-29  Brian McCallister <brianm@apache.org>
+sub   4096R/BE6FEB9B 2009-10-16
+sig          29C90CD8 2009-10-16  Paul Lindner (CODE SIGNING KEY) <lindner@apache.org>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.9 (GNU/Linux)
+
+mQGiBDwHUowRBACNOdhA/O6xXHNOVwtHHHIbleYHowOGQd1Mp3wg/yshK2iX1rin
+m8GwBp62xJ52XslBUMh8Nt9iFJNQ8spiVp7wINK3zb7hqEL3sqK33Oa6HZdcCbOT
+8eqkexTm383z8Hi7+l/3kGCPESU0l9VjYhJqc3nJ1R03ObmEHtz7XjrJpwCgra4X
+hpV5j/NVHR/qAwqj9wvhk58D/izahgeWTeHG0mKun9VdSRYYgRM0kM1PXOrnKczf
+CaGKpkcKP1NZ7CZIlV1kTgIImfdk1RqR2QJW3idHpzsk2MEZrjZ4gj5JDBXgvT/e
+d0zi/cEYFhzCfcoQflKqTCvvb7CCAV6GYx4zKJuoW5RR5n9ilVJUF1c1huKyBMyS
+CJvMA/4kifP+nDH5/oW2Bhd/9s4LfzZE8k7AlG1grpPTKqSqmVpE9LnP3m7OJ67V
+ac39+gXn/yGm/nUDeOnjwZfg6D8aHsqPOWVOjrppBDUv6sINS7EM0su4ceDEsVb0
+R77vjzmBoA3SlsH4ednHLpTHF7OR+Bd3L8M/aTM1YhMRTktrJ7QgUGF1bCBMaW5k
+bmVyIDxsaW5kbmVyQGludXVzLmNvbT6IbgQTEQIALgULBwoDBAMVAwIDFgIBAheA
+AhkBBQJKxRjVExhodHRwOi8vcGdwLm1pdC5lZHUACgkQ+FkoUnP1gFRCdACfeqs0
+UPaH2W4l43n6asW6iqhuwHwAoKCSGM+GaO9rDwoRDrwXX47ErYiniFoEExECABoF
+CwcKAwQDFQMCAxYCAQIXgAIZAQUCPAdSjQAKCRD4WShSc/WAVN6dAKCXRGzXtwGC
+4/kCInEvdGLqpcSRQACgmyEE9lAk3Qrw58AmXgnqC6XqyuSJAhwEEAEKAAYFAkrY
+3pUACgkQXJEumCnJDNjUzRAAlKCLG6tTiviPSMEW092/7nEeIg/XD1FXy+nMmLvV
+L/CblCqP0HVaZNpiUaQuvpd2t+6ZEQS4oFleByrNGl3q6CpbRK/Az/sHj05DRpMm
+if4Q8NFl1TKc2oJ7dNdasLhFIs7v9AS6GiYHnP6E4/I7/C2yu9tAsr++4aAeOUPc
+UbBGAsvXkWNWxqAJzpbKnQoZeUDvb613zA3vPdXFK/KmP2byrX9hpUNVcxUGBYAd
+bjQZTaNWgnQo08qMv7As+rFDzo5rZ8+jkY/kjWzqVYptZz8vEuJXQhGu2TaMA1ds
+gBPyIZZrElFLQRbrzeLwcZWXzjkxgGRkshEYgf/BRFhrehBqJQcwtSA7tNewleVj
+su/r+fP2LVnoPDQigKJWwz0Qz5L9iiOZgMZKEto9u/RXR3k9/D1GdOsF9D5I28Cp
+GAVgCjiPpp+w7gEQjl66/NZw/PuNnL9ZXo3WO/hwflE8UjXkztbIXT1Dz7kRo3ST
+rbESss/cNpWbdpzkdbcfuwvR8jhvVCWg4NTI8xhID6RBvpVCL/GbEf1ZXOZDTGpw
+D49830EQuP1Eq1CKI+lF4hPQLM435TEYt/+55kTPLNwK+pw9ofWW3ebD5/CjiHf2
+pg+dukkMxIQ6tZ4MfcfnqxMpAN40EefAeKYt4mXFd8/g4SYD67H6CrjeMeWyS49p
+TB20IVBhdWwgTGluZG5lciA8bGluZG5lckBhcGFjaGUub3JnPoh0BBMRAgA0Ahsj
+BgsJCAcDAgQVAggDBBYCAwECHgECF4AFAkrFGNgTGGh0dHA6Ly9wZ3AubWl0LmVk
+dQAKCRD4WShSc/WAVGq7AKCX+J2pah29qcrciz9DtFv/jzkmaQCfYIxUUAn/jNY3
+dhoQ5PJ7wvGJZ5uJAhwEEAEKAAYFAkrY3pgACgkQXJEumCnJDNh78g//YMYP8Kz3
+gUgM/RYzcqdUySR4rtzSbwclsjLPO9l6q31VY2SbPjUsWj6Yeewwmger6kil3fBA
+9+jWyATkWHtk+oambkwZhbeg4NjHK98YloZZjlYFML37Dv0BCmaTEChZN0LHN8sp
+42tPyibnrdR4XUurOvsUBZmo3NepuKRqLWzsrzqEcCLaOLOnlBFsLH6QAeY43sZp
+PZ4t/wCVYCLftP29XQcgxgMg2l6x8YD3rsVqbh7eKt3qfAohnYBf592H9jYGp55n
+KiPDzatQQ0YgdxXUPAgOgRXFFA/ZOsztEnxYfRPBz5e1jSbx6eAlu+cNCHQYQYAV
+y//zuMg0OP+YS/OdHzlPuiWQEMR4slI3+7kxuO0wRrQHg19f10vROzf0hwrvDwQH
+u7inT5hyP8K3NHmtBkqb+Y8xAlMJhVbX5URRAzBx/Pu7SOgMtwNtwsR5AqKuOKpY
+Jd7FcuImiQ/mHMS3gycPNqjHC/+idiT2eJufGN6fG3Y9d5gFxtCVCa8dSCDPhJgM
+iIkSPlDn4yvFik5wVeJMsWgWOXtqrLx3be1l2LPp7cOOsPmvWc9qrnliBsuzpH2r
+Zz+4aIVDRd6Fknd+q8RABrAJkaHbXzZ6QRXaJC+LE7cG5XBS0lPp+c9LaP4b4SZH
+GxmjAwDBM2cunJt3MVERvyzEbJADjbUAAu65AQ0EPAdSjxAEALjWCG0R8aeFOzDO
+dBWCCADeXZCFjbY2HIbApTtIQ1CwGwKbqUtNudGimXHbUfAx4dBaZgThvNq32q/j
+LMzPfexVUZBvYAPcsMkQICO+bAa7A9QUDKRBhJAuLZCjM2T7lO39/5Dr7mgF57D5
+lG5PKpejJ5Dqg/MSuQf9nvc82SyHAAMHBACUrVGs4Dnuwk7QArOje9WoUSlCgZ2i
+s4+RDgqPmgAaYDYqa3uXgzs/egKzSG3D3Z5u1a4eCW4QjW4+zOlGgHHPAj8EmvQh
+HNITeFTJOAT6NmuFOdcN69iUoEf0U2bgPuqI5CSDnA9dr63aLVuxhl+z4s02USC1
+AVPwFXuQu1uLrIhGBBgRAgAGBQI8B1KPAAoJEPhZKFJz9YBUSOYAn0ZACoFrHF7t
+/AeWz/ku9MnWBV/SAKCoTKb43yE9KLOypJqu7DG2YC/sgZkCDQRK2M3wARAAlzhm
+GmKt30o/UzM8r/m0RSYZvGb9owXYiSZwNJtUkpQW6thy/cNgI5gzk6zbCN5HdhkB
+Rq8nqssLbn+0HQkeUH04JiAaFSBwhOymYIc+BsE3/HbXMPuO0CKJ5my5GOXwY3+u
+Bp35HunZvkQyAqxjvL6Nc7leofHZ5Sb76dZ4Pli3fykb4vEKCh4WOpmeuFlk/hF8
+B/x4VvIwwQEAOhqyaBXClUHodCXmqcTDUR60eGaAkrmVc8keuXCFP0rGLqesgAxi
+aabLDMKnFIxirO/GeTqgXW3SjqK4cXXBz3y8MIG0pB4Z7VPFNOcxv2n2SoqeH+pX
+g/+WWB57HSpml5qmbhcaiGv9ncSnE9Crga/WIKsdJAMOaQH8gfn3KOLzODNuJi+k
+XOWTpO3JRij/29LzR6tVQr24E/KZqnkBBdY4MqeUpTuJpfDhYF+J6ZNa71W+pmWG
+l2s76AbkODPAPNvLgFMBDCo3+zOFjiZlIVT8Y29Z7gjL/nVEIVhHEm8f2+dFr7tj
+Qu0x3s2OvkHcwoWQQ3cwFxaeHZsli8oXK4H+gabHAml/JTiaK/yIFTv/6RZNiiGI
+kbgCTVeZhafQDth+/EFwpXVYqRb7nCELuftY175/dHCmHykK5gEfyGSy28fuKCze
+6JHAb3nSVvEJqpkWebCZbNnZcgt+Y/7wQGRxFEMAEQEAAbQgUGF1bCBMaW5kbmVy
+IDxsaW5kbmVyQGludXVzLmNvbT6JAjYEEwECACACGwMCHgECF4AFAkrY0C8FCwkI
+BwMFFQoJCAsEFgIDAQAKCRBckS6YKckM2AWID/41Zy+w6wjdv++hrnl1SDUX84oU
+rMCN+tkBzWLCd+rZpP8oiGvLfj656Mn45fZMKICVtJ30YLekHYVurru4fLxw2SiT
+LWx+1qw+zhh/6xUoWLe/sBW+lgTyzcqNZWkk83+dP4NmbafdvEIg0rUmWdsPbjxh
+iIBhHSh36f7wwPfc6fEy8uhoSxKTr+lroM5zpk+/81Np8jEDR/7btGwKwPf54mer
+duf2+4umgmjzR/x0OXZFlz8EgOxDnY70WCsytaPOzE2QMNWVRqmJjsSvkPVUj46u
+etBlFmI1FCV+FUv3sACTcCnEKa9Wy4iMGhfnDmboQm5zOQ38r0l0D7fWOKfcvAuO
+218OIVKrmk6Rp+t2wkhNgfkHk8Hb94apqr8P9hRS2iZTGRq6a17M4F2ZEVO12tiy
+q+O3ImLNPfIJdI4K01xEH3AmlSEGrXveAQYx9UfY+GByxpyp0lyUSaPvzlb6Kvpb
+sKzqiRqGXu5Woe+khpRoavDE31iVlTLmPWjElcOHqit8WcyiHVhNNBLu+Z5HJk+i
+rdVQgohi+QJaAwAQHBaog6Zlr0ZzFyowi0gDEvXlOv3Rnrzhp/hIpvEpJKeTJHRv
+Nq/wFQoFLbJXw34yLhO5eZo8/flhAQ6ybr0JUnNpYbIS0IaKV+QTBz1CzKxoPjAP
+S3tHWK7+6VP+83vGrIhGBBARCAAGBQJK6fSsAAoJEGjrjHb46iln904An12jnW3k
+sQWxxm7AoHt73GpKogMzAJ91KwjNUaJnl+pBa1fbKb8+CF1B3LQ0UGF1bCBMaW5k
+bmVyIChDT0RFIFNJR05JTkcgS0VZKSA8bGluZG5lckBhcGFjaGUub3JnPokCNwQT
+AQoAIQUCStjeIQIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRBckS6YKckM
+2NC0D/9uq+WaI9kcg3Q84EU2VdChvLHQ+7K2dMl6+x75plZefbT1caGR7blnZ2Uc
+VPTc/u4nBt5E3Jfprak9DUOz2mPEKQRCdag8zZT2/UXyU79oRfDqMHx4RDnDZWzA
+rEau9skc0RjweER79PQBdVVx1O15OolF/L0fU6pETgAsAVbYLFOCV30tpxHijQyZ
+f7LM2WJlE4rol9BRtGLRHXQhPnS1No+RHORt+4j/38+CeA+0CVKg9JpWkLYQIuhF
+M53LwcuF1bfcX4j9vR2kQdS3aUfSAdPkRuZsbXlaYaQLKPr74XBbWm9/SjRLorIn
+XRV6Dz2syhXvQ6PzTSDdQThXtJhKYmh1oRS4GyQxV5QVfgcp03BsvUDhGFJ12Cjk
+jmIzHW99HFDOfzQAaiD83B49QNNusr/7qT2XaStz+iq1eyefHpn0cE/zDWv8E6RR
+CQ73+OWPAXJlia0BTgKzSUzdaVNrjjCGA6dKNXhYk9tTfmeRxgBOWMQ9pn9jtq0N
+zDof9wsGXG7hRfsBaq6jKc3bcFzKAHl/0nKUWVwWrhLGkVs39talB57HKN2UxrTj
+bEisCBcu7y4AehkdU5uwUTbRwhhfLGwZ/7qJ5XCIAARcLVzisah8NnZx1EcNutWX
+jSiPsxCV2OiJZOj88hOMoPS2c/TnStSydiba2PpWAZPI+hInaohGBBARCAAGBQJK
+6fSsAAoJEGjrjHb46ilnMDUAn21X6AEjHC5djKeBwajlFbI0805BAJ9cROA+Cs3j
+ynxW7/RX9FySZUcGbbkCDQRK2M3wARAAzK7WdTJlvrS0cf8wIhvajQaKh7OoKdKF
+2rkxVZuLqh6CvcFNHWkSJt8O5SomvL7lIdRHT6f9XYEfZBnREZPG93UoGAw1R8kQ
+cN9+QqR+nRKB0crlUxzfDtDqTMfeSCl2ZX/Fny+mb1TqhH+ASHMCNVDD4qMWtnXQ
+B34FSyMvS68BTTcslRFuVcLwERjSAspXPBYE1+lVAthKBWsqsKRICs5IIGoJuB9x
++S/vsFOtRv+9Ap6vTwX/LyTfYY71CwGhMdAAk/Ul7yjuZ6LMqP9avL2q0VHqD/69
+0eSYI8w6u4mDHmiwKnoPNlq/ACj4OSHTSQZIH0RYAaiGzfjMIQWDsuR8zsO065yF
+Qdyi8uvb5ZfDIGyJYJKv5gus/vfLZLqIW8zz6GDSv30PwYflJekUFKNxoC8ZzY1k
+PJ2aHEumDLmJAQKNbaGT+J/3px6aX3GZHQ0M/u0qtCoNcuRRLSogRveSap1ScAN8
+0Gzd4OImFTnzjO4zrELbCdsZaH9FZmOyUzYFiPBSaGGcV4D+yXbolwkxw1QKp3LA
+vvegDt0wl4/UdFCAHYZiauQtkaAb1gbs1g91c13osbCV+F01/ZKnEgHhFYQgoeT9
+c1YPGu3CMfck4uSNrJyV29au9fD4Kmg6udqMH3NjM0owCvB7WQlXjq3NCJqFz64E
+S7b8kfv14VkAEQEAAYkCHwQYAQIACQUCStjN8AIbDAAKCRBckS6YKckM2M3QD/9r
+GF1RNK4lnjQ3Bcw5rWNFqJQOiPx3mIPgF5cuxtRIuXjS1EG4TdYDd9KpUHqlCMgY
+Evwt6JC6bK4BPdHD7A0lq0qtCvlOdxPMc334JeMa0Sp4ZoXQ+ub+hrt7OsGrsX9s
+1W/o5iLuU/huQuOkX+ig6I5gWCNOTOGC7UnnksrX9rei2Vu68gzv2DqXzvKU3UiX
+fauqCMT6ojMT5LNVlr9yC17wjiPxZSYR5bEQ5nyaknibbVUGF7MjSnErgXfbrkuM
+6qJkPWzbImbJU8mIU6axD+V4VXvfZFcunz5muEeEWf8oaF9XNgpRJibKpH24S4pO
+0JOSH96c1V7ck1KdNXKBBI52FPq2BBwqUSkFR5kogTZQcDGZFYwjlq8xiA/CxK2w
+wiywqyisyQuqkXMUOJpFTX6K36MJqNhFTkzN7oJaTk8lr9tD2QovLl3rrzwOJdGx
+/fXqiM5et6p+FSq0ksa3nA3V40mrxe+OhhydUwziMxA46FPABHPQCvle8+5GmYgs
+m8UUeaZG+eHRl1slJGfOpmXtfvf5nZVCLSgKODQt+DWpz9Zk5rhjPrNqGhz8O5xd
+w4fHO4B4GPfw4O+S1yomAhx9ieohOSujylp5MrHFla3YV5cjBDdAxElQFqo1mFgw
+kGwvWuAYzrj8X9DQdDOIt/pGejT4IUhy/4nf/PnOyQ==
+=79yX
+-----END PGP PUBLIC KEY BLOCK-----
+pub   4096R/680DB18A 2012-05-24
+      Key fingerprint = 6179 0832 009C 2247 F56D  42F4 D3FC CEEE 680D B18A
+uid                  Ryan Jason Baxter (CODE SIGNING KEY) <rbaxter85@apache.org>
+sig 3        680DB18A 2012-05-24  Ryan Jason Baxter (CODE SIGNING KEY) <rbaxter85@apache.org>
+sig          EEAA42C0 2012-05-30  Matt Franklin (CODE SIGNING KEY) <mfranklin@apache.org>
+sub   4096R/28C41552 2012-05-24
+sig          680DB18A 2012-05-24  Ryan Jason Baxter (CODE SIGNING KEY) <rbaxter85@apache.org>
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG/MacGPG2 v2.0.17 (Darwin)
+Comment: GPGTools - http://gpgtools.org
+
+mQINBE+9jsgBEACyZZh1gGgdCfKOxLD7qhI3jWgIlj4Vr65LcohYkmygWFvANgCB
+pdFzm+DL7AtR3lUVaAaT2IuJbbpJUR4BQT5urMA82j8RznUGQmbYrTPwf4yoANQA
+Vxu4pHgZGDHzqWJsO1CnROusA+14rwtUQL/yUDjUHVXlo8Xy++jUUXLyc+MAMYQ/
+KicOFcmLz3V/CugpRAO/1Fl9IRHWDSp4/g/EsY3XjQEAheiQRa4o631I+Bxc74gu
+e00jMmvyTuEdXt5BU6lZdgmfHo9AVcbF/vAl6WhJ/IWrY+VW6v1sHRl2nGZPPlml
+McYaueV71QHCtprm/7WHdsEYM26C1y0+FkMCvtbx9XMLvzV+NFtWp4qhrR2Dycrh
+MMtRc1/KpRIYmT8C2t9jUnQibmBb392i2i9a8oFsnCnGLE2F8Od6yvakJsBS9d2C
+6aKhsnVDaLSHAmFMGEF9LT8xl2TK40dRymPQcx0pjOAlSjWqzQZQ/ndOkDtPdtVI
+E5IoTLcyX9PDCnjuC9v8VOAeDDsfq4kl3eM3AVFo/t7eJySE4sd0Y5BsIEMpzLXg
+9us8MmaGOhhisz+GosmOwadfQZWZtUQ3EQUchAC9vt7Tb29nKwQrQufD13Bb7fzc
+Xw6D1QD2kW80FBYQYnQhR+Tj4iO3EXCUIF4yRWpPjwsHuZv3Ojmt6B2CtwARAQAB
+tDtSeWFuIEphc29uIEJheHRlciAoQ09ERSBTSUdOSU5HIEtFWSkgPHJiYXh0ZXI4
+NUBhcGFjaGUub3JnPokCMQQTAQIAGwIbAwIeAQIXgAUCT72QXQULCQgHAwUVCgkI
+CwAKCRDT/M7uaA2xilQlD/sF+z8OHzhQ5lt1NcVp4PA9rSeez2gd9NJ20l2nFrdw
+XuUow3vMnCRIFSGt4BZEuSUpYfjSrzM8Gn5lVadjDgMwEgfU8RFBJQu3t5gxBknt
+qJpVgcqOArJyL1bZtxsYe1hrZByQ6UcdpQxfCaUie6T0maMzfROWDbf+Zyn/So0N
+xitudI8DiTSV2QPK6Ki1265R7qkAEoV2yE/E2xvbZPBv0XP8rN/FReYP9zRfLQS7
+SgY2PAEeaIYg++QqEhsNKw/nOPF7KkvLJJ9gq4KqLHdbh3/pO4yvl861M0OZI8JG
+vVxIsIHDfgi+9XRs9D/w93UmIFaEi457/f62HQymcBQs5i1aaEgVBMOPh16kcfe7
+Ddt65aa8h1Ph5TTKL5eJmT6x2AybIneiPvrOphVlpQRRH9CVzfiTE9vIgUfNd3b2
+0mi5SbewkVmurqA+vP6bfEEUQ34a6+EcPmkJnX2eRdwmRm9r08lPpLYzh0n2XqBB
+kjMFN09wZADCAMtWOkn2DUowrvW+le1hhyGljPl1toNc2y55UQUcYi9jZ0YeGmLD
+u9MV5KMNwldWO//GHBGclEvppdrDVmD/VZEXG1CfFNr8m209aE371TlDVVx+uenF
+t0gaJFZBBY6j7XkHfbHyKjXdXa1+z3Puo3E5fMzdU+opacgB/tIe5WoGHlECVpok
+dYkCHAQQAQIABgUCT8YMPwAKCRAcVyFb7qpCwJwxEACROTJYQwnAYBAe5VyBXF9d
+fyt5TJPjFF8SO5DRY9c4TYmd8UXIURAkV3GUwy4AK1/rAakHz+qeq4K2e0vuNBwy
+3dA5YDR2RwGUzIFsPY3IsKj5aKTgIc8sO20zKDnvbUZr+4MT3DjUmfEJEqsSGLc1
+d+bn1RW9CI+1H9F+Q5JlnGE4nE+QxkFsRt0Apk9Ki898CSN46+mqb2xmuL2sku6u
+Ds88zlGA2U+Y1iEhf3ceMOQJjI+bkNqQ6sO9cOd5WP+76r8iJtvQo7KxFqMcT6k0
+EsnHSju+pQZtVsNAhM3ji3tU0pv5ffT57qtCguLRNNYO0Auf4ycGry8CKAMrEPAO
+9jUMBGKMwXAvJC5GoY9V4uUYbu8YnUUMbul6iG2Cl0RWchxiyraDiixSDNtNgIAk
+njdwtwPCvJ3YupWPe6821uS3qYa7aD0VIy4j5LBP1hMlqxMzfz4GWcXLgFTmAJpT
+4DNUlt3uHv6p6z4KNqe4OyaRi+d8IF0nNRhqLBNUqQNuaoDBM8hgRcpZhtFLvXLA
+JmSm+JFFwFJTKLcqYGlnuu6DLVzpuqPCoJ6b51WYIy98PR/yYhhx625Blpmb19H2
+q55EjjP+SdJ9PpLitynyWmP51ilc30nL433UffwN1D3LVH76HeZplKqmAXBDRCvc
++aTuIJb3RvOzb61+6oMp87kCDQRPvY7IARAA4Ue+GZol2JrDJm+uL5GI7zlB5+Mw
+1ozElKUlwav5fKjpH1QEVyqi1hO0L+OGm/dWlZ0jN7yjnRGZleHFnEc5ZgmfY1lZ
+gjhaFcr9tgPeNah5BeKSbIjNJVEVhuf/fd5G3pcrOyf7kkQImOlClj9gUR/iY7DG
+WxMJuY10hkWqMFjxxX4/8Lh73UVZx25582DfbutBf2c6bcBv4cwex5vWFNKXC/7a
+5Wz9jMU3MJGRRTWGW1goIyAYDSe4i4LNew5zObN6kQEgg32f1pPyYpc3BpVHexBi
+1eAMla+lZNblHa5CWWmrG83U3wwGdjrJBVqCTO3XjLKXyOJq//gsV6rjFA1nltVC
+Z1InKdIfycM/oyWokvP1I9ZNLwCwCD2pXxFiRG5RUgZ3FuLxjlzb4FjqNo58PhmL
+zmcUBTB+2TQRe0/yReBnbTEQSu5mTZzev5HRJSGQJ8xjYr6/rWGWnuiyVQFoDkca
+9cbsjAQB18m8PqwkXlfXjK1QARWTAG8LuamBcygJV+ZUEsrI+q7wjD+cLq1JwTJX
+5J1EHFN2dlO6HVcEzgF3ivrGVUl1kPgD5EHJH4VLML7ca5Gul19nHSza2aLuYoeJ
+OV0vcKK5xIOR/JB79fP+VObcliowqfkIbVQOSxJuM2IiC1PWpyEmNaqx8kXAotxu
+wwoaQB5UFkti++kAEQEAAYkCHwQYAQIACQUCT72OyAIbDAAKCRDT/M7uaA2xigVt
+D/40YtYfD+kLkhOZANGxabJR17pz+Fcylg4XXjFLh4/RZMgZeQ4rKmTEqkJO0eCe
+MzSlbj3QFeF3KRY9FD8qbI6ltu2jBYrIlw2/ZnvhaWKMn5grDNIMrMmFL+FmmMMb
++5pV3kCTV4z4JwYhgz/KLlnpI67Ax4V5pFItFg2OWRhTF9gr4sUAOMQP2Eof3Tcr
+JbIjKuV2yQ9zIn8/mN0jGjxpr2pAdbR1U8r/Myl3qYUfQErNRQC+7l/VVv/fBq72
+cEznyYX7PIRxpxraJ/GrbCMijxNbGPAh5iSrvhyPrYfBWdpgMq6YTp7c9KuJyhhQ
+2DPEjkJwXbG+78hkFPmIdnSx1yUFhw67jcVzKvNRHydayYqCapZ87wOht7AWUp4s
+oux+U0PCFoMptzOfmo/xIvTL/Ws+QykA3nQPmO+wOHjbCmzFnETPCTNSmkkZaJ8i
+3vuLFY5uAin+pwzlw/IvVGOK4MCiXJL+uisSkhT2YSnILpktsaeoZwnNSKGjNK3G
+YZ2NOZqrlt2iSCCXQcajxFtITk4gZg6gmi4a6FuLkWoaBG0+fDZ14iC+b09e3HnL
+RlAY1mZmTUbsHIe50jJeUl3FBlaJfvss8dmLSn+gKj67mtrqRyACOpiYZPKd5/em
+stIf3N+zGQxbzJAL5OXRuaCVmipz2jzx124jWzjbRENxhQ==
+=wScH
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/trunk/LICENSE b/trunk/LICENSE
new file mode 100644
index 0000000..43811be
--- /dev/null
+++ b/trunk/LICENSE
@@ -0,0 +1,417 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+
+===============================================================================
+
+The Apache Shindig distribution includes a number of subcomponents
+with separate copyright notices and license terms. Your use of the
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+===============================================================================
+Zend Framework:
+
+Copyright (c) 2006-2007, Zend Technologies USA, Inc.
+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 Zend Technologies USA, Inc. 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 COPYRIGHT HOLDERS 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 COPYRIGHT OWNER OR 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.
+
+===============================================================================
+PHPUnit:
+
+Copyright (c) 2002-2008, Sebastian Bergmann <sb@sebastian-bergmann.de>.
+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 Sebastian Bergmann nor the names of his
+    contributors may be used to endorse or promote products derived
+    from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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
+COPYRIGHT OWNER OR 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.
+
+===============================================================================
+jsmin.php - PHP implementation of Douglas Crockford's JSMin.
+
+This is pretty much a direct port of jsmin.c to PHP with just a few
+PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
+outputs to stdout, this library accepts a string as input and returns another
+string as output.
+
+PHP 5 or higher is required.
+
+Permission is hereby granted to use this version of the library under the
+same terms as jsmin.c, which has the following license:
+
+--
+Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
+
+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 shall be used for Good, not Evil.
+
+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.
+
+===============================================================================
+OpenSocial Specification 0.8:
+
+Copyright (c) 2008 OpenSocial Foundation (http://www.opensocial.org)
+Released under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+===============================================================================
+Code Mirror:
+ Copyright (c) 2007-2010 Marijn Haverbeke
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any
+ damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any
+ purpose, including commercial applications, and to alter it and
+ redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software. If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+    distribution.
+
+===============================================================================
+swfobject:
+
+The MIT License
+
+Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
+
+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.
+
+
+ Marijn Haverbeke
+ marijnh@gmail.com
+
+===============================================================================
+OAuth.php:
+
+The MIT License
+
+Copyright (c) 2007 Andy Smith
+
+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.
+
+===============================================================================
+Symphony ClassLoader:
+
+Copyright (c) 2004-2011 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/trunk/NOTICE b/trunk/NOTICE
new file mode 100644
index 0000000..1e801bb
--- /dev/null
+++ b/trunk/NOTICE
@@ -0,0 +1,22 @@
+Apache Shindig
+Copyright 2013 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+-----------------------------------------------------------
+
+This product includes software (Gadget Server, Gadget Container)
+originally developed by Google Inc. (http://code.google.com/) and licensed
+to the ASF as initial contribution for Shindig.
+
+This product includes software (JSUnit) developed by
+Joerg Schaible (http://jsunit.berlios.de/).
+
+This product contains software (sha1 JS impl) developed by Google Inc.
+
+This product includes software (wave) developed by Google, Inc
+Copyright 2010 Google Inc.
+
+This product includes software (OAuth2 Support) developed by IBM Corporation
+Copyright 2011 IBM Corp.
\ No newline at end of file
diff --git a/trunk/README b/trunk/README
new file mode 100644
index 0000000..3f661dc
--- /dev/null
+++ b/trunk/README
@@ -0,0 +1,69 @@
+                          Apache Shindig
+
+  What is it?
+  -----------
+
+  Shindig is a JavaScript container and implementations of the backend APIs
+  and proxy required for hosting OpenSocial applications.
+
+  Documentation
+  -------------
+
+  The most up-to-date documentation can be found at http://shindig.apache.org.
+
+  Read BUILD-JAVA for instructions on how to build and run the Java server.
+
+  Read java/README for instructions on how to run a Java gadget server.
+
+  Read php/README for instructions on how to run a php gadget server.
+
+  Read javascript/README for instructions for using the Shindig Gadget Container 
+  JavaScript to enable your page to render Gadgets.
+
+  Read features/README for instructions on how to use features.
+
+  Licensing
+  ---------
+
+  Please see the file called LICENSE in the java and php directories.
+
+  Shindig URLS
+  ------------
+
+  Home Page:          http://shindig.apache.org/
+  Downloads:          http://shindig.apache.org/download/index.html
+  Mailing Lists:      http://shindig.apache.org/mail-lists.html
+  Source Code:        http://svn.apache.org/repos/asf/shindig
+  Issue Tracking:     https://issues.apache.org/jira/browse/SHINDIG
+  Wiki:               http://cwiki.apache.org/confluence/display/SHINDIG/
+
+
+This distribution includes cryptographic software.  The country in
+which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of
+encryption software.  BEFORE using any encryption software, please
+check your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software, to
+see if this is permitted.  See <http://www.wassenaar.org/> for more
+information.
+
+The U.S. Government Department of Commerce, Bureau of Industry and
+Security (BIS), has classified this software as Export Commodity
+Control Number (ECCN) 5D002.C.1, which includes information security
+software using or performing cryptographic functions with asymmetric
+algorithms.  The form and manner of this Apache Software Foundation
+distribution makes it eligible for export under the License Exception
+ENC Technology Software Unrestricted (TSU) exception (see the BIS
+Export Administration Regulations, Section 740.13) for both object
+code and source code.
+
+The following provides more details on the included cryptographic
+software:
+
+    Apache Shindig PHP interfaces with the mcrypt API
+    <http://mcrypt.sourceforge.net/> to provide encryption
+    of messages using the AES standard.
+
+    Apache Shindig interfaces with the Java JCE APIs to provide
+    encryption of messages using the AES standard.
+
diff --git a/trunk/UPGRADING b/trunk/UPGRADING
new file mode 100644
index 0000000..42661a5
--- /dev/null
+++ b/trunk/UPGRADING
@@ -0,0 +1,180 @@
+FROM 2.0.x TO 2.5.x
+===================
+
+== API changes ==
+
+The JavaScript GagdetSite.getActiveGadgetHolder has been changed to GagdetSite.getActiveSiteHolder
+
+== container.js config changes ==
+
+* The "gadgets.securityTokenKeyFile" property has been replaced with "gadgets.securityTokenKey".
+The new property allows for embedding the key directly or referencing a classpath or filesystem
+resource.  Please see the comments at the top of container.js and around the new property for more
+details.
+
+== Java Dependency Changes ==
+
+Updates
+* caja r4884 -> r5054
+* closure-compiler (new) r1918
+* commons-codec 1.5 -> 1.7
+* commons-lang to commons-lang3 3.1
+* commons-io 2.1 -> 2.4
+* easymock 3.0->3.1
+* ehcache  2.3.2 -> 2.5.2
+* guava r09->11.0.1
+* guice 2.0->3.0
+* joda-time 2.0->2.1
+* junit 4.10->4.11
+* log4j 1.2.16->1.2.17
+* htmlunit 2.8->2.9 
+* nekohtml 1.9.14->1.9.17
+* slf4j 1.5.11->1.6.1
+* xstream 1.3.1->1.4.3
+
+New
+* tomcat el-api/jasper-el
+
+
+FROM 1.0.x TO 2.0.x
+===================
+
+Almost all interfaces have been updated from 1.0.x -> 2.0.x.  The 
+following information is not complete.
+
+
+== container.js config changes ==
+
+* gadgets.parentOrigins: Default ["*"] (NEW)
+
+An array of valid origin domains for the container.
+
+* Endpoint Changes for rpc
+
+The default RPC endpoints used for json-rpc were moved from /gadgets/api/rpc and /social/rpc to just /rpc
+
+* css for tabs/minimessage is now included in container.js
+
+* System properties shindig.host/jetty.host and shindig.port/jetty.port are injected as SERVER_HOST/SERVER_PORT
+
+== Java Dependency Changes ==
+
+Too many to mention.  Check the top-level pom.xml for all the new versions.
+Here are some highlights:
+
+* slf4j dependencies are needed if you use the EhCache module See http://www.slf4j.org/manual.html
+* guava replaces google-collections
+* caja r3034 -> r4209
+* guice 1.0->2.0
+* guice-multibindings (NEW)
+* nekohtml 1.9.9 -> 1.9.13
+* oauth-* 20080621 -> 20100527 (and others)
+* rome 0.9 -> 1.0
+* rome-modules 0.3.2 (NEW)
+* servlet-api 2.4->2.5
+* ehcache 1.5 -> 1.6.2
+* xstream 1.2 -> 1.3.1
+* xpp3 1.1.3.3 -> 1.1.4c
+* commons-codec 1.3 -> 1.4
+
+== Java Interface Changes ==
+
+* AbstractContainerConfig
+
+Changed signature on getMap() and getList() to use Java
+generics.
+
+* SecurityToken
+
+New methods: getExpiresAt() and isExpired() are now required.  A new AbstractSecurityToken
+base class is available.
+
+* SecurityTokenDecoder 
+
+The interface and implementation are replaced
+with the new SecurityTokenCodec interface and implementations.
+
+You will need to adjust any custom SecurityToken decoders to 
+encode tokens as well as decode them.
+
+* SocialSpiException class is removed, use ProtocolException instead
+
+* GuiceBeanConverter.DuplicateFieldException class is removed 
+
+* RestfulCollection
+
+The constructor RestfulCollection(List<T> entry, int startIndex, int totalResults) is removed.  
+Use the constructor with an items-per-page parameter instead.
+
+* RequestRewriter, ImageRewriter -> ResponseRewriter
+
+ResponseRewriter is replacing RequestRewriter and ImageRewriter. Its interface method is:
+void rewrite(HttpRequest req, HttpResponseBuilder builder);
+
+HttpResponseBuilder extends MutableContent. RequestRewriters may be migrated by
+mutating builder rather than the previously-passed MutableContent. There is no
+provision for reading "original" HttpResponse headers.
+
+ImageRewriters may be migrated to ResponseRewriters as well by mutating the builder,
+where previously a new HttpResponse was returned.
+
+* UrlManager -> IframeUriManager, JsUriManager, OAuthUriManager
+
+The UrlManager interface has been removed. In its place are IframeUriManager, JsUriManager, and OAuthUriManager,
+producing Uris (equivalent to previous Strings). This change is done to better encapsulate Uri logic, putting
+creation and processing logic in the same place.
+
+@see (now-removed) shim class
+http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/GlueUrlGenerator.java?revision=906688&view=markup
+
+...for a schematic on how the previous methods map to new versions. This class was a bridge between the new
+and old interfaces.
+
+Default implementations of each UriManager class are provided in org.apache.shindig.gadgets.uri, each
+named DefaultX, where X = interface. These classes are suitable for subclassing to extend, should you prefer.
+Note that the ContainerConfig values the default implementations use are different (mostly by name/key)
+than those DefaultUrlGenerator used. Specific values are documented in the class comment and statics
+for each impl.
+
+* MediaItem
+
+Location field changed from String to Address.
+
+== Java Guice Changes ==
+
+2.0.x uses Guice 2.0 which allows for @Provides annotations and much more.
+
+* TemplateModule
+
+If you had previously customized the Set of TagHandlers you'll need to start
+using Guice Multibindings instead.  This is much easier than subclassing the
+Guice module.  Here's what you would add to your local module to add a new 
+Tag handler.
+
+  Multibinder.newSetBinder(binder(), TagHandler.class).addBinding().to(MyCustomTagHandler.class);
+
+* SocialApiGuiceModule, DefaultGuiceModule
+
+Configuring a new Rest/RPC handler now uses Multibindings.  Adding a new binding
+is easy, just use the following syntax:
+
+    Multibinder.newSetBinder(binder(), Object.class, Names.named("org.apache.shindig.handlers"))
+        .addBinding().toInstance(MyHandler.class);
+
+The long value annotated with the name "org.apache.shindig.serviceExpirationDurationMinutes" has
+been moved to shindig/common/conf/shindig.properties.  Guice 2.0 can inject Long values from Strings
+automatically.
+
+The Executor.class injection is removed.  Use ExecutorService.class injection instead.
+
+
+* Rename SecurityTokenDecoder to SecurityTokenCodec
+
+This class is renamed to provide a single place to capture both encoding and decoding work
+for gadget security tokens. This also affects classes previously implementing SecurityTokenDecoder
+and previously extending DefaultSecurityTokenDecoder.
+
+== PHP Changes ==
+
+TBD
+
diff --git a/trunk/assembly/pom.xml b/trunk/assembly/pom.xml
new file mode 100644
index 0000000..143a9c8
--- /dev/null
+++ b/trunk/assembly/pom.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Apache Shindig Distribution assembly</name>
+  <description>Assembles the Java/PHP code base into a deployment package.</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/assembly
+    </connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/assembly
+    </developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/assembly</url>
+  </scm>
+
+  <build>
+    <filters>
+      <filter>src/main/assembly/binary-src/README</filter>
+    </filters>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <version>2.2.1</version>
+        <configuration>
+          <descriptors>
+            <descriptor>src/main/assembly/php.xml</descriptor>
+            <descriptor>src/main/assembly/java.xml</descriptor>
+            <descriptor>src/main/assembly/source.xml</descriptor>
+          </descriptors>
+          <tarLongFileMode>gnu</tarLongFileMode>
+          <filters>
+            <filter>${project.build.directory}/assemblyFilter.properties</filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <id>make-assembly</id>
+            <phase>package</phase>
+            <goals>
+              <goal>single</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.geronimo.genesis.plugins</groupId>
+        <artifactId>tools-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>verify-legal-files</id>
+            <phase>verify</phase>
+            <goals>
+              <goal>verify-legal-files</goal>
+            </goals>
+            <configuration>
+              <strict>false</strict>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>align-php-structure</id>
+            <phase>validate</phase>
+            <configuration>
+              <tasks>
+
+                <!-- Rewrite default configuration to release structure -->
+                <mkdir dir="${project.build.directory}/php/config" />
+                <copy todir="${project.build.directory}/php/config">
+                  <fileset dir="../php/config" />
+                </copy>
+
+                <replace file="${project.build.directory}/php/config/container.php">
+                  <replacetoken><![CDATA[/../../]]></replacetoken>
+                  <replacevalue><![CDATA[/../]]></replacevalue>
+                </replace>
+
+               <tstamp>
+                 <format property="year" pattern="yyyy" />
+               </tstamp>
+               <echo file="${project.build.directory}/assemblyFilter.properties">
+year=${year}
+               </echo>
+
+              </tasks>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-gadgets</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-social-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-features</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-extras</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-server</artifactId>
+      <version>${project.version}</version>
+      <type>war</type>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/assembly/src/main/assembly/binary-src/README b/trunk/assembly/src/main/assembly/binary-src/README
new file mode 100644
index 0000000..40cba8c
--- /dev/null
+++ b/trunk/assembly/src/main/assembly/binary-src/README
@@ -0,0 +1,10 @@
+Welcome to Apache Shindig ${project.version} !
+
+Apache Shindig is a reference implementation of the OpenSocial specification.
+
+This is the binary distribution for Shindig, containing a configured war containing
+the sample server and the individual jar files for the features, common, gadgets and
+social-api projects. If you intend to work with the Shindig source, then please download a 
+source distribution.
+
+For more information, see http://shindig.apache.org/
diff --git a/trunk/assembly/src/main/assembly/java.xml b/trunk/assembly/src/main/assembly/java.xml
new file mode 100644
index 0000000..57e104a
--- /dev/null
+++ b/trunk/assembly/src/main/assembly/java.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<assembly>
+  <id>java</id>
+  <formats>
+    <format>zip</format>
+    <format>tar.gz</format>
+    <format>tar.bz2</format>
+  </formats>
+  <includeBaseDirectory>false</includeBaseDirectory>
+  <dependencySets>
+    <dependencySet>
+      <outputDirectory>shindig-${project.version}-java</outputDirectory>
+      <outputFileNameMapping>${artifact.artifactId}-${artifact.version}${dashClassifier?}.${artifact.extension}</outputFileNameMapping>
+      <useTransitiveDependencies>false</useTransitiveDependencies>
+      <includes>
+        <include>org.apache.shindig:shindig-common</include>
+        <include>org.apache.shindig:shindig-features</include>
+        <include>org.apache.shindig:shindig-gadgets</include>
+        <include>org.apache.shindig:shindig-social-api</include>
+        <include>org.apache.shindig:shindig-extras</include>
+      </includes>
+    </dependencySet>
+  </dependencySets>
+  <fileSets>
+    <fileSet>
+      <outputDirectory>shindig-${project.version}-java</outputDirectory>
+      <directory>../</directory>
+      <includes>
+        <include>UPGRADING</include>
+      </includes>
+      <excludes>
+        <exclude>LICENSE</exclude>
+        <exclude>NOTICE</exclude>
+        <exclude>README</exclude>
+      </excludes>
+   </fileSet>
+    <fileSet>
+      <outputDirectory>shindig-${project.version}-java</outputDirectory>
+      <directory>../java</directory>
+      <includes>
+        <include>LICENSE</include>
+      </includes>
+    </fileSet>
+  </fileSets>
+  <files>
+    <file>
+      <source>../java/README</source>
+      <outputDirectory>shindig-${project.version}-java</outputDirectory>
+      <filtered>true</filtered>
+    </file>
+    <file>
+      <source>../java/NOTICE</source>
+      <outputDirectory>shindig-${project.version}-java</outputDirectory>
+      <filtered>true</filtered>
+    </file>
+  </files>
+</assembly>
diff --git a/trunk/assembly/src/main/assembly/php.xml b/trunk/assembly/src/main/assembly/php.xml
new file mode 100644
index 0000000..9c16f27
--- /dev/null
+++ b/trunk/assembly/src/main/assembly/php.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<assembly>
+  <id>php</id>
+  <formats>
+    <format>zip</format>
+    <format>tar.gz</format>
+    <format>tar.bz2</format>
+  </formats>
+  <includeBaseDirectory>false</includeBaseDirectory>
+  <fileSets>
+    <fileSet>
+      <outputDirectory>shindig-${project.version}-php</outputDirectory>
+      <directory>../</directory>
+      <includes>
+        <include>features/**</include>
+        <include>content/**</include>
+        <include>config/**</include>
+        <include>extras/src/main/javascript/**</include>
+      </includes>
+      <excludes>
+        <exclude>site/**</exclude>
+        <exclude>resources/**</exclude>
+        <exclude>etc/**</exclude>
+        <exclude>assembly/**</exclude>
+        <exclude>java/**</exclude>
+        <!-- scm -->
+        <exclude>**/.git/**</exclude>
+        <exclude>**/.svn/**</exclude>
+        <!-- IDE -->
+        <exclude>**/*.iws</exclude>
+        <exclude>**/*.ipr</exclude>
+        <exclude>**/*.iml</exclude>
+        <exclude>**/.project/**</exclude>
+        <exclude>**/.classpath/**</exclude>
+        <exclude>**/.settings/**</exclude>
+        <exclude>**/.externalToolBuilders/**</exclude>
+        <exclude>**/maven-eclipse.xml</exclude>
+        <exclude>**/.deployables/**</exclude>
+        <exclude>**/.wtpmodules/**</exclude>
+        <!-- maven -->
+        <exclude>**/pom.xml</exclude>
+        <exclude>**/target/**</exclude>
+        <!-- misc -->
+        <exclude>*.patch</exclude>
+        <exclude>*.diff</exclude>
+        <exclude>LICENSE</exclude>
+        <exclude>NOTICE</exclude>
+        <exclude>README</exclude>
+      </excludes>
+    </fileSet>
+    <fileSet>
+      <outputDirectory>shindig-${project.version}-php</outputDirectory>
+      <directory>../php</directory>
+      <excludes>
+        <exclude>config/**</exclude>
+        <!-- scm -->
+        <exclude>**/.git/**</exclude>
+        <exclude>**/.svn/**</exclude>
+        <!-- IDE -->
+        <exclude>**/*.iws</exclude>
+        <exclude>**/*.ipr</exclude>
+        <exclude>**/*.iml</exclude>
+        <exclude>**/.project/**</exclude>
+        <exclude>**/.classpath/**</exclude>
+        <exclude>**/.settings/**</exclude>
+        <exclude>**/.externalToolBuilders/**</exclude>
+        <exclude>**/maven-eclipse.xml</exclude>
+        <exclude>**/.deployables/**</exclude>
+        <exclude>**/.wtpmodules/**</exclude>
+        <!-- maven -->
+        <exclude>**/pom.xml</exclude>
+        <exclude>**/target/**</exclude>
+        <!-- misc -->
+        <exclude>*.patch</exclude>
+        <exclude>*.diff</exclude>
+        <exclude>*.zip</exclude>
+        <exclude>*.tar.gz</exclude>
+        <exclude>*.tar.bz2</exclude>
+        <exclude>*.sh</exclude>
+        <exclude>*.bat</exclude>
+        <exclude>**/*.log</exclude>
+        <exclude>**/*.bak</exclude>
+
+        <exclude>NOTICE</exclude>
+        <exclude>README</exclude>
+      </excludes>
+    </fileSet>
+    <!-- Using the temp container.php created by Maven using Antrun plugin to rewrite
+      default configuration to release structure -->
+    <fileSet>
+      <outputDirectory>shindig-${project.version}-php/config</outputDirectory>
+      <directory>target/php/config</directory>
+      <excludes>
+        <!-- scm -->
+        <exclude>**/.git/**</exclude>
+        <exclude>**/.svn/**</exclude>
+      </excludes>
+    </fileSet>
+  </fileSets>
+  <files>
+    <file>
+      <source>../php/README</source>
+      <outputDirectory>shindig-${project.version}-php</outputDirectory>
+      <filtered>true</filtered>
+    </file>
+    <file>
+      <source>../php/NOTICE</source>
+      <outputDirectory>shindig-${project.version}-php</outputDirectory>
+      <filtered>true</filtered>
+    </file>
+  </files>
+</assembly>
diff --git a/trunk/assembly/src/main/assembly/source.xml b/trunk/assembly/src/main/assembly/source.xml
new file mode 100644
index 0000000..b3d45db
--- /dev/null
+++ b/trunk/assembly/src/main/assembly/source.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<assembly>
+  <id>source</id>
+  <formats>
+    <format>zip</format>
+    <format>tar.gz</format>
+    <format>tar.bz2</format>
+  </formats>
+  <includeBaseDirectory>false</includeBaseDirectory>
+  <fileSets>
+    <fileSet>
+      <outputDirectory>shindig-${pom.version}-source</outputDirectory>
+      <directory>${project.basedir}/../</directory>
+      <includes>
+        <include>**</include>
+      </includes>
+      <excludes>
+        <!-- scm -->
+        <exclude>**/.git/**</exclude>
+        <exclude>**/.svn/**</exclude>
+        <!-- IDE -->
+        <exclude>**/*.iws</exclude>
+        <exclude>**/*.ipr</exclude>
+        <exclude>**/*.iml</exclude>
+        <exclude>**/.project</exclude>
+        <exclude>**/.classpath</exclude>
+        <exclude>**/.settings/**</exclude>
+        <exclude>**/.deployables/**</exclude>
+        <exclude>**/.wtpmodules/**</exclude>
+        <exclude>**/.externalToolBuilders/**</exclude>
+        <exclude>**/maven-eclipse.xml</exclude>
+        <!-- maven -->
+        <exclude>**/target/**</exclude>
+        <exclude>**/pom.xml.releaseBackup</exclude>
+        <exclude>**/release.properties</exclude>
+
+        <!-- misc -->
+        <exclude>**/*.patch</exclude>
+        <exclude>**/*.diff</exclude>
+        <exclude>**/*.log</exclude>
+        <exclude>**/*.bak</exclude>
+        <exclude>**/*~</exclude>
+        <exclude>**/#*#</exclude>
+        <exclude>**/%*%</exclude>
+        <exclude>**/samples/create.sql</exclude>
+        <exclude>**/samples/drop.sql</exclude>
+        <exclude>**/derby.log</exclude>
+      </excludes>
+    </fileSet>
+  </fileSets>
+</assembly>
diff --git a/trunk/config/OSML_library.xml b/trunk/config/OSML_library.xml
new file mode 100644
index 0000000..bd24374
--- /dev/null
+++ b/trunk/config/OSML_library.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Templates xmlns:os="http://ns.opensocial.org/2008/markup">
+  <Namespace prefix="os" url="http://ns.opensocial.org/2008/markup"/>
+  <Template tag="os:Name">
+    <os:If condition="${!My.person.profileUrl}">
+      ${My.person.name.formatted}
+    </os:If>
+    <a href="${My.person.profileUrl}" if="${My.person.profileUrl}">${My.person.name.formatted}</a>
+  </Template>
+  <Template tag="os:Badge">
+    <div>
+      <img src="${My.person.thumbnailUrl}" if="${My.person.thumbnailUrl}"/>
+      <os:If condition="${!My.person.profileUrl}">
+        ${My.person.name.formatted}
+      </os:If>
+      <a href="${My.person.profileUrl}" if="${My.person.profileUrl}">${My.person.name.formatted}</a>
+    </div>
+  </Template>
+  <TemplateDef tag="os:PeopleSelector">
+    <Template>
+      <select onchange="os_PeopleSelector_onchange(this, '${My.var}', ${My.max ? My.max : 0}, '${My.onselect}')"
+          name="${My.inputName}"
+          multiple="${My.multiple}">
+        <option repeat="${My.group}" value="${Cur.id}" selected="${Cur.id == My.selected}">${Cur.name.formatted}</option>
+      </select>
+    </Template>
+    <JavaScript><![CDATA[
+        function os_PeopleSelector_onchange(select, varAttr, maxAttr, onSelectAttr) {
+          var selected;
+          if (!select.multiple) {
+            selected = select.options[select.selectedIndex].value;
+          } else {
+            selected = [];
+            for (var i = 0; i < select.options.length; i++) {
+              if (select.options[i].selected) {
+                selected.push(select.options[i].value);
+              }
+            }
+            try {
+              maxAttr = 1*maxAttr;
+            } catch (e) {
+              maxAttr = 0;
+            }
+            if (maxAttr && selected.length > maxAttr && select['x-selected']) {
+              selected = select['x-selected'];
+              for (var i = 0; i < select.options.length; i++) {
+                select.options[i].selected = false;
+                for (var j = 0; j < selected.length; j++) {
+                  if (select.options[i].value == selected[j]) {
+                    select.options[i].selected = true;
+                    break;
+                  }
+                }
+              }
+            }
+          }
+          select['x-selected'] = selected;
+          if (varAttr) {
+            if (opensocial.data) {
+              opensocial.data.getDataContext().putDataSet(varAttr, selected);
+            }
+          }
+
+          if (onSelectAttr) {
+            if (window[onSelectAttr] && typeof(window[onSelectAttr]) == 'function') {
+              window[onSelectAttr](selected);
+            } else {
+              if (!select['x-onselect-fn']) {
+                select['x-onselect-fn'] = new Function(onSelectAttr);
+              }
+              select['x-onselect-fn'].apply(select);
+            }
+          }
+        }
+    ]]></JavaScript>
+  </TemplateDef>
+</Templates>
diff --git a/trunk/config/container.js b/trunk/config/container.js
new file mode 100644
index 0000000..b691d87
--- /dev/null
+++ b/trunk/config/container.js
@@ -0,0 +1,344 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+// Default container configuration. To change the configuration, you have two options:
+//
+// A. If you run the Java server: Create your own "myContainer.js" file and
+// modify the value in web.xml.
+//
+//  B. If you run the PHP server: Create a myContainer.js, copy the contents of container.js to it,
+//  change
+//		{"gadgets.container" : ["default"],
+//  to
+//		{"gadgets.container" : ["myContainer"],
+// And make your changes that you need to myContainer.js.
+// Just make sure on the iframe URL you specify &container=myContainer
+// for it to use that config.
+//
+// All configurations will automatically inherit values from this
+// config, so you only need to provide configuration for items
+// that you require explicit special casing for.
+//
+// Please namespace your attributes using the same conventions
+// as you would for javascript objects, e.g. gadgets.features
+// rather than "features".
+
+// NOTE: Please _don't_ leave trailing commas because the php json parser
+// errors out on this.
+
+// Container must be an array; this allows multiple containers
+// to share configuration.
+
+// Note that you can embed values directly or you can choose to have values read from a file on disk
+// or read from the classpath ("foo-key" : "file:///foo-file.txt" || "foo-key" : "res://foo-file.txt")
+// TODO: Move out accel container config into a separate accel.js file.
+{"gadgets.container" : ["default", "accel"],
+
+// Set of regular expressions to validate the parent parameter. This is
+// necessary to support situations where you want a single container to support
+// multiple possible host names (such as for localized domains, such as
+// <language>.example.org. If left as null, the parent parameter will be
+// ignored; otherwise, any requests that do not include a parent
+// value matching this set will return a 404 error.
+"gadgets.parent" : null,
+
+// Origins for CORS requests and/or Referer validation
+// Indicate a set of origins or an entry with * to indicate that all origins are allowed
+"gadgets.parentOrigins" : ["*"],
+
+// Various urls generated throughout the code base.
+// iframeBaseUri will automatically have the host inserted
+// if locked domain is enabled and the implementation supports it.
+// query parameters will be added.
+"gadgets.iframeBaseUri" : "${CONTEXT_ROOT}/gadgets/ifr",
+"gadgets.uri.iframe.basePath" : "${CONTEXT_ROOT}/gadgets/ifr",
+
+// Callback URL.  Scheme relative URL for easy switch between https/http.
+"gadgets.uri.oauth.callbackTemplate" : "//%host%${CONTEXT_ROOT}/gadgets/oauthcallback",
+
+// Config param to load Opensocial data for social
+// preloads in data pipelining.  %host% will be
+// substituted with the current host.
+"gadgets.osDataUri" : "//%host%${CONTEXT_ROOT}/rpc",
+
+// Use an insecure security token by default
+"gadgets.securityTokenType" : "insecure",
+
+// Uncomment the securityTokenType and one of the securityTokenKey's to switch to a secure version.
+// Note that you can choose to use an embedded key, a filesystem reference or a classpath reference.
+// The best way to generate a key is to do something like this:
+// dd if=/dev/random bs=32 count=1 | openssl base64
+//
+//"gadgets.securityTokenType" : "secure",
+//"gadgets.securityTokenKey" : "default-insecure-embedded-key",
+//"gadgets.securityTokenKey" : "file:///path/to/key/file.txt",
+//"gadgets.securityTokenKey" : "res://some-file-on-the-classpath.txt",
+
+// OS 2.0 Gadget DOCTYPE: used in Gadgets with @specificationVersion 2.0 or greater and
+// quirksmode on Gadget has not been set.
+"gadgets.doctype_qname" : "HTML",  //HTML5 doctype
+"gadgets.doctype_pubid" : "",
+"gadgets.doctype_sysid" : "",
+
+// In a locked domain config, these can remain as-is in order to have requests encountered use the
+// host they came in on (locked host).
+"default.domain.locked.client" : "%host%",
+"default.domain.locked.server" : "%authority%",
+
+// IMPORTANT: EDITME: In a locked domain configuration, these should be changed to explicit values of
+// your unlocked host. You should not use %host% or %authority% replacements or these defaults in a
+// locked domain deployment.
+// Both of these values will likely be identical in a real locked domain deployment.
+"default.domain.unlocked.client" : "${Cur['default.domain.locked.client']}",
+"default.domain.unlocked.server" : "${Cur['default.domain.locked.server']}",
+
+// You can change this if you wish unlocked gadgets to render on a different domain from the default.
+"gadgets.uri.iframe.unlockedDomain" : "${Cur['default.domain.unlocked.server']}", // DNS domain on which *unlocked* gadgets should render.
+
+// IMPORTANT: EDITME: In a locked domain configuration, this suffix should be provided explicitly.
+// It is recommended that it be a separate top-level-domain (TLD) than the unlocked TLD.
+// You should not use replacement here (avoid %authority%)
+// Example: unlockedDomain="shindig.example.com" lockedDomainSuffix="-locked.example-gadgets.com"
+"gadgets.uri.iframe.lockedDomainSuffix" : "${Cur['default.domain.locked.server']}", // DNS domain on which *locked* gadgets should render.
+
+// Should all gadgets be forced on to a locked domain?
+"gadgets.uri.iframe.lockedDomainRequired" : false,
+
+// The permitted domain where the render request is sent from. For examle: ["www.hostA.com", "www.hostB.com"]
+// Empty means all domains are permitted.
+"shindig.locked-domain.permittedRefererDomains" : [],
+
+// Default Js Uri config: also must be overridden.
+// gadgets.uri.js.host should be protocol relative.
+"gadgets.uri.js.host" : "//${Cur['default.domain.unlocked.server']}", // Use unlocked host for better caching.
+
+// If you change the js.path you will need to define window.__CONTAINER_SCRIPT_ID prior to loading the <script>
+// tag for container JavaScript into the DOM.
+"gadgets.uri.js.path" : "${CONTEXT_ROOT}/gadgets/js",
+
+// Default concat Uri config; used for testing.
+"gadgets.uri.concat.host" : "${Cur['default.domain.unlocked.server']}", // Use unlocked host for better caching.
+"gadgets.uri.concat.path" : "${CONTEXT_ROOT}/gadgets/concat",
+"gadgets.uri.concat.js.splitToken" : "false",
+
+// Default proxy Uri config; used for testing.
+"gadgets.uri.proxy.host" : "${Cur['default.domain.unlocked.server']}", // Use unlocked host for better caching.
+"gadgets.uri.proxy.path" : "${CONTEXT_ROOT}/gadgets/proxy",
+
+// Enables/Disables feature administration
+"gadgets.admin.enableFeatureAdministration" : false,
+
+// Enables whitelist checks
+"gadgets.admin.enableGadgetWhitelist" : false,
+
+// Max post size for posts through the makeRequest proxy.
+"gadgets.jsonProxyUrl.maxPostSize" : 5242880, // 5 MiB
+
+// This config data will be passed down to javascript. Please
+// configure your object using the feature name rather than
+// the javascript name.
+
+// Only configuration for required features will be used.
+// See individual feature.xml files for configuration details.
+"gadgets.features" : {
+  "core.io" : {
+    // Note: ${Cur['gadgets.uri.proxy.path']} is an open proxy. Be careful how you expose this!
+    // Note: These urls should be protocol relative (start with //)
+    "proxyUrl" : "//${Cur['default.domain.unlocked.client']}${Cur['gadgets.uri.proxy.path']}%filename%?container=%container%&refresh=%refresh%&url=%url%%authz%%rewriteMime%",
+    "jsonProxyUrl" : "//${Cur['default.domain.locked.client']}${CONTEXT_ROOT}/gadgets/makeRequest",
+    // Note: this setting MUST be supplied in every container config object, as there is no default if it is not supplied.
+    "unparseableCruft" : "throw 1; < don't be evil' >",
+
+    // This variable is needed during the config init to parse config augmentation
+    "jsPath" : "${Cur['gadgets.uri.js.path']}",
+
+    // interval in milliseconds used to poll xhr request for the readyState
+    "xhrPollIntervalMs" : 50
+  },
+  "views" : {
+    "profile" : {
+      "isOnlyVisible" : false,
+      "urlTemplate" : "http://localhost${CONTEXT_ROOT}/gadgets/profile?{var}",
+      "aliases": ["DASHBOARD", "default"]
+    },
+    "canvas" : {
+      "isOnlyVisible" : true,
+      "urlTemplate" : "http://localhost${CONTEXT_ROOT}/gadgets/canvas?{var}",
+      "aliases" : ["FULL_PAGE"]
+    },
+    "default" : {
+      "isOnlyVisible" : false,
+      "urlTemplate" : "http://localhost${CONTEXT_ROOT}/gadgets/default?{var}",
+      "aliases" : ["home", "profile", "canvas"]
+    },
+    "embedded" : {
+      "isOnlyVisible" : false,
+      "urlTemplate" : "http://localhost${CONTEXT_ROOT}/gadgets/embedded?{var}",
+      "aliases" : ["embedded"]
+    }
+  },
+  "tabs": {
+    "css" : [
+      ".tablib_table {",
+      "width: 100%;",
+      "border-collapse: separate;",
+      "border-spacing: 0px;",
+      "empty-cells: show;",
+      "font-size: 11px;",
+      "text-align: center;",
+    "}",
+    ".tablib_emptyTab {",
+      "border-bottom: 1px solid #676767;",
+      "padding: 0px 1px;",
+    "}",
+    ".tablib_spacerTab {",
+      "border-bottom: 1px solid #676767;",
+      "padding: 0px 1px;",
+      "width: 1px;",
+    "}",
+    ".tablib_selected {",
+      "padding: 2px;",
+      "background-color: #ffffff;",
+      "border: 1px solid #676767;",
+      "border-bottom-width: 0px;",
+      "color: #3366cc;",
+      "font-weight: bold;",
+      "width: 80px;",
+      "cursor: default;",
+    "}",
+    ".tablib_unselected {",
+      "padding: 2px;",
+      "background-color: #dddddd;",
+      "border: 1px solid #aaaaaa;",
+      "border-bottom-color: #676767;",
+      "color: #000000;",
+      "width: 80px;",
+      "cursor: pointer;",
+    "}",
+    ".tablib_navContainer {",
+      "width: 10px;",
+      "vertical-align: middle;",
+    "}",
+    ".tablib_navContainer a:link, ",
+    ".tablib_navContainer a:visited, ",
+    ".tablib_navContainer a:hover {",
+      "color: #3366aa;",
+      "text-decoration: none;",
+    "}"
+    ]
+  },
+  "minimessage": {
+    "css": [
+      ".mmlib_table {",
+      "width: 100%;",
+      "font: bold 9px arial,sans-serif;",
+      "background-color: #fff4c2;",
+      "border-collapse: separate;",
+      "border-spacing: 0px;",
+      "padding: 1px 0px;",
+      "}",
+      ".mmlib_xlink {",
+        "font: normal 1.1em arial,sans-serif;",
+        "font-weight: bold;",
+        "color: #0000cc;",
+        "cursor: pointer;",
+      "}"
+    ]
+  },
+  "rpc" : {
+    // Path to the relay file. Automatically appended to the parent
+    // parameter if it passes input validation and is not null.
+    // This should never be on the same host in a production environment!
+    // Only use this for TESTING!
+    "parentRelayUrl" : "/container/rpc_relay.html",
+
+    // If true, this will use the legacy ifpc wire format when making rpc
+    // requests.
+    "useLegacyProtocol" : false,
+
+    // Path to the cross-domain enabling SWF for rpc's Flash transport.
+    "commSwf": "/xpc.swf",
+    "passReferrer": "c2p:query"
+  },
+  // Skin defaults
+  "skins" : {
+    "properties" : {
+      "BG_COLOR": "",
+      "BG_IMAGE": "",
+      "BG_POSITION": "",
+      "BG_REPEAT": "",
+      "FONT_COLOR": "",
+      "ANCHOR_COLOR": ""
+    }
+  },
+  "opensocial" : {
+    // Path to fetch opensocial data from
+    // Must be on the same domain as the gadget rendering server
+    "path" : "//%host%${CONTEXT_ROOT}/rpc",
+    // Path to issue invalidate calls
+    "invalidatePath" : "//%host%${CONTEXT_ROOT}/rpc",
+    "domain" : "shindig",
+    "enableCaja" : false,
+    "supportedFields" : {
+       "person" : ["id", {"name" : ["familyName", "givenName", "unstructured"]}, "thumbnailUrl", "profileUrl"],
+       "group" : ["id", "title", "description"],
+       "activity" : ["appId", "body", "bodyId", "externalId", "id", "mediaItems", "postedTime", "priority",
+                     "streamFaviconUrl", "streamSourceUrl", "streamTitle", "streamUrl", "templateParams", "title",
+                     "url", "userId"],
+       "activityEntry" : ["actor", "content", "generator", "icon", "id", "object", "published", "provider", "target",
+                          "title", "updated", "url", "verb", "openSocial", "extensions"],
+       "album" : ["id", "thumbnailUrl", "title", "description", "location", "ownerId"],
+       "mediaItem" : ["album_id", "created", "description", "duration", "file_size", "id", "language", "last_updated",
+                      "location", "mime_type", "num_comments", "num_views", "num_votes", "rating", "start_time",
+                      "tagged_people", "tags", "thumbnail_url", "title", "type", "url"]
+    }
+  },
+  "osapi.services" : {
+    // Specifying a binding to "container.listMethods" instructs osapi to dynamicaly introspect the services
+    // provided by the container and delay the gadget onLoad handler until that introspection is
+    // complete.
+    // Alternatively a container can directly configure services here rather than having them
+    // introspected. Simply list out the available servies and omit "container.listMethods" to
+    // avoid the initialization delay caused by gadgets.rpc
+    // E.g. "gadgets.rpc" : ["activities.requestCreate", "messages.requestSend", "requestShareApp", "requestPermission"]
+    "gadgets.rpc" : ["container.listMethods"]
+  },
+  "osapi" : {
+    // The endpoints to query for available JSONRPC/REST services
+    "endPoints" : [ "//%host%${CONTEXT_ROOT}/rpc" ]
+  },
+  "osml": {
+    // OSML library resource.  Can be set to null or the empty string to disable OSML
+    // for a container.
+    "library": "config/OSML_library.xml"
+  },
+  "shindig-container": {
+    "serverBase": "${CONTEXT_ROOT}/gadgets/"
+  },
+  "container" : {
+    "relayPath": "${CONTEXT_ROOT}/gadgets/files/container/rpc_relay.html",
+
+    //Enables/Disables the RPC arbitrator functionality in the common container
+    "enableRpcArbitration": false,
+
+    // This variable is needed during the container feature init.
+    "jsPath" : "${Cur['gadgets.uri.js.path']}"
+  }
+}
+}
diff --git a/trunk/config/gadget-admin.json b/trunk/config/gadget-admin.json
new file mode 100644
index 0000000..166b2a0
--- /dev/null
+++ b/trunk/config/gadget-admin.json
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+{
+  "default" : {
+    "gadgets" : {
+      "http://www.google.com/ig/modules/horoscope.xml" : {
+        "features" : {
+          "names" : ["views", "tabs", "setprefs", "dynamic-height"],
+          "type" : "blacklist"
+        }
+      },
+      "http://www.labpixies.com/campaigns/todo/todo.xml" : {
+        "features" : {
+          "names" : ["setprefs", "dynamic-height", "views"],
+          "type" : "whitelist"
+        }
+      },
+      "http://localhost:8080/gadgets/media-openGadgets/Media.xml" : {
+        "features" : {
+          "names" : [],
+          "type" : "blacklist"
+        }
+      },
+      "http://localhost:8080/*" : {
+        "features" : {
+          "names" : [],
+          "type" : "whitelist"
+        },
+        "rpc" : {
+          "additionalServiceIds" : []
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/trunk/config/oauth.json b/trunk/config/oauth.json
new file mode 100644
index 0000000..2889a9d
--- /dev/null
+++ b/trunk/config/oauth.json
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+{
+  "http://localhost:8080/gadgets/oauth.xml" : {
+    "" : {
+      "consumer_key" : "gadgetConsumer",
+      "consumer_secret" : "gadgetSecret",
+      "key_type" : "HMAC_SYMMETRIC"
+    }
+  },
+  "http://localhost:8080/gadgets/shindigoauth.xml" : {
+    "shindig" : {
+      "consumer_key" : "http://localhost:8080/gadgets/shindigoauth.xml",
+      "consumer_secret" : "secret",
+      "key_type" : "HMAC_SYMMETRIC"
+    }
+  }
+}
+  
+
diff --git a/trunk/config/oauth2.json b/trunk/config/oauth2.json
new file mode 100644
index 0000000..26db49d
--- /dev/null
+++ b/trunk/config/oauth2.json
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+ 
+/*******************************************************************************
+ * OAuth2Client persistence for the default OAuth2Persister                    *
+ *    org.apache.shindig.gadgets.oauth2.persistence.sample.JSONOAuth2Persister *
+ *                                                                             *
+ * Used in conjunction with the OAuth2 <ModulePrefs> described in:             *
+ *                                                                             *
+ *    http://code.google.com/p/opensocial-resources/issues/detail?id=1209      *
+ *                                                                             *
+ * to attain the information necessary to complete the OAuth 2.0 request       *
+ *                                                                             *
+ *                                                                             *
+ *******************************************************************************
+*/
+{
+   "gadgetBindings" : {
+      "%origin%%contextRoot%/gadgets/oauth2/oauth2_google.xml" : {
+         "googleAPI" : {
+            "clientName"          : "googleApi_client1",
+            "allowModuleOverride" : "true"
+         }
+      },
+      "%origin%%contextRoot%/gadgets/oauth2/oauth2_google_shared1.xml" : {
+         "googleAPI" : {
+            "clientName"          : "googleApi_shared_client",
+            "allowModuleOverride" : "true"
+         }
+      },
+      "%origin%%contextRoot%/gadgets/oauth2/oauth2_google_shared2.xml" : {
+         "googleAPI" : {
+            "clientName"          : "googleApi_shared_client",
+            "allowModuleOverride" : "true"
+         }
+      },
+      "%origin%%contextRoot%/gadgets/oauth2/oauth2_facebook.xml" : {
+          "facebook" : {
+             "clientName"          : "facebook_client1",
+             "allowModuleOverride" : "true"
+          }
+      },
+      "%origin%%contextRoot%/gadgets/oauth2/oauth2_windowslive.xml" : {
+          "windows_live" : {
+             "clientName"          : "wl_client1",
+             "allowModuleOverride" : "true"
+          }
+      },
+      "%origin%%contextRoot%/gadgets/oauth2/shindig_authorization.xml" : {
+		  "shindigOAuth2Provider" : {
+            "clientName"          : "shindig_client1",
+            "allowModuleOverride" : "true"
+          }
+      },
+      "%origin%%contextRoot%/gadgets/oauth2/shindig_client_credentials.xml" : {
+        "shindigOAuth2Provider" : {
+            "clientName"          : "shindig_client2",
+            "allowModuleOverride" : "true"
+          }
+      },
+      "%origin%%contextRoot%/gadgets/oauth2/oauth2_spring_proxy.xml" : {
+         "springAPI" : {
+            "clientName"          : "spring_client1",
+            "allowModuleOverride" : "true"
+         }
+      }
+   },
+   "clients" : {
+      "googleApi_client1" : {
+         "providerName"  : "googleAPI",
+         "redirect_uri"  : "%origin%%contextRoot%/gadgets/oauth2callback",
+         "type"          : "confidential",
+         "grant_type"    : "code",
+         "client_id"     : "YOUR_GOOGLE_APP_ID",
+         "client_secret" : "YOUR_GOOGLE_APP_SECRET",
+         "sharedToken"   : "false"
+      },
+      "googleApi_shared_client" : {
+         "providerName"  : "googleAPI",
+         "redirect_uri"  : "%origin%%contextRoot%/gadgets/oauth2callback",
+         "type"          : "confidential",
+         "grant_type"    : "code",
+         "client_id"     : "YOUR_GOOGLE_APP_ID",
+         "client_secret" : "YOUR_GOOGLE_APP_SECRET",
+         "sharedToken"   : "true"
+      },
+      "facebook_client1" : {
+         "providerName"  : "facebook",
+         "redirect_uri"  : "%origin%%contextRoot%/gadgets/oauth2callback",
+         "type"          : "confidential",
+         "grant_type"    : "code",
+         "client_id"     : "YOUR_FACEBOOK_APP_ID",
+         "client_secret" : "YOUR_FACEBOOK_APP_SECRET"
+      },
+      "wl_client1" : {
+         "providerName"  : "wlProvider",
+         "type"          : "confidential",
+         "redirect_uri"  : "%origin%%contextRoot%/gadgets/oauth2callback",
+         "grant_type"    : "code",
+         "client_id"     : "YOUR_WINDOWS_LIVE_APP_ID",
+         "client_secret" : "YOUR_WINDOWS_LIVE_APP_SECRET"
+      } ,
+      "shindig_client1" : {
+         "providerName"  : "shindigOAuth2Provider",
+         "type"          : "confidential",
+         "grant_type"    : "code",
+         "client_id"     : "shindigClient",
+         "client_secret" : "U78KJM98372AMGL87612993M"
+      } ,
+      "shindig_client2" : {
+         "providerName"  : "shindigOAuth2Provider",
+         "type"          : "confidential",
+         "grant_type"    : "code",
+         "client_id"     : "testClientCredentialsClient",
+         "client_secret" : "clientCredentialsClient_secret"
+      },
+      "spring_client1" : {
+         "providerName"  : "springProvider",
+         "redirect_uri"  : "%origin%%contextRoot%/gadgets/oauth2callback",
+         "type"          : "confidential",
+         "grant_type"    : "code",
+         "client_id"     : "tonr",
+         "client_secret" : "secret",
+         "sharedToken"   : "false"
+      }
+   },
+   "providers" : {
+      "googleAPI" : {
+        "client_authentication" : "STANDARD",
+        "usesAuthorizationHeader" : "false",
+        "usesUrlParameter" : "true",
+        "endpoints" : {
+            "authorizationUrl"  : "https://accounts.google.com/o/oauth2/auth",
+            "tokenUrl"          : "https://accounts.google.com/o/oauth2/token"
+        }
+      },
+      "facebook" : {
+         "client_authentication" : "STANDARD",
+        "usesAuthorizationHeader" : "false",
+        "usesUrlParameter" : "true",
+        "endpoints" : {
+            "authorizationUrl"   : "https://www.facebook.com/dialog/oauth",
+            "tokenUrl"           : "https://graph.facebook.com/oauth/access_token"
+        }
+      },
+      "wlProvider" : {
+         "client_authentication" : "STANDARD",
+         "usesAuthorizationHeader" : "false",
+         "usesUrlParameter" : "true",
+         "endpoints" : {
+            "authorizationUrl"   : "https://oauth.live.com/authorize/",
+            "tokenUrl"           : "https://oauth.live.com/token"
+         }
+      },
+      "shindigOAuth2Provider" : {
+         "client_authentication" : "Basic",
+         "usesAuthorizationHeader" : "true",
+         "usesUrlParameter" : "false",
+         "endpoints" : {
+            "authorizationUrl"   : "%origin%%contextRoot%/oauth2/authorize/",
+            "tokenUrl"           : "%origin%%contextRoot%/oauth2/token"
+         }
+      },
+      "springProvider" : {
+         "client_authentication" : "Basic",
+         "usesAuthorizationHeader" : "true",
+         "usesUrlParameter" : "false",
+         "endpoints" : {
+            "authorizationUrl"   : "%origin%/sparklr2/oauth/authorize",
+            "tokenUrl"           : "%origin%/sparklr2/oauth/token"
+         }
+      }
+   }
+}
diff --git a/trunk/content/README b/trunk/content/README
new file mode 100644
index 0000000..cb1e694
--- /dev/null
+++ b/trunk/content/README
@@ -0,0 +1,80 @@
+                          Apache Shindig Javascript
+
+  What is it?
+  -----------
+
+  Shindig is a JavaScript container and implementations of the backend APIs
+  and proxy required for hosting OpenSocial applications.
+
+  This is the Javascript component of Shindig.
+
+  Documentation
+  -------------
+
+  The most up-to-date documentation can be found at http://shindig.apache.org/
+  
+  Using Shindig Gadget Container JavaScript
+  -----------------------------------------
+
+  1) Try out the samples.
+     A) Set up your own Shindig Gadget Server. See java/README for details.
+
+     B) Assuming your server is running on http://yourserver:yourport/gadgets/...
+        you can hit these html files in your favorite browser to see your local
+        Shindig in action:
+
+        (Note: yourserver:yourport defaults to localhost:8080 for the java server,
+        and just localhost for the php server)
+
+        * http://yourserver:yourport/containers/commoncontainer/index.html - basic common container example
+        * http://yourserver:yourport/containers/embeddedexperiences/index.html - sample container demonstrating embedded experiences
+        * http://yourserver:yourport/containers/conservcontainer/index.html - sample container demonstrating actions and selection
+        
+        These samples are very basic and aren't production-ready.
+        
+     C) There are serveral deprecated sample containers that use older container code.
+      
+        * http://yourserver:yourport/container/sample1.html - basic container
+        * http://yourserver:yourport/container/sample2.html - custom rendering
+        * http://yourserver:yourport/container/sample3.html - custom layouts
+        * http://yourserver:yourport/container/sample4.html - set pref
+        * http://yourserver:yourport/container/sample5.html - set pref
+        * http://yourserver:yourport/container/sample6.html - dynamic height
+        * http://yourserver:yourport/container/sample7.html - set title
+
+        
+
+  2) Play around with the code.
+
+     A) Create an HTML file including the following <head> boilerplate:
+        <script type="text/javascript" src="/gadgets/js/container:rpc:xmlutil.js?c=1&debug=1&container=default"></script>
+
+     B) Initialize the common container.
+     
+        var config = {};
+        config[osapi.container.ContainerConfig.RENDER_DEBUG] = '1';
+        var CommonContainer = new osapi.container.Container(config);
+
+     C) Render a gadget.
+       
+        var el = document.getElementById('divId');
+        var params = {};
+        params[osapi.container.RenderParam.WIDTH] = '100%';
+	    var gadgetSite = CommonContainer.newGadgetSite(el);
+	    CommonContainer.navigateGadget(gadgetSite, 'http://examples.com/gadget.xml', {}, params);
+
+  Licensing
+  ---------
+
+  Please see the file called LICENSE.
+
+
+  Shindig URLS
+  ------------
+
+  Home Page:          http://shindig.apache.org/
+  Downloads:          http://shindig.apache.org/download/index.html
+  Mailing Lists:      http://shindig.apache.org/mail-lists.html
+  Source Code:        http://svn.apache.org/repos/asf/shindig
+  Issue Tracking:     https://issues.apache.org/jira/browse/SHINDIG
+  Wiki:               http://cwiki.apache.org/confluence/display/SHINDIG/
diff --git a/trunk/content/containers/commoncontainer/assembler.js b/trunk/content/containers/commoncontainer/assembler.js
new file mode 100644
index 0000000..1c191b3
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/assembler.js
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+*/
+
+// url base should be <host>:<port>//<contextRoot>
+var urlBase = location.href.substr(0, location.href.indexOf('/containers/commoncontainer/'));
+var contextRoot = urlBase.substr(urlBase.indexOf(location.host) + location.host.length);
+
+var testConfig = testConfig || {};
+testConfig[osapi.container.ServiceConfig.API_PATH] = contextRoot + '/rpc';
+testConfig[osapi.container.ContainerConfig.RENDER_DEBUG] = '1';
+
+// Default the security token for the container. Using this example security token requires enabling
+// the DefaultSecurityTokenCodec to let UrlParameterAuthenticationHandler create valid security token.
+// 10 seconds is fast, but this is mostly for demonstration purposes.
+testConfig[osapi.container.ContainerConfig.GET_CONTAINER_TOKEN] = function(callback) {
+  gadgets.log('Updating container security token.');
+  callback('john.doe:john.doe:appid:cont:url:0:default', 10);
+};
+
+//  Create the new CommonContainer
+var CommonContainer = new osapi.container.Container(testConfig);
+
+//Gadget site to title id map
+var siteToTitleMap = {};
+
+// Need to pull these from values supplied in the dialog
+CommonContainer.init = function() {
+
+  //Create my new managed hub
+  CommonContainer.managedHub = new OpenAjax.hub.ManagedHub({
+    onSubscribe: function(topic, container) {
+      log(container.getClientID() + " subscribes to this topic '" + topic + "'");
+      return true;// return false to reject the request.
+    },
+    onUnsubscribe: function(topic, container) {
+      log(container.getClientID() + " unsubscribes from tthis topic '" + topic + "'");
+      return true;
+    },
+    onPublish: function(topic, data, pcont, scont) {
+      log(pcont.getClientID() + " publishes '" + data + "' to topic '" + topic + "' subscribed by " + scont.getClientID());
+      return true;
+      // return false to reject the request.
+    }
+  });
+  //  initialize managed hub for the Container
+  gadgets.pubsub2router.init({
+    hub: CommonContainer.managedHub
+  });
+
+  CommonContainer.rpcRegister('set_title', window.setTitleHandler);
+  CommonContainer.addGadgetLifecycleCallback('com.example.commoncontainer', lifecycle());
+
+  try {
+
+    // Connect to the ManagedHub
+    CommonContainer.inlineClient =
+      new OpenAjax.hub.InlineContainer(CommonContainer.managedHub, 'container',
+    {
+      Container: {
+        onSecurityAlert: function(source, alertType) { /* Handle client-side security alerts */ },
+        onConnect: function(container) { /* Called when client connects */ },
+        onDisconnect: function(container) { /* Called when client connects */ }
+      }
+    });
+    //connect to the inline client
+    CommonContainer.inlineClient.connect();
+
+  } catch (e) {
+    // TODO: error handling should be consistent with other OS gadget initialization error handling
+    alert('ERROR creating or connecting InlineClient in CommonContainer.managedHub [' + e.message + ']');
+  }
+};
+
+//Wrapper function to set the gadget site/id and default width.  Currently have some inconsistency with width actually being set. This
+//seems to be related to the pubsub2 feature.
+CommonContainer.renderGadget = function(gadgetURL, gadgetId) {
+	//going to hardcode these values for width.
+    var el = document.getElementById('gadget-site-' + gadgetId);
+    var params = {};
+    params[osapi.container.RenderParam.WIDTH] = '100%';
+	var gadgetSite = CommonContainer.newGadgetSite(el);
+	CommonContainer.navigateGadget(gadgetSite, gadgetURL, {}, params);
+	return gadgetSite;
+
+};
+
+//TODO:  To be implemented. Identify where to hook this into the page (in the gadget title bar/gadget management, etc)
+CommonContainer.navigateView = function(gadgetSite, gadgetURL, view) {
+	var renderParms = {};
+	if (view === null || view === '') {
+		view = 'default';
+	}
+	//TODO Evaluate Parms based on configuration
+    renderParms[osapi.container.RenderParam.WIDTH] = '100%';
+    renderParms['view'] = view;
+
+    CommonContainer.navigateGadget(gadgetSite, gadgetURL, {}, renderParms);
+};
+
+//TODO:  Add in UI controls in portlet header to remove gadget from the canvas
+CommonContainer.collapseGadget = function(gadgetSite) {
+	CommonContainer.closeGadget(gadgetSite);
+};
+
+//display the pubsub 2 event details
+function log(message) {
+  document.getElementById('output').innerHTML = gadgets.util.escapeString(message) + '<br/>' + document.getElementById('output').innerHTML;
+}
+
+var lifecycle = function() {
+  var preloadStart;
+  var navigateStart;
+  var closeStart;
+  var unloadStart;
+  var renderStart;
+  var listeners = {};
+  listeners[osapi.container.CallbackType.ON_BEFORE_PRELOAD] = function(gadgetUrls) {
+    preloadStart = osapi.container.util.getCurrentTimeMs();
+  };
+  listeners[osapi.container.CallbackType.ON_PRELOADED] = function(response) {
+    var urls = [];
+    for(url in response) {
+      urls[urls.length] = url;
+    }
+    var dif = osapi.container.util.getCurrentTimeMs() - preloadStart;
+    log('It took ' + dif + 'ms to preload the URL(s) ' + urls + '.');
+  };
+  listeners[osapi.container.CallbackType.ON_BEFORE_NAVIGATE] = function(gadgetUrl) {
+    navigateStart = osapi.container.util.getCurrentTimeMs();
+  };
+  listeners[osapi.container.CallbackType.ON_NAVIGATED] = function(site) {
+   log('It took ' + (osapi.container.util.getCurrentTimeMs() - navigateStart) + ' ms' +
+           ' for the site ' + site.getId() + ' to navigate.');
+  };
+  listeners[osapi.container.CallbackType.ON_BEFORE_CLOSE] = function(site) {
+    closeStart = osapi.container.util.getCurrentTimeMs();
+  };
+  listeners[osapi.container.CallbackType.ON_CLOSED] = function(site) {
+    log('It took ' + (osapi.container.util.getCurrentTimeMs() - closeStart) +
+            ' ms to close the gadget in the site with id ' + site.getId());
+  };
+  listeners[osapi.container.CallbackType.ON_BEFORE_RENDER] = function(gadgetUrl) {
+    renderStart = osapi.container.util.getCurrentTimeMs();
+  };
+  listeners[osapi.container.CallbackType.ON_RENDER] = function(gadgetUrl) {
+    log('It took ' + (osapi.container.util.getCurrentTimeMs() - renderStart) +
+            ' ms to render the gadget at the URL ' + gadgetUrl);
+  };
+  return listeners;
+}
+
diff --git a/trunk/content/containers/commoncontainer/cconviews.js b/trunk/content/containers/commoncontainer/cconviews.js
new file mode 100644
index 0000000..a1af386
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/cconviews.js
@@ -0,0 +1,381 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * overview Container implementation of view enhancement.
+ */
+
+CommonContainer['views'] = CommonContainer['views'] || {};
+
+/**
+ * Method will be called to create the DOM element to place the Gadget
+ * Site in.
+ *
+ * @param {Object}
+ *          metadata: Gadget meta data for the gadget being opened in
+ *          this GadgetSite.
+ * @param {Element}
+ *          rel: The element to which opt_coordinates values are
+ *          relative.
+ * @param {string=}
+ *          opt_view: Optional parameter, the view that indicates the
+ *          type of GadgetSite.
+ * @param {string=}
+ *          opt_viewTarget: Optional parameter, the view target indicates
+ *          where to open the gadget.
+ * @param {Object=}
+ *          opt_coordinates: Object containing the desired absolute
+ *          positioning css parameters (top|bottom|left|right) with
+ *          appropriate values. All values are relative to the calling
+ *          gadget.
+ * @return {Object} The DOM element to place the GadgetSite in.
+ */
+CommonContainer.views.createElementForGadget = function(metadata, rel, opt_view, opt_viewTarget,
+        opt_coordinates) {
+
+  var surfaceView = 'default';
+  var viewTarget = 'default';
+
+  if (typeof opt_view != 'undefined') {
+    surfaceView = opt_view;
+  }
+  if (typeof opt_viewTarget != 'undefined') {
+    viewTarget = opt_viewTarget;
+  }
+
+  switch (viewTarget) {
+    case 'tab':
+      return openInNewTab(metadata);
+      break;
+    case 'dialog':
+      return openInDialog(false, surfaceView, true, metadata);
+      break;
+    case 'modalDialog':
+      return openInDialog(true, surfaceView, true, metadata);
+      break;
+    default:
+      return openInDialog(false, surfaceView, true, metadata);
+  }
+};
+
+
+
+/**
+ * Method will be called to create the DOM element to place the UrlSite
+ * in.
+ *
+ * @param {Element}
+ *          rel: The element to which opt_coordinates values are
+ *          relative.
+ * @param {string=}
+ *          opt_view: Optional parameter, the view to open. If not
+ *          included the container should use its default view.
+ * @param {Object=}
+ *          opt_coordinates: Object containing the desired absolute
+ *          positioning css parameters (top|bottom|left|right) with
+ *          appropriate values. All values are relative to the calling
+ *          gadget.
+ * @return {Object} The DOM element to place the UrlSite object in.
+ */
+CommonContainer.views.createElementForUrl = function(rel, opt_viewTarget, opt_coordinates) {
+  var viewTarget = 'dialog';
+
+  if (typeof opt_viewTarget != 'undefined') {
+    viewTarget = opt_viewTarget;
+  }
+
+  switch (viewTarget) {
+    case 'tab':
+      return openInNewTab();
+      break;
+    case 'dialog':
+      return openInDialog(false, 'canvas', false);
+      break;
+    case 'modalDialog':
+      return openInDialog(true, 'canvas', false);
+      break;
+    default:
+      return openInDialog(false, 'canvas', false);
+  }
+};
+
+
+/**
+ * Method will be called when a gadget wants to close itself or the parent
+ * gadget wants to close a gadget or url site it has opened.
+ *
+ * @param {object=} site
+ *          The id of the site to close.
+ */
+CommonContainer.views.destroyElement = function(site) {
+  closeDialog(site);
+};
+
+var dialog_counter = 0;
+var tab_counter = 2;
+var newTabId;
+var $tabs;
+
+
+/**
+ * private method will be called to create the dialog DOM element.
+ * @private
+ * @param {boolean} modaldialog
+ *          true for modal dialog.
+ * @param {string} view
+ *          view type.
+ * @param {boolean} isGadget
+ *          true for gadget, false for url.
+ * @param {string} opt_gadgetMetadata
+ *          gadget metadate.
+ * @return {Object} The DOM element to place the gadget or url site object in.
+ */
+function openInDialog(modaldialog, view, isGadget, opt_gadgetMetadata) {
+
+  var dialog_width = 450; // default width
+  if (view == 'canvas') {
+    dialog_width = 675; // width for canvas
+  }
+
+  dialog_counter++;
+  var dialog = document.createElement('div');
+  dialog.id = 'dialog_' + dialog_counter;
+
+  if (typeof opt_gadgetMetadata != 'undefined') {
+    // open gadget, get the title from gadgetMetadata
+    dialog.title = opt_gadgetMetadata['modulePrefs'].title;
+  }
+
+  document.getElementById('content').appendChild(dialog);
+  if (isGadget) {
+    var id = 'dialog_' + dialog_counter;
+    // use jquery to create the dialog
+    $('#' + id).dialog({
+      resizable: false,
+      width: dialog_width, // height will be auto
+      modal: modaldialog, // set modal: true or false
+      beforeClose: function(ev, ui) {
+        var dialogDiv = $('#' + id + ' iframe');
+        if(dialogDiv.length) {
+          //Means the user most likely clicked the 'x' in the dialog chrome
+          //If they clicked the OK or Cancel buttons in the dialog than the gadget
+          //iFrame would have already been removed from the DOM
+          var site = CommonContainer.getGadgetSiteByIframeId_(dialogDiv[0].id);
+          CommonContainer.closeGadget(site);
+        }
+      }
+    });
+    return dialog;
+  } else {
+    var dialog_height = 350; // default height
+    if (view == 'canvas') {
+      dialog_height = 530; // height for canvas
+    }
+    $('#dialog_' + dialog_counter).dialog({
+      resizable: false,
+      width: dialog_width,
+      height: dialog_height,
+      modal: modaldialog, // set modal: true or false
+      close: function(ev, ui) {
+        $(this).remove();
+      }
+    });
+    // Maybe an issue in jquery, we need to create another div, otherwise it
+    // will show vertical scroll bar
+    var dialog_content = document.createElement('div');
+    dialog_content.style.height = '100%';
+    dialog_content.style.width = '100%';
+    dialog.appendChild(dialog_content);
+    return dialog_content;
+  }
+}
+
+
+/**
+ * private method will be called to create the tab DOM element.
+ * @private
+ * @param {string} gadgetMetadata
+ *          gadget metadate.
+ * @return {Object} The DOM element to place the gadget or url site object in.
+ */
+function openInNewTab(gadgetMetadata) {
+
+  var tabsNode;
+
+  if (!document.getElementById('tabs')) {
+    // add the new tab the first time, will create the default tab for the
+    // current content, and add a new tab
+    var controlPanelNode = document.getElementById('controlPanel');
+    var testAreaNode = document.getElementById('testArea');
+    var contentNode = document.getElementById('content');
+
+    contentNode.removeChild(controlPanelNode);
+    contentNode.removeChild(testAreaNode);
+
+    tabsNode = document.createElement('div');
+    tabsNode.id = 'tabs';
+
+    tabsNode.innerHTML = "<ul><li><a href='#tabs-1'>Default</a></li></ul>";
+
+    var tabs_1_Node = document.createElement('div');
+    tabs_1_Node.id = 'tabs-1';
+
+    // put the default content into the tabs-1
+    tabs_1_Node.appendChild(controlPanelNode);
+    tabs_1_Node.appendChild(testAreaNode);
+
+    tabsNode.appendChild(tabs_1_Node);
+    contentNode.appendChild(tabsNode);
+    newTabId = 'tabs-2';
+
+    // use jquery to create new tab
+    $tabs = $('#tabs')
+    .tabs(
+            {
+              tabTemplate: "<li><a href='#{href}'>#{label}</a>" +
+              "<span class='ui-icon ui-icon-close'>Remove Tab</span></li>",
+              add: function(event, ui) {
+                var tab_content_id = 'tab_content' + tab_counter;
+                var tab_content = document.createElement('div');
+                tab_content.id = tab_content_id;
+                // tab_content.className = "column";
+
+                tab_content.style.height = '1000px';
+                tab_content.style.width = '100%';
+
+                $(ui.panel).append(tab_content);
+
+                // set the focus to the new tab
+                $('#tabs').tabs('select', '#' + newTabId);
+              },
+
+          remove: function(event, ui) {
+            // If there is gadget inside, close the gadget
+            var iframes = $(this).parent().get(0)
+            .getElementsByTagName('iframe');
+            if (iframes.lenth > 0 && (typeof iframes[0].id != 'undefined')) {
+              var site = CommonContainer
+              .getGadgetSiteByIframeId_(iframes[0].id);
+              CommonContainer.closeGadget(site);
+            }
+          }
+            });
+
+    // close icon: removing the tab on click
+    // note: closable tabs gonna be an option in the future - see
+    // http://dev.jqueryui.com/ticket/3924
+    $('#tabs span.ui-icon-close').live('click', function() {
+      var index = $('li', $tabs).index($(this).parent());
+
+      $tabs.tabs('remove', index);
+      if ($tabs.tabs('length') < 2) {
+
+        controlPanelNode = document.getElementById('controlPanel');
+        testAreaNode = document.getElementById('testArea');
+        contentNode = document.getElementById('content');
+        tabsNode = document.getElementById('tabs');
+
+        tabs_1_Node = document.getElementById('tabs-1');
+
+        tabs_1_Node.removeChild(controlPanelNode);
+        tabs_1_Node.removeChild(testAreaNode);
+
+        contentNode.removeChild(tabsNode);
+
+        tab_counter = 2;
+        newTabId = null;
+        $tabs = null;
+
+        contentNode.appendChild(controlPanelNode);
+        contentNode.appendChild(testAreaNode);
+
+      }
+
+    });
+  }
+
+  newTabId = 'tabs-' + tab_counter;
+  var tab_content_id = 'tab_content' + tab_counter;
+
+  // add new tab with new id and new title
+  var tab_title = 'new tab ';
+  if ((typeof gadgetMetadata != 'undefined') &&
+          (typeof gadgetMetadata['modulePrefs'].title != 'undefined')) {
+    // open gadget, get the title from gadgetMetadata
+    tab_title = gadgetMetadata['modulePrefs'].title;
+    if (tab_title.length > 7) {
+      tab_title = tab_title.substring(0, 6) + '...';
+    }
+  }
+  $tabs.tabs('add', '#tabs-' + tab_counter, tab_title);
+
+  if (typeof gadgetMetadata != 'undefined') {
+    // rendering gadget's header
+    var gadgetSiteId = 'gadget-site-' + newTabId;
+    var gadgetTemplate = '<div class="portlet">' +
+        '<div class="portlet-header">sample to replace</div>' +
+        '<div id=' + gadgetSiteId +
+        ' class="portlet-content"></div>' + '</div>';
+
+    $(gadgetTemplate).appendTo($('#' + tab_content_id)).addClass(
+        'ui-widget ui-widget-content ui-helper-clearfix ui-corner-all')
+    .find('.portlet-header')
+    .addClass('ui-widget-header ui-corner-all')
+    .text(gadgetMetadata['modulePrefs'].title)
+    .append('<span id="remove" class="ui-icon ui-icon-closethick"></span>');
+  }
+
+  tab_counter++;
+
+  // return the div
+  if (typeof gadgetMetadata != 'undefined') {
+    return document.getElementById('gadget-site-' + newTabId);
+  } else {
+    return document.getElementById(tab_content_id);
+  }
+}
+
+
+/**
+ * private method will be called to destroy dialog object.
+ * @private
+ * @param {object} site
+ *          gadget site.
+ */
+function closeDialog(site) {
+  // this is the site id, we also need to find the dojo dialog widget id
+  var iframeId;
+  var widgetId = 'dialog_' + dialog_counter; //default
+
+  if (site && site.getActiveSiteHolder()) {
+    // get iframe id
+    iframeId = site.getActiveSiteHolder().getIframeId();
+    if (typeof iframeId != 'undefined') {
+      var iframeNode = document.getElementById(iframeId);
+      // get dialog widget id
+      widgetId = iframeNode.parentNode.id;
+    }
+  }
+  // close the gadget
+  CommonContainer.closeGadget(site);
+
+  // close the widget
+  $('#' + widgetId).dialog('close');
+
+}
diff --git a/trunk/content/containers/commoncontainer/gadgetCollections.json b/trunk/content/containers/commoncontainer/gadgetCollections.json
new file mode 100644
index 0000000..6a6b866
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/gadgetCollections.json
@@ -0,0 +1,99 @@
+{ "collections":
+        [
+            {
+            "name": "Publish Subscribe Demo",
+            "Description": "This is a sample pub/sub demo showcasing the pubsub-2 feature",
+            "apps" : [
+                     {"name": "publisher", "url": "/gadgets/sample-pubsub-2-publisher.xml"},
+                     {"name": "subscriber", "url": "/gadgets/sample-pubsub-2-subscriber.xml"}
+                     ]
+             },
+             {
+            "name": "Activity Streams Sample",
+            "Description": "Simple gadget to test base ActivityStreams implementation in features",
+            "apps" : [
+                     {"name": "ActivityStreams Sample", "url": "/gadgets/ActivityStreams/ActivityStreamGadget.xml"}
+                     ]
+             },
+                         {
+            "name": "Sample Media Items Gadget",
+            "Description": "This is a sample pub/sub demo showcasing the pubsub-2 feature",
+            "apps" : [
+                     {"name": "publisher", "url": "/gadgets/media/Media.xml"}
+                     ]
+             },
+             {
+             "name": "ToDo & Horoscope",
+             "Description": "Sample gadgets used in the simple sample (sample1.html)",
+             "apps" : [
+                      {"name": "Horoscope", "url": "http://www.google.com/ig/modules/horoscope.xml"},
+                      {"name": "TODO", "url": "http://www.labpixies.com/campaigns/todo/todo.xml"}
+                      ]
+             },
+             {
+             "name": "Sample Media Items Gadget with openGadget API",
+             "Description": "Sample gadget used in the view enhancements sample",
+             "apps" : [
+                     {"name": "publisher", "url": "/gadgets/media-openGadgets/Media.xml"}
+                     ]
+             },
+             {
+             "name": "OpenSearch Demo",
+             "Description": "Sample gadgets used for opensearch",
+             "apps" : [
+                      {"name": "Twitter", "url": "http://hosting.gmodules.com/ig/gadgets/file/109228598702359180066/twitterfinal.xml"},
+                      {"name": "MySpace", "url": "http://hosting.gmodules.com/ig/gadgets/file/109228598702359180066/myspacefinal.xml"}
+                      ]
+             },
+             {
+             "name": "OAuth2 Demo with Google Provider",
+             "Description": "OAuth2 Demo with Google Provider",
+             "apps" : [
+                      {"name": "oauth2_authorization_g", "url": "/gadgets/oauth2/oauth2_google.xml"}
+                      ]
+             },
+             {
+             "name": "OAuth2 Demo with Facebook Provider",
+             "Description": "OAuth2 Demo with Facebook Provider",
+             "apps" : [
+                      {"name": "oauth2_authorization_f", "url": "/gadgets/oauth2/oauth2_facebook.xml"}
+                      ]
+             },
+             {
+             "name": "OAuth2 Demo with Windows Live Provider",
+             "Description": "OAuth2 Demo with Windows Live Provider",
+             "apps" : [
+                      {"name": "oauth2_authorization_w", "url": "/gadgets/oauth2/oauth2_windowslive.xml"}
+                      ]
+             },
+             {
+             "name": "OAuth2 Demo with Shindig Provider (Authorization Code)",
+             "Description": "OAuth2 Demo with Shindig Provider (Authorization Code)",
+             "apps" : [
+                      {"name": "oauth2_authorization_s", "url": "/gadgets/oauth2/shindig_authorization.xml"}
+                      ]
+             },
+             {
+             "name": "OAuth2 Demo with Shindig Provider (Client Credentials)",
+             "Description": "OAuth2 Demo with Shindig Provider (Client Credentials)",
+             "apps" : [
+                      {"name": "oauth2_client_credentials_s", "url": "/gadgets/oauth2/shindig_client_credentials.xml"}
+                      ]
+             },
+             {
+             "name" : "Test Gadget and Container Domain Configuration",
+             "Description" : "Tests Gadget and Container domain configuration by trying to access container page information from within a gadget",
+             "apps" : [ {"name" : "Domain Test", "url" : "/gadgets/ContainerGadgetDomainTest.xml"} ]
+             },
+             {
+             "name" : "Sample gadget for context proxy with OAuth2",
+             "Description" : "Sample gadget that demonstrate use of context proxy to retrieve resources protected by OAuth2",
+             "apps" : [ {"name" : "oauth2_context_proxy", "url" : "/gadgets/oauth2/oauth2_spring_proxy.xml"} ]
+             },
+             {
+             "name" : "Sample gadget for leveraging EL within gadget ",
+             "Description" : "Sample gadget demonstrate how to use EL for variable substitution in gadget content ",
+             "apps" : [ {"name" : "el_variable_sub", "url" : "/gadgets/ELInGadgetDemo.xml"} ]
+             }
+         ]
+}
\ No newline at end of file
diff --git a/trunk/content/containers/commoncontainer/index.html b/trunk/content/containers/commoncontainer/index.html
new file mode 100644
index 0000000..7330619
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/index.html
@@ -0,0 +1,236 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+<head>
+  <title>OpenSocial Common Container Test Environment</title>
+  <!-- My OpenSocial Beginnings -->
+  <link rel="stylesheet"
+        href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/cupertino/jquery-ui.css"
+        type="text/css" media="all"/>
+
+  <script type="text/javascript" src="../../../gadgets/js/container:open-views:opensearch:rpc:xmlutil:pubsub-2.js?c=1&debug=1&container=default"></script>
+  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.3/jquery.min.js"></script>
+  <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.5/jquery-ui.min.js"></script>
+  <script type="text/javascript" src="./assembler.js"></script>
+  <script type="text/javascript" src="./viewController.js"></script>
+  <script type="text/javascript" src="./layout.js"></script>
+  <script type="text/javascript" src="./cconviews.js"></script>
+  <script type="text/javascript" src="./search.js"></script>
+
+  <style type="text/css">
+    .portlet-header .ui-icon { float: right; }
+    #content {
+    display: table;
+    width: 100%;
+    }
+
+    #testArea {
+    display: table-cell;
+    width: 75%;
+    padding-left:22px;
+    }
+
+    #controlPanel {
+    display: table-cell;
+    width: 25%;
+    }
+
+    #viewsDropdown, #viewsDropdown ul {
+    list-style: none;
+    }
+    #viewsDropdown, #viewsdropdown * {
+    padding: 0; margin: 0;
+    }
+
+    #viewsDropdown li.li-header {
+    float: right; margin-left: -1px;
+    }
+    #viewsDropdown li.li-header a {
+    display: block;
+    }
+
+    #viewsDropdown li.li-header ul {
+    display: none; border: 1px black solid; text-align: left;
+    }
+
+    #viewsDropdown li.li-header:hover ul {
+    display: block;
+    }
+
+    #viewsDropdown li.li-header ul li a {
+    padding: 5px; height: 17px;
+    }
+
+    #tabs li .ui-icon-close { float: left; margin: 0.4em 0.2em 0 0; cursor: pointer; }
+
+    .resultTitle {
+    color: blue;
+    }
+
+    .searchEngine {
+    border: 1px solid blue;
+    }
+
+    .widerInput {
+      width: 270px;
+    }
+
+  </style>
+</head>
+
+<body onload="CommonContainer.init();">
+
+  <!--  Need to add in an improved header with links to specification -->
+  <a href="http://opensocial.org/">
+    <img alt="OpenSocial" src="http://opensocial.org/wp-content/themes/Lucid/images/logo.png"/>
+  </a>
+  <span style="float:right">
+    <input type="text" id="query"/>
+    <button name="search" id="searchButton" onclick="updateSearchURLs()">Search</button>
+    <br/><span id="engineList"></span>
+  </span>
+
+  <div id="content">
+
+    <div id="controlPanel">
+
+      <div id="accordionResizerControls"
+           style="padding: 10px; width: 100%; height: 100%;"
+           class="ui-widget-content">
+
+        <div id="accordionGadgetControls">
+          <h3><a href="#">Manage Gadgets</a></h3>
+
+          <div>
+
+            <p>Enter the gadget url (ex.
+            http://localhost:8080/gadgets/SocialHelloWorld.xml) and select 'Add' to
+            render the gadget</p>
+
+            <div style='display: block;'>
+	            <input type="text" name="gadgetUrl" id="gadgetUrl"
+	                   class="text ui-widget-content ui-corner-all"
+	                   style="width: 100%"/>
+	            <br />
+	            <button id="preloadAndAddGadget">Preload and Add</button><br />
+	            <button id="addGadget">Add only</button><br />
+	            <button id="preloadGadget">Preload only</button><br />
+            </div>
+           <p>-OR-</p>
+
+            <p>
+            Pick from one of the collections and select Add Gadgets to render the associated gadgets
+            </p>
+            <div style='display: block;'>
+	            <select id="gadgetCollection"> </select>
+	            <button id="addGadgets">Add Gadgets</button>
+            </div>
+
+          </div>
+          <h3><a href="#">Container Events</a></h3>
+
+          <div>
+            <p>
+            Enter the event topic (ex. org.opensocial.container.event.sampleevent)
+            </p>
+
+            <input type="text" name="eventTopic" id="eventTopic"
+                   class="text ui-widget-content ui-corner-all"/>
+            <p>
+            Enter the event payload
+            </p>
+            <div style='display: block;'>
+	            <textarea name="eventPayload" id="eventPayload"
+	                      class="text ui-widget-content ui-corner-all"> </textarea>
+	            <button id="pubEvent">Publish</button>
+            </div>
+          </div>
+          <h3><a href="#">Embedded Experience Gadgets</a></h3>
+
+          <div>
+            <div style='display: block; padding-bottom: 5px'>
+              Enter the embedded experience gadget URL
+            </div>
+            <div style='display: block; padding-bottom: 10px'>
+	            <input type="text" name="eeUrl" id="eeUrl"
+	                   class="text ui-widget-content ui-corner-all widerInput"/>
+            </div>
+
+            <div style='display: block; padding-bottom: 5px;'>
+	            <label style='display: inline-block; width: 75px;' for="eeHeight">Height: </label>
+	            <input type="text" name="eeHeight" id="eeHeight" class="text ui-widget-content ui-corner-all" value="400"></input>
+            </div>
+            <div style='display: block; padding-bottom: 10px;'>
+	            <label style='display: inline-block; width: 75px;' for="eeWidth">Width: </label>
+	            <input type="text" name="eeWidth" id="eeWidth" class="text ui-widget-content ui-corner-all" value="640"></input>
+            </div>
+
+            <p>
+            Enter the context as a JSON String, and select 'Add' to render the embedded experience gadget
+            </p>
+            <textarea name="eecontextPayload" id="eecontextPayload"
+                      class="text ui-widget-content ui-corner-all widerInput"> </textarea>
+            <p><button id="addEmbeddedExperience">Add</button></p>
+          </div>
+        </div>
+        <span class="ui-icon ui-icon-grip-dotted-horizontal" style="margin: 2px auto;"></span>
+
+        <div id="accordionContainerControls">
+          <h3><a href="#">Manage Container</a></h3>
+
+          <div>More to come here with common container....</div>
+        </div>
+      </div>
+    </div>
+
+    <div id="testArea">
+      <div id="accordionResizer"
+           style="padding: 10px; width: 100%; height: 650px;"
+           class="ui-widget-content">
+
+        <div id="accordion">
+          <h3><a href="#">Test Area</a></h3>
+
+          <div>
+            <div id="gadgetArea" class="column">
+            </div>
+            <div id="results"></div>
+          </div>
+        </div>
+
+      <span class="ui-icon ui-icon-grip-dotted-horizontal" style="margin: 2px auto;"></span></div>
+
+      <div id="accordionResizerEvents"
+           style="padding: 10px; width: 100%; height: 350px;"
+           class="ui-widget-content">
+
+        <div id="accordionEvents">
+          <h3><a href="#">events</a></h3>
+
+          <div>
+            <div id="output" class="column"></div>
+          </div>
+
+        </div>
+        <span class="ui-icon ui-icon-grip-dotted-horizontal" style="margin: 2px auto;"></span></div>
+    </div>
+  </div>
+</body>
+</html>
diff --git a/trunk/content/containers/commoncontainer/layout.js b/trunk/content/containers/commoncontainer/layout.js
new file mode 100644
index 0000000..4860364
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/layout.js
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+*/
+
+ //  Setup the base container objects for managing layout, gadget, and container configuration
+$(function() {
+
+		//TODO:  enable drag and drop with the portlet at some point
+		$('.column').sortable({
+			connectWith: '.column',
+			update: function(event, ui) {
+					//TODO:  There is an issue with drag & drop
+		}
+		});
+
+		$('.portlet').addClass('ui-widget ui-widget-content ui-helper-clearfix ui-corner-all')
+			.find('.portlet-header')
+				.addClass('ui-widget-header ui-corner-all')
+				.prepend('<span class="ui-icon ui-icon-minusthick"></span>')
+				.end()
+			.find('.portlet-content');
+		$('.portlet-header .ui-icon').click(function() {
+			$(this).toggleClass('ui-icon-minusthick').toggleClass('ui-icon-plusthick');
+			$(this).parents('.portlet:first').find('.portlet-content').toggle();
+		});
+
+		$('.column').disableSelection();
+	});
+
+	$(function() {
+		$('#accordion').accordion({
+			fillSpace: true,
+			collapsible: true
+
+		});
+	});
+	$(function() {
+		$('#accordionResizer').resizable({
+			minHeight: 140,
+			resize: function() {
+				$('#accordion').accordion('resize');
+			}
+		});
+	});
+	$(function() {
+		$('#accordionEvents').accordion({
+			fillSpace: true,
+			collapsible: true
+
+		});
+	});
+	$(function() {
+		$('#accordionResizerEvents').resizable({
+			minHeight: 140,
+			resize: function() {
+				$('#accordionEvents').accordion('resize');
+			}
+		});
+	});
+	$(function() {
+		$('#accordionGadgetControls').accordion({
+			fillSpace: true,
+			collapsible: true
+
+		});
+	});
+	$(function() {
+		$('#accordionContainerControls').accordion({
+			fillSpace: true,
+			collapsible: true
+
+		});
+	});
+	$(function() {
+		$('#accordionResizerControls').resizable({
+			minHeight: 140,
+			resize: function() {
+				$('#accordionGadgetControls').accordion('resize');
+			}
+		});
+	});
diff --git a/trunk/content/containers/commoncontainer/pubsub2.json b/trunk/content/containers/commoncontainer/pubsub2.json
new file mode 100644
index 0000000..9b9768c
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/pubsub2.json
@@ -0,0 +1,8 @@
+{ "Events":
+  [
+    {"topic": "org.opensocial.event.social.person.selected", "value": "12345"},
+    {"topic": "org.opensocial.event.social.person.updtaed", "value": "12345"},                  
+    {"topic": "org.opensocial.event.container.ee.render", "value": "someContext"},
+    {"topic": "org.opensocial.event.container.itemSelected", "value": "ItemSelected"}
+  ]
+}
\ No newline at end of file
diff --git a/trunk/content/containers/commoncontainer/sample-views.xml b/trunk/content/containers/commoncontainer/sample-views.xml
new file mode 100644
index 0000000..dd8524d
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/sample-views.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Sample Views"
+             height="250">
+    <Require feature="views" />
+  </ModulePrefs>
+  <Content type="html" view="home,default">
+    <![CDATA[
+      <h1>Home view</h1>
+    ]]>
+  </Content>
+  <Content type="html" view="canvas">
+    <![CDATA[
+      <h1>Canvas view</h1>
+    ]]>
+  </Content>
+
+</Module>
diff --git a/trunk/content/containers/commoncontainer/search.js b/trunk/content/containers/commoncontainer/search.js
new file mode 100644
index 0000000..82ba80d
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/search.js
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+*/
+
+var currentEngines = [];
+var templateMap = [];
+/**
+ * Helper function to extract create map of new engines, keyed by template url
+ * @param searchURLs template search urls.
+ * @param description
+ *        opensearch description being added or removed.
+ * @param added
+ *            true if new description, false if removed.
+ */
+extractURLs = function(searchUrls, description, added) {
+  var newEngines = [];
+  for (var i in searchUrls) {
+    var template = searchUrls[i]['@template'];
+    if (template != null) {
+      var descType = searchUrls[i]['@type'];
+      if (descType == 'application/atom+xml') {
+        if (added) {
+          if (currentEngines[template] == null) {
+            newEngines[template] = description.OpenSearchDescription.ShortName;
+            templateMap[template] = 1;
+          } else {
+            templateMap[template]++;
+          }
+        } else {
+          if (currentEngines[template] != null) {
+            if (templateMap[template] == 1) {
+              var oldEngine = document.getElementById(template);
+              oldEngine.parentNode.removeChild(oldEngine);
+              delete currentEngines[template];
+            } else {
+              templateMap[template]--;
+            }
+          }
+        }
+      }
+    }
+  }
+  return newEngines;
+};
+
+/**
+ * Callback passed to the opensearch feature to react to addition/removal of
+ * gadgets containing OpenSearch descriptions.
+ *
+ * @param description
+ *            opensearch description being added or removed.
+ * @param added
+ *            true if new description, false if removed.
+ */
+updateEngines = function(description, added) {
+
+  var searchUrls = [];
+  if (!(description.OpenSearchDescription.Url instanceof Array)) {
+    searchUrls.push(description.OpenSearchDescription.Url);
+  } else {
+    searchUrls = description.OpenSearchDescription.Url;
+  }
+
+  var newEngines = extractURLs(searchUrls, description, added);
+
+  var span = document.getElementById('engineList');
+  for (templateUrl in newEngines) {
+    var current = newEngines[templateUrl];
+    span.innerHTML = span.innerHTML
+        + '<input type=\"checkbox\" checked=\"true\" id=\"' + templateUrl
+        + '\" />' + current;
+    span.innerHTML = span.innerHTML + '<br/>';
+    currentEngines[templateUrl] = current;
+  }
+
+};
+
+CommonContainer.opensearch.addOpenSearchCallback(updateEngines);
+
+/**
+ * Clears old search results, and fetches new ones.
+ *
+ */
+function updateSearchURLs() {
+  // clear the old results
+  $(function() {
+    $('#results').dialog({
+      autoOpen: false
+    });
+  });
+  $('#results').dialog('option', 'minWidth', 1000);
+  $('#results').dialog('open');
+  //var div = document.getElementById("results");
+  /*while (div.hasChildNodes()) {
+    div.removeChild(div.firstChild);
+  }*/
+  $('#results').empty();
+  //div.innerHTML = document.getElementById("query").value;
+  $('#results').innerHTML = document.getElementById('query').value;
+  // fetch new results.
+  getSearchResults(currentEngines, document.getElementById('query').value);
+}
+
+/**
+ * Iterates over template urls and fetches search results.
+ *
+ * @param urls
+ *            all the opensearch template urls in the container.
+ * @param query
+ *            query string.
+ */
+function getSearchResults(urls, query) {
+  // callback function to be called by the fetching code.
+  /**
+   * @param obj
+   *            the result data object, should be XML.
+   * @param engineTitle
+   *            title of the engine being searched.
+   */
+  function urlResponse(obj, engineTitle) {
+    // create placeholder for results
+    var su = document.getElementById('results');
+    var resultDiv = document.createElement('div');
+    su.appendChild(resultDiv);
+    // if there are no errors, parse the results
+    if (obj.status == 200) {
+      resultDiv.className = 'searchEngine';
+      var stringDom = obj.content;
+      var domdata=opensocial.xmlutil.parseXML(stringDom);
+      if (domdata != null) {
+        var entries = domdata.getElementsByTagName('entry');
+        resultDiv.innerHTML = resultDiv.innerHTML + engineTitle + ':<br/>';
+        if (entries.legnth == 0) {
+          resultDiv.innerHTML = resultDiv.innerHTML + ('No results found');
+        } else {
+          var resultCount = entries.length;
+          if (resultCount > 15) {
+            resultCount = 15;
+          }
+          for (i = 0; i < resultCount; i++) {
+            if (entries[i].getElementsByTagName('title').length > 0) {
+              titles = entries[i].getElementsByTagName('title');
+              title = titles[0].childNodes[0].nodeValue;
+            } else {
+              title = 'Untitled';
+            }
+            var link = null;
+            //for standard atom results, we can extract the link
+            if (entries[i].getElementsByTagName('link').length > 0) {
+              links = entries[i].getElementsByTagName('link');
+              link = links[0].attributes.href.nodeValue;
+            }
+            var summaryNode = entries[i].getElementsByTagName('summary')[0];
+            if (summaryNode == null) {
+              summaryNode = entries[i].getElementsByTagName('description')[0];
+            }
+            if (link == null) {
+            resultDiv.innerHTML = resultDiv.innerHTML
+                + '<p style=\"color:blue\"/>'
+                + gadgets.util.escapeString(title);
+            } else {
+              resultDiv.innerHTML = resultDiv.innerHTML
+              + '<p style=\"color:blue\"/>'
+              + '<a href=\"'+ link + '\" target=\"_blank\">'
+              + gadgets.util.escapeString(title)
+              + '</a>';
+            }
+            if (summaryNode != null) {
+              var summary = summaryNode.textContent;
+              if (summary != null) {
+                resultDiv.innerHTML = resultDiv.innerHTML
+                    + gadgets.util.escapeString(summary);
+              }
+            }
+          }
+        }
+      }
+    } else { // errors occured, notify the user.
+      resultDiv.innerHTML = resultDiv.innerHTML + engineTitle
+          + '<br/> An error has occured:' + obj.status;
+    }
+  }
+  var params = {};
+  for (url in currentEngines) {
+    // check if the current engine is selected.
+    if (document.getElementById(url).checked) {
+      title = currentEngines[url];
+      // replace placeholder with actual search term.
+      url = url.replace('{searchTerms}', query);
+      // for now, start on page 1
+      url = url.replace('{startPage?}', 1);
+      // makes sure that the title corresponds to the engine being search.
+      // Resolves a prior timing issue.
+      var callback = function() {
+        var myTitle = '' + title;
+        return function(response) {
+          urlResponse(response, myTitle);
+        };
+      }();
+      // go fetch the results.
+      osapi.http.get({
+                  'href' : url,
+                  'format' : 'text'
+                }).execute(callback)
+    }
+  }
+
+}
diff --git a/trunk/content/containers/commoncontainer/viewController.js b/trunk/content/containers/commoncontainer/viewController.js
new file mode 100644
index 0000000..e03831b
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/viewController.js
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+$(function() {
+
+	// Input field that contains gadget urls added by the user manually
+	var newGadgetUrl = $('#gadgetUrl');
+
+	// Input fields that contains EE gadget URL and EE context
+	var eeUrl = $('#eeUrl');
+	var eecontextPayload = $('#eecontextPayload');
+	var eeHeight = $('#eeHeight');
+	var eeWidth = $('#eeWidth');
+
+	//  Input fields for container event testing
+	var newEventTopic = $('#eventTopic');
+	var newEventPayload = $('#eventPayload');
+
+	// Base html template that is used for the gadget wrapper and site
+	var gadgetTemplate = '<div class="portlet">' +
+				                '<div class="portlet-header">sample to replace</div>' +
+				                '<div id="gadget-site" class="portlet-content"></div>' +
+	                     '</div>';
+
+	//variable to keep track of gadget current view for collapse and expand gadget actions.
+	var currentView = 'default';
+
+	// ID used to associate gadget site
+	var curId = 0;
+
+	//  Load the default collections stored and update the options with the collection name
+	$.ajax({
+			url: './gadgetCollections.json',
+			dataType: 'json',
+			success: function(data) {
+			  $.each(data.collections, function(i,data) {
+				 var optionVal = [];
+				 $.each(data.apps, function(i,data) {
+				   if (data.url.indexOf('http') < 0 && data.url.indexOf('/') == 0) {
+					 optionVal.push(urlBase + data.url);
+				   }else {
+					 optionVal.push(data.url);
+				   }
+				 });
+			     $('#gadgetCollection').append('<option value="' + optionVal.toString() + '">' + data.name + '</option>');
+			   });
+			}
+	});
+
+	$.ajax({
+		url: './viewsMenu.json',
+		dataType: 'json',
+		success: function(data) {
+		  $.each(data.views, function(i,selection) {
+		     $('#viewOptions').append('<option value="' + selection.value + '">' + selection.name + '</option>');
+		  });
+		}
+	});
+
+	//navigate to the new view and save it as current view
+    navigateView = function(gadgetSite, gadgetURL, toView) {
+    	//save the current view for collapse, expand gadget
+    	currentView = toView;
+    	CommonContainer.navigateView(gadgetSite, gadgetURL, toView);
+    };
+
+    //handle gadget collapse, expand, and remove gadget actions
+    handleNavigateAction = function(portlet,gadgetSite,gadgetURL,actionId) {
+      //remove button was click, remove the portlet/gadget
+      if(typeof gadgetSite !== 'undefined'){
+        if (actionId === 'remove') {
+          if (confirm('This gadget will be removed, ok?')) {
+            CommonContainer.closeGadget(gadgetSite);
+            portlet.remove();
+            delete siteToTitleMap[gadgetSite.getId()];
+          }
+        }else if (actionId === 'expand') {
+          //navigate to currentView prior to colapse gadget
+          CommonContainer.navigateView(gadgetSite, gadgetURL, currentView);
+        }else if (actionId === 'collapse') {
+          CommonContainer.collapseGadget(gadgetSite);
+        }
+      }
+    };
+
+    //RPC handler for the set-title feature
+    window.setTitleHandler = function(rpcArgs, title) {
+      var titleId = siteToTitleMap[rpcArgs.gs.id_];
+      $('#' + titleId).text(title);
+    };
+
+    window.getNewGadgetElement = function(result, gadgetURL){
+      result[gadgetURL] = result[gadgetURL] || {};
+      var gadgetSiteString = "$(this).closest(\'.portlet\').find(\'.portlet-content\').data(\'gadgetSite\')";
+      var viewItems = '';
+      var gadgetViews = result[gadgetURL].views || {};
+      for (var aView in gadgetViews) {
+        viewItems = viewItems + '<li><a href="#" onclick="navigateView(' + gadgetSiteString + ',' + '\'' + gadgetURL + '\'' + ',' + '\'' + aView + '\'' + '); return false;">' + aView + '</a></li>';
+      }
+      var newGadgetSite = gadgetTemplate;
+      newGadgetSite = newGadgetSite.replace(/(gadget-site)/g, '$1-' + curId);
+      siteToTitleMap['gadget-site-' + curId] = 'gadget-title-' + curId;
+      var gadgetTitle = (result[gadgetURL] && result[gadgetURL]['modulePrefs'] && result[gadgetURL]['modulePrefs'].title) || 'Title not set';
+      $(newGadgetSite).appendTo($('#gadgetArea')).addClass('ui-widget ui-widget-content ui-helper-clearfix ui-corner-all')
+      .find('.portlet-header')
+      .addClass('ui-widget-header ui-corner-all')
+      .text('')
+      .append('<span id="gadget-title-' + curId + '">' + gadgetTitle + '</span>' +
+              '<ul id="viewsDropdown">' +
+             '<li class="li-header">' +
+               '<a href="#" class="hidden"><span id="dropdownIcon" class="ui-icon ui-icon-triangle-1-s"></span></a>' +
+             '<ul>' +
+               viewItems +
+             '</ul>' +
+              '</li>' +
+               '</ul>')
+      .append('<span id="remove" class="ui-icon ui-icon-closethick"></span>')
+      .append('<span id="expand" class="ui-icon ui-icon-plusthick"></span>')
+      .append('<span id="collapse" class="ui-icon ui-icon-minusthick"></span>');
+
+      return $('#gadget-site-'+curId).get([0]);
+    }
+
+    //create a gadget with navigation tool bar header enabling gadget collapse, expand, remove, navigate to view actions.
+    window.buildGadget = function(result,gadgetURL){
+      result = result || {};
+      var element =  window.getNewGadgetElement(result, gadgetURL);
+      $(element).data('gadgetSite', CommonContainer.renderGadget(gadgetURL, curId));
+
+       //determine which button was click and handle the appropriate event.
+      $('.portlet-header .ui-icon').click(function() {
+        handleNavigateAction($(this).closest('.portlet'), $(this).closest('.portlet').find('.portlet-content').data('gadgetSite'), gadgetURL, this.id);
+      });
+    };
+
+	//  Publish the container event
+	$('#pubEvent').click(function() {
+		CommonContainer.inlineClient.publish(newEventTopic.val(), newEventPayload.val());
+
+		//TODO:  Need to add in some additional logic in the Container to enable point to point for things like Embedded Experience...
+
+		//var ppcont = CommonContainer.managedHub.getContainer("__gadget_1");
+		//CommonContainer.managedHub.publishForClient(ppcont, newEventTopic.val(), newEventPayload.val());
+		//ppcont.sendToClient('org.apache.shindig.random-number', '1111', ppcont.getClientID());
+		//clear values
+		newEventTopic.val('');
+		newEventPayload.val('');
+		    return true;
+	});
+
+	//  Preload then add a single gadget entered by user
+	$('#preloadAndAddGadget').click(function() {
+		CommonContainer.preloadGadget(newGadgetUrl.val(), function(result) {
+		  for (var gadgetURL in result) {
+		    if(!result[gadgetURL].error) {
+			    window.buildGadget(result, gadgetURL);
+			    curId++;
+		    }
+		  }
+
+	      //Clear Values
+	      newGadgetUrl.val('');
+		});
+
+		return true;
+	});
+
+	 //  Preload a single gadget entered by user (don't add it to the page)
+  $('#preloadGadget').click(function() {
+    CommonContainer.preloadGadget(newGadgetUrl.val(), function(result) {
+      for (var gadgetURL in result) {
+        if(!result[gadgetURL].error) {
+          //window.buildGadget(result, gadgetURL);
+          curId++;
+        }
+      }
+
+        //Clear Values
+        newGadgetUrl.val('');
+    });
+
+    return true;
+  });
+
+	//  Add a single gadget entered by user (no preloading)
+	 $('#addGadget').click(function() {
+	        window.buildGadget({}, newGadgetUrl.val());
+	        curId++;
+
+	        //Clear Values
+	        newGadgetUrl.val('');
+	        return true;
+	    });
+
+	//  Load the select collection of gadgets and render them the gadget test area
+	$('#addGadgets').click(function() {
+
+		//TODO:  This just provides and example to load configurations
+		//var testGadgets=["http://localhost:8080/container/sample-pubsub-2-publisher.xml","http://localhost:8080/container/sample-pubsub-2-subscriber.xml"];
+		var testGadgets = $('#gadgetCollection').val().split(',');
+		CommonContainer.preloadGadgets(testGadgets, function(result) {
+		  for (var gadgetURL in result) {
+		    if(!result[gadgetURL].error) {
+		      window.buildGadget(result, gadgetURL);
+		      curId++;
+		    }
+		  }
+
+		});
+		return true;
+
+	});
+
+	$('#addEmbeddedExperience').click(function(){
+	  CommonContainer.preloadGadgets(eeUrl.val(), function(result) {
+	    for (var gadgetURL in result) {
+	      if(!result[gadgetURL].error) {
+	        var eeElement = window.getNewGadgetElement(result, gadgetURL);
+
+	        var model = new Object();
+
+	        model.context = gadgets.json.parse(eecontextPayload.val());
+	        model.gadget = gadgetURL;
+
+	        var params = [];
+	        params[osapi.container.ee.RenderParam.GADGET_RENDER_PARAMS] = {
+	            'height' : eeHeight.val(),
+	            'width' : eeWidth.val()
+	        };
+	        var currentEESite = CommonContainer.ee.navigate(eeElement, model, params, null);
+	        curId++;
+	      }
+	    }
+	  });
+
+	  return true;
+	});
+
+
+});
diff --git a/trunk/content/containers/commoncontainer/viewsMenu.json b/trunk/content/containers/commoncontainer/viewsMenu.json
new file mode 100644
index 0000000..a5b2fa4
--- /dev/null
+++ b/trunk/content/containers/commoncontainer/viewsMenu.json
@@ -0,0 +1,7 @@
+{ "views":
+  [
+    {"name": "home", "value": "home", "height": "400px", "width": "450px"},
+    {"name": "canvas", "value": "canvas", "height": "500px", "width": "800px"},
+    {"name": "profile", "profile", "height": "500px", "width": "300px"}
+  ]
+}
\ No newline at end of file
diff --git a/trunk/content/containers/conservcontainer/ConServContainer.js b/trunk/content/containers/conservcontainer/ConServContainer.js
new file mode 100644
index 0000000..94ecd67
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/ConServContainer.js
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+// Create the common container object.
+var CommonContainer = new osapi.container.Container({});
+
+// Default the security token for the container. Using this example security
+// token requires enabling the DefaultSecurityTokenCodec to let
+// UrlParameterAuthenticationHandler create valid security token.
+shindig.auth.updateSecurityToken('john.doe:john.doe:appid:cont:url:0:default');
+
+// Wrapper function to set the gadget site/id and default width.
+CommonContainer.renderGadget = function(gadgetURL, gadgetId) {
+  // going to hardcode these values for width.
+  var el = document.getElementById('gadget-site-' + gadgetId);
+  var parms = {};
+  parms[osapi.container.RenderParam.WIDTH] = '100%';
+  var gadgetSite = CommonContainer.newGadgetSite(el);
+  CommonContainer.navigateGadget(gadgetSite, gadgetURL, {}, parms);
+  return gadgetSite;
+};
+
+// Function for pre-rendering gadgets.  Gadget pre-rendering
+// occurs when an action contributed by a pre-loaded gadget
+// is executed.
+function preRenderGadget(gadgetUrl, opt_params) {
+  var gadgetId = getGadgetId(gadgetUrl);
+  var el = $('#gadget-site-' + gadgetId);
+  var gadgetSite = CommonContainer.renderGadget(gadgetUrl, gadgetId);
+  el.data('gadgetSite', gadgetSite);
+  return gadgetSite;
+}
+
+// Common container init function.
+CommonContainer.init = new function() {
+  // Map needed for lazy loading of gadgets
+  urlsToGadgetIdMap = {};
+
+  // Register our rendering functions with the action service
+  if (CommonContainer.actions) {
+    // Called when an action should be displayed in the container
+    CommonContainer.actions.registerShowActionsHandler(showActions);
+
+    // Called when a action should be removed from the container
+    CommonContainer.actions.registerHideActionsHandler(hideActions);
+
+    // Called for actions contributed by pre-loaded gadgets (lazy load)
+    CommonContainer.actions.registerNavigateGadgetHandler(preRenderGadget);
+  }
+}
+
+// Support for lazy loading gadgets
+function getGadgetId(url) {
+  if (urlsToGadgetIdMap) {
+    return urlsToGadgetIdMap[url];
+  }
+}
+
+// Wrapper function to add gadgets to the page.
+CommonContainer.addGadgetToPage = function(gadgetURL, lazyLoad) {
+  addGadget(gadgetURL, lazyLoad);
+};
+
+// Wrapper function to expand a gadget
+CommonContainer.navigateView = function(gadgetSite, gadgetURL, view) {
+  var renderParms = {};
+  if (view === null || view === '') {
+    view = 'default';
+  }
+  renderParms[osapi.container.RenderParam.WIDTH] = '100%';
+  renderParms['view'] = view;
+  CommonContainer.navigateGadget(gadgetSite, gadgetURL, {}, renderParms);
+};
+
+// see peoplehelpers.js
+osapi.people.getViewer = function(options) {
+  options = options || {};
+  options.userId = '@viewer';
+  options.groupId = '@self';
+  return osapi.people.get(options);
+};
+
+// see peoplehelpers.js
+osapi.people.getViewerFriends = function(options) {
+  options = options || {};
+  options.userId = '@viewer';
+  options.groupId = '@friends';
+  return osapi.people.get(options);
+};
+
+// Function to display actions
+function showActions(actions) {
+  var itemObj = actions[0];
+  if (!itemObj.path && !itemObj.dataType) {
+    // object is invalid!
+    return;
+  }
+  // bind the action to the specified data object type
+  if (itemObj.dataType) {
+    if (itemObj.dataType == 'opensocial.Person') {
+      addPersonAction(itemObj);
+    }
+  }
+  // bind the action to the specified path (container UI elements)
+  if (itemObj.path && itemObj.path.length > 0) {
+    addContainerAction(itemObj);
+  }
+}
+
+// Adds the specified action to a person element.
+function addPersonAction(itemObj) {
+  // select all person elements
+  var personActionDiv = $('.personActions');
+
+  // create a link and append it to each person element
+  var actionStr = '';
+  if (itemObj.icon && itemObj.icon.length > 0) {
+    actionStr += '<img src="' + itemObj.icon + '"/>';
+  }
+  actionStr += '<a name="person-action-' + itemObj.id + '" title="' +
+               itemObj.tooltip + '" href="#">' + itemObj.label + '</a>';
+  actionStr = '<span class="' + itemObj.id + '">' + actionStr + '</span>';
+  var actionLink = $(actionStr);
+
+  // add a separator if needed
+  if (personActionDiv.children().length > 0) {
+    personActionDiv.append(' | ');
+  }
+
+  // select all links that were added and set the click handler
+  personActionDiv.append(actionLink);
+  $('a[name="person-action-' + itemObj.id + '"]').each(function(i) {
+    $(this).click(function() {
+      CommonContainer.actions.runAction(itemObj.id);
+    });
+  });
+}
+
+// Adds an action to the container UI
+function addContainerAction(itemObj) {
+  var pathParts = itemObj.path.split('/');
+  var pathType = pathParts.shift();
+  var pathScope = pathParts.shift();
+  var remainingPath = pathParts.join('/');
+  var contributionBar;
+
+  // right now we support contributing to the global menubar
+  if (pathType == 'container' && pathScope == 'navigationLinks') {
+    contributionBar = $('#globalMenubar');
+    contributionBar.show();
+  }
+
+  // create the action element
+  var actionStr = '';
+  if (itemObj.icon && itemObj.icon.length > 0) {
+    actionStr += '<img src="' + itemObj.icon + '"/>';
+  }
+  actionStr += '<a title="' + itemObj.tooltip + '" href="#">'
+               + itemObj.label + '</a>';
+  actionStr = '<span class="' + itemObj.id + '">' + actionStr + '</span>';
+  var actionLink = $(actionStr);
+
+  // add a separator if needed
+  if (contributionBar.children().length > 0) {
+    contributionBar.append(' | ');
+  }
+
+  // add the new action
+  contributionBar.append(actionLink);
+  actionLink.click(function() {
+    CommonContainer.actions.runAction(itemObj.id);
+  });
+}
+
+// Function to hide actions
+function hideActions(actions) {
+  var itemObj = actions[0];
+  if (itemObj.path || itemObj.dataType) {
+    // remove the action from the specified data object type
+    if (itemObj.dataType && itemObj.dataType == 'opensocial.Person') {
+      removePersonAction(itemObj);
+    }
+    // remove the action to the specified path (container UI elements)
+    if (itemObj.path && itemObj.path.length > 0) {
+      removeContainerAction(itemObj);
+    }
+  }
+}
+
+// Removes the specified action from a person element.
+function removePersonAction(itemObj) {
+  // hack - actions should be removed individually
+  $('.personActions').empty();
+}
+
+// Removes the specified action from the container UI
+function removeContainerAction(itemObj) {
+  // hack - actions should be removed individually
+  $('#globalMenubar').empty();
+  $('#globalMenubar').hide();
+}
+
+// Runs the action specified in the action_id_field
+function runAction() {
+  id = document.getElementById('action_id_field').value;
+    CommonContainer.actions.runAction(id, CommonContainer.selection.getSelection());
+}
diff --git a/trunk/content/containers/conservcontainer/index.html b/trunk/content/containers/conservcontainer/index.html
new file mode 100644
index 0000000..19780e7
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/index.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+	<head>
+		<link rel="stylesheet" href="../../../container/gadgets.css">
+		<link rel="stylesheet" href="portlet.css">
+		<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css" rel="stylesheet" type="text/css"/>
+		<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
+  		<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"></script>
+		<script type="text/javascript" src="../../../gadgets/js/core:container:rpc:selection:actions.js?c=1&debug=1&container=default"></script>	
+		<script type="text/javascript" src="ConServContainer.js"></script>
+		<script type="text/javascript" src="viewController.js"></script>
+		<script type="text/javascript" src="layout.js"></script>
+		<script type="text/javascript">
+			var my = {};
+			var str = '/conservcontainer/';
+			var base = location.href.substr(0, location.href.indexOf(str)+str.length);
+			my.gadgetSpecUrls = [ base + 'sample-selection-listener.xml', base + 'sample-actions-runner.xml'];
+			my.renderGadgets = function() {
+					$(".gadgetUrl")[0].value = base + "sample-actions-voip.xml";
+				  // uncomment this to render gadgets specified in my.gadgetSpecUrls
+				  for (var i = 0; i < my.gadgetSpecUrls.length; ++i) {
+				    //var gadgetSite = CommonContainer.renderGadget(my.gadgetSpecUrls[i], "gadget-chrome-"+i);
+				    CommonContainer.addGadgetToPage(my.gadgetSpecUrls[i]);
+				  }
+			};
+			function onPreloadGadget() {
+				var gadgetSpecUrl = $(".gadgetUrl")[0].value;
+				CommonContainer.addGadgetToPage(gadgetSpecUrl, true);
+			}
+			function onAddGadget() {
+				var gadgetSpecUrl = $(".gadgetUrl")[0].value;
+				CommonContainer.addGadgetToPage(gadgetSpecUrl, false);
+			}
+			function log(message) {
+			  document.getElementById("output").innerHTML += gadgets.util.escapeString(message) + "<br/>";
+			}
+			function setSelection(selection) {
+			  CommonContainer.selection.setSelection(selection);
+			}
+		</script>
+		
+		<style type="text/css">
+		     #helloworlds {
+		       margin: 20px;
+		       font-family: arial, sans-serif;
+		       width: 310px;
+		     }
+		     div.person img {
+		       margin-bottom: 10px;
+		     }
+		     div.bubble {
+		       background-image: url(../../../images/bubble.gif);
+		       background-repeat: no-repeat;
+		       width: 202px;
+		       height: 66px;
+		       padding: 12px 0px 0px 12px;
+		       font-weight: bold;
+		       font-size: 18px;
+		       float: right;
+		     }
+		     .c0 { color: #008000; }
+		     .c1 { color: #FF8A00; }
+		     .c2 { color: #7777CC; }
+		     .c3 { color: #008000; }
+		     .c4 { color: #CC0000; }
+		     .c5 { color: #73A6FF; }
+		     div.name, div.personAction {
+		       width: 150px;
+		       text-align: right;
+		       font-weight: normal;
+		       font-size: 12px;
+		       color: #999;
+		       position:relative;
+		       top: 10px;
+		       right: -35px;
+		       margin: 5px;
+		     }
+		     #globalMenubar {
+				font-family: Arial;
+				color: #666666;
+				text-align: right;
+				padding: 10px;
+			}
+	    </style>
+	
+	    <script type="text/javascript">
+		     var hellos = new Array('Hello World', 'Hallo Welt', 'Ciao a tutti', 'Hola mundo',
+		       '&#1055;&#1086;&#1103;&#1074;&#1083;&#1077;&#1085;&#1080;&#1077; &#1085;&#1072; &#1089;&#1074;&#1077;&#1090;',
+		       '&#12371;&#12435;&#12395;&#12385;&#12399;&#19990;&#30028;',
+		       '&#20320;&#22909;&#19990;&#30028;',
+		       '&#50668;&#47084;&#48516;, &#50504;&#45397;&#54616;&#49464;&#50836;');
+		     var numberOfStyles = 6;
+		     var viewerCount;
+		     var allPeople, viewerFriendData;
+		     var selected_id = null;
+
+		     function highlight(id) {
+		     	if (selected_id == id) return;
+		     	element = document.getElementById("person_"+id);
+				element.style.background = "#73A6FF";
+		     }
+
+		     function unhighlight(id) {
+		     	if (selected_id == id) return;
+		     	element = document.getElementById("person_"+id);
+				element.style.background = "#FFF";
+		     }
+
+		     function select(id) {
+		     	 if (selected_id != null) {
+		     		 unselect = selected_id;
+		     		 selected_id = null;
+		     		 unhighlight(unselect);
+		     	 }
+		     	 selected_id = id;
+		     	 element = document.getElementById("person_"+id);
+				   element.style.background = "#CCC";
+				
+				   // set selection
+				   setSelection([{
+				     type: 'opensocial.Person',
+				     dataObject: allPeople[id]
+				   }]);
+		     }
+
+		     function render(data) {
+		       var viewer = data.viewer;
+		       allPeople = data.viewerFriends.list;
+		       if (viewer) {
+		         allPeople.push(viewer);
+		       }
+		
+		       var viewerData = data.viewerData;
+		       viewerCount = getCount(viewerData[viewer.id]);
+		
+		       viewerFriendData = data.viewerFriendData;
+		       viewerFriendData[viewer.id] = viewerData[viewer.id];
+		
+		       var html = '';
+		       for (var i = 0; i < allPeople.length; i++) {
+		         var count = getCount(viewerFriendData[allPeople[i].id]);
+		         if (count == 0) {
+		           //continue;
+		         }
+		         html += '<div class="personActions"></div>';
+		         html += '<div id="person_'+i+'" ';
+		         html += 'class="person" onMouseDown="select('+i+');" onMouseOver="highlight('+i+');" onMouseOut="unhighlight('+i+');">';
+		         html += '<div id="bubble_'+i+'" class="bubble c' + count % numberOfStyles + '">';
+		         html += hellos[count % hellos.length];
+		         html += '<div class="name">' + allPeople[i].name.formatted + ' (' + count + ') ' + allPeople[i].gender;
+		         html += '</div></div>';
+		
+		         if (allPeople[i].thumbnailUrl
+		             && allPeople[i].thumbnailUrl.indexOf('null') == -1) {
+		           html += '<img src="' + allPeople[i].thumbnailUrl + '"/>';
+		         } else {
+		           html += '<img src="../../../images/nophoto.gif"/>';
+		         }
+		         html += '<br style="clear:both"></div>';
+		       }
+		       document.getElementById('helloworlds').innerHTML = html;
+		     }
+		
+		     function getCount(data) {
+		       return data && data['count'] ? Number(data['count']) : 0;
+		     }
+		
+		     function initData() {
+		       var fields = ['id', 'displayName', 'age','name','gender','profileUrl','thumbnailUrl'];
+		       var batch = osapi.newBatch();
+		       batch.add('viewer', osapi.people.getViewer({sortBy:'name',fields:fields}));
+		       batch.add('viewerFriends', osapi.people.getViewerFriends({sortBy:'name',fields:fields}));
+		       batch.add('viewerData', osapi.appdata.get({keys:['count']}));
+		       batch.add('viewerFriendData', osapi.appdata.get({groupId:'@friends',keys:['count']}));
+		       batch.execute(render);
+		     }
+		     gadgets.util.registerOnLoadHandler(initData);
+	   </script>
+
+	</head>
+
+	<body onLoad="my.renderGadgets(); initData();">
+		<div id="globalMenubar" width="100%">
+		    <input type="text" id="action_id_field" value="org.samplevoip.chatwithperson"/>
+		    <input type="button" value="Run Action" onclick="runAction()"/>
+		</div>
+		<h2>Sample: Action+Selection Service</h2>
+		<p>Press preload to preload the voip gadget.  You will see actions appear that
+		   have been contributed declaratively by the voip gadget.  Select a person
+		   object from the list and then select one of the actions.  This will
+		   render the voip gadget and execute the selected action.  Finally, you can
+		   close the gadget by pressing the 'x' icon and the action will be removed.
+		   You can also use the Sample Selection Listener gadget to view the contents
+		   of the currently selected object.
+		<div id="helloworlds" style="margin: 4px; float: left;">
+		</div>
+		<div style="font-family:Arial">
+		Preload Gadget: <input type="text" class="gadgetUrl" />
+	  	<input type="submit" value="Preload" onclick="onPreloadGadget();"/>
+		<input type="submit" value="Add" onclick="onAddGadget();"/>
+		</div>
+	  	<div id="gadgetArea" class="column" style="float: left;">
+	  	</div>
+	</body>
+</html>
diff --git a/trunk/content/containers/conservcontainer/layout.js b/trunk/content/containers/conservcontainer/layout.js
new file mode 100644
index 0000000..8b06fcd
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/layout.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+//  Setup the base container objects for managing layout, gadget, and container configuration
+$(function() {
+  // TODO: enable drag and drop with the portlet at some point
+  $('.column').sortable({
+    connectWith: '.column',
+    update: function(event, ui) {
+      // TODO: There is an issue with drag & drop
+    }
+  });
+
+  $('.portlet').addClass(
+      'ui-widget ui-widget-content ui-helper-clearfix ui-corner-all').find(
+      '.portlet-header').addClass('ui-widget-header ui-corner-all').prepend(
+      '<span class="ui-icon ui-icon-minusthick"></span>').end().find(
+      '.portlet-content');
+  $('.portlet-header .ui-icon').click(function() {
+    $(this).toggleClass('ui-icon-minusthick').toggleClass('ui-icon-plusthick');
+    $(this).parents('.portlet:first').find('.portlet-content').toggle();
+  });
+
+  $('.column').disableSelection();
+});
diff --git a/trunk/content/containers/conservcontainer/portlet.css b/trunk/content/containers/conservcontainer/portlet.css
new file mode 100644
index 0000000..be38514
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/portlet.css
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+.portlet {
+	width: 500px;
+}
+.portlet-header .ui-icon { 
+ 	float: right; 
+ }
+ 
+ #viewsDropdown, #viewsDropdown ul { 
+   list-style: none; margin: 10px;
+ }
+ #viewsDropdown, #viewsdropdown * {
+   padding: 0; margin: 0; 
+ }
+
+ #viewsDropdown li.li-header {
+   float: right; margin-left: -1px;
+ }
+ #viewsDropdown li.li-header a { 
+   display: block; font-size: small;
+ }
+
+ #viewsDropdown li.li-header ul { 
+   display: none; border: 1px black solid; text-align: left; 
+ }
+ 
+ #viewsDropdown li.li-header:hover ul { 
+   display: block; 
+ }
+
+ #viewsDropdown li.li-header ul li a { 
+   padding: 5px; height: 17px; font-size: small;
+ }
\ No newline at end of file
diff --git a/trunk/content/containers/conservcontainer/sample-actions-runner.xml b/trunk/content/containers/conservcontainer/sample-actions-runner.xml
new file mode 100644
index 0000000..2d7595c
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/sample-actions-runner.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+<ModulePrefs title="Sample Action Runner"
+             height="250">
+<Require feature="actions"></Require>
+<Require feature="selection"></Require>
+</ModulePrefs>
+<Content type="html">
+<![CDATA[
+<script>
+var selection;
+gadgets.selection.addListener(
+  function(s){selection = s;}
+);
+
+function runAction() {
+  id = document.getElementById('action_id_field').value;
+  gadgets.actions.runAction(id, selection);
+}
+
+function showActions(actions) {
+  var a = document.createElement("a");
+  a.href="#";
+  a.onclick = function() {
+    gadgets.actions.runAction(actions[0].id);
+  };
+  var t = document.createTextNode(actions[0].label);
+  a.appendChild(t);
+  a.id = actions[0].id;
+  var m = document.getElementById("menu")
+  if (m.childNodes.length > 0) {
+    var s = document.createTextNode(" | ");
+    m.appendChild(s);
+  }
+  m.appendChild(a);
+}
+
+function hideActions(actions) {
+  var m = document.getElementById("menu");
+  while (m.childNodes.length > 0) 
+    m.removeChild(m.childNodes[0]);
+}
+
+gadgets.actions.registerShowActionsListener(showActions);
+gadgets.actions.registerHideActionsListener(hideActions);
+
+</script>
+<div id="menu"></div>
+
+<div>
+<input type="text" id="action_id_field">
+<input type="button" value="Run Action" onclick="runAction()"/>
+</div>
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/containers/conservcontainer/sample-actions-voip.xml b/trunk/content/containers/conservcontainer/sample-actions-voip.xml
new file mode 100644
index 0000000..ba79564
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/sample-actions-voip.xml
@@ -0,0 +1,210 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="VOIP Gadget" height="250">
+    <Require feature="selection"/>
+    <Require feature="open-views"/>
+    <Require feature="actions">
+      <Param name="action-contributions"><![CDATA[
+        <action id="org-samplevoip-chatwithperson" dataType="opensocial.Person" label="Chat" tooltip="Chat" />
+        <action id="org-samplevoip-callbyperson" dataType="opensocial.Person" label="Call" tooltip="Call" />
+        <action id="org-samplevoip-globalcall" path="container/navigationLinks" label="VOIP Call" tooltip="Call using VOIP" />
+      ]]></Param>
+    </Require>
+    <Optional feature="settitle"/>
+    <Optional feature="dynamic-height"/>
+  </ModulePrefs>
+  <Content type="html" view="dialog"><![CDATA[
+    <script>
+
+    </script>
+  ]]></Content>
+
+  <Content type="html"><![CDATA[
+    <script>
+      function status(str) {
+        document.getElementById("status").innerHTML = str;
+        if (gadgets.util.hasFeature('dynamic-height')) {
+          gadgets.window.adjustHeight();
+        }
+      }
+
+      function findFirstPerson(selection) {
+        selection = [].concat(selection);
+        var person;
+        for (var i = 0, selected; !person && (selected = selection[i]); i++) {
+          if (selected.type && selected.dataObject && selected.type.toLowerCase() == 'opensocial.person') {
+            person = selected.dataObject;
+          }
+        }
+        return person;
+      }
+
+      function chat(selection) {
+        var person = findFirstPerson(selection);
+        if (!person) {
+           return status('No person info!');
+        }
+
+        var params = {
+          viewTarget: 'modalDialog',
+          view: 'chat',
+          viewParams: person
+        };
+
+        status('Chat started with <span class="name">' + person.displayName + '</span> ...');
+        gadgets.views.openGadget(function(result){
+          status('Chat with <span class="name">' + person.displayName + '</span> ended.');
+        }, function(){}, params);
+      }
+
+      function call(selection) {
+        var person = findFirstPerson(selection),
+            params = {
+		          viewTarget: 'modalDialog',
+		          view: 'call',
+		          viewParams: person
+		        };
+
+        if (person) {
+          status('Call started with <span class="name">' + person.displayName + '</span> ...');
+        } else {
+          status('Call started...');
+        }
+        gadgets.views.openGadget(function(result) {
+          if (result) {
+            status('Call with <span class="name">' + result + '</span> ended.');
+          } else {
+            status('Call ended.');
+          }
+        }, function(){}, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(function() {
+        if (gadgets.actions) {
+          gadgets.actions.updateAction({
+            id: "org-samplevoip-chatwithperson",
+            callback: chat
+          });
+
+          gadgets.actions.updateAction({
+            id: "org-samplevoip-callbyperson",
+            callback: call
+          });
+
+          gadgets.actions.updateAction({
+            id: "org-samplevoip-globalcall",
+            callback: call
+          });
+        }
+
+        if (gadgets.util.hasFeature('dynamic-height')) {
+          gadgets.window.adjustHeight();
+        }
+      });
+    </script>
+    <div>VOIP Status:</div>
+    <div id="status"></div>
+  ]]></Content>
+
+  <Content type="html" view="chat"><![CDATA[
+    <script>
+      gadgets.util.registerOnLoadHandler(function() {
+        var person = gadgets.views.getParams();
+        if (!person) {
+          gadgets.views.setReturnValue('No person!');
+          gadgets.views.close();
+        }
+
+        if(gadgets.util.hasFeature('settitle')) {
+          gadgets.window.setTitle('Chat with ' + person.displayName);
+        }
+        var url = gadgets.io.getProxyUrl(person.thumbnailUrl || 'http://www.gravatar.com/avatar?d=mm');
+        document.getElementById('thumbnail').setAttribute('src', url);
+
+        document.getElementById('chatlog').innerHTML += '<div><span class="them">' + person.displayName + ':</span>Hi!</div>';
+        if (gadgets.util.hasFeature('dynamic-height')) {
+          gadgets.window.adjustHeight();
+        }
+        document.getElementById('chatmessage').focus();
+      });
+
+      function sendMessage(elem, event) {
+        if (event.keyCode == 13) {
+          var log = document.getElementById('chatlog');
+          log.innerHTML += '<div><span class="me">Me:</span>' + elem.value + '</div>';
+          log.scrollTop = log.scrollHeight;
+          elem.value="";
+        }
+      }
+    </script>
+
+    <style>
+      #container { padding-left: 110px; }
+      #thumbnail { width: 100px; height: 100px; margin-left: -110px; float: left; }
+      #chatarea { width: 100%; }
+      #chatlog { height: 100px; width: 100%; display: block; overflow-y: auto; }
+      #chatmessage { width: 100%; display: block; }
+      .me { color: red; margin-right: 5px; }
+      .them { color: blue; margin-right: 5px; }
+    </style>
+
+    <div id="container">
+      <img src="" id="thumbnail">
+      <div id="chatarea">
+        <div id="chatlog"></div>
+        <input type="text" id="chatmessage" onkeyup="sendMessage(this, event);"></input>
+      </div>
+    </div>
+  ]]></Content>
+
+  <Content type="html" view="call"><![CDATA[
+    <script>
+      gadgets.util.registerOnLoadHandler(function() {
+        var person = gadgets.views.getParams();
+        if (!person) {
+
+        } else {
+	        if(gadgets.util.hasFeature('settitle')) {
+	          gadgets.window.setTitle('Chat with ' + person.displayName);
+	        }
+	        var url = gadgets.io.getProxyUrl(person.thumbnailUrl || 'http://www.gravatar.com/avatar?d=mm');
+	        document.getElementById('thumbnail').setAttribute('src', url);
+	        document.getElementById('callarea').innerHTML = 'Calling ' + person.displayName;
+        }
+
+        if (gadgets.util.hasFeature('dynamic-height')) {
+          gadgets.window.adjustHeight();
+        }
+      });
+    </script>
+
+    <style>
+      #container { padding-left: 110px; }
+      #thumbnail { width: 100px; height: 100px; margin-left: -110px; float: left; }
+      #callarea { width: 100%; }
+    </style>
+
+    <div id="container">
+      <img src="" id="thumbnail">
+      <div id="callarea"></div>
+    </div>
+  ]]></Content>
+</Module>
diff --git a/trunk/content/containers/conservcontainer/sample-selection-changer.xml b/trunk/content/containers/conservcontainer/sample-selection-changer.xml
new file mode 100644
index 0000000..8bb3334
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/sample-selection-changer.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Sample Selection Changer" height="250">
+    <Require feature="selection"/>
+  </ModulePrefs>
+  <Content type="html"><![CDATA[
+    <style>
+      #output {
+        word-wrap: break-word;
+      }
+    </style>
+    <script>
+      function setSelection(selection) {
+        selection = [{type:'com.example.food', dataObject: selection}];
+        gadgets.selection.setSelection(selection);
+        document.getElementById("output").innerHTML = 'selection: '
+         + gadgets.util.escapeString('' + gadgets.json.stringify(selection)) + '<br/>';
+      }
+    </script>
+    <div>
+      <input type="radio" name="group1" value="Milk" onclick="setSelection('Milk')"/> Milk<br>
+      <input type="radio" name="group1" value="Butter" onclick="setSelection('Butter')"/> Butter<br>
+      <input type="radio" name="group1" value="Cheese" onclick="setSelection('Cheese')"/> Cheese<br>
+      <input type="radio" name="group1" value="Water" onclick="setSelection('Water')"/> Water<br>
+      <input type="radio" name="group1" value="Beer" onclick="setSelection('Beer')"/> Beer<br>
+      <input type="radio" name="group1" value="Wine" onclick="setSelection('Wine')"/> Wine
+    </div>
+    <div id="output"></div>
+  ]]></Content>
+</Module>
diff --git a/trunk/content/containers/conservcontainer/sample-selection-listener.xml b/trunk/content/containers/conservcontainer/sample-selection-listener.xml
new file mode 100644
index 0000000..74374aa
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/sample-selection-listener.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Sample Selection Listener" height="250">
+    <Require feature="selection"></Require>
+  </ModulePrefs>
+  <Content type="html"><![CDATA[
+    <style>
+      #output {
+        word-wrap: break-word;
+        overflow-y: auto;
+      }
+    </style>
+    <script>
+      function selectionListener(/*Array*/ selection) {
+        var output = document.getElementById("output");
+        output.innerHTML = 'selection: '
+         + gadgets.util.escapeString('' + gadgets.json.stringify(selection))
+         + '<br/>' + output.innerHTML;
+      }
+
+      function addListener() {
+        gadgets.selection.addListener(selectionListener);
+        document.getElementById("output").innerHTML = "Selection listener added...";
+      }
+
+      function removeListener() {
+        gadgets.selection.removeListener(selectionListener);
+        document.getElementById("output").innerHTML = "";
+      }
+    </script>
+    <div>
+      <input type="button" value="Add Selection Listener" onclick="addListener()"/>
+      <input type="button" value="Remove Selection Listener" onclick="removeListener()"/>
+    </div>
+    <div id="output"></div>
+  ]]></Content>
+</Module>
diff --git a/trunk/content/containers/conservcontainer/sample-selection-query.xml b/trunk/content/containers/conservcontainer/sample-selection-query.xml
new file mode 100644
index 0000000..303f59d
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/sample-selection-query.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Sample Selection Query" height="250">
+    <Require feature="selection"/>
+  </ModulePrefs>
+  <Content type="html"><![CDATA[
+    <style>
+      #output {
+        word-wrap: break-word;
+      }
+    </style>
+    <script>
+      function querySelection() {
+        var selection = gadgets.selection.getSelection();
+        document.getElementById("output").innerHTML = 'selection: '
+         + gadgets.util.escapeString('' + gadgets.json.stringify(selection)) + '<br/>';
+      }
+    </script>
+    <div>
+      <input type="button" value="Query Current Selection" onclick="querySelection()"/>
+    </div>
+    <div id="output"></div>
+  ]]></Content>
+</Module>
diff --git a/trunk/content/containers/conservcontainer/viewController.js b/trunk/content/containers/conservcontainer/viewController.js
new file mode 100644
index 0000000..ad5e526
--- /dev/null
+++ b/trunk/content/containers/conservcontainer/viewController.js
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+$(function() {
+
+  // Base html template that is used for the gadget wrapper and site
+  var gadgetTemplate = '<div class="portlet">' +
+                '<div class="portlet-header" id="portlet-id">' +
+                'sample to replace</div>' +
+                '<div id="gadget-site" class="portlet-content"></div>' +
+                '</div>';
+
+  // Variable to keep track of gadget current view
+  // for collapse and expand gadget actions.
+  var currentView = 'default';
+
+  // ID used to associate gadget site
+  var curId = 0;
+
+  // Navigate to the new view and save it as current view
+  navigateView = function(gadgetSite, gadgetURL, toView) {
+    // Save the current view for collapse, expand gadget
+    currentView = toView;
+    CommonContainer.navigateView(gadgetSite, gadgetURL, toView);
+  };
+
+  // Handle gadget collapse, expand, and remove gadget actions
+  handleNavigateAction = function(portlet, gadgetSite, gadgetURL, actionId) {
+    // Remove button was click, remove the portlet/gadget
+    if (actionId === 'remove') {
+      if (confirm('This gadget will be removed, ok?')) {
+        if (gadgetSite) {
+          CommonContainer.closeGadget(gadgetSite);
+        }
+        if (gadgetURL) {
+          CommonContainer.unloadGadget(gadgetURL);
+          urlsToGadgetIdMap[gadgetURL] = null;
+        }
+        portlet.remove();
+      }
+    } else if (actionId === 'expand') {
+      // Navigate to currentView prior to collapse gadget
+      if (gadgetSite) {
+        CommonContainer.navigateView(gadgetSite, gadgetURL, currentView);
+      }
+      else {
+        preRenderGadget(gadgetURL);
+      }
+    } else if (actionId === 'collapse') {
+      CommonContainer.closeGadget(gadgetSite);
+    }
+  };
+
+  // Create a gadget with navigation tool bar header
+  // enabling gadget collapse, expand and remove.
+  window.buildGadget = function(result,gadgetURL,lazyload) {
+    var gadgetSiteString = "$(this).closest(\'.portlet\')." +
+      "find(\'.portlet-content\').data(\'gadgetSite\')";
+    var newGadgetSite = gadgetTemplate;
+    newGadgetSite = newGadgetSite.replace(/(portlet-id)/g, '$1-' + curId);
+    newGadgetSite = newGadgetSite.replace(/(gadget-site)/g, '$1-' + curId);
+    var gadgetSiteData = null;
+    $(newGadgetSite).appendTo($('#gadgetArea')).addClass(
+        'ui-widget ui-widget-content ui-helper-clearfix ui-corner-all')
+        .find('.portlet-header')
+        .addClass('ui-widget-header ui-corner-all')
+        .text(result[gadgetURL]['modulePrefs'].title)
+    .append('<span id="remove" class="ui-icon ui-icon-closethick"></span>')
+    .append('<span id="expand" class="ui-icon ui-icon-plusthick"></span>')
+    .append('<span id="collapse" class="ui-icon ui-icon-minusthick"></span>')
+    .end()
+      .find('.portlet-content')
+      .data('gadgetSite', lazyload ? null :
+        CommonContainer.renderGadget(gadgetURL, curId));
+
+    // determine which button was clicked and handle the appropriate event.
+    $('#portlet-id-'+ curId + ' > .ui-icon').click(
+        function() {
+          handleNavigateAction(
+              $(this).closest('.portlet'),
+              $(this).closest('.portlet').find('.portlet-content')
+                .data('gadgetSite'),
+              gadgetURL, this.id);
+        }
+    );
+
+    var gadgetId = urlsToGadgetIdMap[gadgetURL];
+    if (!gadgetId) {
+      urlsToGadgetIdMap[gadgetURL] = curId;
+    }
+  };
+
+  // Add single gadget
+  addGadget = function(gadgetUrl, lazyload) {
+    CommonContainer.preloadGadget(gadgetUrl, function(result) {
+      for (var gadgetURL in result) {
+       window.buildGadget(result, gadgetURL, lazyload);
+       curId++;
+      }
+    });
+    return true;
+  };
+
+});
diff --git a/trunk/content/containers/deprecated/container/Bridge.as b/trunk/content/containers/deprecated/container/Bridge.as
new file mode 100644
index 0000000..a43b363
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/Bridge.as
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+import flash.external.ExternalInterface;
+
+class com.google.Bridge extends MovieClip {
+  // The lcJS object stores a listening LocalConnection for each SWF on the page.
+  // These connections carry messages intentded for JavaScript code.
+  private var lcJS;
+  // We only need one LocalConnection for sending.
+  private var lcSWF;
+  
+  function log(msg) {
+    if (_root.logging) {
+      ExternalInterface.call('gadgets.log', msg);
+    }
+  }
+  
+  function callSWF(channel, methodName, argv) {
+    log('callSWF: ' + methodName + ', ' + lcSWF);
+    return {
+				channel: channel, 
+				methodName: methodName, 
+				argv: argv, 
+				result: lcSWF.send(channel + "swf", methodName, argv)
+			};
+  }
+    
+  function callJS(methodName, argv) {
+		log('callJS: ' + methodName);
+    return ExternalInterface.call("callJS", methodName, argv);
+  }
+  
+  function registerChannel(channel) {
+    log('registerChannel: ' + channel);
+    lcJS[channel] = new LocalConnection();
+    log('registerChannel: ' + lcJS[channel].connect(channel + "js"));
+    lcJS[channel].callJS = callJS;
+    lcJS[channel].allowDomain = function (domain) {
+      log('allowDomain: ' + domain);
+      return true;
+    }
+    return channel;
+  }
+  
+  // Fires when the movie loads  
+  function onLoad() {
+    lcJS = [];
+    lcSWF = new LocalConnection();
+
+    log('onLoad; domain = ' + lcSWF.domain());
+    ExternalInterface.addCallback('callSWF', this, callSWF);
+    ExternalInterface.addCallback('registerChannel', this, registerChannel);
+    ExternalInterface.call('onFlashBridgeReady');
+  }
+}
diff --git a/trunk/content/containers/deprecated/container/Bridge.fla b/trunk/content/containers/deprecated/container/Bridge.fla
new file mode 100644
index 0000000..f861dc2
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/Bridge.fla
Binary files differ
diff --git a/trunk/content/containers/deprecated/container/Bridge.swf b/trunk/content/containers/deprecated/container/Bridge.swf
new file mode 100644
index 0000000..87a927c
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/Bridge.swf
Binary files differ
diff --git a/trunk/content/containers/deprecated/container/cookiebaseduserprefstore.js b/trunk/content/containers/deprecated/container/cookiebaseduserprefstore.js
new file mode 100644
index 0000000..7e5e03f
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/cookiebaseduserprefstore.js
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Implements the gadgets.UserPrefStore interface using a cookies
+ * based implementation. Depends on cookies.js. This code should not be used in
+ * a production environment.
+ */
+
+/**
+ * Cookie-based user preference store.
+ * @constructor
+ */
+gadgets.CookieBasedUserPrefStore = function() {
+  gadgets.UserPrefStore.call(this);
+};
+
+gadgets.CookieBasedUserPrefStore.inherits(gadgets.UserPrefStore);
+
+gadgets.CookieBasedUserPrefStore.prototype.USER_PREFS_PREFIX =
+    'gadgetUserPrefs-';
+
+gadgets.CookieBasedUserPrefStore.prototype.getPrefs = function(gadget) {
+  var userPrefs = {};
+  var cookieName = this.USER_PREFS_PREFIX + gadget.id;
+  var cookie = shindig.cookies.get(cookieName);
+  if (cookie) {
+    var pairs = cookie.split('&');
+    for (var i = 0; i < pairs.length; i++) {
+      var nameValue = pairs[i].split('=');
+      var name = decodeURIComponent(nameValue[0]);
+      var value = decodeURIComponent(nameValue[1]);
+      userPrefs[name] = value;
+    }
+  }
+
+  return userPrefs;
+};
+
+gadgets.CookieBasedUserPrefStore.prototype.savePrefs = function(gadget) {
+  var pairs = [];
+  for (var name in gadget.getUserPrefs()) {
+    var value = gadget.getUserPref(name);
+    var pair = encodeURIComponent(name) + '=' + encodeURIComponent(value);
+    pairs.push(pair);
+  }
+
+  var cookieName = this.USER_PREFS_PREFIX + gadget.id;
+  var cookieValue = pairs.join('&');
+  shindig.cookies.set(cookieName, cookieValue);
+};
+
+gadgets.Container.prototype.userPrefStore =
+    new gadgets.CookieBasedUserPrefStore();
diff --git a/trunk/content/containers/deprecated/container/datauri_proxy.html b/trunk/content/containers/deprecated/container/datauri_proxy.html
new file mode 100644
index 0000000..9b14d2b
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/datauri_proxy.html
@@ -0,0 +1,69 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+  <head>
+    <title>Data URI via proxy, sample/test page</title>
+    <script>
+      function makeXhr() {
+        if (window.ActiveXObject) {
+          x = new ActiveXObject("Msxml2.XMLHTTP");
+          if (!x) {
+            x = new ActiveXObject("Microsoft.XMLHTTP");
+          }
+          return x;
+        }
+        else if (window.XMLHttpRequest) {
+          return new window.XMLHttpRequest();
+        }
+      }
+
+      function displayViaProxy() {
+        var uri = document.getElementById("contentUri").value;
+        var xhr = makeXhr();
+        xhr.open("GET", "/gadgets/proxy?output=js&container=default&url=" + encodeURIComponent(uri));
+        xhr.onreadystatechange = function() {
+          if (xhr.readyState != 4) {
+            return;
+          } else if (xhr.status != 200) {
+            alert("Error attempting to fetch through proxy: " + xhr.responseText);
+            return;
+          }
+          var responseText = xhr.responseText;
+          var jsonPiece = responseText.length > 100 ? responseText.substring(0, 100) + "..." : responseText;
+          document.getElementById("status").innerHTML = new Date().toString() + ": got data [" + jsonPiece + "]";
+          var json = eval('(' + responseText + ')');
+          document.getElementById("theimage").src = json.dataUri;
+        };
+        xhr.send(null);
+      }
+    </script>
+  </head>
+
+  <body>
+    This page demonstrates the use of the content proxy to retrieve data URI encoded content, by displaying a retrieved URI as an image.<br/><br/>
+    Content URI:
+    <script>
+      document.write('<input type="text" size="100" name="contentUri" id="contentUri" value="' + ("http://" + window.location.host + "/gadgets/rewriter/feather.png") + '"/>');
+    </script>
+    &nbsp;<input type="button" name="display" id="display" value="Display!" onclick="displayViaProxy();"/><br/><br/><hr/><br/>
+    <div id="status"></div>
+    <div><img id="theimage" src="" alt="[image will appear here]"/></div>
+  </body>
+
+</html>
diff --git a/trunk/content/containers/deprecated/container/gadgets.css b/trunk/content/containers/deprecated/container/gadgets.css
new file mode 100644
index 0000000..7159fda
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/gadgets.css
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+ 
+.gadgets-gadget-chrome {
+  float: left;
+  margin: 4px;
+  border: 1px solid #7aa5d6;
+}
+
+.gadgets-gadget {
+  border: none;
+}
+
+.gadgets-gadget-title-bar {
+  padding: 2px 4px;
+  background-color: #e5ecf9;
+}
+
+.gadgets-gadget-title {
+  font-weight: bold;
+  color: #3366cc;
+}
+
+.gadgets-gadget-title-button-bar {
+  font-size: smaller;
+}
+
+.gadgets-gadget-user-prefs-dialog {
+  background-color: #e5ecf9;
+}
+
+.gadgets-gadget-user-prefs-dialog-action-bar {
+  text-align: center;
+  padding-bottom: 4px;
+}
+
+.gadgets-gadget-title-button {
+}
+
+.gadgets-gadget-content {
+  padding: 4px;
+}
+
+.gadgets-log-entry {
+}
+
+/* Used to style messages produced during rewriting by CajaContentRewriter */
+.gadgets-messages {
+	
+}
diff --git a/trunk/content/containers/deprecated/container/payment-processor.html b/trunk/content/containers/deprecated/container/payment-processor.html
new file mode 100644
index 0000000..b52193a
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/payment-processor.html
@@ -0,0 +1,317 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: Payment Processor</title>
+
+<style>
+body, td, div, span, p {
+  font-family:arial,sans-serif;
+}
+body {
+  padding:0px;
+  margin:0px;
+}
+.payment-processor-shadow {
+  filter: alpha(opacity=30);
+  -moz-opacity:.3;
+  opacity:0.3;
+  background-color:#000;
+  width:690px;
+  height:390px;
+  margin:5px 0px 0px 5px;
+  position:absolute;
+  z-index:100;
+}
+.payment-processor-border1 {
+  background-color:#E5ECF9;
+  width:690px;
+  height:390px;
+  position:absolute;
+  z-index:200;
+}
+.payment-processor-border2 {
+  background-color:#FFF;
+  margin:5px;
+  height:380px;
+}
+.payment-processor-content {
+  padding:20px;
+  font-size:13px;
+}
+.payment-processor-content #loading-tab {
+  color:#777;
+}
+.caption {
+  font-weight:bold;
+  width:80px;
+  display:inline;
+}
+.desc {
+  color:#007F00;
+}
+.head {
+  font-weight:bold;
+}
+</style>
+
+
+<script type="text/javascript">
+
+/**
+ * @static
+ * @class A sample payment process panel provides the UI and logic for the real payment excution on
+ *        container api server. 
+ *
+ *        NOTE:
+ *
+ *          All functions or logics or names in this page are customizable. Indeed containers have
+ *          to customize them to make the UI consistent. This sample panel page is embeded in the 
+ *          parent container page as an iframe for better code structure, but indeed it is not 
+ *          necessary. It can be on the same page as container page.
+ *
+ *          You can implement their processor panel page using this file but replace the UI and 
+ *          mock codes, or use your completely own codes.  If you use your own page, just to make 
+ *          sure <code>shindig.paymentprocessor.initPayment</code> function is called with necessary
+ *          callbacks (open and close event handlers) passed in when initializing the page.
+ *
+ */
+var myProcessorPanel = (function() {
+
+  /** Element which holding this processor panel page in parent window. */
+  var parentDiv;
+
+  /** Just a reference to <code>shindig.paymentprocessor</code> object, which holding necessary 
+      parameters needed in the payment process */
+  var processor;
+
+  /**
+   * Called by <code>shindig.paymentprocessor</code> when the counter 
+   * panel is closing.
+   */
+  function closeEvent() {
+    // Set the div in the parent window to invisible.
+    parentDiv.style.display = 'none';
+  };
+
+
+  /**
+   * Draws the pay counter panel UI itself.
+   * (NOTE that this page is a iframe in its parent container window);
+   * Assigns the submit callback and cancel callback to the buttons.
+   * So from this panel, submit or cancel actions can be made.
+   *
+   * @param {Object} paymentJson The payment parameters.
+   * @param {Object} extraParams The extra parameters for the payment 
+   *                 procedure, including handler url, app title and spec.
+   * @param {Function} submitCallback The submit callback in 
+   *                   <code>shindig.paymentprocessor</code>.
+   * @param {Function} cancelCallback The cancel callback in 
+   *                   <code>shindig.paymentprocessor</code>.
+   */
+  function openEvent() {
+    // Set the UI.
+    document.getElementById('loading-tab').style.display = 'none';
+
+    document.getElementById('payment-appname').innerHTML = processor.getParam('appTitle');
+    document.getElementById('payment-appspec').innerHTML = processor.getParam('appSpec');
+    
+    document.getElementById('payment-type').innerHTML = processor.getParam('payment.paymentType');
+    document.getElementById('payment-amount').innerHTML = processor.getParam('payment.amount');
+    document.getElementById('payment-message').innerHTML = processor.getParam('payment.message');
+
+    var items = processor.getParam('payment.items');
+    if (items) {
+      var html = '<table border=1><tbody><tr class=head><td>SKU_ID</td><td>Price</td>' +
+                 '<td>Count</td><td>Description</td></tr>';
+      for (var i = 0; i < items.length; i++) {
+        html += '<tr>' + 
+            '<td>' + items[i]['skuId'] + '</td>' + 
+            '<td>' + items[i]['price'] + '</td>' + 
+            '<td>' + items[i]['count'] + '</td>' + 
+            '<td>' + items[i]['description'] + '</td>' + 
+            '</tr>';
+      }
+      html += '</tbody></table>';
+      document.getElementById('payment-items').innerHTML = html;
+    } else {
+      document.getElementById('payment-items').innerHTML = 'No detail items';
+    }
+
+    document.getElementById('payment-orderedtime').innerHTML = 
+        new Date(processor.getParam('payment.orderedTime')).toLocaleString();
+
+    if (processor.getParam('payment.paymentType') == 'credit') {
+      // If the payment type is 'credit', skip the confirm panel UI and 
+      // call the submitEvent directly.
+      window.setTimeout(submitHandler, 500);
+    } else {
+      // If the payment type is normal 'payment', add click listeners and 
+      // wait for user confirmation.
+      document.getElementById('button-tab').style.display = 'block';
+      document.getElementById('payment-submit').onclick = submitHandler;
+      document.getElementById('payment-cancel').onclick = cancelHandler;
+    }
+
+    // Set the div in the parent window to visible.
+    parentDiv.style.display = 'block';
+  };
+
+  /**
+   * Called by submit button clicked by the user.
+   *
+   * This function should send the pay request to container virtual currency
+   * api with Ajax POST.
+   *
+   * Then usually an acknowledge tab will be shown in the  with a button to
+   * call the callback function.
+   */
+  function submitHandler() {
+    document.getElementById('button-tab').style.display = 'none';
+    document.getElementById('loading-tab').style.display = 'block';
+
+
+    var requestData = processor.getParam('payment');
+    requestData['st'] = processor.getParam('stoken'); // or other security token
+
+
+
+
+    //////////////////////////////////////////////////////////////////////////////////////
+    // Here the logic should be on container sever with communication with app server.  //
+    // See the proposal doc Rivision#4.                                                 //
+    //////////////////////////////////////////////////////////////////////////////////////
+    var sendPaymentRequest = function(ajaxCallback) {
+      // The Server will communicate with App Backend Server then response.
+      // Here is just a fake call. You should replace these codes with actual ajax.
+      // Wait 1 second to simulate the network connection.
+      window.setTimeout(function() {
+        var responseData = {};
+        responseData['submittedTime'] = new Date().getTime();
+
+
+        // Do some fake check here. Can be any type of error during server-to-server roundtrips.
+        if (requestData['amount'] > 1000) {
+          responseData['responseCode'] = 'INSUFFICIENT_MONEY';
+          responseData['responseMessage'] = 'Fake not enough money response!';
+          ajaxCallback(responseData);
+          return;
+        }
+
+        // Simulate success response.
+        responseData['orderId'] = 'ORDER_ID_FROM_APP_' + Math.round(Math.random() * 10000);
+        responseData['executedTime'] = new Date().getTime();
+        responseData['responseCode'] = 'OK';
+        responseData['responseMessage'] = 'Fake success response!';
+        ajaxCallback(responseData);
+
+      }, 1000);
+    };
+    //////////////////////////////////////////////////////////////////////////////////////
+
+
+
+
+    // Send Ajax Call to Container Virtual Currency API Server.
+    sendPaymentRequest(function(responseData) {
+
+      processor.setParam('payment.responseCode', responseData['responseCode']);
+      processor.setParam('payment.responseMessage', responseData['responseMessage']);
+
+      if (responseData['responseCode'] == 'OK') {
+        // Copy the server generated fields back to processor parameters.
+        processor.setParam('payment.submittedTime', responseData['submittedTime']);
+        processor.setParam('payment.executedTime', responseData['executedTime']);
+        processor.setParam('payment.orderId', responseData['orderId']);
+      }
+
+      // Close the processor panel and return to app.
+      processor.closePayment();
+    });  
+  };
+
+
+  /**
+   * Invoked when cancel button clicked by user.
+   */
+  function cancelHandler() {
+    // You can also show a message to say the order is canceled.
+    // Here just call the callback and return.
+    processor.setParam('payment.responseCode', 'USER_CANCELLED');
+    processor.closePayment();
+  };
+
+
+
+  return {
+    /**
+     * Initializes the counter module. It can be called by this page's <code>body.onload()</code> 
+     * function or in other initializing steps.
+     * Note the <code>shindig.paymentprocessor</code> object is passed from the parent window.
+     */
+    init: function() {
+      // Store the parent node in which there is an iframe holding this page.
+      parentDiv = window.frameElement.parentNode;
+
+      processor = parent.shindig.paymentprocessor;
+
+      // Initialize the paymentprocessor module with four events.
+      // The container need to fully implement these event functions for
+      // UI/Backend interaction.
+      processor.initPayment(openEvent, closeEvent);
+    }
+  };
+
+})();
+
+</script>
+</head>
+<body onload="myProcessorPanel.init();">
+  <!-- Customize the UI -->
+  <div class="payment-processor-shadow"></div>
+  <div class="payment-processor-border1">
+    <div class="payment-processor-border2">
+      <div class="payment-processor-content">
+        <p class="desc">
+          This panel is in an iframe from another page in the same container domain:<br>
+          <b><script>document.write(window.location.href);</script></b>
+        </p>
+        <div class="caption">App Name: </div><span id="payment-appname"></span><br>
+        <div class="caption">App Spec: </div><span id="payment-appspec"></span><br>
+        <br>
+        <div class="caption">Payment Type: </div><span id="payment-type"></span><br>
+        <div class="caption">Amount: </div><span id="payment-amount"></span><br>
+        <div class="caption">Message: </div><span id="payment-message"></span><br>
+        <div class="caption">Items: </div><br><div id="payment-items"></div>
+        <div class="caption">Ordered Time: </div><span id="payment-orderedtime"></span><br>
+        <br>
+        <div id="button-tab" style="display:none;">
+          <button id="payment-submit">Submit</button>
+          <button id="payment-cancel">Cancel</button>
+        </div>
+        <div id="loading-tab" style="display:none">
+          Please wait...
+        </div>
+      </div>
+    </div>
+  </div>
+</body>
+</html>
+
diff --git a/trunk/content/containers/deprecated/container/payment-records-processor.html b/trunk/content/containers/deprecated/container/payment-records-processor.html
new file mode 100644
index 0000000..23169d8
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/payment-records-processor.html
@@ -0,0 +1,406 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: Payment Records Processor</title>
+
+<style>
+body, td, div, span, p {
+  font-family:arial,sans-serif;
+}
+body {
+  padding:0px;
+  margin:0px;
+}
+.payment-processor-shadow {
+  filter: alpha(opacity=30);
+  -moz-opacity:.3;
+  opacity:0.3;
+  background-color:#000;
+  width:690px;
+  height:390px;
+  margin:5px 0px 0px 5px;
+  position:absolute;
+  z-index:100;
+}
+.payment-processor-border1 {
+  background-color:#E5ECF9;
+  width:690px;
+  height:390px;
+  position:absolute;
+  z-index:200;
+}
+.payment-processor-border2 {
+  background-color:#FFF;
+  margin:5px;
+  height:380px;
+}
+.payment-processor-content {
+  padding:20px;
+  font-size:13px;
+}
+.payment-processor-content #loading-tab {
+  color:#777;
+}
+.caption {
+  font-weight:bold;
+  width:80px;
+  display:inline;
+}
+.desc {
+  color:#007F00;
+}
+.head {
+  font-weight:bold;
+}
+</style>
+
+
+<script type="text/javascript">
+
+/**
+ * @static
+ * @class A sample records processor panel provides the UI and logic for the real records fetching
+ *        and fixing requests to container api server. 
+ *
+ *        NOTE:
+ *
+ *          All functions or logics or names in this page are customizable. Indeed containers have
+ *          to customize them to make the UI consistent. This sample panel page is embeded in the 
+ *          parent container page as an iframe for better code structure, but indeed it is not 
+ *          necessary. It can be on the same page as container page.
+ *
+ *          You can implement their processor panel page using this file but replace the UI and 
+ *          mock codes, or use your completely own codes.  If you use your own page, just to make 
+ *          sure <code>shindig.paymentprocessor.initPaymentRecords</code> function is called with 
+ *          necessary callbacks (open and close event handlers) passed in when initializing the 
+ *          page.
+ *
+ */
+var myRecordsProcessorPanel = (function() {
+
+  /** Element which holding this processor panel page in parent window. */
+  var parentDiv;
+
+  /** Just a reference to <code>shindig.paymentprocessor</code> object, which holding necessary 
+      parameters needed in the payment process */
+  var processor;
+
+  /**
+   * Called by <code>shindig.paymentprocessor</code> when the counter 
+   * panel is closing.
+   */
+  function closeEvent() {
+    // Set the div in the parent window to invisible.
+    parentDiv.style.display = 'none';
+  };
+
+
+  /**
+   * Draws the pay counter panel UI itself.
+   * (NOTE that this page is a iframe in its parent container window);
+   * Assigns the submit callback and cancel callback to the buttons.
+   * So from this panel, submit or cancel actions can be made.
+   *
+   * @param {Object} paymentJson The payment parameters.
+   * @param {Object} extraParams The extra parameters for the payment 
+   *                 procedure, including handler url, app title and spec.
+   * @param {Function} submitCallback The submit callback in 
+   *                   <code>shindig.paymentprocessor</code>.
+   * @param {Function} cancelCallback The cancel callback in 
+   *                   <code>shindig.paymentprocessor</code>.
+   */
+  function openEvent() {
+    // Set the div in the parent window to visible.
+    parentDiv.style.display = 'block';
+    document.getElementById('payment-appname').innerHTML = processor.getParam('appTitle');
+    document.getElementById('payment-appspec').innerHTML = processor.getParam('appSpec');
+
+    document.getElementById('payment-records-close').onclick = cancelHandler;
+
+    // The requestData is going to post to server.
+    var requestData = {
+      'appSpec' : processor.getParam('appSpec'),
+      'appTitle' : processor.getParam('appTitle'),
+      'st' : processor.getParam('stoken'),       // or other security token if needed
+
+      'params' : processor.getParam('reqParams'),
+      'sandbox': processor.getParam('reqParams.sandbox')
+    };
+
+    //////////////////////////////////////////////////////////////////////////////////////
+    // Here the logic should be on container sever for fetching payment records.        //
+    // See the proposal doc Rivision#4.                                                 //
+    //////////////////////////////////////////////////////////////////////////////////////
+    var sendFetchPaymentRecordsRequest = function(ajaxCallback) {
+      // The Server will fetch data from it's own database then response.
+      // Here is just a fake call. You should replace these codes with actual ajax.
+      // Wait 1 second to simulate the network connection.
+      window.setTimeout(function() {
+        // Get the payment records in database by querying with appSpec. Here uses mock data.
+        var mockData = [
+          {
+            'orderId' : 'ORDER_ID_FROM_APP_' + Math.round(Math.random() * 10000),
+            'items': [
+              {'skuId':'1234', 'price':'10', 'count': 5, 'description':'this is fake.'},
+              {'skuId':'2345', 'price':'11', 'count': 7, 'description':'this is fake2.'}
+            ],
+            'amount': 127,
+            'message': 'Fake message',
+            'paymentType': 'payment',
+            'orderedTime': new Date().getTime(),
+            'submittedTime': new Date().getTime(),
+            'executedTime': new Date().getTime(),
+            'responseCode': 'OK',
+            'responseMessage': 'Payment done.',
+            'paymentComplete': true,
+            'sandbox': !!requestData['sandbox']
+          }, {
+            'orderId' : 'ORDER_ID_FROM_APP_' + Math.round(Math.random() * 10000),
+            'items': [
+              {'skuId':'3456', 'price':'5', 'count': 30, 'description':'this is fake3.'},
+              {'skuId':'6789', 'price':'100', 'count': 1, 'description':'this is fake4.'}
+            ],
+            'amount': 250,
+            'message': 'Fake message2',
+            'paymentType': 'payment',
+            'orderedTime': new Date().getTime(),
+            'submittedTime': new Date().getTime(),
+            'executedTime': new Date().getTime(),
+            'responseCode': 'APP_LOGIC_ERROR',
+            'responseMessage': 'Payment failed on app.',
+            'paymentComplete': false,
+            'sandbox': !!requestData['sandbox']
+          }, {
+            'orderId' : 'ORDER_ID_FROM_APP_' + Math.round(Math.random() * 10000),
+            'items': [
+              {'skuId':'abcd', 'price':'3', 'count': 3, 'description':'this is fake5.'},
+              {'skuId':'efgh', 'price':'12', 'count': 4, 'description':'this is fake6.'}
+            ],
+            'amount': 57,
+            'message': 'Fake message3',
+            'paymentType': 'payment',
+            'orderedTime': new Date().getTime(),
+            'submittedTime': new Date().getTime(),
+            'executedTime': new Date().getTime(),
+            'responseCode': 'PAYMENT_ERROR',
+            'responseMessage': 'Payment failed on container.',
+            'paymentComplete': false,
+            'sandbox': !!requestData['sandbox']
+          }
+        ];
+
+        var max = Number(requestData['params']['max']);
+        if (!max) {
+          max = 3 // If not set or incorrectly set, set default value.
+        }
+
+        ajaxCallback(mockData.slice(0, max));
+      }, 1000);
+    };
+    ////////////////////////////////////////////////////////////////////////////////////
+
+
+
+
+
+    // Send ajax request
+    document.getElementById('loading-tab').style.display = 'block';
+
+    sendFetchPaymentRecordsRequest(function(responseData) {
+      document.getElementById('loading-tab').style.display = 'none';
+
+      var records = processor.getParam('records.payments');
+
+      var incompleteIds = [];
+
+      // Generate the payment records table UI
+      var html = '<table border=1><tbody><tr class="head">' +
+          '<td>Amount</td><td>Message</td><td>SubmittedTime</td>' + 
+          '<td>ResponseCode</td><td>ExecutedTime</td>'+ 
+          '</tr>';
+      for (var i = 0; i < responseData.length; i++) {
+        var paymentJson = responseData[i];
+        var orderId = paymentJson['orderId'];
+        html += '<tr><td>' + paymentJson['amount'] + '</td>' +
+            '<td>' + paymentJson['message'] + '</td>' +
+            '<td>' + new Date(paymentJson['submittedTime']).toLocaleString() + '</td>' +
+            '<td>' + paymentJson['responseCode'] + '</td>' +
+            '<td id=\'td_' + orderId + '\'>';
+        if (!paymentJson['paymentComplete']) {
+          // Show a 'FixIt' button for non-complete payment with ID equals orderId.
+          html += '<button id=\'' + orderId + '\'>FixIt</button>';
+          // Add the incompletes to records.
+          records[orderId] = paymentJson;
+          incompleteIds.push(orderId);
+        } else {
+          html += new Date(paymentJson['executedTime']).toLocaleString();
+        }
+        html += '</td></tr>';
+      }
+      html += '</tbody></table>';
+      document.getElementById('payment-records').innerHTML = html;
+
+      // Assign onclick handler's for incomplete payments.
+      for (var j = 0; j < incompleteIds.length; j++) {
+        document.getElementById(incompleteIds[j]).onclick = submitHandler;
+      }
+    });
+    
+  };
+
+  /**
+   * Called by submit button clicked by the user.
+   *
+   * This function should send the payment fixing request to container virtual currency
+   * api with Ajax POST.
+   */
+  function submitHandler() {
+    var orderId = this.id;
+    var requestData = {
+      'appSpec' : processor.getParam('appSpec'),
+      'appTitle' : processor.getParam('appTitle'),
+      'st' : processor.getParam('stoken'),       // or other security token
+
+      'orderId' : orderId,
+      'sandbox': processor.getParam('reqParams.sandbox')
+    };
+
+    //////////////////////////////////////////////////////////////////////////////////////
+    // Here the logic should be on container sever for updating an incomplete payment.  //
+    // See the proposal doc Rivision#4.                                                 //
+    //////////////////////////////////////////////////////////////////////////////////////
+    var sendFixPaymentRecordRequest = function(ajaxCallback) {
+      // The Server will communicate with App Backend Server then response.
+      // Here is just a fake call.
+      // Wait 1 second to simulate the network connection.
+      window.setTimeout(function() {
+        // let say it will always succeed.
+        var responseData = {};
+
+        // Simulate success response.
+        responseData['paymentComplete'] = true;
+        responseData['executedTime'] = new Date().getTime();
+        responseData['responseCode'] = 'OK';
+        if (!!requestData['sandbox']) {
+          responseData['responseMessage'] = 'Fake success response in sandbox!';
+        } else {
+          responseData['responseMessage'] = 'Fake success response!';
+        }
+        
+        ajaxCallback(responseData);
+      }, 1000);
+    };
+    ///////////////////////////////////////////////////////////////////////////////////// 
+
+
+
+
+
+    // Send ajax request
+    document.getElementById('loading-tab').style.display = 'block';
+
+    sendFixPaymentRecordRequest(function(responseData) {
+      document.getElementById('loading-tab').style.display = 'none';
+
+      if (responseData['responseCode'] != 'OK') {
+        // something fail, display exception message and let user try again.
+        document.getElementById(orderId).innerHTML = 'Try again';
+        return;
+      }
+
+      // If fixing request succeeded, replay the button with payment executed time.
+      document.getElementById('td_' + orderId).innerHTML = 
+          new Date(responseData['executedTime']).toLocaleString();
+
+      // Updates the payment json object in the records.
+      var paymentJson = processor.getParam('records.payments.' + orderId);
+      paymentJson['paymentComplete'] = true;
+      paymentJson['executedTime'] = responseData['executedTime'];
+      paymentJson['responseCode'] = responseData['responseCode'];
+      paymentJson['responseMessage'] = responseData['responseMessage'];
+
+    });
+
+  };
+
+  /**
+   * Invoked when cancel button clicked by user. Closes the processor.
+   */
+  function cancelHandler() {
+    // You can also show a message to say the order is canceled.
+    // Here just call the callback and return.
+
+    processor.setParam('records.responseCode', 'OK');
+
+    processor.closePaymentRecords();
+  };
+
+  return {
+  
+    /**
+     * Initializes the counter module. It can be called by this page's <code>body.onload()</code> 
+     * function or in other initializing steps.
+     * Note the <code>shindig.paymentprocessor</code> object is passed from the parent window.
+     */
+    init: function() {
+      // Store the parent node in which there is an iframe holding this page.
+      parentDiv = window.frameElement.parentNode;
+
+      processor = parent.shindig.paymentprocessor;
+
+      // Initialize the paymentprocessor module with four events.
+      // The container need to fully implement these event functions for
+      // UI/Backend interaction.
+      processor.initPaymentRecords(openEvent, closeEvent);
+    }
+
+  };
+})();
+
+</script>
+</head>
+<body onload="myRecordsProcessorPanel.init();">
+  <!-- Customize the UI -->
+  <div class="payment-processor-shadow"></div>
+  <div class="payment-processor-border1">
+    <div class="payment-processor-border2">
+      <div class="payment-processor-content">
+        <p class="desc">
+          This panel is in an iframe from another page in the same container domain:<br>
+          <b><script>document.write(window.location.href);</script></b>
+        </p>
+
+        <div class="caption">App Name: </div><span id="payment-appname"></span><br>
+        <div class="caption">App Spec: </div><span id="payment-appspec"></span><br>
+
+        <p id="payment-records"></p>
+
+        <div id="button-tab">
+          <button id="payment-records-close">Close</button>
+        </div>
+
+        <div id="loading-tab" style="display:none">Please wait...</div>
+      </div>
+    </div>
+  </div>
+</body>
+</html>
+
diff --git a/trunk/content/containers/deprecated/container/rpc_relay.html b/trunk/content/containers/deprecated/container/rpc_relay.html
new file mode 100644
index 0000000..d04d130
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/rpc_relay.html
@@ -0,0 +1,27 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<script>
+var u = location.href, h = u.substr(u.indexOf('#') + 1).split('&'), t, r;
+try {
+  t = h[0] === '..' ? parent.parent : parent.frames[h[0]];
+  r = t.gadgets.rpc.receive;
+} catch (e) {
+}
+r && r(h, window);
+</script>
diff --git a/trunk/content/containers/deprecated/container/rpc_relay.uncompressed.html b/trunk/content/containers/deprecated/container/rpc_relay.uncompressed.html
new file mode 100644
index 0000000..dd4544a
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/rpc_relay.uncompressed.html
@@ -0,0 +1,32 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<script>
+var url = location.href,
+    hashParams = url.substr(url.indexOf('#') + 1).split('&'),
+    targetFrame, receive;
+
+try {
+  targetFrame = hashParams[0] === '..' ? parent.parent
+                                       : parent.frames[hashParams[0]];
+  receive = targetFrame.gadgets.rpc.receive;
+} catch (e) {
+}
+
+receive && receive(hashParams, window);
+</script>
diff --git a/trunk/content/containers/deprecated/container/rpctest_childgadget.xml b/trunk/content/containers/deprecated/container/rpctest_childgadget.xml
new file mode 100644
index 0000000..e672fa2
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/rpctest_childgadget.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="gadgets.rpc Performance/correctness tests: gadget-in-a-gadget">
+    <Require feature="rpc"/>
+  </ModulePrefs>
+  <Content type="html">
+  <![CDATA[
+    <script>
+      function callGadgetServicePing() {
+        gadgets.rpc.call(null, 'gadget_service_ping');
+      }
+    </script>
+    <div>gadget-in-a-gadget</div><hr/>
+    <div><input type="button" value="Ping Parent Gadget" onclick="callGadgetServicePing();" /></div>
+  ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/containers/deprecated/container/rpctest_container.html b/trunk/content/containers/deprecated/container/rpctest_container.html
new file mode 100644
index 0000000..7106fd9
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/rpctest_container.html
@@ -0,0 +1,169 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+
+<!--
+  Simple page for testing gadgets.rpc performance.
+  Allows you to add a simulated "gadget" (in this case just a static
+  HTML page which loads gadgets.rpc also), and pass some
+  specified number of random messages of specified size to
+  and from it.
+
+  To use, start up two instances of
+  the Shindig Gadgets Server on two separate ports to test
+  "real" cross-domain communication, since port is factored
+  into the same-domain policy enforced by browsers. One
+  server will be used for the container, the other for the gadget.
+
+  If your container on localhost:8080 with gadget on localhost:8081,
+  then to load the test load the following in your browser:
+  http://localhost:8080/container/rpctest_container.html?localhost:8081
+
+  This test container/gadget pair is configurable via two other options:
+  * &gadgetdeferred=1 - tests early-message queueing from container to
+    gadget, by preventing attachment of gadget until a button is pressed.
+  * &uabackward=1 - tests "incorrect" rpc.js setup in container:
+    gadgets.rpc.setAuthToken(...) called before, not after, gadget frame exists.
+
+  These options may be provided in any combination.
+-->
+<html>
+  <head>
+    <title>gadgets.rpc Performance Tests: Container</title>
+    <script>
+      // Configurable test harness options.
+      var gadgethost = 'http://' + window.location.search.substring(1).split('&')[0];
+      var gadgetdeferred = window.location.search.indexOf('&gadgetdeferred=1') !== -1;
+      var uabackward = window.location.search.indexOf('&uabackward=1') !== -1;
+      var flash = window.location.search.indexOf('&rpctx=flash') !== -1;
+      var rmr = window.location.search.indexOf('&rpctx=rmr') !== -1;
+
+      // Useful per-page variables.
+      var gadgeturl = gadgethost + '/container/rpctest_gadget.xml';
+      var gadgetrelay = gadgethost + '/container/rpc_relay.uncompressed.html';
+      </script>
+      <script language="JavaScript" type="text/javascript" src="/gadgets/js/rpc.js?c=1&nocache=1&debug=1"></script>
+      <script>
+      var gadgetrenderingurl = gadgethost + '/gadgets/ifr?url=' + gadgeturl + '&libs=rpc&parent=' + window.location.protocol + '//' + window.location.host + '&debug=1&nocache=1&' + (flash ? '&rpctx=flash' : '') + (rmr ? '&rpctx=rmr' : '');
+      </script>
+      <script language="JavaScript" type="text/javascript" src="/container/rpctest_perf.js"></script>
+    </script>
+    <!-- need a script break to allow rpc.js to load before calling referenced methods -->
+    <script>
+      // gadgets.rpc "service" that receives a message sent before body onload.
+      function handleInitialGadgetMessage(message) {
+        var status = document.getElementById('initconsole');
+        status.innerHTML = 'gadget says: ' + message;
+      }
+      gadgets.rpc.register('initial_gadget_message', handleInitialGadgetMessage);
+
+      function handleGadgetServicePing() {
+        var childping = document.getElementById('childping');
+        var pingval = childping.innerHTML;
+        pingval++;
+        childping.innerHTML = pingval;
+      }
+      gadgets.rpc.register('gadget_service_ping', handleGadgetServicePing);
+    
+      function appendGadget() {
+        var secret = Math.round(Math.random() * 10000000);
+        var renderUrl = gadgetrenderingurl + '#rpctoken=' + secret;
+        var container = document.getElementById("container");
+
+        // Rendering about:blank first seems to fix bfcache issue (mismatched rpc tokens)
+        var iframeHtml = "<iframe id='gadget' name='gadget' height=400 width=800 src='about:blank'></iframe>";
+        if (uabackward) {
+          // incorrect but likely widely used
+          gadgets.rpc.setRelayUrl('gadget', gadgetrelay);
+          gadgets.rpc.setAuthToken('gadget', secret);
+          container.innerHTML = iframeHtml;
+          document.getElementById('gadget').src = renderUrl;
+        } else {
+          // "correct" way.
+          container.innerHTML = iframeHtml;
+          document.getElementById('gadget').src = renderUrl;
+          gadgets.rpc.setupReceiver('gadget'); // use the new init API, which parses out all needed variables.
+        }
+      }
+
+      function initTestContainer() {
+        if (!gadgetdeferred) {
+          appendGadget();
+        } else {
+          document.getElementById('showgadget').style.display = '';
+        }
+
+        document.getElementById('relaymethod').innerHTML = gadgets.rpc.getRelayChannel();
+
+        // Method called from rpctest_perf.js
+        initPerfTest();
+      };
+    </script>
+  </head>
+  <body style="background-color: #cccccc" onload="initTestContainer();">
+    <div>gadgets.rpc Performance: Container Page (method: <span id="relaymethod"></span>)</div><hr/>
+    <div>Initial gadget render console: <span id="initconsole">Gadget hasn't commented yet.</span></div>
+    <div>Child gadget ping count: <span id="childping">0</span></div><hr/>
+    <div>Test<br/>
+      <ul>
+        <li>Number of messages to send:
+          <select name="num_msgs" id="num_msgs">
+            <option value="1" selected>1</option>
+            <option value="10">10</option>
+            <option value="100">100</option>
+            <option value="1000">1000</option>
+          </select>
+        </li>
+        <li>Message size:
+          <select name="msg_size" id="msg_size">
+            <option value="10">10 B</option>
+            <option value="100">100 B</option>
+            <option value="1024" selected>1 kB</option>
+            <option value="10240">10 kB</option>
+            <option value="102400">100 kB</option>
+            <option value="1048576">1 MB</option>
+          </select>
+        </li>
+        <li>
+          <input type="button" value="Start The Test!" onclick="runPerfTest('gadget');" />
+        </li>
+      </ul>
+    </div>
+    <div id="test_running" style="display:none;">
+      Running test...
+    </div>
+    <div id="results" style="display:none;">
+      Results: Gadget-to-Container<br/>
+      Messages: <span id="results_num_received"></span>, Bytes: <span id="results_bytes_received"></span> <span id="in_or_out"></span><br/>
+      Time elapsed for test run: <span id="results_time_used"></span><br/>
+      Messages/second: <span id="results_msgs_per_sec"></span><br/>
+      Bytes/second: <span id="results_bytes_per_sec"></span><br/>
+      Referrer: <span id="results_referrer"></span>
+    </div>
+    <hr/>
+    <div>Callback<br/>
+      <ul>
+        <li>Input: <input type="text" value="test-value" size="20" id="echo_test_input"/> <input type="button" value="Sync Callback Test" onclick="runCallbackTest('gadget',true);"/> <input type="button" value="Async Callback Test" onclick="runCallbackTest('gadget',false);"/></li>
+        <li>Result: <span id="echo_test_result"></span></li>
+      </ul>
+    </div>
+    <hr/>
+    <div>Gadget: <span id="showgadget" style="display:none"><input type="button" onclick="appendGadget(); this.style.display='none';" value="Append Gadget Now (for delayed load testing)"/></span></div>
+    <div id="container"></div>
+  </body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/rpctest_gadget.xml b/trunk/content/containers/deprecated/container/rpctest_gadget.xml
new file mode 100644
index 0000000..807a79d
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/rpctest_gadget.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="gadgets.rpc Performance/correctness tests: Gadget">
+    <Require feature="rpc"/>
+  </ModulePrefs>
+  <Content type="html">
+  <![CDATA[
+    <script>
+      // Pre-onload message send.
+      gadgets.rpc.call(null, 'initial_gadget_message', null, 'Hello there! Pre-onload message sent successfully.');
+    
+      var cachebust = 'cachebust=1';// + Math.random();
+      document.write('<scr' + 'ipt language="JavaScript" type="text/javascript" src="/container/rpctest_perf.js?' + cachebust + '"></scr' + 'ipt>');
+    </script>
+    <script>
+      // Register ping handler.
+      gadgets.rpc.register('gadget_service_ping', function() {
+        var queryConsole = document.getElementById('queryconsole');
+        var numQueries = queryConsole.innerHTML;
+        ++numQueries;
+        queryConsole.innerHTML = numQueries;
+        var whoAsked = document.getElementById('whoasked');
+        whoAsked.innerHTML = this.f;
+      });
+
+      function appendChildGadget() {
+        var childgadgetdiv = document.getElementById('childgadgetdiv');
+        var parentDomain = gadgets.rpc.getOrigin(gadgets.util.getUrlParameters().parent);
+        var myDomain = gadgets.rpc.getOrigin(window.location.href);
+        var rpctoken = Math.round(Math.random() * 10000000);
+        var rpctx = gadgets.util.getUrlParameters()["rpctx"] || "";
+        var childGadgetUrl = parentDomain + '/gadgets/ifr?url=' + parentDomain + '/container/rpctest_childgadget.xml&parent=' + myDomain + '&libs=rpc&debug=1&rpctx=' + rpctx + '#rpctoken=' + rpctoken;
+        childgadgetdiv.innerHTML = '<div><input type="button" value="Ping Parent (Container)" onclick="callGadgetServicePing();"/><hr/>' +
+          '<div>Who-am-I query count: <span id="queryconsole">0</span>, last q from: <span id="whoasked"></span></div>' +
+          '<div><iframe id="childgadget" name="childgadget" height=100 width=200 src="about:blank"></iframe></div>';
+        document.getElementById("childgadget").src = childGadgetUrl;
+        gadgets.rpc.setRelayUrl('childgadget', parentDomain);
+        gadgets.rpc.setAuthToken('childgadget', rpctoken);
+      }
+
+      function callGadgetServicePing() {
+        gadgets.rpc.call(null, 'gadget_service_ping');
+      }
+
+      // Initialize performance test onLoad.
+      gadgets.util.registerOnLoadHandler(initPerfTest);
+    </script>
+    <div>gadgets.rpc Performance: "Gadget" page</div><hr/>
+    <script>
+      document.write("<div>Parent relay: " + gadgets.rpc.getRelayUrl('..') + "<br/>method: " + gadgets.rpc.getRelayChannel() + "</div><hr/>");
+    </script>
+    <div>Test<br/>
+      <ul>
+        <li>Number of messages to send:
+          <select name="num_msgs" id="num_msgs">
+            <option value="1" selected>1</option>
+            <option value="10">10</option>
+            <option value="100">100</option>
+            <option value="1000">1000</option>
+          </select>
+        </li>
+        <li>Message size:
+          <select name="msg_size" id="msg_size">
+            <option value="10">10 B</option>
+            <option value="100">100 B</option>
+            <option value="1024" selected>1 kB</option>
+            <option value="10240">10 kB</option>
+            <option value="102400">100 kB</option>
+            <option value="1048576">1 MB</option>
+          </select>
+        </li>
+        <li>
+          <input type="button" value="Start The Test!" onclick="runPerfTest();" />
+        </li>
+      </ul>
+    </div>
+    <div id="test_running" style="display:none;">
+      Running test...
+    </div>
+    <div id="results" style="display:none;">
+      Results: Gadget-to-Container<br/>
+      Messages: <span id="results_num_received"></span>, Bytes: <span id="results_bytes_received"></span> <span id="in_or_out"></span><br/>
+      Time elapsed for test run: <span id="results_time_used"></span><br/>
+      Messages/second: <span id="results_msgs_per_sec"></span><br/>
+      Bytes/second: <span id="results_bytes_per_sec"></span><br/>
+      Referrer: <span id="results_referrer"></span>
+    </div><hr/>
+    <div>Callback<br/>
+      <ul>
+        <li>Input: <input type="text" value="test-value" size="20" id="echo_test_input"/> <input type="button" value="Sync Callback Test" onclick="runCallbackTest(null,true);"/> <input type="button" value="Async Callback Test" onclick="runCallbackTest(null,false);"/></li>
+        <li>Result: <span id="echo_test_result"></span></li>
+      </ul>
+    </div><hr/>
+    <div id="childgadgetdiv"><input type="button" value="Child Gadget Tests" onclick="appendChildGadget();"/></div>
+  ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/containers/deprecated/container/rpctest_perf.js b/trunk/content/containers/deprecated/container/rpctest_perf.js
new file mode 100644
index 0000000..0779ccc
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/rpctest_perf.js
@@ -0,0 +1,137 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+var perfStats = null;
+var currentRun = {};
+
+function perfService(message) {
+  if (perfStats.numResults++ === 0) {
+    perfStats.firstMsg = message; // stored since it has "real" start time
+  }
+  perfStats.bytesReceived += message.length;
+}
+
+function clearPerfStats(inOrOut) {
+  perfStats = {
+    numResults: 0,
+    bytesReceived: 0,
+    firstMsg: null
+  };
+
+  document.getElementById('in_or_out').innerHTML = inOrOut;
+
+  // hide results fields
+  document.getElementById('results').style.display = 'none';
+}
+
+function completePerfStats() {
+  perfStats.timeEnded = new Date().getTime();
+
+  // get time started from the first sent message
+  perfStats.timeStarted = perfStats.firstMsg.substr(0, perfStats.firstMsg.indexOf(':'));
+
+  var timeUsedMs = perfStats.timeEnded - perfStats.timeStarted;
+
+  // fill in fields
+  document.getElementById('results_num_received').innerHTML = perfStats.numResults;
+  document.getElementById('results_bytes_received').innerHTML = perfStats.bytesReceived;
+  document.getElementById('results_time_used').innerHTML = timeUsedMs + 'ms';
+  document.getElementById('results_msgs_per_sec').innerHTML = (perfStats.numResults / (timeUsedMs / 1000));
+  document.getElementById('results_bytes_per_sec').innerHTML = (perfStats.bytesReceived / (timeUsedMs / 1000));
+  document.getElementById('results_referrer').innerHTML = (this['referer'] || 'n/a') + ' -- config: ' +
+      (gadgets.config.get('rpc')['passReferrer'] || '<empty>');
+  document.getElementById('test_running').style.display = 'none';
+  document.getElementById('results').style.display = '';
+}
+
+function syncCallbackService(toEcho) {
+  return toEcho;
+}
+
+function asyncCallbackService(toEcho) {
+  var self = this;
+  window.setTimeout(function() {
+    self.callback(toEcho);
+  }, 0);
+}
+
+function initPerfTest() {
+  clearPerfStats();
+  gadgets.rpc.register('perf_service', perfService);
+  gadgets.rpc.register('clear_perf_stats', clearPerfStats);
+  gadgets.rpc.register('complete_perf_stats', completePerfStats);
+  gadgets.rpc.register('sync_callback_service', syncCallbackService);
+  gadgets.rpc.register('async_callback_service', asyncCallbackService);
+}
+
+var alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 _-*&(){}'";
+
+function sendPerfMessage() {
+  var msgToSend = currentRun.msg;
+  if (currentRun.curMsgId++ <= 1) {
+    var nowString = new Date().getTime() + ':';
+    msgToSend = nowString + currentRun.msg.substring(nowString.length);
+  }
+
+  gadgets.rpc.call(currentRun.targetId, 'perf_service', null, msgToSend);
+  if (currentRun.curMsgId < currentRun.endMsgId) {
+    // loop, giving up execution in case rpc technique demands it
+    window.setTimeout(sendPerfMessage, 0);
+  } else {
+    // send finisher
+    window.setTimeout(function() { gadgets.rpc.call(currentRun.targetId, 'complete_perf_stats', null); }, 0);
+  }
+}
+
+function runPerfTest(targetId) {
+  document.getElementById('test_running').style.display = '';
+
+  // initialize the current run
+  var num_msgs = document.getElementById('num_msgs').value;
+  var msg_size = document.getElementById('msg_size').value;
+
+  currentRun.targetId = targetId;
+  currentRun.curMsgId = 0;
+  currentRun.endMsgId = num_msgs;
+
+  var msg = [];
+  for (var i = 0; i < msg_size; ++i) {
+    msg[i] = alphabet.charAt(Math.round(Math.random(alphabet.length)));
+  }
+  currentRun.msg = msg.join('');
+
+  // clear local perf stats
+  clearPerfStats('(outbound)');
+
+  // clear target perf stats
+  gadgets.rpc.call(targetId, 'clear_perf_stats', null, '(inbound)');
+
+  // kick off the send loop
+  sendPerfMessage();
+}
+
+function runCallbackTest(targetId, isSync) {
+  document.getElementById('echo_test_result').innerHTML = '';
+  var service = (isSync ? '' : 'a') + 'sync_callback_service';
+  var echoValue = document.getElementById('echo_test_input').value;
+  var callback = function(response) {
+    document.getElementById('echo_test_result').innerHTML = response + ' at ' + new Date().toUTCString() + ' from referer: ' + this['referer'];
+  };
+  gadgets.rpc.call(targetId, service, callback, echoValue);
+}
diff --git a/trunk/content/containers/deprecated/container/sample-metadata.html b/trunk/content/containers/deprecated/container/sample-metadata.html
new file mode 100644
index 0000000..07da7d3
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample-metadata.html
@@ -0,0 +1,124 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+<head>
+<title>Metadata Demo</title>
+<style>
+  .gadget {
+    border: solid 1px #000;
+    margin: 10px;
+    float: left;
+    text-align: center;
+  }
+  .gadget h2 {
+    background: #ccf;
+    border-bottom: solid 1px #000;
+    margin: 0;
+    padding: 5px;
+  }
+  .gadget iframe {
+    margin: 5px;
+    border: none;
+    height: 300px;
+    width: 300px;
+  }
+</style>
+</head>
+<script src="/gadgets/js/core.js?c=1"></script>
+<body>
+<script>
+  function makeXhr() {
+    if (window.XMLHttpRequest) {
+      return new XMLHttpRequest();
+    } else if (window.ActiveXObject) {
+      var x = new ActiveXObject("Msxml2.XMLHTTP");
+      if (!x) {
+        x = new ActiveXObject("Microsoft.XMLHTTP");
+      }
+      return x;
+    }
+  }
+
+  function renderGadgets(obj) {
+    var gadgetList = obj.gadgets;
+    var features = {};
+    for (var i = 0, gadget; gadget = gadgetList[i]; ++i) {
+      var feats = gadget.features || [];
+      for (var j = 0, feature; feature = feats[j]; ++j) {
+        features[feature] = true;
+      }
+    }
+    var libs = [];
+    for (var lib in features) {libs.push(lib);}
+    libs.sort();
+    libs = libs.join(":");
+    for (var i = 0, gadget; gadget = gadgetList[i]; ++i) {
+      var newGadget = document.createElement("div");
+      if (gadget.errors && gadget.errors.length > 0) {
+        newGadget.innerHTML = ["Unable to process gadget: ", gadget.url, ". Errors: <pre>", gadget.errors.join("\n"), "</pre>"].join("");
+      } else {
+        newGadget.innerHTML = ['<h2>', gadget.title, '</h2>',
+          '<iframe src="', gadget.iframeUrl, '&libs=', libs ,'" id="remote_iframe_', gadget.moduleId, '" name="remote_iframe_', gadget.moduleId, '"></iframe>'
+        ].join("");
+      }
+      newGadget.className = "gadget";
+      document.body.appendChild(newGadget);
+    }
+  }
+
+  function processResp(xhr) {
+    if (xhr.readyState !== 4) {return;}
+    renderGadgets(gadgets.json.parse(xhr.responseText));
+  }
+
+  var request = {
+    context: {
+      country: "US",
+      language: "en",
+      view: "default",
+      container: "default"
+    },
+    gadgets: [
+      {
+        url: "http://www.google.com/ig/modules/hello.xml",
+        moduleId: 1
+      },
+      {
+        url: "http://www.labpixies.com/campaigns/todo/todo.xml",
+        moduleId: 2
+      },
+      {
+        url: "http://www.example.org/fake/fake/fake.xml",
+        moduleId: 3
+      }
+    ]
+  };
+
+  var xhr = makeXhr();
+  xhr.open("POST", "/gadgets/metadata", true);
+  xhr.onreadystatechange = function(xobj) {
+    return function() {
+      processResp(xobj);
+    };
+  }(xhr);
+  var req = gadgets.json.stringify(request);
+  xhr.send(req);
+</script>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample-payment-container.html b/trunk/content/containers/deprecated/container/sample-payment-container.html
new file mode 100644
index 0000000..2aa7de2
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample-payment-container.html
@@ -0,0 +1,128 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: Virtual Currency Payment</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<style>
+  .gadgets-gadget-chrome {
+    width: 80%;
+    float: none;
+    margin: auto;
+  }
+  .gadgets-gadget {
+    width: 100%;
+  }
+  .desc {
+    color:#007F00;
+  }
+  .desc script {
+    color:#FF0000;
+  }
+</style>
+
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc:opensocial-payment.js?c=1&debug=1"></script>
+<script type="text/javascript">
+
+function output(message) {
+  document.getElementById("output").innerHTML += gadgets.util.escapeString(message) + "<br/>";
+};
+
+// The container domain.
+var containerHost = 'http://' + window.location.host;
+
+// NOTE: Set the gadget serverBase here to replace 'window.location.host' which is just for demo.
+// The shindig domain must be different from the container domain for security reason.
+var shindigHost = 'http://' + window.location.host;
+
+// The location of the demo app spec. It is located on container domain.
+var demoGadgetSpecs = [
+   containerHost + '/container/sample-payment.xml',
+];
+
+function renderGadgets() {
+  var demoGadgets = [];
+  var chromeIds = [];
+  for (var i = 0; i < demoGadgetSpecs.length; ++i) {
+    var gadget = shindig.container.createGadget({
+        specUrl: demoGadgetSpecs[i],
+        title: ("Sample Payment App - " + i)
+    });
+    gadget.setServerBase(shindigHost + '/gadgets/');
+    shindig.container.addGadget(gadget);
+    chromeIds.push('gadget-chrome-' + i);
+    demoGadgets.push(gadget);
+  }
+  shindig.container.layoutManager.setGadgetChromeIds(chromeIds);
+  for (var i = 0; i < demoGadgets.length; ++i) {
+    shindig.container.renderGadget(demoGadgets[i]);
+  }
+};
+
+
+</script>
+</head>
+<body onLoad="renderGadgets();">
+  <center>
+    <h2>OpenSocial Virtual Currency Proposal Revision #4 Demo</h2>
+
+    <h4>opensocial.requestPayment<br>opensocial.requestPaymentRecords</h4>
+    <div>For detail, please checkout <a href="http://docs.google.com/View?id=dhcrsqrj_0d86fkdfv" target=_blank>proposal doc</a>, 
+      <a href="http://groups.google.com/group/opensocial-and-gadgets-spec/browse_thread/thread/7341f1716e50f4d/8553e6aa696bd088?lnk=gst" target=_blank>discussion thread</a>, and 
+      <a href="http://code.google.com/p/opensocial-virtual-currency" target=_blank>code project</a>.
+    </div>
+    <p class="desc">
+      This page is a container page:<br>
+      <b><script>document.write(window.location.href);</script></b>
+    </p>
+    </center>
+  <div id="gadget-chrome-0" class="gadgets-gadget-chrome"></div>
+
+  <div id="output" style="clear: left;">
+  </div>
+
+  <!-- The counter panel -->
+  <style>
+    .payment-panel {
+      width:700px;
+      height:400px;
+      left:100px;
+      top:200px;
+      position:absolute;
+    }
+    .payment-panel iframe {
+      width:700px;
+      height:400px;
+    }
+  </style>
+  <!-- The payment processor panel, the processor page's domain should be the same as container domain -->
+  <div id="payment-processor" style="display:none;" class="payment-panel">
+    <iframe name="payment-processor-frame" frameborder=0 src="/container/payment-processor.html"></iframe>
+  </div>
+
+  <!-- The payment records processor panel, the processor page's domain should be the same as container domain -->
+  <div id="payment-records-processor" style="display:none;" class="payment-panel">
+    <iframe name="payment-processor-frame" frameborder=0 src="/container/payment-records-processor.html"></iframe>
+  </div>
+
+</body>
+</html>
+
diff --git a/trunk/content/containers/deprecated/container/sample-payment.xml b/trunk/content/containers/deprecated/container/sample-payment.xml
new file mode 100644
index 0000000..01fdacf
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample-payment.xml
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="My App Test"
+               author_email="yizi.wu@gmail.com" height="500">
+    <Require feature="opensocial-0.9"/>
+    <Require feature="dynamic-height"/>
+    <Require feature="settitle"/>
+    <Require feature="views"/>
+    <Require feature="rpc"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <style>
+        #main {font-size:13px;}
+        .t {width:300px; margin-left:3px;}
+        .f {border-collapse:collapse;margin-left:10px;}
+        .f tbody tr td {font-size:12px;font-weight:bold;white-space:nowrap;vertical-align:top;}
+        .f tbody tr td span {font-size:10px;white-space:normal;}
+        .desc {color:#007F7F;}
+      </style>
+      <script>
+        function requestPayment() {
+          var params = {};
+          params[opensocial.Payment.Field.AMOUNT] = document.getElementById('amount').value;
+          params[opensocial.Payment.Field.MESSAGE] = document.getElementById('message').value;
+          params[opensocial.Payment.Field.PARAMETERS] = gadgets.util.escapeString(document.getElementById('parameters').value);
+          params[opensocial.Payment.Field.PAYMENT_TYPE] = document.getElementById('creditType').checked ? 
+              opensocial.Payment.PaymentType.CREDIT : opensocial.Payment.PaymentType.PAYMENT;
+
+          var itemParams = {};
+          itemParams[opensocial.BillingItem.Field.SKU_ID] = 'test_sku1';
+          itemParams[opensocial.BillingItem.Field.PRICE] = 20;
+          itemParams[opensocial.BillingItem.Field.COUNT] = 2;
+          itemParams[opensocial.BillingItem.Field.DESCRIPTION] = 'demo description red flower';
+          var item1 = opensocial.newBillingItem(itemParams);
+
+          itemParams = {};
+          itemParams[opensocial.BillingItem.Field.SKU_ID] = 'test_sku2';
+          itemParams[opensocial.BillingItem.Field.PRICE] = 30;
+          itemParams[opensocial.BillingItem.Field.COUNT] = 4;
+          itemParams[opensocial.BillingItem.Field.DESCRIPTION] = 'demo description yellow flower';
+          var item2 = opensocial.newBillingItem(itemParams);
+
+
+          params[opensocial.Payment.Field.ITEMS] = [item1, item2];
+          var payment = opensocial.newPayment(params);
+
+          opensocial.requestPayment(payment, function(responseItem) {
+            document.getElementById('paymentOutput').style.display = 'block';
+            document.getElementById('status').innerHTML = responseItem.hadError() ? 'FAILED' : 'SUCCESS';
+            var data = responseItem.getData();
+            document.getElementById('type').innerHTML = data.getField(opensocial.Payment.Field.PAYMENT_TYPE);
+            document.getElementById('orderid').innerHTML = data.getField(opensocial.Payment.Field.ORDER_ID);
+            document.getElementById('code').innerHTML = data.getField(opensocial.Payment.Field.RESPONSE_CODE);
+            document.getElementById('resmsg').innerHTML = data.getField(opensocial.Payment.Field.RESPONSE_MESSAGE);
+            document.getElementById('orderedtime').innerHTML = new Date(data.getField(opensocial.Payment.Field.ORDERED_TIME)).toLocaleString();
+            document.getElementById('submittedtime').innerHTML = new Date(data.getField(opensocial.Payment.Field.SUBMITTED_TIME)).toLocaleString();
+            document.getElementById('executedtime').innerHTML = new Date(data.getField(opensocial.Payment.Field.EXECUTED_TIME)).toLocaleString();
+
+            gadgets.window.adjustHeight();
+          });
+          document.getElementById('paymentOutput').style.display = 'none';
+
+        };
+
+
+        function requestPaymentRecords() {
+
+          var params = {};
+          params[opensocial.Payment.RecordsRequestFields.MAX] = document.getElementById('max').value;
+          params[opensocial.Payment.RecordsRequestFields.SANDBOX] = document.getElementById('r_sandbox').checked;
+
+          opensocial.requestPaymentRecords(function(responseItem) {
+            document.getElementById('recordsOutput').style.display = 'block';
+            var data = responseItem.getData();
+
+            var html = 'Listing original incomplete payments before request.<br> Payments in bold are fixed manually by user afterward.<br>';
+            for (var i = 0; i < data.length; i++) {
+              var bold = data[i].getField(opensocial.Payment.Field.PAYMENT_COMPLETE);
+              if (bold) html += '<b>';
+              html += data[i].getField(opensocial.Payment.Field.ORDER_ID) + '&emsp;' + 
+                      data[i].getField(opensocial.Payment.Field.AMOUNT) + '&emsp;' + 
+                      data[i].getField(opensocial.Payment.Field.RESPONSE_MESSAGE) + '&emsp;' +
+                      new Date(data[i].getField(opensocial.Payment.Field.EXECUTED_TIME)).toLocaleString();
+              if (bold) html += '</b>';
+              html += '<br>';
+            }
+            document.getElementById('records').innerHTML = html;
+
+            gadgets.window.adjustHeight();
+          }, params);
+
+          document.getElementById('recordsOutput').style.display = 'none';
+        };
+
+        function init() {
+          var req = opensocial.newDataRequest();
+          req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER), "req");
+          req.send(function(data) {
+            if (!data.hadError()) {
+              document.getElementById('myname').innerHTML = 'Current Viewer: <b>' + data.get("req").getData().getDisplayName() + '</b>';
+            }
+            gadgets.window.adjustHeight();
+          });
+        };
+        gadgets.util.registerOnLoadHandler(init);
+      </script>
+
+
+      <div id="main">
+        <p class="desc">
+            Here is the app domain inside the gadget iframe, usually different from container domain:<br>
+            <b><script>document.write('http://' + window.location.host + window.location.pathname + location.search.substring(0, 30) + '...');</script></b>
+        </p>
+        <p><span id="myname"></span></p><hr>
+
+        <div id=req>
+          <b>Make a Payment Request: </b><br>
+          <table class=f><tbody>
+          <tr><td>Amount: </td><td><input class=t id=amount value=100></td></tr>
+          <tr><td>Message: </td><td><input class=t id=message value="You are ordering some flowers."></td></tr>
+          <tr><td>Parameters: </td><td><input class=t id=parameters value="{type:'Tulip',quantity:5}"></td></tr>
+          <tr><td>Payment Type: </td><td>
+              <input type=radio id=paymentType name=pt checked><label for=paymentType>Payment</label>
+              <input type=radio id=creditType name=pt><label for=creditType>Credit</label>
+          </td></tr>
+          </tbody></table>
+          <button onclick="requestPayment();">Request Payment</button>
+        </div>
+
+        <div id=paymentOutput style="display:none">
+          <hr>
+          <b>Payment Response: </b><br>
+          <table class=f><tbody>
+          <tr><td>Payment Type: </td><td><span id=type></span></td></tr>
+          <tr><td>Status: </td><td><span id=status></span></td></tr>
+          <tr><td>Order ID: </td><td><span id=orderid></span></td></tr>
+          <tr><td>Response Code: </td><td><span id=code></span></td></tr>
+          <tr><td>Response Message: </td><td><span id=resmsg></span></td></tr>
+          <tr><td>Ordered Time: </td><td><span id=orderedtime></span></td></tr>
+          <tr><td>Submitted Time: </td><td><span id=submittedtime></span></td></tr>
+          <tr><td>Executed Time: </td><td><span id=executedtime></span></td></tr>
+          </tbody></table>
+        </div>
+        <hr>
+
+        <div>
+          <b>Make a Payment Records Request: </b><br>
+          <table class=f><tbody>
+          <tr><td><label for=sandbox>Sandbox: </label></td><td><input type=checkbox id=r_sandbox checked></td></tr>
+          <tr><td>Max: </td><td><input class=t id=max value=3></td></tr>
+          </tbody></table>
+          <button onclick="requestPaymentRecords();">Request Payment Records</button>
+        </div>
+        <div id=recordsOutput style="display:none">
+          <hr>
+          <div id=records></div>
+        </div>
+      </div>
+     ]]>
+  </Content>
+</Module>
+
+
diff --git a/trunk/content/containers/deprecated/container/sample-pubsub-2.html b/trunk/content/containers/deprecated/container/sample-pubsub-2.html
new file mode 100644
index 0000000..87db081
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample-pubsub-2.html
@@ -0,0 +1,156 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: PubSub-2</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:pubsub-2.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var my = {};
+
+my.gadgetSpecUrls = [
+  'http://' + window.location.host + '/container/sample-pubsub-2-publisher.xml',
+  'http://' + window.location.host + '/container/sample-pubsub-2-subscriber.xml',
+  'http://' + window.location.host + '/container/sample-pubsub-2-subscriber.xml'
+];
+
+my.LayoutManager = function() {
+  shindig.LayoutManager.call(this);
+};
+
+my.LayoutManager.inherits(shindig.LayoutManager);
+
+my.LayoutManager.prototype.getGadgetChrome = function(gadget) {
+  var chromeId = 'gadget-chrome-' + gadget.id;
+  return document.getElementById(chromeId);
+};
+
+my.init = function() {
+  gadgets.pubsub2router.init(
+    {
+      onSubscribe: function(topic, container) {
+        log(container.getClientID() + " subscribes to topic '" + topic + "'");
+        return true;
+        // return false to reject the request.
+      },
+      onUnsubscribe: function(topic, container) {
+        log(container.getClientID() + " unsubscribes from topic '" + topic + "'");
+      },
+      onPublish: function(topic, data, pcont, scont) {
+        log(pcont.getClientID() + " publishes '" + data + "' to topic '" + topic + "' subscribed by " + scont.getClientID());
+        return true;
+        // return false to reject the request.
+      }
+    });
+  shindig.container.layoutManager = new my.LayoutManager();
+};
+
+my.renderGadgets = function() {
+  for (var i = 0; i < my.gadgetSpecUrls.length; ++i) {
+    var gadget = shindig.container.createGadget(
+        {specUrl: my.gadgetSpecUrls[i], title: (i ? "Subscriber" : "Publisher"), debug: true});
+    shindig.container.addGadget(gadget);
+    shindig.container.renderGadget(gadget);
+  }
+};
+
+my.printMetadata = function() {
+  var request = {
+    context: {
+      country: "default",
+      language: "default",
+      view: "default",
+      container: "default"
+    },
+    gadgets: [
+      {  url: my.gadgetSpecUrls[0],
+        moduleId: 1
+      },
+      { url: my.gadgetSpecUrls[1],
+        moduleId: 1
+      },
+      { url: my.gadgetSpecUrls[2],
+        moduleId: 1
+      }
+    ]
+  };
+
+  var makeRequestParams = {
+    "CONTENT_TYPE" : "JSON",
+    "METHOD" : "POST",
+    "POST_DATA" : gadgets.json.stringify(request)
+  };
+
+  var url = "http://" + window.location.host + "/gadgets/metadata";
+
+  gadgets.io.makeNonProxiedRequest(url,
+    handleJSONResponse,
+    makeRequestParams,
+    "application/javascript"
+  );  
+  
+  function handleJSONResponse(obj) {
+    var metadata = obj.data.gadgets;
+    for (var i = 0; i < metadata.length; i++) {
+      var gadget = metadata[i].url.match( /.*\/([^/]+)$/ )[1];
+      var topics = metadata[i].featureDetails['pubsub-2'].parameters.topics;
+      for (var j = 0; j < topics.length; j++) {
+        var topic = topics[j];
+        // Depending on how the gadget metadata was written, the topic metadata may be either a
+        // string or an object (parsed from the XML).
+        if (typeof(topic) == "string") {
+          var attrs = topic.match( /\w+=(?:"[^"]*"|'[^']*')/g );
+          topic = {};
+          for (var k = 0; k < attrs.length; k++) {
+            var pairs = attrs[k].match( /(\w+)=(?:"([^"]*)"|'([^']*)')/ );
+            var attr = pairs[1],
+                value = pairs[2] || pairs[3];
+            topic[attr] = value === "true" ? true :
+                          value === "false" ? false :
+                          value;
+          }
+        }
+        
+        log( "<" + gadget + "> " + (topic.subscribe ? "subscribes " + (topic.publish ? "and " : "") : "") +
+            (topic.publish ? "publishes " : "") + "on the event topic <" + topic.name + "> " +
+            (topic.title ? "('" + topic.title + "') " : "") +
+            (topic.type ? "[event type is '" + topic.type + "']" : "") );
+      }
+    }
+  }
+}
+function log(message) {
+  document.getElementById("output").innerHTML += gadgets.util.escapeString(message) + "<br/>";
+}
+</script>
+</head>
+<body onLoad="my.init();my.renderGadgets();">
+  <h2>Sample: PubSub-2</h2>
+  <div id="gadget-chrome-0" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-1" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-2" class="gadgets-gadget-chrome"></div>
+  <div style="clear: left;">
+    <input type="button" value="Print gadgets' pubsub metadata" onclick="my.printMetadata()"/>
+  </div>
+  <div id="output" style="clear: left;">
+  </div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample-pubsub-publisher.xml b/trunk/content/containers/deprecated/container/sample-pubsub-publisher.xml
new file mode 100644
index 0000000..56d2382
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample-pubsub-publisher.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+<ModulePrefs title="Sample PubSub Publisher"
+             height="250">
+<Require feature="pubsub"/>
+</ModulePrefs>
+<Content type="html">
+<![CDATA[
+<script>
+function publish() {
+  var message = Math.random();
+  gadgets.pubsub.publish("random-number", message);
+  document.getElementById("output").innerHTML = message;
+}
+
+</script>
+<div>
+<input type="button" value="Publish a random number" onclick="publish()"/>
+</div>
+<div id="output">
+</div>
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/containers/deprecated/container/sample-pubsub-subscriber.xml b/trunk/content/containers/deprecated/container/sample-pubsub-subscriber.xml
new file mode 100644
index 0000000..a2ad3b6
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample-pubsub-subscriber.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+<ModulePrefs title="Sample PubSub Subscriber"
+             height="250">
+<Require feature="pubsub"/>
+</ModulePrefs>
+<Content type="html">
+<![CDATA[
+<script>
+function callback(sender, message) {
+  document.getElementById("output").innerHTML =
+    "message : " + gadgets.util.escapeString(message + "") + "<br/>" +
+    "sender : " + gadgets.util.escapeString(sender);
+}
+
+function subscribe() {
+  gadgets.pubsub.subscribe("random-number", callback);
+}
+
+function unsubscribe() {
+  gadgets.pubsub.unsubscribe("random-number");
+  document.getElementById("output").innerHTML = "";
+}
+
+</script>
+<div>
+<input type="button" value="Subscribe" onclick="subscribe()"/>
+<input type="button" value="Unsubscribe" onclick="unsubscribe()"/>
+</div>
+<div id="output">
+</div>
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/containers/deprecated/container/sample-pubsub.html b/trunk/content/containers/deprecated/container/sample-pubsub.html
new file mode 100644
index 0000000..0efd450
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample-pubsub.html
@@ -0,0 +1,95 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: PubSub</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:pubsub.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var my = {};
+
+my.gadgetSpecUrls = [
+  'http://' + window.location.host + '/container/sample-pubsub-publisher.xml',
+  'http://' + window.location.host + '/container/sample-pubsub-subscriber.xml',
+  'http://' + window.location.host + '/container/sample-pubsub-subscriber.xml'
+];
+
+my.LayoutManager = function() {
+  shindig.LayoutManager.call(this);
+};
+
+my.LayoutManager.inherits(shindig.LayoutManager);
+
+my.LayoutManager.prototype.getGadgetChrome = function(gadget) {
+  var chromeId = 'gadget-chrome-' + gadget.id;
+  return chromeId ? document.getElementById(chromeId) : null;
+};
+
+my.init = function() {
+  gadgets.pubsubrouter.init(function(id) {
+    var gadgetId = shindig.container.gadgetService.getGadgetIdFromModuleId(id);
+    var gadget = shindig.container.getGadget(gadgetId);
+    return gadget.specUrl;
+  }, {
+    onSubscribe: function(sender, channel) {
+      log(sender + " subscribes to channel '" + channel + "'");
+      // return true to reject the request.
+      return false;
+    },
+    onUnsubscribe: function(sender, channel) {
+      log(sender + " unsubscribes from channel '" + channel + "'");
+      // return true to reject the request.
+      return false;
+    },
+    onPublish: function(sender, channel, message) {
+      log(sender + " publishes '" + message + "' to channel '" + channel + "'");
+      // return true to reject the request.
+      return false;
+    }
+  });
+  shindig.container.layoutManager = new my.LayoutManager();
+};
+
+my.renderGadgets = function() {
+  shindig.container.setParentUrl("http://" + window.location.host + "/");
+  for (var i = 0; i < my.gadgetSpecUrls.length; ++i) {
+    var gadget = shindig.container.createGadget(
+        {debug:1,specUrl: my.gadgetSpecUrls[i], title: (i ? "Subscriber" : "Publisher")});
+    shindig.container.addGadget(gadget);
+    shindig.container.renderGadget(gadget);
+   
+  }
+};
+
+function log(message) {
+  document.getElementById("output").innerHTML += gadgets.util.escapeString(message) + "<br/>";
+}
+</script>
+</head>
+<body onLoad="my.init();my.renderGadgets();">
+  <h2>Sample: PubSub</h2>
+  <div id="gadget-chrome-0" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-1" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-2" class="gadgets-gadget-chrome"></div>
+  <div id="output" style="clear: left;">
+  </div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample1.html b/trunk/content/containers/deprecated/container/sample1.html
new file mode 100644
index 0000000..02bedda
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample1.html
@@ -0,0 +1,51 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: Simple Container</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="../gadgets/js/shindig-container:rpc.js?c=1&debug=1&nocache=1"></script>
+<script type="text/javascript">
+var specUrl0 = 'http://www.google.com/ig/modules/horoscope.xml';
+var specUrl1 = 'http://www.labpixies.com/campaigns/todo/todo.xml';
+
+// This container lays out and renders gadgets itself.
+
+function renderGadgets() {
+  var gadget0 = shindig.container.createGadget({specUrl: specUrl0});
+  var gadget1 = shindig.container.createGadget({specUrl: specUrl1});
+
+  shindig.container.addGadget(gadget0);
+  shindig.container.addGadget(gadget1);
+  shindig.container.layoutManager.setGadgetChromeIds(
+      ['gadget-chrome-x', 'gadget-chrome-y']);
+
+  shindig.container.renderGadget(gadget0);
+  shindig.container.renderGadget(gadget1);
+};
+</script>
+</head>
+<body onLoad="renderGadgets();">
+  <h2>Sample: Simple Container</h2>
+  <div id="gadget-chrome-x" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-y" class="gadgets-gadget-chrome"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample2.html b/trunk/content/containers/deprecated/container/sample2.html
new file mode 100644
index 0000000..83c7a76
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample2.html
@@ -0,0 +1,69 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: Dynamic Height</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var my = {};
+
+my.gadgetSpecUrls = [
+  'http://www.google.com/ig/modules/horoscope.xml',
+  'http://www.google.com/ig/modules/aue07otr.xml',
+  'http://www.labpixies.com/campaigns/todo/todo.xml'
+];
+
+
+// This container lays out and renders gadgets itself.
+
+my.LayoutManager = function() {
+  shindig.LayoutManager.call(this);
+};
+
+my.LayoutManager.inherits(shindig.LayoutManager);
+
+my.LayoutManager.prototype.getGadgetChrome = function(gadget) {
+  var chromeId = 'gadget-chrome-' + gadget.id;
+  return chromeId ? document.getElementById(chromeId) : null;
+};
+
+my.init = function() {
+  shindig.container.layoutManager = new my.LayoutManager();
+};
+
+my.renderGadgets = function() {
+  for (var i = 0; i < my.gadgetSpecUrls.length; ++i) {
+    var gadget = shindig.container.createGadget(
+        {specUrl: my.gadgetSpecUrls[i]});
+    shindig.container.addGadget(gadget);
+    shindig.container.renderGadget(gadget);
+  }
+};
+</script>
+</head>
+<body onLoad="my.init();my.renderGadgets();">
+  <h2>Sample: Dynamic Height</h2>
+  <div id="gadget-chrome-0" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-1" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-2" class="gadgets-gadget-chrome"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample3.html b/trunk/content/containers/deprecated/container/sample3.html
new file mode 100644
index 0000000..b50b377
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample3.html
@@ -0,0 +1,48 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: Container with FloatLeft Layout</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var specUrl0 = 'http://www.google.com/ig/modules/horoscope.xml';
+
+function init() {
+  shindig.container.layoutManager =
+      new shindig.FloatLeftLayoutManager('layout-root');
+
+  for (var i = 0; i < 13; i++) {
+    shindig.container.addGadget(
+        shindig.container.createGadget({specUrl: specUrl0}));
+  }
+};
+
+function renderGadgets() {
+  shindig.container.renderGadgets();
+};
+</script>
+</head>
+<body onLoad="init();renderGadgets();">
+  <h2>Sample: Container with FloatLeft Layout</h2>
+  <div id="layout-root" class="gadgets-layout-root"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample4.html b/trunk/content/containers/deprecated/container/sample4.html
new file mode 100644
index 0000000..1c2a0f4
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample4.html
@@ -0,0 +1,46 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: set-pref support</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var specUrl0 = 'http://www.google.com/ig/modules/test_setprefs_multiple_ifpc.xml';
+
+function init() {
+  shindig.container.layoutManager =
+      new shindig.FloatLeftLayoutManager('gadget-parent');
+
+  shindig.container.addGadget(
+      shindig.container.createGadget({specUrl: specUrl0}));
+};
+
+function renderGadgets() {
+  shindig.container.renderGadgets();
+};
+</script>
+</head>
+<body onLoad="init();renderGadgets();">
+  <h2>Sample: set-pref support</h2>
+  <div id="gadget-parent" class="gadgets-gadget-parent"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample5.html b/trunk/content/containers/deprecated/container/sample5.html
new file mode 100644
index 0000000..92b55f9
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample5.html
@@ -0,0 +1,46 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: set-pref support</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var specUrl0 = 'http://www.google.com/ig/modules/test_setprefs_multiple_ifpc.xml';
+
+function init() {
+  shindig.container.layoutManager =
+      new shindig.FloatLeftLayoutManager('gadget-parent');
+
+  var gadget = shindig.container.createGadget({specUrl: specUrl0});
+  shindig.container.addGadget(gadget);
+};
+
+function renderGadgets() {
+  shindig.container.renderGadgets();
+};
+</script>
+</head>
+<body onLoad="init();renderGadgets();">
+  <h2>Sample: set-pref support</h2>
+  <div id="gadget-parent" class="gadgets-gadget-parent"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample6.html b/trunk/content/containers/deprecated/container/sample6.html
new file mode 100644
index 0000000..078c38c
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample6.html
@@ -0,0 +1,46 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: dynamic-height support</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var specUrl0 = 'http://www.google.com/ig/modules/aue07otr.xml';
+
+function init() {
+  shindig.container.layoutManager =
+      new shindig.FloatLeftLayoutManager('gadget-parent');
+
+  var gadget = shindig.container.createGadget({specUrl: specUrl0, title: "Dynamic Height Demo", width: 500});
+  shindig.container.addGadget(gadget);
+};
+
+function renderGadgets() {
+  shindig.container.renderGadgets();
+};
+</script>
+</head>
+<body onLoad="init();renderGadgets();">
+  <h2>Sample: dynamic-height support</h2>
+  <div id="gadget-parent" class="gadgets-gadget-parent"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/container/sample7.html b/trunk/content/containers/deprecated/container/sample7.html
new file mode 100644
index 0000000..fa0355e
--- /dev/null
+++ b/trunk/content/containers/deprecated/container/sample7.html
@@ -0,0 +1,45 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE html>
+<html>
+<head>
+<title>Sample: settitle support</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1&debug=1"></script>
+<script type="text/javascript">
+var specUrl0 = 'http://www.google.com/ig/modules/test_settitle_html.xml';
+
+function init() {
+  shindig.container.layoutManager =
+      new shindig.FloatLeftLayoutManager('gadget-parent');
+  var gadget = shindig.container.createGadget({specUrl: specUrl0});
+  shindig.container.addGadget(gadget);
+};
+
+function renderGadgets() {
+  shindig.container.renderGadgets();
+};
+</script>
+</head>
+<body onLoad="init();renderGadgets();">
+  <h2>Sample: settitle support</h2>
+  <div id="gadget-parent" class="gadgets-gadget-parent"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/samplecontainer/getting-started.html b/trunk/content/containers/deprecated/samplecontainer/getting-started.html
new file mode 100644
index 0000000..a9499ec
--- /dev/null
+++ b/trunk/content/containers/deprecated/samplecontainer/getting-started.html
@@ -0,0 +1,81 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+<head>
+  <title>OpenSocial Container Sample</title>
+</head>
+
+<body>
+  <h1>OpenSocial Container Sample - Getting started</h1>
+
+  <ul>
+    <li><a href="#Purpose">Purpose</a>
+    <li><a href="#Theory">Theory</a>
+    <li><a href="#Tips">Tips</a>
+  </ul>
+
+  <h2 id="Purpose">Purpose</h2>
+  This sample serves two primary purposes:
+  <ol>
+    <li>To demonstrate how a container can be implemented using a simple
+      example.
+    <li>To create an environment for easy gadget testing. OpenSocial is all
+      about social APIs, which means that gadget testing usually involves
+      multiple user accounts. This container makes testing easy by letting
+      gadgets specify arbitrary state for any number of users.
+  </ol>
+
+  <h2 id="Theory">How it works? (Theory)</h2>
+  As the gadget developer you need to specify two pieces of information:
+  <ol type="a">
+    <li>URL to the gadget definition; and</li>
+    <li>initial state of the container in the form of a URL to a state file.</li>
+  </ol>
+
+  You can find the DTD for the state definition in the docs/state.dtd folder.
+
+  The state definition file allows you to specify the viewer of the gadget,
+  the owner, the friends and the activities of those users. Once the gadget and
+  its state are loaded you can use the gadget in the same way as any other
+  container. At any point you can also dump a snap shot of the state of the
+  environment to an XML file (with the same format as the state definition
+  file).
+
+  <h2 id="Tips">Tips/caveats</h2>
+  <ul>
+    <li>Due to browser security restrictions, your gadget definition
+      file and system state file must be on the same server as the
+      container if you specify a url. The above step by step procedure runs the test container from the local
+      file system. You can also copy the files to a web server and run off it
+      instead.
+    <li>The gadget definition URL is stored in a cookie.
+      You can set the values to empty or clear your cookies to clear existing
+      values for those fields.
+    <li>For easier debugging of your gadget script in Firebug, include the
+      gadget script using a script tag in the gadget definition file, instead
+      of inlining the script. Sometimes Firebug still cannot display the gadget
+      javascript if it is on the local file system. To avoid this, you can untar
+      the package to a web server and access it through an <code>http</code> URL
+      to that server.
+    <li>You always need to specify a state file for the container. At a minimum
+      it must include the viewer name. All other fields are optional.
+  </ul>
+
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/samplecontainer/samplecontainer.html b/trunk/content/containers/deprecated/samplecontainer/samplecontainer.html
new file mode 100644
index 0000000..af3a0f0
--- /dev/null
+++ b/trunk/content/containers/deprecated/samplecontainer/samplecontainer.html
@@ -0,0 +1,94 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+<head>
+<title>Gadget testing container</title>
+<link rel="stylesheet" href="../container/gadgets.css">
+<style type="text/css">
+  body {
+    font-family: arial, sans-serif;
+  }
+
+  #headerDiv {
+    padding: 10px;
+    margin-bottom: 20px;
+    background-color: #e5ecf9;
+    color: #3366cc;
+    font-size: larger;
+    font-weight: bold;
+  }
+
+  .subTitle {
+    font-size: smaller;
+    float: right;
+  }
+
+  .gadgets-gadget-chrome {
+    width: 60%;
+    float: none;
+    margin: auto;
+  }
+
+  .gadgets-gadget {
+    width: 100%;
+  }
+
+</style>
+
+<script type="text/javascript" src="../gadgets/js/core:rpc:pubsub:shindig-container.js?c=1&debug=1"></script>
+<script type="text/javascript" src="samplecontainer.js"></script>
+
+</head>
+<body onLoad="shindig.samplecontainer.initSampleContainer();
+    shindig.samplecontainer.unpackFormState(); shindig.samplecontainer.initGadget();">
+  <div id="headerDiv">
+    <div style="float:left">Gadget testing container</div>
+    <div class="subTitle">
+      Displaying gadget: <input type="text" size="75" id="gadgetUrl"/>
+      <input type="checkbox" id="useCacheCheckbox" checked="true"
+       /><label for="useCacheCheckbox">use cache</label>
+      <input type="checkbox" id="useCajaCheckbox"
+       /><label for="useCajaCheckbox">use caja</label>
+      <input type="checkbox" id="useDebugCheckbox"
+       /><label for="useDebugCheckbox">use debug</label>
+
+      <br/>
+
+      Using state: <input type="text" size="75" id="stateFileUrl"/>
+      <input type="checkbox" id="doEvilCheckbox"
+       /><label for="doEvilCheckbox">do evil</label>
+
+      <br/>
+      <br/>
+      Viewer id: <input type="text" size="20" id="viewerId"/>
+      Owner id: <input type="text" size="20" id="ownerId"/>
+
+      <br/>
+
+      <input type="button" value="reset all" onclick="shindig.samplecontainer.changeGadgetUrl();"/>
+      <input type="button" value="dump state" onclick="shindig.samplecontainer.dumpStateFile();"/>
+      <input type="button" value="Send Hello" onclick="shindig.samplecontainer.sendHello();"/>
+    </div>
+    <div style="clear:both; height: 1px;">&nbsp;</div>
+  </div>
+
+  <div id="gadgetState" style="font-size:smaller"></div>
+  <div id="gadget-chrome" class="gadgets-gadget-chrome"></div>
+</body>
+</html>
diff --git a/trunk/content/containers/deprecated/samplecontainer/samplecontainer.js b/trunk/content/containers/deprecated/samplecontainer/samplecontainer.js
new file mode 100644
index 0000000..380c364
--- /dev/null
+++ b/trunk/content/containers/deprecated/samplecontainer/samplecontainer.js
@@ -0,0 +1,302 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @Functions for the samplecontainer
+ */
+
+/**
+ * Public Shindig namespace with samplecontainer object
+ */
+
+var shindig = shindig || {};
+shindig.samplecontainer = {};
+
+/**
+ * Hide our functions and variables from other javascript
+ */
+
+(function() {
+
+  /**
+   * Private Variables
+  */
+
+  var parentUrl = document.location.href;
+  var baseUrl = parentUrl.substring(0, parentUrl.indexOf('samplecontainer'));
+
+  // TODO: This is gross, it needs to use the config just like the gadget js does
+  var socialDataPath = baseUrl + 'social/rest/samplecontainer/';
+
+  var gadgetUrlMatches = /[?&]url=((?:[^#&]+|&amp;)+)/.exec(parentUrl);
+  var gadgetUrl = (gadgetUrlMatches)
+      ? gadgetUrlMatches[1]
+      : baseUrl + 'gadgets/SocialHelloWorld.xml';
+
+  var gadgetUrlCookie = 'sampleContainerGadgetUrl';
+
+  var stateFileUrl = baseUrl + 'sampledata/canonicaldb.json';
+  var stateFileUrlCookie = 'sampleContainerStateFileUrl';
+
+  var useCaja;
+  var useCache;
+  var useDebug;
+  var doEvil;
+  var gadget;
+
+  var viewerId = 'john.doe';
+  var ownerId = 'canonical';
+
+  var viewMatches = /[?&]view=((?:[^#&]+|&amp;)+)/.exec(parentUrl);
+  var current_view = (viewMatches)
+      ? viewMatches[1]
+      : 'default';
+
+  /**
+   * Public Variables
+   */
+
+  /**
+   * Private Functions
+   */
+
+  function generateSecureToken() {
+    // TODO: Use a less silly mechanism of mapping a gadget URL to an appid
+    var appId = 0;
+    for (var i = 0; i < gadgetUrl.length; i++) {
+      appId += gadgetUrl.charCodeAt(i);
+    }
+    var fields = [ownerId, viewerId, appId, 'shindig', gadgetUrl, '0', 'default'];
+    for (var i = 0; i < fields.length; i++) {
+      // escape each field individually, for metachars in URL
+      fields[i] = escape(fields[i]);
+    }
+    return fields.join(':');
+  }
+
+  SampleContainerGadget = function(opt_params) {
+    shindig.BaseIfrGadget.call(this, opt_params);
+
+    // mix-in IfrGadget functions
+    for (var name in shindig.IfrGadget) if (shindig.IfrGadget.hasOwnProperty(name)) {
+      SampleContainerGadget[name] = shindig.IfrGadget[name];
+    }
+  };
+
+  SampleContainerGadget.inherits(shindig.BaseIfrGadget);
+
+  SampleContainerGadget.prototype.getAdditionalParams = function() {
+    var params = '';
+
+    if (useCaja) {
+      params += '&caja=1&libs=caja';
+    }
+    if (useDebug) {
+      params += '&debug=1';
+    }
+    return params;
+  };
+
+  shindig.container.gadgetClass = SampleContainerGadget;
+
+  function setEvilBit() {
+    sendRequestToServer('setevilness/' + doEvil, 'POST');
+  };
+
+  function reloadStateFile(opt_callback) {
+    sendRequestToServer('setstate', 'POST',
+        gadgets.json.stringify({'fileurl' : stateFileUrl}),
+        opt_callback);
+  };
+
+  function sendRequestToServer(url, method, opt_postParams, opt_callback, opt_excludeSecurityToken) {
+    // TODO: Should re-use the jsoncontainer code somehow
+    opt_postParams = opt_postParams || {};
+
+    var makeRequestParams = {
+      'CONTENT_TYPE' : 'JSON',
+      'METHOD' : method,
+      'POST_DATA' : opt_postParams};
+
+    if (!opt_excludeSecurityToken) {
+      url = socialDataPath + url + '?st=' + gadget.secureToken;
+    }
+
+    gadgets.io.makeNonProxiedRequest(url,
+      function(data) {
+        data = data.data;
+        if (opt_callback) {
+            opt_callback(data);
+        }
+      },
+      makeRequestParams,
+      'application/javascript'
+    );
+  };
+
+  function generateGadgets(metadata) {
+    // TODO: The gadget.js file should really have a clearGadgets method
+    shindig.container.view_ = current_view;
+    shindig.container.gadgets_ = {};
+    for (var i = 0; i < metadata.gadgets.length; i++) {
+      gadget = shindig.container.createGadget({'specUrl': metadata.gadgets[i].url,
+          'title': metadata.gadgets[i].title, 'userPrefs': metadata.gadgets[i].userPrefs});
+      // Shindigs rpc code uses direct javascript calls when running on the same domain
+      // to simulate cross-domain when running sample container we replace
+      // 'localhost' with '127.0.0.1'
+      var iframeBaseUrl = baseUrl.replace('localhost', '127.0.0.1') + 'gadgets/';
+
+      gadget.setServerBase(iframeBaseUrl);
+      gadget.secureToken = escape(generateSecureToken());
+      shindig.container.addGadget(gadget);
+    }
+
+    shindig.container.layoutManager.setGadgetChromeIds(['gadget-chrome']);
+    reloadStateFile(function() {
+      shindig.container.renderGadgets();
+    });
+  };
+
+  function refreshGadgets(metadata) {
+    // TODO: The gadget.js file should really have a getGadgets method
+    for (var gadget in shindig.container.gadgets_) {
+      var gadgetMetadata = metadata.gadgets[0];
+      shindig.container.gadgets_[gadget].title = gadgetMetadata.title;
+      shindig.container.gadgets_[gadget].specUrl = gadgetMetadata.url;
+      shindig.container.gadgets_[gadget].userPrefs = gadgetMetadata.userPrefs;
+      shindig.container.gadgets_[gadget].secureToken = escape(generateSecureToken());
+    }
+    reloadStateFile(function() {
+      shindig.container.refreshGadgets();
+    });
+  }
+
+  function requestGadgetMetaData(opt_callback) {
+    var request = {
+      context: {
+        country: 'default',
+        language: 'default',
+        view: current_view,
+        container: 'default'
+      },
+      gadgets: [{
+        url: gadgetUrl,
+        moduleId: 1
+      }]
+    };
+
+    sendRequestToServer(baseUrl + 'gadgets/metadata', 'POST',
+        gadgets.json.stringify(request), opt_callback, true);
+  }
+
+  /**
+   * Public Functions
+   */
+  shindig.samplecontainer.initSampleContainer = function() {
+     // Upon initial load, check for the cache query parameter (we don't want
+     // to overwrite when clicking "refresh all")
+     var cacheUrlMatches = /[?&]cache=([01])/.exec(parentUrl);
+     if (cacheUrlMatches && cacheUrlMatches[1] == '0') {
+       document.getElementById('useCacheCheckbox').checked = false;
+     }
+     gadgets.pubsubrouter.init(function() { return gadgetUrl; });
+  };
+
+  shindig.samplecontainer.initGadget = function() {
+    // Fetch cookies
+    var cookieGadgetUrl = decodeURIComponent(shindig.cookies.get(gadgetUrlCookie));
+    if (cookieGadgetUrl && cookieGadgetUrl != 'undefined') {
+      gadgetUrl = cookieGadgetUrl;
+    }
+
+    var cookieStateFileUrl = decodeURIComponent(shindig.cookies.get(stateFileUrlCookie));
+    if (cookieStateFileUrl && cookieStateFileUrl != 'undefined') {
+      stateFileUrl = cookieStateFileUrl;
+    }
+
+    // Setup state file
+    document.getElementById('stateFileUrl').value = stateFileUrl;
+
+    // Render gadget
+    document.getElementById('gadgetUrl').value = gadgetUrl;
+
+    // Viewer and Owner
+    document.getElementById('viewerId').value = viewerId;
+    document.getElementById('ownerId').value = ownerId;
+
+    requestGadgetMetaData(generateGadgets);
+  };
+
+  shindig.samplecontainer.unpackFormState = function() {
+    useCaja = document.getElementById('useCajaCheckbox').checked;
+    useCache = document.getElementById('useCacheCheckbox').checked;
+    useDebug = document.getElementById('useDebugCheckbox').checked;
+    doEvil = document.getElementById('doEvilCheckbox').checked;
+  };
+
+  shindig.samplecontainer.changeGadgetUrl = function() {
+    shindig.samplecontainer.unpackFormState();
+    shindig.container.nocache_ = useCache ? 0 : 1;
+
+    // TODO(felix8a): implement in server
+    //setEvilBit();
+
+    stateFileUrl = document.getElementById('stateFileUrl').value;
+    shindig.cookies.set(stateFileUrlCookie, encodeURIComponent(stateFileUrl));
+
+    viewerId = document.getElementById('viewerId').value;
+    ownerId = document.getElementById('ownerId').value;
+    gadgetUrl = document.getElementById('gadgetUrl').value;
+
+    shindig.cookies.set(gadgetUrlCookie, encodeURIComponent(gadgetUrl));
+
+    requestGadgetMetaData(refreshGadgets);
+  };
+
+  shindig.samplecontainer.dumpStateFile = function() {
+    sendRequestToServer('dumpstate', 'GET', null,
+      function(data) {
+        if (!data) {
+          alert('Could not dump the current state.');
+        }
+        document.getElementById('gadgetState').innerHTML
+          = gadgets.json.stringify(data);
+      }
+    );
+  };
+
+  shindig.samplecontainer.sendHello = function() {
+    gadgets.pubsubrouter.publish('helloworld', 'hello from the container');
+  };
+
+  osapi.messages = {};
+  osapi.messages.requestSend = function(request, callback) {
+    alert('osapi.messages.requestSend called');
+    callback({});
+  };
+
+  osapi.requestShareApp = function(request, callback) {
+    alert('osapi.requestShareApp called');
+    callback({});
+  };
+
+  osapi.requestPermission = function(request, callback) {
+    alert('osapi.requestPermission called');
+    callback({});
+  };
+
+})();
diff --git a/trunk/content/containers/deprecated/samplecontainer/state-basicfriendlist.xml b/trunk/content/containers/deprecated/samplecontainer/state-basicfriendlist.xml
new file mode 100644
index 0000000..8afe841
--- /dev/null
+++ b/trunk/content/containers/deprecated/samplecontainer/state-basicfriendlist.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<container>
+  <people>
+    <person id="john.doe" name="John Doe" gender="M">
+      <friend>jane.doe</friend>
+      <friend>george.doe</friend>
+      <friend>maija.m</friend>
+      <count type="inbox">10</count>
+      <address type="office">
+        <latitude>37.78940406684465</latitude>
+        <longitude>-122.40046262741089</longitude>
+        <streetAddress>55 2nd Street, Suite 300</streetAddress>
+        <locality>San Francisco</locality>
+        <region>CA</region>
+        <postalCode>94105</postalCode>
+        <country>United States</country>
+      </address>
+    </person>
+    <person id="jane.doe" name="Jane Doe" phone="867-5309" gender="F">
+      <friend>john.doe</friend>
+      <count type="inbox">3</count>
+    </person>
+    <person id="george.doe" name="George Doe" gender="M">
+      <friend>john.doe</friend>
+    </person>
+    <person id="mario.rossi" name="Mario Rossi" gender="M">
+    </person>
+    <person id="maija.m" name="Maija Meikäläinen" gender="F">
+    </person>
+  </people>
+
+  <personAppData>
+    <data person="george.doe" field="count">2</data>
+    <data person="jane.doe" field="count">7</data>
+  </personAppData>
+
+  <activities>
+    <stream title="jane's photos" userId="jane.doe">
+      <activity title="Jane just posted a photo of a monkey" id="1"
+          body="and she thinks you look like him!">
+        <mediaItem type="IMAGE" mimeType="image/jpeg" url="http://animals.nationalgeographic.com/staticfiles/NGS/Shared/StaticFiles/animals/images/primary/black-spider-monkey.jpg"></mediaItem>
+        <mediaItem type="IMAGE" mimeType="image/jpeg" url="http://image.guardian.co.uk/sys-images/Guardian/Pix/gallery/2002/01/03/monkey300.jpg"></mediaItem>
+      </activity>
+      <activity title="Jane says George likes yoda!" id="1"
+          body="or is it you?">
+        <mediaItem type="IMAGE" mimeType="image/jpeg" url="http://www.funnyphotos.net.au/images/fancy-dress-dog-yoda-from-star-wars1.jpg"></mediaItem>
+      </activity>
+    </stream>
+  </activities>
+  
+  <applications>
+  	<application id="6729">
+		<user>john.doe</user>
+		<user>jane.doe</user>
+	</application>
+  </applications>
+
+</container>
diff --git a/trunk/content/containers/deprecated/samplecontainer/state-smallfriendlist.xml b/trunk/content/containers/deprecated/samplecontainer/state-smallfriendlist.xml
new file mode 100644
index 0000000..cec4b3a
--- /dev/null
+++ b/trunk/content/containers/deprecated/samplecontainer/state-smallfriendlist.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<container>
+  <people>
+    <person id="john.doe" name="John Doe" gender="M">
+      <friend>jane.doe</friend>
+    </person>
+    <person id="jane.doe" name="Jane Doe" phone="867-5309" gender="F">
+      <friend>john.doe</friend>
+    </person>
+  </people>
+
+  <personAppData>
+    <data person="jane.doe" field="count">7</data>
+  </personAppData>
+
+  <activities>
+    <stream title="jane's photos" userId="jane.doe">
+      <activity title="Jane just posted a photo of a monkey" id="1"
+          body="and she thinks you look like him!">
+        <mediaItem type="IMAGE" mimeType="image/jpeg" url="http://animals.nationalgeographic.com/staticfiles/NGS/Shared/StaticFiles/animals/images/primary/black-spider-monkey.jpg"></mediaItem>
+        <mediaItem type="IMAGE" mimeType="image/jpeg" url="http://image.guardian.co.uk/sys-images/Guardian/Pix/gallery/2002/01/03/monkey300.jpg"></mediaItem>
+      </activity>
+      <activity title="Jane says George likes yoda!" id="1"
+          body="or is it you?">
+        <mediaItem type="IMAGE" mimeType="image/jpeg" url="http://www.funnyphotos.net.au/images/fancy-dress-dog-yoda-from-star-wars1.jpg"></mediaItem>
+      </activity>
+    </stream>
+  </activities>
+
+</container>
diff --git a/trunk/content/containers/deprecated/samplecontainer/state.dtd b/trunk/content/containers/deprecated/samplecontainer/state.dtd
new file mode 100644
index 0000000..8e9f1f1
--- /dev/null
+++ b/trunk/content/containers/deprecated/samplecontainer/state.dtd
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+<!ELEMENT container (people, appId?,
+    personAppData?, activities?)>
+
+<!ELEMENT people (person*)>
+
+<!ELEMENT person (friend*, count*, address*)>
+<!ATTLIST person id CDATA #REQUIRED
+    name CDATA #IMPLIED
+    gender CDATA #IMPLIED
+    phone CDATA #IMPLIED
+    thumbnailUrl CDATA #IMPLIED
+    profileUrl CDATA #IMPLIED>
+
+<!ELEMENT friend (#PCDATA)>
+
+<!ELEMENT count (#PCDATA)>
+<!ATTLIST count type CDATA #REQUIRED>
+
+<!ELEMENT address (latitude?, longitude?, streetAddress?, locality?, region?,
+    postalCode?, country?, extendedAddress?, poBox?, unstructuredAddress?)>
+<!ATTLIST address type CDATA #REQUIRED>
+
+<!ELEMENT latitude (#PCDATA)>
+<!ELEMENT longitude (#PCDATA)>
+<!ELEMENT streetAddress (#PCDATA)>
+<!ELEMENT locality (#PCDATA)>
+<!ELEMENT region (#PCDATA)>
+<!ELEMENT postalCode (#PCDATA)>
+<!ELEMENT country (#PCDATA)>
+<!ELEMENT extendedAddress (#PCDATA)>
+<!ELEMENT poBox (#PCDATA)>
+<!ELEMENT unstructuredAddress (#PCDATA)>
+
+<!ELEMENT appId (#PCDATA)>
+
+<!ELEMENT personAppData (data*)>
+
+<!ELEMENT data (#PCDATA)>
+<!ATTLIST data field CDATA #REQUIRED person CDATA #IMPLIED>
+
+<!ELEMENT activities (stream*)>
+
+<!ELEMENT stream (activity*)>
+<!ATTLIST stream title CDATA #REQUIRED
+    url CDATA #IMPLIED
+    userId CDATA #IMPLIED
+    sourceUrl CDATA #IMPLIED
+    faviconUrl CDATA #IMPLIED>
+
+<!ELEMENT activity (mediaItem*)>
+<!ATTLIST activity title CDATA #REQUIRED
+    id CDATA #REQUIRED
+    externalId CDATA #IMPLIED
+    body CDATA #IMPLIED
+    url CDATA #IMPLIED
+    postedTime CDATA #IMPLIED>
+
+<!ELEMENT mediaItem (#PCDATA)>
+<!ATTLIST mediaItem mimeType CDATA #REQUIRED
+    url CDATA #REQUIRED
+    type CDATA #IMPLIED>
\ No newline at end of file
diff --git a/trunk/content/containers/embeddedexperiences/AlbumViewer.xml b/trunk/content/containers/embeddedexperiences/AlbumViewer.xml
new file mode 100644
index 0000000..af09002
--- /dev/null
+++ b/trunk/content/containers/embeddedexperiences/AlbumViewer.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+	<ModulePrefs title="Photo Album Viewer" description="View Photos From An Album" height="400" width="650">
+		<Require feature="embedded-experiences"></Require>
+        <Require feature="open-views"></Require>
+	</ModulePrefs>
+	<Content type="html" view="embedded, default">
+   <![CDATA[
+     <style type="text/css">
+       .photo {
+         float: left;
+         margin-left: 20px;
+         margin-right: 20px;
+         margin-bottom: 70px;
+         maring-top: 20px;
+         padding: 2px 2px 2px 2px;
+         border: 1px solid black;
+       }
+
+       #wrapper {
+         border: 1px solid black;
+       }
+
+       #header {
+         font-size: 120%;
+         padding: 10px 10px 10px 10px;
+         color: #0F67A1;
+       }
+
+       .clear {
+         clear: both;
+       }
+     </style>
+
+
+     <script type="text/javascript">
+       function createAlbumHTML(context){
+         var photos = context.photoUrls;
+         var result = '';
+         for(var i = 0; i < photos.length; i++){
+           result = result + '<div class="photo"><img src="' + gadgets.io.getProxyUrl(photos[i]) + '"/></div>';
+         }
+         document.getElementById('header').innerHTML = context.albumName;
+         document.getElementById('album').innerHTML = result;
+         gadgets.views.setReturnValue('Rendered Album');
+       }
+       gadgets.util.registerOnLoadHandler(function() {
+         gadgets.ee.registerContextListener(createAlbumHTML);
+       });
+     </script>
+
+     <div id="wrapper">
+       <div id="header"></div>
+       <div id="album"></div>
+       <div class="clear"/>
+     </div>
+  ]]>
+	</Content>
+</Module>
diff --git a/trunk/content/containers/embeddedexperiences/BlogViewer.xml b/trunk/content/containers/embeddedexperiences/BlogViewer.xml
new file mode 100644
index 0000000..5fad6d5
--- /dev/null
+++ b/trunk/content/containers/embeddedexperiences/BlogViewer.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+    <ModulePrefs title="Blog Viewer" description="Blog Viewer" height="400" width="650">
+        <Require feature="embedded-experiences"></Require>
+        <Require feature="dynamic-height"></Require>
+        <Require feature="open-views"></Require>
+    </ModulePrefs>
+    <Content type="html" view="embedded_canvas, default">
+        <![CDATA[
+            <style type="text/css">
+                #wrapper {
+                    border: 1px solid black;
+                }
+            </style>
+
+            <script type="text/javascript">
+                function updateBlogViewerInfo(context) {
+                    if(context.openSocial) {
+                        var associatedContext = context.openSocial.associatedContext;
+                        if(associatedContext) {
+                            document.getElementById('source').innerHTML = "<p>The associated context source id is " + associatedContext.id + "</p>";
+                            document.getElementById('type').innerHTML = "<p>The associated context source type is " + associatedContext.type + "</p>";
+                            document.getElementById('target').innerHTML = "<p>The blog title is " + associatedContext.objectReference.target.displayName + "<p>";
+                            document.getElementById('photo').innerHTML = "<p>The title of the new photo is " + associatedContext.objectReference.object.summary + "<p>";
+                        }
+                    }
+                }
+
+                gadgets.util.registerOnLoadHandler(function() {
+                    gadgets.ee.registerContextListener(updateBlogViewerInfo);
+                });
+            </script>
+
+            <div id="wrapper">
+                <div id="source"></div>
+                <div id="type"></div>
+                <div id="target"></div>
+                <div id="photo"></div>
+            </div>
+        ]]>
+    </Content>
+</Module>
diff --git a/trunk/content/containers/embeddedexperiences/EEContainer.js b/trunk/content/containers/embeddedexperiences/EEContainer.js
new file mode 100644
index 0000000..00c5453
--- /dev/null
+++ b/trunk/content/containers/embeddedexperiences/EEContainer.js
@@ -0,0 +1,181 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+//When the document is ready kick off the request so we can render the activity stream
+$(document).ready(function() {
+  osapi.activitystreams.get({userId: 'john.doe'}).execute(function(response){
+    renderAS(response);
+  });
+});
+
+
+
+//Initiate the common container code and register any RPC listeners for embedded experiences
+var config = {};
+config[osapi.container.ContainerConfig.RENDER_DEBUG] = '1';
+var CommonContainer = new osapi.container.Container(config);
+CommonContainer.init = new function() {
+
+  CommonContainer.views.createElementForEmbeddedExperience = function(rel, opt_gadgetInfo,
+          opt_viewTarget, opt_coordinates){
+    if(opt_gadgetInfo && !opt_gadgetInfo.error) {
+      var title = opt_gadgetInfo.modulePrefs.title;
+      if(title) {
+        $('#title').html(opt_gadgetInfo.modulePrefs.title);
+      }
+    }
+    var top = $(rel).offset().top,
+      left =  $(rel).offset().left;
+    if(opt_viewTarget == 'FLOAT' && opt_coordinates) {
+      $('#preview').css({
+        top: (top + opt_coordinates.top) + 'px',
+        left: (left + opt_coordinates.left) + 'px',
+        'margin-top': '0px',
+        position: 'absolute'
+      });
+    }
+    else {
+      $('#preview').css({
+        'margin-top': top +'px',
+        top: 'auto',
+        left: 'auto',
+        position: 'static'
+      });
+    }
+
+    return $('#previewSite').get(0);
+  };
+
+  CommonContainer.views.destroyElement = function(site) {
+    CommonContainer.ee.close(site);
+    $('#title').html('');
+  };
+}
+
+/**
+ * Renders the activity stream on the page
+ * @param stream the activity stream json.
+ * @return void.
+ */
+function renderAS(stream) {
+  jQuery.each(stream.list, createAccordianEntry);
+  $('#accordion').accordion({
+    clearStyle: true,
+    active: false,
+    change: function(event, ui) {
+      closeCurrentGadget();
+      onAccordionChange(stream, event, ui);
+    }
+
+  });
+}
+
+/**
+ * Closes the current gadget when a new accordian is selected.
+ */
+var currentEESite;
+function closeCurrentGadget() {
+  if (currentEESite)
+    CommonContainer.ee.close(currentEESite);
+
+  var previewSite = $('#previewSite').get(0);
+  var previewChildren = previewSite.childNodes;
+  if (previewChildren.length > 0) {
+    var iframe = previewChildren[0];
+    var iframeId = iframe.getAttribute('id');
+    var site = CommonContainer.getGadgetSiteByIframeId_(iframeId);
+    CommonContainer.ee.close(site);
+  }
+  $('#title').html('');
+}
+
+/**
+ * Called when a new accordian pane is opened.
+ * @param stream the activity stream for the accordian.
+ * @param event the event that occurred.
+ * @param ui the ui elements changing.
+ * @return void.
+ */
+
+function onAccordionChange(stream, event, ui) {
+  var id = ui.newHeader.context.id;
+  var localStream = stream;
+  var entry = localStream.list[id];
+  var extensions = entry.openSocial;
+  if (extensions) {
+    var embed = extensions.embed;
+    if (embed) {
+      var eeElement = $('#ee' + id).get(0);
+      var urlRenderingParams = {
+          'height' : 400,
+          'width' : 650
+      };
+
+      // Add additional container context
+      var containerContext = {};
+      containerContext.associatedContext = {"id" : entry.id , "type" : "opensocial.ActivityEntry",
+          "objectReference" : entry};
+
+      CommonContainer.ee.navigate(eeElement, embed, {'urlRenderParams' : urlRenderingParams},
+         function(site, metaData) {
+           console.log('Embedded Experiences callback called');
+           console.log(gadgets.json.stringify(metaData));
+           currentEESite = site;
+         }, containerContext);
+    }
+  }
+}
+
+/**
+ * Called for each activity entry and adds the necessary HTML to the page.
+ * Check if preferredExperience is added to the EE model and set title accordingly.
+ * @param i the item in the activity stream we are currently rendering.
+ * @param entry the activity stream entry json.
+ * @return void.
+ */
+function createAccordianEntry(i, entry) {
+  var title = entry.title;
+
+  // Lets try to see if the activity entry has preferredExperience extension.
+  var extensions = entry.openSocial;
+  if(extensions) {
+    var embed = extensions.embed;
+    if(embed && embed.preferredExperience) {
+      var linkText = getPreferredExperienceLinkText(embed[osapi.container.ee.DataModel.PREFERRED_EXPERIENCE]);
+      if(linkText) {
+        title = linkText;
+      }
+    }
+  }
+  var result = '<h3 id=' + i + '><a href="#">' + title + '</a></h3><div>';
+  if (entry.body)
+    result = result + '<p>' + entry.body + '</p>';
+  result = result + '<div id="ee' + i + '"></div></div>';
+
+  $('#accordion').append(result);
+}
+
+function getPreferredExperienceLinkText(preferredExperience) {
+  if(preferredExperience && preferredExperience.display) {
+    if(preferredExperience.display.type !== osapi.container.ee.DisplayType.TEXT) {
+      return null;
+    }
+    var linkText = preferredExperience.display.label;
+    return linkText;
+  }
+}
diff --git a/trunk/content/containers/embeddedexperiences/PhotoList.xml b/trunk/content/containers/embeddedexperiences/PhotoList.xml
new file mode 100644
index 0000000..899b0a5
--- /dev/null
+++ b/trunk/content/containers/embeddedexperiences/PhotoList.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+    <ModulePrefs title="Photo List" description="View Photos From An Album" height="400" width="650">
+        <Require feature="embedded-experiences"></Require>
+        <Require feature="open-views"></Require>
+    </ModulePrefs>
+    <Content type="html" view="embedded, default">
+   <![CDATA[
+        <style type="text/css">
+        .photo {
+            float: left;
+            margin-left: 20px;
+            margin-right: 20px;
+            margin-bottom: 70px;
+            maring-top: 20px;
+            padding: 2px 2px 2px 2px;
+            border: 1px solid black;
+        }
+
+        #wrapper{
+            border: 1px solid black;
+        }
+
+        #header{
+            font-size: 120%;
+            padding: 10px 10px 10px 10px;
+            color: #0F67A1;
+        }
+
+        .clear{
+            clear: both;
+        }
+     </style>
+
+	 <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
+     <script type="text/javascript">
+
+         var currentSite;
+         var context;
+         function createAlbumHTML(context){
+             window.context=context;
+             var photos = [].concat(context.photoUrls);
+             var result = "";
+             for(var i = 0; i < photos.length; i++){
+                 count = i+1;
+                 result = result + '<li id="photo' + i + '" style="text-decoration: underline; color: blue; cursor: pointer; display:inline-block" onclick="showPreviewPhoto(\''+i+'\',\''+context.eeGadget+'\')">Photo '+count+'</li></br>';
+             }
+             $('#header').html(context.albumName);
+             $('#album').html(result);
+         };
+         gadgets.util.registerOnLoadHandler(function() {
+           gadgets.ee.registerContextListener(createAlbumHTML);
+         });
+
+         function showPreviewPhoto(index, eeGadget) {
+            var coordinates = $('#photo' + index).offset();
+            coordinates.left = coordinates.left + $('#photo' + index).width();
+
+            var navigateCallback = function(site, metadata){
+              currentSite = site;
+              console.log("Nagivate callback");
+            };
+            var returnCallback = function(returnValue){
+              console.log("Return Value: " + returnValue);
+            };
+            
+            var eeDataModel;
+            // Test the use case of Url data model
+            if (index == context.photoUrls.length-1) {
+              eeDataModel =
+                  '<embed>'
+                +   '<url>' + context.photoUrls[index] + '</url>'
+                +   '<context>'
+                +     '<albumName>' + context.albumName + '</albumName>'
+                +     '<photoUrls>' + context.photoUrls[index] + '</photoUrls>'
+                +   '</context>'
+                + '</embed>';
+            } else {
+              eeDataModel = {
+                gadget: eeGadget,
+                context: {
+                  albumName: context.albumName,
+                  photoUrls: [context.photoUrls[index]]
+                }
+              };
+            }
+            gadgets.views.openEmbeddedExperience(returnCallback, navigateCallback, eeDataModel, {
+              'viewTarget' : index == 0 ? 'preview' : 'FLOAT',
+              'coordinates' : coordinates
+            });
+         };
+
+         function closePreview(){
+            if(currentSite != null){
+                gadgets.views.close(currentSite);
+            }
+            return false;
+         };
+        </script>
+
+     <div id="wrapper">
+         <div id="header"></div>
+         <div id="album"></div>
+         <div class="clear"/>
+         <a href="#" onclick="closePreview();">Close Preview</a>
+     </div>
+  ]]>
+    </Content>
+</Module>
diff --git a/trunk/content/containers/embeddedexperiences/index.html b/trunk/content/containers/embeddedexperiences/index.html
new file mode 100644
index 0000000..6d5b8c7
--- /dev/null
+++ b/trunk/content/containers/embeddedexperiences/index.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">    
+  <head>
+        <link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/themes/base/jquery-ui.css" rel="stylesheet" type="text/css"/>
+		<style type="text/css">
+			html,body{
+				width: 100%;
+				height: 100%;
+			}
+			#content {
+				width: 100%;
+				height: 100%;
+			}
+
+			#preview {
+				height: 100%;
+				width: 45%;
+				float: left;
+				margin-left: 10px;
+			}
+
+			#previewSite {
+				height: 100%;
+			}
+
+			#accordion {
+				width: 50%;
+				float: left;
+			}
+		</style>
+		<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
+        <script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8/jquery-ui.min.js"></script>
+        <script type="text/javascript" src="../../../gadgets/js/core:container:rpc:open-views.js?c=1&debug=1&container=default"></script>
+        <script type="text/javascript" src="EEContainer.js"></script>
+    </head>
+    <body style="font-size:62.5%;">
+      <div id="content">
+        <div id="accordion">
+        </div>
+        <div id="preview">
+          <div id="title"></div>
+          <div id="previewSite"></div>
+        </div>
+        <div style="clear: both;"/>
+      </div>
+    </body>
+</html>
\ No newline at end of file
diff --git a/trunk/content/gadgets/ActivityStreams/ActivityStreamGadget.xml b/trunk/content/gadgets/ActivityStreams/ActivityStreamGadget.xml
new file mode 100644
index 0000000..30890f5
--- /dev/null
+++ b/trunk/content/gadgets/ActivityStreams/ActivityStreamGadget.xml
@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+	<ModulePrefs title="ActivityStreams Gadget!">
+		<Require feature="opensocial-0.9"/>
+		<Require feature="osapi"/>
+		<Require feature="dynamic-height"/>
+	</ModulePrefs>
+	
+	<Content type="html">
+	<![CDATA[
+		<script type="text/javascript" src="OpenSocialWrapper.js"></script>
+		<script type="text/javascript" src="ActivityStreamsRender.js"></script>
+		
+		<script type="text/javascript">
+			social = new OpenSocialWrapper();
+			render = new ActivityStreamsRender();
+			
+			// TODO: move this stuff into ActivityStreamRender (if you can...)
+			// Renders retrieval of an ActivityEntry by ID
+			function renderActivityEntryId(div, callback) {
+				var html = "<h2>Work with an ActivityEntry</h2>";
+				html += "ActivityEntry ID: <input type='text' size=40 id='activityEntryId'>";
+				html += "<input type='button' value='Retrieve' onclick='retrieveActivityEntryId()'>";
+				html += "<input type='button' value='Delete' onclick='deleteActivityEntryId()'><br>";
+				html += "Note: you must be the owner of the ActivityEntry to retrieve it.";
+				html += "<textarea id='activityEntryText' cols=75 rows=10>No entry to display...</textarea><br>";
+				document.getElementById(div).innerHTML = html;
+				callback();
+			}
+			
+			// Deletes the activity entry
+			function deleteActivityEntryId() {
+				social.deleteActivityEntryById(document.getElementById('activityEntryId').value, function(response) {
+					document.getElementById('activityEntryText').value = 'No entry to display...';
+					render.renderActivityEntries('activityEntryies', refresh);
+				});
+			}
+			
+			// Retrieves the activity entry
+			function retrieveActivityEntryId() {
+				social.getActivityEntryById(document.getElementById('activityEntryId').value, function(response) {
+					document.getElementById('activityEntryText').value = JSON.stringify(response);
+				});
+			}
+			
+			// Creation form for activity entry
+			function renderCreateActivityEntry(div, callback) {
+				var htmlCreateActivityEntry = "<h1>ActivityStreams</h1>";
+				htmlCreateActivityEntry += "Demonstrates use of the ActivityStream service in Apache Shindig.  This implementation follows the JSON draft specfication: http://activitystrea.ms/head/json-activity.html<br>";
+				htmlCreateActivityEntry += "<h2>Create an ActivityEntry</h2>";
+				htmlCreateActivityEntry += "<table>";
+				htmlCreateActivityEntry += "<tr>";
+				htmlCreateActivityEntry += "<td>Title</td>";
+				htmlCreateActivityEntry += "<td><input type='text' size=50 id='activityEntryTitle'></td>"
+				htmlCreateActivityEntry += "</tr>";
+				htmlCreateActivityEntry += "<tr>";
+				htmlCreateActivityEntry += "<td>Body</td>";
+				htmlCreateActivityEntry += "<td><input type='text' size=50 id='activityEntryBody'></td>"
+				htmlCreateActivityEntry += "</tr>";
+				htmlCreateActivityEntry += "<tr>";
+				htmlCreateActivityEntry += "<td>Object Name</td>";
+				htmlCreateActivityEntry += "<td><input type='text' size=50 id='activityObjectName'></td>"
+				htmlCreateActivityEntry += "</tr>";
+				htmlCreateActivityEntry += "<tr>";
+				htmlCreateActivityEntry += "<td>Object Summary</td>";
+				htmlCreateActivityEntry += "<td><input type='text' size=50 id='activityObjectSummary'></td>"
+				htmlCreateActivityEntry += "</tr>";
+				htmlCreateActivityEntry += "<tr>";
+				htmlCreateActivityEntry += "<td>Object Permalink</td>";
+				htmlCreateActivityEntry += "<td><input type='text' size=50 id='activityObjectPermalink'></td>"
+				htmlCreateActivityEntry += "</tr>";
+				htmlCreateActivityEntry += "</table>";
+				htmlCreateActivityEntry += "<table>";
+				htmlCreateActivityEntry += "<tr style='color:blue'>";
+				htmlCreateActivityEntry += "<td colspan='2'> Note: Per the ActivityStreams specification, only a single verb and object type are now supported.</td>";
+				htmlCreateActivityEntry += "</tr>";
+				htmlCreateActivityEntry += "<tr>";
+				htmlCreateActivityEntry += "<td>Verbs</td>";
+				htmlCreateActivityEntry += "<td>Object Types</td>";
+				htmlCreateActivityEntry += "</tr>";
+				htmlCreateActivityEntry += "<tr>";
+				htmlCreateActivityEntry += "<td><select MULTIPLE id='selectVerbs' size=10>";
+				htmlCreateActivityEntry += "<option value='markAsFavorite'>Mark as Favorite</option>";
+				htmlCreateActivityEntry += "<option value='startFollowing'>Start Following</option>";
+				htmlCreateActivityEntry += "<option value='markAsLiked'>Mark as Liked</option>";
+				htmlCreateActivityEntry += "<option value='makeFriend'>Make Friend</option>";
+				htmlCreateActivityEntry += "<option value='join'>Join</option>";
+				htmlCreateActivityEntry += "<option value='play'>Play</option>";
+				htmlCreateActivityEntry += "<option value='post'>Post</option>";
+				htmlCreateActivityEntry += "<option value='save'>Save</option>";
+				htmlCreateActivityEntry += "<option value='share'>Share</option>";
+				htmlCreateActivityEntry += "<option value='tag'>Tag</option>";
+				htmlCreateActivityEntry += "<option value='update'>Update</option>";
+				htmlCreateActivityEntry += "</select></td>";
+				htmlCreateActivityEntry += "<td><select MULTIPLE id='selectObjectTypes' size=10>";
+				htmlCreateActivityEntry += "<option value='article'>Article</option>";
+				htmlCreateActivityEntry += "<option value='audio'>Audio</option>";
+				htmlCreateActivityEntry += "<option value='bookmark'>Bookmark</option>";
+				htmlCreateActivityEntry += "<option value='comment'>Comment</option>";
+				htmlCreateActivityEntry += "<option value='file'>File</option>";
+				htmlCreateActivityEntry += "<option value='folder'>Folder</option>";
+				htmlCreateActivityEntry += "<option value='group'>Group</option>";
+				htmlCreateActivityEntry += "<option value='list'>List</option>";
+				htmlCreateActivityEntry += "<option value='note'>Note</option>";
+				htmlCreateActivityEntry += "<option value='person'>Person</option>";
+				htmlCreateActivityEntry += "<option value='photo'>Photo</option>";
+				htmlCreateActivityEntry += "<option value='photoAlbum'>Photo Album</option>";
+				htmlCreateActivityEntry += "<option value='place'>Place</option>";
+				htmlCreateActivityEntry += "<option value='playlist'>Playlist</option>";
+				htmlCreateActivityEntry += "<option value='product'>Product</option>";
+				htmlCreateActivityEntry += "<option value='review'>Review</option>";
+				htmlCreateActivityEntry += "<option value='service'>Service</option>";
+				htmlCreateActivityEntry += "<option value='status'>Status</option>";
+				htmlCreateActivityEntry += "<option value='video'>Video</option>";
+				htmlCreateActivityEntry += "</select></td>";
+				htmlCreateActivityEntry += "</table>";
+				htmlCreateActivityEntry += "<input type='button' value='Submit' onclick='createActivityEntry()'>";
+				htmlCreateActivityEntry += "<br><br>";
+				document.getElementById(div).innerHTML = htmlCreateActivityEntry;
+				callback();
+			}
+			function createActivityEntry() {
+				// Gather selected verbs
+				verbOptions = document.getElementById('selectVerbs');
+				selVerbs = [];
+				count = 0;
+				for(i = 0; i < verbOptions.options.length; i++) {
+					if(verbOptions.options[i].selected) {
+						selVerbs[count] = verbOptions.options[i].value;
+						count++;
+					}
+				}
+				
+				// Gather selected types
+				typeOptions = document.getElementById('selectObjectTypes');
+				selTypes = [];
+				count = 0;
+				for(i = 0; i < typeOptions.options.length; i++) {
+					if(typeOptions.options[i].selected) {
+						selTypes[count] = typeOptions.options[i].value;
+						count++;
+					}
+				}
+				
+				var title = blankToNull(document.getElementById('activityEntryTitle').value);
+				var body = blankToNull(document.getElementById('activityEntryBody').value)
+				var objectName = blankToNull(document.getElementById('activityObjectName').value);
+				var objectSummary = blankToNull(document.getElementById('activityObjectSummary').value);
+				var objectPermalink = blankToNull(document.getElementById('activityObjectPermalink').value);
+				social.postActivityEntry(title, body, selVerbs[0], viewer.id, viewer.name.formatted,
+												objectName, objectSummary, objectPermalink, selTypes[0],
+												function(response) {
+					render.renderActivityEntries('activityEntries', refresh);
+				});
+			}
+			function blankToNull(str) {
+				return (str == '' ? null : str);
+			}
+		
+			// Adjusts the window height
+			function refresh() {
+				gadgets.window.adjustHeight();
+			}
+		
+			// Initializes the gadget
+			function init() {
+				render.renderWelcome('welcome', refresh);
+				render.renderActivities('activities', refresh);
+				render.renderActivityEntries('activityEntries', refresh);
+				renderActivityEntryId('htmlGetEntry', refresh);
+				renderCreateActivityEntry('htmlCreateEntry', refresh);
+			}
+			
+			gadgets.util.registerOnLoadHandler(init);
+		</script>
+		<div id='welcome'></div>
+		<div id='activities'></div>
+		<div id='htmlCreateEntry'></div>
+		<div id='htmlGetEntry'></div>
+		<div id='activityEntries'></div>
+	]]>
+	</Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/ActivityStreams/ActivityStreamTemplate.xml b/trunk/content/gadgets/ActivityStreams/ActivityStreamTemplate.xml
new file mode 100644
index 0000000..abd7c96
--- /dev/null
+++ b/trunk/content/gadgets/ActivityStreams/ActivityStreamTemplate.xml
@@ -0,0 +1,49 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="ActivityStream Template">
+    <Require feature="opensocial-data"/>
+    <Require feature="opensocial-templates"/>
+    <Require feature="dynamic-height"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+	  <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
+        <os:PeopleRequest key="viewer" userId="@viewer" groupId="@self"/>
+        <os:ActivityStreamsRequest key="entries" userId="john.doe" groupId="@self"/>
+      </script>
+ 
+      <script type="text/os-template">
+      	${viewer.name.formatted}'s ActivityEntries:
+        <ul>
+          <li repeat="${entries}">
+            <span id="id${Context.Index}">${Cur.title}</span>
+          </li>
+        </ul>
+     </script>
+     
+     <script type="text/javascript">
+     	function init() {
+     		gadgets.window.adjustHeight();
+     	}
+     	gadgets.util.registerOnLoadHandler(init);
+     </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/ActivityStreams/ActivityStreamsRender.js b/trunk/content/gadgets/ActivityStreams/ActivityStreamsRender.js
new file mode 100644
index 0000000..fe75b90
--- /dev/null
+++ b/trunk/content/gadgets/ActivityStreams/ActivityStreamsRender.js
@@ -0,0 +1,134 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function ActivityStreamsRender() {
+
+	// Private member that wraps the OpenSocial API
+	var social = new OpenSocialWrapper();
+
+	// =================== PUBLIC ====================
+
+	// Renders the welcome text (viewer, owner, and friends)
+	this.renderWelcome = function(div, callback) {
+		social.loadPeople(function(response) {
+			viewer = response.viewer;
+			owner = response.owner;
+			var viewerFriends = response.viewerFriends;
+			var ownerFriends = response.ownerFriends;
+
+			var html = '<h1>Welcome ' + viewer.name.formatted + '!</h1>';
+			html += 'You are viewing ' + owner.name.formatted + "'s data. <br><br>";
+			html += 'Here is a list of your friends: <br>';
+			html += '<lu>';
+			for (i = 0; i < viewerFriends.list.length; i++) {
+				html += '<li>' + viewerFriends.list[i].name.formatted + '</li>';
+			}
+			html += '</lu>';
+			document.getElementById(div).innerHTML = html;
+			callback();
+		});
+	}
+
+	// Renders the activities
+	this.renderActivities = function(div, callback) {
+		social.loadActivities(function(response) {
+			var viewerActivities = response.viewerActivities.list;
+			var ownerActivities = response.ownerActivities.list;
+			var friendActivities = response.friendActivities.list;
+
+			var html = '<h1>Activities</h1>';
+			html += 'Demonstrates use of the Activities service in Apache Shindig.  The ActivityStreams service does not interfere with this service.<br><br>';
+			html += 'Activities for you and ' + owner.name.formatted + ':<br>';
+			html += "<table border='1'>";
+			html += '<tr>';
+			html += '<td>Name</td>';
+			html += '<td>Title</td>';
+			html += '<td>Body</td>';
+			html += '<td>Images</td>';
+			html += '</tr>';
+			html += processActivities(viewerActivities);
+			html += processActivities(ownerActivities);
+			html += processActivities(friendActivities);
+			html += '</table>';
+			document.getElementById(div).innerHTML = html;
+			callback();
+		});
+	}
+
+	// Renders activity entries
+	this.renderActivityEntries = function(div, callback) {
+		social.loadActivityEntries(function(response) {
+			var html = '';
+			viewerEntries = response.viewerEntries.list;
+			//ownerEntries = response.ownerEntries.list;
+			//friendEntries = response.friendEntries.list;
+			html = '<h2>ActivityEntries</h2>';
+			html += processActivityEntries(viewerEntries);
+			//html += processActivityEntries(ownerEntries);
+			//html += processActivityEntries(friendEntries);
+			if (viewerEntries.length == 0) {
+				html += '<tr><td>No entries to show!</td></tr>';
+			}
+			html += '</table><br><br>';
+			document.getElementById(div).innerHTML = html;
+			callback();
+		});
+	}
+
+	// ================== PRIVATE =====================
+
+	// Processes activities and returns the rendered HTML
+	function processActivities(activities) {
+		var html = '';
+		for (idx = 0; idx < activities.length; idx++) {
+			html += '<tr>';
+			html += '<td>' + activities[idx].userId + '</td>';
+			html += '<td>' + activities[idx].title + '</td>';
+			html += '<td>' + activities[idx].body + '</td>';
+			var mediaItems = activities[idx].mediaItems;
+			if (mediaItems != null) {
+				for (itemIdx = 0; itemIdx < mediaItems.length; itemIdx++) {
+					if (mediaItems[itemIdx].type == 'image') {
+						html += "<td><img src='" + mediaItems[itemIdx].url + "' width=150 height=150/></td>";
+					}
+				}
+			}
+			html += '</tr>';
+		}
+		return html;
+	}
+
+	// Processes activity entries and returns the rendered HTML
+	function processActivityEntries(entries) {
+		var html = '';
+		for (idx = 0; idx < entries.length; idx++) {
+			if (entries[idx].object.url && entries[idx].object.url != 'null') {
+				html += "<h3><a href='" + entries[idx].object.url + "'>" + entries[idx].title + '</a></h3>';
+			} else {
+				html += '<h3>' + entries[idx].title + '</h3>';
+			}
+			html += 'ID: ' + entries[idx].id + '<br>';
+			html += 'Actor: ' + entries[idx].actor.displayName + '<br>';
+			html += 'Posted: ' + entries[idx].published + '<br>';
+			if (entries[idx].content && entries[idx].content != 'null') {
+				html += 'Content: ' + entries[idx].content + '<br>';
+			}
+		}
+		return html;
+	}
+}
diff --git a/trunk/content/gadgets/ActivityStreams/OpenSocialWrapper.js b/trunk/content/gadgets/ActivityStreams/OpenSocialWrapper.js
new file mode 100644
index 0000000..e96532a
--- /dev/null
+++ b/trunk/content/gadgets/ActivityStreams/OpenSocialWrapper.js
@@ -0,0 +1,135 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function OpenSocialWrapper() {
+
+	// =============================== PEOPLE ===============================
+
+	/*
+	 * Loads the owner, the viewer, the owner's friends, and the viewer's
+	 * friends.  Response data is put into the variables owner, viewer,
+	 * ownerFriends, and viewerFriends, respectively.
+	 *
+	 * @param callback is the function to return the response to
+	 */
+	this.loadPeople = function(callback) {
+		var batch = osapi.newBatch();
+		batch.add('viewer', osapi.people.getViewer());
+		batch.add('owner', osapi.people.getOwner());
+		batch.add('viewerFriends', osapi.people.getViewerFriends());
+		batch.add('ownerFriends', osapi.people.getOwnerFriends());
+		batch.execute(callback);
+	}
+
+	this.loadViewerFriends = function(callback) {
+		osapi.people.getViewerFriends().execute(callback);
+	}
+
+	this.loadOwnerFriends = function(callback) {
+		osapi.people.getOwnerFriends().execute(callback);
+	}
+
+	// ========================= ACTIVITIES =============================
+	this.loadActivities = function(callback) {
+		var batch = osapi.newBatch();
+		batch.add('viewerActivities', osapi.activities.get({userId: '@viewer', groupId: '@self'}));
+		batch.add('ownerActivities', osapi.activities.get({userId: '@owner', groupId: '@self'}));
+		batch.add('friendActivities', osapi.activities.get({userId: '@viewer', groupId: '@friends'}));
+		batch.execute(callback);
+	}
+
+	this.loadViewerActivities = function(callback) {
+		var req = osapi.activities.get({userId: '@viewer', groupId: '@self'});
+		req.execute(callback);
+	}
+
+	this.loadViewerFriendsActivities = function(callback) {
+		var req = osapi.activities.get({userId: '@viewer', groupId: '@friends'});
+		req.execute(this.onLoadActivitiesFriends);
+	}
+
+	this.loadOwnerActivities = function(callback) {
+		var req = osapi.activities.get({userId: '@owner', groupId: '@self'});
+		req.execute(callback);
+	}
+
+
+	// ========================= ACTIVITY STREAMS =============================
+	this.loadActivityEntries = function(callback) {
+		var batch = osapi.newBatch();
+		batch.add('viewerEntries', osapi.activitystreams.get({userId: '@viewer', groupId: '@self'}));
+		//batch.add('ownerEntries', osapi.activitystreams.get({userId: '@owner', groupId: '@self'}));
+		//batch.add('friendEntries', osapi.activitystreams.get({userId: '@viewer', groupId: '@friend'}));
+		batch.execute(callback);
+	}
+
+	this.loadViewerActivityEntries = function(callback) {
+		var params = {userId: '@viewer', groupId: '@self'};
+		osapi.activitystreams.get(params).execute(callback);
+	}
+
+	this.loadOwnerActivityEntries = function(callback) {
+		var params = {userId: '@owner', groupId: '@self'};
+		osapi.activitystreams.get(params).execute(callback);
+	}
+
+	this.loadViewerFriendsActivityEntries = function(callback) {
+		var params = {userId: '@viewer', groupId: '@friends'};
+		osapi.activitystreams.get(params).execute(callback);
+	}
+
+	this.postActivityEntry = function(title, content, verb, actorId, actorName, objectName, objectSummary,
+									  objectPermalink, objectType, callback) {
+		var params = {
+			userId: '@viewer',
+			groupId: '@self',
+			activity: {
+				published: '2010-04-27T06:02:36+0000',
+				title: title,
+				content: content,
+				actor: {
+					id: actorId,
+					displayName: actorName
+				},
+				verb: verb,
+				object: {
+					id: new	Date().getTime(),
+					displayName: objectName,
+					url: objectPermalink,
+					objectType: objectType,
+					summary: objectSummary
+				}
+			}
+		};
+		osapi.activitystreams.create(params).execute(callback);
+	}
+
+	this.deleteActivityEntryById = function(activityEntryId, callback) {
+		var params = {
+			userId: '@viewer',
+			groupId: '@self',
+			activityId: activityEntryId
+		};
+		osapi.activitystreams['delete'](params).execute(callback);
+	}
+
+	this.getActivityEntryById = function(activityEntryId, callback) {
+		var params = {activityId: activityEntryId};
+		osapi.activitystreams.get(params).execute(callback);
+	}
+}
diff --git a/trunk/content/gadgets/ContainerGadgetDomainTest.xml b/trunk/content/gadgets/ContainerGadgetDomainTest.xml
new file mode 100644
index 0000000..e395686
--- /dev/null
+++ b/trunk/content/gadgets/ContainerGadgetDomainTest.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs
+    title="Test Gadget and Container Domain Configuration"
+    description="Tests Gadget and Container domain configuration by trying to access container page information from within a gadget"
+  >
+  </ModulePrefs>
+  <Content type="html"><![CDATA[
+    <html>
+      <head>
+        <link href='http://fonts.googleapis.com/css?family=Lora|Istok+Web:700' rel='stylesheet' type='text/css'/>
+        <link rel="stylesheet" href="http://pivotal.github.com/jasmine/css/docco.css"/>
+        <link rel="stylesheet" href="http://pivotal.github.com/jasmine/css/jasmine_docco-1.3.1.css"/>
+        <link rel="stylesheet" href="http://pivotal.github.com/jasmine/lib/jasmine-1.3.1/jasmine.css"/>
+        <style type="text/css">
+          body {
+            margin: 0;
+            overflow: auto;
+          }
+          .exceptions {
+            display: none;
+          }
+          #HTMLReporter .stackTrace {
+            max-height: 5em;
+          }
+        </style>
+        <script src="http://pivotal.github.com/jasmine/lib/jasmine-1.3.1/jasmine.js"></script>
+        <script src="http://pivotal.github.com/jasmine/lib/jasmine-1.3.1/jasmine-html.js"></script>
+
+        <!-- Begin Tests -->
+        <script type="text/javascript">
+          describe("This gadget", function() {
+            it("is rendering over https", function() {
+              expect(window.location.protocol).toMatch(/^https/);
+            });
+
+            it("is rendering on a locked domain", function() {
+              expect(gadgets.config.get()['core.io'].proxyUrl).not.toContain('%host%');
+            });
+
+            var undef, 
+                parentLoc = undef;
+            it("cannot access the parent window", function() {
+              try {
+                parentLoc = window.parent.location.href;
+              } catch (ignore) {}
+              expect(parentLoc).toBeUndefined();
+            });
+
+            if (parentLoc == undef) {
+	            it("cannot access the parent window by lowering its domain", function() {
+	              var parts = document.domain.split('.'),
+	                  undef;
+	              parts.shift();
+	              while(parts.length > 1) {
+	                document.domain = parts.join('.');
+	
+	                var parentLoc = undef;
+	                try {
+	                  parentLoc = window.parent.location.href;
+	                } catch (ignore) {}
+	                expect(parentLoc).toBeUndefined();
+	                parts.shift();
+	              }
+	            });
+	          }
+          });
+        </script>
+
+        <!-- End Tests -->
+        <script type="text/javascript">
+          gadgets.util.registerOnLoadHandler(function go() {
+            // The links jasmine generates in the report break gadgets.
+            var orig = jasmine.Runner.prototype.finishCallback;
+            jasmine.Runner.prototype.finishCallback = function() {
+              orig.call(this);
+              var links = document.body.getElementsByTagName('a');
+              for (var i = 0; i < links.length; i++) {
+                links[i].href = 'javascript: void 0;';
+              }
+            };
+
+            var jasmineEnv = jasmine.getEnv(),
+                htmlReporter = new jasmine.HtmlReporter();
+            jasmineEnv.addReporter(htmlReporter);
+            jasmineEnv.specFilter = function(spec) {
+              return htmlReporter.specFilter(spec);
+            };
+            jasmineEnv.execute();
+          });
+        </script>
+      </head>
+      <body></body>
+    </html>
+  ]]></Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/ContainerPublish.xml b/trunk/content/gadgets/ContainerPublish.xml
new file mode 100644
index 0000000..2574958
--- /dev/null
+++ b/trunk/content/gadgets/ContainerPublish.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Container Publish Example">
+    <Require feature="pubsub"></Require>
+  </ModulePrefs>
+
+  <Content type="html" view="default">
+    <![CDATA[
+      <div>Container Message: <span id="message_display"></span></div>
+
+      <script type="text/javascript">
+        function handleHelloWorld(sender, message) {
+          document.getElementById('message_display').appendChild(
+              document.createTextNode(message))
+        }
+
+        gadgets.pubsub.subscribe('helloworld', handleHelloWorld);
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/DynamicSizeDemo.xml b/trunk/content/gadgets/DynamicSizeDemo.xml
new file mode 100644
index 0000000..88eb645
--- /dev/null
+++ b/trunk/content/gadgets/DynamicSizeDemo.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Dynamic Size example gadget">
+    <Require feature="dynamic-height" />
+    <Require feature="dynamic-width" />
+  </ModulePrefs>
+
+  <Content type="html" view="default"><![CDATA[
+    <head>
+    <script type="text/javascript">
+      gadgets.util.registerOnLoadHandler(function() {
+        window.skinny = function skinny() {
+          gadgets.window.adjustWidth(100);
+        }
+        window.fat = function() {
+          gadgets.window.adjustWidth(600);
+        }
+        window.short = function() {
+          gadgets.window.adjustHeight(100);
+        }
+        window.tall = function() {
+          gadgets.window.adjustHeight(600);
+        }
+        window.fitw = function() {
+          gadgets.window.adjustWidth();
+        }
+        window.fith = function() {
+          gadgets.window.adjustHeight();
+        }
+        window.addrtl = function() {
+          var elem = document.getElementById('rtl');
+          elem.style.display = '';
+        }
+      });
+    </script>
+    </head>
+    <body style="background:#5AF">
+      <div style="background:#FAA; width:300px">
+         I'm styled to be 300px wide.
+      </div>
+      <input type="button" onclick="skinny()" value="skinny" />
+      <input type="button" onclick="fat()" value="fat" /><br>
+      <input type="button" onclick="short()" value="short" />
+      <input type="button" onclick="tall()" value="tall" /><br>
+      <input type="button" onclick="fitw()" value="fit - width" />
+      <input type="button" onclick="fith()" value="fit - height" /><br>
+      <input type="button" onclick="addrtl()" value="show rtl element" />
+      <div id="rtl" style="display:none; background:#BFA; width:300px; height:40px; right:40px; top:100px; position:absolute; direction:rtl;">
+         I'm styled rtl, 300px wide, offset 40px from right.
+      </div>
+    </body>
+  ]]></Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/DynamicSizeDemoHTML5.xml b/trunk/content/gadgets/DynamicSizeDemoHTML5.xml
new file mode 100644
index 0000000..84835bf
--- /dev/null
+++ b/trunk/content/gadgets/DynamicSizeDemoHTML5.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module specificationVersion="2.0">
+  <ModulePrefs title="Dynamic Size example gadget">
+    <Require feature="dynamic-height" />
+    <Require feature="dynamic-width" />
+  </ModulePrefs>
+
+  <Content type="html" view="default"><![CDATA[
+    <head>
+    <script type="text/javascript">
+      gadgets.util.registerOnLoadHandler(function() {
+        window.skinny = function skinny() {
+          gadgets.window.adjustWidth(100);
+        }
+        window.fat = function() {
+          gadgets.window.adjustWidth(600);
+        }
+        window.short = function() {
+          gadgets.window.adjustHeight(100);
+        }
+        window.tall = function() {
+          gadgets.window.adjustHeight(600);
+        }
+        window.fitw = function() {
+          gadgets.window.adjustWidth();
+        }
+        window.fith = function() {
+          gadgets.window.adjustHeight();
+        }
+        window.addrtl = function() {
+          var elem = document.getElementById('rtl');
+          elem.style.display = '';
+        }
+      });
+    </script>
+    </head>
+    <body style="background:#5AF">
+      <div style="background:#FAA; width:300px">
+         I'm styled to be 300px wide.
+      </div>
+      <input type="button" onclick="skinny()" value="skinny" />
+      <input type="button" onclick="fat()" value="fat" /><br>
+      <input type="button" onclick="short()" value="short" />
+      <input type="button" onclick="tall()" value="tall" /><br>
+      <input type="button" onclick="fitw()" value="fit - width" />
+      <input type="button" onclick="fith()" value="fit - height" /><br>
+      <input type="button" onclick="addrtl()" value="show rtl element" />
+      <div id="rtl" style="display:none; background:#BFA; width:300px; height:40px; right:40px; top:100px; position:absolute; direction:rtl;">
+         I'm styled rtl, 300px wide, offset 40px from right.
+      </div>
+    </body>
+  ]]></Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/ELInGadgetDemo.xml b/trunk/content/gadgets/ELInGadgetDemo.xml
new file mode 100644
index 0000000..cda37f9
--- /dev/null
+++ b/trunk/content/gadgets/ELInGadgetDemo.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+	<ModulePrefs title="Varible Replacement Templating Example"
+		height="400" width="400">
+		<Require feature="opensocial-templates">
+		</Require>
+	</ModulePrefs>
+	<UserPref name="userName" default_value="John.Doe" />
+	<Content view="default" type="html">
+	<![CDATA[
+		<div>
+			<script type="text/os-template" xmlns:os="http://ns.opensocial.org/2008/markup" >
+				The usef pref value in template is ${UserPrefs.userName}
+			</script>
+		</div>
+		<div id="userperfarea4">Basic EL: ${1+2}</div>
+		<div id="userperfarea1">EL reading User Pref: ${UserPrefs.userName}</div>
+		<div id="userperfarea2"></div>
+		<div id="userperfarea3">Legacy Usage: __UP_userName__</div>
+		<div>
+			<script type="text/javascript">
+				var prefs = new gadgets.Prefs();
+				console.log(prefs);
+				var name = prefs.getString("userName");
+				document.getElementById("userperfarea2").innerHTML = "User Pref Value via JS: " + name;
+			</script>
+		</div>
+	]]>
+	</Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/FlashBridgeCajaExample.xml b/trunk/content/gadgets/FlashBridgeCajaExample.xml
new file mode 100644
index 0000000..11deec2
--- /dev/null
+++ b/trunk/content/gadgets/FlashBridgeCajaExample.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Flash Caja Demo"
+      height="200" 
+      description="Demonstrates the use of caja to protect flash gadgets and the flash bridge">
+    <Require feature="caja"></Require>
+    <Require feature="flash"></Require>
+    <Require feature="dynamic-height"></Require>
+  </ModulePrefs>
+  <Content type="html">
+  <![CDATA[
+  Caja does not allow <code>object</code> or <code>embed</code> tags
+  in HTML.  However, Shindig provides a tamed JavaScript API for
+  embedding flash on a page.  To embed a flash file, use
+  <code>Require feature="flash"</code> in your
+  ModulePrefs and <code>gadges.flash.embedFlash</code> in the body of
+  your gadget to embed flash.
+  <p>
+  This example illustrates the use of the flash bridge for enabling
+  communication between the sandboxed flash and the cajoled gadget.
+  <p>
+  The sources for this example are on the <a href="http://code.google.com/p/google-caja/downloads/list">Caja downloads page</a>: 
+  <a href="http://google-caja.googlecode.com/files/Boxed.as">Boxed.as</a>, 
+  <a href="http://google-caja.googlecode.com/files/Boxed.fla">Boxed.fla</a>
+  <div id="flashcontainer" style="width:200; height: 200">
+    You need Flash player 10 and JavaScript enabled to view this video.
+  </div>
+  <button id="btn" onclick='bridge.callSWF("mySWFMethod", [5,6,7]);' disabled>mySWFMethod(5,6,7)</button>
+  <div id="result"></div>
+
+  <script>
+    var bridge;
+    function onFlashBridgeReady() {
+      bridge = gadgets.flash.embedFlash(
+          "http://caja.appspot.com/Boxed.swf",
+          "flashcontainer",
+          10);
+      document.getElementById('btn').disabled = false;
+      gadgets.window.adjustHeight();
+    }
+    
+    function myJSMethod(x, y, z) {
+      document.getElementById("result").innerHTML += "myJSMethod("+[x,y,z]+")<br>";
+      gadgets.window.adjustHeight();
+    }
+    gadgets.window.adjustHeight();
+  </script>
+  ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/FlashCajaExample.xml b/trunk/content/gadgets/FlashCajaExample.xml
new file mode 100644
index 0000000..2bc9825
--- /dev/null
+++ b/trunk/content/gadgets/FlashCajaExample.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+ <ModulePrefs title="Flash Caja Demo"
+    height="200" 
+    description="Demonstrates the use of caja to protect flash gadgets">
+   <Require feature="caja"></Require>
+   <Require feature="flash"></Require>
+   <Require feature="dynamic-height"></Require>
+ </ModulePrefs>
+ <Content type="html">
+   <![CDATA[
+
+   Caja does not allow <code>object</code> or <code>embed</code> tags
+   in HTML.  However, Shindig provides a tamed JavaScript API for
+   embedding flash on a page.  To embed a flash file, use
+   <code>Require feature="flash"</code> in your
+   ModulePrefs and <code>gadges.flash.embedFlash</code> in the body of
+   your gadget to embed flash.
+<p>
+For example, here is a YouTube video.
+<p>
+  <div id="ytapiplayer">
+    You need Flash player 10 and JavaScript enabled to view this video.
+  </div>
+  <script type="text/javascript">
+    var success = gadgets.flash.embedFlash(
+        "http://www.youtube.com/v/0AqMb-edXlc", // Flash video
+        "ytapiplayer", // id of div to place flash object
+        "10"           // minimum version
+    );
+  </script>
+<script>gadgets.window.adjustHeight();</script>
+  ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/ImageUploadGadget.xml b/trunk/content/gadgets/ImageUploadGadget.xml
new file mode 100644
index 0000000..18d554f
--- /dev/null
+++ b/trunk/content/gadgets/ImageUploadGadget.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+	<ModulePrefs title="Proxy Tester" height="500">
+		<Require feature="dynamic-height"/>
+		<Require feature="embedded-experiences"/>
+		<Require feature="proxied-form-post"/>
+	</ModulePrefs>
+	<Content type="html"><![CDATA[
+	  <script>
+	    function init() {
+	      gadgets.window.adjustHeight();
+	    }
+
+	    function doPostFile() {
+	      document.getElementById("result").innerHTML = '';
+	      document.getElementById("progbar").style.width = 0;
+
+	      var form = document.getElementById("imageform"),
+	          params = {};
+	      params[gadgets.io.RequestParameters.CONTENT_TYPE] = gadgets.io.ContentType.TEXT;
+	      params[gadgets.io.RequestParameters.HEADERS] = {
+	        Referer: 'http://bayimg.com/',
+	        Origin: 'http://bayimg.com'
+	      };
+
+	      gadgets.io.proxiedMultipartFormPost(form, params, function(response) {
+	        document.getElementById("progbar").style.width = "100%";
+	        window.doAbort = null;
+	        if (response && response.errors && response.errors.length) {
+	          try {
+	            document.getElementById("result").innerHTML = /<body>(.*)<\/body>/.exec(response.errors[0])[1];
+	          } catch(e) {
+	            document.getElementById("result").innerHTML = 'Error: ' + response.errors[0];
+	          }
+	        } else {
+	          try {
+	            var url = /<img[^>]+alt="Image"[^>]+src="([^"]+)"/.exec(response.text)[1];
+	            document.getElementById("result").innerHTML = '<img src="' + url + '" onload="init();">';
+	          } catch(e) {
+	            document.getElementById("result").innerHTML = 'Error';
+	          }
+	        }
+	        gadgets.window.adjustHeight();
+	      },
+	      function(event, abort) {
+	        if (!window.doAbort)
+	          window.doAbort = abort;
+	        if (event && event.lengthComputable) {
+	          var percent = Math.ceil((event.loaded / event.total) * 100) + "%";
+	          document.getElementById("progbar").style.width = percent;
+	        }
+	      });
+	    }
+
+	     gadgets.util.registerOnLoadHandler(init);
+	  </script>
+
+	  <h3>Upload an image file to bayimg.com</h3>
+	  <form id="imageform" action="http://upload.bayimg.com/upload">
+	    <fieldset>
+	      File: <input type="file" name="file"></input>
+	      <input type="hidden" name="code" value="opensocial"></input>
+	      <input type="hidden" name="tags" value=""></input>
+	    </fieldset>
+	    <input type="button" value="Upload" onClick="doPostFile();"/>
+	    <input type="button" value="Abort" onClick="if (window.doAbort) doAbort();"/>
+	  </form>
+
+	  <div style="border:1px solid rgb(90,90,115); background-color:#ffffff; padding:0px; width:200px; height:20px;overflow:hidden;">
+	    <div id="progbar" style="width:0%; height:100%; background-color:rgb(108,157,222)"></div>
+	  </div>
+
+	  <div id="result"></div>
+	]]></Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/OpenViews-OpenEE.xml b/trunk/content/gadgets/OpenViews-OpenEE.xml
new file mode 100644
index 0000000..4f73c53
--- /dev/null
+++ b/trunk/content/gadgets/OpenViews-OpenEE.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module specificationVersion="2.0">
+  <ModulePrefs title="OpenViews-OpenEE">
+    <Require feature="open-views"/>
+  </ModulePrefs>
+  <Content type="html"><![CDATA[
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
+    <script type="text/javascript">
+      gadgets.util.registerOnLoadHandler(function() {
+	      $(document).ready(function(){
+	        $('input#xml').click(function(event) {
+	          var dataModel = '<embed><url>http://www.opensocial.org</url></embed>';
+	          gadgets.views.openEmbeddedExperience(null, null, dataModel);
+	        });
+	        $('input#obj').click(function(event) {
+	          var dataModel = {url:'http://www.opensocial.org'};
+	          gadgets.views.openEmbeddedExperience(null, null, dataModel);
+	        });
+	      });
+	    });
+    </script>
+    <body>
+      <input id="xml" type="button" value="Open EE with XMl"></input>
+      <input id="obj" type="button" value="Open EE with Object"></input>
+    </body>
+  ]]></Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/SharedLockedDomainDemo1.xml b/trunk/content/gadgets/SharedLockedDomainDemo1.xml
new file mode 100644
index 0000000..3660fb2
--- /dev/null
+++ b/trunk/content/gadgets/SharedLockedDomainDemo1.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Shared locked domain with shared-script-frame 1">
+    <Require feature="locked-domain">
+      <!--
+        All participants must be listed for them to all share a locked domain.
+        Gadgets should declare themselves as a participant.
+      -->
+
+      <!-- Change the following to actual deployment locations to test -->
+      <Param name="participant">http://gadgets.another-example.com/SharedLockedDomainDemo1.xml</Param>
+      <Param name="participant">http://gadgets.example.com/SharedLockedDomainDemo2.xml</Param>
+    </Require>
+    <!--
+      The interaction between shared locked-domains and shared-script-frame is:
+      Of the gadgets listed in this shared locked-domain, only 1 script frame will
+      ever be created.  The gadget to render first will have its script frame view
+      rendered.
+    -->
+    <Optional feature="shared-script-frame">
+      <Param name="view">script</Param>
+    </Optional>
+  </ModulePrefs>
+  <Content type="html" view="script"><![CDATA[
+    <script type="text/javascript">
+      (function() {
+        var callbacks = [];
+        window.join = function(callback) {
+          callbacks.push(callback);
+        }
+        window.require = {
+          // the script tag url gets mangled
+          baseUrl: "http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dojo",
+          async: 1,
+          callback: function() {
+            window.join = function(callback) { callback(window.require); };
+            while(callbacks.length) {
+              callbacks.shift()(window.require);
+            }
+          }
+        };
+
+        // It would be lovely if we could have plain script tags in here that shindig would ignore.
+        // Because this is now async we must implement join, above.
+        var scr = document.createElement('script');
+        scr.src = 'http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dojo/dojo.js';
+        document.head.appendChild(scr);
+      })();
+    </script>
+  ]]></Content>
+
+  <Content type="html" view="default"><![CDATA[
+    <link rel="stylesheet" type="text/css" href="http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dijit/themes/claro/claro.css"
+    <h2>Default View</h2>
+    <div>I'm loading a few large things, dojo, dijit.Editor, and dijit.Calendar</div>
+    <div id="scriptframetime"></div>
+    <div id="loadscripttime"></div>
+
+    <script type="text/javascript">
+      gadgets.util.registerOnLoadHandler(function() {
+        gadgets.script.getScriptFrame(function(scriptFrame) {
+          var starttime = new Date().getTime();
+          var joinScriptFrame = function() {
+            if (!scriptFrame.join) {
+              setTimeout(joinScriptFrame, 10);
+            }
+            else {
+              scriptFrame.join(function(require) {
+                window.require = require;
+                var checkpoint = new Date().getTime();
+                require(['dojo', 'dijit/Calendar', 'dijit/Editor'], function(dojo, cal, edit) {
+                  dojo.withDoc(window.document, function() {
+                    var endtime = new Date().getTime();
+                    dojo.byId('scriptframetime').innerHTML = 'It took me ' + (checkpoint - starttime) + ' millis to aquire the script frame';
+                    dojo.byId('loadscripttime').innerHTML = 'It took me ' + (endtime - checkpoint) + ' millis to load code.';
+                  });
+                });
+              });
+            }
+          };
+          joinScriptFrame();
+        });
+      });
+    </script>
+  ]]></Content>
+</Module>
diff --git a/trunk/content/gadgets/SharedLockedDomainDemo2.xml b/trunk/content/gadgets/SharedLockedDomainDemo2.xml
new file mode 100644
index 0000000..68c7239
--- /dev/null
+++ b/trunk/content/gadgets/SharedLockedDomainDemo2.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Shared locked domain with shared-script-frame 2">
+    <Require feature="locked-domain">
+      <!--
+        All participants must be listed for them to all share a locked domain.
+        Gadgets should declare themselves as a participant.
+      -->
+
+      <!-- Change the following to actual deployment locations to test -->
+      <Param name="participant">http://gadgets.another-example.com/SharedLockedDomainDemo1.xml</Param>
+      <Param name="participant">http://gadgets.example.com/SharedLockedDomainDemo2.xml</Param>
+    </Require>
+    <!--
+      The interaction between shared locked-domains and shared-script-frame is:
+      Of the gadgets listed in this shared locked-domain, only 1 script frame will
+      ever be created.  The gadget to render first will have its script frame view
+      rendered.
+    -->
+    <Optional feature="shared-script-frame">
+      <Param name="view">script</Param>
+    </Optional>
+  </ModulePrefs>
+
+  <Content type="html" view="script"><![CDATA[
+    <script type="text/javascript">
+      (function() {
+        var callbacks = [];
+        window.join = function(callback) {
+          callbacks.push(callback);
+        }
+        window.require = {
+          // the script tag url gets mangled
+          baseUrl: "http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dojo",
+          async: 1,
+          callback: function() {
+            window.join = function(callback) { callback(window.require); };
+            while(callbacks.length) {
+              callbacks.shift()(window.require);
+            }
+          }
+        };
+
+        // It would be lovely if we could have plain script tags in here that shindig would ignore.
+        // Because this is now async we must implement join, above.
+        var scr = document.createElement('script');
+        scr.src = 'http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dojo/dojo.js';
+        document.head.appendChild(scr);
+      })();
+    </script>
+  ]]></Content>
+
+  <Content type="html" view="default"><![CDATA[
+    <link rel="stylesheet" type="text/css" href="http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dijit/themes/claro/claro.css"
+    <h2>Default View</h2>
+    <div>I'm loading a few large things, dojo, dijit.Editor, and dijit.Calendar</div>
+    <div id="scriptframetime"></div>
+    <div id="loadscripttime"></div>
+
+    <script type="text/javascript">
+      gadgets.util.registerOnLoadHandler(function() {
+        gadgets.script.getScriptFrame(function(scriptFrame) {
+          var starttime = new Date().getTime();
+          var joinScriptFrame = function() {
+            if (!scriptFrame.join) {
+              setTimeout(joinScriptFrame, 10);
+            }
+            else {
+              scriptFrame.join(function(require) {
+                window.require = require;
+                var checkpoint = new Date().getTime();
+                require(['dojo', 'dijit/Calendar', 'dijit/Editor'], function(dojo, cal, edit) {
+                  dojo.withDoc(window.document, function() {
+                    var endtime = new Date().getTime();
+                    dojo.byId('scriptframetime').innerHTML = 'It took me ' + (checkpoint - starttime) + ' millis to aquire the script frame';
+                    dojo.byId('loadscripttime').innerHTML = 'It took me ' + (endtime - checkpoint) + ' millis to load code.';
+                  });
+                });
+              });
+            }
+          };
+          joinScriptFrame();
+        });
+      });
+    </script>
+  ]]></Content>
+</Module>
diff --git a/trunk/content/gadgets/SharedScriptFrameDemo.xml b/trunk/content/gadgets/SharedScriptFrameDemo.xml
new file mode 100644
index 0000000..1cb9a03
--- /dev/null
+++ b/trunk/content/gadgets/SharedScriptFrameDemo.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Multiple Instance script-frame">
+    <Optional feature="shared-script-frame">
+      <Param name="view">script</Param>
+    </Optional>
+  </ModulePrefs>
+
+  <Content type="html" view="script"><![CDATA[
+    <script type="text/javascript">
+      (function() {
+        var callbacks = [];
+        window.join = function(callback) {
+          callbacks.push(callback);
+        }
+        window.require = {
+          // the script tag url gets mangled
+          baseUrl: "http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dojo",
+          async: 1,
+          callback: function() {
+            window.join = function(callback) { callback(window.require); };
+            while(callbacks.length) {
+              callbacks.shift()(window.require);
+            }
+          }
+        };
+
+        // It would be lovely if we could have plain script tags in here that shindig would ignore.
+        // Because this is now async we must implement join, above.
+        var scr = document.createElement('script');
+        scr.src = 'http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dojo/dojo.js';
+        document.head.appendChild(scr);
+      })();
+    </script>
+  ]]></Content>
+
+  <Content type="html" view="default"><![CDATA[
+    <link rel="stylesheet" type="text/css" href="http://download.dojotoolkit.org/release-1.7.0b2/dojo-release-1.7.0b2/dijit/themes/claro/claro.css"
+    <h2>Default View</h2>
+    <div>I'm loading a few large things, dojo, dijit.Editor, and dijit.Calendar</div>
+    <div id="scriptframetime"></div>
+    <div id="loadscripttime"></div>
+
+    <script type="text/javascript">
+      gadgets.util.registerOnLoadHandler(function() {
+        gadgets.script.getScriptFrame(function(scriptFrame) {
+          var starttime = new Date().getTime();
+          var joinScriptFrame = function() {
+            if (!scriptFrame.join) {
+              setTimeout(joinScriptFrame, 10);
+            }
+            else {
+              scriptFrame.join(function(require) {
+                window.require = require;
+                var checkpoint = new Date().getTime();
+                require(['dojo', 'dijit/Calendar', 'dijit/Editor'], function(dojo, cal, edit) {
+                  dojo.withDoc(window.document, function() {
+                    var endtime = new Date().getTime();
+                    dojo.byId('scriptframetime').innerHTML = 'It took me ' + (checkpoint - starttime) + ' millis to aquire the script frame';
+                    dojo.byId('loadscripttime').innerHTML = 'It took me ' + (endtime - checkpoint) + ' millis to load code.';
+                  });
+                });
+              });
+            }
+          };
+          joinScriptFrame();
+        });
+      });
+    </script>
+  ]]></Content>
+</Module>
diff --git a/trunk/content/gadgets/SocialActivitiesWorld.xml b/trunk/content/gadgets/SocialActivitiesWorld.xml
new file mode 100644
index 0000000..3828687
--- /dev/null
+++ b/trunk/content/gadgets/SocialActivitiesWorld.xml
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+ <ModulePrefs title="Social Activities World"
+              icon="http://localhost:8080/images/icon.png">
+   <Require feature="settitle"/>
+   <Require feature="dynamic-height"/>
+ </ModulePrefs>
+ <Content type="html">
+   <![CDATA[
+<style type="text/css">
+  .streamtitle,
+  .socialHeading {
+    font-family:arial,helvetica,sans-serif;
+    font-size:13pt;
+    font-weight:bold;
+  }
+
+  .streamtitle {
+    background-color: #E0ECFF;
+    border-top: 1px solid blue;
+    padding: .25em;
+  }
+
+  .socialDescription a {
+    color:#999999;
+  }
+
+  .streamdescription,
+  .streamdescription a,
+  .streamdescription a:visited {
+    color:#408BFE;
+    font-size:12pt;
+    font-weight:bold;
+    text-decoration:underline;
+    font-family:arial,helvetica,sans-serif;
+  }
+
+  .streamurl a {
+    color:#008000;
+    font-size:10pt;
+    font-family:arial,helvetica,sans-serif;
+    text-decoration:underline;
+  }
+
+  .streamrow {
+    clear: both;
+  }
+
+  .streamrowline {
+    border-bottom:1px solid #DDE9F5;
+    clear:both;
+    height:0px;
+    margin:5px;
+  }
+
+  .streamcontents {
+    padding: .5em;
+  }
+
+  .streamhtmlcontents {
+    color:#333333;
+    font-size:10pt;
+    line-height:130%;
+    padding:2px 0pt 3px 10px;
+    font-family:arial,helvetica,sans-serif;
+  }
+
+  .mediaitems {
+    padding-left: 5em;
+  }
+
+  .addActivityDiv {
+    clear:both;
+    padding-bottom:15px;
+  }
+
+  #addActivityText {
+    color:#999999;
+    font-size:10pt;
+    font-weight:normal;
+    font-family:arial,helvetica,sans-serif;
+  }
+
+  .leftcolumn {
+    float: left;
+    width: 47%;
+  }
+
+  .rightcolumn {
+    float: right;
+    width: 47%;
+  }
+
+  #addActivity {
+    display:none;
+    border: 2px solid blue;
+    padding: .5em;
+  }
+</style>
+
+<script type="text/javascript">
+gadgets.window.setTitle('Social Activities World');
+gadgets.util.registerOnLoadHandler(refreshActivities);
+
+function refreshActivities() {
+  var req = opensocial.newDataRequest();
+  if (!viewer) {
+    req.add(req.newFetchPersonRequest('VIEWER'), 'viewer');
+  }
+  req.add(req.newFetchActivitiesRequest('VIEWER'), 'viewerActivities');
+  req.add(req.newFetchActivitiesRequest('VIEWER_FRIENDS'), 'activities');
+  req.send(handleActivities);
+}
+
+function postNewActivity() {
+  var activityElement = document.getElementById('newActivity');
+  var mediaItem = new Array(opensocial.newActivityMediaItem("image", "http://cdn.davesdaily.com/pictures/784-awesome-hands.jpg", {'type' : 'image'}));
+  var activity = opensocial.newActivity({ 'title' : viewer.getDisplayName() + ' wrote: ' + activityElement.value,
+    'body' : 'write back!', 'mediaItems' : mediaItem});
+
+  activityElement.value = '';
+  opensocial.requestCreateActivity(activity, "HIGH", refreshActivities);
+}
+
+var viewer;
+var activities;
+function handleActivities(dataResponse) {
+  if (!viewer) {
+    viewer = dataResponse.get('viewer').getData();
+  }
+  activities = dataResponse.get('viewerActivities').getData()['activities'].asArray();
+  activities = activities.concat(dataResponse.get('activities').getData()['activities'].asArray());
+  document.getElementById('stream').style.display = 'block';
+
+  var html = '';
+  if (!activities || activities.length == 0) {
+    document.getElementById('stream').innerHTML = 'You do not have any activities yet';
+    return;
+  }
+
+  for (var i = 0; i < activities.length; i++) {
+    html += '<div class="streamrow">';
+
+    html += '<div class="streamdescription"><a href="' + activities[i].url + '">' + activities[i].getField('title') + '</a></div>';
+
+    html += '<div class="streamcontents">';
+    html += '<img src="http://www.gstatic.com/codesite/ph/images/star_on.gif"/>';
+
+    var body = activities[i].getField('body') || '';
+    html += '<span class="streamhtmlcontents">' + body + '</span>';
+    html += '</div>';
+
+    html += '<div class="mediaitems">';
+    var mediaItems = activities[i].getField('mediaItems');
+    if (mediaItems) {
+      for (var j = 0; j < mediaItems.length; j++) {
+        if (mediaItems[j].getField('type') == 'image') {
+          html += '<img height="150px" style="padding-right:.5em;" src="' + mediaItems[j].getField('url') + '"/>';
+        }
+      }
+    }
+    html += '</div>';
+
+    var shortUrl = activities[i].getField('url');
+    if (shortUrl) {
+      if (shortUrl.indexOf('http://') == 0) {
+        shortUrl = shortUrl.substring(7);
+      }
+      html += '<div class="streamurl"><a href="' + activities[i].getField('url') + '">' + shortUrl + '</a></div>';
+    }
+
+    html += '</div>';
+    html += '<div class="streamrowline"></div>';
+  }
+  document.getElementById('stream').innerHTML = html;
+  gadgets.window.adjustHeight();
+}
+
+function hideShowDiv(divToShow, divToHide) {
+  document.getElementById(divToShow).style.display = 'block';
+  document.getElementById(divToHide).style.display = 'none';
+}
+</script>
+
+<div class="streamtitle">Activities from your friends</div>
+<div class="addActivityDiv">
+  <a id="addActivityText" href="#" onclick="hideShowDiv('addActivity','addActivityText'); return false;"> Add your own activity </a>
+  <span id="addActivity">
+    <input id="newActivity" type="text"/>
+    <input type="button" onclick="postNewActivity(); return false;" value="add"/>
+    <input type="button" onclick="hideShowDiv('addActivityText','addActivity'); return false;" value="cancel"/>
+  </span>
+</div>
+<div id="stream" style="display:none"></div>
+
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/gadgets/SocialCajaWorld.xml b/trunk/content/gadgets/SocialCajaWorld.xml
new file mode 100644
index 0000000..e2419bb
--- /dev/null
+++ b/trunk/content/gadgets/SocialCajaWorld.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+ <ModulePrefs title="Caja Demo"
+    title_url="http://www.cajadores.com/"
+    height="200" 
+    author="Jasvir Nagra" 
+    author_email="jasvir@gmail.com"> 
+   <Require feature="caja"></Require>
+   <Require feature="dynamic-height"></Require>
+ </ModulePrefs>
+ <Content type="html">
+   <![CDATA[
+    <style type="text/css">
+      body { font-family: arial,sans-serif,helvetica; background-color: #E5ECF9; }
+      p,td,span,input,label { font-family: arial,sans-serif, helvetica; font-size:12px }
+      .intro { background-color: #FFFFFF; text-align: center; border: 1px solid; width: 80%; padding: 5px; margin-left: auto; margin-right:auto; overflow:scroll; }
+      .source { background-color: #FFFFFF; text-align: center; border: 1px solid; width: 80%; padding: 5px; margin-left: auto; margin-right:auto; overflow:scroll; }
+      .problem { background-color: #E5ECF9; text-align: center; border-top: 1px solid #6B90DA; padding: 5px; }
+      .explanation { font-size:80%; background-color: #E5ECF9; text-align: center; border: 1px; width: 80%; margin-left: auto; margin-right:auto; padding:5px; }
+      .attack { background:#E5ECF9 none repeat scroll 0 0;
+        text-align:left;
+        border: 1px;
+        padding: 10px 10px;
+      }
+      a.visitattack { color: #0000ff; }
+      a.visitattack:visited { color: #000000; }
+      .name { background:#C3D9FF none repeat scroll 0 0; padding:4px 3px 3px 4px;}
+    </style>
+    <div id="intro">
+      Try out these examples in the Shindig sample container by turning the "use caja" flag on or off. 
+    </div>
+    <div id="attacks">
+      <div id="attack1" class="attack">
+        <div class="name">Redirection</div>
+        <div class="problem">
+          <script>var godoevil = function() { top.location = "http://www.thinkfu.com/evil.gif"; document.getElementById("redirection-result").innerHTML = "Gadget trying to redirect page";  };</script>
+          <form>
+            <input type="button" value="Go Do Evil Redirection" onclick="godoevil()" >
+          </form>
+          <label for="redirection-result">Result:</label><div id="redirection-result"></div>
+        </div>
+        <label for="attack1source">Source:</label><div id="attack1source" class="source">
+        top.location = "http://www.thinkfu.com/evil.gif";
+        </div>
+        <div class="explanation">
+You want to allow gadgets in your page but browsers allow any gadget
+(including one that is in an iframe) to access and navigate the
+browser window.  For example, a gadget can redirect the container
+to a phishing site to steal your password.
+
+Caja does not enforce a policy of its own.  Instead it gives
+containers stricter control over a gadget can do.  For example, it
+allows the container to decide whether a gadget can read or set
+variables such as <code>top.location</code>.  A careful choice of
+policy allows a container to protect its users from being unwittingly
+redirected to phishing and malware sites.
+        </div>
+      </div> 
+
+      <div id="attack2" class="attack">
+        <div class="name">Sniffing User History</div>
+        <div class="problem">
+        <a id="googlesniff" class="visitattack" href="http://www.google.com">Link to Google.com</a>
+            <p>
+            <label for="toplocation">User recently visited Google.com:</label><div id="googlesniff-result"></div>
+        <script>
+          var link = document.getElementById("googlesniff");
+	  var computedColor;
+          if(document.defaultView) {
+               var computedStyle = document.defaultView.getComputedStyle(link, null);
+               try { computedColor = computedStyle.getPropertyValue('color');}catch(e){}
+          } else {
+          	computedColor = link.currentStyle && link.currentStyle['color'];
+          }
+          document.getElementById("googlesniff-result").innerHTML = computedColor == '#000000' || computedColor == 'rgb(0, 0, 0)' ? "Yes!" : "Unknown";
+        </script>
+        </div>
+        <label for="attack2source">Source:</label><div id="attack2source" class="source">
+        var computedStyle = document.defaultView.getComputedStyle(link, null);<br>
+        var computedColor = computedStyle.getPropertyValue('color');<br>
+        var visited = computedColor == '#000000' || computedColor == 'rgb(0, 0, 0)' ? "Yes!" : "Unknown";<br>
+        </div>
+        <div class="explanation">
+When you visit a website, your browser helpfully colors links to that
+site with a different color.  Unfortunately a malicious gadget can use
+this computed style to detect if you have visited particular sites.
+In this way, a malicious gadget try to determine your gender, your
+news tastes, your political leaning, the name of your bank and other
+sensitive information by analyzing the sites you visit.
+
+By default Caja protects users against such leakage of information by
+not granting access to computed styles.
+        </div>
+      </div>
+      <div id="attack3" class="attack">
+        <div class="name">Script Injection</div>
+        <div class="problem">
+        <script>
+          function displayResult() { 
+            var blogComment = document.createElement('div');
+            blogComment.innerHTML = document.getElementById("resultGen").value;
+            document.getElementById("result").appendChild(blogComment);
+          }
+        </script>
+        <form>Enter a comment on my blog:<input id="resultGen" type="text" size="50" value="<b>just some bold text nothing to see here dudes.</b><script defer>alert('XSS Exploited!');</script>"><br>
+        <input type="button" value="Display Comment" onclick="displayResult();"></form><br>
+        <label for="result">Comment:</label><div id="result"></div>
+        </div>
+        <label for="attack3source">Source:</label><div id="attack3source" class="source">
+          var blogComment = document.createElement('div');
+          blogComment.innerHTML = "&lt;b&gt;user entered text which happens to contain a &lt;script&gt; tag.&lt;/b&gt;&lt;script defer&gt;alert('muahahaa');&lt;/script&gt;";
+          document.getElementById("result").appendChild(blogComment);
+        </div>
+        <div class="explanation">
+You want to allow a user to enter comments in your blog using HTML but
+you don't want them to be able to enter scripts which steal cookies of
+other readers of your blog.  In this example, user input is being
+assigned directly to innerHTML.  On some browsers this has no effect
+but on IE, this will result in the embedded script being executed.
+
+Caja prevents such attacks by sanitizing strings before inserting them into the DOM.  
+        </div>
+      </div> 
+      <div id="attack4" class="attack">
+        <div class="name">Cookie Stealing</div>
+        <div class="problem">
+        Document cookie: <div id="cookie"></div>
+        <script>document.getElementById('cookie').innerHTML = (""+document.cookie).substring(0, 10) + "...";</script>
+        </div>
+        <label for="attack4source">Source:</label><div id="attack4source" class="source">
+document.getElementById('cookie').innerHTML = document.cookie
+        </div>
+        <div class="explanation">
+You want to inline gadgets in your page but you don't want it to steal
+your viewer's cookies.  In this example, you can see if a gadget you
+use sets cookies and if a malicious gadget can access it.
+
+ Caja disallows access to any variable which the container does not
+ explicitly grant a gadget access to.  Unless a container explicitly
+ grants a gadget access to your cookies, a gadget is unable to access
+ it.
+        </div>
+      </div>
+<script>gadgets.window.adjustHeight();</script>
+  ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/SocialHelloWorld.xml b/trunk/content/gadgets/SocialHelloWorld.xml
new file mode 100644
index 0000000..a11ca71
--- /dev/null
+++ b/trunk/content/gadgets/SocialHelloWorld.xml
@@ -0,0 +1,264 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Social Hello World"
+               description="The Social Hello World Application Displays multilingual hello messages"
+               thumbnail="http://localhost:8080/"
+               icon="http://localhost:8080/images/icon.png">
+    <Require feature="osapi"></Require>
+    <Require feature="settitle"/>
+    <Require feature="dynamic-height"></Require>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+    <style type="text/css">
+    #helloworlds {
+      margin: 20px;
+      font-family: arial, sans-serif;
+      width: 310px;
+    }
+
+    .person img { margin-bottom: 10px; }
+
+    .person .c0 { color: #008000; }
+    .person .c1 { color: #FF8A00; }
+    .person .c2 { color: #7777CC; }
+    .person .c3 { color: #008000; }
+    .person .c4 { color: #CC0000; }
+    .person .c5 { color: #73A6FF; }
+
+    .person .bubble {
+      background-image: url('/images/bubble.gif');
+      background-repeat: no-repeat;
+      width: 202px;
+      height: 66px;
+      padding: 12px 0px 0px 12px;
+      font-weight: bold;
+      font-size: 18px;
+      float: right;
+    }
+
+    .person .bubble .name {
+      width: 150px;
+      text-align: right;
+      font-weight: normal;
+      font-size: 12px;
+      color: #999;
+      position:relative;
+      top: 10px;
+      right: -35px;
+    }
+  </style>
+
+  <script type="text/javascript">
+    // Set title (if supported by container)
+    gadgets.window.setTitle('Social Hello World');
+
+    // List of hellos
+    var hellos = new Array('Hello World', 'Hallo Welt', 'Ciao a tutti', 'Hola mundo',
+      '&#1055;&#1086;&#1103;&#1074;&#1083;&#1077;&#1085;&#1080;&#1077; &#1085;&#1072; &#1089;&#1074;&#1077;&#1090;',
+      '&#12371;&#12435;&#12395;&#12385;&#12399;&#19990;&#30028;', '&#20320;&#22909;&#19990;&#30028;',
+      '&#50668;&#47084;&#48516;, &#50504;&#45397;&#54616;&#49464;&#50836;');
+    var numberOfStyles = 6;   // Number of .c* styles defined in style tag above
+    var viewerCount = 0;      // Number of times 'Say Hello' has been clickec
+    var viewer;               // Current viewer
+
+    /**
+     *  Render the person bubbles to HTML
+     *
+     *  @param data The date provided by osapi requests
+     */
+    render = function(data) {
+      // When Caja is enabled, data is read-only, so make a writeable copy.
+      var allPeople = new Array();
+
+      // Setup the allPeople array
+      if(data.viewerFriends.list) {
+        allPeople = data.viewerFriends.list.concat();
+      } else if(viewer) {
+        allPeople.push(viewer);
+      }
+
+      var viewerData = data.viewerData;
+      viewerCount = getCount(viewerData[viewer.id]);
+
+      var viewerFriendData = data.viewerFriendData;
+      viewerFriendData[viewer.id] = viewerData[viewer.id];
+
+      // We will load this up with all the people 'bubbles'
+      var html = '';
+
+      // Loop through all the people returned and create a 'bubble' for them
+      for (var i = 0; i < allPeople.length; i++) {
+        var count = getCount(viewerFriendData[allPeople[i].id]);
+        if (count == 0) {
+          //continue;
+        }
+
+        html += '<div class="person">';
+        html += '  <div class="bubble c' + count % numberOfStyles + '">' + hellos[count % hellos.length];
+        html += '    <div class="name">' + allPeople[i].name.formatted + ' (' + count + ') ' + allPeople[i].gender;
+        html += '    </div>';
+        html += '  </div>';
+
+        if (allPeople[i].thumbnailUrl && allPeople[i].thumbnailUrl.indexOf('null') == -1) {
+          html += '  <img src="' + allPeople[i].thumbnailUrl + '"/>';
+        } else {
+          html += '  <img src="/images/nophoto.gif"/>';
+        }
+
+        html += '  <br style="clear:both">';
+        html += '</div>';
+      }
+
+      // Output all the 'bubbles' to the 'helloworld' div
+      document.getElementById('helloworlds').innerHTML = html;
+
+      // Adjust the height of the gadget to fit the new data
+      gadgets.window.adjustHeight();
+    };
+
+    /**
+     *  Get the current count of the number of times 'Say Hello' has been clicked
+     *
+     *  @param data The date provided by osapi requests
+     */
+    getCount = function(data) {
+      return data && data['count'] ? Number(data['count']) : 0;
+    };
+
+    /**
+     *  Builds the groups select input
+     *
+     *  @param list A array of OpenSocial Groups
+     *  @param totalResults The length of list array
+     */
+    buildGroupsSelect = function(list, totalResults) {
+      var select = document.getElementById('groups_select');
+
+      // Fill element with data
+      select.innerHTML += '<option value="@friends">@friends</option>';
+      for(i = 0; i < totalResults; i++) {
+        select.innerHTML += '<option value="' + list[i].id + '">' + list[i].title + '</option>';
+      }
+    };
+
+    /**
+     *  Get the OpenSocial Groups of the viewer by sending an osapi.groups.get request
+     */
+    getGroups = function() {
+      var req = osapi.groups.get({});
+      req.execute(function(result) {
+        buildGroupsSelect(result.list, result.totalResults);
+      });
+    };
+
+    /**
+     *  Get the Viewer (an OpenSocial Person) by sending an osapi.people.getViewer request
+     */
+    getViewer = function() {
+      var req = osapi.people.getViewer({});
+      req.execute(function(result) {
+        // Set the viewer to the result
+        viewer = result;
+        // Update the 'current_viewer' span
+        document.getElementById('current_viewer').innerHTML = result.name.formatted + " (" + result.id + ")";
+      });
+    };
+
+    /**
+     *  The handler called when the 'Say Hello' button is pressed
+     */
+    sayHelloWorld = function() {
+      viewerCount++;
+      osapi.appdata.update({data:{count:viewerCount}}).execute(processSayHello);
+    };
+
+    /**
+     *  Process the request to 'Say Hello'
+     */
+    processSayHello = function() {
+      // Selected field to have returned
+      var fields = ['id','age','name','gender','profileUrl','thumbnailUrl'];
+
+      // Get the value(groupId) of the group selected
+      var group = document.getElementById('groups_select').value;
+
+      // Make sure there actually is a value
+      if(group == '' || group == null) {
+        // Default to @friends
+        group = '@friends';
+      }
+
+      // Initialize a new batch request
+      var batch = osapi.newBatch();
+
+        // Add to batch: Get the viewer's friends
+        batch.add('viewerFriends',
+          osapi.people.get({
+            groupId: group,   // Only get friend in group selected
+            sortBy: 'name',   // Sort by name
+            fields: fields    // Only return defined fields
+          })
+        );
+        // Add to batch: Get the viewer's data
+        batch.add('viewerData',
+          osapi.appdata.get({
+            keys: ['count']
+          })
+        );
+        // Add to batch: Get the viewer's friend's data
+        batch.add('viewerFriendData',
+          osapi.appdata.get({
+            groupId: group,
+            keys: ['count']
+          })
+        );
+
+      // Execute the batch
+      batch.execute(render);
+    };
+
+    /**
+     *  Initialize data for request
+     */
+    initData = function() {
+      // Get the current viewer
+      getViewer();
+
+      // Get the groups of the current viewer
+      getGroups();
+    };
+
+    gadgets.util.registerOnLoadHandler(initData);
+  </script>
+  <div style="margin-bottom: 1em">
+    Current Viewer:
+    <span id="current_viewer"></span>
+  </div>
+  <div style="margin-bottom: 1em">
+    Select a group:&nbsp;
+    <select id="groups_select"></select>
+    <input type="button" value="Say hello" onclick="sayHelloWorld(); return false;"/>
+  </div>
+  <div id="helloworlds" style="margin: 4px"></div>
+  ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/compliance-1.0/ExpressionLangSample.xml b/trunk/content/gadgets/compliance-1.0/ExpressionLangSample.xml
new file mode 100644
index 0000000..5797aff
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/ExpressionLangSample.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Expression Language Test Cases" description="Compliance test for EL features">
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script type="text/os-data">
+      <os:ViewerRequest key='vwr' />
+	  <os:PeopleRequest key='friends' startIndex="1" count="100" />
+	  <os:AlbumsRequest key='myalbums' userid="@viewer" groupid="@self" />
+	  <os:MediaItemsRequest key='mediaItems' userid="@viewer" groupid="@self" albumid="${myalbums[0].id}" count="20" />
+    </script>
+      <style type="text/css">
+        body{
+        background:#CCC;
+        color:blue;
+        }
+		.person{
+		border:3px solid green;
+		background:#AA3;
+		border-radius:3px;
+		padding:3px;
+		}
+		.photo{
+		border:3px solid red;
+		border-radius:3px;
+		padding:3px;
+		margin-bottom:3px;
+		}
+		.row0{
+		  background:lightyellow;
+		  color:black;
+		}
+		.row1{
+		background:orange;
+		color:white;
+		}
+		
+      </style>
+
+
+    <script type="text/os-template">
+      <h1>Expression Language Samples, custom for ${vwr.displayName}</h1>
+<h2>My Albums</h2>
+<div>Total Count of Albums:  ${myalbums.totalResults} (note: this value is not populated if private album access is not granted)</div>
+<div>Start Index:  ${myalbums.startIndex} </div>
+<div>Page Size:  ${myalbums.ItemsPerPage} </div>
+
+        <os:Repeat expression="${myalbums}">
+          <div >
+            <img src="${Cur.thumbnailUrl}" />
+            Album: ${Cur.caption} <br />
+			AlbumId: ${Cur.id}
+          </div>
+        </os:Repeat>
+
+		<h2>First Album's Items</h2>
+<div>Total Count of Media Items:  ${mediaItems.totalResults}</div>
+<div>Start Index:  ${mediaItems.startIndex} </div>
+<div>Page Size:  ${mediaItems.ItemsPerPage} </div>
+		
+		<h3>Alternating Rows Below</h3>
+		
+        <os:Repeat expression="${mediaItems}">
+		${Context.Index%2=0}
+          <div class="photo row${Context.Index%2}" >
+            <img src="${Cur.thumbnailUrl}" />
+            Photo: ${Cur.title} 
+          </div>
+        </os:Repeat>
+
+		<h3>Alternating Rows of Friends With StyleName</h3>
+<div repeat="${friends}" class="row${Context.Index%2}">
+<img src="${Cur.thumbnailUrl}" />&nbsp;${Cur.displayName}
+</div>
+		
+<h2>Simple Math</h2>
+
+1+1=${1+1} <br/>
+Picture count: ${mediaItems.totalResults} <br />
+Picture Count x 2 = ${2*mediaItems.totalResults} <br />
+		
+		
+<h2>Using Variables and JSON</h2>
+<os:Var key="myvar">
+{name:"My Object",
+"state": "Live",
+"colors":[
+"blue", "purple", "red", "orange", "yellow", "green"
+]
+}
+</os:Var>
+		
+
+<div>Object Name: ${myvar.name}</div>
+<div>
+Colors:
+<os:Repeat expression="${myvar.colors}">
+<div style="width:40px;height:40px;float:left;background:${Cur};">&nbsp;</div>
+</os:Repeat>
+</div>
+		
+	  </script>
+  </Content>
+
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/compliance-1.0/customTagTemplates.xml b/trunk/content/gadgets/compliance-1.0/customTagTemplates.xml
new file mode 100644
index 0000000..7b5d40e
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/customTagTemplates.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module xmlns:os="http://ns.opensocial.org/2008/markup" xmlns:my="http://ns.opensocial.org/2008/x">
+  <ModulePrefs title="Simple Custom Tag" 
+  description="I love my new custom tags.  This displays multiple tag and template parameter usage variations" >
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+ 
+    <script type="text/os-template" tag="my:SimpleTag">
+      <h1 style="color:${My.color};" >I am a simple custom tag</h1>
+    </script>
+  </Content>
+ 
+  <Content type="html" view="canvas">
+  <script type="text/os-template" >
+      <div>
+        <h1>I am on canvas</h1>
+        Custom tags below:
+      </div>
+      <my:SimpleTag color="blue" />
+      <my:SimpleTag >
+      <color>red</color>
+      </my:SimpleTag >
+    </script>
+  </Content>
+</Module>
+
diff --git a/trunk/content/gadgets/compliance-1.0/helloViewerAndFriends.xml b/trunk/content/gadgets/compliance-1.0/helloViewerAndFriends.xml
new file mode 100644
index 0000000..3f00979
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/helloViewerAndFriends.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module xmlns:os="http://ns.opensocial.org/2008/markup" >
+  <ModulePrefs title="Friends need Hello also" description="This is the desc">
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script type="text/os-data">
+     <os:ViewerRequest key='vwr' />
+     <os:PeopleRequest key='friends' userId="@viewer" groupid="@friends" />
+    </script>
+    <script type="text/os-template">
+     <h1>Hello world, ${vwr.displayName}</h1>
+	 Your friends are:
+ 
+	 <div>
+	 <os:Repeat expression="${friends}">
+	 <p>
+	 Friend number ${Context.index} is: ${Cur.displayName}
+	 <img src="${Cur.thumbnailUrl}" />
+	 </p>	 
+	 </os:Repeat>	 
+	 </div>
+ 
+  </script>
+ </Content>
+</Module>
+
diff --git a/trunk/content/gadgets/compliance-1.0/helloWorld.xml b/trunk/content/gadgets/compliance-1.0/helloWorld.xml
new file mode 100644
index 0000000..3993f91
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/helloWorld.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module xmlns:os="http://ns.opensocial.org/2008/markup" >
+  <ModulePrefs title="Hello World" description="This is the desc">
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script type="text/os-data">
+     <os:ViewerRequest key='vwr' />
+    </script>
+    <script type="text/os-template">
+     <h1>Hello world, ${vwr.displayName}</h1>
+  </script>
+ </Content>
+</Module>
+
diff --git a/trunk/content/gadgets/compliance-1.0/helloWorld_FriendsAndViews.xml b/trunk/content/gadgets/compliance-1.0/helloWorld_FriendsAndViews.xml
new file mode 100644
index 0000000..a5517df
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/helloWorld_FriendsAndViews.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="OSML Hello World" description="A classic Hello World sample application for OSML"
+  thumbnail="http://c1.ac-images.myspacecdn.com/images02/93/l_9d2ca5019ff9456abdcf743ea5e898d8.png"
+  >
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+
+<Icon>http://developer.myspace.com/views/static/img/editsource_on.png</Icon>
+
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script type="text/os-data">
+      <os:ViewerRequest key='vwr' />
+      <os:PeopleRequest key='myfriends' userid="@viewer" groupid="@friends" />
+    </script>
+
+
+    <script type="text/os-template">
+      <style type="text/css">
+        body{
+        background:black;
+        color:white;
+        }
+		.person{
+		border:3px solid green;
+		background:#AA3;
+		border-radius:3px;
+		padding:3px;
+      </style>
+      <h1>Hello world, ${vwr.displayName}</h1>
+
+        <os:Repeat expression="${Top.myfriends}">
+          <div class="person">
+            <img src="${Cur.thumbnailUrl}" />
+            Friend: ${Cur.displayName} 
+          </div>
+        </os:Repeat>
+
+
+	  </script>
+  </Content>
+
+  <Content type="html" view="profile">
+    <script type="text/os-template">
+<img src="${vwr.thumbnailUrl}" />
+	Hello world, ${vwr.displayName}
+at profile
+</script>
+  </Content>
+
+  <Content type="html" view="home">
+    <script type="text/os-template">
+I'm at home.
+But where's my content?
+</script>
+  </Content>
+
+
+
+</Module>
+
+
diff --git a/trunk/content/gadgets/compliance-1.0/nestedCustomTagsWithFriends.xml b/trunk/content/gadgets/compliance-1.0/nestedCustomTagsWithFriends.xml
new file mode 100644
index 0000000..9d2269d
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/nestedCustomTagsWithFriends.xml
@@ -0,0 +1,95 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module >
+  <ModulePrefs title="Complex Custom Tag Nesting" 
+  description="Testing of custom tag nesting with different repeaters and split repeat with If filtering">
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script type="text/os-data">
+      <os:ViewerRequest key='vwr' fields="@all" />
+      <os:PeopleRequest key='myfriends' fields="@all" userid="@viewer" groupid="@friends" startIndex="1" count="100" />
+    </script>
+    <!-- ==================== CUSTOM TAG ONE - VIEWER DATA =================== -->
+    <script type="text/os-template" tag="my:ViewerData">
+      <div style="font-weight:bold;color:${My.titleColor}">Summary for ${My.person.displayName}
+	  <img src="${My.person.thumbnailUrl}" style="margin-left:250px;" />
+	  </div>
+      <div style="padding:4px;width:250px;border:1px solid green;background:#999;">
+Age: ${My.person.age} <br />
+Gender: ${My.person.gender} <br />
+      </div>
+    </script>
+
+    <!-- ==================== CUSTOM TAG TWO - SimplePerson =================== -->
+    <script type="text/os-template" tag="my:SimplePerson">
+      <div style="font-weight:bold;color:${My.titleColor}">Simple for ${My.person.displayName}</div>
+      <div style="padding:4px;width:250px;border:1px solid green;background:#999;">
+      <os:Badge person="${My.person}" />
+Age: ${My.person.age} <br />
+      </div>
+    </script>
+
+    <!-- ==================== CUSTOM TAG THREE - FriendList =================== -->
+    <script type="text/os-template" tag="my:FriendList">
+    <h2>List of Friends</h2>
+  <os:Repeat expression="${My.people}">
+    <my:SimplePerson titleColor="blue">
+    <person>${Cur}</person>
+    </my:SimplePerson>
+  </os:Repeat>
+    </script>
+
+
+
+    <script type="text/os-template">
+      <style type="text/css">
+        body{
+        background:#eee;
+        color:#222;
+        }
+      </style>
+    </script>
+  </Content>
+
+    <Content type="html" view="canvas">
+    <script type="text/os-template">
+    <h1>Tags Sample</h1>
+    
+<my:ViewerData person="${vwr}" titleColor="orange" />
+
+And the Friend List <br/>
+
+<my:FriendList>
+<people>${myfriends}</people>
+</my:FriendList>
+
+<hr />
+   
+    
+    
+      <hr style="clear:both;" />
+    </script>
+  </Content>
+
+</Module>
+
+
diff --git a/trunk/content/gadgets/compliance-1.0/osVarTestGadget.xml b/trunk/content/gadgets/compliance-1.0/osVarTestGadget.xml
new file mode 100644
index 0000000..4c1e3e0
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/osVarTestGadget.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module xmlns:os="http://ns.opensocial.org/2008/markup" xmlns:myspace="http://ns.opensocial.org/2008/x" xmlns:my="http://ns.opensocial.org/2008/z">
+  <ModulePrefs title="OsVar Test Gadget" description="Test cases on osVar"
+  thumbnail=""
+  >
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script type="text/os-data">
+      <os:ViewerRequest key='vwr' />
+      <os:PeopleRequest key='myfriends' userid="@viewer" groupid="@friends" />
+      <os:Var key="dpVar" >I am registered with Data Pipeline</os:Var>
+      <os:Var key="jsonArrayVar" >[1,"one", "three"]</os:Var>
+      <os:Var key="jsonObjectVar" >{"id":'jsonId',"displayName":"faux Person"}</os:Var>
+    </script>
+
+
+    <script type="text/os-template" tag="vartest:Embedded">
+<div style="border:4px solid gray;padding:4px;margin:10px;float:left;">
+vartest:Embedded tag <br/>
+<b>My.someVal:</b> ${My.someVal}
+</div>
+    </script>
+
+
+<script type="text/os-template" tag="vartest:InDefinition">
+	<os:Var key="altKey" >Built altKey contains someVal: ${My.someVal}</os:Var>
+	<div style="border:4px solid red;padding:4px;margin:10px;float:left;">
+	vartest:InDefinition tag <br/>
+	<b>altKey:</b> ${altKey}
+	</div>
+</script>
+
+<script type="text/os-template" tag="vartest:InDefinition2">
+	<os:Var key="altKey" >Built altKey contains someVal: ${My.someVal}</os:Var>
+	<div style="border:4px solid orange;padding:4px;margin:10px;float:left;">
+	vartest:InDefinition2 tag <br/>
+	<b>altKey:</b> ${My.altKey}
+	</div>
+</script>
+
+
+
+    <script type="text/os-template">
+<h1>os:Var Test Cases</h1>
+<p>
+Testing var "foo"
+</p>
+<p>Before set: ${foo}</p>
+<os:Var key="foo" value="0" />
+<p>Initial set: ${foo}</p>
+<os:Var key="foo" >${foo + 1}</os:Var>
+<p>Added one with EL: ${foo}</p>
+
+<os:Var key="sum" value="0" />
+<h3>Looping increment to sum</h3>
+        <os:Repeat expression="${Top.myfriends}">
+<os:Var key="sum" value="${sum+1}" />
+          <div>
+		  Item: ${Context.Index} is ${Cur.displayName}
+		  </div>
+        </os:Repeat>
+<p>Sum: ${sum}</p>
+
+<hr />
+<h2>Custom Tags</h2>
+<vartest:Embedded someVal="Created with attr param" />
+<vartest:Embedded >
+<someVal>Created with Element Param</someVal>
+</vartest:Embedded>
+
+<vartest:Embedded >
+<os:Var key="someVal">osVar registered value</os:Var>
+</vartest:Embedded>
+
+<br style="clear:both;" />
+
+<vartest:InDefinition >
+<os:Var key="someVal">Any value will do here</os:Var>
+</vartest:InDefinition>
+
+<vartest:InDefinition2 >
+<os:Var key="someVal">Another value is good</os:Var>
+</vartest:InDefinition2>
+
+
+<br style="clear:both;" />
+<p>Deregistered altKey (should be empty): <span id='afterDeregKey'>${altKey}</span></p>
+
+
+<hr style="clear:left;" />
+<h2>Data Pipeline variables</h2>
+string: ${dpVar}
+
+<hr />
+<h2>Json Data</h2>
+
+jsonArrayVar.length: ${jsonArrayVar.length} <br/>
+jsonArrayVar[0]: ${jsonArrayVar[0]} <br/>
+jsonArrayVar[1]: ${jsonArrayVar[1]} <br/>
+jsonArrayVar[2]: ${jsonArrayVar[2]} <br/>
+
+
+jsonObjectVar.length: ${jsonObjectVar.length} <br/>
+jsonObjectVar.id: ${jsonObjectVar.id} <br/>
+jsonObjectVar.displayName: ${jsonObjectVar.displayName} <br/>
+
+<h4>Repeating Data</h4>
+<b>jsonArrayVar</b>
+<ul>
+<li repeat="jsonArrayVar">${Cur}</li>
+</ul>
+
+<b>jsonObjectVar</b>
+<ul>
+<li repeat="jsonObjectVar">${Cur}</li>
+</ul>
+
+
+<script type="text/javascript">
+
+function testKeyDeregistration(){
+	var el = document.getElementById("afterDeregKey");
+	if(el.innerHTML.length > 0){
+		el.style.backgroundColor="red";
+		el.style.color="white";
+	}
+	else{
+		el.style.backgroundColor="#CCFFCC;";
+	}
+}
+
+
+testKeyDeregistration();
+</script>
+	  </script>
+  </Content>
+</Module>
+
+
diff --git a/trunk/content/gadgets/compliance-1.0/ownerRequestViewerRequest.xml b/trunk/content/gadgets/compliance-1.0/ownerRequestViewerRequest.xml
new file mode 100644
index 0000000..4c69c67
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/ownerRequestViewerRequest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Testing Owner and Viewer" 
+  description="Often the values of owner and viewer will be the same"
+>
+    <Require feature="opensocial-1.0"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
+      <os:ViewerRequest key="vwr" fields="@all" />
+      <os:OwnerRequest key="owner" fields="@all" />
+   </script>
+    <script  type="text/os-template">
+    <h2>Owner Viewer Request App</h2>
+	
+	Viewer Name: ${vwr.displayName} <br/>
+	<p>
+	Owner Name: ${owner.displayName}
+	</p>
+	
+	
+    </script>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/compliance-1.0/sampleAlbumAndContents.xml b/trunk/content/gadgets/compliance-1.0/sampleAlbumAndContents.xml
new file mode 100644
index 0000000..51b7569
--- /dev/null
+++ b/trunk/content/gadgets/compliance-1.0/sampleAlbumAndContents.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module xmlns:os="http://ns.opensocial.org/2008/markup" >
+  <ModulePrefs title="First photo album app" description="Media items using EL in the data pipeline call">
+    <Require feature="opensocial-1.0"/>
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html" view="canvas">
+    <script type="text/os-data">
+	<os:ViewerRequest key='vwr' />
+	<os:AlbumsRequest key='myalbums' userid="@viewer" groupid="@self" />
+	<os:MediaItemsRequest key='mediaItems' userid="@viewer" groupid="@self" albumid="${myalbums[0].id}" />
+    </script>
+ 
+    <script type="text/os-template">
+<h1>First photos for ${vwr.displayName}</h1>
+ 
+<h2>Contents of album: ${myalbums[0].caption}</h2>
+ 
+<os:Repeat expression="${mediaItems}">
+  <div class="photo">
+    <img src="${Cur.thumbnailUrl}" />
+    Photo: ${Cur.title} 
+  </div>
+</os:Repeat>
+	  </script>
+  </Content>
+ 
+</Module>
+
diff --git a/trunk/content/gadgets/compliance/javascript-tests/1.1/activities/activitiessuite.js b/trunk/content/gadgets/compliance/javascript-tests/1.1/activities/activitiessuite.js
new file mode 100644
index 0000000..3f09ec0
--- /dev/null
+++ b/trunk/content/gadgets/compliance/javascript-tests/1.1/activities/activitiessuite.js
@@ -0,0 +1,150 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function runActivitiesSuite(){
+	
+	module("OpenSocial JavaScript Activities Tests 1.1");
+	
+	asyncTest("osapi.activities.get defaults", function(){
+		ok(osapi.activities.get, "osapi.activities.get exists");
+		var req = osapi.activities.get();
+		ok(req != null,"Req not null");
+		setTimeout(function(){
+			req.execute(function(response){
+				ok(!response.error, "No error in get response");
+				ok(response.totalResults == 1,"total results = " + response.totalResults + ", expected 1");
+				var count = 0;
+				for(var i in response.list){
+					ok(response.list[i], "Activity " + i);
+					ok(response.list[i].id, "Activity " + i + " id: " + response.list[i].id);
+					ok(response.list[i].title, "Activity " + i + " title: " + response.list[i].title);
+					ok(response.list[i].userId, "Activity " + i + " userId: " + response.list[i].userId);
+					count ++;
+				}
+				ok(count == response.totalResults,"Number of activities in list should match totalResults");
+				start();
+			});			
+		},1000);
+
+	});
+	
+	asyncTest("osapi.activities.get viewer", function(){
+		ok(osapi.activities.get, "osapi.activities.get exists");
+		var req = osapi.activities.get({userId: "@viewer"});
+		ok(req != null,"Req not null");
+		setTimeout(function(){
+			req.execute(function(response){
+				ok(!response.error, "No error in get response");
+				ok(response.totalResults == 1,"total results = " + response.totalResults + ", expected 1");
+				var count = 0;
+				for(var i in response.list){
+					ok(response.list[i], "Activity " + i);
+					ok(response.list[i].id, "Activity " + i + " id: " + response.list[i].id);
+					ok(response.list[i].title, "Activity " + i + " title: " + response.list[i].title);
+					ok(response.list[i].userId, "Activity " + i + " userId: " + response.list[i].userId);
+					count ++;
+				}
+				ok(count == response.totalResults,"Number of activities in list should match totalResults");
+				start();
+			});			
+		},1000);
+
+	});
+	
+	asyncTest("osapi.activities.get owner", function(){
+		ok(osapi.activities.get, "osapi.activities.get exists");
+		var req = osapi.activities.get({userId: "@owner"});
+		ok(req != null,"Req not null");
+		setTimeout(function(){
+			req.execute(function(response){
+				ok(!response.error, "No error in get response");
+				ok(response.totalResults == 1,"total results = " + response.totalResults + ", expected 1");
+				var count = 0;
+				for(var i in response.list){
+					ok(response.list[i], "Activity " + i);
+					ok(response.list[i].id, "Activity " + i + " id: " + response.list[i].id);
+					ok(response.list[i].title, "Activity " + i + " title: " + response.list[i].title);
+					ok(response.list[i].userId, "Activity " + i + " userId: " + response.list[i].userId);
+					count ++;
+				}
+				ok(count == response.totalResults,"Number of activities in list should match totalResults");
+				start();
+			});			
+		},1000);
+
+	});
+	
+	asyncTest("osapi.activities.get viewer friends",function(){
+		ok(osapi.activities.get, "osapi.activities.get exists");
+		var req = osapi.activities.get({userId : "@viewer", groupId : "@friends"});
+		ok(req != null,"Req not null");
+		setTimeout(function(){
+			req.execute(function(response){
+				ok(!response.error, "No error in get response");
+				ok(response.totalResults == 2,"total results = " + response.totalResults + ", expected 2");
+				var count = 0;
+				for(var i in response.list){
+					ok(response.list[i], "Activity " + i);
+					ok(response.list[i].id, "Activity " + i + " id: " + response.list[i].id);
+					ok(response.list[i].title, "Activity " + i + " title: " + response.list[i].title);
+					ok(response.list[i].userId, "Activity " + i + " userId: " + response.list[i].userId);
+					count ++;
+				}
+				ok(count == response.totalResults,"Number of activities in list should match totalResults");
+				start();
+			});			
+		},1000);
+
+	});
+	
+	asyncTest("osapi.activities.get owner friends",function(){
+		ok(osapi.activities.get, "osapi.activities.get exists");
+		var req = osapi.activities.get({userId : "@owner", groupId : "@friends"});
+		ok(req != null,"Req not null");
+		setTimeout(function(){
+			req.execute(function(response){
+				ok(!response.error, "No error in get response");
+				ok(response.totalResults == 2,"total results = " + response.totalResults + ", expected 2");
+				var count = 0;
+				for(var i in response.list){
+					ok(response.list[i], "Activity " + i);
+					ok(response.list[i].id, "Activity " + i + " id: " + response.list[i].id);
+					ok(response.list[i].title, "Activity " + i + " title: " + response.list[i].title);
+					ok(response.list[i].userId, "Activity " + i + " userId: " + response.list[i].userId);
+					count ++;
+				}
+				ok(count == response.totalResults,"Number of activities in list should match totalResults");
+				start();
+			});			
+		},1000);
+
+	});
+	
+	asyncTest("osapi.activities.get with non-existant ID",function(){
+		ok(osapi.activities.get, "osapi.activities.get exists");
+		var req = osapi.activities.get({userId : "DOES_NOT_EXIST"});
+		ok(req != null,"Req not null");
+		setTimeout(function(){
+			req.execute(function(response){
+				ok(!response.error, "No error in get response");
+				console.log(response);
+				ok(response.totalResults == 0);
+				start();
+			});			
+		},1000);
+
+	});
+	
+	
+}
diff --git a/trunk/content/gadgets/compliance/javascript-tests/1.1/activities/suite.xml b/trunk/content/gadgets/compliance/javascript-tests/1.1/activities/suite.xml
new file mode 100644
index 0000000..1f1f0e1
--- /dev/null
+++ b/trunk/content/gadgets/compliance/javascript-tests/1.1/activities/suite.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module specificationVersion="1.1">
+  <ModulePrefs title="Activities Tests 1.1" author="OpenSocial_QA"
+               description="Activities Requests Tests 1.1">
+    <Require feature="dynamic-height"/>
+    <Require feature="osapi"/>
+    <Require feature="opensocial-data"/>
+    <Require feature="opensocial"/>
+  </ModulePrefs>
+  <Content type="html" scrolling="true">
+    <![CDATA[
+<html>
+  <head>
+  <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen" />
+  <script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
+  <script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
+  <script type="text/javascript" src="activitiessuite.js"></script>
+  
+  <script>
+  
+      gadgets.util.registerOnLoadHandler(runActivitiesSuite);
+      gadgets.util.registerOnLoadHandler(function(){
+            gadgets.window.adjustHeight(2000);
+      });
+
+  </script>
+  </head>
+  
+  <body>
+    <h1 id="qunit-header">OpenSocial JavaScript 1.1 Activities</h1>
+    <h2 id="qunit-banner"></h2>
+    <div id="qunit-testrunner-toolbar"></div>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests"></ol>
+    <div id="qunit-fixture">test markup, will be hidden</div>
+  </body>
+</html>
+  
+  
+]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/compliance/javascript-tests/1.1/appdata/appdatasuite.js b/trunk/content/gadgets/compliance/javascript-tests/1.1/appdata/appdatasuite.js
new file mode 100644
index 0000000..be59674
--- /dev/null
+++ b/trunk/content/gadgets/compliance/javascript-tests/1.1/appdata/appdatasuite.js
@@ -0,0 +1,411 @@
+/**
+ * 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.
+ */
+var appDataPersonalValue2 = 'personalValue2 ' + new Date().getTime();
+var appDataPersonalValue1 = 'personalValue1 ' + new Date().getTime();
+function runAppDataSuite(){
+	
+	module("OpenSocial JavaScript AppData Tests 1.1");
+	
+	asyncTest("osapi.appdata.(update, get) defaults" , function(){
+
+		ok(osapi.appdata.update, "osapi.appdata.update exists");
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var pairs = {"testKey1" : appDataPersonalValue1};
+        ok(pairs,"Setting testKey1 to " + appDataPersonalValue1);
+        var params = {data : pairs};
+        var req = osapi.appdata.update(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+
+                var params = {keys : ["testKey1"]};
+                var req2 = osapi.appdata.get(params);
+                ok(req2 != null, "Req not null");
+                req2.execute(function(response){
+                		ok(!response.error,"No error in response");
+                		for(var person in response){
+                			ok(response[person]["testKey1"],"Response contains personalValue1");
+                			ok(response[person]["testKey1"] == appDataPersonalValue1, "personalValue1 matches expected value");
+                		}
+                		start();
+                	});
+        	});
+        }, 1000);
+      
+	});
+	
+	asyncTest("osapi.appdata.(update, get) @me" , function(){
+
+		ok(osapi.appdata.update, "osapi.appdata.update exists");
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var pairs = {"testKey2" : appDataPersonalValue2};
+        ok(pairs,"Setting personalValue to " + appDataPersonalValue2);
+        var params = {userId : "@me", data : pairs};
+        var req = osapi.appdata.update(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+                var params = {userId : "@me" , keys : ["testKey2"]};
+                var req2 = osapi.appdata.get(params);
+                ok(req2 != null, "Req not null");
+                req2.execute(function(response){
+                		ok(!response.error,"No error in response");
+                		var i = 0;
+                		for(var person in response){
+                			i++;
+                			ok(response[person]["testKey2"] == appDataPersonalValue2, 
+                					appDataPersonalValue2 + " matches retreived value "+ response[person]["testKey2"]);
+                		}
+                		ok(i == 1, "Expect 1 Person in response, found " + i);
+                		start();
+                	});
+        	});
+        }, 1000);
+      
+	});
+	
+	asyncTest("osapi.appdata.get w/ wildcard", function(){
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var params = {keys : ["*"]};
+        var req = osapi.appdata.get(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+//        		for(var person in response){
+//TODO VALIDATE RESPONSE
+//        		}
+        		start();
+        	});
+        }, 1000);
+	});
+	
+	//TODO fully validate response against compliance db
+	asyncTest("osapi.appdata.get @viewer", function(){
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var params = {userId : "@viewer"};
+        var req = osapi.appdata.get(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+        		var count = 0;
+        		for(var person in response){
+        			ok(person, person + " in response");
+        			count ++;
+        		}
+        		ok(count == 1, "Expected 1 person in response");
+        		start();
+        	});
+        }, 1000);
+	});
+	
+	
+	//TODO fully validate response against compliance db
+	asyncTest("osapi.appdata.get @owner", function(){
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var params = {userId : "@owner"};
+        var req = osapi.appdata.get(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+        		var count = 0;
+        		for(var person in response){
+        			ok(person, person + " in response");
+        			count ++;
+        		}
+        		ok(count == 1, "Expected 1 person in response");
+        		start();
+        	});
+        }, 1000);
+	});
+	
+	asyncTest("osapi.appdata.get w/ DOES_NOT_EXIST property", function(){
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var params = {keys : ["DOES_NOT_EXIST"]};
+        var req = osapi.appdata.get(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error, "No error in response");
+        		for(var person in response){
+        			ok(person, person +" in response");
+        			for(var data in response[person]){
+        				ok(false,person+" response should not contain data, but found \'" + data+"\'");
+        			}
+        		}
+        		start();
+        	});
+        }, 1000);
+	});
+	
+    
+	asyncTest("osapi.appdata.UPDATE without data property (Expect Error)", function(){
+		ok(osapi.appdata.update, "osapi.appdata.update exists");
+        var params = {};
+        var req = osapi.appdata.update(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(response.error,"Expecting error in UPDATE response");
+        		start();
+        	});
+        }, 1000);
+      
+	});
+	
+	asyncTest("osapi.appdata.DELETE without keys property (Expect Error)", function(){
+		ok(osapi.appdata.delete, "osapi.appdata.delete exists");
+        var params = {};
+        var req = osapi.appdata.delete(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(response.error,"Expecting error in DELETE response");
+        		start();
+        	});
+        }, 1000);
+      
+	});
+        
+	asyncTest("osapi.appdata.GET without keys property (Expect Error)", function(){
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var params = {};
+        var req = osapi.appdata.get(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(response.error,"Expecting error in GET response");
+        		start();
+        	});
+        }, 1000);
+      
+	});
+	
+	asyncTest("osapi.appdata.(update, get) @viewer  w/ key array" , function(){
+
+		ok(osapi.appdata.update, "osapi.appdata.update exists");
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var pairs = {"testKey2" : appDataPersonalValue2, "testKey1" : appDataPersonalValue1};
+        var params = {userId : "@viewer", data : pairs};
+        var req = osapi.appdata.update(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+
+                var params = {userId : "@viewer" , keys : ["testKey2", "testKey1"]};
+                var req2 = osapi.appdata.get(params);
+                ok(req2 != null, "Req not null");
+                req2.execute(function(response){
+                		ok(!response.error,"No error in response");
+                		var i = 0;
+                		for(var person in response){
+                			i++;
+                			var j = 0;
+                			for(var data in response[person]){
+                				j++;
+                				if(data == "testKey1"){
+                        			ok(response[person]["testKey1"] == appDataPersonalValue1, 
+                        					"testKey1 value = " + response[person]["testKey1"] + ", expected " + appDataPersonalValue1);
+                				} else if(data == "testKey2"){
+                        			ok(response[person]["testKey2"] == appDataPersonalValue2, 
+                        					"testKey2 value = " + response[person]["testKey2"] + ", expected " + appDataPersonalValue2);
+                				} else {
+                					ok(false, "Found unexpected key \'" + data + "\', value = " + response[person][data]);
+                				}
+                			}
+                		}
+                		ok(i == 1, "Expect 1 Person in response, found " + i);
+                		start();
+                	});
+        	});
+        }, 1000);
+      
+	});
+	
+	
+	//Assumes that one of the @viewer's friends is jane.doe
+	asyncTest("osapi.appdata.(update, get) Viewer Friends" , function(){
+		ok(osapi.appdata.update, "osapi.appdata.update exists");
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var pairs = {"testKey1" : "friendDataZZ"};
+        ok(pairs,"Setting \'jane.doe\' testKey1 to \'friendDataZZ\'");
+        var params = {userId : "jane.doe", groupId : "@self", data : pairs};
+        var req = osapi.appdata.update(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+                var params = {userId : "@viewer", groupId : "@friends", keys : ["testKey1"]};
+                var req2 = osapi.appdata.get(params);
+                ok(req2 != null, "Req not null");
+                req2.execute(function(response){
+                		ok(!response.error,"No error in response");
+                		for(var person in response){
+                			ok(person != "john.doe", person + " is not john.doe");
+                			if(person == "jane.doe"){
+                    			ok(response[person]["testKey1"], person + " appdata contains contains testKey1");
+                    			ok(response[person]["testKey1"] == "friendDataZZ",
+                    					person + " testKey1 value = " + response[person]["testKey1"] + ", expected = friendDataZZ");
+                			}
+                		}
+                		start();
+                	});
+        	});
+        }, 1000);
+      
+	});
+	
+	//Assumes that one of the @owner's friends is jane.doe
+	asyncTest("osapi.appdata.(update, get) Owner Friends" , function(){
+		ok(osapi.appdata.update, "osapi.appdata.update exists");
+		ok(osapi.appdata.get, "osapi.appdata.get exists");
+        var pairs = {"testKey1" : "friendDataZZ"};
+        ok(pairs,"Setting \'jane.doe\' testKey1 to \'friendDataZZ\'");
+        var params = {userId : "jane.doe", groupId : "@self", data : pairs};
+        var req = osapi.appdata.update(params);
+        ok(req != null, "Req not null");
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"No error in response");
+                var params = {userId : "@owner", groupId : "@friends", keys : ["testKey1"]};
+                var req2 = osapi.appdata.get(params);
+                ok(req2 != null, "Req not null");
+                req2.execute(function(response){
+                		ok(!response.error,"No error in response");
+                		for(var person in response){
+                			ok(person != "john.doe", person + " is not john.doe");
+                			if(person == "jane.doe"){
+                    			ok(response[person]["testKey1"], person + " appdata contains contains testKey1");
+                    			ok(response[person]["testKey1"] == "friendDataZZ",
+                    					person + " testKey1 value = " + response[person]["testKey1"] + ", expected = friendDataZZ");
+                			}
+                		}
+                		start();
+                	});
+        	});
+        }, 1000);
+      
+	});
+	
+	asyncTest("osapi.appdata.(update, delete) Create TO_DELETE appdata then delete it" , function(){
+		ok(osapi.appdata.update, "osapi.appdata.update exists");
+		ok(osapi.appdata.delete, "osapi.appdata.delete exists");
+        var pairs = {"TO_DELETE" : "value"};
+        ok(pairs,"Setting TO_DELETE");
+        var params = {data : pairs};
+        var req = osapi.appdata.update(params);
+        setTimeout(function(){
+        	req.execute(function(response){
+        		ok(!response.error,"TO_DELETE set successfully");
+                var params = {keys : ["TO_DELETE"]};
+                var req2 = osapi.appdata.delete(params);
+                ok(req2 != null, "Delete request not null");
+                req2.execute(function(response){
+            		ok(!response.error,"No error in DELETE response");
+                    var params = {keys : ["*"]};
+                    var req2 = osapi.appdata.get(params);
+                    req2.execute(function(response){
+                		ok(!response.error,"No error in GET response");
+                		for(var person in response){
+                			ok(!response[person]["TO_DELETE"], person + " does not have TO_DELETE appdata");
+                		}
+                		start();
+                	});
+            	});
+        	});
+        }, 1500);
+      
+	});
+	
+	
+	//Assumes the existence of john.doe and jane.doe
+	asyncTest("osapi.batch appdata requests (multi-user multi-key)", function(){
+		ok(osapi.newBatch,"osapi.newBatch exists");
+		var time = new Date().getTime();
+		var johnvalue1 = "john.doe value1 " + time;
+		var johnvalue2 = "john.doe value2 " + time;
+		var janevalue1 = "jane.doe value1 " + time;
+		var janevalue2 = "jane.doe value2 " + time;
+        var batchUpdate = osapi.newBatch().add("john.doe", osapi.appdata.update({userId : "john.doe", 
+		        	data : {
+		        		"batchKey1" : johnvalue1, 
+		        		"batchKey2" : johnvalue2}
+        		})).add("jane.doe", osapi.appdata.update({userId: "jane.doe", 
+	        		data : {
+	        			"batchKey1" : janevalue1,
+	        			"batchKey2" : janevalue2}}));
+        		
+        
+        var batchGet = osapi.newBatch().add("john.doe", osapi.appdata.get({userId : "john.doe", "keys" : ["batchKey1", "batchKey2"]}))
+				.add("jane.doe", osapi.appdata.get({userId: "jane.doe", "keys" : ["batchKey1", "batchKey2"]}));
+        
+        setTimeout(function(){
+    	    batchUpdate.execute(function(result) {
+    	    	ok(!result["john.doe"].error,"No error in john.doe update response");
+    	    	ok(!result["jane.doe"].error,"No error in jane.doe update response");
+    	    	if (!result["jane.doe"].error && !result["jane.doe"].error) {
+    	    	    batchGet.execute(function(result) {
+    	    	    	ok(!result["john.doe"].error,"No error in john.doe get response");
+    	    	    	ok(!result["jane.doe"].error,"No error in jane.doe get response");
+    	    	    	if (!result["jane.doe"].error && !result["jane.doe"].error) {
+    	    	    		ok(result["john.doe"]["john.doe"]["batchKey1"] == johnvalue1,"john.doe key1 = " + johnvalue1 + " (expected "+johnvalue1+")");
+    	    	    		ok(result["jane.doe"]["jane.doe"]["batchKey1"] == janevalue1,"jane.doe key1 = " + janevalue1 + " (expected "+janevalue1+")");
+    	    	    		ok(result["john.doe"]["john.doe"]["batchKey2"] == johnvalue2,"john.doe key2 = " + johnvalue2 + " (expected "+johnvalue2+")");
+    	    	    		ok(result["jane.doe"]["jane.doe"]["batchKey2"] == janevalue2,"jane.doe key2 = " + janevalue2 + " (expected "+janevalue2+")");
+    	    	    	}
+    	    	    	start();
+    	    	     });
+    	    	}
+    	     });
+        }, 1000);
+		
+	});
+	
+	asyncTest("osapi.batch mixed appdata & people requests", function(){
+		var batch = osapi.newBatch().
+		    add("viewer", osapi.people.getViewer()).
+		    add("appdata", osapi.appdata.get({ userId : '@viewer', groupId : '@self', "keys" : ["*"]}));
+		setTimeout(function(){
+			batch.execute(function(result) {
+				    ok(!result.viewer.error, "osapi.people.getViewer() succeeded");
+				    if(result.viewer.error){
+				    	ok(false, result.viewer.error.message);
+				    }
+				    ok(!result.appdata.error, "osapi.appdata.get() succeeded");
+				    if(result.appdata.error){
+				    	ok(false, result.appdata.error.message);
+				    }
+					ok(result.viewer.id,"Got viewer "+result.viewer.id);
+					//TODO validate that appdata values match expected
+					for(var person in result.appdata){
+						for(var data in result.appdata[person]){
+							ok(result.appdata[person][data], person + " - key: " + data + ", value: " + result.appdata[person][data]);
+						}
+					}
+				    start();
+				});
+		}, 1000);
+
+
+		
+	});
+	
+
+	
+}
diff --git a/trunk/content/gadgets/compliance/javascript-tests/1.1/appdata/suite.xml b/trunk/content/gadgets/compliance/javascript-tests/1.1/appdata/suite.xml
new file mode 100644
index 0000000..003917e
--- /dev/null
+++ b/trunk/content/gadgets/compliance/javascript-tests/1.1/appdata/suite.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module specificationVersion="1.1">
+  <ModulePrefs title="AppData Tests 1.1" author="OpenSocial_QA"
+               description="AppData Requests Tests 1.1">
+    <Require feature="dynamic-height"/>
+    <Require feature="osapi"/>
+    <Require feature="opensocial-data"/>
+    <Require feature="opensocial"/>
+  </ModulePrefs>
+  <Content type="html" scrolling="true">
+    <![CDATA[
+<html>
+  <head>
+  <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen" />
+  <script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
+  <script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
+  <script type="text/javascript" src="appdatasuite.js"></script>
+  
+  <script>
+  
+      gadgets.util.registerOnLoadHandler(runAppDataSuite);
+      gadgets.util.registerOnLoadHandler(function(){
+            gadgets.window.adjustHeight(2000);
+      });
+
+  </script>
+  </head>
+  
+  <body>
+    <h1 id="qunit-header">OpenSocial JavaScript 1.1 AppData</h1>
+    <h2 id="qunit-banner"></h2>
+    <div id="qunit-testrunner-toolbar"></div>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests"></ol>
+    <div id="qunit-fixture">test markup, will be hidden</div>
+  </body>
+</html>
+  
+  
+]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/compliance/javascript-tests/1.1/people/peoplesuite.js b/trunk/content/gadgets/compliance/javascript-tests/1.1/people/peoplesuite.js
new file mode 100644
index 0000000..29912f9
--- /dev/null
+++ b/trunk/content/gadgets/compliance/javascript-tests/1.1/people/peoplesuite.js
@@ -0,0 +1,577 @@
+/**
+ * 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.
+ */
+
+//TODO Verify person objects against compliance person data
+function personResponse(dataResponse) {
+	  
+    ok(!dataResponse.error, "No error in response");
+    if (!dataResponse.error) {
+      var viewerData = dataResponse;
+      for (var field in opensocial.Person.Field) {
+        try {
+          var fieldValue = viewerData[opensocial.Person.Field[field]];
+	      	var req = requiredBySpec(opensocial.Person.Field[field]);
+	    	var msg;
+	    	if(req){
+	    		msg = "REQUIRED: ";
+	    	} else {
+	    		msg = "OPTIONAL: ";
+	    	}
+           if(fieldValue != null) {
+              ok(fieldValue != null, msg + (opensocial.Person.Field[field] + " is set."));
+            } else if (opensocial.getEnvironment().supportsField(opensocial.Environment.ObjectType.PERSON, opensocial.Person.Field[field])) {
+          	  	ok(!req, msg + (opensocial.Person.Field[field] +" is empty."));
+            } else {
+          	  ok(fieldValue == null, 'Container non-supported field - ' + opensocial.Person.Field[field]);
+            }
+        } catch (ex) {
+          ok(false,ex);
+        }
+      }
+    }
+  start();
+}
+
+/**
+ * Returns TRUE if field is a required Person field
+ * 
+ * http://opensocial-resources.googlecode.com/svn/spec/1.1/Social-Data.xml#Person
+ * 
+ * @param field Person Field 
+ * @returns {Boolean}  TRUE if required, FALSE if not required (see spec).
+ */
+function requiredBySpec(field){
+	if(field == "id" ||
+		field == "name" ||
+		field == "thumbnailUrl"){
+		return true;
+	}
+	return false;
+}
+
+function runPeopleSuite(){
+	module("OpenSocial JavaScript People/Person Tests 1.1");
+	
+	asyncTest("osapi.people.getViewer() - (no parameters)", function(){
+          var req = osapi.people.getViewer();
+		  ok(osapi.people.getViewer,"osapi.people.getViewer exists");
+          ok(req != null, "Req not null");
+          ok(req.execute != null, "Req has execute method");
+          setTimeout(function(){ 
+        	  req.execute(personResponse);
+    	  }, 1000);
+        
+      });
+	
+	asyncTest("osapi.people.getOwner() - (no parameters)", function(){
+        var req = osapi.people.getOwner();
+        ok(osapi.people.getOwner,"osapi.people.getOwner exists");
+        ok(req != null, "Req not null");
+        ok(req.execute != null, "Req has execute method");
+        setTimeout(function(){ 
+      	  req.execute(personResponse);
+  	  	}, 1000);
+      
+    });
+	
+	asyncTest("osapi.people.get() - VIEWER (with params)", function(){
+
+	  var params = { userId : "@me", groupId : "@self"};
+      var req = osapi.people.getViewer(params);
+      ok(req != null, "osapi request not null");
+      ok(req.execute != null, "Request has execute method");
+      setTimeout(function(){ 
+    	  req.execute(personResponse);
+	  	}, 1000);
+
+	});
+	
+	asyncTest("osapi.people.get() - OWNER (with params)", function(){
+
+	      var params = { userId : "@me", groupId : "@self"};
+	      var req = osapi.people.getOwner(params);
+	      ok(req != null, "osapi request not null");
+	      ok(req.execute != null, "Request has execute method");
+	      setTimeout(function(){ 
+	    	  req.execute(personResponse);
+		  	}, 1000);
+
+		});
+	
+	asyncTest("osapi.people.get() - (by id 'john.doe')", function(){
+
+		var params = {userId : "john.doe", groupId : "@self"};
+        var req = osapi.people.get(params);
+        setTimeout(function(){
+        	
+        	req.execute(function(dataResponse){
+
+            	ok(!dataResponse.error,"no error in data response");
+                if (!dataResponse.error) {
+                  ok(dataResponse.id == "john.doe","ID is john.doe");
+                  ok(dataResponse.name.givenName == "John", "given name is John");
+                  ok(dataResponse.name.familyName == "Doe", "family name is Doe");
+                }
+                start();
+              });
+        	
+      }, 1000);
+	});
+	
+	test("opensocial.hasPermission(VIEWER)", function(){
+        var hasViewerPermission;
+        try {
+          ok(opensocial.hasPermission,"hasPermission exists");
+          ok(opensocial.Permission.VIEWER,"opensocial.Permission.VIEWER exists");
+          hasViewerPermission = opensocial.hasPermission(opensocial.Permission.VIEWER);
+          ok(hasViewerPermission != null,"User permission not null.")
+          ok(!hasViewerPermission,"Gadget should not have Viewer permission");
+        } catch (ex) {
+        	ok(false,ex);
+        }
+	});
+	
+	asyncTest("opensocial.requestPermission(VIEWER)", function(){
+		expect(2);
+		ok(opensocial.requestPermission,"opensocial.requestPermission exists");
+        opensocial.requestPermission(opensocial.Permission.VIEWER, 'test',
+                function(dataResponse) {
+	        		ok(dataResponse instanceof opensocial.ResponseItem,"dataResponse is a opensocial.ResponseItem");
+        		});
+		setTimeout(function(){
+	    	        start();
+		},1000);
+	});
+	
+	
+	asyncTest("opensocial.requestShareApp(VIEWER/VIEWER_FRIENDS/OWNER/OWNER_FRIENDS)", function(){
+
+		var ids = [ 'VIEWER', 'OWNER'];
+		expect(2 + (ids.length*2));
+		ok(opensocial.requestShareApp,"opensocial.requestShareApp exists");
+		ok(opensocial.newMessage,"opensocial.newMessage exists");
+		
+        for (var i = 0; i < ids.length; i++) {
+          var idSpec = {userId : ids[i], 
+          			  groupId : '@friends', 
+          			  networkDistance : 1};
+          opensocial.requestShareApp(idSpec, opensocial.newMessage("test"), function(dataResponse) {
+        	  /* Does this have to be called? */
+        	  ok(dataResponse instanceof opensocial.ResponseItem, ids[i]+" response is a opensocial.ResponseItem");
+          });
+        }
+        
+        for (i = 0; i < ids.length; i++) {
+            var idSpec = {userId : ids[i], 
+            			  groupId : '@self', 
+            			  networkDistance : 1};
+            opensocial.requestShareApp(idSpec, opensocial.newMessage("test"), function(dataResponse) {
+          	  /* Does this have to be called? */
+          	  ok(dataResponse instanceof opensocial.ResponseItem, ids[i]+" response is a opensocial.ResponseItem");
+            });
+        }
+        
+		setTimeout(function(){
+			start();
+	    },3000);
+		
+	});
+	
+	asyncTest("osapi.people.getViewer() (profile_details: addresses)", function(){
+
+		testAsyncViewerFieldResult(opensocial.Person.Field.ADDRESSES);
+      
+	});
+	
+	
+	asyncTest("osapi.people.getViewer() (profile_details: urls)", function(){
+
+		testAsyncViewerFieldResult(opensocial.Person.Field.URLS);
+      
+	});
+	
+	asyncTest("osapi.people.getViewer() (profile_details: name)", function(){
+
+		testAsyncViewerFieldResult(opensocial.Person.Field.NAME);
+      
+	});
+	
+	asyncTest("osapi.people.getViewer() (profile_details: currentLocation)", function(){
+
+		testAsyncViewerFieldResult(opensocial.Person.Field.CURRENT_LOCATION);
+      
+	});
+
+	asyncTest("osapi.people.getViewer() (profile_details: gender)", function(){
+
+		testAsyncViewerFieldResult(opensocial.Person.Field.GENDER);
+      
+	});
+	
+	asyncTest("osapi.people.getViewer() (profile_details: bodyType)", function(){
+
+		testAsyncViewerFieldResult(opensocial.Person.Field.BODY_TYPE);
+      
+	});
+	
+	asyncTest("osapi.people.getViewer() (profile_details: schools)", function(){
+
+		testAsyncViewerFieldResult(opensocial.Person.Field.SCHOOLS);
+      
+	});
+	
+	asyncTest("osapi.people.getOwner() (profile_details: addresses)", function(){
+
+		testAsyncOwnerFieldResult(opensocial.Person.Field.ADDRESSES);
+      
+	});
+	
+	
+	asyncTest("osapi.people.getOwner() (profile_details: urls)", function(){
+
+		testAsyncOwnerFieldResult(opensocial.Person.Field.URLS);
+      
+	});
+	
+	asyncTest("osapi.people.getOwner() (profile_details: name)", function(){
+
+		testAsyncOwnerFieldResult(opensocial.Person.Field.NAME);
+      
+	});
+	
+	asyncTest("osapi.people.getOwner() (profile_details: currentLocation)", function(){
+
+		testAsyncOwnerFieldResult(opensocial.Person.Field.CURRENT_LOCATION);
+      
+	});
+
+	asyncTest("osapi.people.getOwner() (profile_details: gender)", function(){
+
+		testAsyncOwnerFieldResult(opensocial.Person.Field.GENDER);
+      
+	});
+	
+	asyncTest("osapi.people.getOwner() (profile_details: bodyType)", function(){
+
+		testAsyncOwnerFieldResult(opensocial.Person.Field.BODY_TYPE);
+      
+	});
+	
+	asyncTest("osapi.people.getOwner() (profile_details: schools)", function(){
+
+		testAsyncOwnerFieldResult(opensocial.Person.Field.SCHOOLS);
+      
+	});
+	
+	asyncTest("osapi.people.get() - String ID",function(){
+		
+        var req = osapi.people.getViewer();
+        var id;
+        setTimeout(function(){ 
+        	req.execute(function(dataResponse){ 
+        		id = dataResponse['id']
+        		ok(id != null && id != undefined, "Viewer id is " + id);
+
+	      		var params = { userId : id, groupId : "@self"};
+	      		var req = osapi.people.get(params);
+
+	                req.execute(function(dataResponse) {
+	                  ok(!dataResponse.error,"No error in data response");
+	                  if (!dataResponse.error) {
+	                    var actual = dataResponse['id'];
+	                    ok(actual == id, "Expected " + id + " got " + actual);
+	                  }
+	                  start();
+	                });
+
+        	});
+	  	}, 1000);
+        
+	});
+	
+	asyncTest("osapi.people.get() w/bad parameters - Error expected", function(){
+
+
+        var req = osapi.people.get("bad_param");
+
+        setTimeout(function(){
+        	
+        	req.execute(function(dataResponse){
+                ok(dataResponse.error,"Error in data response");
+        		start();
+        	});
+        	
+        	
+          }, 1000);
+
+      
+    });
+	
+	asyncTest("osapi.people.get() - viewer\'s friends  (default) ", function(){
+        
+        var params = {userId : "@viewer", groupId : "@friends", networkDistance : 1};
+        var req = osapi.people.get(params);
+        testAsyncPeopleCollection(req,getSupportedPersonFields());
+      
+    });
+	
+	asyncTest("osapi.people.get() - john.doe\'s friends  (default) ", function(){
+        
+        var params = {userId : "john.doe", groupId : "@friends", networkDistance : 1};
+        var req = osapi.people.get(params);
+        testAsyncPeopleCollection(req,getSupportedPersonFields());
+      
+    });
+	
+	asyncTest("osapi.people.getViewerFriends() - (default) ", function(){
+
+        testAsyncPeopleCollection(osapi.people.getViewerFriends(),getSupportedPersonFields());
+      
+    });
+	
+	asyncTest("osapi.people.getOwnerFriends() - (default) ", function(){
+		var supportedPersonFields = getSupportedPersonFields();
+        testAsyncPeopleCollection(osapi.people.getOwnerFriends(),supportedPersonFields);
+        
+	});
+	
+	asyncTest("osapi.people.getViewerFriends() - w/fields ", function(){
+		var fieldIds = ['id','thumbnailUrl'];
+        var params = {fields : fieldIds};
+        var req = osapi.people.getViewerFriends(params);
+        testAsyncPeopleCollection(req, fieldIds);
+        
+	});
+	
+	asyncTest("osapi.people.getViewerFriends() - (paginated 1 per page, start index 1)", function(){
+
+        var params = {networkDistance : 1, count : 1};
+
+        var req = osapi.people.getViewerFriends(params);
+        
+        var params2 = {networkDistance : 1, startIndex : 1, count : 1};
+
+        var req2 = osapi.people.getViewerFriends(params2);
+        setTimeout(function(){
+        	var count = 0;
+        	var name;
+        	
+        	req.execute(function(dataResponse){
+
+            	ok(!dataResponse.error,"no error in data response");
+                if (!dataResponse.error) {
+                  var dataCollection = dataResponse;
+                  ok(dataResponse.startIndex == 0,"OpenSocial collections are zero indexed");
+                  ok(dataResponse.itemsPerPage == 1, "1 item per page");
+                  ok(dataResponse.list.length == 1,"list contains 1 item");
+                  ok(dataResponse.list[0].id,"Person id at 0 is " + dataResponse.list[0].id);
+                  if(!name){
+                	  name = dataResponse.list[0].id;
+                  } else {
+                	  ok(dataResponse.list[0].id != name,"Different indices returned different names");
+                  }
+                }
+                if(count == 1){
+                	start();
+                }
+                
+                count ++;
+              });
+        	
+        	req2.execute(function(dataResponse){
+
+            	ok(!dataResponse.error,"no error in data response");
+                if (!dataResponse.error) {
+                  var dataCollection = dataResponse;
+                  ok(dataResponse.startIndex == 1,"Expect a start index of 1");
+                  ok(dataResponse.itemsPerPage == 1, "1 item per page");
+                  ok(dataResponse.list.length == 1,"list contains 1 item");
+                  ok(dataResponse.list[0].id,"Person id at 1 is " + dataResponse.list[0].id);
+                  if(!name){
+                	  name = dataResponse.list[0].id;
+                  } else {
+                	  ok(dataResponse.list[0].id != name,"0 and 1 indices point to different Person objects");
+                  }
+                }
+                if(count == 1){
+                	start();
+                }
+                count ++;
+              });
+        	
+      }, 1000);
+        
+      
+	});
+	
+	
+	asyncTest("osapi.people.get() - (viewer's friends sorted by id)", function(){
+
+		var params = {userId : "@viewer", groupId : "@friends", networkDistance : 1, sortBy : "id"};
+        var req = osapi.people.get(params);
+        setTimeout(function(){
+        	
+        	req.execute(function(dataResponse){
+
+            	ok(!dataResponse.error,"no error in data response");
+                if (!dataResponse.error) {
+                  var dataCollection = dataResponse;
+                  ok(dataCollection.sorted,"Collection is marked as sorted");
+                  //TODO Verify sorting
+
+                }
+                start();
+              });
+        	
+      }, 1000);
+	});
+        
+	asyncTest("osapi.people.get() - (viewer's friends fitered - id contains 'doe')", function(){
+
+		var params = {  userId : "@viewer", 
+						groupId : "@friends", 
+						networkDistance : 1, 
+						filterBy : "id",
+						filterValue : "doe"
+					 };
+        var req = osapi.people.get(params);
+        setTimeout(function(){
+        
+        	req.execute(function(dataResponse){
+
+            	ok(!dataResponse.error,"no error in data response");
+                if (!dataResponse.error) {
+                  var dataCollection = dataResponse;
+                  ok(dataCollection.filtered,"Collection is marked as filtered");
+                  for(var i in dataCollection.list){
+                	  ok(dataCollection.list[i].id.indexOf("doe") != -1,dataCollection.list[i].id+" contains \"doe\"");
+                  }
+
+                }
+                start();
+              });
+        	
+      }, 1000);
+        
+      
+	});
+	
+	asyncTest("osapi.people.getViewerFriends() - (Paging out of bounds, startIndex = 9999)", function(){
+
+        var params = {networkDistance : 1, count : 10, startIndex : 9999};
+
+        var req = osapi.people.getViewerFriends(params);
+        setTimeout(function(){
+        
+        	req.execute(function(dataResponse){
+            	ok(dataResponse.error,"Error in data response");
+                start();
+              });
+        	
+      }, 1000);
+        
+      
+	});
+
+}
+
+var _supportedPersonFields;
+
+function getSupportedPersonFields(){
+    if(!_supportedPersonFields){
+    	_supportedPersonFields = new Array();
+	    for(var field in opensocial.Person.Field){
+	    	if(opensocial.getEnvironment().supportsField(opensocial.Environment.ObjectType.PERSON, opensocial.Person.Field[field])){
+	    		_supportedPersonFields.push(opensocial.Person.Field[field]);
+	    	}
+	    }
+	    ok(_supportedPersonFields,"Got list of supported Person fields");
+	}
+    return _supportedPersonFields;
+}
+
+//TODO Verify field values against compliance data
+function testAsyncPeopleCollection(req, fields){
+
+
+    setTimeout(function(){
+    	
+    	req.execute(function(dataResponse){
+    		ok(!dataResponse.error,"No error in data response");
+    		ok(dataResponse.totalResults,"Response has "+dataResponse.totalResults+" results");
+    		ok(dataResponse.list.length == dataResponse.totalResults,"Response has a result list of size " + dataResponse.list.length);
+    		var list = dataResponse.list;
+    		for(var i=0; i<list.length; i++){
+    			for(var j=0; j<fields.length; j++){
+    				ok(requiredBySpec(fields[j]) && list[i][fields[j]] != null, 
+    						"Friend " + i +" : " +fields[j] + " - " + list[i][fields[j]]);
+    			}
+    			
+    		}
+    		start();
+    	});
+    	
+    	
+      }, 1000);
+}
+
+
+function testAsyncViewerFieldResult(field){
+    if (opensocial.getEnvironment().supportsField(opensocial.Environment.ObjectType.PERSON,field)) {
+        	var params = {fields: [opensocial.Person.Field[field]]};
+        	var req = osapi.people.getViewer(params);
+
+			setTimeout(function(){
+				req.execute(function(dataResponse) {
+					ok(!dataResponse.error,"dataResponse without an error");
+		            if (!dataResponse.error) {
+		              var actual = dataResponse[opensocial.Person.Field[field]];
+		              ok(actual != null, opensocial.Person.Field[field] + " value is " + actual);
+		            }
+		            
+		        });
+				
+				start();
+			}, 1000);
+
+    } else {
+    	ok(!requiredBySpec(field),"Container does not declare support for "+ field +" field.")
+    	start();
+    }
+}
+
+function testAsyncOwnerFieldResult(field){
+    if (opensocial.getEnvironment().supportsField(opensocial.Environment.ObjectType.PERSON,field)) {
+        	var params = {"fields": [opensocial.Person.Field[field]]};
+        	var req = osapi.people.getOwner(params);
+
+			setTimeout(function(){
+				req.execute(function(dataResponse) {
+					ok(!dataResponse.error,"dataResponse without an error");
+		            if (!dataResponse.error) {
+		              var actual = dataResponse[opensocial.Person.Field[field]];
+		              ok(actual != null, opensocial.Person.Field[field] + " value is " + actual);
+		            }
+		
+		        });
+				
+				start();
+			},1000);
+
+    } else {
+    	ok(!requiredBySpec(field),"Container does not declare support for  "+ field +" field.")
+    	start();
+    }
+}
+
diff --git a/trunk/content/gadgets/compliance/javascript-tests/1.1/people/suite.xml b/trunk/content/gadgets/compliance/javascript-tests/1.1/people/suite.xml
new file mode 100644
index 0000000..fa6fa03
--- /dev/null
+++ b/trunk/content/gadgets/compliance/javascript-tests/1.1/people/suite.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module specificationVersion="1.1">
+  <ModulePrefs title="People Tests 1.1" author="OpenSocial_QA"
+               description="Person/People Requests Tests 1.1">
+    <Require feature="dynamic-height"/>
+    <Require feature="osapi"/>
+    <Require feature="opensocial-data"/>
+    <Require feature="opensocial"/>
+  </ModulePrefs>
+  <Content type="html" scrolling="true">
+    <![CDATA[
+<html>
+  <head>
+  <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen" />
+  <script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
+  <script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
+  <script type="text/javascript" src="peoplesuite.js"></script>
+  
+  <script>
+  
+      gadgets.util.registerOnLoadHandler(runPeopleSuite);
+      gadgets.util.registerOnLoadHandler(function(){
+            gadgets.window.adjustHeight(2000);
+      });
+
+  </script>
+  </head>
+  
+  <body>
+    <h1 id="qunit-header">OpenSocial JavaScript 1.1 People</h1>
+    <h2 id="qunit-banner"></h2>
+    <div id="qunit-testrunner-toolbar"></div>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests"></ol>
+    <div id="qunit-fixture">test markup, will be hidden</div>
+  </body>
+</html>
+  
+  
+]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/compliance/javascript-tests/1.1/suite.xml b/trunk/content/gadgets/compliance/javascript-tests/1.1/suite.xml
new file mode 100644
index 0000000..16c1bdc
--- /dev/null
+++ b/trunk/content/gadgets/compliance/javascript-tests/1.1/suite.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module specificationVersion="1.1">
+  <ModulePrefs title="OpenSocial JavaScript 1.1 Compliance Tests" author="OpenSocial_QA"
+               description="OpenSocial JavaScript 1.1 Compliance Tests">
+    <Require feature="dynamic-height"/>
+    <Require feature="osapi"/>
+    <Require feature="opensocial-data"/>
+    <Require feature="opensocial"/>
+  </ModulePrefs>
+  <Content type="html" scrolling="true">
+    <![CDATA[
+<html>
+  <head>
+  <link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen" />
+  <script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
+  <script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script>
+  <script type="text/javascript" src="appdata/appdatasuite.js"></script>
+  <script type="text/javascript" src="people/peoplesuite.js"></script>
+  <script type="text/javascript" src="activities/activitiessuite.js"></script>  
+  <script type="text/javascript" src="all.js"></script>
+  
+  
+  <script>
+  
+      gadgets.util.registerOnLoadHandler(function(){
+        runActivitiesSuite();
+        runAppDataSuite();
+        runPeopleSuite();
+        gadgets.window.adjustHeight(3000);
+      });
+
+  </script>
+  </head>
+  
+  <body>
+    <h1 id="qunit-header">OpenSocial JavaScript 1.1 Compliance</h1>
+    <h2 id="qunit-banner"></h2>
+    <div id="qunit-testrunner-toolbar"></div>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests"></ol>
+    <div id="qunit-fixture">test markup, will be hidden</div>
+  </body>
+</html>
+  
+  
+]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/datapipeline/ViewerRequestTest.xml b/trunk/content/gadgets/datapipeline/ViewerRequestTest.xml
new file mode 100644
index 0000000..0fd8681
--- /dev/null
+++ b/trunk/content/gadgets/datapipeline/ViewerRequestTest.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+	<ModulePrefs title="ViewerRequest test"	description="ViewerRequest test">
+      <Require feature="opensocial-data" />
+	</ModulePrefs>
+   <Content view="default" type="html">
+   <![CDATA[
+      <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+      <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
+         <os:ViewerRequest key="viewer" />
+      </script>
+      <!-- Initialize the gadget -->
+      <script type="text/javascript">
+         gadgets.util.registerOnLoadHandler(function() {
+            var currentViewer = opensocial.data.getDataContext().getDataSet('viewer');
+            if (currentViewer) {
+               document.getElementById("gadgetBody").innerHTML = "The current viewer id is " + currentViewer.id;
+            }
+         });
+     </script>
+     <div id="gadgetBody">No current viewer yet</div>
+   ]]>
+   </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/media-openGadgets/Media.xml b/trunk/content/gadgets/media-openGadgets/Media.xml
new file mode 100644
index 0000000..3ce15ac
--- /dev/null
+++ b/trunk/content/gadgets/media-openGadgets/Media.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+    <ModulePrefs title="Albums and MediaItems">
+        <Require feature="osapi" />
+        <Require feature="dynamic-height" />
+        <Require feature="open-views" />
+    </ModulePrefs>
+
+    <Content type="html" view="default"><![CDATA[
+        <html>
+            <head>
+                <!-- Source imports --><script src='http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js' type='text/javascript' djConfig='parseOnLoad:true, isDebug:true'></script>
+                <script src='Social.js' type='text/javascript'></script>
+                <script src='MediaUIOpenGadgets.js' type='text/javascript'></script>
+
+                <!-- Styling -->
+                <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/tundra/tundra.css"></link>
+                <link rel="stylesheet" type="text/css" href="styles.css">
+
+                <!-- DOJO requires -->
+                <script type='text/javascript'>
+                    dojo.require('dijit.form.Button');
+                    dojo.require('dijit.form.Form');
+                    dojo.require('dijit.form.TextBox');
+                    dojo.require('dijit.form.ValidationTextBox');
+                    dojo.require('dijit.Dialog');
+                    dojo.require('dijit.form.Textarea');
+                    dojo.require('dijit.layout.ContentPane');
+                    dojo.require('dijit.layout.TabContainer');
+                </script>
+
+                <!-- JavaScript -->
+                <script type="text/javascript">
+                <!-- Entry point to the gadget -->
+                    function init() {
+                        new MediaUI(new SocialWrapper()).init();
+                    }
+
+                <!-- Register entry point -->
+                    gadgets.util.registerOnLoadHandler(function() {
+                        dojo.addOnLoad(init);
+                    });
+                </script>
+            </head><body class="tundra"></body>
+        </html>
+    ]]></Content>
+
+    <Content type="html" view="albumFullView"><![CDATA[
+        <html>
+            <head>
+                <!-- Source imports -->
+                <script src='http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js' type='text/javascript' djConfig='parseOnLoad:true, isDebug:true'></script>
+                <script src='Social.js' type='text/javascript'></script>
+                <script src='MediaUIOpenGadgets.js' type='text/javascript'></script>
+
+                <!-- Styling -->
+                <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/tundra/tundra.css"></link>
+                <link rel="stylesheet" type="text/css" href="styles.css">
+
+                <!-- DOJO requires -->
+                <script type='text/javascript'>
+                    dojo.require('dijit.form.Button');
+                    dojo.require('dijit.form.Form');
+                    dojo.require('dijit.form.TextBox');
+                    dojo.require('dijit.form.ValidationTextBox');
+                    dojo.require('dijit.Dialog');
+                    dojo.require('dijit.form.Textarea');
+                    dojo.require('dijit.layout.ContentPane');
+                    dojo.require('dijit.layout.TabContainer');
+                </script>
+
+                <!-- JavaScript -->
+                <script type="text/javascript">
+                <!-- Entry point to the gadget -->
+                    function init() {
+                        var params = gadgets.views.getParams();
+                        var m = new MediaUI(new SocialWrapper());
+                        m.openAlbum(params['viewerId'],params['data']);
+                    }
+
+                <!-- Register entry point -->
+                    gadgets.util.registerOnLoadHandler(function() {
+                        dojo.addOnLoad(init);
+                    });
+                </script>
+            </head><body class="tundra"></body>
+        </html>
+    ]]></Content>
+
+    <Content type="html" view="editAlbum"><![CDATA[
+        <html>
+            <head>
+                <!-- Source imports -->
+                <script src='http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js' type='text/javascript' djConfig='parseOnLoad:true, isDebug:true'></script>
+                <script src='Social.js' type='text/javascript'></script>
+                <script src='MediaUIOpenGadgets.js' type='text/javascript'></script>
+
+                <!-- Styling -->
+                <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/tundra/tundra.css"></link>
+                <link rel="stylesheet" type="text/css" href="styles.css">
+
+                <!-- DOJO requires -->
+                <script type='text/javascript'>
+                    dojo.require('dijit.form.Button');
+                    dojo.require('dijit.form.Form');
+                    dojo.require('dijit.form.TextBox');
+                    dojo.require('dijit.form.ValidationTextBox');
+                    dojo.require('dijit.Dialog');
+                    dojo.require('dijit.form.Textarea');
+                    dojo.require('dijit.layout.ContentPane');
+                    dojo.require('dijit.layout.TabContainer');
+                </script>
+
+                <!-- JavaScript -->
+                <script type="text/javascript">
+                <!-- Entry point to the gadget -->
+                    function init() {
+                        var params = gadgets.views.getParams();
+                        var data = params['data'];
+                        new MediaUI(new SocialWrapper()).editAlbum(data);
+                    }
+
+                <!-- Register entry point -->
+                    gadgets.util.registerOnLoadHandler(function() {
+                        dojo.addOnLoad(init);
+                    });
+                </script>
+            </head><body class="tundra"></body>
+        </html>
+    ]]></Content>
+
+    <Content type="html" view="editMediaItem"><![CDATA[
+        <html>
+            <head>
+                <!-- Source imports -->
+                <script src='http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js' type='text/javascript' djConfig='parseOnLoad:true, isDebug:true'></script>
+                <script src='Social.js' type='text/javascript'></script>
+                <script src='MediaUIOpenGadgets.js' type='text/javascript'></script>
+
+                <!-- Styling -->
+                <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/tundra/tundra.css"></link>
+                <link rel="stylesheet" type="text/css" href="styles.css">
+
+                <!-- DOJO requires -->
+                <script type='text/javascript'>
+                    dojo.require('dijit.form.Button');
+                    dojo.require('dijit.form.Form');
+                    dojo.require('dijit.form.TextBox');
+                    dojo.require('dijit.form.ValidationTextBox');
+                    dojo.require('dijit.Dialog');
+                    dojo.require('dijit.form.Textarea');
+                    dojo.require('dijit.layout.ContentPane');
+                    dojo.require('dijit.layout.TabContainer');
+                </script>
+
+                <!-- JavaScript -->
+                <script type="text/javascript">
+                <!-- Entry point to the gadget -->
+                    function init() {
+                        var params = gadgets.views.getParams();
+                        new MediaUI(new SocialWrapper()).editMediaItem(params['data'].album,params['data'].mediaItem);
+                    }
+
+                <!-- Register entry point -->
+                    gadgets.util.registerOnLoadHandler(function() {
+                        dojo.addOnLoad(init);
+                    });
+                </script>
+            </head><body class="tundra"></body>
+        </html>
+    ]]></Content>
+
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/media-openGadgets/MediaUIOpenGadgets.js b/trunk/content/gadgets/media-openGadgets/MediaUIOpenGadgets.js
new file mode 100644
index 0000000..d50e55c
--- /dev/null
+++ b/trunk/content/gadgets/media-openGadgets/MediaUIOpenGadgets.js
@@ -0,0 +1,666 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*
+ * The User Interface for the Albums & MediaItems gadget.
+ *
+ * SHINDIG TODOS set ownerId automatically? delete children mediaitems when
+ * album deleted? update only updates given fields? update album mediaitem
+ * count when inserting/removing mediaitem?
+ *
+ * GADGET TODOS album info such as how many albums are contained fix auto
+ * height for edit album popup thumnail pictures
+ */
+function MediaUI(social) {
+  var viewer = null;
+  var divManager = null;
+
+  var folderUrl = 'http://www.clker.com/cliparts/2/b/b/3/' +
+      '1194983972976950993blue_folder_seth_yastrov_01.svg.med.png';
+  var docUrl = 'http://www.plastyc.com/images/document-icon.png';
+
+  /*
+   * Pre-load data for gadget.
+   */
+  function loadData(callback) {
+    social.getViewer(function(data) {
+      viewer = data;
+      callback();
+    });
+  }
+
+  /*
+   * Manages the gadgets main DIV elements.
+   */
+  function DivManager() {
+    var divs = [];
+
+    this.init = function() {
+      addDiv('albumsDiv');
+      addDiv('mediaItemsDiv');
+      addDiv('mediaItemDiv');
+      hideAll();
+    }
+
+    this.showAlbums = function() {
+      hideAll();
+      divs['albumsDiv'].style.display = 'block';
+      this.refreshWindow();
+    }
+
+    this.showMediaItems = function() {
+      hideAll();
+      divs['mediaItemsDiv'].style.display = 'block';
+      this.refreshWindow();
+    }
+
+    this.showMediaItem = function() {
+      hideAll();
+      divs['mediaItemDiv'].style.display = 'block';
+      this.refreshWindow();
+    }
+
+    this.refreshWindow = function() {
+      gadgets.window.adjustHeight(350);
+    }
+
+    function hideAll() {
+      for (key in divs) { divs[key].style.display = 'none'; }
+    }
+
+    function addDiv(id) { divs[id] = dojo.create('div', {id: id}, dojo.body());}
+  }
+
+  /*
+   * Renders a list of the given albums.
+   */
+  function renderAlbums(albums) {
+
+    dojo.empty('albumsDiv');
+    var albumsDiv = dojo.byId('albumsDiv');
+
+    var albumsBanner = dojo.create('div', null, albumsDiv);
+    var table = dojo.create('table', null, albumsBanner);
+    var tbody = dojo.create('tbody', null, table);
+    var tr = dojo.create('tr', null, tbody);
+    dojo.create('td', {innerHTML: viewer.name.formatted + "'s Albums",
+      className: 'albumsTitle'}, tr);
+    dojo.create('td', null, tr).appendChild(new dijit.form.Button(
+        {label: '+ New Album', onClick: dojo.hitch(
+        this, editAlbumPopup, null)}).domNode);
+
+    var albumsList = dojo.create('div', null, albumsDiv);
+    if (albums.length > 0) {
+      var table = dojo.create('table', {className: 'albumsTable'}, albumsList);
+      var tbody = dojo.create('tbody', null, table);
+      for (i = 0; i < albums.length; i++) {
+        var albumRow = dojo.create('tr', null, tbody);
+        var albumLeft = dojo.create('td', {className: 'albumListThumbnail'},
+            albumRow);
+        var imgLink = dojo.create('a', {href: 'javascript:;',
+          onclick: dojo.hitch(this, onClickAlbum, viewer.id, albums[i])},
+        albumLeft);
+        dojo.create('img', {src: albums[i].thumbnailUrl || folderUrl,
+          onerror: "this.src='" + folderUrl + "';", width:'100', height:'100'},
+          imgLink);
+        var albumRight = dojo.create('td', {className: 'albumListRight'},
+            albumRow);
+        var albumTitleTbody = dojo.create('table', null,
+            albumRight).appendChild(dojo.create('tbody', null));
+        var albumTitleRow = dojo.create('tr', null, albumTitleTbody);
+        var titleTd = dojo.create('td', {className: 'albumListTitle'},
+            albumTitleRow);
+        dojo.create('a', {innerHTML: albums[i].title, href: 'javascript:;',
+          onclick: dojo.hitch(this, onClickAlbum, viewer.id, albums[i])},
+        titleTd);
+        var editTd = dojo.create('td', {className: 'actionLinks'},
+            albumTitleRow);
+        editTd.style.textAlign="right";
+        dojo.create('a', {innerHTML: 'edit', href: 'javascript:;',
+          onclick: dojo.hitch(this, editAlbumPopup, albums[i])}, editTd);
+        editTd.appendChild(dojo.doc.createTextNode(' | '));
+        dojo.create('a', {innerHTML: 'delete', href: 'javascript:;',
+          onclick: dojo.hitch(this, deleteAlbumPopup, albums[i])}, editTd);
+
+        var openTabButton = new dijit.form.Button({label: 'Open in New Tab',
+          onClick: dojo.hitch(this, openAlbumNewTab, albums[i], null)});
+        editTd.appendChild(openTabButton.domNode);
+
+        if (albums[i].description) {
+          var albumDescription = dojo.create('tr', null, albumTitleTbody);
+          dojo.create('td', {innerHTML: albums[i].description,
+            className: 'albumListDescription', colSpan: '2'}, albumDescription);
+        }
+      }
+    } else {
+      albumsDiv.appendChild(dojo.doc.createTextNode('No albums found.'));
+    }
+    divManager.refreshWindow();
+
+    // Handles when user clicks an album
+    function onClickAlbum(userId, album) {
+      social.getMediaItemsByAlbum(userId, album.id, function(response) {
+        renderMediaItems(album, response.list);
+        divManager.showMediaItems();
+      });
+    }
+  }
+
+  /*
+   * Convenience function to retrieve albums and render.
+   */
+  function renderAlbumsByUser(userId, callback) {
+    social.getAlbumsByUser(userId, function(response) {
+      renderAlbums(response.list);
+      divManager.showAlbums();
+      if (callback !== null) callback();
+    });
+  }
+
+  /*
+   * Renders a grid of the given MediaItems.
+   *
+   */
+  function renderMediaItems(album, mediaItems) {
+    dojo.empty('mediaItemsDiv');
+    var mediaItemsDiv = dojo.byId('mediaItemsDiv');
+    var numCols = 5;
+
+    // Div to display navation bar and Create button
+    var topDiv = dojo.create('div', null, mediaItemsDiv);
+    var table = dojo.create('table', null, topDiv);
+    var tbody = dojo.create('tbody', null, table);
+    var tr = dojo.create('tr', null, tbody);
+    var td = dojo.create('td', {style: 'width:100%'}, tr);
+    dojo.create('a', {innerHTML: 'Albums', href: 'javascript:;',
+      onclick: dojo.hitch(this, renderAlbumsByUser, viewer.id, null)}, td);
+    td.appendChild(dojo.doc.createTextNode(' > ' + album.title));
+    td = dojo.create('td', {style: 'width:100%'}, tr);
+    var createButton = new dijit.form.Button({label: '+ New MediaItem',
+      onClick: dojo.hitch(this, editMediaItemPopupInGadget, album, null)});
+    td.appendChild(createButton.domNode);
+
+    // Div to display MediaItems in a grid
+    var gridDiv = dojo.create('div', null, mediaItemsDiv);
+    if (mediaItems.length > 0) {
+      var table = dojo.create('table', null, gridDiv);
+      var tbody = dojo.create('tbody', null, table);
+      var tr = null;
+      for (i = 0; i < mediaItems.length; i++) {
+        if (i % numCols == 0) {
+          tr = dojo.create('tr', null, tbody);
+        }
+        var td = dojo.create('td', {className: 'mediaItemBox'}, tr);
+        var imageTbody = dojo.create('table', null,
+            td).appendChild(dojo.create('tbody', null));
+        var imageTd = dojo.create('tr', null,
+            imageTbody).appendChild(dojo.create('td',
+            {className: 'mediaItemThumbnail'}));
+        if (mediaItems[i].url) {
+          var imageLink = dojo.create('a', {href: 'javascript:;',
+            onclick: dojo.hitch(this, renderMediaItemInDialog, album,
+                mediaItems[i])}, imageTd);
+          imageLink.appendChild(dojo.create('img',
+              {src: mediaItems[i].thumbnailUrl,
+                onerror: "this.src='" + docUrl + "';",
+                height:'100', width:'100'}));
+        } else {
+          dojo.create('img', {src: mediaItems[i].thumbnailUrl,
+            onerror: "this.src='" + docUrl + "';",
+            height:'100', width:'100'}, imageTd);
+        }
+        var titleTbody = dojo.create('table', null,
+            td).appendChild(dojo.create('tbody', null));
+        var titleTd = dojo.create('tr', null, titleTbody).appendChild(
+            dojo.create('td', {
+              style: 'text-align:center;' +
+                  "font-family:'comic sans ms';white-space:nowrap;"}));
+        titleTd.appendChild(dojo.doc.createTextNode(mediaItems[i].title));
+        var actionsTbody = dojo.create('table', null,
+            td).appendChild(dojo.create('tbody', null));
+        var actionsTd = dojo.create('tr', null, actionsTbody).appendChild(
+            dojo.create('td', {className: 'actionLinks',
+              style: 'text-align: center;'}));
+        dojo.create('a', {innerHTML: 'edit', href: 'javascript:;',
+          onclick: dojo.hitch(this, editMediaItemPopupInGadget,
+              album, mediaItems[i])}, actionsTd);
+        actionsTd.appendChild(dojo.doc.createTextNode(' | '));
+        dojo.create('a', {innerHTML: 'delete', href: 'javascript:;',
+          onclick: dojo.hitch(this, deleteMediaItemPopup, album,
+              mediaItems[i])}, actionsTd);
+      }
+    } else {
+      gridDiv.appendChild(dojo.doc.createTextNode('Album is empty'));
+    }
+    divManager.refreshWindow();
+  }
+
+  /*
+   * Convenience function to retriev & render MediaItems by Album.
+   */
+  function retrieveAndRenderMediaItems(album) {
+    social.getMediaItemsByAlbum(viewer.id, album.id, function(response) {
+      divManager.showMediaItems();
+      renderMediaItems(album, response.list);
+    });
+  }
+
+  /*
+   * Renders the view for a single MediaItem.
+   */
+  function renderMediaItem(album, mediaItem) {
+    dojo.empty('mediaItemDiv');
+    var mediaItemDiv = dojo.byId('mediaItemDiv');
+
+    // Div to display navation bar and Create button
+    var topDiv = dojo.create('div', null, mediaItemDiv);
+    var table = dojo.create('table', null, topDiv);
+    var tbody = dojo.create('tbody', null, table);
+    var tr = dojo.create('tr', null, tbody);
+    var td = dojo.create('td', {style: 'width:100%'}, tr);
+    dojo.create('a', {innerHTML: 'Albums', href: 'javascript:;',
+      onclick: dojo.hitch(this, renderAlbumsByUser, viewer.id, null)}, td);
+    td.appendChild(dojo.doc.createTextNode(' > '));
+    dojo.create('a', {innerHTML: album.title, href: 'javascript:;',
+      onclick: dojo.hitch(this, retrieveAndRenderMediaItems, album)}, td);
+    td.appendChild(dojo.doc.createTextNode(' > ' + mediaItem.title));
+
+    // Div to show MediaItem
+    var itemDiv = dojo.create('div', null, mediaItemDiv);
+    var table = dojo.create('table', null, itemDiv);
+    var tbody = dojo.create('tbody', null, table);
+    var tr = dojo.create('tr', null, tbody);
+    var td = dojo.create('td', null, tr);
+    dojo.create('img', {src: mediaItem.url}, td);
+    if (mediaItem.description) {
+      tr = dojo.create('tr', null, tbody);
+      td = dojo.create('td', null, tr);
+      td.appendChild(dojo.doc.createTextNode(mediaItem.description));
+    }
+
+    divManager.showMediaItem();
+  }
+
+
+
+  function renderMediaItemInDialog(album, mediaItem) {
+
+    var url = mediaItem.url;
+    var viewTarget = 'dialog';
+
+    function navigateCallback(site) {
+      gadgets.log('navigateCallback ');
+    }
+    gadgets.views.openUrl(url, navigateCallback, viewTarget);
+
+  }
+
+  /*
+   * Render album gadget in new tab
+   */
+  function openAlbumNewTab(album) {
+
+    function callback(album) {}
+    function navigateCallback(site, metadata) {}
+
+    var viewParams = {'viewerId': viewer.id, 'data': album};
+
+    var opt_params = {};
+    opt_params.view = 'albumFullView';
+    opt_params.viewTarget = 'tab';
+    opt_params.viewParams = viewParams;
+    gadgets.views.openGadget(callback, navigateCallback, opt_params);
+
+  }
+
+  /*
+   * Popup to edit album.
+   */
+  function editAlbumPopup(album) {
+    var opt_view = 'default.modalDialog';
+
+    function callback(album) {
+      social.updateAlbum(viewer.id, album.id, album, function(response) {
+        renderAlbumsByUser(viewer.id);
+      });
+    }
+
+    function navigateCallback(site, metadata) {
+      gadgets.log('navigateCallback');
+    }
+
+    var viewParams = {'data': album};
+
+    var opt_params = {};
+    opt_params.view = 'editAlbum';
+    opt_params.viewTarget = 'modalDialog';
+    opt_params.viewParams = viewParams;
+    gadgets.views.openGadget(callback, navigateCallback, opt_params);
+
+  };
+
+  /*
+   * Popup to edit MediaItem.
+   */
+  function editMediaItemPopupInGadget(album, mediaItem) {
+
+    function resultCallback(result) {
+      if (result != null) {
+        gadgets.log('container width = ' + result.width);
+        gadgets.log('container height = ' + result.height);
+      }
+    }
+    // Just an example to show how to use the getContainerDimensions API,
+    // it doesn't serve any other purpose for editMediaItemPopupInGadget
+    // function.
+    gadgets.window.getContainerDimensions(resultCallback);
+
+    function callback(newMediaItem) {
+      if(newMediaItem) {
+        var albumId = mediaItem == null ? album.id : mediaItem.albumId;
+        social.updateMediaItem(viewer.id, albumId, mediaItem.id, newMediaItem,
+          function(response) {
+            social.getMediaItemsByAlbum(viewer.id, album.id, function(response) {
+              renderMediaItems(album, response.list);
+            });
+        });
+      }
+    }
+
+    function navigateCallback(site, metadata) {
+      gadgets.log('navigateCallback');
+    }
+
+    var viewParams = {'data': {'album': album, 'mediaItem': mediaItem}};
+    var opt_params = {};
+    opt_params.view = 'editMediaItem';
+    opt_params.viewTarget = 'modalDialog';
+    opt_params.viewParams = viewParams;
+    gadgets.views.openGadget(callback, navigateCallback, opt_params);
+
+  }
+
+
+  /*
+   * Popup to confirm that the user wants to delete album.
+   */
+  function deleteAlbumPopup(album) {
+    if (confirm("Delete '" + album.title + "'?")) {
+      social.deleteAlbum(viewer.id, album.id, function(response) {
+        publish('org.apache.shindig.album.deleted', album);
+        gadgets.log('delete album response: ' + JSON.stringify(response));
+        renderAlbumsByUser(viewer.id);
+      });
+    }
+  }
+
+  /*
+   * Popup to confirm user wants to delete MediaItem.
+   */
+  function deleteMediaItemPopup(album, mediaItem) {
+    var albumId = mediaItem.albumId;
+    if (confirm("Delete '" + mediaItem.title + "'?")) {
+      social.deleteMediaItem(viewer.id, albumId, mediaItem.id,
+          function(response) {
+            publish('org.apache.shindig.mediaItem.deleted', mediaItem);
+            gadgets.log('delete mediaItem response: ' +
+                    JSON.stringify(response));
+            social.getMediaItemsByAlbum(viewer.id, albumId, function(response) {
+              renderMediaItems(album, response.list);
+            });
+          });
+    }
+  }
+
+  /*
+   * Publishers.
+   */
+  function publish(topic, payload) {
+    gadgets.Hub.publish(topic, payload);
+  }
+
+
+  return {
+
+    /*
+     * Initializes the gadget.
+     */
+    init: function() {
+
+      // Manages high-level divs
+      divManager = new DivManager();
+      divManager.init();
+
+      // Load data and render
+      loadData(function() {
+        social.getAlbumsByUser(viewer.id, function(response) {
+          renderAlbums(response.list);
+          divManager.showAlbums();
+        });
+      });
+    },
+
+    openAlbum: function(userId, album) {
+
+      // Manages high-level divs
+      divManager = new DivManager();
+      divManager.init();
+
+      loadData(function() {
+        social.getMediaItemsByAlbum(userId, album.id, function(response) {
+          renderMediaItems(album, response.list);
+          divManager.showMediaItems();
+        });
+      });
+
+    },
+
+    editAlbum: function(album) {
+
+      if (dojo.query('editAlbumFormDiv')) {
+        dojo.destroy('editAlbumFormDiv');
+      }
+
+      var formDiv = dojo.create('div', {id: 'editAlbumFormDiv'});
+
+      var form = new dijit.form.Form({id: 'editAlbumForm'});
+      formDiv.appendChild(form.domNode);
+
+      var table = dojo.create('table', null, form.domNode);
+      var tbody = dojo.create('tbody', null, table);
+
+      var tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'Title', 'for': 'title'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.ValidationTextBox({
+            name: 'title',
+            value: album == null ? '' : album.title
+          }).domNode
+      );
+
+      tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'Thumnail URL', 'for': 'thumbnail'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.ValidationTextBox({
+            name: 'thumbnail',
+            value: album == null ? '' : album.thumbnailUrl
+          }).domNode
+      );
+
+      tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'Description', 'for': 'description'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.Textarea({
+            name: 'description',
+            value: album == null ? '' : album.description
+          }).domNode
+      );
+
+      tr = dojo.create('tr', null, tbody);
+      var buttonTd = dojo.create('td', {colspan: '2', align: 'center'}, tr);
+      buttonTd.appendChild(new dijit.form.Button({
+        label: 'Save',
+        onClick: saveForm
+      }).domNode
+      );
+
+      buttonTd.appendChild(new dijit.form.Button({
+        label: 'Cancel',
+        onClick: destroyDialog
+      }).domNode
+      );
+
+      dojo.body().appendChild(formDiv);
+      gadgets.window.adjustHeight();
+
+      function saveForm() {
+        var values = form.get('value');
+
+        album.title = values.title;
+        album.thumbnailUrl = values.thumbnail;
+        album.description = values.description;
+
+        gadgets.views.setReturnValue(album);
+        destroyDialog();
+      }
+
+      function destroyDialog() {
+        gadgets.views.close();
+      }
+    },
+
+    editMediaItem: function(album, mediaItem) {
+
+      var albumId = mediaItem == null ? album.id : mediaItem.albumId;
+      var title = (mediaItem == null ? 'Create' : 'Edit') + ' MediaItem';
+
+      if (dojo.query('editMediaItemDialogDiv')) {
+        dojo.destroy('editMediaItemDialogDiv');
+      }
+      // Form div
+      var formDiv = dojo.create('div', {id: 'editMediaItemFormDiv'});
+      var form = new dijit.form.Form({id: 'editMediaItemForm'});
+      formDiv.appendChild(form.domNode);
+      var table = dojo.create('table', null, form.domNode);
+      var tbody = dojo.create('tbody', null, table);
+      var tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'Title', 'for': 'title'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.ValidationTextBox({
+            name: 'title',
+            value: mediaItem == null ? '' : mediaItem.title
+          }).domNode
+      );
+      tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'Description', 'for': 'description'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.Textarea({
+            name: 'description',
+            value: mediaItem == null ? '' : mediaItem.description
+          }).domNode
+      );
+      tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'Type', 'for': 'type'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.ValidationTextBox({
+            name: 'type',
+            value: mediaItem == null ? '' : mediaItem.type
+          }).domNode
+      );
+      tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'Thumnail URL', 'for': 'thumbnailUrl'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.ValidationTextBox({
+            name: 'thumbnailUrl',
+            value: mediaItem == null ? '' : mediaItem.thumbnailUrl
+          }).domNode
+      );
+      tr = dojo.create('tr', null, tbody);
+      dojo.create('td', null, tr).appendChild(dojo.create('label',
+          {innerHTML: 'URL', 'for': 'url'}));
+      dojo.create('td', null, tr).appendChild(
+          new dijit.form.ValidationTextBox({
+            name: 'url',
+            value: mediaItem == null ? '' : mediaItem.url
+          }).domNode
+      );
+      tr = dojo.create('tr', null, tbody);
+      var buttonTd = dojo.create('td', {colspan: '2', align: 'center'}, tr);
+      buttonTd.appendChild(new dijit.form.Button({
+        label: 'Save',
+        onClick: saveForm
+      }).domNode
+      );
+      buttonTd.appendChild(new dijit.form.Button({
+        label: 'Cancel',
+        onClick: destroyDialog
+      }).domNode
+      );
+
+      // Textarea div for JSON
+      var textAreaDiv = dojo.create('div',
+          {style: 'width:100%; height:100%;', id: 'textAreaDiv'});
+      var textArea = new dijit.form.Textarea({value: JSON.stringify(mediaItem),
+        rows: '20'});
+      textAreaDiv.appendChild(textArea.domNode);
+
+      // Put divs together
+      var tabContainer = new dijit.layout.TabContainer(
+          {style: 'width:400px; height:275px;'});
+      var formContentPane = new dijit.layout.ContentPane(
+          {title: 'Form', content: formDiv});
+      tabContainer.addChild(formContentPane);
+      var textAreaContentPane = new dijit.layout.ContentPane(
+          {title: 'JSON', content: textAreaDiv});
+      tabContainer.addChild(textAreaContentPane);
+      tabContainer.startup();
+      var dialogDiv = dojo.create('div', {id: 'editMediaItemDialogDiv'});
+      dialogDiv.appendChild(tabContainer.domNode);
+
+      dojo.body().appendChild(dialogDiv);
+      gadgets.window.adjustHeight();
+
+      function saveForm() {
+        var values = form.get('value');
+
+        var newMediaItem = {
+          title: values.title,
+          description: values.description,
+          type: values.type,
+          thumbnailUrl: values.thumbnailUrl,
+          url: values.url
+        };
+
+        gadgets.views.setReturnValue(newMediaItem);
+        destroyDialog();
+      }
+
+      function destroyDialog() {
+        gadgets.views.close();
+      }
+    }
+  };
+}
diff --git a/trunk/content/gadgets/media-openGadgets/Social.js b/trunk/content/gadgets/media-openGadgets/Social.js
new file mode 100644
index 0000000..6431c88
--- /dev/null
+++ b/trunk/content/gadgets/media-openGadgets/Social.js
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*
+ * Defines high level functionality to interact with the OpenSocial API.
+ */
+function SocialWrapper() {
+
+  /*
+   * Retrieves the current viewer.
+   */
+  this.getViewer = function(callback) {
+    osapi.people.getViewer().execute(callback);
+  }
+
+  /*
+   * Retrieves the current owner.
+   */
+  this.getOwner = function(callback) {
+    osapi.people.getOwner().execute(callback);
+  }
+
+  // ------------------------ ALBUMS ----------------------
+  /*
+   * Retrieves albums by ID(s).
+   */
+  this.getAlbumsById = function(userId, albumId, callback) {
+    var params = {userId: userId, albumId: albumId};
+    osapi.albums.get(params).execute(callback);
+  }
+
+  /*
+   * Retrieves albums by user.
+   */
+  this.getAlbumsByUser = function(userId, callback) {
+    osapi.albums.get({userId: userId}).execute(callback);
+  }
+
+  /*
+   * Retrieves albums by group.
+   */
+  this.getAlbumsByGroup = function(userId, groupId, callback) {
+    osapi.albums.get({userId: userId, groupId: groupId}).execute(callback);
+  }
+
+  /*
+   * Creates an album for the given user.
+   */
+  this.createAlbum = function(userId, album, callback) {
+    var params = {
+      userId: userId,
+      album: album
+    };
+    osapi.albums.create(params).execute(callback);
+  }
+
+  /*
+   * Updates an album by ID.
+   */
+  this.updateAlbum = function(userId, albumId, album, callback) {
+    var params = {
+      userId: userId,
+      albumId: albumId,
+      album: album
+    };
+    osapi.albums.update(params).execute(callback);
+  }
+
+  /*
+   * Deletes an album by ID.
+   */
+  this.deleteAlbum = function(userId, albumId, callback) {
+    var params = {userId: userId, albumId: albumId};
+    osapi.albums['delete'](params).execute(callback);
+  }
+
+  // ------------------------------- MEDIAITEMS ----------------------------
+  /*
+   * Creates a MediaItem.
+   */
+  this.createMediaItem = function(userId, albumId, mediaItem, callback) {
+    var params = {
+      userId: userId,
+      albumId: albumId,
+      mediaItem: mediaItem
+    };
+    osapi.mediaItems.create(params).execute(callback);
+  }
+
+  /*
+   * Updates a MediaItem by ID.
+   */
+  this.updateMediaItem = function(userId, albumId, mediaItemId, mediaItem,
+          callback) {
+    var params = {
+      userId: userId,
+      albumId: albumId,
+      mediaItemId: mediaItemId,
+      mediaItem: mediaItem
+    };
+    console.log('PARAMS: ' + JSON.stringify(params));
+    osapi.mediaItems.update(params).execute(callback);
+  }
+
+  /*
+   * Retrieves MediaItems by ID(s).
+   */
+  this.getMediaItemsById = function(userId, albumId, mediaItemId, callback) {
+    var params = {
+      userId: userId,
+      albumId: albumId,
+      mediaItemId: mediaItemId
+    };
+    osapi.mediaItems.get(params).execute(callback);
+  }
+
+  /*
+   * Retrieves MediaItems by album.
+   */
+  this.getMediaItemsByAlbum = function(userId, albumId, callback) {
+    osapi.mediaItems.get({userId: userId, albumId: albumId}).execute(callback);
+  }
+
+  /*
+   * Retrieves MediaItems by user and group.
+   */
+  this.getMediaItemsByUser = function(userId, groupId, callback) {
+    osapi.mediaItems.get({userId: userId, groupId: groupId}).execute(callback);
+  }
+
+  /*
+   * Deletes a MediaItem by ID.
+   */
+  this.deleteMediaItem = function(userId, albumId, mediaItemId, callback) {
+    var params = {
+      userId: userId,
+      albumId: albumId,
+      mediaItemId: mediaItemId
+    };
+    osapi.mediaItems['delete'](params).execute(callback);
+  }
+}
diff --git a/trunk/content/gadgets/media-openGadgets/styles.css b/trunk/content/gadgets/media-openGadgets/styles.css
new file mode 100644
index 0000000..799705b
--- /dev/null
+++ b/trunk/content/gadgets/media-openGadgets/styles.css
@@ -0,0 +1,109 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+/* ============ ROUND 2 ============== */
+td.albumsTitle {
+  font-family: 'comic sans ms';
+  width: 100%;
+  font-size: 24px;
+}
+
+td.albumListThumbnail {
+  width: 10%;
+  height: 75px;
+}
+
+td.albumListRight {
+  width: 90%;
+}
+
+td.actionLinks {
+  width: 100%;
+  font-size: 12px;
+}
+
+td.albumListTitle {
+  font-size: 24px;
+  white-space: nowrap;
+  font-family: 'comic sans ms';
+}
+
+td.albumListDescription {
+  font-family: 'comic sans ms';
+}
+
+.albumsTable {
+  width: 100%;
+  border-style: solid;
+  background-color: #b0c4de;
+}
+
+td.mediaItemThumbnail {
+  height: 100%;
+}
+
+td.mediaItemBox {
+  width: 150px;
+  height: 100px;
+}
+
+.mediaItemControls {
+  
+}
+
+/* ============ ROUND 1 ============== */
+.temp1 {
+  background-color: #6495ed;
+}
+
+.temp2 {
+  background-color: #e0ffff;
+}
+
+.temp3 {
+  background-color: #b0c4de;
+  background-image: url("img_flwr.png");
+  background-repeat: no-repeat;
+  background-position: top right;
+}
+
+td2 {
+  border-style: solid;
+}
+
+td.albumLeft {
+  width: 10%;
+}
+
+td.albumRight {
+  width: 90%;
+}
+
+td.albumEdit {
+  width: 100%;
+  text-align: right;
+  vertical-align: middle;
+  font-size: 12px;
+}
+
+.albumTitleStyle {
+  color: blue;
+}
+
+.albumElement {
+  background-color: #b0c4de;
+}
\ No newline at end of file
diff --git a/trunk/content/gadgets/media/Media.xml b/trunk/content/gadgets/media/Media.xml
new file mode 100644
index 0000000..a2b79af
--- /dev/null
+++ b/trunk/content/gadgets/media/Media.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+    <ModulePrefs title="Albums and MediaItems">
+        <Require feature="osapi"/>
+        <Require feature="dynamic-height"/>
+        <Require feature="pubsub-2">
+		  <Param name="topics">
+		    <![CDATA[ 
+		    <Topic title="MediaItem Created" name="org.apache.shindig.mediaItem.created" description="Publishes MediaItem created events." type="org.opensocial.data.mediaItem" publish="true"/>
+		    <Topic title="MediaItem Updated" name="org.apache.shindig.mediaItem.updated" description="Publishes MediaItem updated events." type="org.opensocial.data.mediaItem" publish="true"/>
+		    <Topic title="MediaItem Deleted" name="org.apache.shindig.mediaItem.deleted" description="Publishes MediaItem deleted events." type="org.opensocial.data.mediaItem" publish="true"/>
+		    <Topic title="Album Created" name="org.apache.shindig.album.created" description="Publishes Album created events." type="org.opensocial.data.album" publish="true">
+		    <Topic title="Album Updated" name="org.apache.shindig.album.updated" description="Publishes Album updated events." type="org.opensocial.data.album" publish="true">
+		    <Topic title="Album Deleted" name="org.apache.shindig.album.deleted" description="Publishes Album deleted events." type="org.opensocial.data.album" publish="true">
+		    ]]>
+		  </Param>
+		</Require>
+    </ModulePrefs>
+    
+    <Content type="html"><![CDATA[
+    <html>
+        <head>	
+            <!-- Source imports -->
+            <script src='http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js' type='text/javascript' djConfig='parseOnLoad:true, isDebug:true'></script>
+            <script src='Social.js' type='text/javascript'></script>
+            <script src='MediaUI.js' type='text/javascript'></script>
+            
+            
+            <!-- Styling -->
+            <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/tundra/tundra.css"></link>
+            <link rel="stylesheet" type="text/css" href="styles.css">
+            <style type="text/css">
+            </style>
+            
+            <!-- DOJO requires -->
+            <script type='text/javascript'>
+                dojo.require('dijit.form.Button');
+                dojo.require('dijit.form.Form');
+                dojo.require('dijit.form.TextBox');
+                dojo.require('dijit.form.ValidationTextBox');
+                dojo.require('dijit.Dialog');
+                dojo.require('dijit.form.Textarea');
+                dojo.require('dijit.layout.ContentPane');
+                dojo.require('dijit.layout.TabContainer');
+            </script>
+            
+            <!-- JavaScript -->
+            <script type="text/javascript"> 
+                <!-- Entry point to the gadget -->
+                function init() {
+                    console.log("dojo initialized");
+                    new MediaUI(new SocialWrapper()).init();
+                }
+
+                <!-- Register entry point -->
+                gadgets.util.registerOnLoadHandler(function() {
+                    dojo.addOnLoad(init);
+                });
+            </script>
+        </head>
+        <body class="tundra">
+        </body>
+    </html>
+    ]]></Content>
+</Module>
diff --git a/trunk/content/gadgets/media/MediaUI.js b/trunk/content/gadgets/media/MediaUI.js
new file mode 100644
index 0000000..87ed9a3
--- /dev/null
+++ b/trunk/content/gadgets/media/MediaUI.js
@@ -0,0 +1,544 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/*
+ * The User Interface for the Albums & MediaItems gadget.
+ *
+ * SHINDIG TODOS
+ *  set ownerId automatically?
+ *  delete children mediaitems when album deleted?
+ *  update only updates given fields?
+ *  update album mediaitem count when inserting/removing mediaitem?
+ *
+ * GADGET TODOS
+ *  album info such as how many albums are contained
+ *  fix auto height for edit album popup
+ *  thumnail pictures
+ */
+function MediaUI(social) {
+    var viewer = null;
+    var divManager = null;
+
+    var folderUrl = 'http://www.clker.com/cliparts/2/b/b/3/1194983972976950993blue_folder_seth_yastrov_01.svg.med.png';
+    var docUrl = 'http://www.plastyc.com/images/document-icon.png';
+
+    /*
+     * Initializes the gadget.
+     */
+    this.init = function() {
+        console.log('initializing AlbumsUI');
+
+        // Manages high-level divs
+        divManager = new DivManager();
+        divManager.init();
+
+        // Load data and render
+        loadData(function() {
+            social.getAlbumsByUser(viewer.id, function(response) {
+                renderAlbums(response.list);
+                divManager.showAlbums();
+            });
+        });
+    }
+
+    /*
+     * Pre-load data for gadget.
+     */
+    function loadData(callback) {
+        social.getViewer(function(data) {
+            viewer = data;
+            callback();
+        });
+    }
+
+    /*
+     * Manages the gadgets main DIV elements.
+     *
+     * TODO: use dojo.query() & classes rather than divs[]
+     * TODO: showOnly() function to avoid flashing/pauses
+     */
+    function DivManager() {
+        var divs = [];
+
+        this.init = function() {
+            console.log('DivManager.init');
+            addDiv('albumsDiv');
+            addDiv('mediaItemsDiv');
+            addDiv('mediaItemDiv');
+            hideAll();
+        }
+
+        this.showAlbums = function() {
+            console.log('DivManager.showAlbums');
+            hideAll();
+            divs['albumsDiv'].style.display = 'block';
+            this.refreshWindow();
+        }
+
+        this.showMediaItems = function() {
+            console.log('DivManager.showMediaItems');
+            hideAll();
+            divs['mediaItemsDiv'].style.display = 'block';
+            this.refreshWindow();
+        }
+
+        this.showMediaItem = function() {
+            console.log('DivManager.showMediaItem');
+            hideAll();
+            divs['mediaItemDiv'].style.display = 'block';
+            this.refreshWindow();
+        }
+
+        this.refreshWindow = function() {
+            gadgets.window.adjustHeight(500);
+        }
+
+        function hideAll() {
+            for (key in divs) { divs[key].style.display = 'none'; }
+        }
+
+        function addDiv(id) { divs[id] = dojo.create('div', {id: id}, dojo.body()); }
+    }
+
+    /*
+     * Renders a list of the given albums.
+     */
+    function renderAlbums(albums) {
+        console.log('renderAlbums');
+
+        dojo.empty('albumsDiv');
+        var albumsDiv = dojo.byId('albumsDiv');
+
+        var albumsBanner = dojo.create('div', null, albumsDiv);
+        var table = dojo.create('table', null, albumsBanner);
+        var tbody = dojo.create('tbody', null, table);
+        var tr = dojo.create('tr', null, tbody);
+        dojo.create('td', {innerHTML: viewer.name.formatted + "'s Albums", className: 'albumsTitle'}, tr);
+        dojo.create('td', null, tr).appendChild(new dijit.form.Button({label: '+ New Album', onClick: dojo.hitch(this, editAlbumPopup, null)}).domNode);
+
+        var albumsList = dojo.create('div', null, albumsDiv);
+        if (albums.length > 0) {
+            var table = dojo.create('table', {className: 'albumsTable'}, albumsList);
+            var tbody = dojo.create('tbody', null, table);
+            for (i = 0; i < albums.length; i++) {
+                var albumRow = dojo.create('tr', null, tbody);
+                var albumLeft = dojo.create('td', {className: 'albumListThumbnail'}, albumRow);
+                var imgLink = dojo.create('a', {href: 'javascript:;', onclick: dojo.hitch(this, onClickAlbum, viewer.id, albums[i])}, albumLeft);
+                dojo.create('img', {src: albums[i].thumbnailUrl || folderUrl, onerror: "this.src='" + folderUrl + "';", width: '100', height:'100'}, imgLink);
+                var albumRight = dojo.create('td', {className: 'albumListRight'}, albumRow);
+                var albumTitleTbody = dojo.create('table', null, albumRight).appendChild(dojo.create('tbody', null));
+                var albumTitleRow = dojo.create('tr', null, albumTitleTbody);
+                var titleTd = dojo.create('td', {className: 'albumListTitle'}, albumTitleRow);
+                dojo.create('a', {innerHTML: albums[i].title, href: 'javascript:;', onclick: dojo.hitch(this, onClickAlbum, viewer.id, albums[i])}, titleTd);
+                var editTd = dojo.create('td', {className: 'actionLinks'}, albumTitleRow);
+                editTd.style.textAlign="right";
+                dojo.create('a', {innerHTML: 'edit', href: 'javascript:;', onclick: dojo.hitch(this, editAlbumPopup, albums[i])}, editTd);
+                editTd.appendChild(dojo.doc.createTextNode(' | '));
+                dojo.create('a', {innerHTML: 'delete', href: 'javascript:;', onclick: dojo.hitch(this, deleteAlbumPopup, albums[i])}, editTd);
+                if (albums[i].description) {
+                    var albumDescription = dojo.create('tr', null, albumTitleTbody);
+                    dojo.create('td', {innerHTML: albums[i].description, className: 'albumListDescription', colSpan: '2'}, albumDescription);
+                }
+                //var albumInfo = dojo.create('tr', null, albumRight);
+                //var infoStr = "ID: " + albums[i].id + " | Owner ID: " + albums[i].ownerId;
+                //dojo.create('td', {innerHTML: infoStr, className: 'albumListInfo', colspan: '2'}, albumInfo);
+            }
+        } else {
+            albumsDiv.appendChild(dojo.doc.createTextNode('No albums found.'));
+        }
+        divManager.refreshWindow();
+
+        // Handles when user clicks an album
+        function onClickAlbum(userId, album) {
+            social.getMediaItemsByAlbum(userId, album.id, function(response) {
+                renderMediaItems(album, response.list);
+                divManager.showMediaItems();
+            });
+        }
+    }
+
+    /*
+     * Convenience function to retrieve albums and render.
+     */
+    function renderAlbumsByUser(userId, callback) {
+        social.getAlbumsByUser(userId, function(response) {
+            renderAlbums(response.list);
+            divManager.showAlbums();
+            if (callback != null) callback();
+        });
+    }
+
+    /*
+     * Renders a grid of the given MediaItems.
+     *
+     * TODO: simplify this by simply taking in 'album', retrieving MediaItems here
+     */
+    function renderMediaItems(album, mediaItems) {
+        console.log('renderMediaItems');
+        dojo.empty('mediaItemsDiv');
+        var mediaItemsDiv = dojo.byId('mediaItemsDiv');
+        var numCols = 5;
+
+        // Div to display navation bar and Create button
+        var topDiv = dojo.create('div', null, mediaItemsDiv);
+        var table = dojo.create('table', null, topDiv);
+        var tbody = dojo.create('tbody', null, table);
+        var tr = dojo.create('tr', null, tbody);
+        var td = dojo.create('td', {style: 'width:100%'}, tr);
+        dojo.create('a', {innerHTML: 'Albums', href: 'javascript:;', onclick: dojo.hitch(this, renderAlbumsByUser, viewer.id, null)}, td);
+        td.appendChild(dojo.doc.createTextNode(' > ' + album.title));
+        td = dojo.create('td', {style: 'width:100%'}, tr);
+        var createButton = new dijit.form.Button({label: '+ New MediaItem', onClick: dojo.hitch(this, editMediaItemPopup, album, null)});
+        td.appendChild(createButton.domNode);
+
+        // Div to display MediaItems in a grid
+        var gridDiv = dojo.create('div', null, mediaItemsDiv);
+        if (mediaItems.length > 0) {
+            var table = dojo.create('table', null, gridDiv);
+            var tbody = dojo.create('tbody', null, table);
+            var tr = null;
+            for (i = 0; i < mediaItems.length; i++) {
+                if (i % numCols == 0) {
+                    tr = dojo.create('tr', null, tbody);
+                }
+                var td = dojo.create('td', {className: 'mediaItemBox'}, tr);
+                var imageTbody = dojo.create('table', null, td).appendChild(dojo.create('tbody', null));
+                var imageTd = dojo.create('tr', null, imageTbody).appendChild(dojo.create('td', {className: 'mediaItemThumbnail'}));
+                if (mediaItems[i].url) {
+                    var imageLink = dojo.create('a', {href: 'javascript:;', onclick: dojo.hitch(this, renderMediaItem, album, mediaItems[i])}, imageTd);
+                    imageLink.appendChild(dojo.create('img', {src: mediaItems[i].thumbnailUrl, height: '100', width:'100'}));
+                } else {
+                    dojo.create('img', {src: mediaItems[i].thumbnailUrl, onerror: "this.src='" + docUrl + "';", height:'100', width:'100'}, imageTd);
+                }
+                var titleTbody = dojo.create('table', null, td).appendChild(dojo.create('tbody', null));
+                var titleTd = dojo.create('tr', null, titleTbody).appendChild(dojo.create('td', {style: "text-align:center; font-family:'comic sans ms';white-space:nowrap;"}));
+                titleTd.appendChild(dojo.doc.createTextNode(mediaItems[i].title));
+                var actionsTbody = dojo.create('table', null, td).appendChild(dojo.create('tbody', null));
+                var actionsTd = dojo.create('tr', null, actionsTbody).appendChild(dojo.create('td', {className: 'actionLinks', style: 'text-align: center;'}));
+                dojo.create('a', {innerHTML: 'edit', href: 'javascript:;', onclick: dojo.hitch(this, editMediaItemPopup, album, mediaItems[i])}, actionsTd);
+                actionsTd.appendChild(dojo.doc.createTextNode(' | '));
+                dojo.create('a', {innerHTML: 'delete', href: 'javascript:;', onclick: dojo.hitch(this, deleteMediaItemPopup, album, mediaItems[i])}, actionsTd);
+            }
+        } else {
+            gridDiv.appendChild(dojo.doc.createTextNode('Album is empty'));
+        }
+        divManager.refreshWindow();
+    }
+
+    /*
+     * Convenience function to retriev & render MediaItems by Album.
+     */
+    function retrieveAndRenderMediaItems(album) {
+        social.getMediaItemsByAlbum(viewer.id, album.id, function(response) {
+            divManager.showMediaItems();
+            renderMediaItems(album, response.list);
+        });
+    }
+
+    /*
+     * Renders the view for a single MediaItem.
+     */
+    function renderMediaItem(album, mediaItem) {
+        console.log('renderMediaItem');
+        dojo.empty('mediaItemDiv');
+        var mediaItemDiv = dojo.byId('mediaItemDiv');
+
+        // Div to display navation bar and Create button
+        var topDiv = dojo.create('div', null, mediaItemDiv);
+        var table = dojo.create('table', null, topDiv);
+        var tbody = dojo.create('tbody', null, table);
+        var tr = dojo.create('tr', null, tbody);
+        var td = dojo.create('td', {style: 'width:100%'}, tr);
+        dojo.create('a', {innerHTML: 'Albums', href: 'javascript:;', onclick: dojo.hitch(this, renderAlbumsByUser, viewer.id, null)}, td);
+        td.appendChild(dojo.doc.createTextNode(' > '));
+        dojo.create('a', {innerHTML: album.title, href: 'javascript:;', onclick: dojo.hitch(this, retrieveAndRenderMediaItems, album)}, td);
+        td.appendChild(dojo.doc.createTextNode(' > ' + mediaItem.title));
+
+        // Div to show MediaItem
+        var itemDiv = dojo.create('div', null, mediaItemDiv);
+        var table = dojo.create('table', null, itemDiv);
+        var tbody = dojo.create('tbody', null, table);
+        var tr = dojo.create('tr', null, tbody);
+        var td = dojo.create('td', null, tr);
+        dojo.create('img', {src: mediaItem.url}, td);
+        if (mediaItem.description) {
+            tr = dojo.create('tr', null, tbody);
+            td = dojo.create('td', null, tr);
+            td.appendChild(dojo.doc.createTextNode(mediaItem.description));
+        }
+
+        divManager.showMediaItem();
+    }
+
+    /*
+     * Popup to edit album.
+     */
+    function editAlbumPopup(album) {
+        console.log('editAlbumPopup: ' + JSON.stringify(album));
+
+        var title = (album == null ? 'Create' : 'Edit') + ' Album';
+        var dialog = new dijit.Dialog({id: 'editAlbumPopup', title: title, onCancel: destroyDialog});
+        dojo.body().appendChild(dialog.domNode);
+
+        var formDiv = dojo.create('div', {id: 'editAlbumFormDiv'});
+        var form = new dijit.form.Form({id: 'editAlbumForm'});
+        formDiv.appendChild(form.domNode);
+        var table = dojo.create('table', null, form.domNode);
+        var tbody = dojo.create('tbody', null, table);
+        var tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'Title', 'for': 'title'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.ValidationTextBox({
+                name: 'title',
+                value: album == null ? '' : album.title
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'Thumnail URL', 'for': 'thumbnail'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.ValidationTextBox({
+                name: 'thumbnail',
+                value: album == null ? '' : album.thumbnailUrl
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'Description', 'for': 'description'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.Textarea({
+                name: 'description',
+                value: album == null ? '' : album.description
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        var buttonTd = dojo.create('td', {colspan: '2', align: 'center'}, tr);
+        buttonTd.appendChild(new dijit.form.Button({
+                label: 'Save',
+                onClick: saveForm
+            }).domNode
+        );
+        buttonTd.appendChild(new dijit.form.Button({
+                label: 'Cancel',
+                onClick: destroyDialog
+            }).domNode
+        );
+
+        dialog.set('content', formDiv);
+        dialog.show();
+
+        function saveForm() {
+            console.log('saveForm');
+            var values = form.get('value');
+            var newAlbum = {
+                title: values.title,
+                thumbnailUrl: values.thumbnail,
+                description: values.description,
+                ownerId: viewer.id  // TODO: bug? Albums service should set this
+            };
+            if (album == null) {
+                social.createAlbum(viewer.id, newAlbum, function(response) {
+                	publish('org.apache.shindig.album.created', newAlbum);
+                    console.log('created album response: ' + JSON.stringify(response));
+                    renderAlbumsByUser(viewer.id);
+                });
+            } else {
+                social.updateAlbum(viewer.id, album.id, newAlbum, function(response) {
+                	publish('org.apache.shindig.album.updated', newAlbum);
+                    console.log('updated album response: ' + JSON.stringify(response));
+                    renderAlbumsByUser(viewer.id);
+                });
+            }
+            destroyDialog();
+        }
+
+        // Handles destroying the dialog popup
+        function destroyDialog() {
+            console.log('destroyDialog');
+            dialog.hide();
+            dialog.destroyRecursive(false);
+            dialog.destroyRendering(false);
+            dialog.destroy(false);
+        }
+    }
+
+    /*
+     * Popup to edit MediaItem.
+     */
+    function editMediaItemPopup(album, mediaItem) {
+        console.log('editMediaItemPopup: ' + JSON.stringify(mediaItem));
+
+        var albumId = mediaItem == null ? album.id : mediaItem.albumId;
+        var title = (mediaItem == null ? 'Create' : 'Edit') + ' MediaItem';
+        var dialog = new dijit.Dialog({id: 'editMediaItemPopup', title: title, onCancel: destroyDialog});
+        dojo.body().appendChild(dialog.domNode);
+
+        // Form div
+        var formDiv = dojo.create('div', {id: 'editMediaItemFormDiv'});
+        var form = new dijit.form.Form({id: 'editMediaItemForm'});
+        formDiv.appendChild(form.domNode);
+        var table = dojo.create('table', null, form.domNode);
+        var tbody = dojo.create('tbody', null, table);
+        var tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'Title', 'for': 'title'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.ValidationTextBox({
+                name: 'title',
+                value: mediaItem == null ? '' : mediaItem.title
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'Description', 'for': 'description'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.Textarea({
+                name: 'description',
+                value: mediaItem == null ? '' : mediaItem.description
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'Type', 'for': 'type'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.ValidationTextBox({
+                name: 'type',
+                value: mediaItem == null ? '' : mediaItem.type
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'Thumnail URL', 'for': 'thumbnailUrl'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.ValidationTextBox({
+                name: 'thumbnailUrl',
+                value: mediaItem == null ? '' : mediaItem.thumbnailUrl
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        dojo.create('td', null, tr).appendChild(dojo.create('label', {'innerHTML': 'URL', 'for': 'url'}));
+        dojo.create('td', null, tr).appendChild(
+            new dijit.form.ValidationTextBox({
+                name: 'url',
+                value: mediaItem == null ? '' : mediaItem.url
+            }).domNode
+        );
+        tr = dojo.create('tr', null, tbody);
+        var buttonTd = dojo.create('td', {colspan: '2', align: 'center'}, tr);
+        buttonTd.appendChild(new dijit.form.Button({
+                label: 'Save',
+                onClick: saveForm
+            }).domNode
+        );
+        buttonTd.appendChild(new dijit.form.Button({
+                label: 'Cancel',
+                onClick: destroyDialog
+            }).domNode
+        );
+
+        // Textarea div for JSON
+        var textAreaDiv = dojo.create('div', {style: 'width:100%; height:100%;', id: 'textAreaDiv'});
+        var textArea = new dijit.form.Textarea({value: JSON.stringify(mediaItem), rows: '20'});
+        textAreaDiv.appendChild(textArea.domNode);
+
+        // Put divs together
+        var tabContainer = new dijit.layout.TabContainer({style: 'width:400px; height:300px;'});
+        var formContentPane = new dijit.layout.ContentPane({title: 'Form', content: formDiv});
+        tabContainer.addChild(formContentPane);
+        var textAreaContentPane = new dijit.layout.ContentPane({title: 'JSON', content: textAreaDiv});
+        tabContainer.addChild(textAreaContentPane);
+        tabContainer.startup();
+        var dialogDiv = dojo.create('div', null);
+        dialogDiv.appendChild(tabContainer.domNode);
+
+        dialog.set('content', dialogDiv);
+        dialog.show();
+
+        function saveForm() {
+            console.log('saveForm mediaItem');
+            var values = form.get('value');
+            var newMediaItem = {
+                title: values.title,
+                description: values.description,
+                type: values.type,
+                thumbnailUrl: values.thumbnailUrl,
+                url: values.url
+            };
+            if (newMediaItem.type == null || newMediaItem.type == '') newMediaItem.type = 'image';
+            if (mediaItem == null) {
+                social.createMediaItem(viewer.id, albumId, newMediaItem, function(response) {
+                	publish('org.apache.shindig.mediaItem.created', newMediaItem);
+                    console.log('created MediaItem response: ' + JSON.stringify(response));
+                    social.getMediaItemsByAlbum(viewer.id, album.id, function(response) {
+                        renderMediaItems(album, response.list);
+                    });
+                });
+            } else {
+                social.updateMediaItem(viewer.id, albumId, mediaItem.id, newMediaItem, function(response) {
+                	publish('org.apache.shindig.mediaItem.updated', newMediaItem);
+                    console.log('updated MediaItem response: ' + JSON.stringify(response));
+                    social.getMediaItemsByAlbum(viewer.id, album.id, function(response) {
+                        renderMediaItems(album, response.list);
+                    });
+                });
+            }
+            destroyDialog();
+        }
+
+        // Handles destroying the dialog popup
+        function destroyDialog() {
+            console.log('destroyDialog');
+            dialog.hide();
+            dialog.destroyRecursive(false);
+            dialog.destroyRendering(false);
+            dialog.destroy(false);
+        }
+    }
+
+    /*
+     * Popup to confirm that the user wants to delete album.
+     */
+    function deleteAlbumPopup(album) {
+        console.log('deleteAlbumPopup');
+        if (confirm("Delete '" + album.title + "'?")) {
+            social.deleteAlbum(viewer.id, album.id, function(response) {
+            	publish('org.apache.shindig.album.deleted', album);
+                console.log('delete album response: ' + JSON.stringify(response));
+                renderAlbumsByUser(viewer.id);
+            });
+        }
+    }
+
+    /*
+     * Popup to confirm user wants to delete MediaItem.
+     */
+    function deleteMediaItemPopup(album, mediaItem) {
+        console.log('deleteMediaItemPopup');
+        var albumId = mediaItem.albumId;
+        if (confirm("Delete '" + mediaItem.title + "'?")) {
+            social.deleteMediaItem(viewer.id, albumId, mediaItem.id, function(response) {
+            	publish('org.apache.shindig.mediaItem.deleted', mediaItem);
+                console.log('delete mediaItem response: ' + JSON.stringify(response));
+                social.getMediaItemsByAlbum(viewer.id, albumId, function(response) {
+                    renderMediaItems(album, response.list);
+                });
+            });
+        }
+    }
+
+    /*
+     * Publishers.
+     */
+    function publish(topic, payload) {
+    	gadgets.Hub.publish(topic, payload);
+    }
+}
diff --git a/trunk/content/gadgets/media/Social.js b/trunk/content/gadgets/media/Social.js
new file mode 100644
index 0000000..2a49a37
--- /dev/null
+++ b/trunk/content/gadgets/media/Social.js
@@ -0,0 +1,156 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/*
+ * Defines high level functionality to interact with the OpenSocial API.
+ */
+function SocialWrapper() {
+
+    /*
+     * Retrieves the current viewer.
+     */
+    this.getViewer = function(callback) {
+        osapi.people.getViewer().execute(callback);
+    }
+
+    /*
+     * Retrieves the current owner.
+     */
+    this.getOwner = function(callback) {
+        osapi.people.getOwner().execute(callback);
+    }
+
+    //------------------------ ALBUMS ----------------------
+    /*
+     * Retrieves albums by ID(s).
+     */
+    this.getAlbumsById = function(userId, albumId, callback) {
+        var params = {userId: userId, albumId: albumId};
+        osapi.albums.get(params).execute(callback);
+    }
+
+    /*
+     * Retrieves albums by user.
+     */
+    this.getAlbumsByUser = function(userId, callback) {
+        osapi.albums.get({userId: userId}).execute(callback);
+    }
+
+    /*
+     * Retrieves albums by group.
+     */
+    this.getAlbumsByGroup = function(userId, groupId, callback) {
+        osapi.albums.get({userId: userId, groupId: groupId}).execute(callback);
+    }
+
+    /*
+     * Creates an album for the given user.
+     */
+    this.createAlbum = function(userId, album, callback) {
+        var params = {
+            userId: userId,
+            album: album
+        };
+        osapi.albums.create(params).execute(callback);
+    }
+
+    /*
+     * Updates an album by ID.
+     */
+    this.updateAlbum = function(userId, albumId, album, callback) {
+        var params = {
+            userId: userId,
+            albumId: albumId,
+            album: album
+        };
+        osapi.albums.update(params).execute(callback);
+    }
+
+    /*
+     * Deletes an album by ID.
+     */
+    this.deleteAlbum = function(userId, albumId, callback) {
+        var params = {userId: userId, albumId: albumId};
+        osapi.albums['delete'](params).execute(callback);
+    }
+
+    //------------------------------- MEDIAITEMS ----------------------------
+    /*
+     * Creates a MediaItem.
+     */
+    this.createMediaItem = function(userId, albumId, mediaItem, callback) {
+        var params = {
+            userId: userId,
+            albumId: albumId,
+            mediaItem: mediaItem
+        };
+        osapi.mediaItems.create(params).execute(callback);
+    }
+
+    /*
+     * Updates a MediaItem by ID.
+     */
+    this.updateMediaItem = function(userId, albumId, mediaItemId, mediaItem, callback) {
+        var params = {
+            userId: userId,
+            albumId: albumId,
+            mediaItemId: mediaItemId,
+            mediaItem: mediaItem
+        };
+        console.log('PARAMS: ' + JSON.stringify(params));
+        osapi.mediaItems.update(params).execute(callback);
+    }
+
+    /*
+     * Retrieves MediaItems by ID(s).
+     */
+    this.getMediaItemsById = function(userId, albumId, mediaItemId, callback) {
+        var params = {
+            userId: userId,
+            albumId: albumId,
+            mediaItemId: mediaItemId
+        };
+        osapi.mediaItems.get(params).execute(callback);
+    }
+
+    /*
+     * Retrieves MediaItems by album.
+     */
+    this.getMediaItemsByAlbum = function(userId, albumId, callback) {
+        osapi.mediaItems.get({userId: userId, albumId: albumId}).execute(callback);
+    }
+
+    /*
+     * Retrieves MediaItems by user and group.
+     */
+    this.getMediaItemsByUser = function(userId, groupId, callback) {
+        osapi.mediaItems.get({userId: userId, groupId: groupId}).execute(callback);
+    }
+
+    /*
+     * Deletes a MediaItem by ID.
+     */
+    this.deleteMediaItem = function(userId, albumId, mediaItemId, callback) {
+        var params = {
+            userId: userId,
+            albumId: albumId,
+            mediaItemId: mediaItemId
+        };
+        osapi.mediaItems['delete'](params).execute(callback);
+    }
+}
diff --git a/trunk/content/gadgets/media/styles.css b/trunk/content/gadgets/media/styles.css
new file mode 100644
index 0000000..73a7df8
--- /dev/null
+++ b/trunk/content/gadgets/media/styles.css
@@ -0,0 +1,110 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/* ============ ROUND 2 ============== */
+
+td.albumsTitle {
+    font-family:'comic sans ms';
+    width: 100%;
+    font-size:24px;
+}
+
+td.albumListThumbnail {
+    width: 10%;
+    height: 75px;
+}
+
+td.albumListRight {
+    width: 90%;
+}
+
+td.actionLinks {
+    width:100%;
+    font-size:12px;
+}
+
+td.albumListTitle {
+    font-size:24px;
+    white-space:nowrap;
+    font-family:'comic sans ms';
+}
+
+td.albumListDescription {
+    font-family:'comic sans ms';
+}
+
+.albumsTable {
+    width:100%;
+    border-style:solid;
+    background-color:#b0c4de;
+}
+
+td.mediaItemThumbnail {
+    height:100%;
+}
+
+td.mediaItemBox {
+    width: 150px;
+    height: 100px;
+}
+
+.mediaItemControls {
+
+}
+
+
+/* ============ ROUND 1 ============== */
+.temp1 {background-color:#6495ed;}
+.temp2 {background-color:#e0ffff;}
+.temp3 {
+    background-color:#b0c4de;
+    background-image:url('img_flwr.png');
+    background-repeat:no-repeat;
+    background-position:top right;
+}
+
+td2 {
+    border-style:solid;
+}
+
+td.albumLeft {
+    width:10%;
+}
+
+td.albumRight {
+    width: 90%;
+}
+
+
+
+td.albumEdit {
+    width:100%;
+    text-align:right;
+    vertical-align:middle;
+    font-size:12px;
+}
+
+
+
+.albumTitleStyle {
+    color: blue;
+    
+}
+.albumElement {
+    background-color:#b0c4de;
+}
diff --git a/trunk/content/gadgets/oauth.xml b/trunk/content/gadgets/oauth.xml
new file mode 100644
index 0000000..c7fa09c
--- /dev/null
+++ b/trunk/content/gadgets/oauth.xml
@@ -0,0 +1,116 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Demo OAuth Gadget"
+               icon="http://localhost:8080/images/icon.png">
+    <OAuth>
+      <Service>
+        <Request url="http://localhost:9090/oauth-provider/request_token" />
+        <Access url="http://localhost:9090/oauth-provider/access_token" />
+        <Authorization url="http://localhost:9090/oauth-provider/authorize?oauth_callback=http://localhost:8080/gadgets/oauthcallback" />
+      </Service>
+    </OAuth>
+    <Require feature="oauthpopup" />
+    <Preload authz="oauth" href="http://localhost:9090/oauth-provider/echo" />
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <img src="http://localhost:8080/images/new.gif">
+      <a href="#" id="personalize">Personalize this gadget</a>
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <script type="text/javascript">
+      function $(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = $(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        url = "http://localhost:9090/oauth-provider/echo";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            $('personalize').onclick = popup.createOpenerOnClick();
+            $('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            $('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+            var whoops = document.createTextNode(
+                'OAuth error: ' + response.oauthError + ': ' +
+                response.oauthErrorText);
+            $('main').appendChild(whoops);
+            showOneSection('main');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/oauth2/oauth2_facebook.xml b/trunk/content/gadgets/oauth2/oauth2_facebook.xml
new file mode 100644
index 0000000..6a20565
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/oauth2_facebook.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Demo OAuth2 Authorization Code Gadget (Simple pull from Facebook Friends">
+    <OAuth2>
+      <Service name="facebook" scope="user_about_me">
+      </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+    <!-- <Preload authz="oauth2" oauth_service_name="facebook" href="https://graph.facebook.com/me/friends" 
+      /> -->
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Personalize this gadget</a>
+      <ol>
+        <b><u>In order to use this Demo Gadget you must</u></b> 
+        <li>Have or create a Facebook account and know your userid and password</li>
+        <li>Register a new application at <a href="https://developers.facebook.com/apps">https://developers.facebook.com/apps</a></li>
+        <li>Make sure your app's "Site URL" applies to your shindig environment (e.g. http://localhost:8080/gadgets/oauth2callback)</li>
+        <li>Update the Facebook client "App ID" and "App Secret" in the OAuth2 persistence (default is <code>config/oauth2.json</code>)</li>
+        <li>Restart the server</li>
+        <li>Click the link above to initiate the authorization process</li>
+      </ol>
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+    
+    <script type="text/javascript">
+      function getElement(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = getElement(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        url = "https://graph.facebook.com/me/friends";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "facebook";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            getElement('personalize').onclick = popup.createOpenerOnClick();
+            getElement('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            getElement('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+             getElement('error_code').appendChild(document.createTextNode(response.oauthError));
+             getElement('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             getElement('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             getElement('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             getElement('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+             showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/oauth2/oauth2_google.xml b/trunk/content/gadgets/oauth2/oauth2_google.xml
new file mode 100644
index 0000000..496c8a0
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/oauth2_google.xml
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Demo OAuth2 Authorization Code Gadget (Simple pull from Google Contacts)">
+    <OAuth2>
+      <Service name="googleAPI" scope="https://www.google.com/m8/feeds/">
+      </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+    <!-- <Preload authz="oauth2" oauth_service_name="googleAPI" href="https://www.google.com/m8/feeds/contacts/default/full" 
+      /> -->
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Personalize this gadget</a>
+      <ol>
+        <b><u>In order to use this Demo Gadget you must</u></b> 
+        <li>Have or create a Google account and know your userid and password</li>
+        <li>Register a new application at <a href="https://code.google.com/apis/console">https://code.google.com/apis/console</a></li>
+        <li>Make sure your app's "Redirect URIs" applies to your shindig environment (e.g. http://localhost:8080/gadgets/oauth2callback)</li>
+        <li>Update the Google client "Client ID" and "Client Secret" in the OAuth2 persistence (default is <code>config/oauth2.json</code>)</li>
+        <li>Restart the server</li>
+        <li>Click the link above to initiate the authorization process</li>
+      </ol>    
+     
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+    
+    <script type="text/javascript">
+      function getElement(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = getElement(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        url = "https://www.google.com/m8/feeds/contacts/default/full";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "googleAPI";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            getElement('personalize').onclick = popup.createOpenerOnClick();
+            getElement('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            getElement('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+             getElement('error_code').appendChild(document.createTextNode(response.oauthError));
+             getElement('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             getElement('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             getElement('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             getElement('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+            showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/oauth2/oauth2_google_shared1.xml b/trunk/content/gadgets/oauth2/oauth2_google_shared1.xml
new file mode 100644
index 0000000..15d38e9
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/oauth2_google_shared1.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Demo OAuth2 Authorization Code Gadget, uses sharedToken (1)">
+    <OAuth2>
+      <Service name="googleAPI" scope="https://www.google.com/m8/feeds/">
+      </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Personalize this gadget</a>
+      <ol>
+        <b><u>In order to use this Demo Gadget you must</u></b>
+        <li>Have or create a Google account and know your userid and password</li>
+        <li>Register a new application at <a href="https://code.google.com/apis/console">https://code.google.com/apis/console</a></li>
+        <li>Make sure your app's "Redirect URIs" applies to your shindig environment (e.g. http://localhost:8080/gadgets/oauth2callback)</li>
+        <li>Update the Google client "Client ID" and "Client Secret" in the OAuth2 persistence (default is <code>config/oauth2.json</code>)</li>
+        <li>Restart the server</li>
+        <li>Click the link above to initiate the authorization process</li>
+      </ol>
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+
+    <script type="text/javascript">
+      function getElement(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = getElement(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        url = "https://www.google.com/m8/feeds/contacts/default/full";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "googleAPI";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            getElement('personalize').onclick = popup.createOpenerOnClick();
+            getElement('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            getElement('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+             getElement('error_code').appendChild(document.createTextNode(response.oauthError));
+             getElement('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             getElement('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             getElement('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             getElement('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+            showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/oauth2/oauth2_google_shared2.xml b/trunk/content/gadgets/oauth2/oauth2_google_shared2.xml
new file mode 100644
index 0000000..7920fa3
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/oauth2_google_shared2.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Demo OAuth2 Authorization Code Gadget, uses sharedToken (2)">
+    <OAuth2>
+      <Service name="googleAPI" scope="https://www.google.com/m8/feeds/">
+      </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Personalize this gadget</a>
+      <ol>
+        <b><u>In order to use this Demo Gadget you must</u></b>
+        <li>Have or create a Google account and know your userid and password</li>
+        <li>Register a new application at <a href="https://code.google.com/apis/console">https://code.google.com/apis/console</a></li>
+        <li>Make sure your app's "Redirect URIs" applies to your shindig environment (e.g. http://localhost:8080/gadgets/oauth2callback)</li>
+        <li>Update the Google client "Client ID" and "Client Secret" in the OAuth2 persistence (default is <code>config/oauth2.json</code>)</li>
+        <li>Restart the server</li>
+        <li>Click the link above to initiate the authorization process</li>
+      </ol>
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+
+    <script type="text/javascript">
+      function getElement(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = getElement(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        url = "https://www.google.com/m8/feeds/contacts/default/full";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "googleAPI";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            getElement('personalize').onclick = popup.createOpenerOnClick();
+            getElement('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            getElement('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+             getElement('error_code').appendChild(document.createTextNode(response.oauthError));
+             getElement('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             getElement('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             getElement('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             getElement('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+            showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/oauth2/oauth2_spring_proxy.xml b/trunk/content/gadgets/oauth2/oauth2_spring_proxy.xml
new file mode 100644
index 0000000..40eacd2
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/oauth2_spring_proxy.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="OAuth2 Content Proxy Demo">
+    <OAuth2>
+      <Service name="springAPI" scope="read">
+      </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+        <div id="control">
+            Photos from SPARKLR site
+            <div id="photos"></div>
+        </div>
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Personalize this gadget</a>
+      <ol>
+        <b><u>In order to use this Demo Gadget you must</u></b>
+        <li>Get Sampe OAuth2 provider (SPARKLR) of Spring security on <a href="https://github.com/SpringSource/spring-security-oauth" target="_blank">https://github.com/SpringSource/spring-security-oauth</a>, follow the instructions to build the war package.</li>
+        <li>Deploy the war package to your tomcat server with context root as /sparklr2, you can find the war in samples/oauth2/sparklr/target if you build it by yourself.</li>
+        <li>Restart the server</li>
+        <li>Click the link above to initiate the authorization process</li>
+      </ol>
+
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+
+    <script type="text/javascript">
+      function getElement(x) {
+        return document.getElementById(x);
+      }
+
+      function addPhoto(parent, id) {
+        var requestParam = {};
+        requestParam[gadgets.io.RequestParameters.AUTHORIZATION]="OAUTH2";
+        requestParam[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME]="springAPI";
+        var proxyUrl = gadgets.io.getProxyUrl("http://localhost:8080/sparklr2/photos/" + id, requestParam);
+        var node = document.createElement("div");
+        node.innerHTML='<img src="' + proxyUrl + '"/>';
+        parent.appendChild(node);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = getElement(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        url = "http://localhost:8080/sparklr2/photos?format=xml";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.DOM;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "springAPI";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            getElement('personalize').onclick = popup.createOpenerOnClick();
+            getElement('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            //getElement('content').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+            var list = response.data.getElementsByTagName("photo");
+            for(var i=0; i<list.length; i++) {
+              addPhoto(document.getElementById("photos"), list[i].getAttribute("id"));
+            }
+          } else {
+             getElement('error_code').appendChild(document.createTextNode(response.oauthError));
+             getElement('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             getElement('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             getElement('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             getElement('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+            showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/oauth2/oauth2_windowslive.xml b/trunk/content/gadgets/oauth2/oauth2_windowslive.xml
new file mode 100644
index 0000000..05b5454
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/oauth2_windowslive.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Demo OAuth2 Authorization Code Gadget (Simple pull from Windows Live Contacts)">
+    <OAuth2>
+      <Service name="windows_live" scope="wl.signin wl.basic">
+      </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+    <!-- <Preload authz="oauth2" oauth_service_name="windows_live" href="https://apis.live.net/v5.0/me/contacts" 
+      /> -->
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Personalize this gadget</a>
+      <ol>
+        <b><u>In order to use this Demo Gadget you must</u></b> 
+        <li>Have or create a Windows Live account and know your userid and password</li>
+        <li>Register a new application at <a href="https://manage.dev.live.com">https://manage.dev.live.com</a></li>
+        <li>Make sure your app's "Redirect domain" applies to your shindig environment (e.g. http://localhost:8080/gadgets/oauth2callback)</li>
+        <li>Update the Windows Live client "Client ID" and "Client secret" in the OAuth2 persistence (default is <code>config/oauth2.json</code>)</li>
+        <li>Restart the server</li>
+        <li>Click the link above to initiate the authorization process</li>
+      </ol>    
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+    
+    <script type="text/javascript">
+      function getElement(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = getElement(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        url = "https://apis.live.net/v5.0/me/contacts";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "windows_live";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            getElement('personalize').onclick = popup.createOpenerOnClick();
+            getElement('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            getElement('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+             getElement('error_code').appendChild(document.createTextNode(response.oauthError));
+             getElement('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             getElement('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             getElement('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             getElement('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+            showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/oauth2/shindig_authorization.xml b/trunk/content/gadgets/oauth2/shindig_authorization.xml
new file mode 100644
index 0000000..5bdc21c
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/shindig_authorization.xml
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+
+<!-- requires the patch for https://reviews.apache.org/r/1940/ -->
+
+<Module>
+  <ModulePrefs title="OAuth2 Demo Gadget -- Authorization Code">
+    <OAuth2>
+      <Service name="shindigOAuth2Provider"  >
+     </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+    
+    <div id="overview" style="display: visible">
+    This demo gadget would simply pull open social friends for user - john.doe.
+    </div>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Please log in to authorize the user request to get open social friends</a>
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+    
+    <script type="text/javascript">
+      function $(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = $(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        var gadgetUrl = gadgets.util.getUrlParameters().url;
+	    var url = gadgetUrl.substr(0,gadgetUrl.indexOf("/gadgets/oauth2/shindig_authorization.xml")) + "/social/rest/people/john.doe/@friends/";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "shindigOAuth2Provider";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            $('personalize').onclick = popup.createOpenerOnClick();
+            $('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            $('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+             $('error_code').appendChild(document.createTextNode(response.oauthError));
+             $('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             $('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             $('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             $('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+            showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/oauth2/shindig_client_credentials.xml b/trunk/content/gadgets/oauth2/shindig_client_credentials.xml
new file mode 100644
index 0000000..fa9e9e9
--- /dev/null
+++ b/trunk/content/gadgets/oauth2/shindig_client_credentials.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+
+<!-- requires the patch for https://reviews.apache.org/r/1940/ -->
+
+<Module>
+  <ModulePrefs title="OAuth2 Demo Gadget -- Client Credentials">
+    <OAuth2>
+      <Service name="shindigOAuth2Provider"  >
+     </Service>
+    </OAuth2>
+    <Require feature="oauthpopup" />
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+    
+    <div id="overview" style="display: visible">
+    This demo gadget would simply pull open social friends for user -- john.doe.
+    </div>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <a href="#" id="personalize">Please log in to authorize the user request to get open social friends</a>
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <div id="error" style="display: none;background-color:yellow;font-size:xx-small;" title="An error occured processing your request">
+       <div id="error_code"><u>code:</u></div>
+       <div id="error_uri"><u>uri:</u></div>
+       <div id="error_description"><u>description:</u></div>
+       <div id="error_explanation"><u>explanation:</u></div>
+       <div id="error_trace"><u>trace:</u></div>
+    </div>
+    
+    <script type="text/javascript">
+      function $(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting', 'error' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = $(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        var gadgetUrl = gadgets.util.getUrlParameters().url;
+	    var url = gadgetUrl.substr(0,gadgetUrl.indexOf("/gadgets/oauth2/")) + "/social/rest/people/john.doe/@friends/";
+        
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH2;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "shindigOAuth2Provider";
+        params[gadgets.io.RequestParameters.REFRESH_INTERVAL] = "0";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            $('personalize').onclick = popup.createOpenerOnClick();
+            $('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            $('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+             $('error_code').appendChild(document.createTextNode(response.oauthError));
+             $('error_uri').appendChild(document.createTextNode(response.oauthErrorUri));
+             $('error_description').appendChild(document.createTextNode(response.oauthErrorText));
+             $('error_explanation').appendChild(document.createTextNode(response.oauthErrorExplanation));
+             $('error_trace').appendChild(document.createTextNode(response.oauthErrorTrace));
+            showOneSection('error');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/rewriter/feather.png b/trunk/content/gadgets/rewriter/feather.png
new file mode 100644
index 0000000..6569333
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/feather.png
Binary files differ
diff --git a/trunk/content/gadgets/rewriter/rewriter1.css b/trunk/content/gadgets/rewriter/rewriter1.css
new file mode 100644
index 0000000..794a7e0
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/rewriter1.css
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+#backgrdiv {
+  background-image: url("feather.png");
+  background-repeat: no-repeat;
+  background-position: 20%;
+  border: 2px red solid;
+}
diff --git a/trunk/content/gadgets/rewriter/rewriter1.js b/trunk/content/gadgets/rewriter/rewriter1.js
new file mode 100644
index 0000000..3fd90cf
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/rewriter1.js
@@ -0,0 +1,19 @@
+<!--
+ * Licensed to the Apache Software Foundation(ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+-->
+document.getElementById('jstarget1').innerHTML = 'This content was loaded from rewriter1.js';
diff --git a/trunk/content/gadgets/rewriter/rewriter2.css b/trunk/content/gadgets/rewriter/rewriter2.css
new file mode 100644
index 0000000..890cb4b
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/rewriter2.css
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+#backgrdiv2 {
+  background-image: url("feather.png");
+  background-repeat: no-repeat;
+  background-position: 20%;
+  border: 2px blue solid;
+}
diff --git a/trunk/content/gadgets/rewriter/rewriter2.js b/trunk/content/gadgets/rewriter/rewriter2.js
new file mode 100644
index 0000000..70cf7c0
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/rewriter2.js
@@ -0,0 +1,19 @@
+<!--
+ * Licensed to the Apache Software Foundation(ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+-->
+document.getElementById('jstarget2').innerHTML = 'This content was loaded from rewriter2.js';
diff --git a/trunk/content/gadgets/rewriter/rewriteroff.xml b/trunk/content/gadgets/rewriter/rewriteroff.xml
new file mode 100644
index 0000000..4ce53d8
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/rewriteroff.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+<ModulePrefs title="Rewriter demo"
+             height="250">
+<Optional feature="content-rewrite">
+  <Param name="exclude-urls">.*</Param>
+</Optional>
+</ModulePrefs>
+<Content type="html">
+<![CDATA[
+<style type="text/css"> @import url( http://localhost:8080/gadgets/rewriter/rewriter1.css ); </style>
+<link rel="stylesheet" type="text/css" href="http://localhost:8080/gadgets/rewriter/rewriter2.css"/>
+<p>Demostrates what happens when the rewriter is off</p>
+<div>
+  This is a URL in content that was not rewritten http://www.notrewritten.com
+</div>
+<div id="backgrdiv">
+  This div has a background <br/> image from imported CSS
+</div>
+<div id="backgrdiv2">
+  This div has a background <br/> image from linked CSS
+</div>
+<img id="rewriteimg" src="feather.png" alt="You can read this because without rewrite paths are not resolved relative to the gadget spec on render"/>
+<p id="jstarget1">If you can read this there is a problem</p>
+<p id="jstarget2">If you can read this there is a problem</p>
+<script type="text/javascript" src="http://localhost:8080/gadgets/rewriter/rewriter1.js"></script>
+<script type="text/javascript" src="http://localhost:8080/gadgets/rewriter/rewriter2.js"></script>
+<p>Rendering this gadget will have loaded the resources directly.
+Without the rewriter caching headers have not been set and there are
+more requests because javascript files have not been concatenated</p>
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/gadgets/rewriter/rewriteron.html b/trunk/content/gadgets/rewriter/rewriteron.html
new file mode 100644
index 0000000..32a4c3e
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/rewriteron.html
@@ -0,0 +1,35 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<style type="text/css"> @import url( rewriter1.css ); </style>
+<link rel="stylesheet" type="text/css" href="rewriter2.css"/>
+<p>A simple gadget to demonstrate the content rewriter</p>
+<div>
+  This is a URL in content that was not rewritten http://www.notrewritten.com
+</div>
+<div id="backgrdiv">
+  This div has a background <br/> image from imported CSS
+</div>
+<div id="backgrdiv2">
+  This div has a background <br/> image from linked CSS
+</div>
+<p> This <img id="rewriteimg" src="feather.png" alt="If you can read this there is a problem"/> is an image tag that was rewritten</p>
+<p id="jstarget1">If you can read this there is a problem</p>
+<p id="jstarget2">If you can read this there is a problem</p>
+<script type="text/javascript" src="rewriter1.js"></script>
+<script type="text/javascript" src="rewriter2.js"></script>
diff --git a/trunk/content/gadgets/rewriter/rewriteron.xml b/trunk/content/gadgets/rewriter/rewriteron.xml
new file mode 100644
index 0000000..6d28dfa
--- /dev/null
+++ b/trunk/content/gadgets/rewriter/rewriteron.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+<ModulePrefs title="Rewriter demo"
+             height="250">
+<Require feature="core"/>
+<Require feature="core.io"/>
+<Optional feature="content-rewrite">
+  <Param name="include-urls">.*</Param>
+</Optional>
+</ModulePrefs>
+<Content type="html">
+<![CDATA[
+<style type="text/css"> @import url( rewriter1.css ); </style>
+<link rel="stylesheet" type="text/css" href="rewriter2.css"/>
+<p>A simple gadget to demonstrate the content rewriter</p>
+<div>
+  This is a URL in content that was not rewritten http://www.notrewritten.com
+</div>
+<div id="backgrdiv">
+  This div has a background <br/> image from imported CSS
+</div>
+<div id="backgrdiv2">
+  This div has a background <br/> image from linked CSS
+</div>
+<p> This <img id="rewriteimg" src="feather.png" alt="If you can read this there is a problem"/> is an image tag that was rewritten</p>
+<p id="jstarget1">If you can read this there is a problem</p>
+<p id="jstarget2">If you can read this there is a problem</p>
+<script type="text/javascript" src="rewriter1.js"></script>
+<script type="text/javascript" src="rewriter2.js"></script>
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/gadgets/sample-pubsub-2-publisher.xml b/trunk/content/gadgets/sample-pubsub-2-publisher.xml
new file mode 100644
index 0000000..aa23920
--- /dev/null
+++ b/trunk/content/gadgets/sample-pubsub-2-publisher.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+<ModulePrefs title="Sample PubSub Publisher"
+             height="250">
+<Require feature="pubsub-2">
+  <Param name="topics">
+    <![CDATA[ 
+    <Topic title="Random Number" name="org.apache.shindig.random-number"
+            description="Publishes a random number." type="number"
+            publish="true">
+    </Topic>
+    ]]>
+  </Param>
+</Require>
+</ModulePrefs>
+<Content type="html">
+<![CDATA[
+<script>
+function publish() {
+  var message = Math.random();
+  gadgets.Hub.publish("org.apache.shindig.random-number", message);
+  document.getElementById("output").innerHTML = message;
+}
+
+</script>
+<div>
+<input type="button" value="Publish a random number" onclick="publish()"/>
+</div>
+<div id="output">
+</div>
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/gadgets/sample-pubsub-2-subscriber.xml b/trunk/content/gadgets/sample-pubsub-2-subscriber.xml
new file mode 100644
index 0000000..9069025
--- /dev/null
+++ b/trunk/content/gadgets/sample-pubsub-2-subscriber.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+<ModulePrefs title="Sample PubSub Subscriber"
+             height="250">
+<Require feature="pubsub-2">
+  <Param name="topics">
+    <![CDATA[ 
+    <Topic title="Random Number" name="org.apache.shindig.random-number"
+            description="Subscribes to random number generator." type="number"
+            subscribe="true"/>
+    ]]>
+  </Param>
+</Require>
+</ModulePrefs>
+<Content type="html">
+<![CDATA[
+<script>
+var subId;
+
+// Example of setting a parameter to the HubClient used by pubsub-2 feature.
+gadgets.HubSettings.params.HubClient.onSecurityAlert = function(alertSource, alertType) {
+  alert("SECURITY ERROR!");
+  window.location.href = "about:blank";
+};
+
+function callback(topic, data, subscriberData) {
+  document.getElementById("output").innerHTML =
+    "message : " + gadgets.util.escapeString(data + "") + "<br/>" +
+    "received at: " + (new Date()).toString();
+}
+
+function subscribe() {
+  subId = gadgets.Hub.subscribe("org.apache.shindig.random-number", callback);
+}
+
+function unsubscribe() {
+  gadgets.Hub.unsubscribe(subId);
+  document.getElementById("output").innerHTML = "";
+}
+
+</script>
+<div>
+<input type="button" value="Subscribe" onclick="subscribe()"/>
+<input type="button" value="Unsubscribe" onclick="unsubscribe()"/>
+</div>
+<div id="output">
+</div>
+]]>
+</Content>
+</Module>
diff --git a/trunk/content/gadgets/shindigoauth.xml b/trunk/content/gadgets/shindigoauth.xml
new file mode 100644
index 0000000..753be5d
--- /dev/null
+++ b/trunk/content/gadgets/shindigoauth.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Demo 3-legged OAuth to Shindig"
+               icon="http://localhost:8080/images/icon.png">
+    <OAuth>
+      <Service name="shindig">
+        <Request url="http://localhost:8080/oauth/requestToken" />
+        <Authorization url="http://localhost:8080/oauth/authorize?oauth_callback=http://localhost:8080/gadgets/oauthcallback" />
+        <Access url="http://localhost:8080/oauth/accessToken" />
+      </Service>
+    </OAuth>
+    <Require feature="oauthpopup" />
+  </ModulePrefs>
+  <Content type="html">
+      <![CDATA[
+
+    <style>
+    #main {
+        margin: 0px;
+        padding: 0px;
+        font-size: small;
+    }
+    </style>
+
+    <div id="main" style="display: none">
+    </div>
+
+    <div id="approval" style="display: none">
+      <img src="http://localhost:8080/images/new.gif">
+      <a href="#" id="personalize">Personalize this gadget</a>
+    </div>
+
+    <div id="waiting" style="display: none">
+      Please click
+      <a href="#" id="approvaldone">I've approved access</a>
+      once you've approved access to your data.
+    </div>
+
+    <script type="text/javascript">
+      function $(x) {
+        return document.getElementById(x);
+      }
+
+      function showOneSection(toshow) {
+        var sections = [ 'main', 'approval', 'waiting' ];
+        for (var i=0; i < sections.length; ++i) {
+          var s = sections[i];
+          var el = $(s);
+          if (s === toshow) {
+            el.style.display = "block";
+          } else {
+            el.style.display = "none";
+          }
+        }
+      }
+
+      function fetchData() {
+        var url = "http://localhost:8080/social/rest/people/@me/@self";
+        var params = {};
+        params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+          gadgets.io.ContentType.TEXT;
+        params[gadgets.io.RequestParameters.AUTHORIZATION] =
+          gadgets.io.AuthorizationType.OAUTH;
+        params[gadgets.io.RequestParameters.METHOD] =
+          gadgets.io.MethodType.GET;
+        params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] =
+          "shindig";
+
+        gadgets.io.makeRequest(url, function (response) {
+          if (response.oauthApprovalUrl) {
+            var onOpen = function() {
+              showOneSection('waiting');
+            };
+            var onClose = function() {
+              fetchData();
+            };
+            var popup = new gadgets.oauth.Popup(response.oauthApprovalUrl,
+                null, onOpen, onClose);
+            $('personalize').onclick = popup.createOpenerOnClick();
+            $('approvaldone').onclick = popup.createApprovedOnClick();
+            showOneSection('approval');
+          } else if (response.data) {
+            $('main').appendChild(document.createTextNode(response.data));
+            showOneSection('main');
+          } else {
+            var whoops = document.createTextNode(
+                'OAuth error: ' + response.oauthError + ': ' +
+                response.oauthErrorText);
+            $('main').appendChild(whoops);
+            showOneSection('main');
+          }
+        }, params);
+      }
+
+      gadgets.util.registerOnLoadHandler(fetchData);
+    </script>
+        ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/templates/FlashTag.xml b/trunk/content/gadgets/templates/FlashTag.xml
new file mode 100644
index 0000000..2583fca
--- /dev/null
+++ b/trunk/content/gadgets/templates/FlashTag.xml
@@ -0,0 +1,52 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="FlashTagTest">
+    <Require feature="opensocial-data"/>
+    <Require feature="opensocial-templates">
+      <Param name="process-on-server">true</Param>
+    </Require>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+
+    <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
+      <!--  Load the canonical user -->
+      <os:PeopleRequest key="me" userId="canonical"/>
+    </script>
+
+    <div>
+      <p>Simple test of flash, which will only load and play when the nested content is clicked</p>
+      <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-template">
+        <os:xFlash swf="http://www.adobe.com/shockwave/welcome/flash.swf" width="500px" height="500px" bgcolor="#123456" menu="false" play="onclick" flashvars="${os:xUrlEncode(me.id)}=${os:xUrlEncode(me.name.familyName)}">
+          <b><p>alternate content. Click me!</p></b>
+        </os:Flash>
+      </script>
+    </div>
+    <div>
+      <p>Flash which has internal network access, no script access, liveconnect</p>
+      <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-template">
+        <os:xFlash swf="http://swfobject.googlecode.com/svn/trunk/swfobject/test.swf" width="500px" height="500px" swliveconnect="false" allowscriptaccess="never" allownetworking="internal" play="immediate">
+          <p>alternate content</p>
+        </os:Flash>
+      </script>
+    </div>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/templates/RelativeTemplateLibrary.xml b/trunk/content/gadgets/templates/RelativeTemplateLibrary.xml
new file mode 100644
index 0000000..12f2034
--- /dev/null
+++ b/trunk/content/gadgets/templates/RelativeTemplateLibrary.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="RelativeTemplateLibrary">
+    <Require feature="opensocial-templates">
+	  <Param name="requireLibrary">TestTemplateLibrary.xml</Param>
+	</Require>  
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+    <div>
+      <p>Simple test of gadget using template library with relative path</p>
+    </div>
+    <script type="text/os-template" xmlns:foo="http://foo.com/">
+      <foo:HelloWorld />
+    </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/content/gadgets/templates/TemplateClientAPI.xml b/trunk/content/gadgets/templates/TemplateClientAPI.xml
new file mode 100644
index 0000000..ffe4743
--- /dev/null
+++ b/trunk/content/gadgets/templates/TemplateClientAPI.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+    <ModulePrefs title="TemplateClientAPI">
+        <Require feature="opensocial-templates">
+            <!-- This is required to be present to get access to the JavaScript -->
+            <Param name="client">true</Param>
+            <Param name="requireLibrary">TestTemplateLibrary.xml</Param>
+        </Require>
+        <Optional feature="content-rewrite">
+            <Param name="include-tags"></Param>
+        </Optional>
+    </ModulePrefs>
+    <Content type="html" view="home,profile">
+        <![CDATA[
+        <script type="text/os-template" xmlns:foomod="http://foo.com/module" tag="foomod:HelloWorldModule">
+            <div>Hello World Module</div>
+        </script>
+
+        <div>
+          <p>Simple test of gadget using template library with client API access</p>
+        </div>
+        <script type="text/os-template" xmlns:foo="http://foo.com/" xmlns:foomod="http://foo.com/module">
+          <p>
+            <div>Statically Defined from Module...</div><foomod:HelloWorldModule />
+            <div>Statically Defined from Template...</div><foo:HelloWorld />
+          </p>
+        </script><br/>
+
+        <div>Client Added from Module ...</div>
+        <div><div id='addheremod'/></div><br/>
+
+        <div>Client Added from Template ...</div>
+        <div><div id='addheretemplate'/></div><br/>
+
+        <script type="text/javascript">
+            function handleLoad() {
+                var template = os.getTemplate("foomod:HelloWorldModule");
+                if (template != undefined) {
+                    var placeholder = document.getElementById("addheremod");
+                    template.renderInto(placeholder);
+                } else {
+                    alert('Error: Inline Not Found (Module Defined) - This fails due to OnLoadHandler timing. '
+                        + 'This will succeed if you comment out the template library');
+                }
+
+                template = os.getTemplate("foo:HelloWorld");
+                if (template != undefined) {
+                    var placeholder = document.getElementById("addheretemplate");
+                    template.renderInto(placeholder);
+                } else {
+                    alert('Error: Inline Not Found (Template Defined)');
+                }
+            }
+            gadgets.util.registerOnLoadHandler(handleLoad);
+		</script>
+        ]]>
+    </Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/templates/TestTemplateLibrary.xml b/trunk/content/gadgets/templates/TestTemplateLibrary.xml
new file mode 100644
index 0000000..f0831c1
--- /dev/null
+++ b/trunk/content/gadgets/templates/TestTemplateLibrary.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!--

+ * Licensed to the Apache Software Foundation (ASF) under one

+ * or more contributor license agreements.  See the NOTICE file

+ * distributed with this work for additional information

+ * regarding copyright ownership.  The ASF licenses this file

+ * to you 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.

+-->

+<Templates xmlns:foo="http://foo.com/">

+  <Namespace prefix="foo" url="http://foo.com/"/>

+  <Template tag="foo:HelloWorld">

+    <div>Hello World from template library with relative url!</div>

+  </Template>

+</Templates>
\ No newline at end of file
diff --git a/trunk/content/gadgets/templates/VarTag.xml b/trunk/content/gadgets/templates/VarTag.xml
new file mode 100644
index 0000000..82df051
--- /dev/null
+++ b/trunk/content/gadgets/templates/VarTag.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+	<ModulePrefs title="Varible Replacement Templating Example"
+		height="400" width="400">
+		<Require feature="opensocial-templates">
+		</Require>
+	</ModulePrefs>
+	<Content view="default" type="html">
+	<![CDATA[
+		<div>
+			<script type="text/os-template" xmlns:os="http://ns.opensocial.org/2008/markup" >
+				<os:Var key="myvar" value="3"></os:Var>
+				This value of myvar is ${myvar}
+			</script>
+		</div>
+	]]>
+	</Content>
+</Module>
\ No newline at end of file
diff --git a/trunk/content/gadgets/url-gadget-with-features/url.html b/trunk/content/gadgets/url-gadget-with-features/url.html
new file mode 100644
index 0000000..c29a6d8
--- /dev/null
+++ b/trunk/content/gadgets/url-gadget-with-features/url.html
@@ -0,0 +1,46 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+  <head>
+    <title>Url Gadget Feature Test</title>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
+    <script type="text/javascript">
+      function init() {
+        var match = /[?&]libs=([^&]+)/.exec(window.location.search);
+        if (match[1]) {
+          var url = decodeURIComponent(match[1])/*.replace(/[.]js$/,'?debug=1.js')*/;
+
+          $.getScript(url).done(function(script, status) {
+              gadgets.util.registerOnLoadHandler(function() {
+                gadgets.views.openUrl('http://docs.opensocial.org', function() {
+                    document.getElementById('message').innerHTML = 'navigated';
+                }, 'DIALOG');
+              });
+
+              gadgets.util.runOnLoadHandlers();
+          });
+        }
+     }
+    </script>
+  </head>
+  <body onload="init()">
+    <div>Hi!</div>
+    <div id="message"></div>
+  </body>
+</html>
\ No newline at end of file
diff --git a/trunk/content/gadgets/url-gadget-with-features/url.xml b/trunk/content/gadgets/url-gadget-with-features/url.xml
new file mode 100644
index 0000000..89ab9fd
--- /dev/null
+++ b/trunk/content/gadgets/url-gadget-with-features/url.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Url Feature Loading Gadget">
+    <Require feature="open-views"/>
+  </ModulePrefs>
+  <Content type="url" href="/url.html" />
+</Module>
\ No newline at end of file
diff --git a/trunk/content/images/bubble.gif b/trunk/content/images/bubble.gif
new file mode 100644
index 0000000..9b81aec
--- /dev/null
+++ b/trunk/content/images/bubble.gif
Binary files differ
diff --git a/trunk/content/images/icon.png b/trunk/content/images/icon.png
new file mode 100644
index 0000000..ca0990c
--- /dev/null
+++ b/trunk/content/images/icon.png
Binary files differ
diff --git a/trunk/content/images/new.gif b/trunk/content/images/new.gif
new file mode 100644
index 0000000..a9055cc
--- /dev/null
+++ b/trunk/content/images/new.gif
Binary files differ
diff --git a/trunk/content/images/nophoto.gif b/trunk/content/images/nophoto.gif
new file mode 100644
index 0000000..01be56f
--- /dev/null
+++ b/trunk/content/images/nophoto.gif
Binary files differ
diff --git a/trunk/content/sampledata/canonicaldb.json b/trunk/content/sampledata/canonicaldb.json
new file mode 100644
index 0000000..71ef078
--- /dev/null
+++ b/trunk/content/sampledata/canonicaldb.json
@@ -0,0 +1,861 @@
+//
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you 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.
+//
+
+//  WARNING! Changing this file will affect unit-tests
+//  A canonical JSON backed DB of OpenSocial datastructures in their RESTful forms
+//
+//  Basic structure:
+//  { people : Array<Person>,
+//    activity : Map<Person.Id, Array<Activity>>
+//    data : Map<Person.Id, Map<String,String>>
+//    friendLinks : Map<Person.Id, Array<Person.Id>>
+//    userApplications : Map<Person.Id, Array<Application.Id>>
+//    messages : Map<Person.Id, Map<MessageCollection.Id, MessageCollection>>
+//  }
+//
+//  Notes:
+//   - The structure of Person, Activity MUST! match those in the RESTful spec
+//   - Data for "canonical" user should exercise every field in the spec. And is relied on
+//     for unit-testing so change at your peril
+//   - Consider adding a structure for Map<Person.Id, Array<appId>> to represent installed gadgets
+//
+//  TODO:
+//   - Use URLs for appIds
+//
+//
+{
+"people" : [
+{
+"id" : "canonical",
+"aboutMe" : "I have an example of every piece of data",
+"activities" : ["Coding Shindig"],
+"addresses" : [{
+"country" : "US",
+"latitude" : "28.3043",
+"longitude" : "143.0859",
+"locality" : "who knows",
+"postalCode" : "12345",
+"region" : "Apache, CA",
+"streetAddress" : "1 OpenStandards Way",
+"type" : "home",
+"formatted" : "PoBox 3565, 1 OpenStandards Way, Apache, CA"
+}],
+"age" : 33,
+"bodyType" : {
+"build" : "svelte",
+"eyeColor" : "blue",
+"hairColor" : "black",
+"height" : 1.84,
+"weight" : 74
+},
+"books" : ["The Cathedral & the Bazaar","Catch 22"],
+"cars" : ["beetle","prius"],
+"children" : "3",
+"currentLocation" : {
+"latitude" : "48.858193",
+"longitude" : "2.29419"
+},
+"birthday" : "1975-01-01",
+"displayName" : "Shin Digg",
+"drinker" : {
+"value" : "SOCIALLY",
+"displayValue" : "Socially"
+},
+"emails" : [{
+"value" : "dev@shindig.apache.org",
+"type" : "work"
+}],
+"ethnicity" : "developer",
+"fashion" : "t-shirts",
+"food" : ["sushi","burgers"],
+"gender" : "male",
+"happiestWhen" : "coding",
+"hasApp" : true,
+"heroes" : ["Doug Crockford", "Charles Babbage"],
+"humor" : "none to speak of",
+"interests" : ["PHP","Java"],
+"jobInterests" : "will work for beer",
+"organizations" : [{
+"address" : {
+"formatted" : "1 Shindig Drive"
+},
+"description" : "lots of coding",
+"endDate" : "2010-10-10",
+"field" : "Software Engineering",
+"name" : "Apache.com",
+"salary" : "$1000000000",
+"startDate" : "1995-01-01",
+"subField" : "Development",
+"title" : "Grand PooBah",
+"webpage" : "http://shindig.apache.org/",
+"type" : "job"
+},{
+"address" : {
+"formatted" : "1 Skid Row"
+},
+"description" : "",
+"endDate" : "1995-01-01",
+"field" : "College",
+"name" : "School of hard Knocks",
+"salary" : "$100",
+"startDate" : "1991-01-01",
+"subField" : "Lab Tech",
+"title" : "Gopher",
+"webpage" : "",
+"type" : "job"
+}],
+"languagesSpoken" : ["English","Dutch","Esperanto"],
+"updated" : "2006-06-06T12:12:12Z",
+"livingArrangement" : "in a house",
+"lookingFor" : [
+  {"value" : "RANDOM", "displayValue" : "Random"},
+  {"value" : "NETWORKING", "displayValue" : "Networking"}
+],
+"movies" : ["Iron Man", "Nosferatu"],
+"music" : ["Chieftains","Beck"],
+"name" : {
+"additionalName" : "H",
+"familyName" : "Digg",
+"givenName" : "Shin",
+"honorificPrefix" : "Sir",
+"honorificSuffix" : "Social Butterfly",
+"formatted" : "Sir Shin H. Digg Social Butterfly"
+},
+"networkPresence" : {
+"value" : "ONLINE",
+"displayValue" : "Online"
+},
+"nickname" : "diggy",
+"pets" : "dog,cat",
+"phoneNumbers" : [{
+"value" : "111-111-111",
+"type" : "work"
+},{
+"value" : "999-999-999",
+"type" : "mobile"
+}],
+"politicalViews" : "open leaning",
+"profileSong" : {
+"value" : "http://www.example.org/songs/OnlyTheLonely.mp3",
+"linkText" : "Feelin' blue",
+"type" : "road"
+},
+"profileUrl" : "http://www.example.org/?id=1",
+"profileVideo" : {
+"value" : "http://www.example.org/videos/Thriller.flv",
+"linkText" : "Thriller",
+"type" : "video"
+},
+"quotes" : ["I am therfore I code", "Doh!"],
+"relationshipStatus" : "married to my job",
+"religion" : "druidic",
+"romance" : "twice a year",
+"scaredOf" : "COBOL",
+"sexualOrientation" : "north",
+"smoker" : {
+"value" : "NO",
+"displayValue" : "No"
+},
+"sports" : ["frisbee","rugby"],
+"status" : "happy",
+"tags" : ["C#","JSON","template"],
+"thumbnailUrl" : "/images/nophoto.gif",
+"utcOffset" : "-8",
+"turnOffs" : ["lack of unit tests","cabbage"],
+"turnOns" : ["well document code"],
+"tvShows" : ["House","Battlestar Galactica"],
+"urls" : [{
+"value" : "http://www.example.org/?id=1",
+"linkText" : "my profile",
+"type" : "Profile"
+},{
+"value" : "/images/nophoto.gif",
+"linkText" : "my awesome picture",
+"type" : "Thumbnail"
+}]
+},
+{
+"id" : "john.doe",
+"displayName" : "Johnny",
+"gender" : "male",
+"hasApp" : true,
+"name" : {
+"familyName" : "Doe",
+"givenName" : "John",
+"formatted" : "John Doe"
+}
+},
+{
+"id" : "jane.doe",
+"displayName" : "Janey",
+"gender" : "female",
+"hasApp" : true,
+"name" : {
+"familyName" : "Doe",
+"givenName" : "Jane",
+"formatted" : "Jane Doe"
+}
+},
+{
+"id" : "george.doe",
+"displayName" : "Georgey",
+"gender" : "male",
+"hasApp" : true,
+"name" : {
+"familyName" : "Doe",
+"givenName" : "George",
+"formatted" : "George Doe"
+}
+},
+{
+"id" : "mario.rossi",
+"displayName" : "Mario",
+"gender" : "male",
+"hasApp" : true,
+"name" : {
+"familyName" : "Rossi",
+"givenName" : "Mario",
+"formatted" : "Mario Rossi"
+}
+},
+{
+"id" : "maija.m",
+"displayName" : "Maija",
+"gender" : "female",
+"hasApp" : true,
+"name" : {
+"familyName" : "Meikäläinen",
+"givenName" : "Maija",
+"formatted" : "Maija Meikäläinen"
+}
+}],
+//
+// ----------------------------- Data ---------------------------------------
+//
+"data" : {
+"canonical" : {
+"count" : "2",
+"size" : "100"
+},
+"john.doe" : {
+"count" : "0"
+},
+"george.doe" : {
+"count" : "2"
+},
+"jane.doe" : {
+"count" : "7"
+}
+},
+"activities" : {
+"canonical" : [{
+"appId" : "1",
+"body" : "Went rafting",
+"bodyId" : "1",
+"externalId" : "http://www.example.org/123456",
+"id" : "1",
+"updated" : "2008-06-06T12:12:12Z",
+"mediaItems" : [{
+"mimeType" : "image/*",
+"type" : "image",
+"url" : "http://upload.wikimedia.org/wikipedia/commons/thumb/7/77/Rafting_em_Brotas.jpg/800px-Rafting_em_Brotas.jpg"
+},{
+"mimeType" : "audio/mpeg",
+"type" : "audio",
+"url" : "http://www.archive.org/download/testmp3testfile/mpthreetest.mp3"
+}],
+"postedTime" : "1111111111",
+"priority" : "0.7",
+"streamFaviconUrl" : "http://upload.wikimedia.org/wikipedia/commons/0/02/Nuvola_apps_edu_languages.gif",
+"streamSourceUrl" : "http://www.example.org/canonical/streamsource",
+"streamTitle" : "All my activities",
+"streamUrl" : "http://www.example.org/canonical/activities",
+"templateParams" : {
+"small" : "true",
+"otherContent" : "and got wet"
+},
+"title" : "My trip",
+"titleId" : "1",
+"url" : "http://www.example.org/canonical/activities/1",
+"userId" : "canonical"
+},{
+"appId" : "1",
+"body" : "Went skiing",
+"bodyId" : "1",
+"externalId" : "http://www.example.org/123457",
+"id" : "1",
+"updated" : "2008-06-10T12:12:12Z",
+"postedTime" : "1111111112",
+"priority" : "0.7",
+"streamFaviconUrl" : "http://upload.wikimedia.org/wikipedia/commons/0/02/Nuvola_apps_edu_languages.gif",
+"streamSourceUrl" : "http://www.example.org/canonical/streamsource",
+"streamTitle" : "All my activities",
+"streamUrl" : "http://www.example.org/canonical/activities",
+"templateParams" : {
+"small" : "true",
+"otherContent" : "and went fast"
+},
+"title" : "My next trip",
+"titleId" : "1",
+"url" : "http://www.example.org/canonical/activities/2",
+"userId" : "canonical"
+}],
+"john.doe" : [{
+"id" : "1",
+"userId" : "john.doe",
+"title" : "yellow",
+"body" : "what a color!"
+}],
+"jane.doe" : [{
+"id" : "1",
+"body" : "and she thinks you look like him",
+"mediaItems" : [{
+"mimeType" : "image/jpeg",
+"type" : "image",
+"url" : "http://animals.nationalgeographic.com/staticfiles/NGS/Shared/StaticFiles/animals/images/primary/black-spider-monkey.jpg"
+},{
+"mimeType" : "image/jpeg",
+"type" : "image",
+"url" : "http://image.guardian.co.uk/sys-images/Guardian/Pix/gallery/2002/01/03/monkey300.jpg"
+}],
+"streamTitle" : "jane's photos",
+"title" : "Jane just posted a photo of a monkey",
+"userId" : "jane.doe"
+},{
+"id" : "2",
+"body" : "or is it you?",
+"mediaItems" : [{
+"mimeType" : "image/jpeg",
+"type" : "image",
+"url" : "http://www.funnyphotos.net.au/images/fancy-dress-dog-yoda-from-star-wars1.jpg"
+}],
+"streamTitle" : "jane's photos",
+"title" : "Jane says George likes yoda!",
+"userId" : "jane.doe"
+}]
+},
+"activityEntries" : {
+	"john.doe": [{
+      "id": "activity1",
+      "title": "John shared new photos with you",
+      "published": "2011-02-10T15:04:55Z",
+      "actor": {
+        "url": "http://example.org/john",
+        "objectType" : "person",
+        "id": "john.doe",
+        "image": {
+          "url": "http://example.org/john/image",
+          "width": 250,
+          "height": 250
+        },
+        "displayName": "John Doe"
+      },
+      "verb": "post",
+      "object" : {
+        "url": "http://example.org/blog/2011/02/entry",
+        "id": "object1"
+      },
+      "target" : {
+        "url": "http://example.org/blog/",
+        "objectType": "blog",
+        "id": "target1",
+        "displayName": "John's Blog"
+      },
+      "openSocial": {
+        "embed" : {
+          "gadget" : "%origin%%contextroot%/containers/embeddedexperiences/PhotoList.xml",
+          "context" : {
+            "albumName": "Germany 2009",
+            "eeGadget" : "%origin%%contextroot%/containers/embeddedexperiences/AlbumViewer.xml",
+            "photoUrls": [
+              "http://farm4.static.flickr.com/3495/3925132517_5959dac775.jpg",
+              "http://farm4.static.flickr.com/3629/3394799776_47676abb46.jpg",
+              "http://farm5.static.flickr.com/4009/4413640211_715d924d9b.jpg",
+              "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba.jpg",
+              "http://farm1.static.flickr.com/36/98407782_9c4c5866d1.jpg",
+              "http://farm1.static.flickr.com/48/180544479_bb0d0f6559.jpg",
+              "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7.jpg"
+             ]
+           }
+        }
+      }
+	  }, {
+	  	"id": "activity2",
+      "published": "2011-03-10T15:04:55Z",
+      "generator": {
+        "url": "http://example.org/activities-app"
+      },
+      "provider": {
+        "url": "http://example.org/activity-stream"
+      },
+      "title": "John posted a new photo album.",
+      "actor": {
+        "url": "http://example.org/john",
+        "objectType": "person",
+        "id": "john.doe",
+        "image": {
+          "url": "http://example.org/john/image",
+          "width": 250,
+          "height": 250
+        },
+        "displayName": "John Doe"
+      },
+      "verb": "post",
+      "object" : {
+        "url": "http://example.org/album/my_fluffy_cat.jpg",
+        "objectType": "photo",
+        "id": "object2",
+        "summary": "Photo posted",
+        "image": {
+          "url": "http://example.org/album/my_fluffy_cat_thumb.jpg",
+          "width": 250,
+          "height": 250
+        },
+        "upstreamDuplicates" : ["upstream1", "upstream2"],
+        "downstreamDuplicates" : ["downstream1", "downstream2"],
+        "attachments": [
+          {"id": "attachment1", "objectType": "attachment"},
+          {"id": "attachment2", "objectType": "attachment"}
+        ]
+      },
+      "target": {
+        "url": "http://example.org/album/",
+        "objectType": "photo-album",
+        "id": "target2",
+        "displayName": "John's Photo Album",
+        "image": {
+          "url": "http://example.org/album/thumbnail.jpg",
+          "width": 250,
+          "height": 250
+        }
+      },
+      "openSocial": {
+        "embed" : {
+          "gadget" : "%origin%%contextroot%/containers/embeddedexperiences/AlbumViewer.xml",
+          "context" : {
+            "albumName": "Germany 2009",
+            "photoUrls": [
+              "http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg",
+                 "http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg",
+               "http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg",
+               "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg",
+               "http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg",
+               "http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg",
+               "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg"
+             ]
+          }
+        }
+      }
+    }, {
+      "id": "activity3",
+      "published": "2012-06-02T10:02:55Z",
+      "generator": {
+        "url": "http://example.org/activities-app"
+      },
+      "provider": {
+        "url": "http://example.org/activity-stream"
+      },
+      "title": "John posted a new photo to his blog",
+      "actor": {
+        "url": "http://example.org/john",
+        "objectType": "person",
+        "id": "john.doe",
+        "image": {
+          "url": "http://example.org/john/image",
+          "width": 250,
+          "height": 250
+        },
+        "displayName": "John Doe"
+      },
+      "verb": "post",
+      "object": {
+        "summary": "Photo about new world",
+        "id": "object3",
+        "image": {
+          "height": 250,
+          "width": 250,
+          "url": "http://example.org/album/new_world.jpg"
+        },
+        "objectType": "photo",
+        "url": "http://example.org/album/new_world.jpg"
+      },
+      "target": {
+        "url": "http://example.org/blog/",
+        "objectType": "blog",
+        "id": "target3",
+        "displayName": "John's Blog About Life",
+        "image": {
+          "url": "http://example.org/album/thumbnail.jpg",
+          "width": 250,
+          "height": 250
+        }
+      },
+      "openSocial": {
+        "embed" : {
+          "gadget" : "%origin%%contextroot%/containers/embeddedexperiences/BlogViewer.xml",
+          "context" : {
+            "photoUrl" : "http://example.org/album/new_world.jpg"
+          },
+          "preferredExperience": {
+            "target": {
+              "type": "gadget",
+              "view": "embedded_canvas"
+            },
+            "display": {
+              "type": "text",
+              "label" : "Checkout new photo in John's blog"
+            }
+          }
+        }
+      }
+    }]
+},
+"albums" : {
+	"john.doe": [{
+		"id" : "germany123",
+		"ownerId" : "john.doe",
+  		"thumbnailUrl" : "http://hphotos-snc3.fbcdn.net/hs050.snc3/13734_810445703213_6222631_45135775_1728872_n.jpg",
+  		"title" : "Germany 2009",
+  		"description" : "Garmisch-Partenkirchen for research!",
+  		"location" : { "latitude": 0, "longitude": 0 }
+	}, {
+		"id" : "cruise123",
+		"ownerId" : "john.doe",
+		"thumbnailUrl" : "http://hphotos-snc3.fbcdn.net/hs143.snc3/17062_828584892133_6222631_45795844_8024804_n.jpg",
+		"title" : "Graduation Cruise",
+		"description" : "Graduation cruise!"
+	}, {
+		"id" : "temp123",
+		"ownerId" : "john.doe",
+		"title" : "Album with no Thumbnail",
+		"description" : "I don't have a thumbnail.  I'm a perfect album to delete..."
+	}]
+},
+"mediaItems" : {
+	"john.doe": [{
+		"title" : "Ski Jump",
+		"description" : "We're going to the top!",
+		"id" : "mediaItem2",
+		"albumId" : "germany123",
+		"mimeType" : "image/jpeg",
+		"type" : "image",
+		"thumbnailUrl" : "http://hphotos-snc3.fbcdn.net/hs070.snc3/13734_810445668283_6222631_45135772_741359_n.jpg",
+		"url" : "http://hphotos-snc3.fbcdn.net/hs070.snc3/13734_810445668283_6222631_45135772_741359_n.jpg"
+	}, {
+		"title" : "Frozen",
+		"description" : "Cool effects...",
+		"id" : "mediaItem3",
+		"albumId" : "germany123",
+		"mimeType" : "image/jpeg",
+		"type" : "image",
+		"thumbnailUrl" : "http://sphotos.ak.fbcdn.net/hphotos-ak-snc4/hs369.snc4/45245_928217193203_6222631_49412689_3724203_n.jpg",
+		"url" : "http://sphotos.ak.fbcdn.net/hphotos-ak-snc4/hs369.snc4/45245_928217193203_6222631_49412689_3724203_n.jpg"
+	}, {
+		"title" : "Hotel resort",
+		"id" : "mediaItem4",
+		"albumId" : "germany123",
+		"mimeType" : "image/jpeg",
+		"type" : "image",
+		"thumbnailUrl" : "http://sphotos.ak.fbcdn.net/hphotos-ak-snc3/hs050.snc3/13734_810445788043_6222631_45135783_1591091_n.jpg",
+		"url" : "http://sphotos.ak.fbcdn.net/hphotos-ak-snc3/hs050.snc3/13734_810445788043_6222631_45135783_1591091_n.jpg"
+	}, {
+		"title" : "Authentic? Yes!",
+		"id" : "mediaItem5",
+		"albumId" : "germany123",
+		"mimeType" : "image/jpeg",
+		"type" : "image",
+		"thumbnailUrl" : "http://hphotos-snc3.fbcdn.net/hs050.snc3/13734_810445758103_6222631_45135780_284612_n.jpg",
+		"url" : "http://hphotos-snc3.fbcdn.net/hs050.snc3/13734_810445758103_6222631_45135780_284612_n.jpg"
+	}, {
+		"title" : "Garmisch-Partenkirchen",
+		"id" : "mediaItem6",
+		"albumId" : "germany123",
+		"mimeType" : "image/jpeg",
+		"type" : "image",
+		"thumbnailUrl" : "http://hphotos-snc3.fbcdn.net/hs050.snc3/13734_810445703213_6222631_45135775_1728872_n.jpg",
+		"url" : "http://hphotos-snc3.fbcdn.net/hs050.snc3/13734_810445703213_6222631_45135775_1728872_n.jpg"
+	}]
+},
+//
+// ----------------------------- Data ---------------------------------------
+//
+"friendLinks" : {
+"canonical" : ["john.doe", "jane.doe", "george.doe", "maija.m"],
+"john.doe" : ["jane.doe", "george.doe", "maija.m"],
+"jane.doe" : ["john.doe"],
+"george.doe" : ["john.doe"],
+"maija.m" : []
+},
+//
+// ----------------------------- Groups ---------------------------------------
+//
+"groups":{
+    "canonical":[
+      {
+        "id":{
+          "value":"group0"
+        },
+        "title":"Group 0",
+        "description":"A group where the creater is also a member"
+      },
+      {
+        "id":{
+          "value":"group1"
+        },
+        "title":"Group 1",
+        "description":"A group where the creater is not a membber"
+      }
+    ],
+    "john.doe":[
+      {
+        "id":{
+          "value":"example.com:391nvf03381"
+        },
+        "title":"Group 2",
+        "description":"A group of people"
+      },
+      {
+        "id":{
+          "value":"example.com:390e3kd03"
+        },
+        "title":"Group 3",
+        "description":"Another group of people"
+      }
+    ]
+  },
+"groupMembers":{
+  "example.com:391nvf03381": [
+    "canonical",
+    "john.doe"
+  ],
+  "example.com:390e3kd03": [
+    "canonical",
+    "john.doe",
+    "jane.doe"
+  ]
+},
+//
+//---------------------------- Data For User Applications --------------------------------------------
+//
+"userApplications" : {
+"canonical" : ["9158", "9703", "9143", "8877"],
+"john.doe" : ["8877", "9143", "9158"],
+"jane.doe" : ["9158", "9703"],
+"george.doe" : ["9143"],
+"maija.m" : []
+},
+//
+//--------------------------- Message Collections. -----------------------------------------
+//
+"messages" : {
+"canonical" : {
+  "notification" : {"title" : "Notifications",
+                         "messages" : [
+                           {"id": "1", "title": "whazzup", "type": "notification", "body": "hey dude."},
+                           {"id": "2", "title": "play checkers", "type": "notification", "body": "hot online checkers action"},
+                           {"id": "3", "title": "you won!", "type": "notification", "body": "<b>yes, you really, really won!</b>"}
+
+                           ]},
+  "publicMessage" : { "title" : "Profile Comments",
+        "messages" : [
+          {"id": "1", "senderId": "john.doe", "title": "Hairdo", "type": "publicMessage", "body": "nice &quot;haircut!&quot;", "replies": ["1a","1b"]},
+          {"id": "1a", "senderId": "canonical", "title": "", "type": "publicMessage", "body": "that's not hair, it's a wig!", "inReplyTo": "1"},
+          {"id": "1b", "senderId": "john.doe", "title": "100% polyester", "type": "publicMessage", "body": "only the finest hyrdrocarbons :)","inReplyTo": "1"},
+          {"id": "2", "senderId": "jane.doe", "title": "hola", "type": "publicMessage", "body": "be my bff?"}
+
+        ]},
+  "privateMessage" : {"title" : "Inbox",
+        "messages" : []}
+  
+  },
+  
+"1" : {
+    "notification" : {"title" : "Notifications", "messages" : []},
+    "privateMessage" : {"title" : "Inbox", "messages" : []},
+    "publicMessage" : {"title" : "Inbox", "messages" : []}
+},
+
+"john.doe" : {
+  "notification" : {"title" : "Notifications",
+                          "messages" : [
+                          {"id": "1", "title": "you received a peanut", "type": "notification", "body": "peanuts are healthy"},
+                          {"id": "3", "title": "Group Request", "type": "notification", "body": "Join Cat Lovers Anonymous"}
+                          ]},
+  "privateMessage" : {"title" : "Inbox", "messages" : []},
+  "publicMessage" : {"title" : "Inbox", "messages" : []}
+},
+"jane.doe" : {
+    "notification" : {"title" : "Notifications", "messages" : []},
+    "privateMessage" : {"title" : "Inbox", "messages" : []},
+    "publicMessage" : {"title" : "Inbox", "messages" : []}
+},
+  "george.doe" : {
+        "notification" : {"title" : "Notifications", "messages" : []},
+        "privateMessage" : {"title" : "Inbox", "messages" : []},
+        "publicMessage" : {"title" : "Inbox", "messages" : []}
+  },
+  "maija.m" : { 
+        "notification" : {"title" : "Notifications", "messages" : []},
+        "privateMessage" : {"title" : "Inbox", "messages" : []},
+        "publicMessage" : {"title" : "Inbox", "messages" : []}
+  }
+},
+//
+// Gadgets/App information.  OAuth Consumer Key defaults to the app url.
+//
+"apps" : {
+    "http://localhost:8080%contextroot%/gadgets/SocialHelloWorld.xml":
+        { "title" : "Social Hello World",
+          "consumerSecret" : "secret",
+           "icon" : "http://localhost:8080%contextroot%/images/icon.png"},
+    "http://localhost:8080%contextroot%/gadgets/SocialActivitiesWorld.xml" :
+        { "title" : "Social Activities World",
+          "consumerSecret" : "secret",
+          "icon" : "http://localhost:8080%contextroot%/images/icon.png"},
+    "http://localhost:8080%contextroot%/gadgets/oauth.xml" :
+        { "title" : "Demo OAuth Gadget",
+          "consumerSecret" : "secret",
+          "icon" : "http://localhost:8080%contextroot%/images/icon.png"},
+    "http://localhost:8080%contextroot%/gadgets/shindigoauth.xml" :
+        { "title" : "Demo OAuth Gadget",
+          "consumerSecret" : "secret",
+          "icon" : "http://localhost:8080%contextroot%/images/icon.png"}
+ },
+ 
+ // Registry of OAuth 2.0 clients with Shindig's service provider.
+ "oauth2" : {
+  "advancedAuthorizationCodeClient" : {
+    "registration" : {
+      "id" : "advancedAuthorizationCodeClient",
+      "secret": "advancedAuthorizationCodeClient_secret",
+      "title": "Most Advanced Authorization Code Client Ever!",
+      "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient/friends",
+      "type" : "confidential",
+      "flow" : "authorization_code"
+    },
+    "authorizationCodes" : {
+      "advancedClient_authcode_1" : {
+        // Authentication code has been consumed since associatedSignature exists
+        "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient/friends",
+        //Setting expiration to -1 makes code permanent
+        "expiration" : -1
+      },
+      "advancedClient_authcode_2" : {
+        "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient/friends",
+        "expiration" : -1
+      }
+    },
+    "accessTokens" : {
+      "advancedClient_accesstoken_1" : {
+        "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient/friends",
+        "expiration" : -1
+      }
+    }
+  },
+  "advancedImplicitClient" : {
+    "registration" : {
+      "id" : "advancedImplicitClient",
+      "title" : "Most Advanced Implicit Client Ever!",
+      "type" : "public",
+      "redirectURI" : "http://localhost:8080/oauthclients/ImplicitClientHelper.html",
+      "flow" : "implicit"
+    }
+  },
+  "testClient" : {
+    "registration" : {
+      "id" : "testClient",
+      "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient",
+      "type" : "public",
+      "flow" : "authorization_code"
+    },
+    "authorizationCodes" : {
+      "testClient_authcode_1" : {
+        "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient",
+        "expiration" : -1
+      },
+      "testClient_authcode_2" : {
+        "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient",
+        "expiration" : -1
+      }
+    },
+    "accessTokens" : {
+      "testClient_accesstoken_1" : {
+        "redirectURI" : "http://localhost:8080/oauthclients/AuthorizationCodeClient",
+        "expiration" : -1
+      }
+    }
+  },
+  "testClientCredentialsClient" : {
+    "registration" : {
+      "id" : "testClientCredentialsClient",
+      "secret": "clientCredentialsClient_secret",
+      "type" : "confidential",
+      "flow" : "client_credentials"
+    },
+      "accessTokens" : {
+        "testClientCredentialsClient_accesstoken_1" : {
+        "expiration" : -1
+      }
+    }
+  },
+  "shindigClient" : {
+    "registration" : {
+      "id" : "shindigClient",
+      "secret": "U78KJM98372AMGL87612993M",
+      "title": "shindig client registered for authorization",
+      "redirectURI" : "http://localhost:8080%contextRoot%/gadgets/oauth2callback",
+      "type" : "confidential",
+      "flow" : "authorization_code"
+  },
+    "authorizationCodes" : {
+      "shindigClient_authcode_1" : {
+        // Authentication code has been consumed since associatedSignature exists
+        "redirectURI" : "http://localhost:8080%contextRoot%/gadgets/oauth2callback",
+        //Setting expiration to -1 makes code permanent
+        "expiration" : -1
+      },
+      "shindigClient_authcode_2" : {
+        "redirectURI" : "http://localhost:8080%contextRoot%/gadgets/oauth2callback",
+        "expiration" : -1
+      }
+    }, 
+    "accessTokens" : {
+      "shindigClient_accesstoken_1" : {
+        "redirectURI" : "http://localhost:8080%contextRoot%/gadgets/oauth2callback",
+        "expiration" : -1
+      }
+    }
+  }
+},
+
+ // duplicates userApplications as above..
+ "permissions": {
+   "john.doe" : { "http://localhost:8080%contextroot%/gadgets/SocialHelloWorld.xml" : { "installed" : true},
+                  "http://localhost:8080%contextroot%/gadgets/SocialActivitiesWorld.xml" : { "installed" : true}
+   },
+   "canonical" :{ "http://localhost:8080%contextroot%/gadgets/SocialHelloWorld.xml" : { "installed" : true},
+                  "http://localhost:8080%contextroot%/gadgets/SocialActivitiesWorld.xml" : { "installed" : true}
+   }
+ },
+// Passwords for authenticaiton service
+ "passwords" : {
+    "john.doe" : "password",
+    "jane.doe" : "password",
+    "canonical" : "password"
+  }
+}
diff --git a/trunk/content/xpc.swf b/trunk/content/xpc.swf
new file mode 100644
index 0000000..950fe1f
--- /dev/null
+++ b/trunk/content/xpc.swf
Binary files differ
diff --git a/trunk/doap_shindig.rdf b/trunk/doap_shindig.rdf
new file mode 100644
index 0000000..e211c97
--- /dev/null
+++ b/trunk/doap_shindig.rdf
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/xsl"?>
+<rdf:RDF xml:lang="en"
+         xmlns="http://usefulinc.com/ns/doap#" 
+         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 
+         xmlns:asfext="http://projects.apache.org/ns/asfext#"
+         xmlns:foaf="http://xmlns.com/foaf/0.1/">
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You 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.
+-->
+  <Project rdf:about="http://shindig.apache.org">
+    <created>2012-04-14</created>
+    <license rdf:resource="http://usefulinc.com/doap/licenses/asl20" />
+    <name>Apache Shindig</name>
+    <homepage rdf:resource="http://shindig.apache.org" />
+    <asfext:pmc rdf:resource="http://shindig.apache.org" />
+    <shortdesc>Apache Shindig software is an OpenSocial container and helps you to start hosting OpenSocial apps quickly by providing the code to render gadgets, proxy requests, and handle REST and RPC requests.</shortdesc>
+    <description>Apache Shindig is a container for hosting social application consisting of four parts:
+
+    Gadget Container JavaScript: core JavaScript foundation for general gadget functionality (read more about gadget functionality). This JavaScript manages security, communication, UI layout, and feature extensions, such as the OpenSocial API.
+    Gadget Rendering Server: used to render the gadget XML into JavaScript and HTML for the container to expose via the container JavaScript.
+    OpenSocial Container JavaScript: JavaScript environment that sits on top of the Gadget Container JavaScript and provides OpenSocial specific functionality (profiles, friends, activities, datastore).
+    OpenSocial Data Server: an implementation of the server interface to container-specific information, including the OpenSocial REST APIs, with clear extension points so others can connect it to their own backends.
+
+Apache Shindig is the reference implementation of OpenSocial API specifications, versions 0.8.x and 0.9.x, a standard set of Social Network APIs.</description>
+    <bug-database rdf:resource="https://issues.apache.org/jira/browse/SHINDIG" />
+    <mailing-list rdf:resource="http://shindig.apache.org/mail-lists.html" />
+    <download-page rdf:resource="http://shindig.apache.org/download" />
+    <programming-language>Java</programming-language>
+    <category rdf:resource="http://projects.apache.org/category/javaee" />
+    <release>
+      <Version>
+        <name>Apache Shindig 2.0.0</name>
+        <created>2010-09-10</created>
+        <revision>2.0.0</revision>
+      </Version>
+    </release>
+	    <repository>
+      <SVNRepository>
+        <location rdf:resource="http://svn.apache.org/repos/asf/shindig"/>
+        <browse rdf:resource="http://svn.apache.org/viewvc/shindig"/>
+      </SVNRepository>
+    </repository>
+    <maintainer>
+      <foaf:Person>
+        <foaf:name>Apache Shindig PMC</foaf:name>
+          <foaf:mbox rdf:resource="mailto:dev@shindig.apache.org"/>
+      </foaf:Person>
+    </maintainer>
+  </Project>
+</rdf:RDF>
diff --git a/trunk/etc/check_staged_release.sh b/trunk/etc/check_staged_release.sh
new file mode 100755
index 0000000..57556d8
--- /dev/null
+++ b/trunk/etc/check_staged_release.sh
@@ -0,0 +1,83 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+#
+# This script was orriginally developed under Apache Sling at http://svn.apache.org/repos/asf/sling/trunk/check_staged_release.sh
+# and has been modified for Shindig on 7th August 2009
+#
+
+STAGING=${1}
+DOWNLOAD=${2:-/tmp/shindig-staging}
+mkdir ${DOWNLOAD} 2>/dev/null
+
+if [ -z "${STAGING}" -o ! -d "${DOWNLOAD}" ]
+then
+ echo "Usage: check_staged_release.sh <staging-number> [temp-directory]"
+ echo " eg check_staged_release.sh 011 "
+ exit
+fi
+
+if [ ! -e "${DOWNLOAD}/${STAGING}" ]
+then
+ echo "################################################################################"
+ echo "                           DOWNLOAD STAGED REPOSITORY                           "
+ echo "################################################################################"
+
+ if [ `wget --help | grep "no-check-certificate" | wc -l` -eq 1 ]
+ then
+   CHECK_SSL=--no-check-certificate
+ fi
+
+ wget $CHECK_SSL \
+  -nv -r -np "--reject=html,txt" "--follow-tags=" \
+  -P "${DOWNLOAD}/${STAGING}" -nH "--cut-dirs=3" --ignore-length \
+  "http://repository.apache.org/content/repositories/shindig-staging-${STAGING}/org/apache/shindig/"
+
+else
+ echo "################################################################################"
+ echo "                       USING EXISTING STAGED REPOSITORY                         "
+ echo "################################################################################"
+ echo "${DOWNLOAD}/${STAGING}"
+fi
+
+echo "################################################################################"
+echo "                          CHECK SIGNATURES AND DIGESTS                          "
+echo "################################################################################"
+
+for i in `find "${DOWNLOAD}/${STAGING}" -type f | grep -v '\.\(asc\|sha1\|md5\)$'`
+do
+ f=`echo $i | sed 's/\.asc$//'`
+ echo "$f"
+ gpg --verify $f.asc 2>/dev/null
+ if [ "$?" = "0" ]; then CHKSUM="GOOD"; else CHKSUM="BAD!!!!!!!!"; fi
+ if [ ! -f "$f.asc" ]; then CHKSUM="----"; fi
+ echo "gpg:  ${CHKSUM}"
+ if [ "`cat $f.md5 2>/dev/null`" = "`openssl md5 < $f 2>/dev/null`" ]; then CHKSUM="GOOD (`cat $f.md5`)"; else CHKSUM="BAD!!!!!!!!"; fi
+ if [ ! -f "$f.md5" ]; then CHKSUM="----"; fi
+ echo "md5:  ${CHKSUM}"
+ if [ "`cat $f.sha1 2>/dev/null`" = "`openssl sha1 < $f 2>/dev/null`" ]; then CHKSUM="GOOD (`cat $f.sha1`)"; else CHKSUM="BAD!!!!!!!!"; fi
+ if [ ! -f "$f.sha1" ]; then CHKSUM="----"; fi
+ echo "sha1: ${CHKSUM}"
+done
+
+if [ -z "${CHKSUM}" ]; then echo "WARNING: no files found!"; fi
+
+echo "################################################################################"
+
+
diff --git a/trunk/etc/checkstyle/README b/trunk/etc/checkstyle/README
new file mode 100644
index 0000000..044f0e1
--- /dev/null
+++ b/trunk/etc/checkstyle/README
@@ -0,0 +1 @@
+This directory contains code style configuration files for Checkstyle.
\ No newline at end of file
diff --git a/trunk/etc/checkstyle/checkstyle.xml b/trunk/etc/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..90c69bd
--- /dev/null
+++ b/trunk/etc/checkstyle/checkstyle.xml
@@ -0,0 +1,272 @@
+<?xml version="1.0"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+
+<!DOCTYPE module PUBLIC
+    "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+    "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<!--
+
+  Checkstyle configuration that checks the sun coding conventions from:
+
+    - the Java Language Specification at
+      http://java.sun.com/docs/books/jls/second_edition/html/index.html
+
+    - the Sun Code Conventions at http://java.sun.com/docs/codeconv/
+
+    - the Javadoc guidelines at
+      http://java.sun.com/j2se/javadoc/writingdoccomments/index.html
+
+    - the JDK Api documentation http://java.sun.com/j2se/docs/api/index.html
+
+    - some best practices
+
+  Checkstyle is very configurable. Be sure to read the documentation at
+  http://checkstyle.sf.net (or in your downloaded distribution).
+
+  Most Checks are configurable, be sure to consult the documentation.
+
+  To completely disable a check, just comment it out or delete it from the file.
+
+  Finally, it is worth reading the documentation.
+
+-->
+
+<module name="Checker">
+
+    <!-- Checks that a package.html file exists for each package.     -->
+    <!-- See http://checkstyle.sf.net/config_javadoc.html#PackageHtml -->
+    <module name="JavadocPackage">
+        <property name="severity" value="warning"/>
+        <property name="allowLegacy" value="true" />
+    </module>
+
+    <!-- Checks whether files end with a new line.                        -->
+    <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->
+    <module name="NewlineAtEndOfFile" >
+        <property name="severity" value="warning"/>
+    </module>
+
+    <!-- Checks that property files contain the same keys.         -->
+    <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
+    <module name="Translation"/>
+
+    <module name="StrictDuplicateCode">
+       <property name="min" value="30"/>
+    </module>
+
+    <!-- Defaults to 2000 lines -->
+    <module name="FileLength">
+        <property name="severity" value="warning"/>
+    </module>
+
+    <module name="FileTabCharacter" />
+
+	<!-- Following interprets the header file as regular expressions. -->
+	<module name="Header">
+	    <property name="headerFile" value="${checkstyle.header.file}" />
+	</module>
+
+    <module name="TreeWalker">
+
+        <property name="cacheFile" value="${checkstyle.cache.file}"/>
+
+        <!-- Checks for Javadoc comments.  We try to limit the scope to protected and public only. -->
+        <!-- See http://checkstyle.sf.net/config_javadoc.html                                      -->
+        <!-- See http://checkstyle.sf.net/property_types.html#scope                                -->
+        <module name="JavadocType">
+            <property name="severity" value="info"/>
+        </module>
+        <module name="JavadocMethod" >
+            <property name="severity" value="info"/>
+            <property name="scope" value="protected"/>
+            <!-- If Javadoc is missing on getters and setters, ignore it -->
+            <property name="allowMissingPropertyJavadoc" value="true"/>
+        </module>
+        <module name="JavadocVariable">
+            <property name="severity" value="info"/>
+            <property name="scope" value="protected"/>
+        </module>
+        <module name="JavadocStyle">
+            <property name="severity" value="info"/>
+            <property name="scope" value="protected"/>
+        </module>
+
+
+        <!-- Checks for Naming Conventions.                  -->
+        <!-- See http://checkstyle.sf.net/config_naming.html -->
+        <module name="ConstantName">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="LocalFinalVariableName" />
+        <module name="LocalVariableName"/>
+        <module name="MemberName"/>
+        <module name="MethodName"/>
+        <module name="PackageName"/>
+        <module name="ParameterName"/>
+        <module name="StaticVariableName"/>
+        <module name="TypeName"/>
+
+        <!-- Checks for imports                              -->
+        <!-- See http://checkstyle.sf.net/config_import.html -->
+        <module name="AvoidStarImport"/>
+        <module name="IllegalImport"/> <!-- defaults to sun.* packages -->
+        <module name="RedundantImport" >
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="UnusedImports">
+            <property name="severity" value="warning"/>
+        </module>
+
+
+        <!-- Checks for Size Violations.                    -->
+        <!-- See http://checkstyle.sf.net/config_sizes.html -->
+        <module name="LineLength" >
+            <property name="max" value="100"/>
+        </module>
+        <module name="MethodLength"/>
+        <module name="ParameterNumber"/>
+
+
+        <!-- Checks for whitespace                               -->
+        <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+        <module name="GenericWhitespace"/>
+        <module name="EmptyForIteratorPad">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="MethodParamPad">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="NoWhitespaceAfter">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="NoWhitespaceBefore">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="OperatorWrap">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="ParenPad">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="TypecastParenPad">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="WhitespaceAfter"/>
+        <module name="WhitespaceAround">
+            <property name="tokens" value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, EQUAL, GE, GT, LAND, LCURLY, LE, LITERAL_ASSERT, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, TYPE_EXTENSION_AND"/>
+        </module>
+
+
+        <!-- Modifier Checks                                    -->
+        <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+        <module name="ModifierOrder">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="RedundantModifier"/>
+
+
+        <!-- Checks for blocks. You know, those {}'s         -->
+        <!-- See http://checkstyle.sf.net/config_blocks.html -->
+        <module name="AvoidNestedBlocks">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="EmptyBlock">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="LeftCurly">
+            <property name="maxLineLength" value="100"/>
+        </module>
+        <module name="NeedBraces"/>
+        <module name="RightCurly"/>
+
+        <!-- Checks for common coding problems               -->
+        <!-- See http://checkstyle.sf.net/config_coding.html -->
+        <module name="AvoidInlineConditionals">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="DoubleCheckedLocking"/>    <!-- MY FAVOURITE -->
+        <module name="EmptyStatement"/>
+        <module name="EqualsHashCode"/>
+        <!-- <module name="HiddenField"/> -->
+        <module name="IllegalInstantiation"/>
+        <module name="InnerAssignment"/>
+        <module name="MagicNumber"/>
+        <module name="MissingSwitchDefault"/>
+        <module name="RedundantThrows"/>
+        <module name="SimplifyBooleanExpression"/>
+        <module name="SimplifyBooleanReturn"/>
+
+        <!-- Checks for class design                         -->
+        <!-- See http://checkstyle.sf.net/config_design.html -->
+        <!--  <module name="DesignForExtension"/> -->
+        <module name="FinalClass"/>
+        <module name="HideUtilityClassConstructor"/>
+        <module name="InterfaceIsType">
+            <property name="severity" value="warning"/>
+        </module>
+        <module name="VisibilityModifier"/>
+
+        <!-- Checks code complexity metrics                   -->
+        <!-- See http://checkstyle.sf.net/config_metrics.html -->
+        <module name="ClassFanOutComplexity">
+           <property name="max" value="10"/>
+           <property name="severity" value="warning"/>
+        </module>
+        <module name="CyclomaticComplexity" >
+           <property name="severity" value="warning"/>
+        </module>
+        <module name="ClassDataAbstractionCoupling">
+           <property name="severity" value="warning"/>
+        </module>
+        <module name="BooleanExpressionComplexity">
+           <property name="max" value="7"/>
+           <property name="severity" value="warning"/>
+        </module>
+        <module name="NPathComplexity" >
+           <property name="severity" value="warning"/>
+        </module>
+        <module name="JavaNCSS">
+           <property name="severity" value="warning"/>
+        </module>
+
+        <!-- Miscellaneous other checks.                   -->
+        <!-- See http://checkstyle.sf.net/config_misc.html -->
+        <module name="ArrayTypeStyle"/>
+        <!--
+        <module name="FinalParameters"/>
+        -->
+        <module name="Regexp">
+            <property name="format" value="[ \t]+$"/>
+            <property name="illegalPattern" value="true"/>
+            <property name="message" value="Line has trailing spaces."/>
+        </module>
+        <!-- There are other Jenkins addons to count TODOs and we can make use of those
+        <module name="TodoComment">
+            <property name="severity" value="info"/>
+        </module>
+        -->
+
+        <module name="UpperEll"/>
+
+    </module>
+
+</module>
diff --git a/trunk/etc/checkstyle/java.header b/trunk/etc/checkstyle/java.header
new file mode 100644
index 0000000..7220975
--- /dev/null
+++ b/trunk/etc/checkstyle/java.header
@@ -0,0 +1,18 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
\ No newline at end of file
diff --git a/trunk/etc/cruisecontrol/README b/trunk/etc/cruisecontrol/README
new file mode 100644
index 0000000..dd74f00
--- /dev/null
+++ b/trunk/etc/cruisecontrol/README
@@ -0,0 +1,35 @@
+=== Instructions for running cruise control on the shindig java source code ===
+
+Download the latest CruiseControl (CC) from http://cruisecontrol.sourceforge.net/download.html. As
+of now we are using 2.7.2, but newer version should work as well.
+
+Install CC on the same level as your Shindig project. (You can use other locations as well, but this
+would require you to modify the config.xml file -- which should be easy.)
+
+/usr/local
+       |
+       +---- Shindig
+       +---- CruiseControl
+
+In ~/.m2 create settings.xml with the following content. The site.html.dir property needs to point
+the right location were the httpd can find it.
+
+   <settings>
+     <profiles>
+       <profile>
+         <id>reporting</id>
+         <properties>
+           <site.html.dir>file:///usr/local/google/shindig/html&lt;/site.html.dir>
+         </properties>
+       </profile>
+     </profiles>
+   </settings>
+
+
+Build the project with 'mvn clean package site-deploy -P reporting'
+
+Replace the existing cruisecontrol/config.xml with the one from <shindig trunk>/etc/cruisecontrol/
+
+Restart CruiseControl
+
+Force a build!
diff --git a/trunk/etc/eclipse/README b/trunk/etc/eclipse/README
new file mode 100644
index 0000000..e236ffc
--- /dev/null
+++ b/trunk/etc/eclipse/README
@@ -0,0 +1,3 @@
+This directory contains code style configuration files for Eclipse.
+For more detail on these files see https://issues.apache.org/jira/browse/SHINDIG-76
+
diff --git a/trunk/etc/eclipse/shindig-eclipse-cleanup.xml b/trunk/etc/eclipse/shindig-eclipse-cleanup.xml
new file mode 100644
index 0000000..adc9bb1
--- /dev/null
+++ b/trunk/etc/eclipse/shindig-eclipse-cleanup.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<profiles version="2">
+<profile kind="CleanUpProfile" name="shindig-dev" version="2">
+<setting id="cleanup.sort_members" value="false"/>
+<setting id="cleanup.remove_trailing_whitespaces" value="true"/>
+<setting id="cleanup.qualify_static_member_accesses_with_declaring_class" value="true"/>
+<setting id="cleanup.remove_private_constructors" value="true"/>
+<setting id="cleanup.use_parentheses_in_expressions" value="false"/>
+<setting id="cleanup.remove_unused_local_variables" value="false"/>
+<setting id="cleanup.qualify_static_method_accesses_with_declaring_class" value="false"/>
+<setting id="cleanup.remove_unused_private_fields" value="true"/>
+<setting id="cleanup.always_use_this_for_non_static_field_access" value="true"/>
+<setting id="cleanup.make_private_fields_final" value="true"/>
+<setting id="cleanup.format_source_code_changes_only" value="false"/>
+<setting id="cleanup.never_use_parentheses_in_expressions" value="true"/>
+<setting id="cleanup.qualify_static_member_accesses_through_instances_with_declaring_class" value="true"/>
+<setting id="cleanup.always_use_blocks" value="true"/>
+<setting id="cleanup.add_serial_version_id" value="false"/>
+<setting id="cleanup.remove_unused_private_types" value="true"/>
+<setting id="cleanup.add_default_serial_version_id" value="true"/>
+<setting id="cleanup.correct_indentation" value="false"/>
+<setting id="cleanup.make_local_variable_final" value="true"/>
+<setting id="cleanup.use_this_for_non_static_field_access" value="true"/>
+<setting id="cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class" value="true"/>
+<setting id="cleanup.use_this_for_non_static_method_access_only_if_necessary" value="true"/>
+<setting id="cleanup.make_type_abstract_if_missing_method" value="false"/>
+<setting id="cleanup.add_missing_nls_tags" value="false"/>
+<setting id="cleanup.remove_unused_private_members" value="false"/>
+<setting id="cleanup.use_blocks" value="true"/>
+<setting id="cleanup.make_parameters_final" value="false"/>
+<setting id="cleanup.make_variable_declarations_final" value="true"/>
+<setting id="cleanup.add_missing_override_annotations" value="true"/>
+<setting id="cleanup.format_source_code" value="true"/>
+<setting id="cleanup.use_this_for_non_static_field_access_only_if_necessary" value="false"/>
+<setting id="cleanup.convert_to_enhanced_for_loop" value="false"/>
+<setting id="cleanup.remove_unused_imports" value="true"/>
+<setting id="cleanup.use_this_for_non_static_method_access" value="false"/>
+<setting id="cleanup.qualify_static_field_accesses_with_declaring_class" value="true"/>
+<setting id="cleanup.never_use_blocks" value="false"/>
+<setting id="cleanup.organize_imports" value="true"/>
+<setting id="cleanup.remove_unused_private_methods" value="true"/>
+<setting id="cleanup.use_blocks_only_for_return_and_throw" value="false"/>
+<setting id="cleanup.add_missing_annotations" value="true"/>
+<setting id="cleanup.sort_members_all" value="false"/>
+<setting id="cleanup.always_use_parentheses_in_expressions" value="false"/>
+<setting id="cleanup.remove_trailing_whitespaces_all" value="true"/>
+<setting id="cleanup.add_missing_methods" value="false"/>
+<setting id="cleanup.add_generated_serial_version_id" value="false"/>
+<setting id="cleanup.add_missing_deprecated_annotations" value="true"/>
+<setting id="cleanup.remove_unnecessary_casts" value="true"/>
+<setting id="cleanup.remove_trailing_whitespaces_ignore_empty" value="false"/>
+<setting id="cleanup.always_use_this_for_non_static_method_access" value="false"/>
+<setting id="cleanup.remove_unnecessary_nls_tags" value="true"/>
+<setting id="cleanup.add_missing_override_annotations_interface_methods" value="false"/>
+</profile>
+</profiles>
\ No newline at end of file
diff --git a/trunk/etc/eclipse/shindig-eclipse-codestyle_2.xml b/trunk/etc/eclipse/shindig-eclipse-codestyle_2.xml
new file mode 100644
index 0000000..c1a7e92
--- /dev/null
+++ b/trunk/etc/eclipse/shindig-eclipse-codestyle_2.xml
@@ -0,0 +1,282 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<profiles version="11">
+<profile kind="CodeFormatterProfile" name="shindig-dev" version="11">
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="2"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="100"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.jdt.core.compiler.source" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="100"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.5"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="4"/>
+<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+</profile>
+</profiles>
diff --git a/trunk/etc/eclipse/shindig-eclipse-codetemplate.xml b/trunk/etc/eclipse/shindig-eclipse-codetemplate.xml
new file mode 100644
index 0000000..5798a2b
--- /dev/null
+++ b/trunk/etc/eclipse/shindig-eclipse-codetemplate.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?><templates><template autoinsert="true" context="gettercomment_context" deleted="false" description="Comment for getter method" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.gettercomment" name="gettercomment">/**
+ * @return the ${bare_field_name}
+ */</template><template autoinsert="true" context="settercomment_context" deleted="false" description="Comment for setter method" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.settercomment" name="settercomment">/**
+ * @param ${param} the ${bare_field_name} to set
+ */</template><template autoinsert="true" context="constructorcomment_context" deleted="false" description="Comment for created constructors" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.constructorcomment" name="constructorcomment">/**
+ * ${tags}
+ */</template><template autoinsert="false" context="filecomment_context" deleted="false" description="Comment for created Java files" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.filecomment" name="filecomment">/**
+ *
+ */</template><template autoinsert="false" context="typecomment_context" deleted="false" description="Comment for created types" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.typecomment" name="typecomment">/**
+ * @author &lt;a href="mailto:dev@shindig.apache.org"&gt;Shindig Dev&lt;/a&gt;
+ * @version $$Id: $$
+ *
+ * ${tags}
+ */</template><template autoinsert="true" context="fieldcomment_context" deleted="false" description="Comment for fields" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.fieldcomment" name="fieldcomment">/**
+ * 
+ */</template><template autoinsert="true" context="methodcomment_context" deleted="false" description="Comment for non-overriding methods" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.methodcomment" name="methodcomment">/**
+ * ${tags}
+ */</template><template autoinsert="true" context="overridecomment_context" deleted="false" description="Comment for overriding methods" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.overridecomment" name="overridecomment">/* (non-Javadoc)
+ * ${see_to_overridden}
+ */</template><template autoinsert="true" context="delegatecomment_context" deleted="false" description="Comment for delegate methods" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.delegatecomment" name="delegatecomment">/**
+ * ${tags}
+ * ${see_to_target}
+ */</template><template autoinsert="false" context="newtype_context" deleted="false" description="Newly created files" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.newtype" name="newtype">/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+${filecomment}
+${package_declaration}
+
+/**
+ * @author &lt;a href="mailto:dev@shindig.apache.org"&gt;Shindig Dev&lt;/a&gt;
+ * @version $$Id: $$
+ */
+${typecomment}
+${type_declaration}
+</template><template autoinsert="true" context="classbody_context" deleted="false" description="Code in new class type bodies" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.classbody" name="classbody">
+</template><template autoinsert="true" context="interfacebody_context" deleted="false" description="Code in new interface type bodies" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.interfacebody" name="interfacebody">
+</template><template autoinsert="true" context="enumbody_context" deleted="false" description="Code in new enum type bodies" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.enumbody" name="enumbody">
+</template><template autoinsert="true" context="annotationbody_context" deleted="false" description="Code in new annotation type bodies" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.annotationbody" name="annotationbody">
+</template><template autoinsert="true" context="catchblock_context" deleted="false" description="Code in new catch blocks" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.catchblock" name="catchblock">// ${todo} Auto-generated catch block
+${exception_var}.printStackTrace();</template><template autoinsert="true" context="methodbody_context" deleted="false" description="Code in created method stubs" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.methodbody" name="methodbody">// ${todo} Auto-generated method stub
+${body_statement}</template><template autoinsert="true" context="constructorbody_context" deleted="false" description="Code in created constructor stubs" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.constructorbody" name="constructorbody">${body_statement}
+// ${todo} Auto-generated constructor stub</template><template autoinsert="true" context="getterbody_context" deleted="false" description="Code in created getters" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.getterbody" name="getterbody">return ${field};</template><template autoinsert="true" context="setterbody_context" deleted="false" description="Code in created setters" enabled="true" id="org.eclipse.jdt.ui.text.codetemplates.setterbody" name="setterbody">${field} = ${param};</template></templates>
\ No newline at end of file
diff --git a/trunk/etc/eclipse/shindig-eclipse-javascript-cleanup.xml b/trunk/etc/eclipse/shindig-eclipse-javascript-cleanup.xml
new file mode 100644
index 0000000..3b59dc5
--- /dev/null
+++ b/trunk/etc/eclipse/shindig-eclipse-javascript-cleanup.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?><profiles version="2">
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<profile kind="CleanUpProfile" name="shindig-dev" version="2">
+<setting id="cleanup.sort_members" value="false"/>
+<setting id="cleanup.qualify_static_member_accesses_with_declaring_class" value="true"/>
+<setting id="cleanup.remove_trailing_whitespaces" value="true"/>
+<setting id="cleanup.remove_private_constructors" value="true"/>
+<setting id="cleanup.use_parentheses_in_expressions" value="false"/>
+<setting id="cleanup.qualify_static_method_accesses_with_declaring_class" value="false"/>
+<setting id="cleanup.remove_unused_local_variables" value="false"/>
+<setting id="cleanup.remove_unused_private_fields" value="true"/>
+<setting id="cleanup.always_use_this_for_non_static_field_access" value="false"/>
+<setting id="cleanup.make_private_fields_final" value="true"/>
+<setting id="cleanup.never_use_parentheses_in_expressions" value="true"/>
+<setting id="cleanup.qualify_static_member_accesses_through_instances_with_declaring_class" value="true"/>
+<setting id="cleanup.always_use_blocks" value="true"/>
+<setting id="cleanup.remove_unused_private_types" value="true"/>
+<setting id="cleanup.add_serial_version_id" value="false"/>
+<setting id="cleanup.add_default_serial_version_id" value="true"/>
+<setting id="cleanup.make_local_variable_final" value="true"/>
+<setting id="cleanup.use_this_for_non_static_field_access" value="false"/>
+<setting id="cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class" value="true"/>
+<setting id="cleanup.use_this_for_non_static_method_access_only_if_necessary" value="true"/>
+<setting id="cleanup.add_missing_nls_tags" value="false"/>
+<setting id="cleanup.remove_unused_private_members" value="false"/>
+<setting id="cleanup.make_parameters_final" value="false"/>
+<setting id="cleanup.use_blocks" value="true"/>
+<setting id="cleanup.make_variable_declarations_final" value="false"/>
+<setting id="cleanup.add_missing_override_annotations" value="true"/>
+<setting id="cleanup.format_source_code" value="true"/>
+<setting id="cleanup.use_this_for_non_static_field_access_only_if_necessary" value="true"/>
+<setting id="cleanup.convert_to_enhanced_for_loop" value="false"/>
+<setting id="cleanup.remove_unused_imports" value="true"/>
+<setting id="cleanup.use_this_for_non_static_method_access" value="false"/>
+<setting id="cleanup.qualify_static_field_accesses_with_declaring_class" value="false"/>
+<setting id="cleanup.never_use_blocks" value="false"/>
+<setting id="cleanup.organize_imports" value="false"/>
+<setting id="cleanup.remove_unused_private_methods" value="true"/>
+<setting id="cleanup.use_blocks_only_for_return_and_throw" value="false"/>
+<setting id="cleanup.add_missing_annotations" value="true"/>
+<setting id="cleanup.always_use_parentheses_in_expressions" value="false"/>
+<setting id="cleanup.sort_members_all" value="false"/>
+<setting id="cleanup.remove_trailing_whitespaces_all" value="true"/>
+<setting id="cleanup.add_generated_serial_version_id" value="false"/>
+<setting id="cleanup.add_missing_deprecated_annotations" value="true"/>
+<setting id="cleanup.remove_unnecessary_casts" value="true"/>
+<setting id="cleanup.remove_trailing_whitespaces_ignore_empty" value="false"/>
+<setting id="cleanup.always_use_this_for_non_static_method_access" value="false"/>
+<setting id="cleanup.remove_unnecessary_nls_tags" value="true"/>
+</profile>
+</profiles>
\ No newline at end of file
diff --git a/trunk/etc/eclipse/shindig-eclipse-javascript-codestyle.xml b/trunk/etc/eclipse/shindig-eclipse-javascript-codestyle.xml
new file mode 100644
index 0000000..3d3fa75
--- /dev/null
+++ b/trunk/etc/eclipse/shindig-eclipse-javascript-codestyle.xml
@@ -0,0 +1,290 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<profiles version="11">
+<profile kind="CodeFormatterProfile" name="shindig-dev" version="11">
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_empty_lines" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_compact_if" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_annotation" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_header" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_block_comments" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_object_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_between_type_declarations" value="0"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_assignment" value="0"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.tabulation.size" value="2"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.align_type_members_on_columns" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.continuation_indentation_for_objlit_initializer" value="4"/>
+<setting id="org.eclipse.wst.jsdt.core.compiler.compliance" value="1.5"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_closing_brace_in_objlit_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_source_code" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_switchstatements_compare_to_switch" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.wrap_before_binary_operator" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.continuation_indentation_for_array_initializer" value="4"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_after_package" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_comma_in_objlit_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_enum_constant" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.compiler.problem.enumIdentifier" value="error"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_javadoc_comments" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indentation.size" value="2"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_type_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_objlit_initializer" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.compiler.source" value="1.5"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.continuation_indentation" value="4"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_conditional_expression" value="80"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.indent_parameter_description" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_after_imports" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.indent_root_tags" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_package" value="0"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_member_type" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_enum_constants" value="0"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_imports" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_binary_expression" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.lineSplit" value="100"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_html" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_method_body" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.compiler.codegen.targetPlatform" value="1.5"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.keep_empty_objlit_initializer_on_one_line" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.format_line_comments" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_in_empty_block" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.compiler.problem.assertIdentifier" value="error"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_colon_in_object_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.line_length" value="100"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_block" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_multiple_fields" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.tabulation.char" value="space"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_between_import_groups" value="1"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_field" value="0"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_after_opening_brace_in_objlit_initializer" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.compact_else_if" value="true"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
+<setting id="org.eclipse.wst.jsdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
+</profile>
+</profiles>
diff --git a/trunk/etc/eclipse/shindig-eclipse-javascript-codetemplate.xml b/trunk/etc/eclipse/shindig-eclipse-javascript-codetemplate.xml
new file mode 100644
index 0000000..dec66a0
--- /dev/null
+++ b/trunk/etc/eclipse/shindig-eclipse-javascript-codetemplate.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?><templates><template autoinsert="false" context="org.eclipse.jsdt.newtype_context" deleted="false" description="Newly created files" enabled="true" id="org.eclipse.wst.jsdt.ui.text.codetemplates.newtype" name="newtype">/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+${filecomment}
+${package_declaration}
+
+/**
+ * @author &lt;a href="mailto:dev@shindig.apache.org"&gt;Shindig Dev&lt;/a&gt;
+ */
+${typecomment}
+${type_declaration}</template></templates>
\ No newline at end of file
diff --git a/trunk/etc/eclipse/shindig.importorder b/trunk/etc/eclipse/shindig.importorder
new file mode 100644
index 0000000..c600de0
--- /dev/null
+++ b/trunk/etc/eclipse/shindig.importorder
@@ -0,0 +1,11 @@
+#Organize Import Order
+#Mon May 05 17:33:38 PDT 2008
+8=javax
+7=java
+6=org
+5=org.apache.abdera
+4=org.apache.shindig
+3=net
+2=junit
+1=com
+0=com.google
diff --git a/trunk/etc/run-gjslint b/trunk/etc/run-gjslint
new file mode 100755
index 0000000..6957523
--- /dev/null
+++ b/trunk/etc/run-gjslint
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+gjslint --strict --unix_mode --exclude_directories features/src/main/javascript/features/swfobject,features/src/main/javascript/features/i18n --recurse features/src/main/javascript/features 
diff --git a/trunk/etc/set_svn_properties.sh b/trunk/etc/set_svn_properties.sh
new file mode 100755
index 0000000..7d7ce93
--- /dev/null
+++ b/trunk/etc/set_svn_properties.sh
@@ -0,0 +1,127 @@
+#!/bin/bash
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# This script will set the proper svn properties on all the files in the tree
+# It pretty much requires a gnu compatible xargs (for the -r flag).  Running
+# on Linux is probably the best option or on Windows with cygwin.
+
+# Note: use the following line if you want to remove svn:keywords
+#for ext in java php xml xsl xsd wsdl properties txt htm* css js ; do find . -path '*/.svn' -prune -o  -name "*.$ext" -print0 | grep -v '.svn' | xargs -0  -r  svn propdel  svn:keywords ; done
+
+# Note: use the following line to automatically apply svn ignore 
+#svn propset svn:ignore -F etc/svn-ignores .
+#svn propset svn:ignore -F etc/svn-ignores features
+#svn propset svn:ignore -F etc/svn-ignores java
+#svn propset svn:ignore -F etc/svn-ignores java/common
+#svn propset svn:ignore -F etc/svn-ignores java/gadgets
+#svn propset svn:ignore -F etc/svn-ignores java/social-api
+#svn propset svn:ignore -F etc/svn-ignores java/server
+#svn propset svn:ignore -F etc/svn-ignores java/samples
+#svn propset svn:ignore -F etc/svn-ignores php
+
+# Language files
+find . -name "*.java" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+
+find . -name "*.php" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+
+find . -name "*.properties" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.properties" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+# XML files
+find . -name "*.xml" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.xml" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/xml
+
+find . -name "*.xsl" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.xsl" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/xml
+
+find . -name "*.xsd" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.xsd" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/xml
+
+find . -name "*.wsdl" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.wsdl" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/xml
+
+find . -name "*.wsdd" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.wsdd" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/xml
+
+# HTML files
+find . -name "*.htm*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.htm*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/html
+
+find . -name "*.css" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.css" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/css
+
+find . -name "*.js" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.js" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/javascript
+
+# Image files
+find . -name "*.png" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type image/png
+find . -name "*.gif" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type image/gif
+find . -name "*.jpg" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type image/jpeg
+find . -name "*.jpeg" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type image/jpeg
+
+# Executable files
+find . -name "*.sh" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.sh" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+find . -name "*.sh" | grep -v '.svn' | xargs -n 1 svn propset svn:executable ""
+
+find . -name "*.bat" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.bat" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+find . -name "*.bat" | grep -v '.svn' | xargs -n 1 svn propset svn:executable ""
+
+find . -name "*.cmd" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.cmd" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+find . -name "*.cmd" | grep -v '.svn' | xargs -n 1 svn propset svn:executable ""
+
+# Maven site files
+find . -name "*.apt" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.apt" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "*.fml" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.fml" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/xml
+
+find . -name "*.xdoc" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.xdoc" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/xml
+
+# Other files
+find . -name "*.txt" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "*.txt" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "README*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "README*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "LICENSE*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "LICENSE*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "NOTICE*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "NOTICE*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "KEYS*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "KEYS*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "INSTALL*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "INSTALL*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "UPGRADING*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "UPGRADING*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "COMMITTERS*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "COMMITTERS*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
+
+find . -name "BUILD-JAVA*" | grep -v '.svn' | xargs -n 1 svn propset svn:eol-style native
+find . -name "BUILD-JAVA*" | grep -v '.svn' | xargs -n 1 svn propset svn:mime-type text/plain
diff --git a/trunk/etc/svn-ignores b/trunk/etc/svn-ignores
new file mode 100644
index 0000000..917691c
--- /dev/null
+++ b/trunk/etc/svn-ignores
@@ -0,0 +1,22 @@
+target
+work
+dojo
+*.iws
+*.ipr
+*.iml
+derby.log
+maven.log
+build.xml
+build-dependency.xml
+velocity.log*
+junit*.properties
+surefire*.properties
+.project
+.classpath
+.settings
+.deployables
+.wtpmodules
+.externalToolBuilders
+create.sql
+drop.sql
+derby.log
diff --git a/trunk/etc/svn-props b/trunk/etc/svn-props
new file mode 100644
index 0000000..7b9805f
--- /dev/null
+++ b/trunk/etc/svn-props
@@ -0,0 +1,59 @@
+# Add this stuff at bottom of your local file ~/.subversion/config
+#Note: for Windows this is normally found at
+# C:\Documents and Settings\{username}\Application Data\Subversion\config
+
+[miscellany]
+enable-auto-props = yes
+
+# Note: you may wish to add svn:keywords as well, depending on your project requirements
+# E.g. svn:keywords=Date Author Id Revision HeadURL
+
+# Do not default any files to svn:executable=*.
+# This should only be done on an individual basis as required.
+
+### Section for configuring automatic properties.
+### The format of the entries is:
+###   file-name-pattern = propname[=value][;propname[=value]...]
+### The file-name-pattern can contain wildcards (such as '*' and
+### '?').  All entries which match will be applied to the file.
+### Note that auto-props functionality must be enabled, which
+### is typically done by setting the 'enable-auto-props' option.
+[auto-props]
+INSTALL* = svn:eol-style=native;svn:mime-type=text/plain
+KEYS* = svn:eol-style=native;svn:mime-type=text/plain
+README* = svn:eol-style=native;svn:mime-type=text/plain
+LICENSE* = svn:eol-style=native;svn:mime-type=text/plain
+NOTICE* = svn:eol-style=native;svn:mime-type=text/plain
+TODO* = svn:eol-style=native;svn:mime-type=text/plain
+WHATSNEW* = svn:eol-style=native;svn:mime-type=text/plain
+NEWS* = svn:eol-style=native;svn:mime-type=text/plain
+COPYING* = svn:eol-style=native;svn:mime-type=text/plain
+DISCLAIMER* = svn:eol-style=native;svn:mime-type=text/plain
+Makefile = svn:eol-style=native;svn:mime-type=text/plain
+ChangeLog = svn:eol-style=native;svn:mime-type=text/plain
+*.c = svn:eol-style=native
+*.cpp = svn:eol-style=native
+*.h = svn:eol-style=native
+*.dsp = svn:eol-style=CRLF
+*.dsw = svn:eol-style=CRLF
+*.sh = svn:eol-style=native;svn:executable
+*.png = svn:mime-type=image/png
+*.jpg = svn:mime-type=image/jpeg
+*.gif = svn:mime-type=image/gif
+*.java = svn:eol-style=native
+*.php = svn:eol-style=native
+*.xml = svn:mime-type=text/xml;svn:eol-style=native
+*.xsl = svn:mime-type=text/xml;svn:eol-style=native
+*.xsd = svn:mime-type=text/xml;svn:eol-style=native
+*.wsdl = svn:mime-type=text/xml;svn:eol-style=native
+*.properties = svn:mime-type=text/plain;svn:eol-style=native
+*.txt = svn:eol-style=native;svn:mime-type=text/plain
+*.htm* = svn:eol-style=native;svn:mime-type=text/html
+*.bat = svn:eol-style=native
+*.pl = svn:eol-style=native
+*.py = svn:eol-style=native
+*.cmd = svn:eol-style=native
+*.css = svn:eol-style=native
+*.js = svn:eol-style=native
+*.wsdd = svn:mime-type=text/xml;svn:eol-style=native
+*.fragment = svn:eol-style=native
diff --git a/trunk/etc/to-committers.xsl b/trunk/etc/to-committers.xsl
new file mode 100644
index 0000000..f68d1e3
--- /dev/null
+++ b/trunk/etc/to-committers.xsl
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+
+<!-- ====================================================================== -->
+<!-- XSL to extract developers from a Maven pom.xml                         -->
+<!-- ====================================================================== -->
+<xsl:stylesheet version="1.1"
+    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+    xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:mvn="http://maven.apache.org/POM/4.0.0" exclude-result-prefixes="mvn">
+
+  <xsl:output method="text" encoding="UTF-8"/>
+
+  <xsl:template match="/">
+    <xsl:text>The following people have commit access to the Shindig sources.
+Note that this is not a full list of Shindig's authors, however --
+for that, you'd need to look over the log messages to see all the
+patch contributors.
+
+If you have a question or comment, it's probably best to mail
+dev@shindig.apache.org, rather than mailing any of these
+people directly.
+
+Blanket commit access:&#xa;</xsl:text>
+    <xsl:apply-templates select="mvn:project/mvn:developers/mvn:developer" />
+  </xsl:template>
+
+  <xsl:template match="mvn:developer">
+    <xsl:text>&#xa;&#x9;</xsl:text>
+    <xsl:call-template name="leftPad">
+      <xsl:with-param name="input" select="mvn:id"/>
+      <xsl:with-param name="maxSize" select="number(10)"/>
+    </xsl:call-template>
+    <xsl:text>&#x9;</xsl:text>
+    <xsl:call-template name="rightPad">
+      <xsl:with-param name="input" select="mvn:name"/>
+      <xsl:with-param name="maxSize" select="number(20)"/>
+    </xsl:call-template>
+    <xsl:call-template name="rightPad">
+      <xsl:with-param name="input" select="mvn:email"/>
+      <xsl:with-param name="maxSize" select="number(25)"/>
+    </xsl:call-template>
+    <xsl:call-template name="rightPad">
+      <xsl:with-param name="input" select="normalize-space(mvn:roles)"/>
+      <xsl:with-param name="maxSize" select="number(10)"/>
+    </xsl:call-template>
+  </xsl:template>
+
+  <!-- String Utilities -->
+
+  <xsl:template name="rightPad">
+    <xsl:param name="input" select="string('')"/>
+    <xsl:param name="maxSize" select="0"/>
+    <xsl:variable name="diff" select="$maxSize - string-length($input)" />
+    <xsl:choose>
+      <xsl:when test="$diff &lt; 0" >
+        <xsl:value-of select="$input"/>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:value-of select="$input"/>
+        <xsl:call-template name="space"><xsl:with-param name="repeat" select="$diff"/></xsl:call-template>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+
+  <xsl:template name="space">
+    <xsl:param name="repeat">0</xsl:param>
+    <xsl:param name="fillchar" select="' '"/>
+    <xsl:if test="number($repeat) >= 1">
+      <xsl:call-template name="space">
+        <xsl:with-param name="repeat" select="$repeat - 1"/>
+        <xsl:with-param name="fillchar" select="$fillchar"/>
+      </xsl:call-template>
+      <xsl:value-of select="$fillchar"/>
+    </xsl:if>
+  </xsl:template>
+
+  <xsl:template name="leftPad">
+    <xsl:param name="input" select="string('')"/>
+    <xsl:param name="maxSize" select="0"/>
+    <xsl:variable name="diff" select="$maxSize - string-length($input)" />
+    <xsl:choose>
+      <xsl:when test="$diff &lt; 0" >
+        <xsl:value-of select="$input"/>
+      </xsl:when>
+      <xsl:otherwise>
+        <xsl:call-template name="space"><xsl:with-param name="repeat" select="$diff"/></xsl:call-template>
+        <xsl:value-of select="$input"/>
+      </xsl:otherwise>
+    </xsl:choose>
+  </xsl:template>
+</xsl:stylesheet>
\ No newline at end of file
diff --git a/trunk/extras/pom.xml b/trunk/extras/pom.xml
new file mode 100644
index 0000000..ded9656
--- /dev/null
+++ b/trunk/extras/pom.xml
@@ -0,0 +1,165 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-extras</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Apache Shindig Extra Modules</name>
+  <description>Provides extra, deprecated or extended functionality. The items here have unstable APIs and could change at any time.</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/extras</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/extras</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/extras</url>
+  </scm>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>${basedir}/src/main/javascript</directory>
+        <includes>
+          <include>**/*</include>
+        </includes>
+      </resource>
+    </resources>
+    <pluginManagement>
+      <!-- set versions of common plugins for reproducibility, ordered alphabetically by owner -->
+      <plugins>
+        <!-- Misc -->
+        <plugin>
+          <groupId>de.berlios.jsunit</groupId>
+          <artifactId>jsunit-maven2-plugin</artifactId>
+          <version>1.3</version>
+          <dependencies>
+            <dependency>
+                <groupId>rhino</groupId>
+                <artifactId>js</artifactId>
+                <version>1.7R1</version>
+            </dependency>
+          </dependencies>
+        </plugin>
+		    <plugin>
+		      <groupId>org.eclipse.m2e</groupId>
+		      <artifactId>lifecycle-mapping</artifactId>
+		      <version>1.0.0</version>
+		      <configuration>
+		        <lifecycleMappingMetadata>
+		          <pluginExecutions>
+		            <pluginExecution>
+		              <pluginExecutionFilter>
+		                <groupId>net.alchim31.maven</groupId>
+		                <artifactId>yuicompressor-maven-plugin</artifactId>
+		                <versionRange>[0.0.0,)</versionRange>
+		                <goals>
+		                  <goal>compress</goal>
+		                </goals>
+		              </pluginExecutionFilter>
+		              <action>
+		                <ignore />
+		              </action>
+		            </pluginExecution>
+		          </pluginExecutions>
+		        </lifecycleMappingMetadata>
+		      </configuration>
+		    </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <!-- TODO: Replace this with the more generic javascript plugin that
+          allows the use of arbitrary compressor / compilers.
+          The maven-javascript-plugin does not seem to work.
+        -->
+        <!-- <groupId>net.sf.hammerfest</groupId> -->
+        <!-- <artifactId>maven-javascript-plugin</artifactId> -->
+        <groupId>net.alchim31.maven</groupId>
+        <artifactId>yuicompressor-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>compress</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <suffix>.opt</suffix>
+          <excludes>
+            <exclude>**/*.xml</exclude>
+            <exclude>**/swfobject/*.js</exclude>
+          </excludes>
+          <jswarn>false</jswarn>
+          <statistics>false</statistics>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <dependencies>
+    <!-- project dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-gadgets</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-social-api</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+    </dependency>
+    <!-- external depenencies -->
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-multibindings</artifactId>
+    </dependency>
+
+    <!-- test -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <version>${project.parent.version}</version>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-social-api</artifactId>
+      <version>${project.parent.version}</version>
+      <classifier>tests</classifier>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/extras/src/main/appended-resources/META-INF/LICENSE b/trunk/extras/src/main/appended-resources/META-INF/LICENSE
new file mode 100644
index 0000000..31097bd
--- /dev/null
+++ b/trunk/extras/src/main/appended-resources/META-INF/LICENSE
@@ -0,0 +1,36 @@
+===============================================================================
+
+The Apache Shindig distribution includes a number of subcomponents
+with separate copyright notices and license terms. Your use of the
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+===============================================================================
+swfobject:
+
+The MIT License
+
+Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
+
+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.
+
+
+ Marijn Haverbeke
+ marijnh@gmail.com
+
diff --git a/trunk/extras/src/main/appended-resources/META-INF/NOTICE b/trunk/extras/src/main/appended-resources/META-INF/NOTICE
new file mode 100644
index 0000000..86610a4
--- /dev/null
+++ b/trunk/extras/src/main/appended-resources/META-INF/NOTICE
@@ -0,0 +1,2 @@
+This product includes software (wave) developed by Google, Inc
+Copyright 2010 Google Inc.
diff --git a/trunk/extras/src/main/java/org/apache/shindig/extras/ShindigExtrasGuiceModule.java b/trunk/extras/src/main/java/org/apache/shindig/extras/ShindigExtrasGuiceModule.java
new file mode 100644
index 0000000..3e48d6d
--- /dev/null
+++ b/trunk/extras/src/main/java/org/apache/shindig/extras/ShindigExtrasGuiceModule.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.extras;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Names;
+
+/**
+ * Configures the Extra modules in shindig-extras.
+ */
+public class ShindigExtrasGuiceModule extends AbstractModule {
+  /** {@inheritDoc} */
+  @Override
+  protected void configure() {
+    configureExtraFeatures();
+  }
+
+  /**
+   * Adds the features-extras directory to the search path
+   */
+  protected void configureExtraFeatures() {
+    // This is how you add search paths for features.
+    Multibinder<String> featureBinder = Multibinder.newSetBinder(binder(), String.class, Names.named("org.apache.shindig.features-extended"));
+    featureBinder.addBinding().toInstance("res://features-extras/features.txt");
+  }
+}
diff --git a/trunk/extras/src/main/javascript/features-extras/analytics/feature.xml b/trunk/extras/src/main/javascript/features-extras/analytics/feature.xml
new file mode 100644
index 0000000..b253f27
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/analytics/feature.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>analytics</name>
+  <dependency>core.legacy</dependency>
+  <gadget>
+    <script inline="true" src="http://www.google-analytics.com/urchin.js"/>
+    <script inline="true" src="http://www.google.com/ig/lib/libanalytics.js"/>
+  </gadget>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/analytics.js b/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/analytics.js
new file mode 100644
index 0000000..d3c20ec
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/analytics.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+(function() {
+  gadgets.analytics = function(trackingCode) {
+    this.tracker = _gat._getTracker(trackingCode);
+  };
+
+  gadgets.analytics.prototype.reportPageview = function(path) {
+    this.tracker._trackPageview(path);
+  };
+
+  /**
+   * label and value are optional
+   */
+  gadgets.analytics.prototype.reportEvent = function(name, action, label, value) {
+    this.tracker._trackEvent(name, action, label, value);
+  };
+}());
+
+var _IG_GA = gadgets.analytics;
diff --git a/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/feature.xml b/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/feature.xml
new file mode 100644
index 0000000..f3ec265
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>com.google.gadgets.analytics</name>
+  <dependency>taming</dependency>
+  <dependency>core.legacy</dependency>
+  <gadget>
+    <script inline="true" src="http://www.google.com/ig/lib/libga.js"/>
+    <script src="analytics.js"/>
+    <script src="taming.js" caja="1"/>
+    <exports type="js">_IG_GA</exports>
+    <exports type="js">gadgets.analytics</exports>
+    <exports type="js">gadgets.analytics.prototype.reportPageview</exports>
+  </gadget>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/taming.js b/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/taming.js
new file mode 100644
index 0000000..2ea5a0b
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/com.google.gadgets.analytics/taming.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose Google Analytics API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistCtors([
+    [gadgets, 'analytics', Object]
+  ]);
+  caja___.whitelistMeths([
+    [gadgets.analytics, 'reportPageview'],
+    [gadgets.analytics, 'reportEvent']
+  ]);
+});
diff --git a/trunk/extras/src/main/javascript/features-extras/features.txt b/trunk/extras/src/main/javascript/features-extras/features.txt
new file mode 100644
index 0000000..9e7633b
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/features.txt
@@ -0,0 +1,27 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you 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.
+
+features-extras/analytics/feature.xml
+features-extras/com.google.gadgets.analytics/feature.xml
+features-extras/firebug-lite/feature.xml
+features-extras/org.jquery.core-1.4.2/feature.xml
+features-extras/wave/feature.xml
+features-extras/opensocial-payment/feature.xml
+features-extras/pubsub-2/feature.xml
+features-extras/org.openajax.hub-2.0.7/feature.xml
+features-extras/swfobject/feature.xml
diff --git a/trunk/extras/src/main/javascript/features-extras/firebug-lite/feature.xml b/trunk/extras/src/main/javascript/features-extras/firebug-lite/feature.xml
new file mode 100644
index 0000000..f0eb013
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/firebug-lite/feature.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>firebug-lite</name>
+  <all>
+    <script src="https://getfirebug.com/firebug-lite.js"/>
+  </all>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/billingitem.js b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/billingitem.js
new file mode 100644
index 0000000..c73682c
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/billingitem.js
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Representation of a billing item.
+ ?
+ * @name opensocial.BillingItem
+ */
+
+
+/**
+ * Base interface for billing item objects.
+ *
+ * @param {Map.&lt;opensocial.BillingItem.Field, Object&gt;} params
+ *    Parameters defining the billing item.
+ * @private
+ * @constructor
+ */
+opensocial.BillingItem = function(params) {
+  this.fields_ = params || {};
+  this.fields_[opensocial.BillingItem.Field.COUNT] = 
+      this.fields_[opensocial.BillingItem.Field.COUNT] || 1;
+};
+
+/**
+ * @static
+ * @class
+ * All of the fields that a billing item object can have.
+ *
+ * <p>The SKU_ID and PRINE are required for the request. </p>
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.BillingItem.html#getField">
+ *    opensocial.BillingItem.getField()</a>
+ * </p>
+ *
+ * @name opensocial.Payment.Field
+ */
+opensocial.BillingItem.Field = {
+  /**
+   * @member opensocial.BillingItem.Field
+   */
+  SKU_ID : 'skuId',
+
+  /**
+   * @member opensocial.BillingItem.Field
+   */
+  PRICE : 'price',
+
+  /**
+   * @member opensocial.BillingItem.Field
+   */
+  COUNT : 'count',
+
+  /**
+   * @member opensocial.BillingItem.Field
+   */
+  DESCRIPTION : 'description'
+
+};
+
+
+/**
+ * Gets the billing item field data that's associated with the specified key.
+ *
+ * @param {String} key The key to get data for;
+ *   see the <a href="opensocial.BillingItem.Field.html">Field</a> class
+ * for possible values
+ * @param {Map.&lt;opensocial.DataRequest.DataRequestFields, Object&gt;}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {String} The data
+ * @member opensocial.BillingItem
+ */
+opensocial.BillingItem.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this billing item associated with the given key.
+ *
+ * @param {String} key The key to set data for
+ * @param {String} data The data to set
+ */
+opensocial.BillingItem.prototype.setField = function(key, data) {
+  return this.fields_[key] = data;
+};
+
+
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/container.js b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/container.js
new file mode 100644
index 0000000..ae96565
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/container.js
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+ 
+/**
+ * Requests the container to open a payment processor panel to show and submit
+ * user's order. If the container does not support this method the callback 
+ * will be called with a opensocial.ResponseItem. The response item will have 
+ * its error code set to NOT_IMPLEMENTED.
+ *
+ * @param {opensocial.Payment} payment The Payment object.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The finishing
+ *     callback function.
+ */
+opensocial.Container.prototype.requestPayment = function(payment,
+    opt_callback) {
+  if (opt_callback) {
+    window.setTimeout(function() {
+      opt_callback(new opensocial.ResponseItem(
+          null, payment, opensocial.Payment.ResponseCode.NOT_IMPLEMENTED, 
+          null));
+    }, 0);
+  }
+};
+
+/**
+ * Requests the container to open a payment records processor panel to list all
+ * completed or incomplete payments of the user on current app and allowing 
+ * users to fix the incomplete payments. If the container does not support 
+ * this method the callback will be called with a opensocial.ResponseItem. 
+ * The response item will have its error code set to NOT_IMPLEMENTED.
+ *
+ * @param {function(opensocial.ResponseItem)=} opt_callback The finishing
+ *     callback function.
+ * @param {Object.<opensocial.Payment.RecordsRequestFields, Object>=}
+ *     opt_params Additional parameters to pass to the request. 
+ */
+opensocial.Container.prototype.requestPaymentRecords = function(opt_callback, 
+    opt_params) {
+  if (opt_callback) {
+    window.setTimeout(function() {
+      opt_callback(new opensocial.ResponseItem(
+          null, payment, opensocial.Payment.ResponseCode.NOT_IMPLEMENTED, 
+          null));
+    }, 0);
+  }
+};
+
+
+/**
+ * Creates a payment object.
+ * Creates a payment object.
+ * @param {Map.&lt;opensocial.Payment.Field, Object&gt;} params
+ *     Parameters defining the payment object.
+ * @return {opensocial.Payment} The new
+ *     <a href="opensocial.Payment.html">Payment</a> object
+ * @private
+ */
+opensocial.Container.prototype.newPayment = function(params) {
+  return new opensocial.Payment(params);
+};
+
+
+/**
+ * Creates a billing item object.
+ * @param {Map.&lt;opensocial.BillingItem.Field, Object&gt;} params
+ *     Parameters defining the billing item object.
+ * @return {opensocial.BillingItem} The new
+ *     <a href="opensocial.BillingItem.html">BillingItem</a> object
+ * @private
+ */
+opensocial.Container.prototype.newBillingItem = function(params) {
+  return new opensocial.BillingItem(params);
+};
+
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/feature.xml b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/feature.xml
new file mode 100644
index 0000000..d9bc2f7
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/feature.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>opensocial-payment</name>
+  <dependency>core.io</dependency>
+  <dependency>rpc</dependency>
+  <container>
+    <!-- common -->
+    <script src="billingitem.js"/>
+    <script src="payment.js"/>
+    <script src="jsonpayment.js"/>
+    <script src="opensocial.js"/>
+    <!-- container specific -->
+    <script src="paymentprocessor.js"/>
+    <script src="container.js"/>
+    <script src="jsoncontainer.js"/>
+    <api>
+      <exports type="rpc">shindig.requestPayment_callback</exports>
+      <exports type="rpc">shindig.requestPaymentRecords_callback</exports>
+      <exports type="rpc">shindig.requestPayment</exports>
+      <exports type="rpc">shindig.requestPaymentRecords</exports>
+      <uses type="rpc">shindig.requestPayment</uses>
+      <uses type="rpc">shindig.requestPaymentRecords</uses>
+      <uses type="rpc">shindig.requestPayment_callback</uses>
+      <uses type="rpc">shindig.requestPaymentRecords_callback</uses>
+    </api>
+  </container>
+  <gadget>
+    <!-- common -->
+    <script src="billingitem.js"/>
+    <script src="payment.js"/>
+    <script src="jsonpayment.js"/>
+    <script src="opensocial.js"/>
+  </gadget>
+</feature>
+
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/jsoncontainer.js b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/jsoncontainer.js
new file mode 100644
index 0000000..795a19e
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/jsoncontainer.js
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+//TODO - originally done during construction
+// For opensocial virtual currency extension.
+gadgets.rpc.register('shindig.requestPayment_callback',
+    JsonRpcContainer.requestPaymentCallback_);
+// For opensocial virtual currency extension.
+gadgets.rpc.register('shindig.requestPaymentRecords_callback',
+    JsonRpcContainer.requestPaymentRecordsCallback_);
+
+/**
+ * For OpenSocial VirtualCurrency Ext.
+ * The function invokes the whole process of a payment request. It calls the
+ * payment processor open function in parent container.
+ *
+ * @param {opensocial.Payment} payment The Payment object.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The finishing
+ *     callback function.
+ * @private
+ */
+JsonRpcContainer.prototype.requestPayment = function(payment, opt_callback) {
+  if (!payment) {
+    if (opt_callback) {
+      opt_callback(new opensocial.ResponseItem(null, payment, 
+        opensocial.Payment.ResponseCode.MALFORMED_REQUEST, 
+        'Payment object is undefined.'));
+    }
+    return;
+  }
+ 
+  var callbackId = "cId_" + Math.random();
+  callbackIdStore[callbackId] = opt_callback;
+  // The rpc target is registered in container payment processor page.
+  gadgets.rpc.call('..', 'shindig.requestPayment',
+      null,
+      callbackId,
+      payment.toJsonObject());
+};
+
+/**
+ * For OpenSocial VirtualCurrency Ext. The callback function of receives the
+ * returned results from the parent container.
+ *
+ * @param {Object.<string, Object>} paymentJson A jsonpayment object with
+ *     parameters filled. 
+ * @private
+ */
+JsonRpcContainer.requestPaymentCallback_ = function(callbackId, paymentJson) {
+  callback = callbackIdStore[callbackId];
+  if (callback) {
+    var errorCode = opensocial.Payment.ResponseCode[
+        paymentJson[opensocial.Payment.Field.RESPONSE_CODE]];
+    var message = paymentJson[opensocial.Payment.Field.RESPONSE_MESSAGE];
+
+    paymentJson[opensocial.Payment.Field.RESPONSE_CODE] = errorCode;
+    var payment = new JsonPayment(paymentJson, false);
+    var responseItem = new opensocial.ResponseItem(
+        null,
+        payment,
+        (errorCode == opensocial.Payment.ResponseCode.OK ? null : errorCode),
+        message);
+    callback(responseItem);
+  }
+};
+
+/**
+ * For OpenSocial VirtualCurrency Ext.
+ * The function invokes the payment records panel in parent container.
+ *
+ * @param {function(opensocial.ResponseItem)=} opt_callback The finishing
+ *     callback function.
+ * @param {Object.<pensocial.Payment.RecordsRequestFields, Object>=}
+ *     opt_params Additional parameters to pass to the request. 
+ * @private
+ */
+JsonRpcContainer.prototype.requestPaymentRecords = function(opt_callback, opt_params) {
+  var callbackId = "cId_" + Math.random();
+  callbackIdStore[callbackId] = opt_callback;
+
+  // The rpc target is registered in container payment records page.  
+  gadgets.rpc.call('..', 'shindig.requestPaymentRecords',
+      null, callbackId, opt_params);
+};
+
+/**
+ * For OpenSocial VirtualCurrency Ext. The callback function of receives the
+ * returned results from the parent container.
+ *
+ * @param {Object.<string, Object>} opt_resultParams The fields set with
+ *     result parameters.
+ * @private
+ */
+JsonRpcContainer.requestPaymentRecordsCallback_ = function(callbackId, recordsJson) {
+  callback = callbackIdStore[callbackId];
+  if (callback) {
+    var errorCode = opensocial.Payment.ResponseCode[
+        recordsJson[opensocial.Payment.Field.RESPONSE_CODE]];
+    var message = recordsJson[opensocial.Payment.Field.RESPONSE_MESSAGE];
+    var records = [];
+    var payments = recordsJson['payments'];
+    for (var orderId in payments) {
+      records.push(new JsonPayment(payments[orderId], false));
+    }
+    
+    var responseItem = new opensocial.ResponseItem(
+        null,
+        records, 
+        (errorCode == opensocial.Payment.ResponseCode.OK ? null : errorCode), message);
+    callback(responseItem);
+  }
+};
+ 
+ 
+JsonRpcContainer.prototype.newPayment = function(opt_params) {
+  return new JsonPayment(opt_params, true);
+};
+
+JsonRpcContainer.prototype.newBillingItem = function(opt_params) {
+  return new JsonBillingItem(opt_params);
+};
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/jsonpayment.js b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/jsonpayment.js
new file mode 100644
index 0000000..3cd01af
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/jsonpayment.js
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * Base interface for json based payment objects.
+ * NOTE: This class is mainly copied from jsonactivity.js
+ *
+ * @private
+ * @constructor
+ */
+var JsonPayment = function(opt_params, opt_skipConversions) {
+  opt_params = opt_params || {};
+  if (!opt_skipConversions) {
+    JsonPayment.constructArrayObject(opt_params, 'items', JsonBillingItem);
+  }
+  opensocial.Payment.call(this, opt_params);
+};
+JsonPayment.inherits(opensocial.Payment);
+
+JsonPayment.prototype.toJsonObject = function() {
+  var jsonObject = JsonPayment.copyFields(this.fields_);
+
+  var oldBillingItems = jsonObject['items'] || [];
+  var newBillingItems = [];
+  for (var i = 0; i < oldBillingItems.length; i++) {
+    newBillingItems[i] = oldBillingItems[i].toJsonObject();
+  }
+  jsonObject['items'] = newBillingItems;
+
+  return jsonObject;
+};
+
+
+// TODO: Split into separate class
+var JsonBillingItem = function(opt_params) {
+  opensocial.BillingItem.call(this, opt_params);
+};
+JsonBillingItem.inherits(opensocial.BillingItem);
+
+JsonBillingItem.prototype.toJsonObject = function() {
+  return JsonPayment.copyFields(this.fields_);
+};
+
+
+// TODO: Pull this method into a common class, it is from jsonperson.js
+JsonPayment.constructArrayObject = function(map, fieldName, className) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    for (var i = 0; i < fieldValue.length; i++) {
+      fieldValue[i] = new className(fieldValue[i]);
+    }
+  }
+};
+
+// TODO: Pull into common class as well
+JsonPayment.copyFields = function(oldObject) {
+  var newObject = {};
+  for (var field in oldObject) {
+    newObject[field] = oldObject[field];
+  }
+  return newObject;
+};
+
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/opensocial.js b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/opensocial.js
new file mode 100644
index 0000000..c9961f6
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/opensocial.js
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * Requests the container to open a payment processor panel to show and submit
+ * user's order. If the container does not support this method the callback 
+ * will be called with a opensocial.ResponseItem. The response item will have 
+ * its error code set to NOT_IMPLEMENTED.
+ *
+ * @param {opensocial.Payment} payment The Payment object.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The finishing
+ *     callback function.
+ */
+opensocial.requestPayment = function(payment, opt_callback) {
+  opensocial.Container.get().requestPayment(payment, opt_callback);
+};
+
+/**
+ * Requests the container to open a payment records processor panel to list all
+ * completed or incomplete payments of the user on current app and allowing 
+ * users to fix the incomplete payments. If the container does not support 
+ * this method the callback will be called with a opensocial.ResponseItem.
+ * The response item will have its error code set to NOT_IMPLEMENTED.
+ *
+ * @param {function(opensocial.ResponseItem)=} opt_callback The finishing
+ *     callback function.
+ * @param {Object.<opensocial.Payment.RecordsRequestFields, Object>=}
+ *     opt_params Additional parameters to pass to the request.
+ */
+opensocial.requestPaymentRecords = function(opt_callback, opt_params) {
+  opensocial.Container.get().requestPaymentRecords(opt_callback, opt_params);
+};
+
+
+/**
+ * Creates a payment object.
+ *
+ * @param {Object.<opensocial.Payment.Field, Object>} params
+ *    Parameters defining the payment object.
+ * @return {opensocial.Payment} The new
+ *     <a href="opensocial.Payment.html">Payment</a> object
+ * @member opensocial
+ */
+opensocial.newPayment = function(params) {
+  return opensocial.Container.get().newPayment(params);
+};
+
+
+/**
+ * Creates a billing item object.
+ *
+ * @param {Object.<opensocial.BillingItem.Field, Object>} params
+ *    Parameters defining the billing item object.
+ * @return {opensocial.BillingItem} The new
+ *     <a href="opensocial.BillingItem.html">BillingItem</a> object
+ * @member opensocial
+ */
+opensocial.newBillingItem = function(params) {
+  return opensocial.Container.get().newBillingItem(params);
+};
+
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/payment.js b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/payment.js
new file mode 100644
index 0000000..b156930
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/payment.js
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Representation of a payment.
+ ?
+ * @name opensocial.Payment
+ */
+
+
+/**
+ * Base interface for all payment objects.
+ *
+ * @param {Object.<opensocial.Payment.Field, Object>} params
+ *    Parameters defining the payment.
+ * @private
+ * @constructor
+ */
+opensocial.Payment = function(params) {
+  this.fields_ = params || {};
+  this.fields_[opensocial.Payment.Field.PAYMENT_TYPE] = 
+      this.fields_[opensocial.Payment.Field.PAYMENT_TYPE] || 
+      opensocial.Payment.PaymentType.PAYMENT;
+};
+
+
+opensocial.Payment.prototype.isPayment = function() {
+  return this.fields_[opensocial.Payment.Field.PAYMENT_TYPE] == 
+      opensocial.Payment.PaymentType.PAYMENT;
+};
+
+opensocial.Payment.prototype.isCredit = function() {
+  return this.fields_[opensocial.Payment.Field.PAYMENT_TYPE] == 
+      opensocial.Payment.PaymentType.CREDIT;
+};
+
+opensocial.Payment.prototype.isComplete = function() {
+  return !!this.fields_[opensocial.Payment.Field.PAYMENT_COMPLETE];
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that a payment object can have.
+ *
+ * <p>The ITEMS, AMOUNT, MESSAGE, PARAMETERS are required for the request. </p>
+ *
+ * <p>And the ORDER_ID, ORDERED_TIME, SUBMITTED_TIME, EXECUTED_TIME fields 
+ * will be filled during the procedure and return to the app. </p>
+ * <p>
+ * <b>See also:</b>
+ * <a
+ * href="opensocial.Payment.html#getField">opensocial.Payment.getField()</a>
+ * </p>
+ *
+ * @name opensocial.Payment.Field
+ */
+opensocial.Payment.Field = {
+  /**
+   * @member opensocial.Payment.Field
+   */
+  SANDBOX : 'sandbox',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  ITEMS : 'items',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  AMOUNT : 'amount',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  MESSAGE : 'message',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  PARAMETERS : 'parameters',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  PAYMENT_TYPE : 'paymentType',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  ORDER_ID : 'orderId',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  ORDERED_TIME : 'orderedTime',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  SUBMITTED_TIME : 'submittedTime',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  EXECUTED_TIME : 'executedTime',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  RESPONSE_CODE : 'responseCode',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  RESPONSE_MESSAGE : 'responseMessage',
+
+  /**
+   * @member opensocial.Payment.Field
+   */
+  PAYMENT_COMPLETE : 'paymentComplete'
+
+};
+
+
+/**
+ * Gets the payment field data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *   see the <a href="opensocial.Payment.Field.html">Field</a> class
+ * for possible values
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>=}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data
+ * @member opensocial.Payment
+ */
+opensocial.Payment.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this payment associated with the given key.
+ *
+ * @param {string} key The key to set data for
+ * @param {string} data The data to set
+ */
+opensocial.Payment.prototype.setField = function(key, data) {
+  return this.fields_[key] = data;
+};
+
+
+/**
+ * @static
+ * @class
+ * Types for a payment.
+ *
+ * @name opensocial.Payment.PaymentType
+ */
+opensocial.Payment.PaymentType = {
+  /**
+   * @member opensocial.Payment.PaymentType
+   */
+  PAYMENT : 'payment',
+
+  /**
+   * @member opensocial.Payment.PaymentType
+   */
+  CREDIT : 'credit'
+};
+
+
+/**
+ * @static
+ * @class
+ * Possible response codes for the whole payment process.
+ *
+ * @name opensocial.Payment.ResponseCode
+ */
+opensocial.Payment.ResponseCode = {
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  APP_LOGIC_ERROR : 'appLogicError',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  APP_NETWORK_FAILURE : 'appNetworkFailure',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  INSUFFICIENT_MONEY : 'insufficientMoney',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  INVALID_TOKEN : 'invalidToken',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  MALFORMED_REQUEST : 'malformedRequest',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  NOT_IMPLEMENTED : 'notImplemented',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  OK : 'ok',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  PAYMENT_ERROR : 'paymentError',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  PAYMENT_PROCESSOR_ALREADY_OPENED : 'paymentProcessorAlreadyOpened',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  UNKNOWN_ERROR : 'unknownError',
+
+  /**
+   * @member opensocial.Payment.ResponseCode
+   */
+  USER_CANCELLED : 'userCancelled'
+};
+
+
+/**
+ * @static
+ * @class
+ * Request fields for requesting payment records.
+ *
+ * @name opensocial.Payment.RecordsRequestFields
+ */
+opensocial.Payment.RecordsRequestFields = {
+
+  /**
+   * @member opensocial.Payment.RecordsRequestFields
+   */
+  SANDBOX : 'sandbox',
+
+  /**
+   * @member opensocial.Payment.RecordsRequestFields
+   */
+  MAX : 'max',
+
+  /**
+   * @member opensocial.Payment.RecordsRequestFields
+   */
+  INCOMPLETE_ONLY : 'incompleteOnly'
+
+};
+
+
+
+
diff --git a/trunk/extras/src/main/javascript/features-extras/opensocial-payment/paymentprocessor.js b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/paymentprocessor.js
new file mode 100644
index 0000000..d43f2de
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/opensocial-payment/paymentprocessor.js
@@ -0,0 +1,403 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side codes as a processor logic for the virtual 
+ * currency payment functionality.
+ */
+
+var shindig = shindig || {};
+
+
+/**
+ * @static
+ * @class Provides the virtual currency payment processor features on 
+          container side. Handles the payment request from app, prompts the 
+          container processor page for user to confirm the payment, and 
+          passes the response back to the app. The container need to implement 
+          the open/close event functions to fulfill the functionality.
+ * @name shindig.paymentprocessor
+ */
+shindig.paymentprocessor = (function() {
+  /**
+   * The state indicating if the processor panel is on or off.
+   * @type {boolean}
+   */
+  var isOpened_ = false;
+
+  /**
+   * A set of params for the procedure that holds necessary data needed for container 
+   * processor panel page. In the implementation of the processor panel page, you can use 
+   * <code>getParam</code> function and <code>setParam</code> to access the values in this set. Here
+   * <paymentJson> is the pure json format of an opensocial.Payment object defined on gadget side.
+   * E.g.  getParam('payment.orderId') returns the orderId field in paymentJson.
+   *
+   * Here lists the preset data of params set:
+   * 
+   * {
+   *   frameId : <string>,
+   *   appTitle : <string>,
+   *   appSpec : <string>,
+   *   stoken : <string>,
+   *   callbackId : <string>,
+   *   
+   *   payment : <paymentJson>,      // Only for requestPayment process
+   *
+   *   records : {                   // Only for requestPaymentRecords process
+   *     responseCode : <string>,
+   *     responseMessage : <string>,
+   *     payments : {
+   *       <orderId> : <paymentJson>,
+   *       <orderId> : <paymentJson>,
+   *       ...
+   *     }
+   *   },
+   *   reqParams : <object>          // Only for requestPaymentRecords process
+   * }
+   *
+   * @type {Object.<string, Object>}
+   */
+  var processorParams_ = null;
+
+  /**
+   * A set of event functions which allow customizing the UI and actions of the
+   * processor panel by container. They are passed in and registered in the
+   * init functions.
+   * @type {Object.<string, function()>}
+   */
+  var events_ = {};
+
+  /**
+   * Initiates the gadget parameters for the current processor. It uses a frameId
+   * which indicates which gadget is requesting payment. 
+   *
+   * NOTE: The 'shindig-container' feature is required.
+   * @see /features/shindig-container/
+   *
+   * @return {Object.<string, string>} The gadget meta data.
+   */
+  function initGadgetParams(frameId) {
+    var params = null;
+    if (shindig.container && shindig.container.gadgetService) {
+      params = {};
+      params['frameId'] = frameId;
+
+      // By default, will set the title and spec with default value.
+      params['appTitle'] = 'Unknown Title';
+      params['appSpec'] = 'Unknown SpecUrl';
+      // This part need the shindig.container service support or customized by 
+      // container page.
+      var thisGadget = shindig.container.getGadget(
+          shindig.container.gadgetService.getGadgetIdFromModuleId(frameId));
+      if (thisGadget) {
+        params['appTitle'] = thisGadget['title'];
+        params['appSpec'] = thisGadget['specUrl'];
+        params['stoken'] = thisGadget['securityToken'];
+      }
+    }
+    return params;
+  };
+
+
+  /**
+   * Handles the request called via rpc from opensocial.requestPayment on the
+   * app side. Turns on the processor panel.
+   * <p>
+   * The 'this' in this function is the rpc object, thus contains
+   * some information of the app.
+   * </p>
+   * <p>
+   * See the definition of processorParams_ for the structure of the underlying
+   * object.
+   * </p>
+   *
+   * @param {Object.<string, Object>} paymentJson The json object holding the
+   *     payment parameters from the app with ITEMS, AMOUNT, MESSAGE and
+   *     PARAMETERS fields set. Note that this object is serialized and passed
+   *     through RPC channel so all functions are lost.
+   */
+  function openPayment_(callbackId, paymentJson) {
+    // Checks if the processor panel should be opened.
+    if (isOpened_) {
+      // Shouldn't continue if the processor is already opened.
+      paymentJson['responseCode'] = 'PAYMENT_PROCESSOR_ALREADY_OPENED';
+    }
+
+    if (!paymentJson['amount'] || paymentJson['amount'] <= 0) {
+      // TODO: Need more check on the AMOUNT value and other values.
+      paymentJson['responseCode'] = 'MALFORMED_REQUEST';
+    }
+
+    if (!events_['paymentOpen']) {
+      // If the open event handle is not registered, return not-implemented.
+      paymentJson['responseCode'] = 'NOT_IMPLEMENTED';
+    }
+
+    // Initialize the processor parameters.
+    processorParams_ = initGadgetParams(this.f);
+    if (processorParams_ == null) {
+      paymentJson['responseCode'] = 'NOT_IMPLEMENTED';
+    }
+    
+    if (paymentJson['responseCode'] && paymentJson['responseCode'] != 'OK') {
+      // callback immediately if any errorcode exists here.
+      try {
+        gadgets.rpc.call(this.f, 'shindig.requestPayment_callback', null,
+                         callbackId, paymentJson);
+      } finally {
+        return;
+      }
+    }
+
+    isOpened_ = true;
+
+    // Fill the payment fields before the payment process.
+    paymentJson['orderedTime'] = new Date().getTime();
+    paymentJson['message'] = gadgets.util.escapeString(paymentJson['message']);
+
+    processorParams_['callbackId'] = callbackId;
+    processorParams_['payment'] = paymentJson;
+
+    // Call the container's open event to display the processor panel UI.
+    events_.paymentOpen();
+  };
+
+
+  /**
+   * Invoked by button click event in processor panel on container side to 
+   * close the processor panel. Will calls the rpc callback in app.
+   */
+  function closePayment_() {
+    if (!isOpened_) {
+      return;
+    }
+
+    // Call the container's close event to hide the processor panel.
+    // The close event is optional. If not set, do nothing.
+    // (NOTE that the panel is still visible if do nothing...)
+    if (events_.paymentClose) {
+      events_.paymentClose();
+    }
+
+    // Return to the app via rpc.
+    try {
+      gadgets.rpc.call(processorParams_['frameId'], 
+                       'shindig.requestPayment_callback',
+                       null,
+                       processorParams_['callbackId'],
+                       processorParams_['payment']);
+    } catch(e) {
+      // TODO
+    } finally {
+      // Reset the underlying data.
+      isOpened_ = false;
+      processorParams_ = null;
+    }
+  };
+
+
+  /**
+   * Handles the request called via rpc from opensocial.requestPaymentRecords
+   * on the app side. Turns on the processor panel.
+   * <p>
+   * The 'this' in this function is the rpc object, thus contains
+   * some information of the app.
+   * </p>
+   * <p>
+   * See the definition of processorParams_ for the structure of the underlying object.
+   * </p>
+   *
+   * @param {Object.<opensocial.Payment.RecordsRequestFields, Object>} reqParams
+   *     Additional parameters to pass to the request. 
+   */
+  function openPaymentRecords_(callbackId, reqParams) {
+    // This object is for response.
+    var paymentRecordsJson = {'payments' : {}};
+
+    // Checks if the processor panel should be opened.
+    if (isOpened_) {
+      // Shouldn't continue if the processor is already opened.
+      paymentRecordsJson['responseCode'] = 'PAYMENT_PROCESSOR_ALREADY_OPENED';
+    }
+
+    if (!events_['paymentRecordsOpen']) {
+      // If the open event handler is not registered, return not-implemented.
+      paymentRecordsJson['responseCode'] = 'NOT_IMPLEMENTED';
+    }
+
+    if (paymentRecordsJson['responseCode'] && 
+        paymentRecordsJson['responseCode'] != 'OK') {
+      // callback immediately if any errorcode exists here.
+      try {
+        gadgets.rpc.call(this.f, 'shindig.requestPaymentRecords_callback', null,
+                         callbackId, paymentRecordsJson);
+      } finally {
+        return;
+      }
+    }
+
+    isOpened_ = true;
+
+    // Initialize the processor parameters.
+    processorParams_ = initGadgetParams(this.f);
+    processorParams_['callbackId'] = callbackId;
+    processorParams_['records'] = paymentRecordsJson;
+    
+    processorParams_['reqParams'] = reqParams;
+
+    // Call the container's open event to display the processor panel UI.
+    events_['paymentRecordsOpen']();
+  };
+
+
+  /**
+   * Invoked by button click event in processor panel on container side to 
+   * close the processor panel. Will calls the rpc callback in app.
+   */
+  function closePaymentRecords_() {
+    if (!isOpened_) {
+      return;
+    }
+
+    // Call the container's cancel event to do some UI change if needed.
+    if (events_['paymentRecordsClose']) {
+      events_['paymentRecordsClose']();
+    }
+
+    try {
+      // Return to the app via rpc.
+      gadgets.rpc.call(processorParams_['frameId'], 
+                     'shindig.requestPaymentRecords_callback',
+                     null,
+                     processorParams_['callbackId'],
+                     processorParams_['records']);
+    } finally {
+      // Reset the underlying data.
+      isOpened_ = false;
+      processorParams_ = null;
+    }
+
+  };
+
+  /**
+   * Accessor to get the parameter value by a key. The key can be chained
+   * using dot symbol. E.g.  getParam('foo.bar') will return 
+   * processParams_.foo.bar .
+   *
+   * @param {string} key The access key.
+   * @return {Object} The value stored in processParams_ on the given key. 
+   */
+  function getParam_(key) {
+    if (!key) return null;
+    var value = null;
+    try {
+      var arr = key.split('.');
+      if (arr.length > 0) {
+        var prop = processorParams_;
+        for (var i = 0; i < arr.length; i++) {
+          prop = prop[arr[i]];
+        }
+        value = prop;
+      }
+    } catch(e) {
+      value = null;
+    }
+    return value;
+  };
+
+  /**
+   * Accessor to set the parameter value by a key. The key can be chained
+   * using dot symbol. E.g. setParam('foo.bar', value) will set the value on
+   * processParams_.foo.bar .
+   *
+   * @param {string} key The access key.
+   * @param {Object} value The value to be set.
+   */
+  function setParam_(key, value) {
+    if (!key) return;
+    try {
+      var arr = key.split('.');
+      if (arr.length > 1) {
+        var prop = processorParams_;
+        for (var i = 0; i < arr.length - 1; i++) {
+          prop = prop[arr[i]];
+        }
+        prop[arr[arr.length - 1]] = value;
+      }
+    } finally {
+      return;
+    }
+  };
+
+  return /** @scope shindig.paymentprocessor */ {
+    /**
+     * Initializes the 'requestPayment' rpc. It's called by container page in 
+     * onload function.
+     */
+    initPayment : function(openEvent, closeEvent) {
+      events_.paymentOpen = openEvent;
+      events_.paymentClose = closeEvent;
+      gadgets.rpc.register('shindig.requestPayment', openPayment_);
+    },
+
+    /**
+     * Initializes the 'requestPaymentRecords' rpc. It's called by container 
+     * processor page in onload function.
+     */
+    initPaymentRecords : function(openEvent, closeEvent) {
+      events_.paymentRecordsOpen = openEvent;
+      events_.paymentRecordsClose = closeEvent;
+      gadgets.rpc.register('shindig.requestPaymentRecords',
+                           openPaymentRecords_);
+    },
+
+    /**
+     * The open function for 'requestPayment' pannel. Invoked by rpc from app.
+     */
+    openPayment : openPayment_,
+
+    /**
+     * The close function for 'requestPayment' pannel. Invoked by button click
+     * event in processor panel on container side. 
+     */
+    closePayment : closePayment_,
+
+    /**
+     * The open function for 'requestPaymentRecords' pannel. Invoked by rpc 
+     * from app.
+     */
+    openPaymentRecords : openPaymentRecords_,
+
+    /**
+     * The close function for 'requestPaymentRecords' pannel. Invoked by button 
+     * click event in processor panel on container side. 
+     */
+    closePaymentRecords : closePaymentRecords_,
+
+    /**
+     * Exposes the processor parameters to processor panel.
+     */
+    getParam : getParam_,
+
+    /**
+     * Updates the processor parameters from processor panel.
+     */
+    setParam : setParam_
+  };
+
+})();
diff --git a/trunk/extras/src/main/javascript/features-extras/org.jquery.core-1.4.2/feature.xml b/trunk/extras/src/main/javascript/features-extras/org.jquery.core-1.4.2/feature.xml
new file mode 100644
index 0000000..74509e4
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/org.jquery.core-1.4.2/feature.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>org.jquery.core-1.4.2</name>
+  <gadget>
+    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"/>
+  </gadget>
+  <container>
+    <script inline="true" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"/>
+  </container>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/OpenAjax-mashup.js b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/OpenAjax-mashup.js
new file mode 100644
index 0000000..6755062
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/OpenAjax-mashup.js
@@ -0,0 +1,1357 @@
+/*******************************************************************************
+ * OpenAjax-mashup.js
+ *
+ * Reference implementation of the OpenAjax Hub, as specified by OpenAjax Alliance.
+ * Specification is under development at: 
+ *
+ *   http://www.openajax.org/member/wiki/OpenAjax_Hub_Specification
+ *
+ * Copyright 2006-2009 OpenAjax Alliance
+ *
+ * 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.
+ *
+ ******************************************************************************/
+
+var OpenAjax = OpenAjax || {};
+
+if ( !OpenAjax.hub ) {  // prevent re-definition of the OpenAjax.hub object
+
+OpenAjax.hub = function() {
+    var libs = {};
+    var ooh = "org.openajax.hub.";
+
+    return /** @scope OpenAjax.hub */ {
+        implementer: "http://openajax.org",
+        implVersion: "2.0.7",
+        specVersion: "2.0",
+        implExtraData: {},
+        libraries: libs,
+    
+        registerLibrary: function(prefix, nsURL, version, extra) {
+            libs[prefix] = {
+                prefix: prefix,
+                namespaceURI: nsURL,
+                version: version,
+                extraData: extra 
+            };
+            this.publish(ooh+"registerLibrary", libs[prefix]);
+        },
+        
+        unregisterLibrary: function(prefix) {
+            this.publish(ooh+"unregisterLibrary", libs[prefix]);
+            delete libs[prefix];
+        }
+    };
+}();
+
+/**
+ * Error
+ * 
+ * Standard Error names used when the standard functions need to throw Errors.
+ */
+OpenAjax.hub.Error = {
+    // Either a required argument is missing or an invalid argument was provided
+    BadParameters: "OpenAjax.hub.Error.BadParameters",
+    // The specified hub has been disconnected and cannot perform the requested
+    // operation:
+    Disconnected: "OpenAjax.hub.Error.Disconnected",
+    // Container with specified ID already exists:
+    Duplicate: "OpenAjax.hub.Error.Duplicate",
+    // The specified ManagedHub has no such Container (or it has been removed)
+    NoContainer: "OpenAjax.hub.Error.NoContainer",
+    // The specified ManagedHub or Container has no such subscription
+    NoSubscription: "OpenAjax.hub.Error.NoSubscription",
+    // Permission denied by manager's security policy
+    NotAllowed: "OpenAjax.hub.Error.NotAllowed",
+    // Wrong communications protocol identifier provided by Container or HubClient
+    WrongProtocol: "OpenAjax.hub.Error.WrongProtocol",
+    // A 'tunnelURI' param was specified, but current browser does not support security features
+    IncompatBrowser: "OpenAjax.hub.Error.IncompatBrowser"
+};
+
+/**
+ * SecurityAlert
+ * 
+ * Standard codes used when attempted security violations are detected. Unlike
+ * Errors, these codes are not thrown as exceptions but rather passed into the 
+ * SecurityAlertHandler function registered with the Hub instance.
+ */
+OpenAjax.hub.SecurityAlert = {
+    // Container did not load (possible frame phishing attack)
+    LoadTimeout: "OpenAjax.hub.SecurityAlert.LoadTimeout",
+    // Hub suspects a frame phishing attack against the specified container
+    FramePhish: "OpenAjax.hub.SecurityAlert.FramePhish",
+    // Hub detected a message forgery that purports to come to a specified
+    // container
+    ForgedMsg: "OpenAjax.hub.SecurityAlert.ForgedMsg"
+};
+
+/**
+ * Debugging Help
+ *
+ * OpenAjax.hub.enableDebug
+ *
+ *      If OpenAjax.hub.enableDebug is set to true, then the "debugger" keyword
+ *      will get hit whenever a user callback throws an exception, thereby
+ *      bringing up the JavaScript debugger.
+ */
+OpenAjax.hub._debugger = function() {
+//    if ( OpenAjax.hub.enableDebug ) debugger; // REMOVE ON BUILD
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Hub interface
+ * 
+ * Hub is implemented on the manager side by ManagedHub and on the client side
+ * by ClientHub.
+ */
+//OpenAjax.hub.Hub = function() {}
+
+/**
+ * Subscribe to a topic.
+ *
+ * @param {String} topic
+ *     A valid topic string. MAY include wildcards.
+ * @param {Function} onData   
+ *     Callback function that is invoked whenever an event is 
+ *     published on the topic
+ * @param {Object} [scope]
+ *     When onData callback or onComplete callback is invoked,
+ *     the JavaScript "this" keyword refers to this scope object.
+ *     If no scope is provided, default is window.
+ * @param {Function} [onComplete]
+ *     Invoked to tell the client application whether the 
+ *     subscribe operation succeeded or failed. 
+ * @param {*} [subscriberData]
+ *     Client application provides this data, which is handed
+ *     back to the client application in the subscriberData
+ *     parameter of the onData callback function.
+ * 
+ * @returns subscriptionID
+ *     Identifier representing the subscription. This identifier is an 
+ *     arbitrary ID string that is unique within this Hub instance
+ * @type {String}
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.BadParameters} if the topic is invalid (e.g. contains an empty token)
+ */
+//OpenAjax.hub.Hub.prototype.subscribe = function( topic, onData, scope, onComplete, subscriberData ) {}
+
+/**
+ * Publish an event on a topic
+ *
+ * @param {String} topic
+ *     A valid topic string. MUST NOT include wildcards.
+ * @param {*} data
+ *     Valid publishable data. To be portable across different
+ *     Container implementations, this value SHOULD be serializable
+ *     as JSON.
+ *     
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.BadParameters} if the topic cannot be published (e.g. contains 
+ *     wildcards or empty tokens) or if the data cannot be published (e.g. cannot be serialized as JSON)
+ */
+//OpenAjax.hub.Hub.prototype.publish = function( topic, data ) {}
+
+/**
+ * Unsubscribe from a subscription
+ *
+ * @param {String} subscriptionID
+ *     A subscriptionID returned by Hub.subscribe()
+ * @param {Function} [onComplete]
+ *     Callback function invoked when unsubscribe completes
+ * @param {Object} [scope]
+ *     When onComplete callback function is invoked, the JavaScript "this"
+ *     keyword refers to this scope object.
+ *     If no scope is provided, default is window.
+ *     
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.NoSubscription} if no such subscription is found
+ */
+//OpenAjax.hub.Hub.prototype.unsubscribe = function( subscriptionID, onComplete, scope ) {}
+
+/**
+ * Return true if this Hub instance is in the Connected state.
+ * Else returns false.
+ * 
+ * This function can be called even if the Hub is not in a CONNECTED state.
+ * 
+ * @returns Boolean
+ * @type {Boolean}
+ */
+//OpenAjax.hub.Hub.prototype.isConnected = function() {}
+
+/**
+ * Returns the scope associated with this Hub instance and which will be used
+ * with callback functions.
+ * 
+ * This function can be called even if the Hub is not in a CONNECTED state.
+ * 
+ * @returns scope object
+ * @type {Object}
+ */
+//OpenAjax.hub.Hub.prototype.getScope = function() {}
+
+/**
+ * Returns the subscriberData parameter that was provided when 
+ * Hub.subscribe was called.
+ *
+ * @param {String} subscriptionID
+ *     The subscriberID of a subscription
+ * 
+ * @returns subscriberData
+ * @type {*}
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.NoSubscription} if there is no such subscription
+ */
+//OpenAjax.hub.Hub.prototype.getSubscriberData = function(subscriptionID) {}
+
+/**
+ * Returns the scope associated with a specified subscription.  This scope will
+ * be used when invoking the 'onData' callback supplied to Hub.subscribe().
+ *
+ * @param {String} subscriberID
+ *     The subscriberID of a subscription
+ * 
+ * @returns scope
+ * @type {*}
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.NoSubscription} if there is no such subscription
+ */
+//OpenAjax.hub.Hub.prototype.getSubscriberScope = function(subscriberID) {}
+
+/**
+ * Returns the params object associated with this Hub instance.
+ *
+ * @returns params
+ *     The params object associated with this Hub instance
+ * @type {Object}
+ */
+//OpenAjax.hub.Hub.prototype.getParameters = function() {}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * HubClient interface 
+ * 
+ * Extends Hub interface.
+ * 
+ * A HubClient implementation is typically specific to a particular 
+ * implementation of Container.
+ */
+
+/**
+ * Create a new HubClient. All HubClient constructors MUST have this 
+ * signature.
+ * @constructor
+ * 
+ * @param {Object} params 
+ *    Parameters used to instantiate the HubClient.
+ *    Once the constructor is called, the params object belongs to the
+ *    HubClient. The caller MUST not modify it.
+ *    Implementations of HubClient may specify additional properties
+ *    for the params object, besides those identified below. 
+ * 
+ * @param {Function} params.HubClient.onSecurityAlert
+ *     Called when an attempted security breach is thwarted
+ * @param {Object} [params.HubClient.scope]
+ *     Whenever one of the HubClient's callback functions is called,
+ *     references to "this" in the callback will refer to the scope object.
+ *     If not provided, the default is window.
+ * @param {Function} [params.HubClient.log]
+ *     Optional logger function. Would be used to log to console.log or
+ *     equivalent. 
+ *     
+ * @throws {OpenAjax.hub.Error.BadParameters} if any of the required
+ *     parameters is missing, or if a parameter value is invalid in 
+ *     some way.
+ */
+//OpenAjax.hub.HubClient = function( params ) {}
+
+/**
+ * Requests a connection to the ManagedHub, via the Container
+ * associated with this HubClient.
+ * 
+ * If the Container accepts the connection request, the HubClient's 
+ * state is set to CONNECTED and the HubClient invokes the 
+ * onComplete callback function.
+ * 
+ * If the Container refuses the connection request, the HubClient
+ * invokes the onComplete callback function with an error code. 
+ * The error code might, for example, indicate that the Container 
+ * is being destroyed.
+ * 
+ * In most implementations, this function operates asynchronously, 
+ * so the onComplete callback function is the only reliable way to
+ * determine when this function completes and whether it has succeeded
+ * or failed.
+ * 
+ * A client application may call HubClient.disconnect and then call
+ * HubClient.connect.
+ * 
+ * @param {Function} [onComplete]
+ *     Callback function to call when this operation completes.
+ * @param {Object} [scope]  
+ *     When the onComplete function is invoked, the JavaScript "this"
+ *     keyword refers to this scope object.
+ *     If no scope is provided, default is window.
+ *
+ * @throws {OpenAjax.hub.Error.Duplicate} if the HubClient is already connected
+ */
+//OpenAjax.hub.HubClient.prototype.connect = function( onComplete, scope ) {}
+
+/**
+ * Disconnect from the ManagedHub
+ * 
+ * Disconnect immediately:
+ * 
+ * 1. Sets the HubClient's state to DISCONNECTED.
+ * 2. Causes the HubClient to send a Disconnect request to the 
+ * 		associated Container. 
+ * 3. Ensures that the client application will receive no more
+ * 		onData or onComplete callbacks associated with this 
+ * 		connection, except for the disconnect function's own
+ * 		onComplete callback.
+ * 4. Automatically destroys all of the HubClient's subscriptions.
+ *
+ * In most implementations, this function operates asynchronously, 
+ * so the onComplete callback function is the only reliable way to
+ * determine when this function completes and whether it has succeeded
+ * or failed.
+ * 
+ * A client application is allowed to call HubClient.disconnect and 
+ * then call HubClient.connect.
+ * 	
+ * @param {Function} [onComplete]
+ *     Callback function to call when this operation completes.
+ * @param {Object} [scope]  
+ *     When the onComplete function is invoked, the JavaScript "this"
+ *     keyword refers to the scope object.
+ *     If no scope is provided, default is window.
+ *
+ * @throws {OpenAjax.hub.Error.Disconnected} if the HubClient is already
+ *     disconnected
+ */
+//OpenAjax.hub.HubClient.prototype.disconnect = function( onComplete, scope ) {}
+
+/**
+ * If DISCONNECTED: Returns null
+ * If CONNECTED: Returns the origin associated with the window containing the
+ * Container associated with this HubClient instance. The origin has the format
+ *  
+ * [protocol]://[host]
+ * 
+ * where:
+ * 
+ * [protocol] is "http" or "https"
+ * [host] is the hostname of the partner page.
+ * 
+ * @returns Partner's origin
+ * @type {String}
+ */
+//OpenAjax.hub.HubClient.prototype.getPartnerOrigin = function() {}
+
+/**
+ * Returns the client ID of this HubClient
+ *
+ * @returns clientID
+ * @type {String}
+ */
+//OpenAjax.hub.HubClient.prototype.getClientID = function() {}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * OpenAjax.hub.ManagedHub
+ *
+ * Managed hub API for the manager application and for Containers. 
+ * 
+ * Implements OpenAjax.hub.Hub.
+ */
+
+/**
+ * Create a new ManagedHub instance
+ * @constructor
+ *     
+ * This constructor automatically sets the ManagedHub's state to
+ * CONNECTED.
+ * 
+ * @param {Object} params
+ *     Parameters used to instantiate the ManagedHub.
+ *     Once the constructor is called, the params object belongs exclusively to
+ *     the ManagedHub. The caller MUST not modify it.
+ *     
+ * The params object may contain the following properties:
+ * 
+ * @param {Function} params.onPublish
+ *     Callback function that is invoked whenever a 
+ *     data value published by a Container is about
+ *     to be delivered to some (possibly the same) Container.
+ *     This callback function implements a security policy;
+ *     it returns true if the delivery of the data is
+ *     permitted and false if permission is denied.
+ * @param {Function} params.onSubscribe
+ *     Called whenever a Container tries to subscribe
+ *     on behalf of its client.
+ *     This callback function implements a security policy;
+ *     it returns true if the subscription is permitted 
+ *     and false if permission is denied.
+ * @param {Function} [params.onUnsubscribe]
+ *     Called whenever a Container unsubscribes on behalf of its client. 
+ *     Unlike the other callbacks, onUnsubscribe is intended only for 
+ *     informative purposes, and is not used to implement a security
+ *     policy.
+ * @param {Object} [params.scope]
+ *     Whenever one of the ManagedHub's callback functions is called,
+ *     references to the JavaScript "this" keyword in the callback 
+ *     function refer to this scope object
+ *     If no scope is provided, default is window.
+ * @param {Function} [params.log]  Optional logger function. Would
+ *     be used to log to console.log or equivalent.
+ * 
+ * @throws {OpenAjax.hub.Error.BadParameters} if any of the required
+ *     parameters are missing
+ */
+OpenAjax.hub.ManagedHub = function( params )
+{
+    if ( ! params || ! params.onPublish || ! params.onSubscribe )
+        throw new Error( OpenAjax.hub.Error.BadParameters );
+    
+    this._p = params;
+    this._onUnsubscribe = params.onUnsubscribe ? params.onUnsubscribe : null;
+    this._scope = params.scope || window;
+
+    if ( params.log ) {
+        var that = this;
+        this._log = function( msg ) {
+            try {
+                params.log.call( that._scope, "ManagedHub: " + msg );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+            }
+        };
+    } else {
+        this._log = function() {};
+    }
+
+    this._subscriptions = { c:{}, s:null };
+    this._containers = {};
+
+    // Sequence # used to create IDs that are unique within this hub
+    this._seq = 0;
+
+    this._active = true;
+    
+    this._isPublishing = false;
+    this._pubQ = [];
+}
+
+/**
+ * Subscribe to a topic on behalf of a Container. Called only by 
+ * Container implementations, NOT by manager applications.
+ * 
+ * This function:
+ * 1. Checks with the ManagedHub's onSubscribe security policy
+ *    to determine whether this Container is allowed to subscribe 
+ *    to this topic.
+ * 2. If the subscribe operation is permitted, subscribes to the
+ *    topic and returns the ManagedHub's subscription ID for this
+ *    subscription. 
+ * 3. If the subscribe operation is not permitted, throws
+ *    OpenAjax.hub.Error.NotAllowed.
+ * 
+ * When data is published on the topic, the ManagedHub's 
+ * onPublish security policy will be invoked to ensure that
+ * this Container is permitted to receive the published data.
+ * If the Container is allowed to receive the data, then the
+ * Container's sendToClient function will be invoked.
+ * 
+ * When a Container needs to create a subscription on behalf of
+ * its client, the Container MUST use this function to create
+ * the subscription.
+ * 
+ * @param {OpenAjax.hub.Container} container  
+ *     A Container
+ * @param {String} topic 
+ *     A valid topic
+ * @param {String} containerSubID  
+ *     Arbitrary string ID that the Container uses to 
+ *     represent the subscription. Must be unique within the 
+ *     context of the Container
+ *
+ * @returns managerSubID  
+ *     Arbitrary string ID that this ManagedHub uses to 
+ *     represent the subscription. Will be unique within the 
+ *     context of this ManagedHub
+ * @type {String}
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this.isConnected() returns false
+ * @throws {OpenAjax.hub.Error.NotAllowed} if subscription request is denied by the onSubscribe security policy
+ * @throws {OpenAjax.hub.Error.BadParameters} if one of the parameters, e.g. the topic, is invalid
+ */
+OpenAjax.hub.ManagedHub.prototype.subscribeForClient = function( container, topic, containerSubID )
+{
+    this._assertConn();
+    // check subscribe permission
+    if ( this._invokeOnSubscribe( topic, container ) ) {
+        // return ManagedHub's subscriptionID for this subscription
+        return this._subscribe( topic, this._sendToClient, this, { c: container, sid: containerSubID } );
+    }
+    throw new Error(OpenAjax.hub.Error.NotAllowed);
+}
+
+/**
+ * Unsubscribe from a subscription on behalf of a Container. Called only by 
+ * Container implementations, NOT by manager application code.
+ * 
+ * This function:
+ * 1. Destroys the specified subscription
+ * 2. Calls the ManagedHub's onUnsubscribe callback function
+ * 
+ * This function can be called even if the ManagedHub is not in a CONNECTED state.
+ * 
+ * @param {OpenAjax.hub.Container} container  
+ *    container instance that is unsubscribing
+ * @param {String} managerSubID  
+ *    opaque ID of a subscription, returned by previous call to subscribeForClient()
+ * 
+ * @throws {OpenAjax.hub.Error.NoSubscription} if subscriptionID does not refer to a valid subscription
+ */
+OpenAjax.hub.ManagedHub.prototype.unsubscribeForClient = function( container, managerSubID )
+{
+    this._unsubscribe( managerSubID );
+    this._invokeOnUnsubscribe( container, managerSubID );
+}
+  
+/**
+ * Publish data on a topic on behalf of a Container. Called only by 
+ * Container implementations, NOT by manager application code.
+ *
+ * @param {OpenAjax.hub.Container} container
+ *      Container on whose behalf data should be published
+ * @param {String} topic
+ *      Valid topic string. Must NOT contain wildcards.
+ * @param {*} data
+ *      Valid publishable data. To be portable across different
+ *      Container implementations, this value SHOULD be serializable
+ *      as JSON.
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this.isConnected() returns false
+ * @throws {OpenAjax.hub.Error.BadParameters} if one of the parameters, e.g. the topic, is invalid
+ */
+OpenAjax.hub.ManagedHub.prototype.publishForClient = function( container, topic, data )
+{
+    this._assertConn();
+    this._publish( topic, data, container );
+}
+
+/**
+ * Destroy this ManagedHub
+ * 
+ * 1. Sets state to DISCONNECTED. All subsequent attempts to add containers,
+ *  publish or subscribe will throw the Disconnected error. We will
+ *  continue to allow "cleanup" operations such as removeContainer
+ *  and unsubscribe, as well as read-only operations such as 
+ *  isConnected
+ * 2. Remove all Containers associated with this ManagedHub
+ */
+OpenAjax.hub.ManagedHub.prototype.disconnect = function()
+{
+    this._active = false;
+    for (var c in this._containers) {
+        this.removeContainer( this._containers[c] );
+    }
+}
+
+/**
+ * Get a container belonging to this ManagedHub by its clientID, or null
+ * if this ManagedHub has no such container
+ * 
+ * This function can be called even if the ManagedHub is not in a CONNECTED state.
+ * 
+ * @param {String} containerId
+ *      Arbitrary string ID associated with the container
+ *
+ * @returns container associated with given ID
+ * @type {OpenAjax.hub.Container}
+ */
+OpenAjax.hub.ManagedHub.prototype.getContainer = function( containerId ) 
+{
+    var container = this._containers[containerId];
+    return container ? container : null;
+}
+
+/**
+ * Returns an array listing all containers belonging to this ManagedHub.
+ * The order of the Containers in this array is arbitrary.
+ * 
+ * This function can be called even if the ManagedHub is not in a CONNECTED state.
+ * 
+ * @returns container array
+ * @type {OpenAjax.hub.Container[]}
+ */
+OpenAjax.hub.ManagedHub.prototype.listContainers = function() 
+{
+    var res = [];
+    for (var c in this._containers) { 
+        res.push(this._containers[c]);
+    }
+    return res;
+}
+
+/**
+ * Add a container to this ManagedHub.
+ *
+ * This function should only be called by a Container constructor.
+ * 
+ * @param {OpenAjax.hub.Container} container
+ *      A Container to be added to this ManagedHub
+ * 
+ * @throws {OpenAjax.hub.Error.Duplicate} if there is already a Container
+ *      in this ManagedHub whose clientId is the same as that of container
+ * @throws {OpenAjax.hub.Error.Disconnected} if this.isConnected() returns false
+ */
+OpenAjax.hub.ManagedHub.prototype.addContainer = function( container ) 
+{ 
+    this._assertConn();
+    var containerId = container.getClientID();
+    if ( this._containers[containerId] ) {
+        throw new Error(OpenAjax.hub.Error.Duplicate);
+    }
+    this._containers[containerId] = container;
+}
+
+/**
+ * Remove a container from this ManagedHub immediately
+ * 
+ * This function can be called even if the ManagedHub is not in a CONNECTED state.
+ * 
+ * @param {OpenAjax.hub.Container} container  
+ *      A Container to be removed from this ManagedHub
+ *  
+ * @throws {OpenAjax.hub.Error.NoContainer}  if no such container is found
+ */
+OpenAjax.hub.ManagedHub.prototype.removeContainer = function( container )
+{
+    var containerId = container.getClientID();
+    if ( ! this._containers[ containerId ] ) {
+        throw new Error(OpenAjax.hub.Error.NoContainer);
+    }
+    container.remove();
+    delete this._containers[ containerId ];
+}
+
+    /*** OpenAjax.hub.Hub interface implementation ***/
+
+/**
+ * Subscribe to a topic.
+ * 
+ * This implementation of Hub.subscribe is synchronous. When subscribe 
+ * is called:
+ * 
+ * 1. The ManagedHub's onSubscribe callback is invoked. The 
+ * 		container parameter is null, because the manager application, 
+ * 		rather than a container, is subscribing.
+ * 2. If onSubscribe returns true, then the subscription is created.
+ * 3. The onComplete callback is invoked.
+ * 4. Then this function returns.
+ * 
+ * @param {String} topic
+ *     A valid topic string. MAY include wildcards.
+ * @param {Function} onData   
+ *     Callback function that is invoked whenever an event is 
+ *     published on the topic
+ * @param {Object} [scope]
+ *     When onData callback or onComplete callback is invoked,
+ *     the JavaScript "this" keyword refers to this scope object.
+ *     If no scope is provided, default is window.
+ * @param {Function} [onComplete]
+ *     Invoked to tell the client application whether the 
+ *     subscribe operation succeeded or failed. 
+ * @param {*} [subscriberData]
+ *     Client application provides this data, which is handed
+ *     back to the client application in the subscriberData
+ *     parameter of the onData and onComplete callback functions.
+ * 
+ * @returns subscriptionID
+ *     Identifier representing the subscription. This identifier is an 
+ *     arbitrary ID string that is unique within this Hub instance
+ * @type {String}
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.BadParameters} if the topic is invalid (e.g. contains an empty token)
+ */
+OpenAjax.hub.ManagedHub.prototype.subscribe = function( topic, onData, scope, onComplete, subscriberData ) 
+{
+    this._assertConn();
+    this._assertSubTopic(topic);
+    if ( ! onData ) {
+        throw new Error( OpenAjax.hub.Error.BadParameters );
+    }
+    
+    scope = scope || window;
+    
+    // check subscribe permission
+    if ( ! this._invokeOnSubscribe( topic, null ) ) {
+        this._invokeOnComplete( onComplete, scope, null, false, OpenAjax.hub.Error.NotAllowed );
+        return;
+    }
+    
+    // on publish event, check publish permissions
+    var that = this;
+    function publishCB( topic, data, sd, pcont ) {
+        if ( that._invokeOnPublish( topic, data, pcont, null ) ) {
+            try {
+                onData.call( scope, topic, data, subscriberData );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+                that._log( "caught error from onData callback to Hub.subscribe(): " + e.message );
+            }
+        }
+    }
+    var subID = this._subscribe( topic, publishCB, scope, subscriberData );
+    this._invokeOnComplete( onComplete, scope, subID, true );
+    return subID;
+}
+
+/**
+ * Publish an event on a topic
+ *
+ * This implementation of Hub.publish is synchronous. When publish 
+ * is called:
+ * 
+ * 1. The target subscriptions are identified.
+ * 2. For each target subscription, the ManagedHub's onPublish
+ * 		callback is invoked. Data is only delivered to a target
+ * 		subscription if the onPublish callback returns true.
+ * 		The pcont parameter of the onPublish callback is null.
+ *      This is because the ManagedHub, rather than a container,
+ *      is publishing the data.
+ * 
+ * @param {String} topic
+ *     A valid topic string. MUST NOT include wildcards.
+ * @param {*} data
+ *     Valid publishable data. To be portable across different
+ *     Container implementations, this value SHOULD be serializable
+ *     as JSON.
+ *     
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.BadParameters} if the topic cannot be published (e.g. contains 
+ *     wildcards or empty tokens) or if the data cannot be published (e.g. cannot be serialized as JSON)
+ */
+OpenAjax.hub.ManagedHub.prototype.publish = function( topic, data ) 
+{
+    this._assertConn();
+    this._assertPubTopic(topic);
+    this._publish( topic, data, null );
+}
+
+/**
+ * Unsubscribe from a subscription
+ * 
+ * This implementation of Hub.unsubscribe is synchronous. When unsubscribe 
+ * is called:
+ * 
+ * 1. The subscription is destroyed.
+ * 2. The ManagedHub's onUnsubscribe callback is invoked, if there is one.
+ * 3. The onComplete callback is invoked.
+ * 4. Then this function returns.
+ * 
+ * @param {String} subscriptionID
+ *     A subscriptionID returned by Hub.subscribe()
+ * @param {Function} [onComplete]
+ *     Callback function invoked when unsubscribe completes
+ * @param {Object} [scope]
+ *     When onComplete callback function is invoked, the JavaScript "this"
+ *     keyword refers to this scope object.
+ *     If no scope is provided, default is window.
+ *     
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.NoSubscription} if no such subscription is found
+ */
+OpenAjax.hub.ManagedHub.prototype.unsubscribe = function( subscriptionID, onComplete, scope )
+{
+    this._assertConn();
+    if ( ! subscriptionID ) {
+        throw new Error( OpenAjax.hub.Error.BadParameters );
+    }
+    this._unsubscribe( subscriptionID );
+    this._invokeOnUnsubscribe( null, subscriptionID );
+    this._invokeOnComplete( onComplete, scope, subscriptionID, true );
+}
+
+/**
+ * Returns true if disconnect() has NOT been called on this ManagedHub, 
+ * else returns false
+ * 
+ * @returns Boolean
+ * @type {Boolean}
+ */
+OpenAjax.hub.ManagedHub.prototype.isConnected = function()
+{
+    return this._active;
+}
+
+/**
+* Returns the scope associated with this Hub instance and which will be used
+* with callback functions.
+* 
+* This function can be called even if the Hub is not in a CONNECTED state.
+* 
+* @returns scope object
+* @type {Object}
+ */
+OpenAjax.hub.ManagedHub.prototype.getScope = function()
+{
+    return this._scope;
+}
+
+/**
+ * Returns the subscriberData parameter that was provided when 
+ * Hub.subscribe was called.
+ *
+ * @param subscriberID
+ *     The subscriberID of a subscription
+ * 
+ * @returns subscriberData
+ * @type {*}
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.NoSubscription} if there is no such subscription
+ */
+OpenAjax.hub.ManagedHub.prototype.getSubscriberData = function( subscriberID )
+{
+    this._assertConn();
+    var path = subscriberID.split(".");
+    var sid = path.pop();
+    var sub = this._getSubscriptionObject( this._subscriptions, path, 0, sid );
+    if ( sub ) 
+        return sub.data;
+    throw new Error( OpenAjax.hub.Error.NoSubscription );
+}
+
+/**
+ * Returns the scope associated with a specified subscription.  This scope will
+ * be used when invoking the 'onData' callback supplied to Hub.subscribe().
+ *
+ * @param subscriberID
+ *     The subscriberID of a subscription
+ * 
+ * @returns scope
+ * @type {*}
+ * 
+ * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+ * @throws {OpenAjax.hub.Error.NoSubscription} if there is no such subscription
+ */
+OpenAjax.hub.ManagedHub.prototype.getSubscriberScope = function( subscriberID )
+{
+    this._assertConn();
+    var path = subscriberID.split(".");
+    var sid = path.pop();
+    var sub = this._getSubscriptionObject( this._subscriptions, path, 0, sid );
+    if ( sub ) 
+        return sub.scope;
+    throw new Error( OpenAjax.hub.Error.NoSubscription );
+}
+
+/**
+ * Returns the params object associated with this Hub instance.
+ * Allows mix-in code to access parameters passed into constructor that created
+ * this Hub instance.
+ *
+ * @returns params  the params object associated with this Hub instance
+ * @type {Object}
+ */
+OpenAjax.hub.ManagedHub.prototype.getParameters = function()
+{
+    return this._p;
+}
+
+
+/* PRIVATE FUNCTIONS */
+
+/**
+ * Send a message to a container's client. 
+ * This is an OAH subscriber's data callback. It is private to ManagedHub
+ * and serves as an adapter between the OAH 1.0 API and Container.sendToClient.
+ * 
+ * @param {String} topic Topic on which data was published
+ * @param {Object} data  Data to be delivered to the client
+ * @param {Object} sd    Object containing properties 
+ *     c: container to which data must be sent
+ *     sid: subscription ID within that container
+ * @param {Object} pcont  Publishing container, or null if this data was
+ *      published by the manager
+ */
+OpenAjax.hub.ManagedHub.prototype._sendToClient = function(topic, data, sd, pcont) 
+{
+    if (!this.isConnected()) {
+        return;
+    }
+    if ( this._invokeOnPublish( topic, data, pcont, sd.c ) ) {
+        sd.c.sendToClient( topic, data, sd.sid );
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._assertConn = function() 
+{
+    if (!this.isConnected()) {
+        throw new Error(OpenAjax.hub.Error.Disconnected);
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._assertPubTopic = function(topic) 
+{
+    if ( !topic || topic === "" || (topic.indexOf("*") != -1) ||
+        (topic.indexOf("..") != -1) ||  (topic.charAt(0) == ".") ||
+        (topic.charAt(topic.length-1) == "."))
+    {
+        throw new Error(OpenAjax.hub.Error.BadParameters);
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._assertSubTopic = function(topic) 
+{
+    if ( ! topic ) {
+        throw new Error(OpenAjax.hub.Error.BadParameters);
+    }
+    var path = topic.split(".");
+    var len = path.length;
+    for (var i = 0; i < len; i++) {
+        var p = path[i];
+        if ((p === "") ||
+           ((p.indexOf("*") != -1) && (p != "*") && (p != "**"))) {
+            throw new Error(OpenAjax.hub.Error.BadParameters);
+        }
+        if ((p == "**") && (i < len - 1)) {
+            throw new Error(OpenAjax.hub.Error.BadParameters);
+        }
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._invokeOnComplete = function( func, scope, item, success, errorCode )
+{
+    if ( func ) { // onComplete is optional
+        try {
+            scope = scope || window;
+            func.call( scope, item, success, errorCode );
+        } catch( e ) {
+            OpenAjax.hub._debugger();
+            this._log( "caught error from onComplete callback: " + e.message );
+        }
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._invokeOnPublish = function( topic, data, pcont, scont )
+{
+    try {
+        return this._p.onPublish.call( this._scope, topic, data, pcont, scont );
+    } catch( e ) {
+        OpenAjax.hub._debugger();
+        this._log( "caught error from onPublish callback to constructor: " + e.message );
+    }
+    return false;
+}
+
+OpenAjax.hub.ManagedHub.prototype._invokeOnSubscribe = function( topic, container )
+{
+    try {
+        return this._p.onSubscribe.call( this._scope, topic, container );
+    } catch( e ) {
+        OpenAjax.hub._debugger();
+        this._log( "caught error from onSubscribe callback to constructor: " + e.message );
+    }
+    return false;
+}
+
+OpenAjax.hub.ManagedHub.prototype._invokeOnUnsubscribe = function( container, managerSubID )
+{
+    if ( this._onUnsubscribe ) {
+        var topic = managerSubID.slice( 0, managerSubID.lastIndexOf(".") );
+        try {
+            this._onUnsubscribe.call( this._scope, topic, container );
+        } catch( e ) {
+            OpenAjax.hub._debugger();
+            this._log( "caught error from onUnsubscribe callback to constructor: " + e.message );
+        }
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._subscribe = function( topic, onData, scope, subscriberData ) 
+{
+    var handle = topic + "." + this._seq;
+    var sub = { scope: scope, cb: onData, data: subscriberData, sid: this._seq++ };
+    var path = topic.split(".");
+    this._recursiveSubscribe( this._subscriptions, path, 0, sub );
+    return handle;
+}
+
+OpenAjax.hub.ManagedHub.prototype._recursiveSubscribe = function(tree, path, index, sub) 
+{
+    var token = path[index];
+    if (index == path.length) {
+        sub.next = tree.s;
+        tree.s = sub;
+    } else { 
+        if (typeof tree.c == "undefined") {
+             tree.c = {};
+         }
+        if (typeof tree.c[token] == "undefined") {
+            tree.c[token] = { c: {}, s: null }; 
+            this._recursiveSubscribe(tree.c[token], path, index + 1, sub);
+        } else {
+            this._recursiveSubscribe( tree.c[token], path, index + 1, sub);
+        }
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._publish = function( topic, data, pcont )
+{
+    // if we are currently handling a publish event, then queue this request
+    // and handle later, one by one
+    if ( this._isPublishing ) {
+        this._pubQ.push( { t: topic, d: data, p: pcont } );
+        return;
+    }
+    
+    this._safePublish( topic, data, pcont );
+    
+    while ( this._pubQ.length > 0 ) {
+        var pub = this._pubQ.shift();
+        this._safePublish( pub.t, pub.d, pub.p );
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._safePublish = function( topic, data, pcont )
+{
+    this._isPublishing = true;
+    var path = topic.split(".");
+    this._recursivePublish( this._subscriptions, path, 0, topic, data, pcont );
+    this._isPublishing = false;
+}
+
+OpenAjax.hub.ManagedHub.prototype._recursivePublish = function(tree, path, index, name, msg, pcont) 
+{
+    if (typeof tree != "undefined") {
+        var node;
+        if (index == path.length) {
+            node = tree;
+        } else {
+            this._recursivePublish(tree.c[path[index]], path, index + 1, name, msg, pcont);
+            this._recursivePublish(tree.c["*"], path, index + 1, name, msg, pcont);
+            node = tree.c["**"];
+        }
+        if (typeof node != "undefined") {
+            var sub = node.s;
+            while ( sub ) {
+                var sc = sub.scope;
+                var cb = sub.cb;
+                var d = sub.data;
+                if (typeof cb == "string") {
+                    // get a function object
+                    cb = sc[cb];
+                }
+                cb.call(sc, name, msg, d, pcont);
+                sub = sub.next;
+            }
+        }
+    }
+}
+
+OpenAjax.hub.ManagedHub.prototype._unsubscribe = function( subscriptionID )
+{
+    var path = subscriptionID.split(".");
+    var sid = path.pop();
+    if ( ! this._recursiveUnsubscribe( this._subscriptions, path, 0, sid ) ) {
+        throw new Error( OpenAjax.hub.Error.NoSubscription );
+    }
+}
+
+/**
+ * @returns 'true' if properly unsubscribed; 'false' otherwise
+ */
+OpenAjax.hub.ManagedHub.prototype._recursiveUnsubscribe = function(tree, path, index, sid) 
+{
+    if ( typeof tree == "undefined" ) {
+        return false;
+    }
+    
+    if (index < path.length) {
+        var childNode = tree.c[path[index]];
+        if ( ! childNode ) {
+            return false;
+        }
+        this._recursiveUnsubscribe(childNode, path, index + 1, sid);
+        if ( ! childNode.s ) {
+            for (var x in childNode.c) {
+                return true;
+            }
+            delete tree.c[path[index]];    
+        }
+    } else {
+        var sub = tree.s;
+        var sub_prev = null;
+        var found = false;
+        while ( sub ) {
+            if ( sid == sub.sid ) {
+                found = true;
+                if ( sub == tree.s ) {
+                    tree.s = sub.next;
+                } else {
+                    sub_prev.next = sub.next;
+                }
+                break;
+            }
+            sub_prev = sub;
+            sub = sub.next;
+        }
+        if ( ! found ) {
+            return false;
+        }
+    }
+    
+    return true;
+}
+
+OpenAjax.hub.ManagedHub.prototype._getSubscriptionObject = function( tree, path, index, sid )
+{
+    if (typeof tree != "undefined") {
+        if (index < path.length) {
+            var childNode = tree.c[path[index]];
+            return this._getSubscriptionObject(childNode, path, index + 1, sid);
+        }
+
+        var sub = tree.s;
+        while ( sub ) {
+            if ( sid == sub.sid ) {
+                return sub;
+            }
+            sub = sub.next;
+        }
+    }
+    return null;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Container
+ * @constructor
+ * 
+ * Container represents an instance of a manager-side object that contains and
+ * communicates with a single client of the hub. The container might be an inline
+ * container, an iframe FIM container, or an iframe PostMessage container, or
+ * it might be an instance of some other implementation.
+ *
+ * @param {OpenAjax.hub.ManagedHub} hub
+ *    Managed Hub instance
+ * @param {String} clientID
+ *    A string ID that identifies a particular client of a Managed Hub. Unique
+ *    within the context of the ManagedHub.
+ * @param {Object} params  
+ *    Parameters used to instantiate the Container.
+ *    Once the constructor is called, the params object belongs exclusively to
+ *    the Container. The caller MUST not modify it.
+ *    Implementations of Container may specify additional properties
+ *    for the params object, besides those identified below.
+ *    The following params properties MUST be supported by all Container 
+ *    implementations:
+ * @param {Function} params.Container.onSecurityAlert
+ *    Called when an attempted security breach is thwarted.  Function is defined
+ *    as follows:  function(container, securityAlert)
+ * @param {Function} [params.Container.onConnect]
+ *    Called when the client connects to the Managed Hub.  Function is defined
+ *    as follows:  function(container)
+ * @param {Function} [params.Container.onDisconnect]
+ *    Called when the client disconnects from the Managed Hub.  Function is
+ *    defined as follows:  function(container)
+ * @param {Object} [params.Container.scope]
+ *    Whenever one of the Container's callback functions is called, references
+ *    to "this" in the callback will refer to the scope object. If no scope is
+ *    provided, default is window.
+ * @param {Function} [params.Container.log]
+ *    Optional logger function. Would be used to log to console.log or
+ *    equivalent. 
+ *
+ * @throws {OpenAjax.hub.Error.BadParameters}   if required params are not
+ *   present or null
+ * @throws {OpenAjax.hub.Error.Duplicate}   if a Container with this clientID
+ *   already exists in the given Managed Hub
+ * @throws {OpenAjax.hub.Error.Disconnected}   if ManagedHub is not connected
+ */
+//OpenAjax.hub.Container = function( hub, clientID, params ) {}
+
+/**
+ * Send a message to the client inside this container. This function MUST only
+ * be called by ManagedHub. 
+ * 
+ * @param {String} topic
+ *    The topic name for the published message
+ * @param {*} data
+ *    The payload. Can be any JSON-serializable value.
+ * @param {String} containerSubscriptionId
+ *    Container's ID for a subscription, from previous call to
+ *    subscribeForClient()
+ */
+//OpenAjax.hub.Container.prototype.sendToClient = function( topic, data, containerSubscriptionId ) {}
+
+/**
+ * Shut down a container. remove does all of the following:
+ * - disconnects container from HubClient
+ * - unsubscribes from all of its existing subscriptions in the ManagedHub
+ * 
+ * This function is only called by ManagedHub.removeContainer
+ * Calling this function does NOT cause the container's onDisconnect callback to
+ * be invoked.
+ */
+//OpenAjax.hub.Container.prototype.remove = function() {}
+
+/**
+ * Returns true if the given client is connected to the managed hub.
+ * Else returns false.
+ *
+ * @returns true if the client is connected to the managed hub
+ * @type boolean
+ */
+//OpenAjax.hub.Container.prototype.isConnected = function() {}
+
+/**
+ * Returns the clientID passed in when this Container was instantiated.
+ *
+ * @returns The clientID
+ * @type {String}  
+ */
+//OpenAjax.hub.Container.prototype.getClientID = function() {}
+
+/**
+ * If DISCONNECTED:
+ * Returns null
+ * If CONNECTED:
+ * Returns the origin associated with the window containing the HubClient
+ * associated with this Container instance. The origin has the format
+ *  
+ * [protocol]://[host]
+ * 
+ * where:
+ * 
+ * [protocol] is "http" or "https"
+ * [host] is the hostname of the partner page.
+ * 
+ * @returns Partner's origin
+ * @type {String}
+ */
+//OpenAjax.hub.Container.prototype.getPartnerOrigin = function() {}
+
+/**
+ * Returns the params object associated with this Container instance.
+ *
+ * @returns params
+ *    The params object associated with this Container instance
+ * @type {Object}
+ */
+//OpenAjax.hub.Container.prototype.getParameters = function() {}
+
+/**
+ * Returns the ManagedHub to which this Container belongs.
+ *
+ * @returns ManagedHub
+ *         The ManagedHub object associated with this Container instance
+ * @type {OpenAjax.hub.ManagedHub}
+ */
+//OpenAjax.hub.Container.prototype.getHub = function() {}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/*
+ * Unmanaged Hub
+ */
+
+/**
+ * OpenAjax.hub._hub is the default ManagedHub instance that we use to 
+ * provide OAH 1.0 behavior. 
+ */
+OpenAjax.hub._hub = new OpenAjax.hub.ManagedHub({ 
+    onSubscribe: function(topic, ctnr) { return true; },
+    onPublish: function(topic, data, pcont, scont) { return true; }
+});
+
+/**
+ * Subscribe to a topic.
+ *
+ * @param {String} topic
+ *     A valid topic string. MAY include wildcards.
+ * @param {Function|String} onData
+ *     Callback function that is invoked whenever an event is published on the
+ *     topic.  If 'onData' is a string, then it represents the name of a
+ *     function on the 'scope' object.
+ * @param {Object} [scope]
+ *     When onData callback is invoked,
+ *     the JavaScript "this" keyword refers to this scope object.
+ *     If no scope is provided, default is window.
+ * @param {*} [subscriberData]
+ *     Client application provides this data, which is handed
+ *     back to the client application in the subscriberData
+ *     parameter of the onData callback function.
+ * 
+ * @returns {String} Identifier representing the subscription.
+ * 
+ * @throws {OpenAjax.hub.Error.BadParameters} if the topic is invalid
+ *     (e.g.contains an empty token)
+ */
+OpenAjax.hub.subscribe = function(topic, onData, scope, subscriberData) 
+{
+    // resolve the 'onData' function if it is a string
+    if ( typeof onData === "string" ) {
+        scope = scope || window;
+        onData = scope[ onData ] || null;
+    }
+    
+    return OpenAjax.hub._hub.subscribe( topic, onData, scope, null, subscriberData );
+}
+
+/**
+ * Unsubscribe from a subscription.
+ *
+ * @param {String} subscriptionID
+ *     Subscription identifier returned by subscribe()
+ *     
+ * @throws {OpenAjax.hub.Error.NoSubscription} if no such subscription is found
+ */
+OpenAjax.hub.unsubscribe = function(subscriptionID) 
+{
+    return OpenAjax.hub._hub.unsubscribe( subscriptionID );
+}
+
+/**
+ * Publish an event on a topic.
+ *
+ * @param {String} topic
+ *     A valid topic string. MUST NOT include wildcards.
+ * @param {*} data
+ *     Valid publishable data.
+ *     
+ * @throws {OpenAjax.hub.Error.BadParameters} if the topic cannot be published
+ *     (e.g. contains wildcards or empty tokens)
+ */
+OpenAjax.hub.publish = function(topic, data) 
+{
+    OpenAjax.hub._hub.publish(topic, data);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+// Register the OpenAjax Hub itself as a library.
+OpenAjax.hub.registerLibrary("OpenAjax", "http://openajax.org/hub", "2.0", {});
+
+} // !OpenAjax.hub
diff --git a/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/README.txt b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/README.txt
new file mode 100644
index 0000000..3379a7c
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/README.txt
@@ -0,0 +1,78 @@
+README
+
+This version of openajax.hub is slightly modified from original project. The original source can be found at:
+http://sourceforge.net/projects/openajaxallianc/
+
+The changes only appear in iframe.js. The following differences should be noted:
+
+1) Added a handler for the iframe onload to fire gadget onload events. Modify the iframe.src to check for a hash in the url before appending the rpc token.
+
+
+387	387	          var idText = "";
+388	388	          if ( internalID !== clientID ) {
+389	389	              idText = "&oahId=" + internalID.substring( internalID.lastIndexOf('_') + 1 );
+390	390	          }
+391		-         document.getElementById( internalID ).src = params.IframeContainer.uri +
+392		-                 "#rpctoken=" + securityToken + tunnelText + idText;
+391		+ 
+392		+         var iframe = document.getElementById( internalID );
+393		+         if(iframe.attachEvent) {
+394		+           //Works for IE
+395		+           iframe.attachEvent('onload', function(){
+396		+             window[params.IframeContainer.onGadgetLoad]();
+397		+           });
+398		+         } else {
+399		+           iframe.onload = function(){window[params.IframeContainer.onGadgetLoad]();};
+400		+         }
+401		+ 
+402		+         var uri = params.IframeContainer.uri;
+403		+         var hashIdx = uri.indexOf('#');
+404		+         var joinToken = (hashIdx === -1)?'#':'&';
+405		+ 
+406		+         iframe.src = uri + joinToken + "rpctoken=" + securityToken + tunnelText + idText;
+393	407	      }
+394		-     
+408		+ 
+395	409	      // If the relay iframe used by RPC has not been loaded yet, then we won't have unload protection
+396	410	      // at this point.  Since we can't detect when the relay iframe has loaded, we use a two stage
+
+
+
+2) Slight style & efficiency changes. These do not appear to add an functionality.
+
+522	536	      this._init = function() {
+523		-         var urlParams = OpenAjax.gadgets.util.getUrlParameters();
+537		+         var oaGadgets = OpenAjax.gadgets;
+538		+         var urlParams = oaGadgets.util.getUrlParameters();
+524	539	          if ( ! urlParams.parent ) {
+525	540	              // The RMR transport does not require a valid relay file, but does need a URL
+526	541	              // in the parent's domain. The URL does not need to point to valid file, so just
+527	542	              // point to 'robots.txt' file. See RMR transport code for more info.
+528		-             var parent = urlParams.oahParent + "/robots.txt";
+529		-             OpenAjax.gadgets.rpc.setupReceiver( "..", parent );
+543		+             var parent = urlParams['oahParent'] + "/robots.txt";
+544		+             oaGadgets.rpc.setupReceiver( "..", parent );
+530	545	          }
+531	546	          
+532	547	          if ( params.IframeHubClient && params.IframeHubClient.requireParentVerifiable &&
+533		-              OpenAjax.gadgets.rpc.getReceiverOrigin( ".." ) === null ) {
+548		+                 oaGadgets.rpc.getReceiverOrigin( ".." ) === null ) {
+534	549	              // If user set 'requireParentVerifiable' to true but RPC transport does not
+535	550	              // support this, throw error.
+536		-             OpenAjax.gadgets.rpc.removeReceiver( ".." );
+551		+             oaGadgets.rpc.removeReceiver( ".." );
+537	552	              throw new Error( OpenAjax.hub.Error.IncompatBrowser );
+538	553	          }
+539	554	          
+540	555	          OpenAjax.hub.IframeContainer._rpcRouter.add( "..", this );
+541	556	  // XXX The RPC layer initializes immediately on load, in the child (IframeHubClient). So it is too
+542	557	  //    late here to specify a security token for the RPC layer.  At the moment, only the NIX
+543	558	  //    transport requires a child token (IFPC [aka FIM] is not supported).
+544	559	  //        securityToken = generateSecurityToken( params, scope, log );
+545	560	  
+546	561	          clientID = OpenAjax.gadgets.rpc.RPC_ID;
+547		-         if ( urlParams.oahId ) {
+562		+         if ( urlParams['oahId'] ) {
+548	563	              clientID = clientID.substring( 0, clientID.lastIndexOf('_') );
+549	564	          }
+550	565	      };
\ No newline at end of file
diff --git a/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/crypto.js b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/crypto.js
new file mode 100644
index 0000000..6851b39
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/crypto.js
@@ -0,0 +1,244 @@
+/*
+
+        Copyright 2006-2009 OpenAjax Alliance
+
+        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.
+*/
+// SMASH.CRYPTO
+//
+// Small library containing some minimal crypto functionality for a
+// - a hash-function: SHA-1 (see FIPS PUB 180-2 for definition)
+//     BigEndianWord[5] <- smash.crypto.sha1( BigEndianWord[*] dataWA, int lenInBits)
+//
+// - a message authentication code (MAC): HMAC-SHA-1 (RFC2104/2202)
+//     BigEndianWord[5] <- smash.crypto.hmac_sha1(
+//                            BigEndianWord[3-16] keyWA, 
+//                            Ascii or Unicode string dataS,
+//		 		 		       int chrsz (8 for Asci/16 for Unicode)
+//
+// - pseudo-random number generator (PRNG): HMAC-SHA-1 in counter mode, following
+//   Barak & Halevi, An architecture for robust pseudo-random generation and applications to /dev/random, CCS 2005
+//     rngObj <- smash.crypto.newPRNG( String[>=12] seedS)
+//   where rngObj has methods
+//     addSeed(String seed)
+//     BigEndianWord[len] <- nextRandomOctets(int len)
+//     Base64-String[len] <- nextRandomB64Str(int len)
+//   Note: HMAC-SHA1 in counter-mode does not provide forward-security on corruption. 
+//         However, the PRNG state is kept inside a closure. So if somebody can break the closure, he probably could
+//         break a whole lot more and forward-security of the prng is not the highest of concerns anymore :-)
+
+if ( typeof OpenAjax._smash == 'undefined' ) { OpenAjax._smash = {}; }
+
+OpenAjax._smash.crypto = {
+
+  // Some utilities
+  // convert a string to an array of big-endian words
+  'strToWA': function (/* Ascii or Unicode string */ str, /* int 8 for Asci/16 for Unicode */ chrsz){
+    var bin = Array();
+    var mask = (1 << chrsz) - 1;
+    for(var i = 0; i < str.length * chrsz; i += chrsz)
+      bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (32 - chrsz - i%32);
+    return bin;
+  },
+
+
+  // MAC
+  'hmac_sha1' : function(
+        /* BigEndianWord[3-16]*/             keyWA,
+       /* Ascii or Unicode string */       dataS,
+       /* int 8 for Asci/16 for Unicode */ chrsz)
+  {
+    // write our own hmac derived from paj's so we do not have to do constant key conversions and length checking ...
+    var ipad = Array(16), opad = Array(16);
+    for(var i = 0; i < 16; i++) {
+      ipad[i] = keyWA[i] ^ 0x36363636;
+      opad[i] = keyWA[i] ^ 0x5C5C5C5C;
+    }
+
+    var hash = this.sha1( ipad.concat(this.strToWA(dataS, chrsz)), 512 + dataS.length * chrsz);
+    return     this.sha1( opad.concat(hash), 512 + 160);
+  },
+
+
+  // PRNG factory method
+  // see below 'addSeed', 'nextRandomOctets' & 'nextRandomB64Octets' for public methods of returnd prng object
+  'newPRNG' : function (/* String[>=12] */ seedS) {
+    var that = this;
+
+    // parameter checking
+    // We cannot really verify entropy but obviously the string must have at least a minimal length to have enough entropy
+    // However, a 2^80 security seems ok, so we check only that at least 12 chars assuming somewhat random ASCII
+    if ( (typeof seedS != 'string') || (seedS.length < 12) ) {
+      alert("WARNING: Seed length too short ...");
+    }
+
+    // constants
+    var __refresh_keyWA = [ 0xA999, 0x3E36, 0x4706, 0x816A,
+    		 		 		     0x2571, 0x7850, 0xC26C, 0x9CD0,
+    		 		 		     0xBA3E, 0xD89D, 0x1233, 0x9525,
+    		 		 		     0xff3C, 0x1A83, 0xD491, 0xFF15 ]; // some random key for refresh ...
+
+    // internal state
+    var _keyWA = []; // BigEndianWord[5]
+    var _cnt = 0;  // int
+
+    function extract(seedS) {
+      return that.hmac_sha1(__refresh_keyWA, seedS, 8);
+    }
+
+    function refresh(seedS) {
+      // HMAC-SHA1 is not ideal, Rijndal 256bit block/key in CBC mode with fixed key might be better
+      // but to limit the primitives and given that we anyway have only limited entropy in practise
+      // this seems good enough
+      var uniformSeedWA = extract(seedS);
+      for(var i = 0; i < 5; i++) {
+        _keyWA[i] ^= uniformSeedWA[i];
+      }
+    }
+
+    // inital state seeding
+    refresh(seedS);
+
+    // public methods
+    return {
+      // Mix some additional seed into the PRNG state
+      'addSeed'         : function (/* String */ seed) {
+        // no parameter checking. Any added entropy should be fine ...
+        refresh(seed);
+      },
+
+
+      // Get an array of len random octets
+      'nextRandomOctets' : /* BigEndianWord[len] <- */ function (/* int */ len) {
+		 var randOctets = [];
+		 while (len > 0) {
+		   _cnt+=1;
+		   var nextBlock = that.hmac_sha1(_keyWA, (_cnt).toString(16), 8);
+		   for (i=0; (i < 20) & (len > 0); i++, len--) {
+		     randOctets.push( (nextBlock[i>>2] >> (i % 4) ) % 256);
+		   }
+		   // Note: if len was not a multiple 20, some random octets are ignored here but who cares ..
+		 }
+		 return randOctets;
+      },
+
+
+      // Get a random string of Base64-like (see below) chars of length len
+      // Note: there is a slightly non-standard Base64 with no padding and '-' and '_' for '+' and '/', respectively
+      'nextRandomB64Str' : /* Base64-String <- */ function (/* int */ len) {
+		 var b64StrMap = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+		 var randOctets = this.nextRandomOctets(len);
+		 var randB64Str = '';
+		 for (var i=0; i < len; i++) {
+		   randB64Str += b64StrMap.charAt(randOctets[i] & 0x3F);
+		 }
+        return randB64Str;
+      }
+
+    }
+  },
+
+
+  // Digest function:
+  // BigEndianWord[5] <- sha1( BigEndianWord[*] dataWA, int lenInBits)
+  'sha1' : function(){
+    // Note: all Section references below refer to FIPS 180-2.
+
+    // private utility functions
+
+    // - 32bit addition with wrap-around
+    var add_wa = function (x, y){
+      var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+      var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+      return (msw << 16) | (lsw & 0xFFFF);
+    }
+
+    // - 32bit rotatate left
+    var rol = function(num, cnt) {
+      return (num << cnt) | (num >>> (32 - cnt));
+    }
+
+    // - round-dependent function f_t from Section 4.1.1
+    function sha1_ft(t, b, c, d) {
+      if(t < 20) return (b & c) | ((~b) & d);
+      if(t < 40) return b ^ c ^ d;
+      if(t < 60) return (b & c) | (b & d) | (c & d);
+      return b ^ c ^ d;
+    }
+
+    // - round-dependent SHA-1 constants from Section 4.2.1
+    function sha1_kt(t) {
+      return (t < 20) ?  1518500249 :
+             (t < 40) ?  1859775393 :
+             (t < 60) ? -1894007588 :
+          /* (t < 80) */ -899497514 ;
+    }
+
+    // main algorithm. 
+    return function( /* BigEndianWord[*] */ dataWA, /* int */ lenInBits) {
+
+      // Section 6.1.1: Preprocessing
+      //-----------------------------
+      // 1. padding:  (see also Section 5.1.1)
+      //  - append one 1 followed by 0 bits filling up 448 bits of last (512bit) block
+      dataWA[lenInBits >> 5] |= 0x80 << (24 - lenInBits % 32);
+      //  - encode length in bits in last 64 bits
+      //    Note: we rely on javascript to zero file elements which are beyond last (partial) data-block
+      //    but before this length encoding!
+      dataWA[((lenInBits + 64 >> 9) << 4) + 15] = lenInBits;
+
+      // 2. 512bit blocks (actual split done ondemand later)
+      var W = Array(80);
+
+      // 3. initial hash using SHA-1 constants on page 13
+      var H0 =  1732584193;
+      var H1 = -271733879;
+      var H2 = -1732584194;
+      var H3 =  271733878;
+      var H4 = -1009589776;
+
+      // 6.1.2 SHA-1 Hash Computation
+      for(var i = 0; i < dataWA.length; i += 16) {
+        // 1. Message schedule, done below
+        // 2. init working variables
+        var a = H0; var b = H1; var c = H2; var d = H3; var e = H4;
+
+        // 3. round-functions
+        for(var j = 0; j < 80; j++)
+        {
+      		 // postponed step 2
+          W[j] = ( (j < 16) ? dataWA[i+j] : rol(W[j-3] ^ W[j-8] ^ W[j-14] ^ W[j-16], 1));
+
+          var T = add_wa( add_wa( rol(a, 5), sha1_ft(j, b, c, d)),
+                          add_wa( add_wa(e, W[j]), sha1_kt(j)) );
+          e = d;
+          d = c;
+          c = rol(b, 30);
+          b = a;
+          a = T;
+        }
+
+		 // 4. intermediate hash
+        H0 = add_wa(a, H0);
+        H1 = add_wa(b, H1);
+        H2 = add_wa(c, H2);
+        H3 = add_wa(d, H3);
+        H4 = add_wa(e, H4);
+      }
+
+      return Array(H0, H1, H2, H3, H4);
+    }
+  }()
+
+};
diff --git a/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/feature.xml b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/feature.xml
new file mode 100644
index 0000000..c4bb6f1
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/feature.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>org.openajax.hub-2.0.7</name>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="OpenAjax-mashup.js"/>
+    <script src="iframe.js"/>
+    <script src="crypto.js"/>
+    <api>
+      <exports type="rpc">openajax.pubsub</exports>
+      <uses type="rpc">openajax.pubsub</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="OpenAjax-mashup.js"/>
+    <script src="iframe.js"/>
+    <script src="inline.js"/> 
+    <script src="crypto.js"/>
+    <api>
+      <exports type="rpc">openajax.pubsub</exports>
+      <uses type="rpc">openajax.pubsub</uses>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/iframe.js b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/iframe.js
new file mode 100644
index 0000000..2375d0f
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/iframe.js
@@ -0,0 +1,891 @@
+/*
+
+        Copyright 2006-2009 OpenAjax Alliance
+
+        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.
+*/
+
+var OpenAjax = OpenAjax || {};
+OpenAjax.hub = OpenAjax.hub || {};
+OpenAjax.gadgets = typeof OpenAjax.gadgets === 'object' ? OpenAjax.gadgets :
+                   typeof gadgets === 'object' ? gadgets :
+                   {};
+OpenAjax.gadgets.rpctx = OpenAjax.gadgets.rpctx || {};
+
+(function() {
+    // For now, we only use "oaaConfig" for the global "gadgets" object.  If the "gadgets" global
+    // already exists, then there is no reason to check for "oaaConfig".  In the future, if we use
+    // "oaaConfig" for other purposes, we'll need to remove the check for "!window.gadgets".
+    if (typeof gadgets === 'undefined') {
+        // "oaaConfig" can be specified as a global object.  If not found, then look for it as an
+        // attribute on the script line for the OpenAjax Hub JS file.
+        if (typeof oaaConfig === 'undefined') {
+            var scripts = document.getElementsByTagName("script");
+            // match "OpenAjax-mashup.js", "OpenAjaxManagedHub-all*.js", "OpenAjaxManagedHub-core*.js"
+            var reHub = /openajax(?:managedhub-(?:all|core).*|-mashup)\.js$/i;
+            for ( var i = scripts.length - 1; i >= 0; i-- ) {
+                var src = scripts[i].getAttribute( "src" );
+                if ( !src ) {
+                    continue;
+                }
+                
+                var m = src.match( reHub );
+                if ( m ) {
+                    var config = scripts[i].getAttribute( "oaaConfig" );
+                    if ( config ) {
+                        try {
+                            oaaConfig = eval( "({ " + config + " })" );
+                        } catch (e) {}
+                    }
+                    break;
+                }
+            }
+        }
+        
+        if (typeof oaaConfig !== 'undefined' && oaaConfig.gadgetsGlobal) {
+            gadgets = OpenAjax.gadgets;
+        }
+    }
+})();
+
+
+if (!OpenAjax.hub.IframeContainer) {
+
+(function(){
+
+/**
+ * Create a new Iframe Container.
+ * @constructor
+ * @extends OpenAjax.hub.Container
+ * 
+ * IframeContainer implements the Container interface to provide a container
+ * that isolates client components into secure sandboxes by leveraging the
+ * isolation features provided by browser iframes.
+ * 
+ * SECURITY
+ * 
+ * In order for the connection between the IframeContainer and IframeHubClient
+ * to be fully secure, you must specify a valid 'tunnelURI'. Note that if you
+ * do specify a 'tunnelURI', then only the WPM and NIX transports are used,
+ * covering the following browsers:
+ *   IE 6+, Firefox 3+, Safari 4+, Chrome 2+, Opera 9+.
+ * 
+ * If no 'tunnelURI' is specified, then some security features are disabled:
+ * the IframeContainer will not report FramePhish errors, and on some browsers
+ * IframeContainer and IframeHubClient will not be able to validate the
+ * identity of their partner (i.e. getPartnerOrigin() will return 'null').
+ * However, not providing 'tunnelURI' allows the additional use of the RMR
+ * and FE transports -- in addition to the above browsers, the Hub code will
+ * also work on:
+ *   Firefox 1 & 2, Safari 2 & 3, Chrome 1.
+ * 
+ * @param {OpenAjax.hub.ManagedHub} hub
+ *    Managed Hub instance to which this Container belongs
+ * @param {String} clientID
+ *    A string ID that identifies a particular client of a Managed Hub. Unique
+ *    within the context of the ManagedHub.
+ * @param {Object} params  
+ *    Parameters used to instantiate the IframeContainer.
+ *    Once the constructor is called, the params object belongs exclusively to
+ *    the IframeContainer. The caller MUST not modify it.
+ *    The following are the pre-defined properties on params:
+ * @param {Function} params.Container.onSecurityAlert
+ *    Called when an attempted security breach is thwarted.  Function is defined
+ *    as follows:  function(container, securityAlert)
+ * @param {Function} [params.Container.onConnect]
+ *    Called when the client connects to the Managed Hub.  Function is defined
+ *    as follows:  function(container)
+ * @param {Function} [params.Container.onDisconnect]
+ *    Called when the client disconnects from the Managed Hub.  Function is
+ *    defined as follows:  function(container)
+ * @param {Object} [params.Container.scope]
+ *    Whenever one of the Container's callback functions is called, references
+ *    to "this" in the callback will refer to the scope object. If no scope is
+ *    provided, default is window.
+ * @param {Function} [params.Container.log]
+ *    Optional logger function. Would be used to log to console.log or
+ *    equivalent. 
+ * @param {Object} params.IframeContainer.parent
+ *    DOM element that is to be parent of iframe
+ * @param {String} params.IframeContainer.uri
+ *    Initial Iframe URI (Container will add parameters to this URI)
+ * @param {String} [params.IframeContainer.clientRelay]
+ *    URI of the relay file used by the client.  Must be from the same origin
+ *    as params.IframeContainer.uri.  This value is only used by the IFPC
+ *    transport layer, which is primarily used by IE 6 & 7. This value isn't
+ *    required if you don't need to support those browsers.
+ * @param {String} [params.IframeContainer.tunnelURI]
+ *    URI of the tunnel iframe. Must be from the same origin as the page which
+ *    instantiates the IframeContainer. If not specified, connection will not
+ *    be fully secure (see SECURITY section).
+ * @param {Object} [params.IframeContainer.iframeAttrs]
+ *    Attributes to add to IFRAME DOM entity.  For example:
+ *              { style: { width: "100%",
+ *                         height: "100%" },
+ *                className: "some_class" }
+ * @param {Number} [params.IframeContainer.timeout]
+ *    Load timeout in milliseconds.  If not specified, defaults to 15000.  If
+ *    the client at params.IframeContainer.uri does not establish a connection
+ *    with this container in the given time, the onSecurityAlert callback is
+ *    called with a LoadTimeout error code.
+ * @param {Function} [params.IframeContainer.seed]
+ *    A function that returns a string that will be used to seed the
+ *    pseudo-random number generator, which is used to create the security
+ *    tokens.  An implementation of IframeContainer may choose to ignore this
+ *    value.
+ * @param {Number} [params.IframeContainer.tokenLength]
+ *    Length of the security tokens used when transmitting messages.  If not
+ *    specified, defaults to 6.  An implementation of IframeContainer may choose
+ *    to ignore this value.
+ *
+ * @throws {OpenAjax.hub.Error.BadParameters}   if required params are not
+ *          present or null
+ * @throws {OpenAjax.hub.Error.Duplicate}   if a Container with this clientID
+ *          already exists in the given Managed Hub
+ * @throws {OpenAjax.hub.Error.Disconnected}   if hub is not connected
+ */
+OpenAjax.hub.IframeContainer = function( hub, clientID, params )
+{
+    assertValidParams( arguments );
+    
+    var container = this;
+    var scope = params.Container.scope || window;
+    var connected = false;
+    var subs = {};
+    var securityToken;
+    var internalID;
+    var timeout = params.IframeContainer.timeout || 15000;
+    var loadTimer;
+
+    if ( params.Container.log ) {
+        var log = function( msg ) {
+            try {
+                params.Container.log.call( scope, "IframeContainer::" + clientID + ": " + msg );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+            }
+        };
+    } else {
+        log = function() {};
+    }
+    
+    
+    this._init = function() {
+        // add to ManagedHub first, to see if clientID is a duplicate
+        hub.addContainer( this );
+        
+        // Create an "internal" ID, which is guaranteed to be unique within the
+        // window, not just within the hub.
+        internalID = OpenAjax.hub.IframeContainer._rpcRouter.add( clientID, this );
+        securityToken = generateSecurityToken( params, scope, log );
+        
+        var relay = params.IframeContainer.clientRelay;
+        var transportName = OpenAjax.gadgets.rpc.getRelayChannel();
+        if ( params.IframeContainer.tunnelURI ) {
+            if ( transportName !== "wpm" && transportName !== "ifpc" ) {
+                throw new Error( OpenAjax.hub.Error.IncompatBrowser );
+            }
+        } else {
+            log( "WARNING: Parameter 'IframeContaienr.tunnelURI' not specified. Connection will not be fully secure." );
+            if ( transportName === "rmr" && !relay ) {
+                relay = OpenAjax.gadgets.rpc.getOrigin( params.IframeContainer.uri ) + "/robots.txt"; 
+            }
+        }
+        
+        // Create IFRAME to hold the client
+        createIframe();
+        
+        OpenAjax.gadgets.rpc.setupReceiver( internalID, relay );
+        
+        startLoadTimer();
+    };
+
+        
+  /*** OpenAjax.hub.Container interface ***/
+   
+    this.sendToClient = function( topic, data, subscriptionID ) {
+        OpenAjax.gadgets.rpc.call( internalID, "openajax.pubsub", null, "pub", topic, data,
+                                   subscriptionID );
+    };
+
+    this.remove = function() {
+        finishDisconnect();
+        clearTimeout( loadTimer );
+        OpenAjax.gadgets.rpc.removeReceiver( internalID );
+        var iframe = document.getElementById( internalID );
+        iframe.parentNode.removeChild( iframe );
+        OpenAjax.hub.IframeContainer._rpcRouter.remove( internalID );
+    };
+
+    this.isConnected = function() {
+        return connected;
+    };
+    
+    this.getClientID = function() {
+        return clientID;
+    };
+
+    this.getPartnerOrigin = function() {
+        if ( connected ) {
+            var origin = OpenAjax.gadgets.rpc.getReceiverOrigin( internalID );
+            if ( origin ) {
+                // remove port if present
+                return ( /^([a-zA-Z]+:\/\/[^:]+).*/.exec( origin )[1] );
+            }
+        }
+        return null;
+    };
+    
+    this.getParameters = function() {
+        return params;
+    };
+    
+    this.getHub = function() {
+        return hub;
+    };
+    
+    
+  /*** OpenAjax.hub.IframeContainer interface ***/
+    
+    /**
+     * Get the iframe associated with this iframe container
+     * 
+     * This function returns the iframe associated with an IframeContainer,
+     * allowing the Manager Application to change its size, styles, scrollbars, etc.
+     * 
+     * CAUTION: The iframe is owned exclusively by the IframeContainer. The Manager
+     * Application MUST NOT destroy the iframe directly. Also, if the iframe is
+     * hidden and disconnected, the Manager Application SHOULD NOT attempt to make
+     * it visible. The Container SHOULD automatically hide the iframe when it is
+     * disconnected; to make it visible would introduce security risks. 
+     * 
+     * @returns iframeElement
+     * @type {Object}
+     */
+    this.getIframe = function() {
+        return document.getElementById( internalID );
+    };
+    
+    
+  /*** private functions ***/
+
+    function assertValidParams( args ) {
+        var hub = args[0],
+            clientID = args[1],
+            params = args[2];
+        if ( ! hub || ! clientID || ! params || ! params.Container ||
+             ! params.Container.onSecurityAlert || ! params.IframeContainer ||
+             ! params.IframeContainer.parent || ! params.IframeContainer.uri ) {
+            throw new Error( OpenAjax.hub.Error.BadParameters );
+        }
+    }
+    
+    this._handleIncomingRPC = function( command, topic, data ) {
+        switch ( command ) {
+            // publish
+            // 'data' is topic message
+            case "pub":
+                hub.publishForClient( container, topic, data );
+                break;
+            
+            // subscribe
+            // 'data' is subscription ID
+            case "sub":
+                var errCode = "";  // empty string is success
+                try {
+                    subs[ data ] = hub.subscribeForClient( container, topic, data );
+                } catch( e ) {
+                    errCode = e.message;
+                }
+                return errCode;
+            
+            // unsubscribe
+            // 'data' is subscription ID
+            case "uns":
+                var handle = subs[ data ];
+                hub.unsubscribeForClient( container, handle );
+                delete subs[ data ];
+                return data;
+            
+            // connect
+            case "con":
+                finishConnect();
+                return true;
+            
+            // disconnect
+            case "dis":
+                startLoadTimer();
+                finishDisconnect();
+                if ( params.Container.onDisconnect ) {
+                    try {
+                        params.Container.onDisconnect.call( scope, container );
+                    } catch( e ) {
+                        OpenAjax.hub._debugger();
+                        log( "caught error from onDisconnect callback to constructor: " + e.message );
+                    }
+                }
+                return true;
+        }
+    };
+    
+    this._onSecurityAlert = function( error ) {
+        invokeSecurityAlert( rpcErrorsToOAA[ error ] );
+    };
+    
+    // The RPC code requires that the 'name' attribute be properly set on the
+    // iframe.  However, setting the 'name' property on the iframe object
+    // returned from 'createElement("iframe")' doesn't work on IE --
+    // 'window.name' returns null for the code within the iframe.  The
+    // workaround is to set the 'innerHTML' of a span to the iframe's HTML code,
+    // with 'name' and other attributes properly set.
+    function createIframe() {
+        var span = document.createElement( "span" );
+        params.IframeContainer.parent.appendChild( span );
+        
+        var iframeText = '<iframe id="' + internalID + '" name="' + internalID +
+                '" src="javascript:\'<html></html>\'"';
+        
+        // Add iframe attributes
+        var styleText = '';
+        var attrs = params.IframeContainer.iframeAttrs;
+        if ( attrs ) {
+            for ( var attr in attrs ) {
+                switch ( attr ) {
+                    case "style":
+                        for ( var style in attrs.style ) {
+                            styleText += style + ':' + attrs.style[ style ] + ';';
+                        }
+                        break;
+                    case "className":
+                        iframeText += ' class="' + attrs[ attr ] + '"';
+                        break;
+                    default:
+                        iframeText += ' ' + attr + '="' + attrs[ attr ] + '"';
+                }
+            }
+        }
+        
+        // initially hide IFRAME content, in order to lessen frame phishing impact
+        styleText += 'visibility:hidden;';
+        iframeText += ' style="' + styleText + '"></iframe>';
+        
+        span.innerHTML = iframeText;
+        
+        var tunnelText;
+        if ( params.IframeContainer.tunnelURI ) {
+            tunnelText = "&parent=" + encodeURIComponent( params.IframeContainer.tunnelURI ) +
+                         "&forcesecure=true";
+        } else {
+            tunnelText = "&oahParent=" +
+                         encodeURIComponent( OpenAjax.gadgets.rpc.getOrigin( window.location.href ));
+        }
+        var idText = "";
+        if ( internalID !== clientID ) {
+            idText = "&oahId=" + internalID.substring( internalID.lastIndexOf('_') + 1 );
+        }
+
+        var iframe = document.getElementById( internalID );
+        if(iframe.attachEvent) {
+          //Works for IE
+          iframe.attachEvent('onload', params.IframeContainer.onGadgetLoad);
+        } else {
+          iframe.onload = params.IframeContainer.onGadgetLoad;
+        }
+
+        var uri = params.IframeContainer.uri;
+        var hashIdx = uri.indexOf('#');
+        var joinToken = (hashIdx === -1)?'#':'&';
+
+        iframe.src = uri + joinToken + "rpctoken=" + securityToken + tunnelText + idText;
+    }
+
+    // If the relay iframe used by RPC has not been loaded yet, then we won't have unload protection
+    // at this point.  Since we can't detect when the relay iframe has loaded, we use a two stage
+    // connection process.  First, the child sends a connection msg and the container sends an ack.
+    // Then the container sends a connection msg and the child replies with an ack.  Since the
+    // container can only send a message if the relay iframe has loaded, then we know if we get an
+    // ack here that the relay iframe is ready.  And we are fully connected.
+    function finishConnect() {
+        // connect acknowledgement
+        function callback( result ) {
+            if ( result ) {
+                connected = true;
+                clearTimeout( loadTimer );
+                document.getElementById( internalID ).style.visibility = "visible";
+                if ( params.Container.onConnect ) {
+                    try {
+                        params.Container.onConnect.call( scope, container );
+                    } catch( e ) {
+                        OpenAjax.hub._debugger();
+                        log( "caught error from onConnect callback to constructor: " + e.message );
+                    }
+                }
+            }
+        }
+        OpenAjax.gadgets.rpc.call( internalID, "openajax.pubsub", callback, "cmd", "con" );
+    }
+    
+    function finishDisconnect() {
+        if ( connected ) {
+            connected = false;
+            document.getElementById( internalID ).style.visibility = "hidden";
+        
+            // unsubscribe from all subs
+            for ( var s in subs ) {
+                hub.unsubscribeForClient( container, subs[s] );
+            }
+            subs = {};
+        }
+    }
+    
+    function invokeSecurityAlert( errorMsg ) {
+        try {
+            params.Container.onSecurityAlert.call( scope, container, errorMsg );
+        } catch( e ) {
+            OpenAjax.hub._debugger();
+            log( "caught error from onSecurityAlert callback to constructor: " + e.message );
+        }
+    }
+    
+    function startLoadTimer() {
+        loadTimer = setTimeout(
+            function() {
+                // alert the security alert callback
+                invokeSecurityAlert( OpenAjax.hub.SecurityAlert.LoadTimeout );
+                // don't receive any more messages from HubClient
+                container._handleIncomingRPC = function() {};
+            },
+            timeout
+        );
+    }
+    
+    
+    this._init();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Create a new IframeHubClient.
+ * @constructor
+ * @extends OpenAjax.hub.HubClient
+ * 
+ * @param {Object} params
+ *    Once the constructor is called, the params object belongs to the
+ *    HubClient. The caller MUST not modify it.
+ *    The following are the pre-defined properties on params:
+ * @param {Function} params.HubClient.onSecurityAlert
+ *     Called when an attempted security breach is thwarted
+ * @param {Object} [params.HubClient.scope]
+ *     Whenever one of the HubClient's callback functions is called,
+ *     references to "this" in the callback will refer to the scope object.
+ *     If not provided, the default is window.
+ * @param {Function} [params.HubClient.log]
+ *     Optional logger function. Would be used to log to console.log or
+ *     equivalent. 
+ * @param {Boolean} [params.IframeHubClient.requireParentVerifiable]
+ *     Set to true in order to require that this IframeHubClient use a
+ *     transport that can verify the parent Container's identity.
+ * @param {Function} [params.IframeHubClient.seed]
+ *     A function that returns a string that will be used to seed the
+ *     pseudo-random number generator, which is used to create the security
+ *     tokens.  An implementation of IframeHubClient may choose to ignore
+ *     this value.
+ * @param {Number} [params.IframeHubClient.tokenLength]
+ *     Length of the security tokens used when transmitting messages.  If
+ *     not specified, defaults to 6.  An implementation of IframeHubClient
+ *     may choose to ignore this value.
+ *     
+ * @throws {OpenAjax.hub.Error.BadParameters} if any of the required
+ *          parameters is missing, or if a parameter value is invalid in 
+ *          some way.
+ */
+OpenAjax.hub.IframeHubClient = function( params )
+{
+    if ( ! params || ! params.HubClient || ! params.HubClient.onSecurityAlert ) {
+        throw new Error( OpenAjax.hub.Error.BadParameters );
+    }
+    
+    var client = this;
+    var scope = params.HubClient.scope || window;
+    var connected = false;
+    var subs = {};
+    var subIndex = 0;
+    var clientID;
+//    var securityToken;    // XXX still need "securityToken"?
+    
+    if ( params.HubClient.log ) {
+        var log = function( msg ) {
+            try {
+                params.HubClient.log.call( scope, "IframeHubClient::" + clientID + ": " + msg );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+            }
+        };
+    } else {
+        log = function() {};
+    }
+    
+    this._init = function() {
+        var oaGadgets = OpenAjax.gadgets;
+        var urlParams = oaGadgets.util.getUrlParameters();
+        if ( ! urlParams.parent ) {
+            // The RMR transport does not require a valid relay file, but does need a URL
+            // in the parent's domain. The URL does not need to point to valid file, so just
+            // point to 'robots.txt' file. See RMR transport code for more info.
+            var parent = urlParams['oahParent'] + "/robots.txt";
+            oaGadgets.rpc.setupReceiver( "..", parent );
+        }
+        
+        if ( params.IframeHubClient && params.IframeHubClient.requireParentVerifiable &&
+                oaGadgets.rpc.getReceiverOrigin( ".." ) === null ) {
+            // If user set 'requireParentVerifiable' to true but RPC transport does not
+            // support this, throw error.
+            oaGadgets.rpc.removeReceiver( ".." );
+            throw new Error( OpenAjax.hub.Error.IncompatBrowser );
+        }
+        
+        OpenAjax.hub.IframeContainer._rpcRouter.add( "..", this );
+// XXX The RPC layer initializes immediately on load, in the child (IframeHubClient). So it is too
+//    late here to specify a security token for the RPC layer.  At the moment, only the NIX
+//    transport requires a child token (IFPC [aka FIM] is not supported).
+//        securityToken = generateSecurityToken( params, scope, log );
+
+        clientID = OpenAjax.gadgets.rpc.RPC_ID;
+        if ( urlParams['oahId'] ) {
+            clientID = clientID.substring( 0, clientID.lastIndexOf('_') );
+        }
+    };
+    
+  /*** HubClient interface ***/
+
+    this.connect = function( onComplete, scope ) {
+        if ( connected ) {
+            throw new Error( OpenAjax.hub.Error.Duplicate );
+        }
+        
+        // connect acknowledgement
+        function callback( result ) {
+            if ( result ) {
+                connected = true;
+                if ( onComplete ) {
+                    try {
+                        onComplete.call( scope || window, client, true );
+                    } catch( e ) {
+                        OpenAjax.hub._debugger();
+                        log( "caught error from onComplete callback to connect(): " + e.message );
+                    }
+                }
+            }
+        }
+        OpenAjax.gadgets.rpc.call( "..", "openajax.pubsub", callback, "con" );
+    };
+    
+    this.disconnect = function( onComplete, scope ) {
+        if ( !connected ) {
+            throw new Error( OpenAjax.hub.Error.Disconnected );
+        }
+        
+        connected = false;
+        
+        // disconnect acknowledgement
+        var callback = null;
+        if ( onComplete ) {
+            callback = function( result ) {
+                try {
+                    onComplete.call( scope || window, client, true );
+                } catch( e ) {
+                    OpenAjax.hub._debugger();
+                    log( "caught error from onComplete callback to disconnect(): " + e.message );
+                }
+            };
+        }
+        OpenAjax.gadgets.rpc.call( "..", "openajax.pubsub", callback, "dis" );
+    };
+    
+    this.getPartnerOrigin = function() {
+        if ( connected ) {
+            var origin = OpenAjax.gadgets.rpc.getReceiverOrigin( ".." );
+            if ( origin ) {
+                // remove port if present
+                return ( /^([a-zA-Z]+:\/\/[^:]+).*/.exec( origin )[1] );
+            }
+        }
+        return null;
+    };
+    
+    this.getClientID = function() {
+        return clientID;
+    };
+    
+  /*** Hub interface ***/
+    
+    this.subscribe = function( topic, onData, scope, onComplete, subscriberData ) {
+        assertConn();
+        assertSubTopic( topic );
+        if ( ! onData ) {
+            throw new Error( OpenAjax.hub.Error.BadParameters );
+        }
+    
+        scope = scope || window;
+        var subID = "" + subIndex++;
+        subs[ subID ] = { cb: onData, sc: scope, d: subscriberData };
+        
+        // subscribe acknowledgement
+        function callback( result ) {
+            if ( result !== '' ) {    // error
+                delete subs[ subID ];
+            }
+            if ( onComplete ) {
+                try {
+                    onComplete.call( scope, subID, result === "", result );
+                } catch( e ) {
+                    OpenAjax.hub._debugger();
+                    log( "caught error from onComplete callback to subscribe(): " + e.message );
+                }
+            }
+        }
+        OpenAjax.gadgets.rpc.call( "..", "openajax.pubsub", callback, "sub", topic, subID );
+        
+        return subID;
+    };
+    
+    this.publish = function( topic, data ) {
+        assertConn();
+        assertPubTopic( topic );
+        OpenAjax.gadgets.rpc.call( "..", "openajax.pubsub", null, "pub", topic, data );
+    };
+    
+    this.unsubscribe = function( subscriptionID, onComplete, scope ) {
+        assertConn();
+        if ( ! subscriptionID ) {
+            throw new Error( OpenAjax.hub.Error.BadParameters );
+        }
+        
+        // if no such subscriptionID, or in process of unsubscribing given ID, throw error
+        if ( ! subs[ subscriptionID ] || subs[ subscriptionID ].uns ) {
+            throw new Error( OpenAjax.hub.Error.NoSubscription );
+        }
+        
+        // unsubscribe in progress
+        subs[ subscriptionID ].uns = true;
+        
+        // unsubscribe acknowledgement
+        function callback( result ) {
+            delete subs[ subscriptionID ];
+            if ( onComplete ) {
+                try {
+                    onComplete.call( scope || window, subscriptionID, true );
+                } catch( e ) {
+                    OpenAjax.hub._debugger();
+                    log( "caught error from onComplete callback to unsubscribe(): " + e.message );
+                }
+            }
+        }
+        OpenAjax.gadgets.rpc.call( "..", "openajax.pubsub", callback, "uns", null, subscriptionID );
+    };
+    
+    this.isConnected = function() {
+        return connected;
+    };
+    
+    this.getScope = function() {
+        return scope;
+    };
+    
+    this.getSubscriberData = function( subscriptionID ) {
+        assertConn();
+        if ( subs[ subscriptionID ] ) {
+            return subs[ subscriptionID ].d;
+        }
+        throw new Error( OpenAjax.hub.Error.NoSubscription );
+    };
+    
+    this.getSubscriberScope = function( subscriptionID ) {
+        assertConn();
+        if ( subs[ subscriptionID ] ) {
+            return subs[ subscriptionID ].sc;
+        }
+        throw new Error( OpenAjax.hub.Error.NoSubscription );
+    };
+    
+    this.getParameters = function() {
+        return params;
+    };
+    
+  /*** private functions ***/
+    
+    this._handleIncomingRPC = function( command, topic, data, subscriptionID ) {
+        if ( command === "pub" ) {
+            // if subscription exists and we are not in process of unsubscribing...
+            if ( subs[ subscriptionID ] && ! subs[ subscriptionID ].uns ) {
+                try {
+                    subs[ subscriptionID ].cb.call( subs[ subscriptionID ].sc, topic,
+                            data, subs[ subscriptionID ].d );
+                } catch( e ) {
+                    OpenAjax.hub._debugger();
+                    log( "caught error from onData callback to subscribe(): " + e.message );
+                }
+            }
+        }
+        // else if command === "cmd"...
+        
+        // First time this function is called, topic should be "con".  This is the 2nd stage of the
+        // connection process.  Simply need to return "true" in order to send an acknowledgement
+        // back to container.  See finishConnect() in the container object.
+        if ( topic === "con" ) {
+          return true;
+        }
+        return false;
+    };
+    
+    function assertConn() {
+        if ( ! connected ) {
+            throw new Error( OpenAjax.hub.Error.Disconnected );
+        }
+    }
+    
+    function assertSubTopic( topic )
+    {
+        if ( ! topic ) {
+            throw new Error( OpenAjax.hub.Error.BadParameters );
+        }
+        var path = topic.split(".");
+        var len = path.length;
+        for (var i = 0; i < len; i++) {
+            var p = path[i];
+            if ((p === "") ||
+               ((p.indexOf("*") != -1) && (p != "*") && (p != "**"))) {
+                throw new Error( OpenAjax.hub.Error.BadParameters );
+            }
+            if ((p == "**") && (i < len - 1)) {
+                throw new Error( OpenAjax.hub.Error.BadParameters );
+            }
+        }
+    }
+    
+    function assertPubTopic( topic ) {
+        if ( !topic || topic === "" || (topic.indexOf("*") != -1) ||
+            (topic.indexOf("..") != -1) ||  (topic.charAt(0) == ".") ||
+            (topic.charAt(topic.length-1) == "."))
+        {
+            throw new Error( OpenAjax.hub.Error.BadParameters );
+        }
+    }
+    
+//    function invokeSecurityAlert( errorMsg ) {
+//        try {
+//            params.HubClient.onSecurityAlert.call( scope, client, errorMsg );
+//        } catch( e ) {
+//            OpenAjax.hub._debugger();
+//            log( "caught error from onSecurityAlert callback to constructor: " + e.message );
+//        }
+//    }
+
+    
+    this._init();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+    // RPC object contents:
+    //   s: Service Name
+    //   f: From
+    //   c: The callback ID or 0 if none.
+    //   a: The arguments for this RPC call.
+    //   t: The authentication token.
+OpenAjax.hub.IframeContainer._rpcRouter = function() {
+    var receivers = {};
+    
+    function router() {
+        var r = receivers[ this.f ];
+        if ( r ) {
+            return r._handleIncomingRPC.apply( r, arguments );
+        }
+    }
+    
+    function onSecurityAlert( receiverId, error ) {
+        var r = receivers[ receiverId ];
+        if ( r ) {
+          r._onSecurityAlert.call( r, error );
+        }
+    }
+    
+    return {
+        add: function( id, receiver ) {
+            function _add( id, receiver ) {
+                if ( id === ".." ) {
+                    if ( ! receivers[ ".." ] ) {
+                        receivers[ ".." ] = receiver;
+                    }
+                    return;
+                }
+                
+                var newId = id;
+                while ( document.getElementById(newId) ) {
+                    // a client with the specified ID already exists on this page;
+                    // create a unique ID
+                    newId = id + '_' + ((0x7fff * Math.random()) | 0).toString(16);
+                };
+                receivers[ newId ] = receiver;
+                return newId;
+            }
+            
+            // when this function is first called, register the RPC service
+            OpenAjax.gadgets.rpc.register( "openajax.pubsub", router );
+            OpenAjax.gadgets.rpc.config({
+                securityCallback: onSecurityAlert
+            });
+
+            rpcErrorsToOAA[ OpenAjax.gadgets.rpc.SEC_ERROR_LOAD_TIMEOUT ] = OpenAjax.hub.SecurityAlert.LoadTimeout;
+            rpcErrorsToOAA[ OpenAjax.gadgets.rpc.SEC_ERROR_FRAME_PHISH ] = OpenAjax.hub.SecurityAlert.FramePhish;
+            rpcErrorsToOAA[ OpenAjax.gadgets.rpc.SEC_ERROR_FORGED_MSG ] = OpenAjax.hub.SecurityAlert.ForgedMsg;
+            
+            this.add = _add;
+            return _add( id, receiver );
+        },
+        
+        remove: function( id ) {
+            delete receivers[ id ];
+        }
+    };
+}();
+
+var rpcErrorsToOAA = {};
+
+////////////////////////////////////////////////////////////////////////////////
+
+function generateSecurityToken( params, scope, log ) {
+    if ( ! OpenAjax.hub.IframeContainer._prng ) {
+        // create pseudo-random number generator with a default seed
+        var seed = new Date().getTime() + Math.random() + document.cookie;
+        OpenAjax.hub.IframeContainer._prng = OpenAjax._smash.crypto.newPRNG( seed );
+    }
+    
+    var p = params.IframeContainer || params.IframeHubClient;
+    if ( p && p.seed ) {
+        try {
+            var extraSeed = p.seed.call( scope );
+            OpenAjax.hub.IframeContainer._prng.addSeed( extraSeed );
+        } catch( e ) {
+            OpenAjax.hub._debugger();
+            log( "caught error from 'seed' callback: " + e.message );
+        }
+    }
+    
+    var tokenLength = (p && p.tokenLength) || 6;
+    return OpenAjax.hub.IframeContainer._prng.nextRandomB64Str( tokenLength );
+}
+
+})();
+}
diff --git a/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/inline.js b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/inline.js
new file mode 100644
index 0000000..62a759f
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/org.openajax.hub-2.0.7/inline.js
@@ -0,0 +1,549 @@
+/*
+
+        Copyright 2006-2009 OpenAjax Alliance
+
+        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.
+*/
+
+/**
+ * Create a new Inline Container.
+ * @constructor
+ * @extends OpenAjax.hub.Container
+ *
+ * InlineContainer implements the Container interface to provide a container
+ * that places components within the same browser frame as the main mashup
+ * application. As such, this container does not isolate client components into
+ * secure sandboxes.
+ * 
+ * @param {OpenAjax.hub.ManagedHub} hub
+ *    Managed Hub instance to which this Container belongs
+ * @param {String} clientID
+ *    A string ID that identifies a particular client of a Managed Hub. Unique
+ *    within the context of the ManagedHub.
+ * @param {Object} params  
+ *    Parameters used to instantiate the InlineContainer.
+ *    Once the constructor is called, the params object belongs exclusively to
+ *    the InlineContainer. The caller MUST not modify it.
+ *    The following are the pre-defined properties on params:
+ * @param {Function} params.Container.onSecurityAlert
+ *    Called when an attempted security breach is thwarted.  Function is defined
+ *    as follows:  function(container, securityAlert)
+ * @param {Function} [params.Container.onConnect]
+ *    Called when the client connects to the Managed Hub.  Function is defined
+ *    as follows:  function(container)
+ * @param {Function} [params.Container.onDisconnect]
+ *    Called when the client disconnects from the Managed Hub.  Function is
+ *    defined as follows:  function(container)
+ * @param {Object} [params.Container.scope]
+ *    Whenever one of the Container's callback functions is called, references
+ *    to "this" in the callback will refer to the scope object. If no scope is
+ *    provided, default is window.
+ * @param {Function} [params.Container.log]
+ *    Optional logger function. Would be used to log to console.log or
+ *    equivalent. 
+ *
+ * @throws {OpenAjax.hub.Error.BadParameters}   if required params are not
+ *    present or null
+ * @throws {OpenAjax.hub.Error.Duplicate}   if a Container with this clientID
+ *    already exists in the given Managed Hub
+ * @throws {OpenAjax.hub.Error.Disconnected}   if ManagedHub is not connected
+ */
+OpenAjax.hub.InlineContainer = function( hub, clientID, params )
+{
+    if ( ! hub || ! clientID || ! params ||
+            ! params.Container || ! params.Container.onSecurityAlert ) {
+        throw new Error(OpenAjax.hub.Error.BadParameters);
+    }
+    
+    var cbScope = params.Container.scope || window;
+    var connected = false;
+    var subs = [];
+    var subIndex = 0;
+    var client = null;
+    
+    if ( params.Container.log ) {
+        var log = function( msg ) {
+            try {
+                params.Container.log.call( cbScope, "InlineContainer::" + clientID + ": " + msg );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+            }
+        };
+    } else {
+        log = function() {};
+    }
+    
+    this._init = function() {
+        hub.addContainer( this );
+    };
+
+  /*** OpenAjax.hub.Container interface implementation ***/
+    
+    this.getHub = function() {
+    	return hub;
+    };
+    
+    this.sendToClient = function( topic, data, subscriptionID ) {
+        if ( connected ) {
+            var sub = subs[ subscriptionID ];
+            try {
+                sub.cb.call( sub.sc, topic, data, sub.d );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+                client._log( "caught error from onData callback to HubClient.subscribe(): " + e.message );
+            }
+        }
+    };
+    
+    this.remove = function() {
+        if ( connected ) {
+            finishDisconnect();
+        }
+    };
+    
+    this.isConnected = function() {
+        return connected;
+    };
+    
+    this.getClientID = function() {
+        return clientID;
+    };
+    
+    this.getPartnerOrigin = function() {
+        if ( connected ) {
+            return window.location.protocol + "//" + window.location.hostname;
+        }
+        return null;
+    };
+    
+    this.getParameters = function() {
+        return params;
+    };
+    
+  /*** OpenAjax.hub.HubClient interface implementation ***/
+    
+    this.connect = function( hubClient, onComplete, scope ) {
+        if ( connected ) {
+            throw new Error( OpenAjax.hub.Error.Duplicate );
+        }
+        
+        connected = true;
+        client = hubClient;
+        
+        if ( params.Container.onConnect ) {
+            try {
+                params.Container.onConnect.call( cbScope, this );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+                log( "caught error from onConnect callback to constructor: " + e.message );
+            }
+        }
+        
+        invokeOnComplete( onComplete, scope, hubClient, true );
+    };
+    
+    this.disconnect = function( hubClient, onComplete, scope ) {
+        if ( ! connected ) {
+            throw new Error( OpenAjax.hub.Error.Disconnected );
+        }
+        
+        finishDisconnect();
+    
+        if ( params.Container.onDisconnect ) {
+            try {
+                params.Container.onDisconnect.call( cbScope, this );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+                log( "caught error from onDisconnect callback to constructor: " + e.message );
+            }
+        }
+        
+        invokeOnComplete( onComplete, scope, hubClient, true );
+    };
+    
+  /*** OpenAjax.hub.Hub interface implementation ***/
+    
+    this.subscribe = function( topic, onData, scope, onComplete, subscriberData ) {
+        assertConn();
+        assertSubTopic( topic );
+        if ( ! onData ) {
+            throw new Error( OpenAjax.hub.Error.BadParameters );
+        }
+        
+        var subID = "" + subIndex++;
+        var success = false;
+        var msg = null;
+        try {
+            var handle = hub.subscribeForClient( this, topic, subID );
+            success = true;
+        } catch( e ) {
+            // failure
+            subID = null;
+            msg = e.message;
+        }
+        
+        scope = scope || window;
+        if ( success ) {
+            subs[ subID ] = { h: handle, cb: onData, sc: scope, d: subscriberData };
+        }
+        
+        invokeOnComplete( onComplete, scope, subID, success, msg );
+        return subID;
+    };
+    
+    this.publish = function( topic, data ) {
+        assertConn();
+        assertPubTopic( topic );
+        hub.publishForClient( this, topic, data );
+    };
+    
+    this.unsubscribe = function( subscriptionID, onComplete, scope ) {
+        assertConn();
+        if ( typeof subscriptionID === "undefined" || subscriptionID === null ) {
+            throw new Error( OpenAjax.hub.Error.BadParameters );
+        }
+        var sub = subs[ subscriptionID ];
+        if ( ! sub ) { 
+            throw new Error( OpenAjax.hub.Error.NoSubscription );
+        }    
+        hub.unsubscribeForClient( this, sub.h );
+        delete subs[ subscriptionID ];
+        
+        invokeOnComplete( onComplete, scope, subscriptionID, true );
+    };
+    
+    this.getSubscriberData = function( subID ) {
+        assertConn();
+        return getSubscription( subID ).d;
+    };
+    
+    this.getSubscriberScope = function( subID ) {
+        assertConn();
+        return getSubscription( subID ).sc;
+    };
+    
+  /*** PRIVATE FUNCTIONS ***/
+    
+    function invokeOnComplete( func, scope, item, success, errorCode ) {
+        if ( func ) { // onComplete is optional
+            try {
+                scope = scope || window;
+                func.call( scope, item, success, errorCode );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+                // invokeOnComplete is only called for client interfaces (Hub and HubClient)
+                client._log( "caught error from onComplete callback: " + e.message );
+            }
+        }
+    }
+    
+    function finishDisconnect() {
+        for ( var subID in subs ) {
+            hub.unsubscribeForClient( this, subs[subID].h );
+        }
+        subs = [];
+        subIndex = 0;
+        connected = false;
+    }
+    
+    function assertConn() {
+        if ( ! connected ) {
+            throw new Error( OpenAjax.hub.Error.Disconnected );
+        }
+    }
+    
+    function assertPubTopic( topic ) {
+        if ((topic == null) || (topic === "") || (topic.indexOf("*") != -1) ||
+            (topic.indexOf("..") != -1) ||  (topic.charAt(0) == ".") ||
+            (topic.charAt(topic.length-1) == "."))
+        {
+            throw new Error(OpenAjax.hub.Error.BadParameters);
+        }
+    }
+    
+    function assertSubTopic( topic ) {
+        if ( ! topic ) {
+            throw new Error(OpenAjax.hub.Error.BadParameters);
+        }
+        var path = topic.split(".");
+        var len = path.length;
+        for (var i = 0; i < len; i++) {
+            var p = path[i];
+            if ((p === "") ||
+               ((p.indexOf("*") != -1) && (p != "*") && (p != "**"))) {
+                throw new Error(OpenAjax.hub.Error.BadParameters);
+            }
+            if ((p == "**") && (i < len - 1)) {
+                throw new Error(OpenAjax.hub.Error.BadParameters);
+            }
+        }
+    }
+    
+    function getSubscription( subID ) {
+        var sub = subs[ subID ];
+        if ( sub ) {
+            return sub;
+        }
+        throw new Error( OpenAjax.hub.Error.NoSubscription );
+    }
+    
+    
+    this._init();
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Create a new InlineHubClient.
+ * @constructor
+ * @extends OpenAjax.hub.HubClient
+ * 
+ * @param {Object} params 
+ *    Parameters used to instantiate the HubClient.
+ *    Once the constructor is called, the params object belongs to the
+ *    HubClient. The caller MUST not modify it.
+ *    The following are the pre-defined properties on params:
+ * @param {Function} params.HubClient.onSecurityAlert
+ *     Called when an attempted security breach is thwarted
+ * @param {Object} [params.HubClient.scope]
+ *     Whenever one of the HubClient's callback functions is called,
+ *     references to "this" in the callback will refer to the scope object.
+ *     If not provided, the default is window.
+ * @param {Function} [params.HubClient.log]
+ *     Optional logger function. Would be used to log to console.log or
+ *     equivalent. 
+ * @param {OpenAjax.hub.InlineContainer} params.InlineHubClient.container
+ *     Specifies the InlineContainer to which this HubClient will connect
+ *  
+ * @throws {OpenAjax.hub.Error.BadParameters} if any of the required
+ *     parameters are missing
+ */
+OpenAjax.hub.InlineHubClient = function( params )
+{
+    if ( ! params || ! params.HubClient || ! params.HubClient.onSecurityAlert ||
+            ! params.InlineHubClient || ! params.InlineHubClient.container ) {
+        throw new Error(OpenAjax.hub.Error.BadParameters);
+    }
+    
+    var container = params.InlineHubClient.container;
+    var scope = params.HubClient.scope || window;
+    
+    if ( params.HubClient.log ) {
+        var log = function( msg ) {
+            try {
+                params.HubClient.log.call( scope, "InlineHubClient::" + container.getClientID() + ": " + msg );
+            } catch( e ) {
+                OpenAjax.hub._debugger();
+            }
+        };
+    } else {
+        log = function() {};
+    }
+    this._log = log;
+
+  /*** OpenAjax.hub.HubClient interface implementation ***/
+    
+    /**
+     * Requests a connection to the ManagedHub, via the InlineContainer
+     * associated with this InlineHubClient.
+     * 
+     * If the Container accepts the connection request, this HubClient's 
+     * state is set to CONNECTED and the HubClient invokes the 
+     * onComplete callback function.
+     * 
+     * If the Container refuses the connection request, the HubClient
+     * invokes the onComplete callback function with an error code. 
+     * The error code might, for example, indicate that the Container 
+     * is being destroyed.
+     * 
+     * If the HubClient is already connected, calling connect will cause
+     * the HubClient to immediately invoke the onComplete callback with
+     * the error code OpenAjax.hub.Error.Duplicate.
+     * 
+     * @param {Function} [onComplete]
+     *     Callback function to call when this operation completes.
+     * @param {Object} [scope]  
+     *     When the onComplete function is invoked, the JavaScript "this"
+     *     keyword refers to this scope object.
+     *     If no scope is provided, default is window.
+     *    
+     * In this implementation of InlineHubClient, this function operates 
+     * SYNCHRONOUSLY, so the onComplete callback function is invoked before 
+     * this connect function returns. Developers are cautioned that in  
+     * IframeHubClient implementations, this is not the case.
+     * 
+     * A client application may call InlineHubClient.disconnect and then call
+     * InlineHubClient.connect to reconnect to the Managed Hub.
+     */
+    this.connect = function( onComplete, scope ) {
+        container.connect( this, onComplete, scope );
+    };
+    
+    /**
+     * Disconnect from the ManagedHub
+     * 
+     * Disconnect immediately:
+     * 
+     * 1. Sets the HubClient's state to DISCONNECTED.
+     * 2. Causes the HubClient to send a Disconnect request to the 
+     * 		associated Container. 
+     * 3. Ensures that the client application will receive no more
+     * 		onData or onComplete callbacks associated with this 
+     * 		connection, except for the disconnect function's own
+     * 		onComplete callback.
+     * 4. Automatically destroys all of the HubClient's subscriptions.
+     * 	
+     * @param {Function} [onComplete]
+     *     Callback function to call when this operation completes.
+     * @param {Object} [scope]  
+     *     When the onComplete function is invoked, the JavaScript "this"
+     *     keyword refers to the scope object.
+     *     If no scope is provided, default is window.
+     *    
+     * In this implementation of InlineHubClient, the disconnect function operates 
+     * SYNCHRONOUSLY, so the onComplete callback function is invoked before 
+     * this function returns. Developers are cautioned that in IframeHubClient 
+     * implementations, this is not the case.   
+     * 
+     * A client application is allowed to call HubClient.disconnect and 
+     * then call HubClient.connect in order to reconnect.
+     */
+    this.disconnect = function( onComplete, scope ) {
+        container.disconnect( this, onComplete, scope );
+    };
+    
+    this.getPartnerOrigin = function() {
+        return container.getPartnerOrigin();
+    };
+    
+    this.getClientID = function() {
+        return container.getClientID();
+    };
+    
+  /*** OpenAjax.hub.Hub interface implementation ***/
+    
+    /**
+     * Subscribe to a topic.
+     *
+     * @param {String} topic
+     *     A valid topic string. MAY include wildcards.
+     * @param {Function} onData   
+     *     Callback function that is invoked whenever an event is 
+     *     published on the topic
+     * @param {Object} [scope]
+     *     When onData callback or onComplete callback is invoked,
+     *     the JavaScript "this" keyword refers to this scope object.
+     *     If no scope is provided, default is window.
+     * @param {Function} [onComplete]
+     *     Invoked to tell the client application whether the 
+     *     subscribe operation succeeded or failed. 
+     * @param {*} [subscriberData]
+     *     Client application provides this data, which is handed
+     *     back to the client application in the subscriberData
+     *     parameter of the onData and onComplete callback functions.
+     * 
+     * @returns subscriptionID
+     *     Identifier representing the subscription. This identifier is an 
+     *     arbitrary ID string that is unique within this Hub instance
+     * @type {String}
+     * 
+     * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance is not in CONNECTED state
+     * @throws {OpenAjax.hub.Error.BadParameters} if the topic is invalid (e.g. contains an empty token)
+     *
+     * In this implementation of InlineHubClient, the subscribe function operates 
+     * Thus, onComplete is invoked before this function returns. Developers are 
+     * cautioned that in most implementations of HubClient, onComplete is invoked 
+     * after this function returns.
+     * 
+     * If unsubscribe is called before subscribe completes, the subscription is 
+     * immediately terminated, and onComplete is never invoked.
+     */
+    this.subscribe = function( topic, onData, scope, onComplete, subscriberData ) {
+        return container.subscribe( topic, onData, scope, onComplete, subscriberData );
+    };
+    
+    /**
+     * Publish an event on 'topic' with the given data.
+     *
+     * @param {String} topic
+     *     A valid topic string. MUST NOT include wildcards.
+     * @param {*} data
+     *     Valid publishable data. To be portable across different
+     *     Container implementations, this value SHOULD be serializable
+     *     as JSON.
+     *     
+     * @throws {OpenAjax.hub.Error.Disconnected} if this Hub instance 
+     *     is not in CONNECTED state
+     * 
+     * In this implementation, publish operates SYNCHRONOUSLY. 
+     * Data will be delivered to subscribers after this function returns.
+     * In most implementations, publish operates synchronously, 
+     * delivering its data to the clients before this function returns.
+     */
+    this.publish = function( topic, data ) {
+        container.publish( topic, data );
+    };
+    
+    /**
+     * Unsubscribe from a subscription
+     *
+     * @param {String} subscriptionID
+     *     A subscriptionID returned by InlineHubClient.prototype.subscribe()
+     * @param {Function} [onComplete]
+     *     Callback function invoked when unsubscribe completes
+     * @param {Object} [scope]
+     *     When onComplete callback function is invoked, the JavaScript "this"
+     *     keyword refers to this scope object.
+     *     
+     * @throws {OpenAjax.hub.Error.NoSubscription} if no such subscription is found
+     * 
+     * To facilitate cleanup, it is possible to call unsubscribe even 
+     * when the HubClient is in a DISCONNECTED state.
+     * 
+     * In this implementation of HubClient, this function operates SYNCHRONOUSLY. 
+     * Thus, onComplete is invoked before this function returns. Developers are 
+     * cautioned that in most implementations of HubClient, onComplete is invoked 
+     * after this function returns.
+     */
+    this.unsubscribe = function( subscriptionID, onComplete, scope ) {
+        container.unsubscribe( subscriptionID, onComplete, scope );
+    };
+    
+    this.isConnected = function() {
+        return container.isConnected();
+    };
+    
+    this.getScope = function() {
+        return scope;
+    };
+    
+    this.getSubscriberData = function( subID ) {
+        return container.getSubscriberData( subID );
+    };
+    
+    this.getSubscriberScope = function( subID ) {
+        return container.getSubscriberScope( subID );
+    };
+    
+    /**
+     * Returns the params object associated with this Hub instance.
+     * Allows mix-in code to access parameters passed into constructor that created
+     * this Hub instance.
+     *
+     * @returns params  the params object associated with this Hub instance
+     * @type {Object}
+     */
+    this.getParameters = function() {
+        return params;
+    };
+};
\ No newline at end of file
diff --git a/trunk/extras/src/main/javascript/features-extras/pubsub-2/feature.xml b/trunk/extras/src/main/javascript/features-extras/pubsub-2/feature.xml
new file mode 100644
index 0000000..10feeda
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/pubsub-2/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>pubsub-2</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>org.openajax.hub-2.0.7</dependency>
+  <gadget>
+    <script src="pubsub-2.js"/>
+    <script src="taming.js" caja="1"/>
+  </gadget>
+  <container>
+    <script src="pubsub-2-router.js"/>
+  </container>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/pubsub-2/pubsub-2-router.js b/trunk/extras/src/main/javascript/features-extras/pubsub-2/pubsub-2-router.js
new file mode 100644
index 0000000..435cf52
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/pubsub-2/pubsub-2-router.js
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side message router for PubSub, a gadget-to-gadget
+ * communication library.
+ * 
+ * Uses OpenAjax Hub's ManagedHub class to route pubsub messages and to provide
+ * manager callbacks that allow control over these messages.
+ */
+
+/**
+ * @static
+ * @class Routes PubSub messages.
+ * @name gadgets.pubsub2router
+ */
+gadgets.pubsub2router = function() {
+  return /** @scope gadgets.pubsub2router */ {
+    /**
+     * Initialize the pubsub message router.
+     * 
+     * 'opt_params' is passed directly to the ManagedHub constructor.
+     * For example:
+     * 
+     *     gadgets.pubsub2router.init({
+     *         onSubscribe: function(topic, container) {
+     *           ...
+     *           return true; // return false to reject the request.
+     *         },
+     *         onPublish: function(topic, data, pcont, scont) {
+     *           ...
+     *           return true; // return false to reject the request.
+     *         },
+     *         onUnsubscribe: function(topic, container) {
+     *           ...
+     *         }
+     *     });
+     * 
+     * Alternatively, if you have already created a ManagedHub instance and wish
+     * to use that, you can specify it in 'opt_params.hub'.
+     * 
+     * @param {Object} opt_params
+     * @see http://openajax.org/member/wiki/OpenAjax_Hub_2.0_Specification_Managed_Hub_APIs#OpenAjax.hub.ManagedHub_constructor
+     */
+    init: function( opt_params ) {
+      if (opt_params.hub) {
+        this.hub = opt_params.hub;
+      } else {
+        this.hub = new OpenAjax.hub.ManagedHub({
+          onPublish: opt_params.onPublish,
+          onSubscribe: opt_params.onSubscribe,
+          onUnsubscribe: opt_params.onUnsubscribe
+        });
+      }
+    }
+  };
+}();
diff --git a/trunk/extras/src/main/javascript/features-extras/pubsub-2/pubsub-2.js b/trunk/extras/src/main/javascript/features-extras/pubsub-2/pubsub-2.js
new file mode 100644
index 0000000..87204e1
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/pubsub-2/pubsub-2.js
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Gadget-side PubSub library for gadget-to-gadget communication.
+ * 
+ * Uses OpenAjax Hub in order to do pubsub.  Simple case is to do the following:
+ *    
+ *    gadgets.util.registerOnLoadHandler(function() {
+ *      gadgets.Hub.subscribe(topic, callback);
+ *      // OR
+ *      gadgets.Hub.publish(topic2, message);
+ *    });
+ * 
+ * The gadgets.Hub object implements the OpenAjax.hub.HubClient interface.
+ * 
+ * By default, a HubClient is instantiated automatically by the pubsub-2 code.
+ * If the gadget wants to provide params to the HubClient constructor, it can
+ * do so by setting values on gadgets.HubSettings object:
+ * 
+ *     gadgets.HubSettings = {
+ *         // Parameters object for HubClient constructor.
+ *         // @see http://openajax.org/member/wiki/OpenAjax_Hub_2.0_Specification_Managed_Hub_APIs#OpenAjax.hub.HubClient_constructor
+ *         // @see http://openajax.org/member/wiki/OpenAjax_Hub_2.0_Specification_Managed_Hub_APIs#OpenAjax.hub.IframeHubClient_constructor
+ *         params: {},
+ *         // Callback that is invoked when connection to parent is established,
+ *         // or when errors occur.
+ *         // @see http://openajax.org/member/wiki/OpenAjax_Hub_2.0_Specification_Managed_Hub_APIs#OpenAjax.hub.HubClient.prototype.connect
+ *         onConnect: <function>
+ *     }
+ * 
+ * For example, to set a security alert callback:
+ * 
+ *     gadgets.HubSettings.params.HubClient.onSecurityCallback =
+ *             function(alertSource, alertType) { ... };
+ * 
+ * @see http://openajax.org/member/wiki/OpenAjax_Hub_2.0_Specification_Managed_Hub_APIs#OpenAjax.hub.HubClient
+ */
+
+(function() {
+    // Create a pubsub settings object
+    gadgets.HubSettings = {
+        // Set default HubClient constructor params object
+        params: {
+            HubClient: {
+                onSecurityAlert: function(alertSource, alertType) {
+                    alert( "Gadget stopped attempted security breach: " + alertType );
+                    // Forces container to see Frame Phish alert and probably close this gadget
+                    window.location.href = "about:blank"; 
+                }
+            },
+            IframeHubClient: {}
+        }
+    };
+    if (gadgets.util.getUrlParameters()['forcesecure']) {
+        gadgets.HubSettings.params.IframeHubClient.requireParentVerifiable = true;
+    }
+    
+    // Register an onLoad handler
+    gadgets.util.registerOnLoadHandler(function() {
+        try {
+            // Create the HubClient.
+            gadgets.Hub = new OpenAjax.hub.IframeHubClient(gadgets.HubSettings.params);
+            
+            // Connect to the ManagedHub
+            gadgets.Hub.connect(gadgets.HubSettings.onConnect); 
+        } catch(e) {
+            // TODO: error handling should be consistent with other OS gadget initialization error handling
+            gadgets.error("ERROR creating or connecting IframeHubClient in gadgets.Hub [" + e.message + "]");
+        }
+    });
+})();
diff --git a/trunk/extras/src/main/javascript/features-extras/pubsub-2/taming.js b/trunk/extras/src/main/javascript/features-extras/pubsub-2/taming.js
new file mode 100644
index 0000000..1fd2c15
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/pubsub-2/taming.js
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.pubsub.* API to cajoled gadgets
+ */
+// XXX not sure what to do here
+//tamings___.push(function(imports) {
+//  caja___.whitelistFuncs([
+//    [gadgets.pubsub, 'publish'],
+//    [gadgets.pubsub, 'subscribe'],
+//    [gadgets.pubsub, 'unsubscribe']
+//  ]);
+//});
diff --git a/trunk/extras/src/main/javascript/features-extras/swfobject/feature.xml b/trunk/extras/src/main/javascript/features-extras/swfobject/feature.xml
new file mode 100644
index 0000000..2b6cbd8
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/swfobject/feature.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <!-- Current implementation is swfobject 2.2 -->
+  <name>swfobject</name>
+  <gadget>
+    <script src="swfobject.js"/>
+    <api>
+      <exports type="js">swfobject.registerObject</exports>
+      <exports type="js">swfobject.getObjectById</exports>
+      <exports type="js">swfobject.embedSWF</exports>
+      <exports type="js">swfobject.switchOffAutoHideShow</exports>
+      <exports type="js">swfobject.ua</exports>
+      <exports type="js">swfobject.getFlashPlayerVersion</exports>
+      <exports type="js">swfobject.hasFlashPlayerVersion</exports>
+      <exports type="js">swfobject.createSWF</exports>
+      <exports type="js">swfobject.showExpressInstall</exports>
+      <exports type="js">swfobject.removeSWF</exports>
+      <exports type="js">swfobject.createCSS</exports>
+      <exports type="js">swfobject.addDomLoadEvent</exports>
+      <exports type="js">swfobject.addLoadEvent</exports>
+      <exports type="js">swfobject.getQueryParamValue</exports>
+      <exports type="js">swfobject.expressInstallCallback</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/swfobject/swfobject.js b/trunk/extras/src/main/javascript/features-extras/swfobject/swfobject.js
new file mode 100644
index 0000000..9378c8f
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/swfobject/swfobject.js
@@ -0,0 +1,777 @@
+/*!	SWFObject v2.2 <http://code.google.com/p/swfobject/> 
+	is released under the MIT License <http://www.opensource.org/licenses/mit-license.php> 
+*/
+
+var swfobject = function() {
+	
+	var UNDEF = "undefined",
+		OBJECT = "object",
+		SHOCKWAVE_FLASH = "Shockwave Flash",
+		SHOCKWAVE_FLASH_AX = "ShockwaveFlash.ShockwaveFlash",
+		FLASH_MIME_TYPE = "application/x-shockwave-flash",
+		EXPRESS_INSTALL_ID = "SWFObjectExprInst",
+		ON_READY_STATE_CHANGE = "onreadystatechange",
+		
+		win = window,
+		doc = document,
+		nav = navigator,
+		
+		plugin = false,
+		domLoadFnArr = [main],
+		regObjArr = [],
+		objIdArr = [],
+		listenersArr = [],
+		storedAltContent,
+		storedAltContentId,
+		storedCallbackFn,
+		storedCallbackObj,
+		isDomLoaded = false,
+		isExpressInstallActive = false,
+		dynamicStylesheet,
+		dynamicStylesheetMedia,
+		autoHideShow = true,
+	
+	/* Centralized function for browser feature detection
+		- User agent string detection is only used when no good alternative is possible
+		- Is executed directly for optimal performance
+	*/	
+	ua = function() {
+		var w3cdom = typeof doc.getElementById != UNDEF && typeof doc.getElementsByTagName != UNDEF && typeof doc.createElement != UNDEF,
+			u = nav.userAgent.toLowerCase(),
+			p = nav.platform.toLowerCase(),
+			windows = p ? /win/.test(p) : /win/.test(u),
+			mac = p ? /mac/.test(p) : /mac/.test(u),
+			webkit = /webkit/.test(u) ? parseFloat(u.replace(/^.*webkit\/(\d+(\.\d+)?).*$/, "$1")) : false, // returns either the webkit version or false if not webkit
+			ie = !+"\v1", // feature detection based on Andrea Giammarchi's solution: http://webreflection.blogspot.com/2009/01/32-bytes-to-know-if-your-browser-is-ie.html
+			playerVersion = [0,0,0],
+			d = null;
+		if (typeof nav.plugins != UNDEF && typeof nav.plugins[SHOCKWAVE_FLASH] == OBJECT) {
+			d = nav.plugins[SHOCKWAVE_FLASH].description;
+			if (d && !(typeof nav.mimeTypes != UNDEF && nav.mimeTypes[FLASH_MIME_TYPE] && !nav.mimeTypes[FLASH_MIME_TYPE].enabledPlugin)) { // navigator.mimeTypes["application/x-shockwave-flash"].enabledPlugin indicates whether plug-ins are enabled or disabled in Safari 3+
+				plugin = true;
+				ie = false; // cascaded feature detection for Internet Explorer
+				d = d.replace(/^.*\s+(\S+\s+\S+$)/, "$1");
+				playerVersion[0] = parseInt(d.replace(/^(.*)\..*$/, "$1"), 10);
+				playerVersion[1] = parseInt(d.replace(/^.*\.(.*)\s.*$/, "$1"), 10);
+				playerVersion[2] = /[a-zA-Z]/.test(d) ? parseInt(d.replace(/^.*[a-zA-Z]+(.*)$/, "$1"), 10) : 0;
+			}
+		}
+		else if (typeof win.ActiveXObject != UNDEF) {
+			try {
+				var a = new ActiveXObject(SHOCKWAVE_FLASH_AX);
+				if (a) { // a will return null when ActiveX is disabled
+					d = a.GetVariable("$version");
+					if (d) {
+						ie = true; // cascaded feature detection for Internet Explorer
+						d = d.split(" ")[1].split(",");
+						playerVersion = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)];
+					}
+				}
+			}
+			catch(e) {}
+		}
+		return { w3:w3cdom, pv:playerVersion, wk:webkit, ie:ie, win:windows, mac:mac };
+	}(),
+	
+	/* Cross-browser onDomLoad
+		- Will fire an event as soon as the DOM of a web page is loaded
+		- Internet Explorer workaround based on Diego Perini's solution: http://javascript.nwbox.com/IEContentLoaded/
+		- Regular onload serves as fallback
+	*/ 
+	onDomLoad = function() {
+		if (!ua.w3) { return; }
+		if ((typeof doc.readyState != UNDEF && doc.readyState == "complete") || (typeof doc.readyState == UNDEF && (doc.getElementsByTagName("body")[0] || doc.body))) { // function is fired after onload, e.g. when script is inserted dynamically 
+			callDomLoadFunctions();
+		}
+		if (!isDomLoaded) {
+			if (typeof doc.addEventListener != UNDEF) {
+				doc.addEventListener("DOMContentLoaded", callDomLoadFunctions, false);
+			}		
+			if (ua.ie && ua.win) {
+				doc.attachEvent(ON_READY_STATE_CHANGE, function() {
+					if (doc.readyState == "complete") {
+						doc.detachEvent(ON_READY_STATE_CHANGE, arguments.callee);
+						callDomLoadFunctions();
+					}
+				});
+				if (win == top) { // if not inside an iframe
+					(function(){
+						if (isDomLoaded) { return; }
+						try {
+							doc.documentElement.doScroll("left");
+						}
+						catch(e) {
+							setTimeout(arguments.callee, 0);
+							return;
+						}
+						callDomLoadFunctions();
+					})();
+				}
+			}
+			if (ua.wk) {
+				(function(){
+					if (isDomLoaded) { return; }
+					if (!/loaded|complete/.test(doc.readyState)) {
+						setTimeout(arguments.callee, 0);
+						return;
+					}
+					callDomLoadFunctions();
+				})();
+			}
+			addLoadEvent(callDomLoadFunctions);
+		}
+	}();
+	
+	function callDomLoadFunctions() {
+		if (isDomLoaded) { return; }
+		try { // test if we can really add/remove elements to/from the DOM; we don't want to fire it too early
+			var t = doc.getElementsByTagName("body")[0].appendChild(createElement("span"));
+			t.parentNode.removeChild(t);
+		}
+		catch (e) { return; }
+		isDomLoaded = true;
+		var dl = domLoadFnArr.length;
+		for (var i = 0; i < dl; i++) {
+			domLoadFnArr[i]();
+		}
+	}
+	
+	function addDomLoadEvent(fn) {
+		if (isDomLoaded) {
+			fn();
+		}
+		else { 
+			domLoadFnArr[domLoadFnArr.length] = fn; // Array.push() is only available in IE5.5+
+		}
+	}
+	
+	/* Cross-browser onload
+		- Based on James Edwards' solution: http://brothercake.com/site/resources/scripts/onload/
+		- Will fire an event as soon as a web page including all of its assets are loaded 
+	 */
+	function addLoadEvent(fn) {
+		if (typeof win.addEventListener != UNDEF) {
+			win.addEventListener("load", fn, false);
+		}
+		else if (typeof doc.addEventListener != UNDEF) {
+			doc.addEventListener("load", fn, false);
+		}
+		else if (typeof win.attachEvent != UNDEF) {
+			addListener(win, "onload", fn);
+		}
+		else if (typeof win.onload == "function") {
+			var fnOld = win.onload;
+			win.onload = function() {
+				fnOld();
+				fn();
+			};
+		}
+		else {
+			win.onload = fn;
+		}
+	}
+	
+	/* Main function
+		- Will preferably execute onDomLoad, otherwise onload (as a fallback)
+	*/
+	function main() { 
+		if (plugin) {
+			testPlayerVersion();
+		}
+		else {
+			matchVersions();
+		}
+	}
+	
+	/* Detect the Flash Player version for non-Internet Explorer browsers
+		- Detecting the plug-in version via the object element is more precise than using the plugins collection item's description:
+		  a. Both release and build numbers can be detected
+		  b. Avoid wrong descriptions by corrupt installers provided by Adobe
+		  c. Avoid wrong descriptions by multiple Flash Player entries in the plugin Array, caused by incorrect browser imports
+		- Disadvantage of this method is that it depends on the availability of the DOM, while the plugins collection is immediately available
+	*/
+	function testPlayerVersion() {
+		var b = doc.getElementsByTagName("body")[0];
+		var o = createElement(OBJECT);
+		o.setAttribute("type", FLASH_MIME_TYPE);
+		var t = b.appendChild(o);
+		if (t) {
+			var counter = 0;
+			(function(){
+				if (typeof t.GetVariable != UNDEF) {
+					var d = t.GetVariable("$version");
+					if (d) {
+						d = d.split(" ")[1].split(",");
+						ua.pv = [parseInt(d[0], 10), parseInt(d[1], 10), parseInt(d[2], 10)];
+					}
+				}
+				else if (counter < 10) {
+					counter++;
+					setTimeout(arguments.callee, 10);
+					return;
+				}
+				b.removeChild(o);
+				t = null;
+				matchVersions();
+			})();
+		}
+		else {
+			matchVersions();
+		}
+	}
+	
+	/* Perform Flash Player and SWF version matching; static publishing only
+	*/
+	function matchVersions() {
+		var rl = regObjArr.length;
+		if (rl > 0) {
+			for (var i = 0; i < rl; i++) { // for each registered object element
+				var id = regObjArr[i].id;
+				var cb = regObjArr[i].callbackFn;
+				var cbObj = {success:false, id:id};
+				if (ua.pv[0] > 0) {
+					var obj = getElementById(id);
+					if (obj) {
+						if (hasPlayerVersion(regObjArr[i].swfVersion) && !(ua.wk && ua.wk < 312)) { // Flash Player version >= published SWF version: Houston, we have a match!
+							setVisibility(id, true);
+							if (cb) {
+								cbObj.success = true;
+								cbObj.ref = getObjectById(id);
+								cb(cbObj);
+							}
+						}
+						else if (regObjArr[i].expressInstall && canExpressInstall()) { // show the Adobe Express Install dialog if set by the web page author and if supported
+							var att = {};
+							att.data = regObjArr[i].expressInstall;
+							att.width = obj.getAttribute("width") || "0";
+							att.height = obj.getAttribute("height") || "0";
+							if (obj.getAttribute("class")) { att.styleclass = obj.getAttribute("class"); }
+							if (obj.getAttribute("align")) { att.align = obj.getAttribute("align"); }
+							// parse HTML object param element's name-value pairs
+							var par = {};
+							var p = obj.getElementsByTagName("param");
+							var pl = p.length;
+							for (var j = 0; j < pl; j++) {
+								if (p[j].getAttribute("name").toLowerCase() != "movie") {
+									par[p[j].getAttribute("name")] = p[j].getAttribute("value");
+								}
+							}
+							showExpressInstall(att, par, id, cb);
+						}
+						else { // Flash Player and SWF version mismatch or an older Webkit engine that ignores the HTML object element's nested param elements: display alternative content instead of SWF
+							displayAltContent(obj);
+							if (cb) { cb(cbObj); }
+						}
+					}
+				}
+				else {	// if no Flash Player is installed or the fp version cannot be detected we let the HTML object element do its job (either show a SWF or alternative content)
+					setVisibility(id, true);
+					if (cb) {
+						var o = getObjectById(id); // test whether there is an HTML object element or not
+						if (o && typeof o.SetVariable != UNDEF) { 
+							cbObj.success = true;
+							cbObj.ref = o;
+						}
+						cb(cbObj);
+					}
+				}
+			}
+		}
+	}
+	
+	function getObjectById(objectIdStr) {
+		var r = null;
+		var o = getElementById(objectIdStr);
+		if (o && o.nodeName == "OBJECT") {
+			if (typeof o.SetVariable != UNDEF) {
+				r = o;
+			}
+			else {
+				var n = o.getElementsByTagName(OBJECT)[0];
+				if (n) {
+					r = n;
+				}
+			}
+		}
+		return r;
+	}
+	
+	/* Requirements for Adobe Express Install
+		- only one instance can be active at a time
+		- fp 6.0.65 or higher
+		- Win/Mac OS only
+		- no Webkit engines older than version 312
+	*/
+	function canExpressInstall() {
+		return !isExpressInstallActive && hasPlayerVersion("6.0.65") && (ua.win || ua.mac) && !(ua.wk && ua.wk < 312);
+	}
+	
+	/* Show the Adobe Express Install dialog
+		- Reference: http://www.adobe.com/cfusion/knowledgebase/index.cfm?id=6a253b75
+	*/
+	function showExpressInstall(att, par, replaceElemIdStr, callbackFn) {
+		isExpressInstallActive = true;
+		storedCallbackFn = callbackFn || null;
+		storedCallbackObj = {success:false, id:replaceElemIdStr};
+		var obj = getElementById(replaceElemIdStr);
+		if (obj) {
+			if (obj.nodeName == "OBJECT") { // static publishing
+				storedAltContent = abstractAltContent(obj);
+				storedAltContentId = null;
+			}
+			else { // dynamic publishing
+				storedAltContent = obj;
+				storedAltContentId = replaceElemIdStr;
+			}
+			att.id = EXPRESS_INSTALL_ID;
+			if (typeof att.width == UNDEF || (!/%$/.test(att.width) && parseInt(att.width, 10) < 310)) { att.width = "310"; }
+			if (typeof att.height == UNDEF || (!/%$/.test(att.height) && parseInt(att.height, 10) < 137)) { att.height = "137"; }
+			doc.title = doc.title.slice(0, 47) + " - Flash Player Installation";
+			var pt = ua.ie && ua.win ? "ActiveX" : "PlugIn",
+				fv = "MMredirectURL=" + win.location.toString().replace(/&/g,"%26") + "&MMplayerType=" + pt + "&MMdoctitle=" + doc.title;
+			if (typeof par.flashvars != UNDEF) {
+				par.flashvars += "&" + fv;
+			}
+			else {
+				par.flashvars = fv;
+			}
+			// IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it,
+			// because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work
+			if (ua.ie && ua.win && obj.readyState != 4) {
+				var newObj = createElement("div");
+				replaceElemIdStr += "SWFObjectNew";
+				newObj.setAttribute("id", replaceElemIdStr);
+				obj.parentNode.insertBefore(newObj, obj); // insert placeholder div that will be replaced by the object element that loads expressinstall.swf
+				obj.style.display = "none";
+				(function(){
+					if (obj.readyState == 4) {
+						obj.parentNode.removeChild(obj);
+					}
+					else {
+						setTimeout(arguments.callee, 10);
+					}
+				})();
+			}
+			createSWF(att, par, replaceElemIdStr);
+		}
+	}
+	
+	/* Functions to abstract and display alternative content
+	*/
+	function displayAltContent(obj) {
+		if (ua.ie && ua.win && obj.readyState != 4) {
+			// IE only: when a SWF is loading (AND: not available in cache) wait for the readyState of the object element to become 4 before removing it,
+			// because you cannot properly cancel a loading SWF file without breaking browser load references, also obj.onreadystatechange doesn't work
+			var el = createElement("div");
+			obj.parentNode.insertBefore(el, obj); // insert placeholder div that will be replaced by the alternative content
+			el.parentNode.replaceChild(abstractAltContent(obj), el);
+			obj.style.display = "none";
+			(function(){
+				if (obj.readyState == 4) {
+					obj.parentNode.removeChild(obj);
+				}
+				else {
+					setTimeout(arguments.callee, 10);
+				}
+			})();
+		}
+		else {
+			obj.parentNode.replaceChild(abstractAltContent(obj), obj);
+		}
+	} 
+
+	function abstractAltContent(obj) {
+		var ac = createElement("div");
+		if (ua.win && ua.ie) {
+			ac.innerHTML = obj.innerHTML;
+		}
+		else {
+			var nestedObj = obj.getElementsByTagName(OBJECT)[0];
+			if (nestedObj) {
+				var c = nestedObj.childNodes;
+				if (c) {
+					var cl = c.length;
+					for (var i = 0; i < cl; i++) {
+						if (!(c[i].nodeType == 1 && c[i].nodeName == "PARAM") && !(c[i].nodeType == 8)) {
+							ac.appendChild(c[i].cloneNode(true));
+						}
+					}
+				}
+			}
+		}
+		return ac;
+	}
+	
+	/* Cross-browser dynamic SWF creation
+	*/
+	function createSWF(attObj, parObj, id) {
+		var r, el = getElementById(id);
+		if (ua.wk && ua.wk < 312) { return r; }
+		if (el) {
+			if (typeof attObj.id == UNDEF) { // if no 'id' is defined for the object element, it will inherit the 'id' from the alternative content
+				attObj.id = id;
+			}
+			if (ua.ie && ua.win) { // Internet Explorer + the HTML object element + W3C DOM methods do not combine: fall back to outerHTML
+				var att = "";
+				for (var i in attObj) {
+					if (attObj[i] != Object.prototype[i]) { // filter out prototype additions from other potential libraries
+						if (i.toLowerCase() == "data") {
+							parObj.movie = attObj[i];
+						}
+						else if (i.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword
+							att += ' class="' + attObj[i] + '"';
+						}
+						else if (i.toLowerCase() != "classid") {
+							att += ' ' + i + '="' + attObj[i] + '"';
+						}
+					}
+				}
+				var par = "";
+				for (var j in parObj) {
+					if (parObj[j] != Object.prototype[j]) { // filter out prototype additions from other potential libraries
+						par += '<param name="' + j + '" value="' + parObj[j] + '" />';
+					}
+				}
+				el.outerHTML = '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"' + att + '>' + par + '</object>';
+				objIdArr[objIdArr.length] = attObj.id; // stored to fix object 'leaks' on unload (dynamic publishing only)
+				r = getElementById(attObj.id);	
+			}
+			else { // well-behaving browsers
+				var o = createElement(OBJECT);
+				o.setAttribute("type", FLASH_MIME_TYPE);
+				for (var m in attObj) {
+					if (attObj[m] != Object.prototype[m]) { // filter out prototype additions from other potential libraries
+						if (m.toLowerCase() == "styleclass") { // 'class' is an ECMA4 reserved keyword
+							o.setAttribute("class", attObj[m]);
+						}
+						else if (m.toLowerCase() != "classid") { // filter out IE specific attribute
+							o.setAttribute(m, attObj[m]);
+						}
+					}
+				}
+				for (var n in parObj) {
+					if (parObj[n] != Object.prototype[n] && n.toLowerCase() != "movie") { // filter out prototype additions from other potential libraries and IE specific param element
+						createObjParam(o, n, parObj[n]);
+					}
+				}
+				el.parentNode.replaceChild(o, el);
+				r = o;
+			}
+		}
+		return r;
+	}
+	
+	function createObjParam(el, pName, pValue) {
+		var p = createElement("param");
+		p.setAttribute("name", pName);	
+		p.setAttribute("value", pValue);
+		el.appendChild(p);
+	}
+	
+	/* Cross-browser SWF removal
+		- Especially needed to safely and completely remove a SWF in Internet Explorer
+	*/
+	function removeSWF(id) {
+		var obj = getElementById(id);
+		if (obj && obj.nodeName == "OBJECT") {
+			if (ua.ie && ua.win) {
+				obj.style.display = "none";
+				(function(){
+					if (obj.readyState == 4) {
+						removeObjectInIE(id);
+					}
+					else {
+						setTimeout(arguments.callee, 10);
+					}
+				})();
+			}
+			else {
+				obj.parentNode.removeChild(obj);
+			}
+		}
+	}
+	
+	function removeObjectInIE(id) {
+		var obj = getElementById(id);
+		if (obj) {
+			for (var i in obj) {
+				if (typeof obj[i] == "function") {
+					obj[i] = null;
+				}
+			}
+			obj.parentNode.removeChild(obj);
+		}
+	}
+	
+	/* Functions to optimize JavaScript compression
+	*/
+	function getElementById(id) {
+		var el = null;
+		try {
+			el = doc.getElementById(id);
+		}
+		catch (e) {}
+		return el;
+	}
+	
+	function createElement(el) {
+		return doc.createElement(el);
+	}
+	
+	/* Updated attachEvent function for Internet Explorer
+		- Stores attachEvent information in an Array, so on unload the detachEvent functions can be called to avoid memory leaks
+	*/	
+	function addListener(target, eventType, fn) {
+		target.attachEvent(eventType, fn);
+		listenersArr[listenersArr.length] = [target, eventType, fn];
+	}
+	
+	/* Flash Player and SWF content version matching
+	*/
+	function hasPlayerVersion(rv) {
+		var pv = ua.pv, v = rv.split(".");
+		v[0] = parseInt(v[0], 10);
+		v[1] = parseInt(v[1], 10) || 0; // supports short notation, e.g. "9" instead of "9.0.0"
+		v[2] = parseInt(v[2], 10) || 0;
+		return (pv[0] > v[0] || (pv[0] == v[0] && pv[1] > v[1]) || (pv[0] == v[0] && pv[1] == v[1] && pv[2] >= v[2])) ? true : false;
+	}
+	
+	/* Cross-browser dynamic CSS creation
+		- Based on Bobby van der Sluis' solution: http://www.bobbyvandersluis.com/articles/dynamicCSS.php
+	*/	
+	function createCSS(sel, decl, media, newStyle) {
+		if (ua.ie && ua.mac) { return; }
+		var h = doc.getElementsByTagName("head")[0];
+		if (!h) { return; } // to also support badly authored HTML pages that lack a head element
+		var m = (media && typeof media == "string") ? media : "screen";
+		if (newStyle) {
+			dynamicStylesheet = null;
+			dynamicStylesheetMedia = null;
+		}
+		if (!dynamicStylesheet || dynamicStylesheetMedia != m) { 
+			// create dynamic stylesheet + get a global reference to it
+			var s = createElement("style");
+			s.setAttribute("type", "text/css");
+			s.setAttribute("media", m);
+			dynamicStylesheet = h.appendChild(s);
+			if (ua.ie && ua.win && typeof doc.styleSheets != UNDEF && doc.styleSheets.length > 0) {
+				dynamicStylesheet = doc.styleSheets[doc.styleSheets.length - 1];
+			}
+			dynamicStylesheetMedia = m;
+		}
+		// add style rule
+		if (ua.ie && ua.win) {
+			if (dynamicStylesheet && typeof dynamicStylesheet.addRule == OBJECT) {
+				dynamicStylesheet.addRule(sel, decl);
+			}
+		}
+		else {
+			if (dynamicStylesheet && typeof doc.createTextNode != UNDEF) {
+				dynamicStylesheet.appendChild(doc.createTextNode(sel + " {" + decl + "}"));
+			}
+		}
+	}
+	
+	function setVisibility(id, isVisible) {
+		if (!autoHideShow) { return; }
+		var v = isVisible ? "visible" : "hidden";
+		if (isDomLoaded && getElementById(id)) {
+			getElementById(id).style.visibility = v;
+		}
+		else {
+			createCSS("#" + id, "visibility:" + v);
+		}
+	}
+
+	/* Filter to avoid XSS attacks
+	*/
+	function urlEncodeIfNecessary(s) {
+		var regex = /[\\\"<>\.;]/;
+		var hasBadChars = regex.exec(s) != null;
+		return hasBadChars && typeof encodeURIComponent != UNDEF ? encodeURIComponent(s) : s;
+	}
+	
+	/* Release memory to avoid memory leaks caused by closures, fix hanging audio/video threads and force open sockets/NetConnections to disconnect (Internet Explorer only)
+	*/
+	var cleanup = function() {
+		if (ua.ie && ua.win) {
+			window.attachEvent("onunload", function() {
+				// remove listeners to avoid memory leaks
+				var ll = listenersArr.length;
+				for (var i = 0; i < ll; i++) {
+					listenersArr[i][0].detachEvent(listenersArr[i][1], listenersArr[i][2]);
+				}
+				// cleanup dynamically embedded objects to fix audio/video threads and force open sockets and NetConnections to disconnect
+				var il = objIdArr.length;
+				for (var j = 0; j < il; j++) {
+					removeSWF(objIdArr[j]);
+				}
+				// cleanup library's main closures to avoid memory leaks
+				for (var k in ua) {
+					ua[k] = null;
+				}
+				ua = null;
+				for (var l in swfobject) {
+					swfobject[l] = null;
+				}
+				swfobject = null;
+			});
+		}
+	}();
+	
+	return {
+		/* Public API
+			- Reference: http://code.google.com/p/swfobject/wiki/documentation
+		*/ 
+		registerObject: function(objectIdStr, swfVersionStr, xiSwfUrlStr, callbackFn) {
+			if (ua.w3 && objectIdStr && swfVersionStr) {
+				var regObj = {};
+				regObj.id = objectIdStr;
+				regObj.swfVersion = swfVersionStr;
+				regObj.expressInstall = xiSwfUrlStr;
+				regObj.callbackFn = callbackFn;
+				regObjArr[regObjArr.length] = regObj;
+				setVisibility(objectIdStr, false);
+			}
+			else if (callbackFn) {
+				callbackFn({success:false, id:objectIdStr});
+			}
+		},
+		
+		getObjectById: function(objectIdStr) {
+			if (ua.w3) {
+				return getObjectById(objectIdStr);
+			}
+		},
+		
+		embedSWF: function(swfUrlStr, replaceElemIdStr, widthStr, heightStr, swfVersionStr, xiSwfUrlStr, flashvarsObj, parObj, attObj, callbackFn) {
+			var callbackObj = {success:false, id:replaceElemIdStr};
+			if (ua.w3 && !(ua.wk && ua.wk < 312) && swfUrlStr && replaceElemIdStr && widthStr && heightStr && swfVersionStr) {
+				setVisibility(replaceElemIdStr, false);
+				addDomLoadEvent(function() {
+					widthStr += ""; // auto-convert to string
+					heightStr += "";
+					var att = {};
+					if (attObj && typeof attObj === OBJECT) {
+						for (var i in attObj) { // copy object to avoid the use of references, because web authors often reuse attObj for multiple SWFs
+							att[i] = attObj[i];
+						}
+					}
+					att.data = swfUrlStr;
+					att.width = widthStr;
+					att.height = heightStr;
+					var par = {}; 
+					if (parObj && typeof parObj === OBJECT) {
+						for (var j in parObj) { // copy object to avoid the use of references, because web authors often reuse parObj for multiple SWFs
+							par[j] = parObj[j];
+						}
+					}
+					if (flashvarsObj && typeof flashvarsObj === OBJECT) {
+						for (var k in flashvarsObj) { // copy object to avoid the use of references, because web authors often reuse flashvarsObj for multiple SWFs
+							if (typeof par.flashvars != UNDEF) {
+								par.flashvars += "&" + k + "=" + flashvarsObj[k];
+							}
+							else {
+								par.flashvars = k + "=" + flashvarsObj[k];
+							}
+						}
+					}
+					if (hasPlayerVersion(swfVersionStr)) { // create SWF
+						var obj = createSWF(att, par, replaceElemIdStr);
+						if (att.id == replaceElemIdStr) {
+							setVisibility(replaceElemIdStr, true);
+						}
+						callbackObj.success = true;
+						callbackObj.ref = obj;
+					}
+					else if (xiSwfUrlStr && canExpressInstall()) { // show Adobe Express Install
+						att.data = xiSwfUrlStr;
+						showExpressInstall(att, par, replaceElemIdStr, callbackFn);
+						return;
+					}
+					else { // show alternative content
+						setVisibility(replaceElemIdStr, true);
+					}
+					if (callbackFn) { callbackFn(callbackObj); }
+				});
+			}
+			else if (callbackFn) { callbackFn(callbackObj);	}
+		},
+		
+		switchOffAutoHideShow: function() {
+			autoHideShow = false;
+		},
+		
+		ua: ua,
+		
+		getFlashPlayerVersion: function() {
+			return { major:ua.pv[0], minor:ua.pv[1], release:ua.pv[2] };
+		},
+		
+		hasFlashPlayerVersion: hasPlayerVersion,
+		
+		createSWF: function(attObj, parObj, replaceElemIdStr) {
+			if (ua.w3) {
+				return createSWF(attObj, parObj, replaceElemIdStr);
+			}
+			else {
+				return undefined;
+			}
+		},
+		
+		showExpressInstall: function(att, par, replaceElemIdStr, callbackFn) {
+			if (ua.w3 && canExpressInstall()) {
+				showExpressInstall(att, par, replaceElemIdStr, callbackFn);
+			}
+		},
+		
+		removeSWF: function(objElemIdStr) {
+			if (ua.w3) {
+				removeSWF(objElemIdStr);
+			}
+		},
+		
+		createCSS: function(selStr, declStr, mediaStr, newStyleBoolean) {
+			if (ua.w3) {
+				createCSS(selStr, declStr, mediaStr, newStyleBoolean);
+			}
+		},
+		
+		addDomLoadEvent: addDomLoadEvent,
+		
+		addLoadEvent: addLoadEvent,
+		
+		getQueryParamValue: function(param) {
+			var q = doc.location.search || doc.location.hash;
+			if (q) {
+				if (/\?/.test(q)) { q = q.split("?")[1]; } // strip question mark
+				if (param == null) {
+					return urlEncodeIfNecessary(q);
+				}
+				var pairs = q.split("&");
+				for (var i = 0; i < pairs.length; i++) {
+					if (pairs[i].substring(0, pairs[i].indexOf("=")) == param) {
+						return urlEncodeIfNecessary(pairs[i].substring((pairs[i].indexOf("=") + 1)));
+					}
+				}
+			}
+			return "";
+		},
+		
+		// For internal usage only
+		expressInstallCallback: function() {
+			if (isExpressInstallActive) {
+				var obj = getElementById(EXPRESS_INSTALL_ID);
+				if (obj && storedAltContent) {
+					obj.parentNode.replaceChild(storedAltContent, obj);
+					if (storedAltContentId) {
+						setVisibility(storedAltContentId, true);
+						if (ua.ie && ua.win) { storedAltContent.style.display = "block"; }
+					}
+					if (storedCallbackFn) { storedCallbackFn(storedCallbackObj); }
+				}
+				isExpressInstallActive = false;
+			} 
+		}
+	};
+}();
diff --git a/trunk/extras/src/main/javascript/features-extras/swfobject/swfobject.opt.js b/trunk/extras/src/main/javascript/features-extras/swfobject/swfobject.opt.js
new file mode 100644
index 0000000..8eafe9d
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/swfobject/swfobject.opt.js
@@ -0,0 +1,4 @@
+/*	SWFObject v2.2 <http://code.google.com/p/swfobject/> 
+	is released under the MIT License <http://www.opensource.org/licenses/mit-license.php> 
+*/
+var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y<X;Y++){U[Y]()}}function K(X){if(J){X()}else{U[U.length]=X}}function s(Y){if(typeof O.addEventListener!=D){O.addEventListener("load",Y,false)}else{if(typeof j.addEventListener!=D){j.addEventListener("load",Y,false)}else{if(typeof O.attachEvent!=D){i(O,"onload",Y)}else{if(typeof O.onload=="function"){var X=O.onload;O.onload=function(){X();Y()}}else{O.onload=Y}}}}}function h(){if(T){V()}else{H()}}function V(){var X=j.getElementsByTagName("body")[0];var aa=C(r);aa.setAttribute("type",q);var Z=X.appendChild(aa);if(Z){var Y=0;(function(){if(typeof Z.GetVariable!=D){var ab=Z.GetVariable("$version");if(ab){ab=ab.split(" ")[1].split(",");M.pv=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}else{if(Y<10){Y++;setTimeout(arguments.callee,10);return}}X.removeChild(aa);Z=null;H()})()}else{H()}}function H(){var ag=o.length;if(ag>0){for(var af=0;af<ag;af++){var Y=o[af].id;var ab=o[af].callbackFn;var aa={success:false,id:Y};if(M.pv[0]>0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad<ac;ad++){if(X[ad].getAttribute("name").toLowerCase()!="movie"){ah[X[ad].getAttribute("name")]=X[ad].getAttribute("value")}}P(ai,ah,Y,ab)}else{p(ae);if(ab){ab(aa)}}}}}else{w(Y,true);if(ab){var Z=z(Y);if(Z&&typeof Z.SetVariable!=D){aa.success=true;aa.ref=Z}ab(aa)}}}}}function z(aa){var X=null;var Y=c(aa);if(Y&&Y.nodeName=="OBJECT"){if(typeof Y.SetVariable!=D){X=Y}else{var Z=Y.getElementsByTagName(r)[0];if(Z){X=Z}}}return X}function A(){return !a&&F("6.0.65")&&(M.win||M.mac)&&!(M.wk&&M.wk<312)}function P(aa,ab,X,Z){a=true;E=Z||null;B={success:false,id:X};var ae=c(X);if(ae){if(ae.nodeName=="OBJECT"){l=g(ae);Q=null}else{l=ae;Q=X}aa.id=R;if(typeof aa.width==D||(!/%$/.test(aa.width)&&parseInt(aa.width,10)<310)){aa.width="310"}if(typeof aa.height==D||(!/%$/.test(aa.height)&&parseInt(aa.height,10)<137)){aa.height="137"}j.title=j.title.slice(0,47)+" - Flash Player Installation";var ad=M.ie&&M.win?"ActiveX":"PlugIn",ac="MMredirectURL="+O.location.toString().replace(/&/g,"%26")+"&MMplayerType="+ad+"&MMdoctitle="+j.title;if(typeof ab.flashvars!=D){ab.flashvars+="&"+ac}else{ab.flashvars=ac}if(M.ie&&M.win&&ae.readyState!=4){var Y=C("div");X+="SWFObjectNew";Y.setAttribute("id",X);ae.parentNode.insertBefore(Y,ae);ae.style.display="none";(function(){if(ae.readyState==4){ae.parentNode.removeChild(ae)}else{setTimeout(arguments.callee,10)}})()}u(aa,ab,X)}}function p(Y){if(M.ie&&M.win&&Y.readyState!=4){var X=C("div");Y.parentNode.insertBefore(X,Y);X.parentNode.replaceChild(g(Y),X);Y.style.display="none";(function(){if(Y.readyState==4){Y.parentNode.removeChild(Y)}else{setTimeout(arguments.callee,10)}})()}else{Y.parentNode.replaceChild(g(Y),Y)}}function g(ab){var aa=C("div");if(M.win&&M.ie){aa.innerHTML=ab.innerHTML}else{var Y=ab.getElementsByTagName(r)[0];if(Y){var ad=Y.childNodes;if(ad){var X=ad.length;for(var Z=0;Z<X;Z++){if(!(ad[Z].nodeType==1&&ad[Z].nodeName=="PARAM")&&!(ad[Z].nodeType==8)){aa.appendChild(ad[Z].cloneNode(true))}}}}}return aa}function u(ai,ag,Y){var X,aa=c(Y);if(M.wk&&M.wk<312){return X}if(aa){if(typeof ai.id==D){ai.id=Y}if(M.ie&&M.win){var ah="";for(var ae in ai){if(ai[ae]!=Object.prototype[ae]){if(ae.toLowerCase()=="data"){ag.movie=ai[ae]}else{if(ae.toLowerCase()=="styleclass"){ah+=' class="'+ai[ae]+'"'}else{if(ae.toLowerCase()!="classid"){ah+=" "+ae+'="'+ai[ae]+'"'}}}}}var af="";for(var ad in ag){if(ag[ad]!=Object.prototype[ad]){af+='<param name="'+ad+'" value="'+ag[ad]+'" />'}}aa.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+ah+">"+af+"</object>";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab<ac;ab++){I[ab][0].detachEvent(I[ab][1],I[ab][2])}var Z=N.length;for(var aa=0;aa<Z;aa++){y(N[aa])}for(var Y in M){M[Y]=null}M=null;for(var X in swfobject){swfobject[X]=null}swfobject=null})}}();return{registerObject:function(ab,X,aa,Z){if(M.w3&&ab&&X){var Y={};Y.id=ab;Y.swfVersion=X;Y.expressInstall=aa;Y.callbackFn=Z;o[o.length]=Y;w(ab,false)}else{if(Z){Z({success:false,id:ab})}}},getObjectById:function(X){if(M.w3){return z(X)}},embedSWF:function(ab,ah,ae,ag,Y,aa,Z,ad,af,ac){var X={success:false,id:ah};if(M.w3&&!(M.wk&&M.wk<312)&&ab&&ah&&ae&&ag&&Y){w(ah,false);K(function(){ae+="";ag+="";var aj={};if(af&&typeof af===r){for(var al in af){aj[al]=af[al]}}aj.data=ab;aj.width=ae;aj.height=ag;var am={};if(ad&&typeof ad===r){for(var ak in ad){am[ak]=ad[ak]}}if(Z&&typeof Z===r){for(var ai in Z){if(typeof am.flashvars!=D){am.flashvars+="&"+ai+"="+Z[ai]}else{am.flashvars=ai+"="+Z[ai]}}}if(F(Y)){var an=u(aj,am,ah);if(aj.id==ah){w(ah,true)}X.success=true;X.ref=an}else{if(aa&&A()){aj.data=aa;P(aj,am,ah,ac);return}else{w(ah,true)}}if(ac){ac(X)}})}else{if(ac){ac(X)}}},switchOffAutoHideShow:function(){m=false},ua:M,getFlashPlayerVersion:function(){return{major:M.pv[0],minor:M.pv[1],release:M.pv[2]}},hasFlashPlayerVersion:F,createSWF:function(Z,Y,X){if(M.w3){return u(Z,Y,X)}else{return undefined}},showExpressInstall:function(Z,aa,X,Y){if(M.w3&&A()){P(Z,aa,X,Y)}},removeSWF:function(X){if(M.w3){y(X)}},createCSS:function(aa,Z,Y,X){if(M.w3){v(aa,Z,Y,X)}},addDomLoadEvent:K,addLoadEvent:s,getQueryParamValue:function(aa){var Z=j.location.search||j.location.hash;if(Z){if(/\?/.test(Z)){Z=Z.split("?")[1]}if(aa==null){return L(Z)}var Y=Z.split("&");for(var X=0;X<Y.length;X++){if(Y[X].substring(0,Y[X].indexOf("="))==aa){return L(Y[X].substring((Y[X].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(a){var X=c(R);if(X&&l){X.parentNode.replaceChild(l,X);if(Q){w(Q,true);if(M.ie&&M.win){l.style.display="block"}}if(E){E(B)}}a=false}}}}();
\ No newline at end of file
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/base.js b/trunk/extras/src/main/javascript/features-extras/wave/base.js
new file mode 100644
index 0000000..9e41d85
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/base.js
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides the top level wave object.
+ */
+
+/**
+ * @namespace This namespace defines the top level wave object
+ * within the Wave Gadgets API.
+ */
+var wave = wave || {};
+
+/**
+ * Constructs a callback given the provided callback
+ * and an optional context.
+ *
+ * @constructor
+ * @this {wave.Callback}
+ * @class This class is an immutable utility class for handlings callbacks
+ *     with variable arguments and an optional context.
+ * @param {?(function(wave.State=, Object.<string, string>=)|
+ *           function(Array.<wave.Participant>=)|
+ *           function(wave.Mode.<number>=)
+ *          )} callback A callback function
+ *     or null.
+ * @param {Object=} opt_context If context is specified, the method will be
+ *     called back in the context of that object (optional).
+ */
+wave.Callback = function(callback, opt_context) {
+  this.callback_ = callback;
+  this.context_ = opt_context || null;
+};
+
+/**
+ * Invokes the callback method with any arguments passed.
+ *
+ * @param {...} var_args
+ * @export
+ */
+wave.Callback.prototype.invoke = function(var_args) {
+  if (this.callback_) {
+    this.callback_.apply(this.context_, arguments);
+  }
+};
+
+/**
+ * @name wave.Mode
+ * @class Identifiers for wave modes exhibited by the blip containing
+ *     the gadget.
+ * @enum {number}
+ * @export
+ */
+wave.Mode = {
+  /**
+   * @member wave.Mode
+   * @constant
+   * @name UNKNOWN
+   * @desc The blip containing the gadget is in an unknown mode.
+   * In this case, you should not attempt to edit the blip.
+   */
+  UNKNOWN: 0,
+  /**
+   * @member wave.Mode
+   * @constant
+   * @name VIEW
+   * @desc The blip containing the gadget is in view, but not edit mode.
+   */
+  VIEW: 1,
+  /**
+   * @member wave.Mode
+   * @constant
+   * @name EDIT
+   * @desc Editing the gadget blip
+   */
+  EDIT: 2,
+  /**
+   * @member wave.Mode
+   * @constant
+   * @name DIFF_ON_OPEN
+   * @desc The blip containing the gadget has changed since the last time
+   * it was opened and the gadget should notify this change to the user.
+   */
+  DIFF_ON_OPEN: 3,
+  /**
+   * @member wave.Mode
+   * @constant
+   * @name PLAYBACK
+   * @desc The blip containing the gadget is in playback mode.
+   */
+  PLAYBACK: 4
+};
+
+wave.API_PARAM_ = "wave";
+
+wave.ID_PARAM_ = "waveId";
+
+wave.id_ = null;
+
+wave.viewer_ = null;
+
+wave.host_ = null;
+
+wave.participants_ = [];
+
+wave.participantMap_ = {};
+
+wave.participantCallback_ = new wave.Callback(null);
+
+wave.state_ = null;
+
+wave.stateCallback_ = new wave.Callback(null);
+
+wave.privateState_ = null;
+
+wave.privateStateCallback_ = new wave.Callback(null);
+
+wave.mode_ = null;
+
+wave.modeCallback_ = new wave.Callback(null);
+
+wave.inWaveContainer_ = false;
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/dynamic-width.js b/trunk/extras/src/main/javascript/features-extras/wave/dynamic-width.js
new file mode 100644
index 0000000..e3cb312
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/dynamic-width.js
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+// TODO: Define a more convenient set of methods for iframe resizing in wave.
+
+/**
+ * @fileoverview This library augments gadgets.window with functionality
+ * to change the width of a gadget dynamically. Derived from the
+ * dynamic-height feature source code.
+ * See:
+ * http://svn.apache.org/repos/asf/shindig/trunk/features/src/main/javascript/features/dynamic-height/dynamic-height.js
+ */
+
+/**
+ * @static
+ * @class This namespace is used by the Gadgets API for the features it offers
+ * in all containers, including Wave. Those are documented here:
+ * http://code.google.com/apis/gadgets/docs/reference/
+ * @name gadgets
+ */
+
+/**
+ * @static
+ * @class This namespace is defined by the Gadgets API, and documented here:
+ * http://code.google.com/apis/gadgets/docs/reference/#gadgets.window <br>
+ * The Wave Gadgets API adds an additional method on top of the set documented
+ * there.
+ * @name gadgets.window
+ */
+gadgets.window = gadgets.window || {};
+
+// we wrap these in an anonymous function to avoid storing private data
+// as members of gadgets.window.
+(function() {
+
+  var oldWidth;
+
+  /**
+   * Parse out the value (specified in px) for a CSS attribute of an element.
+   *
+   * @param {Element} elem the element with the attribute to look for.
+   * @param {string} attr the CSS attribute name of interest.
+   * @returns {number} the value of the px attr of the elem.
+   * @private
+   */
+  function parseIntFromElemPxAttribute(elem, attr) {
+    var style = window.getComputedStyle(elem, "");
+    var value = style.getPropertyValue(attr);
+    value.match(/^([0-9]+)/);
+    return parseInt(RegExp.$1, 10);
+  }
+
+  /**
+   * For Webkit-based browsers, calculate the width of the gadget iframe by
+   * iterating through all elements in the gadget, starting with the body tag.
+   * It is not sufficient to only account body children elements, because
+   * CSS style position "float" may place a child element outside of the
+   * containing parent element. Not counting "float" elements may lead to
+   * undercounting.
+   *
+   * @returns {number} the width of the gadget.
+   * @private
+   */
+  function getWidthForWebkit() {
+    var result = 0;
+    var queue = [ document.body ];
+
+    while (queue.length > 0) {
+      var elem = queue.shift();
+      var children = elem.childNodes;
+
+      for (var i = 0; i < children.length; i++) {
+        var child = children[i];
+        if (typeof child.offsetLeft !== 'undefined' &&
+            typeof child.scrollWidth !== 'undefined') {
+          // scrollHeight already accounts for border-bottom, padding-bottom.
+          var right = child.offsetLeft + child.scrollWidth +
+              parseIntFromElemPxAttribute(child, "margin-right");
+          result = Math.max(result, right);
+        }
+        queue.push(child);
+      }
+    }
+
+    // Add border, padding and margin of the containing body.
+    return result
+        + parseIntFromElemPxAttribute(document.body, "border-right")
+        + parseIntFromElemPxAttribute(document.body, "margin-right")
+        + parseIntFromElemPxAttribute(document.body, "padding-right");
+  }
+
+  /**
+   * Adjusts the gadget width
+   * @param {number=} opt_width An optional preferred width in pixels. If not
+   *     specified, will attempt to fit the gadget to its content.
+   * @member gadgets.window
+   */
+  gadgets.window.adjustWidth = function(opt_width) {
+    var newWidth = parseInt(opt_width, 10);
+    var widthAutoCalculated = false;
+    if (isNaN(newWidth)) {
+      widthAutoCalculated = true;
+
+      // Resize the gadget to fit its content.
+
+      // Get the width of the viewport
+      var vw = gadgets.window.getViewportDimensions().width;
+      var body = document.body;
+      var docEl = document.documentElement;
+      if (document.compatMode === 'CSS1Compat' && docEl.scrollWidth) {
+        // In Strict mode:
+        // The inner content height is contained in either:
+        //    document.documentElement.scrollWidth
+        //    document.documentElement.offsetWidth
+        // Based on studying the values output by different browsers,
+        // use the value that's NOT equal to the viewport width found above.
+        newWidth = docEl.scrollWidth !== vw ?
+                     docEl.scrollWidth : docEl.offsetWidth;
+      } else if (navigator.userAgent.indexOf('AppleWebKit') >= 0) {
+        // In Webkit:
+        // Property scrollWidth and offsetWidth will only increase in value.
+        // This will incorrectly calculate reduced width of a gadget
+        // (ie: made smaller).
+        newWidth = getWidthForWebkit();
+      } else if (body && docEl) {
+        // In Quirks mode:
+        // documentElement.clientWidth is equal to documentElement.offsetWidth
+        // except in IE.  In most browsers, document.documentElement can be used
+        // to calculate the inner content width.
+        // However, in other browsers (e.g. IE), document.body must be used
+        // instead.  How do we know which one to use?
+        // If document.documentElement.clientWidth does NOT equal
+        // document.documentElement.offsetWidth, then use document.body.
+        var sw = docEl.scrollWidth;
+        var ow = docEl.offsetWidth;
+        if (docEl.clientWidth !== ow) {
+          sw = body.scrollWidth;
+          ow = body.offsetWidth;
+        }
+
+        // Detect whether the inner content width is bigger or smaller
+        // than the bounding box (viewport).  If bigger, take the larger
+        // value.  If smaller, take the smaller value.
+        if (sw > vw) {
+          // Content is larger
+          newWidth = sw > ow ? sw : ow;
+        } else {
+          // Content is smaller
+          newWidth = sw < ow ? sw : ow;
+        }
+      }
+    }
+
+    // Only make the RPC call if width has changed
+    if (newWidth !== oldWidth &&
+        !isNaN(newWidth) &&
+        !(widthAutoCalculated && newWidth === 0)) {
+      oldWidth = newWidth;
+      gadgets.rpc.call(null, "setIframeWidth", null, newWidth);
+    }
+  };
+}());
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/externs.js b/trunk/extras/src/main/javascript/features-extras/wave/externs.js
new file mode 100644
index 0000000..2b341a3
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/externs.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Contains variable declarations so that the jscompiler
+ * will not report errors on these objects.
+ */
+
+var gadgets;
+gadgets.rpc;
+gadgets.rpc.call = function(parent, serviceName, opt_callback, opt_params) {};
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/fake_gadgets.js b/trunk/extras/src/main/javascript/features-extras/wave/fake_gadgets.js
new file mode 100644
index 0000000..3ba8e4a
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/fake_gadgets.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides stubs and fakes for the gadgets namespace
+ * that is used in the API. This is used by jsunit tests.
+ */
+var gadgets = gadgets || {};
+
+gadgets.util = {};
+
+gadgets.util.registerOnLoadHandler = function(callback) {
+  callback.call();
+};
+
+gadgets.util.getUrlParameters = function() {
+  var params = {};
+  var waveParamName = 'wave';
+  params[waveParamName] = true;
+  params.hasOwnProperty = function(key) {
+    return !!this[key];
+  };
+  return params;
+};
+
+gadgets.json = {};
+
+gadgets.json.parse = function(data) {
+  return data;
+};
+
+gadgets.rpc = {};
+
+gadgets.rpc.register = function() {};
+
+gadgets.rpc.call = function() {};
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/feature.xml b/trunk/extras/src/main/javascript/features-extras/wave/feature.xml
new file mode 100644
index 0000000..77c9e44
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/feature.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>wave</name>
+  <dependency>dynamic-height</dependency>
+  <dependency>locked-domain</dependency>
+  <dependency>taming</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="base.js"/>
+    <script src="dynamic-width.js"/>
+    <script src="participant.js"/>
+    <script src="state.js"/>
+    <script src="taming.js" caja="1"/>
+    <script src="util.js"/>
+    <script src="wave.js"/>
+    <script src="wave.ui.js"/>
+    <api>
+      <exports type="rpc">wave_participants</exports>
+      <exports type="rpc">wave_gadget_state</exports>
+      <exports type="rpc">wave_state_delta</exports>
+      <exports type="rpc">wave_private_gadget_state</exports>
+      <exports type="rpc">wave_private_state_delta</exports>
+      <exports type="rpc">wave_gadget_mode</exports>
+      <uses type="rpc">setIframeWidth</uses>
+      <uses type="rpc">wave_gadget_state</uses>
+      <uses type="rpc">wave_log</uses>
+      <uses type="rpc">set_snippet</uses>
+      <uses type="rpc">wave_enable</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/participant.js b/trunk/extras/src/main/javascript/features-extras/wave/participant.js
new file mode 100644
index 0000000..a8ea922
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/participant.js
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides classes for defining and managing participants
+ * on a wave.
+ */
+
+/**
+ * Creates a new participant.
+ *
+ * @class This class specifies participants on a wave.
+ * @constructor
+ * Participant information can be dynamically updated (except for  IDs).
+ * This includes the thumbnail URL, display name, and  any future extensions to
+ * the participant. Instead of storing this information, gadgets should update
+ * displayed participant data each time they receive a participant callback.
+ * @this {wave.Participant}
+ * @param {string=} id Participant id.
+ * @param {string=} displayName Participant display name.
+ * @param {string=} thumbnailUrl Profile thumbnail URL.
+ */
+wave.Participant = function(id, displayName, thumbnailUrl) {
+  this.id_ = id || '';
+  this.displayName_ = displayName || '';
+  this.thumbnailUrl_ = thumbnailUrl || '';
+};
+
+/**
+ * Gets the unique identifier of this participant.
+ *
+ * @return {string} The participant's id.
+ * @export
+ */
+wave.Participant.prototype.getId = function() {
+  return this.id_;
+};
+
+/**
+ * Gets the human-readable display name of this participant.
+ *
+ * @return {string} The participant's human-readable display name.
+ * @export
+ */
+wave.Participant.prototype.getDisplayName = function() {
+  return this.displayName_;
+};
+
+/**
+ * Gets the url of the thumbnail image for this participant.
+ *
+ * @return {string} The participant's thumbnail image url.
+ * @export
+ */
+wave.Participant.prototype.getThumbnailUrl = function() {
+  return this.thumbnailUrl_;
+};
+
+/**
+ * Constructs a Participant object from JSON data.
+ *
+ * @param {!Object.<string, string>} json JSON object.
+ * @return {wave.Participant}
+ */
+wave.Participant.fromJson_ = function(json) {
+  var p = new wave.Participant();
+  p.id_ = json['id'];
+  p.displayName_ = json['displayName'];
+  p.thumbnailUrl_ = json['thumbnailUrl'];
+  return p;
+};
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/state.js b/trunk/extras/src/main/javascript/features-extras/wave/state.js
new file mode 100644
index 0000000..c65effc
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/state.js
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides classes for defining and managing the
+ * synchronized gadget state.
+ */
+
+/**
+ * Creates a new state object to hold properties of the gadget.
+ *
+ * @constructor
+ * @class This class contains state properties of the Gadget.
+ * @this {wave.State}
+ * @param {string=} opt_rpc rpc name
+ * @export
+ */
+wave.State = function(opt_rpc) {
+  this.setState_(null);
+  this.rpc_ = opt_rpc === undefined ? 'wave_gadget_state' : opt_rpc;
+};
+
+/**
+ * Retrieve a value from the synchronized state.
+ * As of now, get always returns a string. This will change at some point
+ * to return whatever was set.
+ *
+ * @param {string} key Value for the specified key to retrieve.
+ * @param {?string=} opt_default Optional default value if non-existant
+ *     (optional).
+ * @return {?string} Object for the specified key or null if not found.
+ * @export
+ */
+wave.State.prototype.get = function(key, opt_default) {
+  if (key in this.state_) {
+    return this.state_[key];
+  }
+  return opt_default === undefined ? null: opt_default;
+};
+
+/**
+ * Retrieve the valid keys for the synchronized state.
+ *
+ * @return {Array.<string>} set of keys
+ * @export
+ */
+wave.State.prototype.getKeys = function() {
+  var keys = [];
+  for (var key in this.state_) {
+    keys.push(key);
+  }
+  return keys;
+};
+
+/**
+ * Updates the state delta. This is an asynchronous call that
+ * will update the state and not take effect immediately. Creating
+ * any key with a null value will attempt to delete the key.
+ *
+ * @param {!Object.<string, ?string>} delta Map of key-value pairs representing
+ * a delta of keys to update.
+ * @export
+ */
+wave.State.prototype.submitDelta = function(delta) {
+  gadgets.rpc.call(null, this.rpc_, null, delta);
+};
+
+/**
+ * Submits delta that contains only one key-value pair. Note that if value is
+ * null the key will be removed from the state.
+ * See submitDelta(delta) for semantic details.
+ *
+ * @param {string} key
+ * @param {?string} value
+ * @export
+ */
+wave.State.prototype.submitValue = function(key, value) {
+  var delta = {};
+  delta[key] = value;
+  this.submitDelta(delta);
+};
+
+/**
+ * Submits a delta to remove all key-values in the state.
+ *
+ * @export
+ */
+wave.State.prototype.reset = function() {
+  var delta = {};
+  for (var key in this.state_) {
+    delta[key] = null;
+  }
+  this.submitDelta(delta);
+};
+
+/**
+ * Pretty prints the current state object. Note this is a debug method
+ * only.
+ *
+ * @return {string} The stringified state.
+ * @export
+ */
+wave.State.prototype.toString = function() {
+  return wave.util.printJson(this.state_, true);
+};
+
+/**
+ * Set the state object to the given value.
+ *
+ * @param {Object.<string, string>} state
+ */
+wave.State.prototype.setState_ = function(state) {
+  this.state_ = state || {};
+};
+
+/**
+ * Calculate a delta object that would turn this state into the state given
+ * in the parameter when applied to this state.
+ *
+ * @param {!Object.<string, string>} state
+ * @return {!Object.<string, string>} delta
+ */
+wave.State.prototype.calculateDelta_ = function(state) {
+  var delta = {};
+  for (var key in state) {
+    var hasKey = this.state_.hasOwnProperty(key);
+    if (!hasKey || (this.state_[key] != state[key])) {
+      delta[key] = state[key];
+    }
+  }
+  for (var key in this.state_) {
+    if (!state.hasOwnProperty(key)) {
+      delta[key] = null;
+    }
+  }
+  return delta;
+};
+
+/**
+ * Apply the given delta object to this state.
+ *
+ * @param {!Object.<string, string>} delta
+ */
+wave.State.prototype.applyDelta_ = function(delta) {
+  this.state_ = this.state_ || {};
+  for (var key in delta) {
+    if (delta[key] != null) {
+      this.state_[key] = delta[key];
+    } else {
+      delete this.state_[key];
+    }
+  }
+};
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/taming.js b/trunk/extras/src/main/javascript/features-extras/wave/taming.js
new file mode 100644
index 0000000..d0a0be8
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/taming.js
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview  Tame and expose wave.* API to cajoled gadgets.
+ */
+
+tamings___.push(function(imports) {
+  // wave.Mode is an object literal that holds only constants
+  ___.grantRead(wave, 'Mode');
+
+  /**
+   * The following taming of wave.Callback and wave.Callback.invoke
+   * is needed because:
+   *   - wave.Callback is exposed to cajoled code
+   *   - wave.Callback.invoke is exposed to cajoled code
+   *   - the wave api invokes some callbacks constructed by itself
+   *     and others constructed by cajoled code
+   */
+  function SafeCallback(tameCallback, opt_tameContext) {
+   var okCallback = {apply: ___.markFuncFreeze(function(ignored, args) {
+     return ___.callPub(tameCallback, 'apply', [opt_tameContext, args]);
+   })};
+   return new wave.Callback(okCallback, ___.USELESS);
+  }
+
+  SafeCallback.prototype = wave.Callback.prototype;
+  wave.Callback.prototype.constructor = SafeCallback;
+  ___.markCtor(SafeCallback, Object, 'Callback');
+  ___.primFreeze(SafeCallback);
+  ___.tamesTo(wave.Callback, SafeCallback);
+
+  ___.handleGenericMethod(SafeCallback.prototype, 'invoke', function(var_args) {
+   return ___.callPub(this.callback_, 'apply', [___.tame(this.context_),
+                                                Array.slice(arguments, 0)]);
+  });
+
+  caja___.whitelistCtors([
+    [wave, 'Participant', Object],
+    [wave, 'State', Object]
+  ]);
+
+  caja___.whitelistMeths([
+    [wave.Participant, 'getDisplayName'],
+    [wave.Participant, 'getId'],
+    [wave.Participant, 'getThumbnailUrl'],
+
+    [wave.State, 'get'],
+    [wave.State, 'getKeys'],
+    [wave.State, 'reset'],
+    [wave.State, 'submitDelta'],
+    [wave.State, 'submitValue'],
+    [wave.State, 'toString']
+  ]);
+
+  caja___.whitelistFuncs([
+    [wave, 'getHost'],
+    [wave, 'getMode'],
+    [wave, 'getParticipantById'],
+    [wave, 'getParticipants'],
+    [wave, 'getState'],
+    [wave, 'getTime'],
+    [wave, 'getViewer'],
+    [wave, 'isInWaveContainer'],
+    [wave, 'log'],
+    [wave, 'setModeCallback'],
+    [wave, 'setParticipantCallback'],
+    [wave, 'setStateCallback'],
+
+    [wave.util, 'printJson']
+  ]);
+
+  imports.outers.wave = ___.tame(wave);
+  ___.grantRead(imports.outers, 'wave');
+});
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/util.js b/trunk/extras/src/main/javascript/features-extras/wave/util.js
new file mode 100644
index 0000000..a3450f0
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/util.js
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides utility methods.
+ */
+
+/**
+ * @namespace This namespace defines utility methods for use within
+ * the Wave Gadgets API.
+ */
+wave.util = wave.util || {};
+
+wave.util.SPACES_ = '                                                 ';
+
+wave.util.toSpaces_ = function(tabs) {
+  return wave.util.SPACES_.substring(0, tabs * 2);
+};
+
+wave.util.isArray_ = function(obj) {
+  try {
+    return obj && typeof(obj.length) == 'number';
+  } catch (e) {
+    return false;
+  }
+};
+
+/**
+ * Outputs JSON objects in text format. Optionally pretty print.
+ *
+ * @param {Object} obj The object to print.
+ * @param {boolean=} opt_pretty If true, pretty print (optional).
+ * @param {number=} opt_tabs Number of tabs to start indent.
+ * @return {string} The formatted object in text.
+ */
+wave.util.printJson = function(obj, opt_pretty, opt_tabs) {
+  if (!obj || typeof(obj.valueOf()) != 'object') {
+    if (typeof(obj) == 'string') {
+      return '\'' + obj + '\'';
+    }
+    else if (obj instanceof Function) {
+      return '[function]';
+    }
+    return '' + obj;
+  }
+  var text = [];
+  var isArray = wave.util.isArray_(obj);
+  var brace = isArray ? '[]' : '{}';
+  var newline = opt_pretty ? '\n' : '';
+  var spacer = opt_pretty ? ' ' : '';
+  var i = 0;
+  var tabs = opt_tabs || 1;
+  if (!opt_pretty) {
+    tabs = 0;
+  }
+  text.push(brace.charAt(0));
+  for (var key in obj) {
+    var value = obj[key];
+    if (i++ > 0) {
+      text.push(', ');
+    }
+    if (isArray) {
+      text.push(wave.util.printJson(value, opt_pretty, tabs + 1));
+    } else {
+      text.push(newline);
+      text.push(wave.util.toSpaces_(tabs));
+      text.push(key + ': ');
+      text.push(spacer);
+      text.push(wave.util.printJson(value, opt_pretty, tabs + 1));
+    }
+  }
+  if (!isArray) {
+    text.push(newline);
+    text.push(wave.util.toSpaces_(tabs - 1));
+  }
+  text.push(brace.charAt(1));
+  return text.join('');
+};
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/wave.js b/trunk/extras/src/main/javascript/features-extras/wave/wave.js
new file mode 100644
index 0000000..8a530a0
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/wave.js
@@ -0,0 +1,389 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides access to the wave API in gadgets.
+ *
+ * Clients can access the wave API by the wave object.
+ *
+ * Example:
+ * <pre>
+ *   var state;
+ *   var privateState;
+ *   var viewer;
+ *   var participants;
+ *   if (wave && wave.isInWaveContainer()) {
+ *     state = wave.getState();
+ *     privateState = wave.getPrivateState();
+ *     viewer = wave.getViewer();
+ *     participants = wave.getParticipants();
+ *   }
+ * </pre>
+ */
+
+/**
+ * Checks the wave parameter to determine whether the gadget container claims
+ * to be wave-aware.
+ */
+wave.checkWaveContainer_ = function() {
+  var params = gadgets.util.getUrlParameters();
+  wave.inWaveContainer_ =
+      (params.hasOwnProperty(wave.API_PARAM_) && params[wave.API_PARAM_]);
+  wave.id_ = (params.hasOwnProperty(wave.ID_PARAM_) && params[wave.ID_PARAM_]);
+};
+
+/**
+ * Indicates whether the gadget runs inside a wave container.
+ *
+ * @return {boolean} whether the gadget runs inside a wave container
+ * @export
+ */
+wave.isInWaveContainer = function() {
+  return wave.inWaveContainer_;
+};
+
+/**
+ * Participant callback relay.
+ *
+ * @param {{myId: string, authorId: string,
+ *          participants: Object.<string, string>}} data participants object.
+ */
+wave.receiveWaveParticipants_ = function(data) {
+  wave.viewer_ = null;
+  wave.host_ = null;
+  wave.participants_ = [];
+  wave.participantMap_ = {};
+  var myId = data['myId'];
+  var hostId = data['authorId'];
+  var participants = data['participants'];
+  for (var id in participants) {
+    var p = wave.Participant.fromJson_(participants[id]);
+    if (id == myId) {
+      wave.viewer_ = p;
+    }
+    if (id == hostId) {
+      wave.host_ = p;
+    }
+    wave.participants_.push(p);
+    wave.participantMap_[id] = p;
+  }
+  if (!wave.viewer_ && myId) {
+    // In this case, the viewer has not yet been added to the participant
+    // list, and so did not have a complete Participant object created.
+    // Let's create it here.
+    var p = new wave.Participant(myId, myId);
+    wave.viewer_ = p;
+    wave.participants_.push(p);
+    wave.participantMap_[myId] = p;
+  }
+  wave.participantCallback_.invoke(wave.participants_);
+};
+
+/**
+ * State callback relay.
+ *
+ * @param {!Object.<string, string>} data raw state data object.
+ */
+wave.receiveState_ = function(data) {
+  wave.state_ = wave.state_ || new wave.State('wave_gadget_state');
+  var delta = wave.state_.calculateDelta_(data);
+  wave.state_.setState_(data);
+  wave.stateCallback_.invoke(wave.state_, delta);
+};
+
+/**
+ * Private state callback relay.
+ *
+ * @param {!Object.<string, string>} data raw state data object.
+ */
+wave.receivePrivateState_ = function(data) {
+  wave.privateState_ =
+      wave.privateState_ || new wave.State('wave_private_gadget_state');
+  var delta = wave.privateState_.calculateDelta_(data);
+  wave.privateState_.setState_(data);
+  wave.privateStateCallback_.invoke(wave.privateState_, delta);
+};
+
+/**
+ * State delta callback relay.
+ *
+ * @param {!Object.<string, string>} delta the delta object.
+ */
+wave.receiveStateDelta_ = function(delta) {
+  wave.state_ = wave.state_ || new wave.State('wave_gadget_state');
+  wave.state_.applyDelta_(delta);
+  wave.stateCallback_.invoke(wave.state_, delta);
+};
+
+/**
+ * Private state delta callback relay.
+ *
+ * @param {!Object.<string, string>} delta the delta object.
+ */
+wave.receivePrivateStateDelta_ = function(delta) {
+  wave.privateState_ =
+      wave.privateState_ || new wave.State('wave_private_gadget_state');
+  wave.privateState_.applyDelta_(delta);
+  wave.privateStateCallback_.invoke(wave.privateState_, delta);
+};
+
+/**
+ * Mode callback relay.
+ *
+ * @param {!Object.<string, string>} data raw mode object.
+ */
+wave.receiveMode_ = function(data) {
+  wave.mode_ = data || {};
+  wave.modeCallback_.invoke(wave.getMode());
+};
+
+/**
+ * Get the <code>Participant</code> whose client renders this gadget.
+ *
+ * @return {wave.Participant} the viewer (null if not known)
+ * @export
+ */
+wave.getViewer = function() {
+  return wave.viewer_;
+};
+
+/**
+ * Returns the <code>Participant</code> who added this gadget
+ * to the blip.
+ * Note that the host may no longer be in the participant list.
+ *
+ * @return {wave.Participant} host (null if not known)
+ * @export
+ */
+wave.getHost = function() {
+  return wave.host_;
+};
+
+/**
+ * Returns a list of <code>Participant</code>s on the Wave.
+ *
+ * @return {Array.<wave.Participant>} Participant list.
+ * @export
+ */
+wave.getParticipants = function() {
+  return wave.participants_;
+};
+
+/**
+ * Returns a <code>Participant</code> with the given id.
+ *
+ * @param {string} id The id of the participant to retrieve.
+ * @return {wave.Participant} The participant with the given id.
+ * @export
+ */
+wave.getParticipantById = function(id) {
+  return wave.participantMap_[id];
+};
+
+/**
+ * Returns the gadget state as a <code>wave.State</code> object.
+ *
+ * @return {wave.State} gadget state (null if not known)
+ * @export
+ */
+wave.getState = function() {
+  return wave.state_;
+};
+
+/**
+ * Returns the private gadget state as a <code>wave.State</code> object.
+ *
+ * @return {wave.State} private gadget state (null if not known)
+ * @export
+ */
+wave.getPrivateState = function() {
+  return wave.privateState_;
+};
+
+/**
+ * Returns the gadget <code>wave.Mode</code>.
+ *
+ * @return {wave.Mode} gadget mode.
+ * @export
+ */
+wave.getMode = function() {
+  if (wave.mode_) {
+    var playback = wave.mode_['${playback}'];
+    var edit = wave.mode_['${edit}'];
+    if ((playback != null) && (edit != null)) {
+      if (playback == '1') {
+        return wave.Mode.PLAYBACK;
+      } else if (edit == '1') {
+        return wave.Mode.EDIT;
+      } else {
+        return wave.Mode.VIEW;
+      }
+    }
+  }
+  return wave.Mode.UNKNOWN;
+};
+
+/**
+ * Returns the playback state of the wave/wavelet/gadget.
+ * Note: For compatibility UNKNOWN mode identified as PLAYBACK.
+ *
+ * @return {boolean} whether the gadget is in the playback state
+ * @deprecated Use wave.getMode().
+ * @export
+ */
+wave.isPlayback = function() {
+  var mode = wave.getMode();
+  return (mode == wave.Mode.PLAYBACK) || (mode == wave.Mode.UNKNOWN);
+};
+
+/**
+ * Sets the gadget state update callback. If the state is already received
+ * from the container, the callback is invoked immediately to report the
+ * current gadget state. Only invoke callback can be defined. Consecutive calls
+ * would remove the old callback and set the new one.
+ *
+ * @param {function(wave.State=, Object.<string, string>=)} callback function
+ * @param {Object=} opt_context the object that receives the callback
+ * @export
+ */
+wave.setStateCallback = function(callback, opt_context) {
+  wave.stateCallback_ = new wave.Callback(callback, opt_context);
+  if (wave.state_) {
+    wave.stateCallback_.invoke(wave.state_, wave.state_.state_);
+  }
+};
+
+/**
+ * Sets the private gadget state update callback. Works similarly to
+ * setStateCallback but handles the private state events.
+ *
+ * @param {function(wave.State=, Object.<string, string>=)} callback function
+ * @param {Object=} opt_context the object that receives the callback
+ * @export
+ */
+wave.setPrivateStateCallback = function(callback, opt_context) {
+  wave.privateStateCallback_ = new wave.Callback(callback, opt_context);
+  if (wave.privateState_) {
+    wave.privateStateCallback_.invoke(
+        wave.privateState_, wave.privateState_.state_);
+  }
+};
+
+/**
+ * Sets the participant update callback. If the participant information is
+ * already received, the callback is invoked immediately to report the
+ * current participant information. Only one callback can be defined.
+ * Consecutive calls would remove old callback and set the new one.
+ *
+ * @param {function(Array.<wave.Participant>)} callback function
+ * @param {Object=} [opt_context] the object that receives the callback
+ * @export
+ */
+wave.setParticipantCallback = function(callback, opt_context) {
+  wave.participantCallback_ = new wave.Callback(callback, opt_context);
+  if (wave.participants_) {
+    wave.participantCallback_.invoke(wave.participants_);
+  }
+};
+
+/**
+ * Sets the mode change callback.
+ *
+ * @param {function(wave.Mode)} callback function
+ * @param {Object=} [opt_context] the object that receives the callback
+ * @export
+ */
+wave.setModeCallback = function(callback, opt_context) {
+  wave.modeCallback_ = new wave.Callback(callback, opt_context);
+  if (wave.mode_) {
+    wave.modeCallback_.invoke(wave.getMode());
+  }
+};
+
+/**
+ * Retrieves the current time of the viewer.
+ *
+ * TODO: Define the necessary gadget <-> container communication and
+ * implement playback time.
+ *
+ * @return {number} The gadget time.
+ * @export
+ */
+wave.getTime = function() {
+  // For now just return the current time.
+  return new Date().getTime();
+};
+
+/**
+ * Requests the container to output a log message.
+ *
+ * @param {string} message The message to output to the log.
+ * @export
+ */
+wave.log = function(message) {
+  gadgets.rpc.call(null, 'wave_log', null, message || '');
+};
+
+/**
+ * Requests the container to update the snippet visible in wave digest.
+ *
+ * @param {string} snippet Snippet to associate with the gadget.
+ * @export
+ */
+wave.setSnippet = function(snippet) {
+  gadgets.rpc.call(null, 'set_snippet', null, snippet || '');
+};
+
+/**
+ * Returns serialized wave ID or null if not known.
+ *
+ * @return {?string} Serialized wave ID.
+ * @export
+ */
+wave.getWaveId = function() {
+  return wave.id_;
+};
+
+/**
+ * Internal initialization.
+ */
+wave.internalInit_ = function() {
+  wave.checkWaveContainer_();
+  if (wave.isInWaveContainer()) {
+    gadgets.rpc.register('wave_participants', wave.receiveWaveParticipants_);
+    gadgets.rpc.register('wave_gadget_state', wave.receiveState_);
+    gadgets.rpc.register('wave_state_delta', wave.receiveStateDelta_);
+    gadgets.rpc.register(
+        'wave_private_gadget_state', wave.receivePrivateState_);
+    gadgets.rpc.register(
+        'wave_private_state_delta', wave.receivePrivateStateDelta_);
+    gadgets.rpc.register('wave_gadget_mode', wave.receiveMode_);
+    gadgets.rpc.call(null, 'wave_enable', null, '1.0');
+  }
+};
+
+/**
+ * Sets up the wave gadget variables and callbacks.
+ */
+(wave.init_ = function() {
+  if (window['gadgets']) {
+    gadgets.util.registerOnLoadHandler(function() {
+      wave.internalInit_();
+    });
+  }
+})();
diff --git a/trunk/extras/src/main/javascript/features-extras/wave/wave.ui.js b/trunk/extras/src/main/javascript/features-extras/wave/wave.ui.js
new file mode 100644
index 0000000..9939737
--- /dev/null
+++ b/trunk/extras/src/main/javascript/features-extras/wave/wave.ui.js
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview UI extension to the basic wave gadget API.
+ *
+ * wave.ui allows gadgets to get the look and feel of other
+ * wave elements.
+ *
+ * Example: turn a link into a wave button
+ * <pre>
+ *   <a id="butTest" href="#" onclick="alert('hi!')">Text</a>
+ *
+ *   <script>
+ *     wave.ui.makeButton(document.getElementById('butTest'));
+ *   </script>
+ * </pre>
+ */
+
+if (typeof wave == "undefined") {
+  wave = {};
+}
+
+if (typeof wave.ui == "undefined") {
+/**
+ * @namespace This namespace defines methods for creating a wave
+ * look & feel inside a gadget.
+ */
+  wave.ui = {};
+}
+
+wave.ui.BASE = 'http://wave-api.appspot.com/public/';
+
+wave.ui.cssLoaded = false;
+
+/**
+ * Loads a CSS with Wave-like styles into the gadget, including font
+ * properties, link properties, and the properties for the wave-styled
+ * button, dialog, and frame.
+ *
+ * @export
+ */
+wave.ui.loadCss = function() {
+  if (wave.ui.cssLoaded) {
+    return;
+  }
+  wave.ui.cssLoaded = true;
+  var fileref = document.createElement("link");
+  fileref.setAttribute("rel", "stylesheet");
+  fileref.setAttribute("type", "text/css");
+  fileref.setAttribute("href", wave.ui.BASE + "wave.ui.css");
+  document.getElementsByTagName("head")[0].appendChild(fileref);
+};
+
+/**
+ * Converts the passed in target into a wave-styled button.
+ *
+ * @param {Element} target element to turn into a button. The target should be
+ * an anchor element.
+ * @export
+ */
+wave.ui.makeButton = function(target) {
+  wave.ui.loadCss();
+  target.innerHTML = '<span>' + target.innerHTML + '</span>';
+  target.className += ' wavebutton';
+};
+
+/**
+ * Converts the passed in target into a wave-styled dialog.
+ *
+ * For now it only creates a centered box. The close button in the upper right
+ * corner will be default do nothing.
+ *
+ * @param {Element} target element to turn into a dialog. The target should be
+ * a div.
+ * @param {string} title
+ * @export
+ */
+wave.ui.makeDialog = function(target, title, onclick) {
+  wave.ui.loadCss();
+
+  var body = target.innerHTML;
+  target.innerHTML = '';
+
+  var headDiv = document.createElement('div');
+  headDiv.className = 'wavedialoghead';
+
+  var span = document.createElement('span');
+
+  var closeDiv = document.createElement('div');
+  closeDiv.className = 'wavedialogclose';
+  function closeFunction() {
+    target.style.display = 'none';
+  }
+  closeDiv.onclick = onclick || closeFunction;
+
+  span.appendChild(closeDiv);
+  span.appendChild(document.createTextNode(title));
+
+  headDiv.appendChild(span);
+  target.appendChild(headDiv);
+
+  var bodyDiv = document.createElement('div');
+  bodyDiv.className = 'wavedialogbody';
+  bodyDiv.innerHTML = body;
+  target.appendChild(bodyDiv);
+  target.className += ' wavedialog';
+};
+
+/**
+ * Converts the passed in target into a wave-styled frame.
+ *
+ * @param {Element} target element to turn into a frame. The target should be
+ * a div.
+ * @export
+ */
+wave.ui.makeFrame = function(target) {
+  wave.ui.loadCss();
+  target.innerHTML = '<div class="waveboxhead"><span>&nbsp;</span></div>' +
+      '<div class="waveboxbody">' + target.innerHTML + '</div>';
+  target.className += ' wavebox';
+};
diff --git a/trunk/features/README b/trunk/features/README
new file mode 100644
index 0000000..58aa0c9
--- /dev/null
+++ b/trunk/features/README
@@ -0,0 +1,89 @@
+                          Apache Shindig Features
+
+  What is it?
+  -----------
+
+  Shindig is a JavaScript container and implementations of the backend APIs
+  and proxy required for hosting OpenSocial applications.
+
+  This is the features component of Shindig.
+
+  Documentation
+  -------------
+
+  The most up-to-date documentation can be found at http://shindig.apache.org
+
+  Using features
+  --------------
+
+  You can automatically include new features into your shindig server by adding
+  them to this directory. Features should exist in a self-contained directory
+  with the following structure:
+
+  features
+    |_your-feature-name
+     |_feature.xml
+     |_code-to-run-inside-gadget.js
+     |_code-to-run-inside-container.js
+
+  The structure of feature.xml is as follows:
+  TODO: link to xml schema for feature.xml
+
+  <?xml version="1.0"?>
+  <feature>
+    <name>your-feature-name (required)</name>
+    <dependency>any dependency you have (optional, may have multiple)</dependency>
+    <gadget> (optional)
+      <script src="code-to-run-inside-gadget.js"/> (optional, may have multiple,
+          may use web resources as well but we strongly encourage bundling your
+          javascript with the xml)
+    </gadget>
+    <container> (optional)
+      <script src="code-to-run-inside-container.js"> (same as for <gadget>)
+    </container>
+    <all> (optional, to mean for both gadget and container)
+      <script src="code-to-run-inside-either-gadget-or-container.js"> (same as for <gadget> or <container>)
+      <api>
+          optional, to export API in compiled JS for external usages and incremental-loading,
+          via ExportJsProcessor.java and exportJs JS.
+        <exports type="js">gadgets.rpc.call</exports>
+            optional, the API to export, for two scenarios:
+            1. the JS code internally can use obfuscated/property-renamed g.r.c() to reduce
+               size, but external clients continue to use unobfuscated (exported-to-window)
+               gadgets.rpc.call.
+            2. incremental-loading of JS. rpc.js is loaded (gadgets.rpc.* is exported). Then,
+               dynamic-height!rpc.js can be loaded without rpc (and its transitive dependencies)
+               and it will use unobfuscated (or extern'ed) gadgets.rpc.
+      </api>
+    </all>
+  </feature>
+
+  Please always make sure that all files you provide are encoded as utf8.
+
+  When adding new features, your javascript should conform to shindig javascript
+  coding conventions. This means that you should not be producing new globals and
+  your feature should be compatible with Caja.
+
+  When committing a new feature, you should update the features.txt file by
+  running the following script in a unix-like environment:
+
+  ls -R1a features/**/*.xml > features/features.txt
+
+  TODO: Instructions for regenerating features.txt for other operating system
+  environments.
+
+  Licensing
+  ---------
+
+  Please see the file called LICENSE.
+
+
+  Shindig URLS
+  ------------
+
+  Home Page:          http://shindig.apache.org/
+  Downloads:          http://shindig.apache.org/download/index.html
+  Mailing Lists:      http://shindig.apache.org/mail-lists.html
+  Source Code:        http://svn.apache.org/repos/asf/shindig
+  Issue Tracking:     https://issues.apache.org/jira/browse/SHINDIG
+  Wiki:               http://cwiki.apache.org/confluence/display/SHINDIG/
diff --git a/trunk/features/bin/README b/trunk/features/bin/README
new file mode 100644
index 0000000..8103d23
--- /dev/null
+++ b/trunk/features/bin/README
@@ -0,0 +1,13 @@
+The runner.sh script will run the javascript unit tests configured in src/test/javascript/features/alltests.js.
+It requires a version of rhino in your local m2 repository.
+
+It should be launched from the feature root directory,
+
+bin/runner.sh
+
+You should modify this to add your tests to it.
+
+It should be cleaned up to run all tests, but works for the osapi tests at the moment. The problem is that the alltests.js, which is an example from the jsunit site, is not recognizing all the AllTests_Suite.
+
+
+
diff --git a/trunk/features/bin/runner.sh b/trunk/features/bin/runner.sh
new file mode 100755
index 0000000..c0b7f13
--- /dev/null
+++ b/trunk/features/bin/runner.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# This script is designed to be run from the features module root directory.
+# It's very simple; feel free to make it more robust.
+
+java -cp ~/.m2/repository//rhino/js/1.7R1/js-1.7R1.jar org.mozilla.javascript.tools.shell.Main src/test/javascript/features/alltests.js BatchTest OsapiTest JsonRpcTransportTest
diff --git a/trunk/features/pom.xml b/trunk/features/pom.xml
new file mode 100644
index 0000000..70bfa1a
--- /dev/null
+++ b/trunk/features/pom.xml
@@ -0,0 +1,337 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-features</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Apache Shindig Features</name>
+  <description>Packages all the features that shindig provides into a single jar file to allow
+    loading from the classpath
+  </description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/features</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/features</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/features</url>
+  </scm>
+
+  <pluginRepositories>
+    <pluginRepository>
+      <id>jsdoctk2</id>
+      <url>http://jsdoctk-plugin.googlecode.com/svn/repo</url>
+    </pluginRepository>
+  </pluginRepositories>
+
+  <build>
+    <resources>
+      <resource>
+        <targetPath>features</targetPath>
+        <directory>${basedir}/src/main/javascript/features</directory>
+      </resource>
+    </resources>
+    <testResources>
+      <testResource>
+        <targetPath>features</targetPath>
+        <directory>${basedir}/src/test/javascript/features</directory>
+      </testResource>
+    </testResources>
+
+    <pluginManagement>
+      <!-- set versions of common plugins for reproducibility, ordered alphabetically by owner -->
+      <plugins>
+        <!-- Misc -->
+        <plugin>
+          <groupId>de.berlios.jsunit</groupId>
+          <artifactId>jsunit-maven2-plugin</artifactId>
+          <version>1.3</version>
+          <dependencies>
+            <dependency>
+                <groupId>rhino</groupId>
+                <artifactId>js</artifactId>
+                <version>1.7R1</version>
+            </dependency>
+          </dependencies>
+        </plugin>
+        <plugin>
+          <groupId>net.alchim31.maven</groupId>
+          <artifactId>yuicompressor-maven-plugin</artifactId>
+        </plugin>
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>net.alchim31.maven</groupId>
+                    <artifactId>yuicompressor-maven-plugin</artifactId>
+                    <versionRange>[0.0.0,)</versionRange>
+                    <goals>
+                      <goal>compress</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore />
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <!-- ordered alphabetically by owner -->
+    <plugins>
+      <plugin>
+        <groupId>de.berlios.jsunit</groupId>
+        <artifactId>jsunit-maven2-plugin</artifactId>
+        <executions>
+          <execution>
+            <configuration>
+              <sourceDirectory>${basedir}/src/main/javascript/features</sourceDirectory>
+              <sources>
+                <source>../../../../src/test/javascript/features/mocks/env.js</source>
+                <source>../../../../src/test/javascript/features/mocks/window.js</source>
+                <source>../../../../src/test/javascript/features/mocks/xhr.js</source>
+                <source>globals/globals.js</source>
+                <source>domnode/constants.js</source>
+                <source>cloo/cloo.js</source>
+                <source>core.config.base/config.js</source>
+                <source>core.config/validators.js</source>
+                <source>core.json/json-native.js</source>
+                <source>core.json/json-jsimpl.js</source>
+                <source>core.json/json-flatten.js</source>
+                <source>shindig.auth/auth.js</source>
+                <source>core.util.base/base.js</source>
+                <source>core.util.dom/dom.js</source>
+                <source>core.util.event/event.js</source>
+                <source>core.log/log.js</source>
+                <source>core.util.onload/onload.js</source>
+                <source>core.util.string/string.js</source>
+                <source>core.util.urlparams/urlparams.js</source>
+                <source>core.util/util.js</source>
+                <source>core.prefs/prefs.js</source>
+                <source>core.io/io.js</source>
+                <source>container.util/constant.js</source>
+                <source>container.util/util.js</source>
+                <source>container.site/site.js</source>
+                <source>container.site/site_holder.js</source>
+                <source>container.site.gadget/gadget_holder.js</source>
+                <source>container.site.gadget/gadget_site.js</source>
+                <source>container.site.url/url_holder.js</source>
+                <source>container.site.url/url_site.js</source>
+                <source>container/service.js</source>
+                <source>container/container.js</source>
+                <source>i18n/currencycodemap.js</source>
+                <source>i18n/datetimeformat.js</source>
+                <source>i18n/datetimeparse.js</source>
+                <source>i18n/formatting.js</source>
+                <source>i18n/numberformat.js</source>
+                <source>setprefs/setprefs.js</source>
+                <source>views/views.js</source>
+                <source>shindig.uri/uri.js</source>
+                <source>shindig.xhrwrapper/xhrwrapper.js</source>
+                <source>xmlutil/xmlutil.js</source>
+                <source>opensocial-data-context/datacontext.js</source>
+                <source>opensocial-data/data.js</source>
+                <source>opensocial-reference/opensocial.js</source>
+                <source>opensocial-reference/activity.js</source>
+                <source>opensocial-reference/address.js</source>
+                <source>opensocial-reference/album.js</source>
+                <source>opensocial-reference/bodytype.js</source>
+                <source>opensocial-reference/collection.js</source>
+                <source>opensocial-reference/container.js</source>
+                <source>opensocial-reference/datarequest.js</source>
+                <source>opensocial-reference/dataresponse.js</source>
+                <source>opensocial-reference/email.js</source>
+                <source>opensocial-reference/enum.js</source>
+                <source>opensocial-reference/environment.js</source>
+                <source>opensocial-reference/idspec.js</source>
+                <source>opensocial-reference/mediaitem.js</source>
+                <source>opensocial-reference/message.js</source>
+                <source>opensocial-reference/name.js</source>
+                <source>opensocial-reference/navigationparameters.js</source>
+                <source>opensocial-reference/organization.js</source>
+                <source>opensocial-reference/person.js</source>
+                <source>opensocial-reference/phone.js</source>
+                <source>opensocial-reference/responseitem.js</source>
+                <source>opensocial-reference/url.js</source>
+                <source>opensocial-base/fieldtranslations.js</source>
+                <source>opensocial-base/jsonactivity.js</source>
+                <source>opensocial-base/jsonalbum.js</source>
+                <source>opensocial-base/jsonmediaitem.js</source>
+                <source>opensocial-base/jsonperson.js</source>
+                <source>opensocial-jsonrpc/jsonrpccontainer.js</source>
+                <source>osapi.base/osapi.js</source>
+                <source>osapi.base/batch.js</source>
+                <source>osapi/jsonrpctransport.js</source>
+                <source>osapi/peoplehelpers.js</source>
+                <source>../../../../src/test/javascript/lib/testutils.js</source>
+                <source>oauthpopup/oauthpopup.js</source>
+                <source>selection/selection_container.js</source>
+                <source>selection/selection.js</source>
+                <source>actions/actions_container.js</source>
+                <source>actions/actions.js</source>
+                <source>opensearch/opensearch.js</source>
+                <source>embeddedexperiences/constant.js</source>
+                <source>embeddedexperiences/embedded_experiences_container.js</source>
+                <source>proxied-form-post/post.js</source>
+                <source>open-views.common/open-views-common-container.js</source>
+                <source>open-views.common/open-views-common-gadget.js</source>
+                <source>open-views.url/open-views-url-container.js</source>
+                <source>open-views.url/open-views-url-gadget.js</source>
+                <source>open-views.gadget/open-views-gadget-container.js</source>
+                <source>open-views.gadget/open-views-gadget-gadget.js</source>
+                <source>open-views.ee/open-views-ee-container.js</source>
+                <source>open-views.ee/open-views-ee-gadget.js</source>
+                <source>open-views.results/open-views-results-container.js</source>
+                <source>open-views.results/open-views-results-gadget.js</source>
+                <source>gadgets.json.ext/json-xmltojson.js</source>
+              </sources>
+              <testSourceDirectory>${basedir}/src/test/javascript/features</testSourceDirectory>
+              <testSuites>
+                <testSuite>
+                  <name>FeatureTests</name>
+                  <type>TESTCASES</type>
+                  <includes>
+                    <include>mocks/*.js</include>
+                    <include>*/*test.js</include>
+                  </includes>
+                </testSuite>
+              </testSuites>
+            </configuration>
+            <goals>
+              <goal>jsunit-test</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-eclipse-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <!-- TODO: Replace this with the more generic javascript plugin that
+          allows the use of arbitrary compressor / compilers.
+          The maven-javascript-plugin does not seem to work.
+        -->
+        <!-- <groupId>net.sf.hammerfest</groupId> -->
+        <!-- <artifactId>maven-javascript-plugin</artifactId> -->
+        <groupId>net.alchim31.maven</groupId>
+        <artifactId>yuicompressor-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>compress</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <suffix>.opt</suffix>
+          <excludes>
+            <exclude>**/*.xml</exclude>
+            <!-- Caja is already minified -->
+            <exclude>**/caja/*.js</exclude>
+          </excludes>
+          <jswarn>false</jswarn>
+          <statistics>false</statistics>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <!--
+       Profile used for rebuilding the xpc.swf file
+       You will need to put a copy of mtasc in features/mtasc
+       It's available from http://www.mtasc.org/
+  -->
+  <profiles>
+    <profile>
+      <id>flashxpc</id>
+      <build>
+        <defaultGoal>exec:exec</defaultGoal>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>exec-maven-plugin</artifactId>
+            <version>1.2</version>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>exec</goal>
+                 </goals>
+              </execution>
+            </executions>
+            <configuration>
+              <executable>./mtasc/mtasc</executable>
+              <!--<workingDirectory>/tmp</workingDirectory>-->
+              <arguments>
+                <!--  -header 1:1:1 -v  -main -version 8 -swf xpc.swf src/main/flex/Main.as -->
+                <argument>-header</argument>
+                <argument>1:1:1</argument>
+                <argument>-v</argument>
+                <argument>-main</argument>
+                <argument>-version</argument>
+                <argument>8</argument>
+                <argument>-swf</argument>
+                <argument>../content/xpc.swf</argument>
+                <argument>src/main/flex/Main.as</argument>
+              </arguments>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <profile>
+      <id>reporting</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>nl.windgazer</groupId>
+            <artifactId>jsdoctk-plugin</artifactId>
+            <version>2.3.2</version>
+            <configuration>
+              <recurse>3</recurse><!-- This is the level of recursion, not a boolean -->
+              <srcDir>${basedir}/src/main/javascript/features</srcDir>
+              <ext>js</ext>
+              <exclude>
+                <param>.+.opt.js</param>
+              </exclude>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/trunk/features/src/main/appended-resources/META-INF/LICENSE b/trunk/features/src/main/appended-resources/META-INF/LICENSE
new file mode 100644
index 0000000..a451683
--- /dev/null
+++ b/trunk/features/src/main/appended-resources/META-INF/LICENSE
@@ -0,0 +1,36 @@
+===============================================================================
+
+The Apache Shindig distribution includes a number of subcomponents
+with separate copyright notices and license terms. Your use of the
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+===============================================================================
+OpenSocial Specification 0.8:
+
+Copyright (c) 2008 OpenSocial Foundation (http://www.opensocial.org)
+Released under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+===============================================================================
+Code Mirror:
+ Copyright (c) 2007-2010 Marijn Haverbeke
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any
+ damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any
+ purpose, including commercial applications, and to alter it and
+ redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software. If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+    distribution.
+
+
diff --git a/trunk/features/src/main/appended-resources/META-INF/NOTICE b/trunk/features/src/main/appended-resources/META-INF/NOTICE
new file mode 100644
index 0000000..f01da37
--- /dev/null
+++ b/trunk/features/src/main/appended-resources/META-INF/NOTICE
@@ -0,0 +1,5 @@
+This product includes software (Gadget Server, Gadget Container)
+originally developed by Google Inc. (http://code.google.com/) and licensed
+to the ASF as initial contribution for Shindig.
+
+This product contains software (sha1 JS impl) developed by Google Inc.
diff --git a/trunk/features/src/main/flex/Main.as b/trunk/features/src/main/flex/Main.as
new file mode 100644
index 0000000..4965b10
--- /dev/null
+++ b/trunk/features/src/main/flex/Main.as
@@ -0,0 +1,187 @@
+/*

+ * Licensed to the Apache Software Foundation (ASF) under one

+ * or more contributor license agreements. See the NOTICE file

+ * distributed with this work for additional information

+ * regarding copyright ownership. The ASF licenses this file

+ * to you 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.

+ */

+

+import flash.external.ExternalInterface;

+import System.security;

+

+/**

+ * XPC Flash Based Transport

+ * Original design by evn@google.com (Eduardo Vela)

+ */

+class Main {

+  // Ensures that the callbacks installed by the SWF are installed only once per page context.

+  private static var SINGLETON:Boolean = false;

+

+  // Constructor: unused in this case. 

+  public function Main() {

+  }

+

+  /**

+   * Simple helper function to replace instances of the given from_str in the provided

+   * first argument with the given to_str string.

+   * @param str {String} String whose contents to replace.

+   * @param from_str {String} Token to replace in str.

+   * @param to_str {String} String to put in place of from_str in str.

+   * @returns Modified string.

+   */

+  public static function replace(str:String, from_str:String, to_str:String):String {

+    var out_str:String = "";

+    var search_ix:Number = 0;

+    while (search_ix < str.length) {

+      var found_ix:Number = str.indexOf(from_str, search_ix);

+      if (found_ix != -1) {

+        out_str = out_str.concat(str.substring(search_ix, found_ix)).concat(to_str);

+        search_ix = found_ix + from_str.length;

+      } else {

+        out_str = out_str.concat(str.substring(search_ix));

+        search_ix = str.length;

+      }

+    }

+    return out_str;

+  }

+

+  public static function esc(str:String):String {

+    return replace(str, "\\", "\\\\");

+  }

+

+  /**

+   * Removes the port piece of an assumed well-structured provided host:port pair.

+   * @param {String} str String representing a URI authority.

+   * @returns The host-only (minus port) piece of the inbound authority String.

+   */

+  public static function stripPortIfPresent(str:String):String {

+    var col_ix:Number = str.indexOf(":");

+    if (col_ix == -1) {

+      return str;

+    }

+    return str.substring(0, col_ix);

+  }

+

+  /**

+   * Implementation of handlers facilitating cross-domain communication through

+   * Flash's ExternalInterface and LocalConnection facilities, offering sender

+   * domain verification.

+   *

+   * This method may only be run once such that it has any effect, a fact enforced

+   * by a static SINGLETON boolean. This prevents confusion if the SWF is accidentally

+   * loaded more than once in a given Window or page.

+   *

+   * The method whitelists HTTP-to-SWF access only for the domain provided in the

+   * inbound 'origin' argument. This argument in turn is passed along in each

+   * call made that passes along cross-domain messages on the caller's behalf, ensuring

+   * that domain verification works as intended.

+   *

+   * It installs a single 'setup' handler, callable from JS, which in turn sets up

+   * a sendMessage one-way communication method, while starting to listen to

+   * an equivalent channel set up by a party on the opposite side of an IFRAME

+   * boundary. A child context should pass in "INNER" for the role argument to setup;

+   * the parent passes "OUTER". rpc_key is a unique key provided on the IFRAME URL's

+   * hash disambiguating it from all other IFRAMEs on-page, window, or machine, while

+   * channel_id is the child's ID.

+   *

+   * This SWF is written specifically for the gadgets.rpc cross-domain communication

+   * library, but could be rather easily adapted - with passed-in callback method

+   * names, for instance - to use by other libraries as well.

+   */

+  public static function main(swfRoot:MovieClip):Void {

+    var escFn:Function = esc;

+    var replaceFn:Function = replace;

+    

+    if (SINGLETON) return;

+    SINGLETON = true;

+    

+    var my_origin:String;

+

+    if (_level0.origin == undefined){

+      // No origin: accept from all HTTP callers.

+      // Domain verification will not apply.

+      my_origin = "http://*";

+    } else {

+      // Get origin from the query string.

+      my_origin = _level0.origin;

+    }

+

+    var ready_method:String = "gadgets.rpctx.flash._ready";

+    var recv_method:String = "gadgets.rpctx.flash._receiveMessage";

+    var setup_done_method:String = "gadgets.rpctx.flash._setupDone";

+

+    if (_level0.jsl == "1") {

+      // Use 'safe-exported' methods.

+      ready_method = "___jsl._fm.ready";

+      recv_method = "___jsl._fm.receiveMessage";

+      setup_done_method = "___jsl._fm.setupDone";

+    }

+

+    // Flash doesn't accept/honor ports, so we strip one if present

+    // for canonicalization.

+    var domain:String = stripPortIfPresent(

+        my_origin.substr(my_origin.indexOf("//") + 2, my_origin.length));

+    

+    // Whitelist access to this SWF for the sending HTTP domain.

+    // The my_origin field from which domain derives is passed along

+    // with each sent message. Together, these ensure domain verification

+    // of all sent messages is possible.

+    if (my_origin.substr(0,5) === "http:") {

+      security.allowInsecureDomain(domain);

+    } else {

+      security.allowDomain(domain);

+    }

+

+    // Install global communication channel setup method.

+    ExternalInterface.addCallback("setup", { }, function(rpc_key:String, channel_id:String, role:String) {

+      var other_role:String;

+

+      if (role == "INNER") {

+        other_role = "OUTER";

+      } else {

+        other_role = "INNER";

+        role = "OUTER";

+      }

+

+      var receiving_lc:LocalConnection = new LocalConnection();

+      var sending_lc:LocalConnection = new LocalConnection();

+      receiving_lc.receiveMessage =

+          function(to_origin:String, from_origin:String, in_rpc_key:String, message:String) {

+        if ((to_origin === "*" || to_origin === my_origin) && (in_rpc_key == rpc_key)) {

+          ExternalInterface.call(recv_method, escFn(message), escFn(from_origin), escFn(to_origin));

+        }

+      };

+

+      ExternalInterface.addCallback("sendMessage_" + channel_id + "_" + rpc_key + "_" + role,

+            { }, function(message:String, to_origin:String) {

+        if (!to_origin) to_origin = "*";

+        var sendId:String =

+            replaceFn("channel_" + channel_id + "_" + rpc_key + "_" + other_role, ":", "");

+        sending_lc.send(sendId,

+            "receiveMessage", to_origin, my_origin, rpc_key, message);

+      });

+      var recvId:String = replaceFn("channel_" + channel_id + "_" + rpc_key + "_" + role, ":", "");

+      receiving_lc.connect(recvId);

+      if (role == "INNER") {

+        // In child context, trigger notice that the setup method is complete.

+        // This in turn initiates a child-to-parent polling procedure to complete a bidirectional

+        // communication handshake, since otherwise meaningful messages could be passed and dropped

+        // before the receiving end was ready.

+        ExternalInterface.call(setup_done_method);

+      }

+    });

+

+    // Signal completion of the setup callback to calling-context JS for proper ordering.

+    ExternalInterface.call(ready_method);

+  }

+}

diff --git a/trunk/features/src/main/javascript/features/actions/actions.js b/trunk/features/src/main/javascript/features/actions/actions.js
new file mode 100644
index 0000000..aff7efb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/actions/actions.js
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides facilities for registering action callback
+ *               functions to actions that may be rendered anywhere in
+ *               the container.  Available to every gadget.
+ */
+gadgets['actions'] = (function() {
+
+  /**
+   * @constructor Object that maps action ids to callback functions.
+   */
+  function ActionCallbackRegistry() {
+    this.registryById = {};
+    this.addAction = function(actionId, callbackFn) {
+      this.registryById[actionId] = callbackFn;
+    };
+    this.removeAction = function(actionId) {
+      delete this.registryById[actionId];
+    };
+    this.getCallback = function(actionId) {
+      return this.registryById[actionId];
+    };
+  }
+
+  // create the callback registry
+  var callbackRegistry = new ActionCallbackRegistry(),
+      showListeners,
+      hideListeners;
+
+  gadgets.util.registerOnLoadHandler(function() {
+    // register rpc endpoint
+    gadgets.rpc.register('actions.runAction', function(id, selection) {
+      if (this.f !== '..') {
+        return;
+      }
+      var callback = callbackRegistry.getCallback(id);
+      if (callback) {
+        callback.call(this, selection);
+      }
+    });
+  });
+
+  return /** @scope gadgets.actions */ {
+    /**
+     * Registers an action with the actions feature.
+     *
+     * Example:
+     *
+     * <pre>
+     * gadgets.actions.addAction(actionObj);
+     * </pre>
+     *
+     * @param {function(Object)}
+     *          actionObj The action object.
+     *
+     * @member gadgets.actions
+     */
+    addAction: function(actionObj) {
+      var actionId = actionObj.id,
+          callback = actionObj.callback;
+      delete actionObj.callback;
+
+      callbackRegistry.addAction(actionId, callback);
+
+      // notify the container that an action has been added.
+      gadgets.rpc.call('..', 'actions.bindAction', null,
+        actionObj
+      );
+    },
+
+    /**
+     * Updates an action that has already been registered.
+     *
+     * Example:
+     *
+     * <pre>
+     * gadgets.actions.updateAction(actionObj);
+     * </pre>
+     *
+     * @param {function(Object)}
+     *          actionObj The action object.
+     *
+     * @member gadgets.actions
+     */
+    updateAction: function(actionObj) {
+      // TODO for now we only support updating the callback
+      // to support the declaratively contributed actions,
+      // we need to support updating the label as well.
+
+      var actionId = actionObj.id,
+          callback = actionObj.callback;
+      delete actionObj.callback;
+
+      callbackRegistry.addAction(actionId, callback);
+
+      // notify the container that an action has been added.
+      gadgets.rpc.call('..', 'actions.bindAction', null,
+        actionObj
+      );
+    },
+
+    /**
+     * Executes the action callback associated with the specified actionId
+     * in the context of the gadget which contributed that action. The
+     * gadget should call this method whenever an action is triggered by
+     * the user.
+     *
+     * @param {String, Object}
+     *          actionId The id of the action to execute.
+     *          opt_selection The current selection. This is optional.
+     *
+     * @member gadgets.actions
+     */
+    runAction: function(actionId, opt_selection) {
+      gadgets.rpc.call('..', 'actions.runAction', null,
+        actionId, opt_selection
+      );
+    },
+
+    /**
+     * Removes the association of a callback function with an action id.
+     *
+     * Example:
+     *
+     * <pre>
+     * gadgets.actions.removeAction(actionId);
+     * </pre>
+     *
+     * @param {string}
+     *          actionId The action identifier.
+     *
+     * @member gadgets.actions
+     */
+    removeAction: function(actionId) {
+      callbackRegistry.removeAction(actionId);
+
+      // notify the container to remove action from its UI
+      gadgets.rpc.call('..', 'actions.removeAction', null,
+        actionId
+      );
+    },
+
+    /**
+     * Gets array of actions at the specified path and passes the result
+     * to the callback function.
+     *
+     * Example:
+     *
+     * <pre>
+     * var callback = function(actions){
+     *  ...
+     * }
+     * gadgets.actions.getActionsByPath("container/navigationLinks", callback);
+     * </pre>
+     *
+     * @param {string}
+     *          path The path to the actions.
+     * @param {function}
+     *          callback A callback function to handle the returned actions
+     *          array.
+     *
+     * @member gadgets.actions
+     */
+    getActionsByPath: function(path, callback) {
+      gadgets.rpc.call('..', 'actions.get_actions_by_path', callback,
+        path
+      );
+    },
+
+    /**
+     * Gets array of actions for the specified data type and passes the result
+     * to the callback function.
+     *
+     * Example:
+     *
+     * <pre>
+     * var callback = function(actions){
+     *  ...
+     * }
+     * gadgets.actions.getActionsByDataType("opensocial.Person", callback);
+     * </pre>
+     *
+     * @param {string}
+     *          dataType The String representation of an OpenSocial data type.
+     * @param {function}
+     *          callback A callback function to handle the returned actions
+     *          array.
+     *
+     * @member gadgets.actions
+     */
+    getActionsByDataType: function(dataType, callback) {
+      gadgets.rpc.call('..', 'actions.get_actions_by_type', callback,
+        dataType
+      );
+    },
+
+    /**
+     * Registers a function to display actions in the gadget.
+     *
+     * @param {function}
+     *          The gadget's function to render actions
+     *          in its UI. The function takes the action object as
+     *          a parameter.
+     */
+    registerShowActionsListener: function(listener) {
+      if (typeof listener === 'function') {
+        if (!showListeners) {
+          showListeners = [];
+          gadgets.rpc.register('actions.onActionShow', function(actions) {
+            if (this.f !== '..') {
+              return;
+            }
+            for (var i = 0, listener; listener = showListeners[i]; i++) {
+              listener(actions);
+            }
+          });
+          gadgets.rpc.call('..', 'actions.registerShowCallback');
+        }
+        showListeners.push(listener);
+      }
+    },
+
+    /**
+     * Registers a function to hide (remove) actions in the gadget
+     *
+     * @param {function}
+     *          The gadget's function to hide (remove) actions
+     *          in its UI. The function takes the action object as
+     *          a parameter.
+     */
+    registerHideActionsListener: function(listener) {
+      if (typeof listener === 'function') {
+        if (!hideListeners) {
+          hideListeners = [];
+          gadgets.rpc.register('actions.onActionHide', function(actions) {
+            if (this.f !== '..') {
+              return;
+            }
+            for (var i = 0, listener; listener = hideListeners[i]; i++) {
+              listener(actions);
+            }
+          });
+          gadgets.rpc.call('..', 'actions.registerHideCallback');
+        }
+        hideListeners.push(listener);
+      }
+    }
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/actions/actions_container.js b/trunk/features/src/main/javascript/features/actions/actions_container.js
new file mode 100644
index 0000000..a252002
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/actions/actions_container.js
@@ -0,0 +1,853 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides facilities for contributing actions to various parts
+ *               of the UI. Available to the common container.
+ */
+(function() {
+  /**
+   * Determines if the passed param is valid action
+   *
+   * @param {Object} actionObj
+   * @return {boolean} If the passed actionObj is valid
+   */
+  function isValidActionObject(actionObj) {
+    var isValid = true;
+    if (!actionObj) {
+      isValid = false;
+    } else {
+      var id = actionObj.id,
+          path = actionObj.path,
+          dataType = actionObj.dataType;
+
+      if (!id || !(path || dataType)) {
+        isValid = false;
+      }
+    }
+    return isValid;
+  }
+
+  /**
+   * @constructor Object that tracks the actions currently registered with the
+   *              container.
+   */
+  function ActionRegistry() {
+
+    // maps action ids to action objects
+    this.registryById = {};
+
+    // maps actions by contribution path
+    this.registryByPath = {};
+
+    // maps actions to OS data types
+    this.registryByDataType = {};
+
+    // maps actions to URL of the contributor
+    this.registryByUrl = {};
+
+    // one-to-many association of urls to gadget sites
+    this.urlToSite = {};
+
+    // one-to-one relationship of each action to the url
+    this.actionToUrl = {};
+
+    /**
+     * Adds an action object to the registry
+     *
+     * @param {Object}
+     *          actionObj JSON object that represents an action.
+     * @param {String}
+     *          url gadget spec URL, from which the action contribution
+     *          originated.
+     */
+    this.addAction = function(actionObj, url) {
+      if (!isValidActionObject(actionObj)) {
+        return; // invalid object
+      }
+      var id = actionObj.id,
+          path = actionObj.path,
+          dataType = actionObj.dataType;
+
+      if (path) {
+        /**
+         * We maintain a tree of arrays for actions that are contributed
+         * to paths.  This is necessary to realize actions in hierarchical
+         * menus, sub-menus and drop-down toolbar buttons.
+         */
+        var partsOfPath = path.split('/');
+        var parent = this.registryByPath;
+        for (var i = 0; i < partsOfPath.length; i++) {
+          var currentNode = partsOfPath[i];
+          if (!parent[currentNode]) {
+            parent[currentNode] = {};
+          }
+          parent = parent[currentNode];
+        }
+        // store actions as array under attribute "@actions"
+        var actionsAtPath = parent['@actions'];
+        if (!actionsAtPath) {
+          parent['@actions'] = [actionObj];
+        } else {
+          parent['@actions'] = actionsAtPath.concat(actionObj);
+        }
+      } else if (dataType) {
+        /**
+         * We maintain a simple map for actions that are bound to an
+         * OpenSocial data object type such as the person object.
+         */
+        this.registryByDataType[dataType] =
+          this.registryByDataType[dataType] ?
+              this.registryByDataType[dataType].concat(actionObj) :
+                [actionObj];
+      }
+
+      // add action to id registry
+      this.registryById[id] = actionObj;
+
+      // map actions to url, used by runAction to render gadget
+      if (url) {
+        this.actionToUrl[id] = url;
+        this.registryByUrl[url] =
+          this.registryByUrl[url] ?
+              this.registryByUrl[url].concat(actionObj) :
+                [actionObj];
+      }
+    };
+
+    /**
+     * Removes an action object from the registry
+     *
+     * @param {String}
+     *          actionId unique identifier for the action, as specified in the
+     *          action object.
+     */
+    this.removeAction = function(actionId) {
+      var actionObj = this.registryById[actionId];
+
+      // remove from registryById
+      delete this.registryById[actionId];
+
+      // remove from the other registries
+      var path = actionObj.path;
+      if (path) { // remove from registryByPath
+          var actionsAtPath = this.getActionsByPath(path);
+          var i = actionsAtPath.indexOf(actionObj);
+          if (i != -1) {
+            actionsAtPath.splice(i, 1);
+          }
+      } else { // remove from registryByDataType
+        var dataType = actionObj.dataType;
+        var actionsForDataType = this.registryByDataType[dataType];
+        var actionIndex = actionsForDataType.indexOf(actionObj);
+        actionsForDataType.splice(actionIndex, 1);
+        if (actionsForDataType.length == 0) {
+          delete this.registryByDataType[dataType];
+        }
+      }
+
+      // remove from url mappings
+      var url = this.actionToUrl[actionId];
+      if (url) {
+        delete this.actionToUrl[actionId];
+        var actionsForUrl = this.registryByUrl[url];
+        var actionIndex = actionsForUrl.indexOf(actionObj);
+        actionsForUrl.splice(actionIndex, 1);
+        if (actionsForUrl.length == 0) {
+          delete this.registryByUrl[url];
+        }
+      }
+    };
+
+    /**
+     * Returns the action associated with the specified id
+     *
+     * @param {String}
+     *          id Unique identifier for the action object.
+     */
+    this.getItemById = function(id) {
+      var children = this.registryById ? this.registryById : {};
+      return children[id];
+    };
+
+    /**
+     * Returns all actions in the registry
+     */
+    this.getAllActions = function() {
+      var actions = [];
+      for (actionId in this.registryById) {
+        if(this.registryById.hasOwnProperty(actionId)) {
+          actions = actions.concat(this.registryById[actionId]);
+        }
+      }
+      return actions;
+    };
+
+    /**
+     * Returns all items associated with the given path
+     *
+     * @param {String}
+     *          path Navigation path to the action, as specified in the action
+     *          object.
+     */
+    this.getActionsByPath = function(path) {
+      var actions = [];
+      var partsOfPath = path.split('/');
+      var children = this.registryByPath ? this.registryByPath : {};
+      for (var i = 0; i < partsOfPath.length; i++) {
+        var currentNode = partsOfPath[i];
+        if (children[currentNode]) {
+          children = children[currentNode];
+        } else {
+          // if path doesn't exist, return empty array
+          return actions;
+        }
+      }
+      if (children) {
+        actions = children['@actions'];
+      }
+      return actions;
+    };
+
+    /**
+     * Returns the actions associated with the specified data object
+     *
+     * @param {String}
+     *          dataType The Open Social data type associated with the action.
+     */
+    this.getActionsByDataType = function(dataType) {
+      var actions = [];
+      if (this.registryByDataType[dataType]) {
+        actions = this.registryByDataType[dataType];
+      }
+      return actions;
+    };
+
+    /**
+     * Returns the actions associated with the specified url
+     *
+     * @param {String}
+     *          url The gadget spec url associated with the action(s).
+     */
+    this.getActionsByUrl = function(url) {
+      var children = [];
+      if (this.registryByUrl[url]) {
+        children = children.concat(this.registryByUrl[url]);
+      }
+      return children;
+    };
+
+    /**
+     * Adds a new active gadget site to the registry
+     *
+     * @param {String}
+     *          url The gadget spec url associated with the gadget site.
+     * @param {osapi.container.GadgetSite}
+     *          site The instance of the gadget site.
+     */
+    this.addGadgetSite = function(url, site) {
+      var existingSites = this.urlToSite[url];
+      if (existingSites) {
+        this.urlToSite[url] = existingSites.concat(site);
+      } else {
+        this.urlToSite[url] = [site];
+      }
+    };
+
+    /**
+     * Removes a gadget site from the registry
+     *
+     * @param {String}
+     *          siteId The unique identifier for the gadget site instance.
+     */
+    this.removeGadgetSite = function(siteId) {
+      for (var url in this.urlToSite) {
+        if(this.urlToSite.hasOwnProperty(url)) {
+          var sites = this.urlToSite[url];
+          if(!sites) {
+           continue;
+          }
+          for (var i = 0; i < sites.length; i++) {
+            var site = sites[i];
+            if (site && site.getId() == siteId) {
+              sites.splice(i, 1);
+              if (sites.length == 0) {
+                delete this.urlToSite[url];
+              }
+            }
+          }
+        }
+      }
+    };
+
+    /**
+     * Return the gadget sites associated with the specified action object.
+     *
+     * @param {Object}
+     *          actionId The id of the action.
+     * @return {Array} Always an array of the gadget site instances associated
+     *         with the action object.
+     */
+    this.getGadgetSites = function(actionId) {
+      var action = this.getItemById(actionId);
+      var url = this.actionToUrl[actionId];
+      var sites = [];
+      var candidates = this.urlToSite[url];
+
+      if (candidates) {
+        // Return subset of matching sites (gadget view matches declared action view,
+        // if the action declared a view) Do not modify existing array.
+        for (var i = 0; i < candidates.length; i++) {
+          var site = candidates[i];
+          var holder = site.getActiveSiteHolder();
+          if (!action.view || (holder && holder.getView() === action.view)) {
+            sites.push(site);
+          }
+        }
+      }
+
+      return sites;
+    };
+
+    /**
+     * Returns the url associated with an action
+     *
+     * @param {String}
+     *          actionId The id of the action.
+     * @return {String} url Gadget spec url associated with the action.
+     */
+    this.getUrl = function(actionId) {
+      return this.actionToUrl[actionId];
+    };
+  };
+
+  /**
+   * Container handling of an action that has been programmatically added via
+   * gadgets.actions.addAction() API
+   *
+   * @param {Object}
+   *          actionObj The action object coming from the gadget side.
+   *
+   */
+  function bindAction(actionObj) {
+    var actionId = actionObj.id;
+    var containerActionObj = registry.getItemById(actionId);
+    // if action is not in registry, then this is a programmatic add
+    if (!containerActionObj) {
+      addAction(actionObj);
+    } else {
+      // check if this action needs to be run
+      var pendingAction = pendingActions[actionId];
+      if (pendingAction) {
+        runAction(actionId, pendingAction.selection);
+        delete pendingActions[actionId];
+      }
+    }
+  }
+
+  /**
+   * Adds the action to the action registry, and renders the action in the
+   * container UI.
+   *
+   * @param {Object}
+   *          actionObj The action object with id, label, title, icon, and any
+   *          other information needed to render the action in the container's
+   *          UI.
+   * @param {String}
+   *          url Optional value needed to be passed in when adding action via
+   *          preload listener (for subsequent loading of the gadget).
+   *
+   */
+  function addAction(actionObj, url) {
+    if (!isValidActionObject(actionObj)) {
+      return; // invalid action
+    }
+    registry.addAction(actionObj, url);
+
+    // Comply with spec by passing an array of the object
+    // TODO: Update spec, since there will never be more than 1 element in the array
+    showActionHandler([actionObj]);  // notify the container to display the action
+
+    for (var to in showActionSiteIds) {
+      if(showActionSiteIds.hasOwnProperty(to)) {
+        if (!container_.getGadgetSiteByIframeId_(to)) {
+          delete showActionSiteIds[to];
+        }
+        else {
+          // Comply with spec by passing an array of the object
+          // TODO: Update spec, since there will never be more than 1 element in the array
+          gadgets.rpc.call(to, 'actions.onActionShow', null, [actionObj]);
+        }
+      }
+    }
+  }
+
+  /**
+   * Removes the action from the action registry, and removes the action from
+   * the container UI.
+   *
+   * @param {String}
+   *          The action id.
+   *
+   */
+  function removeAction(id) {
+    var actionObj = registry.getItemById(id);
+    registry.removeAction(id);
+
+    // Comply with spec by passing an array of the object
+    // TODO: Update spec, since there will never be more than 1 element in the array
+    hideActionHandler([actionObj]);  // notify the container to hide the action
+
+    for (var to in hideActionSiteIds) {
+      if (hideActionSiteIds.hasOwnProperty(to)) {
+        if (!container_.getGadgetSiteByIframeId_(to)) {
+          delete hideActionSiteIds[to];
+        }
+        else {
+          // Comply with spec by passing an array of the object
+          // TODO: Update spec, since there will never be more than 1 element in the array
+          gadgets.rpc.call(to, 'actions.onActionHide', null, [actionObj]);
+        }
+      }
+    }
+  }
+
+  /**
+   * A map of all listeners.
+   *
+   * @type {Object.<string, Array.<function(string, Array.<Object>)>>}
+   */
+  var actionListenerMap = {};
+
+  /**
+   * A list of listeners to be notified when any action is invoked.
+   *
+   * @type {Array.<function(string, Array.<Object>)>}
+   */
+  var actionListeners = [];
+
+  /**
+   * Runs the action associated with the specified actionId. If the gadget has
+   * not yet been rendered, renders the gadget first, then runs the action.
+   *
+   * @param {string}
+   *         id The unique identifier for the action.
+   * @param {?Array.<Object>=}
+   *         selection The selection to pass to the action.
+   *
+   */
+  function runAction(id, selection) {
+    if (!selection && container_ && container_.selection) {
+      selection = container_.selection.getSelection();
+    }
+
+    // call all container listeners, if any, for this actionId
+    var listenersArray = actionListenerMap[id];
+    var i;
+    if (listenersArray) {
+      for (i = 0; i < listenersArray.length; i++) {
+        var listener = listenersArray[i];
+        listener.call(null, id, selection);
+      }
+    }
+    for (i = 0;  i < actionListeners.length; i++) {
+      var listener = actionListeners[i];
+      listener.call(null, id, selection);
+    }
+
+    // make rpc call to get gadgets to run callback based on action id
+    var gadgetSites = registry.getGadgetSites(id);
+    if (gadgetSites) {
+      for (i = 0; i < gadgetSites.length; i++) {
+        var site = gadgetSites[i];
+        var holder = site.getActiveSiteHolder();
+        if (holder) {
+          gadgets.rpc.call(holder.getIframeId(), 'actions.runAction', null, id, selection);
+        }
+      }
+    }
+  }
+
+  /**
+   * Fix list of actions from actions contributions to check if it has been wrapped with <actions>
+   * tag to avoid DOM parser error.
+   *
+   * @param {string} actionsContributionsParam the string containing the action tags
+   * @return {string} the corrected actions list wrapped with <actions> tag to avoid DOM parser error.
+   */
+  function fixActionContributions(actionsContributionsParam) {
+    var actions = actionsContributionsParam;
+    if(typeof actions !== 'string') {
+      actions = actions.toString();
+    }
+
+    // cleanup the newlines and extra spaces
+    actions = actions.replace(/\n/g, '');
+    actions = actions.replace(/\s+</g, '<');
+    actions = actions.replace(/>\s+/g, '>');
+
+    // check if actions content is wrapped with <actions> tag
+    if (actions.indexOf("<actions>") === -1) {
+     actions = "<actions>" + actions + "</actions>";
+    }
+    return actions;
+  }
+
+  /**
+   * Callback for loading actions after gadget has been preloaded.
+   *
+   * @param {Object}
+   *          Response from container's lifecycle handling of preloading the
+   *          gadget.
+   */
+  var preloadCallback = function(response) {
+    for (var url in response) {
+      if(!response.hasOwnProperty(url)) {
+        continue;
+      }
+      var metadata = response[url];
+      if (metadata.error || !metadata.modulePrefs) {
+        continue; // bail
+      }
+
+      var feature = metadata.modulePrefs.features['actions'],
+          desc = feature && feature.params ? feature.params['action-contributions'] : null;
+      if (!desc) {
+        continue; // bail
+      }
+
+      // fix action-contributions param until OpenSocial specs change is implemented:
+      // http://code.google.com/p/opensocial-resources/issues/detail?id=1264
+      desc = fixActionContributions(desc);
+
+      var dom;
+      try {
+        dom = opensocial.xmlutil.parseXML(desc);
+      } catch(ignore){}
+      if (!dom) {
+        continue; // bail
+      }
+
+      var jsonDesc = gadgets.json.xml.convertXmlToJson(dom),
+          actionsJson = jsonDesc['actions'];
+      if (!actionsJson) {
+        continue; // bail
+      }
+
+      var actions = [].concat(actionsJson['action']);
+      for (var i = 0; i < actions.length; i++) {
+        var actionObj = actions[i];
+        var actionClone = {};
+        // replace @ for attribute keys;
+        for (var key in actionObj) {
+          if(actionObj.hasOwnProperty(key)) {
+            actionClone[key.substring(1)] = actionObj[key];
+          }
+        }
+        // check if action already exists
+        if (!registry.getItemById(actionClone.id)) {
+          addAction(actionClone, url);
+        } else {
+          gadgets.warn(['Duplicated gadget action [', actionClone.id, '] detected, make sure the gadget actions have unique ids.'].join(''));
+        }
+      }
+    }
+  };
+
+  /**
+   * Callback for when gadget site has been navigated.
+   *
+   * @param {Object}
+   *          Gadget site that has been navigated.
+   */
+  var navigatedCallback = function(site) {
+    var holder = site.getActiveSiteHolder();
+    if (holder) {
+      var url = holder.getUrl();
+      registry.addGadgetSite(url, site);
+    }
+  };
+
+  /**
+   * Callback for when a gadget site has been closed.
+   *
+   * @param {Object}
+   *          Gadget site that has been closed.
+   */
+  var closedCallback = function(site) {
+    var siteId = site.getId();
+    registry.removeGadgetSite(siteId);
+  };
+
+  /**
+   * Callback for when a gadget has been unloaded.
+   *
+   * @param {String}
+   *          Gadget spec url for the gadget that has been unloaded.
+   */
+  var unloadedCallback = function(url) {
+    var actionsForUrl = registry.getActionsByUrl(url);
+    for (var i = 0; i < actionsForUrl.length; i++) {
+      var action = actionsForUrl[i];
+      removeAction(action.id);
+    }
+  };
+
+  // Object containing gadget lifecycle listeners
+  var actionsLifecycleCallback = {};
+  actionsLifecycleCallback[osapi.container.CallbackType.ON_PRELOADED] =
+    preloadCallback;
+  actionsLifecycleCallback[osapi.container.CallbackType.ON_NAVIGATED] =
+    navigatedCallback;
+  actionsLifecycleCallback[osapi.container.CallbackType.ON_BEFORE_CLOSE] =
+    closedCallback;
+  actionsLifecycleCallback[osapi.container.CallbackType.ON_UNLOADED] =
+    unloadedCallback;
+
+  /**
+   * Function that renders actions in the container's UI
+   *
+   * @param {Object}
+   *          actionObj The object with id, label, tooltip, icon and any other
+   *          information for the container to use to render the action.
+   */
+  var showActionHandler = function(actions) {},
+      showActionSiteIds = {};
+
+  /**
+   * Function that hides actions from the container's UI
+   *
+   * @param {Object}
+   *          actionObj The object with id, label, tooltip, icon and any other
+   *          information for the container to use to render the action.
+   */
+  var hideActionHandler = function(actions) {},
+      hideActionSiteIds = {};
+
+  /**
+   * Function that renders gadgets in container's UI
+   *
+   * @param {String}
+   *          gadgetSpecUrl The gadget spec url.
+   * @param {Object}
+   *          opt_params  The optional parameters for rendering the gadget.
+   */
+  var renderGadgetInContainer = function(gadgetSpecUrl, opt_params) {};
+
+  // instantiate the singleton action registry
+  var registry = new ActionRegistry();
+
+  // a map to track actions that are scheduled to run after
+  // pre-loaded gadget has been rendered
+  var pendingActions = {};
+
+  // container instance
+  var container_ = null;
+
+  /**
+   * Add the Container API for the action service.
+   */
+  osapi.container.Container.addMixin('actions', function(container) {
+    container_ = container;
+
+    container_.rpcRegister('actions.registerHideCallback', function(rpcArgs) {
+        hideActionSiteIds[rpcArgs.f] = 1;
+      });
+    container_.rpcRegister('actions.registerShowCallback', function(rpcArgs) {
+        showActionSiteIds[rpcArgs.f] = 1;
+      });
+    container_.rpcRegister('actions.bindAction', function(rpcArgs, actionObj) {
+        bindAction(actionObj);
+      });
+    container_.rpcRegister('actions.get_actions_by_type', function (rpcArgs, dataType) {
+        return [].concat(registry.getActionsByDataType(dataType));
+      });
+    container_.rpcRegister('actions.get_actions_by_path', function(rpcArgs, path) {
+        return [].concat(registry.getActionsByPath(path));
+      });
+    container_.rpcRegister('actions.removeAction', function(rpcArgs, id) {
+        return removeAction(id);
+      });
+    container_.rpcRegister('actions.runAction', function (rpcArgs, id, selection) {
+        container_.actions.runAction(id, selection);
+      });
+
+    if (container_.addGadgetLifecycleCallback) {
+      container_.addGadgetLifecycleCallback('actions', actionsLifecycleCallback);
+    }
+
+    return /** @scope osapi.container.actions */ {
+      /**
+       * Registers a function to display actions in the container.
+       *
+       * @param {function}
+       *          The container's function to render actions
+       *          in its UI. The function takes the action object as
+       *          a parameter.
+       */
+      registerShowActionsHandler: function(handler) {
+        if (typeof handler === 'function') {
+          showActionHandler = handler;
+        }
+      },
+
+      /**
+       * Registers a function to hide (remove) actions in the container.
+       *
+       * @param {function}
+       *          The container's function to hide (remove) actions
+       *          in its UI. The function takes the action object as
+       *          a parameter.
+       */
+      registerHideActionsHandler: function(handler) {
+        if (typeof handler === 'function') {
+          hideActionHandler = handler;
+        }
+      },
+
+      /**
+       * Registers a function to render gadgets in the container.
+       *
+       * @param {function}
+       *          The container's function to render gadgets in its UI.
+       *          The function takes in two parameters: the gadget spec
+       *          url and optional parameters.
+       */
+      registerNavigateGadgetHandler: function(renderGadgetFunction) {
+        if (typeof renderGadgetFunction === 'function') {
+          renderGadgetInContainer = renderGadgetFunction;
+        }
+      },
+
+      /**
+       * Executes the action associated with the action id.
+       *
+       * @param {String, Object}
+       *          The id of the action to execute..
+       *          The current selection. This is an optional parameter.
+       */
+      runAction: function(actionId, opt_selection) {
+        var action = registry.getItemById(actionId);
+        if (action) {
+          // if gadget site has not been registered yet
+          // the gadget needs to be rendered
+          var gadgetSites = registry.getGadgetSites(actionId);
+          if (!gadgetSites || (gadgetSites.length === 0)) {
+            var gadgetUrl = registry.getUrl(actionId);
+            pendingActions[actionId] = {
+              selection: opt_selection || container_.selection.getSelection()
+            };
+
+            // set optional params
+            var opt_params = {};
+            if (action.view) {
+              opt_params[osapi.container.actions.OptParam.VIEW] = action.view;
+            }
+            if (action.viewTarget) {
+              opt_params[osapi.container.actions.OptParam.VIEW_TARGET] = action.viewTarget;
+            }
+
+            // render the gadget
+            renderGadgetInContainer(gadgetUrl, opt_params);
+          } else {
+            runAction(actionId, opt_selection);
+          }
+        }
+      },
+
+      /**
+       * Gets the action object from the registry based on the action id.
+       *
+       * @param {String}
+       *          id The action id.
+       * @return {Object} The action object.
+       */
+      getAction: function(id) {
+        return registry.getItemById(id);
+      },
+
+      /**
+       * Gets all action objects in the registry.
+       *
+       * @return {Array} An array with any action objects in the
+       *         registry.
+       */
+      getAllActions: function() {
+        return registry.getAllActions();
+      },
+
+      /**
+       * Gets action object from registry based on the path.
+       *
+       * @param {String}
+       *          The path for the action.
+       * @return {Array} An array with any action objects in the
+       *         specified path.
+       */
+      getActionsByPath: function(path) {
+        return registry.getActionsByPath(path);
+      },
+
+      /**
+       * Gets action object from registry based on the dataType.
+       *
+       * @param {String}
+       *          The String representation of the Open Social data type.
+       * @return {Array} An array of action objects bound to the specified
+       *         data type.
+       */
+      getActionsByDataType: function(dataType) {
+        return registry.getActionsByDataType(dataType);
+      },
+
+      /**
+       * Adds a listener to be notified when an action is invoked.
+       *
+       * @param {function(string, Array.<Object>)} listener
+       *          A callback to fire when a matching action is run.
+       * @param {string=} opt_actionId
+       *          An optional action id.  If not provided, listener will be
+       *          notified for all action ids.
+       */
+      addListener: function(listener, opt_actionId) {
+        if (listener && typeof(listener) != 'function') {
+          throw new Error('listener param must be a function');
+        }
+        if (opt_actionId) {
+          (actionListenerMap[opt_actionId] = actionListenerMap[opt_actionId] || []).push(listener);
+        }
+        else {
+          actionListeners.push(listener);
+        }
+      },
+
+      /**
+       * Removes the specified listener.
+       *
+       * @param {function(string, Array.<Object>)} listener
+       *          The listener to remove.
+       */
+      removeListener: function(listener) {
+        var index = listeners.indexOf(listener);
+        if (index != -1) {
+          listeners.splice(index, 1);
+        }
+      }
+    };
+  });
+})();
diff --git a/trunk/features/src/main/javascript/features/actions/constants.js b/trunk/features/src/main/javascript/features/actions/constants.js
new file mode 100644
index 0000000..5e5eb02
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/actions/constants.js
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Constants used throughout the container classes for
+ * actions.
+ */
+
+/**
+ * Actions namespace
+ * @type {Object}
+ */
+osapi.container.actions = {};
+
+/**
+ * Optional params for actions.
+ * @enum {string}
+ */
+osapi.container.actions.OptParam = {};
+osapi.container.actions.OptParam.VIEW = 'view';
+osapi.container.actions.OptParam.VIEW_TARGET = 'viewTarget';
diff --git a/trunk/features/src/main/javascript/features/actions/feature.xml b/trunk/features/src/main/javascript/features/actions/feature.xml
new file mode 100644
index 0000000..1a39351
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/actions/feature.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<feature>
+  <name>actions</name>
+  <dependency>globals</dependency>
+  <dependency>rpc</dependency>
+  <dependency>gadgets.json.ext</dependency>
+  <dependency>xmlutil</dependency>
+  <gadget>
+    <script src="actions.js"/>
+    <script src="taming.js"/>
+    <api>
+      <exports type="js">gadgets.actions.addAction</exports>
+      <exports type="js">gadgets.actions.updateAction</exports>
+      <exports type="js">gadgets.actions.removeAction</exports>
+      <exports type="js">gadgets.actions.runAction</exports>
+      <exports type="js">gadgets.actions.getActionsByPath</exports>
+      <exports type="js">gadgets.actions.getActionsByDataType</exports>
+      <exports type="js">gadgets.actions.registerShowActionsListener</exports>
+      <exports type="js">gadgets.actions.registerHideActionsListener</exports>
+      <exports type="js">osapi.container.actions.OptParam.VIEW</exports>
+      <exports type="js">osapi.container.actions.OptParam.VIEW_TARGET</exports>
+      <exports type="rpc">actions.onActionShow</exports>
+      <exports type="rpc">actions.onActionHide</exports>
+      <exports type="rpc">actions.runAction</exports>
+      <uses type="rpc">actions.registerHideCallback</uses>
+      <uses type="rpc">actions.registerShowCallback</uses>
+      <uses type="rpc">actions.bindAction</uses>
+      <uses type="rpc">actions.get_actions_by_type</uses>
+      <uses type="rpc">actions.get_actions_by_path</uses>
+      <uses type="rpc">actions.runAction</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="constants.js"/>
+    <script src="actions_container.js"/>
+    <api>
+      <exports type="rpc">actions.registerHideCallback</exports>
+      <exports type="rpc">actions.registerShowCallback</exports>
+      <exports type="rpc">actions.bindAction</exports>
+      <exports type="rpc">actions.get_actions_by_type</exports>
+      <exports type="rpc">actions.get_actions_by_path</exports>
+      <exports type="rpc">actions.removeAction</exports>
+      <exports type="rpc">actions.runAction</exports>
+      <uses type="rpc">actions.onActionShow</uses>
+      <uses type="rpc">actions.onActionHide</uses>
+      <uses type="rpc">actions.runAction</uses>
+    </api>
+  </container>
+</feature>
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/actions/taming.js b/trunk/features/src/main/javascript/features/actions/taming.js
new file mode 100644
index 0000000..2afbf68
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/actions/taming.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+var tamings___ = tamings___ || [];
+tamings___.push(function(imports) {
+  ___.grantRead(gadgets.actions, 'addAction');
+  ___.grantRead(gadgets.actions, 'updateAction');
+  ___.grantRead(gadgets.actions, 'removeAction');
+  ___.grantRead(gadgets.actions, 'runAction');
+  ___.grantRead(gadgets.actions, 'getActionsByPath');
+  ___.grantRead(gadgets.actions, 'getActionsByDataType');
+  ___.grantRead(gadgets.actions, 'registerShowActionsListener');
+  ___.grantRead(gadgets.actions, 'registerHideActionsListener');
+});
diff --git a/trunk/features/src/main/javascript/features/auth-refresh/auth-refresh.js b/trunk/features/src/main/javascript/features/auth-refresh/auth-refresh.js
new file mode 100644
index 0000000..f08ce2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/auth-refresh/auth-refresh.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/*global gadgets, shindig */
+
+/**
+ * @fileoverview Allows the container to refresh the gadget security token.
+ */
+gadgets.rpc.register('update_security_token', function(token) {
+  if (this.f !== '..') {
+    return;
+  }
+  shindig.auth.updateSecurityToken(token);
+});
diff --git a/trunk/features/src/main/javascript/features/auth-refresh/feature.xml b/trunk/features/src/main/javascript/features/auth-refresh/feature.xml
new file mode 100644
index 0000000..e0a6206
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/auth-refresh/feature.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>auth-refresh</name>
+  <dependency>shindig.auth</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="auth-refresh.js"/>
+    <api>
+      <exports type="rpc">update_security_token</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/caja/caja-debug.xml b/trunk/features/src/main/javascript/features/caja/caja-debug.xml
new file mode 100644
index 0000000..3745975
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/caja/caja-debug.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+
+The javascript referenced here should be found in the caja jar.
+-->
+<feature>
+  <name>caja-debug</name>
+  <dependency>caja</dependency>
+  <gadget>
+    <!-- placeholder for backward compatibility -->
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/caja/es53-guest-frame.opt.xml b/trunk/features/src/main/javascript/features/caja/es53-guest-frame.opt.xml
new file mode 100644
index 0000000..83ece56
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/caja/es53-guest-frame.opt.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+
+The javascript referenced here should be found in the caja jar.
+-->
+<feature>
+  <name>es53-guest-frame.opt</name>
+  <gadget>
+    <script src="res://com/google/caja/plugin/es53-guest-frame.opt.js"/>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/caja/es53-guest-frame.xml b/trunk/features/src/main/javascript/features/caja/es53-guest-frame.xml
new file mode 100644
index 0000000..5bf3d32
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/caja/es53-guest-frame.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+
+The javascript referenced here should be found in the caja jar.
+-->
+<feature>
+  <name>es53-guest-frame</name>
+  <gadget>
+    <script src="res://com/google/caja/plugin/es53-guest-frame.js"/>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/caja/es53-taming-frame.opt.xml b/trunk/features/src/main/javascript/features/caja/es53-taming-frame.opt.xml
new file mode 100644
index 0000000..00df33e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/caja/es53-taming-frame.opt.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+
+The javascript referenced here should be found in the caja jar.
+-->
+<feature>
+  <name>es53-taming-frame.opt</name>
+  <gadget>
+    <script src="res://com/google/caja/plugin/es53-taming-frame.opt.js"/>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/caja/es53-taming-frame.xml b/trunk/features/src/main/javascript/features/caja/es53-taming-frame.xml
new file mode 100644
index 0000000..462fdbc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/caja/es53-taming-frame.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+
+The javascript referenced here should be found in the caja jar.
+-->
+<feature>
+  <name>es53-taming-frame</name>
+  <gadget>
+    <script src="res://com/google/caja/plugin/es53-taming-frame.js"/>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/caja/feature.xml b/trunk/features/src/main/javascript/features/caja/feature.xml
new file mode 100644
index 0000000..173b18d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/caja/feature.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>caja</name>
+  <dependency>core.io</dependency>
+  <dependency>core.util.onload</dependency>
+  <dependency>taming</dependency>
+  <gadget>
+    <script src="res://com/google/caja/plugin/caja.js"/>
+    <script src="taming.js"/>
+    <api>
+      <exports type="js">caja___.getJSON</exports>
+      <exports type="js">caja___.getTameGlobal</exports>
+      <exports type="js">caja___.getUseless</exports>
+      <exports type="js">caja___.markFunction</exports>
+      <exports type="js">caja___.start</exports>
+      <exports type="js">caja___.tame</exports>
+      <exports type="js">caja___.tamesTo</exports>
+      <exports type="js">caja___.untame</exports>
+      <exports type="js">caja___.whitelistCtors</exports>
+      <exports type="js">caja___.whitelistFuncs</exports>
+      <exports type="js">caja___.whitelistMeths</exports>
+      <exports type="js">caja___.whitelistProps</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/caja/taming.js b/trunk/features/src/main/javascript/features/caja/taming.js
new file mode 100644
index 0000000..02f3806
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/caja/taming.js
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Caja is a whitelisting javascript sanitizing rewriter.
+ * This file tames the APIs that are exposed to a gadget.
+ * Currently limited to one cajoled gadget per ifr.
+ */
+
+caja___ = (function() {
+
+  // Rewrites all uris in a cajoled gadget
+  var uriCallback = {
+    rewrite: function rewrite(uri, mimeTypes) {
+      uri = String(uri);
+      if (/^#/.test(uri)) {
+        // Allow references to anchors within the gadget
+        return '#' + encodeURIComponent(decodeURIComponent(uri.substring(1)));
+      } else if (/^\/[^\/]/.test(uri)) {
+        // Unqualified uris aren't resolved in a useful way in gadgets, so
+        // this isn't a real case, but some of the samples use relative
+        // uris for images, and it looks odd if they don't work cajoled.
+        return gadgets.io.getProxyUrl(
+          location.protocol + '//' + location.host + uri);
+      } else {
+        // Proxy all other dynamically constructed urls
+        return gadgets.io.getProxyUrl(uri);
+      }
+    }
+  };
+
+  function getTameGlobal() {
+    return caja.iframe.contentWindow;
+  }
+
+  function getJSON() {
+    return caja.iframe.contentWindow.JSON;
+  }
+
+  function getUseless() {
+    return caja.USELESS;
+  }
+
+  function markFunction(func, name) {
+    return caja.markFunction(func, name);
+  }
+
+  function tame(obj) {
+    return caja.tame(obj);
+  }
+
+  function tamesTo(feral, tame) {
+    return caja.tamesTo(feral, tame);
+  }
+
+  function untame(obj) {
+    return caja.untame(obj);
+  }
+
+  function whitelistCtors(schemas) {
+    var length = schemas.length;
+    for (var i = 0; i < length; i++) {
+      var schema = schemas[i];
+      if (typeof schema[0][schema[1]] === 'function') {
+        caja.markCtor(
+            schema[0][schema[1]] /* func */,
+            schema[2] /* parent */,
+            schema[1] /* name */);
+      } else {
+        gadgets.warn('Error taming constructor: ' +
+            schema[0] + '.' + schema[1]);
+      }
+    }
+  }
+
+  function whitelistFuncs(schemas) {
+    var length = schemas.length;
+    for (var i = 0; i < length; i++) {
+      var schema = schemas[i];
+      if (typeof schema[0][schema[1]] === 'function') {
+        caja.markFunction(schema[0][schema[1]], schema[1]);
+      } else {
+        gadgets.warn('Error taming function: ' + schema[0] + '.' + schema[1]);
+      }
+    }
+  }
+
+  function whitelistMeths(schemas) {
+    var length = schemas.length;
+    for (var i = 0; i < length; i++) {
+      var schema = schemas[i];
+      if (typeof schema[0].prototype[schema[1]] == 'function') {
+        caja.grantMethod(schema[0].prototype, schema[1]);
+      } else {
+        gadgets.warn('Error taming method: ' + schema[0] + '.' + schema[1]);
+      }
+    }
+  }
+
+  function whitelistProps(schemas) {
+    var length = schemas.length;
+    for (var i = 0; i < length; i++) {
+      var schema = schemas[i];
+      caja.grantRead(schemas[0], schemas[1]);
+    }
+  }
+
+  function start(script, debug) {
+    caja.initialize({
+      server: '/gadgets',
+      resources: '/gadgets/js',
+      // TODO(felix8a): make debug==false work
+      debug: true
+    });
+    var gadgetBody = document.getElementById('caja_innerContainer___');
+    caja.load(gadgetBody, uriCallback, function (frame) {
+      var api = makeApi();
+      frame.api(api).cajoled(void 0, script)
+        .run(function (result) {
+          gadgets.util.runOnLoadHandlers();
+        });
+    });
+  }
+
+  function makeApi() {
+    var api = {};
+    for (var tamer in tamings___) {
+      if (tamings___.hasOwnProperty(tamer)) {
+        tamings___[tamer].call(void 0, api);
+      }
+    }
+    api.gadgets = caja.tame(window.gadgets);
+    api.opensocial = caja.tame(window.opensocial);
+    api.osapi = caja.tame(window.osapi);
+    api.onerror = caja.tame(caja.markFunction(
+        function (msg, source, line) {
+          gadgets.log([msg, source, line]);
+        }));
+    return api;
+  }
+
+  return {
+    getJSON: getJSON,
+    getTameGlobal: getTameGlobal,
+    getUseless: getUseless,
+    markFunction: markFunction,
+    start: start,
+    tame: tame,
+    tamesTo: tamesTo,
+    untame: untame,
+    whitelistCtors: whitelistCtors,
+    whitelistFuncs: whitelistFuncs,
+    whitelistMeths: whitelistMeths,
+    whitelistProps: whitelistProps
+  };
+})();
+
diff --git a/trunk/features/src/main/javascript/features/cloo/cloo.js b/trunk/features/src/main/javascript/features/cloo/cloo.js
new file mode 100644
index 0000000..65eba3e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/cloo/cloo.js
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Get a cloo!
+ *
+ * Simple utils for CLosure-style Object Orientation.
+ * JavaScript inheritance via prototype is a little awkward and too permissive.
+ * Method overrides are actually method overwrites, and importantly there's
+ * no such thing as private state: all properties attached to "this" are public
+ * and accessible.
+ *
+ * With this library, object-oriented JS APIs are implemented as function
+ * closures with private internal variables. Exported APIs are returned from
+ * these as Objects themselves.
+ *
+ * To define a class, simply define a Function that returns an Object whose
+ * keys define the class's external API, with that Object wrapped as:
+ * cloo.obj(opt_classname, super1, super2, ..., exported);
+ *
+ * To define an interface/abstract method in your exports, use cloo.interfc():
+ * cloo.obj({ foo: cloo.interfc() });
+ *
+ * Exported symbols that begin with an underbar are *not* inherited.
+ *
+ * Consequences of this mechanism are:
+ * + "True" private variables and functions are supported, by declaring them
+ *   in the class's Function body without exporting them.
+ * + Quasi-"protected" methods are supported by defining a base class exporting
+ *   methods whose name begins with an underbar.
+ * + A form of multiple inheritance is implicitly supported, by providing
+ *   multiple superclass objects to the cloo.obj() function. Override precedence
+ *   is that last-superclass-wins. In practice, this should only be used with
+ *   superclasses that don't have state, ie. comprise only of interface methods.
+ *   This is not programmatically enforced by the library to keep code lean.
+ * + Base-class state must be accessed via getters and setters, which may be
+ *   "protected" as described above.
+ *
+ * Meaningless example exhibiting all features:
+ * var MyBase = (function(config, params) {
+ *   var self = cloo.me();
+ *   var handler = function() { };
+ *   var cfg = config;
+ *   var color = params["color"] || "red";
+ *
+ *   function setColor(newColor) {
+ *     color = newColor;
+ *   }
+ *
+ *   function callHandler() {
+ *     handler(self());
+ *   }
+ *
+ *   return cloo.obj({
+ *     setColor: setColor,
+ *     _getColor: function() { return color; },
+ *     getHeight: cloo.interfc()  // Makes MyBase implicitly abstract
+ *   });
+ * });
+ *
+ * var MyClass = (function(config, params) {
+ *   var super = MyBase(config, params);
+ *   var height = params["height"] || 34;
+ *
+ *   function getHeight() {
+ *     // Blue objects are always 123 in height.
+ *     return super._getColor() === "blue" ? 123 : height;
+ *   }
+ *
+ *   return cloo.obj(super, {
+ *     getHeight: getHeight
+ *   });
+ * }
+ *
+ * var myClassInstance = MyClass({}, { height: 12, color: "red"});
+ */
+
+var cloo = (function() {
+  var UNKNOWN_NAME = '(n/a)';
+  var selfs = [];
+
+  function InterfaceMethod(className, methodName) {
+    return function() {
+      throw 'Class ' + className + ' missing ' + methodName + '()';
+    }
+  }
+
+  var INTERFACE_PLACEHOLDER = InterfaceMethod(UNKNOWN_NAME, UNKNOWN_NAME);
+
+  function interfaceCreator() {
+    return INTERFACE_PLACEHOLDER;
+  }
+
+  function hasOwnFunctionProperty(obj, key) {
+    return obj.hasOwnProperty(key) && typeof obj[key] === 'function';
+  }
+
+  function objectCreator() {
+    var args = arguments;
+    var className = UNKNOWN_NAME;
+    var ix = 0;
+    if (typeof args[0] === 'string') {
+      className = args[0];
+      ix = 1;
+    }
+
+    // Create return Object.
+    var out = {};
+
+    for (; ix < (args.length - 1); ++ix) {
+      var parent = args[ix];
+      // Copy keys over from parent that aren't intended
+      // to be "protected" ie starting with an underbar.
+      for (var key in parent) {
+        if (hasOwnFunctionProperty(parent, key) && !/^_/.test(key)) {
+          out[key] = parent[key];
+        }
+      }
+    }
+
+    // Then override with new exports, replacing
+    // interface placeholders with properly-named versions.
+    var exports = args[ix];
+    for (var key in exports) {
+      if (hasOwnFunctionProperty(exports, key)) {
+        if (exports[key] === INTERFACE_PLACEHOLDER) {
+          exports[key] = InterfaceMethod(className, key);
+        }
+        out[key] = exports[key];
+      }
+    }
+
+    var lastIx = selfs.length - 1;
+    if (lastIx >= 0 && !selfs[lastIx]) {
+      selfs[lastIx] = out;
+    }
+
+    return out;
+  }
+
+  function selfStorage() {
+    var ix = selfs.length;
+    if (ix && !selfs[ix - 1]) {
+      throw 'me() must be followed by obj()';
+    }
+    selfs.push(null);
+    return function() {
+      var obj = selfs[ix];
+      if (!obj) {
+        throw 'me() access before obj creation';
+      }
+      return obj;
+    };
+  }
+
+  return {
+    interfc: interfaceCreator,
+    obj: objectCreator,
+    me: selfStorage
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/cloo/feature.xml b/trunk/features/src/main/javascript/features/cloo/feature.xml
new file mode 100644
index 0000000..578f5f5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/cloo/feature.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>cloo</name>
+  <all>
+    <script src="cloo.js"/>
+    <api>
+      <exports type="js">cloo.interfc</exports>
+      <exports type="js">cloo.obj</exports>
+      <exports type="js">cloo.me</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/container.site.gadget/feature.xml b/trunk/features/src/main/javascript/features/container.site.gadget/feature.xml
new file mode 100644
index 0000000..d427316
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site.gadget/feature.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>container.site.gadget</name>
+  <dependency>osapi</dependency>
+  <dependency>globals</dependency>
+  <dependency>core.log</dependency>
+  <dependency>shindig.auth</dependency>
+  <dependency>shindig.uri.ext</dependency>
+  <dependency>core.util</dependency>
+  <dependency>rpc</dependency>
+  <dependency>container.util</dependency>
+  <dependency>container.site</dependency>
+  <container>
+    <script src="gadget_holder.js" />
+    <script src="gadget_site.js" />
+    <api>
+      <!-- inherited -->
+      <exports type="js">osapi.container.GadgetSite</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.onConstructed</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.close</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.setParentId</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.getParentId</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.getActiveSiteHolder</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.getId</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.render</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.setHeight</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.setWidth</exports>
+
+      <exports type="js">osapi.container.GadgetSite.prototype.rpcCall</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.getFeature</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.getModuleId</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.navigateTo</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.onNavigateTo</exports>
+      <exports type="js">osapi.container.GadgetSite.prototype.onRender</exports>
+
+      <!-- inherited -->
+      <exports type="js">osapi.container.GadgetHolder</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.onConstructed</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.createIframeAttributeMap</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.createIframeHtml</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.dispose</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.getElement</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.getIframeElement</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.getIframeId</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.getUrl</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.render</exports>
+
+      <exports type="js">osapi.container.GadgetHolder.prototype.getGadgetInfo</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.getView</exports>
+      <exports type="js">osapi.container.GadgetHolder.prototype.setSecurityToken</exports>
+    </api>
+  </container>
+</feature>
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container.site.gadget/gadget_holder.js b/trunk/features/src/main/javascript/features/container.site.gadget/gadget_holder.js
new file mode 100644
index 0000000..094d9fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site.gadget/gadget_holder.js
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview This represents an HTML element and the associated gadget.
+ */
+
+
+/**
+ * @param {osapi.container.GadgetSite} site The site containing this holder.
+ * @param {Element} el The element to render gadgets in.
+ * @param {string} onLoad The name of an onLoad function to call in window scope
+ *          to assign as the onload handler of this holder's iframe.
+ * @constructor
+ * @extends {osapi.container.SiteHolder}
+ */
+osapi.container.GadgetHolder = function(site, el, onLoad) {
+  osapi.container.SiteHolder.call(this, site, el, onLoad); // call super
+  var undef;
+
+  /**
+   * JSON metadata for gadget
+   * @type {Object}
+   * @private
+   */
+  this.gadgetInfo_ = undef;
+
+  /**
+   * View parameters to pass to gadget.
+   * @type {Object}
+   * @private
+   */
+  this.viewParams_ = undef;
+
+  /**
+   * A dynamically set social/security token.
+   * Social tokens are sent with original view URLs but may need
+   * to be refreshed for long lived gadgets.
+   * @type {string}
+   * @private
+   */
+  this.securityToken_ = undef;
+
+  this.onConstructed();
+};
+osapi.container.GadgetHolder.prototype = new osapi.container.SiteHolder;
+
+/**
+ * Url points to the rpc_relay.html which allows cross-domain communication between
+ *     a gadget and container
+ * @type {string}
+ * @private
+ */
+osapi.container.GadgetHolder.prototype.relayPath_ = null;
+
+/**
+ * @return {Object} The metadata of gadget.
+ */
+osapi.container.GadgetHolder.prototype.getGadgetInfo = function() {
+  return this.gadgetInfo_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+osapi.container.GadgetHolder.prototype.dispose = function() {
+  osapi.container.SiteHolder.prototype.dispose.call(this); // super.dispose();
+  this.gadgetInfo_ = null;
+};
+
+
+/**
+ * @inheritDoc
+ */
+osapi.container.GadgetHolder.prototype.getUrl = function() {
+  return this.gadgetInfo_ && this.gadgetInfo_['url'];
+};
+
+/**
+ * @return {string} The view of current gadget. This is the view that was actually rendered once
+ *         view aliases were applied.
+ * @see osapi.container.GadgetSite.prototype.render
+ */
+osapi.container.GadgetHolder.prototype.getView = function() {
+  return this.renderParams_[osapi.container.RenderParam.VIEW];
+};
+
+
+/**
+ * @inheritDoc
+ * @see osapi.container.GadgetHolder.prototype.doOaaIframeHtml_ and org.openajax.hub-2.0.7/iframe.js:createIframe()
+ */
+osapi.container.GadgetHolder.prototype.getIframeElement = function() {
+  return this.el_.getElementsByTagName('iframe')[0];
+};
+
+
+/**
+ * @param {string} value The value to set this social/security token to.
+ * @return {osapi.container.GadgetHolder} the current GadgetHolder.
+ */
+osapi.container.GadgetHolder.prototype.setSecurityToken = function(value) {
+  this.securityToken_ = value;
+  return this;
+};
+
+
+/**
+ * Render a gadget into the element.
+ *
+ * @override
+ * @param {Object} gadgetInfo the JSON gadget description.
+ * @param {Object} viewParams Look at osapi.container.ViewParam.
+ * @param {Object} renderParams Look at osapi.container.RenderParam.
+ */
+osapi.container.GadgetHolder.prototype.render = function(gadgetInfo, viewParams, renderParams) {
+  this.iframeId_ = osapi.container.GadgetHolder.IFRAME_ID_PREFIX_ +
+      this.site_.getId();
+  this.gadgetInfo_ = gadgetInfo;
+  this.viewParams_ = viewParams;
+  this.renderParams_ = renderParams;
+
+  if (this.hasFeature_(gadgetInfo, 'pubsub-2')) {
+    this.doOaaIframeHtml_();
+  } else {
+    this.doNormalIframeHtml_();
+  }
+};
+
+
+// -----------------------------------------------------------------------------
+// Private variables and methods.
+// -----------------------------------------------------------------------------
+
+
+/**
+ * Prefix for gadget HTML IDs/names.
+ * @type {string}
+ * @private
+ */
+osapi.container.GadgetHolder.IFRAME_ID_PREFIX_ = '__gadget_';
+
+/**
+ * @private
+ */
+osapi.container.GadgetHolder.prototype.doNormalIframeHtml_ = function() {
+  var uri = this.getIframeUrl_();
+  this.el_.innerHTML = this.createIframeHtml(uri, {title:this.site_.getTitle()});
+
+  // Set up RPC channel.
+  var iframeUri = shindig.uri(uri);
+  var relayUri = shindig.uri()
+      .setSchema(iframeUri.getSchema())
+      .setAuthority(iframeUri.getAuthority())
+      .setPath(this.relayPath_);
+  gadgets.rpc.setupReceiver(this.iframeId_, relayUri.toString(),
+      iframeUri.getFP('rpctoken'));
+};
+
+
+/**
+ * @private
+ */
+osapi.container.GadgetHolder.prototype.doOaaIframeHtml_ = function() {
+  //Remove any prior container for the iframe id from the OpenAjax hub prior to registering the new one
+  this.removeOaaContainer_(this.iframeId_);
+  var self = this;
+  new OpenAjax.hub.IframeContainer(
+      gadgets.pubsub2router.hub,
+      this.iframeId_,
+      {
+        Container: {
+          onSecurityAlert: function(source, alertType) {
+            gadgets.error(['Security error for container ',
+                source.getClientID(), ' : ', alertType].join(''));
+            source.getIframe().src = 'about:blank';
+          },
+          onConnect: function(container) {
+            gadgets.log(['connected: ', container.getClientID()].join(''));
+          }
+        },
+        IframeContainer: {
+          parent: this.el_,
+          uri: this.getIframeUrl_(),
+          //tunnelURI: shindig.uri('/test1/gadgets/' + '../container/rpc_relay.html')
+          //   .resolve(shindig.uri(window.location.href)),
+          tunnelURI: shindig.uri(this.relayPath_).resolve(shindig.uri(window.location.href)),
+          iframeAttrs: this.createIframeAttributeMap(this.getIframeUrl_(), {title:this.site_.getTitle()}),
+          onGadgetLoad: function() {
+            if(self.onLoad_) {
+              window[self.onLoad_](self.getUrl(), self.site_.getId());
+            }
+          }
+        }
+      }
+  );
+};
+
+/**
+ * Removes the specified container from the registered pubsub2router hub
+ *
+ * @param {String} containerId the id of the container to remove from the hub
+ * @private
+ */
+osapi.container.GadgetHolder.prototype.removeOaaContainer_ = function(containerId) {
+    var container = gadgets.pubsub2router.hub.getContainer(containerId);
+    //Null is returned from the getContainer function per the OpenAjax spec if the container is not found
+    if(container) {
+        gadgets.pubsub2router.hub.removeContainer(container);
+    }
+};
+
+
+/**
+ * @param {Object} gadgetInfo the JSON gadget description.
+ * @param {string} feature the feature to look for.
+ * @private
+ * @return {boolean} true if feature is set.
+ */
+osapi.container.GadgetHolder.prototype.hasFeature_ = function(gadgetInfo, feature) {
+  var modulePrefs = gadgetInfo[osapi.container.MetadataResponse.MODULE_PREFS];
+  if (modulePrefs) {
+    var features = modulePrefs[osapi.container.MetadataResponse.FEATURES];
+    if (features && features[feature]) {
+      return true;
+    }
+  }
+  return false;
+};
+
+/**
+ * Get the rendering iframe URL.
+ * @private
+ * @return {string} the rendering iframe URL.
+ */
+osapi.container.GadgetHolder.prototype.getIframeUrl_ = function() {
+  var uri = shindig.uri(this.gadgetInfo_[osapi.container.MetadataResponse.IFRAME_URLS][this.getView()]);
+  uri.setQP('debug', this.renderParams_[osapi.container.RenderParam.DEBUG] ? '1' : '0');
+  uri.setQP('nocache', this.renderParams_[osapi.container.RenderParam.NO_CACHE] ? '1' : '0');
+  uri.setQP('testmode', this.renderParams_[osapi.container.RenderParam.TEST_MODE] ? '1' : '0');
+  uri.setQP('view', this.getView());
+  if (this.renderParams_[osapi.container.RenderParam.CAJOLE]) {
+    var libs = uri.getQP('libs');
+    if (libs == null || libs == '') uri.setQP('libs', 'caja');
+    else uri.setQP('libs', [libs, ':caja'].join(''));
+    uri.setQP('caja', '1');
+  }
+  this.updateUserPrefParams_(uri);
+
+  // TODO: Share this base container logic
+  // TODO: Two SD base URIs - one for container, one for gadgets
+  // Need to add parent at end of query due to gadgets parsing bug
+  uri.setQP('parent', window.__CONTAINER_URI.getOrigin());
+
+  // Remove existing social token if we have a new one
+  if (this.securityToken_) {
+    uri.setExistingP('st', this.securityToken_);
+  }
+
+  // Uniquely identify possibly-same gadgets on a page.
+  uri.setQP('mid', String(this.site_.getModuleId()));
+
+  if (!osapi.container.util.isEmptyJson(this.viewParams_)) {
+    var gadgetParamText = gadgets.json.stringify(this.viewParams_);
+    uri.setFP('view-params', gadgetParamText);
+  }
+
+  // add rpctoken fragment to support flash transport if not in the uri
+  if(typeof(uri.getFP('rpctoken')) === 'undefined' ) {
+    var rpcToken = (0x7FFFFFFF * Math.random()) | 0;
+    uri.setFP('rpctoken', rpcToken);
+  }
+  var lang = this.site_.service_.getLanguage();
+  var country = this.site_.service_.getCountry();
+  var templateLang = uri.getQP('lang'), templateCountry = uri.getQP('country');
+  if(templateLang.indexOf('%') != -1){
+    uri.setQP('lang', lang);
+  }
+  if(templateCountry.indexOf('%') != -1){
+    uri.setQP('country', country);
+  }
+  return uri.toString();
+};
+
+
+/**
+ * Replace user prefs specified in url with only those specified. This will
+ * maintain each user prefs existence (or lack of), order (from left to right)
+ * and its appearance (in query params or fragment).
+ * @param {shindig.uri} uri The URL possibly containing user preferences
+ *     parameters prefixed by up_.
+ * @private
+ */
+osapi.container.GadgetHolder.prototype.updateUserPrefParams_ = function(uri) {
+  var userPrefs = this.renderParams_[osapi.container.RenderParam.USER_PREFS];
+  if (userPrefs) {
+    for (var up in userPrefs) {
+      var upKey = 'up_' + up;
+      var upValue = userPrefs[up];
+      if (upValue instanceof Array) {
+        upValue = upValue.join('|');
+      }
+      uri.setExistingP(upKey, upValue);
+    }
+  }
+};
+
+
+// We do run this in the container mode in the new common container
+if (gadgets.config) {
+  gadgets.config.register('container', null, function (config) {
+    if (config['container']) {
+      var rpath = config['container']['relayPath'];
+      osapi.container.GadgetHolder.prototype.relayPath_ = rpath;
+    }
+  });
+}
diff --git a/trunk/features/src/main/javascript/features/container.site.gadget/gadget_site.js b/trunk/features/src/main/javascript/features/container.site.gadget/gadget_site.js
new file mode 100644
index 0000000..b4d26d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site.gadget/gadget_site.js
@@ -0,0 +1,444 @@
+/*

+ * Licensed to the Apache Software Foundation (ASF) under one

+ * or more contributor license agreements. See the NOTICE file

+ * distributed with this work for additional information

+ * regarding copyright ownership. The ASF licenses this file

+ * to you 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.

+ */

+

+/**

+ * @fileoverview This manages rendering of gadgets in a place holder, within an

+ * HTML element in the container. The API for this is low-level. Use the

+ * container APIs to work with gadget sites.

+ */

+

+/**

+ * @param {osapi.container.Container} container The container that hosts this gadget site.

+ * @param {osapi.container.Service} service The container's service.

+ * @param {Object} args containing:

+ *          {osapi.container.Service} service to fetch gadgets metadata, token.

+ *          {string} navigateCallback name of callback function on navigateTo().

+ *          {Element} gadgetEl Element into which to render the gadget.

+ *          {Element} bufferEl Optional element for double buffering.

+ * @constructor

+ * @extends {osapi.container.Site}

+ */

+osapi.container.GadgetSite = function(container, service, args) {

+  var undef;

+

+  osapi.container.Site.call(this, container, service, args['gadgetEl']); // call super

+

+  /**

+   * @type {string}

+   * @private

+   */

+  this.navigateCallback_ = args['navigateCallback'];

+

+  /**

+   * @type {Element}

+   * @private

+   */

+  this.loadingGadgetEl_ = args['bufferEl'];

+

+  /**

+   * @type {string}

+   * @private

+   */

+  this.gadgetOnLoad_ = args['gadgetOnLoad'];

+

+  /**

+   * Unique numeric module ID for this gadget instance.  A module id is used to

+   * identify persisted instances of gadgets.

+   *

+   * @type {number}

+   * @private

+   */

+  this.moduleId_ = 0;

+

+  /**

+   * Information about the currently visible gadget.

+   * @type {osapi.container.GadgetHolder?}

+   * @private

+   */

+  this.currentGadgetHolder_ = undef;

+

+  /**

+   * Information about the currently loading gadget.

+   * @type {osapi.container.GadgetHolder?}

+   * @private

+   */

+  this.loadingGadgetHolder_ = undef;

+

+  this.onConstructed();

+};

+

+osapi.container.GadgetSite.prototype = new osapi.container.Site;

+

+/**

+ * @return {undefined|null|number} The numerical moduleId of this gadget, if

+ *   set.  May return null or undefined if not set.

+ */

+osapi.container.GadgetSite.prototype.getModuleId = function() {

+  return this.moduleId_;

+};

+

+/**

+ * If you want to change the moduleId after a gadget has rendered, re-navigate the site.

+ *

+ * @param {string} url This gadget's url (may not yet be accessible in all cases from the holder).

+ * @param {number} mid The numerical moduleId for this gadget to use.

+ * @param {function} opt_callback Optional callback to run when the moduleId is set.

+ * @private

+ */

+osapi.container.GadgetSite.prototype.setModuleId_ = function(url, mid, opt_callback) {

+  if (mid && this.moduleId_ != mid) {

+    var self = this,

+        url = osapi.container.util.buildTokenRequestUrl(url, mid);

+

+    if (!self.service_.getCachedGadgetToken(url)) {

+      // We need to request a security token for this gadget instance.

+      var request = osapi.container.util.newTokenRequest([url]);

+      self.service_.getGadgetToken(request, function(response) {

+        var ttl, mid;

+        if (response && response[url]) {

+          if (ttl = response[url][osapi.container.TokenResponse.TOKEN_TTL]) {

+            self.container_.scheduleRefreshTokens_(ttl);

+          }

+          var mid = response[url][osapi.container.TokenResponse.MODULE_ID];

+          if (mid || mid == 0) {

+            self.moduleId_ = mid;

+          }

+        }

+        if (opt_callback) {

+          opt_callback();

+        }

+      });

+      return;

+    }

+  }

+  if (opt_callback) {

+    opt_callback();

+  }

+};

+

+/**

+ * @inheritDoc

+ */

+osapi.container.GadgetSite.prototype.getActiveSiteHolder = function() {

+  return this.loadingGadgetHolder_ || this.currentGadgetHolder_;

+};

+

+/**

+ * @inheritDoc

+ */

+osapi.container.GadgetSite.prototype.setTitle = function(title) {

+  osapi.container.Site.prototype.setTitle.call(this, title);

+  // sometimes there are 2 holders

+  if (this.loadingGadgetHolder_ && this.currentGadgetHolder_) {

+    // loadingGadgetHolder_ was set by super call

+    this.currentGadgetHolder_.setTitle(title); // set my other one.

+  }

+  return this;

+};

+

+/**

+ * Returns configuration of a feature with a given name. Defaults to current

+ * loading or visible gadget if no metadata is passed in.

+ * @param {string} name Name of the feature.

+ * @param {Object=} opt_gadgetInfo Optional gadget info.

+ * @return {Object} JSON representing the feature.

+ */

+osapi.container.GadgetSite.prototype.getFeature = function(name, opt_gadgetInfo) {

+  var gadgetInfo = opt_gadgetInfo || this.getActiveSiteHolder().getGadgetInfo();

+  return gadgetInfo[osapi.container.MetadataResponse.FEATURES] &&

+      gadgetInfo[osapi.container.MetadataResponse.FEATURES][name];

+};

+

+/**

+ * Render a gadget in the site, by URI of the gadget XML.

+ * @param {string} gadgetUrl The absolute URL to gadget.

+ * @param {Object} viewParams Look at osapi.container.ViewParam.

+ * @param {Object} renderParams Look at osapi.container.RenderParam.

+ * @param {function(Object)=} opt_callback Function called with gadget info

+ *     after navigation has occurred.

+ */

+osapi.container.GadgetSite.prototype.navigateTo = function(

+    gadgetUrl, viewParams, renderParams, opt_callback) {

+  var start = osapi.container.util.getCurrentTimeMs();

+  var cached = this.service_.getCachedGadgetMetadata(gadgetUrl);

+  var callback = opt_callback || function() {};

+  var request = osapi.container.util.newMetadataRequest([gadgetUrl]);

+  var self = this;

+

+  this.service_.getGadgetMetadata(request, function(response) {

+    var xrt = (!cached) ? (osapi.container.util.getCurrentTimeMs() - start) : 0;

+    var gadgetInfo = response[gadgetUrl];

+    if (gadgetInfo.error) {

+      var message = ['Failed to navigate for gadget ', gadgetUrl, '.'].join('');

+      gadgets.warn(message);

+

+      message = ['Detailed error: ', gadgetInfo.error.code || '', ' ', gadgetInfo.error.message || ''].join('');

+      gadgets.log(message);

+    } else {

+      var moduleId = renderParams[osapi.container.RenderParam.MODULE_ID] || 0;

+      self.setModuleId_(gadgetUrl, moduleId, function() {

+        self.container_.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_RENDER,

+                gadgetInfo);

+        self.render(gadgetInfo, viewParams, renderParams);

+      });

+    }

+

+    // Return metadata server response time.

+    var timingInfo = {};

+    timingInfo[osapi.container.NavigateTiming.URL] = gadgetUrl;

+    timingInfo[osapi.container.NavigateTiming.ID] = self.id_;

+    timingInfo[osapi.container.NavigateTiming.START] = start;

+    timingInfo[osapi.container.NavigateTiming.XRT] = xrt;

+    self.onNavigateTo(timingInfo);

+

+    // Possibly with an error. Leave to user to deal with raw response.

+    callback(gadgetInfo);

+  });

+};

+

+

+/**

+ * Provide overridable callback invoked when navigateTo is completed.

+ * @param {Object} data the statistic/timing information to return.

+ */

+osapi.container.GadgetSite.prototype.onNavigateTo = function(data) {

+  if (this.navigateCallback_) {

+    var func = window[this.navigateCallback_];

+    if (typeof func === 'function') {

+      func(data);

+    }

+  }

+};

+

+

+/**

+ * Render a gadget in this site, using a JSON gadget description.

+ *

+ * Note: A view provided in either renderParams or viewParams is subject to aliasing if the gadget

+ * does not support the view specified.

+ *

+ * @param {Object} gadgetInfo the JSON gadget description.

+ * @param {Object} viewParams Look at osapi.container.ViewParam.

+ * @param {Object} renderParams Look at osapi.container.RenderParam.

+ * @override

+ */

+osapi.container.GadgetSite.prototype.render = function(

+    gadgetInfo, viewParams, renderParams) {

+  var curUrl = this.currentGadgetHolder_ ? this.currentGadgetHolder_.getUrl() : null;

+

+  var previousView = null;

+  if (curUrl == gadgetInfo['url']) {

+    previousView = this.currentGadgetHolder_.getView();

+  }

+

+  // Simple function to find a suitable alias

+  var findAliasInfo = function(viewConf) {

+    if (typeof viewConf !== 'undefined' && viewConf != null) {

+      var aliases = viewConf['aliases'] || [];

+      for (var i = 0; i < aliases.length; i++) {

+        if (gadgetInfo[osapi.container.MetadataResponse.VIEWS][aliases[i]]) {

+          return {'view':aliases[i],

+                  'viewInfo':gadgetInfo[osapi.container.MetadataResponse.VIEWS][aliases[i]]};

+        }

+      }

+    }

+    return null;

+  };

+

+  // Find requested view.

+  var view = renderParams[osapi.container.RenderParam.VIEW] ||

+      viewParams[osapi.container.ViewParam.VIEW] ||

+      previousView;

+  var viewInfo = gadgetInfo[osapi.container.MetadataResponse.VIEWS][view];

+  if (view && !viewInfo) {

+    var aliasInfo = findAliasInfo(gadgets.config.get('views')[view]);

+    if (aliasInfo) {

+      view = aliasInfo['view'];

+      viewInfo = aliasInfo['viewInfo'];

+    }

+  }

+

+  // Allow default view if requested view is not found.  No sense doing this if the view is already "default".

+  if (!viewInfo &&

+          renderParams[osapi.container.RenderParam.ALLOW_DEFAULT_VIEW]  &&

+          view != osapi.container.GadgetSite.DEFAULT_VIEW_) {

+    view = osapi.container.GadgetSite.DEFAULT_VIEW_;

+    viewInfo = gadgetInfo[osapi.container.MetadataResponse.VIEWS][view];

+    if (!viewInfo) {

+      var aliasInfo = findAliasInfo(gadgets.config.get('views')[view]);

+      if (aliasInfo) {

+        view = aliasInfo['view'];

+        viewInfo = aliasInfo['viewInfo'];

+      }

+    }

+  }

+

+  // Check if view exists.

+  if (!viewInfo) {

+    gadgets.warn(['Unsupported view ', view, ' for gadget ', gadgetInfo['url'], '.'].join(''));

+    return;

+  }

+

+  // Set the loading gadget holder:

+  // 1. If the gadget site already has currentGadgetHolder_ set and no loading element passed,

+  //    simply set the current gadget holder as the loading gadget holder.

+  // 2. Else, check if caller pass the loading gadget element. If it does then use it to create new

+  //    instance of osapi.container.GadgetHolder as the loading gadget holder.

+  if (this.currentGadgetHolder_ && !this.loadingGadgetEl_) {

+    this.loadingGadgetHolder_ = this.currentGadgetHolder_;

+    this.currentGadgetHolder_ = null;

+  }

+  else {

+    // Check if we are passed the loading gadget element

+    var el = this.loadingGadgetEl_ || this.el_;

+    this.loadingGadgetHolder_ = new osapi.container.GadgetHolder(this, el, this.gadgetOnLoad_);

+  }

+

+  var localRenderParams = {};

+  for (var key in renderParams) {

+    localRenderParams[key] = renderParams[key];

+  }

+

+  localRenderParams[osapi.container.RenderParam.VIEW] = view;

+  localRenderParams[osapi.container.RenderParam.HEIGHT] =

+      renderParams[osapi.container.RenderParam.HEIGHT] ||

+      viewInfo[osapi.container.MetadataResponse.PREFERRED_HEIGHT] ||

+      gadgetInfo[osapi.container.MetadataResponse.MODULE_PREFS][osapi.container.MetadataResponse.HEIGHT] ||

+      String(osapi.container.GadgetSite.DEFAULT_HEIGHT_);

+  localRenderParams[osapi.container.RenderParam.WIDTH] =

+      renderParams[osapi.container.RenderParam.WIDTH] ||

+      viewInfo[osapi.container.MetadataResponse.PREFERRED_WIDTH] ||

+      gadgetInfo[osapi.container.MetadataResponse.MODULE_PREFS][osapi.container.MetadataResponse.WIDTH] ||

+      String(osapi.container.GadgetSite.DEFAULT_WIDTH_);

+

+  this.updateSecurityToken_(gadgetInfo, localRenderParams);

+

+  this.loadingGadgetHolder_.render(gadgetInfo, viewParams, localRenderParams);

+};

+

+

+/**

+ * Called when a gadget loads in the site. Uses double buffer, if present.

+ */

+osapi.container.GadgetSite.prototype.onRender = function() {

+  this.swapBuffers_();

+

+  if (this.currentGadgetHolder_) {

+    this.currentGadgetHolder_.dispose();

+  }

+

+  this.currentGadgetHolder_ = this.loadingGadgetHolder_;

+  this.loadingGadgetHolder_ = null;

+};

+

+

+/**

+ * Sends RPC call to the current/visible gadget.

+ * @param {string} serviceName RPC service name to call.

+ * @param {function(Object)} callback Function to call upon RPC completion.

+ * @param {...number} var_args payload to pass to the recipient.

+ */

+osapi.container.GadgetSite.prototype.rpcCall = function(

+    serviceName, callback, var_args) {

+  if (this.currentGadgetHolder_) {

+    gadgets.rpc.call(this.currentGadgetHolder_.getIframeId(),

+        serviceName, callback, var_args);

+  }

+};

+

+

+/**

+ * If token has been fetched at least once, set the token to the most recent

+ * one. Otherwise, leave it.

+ * @param {Object} gadgetInfo The gadgetInfo used to update security token.

+ * @param {Object} renderParams Look at osapi.container.RenderParam.

+ * @private

+ */

+osapi.container.GadgetSite.prototype.updateSecurityToken_ = function(gadgetInfo, renderParams) {

+  var url = osapi.container.util.buildTokenRequestUrl(gadgetInfo['url'], this.moduleId_),

+    tokenInfo = this.service_.getCachedGadgetToken(url);

+

+  if (tokenInfo) {

+    var token = tokenInfo[osapi.container.TokenResponse.TOKEN];

+    this.loadingGadgetHolder_.setSecurityToken(token);

+  }

+};

+

+/**

+ * @inheritDoc

+ */

+osapi.container.GadgetSite.prototype.close = function() {

+  if (this.loadingGadgetHolder_) {

+    this.loadingGadgetHolder_.dispose();

+  }

+  if (this.currentGadgetHolder_) {

+    this.currentGadgetHolder_.dispose();

+  }

+};

+

+/**

+ * Swap the double buffer elements, if there is a double buffer.

+ * @private

+ */

+osapi.container.GadgetSite.prototype.swapBuffers_ = function() {

+  // Only process double buffering if loading gadget exists

+  if (this.loadingGadgetEl_) {

+    this.loadingGadgetEl_.style.left = '';

+    this.loadingGadgetEl_.style.position = '';

+    this.el_.style.position = 'absolute';

+    this.el_.style.left = '-2000px';

+

+    // Swap references;  cur_ will now again be what's visible

+    var oldCur = this.el_;

+    this.el_ = this.loadingGadgetEl_;

+    this.loadingGadgetEl_ = oldCur;

+  }

+};

+

+

+/**

+ * Key to identify the calling gadget site.

+ * @type {string}

+ */

+osapi.container.GadgetSite.RPC_ARG_KEY = 'gs';

+

+

+/**

+ * Default height of gadget. Refer to --

+ * http://code.google.com/apis/gadgets/docs/legacy/reference.html.

+ * @type {number}

+ * @private

+ */

+osapi.container.GadgetSite.DEFAULT_HEIGHT_ = 200;

+

+

+/**

+ * Default width of gadget. Refer to --

+ * http://code.google.com/apis/gadgets/docs/legacy/reference.html.

+ * @type {number}

+ * @private

+ */

+osapi.container.GadgetSite.DEFAULT_WIDTH_ = 320;

+

+

+/**

+ * Default view of gadget.

+ * @type {string}

+ * @private

+ */

+osapi.container.GadgetSite.DEFAULT_VIEW_ = 'default';

diff --git a/trunk/features/src/main/javascript/features/container.site.url/feature.xml b/trunk/features/src/main/javascript/features/container.site.url/feature.xml
new file mode 100644
index 0000000..fcdeb3c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site.url/feature.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>container.site.url</name>
+  <dependency>osapi</dependency>
+  <dependency>container.util</dependency>
+  <dependency>container.site</dependency>
+  <container>
+    <script src="url_holder.js" />
+    <script src="url_site.js" />
+    <api>
+      <!-- inherited -->
+      <exports type="js">osapi.container.UrlSite</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.onConstructed</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.close</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.setParentId</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.getParentId</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.getActiveSiteHolder</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.getId</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.render</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.setHeight</exports>
+      <exports type="js">osapi.container.UrlSite.prototype.setWidth</exports>
+
+      <!-- inherited -->
+      <exports type="js">osapi.container.UrlHolder</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.onConstructed</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.createIframeAttributeMap</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.createIframeHtml</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.dispose</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.getElement</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.getIframeElement</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.getIframeId</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.getUrl</exports>
+      <exports type="js">osapi.container.UrlHolder.prototype.render</exports>
+    </api>
+  </container>
+</feature>
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container.site.url/url_holder.js b/trunk/features/src/main/javascript/features/container.site.url/url_holder.js
new file mode 100644
index 0000000..f91ec63
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site.url/url_holder.js
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Constructs a new URL holder. This class is similar in
+ * functionality to GadgetHolder from the common container.
+ */
+
+/**
+ * @param {osapi.container.UrlSite} site The site containing this holder.
+ * @param {Element} el The element to render gadgets in.
+ * @param {string} onLoad The name of an onLoad function to call in window scope
+ *          to assign as the onload handler of this holder's iframe.
+ * @constructor
+ * @extends {osapi.container.SiteHolder}
+ */
+osapi.container.UrlHolder = function(site, el, onLoad) {
+  osapi.container.SiteHolder.call(this, site, el, onLoad); // call super
+  var undef;
+
+  /**
+   * @type {string}
+   * @private
+   */
+  this.url_ = undef;
+
+  this.onConstructed();
+};
+osapi.container.UrlHolder.prototype = new osapi.container.SiteHolder;
+
+/**
+ * @inheridDoc
+ */
+osapi.container.UrlHolder.prototype.dispose = function() {
+  osapi.container.SiteHolder.prototype.dispose.call(this); // super.dispose();
+  this.url_ = null;
+};
+
+/**
+ * @inheritDoc
+ */
+osapi.container.UrlHolder.prototype.getIframeElement = function() {
+  return this.el_.firstChild;
+};
+
+/**
+ * @inheritDoc
+ */
+osapi.container.UrlHolder.prototype.getUrl = function() {
+  return this.url_;
+};
+
+/**
+ * Renders the URL.
+ *
+ * @override
+ * @param {string} url the URL to render.
+ * @param {object} renderParams params to apply to the iFrame.
+ */
+osapi.container.UrlHolder.prototype.render = function(url, renderParams) {
+  this.iframeId_ = osapi.container.UrlHolder.IFRAME_PREFIX_ + this.site_.getId();
+  this.renderParams_ = renderParams;
+  this.el_.innerHTML = this.createIframeHtml(this.url_ = url, {scrolling: 'auto',title: this.site_.getTitle()});
+};
+
+/**
+ * Prefix for iFrame ids.
+ * @private
+ */
+osapi.container.UrlHolder.IFRAME_PREFIX_ = '__url_';
diff --git a/trunk/features/src/main/javascript/features/container.site.url/url_site.js b/trunk/features/src/main/javascript/features/container.site.url/url_site.js
new file mode 100644
index 0000000..f38e4d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site.url/url_site.js
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Constructs a new URL site.  This class is very similar in functionality to
+ * the GadgetSite class which is part of the common container.
+ */
+
+/**
+ * @param {osapi.container.Container} container The container that hosts this gadget site.
+ * @param {osapi.container.Service} service The container's service.
+ * @param {Object} args containing DOM element to rende the iFrame in, and URL
+ *                 to render in the iFrame.
+ * @constructor
+ * @extends {osapi.container.Site}
+ */
+osapi.container.UrlSite = function(container, service, args) {
+  var undef;
+
+  osapi.container.Site.call(this, container, service,
+    args[osapi.container.UrlSite.URL_ELEMENT]
+  ); // call super
+
+  /**
+   * @type {osapi.container.UrlHolder}
+   * @private
+   */
+  this.holder_ = undef;
+
+  /**
+   * @type {string}
+   * @private
+   */
+  this.url_ = undef;
+
+  this.onConstructed();
+};
+osapi.container.UrlSite.prototype = new osapi.container.Site;
+
+/**
+ * @inheritDoc
+ */
+osapi.container.UrlSite.prototype.getActiveSiteHolder = function() {
+  return this.holder_;
+};
+
+/**
+ * Renders the URL in this site
+ * @param {string} url to render in the iFrame.
+ * @param {object} renderParams the parameters to render the site.
+ * @override
+ */
+osapi.container.UrlSite.prototype.render = function(url, renderParams) {
+  this.holder_ = new osapi.container.UrlHolder(this, this.el_);
+
+  var localRenderParams = {};
+  for (var key in renderParams) {
+    localRenderParams[key] = renderParams[key];
+  }
+
+  this.holder_.render(url, localRenderParams);
+};
+
+/**
+ * The URL element key
+ * @const
+ * @type {string}
+ */
+osapi.container.UrlSite.URL_ELEMENT = 'urlEl';
diff --git a/trunk/features/src/main/javascript/features/container.site/feature.xml b/trunk/features/src/main/javascript/features/container.site/feature.xml
new file mode 100644
index 0000000..77121e2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site/feature.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>container.site</name>
+  <!--dependency>container.site.gadget</dependency-->
+  <container>
+    <script src="site.js" />
+    <script src="site_holder.js" />
+    <api>
+      <exports type="js">osapi.container.Site</exports>
+      <exports type="js">osapi.container.Site.prototype.onConstructed</exports>
+      <exports type="js">osapi.container.Site.prototype.close</exports>
+      <exports type="js">osapi.container.Site.prototype.setParentId</exports>
+      <exports type="js">osapi.container.Site.prototype.getParentId</exports>
+      <exports type="js">osapi.container.Site.prototype.getActiveSiteHolder</exports>
+      <exports type="js">osapi.container.Site.prototype.getId</exports>
+      <exports type="js">osapi.container.Site.prototype.render</exports>
+      <exports type="js">osapi.container.Site.prototype.setHeight</exports>
+      <exports type="js">osapi.container.Site.prototype.setWidth</exports>
+
+      <exports type="js">osapi.container.SiteHolder</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.onConstructed</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.createIframeAttributeMap</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.createIframeHtml</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.dispose</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.getElement</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.getIframeElement</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.getIframeId</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.getUrl</exports>
+      <exports type="js">osapi.container.SiteHolder.prototype.render</exports>
+    </api>
+  </container>
+</feature>
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container.site/site.js b/trunk/features/src/main/javascript/features/container.site/site.js
new file mode 100644
index 0000000..0cd4250
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site/site.js
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Abstract base-class for gadget sites and url sites.
+ */
+
+/**
+ * @param {osapi.container.Container} container The container that hosts this gadget site.
+ * @param {osapi.container.Service} service The container's service.
+ * @param {Element} element The main element for this site.
+ */
+osapi.container.Site = function(container, service, element, args) {
+  var undef;
+  /**
+   * @type {osapi.container.Container}
+   * @protected
+   */
+  this.container_ = container;
+
+  /**
+   * @type {osapi.container.Service}
+   * @protected
+   */
+  this.service_ = service;
+
+  /**
+   * @type {Element}
+   * @protected
+   */
+  this.el_ = element;
+
+  /**
+   * Unique ID of this site.  Uses the ID of the site element, if set, or an
+   * auto-generated number.
+   *
+   * @type {string}
+   * @protected
+   */
+  this.id_ = (this.el_ && this.el_.id) ? this.el_.id :
+    osapi.container.Site.prototype.nextUniqueSiteId_++;
+
+  /**
+   * @type {number?}
+   * @protected
+   */
+  this.parentId_ = undef;
+
+  /**
+   * Used primarily by open-views feature.
+   *
+   * @type {string?} ownerId_ The rpc targetId of the gadget that requested
+   *   this site's creation.
+   * @protected
+   */
+  this.ownerId_ = undef;
+
+  /**
+   * Site title which would show up as iframe title.
+   * @type {string}
+   * @private
+   */
+  this.title_ = undef;
+};
+
+/**
+ * Default site title.
+ * @type {string}
+ * @private
+ */
+osapi.container.Site.DEFAULT_TITLE = 'default title';
+
+/**
+ * Unique counter for sites.  Used if no explicit ID was provided in their creation.
+ * @type {number}
+ */
+osapi.container.Site.prototype.nextUniqueSiteId_ = 0;
+
+// Public impl
+
+/**
+ * Callback that occurs after instantiation/construction of any site.
+ * Override on Site to provide your specific functionalities for all sites.
+ * Override on any subclass of Site to provide your specific functionalities
+ * for that subclass of sites. Overriding subclass onConstructed will not fire
+ * generic Site onConstructed unless you do so manually.
+ */
+osapi.container.Site.prototype.onConstructed = function() {};
+
+/**
+ * @return {string} The ID of this site.
+ */
+osapi.container.Site.prototype.getId = function() {
+  return this.id_;
+};
+
+/**
+ * Set the width of the site's iframe.
+ *
+ * @param {number} value The new width.
+ * @return {osapi.container.Site} this
+ */
+osapi.container.Site.prototype.setWidth = function(value) {
+  var holder = this.getActiveSiteHolder();
+  if (holder) {
+    var iframeEl = holder.getIframeElement();
+    if (iframeEl) {
+      iframeEl.style.width = value + 'px';
+    }
+  }
+  return this;
+};
+
+/**
+ * Set the height of the site's iframe.
+ *
+ * @param {number} value The new height.
+ * @return {osapi.container.Site} this.
+ */
+osapi.container.Site.prototype.setHeight = function(value) {
+  var holder = this.getActiveSiteHolder();
+  if (holder) {
+    var iframeEl = holder.getIframeElement();
+    if (iframeEl) {
+      iframeEl.style.height = value + 'px';
+    }
+  }
+  return this;
+};
+
+/**
+ * Set the title of the site's iframe.
+ *
+ * @param {String} title The site title.
+ * @return {osapi.container.Site} this.
+ */
+osapi.container.Site.prototype.setTitle = function(title) {
+  this.title_ = title;
+  var siteHolder = this.getActiveSiteHolder();
+  if (siteHolder) {
+    siteHolder.setTitle(title);
+  }
+  return this;
+};
+
+/**
+ * Get the site title.
+ *
+ * @return {String} the site title.
+ */
+osapi.container.Site.prototype.getTitle = function() {
+  if (typeof(this.title_) !== 'undefined') {
+    return this.title_;
+  } else {
+    var siteHolder = this.getActiveSiteHolder();
+    if (siteHolder && siteHolder.gadgetInfo_) {
+      var gadgetInfo = siteHolder.gadgetInfo_;
+      if (gadgetInfo && gadgetInfo.modulePrefs && gadgetInfo.modulePrefs.title) {
+        return gadgetInfo.modulePrefs.title;
+      }
+    }
+    return osapi.container.Site.DEFAULT_TITLE;
+  }
+};
+
+/**
+ * Closes this site.
+ */
+osapi.container.Site.prototype.close = function() {
+  var holder = this.getActiveSiteHolder();
+  holder && holder.dispose();
+};
+
+/**
+ * @return {string?} The id of parent DOM element containing this site, if
+ *   previously set by osapi.container.Site.prototype.setParentId.
+ */
+osapi.container.Site.prototype.getParentId = function() {
+  return this.parentId_;
+};
+
+/**
+ * Sets the id of the parent DOM element containing this site.
+ *
+ * @param {number} parentId the id of the parent DOM element.
+ * @return {osapi.container.Site} this.
+ */
+osapi.container.Site.prototype.setParentId = function(parentId) {
+  this.parentId_ = parentId;
+  return this;
+};
+
+// Abstract methods
+
+/**
+ * Gets the active site holder for this site.
+ * @abstract
+ * @return {osapi.container.SiteHolder} the holder for this site.
+ */
+osapi.container.Site.prototype.getActiveSiteHolder = function() {
+  throw new Error("This method must be implemented by a subclass.");
+};
+
+/**
+ * Renders this site.
+ * @abstract
+ */
+osapi.container.Site.prototype.render = function() {
+  throw new Error("This method must be implemented by a subclass.");
+};
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container.site/site_holder.js b/trunk/features/src/main/javascript/features/container.site/site_holder.js
new file mode 100644
index 0000000..bf653bd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.site/site_holder.js
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Abstract base-class for site holders.
+ */
+
+/**
+ * @param {osapi.container.Site} site The site containing this holder.
+ * @param {Element} el The element that this holder manages.
+ * @param {string} onLoad The name of an onLoad function to call in window scope
+ *          to assign as the onload handler of this holder's iframe.
+ *
+ * @constructor
+ */
+osapi.container.SiteHolder = function(site, el, onLoad) {
+  var undef;
+
+  /**
+   * The site containing this holder.
+   * @type {osapi.container.Site}
+   * @protected
+   */
+  this.site_ = site;
+
+  /**
+   * The element that this holder manages.
+   * @type {Element}
+   * @protected
+   */
+  this.el_ = el;
+
+  /**
+   * On load function for gadget iFrames in window scope
+   * @type {string}
+   * @protected
+   */
+  this.onLoad_ = onLoad;
+
+  /**
+   * Id of the iframe contained within this holder.
+   * @type {string}
+   * @protected
+   */
+  this.iframeId_ = undef;
+
+  /**
+   * @type {object}
+   * @protected
+   */
+  this.renderParams_ = undef;
+
+  this.onConstructed();
+};
+
+/**
+ * Callback that occurs after instantiation/construction of any SiteHolder.
+ * Override on SiteHolder to provide your specific functionalities for all sites.
+ * Override on any subclass of SiteHolder to provide your specific functionalities
+ * for that subclass of SiteHolder. Overriding subclass onConstructed will not fire
+ * generic SiteHolder onConstructed unless you do so manually.
+ */
+osapi.container.SiteHolder.prototype.onConstructed = function() {};
+
+/**
+ * @return {Element} The holder's HTML element.
+ */
+osapi.container.SiteHolder.prototype.getElement = function() {
+  return this.el_;
+};
+
+/**
+ * Gets the id of the iframe contained within this holder.
+ * @return {string} the id of the iframe contained within this holder.
+ */
+osapi.container.SiteHolder.prototype.getIframeId = function() {
+  return this.iframeId_;
+};
+
+/**
+ * Disposes the gadget holder and performs cleanup of any holder state.
+ */
+osapi.container.SiteHolder.prototype.dispose = function() {
+  if (this.el_ && this.el_.firstChild) {
+    this.el_.removeChild(this.el_.firstChild);
+  }
+};
+
+/**
+ * Creates the iframe element source for this holder's iframe element.
+ * @param {string} url The src url for the iframe.
+ * @param {Object.<string, string>=} overrides A bag of iframe attribute overrides.
+ * @return {string} The new iframe element source.
+ * @protected
+ */
+osapi.container.SiteHolder.prototype.createIframeHtml = function(url, overrides) {
+	var undef,
+	    map = this.createIframeAttributeMap(url, overrides);
+	map['onload'] = this.onLoad_ ?
+	        ('window.' + this.onLoad_ + "('" + this.getUrl() + "', '" + this.site_.getId() + "');") : undef;
+   return osapi.container.util.createIframeHtml(map);
+};
+
+/**
+ * Creates the iframe element source for this holder's iframe element.
+ * @param {string} url The src url for the iframe.
+ * @param {Object=} overrides A bag of iframe attribute overrides.
+ * @return {string} The new iframe element source.
+ * @protected
+ */
+osapi.container.SiteHolder.prototype.createIframeAttributeMap = function(url, overrides) {
+  var renderParams = this.renderParams_ || {},
+      params = {
+        id: this.iframeId_,
+        name: this.iframeId_,
+        src: url,
+        scrolling: 'no',
+        marginwidth: 0,
+        marginheight: 0,
+        frameborder: 0,
+        vspace: 0,
+        hspace: 0,
+        'class': renderParams[osapi.container.RenderParam.CLASS],
+        height: renderParams[osapi.container.RenderParam.HEIGHT],
+        width: renderParams[osapi.container.RenderParam.WIDTH]
+      };
+   if (overrides) {
+     for(var i in overrides) {
+       params[i] = overrides[i];
+     }
+   }
+   return params;
+};
+
+/**
+ * Set a title to the site.
+ * @private
+ */
+osapi.container.SiteHolder.prototype.setTitle = function(title) {
+  var ifr = this.getIframeElement();
+  if (ifr) {
+    ifr.title = title;
+  }
+};
+
+//Abstract methods
+
+/**
+ * Gets the iFrame element itself.
+ * @abstract
+ * @return {Element} The iframe element in this holder.
+ */
+osapi.container.SiteHolder.prototype.getIframeElement = function() {
+  throw new Error("This method must be implemented by a subclass.");
+};
+
+/**
+ * Gets the iFrame element itself.
+ * @abstract
+ * @return {Element} The iframe element in this holder.
+ */
+osapi.container.SiteHolder.prototype.render = function() {
+  throw new Error("This method must be implemented by a subclass.");
+};
+
+/**
+ * @abstract
+ * @return {string} The URL associated with the holder.
+ */
+osapi.container.SiteHolder.prototype.getUrl = function() {
+  throw new Error("This method must be implemented by a subclass.");
+};
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container.util/constant.js b/trunk/features/src/main/javascript/features/container.util/constant.js
new file mode 100644
index 0000000..91d16f2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.util/constant.js
@@ -0,0 +1,372 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Constants used throughout common container.
+ */
+
+
+/**
+ * Set up namespace.
+ * @type {Object}
+ */
+osapi.container = {};
+
+
+/**
+ * Constants to key into gadget metadata state.
+ * @const
+ * @enum {string}
+ */
+osapi.container.MetadataParam = {
+    LOCAL_EXPIRE_TIME: 'localExpireTimeMs',
+    URL: 'url'
+};
+
+
+/**
+ * Constants to key into gadget metadata response JSON.
+ * @enum {string}
+ */
+
+osapi.container.MetadataResponse = {
+  IFRAME_URLS: 'iframeUrls',
+  NEEDS_TOKEN_REFRESH: 'needsTokenRefresh',
+  VIEWS: 'views',
+  EXPIRE_TIME_MS: 'expireTimeMs',
+  FEATURES: 'features',
+  HEIGHT: 'height',
+  MODULE_PREFS: 'modulePrefs',
+  PREFERRED_HEIGHT: 'preferredHeight',
+  PREFERRED_WIDTH: 'preferredWidth',
+  RESPONSE_TIME_MS: 'responseTimeMs',
+  WIDTH: 'width',
+  TOKEN_TTL: 'tokenTTL'
+};
+
+
+/**
+ * Constants to key into gadget token response JSON.
+ * @enum {string}
+ */
+osapi.container.TokenResponse = {
+  TOKEN: 'token',
+  TOKEN_TTL: 'tokenTTL',
+  MODULE_ID: 'moduleId'
+};
+
+
+/**
+ * Constants to key into timing response JSON.
+ * @enum {string}
+ */
+osapi.container.NavigateTiming = {
+  /** The gadget URL reporting this timing information. */
+  URL: 'url',
+  /** The gadget site ID reporting this timing information. */
+  ID: 'id',
+  /** Absolute time (ms) when gadget navigation is requested. */
+  START: 'start',
+  /** Time (ms) to receive XHR response time. In CC, for metadata and token. */
+  XRT: 'xrt',
+  /** Time (ms) to receive first byte. Typically timed at start of page. */
+  SRT: 'srt',
+  /** Time (ms) to load the DOM. Typically timed at end of page. */
+  DL: 'dl',
+  /** Time (ms) when body onload is called. */
+  OL: 'ol',
+  /** Time (ms) when page is ready for use. Typically happen after data XHR (ex:
+   * calendar, email) is received/presented to users. Overridable by user.
+   */
+  PRT: 'prt'
+};
+
+
+/**
+ * Constants to key into request renderParam JSON.
+ * @enum {string}
+ * @const
+ */
+osapi.container.RenderParam = {
+    /** Allow gadgets to render in unspecified view. */
+    ALLOW_DEFAULT_VIEW: 'allowDefaultView',
+
+    /** Whether to enable cajole mode. */
+    CAJOLE: 'cajole',
+
+    /** Style class to associate to iframe. */
+    CLASS: 'class',
+
+    /** Whether to enable debugging mode. */
+    DEBUG: 'debug',
+
+    /** The starting gadget iframe height (in pixels). */
+    HEIGHT: 'height',
+
+    /** Whether to disable cache. */
+    NO_CACHE: 'nocache',
+
+    /** Whether to enable test mode. */
+    TEST_MODE: 'testmode',
+
+    /** The gadget user prefs to render with. */
+    USER_PREFS: 'userPrefs',
+
+    /** The view of gadget to render. */
+    VIEW: 'view',
+
+    /** The starting gadget iframe width (in pixels). */
+    WIDTH: 'width',
+
+    /**
+     * The modduleId of this gadget.  Used to identify saved instances of gadgets.
+     * Defaults to 0, which means the instance of the gadget is not saved.
+     */
+    MODULE_ID: 'moduleid'
+};
+
+/**
+ * Constants to key into request viewParam JSON.
+ * @enum {string}
+ */
+osapi.container.ViewParam = {
+  VIEW: 'view'
+};
+
+/**
+ * Constants to define lifecycle callback
+ * @enum {string}
+ */
+osapi.container.CallbackType = {
+    /** Called before a gadget(s) is preloaded. */
+    ON_BEFORE_PRELOAD: 'onBeforePreload',
+
+    /** Called after a gadget(s) has finished preloading. */
+    ON_PRELOADED: 'onPreloaded',
+
+    /** Called before navigate is called. */
+    ON_BEFORE_NAVIGATE: 'onBeforeNavigate',
+
+    /** Called after navigation has completed. */
+    ON_NAVIGATED: 'onNavigated',
+
+    /** Called before a gadget is closed. */
+    ON_BEFORE_CLOSE: 'onBeforeClose',
+
+    /** Called after a gadget has been closed. */
+    ON_CLOSED: 'onClosed',
+
+    /** Called before a gadget has been unloaded. */
+    ON_BEFORE_UNLOAD: 'onBeforeUnload',
+
+    /** Called after a gadget has been unloaded. */
+    ON_UNLOADED: 'onUnloaded',
+
+    /** Called before render is called. */
+    ON_BEFORE_RENDER: 'onBeforeRender',
+
+    /** Called after a gadget has rendered. */
+    ON_RENDER: 'onRender',
+
+    /** Name of the global function all gadgets will call when they are loaded. */
+    GADGET_ON_LOAD: '__gadgetOnLoad'
+};
+
+/**
+ * Enumeration of configuration keys for a osapi.container.Container. This is specified in
+ * JSON to provide extensible configuration. These enum values are for
+ * documentation purposes only, it is expected that clients use the string
+ * values.
+ * @enum {string}
+ */
+osapi.container.ContainerConfig = {
+  /**
+   * Allow gadgets to render in unspecified view.
+   * @type {string}
+   * @const
+   */
+  ALLOW_DEFAULT_VIEW: 'allowDefaultView',
+
+  /**
+   * Whether cajole mode is turned on.
+   * @type {string}
+   * @const
+   */
+  RENDER_CAJOLE: 'renderCajole',
+
+  /**
+   * Whether debug mode is turned on.
+   * @type {string}
+   * @const
+   */
+  RENDER_DEBUG: 'renderDebug',
+
+  /**
+   * The debug param name to look for in container URL for per-request debugging.
+   * @type {string}
+   * @const
+   */
+  RENDER_DEBUG_PARAM: 'renderDebugParam',
+
+  /**
+   * Whether test mode is turned on.
+   * @type {string}
+   * @const
+   */
+  RENDER_TEST: 'renderTest',
+
+  /**
+   * Security token refresh interval (in ms). Set to 0 in config to disable
+   * token refresh.
+   *
+   * This number should always be >= 0. The smallest encountered token ttl or this
+   * number will be used as the refresh interval, whichever is smaller.
+   *
+   * @type {string}
+   * @const
+   */
+  TOKEN_REFRESH_INTERVAL: 'tokenRefreshInterval',
+
+  /**
+   * Globally-defined callback function upon gadget navigation. Useful to
+   * broadcast timing and stat information back to container.
+   * @type {string}
+   * @const
+   */
+  NAVIGATE_CALLBACK: 'navigateCallback',
+
+  /**
+   * Provide server reference time for preloaded data.
+   * This time is used instead of each response time in order to support server
+   * caching of results.
+   * @type {number}
+   * @const
+   */
+  PRELOAD_REF_TIME: 'preloadRefTime',
+
+  /**
+   * Preloaded hash of gadgets metadata
+   * @type {Object}
+   * @const
+   */
+  PRELOAD_METADATAS: 'preloadMetadatas',
+
+  /**
+   * Preloaded hash of gadgets tokens
+   * @type {Object}
+   * @const
+   */
+  PRELOAD_TOKENS: 'preloadTokens',
+
+  /**
+   * Used to query the language locale part of the container page.
+   * @type {function}
+   */
+  GET_LANGUAGE: 'GET_LANGUAGE',
+
+  /**
+   * Used to query the country locale part of the container page.
+   * @type {function}
+   */
+  GET_COUNTRY: 'GET_COUNTRY',
+
+  /**
+   * Used to retrieve the persisted preferences for a gadget.
+   * @type {function}
+   */
+  GET_PREFERENCES: 'GET_PREFERENCES',
+
+  /**
+   * Used to persist preferences for a gadget.
+   * @type {function}
+   */
+  SET_PREFERENCES: 'SET_PREFERENCES',
+
+  /**
+   * Used to arbitrate RPC calls.
+   * @type {function}
+   */
+  RPC_ARBITRATOR: 'rpcArbitrator',
+
+  /**
+   * Used to retrieve security tokens for gadgets.
+   * @type {function}
+   */
+  GET_GADGET_TOKEN: 'GET_GADGET_TOKEN',
+
+  /**
+   * Used to retrieve a security token for the container.
+   * Containers who specify this config value can call
+   * CommonContainer.updateContainerSecurityToken after the creation of the
+   * common container to start the scheduling of container token refreshes.
+   *
+   * @type {function(function)=}
+   * @param {function(String, number)} callback The function to call to report
+   *   the updated token and the token's new time-to-live in seconds. This
+   *   callback function must be called with unspecified values in the event of
+   *   an error.
+   *
+   *   The first and second arguments to this callback function are the same as
+   *   the second and third arguments to:
+   *     osapi.container.Container.prototype.updateContainerSecurityToken
+   *   Example:
+   *   <code>
+   *     var config = {};
+   *     config[osapi.container.ContainerConfig.GET_CONTAINER_TOKEN] = function(result) {
+   *       var token, ttl, error = false;
+   *       // Do work to set token and ttl values
+   *       if (error) {
+   *         var undef;
+   *         if (error.isFatal()) {
+   *           // Run all callbacks and let them know there was a horrible error.
+   *           // The container token is not valid, and probably won't be any time soon.
+   *           result(undef, 30, 'There was an error!');  // Try again for a miracle in 30 seconds.
+   *         } else {
+   *           result(undef, 15); // Call me again in 15 seconds, please
+   *         }
+   *       } else {
+   *         result(token, ttl);
+   *       }
+   *     };
+   *   </code>
+   * @see osapi.container.Container.prototype.updateContainerSecurityToken
+   */
+  GET_CONTAINER_TOKEN: 'GET_CONTAINER_TOKEN'
+};
+
+/**
+ * Enumeration of configuration keys for a osapi.container.Service. This is specified in
+ * JSON to provide extensible configuration.
+ * @enum {string}
+ */
+osapi.container.ServiceConfig = {
+  /**
+   * Host to fetch gadget information, via XHR.
+   * @type {string}
+   * @const
+   */
+  API_HOST: 'apiHost',
+
+  /**
+   * Path to fetch gadget information, via XHR.
+   * @type {string}
+   * @const
+   */
+  API_PATH: 'apiPath'
+}
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container.util/feature.xml b/trunk/features/src/main/javascript/features/container.util/feature.xml
new file mode 100644
index 0000000..0344f4d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.util/feature.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>container.util</name>
+  <dependency>globals</dependency>
+  <dependency>core.log</dependency>
+  <dependency>shindig.auth</dependency>
+  <dependency>shindig.uri.ext</dependency>
+  <dependency>core.util</dependency>
+  <dependency>osapi</dependency>
+  <dependency>rpc</dependency>
+  <container>
+    <script src="constant.js"/>
+    <script src="util.js"/>
+    <api>
+      <exports type="js">osapi.container.RenderParam.ALLOW_DEFAULT_VIEW</exports>
+      <exports type="js">osapi.container.RenderParam.CAJOLE</exports>
+      <exports type="js">osapi.container.RenderParam.CLASS</exports>
+      <exports type="js">osapi.container.RenderParam.DEBUG</exports>
+      <exports type="js">osapi.container.RenderParam.HEIGHT</exports>
+      <exports type="js">osapi.container.RenderParam.NO_CACHE</exports>
+      <exports type="js">osapi.container.RenderParam.TEST_MODE</exports>
+      <exports type="js">osapi.container.RenderParam.USER_PREFS</exports>
+      <exports type="js">osapi.container.RenderParam.VIEW</exports>
+      <exports type="js">osapi.container.RenderParam.WIDTH</exports>
+      <exports type="js">osapi.container.RenderParam.MODULE_ID</exports>
+
+      <exports type="js">osapi.container.ViewParam.VIEW</exports>
+
+      <exports type="js">osapi.container.ContainerConfig.ALLOW_DEFAULT_VIEW</exports>
+      <exports type="js">osapi.container.ContainerConfig.RENDER_CAJOLE</exports>
+      <exports type="js">osapi.container.ContainerConfig.RENDER_DEBUG</exports>
+      <exports type="js">osapi.container.ContainerConfig.RENDER_DEBUG_PARAM</exports>
+      <exports type="js">osapi.container.ContainerConfig.RENDER_TEST</exports>
+      <exports type="js">osapi.container.ContainerConfig.TOKEN_REFRESH_INTERVAL</exports>
+      <exports type="js">osapi.container.ContainerConfig.NAVIGATE_CALLBACK</exports>
+      <exports type="js">osapi.container.ContainerConfig.PRELOAD_REF_TIME</exports>
+      <exports type="js">osapi.container.ContainerConfig.PRELOAD_METADATAS</exports>
+      <exports type="js">osapi.container.ContainerConfig.PRELOAD_TOKENS</exports>
+      <exports type="js">osapi.container.ContainerConfig.GET_LANGUAGE</exports>
+      <exports type="js">osapi.container.ContainerConfig.GET_COUNTRY</exports>
+      <exports type="js">osapi.container.ContainerConfig.GET_PREFERENCES</exports>
+      <exports type="js">osapi.container.ContainerConfig.SET_PREFERENCES</exports>
+      <exports type="js">osapi.container.ContainerConfig.RPC_ARBITRATOR</exports>
+      <exports type="js">osapi.container.ContainerConfig.GET_GADGET_TOKEN</exports>
+      <exports type="js">osapi.container.ContainerConfig.GET_CONTAINER_TOKEN</exports>
+
+      <exports type="js">osapi.container.ServiceConfig.API_HOST</exports>
+      <exports type="js">osapi.container.ServiceConfig.API_PATH</exports>
+    </api>
+  </container>
+</feature>
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container.util/util.js b/trunk/features/src/main/javascript/features/container.util/util.js
new file mode 100644
index 0000000..dc6625e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container.util/util.js
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Utility methods for common container.
+ */
+
+
+/**
+ * @type {Object}
+ */
+osapi.container.util = {};
+
+
+/**
+ * @param {Object} json The JSON to look up key param from.
+ * @param {string} key Key in config.
+ * @param {*=} defaultValue The default value to return.
+ * @return {*} value of json at key, if valid. Otherwise, return defaultValue.
+ */
+osapi.container.util.getSafeJsonValue = function(json, key, defaultValue) {
+  return (typeof(json[key]) != 'undefined' && json[key] != null) ?
+      json[key] : defaultValue;
+};
+
+
+/**
+ * Merge two JSON together. Keys in json2 will replace than in json1.
+ * @param {Object} json1 JSON to start merge with.
+ * @param {Object} json2 JSON to append/replace json1.
+ * @return {Object} the resulting JSON.
+ */
+osapi.container.util.mergeJsons = function(json1, json2) {
+  var result = {};
+  for (var key in json1) {
+    result[key] = json1[key];
+  }
+  for (var key in json2) {
+    result[key] = json2[key];
+  }
+  return result;
+};
+
+
+/**
+ * Construct a JSON request to get gadget metadata. For now, this will request
+ * a super-set of data needed for all CC APIs requiring gadget metadata, since
+ * the caching of response is not additive.
+ * @param {Array} gadgetUrls An array of gadget URLs.
+ * @return {Object} the resulting JSON.
+ */
+osapi.container.util.newMetadataRequest = function(gadgetUrls) {
+  if (!osapi.container.util.isArray(gadgetUrls)) {
+    gadgetUrls = [gadgetUrls];
+  }
+  return {
+    'container': window.__CONTAINER,
+    'ids': gadgetUrls,
+    'fields': [
+      'iframeUrls',
+      'modulePrefs.*',
+      'needsTokenRefresh',
+      'userPrefs.*',
+      'views.preferredHeight',
+      'views.preferredWidth',
+      'expireTimeMs',
+      'responseTimeMs',
+      'rpcServiceIds',
+      'tokenTTL'
+    ]
+  };
+};
+
+
+/**
+ * Construct a JSON request to get gadget token.
+ * @param {Array} gadgetUrls A list of gadget URLs.
+ * @return {Object} the resulting JSON.
+ */
+osapi.container.util.newTokenRequest = function(gadgetUrls) {
+  if (!osapi.container.util.isArray(gadgetUrls)) {
+    gadgetUrls = [gadgetUrls];
+  }
+  return {
+    'container': window.__CONTAINER,
+    'ids': gadgetUrls,
+    'fields': [
+      'token',
+      'tokenTTL',
+      'moduleId'
+    ]
+  };
+};
+
+
+/**
+ * Extract keys from a JSON to an array.
+ * @param {Object} json to extract keys from.
+ * @return {Array.<string>} keys in the json.
+ */
+osapi.container.util.toArrayOfJsonKeys = function(json) {
+  var result = [];
+  for (var key in json) {
+    result.push(key);
+  }
+  return result;
+};
+
+
+/**
+ * Tests an object to see if it is an array or not.
+ * @param {object} obj Object to test.
+ * @return {boolean} If obj is an array.
+ */
+osapi.container.util.isArray = function(obj) {
+  return Object.prototype.toString.call(obj) == '[object Array]';
+};
+
+
+/**
+ * @param {Object} json to check.
+ * @return {Boolean} true if json is empty.
+ */
+osapi.container.util.isEmptyJson = function(json) {
+  for (var key in json) {
+    return false;
+  }
+  return true;
+};
+
+/**
+ * @return {number} current time in ms.
+ */
+osapi.container.util.getCurrentTimeMs = function() {
+  return new Date().getTime();
+};
+
+/**
+ * Creates the HTML for the iFrame
+ * @param {Object.<string,string>} iframeParams iframe Params.
+ * @return {string} the HTML for the iFrame.
+ */
+osapi.container.util.createIframeHtml = function(iframeParams) {
+
+  // Do not use DOM API (createElement(), setAttribute()), since it is slower,
+  // requires more code, and creating an element with it results in a click
+  // sound in IE (unconfirmed), setAttribute('class') may need browser-specific
+  // variants.
+  var out = [], n = 0;
+  out[n++] = '<iframe ';
+  for (var key in iframeParams) {
+      var value = iframeParams[key];
+      if (typeof(value) != 'undefined') {
+          out[n++] = key;
+          out[n++] = '="';
+          out[n++] = value;
+          out[n++] = '" ';
+      }
+  }
+  out[n++] = '></iframe>';
+
+  return out.join('');
+};
+
+/**
+ * Constructs a url for token refresh given a gadgetUrl and moduleId.
+ *
+ * @param {string} url The gadget's url
+ * @param {number} moduleId A moduleId.
+ * @return {string} A url to use in a TokenRequest for a security token.
+ */
+osapi.container.util.buildTokenRequestUrl = function(url, moduleId) {
+  url = shindig.uri(url);
+  if (moduleId) {
+    url.setFP('moduleId', moduleId);
+  }
+  return url.toString();
+};
diff --git a/trunk/features/src/main/javascript/features/container/container.js b/trunk/features/src/main/javascript/features/container/container.js
new file mode 100644
index 0000000..8f8e203
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container/container.js
@@ -0,0 +1,949 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview This represents the container for the current window or create
+ * the container if none already exists.
+ */
+
+
+/**
+ * @param {Object=} opt_config Configuration JSON.
+ * @constructor
+ */
+osapi.container.Container = function(opt_config) {
+  var config = this.config_ = opt_config || {};
+
+  /**
+   * A list of objects containing functions to be invoked when gadgets are
+   * preloaded, navigated, closed or unloaded. Sample object:
+   *
+   * var callback = new Object();
+   * callback[osapi.container.CallbackType.ON_PRELOADED]
+   *            = function(response){};
+   * callback[osapi.container.CallbackType.ON_CLOSED]
+   *            = function(gadgetSite){};
+   * callback[osapi.container.CallbackType.ON_NAVIGATED]
+   *            = function(gadgetSite){};
+   * callback[osapi.container.CallbackType.ON_UNLOADED]
+   *            = function(gadgetURL){};
+   * @type {Array}
+   * @private
+   */
+  this.gadgetLifecycleCallbacks_ = {};
+
+  /**
+   * A JSON list of preloaded gadget URLs.
+   * @type {Object}
+   * @private
+   */
+  this.preloadedGadgetUrls_ = {};
+
+  /**
+   * @type {Object}
+   * @private
+   */
+  this.sites_ = {};
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.allowDefaultView_ = Boolean(
+      osapi.container.util.getSafeJsonValue(config,
+      osapi.container.ContainerConfig.ALLOW_DEFAULT_VIEW, true));
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.renderCajole_ = Boolean(
+      osapi.container.util.getSafeJsonValue(config,
+      osapi.container.ContainerConfig.RENDER_CAJOLE, false));
+
+  /**
+   * @type {string}
+   * @private
+   */
+  this.renderDebugParam_ = String(osapi.container.util.getSafeJsonValue(
+      config, osapi.container.ContainerConfig.RENDER_DEBUG_PARAM,
+      osapi.container.ContainerConfig.RENDER_DEBUG));
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  var param = window.__CONTAINER_URI.getQP(this.renderDebugParam_);
+  this.renderDebug_ = (typeof param === 'undefined') ?
+      Boolean(osapi.container.util.getSafeJsonValue(config,
+          osapi.container.ContainerConfig.RENDER_DEBUG, false)) :
+      (param === '1');
+
+  /**
+   * @type {boolean}
+   * @private
+   */
+  this.renderTest_ = Boolean(osapi.container.util.getSafeJsonValue(config,
+      osapi.container.ContainerConfig.RENDER_TEST, false));
+
+  /**
+   * @see osapi.container.ContainerConfig.TOKEN_REFRESH_INTERVAL
+   * @type {number}
+   * @private
+   */
+  this.tokenRefreshInterval_ = Number(osapi.container.util.getSafeJsonValue(
+      config, osapi.container.ContainerConfig.TOKEN_REFRESH_INTERVAL, 0));
+
+  /**
+   * The time of the last token refresh.
+   * @type {number}
+   * @private
+   */
+  this.lastRefresh_ = 0;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  this.navigateCallback_ = String(osapi.container.util.getSafeJsonValue(
+      config, osapi.container.ContainerConfig.NAVIGATE_CALLBACK,
+      null));
+
+  /**
+   * @type {osapi.container.Service}
+   * @private
+   */
+  this.service_ = new osapi.container.Service(this);
+
+  /**
+   * result from calling window.setTimeout()
+   * @type {?number}
+   * @private
+   */
+  this.tokenRefreshTimer_ = null;
+
+  var self = this;
+  window[osapi.container.CallbackType.GADGET_ON_LOAD] = function(gadgetUrl, siteId) {
+    self.getSiteById(siteId).onRender();
+    self.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_RENDER, gadgetUrl, siteId);
+  };
+
+  this.initializeMixins_();
+
+  this.setupRpcArbitrator_(config);
+
+  this.preloadCaches(config);
+
+  this.registerRpcServices_();
+
+  this.onConstructed(config);
+};
+
+
+/**
+ * Create a new gadget site.
+ * @param {Element} gadgetEl HTML element into which to render.
+ * @param {Element=} opt_bufferEl Optional HTML element for double buffering.
+ * @return {osapi.container.GadgetSite} site created for client to hold to.
+ */
+osapi.container.Container.prototype.newGadgetSite = function(
+    gadgetEl, opt_bufferEl) {
+  var bufferEl = opt_bufferEl || null;
+  var site = new osapi.container.GadgetSite(this, this.service_, {
+      'navigateCallback' : this.navigateCallback_,
+      'gadgetEl' : gadgetEl,
+      'bufferEl' : bufferEl,
+      'gadgetOnLoad' : osapi.container.CallbackType.GADGET_ON_LOAD
+  });
+  this.sites_[site.getId()] = site;
+  return site;
+};
+
+
+/**
+ * Called when gadget is navigated.
+ *
+ * @param {osapi.container.GadgetSite} site destination gadget to navigate to.
+ * @param {string} gadgetUrl The URI of the gadget.
+ * @param {Object} viewParams view params for the gadget.
+ * @param {Object} renderParams render parameters, including the view.
+ * @param {function(Object)=} opt_callback Callback after gadget is loaded.
+ */
+osapi.container.Container.prototype.navigateGadget = function(
+    site, gadgetUrl, viewParams, renderParams, opt_callback) {
+  var callback = opt_callback || function() {},
+    ContainerConfig = osapi.container.ContainerConfig,
+    RenderParam = osapi.container.RenderParam;
+
+  if (this.allowDefaultView_) {
+    renderParams[RenderParam.ALLOW_DEFAULT_VIEW] = true;
+  }
+  if (this.renderCajole_) {
+    renderParams[RenderParam.CAJOLE] = true;
+  }
+  if (this.renderDebug_) {
+    renderParams[RenderParam.NO_CACHE] = true;
+    renderParams[RenderParam.DEBUG] = true;
+  }
+  if (this.renderTest_) {
+    renderParams[RenderParam.TEST_MODE] = true;
+  }
+
+  this.refreshService_();
+
+  var
+    self = this,
+    finishNavigate = function(preferences) {
+      renderParams[RenderParam.USER_PREFS] = preferences;
+      self.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_NAVIGATE,
+              gadgetUrl, site.getId());
+      // TODO: Lifecycle, add ability for current gadget to cancel nav.
+      site.navigateTo(gadgetUrl, viewParams, renderParams, function(gadgetInfo) {
+        // TODO: Navigate to error screen on primary gadget load failure
+        // TODO: Should display error without doing a standard navigate.
+        // TODO: Bad if the error gadget fails to load.
+        if (gadgetInfo.error) {
+          gadgets.warn(['Failed to possibly schedule token refresh for gadget ',
+              gadgetUrl, '.'].join(''));
+        } else if (gadgetInfo[osapi.container.MetadataResponse.NEEDS_TOKEN_REFRESH]) {
+          self.scheduleRefreshTokens_(gadgetInfo[osapi.container.MetadataResponse.TOKEN_TTL]);
+        }
+
+        self.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_NAVIGATED, site);
+        callback(gadgetInfo);
+      });
+    };
+
+  // Try to retrieve preferences for the gadget if no preferences were explicitly provided.
+  if (this.config_[ContainerConfig.GET_PREFERENCES] && !renderParams[RenderParam.USER_PREFS]) {
+    this.config_[ContainerConfig.GET_PREFERENCES](site.getId(), gadgetUrl, finishNavigate);
+  }
+  else {
+    finishNavigate(renderParams[RenderParam.USER_PREFS]);
+  }
+};
+
+
+/**
+ * Called when gadget is closed. This may stop refreshing of tokens.
+ * @param {osapi.container.GadgetSite} site navigate gadget to close.
+ */
+osapi.container.Container.prototype.closeGadget = function(site) {
+  var id = site.getId();
+  this.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_CLOSE, site);
+  site.close();
+  this.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_CLOSED, site);
+  delete this.sites_[id];
+  if (site instanceof osapi.container.GadgetSite) {
+    this.unscheduleRefreshTokens_();
+  }
+};
+
+
+/**
+ * Add a callback to be called when one or more gadgets are preloaded,
+ * navigated to or closed.
+ *
+ * @param {string} name name of the lifecycle callback.
+ * @param {Object} lifeCycleCallback callback object to call back when a gadget is
+ *     preloaded, navigated to or closed.  called via preloaded, navigated
+ *     and closed methods.
+ *
+ * @return {boolean} true if added successfully, false if a callback
+ *     with that name is already registered.
+ */
+osapi.container.Container.prototype.addGadgetLifecycleCallback = function(name, lifeCycleCallback) {
+  if (!this.gadgetLifecycleCallbacks_[name]) {
+    this.gadgetLifecycleCallbacks_[name] = lifeCycleCallback;
+    return true;
+  }
+  return false;
+};
+
+/**
+ * remove a lifecycle callback previously registered with the container
+ * @param {string} name callback object to be removed.
+ */
+osapi.container.Container.prototype.removeGadgetLifecycleCallback = function(name) {
+  delete this.gadgetLifecycleCallbacks_[name];
+};
+
+/**
+ * Pre-load one gadget metadata information. More details on preloadGadgets().
+ * @param {string} gadgetUrl gadget URI to preload.
+ * @param {function(Object)=} opt_callback function to call upon data receive.
+ */
+osapi.container.Container.prototype.preloadGadget = function(gadgetUrl, opt_callback) {
+  this.preloadGadgets([gadgetUrl], opt_callback);
+};
+
+
+/**
+ * Pre-load gadgets metadata information. This is done by priming the cache,
+ * and making an immediate call to fetch metadata of gadgets fully specified at
+ * gadgetUrls. This will not render, and do additional callback operations.
+ * @param {Array} gadgetUrls gadgets URIs to preload.
+ * @param {function(Object)=} opt_callback function to call upon data receive.
+ */
+osapi.container.Container.prototype.preloadGadgets = function(gadgetUrls, opt_callback) {
+  var callback = opt_callback || function() {};
+  var request = osapi.container.util.newMetadataRequest(gadgetUrls);
+  var self = this;
+
+  this.refreshService_();
+  this.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_PRELOAD, gadgetUrls);
+  this.service_.getGadgetMetadata(request, function(response) {
+    self.addPreloadGadgets_(response);
+    self.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_PRELOADED,
+        response);
+    callback(response);
+  });
+};
+
+
+/**
+ * Unload preloaded gadget. Makes future preload request possibly uncached.
+ * @param {string} gadgetUrl gadget URI to unload.
+ */
+osapi.container.Container.prototype.unloadGadget = function(gadgetUrl) {
+  this.unloadGadgets([gadgetUrl]);
+};
+
+
+/**
+ * Unload preloaded gadgets. Makes future preload request possibly uncached.
+ * @param {Array} gadgetUrls gadgets URIs to unload.
+ */
+osapi.container.Container.prototype.unloadGadgets = function(gadgetUrls) {
+  for (var i = 0; i < gadgetUrls.length; i++) {
+    var url = gadgetUrls[i];
+    this.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_UNLOAD,
+            url);
+    delete this.preloadedGadgetUrls_[url];
+    this.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_UNLOADED,
+        url);
+  }
+};
+
+
+/**
+ * Fetch the gadget metadata commonly used by container for user preferences.
+ * @param {string} gadgetUrl gadgets URI to fetch metadata for. to preload.
+ * @param {function(Object)} callback Function called with gadget metadata.
+ */
+osapi.container.Container.prototype.getGadgetMetadata = function(
+    gadgetUrl, callback) {
+  var request = osapi.container.util.newMetadataRequest([gadgetUrl]);
+
+  this.refreshService_();
+  this.service_.getGadgetMetadata(request, callback);
+};
+
+
+/**
+ * @param {string} service name of RPC service to register.
+ * @param {Function} callback post-RPC function to call, with RPC-related
+ *                   arguments (with the calling GadgetSite augmented) and the
+ *                   callback response itself.
+ *
+ * @return The old service handler for serviceName, if any.
+ */
+osapi.container.Container.prototype.rpcRegister = function(service, callback) {
+  var self = this;
+  return gadgets.rpc.register(service, function() {
+    // this['f'] is set by calling iframe via gadgets.rpc.
+    this[osapi.container.GadgetSite.RPC_ARG_KEY] =
+        self.getGadgetSiteByIframeId_(this['f']);
+    var argsCopy = [this];
+    for (var i = 0; i < arguments.length; ++i) {
+      argsCopy.push(arguments[i]);
+    }
+    return callback.apply(self, argsCopy);
+  });
+};
+
+
+/**
+ * Callback that occurs after instantiation/construction of this. Override to
+ * provide your specific functionalities.
+ * @param {Object=} opt_config Configuration JSON.
+ */
+osapi.container.Container.prototype.onConstructed = function(opt_config) {};
+
+
+/**
+ * Adds a new namespace to the Container object.  The namespace
+ * will contain the result of calling the function passed in.
+ *
+ * @param {string} namespace the namespace to add.
+ * @param {function} func to call when creating the namespace.
+ */
+osapi.container.Container.addMixin = function(namespace, func) {
+  var mixins = osapi.container.Container.prototype.mixins_;
+
+  if (mixins[namespace]) {
+    var orig = mixins[namespace];
+    mixins[namespace] = function(container) {
+      orig.call(this, container);
+      return func.call(this, container);
+    };
+  } else {
+    osapi.container.Container.prototype.mixinsOrder_.push(namespace);
+    mixins[namespace] = func;
+  }
+};
+
+
+// -----------------------------------------------------------------------------
+// Private variables and methods.
+// -----------------------------------------------------------------------------
+
+
+/**
+ * Adds the ability for features to extend the container with
+ * their own functionality that may be specific to that feature.
+ * @type {Object<string,function>}
+ * @private
+ */
+osapi.container.Container.prototype.mixins_ = {};
+
+/**
+ * Order of addMixin calls.
+ * @type {Array<string>}
+ * @private
+ */
+osapi.container.Container.prototype.mixinsOrder_ = [];
+
+
+/**
+ * Called from the constructor to add any namespace extensions.
+ * @private
+ */
+osapi.container.Container.prototype.initializeMixins_ = function() {
+  var mixins = osapi.container.Container.prototype.mixins_,
+      order = osapi.container.Container.prototype.mixinsOrder_;
+  for (var i = 0; i < order.length; i++) {
+    var namespace = order[i];
+    this[namespace] = new mixins[namespace](this);
+  }
+};
+
+
+/**
+ * Add list of gadgets to preload list
+ *
+ * @param {Object}
+ *          response hash of gadgets data.
+ * @param {Object=}
+ *          opt_tokenResponse hash of gadget token data. Used in the case where the container is
+ *          initialized with tokens.
+ * @private
+ */
+osapi.container.Container.prototype.addPreloadGadgets_ = function(response, opt_tokenResponse) {
+  var tokenResponse = opt_tokenResponse || {};
+  for (var id in response) {
+    if (response[id].error) {
+      var message = ['Failed to preload gadget ', id, '.'].join('');
+      gadgets.warn(message);
+
+      message = ['Detailed error: ', response[id].error.code || '', ' ', response[id].error.message || ''].join('');
+      gadgets.log(message);
+    } else {
+      this.addPreloadedGadgetUrl_(id);
+      if (response[id][osapi.container.MetadataResponse.NEEDS_TOKEN_REFRESH]) {
+        // Check the opt_tokenResponse for the TTL of any preloaded tokens
+        var tokenTTL;
+        if (tokenResponse[id]) {
+          tokenTTL = tokenResponse[id][osapi.container.TokenResponse.TOKEN_TTL];
+        } else {
+          tokenTTL = response[id][osapi.container.MetadataResponse.TOKEN_TTL];
+        }
+        // Safe to re-schedule many times.
+        this.scheduleRefreshTokens_(tokenTTL);
+      }
+    }
+  }
+};
+
+
+/**
+ * Preload gadget metadata and tokens to avoid the need for XHR's when navigating gadget sites.
+ * This function is safe to call repeatedly if needed to incrementally build up the internal caches.
+ * Support caching by providing server time to override response time usage.
+ * @param {Object} preloadData object containing data to be preloaded.
+ */
+osapi.container.Container.prototype.preloadCaches = function(preloadData) {
+  var gadgets = osapi.container.util.getSafeJsonValue(
+      preloadData, osapi.container.ContainerConfig.PRELOAD_METADATAS, {});
+  var tokens = osapi.container.util.getSafeJsonValue(
+      preloadData, osapi.container.ContainerConfig.PRELOAD_TOKENS, {});
+  var refTime = osapi.container.util.getSafeJsonValue(
+      preloadData, osapi.container.ContainerConfig.PRELOAD_REF_TIME, null);
+  var gadgetUrls = [];//keys of gadgets
+  for(var k in gadgets) {
+      if (gadgets.hasOwnProperty(k)){
+          gadgetUrls.push(k);
+      }
+  }
+
+  this.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_PRELOAD, gadgetUrls);
+  this.service_.addGadgetMetadatas(gadgets, refTime);
+  this.service_.addGadgetTokens(tokens, refTime);
+  this.addPreloadGadgets_(gadgets, tokens);
+  this.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_PRELOADED, gadgets);
+};
+
+
+/**
+ * Deletes stale cached data in service. The container knows what data are safe
+ * to be marked for deletion.
+ * @private
+ */
+osapi.container.Container.prototype.refreshService_ = function() {
+  var urls = this.getActiveGadgetUrls_();
+  this.service_.uncacheStaleGadgetMetadataExcept(urls);
+  // TODO: also uncache stale gadget tokens.
+};
+
+
+/**
+ * @param {string} iframeId Iframe ID of gadget holder contained in the gadget
+ *     site to get.
+ * @return {osapi.container.GadgetSite} The gadget site.
+ * @private
+ */
+osapi.container.Container.prototype.getGadgetSiteByIframeId_ = function(iframeId) {
+  // TODO: Support getting only the loading/active gadget in 2x buffers.
+  for (var siteId in this.sites_) {
+    var site = this.sites_[siteId];
+    var holder = site.getActiveSiteHolder();
+    if (holder && holder.getIframeId() === iframeId) {
+      return site;
+    }
+  }
+  return null;
+};
+
+/**
+ * @param {string} siteId ID of gadget site to get.
+ * @return {osapi.container.GadgetSite|osapi.container.UrlSite} The gadget site.
+ */
+osapi.container.Container.prototype.getSiteById = function(siteId) {
+  return this.sites_[siteId];
+};
+
+/**
+ * Update and schedule refreshing of container token.  This function will use the config function
+ * osapi.container.ContainerConfig.GET_CONTAINER_TOKEN to fetch a container token, if needed,
+ * unless the token is specified in the optional parameter, in which case the token will be
+ * updated with the provided value immediately.
+ *
+ * @param {function(error)=} callback Function to run when refresh completes or is cancelled.
+ *          error will be undefined if there is no error.
+ * @param {String=|boolean} tokenOrWait
+ *          token The container's new security token.
+ *          wait If the callback should not trigger a token fetch.
+ * @param {number=} ttl The token's ttl in seconds. If token is specified and ttl is 0,
+ *   token refresh will be disabled.
+ * @see osapi.container.ContainerConfig.GET_CONTAINER_TOKEN (constants.js)
+ */
+osapi.container.Container.prototype.updateContainerSecurityToken = function(callback, token, ttl) {
+  this.service_.updateContainerSecurityToken(callback, token, ttl);
+}
+
+/**
+ * Start to schedule refreshing of tokens.
+ * @param {number} Encountered token time to live in seconds.
+ * @private
+ */
+osapi.container.Container.prototype.scheduleRefreshTokens_ = function(tokenTTL) {
+  var self = this,
+      oldInterval = this.tokenRefreshInterval_,
+      newInterval = tokenTTL ? this.setRefreshTokenInterval_(tokenTTL * 1000) : oldInterval,
+      refresh = function() {
+        function callback(error) {
+          if (error) {
+            // try again, but don't force a refresh.
+            setTimeout(gadgets.util.makeClosure(self, self.updateContainerSecurityToken, callback, true), 1);
+          } else {
+            self.lastRefresh_ = osapi.container.util.getCurrentTimeMs();
+            // Schedule the next refresh.
+            self.tokenRefreshTimer_ = setTimeout(refresh, newInterval);
+
+            // Do this last so that if it ever errors, we maintain the refresh schedule.
+            self.refreshTokens_();
+          }
+        }
+        self.updateContainerSecurityToken(callback);
+      };
+
+  // If enabled, check to see if we no schedule or if the two intervals are different and update the schedule.
+  if (this.isRefreshTokensEnabled_() && (!this.tokenRefreshTimer_ || newInterval != oldInterval)) {
+    var now = osapi.container.util.getCurrentTimeMs();
+    if (!this.tokenRefreshTimer_) {
+      this.lastRefresh_ = now;
+      this.tokenRefreshTimer_ = setTimeout(refresh, newInterval);
+    }
+    else {
+      var futureRefresh = (this.lastRefresh_ || 0) + oldInterval;
+      if (futureRefresh < now) {
+        // This really shouldn't happen, but if for some reason we missed a
+        // refresh, make sure we cancel any timer we have and schedule
+        // a new one.
+        futureRefresh = now + newInterval;
+        newInterval = 1;
+      }
+      if (futureRefresh > now + newInterval) {
+        // Cancel the old timer and create a new one if the next refresh is
+        // too far away.
+        clearTimeout(this.tokenRefreshTimer_);
+        this.tokenRefreshTimer_ = setTimeout(refresh, newInterval);
+      }
+    }
+  }
+};
+
+/**
+ * Stop already-scheduled refreshing of tokens.
+ * @private
+ */
+osapi.container.Container.prototype.unscheduleRefreshTokens_ = function() {
+  if (this.tokenRefreshTimer_) {
+    var urls = this.getTokenRefreshableGadgetUrls_();
+    if (urls.length <= 0) {
+      clearTimeout(this.tokenRefreshTimer_);
+      this.tokenRefreshTimer_ = null;
+    }
+  }
+};
+
+
+/**
+ * Token refresh gets enabled if the value of refresh interval is > 0;
+ *
+ * @return {Boolean} if token refresh interval is of valid value.
+ * @private
+ */
+osapi.container.Container.prototype.isRefreshTokensEnabled_ = function() {
+  return this.tokenRefreshInterval_ > 0;
+};
+
+/**
+ * If the refresh interval is < 0, does nothing.  Otherwise updates the tokenTTL
+ * to the smallest value encountered.
+ *
+ * @param {number} Encountered token time to live in milliseconds.
+ * @return {Boolean} The ttl if the set succeeded, otherwise false.
+ * @private
+ */
+osapi.container.Container.prototype.setRefreshTokenInterval_ = function(tokenTTL) {
+  // TODO: Handle the case where we've closed the gadget responsible for the
+  // shortest refresh time, and can now safely extend this.tokenRefreshInterval_
+  if (tokenTTL) {
+    tokenTTL *= .8; // 80% of the TTL value, for buffer.
+    var refresh = this.tokenRefreshInterval_;
+    if (refresh < 0) {
+      return refresh;
+    }
+    else {
+      return this.tokenRefreshInterval_ =
+        refresh == 0 ? tokenTTL : Math.min(refresh, tokenTTL);
+    }
+  }
+};
+
+
+/**
+ * Register standard RPC services
+ * @private
+ */
+osapi.container.Container.prototype.registerRpcServices_ = function() {
+  var self = this;
+
+  this.rpcRegister('resize_iframe', function(rpcArgs, data) {
+    var site = rpcArgs[osapi.container.GadgetSite.RPC_ARG_KEY];
+    if (site) { // Check if site is not already closed.
+      site.setHeight(data);
+    }
+  });
+
+  this.rpcRegister('resize_iframe_width', function(rpcArgs, newWidth) {
+    var site = rpcArgs[osapi.container.GadgetSite.RPC_ARG_KEY];
+    if (site) { // Check if site is not already closed.
+      site.setWidth(newWidth);
+    }
+    return true;
+  });
+
+  /**
+   * @see setprefs.js setprefs feature.
+   */
+  this.rpcRegister('set_pref', function(rpcArgs, key, value) {
+    var site = rpcArgs[osapi.container.GadgetSite.RPC_ARG_KEY];
+    var setPrefs = self.config_[osapi.container.ContainerConfig.SET_PREFERENCES];
+    if (site && setPrefs) { // Check if site is not already closed.
+      var data = {};
+      for (var i = 2, j = arguments.length; i < j; i += 2) {
+        data[arguments[i]] = arguments[i + 1];
+      }
+      setPrefs(site.getId(), site.getActiveSiteHolder().getUrl(), data);
+    }
+  });
+};
+
+/**
+ * Sets up the RPC arbitrator if enabled in the container js.  If
+ * a function is provided in the containers config the container will use
+ * that, if not it will use the default arbitrator.
+ * @private
+ */
+osapi.container.Container.prototype.setupRpcArbitrator_ = function(config) {
+  var container = gadgets.config.get('container');
+  if(typeof container.enableRpcArbitration !== 'undefined' &&
+          container.enableRpcArbitration) {
+    var arbitrate = osapi.container.util.getSafeJsonValue(
+            config, osapi.container.ContainerConfig.RPC_ARBITRATOR, null);
+    if(!arbitrate) {
+      var self = this;
+      //This implementation uses the metadata cache to check for allowed rpc service ids
+      arbitrate = function(serviceId, from) {
+        var site = self.getGadgetSiteByIframeId_(from);
+        if(site && site.getActiveSiteHolder()) {
+          var cachedResponse = self.service_.getCachedGadgetMetadata(
+                  site.getActiveSiteHolder().getUrl());
+          if(!cachedResponse.error && cachedResponse.rpcServiceIds) {
+            for(var i = 0, rpcServiceId; rpcServiceId = cachedResponse.rpcServiceIds[i]; i++) {
+              if(rpcServiceId == serviceId) {
+                return true;
+              }
+            }
+          }
+        }
+        gadgets.warn('RPC call to ' + serviceId + ' was not allowed.');
+        return false;
+      };
+    }
+    gadgets.rpc.config({'arbitrator' : arbitrate});
+  }
+};
+
+
+/**
+ * Keep track of preloaded gadget URLs. These gadgets will have their tokens
+ * refreshed as part of batched token fetch.
+ * @param {string} gadgetUrl URL of preloaded gadget.
+ * @private
+ */
+osapi.container.Container.prototype.addPreloadedGadgetUrl_ = function(gadgetUrl) {
+  this.preloadedGadgetUrls_[gadgetUrl] = null;
+};
+
+
+/**
+ * Collect all URLs of gadgets that require tokens refresh. This comes from both
+ * preloaded gadgets and navigated-to gadgets.
+ * @return {Array} An array of URLs of gadgets.
+ * @private
+ */
+// TODO: this function needs to be renamed, perhaps: getTokenRequestUrls_
+osapi.container.Container.prototype.getTokenRefreshableGadgetUrls_ = function() {
+  var result = {};
+
+  for (var url in this.getActiveGadgetUrls_()) {
+    var metadata = this.service_.getCachedGadgetMetadata(url);
+    if (metadata[osapi.container.MetadataResponse.NEEDS_TOKEN_REFRESH]) {
+      result[url] = 1;
+    }
+  }
+
+  /* Now add all gadget site urls that have moduleIds
+   *
+   * We're basically refreshing a security token for any given
+   * non-persisted (no moduleId) navigated or preloaded gadget instance, as
+   * well as each persisted instance (each moduleId) for any given gadgetUrl.
+   *
+   * In other words, if we've got a gadget preloaded or navigated:
+   *   http://foo.com/gadget.xml
+   * We will refresh a token for non-persisted (no moduleId) instances of that
+   * gadget.
+   * If we've got a navigated persisted gadget on the page, we'll refresh that
+   * security token as well.
+   */
+  for (var siteId in this.sites_) {
+    var site = this.sites_[siteId];
+    if (site instanceof osapi.container.GadgetSite) {
+      var holder = site.getActiveSiteHolder();
+      if (holder) {
+        var url = holder.getUrl();
+            mid = site.getModuleId();
+
+        // If this gadget token does not require refresh
+        // (baseurl is not already in result), don't add it.
+        if (result[url]) {
+          result[osapi.container.util.buildTokenRequestUrl(url, mid)] = 1;
+        }
+      }
+    }
+  }
+
+  return osapi.container.util.toArrayOfJsonKeys(result);
+};
+
+
+/**
+ * Get gadget urls that are either navigated or preloaded.
+ * @return {Object} JSON of gadget URLs.
+ * @private
+ */
+osapi.container.Container.prototype.getActiveGadgetUrls_ = function() {
+  return osapi.container.util.mergeJsons(
+      this.getNavigatedGadgetUrls_(),
+      this.preloadedGadgetUrls_);
+};
+
+
+/**
+ * Get gadget urls that are navigated on page.
+ * @return {Object} JSON of gadget URLs.
+ * @private
+ */
+osapi.container.Container.prototype.getNavigatedGadgetUrls_ = function() {
+  var result = {};
+  for (var siteId in this.sites_) {
+    var site = this.sites_[siteId];
+    if (site instanceof osapi.container.GadgetSite) {
+      var holder = site.getActiveSiteHolder();
+      if(holder) {
+        result[holder.getUrl()] = 1;
+      }
+    }
+  }
+  return result;
+};
+
+
+/**
+ * Refresh security tokens immediately. This will fetch gadget metadata, along
+ * with its token and have the token cache updated.
+ * @private
+ */
+osapi.container.Container.prototype.refreshTokens_ = function() {
+  var ids = this.getTokenRefreshableGadgetUrls_();
+  var request = osapi.container.util.newTokenRequest(ids);
+
+  var self = this;
+  this.service_.getGadgetToken(request, function(response) {
+    // Update active token-requiring gadgets with new tokens. Do not need to
+    // update pre-loaded gadgets, since new tokens will take effect when they
+    // are navigated to, from cache.
+    for (var siteId in self.sites_) {
+      var site = self.sites_[siteId];
+      if (site instanceof osapi.container.GadgetSite) {
+        var holder = site.getActiveSiteHolder();
+        if (holder) {
+          var gadgetInfo = self.service_.getCachedGadgetMetadata(holder.getUrl());
+          if (gadgetInfo[osapi.container.MetadataResponse.NEEDS_TOKEN_REFRESH]) {
+            var mid = site.getModuleId(),
+                url = osapi.container.util.buildTokenRequestUrl(holder.getUrl(), mid),
+                tokenInfo = response[url];
+
+            if (tokenInfo.error) {
+              gadgets.warn(['Failed to get token for gadget ',
+                  url, '.'].join(''));
+            } else {
+              gadgets.rpc.call(holder.getIframeId(), 'update_security_token', null,
+                  tokenInfo[osapi.container.TokenResponse.TOKEN]);
+            }
+          }
+        }
+      }
+    }
+  });
+};
+
+
+/**
+ * invalidate all tokens, so next time the refreshToken_ is called, all the token will be refreshed.
+ * @private
+ */
+osapi.container.Container.prototype.invalidateAllTokens_ = function() {
+  var self = this;
+  for ( var siteId in self.sites_) {
+    var site = self.sites_[siteId];
+    if (site instanceof osapi.container.GadgetSite) {
+      var holder = site.getActiveSiteHolder();
+      var gadgetInfo = self.service_.getCachedGadgetMetadata(holder.getUrl());
+      gadgetInfo[osapi.container.MetadataResponse.NEEDS_TOKEN_REFRESH] = true;
+    }
+  }
+};
+
+/**
+ * Refresh all security tokens on all gadget sites immediately no matter it's needed or not. This
+ * will fetch gadget metadata, along with its token and have the token cache updated
+ */
+osapi.container.Container.prototype.forceRefreshAllTokens = function() {
+  this.invalidateAllTokens_();
+  this.refreshTokens_();
+};
+
+/**
+ * invokes methods on the gadget lifecycle callback registered with the
+ * container.  The callback will be passed the remainder of the arguments after methodName.
+ * @param {string} methodName of the callback method to be called.
+ * @private
+ */
+osapi.container.Container.prototype.applyLifecycleCallbacks_ = function(methodName) {
+  var args = Array.prototype.slice.call(arguments, 1);
+  for (name in this.gadgetLifecycleCallbacks_) {
+    var method = this.gadgetLifecycleCallbacks_[name][methodName];
+    if (method) {
+      method.apply(null, args);
+    }
+  }
+};
+
+/**
+ * Creates a new URL site
+ * @param {Element} element the element to put the site in.
+ * @return {osapi.container.UrlSite} a new site.
+ */
+osapi.container.Container.prototype.newUrlSite = function(element) {
+  var args = {};
+  args[osapi.container.UrlSite.URL_ELEMENT] = element;
+  var site = new osapi.container.UrlSite(this, this.service_, args);
+  this.sites_[site.getId()] = site;
+  return site;
+};
+
+
+/**
+ * Navigates to a URL.
+ * @param {osapi.container.UrlSite} site the URL site to render the URL in.
+ * @param {string} url the URL to render.
+ * @param {Object} renderParams params to augment the rendering. Valid rendering parameters
+ * include osapi.container.RenderParam.CLASS, osapi.container.RenderParam.HEIGHT,
+ * and osapi.container.RenderParam.WIDTH.
+ * @return {osapi.container.UrlSite} the site you passed in.
+ *
+ */
+osapi.container.Container.prototype.navigateUrl = function(site, url, renderParams) {
+  site.render(url, renderParams);
+  return site;
+};
diff --git a/trunk/features/src/main/javascript/features/container/feature.xml b/trunk/features/src/main/javascript/features/container/feature.xml
new file mode 100644
index 0000000..9ab3181
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container/feature.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>container</name>
+  <dependency>globals</dependency>
+  <dependency>core.log</dependency>
+  <dependency>security-token</dependency>
+  <dependency>shindig.uri.ext</dependency>
+  <dependency>osapi</dependency>
+  <dependency>rpc</dependency>
+  <dependency>container.util</dependency>
+  <dependency>container.site.gadget</dependency>
+  <dependency>container.site.url</dependency>
+  <container>
+    <script src="service.js"/>
+    <script src="container.js"/>
+    <script src="init.js"/>
+    <api>
+      <exports type="js">osapi.container.Container</exports>
+      <exports type="js">osapi.container.Container.prototype.newGadgetSite</exports>
+      <exports type="js">osapi.container.Container.prototype.navigateGadget</exports>
+      <exports type="js">osapi.container.Container.prototype.closeGadget</exports>
+      <exports type="js">osapi.container.Container.prototype.preloadGadget</exports>
+      <exports type="js">osapi.container.Container.prototype.preloadGadgets</exports>
+      <exports type="js">osapi.container.Container.prototype.preloadCaches</exports>
+      <exports type="js">osapi.container.Container.prototype.unloadGadget</exports>
+      <exports type="js">osapi.container.Container.prototype.unloadGadgets</exports>
+      <exports type="js">osapi.container.Container.prototype.getGadgetMetadata</exports>
+      <exports type="js">osapi.container.Container.prototype.rpcRegister</exports>
+      <exports type="js">osapi.container.Container.prototype.onConstructed</exports>
+      <exports type="js">osapi.container.Container.prototype.getGadgetSiteById</exports>
+      <exports type="js">osapi.container.Container.prototype.updateContainerSecurityToken</exports>
+      <exports type="js">osapi.container.Container.prototype.forceRefreshAllTokens</exports>
+      <exports type="rpc">resize_iframe</exports>
+      <exports type="rpc">resize_iframe_width</exports>
+      <exports type="rpc">set_pref</exports>
+      <uses type="rpc">update_security_token</uses>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/container/init.js b/trunk/features/src/main/javascript/features/container/init.js
new file mode 100644
index 0000000..dcb504f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container/init.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Initial configuration/boot-strapping work for common container
+ * to operate. This includes setting up global environment variables.
+ */
+gadgets.config.register('container', null, function(config) {
+  var jsPath = config.container.jsPath || null;
+
+  window.__CONTAINER_URI = shindig.uri(window.location.href);
+  window.__API_URI = null;
+  var scriptEl = null;
+  if (window.__CONTAINER_SCRIPT_ID) {
+    scriptEl = document.getElementById(window.__CONTAINER_SCRIPT_ID);
+  } else {
+    var scriptEls = document.getElementsByTagName('script');
+    for(var i = 0; scriptEls && i < scriptEls.length; i++) {
+      var src = scriptEls[i].src,
+          found = jsPath != null && src && src.indexOf(jsPath) || -1;
+      if(found != -1 && /.*container.*[.]js.*[?&]c=1(#|&|$).*/.test(src.substring(found + jsPath.length))) {
+        scriptEl = scriptEls[i];
+        break;
+      }
+    }
+  }
+
+  if (scriptEl) {
+    window.__API_URI = shindig.uri(scriptEl.src);
+    // In case script URI is relative, resolve (make absolute) with container.
+    window.__API_URI.resolve(window.__CONTAINER_URI);
+  }
+
+  window.__CONTAINER = window.__API_URI ? window.__API_URI.getQP('container') : 'default';
+}, false);
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/container/service.js b/trunk/features/src/main/javascript/features/container/service.js
new file mode 100644
index 0000000..09fb826
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/container/service.js
@@ -0,0 +1,514 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview This represents the service layer that talks to OSAPI
+ * endpoints. All RPC requests should go into this class.
+ */
+
+
+/**
+ * @param {osapi.container.Container} The container that this service services.
+ * @constructor
+ */
+osapi.container.Service = function(container) {
+  /**
+   * The container that this service services.
+   * @type {osapi.container.Container}
+   * @private
+   */
+  this.container_ = container;
+
+  var config = this.config_ = container.config_ || {};
+
+  var injectedEndpoint = ((gadgets.config.get('osapi') || {}).endPoints ||
+          [window.__API_URI.getOrigin() + '/rpc'])[0];
+  var matches = /^([^\/]*\/\/[^\/]+)(.*)$/.exec(injectedEndpoint);
+  /**
+   * @type {string}
+   * @private
+   */
+  this.apiHost_ = String(osapi.container.util.getSafeJsonValue(config,
+      osapi.container.ServiceConfig.API_HOST, matches[1]));
+
+  /**
+   * @type {string}
+   * @private
+   */
+  this.apiPath_ = String(osapi.container.util.getSafeJsonValue(config,
+      osapi.container.ServiceConfig.API_PATH, matches[2]));
+
+  /**
+   * Map of gadget URLs to cached gadgetInfo response.
+   * @type {Object}
+   * @private
+   */
+  this.cachedMetadatas_ = {};
+
+  /**
+   * Map of gadget URLs to cached tokenInfo response.
+   * @type {Object}
+   * @private
+   */
+  this.cachedTokens_ = {};
+
+  /**
+   * @see osapi.container.Container.prototype.getLanguage
+   */
+  if (config.GET_LANGUAGE) {
+    this.getLanguage = config.GET_LANGUAGE;
+  }
+
+  /**
+   * @see osapi.container.Container.prototype.getCountry
+   */
+  if (config.GET_COUNTRY) {
+    this.getCountry = config.GET_COUNTRY;
+  }
+
+  this.registerOsapiServices();
+
+  this.onConstructed(config);
+};
+
+
+/**
+ * Callback that occurs after instantiation/construction of this. Override to
+ * provide your specific functionalities.
+ * @param {Object=} opt_config Configuration JSON.
+ */
+osapi.container.Service.prototype.onConstructed = function(opt_config) {};
+
+
+/**
+ * Return a possibly-cached gadgets metadata for gadgets in request.ids, for
+ * container request.container. If metadata is not cache, fetch from server
+ * only for the uncached gadget URLs. The optional callback opt_callback will be
+ * called, after a response is received.
+ * @param {Object} request JSON object representing the request.
+ * @param {function(Object)=} opt_callback function to call upon data receive.
+ */
+osapi.container.Service.prototype.getGadgetMetadata = function(request, opt_callback) {
+  // TODO: come up with an expiration mechanism to evict cached gadgets.
+  // Can be based on renderParam['nocache']. Be careful with preloaded and
+  // arbitrarily-navigated gadgets. The former should be indefinite, unless
+  // unloaded. The later can done without user knowing.
+  var callback = opt_callback || function() {};
+
+  var uncachedUrls = osapi.container.util.toArrayOfJsonKeys(
+      this.getUncachedDataByRequest_(this.cachedMetadatas_, request));
+  var finalResponse = this.getCachedDataByRequest_(this.cachedMetadatas_, request);
+
+  // If fully cached, return from cache.
+  if (uncachedUrls.length == 0) {
+    callback(finalResponse);
+
+  // Otherwise, request for uncached metadatas.
+  } else {
+    var self = this;
+    request['ids'] = uncachedUrls;
+    request['language'] = this.getLanguage();
+    request['country'] = this.getCountry();
+    this.updateContainerSecurityToken(function(error) {
+      if (error) {
+        for (var i = 0; i < request['ids'].length; i++) {
+          var id = request['ids'][i];
+          finalResponse[id] = { 'error' : error };
+        }
+        callback(finalResponse);
+      } else {
+        osapi['gadgets']['metadata'](request).execute(function(response) {
+          // If response entirely fails, augment individual errors.
+          if (response['error']) {
+            for (var i = 0; i < request['ids'].length; i++) {
+              var id = request['ids'][i];
+              finalResponse[id] = { 'error' : response['error'] };
+            }
+
+          // Otherwise, cache response. Augment final response with server response.
+          } else {
+            var currentTimeMs = osapi.container.util.getCurrentTimeMs();
+            for (var id in response) {
+              var resp = response[id];
+              self.updateResponse_(resp, id, currentTimeMs);
+              self.cachedMetadatas_[id] = resp;
+              finalResponse[id] = resp;
+            }
+          }
+
+          callback(finalResponse);
+        });
+      }
+    });
+  }
+};
+
+/**
+ * Add preloaded gadgets to cache
+ * @param {Object} response hash of gadgets metadata.
+ * @param {Object} refTime time to override responseTime (in order to support external caching).
+ */
+osapi.container.Service.prototype.addGadgetMetadatas = function(response, refTime) {
+  this.addToCache_(response, refTime, this.cachedMetadatas_);
+};
+
+
+/**
+ * Add preloaded tokens to cache
+ * @param {Object} response hash of gadgets metadata.
+ * @param {Object} refTime data time to override responseTime
+ *     (in order to support external caching).
+ */
+osapi.container.Service.prototype.addGadgetTokens = function(response, refTime) {
+  this.addToCache_(response, refTime, this.cachedTokens_);
+};
+
+
+/**
+ * Utility function to add data to cache
+ * @param {Object} response hash of gadgets metadata.
+ * @param {Object} refTime data time to override responseTime (in order to support external caching).
+ * @param {Object} cache the cache to update.
+ * @private
+ */
+osapi.container.Service.prototype.addToCache_ = function(response, refTime, cache) {
+  var currentTimeMs = osapi.container.util.getCurrentTimeMs();
+  for (var id in response) {
+    var resp = response[id];
+    this.updateResponse_(resp, id, currentTimeMs, refTime);
+    cache[id] = resp;
+  }
+};
+
+
+/**
+ * Update gadget data, set gadget id and calculate expiration time
+ * @param {Object} resp gadget metadata item.
+ * @param {string} id gadget id.
+ * @param {Object} currentTimeMs current time.
+ * @param {Object} opt_refTime data time to override responseTime (support external caching).
+ * @private
+ */
+osapi.container.Service.prototype.updateResponse_ = function(
+    resp, id, currentTimeMs, opt_refTime) {
+  resp[osapi.container.MetadataParam.URL] = id;
+  // This ignores time to fetch metadata. Okay, expect to be < 2s.
+  resp[osapi.container.MetadataParam.LOCAL_EXPIRE_TIME] =
+      resp[osapi.container.MetadataResponse.EXPIRE_TIME_MS] -
+      (opt_refTime == null ?
+          resp[osapi.container.MetadataResponse.RESPONSE_TIME_MS] : opt_refTime) +
+      currentTimeMs;
+};
+
+
+/**
+ * @param {Object} request JSON object representing the request.
+ * @param {function(Object)=} opt_callback function to call upon data receive.
+ */
+osapi.container.Service.prototype.getGadgetToken = function(request, opt_callback) {
+  var callback = opt_callback || function() {};
+
+  // Do not check against cache. Always do a server fetch.
+  var self = this;
+  var tokenResponseCallback = function(response) {
+    var finalResponse = {};
+
+    // If response entirely fails, augment individual errors.
+    if (response['error']) {
+      for (var i = 0; i < request['ids'].length; i++) {
+        finalResponse[request['ids'][i]] = { 'error' : response['error'] };
+      }
+
+    // Otherwise, cache response. Augment final response with server response.
+    } else {
+      for (var id in response) {
+        var mid = response[id][osapi.container.TokenResponse.MODULE_ID],
+            url = osapi.container.util.buildTokenRequestUrl(id, mid);
+
+        //response[id]['url'] = id; // make sure url is set
+        self.cachedTokens_[url] = response[id];
+        finalResponse[id] = response[id];
+      }
+    }
+
+    callback(finalResponse);
+  };
+
+  // If we have a custom token fetch function, call it -- otherwise use the default
+  self.updateContainerSecurityToken(function(error) {
+    if (error) {
+      tokenResponseCallback({'error': error});
+    } else {
+      if (self.config_[osapi.container.ContainerConfig.GET_GADGET_TOKEN]) {
+        self.config_[osapi.container.ContainerConfig.GET_GADGET_TOKEN](request, tokenResponseCallback);
+      } else {
+        osapi['gadgets']['token'](request).execute(tokenResponseCallback);
+      }
+    }
+  });
+};
+
+
+/**
+ * @param {string} url gadget URL to use as key to get cached metadata.
+ * @return {string} the gadgetInfo referenced by this URL.
+ */
+osapi.container.Service.prototype.getCachedGadgetMetadata = function(url) {
+  return this.cachedMetadatas_[url];
+};
+
+
+/**
+ * @param {string} url gadget URL to use as key to get cached token.
+ * @return {string} the tokenInfo referenced by this URL.
+ */
+osapi.container.Service.prototype.getCachedGadgetToken = function(url) {
+  return this.cachedTokens_[url];
+};
+
+
+/**
+ * @param {Object} urls JSON containing gadget URLs to avoid removing.
+ */
+osapi.container.Service.prototype.uncacheStaleGadgetMetadataExcept =
+    function(urls) {
+  for (var url in this.cachedMetadatas_) {
+    if (typeof urls[url] === 'undefined') {
+      var gadgetInfo = this.cachedMetadatas_[url];
+      if (gadgetInfo[osapi.container.MetadataParam.LOCAL_EXPIRE_TIME] <
+          osapi.container.util.getCurrentTimeMs()) {
+        delete this.cachedMetadatas_[url];
+      }
+    }
+  }
+};
+
+
+/**
+ * Initialize OSAPI endpoint methods/interfaces.
+ */
+osapi.container.Service.prototype.registerOsapiServices = function() {
+  var endPoint = this.apiHost_ + this.apiPath_;
+
+  var osapiServicesConfig = {};
+  osapiServicesConfig['gadgets.rpc'] = ['container.listMethods'];
+  osapiServicesConfig[endPoint] = [
+    'gadgets.metadata',
+    'gadgets.token'
+  ];
+
+  gadgets.config.init({
+    'osapi': { 'endPoints': [endPoint] },
+    'osapi.services': osapiServicesConfig
+  });
+};
+
+
+/**
+ * Get cached data by ids listed in request.
+ * @param {Object} cache JSON containing cached data.
+ * @param {Object} request containing ids.
+ * @return {Object} JSON containing requested and cached entries.
+ * @private
+ */
+osapi.container.Service.prototype.getCachedDataByRequest_ = function(
+    cache, request) {
+  return this.filterCachedDataByRequest_(cache, request,
+      function(data) { return (typeof data !== 'undefined') });
+};
+
+
+/**
+ * Get uncached data by ids listed in request.
+ * @param {Object} cache JSON containing cached data.
+ * @param {Object} request containing ids.
+ * @return {Object} JSON containing requested and uncached entries.
+ * @private
+ */
+osapi.container.Service.prototype.getUncachedDataByRequest_ = function(
+    cache, request) {
+  return this.filterCachedDataByRequest_(cache, request,
+      function(data) { return (typeof data === 'undefined') });
+};
+
+
+/**
+ * Helper to filter out cached data
+ * @param {Object} data JSON containing cached data.
+ * @param {Object} request containing ids.
+ * @param {Function} filterFunc function to filter result.
+ * @return {Object} JSON containing requested and filtered entries.
+ * @private
+ */
+osapi.container.Service.prototype.filterCachedDataByRequest_ = function(
+    data, request, filterFunc) {
+  var result = {};
+  for (var i = 0; i < request['ids'].length; i++) {
+    var id = request['ids'][i];
+    var cachedData = data[id];
+    if (filterFunc(cachedData)) {
+      result[id] = cachedData;
+    }
+  }
+  return result;
+};
+
+
+/**
+ * @return {string} Best-guess locale for current browser.
+ * @private
+ */
+osapi.container.Service.prototype.getLocale_ = function() {
+  var nav = window.navigator;
+  return nav.userLanguage || nav.systemLanguage || nav.language;
+};
+
+
+/**
+ * A callback function that will return the correct language locale part to use when
+ * asking the server to render a gadget or when asking the server for 1 or more
+ * gadget's metadata.
+ * <br>
+ * May be overridden by passing in a config parameter during container construction.
+ *  * @return {string} Language locale part.
+ */
+osapi.container.Service.prototype.getLanguage = function() {
+  try {
+    return this.getLocale_().split('-')[0] || 'ALL';
+  } catch (e) {
+    return 'ALL';
+  }
+};
+
+
+/**
+ * A callback function that will return the correct country locale part to use when
+ * asking the server to render a gadget or when asking the server for 1 or more
+ * gadget's metadata.
+ * <br>
+ * May be overridden by passing in a config parameter during container construction.
+ * @return {string} Country locale part.
+ */
+osapi.container.Service.prototype.getCountry = function() {
+  try {
+    return this.getLocale_().split('-')[1] || 'ALL';
+  } catch (e) {
+    return 'ALL';
+  }
+};
+
+/**
+ * Container Token Refresh
+ */
+(function() {
+  var containerTimeout, lastRefresh, fetching,
+      containerTokenTTL = 1800000 * 0.8, // 30 min default token ttl
+      callbacks = [];
+
+  function runCallbacks(callbacks, error) {
+    while (callbacks.length) {
+      callbacks.shift().call(null, error); // Window context
+    }
+  }
+
+  function refresh(fetch_once) {
+    var self = this;
+    if (containerTimeout) {
+      clearTimeout(containerTimeout);
+      containerTimeout = 0;
+    }
+
+    var fetch = fetch_once || this.config_[osapi.container.ContainerConfig.GET_CONTAINER_TOKEN];
+    if (fetch) {
+      if (!fetching) {
+        fetching = true;
+        fetch(function(token, ttl, error) { // token and ttl may be undefined in the case of an error
+          fetching = false;
+
+          // Use last known ttl if there was an error
+          containerTokenTTL = typeof(ttl) == 'number' ? (ttl * 1000 * 0.8) : containerTokenTTL;
+          if (containerTokenTTL) {
+            // Refresh again in 80% of the reported ttl
+            // Pass null in to closure because FF behaves un-expectedly when that param is not explicitly provided.
+            containerTimeout = setTimeout(gadgets.util.makeClosure(self, refresh, null), containerTokenTTL);
+          }
+
+          if (token) {
+            // Looks like everything worked out...  let's update the token.
+            shindig.auth.updateSecurityToken(token);
+            lastRefresh =  osapi.container.util.getCurrentTimeMs();
+            // And then run all the callbacks waiting for this.
+            runCallbacks(callbacks);
+          } else if (error) {
+            runCallbacks(callbacks, error);
+          }
+        });
+      }
+    } else {
+      fetching = false;
+      // Fail gracefully, container supplied no fetch function. Do not hold on to callbacks.
+      runCallbacks(callbacks);
+    }
+  }
+
+  /**
+   * @see osapi.container.Container.prototype.updateContainerSecurityToken
+   */
+  osapi.container.Service.prototype.updateContainerSecurityToken = function(callback, tokenOrWait, ttl) {
+    var undef,
+        now = osapi.container.util.getCurrentTimeMs(),
+        token = typeof(tokenOrWait) != 'boolean' && tokenOrWait || undef,
+        wait = typeof(tokenOrWait) == 'boolean' && tokenOrWait,
+        needsRefresh = containerTokenTTL &&
+            (fetching || token || !lastRefresh || now > lastRefresh + containerTokenTTL);
+    if (needsRefresh) {
+      // Hard expire in 95% of originial ttl.
+      var expired = !lastRefresh || now > lastRefresh + (containerTokenTTL * 95 / 80);
+      if (!expired && callback) {
+        // Token not expired, but needs refresh.  Don't block operations that need a valid token.
+        callback();
+      } else if (callback) {
+        // We have a callback, there's either a fetch happening, or we otherwise need to refresh the
+        // token.  Place it in the callbacks queue to be run after the fetch (currently running or
+        // soon to be launched) completes.
+        callbacks.push(callback);
+      }
+
+      if (token) {
+        // We are trying to set a token initially.  Run refresh with a fetch_once function that simply
+        // returns the canned values.  Then schedule the refresh using the function in the config
+        refresh.call(this, function(result) {
+          result(token, ttl);
+        });
+      } else if (!fetching && !wait) {
+        // There's no fetch going on right now. Unless wait is true, we need to start one right away
+        // because the token needs a refresh.
+
+        // If wait is true, the callback really just wants a valid token. It may be called with an
+        // error for informational purposes, but it's likely the callback will simply queue up
+        // immediately if there was an error.  To avoid spamming the refresh method, we allow them to
+        // specify `wait` so that it can wait for success without forcing a fetch.
+        refresh.call(this);
+      }
+    } else if (callback) {
+      // No refresh needed, run the callback because the token is fine.
+      callback();
+    }
+  };
+})();
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/content-rewrite/feature.xml b/trunk/features/src/main/javascript/features/content-rewrite/feature.xml
new file mode 100644
index 0000000..d7ef2a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/content-rewrite/feature.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <!-- content-rewrite is implemented as server-side code. -->
+  <name>content-rewrite</name>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.config.base/config.js b/trunk/features/src/main/javascript/features/core.config.base/config.js
new file mode 100644
index 0000000..9a6e494
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.config.base/config.js
@@ -0,0 +1,322 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Provides unified configuration for all features.
+ *
+ *
+ * <p>This is a custom shindig library that has not yet been submitted for
+ * standardization. It is designed to make developing of features for the
+ * opensocial / gadgets platforms easier and is intended as a supplemental
+ * tool to Shindig's standardized feature loading mechanism.
+ *
+ * <p>Usage:
+ * First, you must register a component that needs configuration:
+ * <pre>
+ *   var config = {
+ *     name : gadgets.config.NonEmptyStringValidator,
+ *     url : new gadgets.config.RegExValidator(/.+%mySpecialValue%.+/)
+ *   };
+ *   gadgets.config.register("my-feature", config, myCallback);
+ * </pre>
+ *
+ * <p>This will register a component named "my-feature" that expects input config
+ * containing a "name" field with a value that is a non-empty string, and a
+ * "url" field with a value that matches the given regular expression.
+ *
+ * <p>When gadgets.config.init is invoked by the container, it will automatically
+ * validate your registered configuration and will throw an exception if
+ * the provided configuration does not match what was required.
+ *
+ * <p>Your callback will be invoked by passing all configuration data passed to
+ * gadgets.config.init, which allows you to optionally inspect configuration
+ * from other features, if present.
+ *
+ * <p>Note that the container may optionally bypass configuration validation for
+ * performance reasons. This does not mean that you should duplicate validation
+ * code, it simply means that validation will likely only be performed in debug
+ * builds, and you should assume that production builds always have valid
+ * configuration.
+ */
+
+if (!window['gadgets']['config']) {
+gadgets.config = function() {
+  var ___jsl;
+  var components = {};
+  var configuration = {};
+  var initialized = false;
+
+  function foldConfig(origConfig, updConfig) {
+    for (var key in updConfig) {
+      if (!updConfig.hasOwnProperty(key)) {
+        continue;
+      }
+      if (typeof origConfig[key] === 'object' &&
+          typeof updConfig[key] === 'object') {
+        // Both have the same key with an object value. Recurse.
+        foldConfig(origConfig[key], updConfig[key]);
+      } else {
+        // If updConfig has a new key, or a value of different type
+        // than the original config for the same key, or isn't an object
+        // type, then simply replace the value for the key.
+        origConfig[key] = updConfig[key];
+      }
+    }
+  }
+
+  function getLoadingScript() {
+    var jsPath = (configuration['core.io'] || {})['jsPath'] || null,
+        candidates = [],
+        n = 0;
+
+    // Attempt to retrieve config augmentation from latest script node.
+    var scripts = document.scripts || document.getElementsByTagName('script');
+    if (!scripts || scripts.length == 0) {
+      return candidates;
+    }
+
+    for (var i = 0; i < scripts.length; ++i) {
+      var src = scripts[i].src,
+          found = jsPath != null && src && src.indexOf(jsPath) || -1;
+      if (found != -1 && /.*[.]js.*[?&]c=[01](#|&|$).*/.test(src.substring(found + jsPath.length))) {
+        candidates[n++] = scripts[i];
+      }
+    }
+    if (!candidates.length) {
+      var tag = scripts[scripts.length - 1];
+      if (tag.src) {
+        candidates[0] = tag;
+      }
+    }
+    return candidates;
+  }
+
+  function getInnerText(scriptNode) {
+    var scriptText = '';
+    if (scriptNode.nodeType == DOM_TEXT_NODE || scriptNode.nodeType == DOM_CDATA_SECTION_NODE) {
+      scriptText = scriptNode.nodeValue;
+    } else if (scriptNode.innerText) {
+      scriptText = scriptNode.innerText;
+    } else if (scriptNode.innerHTML) {
+      scriptText = scriptNode.innerHTML;
+    } else if (scriptNode.firstChild) {
+      var content = [];
+      for (var child = scriptNode.firstChild; child; child = child.nextSibling) {
+        content.push(getInnerText(child));
+      }
+      scriptText = content.join('');
+    }
+    return scriptText;
+  }
+
+  function parseConfig(configText) {
+    var config;
+    try {
+      config = (new Function('return (' + configText + '\n)'))();
+    } catch (e) { }
+    if (typeof config === 'object') {
+      return config;
+    }
+    try {
+      config = (new Function('return ({' + configText + '\n})'))();
+    } catch (e) { }
+    return typeof config === 'object' ? config : {};
+  }
+
+  function augmentConfig(baseConfig) {
+    var scripts = getLoadingScript();
+    if (!scripts.length) {
+      return;
+    }
+    for (var i = 0; i < scripts.length; i++) {
+      var scriptText = getInnerText(scripts[i]);
+      var configAugment = parseConfig(scriptText);
+      if (___jsl['f'] && ___jsl['f'].length == 1) {
+        // Single-feature load on current request.
+        // Augmentation adds to just this feature's config if
+        // "short-form" syntax is used ie. skipping top-level feature key.
+        var feature = ___jsl['f'][0];
+        if (!configAugment[feature]) {
+          var newConfig = {};
+          newConfig[___jsl['f'][0]] = configAugment;
+          configAugment = newConfig;
+        }
+      }
+      foldConfig(baseConfig, configAugment);
+
+      var globalConfig = window['___cfg'];
+      if (globalConfig) {
+        foldConfig(baseConfig, globalConfig);
+      }
+    }
+  }
+
+  /**
+   * Iterates through all registered components.
+   * @param {function(string,Object)} processor The processor method.
+   */
+  function forAllComponents(processor) {
+    for (var name in components) {
+      if (components.hasOwnProperty(name)) {
+        var componentList = components[name];
+        for (var i = 0, j = componentList.length; i < j; ++i) {
+          processor(name, componentList[i]);
+        }
+      }
+    }
+  }
+
+  return {
+    /**
+     * Registers a configurable component and its configuration parameters.
+     * Multiple callbacks may be registered for a single component if needed.
+     *
+     * @param {string} component The name of the component to register. Should
+     *     be the same as the fully qualified name of the <Require> feature or
+     *     the name of a fully qualified javascript object reference
+     *     (e.g. "gadgets.io").
+     * @param {Object=} opt_validators Mapping of option name to validation
+     *     functions that take the form function(data) {return isValid(data);}.
+     * @param {function(Object)=} opt_callback A function to be invoked when a
+     *     configuration is registered. If passed, this function will be invoked
+     *     immediately after a call to init has been made. Do not assume that
+     *     dependent libraries have been configured until after init is
+     *     complete. If you rely on this, it is better to defer calling
+     *     dependent libraries until you can be sure that configuration is
+     *     complete. Takes the form function(config), where config will be
+     *     all registered config data for all components. This allows your
+     *     component to read configuration from other components.
+     * @param {boolean=} opt_callOnUpdate Whether the callback shall be call
+     *     on gadgets.config.update() as well.
+     * @member gadgets.config
+     * @name register
+     * @function
+     */
+    register: function(component, opt_validators, opt_callback,
+        opt_callOnUpdate) {
+      var registered = components[component];
+      if (!registered) {
+        registered = [];
+        components[component] = registered;
+      }
+
+      registered.push({
+        validators: opt_validators || {},
+        callback: opt_callback,
+        callOnUpdate: opt_callOnUpdate
+      });
+
+      if (initialized && opt_callback) {
+          opt_callback(configuration);
+      }
+    },
+
+    /**
+     * Retrieves configuration data on demand.
+     *
+     * @param {string=} opt_component The component to fetch. If not provided
+     *     all configuration will be returned.
+     * @return {Object} The requested configuration, or an empty object if no
+     *     configuration has been registered for that component.
+     * @member gadgets.config
+     * @name get
+     * @function
+     */
+    get: function(opt_component) {
+      if (opt_component) {
+        return configuration[opt_component] || {};
+      }
+      return configuration;
+    },
+
+    /**
+     * Initializes the configuration.
+     *
+     * @param {Object} config The full set of configuration data.
+     * @param {boolean=} opt_noValidation True if you want to skip validation.
+     * @throws {Error} If there is a configuration error.
+     * @member gadgets.config
+     * @name init
+     * @function
+     */
+    init: function(config, opt_noValidation) {
+      ___jsl = window['___jsl'] || {};
+      foldConfig(configuration, config);
+      augmentConfig(configuration);
+      var inlineOverride = window['___config'] || {};
+      foldConfig(configuration, inlineOverride);
+      forAllComponents(function(name, component) {
+        var conf = configuration[name];
+        if (conf && !opt_noValidation) {
+          var validators = component.validators;
+          for (var v in validators) {
+            if (validators.hasOwnProperty(v)) {
+              if (!validators[v](conf[v])) {
+                throw new Error('Invalid config value "' + conf[v] +
+                    '" for parameter "' + v + '" in component "' +
+                    name + '"');
+              }
+            }
+          }
+        }
+
+        if (component.callback) {
+          component.callback(configuration);
+        }
+      });
+      initialized = true;
+    },
+
+    /**
+     * Method largely for dev and debugging purposes that
+     * replaces or manually updates feature config.
+     * @param {Object} updateConfig Config object, with keys for features.
+     * @param {boolean} opt_replace true to replace all configuration.
+     */
+    update: function(updateConfig, opt_replace) {
+      // Iterate before changing updateConfig and configuration.
+      var callbacks = [];
+      forAllComponents(function(name, component) {
+        if (updateConfig.hasOwnProperty(name) ||
+            (opt_replace && configuration && configuration[name])) {
+          if (component.callback && component.callOnUpdate) {
+            callbacks.push(component.callback);
+          }
+        }
+      });
+      configuration = opt_replace ? {} : configuration || {};
+      foldConfig(configuration, updateConfig);
+      for (var i = 0, j = callbacks.length; i < j; ++i) {
+        callbacks[i](configuration);
+      }
+    },
+
+    /**
+     * For testing
+     */
+    clear: function() {
+      gadgets.warn('This method is for testing.');
+      var undef;
+      ___jsl = undef;
+      configuration = {};
+      initialized = false;
+    }
+  };
+}();
+} // ! end double inclusion guard
diff --git a/trunk/features/src/main/javascript/features/core.config.base/feature.xml b/trunk/features/src/main/javascript/features/core.config.base/feature.xml
new file mode 100644
index 0000000..4eabb31
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.config.base/feature.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.config.base</name>
+  <dependency>globals</dependency>
+  <dependency>domnode</dependency>
+  <all>
+    <script src="config.js"/>
+    <api>
+      <exports type="js">gadgets.config.register</exports>
+      <exports type="js">gadgets.config.get</exports>
+      <exports type="js">gadgets.config.init</exports>
+      <exports type="js">gadgets.config.update</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.config/feature.xml b/trunk/features/src/main/javascript/features/core.config/feature.xml
new file mode 100644
index 0000000..d7d8d2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.config/feature.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.config</name>
+  <dependency>globals</dependency>
+  <dependency>core.config.base</dependency>
+  <all>
+    <script src="validators.js"/>
+    <api>
+      <exports type="js">gadgets.config.EnumValidator</exports>
+      <exports type="js">gadgets.config.RegExValidator</exports>
+      <exports type="js">gadgets.config.ExistsValidator</exports>
+      <exports type="js">gadgets.config.NonEmptyStringValidator</exports>
+      <exports type="js">gadgets.config.BooleanValidator</exports>
+      <exports type="js">gadgets.config.LikeValidator</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.config/validators.js b/trunk/features/src/main/javascript/features/core.config/validators.js
new file mode 100644
index 0000000..357a33c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.config/validators.js
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+// Defines default validators in a separate file from the rest of the config
+// system, to enable its separability from these.
+
+(function() {
+  /**
+    * Ensures that data is one of a fixed set of items.
+    * Also supports argument sytax: EnumValidator("Dog", "Cat", "Fish");
+    *
+    * @param {Array.<string>} list The list of valid values.
+    */
+  gadgets.config.EnumValidator = function(list) {
+    var listItems = [];
+    if (arguments.length > 1) {
+      for (var i = 0, arg; (arg = arguments[i]); ++i) {
+        listItems.push(arg);
+      }
+    } else {
+      listItems = list;
+    }
+    return function(data) {
+      for (var i = 0, test; (test = listItems[i]); ++i) {
+        if (data === listItems[i]) {
+          return true;
+        }
+      }
+      return false;
+    };
+  };
+
+  /**
+   * Tests the value against a regular expression.
+   * @member gadgets.config
+   */
+  gadgets.config.RegExValidator = function(re) {
+    return function(data) {
+      return re.test(data);
+    };
+  };
+
+  /**
+   * Validates that a value was provided.
+   * @param {*} data
+   */
+  gadgets.config.ExistsValidator = function(data) {
+    return typeof data !== 'undefined';
+  };
+
+  /**
+   * Validates that a value is a non-empty string.
+   * @param {*} data
+   */
+  gadgets.config.NonEmptyStringValidator = function(data) {
+    return typeof data === 'string' && data.length > 0;
+  };
+
+  /**
+   * Validates that the value is a boolean.
+   * @param {*} data
+   */
+  gadgets.config.BooleanValidator = function(data) {
+    return typeof data === 'boolean';
+  };
+
+  /**
+   * Similar to the ECMAScript 4 virtual typing system, ensures that
+   * whatever object was passed in is "like" the existing object.
+   * Doesn't actually do type validation though, but instead relies
+   * on other validators.
+   *
+   * This can be used recursively as well to validate sub-objects.
+   *
+   * @example
+   *
+   *  var validator = new gadgets.config.LikeValidator(
+   *    "booleanField" : gadgets.config.BooleanValidator,
+   *    "regexField" : new gadgets.config.RegExValidator(/foo.+/);
+   *  );
+   *
+   *
+   * @param {Object} test The object to test against.
+   */
+  gadgets.config.LikeValidator = function(test) {
+    return function(data) {
+      for (var member in test) {
+        if (test.hasOwnProperty(member)) {
+          var t = test[member];
+          if (!t(data[member])) {
+            return false;
+          }
+        }
+      }
+      return true;
+    };
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/core.io/feature.xml b/trunk/features/src/main/javascript/features/core.io/feature.xml
new file mode 100644
index 0000000..a7c3278
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.io/feature.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+<!--
+  Required configuration:
+
+  proxyUrl: A url template containing the placeholder "%url%", which will be
+      used for all calls to gadgets.io.getProxyUrl(string), with the value
+      passed in being used as the replacement.
+  jsonProxyUrl: A url pointing to the JSON proxy endpoint, used by
+      gadgets.io.makeRequest. All data passed to this end point will be
+      encoded inside of the POST body.
+-->
+  <name>core.io</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>shindig.auth</dependency>
+  <dependency>core.config.base</dependency>
+  <dependency>core.json</dependency>
+  <dependency>core.util.base</dependency>
+  <dependency>core.util.urlparams</dependency>
+  <dependency>shindig.uri</dependency>
+  <all>
+    <script src="io.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.io.makeRequest</exports>
+      <exports type="js">gadgets.io.makeNonProxiedRequest</exports>
+      <exports type="js">gadgets.io.clearOAuthState</exports>
+      <exports type="js">gadgets.io.encodeValues</exports>
+      <exports type="js">gadgets.io.getProxyUrl</exports>
+      <exports type="js">gadgets.io.RequestParameters</exports>
+      <exports type="js">gadgets.io.MethodType</exports>
+      <exports type="js">gadgets.io.ContentType</exports>
+      <exports type="js">gadgets.io.AuthorizationType</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.io/io.js b/trunk/features/src/main/javascript/features/core.io/io.js
new file mode 100644
index 0000000..f305771
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.io/io.js
@@ -0,0 +1,673 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global ActiveXObject, DOMParser */
+/*global shindig */
+
+/**
+ * @fileoverview Provides remote content retrieval facilities.
+ *     Available to every gadget.
+ */
+
+/**
+ * @class Provides remote content retrieval functions.
+ */
+
+gadgets.io = function() {
+  // Ever incrementing Ajax transaction id
+  var ioTransactionId = 0;
+
+  // Object to store ids for the ajax poll to avoid IE memory leak
+  var ajaxPollQ = {};
+
+  /**
+   * Holds configuration-related data such as proxy urls.
+   */
+  var config = {};
+
+  /**
+   * Holds state for OAuth.
+   */
+  var oauthState;
+
+  /**
+   * Internal facility to create an xhr request.
+   * @return {XMLHttpRequest}
+   */
+  function makeXhr() {
+    var x;
+    if (typeof shindig != 'undefined' &&
+        shindig.xhrwrapper &&
+        shindig.xhrwrapper.createXHR) {
+      return shindig.xhrwrapper.createXHR();
+    } else if (typeof ActiveXObject != 'undefined') {
+      try {
+        x = new ActiveXObject('Msxml2.XMLHTTP');
+        if (!x) {
+          x = new ActiveXObject('Microsoft.XMLHTTP');
+        }
+        return x;
+      } catch (e) {} // An exception will be thrown if ActiveX is disabled
+    }
+
+    // The second construct is for the benefit of jsunit...
+    if (typeof XMLHttpRequest != 'undefined' || window.XMLHttpRequest) {
+      return new window.XMLHttpRequest();
+    }
+    else throw ('no xhr available');
+  }
+
+  /**
+   * Checks the xobj for errors, may call the callback with an error response
+   * if the error is fatal.
+   *
+   * @param {Object} xobj The XHR object to check.
+   * @param {function(Object)} callback The callback to call if the error is fatal.
+   * @return {boolean} true if the xobj is not ready to be processed.
+   */
+  function hadError(xobj, callback) {
+    if (xobj['readyState'] !== 4) {
+      return true;
+    }
+    try {
+      if (xobj['status'] !== 200) {
+        var error = ('' + xobj['status']);
+        if (xobj['responseText']) {
+          error = error + ' ' + xobj['responseText'];
+        }
+        callback({
+          'errors': [error],
+          'rc': xobj['status'],
+          'text': xobj['responseText']
+        });
+        return true;
+      }
+    } catch (e) {
+      callback({
+        'errors': [e['number'] + ' Error not specified'],
+        'rc': e['number'],
+        'text': e['description']
+      });
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Handles non-proxied XHR callback processing.
+   *
+   * @param {string} url
+   * @param {function(Object)} callback
+   * @param {Object} params
+   * @param {Object} xobj
+   */
+  function processNonProxiedResponse(url, callback, params, xobj) {
+    if (hadError(xobj, callback)) {
+      return;
+    }
+    var data = {
+      'body': xobj['responseText']
+    };
+    callback(transformResponseData(params, data));
+  }
+
+  /**
+   * Handles XHR callback processing.
+   *
+   * @param {string} url
+   * @param {function(Object)} callback
+   * @param {Object} params
+   * @param {Object} xobj
+   */
+  function processResponse(url, callback, params, xobj) {
+    if (hadError(xobj, callback)) {
+      return;
+    }
+    var txt = xobj['responseText'];
+
+    var UNPARSEABLE_CRUFT = config['unparseableCruft'];
+    // remove unparseable cruft used to prevent cross-site script inclusion
+    var offset = txt.indexOf(UNPARSEABLE_CRUFT) + UNPARSEABLE_CRUFT.length;
+
+    // If no cruft then just return without a callback - avoid JS errors
+    // TODO craft an error response?
+    if (offset < UNPARSEABLE_CRUFT.length) return;
+    txt = txt.substr(offset);
+
+    // We are using eval directly here  because the outer response comes from a
+    // trusted source, and json parsing is slow in IE.
+    var data = eval('(' + txt + ')');
+    data = data[url];
+    // Save off any transient OAuth state the server wants back later.
+    if (data['oauthState']) {
+      oauthState = data['oauthState'];
+    }
+    // Update the security token if the server sent us a new one
+    if (data['st']) {
+      shindig.auth.updateSecurityToken(data['st']);
+    }
+    callback(transformResponseData(params, data));
+  }
+
+  /**
+   * @param {Object} params
+   * @param {Object} data
+   * @return {Object}
+   */
+
+  function transformResponseData(params, data) {
+    // Sometimes rc is not present, generally when used
+    // by jsonrpccontainer, so assume 200 in its absence.
+    var resp = {
+      'text': data['body'],
+      'rc': data['rc'] || 200,
+      'headers': data['headers'],
+      'oauthApprovalUrl': data['oauthApprovalUrl'],
+      'oauthError': data['oauthError'],
+      'oauthErrorText': data['oauthErrorText'],
+      'oauthErrorTrace': data['oauthErrorTrace'],
+      'oauthErrorUri': data['oauthErrorUri'],
+      'oauthErrorExplanation': data['oauthErrorExplanation'],
+      'errors': []
+    };
+
+    if (resp['rc'] < 200 || resp['rc'] >= 400) {
+      resp['errors'] = [resp['rc'] + ' Error'];
+    } else if (resp['text']) {
+      if (resp['rc'] >= 300 && resp['rc'] < 400) {
+        // Redirect pages will usually contain arbitrary
+        // HTML which will fail during parsing, inadvertently
+        // causing a 500 response. Thus we treat as text.
+        params['CONTENT_TYPE'] = 'TEXT';
+      }
+      switch (params['CONTENT_TYPE']) {
+        case 'JSON':
+        case 'FEED':
+          resp['data'] = gadgets.json.parse(resp.text);
+          if (!resp['data']) {
+            resp['errors'].push('500 Failed to parse JSON');
+            resp['rc'] = 500;
+            resp['data'] = null;
+          }
+          break;
+        case 'DOM':
+          var dom;
+          if (typeof DOMParser != 'undefined') {
+            var parser = new DOMParser();
+            dom = parser.parseFromString(resp['text'], 'text/xml');
+            if ('parsererror' === dom.documentElement.nodeName) {
+              resp['errors'].push('500 Failed to parse XML');
+              resp['rc'] = 500;
+            } else {
+              resp['data'] = dom;
+            }
+          } else if (typeof ActiveXObject != 'undefined') {
+            dom = new ActiveXObject('Microsoft.XMLDOM');
+            dom.async = false;
+            dom.validateOnParse = false;
+            dom.resolveExternals = false;
+            if (!dom.loadXML(resp['text'])) {
+              resp['errors'].push('500 Failed to parse XML');
+              resp['rc'] = 500;
+            } else {
+              resp['data'] = dom;
+            }
+          } else {
+            resp['errors'].push('500 Failed to parse XML because no DOM parser was available');
+            resp['rc'] = 500;
+          }
+          break;
+        default:
+          resp['data'] = resp['text'];
+          break;
+      }
+    }
+    return resp;
+  }
+
+  /**
+   * Sends an XHR post or get request
+   *
+   * @param {string} realUrl The url to fetch data from that was requested by the gadget.
+   * @param {string} proxyUrl The url to proxy through.
+   * @param {function()} callback The function to call once the data is fetched.
+   * @param {Object} paramData The params to use when processing the response.
+   * @param {string} method
+   * @param {function(string,function(Object),Object,Object)}
+   *     processResponseFunction The function that should process the
+   *     response from the sever before calling the callback.
+   * @param {Object=} opt_headers - Optional headers including a Content-Type that defaults to
+   *     'application/x-www-form-urlencoded'.
+   */
+  function makeXhrRequest(realUrl, proxyUrl, callback, paramData, method,
+      params, processResponseFunction, opt_headers) {
+    var xhr = makeXhr();
+
+    if (proxyUrl.indexOf('//') == 0) {
+      proxyUrl = document.location.protocol + proxyUrl;
+    }
+
+    xhr.open(method, proxyUrl, true);
+    if (callback) {
+      var closureCallback = gadgets.util.makeClosure(null, processResponseFunction, realUrl,
+        callback, params, xhr);
+
+      // check for alternate ajax for onreadystatechange event handler
+      var shouldPoll = gadgets.util.shouldPollXhrReadyStateChange();
+      if(shouldPoll) {
+        handleReadyState(xhr, closureCallback);
+      }
+      else {
+        xhr.onreadystatechange = closureCallback;
+      }
+    }
+
+    if (typeof opt_headers === 'string') {
+      // This turned out to come directly from a public API, so we need to
+      // keep compatibility...
+      contentType = opt_headers;
+      opt_headers = {};
+    }
+    var headers = opt_headers || {};
+
+    if (paramData !== null) {
+      var contentTypeHeader = 'Content-Type';
+      var contentType = 'application/x-www-form-urlencoded';
+      if (!headers[contentTypeHeader]) headers[contentTypeHeader] = contentType;
+    }
+
+    for (var headerName in headers) {
+      xhr.setRequestHeader(headerName, headers[headerName]);
+    }
+
+    xhr.send(paramData);
+  }
+
+  /**
+    * Helper function to use poll setInterval to call the callback for Ajax to avoid
+    * memory leak in certain browsers (eg: IE7) due to circular linking.
+    *
+    * The function  will create  interval polling to poll the XHR object's readyState
+    * property instead of binding a callback to the onreadystatechange event.
+    *
+    * @param {xhr} The Ajax object
+    * @param {function} The callback function for the Ajax call
+    * @return void
+    */
+    function handleReadyState(xhr, callback) {
+      var tempTid = ioTransactionId;
+      var pollInterval = config['xhrPollIntervalMs'] || 50;
+      ajaxPollQ[tempTid] = window.setInterval(
+        function() {
+          if(xhr && xhr.readyState === 4) {
+            // Clear the polling interval for the transaction and remove
+            // the reference from ajaxPollQ
+            window.clearInterval(ajaxPollQ[tempTid]);
+            delete ajaxPollQ[tempTid];
+
+            // call the callback
+            if(callback) {
+              callback();
+            }
+          }
+        }, pollInterval);
+
+      ioTransactionId++;
+    }
+
+  /**
+   * Satisfy a request with data that is prefetched as per the gadget Preload
+   * directive. The preloader will only satisfy a request for a specific piece
+   * of content once.
+   *
+   * @param {Object} postData The definition of the request to be executed by the proxy.
+   * @param {Object} params The params to use when processing the response.
+   * @param {function(Object)} callback The function to call once the data is fetched.
+   * @return {boolean} true if the request can be satisfied by the preloaded
+   *         content false otherwise.
+   */
+  function respondWithPreload(postData, params, callback) {
+    if (gadgets.io.preloaded_ && postData.httpMethod === 'GET') {
+      for (var i = 0; i < gadgets.io.preloaded_.length; i++) {
+        var preload = gadgets.io.preloaded_[i];
+        if (preload && (preload.id === postData.url)) {
+          // Only satisfy once
+          delete gadgets.io.preloaded_[i];
+
+          if (preload['rc'] !== 200) {
+            callback({'rc': preload['rc'], 'errors': [preload['rc'] + ' Error']});
+          } else {
+            if (preload['oauthState']) {
+              oauthState = preload['oauthState'];
+            }
+            var resp = {
+              'body': preload['body'],
+              'rc': preload['rc'],
+              'headers': preload['headers'],
+              'oauthApprovalUrl': preload['oauthApprovalUrl'],
+              'oauthError': preload['oauthError'],
+              'oauthErrorText': preload['oauthErrorText'],
+              'oauthErrorTrace': preload['oauthErrorTrace'],
+              'oauthErrorUri': preload['oauthErrorUri'],
+              'oauthErrorExplanation': preload['oauthErrorExplanation'],
+              'errors': []
+            };
+            callback(transformResponseData(params, resp));
+          }
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
+   * @param {Object} configuration Configuration settings.
+   * @private
+   */
+  function init(configuration) {
+    config = configuration['core.io'] || {};
+  }
+
+  gadgets.config.register('core.io', null, init);
+
+  return /** @scope gadgets.io */ {
+    /**
+     * Fetches content from the provided URL and feeds that content into the
+     * callback function.
+     *
+     * Example:
+     * <pre>
+     * gadgets.io.makeRequest(url, fn,
+     *    {contentType: gadgets.io.ContentType.FEED});
+     * </pre>
+     *
+     * @param {string} url The URL where the content is located.
+     * @param {function(Object)} callback The function to call with the data from
+     *     the URL once it is fetched.
+     * @param {Object.<gadgets.io.RequestParameters, Object>=} opt_params
+     *     Additional
+     *     <a href="gadgets.io.RequestParameters.html">parameters</a>
+     *     to pass to the request.
+     *
+     * @member gadgets.io
+     */
+    makeRequest: function(url, callback, opt_params) {
+      // TODO: This method also needs to respect all members of
+      // gadgets.io.RequestParameters, and validate them.
+
+      var params = opt_params || {};
+
+      var httpMethod = params['METHOD'] || 'GET';
+      var refreshInterval = params['REFRESH_INTERVAL'];
+
+      // Check if authorization is requested
+      var auth, st;
+      if (params['AUTHORIZATION'] && params['AUTHORIZATION'] !== 'NONE') {
+        auth = params['AUTHORIZATION'].toLowerCase();
+        st = shindig.auth.getSecurityToken();
+      }
+
+      // Include owner information?
+      var signOwner = true;
+      if (typeof params['SIGN_OWNER'] !== 'undefined') {
+        signOwner = params['SIGN_OWNER'];
+      }
+
+      // Include viewer information?
+      var signViewer = true;
+      if (typeof params['SIGN_VIEWER'] !== 'undefined') {
+        signViewer = params['SIGN_VIEWER'];
+      }
+
+      var headers = params['HEADERS'] || {};
+      if (httpMethod === 'POST' && !headers['Content-Type']) {
+        headers['Content-Type'] = 'application/x-www-form-urlencoded';
+      }
+
+      var urlParams = gadgets.util.getUrlParameters();
+
+      var paramData = {
+        'url': url,
+        'httpMethod': httpMethod,
+        'headers': gadgets.io.encodeValues(headers, false),
+        'postData': params['POST_DATA'] || '',
+        'authz': auth || '',
+        'st': st || '',
+        'contentType': params['CONTENT_TYPE'] || 'TEXT',
+        'numEntries': params['NUM_ENTRIES'] || '3',
+        'getSummaries': !!params['GET_SUMMARIES'],
+        'signOwner': signOwner,
+        'signViewer': signViewer,
+        'gadget': urlParams['url'],
+        'container': urlParams['container'] || urlParams['synd'] || 'default',
+        // should we bypass gadget spec cache (e.g. to read OAuth provider URLs)
+        'bypassSpecCache': gadgets.util.getUrlParameters()['nocache'] || '',
+        'getFullHeaders': !!params['GET_FULL_HEADERS']
+      };
+
+      // OAuth goodies
+      if (auth === 'oauth' || auth === 'signed' || auth === 'oauth2') {
+        if (gadgets.io.oauthReceivedCallbackUrl_) {
+          paramData['OAUTH_RECEIVED_CALLBACK'] = gadgets.io.oauthReceivedCallbackUrl_;
+          gadgets.io.oauthReceivedCallbackUrl_ = null;
+        }
+        paramData['oauthState'] = oauthState || '';
+        // Just copy the OAuth parameters into the req to the server
+        for (var opt in params) {
+          if (params.hasOwnProperty(opt)) {
+            if (opt.indexOf('OAUTH_') === 0 || opt === 'code') {
+              paramData[opt] = params[opt];
+            }
+          }
+        }
+      }
+
+      // Security token may have been set above
+      st = st || shindig.auth.getSecurityToken();
+      var opt_headers = st ? { 'X-Shindig-ST' : st } : {};
+
+      var proxyUrl = config['jsonProxyUrl'].replace('%host%', document.location.host);
+
+      // FIXME -- processResponse is not used in call
+      if (!respondWithPreload(paramData, params, callback)) {
+        if (httpMethod == 'GET' && typeof(refreshInterval) != 'undefined') {
+            paramData['refresh'] = refreshInterval; // gadget requested cache override.
+        }
+
+        if (httpMethod === 'GET' && !paramData['authz']) {
+          var extraparams = '?' + gadgets.io.encodeValues(paramData);
+          makeXhrRequest(url, proxyUrl + extraparams, callback,
+              null, 'GET', params, processResponse, opt_headers);
+        } else {
+          var extraparams = gadgets.io.encodeValues(paramData);
+          makeXhrRequest(url, proxyUrl, callback,
+              extraparams, 'POST', params,
+              processResponse, opt_headers);
+        }
+      }
+    },
+
+    /**
+     * @param {string} relativeUrl url to fetch via xhr.
+     * @param callback callback to call when response is received or for error.
+     * @param {Object=} opt_params
+     * @param {Object=} opt_headers
+     *
+     */
+    makeNonProxiedRequest: function(relativeUrl, callback, opt_params, opt_headers) {
+      var params = opt_params || {};
+      makeXhrRequest(relativeUrl, relativeUrl, callback, params['POST_DATA'],
+          params['METHOD'], params, processNonProxiedResponse, opt_headers);
+    },
+
+    /**
+     * Used to clear out the oauthState, for testing only.
+     *
+     * @private
+     */
+    clearOAuthState: function() {
+      oauthState = undefined;
+    },
+
+    /**
+     * Converts an input object into a URL-encoded data string.
+     * (key=value&amp;...)
+     *
+     * @param {Object} fields The post fields you wish to encode.
+     * @param {boolean=} opt_noEscaping An optional parameter specifying whether
+     *     to turn off escaping of the parameters. Defaults to false.
+     * @return {string} The processed post data in www-form-urlencoded format.
+     *
+     * @member gadgets.io
+     */
+    encodeValues: function(fields, opt_noEscaping) {
+      var escape = !opt_noEscaping;
+
+      var buf = [];
+      var first = false;
+      for (var i in fields) {
+        if (fields.hasOwnProperty(i) && !/___$/.test(i)) {
+          if (!first) {
+            first = true;
+          } else {
+            buf.push('&');
+          }
+          buf.push(escape ? encodeURIComponent(i) : i);
+          buf.push('=');
+          buf.push(escape ? encodeURIComponent(fields[i]) : fields[i]);
+        }
+      }
+      return buf.join('');
+    },
+
+    /**
+     * Gets the proxy version of the passed-in URL.
+     *
+     * @param {string} url The URL to get the proxy URL for.
+     * @param {Object.<gadgets.io.RequestParameters, Object>=} opt_params Optional Parameter Object.
+     *     The following properties are supported:
+     *       .REFRESH_INTERVAL The number of seconds that this
+     *           content should be cached.  Defaults to 3600.
+     *
+     * @return {string} The proxied version of the URL.
+     * @member gadgets.io
+     */
+    getProxyUrl: function(url, opt_params) {
+      var proxyUrl = config['proxyUrl'];
+      if (!proxyUrl) {
+        return proxyUrl;
+      }
+      var params = opt_params || {};
+      var refresh = params['REFRESH_INTERVAL'];
+      if (typeof refresh == 'undefined') {
+        refresh = '3600';
+      }
+
+      var urlParams = gadgets.util.getUrlParameters();
+      var st = shindig.auth.getSecurityToken();
+      var authz = params[gadgets.io.RequestParameters.AUTHORIZATION];
+      var serviceName = params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME];
+
+      var rewriteMimeParam =
+          params['rewriteMime'] ? '&rewriteMime=' + encodeURIComponent(params['rewriteMime']) : '';
+      var authParam = '';
+      if(authz) {
+        if(authz == gadgets.io.AuthorizationType.OAUTH || authz == gadgets.io.AuthorizationType.OAUTH2) {
+          authParam = '&authz=' + authz.toLowerCase() + '&st=' + encodeURIComponent(st)
+            + '&OAUTH_SERVICE_NAME=' + encodeURIComponent(serviceName);
+        } else {
+          authParam = '&authz=' + authz.toLowerCase();
+        }
+      }
+
+      var uri = shindig.uri(url);
+      var path = uri.getPath();
+      var fileName = "";
+      var lSlash = path.lastIndexOf('/');
+      if (lSlash !== -1) {
+        fileName = path.substring(lSlash); // include the slash
+      }
+
+      var ret = proxyUrl.replace('%url%', encodeURIComponent(url)).
+          replace('%host%', document.location.host).
+          replace('%rawurl%', url).
+          replace('%filename%', fileName).
+          replace('%refresh%', encodeURIComponent(refresh)).
+          replace('%gadget%', encodeURIComponent(urlParams['url'])).
+          replace('%container%', encodeURIComponent(urlParams['container'] || urlParams['synd'] || 'default')).
+          replace('%authz%', authParam).
+          replace('%rewriteMime%', rewriteMimeParam);
+      if (ret.indexOf('//') == 0) {
+        ret = window.location.protocol + ret;
+      }
+      return ret;
+    },
+
+    /**
+     * @private
+     */
+    processResponse_: processResponse
+  };
+}();
+
+/**
+ * @const
+ **/
+gadgets.io.RequestParameters = gadgets.util.makeEnum([
+  'ALIAS',
+  'METHOD',
+  'CONTENT_TYPE',
+  'POST_DATA',
+  'HEADERS',
+  'AUTHORIZATION',
+  'NUM_ENTRIES',
+  'GET_SUMMARIES',
+  'GET_FULL_HEADERS',
+  'REFRESH_INTERVAL',
+  'SIGN_OWNER',
+  'SIGN_VIEWER',
+  'OAUTH_SERVICE_NAME',
+  'OAUTH_USE_TOKEN',
+  'OAUTH_TOKEN_NAME',
+  'OAUTH_REQUEST_TOKEN',
+  'OAUTH_REQUEST_TOKEN_SECRET',
+  'OAUTH_RECEIVED_CALLBACK'
+]);
+
+/**
+ * @const
+ */
+gadgets.io.MethodType = gadgets.util.makeEnum([
+  'GET', 'POST', 'PUT', 'DELETE', 'HEAD'
+]);
+
+/**
+ * @const
+ */
+gadgets.io.ContentType = gadgets.util.makeEnum([
+  'TEXT', 'DOM', 'JSON', 'FEED'
+]);
+
+/**
+ * @const
+ */
+gadgets.io.AuthorizationType = gadgets.util.makeEnum([
+  'NONE', 'SIGNED', 'OAUTH', "OAUTH2"
+]);
diff --git a/trunk/features/src/main/javascript/features/core.io/taming.js b/trunk/features/src/main/javascript/features/core.io/taming.js
new file mode 100644
index 0000000..7d2c807
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.io/taming.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.io.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.io, 'encodeValues'],
+    [gadgets.io, 'getProxyUrl'],
+    [gadgets.io, 'makeRequest']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/core.json/feature.xml b/trunk/features/src/main/javascript/features/core.json/feature.xml
new file mode 100644
index 0000000..7a1086e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.json/feature.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.json</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <all>
+    <script src="json-native.js"/>
+    <script src="json-jsimpl.js"/>
+    <script src="json-flatten.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.json.flatten</exports>
+      <exports type="js">gadgets.json.parse</exports>
+      <exports type="js">gadgets.json.stringify</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.json/json-flatten.js b/trunk/features/src/main/javascript/features/core.json/json-flatten.js
new file mode 100644
index 0000000..7d92a4b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.json/json-flatten.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Flatten an object to a stringified values. Useful for dealing with
+ * json->querystring transformations. Note: not in official specification yet
+ *
+ * @param {Object} obj
+ * @return {Object} object with only string values.
+ */
+gadgets.json.flatten = function(obj) {
+  var flat = {};
+
+  if (obj === null || typeof obj == 'undefined') return flat;
+
+  for (var k in obj) {
+    if (obj.hasOwnProperty(k)) {
+      var value = obj[k];
+      if (null === value || typeof value == 'undefined') {
+        continue;
+      }
+      flat[k] = (typeof value === 'string') ? value : gadgets.json.stringify(value);
+    }
+  }
+  return flat;
+};
diff --git a/trunk/features/src/main/javascript/features/core.json/json-jsimpl.js b/trunk/features/src/main/javascript/features/core.json/json-jsimpl.js
new file mode 100644
index 0000000..21316cf
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.json/json-jsimpl.js
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ * The global object gadgets.json contains two methods.
+ *
+ * gadgets.json.stringify(value) takes a JavaScript value and produces a JSON
+ * text. The value must not be cyclical.
+ *
+ * gadgets.json.parse(text) takes a JSON text and produces a JavaScript value.
+ * It will return false if there is an error.
+ */
+
+/**
+ * @static
+ * @class Provides operations for translating objects to and from JSON.
+ * @name gadgets.json
+ */
+
+/**
+ * JavaScript-based implementation when window.JSON is not present.
+ * Port of the public domain JSON library by Douglas Crockford.
+ * See: http://www.json.org/json2.js
+ */
+if (!(window.JSON && window.JSON.parse && window.JSON.stringify)) {
+  /**
+   * Port of the public domain JSON library by Douglas Crockford.
+   * See: http://www.json.org/json2.js
+   */
+  gadgets.json = function() {
+
+    /**
+     * Formats integers to 2 digits.
+     * @param {number} n number to format.
+     * @return {string} the formatted number.
+     * @private
+     */
+    function f(n) {
+      return n < 10 ? '0' + n : n;
+    }
+
+    Date.prototype.toJSON = function() {
+      return [this.getUTCFullYear(), '-',
+        f(this.getUTCMonth() + 1), '-',
+        f(this.getUTCDate()), 'T',
+        f(this.getUTCHours()), ':',
+        f(this.getUTCMinutes()), ':',
+        f(this.getUTCSeconds()), 'Z'].join('');
+    };
+
+    // table of character substitutions
+    /**
+     * @const
+     * @enum {string}
+     */
+    var m = {
+      '\b': '\\b',
+      '\t': '\\t',
+      '\n': '\\n',
+      '\f': '\\f',
+      '\r': '\\r',
+      '"' : '\\"',
+      '\\': '\\\\'
+    };
+
+    /**
+     * Converts a json object into a string.
+     * @param {*} value
+     * @return {string}
+     * @member gadgets.json
+     */
+    function stringify(value) {
+      var a,          // The array holding the partial texts.
+          i,          // The loop counter.
+          k,          // The member key.
+          l,          // Length.
+          r = /[\"\\\x00-\x1f\x7f-\x9f]/g,
+          v;          // The member value.
+
+      switch (typeof value) {
+        case 'string':
+          // If the string contains no control characters, no quote characters, and no
+          // backslash characters, then we can safely slap some quotes around it.
+          // Otherwise we must also replace the offending characters with safe ones.
+          return r.test(value) ?
+              '"' + value.replace(r, function(a) {
+                var c = m[a];
+                if (c) {
+                  return c;
+                }
+                c = a.charCodeAt();
+                return '\\u00' + Math.floor(c / 16).toString(16) +
+                   (c % 16).toString(16);
+              }) + '"' : '"' + value + '"';
+        case 'number':
+          // JSON numbers must be finite. Encode non-finite numbers as null.
+          return isFinite(value) ? String(value) : 'null';
+        case 'boolean':
+        case 'null':
+          return String(value);
+        case 'object':
+          // Due to a specification blunder in ECMAScript,
+          // typeof null is 'object', so watch out for that case.
+          if (!value) {
+            return 'null';
+          }
+          // toJSON check removed; re-implement when it doesn't break other libs.
+          a = [];
+          if (typeof value.length === 'number' &&
+              !value.propertyIsEnumerable('length')) {
+            // The object is an array. Stringify every element. Use null as a
+            // placeholder for non-JSON values.
+            l = value.length;
+            for (i = 0; i < l; i += 1) {
+              a.push(stringify(value[i]) || 'null');
+            }
+            // Join all of the elements together and wrap them in brackets.
+            return '[' + a.join(',') + ']';
+          }
+          // Otherwise, iterate through all of the keys in the object.
+          for (k in value) {
+            if (/___$/.test(k))
+              continue;
+            if (value.hasOwnProperty(k)) {
+              if (typeof k === 'string') {
+                v = stringify(value[k]);
+                if (v) {
+                  a.push(stringify(k) + ':' + v);
+                }
+              }
+            }
+          }
+          // Join all of the member texts together and wrap them in braces.
+          return '{' + a.join(',') + '}';
+      }
+      return '';
+    }
+
+    return {
+      stringify: stringify,
+      parse: function(text) {
+        // Parsing happens in three stages. In the first stage, we run the text against
+        // regular expressions that look for non-JSON patterns. We are especially
+        // concerned with '()' and 'new' because they can cause invocation, and '='
+        // because it can cause mutation. But just to be safe, we want to reject all
+        // unexpected forms.
+
+        // We split the first stage into 4 regexp operations in order to work around
+        // crippling inefficiencies in IE's and Safari's regexp engines. First we
+        // replace all backslash pairs with '@' (a non-JSON character). Second, we
+        // replace all simple value tokens with ']' characters. Third, we delete all
+        // open brackets that follow a colon or comma or that begin the text. Finally,
+        // we look to see that the remaining characters are only whitespace or ']' or
+        // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+        if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/b-u]/g, '@').
+            replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+            replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+          return eval('(' + text + ')');
+        }
+        // If the text is not JSON parseable, then return false.
+
+        return false;
+      }
+    };
+  }();
+}
diff --git a/trunk/features/src/main/javascript/features/core.json/json-native.js b/trunk/features/src/main/javascript/features/core.json/json-native.js
new file mode 100644
index 0000000..076cd24
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.json/json-native.js
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ * The global object gadgets.json contains two methods.
+ *
+ * gadgets.json.stringify(value) takes a JavaScript value and produces a JSON
+ * text. The value must not be cyclical.
+ *
+ * gadgets.json.parse(text) takes a JSON text and produces a JavaScript value.
+ * It will return false if there is an error.
+ */
+
+/**
+ * @static
+ * @class Provides operations for translating objects to and from JSON.
+ * @name gadgets.json
+ */
+
+/**
+ * Just wrap native JSON calls when available.
+ */
+if (window.JSON && window.JSON.parse && window.JSON.stringify) {
+  // HTML5 implementation, or already defined.
+  // Not a direct alias as the opensocial specification disagrees with the HTML5 JSON spec.
+  // JSON says to throw on parse errors and to support filtering functions. OS does not.
+  gadgets.json = (function() {
+    var endsWith___ = /___$/;
+
+    function getOrigValue(key, value) {
+      var origValue = this[key];
+      return origValue;
+    }
+
+    return {
+      /* documented below */
+      parse: function(str) {
+        try {
+          return window.JSON.parse(str);
+        } catch (e) {
+          return false;
+        }
+      },
+      /* documented below */
+      stringify: function(obj) {
+        var orig = window.JSON.stringify;
+        function patchedStringify(val) {
+          return orig.call(this, val, getOrigValue);
+        }
+        var stringifyFn = (Array.prototype.toJSON && orig([{x:1}]) === "\"[{\\\"x\\\": 1}]\"") ?
+            patchedStringify : orig;
+        try {
+          return stringifyFn(obj, function(k,v) {
+            return !endsWith___.test(k) ? v : void 0;
+          });
+        } catch (e) {
+          return null;
+        }
+      }
+    };
+  })();
+}
diff --git a/trunk/features/src/main/javascript/features/core.json/taming.js b/trunk/features/src/main/javascript/features/core.json/taming.js
new file mode 100644
index 0000000..93c29d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.json/taming.js
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.tamesTo(gadgets.json.stringify, caja___.getJSON().stringify);
+  caja___.tamesTo(gadgets.json.parse, caja___.getJSON().parse);
+});
diff --git a/trunk/features/src/main/javascript/features/core.legacy/feature.xml b/trunk/features/src/main/javascript/features/core.legacy/feature.xml
new file mode 100644
index 0000000..d25526b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.legacy/feature.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.legacy</name>
+  <dependency>globals</dependency>
+  <dependency>core.json</dependency>
+  <dependency>core.io</dependency>
+  <dependency>core.prefs</dependency>
+  <dependency>core.util</dependency>
+  <gadget>
+    <script src="legacy.js"/>
+    <api>
+      <exports type="js">JSON</exports>
+      <exports type="js">_IG_Prefs</exports>
+      <exports type="js">_IG_Fetch_wrapper</exports>
+      <exports type="js">_IG_FetchContent</exports>
+      <exports type="js">_IG_FetchXmlContent</exports>
+      <exports type="js">_IG_FetchFeedAsJSON</exports>
+      <exports type="js">_IG_GetCachedUrl</exports>
+      <exports type="js">_IG_GetImageUrl</exports>
+      <exports type="js">_IG_GetImage</exports>
+      <exports type="js">_IG_RegisterOnloadHandler</exports>
+      <exports type="js">_IG_Callback</exports>
+      <exports type="js">_IG_AddDOMEventHandler</exports>
+      <exports type="js">_gel</exports>
+      <exports type="js">_gelstn</exports>
+      <exports type="js">_gelsbyregex</exports>
+      <exports type="js">_esc</exports>
+      <exports type="js">_unesc</exports>
+      <exports type="js">_hesc</exports>
+      <exports type="js">_striptags</exports>
+      <exports type="js">_trim</exports>
+      <exports type="js">_toggle</exports>
+      <exports type="js">_uid</exports>
+      <exports type="js">_min</exports>
+      <exports type="js">_max</exports>
+      <exports type="js">_exportSymbols</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.legacy/legacy.js b/trunk/features/src/main/javascript/features/core.legacy/legacy.js
new file mode 100644
index 0000000..4ac7449
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.legacy/legacy.js
@@ -0,0 +1,381 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview All functions in this file should be treated as deprecated legacy routines.
+ * Gadget authors are explicitly discouraged from using any of them.
+ */
+
+var JSON = window.JSON || gadgets.json;
+
+/**
+ * @deprecated
+ */
+var _IG_Prefs = (function() {
+  var instance = null;
+
+  var _IG_Prefs = function() {
+    if (!instance) {
+      instance = new gadgets.Prefs();
+      instance.setDontEscape_();
+    }
+    return instance;
+  };
+
+  _IG_Prefs._parseURL = gadgets.Prefs.parseUrl;
+
+  return _IG_Prefs;
+})();
+
+function _IG_Fetch_wrapper(callback, obj) {
+  callback(obj.data ? obj.data : '');
+}
+
+/**
+ * @deprecated
+ */
+function _IG_FetchContent(url, callback, opt_params) {
+  var params = opt_params || {};
+  // This is really the only legacy parameter documented
+  // at http://code.google.com/apis/gadgets/docs/remote-content.html#Params
+  if (params.refreshInterval) {
+    params['REFRESH_INTERVAL'] = params.refreshInterval;
+  } else {
+    params['REFRESH_INTERVAL'] = 3600;
+  }
+  // Other params, such as POST_DATA, were supported in lower case.
+  // Upper-case all param keys as a convenience, since all valid values
+  // are uppercased.
+  for (var param in params) {
+    var pvalue = params[param];
+    delete params[param];
+    params[param.toUpperCase()] = pvalue;
+  }
+  var cb = gadgets.util.makeClosure(null, _IG_Fetch_wrapper, callback);
+  gadgets.io.makeRequest(url, cb, params);
+}
+
+/**
+ * @deprecated
+ */
+function _IG_FetchXmlContent(url, callback, opt_params) {
+  var params = opt_params || {};
+  if (params.refreshInterval) {
+    params['REFRESH_INTERVAL'] = params.refreshInterval;
+  } else {
+    params['REFRESH_INTERVAL'] = 3600;
+  }
+  params.CONTENT_TYPE = 'DOM';
+  var cb = gadgets.util.makeClosure(null, _IG_Fetch_wrapper, callback);
+  gadgets.io.makeRequest(url, cb, params);
+}
+
+
+/**
+ * @deprecated
+ */
+function _IG_FetchFeedAsJSON(url, callback, numItems, getDescriptions,
+                             opt_params) {
+  var params = opt_params || {};
+  params.CONTENT_TYPE = 'FEED';
+  params.NUM_ENTRIES = numItems;
+  params.GET_SUMMARIES = getDescriptions;
+  gadgets.io.makeRequest(url,
+      function(resp) {
+        // special case error reporting for back-compatibility
+        // see http://code.google.com/apis/gadgets/docs/legacy/remote-content.html#Fetch_JSON
+        resp.data = resp.data || {};
+        if (resp.errors && resp.errors.length > 0) {
+          resp.data.ErrorMsg = resp.errors[0];
+        }
+        if (resp.data.link) {
+          resp.data.URL = url;
+        }
+        if (resp.data.title) {
+          resp.data.Title = resp.data.title;
+        }
+        if (resp.data.description) {
+          resp.data.Description = resp.data.description;
+        }
+        if (resp.data.link) {
+          resp.data.Link = resp.data.link;
+        }
+        if (resp.data.items && resp.data.items.length > 0) {
+          resp.data.Entry = resp.data.items;
+          for (var index = 0; index < resp.data.Entry.length; ++index) {
+            var entry = resp.data.Entry[index];
+            entry.Title = entry.title;
+            entry.Link = entry.link;
+            entry.Summary = entry.summary || entry.description;
+            entry.Date = entry.pubDate;
+          }
+        }
+        for (var ix = 0; ix < resp.data.Entry.length; ++ix) {
+          var entry = resp.data.Entry[ix];
+          entry.Date = (entry.Date / 1000);  // response in sec, not ms
+        }
+        // for Gadgets back-compatibility, return the feed obj directly
+        callback(resp.data);
+      }, params);
+}
+
+/**
+ * @param {string} url
+ * @param {Object=} opt_params
+ * @deprecated
+ */
+function _IG_GetCachedUrl(url, opt_params) {
+  var params = opt_params || {};
+  params['REFRESH_INTERVAL'] = 3600;
+  if (params.refreshInterval) {
+    params['REFRESH_INTERVAL'] = params.refreshInterval;
+  }
+  return gadgets.io.getProxyUrl(url, params);
+}
+/**
+ * @param {string} url
+ * @param {Object=} opt_params
+ * @deprecated
+ */
+function _IG_GetImageUrl(url, opt_params) {
+  return _IG_GetCachedUrl(url, opt_params);
+}
+
+/**
+ * @param {string} url
+ * @return {Element}
+ * @deprecated
+ */
+function _IG_GetImage(url) {
+  var img = document.createElement('img');
+  img.src = _IG_GetCachedUrl(url);
+  return img;
+}
+
+
+/**
+ * @deprecated
+ */
+function _IG_RegisterOnloadHandler(callback) {
+  gadgets.util.registerOnLoadHandler(callback);
+}
+
+/**
+ * _IG_Callback takes the arguments in the scope the callback is executed and
+ * places them first in the argument array. MakeClosure takes the arguments
+ * from the scope at callback construction and pushes them first in the array
+ *
+ * @deprecated
+ */
+function _IG_Callback(handler_func, var_args) {
+  var orig_args = arguments;
+  return function() {
+    var combined_args = Array.prototype.slice.call(arguments);
+    // call the handler with all args combined
+    handler_func.apply(null,
+        combined_args.concat(Array.prototype.slice.call(orig_args, 1)));
+  };
+}
+
+var _args = gadgets.util.getUrlParameters;
+
+/**
+ * Fetches an object by document id.
+ *
+ * @param {string | Object} el The element you wish to fetch. You may pass
+ *     an object in which allows this to be called regardless of whether or
+ *     not the type of the input is known.
+ * @return {HTMLElement} The element, if it exists in the document, or null.
+ * @deprecated
+ */
+function _gel(el) {
+  return document.getElementById ? document.getElementById(el) : null;
+}
+
+/**
+ * Fetches elements by tag name.
+ * This is functionally identical to document.getElementsByTagName()
+ *
+ * @param {string} tag The tag to match elements against.
+ * @return {Array.<HTMLElement>} All elements of this tag type.
+ * @deprecated
+ */
+function _gelstn(tag) {
+  if (tag === '*' && document.all) {
+    return document.all;
+  }
+  return document.getElementsByTagName ?
+         document.getElementsByTagName(tag) : [];
+}
+
+/**
+ * Fetches elements with ids matching a given regular expression.
+ *
+ * @param {string} tagName The tag to match elements against.
+ * @param {RegEx} regex The expression to match.
+ * @return {Array.<HTMLElement>} All elements of this tag type that match
+ *     regex.
+ * @deprecated
+ */
+function _gelsbyregex(tagName, regex) {
+  var matchingTags = _gelstn(tagName);
+  var matchingRegex = [];
+  for (var i = 0, j = matchingTags.length; i < j; ++i) {
+    if (regex.test(matchingTags[i].id)) {
+      matchingRegex.push(matchingTags[i]);
+    }
+  }
+  return matchingRegex;
+}
+
+/**
+ * URI escapes the given string.
+ * @param {string} str The string to escape.
+ * @return {string} The escaped string.
+ * @deprecated
+ */
+function _esc(str) {
+  return window.encodeURIComponent ? encodeURIComponent(str) : escape(str);
+}
+
+/**
+ * URI unescapes the given string.
+ * @param {string} str The string to unescape.
+ * @return {string} The unescaped string.
+ * @deprecated
+ */
+function _unesc(str) {
+  return window.decodeURIComponent ? decodeURIComponent(str) : unescape(str);
+}
+
+/**
+ * Encodes HTML entities such as <, " and >.
+ *
+ * @param {string} str The string to escape.
+ * @return {string} The escaped string.
+ * @deprecated
+ */
+function _hesc(str) {
+  return gadgets.util.escapeString(str);
+}
+
+/**
+ * Removes HTML tags from the given input string.
+ *
+ * @param {string} str The string to strip.
+ * @return {string} The stripped string.
+ * @deprecated
+ */
+function _striptags(str) {
+  return str.replace(/<\/?[^>]+>/g, '');
+}
+
+/**
+ * Trims leading & trailing whitespace from the given string.
+ *
+ * @param {string} str The string to trim.
+ * @return {string} The trimmed string.
+ * @deprecated
+ */
+function _trim(str) {
+  return str.replace(/^\s+|\s+$/g, '');
+}
+
+/**
+ * Toggles the given element between being shown and block-style display.
+ *
+ * @param {string | HTMLElement} el The element to toggle.
+ * @deprecated
+ */
+function _toggle(el) {
+  el = (typeof el === 'string') ? _gel(el) : el;
+  if (el !== null) {
+    if (el.style.display.length === 0 || el.style.display === 'block') {
+      el.style.display = 'none';
+    } else if (el.style.display === 'none') {
+      el.style.display = 'block';
+    }
+  }
+}
+
+
+var _uid = (function() {
+  /**
+   * @type {number} A counter used by uniqueId().
+   */
+  var _legacy_uidCounter = 0;
+
+  /**
+   * @return {number} a unique number.
+   * @deprecated
+   */
+  return function() {
+    return _legacy_uidCounter++;
+  };
+})();
+
+/**
+ * @param {number} a
+ * @param {number} b
+ * @return {number} The lesser of a or b.
+ * @deprecated
+ */
+function _min(a, b) {
+  return (a < b ? a : b);
+}
+
+/**
+ * @param {number} a
+ * @param {number} b
+ * @return {number} The greater of a or b.
+ * @deprecated
+ */
+function _max(a, b) {
+  return (a > b ? a : b);
+}
+
+/**
+ * @param {string} name
+ * @param {Array.<string | Object>} sym
+ * @deprecated
+ */
+function _exportSymbols(name, sym) {
+  var attach = window;
+  var parts = name.split('.');
+  for (var i = 0, j = parts.length; i < j; i++) {
+    var part = parts[i];
+    attach[part] = attach[part] || {};
+    attach = attach[part];
+  }
+  for (var k = 0, l = sym.length; k < l; k += 2) {
+    attach[sym[k]] = sym[k + 1];
+  }
+}
+
+/**
+ * @deprecated
+ * @param {Object} src
+ * @param {string} etype
+ * @param {function()} func
+ * TODO - implement.
+ */
+function _IG_AddDOMEventHandler(src, etype, func) {
+  gadgets.warn('_IG_AddDOMEventHandler not implemented - see SHINDIG-198');
+}
+
diff --git a/trunk/features/src/main/javascript/features/core.log/feature.xml b/trunk/features/src/main/javascript/features/core.log/feature.xml
new file mode 100644
index 0000000..0fc8df7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.log/feature.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.log</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <all>
+    <script src="log.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.error</exports>
+      <exports type="js">gadgets.log.INFO</exports>
+      <exports type="js">gadgets.log.WARNING</exports>
+      <exports type="js">gadgets.log.NONE</exports>
+      <exports type="js">gadgets.setLogLevel</exports>
+      <exports type="js">gadgets.warn</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.log/log.js b/trunk/features/src/main/javascript/features/core.log/log.js
new file mode 100644
index 0000000..d4d287f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.log/log.js
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Support for basic logging capability for gadgets.
+ *
+ * This functionality replaces alert(msg) and window.console.log(msg).
+ *
+ * <p>Currently only works on browsers with a console (WebKit based browsers,
+ * Firefox with Firebug extension, or Opera).
+ *
+ * <p>API is designed to be equivalent to existing console.log | warn | error
+ * logging APIs supported by Firebug and WebKit based browsers. The only
+ * addition is the ability to call gadgets.setLogLevel().
+ */
+
+/**
+ * @static
+ * @namespace Support for basic logging capability for gadgets.
+ * @name gadgets.log
+ */
+
+gadgets.log = (function() {
+  /** @const */
+  var info_ = 1;
+  /** @const */
+  var warning_ = 2;
+  /** @const */
+  var error_ = 3;
+  /** @const */
+  var none_ = 4;
+
+/**
+ * Log an informational message
+ * @param {Object} message - the message to log.
+ * @member gadgets
+  * @name log
+  * @function
+  */
+  var log = function(message) {
+    logAtLevel(info_, message);
+  };
+
+  /**
+ * Log a warning
+ * @param {Object} message - the message to log.
+ * @static
+ */
+  gadgets.warn = function(message) {
+    logAtLevel(warning_, message);
+  };
+
+  /**
+ * Log an error
+ * @param {Object} message - The message to log.
+ * @static
+ */
+  gadgets.error = function(message) {
+    logAtLevel(error_, message);
+  };
+
+  /**
+ * Sets the log level threshold.
+ * @param {number} logLevel - New log level threshold.
+ * @static
+ * @member gadgets.log
+ * @name setLogLevel
+ */
+  gadgets.setLogLevel = function(logLevel) {
+    logLevelThreshold_ = logLevel;
+  };
+
+  /**
+ * Logs a log message if output console is available, and log threshold is met.
+ * @param {number} level - the level to log with. Optional, defaults to gadgets.log.INFO.
+ * @param {Object} message - The message to log.
+ * @private
+ */
+  function logAtLevel(level, message) {
+    if(typeof _console === 'undefined') {
+      //Purposely set to null if there is no console that way we don't come
+      //back in here
+      _console = window.console ? window.console :
+        window.opera ? window.opera.postError : null;
+    }
+    if (level < logLevelThreshold_ || !_console) {
+      return;
+    }
+
+    if (level === warning_ && _console.warn) {
+      _console.warn(message);
+    } else if (level === error_ && _console.error) {
+      _console.error(message);
+    } else if (_console.log) {
+      _console.log(message);
+    }
+  };
+
+  /**
+ * Log level for informational logging.
+ * @static
+ * @const
+ * @member gadgets.log
+ * @name INFO
+ */
+  log['INFO'] = info_;
+
+  /**
+ * Log level for warning logging.
+ * @static
+ * @const
+ * @member gadgets.log
+ * @name WARNING
+ */
+  log['WARNING'] = warning_;
+
+  /**
+ * Log level for no logging
+ * @static
+ * @const
+ * @member gadgets.log
+ * @name NONE
+ */
+  log['NONE'] = none_;
+
+  /**
+ * Current log level threshold.
+ * @type {number}
+ * @private
+ */
+  var logLevelThreshold_ = info_;
+
+
+
+  /**
+ * Console to log to
+ * @private
+ * @static
+ */
+  var _console;
+
+  return log;
+})();
diff --git a/trunk/features/src/main/javascript/features/core.log/taming.js b/trunk/features/src/main/javascript/features/core.log/taming.js
new file mode 100644
index 0000000..7411a0b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.log/taming.js
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets, 'log'],
+    [gadgets, 'warn'],
+    [gadgets, 'error'],
+    [gadgets, 'setLogLevel']
+  ]);
+  caja___.whitelistProps([
+    [gadgets.log, 'INFO'],
+    [gadgets.log, 'WARNING'],
+    [gadgets.log, 'ERROR'],
+    [gadgets.log, 'NONE']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/core.none/feature.xml b/trunk/features/src/main/javascript/features/core.none/feature.xml
new file mode 100644
index 0000000..a6875a7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.none/feature.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <!--
+    A blank feature whose name starts with the special string "core."
+    Gadget developers may include this if they prefer that the renderer
+    omit *all* core JavaScript from output.
+  -->
+  <name>core.none</name>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.prefs/feature.xml b/trunk/features/src/main/javascript/features/core.prefs/feature.xml
new file mode 100644
index 0000000..b8c632a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.prefs/feature.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.prefs</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>core.util</dependency>
+  <gadget>
+    <script src="prefs.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.Prefs</exports>
+      <exports type="js">gadgets.Prefs.prototype.getString</exports>
+      <exports type="js">gadgets.Prefs.prototype.getInt</exports>
+      <exports type="js">gadgets.Prefs.prototype.getFloat</exports>
+      <exports type="js">gadgets.Prefs.prototype.getBool</exports>
+      <exports type="js">gadgets.Prefs.prototype.set</exports>
+      <exports type="js">gadgets.Prefs.prototype.getArray</exports>
+      <exports type="js">gadgets.Prefs.prototype.setArray</exports>
+      <exports type="js">gadgets.Prefs.prototype.getMsg</exports>
+      <exports type="js">gadgets.Prefs.prototype.getCountry</exports>
+      <exports type="js">gadgets.Prefs.prototype.getLang</exports>
+      <exports type="js">gadgets.Prefs.prototype.getModuleId</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.prefs/prefs.js b/trunk/features/src/main/javascript/features/core.prefs/prefs.js
new file mode 100644
index 0000000..b81e576
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.prefs/prefs.js
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Provides access to user prefs, module dimensions, and messages.
+ *
+ * <p>Clients can access their preferences by constructing an instance of
+ * gadgets.Prefs and passing in their module id.  Example:
+ *
+ * <pre>
+ *   var prefs = new gadgets.Prefs();
+ *   var name = prefs.getString("name");
+ *   var lang = prefs.getLang();
+ * </pre>
+ *
+ * <p>Modules with type=url can also use this library to parse arguments passed
+ * by URL, but this is not the common case:
+ *
+ *   &lt;script src="http://apache.org/shindig/prefs.js"&gt;&lt;/script&gt;
+ *   &lt;script&gt;
+ *   gadgets.Prefs.parseUrl();
+ *   var prefs = new gadgets.Prefs();
+ *   var name = prefs.getString("name");
+ *   &lt;/script&gt;
+ */
+
+(function() {
+
+  var instance = null;
+  var prefs = {};
+  var esc = gadgets.util.escapeString;
+  var messages = {};
+  var defaultPrefs = {};
+  var language = 'en';
+  var country = 'US';
+  var moduleId = 0;
+
+  /**
+ * Parses all parameters from the url and stores them
+ * for later use when creating a new gadgets.Prefs object.
+ */
+  function parseUrl() {
+    var params = gadgets.util.getUrlParameters();
+    for (var i in params) {
+      if (params.hasOwnProperty(i)) {
+        if (i.indexOf('up_') === 0 && i.length > 3) {
+          prefs[i.substr(3)] = String(params[i]);
+        } else if (i === 'country') {
+          country = params[i];
+        } else if (i === 'lang') {
+          language = params[i];
+        } else if (i === 'mid') {
+          moduleId = params[i];
+        }
+      }
+    }
+  }
+
+  /**
+ * Sets default pref values for values left unspecified in the
+ * rendering call, but with default_value provided in the spec.
+ */
+  function mergeDefaults() {
+    for (var name in defaultPrefs) {
+      if (typeof prefs[name] === 'undefined') {
+        prefs[name] = defaultPrefs[name];
+      }
+    }
+  }
+
+  /**
+ * @class
+ * Provides access to user preferences, module dimensions, and messages.
+ *
+ * Clients can access their preferences by constructing an instance of
+ * gadgets.Prefs and passing in their module id.  Example:
+ *
+<pre>var prefs = new gadgets.Prefs();
+var name = prefs.getString("name");
+var lang = prefs.getLang();</pre>
+ *
+ * Note: this is actually a singleton. All prefs are linked. If you're wondering
+ * why this is a singleton and not just a collection of package functions, the
+ * simple answer is that it's how the spec is written. The spec is written this
+ * way for legacy compatibility with igoogle.
+ */
+  gadgets.Prefs = function() {
+    if (!instance) {
+      parseUrl();
+      mergeDefaults();
+      instance = this;
+    }
+    return instance;
+  };
+
+  /**
+ * Sets internal values
+ * @return {boolean} True if the prefs is modified.
+ */
+  gadgets.Prefs.setInternal_ = function(key, value) {
+    var wasModified = false;
+    if (typeof key === 'string') {
+      if (!prefs.hasOwnProperty(key) || prefs[key] !== value) {
+        wasModified = true;
+      }
+      prefs[key] = value;
+    } else {
+      for (var k in key) {
+        if (key.hasOwnProperty(k)) {
+          var v = key[k];
+          if (!prefs.hasOwnProperty(k) || prefs[k] !== v) {
+            wasModified = true;
+          }
+          prefs[k] = v;
+        }
+      }
+    }
+    return wasModified;
+  };
+
+  /**
+ * Initializes message bundles.
+ */
+  gadgets.Prefs.setMessages_ = function(msgs) {
+    messages = msgs;
+  };
+
+  /**
+ * Initializes default user prefs values.
+ */
+  gadgets.Prefs.setDefaultPrefs_ = function(defprefs) {
+    defaultPrefs = defprefs;
+  };
+
+  /**
+ * Retrieves a preference as a string.
+ * Returned value will be html entity escaped.
+ *
+ * @param {string} key The preference to fetch.
+ * @return {string} The preference; if not set, an empty string.
+ */
+  gadgets.Prefs.prototype.getString = function(key) {
+    if (key === '.lang') { key = 'lang'; }
+    return prefs[key] ? esc(prefs[key]) : '';
+  };
+
+  /*
+ * Indicates not to escape string values when retrieving them.
+ * This is an internal detail used by _IG_Prefs for backward compatibility.
+ */
+  gadgets.Prefs.prototype.setDontEscape_ = function() {
+    esc = function(k) { return k; };
+  };
+
+  /**
+ * Retrieves a preference as an integer.
+ * @param {string} key The preference to fetch.
+ * @return {number} The preference; if not set, 0.
+ */
+  gadgets.Prefs.prototype.getInt = function(key) {
+    var val = parseInt(prefs[key], 10);
+    return isNaN(val) ? 0 : val;
+  };
+
+  /**
+ * Retrieves a preference as a floating-point value.
+ * @param {string} key The preference to fetch.
+ * @return {number} The preference; if not set, 0.
+ */
+  gadgets.Prefs.prototype.getFloat = function(key) {
+    var val = parseFloat(prefs[key]);
+    return isNaN(val) ? 0 : val;
+  };
+
+  /**
+ * Retrieves a preference as a boolean.
+ * @param {string} key The preference to fetch.
+ * @return {boolean} The preference; if not set, false.
+ */
+  gadgets.Prefs.prototype.getBool = function(key) {
+    var val = prefs[key];
+    if (val) {
+      return val === 'true' || val === true || !!parseInt(val, 10);
+    }
+    return false;
+  };
+
+  /**
+ * Stores a preference.
+ * To use this call,
+ * the gadget must require the feature setprefs.
+ *
+ * <p class="note">
+ * <b>Note:</b>
+ * If the gadget needs to store an Array it should use setArray instead of
+ * this call.
+ * </p>
+ *
+ * @param {string} key The pref to store.
+ * @param {Object} val The values to store.
+ */
+  gadgets.Prefs.prototype.set = function(key, value) {
+    throw new Error('setprefs feature required to make this call.');
+  };
+
+  /**
+ * Retrieves a preference as an array.
+ * UserPref values that were not declared as lists are treated as
+ * one-element arrays.
+ *
+ * @param {string} key The preference to fetch.
+ * @return {Array.<string>} The preference; if not set, an empty array.
+ */
+  gadgets.Prefs.prototype.getArray = function(key) {
+    var val = prefs[key];
+    if (val) {
+      var arr = val.split('|');
+      // Decode pipe characters.
+      for (var i = 0, j = arr.length; i < j; ++i) {
+        arr[i] = esc(arr[i].replace(/%7C/g, '|'));
+      }
+      return arr;
+    }
+    return [];
+  };
+
+  /**
+ * Stores an array preference.
+ * To use this call,
+ * the gadget must require the feature setprefs.
+ *
+ * @param {string} key The pref to store.
+ * @param {Array} val The values to store.
+ */
+  gadgets.Prefs.prototype.setArray = function(key, val) {
+    throw new Error('setprefs feature required to make this call.');
+  };
+
+  /**
+ * Fetches an unformatted message.
+ * @param {string} key The message to fetch.
+ * @return {string} The message.
+ */
+  gadgets.Prefs.prototype.getMsg = function(key) {
+    return messages[key] || '';
+  };
+
+  /**
+ * Gets the current country, returned as ISO 3166-1 alpha-2 code.
+ *
+ * @return {string} The country for this module instance.
+ */
+  gadgets.Prefs.prototype.getCountry = function() {
+    return country;
+  };
+
+  /**
+ * Gets the current language the gadget should use when rendering, returned as a
+ * ISO 639-1 language code.
+ *
+ * @return {string} The language for this module instance.
+ */
+  gadgets.Prefs.prototype.getLang = function() {
+    return language;
+  };
+
+  /**
+ * Gets the module id for the current instance.
+ *
+ * @return {string | number} The module id for this module instance.
+ */
+  gadgets.Prefs.prototype.getModuleId = function() {
+    return moduleId;
+  };
+
+})();
diff --git a/trunk/features/src/main/javascript/features/core.prefs/taming.js b/trunk/features/src/main/javascript/features/core.prefs/taming.js
new file mode 100644
index 0000000..692a3ef
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.prefs/taming.js
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistCtors([
+    [gadgets, 'Prefs', Object]
+  ]);
+  caja___.whitelistMeths([
+    [gadgets.Prefs, 'getArray'],
+    [gadgets.Prefs, 'getBool'],
+    [gadgets.Prefs, 'getCountry'],
+    [gadgets.Prefs, 'getFloat'],
+    [gadgets.Prefs, 'getInt'],
+    [gadgets.Prefs, 'getLang'],
+    [gadgets.Prefs, 'getMsg'],
+    [gadgets.Prefs, 'getString'],
+    [gadgets.Prefs, 'set'],
+    [gadgets.Prefs, 'setArray']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/core.util.base/base.js b/trunk/features/src/main/javascript/features/core.util.base/base.js
new file mode 100644
index 0000000..ab0c06b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.base/base.js
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview General purpose utilities that gadgets can use.
+ */
+
+
+/**
+ * @class Provides general-purpose utility functions.
+ */
+gadgets.util = gadgets.util || {};
+
+
+/**
+ * Creates a closure that is suitable for passing as a callback.
+ * Any number of arguments
+ * may be passed to the callback;
+ * they will be received in the order they are passed in.
+ *
+ * @param {Object} scope The execution scope; may be null if there is no
+ *     need to associate a specific instance of an object with this
+ *     callback.
+ * @param {function(Object,Object)} callback The callback to invoke when
+ *     this is run; any arguments passed in will be passed after your initial
+ *     arguments.
+ * @param {Object} var_args Initial arguments to be passed to the callback.
+ * @return {function()} a callback function.
+ */
+gadgets.util.makeClosure = function(scope, callback, var_args) {
+  // arguments isn't a real array, so we copy it into one.
+  var baseArgs = [];
+  for (var i = 2, j = arguments.length; i < j; ++i) {
+    baseArgs.push(arguments[i]);
+  }
+  return function() {
+    // append new arguments.
+    var tmpArgs = baseArgs.slice();
+    for (var i = 0, j = arguments.length; i < j; ++i) {
+      tmpArgs.push(arguments[i]);
+    }
+    return callback.apply(scope, tmpArgs);
+  };
+};
+
+
+/**
+ * Utility function for generating an "enum" from an array.
+ *
+ * @param {Array.<string>} values The values to generate.
+ * @return {Object.<string,string>} An object with member fields to handle
+ *   the enum.
+ */
+gadgets.util.makeEnum = function(values) {
+  var i, v, obj = {};
+  for (i = 0; (v = values[i]); ++i) {
+    obj[v] = v;
+  }
+  return obj;
+};
+
+/**
+ * Check if need to poll for Ajax ready state change to avoid browsers memory leak.
+ * The default implementation will check for IE7 browsers.
+ *
+ * @return true if we need to add polling to handle ready state change and false otherwise.
+ */
+gadgets.util.shouldPollXhrReadyStateChange = function() {
+  if (document.all && !document.querySelector) {
+    return true;
+  }
+  return false;
+}
diff --git a/trunk/features/src/main/javascript/features/core.util.base/feature.xml b/trunk/features/src/main/javascript/features/core.util.base/feature.xml
new file mode 100644
index 0000000..28df44c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.base/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.util.base</name>
+  <dependency>globals</dependency>
+  <all>
+    <script src="base.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.util.makeClosure</exports>
+      <exports type="js">gadgets.util.makeEnum</exports>
+      <exports type="js">gadgets.util.shouldPollXhrReadyStateChange</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.util.base/taming.js b/trunk/features/src/main/javascript/features/core.util.base/taming.js
new file mode 100644
index 0000000..eda40d8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.base/taming.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.util, 'makeClosure'],
+    [gadgets.util, 'makeEnum'],
+    [gadgets.util, 'shouldPollXhrReadyStateChange']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/core.util.dom/dom.js b/trunk/features/src/main/javascript/features/core.util.dom/dom.js
new file mode 100644
index 0000000..9a363fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.dom/dom.js
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview General purpose utilities that gadgets can use.
+ */
+
+
+/**
+ * @class Provides general-purpose utility functions.
+ */
+gadgets.util = gadgets.util || {};
+
+(function() {
+
+  var XHTML_SPEC = 'http://www.w3.org/1999/xhtml';
+
+  function attachAttributes(elem, opt_attribs) {
+    var attribs = opt_attribs || {};
+    for (var attrib in attribs) {
+      if (attribs.hasOwnProperty(attrib)) {
+        elem[attrib] = attribs[attrib];
+      }
+    }
+  }
+
+  function stringifyElement(tagName, opt_attribs) {
+    var arr = ['<', tagName];
+    var attribs = opt_attribs || {};
+    for (var attrib in attribs) {
+      if (attribs.hasOwnProperty(attrib)) {
+        arr.push(' ');
+        arr.push(attrib);
+        arr.push('="');
+        arr.push(gadgets.util.escapeString(attribs[attrib]));
+        arr.push('"');
+      }
+    }
+    arr.push('></');
+    arr.push(tagName);
+    arr.push('>');
+    return arr.join('');
+  }
+
+  /**
+   * Creates an HTML or XHTML element.
+   * @param {string} tagName The type of element to construct.
+   * @return {Element} The newly constructed element.
+   */
+  gadgets.util.createElement = function(tagName) {
+    var element;
+    if ((!document.body) || document.body.namespaceURI) {
+      try {
+        element = document.createElementNS(XHTML_SPEC, tagName);
+      } catch (nonXmlDomException) {
+      }
+    }
+    return element || document.createElement(tagName);
+  };
+
+  /**
+   * Creates an HTML or XHTML iframe element with attributes.
+   * @param {Object=} opt_attribs Optional set of attributes to attach. The
+   * only working attributes are spelled the same way in XHTML attribute
+   * naming (most strict, all-lower-case), HTML attribute naming (less strict,
+   * case-insensitive), and JavaScript property naming (some properties named
+   * incompatibly with XHTML/HTML).
+   * @return {Element} The DOM node representing body.
+   */
+  gadgets.util.createIframeElement = function(opt_attribs) {
+    var frame = gadgets.util.createElement('iframe');
+    try {
+      // TODO: provide automatic mapping to only set the needed
+      // and JS-HTML-XHTML compatible subset through stringifyElement (just
+      // 'name' and 'id', AFAIK). The values of the attributes will be
+      // stringified should the stringifyElement code path be taken (IE)
+      var tagString = stringifyElement('iframe', opt_attribs);
+      var ieFrame = gadgets.util.createElement(tagString);
+      if (ieFrame &&
+          ((!frame) ||
+           ((ieFrame.tagName == frame.tagName) &&
+            (ieFrame.namespaceURI == frame.namespaceURI)))) {
+        frame = ieFrame;
+      }
+    } catch (nonStandardCallFailed) {
+    }
+    attachAttributes(frame, opt_attribs);
+    return frame;
+  };
+
+  /**
+   * Gets the HTML or XHTML body element.
+   * @return {Element} The DOM node representing body.
+   */
+  gadgets.util.getBodyElement = function() {
+    if (document.body) {
+      return document.body;
+    }
+    try {
+      var xbodies = document.getElementsByTagNameNS(XHTML_SPEC, 'body');
+      if (xbodies && (xbodies.length == 1)) {
+        return xbodies[0];
+      }
+    } catch (nonXmlDomException) {
+    }
+    return document.documentElement || document;
+  };
+
+})();
diff --git a/trunk/features/src/main/javascript/features/core.util.dom/feature.xml b/trunk/features/src/main/javascript/features/core.util.dom/feature.xml
new file mode 100644
index 0000000..5041735
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.dom/feature.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.util.dom</name>
+  <dependency>globals</dependency>
+  <all>
+    <script src="dom.js"/>
+    <api>
+      <exports type="js">gadgets.util.createElement</exports>
+      <exports type="js">gadgets.util.createIframeElement</exports>
+      <exports type="js">gadgets.util.getBodyElement</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.util.event/event.js b/trunk/features/src/main/javascript/features/core.util.event/event.js
new file mode 100644
index 0000000..8f5c9d8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.event/event.js
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview General purpose utilities that gadgets can use.
+ */
+
+gadgets.util = gadgets.util || {};
+
+/**
+ * Attach an event listener to given DOM element (Not a gadget standard)
+ *
+ * @param {Object} elem  DOM element on which to attach event.
+ * @param {string} eventName  Event type to listen for.
+ * @param {function()} callback  Invoked when specified event occurs.
+ * @param {boolean} useCapture  If true, initiates capture.
+ */
+gadgets.util.attachBrowserEvent = function(elem, eventName, callback, useCapture) {
+  if (typeof elem.addEventListener != 'undefined') {
+    elem.addEventListener(eventName, callback, useCapture);
+  } else if (typeof elem.attachEvent != 'undefined') {
+    elem.attachEvent('on' + eventName, callback);
+  } else {
+    gadgets.warn('cannot attachBrowserEvent: ' + eventName);
+  }
+};
+
+/**
+ * Remove event listener. (Shindig internal implementation only)
+ *
+ * @param {Object} elem  DOM element from which to remove event.
+ * @param {string} eventName  Event type to remove.
+ * @param {function()} callback  Listener to remove.
+ * @param {boolean} useCapture  Specifies whether listener being removed was added with
+ *                              capture enabled.
+ */
+gadgets.util.removeBrowserEvent = function(elem, eventName, callback, useCapture) {
+  if (elem.removeEventListener) {
+    elem.removeEventListener(eventName, callback, useCapture);
+  } else if (elem.detachEvent) {
+    elem.detachEvent('on' + eventName, callback);
+  } else {
+    gadgets.warn('cannot removeBrowserEvent: ' + eventName);
+  }
+};
+
diff --git a/trunk/features/src/main/javascript/features/core.util.event/feature.xml b/trunk/features/src/main/javascript/features/core.util.event/feature.xml
new file mode 100644
index 0000000..0d314a5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.event/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>core.util.event</name>
+  <dependency>globals</dependency>
+  <dependency>core.log</dependency>
+  <all>
+    <script src="event.js"/>
+    <api>
+      <exports type="js">gadgets.util.attachBrowserEvent</exports>
+      <exports type="js">gadgets.util.removeBrowserEvent</exports>
+    </api>
+  </all>
+</feature> 
+
diff --git a/trunk/features/src/main/javascript/features/core.util.onload/feature.xml b/trunk/features/src/main/javascript/features/core.util.onload/feature.xml
new file mode 100644
index 0000000..205458f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.onload/feature.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.util.onload</name>
+  <dependency>globals</dependency>
+  <dependency>core.util.urlparams</dependency>
+  <all>
+    <script src="onload.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.util.registerOnLoadHandler</exports>
+      <exports type="js">gadgets.util.runOnLoadHandlers</exports>
+    </api>
+  </all>
+</feature>
+
diff --git a/trunk/features/src/main/javascript/features/core.util.onload/onload.js b/trunk/features/src/main/javascript/features/core.util.onload/onload.js
new file mode 100644
index 0000000..8ea6307
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.onload/onload.js
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview General purpose utilities that gadgets can use.
+ */
+
+
+/**
+ * @class Provides general-purpose utility functions for onload.
+ */
+gadgets.util = gadgets.util || {};
+
+(function() {
+
+  var onLoadHandlers = [];
+
+  /**
+   * Registers an onload handler.
+   * @param {function()} callback The handler to run.
+   */
+  gadgets.util.registerOnLoadHandler = function(callback) {
+    onLoadHandlers.push(callback);
+  };
+
+  /**
+   * Runs all functions registered via registerOnLoadHandler.
+   * @private Only to be used by the container, not gadgets.
+   */
+  gadgets.util.runOnLoadHandlers = function() {
+    gadgets.util.registerOnLoadHandler = function(cb) {
+      cb();
+    };
+
+    if (onLoadHandlers) {
+      for (var i = 0, j = onLoadHandlers.length; i < j; ++i) {
+        try {
+          onLoadHandlers[i]();
+        } catch (ex) {
+          gadgets.warn("Could not fire onloadhandler "+ ex.message);
+        }
+      }
+      onLoadHandlers = undefined;  // No need to hold these references anymore.
+    }
+  };
+
+  (function() {
+    // If a script is statically inserted into the dom, use events
+    // to call runOnLoadHandlers.
+    // Try to attach to DOMContentLoaded if using a modern browser.
+    gadgets.util.attachBrowserEvent(document,
+            "DOMContentLoaded",
+            gadgets.util.runOnLoadHandlers,
+            false);
+    // Always attach to window.onload as a fallback. We can safely ignore
+    // any repeated calls to runOnLoadHandlers.
+    var oldWindowOnload = window.onload;
+    window.onload = function() {
+      oldWindowOnload && oldWindowOnload();
+      gadgets.util.runOnLoadHandlers();
+    };
+    // If a script is dynamically inserted into the page, the DOMContentLoaded
+    // event will be fired before runOnLoadHandlers can be attached
+    // to the event. In this case, find the script that loads the core libary
+    // and attach runOnLoadHandlers to the script's onload event.
+    var libParam = "";
+    if (window && window.location && window.location.href) {
+      libParam = gadgets.util.getUrlParameters(window.location.href).libs;
+    }
+
+    var regex = /(?:js\/)([^&|\.]+)/g;
+    var match = regex.exec(libParam);
+
+    if (match) {
+      var url = decodeURIComponent(match[1]);
+      var scripts = document.getElementsByTagName("script") || [];
+      for (var i = 0; i < scripts.length; i++) {
+        var script = scripts[i];
+        var src = script.src;
+        if (src && url && src.indexOf(url) !== -1) {
+          // save a reference to the function that is already hooked up
+          // to the event
+          var oldonload = script.onload;
+          script.onload = function() {
+            oldonload && oldonload();
+            gadgets.util.runOnLoadHandlers();
+          };
+        }
+      }
+    }
+  })();
+})();
+
diff --git a/trunk/features/src/main/javascript/features/core.util.onload/taming.js b/trunk/features/src/main/javascript/features/core.util.onload/taming.js
new file mode 100644
index 0000000..50bcd44
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.onload/taming.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.util, 'registerOnLoadHandler']
+  ]);
+});
+
diff --git a/trunk/features/src/main/javascript/features/core.util.string/feature.xml b/trunk/features/src/main/javascript/features/core.util.string/feature.xml
new file mode 100644
index 0000000..cabbd4c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.string/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.util.string</name>
+  <dependency>globals</dependency>
+  <all>
+    <script src="string.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.util.escape</exports>
+      <exports type="js">gadgets.util.escapeString</exports>
+      <exports type="js">gadgets.util.unescapeString</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.util.string/string.js b/trunk/features/src/main/javascript/features/core.util.string/string.js
new file mode 100644
index 0000000..bacb690
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.string/string.js
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview General purpose utilities that gadgets can use.
+ */
+
+
+/**
+ * @class Provides a thin method for parsing url parameters.
+ */
+gadgets.util = gadgets.util || {};
+
+(function() {
+
+  /**
+   * @enum {boolean}
+   * @const
+   * @private
+   * Maps code points to the value to replace them with.
+   * If the value is "false", the character is removed entirely, otherwise
+   * it will be replaced with an html entity.
+   */
+  var escapeCodePoints = {
+    // nul; most browsers truncate because they use c strings under the covers.
+    0 : false,
+    // new line
+    10 : true,
+    // carriage return
+    13 : true,
+    // double quote
+    34 : true,
+    // single quote
+    39 : true,
+    // less than
+    60 : true,
+    // greater than
+    62 : true,
+    // backslash
+    92 : true,
+    // line separator
+    8232 : true,
+    // paragraph separator
+    8233 : true,
+    // fullwidth quotation mark
+    65282 : true,
+    // fullwidth apostrophe
+    65287 : true,
+    // fullwidth less-than sign
+    65308 : true,
+    // fullwidth greater-than sign
+    65310 : true,
+    // fullwidth reverse solidus
+    65340 : true
+  };
+
+  /**
+   * Regular expression callback that returns strings from unicode code points.
+   *
+   * @param {Array} match Ignored.
+   * @param {number} value The codepoint value to convert.
+   * @return {string} The character corresponding to value.
+   */
+  function unescapeEntity(match, value) {
+    // TODO: b0rked for UTF-16 and can easily be convinced to generate
+    // truncating NULs or completely invalid non-Unicode characters. Here's a
+    // fixed version (it handles entities for valid codepoints from U+0001 ...
+    // U+10FFFD, except for the non-character codepoints U+...FFFE and
+    // U+...FFFF; isolated UTF-16 surrogate pairs are supported for
+    // compatibility with previous versions of escapeString, 0 generates the
+    // empty string rather than a possibly-truncating '\0', and all other inputs
+    // generate U+FFFD (the replacement character, standard practice for
+    // non-signalling Unicode codecs like this one)
+    //     return (
+    //         (value > 0) &&
+    //         (value <= 0x10fffd) &&
+    //         ((value & 0xffff) < 0xfffe)) ?
+    //       ((value <= 0xffff) ?
+    //         String.fromCharCode(value) :
+    //         String.fromCharCode(
+    //           ((value - 0x10000) >> 10) | 0xd800,
+    //           ((value - 0x10000) & 0x3ff) | 0xdc00)) :
+    //       ((value === 0) ? '' : '\ufffd');
+    return String.fromCharCode(value);
+  }
+
+  /**
+   * Escapes the input using html entities to make it safer.
+   *
+   * If the input is a string, uses gadgets.util.escapeString.
+   * If it is an array, calls escape on each of the array elements
+   * if it is an object, will only escape all the mapped keys and values if
+   * the opt_escapeObjects flag is set. This operation involves creating an
+   * entirely new object so only set the flag when the input is a simple
+   * string to string map.
+   * Otherwise, does not attempt to modify the input.
+   *
+   * @param {Object} input The object to escape.
+   * @param {boolean=} opt_escapeObjects Whether to escape objects.
+   * @return {Object} The escaped object.
+   * @private Only to be used by the container, not gadgets.
+   */
+  gadgets.util.escape = function(input, opt_escapeObjects) {
+    if (!input) {
+      return input;
+    } else if (typeof input === 'string') {
+      return gadgets.util.escapeString(input);
+    } else if (typeof input === 'array') {
+      for (var i = 0, j = input.length; i < j; ++i) {
+        input[i] = gadgets.util.escape(input[i]);
+      }
+    } else if (typeof input === 'object' && opt_escapeObjects) {
+      var newObject = {};
+      for (var field in input) {
+        if (input.hasOwnProperty(field)) {
+          newObject[gadgets.util.escapeString(field)] =
+              gadgets.util.escape(input[field], true);
+        }
+      }
+      return newObject;
+    }
+    return input;
+  };
+
+  /**
+   * Escapes the input using html entities to make it safer.
+   *
+   * Currently not in the spec -- future proposals may change
+   * how this is handled.
+   *
+   * @param {string} str The string to escape.
+   * @return {string} The escaped string.
+   */
+  gadgets.util.escapeString = function(str) {
+    if (!str) return str;
+    var out = [], ch, shouldEscape;
+    for (var i = 0, j = str.length; i < j; ++i) {
+      ch = str.charCodeAt(i);
+      shouldEscape = escapeCodePoints[ch];
+      if (shouldEscape === true) {
+        out.push('&#', ch, ';');
+      } else if (shouldEscape !== false) {
+        // undefined or null are OK.
+        out.push(str.charAt(i));
+      }
+    }
+    return out.join('');
+  };
+
+  /**
+   * Reverses escapeString
+   *
+   * @param {string} str The string to unescape.
+   * @return {string}
+   */
+  gadgets.util.unescapeString = function(str) {
+    if (!str) return str;
+    return str.replace(/&#([0-9]+);/g, unescapeEntity);
+  };
+
+})();
diff --git a/trunk/features/src/main/javascript/features/core.util.string/taming.js b/trunk/features/src/main/javascript/features/core.util.string/taming.js
new file mode 100644
index 0000000..0889ee4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.string/taming.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.util, 'escape'],
+    [gadgets.util, 'escapeString'],
+    [gadgets.util, 'unescapeString']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/core.util.urlparams/feature.xml b/trunk/features/src/main/javascript/features/core.util.urlparams/feature.xml
new file mode 100644
index 0000000..c5f8dfd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.urlparams/feature.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.util.urlparams</name>
+  <dependency>globals</dependency>
+  <all>
+    <script src="urlparams.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.util.getUrlParameters</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.util.urlparams/taming.js b/trunk/features/src/main/javascript/features/core.util.urlparams/taming.js
new file mode 100644
index 0000000..a6d5791
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.urlparams/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.util, 'getUrlParameters']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/core.util.urlparams/urlparams.js b/trunk/features/src/main/javascript/features/core.util.urlparams/urlparams.js
new file mode 100644
index 0000000..ea7e03a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util.urlparams/urlparams.js
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview General purpose utilities that gadgets can use.
+ */
+
+
+/**
+ * @class Provides a thin method for parsing url parameters.
+ */
+gadgets.util = gadgets.util || {};
+
+(function() {
+  var parameters = null;
+
+  /**
+   * Parses URL parameters into an object.
+   * @param {string} url - the url parameters to parse.
+   * @return {Array.<string>} The parameters as an array.
+   */
+  function parseUrlParams(url) {
+    // Get settings from url, 'hash' takes precedence over 'search' component
+    // don't use document.location.hash due to browser differences.
+    var query;
+    var queryIdx = url.indexOf('?');
+    var hashIdx = url.indexOf('#');
+    if (hashIdx === -1) {
+      query = url.substr(queryIdx + 1);
+    } else {
+      // essentially replaces "#" with "&"
+      query = [url.substr(queryIdx + 1, hashIdx - queryIdx - 1), '&',
+               url.substr(hashIdx + 1)].join('');
+    }
+    return query.split('&');
+  }
+
+  /**
+   * Gets the URL parameters.
+   *
+   * @param {string=} opt_url Optional URL whose parameters to parse.
+   *                         Defaults to window's current URL.
+   * @return {Object} Parameters passed into the query string.
+   * @private Implementation detail.
+   */
+  gadgets.util.getUrlParameters = function(opt_url) {
+    var no_opt_url = typeof opt_url === 'undefined';
+    if (parameters !== null && no_opt_url) {
+      // "parameters" is a cache of current window params only.
+      return parameters;
+    }
+    var parsed = {};
+    var pairs = parseUrlParams(opt_url || document.location.href);
+    var unesc = window.decodeURIComponent ? decodeURIComponent : unescape;
+    for (var i = 0, j = pairs.length; i < j; ++i) {
+      var pos = pairs[i].indexOf('=');
+      if (pos === -1) {
+        continue;
+      }
+      var argName = pairs[i].substring(0, pos);
+      var value = pairs[i].substring(pos + 1);
+      // difference to IG_Prefs, is that args doesn't replace spaces in
+      // argname. Unclear on if it should do:
+      // argname = argname.replace(/\+/g, " ");
+      value = value.replace(/\+/g, ' ');
+      try {
+        parsed[argName] = unesc(value);
+      } catch (e) {
+        // Undecodable/invalid value; ignore.
+      }
+    }
+    if (no_opt_url) {
+      // Cache current-window params in parameters var.
+      parameters = parsed;
+    }
+    return parsed;
+  };
+})();
+
+// Initialize url parameters so that hash data is pulled in before it can be
+// altered by a click.
+gadgets.util.getUrlParameters();
diff --git a/trunk/features/src/main/javascript/features/core.util/feature.xml b/trunk/features/src/main/javascript/features/core.util/feature.xml
new file mode 100644
index 0000000..1d44153
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util/feature.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>core.util</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>core.config</dependency>
+  <dependency>core.util.base</dependency>
+  <dependency>core.util.dom</dependency>
+  <dependency>core.util.event</dependency>
+  <dependency>core.util.onload</dependency>
+  <dependency>core.util.string</dependency>
+  <dependency>core.util.urlparams</dependency>
+  <all>
+    <script src="util.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.util.getFeatureParameters</exports>
+      <exports type="js">gadgets.util.hasFeature</exports>
+      <exports type="js">gadgets.util.getServices</exports>
+      <exports type="js">gadgets.util.isDebug</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/core.util/taming.js b/trunk/features/src/main/javascript/features/core.util/taming.js
new file mode 100644
index 0000000..64645b1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util/taming.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.util, 'getFeatureParameters'],
+    [gadgets.util, 'hasFeature']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/core.util/util.js b/trunk/features/src/main/javascript/features/core.util/util.js
new file mode 100644
index 0000000..27a0910
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core.util/util.js
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview General purpose utilities that gadgets can use.
+ */
+
+
+/**
+ * @class Provides general-purpose utility functions.
+ */
+gadgets.util = gadgets.util || {};
+
+(function() {
+
+  var features = {},
+      services = {},
+      undef,
+      isDebug;
+
+  /**
+   * Checks injected feature script to see if it's debug mode.
+   */
+  function checkIsDebug(config) {
+    var coreio = config['core.io'],
+        jsPath = coreio && coreio.jsPath,
+        scripts = document.getElementsByTagName('script');
+
+    for (var i = 0; jsPath && i < scripts.length; i++) {
+      var src = scripts[i].src;
+      if (src && src.indexOf(jsPath) > -1) {
+        return isDebug = gadgets.util.getUrlParameters(src).debug == '1';
+      }
+    }
+    isDebug = false;
+  }
+
+  /**
+   * Initializes feature parameters.
+   */
+  function init(config) {
+    features = config['core.util'] || {};
+  }
+
+  if (gadgets.config) {
+    gadgets.config.register('core.util', null, init);
+    gadgets.config.register('core.io', undef, checkIsDebug, checkIsDebug);
+  }
+
+  /**
+   * Gets the feature parameters.
+   *
+   * @param {string} feature The feature to get parameters for.
+   * @return {Object} The parameters for the given feature, or null.
+   */
+  gadgets.util.getFeatureParameters = function(feature) {
+    return typeof features[feature] === 'undefined' ? null : features[feature];
+  };
+
+  /**
+   * Returns whether the current feature is supported.
+   *
+   * @param {string} feature The feature to test for.
+   * @return {boolean} True if the feature is supported.
+   */
+  gadgets.util.hasFeature = function(feature) {
+    return typeof features[feature] !== 'undefined';
+  };
+
+  /**
+   * Returns the list of services supported by the server
+   * serving this gadget.
+   *
+   * @return {Object} List of Services that enumerate their methods.
+   */
+  gadgets.util.getServices = function() {
+    return services;
+  };
+
+  /**
+   * @return {boolean} If gadget is being rendered in debug mode.
+   */
+  gadgets.util.isDebug = function() {
+    return isDebug;
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/core/feature.xml b/trunk/features/src/main/javascript/features/core/feature.xml
new file mode 100644
index 0000000..91b7bed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/core/feature.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <!--
+    Core is simply an aggregating feature of all the core.* libraries. It is
+    included in a gadget render that doesn't signal its awareness of "split-core"
+    dependencies (which afford better precision in code inclusion) by including
+    core.* libs.
+  -->
+  <name>core</name>
+  <dependency>shindig.auth</dependency>
+  <dependency>core.config</dependency>
+  <dependency>core.json</dependency>
+  <dependency>core.legacy</dependency>
+  <dependency>core.log</dependency>
+  <dependency>core.io</dependency>
+  <dependency>core.prefs</dependency>
+  <dependency>core.util</dependency>
+  <gadget>
+    <!-- for html_sanitize -->
+    <script src="res://com/google/caja/plugin/html-sanitizer-minified.js"></script>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/defer.test/defertest.js b/trunk/features/src/main/javascript/features/defer.test/defertest.js
new file mode 100644
index 0000000..8ffb42d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/defer.test/defertest.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+shindig.defer = (function() {
+  function callback(callback) {
+    var args = [].slice.call(arguments, 1);
+    callback.apply(null, args);
+  }
+
+  return {
+    callback: callback
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/defer.test/feature.xml b/trunk/features/src/main/javascript/features/defer.test/feature.xml
new file mode 100644
index 0000000..30fcacd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/defer.test/feature.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<feature>
+  <name>defer.test</name>
+  <dependency>globals</dependency>
+  <all>
+    <script src="defertest.js"/>
+    <api supportDefer="true">
+      <exports type="js">shindig.defer.callback</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/deferjs/deferjs.js b/trunk/features/src/main/javascript/features/deferjs/deferjs.js
new file mode 100644
index 0000000..84cfd2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/deferjs/deferjs.js
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This provides a mechanism to defer JS symbols.
+ *
+ * Works in concert with exportJs() to bind dummy symbol names,
+ * provided as namespace + array of method names, which enqueue
+ * arguments passed to them. When a payload bundle of JS is loaded
+ * that defines the full implementation, exportJs() calls the
+ * deferred implementation dequeueing the enqueued arguments with
+ * the actual method.
+ */
+function deferJs(namespace, components) {
+  var JSL = '___jsl';
+  var DEFER_KEY = 'df';
+  var base = window;
+  var nsParts = namespace.split('.');
+  var sliceFn = [].slice;
+
+  // Set up defer function queue.
+  var deferMap = ((window[JSL] = window[JSL] || {})[DEFER_KEY] = window[JSL][DEFER_KEY] || {});
+
+  var part;
+  while (part = nsParts.shift()) {
+    base[part] = base[part] || {};
+    base = base[part];
+  }
+
+  var methods = sliceFn.call(components, 0);
+  var method;
+  while (method = methods.shift()) {
+    // Don't overwrite an existing method if present,
+    // whether deferred or full/exported.
+    if (!base[method]) {
+      var fulltok = namespace + '.' + method;
+      base[method] = (function() {
+        var queue = [];
+        var ret = function() {
+          queue.push(sliceFn.call(arguments, 0));
+        };
+        deferMap[fulltok] = function(ctx, method) {
+          for (var i = 0, len = queue.length; i < len; ++i) {
+            method.apply(ctx, queue[i]);
+          }
+        };
+        return ret;
+      })();
+    }
+  }
+}
diff --git a/trunk/features/src/main/javascript/features/deferjs/feature.xml b/trunk/features/src/main/javascript/features/deferjs/feature.xml
new file mode 100644
index 0000000..a5d8df2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/deferjs/feature.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>deferjs</name>
+  <all>
+    <script src="deferjs.js"/>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/domnode/constants.js b/trunk/features/src/main/javascript/features/domnode/constants.js
new file mode 100644
index 0000000..1a1204b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/domnode/constants.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+// Based on <http://www.w3.org/TR/2000/ REC-DOM-Level-2-Core-20001113/
+// core.html#ID-1950641247>.
+var DOM_ELEMENT_NODE = 1;
+var DOM_ATTRIBUTE_NODE = 2;
+var DOM_TEXT_NODE = 3;
+var DOM_CDATA_SECTION_NODE = 4;
+var DOM_ENTITY_REFERENCE_NODE = 5;
+var DOM_ENTITY_NODE = 6;
+var DOM_PROCESSING_INSTRUCTION_NODE = 7;
+var DOM_COMMENT_NODE = 8;
+var DOM_DOCUMENT_NODE = 9;
+var DOM_DOCUMENT_TYPE_NODE = 10;
+var DOM_DOCUMENT_FRAGMENT_NODE = 11;
+var DOM_NOTATION_NODE = 12;
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/domnode/feature.xml b/trunk/features/src/main/javascript/features/domnode/feature.xml
new file mode 100644
index 0000000..5ce186c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/domnode/feature.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>domnode</name>
+  <all>
+    <script src="constants.js"/>
+    <api>
+      <exports type="js">DOM_ELEMENT_NODE</exports>
+      <exports type="js">DOM_ATTRIBUTE_NODE</exports>
+      <exports type="js">DOM_TEXT_NODE</exports>
+      <exports type="js">DOM_CDATA_SECTION_NODE</exports>
+      <exports type="js">DOM_ENTITY_REFERENCE_NODE</exports>
+      <exports type="js">DOM_ENTITY_NODE</exports>
+      <exports type="js">DOM_PROCESSING_INSTRUCTION_NODE</exports>
+      <exports type="js">DOM_COMMENT_NODE</exports>
+      <exports type="js">DOM_DOCUMENT_NODE</exports>
+      <exports type="js">DOM_DOCUMENT_TYPE_NODE</exports>
+      <exports type="js">DOM_DOCUMENT_FRAGMENT_NODE</exports>
+      <exports type="js">DOM_NOTATION_NODE</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/dynamic-height.height/dynamic-height-height.js b/trunk/features/src/main/javascript/features/dynamic-height.height/dynamic-height-height.js
new file mode 100644
index 0000000..6bdabe3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-height.height/dynamic-height-height.js
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library augments gadgets.window with functionality
+ * to change the height of a gadget dynamically.
+ */
+
+/**
+ * @static
+ * @class Provides operations for getting information about the window the
+ *        gadget is placed in.
+ * @name gadgets.window
+ */
+gadgets.window = gadgets.window || {};
+
+(function() {
+  /**
+   * Calculate inner content height is hard and different between
+   * browsers rendering in Strict vs. Quirks mode.
+   * Currently, Shindig is using algorithm to iterate to all elements in the
+   * body to extract the height property and CSS if available.
+   */
+  gadgets.window.getHeight = function() {
+    return gadgets.window.getDimen(1);
+  };
+}());
diff --git a/trunk/features/src/main/javascript/features/dynamic-height.height/feature.xml b/trunk/features/src/main/javascript/features/dynamic-height.height/feature.xml
new file mode 100644
index 0000000..b6ab65a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-height.height/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>dynamic-height.height</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>dynamic-size.util</dependency>
+  <all>
+    <script src="dynamic-height-height.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.window.getHeight</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/dynamic-height.height/taming.js b/trunk/features/src/main/javascript/features/dynamic-height.height/taming.js
new file mode 100644
index 0000000..b887992
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-height.height/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.window.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.window, 'getHeight']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/dynamic-height/dynamic-height.js b/trunk/features/src/main/javascript/features/dynamic-height/dynamic-height.js
new file mode 100644
index 0000000..973aa8c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-height/dynamic-height.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library augments gadgets.window with functionality
+ * to change the height of a gadget dynamically.
+ */
+
+/**
+ * @static
+ * @class Provides operations for getting information about and modifying the
+ *     window the gadget is placed in.
+ * @name gadgets.window
+ */
+gadgets.window = gadgets.window || {};
+
+// we wrap these in an anonymous function to avoid storing private data
+// as members of gadgets.window.
+(function() {
+
+  var oldHeight;
+
+  /**
+   * Adjusts the gadget height
+   * @param {number=} opt_height An optional preferred height in pixels. If not
+   *     specified, will attempt to fit the gadget to its content.
+   * @member gadgets.window
+   */
+  gadgets.window.adjustHeight = function(opt_height) {
+    var newHeight = parseInt(opt_height, 10);
+    var heightAutoCalculated = false;
+    if (isNaN(newHeight)) {
+      heightAutoCalculated = true;
+      newHeight = gadgets.window.getHeight();
+    }
+
+    // Only make the IFPC call if height has changed
+    if (newHeight !== oldHeight &&
+        !isNaN(newHeight) &&
+        !(heightAutoCalculated && newHeight === 0)) {
+      oldHeight = newHeight;
+      gadgets.rpc.call(null, 'resize_iframe', null, newHeight);
+    }
+  };
+}());
+
+/**
+ * @see gadgets.window#adjustHeight
+ */
+var _IG_AdjustIFrameHeight = gadgets.window.adjustHeight;
+
+// TODO Attach gadgets.window.adjustHeight to the onresize event
diff --git a/trunk/features/src/main/javascript/features/dynamic-height/feature.xml b/trunk/features/src/main/javascript/features/dynamic-height/feature.xml
new file mode 100644
index 0000000..87394cb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-height/feature.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>dynamic-height</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>dynamic-height.height</dependency>
+  <dependency>dynamic-size.util</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="dynamic-height.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.window.adjustHeight</exports>
+      <uses type="rpc">resize_iframe</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/dynamic-height/taming.js b/trunk/features/src/main/javascript/features/dynamic-height/taming.js
new file mode 100644
index 0000000..1e85c9a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-height/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.window.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.window, 'adjustHeight']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/dynamic-size.util/dynamic-size-util.js b/trunk/features/src/main/javascript/features/dynamic-size.util/dynamic-size-util.js
new file mode 100644
index 0000000..d87820d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-size.util/dynamic-size-util.js
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library augments gadgets.window with functionality to get
+ *    the frame's viewport dimensions.
+ */
+
+gadgets.window = gadgets.window || {};
+
+// we wrap these in an anonymous function to avoid storing private data
+// as members of gadgets.window.
+(function() {
+
+  /**
+   * @private
+   */
+  function getElementComputedStyle(elem, attr) {
+    var n = navigator;
+    var dua = n.userAgent,dav = n.appVersion;
+    var isWebKit = parseFloat(dua.split("WebKit/")[1]) || undefined;
+    var isIE = parseFloat(dav.split("MSIE ")[1]) || undefined;
+    var gcs;
+    if(isWebKit){
+      /**
+       * Get the computed style from the dom node, implementation of this function differs in browsers.
+       * @private
+       * @param {DomNode} node the dom node.
+       * @return {Object} the style object.
+       */
+      gcs = function(node){
+        var s;
+        if(node.nodeType == DOM_ELEMENT_NODE){
+          var dv = node.ownerDocument.defaultView;
+          s = dv.getComputedStyle(node, null);
+          if(!s && node.style){
+            node.style.display = "";
+            s = dv.getComputedStyle(node, null);
+          }
+        }
+        return s || {};
+      };
+    } else if (isIE && !window.getComputedStyle) {
+      gcs = function(node){
+        // IE (as of 7) doesn't expose Element like sane browsers
+        return node.nodeType == DOM_ELEMENT_NODE ? node.currentStyle : {};
+      };
+    } else {
+      gcs = function(node){
+        return node.nodeType == DOM_ELEMENT_NODE ?
+          node.ownerDocument.defaultView.getComputedStyle(node, null) : {};
+      };
+    }
+
+    var style = gcs(elem);
+    return attr && style ? style[attr] : style;
+  }
+
+  /**
+   * Parse out the value (specified in px) for a CSS attribute of an element.
+   *
+   * @param {Element} elem the element with the attribute to look for.
+   * @param {string} attr the CSS attribute name of interest.
+   * @return {number} the value of the px attr of the elem, undefined if the attr was undefined.
+   * @private
+   */
+  function parseIntFromElemPxAttribute(elem, attr) {
+    var value = getElementComputedStyle(elem, attr);
+    if (value) {
+      value.match(/^([0-9]+)/);
+      return parseInt(RegExp.$1, 10);
+    }
+  }
+
+  /**
+   * Get the height (truthy) or width (falsey)
+   */
+  gadgets.window.getDimen = function (height) {
+    var result = 0;
+    var queue = [document.body];
+
+    while (queue.length > 0) {
+      var elem = queue.shift();
+      var children = elem.childNodes;
+
+      /*
+       * Here, we are checking if we are a container that clips its overflow with
+       * a specific height, because if so, we should ignore children
+       */
+
+      // check that elem is actually an element, could be a text node otherwise
+      if (typeof elem.style !== 'undefined' && elem !== document.body) {
+        // Get the overflowY value, looking in the computed style if necessary
+        var overflow = elem.style[height ? 'overflowY' : 'overflowX'];
+        if (!overflow) {
+          overflow = getElementComputedStyle(elem, height ? 'overflowY' : 'overflowX');
+        }
+
+        // The only non-clipping values of overflow is 'visible'. We assume that 'inherit'
+        // is also non-clipping at the moment, but should we check this?
+        if (overflow != 'visible' && overflow != 'inherit') {
+          // Make sure this element explicitly specifies a height
+          var size = elem.style[height ? 'height' : 'width'];
+          if (!size) {
+            size = getElementComputedStyle(elem, height ? 'height' : 'width');
+          }
+          if (size && size.length > 0 && size != 'auto') {
+            // We can safely ignore the children of this element,
+            // so move onto the next in the queue
+            continue;
+          }
+        }
+      }
+
+      for (var i = 0; i < children.length; i++) {
+        var child = children[i];
+        if (typeof child.style != 'undefined') {  // Don't measure text nodes
+          var start = child.offsetTop,
+              dimenEnd = 'marginBottom',
+              size = child.offsetHeight,
+              dir = getElementComputedStyle(child, 'direction');
+
+          if (!height) {
+            start = child.offsetLeft;
+            dimenEnd = 'marginRight';
+            size = child.offsetWidth;
+
+            // compute offsetRight
+            if (dir == 'rtl' && typeof start != 'undefined' && typeof size != 'undefined' && child.offsetParent) {
+              start = child.offsetParent.offsetWidth - start - size;
+            }
+          }
+
+          if (typeof start != 'undefined' && typeof size != 'undefined') {
+            // offsetHeight already accounts for borderBottom, paddingBottom.
+            var end = start + size + (parseIntFromElemPxAttribute(child, dimenEnd) || 0);
+            result = Math.max(result, end);
+          }
+        }
+        queue.push(child);
+      }
+    }
+
+    // Add border, padding and margin of the containing body.
+    return result +
+        (parseIntFromElemPxAttribute(document.body, height ? 'borderBottom' : 'borderRight') || 0) +
+        (parseIntFromElemPxAttribute(document.body, height ? 'marginBottom' : 'marginRight') || 0) +
+        (parseIntFromElemPxAttribute(document.body, height ? 'paddingBottom' : 'paddingRight') || 0);
+  };
+
+  /**
+   * Detects the inner dimensions of a frame. See:
+   * http://www.quirksmode.org/viewport/compatibility.html for more information.
+   *
+   * @return {Object} An object with width and height properties.
+   * @member gadgets.window
+   */
+  gadgets.window.getViewportDimensions = function() {
+    var x = 0;
+    var y = 0;
+    if (self.innerHeight) {
+      // all except Explorer
+      x = self.innerWidth;
+      y = self.innerHeight;
+    } else if (document.documentElement && document.documentElement.clientHeight) {
+      // Explorer 6 Strict Mode
+      x = document.documentElement.clientWidth;
+      y = document.documentElement.clientHeight;
+    } else if (document.body) {
+      // other Explorers
+      x = document.body.clientWidth;
+      y = document.body.clientHeight;
+    }
+    return {
+      width : x,
+      height : y
+    };
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/dynamic-size.util/feature.xml b/trunk/features/src/main/javascript/features/dynamic-size.util/feature.xml
new file mode 100644
index 0000000..cb1b76d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-size.util/feature.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+
+<!-- TODO: rename to dynamic-height.viewport -->
+<feature>
+  <name>dynamic-size.util</name>
+  <dependency>globals</dependency>
+  <dependency>domnode</dependency>
+  <dependency>taming</dependency>
+  <all>
+    <script src="dynamic-size-util.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.window.getViewportDimensions</exports>
+      <exports type="js">gadgets.window.getDimen</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/dynamic-size.util/taming.js b/trunk/features/src/main/javascript/features/dynamic-size.util/taming.js
new file mode 100644
index 0000000..ec4d329
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-size.util/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.window.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.window, 'getViewportDimensions']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/dynamic-width.width/dynamic-width-width.js b/trunk/features/src/main/javascript/features/dynamic-width.width/dynamic-width-width.js
new file mode 100644
index 0000000..33bd683
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-width.width/dynamic-width-width.js
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library augments gadgets.window with functionality
+ * to change the width of a gadget dynamically.
+ */
+
+/**
+ * @static
+ * @class Provides operations for getting information about the window the gadget is placed in.
+ * @name gadgets.window
+ */
+gadgets.window = gadgets.window || {};
+
+(function() {
+
+  /**
+   * Calculate inner content width is hard and different between browsers rendering in Strict vs.
+   * Quirks mode. We use a combination of three properties within document.body and
+   * document.documentElement: - scrollWidth - offsetWidth - clientWidth These values differ
+   * significantly between browsers and rendering modes. But there are patterns. It just takes a lot
+   * of time and persistence to figure out.
+   *
+   * @return The width of the content within the iframe
+   */
+  gadgets.window.getWidth = function() {
+    return gadgets.window.getDimen();
+  };
+
+}());
diff --git a/trunk/features/src/main/javascript/features/dynamic-width.width/feature.xml b/trunk/features/src/main/javascript/features/dynamic-width.width/feature.xml
new file mode 100644
index 0000000..6ea515a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-width.width/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>dynamic-width.width</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>dynamic-size.util</dependency>
+  <all>
+    <script src="dynamic-width-width.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.window.getWidth</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/dynamic-width.width/taming.js b/trunk/features/src/main/javascript/features/dynamic-width.width/taming.js
new file mode 100644
index 0000000..3b17168
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-width.width/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.window.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.window, 'getWidth']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/dynamic-width/dynamic-width.js b/trunk/features/src/main/javascript/features/dynamic-width/dynamic-width.js
new file mode 100644
index 0000000..f78a464
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-width/dynamic-width.js
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library augments gadgets.window with functionality
+ * to change the width of a gadget dynamically.
+ */
+
+/**
+ * @static
+ * @class Provides operations for getting information about and modifying the window the gadget is
+ *        placed in.
+ * @name gadgets.window
+ */
+gadgets.window = gadgets.window || {};
+
+// we wrap these in an anonymous function to avoid storing private data
+// as members of gadgets.window.
+(function() {
+
+  /**
+   * Adjusts the gadget width
+   *
+   * @param {number=}
+   *          opt_width An optional preferred width in pixels. If not specified, will attempt to fit
+   *          the gadget to its content.
+   * @member gadgets.window
+   */
+  gadgets.window.adjustWidth = function(opt_width) {
+    opt_width = parseInt(opt_width, 10);
+    var newWidth = opt_width || gadgets.window.getWidth();
+    gadgets.rpc.call(null, 'resize_iframe_width', null, newWidth);
+  };
+}());
+
+/**
+ * @see gadgets.window#adjustWidth
+ */
+var _IG_AdjustIFrameWidth = gadgets.window.adjustWidth;
+
+// TODO Attach gadgets.window.adjustWidth to the onresize event
diff --git a/trunk/features/src/main/javascript/features/dynamic-width/feature.xml b/trunk/features/src/main/javascript/features/dynamic-width/feature.xml
new file mode 100644
index 0000000..57e7507
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-width/feature.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>dynamic-width</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>dynamic-width.width</dependency>
+  <dependency>dynamic-size.util</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="dynamic-width.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.window.adjustWidth</exports>
+      <uses type="rpc">resize_iframe_width</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/dynamic-width/taming.js b/trunk/features/src/main/javascript/features/dynamic-width/taming.js
new file mode 100644
index 0000000..794076e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/dynamic-width/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.window.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.window, 'adjustWidth']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/embeddedexperiences/constant.js b/trunk/features/src/main/javascript/features/embeddedexperiences/constant.js
new file mode 100644
index 0000000..4543623
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/embeddedexperiences/constant.js
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Constants used throughout the container classes for
+ * embedded experiences.
+ */
+
+/**
+ * Embedded experience namespace
+ * @type {Object}
+ */
+osapi.container.ee = {};
+
+/**
+ * Rendering params for an embedded experience
+ * @enum {string}
+ */
+osapi.container.ee.RenderParam = {
+    GADGET_RENDER_PARAMS: 'gadgetRenderParams',
+    GADGET_VIEW_PARAMS: 'gadgetViewParams',
+    URL_RENDER_PARAMS: 'urlRenderParams',
+    DATA_MODEL: 'eeDataModel',
+    EMBEDDED: 'embedded'
+};
+
+/**
+ * Parameters for EE Data Model.
+ * @enum {string}
+ */
+osapi.container.ee.DataModel = {
+    CONTEXT: 'context',
+    GADGET: 'gadget',
+    URL: 'url',
+    PREVIEW_IMAGE: 'previewImage',
+    PREFERRED_EXPERIENCE: 'preferredExperience'
+};
+
+/**
+ * Parameters for EE data model preferredExperience section.
+ * @enum {string}
+ */
+osapi.container.ee.PreferredExperience = {
+    TARGET: 'target',
+    DISPLAY: 'display',
+    TYPE: 'type',
+    VIEW: 'view',
+    VIEW_TARGET: 'viewTarget'
+};
+
+/**
+ * Parameters for EE data model context section.
+ * @enum {string}
+ */
+osapi.container.ee.Context = {
+    ASSOCIATED_CONTEXT: 'associatedContext',
+    OPENSOCIAL: 'openSocial'
+};
+
+/**
+ * Parameters for EE associated context.
+ * @enum {string}
+ */
+osapi.container.ee.AssociatedContext = {
+    ID: 'id',
+    TYPE: 'type',
+    OBJECT_REFERENCE: 'objectReference'
+};
+
+/**
+ * Parameters for EE model preferred experience target type.
+ * @enum {string}
+ */
+osapi.container.ee.TargetType = {
+    GADGET: 'gadget',
+    URL: 'url'
+};
+
+/**
+ * Parameters for EE model preferred experience display type.
+ * @enum {string}
+ */
+osapi.container.ee.DisplayType = {
+    IMAGE: 'image',
+    TEXT: 'text'
+};
+
+/**
+ * Additional config parameter when container support EE.
+ */
+osapi.container.ee.ContainerConfig = {
+    /**
+     * Used by container to override logic to determine target type of the EE.
+     *
+     * The first argument will be the EE data model and the second one will be the optional
+     * context from the container.
+     * The function should return either gadget or url
+     * @type {function}
+     *
+     * Example:
+     *   <code>
+     *     var config = {};
+     *     config[osapi.container.ee.ContainerConfig.GET_EE_NAVIGATION_TYPE] =
+              function(dataModel, opt_containerContext) {
+     *          return "gadget";
+     *        };
+     *   </code>
+     */
+    GET_EE_NAVIGATION_TYPE: 'GET_EE_NAVIGATION_TYPE'
+};
diff --git a/trunk/features/src/main/javascript/features/embeddedexperiences/embedded_experiences_container.js b/trunk/features/src/main/javascript/features/embeddedexperiences/embedded_experiences_container.js
new file mode 100644
index 0000000..67dcd37
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/embeddedexperiences/embedded_experiences_container.js
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * This feature adds additional functionality to the common container to support
+ * rendering embedded experiences.
+ */
+(function() {
+
+  osapi.container.Container.addMixin('ee', function(context) {
+    var ee_data_model = osapi.container.ee.DataModel;
+    var ee_pe = osapi.container.ee.PreferredExperience;
+    var ee_type = osapi.container.ee.TargetType;
+    var ee_context = osapi.container.ee.Context;
+    var ee_containerconfig = osapi.container.ee.ContainerConfig;
+
+    /**
+     * Navigates to an EE gadget
+     * @param {Element} element the element to put the gadget in.
+     * @param {Object} dataModel the EE data model.
+     * @param {Object} renderParams params to augment the rendering.
+     * @param {Function=} opt_callback called once the gadget has been navigated to.
+     * @param {Object=} opt_containerContext additional context that a container could pass to gadget.
+     *    The container context should contain at least "associatedContext" which is used
+     *    to define context where the gadget is displayed.
+     * @param {Element=} opt_bufferEl The optional element to use for double buffering when rendering the gadget.
+     */
+    function navigateGadget_(element, dataModel, renderParams, opt_callback, opt_containerContext, opt_bufferEl) {
+      var viewParams = renderParams[osapi.container.ee.RenderParam.GADGET_VIEW_PARAMS] || {};
+      var localRenderParams =
+        renderParams[osapi.container.ee.RenderParam.GADGET_RENDER_PARAMS] || {};
+      localRenderParams[osapi.container.RenderParam.VIEW] =
+        osapi.container.ee.RenderParam.EMBEDDED;
+
+      // Lets processes the "preferredExperience" part from the data model if available
+      var preferredExperience = dataModel[ee_data_model.PREFERRED_EXPERIENCE];
+      if(preferredExperience) {
+        var targetPE = preferredExperience[ee_pe.TARGET];
+        if(targetPE && targetPE[ee_pe.TYPE] === ee_type.GADGET) {
+          if(!!targetPE[ee_pe.VIEW]) {
+            localRenderParams[osapi.container.RenderParam.VIEW] = targetPE[ee_pe.VIEW];
+          }
+        }
+      }
+
+      // Now, lets update EE data model context if being passed additional context from container
+      if(opt_containerContext) {
+        // Check if the data model has context
+        var eeDataModelContext = dataModel[ee_data_model.CONTEXT];
+        var addContainerContext = true;
+        if (eeDataModelContext) {
+          // Need to check if the context of the EE model is object type to be able to append
+          // container additional context.
+          if (typeof eeDataModelContext != 'object') {
+            addContainerContext = false;
+          }
+        } else {
+          eeDataModelContext = {};
+        }
+
+        if(addContainerContext) {
+          var openSocialContext = {};
+          openSocialContext[ee_context.ASSOCIATED_CONTEXT] = {};
+          for (var property in opt_containerContext) {
+            if (opt_containerContext.hasOwnProperty(property)) {
+              openSocialContext[property] = opt_containerContext[property];
+            }
+          }
+
+          // Lets update the EE model context
+          eeDataModelContext[ee_context.OPENSOCIAL] = openSocialContext;
+          dataModel[ee_data_model.CONTEXT] = eeDataModelContext;
+        }
+      }
+      localRenderParams[osapi.container.ee.RenderParam.DATA_MODEL] = dataModel;
+
+      var site = context.newGadgetSite(element, opt_bufferEl);
+      var gadgetUrl = dataModel[ee_data_model.GADGET];
+
+      context.preloadGadget(gadgetUrl, function(result) {
+        if (!result[gadgetUrl] || result[gadgetUrl].error) {
+          //There was an error preloading the gadget URL lets try and render the
+          //URL EE if there is one
+          if (dataModel[ee_data_model.URL] != null) {
+            navigateUrl_(element, dataModel, renderParams, opt_callback);
+          }
+          else if (opt_callback != null) {
+            opt_callback(site, result[gadgetUrl] || {"error" : result});
+          }
+        }
+        else {
+          context.navigateGadget(site, gadgetUrl, viewParams, localRenderParams,
+            function(metadata) {
+              if (opt_callback != null) {
+                opt_callback(site, metadata);
+              }
+         });
+        }
+      });
+    }
+
+    /**
+     * Navigates to a URL
+     * @param {Element} element the element to render the URL in.
+     * @param {Object} dataModel the EE data model.
+     * @param {Object} renderParams params to augment the rendering.
+     *        Valid rendering parameters include osapi.container.RenderParam.CLASS,
+     *        osapi.container.RenderParam.HEIGHT, and osapi.container.RenderParam.WIDTH.
+     * @param {Function} opt_callback called when the URL has been navigated to.
+     */
+    function navigateUrl_(element, dataModel, renderParams, opt_callback) {
+      var urlRenderParams =
+        renderParams[osapi.container.ee.RenderParam.URL_RENDER_PARAMS] || {};
+      var site = context.newUrlSite(element);
+      var toReturn = context.navigateUrl(site, dataModel[ee_data_model.URL], urlRenderParams);
+      if (opt_callback) {
+        opt_callback(toReturn, null);
+      }
+    }
+
+    /**
+     * Try to get the target type from EE data model preferredExperience if any.
+     *
+     * @param {Object} dataModel the EE data model.
+     * @return {String} The target type of EE data model preferredExperience or null if not set.
+     */
+    function getPreferredEE_(dataModel) {
+      if(dataModel[ee_data_model.PREFERRED_EXPERIENCE]) {
+        var pe = dataModel[ee_data_model.PREFERRED_EXPERIENCE];
+        if (pe[ee_pe.TARGET]) {
+          var peTarget = pe[ee_pe.TARGET];
+          if(peTarget && peTarget[ee_pe.TYPE]) {
+            var type = peTarget[ee_pe.TYPE];
+            if((osapi.container.ee.TargetType.URL === type && typeof dataModel.url !== 'undefined') ||
+                    (osapi.container.ee.TargetType.GADGET === type &&
+                            typeof dataModel.gadget !== 'undefined')) {
+              return type
+            }
+          }
+        }
+      }
+      return null;
+    }
+
+    /**
+     * Handles the RPC request letting the container know that the embedded experience gadget is rendered.
+     * @param rpcArgs the RPC args from the request.
+     * @return void.
+     */
+    function gadgetRendered_(rpcArgs) {
+      var gadgetSite = rpcArgs.gs;
+      var renderParams = gadgetSite.getActiveSiteHolder().renderParams_;
+      var eeDataModel = renderParams.eeDataModel;
+      return eeDataModel ? eeDataModel[ee_data_model.CONTEXT] : null;
+    }
+
+    //Add the RPC handler to pass the context to the gadget
+    context.rpcRegister('ee_gadget_rendered', gadgetRendered_);
+
+    return {
+
+      /**
+       * Navigate to an embedded experience.  Call this method to render any embedded experience.
+       * @param {Element} element the element to render the embedded experience in.
+       * @param {Object} datModel the EE data model.
+       * @param {Object} renderParams parameters for the embedded experience.
+       * @param {Function=} opt_callback callback function which will be called after the
+       *        gadget has rendered.
+       * @param {Object=} opt_containerContext additional context that a container could pass to gadget
+       * @param {Element=} opt_bufferEl Element to use for double buffering when rendering a gadget.
+       */
+      'navigate' : function(element, dataModel, renderParams, opt_callback, opt_containerContext, opt_bufferEl) {
+        var preferredEE = null;
+        if (!!context.config_ && !!context.config_[ee_containerconfig.GET_EE_NAVIGATION_TYPE] &&
+            (typeof context.config_[ee_containerconfig.GET_EE_NAVIGATION_TYPE] === 'function')) {
+          preferredEE =
+            context.config_[ee_containerconfig.GET_EE_NAVIGATION_TYPE].call(context, dataModel);
+        }
+        else {
+          preferredEE = getPreferredEE_(dataModel);
+        }
+
+        // if no preference from the service lets check the model
+        if(preferredEE === null) {
+          if (dataModel[ee_data_model.GADGET]) {
+            preferredEE = osapi.container.ee.TargetType.GADGET;
+          }
+          else if (dataModel[ee_data_model.URL]) {
+            preferredEE = osapi.container.ee.TargetType.URL;
+          }
+        }
+
+        // Lets navigate
+        if (preferredEE === osapi.container.ee.TargetType.GADGET) {
+          navigateGadget_(element, dataModel, renderParams, opt_callback, opt_containerContext, opt_bufferEl);
+        }
+        else if (preferredEE === osapi.container.ee.TargetType.URL) {
+          navigateUrl_(element, dataModel, renderParams, opt_callback);
+        }
+      },
+
+      /**
+       * Closes the embedded experience on the page.
+       * @param {object} site one of osapi.container.GadgetSite or osapi.container.UrlSite.
+       */
+      'close' : function(site) {
+        /*
+         * At the moment this will work fine because an EE can be either a gadget or URL
+         * and at the moment they both have a close method.  However it is hard
+         * to have both the GadgetSite and UrlSite classes adhear to this contract in Javascript
+         * so it may be better to wrap both classes in one class.
+         */
+        if (site instanceof osapi.container.GadgetSite) {
+          context.closeGadget(site);
+        }
+
+        if (site instanceof osapi.container.UrlSite) {
+          site.close();
+        }
+      }
+    };
+  });
+})();
diff --git a/trunk/features/src/main/javascript/features/embeddedexperiences/embedded_experiences_gadgets.js b/trunk/features/src/main/javascript/features/embeddedexperiences/embedded_experiences_gadgets.js
new file mode 100644
index 0000000..e8ec591
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/embeddedexperiences/embedded_experiences_gadgets.js
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets['ee'] = gadgets['ee'] || {};
+
+(function() {
+
+  var contextListeners = [];
+  var isContextSet = false;
+
+  /**
+   * Sets the context for this embedded experience.
+   * @param {Object} context
+   *         The embedded experiences context.
+   */
+  function setDataContext(context) {
+    if (this.f !== '..') {
+      return;
+    }
+    opensocial.data.DataContext.putDataSet('org.opensocial.ee.context', context);
+  };
+
+  /**
+   * Init the embedded experiences feature.  This calls an RPC handler to get
+   * the embedded experiences context and puts it in the gadgets data context.
+   * @param {Object} config
+   *        Configuration for the feature.
+   */
+  function init(config) {
+    gadgets.rpc.call(null, 'ee_gadget_rendered', setDataContext, {});
+    gadgets.rpc.register('ee_set_context', setDataContext);
+    opensocial.data.getDataContext().registerListener('org.opensocial.ee.context', function(key) {
+      var context = opensocial.data.getDataContext().getDataSet(key);
+      isContextSet = true;
+      var length = contextListeners.length;
+      for(var i = length;  i--;) {
+        contextListeners[i](context);
+      }
+    });
+  };
+
+  if (gadgets.config) {
+    gadgets.config.register('embedded-experiences', null, init);
+  }
+
+  /**
+   * Registers a listener for when the embedded experiences context object is set for
+   * this gadget.  This listener will be called whenever the context is set for the gadget.
+   *
+   * @param {Function} listener
+   *        A function to be called when the listener is set.
+   */
+  gadgets.ee.registerContextListener = function(listener) {
+    //Add the listener regardless
+    contextListeners.push(listener);
+
+    //It could be that the context was already set before the gadget called this function
+    //so see if we have a context object in the data context and if we do call the listener
+    //back right away
+    if(isContextSet) {
+      listener(opensocial.data.getDataContext().getDataSet('org.opensocial.ee.context'));
+    }
+  };
+}());
diff --git a/trunk/features/src/main/javascript/features/embeddedexperiences/feature.xml b/trunk/features/src/main/javascript/features/embeddedexperiences/feature.xml
new file mode 100644
index 0000000..72fb09b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/embeddedexperiences/feature.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+    <name>embedded-experiences</name>
+    <dependency>core.config</dependency>
+    <dependency>opensocial-data-context</dependency>
+    <dependency>opensocial</dependency>
+    <dependency>container</dependency>
+    <gadget>
+        <script src="embedded_experiences_gadgets.js" />
+        <api>
+          <exports type="js">gadgets.ee.registerContextListener</exports>
+          <exports type="rpc">ee_set_context</exports>
+          <uses type="rpc">ee_gadget_rendered</uses>
+        </api>
+    </gadget>
+    <container>
+        <script src="constant.js" />
+        <script src="embedded_experiences_container.js" />
+        <api>
+            <exports type="js">osapi.container.Container.ee</exports>
+            <exports type="js">osapi.container.Container.ee.navigate</exports>
+            <exports type="js">osapi.container.Container.ee.close</exports>
+            <exports type="js">osapi.container.ee.RenderParam.GADGET_RENDER_PARAMS</exports>
+            <exports type="js">osapi.container.ee.RenderParam.GADGET_VIEW_PARAMS</exports>
+            <exports type="js">osapi.container.ee.RenderParam.URL_RENDER_PARAMS</exports>
+            <exports type="js">osapi.container.ee.RenderParam.DATA_MODEL</exports>
+            <exports type="js">osapi.container.ee.RenderParam.EMBEDDED</exports>
+            <exports type="js">osapi.container.ee.DataModel.CONTEXT</exports>
+            <exports type="js">osapi.container.ee.DataModel.GADGET</exports>
+            <exports type="js">osapi.container.ee.DataModel.URL</exports>
+            <exports type="js">osapi.container.ee.DataModel.PREVIEW_IMAGE</exports>
+            <exports type="js">osapi.container.ee.DataModel.PREFERRED_EXPERIENCE</exports>
+            <exports type="js">osapi.container.ee.PreferredExperience.TARGET</exports>
+            <exports type="js">osapi.container.ee.PreferredExperience.DISPLAY</exports>
+            <exports type="js">osapi.container.ee.PreferredExperience.TYPE</exports>
+            <exports type="js">osapi.container.ee.PreferredExperience.VIEW</exports>
+            <exports type="js">osapi.container.ee.PreferredExperience.VIEW_TARGET</exports>
+            <exports type="js">osapi.container.ee.TargetType.GADGET</exports>
+            <exports type="js">osapi.container.ee.TargetType.URL</exports>
+            <exports type="js">osapi.container.ee.DisplayType.IMAGE</exports>
+            <exports type="js">osapi.container.ee.DisplayType.TEXT</exports>
+            <exports type="js">osapi.container.ee.Context.ASSOCIATED_CONTEXT</exports>
+            <exports type="js">osapi.container.ee.Context.OPENSOCIAL</exports>
+            <exports type="js">osapi.container.ee.AssociatedContext.ID</exports>
+            <exports type="js">osapi.container.ee.AssociatedContext.TYPE</exports>
+            <exports type="js">osapi.container.ee.AssociatedContext.OBJECT_REFERENCE</exports>
+            <exports type="js">osapi.container.ee.ContainerConfig.GET_EE_NAVIGATION_TYPE</exports>
+            <exports type="rpc">ee_gadget_rendered</exports>
+        </api>
+    </container>
+</feature>
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/exportjs/exportjs.js b/trunk/features/src/main/javascript/features/exportjs/exportjs.js
new file mode 100644
index 0000000..caea301
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/exportjs/exportjs.js
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This provides a mechanism to export symbols, across all
+ * cases below. This implementation works best when used with ClosureJsCompiler.
+ *
+ * "global" object, ie: feature=globals.
+ * feature directive: <exports type="js">gadgets</exports>
+ * code: var gadgets = {};
+ *
+ * "singleton" object, ie: feature=rpc.
+ * feature directive: <exports type="js">gadgets.foo.bar</exports>
+ * gadgets.foo = function() {
+ *   return { bar : function() { ... } };
+ * }();
+ *
+ * "closured" object, ie: feature=shindig.uri.
+ * This wraps to a function that exports any resulting properties it returns
+ * in an Object. feature directive: <exports type="js">gadgets.foo.bar</exports>
+ * gadgets.foo = (function() {
+ *   return { bar : function() { ... } };
+ * })();
+ *
+ * "prototype" object, ie: feature=container.
+ * feature directive: <exports type="js">gadgets.foo.prototype.bar</exports>
+ * gadgets.foo = function() {};
+ * gadgets.foo.prototype.bar = function() { ... };
+ *
+ * "static" object.
+ * feature directive: <exports type="js">gadgets.foo.bar</exports>
+ * gadgets.foo = {};
+ * gadgets.foo.bar = function() { ... };
+ *
+ * Support for deferred symbol binding (via deferJs()) is also provided.
+ * A 'real' method is executed with the enqueued arguments when in deferMap.
+ */
+function exportJs(namespace, components, opt_props) {
+  var JSL = '___jsl';
+  var DEFER_KEY = 'df';
+  var base = window;
+  var prevBase = null;
+  var nsParts = namespace.split('.');
+
+  // Set up defer function queue.
+  var deferMap = ((window[JSL] = window[JSL] || {})[DEFER_KEY] = window[JSL][DEFER_KEY] || {});
+
+  for (var i = 0, part; part = nsParts.shift(); i++) {
+    base[part] = base[part] || components[i] || {};
+    prevBase = base;
+    base = base[part];
+  }
+
+  /**
+   * Exports properties/functions on the provided base object.
+   * If a property to export is a function, does not exist in its full
+   * form, and deferred mode is enabled, a stub is created.
+   * The stub enqueues requests that are executed by the real method
+   * when it is loaded and exported.
+   *
+   * @param {Object} root Base object to which to attach properties.
+   */
+  function exportProps(root) {
+    var props = opt_props || {};
+    for (var prop in props) {
+      if (props.hasOwnProperty(prop)) {
+        var curalias = props[prop];
+        var fulltok = namespace + '.' + curalias;
+        if (root.hasOwnProperty(prop)) {
+          if (!root[curalias]) {
+            root[curalias] = root[prop];
+          } else if (deferMap[fulltok]) {
+            // Executes enqueued requests for the method,
+            // then replaces the export.
+            deferMap[fulltok](root, root[prop]);
+            delete deferMap[fulltok];
+            root[curalias] = root[prop];
+          }
+        }
+      }
+    }
+  };
+
+  if (typeof base === 'object') {
+    exportProps(base);
+  } else if (typeof base === 'function') {
+    var exportedFn = function() {
+      var result = base.apply(null, arguments);
+      if (typeof result === 'object') {
+        exportProps(result);
+      }
+      return result;
+    };
+    prevBase[part] = exportedFn;
+  }
+}
diff --git a/trunk/features/src/main/javascript/features/exportjs/feature.xml b/trunk/features/src/main/javascript/features/exportjs/feature.xml
new file mode 100644
index 0000000..b7dc97d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/exportjs/feature.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>exportjs</name>
+  <all>
+    <script src="exportjs.js"/>
+    <!-- do not api+export exportJs -->
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/features.txt b/trunk/features/src/main/javascript/features/features.txt
new file mode 100644
index 0000000..436eac6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/features.txt
@@ -0,0 +1,111 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you 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.
+#
+# List each feature you want to support here
+
+features/globals/feature.xml
+features/actions/feature.xml
+features/auth-refresh/feature.xml
+features/caja/feature.xml
+features/caja/caja-debug.xml
+features/caja/es53-guest-frame.xml
+features/caja/es53-guest-frame.opt.xml
+features/caja/es53-taming-frame.xml
+features/caja/es53-taming-frame.opt.xml
+features/container/feature.xml
+features/container.site/feature.xml
+features/container.site.gadget/feature.xml
+features/container.site.url/feature.xml
+features/container.util/feature.xml
+features/content-rewrite/feature.xml
+features/core.config/feature.xml
+features/core.config.base/feature.xml
+features/core.io/feature.xml
+features/core.json/feature.xml
+features/core.legacy/feature.xml
+features/core.log/feature.xml
+features/core.none/feature.xml
+features/core.prefs/feature.xml
+features/core.util/feature.xml
+features/core.util.base/feature.xml
+features/core.util.dom/feature.xml
+features/core.util.event/feature.xml
+features/core.util.onload/feature.xml
+features/core.util.string/feature.xml
+features/core.util.urlparams/feature.xml
+features/core/feature.xml
+features/defer.test/feature.xml
+features/deferjs/feature.xml
+features/domnode/feature.xml
+features/dynamic-height.height/feature.xml
+features/dynamic-height/feature.xml
+features/dynamic-size.util/feature.xml
+features/dynamic-width.width/feature.xml
+features/dynamic-width/feature.xml
+features/embeddedexperiences/feature.xml
+features/exportjs/feature.xml
+features/flash/feature.xml
+features/gadgets.json.ext/feature.xml
+features/i18n/feature.xml
+features/jsondom/feature.xml
+features/locked-domain/feature.xml
+features/minimessage/feature.xml
+features/oauthpopup/feature.xml
+features/opensearch/feature.xml
+features/opensocial-0.9/feature.xml
+features/opensocial-1.0/feature.xml
+features/opensocial-2.0/feature.xml
+features/opensocial-2.5/feature.xml
+features/opensocial-base/feature.xml
+features/opensocial-current/feature.xml
+features/opensocial-data-context/feature.xml
+features/opensocial-data/feature.xml
+features/opensocial-jsonrpc/feature.xml
+features/opensocial-reference/feature.xml
+features/opensocial-templates/feature.xml
+features/open-views/feature.xml
+features/open-views.common/feature.xml
+features/open-views.ee/feature.xml
+features/open-views.gadget/feature.xml
+features/open-views.results/feature.xml
+features/open-views.url/feature.xml
+features/osapi.base/feature.xml
+features/osapi/feature.xml
+features/osml/feature.xml
+features/proxied-form-post/feature.xml
+features/pubsub/feature.xml
+features/rpc/feature.xml
+features/shared-script-frame/feature.xml
+features/security-token/feature.xml
+features/selection/feature.xml
+features/setprefs/feature.xml
+features/settitle/feature.xml
+features/shindig.auth/feature.xml
+features/shindig.container/feature.xml
+features/shindig.container-1.0/feature.xml
+features/shindig.random/feature.xml
+features/shindig.sha1/feature.xml
+features/shindig.uri/feature.xml
+features/shindig.uri.ext/feature.xml
+features/shindig.xhrwrapper/feature.xml
+features/skins/feature.xml
+features/tabs/feature.xml
+features/taming/feature.xml
+features/views/feature.xml
+features/xhrwrapper/feature.xml
+features/xmlutil/feature.xml
diff --git a/trunk/features/src/main/javascript/features/flash/feature.xml b/trunk/features/src/main/javascript/features/flash/feature.xml
new file mode 100644
index 0000000..22363f9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/flash/feature.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>flash</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>core.io</dependency>
+  <all>
+    <script src="flash.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.flash.getMajorVersion</exports>
+      <exports type="js">gadgets.flash.embedFlash</exports>
+      <exports type="js">gadgets.flash.embedCachedFlash</exports>
+      <exports type="js">_IG_GetFlashMajorVersion</exports>
+      <exports type="js">_IG_EmbedFlash</exports>
+      <exports type="js">_IG_EmbedCachedFlash</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/flash/flash.js b/trunk/features/src/main/javascript/features/flash/flash.js
new file mode 100644
index 0000000..3da7d0d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/flash/flash.js
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/*global ActiveXObject */
+
+/**
+ * @fileoverview This library provides a standard and convenient way to embed
+ * Flash content into gadgets.
+ */
+
+/**
+ * @class Embeds Flash content in gadgets.
+ */
+gadgets.flash = gadgets.flash || {};
+
+/**
+ * Detects Flash Player and its major version.
+ * @return {number} The major version of Flash Player
+ *                  or 0 if Flash is not supported.
+ */
+gadgets.flash.getMajorVersion = function() {
+  var flashMajorVersion = 0;
+  if (navigator.plugins && navigator.mimeTypes && navigator.mimeTypes.length) {
+    // Flash detection for browsers using Netscape's plugin architecture
+    var i = navigator.plugins['Shockwave Flash'];
+    if (i && i['description']) {
+      flashMajorVersion = parseInt(i['description'].match(/[0-9]+/)[0], 10);
+    }
+  } else {
+    // Flash detection for IE
+    // This is done by trying to create an ActiveX object with the name
+    // "ShockwaveFlash.ShockwaveFlash.{majorVersion}".
+    for (var version = 10; version > 0; version--) {
+      try {
+        var dummy = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.' + version);
+        return version;
+      } catch (e) {
+      }
+    }
+  }
+  return flashMajorVersion;
+};
+
+/**
+ * Used for unique IDs.
+ * @type {number}
+ * @private
+ */
+gadgets.flash.swfContainerId_ = 0;
+
+/**
+ * Injects a Flash file into the DOM tree.
+ * @param {string} swfUrl SWF URL.
+ * @param {string | Object} swfContainer The id or object reference of an
+ *     existing html container element.
+ * @param {number} swfVersion Minimal Flash Player version required.
+ * @param {Object=} opt_params An optional object that may contain any valid html
+ *     parameter. All attributes will be passed through to the flash movie on
+ *     creation.
+ * @return {boolean} Whether the function call completes successfully.
+ */
+gadgets.flash.embedFlash = function(swfUrl, swfContainer, swfVersion, opt_params) {
+  switch (typeof swfContainer) {
+    case 'string':
+      swfContainer = document.getElementById(swfContainer);
+    case 'object':
+      if (swfContainer && (typeof swfContainer.innerHTML === 'string')) {
+        break;
+      }
+    default:
+      return false;
+  }
+
+  switch (typeof opt_params) {
+    case 'undefined':
+      opt_params = {};
+    case 'object':
+      break;
+    default:
+      return false;
+  }
+
+  if (swfUrl.indexOf('//') == 0) {
+    swfUrl = document.location.protocol + swfUrl;
+  }
+
+  var ver = gadgets.flash.getMajorVersion();
+  if (ver) {
+    var swfVer = parseInt(swfVersion, 10);
+    if (isNaN(swfVer)) {
+      swfVer = 0;
+    }
+    if (ver >= swfVer) {
+      // Set default size
+      if (opt_params['width'] === void 0) {
+        opt_params['width'] = '100%';
+      }
+      if (opt_params['height'] === void 0) {
+        opt_params['height'] = '100%';
+      }
+      // Set the default "base" attribute
+      if (typeof opt_params['base'] !== 'string') {
+        var a = document.createElement('a');
+        a.href = swfUrl;
+        // Get the part up to the last slash
+        opt_params['base'] = a.href.match(/^(.*\/)[^\/]*$/)[1];
+      }
+      // Set wmode to "opaque" if it's not defined. The default value
+      // "window" is undesirable because browsers will render Flash
+      // on top of other html elements.
+      if (typeof opt_params['wmode'] !== 'string') {
+        opt_params['wmode'] = 'opaque';
+      }
+      while (!opt_params['id']) {
+        var newId = 'swfContainer' + gadgets.flash.swfContainerId_++;
+        if (!document.getElementById(newId)) {
+          opt_params['id'] = newId;
+        }
+      }
+      // Prepare flash object
+      var flashObj;
+      if (navigator.plugins && navigator.mimeTypes &&
+          navigator.mimeTypes.length) {
+        // Use <embed> tag for Netscape and Mozilla browsers
+        opt_params['type'] = 'application/x-shockwave-flash';
+        opt_params['src'] = swfUrl;
+
+        flashObj = document.createElement('embed');
+        for (var prop in opt_params) {
+          if (!/^swf_/.test(prop) && !/___$/.test(prop)) {
+            flashObj.setAttribute(prop, opt_params[prop]);
+          }
+        }
+        // Inject flash object
+        swfContainer.innerHTML = '';
+        swfContainer.appendChild(flashObj);
+        return true;
+      } else {
+        // Use <object> tag for IE
+        // For some odd reason IE demands that innerHTML be used to set <param>
+        // values; they're otherwise ignored. As such, we need to be careful
+        // what values we accept in opt_params to avoid it being possible to
+        // use this HTML generation for nefarious purposes.
+        var propIsHtmlSafe = function(val) {
+          return !/["<>]/.test(val);
+        };
+
+        opt_params['movie'] = swfUrl;
+        var attr = {
+          'width': opt_params['width'],
+          'height': opt_params['height'],
+          'classid': 'clsid:D27CDB6E-AE6D-11CF-96B8-444553540000'
+        };
+        if (opt_params['id']) {
+          attr['id'] = opt_params['id'];
+        }
+
+        var html = '<object';
+        for (var attrProp in attr) {
+          if (!/___$/.test(attrProp) &&
+              propIsHtmlSafe(attrProp) &&
+              propIsHtmlSafe(attr[attrProp])) {
+            html += ' ' + attrProp + '="' + attr[attrProp] + '"';
+          }
+        }
+        html += '>';
+
+        for (var paramsProp in opt_params) {
+          var param = document.createElement('param');
+          if (!/^swf_/.test(paramsProp) &&
+              !attr[paramsProp] &&
+              !/___$/.test(paramsProp) &&
+              propIsHtmlSafe(paramsProp) &&
+              propIsHtmlSafe(opt_params[paramsProp])) {
+            html += '<param name="' + paramsProp + '" value="'
+                 + opt_params[paramsProp] + '" />';
+          }
+        }
+        html += '</object>';
+      }
+      swfContainer.innerHTML = html;
+      return true;
+    }
+  }
+  return false;
+};
+
+/**
+ * Injects a cached Flash file into the DOM tree.
+ * Accepts the same parameters as gadgets.flash.embedFlash does.
+ * @param {string} swfUrl SWF URL.
+ * @param {string | Object} swfContainer The id or object reference of an
+ *     existing html container element.
+ * @param {number} swfVersion Minimal Flash Player version required.
+ * @param {Object=} opt_params An optional object that may contain any valid html
+ *     parameter. All attributes will be passed through to the flash movie on
+ *     creation.
+ * @return {boolean} Whether the function call completes successfully.
+ *
+ * @member gadgets.flash
+ */
+gadgets.flash.embedCachedFlash = function(swfUrl, swfContainer, swfVersion, opt_params) {
+  var url = gadgets.io.getProxyUrl(swfUrl, { rewriteMime: 'application/x-shockwave-flash' });
+  return gadgets.flash.embedFlash(url, swfContainer, swfVersion, opt_params);
+};
+
+/**
+ * iGoogle compatible way to get flash version.
+ * @deprecated use gadgets.flash.getMajorVersion instead.
+ * @see gadgets.flash.getMajorVersion
+ */
+var _IG_GetFlashMajorVersion = gadgets.flash.getMajorVersion;
+
+
+/**
+ * iGoogle compatible way to embed flash
+ * @deprecated use gadgets.flash.embedFlash instead.
+ * @see gadgets.flash.embedFlash
+ */
+var _IG_EmbedFlash = function(swfUrl, swfContainer, opt_params) {
+  return gadgets.flash.embedFlash(swfUrl, swfContainer, opt_params['swf_version'],
+      opt_params);
+};
+
+/**
+ * iGoogle compatible way to embed cached flash
+ * @deprecated use gadgets.flash.embedCachedFlash() instead.
+ * @see gadgets.flash.embedCachedFlash
+ */
+var _IG_EmbedCachedFlash = function(swfUrl, swfContainer, opt_params) {
+  return gadgets.flash.embedCachedFlash(swfUrl, swfContainer, opt_params['swf_version'],
+      opt_params);
+};
+
diff --git a/trunk/features/src/main/javascript/features/flash/taming.js b/trunk/features/src/main/javascript/features/flash/taming.js
new file mode 100644
index 0000000..f8e9ad0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/flash/taming.js
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.flash.* API to cajoled gadgets
+ */
+
+tamings___.push(function(imports) {
+  // TODO(felix8a): tame flash
+});
diff --git a/trunk/features/src/main/javascript/features/gadgets.json.ext/feature.xml b/trunk/features/src/main/javascript/features/gadgets.json.ext/feature.xml
new file mode 100644
index 0000000..afe5704
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/gadgets.json.ext/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>gadgets.json.ext</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>domnode</dependency>
+  <all>
+    <script src="json-xmltojson.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.json.xml.convertXmlToJson</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/gadgets.json.ext/json-xmltojson.js b/trunk/features/src/main/javascript/features/gadgets.json.ext/json-xmltojson.js
new file mode 100644
index 0000000..607b7ea
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/gadgets.json.ext/json-xmltojson.js
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ * Extends the gadgets.json namespace with code that translates arbitrary XML to JSON.
+ */
+
+/**
+ * @static
+ * @class Translates arbitrary XML to JSON
+ * @name gadgets.json.convertXmlToJson
+ */
+gadgets.json.xml = (function() {
+
+    /**
+     * Parses all the child nodes of a specific DOM element and adds them to the JSON object
+     * passed in.
+     *
+     * @param {Array} childNodes an array of DOM nodes.
+     * @param {Object} json The JSON object to use for the conversion.  The DOM nodes will be added to
+     * this JSON object.
+     */
+  function parseChildNodes(childNodes, json) {
+    for (var index = 0; index < childNodes.length; index++) {
+      var node = childNodes[index];
+      if (node.nodeType == DOM_TEXT_NODE) {
+        setTextNodeValue(json, node.nodeName, node);
+      }
+      else {
+
+        if (node.childNodes.length == 0) {
+          if (node.attributes != null && node.attributes.length != 0) {
+            /*
+             * If there are no children but there are attributes set the value for
+             * this node in the JSON object to the JSON for the attributes.  There is nothing
+             * left to do since there are no children.
+             */
+            setAttributes(node, json);
+          }
+          else {
+            /*
+             * If there are no children and no attributes set the value to null.
+             */
+            json[node.nodeName] = null;
+          }
+        }
+        else {
+          if (node.childNodes.length == 1 && node.firstChild.nodeType == DOM_TEXT_NODE && (node.attributes == null || node.attributes.length == 0)) {
+            /*
+             * There is only one child node and it is a text node AND we have no attributes so
+             * just extract the text value from the text node and set it in the JSON object.
+             */
+            setTextNodeValue(json, node.nodeName, node.firstChild);
+          }
+          else {
+            /*
+             * There are both children and attributes, so recursively call this method until we have
+             * reached the end.
+             */
+            setChildrenValues(json, node);
+          }
+        }
+      }
+    }
+    };
+
+    /**
+     * Sets the JSON values for the children of a specified DOM element.
+     * @param {Object} json the JSON object to set the values in.
+     * @param node the DOM node containing children.
+     */
+    function setChildrenValues(json, node) {
+      var currentValue = json[node.nodeName];
+      if (currentValue == null) {
+        /*
+         * If there is no value for this property (node name) than
+         * add the attributes and parse the children.
+         */
+        json[node.nodeName] = {};
+        if (node.attributes != null && node.attributes.length != 0) {
+          setAttributesValues(node.attributes, json[node.nodeName]);
+        }
+        parseChildNodes(node.childNodes, json[node.nodeName]);
+      }
+      else {
+        /*
+         * There is a value already for this property (node name) so
+         * we need to create an array for the values of this property.
+         * First add all the attributes then parse the children and create
+         * an array from the result.
+         */
+        var temp = {};
+        if (node.attributes != null && node.attributes.length != 0) {
+          setAttributesValues(node.attributes, temp);
+        }
+        parseChildNodes(node.childNodes, temp);
+        json[node.nodeName] = createValue(currentValue, temp);
+      }
+    };
+
+    /**
+     * Sets the JSON value for a text node.
+     * @param {Object} json the JSON object to set the values in.
+     * @param {string} nodeName the node name to set the value to.
+     * @param textNode the text node containing the value to set.
+     */
+    function setTextNodeValue(json, nodeName, textNode) {
+      var currentValue = json[nodeName];
+      if (currentValue != null) {
+        json[nodeName] = createValue(currentValue, textNode.nodeValue);
+      }
+      else {
+        json[nodeName] = textNode.nodeValue;
+      }
+    };
+
+    /**
+     * Handles creating the text node value.  In some cases you may want to
+     * create an array for the value if the node already has a value in the
+     * JSON object.
+     * @param currentValue the current value from the JSON object.
+     * @param node the text node containing the value.
+     */
+    function createValue(currentValue, value) {
+      if (currentValue instanceof Array) {
+        currentValue[currentValue.length] = value;
+        return currentValue;
+      }
+      else {
+        return new Array(currentValue, value);
+      }
+    };
+
+
+    /**
+     * Sets the attributes from a DOM node in a JSON object.
+     * @param node the node to add the attributes are on.
+     * @param json the json object to set the attributes in.
+     */
+    function setAttributes(node, json) {
+      var currentValue = json[node.nodeName];
+      if (currentValue == null) {
+        json[node.nodeName] = {};
+        setAttributesValues(node.attributes, json[node.nodeName]);
+      }
+      else {
+        var temp = {};
+        setAttributesValues(node.attributes, temp);
+        json[node.nodeName] = createValue(currentValue, temp);
+      }
+    };
+
+    /**
+     * Sets the values from attributes from a DOM node in a JSON object.
+     * @param attributes the DOM node's attributes.
+     * @param {Object} json the JSON object to set the values in.
+     */
+    function setAttributesValues(attributes, json) {
+      var attribute = null;
+      for (var attrIndex = 0; attrIndex < attributes.length; attrIndex++) {
+        attribute = attributes[attrIndex];
+        json['@' + attribute.nodeName] = attribute.nodeValue;
+      }
+    };
+
+    return {
+      convertXmlToJson: function(xmlDoc) {
+        var childNodes = xmlDoc.childNodes;
+        var result = {};
+        parseChildNodes(childNodes, result);
+        return result;
+      }
+    };
+
+})();
diff --git a/trunk/features/src/main/javascript/features/gadgets.json.ext/taming.js b/trunk/features/src/main/javascript/features/gadgets.json.ext/taming.js
new file mode 100644
index 0000000..ce2e968
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/gadgets.json.ext/taming.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    // TODO(felix8a): is this safe?
+    // [gadgets.json.xml, 'convertXmlToJson']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/globals/feature.xml b/trunk/features/src/main/javascript/features/globals/feature.xml
new file mode 100644
index 0000000..1f3cbe2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/globals/feature.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>globals</name>
+  <all>
+    <api>
+      <exports type="js">gadgets</exports>
+      <exports type="js">shindig</exports>
+      <exports type="js">osapi</exports>
+    </api>
+    <script src="globals.js"/>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/globals/globals.js b/trunk/features/src/main/javascript/features/globals/globals.js
new file mode 100644
index 0000000..5cd101b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/globals/globals.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @namespace The global gadgets namespace
+ * @type {Object}
+ */
+gadgets = window['gadgets'] || {};
+
+/**
+ * @namespace The global shindig namespace, used for shindig specific extensions and data
+ * @type {Object}
+ */
+shindig = window['shindig'] || {};
+
+/**
+ * @namespace The global osapi namespace, used for opensocial API specific extensions
+ * @type {Object}
+ */
+osapi = window['osapi'] || {};
diff --git a/trunk/features/src/main/javascript/features/i18n/currencycodemap.js b/trunk/features/src/main/javascript/features/i18n/currencycodemap.js
new file mode 100644
index 0000000..7b40b19
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/currencycodemap.js
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Currency code map.
+ */
+
+gadgets.i18n = gadgets.i18n || {};
+
+/**
+ * The mapping of currency symbol through intl currency code.
+ */
+
+gadgets.i18n.CurrencyCodeMap = {
+  'USD': '$',
+  'ARS': '$',
+  'AWG': '\u0192',
+  'AUD': '$',
+  'BSD': '$',
+  'BBD': '$',
+  'BEF': '\u20A3',
+  'BZD': '$',
+  'BMD': '$',
+  'BOB': '$',
+  'BRL': 'R$',
+  'BRC': '\u20A2',
+  'GBP': '\u00A3',
+  'BND': '$',
+  'KHR': '\u17DB',
+  'CAD': '$',
+  'KYD': '$',
+  'CLP': '$',
+  'CNY': '\u00A5',
+  'COP': '\u20B1',
+  'CRC': '\u20A1',
+  'CUP': '\u20B1',
+  'CYP': '\u00A3',
+  'DKK': 'kr',
+  'DOP': '\u20B1',
+  'XCD': '$',
+  'EGP': '\u00A3',
+  'SVC': '\u20A1',
+  'EUR': '\u20AC',
+  'XEU': '\u20A0',
+  'FKP': '\u00A3',
+  'FJD': '$',
+  'FRF': '\u20A3',
+  'GIP': '\u00A3',
+  'GRD': '\u20AF',
+  'GGP': '\u00A3',
+  'GYD': '$',
+  'NLG': '\u0192',
+  'HKD': '$',
+  'INR': '\u20A8',
+  'IRR': '\uFDFC',
+  'IEP': '\u00A3',
+  'IMP': '\u00A3',
+  'ILS': '\u20AA',
+  'ITL': '\u20A4',
+  'JMD': '$',
+  'JPY': '\u00A5',
+  'JEP': '\u00A3',
+  'KPW': '\u20A9',
+  'KRW': '\u20A9',
+  'LAK': '\u20AD',
+  'LBP': '\u00A3',
+  'LRD': '$',
+  'LUF': '\u20A3',
+  'MTL': '\u20A4',
+  'MUR': '\u20A8',
+  'MXN': '$',
+  'MNT': '\u20AE',
+  'NAD': '$',
+  'NPR': '\u20A8',
+  'ANG': '\u0192',
+  'NZD': '$',
+  'OMR': '\uFDFC',
+  'PKR': '\u20A8',
+  'PEN': 'S/.',
+  'PHP': '\u20B1',
+  'QAR': '\uFDFC',
+  'RUB': '\u0440\u0443\u0431',
+  'SHP': '\u00A3',
+  'SAR': '\uFDFC',
+  'SCR': '\u20A8',
+  'SGD': '$',
+  'SBD': '$',
+  'ZAR': 'R',
+  'ESP': '\u20A7',
+  'LKR': '\u20A8',
+  'SEK': 'kr',
+  'SRD': '$',
+  'SYP': '\u00A3',
+  'TWD': '\u5143',
+  'THB': '\u0E3F',
+  'TTD': '$',
+  'TRY': '\u20A4',
+  'TRL': '\u20A4',
+  'TVD': '$',
+  'UYU': '\u20B1',
+  'VAL': '\u20A4',
+  'VND': '\u20AB',
+  'YER': '\uFDFC',
+  'ZWD': '$'
+};
+
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa.js
new file mode 100644
index 0000000..2d2332b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  ERANAMES: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  NARROWMONTHS: ['Q', 'N', 'C', 'A', 'C', 'Q', 'Q', 'L', 'W', 'D', 'X', 'K'],
+  MONTHS: ['Qunxa Garablu', 'Kudo', 'Ciggilta Kudo', 'Agda Baxis', 'Caxah Alsa', 'Qasa Dirri', 'Qado Dirri', 'Liiqen', 'Waysu', 'Diteli', 'Ximoli', 'Kaxxa Garablu'],
+  SHORTMONTHS: ['Qun', 'Nah', 'Cig', 'Agd', 'Cax', 'Qas', 'Qad', 'Leq', 'Way', 'Dit', 'Xim', 'Kax'],
+  WEEKDAYS: ['Acaada', 'Etleeni', 'Talaata', 'Arbaqa', 'Kamiisi', 'Gumqata', 'Sabti'],
+  SHORTWEEKDAYS: ['Aca', 'Etl', 'Tal', 'Arb', 'Kam', 'Gum', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'E', 'T', 'A', 'K', 'G', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['saaku', 'carra'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_DJ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_DJ.js
new file mode 100644
index 0000000..8e399ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_DJ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  ERANAMES: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  NARROWMONTHS: ['Q', 'N', 'C', 'A', 'C', 'Q', 'Q', 'L', 'W', 'D', 'X', 'K'],
+  MONTHS: ['Qunxa Garablu', 'Naharsi Kudo', 'Ciggilta Kudo', 'Agda Baxisso', 'Caxah Alsa', 'Qasa Dirri', 'Qado Dirri', 'Leqeeni', 'Waysu', 'Diteli', 'Ximoli', 'Kaxxa Garablu'],
+  SHORTMONTHS: ['Qun', 'Nah', 'Cig', 'Agd', 'Cax', 'Qas', 'Qad', 'Leq', 'Way', 'Dit', 'Xim', 'Kax'],
+  WEEKDAYS: ['Acaada', 'Etleeni', 'Talaata', 'Arbaqa', 'Kamiisi', 'Gumqata', 'Sabti'],
+  SHORTWEEKDAYS: ['Aca', 'Etl', 'Tal', 'Arb', 'Kam', 'Gum', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'E', 'T', 'A', 'K', 'G', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['saaku', 'carra'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ER.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ER.js
new file mode 100644
index 0000000..a139a75d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ER.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  ERANAMES: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  NARROWMONTHS: ['Q', 'N', 'C', 'A', 'C', 'Q', 'Q', 'L', 'W', 'D', 'X', 'K'],
+  MONTHS: ['Qunxa Garablu', 'Kudo', 'Ciggilta Kudo', 'Agda Baxis', 'Caxah Alsa', 'Qasa Dirri', 'Qado Dirri', 'Liiqen', 'Waysu', 'Diteli', 'Ximoli', 'Kaxxa Garablu'],
+  SHORTMONTHS: ['Qun', 'Nah', 'Cig', 'Agd', 'Cax', 'Qas', 'Qad', 'Leq', 'Way', 'Dit', 'Xim', 'Kax'],
+  WEEKDAYS: ['Acaada', 'Etleeni', 'Talaata', 'Arbaqa', 'Kamiisi', 'Gumqata', 'Sabti'],
+  SHORTWEEKDAYS: ['Aca', 'Etl', 'Tal', 'Arb', 'Kam', 'Gum', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'E', 'T', 'A', 'K', 'G', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['saaku', 'carra'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ER_SAAHO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ER_SAAHO.js
new file mode 100644
index 0000000..f355198
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ER_SAAHO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  ERANAMES: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  NARROWMONTHS: ['Q', 'N', 'C', 'A', 'C', 'Q', 'Q', 'L', 'W', 'D', 'X', 'K'],
+  MONTHS: ['Qunxa Garablu', 'Kudo', 'Ciggilta Kudo', 'Agda Baxis', 'Caxah Alsa', 'Qasa Dirri', 'Qado Dirri', 'Liiqen', 'Waysu', 'Diteli', 'Ximoli', 'Kaxxa Garablu'],
+  SHORTMONTHS: ['Qun', 'Nah', 'Cig', 'Agd', 'Cax', 'Qas', 'Qad', 'Leq', 'Way', 'Dit', 'Xim', 'Kax'],
+  WEEKDAYS: ['Naba Sambat', 'Sani', 'Salus', 'Rabuq', 'Camus', 'Jumqata', 'Qunxa Sambat'],
+  SHORTWEEKDAYS: ['Nab', 'San', 'Sal', 'Rab', 'Cam', 'Jum', 'Qun'],
+  NARROWWEEKDAYS: ['A', 'E', 'T', 'A', 'K', 'G', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['saaku', 'carra'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ET.js
new file mode 100644
index 0000000..a139a75d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__aa_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  ERANAMES: ['Yaasuusuk Duma', 'Yaasuusuk Wadir'],
+  NARROWMONTHS: ['Q', 'N', 'C', 'A', 'C', 'Q', 'Q', 'L', 'W', 'D', 'X', 'K'],
+  MONTHS: ['Qunxa Garablu', 'Kudo', 'Ciggilta Kudo', 'Agda Baxis', 'Caxah Alsa', 'Qasa Dirri', 'Qado Dirri', 'Liiqen', 'Waysu', 'Diteli', 'Ximoli', 'Kaxxa Garablu'],
+  SHORTMONTHS: ['Qun', 'Nah', 'Cig', 'Agd', 'Cax', 'Qas', 'Qad', 'Leq', 'Way', 'Dit', 'Xim', 'Kax'],
+  WEEKDAYS: ['Acaada', 'Etleeni', 'Talaata', 'Arbaqa', 'Kamiisi', 'Gumqata', 'Sabti'],
+  SHORTWEEKDAYS: ['Aca', 'Etl', 'Tal', 'Arb', 'Kam', 'Gum', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'E', 'T', 'A', 'K', 'G', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['saaku', 'carra'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af.js
new file mode 100644
index 0000000..f49021a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v.C.', 'n.C.'],
+  ERANAMES: ['voor Christus', 'na Christus'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', 'Augustus', 'September', 'Oktober', 'November', 'Desember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Sondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag'],
+  SHORTWEEKDAYS: ['So', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Sa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1ste kwartaal', '2de kwartaal', '3de kwartaal', '4de kwartaal'],
+  AMPMS: ['vm.', 'nm.'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'dd MMM y', 'yyyy/MM/dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af_NA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af_NA.js
new file mode 100644
index 0000000..5d39928
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af_NA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v.C.', 'n.C.'],
+  ERANAMES: ['voor Christus', 'na Christus'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', 'Augustus', 'September', 'Oktober', 'November', 'Desember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Sondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag'],
+  SHORTWEEKDAYS: ['So', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Sa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1ste kwartaal', '2de kwartaal', '3de kwartaal', '4de kwartaal'],
+  AMPMS: ['vm.', 'nm.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af_ZA.js
new file mode 100644
index 0000000..f49021a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__af_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v.C.', 'n.C.'],
+  ERANAMES: ['voor Christus', 'na Christus'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januarie', 'Februarie', 'Maart', 'April', 'Mei', 'Junie', 'Julie', 'Augustus', 'September', 'Oktober', 'November', 'Desember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Sondag', 'Maandag', 'Dinsdag', 'Woensdag', 'Donderdag', 'Vrydag', 'Saterdag'],
+  SHORTWEEKDAYS: ['So', 'Ma', 'Di', 'Wo', 'Do', 'Vr', 'Sa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1ste kwartaal', '2de kwartaal', '3de kwartaal', '4de kwartaal'],
+  AMPMS: ['vm.', 'nm.'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'dd MMM y', 'yyyy/MM/dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ak.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ak.js
new file mode 100644
index 0000000..0c349b7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ak.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['AK', 'KE'],
+  ERANAMES: ['Ansa Kristo', 'Kristo Ekyiri'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Sanda-\u0186p\u025bp\u0254n', 'Kwakwar-\u0186gyefuo', 'Eb\u0254w-\u0186benem', 'Eb\u0254bira-Oforisuo', 'Esusow Aketseaba-K\u0254t\u0254nimba', 'Obirade-Ay\u025bwohomumu', 'Ay\u025bwoho-Kitawonsa', 'Difuu-\u0186sandaa', 'Fankwa-\u0190b\u0254', '\u0186b\u025bs\u025b-Ahinime', '\u0186ber\u025bf\u025bw-Obubuo', 'Mumu-\u0186p\u025bnimba'],
+  SHORTMONTHS: ['S-\u0186', 'K-\u0186', 'E-\u0186', 'E-O', 'E-K', 'O-A', 'A-K', 'D-\u0186', 'F-\u0190', '\u0186-A', '\u0186-O', 'M-\u0186'],
+  WEEKDAYS: ['Kwesida', 'Dwowda', 'Benada', 'Wukuda', 'Yawda', 'Fida', 'Memeneda'],
+  SHORTWEEKDAYS: ['Kwe', 'Dwo', 'Ben', 'Wuk', 'Yaw', 'Fia', 'Mem'],
+  NARROWWEEKDAYS: ['K', 'D', 'B', 'W', 'Y', 'F', 'M'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AN', 'EW'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ak_GH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ak_GH.js
new file mode 100644
index 0000000..0c349b7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ak_GH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['AK', 'KE'],
+  ERANAMES: ['Ansa Kristo', 'Kristo Ekyiri'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Sanda-\u0186p\u025bp\u0254n', 'Kwakwar-\u0186gyefuo', 'Eb\u0254w-\u0186benem', 'Eb\u0254bira-Oforisuo', 'Esusow Aketseaba-K\u0254t\u0254nimba', 'Obirade-Ay\u025bwohomumu', 'Ay\u025bwoho-Kitawonsa', 'Difuu-\u0186sandaa', 'Fankwa-\u0190b\u0254', '\u0186b\u025bs\u025b-Ahinime', '\u0186ber\u025bf\u025bw-Obubuo', 'Mumu-\u0186p\u025bnimba'],
+  SHORTMONTHS: ['S-\u0186', 'K-\u0186', 'E-\u0186', 'E-O', 'E-K', 'O-A', 'A-K', 'D-\u0186', 'F-\u0190', '\u0186-A', '\u0186-O', 'M-\u0186'],
+  WEEKDAYS: ['Kwesida', 'Dwowda', 'Benada', 'Wukuda', 'Yawda', 'Fida', 'Memeneda'],
+  SHORTWEEKDAYS: ['Kwe', 'Dwo', 'Ben', 'Wuk', 'Yaw', 'Fia', 'Mem'],
+  NARROWWEEKDAYS: ['K', 'D', 'B', 'W', 'Y', 'F', 'M'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AN', 'EW'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__am.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__am.js
new file mode 100644
index 0000000..994b860
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__am.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u12a5\u1211\u12f5', '\u1230\u129e', '\u121b\u12ad\u1230\u129e', '\u1228\u1261\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1265', '\u1245\u12f3\u121c'],
+  SHORTWEEKDAYS: ['\u12a5\u1211\u12f5', '\u1230\u129e', '\u121b\u12ad\u1230', '\u1228\u1261\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1265', '\u1245\u12f3\u121c'],
+  NARROWWEEKDAYS: ['\u12a5', '\u1230', '\u121b', '\u1228', '\u1210', '\u12d3', '\u1245'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u1321\u12cb\u1275', '\u12a8\u1233\u12d3\u1275'],
+  DATEFORMATS: ['EEEE\u1363 dd MMMM \u1240\u1295 y G', 'dd MMMM y', 'MMM d y', 'dd/MM/yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__am_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__am_ET.js
new file mode 100644
index 0000000..994b860
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__am_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u12a5\u1211\u12f5', '\u1230\u129e', '\u121b\u12ad\u1230\u129e', '\u1228\u1261\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1265', '\u1245\u12f3\u121c'],
+  SHORTWEEKDAYS: ['\u12a5\u1211\u12f5', '\u1230\u129e', '\u121b\u12ad\u1230', '\u1228\u1261\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1265', '\u1245\u12f3\u121c'],
+  NARROWWEEKDAYS: ['\u12a5', '\u1230', '\u121b', '\u1228', '\u1210', '\u12d3', '\u1245'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u1321\u12cb\u1275', '\u12a8\u1233\u12d3\u1275'],
+  DATEFORMATS: ['EEEE\u1363 dd MMMM \u1240\u1295 y G', 'dd MMMM y', 'MMM d y', 'dd/MM/yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar.js
new file mode 100644
index 0000000..c4faaac
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_AE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_AE.js
new file mode 100644
index 0000000..053cdf9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_AE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_BH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_BH.js
new file mode 100644
index 0000000..c4faaac
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_BH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_DZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_DZ.js
new file mode 100644
index 0000000..b8bff53
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_DZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_EG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_EG.js
new file mode 100644
index 0000000..c4faaac
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_EG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_IQ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_IQ.js
new file mode 100644
index 0000000..c4faaac
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_IQ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_JO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_JO.js
new file mode 100644
index 0000000..fd5fee5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_JO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0634\u0628\u0627\u0637', '\u0622\u0630\u0627\u0631', '\u0646\u064a\u0633\u0627\u0646', '\u0623\u064a\u0627\u0631', '\u062d\u0632\u064a\u0631\u0627\u0646', '\u062a\u0645\u0648\u0632', '\u0622\u0628', '\u0623\u064a\u0644\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644'],
+  SHORTMONTHS: ['\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0634\u0628\u0627\u0637', '\u0622\u0630\u0627\u0631', '\u0646\u064a\u0633\u0627\u0646', '\u0623\u064a\u0627\u0631', '\u062d\u0632\u064a\u0631\u0627\u0646', '\u062a\u0645\u0648\u0632', '\u0622\u0628', '\u0623\u064a\u0644\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0627\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_KW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_KW.js
new file mode 100644
index 0000000..b8bff53
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_KW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_LB.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_LB.js
new file mode 100644
index 0000000..79ef21d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_LB.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0634\u0628\u0627\u0637', '\u0622\u0630\u0627\u0631', '\u0646\u064a\u0633\u0627\u0646', '\u0646\u0648\u0627\u0631', '\u062d\u0632\u064a\u0631\u0627\u0646', '\u062a\u0645\u0648\u0632', '\u0622\u0628', '\u0623\u064a\u0644\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644'],
+  SHORTMONTHS: ['\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0634\u0628\u0627\u0637', '\u0622\u0630\u0627\u0631', '\u0646\u064a\u0633\u0627\u0646', '\u0646\u0648\u0627\u0631', '\u062d\u0632\u064a\u0631\u0627\u0646', '\u062a\u0645\u0648\u0632', '\u0622\u0628', '\u0623\u064a\u0644\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0627\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_LY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_LY.js
new file mode 100644
index 0000000..c4faaac
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_LY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_MA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_MA.js
new file mode 100644
index 0000000..c4faaac
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_MA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_OM.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_OM.js
new file mode 100644
index 0000000..b8bff53
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_OM.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_QA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_QA.js
new file mode 100644
index 0000000..9d2261b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_QA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0627\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SA.js
new file mode 100644
index 0000000..cfafb1a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0627\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SD.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SD.js
new file mode 100644
index 0000000..b8bff53
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SD.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0623\u062d\u062f', '\u0625\u062b\u0646\u064a\u0646', '\u062b\u0644\u0627\u062b\u0627\u0621', '\u0623\u0631\u0628\u0639\u0627\u0621', '\u062e\u0645\u064a\u0633', '\u062c\u0645\u0639\u0629', '\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SY.js
new file mode 100644
index 0000000..bfc83aa
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_SY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0634\u0628\u0627\u0637', '\u0622\u0630\u0627\u0631', '\u0646\u064a\u0633\u0627\u0646', '\u0646\u0648\u0627\u0631', '\u062d\u0632\u064a\u0631\u0627\u0646', '\u062a\u0645\u0648\u0632', '\u0622\u0628', '\u0623\u064a\u0644\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644'],
+  SHORTMONTHS: ['\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0634\u0628\u0627\u0637', '\u0622\u0630\u0627\u0631', '\u0646\u064a\u0633\u0627\u0646', '\u0646\u0648\u0627\u0631', '\u062d\u0632\u064a\u0631\u0627\u0646', '\u062a\u0645\u0648\u0632', '\u0622\u0628', '\u0623\u064a\u0644\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u0623\u0648\u0644', '\u062a\u0634\u0631\u064a\u0646 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0643\u0627\u0646\u0648\u0646 \u0627\u0644\u0623\u0648\u0644'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0627\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_TN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_TN.js
new file mode 100644
index 0000000..9d2261b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_TN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0627\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_YE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_YE.js
new file mode 100644
index 0000000..cfafb1a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ar_YE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645', '\u0645'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0644\u0645\u064a\u0644\u0627\u062f', '\u0645\u064a\u0644\u0627\u062f\u064a'],
+  NARROWMONTHS: ['\u064a', '\u0641', '\u0645', '\u0623', '\u0648', '\u0646', '\u0644', '\u063a', '\u0633', '\u0643', '\u0628', '\u062f'],
+  MONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u064a\u0646\u0627\u064a\u0631', '\u0641\u0628\u0631\u0627\u064a\u0631', '\u0645\u0627\u0631\u0633', '\u0623\u0628\u0631\u064a\u0644', '\u0645\u0627\u064a\u0648', '\u064a\u0648\u0646\u064a\u0648', '\u064a\u0648\u0644\u064a\u0648', '\u0623\u063a\u0633\u0637\u0633', '\u0633\u0628\u062a\u0645\u0628\u0631', '\u0623\u0643\u062a\u0648\u0628\u0631', '\u0646\u0648\u0641\u0645\u0628\u0631', '\u062f\u064a\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0625\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  SHORTWEEKDAYS: ['\u0627\u0644\u0623\u062d\u062f', '\u0627\u0644\u0627\u062b\u0646\u064a\u0646', '\u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621', '\u0627\u0644\u0623\u0631\u0628\u0639\u0627\u0621', '\u0627\u0644\u062e\u0645\u064a\u0633', '\u0627\u0644\u062c\u0645\u0639\u0629', '\u0627\u0644\u0633\u0628\u062a'],
+  NARROWWEEKDAYS: ['\u062d', '\u0646', '\u062b', '\u0631', '\u062e', '\u062c', '\u0633'],
+  SHORTQUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  QUARTERS: ['\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0623\u0648\u0644', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0646\u064a', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u062b\u0627\u0644\u062b', '\u0627\u0644\u0631\u0628\u0639 \u0627\u0644\u0631\u0627\u0628\u0639'],
+  AMPMS: ['\u0635', '\u0645'],
+  DATEFORMATS: ['EEEE\u060c d MMMM\u060c y', 'd MMMM\u060c y', 'dd\u200f/MM\u200f/yyyy', 'd\u200f/M\u200f/yyyy'],
+  TIMEFORMATS: ['zzzz h:mm:ss a', 'z h:mm:ss a', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__as.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__as.js
new file mode 100644
index 0000000..01646a7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__as.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09f0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09f0\u09c1\u09af\u09bc\u09be\u09f0\u09c0', '\u09ae\u09be\u09f0\u09cd\u099a', '\u098f\u09aa\u09cd\u09f0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b7\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09f0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09f0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09f0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09f0'],
+  SHORTMONTHS: ['\u099c\u09be\u09a8\u09c1', '\u09ab\u09c7\u09ac\u09cd\u09f0\u09c1', '\u09ae\u09be\u09f0\u09cd\u099a', '\u098f\u09aa\u09cd\u09f0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997', '\u09b8\u09c7\u09aa\u09cd\u099f', '\u0985\u0995\u09cd\u099f\u09cb', '\u09a8\u09ad\u09c7', '\u09a1\u09bf\u09b8\u09c7'],
+  WEEKDAYS: ['\u09a6\u09c7\u0993\u09ac\u09be\u09f0', '\u09b8\u09cb\u09ae\u09ac\u09be\u09f0', '\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09f0', '\u09ac\u09c1\u09a7\u09ac\u09be\u09f0', '\u09ac\u09c3\u09b9\u09b7\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09f0', '\u09b6\u09c1\u0995\u09cd\u09f0\u09ac\u09be\u09f0', '\u09b6\u09a8\u09bf\u09ac\u09be\u09f0'],
+  SHORTWEEKDAYS: ['\u09f0\u09ac\u09bf', '\u09b8\u09cb\u09ae', '\u09ae\u0999\u09cd\u0997\u09b2', '\u09ac\u09c1\u09a7', '\u09ac\u09c3\u09b9\u09b7\u09cd\u09aa\u09a4\u09bf', '\u09b6\u09c1\u0995\u09cd\u09f0', '\u09b6\u09a8\u09bf'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u09aa\u09c2\u09f0\u09cd\u09ac\u09be', '\u0985\u09aa'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'dd-MM-yyyy', 'd-M-yyyy'],
+  TIMEFORMATS: ['h.mm.ss a zzzz', 'h.mm.ss a z', 'h.mm.ss a', 'h.mm. a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__as_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__as_IN.js
new file mode 100644
index 0000000..01646a7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__as_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09f0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09f0\u09c1\u09af\u09bc\u09be\u09f0\u09c0', '\u09ae\u09be\u09f0\u09cd\u099a', '\u098f\u09aa\u09cd\u09f0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b7\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09f0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09f0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09f0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09f0'],
+  SHORTMONTHS: ['\u099c\u09be\u09a8\u09c1', '\u09ab\u09c7\u09ac\u09cd\u09f0\u09c1', '\u09ae\u09be\u09f0\u09cd\u099a', '\u098f\u09aa\u09cd\u09f0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997', '\u09b8\u09c7\u09aa\u09cd\u099f', '\u0985\u0995\u09cd\u099f\u09cb', '\u09a8\u09ad\u09c7', '\u09a1\u09bf\u09b8\u09c7'],
+  WEEKDAYS: ['\u09a6\u09c7\u0993\u09ac\u09be\u09f0', '\u09b8\u09cb\u09ae\u09ac\u09be\u09f0', '\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09f0', '\u09ac\u09c1\u09a7\u09ac\u09be\u09f0', '\u09ac\u09c3\u09b9\u09b7\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09f0', '\u09b6\u09c1\u0995\u09cd\u09f0\u09ac\u09be\u09f0', '\u09b6\u09a8\u09bf\u09ac\u09be\u09f0'],
+  SHORTWEEKDAYS: ['\u09f0\u09ac\u09bf', '\u09b8\u09cb\u09ae', '\u09ae\u0999\u09cd\u0997\u09b2', '\u09ac\u09c1\u09a7', '\u09ac\u09c3\u09b9\u09b7\u09cd\u09aa\u09a4\u09bf', '\u09b6\u09c1\u0995\u09cd\u09f0', '\u09b6\u09a8\u09bf'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u09aa\u09c2\u09f0\u09cd\u09ac\u09be', '\u0985\u09aa'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'dd-MM-yyyy', 'd-M-yyyy'],
+  TIMEFORMATS: ['h.mm.ss a zzzz', 'h.mm.ss a z', 'h.mm.ss a', 'h.mm. a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az.js
new file mode 100644
index 0000000..bf463a6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.\u0259.', 'b.e.'],
+  ERANAMES: ['eram\u0131zdan \u0259vv\u0259l', 'bizim eram\u0131z\u0131n'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Yanvar', 'Fevral', 'Mart', 'Aprel', 'May', '\u0130yun', '\u0130yul', 'Avqust', 'Sentyabr', 'Oktyabr', 'Noyabr', 'Dekabr'],
+  SHORTMONTHS: ['yan', 'fev', 'mar', 'apr', 'may', 'iyn', 'iyl', 'avq', 'sen', 'okt', 'noy', 'dek'],
+  WEEKDAYS: ['bazar', 'bazar ert\u0259si', '\u00e7\u0259r\u015f\u0259nb\u0259 ax\u015fam\u0131', '\u00e7\u0259r\u015f\u0259nb\u0259', 'c\u00fcm\u0259 ax\u015fam\u0131', 'c\u00fcm\u0259', '\u015f\u0259nb\u0259'],
+  SHORTWEEKDAYS: ['B.', 'B.E.', '\u00c7.A.', '\u00c7.', 'C.A.', 'C', '\u015e.'],
+  NARROWWEEKDAYS: ['7', '1', '2', '3', '4', '5', '6'],
+  SHORTQUARTERS: ['1-ci kv.', '2-ci kv.', '3-c\u00fc kv.', '4-c\u00fc kv.'],
+  QUARTERS: ['1-ci kvartal', '2-ci kvartal', '3-c\u00fc kvartal', '4-c\u00fc kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM, y', 'd MMMM , y', 'd MMM, y', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_AZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_AZ.js
new file mode 100644
index 0000000..bf463a6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_AZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.\u0259.', 'b.e.'],
+  ERANAMES: ['eram\u0131zdan \u0259vv\u0259l', 'bizim eram\u0131z\u0131n'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Yanvar', 'Fevral', 'Mart', 'Aprel', 'May', '\u0130yun', '\u0130yul', 'Avqust', 'Sentyabr', 'Oktyabr', 'Noyabr', 'Dekabr'],
+  SHORTMONTHS: ['yan', 'fev', 'mar', 'apr', 'may', 'iyn', 'iyl', 'avq', 'sen', 'okt', 'noy', 'dek'],
+  WEEKDAYS: ['bazar', 'bazar ert\u0259si', '\u00e7\u0259r\u015f\u0259nb\u0259 ax\u015fam\u0131', '\u00e7\u0259r\u015f\u0259nb\u0259', 'c\u00fcm\u0259 ax\u015fam\u0131', 'c\u00fcm\u0259', '\u015f\u0259nb\u0259'],
+  SHORTWEEKDAYS: ['B.', 'B.E.', '\u00c7.A.', '\u00c7.', 'C.A.', 'C', '\u015e.'],
+  NARROWWEEKDAYS: ['7', '1', '2', '3', '4', '5', '6'],
+  SHORTQUARTERS: ['1-ci kv.', '2-ci kv.', '3-c\u00fc kv.', '4-c\u00fc kv.'],
+  QUARTERS: ['1-ci kvartal', '2-ci kvartal', '3-c\u00fc kvartal', '4-c\u00fc kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM, y', 'd MMMM , y', 'd MMM, y', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Cyrl.js
new file mode 100644
index 0000000..6ea5071
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Cyrl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.\u0259.', 'b.e.'],
+  ERANAMES: ['eram\u0131zdan \u0259vv\u0259l', 'bizim eram\u0131z\u0131n'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0458\u0430\u043d\u0432\u0430\u0440', '\u0444\u0435\u0432\u0440\u0430\u043b', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0435\u043b', '\u043c\u0430\u0439', '\u0438\u0458\u0443\u043d', '\u0438\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043d\u0442\u0458\u0430\u0431\u0440', '\u043e\u043a\u0442\u0458\u0430\u0431\u0440', '\u043d\u043e\u0458\u0430\u0431\u0440', '\u0434\u0435\u043a\u0430\u0431\u0440'],
+  SHORTMONTHS: ['yan', 'fev', 'mar', 'apr', 'may', 'iyn', 'iyl', 'avq', 'sen', 'okt', 'noy', 'dek'],
+  WEEKDAYS: ['\u0431\u0430\u0437\u0430\u0440', '\u0431\u0430\u0437\u0430\u0440 \u0435\u0440\u0442\u04d9\u0441\u0438', '\u0447\u04d9\u0440\u0448\u04d9\u043d\u0431\u04d9 \u0430\u0445\u0448\u0430\u043c\u044b', '\u0447\u04d9\u0440\u0448\u04d9\u043d\u0431\u04d9', '\u04b9\u04af\u043c\u04d9 \u0430\u0445\u0448\u0430\u043c\u044b', '\u04b9\u04af\u043c\u04d9', '\u0448\u04d9\u043d\u0431\u04d9'],
+  SHORTWEEKDAYS: ['B.', 'B.E.', '\u00c7.A.', '\u00c7.', 'C.A.', 'C', '\u015e.'],
+  NARROWWEEKDAYS: ['7', '1', '2', '3', '4', '5', '6'],
+  SHORTQUARTERS: ['1-ci kv.', '2-ci kv.', '3-c\u00fc kv.', '4-c\u00fc kv.'],
+  QUARTERS: ['1-ci kvartal', '2-ci kvartal', '3-c\u00fc kvartal', '4-c\u00fc kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM, y', 'd MMMM , y', 'd MMM, y', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Cyrl_AZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Cyrl_AZ.js
new file mode 100644
index 0000000..6ea5071
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Cyrl_AZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.\u0259.', 'b.e.'],
+  ERANAMES: ['eram\u0131zdan \u0259vv\u0259l', 'bizim eram\u0131z\u0131n'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0458\u0430\u043d\u0432\u0430\u0440', '\u0444\u0435\u0432\u0440\u0430\u043b', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0435\u043b', '\u043c\u0430\u0439', '\u0438\u0458\u0443\u043d', '\u0438\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043d\u0442\u0458\u0430\u0431\u0440', '\u043e\u043a\u0442\u0458\u0430\u0431\u0440', '\u043d\u043e\u0458\u0430\u0431\u0440', '\u0434\u0435\u043a\u0430\u0431\u0440'],
+  SHORTMONTHS: ['yan', 'fev', 'mar', 'apr', 'may', 'iyn', 'iyl', 'avq', 'sen', 'okt', 'noy', 'dek'],
+  WEEKDAYS: ['\u0431\u0430\u0437\u0430\u0440', '\u0431\u0430\u0437\u0430\u0440 \u0435\u0440\u0442\u04d9\u0441\u0438', '\u0447\u04d9\u0440\u0448\u04d9\u043d\u0431\u04d9 \u0430\u0445\u0448\u0430\u043c\u044b', '\u0447\u04d9\u0440\u0448\u04d9\u043d\u0431\u04d9', '\u04b9\u04af\u043c\u04d9 \u0430\u0445\u0448\u0430\u043c\u044b', '\u04b9\u04af\u043c\u04d9', '\u0448\u04d9\u043d\u0431\u04d9'],
+  SHORTWEEKDAYS: ['B.', 'B.E.', '\u00c7.A.', '\u00c7.', 'C.A.', 'C', '\u015e.'],
+  NARROWWEEKDAYS: ['7', '1', '2', '3', '4', '5', '6'],
+  SHORTQUARTERS: ['1-ci kv.', '2-ci kv.', '3-c\u00fc kv.', '4-c\u00fc kv.'],
+  QUARTERS: ['1-ci kvartal', '2-ci kvartal', '3-c\u00fc kvartal', '4-c\u00fc kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM, y', 'd MMMM , y', 'd MMM, y', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Latn.js
new file mode 100644
index 0000000..bf463a6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Latn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.\u0259.', 'b.e.'],
+  ERANAMES: ['eram\u0131zdan \u0259vv\u0259l', 'bizim eram\u0131z\u0131n'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Yanvar', 'Fevral', 'Mart', 'Aprel', 'May', '\u0130yun', '\u0130yul', 'Avqust', 'Sentyabr', 'Oktyabr', 'Noyabr', 'Dekabr'],
+  SHORTMONTHS: ['yan', 'fev', 'mar', 'apr', 'may', 'iyn', 'iyl', 'avq', 'sen', 'okt', 'noy', 'dek'],
+  WEEKDAYS: ['bazar', 'bazar ert\u0259si', '\u00e7\u0259r\u015f\u0259nb\u0259 ax\u015fam\u0131', '\u00e7\u0259r\u015f\u0259nb\u0259', 'c\u00fcm\u0259 ax\u015fam\u0131', 'c\u00fcm\u0259', '\u015f\u0259nb\u0259'],
+  SHORTWEEKDAYS: ['B.', 'B.E.', '\u00c7.A.', '\u00c7.', 'C.A.', 'C', '\u015e.'],
+  NARROWWEEKDAYS: ['7', '1', '2', '3', '4', '5', '6'],
+  SHORTQUARTERS: ['1-ci kv.', '2-ci kv.', '3-c\u00fc kv.', '4-c\u00fc kv.'],
+  QUARTERS: ['1-ci kvartal', '2-ci kvartal', '3-c\u00fc kvartal', '4-c\u00fc kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM, y', 'd MMMM , y', 'd MMM, y', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Latn_AZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Latn_AZ.js
new file mode 100644
index 0000000..bf463a6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__az_Latn_AZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.\u0259.', 'b.e.'],
+  ERANAMES: ['eram\u0131zdan \u0259vv\u0259l', 'bizim eram\u0131z\u0131n'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Yanvar', 'Fevral', 'Mart', 'Aprel', 'May', '\u0130yun', '\u0130yul', 'Avqust', 'Sentyabr', 'Oktyabr', 'Noyabr', 'Dekabr'],
+  SHORTMONTHS: ['yan', 'fev', 'mar', 'apr', 'may', 'iyn', 'iyl', 'avq', 'sen', 'okt', 'noy', 'dek'],
+  WEEKDAYS: ['bazar', 'bazar ert\u0259si', '\u00e7\u0259r\u015f\u0259nb\u0259 ax\u015fam\u0131', '\u00e7\u0259r\u015f\u0259nb\u0259', 'c\u00fcm\u0259 ax\u015fam\u0131', 'c\u00fcm\u0259', '\u015f\u0259nb\u0259'],
+  SHORTWEEKDAYS: ['B.', 'B.E.', '\u00c7.A.', '\u00c7.', 'C.A.', 'C', '\u015e.'],
+  NARROWWEEKDAYS: ['7', '1', '2', '3', '4', '5', '6'],
+  SHORTQUARTERS: ['1-ci kv.', '2-ci kv.', '3-c\u00fc kv.', '4-c\u00fc kv.'],
+  QUARTERS: ['1-ci kvartal', '2-ci kvartal', '3-c\u00fc kvartal', '4-c\u00fc kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM, y', 'd MMMM , y', 'd MMM, y', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__be.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__be.js
new file mode 100644
index 0000000..e54c0c0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__be.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0434\u0430 \u043d.\u0435.', '\u043d.\u0435.'],
+  ERANAMES: ['\u0434\u0430 \u043d.\u044d.', '\u043d.\u044d.'],
+  NARROWMONTHS: ['\u0441', '\u043b', '\u0441', '\u043a', '\u0442', '\u0447', '\u043b', '\u0436', '\u0432', '\u043a', '\u043b', '\u0441'],
+  STANDALONENARROWMONTHS: ['\u0441', '\u043b', '\u0441', '\u043a', '\u043c', '\u0447', '\u043b', '\u0436', '\u0432', '\u043a', '\u043b', '\u0441'],
+  MONTHS: ['\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044c', '\u043b\u044e\u0442\u044b', '\u0441\u0430\u043a\u0430\u0432\u0456\u043a', '\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a', '\u043c\u0430\u0439', '\u0447\u044d\u0440\u0432\u0435\u043d\u044c', '\u043b\u0456\u043f\u0435\u043d\u044c', '\u0436\u043d\u0456\u0432\u0435\u043d\u044c', '\u0432\u0435\u0440\u0430\u0441\u0435\u043d\u044c', '\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a', '\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434', '\u0441\u043d\u0435\u0436\u0430\u043d\u044c'],
+  STANDALONEMONTHS: ['\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044c', '\u043b\u044e\u0442\u044b', '\u0441\u0430\u043a\u0430\u0432\u0456\u043a', '\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a', '\u0442\u0440\u0430\u0432\u0435\u043d\u044c', '\u0447\u044d\u0440\u0432\u0435\u043d\u044c', '\u043b\u0456\u043f\u0435\u043d\u044c', '\u0436\u043d\u0456\u0432\u0435\u043d\u044c', '\u0432\u0435\u0440\u0430\u0441\u0435\u043d\u044c', '\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a', '\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434', '\u0441\u043d\u0435\u0436\u0430\u043d\u044c'],
+  SHORTMONTHS: ['\u0441\u0442\u0443', '\u043b\u044e\u0442', '\u0441\u0430\u043a', '\u043a\u0440\u0430', '\u043c\u0430\u0439', '\u0447\u044d\u0440', '\u043b\u0456\u043f', '\u0436\u043d\u0456', '\u0432\u0435\u0440', '\u043a\u0430\u0441', '\u043b\u0456\u0441', '\u0441\u043d\u0435'],
+  STANDALONESHORTMONTHS: ['\u0441\u0442\u0443', '\u043b\u044e\u0442', '\u0441\u0430\u043a', '\u043a\u0440\u0430', '\u0442\u0440\u0430', '\u0447\u044d\u0440', '\u043b\u0456\u043f', '\u0436\u043d\u0456', '\u0432\u0435\u0440', '\u043a\u0430\u0441', '\u043b\u0456\u0441', '\u0441\u043d\u0435'],
+  WEEKDAYS: ['\u043d\u044f\u0434\u0437\u0435\u043b\u044f', '\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a', '\u0430\u045e\u0442\u043e\u0440\u0430\u043a', '\u0441\u0435\u0440\u0430\u0434\u0430', '\u0447\u0430\u0446\u0432\u0435\u0440', '\u043f\u044f\u0442\u043d\u0456\u0446\u0430', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0434', '\u043f\u043d', '\u0430\u045e', '\u0441\u0440', '\u0447\u0446', '\u043f\u0442', '\u0441\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0430', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['1-\u0448\u044b \u043a\u0432.', '2-\u0433\u0456 \u043a\u0432.', '3-\u0446\u0456 \u043a\u0432.', '4-\u0442\u044b \u043a\u0432.'],
+  QUARTERS: ['1-\u0448\u044b \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '2-\u0433\u0456 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '3-\u0446\u0456 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '4-\u0442\u044b \u043a\u0432\u0430\u0440\u0442\u0430\u043b'],
+  AMPMS: ['\u0434\u0430 \u043f\u0430\u043b\u0443\u0434\u043d\u044f', '\u043f\u0430\u0441\u043b\u044f \u043f\u0430\u043b\u0443\u0434\u043d\u044f'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd.M.yyyy', 'd.M.yy'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__be_BY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__be_BY.js
new file mode 100644
index 0000000..e54c0c0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__be_BY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0434\u0430 \u043d.\u0435.', '\u043d.\u0435.'],
+  ERANAMES: ['\u0434\u0430 \u043d.\u044d.', '\u043d.\u044d.'],
+  NARROWMONTHS: ['\u0441', '\u043b', '\u0441', '\u043a', '\u0442', '\u0447', '\u043b', '\u0436', '\u0432', '\u043a', '\u043b', '\u0441'],
+  STANDALONENARROWMONTHS: ['\u0441', '\u043b', '\u0441', '\u043a', '\u043c', '\u0447', '\u043b', '\u0436', '\u0432', '\u043a', '\u043b', '\u0441'],
+  MONTHS: ['\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044c', '\u043b\u044e\u0442\u044b', '\u0441\u0430\u043a\u0430\u0432\u0456\u043a', '\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a', '\u043c\u0430\u0439', '\u0447\u044d\u0440\u0432\u0435\u043d\u044c', '\u043b\u0456\u043f\u0435\u043d\u044c', '\u0436\u043d\u0456\u0432\u0435\u043d\u044c', '\u0432\u0435\u0440\u0430\u0441\u0435\u043d\u044c', '\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a', '\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434', '\u0441\u043d\u0435\u0436\u0430\u043d\u044c'],
+  STANDALONEMONTHS: ['\u0441\u0442\u0443\u0434\u0437\u0435\u043d\u044c', '\u043b\u044e\u0442\u044b', '\u0441\u0430\u043a\u0430\u0432\u0456\u043a', '\u043a\u0440\u0430\u0441\u0430\u0432\u0456\u043a', '\u0442\u0440\u0430\u0432\u0435\u043d\u044c', '\u0447\u044d\u0440\u0432\u0435\u043d\u044c', '\u043b\u0456\u043f\u0435\u043d\u044c', '\u0436\u043d\u0456\u0432\u0435\u043d\u044c', '\u0432\u0435\u0440\u0430\u0441\u0435\u043d\u044c', '\u043a\u0430\u0441\u0442\u0440\u044b\u0447\u043d\u0456\u043a', '\u043b\u0456\u0441\u0442\u0430\u043f\u0430\u0434', '\u0441\u043d\u0435\u0436\u0430\u043d\u044c'],
+  SHORTMONTHS: ['\u0441\u0442\u0443', '\u043b\u044e\u0442', '\u0441\u0430\u043a', '\u043a\u0440\u0430', '\u043c\u0430\u0439', '\u0447\u044d\u0440', '\u043b\u0456\u043f', '\u0436\u043d\u0456', '\u0432\u0435\u0440', '\u043a\u0430\u0441', '\u043b\u0456\u0441', '\u0441\u043d\u0435'],
+  STANDALONESHORTMONTHS: ['\u0441\u0442\u0443', '\u043b\u044e\u0442', '\u0441\u0430\u043a', '\u043a\u0440\u0430', '\u0442\u0440\u0430', '\u0447\u044d\u0440', '\u043b\u0456\u043f', '\u0436\u043d\u0456', '\u0432\u0435\u0440', '\u043a\u0430\u0441', '\u043b\u0456\u0441', '\u0441\u043d\u0435'],
+  WEEKDAYS: ['\u043d\u044f\u0434\u0437\u0435\u043b\u044f', '\u043f\u0430\u043d\u044f\u0434\u0437\u0435\u043b\u0430\u043a', '\u0430\u045e\u0442\u043e\u0440\u0430\u043a', '\u0441\u0435\u0440\u0430\u0434\u0430', '\u0447\u0430\u0446\u0432\u0435\u0440', '\u043f\u044f\u0442\u043d\u0456\u0446\u0430', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0434', '\u043f\u043d', '\u0430\u045e', '\u0441\u0440', '\u0447\u0446', '\u043f\u0442', '\u0441\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0430', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['1-\u0448\u044b \u043a\u0432.', '2-\u0433\u0456 \u043a\u0432.', '3-\u0446\u0456 \u043a\u0432.', '4-\u0442\u044b \u043a\u0432.'],
+  QUARTERS: ['1-\u0448\u044b \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '2-\u0433\u0456 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '3-\u0446\u0456 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '4-\u0442\u044b \u043a\u0432\u0430\u0440\u0442\u0430\u043b'],
+  AMPMS: ['\u0434\u0430 \u043f\u0430\u043b\u0443\u0434\u043d\u044f', '\u043f\u0430\u0441\u043b\u044f \u043f\u0430\u043b\u0443\u0434\u043d\u044f'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd.M.yyyy', 'd.M.yy'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bg.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bg.js
new file mode 100644
index 0000000..a20b8f6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bg.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f\u0440. \u043d. \u0435.', '\u043e\u0442 \u043d. \u0435.'],
+  ERANAMES: ['\u043f\u0440.\u0425\u0440.', '\u0441\u043b.\u0425\u0440.'],
+  NARROWMONTHS: ['\u044f', '\u0444', '\u043c', '\u0430', '\u043c', '\u044e', '\u044e', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u044f\u043d\u0443\u0430\u0440\u0438', '\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0439', '\u044e\u043d\u0438', '\u044e\u043b\u0438', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438', '\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438', '\u043d\u043e\u0435\u043c\u0432\u0440\u0438', '\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438'],
+  SHORTMONTHS: ['\u044f\u043d.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440.', '\u043c\u0430\u0439', '\u044e\u043d\u0438', '\u044e\u043b\u0438', '\u0430\u0432\u0433.', '\u0441\u0435\u043f\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u0435\u043c.', '\u0434\u0435\u043a.'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u043b\u044f', '\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a', '\u0432\u0442\u043e\u0440\u043d\u0438\u043a', '\u0441\u0440\u044f\u0434\u0430', '\u0447\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a', '\u043f\u0435\u0442\u044a\u043a', '\u0441\u044a\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0434', '\u043f\u043d', '\u0432\u0442', '\u0441\u0440', '\u0447\u0442', '\u043f\u0442', '\u0441\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0432', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['I \u0442\u0440\u0438\u043c.', 'II \u0442\u0440\u0438\u043c.', 'III \u0442\u0440\u0438\u043c.', 'IV \u0442\u0440\u0438\u043c.'],
+  QUARTERS: ['1-\u0432\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435', '2-\u0440\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435', '3-\u0442\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435', '4-\u0442\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435'],
+  AMPMS: ['\u043f\u0440. \u043e\u0431.', '\u0441\u043b. \u043e\u0431.'],
+  DATEFORMATS: ['dd MMMM y, EEEE', 'dd MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bg_BG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bg_BG.js
new file mode 100644
index 0000000..a20b8f6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bg_BG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f\u0440. \u043d. \u0435.', '\u043e\u0442 \u043d. \u0435.'],
+  ERANAMES: ['\u043f\u0440.\u0425\u0440.', '\u0441\u043b.\u0425\u0440.'],
+  NARROWMONTHS: ['\u044f', '\u0444', '\u043c', '\u0430', '\u043c', '\u044e', '\u044e', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u044f\u043d\u0443\u0430\u0440\u0438', '\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0439', '\u044e\u043d\u0438', '\u044e\u043b\u0438', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438', '\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438', '\u043d\u043e\u0435\u043c\u0432\u0440\u0438', '\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438'],
+  SHORTMONTHS: ['\u044f\u043d.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440.', '\u043c\u0430\u0439', '\u044e\u043d\u0438', '\u044e\u043b\u0438', '\u0430\u0432\u0433.', '\u0441\u0435\u043f\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u0435\u043c.', '\u0434\u0435\u043a.'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u043b\u044f', '\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a', '\u0432\u0442\u043e\u0440\u043d\u0438\u043a', '\u0441\u0440\u044f\u0434\u0430', '\u0447\u0435\u0442\u0432\u044a\u0440\u0442\u044a\u043a', '\u043f\u0435\u0442\u044a\u043a', '\u0441\u044a\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0434', '\u043f\u043d', '\u0432\u0442', '\u0441\u0440', '\u0447\u0442', '\u043f\u0442', '\u0441\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0432', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['I \u0442\u0440\u0438\u043c.', 'II \u0442\u0440\u0438\u043c.', 'III \u0442\u0440\u0438\u043c.', 'IV \u0442\u0440\u0438\u043c.'],
+  QUARTERS: ['1-\u0432\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435', '2-\u0440\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435', '3-\u0442\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435', '4-\u0442\u043e \u0442\u0440\u0438\u043c\u0435\u0441\u0435\u0447\u0438\u0435'],
+  AMPMS: ['\u043f\u0440. \u043e\u0431.', '\u0441\u043b. \u043e\u0431.'],
+  DATEFORMATS: ['dd MMMM y, EEEE', 'dd MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn.js
new file mode 100644
index 0000000..e8ee317
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0996\u09c3\u09b7\u09cd\u099f\u09aa\u09c2\u09b0\u09cd\u09ac', '\u0996\u09c3\u09b7\u09cd\u099f\u09be\u09ac\u09cd\u09a6'],
+  ERANAMES: ['\u0996\u09c3\u09b7\u09cd\u099f\u09aa\u09c2\u09b0\u09cd\u09ac', '\u0996\u09c3\u09b7\u09cd\u099f\u09be\u09ac\u09cd\u09a6'],
+  NARROWMONTHS: ['\u099c\u09be', '\u09ab\u09c7', '\u09ae\u09be', '\u098f', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1', '\u0986', '\u09b8\u09c7', '\u0985', '\u09a8', '\u09a1\u09bf'],
+  MONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ae\u09be\u09b0\u09cd\u099a', '\u098f\u09aa\u09cd\u09b0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b8\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0'],
+  SHORTMONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ae\u09be\u09b0\u09cd\u099a', '\u098f\u09aa\u09cd\u09b0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b8\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0'],
+  WEEKDAYS: ['\u09b0\u09ac\u09bf\u09ac\u09be\u09b0', '\u09b8\u09cb\u09ae\u09ac\u09be\u09b0', '\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09b0', '\u09ac\u09c1\u09a7\u09ac\u09be\u09b0', '\u09ac\u09c3\u09b9\u09b7\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09b0', '\u09b6\u09c1\u0995\u09cd\u09b0\u09ac\u09be\u09b0', '\u09b6\u09a8\u09bf\u09ac\u09be\u09b0'],
+  SHORTWEEKDAYS: ['\u09b0\u09ac\u09bf', '\u09b8\u09cb\u09ae', '\u09ae\u0999\u09cd\u0997\u09b2', '\u09ac\u09c1\u09a7', '\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf', '\u09b6\u09c1\u0995\u09cd\u09b0', '\u09b6\u09a8\u09bf'],
+  NARROWWEEKDAYS: ['\u09b0', '\u09b8\u09cb', '\u09ae', '\u09ac\u09c1', '\u09ac\u09c3', '\u09b6\u09c1', '\u09b6'],
+  SHORTQUARTERS: ['\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e7', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e8', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e9', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09ea'],
+  QUARTERS: ['\u09aa\u09cd\u09b0\u09a5\u09ae \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u09a6\u09cd\u09ac\u09bf\u09a4\u09c0\u09af\u09bc \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u09a4\u09c3\u09a4\u09c0\u09af\u09bc \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5 \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn_BD.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn_BD.js
new file mode 100644
index 0000000..e8ee317
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn_BD.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0996\u09c3\u09b7\u09cd\u099f\u09aa\u09c2\u09b0\u09cd\u09ac', '\u0996\u09c3\u09b7\u09cd\u099f\u09be\u09ac\u09cd\u09a6'],
+  ERANAMES: ['\u0996\u09c3\u09b7\u09cd\u099f\u09aa\u09c2\u09b0\u09cd\u09ac', '\u0996\u09c3\u09b7\u09cd\u099f\u09be\u09ac\u09cd\u09a6'],
+  NARROWMONTHS: ['\u099c\u09be', '\u09ab\u09c7', '\u09ae\u09be', '\u098f', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1', '\u0986', '\u09b8\u09c7', '\u0985', '\u09a8', '\u09a1\u09bf'],
+  MONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ae\u09be\u09b0\u09cd\u099a', '\u098f\u09aa\u09cd\u09b0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b8\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0'],
+  SHORTMONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ae\u09be\u09b0\u09cd\u099a', '\u098f\u09aa\u09cd\u09b0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b8\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0'],
+  WEEKDAYS: ['\u09b0\u09ac\u09bf\u09ac\u09be\u09b0', '\u09b8\u09cb\u09ae\u09ac\u09be\u09b0', '\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09b0', '\u09ac\u09c1\u09a7\u09ac\u09be\u09b0', '\u09ac\u09c3\u09b9\u09b7\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09b0', '\u09b6\u09c1\u0995\u09cd\u09b0\u09ac\u09be\u09b0', '\u09b6\u09a8\u09bf\u09ac\u09be\u09b0'],
+  SHORTWEEKDAYS: ['\u09b0\u09ac\u09bf', '\u09b8\u09cb\u09ae', '\u09ae\u0999\u09cd\u0997\u09b2', '\u09ac\u09c1\u09a7', '\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf', '\u09b6\u09c1\u0995\u09cd\u09b0', '\u09b6\u09a8\u09bf'],
+  NARROWWEEKDAYS: ['\u09b0', '\u09b8\u09cb', '\u09ae', '\u09ac\u09c1', '\u09ac\u09c3', '\u09b6\u09c1', '\u09b6'],
+  SHORTQUARTERS: ['\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e7', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e8', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e9', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09ea'],
+  QUARTERS: ['\u09aa\u09cd\u09b0\u09a5\u09ae \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u09a6\u09cd\u09ac\u09bf\u09a4\u09c0\u09af\u09bc \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u09a4\u09c3\u09a4\u09c0\u09af\u09bc \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5 \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn_IN.js
new file mode 100644
index 0000000..1d17c48
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bn_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0996\u09c3\u09b7\u09cd\u099f\u09aa\u09c2\u09b0\u09cd\u09ac', '\u0996\u09c3\u09b7\u09cd\u099f\u09be\u09ac\u09cd\u09a6'],
+  ERANAMES: ['\u0996\u09c3\u09b7\u09cd\u099f\u09aa\u09c2\u09b0\u09cd\u09ac', '\u0996\u09c3\u09b7\u09cd\u099f\u09be\u09ac\u09cd\u09a6'],
+  NARROWMONTHS: ['\u099c\u09be', '\u09ab\u09c7', '\u09ae\u09be', '\u098f', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1', '\u0986', '\u09b8\u09c7', '\u0985', '\u09a8', '\u09a1\u09bf'],
+  MONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ae\u09be\u09b0\u09cd\u099a', '\u098f\u09aa\u09cd\u09b0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b8\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0'],
+  SHORTMONTHS: ['\u099c\u09be\u09a8\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ab\u09c7\u09ac\u09cd\u09b0\u09c1\u09af\u09bc\u09be\u09b0\u09c0', '\u09ae\u09be\u09b0\u09cd\u099a', '\u098f\u09aa\u09cd\u09b0\u09bf\u09b2', '\u09ae\u09c7', '\u099c\u09c1\u09a8', '\u099c\u09c1\u09b2\u09be\u0987', '\u0986\u0997\u09b8\u09cd\u099f', '\u09b8\u09c7\u09aa\u09cd\u099f\u09c7\u09ae\u09cd\u09ac\u09b0', '\u0985\u0995\u09cd\u099f\u09cb\u09ac\u09b0', '\u09a8\u09ad\u09c7\u09ae\u09cd\u09ac\u09b0', '\u09a1\u09bf\u09b8\u09c7\u09ae\u09cd\u09ac\u09b0'],
+  WEEKDAYS: ['\u09b0\u09ac\u09bf\u09ac\u09be\u09b0', '\u09b8\u09cb\u09ae\u09ac\u09be\u09b0', '\u09ae\u0999\u09cd\u0997\u09b2\u09ac\u09be\u09b0', '\u09ac\u09c1\u09a7\u09ac\u09be\u09b0', '\u09ac\u09c3\u09b9\u09b7\u09cd\u09aa\u09a4\u09bf\u09ac\u09be\u09b0', '\u09b6\u09c1\u0995\u09cd\u09b0\u09ac\u09be\u09b0', '\u09b6\u09a8\u09bf\u09ac\u09be\u09b0'],
+  SHORTWEEKDAYS: ['\u09b0\u09ac\u09bf', '\u09b8\u09cb\u09ae', '\u09ae\u0999\u09cd\u0997\u09b2', '\u09ac\u09c1\u09a7', '\u09ac\u09c3\u09b9\u09b8\u09cd\u09aa\u09a4\u09bf', '\u09b6\u09c1\u0995\u09cd\u09b0', '\u09b6\u09a8\u09bf'],
+  NARROWWEEKDAYS: ['\u09b0', '\u09b8\u09cb', '\u09ae', '\u09ac\u09c1', '\u09ac\u09c3', '\u09b6\u09c1', '\u09b6'],
+  SHORTQUARTERS: ['\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e7', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e8', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09e9', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6 \u09ea'],
+  QUARTERS: ['\u09aa\u09cd\u09b0\u09a5\u09ae \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u09a6\u09cd\u09ac\u09bf\u09a4\u09c0\u09af\u09bc \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u09a4\u09c3\u09a4\u09c0\u09af\u09bc \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6', '\u099a\u09a4\u09c1\u09b0\u09cd\u09a5 \u099a\u09a4\u09c1\u09b0\u09cd\u09a5\u09be\u0982\u09b6'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo.js
new file mode 100644
index 0000000..c4243e8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0b\u0f66\u0f94\u0f7c\u0f53\u0f0d', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0d'],
+  ERANAMES: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0b\u0f66\u0f94\u0f7c\u0f53\u0f0d', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0d'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b'],
+  SHORTMONTHS: ['\u0f5f\u0fb3\u0f0b\u0f21', '\u0f5f\u0fb3\u0f0b\u0f22', '\u0f5f\u0fb3\u0f0b\u0f23', '\u0f5f\u0fb3\u0f0b\u0f24', '\u0f5f\u0fb3\u0f0b\u0f25', '\u0f5f\u0fb3\u0f0b\u0f26', '\u0f5f\u0fb3\u0f0b\u0f27', '\u0f5f\u0fb3\u0f0b\u0f28', '\u0f5f\u0fb3\u0f0b\u0f29', '\u0f5f\u0fb3\u0f0b\u0f21\u0f20', '\u0f5f\u0fb3\u0f0b\u0f21\u0f21', '\u0f5f\u0fb3\u0f0b\u0f21\u0f22'],
+  WEEKDAYS: ['\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f67\u0fb3\u0f42\u0f0b\u0f54\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0f44\u0f66\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b'],
+  SHORTWEEKDAYS: ['\u0f49\u0f72\u0f0b\u0f58\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f67\u0fb3\u0f42\u0f0b\u0f54\u0f0b', '\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f66\u0f44\u0f66\u0f0b', '\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b'],
+  NARROWWEEKDAYS: ['\u0f49\u0f72', '\u0f5f\u0fb3', '\u0f58\u0f72', '\u0f67\u0fb3', '\u0f55\u0f74', '\u0f66', '\u0f66\u0fa4\u0f7a'],
+  SHORTQUARTERS: ['\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0d', '\u0f0b\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0d'],
+  QUARTERS: ['\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0d', '\u0f0b\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0d'],
+  AMPMS: ['\u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b', '\u0f55\u0fb1\u0f72\u0f0b\u0f51\u0fb2\u0f7c\u0f0b'],
+  DATEFORMATS: ['EEEE, y MMMM dd', '\u0f66\u0fa6\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by MMMM\u0f60\u0f72\u0f0b\u0f59\u0f7a\u0f66\u0f0bd\u0f51', 'y \u0f63\u0f7c\u0f0b\u0f60\u0f72\u0f0bMMM\u0f59\u0f7a\u0f66\u0f0bd', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo_CN.js
new file mode 100644
index 0000000..c4243e8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0b\u0f66\u0f94\u0f7c\u0f53\u0f0d', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0d'],
+  ERANAMES: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0b\u0f66\u0f94\u0f7c\u0f53\u0f0d', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0d'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b'],
+  SHORTMONTHS: ['\u0f5f\u0fb3\u0f0b\u0f21', '\u0f5f\u0fb3\u0f0b\u0f22', '\u0f5f\u0fb3\u0f0b\u0f23', '\u0f5f\u0fb3\u0f0b\u0f24', '\u0f5f\u0fb3\u0f0b\u0f25', '\u0f5f\u0fb3\u0f0b\u0f26', '\u0f5f\u0fb3\u0f0b\u0f27', '\u0f5f\u0fb3\u0f0b\u0f28', '\u0f5f\u0fb3\u0f0b\u0f29', '\u0f5f\u0fb3\u0f0b\u0f21\u0f20', '\u0f5f\u0fb3\u0f0b\u0f21\u0f21', '\u0f5f\u0fb3\u0f0b\u0f21\u0f22'],
+  WEEKDAYS: ['\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f67\u0fb3\u0f42\u0f0b\u0f54\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0f44\u0f66\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b'],
+  SHORTWEEKDAYS: ['\u0f49\u0f72\u0f0b\u0f58\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f67\u0fb3\u0f42\u0f0b\u0f54\u0f0b', '\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f66\u0f44\u0f66\u0f0b', '\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b'],
+  NARROWWEEKDAYS: ['\u0f49\u0f72', '\u0f5f\u0fb3', '\u0f58\u0f72', '\u0f67\u0fb3', '\u0f55\u0f74', '\u0f66', '\u0f66\u0fa4\u0f7a'],
+  SHORTQUARTERS: ['\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0d', '\u0f0b\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0d'],
+  QUARTERS: ['\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0d', '\u0f0b\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0d'],
+  AMPMS: ['\u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b', '\u0f55\u0fb1\u0f72\u0f0b\u0f51\u0fb2\u0f7c\u0f0b'],
+  DATEFORMATS: ['EEEE, y MMMM dd', '\u0f66\u0fa6\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by MMMM\u0f60\u0f72\u0f0b\u0f59\u0f7a\u0f66\u0f0bd\u0f51', 'y \u0f63\u0f7c\u0f0b\u0f60\u0f72\u0f0bMMM\u0f59\u0f7a\u0f66\u0f0bd', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo_IN.js
new file mode 100644
index 0000000..4fdece3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bo_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0b\u0f66\u0f94\u0f7c\u0f53\u0f0d', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0d'],
+  ERANAMES: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0b\u0f66\u0f94\u0f7c\u0f53\u0f0d', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0d'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b'],
+  SHORTMONTHS: ['\u0f5f\u0fb3\u0f0b\u0f21', '\u0f5f\u0fb3\u0f0b\u0f22', '\u0f5f\u0fb3\u0f0b\u0f23', '\u0f5f\u0fb3\u0f0b\u0f24', '\u0f5f\u0fb3\u0f0b\u0f25', '\u0f5f\u0fb3\u0f0b\u0f26', '\u0f5f\u0fb3\u0f0b\u0f27', '\u0f5f\u0fb3\u0f0b\u0f28', '\u0f5f\u0fb3\u0f0b\u0f29', '\u0f5f\u0fb3\u0f0b\u0f21\u0f20', '\u0f5f\u0fb3\u0f0b\u0f21\u0f21', '\u0f5f\u0fb3\u0f0b\u0f21\u0f22'],
+  WEEKDAYS: ['\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f67\u0fb3\u0f42\u0f0b\u0f54\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0f44\u0f66\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b'],
+  SHORTWEEKDAYS: ['\u0f49\u0f72\u0f0b\u0f58\u0f0b', '\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f67\u0fb3\u0f42\u0f0b\u0f54\u0f0b', '\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f66\u0f44\u0f66\u0f0b', '\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b'],
+  NARROWWEEKDAYS: ['\u0f49\u0f72', '\u0f5f\u0fb3', '\u0f58\u0f72', '\u0f67\u0fb3', '\u0f55\u0f74', '\u0f66', '\u0f66\u0fa4\u0f7a'],
+  SHORTQUARTERS: ['\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0d', '\u0f0b\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0d'],
+  QUARTERS: ['\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f7c\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0d', '\u0f0b\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0d', '\u0f51\u0f74\u0f66\u0f0b\u0f5a\u0f72\u0f42\u0f66\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0d'],
+  AMPMS: ['\u0f66\u0f94\u0f0b\u0f51\u0fb2\u0f7c\u0f0b', '\u0f55\u0fb1\u0f72\u0f0b\u0f51\u0fb2\u0f7c\u0f0b'],
+  DATEFORMATS: ['EEEE, y MMMM dd', '\u0f66\u0fa6\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by MMMM\u0f60\u0f72\u0f0b\u0f59\u0f7a\u0f66\u0f0bd\u0f51', 'y \u0f63\u0f7c\u0f0b\u0f60\u0f72\u0f0bMMM\u0f59\u0f7a\u0f66\u0f0bd', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bs.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bs.js
new file mode 100644
index 0000000..0d4146e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bs.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januar', 'Februar', 'Mart', 'April', 'Maj', 'Juni', 'Juli', 'Avgust', 'Septembar', 'Oktobar', 'Novembar', 'Decembar'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'],
+  WEEKDAYS: ['Nedjelja', 'Ponedjeljak', 'Utorak', 'Srijeda', '\u010cetvrtak', 'Petak', 'Subota'],
+  SHORTWEEKDAYS: ['Ned', 'Pon', 'Uto', 'Sri', '\u010cet', 'Pet', 'Sub'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['Prvi kvartal', 'Drugi kvartal', 'Tre\u0107i kvartal', '\u010cetvrti kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bs_BA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bs_BA.js
new file mode 100644
index 0000000..0d4146e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__bs_BA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januar', 'Februar', 'Mart', 'April', 'Maj', 'Juni', 'Juli', 'Avgust', 'Septembar', 'Oktobar', 'Novembar', 'Decembar'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Maj', 'Jun', 'Jul', 'Avg', 'Sep', 'Okt', 'Nov', 'Dec'],
+  WEEKDAYS: ['Nedjelja', 'Ponedjeljak', 'Utorak', 'Srijeda', '\u010cetvrtak', 'Petak', 'Subota'],
+  SHORTWEEKDAYS: ['Ned', 'Pon', 'Uto', 'Sri', '\u010cet', 'Pet', 'Sub'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['Prvi kvartal', 'Drugi kvartal', 'Tre\u0107i kvartal', '\u010cetvrti kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__byn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__byn.js
new file mode 100644
index 0000000..a9a8afb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__byn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12ed\u1305', '\u12a3\u12f5'],
+  ERANAMES: ['\u12ed\u1305', '\u12a3\u12f5'],
+  NARROWMONTHS: ['\u120d', '\u12ab', '\u12ad', '\u134b', '\u12ad', '\u121d', '\u12b0', '\u121b', '\u12eb', '\u1218', '\u121d', '\u1270'],
+  MONTHS: ['\u120d\u12f0\u1275\u122a', '\u12ab\u1265\u12bd\u1265\u1272', '\u12ad\u1265\u120b', '\u134b\u1305\u12ba\u122a', '\u12ad\u1262\u1245\u122a', '\u121d\u12aa\u12a4\u120d \u1275\u131f\u1292\u122a', '\u12b0\u122d\u12a9', '\u121b\u122d\u12eb\u121d \u1275\u122a', '\u12eb\u12b8\u1292 \u1218\u1233\u1245\u1208\u122a', '\u1218\u1270\u1209', '\u121d\u12aa\u12a4\u120d \u1218\u123d\u12c8\u122a', '\u1270\u1215\u1233\u1235\u122a'],
+  SHORTMONTHS: ['\u120d\u12f0\u1275', '\u12ab\u1265\u12bd', '\u12ad\u1265\u120b', '\u134b\u1305\u12ba', '\u12ad\u1262\u1245', '\u121d/\u1275', '\u12b0\u122d', '\u121b\u122d\u12eb', '\u12eb\u12b8\u1292', '\u1218\u1270\u1209', '\u121d/\u121d', '\u1270\u1215\u1233'],
+  WEEKDAYS: ['\u1230\u1295\u1260\u122d \u1245\u12f3\u12c5', '\u1230\u1291', '\u1230\u120a\u131d', '\u1208\u1313 \u12c8\u122a \u1208\u1265\u12cb', '\u12a3\u121d\u12f5', '\u12a3\u122d\u1265', '\u1230\u1295\u1260\u122d \u123d\u1313\u12c5'],
+  SHORTWEEKDAYS: ['\u1230/\u1245', '\u1230\u1291', '\u1230\u120a\u131d', '\u1208\u1313', '\u12a3\u121d\u12f5', '\u12a3\u122d\u1265', '\u1230/\u123d'],
+  NARROWWEEKDAYS: ['\u1230', '\u1230', '\u1230', '\u1208', '\u12a3', '\u12a3', '\u1230'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u134b\u12f1\u1235 \u1303\u1265', '\u134b\u12f1\u1235 \u12f0\u121d\u1262'],
+  DATEFORMATS: ['EEEE\u1361 dd MMMM \u130d\u122d\u130b y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__byn_ER.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__byn_ER.js
new file mode 100644
index 0000000..879b8c4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__byn_ER.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12ed\u1305', '\u12a3\u12f5'],
+  ERANAMES: ['\u12ed\u1305', '\u12a3\u12f5'],
+  NARROWMONTHS: ['\u120d', '\u12ab', '\u12ad', '\u134b', '\u12ad', '\u121d', '\u12b0', '\u121b', '\u12eb', '\u1218', '\u121d', '\u1270'],
+  MONTHS: ['\u120d\u12f0\u1275\u122a', '\u12ab\u1265\u12bd\u1265\u1272', '\u12ad\u1265\u120b', '\u134b\u1305\u12ba\u122a', '\u12ad\u1262\u1245\u122a', '\u121d\u12aa\u12a4\u120d \u1275\u131f\u1292\u122a', '\u12b0\u122d\u12a9', '\u121b\u122d\u12eb\u121d \u1275\u122a', '\u12eb\u12b8\u1292 \u1218\u1233\u1245\u1208\u122a', '\u1218\u1270\u1209', '\u121d\u12aa\u12a4\u120d \u1218\u123d\u12c8\u122a', '\u1270\u1215\u1233\u1235\u122a'],
+  SHORTMONTHS: ['\u120d\u12f0\u1275', '\u12ab\u1265\u12bd', '\u12ad\u1265\u120b', '\u134b\u1305\u12ba', '\u12ad\u1262\u1245', '\u121d/\u1275', '\u12b0\u122d', '\u121b\u122d\u12eb', '\u12eb\u12b8\u1292', '\u1218\u1270\u1209', '\u121d/\u121d', '\u1270\u1215\u1233'],
+  WEEKDAYS: ['\u1230\u1295\u1260\u122d \u1245\u12f3\u12c5', '\u1230\u1291', '\u1230\u120a\u131d', '\u1208\u1313 \u12c8\u122a \u1208\u1265\u12cb', '\u12a3\u121d\u12f5', '\u12a3\u122d\u1265', '\u1230\u1295\u1260\u122d \u123d\u1313\u12c5'],
+  SHORTWEEKDAYS: ['\u1230/\u1245', '\u1230\u1291', '\u1230\u120a\u131d', '\u1208\u1313', '\u12a3\u121d\u12f5', '\u12a3\u122d\u1265', '\u1230/\u123d'],
+  NARROWWEEKDAYS: ['\u1230', '\u1230', '\u1230', '\u1208', '\u12a3', '\u12a3', '\u1230'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u134b\u12f1\u1235 \u1303\u1265', '\u134b\u12f1\u1235 \u12f0\u121d\u1262'],
+  DATEFORMATS: ['EEEE\u1361 dd MMMM \u130d\u122d\u130b y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ca.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ca.js
new file mode 100644
index 0000000..83654ce
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ca.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['aC', 'dC'],
+  ERANAMES: ['aC', 'dC'],
+  NARROWMONTHS: ['g', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['gener', 'febrer', 'mar\u00e7', 'abril', 'maig', 'juny', 'juliol', 'agost', 'setembre', 'octubre', 'novembre', 'desembre'],
+  SHORTMONTHS: ['gen.', 'febr.', 'mar\u00e7', 'abr.', 'maig', 'juny', 'jul.', 'ag.', 'set.', 'oct.', 'nov.', 'des.'],
+  WEEKDAYS: ['diumenge', 'dilluns', 'dimarts', 'dimecres', 'dijous', 'divendres', 'dissabte'],
+  SHORTWEEKDAYS: ['dg.', 'dl.', 'dt.', 'dc.', 'dj.', 'dv.', 'ds.'],
+  STANDALONESHORTWEEKDAYS: ['dg', 'dl', 'dt', 'dc', 'dj', 'dv', 'ds'],
+  NARROWWEEKDAYS: ['g', 'l', 't', 'c', 'j', 'v', 's'],
+  SHORTQUARTERS: ['1T', '2T', '3T', '4T'],
+  QUARTERS: ['1r trimestre', '2n trimestre', '3r trimestre', '4t trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ca_ES.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ca_ES.js
new file mode 100644
index 0000000..83654ce
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ca_ES.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['aC', 'dC'],
+  ERANAMES: ['aC', 'dC'],
+  NARROWMONTHS: ['g', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['gener', 'febrer', 'mar\u00e7', 'abril', 'maig', 'juny', 'juliol', 'agost', 'setembre', 'octubre', 'novembre', 'desembre'],
+  SHORTMONTHS: ['gen.', 'febr.', 'mar\u00e7', 'abr.', 'maig', 'juny', 'jul.', 'ag.', 'set.', 'oct.', 'nov.', 'des.'],
+  WEEKDAYS: ['diumenge', 'dilluns', 'dimarts', 'dimecres', 'dijous', 'divendres', 'dissabte'],
+  SHORTWEEKDAYS: ['dg.', 'dl.', 'dt.', 'dc.', 'dj.', 'dv.', 'ds.'],
+  STANDALONESHORTWEEKDAYS: ['dg', 'dl', 'dt', 'dc', 'dj', 'dv', 'ds'],
+  NARROWWEEKDAYS: ['g', 'l', 't', 'c', 'j', 'v', 's'],
+  SHORTQUARTERS: ['1T', '2T', '3T', '4T'],
+  QUARTERS: ['1r trimestre', '2n trimestre', '3r trimestre', '4t trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cch.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cch.js
new file mode 100644
index 0000000..c9e25ca
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cch.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Pen Dyon', "Pen Ba'a", 'Pen Atat', 'Pen Anas', 'Pen Atyon', 'Pen Achirim', 'Pen Atariba', 'Pen Awurr', 'Pen Shadon', 'Pen Shakur', 'Pen Kur Naba', 'Pen Kur Natat'],
+  SHORTMONTHS: ['Dyon', 'Baa', 'Atat', 'Anas', 'Atyo', 'Achi', 'Atar', 'Awur', 'Shad', 'Shak', 'Naba', 'Nata'],
+  WEEKDAYS: ['Wai Yoka Bawai', 'Wai Tunga', 'Toki Gitung', 'Tsam Kasuwa', 'Wai Na Nas', 'Wai Na Tiyon', 'Wai Na Chirim'],
+  SHORTWEEKDAYS: ['Yok', 'Tung', 'T. Tung', 'Tsan', 'Nas', 'Nat', 'Chir'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cch_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cch_NG.js
new file mode 100644
index 0000000..c9e25ca
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cch_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Pen Dyon', "Pen Ba'a", 'Pen Atat', 'Pen Anas', 'Pen Atyon', 'Pen Achirim', 'Pen Atariba', 'Pen Awurr', 'Pen Shadon', 'Pen Shakur', 'Pen Kur Naba', 'Pen Kur Natat'],
+  SHORTMONTHS: ['Dyon', 'Baa', 'Atat', 'Anas', 'Atyo', 'Achi', 'Atar', 'Awur', 'Shad', 'Shak', 'Naba', 'Nata'],
+  WEEKDAYS: ['Wai Yoka Bawai', 'Wai Tunga', 'Toki Gitung', 'Tsam Kasuwa', 'Wai Na Nas', 'Wai Na Tiyon', 'Wai Na Chirim'],
+  SHORTWEEKDAYS: ['Yok', 'Tung', 'T. Tung', 'Tsan', 'Nas', 'Nat', 'Chir'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cop.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cop.js
new file mode 100644
index 0000000..2f5f5af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cop.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cs.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cs.js
new file mode 100644
index 0000000..bbfe60f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cs.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p\u0159.Kr.', 'po Kr.'],
+  ERANAMES: ['p\u0159.Kr.', 'po Kr.'],
+  NARROWMONTHS: ['l', '\u00fa', 'b', 'd', 'k', '\u010d', '\u010d', 's', 'z', '\u0159', 'l', 'p'],
+  MONTHS: ['ledna', '\u00fanora', 'b\u0159ezna', 'dubna', 'kv\u011btna', '\u010dervna', '\u010dervence', 'srpna', 'z\u00e1\u0159\u00ed', '\u0159\u00edjna', 'listopadu', 'prosince'],
+  STANDALONEMONTHS: ['leden', '\u00fanor', 'b\u0159ezen', 'duben', 'kv\u011bten', '\u010derven', '\u010dervenec', 'srpen', 'z\u00e1\u0159\u00ed', '\u0159\u00edjen', 'listopad', 'prosinec'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  STANDALONESHORTMONTHS: ['1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.', '10.', '11.', '12.'],
+  WEEKDAYS: ['ned\u011ble', 'pond\u011bl\u00ed', '\u00fater\u00fd', 'st\u0159eda', '\u010dtvrtek', 'p\u00e1tek', 'sobota'],
+  SHORTWEEKDAYS: ['ne', 'po', '\u00fat', 'st', '\u010dt', 'p\u00e1', 'so'],
+  NARROWWEEKDAYS: ['N', 'P', '\u00da', 'S', '\u010c', 'P', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. \u010dtvrtlet\u00ed', '2. \u010dtvrtlet\u00ed', '3. \u010dtvrtlet\u00ed', '4. \u010dtvrtlet\u00ed'],
+  AMPMS: ['dop.', 'odp.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cs_CZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cs_CZ.js
new file mode 100644
index 0000000..bbfe60f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cs_CZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p\u0159.Kr.', 'po Kr.'],
+  ERANAMES: ['p\u0159.Kr.', 'po Kr.'],
+  NARROWMONTHS: ['l', '\u00fa', 'b', 'd', 'k', '\u010d', '\u010d', 's', 'z', '\u0159', 'l', 'p'],
+  MONTHS: ['ledna', '\u00fanora', 'b\u0159ezna', 'dubna', 'kv\u011btna', '\u010dervna', '\u010dervence', 'srpna', 'z\u00e1\u0159\u00ed', '\u0159\u00edjna', 'listopadu', 'prosince'],
+  STANDALONEMONTHS: ['leden', '\u00fanor', 'b\u0159ezen', 'duben', 'kv\u011bten', '\u010derven', '\u010dervenec', 'srpen', 'z\u00e1\u0159\u00ed', '\u0159\u00edjen', 'listopad', 'prosinec'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  STANDALONESHORTMONTHS: ['1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.', '10.', '11.', '12.'],
+  WEEKDAYS: ['ned\u011ble', 'pond\u011bl\u00ed', '\u00fater\u00fd', 'st\u0159eda', '\u010dtvrtek', 'p\u00e1tek', 'sobota'],
+  SHORTWEEKDAYS: ['ne', 'po', '\u00fat', 'st', '\u010dt', 'p\u00e1', 'so'],
+  NARROWWEEKDAYS: ['N', 'P', '\u00da', 'S', '\u010c', 'P', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. \u010dtvrtlet\u00ed', '2. \u010dtvrtlet\u00ed', '3. \u010dtvrtlet\u00ed', '4. \u010dtvrtlet\u00ed'],
+  AMPMS: ['dop.', 'odp.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cy.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cy.js
new file mode 100644
index 0000000..acaeb95
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cy.js
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['CC', 'OC'],
+  ERANAMES: ['Cyn Crist', 'Oed Crist'],
+  NARROWMONTHS: ['I', 'C', 'M', 'E', 'M', 'M', 'G', 'A', 'M', 'H', 'T', 'R'],
+  MONTHS: ['Ionawr', 'Chwefror', 'Mawrth', 'Ebrill', 'Mai', 'Mehefin', 'Gorffenaf', 'Awst', 'Medi', 'Hydref', 'Tachwedd', 'Rhagfyr'],
+  STANDALONEMONTHS: ['Ionawr', 'Chwefror', 'Mawrth', 'Ebrill', 'Mai', 'Mehefin', 'Gorffennaf', 'Awst', 'Medi', 'Hydref', 'Tachwedd', 'Rhagfyr'],
+  SHORTMONTHS: ['Ion', 'Chwef', 'Mawrth', 'Ebrill', 'Mai', 'Meh', 'Gorff', 'Awst', 'Medi', 'Hyd', 'Tach', 'Rhag'],
+  STANDALONESHORTMONTHS: ['Ion', 'Chwe', 'Maw', 'Ebr', 'Mai', 'Meh', 'Gor', 'Awst', 'Medi', 'Hyd', 'Tach', 'Rhag'],
+  WEEKDAYS: ['Dydd Sul', 'Dydd Llun', 'Dydd Mawrth', 'Dydd Mercher', 'Dydd Iau', 'Dydd Gwener', 'Dydd Sadwrn'],
+  SHORTWEEKDAYS: ['Sul', 'Llun', 'Maw', 'Mer', 'Iau', 'Gwen', 'Sad'],
+  STANDALONESHORTWEEKDAYS: ['Sul', 'Llun', 'Maw', 'Mer', 'Iau', 'Gwe', 'Sad'],
+  NARROWWEEKDAYS: ['S', 'L', 'M', 'M', 'I', 'G', 'S'],
+  SHORTQUARTERS: ['Ch1', 'Ch2', 'Ch3', 'Ch4'],
+  QUARTERS: ['Chwarter 1af', '2il chwarter', '3ydd chwarter', '4ydd chwarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cy_GB.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cy_GB.js
new file mode 100644
index 0000000..790949d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__cy_GB.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['CC', 'OC'],
+  ERANAMES: ['Cyn Crist', 'Oed Crist'],
+  NARROWMONTHS: ['I', 'C', 'M', 'E', 'M', 'M', 'G', 'A', 'M', 'H', 'T', 'R'],
+  MONTHS: ['Ionawr', 'Chwefror', 'Mawrth', 'Ebrill', 'Mai', 'Mehefin', 'Gorffenaf', 'Awst', 'Medi', 'Hydref', 'Tachwedd', 'Rhagfyr'],
+  STANDALONEMONTHS: ['Ionawr', 'Chwefror', 'Mawrth', 'Ebrill', 'Mai', 'Mehefin', 'Gorffennaf', 'Awst', 'Medi', 'Hydref', 'Tachwedd', 'Rhagfyr'],
+  SHORTMONTHS: ['Ion', 'Chwef', 'Mawrth', 'Ebrill', 'Mai', 'Meh', 'Gorff', 'Awst', 'Medi', 'Hyd', 'Tach', 'Rhag'],
+  STANDALONESHORTMONTHS: ['Ion', 'Chwe', 'Maw', 'Ebr', 'Mai', 'Meh', 'Gor', 'Awst', 'Medi', 'Hyd', 'Tach', 'Rhag'],
+  WEEKDAYS: ['Dydd Sul', 'Dydd Llun', 'Dydd Mawrth', 'Dydd Mercher', 'Dydd Iau', 'Dydd Gwener', 'Dydd Sadwrn'],
+  SHORTWEEKDAYS: ['Sul', 'Llun', 'Maw', 'Mer', 'Iau', 'Gwen', 'Sad'],
+  STANDALONESHORTWEEKDAYS: ['Sul', 'Llun', 'Maw', 'Mer', 'Iau', 'Gwe', 'Sad'],
+  NARROWWEEKDAYS: ['S', 'L', 'M', 'M', 'I', 'G', 'S'],
+  SHORTQUARTERS: ['Ch1', 'Ch2', 'Ch3', 'Ch4'],
+  QUARTERS: ['Chwarter 1af', '2il chwarter', '3ydd chwarter', '4ydd chwarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__da.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__da.js
new file mode 100644
index 0000000..0294e92
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__da.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f.Kr.', 'e.Kr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mar.', 'apr.', 'maj', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'],
+  STANDALONESHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['s\u00f8ndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f8rdag'],
+  SHORTWEEKDAYS: ['s\u00f8n', 'man', 'tir', 'ons', 'tor', 'fre', 'l\u00f8r'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['f.m.', 'e.m.'],
+  DATEFORMATS: ["EEEE 'den' d. MMMM y", 'd. MMM y', 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH:mm:ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__da_DK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__da_DK.js
new file mode 100644
index 0000000..0294e92
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__da_DK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f.Kr.', 'e.Kr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januar', 'februar', 'marts', 'april', 'maj', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mar.', 'apr.', 'maj', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'],
+  STANDALONESHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['s\u00f8ndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f8rdag'],
+  SHORTWEEKDAYS: ['s\u00f8n', 'man', 'tir', 'ons', 'tor', 'fre', 'l\u00f8r'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['f.m.', 'e.m.'],
+  DATEFORMATS: ["EEEE 'den' d. MMMM y", 'd. MMM y', 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH:mm:ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de.js
new file mode 100644
index 0000000..da136e4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_AT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_AT.js
new file mode 100644
index 0000000..ae5b5af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_AT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['J\u00e4nner', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['J\u00e4n', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE, dd. MMMM y', 'dd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_BE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_BE.js
new file mode 100644
index 0000000..4252c6f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_BE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['Son', 'Mon', 'Die', 'Mit', 'Don', 'Fre', 'Sam'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'd/MM/yy'],
+  TIMEFORMATS: ["HH 'h' mm 'min' ss 's' zzzz", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_CH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_CH.js
new file mode 100644
index 0000000..da136e4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_CH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_DE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_DE.js
new file mode 100644
index 0000000..da136e4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_DE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_LI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_LI.js
new file mode 100644
index 0000000..da136e4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_LI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_LU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_LU.js
new file mode 100644
index 0000000..da136e4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__de_LU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dv.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dv.js
new file mode 100644
index 0000000..3775702
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dv.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-M-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 4,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 0
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dv_MV.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dv_MV.js
new file mode 100644
index 0000000..3775702
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dv_MV.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-M-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 4,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 0
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dz.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dz.js
new file mode 100644
index 0000000..a4af52e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dz.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f51\u0f44\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b'],
+  SHORTMONTHS: ['\u0f5f\u0fb3\u0f0b \u0f21', '\u0f5f\u0fb3\u0f0b \u0f22', '\u0f5f\u0fb3\u0f0b \u0f23', '\u0f5f\u0fb3\u0f0b \u0f24', '\u0f5f\u0fb3\u0f0b \u0f25', '\u0f5f\u0fb3\u0f0b \u0f26', '\u0f5f\u0fb3\u0f0b \u0f27', '\u0f5f\u0fb3\u0f0b \u0f28', '\u0f5f\u0fb3\u0f0b \u0f29', '\u0f5f\u0fb3\u0f0b \u0f21\u0f20', '\u0f5f\u0fb3\u0f0b \u0f21\u0f21', '\u0f5f\u0fb3\u0f0b \u0f21\u0f22'],
+  WEEKDAYS: ['\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b'],
+  SHORTWEEKDAYS: ['\u0f5f\u0fb3\u0f0b', '\u0f58\u0f72\u0f62\u0f0b', '\u0f63\u0fb7\u0f42\u0f0b', '\u0f55\u0f74\u0f62\u0f0b', '\u0f66\u0f44\u0f66\u0f0b', '\u0f66\u0fa4\u0f7a\u0f53\u0f0b', '\u0f49\u0f72\u0f0b'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f21', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f22', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f23', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f24'],
+  QUARTERS: ['\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f0b', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b'],
+  AMPMS: ['\u0f66\u0f94\u0f0b\u0f46\u0f0b', '\u0f55\u0fb1\u0f72\u0f0b\u0f46\u0f0b'],
+  DATEFORMATS: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by \u0f5f\u0fb3\u0f0b MMMM \u0f5a\u0f7a\u0f66\u0f0b dd', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by \u0f5f\u0fb3\u0f0b MMMM \u0f5a\u0f7a\u0f66\u0f0b dd', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by \u0f5f\u0fb3\u0f0b MMM \u0f5a\u0f7a\u0f66\u0f0b dd', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b h \u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b mm \u0f66\u0f90\u0f62\u0f0b\u0f46\u0f71\u0f0b ss a zzzz', '\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b h \u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b mm \u0f66\u0f90\u0f62\u0f0b\u0f46\u0f71\u0f0b ss a z', '\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0bh:mm:ss a', '\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b h \u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dz_BT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dz_BT.js
new file mode 100644
index 0000000..a4af52e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__dz_BT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f51\u0f44\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f63\u0f94\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f51\u0fb2\u0f74\u0f42\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f51\u0f74\u0f53\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f62\u0f92\u0fb1\u0f51\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f51\u0f42\u0f74\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f45\u0f72\u0f42\u0f0b\u0f54\u0f0b', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f5f\u0fb3\u0f5d\u0f0b\u0f56\u0f45\u0f74\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b'],
+  SHORTMONTHS: ['\u0f5f\u0fb3\u0f0b \u0f21', '\u0f5f\u0fb3\u0f0b \u0f22', '\u0f5f\u0fb3\u0f0b \u0f23', '\u0f5f\u0fb3\u0f0b \u0f24', '\u0f5f\u0fb3\u0f0b \u0f25', '\u0f5f\u0fb3\u0f0b \u0f26', '\u0f5f\u0fb3\u0f0b \u0f27', '\u0f5f\u0fb3\u0f0b \u0f28', '\u0f5f\u0fb3\u0f0b \u0f29', '\u0f5f\u0fb3\u0f0b \u0f21\u0f20', '\u0f5f\u0fb3\u0f0b \u0f21\u0f21', '\u0f5f\u0fb3\u0f0b \u0f21\u0f22'],
+  WEEKDAYS: ['\u0f42\u0f5f\u0f60\u0f0b\u0f5f\u0fb3\u0f0b\u0f56\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f58\u0f72\u0f42\u0f0b\u0f51\u0f58\u0f62\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f63\u0fb7\u0f42\u0f0b\u0f54\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f55\u0f74\u0f62\u0f0b\u0f56\u0f74\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f54\u0f0b\u0f66\u0f44\u0f66\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f66\u0fa4\u0f7a\u0f53\u0f0b\u0f54\u0f0b', '\u0f42\u0f5f\u0f60\u0f0b\u0f49\u0f72\u0f0b\u0f58\u0f0b'],
+  SHORTWEEKDAYS: ['\u0f5f\u0fb3\u0f0b', '\u0f58\u0f72\u0f62\u0f0b', '\u0f63\u0fb7\u0f42\u0f0b', '\u0f55\u0f74\u0f62\u0f0b', '\u0f66\u0f44\u0f66\u0f0b', '\u0f66\u0fa4\u0f7a\u0f53\u0f0b', '\u0f49\u0f72\u0f0b'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f21', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f22', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f23', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f24'],
+  QUARTERS: ['\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f51\u0f44\u0f0b\u0f54\u0f0b', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f42\u0f49\u0f72\u0f66\u0f0b\u0f54\u0f0b', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f42\u0f66\u0f74\u0f58\u0f0b\u0f54\u0f0b', '\u0f56\u0f5e\u0f72\u0f0b\u0f51\u0f54\u0fb1\u0f0b\u0f56\u0f5e\u0f72\u0f0b\u0f54\u0f0b'],
+  AMPMS: ['\u0f66\u0f94\u0f0b\u0f46\u0f0b', '\u0f55\u0fb1\u0f72\u0f0b\u0f46\u0f0b'],
+  DATEFORMATS: ['\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by \u0f5f\u0fb3\u0f0b MMMM \u0f5a\u0f7a\u0f66\u0f0b dd', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by \u0f5f\u0fb3\u0f0b MMMM \u0f5a\u0f7a\u0f66\u0f0b dd', '\u0f66\u0fa4\u0fb1\u0f72\u0f0b\u0f63\u0f7c\u0f0by \u0f5f\u0fb3\u0f0b MMM \u0f5a\u0f7a\u0f66\u0f0b dd', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b h \u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b mm \u0f66\u0f90\u0f62\u0f0b\u0f46\u0f71\u0f0b ss a zzzz', '\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b h \u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b mm \u0f66\u0f90\u0f62\u0f0b\u0f46\u0f71\u0f0b ss a z', '\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0bh:mm:ss a', '\u0f46\u0f74\u0f0b\u0f5a\u0f7c\u0f51\u0f0b h \u0f66\u0f90\u0f62\u0f0b\u0f58\u0f0b mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee.js
new file mode 100644
index 0000000..01f73f6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['HY', 'Y\u014a'],
+  ERANAMES: ['Hafi Yesu Va Do \u014bg\u0254 na Yesu', 'Yesu \u014a\u0254li'],
+  NARROWMONTHS: ['D', 'D', 'T', 'A', 'D', 'M', 'S', 'D', 'A', 'K', 'A', 'D'],
+  MONTHS: ['Dzove', 'Dzodze', 'Tedoxe', 'Af\u0254fi\u025b', 'Dama', 'Masa', 'Siaml\u0254m', 'Deasiamime', 'Any\u0254ny\u0254', 'Kele', 'Ade\u025bmekp\u0254xe', 'Dzome'],
+  SHORTMONTHS: ['Dzv', 'Dzd', 'Ted', 'Af\u0254', 'Dam', 'Mas', 'Sia', 'Dea', 'Any', 'Kel', 'Ade', 'Dzm'],
+  WEEKDAYS: ['K\u0254si\u0256a', 'Dzo\u0256a', 'Bra\u0256a', 'Ku\u0256a', 'Yawo\u0256a', 'Fi\u0256a', 'Memle\u0256a'],
+  SHORTWEEKDAYS: ['K\u0254s Kwe', 'Dzo', 'Bra', 'Ku\u0256', 'Yaw', 'Fi\u0256', 'Mem'],
+  NARROWWEEKDAYS: ['K', 'D', 'B', 'K', 'Y', 'F', 'M'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AN', 'EW'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee_GH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee_GH.js
new file mode 100644
index 0000000..01f73f6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee_GH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['HY', 'Y\u014a'],
+  ERANAMES: ['Hafi Yesu Va Do \u014bg\u0254 na Yesu', 'Yesu \u014a\u0254li'],
+  NARROWMONTHS: ['D', 'D', 'T', 'A', 'D', 'M', 'S', 'D', 'A', 'K', 'A', 'D'],
+  MONTHS: ['Dzove', 'Dzodze', 'Tedoxe', 'Af\u0254fi\u025b', 'Dama', 'Masa', 'Siaml\u0254m', 'Deasiamime', 'Any\u0254ny\u0254', 'Kele', 'Ade\u025bmekp\u0254xe', 'Dzome'],
+  SHORTMONTHS: ['Dzv', 'Dzd', 'Ted', 'Af\u0254', 'Dam', 'Mas', 'Sia', 'Dea', 'Any', 'Kel', 'Ade', 'Dzm'],
+  WEEKDAYS: ['K\u0254si\u0256a', 'Dzo\u0256a', 'Bra\u0256a', 'Ku\u0256a', 'Yawo\u0256a', 'Fi\u0256a', 'Memle\u0256a'],
+  SHORTWEEKDAYS: ['K\u0254s Kwe', 'Dzo', 'Bra', 'Ku\u0256', 'Yaw', 'Fi\u0256', 'Mem'],
+  NARROWWEEKDAYS: ['K', 'D', 'B', 'K', 'Y', 'F', 'M'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AN', 'EW'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee_TG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee_TG.js
new file mode 100644
index 0000000..01f73f6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ee_TG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['HY', 'Y\u014a'],
+  ERANAMES: ['Hafi Yesu Va Do \u014bg\u0254 na Yesu', 'Yesu \u014a\u0254li'],
+  NARROWMONTHS: ['D', 'D', 'T', 'A', 'D', 'M', 'S', 'D', 'A', 'K', 'A', 'D'],
+  MONTHS: ['Dzove', 'Dzodze', 'Tedoxe', 'Af\u0254fi\u025b', 'Dama', 'Masa', 'Siaml\u0254m', 'Deasiamime', 'Any\u0254ny\u0254', 'Kele', 'Ade\u025bmekp\u0254xe', 'Dzome'],
+  SHORTMONTHS: ['Dzv', 'Dzd', 'Ted', 'Af\u0254', 'Dam', 'Mas', 'Sia', 'Dea', 'Any', 'Kel', 'Ade', 'Dzm'],
+  WEEKDAYS: ['K\u0254si\u0256a', 'Dzo\u0256a', 'Bra\u0256a', 'Ku\u0256a', 'Yawo\u0256a', 'Fi\u0256a', 'Memle\u0256a'],
+  SHORTWEEKDAYS: ['K\u0254s Kwe', 'Dzo', 'Bra', 'Ku\u0256', 'Yaw', 'Fi\u0256', 'Mem'],
+  NARROWWEEKDAYS: ['K', 'D', 'B', 'K', 'Y', 'F', 'M'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AN', 'EW'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el.js
new file mode 100644
index 0000000..e8bd9af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  ERANAMES: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  NARROWMONTHS: ['\u0399', '\u03a6', '\u039c', '\u0391', '\u039c', '\u0399', '\u0399', '\u0391', '\u03a3', '\u039f', '\u039d', '\u0394'],
+  MONTHS: ['\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5', '\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5', '\u039c\u03b1\u0390\u03bf\u03c5', '\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5', '\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5', '\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5', '\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5', '\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5'],
+  STANDALONEMONTHS: ['\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2', '\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2', '\u039c\u03ac\u03b9\u03bf\u03c2', '\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2', '\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2', '\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2', '\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2', '\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2'],
+  SHORTMONTHS: ['\u0399\u03b1\u03bd', '\u03a6\u03b5\u03b2', '\u039c\u03b1\u03c1', '\u0391\u03c0\u03c1', '\u039c\u03b1\u03ca', '\u0399\u03bf\u03c5\u03bd', '\u0399\u03bf\u03c5\u03bb', '\u0391\u03c5\u03b3', '\u03a3\u03b5\u03c0', '\u039f\u03ba\u03c4', '\u039d\u03bf\u03b5', '\u0394\u03b5\u03ba'],
+  WEEKDAYS: ['\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae', '\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1', '\u03a4\u03c1\u03af\u03c4\u03b7', '\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7', '\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7', '\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae', '\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf'],
+  SHORTWEEKDAYS: ['\u039a\u03c5\u03c1', '\u0394\u03b5\u03c5', '\u03a4\u03c1\u03b9', '\u03a4\u03b5\u03c4', '\u03a0\u03b5\u03bc', '\u03a0\u03b1\u03c1', '\u03a3\u03b1\u03b2'],
+  NARROWWEEKDAYS: ['\u039a', '\u0394', '\u03a4', '\u03a4', '\u03a0', '\u03a0', '\u03a3'],
+  SHORTQUARTERS: ['\u03a41', '\u03a42', '\u03a43', '\u03a44'],
+  QUARTERS: ['1\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '2\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '3\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '4\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf'],
+  AMPMS: ['\u03c0.\u03bc.', '\u03bc.\u03bc.'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_CY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_CY.js
new file mode 100644
index 0000000..e8bd9af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_CY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  ERANAMES: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  NARROWMONTHS: ['\u0399', '\u03a6', '\u039c', '\u0391', '\u039c', '\u0399', '\u0399', '\u0391', '\u03a3', '\u039f', '\u039d', '\u0394'],
+  MONTHS: ['\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5', '\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5', '\u039c\u03b1\u0390\u03bf\u03c5', '\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5', '\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5', '\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5', '\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5', '\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5'],
+  STANDALONEMONTHS: ['\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2', '\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2', '\u039c\u03ac\u03b9\u03bf\u03c2', '\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2', '\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2', '\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2', '\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2', '\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2'],
+  SHORTMONTHS: ['\u0399\u03b1\u03bd', '\u03a6\u03b5\u03b2', '\u039c\u03b1\u03c1', '\u0391\u03c0\u03c1', '\u039c\u03b1\u03ca', '\u0399\u03bf\u03c5\u03bd', '\u0399\u03bf\u03c5\u03bb', '\u0391\u03c5\u03b3', '\u03a3\u03b5\u03c0', '\u039f\u03ba\u03c4', '\u039d\u03bf\u03b5', '\u0394\u03b5\u03ba'],
+  WEEKDAYS: ['\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae', '\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1', '\u03a4\u03c1\u03af\u03c4\u03b7', '\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7', '\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7', '\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae', '\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf'],
+  SHORTWEEKDAYS: ['\u039a\u03c5\u03c1', '\u0394\u03b5\u03c5', '\u03a4\u03c1\u03b9', '\u03a4\u03b5\u03c4', '\u03a0\u03b5\u03bc', '\u03a0\u03b1\u03c1', '\u03a3\u03b1\u03b2'],
+  NARROWWEEKDAYS: ['\u039a', '\u0394', '\u03a4', '\u03a4', '\u03a0', '\u03a0', '\u03a3'],
+  SHORTQUARTERS: ['\u03a41', '\u03a42', '\u03a43', '\u03a44'],
+  QUARTERS: ['1\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '2\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '3\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '4\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf'],
+  AMPMS: ['\u03c0.\u03bc.', '\u03bc.\u03bc.'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_GR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_GR.js
new file mode 100644
index 0000000..e8bd9af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_GR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  ERANAMES: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  NARROWMONTHS: ['\u0399', '\u03a6', '\u039c', '\u0391', '\u039c', '\u0399', '\u0399', '\u0391', '\u03a3', '\u039f', '\u039d', '\u0394'],
+  MONTHS: ['\u0399\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5', '\u0391\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5', '\u039c\u03b1\u0390\u03bf\u03c5', '\u0399\u03bf\u03c5\u03bd\u03af\u03bf\u03c5', '\u0399\u03bf\u03c5\u03bb\u03af\u03bf\u03c5', '\u0391\u03c5\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5', '\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u039f\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5', '\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5'],
+  STANDALONEMONTHS: ['\u0399\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2', '\u0391\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2', '\u039c\u03ac\u03b9\u03bf\u03c2', '\u0399\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2', '\u0399\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2', '\u0391\u03cd\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2', '\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u039f\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2', '\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2'],
+  SHORTMONTHS: ['\u0399\u03b1\u03bd', '\u03a6\u03b5\u03b2', '\u039c\u03b1\u03c1', '\u0391\u03c0\u03c1', '\u039c\u03b1\u03ca', '\u0399\u03bf\u03c5\u03bd', '\u0399\u03bf\u03c5\u03bb', '\u0391\u03c5\u03b3', '\u03a3\u03b5\u03c0', '\u039f\u03ba\u03c4', '\u039d\u03bf\u03b5', '\u0394\u03b5\u03ba'],
+  WEEKDAYS: ['\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae', '\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1', '\u03a4\u03c1\u03af\u03c4\u03b7', '\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7', '\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7', '\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae', '\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf'],
+  SHORTWEEKDAYS: ['\u039a\u03c5\u03c1', '\u0394\u03b5\u03c5', '\u03a4\u03c1\u03b9', '\u03a4\u03b5\u03c4', '\u03a0\u03b5\u03bc', '\u03a0\u03b1\u03c1', '\u03a3\u03b1\u03b2'],
+  NARROWWEEKDAYS: ['\u039a', '\u0394', '\u03a4', '\u03a4', '\u03a0', '\u03a0', '\u03a3'],
+  SHORTQUARTERS: ['\u03a41', '\u03a42', '\u03a43', '\u03a44'],
+  QUARTERS: ['1\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '2\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '3\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '4\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf'],
+  AMPMS: ['\u03c0.\u03bc.', '\u03bc.\u03bc.'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_POLYTON.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_POLYTON.js
new file mode 100644
index 0000000..cb11f05
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__el_POLYTON.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  ERANAMES: ['\u03c0.\u03a7.', '\u03bc.\u03a7.'],
+  NARROWMONTHS: ['\u0399', '\u03a6', '\u039c', '\u0391', '\u039c', '\u0399', '\u0399', '\u0391', '\u03a3', '\u039f', '\u039d', '\u0394'],
+  MONTHS: ['\u1f38\u03b1\u03bd\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03b1\u03c1\u03af\u03bf\u03c5', '\u039c\u03b1\u03c1\u03c4\u03af\u03bf\u03c5', '\u1f08\u03c0\u03c1\u03b9\u03bb\u03af\u03bf\u03c5', '\u039c\u03b1\u0390\u03bf\u03c5', '\u1f38\u03bf\u03c5\u03bd\u03af\u03bf\u03c5', '\u1f38\u03bf\u03c5\u03bb\u03af\u03bf\u03c5', '\u0391\u1f50\u03b3\u03bf\u03cd\u03c3\u03c4\u03bf\u03c5', '\u03a3\u03b5\u03c0\u03c4\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u1f48\u03ba\u03c4\u03c9\u03b2\u03c1\u03af\u03bf\u03c5', '\u039d\u03bf\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5', '\u0394\u03b5\u03ba\u03b5\u03bc\u03b2\u03c1\u03af\u03bf\u03c5'],
+  STANDALONEMONTHS: ['\u1f38\u03b1\u03bd\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u03a6\u03b5\u03b2\u03c1\u03bf\u03c5\u03ac\u03c1\u03b9\u03bf\u03c2', '\u039c\u03ac\u03c1\u03c4\u03b9\u03bf\u03c2', '\u1f08\u03c0\u03c1\u03af\u03bb\u03b9\u03bf\u03c2', '\u039c\u03ac\u03b9\u03bf\u03c2', '\u1f38\u03bf\u03cd\u03bd\u03b9\u03bf\u03c2', '\u1f38\u03bf\u03cd\u03bb\u03b9\u03bf\u03c2', '\u0391\u1f54\u03b3\u03bf\u03c5\u03c3\u03c4\u03bf\u03c2', '\u03a3\u03b5\u03c0\u03c4\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u1f48\u03ba\u03c4\u03ce\u03b2\u03c1\u03b9\u03bf\u03c2', '\u039d\u03bf\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2', '\u0394\u03b5\u03ba\u03ad\u03bc\u03b2\u03c1\u03b9\u03bf\u03c2'],
+  SHORTMONTHS: ['\u1f38\u03b1\u03bd', '\u03a6\u03b5\u03b2', '\u039c\u03b1\u03c1', '\u1f08\u03c0\u03c1', '\u039c\u03b1\u03ca', '\u1f38\u03bf\u03c5\u03bd', '\u1f38\u03bf\u03c5\u03bb', '\u0391\u1f50\u03b3', '\u03a3\u03b5\u03c0', '\u1f48\u03ba\u03c4', '\u039d\u03bf\u03b5', '\u0394\u03b5\u03ba'],
+  WEEKDAYS: ['\u039a\u03c5\u03c1\u03b9\u03b1\u03ba\u03ae', '\u0394\u03b5\u03c5\u03c4\u03ad\u03c1\u03b1', '\u03a4\u03c1\u03af\u03c4\u03b7', '\u03a4\u03b5\u03c4\u03ac\u03c1\u03c4\u03b7', '\u03a0\u03ad\u03bc\u03c0\u03c4\u03b7', '\u03a0\u03b1\u03c1\u03b1\u03c3\u03ba\u03b5\u03c5\u03ae', '\u03a3\u03ac\u03b2\u03b2\u03b1\u03c4\u03bf'],
+  SHORTWEEKDAYS: ['\u039a\u03c5\u03c1', '\u0394\u03b5\u03c5', '\u03a4\u03c1\u03b9', '\u03a4\u03b5\u03c4', '\u03a0\u03b5\u03bc', '\u03a0\u03b1\u03c1', '\u03a3\u03b1\u03b2'],
+  NARROWWEEKDAYS: ['\u039a', '\u0394', '\u03a4', '\u03a4', '\u03a0', '\u03a0', '\u03a3'],
+  SHORTQUARTERS: ['\u03a41', '\u03a42', '\u03a43', '\u03a44'],
+  QUARTERS: ['1\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '2\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '3\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf', '4\u03bf \u03c4\u03c1\u03af\u03bc\u03b7\u03bd\u03bf'],
+  AMPMS: ['\u03c0.\u03bc.', '\u03bc.\u03bc.'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_AS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_AS.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_AS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_AU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_AU.js
new file mode 100644
index 0000000..1881b98
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_AU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd/MM/yyyy', 'd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BE.js
new file mode 100644
index 0000000..ac3145b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMM y', 'dd MMM y', 'dd/MM/yy'],
+  TIMEFORMATS: ["HH 'h' mm 'min' ss 's' zzzz", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BW.js
new file mode 100644
index 0000000..60f153d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'MMM d, y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BZ.js
new file mode 100644
index 0000000..6620acf
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_BZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['dd MMMM y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_CA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_CA.js
new file mode 100644
index 0000000..4ac967a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_CA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'yyyy-MM-dd', 'yy-MM-dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Dsrt.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Dsrt.js
new file mode 100644
index 0000000..6de37df
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Dsrt.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u10412\u10417', '\u10408\u10414'],
+  ERANAMES: ['\u10412\u10432\u10441\u1042c\u10449 \u10417\u10449\u10434\u10445\u1043b', '\u10408\u1044c\u1042c \u10414\u10431\u1044b\u1042e\u1044c\u10428'],
+  NARROWMONTHS: ['\u10416', '\u10419', '\u10423', '\u10401', '\u10423', '\u10416', '\u10416', '\u10402', '\u1041d', '\u10409', '\u10424', '\u10414'],
+  MONTHS: ['\u10416\u10430\u1044c\u10437\u1042d\u1042f\u10449\u10428', '\u10419\u1042f\u1043a\u10449\u1042d\u1042f\u10449\u10428', '\u10423\u1042a\u10449\u1043d', '\u10401\u10439\u10449\u1042e\u1044a', '\u10423\u10429', '\u10416\u1042d\u1044c', '\u10416\u1042d\u1044a\u10434', '\u10402\u10440\u10432\u10445\u1043b', '\u1041d\u1042f\u10439\u1043b\u1042f\u1044b\u1043a\u10432\u10449', '\u10409\u1043f\u1043b\u1042c\u1043a\u10432\u10449', '\u10424\u1042c\u10442\u1042f\u1044b\u1043a\u10432\u10449', '\u10414\u10428\u10445\u1042f\u1044b\u1043a\u10432\u10449'],
+  SHORTMONTHS: ['\u10416\u10430\u1044c', '\u10419\u1042f\u1043a', '\u10423\u1042a\u10449', '\u10401\u10439\u10449', '\u10423\u10429', '\u10416\u1042d\u1044c', '\u10416\u1042d\u1044a', '\u10402\u10440', '\u1041d\u1042f\u10439', '\u10409\u1043f\u1043b', '\u10424\u1042c\u10442', '\u10414\u10428\u10445'],
+  WEEKDAYS: ['\u1041d\u10432\u1044c\u1043c\u10429', '\u10423\u10432\u1044c\u1043c\u10429', '\u10413\u1042d\u10446\u1043c\u10429', '\u1040e\u1042f\u1044c\u10446\u1043c\u10429', '\u1041b\u10432\u10449\u10446\u1043c\u10429', '\u10419\u10449\u10434\u1043c\u10429', '\u1041d\u10430\u1043b\u10432\u10449\u1043c\u10429'],
+  SHORTWEEKDAYS: ['\u1041d\u10432\u1044c', '\u10423\u10432\u1044c', '\u10413\u1042d\u10446', '\u1040e\u1042f\u1044c', '\u1041b\u10432\u10449', '\u10419\u10449\u10434', '\u1041d\u10430\u1043b'],
+  NARROWWEEKDAYS: ['\u1041d', '\u10423', '\u10413', '\u1040e', '\u1041b', '\u10419', '\u1041d'],
+  SHORTQUARTERS: ['\u104171', '\u104172', '\u104173', '\u104174'],
+  QUARTERS: ['1\u10445\u1043b \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449', '2\u1044c\u1043c \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449', '3\u10449\u1043c \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449', '4\u10449\u10443 \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449'],
+  AMPMS: ['\u10408\u10423', '\u10411\u10423'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Dsrt_US.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Dsrt_US.js
new file mode 100644
index 0000000..35401d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Dsrt_US.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u10412\u10417', '\u10408\u10414'],
+  ERANAMES: ['\u10412\u10432\u10441\u1042c\u10449 \u10417\u10449\u10434\u10445\u1043b', '\u10408\u1044c\u1042c \u10414\u10431\u1044b\u1042e\u1044c\u10428'],
+  NARROWMONTHS: ['\u10416', '\u10419', '\u10423', '\u10401', '\u10423', '\u10416', '\u10416', '\u10402', '\u1041d', '\u10409', '\u10424', '\u10414'],
+  MONTHS: ['\u10416\u10430\u1044c\u10437\u1042d\u1042f\u10449\u10428', '\u10419\u1042f\u1043a\u10449\u1042d\u1042f\u10449\u10428', '\u10423\u1042a\u10449\u1043d', '\u10401\u10439\u10449\u1042e\u1044a', '\u10423\u10429', '\u10416\u1042d\u1044c', '\u10416\u1042d\u1044a\u10434', '\u10402\u10440\u10432\u10445\u1043b', '\u1041d\u1042f\u10439\u1043b\u1042f\u1044b\u1043a\u10432\u10449', '\u10409\u1043f\u1043b\u1042c\u1043a\u10432\u10449', '\u10424\u1042c\u10442\u1042f\u1044b\u1043a\u10432\u10449', '\u10414\u10428\u10445\u1042f\u1044b\u1043a\u10432\u10449'],
+  SHORTMONTHS: ['\u10416\u10430\u1044c', '\u10419\u1042f\u1043a', '\u10423\u1042a\u10449', '\u10401\u10439\u10449', '\u10423\u10429', '\u10416\u1042d\u1044c', '\u10416\u1042d\u1044a', '\u10402\u10440', '\u1041d\u1042f\u10439', '\u10409\u1043f\u1043b', '\u10424\u1042c\u10442', '\u10414\u10428\u10445'],
+  WEEKDAYS: ['\u1041d\u10432\u1044c\u1043c\u10429', '\u10423\u10432\u1044c\u1043c\u10429', '\u10413\u1042d\u10446\u1043c\u10429', '\u1040e\u1042f\u1044c\u10446\u1043c\u10429', '\u1041b\u10432\u10449\u10446\u1043c\u10429', '\u10419\u10449\u10434\u1043c\u10429', '\u1041d\u10430\u1043b\u10432\u10449\u1043c\u10429'],
+  SHORTWEEKDAYS: ['\u1041d\u10432\u1044c', '\u10423\u10432\u1044c', '\u10413\u1042d\u10446', '\u1040e\u1042f\u1044c', '\u1041b\u10432\u10449', '\u10419\u10449\u10434', '\u1041d\u10430\u1043b'],
+  NARROWWEEKDAYS: ['\u1041d', '\u10423', '\u10413', '\u1040e', '\u1041b', '\u10419', '\u1041d'],
+  SHORTQUARTERS: ['\u104171', '\u104172', '\u104173', '\u104174'],
+  QUARTERS: ['1\u10445\u1043b \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449', '2\u1044c\u1043c \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449', '3\u10449\u1043c \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449', '4\u10449\u10443 \u1043f\u10436\u1042a\u10449\u1043b\u10432\u10449'],
+  AMPMS: ['\u10408\u10423', '\u10411\u10423'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_GB.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_GB.js
new file mode 100644
index 0000000..0f4928a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_GB.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_GU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_GU.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_GU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_HK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_HK.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_HK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_IE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_IE.js
new file mode 100644
index 0000000..5279b76
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_IE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_IN.js
new file mode 100644
index 0000000..968ff98
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_JM.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_JM.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_JM.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MH.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MP.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MP.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MP.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MT.js
new file mode 100644
index 0000000..4fbef51
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_MT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'dd MMMM y', 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_NA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_NA.js
new file mode 100644
index 0000000..dcf7d2b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_NA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_NZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_NZ.js
new file mode 100644
index 0000000..3d59bc6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_NZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd/MM/yyyy', 'd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_PH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_PH.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_PH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_PK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_PK.js
new file mode 100644
index 0000000..dd14bf3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_PK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_SG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_SG.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_SG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Shaw.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Shaw.js
new file mode 100644
index 0000000..8565a58
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_Shaw.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u1045a\u00b7\u10452', '\u10468\u1045b'],
+  ERANAMES: ['\u1045a\u10470\u10453\u1046a\u1046e \u00b7\u10452\u1046e\u10472\u10455\u10451', '\u10468\u10459\u10474 \u1045b\u1046a\u10465\u10466\u10459\u10470'],
+  NARROWMONTHS: ['\u10461', '\u10453', '\u10465', '\u10471', '\u10465', '\u10461', '\u10461', '\u1046a', '\u10455', '\u10477', '\u1046f', '\u1045b'],
+  MONTHS: ['\u00b7\u10461\u10468\u10459\u10458\u1046d\u10462\u1047a\u10470', '\u00b7\u10453\u10467\u1045a\u10458\u10475\u10462\u1047a\u10470', '\u00b7\u10465\u10478\u10457', '\u00b7\u10471\u10450\u1046e\u1046d\u10464', '\u00b7\u10465\u10471', '\u00b7\u10461\u10475\u1046f', '\u00b7\u10461\u1046b\u10464\u10472', '\u00b7\u1046a\u1045c\u1046d\u10455\u10451', '\u00b7\u10455\u10467\u10450\u10451\u10467\u10465\u1045a\u10478', '\u00b7\u10477\u10452\u10451\u10474\u1045a\u10478', '\u00b7\u1046f\u10474\u1045d\u10467\u10465\u1045a\u10478', '\u00b7\u1045b\u1046d\u10455\u10467\u10465\u1045a\u10478'],
+  SHORTMONTHS: ['\u00b7\u10461\u10468', '\u00b7\u10453\u10467', '\u00b7\u10465\u10478', '\u00b7\u10471\u10450', '\u00b7\u10465\u10471', '\u00b7\u10461\u10475', '\u00b7\u10461\u1046b', '\u00b7\u1046a\u1045c', '\u00b7\u10455\u10467', '\u00b7\u10477\u10452', '\u00b7\u1046f\u10474', '\u00b7\u1045b\u1046d'],
+  WEEKDAYS: ['\u00b7\u10455\u1046d\u10459\u1045b\u10471', '\u00b7\u10465\u1046d\u10459\u1045b\u10471', '\u00b7\u10451\u10475\u1045f\u1045b\u10471', '\u00b7\u10462\u10467\u10459\u1045f\u1045b\u10471', '\u00b7\u10454\u1047b\u1045f\u1045b\u10471', '\u00b7\u10453\u1046e\u10472\u1045b\u10471', '\u00b7\u10455\u10468\u1045b\u1047b\u1045b\u10471'],
+  SHORTWEEKDAYS: ['\u00b7\u10455\u1046d', '\u00b7\u10465\u1046d', '\u00b7\u10451\u10475', '\u00b7\u10462\u10467', '\u00b7\u10454\u1047b', '\u00b7\u10453\u1046e', '\u00b7\u10455\u10468'],
+  NARROWWEEKDAYS: ['\u10455', '\u10465', '\u10451', '\u10462', '\u10454', '\u10453', '\u10455'],
+  SHORTQUARTERS: ['\u104521', '\u104522', '\u104523', '\u104524'],
+  QUARTERS: ['1\u10455\u10451 \u10452\u10462\u10478\u1045b\u10478', '2\u1046f\u1045b \u10452\u10462\u10478\u1045b\u10478', '3\u1047b\u1045b \u10452\u10462\u10478\u1045b\u10478', '4\u10479\u10454 \u10452\u10462\u10478\u1045b\u10478'],
+  AMPMS: ['\u10468\u10465', '\u10450\u10465'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_TT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_TT.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_TT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_UM.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_UM.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_UM.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_US.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_US.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_US.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_VI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_VI.js
new file mode 100644
index 0000000..f2ef721
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_VI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_ZA.js
new file mode 100644
index 0000000..fe51f87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'dd MMM y', 'yyyy/MM/dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_ZW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_ZW.js
new file mode 100644
index 0000000..9916292
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__en_ZW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'dd MMM,y', 'd/M/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eo.js
new file mode 100644
index 0000000..156e9ea
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['aK', 'pK'],
+  ERANAMES: ['aK', 'pK'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['januaro', 'februaro', 'marto', 'aprilo', 'majo', 'junio', 'julio', 'a\u016dgusto', 'septembro', 'oktobro', 'novembro', 'decembro'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'a\u016dg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['diman\u0109o', 'lundo', 'mardo', 'merkredo', '\u0135a\u016ddo', 'vendredo', 'sabato'],
+  SHORTWEEKDAYS: ['di', 'lu', 'ma', 'me', '\u0135a', 've', 'sa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1a kvaronjaro', '2a kvaronjaro', '3a kvaronjaro', '4a kvaronjaro'],
+  AMPMS: ['atm', 'ptm'],
+  DATEFORMATS: ["EEEE, d-'a' 'de' MMMM y", 'y-MMMM-dd', 'y-MMM-dd', 'yy-MM-dd'],
+  TIMEFORMATS: ["H-'a' 'horo' 'kaj' m:ss zzzz", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_AR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_AR.js
new file mode 100644
index 0000000..4c09034
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_AR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ["HH'h'''mm:ss zzzz", 'H:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_BO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_BO.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_BO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CL.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CL.js
new file mode 100644
index 0000000..59641c1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CL.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd-MM-yyyy', 'dd-MM-yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CO.js
new file mode 100644
index 0000000..359b32c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'd/MM/yyyy', 'd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CR.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_CR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_DO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_DO.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_DO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_EC.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_EC.js
new file mode 100644
index 0000000..655af3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_EC.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_ES.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_ES.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_ES.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_GT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_GT.js
new file mode 100644
index 0000000..08b46b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_GT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'd/MM/yyyy', 'd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_HN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_HN.js
new file mode 100644
index 0000000..1c6aae0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_HN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE dd 'de' MMMM 'de' y", "dd 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_MX.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_MX.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_MX.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_NI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_NI.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_NI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PA.js
new file mode 100644
index 0000000..f0b67f7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'MM/dd/yyyy', 'MM/dd/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PE.js
new file mode 100644
index 0000000..6a08edd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'd/MM/yy'],
+  TIMEFORMATS: ["HH'H'mm''ss\" zzzz", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PR.js
new file mode 100644
index 0000000..f0b67f7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'MM/dd/yyyy', 'MM/dd/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PY.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_PY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_SV.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_SV.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_SV.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_US.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_US.js
new file mode 100644
index 0000000..9fa1ce2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_US.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_UY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_UY.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_UY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_VE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_VE.js
new file mode 100644
index 0000000..81ccd9c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__es_VE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'anno D\u00f3mini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'],
+  SHORTMONTHS: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'],
+  WEEKDAYS: ['domingo', 'lunes', 'martes', 'mi\u00e9rcoles', 'jueves', 'viernes', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mi\u00e9', 'jue', 'vie', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2\u00ba trimestre', '3er trimestre', '4\u00ba trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ["EEEE d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__et.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__et.js
new file mode 100644
index 0000000..d518765
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__et.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.m.a.', 'm.a.j.'],
+  ERANAMES: ['enne meie aega', 'meie aja j\u00e4rgi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['jaanuar', 'veebruar', 'm\u00e4rts', 'aprill', 'mai', 'juuni', 'juuli', 'august', 'september', 'oktoober', 'november', 'detsember'],
+  SHORTMONTHS: ['jaan', 'veebr', 'm\u00e4rts', 'apr', 'mai', 'juuni', 'juuli', 'aug', 'sept', 'okt', 'nov', 'dets'],
+  WEEKDAYS: ['p\u00fchap\u00e4ev', 'esmasp\u00e4ev', 'teisip\u00e4ev', 'kolmap\u00e4ev', 'neljap\u00e4ev', 'reede', 'laup\u00e4ev'],
+  SHORTWEEKDAYS: ['P', 'E', 'T', 'K', 'N', 'R', 'L'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__et_EE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__et_EE.js
new file mode 100644
index 0000000..d518765
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__et_EE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['e.m.a.', 'm.a.j.'],
+  ERANAMES: ['enne meie aega', 'meie aja j\u00e4rgi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['jaanuar', 'veebruar', 'm\u00e4rts', 'aprill', 'mai', 'juuni', 'juuli', 'august', 'september', 'oktoober', 'november', 'detsember'],
+  SHORTMONTHS: ['jaan', 'veebr', 'm\u00e4rts', 'apr', 'mai', 'juuni', 'juuli', 'aug', 'sept', 'okt', 'nov', 'dets'],
+  WEEKDAYS: ['p\u00fchap\u00e4ev', 'esmasp\u00e4ev', 'teisip\u00e4ev', 'kolmap\u00e4ev', 'neljap\u00e4ev', 'reede', 'laup\u00e4ev'],
+  SHORTWEEKDAYS: ['P', 'E', 'T', 'K', 'N', 'R', 'L'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d, MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eu.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eu.js
new file mode 100644
index 0000000..3d291f3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eu.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
+  MONTHS: ['urtarrila', 'otsaila', 'martxoa', 'apirila', 'maiatza', 'ekaina', 'uztaila', 'abuztua', 'iraila', 'urria', 'azaroa', 'abendua'],
+  SHORTMONTHS: ['urt', 'ots', 'mar', 'api', 'mai', 'eka', 'uzt', 'abu', 'ira', 'urr', 'aza', 'abe'],
+  WEEKDAYS: ['igandea', 'astelehena', 'asteartea', 'asteazkena', 'osteguna', 'ostirala', 'larunbata'],
+  SHORTWEEKDAYS: ['ig', 'al', 'as', 'az', 'og', 'or', 'lr'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1Hh', '2Hh', '3Hh', '4Hh'],
+  QUARTERS: ['1. hiruhilekoa', '2. hiruhilekoa', '3. hiruhilekoa', '4. hiruhilekoa'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, y'eko' MMMM'ren' dd'a'", "y'eko' MMM'ren' dd'a'", 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eu_ES.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eu_ES.js
new file mode 100644
index 0000000..3d291f3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__eu_ES.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['U', 'O', 'M', 'A', 'M', 'E', 'U', 'A', 'I', 'U', 'A', 'A'],
+  MONTHS: ['urtarrila', 'otsaila', 'martxoa', 'apirila', 'maiatza', 'ekaina', 'uztaila', 'abuztua', 'iraila', 'urria', 'azaroa', 'abendua'],
+  SHORTMONTHS: ['urt', 'ots', 'mar', 'api', 'mai', 'eka', 'uzt', 'abu', 'ira', 'urr', 'aza', 'abe'],
+  WEEKDAYS: ['igandea', 'astelehena', 'asteartea', 'asteazkena', 'osteguna', 'ostirala', 'larunbata'],
+  SHORTWEEKDAYS: ['ig', 'al', 'as', 'az', 'og', 'or', 'lr'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1Hh', '2Hh', '3Hh', '4Hh'],
+  QUARTERS: ['1. hiruhilekoa', '2. hiruhilekoa', '3. hiruhilekoa', '4. hiruhilekoa'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, y'eko' MMMM'ren' dd'a'", "y'eko' MMM'ren' dd'a'", 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa.js
new file mode 100644
index 0000000..5172c11
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0632 \u0645\u06cc\u0644\u0627\u062f', '\u0645\u06cc\u0644\u0627\u062f\u06cc'],
+  NARROWMONTHS: ['\u0698', '\u0641', '\u0645', '\u0622', '\u0645', '\u0698', '\u0698', '\u0627', '\u0633', '\u0627', '\u0646', '\u062f'],
+  MONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647\u0654', '\u0641\u0648\u0631\u06cc\u0647\u0654', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647\u0654', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647\u0654', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  STANDALONEMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647', '\u0641\u0648\u0631\u06cc\u0647', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647\u0654', '\u0641\u0648\u0631\u06cc\u0647\u0654', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647\u0654', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647\u0654', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  STANDALONESHORTMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647', '\u0641\u0648\u0631\u06cc\u0647', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '\u0686', '\u067e', '\u062c', '\u0634'],
+  SHORTQUARTERS: ['\u0633\u200c\u0645\u06f1', '\u0633\u200c\u0645\u06f2', '\u0633\u200c\u0645\u06f3', '\u0633\u200c\u0645\u06f4'],
+  QUARTERS: ['\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0627\u0648\u0644', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u062f\u0648\u0645', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0633\u0648\u0645', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0686\u0647\u0627\u0631\u0645'],
+  AMPMS: ['\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631', '\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'yyyy/M/d', 'yy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa_AF.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa_AF.js
new file mode 100644
index 0000000..562b057
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa_AF.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0632 \u0645\u06cc\u0644\u0627\u062f', '\u0645\u06cc\u0644\u0627\u062f\u06cc'],
+  NARROWMONTHS: ['\u062c', '\u0641', '\u0645', '\u0627', '\u0645', '\u062c', '\u062c', '\u0627', '\u0633', '\u0627', '\u0646', '\u062f'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0628\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u067e\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  STANDALONEMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647', '\u0641\u0648\u0631\u06cc\u0647', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648', '\u0641\u0648\u0631\u06cc\u0647\u0654', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0640\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0645'],
+  STANDALONESHORTMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647', '\u0641\u0648\u0631\u06cc\u0647', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '\u0686', '\u067e', '\u062c', '\u0634'],
+  SHORTQUARTERS: ['\u0633\u200c\u0645\u06f1', '\u0633\u200c\u0645\u06f2', '\u0633\u200c\u0645\u06f3', '\u0633\u200c\u0645\u06f4'],
+  QUARTERS: ['\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0627\u0648\u0644', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u062f\u0648\u0645', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0633\u0648\u0645', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0686\u0647\u0627\u0631\u0645'],
+  AMPMS: ['\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631', '\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'yyyy/M/d', 'yy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa_IR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa_IR.js
new file mode 100644
index 0000000..5172c11
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fa_IR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0627\u0632 \u0645\u06cc\u0644\u0627\u062f', '\u0645\u06cc\u0644\u0627\u062f\u06cc'],
+  NARROWMONTHS: ['\u0698', '\u0641', '\u0645', '\u0622', '\u0645', '\u0698', '\u0698', '\u0627', '\u0633', '\u0627', '\u0646', '\u062f'],
+  MONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647\u0654', '\u0641\u0648\u0631\u06cc\u0647\u0654', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647\u0654', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647\u0654', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  STANDALONEMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647', '\u0641\u0648\u0631\u06cc\u0647', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647\u0654', '\u0641\u0648\u0631\u06cc\u0647\u0654', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647\u0654', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647\u0654', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  STANDALONESHORTMONTHS: ['\u0698\u0627\u0646\u0648\u06cc\u0647', '\u0641\u0648\u0631\u06cc\u0647', '\u0645\u0627\u0631\u0633', '\u0622\u0648\u0631\u06cc\u0644', '\u0645\u0647', '\u0698\u0648\u0626\u0646', '\u0698\u0648\u0626\u06cc\u0647', '\u0627\u0648\u062a', '\u0633\u067e\u062a\u0627\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0628\u0631', '\u0646\u0648\u0627\u0645\u0628\u0631', '\u062f\u0633\u0627\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '\u0686', '\u067e', '\u062c', '\u0634'],
+  SHORTQUARTERS: ['\u0633\u200c\u0645\u06f1', '\u0633\u200c\u0645\u06f2', '\u0633\u200c\u0645\u06f3', '\u0633\u200c\u0645\u06f4'],
+  QUARTERS: ['\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0627\u0648\u0644', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u062f\u0648\u0645', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0633\u0648\u0645', '\u0633\u0647\u200c\u0645\u0627\u0647\u0647\u0654 \u0686\u0647\u0627\u0631\u0645'],
+  AMPMS: ['\u0642\u0628\u0644 \u0627\u0632 \u0638\u0647\u0631', '\u0628\u0639\u062f \u0627\u0632 \u0638\u0647\u0631'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'yyyy/M/d', 'yy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fi.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fi.js
new file mode 100644
index 0000000..59493be
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fi.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['eKr.', 'jKr.'],
+  ERANAMES: ['ennen Kristuksen syntym\u00e4\u00e4', 'j\u00e4lkeen Kristuksen syntym\u00e4n'],
+  NARROWMONTHS: ['T', 'H', 'M', 'H', 'T', 'K', 'H', 'E', 'S', 'L', 'M', 'J'],
+  MONTHS: ['tammikuuta', 'helmikuuta', 'maaliskuuta', 'huhtikuuta', 'toukokuuta', 'kes\u00e4kuuta', 'hein\u00e4kuuta', 'elokuuta', 'syyskuuta', 'lokakuuta', 'marraskuuta', 'joulukuuta'],
+  STANDALONEMONTHS: ['tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', 'toukokuu', 'kes\u00e4kuu', 'hein\u00e4kuu', 'elokuu', 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu'],
+  SHORTMONTHS: ['tammikuuta', 'helmikuuta', 'maaliskuuta', 'huhtikuuta', 'toukokuuta', 'kes\u00e4kuuta', 'hein\u00e4kuuta', 'elokuuta', 'syyskuuta', 'lokakuuta', 'marraskuuta', 'joulukuuta'],
+  STANDALONESHORTMONTHS: ['tammi', 'helmi', 'maalis', 'huhti', 'touko', 'kes\u00e4', 'hein\u00e4', 'elo', 'syys', 'loka', 'marras', 'joulu'],
+  WEEKDAYS: ['sunnuntaina', 'maanantaina', 'tiistaina', 'keskiviikkona', 'torstaina', 'perjantaina', 'lauantaina'],
+  STANDALONEWEEKDAYS: ['sunnuntai', 'maanantai', 'tiistai', 'keskiviikko', 'torstai', 'perjantai', 'lauantai'],
+  SHORTWEEKDAYS: ['su', 'ma', 'ti', 'ke', 'to', 'pe', 'la'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'K', 'T', 'P', 'L'],
+  SHORTQUARTERS: ['1. nelj.', '2. nelj.', '3. nelj.', '4. nelj.'],
+  QUARTERS: ['1. nelj\u00e4nnes', '2. nelj\u00e4nnes', '3. nelj\u00e4nnes', '4. nelj\u00e4nnes'],
+  AMPMS: ['ap.', 'ip.'],
+  DATEFORMATS: ['EEEE d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yyyy'],
+  TIMEFORMATS: ['H.mm.ss zzzz', 'H.mm.ss z', 'H.mm.ss', 'H.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fi_FI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fi_FI.js
new file mode 100644
index 0000000..59493be
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fi_FI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['eKr.', 'jKr.'],
+  ERANAMES: ['ennen Kristuksen syntym\u00e4\u00e4', 'j\u00e4lkeen Kristuksen syntym\u00e4n'],
+  NARROWMONTHS: ['T', 'H', 'M', 'H', 'T', 'K', 'H', 'E', 'S', 'L', 'M', 'J'],
+  MONTHS: ['tammikuuta', 'helmikuuta', 'maaliskuuta', 'huhtikuuta', 'toukokuuta', 'kes\u00e4kuuta', 'hein\u00e4kuuta', 'elokuuta', 'syyskuuta', 'lokakuuta', 'marraskuuta', 'joulukuuta'],
+  STANDALONEMONTHS: ['tammikuu', 'helmikuu', 'maaliskuu', 'huhtikuu', 'toukokuu', 'kes\u00e4kuu', 'hein\u00e4kuu', 'elokuu', 'syyskuu', 'lokakuu', 'marraskuu', 'joulukuu'],
+  SHORTMONTHS: ['tammikuuta', 'helmikuuta', 'maaliskuuta', 'huhtikuuta', 'toukokuuta', 'kes\u00e4kuuta', 'hein\u00e4kuuta', 'elokuuta', 'syyskuuta', 'lokakuuta', 'marraskuuta', 'joulukuuta'],
+  STANDALONESHORTMONTHS: ['tammi', 'helmi', 'maalis', 'huhti', 'touko', 'kes\u00e4', 'hein\u00e4', 'elo', 'syys', 'loka', 'marras', 'joulu'],
+  WEEKDAYS: ['sunnuntaina', 'maanantaina', 'tiistaina', 'keskiviikkona', 'torstaina', 'perjantaina', 'lauantaina'],
+  STANDALONEWEEKDAYS: ['sunnuntai', 'maanantai', 'tiistai', 'keskiviikko', 'torstai', 'perjantai', 'lauantai'],
+  SHORTWEEKDAYS: ['su', 'ma', 'ti', 'ke', 'to', 'pe', 'la'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'K', 'T', 'P', 'L'],
+  SHORTQUARTERS: ['1. nelj.', '2. nelj.', '3. nelj.', '4. nelj.'],
+  QUARTERS: ['1. nelj\u00e4nnes', '2. nelj\u00e4nnes', '3. nelj\u00e4nnes', '4. nelj\u00e4nnes'],
+  AMPMS: ['ap.', 'ip.'],
+  DATEFORMATS: ['EEEE d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yyyy'],
+  TIMEFORMATS: ['H.mm.ss zzzz', 'H.mm.ss z', 'H.mm.ss', 'H.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fil.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fil.js
new file mode 100644
index 0000000..0b289fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fil.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['E', 'P', 'M', 'A', 'M', 'H', 'H', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', 'Agosto', 'Setyembre', 'Oktubre', 'Nobyembre', 'Disyembre'],
+  SHORTMONTHS: ['Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', 'Set', 'Okt', 'Nob', 'Dis'],
+  WEEKDAYS: ['Linggo', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado'],
+  SHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Mye', 'Huw', 'Bye', 'Sab'],
+  STANDALONESHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab'],
+  NARROWWEEKDAYS: ['L', 'L', 'M', 'M', 'H', 'B', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM dd y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fil_PH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fil_PH.js
new file mode 100644
index 0000000..0b289fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fil_PH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['E', 'P', 'M', 'A', 'M', 'H', 'H', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', 'Agosto', 'Setyembre', 'Oktubre', 'Nobyembre', 'Disyembre'],
+  SHORTMONTHS: ['Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', 'Set', 'Okt', 'Nob', 'Dis'],
+  WEEKDAYS: ['Linggo', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado'],
+  SHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Mye', 'Huw', 'Bye', 'Sab'],
+  STANDALONESHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab'],
+  NARROWWEEKDAYS: ['L', 'L', 'M', 'M', 'H', 'B', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM dd y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fo.js
new file mode 100644
index 0000000..21bae21
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['fyrir Krist', 'eftir Krist'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['januar', 'februar', 'mars', 'apr\u00edl', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'des'],
+  WEEKDAYS: ['sunnudagur', 'm\u00e1nadagur', 't\u00fdsdagur', 'mikudagur', 'h\u00f3sdagur', 'fr\u00edggjadagur', 'leygardagur'],
+  SHORTWEEKDAYS: ['sun', 'm\u00e1n', 't\u00fds', 'mik', 'h\u00f3s', 'fr\u00ed', 'ley'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'd. MMM y', 'dd-MM-yyyy', 'dd-MM-yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fo_FO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fo_FO.js
new file mode 100644
index 0000000..21bae21
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fo_FO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['fyrir Krist', 'eftir Krist'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['januar', 'februar', 'mars', 'apr\u00edl', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'des'],
+  WEEKDAYS: ['sunnudagur', 'm\u00e1nadagur', 't\u00fdsdagur', 'mikudagur', 'h\u00f3sdagur', 'fr\u00edggjadagur', 'leygardagur'],
+  SHORTWEEKDAYS: ['sun', 'm\u00e1n', 't\u00fds', 'mik', 'h\u00f3s', 'fr\u00ed', 'ley'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'd. MMM y', 'dd-MM-yyyy', 'dd-MM-yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr.js
new file mode 100644
index 0000000..46b02d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_BE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_BE.js
new file mode 100644
index 0000000..77752b1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_BE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'd/MM/yy'],
+  TIMEFORMATS: ["H 'h' mm 'min' ss 's' zzzz", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_CA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_CA.js
new file mode 100644
index 0000000..565e49d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_CA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'yyyy-MM-dd', 'yy-MM-dd'],
+  TIMEFORMATS: ["HH 'h' mm 'min' ss 's' zzzz", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_CH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_CH.js
new file mode 100644
index 0000000..bf81b38
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_CH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd MMM y', 'dd.MM.yy'],
+  TIMEFORMATS: ["HH.mm:ss 'h' zzzz", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_FR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_FR.js
new file mode 100644
index 0000000..46b02d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_FR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_LU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_LU.js
new file mode 100644
index 0000000..46b02d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_LU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_MC.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_MC.js
new file mode 100644
index 0000000..46b02d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_MC.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_SN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_SN.js
new file mode 100644
index 0000000..46b02d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fr_SN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['av. J.-C.', 'ap. J.-C.'],
+  ERANAMES: ['avant J\u00e9sus-Christ', 'apr\u00e8s J\u00e9sus-Christ'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janvier', 'f\u00e9vrier', 'mars', 'avril', 'mai', 'juin', 'juillet', 'ao\u00fbt', 'septembre', 'octobre', 'novembre', 'd\u00e9cembre'],
+  SHORTMONTHS: ['janv.', 'f\u00e9vr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'ao\u00fbt', 'sept.', 'oct.', 'nov.', 'd\u00e9c.'],
+  WEEKDAYS: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'],
+  SHORTWEEKDAYS: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1er trimestre', '2e trimestre', '3e trimestre', '4e trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fur.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fur.js
new file mode 100644
index 0000000..81b81af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fur.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pdC', 'ddC'],
+  ERANAMES: ['pdC', 'ddC'],
+  NARROWMONTHS: ['Z', 'F', 'M', 'A', 'M', 'J', 'L', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Zen\u00e2r', 'Fevr\u00e2r', 'Mar\u00e7', 'Avr\u00eel', 'Mai', 'Jugn', 'Lui', 'Avost', 'Setembar', 'Otubar', 'Novembar', 'Dicembar'],
+  SHORTMONTHS: ['Zen', 'Fev', 'Mar', 'Avr', 'Mai', 'Jug', 'Lui', 'Avo', 'Set', 'Otu', 'Nov', 'Dic'],
+  WEEKDAYS: ['domenie', 'lunis', 'martars', 'miercus', 'joibe', 'vinars', 'sabide'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mie', 'joi', 'vin', 'sab'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['Prin trimestri', 'Secont trimestri', 'Tier\u00e7 trimestri', 'Cuart trimestri'],
+  AMPMS: ['a.', 'p.'],
+  DATEFORMATS: ["EEEE d 'di' MMMM 'dal' y", "d 'di' MMMM 'dal' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fur_IT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fur_IT.js
new file mode 100644
index 0000000..81b81af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__fur_IT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pdC', 'ddC'],
+  ERANAMES: ['pdC', 'ddC'],
+  NARROWMONTHS: ['Z', 'F', 'M', 'A', 'M', 'J', 'L', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Zen\u00e2r', 'Fevr\u00e2r', 'Mar\u00e7', 'Avr\u00eel', 'Mai', 'Jugn', 'Lui', 'Avost', 'Setembar', 'Otubar', 'Novembar', 'Dicembar'],
+  SHORTMONTHS: ['Zen', 'Fev', 'Mar', 'Avr', 'Mai', 'Jug', 'Lui', 'Avo', 'Set', 'Otu', 'Nov', 'Dic'],
+  WEEKDAYS: ['domenie', 'lunis', 'martars', 'miercus', 'joibe', 'vinars', 'sabide'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mie', 'joi', 'vin', 'sab'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['Prin trimestri', 'Secont trimestri', 'Tier\u00e7 trimestri', 'Cuart trimestri'],
+  AMPMS: ['a.', 'p.'],
+  DATEFORMATS: ["EEEE d 'di' MMMM 'dal' y", "d 'di' MMMM 'dal' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ga.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ga.js
new file mode 100644
index 0000000..c7d1d71
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ga.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['RC', 'AD'],
+  ERANAMES: ['Roimh Chr\u00edost', 'Anno Domini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'B', 'M', 'I', 'L', 'M', 'D', 'S', 'N'],
+  MONTHS: ['Ean\u00e1ir', 'Feabhra', 'M\u00e1rta', 'Aibre\u00e1n', 'Bealtaine', 'Meitheamh', 'I\u00fail', 'L\u00fanasa', 'Me\u00e1n F\u00f3mhair', 'Deireadh F\u00f3mhair', 'Samhain', 'Nollaig'],
+  SHORTMONTHS: ['Ean', 'Feabh', 'M\u00e1rta', 'Aib', 'Beal', 'Meith', 'I\u00fail', 'L\u00fan', 'MF\u00f3mh', 'DF\u00f3mh', 'Samh', 'Noll'],
+  WEEKDAYS: ['D\u00e9 Domhnaigh', 'D\u00e9 Luain', 'D\u00e9 M\u00e1irt', 'D\u00e9 C\u00e9adaoin', 'D\u00e9ardaoin', 'D\u00e9 hAoine', 'D\u00e9 Sathairn'],
+  SHORTWEEKDAYS: ['Domh', 'Luan', 'M\u00e1irt', 'C\u00e9ad', 'D\u00e9ar', 'Aoine', 'Sath'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'C', 'D', 'A', 'S'],
+  SHORTQUARTERS: ['R1', 'R2', 'R3', 'R4'],
+  QUARTERS: ['1\u00fa r\u00e1ithe', '2\u00fa r\u00e1ithe', '3\u00fa r\u00e1ithe', '4\u00fa r\u00e1ithe'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ga_IE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ga_IE.js
new file mode 100644
index 0000000..c7d1d71
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ga_IE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['RC', 'AD'],
+  ERANAMES: ['Roimh Chr\u00edost', 'Anno Domini'],
+  NARROWMONTHS: ['E', 'F', 'M', 'A', 'B', 'M', 'I', 'L', 'M', 'D', 'S', 'N'],
+  MONTHS: ['Ean\u00e1ir', 'Feabhra', 'M\u00e1rta', 'Aibre\u00e1n', 'Bealtaine', 'Meitheamh', 'I\u00fail', 'L\u00fanasa', 'Me\u00e1n F\u00f3mhair', 'Deireadh F\u00f3mhair', 'Samhain', 'Nollaig'],
+  SHORTMONTHS: ['Ean', 'Feabh', 'M\u00e1rta', 'Aib', 'Beal', 'Meith', 'I\u00fail', 'L\u00fan', 'MF\u00f3mh', 'DF\u00f3mh', 'Samh', 'Noll'],
+  WEEKDAYS: ['D\u00e9 Domhnaigh', 'D\u00e9 Luain', 'D\u00e9 M\u00e1irt', 'D\u00e9 C\u00e9adaoin', 'D\u00e9ardaoin', 'D\u00e9 hAoine', 'D\u00e9 Sathairn'],
+  SHORTWEEKDAYS: ['Domh', 'Luan', 'M\u00e1irt', 'C\u00e9ad', 'D\u00e9ar', 'Aoine', 'Sath'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'C', 'D', 'A', 'S'],
+  SHORTQUARTERS: ['R1', 'R2', 'R3', 'R4'],
+  QUARTERS: ['1\u00fa r\u00e1ithe', '2\u00fa r\u00e1ithe', '3\u00fa r\u00e1ithe', '4\u00fa r\u00e1ithe'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gaa.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gaa.js
new file mode 100644
index 0000000..fd6822f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gaa.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['DJ', 'KJ'],
+  ERANAMES: ['Dani Jesu', 'KJ'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Aharabata', 'Oflo', 'Ochokrikri', 'Abeibee', 'Agbeinaa', 'Otukwadan', 'Maawe', 'Manyawale', 'Gbo', 'Anton', 'Alemle', 'Afuabee'],
+  SHORTMONTHS: ['Aha', 'Ofl', 'Och', 'Abe', 'Agb', 'Otu', 'Maa', 'Man', 'Gbo', 'Ant', 'Ale', 'Afu'],
+  WEEKDAYS: ['Hogbaa', 'Dzu', 'Dzufo', 'Sho', 'Soo', 'Sohaa', 'Ho'],
+  SHORTWEEKDAYS: ['Ho', 'Dzu', 'Dzf', 'Sho', 'Soo', 'Soh', 'Ho'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['LB', 'SN'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gaa_GH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gaa_GH.js
new file mode 100644
index 0000000..fd6822f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gaa_GH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['DJ', 'KJ'],
+  ERANAMES: ['Dani Jesu', 'KJ'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Aharabata', 'Oflo', 'Ochokrikri', 'Abeibee', 'Agbeinaa', 'Otukwadan', 'Maawe', 'Manyawale', 'Gbo', 'Anton', 'Alemle', 'Afuabee'],
+  SHORTMONTHS: ['Aha', 'Ofl', 'Och', 'Abe', 'Agb', 'Otu', 'Maa', 'Man', 'Gbo', 'Ant', 'Ale', 'Afu'],
+  WEEKDAYS: ['Hogbaa', 'Dzu', 'Dzufo', 'Sho', 'Soo', 'Sohaa', 'Ho'],
+  SHORTWEEKDAYS: ['Ho', 'Dzu', 'Dzf', 'Sho', 'Soo', 'Soh', 'Ho'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['LB', 'SN'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez.js
new file mode 100644
index 0000000..246444e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1320', '\u12a8', '\u1218', '\u12a0', '\u130d', '\u1220', '\u1210', '\u1290', '\u12a8', '\u1320', '\u1280', '\u1280'],
+  MONTHS: ['\u1320\u1210\u1228', '\u12a8\u1270\u1270', '\u1218\u1308\u1260', '\u12a0\u1280\u12d8', '\u130d\u1295\u1263\u1275', '\u1220\u1295\u12e8', '\u1210\u1218\u1208', '\u1290\u1210\u1230', '\u12a8\u1228\u1218', '\u1320\u1240\u1218', '\u1280\u12f0\u1228', '\u1280\u1220\u1220'],
+  SHORTMONTHS: ['\u1320\u1210\u1228', '\u12a8\u1270\u1270', '\u1218\u1308\u1260', '\u12a0\u1280\u12d8', '\u130d\u1295\u1263', '\u1220\u1295\u12e8', '\u1210\u1218\u1208', '\u1290\u1210\u1230', '\u12a8\u1228\u1218', '\u1320\u1240\u1218', '\u1280\u12f0\u1228', '\u1280\u1220\u1220'],
+  WEEKDAYS: ['\u12a5\u1281\u12f5', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u122b\u1265\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1260', '\u1240\u12f3\u121a\u1275'],
+  SHORTWEEKDAYS: ['\u12a5\u1281\u12f5', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u122b\u1265\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1260', '\u1240\u12f3\u121a'],
+  NARROWWEEKDAYS: ['\u12a5', '\u1230', '\u1220', '\u122b', '\u1210', '\u12d3', '\u1240'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u133d\u1263\u1215', '\u121d\u1234\u1275'],
+  DATEFORMATS: ['EEEE\u1365 dd MMMM \u1218\u12d3\u120d\u1275 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez_ER.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez_ER.js
new file mode 100644
index 0000000..685cdd6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez_ER.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1320', '\u12a8', '\u1218', '\u12a0', '\u130d', '\u1220', '\u1210', '\u1290', '\u12a8', '\u1320', '\u1280', '\u1280'],
+  MONTHS: ['\u1320\u1210\u1228', '\u12a8\u1270\u1270', '\u1218\u1308\u1260', '\u12a0\u1280\u12d8', '\u130d\u1295\u1263\u1275', '\u1220\u1295\u12e8', '\u1210\u1218\u1208', '\u1290\u1210\u1230', '\u12a8\u1228\u1218', '\u1320\u1240\u1218', '\u1280\u12f0\u1228', '\u1280\u1220\u1220'],
+  SHORTMONTHS: ['\u1320\u1210\u1228', '\u12a8\u1270\u1270', '\u1218\u1308\u1260', '\u12a0\u1280\u12d8', '\u130d\u1295\u1263', '\u1220\u1295\u12e8', '\u1210\u1218\u1208', '\u1290\u1210\u1230', '\u12a8\u1228\u1218', '\u1320\u1240\u1218', '\u1280\u12f0\u1228', '\u1280\u1220\u1220'],
+  WEEKDAYS: ['\u12a5\u1281\u12f5', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u122b\u1265\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1260', '\u1240\u12f3\u121a\u1275'],
+  SHORTWEEKDAYS: ['\u12a5\u1281\u12f5', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u122b\u1265\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1260', '\u1240\u12f3\u121a'],
+  NARROWWEEKDAYS: ['\u12a5', '\u1230', '\u1220', '\u122b', '\u1210', '\u12d3', '\u1240'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u133d\u1263\u1215', '\u121d\u1234\u1275'],
+  DATEFORMATS: ['EEEE\u1365 dd MMMM \u1218\u12d3\u120d\u1275 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez_ET.js
new file mode 100644
index 0000000..685cdd6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gez_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1320', '\u12a8', '\u1218', '\u12a0', '\u130d', '\u1220', '\u1210', '\u1290', '\u12a8', '\u1320', '\u1280', '\u1280'],
+  MONTHS: ['\u1320\u1210\u1228', '\u12a8\u1270\u1270', '\u1218\u1308\u1260', '\u12a0\u1280\u12d8', '\u130d\u1295\u1263\u1275', '\u1220\u1295\u12e8', '\u1210\u1218\u1208', '\u1290\u1210\u1230', '\u12a8\u1228\u1218', '\u1320\u1240\u1218', '\u1280\u12f0\u1228', '\u1280\u1220\u1220'],
+  SHORTMONTHS: ['\u1320\u1210\u1228', '\u12a8\u1270\u1270', '\u1218\u1308\u1260', '\u12a0\u1280\u12d8', '\u130d\u1295\u1263', '\u1220\u1295\u12e8', '\u1210\u1218\u1208', '\u1290\u1210\u1230', '\u12a8\u1228\u1218', '\u1320\u1240\u1218', '\u1280\u12f0\u1228', '\u1280\u1220\u1220'],
+  WEEKDAYS: ['\u12a5\u1281\u12f5', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u122b\u1265\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1260', '\u1240\u12f3\u121a\u1275'],
+  SHORTWEEKDAYS: ['\u12a5\u1281\u12f5', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u122b\u1265\u12d5', '\u1210\u1219\u1235', '\u12d3\u122d\u1260', '\u1240\u12f3\u121a'],
+  NARROWWEEKDAYS: ['\u12a5', '\u1230', '\u1220', '\u122b', '\u1210', '\u12d3', '\u1240'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u133d\u1263\u1215', '\u121d\u1234\u1275'],
+  DATEFORMATS: ['EEEE\u1365 dd MMMM \u1218\u12d3\u120d\u1275 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gl.js
new file mode 100644
index 0000000..24920de
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'despois de Cristo'],
+  NARROWMONTHS: ['X', 'F', 'M', 'A', 'M', 'X', 'X', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Xaneiro', 'Febreiro', 'Marzo', 'Abril', 'Maio', 'Xu\u00f1o', 'Xullo', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Decembro'],
+  SHORTMONTHS: ['Xan', 'Feb', 'Mar', 'Abr', 'Mai', 'Xu\u00f1', 'Xul', 'Ago', 'Set', 'Out', 'Nov', 'Dec'],
+  WEEKDAYS: ['Domingo', 'Luns', 'Martes', 'M\u00e9rcores', 'Xoves', 'Venres', 'S\u00e1bado'],
+  SHORTWEEKDAYS: ['Dom', 'Lun', 'Mar', 'M\u00e9r', 'Xov', 'Ven', 'S\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'X', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1o trimestre', '2o trimestre', '3o trimestre', '4o trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'd MMM, y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gl_ES.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gl_ES.js
new file mode 100644
index 0000000..24920de
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gl_ES.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['antes de Cristo', 'despois de Cristo'],
+  NARROWMONTHS: ['X', 'F', 'M', 'A', 'M', 'X', 'X', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Xaneiro', 'Febreiro', 'Marzo', 'Abril', 'Maio', 'Xu\u00f1o', 'Xullo', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Decembro'],
+  SHORTMONTHS: ['Xan', 'Feb', 'Mar', 'Abr', 'Mai', 'Xu\u00f1', 'Xul', 'Ago', 'Set', 'Out', 'Nov', 'Dec'],
+  WEEKDAYS: ['Domingo', 'Luns', 'Martes', 'M\u00e9rcores', 'Xoves', 'Venres', 'S\u00e1bado'],
+  SHORTWEEKDAYS: ['Dom', 'Lun', 'Mar', 'M\u00e9r', 'Xov', 'Ven', 'S\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'X', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1o trimestre', '2o trimestre', '3o trimestre', '4o trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'd MMM, y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gsw.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gsw.js
new file mode 100644
index 0000000..7cdd19e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gsw.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'Auguscht', 'Sept\u00e4mber', 'Oktoober', 'Nov\u00e4mber', 'Dez\u00e4mber'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sunntig', 'M\u00e4\u00e4ntig', 'Ziischtig', 'Mittwuch', 'Dunschtig', 'Friitig', 'Samschtig'],
+  SHORTWEEKDAYS: ['Su.', 'M\u00e4.', 'Zi.', 'Mi.', 'Du.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nam.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gsw_CH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gsw_CH.js
new file mode 100644
index 0000000..7cdd19e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gsw_CH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00e4rz', 'April', 'Mai', 'Juni', 'Juli', 'Auguscht', 'Sept\u00e4mber', 'Oktoober', 'Nov\u00e4mber', 'Dez\u00e4mber'],
+  SHORTMONTHS: ['Jan', 'Feb', 'M\u00e4r', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sunntig', 'M\u00e4\u00e4ntig', 'Ziischtig', 'Mittwuch', 'Dunschtig', 'Friitig', 'Samschtig'],
+  SHORTWEEKDAYS: ['Su.', 'M\u00e4.', 'Zi.', 'Mi.', 'Du.', 'Fr.', 'Sa.'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nam.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gu.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gu.js
new file mode 100644
index 0000000..cce1505
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gu.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0a88\u0ab8\u0ab5\u0ac0\u0ab8\u0aa8 \u0aaa\u0ac2\u0ab0\u0acd\u0ab5\u0ac7', '\u0a87\u0ab8\u0ab5\u0ac0\u0ab8\u0aa8'],
+  NARROWMONTHS: ['\u0a9c\u0abe', '\u0aab\u0ac7', '\u0aae\u0abe', '\u0a8f', '\u0aae\u0ac7', '\u0a9c\u0ac2', '\u0a9c\u0ac1', '\u0a91', '\u0ab8', '\u0a91', '\u0aa8', '\u0aa1\u0abf'],
+  MONTHS: ['\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1\u0a86\u0ab0\u0ac0', '\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1\u0a86\u0ab0\u0ac0', '\u0aae\u0abe\u0ab0\u0acd\u0a9a', '\u0a8f\u0aaa\u0acd\u0ab0\u0abf\u0ab2', '\u0aae\u0ac7', '\u0a9c\u0ac2\u0aa8', '\u0a9c\u0ac1\u0ab2\u0abe\u0a88', '\u0a91\u0a97\u0ab8\u0acd\u0a9f', '\u0ab8\u0aaa\u0acd\u0a9f\u0ac7\u0aae\u0acd\u0aac\u0ab0', '\u0a91\u0a95\u0acd\u0a9f\u0acd\u0aac\u0ab0', '\u0aa8\u0ab5\u0ac7\u0aae\u0acd\u0aac\u0ab0', '\u0aa1\u0abf\u0ab8\u0ac7\u0aae\u0acd\u0aac\u0ab0'],
+  SHORTMONTHS: ['\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1', '\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1', '\u0aae\u0abe\u0ab0\u0acd\u0a9a', '\u0a8f\u0aaa\u0acd\u0ab0\u0abf\u0ab2', '\u0aae\u0ac7', '\u0a9c\u0ac2\u0aa8', '\u0a9c\u0ac1\u0ab2\u0abe\u0a88', '\u0a91\u0a97\u0ab8\u0acd\u0a9f', '\u0ab8\u0aaa\u0acd\u0a9f\u0ac7', '\u0a91\u0a95\u0acd\u0a9f\u0acb', '\u0aa8\u0ab5\u0ac7', '\u0aa1\u0abf\u0ab8\u0ac7'],
+  WEEKDAYS: ['\u0ab0\u0ab5\u0abf\u0ab5\u0abe\u0ab0', '\u0ab8\u0acb\u0aae\u0ab5\u0abe\u0ab0', '\u0aae\u0a82\u0a97\u0ab3\u0ab5\u0abe\u0ab0', '\u0aac\u0ac1\u0aa7\u0ab5\u0abe\u0ab0', '\u0a97\u0ac1\u0ab0\u0ac1\u0ab5\u0abe\u0ab0', '\u0ab6\u0ac1\u0a95\u0acd\u0ab0\u0ab5\u0abe\u0ab0', '\u0ab6\u0aa8\u0abf\u0ab5\u0abe\u0ab0'],
+  SHORTWEEKDAYS: ['\u0ab0\u0ab5\u0abf', '\u0ab8\u0acb\u0aae', '\u0aae\u0a82\u0a97\u0ab3', '\u0aac\u0ac1\u0aa7', '\u0a97\u0ac1\u0ab0\u0ac1', '\u0ab6\u0ac1\u0a95\u0acd\u0ab0', '\u0ab6\u0aa8\u0abf'],
+  NARROWWEEKDAYS: ['\u0ab0', '\u0ab8\u0acb', '\u0aae\u0a82', '\u0aac\u0ac1', '\u0a97\u0ac1', '\u0ab6\u0ac1', '\u0ab6'],
+  SHORTQUARTERS: ['\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0ae7', '\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0ae8', '\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0ae9', '\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0aea'],
+  QUARTERS: ['\u0aaa\u0ab9\u0ab2\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95', '\u0aac\u0ac0\u0a9c\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95', '\u0aa4\u0acd\u0ab0\u0ac0\u0a9c\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95', '\u0a9a\u0acc\u0aa5\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd-MM-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gu_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gu_IN.js
new file mode 100644
index 0000000..cce1505
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gu_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0a88\u0ab8\u0ab5\u0ac0\u0ab8\u0aa8 \u0aaa\u0ac2\u0ab0\u0acd\u0ab5\u0ac7', '\u0a87\u0ab8\u0ab5\u0ac0\u0ab8\u0aa8'],
+  NARROWMONTHS: ['\u0a9c\u0abe', '\u0aab\u0ac7', '\u0aae\u0abe', '\u0a8f', '\u0aae\u0ac7', '\u0a9c\u0ac2', '\u0a9c\u0ac1', '\u0a91', '\u0ab8', '\u0a91', '\u0aa8', '\u0aa1\u0abf'],
+  MONTHS: ['\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1\u0a86\u0ab0\u0ac0', '\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1\u0a86\u0ab0\u0ac0', '\u0aae\u0abe\u0ab0\u0acd\u0a9a', '\u0a8f\u0aaa\u0acd\u0ab0\u0abf\u0ab2', '\u0aae\u0ac7', '\u0a9c\u0ac2\u0aa8', '\u0a9c\u0ac1\u0ab2\u0abe\u0a88', '\u0a91\u0a97\u0ab8\u0acd\u0a9f', '\u0ab8\u0aaa\u0acd\u0a9f\u0ac7\u0aae\u0acd\u0aac\u0ab0', '\u0a91\u0a95\u0acd\u0a9f\u0acd\u0aac\u0ab0', '\u0aa8\u0ab5\u0ac7\u0aae\u0acd\u0aac\u0ab0', '\u0aa1\u0abf\u0ab8\u0ac7\u0aae\u0acd\u0aac\u0ab0'],
+  SHORTMONTHS: ['\u0a9c\u0abe\u0aa8\u0acd\u0aaf\u0ac1', '\u0aab\u0ac7\u0aac\u0acd\u0ab0\u0ac1', '\u0aae\u0abe\u0ab0\u0acd\u0a9a', '\u0a8f\u0aaa\u0acd\u0ab0\u0abf\u0ab2', '\u0aae\u0ac7', '\u0a9c\u0ac2\u0aa8', '\u0a9c\u0ac1\u0ab2\u0abe\u0a88', '\u0a91\u0a97\u0ab8\u0acd\u0a9f', '\u0ab8\u0aaa\u0acd\u0a9f\u0ac7', '\u0a91\u0a95\u0acd\u0a9f\u0acb', '\u0aa8\u0ab5\u0ac7', '\u0aa1\u0abf\u0ab8\u0ac7'],
+  WEEKDAYS: ['\u0ab0\u0ab5\u0abf\u0ab5\u0abe\u0ab0', '\u0ab8\u0acb\u0aae\u0ab5\u0abe\u0ab0', '\u0aae\u0a82\u0a97\u0ab3\u0ab5\u0abe\u0ab0', '\u0aac\u0ac1\u0aa7\u0ab5\u0abe\u0ab0', '\u0a97\u0ac1\u0ab0\u0ac1\u0ab5\u0abe\u0ab0', '\u0ab6\u0ac1\u0a95\u0acd\u0ab0\u0ab5\u0abe\u0ab0', '\u0ab6\u0aa8\u0abf\u0ab5\u0abe\u0ab0'],
+  SHORTWEEKDAYS: ['\u0ab0\u0ab5\u0abf', '\u0ab8\u0acb\u0aae', '\u0aae\u0a82\u0a97\u0ab3', '\u0aac\u0ac1\u0aa7', '\u0a97\u0ac1\u0ab0\u0ac1', '\u0ab6\u0ac1\u0a95\u0acd\u0ab0', '\u0ab6\u0aa8\u0abf'],
+  NARROWWEEKDAYS: ['\u0ab0', '\u0ab8\u0acb', '\u0aae\u0a82', '\u0aac\u0ac1', '\u0a97\u0ac1', '\u0ab6\u0ac1', '\u0ab6'],
+  SHORTQUARTERS: ['\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0ae7', '\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0ae8', '\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0ae9', '\u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95 \u0aea'],
+  QUARTERS: ['\u0aaa\u0ab9\u0ab2\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95', '\u0aac\u0ac0\u0a9c\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95', '\u0aa4\u0acd\u0ab0\u0ac0\u0a9c\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95', '\u0a9a\u0acc\u0aa5\u0ac0 \u0aa4\u0acd\u0ab0\u0abf\u0aae\u0abe\u0ab8\u0abf\u0a95'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd-MM-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gv.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gv.js
new file mode 100644
index 0000000..b647393
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gv.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['RC', 'AD'],
+  ERANAMES: ['RC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Jerrey-geuree', 'Toshiaght-arree', 'Mayrnt', 'Averil', 'Boaldyn', 'Mean-souree', 'Jerrey-souree', 'Luanistyn', 'Mean-fouyir', 'Jerrey-fouyir', 'Mee Houney', 'Mee ny Nollick'],
+  SHORTMONTHS: ['J-guer', 'T-arree', 'Mayrnt', 'Avrril', 'Boaldyn', 'M-souree', 'J-souree', 'Luanistyn', 'M-fouyir', 'J-fouyir', 'M.Houney', 'M.Nollick'],
+  WEEKDAYS: ['Jedoonee', 'Jelhein', 'Jemayrt', 'Jercean', 'Jerdein', 'Jeheiney', 'Jesarn'],
+  SHORTWEEKDAYS: ['Jed', 'Jel', 'Jem', 'Jerc', 'Jerd', 'Jeh', 'Jes'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'MMM dd, y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gv_GB.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gv_GB.js
new file mode 100644
index 0000000..b647393
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__gv_GB.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['RC', 'AD'],
+  ERANAMES: ['RC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Jerrey-geuree', 'Toshiaght-arree', 'Mayrnt', 'Averil', 'Boaldyn', 'Mean-souree', 'Jerrey-souree', 'Luanistyn', 'Mean-fouyir', 'Jerrey-fouyir', 'Mee Houney', 'Mee ny Nollick'],
+  SHORTMONTHS: ['J-guer', 'T-arree', 'Mayrnt', 'Avrril', 'Boaldyn', 'M-souree', 'J-souree', 'Luanistyn', 'M-fouyir', 'J-fouyir', 'M.Houney', 'M.Nollick'],
+  WEEKDAYS: ['Jedoonee', 'Jelhein', 'Jemayrt', 'Jercean', 'Jerdein', 'Jeheiney', 'Jesarn'],
+  SHORTWEEKDAYS: ['Jed', 'Jel', 'Jem', 'Jerc', 'Jerd', 'Jeh', 'Jes'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'MMM dd, y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab.js
new file mode 100644
index 0000000..22461bf
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  ERANAMES: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['\u062c\u064e\u0646\u064e\u064a\u0652\u0631\u064f', '\u06a2\u064e\u0628\u0652\u0631\u064e\u064a\u0652\u0631\u064f', '\u0645\u064e\u0631\u0650\u0633\u0652', '\u0623\u064e\u06a2\u0652\u0631\u0650\u0644\u064f', '\u0645\u064e\u064a\u064f', '\u064a\u064f\u0648\u0646\u0650', '\u064a\u064f\u0648\u0644\u0650', '\u0623\u064e\u063a\u064f\u0633\u0652\u062a\u064e', '\u0633\u064e\u062a\u064f\u0645\u0652\u0628\u064e', '\u0623\u064f\u0643\u0652\u062a\u0648\u064f\u0628\u064e', '\u0646\u064f\u0648\u064e\u0645\u0652\u0628\u064e', '\u062f\u0650\u0633\u064e\u0645\u0652\u0628\u064e'],
+  SHORTMONTHS: ['\u062c\u064e\u0646', '\u06a2\u064e\u0628', '\u0645\u064e\u0631', '\u0623\u064e\u06a2\u0652\u0631', '\u0645\u064e\u064a', '\u064a\u064f\u0648\u0646', '\u064a\u064f\u0648\u0644', '\u0623\u064e\u063a\u064f', '\u0633\u064e\u062a', '\u0623\u064f\u0643\u0652\u062a', '\u0646\u064f\u0648', '\u062f\u0650\u0633'],
+  WEEKDAYS: ['\u0644\u064e\u062d\u064e\u062f\u0650', '\u0644\u0650\u062a\u0650\u0646\u0650\u0646\u0652', '\u062a\u064e\u0644\u064e\u062a\u064e', '\u0644\u064e\u0631\u064e\u0628\u064e', '\u0623\u064e\u0644\u0652\u062d\u064e\u0645\u0650\u0633\u0652', '\u062c\u064f\u0645\u064e\u0639\u064e', '\u0623\u064e\u0633\u064e\u0628\u064e\u0631\u0652'],
+  SHORTWEEKDAYS: ['\u0644\u064e\u062d', '\u0644\u0650\u062a', '\u062a\u064e\u0644', '\u0644\u064e\u0631', '\u0623\u064e\u0644\u0652\u062d', '\u062c\u064f\u0645', '\u0623\u064e\u0633\u064e'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab_NG.js
new file mode 100644
index 0000000..22461bf
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  ERANAMES: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['\u062c\u064e\u0646\u064e\u064a\u0652\u0631\u064f', '\u06a2\u064e\u0628\u0652\u0631\u064e\u064a\u0652\u0631\u064f', '\u0645\u064e\u0631\u0650\u0633\u0652', '\u0623\u064e\u06a2\u0652\u0631\u0650\u0644\u064f', '\u0645\u064e\u064a\u064f', '\u064a\u064f\u0648\u0646\u0650', '\u064a\u064f\u0648\u0644\u0650', '\u0623\u064e\u063a\u064f\u0633\u0652\u062a\u064e', '\u0633\u064e\u062a\u064f\u0645\u0652\u0628\u064e', '\u0623\u064f\u0643\u0652\u062a\u0648\u064f\u0628\u064e', '\u0646\u064f\u0648\u064e\u0645\u0652\u0628\u064e', '\u062f\u0650\u0633\u064e\u0645\u0652\u0628\u064e'],
+  SHORTMONTHS: ['\u062c\u064e\u0646', '\u06a2\u064e\u0628', '\u0645\u064e\u0631', '\u0623\u064e\u06a2\u0652\u0631', '\u0645\u064e\u064a', '\u064a\u064f\u0648\u0646', '\u064a\u064f\u0648\u0644', '\u0623\u064e\u063a\u064f', '\u0633\u064e\u062a', '\u0623\u064f\u0643\u0652\u062a', '\u0646\u064f\u0648', '\u062f\u0650\u0633'],
+  WEEKDAYS: ['\u0644\u064e\u062d\u064e\u062f\u0650', '\u0644\u0650\u062a\u0650\u0646\u0650\u0646\u0652', '\u062a\u064e\u0644\u064e\u062a\u064e', '\u0644\u064e\u0631\u064e\u0628\u064e', '\u0623\u064e\u0644\u0652\u062d\u064e\u0645\u0650\u0633\u0652', '\u062c\u064f\u0645\u064e\u0639\u064e', '\u0623\u064e\u0633\u064e\u0628\u064e\u0631\u0652'],
+  SHORTWEEKDAYS: ['\u0644\u064e\u062d', '\u0644\u0650\u062a', '\u062a\u064e\u0644', '\u0644\u064e\u0631', '\u0623\u064e\u0644\u0652\u062d', '\u062c\u064f\u0645', '\u0623\u064e\u0633\u064e'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab_SD.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab_SD.js
new file mode 100644
index 0000000..884df5e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Arab_SD.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  ERANAMES: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['\u062c\u064e\u0646\u064e\u064a\u0652\u0631\u064f', '\u06a2\u064e\u0628\u0652\u0631\u064e\u064a\u0652\u0631\u064f', '\u0645\u064e\u0631\u0650\u0633\u0652', '\u0623\u064e\u06a2\u0652\u0631\u0650\u0644\u064f', '\u0645\u064e\u064a\u064f', '\u064a\u064f\u0648\u0646\u0650', '\u064a\u064f\u0648\u0644\u0650', '\u0623\u064e\u063a\u064f\u0633\u0652\u062a\u064e', '\u0633\u064e\u062a\u064f\u0645\u0652\u0628\u064e', '\u0623\u064f\u0643\u0652\u062a\u0648\u064f\u0628\u064e', '\u0646\u064f\u0648\u064e\u0645\u0652\u0628\u064e', '\u062f\u0650\u0633\u064e\u0645\u0652\u0628\u064e'],
+  SHORTMONTHS: ['\u062c\u064e\u0646', '\u06a2\u064e\u0628', '\u0645\u064e\u0631', '\u0623\u064e\u06a2\u0652\u0631', '\u0645\u064e\u064a', '\u064a\u064f\u0648\u0646', '\u064a\u064f\u0648\u0644', '\u0623\u064e\u063a\u064f', '\u0633\u064e\u062a', '\u0623\u064f\u0643\u0652\u062a', '\u0646\u064f\u0648', '\u062f\u0650\u0633'],
+  WEEKDAYS: ['\u0644\u064e\u062d\u064e\u062f\u0650', '\u0644\u0650\u062a\u0650\u0646\u0650\u0646\u0652', '\u062a\u064e\u0644\u064e\u062a\u064e', '\u0644\u064e\u0631\u064e\u0628\u064e', '\u0623\u064e\u0644\u0652\u062d\u064e\u0645\u0650\u0633\u0652', '\u062c\u064f\u0645\u064e\u0639\u064e', '\u0623\u064e\u0633\u064e\u0628\u064e\u0631\u0652'],
+  SHORTWEEKDAYS: ['\u0644\u064e\u062d', '\u0644\u0650\u062a', '\u062a\u064e\u0644', '\u0644\u064e\u0631', '\u0623\u064e\u0644\u0652\u062d', '\u062c\u064f\u0645', '\u0623\u064e\u0633\u064e'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_GH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_GH.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_GH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_GH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_GH.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_GH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_NE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_NE.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_NE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_NG.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_Latn_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_NE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_NE.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_NE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_NG.js
new file mode 100644
index 0000000..4bb951d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janairu', 'Fabrairu', 'Maris', 'Afrilu', 'Mayu', 'Yuni', 'Yuli', 'Augusta', 'Satumba', 'Oktoba', 'Nuwamba', 'Disamba'],
+  SHORTMONTHS: ['Jan', 'Fab', 'Mar', 'Afr', 'May', 'Yun', 'Yul', 'Aug', 'Sat', 'Okt', 'Nuw', 'Dis'],
+  WEEKDAYS: ['Lahadi', 'Litini', 'Talata', 'Laraba', 'Alhamis', "Jumma'a", 'Asabar'],
+  SHORTWEEKDAYS: ['Lah', 'Lit', 'Tal', 'Lar', 'Alh', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_SD.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_SD.js
new file mode 100644
index 0000000..884df5e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ha_SD.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  ERANAMES: ['\u063a\u064e\u0628\u064e\u0646\u0650\u0646\u0652 \u0645\u0650\u0644\u064e\u062f\u0650', '\u0645\u0650\u0644\u064e\u062f\u0650'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'Y', 'Y', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['\u062c\u064e\u0646\u064e\u064a\u0652\u0631\u064f', '\u06a2\u064e\u0628\u0652\u0631\u064e\u064a\u0652\u0631\u064f', '\u0645\u064e\u0631\u0650\u0633\u0652', '\u0623\u064e\u06a2\u0652\u0631\u0650\u0644\u064f', '\u0645\u064e\u064a\u064f', '\u064a\u064f\u0648\u0646\u0650', '\u064a\u064f\u0648\u0644\u0650', '\u0623\u064e\u063a\u064f\u0633\u0652\u062a\u064e', '\u0633\u064e\u062a\u064f\u0645\u0652\u0628\u064e', '\u0623\u064f\u0643\u0652\u062a\u0648\u064f\u0628\u064e', '\u0646\u064f\u0648\u064e\u0645\u0652\u0628\u064e', '\u062f\u0650\u0633\u064e\u0645\u0652\u0628\u064e'],
+  SHORTMONTHS: ['\u062c\u064e\u0646', '\u06a2\u064e\u0628', '\u0645\u064e\u0631', '\u0623\u064e\u06a2\u0652\u0631', '\u0645\u064e\u064a', '\u064a\u064f\u0648\u0646', '\u064a\u064f\u0648\u0644', '\u0623\u064e\u063a\u064f', '\u0633\u064e\u062a', '\u0623\u064f\u0643\u0652\u062a', '\u0646\u064f\u0648', '\u062f\u0650\u0633'],
+  WEEKDAYS: ['\u0644\u064e\u062d\u064e\u062f\u0650', '\u0644\u0650\u062a\u0650\u0646\u0650\u0646\u0652', '\u062a\u064e\u0644\u064e\u062a\u064e', '\u0644\u064e\u0631\u064e\u0628\u064e', '\u0623\u064e\u0644\u0652\u062d\u064e\u0645\u0650\u0633\u0652', '\u062c\u064f\u0645\u064e\u0639\u064e', '\u0623\u064e\u0633\u064e\u0628\u064e\u0631\u0652'],
+  SHORTWEEKDAYS: ['\u0644\u064e\u062d', '\u0644\u0650\u062a', '\u062a\u064e\u0644', '\u0644\u064e\u0631', '\u0623\u064e\u0644\u0652\u062d', '\u062c\u064f\u0645', '\u0623\u064e\u0633\u064e'],
+  NARROWWEEKDAYS: ['L', 'L', 'T', 'L', 'A', 'J', 'A'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd/M/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__haw.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__haw.js
new file mode 100644
index 0000000..c175798
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__haw.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Ianuali', 'Pepeluali', 'Malaki', '\u02bbApelila', 'Mei', 'Iune', 'Iulai', '\u02bbAukake', 'Kepakemapa', '\u02bbOkakopa', 'Nowemapa', 'Kekemapa'],
+  SHORTMONTHS: ['Ian.', 'Pep.', 'Mal.', '\u02bbAp.', 'Mei', 'Iun.', 'Iul.', '\u02bbAu.', 'Kep.', '\u02bbOk.', 'Now.', 'Kek.'],
+  WEEKDAYS: ['L\u0101pule', 'Po\u02bbakahi', 'Po\u02bbalua', 'Po\u02bbakolu', 'Po\u02bbah\u0101', 'Po\u02bbalima', 'Po\u02bbaono'],
+  SHORTWEEKDAYS: ['LP', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd MMM y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__haw_US.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__haw_US.js
new file mode 100644
index 0000000..9a35167
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__haw_US.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Ianuali', 'Pepeluali', 'Malaki', '\u02bbApelila', 'Mei', 'Iune', 'Iulai', '\u02bbAukake', 'Kepakemapa', '\u02bbOkakopa', 'Nowemapa', 'Kekemapa'],
+  SHORTMONTHS: ['Ian.', 'Pep.', 'Mal.', '\u02bbAp.', 'Mei', 'Iun.', 'Iul.', '\u02bbAu.', 'Kep.', '\u02bbOk.', 'Now.', 'Kek.'],
+  WEEKDAYS: ['L\u0101pule', 'Po\u02bbakahi', 'Po\u02bbalua', 'Po\u02bbakolu', 'Po\u02bbah\u0101', 'Po\u02bbalima', 'Po\u02bbaono'],
+  SHORTWEEKDAYS: ['LP', 'P1', 'P2', 'P3', 'P4', 'P5', 'P6'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd MMM y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__he.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__he.js
new file mode 100644
index 0000000..23c99bd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__he.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u05dc\u05e4\u05e0\u05d4\u05f4\u05e1', '\u05dc\u05e1\u05d4\u05f4\u05e0'],
+  ERANAMES: ['\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e1\u05e4\u05d9\u05e8\u05d4', '\u05dc\u05e1\u05e4\u05d9\u05e8\u05d4'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u05d9\u05e0\u05d5\u05d0\u05e8', '\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8', '\u05de\u05e8\u05e1', '\u05d0\u05e4\u05e8\u05d9\u05dc', '\u05de\u05d0\u05d9', '\u05d9\u05d5\u05e0\u05d9', '\u05d9\u05d5\u05dc\u05d9', '\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8', '\u05e1\u05e4\u05d8\u05de\u05d1\u05e8', '\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8', '\u05e0\u05d5\u05d1\u05de\u05d1\u05e8', '\u05d3\u05e6\u05de\u05d1\u05e8'],
+  SHORTMONTHS: ['\u05d9\u05e0\u05d5', '\u05e4\u05d1\u05e8', '\u05de\u05e8\u05e1', '\u05d0\u05e4\u05e8', '\u05de\u05d0\u05d9', '\u05d9\u05d5\u05e0', '\u05d9\u05d5\u05dc', '\u05d0\u05d5\u05d2', '\u05e1\u05e4\u05d8', '\u05d0\u05d5\u05e7', '\u05e0\u05d5\u05d1', '\u05d3\u05e6\u05de'],
+  WEEKDAYS: ['\u05d9\u05d5\u05dd \u05e8\u05d0\u05e9\u05d5\u05df', '\u05d9\u05d5\u05dd \u05e9\u05e0\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05dc\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e8\u05d1\u05d9\u05e2\u05d9', '\u05d9\u05d5\u05dd \u05d7\u05de\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05d1\u05ea'],
+  SHORTWEEKDAYS: ["\u05d9\u05d5\u05dd \u05d0'", "\u05d9\u05d5\u05dd \u05d1'", "\u05d9\u05d5\u05dd \u05d2'", "\u05d9\u05d5\u05dd \u05d3'", "\u05d9\u05d5\u05dd \u05d4'", "\u05d9\u05d5\u05dd \u05d5'", '\u05e9\u05d1\u05ea'],
+  NARROWWEEKDAYS: ['\u05d0', '\u05d1', '\u05d2', '\u05d3', '\u05d4', '\u05d5', '\u05e9'],
+  SHORTQUARTERS: ['\u05e8\u05d1\u05e2\u05d5\u05df 1', '\u05e8\u05d1\u05e2\u05d5\u05df 2', '\u05e8\u05d1\u05e2\u05d5\u05df 3', '\u05e8\u05d1\u05e2\u05d5\u05df 4'],
+  QUARTERS: ['\u05e8\u05d1\u05e2\u05d5\u05df 1', '\u05e8\u05d1\u05e2\u05d5\u05df 2', '\u05e8\u05d1\u05e2\u05d5\u05df 3', '\u05e8\u05d1\u05e2\u05d5\u05df 4'],
+  AMPMS: ['\u05dc\u05e4\u05e0\u05d4\"\u05e6', '\u05d0\u05d7\u05d4\"\u05e6'],
+  DATEFORMATS: ['EEEE, d \u05d1MMMM y', 'd \u05d1MMMM y', 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__he_IL.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__he_IL.js
new file mode 100644
index 0000000..23c99bd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__he_IL.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u05dc\u05e4\u05e0\u05d4\u05f4\u05e1', '\u05dc\u05e1\u05d4\u05f4\u05e0'],
+  ERANAMES: ['\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e1\u05e4\u05d9\u05e8\u05d4', '\u05dc\u05e1\u05e4\u05d9\u05e8\u05d4'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u05d9\u05e0\u05d5\u05d0\u05e8', '\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8', '\u05de\u05e8\u05e1', '\u05d0\u05e4\u05e8\u05d9\u05dc', '\u05de\u05d0\u05d9', '\u05d9\u05d5\u05e0\u05d9', '\u05d9\u05d5\u05dc\u05d9', '\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8', '\u05e1\u05e4\u05d8\u05de\u05d1\u05e8', '\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8', '\u05e0\u05d5\u05d1\u05de\u05d1\u05e8', '\u05d3\u05e6\u05de\u05d1\u05e8'],
+  SHORTMONTHS: ['\u05d9\u05e0\u05d5', '\u05e4\u05d1\u05e8', '\u05de\u05e8\u05e1', '\u05d0\u05e4\u05e8', '\u05de\u05d0\u05d9', '\u05d9\u05d5\u05e0', '\u05d9\u05d5\u05dc', '\u05d0\u05d5\u05d2', '\u05e1\u05e4\u05d8', '\u05d0\u05d5\u05e7', '\u05e0\u05d5\u05d1', '\u05d3\u05e6\u05de'],
+  WEEKDAYS: ['\u05d9\u05d5\u05dd \u05e8\u05d0\u05e9\u05d5\u05df', '\u05d9\u05d5\u05dd \u05e9\u05e0\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05dc\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e8\u05d1\u05d9\u05e2\u05d9', '\u05d9\u05d5\u05dd \u05d7\u05de\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05d1\u05ea'],
+  SHORTWEEKDAYS: ["\u05d9\u05d5\u05dd \u05d0'", "\u05d9\u05d5\u05dd \u05d1'", "\u05d9\u05d5\u05dd \u05d2'", "\u05d9\u05d5\u05dd \u05d3'", "\u05d9\u05d5\u05dd \u05d4'", "\u05d9\u05d5\u05dd \u05d5'", '\u05e9\u05d1\u05ea'],
+  NARROWWEEKDAYS: ['\u05d0', '\u05d1', '\u05d2', '\u05d3', '\u05d4', '\u05d5', '\u05e9'],
+  SHORTQUARTERS: ['\u05e8\u05d1\u05e2\u05d5\u05df 1', '\u05e8\u05d1\u05e2\u05d5\u05df 2', '\u05e8\u05d1\u05e2\u05d5\u05df 3', '\u05e8\u05d1\u05e2\u05d5\u05df 4'],
+  QUARTERS: ['\u05e8\u05d1\u05e2\u05d5\u05df 1', '\u05e8\u05d1\u05e2\u05d5\u05df 2', '\u05e8\u05d1\u05e2\u05d5\u05df 3', '\u05e8\u05d1\u05e2\u05d5\u05df 4'],
+  AMPMS: ['\u05dc\u05e4\u05e0\u05d4\"\u05e6', '\u05d0\u05d7\u05d4\"\u05e6'],
+  DATEFORMATS: ['EEEE, d \u05d1MMMM y', 'd \u05d1MMMM y', 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hi.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hi.js
new file mode 100644
index 0000000..6272f8f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hi.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0908\u0938\u093e\u092a\u0942\u0930\u094d\u0935', '\u0938\u0928'],
+  ERANAMES: ['\u0908\u0938\u093e\u092a\u0942\u0930\u094d\u0935', '\u0938\u0928'],
+  NARROWMONTHS: ['\u091c', '\u092b\u093c', '\u092e\u093e', '\u0905', '\u092e', '\u091c\u0942', '\u091c\u0941', '\u0905', '\u0938\u093f', '\u0905', '\u0928', '\u0926\u093f'],
+  MONTHS: ['\u091c\u0928\u0935\u0930\u0940', '\u092b\u0930\u0935\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u0948\u0932', '\u092e\u0908', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u093e\u0908', '\u0905\u0917\u0938\u094d\u0924', '\u0938\u093f\u0924\u092e\u094d\u092c\u0930', '\u0905\u0915\u094d\u0924\u0942\u092c\u0930', '\u0928\u0935\u092e\u094d\u092c\u0930', '\u0926\u093f\u0938\u092e\u094d\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u0928\u0935\u0930\u0940', '\u092b\u0930\u0935\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u0948\u0932', '\u092e\u0908', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u093e\u0908', '\u0905\u0917\u0938\u094d\u0924', '\u0938\u093f\u0924\u092e\u094d\u092c\u0930', '\u0905\u0915\u094d\u0924\u0942\u092c\u0930', '\u0928\u0935\u092e\u094d\u092c\u0930', '\u0926\u093f\u0938\u092e\u094d\u092c\u0930'],
+  WEEKDAYS: ['\u0930\u0935\u093f\u0935\u093e\u0930', '\u0938\u094b\u092e\u0935\u093e\u0930', '\u092e\u0902\u0917\u0932\u0935\u093e\u0930', '\u092c\u0941\u0927\u0935\u093e\u0930', '\u0917\u0941\u0930\u0941\u0935\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930', '\u0936\u0928\u093f\u0935\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0930\u0935\u093f', '\u0938\u094b\u092e', '\u092e\u0902\u0917\u0932', '\u092c\u0941\u0927', '\u0917\u0941\u0930\u0941', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['\u0930', '\u0938\u094b', '\u092e\u0902', '\u092c\u0941', '\u0917\u0941', '\u0936\u0941', '\u0936'],
+  SHORTQUARTERS: ['\u092a\u094d\u0930\u0925\u092e \u091a\u094c\u0925\u093e\u0908', '\u0926\u094d\u0935\u093f\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u0924\u0943\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u091a\u0924\u0941\u0930\u094d\u0925 \u091a\u094c\u0925\u093e\u0908'],
+  QUARTERS: ['\u092a\u094d\u0930\u0925\u092e \u091a\u094c\u0925\u093e\u0908', '\u0926\u094d\u0935\u093f\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u0924\u0943\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u091a\u0924\u0941\u0930\u094d\u0925 \u091a\u094c\u0925\u093e\u0908'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hi_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hi_IN.js
new file mode 100644
index 0000000..6272f8f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hi_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0908\u0938\u093e\u092a\u0942\u0930\u094d\u0935', '\u0938\u0928'],
+  ERANAMES: ['\u0908\u0938\u093e\u092a\u0942\u0930\u094d\u0935', '\u0938\u0928'],
+  NARROWMONTHS: ['\u091c', '\u092b\u093c', '\u092e\u093e', '\u0905', '\u092e', '\u091c\u0942', '\u091c\u0941', '\u0905', '\u0938\u093f', '\u0905', '\u0928', '\u0926\u093f'],
+  MONTHS: ['\u091c\u0928\u0935\u0930\u0940', '\u092b\u0930\u0935\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u0948\u0932', '\u092e\u0908', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u093e\u0908', '\u0905\u0917\u0938\u094d\u0924', '\u0938\u093f\u0924\u092e\u094d\u092c\u0930', '\u0905\u0915\u094d\u0924\u0942\u092c\u0930', '\u0928\u0935\u092e\u094d\u092c\u0930', '\u0926\u093f\u0938\u092e\u094d\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u0928\u0935\u0930\u0940', '\u092b\u0930\u0935\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u0948\u0932', '\u092e\u0908', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u093e\u0908', '\u0905\u0917\u0938\u094d\u0924', '\u0938\u093f\u0924\u092e\u094d\u092c\u0930', '\u0905\u0915\u094d\u0924\u0942\u092c\u0930', '\u0928\u0935\u092e\u094d\u092c\u0930', '\u0926\u093f\u0938\u092e\u094d\u092c\u0930'],
+  WEEKDAYS: ['\u0930\u0935\u093f\u0935\u093e\u0930', '\u0938\u094b\u092e\u0935\u093e\u0930', '\u092e\u0902\u0917\u0932\u0935\u093e\u0930', '\u092c\u0941\u0927\u0935\u093e\u0930', '\u0917\u0941\u0930\u0941\u0935\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930', '\u0936\u0928\u093f\u0935\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0930\u0935\u093f', '\u0938\u094b\u092e', '\u092e\u0902\u0917\u0932', '\u092c\u0941\u0927', '\u0917\u0941\u0930\u0941', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['\u0930', '\u0938\u094b', '\u092e\u0902', '\u092c\u0941', '\u0917\u0941', '\u0936\u0941', '\u0936'],
+  SHORTQUARTERS: ['\u092a\u094d\u0930\u0925\u092e \u091a\u094c\u0925\u093e\u0908', '\u0926\u094d\u0935\u093f\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u0924\u0943\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u091a\u0924\u0941\u0930\u094d\u0925 \u091a\u094c\u0925\u093e\u0908'],
+  QUARTERS: ['\u092a\u094d\u0930\u0925\u092e \u091a\u094c\u0925\u093e\u0908', '\u0926\u094d\u0935\u093f\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u0924\u0943\u0924\u0940\u092f \u091a\u094c\u0925\u093e\u0908', '\u091a\u0924\u0941\u0930\u094d\u0925 \u091a\u094c\u0925\u093e\u0908'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hr.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hr.js
new file mode 100644
index 0000000..1d9867f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hr.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pr.n.e.', 'AD'],
+  ERANAMES: ['Prije Krista', 'Poslije Krista'],
+  NARROWMONTHS: ['1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.', '10.', '11.', '12.'],
+  MONTHS: ['sije\u010dnja', 'velja\u010de', 'o\u017eujka', 'travnja', 'svibnja', 'lipnja', 'srpnja', 'kolovoza', 'rujna', 'listopada', 'studenoga', 'prosinca'],
+  STANDALONEMONTHS: ['sije\u010danj', 'velja\u010da', 'o\u017eujak', 'travanj', 'svibanj', 'lipanj', 'srpanj', 'kolovoz', 'rujan', 'listopad', 'studeni', 'prosinac'],
+  SHORTMONTHS: ['01.', '02.', '03.', '04.', '05.', '06.', '07.', '08.', '09.', '10.', '11.', '12.'],
+  WEEKDAYS: ['nedjelja', 'ponedjeljak', 'utorak', 'srijeda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sri', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['1kv', '2kv', '3kv', '4kv'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d. MMMM y.', 'd. MMMM y.', 'd.M.yyyy.', 'dd.MM.yyyy.'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hr_HR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hr_HR.js
new file mode 100644
index 0000000..1d9867f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hr_HR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pr.n.e.', 'AD'],
+  ERANAMES: ['Prije Krista', 'Poslije Krista'],
+  NARROWMONTHS: ['1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.', '10.', '11.', '12.'],
+  MONTHS: ['sije\u010dnja', 'velja\u010de', 'o\u017eujka', 'travnja', 'svibnja', 'lipnja', 'srpnja', 'kolovoza', 'rujna', 'listopada', 'studenoga', 'prosinca'],
+  STANDALONEMONTHS: ['sije\u010danj', 'velja\u010da', 'o\u017eujak', 'travanj', 'svibanj', 'lipanj', 'srpanj', 'kolovoz', 'rujan', 'listopad', 'studeni', 'prosinac'],
+  SHORTMONTHS: ['01.', '02.', '03.', '04.', '05.', '06.', '07.', '08.', '09.', '10.', '11.', '12.'],
+  WEEKDAYS: ['nedjelja', 'ponedjeljak', 'utorak', 'srijeda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sri', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['1kv', '2kv', '3kv', '4kv'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d. MMMM y.', 'd. MMMM y.', 'd.M.yyyy.', 'dd.MM.yyyy.'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hu.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hu.js
new file mode 100644
index 0000000..f1d430e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hu.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['i. e.', 'i. sz.'],
+  ERANAMES: ['id\u0151sz\u00e1m\u00edt\u00e1sunk el\u0151tt', 'id\u0151sz\u00e1m\u00edt\u00e1sunk szerint'],
+  NARROWMONTHS: ['J', 'F', 'M', '\u00c1', 'M', 'J', 'J', 'A', 'Sz', 'O', 'N', 'D'],
+  MONTHS: ['janu\u00e1r', 'febru\u00e1r', 'm\u00e1rcius', '\u00e1prilis', 'm\u00e1jus', 'j\u00fanius', 'j\u00falius', 'augusztus', 'szeptember', 'okt\u00f3ber', 'november', 'december'],
+  SHORTMONTHS: ['jan.', 'febr.', 'm\u00e1rc.', '\u00e1pr.', 'm\u00e1j.', 'j\u00fan.', 'j\u00fal.', 'aug.', 'szept.', 'okt.', 'nov.', 'dec.'],
+  WEEKDAYS: ['vas\u00e1rnap', 'h\u00e9tf\u0151', 'kedd', 'szerda', 'cs\u00fct\u00f6rt\u00f6k', 'p\u00e9ntek', 'szombat'],
+  SHORTWEEKDAYS: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'],
+  NARROWWEEKDAYS: ['V', 'H', 'K', 'Sz', 'Cs', 'P', 'Sz'],
+  SHORTQUARTERS: ['N1', 'N2', 'N3', 'N4'],
+  QUARTERS: ['I. negyed\u00e9v', 'II. negyed\u00e9v', 'III. negyed\u00e9v', 'IV. negyed\u00e9v'],
+  AMPMS: ['de.', 'du.'],
+  DATEFORMATS: ['y. MMMM d., EEEE', 'y. MMMM d.', 'yyyy.MM.dd.', 'yyyy.MM.dd.'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hu_HU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hu_HU.js
new file mode 100644
index 0000000..f1d430e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hu_HU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['i. e.', 'i. sz.'],
+  ERANAMES: ['id\u0151sz\u00e1m\u00edt\u00e1sunk el\u0151tt', 'id\u0151sz\u00e1m\u00edt\u00e1sunk szerint'],
+  NARROWMONTHS: ['J', 'F', 'M', '\u00c1', 'M', 'J', 'J', 'A', 'Sz', 'O', 'N', 'D'],
+  MONTHS: ['janu\u00e1r', 'febru\u00e1r', 'm\u00e1rcius', '\u00e1prilis', 'm\u00e1jus', 'j\u00fanius', 'j\u00falius', 'augusztus', 'szeptember', 'okt\u00f3ber', 'november', 'december'],
+  SHORTMONTHS: ['jan.', 'febr.', 'm\u00e1rc.', '\u00e1pr.', 'm\u00e1j.', 'j\u00fan.', 'j\u00fal.', 'aug.', 'szept.', 'okt.', 'nov.', 'dec.'],
+  WEEKDAYS: ['vas\u00e1rnap', 'h\u00e9tf\u0151', 'kedd', 'szerda', 'cs\u00fct\u00f6rt\u00f6k', 'p\u00e9ntek', 'szombat'],
+  SHORTWEEKDAYS: ['V', 'H', 'K', 'Sze', 'Cs', 'P', 'Szo'],
+  NARROWWEEKDAYS: ['V', 'H', 'K', 'Sz', 'Cs', 'P', 'Sz'],
+  SHORTQUARTERS: ['N1', 'N2', 'N3', 'N4'],
+  QUARTERS: ['I. negyed\u00e9v', 'II. negyed\u00e9v', 'III. negyed\u00e9v', 'IV. negyed\u00e9v'],
+  AMPMS: ['de.', 'du.'],
+  DATEFORMATS: ['y. MMMM d., EEEE', 'y. MMMM d.', 'yyyy.MM.dd.', 'yyyy.MM.dd.'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hy.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hy.js
new file mode 100644
index 0000000..f63766a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hy.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0554\u2024\u0531\u2024', '\u0554\u2024\u0535\u2024'],
+  ERANAMES: ['\u0554\u2024\u0531\u2024', '\u0554\u2024\u0535\u2024'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0545\u0578\u0582\u0576\u0578\u0582\u0561\u0580', '\u0553\u0565\u057f\u0580\u0578\u0582\u0561\u0580', '\u0544\u0561\u0580\u057f', '\u0531\u057a\u0580\u056b\u056c', '\u0544\u0561\u0575\u056b\u057d', '\u0545\u0578\u0582\u0576\u056b\u057d', '\u0545\u0578\u0582\u056c\u056b\u057d', '\u0555\u0563\u0578\u057d\u057f\u0578\u057d', '\u054d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580', '\u0540\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580', '\u0546\u0578\u0575\u0565\u0574\u0562\u0565\u0580', '\u0534\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580'],
+  SHORTMONTHS: ['\u0545\u0576\u0580', '\u0553\u057f\u0580', '\u0544\u0580\u057f', '\u0531\u057a\u0580', '\u0544\u0575\u057d', '\u0545\u0576\u057d', '\u0545\u056c\u057d', '\u0555\u0563\u057d', '\u054d\u0565\u057a', '\u0540\u0578\u056f', '\u0546\u0578\u0575', '\u0534\u0565\u056f'],
+  WEEKDAYS: ['\u053f\u056b\u0580\u0561\u056f\u056b', '\u0535\u0580\u056f\u0578\u0582\u0577\u0561\u0562\u0569\u056b', '\u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b', '\u0549\u0578\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b', '\u0540\u056b\u0576\u0563\u0577\u0561\u0562\u0569\u056b', '\u0548\u0582\u0580\u0562\u0561\u0569', '\u0547\u0561\u0562\u0561\u0569'],
+  SHORTWEEKDAYS: ['\u053f\u056b\u0580', '\u0535\u0580\u056f', '\u0535\u0580\u0584', '\u0549\u0578\u0580', '\u0540\u0576\u0563', '\u0548\u0582\u0580', '\u0547\u0561\u0562'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u0531\u057c\u2024', '\u0535\u0580\u2024'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM dd, y', 'MMM d, y', 'MM/dd/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hy_AM.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hy_AM.js
new file mode 100644
index 0000000..f63766a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__hy_AM.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0554\u2024\u0531\u2024', '\u0554\u2024\u0535\u2024'],
+  ERANAMES: ['\u0554\u2024\u0531\u2024', '\u0554\u2024\u0535\u2024'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0545\u0578\u0582\u0576\u0578\u0582\u0561\u0580', '\u0553\u0565\u057f\u0580\u0578\u0582\u0561\u0580', '\u0544\u0561\u0580\u057f', '\u0531\u057a\u0580\u056b\u056c', '\u0544\u0561\u0575\u056b\u057d', '\u0545\u0578\u0582\u0576\u056b\u057d', '\u0545\u0578\u0582\u056c\u056b\u057d', '\u0555\u0563\u0578\u057d\u057f\u0578\u057d', '\u054d\u0565\u057a\u057f\u0565\u0574\u0562\u0565\u0580', '\u0540\u0578\u056f\u057f\u0565\u0574\u0562\u0565\u0580', '\u0546\u0578\u0575\u0565\u0574\u0562\u0565\u0580', '\u0534\u0565\u056f\u057f\u0565\u0574\u0562\u0565\u0580'],
+  SHORTMONTHS: ['\u0545\u0576\u0580', '\u0553\u057f\u0580', '\u0544\u0580\u057f', '\u0531\u057a\u0580', '\u0544\u0575\u057d', '\u0545\u0576\u057d', '\u0545\u056c\u057d', '\u0555\u0563\u057d', '\u054d\u0565\u057a', '\u0540\u0578\u056f', '\u0546\u0578\u0575', '\u0534\u0565\u056f'],
+  WEEKDAYS: ['\u053f\u056b\u0580\u0561\u056f\u056b', '\u0535\u0580\u056f\u0578\u0582\u0577\u0561\u0562\u0569\u056b', '\u0535\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b', '\u0549\u0578\u0580\u0565\u0584\u0577\u0561\u0562\u0569\u056b', '\u0540\u056b\u0576\u0563\u0577\u0561\u0562\u0569\u056b', '\u0548\u0582\u0580\u0562\u0561\u0569', '\u0547\u0561\u0562\u0561\u0569'],
+  SHORTWEEKDAYS: ['\u053f\u056b\u0580', '\u0535\u0580\u056f', '\u0535\u0580\u0584', '\u0549\u0578\u0580', '\u0540\u0576\u0563', '\u0548\u0582\u0580', '\u0547\u0561\u0562'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u0531\u057c\u2024', '\u0535\u0580\u2024'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'MMMM dd, y', 'MMM d, y', 'MM/dd/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ia.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ia.js
new file mode 100644
index 0000000..235c88e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ia.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.Chr.', 'p.Chr.'],
+  ERANAMES: ['ante Christo', 'post Christo'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['januario', 'februario', 'martio', 'april', 'maio', 'junio', 'julio', 'augusto', 'septembre', 'octobre', 'novembre', 'decembre'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'],
+  WEEKDAYS: ['dominica', 'lunedi', 'martedi', 'mercuridi', 'jovedi', 'venerdi', 'sabbato'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mer', 'jov', 'ven', 'sab'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1-me trimestre', '2-nde trimestre', '3-tie trimestre', '4-te trimestre'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__id.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__id.js
new file mode 100644
index 0000000..257a145
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__id.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'],
+  SHORTWEEKDAYS: ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['kuartal pertama', 'kuartal kedua', 'kuartal ketiga', 'kuartal keempat'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, dd MMMM yyyy', 'd MMMM yyyy', 'd MMM yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__id_ID.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__id_ID.js
new file mode 100644
index 0000000..257a145
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__id_ID.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'],
+  SHORTWEEKDAYS: ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['kuartal pertama', 'kuartal kedua', 'kuartal ketiga', 'kuartal keempat'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, dd MMMM yyyy', 'd MMMM yyyy', 'd MMM yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ig.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ig.js
new file mode 100644
index 0000000..f8eb7c1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ig.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['T.K.', 'A.K.'],
+  ERANAMES: ['Tupu Kristi', 'Af\u1ecd Kristi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Jen\u1ee5war\u1ecb', 'Febr\u1ee5war\u1ecb', 'Maach\u1ecb', 'Eprel', 'Mee', 'Juun', 'Jula\u1ecb', '\u1eccg\u1ecd\u1ecdst', 'Septemba', '\u1eccktoba', 'Novemba', 'Disemba'],
+  SHORTMONTHS: ['Jen', 'Feb', 'Maa', 'Epr', 'Mee', 'Juu', 'Jul', '\u1eccg\u1ecd', 'Sep', '\u1ecckt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Mb\u1ecds\u1ecb \u1ee4ka', 'M\u1ecdnde', 'Tiuzdee', 'Wenezdee', 'T\u1ecd\u1ecdzdee', 'Fra\u1ecbdee', 'Sat\u1ecddee'],
+  SHORTWEEKDAYS: ['\u1ee4ka', 'M\u1ecdn', 'Tiu', 'Wen', 'T\u1ecd\u1ecd', 'Fra\u1ecb', 'Sat'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ig_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ig_NG.js
new file mode 100644
index 0000000..f8eb7c1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ig_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['T.K.', 'A.K.'],
+  ERANAMES: ['Tupu Kristi', 'Af\u1ecd Kristi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Jen\u1ee5war\u1ecb', 'Febr\u1ee5war\u1ecb', 'Maach\u1ecb', 'Eprel', 'Mee', 'Juun', 'Jula\u1ecb', '\u1eccg\u1ecd\u1ecdst', 'Septemba', '\u1eccktoba', 'Novemba', 'Disemba'],
+  SHORTMONTHS: ['Jen', 'Feb', 'Maa', 'Epr', 'Mee', 'Juu', 'Jul', '\u1eccg\u1ecd', 'Sep', '\u1ecckt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Mb\u1ecds\u1ecb \u1ee4ka', 'M\u1ecdnde', 'Tiuzdee', 'Wenezdee', 'T\u1ecd\u1ecdzdee', 'Fra\u1ecbdee', 'Sat\u1ecddee'],
+  SHORTWEEKDAYS: ['\u1ee4ka', 'M\u1ecdn', 'Tiu', 'Wen', 'T\u1ecd\u1ecd', 'Fra\u1ecb', 'Sat'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ii.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ii.js
new file mode 100644
index 0000000..6673a2f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ii.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\ua0c5\ua2ca\ua0bf', '\ua0c5\ua2ca\ua282'],
+  ERANAMES: ['\ua0c5\ua2ca\ua0bf', '\ua0c5\ua2ca\ua282'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\ua2cd\ua1aa', '\ua44d\ua1aa', '\ua315\ua1aa', '\ua1d6\ua1aa', '\ua26c\ua1aa', '\ua0d8\ua1aa', '\ua3c3\ua1aa', '\ua246\ua1aa', '\ua22c\ua1aa', '\ua2b0\ua1aa', '\ua2b0\ua2aa\ua1aa', '\ua2b0\ua44b\ua1aa'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\ua46d\ua18f\ua44d', '\ua18f\ua282\ua2cd', '\ua18f\ua282\ua44d', '\ua18f\ua282\ua315', '\ua18f\ua282\ua1d6', '\ua18f\ua282\ua26c', '\ua18f\ua282\ua0d8'],
+  SHORTWEEKDAYS: ['\ua18f\ua44d', '\ua18f\ua2cd', '\ua18f\ua44d', '\ua18f\ua315', '\ua18f\ua1d6', '\ua18f\ua26c', '\ua18f\ua0d8'],
+  NARROWWEEKDAYS: ['\ua18f', '\ua2cd', '\ua44d', '\ua315', '\ua1d6', '\ua26c', '\ua0d8'],
+  SHORTQUARTERS: ['\ua0c5\ua44c', '\ua0c5\ua3b8', '\ua0c5\ua375', '\ua0c5\ua2c6'],
+  QUARTERS: ['\ua0c5\ua44c', '\ua0c5\ua3b8', '\ua0c5\ua375', '\ua0c5\ua2c6'],
+  AMPMS: ['\ua3b8\ua111', '\ua06f\ua2d2'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ii_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ii_CN.js
new file mode 100644
index 0000000..de254aa
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ii_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\ua0c5\ua2ca\ua0bf', '\ua0c5\ua2ca\ua282'],
+  ERANAMES: ['\ua0c5\ua2ca\ua0bf', '\ua0c5\ua2ca\ua282'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\ua2cd\ua1aa', '\ua44d\ua1aa', '\ua315\ua1aa', '\ua1d6\ua1aa', '\ua26c\ua1aa', '\ua0d8\ua1aa', '\ua3c3\ua1aa', '\ua246\ua1aa', '\ua22c\ua1aa', '\ua2b0\ua1aa', '\ua2b0\ua2aa\ua1aa', '\ua2b0\ua44b\ua1aa'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\ua46d\ua18f\ua44d', '\ua18f\ua282\ua2cd', '\ua18f\ua282\ua44d', '\ua18f\ua282\ua315', '\ua18f\ua282\ua1d6', '\ua18f\ua282\ua26c', '\ua18f\ua282\ua0d8'],
+  SHORTWEEKDAYS: ['\ua18f\ua44d', '\ua18f\ua2cd', '\ua18f\ua44d', '\ua18f\ua315', '\ua18f\ua1d6', '\ua18f\ua26c', '\ua18f\ua0d8'],
+  NARROWWEEKDAYS: ['\ua18f', '\ua2cd', '\ua44d', '\ua315', '\ua1d6', '\ua26c', '\ua0d8'],
+  SHORTQUARTERS: ['\ua0c5\ua44c', '\ua0c5\ua3b8', '\ua0c5\ua375', '\ua0c5\ua2c6'],
+  QUARTERS: ['\ua0c5\ua44c', '\ua0c5\ua3b8', '\ua0c5\ua375', '\ua0c5\ua2c6'],
+  AMPMS: ['\ua3b8\ua111', '\ua06f\ua2d2'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__in.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__in.js
new file mode 100644
index 0000000..257a145
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__in.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'],
+  SHORTWEEKDAYS: ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['kuartal pertama', 'kuartal kedua', 'kuartal ketiga', 'kuartal keempat'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, dd MMMM yyyy', 'd MMMM yyyy', 'd MMM yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__is.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__is.js
new file mode 100644
index 0000000..5f83881
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__is.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['fyrir Krist', 'eftir Krist'],
+  ERANAMES: ['fyrir Krist', 'eftir Krist'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', '\u00e1', 's', 'o', 'n', 'd'],
+  MONTHS: ['jan\u00faar', 'febr\u00faar', 'mars', 'apr\u00edl', 'ma\u00ed', 'j\u00fan\u00ed', 'j\u00fal\u00ed', '\u00e1g\u00fast', 'september', 'okt\u00f3ber', 'n\u00f3vember', 'desember'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'ma\u00ed', 'j\u00fan', 'j\u00fal', '\u00e1g\u00fa', 'sep', 'okt', 'n\u00f3v', 'des'],
+  WEEKDAYS: ['sunnudagur', 'm\u00e1nudagur', '\u00feri\u00f0judagur', 'mi\u00f0vikudagur', 'fimmtudagur', 'f\u00f6studagur', 'laugardagur'],
+  SHORTWEEKDAYS: ['sun', 'm\u00e1n', '\u00feri', 'mi\u00f0', 'fim', 'f\u00f6s', 'lau'],
+  NARROWWEEKDAYS: ['s', 'm', '\u00fe', 'm', 'f', 'f', 'l'],
+  SHORTQUARTERS: ['F1', 'F2', 'F3', 'F4'],
+  QUARTERS: ['1st fj\u00f3r\u00f0ungur', '2nd fj\u00f3r\u00f0ungur', '3rd fj\u00f3r\u00f0ungur', '4th fj\u00f3r\u00f0ungur'],
+  AMPMS: ['f.h.', 'e.h.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__is_IS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__is_IS.js
new file mode 100644
index 0000000..5f83881
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__is_IS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['fyrir Krist', 'eftir Krist'],
+  ERANAMES: ['fyrir Krist', 'eftir Krist'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', '\u00e1', 's', 'o', 'n', 'd'],
+  MONTHS: ['jan\u00faar', 'febr\u00faar', 'mars', 'apr\u00edl', 'ma\u00ed', 'j\u00fan\u00ed', 'j\u00fal\u00ed', '\u00e1g\u00fast', 'september', 'okt\u00f3ber', 'n\u00f3vember', 'desember'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'ma\u00ed', 'j\u00fan', 'j\u00fal', '\u00e1g\u00fa', 'sep', 'okt', 'n\u00f3v', 'des'],
+  WEEKDAYS: ['sunnudagur', 'm\u00e1nudagur', '\u00feri\u00f0judagur', 'mi\u00f0vikudagur', 'fimmtudagur', 'f\u00f6studagur', 'laugardagur'],
+  SHORTWEEKDAYS: ['sun', 'm\u00e1n', '\u00feri', 'mi\u00f0', 'fim', 'f\u00f6s', 'lau'],
+  NARROWWEEKDAYS: ['s', 'm', '\u00fe', 'm', 'f', 'f', 'l'],
+  SHORTQUARTERS: ['F1', 'F2', 'F3', 'F4'],
+  QUARTERS: ['1st fj\u00f3r\u00f0ungur', '2nd fj\u00f3r\u00f0ungur', '3rd fj\u00f3r\u00f0ungur', '4th fj\u00f3r\u00f0ungur'],
+  AMPMS: ['f.h.', 'e.h.'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it.js
new file mode 100644
index 0000000..dba9e3e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['aC', 'dC'],
+  ERANAMES: ['a.C.', 'd.C'],
+  NARROWMONTHS: ['G', 'F', 'M', 'A', 'M', 'G', 'L', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'],
+  STANDALONEMONTHS: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
+  SHORTMONTHS: ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'],
+  WEEKDAYS: ['domenica', 'luned\u00ec', 'marted\u00ec', 'mercoled\u00ec', 'gioved\u00ec', 'venerd\u00ec', 'sabato'],
+  STANDALONEWEEKDAYS: ['Domenica', 'Luned\u00ec', 'Marted\u00ec', 'Mercoled\u00ec', 'Gioved\u00ec', 'Venerd\u00ec', 'Sabato'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'G', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1o trimestre', '2o trimestre', '3o trimestre', '4o trimestre'],
+  AMPMS: ['m.', 'p.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'dd MMMM y', 'dd/MMM/y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it_CH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it_CH.js
new file mode 100644
index 0000000..071c443
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it_CH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['aC', 'dC'],
+  ERANAMES: ['a.C.', 'd.C'],
+  NARROWMONTHS: ['G', 'F', 'M', 'A', 'M', 'G', 'L', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'],
+  STANDALONEMONTHS: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
+  SHORTMONTHS: ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'],
+  WEEKDAYS: ['domenica', 'luned\u00ec', 'marted\u00ec', 'mercoled\u00ec', 'gioved\u00ec', 'venerd\u00ec', 'sabato'],
+  STANDALONEWEEKDAYS: ['Domenica', 'Luned\u00ec', 'Marted\u00ec', 'Mercoled\u00ec', 'Gioved\u00ec', 'Venerd\u00ec', 'Sabato'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'G', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1o trimestre', '2o trimestre', '3o trimestre', '4o trimestre'],
+  AMPMS: ['m.', 'p.'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd-MMM-y', 'dd.MM.yy'],
+  TIMEFORMATS: ["HH.mm:ss 'h' zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it_IT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it_IT.js
new file mode 100644
index 0000000..dba9e3e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__it_IT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['aC', 'dC'],
+  ERANAMES: ['a.C.', 'd.C'],
+  NARROWMONTHS: ['G', 'F', 'M', 'A', 'M', 'G', 'L', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['gennaio', 'febbraio', 'marzo', 'aprile', 'maggio', 'giugno', 'luglio', 'agosto', 'settembre', 'ottobre', 'novembre', 'dicembre'],
+  STANDALONEMONTHS: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
+  SHORTMONTHS: ['gen', 'feb', 'mar', 'apr', 'mag', 'giu', 'lug', 'ago', 'set', 'ott', 'nov', 'dic'],
+  WEEKDAYS: ['domenica', 'luned\u00ec', 'marted\u00ec', 'mercoled\u00ec', 'gioved\u00ec', 'venerd\u00ec', 'sabato'],
+  STANDALONEWEEKDAYS: ['Domenica', 'Luned\u00ec', 'Marted\u00ec', 'Mercoled\u00ec', 'Gioved\u00ec', 'Venerd\u00ec', 'Sabato'],
+  SHORTWEEKDAYS: ['dom', 'lun', 'mar', 'mer', 'gio', 'ven', 'sab'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'G', 'V', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1o trimestre', '2o trimestre', '3o trimestre', '4o trimestre'],
+  AMPMS: ['m.', 'p.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'dd MMMM y', 'dd/MMM/y', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__iu.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__iu.js
new file mode 100644
index 0000000..79ef568
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__iu.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u152d\u14d0\u14c4\u140a\u14d5', '\u1555\u155d\u1557\u140a\u14d5', '\u14ab\u1466\u14ef', '\u140a\u1403\u1449\u1433\u14d7', '\u14aa\u1403', '\u152b\u14c2', '\u152a\u14da\u1403', '\u140a\u1405\u14a1\u148d\u14ef', '\u14f0\u1466\u144f\u155d\u1559', '\u1406\u1466\u1451\u155d\u1559', '\u14c5\u1559\u1403\u155d\u1559', '\u144f\u14f0\u155d\u1559'],
+  SHORTMONTHS: ['\u152d\u14d0\u14c4\u140a\u14d5', '\u1555\u155d\u1557\u140a\u14d5', '\u14ab\u1466\u14ef', '\u140a\u1403\u1449\u1433\u14d7', '\u14aa\u1403', '\u152b\u14c2', '\u152a\u14da\u1403', '\u140a\u1405\u14a1\u148d\u14ef', '\u14f0\u1466\u144f\u155d\u1559', '\u1406\u1466\u1451\u155d\u1559', '\u14c5\u1559\u1403\u155d\u1559', '\u144f\u14f0\u155d\u1559'],
+  WEEKDAYS: ['\u14c8\u1466\u14f0\u1591\u152d', '\u14c7\u14a1\u1490\u153e\u152d\u1405', '\u14c7\u14a1\u1490\u153e\u152d\u1405\u14d5\u1585\u146d', '\u1431\u1593\u1466\u14ef\u1585', '\u14ef\u1455\u14bb\u14a5\u1585', '\u1455\u14ea\u14d5\u1550\u14a5\u1585', '\u14c8\u1466\u14f0\u1591\u152d\u14d5\u1585\u157f'],
+  SHORTWEEKDAYS: ['\u14c8\u1466\u14f0\u1591\u152d', '\u14c7\u14a1\u1490\u153e\u152d\u1405', '\u14c7\u14a1\u1490\u153e\u152d\u1405\u14d5\u1585\u146d', '\u1431\u1593\u1466\u14ef\u1585', '\u14ef\u1455\u14bb\u14a5\u1585', '\u1455\u14ea\u14d5\u1550\u14a5\u1585', '\u14c8\u1466\u14f0\u1591\u152d\u14d5\u1585\u157f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__iw.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__iw.js
new file mode 100644
index 0000000..eae86f2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__iw.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u05dc\u05e4\u05e0\u05d4\u05f4\u05e1', '\u05dc\u05e1\u05d4\u05f4\u05e0'],
+  ERANAMES: ['\u05dc\u05e4\u05e0\u05d9 \u05d4\u05e1\u05e4\u05d9\u05e8\u05d4', '\u05dc\u05e1\u05e4\u05d9\u05e8\u05d4'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u05d9\u05e0\u05d5\u05d0\u05e8', '\u05e4\u05d1\u05e8\u05d5\u05d0\u05e8', '\u05de\u05e8\u05e1', '\u05d0\u05e4\u05e8\u05d9\u05dc', '\u05de\u05d0\u05d9', '\u05d9\u05d5\u05e0\u05d9', '\u05d9\u05d5\u05dc\u05d9', '\u05d0\u05d5\u05d2\u05d5\u05e1\u05d8', '\u05e1\u05e4\u05d8\u05de\u05d1\u05e8', '\u05d0\u05d5\u05e7\u05d8\u05d5\u05d1\u05e8', '\u05e0\u05d5\u05d1\u05de\u05d1\u05e8', '\u05d3\u05e6\u05de\u05d1\u05e8'],
+  SHORTMONTHS: ['\u05d9\u05e0\u05d5', '\u05e4\u05d1\u05e8', '\u05de\u05e8\u05e1', '\u05d0\u05e4\u05e8', '\u05de\u05d0\u05d9', '\u05d9\u05d5\u05e0', '\u05d9\u05d5\u05dc', '\u05d0\u05d5\u05d2', '\u05e1\u05e4\u05d8', '\u05d0\u05d5\u05e7', '\u05e0\u05d5\u05d1', '\u05d3\u05e6\u05de'],
+  WEEKDAYS: ['\u05d9\u05d5\u05dd \u05e8\u05d0\u05e9\u05d5\u05df', '\u05d9\u05d5\u05dd \u05e9\u05e0\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05dc\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e8\u05d1\u05d9\u05e2\u05d9', '\u05d9\u05d5\u05dd \u05d7\u05de\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05d9\u05e9\u05d9', '\u05d9\u05d5\u05dd \u05e9\u05d1\u05ea'],
+  SHORTWEEKDAYS: ["\u05d9\u05d5\u05dd \u05d0'", "\u05d9\u05d5\u05dd \u05d1'", "\u05d9\u05d5\u05dd \u05d2'", "\u05d9\u05d5\u05dd \u05d3'", "\u05d9\u05d5\u05dd \u05d4'", "\u05d9\u05d5\u05dd \u05d5'", '\u05e9\u05d1\u05ea'],
+  NARROWWEEKDAYS: ['\u05d0', '\u05d1', '\u05d2', '\u05d3', '\u05d4', '\u05d5', '\u05e9'],
+  SHORTQUARTERS: ['\u05e8\u05d1\u05e2\u05d5\u05df 1', '\u05e8\u05d1\u05e2\u05d5\u05df 2', '\u05e8\u05d1\u05e2\u05d5\u05df 3', '\u05e8\u05d1\u05e2\u05d5\u05df 4'],
+  QUARTERS: ['\u05e8\u05d1\u05e2\u05d5\u05df 1', '\u05e8\u05d1\u05e2\u05d5\u05df 2', '\u05e8\u05d1\u05e2\u05d5\u05df 3', '\u05e8\u05d1\u05e2\u05d5\u05df 4'],
+  AMPMS: ['\u05dc\u05e4\u05e0\u05d4\"\u05e6', '\u05d0\u05d7\u05d4\"\u05e6'],
+  DATEFORMATS: ['EEEE, d \u05d1MMMM y', 'd \u05d1MMMM y', 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ja.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ja.js
new file mode 100644
index 0000000..3621ac2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ja.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u7d00\u5143\u524d', '\u897f\u66a6'],
+  ERANAMES: ['\u7d00\u5143\u524d', '\u897f\u66a6'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u65e5\u66dc\u65e5', '\u6708\u66dc\u65e5', '\u706b\u66dc\u65e5', '\u6c34\u66dc\u65e5', '\u6728\u66dc\u65e5', '\u91d1\u66dc\u65e5', '\u571f\u66dc\u65e5'],
+  SHORTWEEKDAYS: ['\u65e5', '\u6708', '\u706b', '\u6c34', '\u6728', '\u91d1', '\u571f'],
+  NARROWWEEKDAYS: ['\u65e5', '\u6708', '\u706b', '\u6c34', '\u6728', '\u91d1', '\u571f'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u7b2c1\u56db\u534a\u671f', '\u7b2c2\u56db\u534a\u671f', '\u7b2c3\u56db\u534a\u671f', '\u7b2c4\u56db\u534a\u671f'],
+  AMPMS: ['\u5348\u524d', '\u5348\u5f8c'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy/MM/dd', 'yy/MM/dd'],
+  TIMEFORMATS: ['H\u6642mm\u5206ss\u79d2 zzzz', 'HH:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ja_JP.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ja_JP.js
new file mode 100644
index 0000000..3621ac2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ja_JP.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u7d00\u5143\u524d', '\u897f\u66a6'],
+  ERANAMES: ['\u7d00\u5143\u524d', '\u897f\u66a6'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u65e5\u66dc\u65e5', '\u6708\u66dc\u65e5', '\u706b\u66dc\u65e5', '\u6c34\u66dc\u65e5', '\u6728\u66dc\u65e5', '\u91d1\u66dc\u65e5', '\u571f\u66dc\u65e5'],
+  SHORTWEEKDAYS: ['\u65e5', '\u6708', '\u706b', '\u6c34', '\u6728', '\u91d1', '\u571f'],
+  NARROWWEEKDAYS: ['\u65e5', '\u6708', '\u706b', '\u6c34', '\u6728', '\u91d1', '\u571f'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u7b2c1\u56db\u534a\u671f', '\u7b2c2\u56db\u534a\u671f', '\u7b2c3\u56db\u534a\u671f', '\u7b2c4\u56db\u534a\u671f'],
+  AMPMS: ['\u5348\u524d', '\u5348\u5f8c'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy/MM/dd', 'yy/MM/dd'],
+  TIMEFORMATS: ['H\u6642mm\u5206ss\u79d2 zzzz', 'HH:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ka.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ka.js
new file mode 100644
index 0000000..c4595bc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ka.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u10e9\u10d5\u10d4\u10dc\u10e1 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d0\u10db\u10d3\u10d4', '\u10e9\u10d5\u10d4\u10dc\u10d8 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d8\u10d7'],
+  ERANAMES: ['\u10e9\u10d5\u10d4\u10dc\u10e1 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d0\u10db\u10d3\u10d4', '\u10e9\u10d5\u10d4\u10dc\u10d8 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d8\u10d7'],
+  NARROWMONTHS: ['\u10d8', '\u10d7', '\u10db', '\u10d0', '\u10db', '\u10d8', '\u10d8', '\u10d0', '\u10e1', '\u10dd', '\u10dc', '\u10d3'],
+  MONTHS: ['\u10d8\u10d0\u10dc\u10d5\u10d0\u10e0\u10d8', '\u10d7\u10d4\u10d1\u10d4\u10e0\u10d5\u10d0\u10da\u10d8', '\u10db\u10d0\u10e0\u10e2\u10d8', '\u10d0\u10de\u10e0\u10d8\u10da\u10d8', '\u10db\u10d0\u10d8\u10e1\u10d8', '\u10d8\u10d5\u10dc\u10d8\u10e1\u10d8', '\u10d8\u10d5\u10da\u10d8\u10e1\u10d8', '\u10d0\u10d2\u10d5\u10d8\u10e1\u10e2\u10dd', '\u10e1\u10d4\u10e5\u10e2\u10d4\u10db\u10d1\u10d4\u10e0\u10d8', '\u10dd\u10e5\u10e2\u10dd\u10db\u10d1\u10d4\u10e0\u10d8', '\u10dc\u10dd\u10d4\u10db\u10d1\u10d4\u10e0\u10d8', '\u10d3\u10d4\u10d9\u10d4\u10db\u10d1\u10d4\u10e0\u10d8'],
+  SHORTMONTHS: ['\u10d8\u10d0\u10dc', '\u10d7\u10d4\u10d1', '\u10db\u10d0\u10e0', '\u10d0\u10de\u10e0', '\u10db\u10d0\u10d8', '\u10d8\u10d5\u10dc', '\u10d8\u10d5\u10da', '\u10d0\u10d2\u10d5', '\u10e1\u10d4\u10e5', '\u10dd\u10e5\u10e2', '\u10dc\u10dd\u10d4', '\u10d3\u10d4\u10d9'],
+  WEEKDAYS: ['\u10d9\u10d5\u10d8\u10e0\u10d0', '\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10d8', '\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8'],
+  SHORTWEEKDAYS: ['\u10d9\u10d5\u10d8', '\u10dd\u10e0\u10e8', '\u10e1\u10d0\u10db', '\u10dd\u10d7\u10ee', '\u10ee\u10e3\u10d7', '\u10de\u10d0\u10e0', '\u10e8\u10d0\u10d1'],
+  NARROWWEEKDAYS: ['\u10d9', '\u10dd', '\u10e1', '\u10dd', '\u10ee', '\u10de', '\u10e8'],
+  SHORTQUARTERS: ['I \u10d9\u10d5.', 'II \u10d9\u10d5.', 'III \u10d9\u10d5.', 'IV \u10d9\u10d5.'],
+  QUARTERS: ['1-\u10da\u10d8 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8', '\u10db\u10d4-2 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8', '\u10db\u10d4-3 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8', '\u10db\u10d4-4 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8'],
+  AMPMS: ['\u10d3\u10d8\u10da\u10d8\u10e1', '\u10e1\u10d0\u10e6\u10d0\u10db\u10dd\u10e1'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ka_GE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ka_GE.js
new file mode 100644
index 0000000..c4595bc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ka_GE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u10e9\u10d5\u10d4\u10dc\u10e1 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d0\u10db\u10d3\u10d4', '\u10e9\u10d5\u10d4\u10dc\u10d8 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d8\u10d7'],
+  ERANAMES: ['\u10e9\u10d5\u10d4\u10dc\u10e1 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d0\u10db\u10d3\u10d4', '\u10e9\u10d5\u10d4\u10dc\u10d8 \u10ec\u10d4\u10da\u10d7\u10d0\u10e6\u10e0\u10d8\u10ea\u10ee\u10d5\u10d8\u10d7'],
+  NARROWMONTHS: ['\u10d8', '\u10d7', '\u10db', '\u10d0', '\u10db', '\u10d8', '\u10d8', '\u10d0', '\u10e1', '\u10dd', '\u10dc', '\u10d3'],
+  MONTHS: ['\u10d8\u10d0\u10dc\u10d5\u10d0\u10e0\u10d8', '\u10d7\u10d4\u10d1\u10d4\u10e0\u10d5\u10d0\u10da\u10d8', '\u10db\u10d0\u10e0\u10e2\u10d8', '\u10d0\u10de\u10e0\u10d8\u10da\u10d8', '\u10db\u10d0\u10d8\u10e1\u10d8', '\u10d8\u10d5\u10dc\u10d8\u10e1\u10d8', '\u10d8\u10d5\u10da\u10d8\u10e1\u10d8', '\u10d0\u10d2\u10d5\u10d8\u10e1\u10e2\u10dd', '\u10e1\u10d4\u10e5\u10e2\u10d4\u10db\u10d1\u10d4\u10e0\u10d8', '\u10dd\u10e5\u10e2\u10dd\u10db\u10d1\u10d4\u10e0\u10d8', '\u10dc\u10dd\u10d4\u10db\u10d1\u10d4\u10e0\u10d8', '\u10d3\u10d4\u10d9\u10d4\u10db\u10d1\u10d4\u10e0\u10d8'],
+  SHORTMONTHS: ['\u10d8\u10d0\u10dc', '\u10d7\u10d4\u10d1', '\u10db\u10d0\u10e0', '\u10d0\u10de\u10e0', '\u10db\u10d0\u10d8', '\u10d8\u10d5\u10dc', '\u10d8\u10d5\u10da', '\u10d0\u10d2\u10d5', '\u10e1\u10d4\u10e5', '\u10dd\u10e5\u10e2', '\u10dc\u10dd\u10d4', '\u10d3\u10d4\u10d9'],
+  WEEKDAYS: ['\u10d9\u10d5\u10d8\u10e0\u10d0', '\u10dd\u10e0\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10e1\u10d0\u10db\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10dd\u10d7\u10ee\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10ee\u10e3\u10d7\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8', '\u10de\u10d0\u10e0\u10d0\u10e1\u10d9\u10d4\u10d5\u10d8', '\u10e8\u10d0\u10d1\u10d0\u10d7\u10d8'],
+  SHORTWEEKDAYS: ['\u10d9\u10d5\u10d8', '\u10dd\u10e0\u10e8', '\u10e1\u10d0\u10db', '\u10dd\u10d7\u10ee', '\u10ee\u10e3\u10d7', '\u10de\u10d0\u10e0', '\u10e8\u10d0\u10d1'],
+  NARROWWEEKDAYS: ['\u10d9', '\u10dd', '\u10e1', '\u10dd', '\u10ee', '\u10de', '\u10e8'],
+  SHORTQUARTERS: ['I \u10d9\u10d5.', 'II \u10d9\u10d5.', 'III \u10d9\u10d5.', 'IV \u10d9\u10d5.'],
+  QUARTERS: ['1-\u10da\u10d8 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8', '\u10db\u10d4-2 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8', '\u10db\u10d4-3 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8', '\u10db\u10d4-4 \u10d9\u10d5\u10d0\u10e0\u10e2\u10d0\u10da\u10d8'],
+  AMPMS: ['\u10d3\u10d8\u10da\u10d8\u10e1', '\u10e1\u10d0\u10e6\u10d0\u10db\u10dd\u10e1'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kaj.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kaj.js
new file mode 100644
index 0000000..f5c8d8e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kaj.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['G.M.', 'M.'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Hywan A\u0331yrnig', 'Hywan A\u0331hwa', 'Hywan A\u0331tat', 'Hywan A\u0331naai', 'Hywan A\u0331pfwon', 'Hywan A\u0331kitat', 'Hywan A\u0331tyirin', 'Hywan A\u0331ninai', 'Hywan A\u0331kumviriyin', 'Hywan Swak', "Hywan Swak B'a\u0331yrnig", "Hywan Swak B'a\u0331hwa"],
+  SHORTMONTHS: ['A\u0331yr', 'A\u0331hw', 'A\u0331ta', 'A\u0331na', 'A\u0331pf', 'A\u0331ki', 'A\u0331ty', 'A\u0331ni', 'A\u0331ku', 'Swa', 'Sby', 'Sbh'],
+  WEEKDAYS: ['Ladi', 'Lintani', 'Talata', 'Larba', 'Lamit', 'Juma', 'Asabar'],
+  SHORTWEEKDAYS: ['Lad', 'Lin', 'Tal', 'Lar', 'Lam', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kaj_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kaj_NG.js
new file mode 100644
index 0000000..f5c8d8e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kaj_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['G.M.', 'M.'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Hywan A\u0331yrnig', 'Hywan A\u0331hwa', 'Hywan A\u0331tat', 'Hywan A\u0331naai', 'Hywan A\u0331pfwon', 'Hywan A\u0331kitat', 'Hywan A\u0331tyirin', 'Hywan A\u0331ninai', 'Hywan A\u0331kumviriyin', 'Hywan Swak', "Hywan Swak B'a\u0331yrnig", "Hywan Swak B'a\u0331hwa"],
+  SHORTMONTHS: ['A\u0331yr', 'A\u0331hw', 'A\u0331ta', 'A\u0331na', 'A\u0331pf', 'A\u0331ki', 'A\u0331ty', 'A\u0331ni', 'A\u0331ku', 'Swa', 'Sby', 'Sbh'],
+  WEEKDAYS: ['Ladi', 'Lintani', 'Talata', 'Larba', 'Lamit', 'Juma', 'Asabar'],
+  SHORTWEEKDAYS: ['Lad', 'Lin', 'Tal', 'Lar', 'Lam', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['A.M.', 'P.M.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kam.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kam.js
new file mode 100644
index 0000000..eec907f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kam.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['MY', 'IY'],
+  ERANAMES: ['mbee wa yesu', 'IY'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Mwei wa mbee', 'Mwei wa keli', 'Mwei wa katatu', 'Mwei wa kanne', 'Mwei wa katano', 'Mwei wa thanthatu', 'Mwei wa muonza', 'Mwei wa nyanya', 'Mwei wa kenda', 'Mwei wa ikumi', 'Mwei wa ikumi na imwe', 'Mwei wa ikumi na ili'],
+  SHORTMONTHS: ['Mwei wa mbee', 'Mwei wa keli', 'Mwei wa katatu', 'Mwei wa kanne', 'Mwei wa katano', 'Mwei wa thanthatu', 'Mwei wa muonza', 'Mwei wa nyanya', 'Mwei wa kenda', 'Mwei wa ikumi', 'Mwei wa ikumi na imwe', 'Mwei wa ikumi na ili'],
+  WEEKDAYS: ['Jumapili', 'Jumatatu', 'Jumanne', 'Jumatano', 'Alamisi', 'Ijumaa', 'Jumamosi'],
+  SHORTWEEKDAYS: ['Jpl', 'Jtt', 'Jnn', 'Jtn', 'Alh', 'Ijm', 'Jms'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kam_KE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kam_KE.js
new file mode 100644
index 0000000..640cc70
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kam_KE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['MY', 'IY'],
+  ERANAMES: ['mbee wa yesu', 'IY'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Mwei wa mbee', 'Mwei wa keli', 'Mwei wa katatu', 'Mwei wa kanne', 'Mwei wa katano', 'Mwei wa thanthatu', 'Mwei wa muonza', 'Mwei wa nyanya', 'Mwei wa kenda', 'Mwei wa ikumi', 'Mwei wa ikumi na imwe', 'Mwei wa ikumi na ili'],
+  SHORTMONTHS: ['Mwei wa mbee', 'Mwei wa keli', 'Mwei wa katatu', 'Mwei wa kanne', 'Mwei wa katano', 'Mwei wa thanthatu', 'Mwei wa muonza', 'Mwei wa nyanya', 'Mwei wa kenda', 'Mwei wa ikumi', 'Mwei wa ikumi na imwe', 'Mwei wa ikumi na ili'],
+  WEEKDAYS: ['Jumapili', 'Jumatatu', 'Jumanne', 'Jumatano', 'Alamisi', 'Ijumaa', 'Jumamosi'],
+  SHORTWEEKDAYS: ['Jpl', 'Jtt', 'Jnn', 'Jtn', 'Alh', 'Ijm', 'Jms'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kcg.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kcg.js
new file mode 100644
index 0000000..bb5ecc3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kcg.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Zwat Juwung', 'Zwat Swiyang', 'Zwat Tsat', 'Zwat Nyai', 'Zwat Tswon', 'Zwat Ataah', 'Zwat Anatat', 'Zwat Arinai', 'Zwat Akubunyung', 'Zwat Swag', 'Zwat Mangjuwang', 'Zwat Swag-Ma-Suyang'],
+  SHORTMONTHS: ['Juw', 'Swi', 'Tsa', 'Nya', 'Tsw', 'Ata', 'Ana', 'Ari', 'Aku', 'Swa', 'Man', 'Mas'],
+  WEEKDAYS: ['Ladi', 'Tanii', 'Talata', 'Larba', 'Lamit', 'Juma', 'Asabat'],
+  SHORTWEEKDAYS: ['Lad', 'Tan', 'Tal', 'Lar', 'Lam', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kcg_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kcg_NG.js
new file mode 100644
index 0000000..bb5ecc3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kcg_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['GM', 'M'],
+  ERANAMES: ['Gabanin Miladi', 'Miladi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Zwat Juwung', 'Zwat Swiyang', 'Zwat Tsat', 'Zwat Nyai', 'Zwat Tswon', 'Zwat Ataah', 'Zwat Anatat', 'Zwat Arinai', 'Zwat Akubunyung', 'Zwat Swag', 'Zwat Mangjuwang', 'Zwat Swag-Ma-Suyang'],
+  SHORTMONTHS: ['Juw', 'Swi', 'Tsa', 'Nya', 'Tsw', 'Ata', 'Ana', 'Ari', 'Aku', 'Swa', 'Man', 'Mas'],
+  WEEKDAYS: ['Ladi', 'Tanii', 'Talata', 'Larba', 'Lamit', 'Juma', 'Asabat'],
+  SHORTWEEKDAYS: ['Lad', 'Tan', 'Tal', 'Lar', 'Lam', 'Jum', 'Asa'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kfo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kfo.js
new file mode 100644
index 0000000..bc36a24
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kfo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KMW', 'BCW'],
+  ERANAMES: ['Kafi Mar Wenom', 'Bayan Chi Wenom'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Fai Weyene', 'Fai Fani', 'Fai Tataka', 'Fai Nangra', 'Fai Tuyo', 'Fai Tsoyi', 'Fai Tafaka', 'Fai Warachi', 'Fai Kunobok', 'Fai Bansok', 'Fai Kom', 'Fai Sauk'],
+  SHORTMONTHS: ['Wey', 'Fan', 'Tat', 'Nan', 'Tuy', 'Tso', 'Taf', 'War', 'Kun', 'Ban', 'Kom', 'Sau'],
+  WEEKDAYS: ['Lahadi', 'Je-Kubacha', 'Je-Gbai', 'Tansati', 'Je-Yei', 'Je-Koye', 'Sati'],
+  SHORTWEEKDAYS: ['Lah', 'Kub', 'Gba', 'Tan', 'Yei', 'Koy', 'Sat'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kfo_CI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kfo_CI.js
new file mode 100644
index 0000000..bc36a24
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kfo_CI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KMW', 'BCW'],
+  ERANAMES: ['Kafi Mar Wenom', 'Bayan Chi Wenom'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Fai Weyene', 'Fai Fani', 'Fai Tataka', 'Fai Nangra', 'Fai Tuyo', 'Fai Tsoyi', 'Fai Tafaka', 'Fai Warachi', 'Fai Kunobok', 'Fai Bansok', 'Fai Kom', 'Fai Sauk'],
+  SHORTMONTHS: ['Wey', 'Fan', 'Tat', 'Nan', 'Tuy', 'Tso', 'Taf', 'War', 'Kun', 'Ban', 'Kom', 'Sau'],
+  WEEKDAYS: ['Lahadi', 'Je-Kubacha', 'Je-Gbai', 'Tansati', 'Je-Yei', 'Je-Koye', 'Sati'],
+  SHORTWEEKDAYS: ['Lah', 'Kub', 'Gba', 'Tan', 'Yei', 'Koy', 'Sat'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk.js
new file mode 100644
index 0000000..6dc091a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u049b\u0430\u04a3\u0442\u0430\u0440', '\u0430\u049b\u043f\u0430\u043d', '\u043d\u0430\u0443\u0440\u044b\u0437', '\u0441\u04d9\u0443\u0456\u0440', '\u043c\u0430\u043c\u044b\u0440', '\u043c\u0430\u0443\u0441\u044b\u043c', '\u0448\u0456\u043b\u0434\u0435', '\u0442\u0430\u043c\u044b\u0437', '\u049b\u044b\u0440\u043a\u04af\u0439\u0435\u043a', '\u049b\u0430\u0437\u0430\u043d', '\u049b\u0430\u0440\u0430\u0448\u0430', '\u0436\u0435\u043b\u0442\u043e\u049b\u0441\u0430\u043d'],
+  SHORTMONTHS: ['\u049b\u0430\u04a3.', '\u0430\u049b\u043f.', '\u043d\u0430\u0443.', '\u0441\u04d9\u0443.', '\u043c\u0430\u043c.', '\u043c\u0430\u0443.', '\u0448\u0456\u043b.', '\u0442\u0430\u043c.', '\u049b\u044b\u0440.', '\u049b\u0430\u0437.', '\u049b\u0430\u0440.', '\u0436\u0435\u043b\u0442.'],
+  WEEKDAYS: ['\u0436\u0435\u043a\u0441\u0435\u043d\u0456', '\u0434\u0443\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u04d9\u0440\u0435\u043d\u0431\u0456', '\u0431\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0436\u04b1\u043c\u0430', '\u0441\u0435\u043d\u0431\u0456'],
+  SHORTWEEKDAYS: ['\u0436\u0441.', '\u0434\u0441.', '\u0441\u0441.', '\u0441\u0440.', '\u0431\u0441.', '\u0436\u043c.', '\u0441\u04bb.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0436'.", "d MMMM y '\u0436'.", 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_Cyrl.js
new file mode 100644
index 0000000..6dc091a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_Cyrl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u049b\u0430\u04a3\u0442\u0430\u0440', '\u0430\u049b\u043f\u0430\u043d', '\u043d\u0430\u0443\u0440\u044b\u0437', '\u0441\u04d9\u0443\u0456\u0440', '\u043c\u0430\u043c\u044b\u0440', '\u043c\u0430\u0443\u0441\u044b\u043c', '\u0448\u0456\u043b\u0434\u0435', '\u0442\u0430\u043c\u044b\u0437', '\u049b\u044b\u0440\u043a\u04af\u0439\u0435\u043a', '\u049b\u0430\u0437\u0430\u043d', '\u049b\u0430\u0440\u0430\u0448\u0430', '\u0436\u0435\u043b\u0442\u043e\u049b\u0441\u0430\u043d'],
+  SHORTMONTHS: ['\u049b\u0430\u04a3.', '\u0430\u049b\u043f.', '\u043d\u0430\u0443.', '\u0441\u04d9\u0443.', '\u043c\u0430\u043c.', '\u043c\u0430\u0443.', '\u0448\u0456\u043b.', '\u0442\u0430\u043c.', '\u049b\u044b\u0440.', '\u049b\u0430\u0437.', '\u049b\u0430\u0440.', '\u0436\u0435\u043b\u0442.'],
+  WEEKDAYS: ['\u0436\u0435\u043a\u0441\u0435\u043d\u0456', '\u0434\u0443\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u04d9\u0440\u0435\u043d\u0431\u0456', '\u0431\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0436\u04b1\u043c\u0430', '\u0441\u0435\u043d\u0431\u0456'],
+  SHORTWEEKDAYS: ['\u0436\u0441.', '\u0434\u0441.', '\u0441\u0441.', '\u0441\u0440.', '\u0431\u0441.', '\u0436\u043c.', '\u0441\u04bb.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0436'.", "d MMMM y '\u0436'.", 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_Cyrl_KZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_Cyrl_KZ.js
new file mode 100644
index 0000000..6dc091a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_Cyrl_KZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u049b\u0430\u04a3\u0442\u0430\u0440', '\u0430\u049b\u043f\u0430\u043d', '\u043d\u0430\u0443\u0440\u044b\u0437', '\u0441\u04d9\u0443\u0456\u0440', '\u043c\u0430\u043c\u044b\u0440', '\u043c\u0430\u0443\u0441\u044b\u043c', '\u0448\u0456\u043b\u0434\u0435', '\u0442\u0430\u043c\u044b\u0437', '\u049b\u044b\u0440\u043a\u04af\u0439\u0435\u043a', '\u049b\u0430\u0437\u0430\u043d', '\u049b\u0430\u0440\u0430\u0448\u0430', '\u0436\u0435\u043b\u0442\u043e\u049b\u0441\u0430\u043d'],
+  SHORTMONTHS: ['\u049b\u0430\u04a3.', '\u0430\u049b\u043f.', '\u043d\u0430\u0443.', '\u0441\u04d9\u0443.', '\u043c\u0430\u043c.', '\u043c\u0430\u0443.', '\u0448\u0456\u043b.', '\u0442\u0430\u043c.', '\u049b\u044b\u0440.', '\u049b\u0430\u0437.', '\u049b\u0430\u0440.', '\u0436\u0435\u043b\u0442.'],
+  WEEKDAYS: ['\u0436\u0435\u043a\u0441\u0435\u043d\u0456', '\u0434\u0443\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u04d9\u0440\u0435\u043d\u0431\u0456', '\u0431\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0436\u04b1\u043c\u0430', '\u0441\u0435\u043d\u0431\u0456'],
+  SHORTWEEKDAYS: ['\u0436\u0441.', '\u0434\u0441.', '\u0441\u0441.', '\u0441\u0440.', '\u0431\u0441.', '\u0436\u043c.', '\u0441\u04bb.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0436'.", "d MMMM y '\u0436'.", 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_KZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_KZ.js
new file mode 100644
index 0000000..6dc091a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kk_KZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u049b\u0430\u04a3\u0442\u0430\u0440', '\u0430\u049b\u043f\u0430\u043d', '\u043d\u0430\u0443\u0440\u044b\u0437', '\u0441\u04d9\u0443\u0456\u0440', '\u043c\u0430\u043c\u044b\u0440', '\u043c\u0430\u0443\u0441\u044b\u043c', '\u0448\u0456\u043b\u0434\u0435', '\u0442\u0430\u043c\u044b\u0437', '\u049b\u044b\u0440\u043a\u04af\u0439\u0435\u043a', '\u049b\u0430\u0437\u0430\u043d', '\u049b\u0430\u0440\u0430\u0448\u0430', '\u0436\u0435\u043b\u0442\u043e\u049b\u0441\u0430\u043d'],
+  SHORTMONTHS: ['\u049b\u0430\u04a3.', '\u0430\u049b\u043f.', '\u043d\u0430\u0443.', '\u0441\u04d9\u0443.', '\u043c\u0430\u043c.', '\u043c\u0430\u0443.', '\u0448\u0456\u043b.', '\u0442\u0430\u043c.', '\u049b\u044b\u0440.', '\u049b\u0430\u0437.', '\u049b\u0430\u0440.', '\u0436\u0435\u043b\u0442.'],
+  WEEKDAYS: ['\u0436\u0435\u043a\u0441\u0435\u043d\u0456', '\u0434\u0443\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0441\u04d9\u0440\u0435\u043d\u0431\u0456', '\u0431\u0435\u0439\u0441\u0435\u043d\u0431\u0456', '\u0436\u04b1\u043c\u0430', '\u0441\u0435\u043d\u0431\u0456'],
+  SHORTWEEKDAYS: ['\u0436\u0441.', '\u0434\u0441.', '\u0441\u0441.', '\u0441\u0440.', '\u0431\u0441.', '\u0436\u043c.', '\u0441\u04bb.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0436'.", "d MMMM y '\u0436'.", 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kl.js
new file mode 100644
index 0000000..5792e4a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['januari', 'februari', 'martsi', 'aprili', 'maji', 'juni', 'juli', 'augustusi', 'septemberi', 'oktoberi', 'novemberi', 'decemberi'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['sabaat', 'ataasinngorneq', 'marlunngorneq', 'pingasunngorneq', 'sisamanngorneq', 'tallimanngorneq', 'arfininngorneq'],
+  SHORTWEEKDAYS: ['sab', 'ata', 'mar', 'pin', 'sis', 'tal', 'arf'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'MMM dd, y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kl_GL.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kl_GL.js
new file mode 100644
index 0000000..5792e4a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kl_GL.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['januari', 'februari', 'martsi', 'aprili', 'maji', 'juni', 'juli', 'augustusi', 'septemberi', 'oktoberi', 'novemberi', 'decemberi'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['sabaat', 'ataasinngorneq', 'marlunngorneq', 'pingasunngorneq', 'sisamanngorneq', 'tallimanngorneq', 'arfininngorneq'],
+  SHORTWEEKDAYS: ['sab', 'ata', 'mar', 'pin', 'sis', 'tal', 'arf'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'dd MMMM y', 'MMM dd, y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__km.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__km.js
new file mode 100644
index 0000000..9149bcd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__km.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u1798\u17bb\u1793\u200b\u1782.\u179f.', '\u1782.\u179f.'],
+  ERANAMES: ['\u1798\u17bb\u1793\u200b\u1782\u17d2\u179a\u17b7\u179f\u17d2\u178f\u179f\u1780\u179a\u17b6\u1787', '\u1782\u17d2\u179a\u17b7\u179f\u17d2\u178f\u179f\u1780\u179a\u17b6\u1787'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u1798\u1780\u179a\u17b6', '\u1780\u17bb\u1798\u17d2\u1797\u17c8', '\u1798\u17b7\u1793\u17b6', '\u1798\u17c1\u179f\u17b6', '\u17a7\u179f\u1797\u17b6', '\u1798\u17b7\u1790\u17bb\u1793\u17b6', '\u1780\u1780\u17d2\u1780\u178a\u17b6', '\u179f\u17b8\u17a0\u17b6', '\u1780\u1789\u17d2\u1789\u17b6', '\u178f\u17bb\u179b\u17b6', '\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6', '\u1792\u17d2\u1793\u17bc'],
+  SHORTMONTHS: ['\u17e1', '\u17e2', '\u17e3', '\u17e4', '\u17e5', '\u17e6', '\u17e7', '\u17e8', '\u17e9', '\u17e1\u17e0', '\u17e1\u17e1', '\u17e1\u17e2'],
+  WEEKDAYS: ['\u1790\u17d2\u1784\u17c3\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799', '\u200b\u1790\u17d2\u1784\u17c3\u1785\u17d0\u1793\u17d2\u1791', '\u1790\u17d2\u1784\u17c3\u17a2\u1784\u17d2\u1782\u17b6\u179a', '\u1790\u17d2\u1784\u17c3\u1796\u17bb\u1792', '\u1790\u17d2\u1784\u17c3\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd', '\u1790\u17d2\u1784\u17c3\u179f\u17bb\u1780\u17d2\u179a', '\u1790\u17d2\u1784\u17c3\u179f\u17c5\u179a\u17cd'],
+  SHORTWEEKDAYS: ['\u17a2\u17b6', '\u1785', '\u17a2', '\u1796\u17bb', '\u1796\u17d2\u179a', '\u179f\u17bb', '\u179f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['\u178f\u17d2\u179a\u17b8\u17e1', '\u178f\u17d2\u179a\u17b8\u17e2', '\u178f\u17d2\u179a\u17b8\u17e3', '\u178f\u17d2\u179a\u17b8\u17e4'],
+  QUARTERS: ['\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e1', '\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e2', '\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e3', '\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e4'],
+  AMPMS: ['\u1796\u17d2\u179a\u17b9\u1780', '\u179b\u17d2\u1784\u17b6\u1785'],
+  DATEFORMATS: ['EEEE \u1790\u17d2\u1784\u17c3 d \u1781\u17c2 MMMM \u1786\u17d2\u1793\u17b6\u17c6 y', 'd \u1781\u17c2 MMMM \u1786\u17d2\u1793\u17b6\u17c6 y', 'd MMM y', 'd/M/yyyy'],
+  TIMEFORMATS: ['H \u1798\u17c9\u17c4\u1784 m \u1793\u17b6\u1791\u17b8 ss \u179c\u17b7\u1793\u17b6\u1791\u17b8\u200b zzzz', 'H \u1798\u17c9\u17c4\u1784 m \u1793\u17b6\u1791\u17b8 ss \u179c\u17b7\u1793\u17b6\u1791\u17b8\u200bz', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__km_KH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__km_KH.js
new file mode 100644
index 0000000..9149bcd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__km_KH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u1798\u17bb\u1793\u200b\u1782.\u179f.', '\u1782.\u179f.'],
+  ERANAMES: ['\u1798\u17bb\u1793\u200b\u1782\u17d2\u179a\u17b7\u179f\u17d2\u178f\u179f\u1780\u179a\u17b6\u1787', '\u1782\u17d2\u179a\u17b7\u179f\u17d2\u178f\u179f\u1780\u179a\u17b6\u1787'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u1798\u1780\u179a\u17b6', '\u1780\u17bb\u1798\u17d2\u1797\u17c8', '\u1798\u17b7\u1793\u17b6', '\u1798\u17c1\u179f\u17b6', '\u17a7\u179f\u1797\u17b6', '\u1798\u17b7\u1790\u17bb\u1793\u17b6', '\u1780\u1780\u17d2\u1780\u178a\u17b6', '\u179f\u17b8\u17a0\u17b6', '\u1780\u1789\u17d2\u1789\u17b6', '\u178f\u17bb\u179b\u17b6', '\u179c\u17b7\u1785\u17d2\u1786\u17b7\u1780\u17b6', '\u1792\u17d2\u1793\u17bc'],
+  SHORTMONTHS: ['\u17e1', '\u17e2', '\u17e3', '\u17e4', '\u17e5', '\u17e6', '\u17e7', '\u17e8', '\u17e9', '\u17e1\u17e0', '\u17e1\u17e1', '\u17e1\u17e2'],
+  WEEKDAYS: ['\u1790\u17d2\u1784\u17c3\u17a2\u17b6\u1791\u17b7\u178f\u17d2\u1799', '\u200b\u1790\u17d2\u1784\u17c3\u1785\u17d0\u1793\u17d2\u1791', '\u1790\u17d2\u1784\u17c3\u17a2\u1784\u17d2\u1782\u17b6\u179a', '\u1790\u17d2\u1784\u17c3\u1796\u17bb\u1792', '\u1790\u17d2\u1784\u17c3\u1796\u17d2\u179a\u17a0\u179f\u17d2\u1794\u178f\u17b7\u17cd', '\u1790\u17d2\u1784\u17c3\u179f\u17bb\u1780\u17d2\u179a', '\u1790\u17d2\u1784\u17c3\u179f\u17c5\u179a\u17cd'],
+  SHORTWEEKDAYS: ['\u17a2\u17b6', '\u1785', '\u17a2', '\u1796\u17bb', '\u1796\u17d2\u179a', '\u179f\u17bb', '\u179f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['\u178f\u17d2\u179a\u17b8\u17e1', '\u178f\u17d2\u179a\u17b8\u17e2', '\u178f\u17d2\u179a\u17b8\u17e3', '\u178f\u17d2\u179a\u17b8\u17e4'],
+  QUARTERS: ['\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e1', '\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e2', '\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e3', '\u178f\u17d2\u179a\u17b8\u1798\u17b6\u179f\u1791\u17b8\u17e4'],
+  AMPMS: ['\u1796\u17d2\u179a\u17b9\u1780', '\u179b\u17d2\u1784\u17b6\u1785'],
+  DATEFORMATS: ['EEEE \u1790\u17d2\u1784\u17c3 d \u1781\u17c2 MMMM \u1786\u17d2\u1793\u17b6\u17c6 y', 'd \u1781\u17c2 MMMM \u1786\u17d2\u1793\u17b6\u17c6 y', 'd MMM y', 'd/M/yyyy'],
+  TIMEFORMATS: ['H \u1798\u17c9\u17c4\u1784 m \u1793\u17b6\u1791\u17b8 ss \u179c\u17b7\u1793\u17b6\u1791\u17b8\u200b zzzz', 'H \u1798\u17c9\u17c4\u1784 m \u1793\u17b6\u1791\u17b8 ss \u179c\u17b7\u1793\u17b6\u1791\u17b8\u200bz', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kn.js
new file mode 100644
index 0000000..acb3f7d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0c88\u0cb8\u0caa\u0cc2\u0cb5\u0cef.', '\u0c95\u0ccd\u0cb0\u0cbf\u0cb8\u0ccd\u0ca4 \u0cb6\u0c95'],
+  NARROWMONTHS: ['\u0c9c', '\u0cab\u0cc6', '\u0cae\u0cbe', '\u0c8e', '\u0cae\u0cc7', '\u0c9c\u0cc2', '\u0c9c\u0cc1', '\u0c86', '\u0cb8\u0cc6', '\u0c85', '\u0ca8', '\u0ca1\u0cbf'],
+  MONTHS: ['\u0c9c\u0ca8\u0cb5\u0cb0\u0cc0', '\u0cab\u0cc6\u0cac\u0ccd\u0cb0\u0cb5\u0cb0\u0cc0', '\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd', '\u0c8e\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd', '\u0cae\u0cc6', '\u0c9c\u0cc2\u0ca8\u0ccd', '\u0c9c\u0cc1\u0cb2\u0cc8', '\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd', '\u0cb8\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0c85\u0c95\u0ccd\u0c9f\u0ccb\u0cac\u0cb0\u0ccd', '\u0ca8\u0cb5\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac\u0cb0\u0ccd'],
+  SHORTMONTHS: ['\u0c9c\u0ca8\u0cb5\u0cb0\u0cc0', '\u0cab\u0cc6\u0cac\u0ccd\u0cb0\u0cb5\u0cb0\u0cc0', '\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd', '\u0c8e\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd', '\u0cae\u0cc6', '\u0c9c\u0cc2\u0ca8\u0ccd', '\u0c9c\u0cc1\u0cb2\u0cc8', '\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd', '\u0cb8\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0c85\u0c95\u0ccd\u0c9f\u0ccb\u0cac\u0cb0\u0ccd', '\u0ca8\u0cb5\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac\u0cb0\u0ccd'],
+  WEEKDAYS: ['\u0cb0\u0cb5\u0cbf\u0cb5\u0cbe\u0cb0', '\u0cb8\u0ccb\u0cae\u0cb5\u0cbe\u0cb0', '\u0cae\u0c82\u0c97\u0cb3\u0cb5\u0cbe\u0cb0', '\u0cac\u0cc1\u0ca7\u0cb5\u0cbe\u0cb0', '\u0c97\u0cc1\u0cb0\u0cc1\u0cb5\u0cbe\u0cb0', '\u0cb6\u0cc1\u0c95\u0ccd\u0cb0\u0cb5\u0cbe\u0cb0', '\u0cb6\u0ca8\u0cbf\u0cb5\u0cbe\u0cb0'],
+  SHORTWEEKDAYS: ['\u0cb0.', '\u0cb8\u0ccb.', '\u0cae\u0c82.', '\u0cac\u0cc1.', '\u0c97\u0cc1.', '\u0cb6\u0cc1.', '\u0cb6\u0ca8\u0cbf.'],
+  NARROWWEEKDAYS: ['\u0cb0', '\u0cb8\u0ccb', '\u0cae\u0c82', '\u0cac\u0cc1', '\u0c97\u0cc1', '\u0cb6\u0cc1', '\u0cb6'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0c92\u0c82\u0ca6\u0cc1 1', '\u0c8e\u0cb0\u0ca1\u0cc1 2', '\u0cae\u0cc2\u0cb0\u0cc1 3', '\u0ca8\u0cbe\u0cb2\u0cc3\u0c95 4'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'd-M-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kn_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kn_IN.js
new file mode 100644
index 0000000..acb3f7d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kn_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0c88\u0cb8\u0caa\u0cc2\u0cb5\u0cef.', '\u0c95\u0ccd\u0cb0\u0cbf\u0cb8\u0ccd\u0ca4 \u0cb6\u0c95'],
+  NARROWMONTHS: ['\u0c9c', '\u0cab\u0cc6', '\u0cae\u0cbe', '\u0c8e', '\u0cae\u0cc7', '\u0c9c\u0cc2', '\u0c9c\u0cc1', '\u0c86', '\u0cb8\u0cc6', '\u0c85', '\u0ca8', '\u0ca1\u0cbf'],
+  MONTHS: ['\u0c9c\u0ca8\u0cb5\u0cb0\u0cc0', '\u0cab\u0cc6\u0cac\u0ccd\u0cb0\u0cb5\u0cb0\u0cc0', '\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd', '\u0c8e\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd', '\u0cae\u0cc6', '\u0c9c\u0cc2\u0ca8\u0ccd', '\u0c9c\u0cc1\u0cb2\u0cc8', '\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd', '\u0cb8\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0c85\u0c95\u0ccd\u0c9f\u0ccb\u0cac\u0cb0\u0ccd', '\u0ca8\u0cb5\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac\u0cb0\u0ccd'],
+  SHORTMONTHS: ['\u0c9c\u0ca8\u0cb5\u0cb0\u0cc0', '\u0cab\u0cc6\u0cac\u0ccd\u0cb0\u0cb5\u0cb0\u0cc0', '\u0cae\u0cbe\u0cb0\u0ccd\u0c9a\u0ccd', '\u0c8e\u0caa\u0ccd\u0cb0\u0cbf\u0cb2\u0ccd', '\u0cae\u0cc6', '\u0c9c\u0cc2\u0ca8\u0ccd', '\u0c9c\u0cc1\u0cb2\u0cc8', '\u0c86\u0c97\u0cb8\u0ccd\u0c9f\u0ccd', '\u0cb8\u0caa\u0ccd\u0c9f\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0c85\u0c95\u0ccd\u0c9f\u0ccb\u0cac\u0cb0\u0ccd', '\u0ca8\u0cb5\u0cc6\u0c82\u0cac\u0cb0\u0ccd', '\u0ca1\u0cbf\u0cb8\u0cc6\u0c82\u0cac\u0cb0\u0ccd'],
+  WEEKDAYS: ['\u0cb0\u0cb5\u0cbf\u0cb5\u0cbe\u0cb0', '\u0cb8\u0ccb\u0cae\u0cb5\u0cbe\u0cb0', '\u0cae\u0c82\u0c97\u0cb3\u0cb5\u0cbe\u0cb0', '\u0cac\u0cc1\u0ca7\u0cb5\u0cbe\u0cb0', '\u0c97\u0cc1\u0cb0\u0cc1\u0cb5\u0cbe\u0cb0', '\u0cb6\u0cc1\u0c95\u0ccd\u0cb0\u0cb5\u0cbe\u0cb0', '\u0cb6\u0ca8\u0cbf\u0cb5\u0cbe\u0cb0'],
+  SHORTWEEKDAYS: ['\u0cb0.', '\u0cb8\u0ccb.', '\u0cae\u0c82.', '\u0cac\u0cc1.', '\u0c97\u0cc1.', '\u0cb6\u0cc1.', '\u0cb6\u0ca8\u0cbf.'],
+  NARROWWEEKDAYS: ['\u0cb0', '\u0cb8\u0ccb', '\u0cae\u0c82', '\u0cac\u0cc1', '\u0c97\u0cc1', '\u0cb6\u0cc1', '\u0cb6'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0c92\u0c82\u0ca6\u0cc1 1', '\u0c8e\u0cb0\u0ca1\u0cc1 2', '\u0cae\u0cc2\u0cb0\u0cc1 3', '\u0ca8\u0cbe\u0cb2\u0cc3\u0c95 4'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'd-M-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ko.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ko.js
new file mode 100644
index 0000000..5c6a970
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ko.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\uae30\uc6d0\uc804', '\uc11c\uae30'],
+  ERANAMES: ['\uc11c\ub825\uae30\uc6d0\uc804', '\uc11c\ub825\uae30\uc6d0'],
+  NARROWMONTHS: ['1\uc6d4', '2\uc6d4', '3\uc6d4', '4\uc6d4', '5\uc6d4', '6\uc6d4', '7\uc6d4', '8\uc6d4', '9\uc6d4', '10\uc6d4', '11\uc6d4', '12\uc6d4'],
+  MONTHS: ['1\uc6d4', '2\uc6d4', '3\uc6d4', '4\uc6d4', '5\uc6d4', '6\uc6d4', '7\uc6d4', '8\uc6d4', '9\uc6d4', '10\uc6d4', '11\uc6d4', '12\uc6d4'],
+  SHORTMONTHS: ['1\uc6d4', '2\uc6d4', '3\uc6d4', '4\uc6d4', '5\uc6d4', '6\uc6d4', '7\uc6d4', '8\uc6d4', '9\uc6d4', '10\uc6d4', '11\uc6d4', '12\uc6d4'],
+  WEEKDAYS: ['\uc77c\uc694\uc77c', '\uc6d4\uc694\uc77c', '\ud654\uc694\uc77c', '\uc218\uc694\uc77c', '\ubaa9\uc694\uc77c', '\uae08\uc694\uc77c', '\ud1a0\uc694\uc77c'],
+  SHORTWEEKDAYS: ['\uc77c', '\uc6d4', '\ud654', '\uc218', '\ubaa9', '\uae08', '\ud1a0'],
+  NARROWWEEKDAYS: ['\uc77c', '\uc6d4', '\ud654', '\uc218', '\ubaa9', '\uae08', '\ud1a0'],
+  SHORTQUARTERS: ['1\ubd84\uae30', '2\ubd84\uae30', '3\ubd84\uae30', '4\ubd84\uae30'],
+  QUARTERS: ['\uc81c 1/4\ubd84\uae30', '\uc81c 2/4\ubd84\uae30', '\uc81c 3/4\ubd84\uae30', '\uc81c 4/4\ubd84\uae30'],
+  AMPMS: ['\uc624\uc804', '\uc624\ud6c4'],
+  DATEFORMATS: ['y\ub144 M\uc6d4 d\uc77c EEEE', 'y\ub144 M\uc6d4 d\uc77c', 'yyyy. M. d.', 'yy. M. d.'],
+  TIMEFORMATS: ['a hh\uc2dc mm\ubd84 ss\ucd08 zzzz', 'a hh\uc2dc mm\ubd84 ss\ucd08 z', 'a h:mm:ss', 'a h:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ko_KR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ko_KR.js
new file mode 100644
index 0000000..5c6a970
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ko_KR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\uae30\uc6d0\uc804', '\uc11c\uae30'],
+  ERANAMES: ['\uc11c\ub825\uae30\uc6d0\uc804', '\uc11c\ub825\uae30\uc6d0'],
+  NARROWMONTHS: ['1\uc6d4', '2\uc6d4', '3\uc6d4', '4\uc6d4', '5\uc6d4', '6\uc6d4', '7\uc6d4', '8\uc6d4', '9\uc6d4', '10\uc6d4', '11\uc6d4', '12\uc6d4'],
+  MONTHS: ['1\uc6d4', '2\uc6d4', '3\uc6d4', '4\uc6d4', '5\uc6d4', '6\uc6d4', '7\uc6d4', '8\uc6d4', '9\uc6d4', '10\uc6d4', '11\uc6d4', '12\uc6d4'],
+  SHORTMONTHS: ['1\uc6d4', '2\uc6d4', '3\uc6d4', '4\uc6d4', '5\uc6d4', '6\uc6d4', '7\uc6d4', '8\uc6d4', '9\uc6d4', '10\uc6d4', '11\uc6d4', '12\uc6d4'],
+  WEEKDAYS: ['\uc77c\uc694\uc77c', '\uc6d4\uc694\uc77c', '\ud654\uc694\uc77c', '\uc218\uc694\uc77c', '\ubaa9\uc694\uc77c', '\uae08\uc694\uc77c', '\ud1a0\uc694\uc77c'],
+  SHORTWEEKDAYS: ['\uc77c', '\uc6d4', '\ud654', '\uc218', '\ubaa9', '\uae08', '\ud1a0'],
+  NARROWWEEKDAYS: ['\uc77c', '\uc6d4', '\ud654', '\uc218', '\ubaa9', '\uae08', '\ud1a0'],
+  SHORTQUARTERS: ['1\ubd84\uae30', '2\ubd84\uae30', '3\ubd84\uae30', '4\ubd84\uae30'],
+  QUARTERS: ['\uc81c 1/4\ubd84\uae30', '\uc81c 2/4\ubd84\uae30', '\uc81c 3/4\ubd84\uae30', '\uc81c 4/4\ubd84\uae30'],
+  AMPMS: ['\uc624\uc804', '\uc624\ud6c4'],
+  DATEFORMATS: ['y\ub144 M\uc6d4 d\uc77c EEEE', 'y\ub144 M\uc6d4 d\uc77c', 'yyyy. M. d.', 'yy. M. d.'],
+  TIMEFORMATS: ['a hh\uc2dc mm\ubd84 ss\ucd08 zzzz', 'a hh\uc2dc mm\ubd84 ss\ucd08 z', 'a h:mm:ss', 'a h:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kok.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kok.js
new file mode 100644
index 0000000..1a8dbfc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kok.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u092a\u0942\u0930\u094d\u0935', '\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u0936\u0916\u093e'],
+  ERANAMES: ['\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u092a\u0942\u0930\u094d\u0935', '\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u0936\u0916\u093e'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0913\u0917\u0938\u094d\u091f', '\u0938\u0947\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0913\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u0943\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0913\u0917\u0938\u094d\u091f', '\u0938\u0947\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0913\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  WEEKDAYS: ['\u0906\u0926\u093f\u0924\u094d\u092f\u0935\u093e\u0930', '\u0938\u094b\u092e\u0935\u093e\u0930', '\u092e\u0902\u0917\u0933\u093e\u0930', '\u092c\u0941\u0927\u0935\u093e\u0930', '\u0917\u0941\u0930\u0941\u0935\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930', '\u0936\u0928\u093f\u0935\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0930\u0935\u093f', '\u0938\u094b\u092e', '\u092e\u0902\u0917\u0933', '\u092c\u0941\u0927', '\u0917\u0941\u0930\u0941', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u092e.\u092a\u0942.', '\u092e.\u0928\u0902.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kok_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kok_IN.js
new file mode 100644
index 0000000..1a8dbfc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kok_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u092a\u0942\u0930\u094d\u0935', '\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u0936\u0916\u093e'],
+  ERANAMES: ['\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u092a\u0942\u0930\u094d\u0935', '\u0915\u094d\u0930\u093f\u0938\u094d\u0924\u0936\u0916\u093e'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0913\u0917\u0938\u094d\u091f', '\u0938\u0947\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0913\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u0943\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0913\u0917\u0938\u094d\u091f', '\u0938\u0947\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0913\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  WEEKDAYS: ['\u0906\u0926\u093f\u0924\u094d\u092f\u0935\u093e\u0930', '\u0938\u094b\u092e\u0935\u093e\u0930', '\u092e\u0902\u0917\u0933\u093e\u0930', '\u092c\u0941\u0927\u0935\u093e\u0930', '\u0917\u0941\u0930\u0941\u0935\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930', '\u0936\u0928\u093f\u0935\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0930\u0935\u093f', '\u0938\u094b\u092e', '\u092e\u0902\u0917\u0933', '\u092c\u0941\u0927', '\u0917\u0941\u0930\u0941', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u092e.\u092a\u0942.', '\u092e.\u0928\u0902.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe.js
new file mode 100644
index 0000000..2f5f5af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe_GN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe_GN.js
new file mode 100644
index 0000000..2f5f5af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe_GN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe_LR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe_LR.js
new file mode 100644
index 0000000..2f5f5af
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kpe_LR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku.js
new file mode 100644
index 0000000..e6c3b2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  SHORTWEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Arab.js
new file mode 100644
index 0000000..e6c3b2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Arab.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  SHORTWEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_IQ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_IQ.js
new file mode 100644
index 0000000..e6c3b2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_IQ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  SHORTWEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_IR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_IR.js
new file mode 100644
index 0000000..ad2a796
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_IR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  SHORTWEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Latn.js
new file mode 100644
index 0000000..4d78962
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Latn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BZ', 'PZ'],
+  ERANAMES: ['BZ', 'PZ'],
+  NARROWMONTHS: ['\u00e7', 's', 'a', 'n', 'g', 'h', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u00e7ile', 'sibat', 'adar', 'n\u00eesan', 'gulan', 'hez\u00eeran', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['\u00e7il', 'sib', 'adr', 'n\u00ees', 'gul', 'hez', 't\u00eer', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['yek\u015fem', 'du\u015fem', '\u015f\u00ea', '\u00e7ar\u015fem', 'p\u00eanc\u015fem', '\u00een', '\u015fem\u00ee'],
+  SHORTWEEKDAYS: ['y\u015f', 'd\u015f', 's\u015f', '\u00e7\u015f', 'p\u015f', '\u00een', '\u015f'],
+  NARROWWEEKDAYS: ['y', 'd', 's', '\u00e7', 'p', '\u00ee', '\u015f'],
+  SHORTQUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  QUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  AMPMS: ['BN', 'PN'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Latn_TR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Latn_TR.js
new file mode 100644
index 0000000..4d78962
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_Latn_TR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BZ', 'PZ'],
+  ERANAMES: ['BZ', 'PZ'],
+  NARROWMONTHS: ['\u00e7', 's', 'a', 'n', 'g', 'h', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u00e7ile', 'sibat', 'adar', 'n\u00eesan', 'gulan', 'hez\u00eeran', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['\u00e7il', 'sib', 'adr', 'n\u00ees', 'gul', 'hez', 't\u00eer', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['yek\u015fem', 'du\u015fem', '\u015f\u00ea', '\u00e7ar\u015fem', 'p\u00eanc\u015fem', '\u00een', '\u015fem\u00ee'],
+  SHORTWEEKDAYS: ['y\u015f', 'd\u015f', 's\u015f', '\u00e7\u015f', 'p\u015f', '\u00een', '\u015f'],
+  NARROWWEEKDAYS: ['y', 'd', 's', '\u00e7', 'p', '\u00ee', '\u015f'],
+  SHORTQUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  QUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  AMPMS: ['BN', 'PN'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_SY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_SY.js
new file mode 100644
index 0000000..23507ef
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_SY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  SHORTWEEKDAYS: ['\u06cc\u06d5\u06a9\u0634\u06d5\u0645\u0645\u06d5', '\u062f\u0648\u0648\u0634\u06d5\u0645\u0645\u06d5', '\u0633\u06ce\u0634\u06d5\u0645\u0645\u06d5', '\u0686\u0648\u0627\u0631\u0634\u06d5\u0645\u0645\u06d5', '5', '6', '7'],
+  NARROWWEEKDAYS: ['\u06cc', '\u062f', '\u0633', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_TR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_TR.js
new file mode 100644
index 0000000..4d78962
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ku_TR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BZ', 'PZ'],
+  ERANAMES: ['BZ', 'PZ'],
+  NARROWMONTHS: ['\u00e7', 's', 'a', 'n', 'g', 'h', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u00e7ile', 'sibat', 'adar', 'n\u00eesan', 'gulan', 'hez\u00eeran', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['\u00e7il', 'sib', 'adr', 'n\u00ees', 'gul', 'hez', 't\u00eer', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['yek\u015fem', 'du\u015fem', '\u015f\u00ea', '\u00e7ar\u015fem', 'p\u00eanc\u015fem', '\u00een', '\u015fem\u00ee'],
+  SHORTWEEKDAYS: ['y\u015f', 'd\u015f', 's\u015f', '\u00e7\u015f', 'p\u015f', '\u00een', '\u015f'],
+  NARROWWEEKDAYS: ['y', 'd', 's', '\u00e7', 'p', '\u00ee', '\u015f'],
+  SHORTQUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  QUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  AMPMS: ['BN', 'PN'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kw.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kw.js
new file mode 100644
index 0000000..2ee2abe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kw.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['RC', 'AD'],
+  ERANAMES: ['RC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Mys Genver', 'Mys Whevrel', 'Mys Merth', 'Mys Ebrel', 'Mys Me', 'Mys Efan', 'Mys Gortheren', 'Mye Est', 'Mys Gwyngala', 'Mys Hedra', 'Mys Du', 'Mys Kevardhu'],
+  SHORTMONTHS: ['Gen', 'Whe', 'Mer', 'Ebr', 'Me', 'Efn', 'Gor', 'Est', 'Gwn', 'Hed', 'Du', 'Kev'],
+  WEEKDAYS: ['De Sul', 'De Lun', 'De Merth', 'De Merher', 'De Yow', 'De Gwener', 'De Sadorn'],
+  SHORTWEEKDAYS: ['Sul', 'Lun', 'Mth', 'Mhr', 'Yow', 'Gwe', 'Sad'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kw_GB.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kw_GB.js
new file mode 100644
index 0000000..2ee2abe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__kw_GB.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['RC', 'AD'],
+  ERANAMES: ['RC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Mys Genver', 'Mys Whevrel', 'Mys Merth', 'Mys Ebrel', 'Mys Me', 'Mys Efan', 'Mys Gortheren', 'Mye Est', 'Mys Gwyngala', 'Mys Hedra', 'Mys Du', 'Mys Kevardhu'],
+  SHORTMONTHS: ['Gen', 'Whe', 'Mer', 'Ebr', 'Me', 'Efn', 'Gor', 'Est', 'Gwn', 'Hed', 'Du', 'Kev'],
+  WEEKDAYS: ['De Sul', 'De Lun', 'De Merth', 'De Merher', 'De Yow', 'De Gwener', 'De Sadorn'],
+  SHORTWEEKDAYS: ['Sul', 'Lun', 'Mth', 'Mhr', 'Yow', 'Gwe', 'Sad'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['a.m.', 'p.m.'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ky.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ky.js
new file mode 100644
index 0000000..ecaa8a6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ky.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ky_KG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ky_KG.js
new file mode 100644
index 0000000..ecaa8a6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ky_KG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln.js
new file mode 100644
index 0000000..3bd90b2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['libos\u00f3 ya Y.-K.', 'nsima ya Y.-K.'],
+  ERANAMES: ['libos\u00f3 ya Y.-K.', 'nsima ya Y.-K.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['s\u00e1nz\u00e1 ya yambo', 's\u00e1nz\u00e1 ya m\u00edbal\u00e9', 's\u00e1nz\u00e1 ya m\u00eds\u00e1to', 's\u00e1nz\u00e1 ya m\u00ednei', 's\u00e1nz\u00e1 ya m\u00edt\u00e1no', 's\u00e1nz\u00e1 ya mot\u00f3b\u00e1', 's\u00e1nz\u00e1 ya nsambo', 's\u00e1nz\u00e1 ya mwambe', 's\u00e1nz\u00e1 ya libwa', 's\u00e1nz\u00e1 ya z\u00f3mi', 's\u00e1nz\u00e1 ya z\u00f3mi na m\u0254\u030ck\u0254\u0301', 's\u00e1nz\u00e1 ya z\u00f3mi na m\u00edbal\u00e9'],
+  SHORTMONTHS: ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11', 's12'],
+  WEEKDAYS: ['eyenga', 'mok\u0254l\u0254 ya libos\u00f3', 'mok\u0254l\u0254 ya m\u00edbal\u00e9', 'mok\u0254l\u0254 ya m\u00eds\u00e1to', 'mok\u0254l\u0254 ya m\u00edn\u00e9i', 'mok\u0254l\u0254 ya m\u00edt\u00e1no', 'mp\u0254\u0301s\u0254'],
+  SHORTWEEKDAYS: ['eye', 'm1', 'm2', 'm3', 'm4', 'm5', 'mps'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['SM1', 'SM2', 'SM3', 'SM4'],
+  QUARTERS: ['s\u00e1nz\u00e1 m\u00eds\u00e1to ya yambo', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00edbal\u00e9', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00eds\u00e1to', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00ednei'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln_CD.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln_CD.js
new file mode 100644
index 0000000..3bd90b2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln_CD.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['libos\u00f3 ya Y.-K.', 'nsima ya Y.-K.'],
+  ERANAMES: ['libos\u00f3 ya Y.-K.', 'nsima ya Y.-K.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['s\u00e1nz\u00e1 ya yambo', 's\u00e1nz\u00e1 ya m\u00edbal\u00e9', 's\u00e1nz\u00e1 ya m\u00eds\u00e1to', 's\u00e1nz\u00e1 ya m\u00ednei', 's\u00e1nz\u00e1 ya m\u00edt\u00e1no', 's\u00e1nz\u00e1 ya mot\u00f3b\u00e1', 's\u00e1nz\u00e1 ya nsambo', 's\u00e1nz\u00e1 ya mwambe', 's\u00e1nz\u00e1 ya libwa', 's\u00e1nz\u00e1 ya z\u00f3mi', 's\u00e1nz\u00e1 ya z\u00f3mi na m\u0254\u030ck\u0254\u0301', 's\u00e1nz\u00e1 ya z\u00f3mi na m\u00edbal\u00e9'],
+  SHORTMONTHS: ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11', 's12'],
+  WEEKDAYS: ['eyenga', 'mok\u0254l\u0254 ya libos\u00f3', 'mok\u0254l\u0254 ya m\u00edbal\u00e9', 'mok\u0254l\u0254 ya m\u00eds\u00e1to', 'mok\u0254l\u0254 ya m\u00edn\u00e9i', 'mok\u0254l\u0254 ya m\u00edt\u00e1no', 'mp\u0254\u0301s\u0254'],
+  SHORTWEEKDAYS: ['eye', 'm1', 'm2', 'm3', 'm4', 'm5', 'mps'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['SM1', 'SM2', 'SM3', 'SM4'],
+  QUARTERS: ['s\u00e1nz\u00e1 m\u00eds\u00e1to ya yambo', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00edbal\u00e9', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00eds\u00e1to', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00ednei'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln_CG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln_CG.js
new file mode 100644
index 0000000..3bd90b2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ln_CG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['libos\u00f3 ya Y.-K.', 'nsima ya Y.-K.'],
+  ERANAMES: ['libos\u00f3 ya Y.-K.', 'nsima ya Y.-K.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['s\u00e1nz\u00e1 ya yambo', 's\u00e1nz\u00e1 ya m\u00edbal\u00e9', 's\u00e1nz\u00e1 ya m\u00eds\u00e1to', 's\u00e1nz\u00e1 ya m\u00ednei', 's\u00e1nz\u00e1 ya m\u00edt\u00e1no', 's\u00e1nz\u00e1 ya mot\u00f3b\u00e1', 's\u00e1nz\u00e1 ya nsambo', 's\u00e1nz\u00e1 ya mwambe', 's\u00e1nz\u00e1 ya libwa', 's\u00e1nz\u00e1 ya z\u00f3mi', 's\u00e1nz\u00e1 ya z\u00f3mi na m\u0254\u030ck\u0254\u0301', 's\u00e1nz\u00e1 ya z\u00f3mi na m\u00edbal\u00e9'],
+  SHORTMONTHS: ['s1', 's2', 's3', 's4', 's5', 's6', 's7', 's8', 's9', 's10', 's11', 's12'],
+  WEEKDAYS: ['eyenga', 'mok\u0254l\u0254 ya libos\u00f3', 'mok\u0254l\u0254 ya m\u00edbal\u00e9', 'mok\u0254l\u0254 ya m\u00eds\u00e1to', 'mok\u0254l\u0254 ya m\u00edn\u00e9i', 'mok\u0254l\u0254 ya m\u00edt\u00e1no', 'mp\u0254\u0301s\u0254'],
+  SHORTWEEKDAYS: ['eye', 'm1', 'm2', 'm3', 'm4', 'm5', 'mps'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['SM1', 'SM2', 'SM3', 'SM4'],
+  QUARTERS: ['s\u00e1nz\u00e1 m\u00eds\u00e1to ya yambo', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00edbal\u00e9', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00eds\u00e1to', 's\u00e1nz\u00e1 m\u00eds\u00e1to ya m\u00ednei'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lo.js
new file mode 100644
index 0000000..c1adf1a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0e9b\u0eb5\u0e81\u0ec8\u0ead\u0e99\u0e84\u0eb4\u0e94\u0eaa\u0eb0\u0e81\u0eb2\u0e99\u0e97\u0eb5\u0ec8', '\u0e84.\u0eaa.'],
+  ERANAMES: ['\u0e9b\u0eb5\u0e81\u0ec8\u0ead\u0e99\u0e84\u0eb4\u0e94\u0eaa\u0eb0\u0e81\u0eb2\u0e99\u0e97\u0eb5\u0ec8', '\u0e84.\u0eaa.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99', '\u0e81\u0eb8\u0ea1\u0e9e\u0eb2', '\u0ea1\u0eb5\u0e99\u0eb2', '\u0ec0\u0ea1\u0eaa\u0eb2', '\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2', '\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2', '\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94', '\u0eaa\u0eb4\u0e87\u0eab\u0eb2', '\u0e81\u0eb1\u0e99\u0e8d\u0eb2', '\u0e95\u0eb8\u0ea5\u0eb2', '\u0e9e\u0eb0\u0e88\u0eb4\u0e81', '\u0e97\u0eb1\u0e99\u0ea7\u0eb2'],
+  SHORTMONTHS: ['\u0ea1.\u0e81.', '\u0e81.\u0e9e.', '\u0ea1\u0eb5.\u0e99.', '\u0ea1.\u0eaa..', '\u0e9e.\u0e9e.', '\u0ea1\u0eb4.\u0e96.', '\u0e81.\u0ea5.', '\u0eaa.\u0eab.', '\u0e81.\u0e8d.', '\u0e95.\u0ea5.', '\u0e9e.\u0e88.', '\u0e97.\u0ea7.'],
+  WEEKDAYS: ['\u0ea7\u0eb1\u0e99\u0ead\u0eb2\u0e97\u0eb4\u0e94', '\u0ea7\u0eb1\u0e99\u0e88\u0eb1\u0e99', '\u0ea7\u0eb1\u0e99\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99', '\u0ea7\u0eb1\u0e99\u0e9e\u0eb8\u0e94', '\u0ea7\u0eb1\u0e99\u0e9e\u0eb0\u0eab\u0eb1\u0e94', '\u0ea7\u0eb1\u0e99\u0eaa\u0eb8\u0e81', '\u0ea7\u0eb1\u0e99\u0ec0\u0eaa\u0ebb\u0eb2'],
+  SHORTWEEKDAYS: ['\u0ead\u0eb2.', '\u0e88.', '\u0ead.', '\u0e9e.', '\u0e9e\u0eab.', '\u0eaa\u0e81.', '\u0eaa.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u0e81\u0ec8\u0ead\u0e99\u0e97\u0ec8\u0ebd\u0e87', '\u0eab\u0ea5\u0eb1\u0e87\u0e97\u0ec8\u0ebd\u0e87'],
+  DATEFORMATS: ['EEEE\u0e97\u0eb5 d MMMM G y', 'd MMMM y', 'd MMM y', 'd/M/yyyy'],
+  TIMEFORMATS: ['H\u0ec2\u0ea1\u0e87 m\u0e99\u0eb2\u0e97\u0eb5 ss \u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5zzzz', 'H \u0ec2\u0ea1\u0e87 m\u0e99\u0eb2\u0e97\u0eb5ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lo_LA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lo_LA.js
new file mode 100644
index 0000000..c1adf1a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lo_LA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0e9b\u0eb5\u0e81\u0ec8\u0ead\u0e99\u0e84\u0eb4\u0e94\u0eaa\u0eb0\u0e81\u0eb2\u0e99\u0e97\u0eb5\u0ec8', '\u0e84.\u0eaa.'],
+  ERANAMES: ['\u0e9b\u0eb5\u0e81\u0ec8\u0ead\u0e99\u0e84\u0eb4\u0e94\u0eaa\u0eb0\u0e81\u0eb2\u0e99\u0e97\u0eb5\u0ec8', '\u0e84.\u0eaa.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0ea1\u0eb1\u0e87\u0e81\u0ead\u0e99', '\u0e81\u0eb8\u0ea1\u0e9e\u0eb2', '\u0ea1\u0eb5\u0e99\u0eb2', '\u0ec0\u0ea1\u0eaa\u0eb2', '\u0e9e\u0eb6\u0e94\u0eaa\u0eb0\u0e9e\u0eb2', '\u0ea1\u0eb4\u0e96\u0eb8\u0e99\u0eb2', '\u0e81\u0ecd\u0ea5\u0eb0\u0e81\u0ebb\u0e94', '\u0eaa\u0eb4\u0e87\u0eab\u0eb2', '\u0e81\u0eb1\u0e99\u0e8d\u0eb2', '\u0e95\u0eb8\u0ea5\u0eb2', '\u0e9e\u0eb0\u0e88\u0eb4\u0e81', '\u0e97\u0eb1\u0e99\u0ea7\u0eb2'],
+  SHORTMONTHS: ['\u0ea1.\u0e81.', '\u0e81.\u0e9e.', '\u0ea1\u0eb5.\u0e99.', '\u0ea1.\u0eaa..', '\u0e9e.\u0e9e.', '\u0ea1\u0eb4.\u0e96.', '\u0e81.\u0ea5.', '\u0eaa.\u0eab.', '\u0e81.\u0e8d.', '\u0e95.\u0ea5.', '\u0e9e.\u0e88.', '\u0e97.\u0ea7.'],
+  WEEKDAYS: ['\u0ea7\u0eb1\u0e99\u0ead\u0eb2\u0e97\u0eb4\u0e94', '\u0ea7\u0eb1\u0e99\u0e88\u0eb1\u0e99', '\u0ea7\u0eb1\u0e99\u0ead\u0eb1\u0e87\u0e84\u0eb2\u0e99', '\u0ea7\u0eb1\u0e99\u0e9e\u0eb8\u0e94', '\u0ea7\u0eb1\u0e99\u0e9e\u0eb0\u0eab\u0eb1\u0e94', '\u0ea7\u0eb1\u0e99\u0eaa\u0eb8\u0e81', '\u0ea7\u0eb1\u0e99\u0ec0\u0eaa\u0ebb\u0eb2'],
+  SHORTWEEKDAYS: ['\u0ead\u0eb2.', '\u0e88.', '\u0ead.', '\u0e9e.', '\u0e9e\u0eab.', '\u0eaa\u0e81.', '\u0eaa.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u0e81\u0ec8\u0ead\u0e99\u0e97\u0ec8\u0ebd\u0e87', '\u0eab\u0ea5\u0eb1\u0e87\u0e97\u0ec8\u0ebd\u0e87'],
+  DATEFORMATS: ['EEEE\u0e97\u0eb5 d MMMM G y', 'd MMMM y', 'd MMM y', 'd/M/yyyy'],
+  TIMEFORMATS: ['H\u0ec2\u0ea1\u0e87 m\u0e99\u0eb2\u0e97\u0eb5 ss \u0ea7\u0eb4\u0e99\u0eb2\u0e97\u0eb5zzzz', 'H \u0ec2\u0ea1\u0e87 m\u0e99\u0eb2\u0e97\u0eb5ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lt.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lt.js
new file mode 100644
index 0000000..d0574f6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lt.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pr. Kr.', 'po Kr.'],
+  ERANAMES: ['prie\u0161 Krist\u0173', 'po Kristaus'],
+  NARROWMONTHS: ['S', 'V', 'K', 'B', 'G', 'B', 'L', 'R', 'R', 'S', 'L', 'G'],
+  MONTHS: ['sausio', 'vasario', 'kovo', 'baland\u017eio', 'gegu\u017e\u0117s', 'bir\u017eelio', 'liepos', 'rugpj\u016b\u010dio', 'rugs\u0117jo', 'spalio', 'lapkri\u010dio', 'gruod\u017eio'],
+  STANDALONEMONTHS: ['Sausis', 'Vasaris', 'Kovas', 'Balandis', 'Gegu\u017e\u0117', 'Bir\u017eelis', 'Liepa', 'Rugpj\u016btis', 'Rugs\u0117jis', 'Spalis', 'Lapkritis', 'Gruodis'],
+  SHORTMONTHS: ['Sau', 'Vas', 'Kov', 'Bal', 'Geg', 'Bir', 'Lie', 'Rgp', 'Rgs', 'Spl', 'Lap', 'Grd'],
+  WEEKDAYS: ['sekmadienis', 'pirmadienis', 'antradienis', 'tre\u010diadienis', 'ketvirtadienis', 'penktadienis', '\u0161e\u0161tadienis'],
+  SHORTWEEKDAYS: ['Sk', 'Pr', 'An', 'Tr', 'Kt', 'Pn', '\u0160t'],
+  NARROWWEEKDAYS: ['S', 'P', 'A', 'T', 'K', 'P', '\u0160'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['pirmas ketvirtis', 'antras ketvirtis', 'tre\u010dias ketvirtis', 'ketvirtas ketvirtis'],
+  AMPMS: ['prie\u0161piet', 'popiet'],
+  DATEFORMATS: ["y 'm'. MMMM d 'd'.,EEEE", "y 'm'. MMMM d 'd'.", 'yyyy.MM.dd', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lt_LT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lt_LT.js
new file mode 100644
index 0000000..d0574f6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lt_LT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pr. Kr.', 'po Kr.'],
+  ERANAMES: ['prie\u0161 Krist\u0173', 'po Kristaus'],
+  NARROWMONTHS: ['S', 'V', 'K', 'B', 'G', 'B', 'L', 'R', 'R', 'S', 'L', 'G'],
+  MONTHS: ['sausio', 'vasario', 'kovo', 'baland\u017eio', 'gegu\u017e\u0117s', 'bir\u017eelio', 'liepos', 'rugpj\u016b\u010dio', 'rugs\u0117jo', 'spalio', 'lapkri\u010dio', 'gruod\u017eio'],
+  STANDALONEMONTHS: ['Sausis', 'Vasaris', 'Kovas', 'Balandis', 'Gegu\u017e\u0117', 'Bir\u017eelis', 'Liepa', 'Rugpj\u016btis', 'Rugs\u0117jis', 'Spalis', 'Lapkritis', 'Gruodis'],
+  SHORTMONTHS: ['Sau', 'Vas', 'Kov', 'Bal', 'Geg', 'Bir', 'Lie', 'Rgp', 'Rgs', 'Spl', 'Lap', 'Grd'],
+  WEEKDAYS: ['sekmadienis', 'pirmadienis', 'antradienis', 'tre\u010diadienis', 'ketvirtadienis', 'penktadienis', '\u0161e\u0161tadienis'],
+  SHORTWEEKDAYS: ['Sk', 'Pr', 'An', 'Tr', 'Kt', 'Pn', '\u0160t'],
+  NARROWWEEKDAYS: ['S', 'P', 'A', 'T', 'K', 'P', '\u0160'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['pirmas ketvirtis', 'antras ketvirtis', 'tre\u010dias ketvirtis', 'ketvirtas ketvirtis'],
+  AMPMS: ['prie\u0161piet', 'popiet'],
+  DATEFORMATS: ["y 'm'. MMMM d 'd'.,EEEE", "y 'm'. MMMM d 'd'.", 'yyyy.MM.dd', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lv.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lv.js
new file mode 100644
index 0000000..7997ef0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lv.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p.m.\u0113.', 'm.\u0113.'],
+  ERANAMES: ['pirms m\u016bsu \u0113ras', 'm\u016bsu \u0113r\u0101'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janv\u0101ris', 'febru\u0101ris', 'marts', 'apr\u012blis', 'maijs', 'j\u016bnijs', 'j\u016blijs', 'augusts', 'septembris', 'oktobris', 'novembris', 'decembris'],
+  SHORTMONTHS: ['janv.', 'febr.', 'marts', 'apr.', 'maijs', 'j\u016bn.', 'j\u016bl.', 'aug.', 'sept.', 'okt.', 'nov.', 'dec.'],
+  WEEKDAYS: ['sv\u0113tdiena', 'pirmdiena', 'otrdiena', 'tre\u0161diena', 'ceturtdiena', 'piektdiena', 'sestdiena'],
+  SHORTWEEKDAYS: ['Sv', 'Pr', 'Ot', 'Tr', 'Ce', 'Pk', 'Se'],
+  NARROWWEEKDAYS: ['S', 'P', 'O', 'T', 'C', 'P', 'S'],
+  SHORTQUARTERS: ['C1', 'C2', 'C3', 'C4'],
+  QUARTERS: ['1. ceturksnis', '2. ceturksnis', '3. ceturksnis', '4. ceturksnis'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, y. 'gada' d. MMMM", "y. 'gada' d. MMMM", "y. 'gada' d. MMM", 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lv_LV.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lv_LV.js
new file mode 100644
index 0000000..7997ef0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__lv_LV.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p.m.\u0113.', 'm.\u0113.'],
+  ERANAMES: ['pirms m\u016bsu \u0113ras', 'm\u016bsu \u0113r\u0101'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janv\u0101ris', 'febru\u0101ris', 'marts', 'apr\u012blis', 'maijs', 'j\u016bnijs', 'j\u016blijs', 'augusts', 'septembris', 'oktobris', 'novembris', 'decembris'],
+  SHORTMONTHS: ['janv.', 'febr.', 'marts', 'apr.', 'maijs', 'j\u016bn.', 'j\u016bl.', 'aug.', 'sept.', 'okt.', 'nov.', 'dec.'],
+  WEEKDAYS: ['sv\u0113tdiena', 'pirmdiena', 'otrdiena', 'tre\u0161diena', 'ceturtdiena', 'piektdiena', 'sestdiena'],
+  SHORTWEEKDAYS: ['Sv', 'Pr', 'Ot', 'Tr', 'Ce', 'Pk', 'Se'],
+  NARROWWEEKDAYS: ['S', 'P', 'O', 'T', 'C', 'P', 'S'],
+  SHORTQUARTERS: ['C1', 'C2', 'C3', 'C4'],
+  QUARTERS: ['1. ceturksnis', '2. ceturksnis', '3. ceturksnis', '4. ceturksnis'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, y. 'gada' d. MMMM", "y. 'gada' d. MMMM", "y. 'gada' d. MMM", 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mk.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mk.js
new file mode 100644
index 0000000..bd215a7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mk.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f\u0440.\u043d.\u0435.', '\u0430\u0435.'],
+  ERANAMES: ['\u043f\u0440.\u043d.\u0435.', '\u0430\u0435.'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440\u0438', '\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d\u0438', '\u0458\u0443\u043b\u0438', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438', '\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438', '\u043d\u043e\u0435\u043c\u0432\u0440\u0438', '\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438'],
+  SHORTMONTHS: ['\u0458\u0430\u043d.', '\u0444\u0435\u0432.', '\u043c\u0430\u0440.', '\u0430\u043f\u0440.', '\u043c\u0430\u0458', '\u0458\u0443\u043d.', '\u0458\u0443\u043b.', '\u0430\u0432\u0433.', '\u0441\u0435\u043f\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u0435\u043c.', '\u0434\u0435\u043a\u0435\u043c.'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u043b\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a', '\u0432\u0442\u043e\u0440\u043d\u0438\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u043e\u043a', '\u043f\u0435\u0442\u043e\u043a', '\u0441\u0430\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434.', '\u043f\u043e\u043d.', '\u0432\u0442.', '\u0441\u0440\u0435.', '\u0447\u0435\u0442.', '\u043f\u0435\u0442.', '\u0441\u0430\u0431.'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0432', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u043f\u0440\u0435\u0442\u043f\u043b\u0430\u0434\u043d\u0435', '\u043f\u043e\u043f\u043b\u0430\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'dd.M.yyyy', 'dd.M.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mk_MK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mk_MK.js
new file mode 100644
index 0000000..bd215a7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mk_MK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f\u0440.\u043d.\u0435.', '\u0430\u0435.'],
+  ERANAMES: ['\u043f\u0440.\u043d.\u0435.', '\u0430\u0435.'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440\u0438', '\u0444\u0435\u0432\u0440\u0443\u0430\u0440\u0438', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d\u0438', '\u0458\u0443\u043b\u0438', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0432\u0440\u0438', '\u043e\u043a\u0442\u043e\u043c\u0432\u0440\u0438', '\u043d\u043e\u0435\u043c\u0432\u0440\u0438', '\u0434\u0435\u043a\u0435\u043c\u0432\u0440\u0438'],
+  SHORTMONTHS: ['\u0458\u0430\u043d.', '\u0444\u0435\u0432.', '\u043c\u0430\u0440.', '\u0430\u043f\u0440.', '\u043c\u0430\u0458', '\u0458\u0443\u043d.', '\u0458\u0443\u043b.', '\u0430\u0432\u0433.', '\u0441\u0435\u043f\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u0435\u043c.', '\u0434\u0435\u043a\u0435\u043c.'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u043b\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u043d\u0438\u043a', '\u0432\u0442\u043e\u0440\u043d\u0438\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u043e\u043a', '\u043f\u0435\u0442\u043e\u043a', '\u0441\u0430\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434.', '\u043f\u043e\u043d.', '\u0432\u0442.', '\u0441\u0440\u0435.', '\u0447\u0435\u0442.', '\u043f\u0435\u0442.', '\u0441\u0430\u0431.'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0432', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u043f\u0440\u0435\u0442\u043f\u043b\u0430\u0434\u043d\u0435', '\u043f\u043e\u043f\u043b\u0430\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'dd.M.yyyy', 'dd.M.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ml.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ml.js
new file mode 100644
index 0000000..e70f7f5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ml.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0d15\u0d4d\u0d30\u0d3f.\u0d2e\u0d41.', '\u0d15\u0d4d\u0d30\u0d3f.\u0d2a\u0d3f.'],
+  ERANAMES: ['\u0d15\u0d4d\u0d30\u0d3f\u0d38\u0d4d\u0d24\u0d41\u0d35\u0d3f\u0d28\u0d41\u0d4d \u0d2e\u0d41\u0d2e\u0d4d\u0d2a\u0d4d\u200c', '\u0d15\u0d4d\u0d30\u0d3f\u0d38\u0d4d\u0d24\u0d41\u0d35\u0d3f\u0d28\u0d4d \u0d2a\u0d3f\u0d28\u0d4d\u200d\u0d2a\u0d4d'],
+  NARROWMONTHS: ['\u0d1c', '\u0d2b\u0d46', '\u0d2e\u0d3e', '\u0d0f', '\u0d2e\u0d47', '\u0d1c\u0d42', '\u0d1c\u0d42', '\u0d13', '\u0d38\u0d46', '\u0d12', '\u0d28', '\u0d21\u0d3f'],
+  MONTHS: ['\u0d1c\u0d28\u0d41\u0d35\u0d30\u0d3f', '\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41\u0d35\u0d30\u0d3f', '\u0d2e\u0d3e\u0d30\u0d4d\u200d\u0d1a\u0d4d\u0d1a\u0d4d', '\u0d0f\u0d2a\u0d4d\u0d30\u0d3f\u0d32\u0d4d\u200d', '\u0d2e\u0d47\u0d2f\u0d4d', '\u0d1c\u0d42\u0d23\u0d4d\u200d', '\u0d1c\u0d42\u0d32\u0d48', '\u0d13\u0d17\u0d38\u0d4d\u0d31\u0d4d\u0d31\u0d4d', '\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31\u0d02\u0d2c\u0d30\u0d4d\u200d', '\u0d12\u0d15\u0d4d\u0d1f\u0d4b\u0d2c\u0d30\u0d4d\u200d', '\u0d28\u0d35\u0d02\u0d2c\u0d30\u0d4d\u200d', '\u0d21\u0d3f\u0d38\u0d02\u0d2c\u0d30\u0d4d\u200d'],
+  SHORTMONTHS: ['\u0d1c\u0d28\u0d41', '\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41', '\u0d2e\u0d3e\u0d30\u0d4d\u200d', '\u0d0f\u0d2a\u0d4d\u0d30\u0d3f', '\u0d2e\u0d47\u0d2f\u0d4d', '\u0d1c\u0d42\u0d23\u0d4d\u200d', '\u0d1c\u0d42\u0d32\u0d48', '\u0d13\u0d17', '\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31\u0d02', '\u0d12\u0d15\u0d4d\u0d1f\u0d4b', '\u0d28\u0d35\u0d02', '\u0d21\u0d3f\u0d38\u0d02'],
+  WEEKDAYS: ['\u0d1e\u0d3e\u0d2f\u0d31\u0d3e\u0d34\u0d4d\u0d1a', '\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d33\u0d3e\u0d34\u0d4d\u0d1a', '\u0d1a\u0d4a\u0d35\u0d4d\u0d35\u0d3e\u0d34\u0d4d\u0d1a', '\u0d2c\u0d41\u0d27\u0d28\u0d3e\u0d34\u0d4d\u0d1a', '\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d3e\u0d34\u0d4d\u0d1a', '\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a', '\u0d36\u0d28\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a'],
+  SHORTWEEKDAYS: ['\u0d1e\u0d3e\u0d2f\u0d30\u0d4d\u200d', '\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d33\u0d4d\u200d', '\u0d1a\u0d4a\u0d35\u0d4d\u0d35', '\u0d2c\u0d41\u0d27\u0d28\u0d4d\u200d', '\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d02', '\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f', '\u0d36\u0d28\u0d3f'],
+  NARROWWEEKDAYS: ['\u0d1e\u0d3e', '\u0d24\u0d3f', '\u0d1a\u0d4a', '\u0d2c\u0d41', '\u0d35\u0d4d\u0d2f\u0d3e', '\u0d35\u0d46', '\u0d36'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0d12\u0d28\u0d4d\u0d28\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02', '\u0d30\u0d23\u0d4d\u0d1f\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02', '\u0d2e\u0d42\u0d28\u0d4d\u0d28\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02', '\u0d28\u0d3e\u0d32\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['y, MMMM d, EEEE', 'y, MMMM d', 'y, MMM d', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ml_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ml_IN.js
new file mode 100644
index 0000000..e70f7f5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ml_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0d15\u0d4d\u0d30\u0d3f.\u0d2e\u0d41.', '\u0d15\u0d4d\u0d30\u0d3f.\u0d2a\u0d3f.'],
+  ERANAMES: ['\u0d15\u0d4d\u0d30\u0d3f\u0d38\u0d4d\u0d24\u0d41\u0d35\u0d3f\u0d28\u0d41\u0d4d \u0d2e\u0d41\u0d2e\u0d4d\u0d2a\u0d4d\u200c', '\u0d15\u0d4d\u0d30\u0d3f\u0d38\u0d4d\u0d24\u0d41\u0d35\u0d3f\u0d28\u0d4d \u0d2a\u0d3f\u0d28\u0d4d\u200d\u0d2a\u0d4d'],
+  NARROWMONTHS: ['\u0d1c', '\u0d2b\u0d46', '\u0d2e\u0d3e', '\u0d0f', '\u0d2e\u0d47', '\u0d1c\u0d42', '\u0d1c\u0d42', '\u0d13', '\u0d38\u0d46', '\u0d12', '\u0d28', '\u0d21\u0d3f'],
+  MONTHS: ['\u0d1c\u0d28\u0d41\u0d35\u0d30\u0d3f', '\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41\u0d35\u0d30\u0d3f', '\u0d2e\u0d3e\u0d30\u0d4d\u200d\u0d1a\u0d4d\u0d1a\u0d4d', '\u0d0f\u0d2a\u0d4d\u0d30\u0d3f\u0d32\u0d4d\u200d', '\u0d2e\u0d47\u0d2f\u0d4d', '\u0d1c\u0d42\u0d23\u0d4d\u200d', '\u0d1c\u0d42\u0d32\u0d48', '\u0d13\u0d17\u0d38\u0d4d\u0d31\u0d4d\u0d31\u0d4d', '\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31\u0d02\u0d2c\u0d30\u0d4d\u200d', '\u0d12\u0d15\u0d4d\u0d1f\u0d4b\u0d2c\u0d30\u0d4d\u200d', '\u0d28\u0d35\u0d02\u0d2c\u0d30\u0d4d\u200d', '\u0d21\u0d3f\u0d38\u0d02\u0d2c\u0d30\u0d4d\u200d'],
+  SHORTMONTHS: ['\u0d1c\u0d28\u0d41', '\u0d2b\u0d46\u0d2c\u0d4d\u0d30\u0d41', '\u0d2e\u0d3e\u0d30\u0d4d\u200d', '\u0d0f\u0d2a\u0d4d\u0d30\u0d3f', '\u0d2e\u0d47\u0d2f\u0d4d', '\u0d1c\u0d42\u0d23\u0d4d\u200d', '\u0d1c\u0d42\u0d32\u0d48', '\u0d13\u0d17', '\u0d38\u0d46\u0d2a\u0d4d\u0d31\u0d4d\u0d31\u0d02', '\u0d12\u0d15\u0d4d\u0d1f\u0d4b', '\u0d28\u0d35\u0d02', '\u0d21\u0d3f\u0d38\u0d02'],
+  WEEKDAYS: ['\u0d1e\u0d3e\u0d2f\u0d31\u0d3e\u0d34\u0d4d\u0d1a', '\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d33\u0d3e\u0d34\u0d4d\u0d1a', '\u0d1a\u0d4a\u0d35\u0d4d\u0d35\u0d3e\u0d34\u0d4d\u0d1a', '\u0d2c\u0d41\u0d27\u0d28\u0d3e\u0d34\u0d4d\u0d1a', '\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d3e\u0d34\u0d4d\u0d1a', '\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a', '\u0d36\u0d28\u0d3f\u0d2f\u0d3e\u0d34\u0d4d\u0d1a'],
+  SHORTWEEKDAYS: ['\u0d1e\u0d3e\u0d2f\u0d30\u0d4d\u200d', '\u0d24\u0d3f\u0d19\u0d4d\u0d15\u0d33\u0d4d\u200d', '\u0d1a\u0d4a\u0d35\u0d4d\u0d35', '\u0d2c\u0d41\u0d27\u0d28\u0d4d\u200d', '\u0d35\u0d4d\u0d2f\u0d3e\u0d34\u0d02', '\u0d35\u0d46\u0d33\u0d4d\u0d33\u0d3f', '\u0d36\u0d28\u0d3f'],
+  NARROWWEEKDAYS: ['\u0d1e\u0d3e', '\u0d24\u0d3f', '\u0d1a\u0d4a', '\u0d2c\u0d41', '\u0d35\u0d4d\u0d2f\u0d3e', '\u0d35\u0d46', '\u0d36'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0d12\u0d28\u0d4d\u0d28\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02', '\u0d30\u0d23\u0d4d\u0d1f\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02', '\u0d2e\u0d42\u0d28\u0d4d\u0d28\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02', '\u0d28\u0d3e\u0d32\u0d3e\u0d02 \u0d2a\u0d3e\u0d26\u0d02'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['y, MMMM d, EEEE', 'y, MMMM d', 'y, MMM d', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn.js
new file mode 100644
index 0000000..af712d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043c.\u044d.\u04e9', '\u043c.\u044d.'],
+  ERANAMES: ['\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439 \u04e9\u043c\u043d\u04e9\u0445', '\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0425\u0443\u043b\u0433\u0430\u043d\u0430', '\u04ae\u0445\u044d\u0440', '\u0411\u0430\u0440', '\u0422\u0443\u0443\u043b\u0430\u0439', '\u041b\u0443\u0443', '\u041c\u043e\u0433\u043e\u0439', '\u041c\u043e\u0440\u044c', '\u0425\u043e\u043d\u044c', '\u0411\u0438\u0447', '\u0422\u0430\u0445\u0438\u0430', '\u041d\u043e\u0445\u043e\u0439', '\u0413\u0430\u0445\u0430\u0439'],
+  SHORTMONTHS: ['\u0445\u0443\u043b', '\u04af\u0445\u044d', '\u0431\u0430\u0440', '\u0442\u0443\u0443', '\u043b\u0443\u0443', '\u043c\u043e\u0433', '\u043c\u043e\u0440', '\u0445\u043e\u043d', '\u0431\u0438\u0447', '\u0442\u0430\u0445', '\u043d\u043e\u0445', '\u0433\u0430\u0445'],
+  WEEKDAYS: ['\u043d\u044f\u043c', '\u0434\u0430\u0432\u0430\u0430', '\u043c\u044f\u0433\u043c\u0430\u0440', '\u043b\u0445\u0430\u0433\u0432\u0430', '\u043f\u04af\u0440\u044d\u0432', '\u0431\u0430\u0430\u0441\u0430\u043d', '\u0431\u044f\u043c\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u044f', '\u0414\u0430', '\u041c\u044f', '\u041b\u0445', '\u041f\u04af', '\u0411\u0430', '\u0411\u044f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1/4', '2/4', '3/4', '4/4'],
+  QUARTERS: ['\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u043d\u044d\u0433', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0445\u043e\u0451\u0440', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0433\u0443\u0440\u0430\u0432', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0434\u04e9\u0440\u04e9\u0432'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_CN.js
new file mode 100644
index 0000000..af712d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043c.\u044d.\u04e9', '\u043c.\u044d.'],
+  ERANAMES: ['\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439 \u04e9\u043c\u043d\u04e9\u0445', '\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0425\u0443\u043b\u0433\u0430\u043d\u0430', '\u04ae\u0445\u044d\u0440', '\u0411\u0430\u0440', '\u0422\u0443\u0443\u043b\u0430\u0439', '\u041b\u0443\u0443', '\u041c\u043e\u0433\u043e\u0439', '\u041c\u043e\u0440\u044c', '\u0425\u043e\u043d\u044c', '\u0411\u0438\u0447', '\u0422\u0430\u0445\u0438\u0430', '\u041d\u043e\u0445\u043e\u0439', '\u0413\u0430\u0445\u0430\u0439'],
+  SHORTMONTHS: ['\u0445\u0443\u043b', '\u04af\u0445\u044d', '\u0431\u0430\u0440', '\u0442\u0443\u0443', '\u043b\u0443\u0443', '\u043c\u043e\u0433', '\u043c\u043e\u0440', '\u0445\u043e\u043d', '\u0431\u0438\u0447', '\u0442\u0430\u0445', '\u043d\u043e\u0445', '\u0433\u0430\u0445'],
+  WEEKDAYS: ['\u043d\u044f\u043c', '\u0434\u0430\u0432\u0430\u0430', '\u043c\u044f\u0433\u043c\u0430\u0440', '\u043b\u0445\u0430\u0433\u0432\u0430', '\u043f\u04af\u0440\u044d\u0432', '\u0431\u0430\u0430\u0441\u0430\u043d', '\u0431\u044f\u043c\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u044f', '\u0414\u0430', '\u041c\u044f', '\u041b\u0445', '\u041f\u04af', '\u0411\u0430', '\u0411\u044f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1/4', '2/4', '3/4', '4/4'],
+  QUARTERS: ['\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u043d\u044d\u0433', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0445\u043e\u0451\u0440', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0433\u0443\u0440\u0430\u0432', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0434\u04e9\u0440\u04e9\u0432'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Cyrl.js
new file mode 100644
index 0000000..af712d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Cyrl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043c.\u044d.\u04e9', '\u043c.\u044d.'],
+  ERANAMES: ['\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439 \u04e9\u043c\u043d\u04e9\u0445', '\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0425\u0443\u043b\u0433\u0430\u043d\u0430', '\u04ae\u0445\u044d\u0440', '\u0411\u0430\u0440', '\u0422\u0443\u0443\u043b\u0430\u0439', '\u041b\u0443\u0443', '\u041c\u043e\u0433\u043e\u0439', '\u041c\u043e\u0440\u044c', '\u0425\u043e\u043d\u044c', '\u0411\u0438\u0447', '\u0422\u0430\u0445\u0438\u0430', '\u041d\u043e\u0445\u043e\u0439', '\u0413\u0430\u0445\u0430\u0439'],
+  SHORTMONTHS: ['\u0445\u0443\u043b', '\u04af\u0445\u044d', '\u0431\u0430\u0440', '\u0442\u0443\u0443', '\u043b\u0443\u0443', '\u043c\u043e\u0433', '\u043c\u043e\u0440', '\u0445\u043e\u043d', '\u0431\u0438\u0447', '\u0442\u0430\u0445', '\u043d\u043e\u0445', '\u0433\u0430\u0445'],
+  WEEKDAYS: ['\u043d\u044f\u043c', '\u0434\u0430\u0432\u0430\u0430', '\u043c\u044f\u0433\u043c\u0430\u0440', '\u043b\u0445\u0430\u0433\u0432\u0430', '\u043f\u04af\u0440\u044d\u0432', '\u0431\u0430\u0430\u0441\u0430\u043d', '\u0431\u044f\u043c\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u044f', '\u0414\u0430', '\u041c\u044f', '\u041b\u0445', '\u041f\u04af', '\u0411\u0430', '\u0411\u044f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1/4', '2/4', '3/4', '4/4'],
+  QUARTERS: ['\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u043d\u044d\u0433', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0445\u043e\u0451\u0440', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0433\u0443\u0440\u0430\u0432', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0434\u04e9\u0440\u04e9\u0432'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Cyrl_MN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Cyrl_MN.js
new file mode 100644
index 0000000..af712d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Cyrl_MN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043c.\u044d.\u04e9', '\u043c.\u044d.'],
+  ERANAMES: ['\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439 \u04e9\u043c\u043d\u04e9\u0445', '\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0425\u0443\u043b\u0433\u0430\u043d\u0430', '\u04ae\u0445\u044d\u0440', '\u0411\u0430\u0440', '\u0422\u0443\u0443\u043b\u0430\u0439', '\u041b\u0443\u0443', '\u041c\u043e\u0433\u043e\u0439', '\u041c\u043e\u0440\u044c', '\u0425\u043e\u043d\u044c', '\u0411\u0438\u0447', '\u0422\u0430\u0445\u0438\u0430', '\u041d\u043e\u0445\u043e\u0439', '\u0413\u0430\u0445\u0430\u0439'],
+  SHORTMONTHS: ['\u0445\u0443\u043b', '\u04af\u0445\u044d', '\u0431\u0430\u0440', '\u0442\u0443\u0443', '\u043b\u0443\u0443', '\u043c\u043e\u0433', '\u043c\u043e\u0440', '\u0445\u043e\u043d', '\u0431\u0438\u0447', '\u0442\u0430\u0445', '\u043d\u043e\u0445', '\u0433\u0430\u0445'],
+  WEEKDAYS: ['\u043d\u044f\u043c', '\u0434\u0430\u0432\u0430\u0430', '\u043c\u044f\u0433\u043c\u0430\u0440', '\u043b\u0445\u0430\u0433\u0432\u0430', '\u043f\u04af\u0440\u044d\u0432', '\u0431\u0430\u0430\u0441\u0430\u043d', '\u0431\u044f\u043c\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u044f', '\u0414\u0430', '\u041c\u044f', '\u041b\u0445', '\u041f\u04af', '\u0411\u0430', '\u0411\u044f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1/4', '2/4', '3/4', '4/4'],
+  QUARTERS: ['\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u043d\u044d\u0433', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0445\u043e\u0451\u0440', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0433\u0443\u0440\u0430\u0432', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0434\u04e9\u0440\u04e9\u0432'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_MN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_MN.js
new file mode 100644
index 0000000..af712d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_MN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043c.\u044d.\u04e9', '\u043c.\u044d.'],
+  ERANAMES: ['\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439 \u04e9\u043c\u043d\u04e9\u0445', '\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0425\u0443\u043b\u0433\u0430\u043d\u0430', '\u04ae\u0445\u044d\u0440', '\u0411\u0430\u0440', '\u0422\u0443\u0443\u043b\u0430\u0439', '\u041b\u0443\u0443', '\u041c\u043e\u0433\u043e\u0439', '\u041c\u043e\u0440\u044c', '\u0425\u043e\u043d\u044c', '\u0411\u0438\u0447', '\u0422\u0430\u0445\u0438\u0430', '\u041d\u043e\u0445\u043e\u0439', '\u0413\u0430\u0445\u0430\u0439'],
+  SHORTMONTHS: ['\u0445\u0443\u043b', '\u04af\u0445\u044d', '\u0431\u0430\u0440', '\u0442\u0443\u0443', '\u043b\u0443\u0443', '\u043c\u043e\u0433', '\u043c\u043e\u0440', '\u0445\u043e\u043d', '\u0431\u0438\u0447', '\u0442\u0430\u0445', '\u043d\u043e\u0445', '\u0433\u0430\u0445'],
+  WEEKDAYS: ['\u043d\u044f\u043c', '\u0434\u0430\u0432\u0430\u0430', '\u043c\u044f\u0433\u043c\u0430\u0440', '\u043b\u0445\u0430\u0433\u0432\u0430', '\u043f\u04af\u0440\u044d\u0432', '\u0431\u0430\u0430\u0441\u0430\u043d', '\u0431\u044f\u043c\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u044f', '\u0414\u0430', '\u041c\u044f', '\u041b\u0445', '\u041f\u04af', '\u0411\u0430', '\u0411\u044f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1/4', '2/4', '3/4', '4/4'],
+  QUARTERS: ['\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u043d\u044d\u0433', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0445\u043e\u0451\u0440', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0433\u0443\u0440\u0430\u0432', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0434\u04e9\u0440\u04e9\u0432'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Mong.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Mong.js
new file mode 100644
index 0000000..af712d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Mong.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043c.\u044d.\u04e9', '\u043c.\u044d.'],
+  ERANAMES: ['\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439 \u04e9\u043c\u043d\u04e9\u0445', '\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0425\u0443\u043b\u0433\u0430\u043d\u0430', '\u04ae\u0445\u044d\u0440', '\u0411\u0430\u0440', '\u0422\u0443\u0443\u043b\u0430\u0439', '\u041b\u0443\u0443', '\u041c\u043e\u0433\u043e\u0439', '\u041c\u043e\u0440\u044c', '\u0425\u043e\u043d\u044c', '\u0411\u0438\u0447', '\u0422\u0430\u0445\u0438\u0430', '\u041d\u043e\u0445\u043e\u0439', '\u0413\u0430\u0445\u0430\u0439'],
+  SHORTMONTHS: ['\u0445\u0443\u043b', '\u04af\u0445\u044d', '\u0431\u0430\u0440', '\u0442\u0443\u0443', '\u043b\u0443\u0443', '\u043c\u043e\u0433', '\u043c\u043e\u0440', '\u0445\u043e\u043d', '\u0431\u0438\u0447', '\u0442\u0430\u0445', '\u043d\u043e\u0445', '\u0433\u0430\u0445'],
+  WEEKDAYS: ['\u043d\u044f\u043c', '\u0434\u0430\u0432\u0430\u0430', '\u043c\u044f\u0433\u043c\u0430\u0440', '\u043b\u0445\u0430\u0433\u0432\u0430', '\u043f\u04af\u0440\u044d\u0432', '\u0431\u0430\u0430\u0441\u0430\u043d', '\u0431\u044f\u043c\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u044f', '\u0414\u0430', '\u041c\u044f', '\u041b\u0445', '\u041f\u04af', '\u0411\u0430', '\u0411\u044f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1/4', '2/4', '3/4', '4/4'],
+  QUARTERS: ['\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u043d\u044d\u0433', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0445\u043e\u0451\u0440', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0433\u0443\u0440\u0430\u0432', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0434\u04e9\u0440\u04e9\u0432'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Mong_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Mong_CN.js
new file mode 100644
index 0000000..af712d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mn_Mong_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043c.\u044d.\u04e9', '\u043c.\u044d.'],
+  ERANAMES: ['\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439 \u04e9\u043c\u043d\u04e9\u0445', '\u043c\u0430\u043d\u0430\u0439 \u044d\u0440\u0438\u043d\u0438\u0439'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u0425\u0443\u043b\u0433\u0430\u043d\u0430', '\u04ae\u0445\u044d\u0440', '\u0411\u0430\u0440', '\u0422\u0443\u0443\u043b\u0430\u0439', '\u041b\u0443\u0443', '\u041c\u043e\u0433\u043e\u0439', '\u041c\u043e\u0440\u044c', '\u0425\u043e\u043d\u044c', '\u0411\u0438\u0447', '\u0422\u0430\u0445\u0438\u0430', '\u041d\u043e\u0445\u043e\u0439', '\u0413\u0430\u0445\u0430\u0439'],
+  SHORTMONTHS: ['\u0445\u0443\u043b', '\u04af\u0445\u044d', '\u0431\u0430\u0440', '\u0442\u0443\u0443', '\u043b\u0443\u0443', '\u043c\u043e\u0433', '\u043c\u043e\u0440', '\u0445\u043e\u043d', '\u0431\u0438\u0447', '\u0442\u0430\u0445', '\u043d\u043e\u0445', '\u0433\u0430\u0445'],
+  WEEKDAYS: ['\u043d\u044f\u043c', '\u0434\u0430\u0432\u0430\u0430', '\u043c\u044f\u0433\u043c\u0430\u0440', '\u043b\u0445\u0430\u0433\u0432\u0430', '\u043f\u04af\u0440\u044d\u0432', '\u0431\u0430\u0430\u0441\u0430\u043d', '\u0431\u044f\u043c\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u044f', '\u0414\u0430', '\u041c\u044f', '\u041b\u0445', '\u041f\u04af', '\u0411\u0430', '\u0411\u044f'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1/4', '2/4', '3/4', '4/4'],
+  QUARTERS: ['\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u043d\u044d\u0433', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0445\u043e\u0451\u0440', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0433\u0443\u0440\u0430\u0432', '\u0434\u04e9\u0440\u04e9\u0432\u043d\u0438\u0439 \u0434\u04e9\u0440\u04e9\u0432'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mo.js
new file mode 100644
index 0000000..4289012
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u00ee.Hr.', 'd.Hr.'],
+  ERANAMES: ['\u00eenainte de Hristos', 'dup\u0103 Hristos'],
+  NARROWMONTHS: ['I', 'F', 'M', 'A', 'M', 'I', 'I', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'],
+  SHORTMONTHS: ['ian.', 'feb.', 'mar.', 'apr.', 'mai', 'iun.', 'iul.', 'aug.', 'sept.', 'oct.', 'nov.', 'dec.'],
+  WEEKDAYS: ['duminic\u0103', 'luni', 'mar\u021bi', 'miercuri', 'joi', 'vineri', 's\u00e2mb\u0103t\u0103'],
+  SHORTWEEKDAYS: ['Du', 'Lu', 'Ma', 'Mi', 'Jo', 'Vi', 'S\u00e2'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['trim. I', 'trim. II', 'trim. III', 'trim. IV'],
+  QUARTERS: ['trimestrul I', 'trimestrul al II-lea', 'trimestrul al III-lea', 'trimestrul al IV-lea'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mr.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mr.js
new file mode 100644
index 0000000..c4b4bb3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mr.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0908.\u0938.\u092a\u0942.', '\u0908.\u0938.'],
+  ERANAMES: ['\u0908\u0938\u0935\u0940\u0938\u0928\u092a\u0942\u0930\u094d\u0935', '\u0908\u0938\u0935\u0940\u0938\u0928'],
+  NARROWMONTHS: ['\u091c\u093e', '\u092b\u0947', '\u092e\u093e', '\u090f', '\u092e\u0947', '\u091c\u0942', '\u091c\u0941', '\u0911', '\u0938', '\u0911', '\u0928\u094b', '\u0921\u093f'],
+  MONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0911\u0917\u0938\u094d\u091f', '\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0911\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0911\u0917\u0938\u094d\u091f', '\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0911\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  WEEKDAYS: ['\u0930\u0935\u093f\u0935\u093e\u0930', '\u0938\u094b\u092e\u0935\u093e\u0930', '\u092e\u0902\u0917\u0933\u0935\u093e\u0930', '\u092c\u0941\u0927\u0935\u093e\u0930', '\u0917\u0941\u0930\u0941\u0935\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930', '\u0936\u0928\u093f\u0935\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0930\u0935\u093f', '\u0938\u094b\u092e', '\u092e\u0902\u0917\u0933', '\u092c\u0941\u0927', '\u0917\u0941\u0930\u0941', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['\u0930', '\u0938\u094b', '\u092e\u0902', '\u092c\u0941', '\u0917\u0941', '\u0936\u0941', '\u0936'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u092a\u094d\u0930\u0925\u092e \u0924\u093f\u092e\u093e\u0939\u0940', '\u0926\u094d\u0935\u093f\u0924\u0940\u092f \u0924\u093f\u092e\u093e\u0939\u0940', '\u0924\u0943\u0924\u0940\u092f \u0924\u093f\u092e\u093e\u0939\u0940', '\u091a\u0924\u0941\u0930\u094d\u0925 \u0924\u093f\u092e\u093e\u0939\u0940'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'd-M-yy'],
+  TIMEFORMATS: ['h-mm-ss a zzzz', 'h-mm-ss a z', 'h-mm-ss a', 'h-mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mr_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mr_IN.js
new file mode 100644
index 0000000..c4b4bb3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mr_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0908.\u0938.\u092a\u0942.', '\u0908.\u0938.'],
+  ERANAMES: ['\u0908\u0938\u0935\u0940\u0938\u0928\u092a\u0942\u0930\u094d\u0935', '\u0908\u0938\u0935\u0940\u0938\u0928'],
+  NARROWMONTHS: ['\u091c\u093e', '\u092b\u0947', '\u092e\u093e', '\u090f', '\u092e\u0947', '\u091c\u0942', '\u091c\u0941', '\u0911', '\u0938', '\u0911', '\u0928\u094b', '\u0921\u093f'],
+  MONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0911\u0917\u0938\u094d\u091f', '\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0911\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u093e\u0928\u0947\u0935\u093e\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0935\u093e\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u090f\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0942\u0928', '\u091c\u0941\u0932\u0948', '\u0911\u0917\u0938\u094d\u091f', '\u0938\u092a\u094d\u091f\u0947\u0902\u092c\u0930', '\u0911\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u0935\u094d\u0939\u0947\u0902\u092c\u0930', '\u0921\u093f\u0938\u0947\u0902\u092c\u0930'],
+  WEEKDAYS: ['\u0930\u0935\u093f\u0935\u093e\u0930', '\u0938\u094b\u092e\u0935\u093e\u0930', '\u092e\u0902\u0917\u0933\u0935\u093e\u0930', '\u092c\u0941\u0927\u0935\u093e\u0930', '\u0917\u0941\u0930\u0941\u0935\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u0935\u093e\u0930', '\u0936\u0928\u093f\u0935\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0930\u0935\u093f', '\u0938\u094b\u092e', '\u092e\u0902\u0917\u0933', '\u092c\u0941\u0927', '\u0917\u0941\u0930\u0941', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['\u0930', '\u0938\u094b', '\u092e\u0902', '\u092c\u0941', '\u0917\u0941', '\u0936\u0941', '\u0936'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u092a\u094d\u0930\u0925\u092e \u0924\u093f\u092e\u093e\u0939\u0940', '\u0926\u094d\u0935\u093f\u0924\u0940\u092f \u0924\u093f\u092e\u093e\u0939\u0940', '\u0924\u0943\u0924\u0940\u092f \u0924\u093f\u092e\u093e\u0939\u0940', '\u091a\u0924\u0941\u0930\u094d\u0925 \u0924\u093f\u092e\u093e\u0939\u0940'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'd-M-yy'],
+  TIMEFORMATS: ['h-mm-ss a zzzz', 'h-mm-ss a z', 'h-mm-ss a', 'h-mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms.js
new file mode 100644
index 0000000..ad020b2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['S.M.', 'T.M.'],
+  ERANAMES: ['S.M.', 'T.M.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Mac', 'April', 'Mei', 'Jun', 'Julai', 'Ogos', 'September', 'Oktober', 'November', 'Disember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mac', 'Apr', 'Mei', 'Jun', 'Jul', 'Ogos', 'Sep', 'Okt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Ahad', 'Isnin', 'Selasa', 'Rabu', 'Khamis', 'Jumaat', 'Sabtu'],
+  SHORTWEEKDAYS: ['Ahd', 'Isn', 'Sel', 'Rab', 'Kha', 'Jum', 'Sab'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['S1', 'S2', 'S3', 'S4'],
+  QUARTERS: ['suku pertama', 'suku kedua', 'suku ketiga', 'suku keempat'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMM y', 'dd MMMM y', 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms_BN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms_BN.js
new file mode 100644
index 0000000..8016a0a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms_BN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['S.M.', 'T.M.'],
+  ERANAMES: ['S.M.', 'T.M.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Mac', 'April', 'Mei', 'Jun', 'Julai', 'Ogos', 'September', 'Oktober', 'November', 'Disember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mac', 'Apr', 'Mei', 'Jun', 'Jul', 'Ogos', 'Sep', 'Okt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Ahad', 'Isnin', 'Selasa', 'Rabu', 'Khamis', 'Jumaat', 'Sabtu'],
+  SHORTWEEKDAYS: ['Ahd', 'Isn', 'Sel', 'Rab', 'Kha', 'Jum', 'Sab'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['S1', 'S2', 'S3', 'S4'],
+  QUARTERS: ['suku pertama', 'suku kedua', 'suku ketiga', 'suku keempat'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['dd MMMM y', 'dd MMMM y', 'dd/MM/yyyy', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss aa zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms_MY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms_MY.js
new file mode 100644
index 0000000..ad020b2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ms_MY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['S.M.', 'T.M.'],
+  ERANAMES: ['S.M.', 'T.M.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Mac', 'April', 'Mei', 'Jun', 'Julai', 'Ogos', 'September', 'Oktober', 'November', 'Disember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mac', 'Apr', 'Mei', 'Jun', 'Jul', 'Ogos', 'Sep', 'Okt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Ahad', 'Isnin', 'Selasa', 'Rabu', 'Khamis', 'Jumaat', 'Sabtu'],
+  SHORTWEEKDAYS: ['Ahd', 'Isn', 'Sel', 'Rab', 'Kha', 'Jum', 'Sab'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['S1', 'S2', 'S3', 'S4'],
+  QUARTERS: ['suku pertama', 'suku kedua', 'suku ketiga', 'suku keempat'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMM y', 'dd MMMM y', 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mt.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mt.js
new file mode 100644
index 0000000..3a62840
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mt.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['QK', 'WK'],
+  ERANAMES: ['Qabel Kristu', 'Wara Kristu'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', '\u0120', 'L', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Jannar', 'Frar', 'Marzu', 'April', 'Mejju', '\u0120unju', 'Lulju', 'Awwissu', 'Settembru', 'Ottubru', 'Novembru', 'Di\u010bembru'],
+  SHORTMONTHS: ['Jan', 'Fra', 'Mar', 'Apr', 'Mej', '\u0120un', 'Lul', 'Aww', 'Set', 'Ott', 'Nov', 'Di\u010b'],
+  WEEKDAYS: ['Il-\u0126add', 'It-Tnejn', 'It-Tlieta', 'L-Erbg\u0127a', 'Il-\u0126amis', 'Il-\u0120img\u0127a', 'Is-Sibt'],
+  SHORTWEEKDAYS: ['\u0126ad', 'Tne', 'Tli', 'Erb', '\u0126am', '\u0120im', 'Sib'],
+  NARROWWEEKDAYS: ['\u0126', 'T', 'T', 'E', '\u0126', '\u0120', 'S'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  AMPMS: ['QN', 'WN'],
+  DATEFORMATS: ["EEEE, d 'ta'\u2019 MMMM y", "d 'ta'\u2019 MMMM y", 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mt_MT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mt_MT.js
new file mode 100644
index 0000000..3a62840
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__mt_MT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['QK', 'WK'],
+  ERANAMES: ['Qabel Kristu', 'Wara Kristu'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', '\u0120', 'L', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Jannar', 'Frar', 'Marzu', 'April', 'Mejju', '\u0120unju', 'Lulju', 'Awwissu', 'Settembru', 'Ottubru', 'Novembru', 'Di\u010bembru'],
+  SHORTMONTHS: ['Jan', 'Fra', 'Mar', 'Apr', 'Mej', '\u0120un', 'Lul', 'Aww', 'Set', 'Ott', 'Nov', 'Di\u010b'],
+  WEEKDAYS: ['Il-\u0126add', 'It-Tnejn', 'It-Tlieta', 'L-Erbg\u0127a', 'Il-\u0126amis', 'Il-\u0120img\u0127a', 'Is-Sibt'],
+  SHORTWEEKDAYS: ['\u0126ad', 'Tne', 'Tli', 'Erb', '\u0126am', '\u0120im', 'Sib'],
+  NARROWWEEKDAYS: ['\u0126', 'T', 'T', 'E', '\u0126', '\u0120', 'S'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  AMPMS: ['QN', 'WN'],
+  DATEFORMATS: ["EEEE, d 'ta'\u2019 MMMM y", "d 'ta'\u2019 MMMM y", 'dd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__my.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__my.js
new file mode 100644
index 0000000..32e2f53
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__my.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u1018\u102e\u1005\u102e', '\u1021\u1031\u1012\u102e'],
+  ERANAMES: ['\u1001\u101b\u1005\u103a\u1010\u1031\u102c\u103a \u1019\u1015\u1031\u102b\u103a\u1019\u102e\u1000\u102c\u101c', '\u1001\u101b\u1005\u103a\u1010\u1031\u102c\u103a \u1015\u1031\u102b\u103a\u1011\u103d\u1014\u103a\u1038\u1015\u103c\u102e\u1038\u1000\u102c\u101c'],
+  NARROWMONTHS: ['\u1007', '\u1016', '\u1019', '\u1027', '\u1019', '\u1007', '\u1007', '\u1029', '\u1005', '\u1021', '\u1014', '\u1012'],
+  MONTHS: ['\u1007\u1014\u103a\u1014\u101d\u102b\u101b\u102e', '\u1016\u1031\u1016\u1031\u102c\u103a\u101d\u102b\u101b\u102e', '\u1019\u1010\u103a', '\u1027\u1015\u103c\u102e', '\u1019\u1031', '\u1007\u103d\u1014\u103a', '\u1007\u1030\u101c\u102d\u102f\u1004\u103a', '\u1029\u1002\u102f\u1010\u103a', '\u1005\u1000\u103a\u1010\u1004\u103a\u1018\u102c', '\u1021\u1031\u102c\u1000\u103a\u1010\u102d\u102f\u1018\u102c', '\u1014\u102d\u102f\u101d\u1004\u103a\u1018\u102c', '\u1012\u102e\u1007\u1004\u103a\u1018\u102c'],
+  SHORTMONTHS: ['\u1007\u1014\u103a', '\u1016\u1031', '\u1019\u1010\u103a', '\u1027', '\u1019\u1031', '\u1007\u103d\u1014\u103a', '\u1007\u1030', '\u1029', '\u1005\u1000\u103a', '\u1021\u1031\u102c\u1000\u103a', '\u1014\u102d\u102f', '\u1012\u102e'],
+  WEEKDAYS: ['\u1010\u1014\u1004\u103a\u1039\u1002\u1014\u103d\u1031', '\u1010\u1014\u1004\u103a\u1039\u101c\u102c', '\u1021\u1004\u103a\u1039\u1002\u102b', '\u1017\u102f\u1012\u1039\u1013\u101f\u1030\u1038', '\u1000\u103c\u102c\u101e\u1015\u1010\u1031\u1038', '\u101e\u1031\u102c\u1000\u103c\u102c', '\u1005\u1014\u1031'],
+  SHORTWEEKDAYS: ['\u1014\u103d\u1031', '\u101c\u102c', '\u1002\u102b', '\u101f\u1030\u1038', '\u1010\u1031\u1038', '\u1000\u103c\u102c', '\u1014\u1031'],
+  NARROWWEEKDAYS: ['\u1010', '\u1010', '\u1021', '\u1017', '\u1000', '\u101e', '\u1005'],
+  SHORTQUARTERS: ['\u1015-\u1005\u102d\u1010\u103a', '\u1012\u102f-\u1005\u102d\u1010\u103a', '\u1010-\u1005\u102d\u1010\u103a', '\u1005-\u1005\u102d\u1010\u103a'],
+  QUARTERS: ['\u1015\u1011\u1019 \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a', '\u1012\u102f\u1010\u102d\u101a \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a', '\u1010\u1010\u102d\u101a \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a', '\u1005\u1010\u102f\u1010\u1039\u1011 \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a'],
+  AMPMS: ['\u1014\u1036\u1014\u1000\u103a', '\u100a\u1014\u1031'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__my_MM.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__my_MM.js
new file mode 100644
index 0000000..32e2f53
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__my_MM.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u1018\u102e\u1005\u102e', '\u1021\u1031\u1012\u102e'],
+  ERANAMES: ['\u1001\u101b\u1005\u103a\u1010\u1031\u102c\u103a \u1019\u1015\u1031\u102b\u103a\u1019\u102e\u1000\u102c\u101c', '\u1001\u101b\u1005\u103a\u1010\u1031\u102c\u103a \u1015\u1031\u102b\u103a\u1011\u103d\u1014\u103a\u1038\u1015\u103c\u102e\u1038\u1000\u102c\u101c'],
+  NARROWMONTHS: ['\u1007', '\u1016', '\u1019', '\u1027', '\u1019', '\u1007', '\u1007', '\u1029', '\u1005', '\u1021', '\u1014', '\u1012'],
+  MONTHS: ['\u1007\u1014\u103a\u1014\u101d\u102b\u101b\u102e', '\u1016\u1031\u1016\u1031\u102c\u103a\u101d\u102b\u101b\u102e', '\u1019\u1010\u103a', '\u1027\u1015\u103c\u102e', '\u1019\u1031', '\u1007\u103d\u1014\u103a', '\u1007\u1030\u101c\u102d\u102f\u1004\u103a', '\u1029\u1002\u102f\u1010\u103a', '\u1005\u1000\u103a\u1010\u1004\u103a\u1018\u102c', '\u1021\u1031\u102c\u1000\u103a\u1010\u102d\u102f\u1018\u102c', '\u1014\u102d\u102f\u101d\u1004\u103a\u1018\u102c', '\u1012\u102e\u1007\u1004\u103a\u1018\u102c'],
+  SHORTMONTHS: ['\u1007\u1014\u103a', '\u1016\u1031', '\u1019\u1010\u103a', '\u1027', '\u1019\u1031', '\u1007\u103d\u1014\u103a', '\u1007\u1030', '\u1029', '\u1005\u1000\u103a', '\u1021\u1031\u102c\u1000\u103a', '\u1014\u102d\u102f', '\u1012\u102e'],
+  WEEKDAYS: ['\u1010\u1014\u1004\u103a\u1039\u1002\u1014\u103d\u1031', '\u1010\u1014\u1004\u103a\u1039\u101c\u102c', '\u1021\u1004\u103a\u1039\u1002\u102b', '\u1017\u102f\u1012\u1039\u1013\u101f\u1030\u1038', '\u1000\u103c\u102c\u101e\u1015\u1010\u1031\u1038', '\u101e\u1031\u102c\u1000\u103c\u102c', '\u1005\u1014\u1031'],
+  SHORTWEEKDAYS: ['\u1014\u103d\u1031', '\u101c\u102c', '\u1002\u102b', '\u101f\u1030\u1038', '\u1010\u1031\u1038', '\u1000\u103c\u102c', '\u1014\u1031'],
+  NARROWWEEKDAYS: ['\u1010', '\u1010', '\u1021', '\u1017', '\u1000', '\u101e', '\u1005'],
+  SHORTQUARTERS: ['\u1015-\u1005\u102d\u1010\u103a', '\u1012\u102f-\u1005\u102d\u1010\u103a', '\u1010-\u1005\u102d\u1010\u103a', '\u1005-\u1005\u102d\u1010\u103a'],
+  QUARTERS: ['\u1015\u1011\u1019 \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a', '\u1012\u102f\u1010\u102d\u101a \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a', '\u1010\u1010\u102d\u101a \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a', '\u1005\u1010\u102f\u1010\u1039\u1011 \u101e\u102f\u1036\u1038\u101c\u1015\u1010\u103a'],
+  AMPMS: ['\u1014\u1036\u1014\u1000\u103a', '\u100a\u1014\u1031'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nb.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nb.js
new file mode 100644
index 0000000..8edb505
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nb.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f.Kr.', 'e.Kr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mars', 'apr.', 'mai', 'juni', 'juli', 'aug.', 'sep.', 'okt.', 'nov.', 'des.'],
+  WEEKDAYS: ['s\u00f8ndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f8rdag'],
+  SHORTWEEKDAYS: ['s\u00f8n.', 'man.', 'tir.', 'ons.', 'tor.', 'fre.', 'l\u00f8r.'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d. MMMM y', 'd. MMMM y', 'd. MMM y', 'dd.MM.yy'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nb_NO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nb_NO.js
new file mode 100644
index 0000000..8edb505
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nb_NO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f.Kr.', 'e.Kr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mars', 'apr.', 'mai', 'juni', 'juli', 'aug.', 'sep.', 'okt.', 'nov.', 'des.'],
+  WEEKDAYS: ['s\u00f8ndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f8rdag'],
+  SHORTWEEKDAYS: ['s\u00f8n.', 'man.', 'tir.', 'ons.', 'tor.', 'fre.', 'l\u00f8r.'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d. MMMM y', 'd. MMMM y', 'd. MMM y', 'dd.MM.yy'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nds.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nds.js
new file mode 100644
index 0000000..01cb174
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nds.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nds_DE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nds_DE.js
new file mode 100644
index 0000000..01cb174
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nds_DE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne.js
new file mode 100644
index 0000000..3f3f141
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0908\u0938\u093e \u092a\u0942\u0930\u094d\u0935', '\u0938\u0928\u094d'],
+  ERANAMES: ['\u0908\u0938\u093e \u092a\u0942\u0930\u094d\u0935', '\u0938\u0928\u094d'],
+  NARROWMONTHS: ['\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e', '\u096f', '\u0967\u0966', '\u0967\u0967', '\u0967\u0968'],
+  MONTHS: ['\u091c\u0928\u0935\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0905\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0941\u0928', '\u091c\u0941\u0932\u093e\u0908', '\u0905\u0917\u0938\u094d\u0924', '\u0938\u0947\u092a\u094d\u091f\u0947\u092e\u094d\u092c\u0930', '\u0905\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u092d\u0947\u092e\u094d\u092c\u0930', '\u0921\u093f\u0938\u0947\u092e\u094d\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u0928', '\u092b\u0947\u092c', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u093f', '\u092e\u0947', '\u091c\u0941\u0928', '\u091c\u0941\u0932\u093e', '\u0905\u0917', '\u0938\u0947\u092a\u094d\u091f', '\u0905\u0915\u094d\u091f\u094b', '\u0928\u094b\u092d\u0947', '\u0921\u093f\u0938\u0947'],
+  WEEKDAYS: ['\u0906\u0907\u0924\u092c\u093e\u0930', '\u0938\u094b\u092e\u092c\u093e\u0930', '\u092e\u0919\u094d\u0917\u0932\u092c\u093e\u0930', '\u092c\u0941\u0927\u092c\u093e\u0930', '\u092c\u093f\u0939\u0940\u092c\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u092c\u093e\u0930', '\u0936\u0928\u093f\u092c\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0906\u0907\u0924', '\u0938\u094b\u092e', '\u092e\u0919\u094d\u0917\u0932', '\u092c\u0941\u0927', '\u092c\u093f\u0939\u0940', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u092a\u0939\u093f\u0932\u094b \u0938\u0924\u094d\u0930', '\u0926\u094b\u0938\u094d\u0930\u094b \u0938\u0924\u094d\u0930', '\u0924\u0947\u0938\u094d\u0930\u094b \u0938\u0924\u094d\u0930', '\u091a\u094c\u0925\u094b \u0938\u0924\u094d\u0930'],
+  AMPMS: ['\u092a\u0942\u0930\u094d\u0935 \u092e\u0927\u094d\u092f\u093e\u0928\u094d\u0939', '\u0909\u0924\u094d\u0924\u0930 \u092e\u0927\u094d\u092f\u093e\u0928\u094d\u0939'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne_IN.js
new file mode 100644
index 0000000..27a4a34
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0908\u0938\u093e \u092a\u0942\u0930\u094d\u0935', '\u0938\u0928\u094d'],
+  ERANAMES: ['\u0908\u0938\u093e \u092a\u0942\u0930\u094d\u0935', '\u0938\u0928\u094d'],
+  NARROWMONTHS: ['\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e', '\u096f', '\u0967\u0966', '\u0967\u0967', '\u0967\u0968'],
+  MONTHS: ['\u091c\u0928\u0935\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0905\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0941\u0928', '\u091c\u0941\u0932\u093e\u0908', '\u0905\u0917\u0938\u094d\u0924', '\u0938\u0947\u092a\u094d\u091f\u0947\u092e\u094d\u092c\u0930', '\u0905\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u092d\u0947\u092e\u094d\u092c\u0930', '\u0921\u093f\u0938\u0947\u092e\u094d\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u0928', '\u092b\u0947\u092c', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u093f', '\u092e\u0947', '\u091c\u0941\u0928', '\u091c\u0941\u0932\u093e', '\u0905\u0917', '\u0938\u0947\u092a\u094d\u091f', '\u0905\u0915\u094d\u091f\u094b', '\u0928\u094b\u092d\u0947', '\u0921\u093f\u0938\u0947'],
+  WEEKDAYS: ['\u0906\u0907\u0924\u092c\u093e\u0930', '\u0938\u094b\u092e\u092c\u093e\u0930', '\u092e\u0919\u094d\u0917\u0932\u092c\u093e\u0930', '\u092c\u0941\u0927\u092c\u093e\u0930', '\u092c\u093f\u0939\u0940\u092c\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u092c\u093e\u0930', '\u0936\u0928\u093f\u092c\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0906\u0907\u0924', '\u0938\u094b\u092e', '\u092e\u0919\u094d\u0917\u0932', '\u092c\u0941\u0927', '\u092c\u093f\u0939\u0940', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u092a\u0939\u093f\u0932\u094b \u0938\u0924\u094d\u0930', '\u0926\u094b\u0938\u094d\u0930\u094b \u0938\u0924\u094d\u0930', '\u0924\u0947\u0938\u094d\u0930\u094b \u0938\u0924\u094d\u0930', '\u091a\u094c\u0925\u094b \u0938\u0924\u094d\u0930'],
+  AMPMS: ['\u092a\u0942\u0930\u094d\u0935 \u092e\u0927\u094d\u092f\u093e\u0928\u094d\u0939', '\u0909\u0924\u094d\u0924\u0930 \u092e\u0927\u094d\u092f\u093e\u0928\u094d\u0939'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne_NP.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne_NP.js
new file mode 100644
index 0000000..3f3f141
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ne_NP.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0908\u0938\u093e \u092a\u0942\u0930\u094d\u0935', '\u0938\u0928\u094d'],
+  ERANAMES: ['\u0908\u0938\u093e \u092a\u0942\u0930\u094d\u0935', '\u0938\u0928\u094d'],
+  NARROWMONTHS: ['\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e', '\u096f', '\u0967\u0966', '\u0967\u0967', '\u0967\u0968'],
+  MONTHS: ['\u091c\u0928\u0935\u0930\u0940', '\u092b\u0947\u092c\u094d\u0930\u0941\u0905\u0930\u0940', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u093f\u0932', '\u092e\u0947', '\u091c\u0941\u0928', '\u091c\u0941\u0932\u093e\u0908', '\u0905\u0917\u0938\u094d\u0924', '\u0938\u0947\u092a\u094d\u091f\u0947\u092e\u094d\u092c\u0930', '\u0905\u0915\u094d\u091f\u094b\u092c\u0930', '\u0928\u094b\u092d\u0947\u092e\u094d\u092c\u0930', '\u0921\u093f\u0938\u0947\u092e\u094d\u092c\u0930'],
+  SHORTMONTHS: ['\u091c\u0928', '\u092b\u0947\u092c', '\u092e\u093e\u0930\u094d\u091a', '\u0905\u092a\u094d\u0930\u093f', '\u092e\u0947', '\u091c\u0941\u0928', '\u091c\u0941\u0932\u093e', '\u0905\u0917', '\u0938\u0947\u092a\u094d\u091f', '\u0905\u0915\u094d\u091f\u094b', '\u0928\u094b\u092d\u0947', '\u0921\u093f\u0938\u0947'],
+  WEEKDAYS: ['\u0906\u0907\u0924\u092c\u093e\u0930', '\u0938\u094b\u092e\u092c\u093e\u0930', '\u092e\u0919\u094d\u0917\u0932\u092c\u093e\u0930', '\u092c\u0941\u0927\u092c\u093e\u0930', '\u092c\u093f\u0939\u0940\u092c\u093e\u0930', '\u0936\u0941\u0915\u094d\u0930\u092c\u093e\u0930', '\u0936\u0928\u093f\u092c\u093e\u0930'],
+  SHORTWEEKDAYS: ['\u0906\u0907\u0924', '\u0938\u094b\u092e', '\u092e\u0919\u094d\u0917\u0932', '\u092c\u0941\u0927', '\u092c\u093f\u0939\u0940', '\u0936\u0941\u0915\u094d\u0930', '\u0936\u0928\u093f'],
+  NARROWWEEKDAYS: ['\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u092a\u0939\u093f\u0932\u094b \u0938\u0924\u094d\u0930', '\u0926\u094b\u0938\u094d\u0930\u094b \u0938\u0924\u094d\u0930', '\u0924\u0947\u0938\u094d\u0930\u094b \u0938\u0924\u094d\u0930', '\u091a\u094c\u0925\u094b \u0938\u0924\u094d\u0930'],
+  AMPMS: ['\u092a\u0942\u0930\u094d\u0935 \u092e\u0927\u094d\u092f\u093e\u0928\u094d\u0939', '\u0909\u0924\u094d\u0924\u0930 \u092e\u0927\u094d\u092f\u093e\u0928\u094d\u0939'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl.js
new file mode 100644
index 0000000..093ed6d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['Voor Christus', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mrt.', 'apr.', 'mei', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'],
+  WEEKDAYS: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
+  SHORTWEEKDAYS: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
+  NARROWWEEKDAYS: ['Z', 'M', 'D', 'W', 'D', 'V', 'Z'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1e kwartaal', '2e kwartaal', '3e kwartaal', '4e kwartaal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd-MM-yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl_BE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl_BE.js
new file mode 100644
index 0000000..c24a980
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl_BE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['Voor Christus', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mrt.', 'apr.', 'mei', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'],
+  WEEKDAYS: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
+  SHORTWEEKDAYS: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
+  NARROWWEEKDAYS: ['Z', 'M', 'D', 'W', 'D', 'V', 'Z'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1e kwartaal', '2e kwartaal', '3e kwartaal', '4e kwartaal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd-MMM-y', 'd/MM/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl_NL.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl_NL.js
new file mode 100644
index 0000000..093ed6d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nl_NL.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['Voor Christus', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januari', 'februari', 'maart', 'april', 'mei', 'juni', 'juli', 'augustus', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mrt.', 'apr.', 'mei', 'jun.', 'jul.', 'aug.', 'sep.', 'okt.', 'nov.', 'dec.'],
+  WEEKDAYS: ['zondag', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag'],
+  SHORTWEEKDAYS: ['zo', 'ma', 'di', 'wo', 'do', 'vr', 'za'],
+  NARROWWEEKDAYS: ['Z', 'M', 'D', 'W', 'D', 'V', 'Z'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1e kwartaal', '2e kwartaal', '3e kwartaal', '4e kwartaal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd-MM-yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nn.js
new file mode 100644
index 0000000..fab423e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f.Kr.', 'e.Kr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'des'],
+  WEEKDAYS: ['s\u00f8ndag', 'm\u00e5ndag', 'tysdag', 'onsdag', 'torsdag', 'fredag', 'laurdag'],
+  SHORTWEEKDAYS: ['s\u00f8.', 'm\u00e5.', 'ty.', 'on.', 'to.', 'fr.', 'la.'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['formiddag', 'ettermiddag'],
+  DATEFORMATS: ['EEEE d. MMMM y', 'd. MMMM y', 'd. MMM. y', 'dd.MM.yy'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nn_NO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nn_NO.js
new file mode 100644
index 0000000..fab423e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nn_NO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f.Kr.', 'e.Kr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'mai', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'des'],
+  WEEKDAYS: ['s\u00f8ndag', 'm\u00e5ndag', 'tysdag', 'onsdag', 'torsdag', 'fredag', 'laurdag'],
+  SHORTWEEKDAYS: ['s\u00f8.', 'm\u00e5.', 'ty.', 'on.', 'to.', 'fr.', 'la.'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['formiddag', 'ettermiddag'],
+  DATEFORMATS: ['EEEE d. MMMM y', 'd. MMMM y', 'd. MMM. y', 'dd.MM.yy'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__no.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__no.js
new file mode 100644
index 0000000..8edb505
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__no.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f.Kr.', 'e.Kr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember'],
+  SHORTMONTHS: ['jan.', 'feb.', 'mars', 'apr.', 'mai', 'juni', 'juli', 'aug.', 'sep.', 'okt.', 'nov.', 'des.'],
+  WEEKDAYS: ['s\u00f8ndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f8rdag'],
+  SHORTWEEKDAYS: ['s\u00f8n.', 'man.', 'tir.', 'ons.', 'tor.', 'fre.', 'l\u00f8r.'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d. MMMM y', 'd. MMMM y', 'd. MMM y', 'dd.MM.yy'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nr.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nr.js
new file mode 100644
index 0000000..578ff30
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nr.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Janabari', 'uFeberbari', 'uMatjhi', 'u-Apreli', 'Meyi', 'Juni', 'Julayi', 'Arhostosi', 'Septemba', 'Oktoba', 'Usinyikhaba', 'Disemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mat', 'Apr', 'Mey', 'Jun', 'Jul', 'Arh', 'Sep', 'Okt', 'Usi', 'Dis'],
+  WEEKDAYS: ['uSonto', 'uMvulo', 'uLesibili', 'Lesithathu', 'uLesine', 'ngoLesihlanu', 'umGqibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mvu', 'Bil', 'Tha', 'Ne', 'Hla', 'Gqi'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nr_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nr_ZA.js
new file mode 100644
index 0000000..578ff30
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nr_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Janabari', 'uFeberbari', 'uMatjhi', 'u-Apreli', 'Meyi', 'Juni', 'Julayi', 'Arhostosi', 'Septemba', 'Oktoba', 'Usinyikhaba', 'Disemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mat', 'Apr', 'Mey', 'Jun', 'Jul', 'Arh', 'Sep', 'Okt', 'Usi', 'Dis'],
+  WEEKDAYS: ['uSonto', 'uMvulo', 'uLesibili', 'Lesithathu', 'uLesine', 'ngoLesihlanu', 'umGqibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mvu', 'Bil', 'Tha', 'Ne', 'Hla', 'Gqi'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nso.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nso.js
new file mode 100644
index 0000000..22c1765
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nso.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Janaware', 'Feberware', 'Mat\u0161he', 'Aporele', 'Mei', 'June', 'Julae', 'Agostose', 'Setemere', 'Oktobore', 'Nofemere', 'Disemere'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mat', 'Apo', 'Mei', 'Jun', 'Jul', 'Ago', 'Set', 'Okt', 'Nof', 'Dis'],
+  WEEKDAYS: ['Sontaga', 'Mosupalogo', 'Labobedi', 'Laboraro', 'Labone', 'Labohlano', 'Mokibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mos', 'Bed', 'Rar', 'Ne', 'Hla', 'Mok'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nso_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nso_ZA.js
new file mode 100644
index 0000000..22c1765
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__nso_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Janaware', 'Feberware', 'Mat\u0161he', 'Aporele', 'Mei', 'June', 'Julae', 'Agostose', 'Setemere', 'Oktobore', 'Nofemere', 'Disemere'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mat', 'Apo', 'Mei', 'Jun', 'Jul', 'Ago', 'Set', 'Okt', 'Nof', 'Dis'],
+  WEEKDAYS: ['Sontaga', 'Mosupalogo', 'Labobedi', 'Laboraro', 'Labone', 'Labohlano', 'Mokibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mos', 'Bed', 'Rar', 'Ne', 'Hla', 'Mok'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ny.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ny.js
new file mode 100644
index 0000000..ad58225
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ny.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januwale', 'Febuluwale', 'Malichi', 'Epulo', 'Mei', 'Juni', 'Julai', 'Ogasiti', 'Seputemba', 'Okutoba', 'Novemba', 'Disemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mal', 'Epu', 'Mei', 'Jun', 'Jul', 'Oga', 'Sep', 'Oku', 'Nov', 'Dis'],
+  WEEKDAYS: ['Lamulungu', 'Lolemba', 'Lachiwiri', 'Lachitatu', 'Lachinayi', 'Lachisanu', 'Loweruka'],
+  SHORTWEEKDAYS: ['Mul', 'Lem', 'Wir', 'Tat', 'Nai', 'San', 'Wer'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ny_MW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ny_MW.js
new file mode 100644
index 0000000..ad58225
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ny_MW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januwale', 'Febuluwale', 'Malichi', 'Epulo', 'Mei', 'Juni', 'Julai', 'Ogasiti', 'Seputemba', 'Okutoba', 'Novemba', 'Disemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mal', 'Epu', 'Mei', 'Jun', 'Jul', 'Oga', 'Sep', 'Oku', 'Nov', 'Dis'],
+  WEEKDAYS: ['Lamulungu', 'Lolemba', 'Lachiwiri', 'Lachitatu', 'Lachinayi', 'Lachisanu', 'Loweruka'],
+  SHORTWEEKDAYS: ['Mul', 'Lem', 'Wir', 'Tat', 'Nai', 'San', 'Wer'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__oc.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__oc.js
new file mode 100644
index 0000000..f8e14ab
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__oc.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['geni\u00e8r', 'febri\u00e8r', 'mar\u00e7', 'abril', 'mai', 'junh', 'julhet', 'agost', 'setembre', 'oct\u00f2bre', 'novembre', 'dezembre'],
+  SHORTMONTHS: ['geni\u00e8r', 'febri\u00e8r', 'mar\u00e7', 'abril', 'mai', 'junh', 'julhet', 'agost', 'setembre', 'oct\u00f2bre', 'novembre', 'dezembre'],
+  WEEKDAYS: ['Dimenge', 'diluns', 'dimar\u00e7', 'dim\u00e8cres', 'dij\u00f2us', 'div\u00e8ndres', 'dissabte'],
+  SHORTWEEKDAYS: ['Dimenge', 'diluns', 'dimar\u00e7', 'dim\u00e8cres', 'dij\u00f2us', 'div\u00e8ndres', 'dissabte'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__oc_FR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__oc_FR.js
new file mode 100644
index 0000000..f8e14ab
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__oc_FR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['geni\u00e8r', 'febri\u00e8r', 'mar\u00e7', 'abril', 'mai', 'junh', 'julhet', 'agost', 'setembre', 'oct\u00f2bre', 'novembre', 'dezembre'],
+  SHORTMONTHS: ['geni\u00e8r', 'febri\u00e8r', 'mar\u00e7', 'abril', 'mai', 'junh', 'julhet', 'agost', 'setembre', 'oct\u00f2bre', 'novembre', 'dezembre'],
+  WEEKDAYS: ['Dimenge', 'diluns', 'dimar\u00e7', 'dim\u00e8cres', 'dij\u00f2us', 'div\u00e8ndres', 'dissabte'],
+  SHORTWEEKDAYS: ['Dimenge', 'diluns', 'dimar\u00e7', 'dim\u00e8cres', 'dij\u00f2us', 'div\u00e8ndres', 'dissabte'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om.js
new file mode 100644
index 0000000..cf99233
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KD', 'KB'],
+  ERANAMES: ['KD', 'KB'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Amajjii', 'Guraandhala', 'Bitooteessa', 'Elba', 'Caamsa', 'Waxabajjii', 'Adooleessa', 'Hagayya', 'Fuulbana', 'Onkololeessa', 'Sadaasa', 'Muddee'],
+  SHORTMONTHS: ['Ama', 'Gur', 'Bit', 'Elb', 'Cam', 'Wax', 'Ado', 'Hag', 'Ful', 'Onk', 'Sad', 'Mud'],
+  WEEKDAYS: ['Dilbata', 'Wiixata', 'Qibxata', 'Roobii', 'Kamiisa', 'Jimaata', 'Sanbata'],
+  SHORTWEEKDAYS: ['Dil', 'Wix', 'Qib', 'Rob', 'Kam', 'Jim', 'San'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['WD', 'WB'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om_ET.js
new file mode 100644
index 0000000..024c7d7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KD', 'KB'],
+  ERANAMES: ['KD', 'KB'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Amajjii', 'Guraandhala', 'Bitooteessa', 'Elba', 'Caamsa', 'Waxabajjii', 'Adooleessa', 'Hagayya', 'Fuulbana', 'Onkololeessa', 'Sadaasa', 'Muddee'],
+  SHORTMONTHS: ['Ama', 'Gur', 'Bit', 'Elb', 'Cam', 'Wax', 'Ado', 'Hag', 'Ful', 'Onk', 'Sad', 'Mud'],
+  WEEKDAYS: ['Dilbata', 'Wiixata', 'Qibxata', 'Roobii', 'Kamiisa', 'Jimaata', 'Sanbata'],
+  SHORTWEEKDAYS: ['Dil', 'Wix', 'Qib', 'Rob', 'Kam', 'Jim', 'San'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['WD', 'WB'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om_KE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om_KE.js
new file mode 100644
index 0000000..024c7d7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__om_KE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KD', 'KB'],
+  ERANAMES: ['KD', 'KB'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Amajjii', 'Guraandhala', 'Bitooteessa', 'Elba', 'Caamsa', 'Waxabajjii', 'Adooleessa', 'Hagayya', 'Fuulbana', 'Onkololeessa', 'Sadaasa', 'Muddee'],
+  SHORTMONTHS: ['Ama', 'Gur', 'Bit', 'Elb', 'Cam', 'Wax', 'Ado', 'Hag', 'Ful', 'Onk', 'Sad', 'Mud'],
+  WEEKDAYS: ['Dilbata', 'Wiixata', 'Qibxata', 'Roobii', 'Kamiisa', 'Jimaata', 'Sanbata'],
+  SHORTWEEKDAYS: ['Dil', 'Wix', 'Qib', 'Rob', 'Kam', 'Jim', 'San'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['WD', 'WB'],
+  DATEFORMATS: ['EEEE, MMMM d, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__or.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__or.js
new file mode 100644
index 0000000..67bc79a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__or.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['\u0b1c\u0b3e', '\u0b2b\u0b47', '\u0b2e\u0b3e', '\u0b05', '\u0b2e\u0b47', '\u0b1c\u0b41', '\u0b1c\u0b41', '\u0b05', '\u0b38\u0b47', '\u0b05', '\u0b28', '\u0b21\u0b3f'],
+  MONTHS: ['\u0b1c\u0b3e\u0b28\u0b41\u0b06\u0b30\u0b40', '\u0b2b\u0b47\u0b2c\u0b4d\u0b30\u0b41\u0b5f\u0b3e\u0b30\u0b40', '\u0b2e\u0b3e\u0b30\u0b4d\u0b1a\u0b4d\u0b1a', '\u0b05\u0b2a\u0b4d\u0b30\u0b47\u0b32', '\u0b2e\u0b47', '\u0b1c\u0b41\u0b28', '\u0b1c\u0b41\u0b32\u0b3e\u0b07', '\u0b05\u0b17\u0b37\u0b4d\u0b1f', '\u0b38\u0b47\u0b2a\u0b4d\u0b1f\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b05\u0b15\u0b4d\u0b1f\u0b4b\u0b2c\u0b30', '\u0b28\u0b2d\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b21\u0b3f\u0b38\u0b47\u0b2e\u0b4d\u0b2c\u0b30'],
+  SHORTMONTHS: ['\u0b1c\u0b3e\u0b28\u0b41\u0b06\u0b30\u0b40', '\u0b2b\u0b47\u0b2c\u0b4d\u0b30\u0b41\u0b5f\u0b3e\u0b30\u0b40', '\u0b2e\u0b3e\u0b30\u0b4d\u0b1a\u0b4d\u0b1a', '\u0b05\u0b2a\u0b4d\u0b30\u0b47\u0b32', '\u0b2e\u0b47', '\u0b1c\u0b41\u0b28', '\u0b1c\u0b41\u0b32\u0b3e\u0b07', '\u0b05\u0b17\u0b37\u0b4d\u0b1f', '\u0b38\u0b47\u0b2a\u0b4d\u0b1f\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b05\u0b15\u0b4d\u0b1f\u0b4b\u0b2c\u0b30', '\u0b28\u0b2d\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b21\u0b3f\u0b38\u0b47\u0b2e\u0b4d\u0b2c\u0b30'],
+  WEEKDAYS: ['\u0b30\u0b2c\u0b3f\u0b2c\u0b3e\u0b30', '\u0b38\u0b4b\u0b2e\u0b2c\u0b3e\u0b30', '\u0b2e\u0b19\u0b4d\u0b17\u0b33\u0b2c\u0b3e\u0b30', '\u0b2c\u0b41\u0b27\u0b2c\u0b3e\u0b30', '\u0b17\u0b41\u0b30\u0b41\u0b2c\u0b3e\u0b30', '\u0b36\u0b41\u0b15\u0b4d\u0b30\u0b2c\u0b3e\u0b30', '\u0b36\u0b28\u0b3f\u0b2c\u0b3e\u0b30'],
+  SHORTWEEKDAYS: ['\u0b30\u0b2c\u0b3f', '\u0b38\u0b4b\u0b2e', '\u0b2e\u0b19\u0b4d\u0b17\u0b33', '\u0b2c\u0b41\u0b27', '\u0b17\u0b41\u0b30\u0b41', '\u0b36\u0b41\u0b15\u0b4d\u0b30', '\u0b36\u0b28\u0b3f'],
+  NARROWWEEKDAYS: ['\u0b30', '\u0b38\u0b4b', '\u0b2e', '\u0b2c\u0b41', '\u0b17\u0b41', '\u0b36\u0b41', '\u0b36'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd MMM y', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__or_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__or_IN.js
new file mode 100644
index 0000000..67bc79a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__or_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['\u0b1c\u0b3e', '\u0b2b\u0b47', '\u0b2e\u0b3e', '\u0b05', '\u0b2e\u0b47', '\u0b1c\u0b41', '\u0b1c\u0b41', '\u0b05', '\u0b38\u0b47', '\u0b05', '\u0b28', '\u0b21\u0b3f'],
+  MONTHS: ['\u0b1c\u0b3e\u0b28\u0b41\u0b06\u0b30\u0b40', '\u0b2b\u0b47\u0b2c\u0b4d\u0b30\u0b41\u0b5f\u0b3e\u0b30\u0b40', '\u0b2e\u0b3e\u0b30\u0b4d\u0b1a\u0b4d\u0b1a', '\u0b05\u0b2a\u0b4d\u0b30\u0b47\u0b32', '\u0b2e\u0b47', '\u0b1c\u0b41\u0b28', '\u0b1c\u0b41\u0b32\u0b3e\u0b07', '\u0b05\u0b17\u0b37\u0b4d\u0b1f', '\u0b38\u0b47\u0b2a\u0b4d\u0b1f\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b05\u0b15\u0b4d\u0b1f\u0b4b\u0b2c\u0b30', '\u0b28\u0b2d\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b21\u0b3f\u0b38\u0b47\u0b2e\u0b4d\u0b2c\u0b30'],
+  SHORTMONTHS: ['\u0b1c\u0b3e\u0b28\u0b41\u0b06\u0b30\u0b40', '\u0b2b\u0b47\u0b2c\u0b4d\u0b30\u0b41\u0b5f\u0b3e\u0b30\u0b40', '\u0b2e\u0b3e\u0b30\u0b4d\u0b1a\u0b4d\u0b1a', '\u0b05\u0b2a\u0b4d\u0b30\u0b47\u0b32', '\u0b2e\u0b47', '\u0b1c\u0b41\u0b28', '\u0b1c\u0b41\u0b32\u0b3e\u0b07', '\u0b05\u0b17\u0b37\u0b4d\u0b1f', '\u0b38\u0b47\u0b2a\u0b4d\u0b1f\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b05\u0b15\u0b4d\u0b1f\u0b4b\u0b2c\u0b30', '\u0b28\u0b2d\u0b47\u0b2e\u0b4d\u0b2c\u0b30', '\u0b21\u0b3f\u0b38\u0b47\u0b2e\u0b4d\u0b2c\u0b30'],
+  WEEKDAYS: ['\u0b30\u0b2c\u0b3f\u0b2c\u0b3e\u0b30', '\u0b38\u0b4b\u0b2e\u0b2c\u0b3e\u0b30', '\u0b2e\u0b19\u0b4d\u0b17\u0b33\u0b2c\u0b3e\u0b30', '\u0b2c\u0b41\u0b27\u0b2c\u0b3e\u0b30', '\u0b17\u0b41\u0b30\u0b41\u0b2c\u0b3e\u0b30', '\u0b36\u0b41\u0b15\u0b4d\u0b30\u0b2c\u0b3e\u0b30', '\u0b36\u0b28\u0b3f\u0b2c\u0b3e\u0b30'],
+  SHORTWEEKDAYS: ['\u0b30\u0b2c\u0b3f', '\u0b38\u0b4b\u0b2e', '\u0b2e\u0b19\u0b4d\u0b17\u0b33', '\u0b2c\u0b41\u0b27', '\u0b17\u0b41\u0b30\u0b41', '\u0b36\u0b41\u0b15\u0b4d\u0b30', '\u0b36\u0b28\u0b3f'],
+  NARROWWEEKDAYS: ['\u0b30', '\u0b38\u0b4b', '\u0b2e', '\u0b2c\u0b41', '\u0b17\u0b41', '\u0b36\u0b41', '\u0b36'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'd MMM y', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa.js
new file mode 100644
index 0000000..4baa43a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0a08\u0a38\u0a3e\u0a2a\u0a42\u0a30\u0a35', '\u0a38\u0a70\u0a28'],
+  NARROWMONTHS: ['\u0a1c', '\u0a2b', '\u0a2e\u0a3e', '\u0a05', '\u0a2e', '\u0a1c\u0a42', '\u0a1c\u0a41', '\u0a05', '\u0a38', '\u0a05', '\u0a28', '\u0a26'],
+  MONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  SHORTMONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  WEEKDAYS: ['\u0a10\u0a24\u0a35\u0a3e\u0a30', '\u0a38\u0a4b\u0a2e\u0a35\u0a3e\u0a30', '\u0a2e\u0a70\u0a17\u0a32\u0a35\u0a3e\u0a30', '\u0a2c\u0a41\u0a27\u0a35\u0a3e\u0a30', '\u0a35\u0a40\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a41\u0a71\u0a15\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a28\u0a40\u0a1a\u0a30\u0a35\u0a3e\u0a30'],
+  SHORTWEEKDAYS: ['\u0a10\u0a24.', '\u0a38\u0a4b\u0a2e.', '\u0a2e\u0a70\u0a17\u0a32.', '\u0a2c\u0a41\u0a27.', '\u0a35\u0a40\u0a30.', '\u0a38\u0a3c\u0a41\u0a15\u0a30.', '\u0a38\u0a3c\u0a28\u0a40.'],
+  NARROWWEEKDAYS: ['\u0a10', '\u0a38\u0a4b', '\u0a2e\u0a70', '\u0a2c\u0a41\u0a71', '\u0a35\u0a40', '\u0a38\u0a3c\u0a41\u0a71', '\u0a38\u0a3c'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0a2a\u0a39\u0a3f\u0a32\u0a3e\u0a02 \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a26\u0a42\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a24\u0a40\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a1a\u0a4c\u0a25\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08'],
+  AMPMS: ['\u0a38\u0a35\u0a47\u0a30\u0a47', '\u0a38\u0a3c\u0a3e\u0a2e'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Arab.js
new file mode 100644
index 0000000..22097f3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Arab.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0627\u064a\u0633\u0627\u067e\u0648\u0631\u0648', '\u0633\u06ba'],
+  NARROWMONTHS: ['\u0a1c', '\u0a2b', '\u0a2e\u0a3e', '\u0a05', '\u0a2e', '\u0a1c\u0a42', '\u0a1c\u0a41', '\u0a05', '\u0a38', '\u0a05', '\u0a28', '\u0a26'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u06cc\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u064f\u062f\u06be', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  SHORTWEEKDAYS: ['\u0a10\u0a24.', '\u0a38\u0a4b\u0a2e.', '\u0a2e\u0a70\u0a17\u0a32.', '\u0a2c\u0a41\u0a27.', '\u0a35\u0a40\u0a30.', '\u0a38\u0a3c\u0a41\u0a15\u0a30.', '\u0a38\u0a3c\u0a28\u0a40.'],
+  NARROWWEEKDAYS: ['\u0a10', '\u0a38\u0a4b', '\u0a2e\u0a70', '\u0a2c\u0a41\u0a71', '\u0a35\u0a40', '\u0a38\u0a3c\u0a41\u0a71', '\u0a38\u0a3c'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0686\u0648\u062a\u06be\u0627\u064a \u067e\u06c1\u0644\u0627\u06ba', '\u0686\u0648\u062a\u06be\u0627\u064a \u062f\u0648\u062c\u0627', '\u0686\u0648\u062a\u06be\u0627\u064a \u062a\u064a\u062c\u0627', '\u0686\u0648\u062a\u06be\u0627\u064a \u0686\u0648\u062a\u06be\u0627'],
+  AMPMS: ['\u0a38\u0a35\u0a47\u0a30\u0a47', '\u0a38\u0a3c\u0a3e\u0a2e'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Arab_PK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Arab_PK.js
new file mode 100644
index 0000000..a496ec4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Arab_PK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0627\u064a\u0633\u0627\u067e\u0648\u0631\u0648', '\u0633\u06ba'],
+  NARROWMONTHS: ['\u0a1c', '\u0a2b', '\u0a2e\u0a3e', '\u0a05', '\u0a2e', '\u0a1c\u0a42', '\u0a1c\u0a41', '\u0a05', '\u0a38', '\u0a05', '\u0a28', '\u0a26'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u06cc\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u064f\u062f\u06be', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  SHORTWEEKDAYS: ['\u0a10\u0a24.', '\u0a38\u0a4b\u0a2e.', '\u0a2e\u0a70\u0a17\u0a32.', '\u0a2c\u0a41\u0a27.', '\u0a35\u0a40\u0a30.', '\u0a38\u0a3c\u0a41\u0a15\u0a30.', '\u0a38\u0a3c\u0a28\u0a40.'],
+  NARROWWEEKDAYS: ['\u0a10', '\u0a38\u0a4b', '\u0a2e\u0a70', '\u0a2c\u0a41\u0a71', '\u0a35\u0a40', '\u0a38\u0a3c\u0a41\u0a71', '\u0a38\u0a3c'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0686\u0648\u062a\u06be\u0627\u064a \u067e\u06c1\u0644\u0627\u06ba', '\u0686\u0648\u062a\u06be\u0627\u064a \u062f\u0648\u062c\u0627', '\u0686\u0648\u062a\u06be\u0627\u064a \u062a\u064a\u062c\u0627', '\u0686\u0648\u062a\u06be\u0627\u064a \u0686\u0648\u062a\u06be\u0627'],
+  AMPMS: ['\u0a38\u0a35\u0a47\u0a30\u0a47', '\u0a38\u0a3c\u0a3e\u0a2e'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Guru.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Guru.js
new file mode 100644
index 0000000..4baa43a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Guru.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0a08\u0a38\u0a3e\u0a2a\u0a42\u0a30\u0a35', '\u0a38\u0a70\u0a28'],
+  NARROWMONTHS: ['\u0a1c', '\u0a2b', '\u0a2e\u0a3e', '\u0a05', '\u0a2e', '\u0a1c\u0a42', '\u0a1c\u0a41', '\u0a05', '\u0a38', '\u0a05', '\u0a28', '\u0a26'],
+  MONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  SHORTMONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  WEEKDAYS: ['\u0a10\u0a24\u0a35\u0a3e\u0a30', '\u0a38\u0a4b\u0a2e\u0a35\u0a3e\u0a30', '\u0a2e\u0a70\u0a17\u0a32\u0a35\u0a3e\u0a30', '\u0a2c\u0a41\u0a27\u0a35\u0a3e\u0a30', '\u0a35\u0a40\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a41\u0a71\u0a15\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a28\u0a40\u0a1a\u0a30\u0a35\u0a3e\u0a30'],
+  SHORTWEEKDAYS: ['\u0a10\u0a24.', '\u0a38\u0a4b\u0a2e.', '\u0a2e\u0a70\u0a17\u0a32.', '\u0a2c\u0a41\u0a27.', '\u0a35\u0a40\u0a30.', '\u0a38\u0a3c\u0a41\u0a15\u0a30.', '\u0a38\u0a3c\u0a28\u0a40.'],
+  NARROWWEEKDAYS: ['\u0a10', '\u0a38\u0a4b', '\u0a2e\u0a70', '\u0a2c\u0a41\u0a71', '\u0a35\u0a40', '\u0a38\u0a3c\u0a41\u0a71', '\u0a38\u0a3c'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0a2a\u0a39\u0a3f\u0a32\u0a3e\u0a02 \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a26\u0a42\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a24\u0a40\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a1a\u0a4c\u0a25\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08'],
+  AMPMS: ['\u0a38\u0a35\u0a47\u0a30\u0a47', '\u0a38\u0a3c\u0a3e\u0a2e'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Guru_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Guru_IN.js
new file mode 100644
index 0000000..4baa43a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_Guru_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0a08\u0a38\u0a3e\u0a2a\u0a42\u0a30\u0a35', '\u0a38\u0a70\u0a28'],
+  NARROWMONTHS: ['\u0a1c', '\u0a2b', '\u0a2e\u0a3e', '\u0a05', '\u0a2e', '\u0a1c\u0a42', '\u0a1c\u0a41', '\u0a05', '\u0a38', '\u0a05', '\u0a28', '\u0a26'],
+  MONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  SHORTMONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  WEEKDAYS: ['\u0a10\u0a24\u0a35\u0a3e\u0a30', '\u0a38\u0a4b\u0a2e\u0a35\u0a3e\u0a30', '\u0a2e\u0a70\u0a17\u0a32\u0a35\u0a3e\u0a30', '\u0a2c\u0a41\u0a27\u0a35\u0a3e\u0a30', '\u0a35\u0a40\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a41\u0a71\u0a15\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a28\u0a40\u0a1a\u0a30\u0a35\u0a3e\u0a30'],
+  SHORTWEEKDAYS: ['\u0a10\u0a24.', '\u0a38\u0a4b\u0a2e.', '\u0a2e\u0a70\u0a17\u0a32.', '\u0a2c\u0a41\u0a27.', '\u0a35\u0a40\u0a30.', '\u0a38\u0a3c\u0a41\u0a15\u0a30.', '\u0a38\u0a3c\u0a28\u0a40.'],
+  NARROWWEEKDAYS: ['\u0a10', '\u0a38\u0a4b', '\u0a2e\u0a70', '\u0a2c\u0a41\u0a71', '\u0a35\u0a40', '\u0a38\u0a3c\u0a41\u0a71', '\u0a38\u0a3c'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0a2a\u0a39\u0a3f\u0a32\u0a3e\u0a02 \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a26\u0a42\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a24\u0a40\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a1a\u0a4c\u0a25\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08'],
+  AMPMS: ['\u0a38\u0a35\u0a47\u0a30\u0a47', '\u0a38\u0a3c\u0a3e\u0a2e'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_IN.js
new file mode 100644
index 0000000..4baa43a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0a08\u0a38\u0a3e\u0a2a\u0a42\u0a30\u0a35', '\u0a38\u0a70\u0a28'],
+  NARROWMONTHS: ['\u0a1c', '\u0a2b', '\u0a2e\u0a3e', '\u0a05', '\u0a2e', '\u0a1c\u0a42', '\u0a1c\u0a41', '\u0a05', '\u0a38', '\u0a05', '\u0a28', '\u0a26'],
+  MONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  SHORTMONTHS: ['\u0a1c\u0a28\u0a35\u0a30\u0a40', '\u0a2b\u0a3c\u0a30\u0a35\u0a30\u0a40', '\u0a2e\u0a3e\u0a30\u0a1a', '\u0a05\u0a2a\u0a4d\u0a30\u0a48\u0a32', '\u0a2e\u0a08', '\u0a1c\u0a42\u0a28', '\u0a1c\u0a41\u0a32\u0a3e\u0a08', '\u0a05\u0a17\u0a38\u0a24', '\u0a38\u0a24\u0a70\u0a2c\u0a30', '\u0a05\u0a15\u0a24\u0a42\u0a2c\u0a30', '\u0a28\u0a35\u0a70\u0a2c\u0a30', '\u0a26\u0a38\u0a70\u0a2c\u0a30'],
+  WEEKDAYS: ['\u0a10\u0a24\u0a35\u0a3e\u0a30', '\u0a38\u0a4b\u0a2e\u0a35\u0a3e\u0a30', '\u0a2e\u0a70\u0a17\u0a32\u0a35\u0a3e\u0a30', '\u0a2c\u0a41\u0a27\u0a35\u0a3e\u0a30', '\u0a35\u0a40\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a41\u0a71\u0a15\u0a30\u0a35\u0a3e\u0a30', '\u0a38\u0a3c\u0a28\u0a40\u0a1a\u0a30\u0a35\u0a3e\u0a30'],
+  SHORTWEEKDAYS: ['\u0a10\u0a24.', '\u0a38\u0a4b\u0a2e.', '\u0a2e\u0a70\u0a17\u0a32.', '\u0a2c\u0a41\u0a27.', '\u0a35\u0a40\u0a30.', '\u0a38\u0a3c\u0a41\u0a15\u0a30.', '\u0a38\u0a3c\u0a28\u0a40.'],
+  NARROWWEEKDAYS: ['\u0a10', '\u0a38\u0a4b', '\u0a2e\u0a70', '\u0a2c\u0a41\u0a71', '\u0a35\u0a40', '\u0a38\u0a3c\u0a41\u0a71', '\u0a38\u0a3c'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0a2a\u0a39\u0a3f\u0a32\u0a3e\u0a02 \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a26\u0a42\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a24\u0a40\u0a1c\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08', '\u0a1a\u0a4c\u0a25\u0a3e \u0a1a\u0a4c\u0a25\u0a3e\u0a08'],
+  AMPMS: ['\u0a38\u0a35\u0a47\u0a30\u0a47', '\u0a38\u0a3c\u0a3e\u0a2e'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_PK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_PK.js
new file mode 100644
index 0000000..a496ec4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pa_PK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0627\u064a\u0633\u0627\u067e\u0648\u0631\u0648', '\u0633\u06ba'],
+  NARROWMONTHS: ['\u0a1c', '\u0a2b', '\u0a2e\u0a3e', '\u0a05', '\u0a2e', '\u0a1c\u0a42', '\u0a1c\u0a41', '\u0a05', '\u0a38', '\u0a05', '\u0a28', '\u0a26'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u06cc\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u064f\u062f\u06be', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  SHORTWEEKDAYS: ['\u0a10\u0a24.', '\u0a38\u0a4b\u0a2e.', '\u0a2e\u0a70\u0a17\u0a32.', '\u0a2c\u0a41\u0a27.', '\u0a35\u0a40\u0a30.', '\u0a38\u0a3c\u0a41\u0a15\u0a30.', '\u0a38\u0a3c\u0a28\u0a40.'],
+  NARROWWEEKDAYS: ['\u0a10', '\u0a38\u0a4b', '\u0a2e\u0a70', '\u0a2c\u0a41\u0a71', '\u0a35\u0a40', '\u0a38\u0a3c\u0a41\u0a71', '\u0a38\u0a3c'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0686\u0648\u062a\u06be\u0627\u064a \u067e\u06c1\u0644\u0627\u06ba', '\u0686\u0648\u062a\u06be\u0627\u064a \u062f\u0648\u062c\u0627', '\u0686\u0648\u062a\u06be\u0627\u064a \u062a\u064a\u062c\u0627', '\u0686\u0648\u062a\u06be\u0627\u064a \u0686\u0648\u062a\u06be\u0627'],
+  AMPMS: ['\u0a38\u0a35\u0a47\u0a30\u0a47', '\u0a38\u0a3c\u0a3e\u0a2e'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'd MMMM y', 'd MMM y', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pl.js
new file mode 100644
index 0000000..a35bea4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p.n.e.', 'n.e.'],
+  ERANAMES: ['p.n.e.', 'n.e.'],
+  NARROWMONTHS: ['s', 'l', 'm', 'k', 'm', 'c', 'l', 's', 'w', 'p', 'l', 'g'],
+  MONTHS: ['stycznia', 'lutego', 'marca', 'kwietnia', 'maja', 'czerwca', 'lipca', 'sierpnia', 'wrze\u015bnia', 'pa\u017adziernika', 'listopada', 'grudnia'],
+  STANDALONEMONTHS: ['stycze\u0144', 'luty', 'marzec', 'kwiecie\u0144', 'maj', 'czerwiec', 'lipiec', 'sierpie\u0144', 'wrzesie\u0144', 'pa\u017adziernik', 'listopad', 'grudzie\u0144'],
+  SHORTMONTHS: ['sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', 'sie', 'wrz', 'pa\u017a', 'lis', 'gru'],
+  WEEKDAYS: ['niedziela', 'poniedzia\u0142ek', 'wtorek', '\u015broda', 'czwartek', 'pi\u0105tek', 'sobota'],
+  SHORTWEEKDAYS: ['niedz.', 'pon.', 'wt.', '\u015br.', 'czw.', 'pt.', 'sob.'],
+  NARROWWEEKDAYS: ['N', 'P', 'W', '\u015a', 'C', 'P', 'S'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['I kwarta\u0142', 'II kwarta\u0142', 'III kwarta\u0142', 'IV kwarta\u0142'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'dd-MM-yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pl_PL.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pl_PL.js
new file mode 100644
index 0000000..a35bea4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pl_PL.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p.n.e.', 'n.e.'],
+  ERANAMES: ['p.n.e.', 'n.e.'],
+  NARROWMONTHS: ['s', 'l', 'm', 'k', 'm', 'c', 'l', 's', 'w', 'p', 'l', 'g'],
+  MONTHS: ['stycznia', 'lutego', 'marca', 'kwietnia', 'maja', 'czerwca', 'lipca', 'sierpnia', 'wrze\u015bnia', 'pa\u017adziernika', 'listopada', 'grudnia'],
+  STANDALONEMONTHS: ['stycze\u0144', 'luty', 'marzec', 'kwiecie\u0144', 'maj', 'czerwiec', 'lipiec', 'sierpie\u0144', 'wrzesie\u0144', 'pa\u017adziernik', 'listopad', 'grudzie\u0144'],
+  SHORTMONTHS: ['sty', 'lut', 'mar', 'kwi', 'maj', 'cze', 'lip', 'sie', 'wrz', 'pa\u017a', 'lis', 'gru'],
+  WEEKDAYS: ['niedziela', 'poniedzia\u0142ek', 'wtorek', '\u015broda', 'czwartek', 'pi\u0105tek', 'sobota'],
+  SHORTWEEKDAYS: ['niedz.', 'pon.', 'wt.', '\u015br.', 'czw.', 'pt.', 'sob.'],
+  NARROWWEEKDAYS: ['N', 'P', 'W', '\u015a', 'C', 'P', 'S'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['I kwarta\u0142', 'II kwarta\u0142', 'III kwarta\u0142', 'IV kwarta\u0142'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'dd-MM-yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ps.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ps.js
new file mode 100644
index 0000000..3ea7e9e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ps.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642.\u0645.', '\u0645.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u064a', '\u0641\u0628\u0631\u0648\u0631\u064a', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u06cc', '\u0627\u06ab\u0633\u062a', '\u0633\u067e\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['1', '2', '3', '4', '\u0645\u0640\u06cc', '\u062c\u0648\u0646', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u063a.\u0645.', '\u063a.\u0648.'],
+  DATEFORMATS: ['EEEE \u062f y \u062f MMMM d', '\u062f y \u062f MMMM d', 'd MMM y', 'yyyy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ps_AF.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ps_AF.js
new file mode 100644
index 0000000..3ea7e9e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ps_AF.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642.\u0645.', '\u0645.'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u064a', '\u0641\u0628\u0631\u0648\u0631\u064a', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u06cc', '\u0627\u06ab\u0633\u062a', '\u0633\u067e\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['1', '2', '3', '4', '\u0645\u0640\u06cc', '\u062c\u0648\u0646', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u063a.\u0645.', '\u063a.\u0648.'],
+  DATEFORMATS: ['EEEE \u062f y \u062f MMMM d', '\u062f y \u062f MMMM d', 'd MMM y', 'yyyy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt.js
new file mode 100644
index 0000000..7416c3c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['Antes de Cristo', 'Ano do Senhor'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janeiro', 'fevereiro', 'mar\u00e7o', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'],
+  SHORTMONTHS: ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'],
+  WEEKDAYS: ['domingo', 'segunda-feira', 'ter\u00e7a-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1\u00ba trimestre', '2\u00ba trimestre', '3\u00ba trimestre', '4\u00ba trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ["HH'h'mm'min'ss's' zzzz", "HH'h'mm'min'ss's' z", 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt_BR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt_BR.js
new file mode 100644
index 0000000..7416c3c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt_BR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['Antes de Cristo', 'Ano do Senhor'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['janeiro', 'fevereiro', 'mar\u00e7o', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'],
+  SHORTMONTHS: ['jan', 'fev', 'mar', 'abr', 'mai', 'jun', 'jul', 'ago', 'set', 'out', 'nov', 'dez'],
+  WEEKDAYS: ['domingo', 'segunda-feira', 'ter\u00e7a-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1\u00ba trimestre', '2\u00ba trimestre', '3\u00ba trimestre', '4\u00ba trimestre'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", 'dd/MM/yyyy', 'dd/MM/yy'],
+  TIMEFORMATS: ["HH'h'mm'min'ss's' zzzz", "HH'h'mm'min'ss's' z", 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt_PT.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt_PT.js
new file mode 100644
index 0000000..ac4ace7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__pt_PT.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['a.C.', 'd.C.'],
+  ERANAMES: ['Antes de Cristo', 'Ano do Senhor'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Janeiro', 'Fevereiro', 'Mar\u00e7o', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'],
+  SHORTMONTHS: ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'],
+  WEEKDAYS: ['domingo', 'segunda-feira', 'ter\u00e7a-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 's\u00e1bado'],
+  SHORTWEEKDAYS: ['dom', 'seg', 'ter', 'qua', 'qui', 'sex', 's\u00e1b'],
+  NARROWWEEKDAYS: ['D', 'S', 'T', 'Q', 'Q', 'S', 'S'],
+  SHORTQUARTERS: ['T1', 'T2', 'T3', 'T4'],
+  QUARTERS: ['1.\u00ba trimestre', '2.\u00ba trimestre', '3.\u00ba trimestre', '4.\u00ba trimestre'],
+  AMPMS: ['Antes do meio-dia', 'Depois do meio-dia'],
+  DATEFORMATS: ["EEEE, d 'de' MMMM 'de' y", "d 'de' MMMM 'de' y", "d 'de' MMM 'de' yyyy", 'dd/MM/yy'],
+  TIMEFORMATS: ["HH'h'mm'min'ss's' zzzz", "HH'h'mm'min'ss's' z", 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro.js
new file mode 100644
index 0000000..4289012
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u00ee.Hr.', 'd.Hr.'],
+  ERANAMES: ['\u00eenainte de Hristos', 'dup\u0103 Hristos'],
+  NARROWMONTHS: ['I', 'F', 'M', 'A', 'M', 'I', 'I', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'],
+  SHORTMONTHS: ['ian.', 'feb.', 'mar.', 'apr.', 'mai', 'iun.', 'iul.', 'aug.', 'sept.', 'oct.', 'nov.', 'dec.'],
+  WEEKDAYS: ['duminic\u0103', 'luni', 'mar\u021bi', 'miercuri', 'joi', 'vineri', 's\u00e2mb\u0103t\u0103'],
+  SHORTWEEKDAYS: ['Du', 'Lu', 'Ma', 'Mi', 'Jo', 'Vi', 'S\u00e2'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['trim. I', 'trim. II', 'trim. III', 'trim. IV'],
+  QUARTERS: ['trimestrul I', 'trimestrul al II-lea', 'trimestrul al III-lea', 'trimestrul al IV-lea'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro_MD.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro_MD.js
new file mode 100644
index 0000000..4289012
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro_MD.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u00ee.Hr.', 'd.Hr.'],
+  ERANAMES: ['\u00eenainte de Hristos', 'dup\u0103 Hristos'],
+  NARROWMONTHS: ['I', 'F', 'M', 'A', 'M', 'I', 'I', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'],
+  SHORTMONTHS: ['ian.', 'feb.', 'mar.', 'apr.', 'mai', 'iun.', 'iul.', 'aug.', 'sept.', 'oct.', 'nov.', 'dec.'],
+  WEEKDAYS: ['duminic\u0103', 'luni', 'mar\u021bi', 'miercuri', 'joi', 'vineri', 's\u00e2mb\u0103t\u0103'],
+  SHORTWEEKDAYS: ['Du', 'Lu', 'Ma', 'Mi', 'Jo', 'Vi', 'S\u00e2'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['trim. I', 'trim. II', 'trim. III', 'trim. IV'],
+  QUARTERS: ['trimestrul I', 'trimestrul al II-lea', 'trimestrul al III-lea', 'trimestrul al IV-lea'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro_RO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro_RO.js
new file mode 100644
index 0000000..4289012
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ro_RO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u00ee.Hr.', 'd.Hr.'],
+  ERANAMES: ['\u00eenainte de Hristos', 'dup\u0103 Hristos'],
+  NARROWMONTHS: ['I', 'F', 'M', 'A', 'M', 'I', 'I', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['ianuarie', 'februarie', 'martie', 'aprilie', 'mai', 'iunie', 'iulie', 'august', 'septembrie', 'octombrie', 'noiembrie', 'decembrie'],
+  SHORTMONTHS: ['ian.', 'feb.', 'mar.', 'apr.', 'mai', 'iun.', 'iul.', 'aug.', 'sept.', 'oct.', 'nov.', 'dec.'],
+  WEEKDAYS: ['duminic\u0103', 'luni', 'mar\u021bi', 'miercuri', 'joi', 'vineri', 's\u00e2mb\u0103t\u0103'],
+  SHORTWEEKDAYS: ['Du', 'Lu', 'Ma', 'Mi', 'Jo', 'Vi', 'S\u00e2'],
+  NARROWWEEKDAYS: ['D', 'L', 'M', 'M', 'J', 'V', 'S'],
+  SHORTQUARTERS: ['trim. I', 'trim. II', 'trim. III', 'trim. IV'],
+  QUARTERS: ['trimestrul I', 'trimestrul al II-lea', 'trimestrul al III-lea', 'trimestrul al IV-lea'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, d MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru.js
new file mode 100644
index 0000000..eba51ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0434\u043e \u043d.\u044d.', '\u043d.\u044d.'],
+  ERANAMES: ['\u0434\u043e \u043d.\u044d.', '\u043d.\u044d.'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u044f\u043d\u0432\u0430\u0440\u044f', '\u0444\u0435\u0432\u0440\u0430\u043b\u044f', '\u043c\u0430\u0440\u0442\u0430', '\u0430\u043f\u0440\u0435\u043b\u044f', '\u043c\u0430\u044f', '\u0438\u044e\u043d\u044f', '\u0438\u044e\u043b\u044f', '\u0430\u0432\u0433\u0443\u0441\u0442\u0430', '\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f', '\u043e\u043a\u0442\u044f\u0431\u0440\u044f', '\u043d\u043e\u044f\u0431\u0440\u044f', '\u0434\u0435\u043a\u0430\u0431\u0440\u044f'],
+  STANDALONEMONTHS: ['\u042f\u043d\u0432\u0430\u0440\u044c', '\u0424\u0435\u0432\u0440\u0430\u043b\u044c', '\u041c\u0430\u0440\u0442', '\u0410\u043f\u0440\u0435\u043b\u044c', '\u041c\u0430\u0439', '\u0418\u044e\u043d\u044c', '\u0418\u044e\u043b\u044c', '\u0410\u0432\u0433\u0443\u0441\u0442', '\u0421\u0435\u043d\u0442\u044f\u0431\u0440\u044c', '\u041e\u043a\u0442\u044f\u0431\u0440\u044c', '\u041d\u043e\u044f\u0431\u0440\u044c', '\u0414\u0435\u043a\u0430\u0431\u0440\u044c'],
+  SHORTMONTHS: ['\u044f\u043d\u0432.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442\u0430', '\u0430\u043f\u0440.', '\u043c\u0430\u044f', '\u0438\u044e\u043d\u044f', '\u0438\u044e\u043b\u044f', '\u0430\u0432\u0433.', '\u0441\u0435\u043d\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u044f\u0431.', '\u0434\u0435\u043a.'],
+  STANDALONESHORTMONTHS: ['\u044f\u043d\u0432.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440.', '\u043c\u0430\u0439', '\u0438\u044e\u043d\u044c', '\u0438\u044e\u043b\u044c', '\u0430\u0432\u0433.', '\u0441\u0435\u043d\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u044f\u0431.', '\u0434\u0435\u043a.'],
+  WEEKDAYS: ['\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435', '\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a', '\u0432\u0442\u043e\u0440\u043d\u0438\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0435\u0440\u0433', '\u043f\u044f\u0442\u043d\u0438\u0446\u0430', '\u0441\u0443\u0431\u0431\u043e\u0442\u0430'],
+  STANDALONEWEEKDAYS: ['\u0412\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435', '\u041f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a', '\u0412\u0442\u043e\u0440\u043d\u0438\u043a', '\u0421\u0440\u0435\u0434\u0430', '\u0427\u0435\u0442\u0432\u0435\u0440\u0433', '\u041f\u044f\u0442\u043d\u0438\u0446\u0430', '\u0421\u0443\u0431\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u0412\u0441', '\u041f\u043d', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041f\u0442', '\u0421\u0431'],
+  NARROWWEEKDAYS: ['\u0412', '\u041f', '\u0412', '\u0421', '\u0427', '\u041f', '\u0421'],
+  SHORTQUARTERS: ['1-\u0439 \u043a\u0432.', '2-\u0439 \u043a\u0432.', '3-\u0439 \u043a\u0432.', '4-\u0439 \u043a\u0432.'],
+  QUARTERS: ['1-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '2-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '3-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '4-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0433'.", "d MMMM y '\u0433'.", 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru_RU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru_RU.js
new file mode 100644
index 0000000..eba51ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru_RU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0434\u043e \u043d.\u044d.', '\u043d.\u044d.'],
+  ERANAMES: ['\u0434\u043e \u043d.\u044d.', '\u043d.\u044d.'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u044f\u043d\u0432\u0430\u0440\u044f', '\u0444\u0435\u0432\u0440\u0430\u043b\u044f', '\u043c\u0430\u0440\u0442\u0430', '\u0430\u043f\u0440\u0435\u043b\u044f', '\u043c\u0430\u044f', '\u0438\u044e\u043d\u044f', '\u0438\u044e\u043b\u044f', '\u0430\u0432\u0433\u0443\u0441\u0442\u0430', '\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f', '\u043e\u043a\u0442\u044f\u0431\u0440\u044f', '\u043d\u043e\u044f\u0431\u0440\u044f', '\u0434\u0435\u043a\u0430\u0431\u0440\u044f'],
+  STANDALONEMONTHS: ['\u042f\u043d\u0432\u0430\u0440\u044c', '\u0424\u0435\u0432\u0440\u0430\u043b\u044c', '\u041c\u0430\u0440\u0442', '\u0410\u043f\u0440\u0435\u043b\u044c', '\u041c\u0430\u0439', '\u0418\u044e\u043d\u044c', '\u0418\u044e\u043b\u044c', '\u0410\u0432\u0433\u0443\u0441\u0442', '\u0421\u0435\u043d\u0442\u044f\u0431\u0440\u044c', '\u041e\u043a\u0442\u044f\u0431\u0440\u044c', '\u041d\u043e\u044f\u0431\u0440\u044c', '\u0414\u0435\u043a\u0430\u0431\u0440\u044c'],
+  SHORTMONTHS: ['\u044f\u043d\u0432.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442\u0430', '\u0430\u043f\u0440.', '\u043c\u0430\u044f', '\u0438\u044e\u043d\u044f', '\u0438\u044e\u043b\u044f', '\u0430\u0432\u0433.', '\u0441\u0435\u043d\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u044f\u0431.', '\u0434\u0435\u043a.'],
+  STANDALONESHORTMONTHS: ['\u044f\u043d\u0432.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440.', '\u043c\u0430\u0439', '\u0438\u044e\u043d\u044c', '\u0438\u044e\u043b\u044c', '\u0430\u0432\u0433.', '\u0441\u0435\u043d\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u044f\u0431.', '\u0434\u0435\u043a.'],
+  WEEKDAYS: ['\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435', '\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a', '\u0432\u0442\u043e\u0440\u043d\u0438\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0435\u0440\u0433', '\u043f\u044f\u0442\u043d\u0438\u0446\u0430', '\u0441\u0443\u0431\u0431\u043e\u0442\u0430'],
+  STANDALONEWEEKDAYS: ['\u0412\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435', '\u041f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a', '\u0412\u0442\u043e\u0440\u043d\u0438\u043a', '\u0421\u0440\u0435\u0434\u0430', '\u0427\u0435\u0442\u0432\u0435\u0440\u0433', '\u041f\u044f\u0442\u043d\u0438\u0446\u0430', '\u0421\u0443\u0431\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u0412\u0441', '\u041f\u043d', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041f\u0442', '\u0421\u0431'],
+  NARROWWEEKDAYS: ['\u0412', '\u041f', '\u0412', '\u0421', '\u0427', '\u041f', '\u0421'],
+  SHORTQUARTERS: ['1-\u0439 \u043a\u0432.', '2-\u0439 \u043a\u0432.', '3-\u0439 \u043a\u0432.', '4-\u0439 \u043a\u0432.'],
+  QUARTERS: ['1-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '2-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '3-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '4-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0433'.", "d MMMM y '\u0433'.", 'dd.MM.yyyy', 'dd.MM.yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru_UA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru_UA.js
new file mode 100644
index 0000000..1e28175
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ru_UA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0434\u043e \u043d.\u044d.', '\u043d.\u044d.'],
+  ERANAMES: ['\u0434\u043e \u043d.\u044d.', '\u043d.\u044d.'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u044f\u043d\u0432\u0430\u0440\u044f', '\u0444\u0435\u0432\u0440\u0430\u043b\u044f', '\u043c\u0430\u0440\u0442\u0430', '\u0430\u043f\u0440\u0435\u043b\u044f', '\u043c\u0430\u044f', '\u0438\u044e\u043d\u044f', '\u0438\u044e\u043b\u044f', '\u0430\u0432\u0433\u0443\u0441\u0442\u0430', '\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f', '\u043e\u043a\u0442\u044f\u0431\u0440\u044f', '\u043d\u043e\u044f\u0431\u0440\u044f', '\u0434\u0435\u043a\u0430\u0431\u0440\u044f'],
+  STANDALONEMONTHS: ['\u042f\u043d\u0432\u0430\u0440\u044c', '\u0424\u0435\u0432\u0440\u0430\u043b\u044c', '\u041c\u0430\u0440\u0442', '\u0410\u043f\u0440\u0435\u043b\u044c', '\u041c\u0430\u0439', '\u0418\u044e\u043d\u044c', '\u0418\u044e\u043b\u044c', '\u0410\u0432\u0433\u0443\u0441\u0442', '\u0421\u0435\u043d\u0442\u044f\u0431\u0440\u044c', '\u041e\u043a\u0442\u044f\u0431\u0440\u044c', '\u041d\u043e\u044f\u0431\u0440\u044c', '\u0414\u0435\u043a\u0430\u0431\u0440\u044c'],
+  SHORTMONTHS: ['\u044f\u043d\u0432.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442\u0430', '\u0430\u043f\u0440.', '\u043c\u0430\u044f', '\u0438\u044e\u043d\u044f', '\u0438\u044e\u043b\u044f', '\u0430\u0432\u0433.', '\u0441\u0435\u043d\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u044f\u0431.', '\u0434\u0435\u043a.'],
+  STANDALONESHORTMONTHS: ['\u044f\u043d\u0432.', '\u0444\u0435\u0432\u0440.', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440.', '\u043c\u0430\u0439', '\u0438\u044e\u043d\u044c', '\u0438\u044e\u043b\u044c', '\u0430\u0432\u0433.', '\u0441\u0435\u043d\u0442.', '\u043e\u043a\u0442.', '\u043d\u043e\u044f\u0431.', '\u0434\u0435\u043a.'],
+  WEEKDAYS: ['\u0432\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435', '\u043f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a', '\u0432\u0442\u043e\u0440\u043d\u0438\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0435\u0440\u0433', '\u043f\u044f\u0442\u043d\u0438\u0446\u0430', '\u0441\u0443\u0431\u0431\u043e\u0442\u0430'],
+  STANDALONEWEEKDAYS: ['\u0412\u043e\u0441\u043a\u0440\u0435\u0441\u0435\u043d\u044c\u0435', '\u041f\u043e\u043d\u0435\u0434\u0435\u043b\u044c\u043d\u0438\u043a', '\u0412\u0442\u043e\u0440\u043d\u0438\u043a', '\u0421\u0440\u0435\u0434\u0430', '\u0427\u0435\u0442\u0432\u0435\u0440\u0433', '\u041f\u044f\u0442\u043d\u0438\u0446\u0430', '\u0421\u0443\u0431\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u0412\u0441', '\u041f\u043d', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041f\u0442', '\u0421\u0431'],
+  NARROWWEEKDAYS: ['\u0412', '\u041f', '\u0412', '\u0421', '\u0427', '\u041f', '\u0421'],
+  SHORTQUARTERS: ['1-\u0439 \u043a\u0432.', '2-\u0439 \u043a\u0432.', '3-\u0439 \u043a\u0432.', '4-\u0439 \u043a\u0432.'],
+  QUARTERS: ['1-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '2-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '3-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b', '4-\u0439 \u043a\u0432\u0430\u0440\u0442\u0430\u043b'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0433'.", 'd MMMM y', 'd MMM y', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__rw.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__rw.js
new file mode 100644
index 0000000..22513b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__rw.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Mutarama', 'Gashyantare', 'Werurwe', 'Mata', 'Gicuransi', 'Kamena', 'Nyakanga', 'Kanama', 'Nzeli', 'Ukwakira', 'Ugushyingo', 'Ukuboza'],
+  SHORTMONTHS: ['mut.', 'gas.', 'wer.', 'mat.', 'gic.', 'kam.', 'nya.', 'kan.', 'nze.', 'ukw.', 'ugu.', 'uku.'],
+  WEEKDAYS: ['Ku cyumweru', 'Kuwa mbere', 'Kuwa kabiri', 'Kuwa gatatu', 'Kuwa kane', 'Kuwa gatanu', 'Kuwa gatandatu'],
+  SHORTWEEKDAYS: ['cyu.', 'mbe.', 'kab.', 'gtu.', 'kan.', 'gnu.', 'gnd.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['I1', 'I2', 'I3', 'I4'],
+  QUARTERS: ['igihembwe cya mbere', 'igihembwe cya kabiri', 'igihembwe cya gatatu', 'igihembwe cya kane'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__rw_RW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__rw_RW.js
new file mode 100644
index 0000000..22513b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__rw_RW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Mutarama', 'Gashyantare', 'Werurwe', 'Mata', 'Gicuransi', 'Kamena', 'Nyakanga', 'Kanama', 'Nzeli', 'Ukwakira', 'Ugushyingo', 'Ukuboza'],
+  SHORTMONTHS: ['mut.', 'gas.', 'wer.', 'mat.', 'gic.', 'kam.', 'nya.', 'kan.', 'nze.', 'ukw.', 'ugu.', 'uku.'],
+  WEEKDAYS: ['Ku cyumweru', 'Kuwa mbere', 'Kuwa kabiri', 'Kuwa gatatu', 'Kuwa kane', 'Kuwa gatanu', 'Kuwa gatandatu'],
+  SHORTWEEKDAYS: ['cyu.', 'mbe.', 'kab.', 'gtu.', 'kan.', 'gnu.', 'gnd.'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['I1', 'I2', 'I3', 'I4'],
+  QUARTERS: ['igihembwe cya mbere', 'igihembwe cya kabiri', 'igihembwe cya gatatu', 'igihembwe cya kane'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sa.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sa.js
new file mode 100644
index 0000000..fba1b1b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sa.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-MM-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sa_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sa_IN.js
new file mode 100644
index 0000000..0ffddff
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sa_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'dd-MM-yyyy', 'd-MM-yy'],
+  TIMEFORMATS: ['hh:mm:ss a zzzz', 'hh:mm:ss a z', 'hh:mm:ss a', 'hh:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se.js
new file mode 100644
index 0000000..35161c5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['o.Kr.', 'm.Kr.'],
+  ERANAMES: ['ovdal Kristtusa', 'ma\u014b\u014bel Kristtusa'],
+  NARROWMONTHS: ['O', 'G', 'N', 'C', 'M', 'G', 'S', 'B', '\u010c', 'G', 'S', 'J'],
+  MONTHS: ['o\u0111\u0111ajagem\u00e1nnu', 'guovvam\u00e1nnu', 'njuk\u010dam\u00e1nnu', 'cuo\u014bom\u00e1nnu', 'miessem\u00e1nnu', 'geassem\u00e1nnu', 'suoidnem\u00e1nnu', 'borgem\u00e1nnu', '\u010dak\u010dam\u00e1nnu', 'golggotm\u00e1nnu', 'sk\u00e1bmam\u00e1nnu', 'juovlam\u00e1nnu'],
+  SHORTMONTHS: ['o\u0111\u0111j', 'guov', 'njuk', 'cuo', 'mies', 'geas', 'suoi', 'borg', '\u010dak\u010d', 'golg', 'sk\u00e1b', 'juov'],
+  WEEKDAYS: ['sotnabeaivi', 'vuoss\u00e1rga', 'ma\u014b\u014beb\u00e1rga', 'gaskavahkku', 'duorasdat', 'bearjadat', 'l\u00e1vvardat'],
+  SHORTWEEKDAYS: ['sotn', 'vuos', 'ma\u014b', 'gask', 'duor', 'bear', 'l\u00e1v'],
+  NARROWWEEKDAYS: ['s', 'v', 'm', 'g', 'd', 'b', 'L'],
+  SHORTQUARTERS: ['Q1', 'K2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se_FI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se_FI.js
new file mode 100644
index 0000000..2777a01
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se_FI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'm.Kr.'],
+  ERANAMES: ['BCE', 'ma\u014b\u014bel Kristtusa'],
+  NARROWMONTHS: ['O', 'G', 'N', 'C', 'M', 'G', 'S', 'B', '\u010c', 'G', 'S', 'J'],
+  MONTHS: ['o\u0111\u0111ajagem\u00e1nnu', 'guovvam\u00e1nnu', 'njuk\u010dam\u00e1nnu', 'cuo\u014bom\u00e1nnu', 'miessem\u00e1nnu', 'geassem\u00e1nnu', 'suoidnem\u00e1nnu', 'borgem\u00e1nnu', '\u010dak\u010dam\u00e1nnu', 'golggotm\u00e1nnu', 'sk\u00e1bmam\u00e1nnu', 'juovlam\u00e1nnu'],
+  SHORTMONTHS: ['o\u0111\u0111ajage', 'guovva', 'njuk\u010da', 'cuo\u014bo', 'miesse', 'geasse', 'suoidne', 'borge', '\u010dak\u010da', 'golggot', 'sk\u00e1bma', 'juovla'],
+  WEEKDAYS: ['aejlege', 'm\u00e5anta', 'd\u00e4jsta', 'gaskevahkoe', 'd\u00e5arsta', 'bearjadahke', 'laavadahke'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'G', 'D', 'B', 'L'],
+  SHORTQUARTERS: ['Q1', 'K2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se_NO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se_NO.js
new file mode 100644
index 0000000..35161c5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__se_NO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['o.Kr.', 'm.Kr.'],
+  ERANAMES: ['ovdal Kristtusa', 'ma\u014b\u014bel Kristtusa'],
+  NARROWMONTHS: ['O', 'G', 'N', 'C', 'M', 'G', 'S', 'B', '\u010c', 'G', 'S', 'J'],
+  MONTHS: ['o\u0111\u0111ajagem\u00e1nnu', 'guovvam\u00e1nnu', 'njuk\u010dam\u00e1nnu', 'cuo\u014bom\u00e1nnu', 'miessem\u00e1nnu', 'geassem\u00e1nnu', 'suoidnem\u00e1nnu', 'borgem\u00e1nnu', '\u010dak\u010dam\u00e1nnu', 'golggotm\u00e1nnu', 'sk\u00e1bmam\u00e1nnu', 'juovlam\u00e1nnu'],
+  SHORTMONTHS: ['o\u0111\u0111j', 'guov', 'njuk', 'cuo', 'mies', 'geas', 'suoi', 'borg', '\u010dak\u010d', 'golg', 'sk\u00e1b', 'juov'],
+  WEEKDAYS: ['sotnabeaivi', 'vuoss\u00e1rga', 'ma\u014b\u014beb\u00e1rga', 'gaskavahkku', 'duorasdat', 'bearjadat', 'l\u00e1vvardat'],
+  SHORTWEEKDAYS: ['sotn', 'vuos', 'ma\u014b', 'gask', 'duor', 'bear', 'l\u00e1v'],
+  NARROWWEEKDAYS: ['s', 'v', 'm', 'g', 'd', 'b', 'L'],
+  SHORTQUARTERS: ['Q1', 'K2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_BA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_BA.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_BA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_CS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_CS.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_CS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_YU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_YU.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sh_YU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__si.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__si.js
new file mode 100644
index 0000000..d56874e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__si.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0d9a\u0dca\u200d\u0dbb\u0dd2.\u0db4\u0dd6.', '\u0d9a\u0dca\u200d\u0dbb\u0dd2.\u0dc0.'],
+  ERANAMES: ['\u0d9a\u0dca\u200d\u0dbb\u0dd2\u0dc3\u0dca\u0dad\u0dd4 \u0db4\u0dd6\u0dbb\u0dca\u200d\u0dc0', '\u0d9a\u0dca\u200d\u0dbb\u0dd2\u0dc3\u0dca\u0dad\u0dd4 \u0dc0\u0dbb\u0dca\u200d\u0dc2'],
+  NARROWMONTHS: ['\u0da2', '\u0db4\u0dd9', '\u0db8\u0dcf', '\u0d85', '\u0db8\u0dd0', '\u0da2\u0dd6', '\u0da2\u0dd6', '\u0d85', '\u0dc3\u0dd0', '\u0d94', '\u0db1\u0ddc', '\u0daf\u0dd9'],
+  MONTHS: ['\u0da2\u0db1\u0dc0\u0dcf\u0dbb', '\u0db4\u0dd9\u0db6\u0dbb\u0dc0\u0dcf\u0dbb', '\u0db8\u0dcf\u0dbb\u0dca\u0dad', '\u0d85\u0db4\u0dca\u200d\u0dbb\u0dda\u0dbd\u0dca', '\u0db8\u0dd0\u0dba\u0dd2', '\u0da2\u0dd6\u0db1', '\u0da2\u0dd6\u0dbd\u0dd2', '\u0d85\u0d9c\u0ddd\u0dc3\u0dca\u0dad\u0dd4', '\u0dc3\u0dd0\u0db4\u0dca\u0dad\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca', '\u0d94\u0d9a\u0dca\u0dad\u0ddd\u0db6\u0dbb\u0dca', '\u0db1\u0ddc\u0dc0\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca', '\u0daf\u0dd9\u0dc3\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca'],
+  SHORTMONTHS: ['\u0da2\u0db1', '\u0db4\u0dd9\u0db6', '\u0db8\u0dcf\u0dbb\u0dca\u0dad', '\u0d85\u0db4\u0dca\u200d\u0dbb\u0dda\u0dbd', '\u0db8\u0dd0\u0dba', '\u0da2\u0dd6\u0db1', '\u0da2\u0dd6\u0dbd', '\u0d85\u0d9c\u0ddd', '\u0dc3\u0dd0\u0db4', '\u0d94\u0d9a', '\u0db1\u0ddc\u0dc0\u0dd0', '\u0daf\u0dd9\u0dc3\u0dd0'],
+  WEEKDAYS: ['\u0d89\u0dbb\u0dd2\u0daf\u0dcf', '\u0dc3\u0db3\u0dd4\u0daf\u0dcf', '\u0d85\u0d9f\u0dc4\u0dbb\u0dd4\u0dc0\u0dcf\u0daf\u0dcf', '\u0db6\u0daf\u0dcf\u0daf\u0dcf', '\u0db6\u0dca\u200d\u0dbb\u0dc4\u0dc3\u0dca\u0db4\u0dad\u0dd2\u0db1\u0dca\u0daf\u0dcf', '\u0dc3\u0dd2\u0d9a\u0dd4\u0dbb\u0dcf\u0daf\u0dcf', '\u0dc3\u0dd9\u0db1\u0dc3\u0dd4\u0dbb\u0dcf\u0daf\u0dcf'],
+  SHORTWEEKDAYS: ['\u0d89\u0dbb\u0dd2', '\u0dc3\u0db3\u0dd4', '\u0d85\u0d9f', '\u0db6\u0daf\u0dcf', '\u0db6\u0dca\u200d\u0dbb\u0dc4', '\u0dc3\u0dd2\u0d9a\u0dd4', '\u0dc3\u0dd9\u0db1'],
+  NARROWWEEKDAYS: ['\u0d89', '\u0dc3', '\u0d85', '\u0db6', '\u0db6\u0dca\u200d\u0dbb', '\u0dc3\u0dd2', '\u0dc3\u0dd9'],
+  SHORTQUARTERS: ['\u0d9a\u0dcf\u0dbb\u0dca:1', '\u0d9a\u0dcf\u0dbb\u0dca:2', '\u0d9a\u0dcf\u0dbb\u0dca:3', '\u0d9a\u0dcf\u0dbb\u0dca:4'],
+  QUARTERS: ['1 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0', '2 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0', '3 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0', '4 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0'],
+  AMPMS: ['\u0db4\u0dd9.\u0dc0.', '\u0db4.\u0dc0.'],
+  DATEFORMATS: ['EEEE, y MMMM d', 'y MMMM d', 'y MMM d', 'yyyy/MM/dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__si_LK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__si_LK.js
new file mode 100644
index 0000000..d56874e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__si_LK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0d9a\u0dca\u200d\u0dbb\u0dd2.\u0db4\u0dd6.', '\u0d9a\u0dca\u200d\u0dbb\u0dd2.\u0dc0.'],
+  ERANAMES: ['\u0d9a\u0dca\u200d\u0dbb\u0dd2\u0dc3\u0dca\u0dad\u0dd4 \u0db4\u0dd6\u0dbb\u0dca\u200d\u0dc0', '\u0d9a\u0dca\u200d\u0dbb\u0dd2\u0dc3\u0dca\u0dad\u0dd4 \u0dc0\u0dbb\u0dca\u200d\u0dc2'],
+  NARROWMONTHS: ['\u0da2', '\u0db4\u0dd9', '\u0db8\u0dcf', '\u0d85', '\u0db8\u0dd0', '\u0da2\u0dd6', '\u0da2\u0dd6', '\u0d85', '\u0dc3\u0dd0', '\u0d94', '\u0db1\u0ddc', '\u0daf\u0dd9'],
+  MONTHS: ['\u0da2\u0db1\u0dc0\u0dcf\u0dbb', '\u0db4\u0dd9\u0db6\u0dbb\u0dc0\u0dcf\u0dbb', '\u0db8\u0dcf\u0dbb\u0dca\u0dad', '\u0d85\u0db4\u0dca\u200d\u0dbb\u0dda\u0dbd\u0dca', '\u0db8\u0dd0\u0dba\u0dd2', '\u0da2\u0dd6\u0db1', '\u0da2\u0dd6\u0dbd\u0dd2', '\u0d85\u0d9c\u0ddd\u0dc3\u0dca\u0dad\u0dd4', '\u0dc3\u0dd0\u0db4\u0dca\u0dad\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca', '\u0d94\u0d9a\u0dca\u0dad\u0ddd\u0db6\u0dbb\u0dca', '\u0db1\u0ddc\u0dc0\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca', '\u0daf\u0dd9\u0dc3\u0dd0\u0db8\u0dca\u0db6\u0dbb\u0dca'],
+  SHORTMONTHS: ['\u0da2\u0db1', '\u0db4\u0dd9\u0db6', '\u0db8\u0dcf\u0dbb\u0dca\u0dad', '\u0d85\u0db4\u0dca\u200d\u0dbb\u0dda\u0dbd', '\u0db8\u0dd0\u0dba', '\u0da2\u0dd6\u0db1', '\u0da2\u0dd6\u0dbd', '\u0d85\u0d9c\u0ddd', '\u0dc3\u0dd0\u0db4', '\u0d94\u0d9a', '\u0db1\u0ddc\u0dc0\u0dd0', '\u0daf\u0dd9\u0dc3\u0dd0'],
+  WEEKDAYS: ['\u0d89\u0dbb\u0dd2\u0daf\u0dcf', '\u0dc3\u0db3\u0dd4\u0daf\u0dcf', '\u0d85\u0d9f\u0dc4\u0dbb\u0dd4\u0dc0\u0dcf\u0daf\u0dcf', '\u0db6\u0daf\u0dcf\u0daf\u0dcf', '\u0db6\u0dca\u200d\u0dbb\u0dc4\u0dc3\u0dca\u0db4\u0dad\u0dd2\u0db1\u0dca\u0daf\u0dcf', '\u0dc3\u0dd2\u0d9a\u0dd4\u0dbb\u0dcf\u0daf\u0dcf', '\u0dc3\u0dd9\u0db1\u0dc3\u0dd4\u0dbb\u0dcf\u0daf\u0dcf'],
+  SHORTWEEKDAYS: ['\u0d89\u0dbb\u0dd2', '\u0dc3\u0db3\u0dd4', '\u0d85\u0d9f', '\u0db6\u0daf\u0dcf', '\u0db6\u0dca\u200d\u0dbb\u0dc4', '\u0dc3\u0dd2\u0d9a\u0dd4', '\u0dc3\u0dd9\u0db1'],
+  NARROWWEEKDAYS: ['\u0d89', '\u0dc3', '\u0d85', '\u0db6', '\u0db6\u0dca\u200d\u0dbb', '\u0dc3\u0dd2', '\u0dc3\u0dd9'],
+  SHORTQUARTERS: ['\u0d9a\u0dcf\u0dbb\u0dca:1', '\u0d9a\u0dcf\u0dbb\u0dca:2', '\u0d9a\u0dcf\u0dbb\u0dca:3', '\u0d9a\u0dcf\u0dbb\u0dca:4'],
+  QUARTERS: ['1 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0', '2 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0', '3 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0', '4 \u0dc0\u0db1 \u0d9a\u0dcf\u0dbb\u0dca\u0dad\u0dd4\u0dc0'],
+  AMPMS: ['\u0db4\u0dd9.\u0dc0.', '\u0db4.\u0dc0.'],
+  DATEFORMATS: ['EEEE, y MMMM d', 'y MMMM d', 'y MMM d', 'yyyy/MM/dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sid.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sid.js
new file mode 100644
index 0000000..381457d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sid.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['YIA', 'YIG'],
+  ERANAMES: ['YIA', 'YIG'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sambata', 'Sanyo', 'Maakisanyo', 'Roowe', 'Hamuse', 'Arbe', 'Qidaame'],
+  SHORTWEEKDAYS: ['Sam', 'San', 'Mak', 'Row', 'Ham', 'Arb', 'Qid'],
+  NARROWWEEKDAYS: ['S', 'S', 'M', 'R', 'H', 'A', 'Q'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['soodo', 'hawwaro'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sid_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sid_ET.js
new file mode 100644
index 0000000..08594e8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sid_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['YIA', 'YIG'],
+  ERANAMES: ['YIA', 'YIG'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sambata', 'Sanyo', 'Maakisanyo', 'Roowe', 'Hamuse', 'Arbe', 'Qidaame'],
+  SHORTWEEKDAYS: ['Sam', 'San', 'Mak', 'Row', 'Ham', 'Arb', 'Qid'],
+  NARROWWEEKDAYS: ['S', 'S', 'M', 'R', 'H', 'A', 'Q'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['soodo', 'hawwaro'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sk.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sk.js
new file mode 100644
index 0000000..4bc333e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sk.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pred n.l.', 'n.l.'],
+  ERANAMES: ['pred n.l.', 'n.l.'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['janu\u00e1ra', 'febru\u00e1ra', 'marca', 'apr\u00edla', 'm\u00e1ja', 'j\u00fana', 'j\u00fala', 'augusta', 'septembra', 'okt\u00f3bra', 'novembra', 'decembra'],
+  STANDALONEMONTHS: ['janu\u00e1r', 'febru\u00e1r', 'marec', 'apr\u00edl', 'm\u00e1j', 'j\u00fan', 'j\u00fal', 'august', 'september', 'okt\u00f3ber', 'november', 'december'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'm\u00e1j', 'j\u00fan', 'j\u00fal', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nede\u013ea', 'pondelok', 'utorok', 'streda', '\u0161tvrtok', 'piatok', 'sobota'],
+  SHORTWEEKDAYS: ['ne', 'po', 'ut', 'st', '\u0161t', 'pi', 'so'],
+  NARROWWEEKDAYS: ['N', 'P', 'U', 'S', '\u0160', 'P', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. \u0161tvr\u0165rok', '2. \u0161tvr\u0165rok', '3. \u0161tvr\u0165rok', '4. \u0161tvr\u0165rok'],
+  AMPMS: ['dopoludnia', 'popoludn\u00ed'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yyyy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sk_SK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sk_SK.js
new file mode 100644
index 0000000..4bc333e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sk_SK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pred n.l.', 'n.l.'],
+  ERANAMES: ['pred n.l.', 'n.l.'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['janu\u00e1ra', 'febru\u00e1ra', 'marca', 'apr\u00edla', 'm\u00e1ja', 'j\u00fana', 'j\u00fala', 'augusta', 'septembra', 'okt\u00f3bra', 'novembra', 'decembra'],
+  STANDALONEMONTHS: ['janu\u00e1r', 'febru\u00e1r', 'marec', 'apr\u00edl', 'm\u00e1j', 'j\u00fan', 'j\u00fal', 'august', 'september', 'okt\u00f3ber', 'november', 'december'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'm\u00e1j', 'j\u00fan', 'j\u00fal', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nede\u013ea', 'pondelok', 'utorok', 'streda', '\u0161tvrtok', 'piatok', 'sobota'],
+  SHORTWEEKDAYS: ['ne', 'po', 'ut', 'st', '\u0161t', 'pi', 'so'],
+  NARROWWEEKDAYS: ['N', 'P', 'U', 'S', '\u0160', 'P', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. \u0161tvr\u0165rok', '2. \u0161tvr\u0165rok', '3. \u0161tvr\u0165rok', '4. \u0161tvr\u0165rok'],
+  AMPMS: ['dopoludnia', 'popoludn\u00ed'],
+  DATEFORMATS: ['EEEE, d. MMMM y', 'd. MMMM y', 'd.M.yyyy', 'd.M.yyyy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sl.js
new file mode 100644
index 0000000..79a0a3e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pr. n. \u0161t.', 'po Kr.'],
+  ERANAMES: ['pred na\u0161im \u0161tetjem', 'na\u0161e \u0161tetje'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'marec', 'april', 'maj', 'junij', 'julij', 'avgust', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljek', 'torek', 'sreda', '\u010detrtek', 'petek', 'sobota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'tor', 'sre', '\u010det', 'pet', 'sob'],
+  NARROWWEEKDAYS: ['n', 'p', 't', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. \u010detrtletje', '2. \u010detrtletje', '3. \u010detrtletje', '4. \u010detrtletje'],
+  AMPMS: ['dop.', 'pop.'],
+  DATEFORMATS: ['EEEE, dd. MMMM y', 'dd. MMMM y', 'd. MMM. yyyy', 'd. MM. yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sl_SI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sl_SI.js
new file mode 100644
index 0000000..79a0a3e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sl_SI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['pr. n. \u0161t.', 'po Kr.'],
+  ERANAMES: ['pred na\u0161im \u0161tetjem', 'na\u0161e \u0161tetje'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'marec', 'april', 'maj', 'junij', 'julij', 'avgust', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljek', 'torek', 'sreda', '\u010detrtek', 'petek', 'sobota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'tor', 'sre', '\u010det', 'pet', 'sob'],
+  NARROWWEEKDAYS: ['n', 'p', 't', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. \u010detrtletje', '2. \u010detrtletje', '3. \u010detrtletje', '4. \u010detrtletje'],
+  AMPMS: ['dop.', 'pop.'],
+  DATEFORMATS: ['EEEE, dd. MMMM y', 'dd. MMMM y', 'd. MMM. yyyy', 'd. MM. yy'],
+  TIMEFORMATS: ['H:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so.js
new file mode 100644
index 0000000..0f08585
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Ciise ka hor', 'Ciise ka dib'],
+  ERANAMES: ['Ciise ka hor', 'Ciise ka dib'],
+  NARROWMONTHS: ['K', 'L', 'S', 'A', 'S', 'L', 'T', 'S', 'S', 'T', 'K', 'L'],
+  MONTHS: ['Bisha Koobaad', 'Bisha Labaad', 'Bisha Saddexaad', 'Bisha Afraad', 'Bisha Shanaad', 'Bisha Lixaad', 'Bisha Todobaad', 'Bisha Sideedaad', 'Bisha Sagaalaad', 'Bisha Tobnaad', 'Bisha Kow iyo Tobnaad', 'Bisha Laba iyo Tobnaad'],
+  SHORTMONTHS: ['Kob', 'Lab', 'Sad', 'Afr', 'Sha', 'Lix', 'Tod', 'Sid', 'Sag', 'Tob', 'KIT', 'LIT'],
+  WEEKDAYS: ['Axad', 'Isniin', 'Salaaso', 'Arbaco', 'Khamiis', 'Jimco', 'Sabti'],
+  SHORTWEEKDAYS: ['Axa', 'Isn', 'Sal', 'Arb', 'Kha', 'Jim', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'I', 'S', 'A', 'K', 'J', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['sn', 'gn'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_DJ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_DJ.js
new file mode 100644
index 0000000..0f08585
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_DJ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Ciise ka hor', 'Ciise ka dib'],
+  ERANAMES: ['Ciise ka hor', 'Ciise ka dib'],
+  NARROWMONTHS: ['K', 'L', 'S', 'A', 'S', 'L', 'T', 'S', 'S', 'T', 'K', 'L'],
+  MONTHS: ['Bisha Koobaad', 'Bisha Labaad', 'Bisha Saddexaad', 'Bisha Afraad', 'Bisha Shanaad', 'Bisha Lixaad', 'Bisha Todobaad', 'Bisha Sideedaad', 'Bisha Sagaalaad', 'Bisha Tobnaad', 'Bisha Kow iyo Tobnaad', 'Bisha Laba iyo Tobnaad'],
+  SHORTMONTHS: ['Kob', 'Lab', 'Sad', 'Afr', 'Sha', 'Lix', 'Tod', 'Sid', 'Sag', 'Tob', 'KIT', 'LIT'],
+  WEEKDAYS: ['Axad', 'Isniin', 'Salaaso', 'Arbaco', 'Khamiis', 'Jimco', 'Sabti'],
+  SHORTWEEKDAYS: ['Axa', 'Isn', 'Sal', 'Arb', 'Kha', 'Jim', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'I', 'S', 'A', 'K', 'J', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['sn', 'gn'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_ET.js
new file mode 100644
index 0000000..0f08585
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Ciise ka hor', 'Ciise ka dib'],
+  ERANAMES: ['Ciise ka hor', 'Ciise ka dib'],
+  NARROWMONTHS: ['K', 'L', 'S', 'A', 'S', 'L', 'T', 'S', 'S', 'T', 'K', 'L'],
+  MONTHS: ['Bisha Koobaad', 'Bisha Labaad', 'Bisha Saddexaad', 'Bisha Afraad', 'Bisha Shanaad', 'Bisha Lixaad', 'Bisha Todobaad', 'Bisha Sideedaad', 'Bisha Sagaalaad', 'Bisha Tobnaad', 'Bisha Kow iyo Tobnaad', 'Bisha Laba iyo Tobnaad'],
+  SHORTMONTHS: ['Kob', 'Lab', 'Sad', 'Afr', 'Sha', 'Lix', 'Tod', 'Sid', 'Sag', 'Tob', 'KIT', 'LIT'],
+  WEEKDAYS: ['Axad', 'Isniin', 'Salaaso', 'Arbaco', 'Khamiis', 'Jimco', 'Sabti'],
+  SHORTWEEKDAYS: ['Axa', 'Isn', 'Sal', 'Arb', 'Kha', 'Jim', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'I', 'S', 'A', 'K', 'J', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['sn', 'gn'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_KE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_KE.js
new file mode 100644
index 0000000..0f08585
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_KE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Ciise ka hor', 'Ciise ka dib'],
+  ERANAMES: ['Ciise ka hor', 'Ciise ka dib'],
+  NARROWMONTHS: ['K', 'L', 'S', 'A', 'S', 'L', 'T', 'S', 'S', 'T', 'K', 'L'],
+  MONTHS: ['Bisha Koobaad', 'Bisha Labaad', 'Bisha Saddexaad', 'Bisha Afraad', 'Bisha Shanaad', 'Bisha Lixaad', 'Bisha Todobaad', 'Bisha Sideedaad', 'Bisha Sagaalaad', 'Bisha Tobnaad', 'Bisha Kow iyo Tobnaad', 'Bisha Laba iyo Tobnaad'],
+  SHORTMONTHS: ['Kob', 'Lab', 'Sad', 'Afr', 'Sha', 'Lix', 'Tod', 'Sid', 'Sag', 'Tob', 'KIT', 'LIT'],
+  WEEKDAYS: ['Axad', 'Isniin', 'Salaaso', 'Arbaco', 'Khamiis', 'Jimco', 'Sabti'],
+  SHORTWEEKDAYS: ['Axa', 'Isn', 'Sal', 'Arb', 'Kha', 'Jim', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'I', 'S', 'A', 'K', 'J', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['sn', 'gn'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_SO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_SO.js
new file mode 100644
index 0000000..0f08585
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__so_SO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['Ciise ka hor', 'Ciise ka dib'],
+  ERANAMES: ['Ciise ka hor', 'Ciise ka dib'],
+  NARROWMONTHS: ['K', 'L', 'S', 'A', 'S', 'L', 'T', 'S', 'S', 'T', 'K', 'L'],
+  MONTHS: ['Bisha Koobaad', 'Bisha Labaad', 'Bisha Saddexaad', 'Bisha Afraad', 'Bisha Shanaad', 'Bisha Lixaad', 'Bisha Todobaad', 'Bisha Sideedaad', 'Bisha Sagaalaad', 'Bisha Tobnaad', 'Bisha Kow iyo Tobnaad', 'Bisha Laba iyo Tobnaad'],
+  SHORTMONTHS: ['Kob', 'Lab', 'Sad', 'Afr', 'Sha', 'Lix', 'Tod', 'Sid', 'Sag', 'Tob', 'KIT', 'LIT'],
+  WEEKDAYS: ['Axad', 'Isniin', 'Salaaso', 'Arbaco', 'Khamiis', 'Jimco', 'Sabti'],
+  SHORTWEEKDAYS: ['Axa', 'Isn', 'Sal', 'Arb', 'Kha', 'Jim', 'Sab'],
+  NARROWWEEKDAYS: ['A', 'I', 'S', 'A', 'K', 'J', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['sn', 'gn'],
+  DATEFORMATS: ['EEEE, MMMM dd, y', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sq.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sq.js
new file mode 100644
index 0000000..2d98d23
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sq.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p.e.r.', 'n.e.r.'],
+  ERANAMES: ['p.e.r.', 'n.e.r.'],
+  NARROWMONTHS: ['J', 'S', 'M', 'P', 'M', 'Q', 'K', 'G', 'S', 'T', 'N', 'D'],
+  MONTHS: ['janar', 'shkurt', 'mars', 'prill', 'maj', 'qershor', 'korrik', 'gusht', 'shtator', 'tetor', 'n\u00ebntor', 'dhjetor'],
+  SHORTMONTHS: ['Jan', 'Shk', 'Mar', 'Pri', 'Maj', 'Qer', 'Kor', 'Gsh', 'Sht', 'Tet', 'N\u00ebn', 'Dhj'],
+  WEEKDAYS: ['e diel', 'e h\u00ebn\u00eb', 'e mart\u00eb', 'e m\u00ebrkur\u00eb', 'e enjte', 'e premte', 'e shtun\u00eb'],
+  SHORTWEEKDAYS: ['Die', 'H\u00ebn', 'Mar', 'M\u00ebr', 'Enj', 'Pre', 'Sht'],
+  NARROWWEEKDAYS: ['D', 'H', 'M', 'M', 'E', 'P', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['PD', 'MD'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'yyyy-MM-dd', 'yy-MM-dd'],
+  TIMEFORMATS: ['h.mm.ss.a zzzz', 'h.mm.ss.a z', 'h.mm.ss.a', 'h.mm.a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sq_AL.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sq_AL.js
new file mode 100644
index 0000000..2d98d23
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sq_AL.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p.e.r.', 'n.e.r.'],
+  ERANAMES: ['p.e.r.', 'n.e.r.'],
+  NARROWMONTHS: ['J', 'S', 'M', 'P', 'M', 'Q', 'K', 'G', 'S', 'T', 'N', 'D'],
+  MONTHS: ['janar', 'shkurt', 'mars', 'prill', 'maj', 'qershor', 'korrik', 'gusht', 'shtator', 'tetor', 'n\u00ebntor', 'dhjetor'],
+  SHORTMONTHS: ['Jan', 'Shk', 'Mar', 'Pri', 'Maj', 'Qer', 'Kor', 'Gsh', 'Sht', 'Tet', 'N\u00ebn', 'Dhj'],
+  WEEKDAYS: ['e diel', 'e h\u00ebn\u00eb', 'e mart\u00eb', 'e m\u00ebrkur\u00eb', 'e enjte', 'e premte', 'e shtun\u00eb'],
+  SHORTWEEKDAYS: ['Die', 'H\u00ebn', 'Mar', 'M\u00ebr', 'Enj', 'Pre', 'Sht'],
+  NARROWWEEKDAYS: ['D', 'H', 'M', 'M', 'E', 'P', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['PD', 'MD'],
+  DATEFORMATS: ['EEEE, dd MMMM y', 'dd MMMM y', 'yyyy-MM-dd', 'yy-MM-dd'],
+  TIMEFORMATS: ['h.mm.ss.a zzzz', 'h.mm.ss.a z', 'h.mm.ss.a', 'h.mm.a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_BA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_BA.js
new file mode 100644
index 0000000..2cec61e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_BA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d\u0438', '\u0458\u0443\u043b\u0438', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0438\u0458\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0438', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'yyyy-MM-dd', 'yy-MM-dd'],
+  TIMEFORMATS: ["HH '\u0447\u0430\u0441\u043e\u0432\u0430', mm '\u043c\u0438\u043d\u0443\u0442\u0430', ss '\u0441\u0435\u043a\u0443\u043d\u0434\u0438' zzzz", 'HH.mm.ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_CS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_CS.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_CS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_BA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_BA.js
new file mode 100644
index 0000000..2cec61e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_BA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d\u0438', '\u0458\u0443\u043b\u0438', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0438\u0458\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0438', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'yyyy-MM-dd', 'yy-MM-dd'],
+  TIMEFORMATS: ["HH '\u0447\u0430\u0441\u043e\u0432\u0430', mm '\u043c\u0438\u043d\u0443\u0442\u0430', ss '\u0441\u0435\u043a\u0443\u043d\u0434\u0438' zzzz", 'HH.mm.ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_CS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_CS.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_CS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_ME.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_ME.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_ME.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_RS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_RS.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_RS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_YU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_YU.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Cyrl_YU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_BA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_BA.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_BA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_CS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_CS.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_CS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_ME.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_ME.js
new file mode 100644
index 0000000..972d4d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_ME.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'd.MM.yyyy.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_RS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_RS.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_RS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_YU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_YU.js
new file mode 100644
index 0000000..35acf87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_Latn_YU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_ME.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_ME.js
new file mode 100644
index 0000000..972d4d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_ME.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['p. n. e.', 'n. e'],
+  ERANAMES: ['Pre nove ere', 'Nove ere'],
+  NARROWMONTHS: ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'],
+  MONTHS: ['januar', 'februar', 'mart', 'april', 'maj', 'jun', 'jul', 'avgust', 'septembar', 'oktobar', 'novembar', 'decembar'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'avg', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['nedelja', 'ponedeljak', 'utorak', 'sreda', '\u010detvrtak', 'petak', 'subota'],
+  SHORTWEEKDAYS: ['ned', 'pon', 'uto', 'sre', '\u010det', 'pet', 'sub'],
+  NARROWWEEKDAYS: ['n', 'p', 'u', 's', '\u010d', 'p', 's'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. kvartal', '2. kvartal', '3. kvartal', '4. kvartal'],
+  AMPMS: ['pre podne', 'popodne'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'd.MM.yyyy.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_RS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_RS.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_RS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_YU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_YU.js
new file mode 100644
index 0000000..8035156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sr_YU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u043f. \u043d. \u0435.', '\u043d. \u0435'],
+  ERANAMES: ['\u041f\u0440\u0435 \u043d\u043e\u0432\u0435 \u0435\u0440\u0435', '\u041d\u043e\u0432\u0435 \u0435\u0440\u0435'],
+  NARROWMONTHS: ['\u0458', '\u0444', '\u043c', '\u0430', '\u043c', '\u0458', '\u0458', '\u0430', '\u0441', '\u043e', '\u043d', '\u0434'],
+  MONTHS: ['\u0458\u0430\u043d\u0443\u0430\u0440', '\u0444\u0435\u0431\u0440\u0443\u0430\u0440', '\u043c\u0430\u0440\u0442', '\u0430\u043f\u0440\u0438\u043b', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433\u0443\u0441\u0442', '\u0441\u0435\u043f\u0442\u0435\u043c\u0431\u0430\u0440', '\u043e\u043a\u0442\u043e\u0431\u0430\u0440', '\u043d\u043e\u0432\u0435\u043c\u0431\u0430\u0440', '\u0434\u0435\u0446\u0435\u043c\u0431\u0430\u0440'],
+  SHORTMONTHS: ['\u0458\u0430\u043d', '\u0444\u0435\u0431', '\u043c\u0430\u0440', '\u0430\u043f\u0440', '\u043c\u0430\u0458', '\u0458\u0443\u043d', '\u0458\u0443\u043b', '\u0430\u0432\u0433', '\u0441\u0435\u043f', '\u043e\u043a\u0442', '\u043d\u043e\u0432', '\u0434\u0435\u0446'],
+  WEEKDAYS: ['\u043d\u0435\u0434\u0435\u0459\u0430', '\u043f\u043e\u043d\u0435\u0434\u0435\u0459\u0430\u043a', '\u0443\u0442\u043e\u0440\u0430\u043a', '\u0441\u0440\u0435\u0434\u0430', '\u0447\u0435\u0442\u0432\u0440\u0442\u0430\u043a', '\u043f\u0435\u0442\u0430\u043a', '\u0441\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u043d\u0435\u0434', '\u043f\u043e\u043d', '\u0443\u0442\u043e', '\u0441\u0440\u0435', '\u0447\u0435\u0442', '\u043f\u0435\u0442', '\u0441\u0443\u0431'],
+  NARROWWEEKDAYS: ['\u043d', '\u043f', '\u0443', '\u0441', '\u0447', '\u043f', '\u0441'],
+  SHORTQUARTERS: ['\u041a1', '\u041a2', '\u041a3', '\u041a4'],
+  QUARTERS: ['\u041f\u0440\u0432\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0414\u0440\u0443\u0433\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0422\u0440\u0435\u045b\u0435 \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435', '\u0427\u0435\u0442\u0432\u0440\u0442\u043e \u0442\u0440\u043e\u043c\u0435\u0441\u0435\u0447\u0458\u0435'],
+  AMPMS: ['\u043f\u0440\u0435 \u043f\u043e\u0434\u043d\u0435', '\u043f\u043e\u043f\u043e\u0434\u043d\u0435'],
+  DATEFORMATS: ['EEEE, dd. MMMM y.', 'dd. MMMM y.', 'dd.MM.y.', 'd.M.yy.'],
+  TIMEFORMATS: ['HH.mm.ss zzzz', 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss.js
new file mode 100644
index 0000000..847ee2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Bhimbidvwane', 'iNdlovana', 'iNdlovu-lenkhulu', 'Mabasa', 'iNkhwekhweti', 'iNhlaba', 'Kholwane', 'iNgci', 'iNyoni', 'iMphala', 'Lweti', 'iNgongoni'],
+  SHORTMONTHS: ['Bhi', 'Van', 'Vol', 'Mab', 'Nkh', 'Nhl', 'Kho', 'Ngc', 'Nyo', 'Mph', 'Lwe', 'Ngo'],
+  WEEKDAYS: ['Lisontfo', 'uMsombuluko', 'Lesibili', 'Lesitsatfu', 'Lesine', 'Lesihlanu', 'uMgcibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mso', 'Bil', 'Tsa', 'Ne', 'Hla', 'Mgc'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss_SZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss_SZ.js
new file mode 100644
index 0000000..847ee2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss_SZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Bhimbidvwane', 'iNdlovana', 'iNdlovu-lenkhulu', 'Mabasa', 'iNkhwekhweti', 'iNhlaba', 'Kholwane', 'iNgci', 'iNyoni', 'iMphala', 'Lweti', 'iNgongoni'],
+  SHORTMONTHS: ['Bhi', 'Van', 'Vol', 'Mab', 'Nkh', 'Nhl', 'Kho', 'Ngc', 'Nyo', 'Mph', 'Lwe', 'Ngo'],
+  WEEKDAYS: ['Lisontfo', 'uMsombuluko', 'Lesibili', 'Lesitsatfu', 'Lesine', 'Lesihlanu', 'uMgcibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mso', 'Bil', 'Tsa', 'Ne', 'Hla', 'Mgc'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss_ZA.js
new file mode 100644
index 0000000..847ee2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ss_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Bhimbidvwane', 'iNdlovana', 'iNdlovu-lenkhulu', 'Mabasa', 'iNkhwekhweti', 'iNhlaba', 'Kholwane', 'iNgci', 'iNyoni', 'iMphala', 'Lweti', 'iNgongoni'],
+  SHORTMONTHS: ['Bhi', 'Van', 'Vol', 'Mab', 'Nkh', 'Nhl', 'Kho', 'Ngc', 'Nyo', 'Mph', 'Lwe', 'Ngo'],
+  WEEKDAYS: ['Lisontfo', 'uMsombuluko', 'Lesibili', 'Lesitsatfu', 'Lesine', 'Lesihlanu', 'uMgcibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mso', 'Bil', 'Tsa', 'Ne', 'Hla', 'Mgc'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st.js
new file mode 100644
index 0000000..cb6a9f9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Phesekgong', 'Hlakola', 'Hlakubele', 'Mmese', 'Motsheanong', 'Phupjane', 'Phupu', 'Phata', 'Leotshe', 'Mphalane', 'Pundungwane', 'Tshitwe'],
+  SHORTMONTHS: ['Phe', 'Kol', 'Ube', 'Mme', 'Mot', 'Jan', 'Upu', 'Pha', 'Leo', 'Mph', 'Pun', 'Tsh'],
+  WEEKDAYS: ['Sontaha', 'Mmantaha', 'Labobedi', 'Laboraru', 'Labone', 'Labohlane', 'Moqebelo'],
+  SHORTWEEKDAYS: ['Son', 'Mma', 'Bed', 'Rar', 'Ne', 'Hla', 'Moq'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st_LS.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st_LS.js
new file mode 100644
index 0000000..cb6a9f9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st_LS.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Phesekgong', 'Hlakola', 'Hlakubele', 'Mmese', 'Motsheanong', 'Phupjane', 'Phupu', 'Phata', 'Leotshe', 'Mphalane', 'Pundungwane', 'Tshitwe'],
+  SHORTMONTHS: ['Phe', 'Kol', 'Ube', 'Mme', 'Mot', 'Jan', 'Upu', 'Pha', 'Leo', 'Mph', 'Pun', 'Tsh'],
+  WEEKDAYS: ['Sontaha', 'Mmantaha', 'Labobedi', 'Laboraru', 'Labone', 'Labohlane', 'Moqebelo'],
+  SHORTWEEKDAYS: ['Son', 'Mma', 'Bed', 'Rar', 'Ne', 'Hla', 'Moq'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st_ZA.js
new file mode 100644
index 0000000..cb6a9f9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__st_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Phesekgong', 'Hlakola', 'Hlakubele', 'Mmese', 'Motsheanong', 'Phupjane', 'Phupu', 'Phata', 'Leotshe', 'Mphalane', 'Pundungwane', 'Tshitwe'],
+  SHORTMONTHS: ['Phe', 'Kol', 'Ube', 'Mme', 'Mot', 'Jan', 'Upu', 'Pha', 'Leo', 'Mph', 'Pun', 'Tsh'],
+  WEEKDAYS: ['Sontaha', 'Mmantaha', 'Labobedi', 'Laboraru', 'Labone', 'Labohlane', 'Moqebelo'],
+  SHORTWEEKDAYS: ['Son', 'Mma', 'Bed', 'Rar', 'Ne', 'Hla', 'Moq'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv.js
new file mode 100644
index 0000000..35eab13
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f\u00f6re Kristus', 'efter Kristus'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['s\u00f6ndag', 'm\u00e5ndag', 'tisdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f6rdag'],
+  SHORTWEEKDAYS: ['s\u00f6n', 'm\u00e5n', 'tis', 'ons', 'tors', 'fre', 'l\u00f6r'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1:a kvartalet', '2:a kvartalet', '3:e kvartalet', '4:e kvartalet'],
+  AMPMS: ['fm', 'em'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'yyyy-MM-dd'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv_FI.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv_FI.js
new file mode 100644
index 0000000..35eab13
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv_FI.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f\u00f6re Kristus', 'efter Kristus'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['s\u00f6ndag', 'm\u00e5ndag', 'tisdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f6rdag'],
+  SHORTWEEKDAYS: ['s\u00f6n', 'm\u00e5n', 'tis', 'ons', 'tors', 'fre', 'l\u00f6r'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1:a kvartalet', '2:a kvartalet', '3:e kvartalet', '4:e kvartalet'],
+  AMPMS: ['fm', 'em'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'yyyy-MM-dd'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv_SE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv_SE.js
new file mode 100644
index 0000000..35eab13
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sv_SE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['f.Kr.', 'e.Kr.'],
+  ERANAMES: ['f\u00f6re Kristus', 'efter Kristus'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['januari', 'februari', 'mars', 'april', 'maj', 'juni', 'juli', 'augusti', 'september', 'oktober', 'november', 'december'],
+  SHORTMONTHS: ['jan', 'feb', 'mar', 'apr', 'maj', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec'],
+  WEEKDAYS: ['s\u00f6ndag', 'm\u00e5ndag', 'tisdag', 'onsdag', 'torsdag', 'fredag', 'l\u00f6rdag'],
+  SHORTWEEKDAYS: ['s\u00f6n', 'm\u00e5n', 'tis', 'ons', 'tors', 'fre', 'l\u00f6r'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'O', 'T', 'F', 'L'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['1:a kvartalet', '2:a kvartalet', '3:e kvartalet', '4:e kvartalet'],
+  AMPMS: ['fm', 'em'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'yyyy-MM-dd'],
+  TIMEFORMATS: ["'kl'. HH.mm.ss zzzz", 'HH.mm.ss z', 'HH.mm.ss', 'HH.mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw.js
new file mode 100644
index 0000000..e12df74
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KK', 'BK'],
+  ERANAMES: ['Kabla ya Kristo', 'Baada ya Kristo'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Machi', 'Aprili', 'Mei', 'Juni', 'Julai', 'Agosti', 'Septemba', 'Oktoba', 'Novemba', 'Desemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mac', 'Apr', 'Mei', 'Jun', 'Jul', 'Ago', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Jumapili', 'Jumatatu', 'Jumanne', 'Jumatano', 'Alhamisi', 'Ijumaa', 'Jumamosi'],
+  SHORTWEEKDAYS: ['Jpi', 'Jtt', 'Jnn', 'Jtn', 'Alh', 'Iju', 'Jmo'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['R1', 'R2', 'R3', 'R4'],
+  QUARTERS: ['robo ya kwanza', 'robo ya pili', 'robo ya tatu', 'robo ya nne'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw_KE.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw_KE.js
new file mode 100644
index 0000000..3276339
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw_KE.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KK', 'BK'],
+  ERANAMES: ['Kabla ya Kristo', 'Baada ya Kristo'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Machi', 'Aprili', 'Mei', 'Juni', 'Julai', 'Agosti', 'Septemba', 'Oktoba', 'Novemba', 'Desemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mac', 'Apr', 'Mei', 'Jun', 'Jul', 'Ago', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Jumapili', 'Jumatatu', 'Jumanne', 'Jumatano', 'Alhamisi', 'Ijumaa', 'Jumamosi'],
+  SHORTWEEKDAYS: ['Jpi', 'Jtt', 'Jnn', 'Jtn', 'Alh', 'Iju', 'Jmo'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['R1', 'R2', 'R3', 'R4'],
+  QUARTERS: ['robo ya kwanza', 'robo ya pili', 'robo ya tatu', 'robo ya nne'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw_TZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw_TZ.js
new file mode 100644
index 0000000..e12df74
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__sw_TZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KK', 'BK'],
+  ERANAMES: ['Kabla ya Kristo', 'Baada ya Kristo'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Januari', 'Februari', 'Machi', 'Aprili', 'Mei', 'Juni', 'Julai', 'Agosti', 'Septemba', 'Oktoba', 'Novemba', 'Desemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mac', 'Apr', 'Mei', 'Jun', 'Jul', 'Ago', 'Sep', 'Okt', 'Nov', 'Des'],
+  WEEKDAYS: ['Jumapili', 'Jumatatu', 'Jumanne', 'Jumatano', 'Alhamisi', 'Ijumaa', 'Jumamosi'],
+  SHORTWEEKDAYS: ['Jpi', 'Jtt', 'Jnn', 'Jtn', 'Alh', 'Iju', 'Jmo'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['R1', 'R2', 'R3', 'R4'],
+  QUARTERS: ['robo ya kwanza', 'robo ya pili', 'robo ya tatu', 'robo ya nne'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__syr.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__syr.js
new file mode 100644
index 0000000..c666ca1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__syr.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u070f\u071f\u0722 \u070f\u0712', '\u072b\u0712\u071b', '\u0710\u0715\u072a', '\u0722\u071d\u0723\u0722', '\u0710\u071d\u072a', '\u071a\u0719\u071d\u072a\u0722', '\u072c\u0721\u0718\u0719', '\u0710\u0712', '\u0710\u071d\u0720\u0718\u0720', '\u070f\u072c\u072b \u070f\u0710', '\u070f\u072c\u072b \u070f\u0712', '\u070f\u071f\u0722 \u070f\u0710'],
+  SHORTMONTHS: ['\u070f\u071f\u0722 \u070f\u0712', '\u072b\u0712\u071b', '\u0710\u0715\u072a', '\u0722\u071d\u0723\u0722', '\u0710\u071d\u072a', '\u071a\u0719\u071d\u072a\u0722', '\u072c\u0721\u0718\u0719', '\u0710\u0712', '\u0710\u071d\u0720\u0718\u0720', '\u070f\u072c\u072b \u070f\u0710', '\u070f\u072c\u072b \u070f\u0712', '\u070f\u071f\u0722 \u070f\u0710'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['dd MMMM, y', 'dd MMMM, y', 'dd/MM/yyyy', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss', 'h:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__syr_SY.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__syr_SY.js
new file mode 100644
index 0000000..7fdbb5c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__syr_SY.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u070f\u071f\u0722 \u070f\u0712', '\u072b\u0712\u071b', '\u0710\u0715\u072a', '\u0722\u071d\u0723\u0722', '\u0710\u071d\u072a', '\u071a\u0719\u071d\u072a\u0722', '\u072c\u0721\u0718\u0719', '\u0710\u0712', '\u0710\u071d\u0720\u0718\u0720', '\u070f\u072c\u072b \u070f\u0710', '\u070f\u072c\u072b \u070f\u0712', '\u070f\u071f\u0722 \u070f\u0710'],
+  SHORTMONTHS: ['\u070f\u071f\u0722 \u070f\u0712', '\u072b\u0712\u071b', '\u0710\u0715\u072a', '\u0722\u071d\u0723\u0722', '\u0710\u071d\u072a', '\u071a\u0719\u071d\u072a\u0722', '\u072c\u0721\u0718\u0719', '\u0710\u0712', '\u0710\u071d\u0720\u0718\u0720', '\u070f\u072c\u072b \u070f\u0710', '\u070f\u072c\u072b \u070f\u0712', '\u070f\u071f\u0722 \u070f\u0710'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['dd MMMM, y', 'dd MMMM, y', 'dd/MM/yyyy', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss', 'h:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [4, 5],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ta.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ta.js
new file mode 100644
index 0000000..df7ce5f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ta.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0b95\u0bbf\u0bae\u0bc1', '\u0b95\u0bbf\u0baa\u0bbf'],
+  ERANAMES: ['\u0b95\u0bbf\u0bb1\u0bbf\u0bb8\u0bcd\u0ba4\u0bc1\u0bb5\u0bc1\u0b95\u0bcd\u0b95\u0bc1 \u0bae\u0bc1\u0ba9\u0bcd', '\u0b85\u0ba9\u0bcb \u0b9f\u0bcb\u0bae\u0bbf\u0ba9\u0bbf'],
+  NARROWMONTHS: ['\u0b9c', '\u0baa\u0bbf', '\u0bae\u0bbe', '\u0b8f', '\u0bae\u0bc7', '\u0b9c\u0bc2', '\u0b9c\u0bc2', '\u0b86', '\u0b9a\u0bc6', '\u0b85', '\u0ba8', '\u0b9f\u0bbf'],
+  MONTHS: ['\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf', '\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf', '\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd', '\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd', '\u0bae\u0bc7', '\u0b9c\u0bc2\u0ba9\u0bcd', '\u0b9c\u0bc2\u0bb2\u0bc8', '\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd', '\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bae\u0bcd\u0baa\u0bb0\u0bcd', '\u0b85\u0b95\u0bcd\u0b9f\u0bcb\u0baa\u0bb0\u0bcd', '\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd', '\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd'],
+  SHORTMONTHS: ['\u0b9c\u0ba9.', '\u0baa\u0bbf\u0baa\u0bcd.', '\u0bae\u0bbe\u0bb0\u0bcd.', '\u0b8f\u0baa\u0bcd.', '\u0bae\u0bc7', '\u0b9c\u0bc2\u0ba9\u0bcd', '\u0b9c\u0bc2\u0bb2\u0bc8', '\u0b86\u0b95.', '\u0b9a\u0bc6\u0baa\u0bcd.', '\u0b85\u0b95\u0bcd.', '\u0ba8\u0bb5.', '\u0b9f\u0bbf\u0b9a.'],
+  WEEKDAYS: ['\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bc1', '\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0bb3\u0bcd', '\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd', '\u0baa\u0bc1\u0ba4\u0ba9\u0bcd', '\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0ba9\u0bcd', '\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf', '\u0b9a\u0ba9\u0bbf'],
+  SHORTWEEKDAYS: ['\u0b9e\u0bbe', '\u0ba4\u0bbf', '\u0b9a\u0bc6', '\u0baa\u0bc1', '\u0bb5\u0bbf', '\u0bb5\u0bc6', '\u0b9a'],
+  NARROWWEEKDAYS: ['\u0b9e\u0bbe', '\u0ba4\u0bbf', '\u0b9a\u0bc6', '\u0baa\u0bc1', '\u0bb5\u0bbf', '\u0bb5\u0bc6', '\u0b9a'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1', '2\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1', '3\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1', '4\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ta_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ta_IN.js
new file mode 100644
index 0000000..df7ce5f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ta_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0b95\u0bbf\u0bae\u0bc1', '\u0b95\u0bbf\u0baa\u0bbf'],
+  ERANAMES: ['\u0b95\u0bbf\u0bb1\u0bbf\u0bb8\u0bcd\u0ba4\u0bc1\u0bb5\u0bc1\u0b95\u0bcd\u0b95\u0bc1 \u0bae\u0bc1\u0ba9\u0bcd', '\u0b85\u0ba9\u0bcb \u0b9f\u0bcb\u0bae\u0bbf\u0ba9\u0bbf'],
+  NARROWMONTHS: ['\u0b9c', '\u0baa\u0bbf', '\u0bae\u0bbe', '\u0b8f', '\u0bae\u0bc7', '\u0b9c\u0bc2', '\u0b9c\u0bc2', '\u0b86', '\u0b9a\u0bc6', '\u0b85', '\u0ba8', '\u0b9f\u0bbf'],
+  MONTHS: ['\u0b9c\u0ba9\u0bb5\u0bb0\u0bbf', '\u0baa\u0bbf\u0baa\u0bcd\u0bb0\u0bb5\u0bb0\u0bbf', '\u0bae\u0bbe\u0bb0\u0bcd\u0b9a\u0bcd', '\u0b8f\u0baa\u0bcd\u0bb0\u0bb2\u0bcd', '\u0bae\u0bc7', '\u0b9c\u0bc2\u0ba9\u0bcd', '\u0b9c\u0bc2\u0bb2\u0bc8', '\u0b86\u0b95\u0bb8\u0bcd\u0b9f\u0bcd', '\u0b9a\u0bc6\u0baa\u0bcd\u0b9f\u0bae\u0bcd\u0baa\u0bb0\u0bcd', '\u0b85\u0b95\u0bcd\u0b9f\u0bcb\u0baa\u0bb0\u0bcd', '\u0ba8\u0bb5\u0bae\u0bcd\u0baa\u0bb0\u0bcd', '\u0b9f\u0bbf\u0b9a\u0bae\u0bcd\u0baa\u0bb0\u0bcd'],
+  SHORTMONTHS: ['\u0b9c\u0ba9.', '\u0baa\u0bbf\u0baa\u0bcd.', '\u0bae\u0bbe\u0bb0\u0bcd.', '\u0b8f\u0baa\u0bcd.', '\u0bae\u0bc7', '\u0b9c\u0bc2\u0ba9\u0bcd', '\u0b9c\u0bc2\u0bb2\u0bc8', '\u0b86\u0b95.', '\u0b9a\u0bc6\u0baa\u0bcd.', '\u0b85\u0b95\u0bcd.', '\u0ba8\u0bb5.', '\u0b9f\u0bbf\u0b9a.'],
+  WEEKDAYS: ['\u0b9e\u0bbe\u0baf\u0bbf\u0bb1\u0bc1', '\u0ba4\u0bbf\u0b99\u0bcd\u0b95\u0bb3\u0bcd', '\u0b9a\u0bc6\u0bb5\u0bcd\u0bb5\u0bbe\u0baf\u0bcd', '\u0baa\u0bc1\u0ba4\u0ba9\u0bcd', '\u0bb5\u0bbf\u0baf\u0bbe\u0bb4\u0ba9\u0bcd', '\u0bb5\u0bc6\u0bb3\u0bcd\u0bb3\u0bbf', '\u0b9a\u0ba9\u0bbf'],
+  SHORTWEEKDAYS: ['\u0b9e\u0bbe', '\u0ba4\u0bbf', '\u0b9a\u0bc6', '\u0baa\u0bc1', '\u0bb5\u0bbf', '\u0bb5\u0bc6', '\u0b9a'],
+  NARROWWEEKDAYS: ['\u0b9e\u0bbe', '\u0ba4\u0bbf', '\u0b9a\u0bc6', '\u0baa\u0bc1', '\u0bb5\u0bbf', '\u0bb5\u0bc6', '\u0b9a'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1', '2\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1', '3\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1', '4\u0b86\u0bae\u0bcd \u0b95\u0bbe\u0bb2\u0bbe\u0ba3\u0bcd\u0b9f\u0bc1'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE, d MMMM, y', 'd MMMM, y', 'd MMM, y', 'd-M-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__te.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__te.js
new file mode 100644
index 0000000..aefb208
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__te.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0c08\u0c38\u0c3e\u0c2a\u0c42\u0c30\u0c4d\u0c35.', '\u0c38\u0c28\u0c4d.'],
+  NARROWMONTHS: ['\u0c1c', '\u0c2b\u0c3f', '\u0c2e', '\u0c0e', '\u0c2e\u0c46', '\u0c1c\u0c41', '\u0c1c\u0c41', '\u0c06', '\u0c38\u0c46', '\u0c05', '\u0c28', '\u0c21\u0c3f'],
+  MONTHS: ['\u0c1c\u0c28\u0c35\u0c30\u0c3f', '\u0c2b\u0c3f\u0c2c\u0c4d\u0c30\u0c35\u0c30\u0c3f', '\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f', '\u0c0f\u0c2a\u0c4d\u0c30\u0c3f\u0c32\u0c4d', '\u0c2e\u0c47', '\u0c1c\u0c42\u0c28\u0c4d', '\u0c1c\u0c42\u0c32\u0c48', '\u0c06\u0c17\u0c38\u0c4d\u0c1f\u0c41', '\u0c38\u0c46\u0c2a\u0c4d\u0c1f\u0c46\u0c02\u0c2c\u0c30\u0c4d', '\u0c05\u0c15\u0c4d\u0c1f\u0c4b\u0c2c\u0c30\u0c4d', '\u0c28\u0c35\u0c02\u0c2c\u0c30\u0c4d', '\u0c21\u0c3f\u0c38\u0c46\u0c02\u0c2c\u0c30\u0c4d'],
+  SHORTMONTHS: ['\u0c1c\u0c28\u0c35\u0c30\u0c3f', '\u0c2b\u0c3f\u0c2c\u0c4d\u0c30\u0c35\u0c30\u0c3f', '\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f', '\u0c0f\u0c2a\u0c4d\u0c30\u0c3f\u0c32\u0c4d', '\u0c2e\u0c47', '\u0c1c\u0c42\u0c28\u0c4d', '\u0c1c\u0c42\u0c32\u0c48', '\u0c06\u0c17\u0c38\u0c4d\u0c1f\u0c41', '\u0c38\u0c46\u0c2a\u0c4d\u0c1f\u0c46\u0c02\u0c2c\u0c30\u0c4d', '\u0c05\u0c15\u0c4d\u0c1f\u0c4b\u0c2c\u0c30\u0c4d', '\u0c28\u0c35\u0c02\u0c2c\u0c30\u0c4d', '\u0c21\u0c3f\u0c38\u0c46\u0c02\u0c2c\u0c30\u0c4d'],
+  WEEKDAYS: ['\u0c06\u0c26\u0c3f\u0c35\u0c3e\u0c30\u0c02', '\u0c38\u0c4b\u0c2e\u0c35\u0c3e\u0c30\u0c02', '\u0c2e\u0c02\u0c17\u0c33\u0c35\u0c3e\u0c30\u0c02', '\u0c2c\u0c41\u0c27\u0c35\u0c3e\u0c30\u0c02', '\u0c17\u0c41\u0c30\u0c41\u0c35\u0c3e\u0c30\u0c02', '\u0c36\u0c41\u0c15\u0c4d\u0c30\u0c35\u0c3e\u0c30\u0c02', '\u0c36\u0c28\u0c3f\u0c35\u0c3e\u0c30\u0c02'],
+  SHORTWEEKDAYS: ['\u0c06\u0c26\u0c3f', '\u0c38\u0c4b\u0c2e', '\u0c2e\u0c02\u0c17\u0c33', '\u0c2c\u0c41\u0c27', '\u0c17\u0c41\u0c30\u0c41', '\u0c36\u0c41\u0c15\u0c4d\u0c30', '\u0c36\u0c28\u0c3f'],
+  NARROWWEEKDAYS: ['\u0c06', '\u0c38\u0c4b', '\u0c2e', '\u0c2d\u0c41', '\u0c17\u0c41', '\u0c36\u0c41', '\u0c36'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0c12\u0c15\u0c1f\u0c3f 1', '\u0c30\u0c46\u0c02\u0c21\u0c41 2', '\u0c2e\u0c42\u0c21\u0c41 3', '\u0c28\u0c3e\u0c32\u0c41\u0c17\u0c41 4'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd-MM-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__te_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__te_IN.js
new file mode 100644
index 0000000..aefb208
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__te_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['\u0c08\u0c38\u0c3e\u0c2a\u0c42\u0c30\u0c4d\u0c35.', '\u0c38\u0c28\u0c4d.'],
+  NARROWMONTHS: ['\u0c1c', '\u0c2b\u0c3f', '\u0c2e', '\u0c0e', '\u0c2e\u0c46', '\u0c1c\u0c41', '\u0c1c\u0c41', '\u0c06', '\u0c38\u0c46', '\u0c05', '\u0c28', '\u0c21\u0c3f'],
+  MONTHS: ['\u0c1c\u0c28\u0c35\u0c30\u0c3f', '\u0c2b\u0c3f\u0c2c\u0c4d\u0c30\u0c35\u0c30\u0c3f', '\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f', '\u0c0f\u0c2a\u0c4d\u0c30\u0c3f\u0c32\u0c4d', '\u0c2e\u0c47', '\u0c1c\u0c42\u0c28\u0c4d', '\u0c1c\u0c42\u0c32\u0c48', '\u0c06\u0c17\u0c38\u0c4d\u0c1f\u0c41', '\u0c38\u0c46\u0c2a\u0c4d\u0c1f\u0c46\u0c02\u0c2c\u0c30\u0c4d', '\u0c05\u0c15\u0c4d\u0c1f\u0c4b\u0c2c\u0c30\u0c4d', '\u0c28\u0c35\u0c02\u0c2c\u0c30\u0c4d', '\u0c21\u0c3f\u0c38\u0c46\u0c02\u0c2c\u0c30\u0c4d'],
+  SHORTMONTHS: ['\u0c1c\u0c28\u0c35\u0c30\u0c3f', '\u0c2b\u0c3f\u0c2c\u0c4d\u0c30\u0c35\u0c30\u0c3f', '\u0c2e\u0c3e\u0c30\u0c4d\u0c1a\u0c3f', '\u0c0f\u0c2a\u0c4d\u0c30\u0c3f\u0c32\u0c4d', '\u0c2e\u0c47', '\u0c1c\u0c42\u0c28\u0c4d', '\u0c1c\u0c42\u0c32\u0c48', '\u0c06\u0c17\u0c38\u0c4d\u0c1f\u0c41', '\u0c38\u0c46\u0c2a\u0c4d\u0c1f\u0c46\u0c02\u0c2c\u0c30\u0c4d', '\u0c05\u0c15\u0c4d\u0c1f\u0c4b\u0c2c\u0c30\u0c4d', '\u0c28\u0c35\u0c02\u0c2c\u0c30\u0c4d', '\u0c21\u0c3f\u0c38\u0c46\u0c02\u0c2c\u0c30\u0c4d'],
+  WEEKDAYS: ['\u0c06\u0c26\u0c3f\u0c35\u0c3e\u0c30\u0c02', '\u0c38\u0c4b\u0c2e\u0c35\u0c3e\u0c30\u0c02', '\u0c2e\u0c02\u0c17\u0c33\u0c35\u0c3e\u0c30\u0c02', '\u0c2c\u0c41\u0c27\u0c35\u0c3e\u0c30\u0c02', '\u0c17\u0c41\u0c30\u0c41\u0c35\u0c3e\u0c30\u0c02', '\u0c36\u0c41\u0c15\u0c4d\u0c30\u0c35\u0c3e\u0c30\u0c02', '\u0c36\u0c28\u0c3f\u0c35\u0c3e\u0c30\u0c02'],
+  SHORTWEEKDAYS: ['\u0c06\u0c26\u0c3f', '\u0c38\u0c4b\u0c2e', '\u0c2e\u0c02\u0c17\u0c33', '\u0c2c\u0c41\u0c27', '\u0c17\u0c41\u0c30\u0c41', '\u0c36\u0c41\u0c15\u0c4d\u0c30', '\u0c36\u0c28\u0c3f'],
+  NARROWWEEKDAYS: ['\u0c06', '\u0c38\u0c4b', '\u0c2e', '\u0c2d\u0c41', '\u0c17\u0c41', '\u0c36\u0c41', '\u0c36'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0c12\u0c15\u0c1f\u0c3f 1', '\u0c30\u0c46\u0c02\u0c21\u0c41 2', '\u0c2e\u0c42\u0c21\u0c41 3', '\u0c28\u0c3e\u0c32\u0c41\u0c17\u0c41 4'],
+  AMPMS: ['am', 'pm'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd-MM-yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg.js
new file mode 100644
index 0000000..574711a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u041f\u0435\u041c', '\u041f\u0430\u041c'],
+  ERANAMES: ['\u041f\u0435\u0448 \u0430\u0437 \u043c\u0438\u043b\u043e\u0434', '\u041f\u0430\u041c'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u042f\u043d\u0432\u0430\u0440', '\u0424\u0435\u0432\u0440\u0430\u043b', '\u041c\u0430\u0440\u0442', '\u0410\u043f\u0440\u0435\u043b', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433\u0443\u0441\u0442', '\u0421\u0435\u043d\u0442\u044f\u0431\u0440', '\u041e\u043a\u0442\u044f\u0431\u0440', '\u041d\u043e\u044f\u0431\u0440', '\u0414\u0435\u043a\u0430\u0431\u0440'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u042f\u043a\u0448\u0430\u043d\u0431\u0435', '\u0414\u0443\u0448\u0430\u043d\u0431\u0435', '\u0421\u0435\u0448\u0430\u043d\u0431\u0435', '\u0427\u043e\u0440\u0448\u0430\u043d\u0431\u0435', '\u041f\u0430\u043d\u04b7\u0448\u0430\u043d\u0431\u0435', '\u04b6\u0443\u043c\u044a\u0430', '\u0428\u0430\u043d\u0431\u0435'],
+  SHORTWEEKDAYS: ['\u042f\u0448\u0431', '\u0414\u0448\u0431', '\u0421\u0448\u0431', '\u0427\u0448\u0431', '\u041f\u0448\u0431', '\u04b6\u043c\u044a', '\u0428\u043d\u0431'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u043f\u0435. \u0447\u043e.', '\u043f\u0430. \u0447\u043e.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_Cyrl.js
new file mode 100644
index 0000000..574711a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_Cyrl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u041f\u0435\u041c', '\u041f\u0430\u041c'],
+  ERANAMES: ['\u041f\u0435\u0448 \u0430\u0437 \u043c\u0438\u043b\u043e\u0434', '\u041f\u0430\u041c'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u042f\u043d\u0432\u0430\u0440', '\u0424\u0435\u0432\u0440\u0430\u043b', '\u041c\u0430\u0440\u0442', '\u0410\u043f\u0440\u0435\u043b', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433\u0443\u0441\u0442', '\u0421\u0435\u043d\u0442\u044f\u0431\u0440', '\u041e\u043a\u0442\u044f\u0431\u0440', '\u041d\u043e\u044f\u0431\u0440', '\u0414\u0435\u043a\u0430\u0431\u0440'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u042f\u043a\u0448\u0430\u043d\u0431\u0435', '\u0414\u0443\u0448\u0430\u043d\u0431\u0435', '\u0421\u0435\u0448\u0430\u043d\u0431\u0435', '\u0427\u043e\u0440\u0448\u0430\u043d\u0431\u0435', '\u041f\u0430\u043d\u04b7\u0448\u0430\u043d\u0431\u0435', '\u04b6\u0443\u043c\u044a\u0430', '\u0428\u0430\u043d\u0431\u0435'],
+  SHORTWEEKDAYS: ['\u042f\u0448\u0431', '\u0414\u0448\u0431', '\u0421\u0448\u0431', '\u0427\u0448\u0431', '\u041f\u0448\u0431', '\u04b6\u043c\u044a', '\u0428\u043d\u0431'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u043f\u0435. \u0447\u043e.', '\u043f\u0430. \u0447\u043e.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_Cyrl_TJ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_Cyrl_TJ.js
new file mode 100644
index 0000000..574711a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_Cyrl_TJ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u041f\u0435\u041c', '\u041f\u0430\u041c'],
+  ERANAMES: ['\u041f\u0435\u0448 \u0430\u0437 \u043c\u0438\u043b\u043e\u0434', '\u041f\u0430\u041c'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u042f\u043d\u0432\u0430\u0440', '\u0424\u0435\u0432\u0440\u0430\u043b', '\u041c\u0430\u0440\u0442', '\u0410\u043f\u0440\u0435\u043b', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433\u0443\u0441\u0442', '\u0421\u0435\u043d\u0442\u044f\u0431\u0440', '\u041e\u043a\u0442\u044f\u0431\u0440', '\u041d\u043e\u044f\u0431\u0440', '\u0414\u0435\u043a\u0430\u0431\u0440'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u042f\u043a\u0448\u0430\u043d\u0431\u0435', '\u0414\u0443\u0448\u0430\u043d\u0431\u0435', '\u0421\u0435\u0448\u0430\u043d\u0431\u0435', '\u0427\u043e\u0440\u0448\u0430\u043d\u0431\u0435', '\u041f\u0430\u043d\u04b7\u0448\u0430\u043d\u0431\u0435', '\u04b6\u0443\u043c\u044a\u0430', '\u0428\u0430\u043d\u0431\u0435'],
+  SHORTWEEKDAYS: ['\u042f\u0448\u0431', '\u0414\u0448\u0431', '\u0421\u0448\u0431', '\u0427\u0448\u0431', '\u041f\u0448\u0431', '\u04b6\u043c\u044a', '\u0428\u043d\u0431'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u043f\u0435. \u0447\u043e.', '\u043f\u0430. \u0447\u043e.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_TJ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_TJ.js
new file mode 100644
index 0000000..574711a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tg_TJ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u041f\u0435\u041c', '\u041f\u0430\u041c'],
+  ERANAMES: ['\u041f\u0435\u0448 \u0430\u0437 \u043c\u0438\u043b\u043e\u0434', '\u041f\u0430\u041c'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['\u042f\u043d\u0432\u0430\u0440', '\u0424\u0435\u0432\u0440\u0430\u043b', '\u041c\u0430\u0440\u0442', '\u0410\u043f\u0440\u0435\u043b', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433\u0443\u0441\u0442', '\u0421\u0435\u043d\u0442\u044f\u0431\u0440', '\u041e\u043a\u0442\u044f\u0431\u0440', '\u041d\u043e\u044f\u0431\u0440', '\u0414\u0435\u043a\u0430\u0431\u0440'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u042f\u043a\u0448\u0430\u043d\u0431\u0435', '\u0414\u0443\u0448\u0430\u043d\u0431\u0435', '\u0421\u0435\u0448\u0430\u043d\u0431\u0435', '\u0427\u043e\u0440\u0448\u0430\u043d\u0431\u0435', '\u041f\u0430\u043d\u04b7\u0448\u0430\u043d\u0431\u0435', '\u04b6\u0443\u043c\u044a\u0430', '\u0428\u0430\u043d\u0431\u0435'],
+  SHORTWEEKDAYS: ['\u042f\u0448\u0431', '\u0414\u0448\u0431', '\u0421\u0448\u0431', '\u0427\u0448\u0431', '\u041f\u0448\u0431', '\u04b6\u043c\u044a', '\u0428\u043d\u0431'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u043f\u0435. \u0447\u043e.', '\u043f\u0430. \u0447\u043e.'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__th.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__th.js
new file mode 100644
index 0000000..0a0bd4e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__th.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0e1b\u0e35\u0e01\u0e48\u0e2d\u0e19 \u0e04.\u0e28.', '\u0e04.\u0e28.'],
+  ERANAMES: ['\u0e1b\u0e35\u0e01\u0e48\u0e2d\u0e19\u0e04\u0e23\u0e34\u0e2a\u0e15\u0e4c\u0e28\u0e31\u0e01\u0e23\u0e32\u0e0a', '\u0e04\u0e23\u0e34\u0e2a\u0e15\u0e4c\u0e28\u0e31\u0e01\u0e23\u0e32\u0e0a'],
+  NARROWMONTHS: ['\u0e21.\u0e04.', '\u0e01.\u0e1e.', '\u0e21\u0e35.\u0e04.', '\u0e40\u0e21.\u0e22.', '\u0e1e.\u0e04.', '\u0e21\u0e34.\u0e22.', '\u0e01.\u0e04.', '\u0e2a.\u0e04.', '\u0e01.\u0e22.', '\u0e15.\u0e04.', '\u0e1e.\u0e22.', '\u0e18.\u0e04.'],
+  MONTHS: ['\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21', '\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c', '\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21', '\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19', '\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21', '\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19', '\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21', '\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21', '\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19', '\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21', '\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19', '\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21'],
+  SHORTMONTHS: ['\u0e21.\u0e04.', '\u0e01.\u0e1e.', '\u0e21\u0e35.\u0e04.', '\u0e40\u0e21.\u0e22.', '\u0e1e.\u0e04.', '\u0e21\u0e34.\u0e22.', '\u0e01.\u0e04.', '\u0e2a.\u0e04.', '\u0e01.\u0e22.', '\u0e15.\u0e04.', '\u0e1e.\u0e22.', '\u0e18.\u0e04.'],
+  WEEKDAYS: ['\u0e27\u0e31\u0e19\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c', '\u0e27\u0e31\u0e19\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c', '\u0e27\u0e31\u0e19\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23', '\u0e27\u0e31\u0e19\u0e1e\u0e38\u0e18', '\u0e27\u0e31\u0e19\u0e1e\u0e24\u0e2b\u0e31\u0e2a\u0e1a\u0e14\u0e35', '\u0e27\u0e31\u0e19\u0e28\u0e38\u0e01\u0e23\u0e4c', '\u0e27\u0e31\u0e19\u0e40\u0e2a\u0e32\u0e23\u0e4c'],
+  SHORTWEEKDAYS: ['\u0e2d\u0e32.', '\u0e08.', '\u0e2d.', '\u0e1e.', '\u0e1e\u0e24.', '\u0e28.', '\u0e2a.'],
+  NARROWWEEKDAYS: ['\u0e2d', '\u0e08', '\u0e2d', '\u0e1e', '\u0e1e', '\u0e28', '\u0e2a'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 1', '\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 2', '\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 3', '\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 4'],
+  AMPMS: ['\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07', '\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07'],
+  DATEFORMATS: ['EEEE\u0e17\u0e35\u0e48 d MMMM G y', 'd MMMM y', 'd MMM y', 'd/M/yyyy'],
+  TIMEFORMATS: ['H \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 m \u0e19\u0e32\u0e17\u0e35 ss \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35 zzzz', 'H \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 m \u0e19\u0e32\u0e17\u0e35 ss \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35 z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__th_TH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__th_TH.js
new file mode 100644
index 0000000..0a0bd4e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__th_TH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0e1b\u0e35\u0e01\u0e48\u0e2d\u0e19 \u0e04.\u0e28.', '\u0e04.\u0e28.'],
+  ERANAMES: ['\u0e1b\u0e35\u0e01\u0e48\u0e2d\u0e19\u0e04\u0e23\u0e34\u0e2a\u0e15\u0e4c\u0e28\u0e31\u0e01\u0e23\u0e32\u0e0a', '\u0e04\u0e23\u0e34\u0e2a\u0e15\u0e4c\u0e28\u0e31\u0e01\u0e23\u0e32\u0e0a'],
+  NARROWMONTHS: ['\u0e21.\u0e04.', '\u0e01.\u0e1e.', '\u0e21\u0e35.\u0e04.', '\u0e40\u0e21.\u0e22.', '\u0e1e.\u0e04.', '\u0e21\u0e34.\u0e22.', '\u0e01.\u0e04.', '\u0e2a.\u0e04.', '\u0e01.\u0e22.', '\u0e15.\u0e04.', '\u0e1e.\u0e22.', '\u0e18.\u0e04.'],
+  MONTHS: ['\u0e21\u0e01\u0e23\u0e32\u0e04\u0e21', '\u0e01\u0e38\u0e21\u0e20\u0e32\u0e1e\u0e31\u0e19\u0e18\u0e4c', '\u0e21\u0e35\u0e19\u0e32\u0e04\u0e21', '\u0e40\u0e21\u0e29\u0e32\u0e22\u0e19', '\u0e1e\u0e24\u0e29\u0e20\u0e32\u0e04\u0e21', '\u0e21\u0e34\u0e16\u0e38\u0e19\u0e32\u0e22\u0e19', '\u0e01\u0e23\u0e01\u0e0e\u0e32\u0e04\u0e21', '\u0e2a\u0e34\u0e07\u0e2b\u0e32\u0e04\u0e21', '\u0e01\u0e31\u0e19\u0e22\u0e32\u0e22\u0e19', '\u0e15\u0e38\u0e25\u0e32\u0e04\u0e21', '\u0e1e\u0e24\u0e28\u0e08\u0e34\u0e01\u0e32\u0e22\u0e19', '\u0e18\u0e31\u0e19\u0e27\u0e32\u0e04\u0e21'],
+  SHORTMONTHS: ['\u0e21.\u0e04.', '\u0e01.\u0e1e.', '\u0e21\u0e35.\u0e04.', '\u0e40\u0e21.\u0e22.', '\u0e1e.\u0e04.', '\u0e21\u0e34.\u0e22.', '\u0e01.\u0e04.', '\u0e2a.\u0e04.', '\u0e01.\u0e22.', '\u0e15.\u0e04.', '\u0e1e.\u0e22.', '\u0e18.\u0e04.'],
+  WEEKDAYS: ['\u0e27\u0e31\u0e19\u0e2d\u0e32\u0e17\u0e34\u0e15\u0e22\u0e4c', '\u0e27\u0e31\u0e19\u0e08\u0e31\u0e19\u0e17\u0e23\u0e4c', '\u0e27\u0e31\u0e19\u0e2d\u0e31\u0e07\u0e04\u0e32\u0e23', '\u0e27\u0e31\u0e19\u0e1e\u0e38\u0e18', '\u0e27\u0e31\u0e19\u0e1e\u0e24\u0e2b\u0e31\u0e2a\u0e1a\u0e14\u0e35', '\u0e27\u0e31\u0e19\u0e28\u0e38\u0e01\u0e23\u0e4c', '\u0e27\u0e31\u0e19\u0e40\u0e2a\u0e32\u0e23\u0e4c'],
+  SHORTWEEKDAYS: ['\u0e2d\u0e32.', '\u0e08.', '\u0e2d.', '\u0e1e.', '\u0e1e\u0e24.', '\u0e28.', '\u0e2a.'],
+  NARROWWEEKDAYS: ['\u0e2d', '\u0e08', '\u0e2d', '\u0e1e', '\u0e1e', '\u0e28', '\u0e2a'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 1', '\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 2', '\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 3', '\u0e44\u0e15\u0e23\u0e21\u0e32\u0e2a 4'],
+  AMPMS: ['\u0e01\u0e48\u0e2d\u0e19\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07', '\u0e2b\u0e25\u0e31\u0e07\u0e40\u0e17\u0e35\u0e48\u0e22\u0e07'],
+  DATEFORMATS: ['EEEE\u0e17\u0e35\u0e48 d MMMM G y', 'd MMMM y', 'd MMM y', 'd/M/yyyy'],
+  TIMEFORMATS: ['H \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 m \u0e19\u0e32\u0e17\u0e35 ss \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35 zzzz', 'H \u0e19\u0e32\u0e2c\u0e34\u0e01\u0e32 m \u0e19\u0e32\u0e17\u0e35 ss \u0e27\u0e34\u0e19\u0e32\u0e17\u0e35 z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti.js
new file mode 100644
index 0000000..130df64
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u1230\u1295\u1260\u1275', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u1228\u1261\u12d5', '\u1283\u1219\u1235', '\u12d3\u122d\u1262', '\u1240\u12f3\u121d'],
+  SHORTWEEKDAYS: ['\u1230\u1295\u1260', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u1228\u1261\u12d5', '\u1283\u1219\u1235', '\u12d3\u122d\u1262', '\u1240\u12f3\u121d'],
+  NARROWWEEKDAYS: ['\u1230', '\u1230', '\u1220', '\u1228', '\u1283', '\u12d3', '\u1240'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u1295\u1309\u1206 \u1230\u12d3\u1270', '\u12f5\u1215\u122d \u1230\u12d3\u1275'],
+  DATEFORMATS: ['EEEE\u1363 dd MMMM \u1218\u12d3\u120d\u1272 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti_ER.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti_ER.js
new file mode 100644
index 0000000..1179edd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti_ER.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1325\u122a', '\u1208\u12ab\u1272\u1275', '\u1218\u130b\u1262\u1275', '\u121a\u12eb\u12dd\u12eb', '\u130d\u1295\u1266\u1275', '\u1230\u1290', '\u1213\u121d\u1208', '\u1290\u1213\u1230', '\u1218\u1235\u12a8\u1228\u121d', '\u1325\u1245\u121d\u1272', '\u1215\u12f3\u122d', '\u1273\u1215\u1233\u1235'],
+  SHORTMONTHS: ['\u1325\u122a', '\u1208\u12ab\u1272', '\u1218\u130b\u1262', '\u121a\u12eb\u12dd', '\u130d\u1295\u1266', '\u1230\u1290', '\u1213\u121d\u1208', '\u1290\u1213\u1230', '\u1218\u1235\u12a8', '\u1325\u1245\u121d', '\u1215\u12f3\u122d', '\u1273\u1215\u1233'],
+  WEEKDAYS: ['\u1230\u1295\u1260\u1275', '\u1230\u1291\u12ed', '\u1230\u1209\u1235', '\u1228\u1261\u12d5', '\u1213\u1219\u1235', '\u12d3\u122d\u1262', '\u1240\u12f3\u121d'],
+  SHORTWEEKDAYS: ['\u1230\u1295\u1260', '\u1230\u1291\u12ed', '\u1230\u1209\u1235', '\u1228\u1261\u12d5', '\u1213\u1219\u1235', '\u12d3\u122d\u1262', '\u1240\u12f3\u121d'],
+  NARROWWEEKDAYS: ['\u1230', '\u1230', '\u1220', '\u1228', '\u1283', '\u12d3', '\u1240'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u1295\u1309\u1206 \u1230\u12d3\u1270', '\u12f5\u1215\u122d \u1230\u12d3\u1275'],
+  DATEFORMATS: ['EEEE\u1361 dd MMMM \u1218\u12d3\u120d\u1272 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti_ET.js
new file mode 100644
index 0000000..130df64
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ti_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u1230\u1295\u1260\u1275', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u1228\u1261\u12d5', '\u1283\u1219\u1235', '\u12d3\u122d\u1262', '\u1240\u12f3\u121d'],
+  SHORTWEEKDAYS: ['\u1230\u1295\u1260', '\u1230\u1291\u12ed', '\u1220\u1209\u1235', '\u1228\u1261\u12d5', '\u1283\u1219\u1235', '\u12d3\u122d\u1262', '\u1240\u12f3\u121d'],
+  NARROWWEEKDAYS: ['\u1230', '\u1230', '\u1220', '\u1228', '\u1283', '\u12d3', '\u1240'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u1295\u1309\u1206 \u1230\u12d3\u1270', '\u12f5\u1215\u122d \u1230\u12d3\u1275'],
+  DATEFORMATS: ['EEEE\u1363 dd MMMM \u1218\u12d3\u120d\u1272 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tig.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tig.js
new file mode 100644
index 0000000..c522e77
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tig.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u1230\u1295\u1260\u1275 \u12d3\u1263\u12ed', '\u1230\u1296', '\u1273\u120b\u1238\u1296', '\u12a3\u1228\u122d\u1263\u12d3', '\u12a8\u121a\u123d', '\u1305\u121d\u12d3\u1275', '\u1230\u1295\u1260\u1275 \u1295\u12a2\u123d'],
+  SHORTWEEKDAYS: ['\u1230/\u12d3', '\u1230\u1296', '\u1273\u120b\u1238', '\u12a3\u1228\u122d', '\u12a8\u121a\u123d', '\u1305\u121d\u12d3', '\u1230/\u1295'],
+  NARROWWEEKDAYS: ['\u1230', '\u1230', '\u1273', '\u12a3', '\u12a8', '\u1305', '\u1230'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u1240\u12f0\u121d \u1230\u122d\u121d\u12d5\u120d', '\u1213\u1246 \u1235\u122d\u121d\u12d5\u120d'],
+  DATEFORMATS: ['EEEE\u1361 dd MMMM \u12ee\u121d y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tig_ER.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tig_ER.js
new file mode 100644
index 0000000..bd19af7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tig_ER.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  ERANAMES: ['\u12d3/\u12d3', '\u12d3/\u121d'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u1230\u1295\u1260\u1275 \u12d3\u1263\u12ed', '\u1230\u1296', '\u1273\u120b\u1238\u1296', '\u12a3\u1228\u122d\u1263\u12d3', '\u12a8\u121a\u123d', '\u1305\u121d\u12d3\u1275', '\u1230\u1295\u1260\u1275 \u1295\u12a2\u123d'],
+  SHORTWEEKDAYS: ['\u1230/\u12d3', '\u1230\u1296', '\u1273\u120b\u1238', '\u12a3\u1228\u122d', '\u12a8\u121a\u123d', '\u1305\u121d\u12d3', '\u1230/\u1295'],
+  NARROWWEEKDAYS: ['\u1230', '\u1230', '\u1273', '\u12a3', '\u12a8', '\u1305', '\u1230'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u1240\u12f0\u121d \u1230\u122d\u121d\u12d5\u120d', '\u1213\u1246 \u1235\u122d\u121d\u12d5\u120d'],
+  DATEFORMATS: ['EEEE\u1361 dd MMMM \u12ee\u121d y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tl.js
new file mode 100644
index 0000000..0b289fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['E', 'P', 'M', 'A', 'M', 'H', 'H', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', 'Agosto', 'Setyembre', 'Oktubre', 'Nobyembre', 'Disyembre'],
+  SHORTMONTHS: ['Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', 'Set', 'Okt', 'Nob', 'Dis'],
+  WEEKDAYS: ['Linggo', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado'],
+  SHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Mye', 'Huw', 'Bye', 'Sab'],
+  STANDALONESHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab'],
+  NARROWWEEKDAYS: ['L', 'L', 'M', 'M', 'H', 'B', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM dd y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tl_PH.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tl_PH.js
new file mode 100644
index 0000000..0b289fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tl_PH.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['E', 'P', 'M', 'A', 'M', 'H', 'H', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Enero', 'Pebrero', 'Marso', 'Abril', 'Mayo', 'Hunyo', 'Hulyo', 'Agosto', 'Setyembre', 'Oktubre', 'Nobyembre', 'Disyembre'],
+  SHORTMONTHS: ['Ene', 'Peb', 'Mar', 'Abr', 'May', 'Hun', 'Hul', 'Ago', 'Set', 'Okt', 'Nob', 'Dis'],
+  WEEKDAYS: ['Linggo', 'Lunes', 'Martes', 'Miyerkules', 'Huwebes', 'Biyernes', 'Sabado'],
+  SHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Mye', 'Huw', 'Bye', 'Sab'],
+  STANDALONESHORTWEEKDAYS: ['Lin', 'Lun', 'Mar', 'Miy', 'Huw', 'Biy', 'Sab'],
+  NARROWWEEKDAYS: ['L', 'L', 'M', 'M', 'H', 'B', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM dd y', 'MMMM d, y', 'MMM d, y', 'M/d/yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tn.js
new file mode 100644
index 0000000..f163994
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Ferikgong', 'Tlhakole', 'Mopitlo', 'Moranang', 'Motsheganang', 'Seetebosigo', 'Phukwi', 'Phatwe', 'Lwetse', 'Diphalane', 'Ngwanatsele', 'Sedimonthole'],
+  SHORTMONTHS: ['Fer', 'Tlh', 'Mop', 'Mor', 'Mot', 'See', 'Phu', 'Pha', 'Lwe', 'Dip', 'Ngw', 'Sed'],
+  WEEKDAYS: ['Tshipi', 'Mosopulogo', 'Labobedi', 'Laboraro', 'Labone', 'Labotlhano', 'Matlhatso'],
+  SHORTWEEKDAYS: ['Tsh', 'Mos', 'Bed', 'Rar', 'Ne', 'Tla', 'Mat'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tn_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tn_ZA.js
new file mode 100644
index 0000000..f163994
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tn_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Ferikgong', 'Tlhakole', 'Mopitlo', 'Moranang', 'Motsheganang', 'Seetebosigo', 'Phukwi', 'Phatwe', 'Lwetse', 'Diphalane', 'Ngwanatsele', 'Sedimonthole'],
+  SHORTMONTHS: ['Fer', 'Tlh', 'Mop', 'Mor', 'Mot', 'See', 'Phu', 'Pha', 'Lwe', 'Dip', 'Ngw', 'Sed'],
+  WEEKDAYS: ['Tshipi', 'Mosopulogo', 'Labobedi', 'Laboraro', 'Labone', 'Labotlhano', 'Matlhatso'],
+  SHORTWEEKDAYS: ['Tsh', 'Mos', 'Bed', 'Rar', 'Ne', 'Tla', 'Mat'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__to.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__to.js
new file mode 100644
index 0000000..aae5582
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__to.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KM', 'TS'],
+  ERANAMES: ['ki mu\u02bba', 'ta\u02bbu \u02bbo S\u012bs\u016b'],
+  NARROWMONTHS: ['S', 'F', 'M', 'E', 'M', 'S', 'S', 'A', 'S', 'O', 'N', 'T'],
+  MONTHS: ['S\u0101nuali', 'F\u0113pueli', 'Ma\u02bbasi', '\u02bbEpeleli', 'M\u0113', 'Sune', 'Siulai', '\u02bbAokosi', 'S\u0113pitema', '\u02bbOkatopa', 'N\u014dvema', 'Tisema'],
+  SHORTMONTHS: ['S\u0101n', 'F\u0113p', 'Ma\u02bba', '\u02bbEpe', 'M\u0113', 'Sun', 'Siu', '\u02bbAok', 'S\u0113p', '\u02bbOka', 'N\u014dv', 'Tis'],
+  WEEKDAYS: ['S\u0101pate', 'M\u014dnite', 'Tusite', 'Pulelulu', 'Tu\u02bbapulelulu', 'Falaite', 'Tokonaki'],
+  SHORTWEEKDAYS: ['S\u0101p', 'M\u014dn', 'Tus', 'Pul', 'Tu\u02bba', 'Fal', 'Tok'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'P', 'T', 'F', 'T'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['kuata \u02bbuluaki', 'kuata ua', 'kuata tolu', 'kuata f\u0101'],
+  AMPMS: ['HH', 'EA'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd-MM-yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__to_TO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__to_TO.js
new file mode 100644
index 0000000..aae5582
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__to_TO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['KM', 'TS'],
+  ERANAMES: ['ki mu\u02bba', 'ta\u02bbu \u02bbo S\u012bs\u016b'],
+  NARROWMONTHS: ['S', 'F', 'M', 'E', 'M', 'S', 'S', 'A', 'S', 'O', 'N', 'T'],
+  MONTHS: ['S\u0101nuali', 'F\u0113pueli', 'Ma\u02bbasi', '\u02bbEpeleli', 'M\u0113', 'Sune', 'Siulai', '\u02bbAokosi', 'S\u0113pitema', '\u02bbOkatopa', 'N\u014dvema', 'Tisema'],
+  SHORTMONTHS: ['S\u0101n', 'F\u0113p', 'Ma\u02bba', '\u02bbEpe', 'M\u0113', 'Sun', 'Siu', '\u02bbAok', 'S\u0113p', '\u02bbOka', 'N\u014dv', 'Tis'],
+  WEEKDAYS: ['S\u0101pate', 'M\u014dnite', 'Tusite', 'Pulelulu', 'Tu\u02bbapulelulu', 'Falaite', 'Tokonaki'],
+  SHORTWEEKDAYS: ['S\u0101p', 'M\u014dn', 'Tus', 'Pul', 'Tu\u02bba', 'Fal', 'Tok'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'P', 'T', 'F', 'T'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['kuata \u02bbuluaki', 'kuata ua', 'kuata tolu', 'kuata f\u0101'],
+  AMPMS: ['HH', 'EA'],
+  DATEFORMATS: ['EEEE d MMMM y', 'd MMMM y', 'd MMM y', 'dd-MM-yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tr.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tr.js
new file mode 100644
index 0000000..93e065a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tr.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['M\u00d6', 'MS'],
+  ERANAMES: ['Milattan \u00d6nce', 'Milattan Sonra'],
+  NARROWMONTHS: ['O', '\u015e', 'M', 'N', 'M', 'H', 'T', 'A', 'E', 'E', 'K', 'A'],
+  MONTHS: ['Ocak', '\u015eubat', 'Mart', 'Nisan', 'May\u0131s', 'Haziran', 'Temmuz', 'A\u011fustos', 'Eyl\u00fcl', 'Ekim', 'Kas\u0131m', 'Aral\u0131k'],
+  SHORTMONTHS: ['Oca', '\u015eub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'A\u011fu', 'Eyl', 'Eki', 'Kas', 'Ara'],
+  WEEKDAYS: ['Pazar', 'Pazartesi', 'Sal\u0131', '\u00c7ar\u015famba', 'Per\u015fembe', 'Cuma', 'Cumartesi'],
+  SHORTWEEKDAYS: ['Paz', 'Pzt', 'Sal', '\u00c7ar', 'Per', 'Cum', 'Cmt'],
+  NARROWWEEKDAYS: ['P', 'P', 'S', '\u00c7', 'P', 'C', 'C'],
+  SHORTQUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  QUARTERS: ['1. \u00e7eyrek', '2. \u00e7eyrek', '3. \u00e7eyrek', '4. \u00e7eyrek'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['dd MMMM y EEEE', 'dd MMMM y', 'dd MMM y', 'dd MM yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tr_TR.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tr_TR.js
new file mode 100644
index 0000000..93e065a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tr_TR.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['M\u00d6', 'MS'],
+  ERANAMES: ['Milattan \u00d6nce', 'Milattan Sonra'],
+  NARROWMONTHS: ['O', '\u015e', 'M', 'N', 'M', 'H', 'T', 'A', 'E', 'E', 'K', 'A'],
+  MONTHS: ['Ocak', '\u015eubat', 'Mart', 'Nisan', 'May\u0131s', 'Haziran', 'Temmuz', 'A\u011fustos', 'Eyl\u00fcl', 'Ekim', 'Kas\u0131m', 'Aral\u0131k'],
+  SHORTMONTHS: ['Oca', '\u015eub', 'Mar', 'Nis', 'May', 'Haz', 'Tem', 'A\u011fu', 'Eyl', 'Eki', 'Kas', 'Ara'],
+  WEEKDAYS: ['Pazar', 'Pazartesi', 'Sal\u0131', '\u00c7ar\u015famba', 'Per\u015fembe', 'Cuma', 'Cumartesi'],
+  SHORTWEEKDAYS: ['Paz', 'Pzt', 'Sal', '\u00c7ar', 'Per', 'Cum', 'Cmt'],
+  NARROWWEEKDAYS: ['P', 'P', 'S', '\u00c7', 'P', 'C', 'C'],
+  SHORTQUARTERS: ['\u00c71', '\u00c72', '\u00c73', '\u00c74'],
+  QUARTERS: ['1. \u00e7eyrek', '2. \u00e7eyrek', '3. \u00e7eyrek', '4. \u00e7eyrek'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['dd MMMM y EEEE', 'dd MMMM y', 'dd MMM y', 'dd MM yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__trv.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__trv.js
new file mode 100644
index 0000000..25abace
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__trv.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BRY', 'BUY'],
+  ERANAMES: ['Brah jikan Yisu Thulang', 'Bukuy jikan Yisu Thulang'],
+  NARROWMONTHS: ['K', 'D', 'T', 'S', 'R', 'M', 'E', 'P', 'A', 'M', 'K', 'D'],
+  MONTHS: ['Kingal idas', 'Dha idas', 'Tru idas', 'Spat idas', 'Rima idas', 'Mataru idas', 'Empitu idas', 'Maspat idas', 'Mngari idas', 'Maxal idas', 'Maxal kingal idas', 'Maxal dha idas'],
+  SHORTMONTHS: ['Kii', 'Dhi', 'Tri', 'Spi', 'Rii', 'Mti', 'Emi', 'Mai', 'Mni', 'Mxi', 'Mxk', 'Mxd'],
+  WEEKDAYS: ['Jiyax sngayan', 'tgKingal jiyax iyax sngayan', 'tgDha jiyax iyax sngayan', 'tgTru jiyax iyax sngayan', 'tgSpac jiyax iyax sngayan', 'tgRima jiyax iyax sngayan', 'tgMataru jiyax iyax sngayan'],
+  SHORTWEEKDAYS: ['Emp', 'Kin', 'Dha', 'Tru', 'Spa', 'Rim', 'Mat'],
+  NARROWWEEKDAYS: ['E', 'K', 'D', 'T', 'S', 'R', 'M'],
+  SHORTQUARTERS: ['mn1', 'mn2', 'mn3', 'mn4'],
+  QUARTERS: ['mnprxan', 'mndha', 'mntru', 'mnspat'],
+  AMPMS: ['Brax kndaax', 'Baubau kndaax'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__trv_TW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__trv_TW.js
new file mode 100644
index 0000000..20ecd38
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__trv_TW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BRY', 'BUY'],
+  ERANAMES: ['Brah jikan Yisu Thulang', 'Bukuy jikan Yisu Thulang'],
+  NARROWMONTHS: ['K', 'D', 'T', 'S', 'R', 'M', 'E', 'P', 'A', 'M', 'K', 'D'],
+  MONTHS: ['Kingal idas', 'Dha idas', 'Tru idas', 'Spat idas', 'Rima idas', 'Mataru idas', 'Empitu idas', 'Maspat idas', 'Mngari idas', 'Maxal idas', 'Maxal kingal idas', 'Maxal dha idas'],
+  SHORTMONTHS: ['Kii', 'Dhi', 'Tri', 'Spi', 'Rii', 'Mti', 'Emi', 'Mai', 'Mni', 'Mxi', 'Mxk', 'Mxd'],
+  WEEKDAYS: ['Jiyax sngayan', 'tgKingal jiyax iyax sngayan', 'tgDha jiyax iyax sngayan', 'tgTru jiyax iyax sngayan', 'tgSpac jiyax iyax sngayan', 'tgRima jiyax iyax sngayan', 'tgMataru jiyax iyax sngayan'],
+  SHORTWEEKDAYS: ['Emp', 'Kin', 'Dha', 'Tru', 'Spa', 'Rim', 'Mat'],
+  NARROWWEEKDAYS: ['E', 'K', 'D', 'T', 'S', 'R', 'M'],
+  SHORTQUARTERS: ['mn1', 'mn2', 'mn3', 'mn4'],
+  QUARTERS: ['mnprxan', 'mndha', 'mntru', 'mnspat'],
+  AMPMS: ['Brax kndaax', 'Baubau kndaax'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ts.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ts.js
new file mode 100644
index 0000000..5b22fea
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ts.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Sunguti', 'Nyenyenyani', 'Nyenyankulu', 'Dzivamisoko', 'Mudyaxihi', 'Khotavuxika', 'Mawuwani', 'Mhawuri', 'Ndzhati', 'Nhlangula', 'Hukuri', "N'wendzamhala"],
+  SHORTMONTHS: ['Sun', 'Yan', 'Kul', 'Dzi', 'Mud', 'Kho', 'Maw', 'Mha', 'Ndz', 'Nhl', 'Huk', "N'w"],
+  WEEKDAYS: ['Sonto', 'Musumbhunuku', 'Ravumbirhi', 'Ravunharhu', 'Ravumune', 'Ravuntlhanu', 'Mugqivela'],
+  SHORTWEEKDAYS: ['Son', 'Mus', 'Bir', 'Har', 'Ne', 'Tlh', 'Mug'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['Kotara yo sungula', 'Kotara ya vumbirhi', 'Kotara ya vunharhu', 'Kotara ya vumune'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ts_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ts_ZA.js
new file mode 100644
index 0000000..5b22fea
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ts_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Sunguti', 'Nyenyenyani', 'Nyenyankulu', 'Dzivamisoko', 'Mudyaxihi', 'Khotavuxika', 'Mawuwani', 'Mhawuri', 'Ndzhati', 'Nhlangula', 'Hukuri', "N'wendzamhala"],
+  SHORTMONTHS: ['Sun', 'Yan', 'Kul', 'Dzi', 'Mud', 'Kho', 'Maw', 'Mha', 'Ndz', 'Nhl', 'Huk', "N'w"],
+  WEEKDAYS: ['Sonto', 'Musumbhunuku', 'Ravumbirhi', 'Ravunharhu', 'Ravumune', 'Ravuntlhanu', 'Mugqivela'],
+  SHORTWEEKDAYS: ['Son', 'Mus', 'Bir', 'Har', 'Ne', 'Tlh', 'Mug'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['Kotara yo sungula', 'Kotara ya vumbirhi', 'Kotara ya vunharhu', 'Kotara ya vumune'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tt.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tt.js
new file mode 100644
index 0000000..47cfb13
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tt.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['d MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tt_RU.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tt_RU.js
new file mode 100644
index 0000000..47cfb13
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__tt_RU.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['d MMMM y', 'd MMMM y', 'dd.MM.yyyy', 'dd.MM.yyyy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'H:mm:ss z', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug.js
new file mode 100644
index 0000000..68f55f2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_Arab.js
new file mode 100644
index 0000000..01cb174
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_Arab.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_Arab_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_Arab_CN.js
new file mode 100644
index 0000000..68f55f2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_Arab_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_CN.js
new file mode 100644
index 0000000..68f55f2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ug_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uk.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uk.js
new file mode 100644
index 0000000..2110882
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uk.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0434\u043e \u043d.\u0435.', '\u043d.\u0435.'],
+  ERANAMES: ['\u0434\u043e \u043d\u0430\u0448\u043e\u0457 \u0435\u0440\u0438', '\u043d\u0430\u0448\u043e\u0457 \u0435\u0440\u0438'],
+  NARROWMONTHS: ['\u0421', '\u041b', '\u0411', '\u041a', '\u0422', '\u0427', '\u041b', '\u0421', '\u0412', '\u0416', '\u041b', '\u0413'],
+  MONTHS: ['\u0441\u0456\u0447\u043d\u044f', '\u043b\u044e\u0442\u043e\u0433\u043e', '\u0431\u0435\u0440\u0435\u0437\u043d\u044f', '\u043a\u0432\u0456\u0442\u043d\u044f', '\u0442\u0440\u0430\u0432\u043d\u044f', '\u0447\u0435\u0440\u0432\u043d\u044f', '\u043b\u0438\u043f\u043d\u044f', '\u0441\u0435\u0440\u043f\u043d\u044f', '\u0432\u0435\u0440\u0435\u0441\u043d\u044f', '\u0436\u043e\u0432\u0442\u043d\u044f', '\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434\u0430', '\u0433\u0440\u0443\u0434\u043d\u044f'],
+  STANDALONEMONTHS: ['\u0421\u0456\u0447\u0435\u043d\u044c', '\u041b\u044e\u0442\u0438\u0439', '\u0411\u0435\u0440\u0435\u0437\u0435\u043d\u044c', '\u041a\u0432\u0456\u0442\u0435\u043d\u044c', '\u0422\u0440\u0430\u0432\u0435\u043d\u044c', '\u0427\u0435\u0440\u0432\u0435\u043d\u044c', '\u041b\u0438\u043f\u0435\u043d\u044c', '\u0421\u0435\u0440\u043f\u0435\u043d\u044c', '\u0412\u0435\u0440\u0435\u0441\u0435\u043d\u044c', '\u0416\u043e\u0432\u0442\u0435\u043d\u044c', '\u041b\u0438\u0441\u0442\u043e\u043f\u0430\u0434', '\u0413\u0440\u0443\u0434\u0435\u043d\u044c'],
+  SHORTMONTHS: ['\u0441\u0456\u0447.', '\u043b\u044e\u0442.', '\u0431\u0435\u0440.', '\u043a\u0432\u0456\u0442.', '\u0442\u0440\u0430\u0432.', '\u0447\u0435\u0440\u0432.', '\u043b\u0438\u043f.', '\u0441\u0435\u0440\u043f.', '\u0432\u0435\u0440.', '\u0436\u043e\u0432\u0442.', '\u043b\u0438\u0441\u0442.', '\u0433\u0440\u0443\u0434.'],
+  STANDALONESHORTMONTHS: ['\u0421\u0456\u0447', '\u041b\u044e\u0442', '\u0411\u0435\u0440', '\u041a\u0432\u0456', '\u0422\u0440\u0430', '\u0427\u0435\u0440', '\u041b\u0438\u043f', '\u0421\u0435\u0440', '\u0412\u0435\u0440', '\u0416\u043e\u0432', '\u041b\u0438\u0441', '\u0413\u0440\u0443'],
+  WEEKDAYS: ['\u041d\u0435\u0434\u0456\u043b\u044f', '\u041f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a', '\u0412\u0456\u0432\u0442\u043e\u0440\u043e\u043a', '\u0421\u0435\u0440\u0435\u0434\u0430', '\u0427\u0435\u0442\u0432\u0435\u0440', '\u041f\u02bc\u044f\u0442\u043d\u0438\u0446\u044f', '\u0421\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u0434', '\u041f\u043d', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041f\u0442', '\u0421\u0431'],
+  NARROWWEEKDAYS: ['\u041d', '\u041f', '\u0412', '\u0421', '\u0427', '\u041f', '\u0421'],
+  SHORTQUARTERS: ['I \u043a\u0432.', 'II \u043a\u0432.', 'III \u043a\u0432.', 'IV \u043a\u0432.'],
+  QUARTERS: ['I \u043a\u0432\u0430\u0440\u0442\u0430\u043b', 'II \u043a\u0432\u0430\u0440\u0442\u0430\u043b', 'III \u043a\u0432\u0430\u0440\u0442\u0430\u043b', 'IV \u043a\u0432\u0430\u0440\u0442\u0430\u043b'],
+  AMPMS: ['\u0434\u043f', '\u043f\u043f'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0440'.", "d MMMM y '\u0440'.", 'd MMM y', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uk_UA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uk_UA.js
new file mode 100644
index 0000000..2110882
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uk_UA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0434\u043e \u043d.\u0435.', '\u043d.\u0435.'],
+  ERANAMES: ['\u0434\u043e \u043d\u0430\u0448\u043e\u0457 \u0435\u0440\u0438', '\u043d\u0430\u0448\u043e\u0457 \u0435\u0440\u0438'],
+  NARROWMONTHS: ['\u0421', '\u041b', '\u0411', '\u041a', '\u0422', '\u0427', '\u041b', '\u0421', '\u0412', '\u0416', '\u041b', '\u0413'],
+  MONTHS: ['\u0441\u0456\u0447\u043d\u044f', '\u043b\u044e\u0442\u043e\u0433\u043e', '\u0431\u0435\u0440\u0435\u0437\u043d\u044f', '\u043a\u0432\u0456\u0442\u043d\u044f', '\u0442\u0440\u0430\u0432\u043d\u044f', '\u0447\u0435\u0440\u0432\u043d\u044f', '\u043b\u0438\u043f\u043d\u044f', '\u0441\u0435\u0440\u043f\u043d\u044f', '\u0432\u0435\u0440\u0435\u0441\u043d\u044f', '\u0436\u043e\u0432\u0442\u043d\u044f', '\u043b\u0438\u0441\u0442\u043e\u043f\u0430\u0434\u0430', '\u0433\u0440\u0443\u0434\u043d\u044f'],
+  STANDALONEMONTHS: ['\u0421\u0456\u0447\u0435\u043d\u044c', '\u041b\u044e\u0442\u0438\u0439', '\u0411\u0435\u0440\u0435\u0437\u0435\u043d\u044c', '\u041a\u0432\u0456\u0442\u0435\u043d\u044c', '\u0422\u0440\u0430\u0432\u0435\u043d\u044c', '\u0427\u0435\u0440\u0432\u0435\u043d\u044c', '\u041b\u0438\u043f\u0435\u043d\u044c', '\u0421\u0435\u0440\u043f\u0435\u043d\u044c', '\u0412\u0435\u0440\u0435\u0441\u0435\u043d\u044c', '\u0416\u043e\u0432\u0442\u0435\u043d\u044c', '\u041b\u0438\u0441\u0442\u043e\u043f\u0430\u0434', '\u0413\u0440\u0443\u0434\u0435\u043d\u044c'],
+  SHORTMONTHS: ['\u0441\u0456\u0447.', '\u043b\u044e\u0442.', '\u0431\u0435\u0440.', '\u043a\u0432\u0456\u0442.', '\u0442\u0440\u0430\u0432.', '\u0447\u0435\u0440\u0432.', '\u043b\u0438\u043f.', '\u0441\u0435\u0440\u043f.', '\u0432\u0435\u0440.', '\u0436\u043e\u0432\u0442.', '\u043b\u0438\u0441\u0442.', '\u0433\u0440\u0443\u0434.'],
+  STANDALONESHORTMONTHS: ['\u0421\u0456\u0447', '\u041b\u044e\u0442', '\u0411\u0435\u0440', '\u041a\u0432\u0456', '\u0422\u0440\u0430', '\u0427\u0435\u0440', '\u041b\u0438\u043f', '\u0421\u0435\u0440', '\u0412\u0435\u0440', '\u0416\u043e\u0432', '\u041b\u0438\u0441', '\u0413\u0440\u0443'],
+  WEEKDAYS: ['\u041d\u0435\u0434\u0456\u043b\u044f', '\u041f\u043e\u043d\u0435\u0434\u0456\u043b\u043e\u043a', '\u0412\u0456\u0432\u0442\u043e\u0440\u043e\u043a', '\u0421\u0435\u0440\u0435\u0434\u0430', '\u0427\u0435\u0442\u0432\u0435\u0440', '\u041f\u02bc\u044f\u0442\u043d\u0438\u0446\u044f', '\u0421\u0443\u0431\u043e\u0442\u0430'],
+  SHORTWEEKDAYS: ['\u041d\u0434', '\u041f\u043d', '\u0412\u0442', '\u0421\u0440', '\u0427\u0442', '\u041f\u0442', '\u0421\u0431'],
+  NARROWWEEKDAYS: ['\u041d', '\u041f', '\u0412', '\u0421', '\u0427', '\u041f', '\u0421'],
+  SHORTQUARTERS: ['I \u043a\u0432.', 'II \u043a\u0432.', 'III \u043a\u0432.', 'IV \u043a\u0432.'],
+  QUARTERS: ['I \u043a\u0432\u0430\u0440\u0442\u0430\u043b', 'II \u043a\u0432\u0430\u0440\u0442\u0430\u043b', 'III \u043a\u0432\u0430\u0440\u0442\u0430\u043b', 'IV \u043a\u0432\u0430\u0440\u0442\u0430\u043b'],
+  AMPMS: ['\u0434\u043f', '\u043f\u043f'],
+  DATEFORMATS: ["EEEE, d MMMM y '\u0440'.", "d MMMM y '\u0440'.", 'd MMM y', 'dd.MM.yy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur.js
new file mode 100644
index 0000000..5d78093
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642 \u0645', '\u0639\u064a\u0633\u0648\u06cc \u0633\u0646'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0645\u0633\u064a\u062d', '\u0639\u064a\u0633\u0648\u06cc \u0633\u0646'],
+  NARROWMONTHS: ['\u062c', '\u0641', '\u0645', '\u0627', '\u0645', '\u062c', '\u062c', '\u0627', '\u0633', '\u0627', '\u0646', '\u062f'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631 \u0686', '\u0627\u067e\u0631\u064a\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631 \u0686', '\u0627\u067e\u0631\u064a\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u064a\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u062f\u0647', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  SHORTWEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u064a\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u062f\u0647', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  NARROWWEEKDAYS: ['\u0627', '\u067e', '\u0645', '\u0628', '\u062c', '\u062c', '\u06c1'],
+  STANDALONENARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '2\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '3\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '4\u0633\u06c1 \u0645\u0627\u06c1\u06cc'],
+  QUARTERS: ['\u067e\u06c1\u0644\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u062f\u0648\u0633\u0631\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u062a\u064a\u0633\u0631\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u0686\u0648\u062a\u0647\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc'],
+  AMPMS: ['\u0642\u0628\u0644 \u062f\u0648\u067e\u06c1\u0631', '\u0628\u0639\u062f \u062f\u0648\u067e\u06c1\u0631'],
+  DATEFORMATS: ['EEEE, d, MMMM y', 'd, MMMM y', 'd, MMM y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur_IN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur_IN.js
new file mode 100644
index 0000000..221a326
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur_IN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642 \u0645', '\u0639\u064a\u0633\u0648\u06cc \u0633\u0646'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0645\u0633\u064a\u062d', '\u0639\u064a\u0633\u0648\u06cc \u0633\u0646'],
+  NARROWMONTHS: ['\u062c', '\u0641', '\u0645', '\u0627', '\u0645', '\u062c', '\u062c', '\u0627', '\u0633', '\u0627', '\u0646', '\u062f'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631 \u0686', '\u0627\u067e\u0631\u064a\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631 \u0686', '\u0627\u067e\u0631\u064a\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u064a\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u062f\u0647', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  SHORTWEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u064a\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u062f\u0647', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  NARROWWEEKDAYS: ['\u0627', '\u067e', '\u0645', '\u0628', '\u062c', '\u062c', '\u06c1'],
+  STANDALONENARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '2\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '3\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '4\u0633\u06c1 \u0645\u0627\u06c1\u06cc'],
+  QUARTERS: ['\u067e\u06c1\u0644\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u062f\u0648\u0633\u0631\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u062a\u064a\u0633\u0631\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u0686\u0648\u062a\u0647\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc'],
+  AMPMS: ['\u0642\u0628\u0644 \u062f\u0648\u067e\u06c1\u0631', '\u0628\u0639\u062f \u062f\u0648\u067e\u06c1\u0631'],
+  DATEFORMATS: ['EEEE, d, MMMM y', 'd, MMMM y', 'd, MMM y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [6, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur_PK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur_PK.js
new file mode 100644
index 0000000..5d78093
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ur_PK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642 \u0645', '\u0639\u064a\u0633\u0648\u06cc \u0633\u0646'],
+  ERANAMES: ['\u0642\u0628\u0644 \u0645\u0633\u064a\u062d', '\u0639\u064a\u0633\u0648\u06cc \u0633\u0646'],
+  NARROWMONTHS: ['\u062c', '\u0641', '\u0645', '\u0627', '\u0645', '\u062c', '\u062c', '\u0627', '\u0633', '\u0627', '\u0646', '\u062f'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631 \u0686', '\u0627\u067e\u0631\u064a\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631 \u0686', '\u0627\u067e\u0631\u064a\u0644', '\u0645\u0626', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u0626', '\u0627\u06af\u0633\u062a', '\u0633\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  WEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u064a\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u062f\u0647', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  SHORTWEEKDAYS: ['\u0627\u062a\u0648\u0627\u0631', '\u067e\u064a\u0631', '\u0645\u0646\u06af\u0644', '\u0628\u062f\u0647', '\u062c\u0645\u0639\u0631\u0627\u062a', '\u062c\u0645\u0639\u06c1', '\u06c1\u0641\u062a\u06c1'],
+  NARROWWEEKDAYS: ['\u0627', '\u067e', '\u0645', '\u0628', '\u062c', '\u062c', '\u06c1'],
+  STANDALONENARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['1\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '2\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '3\u0633\u06c1 \u0645\u0627\u06c1\u06cc', '4\u0633\u06c1 \u0645\u0627\u06c1\u06cc'],
+  QUARTERS: ['\u067e\u06c1\u0644\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u062f\u0648\u0633\u0631\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u062a\u064a\u0633\u0631\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc', '\u0686\u0648\u062a\u0647\u06cc \u0633\u06c1 \u0645\u0627\u06c1\u06cc'],
+  AMPMS: ['\u0642\u0628\u0644 \u062f\u0648\u067e\u06c1\u0631', '\u0628\u0639\u062f \u062f\u0648\u067e\u06c1\u0631'],
+  DATEFORMATS: ['EEEE, d, MMMM y', 'd, MMMM y', 'd, MMM y', 'd/M/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz.js
new file mode 100644
index 0000000..dcb7bb7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u041c\u0443\u04b3\u0430\u0440\u0440\u0430\u043c', '\u0421\u0430\u0444\u0430\u0440', '\u0420\u0430\u0431\u0438\u0443\u043b-\u0430\u0432\u0432\u0430\u043b', '\u0420\u0430\u0431\u0438\u0443\u043b-\u043e\u0445\u0438\u0440', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u043b\u043e', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u0445\u0440\u043e', '\u0420\u0430\u0436\u0430\u0431', '\u0428\u0430\u044a\u0431\u043e\u043d', '\u0420\u0430\u043c\u0430\u0437\u043e\u043d', '\u0428\u0430\u0432\u0432\u043e\u043b', '\u0417\u0438\u043b-\u049b\u0430\u044a\u0434\u0430', '\u0417\u0438\u043b-\u04b3\u0438\u0436\u0436\u0430'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u044f\u043a\u0448\u0430\u043d\u0431\u0430', '\u0434\u0443\u0448\u0430\u043d\u0431\u0430', '\u0441\u0435\u0448\u0430\u043d\u0431\u0430', '\u0447\u043e\u0440\u0448\u0430\u043d\u0431\u0430', '\u043f\u0430\u0439\u0448\u0430\u043d\u0431\u0430', '\u0436\u0443\u043c\u0430', '\u0448\u0430\u043d\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u042f\u043a\u0448', '\u0414\u0443\u0448', '\u0421\u0435\u0448', '\u0427\u043e\u0440', '\u041f\u0430\u0439', '\u0416\u0443\u043c', '\u0428\u0430\u043d'],
+  NARROWWEEKDAYS: ['\u042f', '\u0414', '\u0421', '\u0427', '\u041f', '\u0416', '\u0428'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_AF.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_AF.js
new file mode 100644
index 0000000..b56570a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_AF.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642.\u0645.', '\u0645.'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0628\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u067e\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648', '\u0641\u0628\u0631', '\u0645\u0627\u0631', '\u0627\u067e\u0631', '\u0645\u0640\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644', '\u0627\u06af\u0633', '\u0633\u067e\u062a', '\u0627\u06a9\u062a', '\u0646\u0648\u0645', '\u062f\u0633\u0645'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['\u06cc.', '\u062f.', '\u0633.', '\u0686.', '\u067e.', '\u062c.', '\u0634.'],
+  NARROWWEEKDAYS: ['\u042f', '\u0414', '\u0421', '\u0427', '\u041f', '\u0416', '\u0428'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['y \u0646\u0686\u06cc \u06cc\u06cc\u0644 d \u0646\u0686\u06cc MMMM EEEE \u06a9\u0648\u0646\u06cc', 'd \u0646\u0686\u06cc MMMM y', 'd MMM y', 'yyyy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Arab.js
new file mode 100644
index 0000000..fd388a4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Arab.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642.\u0645.', '\u0645.'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0628\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u067e\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648', '\u0641\u0628\u0631', '\u0645\u0627\u0631', '\u0627\u067e\u0631', '\u0645\u0640\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644', '\u0627\u06af\u0633', '\u0633\u067e\u062a', '\u0627\u06a9\u062a', '\u0646\u0648\u0645', '\u062f\u0633\u0645'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['\u06cc.', '\u062f.', '\u0633.', '\u0686.', '\u067e.', '\u062c.', '\u0634.'],
+  NARROWWEEKDAYS: ['\u042f', '\u0414', '\u0421', '\u0427', '\u041f', '\u0416', '\u0428'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['y \u0646\u0686\u06cc \u06cc\u06cc\u0644 d \u0646\u0686\u06cc MMMM EEEE \u06a9\u0648\u0646\u06cc', 'd \u0646\u0686\u06cc MMMM y', 'd MMM y', 'yyyy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Arab_AF.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Arab_AF.js
new file mode 100644
index 0000000..b56570a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Arab_AF.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u0642.\u0645.', '\u0645.'],
+  ERANAMES: ['\u0642.\u0645.', '\u0645.'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u062c\u0646\u0648\u0631\u06cc', '\u0641\u0628\u0631\u0648\u0631\u06cc', '\u0645\u0627\u0631\u0686', '\u0627\u067e\u0631\u06cc\u0644', '\u0645\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644\u0627\u06cc', '\u0627\u06af\u0633\u062a', '\u0633\u067e\u062a\u0645\u0628\u0631', '\u0627\u06a9\u062a\u0648\u0628\u0631', '\u0646\u0648\u0645\u0628\u0631', '\u062f\u0633\u0645\u0628\u0631'],
+  SHORTMONTHS: ['\u062c\u0646\u0648', '\u0641\u0628\u0631', '\u0645\u0627\u0631', '\u0627\u067e\u0631', '\u0645\u0640\u06cc', '\u062c\u0648\u0646', '\u062c\u0648\u0644', '\u0627\u06af\u0633', '\u0633\u067e\u062a', '\u0627\u06a9\u062a', '\u0646\u0648\u0645', '\u062f\u0633\u0645'],
+  WEEKDAYS: ['\u06cc\u06a9\u0634\u0646\u0628\u0647', '\u062f\u0648\u0634\u0646\u0628\u0647', '\u0633\u0647\u200c\u0634\u0646\u0628\u0647', '\u0686\u0647\u0627\u0631\u0634\u0646\u0628\u0647', '\u067e\u0646\u062c\u0634\u0646\u0628\u0647', '\u062c\u0645\u0639\u0647', '\u0634\u0646\u0628\u0647'],
+  SHORTWEEKDAYS: ['\u06cc.', '\u062f.', '\u0633.', '\u0686.', '\u067e.', '\u062c.', '\u0634.'],
+  NARROWWEEKDAYS: ['\u042f', '\u0414', '\u0421', '\u0427', '\u041f', '\u0416', '\u0428'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['y \u0646\u0686\u06cc \u06cc\u06cc\u0644 d \u0646\u0686\u06cc MMMM EEEE \u06a9\u0648\u0646\u06cc', 'd \u0646\u0686\u06cc MMMM y', 'd MMM y', 'yyyy/M/d'],
+  TIMEFORMATS: ['H:mm:ss (zzzz)', 'H:mm:ss (z)', 'H:mm:ss', 'H:mm'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [3, 4],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Cyrl.js
new file mode 100644
index 0000000..dcb7bb7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Cyrl.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u041c\u0443\u04b3\u0430\u0440\u0440\u0430\u043c', '\u0421\u0430\u0444\u0430\u0440', '\u0420\u0430\u0431\u0438\u0443\u043b-\u0430\u0432\u0432\u0430\u043b', '\u0420\u0430\u0431\u0438\u0443\u043b-\u043e\u0445\u0438\u0440', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u043b\u043e', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u0445\u0440\u043e', '\u0420\u0430\u0436\u0430\u0431', '\u0428\u0430\u044a\u0431\u043e\u043d', '\u0420\u0430\u043c\u0430\u0437\u043e\u043d', '\u0428\u0430\u0432\u0432\u043e\u043b', '\u0417\u0438\u043b-\u049b\u0430\u044a\u0434\u0430', '\u0417\u0438\u043b-\u04b3\u0438\u0436\u0436\u0430'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u044f\u043a\u0448\u0430\u043d\u0431\u0430', '\u0434\u0443\u0448\u0430\u043d\u0431\u0430', '\u0441\u0435\u0448\u0430\u043d\u0431\u0430', '\u0447\u043e\u0440\u0448\u0430\u043d\u0431\u0430', '\u043f\u0430\u0439\u0448\u0430\u043d\u0431\u0430', '\u0436\u0443\u043c\u0430', '\u0448\u0430\u043d\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u042f\u043a\u0448', '\u0414\u0443\u0448', '\u0421\u0435\u0448', '\u0427\u043e\u0440', '\u041f\u0430\u0439', '\u0416\u0443\u043c', '\u0428\u0430\u043d'],
+  NARROWWEEKDAYS: ['\u042f', '\u0414', '\u0421', '\u0427', '\u041f', '\u0416', '\u0428'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Cyrl_UZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Cyrl_UZ.js
new file mode 100644
index 0000000..dcb7bb7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Cyrl_UZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u041c\u0443\u04b3\u0430\u0440\u0440\u0430\u043c', '\u0421\u0430\u0444\u0430\u0440', '\u0420\u0430\u0431\u0438\u0443\u043b-\u0430\u0432\u0432\u0430\u043b', '\u0420\u0430\u0431\u0438\u0443\u043b-\u043e\u0445\u0438\u0440', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u043b\u043e', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u0445\u0440\u043e', '\u0420\u0430\u0436\u0430\u0431', '\u0428\u0430\u044a\u0431\u043e\u043d', '\u0420\u0430\u043c\u0430\u0437\u043e\u043d', '\u0428\u0430\u0432\u0432\u043e\u043b', '\u0417\u0438\u043b-\u049b\u0430\u044a\u0434\u0430', '\u0417\u0438\u043b-\u04b3\u0438\u0436\u0436\u0430'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u044f\u043a\u0448\u0430\u043d\u0431\u0430', '\u0434\u0443\u0448\u0430\u043d\u0431\u0430', '\u0441\u0435\u0448\u0430\u043d\u0431\u0430', '\u0447\u043e\u0440\u0448\u0430\u043d\u0431\u0430', '\u043f\u0430\u0439\u0448\u0430\u043d\u0431\u0430', '\u0436\u0443\u043c\u0430', '\u0448\u0430\u043d\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u042f\u043a\u0448', '\u0414\u0443\u0448', '\u0421\u0435\u0448', '\u0427\u043e\u0440', '\u041f\u0430\u0439', '\u0416\u0443\u043c', '\u0428\u0430\u043d'],
+  NARROWWEEKDAYS: ['\u042f', '\u0414', '\u0421', '\u0427', '\u041f', '\u0416', '\u0428'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Latn.js
new file mode 100644
index 0000000..dd78b6e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Latn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['Y', 'F', 'M', 'A', 'M', 'I', 'I', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['\u041c\u0443\u04b3\u0430\u0440\u0440\u0430\u043c', '\u0421\u0430\u0444\u0430\u0440', '\u0420\u0430\u0431\u0438\u0443\u043b-\u0430\u0432\u0432\u0430\u043b', '\u0420\u0430\u0431\u0438\u0443\u043b-\u043e\u0445\u0438\u0440', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u043b\u043e', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u0445\u0440\u043e', '\u0420\u0430\u0436\u0430\u0431', '\u0428\u0430\u044a\u0431\u043e\u043d', '\u0420\u0430\u043c\u0430\u0437\u043e\u043d', '\u0428\u0430\u0432\u0432\u043e\u043b', '\u0417\u0438\u043b-\u049b\u0430\u044a\u0434\u0430', '\u0417\u0438\u043b-\u04b3\u0438\u0436\u0436\u0430'],
+  SHORTMONTHS: ['Yanv', 'Fev', 'Mar', 'Apr', 'May', 'Iyun', 'Iyul', 'Avg', 'Sen', 'Okt', 'Noya', 'Dek'],
+  WEEKDAYS: ['yakshanba', 'dushanba', 'seshanba', 'chorshanba', 'payshanba', 'juma', 'shanba'],
+  SHORTWEEKDAYS: ['Yaksh', 'Dush', 'Sesh', 'Chor', 'Pay', 'Jum', 'Shan'],
+  NARROWWEEKDAYS: ['Y', 'D', 'S', 'C', 'P', 'J', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Latn_UZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Latn_UZ.js
new file mode 100644
index 0000000..dd78b6e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_Latn_UZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['Y', 'F', 'M', 'A', 'M', 'I', 'I', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['\u041c\u0443\u04b3\u0430\u0440\u0440\u0430\u043c', '\u0421\u0430\u0444\u0430\u0440', '\u0420\u0430\u0431\u0438\u0443\u043b-\u0430\u0432\u0432\u0430\u043b', '\u0420\u0430\u0431\u0438\u0443\u043b-\u043e\u0445\u0438\u0440', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u043b\u043e', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u0445\u0440\u043e', '\u0420\u0430\u0436\u0430\u0431', '\u0428\u0430\u044a\u0431\u043e\u043d', '\u0420\u0430\u043c\u0430\u0437\u043e\u043d', '\u0428\u0430\u0432\u0432\u043e\u043b', '\u0417\u0438\u043b-\u049b\u0430\u044a\u0434\u0430', '\u0417\u0438\u043b-\u04b3\u0438\u0436\u0436\u0430'],
+  SHORTMONTHS: ['Yanv', 'Fev', 'Mar', 'Apr', 'May', 'Iyun', 'Iyul', 'Avg', 'Sen', 'Okt', 'Noya', 'Dek'],
+  WEEKDAYS: ['yakshanba', 'dushanba', 'seshanba', 'chorshanba', 'payshanba', 'juma', 'shanba'],
+  SHORTWEEKDAYS: ['Yaksh', 'Dush', 'Sesh', 'Chor', 'Pay', 'Jum', 'Shan'],
+  NARROWWEEKDAYS: ['Y', 'D', 'S', 'C', 'P', 'J', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_UZ.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_UZ.js
new file mode 100644
index 0000000..dcb7bb7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__uz_UZ.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['\u042f', '\u0424', '\u041c', '\u0410', '\u041c', '\u0418', '\u0418', '\u0410', '\u0421', '\u041e', '\u041d', '\u0414'],
+  MONTHS: ['\u041c\u0443\u04b3\u0430\u0440\u0440\u0430\u043c', '\u0421\u0430\u0444\u0430\u0440', '\u0420\u0430\u0431\u0438\u0443\u043b-\u0430\u0432\u0432\u0430\u043b', '\u0420\u0430\u0431\u0438\u0443\u043b-\u043e\u0445\u0438\u0440', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u043b\u043e', '\u0416\u0443\u043c\u043e\u0434\u0438\u0443\u043b-\u0443\u0445\u0440\u043e', '\u0420\u0430\u0436\u0430\u0431', '\u0428\u0430\u044a\u0431\u043e\u043d', '\u0420\u0430\u043c\u0430\u0437\u043e\u043d', '\u0428\u0430\u0432\u0432\u043e\u043b', '\u0417\u0438\u043b-\u049b\u0430\u044a\u0434\u0430', '\u0417\u0438\u043b-\u04b3\u0438\u0436\u0436\u0430'],
+  SHORTMONTHS: ['\u042f\u043d\u0432', '\u0424\u0435\u0432', '\u041c\u0430\u0440', '\u0410\u043f\u0440', '\u041c\u0430\u0439', '\u0418\u044e\u043d', '\u0418\u044e\u043b', '\u0410\u0432\u0433', '\u0421\u0435\u043d', '\u041e\u043a\u0442', '\u041d\u043e\u044f', '\u0414\u0435\u043a'],
+  WEEKDAYS: ['\u044f\u043a\u0448\u0430\u043d\u0431\u0430', '\u0434\u0443\u0448\u0430\u043d\u0431\u0430', '\u0441\u0435\u0448\u0430\u043d\u0431\u0430', '\u0447\u043e\u0440\u0448\u0430\u043d\u0431\u0430', '\u043f\u0430\u0439\u0448\u0430\u043d\u0431\u0430', '\u0436\u0443\u043c\u0430', '\u0448\u0430\u043d\u0431\u0430'],
+  SHORTWEEKDAYS: ['\u042f\u043a\u0448', '\u0414\u0443\u0448', '\u0421\u0435\u0448', '\u0427\u043e\u0440', '\u041f\u0430\u0439', '\u0416\u0443\u043c', '\u0428\u0430\u043d'],
+  NARROWWEEKDAYS: ['\u042f', '\u0414', '\u0421', '\u0427', '\u041f', '\u0416', '\u0428'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ve.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ve.js
new file mode 100644
index 0000000..62a5ba1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ve.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Phando', 'Luhuhi', '\u1e70hafamuhwe', 'Lambamai', 'Shundunthule', 'Fulwi', 'Fulwana', '\u1e70hangule', 'Khubvumedzi', 'Tshimedzi', '\u1e3cara', 'Nyendavhusiku'],
+  SHORTMONTHS: ['Pha', 'Luh', '\u1e70ha', 'Lam', 'Shu', 'Lwi', 'Lwa', '\u1e70ha', 'Khu', 'Tsh', '\u1e3car', 'Nye'],
+  WEEKDAYS: ['Swondaha', 'Musumbuluwo', '\u1e3cavhuvhili', '\u1e3cavhuraru', '\u1e3cavhu\u1e4ba', '\u1e3cavhu\u1e71anu', 'Mugivhela'],
+  SHORTWEEKDAYS: ['Swo', 'Mus', 'Vhi', 'Rar', '\u1e4aa', '\u1e70an', 'Mug'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['Kotara ya u thoma', 'Kotara ya vhuvhili', 'Kotara ya vhuraru', 'Kotara ya vhu\u1e4ba'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ve_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ve_ZA.js
new file mode 100644
index 0000000..62a5ba1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__ve_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Phando', 'Luhuhi', '\u1e70hafamuhwe', 'Lambamai', 'Shundunthule', 'Fulwi', 'Fulwana', '\u1e70hangule', 'Khubvumedzi', 'Tshimedzi', '\u1e3cara', 'Nyendavhusiku'],
+  SHORTMONTHS: ['Pha', 'Luh', '\u1e70ha', 'Lam', 'Shu', 'Lwi', 'Lwa', '\u1e70ha', 'Khu', 'Tsh', '\u1e3car', 'Nye'],
+  WEEKDAYS: ['Swondaha', 'Musumbuluwo', '\u1e3cavhuvhili', '\u1e3cavhuraru', '\u1e3cavhu\u1e4ba', '\u1e3cavhu\u1e71anu', 'Mugivhela'],
+  SHORTWEEKDAYS: ['Swo', 'Mus', 'Vhi', 'Rar', '\u1e4aa', '\u1e70an', 'Mug'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['K1', 'K2', 'K3', 'K4'],
+  QUARTERS: ['Kotara ya u thoma', 'Kotara ya vhuvhili', 'Kotara ya vhuraru', 'Kotara ya vhu\u1e4ba'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yy/MM/dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__vi.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__vi.js
new file mode 100644
index 0000000..2c2905e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__vi.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['tr. CN', 'sau CN'],
+  ERANAMES: ['tr. CN', 'sau CN'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['th\u00e1ng m\u1ed9t', 'th\u00e1ng hai', 'th\u00e1ng ba', 'th\u00e1ng t\u01b0', 'th\u00e1ng n\u0103m', 'th\u00e1ng s\u00e1u', 'th\u00e1ng b\u1ea3y', 'th\u00e1ng t\u00e1m', 'th\u00e1ng ch\u00edn', 'th\u00e1ng m\u01b0\u1eddi', 'th\u00e1ng m\u01b0\u1eddi m\u1ed9t', 'th\u00e1ng m\u01b0\u1eddi hai'],
+  SHORTMONTHS: ['thg 1', 'thg 2', 'thg 3', 'thg 4', 'thg 5', 'thg 6', 'thg 7', 'thg 8', 'thg 9', 'thg 10', 'thg 11', 'thg 12'],
+  WEEKDAYS: ['Ch\u1ee7 nh\u1eadt', 'Th\u1ee9 hai', 'Th\u1ee9 ba', 'Th\u1ee9 t\u01b0', 'Th\u1ee9 n\u0103m', 'Th\u1ee9 s\u00e1u', 'Th\u1ee9 b\u1ea3y'],
+  SHORTWEEKDAYS: ['CN', 'Th 2', 'Th 3', 'Th 4', 'Th 5', 'Th 6', 'Th 7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['SA', 'CH'],
+  DATEFORMATS: ["EEEE, 'ng\u00e0y' dd MMMM 'n\u0103m' y", "'Ng\u00e0y' dd 'th\u00e1ng' M 'n\u0103m' y", 'dd-MM-yyyy', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__vi_VN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__vi_VN.js
new file mode 100644
index 0000000..2c2905e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__vi_VN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['tr. CN', 'sau CN'],
+  ERANAMES: ['tr. CN', 'sau CN'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['th\u00e1ng m\u1ed9t', 'th\u00e1ng hai', 'th\u00e1ng ba', 'th\u00e1ng t\u01b0', 'th\u00e1ng n\u0103m', 'th\u00e1ng s\u00e1u', 'th\u00e1ng b\u1ea3y', 'th\u00e1ng t\u00e1m', 'th\u00e1ng ch\u00edn', 'th\u00e1ng m\u01b0\u1eddi', 'th\u00e1ng m\u01b0\u1eddi m\u1ed9t', 'th\u00e1ng m\u01b0\u1eddi hai'],
+  SHORTMONTHS: ['thg 1', 'thg 2', 'thg 3', 'thg 4', 'thg 5', 'thg 6', 'thg 7', 'thg 8', 'thg 9', 'thg 10', 'thg 11', 'thg 12'],
+  WEEKDAYS: ['Ch\u1ee7 nh\u1eadt', 'Th\u1ee9 hai', 'Th\u1ee9 ba', 'Th\u1ee9 t\u01b0', 'Th\u1ee9 n\u0103m', 'Th\u1ee9 s\u00e1u', 'Th\u1ee9 b\u1ea3y'],
+  SHORTWEEKDAYS: ['CN', 'Th 2', 'Th 3', 'Th 4', 'Th 5', 'Th 6', 'Th 7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['SA', 'CH'],
+  DATEFORMATS: ["EEEE, 'ng\u00e0y' dd MMMM 'n\u0103m' y", "'Ng\u00e0y' dd 'th\u00e1ng' M 'n\u0103m' y", 'dd-MM-yyyy', 'dd/MM/yyyy'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wal.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wal.js
new file mode 100644
index 0000000..0f10bb4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wal.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12a0\u12f3 \u12ce\u12f4', '\u130d\u122e\u1270\u1273 \u120b\u12ed\u1273'],
+  ERANAMES: ['\u12a0\u12f3 \u12ce\u12f4', '\u130d\u122e\u1270\u1273 \u120b\u12ed\u1273'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u12c8\u130b', '\u1233\u12ed\u1296', '\u121b\u1246\u1233\u129b', '\u12a0\u1229\u12cb', '\u1203\u1219\u1233', '\u12a0\u122d\u1263', '\u1244\u122b'],
+  SHORTWEEKDAYS: ['\u12c8\u130b', '\u1233\u12ed\u1296', '\u121b\u1246\u1233\u129b', '\u12a0\u1229\u12cb', '\u1203\u1219\u1233', '\u12a0\u122d\u1263', '\u1244\u122b'],
+  NARROWWEEKDAYS: ['\u12c8', '\u1233', '\u121b', '\u12a0', '\u1203', '\u12a0', '\u1244'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u121b\u1208\u12f6', '\u1243\u121b'],
+  DATEFORMATS: ['EEEE\u1365 dd MMMM \u130b\u120b\u1233 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wal_ET.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wal_ET.js
new file mode 100644
index 0000000..1bfb440
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wal_ET.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u12a0\u12f3 \u12ce\u12f4', '\u130d\u122e\u1270\u1273 \u120b\u12ed\u1273'],
+  ERANAMES: ['\u12a0\u12f3 \u12ce\u12f4', '\u130d\u122e\u1270\u1273 \u120b\u12ed\u1273'],
+  NARROWMONTHS: ['\u1303', '\u134c', '\u121b', '\u12a4', '\u121c', '\u1301', '\u1301', '\u12a6', '\u1234', '\u12a6', '\u1296', '\u12f2'],
+  MONTHS: ['\u1303\u1295\u12e9\u12c8\u122a', '\u134c\u1265\u1229\u12c8\u122a', '\u121b\u122d\u127d', '\u12a4\u1355\u1228\u120d', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235\u1275', '\u1234\u1355\u1274\u121d\u1260\u122d', '\u12a6\u12ad\u1270\u12cd\u1260\u122d', '\u1296\u126c\u121d\u1260\u122d', '\u12f2\u1234\u121d\u1260\u122d'],
+  SHORTMONTHS: ['\u1303\u1295\u12e9', '\u134c\u1265\u1229', '\u121b\u122d\u127d', '\u12a4\u1355\u1228', '\u121c\u12ed', '\u1301\u1295', '\u1301\u120b\u12ed', '\u12a6\u1308\u1235', '\u1234\u1355\u1274', '\u12a6\u12ad\u1270', '\u1296\u126c\u121d', '\u12f2\u1234\u121d'],
+  WEEKDAYS: ['\u12c8\u130b', '\u1233\u12ed\u1296', '\u121b\u1246\u1233\u129b', '\u12a0\u1229\u12cb', '\u1203\u1219\u1233', '\u12a0\u122d\u1263', '\u1244\u122b'],
+  SHORTWEEKDAYS: ['\u12c8\u130b', '\u1233\u12ed\u1296', '\u121b\u1246\u1233\u129b', '\u12a0\u1229\u12cb', '\u1203\u1219\u1233', '\u12a0\u122d\u1263', '\u1244\u122b'],
+  NARROWWEEKDAYS: ['\u12c8', '\u1233', '\u121b', '\u12a0', '\u1203', '\u12a0', '\u1244'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u121b\u1208\u12f6', '\u1243\u121b'],
+  DATEFORMATS: ['EEEE\u1365 dd MMMM \u130b\u120b\u1233 y G', 'dd MMMM y', 'dd-MMM-y', 'dd/MM/yy'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 5,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 1
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo.js
new file mode 100644
index 0000000..01cb174
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_Latn.js
new file mode 100644
index 0000000..01cb174
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_Latn.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_Latn_SN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_Latn_SN.js
new file mode 100644
index 0000000..01cb174
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_Latn_SN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_SN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_SN.js
new file mode 100644
index 0000000..01cb174
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__wo_SN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BCE', 'CE'],
+  ERANAMES: ['BCE', 'CE'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  SHORTMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  WEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__xh.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__xh.js
new file mode 100644
index 0000000..b98d6ae
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__xh.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'umnyaka wokuzalwa kukaYesu'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Janyuwari', 'Februwari', 'Matshi', 'Epreli', 'Meyi', 'Juni', 'Julayi', 'Agasti', 'Septemba', 'Okthoba', 'Novemba', 'Disemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mat', 'Epr', 'Mey', 'Jun', 'Jul', 'Aga', 'Sep', 'Okt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Cawe', 'Mvulo', 'Lwesibini', 'Lwesithathu', 'Lwesine', 'Lwesihlanu', 'Mgqibelo'],
+  SHORTWEEKDAYS: ['Caw', 'Mvu', 'Bin', 'Tha', 'Sin', 'Hla', 'Mgq'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1 unyangantathu', '2 unyangantathu', '3 unyangantathu', '4 unyangantathu'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__xh_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__xh_ZA.js
new file mode 100644
index 0000000..b98d6ae
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__xh_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'umnyaka wokuzalwa kukaYesu'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['Janyuwari', 'Februwari', 'Matshi', 'Epreli', 'Meyi', 'Juni', 'Julayi', 'Agasti', 'Septemba', 'Okthoba', 'Novemba', 'Disemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mat', 'Epr', 'Mey', 'Jun', 'Jul', 'Aga', 'Sep', 'Okt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Cawe', 'Mvulo', 'Lwesibini', 'Lwesithathu', 'Lwesine', 'Lwesihlanu', 'Mgqibelo'],
+  SHORTWEEKDAYS: ['Caw', 'Mvu', 'Bin', 'Tha', 'Sin', 'Hla', 'Mgq'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1 unyangantathu', '2 unyangantathu', '3 unyangantathu', '4 unyangantathu'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__yo.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__yo.js
new file mode 100644
index 0000000..56715ea
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__yo.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['SK', 'LK'],
+  ERANAMES: ['Saju Kristi', 'Lehin Kristi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['O\u1e63\u00f9 \u1e62\u1eb9\u0301r\u1eb9\u0301', 'O\u1e63\u00f9 \u00c8r\u00e8l\u00e8', 'O\u1e63\u00f9 \u1eb8r\u1eb9\u0300n\u00e0', 'O\u1e63\u00f9 \u00ccgb\u00e9', 'O\u1e63\u00f9 \u1eb8\u0300bibi', 'O\u1e63\u00f9 \u00d2k\u00fadu', 'O\u1e63\u00f9 Ag\u1eb9m\u1ecd', 'O\u1e63\u00f9 \u00d2g\u00fan', 'O\u1e63\u00f9 Owewe', 'O\u1e63\u00f9 \u1ecc\u0300w\u00e0r\u00e0', 'O\u1e63\u00f9 B\u00e9l\u00fa', 'O\u1e63\u00f9 \u1ecc\u0300p\u1eb9\u0300'],
+  SHORTMONTHS: ['\u1e62\u1eb9\u0301r\u1eb9\u0301', '\u00c8r\u00e8l\u00e8', '\u1eb8r\u1eb9\u0300n\u00e0', '\u00ccgb\u00e9', '\u1eb8\u0300bibi', '\u00d2k\u00fadu', 'Ag\u1eb9m\u1ecd', '\u00d2g\u00fan', 'Owewe', '\u1ecc\u0300w\u00e0r\u00e0', 'B\u00e9l\u00fa', '\u1ecc\u0300p\u1eb9\u0300'],
+  WEEKDAYS: ['\u1eccj\u1ecd\u0301 \u00c0\u00eck\u00fa', '\u1eccj\u1ecd\u0301 Aj\u00e9', '\u1eccj\u1ecd\u0301 \u00ccs\u1eb9\u0301gun', '\u1eccj\u1ecd\u0301r\u00fa', '\u1eccj\u1ecd\u0301 \u00c0\u1e63\u1eb9\u0300\u1e63\u1eb9\u0300d\u00e1iy\u00e9', '\u1eccj\u1ecd\u0301 \u1eb8t\u00ec', '\u1eccj\u1ecd\u0301 \u00c0b\u00e1m\u1eb9\u0301ta'],
+  SHORTWEEKDAYS: ['\u00c0\u00eck\u00fa', 'Aj\u00e9', '\u00ccs\u1eb9\u0301gun', '\u1eccj\u1ecd\u0301r\u00fa', '\u00c0\u1e63\u1eb9\u0300\u1e63\u1eb9\u0300d\u00e1iy\u00e9', '\u1eb8t\u00ec', '\u00c0b\u00e1m\u1eb9\u0301ta'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u00e0\u00e1r\u1ecd\u0300', '\u1ecd\u0300s\u00e1n'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__yo_NG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__yo_NG.js
new file mode 100644
index 0000000..56715ea
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__yo_NG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['SK', 'LK'],
+  ERANAMES: ['Saju Kristi', 'Lehin Kristi'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['O\u1e63\u00f9 \u1e62\u1eb9\u0301r\u1eb9\u0301', 'O\u1e63\u00f9 \u00c8r\u00e8l\u00e8', 'O\u1e63\u00f9 \u1eb8r\u1eb9\u0300n\u00e0', 'O\u1e63\u00f9 \u00ccgb\u00e9', 'O\u1e63\u00f9 \u1eb8\u0300bibi', 'O\u1e63\u00f9 \u00d2k\u00fadu', 'O\u1e63\u00f9 Ag\u1eb9m\u1ecd', 'O\u1e63\u00f9 \u00d2g\u00fan', 'O\u1e63\u00f9 Owewe', 'O\u1e63\u00f9 \u1ecc\u0300w\u00e0r\u00e0', 'O\u1e63\u00f9 B\u00e9l\u00fa', 'O\u1e63\u00f9 \u1ecc\u0300p\u1eb9\u0300'],
+  SHORTMONTHS: ['\u1e62\u1eb9\u0301r\u1eb9\u0301', '\u00c8r\u00e8l\u00e8', '\u1eb8r\u1eb9\u0300n\u00e0', '\u00ccgb\u00e9', '\u1eb8\u0300bibi', '\u00d2k\u00fadu', 'Ag\u1eb9m\u1ecd', '\u00d2g\u00fan', 'Owewe', '\u1ecc\u0300w\u00e0r\u00e0', 'B\u00e9l\u00fa', '\u1ecc\u0300p\u1eb9\u0300'],
+  WEEKDAYS: ['\u1eccj\u1ecd\u0301 \u00c0\u00eck\u00fa', '\u1eccj\u1ecd\u0301 Aj\u00e9', '\u1eccj\u1ecd\u0301 \u00ccs\u1eb9\u0301gun', '\u1eccj\u1ecd\u0301r\u00fa', '\u1eccj\u1ecd\u0301 \u00c0\u1e63\u1eb9\u0300\u1e63\u1eb9\u0300d\u00e1iy\u00e9', '\u1eccj\u1ecd\u0301 \u1eb8t\u00ec', '\u1eccj\u1ecd\u0301 \u00c0b\u00e1m\u1eb9\u0301ta'],
+  SHORTWEEKDAYS: ['\u00c0\u00eck\u00fa', 'Aj\u00e9', '\u00ccs\u1eb9\u0301gun', '\u1eccj\u1ecd\u0301r\u00fa', '\u00c0\u1e63\u1eb9\u0300\u1e63\u1eb9\u0300d\u00e1iy\u00e9', '\u1eb8t\u00ec', '\u00c0b\u00e1m\u1eb9\u0301ta'],
+  NARROWWEEKDAYS: ['1', '2', '3', '4', '5', '6', '7'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['\u00e0\u00e1r\u1ecd\u0300', '\u1ecd\u0300s\u00e1n'],
+  DATEFORMATS: ['EEEE, y MMMM dd', 'y MMMM d', 'y MMM d', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['HH:mm:ss zzzz', 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEMONTHS = gadgets.i18n.DateTimeConstants.MONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh.js
new file mode 100644
index 0000000..c81eb40
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'yy-M-d'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'zah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_CN.js
new file mode 100644
index 0000000..c81eb40
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'yy-M-d'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'zah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_HK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_HK.js
new file mode 100644
index 0000000..41222ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_HK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u897f\u5143\u524d', '\u897f\u5143'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u9031\u65e5', '\u9031\u4e00', '\u9031\u4e8c', '\u9031\u4e09', '\u9031\u56db', '\u9031\u4e94', '\u9031\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63', '\u7b2c2\u5b63', '\u7b2c3\u5b63', '\u7b2c4\u5b63'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'y\u5e74M\u6708d\u65e5', 'yy\u5e74M\u6708d\u65e5'],
+  TIMEFORMATS: ['zzzzah\u6642mm\u5206ss\u79d2', 'zah\u6642mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans.js
new file mode 100644
index 0000000..c81eb40
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'yy-M-d'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'zah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_CN.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_CN.js
new file mode 100644
index 0000000..c81eb40
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_CN.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'yy-M-d'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'zah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_HK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_HK.js
new file mode 100644
index 0000000..c81eb40
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_HK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'yy-M-d'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'zah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_MO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_MO.js
new file mode 100644
index 0000000..c81eb40
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_MO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'yy-M-d'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'zah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_SG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_SG.js
new file mode 100644
index 0000000..ea7933e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hans_SG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'dd/MM/yy'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ssz', 'ahh:mm:ss', 'ahh:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant.js
new file mode 100644
index 0000000..7ddbce4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u897f\u5143\u524d', '\u897f\u5143'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u9031\u65e5', '\u9031\u4e00', '\u9031\u4e8c', '\u9031\u4e09', '\u9031\u56db', '\u9031\u4e94', '\u9031\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63', '\u7b2c2\u5b63', '\u7b2c3\u5b63', '\u7b2c4\u5b63'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy/M/d', 'yy/M/d'],
+  TIMEFORMATS: ['zzzzah\u6642mm\u5206ss\u79d2', 'zah\u6642mm\u5206ss\u79d2', 'ah:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_HK.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_HK.js
new file mode 100644
index 0000000..41222ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_HK.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u897f\u5143\u524d', '\u897f\u5143'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u9031\u65e5', '\u9031\u4e00', '\u9031\u4e8c', '\u9031\u4e09', '\u9031\u56db', '\u9031\u4e94', '\u9031\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63', '\u7b2c2\u5b63', '\u7b2c3\u5b63', '\u7b2c4\u5b63'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'y\u5e74M\u6708d\u65e5', 'yy\u5e74M\u6708d\u65e5'],
+  TIMEFORMATS: ['zzzzah\u6642mm\u5206ss\u79d2', 'zah\u6642mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_MO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_MO.js
new file mode 100644
index 0000000..724fd72
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_MO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u897f\u5143\u524d', '\u897f\u5143'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u9031\u65e5', '\u9031\u4e00', '\u9031\u4e8c', '\u9031\u4e09', '\u9031\u56db', '\u9031\u4e94', '\u9031\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63', '\u7b2c2\u5b63', '\u7b2c3\u5b63', '\u7b2c4\u5b63'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74MM\u6708dd\u65e5EEEE', 'y\u5e74MM\u6708dd\u65e5', 'y\u5e74M\u6708d\u65e5', 'yy\u5e74M\u6708d\u65e5'],
+  TIMEFORMATS: ['zzzzah\u6642mm\u5206ss\u79d2', 'zah\u6642mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_TW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_TW.js
new file mode 100644
index 0000000..7ddbce4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_Hant_TW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u897f\u5143\u524d', '\u897f\u5143'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u9031\u65e5', '\u9031\u4e00', '\u9031\u4e8c', '\u9031\u4e09', '\u9031\u56db', '\u9031\u4e94', '\u9031\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63', '\u7b2c2\u5b63', '\u7b2c3\u5b63', '\u7b2c4\u5b63'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy/M/d', 'yy/M/d'],
+  TIMEFORMATS: ['zzzzah\u6642mm\u5206ss\u79d2', 'zah\u6642mm\u5206ss\u79d2', 'ah:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_MO.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_MO.js
new file mode 100644
index 0000000..724fd72
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_MO.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u897f\u5143\u524d', '\u897f\u5143'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u9031\u65e5', '\u9031\u4e00', '\u9031\u4e8c', '\u9031\u4e09', '\u9031\u56db', '\u9031\u4e94', '\u9031\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63', '\u7b2c2\u5b63', '\u7b2c3\u5b63', '\u7b2c4\u5b63'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74MM\u6708dd\u65e5EEEE', 'y\u5e74MM\u6708dd\u65e5', 'y\u5e74M\u6708d\u65e5', 'yy\u5e74M\u6708d\u65e5'],
+  TIMEFORMATS: ['zzzzah\u6642mm\u5206ss\u79d2', 'zah\u6642mm\u5206ss\u79d2', 'ahh:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_SG.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_SG.js
new file mode 100644
index 0000000..ea7933e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_SG.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  NARROWMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONESHORTMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u5468\u65e5', '\u5468\u4e00', '\u5468\u4e8c', '\u5468\u4e09', '\u5468\u56db', '\u5468\u4e94', '\u5468\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63\u5ea6', '\u7b2c2\u5b63\u5ea6', '\u7b2c3\u5b63\u5ea6', '\u7b2c4\u5b63\u5ea6'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy-M-d', 'dd/MM/yy'],
+  TIMEFORMATS: ['zzzzah\u65f6mm\u5206ss\u79d2', 'ahh:mm:ssz', 'ahh:mm:ss', 'ahh:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_TW.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_TW.js
new file mode 100644
index 0000000..7ddbce4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zh_TW.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['\u516c\u5143\u524d', '\u516c\u5143'],
+  ERANAMES: ['\u897f\u5143\u524d', '\u897f\u5143'],
+  NARROWMONTHS: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'],
+  MONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  STANDALONEMONTHS: ['\u4e00\u6708', '\u4e8c\u6708', '\u4e09\u6708', '\u56db\u6708', '\u4e94\u6708', '\u516d\u6708', '\u4e03\u6708', '\u516b\u6708', '\u4e5d\u6708', '\u5341\u6708', '\u5341\u4e00\u6708', '\u5341\u4e8c\u6708'],
+  SHORTMONTHS: ['1\u6708', '2\u6708', '3\u6708', '4\u6708', '5\u6708', '6\u6708', '7\u6708', '8\u6708', '9\u6708', '10\u6708', '11\u6708', '12\u6708'],
+  WEEKDAYS: ['\u661f\u671f\u65e5', '\u661f\u671f\u4e00', '\u661f\u671f\u4e8c', '\u661f\u671f\u4e09', '\u661f\u671f\u56db', '\u661f\u671f\u4e94', '\u661f\u671f\u516d'],
+  SHORTWEEKDAYS: ['\u9031\u65e5', '\u9031\u4e00', '\u9031\u4e8c', '\u9031\u4e09', '\u9031\u56db', '\u9031\u4e94', '\u9031\u516d'],
+  NARROWWEEKDAYS: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d'],
+  SHORTQUARTERS: ['1\u5b63', '2\u5b63', '3\u5b63', '4\u5b63'],
+  QUARTERS: ['\u7b2c1\u5b63', '\u7b2c2\u5b63', '\u7b2c3\u5b63', '\u7b2c4\u5b63'],
+  AMPMS: ['\u4e0a\u5348', '\u4e0b\u5348'],
+  DATEFORMATS: ['y\u5e74M\u6708d\u65e5EEEE', 'y\u5e74M\u6708d\u65e5', 'yyyy/M/d', 'yy/M/d'],
+  TIMEFORMATS: ['zzzzah\u6642mm\u5206ss\u79d2', 'zah\u6642mm\u5206ss\u79d2', 'ah:mm:ss', 'ah:mm'],
+  FIRSTDAYOFWEEK: 6,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 2
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zu.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zu.js
new file mode 100644
index 0000000..24f6c6b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zu.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januwari', 'Februwari', 'Mashi', 'Apreli', 'Meyi', 'Juni', 'Julayi', 'Agasti', 'Septhemba', 'Okthoba', 'Novemba', 'Disemba'],
+  STANDALONEMONTHS: ['uJanuwari', 'uFebruwari', 'uMashi', 'u-Apreli', 'uMeyi', 'uJuni', 'uJulayi', 'uAgasti', 'uSepthemba', 'u-Okthoba', 'uNovemba', 'uDisemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mas', 'Apr', 'Mey', 'Jun', 'Jul', 'Aga', 'Sep', 'Okt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Sonto', 'Msombuluko', 'Lwesibili', 'Lwesithathu', 'uLwesine', 'Lwesihlanu', 'Mgqibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mso', 'Bil', 'Tha', 'Sin', 'Hla', 'Mgq'],
+  NARROWWEEKDAYS: ['S', 'M', 'B', 'T', 'S', 'H', 'M'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'd MMMM y', 'd MMM y', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zu_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zu_ZA.js
new file mode 100644
index 0000000..24f6c6b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/DateTimeConstants__zu_ZA.js
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.DateTimeConstants = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['BC', 'AD'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januwari', 'Februwari', 'Mashi', 'Apreli', 'Meyi', 'Juni', 'Julayi', 'Agasti', 'Septhemba', 'Okthoba', 'Novemba', 'Disemba'],
+  STANDALONEMONTHS: ['uJanuwari', 'uFebruwari', 'uMashi', 'u-Apreli', 'uMeyi', 'uJuni', 'uJulayi', 'uAgasti', 'uSepthemba', 'u-Okthoba', 'uNovemba', 'uDisemba'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mas', 'Apr', 'Mey', 'Jun', 'Jul', 'Aga', 'Sep', 'Okt', 'Nov', 'Dis'],
+  WEEKDAYS: ['Sonto', 'Msombuluko', 'Lwesibili', 'Lwesithathu', 'uLwesine', 'Lwesihlanu', 'Mgqibelo'],
+  SHORTWEEKDAYS: ['Son', 'Mso', 'Bil', 'Tha', 'Sin', 'Hla', 'Mgq'],
+  NARROWWEEKDAYS: ['S', 'M', 'B', 'T', 'S', 'H', 'M'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE dd MMMM y', 'd MMMM y', 'd MMM y', 'yyyy-MM-dd'],
+  TIMEFORMATS: ['h:mm:ss a zzzz', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  FIRSTDAYOFWEEK: 0,
+  WEEKENDRANGE: [5, 6],
+  FIRSTWEEKCUTOFFDAY: 3
+};
+gadgets.i18n.DateTimeConstants.STANDALONENARROWMONTHS = gadgets.i18n.DateTimeConstants.NARROWMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTMONTHS = gadgets.i18n.DateTimeConstants.SHORTMONTHS;
+gadgets.i18n.DateTimeConstants.STANDALONEWEEKDAYS = gadgets.i18n.DateTimeConstants.WEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONESHORTWEEKDAYS = gadgets.i18n.DateTimeConstants.SHORTWEEKDAYS;
+gadgets.i18n.DateTimeConstants.STANDALONENARROWWEEKDAYS = gadgets.i18n.DateTimeConstants.NARROWWEEKDAYS;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa.js
new file mode 100644
index 0000000..076cca3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'DJF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_DJ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_DJ.js
new file mode 100644
index 0000000..076cca3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_DJ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'DJF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ER.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ER.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ER.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ER_SAAHO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ER_SAAHO.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ER_SAAHO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ET.js
new file mode 100644
index 0000000..7f0c064
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__aa_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af_NA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af_NA.js
new file mode 100644
index 0000000..da347d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af_NA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__af_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ak.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ak.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ak.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ak_GH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ak_GH.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ak_GH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__am.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__am.js
new file mode 100644
index 0000000..fae73cb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__am.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__am_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__am_ET.js
new file mode 100644
index 0000000..fae73cb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__am_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar.js
new file mode 100644
index 0000000..c5e812c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'AED'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_AE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_AE.js
new file mode 100644
index 0000000..c5e812c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_AE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'AED'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_BH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_BH.js
new file mode 100644
index 0000000..bd84851
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_BH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'BHD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_DZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_DZ.js
new file mode 100644
index 0000000..26e3fa5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_DZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'DZD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_EG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_EG.js
new file mode 100644
index 0000000..37b8930
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_EG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'EGP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_IQ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_IQ.js
new file mode 100644
index 0000000..ed0f156
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_IQ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'IQD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_JO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_JO.js
new file mode 100644
index 0000000..87d1cfb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_JO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'JOD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_KW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_KW.js
new file mode 100644
index 0000000..e721c0e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_KW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'KWD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_LB.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_LB.js
new file mode 100644
index 0000000..d16a46b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_LB.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'LBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_LY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_LY.js
new file mode 100644
index 0000000..ee6b0a2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_LY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'LYD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_MA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_MA.js
new file mode 100644
index 0000000..f4f30ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_MA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'MAD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_OM.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_OM.js
new file mode 100644
index 0000000..77aaa49
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_OM.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'OMR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_QA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_QA.js
new file mode 100644
index 0000000..c31c880
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_QA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#0.###;#0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#0.00',
+  DEF_CURRENCY_CODE: 'QAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SA.js
new file mode 100644
index 0000000..23f95f2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#0.###;#0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#0.00',
+  DEF_CURRENCY_CODE: 'SAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SD.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SD.js
new file mode 100644
index 0000000..5e62dd7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SD.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'SDD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SY.js
new file mode 100644
index 0000000..59976b2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_SY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#0.###;#0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#0.00',
+  DEF_CURRENCY_CODE: 'SYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_TN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_TN.js
new file mode 100644
index 0000000..905ecb4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_TN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#0.###;#0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#0.00',
+  DEF_CURRENCY_CODE: 'TND'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_YE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_YE.js
new file mode 100644
index 0000000..3915680
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ar_YE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#0.###;#0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#0.00',
+  DEF_CURRENCY_CODE: 'YER'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__as.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__as.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__as.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__as_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__as_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__as_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az.js
new file mode 100644
index 0000000..0832107
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'AZN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_AZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_AZ.js
new file mode 100644
index 0000000..0832107
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_AZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'AZN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Cyrl.js
new file mode 100644
index 0000000..0832107
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Cyrl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'AZN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Cyrl_AZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Cyrl_AZ.js
new file mode 100644
index 0000000..0832107
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Cyrl_AZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'AZN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Latn.js
new file mode 100644
index 0000000..0832107
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Latn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'AZN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Latn_AZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Latn_AZ.js
new file mode 100644
index 0000000..0832107
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__az_Latn_AZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'AZN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__be.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__be.js
new file mode 100644
index 0000000..d202e0a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__be.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'BYR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__be_BY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__be_BY.js
new file mode 100644
index 0000000..d202e0a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__be_BY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'BYR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bg.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bg.js
new file mode 100644
index 0000000..b6a520c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bg.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u041D/\u0427',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bg_BG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bg_BG.js
new file mode 100644
index 0000000..b6a520c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bg_BG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u041D/\u0427',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn.js
new file mode 100644
index 0000000..be894b9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '#,##,##0.00\u00A4;(#,##,##0.00\u00A4)',
+  DEF_CURRENCY_CODE: 'BDT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn_BD.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn_BD.js
new file mode 100644
index 0000000..be894b9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn_BD.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '#,##,##0.00\u00A4;(#,##,##0.00\u00A4)',
+  DEF_CURRENCY_CODE: 'BDT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn_IN.js
new file mode 100644
index 0000000..1a554f7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bn_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '#,##,##0.00\u00A4;(#,##,##0.00\u00A4)',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo_CN.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo_IN.js
new file mode 100644
index 0000000..c03ba71
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bo_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bs.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bs.js
new file mode 100644
index 0000000..9e8589d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bs.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'BAM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bs_BA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bs_BA.js
new file mode 100644
index 0000000..9e8589d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__bs_BA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'BAM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__byn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__byn.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__byn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__byn_ER.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__byn_ER.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__byn_ER.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ca.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ca.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ca.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ca_ES.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ca_ES.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ca_ES.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cch.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cch.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cch.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cch_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cch_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cch_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cop.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cop.js
new file mode 100644
index 0000000..b86cb8c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cop.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EGP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cs.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cs.js
new file mode 100644
index 0000000..6272382
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cs.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'CZK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cs_CZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cs_CZ.js
new file mode 100644
index 0000000..6272382
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cs_CZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'CZK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cy.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cy.js
new file mode 100644
index 0000000..37f95cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cy.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cy_GB.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cy_GB.js
new file mode 100644
index 0000000..37f95cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__cy_GB.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__da.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__da.js
new file mode 100644
index 0000000..763da35
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__da.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'DKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__da_DK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__da_DK.js
new file mode 100644
index 0000000..763da35
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__da_DK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'DKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_AT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_AT.js
new file mode 100644
index 0000000..f512946
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_AT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_BE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_BE.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_BE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_CH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_CH.js
new file mode 100644
index 0000000..f5d1a3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_CH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: "'",
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'CHF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_DE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_DE.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_DE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_LI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_LI.js
new file mode 100644
index 0000000..8aae21c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_LI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: "'",
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CHF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_LU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_LU.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__de_LU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dv.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dv.js
new file mode 100644
index 0000000..22a7991
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dv.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'MVR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dv_MV.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dv_MV.js
new file mode 100644
index 0000000..22a7991
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dv_MV.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'MVR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dz.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dz.js
new file mode 100644
index 0000000..ef41b59
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dz.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E+00',
+  PERCENT_PATTERN: '#,##,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4#,##,##0.00',
+  DEF_CURRENCY_CODE: 'BTN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dz_BT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dz_BT.js
new file mode 100644
index 0000000..ef41b59
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__dz_BT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E+00',
+  PERCENT_PATTERN: '#,##,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4#,##,##0.00',
+  DEF_CURRENCY_CODE: 'BTN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee_GH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee_GH.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee_GH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee_TG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee_TG.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ee_TG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el.js
new file mode 100644
index 0000000..aa7035d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'e',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'CYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_CY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_CY.js
new file mode 100644
index 0000000..b634e19
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_CY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'e',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'CYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_GR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_GR.js
new file mode 100644
index 0000000..77802b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_GR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'e',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_POLYTON.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_POLYTON.js
new file mode 100644
index 0000000..aa7035d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__el_POLYTON.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'e',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'CYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_AS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_AS.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_AS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_AU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_AU.js
new file mode 100644
index 0000000..d7210d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_AU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'AUD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BE.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BW.js
new file mode 100644
index 0000000..48ca9ba
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'BWP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BZ.js
new file mode 100644
index 0000000..4135061
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_BZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'BZD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_CA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_CA.js
new file mode 100644
index 0000000..eedaace
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_CA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'CAD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Dsrt.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Dsrt.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Dsrt.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Dsrt_US.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Dsrt_US.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Dsrt_US.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_GB.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_GB.js
new file mode 100644
index 0000000..37f95cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_GB.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_GU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_GU.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_GU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_HK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_HK.js
new file mode 100644
index 0000000..646ab39
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_HK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'HKD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_IE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_IE.js
new file mode 100644
index 0000000..68c0935
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_IE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_JM.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_JM.js
new file mode 100644
index 0000000..d0f7084
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_JM.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'JMD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MH.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MP.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MP.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MP.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MT.js
new file mode 100644
index 0000000..a0893d9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_MT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'MTL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_NA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_NA.js
new file mode 100644
index 0000000..c0fa26a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_NA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_NZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_NZ.js
new file mode 100644
index 0000000..b21d3f9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_NZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'NZD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_PH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_PH.js
new file mode 100644
index 0000000..e634706
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_PH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'PHP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_PK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_PK.js
new file mode 100644
index 0000000..8d01b94
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_PK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'PKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_SG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_SG.js
new file mode 100644
index 0000000..cb38062
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_SG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'SGD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Shaw.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Shaw.js
new file mode 100644
index 0000000..dcf72a6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_Shaw.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_TT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_TT.js
new file mode 100644
index 0000000..7719d10
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_TT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'TTD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_UM.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_UM.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_UM.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_US.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_US.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_US.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_VI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_VI.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_VI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_ZW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_ZW.js
new file mode 100644
index 0000000..9aedd52
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__en_ZW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZWD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eo.js
new file mode 100644
index 0000000..a3567d7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es.js
new file mode 100644
index 0000000..f2c2b04
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'ARS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_AR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_AR.js
new file mode 100644
index 0000000..f2c2b04
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_AR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'ARS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_BO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_BO.js
new file mode 100644
index 0000000..25ae2b9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_BO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'BOB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CL.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CL.js
new file mode 100644
index 0000000..cf3fa82
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CL.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'CLP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CO.js
new file mode 100644
index 0000000..5baf883
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'COP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CR.js
new file mode 100644
index 0000000..1715781
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_CR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CRC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_DO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_DO.js
new file mode 100644
index 0000000..526b5a4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_DO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'DOP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_EC.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_EC.js
new file mode 100644
index 0000000..ad5c80e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_EC.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_ES.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_ES.js
new file mode 100644
index 0000000..6500647
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_ES.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_GT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_GT.js
new file mode 100644
index 0000000..be0ab9e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_GT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GTQ'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_HN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_HN.js
new file mode 100644
index 0000000..2f9031e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_HN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'HNL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_MX.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_MX.js
new file mode 100644
index 0000000..07b525b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_MX.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MXN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_NI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_NI.js
new file mode 100644
index 0000000..1671086
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_NI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NIO'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PA.js
new file mode 100644
index 0000000..b013441
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'PAB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PE.js
new file mode 100644
index 0000000..a7cd5c5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'PEN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PR.js
new file mode 100644
index 0000000..386f52a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PY.js
new file mode 100644
index 0000000..da8851f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_PY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0-#,##0.00',
+  DEF_CURRENCY_CODE: 'PYG'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_SV.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_SV.js
new file mode 100644
index 0000000..1364bf1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_SV.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'SVC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_US.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_US.js
new file mode 100644
index 0000000..386f52a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_US.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_UY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_UY.js
new file mode 100644
index 0000000..f882be2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_UY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;(\u00A4\u00A0#,##0.00)',
+  DEF_CURRENCY_CODE: 'UYU'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_VE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_VE.js
new file mode 100644
index 0000000..0994f65
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__es_VE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'VEB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__et.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__et.js
new file mode 100644
index 0000000..e7295e7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__et.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EEK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__et_EE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__et_EE.js
new file mode 100644
index 0000000..e7295e7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__et_EE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EEK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eu.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eu.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eu.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eu_ES.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eu_ES.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__eu_ES.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa.js
new file mode 100644
index 0000000..8e12942
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: "#,##0.###;'\u202A'-#,##0.###'\u202C'",
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: "'\u202A'%#,##0'\u202C'",
+  CURRENCY_PATTERN: "#,##0.00\u00A0\u00A4;'\u202A'-#,##0.00'\u202C'\u00A0\u00A4",
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa_AF.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa_AF.js
new file mode 100644
index 0000000..3d26def
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa_AF.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: "#,##0.###;'\u202A'-#,##0.###'\u202C'",
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: "'\u202A'#,##0%'\u202C'",
+  CURRENCY_PATTERN: "#,##0.00\u00A0\u00A4;'\u202A'-#,##0.00'\u202C'\u00A0\u00A4",
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa_IR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa_IR.js
new file mode 100644
index 0000000..2dbae90
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fa_IR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: "#,##0.###;'\u202A'-#,##0.###'\u202C'",
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: "'\u202A'%#,##0'\u202C'",
+  CURRENCY_PATTERN: "#,##0.00\u00A0\u00A4;'\u202A'-#,##0.00'\u202C'\u00A0\u00A4",
+  DEF_CURRENCY_CODE: 'IRR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fi.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fi.js
new file mode 100644
index 0000000..8d2d372
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fi.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'ep\u00E4luku',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fi_FI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fi_FI.js
new file mode 100644
index 0000000..8d2d372
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fi_FI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'ep\u00E4luku',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fil.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fil.js
new file mode 100644
index 0000000..e14a3ad
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fil.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'PHP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fil_PH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fil_PH.js
new file mode 100644
index 0000000..e14a3ad
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fil_PH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'PHP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fo.js
new file mode 100644
index 0000000..37cbe95
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'DKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fo_FO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fo_FO.js
new file mode 100644
index 0000000..37cbe95
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fo_FO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'DKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr.js
new file mode 100644
index 0000000..9bdd460
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_BE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_BE.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_BE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_CA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_CA.js
new file mode 100644
index 0000000..8d280e9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_CA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4;(#,##0.00\u00A0\u00A4)',
+  DEF_CURRENCY_CODE: 'CAD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_CH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_CH.js
new file mode 100644
index 0000000..f5d1a3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_CH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: "'",
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'CHF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_FR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_FR.js
new file mode 100644
index 0000000..9bdd460
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_FR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_LU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_LU.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_LU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_MC.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_MC.js
new file mode 100644
index 0000000..9bdd460
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_MC.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_SN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_SN.js
new file mode 100644
index 0000000..90f56e7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fr_SN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fur.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fur.js
new file mode 100644
index 0000000..6500647
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fur.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fur_IT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fur_IT.js
new file mode 100644
index 0000000..6500647
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__fur_IT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ga.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ga.js
new file mode 100644
index 0000000..68c0935
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ga.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ga_IE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ga_IE.js
new file mode 100644
index 0000000..68c0935
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ga_IE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gaa.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gaa.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gaa.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gaa_GH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gaa_GH.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gaa_GH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez.js
new file mode 100644
index 0000000..080f114
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: '\u12C8',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez_ER.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez_ER.js
new file mode 100644
index 0000000..080f114
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez_ER.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: '\u12C8',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez_ET.js
new file mode 100644
index 0000000..6765cf2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gez_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: '\u12C8',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gl.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gl_ES.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gl_ES.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gl_ES.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gsw.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gsw.js
new file mode 100644
index 0000000..a60af09
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gsw.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: '\u2019',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'CHF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gsw_CH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gsw_CH.js
new file mode 100644
index 0000000..a60af09
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gsw_CH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: '\u2019',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'CHF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gu.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gu.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gu.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gu_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gu_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gu_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gv.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gv.js
new file mode 100644
index 0000000..37f95cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gv.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gv_GB.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gv_GB.js
new file mode 100644
index 0000000..37f95cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__gv_GB.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab_SD.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab_SD.js
new file mode 100644
index 0000000..319c22d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Arab_SD.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'SDD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_GH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_GH.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_GH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_GH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_GH.js
new file mode 100644
index 0000000..f77d7a3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_GH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GHC'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_NE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_NE.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_NE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_Latn_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_NE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_NE.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_NE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_SD.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_SD.js
new file mode 100644
index 0000000..319c22d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ha_SD.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'SDD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__haw.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__haw.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__haw.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__haw_US.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__haw_US.js
new file mode 100644
index 0000000..b0a71fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__haw_US.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__he.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__he.js
new file mode 100644
index 0000000..f51ba07
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__he.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'ILS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__he_IL.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__he_IL.js
new file mode 100644
index 0000000..f51ba07
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__he_IL.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'ILS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hi.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hi.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hi.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hi_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hi_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hi_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hr.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hr.js
new file mode 100644
index 0000000..6ff68cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hr.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'HRK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hr_HR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hr_HR.js
new file mode 100644
index 0000000..6ff68cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hr_HR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'HRK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hu.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hu.js
new file mode 100644
index 0000000..c4f54b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hu.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'HUF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hu_HU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hu_HU.js
new file mode 100644
index 0000000..c4f54b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hu_HU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'HUF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hy.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hy.js
new file mode 100644
index 0000000..c52aac7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hy.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#0%',
+  CURRENCY_PATTERN: '#0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'AMD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hy_AM.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hy_AM.js
new file mode 100644
index 0000000..c52aac7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__hy_AM.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#0%',
+  CURRENCY_PATTERN: '#0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'AMD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ia.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ia.js
new file mode 100644
index 0000000..060906e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ia.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__id.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__id.js
new file mode 100644
index 0000000..7a7b313
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__id.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'IDR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__id_ID.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__id_ID.js
new file mode 100644
index 0000000..7a7b313
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__id_ID.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'IDR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ig.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ig.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ig.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ig_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ig_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ig_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ii.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ii.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ii.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ii_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ii_CN.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ii_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__in.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__in.js
new file mode 100644
index 0000000..7a7b313
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__in.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'IDR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__is.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__is.js
new file mode 100644
index 0000000..63b9fd6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__is.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'EiTa',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'ISK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__is_IS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__is_IS.js
new file mode 100644
index 0000000..63b9fd6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__is_IS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'EiTa',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'ISK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it.js
new file mode 100644
index 0000000..eee52e4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CHF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it_CH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it_CH.js
new file mode 100644
index 0000000..a9d2eee
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it_CH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: "'",
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'CHF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it_IT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it_IT.js
new file mode 100644
index 0000000..6500647
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__it_IT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__iu.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__iu.js
new file mode 100644
index 0000000..cf9d202
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__iu.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CAD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__iw.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__iw.js
new file mode 100644
index 0000000..f51ba07
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__iw.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'ILS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ja.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ja.js
new file mode 100644
index 0000000..4832a1b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ja.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'JPY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ja_JP.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ja_JP.js
new file mode 100644
index 0000000..4832a1b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ja_JP.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'JPY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ka.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ka.js
new file mode 100644
index 0000000..6363854
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ka.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GEL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ka_GE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ka_GE.js
new file mode 100644
index 0000000..6363854
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ka_GE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GEL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kaj.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kaj.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kaj.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kaj_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kaj_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kaj_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kam.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kam.js
new file mode 100644
index 0000000..cbfbd56
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kam.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'KES'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kam_KE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kam_KE.js
new file mode 100644
index 0000000..cbfbd56
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kam_KE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'KES'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kcg.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kcg.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kcg.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kcg_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kcg_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kcg_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kfo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kfo.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kfo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kfo_CI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kfo_CI.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kfo_CI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk.js
new file mode 100644
index 0000000..ce1c49a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'KZT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_Cyrl.js
new file mode 100644
index 0000000..ce1c49a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_Cyrl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'KZT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_Cyrl_KZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_Cyrl_KZ.js
new file mode 100644
index 0000000..ce1c49a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_Cyrl_KZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'KZT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_KZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_KZ.js
new file mode 100644
index 0000000..ce1c49a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kk_KZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'KZT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kl.js
new file mode 100644
index 0000000..13a2bfd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'DKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kl_GL.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kl_GL.js
new file mode 100644
index 0000000..13a2bfd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kl_GL.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'DKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__km.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__km.js
new file mode 100644
index 0000000..ec266a7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__km.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A4',
+  DEF_CURRENCY_CODE: 'KHR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__km_KH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__km_KH.js
new file mode 100644
index 0000000..ec266a7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__km_KH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A4',
+  DEF_CURRENCY_CODE: 'KHR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kn.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kn_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kn_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kn_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ko.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ko.js
new file mode 100644
index 0000000..b237afd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ko.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'KPW'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ko_KR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ko_KR.js
new file mode 100644
index 0000000..158e9ac
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ko_KR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'KRW'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kok.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kok.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kok.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kok_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kok_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kok_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe.js
new file mode 100644
index 0000000..be6de30
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GNF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe_GN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe_GN.js
new file mode 100644
index 0000000..be6de30
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe_GN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'GNF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe_LR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe_LR.js
new file mode 100644
index 0000000..ad690d1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kpe_LR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'LRD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku.js
new file mode 100644
index 0000000..bf877ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'IQD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Arab.js
new file mode 100644
index 0000000..bf877ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Arab.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'IQD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_IQ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_IQ.js
new file mode 100644
index 0000000..bf877ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_IQ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'IQD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_IR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_IR.js
new file mode 100644
index 0000000..b7986b3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_IR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'IRR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Latn.js
new file mode 100644
index 0000000..bf877ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Latn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'IQD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Latn_TR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Latn_TR.js
new file mode 100644
index 0000000..f571c0e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_Latn_TR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TRY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_SY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_SY.js
new file mode 100644
index 0000000..da6964d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_SY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'SYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_TR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_TR.js
new file mode 100644
index 0000000..f571c0e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ku_TR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TRY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kw.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kw.js
new file mode 100644
index 0000000..37f95cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kw.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kw_GB.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kw_GB.js
new file mode 100644
index 0000000..37f95cd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__kw_GB.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'GBP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ky.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ky.js
new file mode 100644
index 0000000..8faf026
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ky.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'KGS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ky_KG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ky_KG.js
new file mode 100644
index 0000000..8faf026
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ky_KG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'KGS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln.js
new file mode 100644
index 0000000..213d7c6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CDF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln_CD.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln_CD.js
new file mode 100644
index 0000000..213d7c6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln_CD.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CDF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln_CG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln_CG.js
new file mode 100644
index 0000000..205f52d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ln_CG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XAF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lo.js
new file mode 100644
index 0000000..886d85b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'LAK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lo_LA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lo_LA.js
new file mode 100644
index 0000000..886d85b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lo_LA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'LAK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lt.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lt.js
new file mode 100644
index 0000000..c2b5170
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lt.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'LTL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lt_LT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lt_LT.js
new file mode 100644
index 0000000..c2b5170
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lt_LT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'LTL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lv.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lv.js
new file mode 100644
index 0000000..287bb99
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lv.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'nav\u00A0skaitlis',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'LVL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lv_LV.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lv_LV.js
new file mode 100644
index 0000000..287bb99
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__lv_LV.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'nav\u00A0skaitlis',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'LVL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mk.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mk.js
new file mode 100644
index 0000000..b1de25e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mk.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;(#,##0.###)',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MKD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mk_MK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mk_MK.js
new file mode 100644
index 0000000..b1de25e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mk_MK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;(#,##0.###)',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MKD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ml.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ml.js
new file mode 100644
index 0000000..4b4222d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ml.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '#,##,##0.00\u00A4',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ml_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ml_IN.js
new file mode 100644
index 0000000..4b4222d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ml_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '#,##,##0.00\u00A4',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn.js
new file mode 100644
index 0000000..6503c3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MNT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_CN.js
new file mode 100644
index 0000000..2f7b5ee
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Cyrl.js
new file mode 100644
index 0000000..6503c3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Cyrl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MNT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Cyrl_MN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Cyrl_MN.js
new file mode 100644
index 0000000..6503c3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Cyrl_MN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MNT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_MN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_MN.js
new file mode 100644
index 0000000..6503c3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_MN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MNT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Mong.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Mong.js
new file mode 100644
index 0000000..6503c3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Mong.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MNT'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Mong_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Mong_CN.js
new file mode 100644
index 0000000..2f7b5ee
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mn_Mong_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mo.js
new file mode 100644
index 0000000..43abb45
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'MDL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mr.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mr.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mr.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mr_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mr_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mr_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms.js
new file mode 100644
index 0000000..ffc5743
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'AUD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms_BN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms_BN.js
new file mode 100644
index 0000000..7d638a2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms_BN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'BND'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms_MY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms_MY.js
new file mode 100644
index 0000000..5d6602d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ms_MY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'MYR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mt.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mt.js
new file mode 100644
index 0000000..a0893d9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mt.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'MTL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mt_MT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mt_MT.js
new file mode 100644
index 0000000..a0893d9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__mt_MT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'MTL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__my.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__my.js
new file mode 100644
index 0000000..ce99d24
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__my.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MMK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__my_MM.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__my_MM.js
new file mode 100644
index 0000000..ce99d24
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__my_MM.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MMK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nb.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nb.js
new file mode 100644
index 0000000..8c28596
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nb.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NOK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nb_NO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nb_NO.js
new file mode 100644
index 0000000..8c28596
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nb_NO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NOK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nds.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nds.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nds.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nds_DE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nds_DE.js
new file mode 100644
index 0000000..c685e1f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nds_DE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne.js
new file mode 100644
index 0000000..481944b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NPR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne_IN.js
new file mode 100644
index 0000000..c03ba71
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne_NP.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne_NP.js
new file mode 100644
index 0000000..481944b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ne_NP.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NPR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl.js
new file mode 100644
index 0000000..52e6e80
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl_BE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl_BE.js
new file mode 100644
index 0000000..808ed88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl_BE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl_NL.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl_NL.js
new file mode 100644
index 0000000..52e6e80
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nl_NL.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nn.js
new file mode 100644
index 0000000..c63223c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'NOK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nn_NO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nn_NO.js
new file mode 100644
index 0000000..c63223c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nn_NO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'NOK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__no.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__no.js
new file mode 100644
index 0000000..8c28596
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__no.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NOK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nr.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nr.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nr.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nr_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nr_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nr_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nso.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nso.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nso.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nso_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nso_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__nso_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ny.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ny.js
new file mode 100644
index 0000000..498de63
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ny.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MWK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ny_MW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ny_MW.js
new file mode 100644
index 0000000..498de63
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ny_MW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'MWK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__oc.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__oc.js
new file mode 100644
index 0000000..1d92c4c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__oc.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__oc_FR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__oc_FR.js
new file mode 100644
index 0000000..1d92c4c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__oc_FR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om.js
new file mode 100644
index 0000000..7f0c064
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om_ET.js
new file mode 100644
index 0000000..7f0c064
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om_KE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om_KE.js
new file mode 100644
index 0000000..afc5d7f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__om_KE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'KES'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__or.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__or.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__or.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__or_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__or_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__or_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa.js
new file mode 100644
index 0000000..8d01b94
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'PKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Arab.js
new file mode 100644
index 0000000..8d01b94
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Arab.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'PKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Arab_PK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Arab_PK.js
new file mode 100644
index 0000000..8d01b94
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Arab_PK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'PKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Guru.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Guru.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Guru.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Guru_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Guru_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_Guru_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_PK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_PK.js
new file mode 100644
index 0000000..8d01b94
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pa_PK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'PKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pl.js
new file mode 100644
index 0000000..44fa008
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'PLN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pl_PL.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pl_PL.js
new file mode 100644
index 0000000..44fa008
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pl_PL.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'PLN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ps.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ps.js
new file mode 100644
index 0000000..39eac80
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ps.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ps_AF.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ps_AF.js
new file mode 100644
index 0000000..39eac80
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ps_AF.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt.js
new file mode 100644
index 0000000..e1ee98d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'AOA'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt_BR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt_BR.js
new file mode 100644
index 0000000..a55d5d4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt_BR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'BRL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt_PT.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt_PT.js
new file mode 100644
index 0000000..5479a63
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__pt_PT.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro.js
new file mode 100644
index 0000000..43abb45
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'MDL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro_MD.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro_MD.js
new file mode 100644
index 0000000..43abb45
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro_MD.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'MDL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro_RO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro_RO.js
new file mode 100644
index 0000000..f9cdb54
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ro_RO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'RON'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru.js
new file mode 100644
index 0000000..5a09976
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BYR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru_RU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru_RU.js
new file mode 100644
index 0000000..8b09954
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru_RU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'RUB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru_UA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru_UA.js
new file mode 100644
index 0000000..72f2c4b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ru_UA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'UAH'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__rw.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__rw.js
new file mode 100644
index 0000000..0a83db6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__rw.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'RWF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__rw_RW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__rw_RW.js
new file mode 100644
index 0000000..0a83db6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__rw_RW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'RWF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sa.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sa.js
new file mode 100644
index 0000000..3541f93
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sa.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sa_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sa_IN.js
new file mode 100644
index 0000000..3541f93
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sa_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se.js
new file mode 100644
index 0000000..fd0bf05
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se_FI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se_FI.js
new file mode 100644
index 0000000..fd0bf05
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se_FI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se_NO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se_NO.js
new file mode 100644
index 0000000..d95459b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__se_NO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'NOK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh.js
new file mode 100644
index 0000000..2fcf892
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'RSD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_BA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_BA.js
new file mode 100644
index 0000000..8784cd4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_BA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BAM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_CS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_CS.js
new file mode 100644
index 0000000..712d0ec
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_CS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_YU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_YU.js
new file mode 100644
index 0000000..767edc0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sh_YU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'YUM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__si.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__si.js
new file mode 100644
index 0000000..931c93a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__si.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##,##0.00;(\u00A4#,##,##0.00)',
+  DEF_CURRENCY_CODE: 'LKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__si_LK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__si_LK.js
new file mode 100644
index 0000000..931c93a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__si_LK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##,##0.00;(\u00A4#,##,##0.00)',
+  DEF_CURRENCY_CODE: 'LKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sid.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sid.js
new file mode 100644
index 0000000..7f0c064
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sid.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sid_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sid_ET.js
new file mode 100644
index 0000000..7f0c064
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sid_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sk.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sk.js
new file mode 100644
index 0000000..e6a3615
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sk.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'SKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sk_SK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sk_SK.js
new file mode 100644
index 0000000..e6a3615
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sk_SK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'SKK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sl.js
new file mode 100644
index 0000000..77802b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'e',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sl_SI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sl_SI.js
new file mode 100644
index 0000000..77802b6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sl_SI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'e',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so.js
new file mode 100644
index 0000000..076cca3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'DJF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_DJ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_DJ.js
new file mode 100644
index 0000000..076cca3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_DJ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'DJF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_ET.js
new file mode 100644
index 0000000..7f0c064
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_KE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_KE.js
new file mode 100644
index 0000000..afc5d7f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_KE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'KES'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_SO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_SO.js
new file mode 100644
index 0000000..093d695
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__so_SO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'SOS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sq.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sq.js
new file mode 100644
index 0000000..a5efe05
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sq.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'MKD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sq_AL.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sq_AL.js
new file mode 100644
index 0000000..5857c9a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sq_AL.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ALL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr.js
new file mode 100644
index 0000000..8784cd4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BAM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_BA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_BA.js
new file mode 100644
index 0000000..8784cd4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_BA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BAM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_CS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_CS.js
new file mode 100644
index 0000000..712d0ec
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_CS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl.js
new file mode 100644
index 0000000..712d0ec
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_BA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_BA.js
new file mode 100644
index 0000000..8784cd4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_BA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BAM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_CS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_CS.js
new file mode 100644
index 0000000..712d0ec
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_CS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_ME.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_ME.js
new file mode 100644
index 0000000..712d0ec
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_ME.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_RS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_RS.js
new file mode 100644
index 0000000..203304c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_RS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_YU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_YU.js
new file mode 100644
index 0000000..767edc0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Cyrl_YU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'YUM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn.js
new file mode 100644
index 0000000..203304c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_BA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_BA.js
new file mode 100644
index 0000000..8784cd4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_BA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'BAM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_CS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_CS.js
new file mode 100644
index 0000000..712d0ec
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_CS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_ME.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_ME.js
new file mode 100644
index 0000000..1d92c4c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_ME.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_RS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_RS.js
new file mode 100644
index 0000000..203304c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_RS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_YU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_YU.js
new file mode 100644
index 0000000..767edc0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_Latn_YU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'YUM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_ME.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_ME.js
new file mode 100644
index 0000000..1d92c4c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_ME.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_RS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_RS.js
new file mode 100644
index 0000000..203304c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_RS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_YU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_YU.js
new file mode 100644
index 0000000..767edc0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sr_YU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'YUM'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss_SZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss_SZ.js
new file mode 100644
index 0000000..a735f46
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss_SZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'SZL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ss_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st_LS.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st_LS.js
new file mode 100644
index 0000000..b0fd62a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st_LS.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'LSL'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__st_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv.js
new file mode 100644
index 0000000..fd0bf05
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv_FI.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv_FI.js
new file mode 100644
index 0000000..fd0bf05
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv_FI.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'EUR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv_SE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv_SE.js
new file mode 100644
index 0000000..cbff6dd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sv_SE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: '\u00D710^',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: '\u00A4\u00A4\u00A4',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0\u00A0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'SEK'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw.js
new file mode 100644
index 0000000..21d19e4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'KES'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw_KE.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw_KE.js
new file mode 100644
index 0000000..afc5d7f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw_KE.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'KES'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw_TZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw_TZ.js
new file mode 100644
index 0000000..9aa5fab
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__sw_TZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'TZS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__syr.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__syr.js
new file mode 100644
index 0000000..a642c21
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__syr.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'SYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__syr_SY.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__syr_SY.js
new file mode 100644
index 0000000..a642c21
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__syr_SY.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###;#,##0.###-',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00;\u00A4\u00A0#,##0.00-',
+  DEF_CURRENCY_CODE: 'SYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ta.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ta.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ta.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ta_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ta_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ta_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__te.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__te.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__te.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__te_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__te_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__te_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg.js
new file mode 100644
index 0000000..e694116
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TJS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_Cyrl.js
new file mode 100644
index 0000000..e694116
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_Cyrl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TJS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_Cyrl_TJ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_Cyrl_TJ.js
new file mode 100644
index 0000000..e694116
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_Cyrl_TJ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TJS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_TJ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_TJ.js
new file mode 100644
index 0000000..e694116
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tg_TJ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TJS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__th.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__th.js
new file mode 100644
index 0000000..0e50bf0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__th.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'THB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__th_TH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__th_TH.js
new file mode 100644
index 0000000..0e50bf0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__th_TH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;\u00A4-#,##0.00',
+  DEF_CURRENCY_CODE: 'THB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti_ER.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti_ER.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti_ER.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti_ET.js
new file mode 100644
index 0000000..7f0c064
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ti_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tig.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tig.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tig.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tig_ER.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tig_ER.js
new file mode 100644
index 0000000..229c23f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tig_ER.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ERN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tl.js
new file mode 100644
index 0000000..e14a3ad
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'PHP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tl_PH.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tl_PH.js
new file mode 100644
index 0000000..e14a3ad
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tl_PH.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'PHP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tn.js
new file mode 100644
index 0000000..882bc22
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'BWP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tn_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tn_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tn_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__to.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__to.js
new file mode 100644
index 0000000..e8d57ce
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__to.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TOP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__to_TO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__to_TO.js
new file mode 100644
index 0000000..e8d57ce
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__to_TO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TOP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tr.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tr.js
new file mode 100644
index 0000000..0e7f9a4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tr.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '%\u00A0#,##0',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'CYP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tr_TR.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tr_TR.js
new file mode 100644
index 0000000..b5f6dfa
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tr_TR.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '%\u00A0#,##0',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'TRY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__trv.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__trv.js
new file mode 100644
index 0000000..15a2a51
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__trv.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TWD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__trv_TW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__trv_TW.js
new file mode 100644
index 0000000..15a2a51
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__trv_TW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'TWD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ts.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ts.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ts.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ts_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ts_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ts_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tt.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tt.js
new file mode 100644
index 0000000..89ad4d8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tt.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A4',
+  DEF_CURRENCY_CODE: 'RUB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tt_RU.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tt_RU.js
new file mode 100644
index 0000000..89ad4d8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__tt_RU.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A4',
+  DEF_CURRENCY_CODE: 'RUB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_Arab.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_Arab.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_Arab_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_Arab_CN.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_Arab_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_CN.js
new file mode 100644
index 0000000..2552160
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ug_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uk.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uk.js
new file mode 100644
index 0000000..3990560
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uk.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'UAH'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uk_UA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uk_UA.js
new file mode 100644
index 0000000..3990560
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uk_UA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'UAH'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur.js
new file mode 100644
index 0000000..0d48761
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur_IN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur_IN.js
new file mode 100644
index 0000000..9bf9f96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur_IN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##,##0.00',
+  DEF_CURRENCY_CODE: 'INR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur_PK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur_PK.js
new file mode 100644
index 0000000..76ad044
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ur_PK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'PKR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz.js
new file mode 100644
index 0000000..504b23b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_AF.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_AF.js
new file mode 100644
index 0000000..39eac80
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_AF.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Arab.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Arab.js
new file mode 100644
index 0000000..39eac80
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Arab.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Arab_AF.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Arab_AF.js
new file mode 100644
index 0000000..39eac80
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Arab_AF.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '\u2212',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'AFN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Cyrl.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Cyrl.js
new file mode 100644
index 0000000..b4af4fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Cyrl.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'UZS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Cyrl_UZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Cyrl_UZ.js
new file mode 100644
index 0000000..b4af4fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Cyrl_UZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'UZS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Latn.js
new file mode 100644
index 0000000..b4af4fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Latn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'UZS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Latn_UZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Latn_UZ.js
new file mode 100644
index 0000000..b4af4fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_Latn_UZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'UZS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_UZ.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_UZ.js
new file mode 100644
index 0000000..b4af4fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__uz_UZ.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'UZS'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ve.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ve.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ve.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ve_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ve_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__ve_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__vi.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__vi.js
new file mode 100644
index 0000000..7e9cda9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__vi.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'VND'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__vi_VN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__vi_VN.js
new file mode 100644
index 0000000..7e9cda9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__vi_VN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '.',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '#,##0.00\u00A0\u00A4',
+  DEF_CURRENCY_CODE: 'VND'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wal.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wal.js
new file mode 100644
index 0000000..2d16bf2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wal.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: '\u2019',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wal_ET.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wal_ET.js
new file mode 100644
index 0000000..2d16bf2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wal_ET.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: '\u2019',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ETB'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_Latn.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_Latn.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_Latn.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_Latn_SN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_Latn_SN.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_Latn_SN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_SN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_SN.js
new file mode 100644
index 0000000..7862d7c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__wo_SN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'XOF'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__xh.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__xh.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__xh.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__xh_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__xh_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__xh_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__yo.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__yo.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__yo.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__yo_NG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__yo_NG.js
new file mode 100644
index 0000000..d18a4d2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__yo_NG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4\u00A0#,##0.00',
+  DEF_CURRENCY_CODE: 'NGN'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh.js
new file mode 100644
index 0000000..f6f8f1d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_CN.js
new file mode 100644
index 0000000..f6f8f1d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_HK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_HK.js
new file mode 100644
index 0000000..646ab39
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_HK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'HKD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans.js
new file mode 100644
index 0000000..f6f8f1d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_CN.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_CN.js
new file mode 100644
index 0000000..f6f8f1d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_CN.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'CNY'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_HK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_HK.js
new file mode 100644
index 0000000..4c4705f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_HK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'HKD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_MO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_MO.js
new file mode 100644
index 0000000..0bb4203
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_MO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'MOP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_SG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_SG.js
new file mode 100644
index 0000000..7c4ddbe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hans_SG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'SGD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant.js
new file mode 100644
index 0000000..115c069
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'TWD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_HK.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_HK.js
new file mode 100644
index 0000000..646ab39
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_HK.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00;(\u00A4#,##0.00)',
+  DEF_CURRENCY_CODE: 'HKD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_MO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_MO.js
new file mode 100644
index 0000000..0bb4203
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_MO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'MOP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_TW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_TW.js
new file mode 100644
index 0000000..115c069
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_Hant_TW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'TWD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_MO.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_MO.js
new file mode 100644
index 0000000..0bb4203
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_MO.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'MOP'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_SG.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_SG.js
new file mode 100644
index 0000000..7c4ddbe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_SG.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'SGD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_TW.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_TW.js
new file mode 100644
index 0000000..115c069
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zh_TW.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'TWD'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zu.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zu.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zu.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zu_ZA.js b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zu_ZA.js
new file mode 100644
index 0000000..eaf20d0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/data/NumberFormatConstants__zu_ZA.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.NumberFormatConstants = {
+  DECIMAL_SEP: ',',
+  GROUP_SEP: '\u00A0',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'ZAR'
+};
+
+gadgets.i18n.NumberFormatConstants.MONETARY_SEP = gadgets.i18n.NumberFormatConstants.DECIMAL_SEP;
+gadgets.i18n.NumberFormatConstants.MONETARY_GROUP_SEP = gadgets.i18n.NumberFormatConstants.GROUP_SEP;
diff --git a/trunk/features/src/main/javascript/features/i18n/datetimeformat.js b/trunk/features/src/main/javascript/features/i18n/datetimeformat.js
new file mode 100644
index 0000000..d7ace32
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/datetimeformat.js
@@ -0,0 +1,579 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Functions for dealing with Date formatting.
+ */
+
+gadgets.i18n = gadgets.i18n || {};
+
+/**
+ * DateTime formatting functions following the pattern specification as defined
+ * in JDK, ICU and CLDR, with minor modification for typical usage in JS.
+ * Pattern specification: (Refer to JDK/ICU/CLDR)
+ * <pre>
+ * Symbol Meaning Presentation        Example
+ * ------   -------                 ------------        -------
+ * G        era designator          (Text)              AD
+ * y#       year                    (Number)            1996
+ * Y*       year (week of year)     (Number)            1997
+ * u*       extended year           (Number)            4601
+ * M        month in year           (Text & Number)     July & 07
+ * d        day in month            (Number)            10
+ * h        hour in am/pm (1~12)    (Number)            12
+ * H        hour in day (0~23)      (Number)            0
+ * m        minute in hour          (Number)            30
+ * s        second in minute        (Number)            55
+ * S        fractional second       (Number)            978
+ * E        day of week             (Text)              Tuesday
+ * e*       day of week (local 1~7) (Number)            2
+ * D*       day in year             (Number)            189
+ * F*       day of week in month    (Number)            2 (2nd Wed in July)
+ * w*       week in year            (Number)            27
+ * W*       week in month           (Number)            2
+ * a        am/pm marker            (Text)              PM
+ * k        hour in day (1~24)      (Number)            24
+ * K        hour in am/pm (0~11)    (Number)            0
+ * z        time zone               (Text)              Pacific Standard Time
+ * Z        time zone (RFC 822)     (Number)            -0800
+ * v        time zone (generic)     (Text)              Pacific Time
+ * g*       Julian day              (Number)            2451334
+ * A*       milliseconds in day     (Number)            69540000
+ * '        escape for text         (Delimiter)         'Date='
+ * ''       single quote            (Literal)           'o''clock'
+ *
+ * Item marked with '*' are not supported yet.
+ * Item marked with '#' works different than java
+ *
+ * The count of pattern letters determine the format.
+ * (Text): 4 or more, use full form, <4, use short or abbreviated form if it
+ * exists. (e.g., "EEEE" produces "Monday", "EEE" produces "Mon")
+ *
+ * (Number): the minimum number of digits. Shorter numbers are zero-padded to
+ * this amount (e.g. if "m" produces "6", "mm" produces "06"). Year is handled
+ * specially; that is, if the count of 'y' is 2, the Year will be truncated to
+ * 2 digits. (e.g., if "yyyy" produces "1997", "yy" produces "97".) Unlike other
+ * fields, fractional seconds are padded on the right with zero.
+ *
+ * (Text & Number): 3 or over, use text, otherwise use number. (e.g., "M"
+ * produces "1", "MM" produces "01", "MMM" produces "Jan", and "MMMM" produces
+ * "January".)
+ *
+ * Any characters in the pattern that are not in the ranges of ['a'..'z'] and
+ * ['A'..'Z'] will be treated as quoted text. For instance, characters like ':',
+ * '.', ' ', '#' and '@' will appear in the resulting time text even they are
+ * not embraced within single quotes.
+ * </pre>
+ *
+ */
+
+/**
+ * Construct a DateTimeFormat object based on current locale by using
+ * the symbol table passed in.
+ * @constructor
+ */
+gadgets.i18n.DateTimeFormat = function(symbol) {
+  this.symbols_ = symbol;
+};
+
+/**
+ * regular expression pattern for parsing pattern string
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.TOKENS_ = [
+  //quote string
+  /^\'(?:[^\']|\'\')*\'/,
+  // pattern chars
+  /^(?:G+|y+|M+|k+|S+|E+|a+|h+|K+|H+|c+|L+|Q+|d+|m+|s+|v+|z+|Z+)/,
+  // and all the other chars
+  /^[^\'GyMkSEahKHcLQdmsvzZ]+/  // and all the other chars
+];
+
+/**
+ * These are token types, corresponding to above token definitions.
+ * @enum {number}
+ */
+gadgets.i18n.DateTimeFormat.PartTypes = {
+  QUOTED_STRING: 0,
+  FIELD: 1,
+  LITERAL: 2
+};
+
+/**
+ * Pads number to given length and optionally rounds it to a given precision.
+ * For example:
+ * <pre>padNumber(1.25, 2, 3) -> '01.250'
+ * padNumber(1.25, 2) -> '01.25'
+ * padNumber(1.25, 2, 1) -> '01.3'
+ * padNumber(1.25, 0) -> '1.25'</pre>
+ *
+ * @param {number} num The number to pad.
+ * @param {number} length The desired length.
+ * @return {string} {@code num} as a string with the given options.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.padNumber_ = function(num, length) {
+  var s = String(num);
+  var index = s.indexOf('.');
+  if (index == -1) {
+    index = s.length;
+  }
+  var tempArray = new Array(Math.max(0, length - index) + 1);
+  return tempArray.join('0') + s;
+};
+
+/**
+ * Apply specified pattern to this formatter object.
+ * @param {string} pattern String specifying how the date should be formatted.
+ */
+gadgets.i18n.DateTimeFormat.prototype.applyPattern = function(pattern) {
+  this.patternParts_ = [];
+
+  // lex the pattern, once for all uses
+  while (pattern) {
+    for (var i = 0; i < gadgets.i18n.DateTimeFormat.TOKENS_.length; ++i) {
+      var m = pattern.match(gadgets.i18n.DateTimeFormat.TOKENS_[i]);
+      if (m) {
+        var part = m[0];
+        pattern = pattern.substring(part.length);
+        if (i == gadgets.i18n.DateTimeFormat.PartTypes.QUOTED_STRING) {
+          if (part == "''") {
+            part = "'";  // '' -> '
+          } else {
+            // strip quotes
+            part = part.substring(1, part.length - 1);
+            part = part.replace(/\'\'/, "'");
+          }
+        }
+        this.patternParts_.push({ text: part, type: i });
+        break;
+      }
+    }
+  }
+};
+
+/**
+ * Format the given date object according to preset pattern and current lcoale.
+ * @param {Date} date The Date object that is being formatted.
+ * @return {string} Formatted string for the given date.
+ */
+gadgets.i18n.DateTimeFormat.prototype.format = function(date) {
+  /*  if (!opt_timeZone) {
+        opt_timeZone =
+          gadgets.i18n.TimeZone.createTimeZone(date.getTimezoneOffset());
+      }
+
+      // We don't want to write code to calculate each date field because we
+      // want to maximize performance and minimize code size.
+      // JavaScript only provide API to render local time.
+      // Suppose target date is: 16:00 GMT-0400
+      // OS local time is:       12:00 GMT-0800
+      // We want to create a Local Date Object : 16:00 GMT-0800, and fix the
+      // time zone display ourselves.
+      // Thing get a little bit tricky when daylight time transition happens. For
+      // example, suppose OS timeZone is America/Los_Angeles, it is impossible to
+      // represent "2006/4/2 02:30" even for those timeZone that has no transition
+      // at this time. Because 2:00 to 3:00 on that day does not exising in
+      // America/Los_Angeles time zone. To avoid calculating date field through
+      // our own code, we uses 3 Date object instead, one for "Year, month, day",
+      // one for time within that day, and one for timeZone object since it need
+      // the real time to figure out actual time zone offset.
+      var diff = (date.getTimezoneOffset() - opt_timeZone.getOffset(date)) * 60000;
+      var dateForDate = diff ? new Date(date.getTime() + diff) : date;
+      var dateForTime = dateForDate;
+      // in daylight time switch on/off hour, diff adjustment could alter time
+      // because of timeZone offset change, move 1 day forward or backward.
+      if (dateForDate.getTimezoneOffset() != date.getTimezoneOffset()) {
+        diff += diff > 0 ? -24 * 60 * 60000 : 24 * 60 * 60000;
+        dateForTime = new Date(date.getTime() + diff);
+      }
+    */
+  var out = [];
+  for (var i = 0; i < this.patternParts_.length; ++i) {
+    var text = this.patternParts_[i].text;
+    if (gadgets.i18n.DateTimeFormat.PartTypes.FIELD ==
+            this.patternParts_[i].type) {
+      out.push(this.formatField_(text, date));
+    } else {
+      out.push(text);
+    }
+  }
+  return out.join('');
+};
+
+/**
+ * Apply a predefined pattern as identified by formatType, which is stored in
+ * locale specific repository.
+ * @param {number} formatType A number that identified the predefined pattern.
+ */
+gadgets.i18n.DateTimeFormat.prototype.applyStandardPattern =
+    function(formatType) {
+  var pattern;
+  if (formatType < 4) {
+    pattern = this.symbols_.DATEFORMATS[formatType];
+  } else if (formatType < 8) {
+    pattern = this.symbols_.TIMEFORMATS[formatType - 4];
+  } else if (formatType < 12) {
+    pattern = this.symbols_.DATEFORMATS[formatType - 8] +
+                  ' ' + this.symbols_.TIMEFORMATS[formatType - 8];
+  } else {
+    this.applyStandardPattern(gadgets.i18n.MEDIUM_DATETIME_FORMAT);
+  }
+  return this.applyPattern(pattern);
+};
+
+/**
+ * Formats Era field according to pattern specified.
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatEra_ = function(count, date) {
+  var value = date.getFullYear() > 0 ? 1 : 0;
+  return count >= 4 ? this.symbols_.ERANAMES[value] : this.symbols_.ERAS[value];
+};
+
+/**
+ * Formats Year field according to pattern specified
+ *   Javascript Date object seems incapable handling 1BC and
+ *   year before. It can show you year 0 which does not exists.
+ *   following we just keep consistent with javascript's
+ *   toString method. But keep in mind those things should be
+ *   unsupported.
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatYear_ = function(count, date) {
+  var value = date.getFullYear();
+  if (value < 0) {
+    value = -value;
+  }
+  return count == 2 ?
+      gadgets.i18n.DateTimeFormat.padNumber_(value % 100, 2) :
+      String(value);
+};
+
+/**
+ * Formats Month field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatMonth_ = function(count, date) {
+  var value = date.getMonth();
+  switch (count) {
+    case 5: return this.symbols_.NARROWMONTHS[value];
+    case 4: return this.symbols_.MONTHS[value];
+    case 3: return this.symbols_.SHORTMONTHS[value];
+    default:
+      return gadgets.i18n.DateTimeFormat.padNumber_(value + 1, count);
+  }
+};
+
+/**
+ * Formats (1..24) Hours field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats. This controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.format24Hours_ = function(count, date) {
+  return gadgets.i18n.DateTimeFormat.padNumber_(date.getHours() || 24, count);
+};
+
+/**
+ * Formats Fractional seconds field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ *
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatFractionalSeconds_ =
+    function(count, date) {
+  // Fractional seconds left-justify, append 0 for precision beyond 3
+  var value = date.getTime() % 1000 / 1000;
+  return value.toFixed(Math.min(3, count)).substr(2) +
+      (count > 3 ? gadgets.i18n.DateTimeFormat.padNumber_(0, count - 3) : '');
+};
+
+/**
+ * Formats Day of week field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatDayOfWeek_ = function(count, date) {
+  var value = date.getDay();
+  return count >= 4 ? this.symbols_.WEEKDAYS[value] :
+      this.symbols_.SHORTWEEKDAYS[value];
+};
+
+/**
+ * Formats Am/Pm field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatAmPm_ = function(count, date) {
+  var hours = date.getHours();
+  return this.symbols_.AMPMS[hours >= 12 && hours < 24 ? 1 : 0];
+};
+
+/**
+ * Formats (1..12) Hours field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.format1To12Hours_ = function(count, date) {
+  return gadgets.i18n.DateTimeFormat.padNumber_(date.getHours() % 12 || 12, count);
+};
+
+/**
+ * Formats (0..11) Hours field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.format0To11Hours_ = function(count, date) {
+  return gadgets.i18n.DateTimeFormat.padNumber_(date.getHours() % 12, count);
+};
+
+/**
+ * Formats (0..23) Hours field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.format0To23Hours_ = function(count, date) {
+  return gadgets.i18n.DateTimeFormat.padNumber_(date.getHours(), count);
+};
+
+/**
+ * Formats Standalone weekday field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatStandaloneDay_ =
+    function(count, date) {
+  var value = date.getDay();
+  switch (count) {
+    case 5:
+      return this.symbols_.STANDALONENARROWWEEKDAYS[value];
+    case 4:
+      return this.symbols_.STANDALONEWEEKDAYS[value];
+    case 3:
+      return this.symbols_.STANDALONESHORTWEEKDAYS[value];
+    default:
+      return gadgets.i18n.DateTimeFormat.padNumber_(value, 1);
+  }
+};
+
+/**
+ * Formats Standalone Month field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatStandaloneMonth_ =
+    function(count, date) {
+  var value = date.getMonth();
+  switch (count) {
+    case 5:
+      return this.symbols_.STANDALONENARROWMONTHS[value];
+    case 4:
+      return this.symbols_.STANDALONEMONTHS[value];
+    case 3:
+      return this.symbols_.STANDALONESHORTMONTHS[value];
+    default:
+      return gadgets.i18n.DateTimeFormat.padNumber_(value + 1, count);
+  }
+};
+
+/**
+ * Formats Quarter field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatQuarter_ = function(count, date) {
+  var value = Math.floor(date.getMonth() / 3);
+  return count < 4 ? this.symbols_.SHORTQUARTERS[value] :
+      this.symbols_.QUARTERS[value];
+};
+
+/**
+ * Formats Date field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatDate_ = function(count, date) {
+  return gadgets.i18n.DateTimeFormat.padNumber_(date.getDate(), count);
+};
+
+/**
+ * Formats Minutes field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatMinutes_ = function(count, date) {
+  return gadgets.i18n.DateTimeFormat.padNumber_(date.getMinutes(), count);
+};
+
+/**
+ * Formats Seconds field according to pattern specified
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatSeconds_ = function(count, date) {
+  return gadgets.i18n.DateTimeFormat.padNumber_(date.getSeconds(), count);
+};
+
+/**
+ * Formats TimeZone field following RFC
+ *
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date It holds the date object to be formatted.
+ * @param {gadgets.i18n.TimeZone} timeZone holds current time zone info.
+ * @return {string} Formatted string that represent this field.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatTimeZoneRFC_ =
+    function(count, date) {
+  if (count < 4) {
+    // 'short' (standard Java) form, must use ASCII digits
+    var val = date.getTimezoneOffset();
+    var sign = '-';
+    if (val < 0) {
+      val = -val;
+      sign = '+';
+    }
+    val = val / 3 * 5 + val % 60;
+    // minutes => KKmm
+    return sign + gadgets.i18n.DateTimeFormat.padNumber_(val, 4);
+  }
+  return this.formatGMT_(count, date);
+};
+
+/**
+ * Generate GMT timeZone string for given date
+ * @param {number} count Number of time pattern char repeats, it controls
+ *     how a field should be formatted.
+ * @param {Date} date Whose value being evaluated.
+ * @return {string} GMT timeZone string.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatGMT_ = function(count, date) {
+  var value = date.getTimezoneOffset();
+  var out = [];
+  if (value > 0) {
+    out.push('GMT-');
+  } else {
+    value = -value;
+    out.push('GMT+');
+  }
+
+  out.push(gadgets.i18n.DateTimeFormat.padNumber_(value / 60, 2));
+  out.push(':');
+  out.push(gadgets.i18n.DateTimeFormat.padNumber_(value % 60, 2));
+  return out.join('');
+};
+
+/**
+ * Formatting one date field.
+ * @param {string} patternStr The pattern string for the field being formatted.
+ * @param {Date} date The Date object whose field will be formatted.
+ * @private
+ */
+gadgets.i18n.DateTimeFormat.prototype.formatField_ = function(patternStr, date) {
+  var count = patternStr.length;
+  switch (patternStr.charAt(0)) {
+    case 'G': return this.formatEra_(count, date);
+    case 'y': return this.formatYear_(count, date);
+    case 'M': return this.formatMonth_(count, date);
+    case 'k': return this.format24Hours_(count, date);
+    case 'S': return this.formatFractionalSeconds_(count, date);
+    case 'E': return this.formatDayOfWeek_(count, date);
+    case 'a': return this.formatAmPm_(count, date);
+    case 'h': return this.format1To12Hours_(count, date);
+    case 'K': return this.format0To11Hours_(count, date);
+    case 'H': return this.format0To23Hours_(count, date);
+    case 'c': return this.formatStandaloneDay_(count, date);
+    case 'L': return this.formatStandaloneMonth_(count, date);
+    case 'Q': return this.formatQuarter_(count, date);
+    case 'd': return this.formatDate_(count, date);
+    case 'm': return this.formatMinutes_(count, date);
+    case 's': return this.formatSeconds_(count, date);
+    case 'v': return this.formatGMT_(count, date);
+    case 'z': return this.formatGMT_(count, date);
+    case 'Z': return this.formatTimeZoneRFC_(count, date);
+    default: return '';
+  }
+};
diff --git a/trunk/features/src/main/javascript/features/i18n/datetimeformattest.js b/trunk/features/src/main/javascript/features/i18n/datetimeformattest.js
new file mode 100644
index 0000000..19eb4e0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/datetimeformattest.js
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Unit Tests - gadgets.i18n.DateTimeFormat.
+ */
+
+function DateTimeFormatTest(name) {
+  TestCase.call(this, name);
+}
+
+DateTimeFormatTest.inherits(TestCase);
+
+var DateTimeConstants_de = {
+  ERAS: ['v. Chr.', 'n. Chr.'],
+  ERANAMES: ['v. Chr.', 'n. Chr.'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['Januar', 'Februar', 'M\u00E4rz', 'April', 'Mai', 'Juni', 'Juli',
+    'August', 'September', 'Oktober', 'November', 'Dezember'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep',
+    'Okt', 'Nov', 'Dez'],
+  WEEKDAYS: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag',
+    'Freitag', 'Samstag'],
+  SHORTWEEKDAYS: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'],
+  NARROWWEEKDAYS: ['S', 'M', 'D', 'M', 'D', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1. Quartal', '2. Quartal', '3. Quartal', '4. Quartal'],
+  AMPMS: ['vorm.', 'nachm.'],
+  DATEFORMATS: ['EEEE, d. MMMM yyyy', 'd. MMMM yyyy', 'dd.MM.yyyy',
+    'dd.MM.yy'],
+  TIMEFORMATS: ["H:mm' Uhr 'z", 'HH:mm:ss z', 'HH:mm:ss', 'HH:mm'],
+  ZONESTRINGS: null
+};
+
+DateTimeConstants_de.STANDALONENARROWMONTHS =
+    DateTimeConstants_de.NARROWMONTHS;
+DateTimeConstants_de.STANDALONEMONTHS =
+    DateTimeConstants_de.MONTHS;
+DateTimeConstants_de.STANDALONESHORTMONTHS =
+    DateTimeConstants_de.SHORTMONTHS;
+DateTimeConstants_de.STANDALONEWEEKDAYS = DateTimeConstants_de.WEEKDAYS;
+DateTimeConstants_de.STANDALONESHORTWEEKDAYS =
+    DateTimeConstants_de.SHORTWEEKDAYS;
+DateTimeConstants_de.STANDALONENARROWWEEKDAYS =
+    DateTimeConstants_de.NARROWWEEKDAYS;
+
+// Helpers to make tests work regardless of the timeZone we're in.
+function timezoneString(date) {
+  return (new gadgets.i18n.DateTimeFormat(DateTimeConstants_de)).formatGMT_(1, date);
+}
+
+function timezoneStringRFC(date) {
+  return (new gadgets.i18n.DateTimeFormat(DateTimeConstants_de)).formatTimeZoneRFC_(1, date);
+}
+
+DateTimeFormatTest.prototype.setUp = function() {
+  gadgets.i18n.dtFormatter_
+            = new gadgets.i18n.DateTimeFormat(DateTimeConstants_de);
+};
+
+DateTimeFormatTest.prototype.testHHmmss = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('13:10:10',
+      gadgets.i18n.formatDateTime('HH:mm:ss', date));
+};
+
+DateTimeFormatTest.prototype.testhhmmssa = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('1:10:10 nachm.',
+      gadgets.i18n.formatDateTime('h:mm:ss a', date));
+};
+
+DateTimeFormatTest.prototype.testEEEMMMddyy = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('Do, Jul 27, 06',
+      gadgets.i18n.formatDateTime('EEE, MMM d, yy', date));
+};
+
+DateTimeFormatTest.prototype.testEEEEMMMddyy = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('Donnerstag,Juli 27, 2006',
+      gadgets.i18n.formatDateTime('EEEE,MMMM dd, yyyy', date));
+};
+
+DateTimeFormatTest.prototype.testyyyyMMddG = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('2006.07.27 n. Chr. at 13:10:10 ' + timezoneString(date),
+      gadgets.i18n.formatDateTime("yyyy.MM.dd G 'at' HH:mm:ss vvvv",
+                    date));
+};
+
+DateTimeFormatTest.prototype.testyyyyyMMMMM = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('2006.J.27 n. Chr. 01:10 nachm.',
+      gadgets.i18n.formatDateTime('yyyyy.MMMMM.dd GGG hh:mm aaa', date));
+};
+
+DateTimeFormatTest.prototype.testQQQQyy = function() {
+  var date = new Date(2006, 0, 27, 13, 10, 10, 250);
+  this.assertEquals('1. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 1, 27, 13, 10, 10, 250);
+  this.assertEquals('1. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 2, 27, 13, 10, 10, 250);
+  this.assertEquals('1. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 3, 27, 13, 10, 10, 250);
+  this.assertEquals('2. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 4, 27, 13, 10, 10, 250);
+  this.assertEquals('2. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 5, 27, 13, 10, 10, 250);
+  this.assertEquals('2. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('3. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 7, 27, 13, 10, 10, 250);
+  this.assertEquals('3. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 8, 27, 13, 10, 10, 250);
+  this.assertEquals('3. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 9, 27, 13, 10, 10, 250);
+  this.assertEquals('4. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 10, 27, 13, 10, 10, 250);
+  this.assertEquals('4. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+  date = new Date(2006, 11, 27, 13, 10, 10, 250);
+  this.assertEquals('4. Quartal 06',
+      gadgets.i18n.formatDateTime('QQQQ yy', date));
+};
+
+DateTimeFormatTest.prototype.testQQyyyy = function() {
+  var date = new Date(2006, 0, 27, 13, 10, 10, 250);
+  this.assertEquals('Q1 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 1, 27, 13, 10, 10, 250);
+  this.assertEquals('Q1 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 2, 27, 13, 10, 10, 250);
+  this.assertEquals('Q1 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 3, 27, 13, 10, 10, 250);
+  this.assertEquals('Q2 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 4, 27, 13, 10, 10, 250);
+  this.assertEquals('Q2 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 5, 27, 13, 10, 10, 250);
+  this.assertEquals('Q2 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('Q3 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 7, 27, 13, 10, 10, 250);
+  this.assertEquals('Q3 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 8, 27, 13, 10, 10, 250);
+  this.assertEquals('Q3 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 9, 27, 13, 10, 10, 250);
+  this.assertEquals('Q4 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 10, 27, 13, 10, 10, 250);
+  this.assertEquals('Q4 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+  date = new Date(2006, 11, 27, 13, 10, 10, 250);
+  this.assertEquals('Q4 2006', gadgets.i18n.formatDateTime('QQ yyyy', date));
+};
+
+DateTimeFormatTest.prototype.testMMddyyyyHHmmsszzz = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('07/27/2006 13:10:10 ' + timezoneString(date),
+      gadgets.i18n.formatDateTime('MM/dd/yyyy HH:mm:ss zzz', date));
+};
+
+DateTimeFormatTest.prototype.testMMddyyyyHHmmssZ = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals('07/27/2006 13:10:10 ' + timezoneStringRFC(date),
+      gadgets.i18n.formatDateTime('MM/dd/yyyy HH:mm:ss Z', date));
+};
+
+DateTimeFormatTest.prototype.testQuote = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 250);
+  this.assertEquals("13 o'clock",
+      gadgets.i18n.formatDateTime("HH 'o''clock'", date));
+  this.assertEquals('13 oclock',
+      gadgets.i18n.formatDateTime("HH 'oclock'", date));
+  this.assertEquals("13 '", gadgets.i18n.formatDateTime("HH ''", date));
+};
+
+DateTimeFormatTest.prototype.testPredefinedFormat = function() {
+  var date = new Date(2006, 7, 4, 13, 49, 24, 000);
+  this.assertEquals('Freitag, 4. August 2006',
+      gadgets.i18n.formatDateTime(gadgets.i18n.FULL_DATE_FORMAT, date));
+  this.assertEquals('4. August 2006',
+      gadgets.i18n.formatDateTime(gadgets.i18n.LONG_DATE_FORMAT, date));
+  this.assertEquals('04.08.2006',
+      gadgets.i18n.formatDateTime(gadgets.i18n.MEDIUM_DATE_FORMAT, date));
+  this.assertEquals('04.08.06',
+      gadgets.i18n.formatDateTime(gadgets.i18n.SHORT_DATE_FORMAT, date));
+  this.assertEquals('13:49 Uhr ' + timezoneString(date),
+      gadgets.i18n.formatDateTime(gadgets.i18n.FULL_TIME_FORMAT, date));
+  this.assertEquals('13:49:24 ' + timezoneString(date),
+      gadgets.i18n.formatDateTime(gadgets.i18n.LONG_TIME_FORMAT, date));
+  this.assertEquals('13:49:24',
+      gadgets.i18n.formatDateTime(gadgets.i18n.MEDIUM_TIME_FORMAT, date));
+  this.assertEquals('13:49',
+      gadgets.i18n.formatDateTime(gadgets.i18n.SHORT_TIME_FORMAT, date));
+  this.assertEquals('Freitag, 4. August 2006 13:49 Uhr '
+            + timezoneString(date),
+      gadgets.i18n.formatDateTime(gadgets.i18n.FULL_DATETIME_FORMAT,
+                    date));
+  this.assertEquals('4. August 2006 13:49:24 ' + timezoneString(date),
+      gadgets.i18n.formatDateTime(gadgets.i18n.LONG_DATETIME_FORMAT,
+                    date));
+  this.assertEquals('04.08.2006 13:49:24',
+      gadgets.i18n.formatDateTime(gadgets.i18n.MEDIUM_DATETIME_FORMAT,
+                    date));
+  this.assertEquals('04.08.06 13:49',
+      gadgets.i18n.formatDateTime(gadgets.i18n.SHORT_DATETIME_FORMAT,
+                    date));
+};
+
+DateTimeFormatTest.prototype.testFractionalSeconds = function() {
+  var date = new Date(2006, 6, 27, 13, 10, 10, 256);
+  this.assertEquals('10:3', gadgets.i18n.formatDateTime('s:S', date));
+  this.assertEquals('10:26', gadgets.i18n.formatDateTime('s:SS', date));
+  this.assertEquals('10:256', gadgets.i18n.formatDateTime('s:SSS', date));
+  this.assertEquals('10:2560', gadgets.i18n.formatDateTime('s:SSSS', date));
+  this.assertEquals('10:25600', gadgets.i18n.formatDateTime('s:SSSSS', date));
+};
diff --git a/trunk/features/src/main/javascript/features/i18n/datetimeparse.js b/trunk/features/src/main/javascript/features/i18n/datetimeparse.js
new file mode 100644
index 0000000..b725e30
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/datetimeparse.js
@@ -0,0 +1,997 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Date/Time parsing library with locale support.
+ */
+
+gadgets.i18n = gadgets.i18n || {};
+
+/**
+ * DateTimeParse is for parsing date in a locale-sensitive manner. It allows
+ * user to use any customized patterns to parse date-time string under certain
+ * locale. Things varies across locales like month name, weekname, field
+ * order, etc.
+ *
+ * This module is the counter-part of DateTimeFormat. They use the same
+ * date/time pattern specification, which is borrowed from ICU/JDK.
+ *
+ * This implementation could parse partial date/time.
+ *
+ * Time Format Syntax: To specify the time format use a time pattern string.
+ * In this pattern, following letters are reserved as pattern letters, which
+ * are defined as the following:
+ *
+ * <pre>
+ * Symbol   Meaning                 Presentation        Example
+ * ------   -------                 ------------        -------
+ * G        era designator          (Text)              AD
+ * y#       year                    (Number)            1996
+ * M        month in year           (Text & Number)     July & 07
+ * d        day in month            (Number)            10
+ * h        hour in am/pm (1~12)    (Number)            12
+ * H        hour in day (0~23)      (Number)            0
+ * m        minute in hour          (Number)            30
+ * s        second in minute        (Number)            55
+ * S        fractional second       (Number)            978
+ * E        day of week             (Text)              Tuesday
+ * D        day in year             (Number)            189
+ * a        am/pm marker            (Text)              PM
+ * k        hour in day (1~24)      (Number)            24
+ * K        hour in am/pm (0~11)    (Number)            0
+ * z        time zone               (Text)              Pacific Standard Time
+ * Z        time zone (RFC 822)     (Number)            -0800
+ * v        time zone (generic)     (Text)              Pacific Time
+ * '        escape for text         (Delimiter)         'Date='
+ * ''       single quote            (Literal)           'o''clock'
+ * </pre>
+ *
+ * The count of pattern letters determine the format. <p>
+ * (Text): 4 or more pattern letters--use full form,
+ *         less than 4--use short or abbreviated form if one exists.
+ *         In parsing, we will always try long format, then short. <p>
+ * (Number): the minimum number of digits. <p>
+ * (Text & Number): 3 or over, use text, otherwise use number. <p>
+ * Any characters that not in the pattern will be treated as quoted text. For
+ * instance, characters like ':', '.', ' ', '#' and '@' will appear in the
+ * resulting time text even they are not embraced within single quotes. In our
+ * current pattern usage, we didn't use up all letters. But those unused
+ * letters are strongly discouraged to be used as quoted text without quote.
+ * That's because we may use other letter for pattern in future. <p>
+ *
+ * Examples Using the US Locale:
+ *
+ * Format Pattern                         Result
+ * --------------                         -------
+ * "yyyy.MM.dd G 'at' HH:mm:ss vvvv" ->>  1996.07.10 AD at 15:08:56 Pacific Time
+ * "EEE, MMM d, ''yy"                ->>  Wed, July 10, '96
+ * "h:mm a"                          ->>  12:08 PM
+ * "hh 'o''clock' a, zzzz"           ->>  12 o'clock PM, Pacific Daylight Time
+ * "K:mm a, vvv"                     ->>  0:00 PM, PT
+ * "yyyyy.MMMMM.dd GGG hh:mm aaa"    ->>  01996.July.10 AD 12:08 PM
+ *
+ * <p> When parsing a date string using the abbreviated year pattern ("yy"),
+ * DateTimeParse must interpret the abbreviated year relative to some
+ * century. It does this by adjusting dates to be within 80 years before and 20
+ * years after the time the parse function is called. For example, using a
+ * pattern of "MM/dd/yy" and a DateTimeParse instance created on Jan 1, 1997,
+ * the string "01/11/12" would be interpreted as Jan 11, 2012 while the string
+ * "05/04/64" would be interpreted as May 4, 1964. During parsing, only
+ * strings consisting of exactly two digits, as defined by {@link
+ * java.lang.Character#isDigit(char)}, will be parsed into the default
+ * century. Any other numeric string, such as a one digit string, a three or
+ * more digit string will be interpreted as its face value.
+ *
+ * <p> If the year pattern does not have exactly two 'y' characters, the year is
+ * interpreted literally, regardless of the number of digits. So using the
+ * pattern "MM/dd/yyyy", "01/11/12" parses to Jan 11, 12 A.D.
+ *
+ * <p> When numeric fields abut one another directly, with no intervening
+ * delimiter characters, they constitute a run of abutting numeric fields. Such
+ * runs are parsed specially. For example, the format "HHmmss" parses the input
+ * text "123456" to 12:34:56, parses the input text "12345" to 1:23:45, and
+ * fails to parse "1234". In other words, the leftmost field of the run is
+ * flexible, while the others keep a fixed width. If the parse fails anywhere in
+ * the run, then the leftmost field is shortened by one character, and the
+ * entire run is parsed again. This is repeated until either the parse succeeds
+ * or the leftmost field is one character in length. If the parse still fails at
+ * that point, the parse of the run fails.
+ *
+ * <p> Now timezone parsing only support GMT:hhmm, GMT:+hhmm, GMT:-hhmm
+ *
+ */
+
+/**
+ * Construct a DateTimeParse object based on current locale by using
+ * the symbol table passed in.
+ * @constructor
+ */
+gadgets.i18n.DateTimeParse = function(symbol) {
+  this.symbols_ = symbol;
+};
+
+/**
+ * Year.
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.prototype.year = 0;
+
+
+/**
+ * Month.
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.prototype.month = 0;
+
+
+/**
+ * Day of month.
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.prototype.dayOfMonth = 0;
+
+
+/**
+ * Hours.
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.prototype.hours = 0;
+
+
+/**
+ * Minutes.
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.prototype.minutes = 0;
+
+
+/**
+ * Seconds.
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.prototype.seconds = 0;
+
+
+/**
+ * Milliseconds.
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.prototype.milliseconds = 0;
+
+
+/**
+ * Number of years prior to now that the century used to
+ * disambiguate two digit years will begin
+ *
+ * @type {number}
+ */
+gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart = 80;
+
+/**
+ * Apply a pattern to this Parser. The pattern string will be parsed and saved
+ * in "compiled" form.
+ * Note: this method is somewhat similar to the pattern parsing methold in
+ *       datetimeformat. If you see something wrong here, you might want
+ *       to check the other.
+ * @param {string} pattern It describes the format of date string that need to
+ *     be parsed.
+ */
+gadgets.i18n.DateTimeParse.prototype.applyPattern = function(pattern) {
+  this.patternParts_ = [];
+  var inQuote = false;
+  var buf = '';
+
+  for (var i = 0; i < pattern.length; i++) {
+    var ch = pattern.charAt(i);
+
+    // handle space, add literal part (if exist), and add space part
+    if (ch == ' ') {
+      if (buf.length > 0) {
+        this.patternParts_.push({text: buf, count: 0, abutStart: false});
+        buf = '';
+      }
+      this.patternParts_.push({text: ' ', count: 0, abutStart: false});
+      while (i + 1 < pattern.length && pattern.charAt(i + 1) == ' ') {
+        i++;
+      }
+    } else if (inQuote) {
+      // inside quote, except '', just copy or exit
+      if (ch == '\'') {
+        if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
+          // quote appeared twice continuously, interpret as one quote.
+          buf += ch;
+          ++i;
+        } else {
+          // exit quote
+          inQuote = false;
+        }
+      } else {
+        // literal
+        buf += ch;
+      }
+    } else if (gadgets.i18n.DateTimeParse.PATTERN_CHARS_.indexOf(ch) >= 0) {
+      // outside quote, it is a pattern char
+      if (buf.length > 0) {
+        this.patternParts_.push({text: buf, count: 0, abutStart: false});
+        buf = '';
+      }
+      var count = this.getNextCharCount_(pattern, i);
+      this.patternParts_.push({text: ch, count: count, abutStart: false});
+      i += count - 1;
+    } else if (ch == '\'') {
+      // Two consecutive quotes is a quote literal, inside or outside of quotes.
+      if (i + 1 < pattern.length && pattern.charAt(i + 1) == '\'') {
+        buf += "'";
+        i++;
+      } else {
+        inQuote = true;
+      }
+    } else {
+      buf += ch;
+    }
+  }
+
+  if (buf.length > 0) {
+    this.patternParts_.push({text: buf, count: 0, abutStart: false});
+  }
+
+  this.markAbutStart_();
+};
+
+
+/**
+ * Apply a predefined pattern to this Parser.
+ * @param {number} formatType A constant used to identified the predefined
+ *     pattern string stored in locale repository.
+ * @return {void} nothing.
+ */
+gadgets.i18n.DateTimeParse.prototype.applyStandardPattern = function(formatType)
+    {
+  var pattern;
+  // formatType constants are defined in a way so that following resolution is
+  // possible.
+  if (formatType < 4) {
+    pattern = this.symbols_.DATEFORMATS[formatType];
+  } else if (formatType < 8) {
+    pattern = this.symbols_.TIMEFORMATS[formatType - 4];
+  } else if (formatType < 12) {
+    pattern = this.symbols_.DATEFORMATS[formatType - 8];
+    pattern += ' ';
+    pattern += this.symbols_.TIMEFORMATS[formatType - 8];
+  } else {
+    return this.applyStandardPattern(gadgets.i18n.MEDIUM_DATETIME_FORMAT);
+  }
+  return this.applyPattern(pattern);
+};
+
+
+/**
+ * Parse the given string and fill info into date object. This version does
+ * not validate the input.
+ * @param {string} text The string being parsed.
+ * @param {number} start The position from where parse should begin.
+ * @param {Date} date The Date object to hold the parsed date.
+ * @return {number} How many characters parser advanced.
+ */
+gadgets.i18n.DateTimeParse.prototype.parse = function(text, start, date) {
+  return this.internalParse_(text, start, date, false);
+};
+
+
+/**
+ * Parse the given string and fill info into date object.
+ * @param {string} text The string being parsed.
+ * @param {number} start The position from where parse should begin.
+ * @param {Date} date The Date object to hold the parsed date.
+ * @param {boolean} validation If true, input string need to be a valid
+ *     date/time string.
+ * @return {number} How many characters parser advanced.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.internalParse_ =
+    function(text, start, date, validation) {
+  var cal = new gadgets.i18n.DateTimeParse.MyDate_();
+  var parsePos = [start];
+
+  // For parsing abutting numeric fields. 'abutPat' is the
+  // offset into 'pattern' of the first of 2 or more abutting
+  // numeric fields. 'abutStart' is the offset into 'text'
+  // where parsing the fields begins. 'abutPass' starts off as 0
+  // and increments each time we try to parse the fields.
+  var abutPat = -1; // If >=0, we are in a run of abutting numeric fields
+  var abutStart = 0;
+  var abutPass = 0;
+
+  for (var i = 0; i < this.patternParts_.length; ++i) {
+    if (this.patternParts_[i].count > 0) {
+      if (abutPat < 0 && this.patternParts_[i].abutStart) {
+        abutPat = i;
+        abutStart = start;
+        abutPass = 0;
+      }
+
+      // Handle fields within a run of abutting numeric fields. Take
+      // the pattern "HHmmss" as an example. We will try to parse
+      // 2/2/2 characters of the input text, then if that fails,
+      // 1/2/2. We only adjust the width of the leftmost field; the
+      // others remain fixed. This allows "123456" => 12:34:56, but
+      // "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we
+      // try 4/2/2, 3/2/2, 2/2/2, and finally 1/2/2.
+      if (abutPat >= 0) {
+        // If we are at the start of a run of abutting fields, then
+        // shorten this field in each pass. If we can't shorten
+        // this field any more, then the parse of this set of
+        // abutting numeric fields has failed.
+        var count = this.patternParts_[i].count;
+        if (i == abutPat) {
+          count -= abutPass;
+          abutPass++;
+          if (count == 0) {
+            // tried all possible width, fail now
+            return 0;
+          }
+        }
+
+        if (!this.subParse_(text, parsePos, this.patternParts_[i], count,
+            cal)) {
+          // If the parse fails anywhere in the run, back up to the
+          // start of the run and retry.
+          i = abutPat - 1;
+          parsePos[0] = abutStart;
+          continue;
+        }
+      }
+
+    // Handle non-numeric fields and non-abutting numeric fields.
+      else {
+        abutPat = -1;
+        if (!this.subParse_(text, parsePos, this.patternParts_[i], 0, cal)) {
+          return 0;
+        }
+      }
+    } else {
+      // Handle literal pattern characters. These are any
+      // quoted characters and non-alphabetic unquoted
+      // characters.
+      abutPat = -1;
+      // A run of white space in the pattern matches a run
+      // of white space in the input text.
+      if (this.patternParts_[i].text.charAt(0) == ' ') {
+        // Advance over run in input text
+        var s = parsePos[0];
+        this.skipSpace_(text, parsePos);
+
+        // Must see at least one white space char in input
+        if (parsePos[0] > s) {
+          continue;
+        }
+      } else if (text.indexOf(this.patternParts_[i].text, parsePos[0]) ==
+          parsePos[0]) {
+        parsePos[0] += this.patternParts_[i].text.length;
+        continue;
+      }
+      // We fall through to this point if the match fails
+      return 0;
+    }
+  }
+
+  // return progress
+  return cal.calcDate_(date, validation) ? parsePos[0] - start : 0;
+};
+
+/**
+ * Calculate character repeat count in pattern.
+ *
+ * @param {string} pattern It describes the format of date string that need to
+ *     be parsed.
+ * @param {number} start the position of pattern character.
+ *
+ * @return {number} Repeat count.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.getNextCharCount_ =
+    function(pattern, start) {
+  var ch = pattern.charAt(start);
+  var next = start + 1;
+  while (next < pattern.length && pattern.charAt(next) == ch) {
+    ++next;
+  }
+  return next - start;
+};
+
+/**
+ * All acceptable pattern characters.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.PATTERN_CHARS_ = 'GyMdkHmsSEDahKzZv';
+
+/**
+ * Pattern characters that specify numerical field.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_ = 'MydhHmsSDkK';
+
+/**
+ * Check if the pattern part is a numeric field.
+ *
+ * @param {Object} part pattern part to be examined.
+ *
+ * @return {boolean} true if the pattern part is numberic field.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.isNumericField_ = function(part) {
+  if (part.count <= 0) {
+    return false;
+  }
+  var i = gadgets.i18n.DateTimeParse.NUMERIC_FORMAT_CHARS_.indexOf(
+      part.text.charAt(0));
+  return i > 0 || i == 0 && part.count < 3;
+};
+
+
+/**
+ * Identify the start of an abutting numeric fields' run. Taking pattern
+ * "HHmmss" as an example. It will try to parse 2/2/2 characters of the input
+ * text, then if that fails, 1/2/2. We only adjust the width of the leftmost
+ * field; the others remain fixed. This allows "123456" => 12:34:56, but
+ * "12345" => 1:23:45. Likewise, for the pattern "yyyyMMdd" we try 4/2/2,
+ * 3/2/2, 2/2/2, and finally 1/2/2. The first field of connected numeric
+ * fields will be marked as abutStart, its width can be reduced to accomodate
+ * others.
+ *
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.markAbutStart_ = function() {
+  // abut parts are continuous numeric parts. abutStart is the switch
+  // point from non-abut to abut
+  var abut = false;
+
+  for (var i = 0; i < this.patternParts_.length; i++) {
+    if (this.isNumericField_(this.patternParts_[i])) {
+      // if next part is not following abut sequence, and isNumericField_
+      if (!abut && i + 1 < this.patternParts_.length &&
+          this.isNumericField_(this.patternParts_[i + 1])) {
+        abut = true;
+        this.patternParts_[i].abutStart = true;
+      }
+    } else {
+      abut = false;
+    }
+  }
+};
+
+
+/**
+ * Skip space in the string.
+ *
+ * @param {string} text input string.
+ * @param {Array} pos where skip start, and return back where the skip stops.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.skipSpace_ = function(text, pos) {
+  var m = text.substring(pos[0]).match(/^\s+/);
+  if (m) {
+    pos[0] += m[0].length;
+  }
+};
+
+/**
+ * Protected method that converts one field of the input string into a
+ * numeric field value.
+ *
+ * @param {string} text the time text to be parsed.
+ * @param {Array} pos Parse position.
+ * @param {Object} part the pattern part for this field.
+ * @param {number} digitCount when > 0, numeric parsing must obey the count.
+ * @param {Object} cal MyDate_ object that will hold parsed value.
+ *
+ * @return {boolean} True if it parses successfully.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.subParse_ =
+    function(text, pos, part, digitCount, cal) {
+  this.skipSpace_(text, pos);
+
+  var start = pos[0];
+  var ch = part.text.charAt(0);
+
+  // parse integer value if it is a numeric field
+  var value = -1;
+  if (this.isNumericField_(part)) {
+    if (digitCount > 0) {
+      if ((start + digitCount) > text.length) {
+        return false;
+      }
+      value = this.parseInt_(
+          text.substring(0, start + digitCount), pos);
+    } else {
+      value = this.parseInt_(text, pos);
+    }
+  }
+
+  switch (ch) {
+    case 'G': // ERA
+      cal.era = this.matchString_(text, pos, this.symbols_.ERAS);
+      return true;
+    case 'M': // MONTH
+      return this.subParseMonth_(text, pos, cal, value);
+    case 'E':
+      return this.subParseDayOfWeek_(text, pos, cal);
+    case 'a': // AM_PM
+      cal.ampm = this.matchString_(text, pos, this.symbols_.AMPMS);
+      return true;
+    case 'y': // YEAR
+      return this.subParseYear_(text, pos, start, value, part, cal);
+    case 'd': // DATE
+      cal.day = value;
+      return true;
+    case 'S': // FRACTIONAL_SECOND
+      return this.subParseFractionalSeconds_(value, pos, start, cal);
+    case 'h': // HOUR (1..12)
+      if (value == 12) {
+        value = 0;
+      }
+    case 'K': // HOUR (0..11)
+    case 'H': // HOUR_OF_DAY (0..23)
+    case 'k': // HOUR_OF_DAY (1..24)
+      cal.hours = value;
+      return true;
+    case 'm': // MINUTE
+      cal.minutes = value;
+      return true;
+    case 's': // SECOND
+      cal.seconds = value;
+      return true;
+
+    case 'z': // ZONE_OFFSET
+    case 'Z': // TIMEZONE_RFC
+    case 'v': // TIMEZONE_GENERIC
+      return this.subparseTimeZoneInGMT_(text, pos, cal);
+    default:
+      return false;
+  }
+};
+
+/**
+ * Parse year field. Year field is special because
+ * 1) two digit year need to be resolved.
+ * 2) we allow year to take a sign.
+ * 3) year field participate in abut processing.
+ *
+ * @param {string} text the time text to be parsed.
+ * @param {Array} pos Parse position.
+ * @param {number} start where this field start.
+ * @param {number} value integer value of year.
+ * @param {Object} part the pattern part for this field.
+ * @param {Object} cal MyDate_ object that will hold parsed value.
+ *
+ * @return {boolean} True if successful.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.subParseYear_ =
+    function(text, pos, start, value, part, cal) {
+  var ch;
+  if (value < 0) {
+    //possible sign
+    ch = text.charAt(pos[0]);
+    if (ch != '+' && ch != '-') {
+      return false;
+    }
+    pos[0]++;
+    value = this.parseInt_(text, pos);
+    if (value < 0) {
+      return false;
+    }
+    if (ch == '-') {
+      value = -value;
+    }
+  }
+
+  // only if 2 digit was actually parsed, and pattern say it has 2 digit.
+  if (ch == null && (pos[0] - start) == 2 && part.count == 2) {
+    cal.setTwoDigitYear_(value);
+  } else {
+    cal.year = value;
+  }
+  return true;
+};
+
+/**
+ * Parse Month field.
+ *
+ * @param {string} text the time text to be parsed.
+ * @param {Array} pos Parse position.
+ * @param {Object} cal MyDate_ object that will hold parsed value.
+ * @param {number} value numeric value if this field is expressed using
+ *      numeric pattern, or -1 if not.
+ *
+ * @return {boolean} True if parsing successful.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.subParseMonth_ =
+    function(text, pos, cal, value) {
+  // when month is symbols, i.e., MMM or MMMM, value will be -1
+  if (value < 0) {
+    // Want to be able to parse both short and long forms.
+    // Try count == 4 first:
+    value = this.matchString_(text, pos, this.symbols_.MONTHS);
+    if (value < 0) { // count == 4 failed, now try count == 3
+      value = this.matchString_(text, pos, this.symbols_.SHORTMONTHS);
+    }
+    if (value < 0) {
+      return false;
+    }
+    cal.month = value;
+    return true;
+  } else {
+    cal.month = value - 1;
+    return true;
+  }
+};
+
+/**
+ * Parse Day of week field.
+ * @param {string} text the time text to be parsed.
+ * @param {Array} pos Parse position.
+ * @param {Object} cal MyDate_ object that holds parsed value.
+ *
+ * @return {boolean} True if successful.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.subParseDayOfWeek_ =
+    function(text, pos, cal) {
+  // Handle both short and long forms.
+  // Try count == 4 (DDDD) first:
+  var value = this.matchString_(text, pos, this.symbols_.WEEKDAYS);
+  if (value < 0) {
+    value = this.matchString_(text, pos, this.symbols_.SHORTWEEKDAYS);
+  }
+  if (value < 0) {
+    return false;
+  }
+  cal.dayOfWeek = value;
+  return true;
+};
+
+/**
+ * Parse fractional seconds field.
+ *
+ * @param {number} value parsed numberic value.
+ * @param {Array} pos current parse position.
+ * @param {number} start where this field start.
+ * @param {Object} cal MyDate_ object that holds parsed value.
+ *
+ * @return {boolean} True if successful.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.subParseFractionalSeconds_ =
+    function(value, pos, start, cal) {
+  // Fractional seconds left-justify
+  var len = pos[0] - start;
+  cal.milliseconds = len < 3 ? value * Math.pow(10, 3 - len) :
+      Math.round(value / Math.pow(10, len - 3));
+  return true;
+};
+
+/**
+ * Parse GMT type timezone.
+ *
+ * @param {string} text the time text to be parsed.
+ * @param {Array} pos Parse position.
+ * @param {Object} cal MyDate_ object that holds parsed value.
+ *
+ * @return {boolean} True if successful.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.subparseTimeZoneInGMT_ =
+    function(text, pos, cal) {
+  // First try to parse generic forms such as GMT-07:00. Do this first
+  // in case localized DateFormatZoneData contains the string "GMT"
+  // for a zone; in that case, we don't want to match the first three
+  // characters of GMT+/-HH:MM etc.
+
+  // For time zones that have no known names, look for strings
+  // of the form:
+  //    GMT[+-]hours:minutes or
+  //    GMT[+-]hhmm or
+  //    GMT.
+  if (text.indexOf('GMT', pos[0]) == pos[0]) {
+    pos[0] += 3;  // 3 is the length of GMT
+    return this.parseTimeZoneOffset_(text, pos, cal);
+  }
+
+  // TODO(shanjian): check for named time zones by looking through the locale
+  // data from the DateFormatZoneData strings. should parse both short and long
+  // forms.
+  // subParseZoneString(text, start, cal);
+
+  // As a last resort, look for numeric timezones of the form
+  // [+-]hhmm as specified by RFC 822.  This code is actually
+  // a little more permissive than RFC 822.  It will try to do
+  // its best with numbers that aren't strictly 4 digits long.
+  return this.parseTimeZoneOffset_(text, pos, cal);
+};
+
+/**
+ * Parse time zone offset.
+ *
+ * @param {string} text the time text to be parsed.
+ * @param {Array} pos Parse position.
+ * @param {Object} cal MyDate_ object that holds parsed value.
+ *
+ * @return {boolean} True if successful.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.parseTimeZoneOffset_ =
+    function(text, pos, cal) {
+  if (pos[0] >= text.length) {
+    cal.tzOffset = 0;
+    return true;
+  }
+
+  var sign = 1;
+  switch (text.charAt(pos[0])) {
+    case '-': sign = -1;  // fall through
+    case '+': pos[0]++;
+  }
+
+  // Look for hours:minutes or hhmm.
+  var st = pos[0];
+  var value = this.parseInt_(text, pos);
+  if (value == 0 && pos[0] == st) {
+    return false;
+  }
+
+  var offset;
+  if (pos[0] < text.length && text.charAt(pos[0]) == ':') {
+    // This is the hours:minutes case
+    offset = value * 60;
+    pos[0]++;
+    st = pos[0];
+    value = this.parseInt_(text, pos);
+    if (value == 0 && pos[0] == st) {
+      return false;
+    }
+    offset += value;
+  } else {
+    // This is the hhmm case.
+    offset = value;
+    // Assume "-23".."+23" refers to hours.
+    if (offset < 24 && (pos[0] - st) <= 2)
+      offset *= 60;
+    else;
+    // todo: this looks questionable, should have more error checking
+    offset = offset % 100 + offset / 100 * 60;
+  }
+
+  offset *= sign;
+  cal.tzOffset = -offset;
+  return true;
+};
+
+/**
+ * Parse a integer string and return integer value.
+ *
+ * @param {string} text string being parsed.
+ * @param {Array} pos parse position.
+ *
+ * @return {number} Converted integer value.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.parseInt_ = function(text, pos) {
+  var m = text.substring(pos[0]).match(/^\d+/);
+  if (!m) {
+    return -1;
+  }
+  pos[0] += m[0].length;
+  return parseInt(m[0], 10);
+};
+
+/**
+ * Attempt to match the text at a given position against an array of strings.
+ * Since multiple strings in the array may match (for example, if the array
+ * contains "a", "ab", and "abc", all will match the input string "abcd") the
+ * longest match is returned.
+ *
+ * @param {string} text The string to match to.
+ * @param {Array} pos parsing position.
+ * @param {Array} data The string array that is used to found match from.
+ *
+ * @return {number} the new start position if matching succeeded; a negative
+ *     number indicating matching failure.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.prototype.matchString_ = function(text, pos, data) {
+  // There may be multiple strings in the data[] array which begin with
+  // the same prefix (e.g., Cerven and Cervenec (June and July) in Czech).
+  // We keep track of the longest match, and return that. Note that this
+  // unfortunately requires us to test all array elements.
+  var bestMatchLength = 0;
+  var bestMatch = -1;
+  var lower_text = text.substring(pos[0]).toLowerCase();
+  for (var i = 0; i < data.length; ++i) {
+    var len = data[i].length;
+    // Always compare if we have no match yet; otherwise only compare
+    // against potentially better matches (longer strings).
+    if (len > bestMatchLength &&
+            lower_text.indexOf(data[i].toLowerCase()) == 0) {
+      bestMatch = i;
+      bestMatchLength = len;
+    }
+  }
+  if (bestMatch >= 0) {
+    pos[0] += bestMatchLength;
+  }
+  return bestMatch;
+};
+
+
+/**
+ * This class hold the intermediate parsing result. After all fields are
+ * consumed, final result will be resolved from this class.
+ * @constructor
+ * @private
+ */
+gadgets.i18n.DateTimeParse.MyDate_ = function() {
+};
+
+/**
+ * 2 digit year special handling. Assuming for example that the
+ * defaultCenturyStart is 6/18/1903. This means that two-digit years will be
+ * forced into the range 6/18/1903 to 6/17/2003. As a result, years 00, 01, and
+ * 02 correspond to 2000, 2001, and 2002. Years 04, 05, etc. correspond
+ * to 1904, 1905, etc. If the year is 03, then it is 2003 if the
+ * other fields specify a date before 6/18, or 1903 if they specify a
+ * date afterwards. As a result, 03 is an ambiguous year. All other
+ * two-digit years are unambiguous.
+ *
+ * @param {number} year 2 digit year value before adjustment.
+ * @return {number} disambiguated year.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.MyDate_.prototype.setTwoDigitYear_ = function(year)
+    {
+  var now = new Date();
+  var defaultCenturyStartYear =
+      now.getFullYear() - gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart;
+  var ambiguousTwoDigitYear = defaultCenturyStartYear % 100;
+  this.ambiguousYear = (year == ambiguousTwoDigitYear);
+  year += Math.floor(defaultCenturyStartYear / 100) * 100 +
+      (year < ambiguousTwoDigitYear ? 100 : 0);
+  return this.year = year;
+};
+
+/**
+ * Based on the fields set, fill a Date object. For those fields that not
+ * set, use the passed in date object's value.
+ *
+ * @param {Date} date Date object to be filled.
+ * @param {boolean} validation If true, input string will be checked to make
+ *     sure it is valid.
+ *
+ * @return {boolean} false if fields specify a invalid date.
+ * @private
+ */
+gadgets.i18n.DateTimeParse.MyDate_.prototype.calcDate_ =
+    function(date, validation) {
+  // year 0 is 1 BC, and so on.
+  if ((typeof this.era != 'undefined') && (typeof this.year != 'undefined') &&
+      this.era == 0 && this.year > 0) {
+    this.year = -(this.year - 1);
+  }
+
+  if (typeof this.year != 'undefined') {
+    date.setFullYear(this.year);
+  }
+
+  // The setMonth and setDate logic is a little tricky. We need to make sure
+  // day of month is smaller enough so that it won't cause a month switch when
+  // setting month. For example, if data in date is Nov 30, when month is set
+  // to Feb, because there is no Feb 30, JS adjust it to Mar 2. So Feb 12 will
+  // become  Mar 12.
+  var org_date = date.getDate();
+  date.setDate(1); // every month has a 1st day, this can actually be anything
+  // less than 29.
+
+  if (typeof this.month != 'undefined') {
+    date.setMonth(this.month);
+  }
+
+  if (typeof this.day != 'undefined') {
+    date.setDate(this.day);
+  } else {
+    date.setDate(org_date);
+  }
+
+  if (typeof this.hours == 'undefined') {
+    this.hours = date.getHours();
+  }
+
+  // adjust ampm
+  if ((typeof this.ampm != 'undefined') && this.ampm > 0) {
+    if (this.hours < 12) {
+      this.hours += 12;
+    }
+  }
+  date.setHours(this.hours);
+
+  if (typeof this.minutes != 'undefined') {
+    date.setMinutes(this.minutes);
+  }
+
+  if (typeof this.seconds != 'undefined') {
+    date.setSeconds(this.seconds);
+  }
+
+  if (typeof this.milliseconds != 'undefined') {
+    date.setMilliseconds(this.milliseconds);
+  }
+
+  // If validation is needed, verify that the uncalculated date fields
+  // match the calculated date fields.  We do this before we set the
+  // timezone offset, which will skew all of the dates.
+  //
+  // Don't need to check the day of week as it is guaranteed to be
+  // correct or return false below.
+  if (validation &&
+      ((typeof this.year != 'undefined') && this.year != date.getFullYear() ||
+      (typeof this.month != 'undefined') && this.month != date.getMonth() ||
+      (typeof this.dayOfMonth != 'undefined') && this.dayOfMonth != date.getDate() ||
+      this.hours >= 24 || this.minutes >= 60 || this.seconds >= 60 ||
+      this.milliseconds >= 1000)) {
+    return false;
+  }
+
+  // adjust time zone
+  if (typeof this.tzOffset != 'undefined') {
+    var offset = date.getTimezoneOffset();
+    date.setTime(date.getTime() + (this.tzOffset - offset) * 60 * 1000);
+  }
+
+  // resolve ambiguous year if needed
+  if (this.ambiguousYear) { // the two-digit year == the default start year
+    var defaultCenturyStart = new Date();
+    defaultCenturyStart.setFullYear(
+        defaultCenturyStart.getFullYear() -
+        gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart);
+    if (date.getTime() < defaultCenturyStart.getTime()) {
+      date.setFullYear(defaultCenturyStart.getFullYear() + 100);
+    }
+  }
+
+  // dayOfWeek, validation only
+  if (typeof this.dayOfWeek != 'undefined') {
+    if (typeof this.day == 'undefined') {
+      // adjust to the nearest day of the week
+      var adjustment = (7 + this.dayOfWeek - date.getDay()) % 7;
+      if (adjustment > 3) {
+        adjustment -= 7;
+      }
+      var orgMonth = date.getMonth();
+      date.setDate(date.getDate() + adjustment);
+
+      // don't let it switch month
+      if (date.getMonth() != orgMonth) {
+        date.setDate(date.getDate() + (adjustment > 0 ? -7 : 7));
+      }
+    } else if (this.dayOfWeek != date.getDay()) {
+      return false;
+    }
+  }
+  return true;
+};
diff --git a/trunk/features/src/main/javascript/features/i18n/datetimeparsetest.js b/trunk/features/src/main/javascript/features/i18n/datetimeparsetest.js
new file mode 100644
index 0000000..221ec8a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/datetimeparsetest.js
@@ -0,0 +1,617 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Unit Tests - gadgets.i18n.DateTimeFormat.
+ */
+
+function DateTimeParseTest(name) {
+  TestCase.call(this, name);
+}
+
+DateTimeParseTest.inherits(TestCase);
+
+var DateTimeConstants_en = {
+  ERAS: ['BC', 'AD'],
+  ERANAMES: ['Before Christ', 'Anno Domini'],
+  NARROWMONTHS: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'],
+  MONTHS: ['January', 'February', 'March', 'April', 'May', 'June', 'July',
+    'August', 'September', 'October', 'November', 'December'],
+  SHORTMONTHS: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
+    'Oct', 'Nov', 'Dec'],
+  WEEKDAYS: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
+    'Saturday'],
+  SHORTWEEKDAYS: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
+  NARROWWEEKDAYS: ['S', 'M', 'T', 'W', 'T', 'F', 'S'],
+  SHORTQUARTERS: ['Q1', 'Q2', 'Q3', 'Q4'],
+  QUARTERS: ['1st quarter', '2nd quarter', '3rd quarter', '4th quarter'],
+  AMPMS: ['AM', 'PM'],
+  DATEFORMATS: ['EEEE, MMMM d, yyyy', 'MMMM d, yyyy', 'MMM d, yyyy', 'M/d/yy'],
+  TIMEFORMATS: ['h:mm:ss a v', 'h:mm:ss a z', 'h:mm:ss a', 'h:mm a'],
+  ZONESTRINGS: null
+};
+
+DateTimeConstants_en.STANDALONENARROWMONTHS =
+    DateTimeConstants_en.NARROWMONTHS;
+DateTimeConstants_en.STANDALONEMONTHS = DateTimeConstants_en.MONTHS;
+DateTimeConstants_en.STANDALONESHORTMONTHS = DateTimeConstants_en.SHORTMONTHS;
+DateTimeConstants_en.STANDALONEWEEKDAYS = DateTimeConstants_en.WEEKDAYS;
+DateTimeConstants_en.STANDALONESHORTWEEKDAYS =
+    DateTimeConstants_en.SHORTWEEKDAYS;
+DateTimeConstants_en.STANDALONENARROWWEEKDAYS =
+    DateTimeConstants_en.NARROWWEEKDAYS;
+
+DateTimeParseTest.prototype.setUp = function() {
+  gadgets.i18n.dtParser_
+            = new gadgets.i18n.DateTimeParse(DateTimeConstants_en);
+};
+
+DateTimeParseTest.prototype.testNegativeYear = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd, yyyy', '11/22, 1999',
+      0, date) > 0);
+  this.assertEquals(1999, date.getFullYear());
+  this.assertEquals(11 - 1, date.getMonth());
+  this.assertEquals(22, date.getDate());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd, yyyy', '11/22, -1999',
+      0, date) > 0);
+  this.assertEquals(-1999, date.getFullYear());
+  this.assertEquals(11 - 1, date.getMonth());
+  this.assertEquals(22, date.getDate());
+};
+
+DateTimeParseTest.prototype.testEra = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd, yyyyG', '11/22, 1999BC',
+      0, date) > 0);
+  this.assertEquals(-1998, date.getFullYear());
+  this.assertEquals(11 - 1, date.getMonth());
+  this.assertEquals(22, date.getDate());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd, yyyyG', '11/22, 1BC',
+      0, date) > 0);
+  this.assertEquals(0, date.getFullYear());
+  this.assertEquals(11 - 1, date.getMonth());
+  this.assertEquals(22, date.getDate());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd, yyyyG', '11/22, 1999AD',
+      0, date) > 0);
+  this.assertEquals(1999, date.getFullYear());
+  this.assertEquals(11 - 1, date.getMonth());
+  this.assertEquals(22, date.getDate());
+};
+
+DateTimeParseTest.prototype.testFractionalSeconds = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hh:mm:ss.SSS', '11:12:13.956',
+      0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(12, date.getMinutes());
+  this.assertEquals(13, date.getSeconds());
+  this.assertEquals(956, date.getTime() % 1000);
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hh:mm:ss.SSS', '11:12:13.95',
+      0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(12, date.getMinutes());
+  this.assertEquals(13, date.getSeconds());
+  this.assertEquals(950, date.getTime() % 1000);
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hh:mm:ss.SSS', '11:12:13.9',
+      0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(12, date.getMinutes());
+  this.assertEquals(13, date.getSeconds());
+  this.assertEquals(900, date.getTime() % 1000);
+};
+
+DateTimeParseTest.prototype.testAmbiguousYear = function() {
+  var date = new Date();
+
+  // assume this year is 2006, year 27 to 99 will be interpret as 1927 to 1999
+  // year 00 to 25 will be 2000 to 2025. Year 26 can be either 1926 or 2026
+  // depend on the exact time.
+  var org_date = new Date();
+  org_date.setFullYear(org_date.getFullYear() + 20);
+
+  // following 2 lines only works in 2006. Keep them here as they explained
+  // our intention better.
+  //assertTrue(DateTimeParse.parse("01/01/26", 0, "MM/dd/yy", date) > 0);
+  //assertTrue(date.getYear() == 2026 - 1900);
+
+  // rewrite so that it works in any year.
+  org_date.setMonth(0);
+  org_date.setDate(1);
+  org_date.setHours(0);
+  org_date.setMinutes(0);
+  org_date.setSeconds(0);
+  org_date.setMilliseconds(1);
+  var str = '01/01/' + (org_date.getFullYear() % 100);
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yy', str, 0, date) > 0);
+  this.assertEquals(org_date.getFullYear(), date.getFullYear());
+
+  // following 2 lines only works in 2006. Keep them here as they explained
+  // our intention better.
+  //assertTrue(DateTimeParse.parse("MM/dd/yy", "12/30/26", 0, date) > 0);
+  //assertTrue(date.getYear() == 1926 - 1900);
+
+  // rewrite so that it works in any year.
+  org_date.setMonth(11);
+  org_date.setDate(31);
+  org_date.setHours(23);
+  org_date.setMinutes(59);
+  org_date.setSeconds(59);
+  org_date.setMilliseconds(999);
+  str = '12/31/' + (org_date.getFullYear() % 100);
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yy', str, 0, date) > 0);
+  this.assertEquals(org_date.getFullYear(), date.getFullYear() + 100);
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('yy,MM,dd', '2097,07,21', 0, date) > 0);
+  this.assertEquals(2097, date.getFullYear());
+
+  // Test the ability to move the disambiguation century
+  gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart = 60;
+
+  org_date.setMonth(0);
+  org_date.setDate(1);
+  org_date.setHours(0);
+  org_date.setMinutes(0);
+  org_date.setSeconds(0);
+  org_date.setMilliseconds(1);
+  str = '01/01/' + (org_date.getFullYear() % 100);
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yy', str, 0, date) > 0);
+
+  this.assertEquals(org_date.getFullYear(), date.getFullYear());
+
+  // Increment org_date 20 more years
+  org_date.setFullYear(date.getFullYear() + 20);
+  str = '01/01/' + (org_date.getFullYear() % 100);
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yy', str, 0, date) > 0);
+  this.assertEquals(org_date.getFullYear(), date.getFullYear());
+
+  org_date.setFullYear(date.getFullYear() + 21);
+  str = '01/01/' + (org_date.getFullYear() % 100);
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yy', str, 0, date) > 0);
+  this.assertEquals(org_date.getFullYear(), date.getFullYear() + 100);
+
+  // Reset parameter for other test cases
+  gadgets.i18n.DateTimeParse.ambiguousYearCenturyStart = 80;
+};
+
+DateTimeParseTest.prototype.testLeapYear = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MMdd, yyyy', '0229, 2001',
+      0, date) > 0);
+  this.assertEquals(3 - 1, date.getMonth());
+  this.assertEquals(1, date.getDate());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MMdd, yyyy', '0229, 2000',
+      0, date) > 0);
+  this.assertEquals(2 - 1, date.getMonth());
+  this.assertEquals(29, date.getDate());
+};
+
+DateTimeParseTest.prototype.testAbutField = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmm', '1122', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmm', '122', 0, date) > 0);
+  this.assertEquals(1, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('hhmmss', '112233', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+  this.assertEquals(33, date.getSeconds());
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('hhmmss', '12233', 0, date) > 0);
+  this.assertEquals(1, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+  this.assertEquals(33, date.getSeconds());
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('yyyyMMdd', '19991202', 0, date) > 0);
+  this.assertEquals(1999, date.getFullYear());
+  this.assertEquals(12 - 1, date.getMonth());
+  this.assertEquals(02, date.getDate());
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('yyyyMMdd', '9991202', 0, date) > 0);
+  this.assertTrue(date.getFullYear() == 999);
+  this.assertEquals(12 - 1, date.getMonth());
+  this.assertEquals(02, date.getDate());
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('yyyyMMdd', '991202', 0, date) > 0);
+  this.assertEquals(99, date.getFullYear());
+  this.assertEquals(12 - 1, date.getMonth());
+  this.assertEquals(02, date.getDate());
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('yyyyMMdd', '91202', 0, date) > 0);
+  this.assertEquals(9, date.getFullYear());
+  this.assertEquals(12 - 1, date.getMonth());
+  this.assertEquals(02, date.getDate());
+};
+
+DateTimeParseTest.prototype.testYearParsing = function() {
+  var date = new Date();
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('yyMMdd', '991202', 0, date) > 0);
+  this.assertEquals(1999, date.getFullYear());
+  this.assertEquals(12 - 1, date.getMonth());
+  this.assertEquals(02, date.getDate());
+
+  this.assertTrue(
+      gadgets.i18n.parseDateTime('yyyyMMdd', '20051202', 0, date) > 0);
+  this.assertEquals(2005, date.getFullYear());
+  this.assertEquals(12 - 1, date.getMonth());
+  this.assertEquals(02, date.getDate());
+};
+
+DateTimeParseTest.prototype.testHourParsing_hh = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmm', '0022', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmm', '1122', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmm', '1222', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmm', '2322', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmm', '2422', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '0022am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '1122am', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '1222am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '2322am', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '2422am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '0022pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '1122pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '1222pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '2322pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('hhmma', '2422pm', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+};
+
+DateTimeParseTest.prototype.testHourParsing_KK = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmm', '0022', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmm', '1122', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmm', '1222', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmm', '2322', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmm', '2422', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '0022am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '1122am', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '1222am', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '2322am', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '2422am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '0022pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '1122pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '1222pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '2322pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('KKmma', '2422pm', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+};
+
+DateTimeParseTest.prototype.testHourParsing_kk = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmm', '0022', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmm', '1122', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmm', '1222', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmm', '2322', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmm', '2422', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '0022am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '1122am', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '1222am', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '2322am', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '2422am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '0022pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '1122pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '1222pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '2322pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('kkmma', '2422pm', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+};
+
+DateTimeParseTest.prototype.testHourParsing_HH = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmm', '0022', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmm', '1122', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmm', '1222', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmm', '2322', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmm', '2422', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '0022am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '1122am', 0, date) > 0);
+  this.assertEquals(11, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '1222am', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '2322am', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '2422am', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '0022pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '1122pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '1222pm', 0, date) > 0);
+  this.assertEquals(12, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '2322pm', 0, date) > 0);
+  this.assertEquals(23, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+
+  this.assertTrue(gadgets.i18n.parseDateTime('HHmma', '2422pm', 0, date) > 0);
+  this.assertEquals(00, date.getHours());
+  this.assertEquals(22, date.getMinutes());
+};
+
+DateTimeParseTest.prototype.testEnglishDate = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('yyyy MMM dd hh:mm',
+      '2006 Jul 10 15:44', 0, date) > 0);
+  this.assertEquals(2006, date.getFullYear());
+  this.assertEquals(7 - 1, date.getMonth());
+  this.assertEquals(10, date.getDate());
+  this.assertEquals(15, date.getHours());
+  this.assertEquals(44, date.getMinutes());
+};
+
+DateTimeParseTest.prototype.testTimeZone = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yyyy, hh:mm:ss zzz',
+      '07/21/2003, 11:22:33 GMT-0700', 0,
+      date) > 0);
+  var hour_GmtMinus07 = date.getHours();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yyyy, hh:mm:ss zzz',
+      '07/21/2003, 11:22:33 GMT-0600', 0,
+      date) > 0);
+  var hour_GmtMinus06 = date.getHours();
+  this.assertEquals(1, (hour_GmtMinus07 + 24 - hour_GmtMinus06) % 24);
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yyyy, hh:mm:ss zzz',
+      '07/21/2003, 11:22:33 GMT-0800', 0,
+      date) > 0);
+  var hour_GmtMinus08 = date.getHours();
+  this.assertEquals(1, (hour_GmtMinus08 + 24 - hour_GmtMinus07) % 24);
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yyyy, HH:mm:ss zzz',
+      '07/21/2003, 23:22:33 GMT-0800', 0,
+      date) > 0);
+  this.assertEquals((date.getHours() + 24 - hour_GmtMinus07) % 24, 13);
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yyyy, HH:mm:ss zzz',
+      '07/21/2003, 11:22:33 GMT+0800', 0,
+      date) > 0);
+  var hour_Gmt08 = date.getHours();
+  this.assertEquals(16, (hour_GmtMinus08 + 24 - hour_Gmt08) % 24);
+
+  this.assertTrue(gadgets.i18n.parseDateTime('MM/dd/yyyy, HH:mm:ss zzz',
+      '07/21/2003, 11:22:33 GMT0800', 0,
+      date) > 0);
+  this.assertEquals(hour_Gmt08, date.getHours());
+};
+
+DateTimeParseTest.prototype.testWeekDay = function() {
+  var date = new Date();
+
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/dd/yyyy',
+      'Wednesday, 08/16/2006', 0, date) > 0);
+  this.assertEquals(2006, date.getFullYear());
+  this.assertEquals(8 - 1, date.getMonth());
+  this.assertEquals(16, date.getDate());
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/dd/yyyy',
+      'Tuesday, 08/16/2006', 0, date) == 0);
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/dd/yyyy',
+      'Thursday, 08/16/2006', 0, date) == 0);
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/dd/yyyy',
+      'Wed, 08/16/2006', 0, date) > 0);
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/dd/yyyy',
+      'Wasdfed, 08/16/2006', 0, date) == 0);
+
+  date.setDate(25);
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/yyyy',
+      'Wed, 09/2006', 0, date) > 0);
+  this.assertEquals(27, date.getDate());
+
+  date.setDate(30);
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/yyyy',
+      'Wed, 09/2006', 0, date) > 0);
+  this.assertEquals(27, date.getDate());
+  date.setDate(30);
+  this.assertTrue(gadgets.i18n.parseDateTime('EEEE, MM/yyyy',
+      'Mon, 09/2006', 0, date) > 0);
+  this.assertEquals(25, date.getDate());
+
+};
diff --git a/trunk/features/src/main/javascript/features/i18n/feature.xml b/trunk/features/src/main/javascript/features/i18n/feature.xml
new file mode 100644
index 0000000..17e652c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/feature.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>opensocial-i18n</name>
+  <dependency>globals</dependency>
+  <gadget>
+    <script src="formatting.js"/>
+    <script src="datetimeformat.js"/>
+    <script src="datetimeparse.js"/>
+    <script src="numberformat.js"/>
+    <script src="currencycodemap.js"/>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/i18n/formatting.js b/trunk/features/src/main/javascript/features/i18n/formatting.js
new file mode 100644
index 0000000..8befa21
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/formatting.js
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Functions for dealing with locale-specific formatting.
+ *
+ * Note: Gadgets locale is set at render time. Gadget containers should emit
+ * the data files required by the i18n feature by emitting
+ * DateTimeConstants__<2 letter language code>_<2 letter country code>.js
+ * and NumberFormatConstants__<2 letter language code>_<2 letter countrycode>.js.
+ * Data files are located at features/i18n/data. Note the _<2 letter country code>
+ * part is optional. The i18n package above will then load the corresponding
+ * formatter/parser for that locale if any of the functions in the package are
+ * invoked.
+ */
+
+gadgets.i18n = gadgets.i18n || {};
+
+gadgets.i18n.dtFormatter_ = null;
+gadgets.i18n.dtParser_ = null;
+gadgets.i18n.numFormatter_ = null;
+
+/**
+ * Format the given date object into a string representation using pattern
+ * specified.
+ * @param {string|number} pattern String to specify patterns or Number used to reference predefined
+ *        pattern that a date should be formatted into.
+ * @param {Date} date Date object being formatted.
+ *
+ * @return {string} string representation of date/time.
+ */
+gadgets.i18n.formatDateTime = function(pattern, date) {
+  if (!gadgets.i18n.dtFormatter_) {
+    gadgets.i18n.dtFormatter_ = new gadgets.i18n.DateTimeFormat(gadgets.i18n.DateTimeConstants);
+    typeof pattern == 'string'
+                ? gadgets.i18n.dtFormatter_.applyPattern(pattern)
+                : gadgets.i18n.dtFormatter_.applyStandardPattern(pattern);
+    gadgets.i18n.dtFormatter_.patternInUse_ = pattern;
+  } else if (gadgets.i18n.dtFormatter_.patternInUse_ != pattern) {
+    typeof pattern == 'string'
+                ? gadgets.i18n.dtFormatter_.applyPattern(pattern)
+                : gadgets.i18n.dtFormatter_.applyStandardPattern(pattern);
+    gadgets.i18n.dtFormatter_.patternInUse_ = pattern;
+  }
+  return gadgets.i18n.dtFormatter_.format(date);
+};
+
+
+/**
+ * Parse a string using the format as specified in pattern string, and
+ * return date in the passed "date" parameter.
+ *
+ * @param {string|number} pattern String to specify patterns or Number used to
+ *        reference predefined
+ *        pattern that a date should be parsed from.
+ * @param {string} text The string that need to be parsed.
+ * @param {number} start The character position in "text" where parse begins.
+ * @param {Date} date The date object that will hold parsed value.
+ *
+ * @return {number} The number of characters advanced or 0 if failed.
+ */
+gadgets.i18n.parseDateTime = function(pattern, text, start, date) {
+  if (!gadgets.i18n.dtParser_) {
+    gadgets.i18n.dtParser_ = new gadgets.i18n.DateTimeParse(gadgets.i18n.DateTimeConstants);
+    typeof pattern == 'string'
+                ? gadgets.i18n.dtParser_.applyPattern(pattern)
+                : gadgets.i18n.dtParser_.applyStandardPattern(pattern);
+    gadgets.i18n.dtParser_.patternInUse_ = pattern;
+  } else if (gadgets.i18n.dtParser_.patternInUse_ != pattern) {
+    typeof pattern == 'string'
+                ? gadgets.i18n.dtParser_.applyPattern(pattern)
+                : gadgets.i18n.dtParser_.applyStandardPattern(pattern);
+    gadgets.i18n.dtParser_.patternInUse_ = pattern;
+  }
+  return gadgets.i18n.dtParser_.parse(text, start, date);
+};
+
+
+/**
+ * Format the number using given pattern.
+ * @param {string|number} pattern String to specify patterns or Number used to
+ *        reference predefined
+ *        pattern that a number should be formatted into.
+ * @param {number} value The number being formatted.
+ * @param {string=} opt_currencyCode optional international currency code, it
+ *     determines the currency code/symbol should be used in format/parse. If
+ *     not given, the currency code for current locale will be used.
+ * @return {string} The formatted string.
+ */
+gadgets.i18n.formatNumber = function(pattern, value, opt_currencyCode) {
+  if (!gadgets.i18n.numFormatter_) {
+    gadgets.i18n.numFormatter_ = new gadgets.i18n.NumberFormat(gadgets.i18n.NumberFormatConstants);
+    typeof pattern == 'string'
+                ? gadgets.i18n.numFormatter_.applyPattern(
+        pattern, opt_currencyCode)
+                : gadgets.i18n.numFormatter_.applyStandardPattern(
+                  pattern, opt_currencyCode);
+    gadgets.i18n.numFormatter_.patternInUse_ = pattern;
+  } else if (gadgets.i18n.numFormatter_.patternInUse_ != pattern) {
+    typeof pattern == 'string'
+                ? gadgets.i18n.numFormatter_.applyPattern(
+        pattern, opt_currencyCode)
+                : gadgets.i18n.numFormatter_.applyStandardPattern(
+                  pattern, opt_currencyCode);
+    gadgets.i18n.numFormatter_.patternInUse_ = pattern;
+  }
+  return gadgets.i18n.numFormatter_.format(value);
+};
+
+
+/**
+ * Parse the given text using specified pattern to get a number.
+ * @param {string|number} pattern String to specify patterns or Number used
+ *        to reference predefined
+ *        pattern that a number should be parsed from.
+ * @param {string} text input text being parsed.
+ * @param {Array=} opt_pos optional one element array that holds position
+ *     information. It tells from where parse should begin. Upon return, it
+ *     holds parse stop position.
+ * @param {string=} opt_currencyCode optional international currency code, it
+ *     determines the currency code/symbol should be used in format/parse. If
+ *     not given, the currency code for current locale will be used.
+ * @return {number} Parsed number, 0 if in error.
+ */
+gadgets.i18n.parseNumber = function(pattern, text, opt_pos, opt_currencyCode) {
+  if (!gadgets.i18n.numFormatter_) {
+    gadgets.i18n.numFormatter_ = new gadgets.i18n.NumberFormat();
+    typeof pattern == 'string'
+                ? gadgets.i18n.numFormatter_.applyPattern(pattern,
+                                                          opt_currencyCode)
+                : gadgets.i18n.numFormatter_.applyStandardPattern(
+                  pattern, opt_currencyCode);
+    gadgets.i18n.numFormatter_.patternInUse_ = pattern;
+    gadgets.i18n.numFormatter_.currencyCodeInUse_ = opt_currencyCode;
+  } else if (gadgets.i18n.numFormatter_.patternInUse_ != pattern ||
+      gadgets.i18n.numFormatter_.currencyCodeInUse_
+                       != opt_currencyCode) {
+    typeof pattern == 'string'
+                ? gadgets.i18n.numFormatter_.applyPattern(pattern,
+                                                          opt_currencyCode)
+                : gadgets.i18n.numFormatter_.applyStandardPattern(
+                  pattern, opt_currencyCode);
+    gadgets.i18n.numFormatter_.patternInUse_ = pattern;
+    gadgets.i18n.numFormatter_.currencyCodeInUse_ = opt_currencyCode;
+  }
+  return gadgets.i18n.numFormatter_.parse(text, opt_pos);
+};
+
+// Couple of constants to represent predefined Date/Time format type.
+
+/**
+ * Format for full representations of dates.
+ * @type {number}
+ */
+gadgets.i18n.FULL_DATE_FORMAT = 0;
+
+
+/**
+ * Format for long representations of dates.
+ * @type {number}
+ */
+gadgets.i18n.LONG_DATE_FORMAT = 1;
+
+
+/**
+ * Format for medium representations of dates.
+ * @type {number}
+ */
+gadgets.i18n.MEDIUM_DATE_FORMAT = 2;
+
+
+/**
+ * Format for short representations of dates.
+ * @type {number}
+ */
+gadgets.i18n.SHORT_DATE_FORMAT = 3;
+
+
+/**
+ * Format for full representations of times.
+ * @type {number}
+ */
+gadgets.i18n.FULL_TIME_FORMAT = 4;
+
+
+/**
+ * Format for long representations of times.
+ * @type {number}
+ */
+gadgets.i18n.LONG_TIME_FORMAT = 5;
+
+
+/**
+ * Format for medium representations of times.
+ * @type {number}
+ */
+gadgets.i18n.MEDIUM_TIME_FORMAT = 6;
+
+
+/**
+ * Format for short representations of times.
+ * @type {number}
+ */
+gadgets.i18n.SHORT_TIME_FORMAT = 7;
+
+
+/**
+ * Format for short representations of datetimes.
+ * @type {number}
+ */
+gadgets.i18n.FULL_DATETIME_FORMAT = 8;
+
+
+/**
+ * Format for short representations of datetimes.
+ * @type {number}
+ */
+gadgets.i18n.LONG_DATETIME_FORMAT = 9;
+
+
+/**
+ * Format for medium representations of datetimes.
+ * @type {number}
+ */
+gadgets.i18n.MEDIUM_DATETIME_FORMAT = 10;
+
+
+/**
+ * Format for short representations of datetimes.
+ * @type {number}
+ */
+gadgets.i18n.SHORT_DATETIME_FORMAT = 11;
+
+
+/**
+ * Predefined number format pattern type. The actual pattern is defined
+ * separately for each locale.
+ */
+
+
+/**
+ * Pattern for decimal numbers.
+ * @type {number}
+ */
+gadgets.i18n.DECIMAL_PATTERN = 1;
+
+
+/**
+ * Pattern for scientific numbers.
+ * @type {number}
+ */
+gadgets.i18n.SCIENTIFIC_PATTERN = 2;
+
+
+/**
+ * Pattern for percentages.
+ * @type {number}
+ */
+gadgets.i18n.PERCENT_PATTERN = 3;
+
+
+/**
+ * Pattern for currency.
+ * @type {number}
+ */
+gadgets.i18n.CURRENCY_PATTERN = 4;
diff --git a/trunk/features/src/main/javascript/features/i18n/numberformat.js b/trunk/features/src/main/javascript/features/i18n/numberformat.js
new file mode 100644
index 0000000..0425e85
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/numberformat.js
@@ -0,0 +1,755 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Number format/parse library with locale support.
+ */
+
+gadgets.i18n = gadgets.i18n || {};
+
+
+/**
+ * Construct a NumberFormat object based on current locale by using
+ * the symbol table passed in.
+ * @constructor
+ */
+gadgets.i18n.NumberFormat = function(symbol) {
+  this.symbols_ = symbol;
+};
+
+/**
+ * Apply a predefined pattern to NumberFormat object.
+ * @param {number} patternType The number that indicates a predefined number
+ *     format pattern.
+ * @param {string=} opt_currency Optional international currency code. This
+ *     determines the currency code/symbol used in format/parse. If not given,
+ *     the currency code for current locale will be used.
+ */
+gadgets.i18n.NumberFormat.prototype.applyStandardPattern =
+    function(patternType, opt_currency) {
+  switch (patternType) {
+    case gadgets.i18n.DECIMAL_PATTERN:
+      this.applyPattern(this.symbols_.DECIMAL_PATTERN, opt_currency);
+      break;
+    case gadgets.i18n.SCIENTIFIC_PATTERN:
+      this.applyPattern(this.symbols_.SCIENTIFIC_PATTERN, opt_currency);
+      break;
+    case gadgets.i18n.PERCENT_PATTERN:
+      this.applyPattern(this.symbols_.PERCENT_PATTERN, opt_currency);
+      break;
+    case gadgets.i18n.CURRENCY_PATTERN:
+      this.applyPattern(this.symbols_.CURRENCY_PATTERN, opt_currency);
+      break;
+    default:
+      throw Error('Unsupported pattern type.');
+  }
+};
+
+
+/**
+ * Apply a pattern to NumberFormat object.
+ * @param {string} pattern The number format pattern string.
+ * @param {string=} opt_currency Optional international currency code. This
+ *     determines the currency code/symbol used in format/parse. If not given,
+ *     the currency code for current locale will be used.
+ */
+gadgets.i18n.NumberFormat.prototype.applyPattern =
+    function(pattern, opt_currency) {
+  this.pattern_ = pattern;
+  this.intlCurrencyCode_ = opt_currency || this.symbols_.DEF_CURRENCY_CODE;
+  this.currencySymbol_ = gadgets.i18n.CurrencyCodeMap[this.intlCurrencyCode_];
+
+  this.maximumIntegerDigits_ = 40;
+  this.minimumIntegerDigits_ = 1;
+  this.maximumFractionDigits_ = 3; // invariant, >= minFractionDigits
+  this.minimumFractionDigits_ = 0;
+  this.minExponentDigits_ = 0;
+
+  this.positivePrefix_ = '';
+  this.positiveSuffix_ = '';
+  this.negativePrefix_ = '-';
+  this.negativeSuffix_ = '';
+
+  // The multiplier for use in percent, per mille, etc.
+  this.multiplier_ = 1;
+  this.groupingSize_ = 3;
+  this.decimalSeparatorAlwaysShown_ = false;
+  this.isCurrencyFormat_ = false;
+  this.useExponentialNotation_ = false;
+
+  this.parsePattern_(this.pattern_);
+};
+
+
+/**
+ * Parses text string to produce a Number.
+ *
+ * This method attempts to parse text starting from position "opt_pos" if it
+ * is given. Otherwise the parse will start from the beginning of the text.
+ * When opt_pos presents, opt_pos will be updated to the character next to where
+ * parsing stops after the call. If an error occurs, opt_pos won't be updated.
+ *
+ * @param {string} text the string to be parsed.
+ * @param {Array=} opt_pos position to pass in and get back.
+ * @return {number} Parsed number, or 0 if the parse fails.
+ */
+gadgets.i18n.NumberFormat.prototype.parse = function(text, opt_pos) {
+  var pos = opt_pos || [0];
+
+  var start = pos[0];
+  var ret = 0;
+
+  var gotPositive = text.indexOf(this.positivePrefix_, pos[0]) == pos[0];
+  var gotNegative = text.indexOf(this.negativePrefix_, pos[0]) == pos[0];
+
+  // check for the longest match
+  if (gotPositive && gotNegative) {
+    if (this.positivePrefix_.length > this.negativePrefix_.length) {
+      gotNegative = false;
+    } else if (this.positivePrefix_.length < this.negativePrefix_.length) {
+      gotPositive = false;
+    }
+  }
+
+  if (gotPositive) {
+    pos[0] += this.positivePrefix_.length;
+  } else if (gotNegative) {
+    pos[0] += this.negativePrefix_.length;
+  }
+
+  // process digits or Inf, find decimal position
+  if (text.indexOf(this.symbols_.INFINITY, pos[0]) == pos[0]) {
+    pos[0] += this.symbols_.INFINITY.length;
+    ret = Infinity;
+  } else {
+    ret = this.parseNumber_(text, pos);
+  }
+
+  // check for suffix
+  if (gotPositive) {
+    if (!(text.indexOf(this.positiveSuffix_, pos[0]) == pos[0])) {
+      pos[0] = start;
+      return 0;
+    }
+    pos[0] += this.positiveSuffix_.length;
+  } else if (gotNegative) {
+    if (!(text.indexOf(this.negativeSuffix_, pos[0]) == pos[0])) {
+      pos[0] = start;
+      return 0;
+    }
+    pos[0] += this.negativeSuffix_.length;
+  }
+
+  return gotNegative ? -ret : ret;
+};
+
+
+/**
+ * This function will parse a "localized" text into a Number. It needs to
+ * handle locale specific decimal, grouping, exponent and digits.
+ *
+ * @param {string} text The text that need to be parsed.
+ * @param {Array} pos  In/out parsing position. In case of failure, pos value
+ *   won't be changed.
+ * @return {number} Number value, could be 0.0 if nothing can be parsed.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.parseNumber_ = function(text, pos) {
+  var sawDecimal = false;
+  var sawExponent = false;
+  var sawDigit = false;
+  var scale = 1;
+  var decimal = this.isCurrencyFormat_ ? this.symbols_.MONETARY_SEP :
+      this.symbols_.DECIMAL_SEP;
+  var grouping = this.isCurrencyFormat_ ? this.symbols_.MONETARY_GROUP_SEP :
+      this.symbols_.GROUP_SEP;
+  var exponentChar = this.symbols_.EXP_SYMBOL;
+
+  var normalizedText = '';
+  for (; pos[0] < text.length; pos[0]++) {
+    var ch = text.charAt(pos[0]);
+    var digit = this.getDigit_(ch);
+    if (digit >= 0 && digit <= 9) {
+      normalizedText += digit;
+      sawDigit = true;
+    } else if (ch == decimal.charAt(0)) {
+      if (sawDecimal || sawExponent) {
+        break;
+      }
+      normalizedText += '.';
+      sawDecimal = true;
+    } else if (ch == grouping.charAt(0) || '\u00a0' == grouping.charAt(0) &&
+        ch == ' ' && pos[0] + 1 < text.length &&
+        this.getDigit_(text.charAt(pos[0] + 1)) >= 0) {
+      if (sawDecimal || sawExponent) {
+        break;
+      }
+      continue;
+    } else if (ch == exponentChar.charAt(0)) {
+      if (sawExponent) {
+        break;
+      }
+      normalizedText += 'E';
+      sawExponent = true;
+    } else if (ch == '+' || ch == '-') {
+      normalizedText += ch;
+    } else if (ch == this.symbols_.PERCENT.charAt(0)) {
+      if (scale != 1) {
+        break;
+      }
+      scale = 100;
+      if (sawDigit) {
+        pos[0]++; // eat this character if parse end here
+        break;
+      }
+    } else if (ch == this.symbols_.PERMILL.charAt(0)) {
+      if (scale != 1) {
+        break;
+      }
+      scale = 1000;
+      if (sawDigit) {
+        pos[0]++; // eat this character if parse end here
+        break;
+      }
+    } else {
+      break;
+    }
+  }
+  return parseFloat(normalizedText) / scale;
+};
+
+
+/**
+ * Formats a Number to produce a string.
+ *
+ * @param {number} number The Number to be formatted.
+ * @return {string} The formatted number string.
+ */
+gadgets.i18n.NumberFormat.prototype.format = function(number) {
+  if (isNaN(number)) {
+    return this.symbols_.NAN;
+  }
+
+  var parts = [];
+
+  // in icu code, it is commented that certain computation need to keep the
+  // negative sign for 0.
+  var isNegative = number < 0.0 || number == 0.0 && 1 / number < 0.0;
+
+  parts.push(isNegative ? this.negativePrefix_ : this.positivePrefix_);
+
+  if (!isFinite(number)) {
+    parts.push(this.symbols_.INFINITY);
+  } else {
+    // convert number to non-negative value
+    number *= isNegative ? -1 : 1;
+
+    number *= this.multiplier_;
+    this.useExponentialNotation_ ?
+        this.subformatExponential_(number, parts) :
+        this.subformatFixed_(number, this.minimumIntegerDigits_, parts);
+  }
+
+  parts.push(isNegative ? this.negativeSuffix_ : this.positiveSuffix_);
+
+  return parts.join('');
+};
+
+
+/**
+ * Formats a Number in fraction format.
+ *
+ * @param {number} number Value need to be formated.
+ * @param {number} minIntDigits Minimum integer digits.
+ * @param {Array} parts This array holds the pieces of formatted string.
+ *     This function will add its formatted pieces to the array.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.subformatFixed_ = function(number,
+                                                               minIntDigits,
+                                                               parts) {
+  // round the number
+  var power = Math.pow(10, this.maximumFractionDigits_);
+  number = Math.round(number * power);
+  var intValue = Math.floor(number / power);
+  var fracValue = Math.floor(number - intValue * power);
+
+  var fractionPresent = this.minimumFractionDigits_ > 0 || fracValue > 0;
+
+  var intPart = '';
+  var translatableInt = intValue;
+  while (translatableInt > 1E20) {
+    // here it goes beyond double precision, add '0' make it look better
+    intPart = '0' + intPart;
+    translatableInt = Math.round(translatableInt / 10);
+  }
+  intPart = translatableInt + intPart;
+
+  var decimal = this.isCurrencyFormat_ ? this.symbols_.MONETARY_SEP :
+      this.symbols_.DECIMAL_SEP;
+  var grouping = this.isCurrencyFormat_ ? this.symbols_.MONETARY_GROUP_SEP :
+      this.symbols_.GROUP_SEP;
+
+  var zeroCode = this.symbols_.ZERO_DIGIT.charCodeAt(0);
+  var digitLen = intPart.length;
+
+  if (intValue > 0 || minIntDigits > 0) {
+    for (var i = digitLen; i < minIntDigits; i++) {
+      parts.push(this.symbols_.ZERO_DIGIT);
+    }
+
+    for (var i = 0; i < digitLen; i++) {
+      parts.push(String.fromCharCode(zeroCode + intPart.charAt(i) * 1));
+
+      if (digitLen - i > 1 && this.groupingSize_ > 0 &&
+          ((digitLen - i) % this.groupingSize_ == 1)) {
+        parts.push(grouping);
+      }
+    }
+  } else if (!fractionPresent) {
+    // If there is no fraction present, and we haven't printed any
+    // integer digits, then print a zero.
+    parts.push(this.symbols_.ZERO_DIGIT);
+  }
+
+  // Output the decimal separator if we always do so.
+  if (this.decimalSeparatorAlwaysShown_ || fractionPresent) {
+    parts.push(decimal);
+  }
+
+  var fracPart = '' + (fracValue + power);
+  var fracLen = fracPart.length;
+  while (fracPart.charAt(fracLen - 1) == '0' &&
+      fracLen > this.minimumFractionDigits_ + 1) {
+    fracLen--;
+  }
+
+  for (var i = 1; i < fracLen; i++) {
+    parts.push(String.fromCharCode(zeroCode + fracPart.charAt(i) * 1));
+  }
+};
+
+
+/**
+ * Formats exponent part of a Number.
+ *
+ * @param {number} exponent exponential value.
+ * @param {Array} parts This array holds the pieces of formatted string.
+ *     This function will add its formatted pieces to the array.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.addExponentPart_ = function(exponent,
+                                                                parts) {
+  parts.push(this.symbols_.EXP_SYMBOL);
+
+  if (exponent < 0) {
+    exponent = -exponent;
+    parts.push(this.symbols_.MINUS_SIGN);
+  }
+
+  var exponentDigits = '' + exponent;
+  for (var i = exponentDigits.length; i < this.minExponentDigits_; i++) {
+    parts.push(this.symbols_.ZERO_DIGIT);
+  }
+  parts.push(exponentDigits);
+};
+
+
+/**
+ * Formats Number in exponential format.
+ *
+ * @param {number} number Value need to be formated.
+ * @param {Array} parts This array holds the pieces of formatted string.
+ *     This function will add its formatted pieces to the array.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.subformatExponential_ = function(number,
+                                                                     parts) {
+  if (number == 0.0) {
+    this.subformatFixed_(number, this.minimumIntegerDigits_, parts);
+    this.addExponentPart_(0, parts);
+    return;
+  }
+
+  var exponent = Math.floor(Math.log(number) / Math.log(10));
+  number /= Math.pow(10, exponent);
+
+  var minIntDigits = this.minimumIntegerDigits_;
+  if (this.maximumIntegerDigits_ > 1 &&
+      this.maximumIntegerDigits_ > this.minimumIntegerDigits_) {
+    // A repeating range is defined; adjust to it as follows.
+    // If repeat == 3, we have 6,5,4=>3; 3,2,1=>0; 0,-1,-2=>-3;
+    // -3,-4,-5=>-6, etc. This takes into account that the
+    // exponent we have here is off by one from what we expect;
+    // it is for the format 0.MMMMMx10^n.
+    while ((exponent % this.maximumIntegerDigits_) != 0) {
+      number *= 10;
+      exponent--;
+    }
+    minIntDigits = 1;
+  } else {
+    // No repeating range is defined; use minimum integer digits.
+    if (this.minimumIntegerDigits_ < 1) {
+      exponent++;
+      number /= 10;
+    } else {
+      exponent -= this.minimumIntegerDigits_ - 1;
+      number *= Math.pow(10, this.minimumIntegerDigits_ - 1);
+    }
+  }
+  this.subformatFixed_(number, minIntDigits, parts);
+  this.addExponentPart_(exponent, parts);
+};
+
+
+/**
+ * Returns the digit value of current character. The character could be either
+ * '0' to '9', or a locale specific digit.
+ *
+ * @param {string} ch Character that represents a digit.
+ * @return {number} The digit value, or -1 on error.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.getDigit_ = function(ch) {
+  var code = ch.charCodeAt(0);
+  // between '0' to '9'
+  if (48 <= code && code < 58) {
+    return code - 48;
+  } else {
+    var zeroCode = this.symbols_.ZERO_DIGIT.charCodeAt(0);
+    return zeroCode <= code && code < zeroCode + 10 ? code - zeroCode : -1;
+  }
+};
+
+
+// ----------------------------------------------------------------------
+// CONSTANTS
+// ----------------------------------------------------------------------
+// Constants for characters used in programmatic (unlocalized) patterns.
+/**
+ * A zero digit character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_ZERO_DIGIT_ = '0';
+
+
+/**
+ * A grouping separator character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_GROUPING_SEPARATOR_ = ',';
+
+
+/**
+ * A decimal separator character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_DECIMAL_SEPARATOR_ = '.';
+
+
+/**
+ * A per mille character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_PER_MILLE_ = '\u2030';
+
+
+/**
+ * A percent character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_PERCENT_ = '%';
+
+
+/**
+ * A digit character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_DIGIT_ = '#';
+
+
+/**
+ * A separator character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_SEPARATOR_ = ';';
+
+
+/**
+ * An exponent character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_EXPONENT_ = 'E';
+
+
+/**
+ * A minus character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_MINUS_ = '-';
+
+
+/**
+ * A quote character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.PATTERN_CURRENCY_SIGN_ = '\u00A4';
+
+
+/**
+ * A quote character.
+ * @type {string}
+ * @private
+ */
+gadgets.i18n.NumberFormat.QUOTE_ = '\'';
+
+
+/**
+ * Parses affix part of pattern.
+ *
+ * @param {string} pattern Pattern string that need to be parsed.
+ * @param {Array} pos  One element position array to set and receive parsing
+ *     position.
+ *
+ * @return {string} affix received from parsing.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.parseAffix_ = function(pattern, pos) {
+  var affix = '';
+  var inQuote = false;
+  var len = pattern.length;
+
+  for (; pos[0] < len; pos[0]++) {
+    var ch = pattern.charAt(pos[0]);
+    if (ch == gadgets.i18n.NumberFormat.QUOTE_) {
+      if (pos[0] + 1 < len &&
+          pattern.charAt(pos[0] + 1) == gadgets.i18n.NumberFormat.QUOTE_) {
+        pos[0]++;
+        affix += '\''; // 'don''t'
+      } else {
+        inQuote = !inQuote;
+      }
+      continue;
+    }
+
+    if (inQuote) {
+      affix += ch;
+    } else {
+      switch (ch) {
+        case gadgets.i18n.NumberFormat.PATTERN_DIGIT_:
+        case gadgets.i18n.NumberFormat.PATTERN_ZERO_DIGIT_:
+        case gadgets.i18n.NumberFormat.PATTERN_GROUPING_SEPARATOR_:
+        case gadgets.i18n.NumberFormat.PATTERN_DECIMAL_SEPARATOR_:
+        case gadgets.i18n.NumberFormat.PATTERN_SEPARATOR_:
+          return affix;
+        case gadgets.i18n.NumberFormat.PATTERN_CURRENCY_SIGN_:
+          this.isCurrencyFormat_ = true;
+          if ((pos[0] + 1) < len &&
+              pattern.charAt(pos[0] + 1) ==
+              gadgets.i18n.NumberFormat.PATTERN_CURRENCY_SIGN_) {
+            pos[0]++;
+            affix += this.intlCurrencyCode_;
+          } else {
+            affix += this.currencySymbol_;
+          }
+          break;
+        case gadgets.i18n.NumberFormat.PATTERN_PERCENT_:
+          if (this.multiplier_ != 1) {
+            throw Error('Too many percent/permill');
+          }
+          this.multiplier_ = 100;
+          affix += this.symbols_.PERCENT;
+          break;
+        case gadgets.i18n.NumberFormat.PATTERN_PER_MILLE_:
+          if (this.multiplier_ != 1) {
+            throw Error('Too many percent/permill');
+          }
+          this.multiplier_ = 1000;
+          affix += this.symbols_.PERMILL;
+          break;
+        default:
+          affix += ch;
+      }
+    }
+  }
+
+  return affix;
+};
+
+
+/**
+ * Parses the trunk part of a pattern.
+ *
+ * @param {string} pattern Pattern string that need to be parsed.
+ * @param {Array} pos One element position array to set and receive parsing
+ *     position.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.parseTrunk_ = function(pattern, pos) {
+  var decimalPos = -1;
+  var digitLeftCount = 0;
+  var zeroDigitCount = 0;
+  var digitRightCount = 0;
+  var groupingCount = -1;
+
+  var len = pattern.length;
+  for (var loop = true; pos[0] < len && loop; pos[0]++) {
+    var ch = pattern.charAt(pos[0]);
+    switch (ch) {
+      case gadgets.i18n.NumberFormat.PATTERN_DIGIT_:
+        if (zeroDigitCount > 0) {
+          digitRightCount++;
+        } else {
+          digitLeftCount++;
+        }
+        if (groupingCount >= 0 && decimalPos < 0) {
+          groupingCount++;
+        }
+        break;
+      case gadgets.i18n.NumberFormat.PATTERN_ZERO_DIGIT_:
+        if (digitRightCount > 0) {
+          throw Error('Unexpected "0" in pattern "' + pattern + '"');
+        }
+        zeroDigitCount++;
+        if (groupingCount >= 0 && decimalPos < 0) {
+          groupingCount++;
+        }
+        break;
+      case gadgets.i18n.NumberFormat.PATTERN_GROUPING_SEPARATOR_:
+        groupingCount = 0;
+        break;
+      case gadgets.i18n.NumberFormat.PATTERN_DECIMAL_SEPARATOR_:
+        if (decimalPos >= 0) {
+          throw Error('Multiple decimal separators in pattern "'
+                            + pattern + '"');
+        }
+        decimalPos = digitLeftCount + zeroDigitCount + digitRightCount;
+        break;
+      case gadgets.i18n.NumberFormat.PATTERN_EXPONENT_:
+        if (this.useExponentialNotation_) {
+          throw Error('Multiple exponential symbols in pattern "'
+                            + pattern + '"');
+        }
+        this.useExponentialNotation_ = true;
+        this.minExponentDigits_ = 0;
+
+        // Use lookahead to parse out the exponential part
+        // of the pattern, then jump into phase 2.
+        while ((pos[0] + 1) < len && pattern.charAt(pos[0] + 1) ==
+            this.symbols_.ZERO_DIGIT.charAt(0)) {
+          pos[0]++;
+          this.minExponentDigits_++;
+        }
+
+        if ((digitLeftCount + zeroDigitCount) < 1 ||
+            this.minExponentDigits_ < 1) {
+          throw Error('Malformed exponential pattern "' + pattern + '"');
+        }
+        loop = false;
+        break;
+      default:
+        pos[0]--;
+        loop = false;
+        break;
+    }
+  }
+
+  if (zeroDigitCount == 0 && digitLeftCount > 0 && decimalPos >= 0) {
+    // Handle '###.###' and '###.' and '.###'
+    var n = decimalPos;
+    if (n == 0) { // Handle '.###'
+      n++;
+    }
+    digitRightCount = digitLeftCount - n;
+    digitLeftCount = n - 1;
+    zeroDigitCount = 1;
+  }
+
+  // Do syntax checking on the digits.
+  if (decimalPos < 0 && digitRightCount > 0 ||
+      decimalPos >= 0 && (decimalPos < digitLeftCount ||
+      decimalPos > digitLeftCount + zeroDigitCount) ||
+      groupingCount == 0) {
+    throw Error('Malformed pattern "' + pattern + '"');
+  }
+  var totalDigits = digitLeftCount + zeroDigitCount + digitRightCount;
+
+  this.maximumFractionDigits_ = decimalPos >= 0 ? totalDigits - decimalPos : 0;
+  if (decimalPos >= 0) {
+    this.minimumFractionDigits_ = digitLeftCount + zeroDigitCount - decimalPos;
+    if (this.minimumFractionDigits_ < 0) {
+      this.minimumFractionDigits_ = 0;
+    }
+  }
+
+  // The effectiveDecimalPos is the position the decimal is at or would be at
+  // if there is no decimal. Note that if decimalPos<0, then digitTotalCount ==
+  // digitLeftCount + zeroDigitCount.
+  var effectiveDecimalPos = decimalPos >= 0 ? decimalPos : totalDigits;
+  this.minimumIntegerDigits_ = effectiveDecimalPos - digitLeftCount;
+  if (this.useExponentialNotation_) {
+    this.maximumIntegerDigits_ = digitLeftCount + this.minimumIntegerDigits_;
+
+    // in exponential display, we need to at least show something.
+    if (this.maximumFractionDigits_ == 0 && this.minimumIntegerDigits_ == 0) {
+      this.minimumIntegerDigits_ = 1;
+    }
+  }
+
+  this.groupingSize_ = Math.max(0, groupingCount);
+  this.decimalSeparatorAlwaysShown_ = decimalPos == 0 ||
+      decimalPos == totalDigits;
+};
+
+
+/**
+ * Parses provided pattern, result are stored in member variables.
+ *
+ * @param {string} pattern string pattern being applied.
+ * @private
+ */
+gadgets.i18n.NumberFormat.prototype.parsePattern_ = function(pattern) {
+  var pos = [0];
+
+  this.positivePrefix_ = this.parseAffix_(pattern, pos);
+  var trunkStart = pos[0];
+  this.parseTrunk_(pattern, pos);
+  var trunkLen = pos[0] - trunkStart;
+  this.positiveSuffix_ = this.parseAffix_(pattern, pos);
+
+  if (pos[0] < pattern.length &&
+      pattern.charAt(pos[0]) == gadgets.i18n.NumberFormat.PATTERN_SEPARATOR_) {
+    pos[0]++;
+    this.negativePrefix_ = this.parseAffix_(pattern, pos);
+    // we assume this part is identical to positive part.
+    // user must make sure the pattern is correctly constructed.
+    pos[0] += trunkLen;
+    this.negativeSuffix_ = this.parseAffix_(pattern, pos);
+  }
+};
diff --git a/trunk/features/src/main/javascript/features/i18n/numberformattest.js b/trunk/features/src/main/javascript/features/i18n/numberformattest.js
new file mode 100644
index 0000000..5e9a488
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/i18n/numberformattest.js
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview Unit Tests - gadgets.i18n.NumberFormat.
+ */
+
+function NumberFormatTest(name) {
+  TestCase.call(this, name);
+}
+
+NumberFormatTest.inherits(TestCase);
+
+var NumberFormatConstants_en = {
+  DECIMAL_SEP: '.',
+  GROUP_SEP: ',',
+  PERCENT: '%',
+  ZERO_DIGIT: '0',
+  PLUS_SIGN: '+',
+  MINUS_SIGN: '-',
+  EXP_SYMBOL: 'E',
+  PERMILL: '\u2030',
+  INFINITY: '\u221E',
+  NAN: 'NaN',
+  MONETARY_SEP: '.',
+  MONETARY_GROUP_SEP: ',',
+  DECIMAL_PATTERN: '#,##0.###',
+  SCIENTIFIC_PATTERN: '#E0',
+  PERCENT_PATTERN: '#,##0%',
+  CURRENCY_PATTERN: '\u00A4#,##0.00',
+  DEF_CURRENCY_CODE: 'USD'
+};
+
+NumberFormatTest.prototype.setUp = function() {
+  gadgets.i18n.numFormatter_
+            = new gadgets.i18n.NumberFormat(NumberFormatConstants_en);
+};
+
+NumberFormatTest.prototype.testStandardFormat = function() {
+  var str;
+  str = gadgets.i18n.formatNumber(gadgets.i18n.CURRENCY_PATTERN, 1234.579);
+  this.assertEquals('$1,234.58', str);
+  str = gadgets.i18n.formatNumber(gadgets.i18n.DECIMAL_PATTERN, 1234.579);
+  this.assertEquals('1,234.579', str);
+  str = gadgets.i18n.formatNumber(gadgets.i18n.PERCENT_PATTERN, 1234.579);
+  this.assertEquals('123,458%', str);
+  str = gadgets.i18n.formatNumber(gadgets.i18n.SCIENTIFIC_PATTERN, 1234.579);
+  this.assertEquals('1E3', str);
+};
+
+NumberFormatTest.prototype.testBasicParse = function() {
+  var value;
+
+  value = gadgets.i18n.parseNumber('0.0000', '123.4579');
+  this.assertEquals(123.4579, value);
+
+  value = gadgets.i18n.parseNumber('0.0000', '+123.4579');
+  this.assertEquals(123.4579, value);
+
+  value = gadgets.i18n.parseNumber('0.0000', '-123.4579');
+  this.assertEquals(-123.4579, value);
+};
+
+NumberFormatTest.prototype.testPrefixParse = function() {
+  var value;
+
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '123.4579');
+  this.assertEquals(123.4579, value);
+
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '(123.4579)');
+  this.assertEquals(-123.4579, value);
+};
+
+NumberFormatTest.prototype.testPrecentParse = function() {
+  var value;
+
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '123.4579%');
+  this.assertEquals((123.4579 / 100), value);
+
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '(%123.4579)');
+  this.assertEquals((-123.4579 / 100), value);
+
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '123.4579\u2030');
+  this.assertEquals((123.4579 / 1000), value);
+
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '(\u2030123.4579)');
+  this.assertEquals((-123.4579 / 1000), value);
+};
+
+NumberFormatTest.prototype.testPercentAndPerMillAdvance = function() {
+  var value;
+  var pos = [0];
+  value = gadgets.i18n.parseNumber('0', '120%', pos);
+  this.assertEquals(1.2, value);
+  this.assertEquals(4, pos[0]);
+  pos[0] = 0;
+  value = gadgets.i18n.parseNumber('0', '120\u2030', pos);
+  this.assertEquals(0.12, value);
+  this.assertEquals(4, pos[0]);
+};
+
+NumberFormatTest.prototype.testInfinityParse = function() {
+  var value;
+
+  // gwt need to add those symbols first
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '\u221e');
+  this.assertEquals(Number.POSITIVE_INFINITY, value);
+
+  value = gadgets.i18n.parseNumber('0.0;(0.0)', '(\u221e)');
+  this.assertEquals(Number.NEGATIVE_INFINITY, value);
+};
+NumberFormatTest.prototype.testExponentParse = function() {
+  var value;
+
+  value = gadgets.i18n.parseNumber('#E0', '1.234E3');
+  this.assertEquals(1.234E+3, value);
+
+  value = gadgets.i18n.parseNumber('0.###E0', '1.234E3');
+  this.assertEquals(1.234E+3, value);
+
+  value = gadgets.i18n.parseNumber('#E0', '1.2345E4');
+  this.assertEquals(12345.0, value);
+
+  value = gadgets.i18n.parseNumber('0E0', '1.2345E4');
+  this.assertEquals(12345.0, value);
+
+  value = gadgets.i18n.parseNumber('0E0', '1.2345E+4');
+  this.assertEquals(12345.0, value);
+};
+
+NumberFormatTest.prototype.testGroupingParse = function() {
+  var value;
+
+  value = gadgets.i18n.parseNumber('#,###', '1,234,567,890');
+  this.assertEquals(1234567890, value);
+  value = gadgets.i18n.parseNumber('#,####', '12,3456,7890');
+  this.assertEquals(1234567890, value);
+
+  value = gadgets.i18n.parseNumber('#', '1234567890');
+  this.assertEquals(1234567890, value);
+};
+
+/**
+ * Add as many tests as you like.
+ */
+NumberFormatTest.prototype.testBasicFormat = function() {
+  var str = gadgets.i18n.formatNumber('0.0000', 123.45789179565757);
+  this.assertEquals('123.4579', str);
+};
+
+NumberFormatTest.prototype.testGrouping = function() {
+  var str;
+
+  str = gadgets.i18n.formatNumber('#,###', 1234567890);
+  this.assertEquals('1,234,567,890', str);
+  str = gadgets.i18n.formatNumber('#,####', 1234567890);
+  this.assertEquals('12,3456,7890', str);
+
+  str = gadgets.i18n.formatNumber('#', 1234567890);
+  this.assertEquals('1234567890', str);
+};
+
+NumberFormatTest.prototype.testPerMill = function() {
+  var str;
+
+  str = gadgets.i18n.formatNumber('###.###\u2030', 0.4857);
+  this.assertEquals('485.7\u2030', str);
+};
+
+NumberFormatTest.prototype.testCurrency = function() {
+  var str;
+
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;-\u00a4#,##0.00', 1234.56);
+  this.assertEquals('$1,234.56', str);
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;-\u00a4#,##0.00', -1234.56);
+  this.assertEquals('-$1,234.56', str);
+
+  str = gadgets.i18n.formatNumber(
+      '\u00a4\u00a4 #,##0.00;-\u00a4\u00a4 #,##0.00', 1234.56);
+  this.assertEquals('USD 1,234.56', str);
+  str = gadgets.i18n.formatNumber(
+      '\u00a4\u00a4 #,##0.00;\u00a4\u00a4 -#,##0.00', -1234.56);
+  this.assertEquals('USD -1,234.56', str);
+
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;-\u00a4#,##0.00',
+      1234.56, 'BRL');
+  this.assertEquals('R$1,234.56', str);
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;-\u00a4#,##0.00',
+      -1234.56, 'BRL');
+  this.assertEquals('-R$1,234.56', str);
+
+  str = gadgets.i18n.formatNumber(
+      '\u00a4\u00a4 #,##0.00;(\u00a4\u00a4 #,##0.00)', 1234.56, 'BRL');
+  this.assertEquals('BRL 1,234.56', str);
+  str = gadgets.i18n.formatNumber(
+      '\u00a4\u00a4 #,##0.00;(\u00a4\u00a4 #,##0.00)', -1234.56, 'BRL');
+  this.assertEquals('(BRL 1,234.56)', str);
+};
+
+NumberFormatTest.prototype.testQuotes = function() {
+  var str;
+
+  str = gadgets.i18n.formatNumber("a'fo''o'b#", 123);
+  this.assertEquals("afo'ob123", str);
+
+  str = gadgets.i18n.formatNumber("a''b#", 123);
+  this.assertEquals("a'b123", str);
+};
+
+NumberFormatTest.prototype.testZeros = function() {
+  var str;
+
+  str = gadgets.i18n.formatNumber('#.#', 0);
+  this.assertEquals('0', str);
+  str = gadgets.i18n.formatNumber('#.', 0);
+  this.assertEquals('0.', str);
+  str = gadgets.i18n.formatNumber('.#', 0);
+  this.assertEquals('.0', str);
+  str = gadgets.i18n.formatNumber('#', 0);
+  this.assertEquals('0', str);
+
+  str = gadgets.i18n.formatNumber('#0.#', 0);
+  this.assertEquals('0', str);
+  str = gadgets.i18n.formatNumber('#0.', 0);
+  this.assertEquals('0.', str);
+  str = gadgets.i18n.formatNumber('#.0', 0);
+  this.assertEquals('.0', str);
+  str = gadgets.i18n.formatNumber('#', 0);
+  this.assertEquals('0', str);
+  str = gadgets.i18n.formatNumber('000', 0);
+  this.assertEquals('000', str);
+};
+
+NumberFormatTest.prototype.testExponential = function() {
+  var str;
+
+  str = gadgets.i18n.formatNumber('0.####E0', 0.01234);
+  this.assertEquals('1.234E-2', str);
+  str = gadgets.i18n.formatNumber('00.000E00', 0.01234);
+  this.assertEquals('12.340E-03', str);
+  str = gadgets.i18n.formatNumber('##0.######E000', 0.01234);
+  this.assertEquals('12.34E-003', str);
+  str = gadgets.i18n.formatNumber('0.###E0;[0.###E0]', 0.01234);
+  this.assertEquals('1.234E-2', str);
+
+  str = gadgets.i18n.formatNumber('0.####E0', 123456789);
+  this.assertEquals('1.2346E8', str);
+  str = gadgets.i18n.formatNumber('00.000E00', 123456789);
+  this.assertEquals('12.346E07', str);
+  str = gadgets.i18n.formatNumber('##0.######E000', 123456789);
+  this.assertEquals('123.456789E006', str);
+  str = gadgets.i18n.formatNumber('0.###E0;[0.###E0]', 123456789);
+  this.assertEquals('1.235E8', str);
+
+  str = gadgets.i18n.formatNumber('0.####E0', 1.23e300);
+  this.assertEquals('1.23E300', str);
+  str = gadgets.i18n.formatNumber('00.000E00', 1.23e300);
+  this.assertEquals('12.300E299', str);
+  str = gadgets.i18n.formatNumber('##0.######E000', 1.23e300);
+  this.assertEquals('1.23E300', str);
+  str = gadgets.i18n.formatNumber('0.###E0;[0.###E0]', 1.23e300);
+  this.assertEquals('1.23E300', str);
+
+  str = gadgets.i18n.formatNumber('0.####E0', -3.141592653e-271);
+  this.assertEquals('-3.1416E-271', str);
+  str = gadgets.i18n.formatNumber('00.000E00', -3.141592653e-271);
+  this.assertEquals('-31.416E-272', str);
+  str = gadgets.i18n.formatNumber('##0.######E000', -3.141592653e-271);
+  this.assertEquals('-314.159265E-273', str);
+  str = gadgets.i18n.formatNumber('0.###E0;[0.###E0]', -3.141592653e-271);
+  this.assertEquals('[3.142E-271]', str);
+
+  str = gadgets.i18n.formatNumber('0.####E0', 0);
+  this.assertEquals('0E0', str);
+  str = gadgets.i18n.formatNumber('00.000E00', 0);
+  this.assertEquals('00.000E00', str);
+  str = gadgets.i18n.formatNumber('##0.######E000', 0);
+  this.assertEquals('0E000', str);
+  str = gadgets.i18n.formatNumber('0.###E0;[0.###E0]', 0);
+  this.assertEquals('0E0', str);
+
+  str = gadgets.i18n.formatNumber('0.####E0', -1);
+  this.assertEquals('-1E0', str);
+  str = gadgets.i18n.formatNumber('00.000E00', -1);
+  this.assertEquals('-10.000E-01', str);
+  str = gadgets.i18n.formatNumber('##0.######E000', -1);
+  this.assertEquals('-1E000', str);
+  str = gadgets.i18n.formatNumber('0.###E0;[0.###E0]', -1);
+  this.assertEquals('[1E0]', str);
+
+  str = gadgets.i18n.formatNumber('0.####E0', 1);
+  this.assertEquals('1E0', str);
+  str = gadgets.i18n.formatNumber('00.000E00', 1);
+  this.assertEquals('10.000E-01', str);
+  str = gadgets.i18n.formatNumber('##0.######E000', 1);
+  this.assertEquals('1E000', str);
+  str = gadgets.i18n.formatNumber('0.###E0;[0.###E0]', 1);
+  this.assertEquals('1E0', str);
+
+  str = gadgets.i18n.formatNumber('#E0', 12345.0);
+  //assertEquals(".1E5", str);
+  this.assertEquals('1E4', str);
+  str = gadgets.i18n.formatNumber('0E0', 12345.0);
+  this.assertEquals('1E4', str);
+  str = gadgets.i18n.formatNumber('##0.###E0', 12345.0);
+  this.assertEquals('12.345E3', str);
+  str = gadgets.i18n.formatNumber('##0.###E0', 12345.00001);
+  this.assertEquals('12.345E3', str);
+  str = gadgets.i18n.formatNumber('##0.###E0', 12345);
+  this.assertEquals('12.345E3', str);
+
+  str = gadgets.i18n.formatNumber('##0.####E0', 789.12345e-9);
+  this.assertEquals('789.1235E-9', str);
+  str = gadgets.i18n.formatNumber('##0.####E0', 780.e-9);
+  this.assertEquals('780E-9', str);
+  str = gadgets.i18n.formatNumber('.###E0', 45678.0);
+  this.assertEquals('.457E5', str);
+  str = gadgets.i18n.formatNumber('.###E0', 0);
+  this.assertEquals('.0E0', str);
+
+  str = gadgets.i18n.formatNumber('#E0', 45678000);
+  this.assertEquals('5E7', str);
+  str = gadgets.i18n.formatNumber('##E0', 45678000);
+  this.assertEquals('46E6', str);
+  str = gadgets.i18n.formatNumber('####E0', 45678000);
+  this.assertEquals('4568E4', str);
+  str = gadgets.i18n.formatNumber('0E0', 45678000);
+  this.assertEquals('5E7', str);
+  str = gadgets.i18n.formatNumber('00E0', 45678000);
+  this.assertEquals('46E6', str);
+  str = gadgets.i18n.formatNumber('000E0', 45678000);
+  this.assertEquals('457E5', str);
+  str = gadgets.i18n.formatNumber('###E0', 0.0000123);
+  this.assertEquals('12E-6', str);
+  str = gadgets.i18n.formatNumber('###E0', 0.000123);
+  this.assertEquals('123E-6', str);
+  str = gadgets.i18n.formatNumber('###E0', 0.00123);
+  this.assertEquals('1E-3', str);
+  str = gadgets.i18n.formatNumber('###E0', 0.0123);
+  this.assertEquals('12E-3', str);
+  str = gadgets.i18n.formatNumber('###E0', 0.123);
+  this.assertEquals('123E-3', str);
+  str = gadgets.i18n.formatNumber('###E0', 1.23);
+  this.assertEquals('1E0', str);
+  str = gadgets.i18n.formatNumber('###E0', 12.3);
+  this.assertEquals('12E0', str);
+  str = gadgets.i18n.formatNumber('###E0', 123.0);
+  this.assertEquals('123E0', str);
+  str = gadgets.i18n.formatNumber('###E0', 1230.0);
+  this.assertEquals('1E3', str);
+};
+
+NumberFormatTest.prototype.testGroupingParse2 = function() {
+  var value;
+
+  value = gadgets.i18n.parseNumber('#,###', '1,234,567,890');
+  this.assertEquals(1234567890, value);
+  value = gadgets.i18n.parseNumber('#,####', '12,3456,7890');
+  this.assertEquals(1234567890, value);
+
+  value = gadgets.i18n.parseNumber('#', '1234567890');
+  this.assertEquals(1234567890, value);
+};
+
+NumberFormatTest.prototype.testApis = function() {
+  var str;
+
+  str = gadgets.i18n.formatNumber('#,###', 1234567890);
+  this.assertEquals('1,234,567,890', str);
+
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;-\u00a4#,##0.00', 1234.56);
+  this.assertEquals('$1,234.56', str);
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;(\u00a4#,##0.00)',
+      -1234.56);
+  this.assertEquals('($1,234.56)', str);
+
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;-\u00a4#,##0.00',
+      1234.56, 'SEK');
+  this.assertEquals('kr1,234.56', str);
+  str = gadgets.i18n.formatNumber('\u00a4#,##0.00;(\u00a4#,##0.00)',
+      -1234.56, 'SEK');
+  this.assertEquals('(kr1,234.56)', str);
+};
diff --git a/trunk/features/src/main/javascript/features/jsondom/feature.xml b/trunk/features/src/main/javascript/features/jsondom/feature.xml
new file mode 100644
index 0000000..2acb6b8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/jsondom/feature.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <!-- 
+    jsondom (Read Only DOM) is implemented in both JavaScript
+    and server-side code. Its JavaScript-only implementation
+    provides a DOM parser that uses underlying browser objects.
+    This is, however, not Caja-compatible.
+
+    Therefore a server-side DOM-to-JSON parser mode is provided
+    to support this use case, where either explicitly required
+    (Caja context) or otherwise directly requested.
+  -->
+  <name>jsondom</name>
+  <dependency>globals</dependency>
+  <dependency>domnode</dependency>
+  <dependency>xmlutil</dependency>
+  <gadget>
+    <script src="jsondom.js"/>
+    <api>
+      <exports type="js">gadgets.jsondom.parse</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/jsondom/jsondom.js b/trunk/features/src/main/javascript/features/jsondom/jsondom.js
new file mode 100644
index 0000000..387bb13
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/jsondom/jsondom.js
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+gadgets.jsondom = (function() {
+  var domCache = {};
+
+  function Node(data, opt_nextSibling) {
+    if (typeof data === 'string') {
+      return Text(data, opt_nextSibling);
+    } else if (typeof data === 'object') {
+      if (data.e) {
+        throw new Error(data.e);
+      }
+      return Element(data, opt_nextSibling);
+    }
+    return null;
+  }
+
+  function Element(json, opt_nextSibling) {
+    var nodeType = DOM_ELEMENT_NODE;
+    var tagName = json.n;
+    var attributes = [];
+    var children = [];
+    var nextSibling = opt_nextSibling;
+
+    // Set up attributes.
+    // They are passed as an array named "a", with
+    // each value having "n" = name and "v" = value.
+    for (var i = 0; i < json.a.length; ++i) {
+      attributes.push(Attr(json.a[i].n, json.a[i].v));
+    }
+
+    // Set up children. Do so from the back of the list to
+    // properly set up nextSibling references.
+    var reverseChildren = [];
+    var backChild = (json.c.length > 0 ? Node(json.c[json.c.length - 1]) : null);
+    for (var i = json.c.length - 2; i >= 0; --i) {
+      var next = Node(json.c[i], backChild);
+      reverseChildren.push(next);
+      backChild = next;
+    }
+
+    // children is the reverse of reverseChildren
+    for (var i = reverseChildren.length - 1; i >= 0; --i) {
+      children.push(reverseChildren[i]);
+    }
+
+    return {
+      nodeType: nodeType,
+      tagName: tagName,
+      children: children,
+      attributes: attributes,
+      firstChild: children[0],
+      nextSibling: nextSibling,
+      getAttribute: function(key) {
+        for (var i = 0; i < attributes.length; ++i) {
+          if (attributes[i].nodeName == key) {
+            return attributes[i];
+          }
+        }
+        return null;
+      }
+    };
+  }
+
+  function Text(value, opt_nextSibling, opt_name, opt_type) {
+    var nodeType = opt_type || DOM_TEXT_NODE;
+    var nodeName = opt_name || '#text';
+    var nodeValue = value;
+    var nextSibling = opt_nextSibling;
+
+    return {
+      nodeType: nodeType,
+      nodeName: nodeName,
+      nodeValue: nodeValue,
+      data: nodeValue,
+      nextSibling: nextSibling,
+      cloneNode: function() {
+        return Text(nodeValue, nodeName);
+      }
+    };
+  }
+
+  function Attr(name, value) {
+    return Text(value, null, name, DOM_ATTR_NODE);
+  }
+
+  function preload(id, json) {
+    domCache[id] = Node(json);
+  }
+
+  function parse(str, opt_id) {
+    // Unique ID per parseable String.
+    if (opt_id && domCache[opt_id]) {
+      return domCache[opt_id];
+    }
+
+    // Parse using browser primitives.
+    var doc = opensocial.xmlutil.parseXML(str);
+
+    if (opt_id) {
+      domCache[opt_id] = doc;
+    }
+
+    return doc;
+  }
+
+  return {
+    parse: parse,
+    preload_: preload
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/locked-domain/feature.xml b/trunk/features/src/main/javascript/features/locked-domain/feature.xml
new file mode 100644
index 0000000..a4062c0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/locked-domain/feature.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>locked-domain</name>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/minimessage/feature.xml b/trunk/features/src/main/javascript/features/minimessage/feature.xml
new file mode 100644
index 0000000..2c45200
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/minimessage/feature.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>minimessage</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>core.config</dependency>
+  <dependency>domnode</dependency>
+  <gadget>
+    <script src="minimessage.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.MiniMessage</exports>
+      <exports type="js">gadgets.MiniMessage.prototype.createDismissibleMessage</exports>
+      <exports type="js">gadgets.MiniMessage.prototype.createTimerMessage</exports>
+      <exports type="js">gadgets.MiniMessage.prototype.createStaticMessage</exports>
+      <exports type="js">gadgets.MiniMessage.prototype.dismissMessage</exports>
+      <exports type="js">_IG_MiniMessage</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/minimessage/minimessage.js b/trunk/features/src/main/javascript/features/minimessage/minimessage.js
new file mode 100644
index 0000000..c4e6a46
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/minimessage/minimessage.js
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Library for creating small dismissible messages in gadgets.
+ * Typical use cases:
+ * <ul>
+ * <li> status messages, e.g. loading, saving, etc.
+ * <li> promotional messages, e.g. new features, new gadget, etc.
+ * <li> debug/error messages, e.g. bad input, failed connection to server.
+ * </ul>
+ */
+
+/**
+ * @class MiniMessage class.
+ *
+ * @description Used to create messages that will appear to the user within the
+ *     gadget.
+ * @constructor
+ * @param {string=} opt_moduleId Optional module Id.
+ * @param {Element=} opt_container Optional HTML container element where
+ *                                mini-messages will appear.
+ */
+gadgets.MiniMessage = function(opt_moduleId, opt_container) {
+  this.numMessages_ = 0;
+  this.moduleId_ = opt_moduleId || 0;
+  this.container_ = typeof opt_container === 'object' ?
+                    opt_container : this.createContainer_();
+};
+
+/**
+ * Helper function that creates a container HTML element where mini-messages
+ * will be appended to.  The container element is inserted at the top of gadget.
+ * @return {Element} An HTML div element as the message container.
+ * @private
+ */
+gadgets.MiniMessage.prototype.createContainer_ = function() {
+  var containerId = 'mm_' + this.moduleId_;
+  var container = document.getElementById(containerId);
+
+  if (!container) {
+    container = document.createElement('div');
+    container.id = containerId;
+
+    document.body.insertBefore(container, document.body.firstChild);
+  }
+
+  return container;
+};
+
+/**
+ * Helper function that dynamically inserts CSS rules to the page.
+ * @param {string} cssText CSS rules to inject.
+ * @private
+ */
+gadgets.MiniMessage.addCSS_ = function(cssText) {
+  var head = document.getElementsByTagName('head')[0];
+  if (head) {
+    var styleElement = document.createElement('style');
+    styleElement.type = 'text/css';
+    if (styleElement.styleSheet) {
+      styleElement.styleSheet.cssText = cssText;
+    } else {
+      styleElement.appendChild(document.createTextNode(cssText));
+    }
+    head.insertBefore(styleElement, head.firstChild);
+  }
+};
+
+/**
+ * Helper function that expands a class name into two class names.
+ * @param {string} label The CSS class name.
+ * @return {string} "X Xn", with n is the ID of this module.
+ * @private
+ */
+gadgets.MiniMessage.prototype.cascade_ = function(label) {
+  return label + ' ' + label + this.moduleId_;
+};
+
+/**
+ * Helper function that returns a function that dismisses a message by removing
+ * the message table element from the DOM.  The action is cancelled if the
+ * callback function returns false.
+ * @param {Element} element HTML element to remove.
+ * @param {function()=} opt_callback Optional callback function to be called when
+ *                                the message is dismissed.
+ * @return {function()} A function that dismisses the specified message.
+ * @private
+ */
+gadgets.MiniMessage.prototype.dismissFunction_ = function(element, opt_callback) {
+  return function() {
+    if (typeof opt_callback === 'function' && !opt_callback()) {
+      return;
+    }
+    try {
+      element.parentNode.removeChild(element);
+    } catch (e) {
+      // Silently fail in case the element was already removed.
+    }
+  };
+};
+
+/**
+ * Creates a dismissible message with an [[]x] icon that allows users to dismiss
+ * the message. When the message is dismissed, it is removed from the DOM
+ * and the optional callback function, if defined, is called.
+ * @param {string | Object} message The message as an HTML string or DOM element.
+ * @param {function()=} opt_callback Optional callback function to be called when
+ *                                the message is dismissed.
+ * @return {Element} HTML element of the created message.
+ */
+gadgets.MiniMessage.prototype.createDismissibleMessage = function(message,
+                                                         opt_callback) {
+  var table = this.createStaticMessage(message);
+  var td = document.createElement('td');
+  td.width = 10;
+
+  var span = td.appendChild(document.createElement('span'));
+  span.className = this.cascade_('mmlib_xlink');
+  span.onclick = this.dismissFunction_(table, opt_callback);
+  span.innerHTML = '[x]';
+
+  table.rows[0].appendChild(td);
+
+  return table;
+};
+
+/**
+ * Creates a message that displays for the specified number of seconds.
+ * When the timer expires,
+ * the message is dismissed and the optional callback function is executed.
+ * @param {string | Object} message The message as an HTML string or DOM element.
+ * @param {number} seconds Number of seconds to wait before dismissing
+ *                         the message.
+ * @param {function()=} opt_callback Optional callback function to be called when
+ *                                the message is dismissed.
+ * @return {Element} HTML element of the created message.
+ */
+gadgets.MiniMessage.prototype.createTimerMessage = function(message, seconds,
+                                                            opt_callback) {
+  var table = this.createStaticMessage(message);
+  window.setTimeout(this.dismissFunction_(table, opt_callback), seconds * 1000);
+  return table;
+};
+
+/**
+ * Creates a static message that can only be dismissed programmatically
+ * (by calling dismissMessage()).
+ * @param {string | Object} message The message as an HTML string or DOM element.
+ * @return {Element} HTML element of the created message.
+ */
+gadgets.MiniMessage.prototype.createStaticMessage = function(message) {
+  // Generate and assign unique DOM ID to table.
+  var table = document.createElement('table');
+  table.id = 'mm_' + this.moduleId_ + '_' + this.numMessages_;
+  table.className = this.cascade_('mmlib_table');
+  table.cellSpacing = 0;
+  table.cellPadding = 0;
+  this.numMessages_++;
+
+  var tbody = table.appendChild(document.createElement('tbody'));
+  var tr = tbody.appendChild(document.createElement('tr'));
+
+  // Create message column
+  var td = tr.appendChild(document.createElement('td'));
+
+  // If the message already exists in DOM, preserve its location.
+  // Otherwise, insert it at the top.
+  if (typeof message === 'object' &&
+      message.parentNode &&
+      message.parentNode.nodeType === DOM_ELEMENT_NODE) {
+    var messageClone = message.cloneNode(true);
+    message.style.display = 'none';
+    messageClone.id = '';
+    td.appendChild(messageClone);
+    message.parentNode.insertBefore(table, message.nextSibling);
+  } else {
+    if (typeof message === 'object') {
+      td.appendChild(message);
+    } else {
+      td.innerHTML = html_sanitize(message);
+    }
+    this.container_.appendChild(table);
+  }
+
+  return table;
+};
+
+/**
+ * Dismisses the specified message.
+ * @param {Element} message HTML element of the message to remove.
+ */
+gadgets.MiniMessage.prototype.dismissMessage = function(message) {
+  this.dismissFunction_(message)();
+};
+
+// Injects the default stylesheet for mini-messages.
+gadgets.config.register('minimessage', {}, function(configuration) {
+  // Injects the default stylesheet for mini-messages
+  gadgets.MiniMessage.addCSS_(configuration['minimessage']['css'].join(''));
+});
+
+// Alias for legacy code
+var _IG_MiniMessage = gadgets.MiniMessage;
+
diff --git a/trunk/features/src/main/javascript/features/minimessage/taming.js b/trunk/features/src/main/javascript/features/minimessage/taming.js
new file mode 100644
index 0000000..d49b130
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/minimessage/taming.js
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose gadgets.MiniMessage.* API to cajoled gadgets
+ */
+
+tamings___.push(function(imports) {
+  caja___.whitelistCtors([
+    [gadgets, 'MiniMessage', Object]
+  ]);
+  caja___.whitelistMeths([
+    [gadgets.MiniMessage, 'createDismissibleMessage'],
+    [gadgets.MiniMessage, 'createStaticMessage'],
+    [gadgets.MiniMessage, 'createTimerMessage'],
+    [gadgets.MiniMessage, 'dismissMessage']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/oauthpopup/feature.xml b/trunk/features/src/main/javascript/features/oauthpopup/feature.xml
new file mode 100644
index 0000000..79d77de
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/oauthpopup/feature.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>oauthpopup</name>
+  <dependency>globals</dependency>
+  <gadget>
+    <script src="oauthpopup.js"/>
+    <api>
+      <exports type="js">gadgets.oauth.Popup</exports>
+      <exports type="js">gadgets.oauth.Popup.prototype.createOpenerOnClick</exports>
+      <exports type="js">gadgets.oauth.Popup.prototype.createApprovedOnClick</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/oauthpopup/oauthpopup.js b/trunk/features/src/main/javascript/features/oauthpopup/oauthpopup.js
new file mode 100644
index 0000000..346e338
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/oauthpopup/oauthpopup.js
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview API to assist with management of the OAuth popup window.
+ */
+
+/**
+ * @constructor
+ */
+gadgets.oauth = gadgets.oauth || {};
+
+/**
+ * @class OAuth popup window manager.
+ *
+ * <p>
+ * Expected usage:
+ * </p>
+ *
+ * <ol>
+ * <li>
+ * <p>
+ * Gadget attempts to fetch OAuth data for the user and discovers that
+ * approval is needed.  The gadget creates two new UI elements:
+ * </p>
+ * <ul>
+ *   <li>
+ *      a "personalize this gadget" button or link.
+ *   </li>
+ *   <li>
+ *      a "personalization done" button or link, which is initially hidden.
+ *   </li>
+ * </ul>
+ * <p>
+ * The "personalization done" button may be unnecessary.  The popup window
+ * manager will attempt to detect when the window closes.  However, the
+ * "personalization done" button should still be displayed to handle cases
+ * where the popup manager is unable to detect that a window has closed.  This
+ * allows the user to signal approval manually.
+ * </p>
+ * </li>
+ *
+ * <li>
+ * Gadget creates a popup object and associates event handlers with the UI
+ * elements:
+ *
+ * <pre>
+ *    // Called when the user opens the popup window.
+ *    var onOpen = function() {
+ *      $("personalizeDone").style.display = "block"
+ *    }
+ *    // Called when the user closes the popup window.
+ *    var onClose = function() {
+ *      $("personalizeDone").style.display = "none"
+ *      fetchData();
+ *    }
+ *    var popup = new gadgets.oauth.Popup(
+ *        response.oauthApprovalUrl,
+ *        "height=300,width=200",
+ *        onOpen,
+ *        onClose
+ *    );
+ *
+ *    personalizeButton.onclick = popup.createOpenerOnClick();
+ *    personalizeDoneButton.onclick = popup.createApprovedOnClick();
+ * </pre>
+ * </li>
+ *
+ * <li>
+ * <p>
+ * When the user clicks the personalization button/link, a window is opened
+ * to the approval URL.  The onOpen function is called to notify the gadget
+ * that the window was opened.
+ * </p>
+ * </li>
+ *
+ * <li>
+ * <p>
+ * When the window is closed, the popup manager calls the onClose function
+ * and the gadget attempts to fetch the user's data.
+ * </p>
+ * </li>
+ * </ol>
+ *
+ * @constructor
+ *
+ * @description used to create a new OAuth popup window manager.
+ *
+ * @param {string} destination Target URL for the popup window.
+ * @param {string} windowOptions Options for window.open, used to specify
+ *     look and feel of the window.
+ * @param {function()} openCallback Function to call when the window is opened.
+ * @param {function()} closeCallback Function to call when the window is closed.
+ */
+gadgets.oauth.Popup = function(destination, windowOptions, openCallback,
+    closeCallback) {
+  this.destination_ = destination;
+  this.windowOptions_ = windowOptions;
+  this.openCallback_ = openCallback;
+  this.closeCallback_ = closeCallback;
+  this.win_ = null;
+};
+
+/**
+ * @return {function()} an onclick handler for the "open the approval window" link.
+ */
+gadgets.oauth.Popup.prototype.createOpenerOnClick = function() {
+  var self = this;
+  return function() {
+    self.onClick_();
+  };
+};
+
+/**
+ * Called when the user clicks to open the popup window.
+ *
+ * @return {boolean} false to prevent the default action for the click.
+ * @private
+ */
+gadgets.oauth.Popup.prototype.onClick_ = function() {
+  // If a popup blocker blocks the window, we do nothing.  The user will
+  // need to approve the popup, then click again to open the window.
+  // Note that because we don't call window.open until the user has clicked
+  // something the popup blockers *should* let us through.
+  this.win_ = window.open(this.destination_, '_blank', this.windowOptions_);
+  if (this.win_) {
+    // Poll every 100ms to check if the window has been closed
+    var self = this;
+    var closure = function() {
+      self.checkClosed_();
+    };
+    this.timer_ = window.setInterval(closure, 100);
+    this.openCallback_();
+  }
+  return false;
+};
+
+/**
+ * Called at intervals to check whether the window has closed.
+ * @private
+ */
+gadgets.oauth.Popup.prototype.checkClosed_ = function() {
+  if ((!this.win_) || this.win_.closed) {
+    this.win_ = null;
+    this.handleApproval_();
+  }
+};
+
+/**
+ * Called when we recieve an indication the user has approved access, either
+ * because they closed the popup window or clicked an "I've approved" button.
+ * @private
+ */
+gadgets.oauth.Popup.prototype.handleApproval_ = function() {
+  if (this.timer_) {
+    window.clearInterval(this.timer_);
+    this.timer_ = null;
+  }
+  if (this.win_) {
+    this.win_.close();
+    this.win_ = null;
+  }
+  this.closeCallback_();
+  return false;
+};
+
+/**
+ * @return {function()} an onclick handler for the "I've approved" link.  This may not
+ * ever be called.  If we successfully detect that the window was closed,
+ * this link is unnecessary.
+ */
+gadgets.oauth.Popup.prototype.createApprovedOnClick = function() {
+  var self = this;
+  return function() {
+    self.handleApproval_();
+  };
+};
diff --git a/trunk/features/src/main/javascript/features/open-views.common/feature.xml b/trunk/features/src/main/javascript/features/open-views.common/feature.xml
new file mode 100644
index 0000000..91da11a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.common/feature.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>open-views.common</name>
+  <dependency>globals</dependency>
+  <dependency>rpc</dependency>
+  <dependency>container</dependency>
+  <dependency>views</dependency>
+  <gadget>
+    <script src="open-views-common-gadget.js"/>
+    <api>
+      <exports type="js">gadgets.views.close</exports>
+      <exports type="js">gadgets.window.getContainerDimensions</exports>
+      <uses type="rpc">gadgets.views.close</uses>
+      <uses type="rpc">gadgets.window.getContainerDimensions</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="open-views-common-container.js"/>
+    <api>
+      <exports type="rpc">gadgets.views.close</exports>
+      <exports type="rpc">gadgets.window.getContainerDimensions</exports>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/open-views.common/open-views-common-container.js b/trunk/features/src/main/javascript/features/open-views.common/open-views-common-container.js
new file mode 100644
index 0000000..8dcd942
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.common/open-views-common-container.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side common script.
+ */
+
+osapi.container.Container.addMixin('views', function(container) {
+  var self = this;
+
+  /**
+   * Closes an opened site. If the opt_id parameter is null the container will
+   * close the calling site.
+   *
+   * @param {Object=}
+   *          opt_site: Optional parameter which specifies what site to close.
+   *          If not provided or null, it will close the current gadget site.
+   */
+  container.rpcRegister('gadgets.views.close', function(rpcArgs, opt_site) {
+    // opt_site may be 0, do not do a truthy test on the value.
+    var orig_site = container.getGadgetSiteByIframeId_(rpcArgs.f),
+        site = typeof(opt_site) != 'undefined' && opt_site != null ?
+                container.getSiteById(opt_site) : orig_site;
+
+    if (site && (site == orig_site || site.ownerId_ == rpcArgs.f)) {
+      // The provided method must ultimately call container.closeGadget(site);
+      self.destroyElement(site);
+    }
+  });
+
+  /**
+   * Gets the dimensions of the container displaying the gadget.
+   */
+  container.rpcRegister('gadgets.window.getContainerDimensions', function(rpcArgs) {
+    var el = document.documentElement; // Container element
+    return {
+      width : el ? el.clientWidth : -1,
+      height: el ? el.clientHeight : -1
+    };
+  });
+
+  /**
+   * Method will be called when a gadget wants to close itself or the
+   * parent gadget wants to close a gadget or url site it has opened.
+   *
+   * @param {Object}
+   *          site: The site to close.
+   */
+  this.destroyElement = function(site) {
+    console.log('container needs to define destroyElement function');
+  };
+});
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.common/open-views-common-gadget.js b/trunk/features/src/main/javascript/features/open-views.common/open-views-common-gadget.js
new file mode 100644
index 0000000..bb5d81d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.common/open-views-common-gadget.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview view enhancement library for gadgets.
+ */
+
+gadgets['window'] = gadgets['window'] || {};
+gadgets.views = gadgets.views || {};
+
+/**
+ * Closes an opened site. If the opt_id parameter is null the container will
+ * close the calling site.
+ *
+ * @param {Object=}
+ *          opt_site: Optional parameter which specifies what site to close.
+ *          If null it will close the current gadget site.
+ */
+gadgets.views.close = function(opt_site) {
+  gadgets.rpc.call('..', 'gadgets.views.close', null,
+    opt_site
+  );
+};
+
+/**
+ * Gets the dimensions of the container displaying this gadget through
+ * callback function which will be called with the return value as a
+ * parameter.
+ *
+ * @param {function}
+ *          resultCallback: Callback function will be called with the return
+ *          value as a parameter.
+ */
+gadgets.window.getContainerDimensions = function(resultCallback) {
+  gadgets.rpc.call('..', 'gadgets.window.getContainerDimensions', resultCallback);
+};
diff --git a/trunk/features/src/main/javascript/features/open-views.ee/feature.xml b/trunk/features/src/main/javascript/features/open-views.ee/feature.xml
new file mode 100644
index 0000000..a0990c0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.ee/feature.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>open-views.ee</name>
+  <dependency>open-views.common</dependency>
+  <dependency>open-views.results</dependency>
+  <dependency>container.site.gadget</dependency>
+  <dependency>container.site.url</dependency>
+  <dependency>embedded-experiences</dependency>
+  <dependency>gadgets.json.ext</dependency>
+  <dependency>xmlutil</dependency>
+  <gadget>
+    <script src="open-views-ee-gadget.js"/>
+    <api>
+      <exports type="js">gadgets.views.openEmbeddedExperience</exports>
+      <uses type="rpc">gadgets.views.openEmbeddedExperience</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="open-views-ee-container.js"/>
+    <api>
+      <exports type="rpc">gadgets.views.openEmbeddedExperience</exports>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/open-views.ee/open-views-ee-container.js b/trunk/features/src/main/javascript/features/open-views.ee/open-views-ee-container.js
new file mode 100644
index 0000000..5ebb0e7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.ee/open-views-ee-container.js
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side url script.
+ */
+
+osapi.container.Container.addMixin('views', function(container) {
+  var self = this;
+
+  /**
+   * Opens an embedded experience in the container UI. The location of the site
+   * in the container will be determined by the view target passed in. The
+   * container would open the embedded experience in a dialog, if view target is
+   * dialog or the embedded experience view in a tab for view target is tab.
+   *
+   * @param {number}
+   *          resultCallback: Callback function id to be called when the embedded
+   *          experience closes. The function will be called with the return
+   *          value as a parameter.
+   * @param {Object|string}
+   *          dataModel: The embedded experiences data model object or the xml or
+   *          json string representation of that data model.
+   * @param {Object}
+   *          opt_params: These are optional parameters which can be used to
+   *            open gadgets. The following parameters may be included in this
+   *            object.
+   *            {string} viewTarget: The view that indicates where to open
+   *              the gadget. For example, tab, dialog or modaldialog
+   *            {Object} viewParams: View parameters for the view being
+   *              rendered.
+   *            {Object} coordinates: Object containing the desired absolute
+   *              positioning css parameters (top|bottom|left|right) with
+   *              appropriate values.  All values are relative to the calling
+   *              gadget.
+   *              Do not specify top AND bottom or left AND right parameters to
+   *              indirectly define height and width. Use viewParams for that.
+   *              The result of doing so here is undefined.
+   *              It is expected that coordinates will only be used with
+   *              viewTargets of FLOAT. Containers may implement the behavior
+   *              for other viewTargets, and custom viewTargets at their
+   *              discretion.
+   */
+  container.rpcRegister('gadgets.views.openEmbeddedExperience', function (rpcArgs, resultCallback, dataModel, opt_params) {
+    var navigateCallback = rpcArgs.callback,
+        siteOwnerId = rpcArgs.f,
+        gadgetUrl = dataModel.gadget;
+
+    if (typeof(dataModel) == 'string') {
+      var match = new RegExp('^<(embed)>', 'i').exec(dataModel);
+      if (match && match[1]) {
+        try {
+          var parsed = gadgets.json.xml.convertXmlToJson(opensocial.xmlutil.parseXML(dataModel));
+          dataModel = parsed && parsed[match[1]] || dataModel;
+        } catch(ignore){}
+      } else {
+        try {
+          var parsed = gadgets.json.parse(dataModel);
+          dataModel = parsed || dataModel;
+        } catch(ignore){}
+      }
+    }
+
+    var navigateEE = function(opt_metadata) {
+      var viewTarget = '',
+          viewParams = {},
+          coordinates;
+      if (opt_params) {
+        if (opt_params.viewTarget)
+          viewTarget = opt_params.viewTarget;
+        if (opt_params.viewParams)
+          viewParams = opt_params.viewParams;
+        if (opt_params.coordinates) {
+          coordinates = opt_params.coordinates;
+        }
+      }
+      var orig_site = container.getGadgetSiteByIframeId_(siteOwnerId),
+          rel = orig_site.getActiveSiteHolder().getIframeElement();
+
+      var opt_containerContext = self.getContainerAssociatedContext(dataModel, opt_metadata);
+
+      function callback(element) {
+        var gadgetRenderParams = {};
+        gadgetRenderParams[osapi.container.RenderParam.VIEW] =
+            osapi.container.ee.RenderParam.EMBEDDED;
+        gadgetRenderParams[osapi.container.RenderParam.WIDTH] = '100%';
+        gadgetRenderParams[osapi.container.RenderParam.HEIGHT] = '100%';
+
+        var urlRenderParams = {};
+        urlRenderParams[osapi.container.RenderParam.WIDTH] = '100%';
+        urlRenderParams[osapi.container.RenderParam.HEIGHT] = '100%';
+
+        var eeRenderParams = {};
+        eeRenderParams[osapi.container.ee.RenderParam.GADGET_RENDER_PARAMS] =
+            gadgetRenderParams;
+        eeRenderParams[osapi.container.ee.RenderParam.URL_RENDER_PARAMS] =
+            urlRenderParams;
+        eeRenderParams[osapi.container.ee.RenderParam.GADGET_VIEW_PARAMS] =
+            viewParams;
+
+        container.ee.navigate(element, dataModel, eeRenderParams, function(site, result) {
+          site.ownerId_ = siteOwnerId;
+          if (result) {
+            self.resultCallbacks_[site.getId()] = resultCallback;
+          }
+          if (navigateCallback) {
+            navigateCallback([site.getId(), result]);
+          }
+        }, opt_containerContext);
+      }
+
+      var element = self.createElementForEmbeddedExperience(
+        rel, opt_metadata, viewTarget, coordinates, orig_site, callback
+      );
+
+      if (element) {
+        callback(element);
+      }
+    };
+
+    if(gadgetUrl) {
+      //Check to make sure we can actually reach the gadget we are going to try
+      //to render before we do anything else
+      container.preloadGadget(gadgetUrl, function(result) {
+        if (!result[gadgetUrl] || result[gadgetUrl].error) {
+          //There was an error, check to see if there is still the option to
+          //render the url, else just call the navigateCallback
+          if (!dataModel.url) {
+            if (navigateCallback != null) {
+              navigateCallback([null, result[gadgetUrl] || {"error" : result}]);
+            }
+            return;
+          }
+        }
+        navigateEE(result[gadgetUrl]);
+      });
+    } else {
+      navigateEE();
+    }
+  });
+
+  /**
+   * This function will be called to create the DOM element to place the embedded
+   * experience in. An implementation must either return an element or call
+   * the provided callback asynchronously, but not both.
+   *
+   *@param {Element}
+   *          rel: The element to which opt_coordinates values are
+   *          relative.
+   * @param {Object}
+   *          opt_gadgetInfo: Info for the gadget embedded experience,
+   *          if the data model contains a gadget URL.
+   * @param {string=}
+   *          opt_viewTarget:  Optional parameter, the view target indicates
+   *          where to open.
+   * @param {Object=}
+   *          opt_coordinates: Object containing the desired absolute
+   *          positioning css parameters (top|bottom|left|right) with
+   *          appropriate values. All values are relative to the calling
+   *          gadget.
+   * @param {osapi.container.Site} parentSite
+   *          The site opening the EE.
+   * @param {function(element)} opt_callback
+   *          A callback to asynchronously provide the result of the createElement call.
+   * @return {Object} The DOM element to place the embedded experience in.
+   */
+  this.createElementForEmbeddedExperience = function(rel, opt_gadgetInfo, opt_viewTarget,
+      opt_coordinates, parentSite, opt_callback) {
+    console.log('container needs to define createElementForEmbeddedExperience function');
+  };
+
+  /**
+   * This function will be called to inject additional context when opening gadget in EE mode.
+   *
+   * @param {Object} dataModel: The embedded experiences data model.
+   * @param {Object} opt_gadgetInfo: Info for the gadget embedded experience,
+   *                 if the data model contains a gadget URL.
+   * @return {Object} Additional context need to be passed by container.
+   */
+  this.getContainerAssociatedContext = function(dataModel, opt_gadgetInfo) {
+    return null;
+  }
+
+});
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.ee/open-views-ee-gadget.js b/trunk/features/src/main/javascript/features/open-views.ee/open-views-ee-gadget.js
new file mode 100644
index 0000000..16e256b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.ee/open-views-ee-gadget.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview view enhancement library for gadgets.
+ */
+
+gadgets.views = gadgets.views || {};
+
+/**
+ * Opens an embedded experience in the container UI. The location of the
+ * gadget site in the container will be determined by the view target passed
+ * in. The container would open the view in a dialog, if view target is dialog
+ * or the gadgets view in a tab for view target is tab.
+ *
+ * @param {function}
+ *          resultCallback: Callback function to be called when the gadget
+ *          closes. The function will be called with the return value as a
+ *          parameter.
+ * @param {function}
+ *          navigateCallback: Callback function to be called with the site and
+ *          gadget metadata.
+ * @param {Object|string}
+ *          dataModel: The embedded experiences data model object or the xml or
+ *          json string representation of that data model.
+ * @param {Object}
+ *          opt_params: These are optional parameters which can be used to
+ *            open gadgets. The following parameters may be included in this
+ *            object.
+ *            {string} viewTarget: The view that indicates where to open the
+ *              gadget. For example, tab, dialog or modaldialog
+ *            {Object} viewParams: View parameters for the view being
+ *              rendered.
+ *            {Object} coordinates: Object containing the desired absolute
+ *              positioning css parameters (top|bottom|left|right) with
+ *              appropriate values. All values are relative to the calling
+ *              gadget.
+ *              Do not specify top AND bottom or left AND right parameters to
+ *              indirectly define height and width. Use viewParams for that.
+ *              The result of doing so here is undefined.
+ */
+gadgets.views.openEmbeddedExperience = function(resultCallback,
+        navigateCallback, dataModel, opt_params) {
+  gadgets.rpc.call('..', 'gadgets.views.openEmbeddedExperience', function(result) {
+      navigateCallback.apply(this, result);
+    }, gadgets.views.registerCallback_(resultCallback), dataModel, opt_params
+  );
+};
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.gadget/feature.xml b/trunk/features/src/main/javascript/features/open-views.gadget/feature.xml
new file mode 100644
index 0000000..6bff7a0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.gadget/feature.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>open-views.gadget</name>
+  <dependency>open-views.common</dependency>
+  <dependency>open-views.results</dependency>
+  <dependency>container.site.gadget</dependency>
+  <gadget>
+    <script src="open-views-gadget-gadget.js"/>
+    <api>
+      <exports type="js">gadgets.views.openGadget</exports>
+      <uses type="rpc">gadgets.views.openGadget</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="open-views-gadget-container.js"/>
+    <api>
+      <exports type="rpc">gadgets.views.openGadget</exports>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/open-views.gadget/open-views-gadget-container.js b/trunk/features/src/main/javascript/features/open-views.gadget/open-views-gadget-container.js
new file mode 100644
index 0000000..15f5e0a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.gadget/open-views-gadget-container.js
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side url script.
+ */
+
+osapi.container.Container.addMixin('views', function(container) {
+  var self = this;
+
+  /**
+   * Opens a gadget in the container UI. The location of the gadget site in the
+   * container will be determined by the view target passed in. The container
+   * would open the view in a dialog, if view target is dialog or the gadgets
+   * view in a tab for view target is tab.
+   *
+   * @param {number}
+   *          resultCallback: Callback id of function to be called when the gadget
+   *          closes. The function will be called with the return value as a
+   *          parameter.
+   * @param {Object.<string, string|Object>=}
+   *          opt_params: These are optional parameters which can be used to
+   *            open gadgets. The following parameters may be included in this
+   *            object.
+   *            {string} view: The view to render. Should be one of the
+   *              views returned by calling gadgets.views.getSupportedViews. If
+   *              the view is not included the default view will be rendered.
+   *            {string} viewTarget: The view that indicates where to open the
+   *              gadget. For example, tab, dialog or modaldialog
+   *            {Object} viewParams: View parameters for the view being
+   *              rendered.
+   *            {Object} coordinates: Object containing the desired absolute
+   *              positioning css parameters (top|bottom|left|right) with
+   *              appropriate values. All values are relative to the calling
+   *              gadget.
+   *              Do not specify top AND bottom or left AND right parameters to
+   *              indirectly define height and width, use viewParams for that.
+   *              The result of doing so here is undefined.
+   *              It is expected that coordinates will only be used with
+   *              viewTargets of FLOAT. Containers may implement the behavior
+   *              for other viewTargets, and custom viewTargets at their
+   *              discretion.
+   */
+  container.rpcRegister('gadgets.views.openGadget', function (rpcArgs, resultCallback, opt_params) {
+    var navigateCallback = rpcArgs.callback,
+        siteOwnerId = rpcArgs.f,
+        gadgetUrl = '',
+        orig_site = container.getGadgetSiteByIframeId_(rpcArgs.f);
+
+    if ((typeof orig_site != 'undefined') &&
+            (typeof orig_site.getActiveSiteHolder() != 'undefined')) {
+      // get url through gadget holder
+      gadgetUrl = orig_site.getActiveSiteHolder().getUrl();
+    }
+
+    var view = '',
+        viewTarget = '',
+        viewParams = {},
+        coordinates;
+    if (opt_params) {
+      if (opt_params.view)
+        view = opt_params.view;
+      if (opt_params.viewTarget)
+        viewTarget = opt_params.viewTarget;
+      if (opt_params.viewParams)
+        viewParams = opt_params.viewParams;
+      if(opt_params.coordinates) {
+        coordinates = opt_params.coordinates;
+      }
+    }
+
+    var rel = container.getGadgetSiteByIframeId_(rpcArgs.f).getActiveSiteHolder()
+    .getIframeElement();
+
+    container.preloadGadget(gadgetUrl, function(result) {
+      /*
+       * result[gadgetUrl] : metadata
+       */
+      var metadata = {};
+      if ((typeof result != 'undefined') && (typeof result[gadgetUrl] != 'undefined')) {
+        if (result[gadgetUrl].error) {
+          gadgets.error('Failed to preload gadget : ' + gadgetUrl);
+          if (navigateCallback != null) {
+            navigateCallback([null, result[gadgetUrl]]);
+          }
+          return;
+        } else {
+          metadata = result[gadgetUrl];
+        }
+      }
+
+      function callback(elem) {
+        var renderParams = {},
+            site = container.newGadgetSite(elem);
+
+        site.ownerId_ = siteOwnerId;
+
+        if ((typeof view != 'undefined') && view !== '') {
+          renderParams[osapi.container.RenderParam.VIEW] = view;
+        }
+        renderParams[osapi.container.RenderParam.WIDTH] = '100%';
+        renderParams[osapi.container.RenderParam.HEIGHT] = '100%';
+
+        container.navigateGadget(site, gadgetUrl, viewParams, renderParams, function(metadata) {
+          if (metadata) {
+            self.resultCallbacks_[site.getId()] = resultCallback;
+          }
+          if (navigateCallback) {
+            navigateCallback([site.getId(), metadata]);
+          }
+        });
+      }
+
+      var elem = self.createElementForGadget(
+        metadata, rel, view, viewTarget, coordinates, orig_site, callback
+      );
+      if (elem) {
+        callback(elem);
+      }
+    });
+  });
+
+  /**
+   * Method will be called to create the DOM element to place the Gadget
+   * Site in. An implementation must either return an element or call
+   * the provided callback asynchronously, but not both.
+   *
+   * @param {Object}
+   *          metadata: Gadget meta data for the gadget being opened in
+   *          this GadgetSite.
+   * @param {Element}
+   *          rel: The element to which opt_coordinates values are
+   *          relative.
+   * @param {string=}
+   *          opt_view: Optional parameter, the view that indicates the
+   *          type of GadgetSite.
+   * @param {string=}
+   *          opt_viewTarget: Optional parameter, the view target indicates
+   *          where to open the gadget.
+   * @param {Object=}
+   *          opt_coordinates: Object containing the desired absolute
+   *          positioning css parameters (top|bottom|left|right) with
+   *          appropriate values. All values are relative to the calling
+   *          gadget.
+   * @param {osapi.container.Site} parentSite
+   *          The site opening the gadget view.
+   * @param {function(element)} opt_callback
+   *          A callback to asynchronously provide the result of the createElement call.
+   * @return {Object} The DOM element to place the GadgetSite in.
+   */
+  this.createElementForGadget = function(metadata, rel, opt_view, opt_viewTarget,
+      opt_coordinates, parentSite, opt_callback) {
+    console.log('container needs to define createElementForGadget function');
+  };
+});
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.gadget/open-views-gadget-gadget.js b/trunk/features/src/main/javascript/features/open-views.gadget/open-views-gadget-gadget.js
new file mode 100644
index 0000000..77aec53
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.gadget/open-views-gadget-gadget.js
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview view enhancement library for gadgets.
+ */
+
+gadgets.views = gadgets.views || {};
+
+/**
+ * Opens a gadget in the container UI. The location of the gadget site in the
+ * container will be determined by the view target passed in. The container
+ * would open the view in a dialog, if view target is dialog or the gadgets
+ * view in a tab for view target is tab
+ *
+ * @param {function}
+ *          resultCallback: Callback function to be called when the gadget
+ *          closes. The function will be called with the return value as a
+ *          parameter.
+ * @param {function}
+ *          navigateCallback: Callback function to be called with the
+ *          site and gadget metadata.
+ * @param {Object}
+ *          opt_params: These are optional parameters which can be used to
+ *          open gadgets. The following parameters may be included in this
+ *          object.
+ *            {string} view: The view to render. Should be one of the views
+ *              returned by calling gadgets.views.getSupportedViews. If the
+ *              view is not included the default view will be rendered.
+ *            {string} viewTarget: The view that indicates where to open the
+ *              gadget. For example, tab, dialog or modaldialog
+ *            {Object} viewParams: View parameters for the view being
+ *              rendered.
+ *            {Object} coordinates: Object containing the desired absolute
+ *              positioning css parameters (top|bottom|left|right) with
+ *              appropriate values. All values are relative to the calling
+ *              gadget.
+ *              Do not specify top AND bottom or left AND right parameters to
+ *              indirectly define height and width. Use viewParams for that.
+ *              The result of doing so here is undefined.
+ */
+
+gadgets.views.openGadget = function(resultCallback, navigateCallback,
+        opt_params) {
+  gadgets.rpc.call('..', 'gadgets.views.openGadget', function(result) {
+      navigateCallback.apply(this, result);
+    }, gadgets.views.registerCallback_(resultCallback), opt_params
+  );
+};
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.results/feature.xml b/trunk/features/src/main/javascript/features/open-views.results/feature.xml
new file mode 100644
index 0000000..0aa44f9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.results/feature.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>open-views.results</name>
+  <dependency>open-views.common</dependency>
+  <gadget>
+    <script src="open-views-results-gadget.js"/>
+    <api>
+      <exports type="js">gadgets.views.setReturnValue</exports>
+      <exports type="rpc">gadgets.views.deliverResult</exports>
+      <uses type="rpc">gadgets.views.setReturnValue</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="open-views-results-container.js"/>
+    <api>
+      <exports type="rpc">gadgets.views.setReturnValue</exports>
+      <uses type="rpc">gadgets.views.deliverResult</uses>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/open-views.results/open-views-results-container.js b/trunk/features/src/main/javascript/features/open-views.results/open-views-results-container.js
new file mode 100644
index 0000000..7250c85
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.results/open-views-results-container.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side common script.
+ */
+
+osapi.container.Container.addMixin('views', function(container) {
+  this.resultCallbacks_ = {}; // Mapping between id and callback function
+  this.returnValues_ = {}; // Mapping between id and return value
+
+  var self = this,
+      lifecyclecb = {};
+  lifecyclecb[osapi.container.CallbackType.ON_BEFORE_CLOSE] = function(site) {
+    var id = site.getId(),
+        returnValue = self.returnValues_[id],
+        resultCallback = self.resultCallbacks_[id];
+
+    // Checking the truthiness of resultCallback is bad because 0 is a valid value.
+    // Check whether it is undefined
+    if (typeof resultCallback !== 'undefined') {
+      if (site.ownerId_) {
+        gadgets.rpc.call(site.ownerId_, 'gadgets.views.deliverResult', null,
+          resultCallback, returnValue
+        );
+      }
+    }
+
+    delete self.returnValues_[id];
+    delete self.resultCallbacks_[id];
+  };
+  container.addGadgetLifecycleCallback("open-views", lifecyclecb);
+
+  /**
+   * Sets the return value for the current window. This method should only be
+   * called inside those secondary view types defined in gadgets.views.ViewType.
+   * For example, DIALOG or MODALDIALOG
+   *
+   * @param {object}
+   *          returnValue: Return value for this window.
+   */
+  container.rpcRegister('gadgets.views.setReturnValue', function (rpcArgs, returnValue) {
+    var site = container.getGadgetSiteByIframeId_(rpcArgs.f);
+    if (site) {
+      self.returnValues_[site.getId()] = returnValue;
+    }
+  });
+});
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.results/open-views-results-gadget.js b/trunk/features/src/main/javascript/features/open-views.results/open-views-results-gadget.js
new file mode 100644
index 0000000..437fd29
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.results/open-views-results-gadget.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview view enhancement library for gadgets.
+ */
+
+gadgets.views = gadgets.views || {};
+
+(function() {
+  var resultCallbackMap = {},
+      rcbnum = 0;
+
+  gadgets.util.registerOnLoadHandler(function() {
+    gadgets.rpc.register('gadgets.views.deliverResult', function(rcbnum, result) {
+      if (this.f !== '..') {
+        return;
+      }
+      var resultCallback;
+      if (resultCallback = resultCallbackMap[rcbnum]) {
+        delete resultCallbackMap[rcbnum];
+        resultCallback(result);
+      }
+    });
+  });
+
+  gadgets.views.registerCallback_ = function(cb) {
+    resultCallbackMap[rcbnum] = cb;
+    return rcbnum++;
+  };
+
+  /**
+   * Sets the return value for the current window. This method should only be
+   * called inside those secondary view types defined in gadgets.views.ViewType.
+   * For example, DIALOG or MODALDIALOG
+   *
+   * @param {object}
+   *          returnValue: Return value for this window.
+   */
+  gadgets.views.setReturnValue = function(returnValue) {
+    gadgets.rpc.call('..', 'gadgets.views.setReturnValue', null,
+      returnValue
+    );
+  };
+})();
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.url/feature.xml b/trunk/features/src/main/javascript/features/open-views.url/feature.xml
new file mode 100644
index 0000000..90940f3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.url/feature.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>open-views.url</name>
+  <dependency>open-views.common</dependency>
+  <dependency>container.site.url</dependency>
+  <gadget>
+    <script src="open-views-url-gadget.js"/>
+    <api>
+      <exports type="js">gadgets.views.openUrl</exports>
+      <uses type="rpc">gadgets.views.openUrl</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="open-views-url-container.js"/>
+    <api>
+      <exports type="rpc">gadgets.views.openUrl</exports>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/open-views.url/open-views-url-container.js b/trunk/features/src/main/javascript/features/open-views.url/open-views-url-container.js
new file mode 100644
index 0000000..712f39b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.url/open-views-url-container.js
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side url script.
+ */
+
+osapi.container.Container.addMixin('views', function(container) {
+  var self = this;
+
+  /**
+   * Opens a URL in the container UI. The location of the URL site will be
+   * determined by the container based on the target view. The container would
+   * open the view in a dialog, if opt_viewTarget=dialog or the gadgets view in
+   * a tab for opt_viewTarget=tab
+   *
+   * @param {string}
+   *          url: URL to a web page to open in a URL site in the container.
+   *          (Note this should not be a URL to a gadget definition.).
+   * @param {string=}
+   *          opt_viewTarget: Optional parameter, the view that indicates where
+   *          to open the URL.
+   * @param {Object=} opt_coordinates: Object containing the desired absolute
+   *          positioning css parameters (top|bottom|left|right) with
+   *          appropriate values.  All values are relative to the calling
+   *          gadget.
+   *          You may specify top AND bottom or left AND right parameters to
+   *          indirectly define height and width.
+   *          It is expected that coordinates will only be used with
+   *          viewTargets of FLOAT. Containers may implement the behavior
+   *          for other viewTargets, and custom viewTargets at their
+   *          discretion.
+   * @returns {string} The ID of the site created, if a callback was registered.
+   */
+  container.rpcRegister('gadgets.views.openUrl', function (rpcArgs, url, opt_viewTarget, opt_coordinates) {
+    var orig_site = container.getGadgetSiteByIframeId_(rpcArgs.f),
+      rel = orig_site.getActiveSiteHolder().getIframeElement();
+
+    function callback(content_div) {
+      var site = container.newUrlSite(content_div);
+
+      var renderParams = {}; // (height, width, class,userPrefsObject)
+      renderParams[osapi.container.RenderParam.WIDTH] = '100%';
+      renderParams[osapi.container.RenderParam.HEIGHT] = '100%';
+
+      container.navigateUrl(site, url, renderParams);
+
+      // record who opened this site, so that if they use the siteId to close it later,
+      // we don't inadvertently allow other gadgets to guess the id and close the site.
+      site.ownerId_ = rpcArgs.f;
+      rpcArgs.callback([site.getId()]);
+    };
+
+    var content_div = self.createElementForUrl(rel, opt_viewTarget, opt_coordinates, orig_site, callback);
+    if (content_div) {
+      callback(content_div);
+    }
+  });
+
+  /**
+   * Method will be called to create the DOM element to place the UrlSite
+   * in. An implementation must either return an element or call
+   * the provided callback asynchronously, but not both.
+   *
+   * @param {Element}
+   *          rel: The element to which opt_coordinates values are
+   *          relative.
+   * @param {string=}
+   *          opt_view: Optional parameter, the view to open. If not
+   *          included the container should use its default view.
+   * @param {Object=}
+   *          opt_coordinates: Object containing the desired absolute
+   *          positioning css parameters (top|bottom|left|right) with
+   *          appropriate values. All values are relative to the calling
+   *          gadget.
+   * @param {osapi.container.Site} parentSite
+   *          The site opening the url.
+   * @param {function(element)} opt_callback
+   *          A callback to asynchronously provide the result of the createElement call.
+   * @return {Object} The DOM element to place the UrlSite object in.
+   */
+  this.createElementForUrl = function(rel, opt_viewTarget, opt_coordinates, parentSite, opt_callback) {
+    console.log('container needs to define createElementForUrl function');
+  };
+});
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views.url/open-views-url-gadget.js b/trunk/features/src/main/javascript/features/open-views.url/open-views-url-gadget.js
new file mode 100644
index 0000000..ff2fbda
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views.url/open-views-url-gadget.js
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview view enhancement library for gadgets.
+ */
+
+gadgets.views = gadgets.views || {};
+
+/**
+ * Opens a URL in the container UI. The location of the URL site will be
+ * determined by the container based on the target view. The container would
+ * open the view in a dialog, if opt_viewTarget=dialog or the gadgets view in
+ * a tab for opt_viewTarget=tab
+ *
+ * @param {string}
+ *          url: URL to a web page to open in a URL site in the container.
+ *          (Note this should not be a URL to a gadget definition.).
+ * @param {function}
+ *          navigateCallback: Callback function to be called with the
+ *          site which has been opened.
+ * @param {string=}
+ *          opt_viewTarget: Optional parameter, the view that indicates where
+ *          to open the URL.
+ * @param {Object=} opt_coordinates: Object containing the desired absolute
+ *          positioning css parameters (top|bottom|left|right) with
+ *          appropriate values. All values are relative to the calling
+ *          gadget.
+ *          You may specify top AND bottom or left AND right parameters to
+ *          indirectly define height and width
+ */
+gadgets.views.openUrl = function(url, navigateCallback, opt_viewTarget, opt_coordinates) {
+  gadgets.rpc.call('..', 'gadgets.views.openUrl', function(result) {
+    navigateCallback.call(this, result);
+  }, url, opt_viewTarget, opt_coordinates);
+};
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/open-views/feature.xml b/trunk/features/src/main/javascript/features/open-views/feature.xml
new file mode 100644
index 0000000..b6ec356
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/open-views/feature.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>open-views</name>
+  <dependency>open-views.ee</dependency>
+  <dependency>open-views.gadget</dependency>
+  <dependency>open-views.url</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensearch/feature.xml b/trunk/features/src/main/javascript/features/opensearch/feature.xml
new file mode 100644
index 0000000..c868034
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensearch/feature.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>opensearch</name>
+  <dependency>globals</dependency>
+  <dependency>container</dependency>
+  <dependency>gadgets.json.ext</dependency>
+  <container>
+    <script src="opensearch.js"/>
+    <api>
+      <exports type="js">osapi.container.Container.prototype.opensearch.getOpenSearchURLs</exports>
+      <exports type="js">osapi.container.Container.prototype.opensearch.getOpenSearchDescriptions</exports>
+      <exports type="js">osapi.container.Container.prototype.opensearch.addOpenSearchCallback</exports>
+      <exports type="js">osapi.container.Container.prototype.opensearch.removeOpenSearchCallback</exports>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensearch/opensearch.js b/trunk/features/src/main/javascript/features/opensearch/opensearch.js
new file mode 100644
index 0000000..ff78cdd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensearch/opensearch.js
@@ -0,0 +1,306 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library adds open search support to the container.
+ */
+
+(function() {
+  // array of opensearch descriptors.
+  var descriptions = {};
+  var ids = {};
+  // open search callbacks.
+  var callbacks = new Array();
+
+  /**
+   * Converts an XML string into a DOM object
+   *
+   * @param {string} xmlString representation of a valid XML object.
+   * @return DOM Object
+   */
+  function createDom(xmlString) {
+    var xmlDoc;
+    if (window.DOMParser) {
+      var parser = new DOMParser();
+      xmlDoc = parser.parseFromString(xmlString, 'text/xml');
+    } else {
+      xmlDoc = new ActiveXObject('Microsoft.XMLDOM');
+      xmlDoc.async = 'false';
+      xmlDoc.loadXML(xmlString);
+    }
+    return xmlDoc;
+  }
+
+  /**
+   * Extracts OpenSearch descriptions from XML string containing the
+   * description and stores them in the internal map If valid
+   * descriptions are found, notifies all callbacks about changes.
+   *
+   * @param {string} domDesc
+   *            stringified XML OpenSearch description.
+   * @param {string} title
+   *            the title of the gadget that the description belongs to.
+   */
+  function extractDescriptions(domDesc, url) {
+    var jsonDesc = gadgets.json.xml.convertXmlToJson(domDesc);
+    if (jsonDesc != null) {
+      if (jsonDesc.OpenSearchDescription.Url != null) {
+        if (descriptions[url] == null) {
+          descriptions[url] = jsonDesc;
+          applyCallbacks(true, jsonDesc);
+        }
+      }
+    }
+  }
+
+  /**
+   * Notifies the registered callbacks of changes in the OpenSearch registry.
+   *
+   * @param {boolean} added
+   *            true if added, false if removed.
+   * @param {string} description opensearch description.
+   */
+  function applyCallbacks(added, description) {
+    for (var i in callbacks) {
+      callbacks[i].apply(this, [description, added]);
+    }
+  }
+
+  /**
+   * Removes an opensearch description from the registry after the containing
+   * gadget is unloaded or closed.
+   *
+   * @param {string} url Url of the gadget to be removed.
+   */
+  function removeDescription(url) {
+    if (descriptions[url] != null) {
+      applyCallbacks(false, descriptions[url]);
+      delete descriptions[url];
+    }
+  }
+
+  /**
+   * Processes a new gadget definition and checks if it has an opensearch
+   * feature
+   *
+   * @param {Array} response
+   *            Metadata response containing the json representation of the
+   *            gadget module.
+   */
+  function preloaded(response) {
+    for (var item in response) {
+      if (!response[item].error && response[item].modulePrefs) {
+        // check for os feature
+        var feature = response[item].modulePrefs.features['opensearch'];
+        var title = response[item].modulePrefs.title;
+        if (feature != null) {
+          var params = feature.params;
+          if (params != null) {
+            // retrieve the description
+            var desc = params['opensearch-description'];
+            // The full description is in the gadget
+            if (desc != null) {
+              // convert string to dom object
+              var domDesc = createDom(desc);
+              // convert dom object to json
+              extractDescriptions(domDesc, response[item].url);
+              // only the url to the full description is provided.
+            } else {
+              var openSearchUrl = params['opensearch-url'];
+              if (openSearchUrl != null) {
+                function urlCallback(response) {
+                  if (response.errors.length == 0) {
+                    var domData = response.data;
+                    if (domData != null) {
+                      extractDescriptions(domData, response[item].url);
+                    }
+                  }
+                }
+                var params = {};
+                params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+                    gadgets.io.ContentType.DOM;
+                gadgets.io.makeRequest(openSearchUrl, urlCallback, params);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * When a gadget is closed, checks if it has a corresponding OpenSearch
+   * definition that needs to be removed, and notifies the appropriate
+   * callbacks.
+   *
+   * @param {gadgetSite}
+   *            site of the gadget being closed.
+   */
+  function closed(gadgetSite) {
+    if (gadgetSite != null) {
+      url = ids[gadgetSite.getId()];
+      removeDescription(url);
+    }
+  }
+
+  /**
+   * called when a gadget is navigated to.
+   *
+   * @param {gadgetInfo}
+   *            json object representing the gadget module.
+   */
+  function navigated(gadgetSite) {
+    if (gadgetSite != null) {
+      if (gadgetSite.getActiveSiteHolder() != null) {
+        url = gadgetSite.getActiveSiteHolder().getUrl();
+        if (descriptions[url] == null) {
+          preloaded([gadgetSite.getActiveSiteHolder().getGadgetInfo()]);
+        }
+        ids[gadgetSite.getId()] = url;
+      }
+    }
+  }
+
+  /**
+   * called when a gadget is unloaded
+   *
+   * @param {gadgetURL}
+   *            the gadget url of the unloaded gadget.
+   */
+  function unloaded(gadgetURL) {
+    // do nothing--this doesn't guarantee the gadget is actually removed.
+  }
+
+  /**
+   * finds opensearch descriptions/urls based on the mimetype of the search
+   * results
+   *
+   * @param {type}
+   *            mimeType of the search results.
+   * @param {isUrl}
+   *            true if looking for template urls, false if looking for full
+   *            OpenSearch descriptions.
+   */
+  var findByType = function(type, isUrl) {
+    var typedDescriptions = [];
+    for (url in descriptions) {
+      var searchUrls = [];
+      if (!(descriptions[url].OpenSearchDescription.Url instanceof Array)) {
+        searchUrls.push(descriptions[url].OpenSearchDescription.Url);
+      } else {
+        searchUrls = descriptions[url].OpenSearchDescription.Url;
+      }
+      var found = false;
+      // go through all the urls in a description.
+      // if a description contains a template of the type, the
+      // entire description is returned. For URLs, only the matching
+      // template url is returned.
+      for (var i in searchUrls) {
+        var template = searchUrls[i]['@template'];
+        if (template != null) {
+          var descType = searchUrls[i]['@type'];
+          if (descType == type || type == null) {
+            if (isUrl) {
+              typedDescriptions.push(template);
+            } else {
+              typedDescriptions.push(descriptions[url]);
+              break;
+            }
+          }
+        }
+      }
+    }
+    return typedDescriptions;
+  }
+
+  var containerCallback = new Object();
+  containerCallback[osapi.container.CallbackType.ON_PRELOADED] =
+      preloaded;
+  containerCallback[osapi.container.CallbackType.ON_BEFORE_CLOSE] =
+      closed;
+  containerCallback[osapi.container.CallbackType.ON_NAVIGATED] =
+      navigated;
+
+  osapi.container.Container.addMixin('opensearch', function(context) {
+    context.addGadgetLifecycleCallback('opensearch', containerCallback);
+
+    return /** @scope container.opensearch */ {
+    /**
+     * @param {type} type type name, eg search-xml, search-atom, search-hmtl.
+     * @return opensearch template URLs of a given type, or all URLs if type
+     *         was null.
+     */
+    getOpenSearchURLs: function(type) {
+      return findByType(type, true);
+    },
+
+    /**
+     * @param {type}
+     *            type type name, eg search-xml, search-atom, search-html.
+     * @return Returns OpenSearch descriptions of a given type, or all
+     *         descriptions if type was null.
+     */
+    getOpenSearchDescriptions: function(type) {
+      return findByType(type, false);
+    },
+
+    /**
+     * Allows other functions to subscribe to changes in the OpenSearch
+     * registry.
+     *
+     * @param {callback}
+     *           function(description, boolean added), where description is the
+     *           opensearch description being updated and where added is true
+     *           if the gadget is new, and false if it has been
+     *           closed/unloaded.
+     *
+     */
+    addOpenSearchCallback: function(callback) {
+      callbacks.push(callback);
+    },
+
+    /**
+     * Removes a previously registered callback
+     *
+     * @param {callback}
+     *            previously registered callback function.
+     * @return {boolean} true if function was present, false otherwise.
+     *
+     */
+    removeOpenSearchCallback: function(callback) {
+      for (index in callbacks) {
+        if (callbacks[index] == callback) {
+          callbacks.splice(index, 1);
+          return true;
+        }
+      }
+      return false;
+    }
+
+    /*
+     * Convenience method for unit testing, allowing the test script to set
+     * internal descriptions. Uncomment for unit testing.
+     *
+     */
+    //setDescriptions_: function(testDescriptions) {
+    // descriptions=testDescriptions;
+    //}
+    };
+  });
+})();
+
diff --git a/trunk/features/src/main/javascript/features/opensocial-0.9/feature.xml b/trunk/features/src/main/javascript/features/opensocial-0.9/feature.xml
new file mode 100644
index 0000000..8c3786f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-0.9/feature.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-0.9</name>
+  <dependency>opensocial</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-1.0/feature.xml b/trunk/features/src/main/javascript/features/opensocial-1.0/feature.xml
new file mode 100644
index 0000000..989f3b4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-1.0/feature.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-1.0</name>
+  <dependency>opensocial</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-2.0/feature.xml b/trunk/features/src/main/javascript/features/opensocial-2.0/feature.xml
new file mode 100644
index 0000000..50faaf9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-2.0/feature.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-2.0</name>
+  <dependency>opensocial</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-2.5/feature.xml b/trunk/features/src/main/javascript/features/opensocial-2.5/feature.xml
new file mode 100644
index 0000000..85ae990
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-2.5/feature.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-2.5</name>
+  <dependency>opensocial</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/feature.xml b/trunk/features/src/main/javascript/features/opensocial-base/feature.xml
new file mode 100644
index 0000000..dcb41d6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/feature.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-base</name>
+  <dependency>opensocial-reference</dependency> 
+  <gadget>
+    <script src="fieldtranslations.js"></script>
+    <script src="jsonmediaitem.js"></script>
+    <script src="jsonalbum.js"></script>
+    <script src="jsonactivity.js"></script>
+    <script src="jsonperson.js"></script>    
+    <script src="jsonmessagecollection.js"></script>
+    <script src="jsonmessage.js"></script>
+    <script src="taming.js" caja="1"></script>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/fieldtranslations.js b/trunk/features/src/main/javascript/features/opensocial-base/fieldtranslations.js
new file mode 100644
index 0000000..7de3fa1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/fieldtranslations.js
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Helper class used to translate from the 0.8 server apis to the 0.8 js apis
+ * (which are unfortunately not the same).
+ */
+
+window['FieldTranslations'] = (function() {
+  // code that is used internally
+  function translateEnumJson(enumJson) {
+    if (enumJson) {
+      enumJson.key = enumJson.value;
+    }
+  };
+
+  function translateUrlJson(urlJson) {
+    if (urlJson) {
+      urlJson.address = urlJson.value;
+    }
+  };
+
+  return {
+    'translateEnumJson' : translateEnumJson,
+    'translateUrlJson' : translateUrlJson,
+    'translateServerPersonToJsPerson' :
+        function(serverJson, opt_params) {
+          if (serverJson.emails) {
+            for (var i = 0; i < serverJson.emails.length; i++) {
+              serverJson.emails[i].address = serverJson.emails[i].value;
+            }
+          }
+
+          if (serverJson.phoneNumbers) {
+            for (var p = 0; p < serverJson.phoneNumbers.length; p++) {
+              serverJson.phoneNumbers[p].number = serverJson.phoneNumbers[p].value;
+            }
+          }
+
+          if (serverJson.birthday) {
+            serverJson.dateOfBirth = serverJson.birthday;
+          }
+
+          if (serverJson.utcOffset) {
+            serverJson.timeZone = serverJson.utcOffset;
+          }
+
+          if (serverJson.addresses) {
+            for (var j = 0; j < serverJson.addresses.length; j++) {
+              serverJson.addresses[j].unstructuredAddress = serverJson.addresses[j].formatted;
+            }
+          }
+
+          if (serverJson.gender) {
+            var key = serverJson.gender == 'male' ? 'MALE' :
+                (serverJson.gender == 'female') ? 'FEMALE' :
+                null;
+            serverJson.gender = {key: key, displayValue: serverJson.gender};
+          }
+
+          translateUrlJson(serverJson.profileSong);
+          translateUrlJson(serverJson.profileVideo);
+
+          if (serverJson.urls) {
+            for (var u = 0; u < serverJson.urls.length; u++) {
+              translateUrlJson(serverJson.urls[u]);
+            }
+          }
+
+          translateEnumJson(serverJson.drinker);
+          translateEnumJson(serverJson.lookingFor);
+          translateEnumJson(serverJson.networkPresence);
+          translateEnumJson(serverJson.smoker);
+
+          if (serverJson.organizations) {
+            serverJson.jobs = [];
+            serverJson.schools = [];
+
+            for (var o = 0; o < serverJson.organizations.length; o++) {
+              var org = serverJson.organizations[o];
+              if (org.type == 'job') {
+                serverJson.jobs.push(org);
+              } else if (org.type == 'school') {
+                serverJson.schools.push(org);
+              }
+            }
+          }
+
+          if (serverJson.name) {
+            serverJson.name.unstructured = serverJson.name.formatted;
+          }
+
+          if (serverJson.appData) {
+            serverJson.appData = opensocial.Container.escape(
+                serverJson.appData, opt_params, true);
+          }
+
+        },
+
+
+    'translateJsPersonFieldsToServerFields' :
+        function(fields) {
+          for (var i = 0; i < fields.length; i++) {
+            if (fields[i] == 'dateOfBirth') {
+              fields[i] = 'birthday';
+            } else if (fields[i] == 'timeZone') {
+              fields[i] = 'utcOffset';
+            } else if (fields[i] == 'jobs') {
+              fields[i] = 'organizations';
+            } else if (fields[i] == 'schools') {
+              fields[i] = 'organizations';
+            }
+          }
+
+          // displayName and id always need to be requested
+          fields.push('id');
+          fields.push('displayName');
+        },
+
+    'translateIsoStringToDate' : function(isoString) {
+      // Date parsing code from http://delete.me.uk/2005/03/iso8601.html
+      var regexp = '([0-9]{4})(-([0-9]{2})(-([0-9]{2})' +
+          '(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?' +
+          '(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?';
+      var d = isoString.match(new RegExp(regexp));
+
+      var offset = 0;
+      var date = new Date(d[1], 0, 1);
+
+      if (d[3]) { date.setMonth(d[3] - 1); }
+      if (d[5]) { date.setDate(d[5]); }
+      if (d[7]) { date.setHours(d[7]); }
+      if (d[8]) { date.setMinutes(d[8]); }
+      if (d[10]) { date.setSeconds(d[10]); }
+      if (d[12]) { date.setMilliseconds(Number('0.' + d[12]) * 1000); }
+      if (d[14]) {
+        offset = (Number(d[16]) * 60) + Number(d[17]);
+        offset *= ((d[15] == '-') ? 1 : -1);
+      }
+
+      offset -= date.getTimezoneOffset();
+      var time = (Number(date) + (offset * 60 * 1000));
+
+      return new Date(Number(time));
+    },
+
+    /**
+     * AppData is provided by the REST and JSON-RPC protocols using
+     * an "appData" or "appData.key" field, but is described by
+     * the JS fetchPerson() API in terms of an appData param.  Translate
+     * between the two.
+     */
+    'addAppDataAsProfileFields' : function(opt_params) {
+      if (opt_params) {
+        // Push the appData keys in as profileDetails
+        if (opt_params['appData']) {
+          var appDataKeys = opt_params['appData'];
+          if (typeof appDataKeys === 'string') {
+            appDataKeys = [appDataKeys];
+          }
+
+          var profileDetail = opt_params['profileDetail'] || [];
+          for (var i = 0; i < appDataKeys.length; i++) {
+            if (appDataKeys[i] === '*') {
+              profileDetail.push('appData');
+            } else {
+              profileDetail.push('appData.' + appDataKeys[i]);
+            }
+          }
+
+          opt_params['appData'] = appDataKeys;
+        }
+      }
+    },
+
+    /**
+     * Translate standard Javascript arguments to JSON-RPC protocol format.
+     */
+    'translateStandardArguments' :
+        function(opt_params, rpc_params) {
+          if (opt_params['first']) {
+            rpc_params.startIndex = opt_params['first'];
+          }
+          if (opt_params['max']) {
+            rpc_params.count = opt_params['max'];
+          }
+          if (opt_params['sortOrder']) {
+            rpc_params.sortBy = opt_params['sortOrder'];
+          }
+          if (opt_params['filter']) {
+            rpc_params.filterBy = opt_params['filter'];
+          }
+          if (opt_params['filterOp']) {
+            rpc_params.filterOp = opt_params['filterOp'];
+          }
+          if (opt_params['filterValue']) {
+            rpc_params.filterValue = opt_params['filterValue'];
+          }
+          if (opt_params['fields']) {
+            rpc_params.fields = opt_params['fields'];
+          }
+        },
+
+    /**
+     * Translate network distance from id spec to JSON-RPC parameters.
+     */
+    'translateNetworkDistance' :
+        function(idSpec, rpc_params) {
+          if (idSpec.getField('networkDistance')) {
+            rpc_params.networkDistance = idSpec.getField('networkDistance');
+          }
+        }
+        // end of returned Object, please no commas
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/jsonactivity.js b/trunk/features/src/main/javascript/features/opensocial-base/jsonactivity.js
new file mode 100644
index 0000000..66ce2b3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/jsonactivity.js
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * Base interface for json based person objects.
+ *
+ * @private
+ * @constructor
+ */
+var JsonActivity = function(opt_params, opt_skipConversions) {
+  opt_params = opt_params || {};
+  if (!opt_skipConversions) {
+    JsonActivity.constructArrayObject(opt_params, 'mediaItems', JsonMediaItem);
+  }
+  opensocial.Activity.call(this, opt_params);
+};
+JsonActivity.inherits(opensocial.Activity);
+
+JsonActivity.prototype.toJsonObject = function() {
+  var jsonObject = JsonActivity.copyFields(this.fields_);
+
+  var oldMediaItems = jsonObject['mediaItems'] || [];
+  var newMediaItems = [];
+  for (var i = 0; i < oldMediaItems.length; i++) {
+    newMediaItems[i] = oldMediaItems[i].toJsonObject();
+  }
+  jsonObject['mediaItems'] = newMediaItems;
+
+  return jsonObject;
+};
+
+// TODO: Pull this method into a common class, it is from jsonperson.js
+JsonActivity.constructArrayObject = function(map, fieldName, className) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    for (var i = 0; i < fieldValue.length; i++) {
+      fieldValue[i] = new className(fieldValue[i]);
+    }
+  }
+};
+
+// TODO: Pull into common class as well
+JsonActivity.copyFields = function(oldObject) {
+  var newObject = {};
+  for (var field in oldObject) {
+    newObject[field] = oldObject[field];
+  }
+  return newObject;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/jsonalbum.js b/trunk/features/src/main/javascript/features/opensocial-base/jsonalbum.js
new file mode 100644
index 0000000..c2714b4
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/jsonalbum.js
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+var JsonAlbum = function(opt_params) {
+  opt_params = opt_params || {};
+
+  JsonAlbum.constructObject(opt_params, 'location', opensocial.Address);
+
+  opensocial.Album.call(this, opt_params);
+};
+JsonAlbum.inherits(opensocial.Album);
+
+JsonAlbum.prototype.toJsonObject = function() {
+  return JsonAlbum.copyFields(this.fields_);
+};
+
+// Converts the fieldName into an instance of the specified object
+JsonAlbum.constructObject = function(map, fieldName, className) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    map[fieldName] = new className(fieldValue);
+  }
+};
+
+//TODO: Pull into common class as well
+JsonAlbum.copyFields = function(oldObject) {
+  var newObject = {};
+  for (var field in oldObject) {
+    newObject[field] = oldObject[field];
+  }
+  return newObject;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/jsonmediaitem.js b/trunk/features/src/main/javascript/features/opensocial-base/jsonmediaitem.js
new file mode 100644
index 0000000..e874306
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/jsonmediaitem.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+var JsonMediaItem = function(opt_params) {
+  opt_params = opt_params || {};
+
+  opensocial.MediaItem.call(this, opt_params['mimeType'], opt_params['url'], opt_params);
+};
+
+JsonMediaItem.inherits(opensocial.MediaItem);
+
+JsonMediaItem.prototype.toJsonObject = function() {
+  return JsonMediaItem.copyFields(this.fields_);
+};
+
+//TODO: Pull into common class as well
+JsonMediaItem.copyFields = function(oldObject) {
+  var newObject = {};
+  for (var field in oldObject) {
+    newObject[field] = oldObject[field];
+  }
+  return newObject;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/jsonmessage.js b/trunk/features/src/main/javascript/features/opensocial-base/jsonmessage.js
new file mode 100644
index 0000000..01a6df8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/jsonmessage.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Base interface for json-based message objects.
+ *
+ * @private
+ * @constructor
+ * @param {string} body
+ * @param {Object=} opt_params
+ */
+var JsonMessage = function(body, opt_params) {
+  opt_params = opt_params || {};
+  opensocial.Message.call(this, body, opt_params);
+};
+JsonMessage.inherits(opensocial.Message);
+
+JsonMessage.prototype.toJsonObject = function() {
+  return JsonMessage.copyFields(this.fields_);
+};
+
+// TODO: Pull into common class as well
+JsonMessage.copyFields = function(oldObject) {
+  var newObject = {};
+  for (var field in oldObject) {
+    newObject[field] = oldObject[field];
+  }
+  return newObject;
+};
+
+
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/jsonmessagecollection.js b/trunk/features/src/main/javascript/features/opensocial-base/jsonmessagecollection.js
new file mode 100644
index 0000000..27dee88
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/jsonmessagecollection.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Base interface for json-based message objects.
+ *
+ * @private
+ * @constructor
+ */
+var JsonMessageCollection = function(opt_params) {
+  opt_params = opt_params || {};
+  opensocial.MessageCollection.call(this, opt_params);
+};
+JsonMessageCollection.inherits(opensocial.MessageCollection);
+
+JsonMessageCollection.prototype.toJsonObject = function() {
+  return JsonMessageCollection.copyFields(this.fields_);
+};
+
+// TODO: Pull this method into a common class, it is from jsonperson.js
+//JsonMessage.constructArrayObject = function(map, fieldName, className) {
+//  var fieldValue = map[fieldName];
+//  if (fieldValue) {
+//    for (var i = 0; i < fieldValue.length; i++) {
+//      fieldValue[i] = new className(fieldValue[i]);
+//    }
+//  }
+//}
+
+// TODO: Pull into common class as well
+JsonMessageCollection.copyFields = function(oldObject) {
+  var newObject = {};
+  for (var field in oldObject) {
+    newObject[field] = oldObject[field];
+  }
+  return newObject;
+};
+
+
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/jsonperson.js b/trunk/features/src/main/javascript/features/opensocial-base/jsonperson.js
new file mode 100644
index 0000000..4d69c17
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/jsonperson.js
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * Base interface for json based person objects.
+ *
+ * @private
+ * @constructor
+ */
+var JsonPerson = function(opt_params) {
+  opt_params = opt_params || {};
+
+  JsonPerson.constructObject(opt_params, 'bodyType', opensocial.BodyType);
+  JsonPerson.constructObject(opt_params, 'currentLocation', opensocial.Address);
+  JsonPerson.constructObject(opt_params, 'name', opensocial.Name);
+  JsonPerson.constructObject(opt_params, 'profileSong', opensocial.Url);
+  JsonPerson.constructObject(opt_params, 'profileVideo', opensocial.Url);
+
+  JsonPerson.constructDate(opt_params, 'dateOfBirth');
+
+  JsonPerson.constructArrayObject(opt_params, 'addresses', opensocial.Address);
+  JsonPerson.constructArrayObject(opt_params, 'emails', opensocial.Email);
+  JsonPerson.constructArrayObject(opt_params, 'jobs', opensocial.Organization);
+  JsonPerson.constructArrayObject(opt_params, 'phoneNumbers', opensocial.Phone);
+  JsonPerson.constructArrayObject(opt_params, 'schools',
+      opensocial.Organization);
+  JsonPerson.constructArrayObject(opt_params, 'urls', opensocial.Url);
+
+  JsonPerson.constructEnum(opt_params, 'gender');
+  JsonPerson.constructEnum(opt_params, 'smoker');
+  JsonPerson.constructEnum(opt_params, 'drinker');
+  JsonPerson.constructEnum(opt_params, 'networkPresence');
+  JsonPerson.constructEnumArray(opt_params, 'lookingFor');
+
+  opensocial.Person.call(this, opt_params, opt_params['isOwner'],
+      opt_params['isViewer']);
+};
+JsonPerson.inherits(opensocial.Person);
+
+// Converts the fieldName into an instance of an opensocial.Enum
+JsonPerson.constructEnum = function(map, fieldName) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    map[fieldName] = new opensocial.Enum(fieldValue.key, fieldValue.displayValue);
+  }
+};
+
+// Converts the fieldName into an array of instances of an opensocial.Enum
+JsonPerson.constructEnumArray = function(map, fieldName) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    for (var i = 0; i < fieldValue.length; i++) {
+      fieldValue[i] = new opensocial.Enum(fieldValue[i].key, fieldValue[i].displayValue);
+    }
+  }
+};
+
+// Converts the fieldName into an instance of the specified object
+JsonPerson.constructObject = function(map, fieldName, className) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    map[fieldName] = new className(fieldValue);
+  }
+};
+
+// Converts the fieldName into an instance of a Date
+JsonPerson.constructDate = function(map, fieldName) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    map[fieldName] = FieldTranslations.translateIsoStringToDate(fieldValue);
+  }
+};
+
+JsonPerson.constructArrayObject = function(map, fieldName, className) {
+  var fieldValue = map[fieldName];
+  if (fieldValue) {
+    for (var i = 0; i < fieldValue.length; i++) {
+      fieldValue[i] = new className(fieldValue[i]);
+    }
+  }
+};
+
+JsonPerson.prototype.getDisplayName = function() {
+  return this.getField('displayName');
+};
+
+JsonPerson.prototype.getAppData = function(key) {
+  var appData = this.getField('appData');
+  return appData && appData[key];
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-base/taming.js b/trunk/features/src/main/javascript/features/opensocial-base/taming.js
new file mode 100644
index 0000000..0362fff
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-base/taming.js
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+tamings___.push(function(imports) {
+  caja___.whitelistCtors([
+    [window, 'JsonActivity', opensocial.Activity],
+    [window, 'JsonAlbum', opensocial.Album],
+    [window, 'JsonMediaItem', opensocial.MediaItem],
+    [window, 'JsonMessage', opensocial.Message],
+    [window, 'JsonMessageCollection', opensocial.MessageCollection],
+    [window, 'JsonPerson', opensocial.Person]
+  ]);
+  caja___.whitelistMeths([
+    [JsonPerson, 'getDisplayName'],
+    [JsonPerson, 'getAppData']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/opensocial-current/feature.xml b/trunk/features/src/main/javascript/features/opensocial-current/feature.xml
new file mode 100644
index 0000000..80ac716
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-current/feature.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial</name>
+  <dependency>core.config</dependency>
+  <dependency>opensocial-jsonrpc</dependency>
+  <dependency>security-token</dependency>
+  <!-- <dependency>caja</dependency> -->
+  <!-- Must include the "caja" feature to display samplecontainer -->
+  <!-- gadgets when "use caja" is checked -->
+  <gadget>    
+    <script>
+      var requiredConfig = {
+        "path": gadgets.config.NonEmptyStringValidator,
+        "invalidatePath": gadgets.config.NonEmptyStringValidator,
+        "domain": gadgets.config.NonEmptyStringValidator,
+        "enableCaja": gadgets.config.BooleanValidator,
+        "supportedFields": gadgets.config.ExistsValidator
+      };
+
+      gadgets.config.register("opensocial", requiredConfig,
+        function(config) {
+          ShindigContainer = function() {
+            JsonRpcContainer.call(this, config["opensocial"]);
+          };
+          ShindigContainer.inherits(JsonRpcContainer);
+
+          opensocial.Container.setContainer(new ShindigContainer());
+      });
+    </script>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-data-context/datacontext.js b/trunk/features/src/main/javascript/features/opensocial-data-context/datacontext.js
new file mode 100644
index 0000000..3295d3f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-data-context/datacontext.js
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Implements the global implicit data context for containers.
+ */
+
+var opensocial = opensocial || {};
+
+/**
+ * @type {Object} The namespace declaration for this file.
+ */
+opensocial.data = opensocial.data || {};
+
+/**
+ * @type {Object} Global DataContext to contain requested data sets.
+ */
+opensocial.data.DataContext = function() {
+  var listeners = [];
+  var dataSets = {};
+
+  /**
+   * Puts a data set into the global DataContext object. Fires listeners
+   * if they are satisfied by the associated key being inserted.
+   *
+   * @param {string} key The key to associate with this object.
+   * @param {ResponseItem|Object} obj The data object.
+   * @param {boolean=} opt_fireListeners Default true.
+   */
+  var putDataSet = function(key, obj, opt_fireListeners) {
+    if (typeof obj === 'undefined' || obj === null) {
+      return;
+    }
+
+    dataSets[key] = obj;
+    if (!(opt_fireListeners === false)) {
+      fireCallbacks(key);
+    }
+  };
+
+  /**
+   * Registers a callback listener for a given set of keys.
+   * @param {string|Array.<string>} keys Key or set of keys to listen on.
+   * @param {function(Array.<string>)} callback Function to call when a
+   * listener is fired.
+   * @param {booelan} oneTimeListener Remove this listener after first callback?
+   * @param {boolean} fireIfReady Instantly fire this if all data is available?
+   */
+  var registerListener = function(keys, callback, oneTimeListener, fireIfReady) {
+    var oneTime = !!oneTimeListener;
+    var listener = {keys: {}, callback: callback, oneTime: oneTime};
+
+    if (typeof keys === 'string') {
+      listener.keys[keys] = true;
+      if (keys != '*') {
+        keys = [keys];
+      }
+    } else {
+      for (var i = 0; i < keys.length; i++) {
+        listener.keys[keys[i]] = true;
+      }
+    }
+
+    listeners.push(listener);
+
+    // Check to see if this one should fire immediately.
+    if (fireIfReady && keys !== '*' && isDataReady(listener.keys)) {
+      window.setTimeout(function() {
+        maybeFireListener(listener, keys);
+      }, 1);
+    }
+  };
+
+  /**
+   * Checks if the data for a map of keys is available.
+   * @param {Object.<string, *>} keys An map of keys to check.
+   * @return {boolean} Data for all the keys is present.
+   */
+  var isDataReady = function(keys) {
+    if (keys['*']) {
+      return true;
+    }
+
+    for (var key in keys) {
+      if (typeof dataSets[key] === 'undefined') {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  /**
+   * Fires a listener for a key, but only if the data is ready for other
+   * keys this listener is bound to.
+   * @param {Object} listener The listener object.
+   * @param {string} key The key that this listener is being fired for.
+   */
+  var maybeFireListener = function(listener, key) {
+    if (isDataReady(listener.keys)) {
+      listener.callback(key);
+      if (listener.oneTime) {
+        removeListener(listener);
+      }
+    }
+  };
+
+  /**
+   * Removes a listener from the list.
+   * @param {Object} listener The listener to remove.
+   */
+  var removeListener = function(listener) {
+    for (var i = 0; i < listeners.length; ++i) {
+      if (listeners[i] == listener) {
+        listeners.splice(i, 1);
+        return;
+      }
+    }
+  };
+
+  /**
+   * Scans all active listeners and fires off any callbacks that inserting this
+   * key or list of keys satisfies.
+   * @param {string|Array.<string>} keys The key that was updated.
+   * @private
+   */
+  var fireCallbacks = function(keys) {
+    if (typeof(keys) == 'string') {
+      keys = [keys];
+    }
+    for (var i = 0; i < listeners.length; ++i) {
+      var listener = listeners[i];
+      for (var j = 0; j < keys.length; j++) {
+        var key = keys[j];
+        if (listener.keys[key] || listener.keys['*']) {
+          maybeFireListener(listener, keys);
+          break;
+        }
+      }
+    }
+  };
+
+
+  return {
+
+    /**
+     * Returns a map of existing data.
+     * @return {Object} A map of current data sets.
+     * TODO: Add to the spec API?
+     */
+    getData: function() {
+      var data = {};
+      for (var key in dataSets) {
+        if (dataSets.hasOwnProperty(key)) {
+          data[key] = dataSets[key];
+        }
+      }
+      return data;
+    },
+
+    /**
+     * Registers a callback listener for a given set of keys.
+     * @param {string|Array.<string>} keys Key or set of keys to listen on.
+     * @param {function(Array.<string>)} callback Function to call when a
+     * listener is fired.
+     */
+    registerListener: function(keys, callback) {
+      registerListener(keys, callback, false, true);
+    },
+
+    /**
+     * Private version of registerListener which allows one-time listeners to
+     * be registered. Not part of the spec. Exposed because needed by
+     * opensocial-templates.
+     * @param {string|Array.<string>} keys Key or set of keys to listen on.
+     * @param {function(Array.<string>)} callback Function to call when a.
+     */
+    registerOneTimeListener_: function(keys, callback) {
+      registerListener(keys, callback, true, true);
+    },
+
+    /**
+     * Private version of registerListener which allows listeners to be
+     * registered that do not fire initially, but only after a data change.
+     * Exposed because needed by opensocial-templates.
+     * @param {string|Array.<string>} keys Key or set of keys to listen on.
+     * @param {function(Array.<string>)} callback Function to call when a.
+     */
+    registerDeferredListener_: function(keys, callback) {
+      registerListener(keys, callback, false, false);
+    },
+
+    /**
+     * Retrieve a data set for a given key.
+     * @param {string} key Key for the requested data set.
+     * @return {Object} The data set object.
+     */
+    getDataSet: function(key) {
+      return dataSets[key];
+    },
+
+    /**
+     * Puts a data set into the global DataContext object. Fires listeners
+     * if they are satisfied by the associated key being inserted.
+     *
+     * @param {string} key The key to associate with this object.
+     * @param {ResponseItem|Object} obj The data object.
+     */
+    putDataSet: function(key, obj) {
+      putDataSet(key, obj, true);
+    },
+
+    /**
+     * Inserts multiple data sets from a JSON object.
+     * @param {Object.<string, Object>} dataSets a JSON object containing Data
+     * sets keyed by Data Set Key. All the DataSets are added, before firing
+     * listeners.
+     */
+    putDataSets: function(dataSets) {
+      var keys = [];
+      for (var key in dataSets) {
+        keys.push(key);
+        putDataSet(key, dataSets[key], false);
+      }
+      fireCallbacks(keys);
+    }
+  };
+}();
+
+
+/**
+ * Accessor to the shared, global DataContext.
+ */
+opensocial.data.getDataContext = function() {
+  return opensocial.data.DataContext;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-data-context/feature.xml b/trunk/features/src/main/javascript/features/opensocial-data-context/feature.xml
new file mode 100644
index 0000000..003f1fb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-data-context/feature.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-data-context</name>
+  <dependency>taming</dependency>
+  <gadget>    
+    <script src="datacontext.js"></script>
+    <script src="taming.js" caja="1"></script>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-data-context/taming.js b/trunk/features/src/main/javascript/features/opensocial-data-context/taming.js
new file mode 100644
index 0000000..c926803
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-data-context/taming.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose the opensocial.data.DataContext API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [opensocial.data, 'getDataContext'],
+    [opensocial.data.DataContext, 'putDataSet']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/opensocial-data/data.js b/trunk/features/src/main/javascript/features/opensocial-data/data.js
new file mode 100644
index 0000000..1513bb9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-data/data.js
@@ -0,0 +1,632 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Implements the global implicit data context for containers.
+ *
+ * TODO:
+ *   Variable substitution in markup.
+ *   Support cross-cutting predicates (page, sort, search).
+ *   URL parameter support.
+ */
+
+/**
+ * @type {string} The key attribute constant.
+ * @const
+ */
+
+opensocial.data.ATTR_KEY = 'key';
+
+/**
+ * @type {string} The type of script tags that contain data markup.
+ * @const
+ */
+opensocial.data.SCRIPT_TYPE = 'text/os-data';
+
+opensocial.data.NSMAP = {};
+
+opensocial.data.VAR_REGEX = /^([\w\W]*?)(\$\{[^\}]*\})([\w\W]*)$/;
+
+/**
+ * A RequestDescriptor is a wrapper for an XML tag specifying a data request.
+ * This object can be used to access attributes of the request - performing
+ * necessary variable substitutions from the global DataContext. An instance of
+ * this object will be passed to the Data Request Handler so it can obtain its
+ * parameters through it.
+ * @constructor
+ * @param {Element} xmlNode An XML DOM node representing the request.
+ */
+opensocial.data.RequestDescriptor = function(xmlNode) {
+  this.tagName = xmlNode.tagName;
+  this.tagParts = this.tagName.split(':');
+  this.attributes = {};
+
+  // Flag to indicate that this request depends on other requests.
+  this.dependencies = false;
+
+  for (var i = 0; i < xmlNode.attributes.length; ++i) {
+    var name = xmlNode.attributes[i].nodeName;
+    if (name) {
+      var value = xmlNode.getAttribute(name);
+      if (name && value) {
+        this.attributes[name] = value;
+        // TODO: This attribute may not be used by the handler.
+        this.computeNeededKeys_(value);
+      }
+    }
+  }
+
+  this.key = this.attributes[opensocial.data.ATTR_KEY];
+  this.register_();
+};
+
+
+/**
+ * Checks if an attribute has been specified for this tag.
+ * @param {string} name The attribute name.
+ * @return {boolean} The attribute is set.
+ */
+opensocial.data.RequestDescriptor.prototype.hasAttribute = function(name) {
+  return !!this.attributes[name];
+};
+
+
+/**
+ * Returns the value of a specified attribute. If the attribute includes
+ * variable substitutions, they will be evaluated against the DataContext and
+ * the result returned.
+ *
+ * @param {string} name The attribute name to look up.
+ * @return {Object} The result of evaluation.
+ */
+opensocial.data.RequestDescriptor.prototype.getAttribute = function(name) {
+  var attrExpression = this.attributes[name];
+  if (!attrExpression) {
+    return attrExpression;
+  }
+  // TODO: Don't do this every time - cache the result.
+  var expression = opensocial.data.parseExpression_(attrExpression);
+  if (!expression) {
+    return attrExpression;
+  }
+  return opensocial.data.DataContext.evalExpression(expression);
+};
+
+
+opensocial.data.parseExpression_ = function(value) {
+  if (!value.length) {
+    return null;
+  }
+  var substRex = opensocial.data.VAR_REGEX;
+  var text = value;
+  var parts = [];
+  var match = text.match(substRex);
+  if (!match) {
+    return null;
+  }
+  while (match) {
+    if (match[1].length > 0) {
+      parts.push(opensocial.data.transformLiteral_(match[1]));
+    }
+    var expr = match[2].substring(2, match[2].length - 1);
+    parts.push('(' + expr + ')');
+    text = match[3];
+    match = text.match(substRex);
+  }
+  if (text.length > 0) {
+    parts.push(opensocial.data.transformLiteral_(text));
+  }
+  return parts.join('+');
+};
+
+
+/**
+ * Transforms a literal string for inclusion into a variable evaluation:
+ *   - Escapes single quotes.
+ *   - Replaces newlines with spaces.
+ *   - Addes single quotes around the string.
+ * @private
+ */
+opensocial.data.transformLiteral_ = function(string) {
+  return "'" + string.replace(/'/g, "\\'").
+      replace(/\n/g, ' ') + "'";
+};
+
+
+/**
+ * Sends this request off to be fulfilled. The current DataContext state will
+ * be used to reslove any variable references.
+ */
+opensocial.data.RequestDescriptor.prototype.sendRequest = function() {
+  var ns = opensocial.data.NSMAP[this.tagParts[0]];
+  var handler = null;
+  if (ns) {
+    handler = ns[this.tagParts[1]];
+  }
+  if (!handler) {
+    throw Error('Data handler undefined for ' + this.tagName);
+  }
+  handler(this);
+};
+
+
+/**
+ * Creates a closure to this RequestDescriptor's sendRequest() method.
+ */
+opensocial.data.RequestDescriptor.prototype.getSendRequestClosure = function() {
+  var self = this;
+  return function() {
+    self.sendRequest();
+  };
+};
+
+
+/**
+ * Computes the keys needed by an attribute by looking for variable substitution
+ * markup. For example if the attribute is "http://example.com/${user.id}", the
+ * "user" key is needed. The needed keys are set as properties into a member of
+ * this RequestDescriptor.
+ * @param {string} attribute The value of the attribute to inspect.
+ * @private
+ */
+opensocial.data.RequestDescriptor.prototype.computeNeededKeys_ = function(attribute) {
+  var substRex = opensocial.data.VAR_REGEX;
+  var match = attribute.match(substRex);
+  while (match) {
+    var token = match[2].substring(2, match[2].length - 1);
+    var key = token.split('.')[0];
+    if (!this.neededKeys) {
+      this.neededKeys = {};
+    }
+    this.neededKeys[key] = true;
+    match = match[3].match(substRex);
+  }
+};
+
+
+/**
+ * Registers this RequestDescriptor using its key.
+ * @private
+ */
+opensocial.data.RequestDescriptor.prototype.register_ = function() {
+  opensocial.data.registerRequestDescriptor(this);
+};
+
+
+
+/**
+ * Evaluates a JS expression against the DataContext.
+ * @param {string} expr The expression to evaluate.
+ * @return {Object} The result of evaluation.
+ */
+opensocial.data.DataContext.evalExpression = function(expr) {
+  return (new Function('context',
+      'with (context) return ' + expr))(opensocial.data.DataContext.getData());
+};
+
+
+/**
+ * @type {Object} Map of currently registered RequestDescriptors (by key).
+ * @private
+ */
+opensocial.data.requests_ = {};
+
+
+/**
+ * Registers a RequestDescriptor by key in the global registry.
+ * @param {RequestDescriptor} requestDescriptor The RequestDescriptor to
+ * register.
+ */
+opensocial.data.registerRequestDescriptor = function(requestDescriptor) {
+  if (opensocial.data.requests_[requestDescriptor.key]) {
+    throw Error('Request already registered for ' + requestDescriptor.key);
+  }
+  opensocial.data.requests_[requestDescriptor.key] = requestDescriptor;
+};
+
+
+/**
+ * @type {DataRequest} A shared DataRequest object for batching OS API data
+ * calls.
+ * @private
+ */
+opensocial.data.currentAPIRequest_ = null;
+
+
+/**
+ * @type {Array.<string>} An array of keys requested by the shared DataRequest.
+ * @private
+ */
+opensocial.data.currentAPIRequestKeys_ = null;
+
+
+/**
+ * @type {Object.<string, function(Object)>} A map of custom callbacks for the
+ * keys in the shared DataRequest.
+ * @private
+ */
+opensocial.data.currentAPIRequestCallbacks_ = null;
+
+
+/**
+ * Gets the shared DataRequest, constructing it lazily when needed.
+ * Access to this object is provided so that various sub-requests can be
+ * constructed (i.e. via newFetchPersonRequest()). Neither add() nor send()
+ * should be called on this object - doing so will lead to undefined behavior.
+ * Use opensocial.data.addToCurrentAPIRequest() instead.
+ * TODO: Create a wrapper that doesn't support add() and send().
+ * @return {DataRequest} The shared DataRequest.
+ */
+opensocial.data.getCurrentAPIRequest = function() {
+  if (!opensocial.data.currentAPIRequest_) {
+    opensocial.data.currentAPIRequest_ = opensocial.newDataRequest();
+    opensocial.data.currentAPIRequestKeys_ = [];
+    opensocial.data.currentAPIRequestCallbacks_ = {};
+  }
+  return opensocial.data.currentAPIRequest_;
+};
+
+
+/**
+ * Adds a request to the current shared DataRequest object. Any requests
+ * added in a synchronous block of code will be batched. The requests will be
+ * automatically sent once the syncronous block is done executing.
+ * @param {Object} request Specifies data to fetch
+ * (constructed via DataRequest's newFetch???Request() methods).
+ * @param {string} key The key to map generated response data to.
+ * @param {function(string, ResponseItem)=} opt_callback An optional callback
+ * function to pass the returned ResponseItem to. If present, the function will
+ * be called with the key and ResponseItem as params. If this is omitted, the
+ * ResponseItem will be passed to putDataSet() with the specified key.
+ */
+opensocial.data.addToCurrentAPIRequest = function(request, key, opt_callback) {
+  opensocial.data.getCurrentAPIRequest().add(request, key);
+  opensocial.data.currentAPIRequestKeys_.push(key);
+
+  if (opt_callback) {
+    opensocial.data.currentAPIRequestCallbacks_[key] = opt_callback;
+  }
+
+  window.setTimeout(opensocial.data.sendCurrentAPIRequest_, 0);
+};
+
+
+/**
+ * Sends out the current shared DataRequest. The reference is removed, so that
+ * when new requests are added, a new shared DataRequest object will be
+ * constructed.
+ * @private
+ */
+opensocial.data.sendCurrentAPIRequest_ = function() {
+  if (opensocial.data.currentAPIRequest_) {
+    opensocial.data.currentAPIRequest_.send(opensocial.data.createSharedRequestCallback_());
+    opensocial.data.currentAPIRequest_ = null;
+  }
+};
+
+
+/**
+ * Creates a callback closure for processing a DataResponse. The closure
+ * remembers which keys were requested, and what custom callbacks need to be
+ * called.
+ * @return {function(DataResponse)} a handler for DataResponse.
+ * @private
+ */
+opensocial.data.createSharedRequestCallback_ = function() {
+  var keys = opensocial.data.currentAPIRequestKeys_;
+  var callbacks = opensocial.data.currentAPIRequestCallbacks_;
+  return function(data) {
+    opensocial.data.onAPIResponse(data, keys, callbacks);
+  };
+};
+
+
+/**
+ * Processes a response to the shared API DataRequest by looping through
+ * requested keys and notifying appropriate parties of the received data.
+ * @param {DataResonse} responseItem Data received from the server.
+ * @param {Array.<string>} keys The list of keys that were requested.
+ * @param {Object.<string, function(string, ResponseItem)>} callbacks A map of
+ * any custom callbacks by key.
+ */
+opensocial.data.onAPIResponse = function(responseItem, keys, callbacks) {
+  for (var i = 0; i < keys.length; i++) {
+    var key = keys[i];
+    var item = responseItem.get(key);
+    if (!item.hadError()) {
+      var data = opensocial.data.extractJson_(item, key);
+      if (callbacks[key]) {
+        callbacks[key](key, data);
+      } else {
+        opensocial.data.DataContext.putDataSet(key, data);
+      }
+    }
+    // TODO: What should we do if there *is* an error?
+  }
+};
+
+/**
+ * Extract the JSON payload from the ResponseItem. This includes
+ * iterating over an array of API objects and extracting their JSON into a
+ * simple array structure.
+ * @private
+ */
+opensocial.data.extractJson_ = function(responseItem, key) {
+  var data = responseItem.getData();
+  if (data.array_) {
+    var out = [];
+    for (var i = 0; i < data.array_.length; i++) {
+      out.push(data.array_[i].fields_);
+    }
+    data = out;
+
+    // For os:PeopleRequests that request @groupId="self", crack the array
+    var request = opensocial.data.requests_[key];
+    if (request.tagName == 'os:PeopleRequest') {
+      var groupId = request.getAttribute('groupId');
+      if ((!groupId || groupId == '@self') && data.length == 1) {
+        data = data[0];
+      }
+    }
+  } else {
+    data = data.fields_ || data;
+  }
+  return data;
+};
+
+
+/**
+ * Registers a tag as a data request handler.
+ * @param {string} name Prefixed tag name.
+ * @param {function(DataResponse)} handler Method to call when this tag is invoked.
+ *
+ * TODO: Store these tag handlers separately from the ones for UI tags.
+ * TODO: Formalize the callback interface.
+ */
+opensocial.data.registerRequestHandler = function(name, handler) {
+  var tagParts = name.split(':');
+  var ns = opensocial.data.NSMAP[tagParts[0]];
+  if (!ns) {
+    if (!opensocial.xmlutil.NSMAP[tagParts[0]]) {
+      opensocial.xmlutil.NSMAP[tagParts[0]] = null;
+    }
+    ns = opensocial.data.NSMAP[tagParts[0]] = {};
+  } else if (ns[tagParts[1]]) {
+    throw Error('Request handler ' + tagParts[1] + ' is already defined.');
+  }
+
+  ns[tagParts[1]] = handler;
+};
+
+
+/**
+ * Loads and executes all inline data request sections.
+ * @param {Object=} opt_doc Optional document to use instead of window.document.
+ * TODO: Currently this processes all 'script' blocks together,
+ *     instead of collecting them all and then processing together. Not sure
+ *     which is preferred yet.
+ * TODO: Figure out a way to pass in params used only for data
+ *     and not for template rendering.
+ */
+opensocial.data.processDocumentMarkup = function(opt_doc) {
+  var doc = opt_doc || document;
+  var nodes = doc.getElementsByTagName('script');
+  for (var i = 0; i < nodes.length; ++i) {
+    var node = nodes[i];
+    if (node.type == opensocial.data.SCRIPT_TYPE) {
+      opensocial.data.loadRequests(node);
+    }
+  }
+  opensocial.data.registerRequestDependencies();
+  opensocial.data.executeRequests();
+};
+
+
+/**
+ * Process the document when it's ready.
+ */
+if (window['gadgets'] && window['gadgets']['util']) {
+  gadgets.util.registerOnLoadHandler(opensocial.data.processDocumentMarkup);
+}
+
+
+/**
+ * Parses XML data and constructs the pending request list.
+ * @param {Element|string} xml A DOM element or string containing XML.
+ */
+opensocial.data.loadRequests = function(xml) {
+  if (typeof(xml) == 'string') {
+    opensocial.data.loadRequestsFromMarkup_(xml);
+    return;
+  }
+  var node = xml;
+  xml = node.value || node.innerHTML;
+  opensocial.data.loadRequestsFromMarkup_(xml);
+};
+
+
+/**
+ * Parses XML data and constructs the pending request list.
+ * @param {string} xml A string containing XML markup.
+ * @private
+ */
+opensocial.data.loadRequestsFromMarkup_ = function(xml) {
+  xml = opensocial.xmlutil.prepareXML(xml);
+  var doc = opensocial.xmlutil.parseXML(xml);
+
+  // Find the <root> node (skip DOCTYPE).
+  var node = doc.firstChild;
+  while (node.nodeType != DOM_ELEMENT_NODE) {
+    node = node.nextSibling;
+  }
+
+  opensocial.data.processDataNode_(node);
+};
+
+
+/**
+ * Processes a data request node for data sets.
+ * @param {Node} node The node to process.
+ * @private
+ */
+opensocial.data.processDataNode_ = function(node) {
+  for (var child = node.firstChild; child; child = child.nextSibling) {
+    if (child.nodeType == DOM_ELEMENT_NODE) {
+      var requestDescriptor = new opensocial.data.RequestDescriptor(child);
+    }
+  }
+};
+
+
+opensocial.data.registerRequestDependencies = function() {
+  for (var key in opensocial.data.requests_) {
+    var request = opensocial.data.requests_[key];
+    var neededKeys = request.neededKeys;
+    var dependencies = [];
+    for (var neededKey in neededKeys) {
+      if (opensocial.data.DataContext.getDataSet(neededKey) == null &&
+          opensocial.data.requests_[neededKey]) {
+        dependencies.push(neededKey);
+      }
+    }
+    if (dependencies.length > 0) {
+      opensocial.data.DataContext.registerListener(dependencies,
+          request.getSendRequestClosure());
+      request.dependencies = true;
+    }
+  }
+};
+
+
+opensocial.data.executeRequests = function() {
+  for (var key in opensocial.data.requests_) {
+    var request = opensocial.data.requests_[key];
+    if (!request.dependencies) {
+      request.sendRequest();
+    }
+  }
+};
+
+
+/**
+ * Transforms "@"-based special values such as "@owner" into uppercase
+ * keywords like "OWNER".
+ * @param {string} value The value to transform.
+ * @return {string} Transformed or original value.
+ */
+opensocial.data.transformSpecialValue = function(value) {
+  if (value.substring(0, 1) == '@') {
+    return value.substring(1).toUpperCase();
+  }
+  return value;
+};
+
+
+/**
+ * Parses a string of comma-separated field names and adds the resulting array
+ * (if any) to the params object.
+ * @param {Object} params The params object used to construct an Opensocial
+ * DataRequest.
+ * @param {string} fieldsStr A string containing comma-separated field names.
+ * @private
+ */
+opensocial.data.addFieldsToParams_ = function(params, fieldsStr) {
+  if (!fieldsStr) {
+    return;
+  }
+  var fields = fieldsStr.replace(/(^\s*|\s*$)/g, '').split(/\s*,\s*/);
+  params[opensocial.DataRequest.PeopleRequestFields.PROFILE_DETAILS] = fields;
+};
+
+
+/**
+ * Anonymous function defines OpenSocial specific requests.
+ * Automatically called when this file is loaded.
+ */
+(function() {
+  opensocial.data.registerRequestHandler('os:ViewerRequest', function(descriptor) {
+    var params = {};
+    opensocial.data.addFieldsToParams_(params, descriptor.getAttribute('fields'));
+    var req = opensocial.data.getCurrentAPIRequest().newFetchPersonRequest('VIEWER', params);
+    // TODO: Support @fields param.
+    opensocial.data.addToCurrentAPIRequest(req, descriptor.key);
+  });
+
+  opensocial.data.registerRequestHandler('os:OwnerRequest', function(descriptor) {
+    var params = {};
+    opensocial.data.addFieldsToParams_(params, descriptor.getAttribute('fields'));
+    var req = opensocial.data.getCurrentAPIRequest().newFetchPersonRequest('OWNER', params);
+    // TODO: Support @fields param.
+    opensocial.data.addToCurrentAPIRequest(req, descriptor.key);
+  });
+
+  opensocial.data.registerRequestHandler('os:PeopleRequest', function(descriptor) {
+    var userId = descriptor.getAttribute('userId');
+    var groupId = descriptor.getAttribute('groupId') || '@self';
+    var idSpec = {};
+    idSpec.userId = opensocial.data.transformSpecialValue(userId);
+    if (groupId != '@self') {
+      idSpec.groupId = opensocial.data.transformSpecialValue(groupId);
+    }
+    var params = {};
+    opensocial.data.addFieldsToParams_(params, descriptor.getAttribute('fields'));
+    // TODO: Support other params.
+    var req = opensocial.data.getCurrentAPIRequest().newFetchPeopleRequest(
+        opensocial.newIdSpec(idSpec), params);
+    // TODO: Annotate with the @ids property.
+    opensocial.data.addToCurrentAPIRequest(req, descriptor.key);
+  });
+
+  opensocial.data.registerRequestHandler('os:ActivitiesRequest', function(descriptor) {
+    var userId = descriptor.getAttribute('userId');
+    var groupId = descriptor.getAttribute('groupId') || '@self';
+    var idSpec = {};
+    idSpec.userId = opensocial.data.transformSpecialValue(userId);
+    if (groupId != '@self') {
+      idSpec.groupId = opensocial.data.transformSpecialValue(groupId);
+    }
+    // TODO: Support other params.
+    var req = opensocial.data.getCurrentAPIRequest().newFetchActivitiesRequest(
+        opensocial.newIdSpec(idSpec));
+    opensocial.data.addToCurrentAPIRequest(req, descriptor.key);
+  });
+
+  opensocial.data.registerRequestHandler('os:HttpRequest', function(descriptor) {
+    var href = descriptor.getAttribute('href');
+    var format = descriptor.getAttribute('format') || 'json';
+    var params = {};
+    params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+        format.toLowerCase() == 'text' ? gadgets.io.ContentType.TEXT :
+            gadgets.io.ContentType.JSON;
+    params[gadgets.io.RequestParameters.METHOD] =
+        gadgets.io.MethodType.GET;
+    gadgets.io.makeRequest(href, function(obj) {
+      opensocial.data.DataContext.putDataSet(descriptor.key, obj.data);
+    }, params);
+  });
+})();
+
+
+/**
+ * Pre-populate a Data Set based on application's URL parameters.
+ */
+(opensocial.data.populateParams_ = function() {
+  if (window['gadgets'] && gadgets.util.hasFeature('views')) {
+    opensocial.data.DataContext.putDataSet('ViewParams', gadgets.views.getParams());
+  }
+})();
diff --git a/trunk/features/src/main/javascript/features/opensocial-data/feature.xml b/trunk/features/src/main/javascript/features/opensocial-data/feature.xml
new file mode 100644
index 0000000..36f8c3e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-data/feature.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-data</name>
+  <dependency>core.io</dependency>
+  <dependency>core.util</dependency>
+  <dependency>opensocial-data-context</dependency>
+  <dependency>opensocial</dependency>
+  <dependency>xmlutil</dependency>
+  <dependency>domnode</dependency>
+  <gadget>
+    <script src="data.js"></script>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-jsonrpc/feature.xml b/trunk/features/src/main/javascript/features/opensocial-jsonrpc/feature.xml
new file mode 100644
index 0000000..0e95206
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-jsonrpc/feature.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-jsonrpc</name>
+  <dependency>shindig.auth</dependency>
+  <dependency>core.prefs</dependency>
+  <dependency>core.io</dependency>
+  <dependency>core.json</dependency>
+  <dependency>core.util</dependency>
+  <dependency>opensocial-base</dependency>
+  <dependency>rpc</dependency>
+  <dependency>security-token</dependency>
+  <gadget>   
+    <script src="jsonrpccontainer.js"></script>
+    <exports type="rpc">shindig.requestShareApp_callback</exports>
+    <uses type="rpc">shindig.requestShareApp</uses>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-jsonrpc/jsonrpccontainer.js b/trunk/features/src/main/javascript/features/opensocial-jsonrpc/jsonrpccontainer.js
new file mode 100644
index 0000000..e0d71d3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-jsonrpc/jsonrpccontainer.js
@@ -0,0 +1,594 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial, gadgets, shindig */
+/*global JsonPerson, JsonActivity, JsonMediaItem, FieldTranslations */
+
+/**
+ * @fileoverview JSON-RPC based opensocial container.
+ */
+
+var JsonRpcContainer = function(configParams) {
+  opensocial.Container.call(this);
+
+  var path = configParams.path;
+  // Path for social API calls
+  this.path_ = path.replace('%host%', document.location.host);
+
+  // Path for calls to invalidate
+  var invalidatePath = configParams.invalidatePath;
+  this.invalidatePath_ = invalidatePath.replace('%host%',
+      document.location.host);
+
+  var supportedFieldsArray = configParams.supportedFields;
+  var supportedFieldsMap = {};
+  for (var objectType in supportedFieldsArray) {
+    if (supportedFieldsArray.hasOwnProperty(objectType)) {
+      supportedFieldsMap[objectType] = {};
+      for (var i = 0; i < supportedFieldsArray[objectType].length; i++) {
+        var supportedField = supportedFieldsArray[objectType][i];
+        supportedFieldsMap[objectType][supportedField] = true;
+      }
+    }
+  }
+
+  this.environment_ = new opensocial.Environment(configParams.domain,
+      supportedFieldsMap);
+
+  // Enable usage of Auth2 token passing in rpc
+  this.useOAuth2 = configParams.useOAuth2;
+
+  this.securityToken_ = shindig.auth.getSecurityToken();
+
+  gadgets.rpc.register('shindig.requestShareApp_callback',
+      JsonRpcContainer.requestShareAppCallback_);
+};
+
+var JsonRpcRequestItem = function(rpc, opt_processData) {
+  this.rpc = rpc;
+  this.processData = opt_processData ||
+                     function(rawJson) {
+                       return rawJson;
+                     };
+
+  this.processResponse = function(originalDataRequest, rawJson, error, errorMessage) {
+    var errorCode = error ? JsonRpcContainer.translateHttpError(error['code']) : null;
+    return new opensocial.ResponseItem(originalDataRequest,
+        error ? null : this.processData(rawJson), errorCode, errorMessage);
+  };
+};
+
+(function() {
+  var callbackIdStore = {};
+
+  JsonRpcContainer.inherits(opensocial.Container);
+
+  JsonRpcContainer.prototype.getEnvironment = function() {
+    return this.environment_;
+  };
+
+  JsonRpcContainer.prototype.requestShareApp = function(recipientIds, reason,
+      opt_callback, opt_params) {
+    var callbackId = 'cId_' + Math.random();
+    callbackIdStore[callbackId] = opt_callback;
+
+    var body = gadgets.util.unescapeString(reason.getField(
+        opensocial.Message.Field.BODY));
+
+    if (!body || body.length === 0) {
+      var bodyMsgKey = gadgets.util.unescapeString(reason.getField(
+          opensocial.Message.Field.BODY_ID));
+      body = gadgets.Prefs.getMsg(bodyMsgKey);
+    }
+
+    gadgets.rpc.call('..', 'shindig.requestShareApp',
+        null,
+        callbackId,
+        recipientIds,
+        body);
+  };
+
+
+  /**
+   * Receives the returned results from the parent container.
+   *
+   * @param {boolean} success if false, the message will not be sent.
+   * @param {string=} opt_errorCode an error code if success is false.
+   * @param {?Array.<string>} recipientIds an array of recipient IDs,
+   *     if success is true.
+   * @private
+   */
+  JsonRpcContainer.requestShareAppCallback_ = function(callbackId,
+      success, opt_errorCode, recipientIds) {
+    if (this.f !== '..') {
+      return;
+    }
+    callback = callbackIdStore[callbackId];
+    if (callback) {
+      callbackIdStore[callbackId] = null;
+
+      var data = null;
+      if (recipientIds) {
+        data = {'recipientIds': recipientIds};
+      }
+
+      var responseItem = new opensocial.ResponseItem(null, data, opt_errorCode);
+      callback(responseItem);
+    }
+  };
+
+
+  JsonRpcContainer.prototype.requestCreateActivity = function(activity, priority,
+      opt_callback) {
+    opt_callback = opt_callback || function() {};
+
+    var req = opensocial.newDataRequest();
+    var viewer = opensocial.newIdSpec({'userId' : 'VIEWER'});
+    req.add(this.newCreateActivityRequest(viewer, activity), 'key');
+    req.send(function(response) {
+      opt_callback(response.get('key'));
+    });
+  };
+
+  JsonRpcContainer.prototype.requestData = function(dataRequest, callback) {
+    callback = callback || function() {};
+
+    var requestObjects = dataRequest.getRequestObjects();
+    var totalRequests = requestObjects.length;
+
+    if (totalRequests === 0) {
+      window.setTimeout(function() {
+        callback(new opensocial.DataResponse({}, true));
+      }, 0);
+      return;
+    }
+
+    var jsonBatchData = new Array(totalRequests);
+
+    for (var j = 0; j < totalRequests; j++) {
+      var requestObject = requestObjects[j];
+
+      jsonBatchData[j] = requestObject.request.rpc;
+      if (requestObject.key) {
+        jsonBatchData[j].id = requestObject.key;
+      }
+    }
+
+    var sendResponse = function(result) {
+      if (result.errors[0]) {
+        JsonRpcContainer.generateErrorResponse(result, requestObjects, callback);
+        return;
+      }
+
+      // Support old 'data' element and correct 'result' element
+      result = result.result || result.data;
+
+      var globalError = false;
+      var responseMap = {};
+
+      // Map from indices to ids.
+      for (var i = 0; i < result.length; i++) {
+        result[result[i].id] = result[i];
+      }
+
+      for (var k = 0; k < requestObjects.length; k++) {
+        var request = requestObjects[k];
+        var response = result[k];
+
+        if (request.key && response.id !== request.key) {
+          throw 'Request key(' + request.key +
+              ') and response id(' + response.id + ') do not match';
+        }
+
+        // A false response may be valid, careful with truthiness here.
+        var rawData = typeof(response.result) != 'undefined' ? response.result : response.data;
+        var error = response.error;
+        var errorMessage = '';
+
+        if (error) {
+          errorMessage = error.message;
+        }
+
+        var processedData = request.request.processResponse(
+            request.request, rawData, error, errorMessage);
+        globalError = globalError || processedData.hadError();
+        if (request.key) {
+          responseMap[request.key] = processedData;
+        }
+      }
+
+      var dataResponse = new opensocial.DataResponse(responseMap, globalError);
+      callback(dataResponse);
+    };
+
+    // TODO: get the jsonbatch url from the container config
+    var makeRequestParams = {
+      'CONTENT_TYPE' : 'JSON',
+      'METHOD' : 'POST',
+      'AUTHORIZATION' : 'SIGNED',
+      'POST_DATA' : gadgets.json.stringify(jsonBatchData)
+    };
+
+    var headers = {'Content-Type':'application/json'};
+    var url = [this.path_];
+    
+    var token = shindig.auth.getSecurityToken();
+    if (token) {
+      if (this.useOAuth2) {
+        headers['Authorization'] = 'OAuth2 ' + token;
+      } else {
+        url.push('?st=', encodeURIComponent(token));
+      }
+    }
+
+    this.sendRequest(url.join(''), sendResponse, makeRequestParams, headers);
+  };
+
+  JsonRpcContainer.prototype.sendRequest = function(relativeUrl, callback, params, headers) {
+    gadgets.io.makeNonProxiedRequest(relativeUrl, callback, params, headers);
+  };
+
+  JsonRpcContainer.generateErrorResponse = function(result, requestObjects,
+      callback) {
+    var globalErrorCode =
+        JsonRpcContainer.translateHttpError(result.rc
+                    || result.result.error || result.data.error)
+                    || opensocial.ResponseItem.Error.INTERNAL_ERROR;
+
+    var errorResponseMap = {};
+    for (var i = 0; i < requestObjects.length; i++) {
+      errorResponseMap[requestObjects[i].key] = new opensocial.ResponseItem(
+          requestObjects[i].request, null, globalErrorCode);
+    }
+    callback(new opensocial.DataResponse(errorResponseMap, true));
+  };
+
+  JsonRpcContainer.translateHttpError = function(httpError) {
+    if (httpError == 501) {
+      return opensocial.ResponseItem.Error.NOT_IMPLEMENTED;
+    } else if (httpError == 401) {
+      return opensocial.ResponseItem.Error.UNAUTHORIZED;
+    } else if (httpError == 403) {
+      return opensocial.ResponseItem.Error.FORBIDDEN;
+    } else if (httpError == 400) {
+      return opensocial.ResponseItem.Error.BAD_REQUEST;
+    } else if (httpError == 500) {
+      return opensocial.ResponseItem.Error.INTERNAL_ERROR;
+    } else if (httpError == 404) {
+      return opensocial.ResponseItem.Error.BAD_REQUEST;
+    } else if (httpError == 417) {
+      return opensocial.ResponseItem.Error.LIMIT_EXCEEDED;
+    }
+  };
+
+  JsonRpcContainer.prototype.makeIdSpec = function(id) {
+    return opensocial.newIdSpec({'userId' : id});
+  };
+
+  JsonRpcContainer.prototype.translateIdSpec = function(newIdSpec) {
+    var userIds = newIdSpec.getField('userId');
+    var groupId = newIdSpec.getField('groupId');
+
+    // Upconvert to array for convenience
+    if (!opensocial.Container.isArray(userIds)) {
+      userIds = [userIds];
+    }
+
+    for (var i = 0; i < userIds.length; i++) {
+      if (userIds[i] === 'OWNER') {
+        userIds[i] = '@owner';
+      } else if (userIds[i] === 'VIEWER') {
+        userIds[i] = '@viewer';
+      }
+    }
+
+    if (groupId === 'FRIENDS') {
+      groupId = '@friends';
+    } else if (groupId == 'ALL') {
+      groupId = '@all';
+    } else if (groupId === 'SELF' || !groupId) {
+      groupId = '@self';
+    }
+
+    return { userId: userIds, groupId: groupId};
+  };
+
+  JsonRpcContainer.prototype.newFetchPersonRequest = function(id, opt_params) {
+    var peopleRequest = this.newFetchPeopleRequest(
+        this.makeIdSpec(id), opt_params);
+
+    var me = this;
+    return new JsonRpcRequestItem(peopleRequest.rpc,
+        function(rawJson) {
+          return me.createPersonFromJson(rawJson, opt_params);
+        });
+  };
+
+  JsonRpcContainer.prototype.newFetchPeopleRequest = function(idSpec,
+      opt_params) {
+    var rpc = { method: 'people.get' };
+    rpc.params = this.translateIdSpec(idSpec);
+
+    FieldTranslations.addAppDataAsProfileFields(opt_params);
+    FieldTranslations.translateStandardArguments(opt_params, rpc.params);
+    FieldTranslations.translateNetworkDistance(idSpec, rpc.params);
+
+    if (opt_params['profileDetail']) {
+      FieldTranslations.translateJsPersonFieldsToServerFields(opt_params['profileDetail']);
+      rpc.params.fields = opt_params['profileDetail'];
+    }
+    var me = this;
+    return new JsonRpcRequestItem(rpc,
+        function(rawJson) {
+          var jsonPeople;
+          if (rawJson['list']) {
+            // For the array of people response
+            jsonPeople = rawJson['list'];
+          } else {
+            // For the single person response
+            jsonPeople = [rawJson];
+          }
+
+          var people = [];
+          for (var i = 0; i < jsonPeople.length; i++) {
+            people.push(me.createPersonFromJson(jsonPeople[i], opt_params));
+          }
+          return new opensocial.Collection(people,
+              rawJson['startIndex'], rawJson['totalResults']);
+        });
+  };
+
+  JsonRpcContainer.prototype.createPersonFromJson = function(serverJson, opt_params) {
+    FieldTranslations.translateServerPersonToJsPerson(serverJson, opt_params);
+    return new JsonPerson(serverJson);
+  };
+
+  JsonRpcContainer.prototype.getFieldsList = function(keys) {
+    // datarequest.js guarantees that keys is an array
+    if (this.hasNoKeys(keys) || this.isWildcardKey(keys[0])) {
+      return [];
+    } else {
+      return keys;
+    }
+  };
+
+  JsonRpcContainer.prototype.hasNoKeys = function(keys) {
+    return !keys || keys.length === 0;
+  };
+
+  JsonRpcContainer.prototype.isWildcardKey = function(key) {
+    // Some containers support * to mean all keys in the js apis.
+    // This allows the RESTful apis to be compatible with them.
+    return key === '*';
+  };
+
+  JsonRpcContainer.prototype.newFetchPersonAppDataRequest = function(idSpec, keys,
+      opt_params) {
+    var rpc = { method: 'appdata.get' };
+    rpc.params = this.translateIdSpec(idSpec);
+    rpc.params.appId = '@app';
+    rpc.params.fields = this.getFieldsList(keys);
+    FieldTranslations.translateNetworkDistance(idSpec, rpc.params);
+
+    return new JsonRpcRequestItem(rpc,
+        function(appData) {
+          return opensocial.Container.escape(appData, opt_params, true);
+        });
+  };
+
+  JsonRpcContainer.prototype.newUpdatePersonAppDataRequest = function(key,
+      value) {
+    var rpc = { method: 'appdata.update' };
+    rpc.params = {userId: ['@viewer'], groupId: '@self'};
+    rpc.params.appId = '@app';
+    rpc.params.data = {};
+    rpc.params.data[key] = value;
+    rpc.params.fields = key;
+    return new JsonRpcRequestItem(rpc);
+  };
+
+  JsonRpcContainer.prototype.newRemovePersonAppDataRequest = function(keys) {
+    var rpc = { method: 'appdata.delete' };
+    rpc.params = {userId: ['@viewer'], groupId: '@self'};
+    rpc.params.appId = '@app';
+    rpc.params.fields = this.getFieldsList(keys);
+
+    return new JsonRpcRequestItem(rpc);
+  };
+
+  JsonRpcContainer.prototype.newFetchActivitiesRequest = function(idSpec,
+      opt_params) {
+    var rpc = { method: 'activities.get' };
+    rpc.params = this.translateIdSpec(idSpec);
+    rpc.params.appId = '@app';
+    FieldTranslations.translateStandardArguments(opt_params, rpc.params);
+    FieldTranslations.translateNetworkDistance(idSpec, rpc.params);
+
+    return new JsonRpcRequestItem(rpc,
+        function(rawJson) {
+          rawJson = rawJson['list'];
+          var activities = [];
+          for (var i = 0; i < rawJson.length; i++) {
+            activities.push(new JsonActivity(rawJson[i]));
+          }
+          return new opensocial.Collection(activities);
+        });
+  };
+
+  JsonRpcContainer.prototype.newActivity = function(opt_params) {
+    return new JsonActivity(opt_params, true);
+  };
+
+  JsonRpcContainer.prototype.newAlbum = function(opt_params) {
+    return new JsonAlbum(opt_params);
+  };
+
+  JsonRpcContainer.prototype.newMediaItem = function(mimeType, url, opt_params) {
+    opt_params = opt_params || {};
+    opt_params['mimeType'] = mimeType;
+    opt_params['url'] = url;
+    return new JsonMediaItem(opt_params);
+  };
+
+  JsonRpcContainer.prototype.newCreateActivityRequest = function(idSpec,
+      activity) {
+    var rpc = { method: 'activities.create' };
+    rpc.params = this.translateIdSpec(idSpec);
+    rpc.params.appId = '@app';
+    FieldTranslations.translateNetworkDistance(idSpec, rpc.params);
+    rpc.params.activity = activity.toJsonObject();
+
+    return new JsonRpcRequestItem(rpc);
+  };
+
+  JsonRpcContainer.prototype.invalidateCache = function() {
+    var rpc = { method: 'cache.invalidate' };
+    var invalidationKeys = { invalidationKeys: ['@viewer'] };
+    rpc.params = invalidationKeys;
+
+    var makeRequestParams = {
+      'CONTENT_TYPE' : 'JSON',
+      'METHOD' : 'POST',
+      'AUTHORIZATION' : 'SIGNED',
+      'POST_DATA' : gadgets.json.stringify(rpc)
+    };
+
+    var headers = {'Content-Type': 'application/json'};
+    var url = [this.invalidatePath_];
+    var token = shindig.auth.getSecurityToken();
+    if (token) {
+      if (this.useOAuth2) {
+        headers['Authorization'] = 'OAuth2 ' + token;
+      } else {
+        url.push('?st=', encodeURIComponent(token));
+      }
+    }
+
+    this.sendRequest(url.join(''), null, makeRequestParams, headers);
+  };
+
+})();
+
+JsonRpcContainer.prototype.newMessage = function(body, opt_params) {
+  return new JsonMessage(body, opt_params);
+};
+
+JsonRpcContainer.prototype.newMessageCollection = function(opt_params) {
+  return new JsonMessageCollection(opt_params);
+};
+
+JsonRpcContainer.prototype.newFetchMessageCollectionsRequest = function(idSpec, opt_params) {
+  var rpc = { method: 'messages.get' };
+  rpc.params = this.translateIdSpec(idSpec);
+  
+  FieldTranslations.translateStandardArguments(opt_params, rpc.params);
+  
+  return new JsonRpcRequestItem(rpc,
+      function(rawJson) {
+        rawJson = rawJson['list'];
+        var messagecollections = [];
+        for (var i = 0; i < rawJson.length; i++) {
+          messagecollections.push(new JsonMessageCollection(rawJson[i]));
+        }
+        return new opensocial.Collection(messagecollections);
+      });
+};
+
+JsonRpcContainer.prototype.newFetchMessagesRequest = function(idSpec, msgCollId, opt_params) {
+  var rpc = { method: 'messages.get' };
+  rpc.params = this.translateIdSpec(idSpec);
+  rpc.params.msgCollId = msgCollId;
+  
+  FieldTranslations.translateStandardArguments(opt_params, rpc.params);
+
+  return new JsonRpcRequestItem(rpc,
+      function(rawJson) {
+        rawJson = rawJson['list'];
+        var messages = [];
+        for (var i = 0; i < rawJson.length; i++) {
+          messages.push(new JsonMessage(rawJson[i]));
+        }
+        return new opensocial.Collection(messages);
+      });
+};
+
+JsonRpcContainer.prototype.newCreateAlbumRequest = function(idSpec, album) {
+  var rpc = { method: 'albums.create' };
+  rpc.params = this.translateIdSpec(idSpec);
+  rpc.params.appId = '@app';
+  rpc.params.album = album.toJsonObject();
+
+  return new JsonRpcRequestItem(rpc);
+};
+
+JsonRpcContainer.prototype.newDeleteAlbumRequest = function(idSpec, albumId) {
+  var rpc = { method: 'albums.delete' };
+  rpc.params = this.translateIdSpec(idSpec);
+  rpc.params.appId = '@app';
+  rpc.params.albumId = albumId;
+
+  return new JsonRpcRequestItem(rpc);
+};
+
+JsonRpcContainer.prototype.newFetchAlbumsRequest = function(idSpec, opt_params) {
+  var rpc = { method: 'albums.get' };
+  rpc.params = this.translateIdSpec(idSpec);
+  rpc.params.appId = '@app';
+  
+  FieldTranslations.translateStandardArguments(opt_params, rpc.params);
+  
+  return new JsonRpcRequestItem(rpc, function(rawJson) {
+    rawJson = rawJson['list'];
+    var albums = [];
+    for (var i = 0; i < rawJson.length; i++) {
+      albums.push(new JsonAlbum(rawJson[i]));
+    }
+
+    return new opensocial.Collection(albums);
+  });
+};
+
+JsonRpcContainer.prototype.newCreateMediaItemRequest = function(idSpec, albumId, mediaItem) {
+  var rpc = { method: 'mediaItems.create' };
+  rpc.params = this.translateIdSpec(idSpec);
+  rpc.params.appId = '@app';
+  rpc.params.albumId = albumId;
+  rpc.params.mediaItem = mediaItem.toJsonObject();
+
+  return new JsonRpcRequestItem(rpc);
+};
+
+JsonRpcContainer.prototype.newFetchMediaItemsRequest = function(idSpec, albumId, opt_params) {
+  var rpc = { method: 'mediaItems.get' };
+  rpc.params = this.translateIdSpec(idSpec);
+  rpc.params.appId = '@app';
+  rpc.params.albumId = albumId;
+  
+  FieldTranslations.translateStandardArguments(opt_params, rpc.params);
+
+  return new JsonRpcRequestItem(rpc, function(rawJson) {
+    rawJson = rawJson['list'];
+    var mediaItems = [];
+    for (var i = 0; i < rawJson.length; i++) {
+      mediaItems.push(new JsonMediaItem(rawJson[i]));
+    }
+
+    return new opensocial.Collection(mediaItems);
+  });
+
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/activity.js b/trunk/features/src/main/javascript/features/opensocial-reference/activity.js
new file mode 100644
index 0000000..bd5bb41
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/activity.js
@@ -0,0 +1,386 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @class
+ * Representation of an activity.
+ *
+ * <p>Activities are rendered with a title and an optional activity body.</p>
+ *
+ * <p>You may set the title and body directly as strings when calling
+ * opensocial.createActivity.</p>
+ *
+ * <p>However, it is usually beneficial to create activities using
+ * Activity Templates for the title and body. Activity Templates support:</p>
+ * <ul>
+ *   <li>Internationalization</li>
+ *   <li>Replacement variables in the message</li>
+ *   <li>Activity Summaries, which are message variations used to summarize
+ *     repeated activities that share something in common.</li>
+ * </ul>
+ *
+ * <p>Activity Templates are defined as messages in the gadget specification.
+ * To define messages, you create and reference message bundle XML files for
+ * each locale you support.</p>
+ *
+ * <p>Example module spec in gadget XML:
+ * <pre>
+ * &lt;ModulePrefs title="ListenToThis"&gt;
+ *   &lt;Locale messages="http://www.listentostuff.com/messages.xml"/&gt;
+ *   &lt;Locale lang="de" messages="http://www.listentostuff.com/messages-DE.xml"/&gt;
+ * &lt;/ModulePrefs&gt;
+ * </pre>
+ * </p>
+ *
+ * <p>Example message bundle:
+ * <pre>
+ * &lt;messagebundle&gt;
+ *  &lt;msg name="LISTEN_TO_THIS_SONG"&gt;
+ *     ${Subject.DisplayName} told ${Owner.DisplayName} to
+ *     listen to a song!
+ *  &lt;/msg&gt;
+ * &lt;/messagebundle&gt;
+ * </pre>
+ * </p>
+ *
+ * <p>You can set custom key/value string pairs when posting an activity.
+ * These values will be used for variable substitution in the templates.</p>
+ * <p>Example JS call:
+ * <pre>
+ *   var owner = ...;
+ *   var viewer = ...;
+ *   var activity = opensocial.newActivity('LISTEN_TO_THIS_SONG',
+ *    {Song: 'Do That There - (Young Einstein hoo-hoo mix)',
+ *     Artist: 'Lyrics Born', Subject: viewer, Owner: owner})
+ * </pre>
+ * </p>
+ *
+ * <p> Associated message:
+ * <pre>
+ * &lt;msg name="LISTEN_TO_THIS_SONG"&gt;
+ *     ${Subject.DisplayName} told ${Owner.DisplayName} to listen
+ *     to ${Song} by ${Artist}
+ * &lt;/msg&gt;
+ * </pre>
+ * </p>
+ *
+ * <p>People can also be set as values in key/value pairs when posting
+ * an activity. You can then reference the following fields on a person:</p>
+ * <ul>
+ *  <li>${Person.DisplayName} The person's name</li>
+ *  <li>${Person.Id} The user ID of the person</li>
+ *  <li>${Person.ProfileUrl} The profile URL of the person</li>
+ *  <li>${Person} This will show the display name, but containers may optionally
+ *     provide special formatting, such as showing the name as a link</li>
+ * </ul>
+ *
+ * <p>Users will have many activities in their activity streams, and containers
+ * will not show every activity that is visible to a user. To help display
+ * large numbers of activities, containers will summarize a list of activities
+ * from a given source to a single entry.</p>
+ *
+ * <p>You can provide Activity Summaries to customize the text shown when
+ * multiple activities are summarized. If no customization is provided, a
+ * container may ignore your activities altogether or provide default text
+ * such as "Bob changed his status message + 20 other events like this."</p>
+ * <ul>
+ *  <li>Activity Summaries will always summarize around a specific key in a
+ *   key/value pair. This is so that the summary can say something concrete
+ *   (this is clearer in the example below).</li>
+ *  <li>Other variables will have synthetic "Count" variables created with
+ *   the total number of items summarized.</li>
+ *  <li>Message ID of the summary is the message ID of the main template + ":" +
+ *   the data key</li>
+ * </ul>
+ *
+ * <p>Example summaries:
+ * <pre>
+ * &lt;messagebundle&gt;
+ *   &lt;msg name="LISTEN_TO_THIS_SONG:Artist"&gt;
+ *     ${Subject.Count} of your friends have suggested listening to songs
+ *     by ${Artist}!
+ *   &lt;/msg&gt;
+ *   &lt;msg name="LISTEN_TO_THIS_SONG:Song"&gt;
+ *     ${Subject.Count} of your friends have suggested listening to ${Song}
+ *   !&lt;/msg&gt;
+ *   &lt;msg name="LISTEN_TO_THIS_SONG:Subject"&gt;
+ *    ${Subject.DisplayName} has recommended ${Song.Count} songs to you.
+ *   &lt;/msg&gt;
+ * &lt;/messagebundle&gt;
+ * </pre></p>
+ *
+ * <p>Activity Templates may only have the following HTML tags: &lt;b&gt;,
+ * &lt;i&gt;, &lt;a&gt;, &lt;span&gt;. The container also has the option
+ * to strip out these tags when rendering the activity.</p>
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.html#newActivity">opensocial.newActivity()</a>,
+ * <a href="opensocial.html#requestCreateActivity">
+ * opensocial.requestCreateActivity()</a>
+ *
+ * @name opensocial.Activity
+ */
+
+
+/**
+ * Base interface for all activity objects.
+ *
+ * Private, see opensocial.createActivity() for usage.
+ *
+ * @param {Object.<opensocial.Activity.Field, Object>} params Parameters defining the activity.
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.2.
+ */
+opensocial.Activity = function(params) {
+  this.fields_ = params;
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that activities can have.
+ *
+ * <p>It is only required to set one of TITLE_ID or TITLE. In addition, if you
+ * are using any variables in your title or title template,
+ * you must set TEMPLATE_PARAMS.</p>
+ *
+ * <p>Other possible fields to set are: URL, MEDIA_ITEMS, BODY_ID, BODY,
+ * EXTERNAL_ID, PRIORITY, STREAM_TITLE, STREAM_URL, STREAM_SOURCE_URL,
+ * and STREAM_FAVICON_URL.</p>
+ *
+ * <p>Containers are only required to use TITLE_ID or TITLE, they may ignore
+ * additional parameters.</p>
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a
+ * href="opensocial.Activity.html#getField">opensocial.Activity.getField()</a>
+ * </p>
+ *
+ * @name opensocial.Activity.Field
+ * @enum {string}
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.3.
+ */
+opensocial.Activity.Field = {
+  /**
+   * <p>A string specifying the title template message ID in the gadget
+   *   spec.</p>
+   *
+   * <p>The title is the primary text of an activity.</p>
+   *
+   * <p>Titles may only have the following HTML tags: &lt;b&gt; &lt;i&gt;,
+   * &lt;a&gt;, &lt;span&gt;.
+   * The container may ignore this formatting when rendering the activity.</p>
+   *
+   * @member opensocial.Activity.Field
+   */
+  TITLE_ID: 'titleId',
+
+  /**
+   * <p>A string specifying the primary text of an activity.</p>
+   *
+   * <p>Titles may only have the following HTML tags: &lt;b&gt; &lt;i&gt;,
+   * &lt;a&gt;, &lt;span&gt;.
+   * The container may ignore this formatting when rendering the activity.</p>
+   *
+   * @member opensocial.Activity.Field
+   */
+  TITLE: 'title',
+
+  /**
+   * <p>A map of custom keys to values associated with this activity.
+   * These will be used for evaluation in templates.</p>
+   *
+   * <p>The data has type <code>Map&lt;String, Object&gt;</code>. The
+   * object may be either a String or an opensocial.Person.</p>
+   *
+   * <p>When passing in a person with key PersonKey, can use the following
+   * replacement variables in the template:</p>
+   * <ul>
+   *  <li>PersonKey.DisplayName - Display name for the person</li>
+   *  <li>PersonKey.ProfileUrl. URL of the person's profile</li>
+   *  <li>PersonKey.Id -  The ID of the person</li>
+   *  <li>PersonKey - Container may replace with DisplayName, but may also
+   *     optionally link to the user.</li>
+   * </ul>
+   *
+   * @member opensocial.Activity.Field
+   */
+  TEMPLATE_PARAMS: 'templateParams',
+
+  /**
+   * A string specifying the
+   * URL that represents this activity.
+   * @member opensocial.Activity.Field
+   */
+  URL: 'url',
+
+  /**
+   * Any photos, videos, or images that should be associated
+   * with the activity. Higher priority ones are higher in the list.
+   * The data has type <code>Array&lt;
+   * <a href="opensocial.MediaItem.html">MediaItem</a>&gt;</code>.
+   * @member opensocial.Activity.Field
+   */
+  MEDIA_ITEMS: 'mediaItems',
+
+  /**
+   * <p>A string specifying the body template message ID in the gadget spec.</p>
+   *
+   * <p>The body is an optional expanded version of an activity.</p>
+   *
+   * <p>Bodies may only have the following HTML tags: &lt;b&gt; &lt;i&gt;,
+   * &lt;a&gt;, &lt;span&gt;.
+   * The container may ignore this formatting when rendering the activity.</p>
+   *
+   * @member opensocial.Activity.Field
+   */
+  BODY_ID: 'bodyId',
+
+  /**
+   * <p>A string specifying an optional expanded version of an activity.</p>
+   *
+   * <p>Bodies may only have the following HTML tags: &lt;b&gt; &lt;i&gt;,
+   * &lt;a&gt;, &lt;span&gt;.
+   * The container may ignore this formatting when rendering the activity.</p>
+   *
+   * @member opensocial.Activity.Field
+   */
+  BODY: 'body',
+
+  /**
+   * An optional string ID generated by the posting application.
+   * @member opensocial.Activity.Field
+   */
+  EXTERNAL_ID: 'externalId',
+
+  /**
+   * A string specifing the title of the stream.
+   * @member opensocial.Activity.Field
+   */
+  STREAM_TITLE: 'streamTitle',
+
+  /**
+   * A string specifying the stream's URL.
+   * @member opensocial.Activity.Field
+   */
+  STREAM_URL: 'streamUrl',
+
+  /**
+   * A string specifying the stream's source URL.
+   * @member opensocial.Activity.Field
+   */
+  STREAM_SOURCE_URL: 'streamSourceUrl',
+
+  /**
+   * A string specifying the URL for the stream's favicon.
+   * @member opensocial.Activity.Field
+   */
+  STREAM_FAVICON_URL: 'streamFaviconUrl',
+
+  /**
+   * A number between 0 and 1 representing the relative priority of
+   * this activity in relation to other activities from the same source
+   * @member opensocial.Activity.Field
+   */
+  PRIORITY: 'priority',
+
+  /**
+   * A string ID that is permanently associated with this activity.
+   * This value can not be set.
+   * @member opensocial.Activity.Field
+   */
+  ID: 'id',
+
+  /**
+   * The string ID of the user who this activity is for.
+   * This value can not be set.
+   * @member opensocial.Activity.Field
+   */
+  USER_ID: 'userId',
+
+  /**
+   * A string specifying the application that this activity is associated with.
+   * This value can not be set.
+   * @member opensocial.Activity.Field
+   */
+  APP_ID: 'appId',
+
+  /**
+   * A string specifying the time at which this activity took place
+   * in milliseconds since the epoch.
+   * This value can not be set.
+   * @member opensocial.Activity.Field
+   */
+  POSTED_TIME: 'postedTime',
+  
+  /**
+   * An array specifing the embed experiences that should be added to the activity
+   * @member opensocial.Activity.Field
+   */
+  EMBEDS: 'embeds'
+};
+
+
+/**
+ * Gets an ID that can be permanently associated with this activity.
+ *
+ * @return {string} The ID.
+ * @member opensocial.Activity
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.2.1.2.
+ */
+opensocial.Activity.prototype.getId = function() {
+  return this.getField(opensocial.Activity.Field.ID);
+};
+
+
+/**
+ * Gets the activity data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *   see the <a href="opensocial.Activity.Field.html">Field</a> class
+ * for possible values.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>=}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @member opensocial.Activity
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.2.1.1.
+ */
+opensocial.Activity.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this activity associated with the given key.
+ *
+ * @param {string} key The key to set data for.
+ * @param {string} data The data to set.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.2.1.3.
+ */
+opensocial.Activity.prototype.setField = function(key, data) {
+  return (this.fields_[key] = data);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/address.js b/trunk/features/src/main/javascript/features/opensocial-reference/address.js
new file mode 100644
index 0000000..3c80540
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/address.js
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of an address.
+ */
+
+
+/**
+ * @class
+ * Base interface for all address objects.
+ *
+ * @name opensocial.Address
+ */
+
+
+/**
+ * Base interface for all address objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.4.
+ */
+opensocial.Address = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that an address has. These are the supported keys for the
+ * <a href="opensocial.Address.html#getField">Address.getField()</a> method.
+ *
+ * @name opensocial.Address.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.5.
+ */
+opensocial.Address.Field = {
+  /**
+   * The address type or label. Examples: work, my favorite store, my house, etc
+   * Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  TYPE: 'type',
+
+  /**
+   * If the container does not have structured addresses in its data store,
+   * this field will return the unstructured address that the user entered. Use
+   * opensocial.getEnvironment().supportsField to see which fields are
+   * supported. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  UNSTRUCTURED_ADDRESS: 'unstructuredAddress',
+
+  /**
+   * The po box of the address if there is one. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  PO_BOX: 'poBox',
+
+  /**
+   * The street address. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  STREET_ADDRESS: 'streetAddress',
+
+  /**
+   * The extended street address. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  EXTENDED_ADDRESS: 'extendedAddress',
+
+  /**
+   * The region. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  REGION: 'region',
+
+  /**
+   * The locality. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  LOCALITY: 'locality',
+
+  /**
+   * The postal code. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  POSTAL_CODE: 'postalCode',
+
+  /**
+   * The country. Specified as a String.
+   *
+   * @member opensocial.Address.Field
+   */
+  COUNTRY: 'country',
+
+  /**
+   * The latitude. Specified as a Number.
+   *
+   * @member opensocial.Address.Field
+   */
+  LATITUDE: 'latitude',
+
+  /**
+   * The longitude. Specified as a Number.
+   *
+   * @member opensocial.Address.Field
+   */
+  LONGITUDE: 'longitude'
+};
+
+
+/**
+ * Gets data for this body type that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.Address.Field.html"><code>
+ *    Address.Field.</code></a>
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.4.1.1.
+ */
+opensocial.Address.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/album.js b/trunk/features/src/main/javascript/features/opensocial-reference/album.js
new file mode 100644
index 0000000..a37938c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/album.js
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of an album.
+ */
+
+/**
+ * @class
+ * Represents collection of media item images, movies, and audio.
+ * Create a <code>Album</code> object using the <a href="opensocial.html#newAlbum">
+ * opensocial.newAlbum()</a> method.
+ *
+ * @name opensocial.Album
+ */
+
+/**
+ * Base interface for collection of media items.
+ *
+ * @param {Object.<opensocial.Album.Field, Object>=} opt_params
+ *    Any other fields that should be set on the message object.
+ *    All of the defined Fields are supported.
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.6.
+ */
+opensocial.Album = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+/**
+ * @static
+ * @class
+ * All of the fields that an Album can have.
+ *
+ * @name opensocial.Album.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.7.
+ */
+opensocial.Album.Field = {
+  /**
+   * A description of the album.
+   *
+   * @member opensocial.Album.Field
+   */
+  DESCRIPTION: 'description',
+
+  /**
+   * A unique identifier for the album.
+   *
+   * @member opensocial.Album.Field
+   */
+  ID: 'id',
+
+  /**
+   * A location corresponding to the album as opensocial.Address.
+   *
+   * @member opensocial.Album.Field
+   */
+  LOCATION: 'location',
+
+  /**
+   * The number of items in the album.
+   *
+   * @member opensocial.Album.Field
+   */
+  MEDIA_ITEM_COUNT: 'mediaItemCount',
+
+
+  /**
+   * The types of MediaItems in the Album.
+   *
+   * @member opensocial.Album.Field
+   */
+  MEDIA_MIME_TYPE: 'mediaMimeType',
+
+
+  /**
+   * The types of MediaItems in the album.
+   *
+   * @member opensocial.Album.Field
+   */
+  MEDIA_TYPE: 'mediaType',
+
+
+  /**
+   * The string ID of the owner of the album.
+   *
+   * @member opensocial.Album.Field
+   */
+  OWNER_ID: 'ownerId',
+
+  /**
+   * URL to a thumbnail cover of the album as string.
+   *
+   * @member opensocial.Album.Field
+   */
+  THUMBNAIL_URL: 'thumbnailUrl',
+
+  /**
+   * The title of the album.
+   *
+   * @member opensocial.Album.Field
+   */
+  TITLE: 'title'
+};
+
+/**
+ * Gets the album data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *   see the <a href="opensocial.Album.Field.html">Field</a> class
+ * for possible values.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @member opensocial.Album
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.6.1.1.
+ */
+opensocial.Album.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this album associated with the given key.
+ *
+ * @param {string} key The key to set data for.
+ * @param {string} data The data to set.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.6.1.2.
+ */
+opensocial.Album.prototype.setField = function(key, data) {
+  return this.fields_[key] = data;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/bodytype.js b/trunk/features/src/main/javascript/features/opensocial-reference/bodytype.js
new file mode 100644
index 0000000..8a43769
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/bodytype.js
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of a body type.
+ */
+
+
+/**
+ * @class
+ * Base interface for all body type objects.
+ *
+ * @name opensocial.BodyType
+ */
+
+
+/**
+ * Base interface for all body type objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.8.
+ */
+opensocial.BodyType = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that a body type has. These are the supported keys for the
+ * <a href="opensocial.BodyType.html#getField">BodyType.getField()</a>
+ * method.
+ *
+ * @name opensocial.BodyType.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.9.
+ */
+opensocial.BodyType.Field = {
+  /**
+   * The build of the person's body, specified as a string.
+   * Not supported by all containers.
+   * @member opensocial.BodyType.Field
+   */
+  BUILD: 'build',
+
+  /**
+   * The height of the person in meters, specified as a number.
+   * Not supported by all containers.
+   * @member opensocial.BodyType.Field
+   */
+  HEIGHT: 'height',
+
+  /**
+   * The weight of the person in kilograms, specified as a number.
+   * Not supported by all containers.
+   * @member opensocial.BodyType.Field
+   */
+  WEIGHT: 'weight',
+
+  /**
+   * The eye color of the person, specified as a string.
+   * Not supported by all containers.
+   * @member opensocial.BodyType.Field
+   */
+  EYE_COLOR: 'eyeColor',
+
+  /**
+   * The hair color of the person, specified as a string.
+   * Not supported by all containers.
+   * @member opensocial.BodyType.Field
+   */
+  HAIR_COLOR: 'hairColor'
+};
+
+
+/**
+ * Gets data for this body type that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.BodyType.Field.html"><code>
+ *    BodyType.Field</code></a>.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.8.1.1.
+ */
+opensocial.BodyType.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/collection.js b/trunk/features/src/main/javascript/features/opensocial-reference/collection.js
new file mode 100644
index 0000000..5131316
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/collection.js
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Collection of multiple objects with useful accessors.
+ *
+ * May also represent subset of a larger collection (i.e. page 1 of 10), and
+ * contain information about the larger collection.
+ */
+
+
+/**
+ * @class
+ * Collection of multiple objects with useful accessors.
+ * May also represent subset of a larger collection
+ * (for example, page 1 of 10)
+ * and contain information about the larger collection.
+ *
+ * @name opensocial.Collection
+ */
+
+
+/**
+ * Create a collection.
+ *
+ * @private
+ * @constructor
+ * @param {Array} array
+ * @param {number=} opt_offset
+ * @param {number=} opt_totalSize
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.10.
+ */
+opensocial.Collection = function(array, opt_offset, opt_totalSize) {
+  this.array_ = array || [];
+  this.offset_ = opt_offset || 0;
+  this.totalSize_ = opt_totalSize || this.array_.length;
+};
+
+
+/**
+ * Finds the entry with the given ID value, or returns null if none is found.
+ * @param {string} id The ID to look for.
+ * @return {?Object} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.10.1.3.
+ */
+opensocial.Collection.prototype.getById = function(id) {
+  // TODO(doll): A non-linear search would be better
+  for (var i = 0; i < this.size(); i++) {
+    var item = this.array_[i];
+    if (item.getId() === id) {
+      return item;
+    }
+  }
+
+  return null;
+};
+
+
+/**
+ * Gets the size of this collection,
+ * which is equal to or less than the
+ * total size of the result.
+ *
+ * @return {number} The size of this collection.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.10.1.6.
+ */
+opensocial.Collection.prototype.size = function() {
+  return this.array_.length;
+};
+
+
+/**
+ * Executes the provided function once per member of the collection,
+ * with each member in turn as the
+ * parameter to the function.
+ *
+ * @param {function(Object)} fn The function to call with each collection entry.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.10.1.2.
+ */
+opensocial.Collection.prototype.each = function(fn) {
+  for (var i = 0; i < this.size(); i++) {
+    fn(this.array_[i]);
+  }
+};
+
+
+/**
+ * Returns an array of all the objects in this collection.
+ * @return {Array.<Object>} The values in this collection.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.10.1.1.
+ */
+opensocial.Collection.prototype.asArray = function() {
+  return this.array_;
+};
+
+
+/**
+ * Gets the total size of the larger result set
+ * that this collection belongs to.
+ * @return {number} The total size of the result.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.10.1.5.
+ */
+opensocial.Collection.prototype.getTotalSize = function() {
+  return this.totalSize_;
+};
+
+
+/**
+ * Gets the offset of this collection within a larger result set.
+ * @return {number} The offset into the total collection.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.10.1.4.
+ */
+opensocial.Collection.prototype.getOffset = function() {
+  return this.offset_;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/container.js b/trunk/features/src/main/javascript/features/opensocial-reference/container.js
new file mode 100644
index 0000000..e4f204b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/container.js
@@ -0,0 +1,601 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Interface for containers of people functionality.
+ */
+
+
+/**
+ * Base interface for all containers.
+ *
+ * @constructor
+ * @private
+ */
+opensocial.Container = function() {};
+
+
+/**
+ * The container instance.
+ *
+ * @type {Container}
+ * @private
+ */
+opensocial.Container.container_ = null;
+
+
+/**
+ * Set the current container object.
+ *
+ * @param {opensocial.Container} container The container.
+ * @private
+ */
+opensocial.Container.setContainer = function(container) {
+  opensocial.Container.container_ = container;
+};
+
+
+/**
+ * Get the current container object.
+ *
+ * @return {opensocial.Container} container The current container.
+ * @private
+ */
+opensocial.Container.get = function() {
+  return opensocial.Container.container_;
+};
+
+
+/**
+ * Gets the current environment for this gadget. You can use the environment to
+ * query things like what profile fields and surfaces are supported by this
+ * container, what parameters were passed to the current gadget and so forth.
+ *
+ * @return {opensocial.Environment} The current environment.
+ *
+ * @private
+ */
+opensocial.Container.prototype.getEnvironment = function() {};
+
+/**
+ * Requests the container to send a specific message to the specified users. If
+ * the container does not support this method the callback will be called with a
+ * opensocial.ResponseItem. The response item will have its error code set to
+ * NOT_IMPLEMENTED.
+ *
+ * @param {Array.<string> | string} recipients An ID, array of IDs, or a
+ *     group reference; the supported keys are VIEWER, OWNER, VIEWER_FRIENDS,
+ *    OWNER_FRIENDS, or a single ID within one of those groups.
+ * @param {opensocial.Message} message The message to send to the specified
+ *     users.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call once the request has been
+ *    processed; either this callback will be called or the gadget will be
+ *    reloaded from scratch. This function will be passed one parameter, an
+ *    opensocial.ResponseItem. The error code will be set to reflect whether
+ *    there were any problems with the request. If there was no error, the
+ *    message was sent. If there was an error, you can use the response item's
+ *    getErrorCode method to determine how to proceed. The data on the response
+ *    item will not be set.
+ * @param {Object=} opt_params TODO.
+ *
+ * @member opensocial
+ * @private
+ */
+opensocial.Container.prototype.requestSendMessage = function(recipients,
+    message, opt_callback, opt_params) {
+  gadgets.rpc.call(null, 'requestSendMessage', opt_callback, recipients,
+      message.toJsonObject(), opt_callback, opt_params);
+};
+
+
+/**
+ * Requests the container to share this gadget with the specified users. If the
+ * container does not support this method the callback will be called with a
+ * opensocial.ResponseItem. The response item will have its error code set to
+ * NOT_IMPLEMENTED.
+ *
+ * @param {Array.<string> | string} recipients An ID, array of IDs, or a
+ *     group reference; the supported keys are VIEWER, OWNER, VIEWER_FRIENDS,
+ *    OWNER_FRIENDS, or a single ID within one of those groups.
+ * @param {opensocial.Message} reason The reason the user wants the gadget to
+ *     share itself. This reason can be used by the container when prompting the
+ *     user for permission to share the app. It may also be ignored.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call once the request has been
+ *    processed; either this callback will be called or the gadget will be
+ *    reloaded from scratch. This function will be passed one parameter, an
+ *    opensocial.ResponseItem. The error code will be set to reflect whether
+ *    there were any problems with the request. If there was no error, the
+ *    sharing request was sent. If there was an error, you can use the response
+ *    item's getErrorCode method to determine how to proceed. The data on the
+ *    response item will not be set.
+ * @param {Object=} opt_params TODO.
+ *
+ * @member opensocial
+ * @private
+ */
+opensocial.Container.prototype.requestShareApp = function(recipients, reason,
+    opt_callback, opt_params) {
+  if (opt_callback) {
+    window.setTimeout(function() {
+      opt_callback(new opensocial.ResponseItem(
+          null, null, opensocial.ResponseItem.Error.NOT_IMPLEMENTED, null));
+    }, 0);
+  }
+};
+
+
+/**
+ * Request for the container to make the specified person not a friend.
+ *
+ * Note: If this is the first activity that has been created for the user and
+ * the request is marked as HIGH priority then this call may open a user flow
+ * and navigate away from your gadget.
+ *
+ * @param {Activity} activity The activity to create. The only required field is
+ *     title.
+ * @param {CreateActivityPriority} priority The priority for this request.
+ * @param {function(opensocial.ResponseItem)=} opt_callback Function to call once the request has been
+ *    processed.
+ * @private
+ */
+opensocial.Container.prototype.requestCreateActivity = function(activity,
+    priority, opt_callback) {
+  if (opt_callback) {
+    window.setTimeout(function() {
+      opt_callback(new opensocial.ResponseItem(
+          null, null, opensocial.ResponseItem.Error.NOT_IMPLEMENTED, null));
+    }, 0);
+  }
+};
+
+
+/**
+ * Returns whether the current gadget has access to the specified
+ * permission.
+ *
+ * @param {opensocial.Permission | string} permission The permission.
+ * @return {boolean} Whether the gadget has access for the permission.
+ *
+ * @private
+ */
+opensocial.Container.prototype.hasPermission = function(permission) {
+  return false;
+};
+
+
+/**
+ * Requests the user grants access to the specified permissions.
+ *
+ * @param {Array.<opensocial.Permission>} permissions The permissions to request
+ *    access to from the viewer.
+ * @param {string} reason Will be displayed to the user as the reason why these
+ *    permissions are needed.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call once the request has been
+ *    processed. This callback will either be called or the gadget will be
+ *    reloaded from scratch.
+ *
+ * @private
+ */
+opensocial.Container.prototype.requestPermission = function(permissions, reason,
+    opt_callback) {
+  if (opt_callback) {
+    window.setTimeout(function() {
+      opt_callback(new opensocial.ResponseItem(
+          null, null, opensocial.ResponseItem.Error.NOT_IMPLEMENTED, null));
+    }, 0);
+  }
+};
+
+
+/**
+ * Calls the callback function with a dataResponse object containing the data
+ * asked for in the dataRequest object.
+ *
+ * @param {opensocial.DataRequest} dataRequest Specifies which data to get from
+ *    the server.
+ * @param {function(opensocial.ResponseItem)} callback Function to call after the data is fetched.
+ * @private
+ */
+opensocial.Container.prototype.requestData = function(dataRequest, callback) {};
+
+
+/**
+ * Creates a new album and returns the ID of the album created.
+ *
+ * @param {opensocial.IdSpec} idSpec The ID of the used to specify which people/groups
+ *   to create an album for.
+ * @param {opensocial.Album} album The album to create.
+ * @return {Object} A request object.
+ * @private
+ */
+opensocial.Container.prototype.newCreateAlbumRequest = function(idSpec, album) {};
+
+/**
+ * Creates a new media item in the album and returns the ID of the album created.
+ *
+ * @param {opensocial.IdSpec} idSpec The ID of the used to specify which people/groups
+ *   to create an album for.
+ * @param {string} albumId The ID of album to add the media item to.
+ * @param {openSocial.MediaItem} mediaItem The media item instance to add to the album.
+ * @return {Object} A request object.
+ * @private
+ */
+opensocial.Container.prototype.newCreateMediaItemRequest = function(idSpec, albumId,
+    mediaItem) {};
+
+/**
+ * Deletes the album specified.
+ *
+ * @param {opensocial.IdSpec} idSpec The ID of the used to specify which people/groups
+ *   to create an album for.
+ * @param {string} albumId The ID of the album to create.
+ * @return {Object} A request object.
+ * @private
+ */
+opensocial.Container.prototype.newDeleteAlbumRequest = function(idSpec, albumId) {};
+
+/**
+ * Request a profile for the specified person id.
+ * When processed, returns a Person object.
+ *
+ * @param {string} id The id of the person to fetch. Can also be standard
+ *    person IDs of VIEWER and OWNER.
+ * @param {Object.<opensocial.DataRequest.PeopleRequestFields, Object>=} opt_params
+ *    Additional params to pass to the request. This request supports
+ *    PROFILE_DETAILS.
+ * @return {Object} a request object.
+ * @private
+ */
+opensocial.Container.prototype.newFetchPersonRequest = function(id,
+    opt_params) {};
+
+
+/**
+ * Used to request friends from the server.
+ * When processed, returns a Collection&lt;Person&gt; object.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people to
+ *     fetch. See also <a href="opensocial.IdSpec.html">IdSpec</a>.
+ * @param {Object.<opensocial.DataRequest.PeopleRequestFields, Object>=} opt_params
+ *    Additional params to pass to the request. This request supports
+ *    PROFILE_DETAILS, SORT_ORDER, FILTER, FILTER_OPTIONS, FIRST, and MAX.
+ * @return {Object} a request object.
+ * @private
+ */
+opensocial.Container.prototype.newFetchPeopleRequest = function(idSpec,
+    opt_params) {};
+
+
+/**
+ * Used to request app data for the given people.
+ * When processed, returns a Map&lt;person id, Map&lt;String, String&gt;&gt;
+ * object.TODO: All of the data values returned will be valid json.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people to
+ *     fetch. See also <a href="opensocial.IdSpec.html">IdSpec</a>.
+ * @param {Array.<string> | string} keys The keys you want data for. This
+ *     can be an array of key names, a single key name, or "*" to mean
+ *     "all keys".
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {Object} a request object.
+ * @private
+ */
+opensocial.Container.prototype.newFetchPersonAppDataRequest = function(idSpec,
+    keys, opt_params) {};
+
+
+/**
+ * Creates an item to request an update of an app field for the current VIEWER
+ * When processed, does not return any data.
+ * App Data is stored as a series of key value pairs of strings, scoped per
+ * person, per application. Containers supporting this request SHOULD provide
+ * at least 10KB of space per user per application for this data.
+ *
+ * @param {string} key The name of the key.
+ * @param {string} value The value.
+ * @return {Object} a request object.
+ * @private
+ */
+opensocial.Container.prototype.newUpdatePersonAppDataRequest = function(
+    key, value) {};
+
+
+/**
+ * Deletes the given keys from the datastore for the current VIEWER.
+ * When processed, does not return any data.
+ *
+ * @param {Array.<string> | string} keys The keys you want to delete from
+ *     the datastore; this can be an array of key names, a single key name,
+ *     or "*" to mean "all keys".
+ * @return {Object} A request object.
+ * @private
+ */
+opensocial.Container.prototype.newRemovePersonAppDataRequest = function(
+    keys) {};
+
+/**
+ * Updates the fields for an album specified in the params.
+ * The following fields cannot be set: MEDIA_ITEM_COUNT, OWNER_ID, ID.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people/groups
+ *    to own the album.
+ * @param {string} albumId The ID of album to update.
+ * @param {Object.<opensocial.Album.Field, Object>=} fields The album fields to update.
+ * @return {Object} A request object.
+ */
+opensocial.Container.prototype.newUpdateAlbumRequest = function(idSpec, albumId, fields) {};
+
+/**
+ * Updates the fields for a media item specified in the params.
+ * The following fields cannot be set: ID, CREATED, ALBUM_ID, FILE_SIZE, NUM_COMMENTS.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people/groups
+ *    own the album/media item.
+ * @param {string} albumId The ID of the album containing the media item to update.
+ * @param {string} mediaItemId ID of media item to update.
+ * @param {Object.<opensocial.MediaItem.Field, Object>=} fields The media item fields to update.
+ * @return {Object} A request object.
+ */
+opensocial.Container.prototype.newUpdateMediaItemRequest = function(idSpec, albumId,
+    mediaItemId, fields) {};
+
+/**
+ * Used to request an activity stream from the server.
+ *
+ * When processed, returns a Collection&lt;Activity&gt;.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people to
+ *     fetch. See also <a href="opensocial.IdSpec.html">IdSpec</a>.
+ * @param {Object.<opensocial.DataRequest.ActivityRequestFields, Object>=} opt_params
+ *    Additional params to pass to the request.
+ * @return {Object} a request object.
+ * @private
+ */
+opensocial.Container.prototype.newFetchActivitiesRequest = function(idSpec,
+    opt_params) {};
+
+opensocial.Container.prototype.newFetchAlbumsRequest = function(idSpec, opt_params) {};
+
+/**
+ * Creates an item to request media items from the container.
+ *
+ * @param {opensocial.IdSpec}
+ *          idSpec An IdSpec used to specify which media items to fetch.
+ * @param {string}
+ *          albumId The id of the album to fetch MediaItems from.
+ * @param {Object.<Object, Object>=} opt_params Additional parameters to pass to the request.
+ * @return {Object} A request object.
+ */
+opensocial.Container.prototype.newFetchMediaItemsRequest = function(idSpec, opt_params) {};
+
+opensocial.Container.prototype.newFetchMessageCollectionsRequest = function(idSpec, opt_params) {};
+opensocial.Container.prototype.newFetchMessagesRequest = function(idSpec, msgCollId, opt_params) {};
+
+/**
+ * Creates a new collection with caja support if enabled.
+ * @return {opensocial.Collection} the collection object.
+ * @private
+ */
+opensocial.Container.prototype.newCollection = function(array, opt_offset,
+    opt_totalSize) {
+  return new opensocial.Collection(array, opt_offset, opt_totalSize);
+};
+
+
+/**
+ * Creates a new person with caja support if enabled.
+ * @return {opensocial.Person} the person object.
+ * @private
+ */
+opensocial.Container.prototype.newPerson = function(opt_params, opt_isOwner,
+    opt_isViewer) {
+  return new opensocial.Person(opt_params, opt_isOwner, opt_isViewer);
+};
+
+
+/**
+ * Get an activity object used to create activities on the server
+ *
+ * @param {Object.<opensocial.Activity.Field, Object>=} opt_params Any other
+ *    fields that should be set on the activity object. All of the defined
+ *    Fields are supported.
+ * @return {opensocial.Activity} the activity object.
+ * @private
+ */
+opensocial.Container.prototype.newActivity = function(opt_params) {
+  return new opensocial.Activity(opt_params);
+};
+
+/**
+ * Get a collection of images, movies, and audio.
+ * Used when creating albums on the server.
+ *
+ * @param {Object.<opensocial.MediaItem.Field, Object>=} opt_params
+ *    Any other fields that should be set on the album object;
+ *    all of the defined
+ *    <a href="opensocial.Album.Field.html">Field</a>s
+ *    are supported.
+ *
+ * @return {opensocial.Album} the album object.
+ * @private
+ */
+opensocial.Container.prototype.newAlbum = function(opt_params) {
+  return new opensocial.Album(opt_params);
+};
+
+
+/**
+ * Creates a media item. Represents images, movies, and audio.
+ * Used when creating activities on the server.
+ *
+ * @param {string} mimeType of the media.
+ * @param {string} url where the media can be found.
+ * @param {Object.<opensocial.MediaItem.Field, Object>=} opt_params
+ *    Any other fields that should be set on the media item object.
+ *    All of the defined Fields are supported.
+ *
+ * @return {opensocial.MediaItem} the media item object.
+ * @private
+ */
+opensocial.Container.prototype.newMediaItem = function(mimeType, url,
+    opt_params) {
+  return new opensocial.MediaItem(mimeType, url, opt_params);
+};
+
+
+/**
+ * Creates a media item associated with an activity.
+ * Represents images, movies, and audio.
+ * Used when creating activities on the server.
+ *
+ * @param {string} body The main text of the message.
+ * @param {Object.<opensocial.Message.Field, Object>=} opt_params
+ *    Any other fields that should be set on the message object;
+ *    all of the defined
+ *    <a href="opensocial.Message.Field.html">Field</a>s
+ *    are supported.
+ *
+ * @return {opensocial.Message} The new
+ *    <a href="opensocial.Message.html">message</a> object.
+ * @private
+ */
+opensocial.Container.prototype.newMessage = function(body, opt_params) {
+  return new opensocial.Message(body, opt_params);
+};
+
+
+/**
+ * Creates an IdSpec object.
+ *
+ * @param {Object.<opensocial.IdSpec.Field, Object>} params
+ *    Parameters defining the id spec.
+ * @return {opensocial.IdSpec} The new
+ *     <a href="opensocial.IdSpec.html">IdSpec</a> object.
+ * @private
+ */
+opensocial.Container.prototype.newIdSpec = function(params) {
+  return new opensocial.IdSpec(params);
+};
+
+
+/**
+ * Creates a NavigationParameters object.
+ *
+ * @param {Object.<opensocial.NavigationParameters.Field, Object>} params
+ *     Parameters defining the navigation.
+ * @return {opensocial.NavigationParameters} The new
+ *     <a href="opensocial.NavigationParameters.html">NavigationParameters</a>
+ *     object.
+ * @private
+ */
+opensocial.Container.prototype.newNavigationParameters = function(params) {
+  return new opensocial.NavigationParameters(params);
+};
+
+
+/**
+ * Creates a new response item with caja support if enabled.
+ * @return {opensocial.ResponseItem} the response item object.
+ * @private
+ */
+opensocial.Container.prototype.newResponseItem = function(originalDataRequest,
+    data, opt_errorCode, opt_errorMessage) {
+  return new opensocial.ResponseItem(originalDataRequest, data, opt_errorCode,
+      opt_errorMessage);
+};
+
+
+/**
+ * Creates a new data response with caja support if enabled.
+ * @return {opensocial.DataResponse} the data response object.
+ * @private
+ */
+opensocial.Container.prototype.newDataResponse = function(responseItems,
+    opt_globalError) {
+  return new opensocial.DataResponse(responseItems, opt_globalError);
+};
+
+
+/**
+ * Get a data request object to use for sending and fetching data from the
+ * server.
+ *
+ * @return {opensocial.DataRequest} the request object.
+ * @private
+ */
+opensocial.Container.prototype.newDataRequest = function() {
+  return new opensocial.DataRequest();
+};
+
+
+/**
+ * Get a new environment object.
+ *
+ * @return {opensocial.Environment} the environment object.
+ * @private
+ */
+opensocial.Container.prototype.newEnvironment = function(domain,
+    supportedFields) {
+  return new opensocial.Environment(domain, supportedFields);
+};
+
+/**
+ * Invalidates all resources cached for the current viewer.
+ */
+opensocial.Container.prototype.invalidateCache = function() {
+};
+
+/**
+ * Returns true if the specified value is an array
+ * @param {Object} val Variable to test.
+ * @return {boolean} Whether variable is an array.
+ * @private
+ */
+opensocial.Container.isArray = function(val) {
+  return val instanceof Array;
+};
+
+
+/**
+ * Returns the value corresponding to the key in the fields map. Escapes
+ * the value appropriately.
+ * @param {Object.<string, Object>} fields All of the values mapped by key.
+ * @param {string} key The key to get data for.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @private
+ */
+opensocial.Container.getField = function(fields, key, opt_params) {
+  var value = fields[key];
+  return opensocial.Container.escape(value, opt_params, false);
+};
+
+opensocial.Container.escape = function(value, opt_params, opt_escapeObjects) {
+  if (opt_params && opt_params[opensocial.DataRequest.DataRequestFields.ESCAPE_TYPE] == opensocial.EscapeType.NONE) {
+    return value;
+  } else {
+    return gadgets.util.escape(value, opt_escapeObjects);
+  }
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/datarequest.js b/trunk/features/src/main/javascript/features/opensocial-reference/datarequest.js
new file mode 100644
index 0000000..476c5c6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/datarequest.js
@@ -0,0 +1,715 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Object used to request social information from the container.
+ * This includes data for friends, profiles, app data, and activities.
+ *
+ * All apps that require access to people information should send a dataRequest
+ * in order to receieve a dataResponse
+ */
+
+
+/**
+ * @class
+ * <p>
+ * Used to request social information from the container.
+ * This includes data for friends, profiles, app data, and activities.
+ * All apps that require access to people information
+ * should send a DataRequest.
+ * </p>
+ *
+ * <p>
+ * Here's an example of creating, initializing, sending, and handling
+ * the results of a data request:
+ * </p>
+ *
+ * <pre>function requestMe() {
+  var req = opensocial.newDataRequest();
+  req.add(req.newFetchPersonRequest(
+            opensocial.DataRequest.PersonId.VIEWER),
+          "viewer");
+  req.send(handleRequestMe);
+};
+
+function handleRequestMe(data) {
+  var viewer = data.get("viewer");
+  if (viewer.hadError()) {
+    <em>//Handle error using viewer.getError()...</em>
+    return;
+  }
+
+  <em>//No error. Do something with viewer.getData()...</em>
+}
+</pre>
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.html#newDataRequest"><code>
+ * opensocial.newDataRequest()</code></a>
+ * </p>
+ *
+ * @name opensocial.DataRequest
+ */
+
+/**
+ * Do not create a DataRequest directly, instead use
+ * opensocial.newDataRequest();
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.
+ */
+opensocial.DataRequest = function() {
+  this.requestObjects_ = [];
+};
+
+
+/**
+ * {Array.<{key: string, request:Object}>}
+ *    requestObjects An array of
+ *    data requests that the container should fetch data for
+ * @private
+ */
+opensocial.DataRequest.prototype.requestObjects_ = null;
+
+
+/**
+ * Get the requested objects
+ *
+ * @return {Array.<{key:string, request:Object}>}
+ *    requestObjects An array of data requests that the container should fetch
+ *    data for
+ * @private
+ */
+opensocial.DataRequest.prototype.getRequestObjects = function() {
+  return this.requestObjects_;
+};
+
+
+/**
+ * Adds an item to fetch (get) or update (set) data from the server.
+ * A single DataRequest object can have multiple items.
+ * As a rule, each item is executed in the order it was added,
+ * starting with the item that was added first.
+ * However, items that can't collide might be executed in parallel.
+ *
+ * @param {Object} request Specifies which data to fetch or update.
+ * @param {string} opt_key A key to map the generated response data to.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.1.
+ */
+opensocial.DataRequest.prototype.add = function(request, opt_key) {
+  return this.requestObjects_.push({'key': opt_key, 'request': request});
+};
+
+
+/**
+ * Sends a data request to the server in order to get a data response.
+ * Although the server may optimize these requests,
+ * they will always be executed
+ * as though they were serial.
+ *
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call with the
+ *   <a href="opensocial.DataResponse.html">data response</a>
+ *    generated by the server.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.2.
+ */
+opensocial.DataRequest.prototype.send = function(opt_callback) {
+  var callback = opt_callback || function() {};
+  opensocial.Container.get().requestData(this, callback);
+};
+
+
+/**
+ * @static
+ * @class
+ * The sort orders available for ordering person objects.
+ *
+ * @name opensocial.DataRequest.SortOrder
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.19.
+ */
+opensocial.DataRequest.SortOrder = {
+  /**
+   * When used will sort people by the container's definition of top friends.
+   * @member opensocial.DataRequest.SortOrder
+   */
+  TOP_FRIENDS: 'topFriends',
+  /**
+   * When used will sort people alphabetically by the name field.
+   *
+   * @member opensocial.DataRequest.SortOrder
+   */
+  NAME: 'name'
+};
+
+
+/**
+ * @static
+ * @class
+ * The filters available for limiting person requests.
+ *
+ * @name opensocial.DataRequest.FilterType
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.16.
+ */
+opensocial.DataRequest.FilterType = {
+  /**
+   * Retrieves all friends.
+   *
+   * @member opensocial.DataRequest.FilterType
+   */
+  ALL: 'all',
+  /**
+   * Retrieves all friends with any data for this application.
+   *
+   * @member opensocial.DataRequest.FilterType
+   */
+  HAS_APP: 'hasApp',
+  /**
+   * Retrieves only the user's top friends.
+   *
+   * @member opensocial.DataRequest.FilterType
+   */
+  TOP_FRIENDS: 'topFriends',
+  /**
+   * Will filter the people requested by checking if they are friends with
+   * the given <a href="opensocial.IdSpec.html">idSpec</a>. Expects a
+   *    filterOptions parameter to be passed with the following fields defined:
+   *  - idSpec The <a href="opensocial.IdSpec.html">idSpec</a> that each person
+   *        must be friends with.
+  */
+  IS_FRIENDS_WITH: 'isFriendsWith'
+};
+
+
+/**
+ * @static
+ * @class
+ * @name opensocial.DataRequest.PeopleRequestFields
+ * @enum {string}
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.18.
+ */
+opensocial.DataRequest.PeopleRequestFields = {
+  /**
+   * An array of
+   * <a href="opensocial.Person.Field.html">
+   * <code>opensocial.Person.Field</code></a>
+   * specifying what profile data to fetch
+   * for each of the person objects. The server will always include
+   * ID, NAME, and THUMBNAIL_URL.
+   *
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  PROFILE_DETAILS: 'profileDetail',
+
+  /**
+   * A sort order for the people objects; defaults to TOP_FRIENDS.
+   * Possible values are defined by
+   * <a href="opensocial.DataRequest.SortOrder.html">SortOrder</a>.
+   *
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  SORT_ORDER: 'sortOrder',
+
+  /**
+   * How to filter the people objects; defaults to ALL.
+   * Possible values are defined by
+   * <a href="opensocial.DataRequest.FilterType.html">FilterType</a>.
+   *
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  FILTER: 'filter',
+
+  /**
+   * Additional options to be passed into the filter,
+   * specified as a Map&lt;String, Object>.
+   *
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  FILTER_OPTIONS: 'filterOptions',
+
+  /**
+   * When paginating, the index of the first item to fetch.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  FIRST: 'first',
+
+  /**
+   * The maximum number of items to fetch; defaults to 20. If set to a larger
+   * number, a container may honor the request, or may limit the number to a
+   * container-specified limit of at least 20.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  MAX: 'max',
+
+  /**
+   * A string or array of strings, specifying the app data keys to fetch for
+   * each of the Person objects. This field may be used interchangeably with
+   * the string 'appData'.  Pass the string '*' to fetch all app data keys.
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  APP_DATA: 'appData',
+
+  /**
+   * How to escape app data returned from the server;
+   * defaults to HTML_ESCAPE. Possible values are defined by
+   * <a href="opensocial.EscapeType.html">EscapeType</a>.
+   * This field may be used interchangeably with the string 'escapeType'.
+   * @member opensocial.DataRequest.PeopleRequestFields
+   */
+  ESCAPE_TYPE: 'escapeType'
+};
+
+
+/**
+ * If the named param does not exist sets it to the default value.
+ *
+ * @param {Object.<string,Object>} params Parameter map.
+ * @param {string} name of the param to check.
+ * @param {*} defaultValue The value to set if the param does not exist.
+ * @private
+ */
+opensocial.DataRequest.prototype.addDefaultParam = function(params, name,
+    defaultValue) {
+  params[name] = params[name] || defaultValue;
+};
+
+
+/**
+ * Adds the default profile fields to the desired array.
+ *
+ * @param {Object} params Parameter map.
+ * @private
+ */
+opensocial.DataRequest.prototype.addDefaultProfileFields = function(params) {
+  var fields = opensocial.DataRequest.PeopleRequestFields;
+  var profileFields = params[fields.PROFILE_DETAILS] || [];
+  params[fields.PROFILE_DETAILS] = profileFields.concat(
+      [opensocial.Person.Field.ID, opensocial.Person.Field.NAME,
+       opensocial.Person.Field.THUMBNAIL_URL]);
+};
+
+
+/**
+ * Returns the keys object as an array.
+ *
+ * @param {Object} keys
+ * @private
+ */
+opensocial.DataRequest.prototype.asArray = function(keys) {
+  if (opensocial.Container.isArray(keys)) {
+    return keys;
+  } else {
+    return [keys];
+  }
+};
+
+
+/**
+ * Creates a new album and returns the ID of the album created.
+ *
+ * @param {opensocial.IdSpec} idSpec The ID of the used to specify which people/groups
+ *   to create an album for.
+ * @param {opensocial.Album} album The album to create.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.2.
+ */
+opensocial.DataRequest.prototype.newCreateAlbumRequest = function(idSpec, album) {
+  return opensocial.Container.get().newCreateAlbumRequest(idSpec, album);
+};
+
+/**
+ * Creates a new media item in the album and returns the ID of the album created.
+ *
+ * @param {opensocial.IdSpec} idSpec The ID of the used to specify which people/groups
+ *   to create an album for.
+ * @param {string} albumId The ID of album to add the media item to.
+ * @param {openSocial.MediaItem} mediaItem The MediaItem to add to the album.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.3.
+ */
+opensocial.DataRequest.prototype.newCreateMediaItemRequest = function(idSpec, albumId,
+    mediaItem) {
+  return opensocial.Container.get().newCreateMediaItemRequest(idSpec, albumId, mediaItem);
+};
+
+/**
+ * Deletes the album specified.
+ *
+ * @param {opensocial.IdSpec} idSpec The ID of the used to specify which people/groups
+ *   to create an album for.
+ * @param {string} albumId The ID of the album to create.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.4.
+ */
+opensocial.DataRequest.prototype.newDeleteAlbumRequest = function(idSpec, albumId) {
+  return opensocial.Container.get().newDeleteAlbumRequest(idSpec, albumId);
+};
+
+/**
+ * Creates an item to request a profile for the specified person ID.
+ * When processed, returns a
+ * <a href="opensocial.Person.html"><code>Person</code></a> object.
+ *
+ * @param {string} id The ID of the person to fetch; can be the standard
+ *    <a href="opensocial.IdSpec.PersonId.html">person ID</a>
+ *    of VIEWER or OWNER.
+ * @param {Object.<opensocial.DataRequest.PeopleRequestFields, Object>=} opt_params
+ *    Additional
+ *    <a href="opensocial.DataRequest.PeopleRequestFields.html">parameters</a>
+ *    to pass to the request; this request supports PROFILE_DETAILS.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.10.
+ */
+opensocial.DataRequest.prototype.newFetchPersonRequest = function(id,
+    opt_params) {
+  opt_params = opt_params || {};
+  this.addDefaultProfileFields(opt_params);
+
+  return opensocial.Container.get().newFetchPersonRequest(id, opt_params);
+};
+
+
+/**
+ * Creates an item to request friends from the server.
+ * When processed, returns a <a href="opensocial.Collection.html">Collection</a>
+ * &lt;<a href="opensocial.Person.html">Person</a>&gt; object.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify
+ *    which people to fetch. See also <a href="opensocial.IdSpec.html">IdSpec</a>.
+ * @param {Object.<opensocial.DataRequest.PeopleRequestFields, Object>=} opt_params
+ *    Additional
+ *    <a href="opensocial.DataRequest.PeopleRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.8.
+ */
+opensocial.DataRequest.prototype.newFetchPeopleRequest = function(idSpec,
+    opt_params) {
+  opt_params = opt_params || {};
+  var fields = opensocial.DataRequest.PeopleRequestFields;
+
+  this.addDefaultProfileFields(opt_params);
+
+  this.addDefaultParam(opt_params, fields.SORT_ORDER,
+      opensocial.DataRequest.SortOrder.TOP_FRIENDS);
+
+  this.addDefaultParam(opt_params, fields.FILTER,
+      opensocial.DataRequest.FilterType.ALL);
+
+  this.addDefaultParam(opt_params, fields.FIRST, 0);
+
+  this.addDefaultParam(opt_params, fields.MAX, 20);
+
+  return opensocial.Container.get().newFetchPeopleRequest(idSpec, opt_params);
+};
+
+
+/**
+ * @static
+ * @class
+ * @name opensocial.DataRequest.AlbumRequestFields
+ * @enum {string}
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.14.
+ */
+opensocial.DataRequest.AlbumRequestFields = {
+  /**
+   * When paginating, the index of the first item to fetch.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.AlbumRequestFields
+   */
+  FIRST: 'first',
+
+  /**
+   * The maximum number of items to fetch; defaults to 20.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.AlbumRequestFields
+   */
+  MAX: 'max'
+};
+
+/**
+ * @static
+ * @class
+ * @name opensocial.DataRequest.MediaItemRequestFields
+ * @enum {string}
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.17.
+ */
+opensocial.DataRequest.MediaItemRequestFields = {
+  /**
+   * When paginating, the index of the first item to fetch.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.MediaItemRequestFields
+   */
+  FIRST: 'first',
+
+  /**
+   * The maximum number of items to fetch; defaults to 20.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.MediaItemRequestFields
+   */
+  MAX: 'max'
+};
+
+/**
+ * @static
+ * @class
+ * @name opensocial.DataRequest.DataRequestFields
+ * @enum {string}
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.15.
+ */
+opensocial.DataRequest.DataRequestFields = {
+  /**
+   * How to escape person data returned from the server; defaults to HTML_ESCAPE.
+   * Possible values are defined by
+   * <a href="opensocial.EscapeType.html">EscapeType</a>.
+   * Use of this function is deprecated in favor of using the
+   * <a href="#opensocial.DataRequest.PeopleRequestFields.ESCAPE_TYPE">
+   * ESCAPE_TYPE</a> request field.
+   *
+   * @deprecated
+   * @member opensocial.DataRequest.DataRequestFields
+   */
+  ESCAPE_TYPE: 'escapeType'
+};
+
+
+/**
+ * Creates an item to request app data for the given people.
+ * When processed, returns a Map&lt;
+ * <a href="opensocial.DataRequest.PersonId.html">PersonId</a>,
+ * Map&lt;String, String&gt;&gt; object.
+ * All of the data values returned will be valid json.
+ * Use of this function is deprecated in favor of using the
+ * <a href="#opensocial.DataRequest.PeopleRequestFields.APP_DATA">APP_DATA</a>
+ * request field.
+ *
+ * @deprecated
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people to
+ *     fetch. See also <a href="opensocial.IdSpec.html">IdSpec</a>.
+ * @param {Array.<string> | string} keys The keys you want data for; this
+ *     can be an array of key names, a single key name, or "*" to mean
+ *     "all keys".
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>=} opt_params
+ *    Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.9.
+ */
+opensocial.DataRequest.prototype.newFetchPersonAppDataRequest = function(idSpec,
+    keys, opt_params) {
+  return opensocial.Container.get().newFetchPersonAppDataRequest(idSpec,
+      this.asArray(keys), opt_params);
+};
+
+/**
+ * Updates the fields for an album specified in the params.
+ * The following fields cannot be set: MEDIA_ITEM_COUNT, OWNER_ID, ID.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people/groups
+ *    to own the album.
+ * @param {string} albumId The ID of album to update.
+ * @param {Object.<opensocial.Album.Field, Object>=} opt_params The Album Fields to update.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.12.
+ */
+opensocial.DataRequest.prototype.newUpdateAlbumRequest = function(idSpec, albumId,
+    opt_params) {
+  return opensocial.Container.get().newUpdateAlbumRequest(idSpec, albumId, opt_params);
+};
+
+/**
+ * Updates the fields for a media item specified in the params.
+ * The following fields cannot be set: ID, CREATED, ALBUM_ID, FILE_SIZE, NUM_COMMENTS.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people/groups
+ *    own the album/media item.
+ * @param {string} albumId The ID of the album containing the media item to update.
+ * @param {string} mediaItemId ID of media item to update.
+ * @param {Object.<opensocial.MediaItem.Field, Object>=} opt_params The Album Fields to update.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.12.
+ */
+opensocial.DataRequest.prototype.newUpdateMediaItemRequest = function(idSpec, albumId,
+    mediaItemId, opt_params) {
+  return opensocial.Container.get().newUpdateMediaItemRequest(idSpec, albumId,
+      mediaItemId, opt_params);
+};
+
+/**
+ * Creates an item to request an update of an app field for the current VIEWER
+ * When processed, does not return any data.
+ * App Data is stored as a series of key value pairs of strings, scoped per
+ * person, per application. Containers supporting this request SHOULD provide
+ * at least 10KB of space per user per application for this data.
+ * @param {string} key The name of the key. This may only contain alphanumeric
+ *     (A-Za-z0-9) characters, underscore(_), dot(.) or dash(-).
+ * @param {string} value The value, must be valid json.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.14.
+ */
+opensocial.DataRequest.prototype.newUpdatePersonAppDataRequest = function(
+    key, value) {
+  return opensocial.Container.get().newUpdatePersonAppDataRequest(key,
+      value);
+};
+
+
+/**
+ * Deletes the given keys from the datastore for the current VIEWER.
+ * When processed, does not return any data.
+ *
+ * @param {Array.<string> | string} keys The keys you want to delete from
+ *     the datastore; this can be an array of key names, a single key name,
+ *     or "*" to mean "all keys".
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.11.
+ */
+opensocial.DataRequest.prototype.newRemovePersonAppDataRequest = function(keys) {
+  return opensocial.Container.get().newRemovePersonAppDataRequest(keys);
+};
+
+
+/**
+ * @static
+ * @class
+ * Used by
+ * <a href="opensocial.DataRequest.html#newFetchActivitiesRequest">
+ * <code>DataRequest.newFetchActivitiesRequest()</code></a>.
+ * @name opensocial.DataRequest.ActivityRequestFields
+ * @private
+ * @enum {string}
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.13.
+ */
+opensocial.DataRequest.ActivityRequestFields = {
+  /**
+   * If provided will filter all activities by this app Id.
+   * @private - at the moment you can only request activities for your own app
+   */
+  APP_ID: 'appId',
+  /**
+   * When paginating, the index of the first item to fetch.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.ActivityRequestFields
+   */
+  FIRST: 'first',
+
+  /**
+   * The maximum number of items to fetch; defaults to 20. If set to a larger
+   * number, a container may honor the request, or may limit the number to a
+   * container-specified limit of at least 20.
+   * Specified as a <code>Number</code>.
+   *
+   * @member opensocial.DataRequest.ActivityRequestFields
+   */
+  MAX: 'max'
+};
+
+
+/**
+ * Creates an item to request an activity stream from the server.
+ *
+ * <p>
+ * When processed, returns a Collection&lt;Activity&gt;.
+ * </p>
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which people to
+ *     fetch. See also <a href="opensocial.IdSpec.html">IdSpec</a>.
+ * @param {Object.<opensocial.DataRequest.ActivityRequestFields, Object>=} opt_params
+ *    Additional parameters
+ *    to pass to the request; not currently used.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.5.
+ */
+opensocial.DataRequest.prototype.newFetchActivitiesRequest = function(idSpec,
+    opt_params) {
+  opt_params = opt_params || {};
+
+  var fields = opensocial.DataRequest.ActivityRequestFields;
+
+  this.addDefaultParam(opt_params, fields.FIRST, 0);
+  this.addDefaultParam(opt_params, fields.MAX, 20);
+
+  return opensocial.Container.get().newFetchActivitiesRequest(idSpec,
+      opt_params);
+};
+
+/**
+ * Creates an item to request albums from the container.
+ *
+ * @param {opensocial.IdSpec} idSpec An IdSpec used to specify which albums to fetch.
+ * @param {Object.<Object, Object>=} opt_params Additional parameters to pass to the request.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.6.
+ */
+opensocial.DataRequest.prototype.newFetchAlbumsRequest = function(idSpec, opt_params) {
+  opt_params = opt_params || {};
+
+  var fields = opensocial.DataRequest.AlbumRequestFields;
+
+  this.addDefaultParam(opt_params, fields.FIRST, 0);
+  this.addDefaultParam(opt_params, fields.MAX, 20);
+
+  return opensocial.Container.get().newFetchAlbumsRequest(idSpec, opt_params);
+};
+
+/**
+ * Creates an item to request media items from the container.
+ *
+ * @param {opensocial.IdSpec}
+ *          idSpec An IdSpec used to specify which media items to fetch.
+ * @param {string}
+ *          albumId The id of the album to fetch media items from.
+ * @param {Object.<Object, Object>=} opt_params Additional parameters to pass to the request.
+ * @return {Object} A request object.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.12.1.7.
+ */
+opensocial.DataRequest.prototype.newFetchMediaItemsRequest = function(idSpec, albumId, opt_params) {
+  opt_params = opt_params || {};
+
+  var fields = opensocial.DataRequest.MediaItemRequestFields;
+
+  this.addDefaultParam(opt_params, fields.FIRST, 0);
+  this.addDefaultParam(opt_params, fields.MAX, 20);
+
+  return opensocial.Container.get().newFetchMediaItemsRequest(idSpec, albumId, opt_params);
+};
+
+/**
+ * Creates an item to request messages from the container.
+ */
+opensocial.DataRequest.prototype.newFetchMessageCollectionsRequest = function(idSpec, opt_params) {
+  opt_params = opt_params || {};
+  return opensocial.Container.get().newFetchMessageCollectionsRequest(idSpec, opt_params);
+};
+
+opensocial.DataRequest.prototype.newFetchMessagesRequest = function(idSpec, msgCollId, opt_params) {
+  opt_params = opt_params || {};
+  return opensocial.Container.get().newFetchMessagesRequest(idSpec, msgCollId, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/dataresponse.js b/trunk/features/src/main/javascript/features/opensocial-reference/dataresponse.js
new file mode 100644
index 0000000..30cb766
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/dataresponse.js
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview DataResponse containing information about
+ * friends, contacts, profile, app data, and activities.
+ *
+ * Whenever a dataRequest is sent to the server it will return a dataResponse
+ * object. Values from the server will be mapped to the requested keys specified
+ * in the dataRequest.
+ */
+
+
+/**
+ * @class
+ * This object contains the requested server data mapped to the requested keys.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.DataRequest.html">DataRequest</a>
+ * </p>
+ *
+ * @name opensocial.DataResponse
+ */
+
+/**
+ * Construct the data response.
+ * This object contains the requested server data mapped to the requested keys.
+ *
+ * @param {Object.<string, ResponseItem>} responseItems Key/value map of data
+ *    response information.
+ * @param {boolean=} opt_globalError Optional field indicating whether there were
+ *    any errors generating this data response.
+ * @param {string=} opt_errorMessage
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.20.
+ */
+opensocial.DataResponse = function(responseItems, opt_globalError,
+    opt_errorMessage) {
+  this.responseItems_ = responseItems;
+  this.globalError_ = opt_globalError;
+  this.errorMessage_ = opt_errorMessage;
+};
+
+
+/**
+ * Returns true if there was an error in fetching this data from the server.
+ *
+ * @return {boolean} True if there was an error; otherwise, false.
+ * @member opensocial.DataResponse
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.20.1.3.
+ */
+opensocial.DataResponse.prototype.hadError = function() {
+  return !!this.globalError_;
+};
+
+
+/**
+ * If the entire request had a batch level error, returns the error message.
+ *
+ * @return {string} A human-readable description of the error that occurred.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.20.1.2.
+ */
+opensocial.DataResponse.prototype.getErrorMessage = function() {
+  return this.errorMessage_;
+};
+
+
+/**
+ * Gets the ResponseItem for the requested field.
+ *
+ * @return {opensocial.ResponseItem} The requested
+ *    <a href="opensocial.ResponseItem.html">response</a> calculated by the
+ *    server.
+ * @member opensocial.DataResponse
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.20.1.1.
+ */
+opensocial.DataResponse.prototype.get = function(key) {
+  return this.responseItems_[key];
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/email.js b/trunk/features/src/main/javascript/features/opensocial-reference/email.js
new file mode 100644
index 0000000..b5ae44d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/email.js
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of an email.
+ */
+
+
+/**
+ * @class
+ * Base interface for all email objects.
+ *
+ * @name opensocial.Email
+ */
+
+
+/**
+ * Base interface for all email objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.21.
+ */
+opensocial.Email = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that an email has. These are the supported keys for the
+ * <a href="opensocial.Email.html#getField">Email.getField()</a> method.
+ *
+ * @name opensocial.Email.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.22.
+ */
+opensocial.Email.Field = {
+  /**
+   * The email type or label, specified as a String.
+   * Examples: work, my favorite store, my house, etc.
+   *
+   * @member opensocial.Email.Field
+   */
+  TYPE: 'type',
+
+  /**
+   * The email address, specified as a String.
+   *
+   * @member opensocial.Email.Field
+   */
+  ADDRESS: 'address'
+};
+
+
+/**
+ * Gets data for this body type that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.Email.Field.html"><code>
+ *    Email.Field</code></a>.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.21.1.1.
+ */
+opensocial.Email.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/enum.js b/trunk/features/src/main/javascript/features/opensocial-reference/enum.js
new file mode 100644
index 0000000..16c754f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/enum.js
@@ -0,0 +1,240 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial,gadgets */
+
+/**
+ * @fileoverview Representation of an enum.
+ */
+
+
+/**
+ * @class
+ * Base interface for all enum objects.
+ * This class allows containers to use constants for fields that are usually
+ * have a common set of values.
+ * There are two main ways to use this class.
+ *
+ * <p>
+ * If your gadget just wants to display how much of a smoker someone is,
+ * it can simply use:
+ * </p>
+ *
+ * <pre>html = "This person smokes: " + person.getField('smoker').getValue();</pre>
+ *
+ * <p>
+ * This value field will be correctly set up by the container. This is a place
+ * where the container can even localize the value for the gadget so that it
+ * always shows the right thing.
+ * </p>
+ *
+ * <p>
+ * If your gadget wants to have some logic around the smoker
+ * field it can use:
+ * </p>
+ *
+ * <pre>if (person.getField('smoker').getKey() != "NO") { //gadget logic here }</pre>
+ *
+ * <p class="note">
+ * <b>Note:</b>
+ * The key may be null if the person's smoker field cannot be coerced
+ * into one of the standard enum types.
+ * The value, on the other hand, is never null.
+ * </p>
+ *
+ * @name opensocial.Enum
+ */
+
+
+/**
+ * Base interface for all enum objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.23.
+ */
+opensocial.Enum = function(key, displayValue) {
+  this.key = key;
+  this.displayValue = displayValue;
+};
+
+
+/**
+ * Use this for logic within your gadget. If they key is null then the value
+ * does not fit in the defined enums.
+ *
+ * @return {string} The enum's key. This should be one of the defined enums
+ *     below.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.23.1.2.
+ */
+opensocial.Enum.prototype.getKey = function() {
+  return gadgets.util.escape(this.key);
+};
+
+
+/**
+ * The value of this enum. This will be a user displayable string. If the
+ * container supports localization, the string will be localized.
+ *
+ * @return {string} The enum's value.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.23.1.1.
+ */
+opensocial.Enum.prototype.getDisplayValue = function() {
+  return gadgets.util.escape(this.displayValue);
+};
+
+
+/**
+ * @static
+ * @class
+ * The enum keys used by the smoker field.
+ * <p><b>See also:</b>
+ * <a href="opensocial.Person.Field.html">
+ * opensocial.Person.Field.Smoker</a>
+ * </p>
+ *
+ * @name opensocial.Enum.Smoker
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.28.
+ */
+opensocial.Enum.Smoker = {
+  /** @member opensocial.Enum.Smoker */
+  NO: 'NO',
+  /** @member opensocial.Enum.Smoker */
+  YES: 'YES',
+  /** @member opensocial.Enum.Smoker */
+  SOCIALLY: 'SOCIALLY',
+  /** @member opensocial.Enum.Smoker */
+  OCCASIONALLY: 'OCCASIONALLY',
+  /** @member opensocial.Enum.Smoker */
+  REGULARLY: 'REGULARLY',
+  /** @member opensocial.Enum.Smoker */
+  HEAVILY: 'HEAVILY',
+  /** @member opensocial.Enum.Smoker */
+  QUITTING: 'QUITTING',
+  /** @member opensocial.Enum.Smoker */
+  QUIT: 'QUIT'
+};
+
+
+/**
+ * @static
+ * @class
+ * The enum keys used by the drinker field.
+ * <p><b>See also:</b>
+ * <a href="opensocial.Person.Field.html">
+ * opensocial.Person.Field.Drinker</a>
+ * </p>
+ *
+ * @name opensocial.Enum.Drinker
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.24.
+ */
+opensocial.Enum.Drinker = {
+  /** @member opensocial.Enum.Drinker */
+  NO: 'NO',
+  /** @member opensocial.Enum.Drinker */
+  YES: 'YES',
+  /** @member opensocial.Enum.Drinker */
+  SOCIALLY: 'SOCIALLY',
+  /** @member opensocial.Enum.Drinker */
+  OCCASIONALLY: 'OCCASIONALLY',
+  /** @member opensocial.Enum.Drinker */
+  REGULARLY: 'REGULARLY',
+  /** @member opensocial.Enum.Drinker */
+  HEAVILY: 'HEAVILY',
+  /** @member opensocial.Enum.Drinker */
+  QUITTING: 'QUITTING',
+  /** @member opensocial.Enum.Drinker */
+  QUIT: 'QUIT'
+};
+
+
+/**
+ * @static
+ * @class
+ * The enum keys used by the gender field.
+ * <p><b>See also:</b>
+ * <a href="opensocial.Person.Field.html">
+ * opensocial.Person.Field.Gender</a>
+ * </p>
+ *
+ * @name opensocial.Enum.Gender
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.25.
+ */
+opensocial.Enum.Gender = {
+  /** @member opensocial.Enum.Gender */
+  MALE: 'MALE',
+  /** @member opensocial.Enum.Gender */
+  FEMALE: 'FEMALE'
+};
+
+
+/**
+ * @static
+ * @class
+ * The enum keys used by the lookingFor field.
+ * <p><b>See also:</b>
+ * <a href="opensocial.Person.Field.html">
+ * opensocial.Person.Field.LookingFor</a>
+ * </p>
+ *
+ * @name opensocial.Enum.LookingFor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.26.
+ */
+opensocial.Enum.LookingFor = {
+  /** @member opensocial.Enum.LookingFor */
+  DATING: 'DATING',
+  /** @member opensocial.Enum.LookingFor */
+  FRIENDS: 'FRIENDS',
+  /** @member opensocial.Enum.LookingFor */
+  RELATIONSHIP: 'RELATIONSHIP',
+  /** @member opensocial.Enum.LookingFor */
+  NETWORKING: 'NETWORKING',
+  /** @member opensocial.Enum.LookingFor */
+  ACTIVITY_PARTNERS: 'ACTIVITY_PARTNERS',
+  /** @member opensocial.Enum.LookingFor */
+  RANDOM: 'RANDOM'
+};
+
+
+/**
+ * @static
+ * @class
+ * The enum keys used by the networkPresence field.
+ * <p><b>See also:</b>
+ * <a href="opensocial.Person.Field.html">
+ * opensocial.Person.Field.NetworkPresence</a>
+ * </p>
+ *
+ * @name opensocial.Enum.Presence
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.27.
+ */
+opensocial.Enum.Presence = {
+  /** @member opensocial.Enum.Presence */
+  AWAY: 'AWAY',
+  /** @member opensocial.Enum.Presence */
+  CHAT: 'CHAT',
+  /** @member opensocial.Enum.Presence */
+  DND: 'DND',
+  /** @member opensocial.Enum.Presence */
+  OFFLINE: 'OFFLINE',
+  /** @member opensocial.Enum.Presence */
+  ONLINE: 'ONLINE',
+  /** @member opensocial.Enum.Presence */
+  XA: 'XA'
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/environment.js b/trunk/features/src/main/javascript/features/opensocial-reference/environment.js
new file mode 100644
index 0000000..e745d9f
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/environment.js
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of a environment.
+ */
+
+
+/**
+ * @class
+ * Represents the current environment for a gadget.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.html#getEnvironment">opensocial.getEnvironment()</a>,
+ *
+ * @name opensocial.Environment
+ */
+
+
+/**
+ * Base interface for all environment objects.
+ *
+ * @param {string} domain The current domain.
+ * @param {Object.<string, Object.<string, boolean>>} supportedFields
+ *    The fields supported by this container.
+ *
+ * @private
+ * @constructor
+ */
+opensocial.Environment = function(domain, supportedFields) {
+  this.domain = domain;
+  this.supportedFields = supportedFields;
+};
+
+
+/**
+ * Returns the current domain &mdash;
+ * for example, "orkut.com" or "myspace.com".
+ *
+ * @return {string} The domain.
+ */
+opensocial.Environment.prototype.getDomain = function() {
+  return this.domain;
+};
+
+
+/**
+ * @static
+ * @class
+ *
+ * The types of objects in this container.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.Environment.html#supportsField">
+ * <code>Environment.supportsField()</code></a>
+ *
+ * @name opensocial.Environment.ObjectType
+ */
+opensocial.Environment.ObjectType = {
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  PERSON: 'person',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  ADDRESS: 'address',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  BODY_TYPE: 'bodyType',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  EMAIL: 'email',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  NAME: 'name',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  ORGANIZATION: 'organization',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  PHONE: 'phone',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  URL: 'url',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  ACTIVITY: 'activity',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  MEDIA_ITEM: 'mediaItem',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  MESSAGE: 'message',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  MESSAGE_TYPE: 'messageType',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  SORT_ORDER: 'sortOrder',
+  /**
+   * @member opensocial.Environment.ObjectType
+   */
+  FILTER_TYPE: 'filterType'
+};
+
+
+/**
+ * Returns true if the specified field is supported in this container on the
+ * given object type.
+ *
+ * @param {opensocial.Environment.ObjectType} objectType
+ *    The <a href="opensocial.Environment.ObjectType.html">object type</a>
+ *    to check for the field.
+ * @param {string} fieldName The name of the field to check for.
+ * @return {boolean} True if the field is supported on the specified object type.
+ */
+opensocial.Environment.prototype.supportsField = function(objectType,
+    fieldName) {
+  var supportedObjectFields = this.supportedFields[objectType] || [];
+  return !!supportedObjectFields[fieldName];
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/feature.xml b/trunk/features/src/main/javascript/features/opensocial-reference/feature.xml
new file mode 100644
index 0000000..f61c4d6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/feature.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>opensocial-reference</name>
+  <dependency>taming</dependency>
+  <dependency>core.util</dependency>
+  <gadget>
+    <script src="opensocial.js"/>
+    <script src="activity.js"/>
+    <script src="address.js"/>
+    <script src="album.js"/>
+    <script src="bodytype.js"/>
+    <script src="collection.js"/>
+    <script src="container.js"/>
+    <script src="datarequest.js"/>
+    <script src="dataresponse.js"/>
+    <script src="email.js"/>
+    <script src="enum.js"/>
+    <script src="environment.js"/>
+    <script src="idspec.js"/>
+    <script src="mediaitem.js"/>
+    <script src="messagecollection.js"/>
+    <script src="message.js"/>
+    <script src="name.js"/>
+    <script src="navigationparameters.js"/>
+    <script src="organization.js"/>
+    <script src="person.js"/>
+    <script src="phone.js"/>
+    <script src="responseitem.js"/>
+    <script src="url.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <uses type="rpc">requestSendMessage</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/idspec.js b/trunk/features/src/main/javascript/features/opensocial-reference/idspec.js
new file mode 100644
index 0000000..1daa19b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/idspec.js
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of an group of people ids.
+ */
+
+
+/**
+ * @class
+ * Base interface for all id spec objects.
+ *
+ * @name opensocial.IdSpec
+ */
+
+
+/**
+ * Base interface for all id spec objects. Use this class when specifying which
+ * people you want to fetch.
+ *
+ * For example, opensocial.newIdSpec({userId : 'VIEWER', groupId : 'FRIENDS'})
+ *                means you are looking for all of the viewer's friends.
+ * For example, opensocial.newIdSpec({userId : 'VIEWER',
+ *                                    groupId : 'FRIENDS', networkDistance : 2})
+ *                means you are looking for all of the viewer's friends of friends.
+ * For example, opensocial.newIdSpec({userId : 'OWNER'})
+ *                means you are looking for the owner.
+ *
+ * Private, see opensocial.newIdSpec() for usage.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.30.
+ */
+opensocial.IdSpec = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that id specs can have.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a
+ * href="opensocial.IdSpec.html#getField">opensocial.IdSpec.getField()</a>
+ * </p>
+ *
+ * @name opensocial.IdSpec.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.31.
+ */
+opensocial.IdSpec.Field = {
+  /**
+   * A string or an array of strings representing the user id. Can be
+   * one of the opensocial.IdSpec.PersonId values.
+   * @member opensocial.IdSpec.Field
+   */
+  USER_ID: 'userId',
+
+  /**
+   * A string representing the group id or one of the
+   * opensocial.IdSpec.GroupId values. Defaults to SELF.
+   * @member opensocial.IdSpec.Field
+   */
+  GROUP_ID: 'groupId',
+
+  /**
+   * An optional numeric parameter, used to specify how many "hops"
+   * are allowed between two people still considered part of the
+   * same group.
+   * Defaults to 1 (they must be the same person or
+   * directly be connected by the group).
+   *
+   * Not all containers will support networkDistances greater than 1.
+   *
+   * @member opensocial.IdSpec.Field
+   */
+  NETWORK_DISTANCE: 'networkDistance'
+};
+
+
+/**
+ * @static
+ * @class
+ * Constant person IDs available when fetching person information.
+ *
+ * @name opensocial.IdSpec.PersonId
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.33.
+ */
+opensocial.IdSpec.PersonId = {
+  /**
+  * @member opensocial.IdSpec.PersonId
+  */
+  OWNER: 'OWNER',
+  /**
+  * @member opensocial.IdSpec.PersonId
+  */
+  VIEWER: 'VIEWER'
+};
+
+
+/**
+ * @static
+ * @class
+ * Constant group IDs available when fetching collections of people.
+ *
+ * @name opensocial.IdSpec.GroupId
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.32.
+ */
+opensocial.IdSpec.GroupId = {
+  /**
+  * @member opensocial.IdSpec.GroupId
+  */
+  SELF: 'SELF',
+  /**
+  * @member opensocial.IdSpec.GroupId
+  */
+  FRIENDS: 'FRIENDS',
+  /**
+  * @member opensocial.IdSpec.GroupId
+  */
+  ALL: 'ALL'
+};
+
+
+/**
+ * Gets the id spec's data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *   see the <a href="opensocial.IdSpec.Field.html">Field</a> class
+ * for possible values.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @member opensocial.IdSpec
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.30.1.1.
+ */
+opensocial.IdSpec.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this id spec associated with the given key.
+ *
+ * @param {string} key The key to set data for.
+ * @param {string} data The data to set.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.30.1.2.
+ */
+opensocial.IdSpec.prototype.setField = function(key, data) {
+  return (this.fields_[key] = data);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/mediaitem.js b/trunk/features/src/main/javascript/features/opensocial-reference/mediaitem.js
new file mode 100644
index 0000000..c967a95
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/mediaitem.js
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @class
+ * Represents images, movies, and audio.
+ * Create a <code>MediaItem</code> object using the
+ * <a href="opensocial.html#newMediaItem">
+ * opensocial.newMediaItem()</a> method.
+ *
+ * @name opensocial.MediaItem
+ */
+
+/**
+ * Represents images, movies, and audio.
+ *
+ * @param {string} mimeType The media's type.
+ * @param {string} url The media's location.
+ * @param {Object.<opensocial.MediaItem.Field, Object>=} opt_params
+ *    Any other fields that should be set on the media item object.
+ *    All of the defined Fields are supported.
+ * @constructor
+ * @private
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.34.
+ */
+opensocial.MediaItem = function(mimeType, url, opt_params) {
+  this.fields_ = {};
+  if (opt_params) {
+    for (var k in opt_params) {
+      if (opt_params.hasOwnProperty(k)) {
+        this.fields_[k] = opt_params[k];
+      }
+    }
+  }
+  this.fields_[opensocial.MediaItem.Field.MIME_TYPE] = mimeType;
+  this.fields_[opensocial.MediaItem.Field.URL] = url;
+};
+
+
+/**
+ * @static
+ * @class
+ * The possible types of media items.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.MediaItem.Field.html">
+ * opensocial.MediaItem.Field</a>
+ * </p>
+ *
+ * @name opensocial.MediaItem.Type
+ * @enum {string}
+ * @const
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.36.
+ */
+opensocial.MediaItem.Type = {
+  /** @member opensocial.MediaItem.Type */
+  IMAGE: 'image',
+  /** @member opensocial.MediaItem.Type */
+  VIDEO: 'video',
+  /** @member opensocial.MediaItem.Type */
+  AUDIO: 'audio'
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that media items have.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.MediaItem.html#getField">
+ * opensocial.MediaItem.getField()</a>
+ * </p>
+ *
+ * @name opensocial.MediaItem.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.35.
+ */
+opensocial.MediaItem.Field = {
+
+  /**
+   * The album to which the media item belongs, specified as a String.
+   * @member opensocial.MediaItem.Field
+   */
+  ALBUM_ID: 'albumId',
+
+  /**
+   * The creation time associated with the media item - assigned by container in UTC, specified as a String.
+   * @member opensocial.MediaItem.Field
+  */
+  CREATED: 'created',
+
+  /**
+   * The description of the media item, specified as a String.
+   *
+   * @member opensocial.MediaItem.Field
+   */
+  DESCRIPTION: 'description',
+
+  /**
+   * An integer specified for audio/video clips - playtime length in seconds, set to -1/not defined if unknown.
+   * @member opensocial.MediaItem.Field
+   */
+  DURATION: 'duration',
+
+  /**
+   * A long specified the number of bytes (set to -1/undefined if unknown).
+   * @member opensocial.MediaItem.Field
+   */
+  FILE_SIZE: 'fileSize',
+
+  /**
+   * An id associated with the media item, specified as a String.
+   * @member opensocial.MediaItem.Field
+   */
+  ID: 'id',
+
+  /**
+   * A language associated with the media item in ISO 639-3 format, specified as a String.
+   * @member opensocial.MediaItem.Field
+   */
+  LANGUAGE: 'language',
+
+  /**
+   * An update time associated with the media item - assigned by container in UTC, specified as a String.
+   * @member opensocial.MediaItem.Field
+   */
+  LAST_UPDATED: 'lastUpdated',
+
+  /**
+   * A location corresponding to the media item, specified as a <a href="opensocial.MediaItem.html"> object.
+   * @member opensocial.MediaItem.Field
+   */
+  LOCATION: 'location',
+
+  /**
+   * The MIME type of media, specified as a String.
+   * @member opensocial.MediaItem.Field
+   */
+  MIME_TYPE: 'mimeType',
+
+  /**
+   * A number of comments on the photo, specified as a integer.
+   * @member opensocial.MediaItem.Field
+   */
+  NUM_COMMENTS: 'numComments',
+
+  /**
+   * A number of views for the media item, specified as a integer.
+   * @member opensocial.MediaItem.Field
+   */
+  NUM_VIEWS: 'numViews',
+
+  /**
+   * A number of votes received for voting, specified as a integer.
+   * @member opensocial.MediaItem.Field
+   */
+  NUM_VOTES: 'numVotes',
+
+  /**
+   * An average rating of the media item on a scale of 0-10, specified as a integer.
+   * @member opensocial.MediaItem.Field
+   */
+  RATING: 'rating',
+
+  /**
+   * A string specified for streaming/live content - time when the content is available.
+   * @member opensocial.MediaItem.Field
+   */
+  START_TIME: 'startTime',
+
+  /**
+   * An array of string (IDs) of people tagged in the media item, specified as an array of Strings.
+   * @member opensocial.MediaItem.Field
+   */
+  TAGGED_PEOPLE: 'taggedPeople',
+
+  /**
+   * Tags associated with this media item, specified as an array of Strings.
+   * @member opensocial.MediaItem.Field
+   */
+  TAGS: 'tags',
+
+  /**
+   * URL to a thumbnail image of the media item, specified as a String.
+   * @member opensocial.MediaItem.Field
+   */
+  THUMBNAIL_URL: 'thumbnailUrl',
+
+  /**
+   * A string describing the media item, specified as a String.
+   * @member opensocial.MediaItem.Field
+   */
+  TITLE: 'title',
+
+  /**
+   * The type of media, specified as a
+   * <a href="opensocial.MediaItem.Type.html">
+   * <code>MediaItem.Type</code></a> object.
+   * @member opensocial.MediaItem.Field
+   */
+  TYPE: 'type',
+
+  /**
+   * A string specifying the URL where the media can be found.
+   * @member opensocial.MediaItem.Field
+   */
+  URL: 'url'
+};
+
+
+/**
+ * Gets the media item data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for; see the
+ *   <a href="opensocial.MediaItem.Field.html">Field</a> class
+ *   for possible values.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.34.1.1.
+ */
+opensocial.MediaItem.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this media item associated with the given key.
+ *
+ * @param {string} key The key to set data for.
+ * @param {string} data The data to set.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.34.1.2.
+ */
+opensocial.MediaItem.prototype.setField = function(key, data) {
+  return (this.fields_[key] = data);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/message.js b/trunk/features/src/main/javascript/features/opensocial-reference/message.js
new file mode 100644
index 0000000..adb9478
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/message.js
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of a message.
+ */
+
+
+/**
+ * @class
+ * Base interface for all message objects.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.html#newMessage">opensocial.newMessage()</a>,
+ * <a href="opensocial.html#requestSendMessage">
+ * opensocial.requestSendMessage()</a>
+ *
+ * @name opensocial.Message
+ */
+
+
+/**
+ * Base interface for all message objects.
+ *
+ * @param {string} body_or_params The main text of the message.
+ * @param {Object.<opensocial.Message.Field, Object>=} opt_params Any other
+ *    fields that should be set on the message object. All of the defined
+ *    Fields are supported.
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.37.
+ */
+opensocial.Message = function(body_or_params, opt_params) {
+  if (typeof body_or_params == 'string') {
+    // We have a string
+    this.fields_ = opt_params || {};
+    this.fields_[opensocial.Message.Field.BODY] = body_or_params;
+  } else {
+    this.fields_ = body_or_params || {};
+  }
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that messages can have.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a
+ * href="opensocial.Message.html#getField">opensocial.Message.getField()</a>
+ * </p>
+ *
+ * @name opensocial.Message.Field
+ * @enum {string}
+ * @const
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.38.
+ */
+opensocial.Message.Field = {
+  /**
+   * The URL of the application that generated this message, if applicable.
+   * @member opensocial.Message.Field
+   */
+  APP_URL: 'appUrl',
+
+  /**
+   * The main text of the message. HTML attributes are allowed and are
+   * sanitized by the container.
+   * @member opensocial.Message.Field
+   * @const
+   */
+  BODY: 'body',
+
+  /**
+   * The main text of the message as a message template. Specifies the
+   * message ID to use in the gadget xml.
+   * @member opensocial.Message.Field
+   */
+  BODY_ID: 'bodyId',
+
+
+  /**
+   * Collection IDs this Message belongs to
+   * @member opensocial.Message.Field
+   */
+  COLLECTION_IDS: 'collectionIds',
+
+  /**
+   * The Unique ID of this message.
+   * @member opensocial.Message.Field
+   */
+  ID: 'id',
+
+  /**
+   * The Parent ID of this message.  Useful for message threading.
+   * @member opensocial.Message.Field
+   */
+  PARENT_ID: 'parentId',
+
+  /**
+   * The Recipients of this message.
+   * @member opensocial.Message.Field
+   */
+  RECIPIENTS: 'recipients',
+
+  /**
+   * The Person ID that sent this message.
+   * @member opensocial.Message.Field
+   */
+  SENDER_ID: 'senderId',
+
+  /**
+   * The Status of this message.  Specified as an opensocial.Message.Status.
+   */
+  STATUS: 'status',
+
+  /**
+   * The time this message was sent.
+   */
+  TIME_SENT: 'timeSent',
+
+  /**
+   * The title of the message. HTML attributes are allowed and are
+   * sanitized by the container.
+   * @member opensocial.Message.Field
+   */
+  TITLE: 'title',
+
+  /**
+   * The title of the message as a message template. Specifies the
+   * message ID to use in the gadget xml.
+   * @member opensocial.Message.Field
+   */
+  TITLE_ID: 'titleId',
+
+  /**
+   * The title of the message, specified as an opensocial.Message.Type.
+   * @member opensocial.Message.Field
+   */
+  TYPE: 'type',
+
+  /**
+   * The last updated time of this message.
+   * @member opensocial.Message.Field
+   */
+
+  UPDATED: 'updated',
+
+  /**
+   * Urls associated with this message, specified as an array of opensocial.Url
+   */
+  URLS: 'urls'
+};
+
+
+/**
+ * @static
+ * @class
+ * The types of messages that can be sent.
+ *
+ * @name opensocial.Message.Type
+ */
+opensocial.Message.Type = {
+  /**
+   * An email.
+   *
+   * @member opensocial.Message.Type
+   */
+  EMAIL: 'email',
+
+  /**
+   * A short private message.
+   *
+   * @member opensocial.Message.Type
+   */
+  NOTIFICATION: 'notification',
+
+  /**
+   * A message to a specific user that can be seen only by that user.
+   *
+   * @member opensocial.Message.Type
+   */
+  PRIVATE_MESSAGE: 'privateMessage',
+
+  /**
+   * A message to a specific user that can be seen by more than that user.
+   * @member opensocial.Message.Type
+   */
+  PUBLIC_MESSAGE: 'publicMessage'
+};
+
+/**
+ * @static
+ * @class
+ * The different status states of a message.
+ * @name opensocial.Message.Status
+ */
+
+opensocial.Message.Status = {
+  /**
+   * A new, unread message
+   * @member opensocial.Message.Status
+   */
+  NEW: 'new',
+
+  /**
+   * A deleted message
+   * @member opensocial.Message.Status
+   */
+  DELETED: 'deleted',
+
+  /**
+   * A flagged message
+   * @member opensocial.Message.Status
+   */
+  FLAGGED: 'flagged'
+};
+
+/**
+ * Gets the message data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *   see the <a href="opensocial.Message.Field.html">Field</a> class
+ * for possible values.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @member opensocial.Message
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.37.1.1.
+ */
+opensocial.Message.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this message associated with the given key.
+ *
+ * @param {string} key The key to set data for.
+ * @param {string} data The data to set.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.37.1.2.
+ */
+opensocial.Message.prototype.setField = function(key, data) {
+  return (this.fields_[key] = data);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/messagecollection.js b/trunk/features/src/main/javascript/features/opensocial-reference/messagecollection.js
new file mode 100644
index 0000000..a4a8592
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/messagecollection.js
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Representation of a message.
+ */
+
+
+/**
+ * @class
+ * Base interface for all message collection objects.
+ *
+ * <p>
+ *
+ * @name opensocial.MessageCollection
+ */
+
+
+/**
+ * Base interface for all message collection objects.
+ *
+ * @param {Object.<opensocial.MessageCollection.Field, Object>=} opt_params Any other
+ *    fields that should be set on the message object. All of the defined
+ *    Fields are supported.
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.39.
+ */
+opensocial.MessageCollection = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that message collections can have.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a
+ * href="opensocial.MessageCollection.html#getField">opensocial.MessageCollection.getField()</a>
+ * </p>
+ *
+ * @name opensocial.MessageCollection.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.40.
+ */
+opensocial.MessageCollection.Field = {
+  /**
+   * The Unique ID of this message collection.
+   * @member opensocial.MessageCollection.Field
+   */
+  ID: 'id',
+
+  /**
+   * The title of the message collection.
+   * @member opensocial.MessageCollection.Field
+   */
+  TITLE: 'title',
+
+  /**
+   * The total number of messages in this collection.
+   * @member opensocial.MessageCollection.Field
+   */
+  TOTAL: 'total',
+
+  /**
+   * The total number of unread messages in this collection
+   * @member opensocial.MessageCollection.Field
+   */
+  UNREAD: 'unread',
+
+  /**
+   * The updated timestamp for this collection
+   * @member opensocial.MessageCollection.Field
+   */
+  UPDATED: 'updated',
+
+  /**
+   * Urls associated with this collection
+   * @member opensocial.MessageCollection.Field
+   */
+  URLS: 'urls'
+};
+
+
+/**
+ * Gets the message data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *   see the <a href="opensocial.MessageCollection.Field.html">Field</a> class
+ * for possible values.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @member opensocial.MessageCollection
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.39.1.1.
+ */
+opensocial.MessageCollection.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this message associated with the given key.
+ *
+ * @param {string} key The key to set data for.
+ * @param {string} data The data to set.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.39.1.2.
+ */
+opensocial.MessageCollection.prototype.setField = function(key, data) {
+  return this.fields_[key] = data;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/name.js b/trunk/features/src/main/javascript/features/opensocial-reference/name.js
new file mode 100644
index 0000000..f04a4c7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/name.js
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of an name.
+ */
+
+
+/**
+ * @class
+ * Base interface for all name objects.
+ *
+ * @name opensocial.Name
+ */
+
+
+/**
+ * Base interface for all name objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.41.
+ */
+opensocial.Name = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that a name has. These are the supported keys for the
+ * <a href="opensocial.Name.html#getField">Name.getField()</a> method.
+ *
+ * @name opensocial.Name.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.42.
+ */
+opensocial.Name.Field = {
+  /**
+   * The family name. Specified as a String.
+   *
+   * @member opensocial.Name.Field
+   */
+  FAMILY_NAME: 'familyName',
+
+  /**
+   * The given name. Specified as a String.
+   *
+   * @member opensocial.Name.Field
+   */
+  GIVEN_NAME: 'givenName',
+
+  /**
+   * The additional name. Specified as a String.
+   *
+   * @member opensocial.Name.Field
+   */
+  ADDITIONAL_NAME: 'additionalName',
+
+  /**
+   * The honorific prefix. Specified as a String.
+   *
+   * @member opensocial.Name.Field
+   */
+  HONORIFIC_PREFIX: 'honorificPrefix',
+
+  /**
+   * The honorific suffix. Specified as a String.
+   *
+   * @member opensocial.Name.Field
+   */
+  HONORIFIC_SUFFIX: 'honorificSuffix',
+
+  /**
+   * The unstructured name. Specified as a String.
+   *
+   * @member opensocial.Name.Field
+   */
+  UNSTRUCTURED: 'unstructured'
+};
+
+
+/**
+ * Gets data for this name that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.Name.Field.html"><code>
+ *    Name.Field</code></a>.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.41.1.1.
+ */
+opensocial.Name.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/navigationparameters.js b/trunk/features/src/main/javascript/features/opensocial-reference/navigationparameters.js
new file mode 100644
index 0000000..71c9cb1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/navigationparameters.js
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of navigation parameters for RequestShareApp.
+ */
+
+
+/**
+ * @class
+ * Parameters used by RequestShareApp to instruct the container on where to go
+ * after the request is made.
+ *
+ *  It could be used, for example, to specify where viewers get routed
+ *  in one of two cases:
+ * 1) After a user gets a shareApp invitation or receives a message a gadget
+ *     developer should be able to send that user to a context sensitive place.
+ * 2) After a viewer actually shares an app with someone else the gadget
+ *     developer should be able to redirect the viewer to a context sensitive
+ *     place.
+ *
+ *
+ * @name opensocial.NavigationParameters
+ */
+
+
+/**
+ * Use this class to hold navigation parameters for RequestShareApp.
+ *
+ * For example, opensocial.newNavigationParameters({view : 'preview',
+ *                                                  owner: 'xx',
+ *                                                  parameters: {}).
+ *
+ * Private, see <a href="opensocial.html#newNavigationParameters">
+ *              opensocial.newNavigationParameters()</a> for usage.
+ *
+ * @private
+ * @constructor
+ */
+opensocial.NavigationParameters = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that NavigationParameters can have.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a
+ * href="opensocial.NavigationParameters.html#getField">
+ *         opensocial.NavigationParameters.getField()</a>
+ * </p>
+ *
+ * @name opensocial.NavigationParameters.Field
+ */
+opensocial.NavigationParameters.Field = {
+  /**
+   * The <a href="gadgets.views.View.html">View</a> to navigate to.
+   *
+   * @member opensocial.NavigationParameters.Field
+   */
+  VIEW: 'view',
+
+  /**
+   * A string representing the owner id.
+   *
+   * @member opensocial.NavigationParameters.Field
+   */
+  OWNER: 'owner',
+
+  /**
+   * An optional list of parameters passed to the gadget once the new view,
+   * with the new owner, has been loaded.
+   *
+   *
+   * @member opensocial.NavigationParameters.Field
+   */
+  PARAMETERS: 'parameters'
+};
+
+
+/**
+ * Gets the NavigationParameters' data that's associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *     see the <a href="opensocial.NavigationParameters.Field.html">Field</a>
+ *     class for possible values.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *     opt_params Additional
+ *     <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *     to pass to the request.
+ * @return {string} The data.
+ * @member opensocial.NavigationParameters
+ */
+opensocial.NavigationParameters.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Sets data for this NavigationParameters associated with the given key.
+ *
+ * @param {string} key The key to set data for.
+ * @param {Object} data The data to set.
+ */
+opensocial.NavigationParameters.prototype.setField = function(key, data) {
+  return (this.fields_[key] = data);
+};
+
+
+/**
+ * @static
+ * @class
+ *
+ * The destinations available for navigation in
+ * <a href="opensocial.html#requestShareApp">requestShareApp</a>
+ * and <a href="opensocial.html#requestSendMessage">requestSendMessage</a>.
+ *
+ * @name opensocial.NavigationParameters.DestinationType
+ */
+opensocial.NavigationParameters.DestinationType = {
+  /** @member opensocial.NavigationParameters.DestinationType */
+  VIEWER_DESTINATION: 'viewerDestination',
+
+  /** @member opensocial.NavigationParameters.DestinationType  */
+  RECIPIENT_DESTINATION: 'recipientDestination'
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/opensocial.js b/trunk/features/src/main/javascript/features/opensocial-reference/opensocial.js
new file mode 100644
index 0000000..a8bd0d6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/opensocial.js
@@ -0,0 +1,501 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Browser environment for interacting with people.
+ */
+
+
+/**
+ * @static
+ * @class
+ * Namespace for top-level people functions.
+ *
+ * @name opensocial
+ */
+
+/**
+ * Namespace for top level people functions.
+ *
+ * @private
+ * @constructor (note: a constructor for JsDoc purposes)
+ * @deprecated since 1.0 (see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1).
+ */
+var opensocial = opensocial || {};
+
+
+/**
+ * Requests the container to send a specific message to the specified users.
+ *
+ * <p>
+ * The callback function is passed one parameter, an
+ *    opensocial.ResponseItem. The error code will be set to reflect whether
+ *    there were any problems with the request. If there was no error, the
+ *    message was sent. If there was an error, you can use the response item's
+ *    getErrorCode method to determine how to proceed. The data on the response
+ *    item will not be set.
+ * </p>
+ *
+ * <p>
+ * If the container does not support this method
+ * the callback will be called with an
+ * opensocial.ResponseItem that has an error code of
+ * NOT_IMPLEMENTED.
+ * </p>
+ *
+ * @param {Array.<string> | string} recipients An ID, array of IDs, or a
+ *     group reference; the supported keys are VIEWER, OWNER, VIEWER_FRIENDS,
+ *    OWNER_FRIENDS, or a single ID within one of those groups.
+ * @param {opensocial.Message} message The message to send to the specified
+ *     users.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call once the request has been
+ *    processed; either this callback will be called or the gadget will be
+ *    reloaded from scratch.
+ * @param {opensocial.NavigationParameters=} opt_params The optional parameters
+ *     indicating where to send a user when a request is made, or when a request is
+ *     accepted; options are of type
+ *     <a href="opensocial.NavigationParameters.DestinationType.html">
+ *     NavigationParameters.DestinationType.</a>
+ *
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.9.
+ */
+opensocial.requestSendMessage = function(recipients, message, opt_callback,
+    opt_params) {
+  return opensocial.Container.get().requestSendMessage(recipients, message,
+      opt_callback, opt_params);
+};
+
+
+/**
+ * Requests the container to share this gadget with the specified users.
+ *
+ * <p>
+ * The callback function is passed one parameter, an
+ *    opensocial.ResponseItem. The error code will be set to reflect whether
+ *    there were any problems with the request. If there was no error, the
+ *    sharing request was sent. If there was an error, you can use the response
+ *    item's getErrorCode method to determine how to proceed. The data on the
+ *    response item will not be set.
+ * </p>
+ *
+ * <p>
+ * If the
+ * container does not support this method the callback will be called with a
+ * opensocial.ResponseItem. The response item will have its error code set to
+ * NOT_IMPLEMENTED.
+ * </p>
+ *
+ * @param {Array.<string> | string} recipients An ID, array of IDs, or a
+ *     group reference; the supported keys are VIEWER, OWNER, VIEWER_FRIENDS,
+ *    OWNER_FRIENDS, or a single ID within one of those groups.
+ * @param {opensocial.Message} reason The reason the user wants the gadget to
+ *     share itself. This reason can be used by the container when prompting the
+ *     user for permission to share the app. It may also be ignored.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call once the request has been
+ *    processed; either this callback will be called or the gadget will be
+ *    reloaded from scratch.
+ * @param {opensocial.NavigationParameters=} opt_params The optional parameters
+ *     indicating where to send a user when a request is made, or when a request is
+ *     accepted; options are of type
+ *     <a href="opensocial.NavigationParameters.DestinationType.html">
+ *     NavigationParameters.DestinationType.</a>
+ *
+ * @member opensocial
+ */
+opensocial.requestShareApp = function(recipients, reason, opt_callback,
+    opt_params) {
+  opensocial.Container.get().requestShareApp(recipients, reason, opt_callback,
+      opt_params);
+};
+
+
+/**
+ * Takes an activity and tries to create it,
+ * without waiting for the operation to complete.
+ * Optionally calls a function when the operation completes.
+ * <p>
+ * <b>See also:</b>
+ * <a href="#newActivity">newActivity()</a>
+ * </p>
+ *
+ * <p class="note">
+ * <b>Note:</b>
+ * If this is the first activity that has been created for the user and
+ * the request is marked as HIGH priority then this call may open a user flow
+ * and navigate away from your gadget.
+ *
+ * <p>
+ * This callback will either be called or the gadget will be
+ *    reloaded from scratch. This function will be passed one parameter, an
+ *    opensocial.ResponseItem. The error code will be set to reflect whether
+ *    there were any problems with the request. If there was no error, the
+ *    activity was created. If there was an error, you can use the response
+ *    item's getErrorCode method to determine how to proceed. The data on the
+ *    response item will not be set.
+ * </p>
+ *
+ * <p>
+ * If the container does not support this method the callback will be called
+ * with a opensocial.ResponseItem. The response item will have its error code
+ * set to NOT_IMPLEMENTED.
+ * </p>
+ *
+ * @param {opensocial.Activity} activity The <a href="opensocial.Activity.html">
+ *    activity</a> to create.
+ * @param {opensocial.CreateActivityPriority} priority The
+ *    <a href="opensocial.CreateActivityPriority.html">priority</a> for this
+ *    request.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call once the request has been
+ *    processed.
+ *
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.8.
+ */
+opensocial.requestCreateActivity = function(activity, priority, opt_callback) {
+  if (!activity || (!activity.getField(opensocial.Activity.Field.TITLE) && !activity.getField(opensocial.Activity.Field.TITLE_ID))) {
+    if (opt_callback) {
+      window.setTimeout(function() {
+        opt_callback(new opensocial.ResponseItem(null, null,
+            opensocial.ResponseItem.Error.BAD_REQUEST,
+            'You must pass in an activity with a title or title id.'));
+      }, 0);
+    }
+    return;
+  }
+
+  opensocial.Container.get().requestCreateActivity(activity, priority,
+      opt_callback);
+};
+
+
+/**
+ * @static
+ * @class
+ * The priorities a create activity request can have.
+ * <p><b>See also:</b>
+ * <a href="opensocial.html#requestCreateActivity">
+ * opensocial.requestCreateActivity()</a>
+ * </p>
+ *
+ * @name opensocial.CreateActivityPriority
+ * @enum {string}
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.11.
+ */
+opensocial.CreateActivityPriority = {
+  /**
+   * If the activity is of high importance, it will be created even if this
+   * requires asking the user for permission. This may cause the container to
+   * open a user flow which may navigate away from your gagdet.
+   *
+   * @member opensocial.CreateActivityPriority
+   */
+  HIGH: 'HIGH',
+
+  /**
+   * If the activity is of low importance, it will not be created if the
+   * user has not given permission for the current app to create activities.
+   * With this priority, the requestCreateActivity call will never open a user
+   * flow.
+   *
+   * @member opensocial.CreateActivityPriority
+   */
+  LOW: 'LOW'
+};
+
+
+/**
+ * Returns true if the current gadget has access to the specified
+ * permission. If the gadget calls opensocial.requestPermission and permissions
+ * are granted then this function must return true on all subsequent calls.
+ *
+ * @param {opensocial.Permission} permission
+ *    The <a href="opensocial.Permission.html">permission.</a>
+ * @return {boolean}
+ *    True if the gadget has access for the permission; false if it doesn't.
+ *
+ * @member opensocial
+ */
+opensocial.hasPermission = function(permission) {
+  return opensocial.Container.get().hasPermission(permission);
+};
+
+
+/**
+ * Requests the user to grant access to the specified permissions. If the
+ * container does not support this method the callback will be called with a
+ * opensocial.ResponseItem. The response item will have its error code set to
+ * NOT_IMPLEMENTED.
+ *
+ * @param {Array.<opensocial.Permission>} permissions
+ *    The <a href="opensocial.Permission.html">permissions</a> to request
+ *    from the viewer.
+ * @param {string} reason Displayed to the user as the reason why these
+ *    permissions are needed.
+ * @param {function(opensocial.ResponseItem)=} opt_callback The function to call once the request has been
+ *    processed; either this callback will be called or the gadget will be
+ *    reloaded from scratch. This function will be passed one parameter, an
+ *    opensocial.ResponseItem. The error code will be set to reflect whether
+ *    there were any problems with the request. If there was no error, all
+ *    permissions were granted. If there was an error, you can use
+ *    opensocial.hasPermission to check which permissions are still denied. The
+ *    data on the response item will be set. It will be an array of the
+ *    opensocial.Permissions that were granted.
+ *
+ * @member opensocial
+ */
+opensocial.requestPermission = function(permissions, reason, opt_callback) {
+  opensocial.Container.get().requestPermission(permissions, reason,
+      opt_callback);
+};
+
+
+/**
+ * @static
+ * @class
+ *
+ * The permissions an app can ask for.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="opensocial.html#hasPermission">
+ * <code>opensocial.hasPermission()</code></a>,
+ * <a href="opensocial.html#requestPermission">
+ * <code>opensocial.requestPermission()</code></a>
+ *
+ * @name opensocial.Permission
+ */
+opensocial.Permission = {
+  /**
+   * Access to the viewer person object
+   *
+   * @member opensocial.Permission
+   */
+  VIEWER: 'viewer'
+};
+
+
+/**
+ * Gets the current environment for this gadget. You can use the environment to
+ * make queries such as what profile fields and surfaces are supported by this
+ * container, what parameters were passed to the current gadget, and so on.
+ *
+ * @return {opensocial.Environment}
+ *    The current <a href="opensocial.Environment.html">environment.</a>
+ *
+ * @member opensocial
+ */
+opensocial.getEnvironment = function() {
+  return opensocial.Container.get().getEnvironment();
+};
+
+
+/**
+ * Creates a data request object to use for sending and fetching data from the
+ * server.
+ *
+ * @return {opensocial.DataRequest} The
+ *    <a href="opensocial.DataRequest.html">request</a> object.
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.4.
+ */
+opensocial.newDataRequest = function() {
+  return opensocial.Container.get().newDataRequest();
+};
+
+
+/**
+ * Creates an activity object,
+ * which represents an activity on the server.
+ * <p>
+ * <b>See also:</b>
+ * <a href="#requestCreateActivity">requestCreateActivity()</a>,
+ * </p>
+ *
+ * <p>It is only required to set one of TITLE_ID or TITLE. In addition, if you
+ * are using any variables in your title or title template,
+ * you must set TEMPLATE_PARAMS.</p>
+ *
+ * <p>Other possible fields to set are: URL, MEDIA_ITEMS, BODY_ID, BODY,
+ * EXTERNAL_ID, PRIORITY, STREAM_TITLE, STREAM_URL, STREAM_SOURCE_URL,
+ * and STREAM_FAVICON_URL.</p>
+ *
+ * <p>Containers are only required to use TITLE_ID or TITLE, and may choose to
+ * ignore additional parameters.</p>
+ *
+ * <p>See <a href="opensocial.Activity.Field.html">Field</a>s are supported for
+ * more details.</p>
+ *
+ * @param {Object.<opensocial.Activity.Field, Object>} params
+ *    Parameters defining the activity.
+ * @return {opensocial.Activity} The new
+ *    <a href="opensocial.Activity.html">activity</a> object.
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.2.
+ */
+opensocial.newActivity = function(params) {
+  return opensocial.Container.get().newActivity(params);
+};
+
+/**
+ * Creates an album.
+ * Represents a collection of images, movies, and audio.
+ * Used when creating albums on the server.
+ *
+ * @param {Object.<opensocial.MediaItem.Field, Object>=} opt_params
+ *    Any other fields that should be set on the album object;
+ *    all of the defined
+ *    <a href="opensocial.Album.Field.html">Field</a>s
+ *    are supported.
+ *
+ * @return {opensocial.Album} The new
+ *    <a href="opensocial.Album.html">album</a> object.
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.3.
+ */
+opensocial.newAlbum = function(opt_params) {
+  return opensocial.Container.get().newAlbum(opt_params);
+};
+
+
+/**
+ * Creates a media item.
+ * Represents images, movies, and audio.
+ * Used when creating activities on the server.
+ *
+ * @param {string} mimeType
+ *    <a href="opensocial.MediaItem.Type.html">MIME type</a> of the
+ *    media.
+ * @param {string} url Where the media can be found.
+ * @param {Object.<opensocial.MediaItem.Field, Object>=} opt_params
+ *    Any other fields that should be set on the media item object;
+ *    all of the defined
+ *    <a href="opensocial.MediaItem.Field.html">Field</a>s
+ *    are supported.
+ *
+ * @return {opensocial.MediaItem} The new
+ *    <a href="opensocial.MediaItem.html">media item</a> object.
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.6.
+ */
+opensocial.newMediaItem = function(mimeType, url, opt_params) {
+  return opensocial.Container.get().newMediaItem(mimeType, url, opt_params);
+};
+
+
+/**
+ * Creates a media item associated with an activity.
+ * Represents images, movies, and audio.
+ * Used when creating activities on the server.
+ *
+ * @param {string} body The main text of the message.
+ * @param {Object.<opensocial.Message.Field, Object>=} opt_params
+ *    Any other fields that should be set on the message object;
+ *    all of the defined
+ *    <a href="opensocial.Message.Field.html">Field</a>s
+ *    are supported.
+ *
+ * @return {opensocial.Message} The new
+ *    <a href="opensocial.Message.html">message</a> object.
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.7.
+ */
+opensocial.newMessage = function(body, opt_params) {
+  return opensocial.Container.get().newMessage(body, opt_params);
+};
+
+
+/**
+ * @static
+ * @class
+ * The types of escaping that can be applied to person data or fields.
+ *
+ * @name opensocial.EscapeType
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.29.
+ */
+opensocial.EscapeType = {
+  /**
+   * When used will HTML-escape the data.
+   * @member opensocial.EscapeType
+   */
+  HTML_ESCAPE: 'htmlEscape',
+  /**
+   * When used will not escape the data.
+   *
+   * @member opensocial.EscapeType
+   */
+  NONE: 'none'
+};
+
+
+/**
+ * Creates an IdSpec object.
+ *
+ * @param {Object.<opensocial.IdSpec.Field, Object>} params
+ *    Parameters defining the id spec.
+ * @return {opensocial.IdSpec} The new
+ *     <a href="opensocial.IdSpec.html">IdSpec</a> object.
+ * @member opensocial
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.5.
+ */
+opensocial.newIdSpec = function(params) {
+  return opensocial.Container.get().newIdSpec(params);
+};
+
+
+/**
+ * Creates a NavigationParameters object.
+ * <p>
+ * <b>See also:</b>
+ * <a href="#requestShareApp">requestShareApp()</a>
+ * </p>
+ *
+ *
+ * @param {Object.<opensocial.NavigationParameters.Field, Object>} params
+ *     Parameters defining the navigation.
+ * @return {opensocial.NavigationParameters} The new
+ *     <a href="opensocial.NavigationParameters.html">NavigationParameters</a>
+ *     object.
+ * @member opensocial
+ */
+opensocial.newNavigationParameters = function(params) {
+  return opensocial.Container.get().newNavigationParameters(params);
+};
+
+
+/**
+ * Invalidates all resources cached for the current viewer.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.1.1.1.
+ */
+opensocial.invalidateCache = function() {
+  opensocial.Container.get().invalidateCache();
+};
+
+
+// TODO(doll): Util function - pull up the gadgets inherits in shindig so that
+// opensocial and gadgets use the same one
+/** @private */
+Function.prototype.inherits = function(parentCtor) {
+  function tempCtor() {}
+
+  tempCtor.prototype = parentCtor.prototype;
+  this.superClass_ = parentCtor.prototype;
+  this.prototype = new tempCtor();
+  this.prototype.constructor = this;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/organization.js b/trunk/features/src/main/javascript/features/opensocial-reference/organization.js
new file mode 100644
index 0000000..e6e1fbf
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/organization.js
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of a organization.
+ */
+
+
+/**
+ * @class
+ * Base interface for all organization objects.
+ *
+ * @name opensocial.Organization
+ */
+
+
+/**
+ * Base interface for all organization objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.43.
+ */
+opensocial.Organization = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that a organization has. These are the supported keys for
+ * the <a href="opensocial.Organization.html#getField">
+ * Organization.getField()</a> method.
+ *
+ * @name opensocial.Organization.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.44.
+ */
+opensocial.Organization.Field = {
+  /**
+   * The name of the organization. For example, could be a school name or a job
+   * company. Specified as a string.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  NAME: 'name',
+
+  /**
+   * The title or role the person has in the organization, specified as a
+   * string. This could be graduate student, or software engineer.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  TITLE: 'title',
+
+  /**
+   * A description or notes about the person's work in the organization,
+   * specified as a string. This could be the courses taken by a student, or a
+   * more detailed description about a Organization role.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  DESCRIPTION: 'description',
+
+  /**
+   * The field the organization is in, specified as a string. This could be the
+   * degree pursued if the organization is a school.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  FIELD: 'field',
+
+  /**
+   * The subfield the Organization is in, specified as a string.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  SUB_FIELD: 'subField',
+
+  /**
+   * The date the person started at the organization, specified as a Date.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  START_DATE: 'startDate',
+
+  /**
+   * The date the person stopped at the organization, specified as a Date.
+   * A null date indicates that the person is still involved with the
+   * organization.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  END_DATE: 'endDate',
+
+  /**
+   * The salary the person receieves from the organization, specified as a
+   * string.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  SALARY: 'salary',
+
+  /**
+   * The address of the organization, specified as an opensocial.Address.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  ADDRESS: 'address',
+
+  /**
+   * A webpage related to the organization, specified as a string.
+   * Not supported by all containers.
+   * @member opensocial.Organization.Field
+   */
+  WEBPAGE: 'webpage'
+};
+
+
+/**
+ * Gets data for this body type that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.Organization.Field.html"><code>
+ *    Organization.Field</code></a>.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.43.1.1.
+ */
+opensocial.Organization.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/person.js b/trunk/features/src/main/javascript/features/opensocial-reference/person.js
new file mode 100644
index 0000000..829ab96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/person.js
@@ -0,0 +1,596 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of a person.
+ */
+
+
+/**
+ * @class
+ * Base interface for all person objects.
+ *
+ * @name opensocial.Person
+ */
+
+
+/**
+ * Base interface for all person objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.45.
+ */
+opensocial.Person = function(opt_params, opt_isOwner, opt_isViewer) {
+  this.fields_ = opt_params || {};
+  this.isOwner_ = opt_isOwner;
+  this.isViewer_ = opt_isViewer;
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that a person has. These are the supported keys for the
+ * <a href="opensocial.Person.html#getField">Person.getField()</a> method.
+ *
+ * @name opensocial.Person.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.46.
+ */
+opensocial.Person.Field = {
+  /**
+   * A string ID that can be permanently associated with this person.
+   * @member opensocial.Person.Field
+   */
+  ID: 'id',
+
+  /**
+   * A opensocial.Name object containing the person's name.
+   * @member opensocial.Person.Field
+   */
+  NAME: 'name',
+
+  /**
+   * A String representing the person's nickname.
+   * @member opensocial.Person.Field
+   */
+  NICKNAME: 'nickname',
+
+  /**
+   * Person's photo thumbnail URL, specified as a string.
+   * This URL must be fully qualified. Relative URLs will not work in gadgets.
+   * @member opensocial.Person.Field
+   */
+  THUMBNAIL_URL: 'thumbnailUrl',
+
+  /**
+   * Person's profile URL, specified as a string.
+   * This URL must be fully qualified. Relative URLs will not work in gadgets.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  PROFILE_URL: 'profileUrl',
+
+  /**
+   * Person's current location, specified as an
+   * <a href="opensocial.Address.html">Address</a>.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  CURRENT_LOCATION: 'currentLocation',
+
+  /**
+   * Addresses associated with the person, specified as an Array of
+   * <a href="opensocial.Address.html">Address</a>es.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  ADDRESSES: 'addresses',
+
+  /**
+   * Emails associated with the person, specified as an Array of
+   * <a href="opensocial.Email.html">Email</a>s.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  EMAILS: 'emails',
+
+  /**
+   * Phone numbers associated with the person, specified as an Array of
+   * <a href="opensocial.Phone.html">Phone</a>s.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  PHONE_NUMBERS: 'phoneNumbers',
+
+  /**
+   * A general statement about the person, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  ABOUT_ME: 'aboutMe',
+
+  /**
+   * Person's status, headline or shoutout, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  STATUS: 'status',
+
+  /**
+   * Person's profile song, specified as an opensocial.Url.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  PROFILE_SONG: 'profileSong',
+
+  /**
+   * Person's profile video, specified as an opensocial.Url.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  PROFILE_VIDEO: 'profileVideo',
+
+  /**
+   * Person's gender, specified as an opensocial.Enum with the enum's
+   * key referencing opensocial.Enum.Gender.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  GENDER: 'gender',
+
+  /**
+   * Person's sexual orientation, specified as a string.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  SEXUAL_ORIENTATION: 'sexualOrientation',
+
+  /**
+   * Person's relationship status, specified as a string.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  RELATIONSHIP_STATUS: 'relationshipStatus',
+
+  /**
+   * Person's age, specified as a number.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  AGE: 'age',
+
+  /**
+   * Person's date of birth, specified as a Date object.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  DATE_OF_BIRTH: 'dateOfBirth',
+
+  /**
+   * Person's body characteristics, specified as an opensocial.BodyType.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  BODY_TYPE: 'bodyType',
+
+  /**
+   * Person's ethnicity, specified as a string.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  ETHNICITY: 'ethnicity',
+
+  /**
+   * Person's smoking status, specified as an opensocial.Enum with the enum's
+   * key referencing opensocial.Enum.Smoker.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  SMOKER: 'smoker',
+
+  /**
+   * Person's drinking status, specified as an opensocial.Enum with the enum's
+   * key referencing opensocial.Enum.Drinker.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  DRINKER: 'drinker',
+
+  /**
+   * Description of the person's children, specified as a string.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  CHILDREN: 'children',
+
+  /**
+   * Description of the person's pets, specified as a string.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  PETS: 'pets',
+
+  /**
+   * Description of the person's living arrangement, specified as a string.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  LIVING_ARRANGEMENT: 'livingArrangement',
+
+  /**
+   * Person's time zone, specified as the difference in minutes between
+   * Greenwich Mean Time (GMT) and the user's local time. See
+   * Date.getTimezoneOffset() in javascript for more details on this format.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  TIME_ZONE: 'timeZone',
+
+  /**
+   * List of the languages that the person speaks as ISO 639-1 codes,
+   * specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   * @member opensocial.Person.Field
+   */
+  LANGUAGES_SPOKEN: 'languagesSpoken',
+
+  /**
+   * Jobs the person has held, specified as an Array of
+   * <a href="opensocial.Organization.html">Organization</a>s.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  JOBS: 'jobs',
+
+  /**
+   * Person's favorite jobs, or job interests and skills, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  JOB_INTERESTS: 'jobInterests',
+
+  /**
+   * Schools the person has attended, specified as an Array of
+   * <a href="opensocial.Organization.html">Organization</a>s.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  SCHOOLS: 'schools',
+
+  /**
+   * Person's interests, hobbies or passions, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  INTERESTS: 'interests',
+
+  /**
+   * URLs related to the person, their webpages, or feeds. Specified as an
+   * Array of opensocial.Url.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  URLS: 'urls',
+
+  /**
+   * Person's favorite music, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  MUSIC: 'music',
+
+  /**
+   * Person's favorite movies, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  MOVIES: 'movies',
+
+  /**
+   * Person's favorite TV shows, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  TV_SHOWS: 'tvShows',
+
+  /**
+   * Person's favorite books, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  BOOKS: 'books',
+
+  /**
+   * Person's favorite activities, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  ACTIVITIES: 'activities',
+
+  /**
+   * Person's favorite sports, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  SPORTS: 'sports',
+
+  /**
+   * Person's favorite heroes, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  HEROES: 'heroes',
+
+  /**
+   * Person's favorite quotes, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  QUOTES: 'quotes',
+
+  /**
+   * Person's favorite cars, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  CARS: 'cars',
+
+  /**
+   * Person's favorite food, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  FOOD: 'food',
+
+  /**
+   * Person's turn ons, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  TURN_ONS: 'turnOns',
+
+  /**
+   * Person's turn offs, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  TURN_OFFS: 'turnOffs',
+
+  /**
+   * Arbitrary tags about the person, specified as an Array of strings.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  TAGS: 'tags',
+
+  /**
+   * Person's comments about romance, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  ROMANCE: 'romance',
+
+  /**
+   * What the person is scared of, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  SCARED_OF: 'scaredOf',
+
+  /**
+   * Describes when the person is happiest, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  HAPPIEST_WHEN: 'happiestWhen',
+
+  /**
+   * Person's thoughts on fashion, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  FASHION: 'fashion',
+
+  /**
+   * Person's thoughts on humor, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  HUMOR: 'humor',
+
+  /**
+   * Person's statement about who or what they are looking for, or what they are
+   * interested in meeting people for. Specified as an Array of opensocial.Enum
+   * with the enum's key referencing opensocial.Enum.LookingFor.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  LOOKING_FOR: 'lookingFor',
+
+  /**
+   * Person's relgion or religious views, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  RELIGION: 'religion',
+
+  /**
+   * Person's political views, specified as a string.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  POLITICAL_VIEWS: 'politicalViews',
+
+  /**
+   * A boolean indicating whether the person has used the current app.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  HAS_APP: 'hasApp',
+
+  /**
+   * Person's current network status. Specified as an Enum with the enum's
+   * key referencing opensocial.Enum.Presence.
+   * Container support for this field is OPTIONAL.
+   *
+   * @member opensocial.Person.Field
+   */
+  NETWORK_PRESENCE: 'networkPresence'
+};
+
+
+/**
+ * Gets an ID that can be permanently associated with this person.
+ *
+ * @return {string} The ID.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.45.1.4.
+ */
+opensocial.Person.prototype.getId = function() {
+  return this.getField(opensocial.Person.Field.ID);
+};
+
+
+var ORDERED_NAME_FIELDS_ = [
+  opensocial.Name.Field.HONORIFIC_PREFIX,
+  opensocial.Name.Field.GIVEN_NAME,
+  opensocial.Name.Field.FAMILY_NAME,
+  opensocial.Name.Field.HONORIFIC_SUFFIX,
+  opensocial.Name.Field.ADDITIONAL_NAME];
+
+/**
+ * Gets a text display name for this person; guaranteed to return
+ * a useful string.
+ *
+ * @return {string} The display name.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.45.1.2.
+ */
+opensocial.Person.prototype.getDisplayName = function() {
+  var name = this.getField(opensocial.Person.Field.NAME);
+  if (name) {
+    // Try unstructured field first
+    var unstructured = name.getField(opensocial.Name.Field.UNSTRUCTURED);
+    if (unstructured) {
+      return unstructured;
+    }
+
+    // Next try to construct the name from the individual components
+    var fullName = '';
+    for (var i = 0; i < ORDERED_NAME_FIELDS_.length; i++) {
+      var nameValue = name.getField(ORDERED_NAME_FIELDS_[i]);
+      if (nameValue) {
+        fullName += nameValue + ' ';
+      }
+    }
+    return fullName.replace(/^\s+|\s+$/g, '');
+  }
+
+  // Finally, try the nickname field
+  return this.getField(opensocial.Person.Field.NICKNAME);
+};
+
+
+/**
+ * Gets data for this person that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.Person.Field.html"><code>
+ *    Person.Field</code></a>.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>=}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.45.1.3.
+ */
+opensocial.Person.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
+
+
+/**
+ * Gets the app data for this person that is associated with the specified
+ * key.
+ *
+ * @param {string} key The key to get app data for.
+ * @return {string} The corresponding app data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.45.1.1.
+ */
+opensocial.Person.prototype.getAppData = function(key) {
+};
+
+
+/**
+ * Returns true if this person object represents the currently logged in user.
+ *
+ * @return {boolean} True if this is the currently logged in user;
+ *   otherwise, false.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.45.1.6.
+ */
+opensocial.Person.prototype.isViewer = function() {
+  return !!this.isViewer_;
+};
+
+
+/**
+ * Returns true if this person object represents the owner of the current page.
+ *
+ * @return {boolean} True if this is the owner of the page;
+ *   otherwise, false.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.45.1.5.
+ */
+opensocial.Person.prototype.isOwner = function() {
+  return !!this.isOwner_;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/phone.js b/trunk/features/src/main/javascript/features/opensocial-reference/phone.js
new file mode 100644
index 0000000..46eeb87
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/phone.js
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of an phone number.
+ */
+
+
+/**
+ * @class
+ * Base interface for all phone objects.
+ *
+ * @name opensocial.Phone
+ */
+
+
+/**
+ * Base interface for all phone objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.47.
+ */
+opensocial.Phone = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that a phone has. These are the supported keys for the
+ * <a href="opensocial.Phone.html#getField">Phone.getField()</a> method.
+ *
+ * @name opensocial.Phone.Field
+ * @enum {string}
+ * @const
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.48.
+ */
+opensocial.Phone.Field = {
+  /**
+   * The phone number type or label, specified as a String.
+   * Examples: work, my favorite store, my house, etc.
+   *
+   * @member opensocial.Phone.Field
+   */
+  TYPE: 'type',
+
+  /**
+   * The phone number, specified as a String.
+   *
+   * @member opensocial.Phone.Field
+   */
+  NUMBER: 'number'
+};
+
+
+/**
+ * Gets data for this phone that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.Phone.Field.html"><code>
+ *    Phone.Field</code></a>.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.47.1.1.
+ */
+opensocial.Phone.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/responseitem.js b/trunk/features/src/main/javascript/features/opensocial-reference/responseitem.js
new file mode 100644
index 0000000..5744239
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/responseitem.js
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview ResponseItem containing information about a specific response
+ * from the server.
+ */
+
+
+/**
+ * @class
+ * Represents a response that was generated
+ * by processing a data request item on the server.
+ *
+ * @name opensocial.ResponseItem
+ */
+
+
+/**
+ * Represents a response that was generated by processing a data request item
+ * on the server.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.49.
+ */
+opensocial.ResponseItem = function(originalDataRequest, data,
+    opt_errorCode, opt_errorMessage) {
+  this.originalDataRequest_ = originalDataRequest;
+  this.data_ = data;
+  this.errorCode_ = opt_errorCode;
+  this.errorMessage_ = opt_errorMessage;
+};
+
+
+/**
+ * Returns true if there was an error in fetching this data from the server.
+ *
+ * @return {boolean} True if there was an error; otherwise, false.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.49.1.5.
+ */
+opensocial.ResponseItem.prototype.hadError = function() {
+  return !!this.errorCode_;
+};
+
+
+/**
+ * @static
+ * @class
+ *
+ * Error codes that a response item can return.
+ *
+ * @name opensocial.ResponseItem.Error
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.50.
+ */
+opensocial.ResponseItem.Error = {
+  /**
+   * This container does not support the request that was made.
+   *
+   * @member opensocial.ResponseItem.Error
+   */
+  NOT_IMPLEMENTED: 'notImplemented',
+
+  /**
+   * The gadget does not have access to the requested data.
+   * To get access, use
+   * <a href="opensocial.html#requestPermission">
+   * opensocial.requestPermission()</a>.
+   *
+   * @member opensocial.ResponseItem.Error
+   */
+  UNAUTHORIZED: 'unauthorized',
+
+  /**
+   * The gadget can never have access to the requested data.
+   *
+   * @member opensocial.ResponseItem.Error
+   */
+  FORBIDDEN: 'forbidden',
+
+  /**
+   * The request was invalid. Example: 'max' was -1.
+   *
+   * @member opensocial.ResponseItem.Error
+   */
+  BAD_REQUEST: 'badRequest',
+
+  /**
+   * The request encountered an unexpected condition that
+   * prevented it from fulfilling the request.
+   *
+   * @member opensocial.ResponseItem.Error
+   */
+  INTERNAL_ERROR: 'internalError',
+
+  /**
+   * The gadget exceeded a quota on the request. Example quotas include a
+   * max number of calls per day, calls per user per day, calls within a
+   * certain time period and so forth.
+   *
+   * @member opensocial.ResponseItem.Error
+   */
+  LIMIT_EXCEEDED: 'limitExceeded'
+};
+
+
+/**
+ * If the request had an error, returns the error code.
+ * The error code can be container-specific
+ * or one of the values defined by
+ * <a href="opensocial.ResponseItem.Error.html"><code>Error</code></a>.
+ *
+ * @return {string} The error code, or null if no error occurred.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.49.1.2.
+ */
+opensocial.ResponseItem.prototype.getErrorCode = function() {
+  return this.errorCode_;
+};
+
+
+/**
+ * If the request had an error, returns the error message.
+ *
+ * @return {string} A human-readable description of the error that occurred;
+ *    can be null, even if an error occurred.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.49.1.3.
+ */
+opensocial.ResponseItem.prototype.getErrorMessage = function() {
+  return this.errorMessage_;
+};
+
+
+/**
+ * Returns the original data request.
+ *
+ * @return {opensocial.DataRequest} The data request used to fetch this data
+ *    response.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.49.1.4.
+ */
+opensocial.ResponseItem.prototype.getOriginalDataRequest = function() {
+  return this.originalDataRequest_;
+};
+
+
+/**
+ * Gets the response data.
+ *
+ * @return {Object} The requested value calculated by the server; the type of
+ *    this value is defined by the type of request that was made.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.49.1.1.
+ */
+opensocial.ResponseItem.prototype.getData = function() {
+  return this.data_;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/taming.js b/trunk/features/src/main/javascript/features/opensocial-reference/taming.js
new file mode 100644
index 0000000..eb8dbec
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/taming.js
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose opensocial.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistCtors([
+    [window, 'JsonRpcRequestItem', Object],
+    [opensocial, 'Activity', Object],
+    [opensocial, 'Address', Object],
+    [opensocial, 'Album', Object],
+    [opensocial, 'BodyType', Object],
+    [opensocial, 'Container', Object],
+    [opensocial, 'Collection', Object],
+    [opensocial, 'DataRequest', Object],
+    [opensocial, 'DataResponse', Object],
+    [opensocial, 'Email', Object],
+    [opensocial, 'Enum', Object],
+    [opensocial, 'Environment', Object],
+    [opensocial, 'IdSpec', Object],
+    [opensocial, 'MediaItem', Object],
+    [opensocial, 'Message', Object],
+    [opensocial, 'MessageCollection', Object],
+    [opensocial, 'Name', Object],
+    [opensocial, 'NavigationParameters', Object],
+    [opensocial, 'Organization', Object],
+    [opensocial, 'Person', Object],
+    [opensocial, 'Phone', Object],
+    [opensocial, 'ResponseItem', Object],
+    [opensocial, 'Url', Object]
+  ]);
+
+  caja___.whitelistMeths([
+    [opensocial.Activity, 'getField'],
+    [opensocial.Activity, 'getId'],
+    [opensocial.Activity, 'setField'],
+    [opensocial.Address, 'getField'],
+    [opensocial.Album, 'getField'],
+    [opensocial.Album, 'setField'],
+    [opensocial.BodyType, 'getField'],
+    [opensocial.Container, 'getEnvironment'],
+    [opensocial.Container, 'requestSendMessage'],
+    [opensocial.Container, 'requestShareApp'],
+    [opensocial.Container, 'requestCreateActivity'],
+    [opensocial.Container, 'hasPermission'],
+    [opensocial.Container, 'requestPermission'],
+    [opensocial.Container, 'requestData'],
+    [opensocial.Container, 'newCreateAlbumRequest'],
+    [opensocial.Container, 'newCreateMediaItemRequest'],
+    [opensocial.Container, 'newDeleteAlbumRequest'],
+    [opensocial.Container, 'newFetchPersonRequest'],
+    [opensocial.Container, 'newFetchPeopleRequest'],
+    [opensocial.Container, 'newFetchPersonAppDataRequest'],
+    [opensocial.Container, 'newUpdatePersonAppDataRequest'],
+    [opensocial.Container, 'newRemovePersonAppDataRequest'],
+    [opensocial.Container, 'newUpdateAlbumRequest'],
+    [opensocial.Container, 'newUpdateMediaItemRequest'],
+    [opensocial.Container, 'newFetchActivitiesRequest'],
+    [opensocial.Container, 'newFetchAlbumsRequest'],
+    [opensocial.Container, 'newFetchMediaItemsRequest'],
+    [opensocial.Container, 'newFetchMessageCollectionsRequest'],
+    [opensocial.Container, 'newFetchMessagesRequest'],
+    [opensocial.Container, 'newCollection'],
+    [opensocial.Container, 'newPerson'],
+    [opensocial.Container, 'newActivity'],
+    [opensocial.Container, 'newAlbum'],
+    [opensocial.Container, 'newMediaItem'],
+    [opensocial.Container, 'newMessage'],
+    [opensocial.Container, 'newIdSpec'],
+    [opensocial.Container, 'newNavigationParameters'],
+    [opensocial.Container, 'newResponseItem'],
+    [opensocial.Container, 'newDataResponse'],
+    [opensocial.Container, 'newDataRequest'],
+    [opensocial.Container, 'newEnvironment'],
+    [opensocial.Container, 'invalidateCache'],
+    [opensocial.Collection, 'asArray'],
+    [opensocial.Collection, 'each'],
+    [opensocial.Collection, 'getById'],
+    [opensocial.Collection, 'getOffset'],
+    [opensocial.Collection, 'getTotalSize'],
+    [opensocial.Collection, 'size'],
+    [opensocial.DataRequest, 'add'],
+    [opensocial.DataRequest, 'newCreateAlbumRequest'],
+    [opensocial.DataRequest, 'newCreateMediaItemRequest'],
+    [opensocial.DataRequest, 'newDeleteAlbumRequest'],
+    [opensocial.DataRequest, 'newFetchActivitiesRequest'],
+    [opensocial.DataRequest, 'newFetchAlbumsRequest'],
+    [opensocial.DataRequest, 'newFetchMediaItemsRequest'],
+    [opensocial.DataRequest, 'newFetchPeopleRequest'],
+    [opensocial.DataRequest, 'newFetchPersonAppDataRequest'],
+    [opensocial.DataRequest, 'newUpdateAlbumRequest'],
+    [opensocial.DataRequest, 'newUpdateMediaItemRequest'],
+    [opensocial.DataRequest, 'newFetchPersonRequest'],
+    [opensocial.DataRequest, 'newRemovePersonAppDataRequest'],
+    [opensocial.DataRequest, 'newUpdatePersonAppDataRequest'],
+    [opensocial.DataRequest, 'send'],
+    [opensocial.DataResponse, 'get'],
+    [opensocial.DataResponse, 'getErrorMessage'],
+    [opensocial.DataResponse, 'hadError'],
+    [opensocial.Email, 'getField'],
+    [opensocial.Enum, 'getDisplayValue'],
+    [opensocial.Enum, 'getKey'],
+    [opensocial.Environment, 'getDomain'],
+    [opensocial.Environment, 'supportsField'],
+    [opensocial.IdSpec, 'getField'],
+    [opensocial.IdSpec, 'setField'],
+    [opensocial.MediaItem, 'getField'],
+    [opensocial.MediaItem, 'setField'],
+    [opensocial.Message, 'getField'],
+    [opensocial.Message, 'setField'],
+    [opensocial.Name, 'getField'],
+    [opensocial.NavigationParameters, 'getField'],
+    [opensocial.NavigationParameters, 'setField'],
+    [opensocial.Organization, 'getField'],
+    [opensocial.Person, 'getDisplayName'],
+    [opensocial.Person, 'getField'],
+    [opensocial.Person, 'getId'],
+    [opensocial.Person, 'isOwner'],
+    [opensocial.Person, 'isViewer'],
+    [opensocial.Phone, 'getField'],
+    [opensocial.ResponseItem, 'getData'],
+    [opensocial.ResponseItem, 'getErrorCode'],
+    [opensocial.ResponseItem, 'getErrorMessage'],
+    [opensocial.ResponseItem, 'getOriginalDataRequest'],
+    [opensocial.ResponseItem, 'hadError'],
+    [opensocial.Url, 'getField']
+  ]);
+  caja___.whitelistFuncs([
+    [opensocial.Container, 'setContainer'],
+    [opensocial.Container, 'get'],
+    [opensocial.Container, 'getField'],
+    [opensocial, 'getEnvironment'],
+    [opensocial, 'hasPermission'],
+    [opensocial, 'newActivity'],
+    [opensocial, 'newAlbum'],
+    [opensocial, 'newDataRequest'],
+    [opensocial, 'newIdSpec'],
+    [opensocial, 'newMediaItem'],
+    [opensocial, 'newMessage'],
+    [opensocial, 'newNavigationParameters'],
+    [opensocial, 'requestCreateActivity'],
+    [opensocial, 'requestPermission'],
+    [opensocial, 'requestSendMessage'],
+    [opensocial, 'requestShareApp']
+  ]);
+
+  caja___.whitelistProps([
+    [opensocial, 'CreateActivityPriority'],
+    [opensocial, 'EscapeType'],
+    [opensocial.Activity, 'Field'],
+    [opensocial.Address, 'Field'],
+    [opensocial.Album, 'Field'],
+    [opensocial.BodyType, 'Field'],
+    [opensocial.DataRequest, 'ActivityRequestFields'],
+    [opensocial.DataRequest, 'DataRequestFields'],
+    [opensocial.DataRequest, 'FilterType'],
+    [opensocial.DataRequest, 'Group'],
+    [opensocial.DataRequest, 'PeopleRequestFields'],
+    [opensocial.DataRequest, 'SortOrder'],
+    [opensocial.Email, 'Field'],
+    [opensocial.Enum, 'Smoker'],
+    [opensocial.Enum, 'Drinker'],
+    [opensocial.Enum, 'Gender'],
+    [opensocial.Enum, 'LookingFor'],
+    [opensocial.Enum, 'Presence'],
+    [opensocial.Environment, 'ObjectType'],
+    [opensocial.IdSpec, 'Field'],
+    [opensocial.IdSpec, 'GroupId'],
+    [opensocial.IdSpec, 'PersonId'],
+    [opensocial.MediaItem, 'Field'],
+    [opensocial.MediaItem, 'Type'],
+    [opensocial.Message, 'Field'],
+    [opensocial.Message, 'Type'],
+    [opensocial.MessageCollection, 'Field'],
+    [opensocial.Name, 'Field'],
+    [opensocial.NavigationParameters, 'DestinationType'],
+    [opensocial.NavigationParameters, 'Field'],
+    [opensocial.Organization, 'Field'],
+    [opensocial.Person, 'Field'],
+    [opensocial.Phone, 'Field'],
+    [opensocial.ResponseItem, 'Error'],
+    [opensocial.Url, 'Field']
+  ]);
+
+  // TODO(jasvir): The following object *is* exposed to gadget
+  // code because its returned by opensocial.DataRequest.*
+  // but isn't documented in gadget API.
+  caja___.whitelistProps([
+    [JsonRpcRequestItem, 'rpc'],
+    [JsonRpcRequestItem, 'processData'],
+    [JsonRpcRequestItem, 'processResponse'],
+    [JsonRpcRequestItem, 'errors']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/opensocial-reference/url.js b/trunk/features/src/main/javascript/features/opensocial-reference/url.js
new file mode 100644
index 0000000..c87aa5c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-reference/url.js
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global opensocial */
+
+/**
+ * @fileoverview Representation of an url.
+ */
+
+
+/**
+ * @class
+ * Base interface for all URL objects.
+ *
+ * @name opensocial.Url
+ */
+
+
+/**
+ * Base interface for all url objects.
+ *
+ * @private
+ * @constructor
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.51.
+ */
+opensocial.Url = function(opt_params) {
+  this.fields_ = opt_params || {};
+};
+
+
+/**
+ * @static
+ * @class
+ * All of the fields that a url has. These are the supported keys for the
+ * <a href="opensocial.Url.html#getField">Url.getField()</a> method.
+ *
+ * @name opensocial.Url.Field
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.52.
+ */
+opensocial.Url.Field = {
+  /**
+   * The url number type or label. Examples: work, blog feed,
+   * website, etc Specified as a String.
+   *
+   * @member opensocial.Url.Field
+   */
+  TYPE: 'type',
+
+  /**
+   * The text of the link. Specified as a String.
+   *
+   * @member opensocial.Url.Field
+   */
+  LINK_TEXT: 'linkText',
+
+  /**
+   * The address the url points to. Specified as a String.
+   *
+   * @member opensocial.Url.Field
+   */
+  ADDRESS: 'address'
+};
+
+
+/**
+ * Gets data for this URL that is associated with the specified key.
+ *
+ * @param {string} key The key to get data for;
+ *    keys are defined in <a href="opensocial.Url.Field.html"><code>
+ *    Url.Field</code></a>.
+ * @param {Object.<opensocial.DataRequest.DataRequestFields, Object>}
+ *  opt_params Additional
+ *    <a href="opensocial.DataRequest.DataRequestFields.html">params</a>
+ *    to pass to the request.
+ * @return {string} The data.
+ * @deprecated since 1.0 see http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Gadget.xml#rfc.section.A.51.1.1.
+ */
+opensocial.Url.prototype.getField = function(key, opt_params) {
+  return opensocial.Container.getField(this.fields_, key, opt_params);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/README b/trunk/features/src/main/javascript/features/opensocial-templates/README
new file mode 100644
index 0000000..002733a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/README
@@ -0,0 +1,23 @@
+# History
+
+Original work come from http://google-jstemplate.googlecode.com and was included at 2009-04-03.
+
+# Running Tests
+
+You need to download JsUnit from http://jsunit.net/ and unzip to this folder,
+or create a symlink 'jsunit' to your local jsunit directory.
+Then you can run the tests for OpenSocial Template library by running the
+following script in a unix-like environment:
+
+firefox jsunit/testRunner.html?testpage=$PWD/ost_test.html
+
+TODO: Instructions for loading test page for other operating system/browser
+environments.
+
+N.B.: Has been verified only on FF1.5 and FF2.
+
+N.B.: These tests run in the jsunit.net version JsUnit, other than the
+jsunit.berlios.de version used in the tests for other features. The heavy use of
+client-side objects requires a browser-based JsUnit. This rules out the other
+version, which uses an embedded Rhino engine. jsnuit.net does not support Maven,
+so we cannot integrate the testing into Maven build process.
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/base.js b/trunk/features/src/main/javascript/features/opensocial-templates/base.js
new file mode 100644
index 0000000..b2b87ed
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/base.js
@@ -0,0 +1,403 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Prototype for OpenSocial Templates implementation.
+ *
+ * Simple usage of templates:
+ *   var template = os.compileTemplate("<span>Hello, ${name}</span>");
+ *   template.renderInto(document.getElementById("output"), { name: "Bob" });
+ *
+ * More complex usecase:
+ *   var data = { ... };
+ *   var template = os.compileTemplate(document.getElementById("template"));
+ *   var context = os.createContext(data);
+ *   var output = template.render(data, context);
+ *   // ... attach the output node ...
+ *   os.fireCallbacks(context);
+ *
+ * TODO(levik): Optimization:
+ *   - Define all regexps as globals once, not once per function call.
+ *   - Use queue-based DOM walker instead of recursion.
+ *   - doTag() safeguards node from abuse (no parent access, etc).
+ */
+
+opensocial = opensocial || {};
+opensocial.template = opensocial.template || {};
+var os = opensocial.template;
+
+/**
+ * Sends a log to the console. Currently uses Firebug console if available,
+ * otherwise supresses the message.
+ * TODO: What other logging APIs can we use? Does gadgets provide one?
+ * @param {string} msg The message to send.
+ */
+os.log = function(msg) {
+  var console = window['console'];
+  if (console && console.log) {
+    console.log(msg);
+  }
+};
+
+// Register our logging function as the global logger function.
+// TODO: Remove global variables once JsTemplates supports setting logger
+window['log'] = os.log;
+
+/**
+ * Logs a warning to the console.
+ */
+os.warn = function(msg) {
+  os.log('WARNING: ' + msg);
+};
+
+/**
+ * Is the object an array?
+ */
+os.isArray = function(obj) {
+  return typeof(obj) == 'object' &&
+      typeof(obj.length) == 'number' &&
+      typeof(obj.push) == 'function';
+};
+
+/**
+ * Constants
+ * TODO(davidbyttow): Pull these out of os and make them global (optimization)
+ */
+os.ATT_customtag = 'customtag';
+
+os.VAR_my = '$my';
+os.VAR_cur = '$cur';
+os.VAR_node = '$node';
+os.VAR_msg = 'Msg';
+os.VAR_parentnode = '$parentnode';
+os.VAR_uniqueId = '$uniqueId';
+os.VAR_identifierresolver = '$_ir';
+os.VAR_emptyArray = '$_ea';
+os.VAR_callbacks = '$callbacks_';
+
+/**
+ * Reusable empty array instance
+ * IE6 PERF: To avoid creating empty arrays when they are needed.
+ */
+os.EMPTY_ARRAY = [];
+
+/**
+ * Regular expressions
+ * TODO(levik): Move all regular expressions here.
+ * @private
+ */
+os.regExps_ = {
+  ONLY_WHITESPACE: /^[ \t\n]*$/,
+  VARIABLE_SUBSTITUTION: /^([\w\W]*?)(\$\{[^\}]*\})([\w\W]*)$/
+};
+
+/**
+ * Preprocess the template.
+ * @param {Element|TextNode|string} node DOM node containing the template data, or the
+ * string source.
+ * @param {string=} opt_id An optional ID for the new template.
+ * @return {os.Template} A compiled Template object.
+ */
+os.compileTemplate = function(node, opt_id) {
+  // Allow polymorphic behavior.
+  if (typeof(node) == 'string') {
+    return os.compileTemplateString(node, opt_id);
+  }
+
+  opt_id = opt_id || node.name;
+  var src = node.value || node.innerHTML;
+  src = os.trim(src);
+  var template = os.compileTemplateString(src, opt_id, node);
+  // Decorate the node with the template's ID, so we consistently render it
+  // into the same DIV, and so that it doesn't get treated as anonymous anymore.
+  if (! node.name) {
+    node.name = template.id;
+  }
+  return template;
+};
+
+/**
+ * Compile a template without requiring a DOM node.
+ * @param {string} src XML data to be compiled.
+ * @param {string=} opt_id An optional ID for the new template.
+ * @param {Element=} opt_container An optional container DOM Element
+ * to look for namespaces.
+ * @return {opensocial.template.Template} A compiled Template object.
+ */
+os.compileTemplateString = function(src, opt_id, opt_container) {
+  src = opensocial.xmlutil.prepareXML(src, opt_container);
+  var doc = gadgets.jsondom.parse(src, opt_id);
+  return os.compileXMLDoc(doc, opt_id);
+};
+
+/**
+ * Render one compiled node with a context.
+ * @return {Element} a DOM element containing the result of template processing.
+ * @private
+ */
+os.renderTemplateNode_ = function(compiledNode, context) {
+  var template = domCloneElement(compiledNode);
+  if (template.removeAttribute) {
+    template.removeAttribute(STRING_id);
+  }
+  jstProcess(context, template);
+  return template;
+};
+
+/**
+ * @type {number} A global counter for rendered elements.
+ * @private
+ */
+os.elementIdCounter_ = 0;
+
+/**
+ * Creates a custom tag function for rendering a compiled template.
+ */
+os.createTemplateCustomTag = function(template) {
+  return function(node, data, context) {
+    context.setVariable(os.VAR_my, node);
+    context.setVariable(os.VAR_node, node);
+    context.setVariable(os.VAR_uniqueId, os.elementIdCounter_++);
+    var ret = template.render(data, context);
+
+    // Prevent reprocessing after attachment.
+    os.markNodeToSkip(ret);
+
+    return ret;
+  };
+};
+
+/**
+ * Creates a map of the named children of a node. Lower-cased element names
+ * (including transformed custom tags) are used as keys.
+ * Where multiple elements share a name, the map value will be an array.
+ * @param {Element} node The node whose children are to be mapped.
+ * @return {Object} A Map of Element names to Elements.
+ * @private
+ */
+os.computeChildMap_ = function(node) {
+  var map = {};
+  for (var i = 0; i < node.childNodes.length; i++) {
+    var child = node.childNodes[i];
+    if (!child.tagName) {
+      continue;
+    }
+    var name = child.getAttribute(os.ATT_customtag);
+    if (name) {
+      var parts = name.split(':');
+      parts.length == 2 ? name = parts[1] : name = parts[0];
+    } else {
+      name = child.tagName;
+    }
+    name = name.toLowerCase();
+    var prev = map[name];
+    if (!prev) {
+      map[name] = child;
+    } else if (os.isArray(prev)) {
+      prev.push(child);
+    } else {
+      map[name] = [];
+      map[name].push(prev);
+      map[name].push(child);
+    }
+  }
+  return map;
+};
+
+/**
+ * Creates a functor which returns a value from the specified node given a
+ * name.
+ * @param {Node} node Node to get the value from.
+ * @return {function(string)} The functor which takes a type {string}.
+ * @private
+ */
+os.createNodeAccessor_ = function(node) {
+  return function(name) {
+    return os.getValueFromNode_(node, name);
+  };
+};
+
+/**
+ * A singleton instance of the current gadget Prefs - only instantiated if
+ * we are in a gadget container.
+ * @type {gadgets.Prefs}
+ * @private
+ */
+os.gadgetPrefs_ = null;
+os.getGadgetPrefs = function() {
+  if(os.gadgetPrefs === null) {
+    if (window['gadgets'] && window['gadgets']['Prefs']) {
+      os.gadgetPrefs_ = new window['gadgets']['Prefs']();
+    }
+  }
+  return os.gadgetPrefs_;
+}
+
+/**
+ * A convenience function to get a localized message by key from the shared
+ * gadgets.Prefs object.
+ * @param {string} key The message key to get.
+ * @return {?string} The localized message for a given key, or null if not
+ * found, or not in the gadgets environment.
+ */
+os.getPrefMessage = function(key) {
+  if (!os.getGadgetPrefs()) {
+    return null;
+  }
+  return os.gadgetPrefs_.getMsg(key);
+};
+
+/**
+ * A map of custom attributes keyed by attribute name.
+ * Maps {string} types onto Function({Element|string|Object|JSEvalContext}).
+ * @type {Object}
+ * @private
+ */
+os.customAttributes_ = {};
+
+/**
+ * Registers a custom attribute functor. When this attribute is encountered in
+ * a DOM node, the specified functor will be called.
+ * @param {string} attrName The name of the custom attribute.
+ * @param {function(string)} functor A function with signature
+ *     function({Element}, {string}, {Object}, {JSEvalContext}).
+ * @private
+ */
+os.registerAttribute_ = function(attrName, functor) {
+  os.customAttributes_[attrName] = functor;
+};
+
+/**
+ * Calls a pre-registered custom attribute handler.
+ */
+os.doAttribute = function(node, attrName, data, context) {
+  if (!os.customAttributes_.hasOwnProperty(attrName)) {
+    return;
+  }
+  var attrFunctor = os.customAttributes_[attrName];
+  attrFunctor(node, node.getAttribute(attrName), data, context);
+};
+
+/**
+ * Processes a custom tag by invoking the appropriate custom tag function.
+ * @param {Element} node Parent DOM node.
+ * @param {string} ns Namespace.
+ * @param {string} tag Tag name.
+ * @param {Object} data Current evaluation data.
+ * @param {Object} context JSEvalContext object encapsulating data.
+ */
+os.doTag = function(node, ns, tag, data, context) {
+  var tagFunction = os.getCustomTag(ns, tag);
+  if (!tagFunction) {
+    os.warn('Custom tag <' + ns + ':' + tag + '> not defined.');
+    return;
+  }
+
+  var ctx = null;
+  // Process tag's inner content before processing the tag.
+  for (var child = node.firstChild; child; child = child.nextSibling) {
+    if (child.nodeType == DOM_ELEMENT_NODE) {
+      if (ctx == null) {
+        var selectInner = node[PROP_jstcache] ? node[PROP_jstcache][ATT_innerselect] : null;
+        if (selectInner) {
+          var data = context.jsexec(selectInner, node);
+          ctx = context.clone(data, 0, 0);
+        } else {
+          ctx = context;
+        }
+      }
+      jstProcess(ctx, child);
+      os.markNodeToSkip(child);
+    }
+  }
+
+  ctx = context.clone({}, 0, 0);
+  var result = tagFunction.call(null, node, data, ctx);
+
+  if (!result && typeof(result) != 'string') {
+    throw Error('Custom tag <' + ns + ':' + tag + '> failed to return anything.');
+  }
+
+  if (typeof(result) == 'string') {
+    node.innerHTML = result ? result : '';
+  } else if (os.isArray(result)) {
+    os.removeChildren(node);
+    for (var i = 0; i < result.length; i++) {
+      if (result[i].nodeType == DOM_ELEMENT_NODE ||
+          result[i].nodeType == DOM_TEXT_NODE) {
+        node.appendChild(result[i]);
+        if (result[i].nodeType == DOM_ELEMENT_NODE) {
+          os.markNodeToSkip(result[i]);
+        }
+      }
+    }
+  } else {
+    var callbacks = context.getVariable(os.VAR_callbacks);
+    var resultNode = null;
+    if (result.nodeType == DOM_ELEMENT_NODE) {
+      resultNode = result;
+    } else if (result.root && result.root.nodeType == DOM_ELEMENT_NODE) {
+      resultNode = result.root;
+    }
+
+    // Only attach the result DOM if it's not the same as the container node,
+    // or not already attached. In IE, detached nodes can be parented in
+    // DocumentFragments, so we check for that as well.
+    if (resultNode && resultNode != node && (
+        !resultNode.parentNode ||
+        resultNode.parentNode.nodeType == DOM_DOCUMENT_FRAGMENT_NODE)) {
+      os.removeChildren(node);
+      node.appendChild(resultNode);
+      os.markNodeToSkip(resultNode);
+    }
+    if (result.onAttach) {
+      callbacks.push(result);
+    }
+  }
+};
+
+
+/**
+ * Checks the current context, and if it's an element node, sets it to be used
+ * for future <os:renderAll/> operations.
+ * @private
+ */
+os.setContextNode_ = function(data, context) {
+  if (data.nodeType == DOM_ELEMENT_NODE) {
+    context.setVariable(os.VAR_node, data);
+  }
+};
+
+/**
+ * Mark the node to not be re-processed by continued template processing.
+ * Useful if the node contains a template that needs to be processed with a
+ * different context.
+ */
+os.markNodeToSkip = function(node) {
+  node.setAttribute(ATT_skip, 'true');
+
+  // Remove the attributes processed when jsskip is true
+  node.removeAttribute(ATT_select);
+  node.removeAttribute(ATT_eval);
+  node.removeAttribute(ATT_values);
+  node.removeAttribute(ATT_display);
+
+  // Cause the cache to be re-calculated
+  node[PROP_jstcache] = null;
+  node.removeAttribute(ATT_jstcache);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/compiler.js b/trunk/features/src/main/javascript/features/opensocial-templates/compiler.js
new file mode 100644
index 0000000..8dac3db
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/compiler.js
@@ -0,0 +1,1095 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Implements compiler functionality for OpenSocial Templates.
+ *
+ * TODO(davidbyttow): Move into os.Compiler.
+ */
+
+/**
+ * Literal semcolons have special meaning in JST, so we need to change them to
+ * variable references.
+ */
+os.SEMICOLON = ';';
+
+/**
+ * Check if the browser is Internet Explorer.
+ *
+ * TODO(levik): Find a better, more general way to do this, esp. if we need
+ * to do other browser checks elswhere.
+ */
+os.isIe = navigator.userAgent.indexOf('Opera') != 0 &&
+    navigator.userAgent.indexOf('MSIE') != -1;
+
+/**
+ * Takes an XML node containing Template markup and compiles it into a Template.
+ * The node itself is not considered part of the markup.
+ * @param {Node} node XML node to be compiled.
+ * @param {string=} opt_id An optional ID for the new template.
+ * @return {os.Template} A compiled Template object.
+ */
+os.compileXMLNode = function(node, opt_id) {
+  var nodes = [];
+  for (var child = node.firstChild; child; child = child.nextSibling) {
+    if (child.nodeType == DOM_ELEMENT_NODE) {
+      nodes.push(os.compileNode_(child));
+    } else if (child.nodeType == DOM_TEXT_NODE) {
+      if (child != node.firstChild ||
+          !child.nodeValue.match(os.regExps_.ONLY_WHITESPACE)) {
+        var compiled = os.breakTextNode_(child);
+        for (var i = 0; i < compiled.length; i++) {
+          nodes.push(compiled[i]);
+        }
+      }
+    }
+  }
+  var template = new os.Template(opt_id);
+  template.setCompiledNodes_(nodes);
+  return template;
+};
+
+/**
+ * Takes an XML Document and compiles it into a Template object.
+ * @param {Document} doc XML document to be compiled.
+ * @param {string=} opt_id An optional ID for the new template.
+ * @return {os.Template} A compiled Template object.
+ */
+os.compileXMLDoc = function(doc, opt_id) {
+  var node = doc.firstChild;
+  // Find the <root> node (skip DOCTYPE).
+  while (node.nodeType != DOM_ELEMENT_NODE) {
+    node = node.nextSibling;
+  }
+
+  return os.compileXMLNode(node, opt_id);
+};
+
+/**
+ * Map of special operators to be transformed.
+ */
+os.operatorMap = {
+  'and': '&&',
+  'eq': '==',
+  'lte': '<=',
+  'lt': '<',
+  'gte': '>=',
+  'gt': '>',
+  'neq': '!=',
+  'or': '||',
+  'not': '!'
+};
+
+/**
+ * Shared regular expression to split a string into lexical parts. Quoted
+ * strings are treated as tokens, so are identifiers and any characters between
+ * them.
+ * In "foo + bar = 'baz - bing'", the tokens are
+ *   ["foo", " + ", "bar", " = ", "'baz - bing'"]
+ */
+os.regExps_.SPLIT_INTO_TOKENS =
+    /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\w+|[^"'\w]+/g;
+
+/**
+ * Parses operator markup into JS code. See operator map above.
+ *
+ * TODO: Simplify this to only work on neccessary operators - binary ones that
+ * use "<" or ">".
+ *
+ * @param {string} src The string snippet to parse.
+ * @private
+ */
+os.remapOperators_ = function(src) {
+  return src.replace(os.regExps_.SPLIT_INTO_TOKENS,
+      function(token) {
+        return os.operatorMap.hasOwnProperty(token) ?
+            os.operatorMap[token] : token;
+      });
+};
+
+/**
+ * Remap variable references in the expression.
+ * @param {string} expr The expression to transform.
+ * @return {string} Transformed exression.
+ * @private
+ */
+os.transformVariables_ = function(expr) {
+  expr = os.replaceTopLevelVars_(expr);
+
+  return expr;
+};
+
+/**
+ * Map of variables to transform
+ * @private
+ */
+os.variableMap_ = {
+  'my': os.VAR_my,
+  'My': os.VAR_my,
+  'cur': VAR_this,
+  'Cur': VAR_this,
+  '$cur': VAR_this,
+  'Top': VAR_top,
+  'Context': VAR_loop
+};
+
+/**
+ * Replace the top level variables
+ * @param {string} text The expression.
+ * @return {string} Expression with replacements.
+ * @private
+ */
+os.replaceTopLevelVars_ = function(text) {
+
+  var regex;
+
+  regex = os.regExps_.TOP_LEVEL_VAR_REPLACEMENT;
+  if (!regex) {
+    regex = /(^|[^.$a-zA-Z0-9])([$a-zA-Z0-9]+)/g;
+    os.regExps_.TOP_LEVEL_VAR_REPLACEMENT = regex;
+  }
+
+  return text.replace(regex,
+      function(whole, left, right) {
+        if (os.variableMap_.hasOwnProperty(right)) {
+          return left + os.variableMap_[right];
+        } else {
+          return whole;
+        }
+      });
+};
+
+/**
+ * This function is used to lookup named properties of objects.
+ * By default only a simple lookup is performed, but using
+ * os.setIdentifierResolver() it's possible to plug in a more complex function,
+ * for example one that looks up foo -> getFoo() -> get("foo").
+ *
+ * TODO: This should not be in compiler.
+ * @private
+ */
+os.identifierResolver_ = function(data, name) {
+  return data.hasOwnProperty(name) ? data[name] : ('get' in data ? data.get(name) : null);
+};
+
+/**
+ * Sets the Identifier resolver function. This is global, and must be done
+ * before any compilation of templates takes place.
+ *
+ * TODO: This should possibly not be in compiler?
+ */
+os.setIdentifierResolver = function(resolver) {
+  os.identifierResolver_ = resolver;
+};
+
+/**
+ * Gets a named property from a JsEvalContext (by checking data_ and vars_) or
+ * from a simple JSON object by looking at properties. The IdentifierResolver
+ * function is used in either case.
+ *
+ * TODO: This should not be in compiler.
+ *
+ * @param {JsEvalContext|Object} context Context to get property from.
+ * @param {string} name Name of the property.
+ * @return {Object|string}
+ */
+os.getFromContext = function(context, name, opt_default) {
+  if (!context) {
+    return opt_default;
+  }
+  var ret;
+  // Check if this is a context object.
+  if (context.vars_ && context.data_) {
+    // Is the context payload a DOM node?
+    if (context.data_.nodeType == DOM_ELEMENT_NODE) {
+      ret = os.getValueFromNode_(context.data_, name);
+      if (ret == null) {
+        // Set to undefined
+        ret = void(0);
+      }
+    } else {
+      ret = os.identifierResolver_(context.data_, name);
+    }
+    if (typeof(ret) == 'undefined') {
+      ret = os.identifierResolver_(context.vars_, name);
+    }
+    if (typeof(ret) == 'undefined' && context.vars_[os.VAR_my]) {
+      ret = os.getValueFromNode_(context.vars_[os.VAR_my], name);
+    }
+    if (typeof(ret) == 'undefined' && context.vars_[VAR_top]) {
+      ret = context.vars_[VAR_top][name];
+    }
+  } else if (context.nodeType == DOM_ELEMENT_NODE) {
+    // Is the context a DOM node?
+    ret = os.getValueFromNode_(context, name);
+  } else {
+    ret = os.identifierResolver_(context, name);
+  }
+  if (typeof(ret) == 'undefined' || ret == null) {
+    if (typeof(opt_default) != 'undefined') {
+      ret = opt_default;
+    } else {
+      ret = '';
+    }
+  } else if (opt_default && os.isArray(opt_default) && !os.isArray(ret) &&
+      ret.list && os.isArray(ret.list)) {
+    // If we were trying to get an array, but got a JSON object with an
+    // array property "list", return that instead.
+    ret = ret.list;
+  }
+  return ret;
+};
+
+/**
+ * Prepares an expression for JS evaluation.
+ * @param {string} expr The expression snippet to parse.
+ * @param {string=} opt_default An optional default value reference (such as the
+ * literal string 'null').
+ * @private
+ */
+os.transformExpression_ = function(expr, opt_default) {
+  expr = os.remapOperators_(expr);
+  expr = os.transformVariables_(expr);
+  if (os.identifierResolver_) {
+    expr = os.wrapIdentifiersInExpression(expr, opt_default);
+  }
+  return expr;
+};
+
+/**
+ * A Map of special attribute names to change while copying attributes during
+ * compilation. The key is OST-spec attribute, while the value is JST attribute
+ * used to implement that feature.
+ * @private
+ */
+os.attributeMap_ = {
+  'if': ATT_display,
+  'repeat': ATT_select,
+  'cur': ATT_innerselect
+};
+
+/**
+ * Appends a JSTemplate attribute value while maintaining previous values.
+ * @private
+ */
+os.appendJSTAttribute_ = function(node, attrName, value) {
+  var previousValue = node.getAttribute(attrName);
+  if (previousValue) {
+    value = previousValue + ';' + value;
+  }
+  node.setAttribute(attrName, value);
+};
+
+/**
+ * Copies attributes from one node (xml or html) to another (html),.
+ * Special OpenSocial attributes are substituted for their JStemplate
+ * counterparts.
+ * @param {Element} from An XML or HTML node to copy attributes from.
+ * @param {Element} to An HTML node to copy attributes to.
+ * @param {string=} opt_customTag The name of the custom tag, being processed if
+ * any.
+ *
+ * TODO(levik): On IE, some properties/attributes might be case sensitive when
+ * set through script (such as "colSpan") - since they're not case sensitive
+ * when defined in HTML, we need to support this type of use.
+ * @private
+ */
+os.copyAttributes_ = function(from, to, opt_customTag) {
+
+  var dynamicAttributes = null;
+
+  for (var i = 0; i < from.attributes.length; i++) {
+    var name = from.attributes[i].nodeName;
+    var value = from.getAttribute(name);
+    if (name && value) {
+      if (name == 'var') {
+        os.appendJSTAttribute_(to, ATT_vars, from.getAttribute(name) +
+            ': $this');
+      } else if (name == 'context') {
+        os.appendJSTAttribute_(to, ATT_vars, from.getAttribute(name) +
+            ': ' + VAR_loop);
+      } else if (name.length < 7 || name.substring(0, 6) != 'xmlns:') {
+        if (os.customAttributes_[name]) {
+          os.appendJSTAttribute_(to, ATT_eval, "os.doAttribute(this, '" + name +
+              "', $this, $context)");
+        } else if (name == 'repeat') {
+          os.appendJSTAttribute_(to, ATT_eval,
+              'os.setContextNode_($this, $context)');
+        }
+        var outName = os.attributeMap_.hasOwnProperty(name) ?
+            os.attributeMap_[name] : name;
+        var substitution =
+            (os.attributeMap_[name]) ?
+            null : os.parseAttribute_(value);
+
+        if (substitution) {
+          if (outName == 'class') {
+            // Dynamically setting the @class attribute gets ignored by the
+            // browser. We need to set the .className property instead.
+            outName = '.className';
+          } else if (outName == 'style') {
+            // Similarly, on IE, setting the @style attribute has no effect.
+            // The cssText property of the style object must be set instead.
+            outName = '.style.cssText';
+          } else if (to.getAttribute(os.ATT_customtag)) {
+            // For custom tags, it is more useful to put values into properties
+            // where they can be accessed as objects, rather than placing them
+            // into attributes where they need to be serialized.
+            outName = '.' + outName;
+          } else if (os.isIe && !os.customAttributes_[outName] &&
+              outName.substring(0, 2).toLowerCase() == 'on') {
+            // For event handlers on IE, setAttribute doesn't work, so we need
+            // to create a function to set as a property.
+            outName = '.' + outName;
+            substitution = 'new Function(' + substitution + ')';
+          } else if (outName == 'selected' && to.tagName == 'OPTION') {
+            // For the @selected attribute of an option, set the property
+            // instead to allow false values to not mark the option selected.
+            outName = '.selected';
+          }
+
+          // TODO: reuse static array (IE6 perf).
+          if (!dynamicAttributes) {
+            dynamicAttributes = [];
+          }
+          dynamicAttributes.push(outName + ':' + substitution);
+        } else {
+          // For special attributes, do variable transformation.
+          if (os.attributeMap_.hasOwnProperty(name)) {
+            // If the attribute value looks like "${expr}", just use the "expr".
+            if (value.length > 3 &&
+                value.substring(0, 2) == '${' &&
+                value.charAt(value.length - 1) == '}') {
+              value = value.substring(2, value.length - 1);
+            }
+            // In special attributes, default value is empty array for repeats,
+            // null for others
+            value = os.transformExpression_(value,
+                name == 'repeat' ? os.VAR_emptyArray : 'null');
+          } else if (outName == 'class') {
+            // In IE, we must set className instead of class.
+            to.setAttribute('className', value);
+          } else if (outName == 'style') {
+            // Similarly, on IE, setting the @style attribute has no effect.
+            // The cssText property of the style object must be set instead.
+            to.style.cssText = value;
+          }
+          if (os.isIe && !os.customAttributes_.hasOwnProperty(outName) &&
+              outName.substring(0, 2).toLowerCase() == 'on') {
+            // In IE, setAttribute doesn't create event handlers, so we must
+            // use attachEvent in order to create handlers that are preserved
+            // by calls to cloneNode().
+            to.attachEvent(outName, new Function(value));
+          } else {
+            to.setAttribute(outName, value);
+          }
+        }
+      }
+    }
+  }
+
+  if (dynamicAttributes) {
+    os.appendJSTAttribute_(to, ATT_values, dynamicAttributes.join(';'));
+  }
+};
+
+/**
+ * Recursively compiles an individual node from XML to DOM (for JSTemplate)
+ * Special os.* tags and tags for which custom functions are defined
+ * are converted into markup recognizable by JSTemplate.
+ *
+ * TODO: process text nodes and attributes  with ${} notation here
+ * @private
+ */
+os.compileNode_ = function(node) {
+  if (node.nodeType == DOM_TEXT_NODE) {
+    var textNode = node.cloneNode(false);
+    return os.breakTextNode_(textNode);
+  } else if (node.nodeType == DOM_ELEMENT_NODE) {
+    var output;
+    if (node.tagName.indexOf(':') > 0) {
+      if (node.tagName == 'os:Repeat') {
+        output = document.createElement(os.computeContainerTag_(node));
+        output.setAttribute(ATT_select, os.parseAttribute_(node.getAttribute('expression')));
+        var varAttr = node.getAttribute('var');
+        if (varAttr) {
+          os.appendJSTAttribute_(output, ATT_vars, varAttr + ': $this');
+        }
+        var contextAttr = node.getAttribute('context');
+        if (contextAttr) {
+          os.appendJSTAttribute_(output, ATT_vars, contextAttr + ': ' + VAR_loop);
+        }
+        os.appendJSTAttribute_(output, ATT_eval, 'os.setContextNode_($this, $context)');
+      } else if (node.tagName == 'os:If') {
+        output = document.createElement(os.computeContainerTag_(node));
+        output.setAttribute(ATT_display, os.parseAttribute_(node.getAttribute('condition')));
+      } else {
+        output = document.createElement('span');
+        output.setAttribute(os.ATT_customtag, node.tagName);
+
+        var custom = node.tagName.split(':');
+        os.appendJSTAttribute_(output, ATT_eval, 'os.doTag(this, \"'
+            + custom[0] + '\", \"' + custom[1] + '\", $this, $context)');
+        var context = node.getAttribute('cur') || '{}';
+        output.setAttribute(ATT_innerselect, context);
+
+        // For os:Render, create a parent node reference.
+        // TODO: remove legacy support
+        if (node.tagName == 'os:render' || node.tagName == 'os:Render' ||
+            node.tagName == 'os:renderAll' || node.tagName == 'os:RenderAll') {
+          os.appendJSTAttribute_(output, ATT_values, os.VAR_parentnode + ':' +
+              os.VAR_node);
+        }
+
+        os.copyAttributes_(node, output, node.tagName);
+      }
+    } else {
+      output = os.xmlToHtml_(node);
+    }
+    if (output && !os.processTextContent_(node, output)) {
+      for (var child = node.firstChild; child; child = child.nextSibling) {
+        var compiledChild = os.compileNode_(child);
+        if (compiledChild) {
+          if (os.isArray(compiledChild)) {
+            for (var i = 0; i < compiledChild.length; i++) {
+              output.appendChild(compiledChild[i]);
+            }
+          } else {
+            // If inserting a TR into a TABLE, inject a TBODY element.
+            if (compiledChild.tagName == 'TR' && output.tagName == 'TABLE') {
+              var lastEl = output.lastChild;
+              while (lastEl && lastEl.nodeType != DOM_ELEMENT_NODE &&
+                  lastEl.previousSibling) {
+                lastEl = lastEl.previousSibling;
+              }
+              if (!lastEl || lastEl.tagName != 'TBODY') {
+                lastEl = document.createElement('tbody');
+                output.appendChild(lastEl);
+              }
+              lastEl.appendChild(compiledChild);
+            } else {
+              output.appendChild(compiledChild);
+            }
+          }
+        }
+      }
+    }
+    return output;
+  }
+  return null;
+};
+
+/**
+ * Calculates the type of element best suited to encapsulating contents of a
+ * <os:Repeat> or <os:If> tags. Inspects the element's children to see if one
+ * of the special cases should be used.
+ * "optgroup" for <option>s
+ * "tbody" for <tr>s
+ * "span" otherwise
+ * @param {Element} element The repeater/conditional element.
+ * @return {stirng} Name of the node ot represent this repeater.
+ * @private
+ */
+os.computeContainerTag_ = function(element) {
+  var child = element.firstChild;
+  if (child) {
+    while (child && !child.tagName) {
+      child = child.nextSibling;
+    }
+    if (child) {
+      var tag = child.tagName.toLowerCase();
+      if (tag == 'option') {
+        return 'optgroup';
+      }
+      if (tag == 'tr') {
+        return 'tbody';
+      }
+    }
+  }
+  return 'span';
+};
+
+/**
+ * XHTML Entities we need to support in XML, defined in DOCTYPE format.
+ *
+ * TODO(levik): A better way to do this.
+ */
+os.ENTITIES = '<!ENTITY nbsp \"&#160;\">';
+
+/**
+ * Creates an HTML node that's a shallow copy of an XML node
+ * (includes attributes).
+ * @private
+ */
+os.xmlToHtml_ = function(xmlNode) {
+  var htmlNode = document.createElement(xmlNode.tagName);
+  os.copyAttributes_(xmlNode, htmlNode);
+  return htmlNode;
+};
+
+/**
+ * Fires callbacks on a context object
+ */
+os.fireCallbacks = function(context) {
+  var callbacks = context.getVariable(os.VAR_callbacks);
+  while (callbacks.length > 0) {
+    var callback = callbacks.pop();
+    if (callback.onAttach) {
+      callback.onAttach();
+    // TODO(levik): Remove no-context handlers?
+    } else if (typeof(callback) == 'function') {
+      callback.apply({});
+    }
+  }
+};
+
+/**
+ * Checks for and processes an optimized case where a node only has text content
+ * In this instance, any variable substitutions happen without creating
+ * intermediary spans.
+ *
+ * This will work when node content looks like:
+ *   - "Plain text"
+ *   - "${var}"
+ *   - "Plain text with ${var} inside"
+ * But not when it is
+ *   - "Text <b>With HTML content</b> (with or without a ${var})
+ *   - Custom tags are also exempt from this optimization.
+ *
+ * @return {boolean} true if node only had text data and needs no further
+ * processing, false otherwise.
+ * @private
+ */
+os.processTextContent_ = function(fromNode, toNode) {
+  if (fromNode.childNodes.length == 1 &&
+      !toNode.getAttribute(os.ATT_customtag) &&
+      fromNode.firstChild.nodeType == DOM_TEXT_NODE) {
+    var substitution = os.parseAttribute_(fromNode.firstChild.data);
+    if (toNode.nodeName == 'SCRIPT') {
+      toNode.text = os.trimWhitespaceForIE_(fromNode.firstChild.data, true, true);
+    } else if (substitution) {
+      toNode.setAttribute(ATT_content, substitution);
+    } else {
+      toNode.appendChild(document.createTextNode(
+          os.trimWhitespaceForIE_(fromNode.firstChild.data, true, true)));
+    }
+    return true;
+  }
+  return false;
+};
+
+/**
+ * Create a textNode out of a string, if non-empty, then puts into an array.
+ * @param {string} text A string to be created as a text node.
+ */
+os.pushTextNode = function(array, text) {
+  if (text.length > 0) {
+    array.push(document.createTextNode(text));
+  }
+};
+
+/**
+ * Removes extra whitespace and newline characters for IE - to be used for
+ * transforming strings that are destined for textNode content.
+ * @param {string} string The string to trim spaces from.
+ * @param {boolean=} opt_trimStart Trim the start of the string.
+ * @param {boolean=} opt_trimEnd Trim the end of the string.
+ * @return {string} The string with extra spaces removed on IE, original
+ * string on other browsers.
+ * @private
+ */
+os.trimWhitespaceForIE_ = function(string, opt_trimStart, opt_trimEnd) {
+  if (os.isIe) {
+    // Replace newlines with spaces, then multiple spaces with single ones.
+    // Then remove leading and trailing spaces.
+    var ret = string.replace(/[\x09-\x0d ]+/g, ' ');
+    if (opt_trimStart) {
+      ret = ret.replace(/^\s/, '');
+    }
+    if (opt_trimEnd) {
+      ret = ret.replace(/\s$/, '');
+    }
+    return ret;
+  }
+  return string;
+};
+
+/**
+ * Breaks up a text node with special ${var} markup into a series of text nodes
+ * and spans with appropriate jscontent attribute.
+ *
+ * @return {Array.<Node>} An array of textNodes and Span Elements if variable
+ * substitutions were found, or an empty array if none were.
+ * @private
+ */
+os.breakTextNode_ = function(textNode) {
+  var substRex = os.regExps_.VARIABLE_SUBSTITUTION;
+  var text = textNode.data;
+  var nodes = [];
+  var match = text.match(substRex);
+  while (match) {
+    if (match[1].length > 0) {
+      os.pushTextNode(nodes, os.trimWhitespaceForIE_(match[1]));
+    }
+    var token = match[2].substring(2, match[2].length - 1);
+    if (!token) {
+      token = VAR_this;
+    }
+    var tokenSpan = document.createElement('span');
+    tokenSpan.setAttribute(ATT_content, os.transformExpression_(token));
+    nodes.push(tokenSpan);
+    match = text.match(substRex);
+    text = match[3];
+    match = text.match(substRex);
+  }
+  if (text.length > 0) {
+    os.pushTextNode(nodes, os.trimWhitespaceForIE_(text));
+  }
+  return nodes;
+};
+
+/**
+ * Transforms a literal string for inclusion into a variable evaluation
+ * (a JS string):
+ *   - Escapes single quotes.
+ *   - Replaces newlines with spaces.
+ *   - Substitutes variable references for literal semicolons.
+ *   - Addes single quotes around the string.
+ * @private
+ */
+os.transformLiteral_ = function(string) {
+  return "'" + string.replace(/'/g, "\\'").
+      replace(/\n/g, ' ').replace(/;/g, "'+os.SEMICOLON+'") + "'";
+};
+
+/**
+ * Parses an attribute value into a JS expression. "Hello, ${user}!" becomes
+ * "Hello, " + user + "!".
+ *
+ * @param {string} value Attribute value to parse
+ * TODO: Rename to parseExpression().
+ * @private
+ */
+os.parseAttribute_ = function(value) {
+  if (!value.length) {
+    return null;
+  }
+  var substRex = os.regExps_.VARIABLE_SUBSTITUTION;
+  var text = value;
+  var parts = [];
+  var match = text.match(substRex);
+  if (!match) {
+    return null;
+  }
+  while (match) {
+    if (match[1].length > 0) {
+      parts.push(os.transformLiteral_(
+          os.trimWhitespaceForIE_(match[1], parts.length == 0)));
+    }
+    var expr = match[2].substring(2, match[2].length - 1);
+    if (!expr) {
+      expr = VAR_this;
+    }
+    parts.push('(' +
+        os.transformExpression_(expr) + ')');
+    text = match[3];
+    match = text.match(substRex);
+  }
+  if (text.length > 0) {
+    parts.push(os.transformLiteral_(
+        os.trimWhitespaceForIE_(text, false, true)));
+  }
+  return parts.join('+');
+};
+
+/**
+ * Returns a named value of a given node. First looks for a property, then
+ * attribute, then a child node (or nodes). If multiple child nodes are found,
+ * they will be returned in an array. If we find a single Node that is an
+ * Element, it's children will be returned in an array.
+ * @param {Element} node The DOM node to inspect.
+ * @param {string} name The name of the property/attribute/child node(s) to get.
+ * The special value "*" means return all child Nodes.
+ * @return {string|Element|Object|Array.<Element>} The value as a String,
+ * Object, Element or array of Elements.
+ * @private
+ */
+os.getValueFromNode_ = function(node, name) {
+
+  if (name == '*') {
+    var children = [];
+    for (var child = node.firstChild; child; child = child.nextSibling) {
+      children.push(child);
+    }
+    return children;
+  }
+
+  // Since namespaces are not supported, strip off prefix.
+  if (name.indexOf(':') >= 0) {
+    name = name.substring(name.indexOf(':') + 1);
+  }
+
+  var ret = node[name];
+  if (typeof(ret) == 'undefined' || ret == null) {
+    ret = node.getAttribute(name);
+  }
+
+  if (typeof(ret) != 'undefined' && ret != null) {
+    // Process special cases where ret would be wrongly evaluated as "true"
+    if (ret == 'false') {
+      ret = false;
+    } else if (ret == '0') {
+      ret = 0;
+    }
+    return ret;
+  }
+
+  var myMap = node[os.VAR_my];
+  if (!myMap) {
+    myMap = os.computeChildMap_(node);
+    node[os.VAR_my] = myMap;
+  }
+  ret = myMap[name.toLowerCase()];
+  return ret;
+};
+
+//------------------------------------------------------------------------------
+// The functions below are for parsing JS expressions to wrap identifiers.
+// They should be move into a separate file/js-namespace.
+//------------------------------------------------------------------------------
+
+/**
+ * A map of identifiers that should not be wrapped
+ * (such as JS built-ins and special method names).
+ * @private
+ */
+os.identifiersNotToWrap_ = {};
+os.identifiersNotToWrap_['true'] = true;
+os.identifiersNotToWrap_['false'] = true;
+os.identifiersNotToWrap_['null'] = true;
+os.identifiersNotToWrap_['var'] = true;
+os.identifiersNotToWrap_[os.VAR_my] = true;
+os.identifiersNotToWrap_[VAR_this] = true;
+os.identifiersNotToWrap_[VAR_context] = true;
+os.identifiersNotToWrap_[VAR_top] = true;
+os.identifiersNotToWrap_[VAR_loop] = true;
+
+/**
+ * Checks if a character can begin a legal JS identifier name.
+ * @param {string} ch Character to check.
+ * @return {boolean} This character can start an identifier.
+ */
+os.canStartIdentifier = function(ch) {
+  return (ch >= 'a' && ch <= 'z') ||
+      (ch >= 'A' && ch <= 'Z') ||
+      ch == '_' || ch == '$';
+};
+
+/**
+ * Checks if a character can be contained in a legal identifier name.
+ * (A legal identifier in Templates can contain any character a legal
+ * JS identifier can plus the colon - to support ${My.os:Foo})
+ * @param {string} ch Character to check.
+ * @return {string} This is a valid identifier character.
+ */
+os.canBeInIdentifier = function(ch) {
+  return os.canStartIdentifier(ch) || (ch >= '0' && ch <= '9') ||
+      // The colon char cannot be in a real JS identifier, but we allow it,
+      // so that namespaced tag names are treated as whole identifiers.
+      ch == ':';
+};
+
+/**
+ * Checks if a character can be contained in a expression token.
+ * @param {string} ch Character to check.
+ * @return {string} This is a valid token character.
+ */
+os.canBeInToken = function(ch) {
+  return os.canBeInIdentifier(ch) || ch == '(' || ch == ')' ||
+      ch == '[' || ch == ']' || ch == '.';
+};
+
+/**
+ * Wraps an identifier for Identifier Resolution with respect to the context.
+ * os.VAR_idenfitierresolver ("$_ir") is used as the function name.
+ * So, "foo.bar" becomes "$_ir($_ir($context, 'foo'), 'bar')"
+ * @param {string} iden A string representing an identifier.
+ * @param {string=} opt_context A string expression to use for context.
+ * @param {string=} opt_default An optional default value reference (such as the
+ * literal string 'null').
+ * @private
+ */
+os.wrapSingleIdentifier = function(iden, opt_context, opt_default) {
+  if (os.identifiersNotToWrap_.hasOwnProperty(iden) &&
+      (!opt_context || opt_context == VAR_context)) {
+    return iden;
+  }
+  return os.VAR_identifierresolver + '(' +
+      (opt_context || VAR_context) + ', \'' + iden + '\'' +
+      (opt_default ? ', ' + opt_default : '') +
+      ')';
+};
+
+/**
+ * Wraps identifiers in a single token of JS.
+ */
+os.wrapIdentifiersInToken = function(token, opt_default) {
+  if (!os.canStartIdentifier(token.charAt(0))) {
+    return token;
+  }
+
+  // If the identifier is accessing a message
+  // (and gadget messages are obtainable), inline it here.
+  // TODO: This is inefficient for times when the message contains no markup -
+  // such cases should be optimized.
+  if (token.substring(0, os.VAR_msg.length + 1) == (os.VAR_msg + '.') &&
+      os.getGadgetPrefs()) {
+    var key = token.split('.')[1];
+    var msg = os.getPrefMessage(key) || '';
+    return os.parseAttribute_(msg) || os.transformLiteral_(msg);
+  }
+
+  var identifiers = os.tokenToIdentifiers(token);
+  var parts = false;
+  var buffer = [];
+  var output = null;
+  for (var i = 0; i < identifiers.length; i++) {
+    var iden = identifiers[i];
+    parts = os.breakUpParens(iden);
+    if (!parts) {
+      if (i == identifiers.length - 1) {
+        output = os.wrapSingleIdentifier(iden, output, opt_default);
+      } else {
+        output = os.wrapSingleIdentifier(iden, output);
+      }
+    } else {
+      buffer.length = 0;
+      buffer.push(os.wrapSingleIdentifier(parts[0], output));
+      for (var j = 1; j < parts.length; j += 3) {
+        buffer.push(parts[j]);
+        if (parts[j + 1]) {
+          buffer.push(os.wrapIdentifiersInExpression(parts[j + 1]));
+        }
+        buffer.push(parts[j + 2]);
+      }
+      output = buffer.join('');
+    }
+  }
+  return output;
+};
+
+/**
+ * Wraps all identifiers in a JS expression. The expression is tokenized, then
+ * each token is wrapped individually.
+ * @param {string} expr The expression to wrap.
+ * @param {string=} opt_default An optional default value reference (such as the
+ * literal string 'null').
+ */
+os.wrapIdentifiersInExpression = function(expr, opt_default) {
+  var out = [];
+  var tokens = os.expressionToTokens(expr);
+  for (var i = 0; i < tokens.length; i++) {
+    out.push(os.wrapIdentifiersInToken(tokens[i], opt_default));
+  }
+  return out.join('');
+};
+
+/**
+ * Tokenizes a JS expression. Each token is either an operator, a literal
+ * string, an identifier, or a function call.
+ * For example,
+ *   "foo||bar" is tokenized as ["foo", "||", "bar"], but
+ *   "bing(foo||bar)" becomes   ["bing(foo||bar)"].
+ */
+os.expressionToTokens = function(expr) {
+  var tokens = [];
+  var inquotes = false;
+  var inidentifier = false;
+  var inparens = 0;
+  var escaped = false;
+  var quotestart = null;
+  var buffer = [];
+  for (var i = 0; i < expr.length; i++) {
+    var ch = expr.charAt(i);
+    if (inquotes) {
+      if (!escaped && ch == quotestart) {
+        inquotes = false;
+      } else if (ch == '\\') {
+        escaped = true;
+      } else {
+        escaped = false;
+      }
+      buffer.push(ch);
+    } else {
+      if (ch == "'" || ch == '"') {
+        inquotes = true;
+        quotestart = ch;
+        buffer.push(ch);
+        continue;
+      }
+      if (ch == '(') {
+        inparens++;
+      } else if (ch == ')' && inparens > 0) {
+        inparens--;
+      }
+      if (inparens > 0) {
+        buffer.push(ch);
+        continue;
+      }
+      if (!inidentifier && os.canStartIdentifier(ch)) {
+        if (buffer.length > 0) {
+          tokens.push(buffer.join(''));
+          buffer.length = 0;
+        }
+        inidentifier = true;
+        buffer.push(ch);
+        continue;
+      }
+      if (inidentifier) {
+        if (os.canBeInToken(ch)) {
+          buffer.push(ch);
+        } else {
+          tokens.push(buffer.join(''));
+          buffer.length = 0;
+          inidentifier = false;
+          buffer.push(ch);
+        }
+      } else {
+        buffer.push(ch);
+      }
+    }
+  }
+  tokens.push(buffer.join(''));
+  return tokens;
+};
+
+/**
+ * Breaks up a JS token into identifiers, separated by '.'
+ * "foo.bar" becomes ["foo", "bar"].
+ */
+os.tokenToIdentifiers = function(token) {
+  var inquotes = false;
+  var quotestart = null;
+  var escaped = false;
+  var buffer = [];
+  var identifiers = [];
+  for (var i = 0; i < token.length; i++) {
+    var ch = token.charAt(i);
+    if (inquotes) {
+      if (!escaped && ch == quotestart) {
+        inquotes = false;
+      } else if (ch == '\\') {
+        escaped = true;
+      } else {
+        escaped = false;
+      }
+      buffer.push(ch);
+      continue;
+    } else {
+      if (ch == "'" || ch == '"') {
+        buffer.push(ch);
+        inquotes = true;
+        quotestart = ch;
+        continue;
+      }
+    }
+    if (ch == '.' && !inquotes) {
+      identifiers.push(buffer.join(''));
+      buffer.length = 0;
+      continue;
+    }
+    buffer.push(ch);
+  }
+  identifiers.push(buffer.join(''));
+  return identifiers;
+};
+
+
+/**
+ * Checks if a JS identifier has parenthesis and bracket parts. If no such
+ * parts are found, return false. Otherwise, the expression is returned as
+ * an array of components:
+ *   "foo(bar)"       -> ["foo", "(", "bar", ")"]
+ *   "foo[bar](baz)"  -> ["foo", "[", "bar", "]", "(", "baz", ")"]
+ */
+os.breakUpParens = function(identifier) {
+  var parenIndex = identifier.indexOf('(');
+  var bracketIndex = identifier.indexOf('[');
+  if (parenIndex < 0 && bracketIndex < 0) {
+    return false;
+  }
+  var parts = [];
+  if (parenIndex < 0 || (bracketIndex >= 0 && bracketIndex < parenIndex)) {
+    parenIndex = 0;
+    parts.push(identifier.substring(0, bracketIndex));
+  } else {
+    bracketIndex = 0;
+    parts.push(identifier.substring(0, parenIndex));
+  }
+  var parenstart = null;
+  var inquotes = false;
+  var quotestart = null;
+  var parenlevel = 0;
+  var escaped = false;
+  var buffer = [];
+  for (var i = bracketIndex + parenIndex; i < identifier.length; i++) {
+    var ch = identifier.charAt(i);
+    if (inquotes) {
+      if (!escaped && ch == quotestart) {
+        inquotes = false;
+      } else if (ch == '\\') {
+        escaped = true;
+      } else {
+        escaped = false;
+      }
+      buffer.push(ch);
+    } else {
+      if (ch == "'" || ch == '"') {
+        inquotes = true;
+        quotestart = ch;
+        buffer.push(ch);
+        continue;
+      }
+      if (parenlevel == 0) {
+        if (ch == '(' || ch == '[') {
+          parenstart = ch;
+          parenlevel++;
+          parts.push(ch);
+          buffer.length = 0;
+        }
+      } else {
+        if ((parenstart == '(' && ch == ')') ||
+            (parenstart == '[' && ch == ']')) {
+          parenlevel--;
+          if (parenlevel == 0) {
+            parts.push(buffer.join(''));
+            parts.push(ch);
+          } else {
+            buffer.push(ch);
+          }
+        } else {
+          if (ch == parenstart) {
+            parenlevel++;
+          }
+          buffer.push(ch);
+        }
+      }
+    }
+  }
+  return parts;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/container.js b/trunk/features/src/main/javascript/features/opensocial-templates/container.js
new file mode 100644
index 0000000..7ddf7b3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/container.js
@@ -0,0 +1,514 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Standard methods invoked by containers to use the template API.
+ *
+ * Sample usage:
+ *  &lt;script type="text/os-template" tag="os:Button"&gt;
+ *    &lt;button onclick="alert('Clicked'); return false;"&gt;
+ *      &lt;os:renderAll/&gt;
+ *    &lt;/button&gt;
+ *  &lt;/script&gt;
+ *
+ *  &lt;script type="text/os-template"&gt;
+ *    &lt;os:Button&gt;
+ *      &lt;div&gt;Click me&lt;/div&gt;
+ *    &lt;/os:Button&gt;
+ *  &lt;/script&gt;
+ *
+ * os.Container.registerDocumentTemplates();
+ * os.Container.renderInlineTemplates();
+ */
+
+os.Container = {};
+
+/***
+ * @type {Array.<Object>} Array of registered inline templates.
+ * @private
+ */
+os.Container.inlineTemplates_ = [];
+
+/**
+ * @type {Array.<Function>} An array of callbacks to fire when the page DOM has
+ * loaded. This will be null until the first callback is added
+ * @see registerDomListener_
+ * @private
+ */
+os.Container.domLoadCallbacks_ = null;
+
+/**
+ * @type {boolean} A boolean flag determining wether the page DOM has loaded.
+ * @private
+ */
+os.Container.domLoaded_ = false;
+
+/**
+ * @type {number} The number of libraries needed to load.
+ * @private
+ */
+os.Container.requiredLibraries_ = 0;
+
+/**
+ * @type {boolean} Determines whether all templates are automatically processed.
+ * @private
+ */
+os.Container.autoProcess_ = true;
+
+/**
+ * @type {boolean} Has the document been processed already?
+ * @private
+ */
+os.Container.processed_ = false;
+
+os.Container.disableAutoProcessing = function() {
+  if (os.Container.processed_) {
+    throw Error('Document already processed.');
+  }
+  os.Container.autoProcess_ = false;
+};
+
+// Create reference from opensocial-templates.
+os.disableAutoProcessing = os.Container.disableAutoProcessing;
+
+/**
+ * Registers the DOM Load listener to fire when the page DOM is available.
+ * TODO: See if we can use gadgets.util.regiterOnLoadHandler() here.
+ * TODO: Currently for everything but Mozilla, this just registers an
+ * onLoad listener on the window. Should use DOMContentLoaded on Opera9,
+ * appropriate hacks (polling?) on IE and Safari.
+ * @private
+ */
+os.Container.registerDomLoadListener_ = function() {
+  var gadgets = window['gadgets'];
+  if (gadgets && gadgets.util) {
+    gadgets.util.registerOnLoadHandler(os.Container.onDomLoad_);
+  } else if (typeof(navigator) != 'undefined' && navigator.product &&
+      navigator.product == 'Gecko') {
+    window.addEventListener('DOMContentLoaded', os.Container.onDomLoad_, false);
+  } if (window.addEventListener) {
+    window.addEventListener('load', os.Container.onDomLoad_, false);
+  } else {
+    if (!document.body) {
+      setTimeout(arguments.callee, 0);
+      return;
+    }
+    var oldOnLoad = window.onload || function() {};
+    window.onload = function() {
+      oldOnLoad();
+      os.Container.onDomLoad_();
+    };
+  }
+};
+
+/**
+ * To be called when the page DOM is available - will fire all the callbacks
+ * in os.Container.domLoadCallbacks_.
+ * @private
+ */
+os.Container.onDomLoad_ = function() {
+  if (os.Container.domLoaded_) {
+    return;
+  }
+  for (var i = 0; i < os.Container.domLoadCallbacks_.length; i++) {
+    try {
+      os.Container.domLoadCallbacks_[i]();
+    } catch (e) {
+      os.log(e);
+    }
+  }
+  os.Container.domLoaded_ = true;
+};
+
+/**
+ * Adds a callback to be fired when the page DOM is available. If the page
+ * is already loaded, the callback will execute asynchronously.
+ * @param {Function} callback The callback to be fired when DOM is loaded.
+ */
+os.Container.executeOnDomLoad = function(callback) {
+  if (os.Container.domLoaded_) {
+    setTimeout(callback, 0);
+  } else {
+    if (os.Container.domLoadCallbacks_ == null) {
+      os.Container.domLoadCallbacks_ = [];
+      os.Container.registerDomLoadListener_();
+    }
+    os.Container.domLoadCallbacks_.push(callback);
+  }
+};
+
+/**
+ * Compiles and registers all DOM elements in the document. Templates are
+ * registered as tags if they specify their name with the "tag" attribute
+ * and as templates if they have a name (or id) attribute.
+ * @param {Object=} opt_doc Optional document to use rather than the global doc.
+ */
+os.Container.registerDocumentTemplates = function(opt_doc) {
+  var doc = opt_doc || document;
+  var nodes = doc.getElementsByTagName(os.Container.TAG_script_);
+  for (var i = 0; i < nodes.length; ++i) {
+    var node = nodes[i];
+    if (os.Container.isTemplateType_(node.type)) {
+      var tag = node.getAttribute('tag');
+      if (tag) {
+        os.Container.registerTagElement_(node, tag);
+      } else if (node.getAttribute('name')) {
+        os.Container.registerTemplateElement_(node, node.getAttribute('name'));
+      }
+    }
+  }
+};
+
+/**
+ * Compiles and registers all unnamed templates in the document.
+ * @param {Object=} opt_data Optional JSON data.
+ * @param {Object=} opt_doc Optional document to use instead of window.document.
+ */
+os.Container.compileInlineTemplates = function(opt_data, opt_doc) {
+  var doc = opt_doc || document;
+  var nodes = doc.getElementsByTagName(os.Container.TAG_script_);
+  for (var i = 0; i < nodes.length; ++i) {
+    var node = nodes[i];
+    if (os.Container.isTemplateType_(node.type)) {
+      var name = node.getAttribute('tag');
+      if (!name || name.length < 0) {
+        var template = os.compileTemplate(node, name);
+        if (template) {
+          os.Container.inlineTemplates_.push(
+              {'template': template, 'node': node});
+        } else {
+          os.warn('Failed compiling inline template.');
+        }
+      }
+    }
+  }
+};
+
+/**
+ * @return {JsEvalContext} the default rendering context to use - this will
+ * contain all available data.
+ */
+os.Container.getDefaultContext = function() {
+  if ((window['gadgets'] && gadgets.util.hasFeature('opensocial-data')) ||
+      (opensocial.data.getDataContext)) {
+    return os.createContext(opensocial.data.getDataContext().getData());
+  }
+  return os.createContext({});
+};
+
+/**
+ * Renders any registered inline templates.
+ * @param {Object=} opt_doc Optional document to use instead of window.document.
+ */
+os.Container.renderInlineTemplates = function(opt_doc) {
+  var doc = opt_doc || document;
+  var context = os.Container.getDefaultContext();
+  var inlined = os.Container.inlineTemplates_;
+  for (var i = 0; i < inlined.length; ++i) {
+    var template = inlined[i].template;
+    var node = inlined[i].node;
+    var id = '_T_' + template.id;
+    var rendered = true;
+    var el = doc.getElementById(id);
+    if (!el) {
+      el = doc.createElement('div');
+      el.setAttribute('id', id);
+      node.parentNode.insertBefore(el, node);
+      rendered = false;
+    }
+
+    // Only honor @before and @require attributes if the opensocial-data
+    // feature is present.
+    if ((window['gadgets'] && gadgets.util.hasFeature('opensocial-data')) ||
+        (opensocial.data.DataContext)) {
+      var beforeData = node.getAttribute('before') ||
+          node.getAttribute('beforeData');
+      if (beforeData) {
+        // Automatically hide this template when specified data is available.
+        var keys = beforeData.split(/[\, ]+/);
+        opensocial.data.DataContext.registerListener(keys,
+            os.Container.createHideElementClosure(el));
+      }
+
+      var requiredData = node.getAttribute('require') ||
+          node.getAttribute('requireData');
+      if (requiredData) {
+        // This template will render when the specified data is available.
+        var keys = requiredData.split(/[\, ]+/);
+        var callback = os.Container.createRenderClosure(template, el);
+        if ('true' == node.getAttribute('autoUpdate')) {
+          if (rendered) {
+            opensocial.data.getDataContext().registerDeferredListener_(keys, callback);
+          } else {
+            opensocial.data.getDataContext().registerListener(keys, callback);
+          }
+        } else {
+          opensocial.data.getDataContext().registerOneTimeListener_(keys, callback);
+        }
+      } else {
+        template.renderInto(el, null, context);
+      }
+    } else {
+      template.renderInto(el, null, context);
+    }
+  }
+};
+
+/**
+* Creates a closure that will render the a template into an element with
+* optional data.
+* @param {Object} template The template object to use.
+* @param {Element} element The DOM element to inject the template into.
+* @param {Object=} opt_data Optional data to be used as to create a context.
+* @param {Object=} opt_context Optional pre-constructed rendering context.
+* @return {Function} The constructed closure.
+* TODO(davidbyttow): Move this into util.js.
+*/
+os.Container.createRenderClosure = function(template, element, opt_data,
+    opt_context) {
+  var closure = function() {
+    var context = opt_context;
+    var data = opt_data;
+    if (!context) {
+      if (data) {
+        context = os.createContext(data);
+      } else {
+        context = os.Container.getDefaultContext();
+        data = context.data_;
+      }
+    }
+    template.renderInto(element, data, context);
+  };
+  return closure;
+};
+
+/**
+ * Creates a closure that will hide a DOM element.
+ * @param {Element} element The DOM element to inject the template into.
+ * @return {Function} The constructed closure.
+ * TODO(davidbyttow): Move this into util.js.
+ */
+os.Container.createHideElementClosure = function(element) {
+  var closure = function() {
+    displayNone(element);
+  };
+  return closure;
+};
+
+/**
+ * Compiles and registers a template from a DOM element.
+ * @param {string} elementId Id of DOM element from which to create a template.
+ * @return {Object} The compiled and registered template object.
+ */
+os.Container.registerTemplate = function(elementId) {
+  var element = document.getElementById(elementId);
+  return os.Container.registerTemplateElement_(element);
+};
+
+/**
+ * Registers a custom tag from a namespaced DOM element.
+ * @param {string} elementId Id of the DOM element to register.
+ */
+os.Container.registerTag = function(elementId) {
+  var element = document.getElementById(elementId);
+  os.Container.registerTagElement_(element, elementId);
+};
+
+/**
+ * Renders a DOM element with a specified template and contextual data.
+ * @param {string} elementId Id of DOM element to inject into.
+ * @param {string} templateId Id of the template.
+ * @param {Object=} opt_data Data to supply to template.
+ */
+os.Container.renderElement = function(elementId, templateId, opt_data) {
+  var template = os.getTemplate(templateId);
+  if (template) {
+    var element = document.getElementById(elementId);
+    if (element) {
+      template.renderInto(element, opt_data);
+    } else {
+      os.warn('Element (' + elementId + ') not found to render into.');
+    }
+  } else {
+    os.warn('Template (' + templateId + ') not registered.');
+  }
+};
+
+/**
+ * Compiles and renders all inline templates.
+ * @param {Object=} opt_doc Optional document to use instead of window.document.
+ */
+os.Container.processInlineTemplates = function(opt_doc) {
+  os.Container.compileInlineTemplates(opt_doc);
+  os.Container.renderInlineTemplates(opt_doc);
+};
+
+/**
+ * Process the gadget configuration when it is available.
+ */
+os.Container.processGadget = function() {
+  if (!window['gadgets']) {
+    return;
+  }
+
+  // Honor the "disableAutoProcessing" feature param.
+  var params = gadgets.util.getFeatureParameters('opensocial-templates');
+  if (!params) {
+    return;
+  }
+  if (params.disableAutoProcessing &&
+      params.disableAutoProcessing.toLowerCase != 'false') {
+    os.Container.autoProcess_ = false;
+  }
+
+  // Honor the "requireLibrary" feature param(s).
+  if (params.requireLibrary) {
+    if (typeof params.requireLibrary == 'string') {
+      os.Container.addRequiredLibrary(params.requireLibrary);
+    } else {
+      for (var i = 0; i < params.requireLibrary.length; i++) {
+        os.Container.addRequiredLibrary(params.requireLibrary[i]);
+      }
+    }
+  }
+};
+
+//Process the gadget when the page loads.
+os.Container.executeOnDomLoad(os.Container.processGadget);
+
+/**
+ * A flag to determine if auto processing is waiting for libraries to load.
+ * @type {boolean}
+ * @private
+ */
+os.Container.processWaitingForLibraries_ = false;
+
+/**
+ * Utility method which will automatically register all templates
+ * and render all that are inline.
+ * @param {Object=} opt_data Optional JSON object to render templates against.
+ * @param {Document=} opt_doc Optional document to use instead of window.document.
+ */
+os.Container.processDocument = function(opt_data, opt_doc) {
+  if (os.Container.requiredLibraries_ > 0) {
+    os.Container.processWaitingForLibraries_ = true;
+    return;
+  }
+  os.Container.processWaitingForLibraries_ = false;
+  os.Container.registerDocumentTemplates(opt_doc);
+  os.Container.processInlineTemplates(opt_data, opt_doc);
+  os.Container.processed_ = true;
+};
+
+// Expose function in opensocial.template namespace.
+os.process = os.Container.processDocument;
+
+// Process the document when the page loads - unless requested not to.
+os.Container.executeOnDomLoad(function() {
+  if (os.Container.autoProcess_) {
+    os.Container.processDocument();
+  }
+});
+
+/**
+ * A handler called when one of the required libraries loads.
+ * @private
+ */
+os.Container.onLibraryLoad_ = function() {
+  if (os.Container.requiredLibraries_ > 0) {
+    os.Container.requiredLibraries_--;
+    if (os.Container.requiredLibraries_ == 0 &&
+        os.Container.processWaitingForLibraries_) {
+      os.Container.processDocument();
+    }
+  }
+};
+
+/**
+ * Adds a required library - the processing will be deferred until all
+ * required libraries have loaded.
+ * @param {string} libUrl The URL of the library needed to process this page.
+ */
+os.Container.addRequiredLibrary = function(libUrl) {
+  os.Container.requiredLibraries_++;
+  os.Loader.loadUrl(libUrl, os.Container.onLibraryLoad_);
+};
+
+/**
+ * @type {string} Tag name of a template.
+ * @private
+ */
+os.Container.TAG_script_ = 'script';
+
+/**
+ * @type {Object} Map of allowed template content types.
+ * @private
+ * TODO(davidbyttow): Remove text/template.
+ */
+os.Container.templateTypes_ = {};
+os.Container.templateTypes_['text/os-template'] = true;
+os.Container.templateTypes_['text/template'] = true;
+
+/**
+ * Checks if a given type name is properly named as a template.
+ * @param {string} typeName Name of a given type.
+ * @return {boolean} This type is considered a template.
+ * @private
+ */
+os.Container.isTemplateType_ = function(typeName) {
+  return os.Container.templateTypes_[typeName] != null;
+};
+
+/**
+ * Compiles and registers a template from a DOM element.
+ * @param {Element} element DOM element from which to create a template.
+ * @param {string=} opt_id Optional id for template.
+ * @return {Object} The compiled and registered template object.
+ * @private
+ */
+os.Container.registerTemplateElement_ = function(element, opt_id) {
+  var template = os.compileTemplate(element, opt_id);
+  if (template) {
+    os.registerTemplate(template);
+  } else {
+    os.warn('Could not compile template (' + element.id + ')');
+  }
+  return template;
+};
+
+/**
+ * Registers a custom tag from a namespaced DOM element.
+ * @param {Element} element DOM element to register.
+ * @param {string} name Name of the tag.
+ * @private
+ */
+os.Container.registerTagElement_ = function(element, name) {
+  var template = os.Container.registerTemplateElement_(element, name);
+  if (template) {
+    var tagParts = name.split(':');
+    // Only register custom tags of the "ns:Tag" format.
+    if (tagParts.length == 2) {
+      var nsObj = os.getNamespace(tagParts[0]);
+      if (!nsObj) {
+        // Auto Create a namespace for lazy registration.
+        nsObj = os.createNamespace(tagParts[0], null);
+      }
+      nsObj[tagParts[1]] = os.createTemplateCustomTag(template);
+    }
+  }
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/feature.xml b/trunk/features/src/main/javascript/features/opensocial-templates/feature.xml
new file mode 100644
index 0000000..8e997ea
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/feature.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+
+TODO(davidbyttow): In order to use this feature, you must specify
+tell the content-rewrite feature to allow tags. Otherwise,
+<script type="text/os-template"> will be parsed as JS.
+  E.g.,
+  <Optional feature="content-rewrite">
+    <Param name="include-tags"/>
+  </Optional>
+
+TODO(davidbyttow): Implement local versions of jstemplate.
+
+N.B.: Currently the jstemplate library is served directly from googlecode.
+This should not be used in a production setting.
+-->
+<feature>
+  <name>opensocial-templates</name>
+  <dependency>globals</dependency>
+  <dependency>opensocial-data-context</dependency>
+  <dependency>jsondom</dependency>
+  <dependency>security-token</dependency>
+  <dependency>xmlutil</dependency>
+  <gadget>
+    <script src="jsTemplate/util.js"></script>
+    <script src="jsTemplate/jsevalcontext.js"></script>
+    <script src="jsTemplate/jstemplate.js"></script>
+    <script src="base.js"></script>
+    <script src="namespaces.js"></script>
+    <script src="util.js"></script>
+    <script src="template.js"></script>
+    <script src="compiler.js"></script>
+    <script src="loader.js"></script>
+    <script src="container.js"></script>
+    <script src="os.js"></script>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/jsevalcontext.js b/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/jsevalcontext.js
new file mode 100644
index 0000000..eaf5206
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/jsevalcontext.js
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Author: Steffen Meschkat <mesch@google.com>
+ *
+ * @fileoverview This class is used to evaluate expressions in a local
+ * context. Used by JstProcessor.
+ */
+
+
+/**
+ * Names of special variables defined by the jstemplate evaluation
+ * context. These can be used in js expression in jstemplate
+ * attributes.
+ */
+var VAR_index = 'Index';
+var VAR_count = 'Count';
+var VAR_this = '$this';
+var VAR_context = '$context';
+var VAR_top = '$top';
+var VAR_loop = '$loop';
+
+
+/**
+ * The name of the global variable which holds the value to be returned if
+ * context evaluation results in an error.
+ * Use JsEvalContext.setGlobal(GLOB_default, value) to set this.
+ */
+var GLOB_default = '$default';
+
+
+/**
+ * Un-inlined literals, to avoid object creation in IE6. TODO:
+ * So far, these are only used here, but we could use them thoughout
+ * the code and thus move them to constants.js.
+ */
+var CHAR_colon = ':';
+var REGEXP_semicolon = /\s*;\s*/;
+
+
+/**
+ * See constructor_()
+ * @param {Object|null=} opt_data
+ * @param {Object=} opt_parent
+ * @constructor
+ */
+function JsEvalContext(opt_data, opt_parent) {
+  this.constructor_.apply(this, arguments);
+}
+
+/**
+ * Context for processing a jstemplate. The context contains a context
+ * object, whose properties can be referred to in jstemplate
+ * expressions, and it holds the locally defined variables.
+ *
+ * @param {Object|null=} opt_data The context object. Null if no context.
+ *
+ * @param {Object=} opt_parent The parent context, from which local
+ * variables are inherited. Normally the context object of the parent
+ * context is the object whose property the parent object is. Null for the
+ * context of the root object.
+ * @private
+ */
+JsEvalContext.prototype.constructor_ = function(opt_data, opt_parent) {
+  var me = this;
+
+  /**
+   * The context for variable definitions in which the jstemplate
+   * expressions are evaluated. Other than for the local context,
+   * which replaces the parent context, variable definitions of the
+   * parent are inherited. The special variable $this points to data_.
+   *
+   * If this instance is recycled from the cache, then the property is
+   * already initialized.
+   *
+   * @type {Object}
+   */
+  if (!me.vars_) {
+    me.vars_ = {};
+  }
+  if (opt_parent) {
+    // If there is a parent node, inherit local variables from the
+    // parent.
+    copyProperties(me.vars_, opt_parent.vars_);
+  } else {
+    // If a root node, inherit global symbols. Since every parent
+    // chain has a root with no parent, global variables will be
+    // present in the case above too. This means that globals can be
+    // overridden by locals, as it should be.
+    copyProperties(me.vars_, JsEvalContext.globals_);
+  }
+
+  /**
+   * The current context object is assigned to the special variable
+   * $this so it is possible to use it in expressions.
+   * @type {Object}
+   */
+  me.vars_[VAR_this] = opt_data;
+
+  /**
+   * The entire context structure is exposed as a variable so it can be
+   * passed to javascript invocations through jseval.
+   */
+  me.vars_[VAR_context] = me;
+
+  /**
+   * The local context of the input data in which the jstemplate
+   * expressions are evaluated. Notice that this is usually an Object,
+   * but it can also be a scalar value (and then still the expression
+   * $this can be used to refer to it). Notice this can even be value,
+   * undefined or null. Hence, we have to protect jsexec() from using
+   * undefined or null, yet we want $this to reflect the true value of
+   * the current context. Thus we assign the original value to $this,
+   * above, but for the expression context we replace null and
+   * undefined by the empty string.
+   *
+   * @type {?Object}
+   */
+  me.data_ = getDefaultObject(opt_data, STRING_empty);
+
+  if (!opt_parent) {
+    // If this is a top-level context, create a variable reference to the data
+    // to allow for  accessing top-level properties of the original context
+    // data from child contexts.
+    me.vars_[VAR_top] = me.data_;
+  }
+};
+
+
+/**
+ * A map of globally defined symbols. Every instance of JsExprContext
+ * inherits them in its vars_.
+ * @type {Object}
+ * @private
+ */
+JsEvalContext.globals_ = {};
+
+
+/**
+ * Sets a global symbol. It will be available like a variable in every
+ * JsEvalContext instance. This is intended mainly to register
+ * immutable global objects, such as functions, at load time, and not
+ * to add global data at runtime. I.e. the same objections as to
+ * global variables in general apply also here. (Hence the name
+ * "global", and not "global var".)
+ * @param {string} name
+ * @param {?Object} value
+ */
+JsEvalContext.setGlobal = function(name, value) {
+  JsEvalContext.globals_[name] = value;
+};
+
+
+/**
+ * Set the default value to be returned if context evaluation results in an
+ * error. (This can occur if a non-existent value was requested).
+ */
+JsEvalContext.setGlobal(GLOB_default, null);
+
+
+/**
+ * A cache to reuse JsEvalContext instances. (IE6 perf)
+ *
+ * @type {Array.<JsEvalContext>}
+ * @private
+ */
+JsEvalContext.recycledInstances_ = [];
+
+
+/**
+ * A factory to create a JsEvalContext instance, possibly reusing
+ * one from recycledInstances_. (IE6 perf)
+ *
+ * @param {Object=} opt_data
+ * @param {JsEvalContext=} opt_parent
+ * @return {JsEvalContext}
+ */
+JsEvalContext.create = function(opt_data, opt_parent) {
+  if (jsLength(JsEvalContext.recycledInstances_) > 0) {
+    var instance = JsEvalContext.recycledInstances_.pop();
+    JsEvalContext.call(instance, opt_data, opt_parent);
+    return instance;
+  } else {
+    return new JsEvalContext(opt_data, opt_parent);
+  }
+};
+
+
+/**
+ * Recycle a used JsEvalContext instance, so we can avoid creating one
+ * the next time we need one. (IE6 perf)
+ *
+ * @param {JsEvalContext} instance
+ */
+JsEvalContext.recycle = function(instance) {
+  for (var i in instance.vars_) {
+    // NOTE: We avoid object creation here. (IE6 perf)
+    delete instance.vars_[i];
+  }
+  instance.data_ = null;
+  JsEvalContext.recycledInstances_.push(instance);
+};
+
+
+/**
+ * Executes a function created using jsEvalToFunction() in the context
+ * of vars, data, and template.
+ *
+ * @param {Function} exprFunction A javascript function created from
+ * a jstemplate attribute value.
+ *
+ * @param {Element} template DOM node of the template.
+ *
+ * @return {?Object} The value of the expression from which
+ * exprFunction was created in the current js expression context and
+ * the context of template.
+ */
+JsEvalContext.prototype.jsexec = function(exprFunction, template) {
+  try {
+    return exprFunction.call(template, this.vars_, this.data_);
+  } catch (e) {
+    log('jsexec EXCEPTION: ' + e + ' at ' + template +
+        ' with ' + exprFunction);
+    return JsEvalContext.globals_[GLOB_default];
+  }
+};
+
+
+/**
+ * Clones the current context for a new context object. The cloned
+ * context has the data object as its context object and the current
+ * context as its parent context. It also sets the $index variable to
+ * the given value. This value usually is the position of the data
+ * object in a list for which a template is instantiated multiply.
+ *
+ * @param {Object} data The new context object.
+ *
+ * @param {number} index Position of the new context when multiply
+ * instantiated. (See implementation of jstSelect().).
+ *
+ * @param {number} count The total number of contexts that were multiply
+ * instantiated. (See implementation of jstSelect().).
+ *
+ * @return {JsEvalContext}
+ */
+JsEvalContext.prototype.clone = function(data, index, count) {
+  var ret = JsEvalContext.create(data, this);
+  if (typeof(index) == 'number' || typeof(count) == 'number') {
+    var loopContext = {};
+    loopContext[VAR_index] = index;
+    loopContext[VAR_count] = count;
+    ret.setVariable(VAR_loop, loopContext);
+  }
+  return ret;
+};
+
+
+/**
+ * Binds a local variable to the given value. If set from jstemplate
+ * jsvalue expressions, variable names must start with $, but in the
+ * API they only have to be valid javascript identifier.
+ *
+ * @param {string} name
+ *
+ * @param {?Object} value
+ */
+JsEvalContext.prototype.setVariable = function(name, value) {
+  this.vars_[name] = value;
+};
+
+
+/**
+ * Returns the value bound to the local variable of the given name, or
+ * undefined if it wasn't set. There is no way to distinguish a
+ * variable that wasn't set from a variable that was set to
+ * undefined. Used mostly for testing.
+ *
+ * @param {string} name
+ *
+ * @return {?Object} value.
+ */
+JsEvalContext.prototype.getVariable = function(name) {
+  return this.vars_[name];
+};
+
+
+/**
+ * Evaluates a string expression within the scope of this context
+ * and returns the result.
+ *
+ * @param {string} expr A javascript expression.
+ * @param {Element=} opt_template An optional node to serve as "this".
+ *
+ * @return {?Object} value.
+ */
+JsEvalContext.prototype.evalExpression = function(expr, opt_template) {
+  var exprFunction = jsEvalToFunction(expr);
+  return this.jsexec(exprFunction, opt_template);
+};
+
+
+/**
+ * Uninlined string literals for jsEvalToFunction() (IE6 perf).
+ */
+var STRING_a = 'a_';
+var STRING_b = 'b_';
+var STRING_with = 'with (a_) with (b_) return ';
+
+
+/**
+ * Cache for jsEvalToFunction results.
+ * @type {Object}
+ * @private
+ */
+JsEvalContext.evalToFunctionCache_ = {};
+
+
+/**
+ * Evaluates the given expression as the body of a function that takes
+ * vars and data as arguments. Since the resulting function depends
+ * only on expr, we cache the result so we save some Function
+ * invocations, and some object creations in IE6.
+ *
+ * @param {string} expr A javascript expression.
+ *
+ * @return {Function} A function that returns the value of expr in the
+ * context of vars and data.
+ */
+function jsEvalToFunction(expr) {
+  if (!JsEvalContext.evalToFunctionCache_[expr]) {
+    try {
+      // NOTE: The Function constructor is faster than eval().
+      JsEvalContext.evalToFunctionCache_[expr] =
+          new Function(STRING_a, STRING_b, STRING_with + expr);
+    } catch (e) {
+      log('jsEvalToFunction (' + expr + ') EXCEPTION ' + e);
+    }
+  }
+  return JsEvalContext.evalToFunctionCache_[expr];
+}
+
+
+/**
+ * Evaluates the given expression to itself. This is meant to pass
+ * through string attribute values.
+ *
+ * @param {string} expr
+ *
+ * @return {string}
+ */
+function jsEvalToSelf(expr) {
+  return expr;
+}
+
+
+/**
+ * Parses the value of the jsvalues attribute in jstemplates: splits
+ * it up into a map of labels and expressions, and creates functions
+ * from the expressions that are suitable for execution by
+ * JsEvalContext.jsexec(). All that is returned as a flattened array
+ * of pairs of a String and a Function.
+ *
+ * @param {string} expr
+ *
+ * @return {Array}
+ */
+function jsEvalToValues(expr) {
+  // TODO: It is insufficient to split the values by simply
+  // finding semi-colons, as the semi-colon may be part of a string
+  // constant or escaped.
+  var ret = [];
+  var values = expr.split(REGEXP_semicolon);
+  for (var i = 0, I = jsLength(values); i < I; ++i) {
+    var colon = values[i].indexOf(CHAR_colon);
+    if (colon < 0) {
+      continue;
+    }
+    var label = stringTrim(values[i].substr(0, colon));
+    var value = jsEvalToFunction(values[i].substr(colon + 1));
+    ret.push(label, value);
+  }
+  return ret;
+}
+
+
+/**
+ * Parses the value of the jseval attribute of jstemplates: splits it
+ * up into a list of expressions, and creates functions from the
+ * expressions that are suitable for execution by
+ * JsEvalContext.jsexec(). All that is returned as an Array of
+ * Function.
+ *
+ * @param {string} expr
+ *
+ * @return {Array.<Function>}
+ */
+function jsEvalToExpressions(expr) {
+  var ret = [];
+  var values = expr.split(REGEXP_semicolon);
+  for (var i = 0, I = jsLength(values); i < I; ++i) {
+    if (values[i]) {
+      var value = jsEvalToFunction(values[i]);
+      ret.push(value);
+    }
+  }
+  return ret;
+}
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/jstemplate.js b/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/jstemplate.js
new file mode 100644
index 0000000..7e72069
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/jstemplate.js
@@ -0,0 +1,1056 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Author: Steffen Meschkat <mesch@google.com>
+ *
+ * @fileoverview A simple formatter to project JavaScript data into
+ * HTML templates. The template is edited in place. I.e. in order to
+ * instantiate a template, clone it from the DOM first, and then
+ * process the cloned template. This allows for updating of templates:
+ * If the templates is processed again, changed values are merely
+ * updated.
+ *
+ * NOTE: IE DOM doesn't have importNode().
+ *
+ * NOTE: The property name "length" must not be used in input
+ * data, see comment in jstSelect_().
+ */
+
+
+/**
+ * Names of jstemplate attributes. These attributes are attached to
+ * normal HTML elements and bind expression context data to the HTML
+ * fragment that is used as template.
+ */
+var ATT_select = 'jsselect';
+var ATT_instance = 'jsinstance';
+var ATT_display = 'jsdisplay';
+var ATT_values = 'jsvalues';
+var ATT_vars = 'jsvars';
+var ATT_eval = 'jseval';
+var ATT_transclude = 'transclude';
+var ATT_content = 'jscontent';
+var ATT_skip = 'jsskip';
+var ATT_innerselect = 'jsinnerselect';
+
+
+/**
+ * Name of the attribute that caches a reference to the parsed
+ * template processing attribute values on a template node.
+ */
+var ATT_jstcache = 'jstcache';
+
+
+/**
+ * Name of the property that caches the parsed template processing
+ * attribute values on a template node.
+ */
+var PROP_jstcache = '__jstcache';
+
+
+/**
+ * ID of the element that contains dynamically loaded jstemplates.
+ */
+var STRING_jsts = 'jsts';
+
+
+/**
+ * Un-inlined string literals, to avoid object creation in
+ * IE6.
+ */
+var CHAR_asterisk = '*';
+var CHAR_dollar = '$';
+var CHAR_period = '.';
+var CHAR_ampersand = '&';
+var STRING_div = 'div';
+var STRING_id = 'id';
+var STRING_asteriskzero = '*0';
+var STRING_zero = '0';
+
+
+/**
+ * HTML template processor. Data values are bound to HTML templates
+ * using the attributes transclude, jsselect, jsdisplay, jscontent,
+ * jsvalues. The template is modifed in place. The values of those
+ * attributes are JavaScript expressions that are evaluated in the
+ * context of the data object fragment.
+ *
+ * @param {JsEvalContext} context Context created from the input data
+ * object.
+ *
+ * @param {Element} template DOM node of the template. This will be
+ * processed in place. After processing, it will still be a valid
+ * template that, if processed again with the same data, will remain
+ * unchanged.
+ *
+ * @param {boolean=} opt_debugging Optional flag to collect debugging
+ *     information while processing the template.  Only takes effect
+ *     in MAPS_DEBUG.
+ */
+function jstProcess(context, template, opt_debugging) {
+  var processor = new JstProcessor;
+  if (MAPS_DEBUG && opt_debugging) {
+    processor.setDebugging(opt_debugging);
+  }
+  JstProcessor.prepareTemplate_(template);
+
+  /**
+   * Caches the document of the template node, so we don't have to
+   * access it through ownerDocument.
+   * @type {Document}
+   */
+  processor.document_ = ownerDocument(template);
+
+  processor.run_(bindFully(processor, processor.jstProcessOuter_,
+                           context, template));
+  if (MAPS_DEBUG && opt_debugging) {
+    log('jstProcess:' + '\n' + processor.getLogs().join('\n'));
+  }
+}
+
+
+/**
+ * Internal class used by jstemplates to maintain context.  This is
+ * necessary to process deep templates in Safari which has a
+ * relatively shallow maximum recursion depth of 100.
+ * @class
+ * @constructor
+ */
+function JstProcessor() {
+  if (MAPS_DEBUG) {
+    /**
+     * An array of logging messages.  These are collected during processing
+     * and dumped to the console at the end.
+     * @type {Array.<string>}
+     * @private
+     */
+    this.logs_ = [];
+  }
+}
+
+
+/**
+ * Counter to generate node ids. These ids will be stored in
+ * ATT_jstcache and be used to lookup the preprocessed js attributes
+ * from the jstcache_. The id is stored in an attribute so it
+ * suvives cloneNode() and thus cloned template nodes can share the
+ * same cache entry.
+ * @type {number}
+ * @private
+ */
+JstProcessor.jstid_ = 0;
+
+
+/**
+ * Map from jstid to processed js attributes.
+ * @type {Object}
+ * @private
+ */
+JstProcessor.jstcache_ = {};
+
+/**
+ * The neutral cache entry. Used for all nodes that don't have any
+ * jst attributes. We still set the jsid attribute on those nodes so
+ * we can avoid to look again for all the other jst attributes that
+ * aren't there. Remember: not only the processing of the js
+ * attribute values is expensive and we thus want to cache it. The
+ * access to the attributes on the Node in the first place is
+ * expensive too.
+ */
+JstProcessor.jstcache_[0] = {};
+
+
+/**
+ * Map from concatenated attribute string to jstid.
+ * The key is the concatenation of all jst atributes found on a node
+ * formatted as "name1=value1&name2=value2&...", in the order defined by
+ * JST_ATTRIBUTES. The value is the id of the jstcache_ entry that can
+ * be used for this node. This allows the reuse of cache entries in cases
+ * when a cached entry already exists for a given combination of attribute
+ * values. (For example when two different nodes in a template share the same
+ * JST attributes.)
+ * @type {Object}
+ * @private
+ */
+JstProcessor.jstcacheattributes_ = {};
+
+
+/**
+ * Map for storing temporary attribute values in prepareNode_() so they don't
+ * have to be retrieved twice. (IE6 perf)
+ * @type {Object}
+ * @private
+ */
+JstProcessor.attributeValues_ = {};
+
+
+/**
+ * A list for storing non-empty attributes found on a node in prepareNode_().
+ * The array is global since it can be reused - this way there is no need to
+ * construct a new array object for each invocation. (IE6 perf)
+ * @type {Array}
+ * @private
+ */
+JstProcessor.attributeList_ = [];
+
+
+/**
+ * Prepares the template: preprocesses all jstemplate attributes.
+ *
+ * @param {Element} template
+ * @private
+ */
+JstProcessor.prepareTemplate_ = function(template) {
+  if (!template[PROP_jstcache]) {
+    domTraverseElements(template, function(node) {
+      JstProcessor.prepareNode_(node);
+    });
+  }
+};
+
+
+/**
+ * A list of attributes we use to specify jst processing instructions,
+ * and the functions used to parse their values.
+ *
+ * @type {Array.<Array>}
+ */
+var JST_ATTRIBUTES = [
+  [ATT_select, jsEvalToFunction],
+  [ATT_display, jsEvalToFunction],
+  [ATT_values, jsEvalToValues],
+  [ATT_vars, jsEvalToValues],
+  [ATT_eval, jsEvalToExpressions],
+  [ATT_transclude, jsEvalToSelf],
+  [ATT_content, jsEvalToFunction],
+  [ATT_skip, jsEvalToFunction],
+  [ATT_innerselect, jsEvalToFunction]
+];
+
+
+/**
+ * Prepares a single node: preprocesses all template attributes of the
+ * node, and if there are any, assigns a jsid attribute and stores the
+ * preprocessed attributes under the jsid in the jstcache.
+ *
+ * @param {Element} node
+ *
+ * @return {Object} The jstcache entry. The processed jst attributes
+ * are properties of this object. If the node has no jst attributes,
+ * returns an object with no properties (the jscache_[0] entry).
+ * @private
+ */
+JstProcessor.prepareNode_ = function(node) {
+  // If the node already has a cache property, return it.
+  if (node[PROP_jstcache]) {
+    return node[PROP_jstcache];
+  }
+
+  // If it is not found, we always set the PROP_jstcache property on the node.
+  // Accessing the property is faster than executing getAttribute(). If we
+  // don't find the property on a node that was cloned in jstSelect_(), we
+  // will fall back to check for the attribute and set the property
+  // from cache.
+
+  // If the node has an attribute indexing a cache object, set it as a property
+  // and return it.
+  var jstid = domGetAttribute(node, ATT_jstcache);
+  if (jstid != null) {
+    return node[PROP_jstcache] = JstProcessor.jstcache_[jstid];
+  }
+
+  var attributeValues = JstProcessor.attributeValues_;
+  var attributeList = JstProcessor.attributeList_;
+  attributeList.length = 0;
+
+  // Look for interesting attributes.
+  for (var i = 0, I = jsLength(JST_ATTRIBUTES); i < I; ++i) {
+    var name = JST_ATTRIBUTES[i][0];
+    var value = domGetAttribute(node, name);
+    attributeValues[name] = value;
+    if (value != null) {
+      attributeList.push(name + '=' + value);
+    }
+  }
+
+  // If none found, mark this node to prevent further inspection, and return
+  // an empty cache object.
+  if (attributeList.length == 0) {
+    domSetAttribute(node, ATT_jstcache, STRING_zero);
+    return node[PROP_jstcache] = JstProcessor.jstcache_[0];
+  }
+
+  // If we already have a cache object corresponding to these attributes,
+  // annotate the node with it, and return it.
+  var attstring = attributeList.join(CHAR_ampersand);
+  if (jstid = JstProcessor.jstcacheattributes_[attstring]) {
+    domSetAttribute(node, ATT_jstcache, jstid);
+    return node[PROP_jstcache] = JstProcessor.jstcache_[jstid];
+  }
+
+  // Otherwise, build a new cache object.
+  var jstcache = {};
+  for (var i = 0, I = jsLength(JST_ATTRIBUTES); i < I; ++i) {
+    var att = JST_ATTRIBUTES[i];
+    var name = att[0];
+    var parse = att[1];
+    var value = attributeValues[name];
+    if (value != null) {
+      jstcache[name] = parse(value);
+      if (MAPS_DEBUG) {
+        jstcache.jstAttributeValues = jstcache.jstAttributeValues || {};
+        jstcache.jstAttributeValues[name] = value;
+      }
+    }
+  }
+
+  jstid = STRING_empty + ++JstProcessor.jstid_;
+  domSetAttribute(node, ATT_jstcache, jstid);
+  JstProcessor.jstcache_[jstid] = jstcache;
+  JstProcessor.jstcacheattributes_[attstring] = jstid;
+
+  return node[PROP_jstcache] = jstcache;
+};
+
+
+/**
+ * Runs the given function in our state machine.
+ *
+ * It's informative to view the set of all function calls as a tree:
+ * - nodes are states
+ * - edges are state transitions, implemented as calls to the pending
+ *   functions in the stack.
+ *   - pre-order function calls are downward edges (recursion into call).
+ *   - post-order function calls are upward edges (return from call).
+ * - leaves are nodes which do not recurse.
+ * We represent the call tree as an array of array of calls, indexed as
+ * stack[depth][index].  Here [depth] indexes into the call stack, and
+ * [index] indexes into the call queue at that depth.  We require a call
+ * queue so that a node may branch to more than one child
+ * (which will be called serially), typically due to a loop structure.
+ *
+ * @param {Function} f The first function to run.
+ * @private
+ */
+JstProcessor.prototype.run_ = function(f) {
+  var me = this;
+
+  /**
+   * A stack of queues of pre-order calls.
+   * The inner arrays (constituent queues) are structured as
+   * [ arg2, arg1, method, arg2, arg1, method, ...]
+   * ie. a flattened array of methods with 2 arguments, in reverse order
+   * for efficient push/pop.
+   *
+   * The outer array is a stack of such queues.
+   *
+   * @type {Array.<Array>}
+   */
+  var calls = me.calls_ = [];
+
+  /**
+   * The index into the queue for each depth. NOTE: Alternative would
+   * be to maintain the queues in reverse order (popping off of the
+   * end) but the repeated calls to .pop() consumed 90% of this
+   * function's execution time.
+   * @type {Array.<number>}
+   */
+  var queueIndices = me.queueIndices_ = [];
+
+  /**
+   * A pool of empty arrays.  Minimizes object allocation for IE6's benefit.
+   * @type {Array.<Array>}
+   */
+  var arrayPool = me.arrayPool_ = [];
+
+  f();
+  var queue, queueIndex;
+  var method, arg1, arg2;
+  var temp;
+  while (calls.length) {
+    queue = calls[calls.length - 1];
+    queueIndex = queueIndices[queueIndices.length - 1];
+    if (queueIndex >= queue.length) {
+      me.recycleArray_(calls.pop());
+      queueIndices.pop();
+      continue;
+    }
+
+    // Run the first function in the queue.
+    method = queue[queueIndex++];
+    arg1 = queue[queueIndex++];
+    arg2 = queue[queueIndex++];
+    queueIndices[queueIndices.length - 1] = queueIndex;
+    method.call(me, arg1, arg2);
+  }
+};
+
+
+/**
+ * Pushes one or more functions onto the stack.  These will be run in sequence,
+ * interspersed with any recursive calls that they make.
+ *
+ * This method takes ownership of the given array!
+ *
+ * @param {Array} args Array of method calls structured as
+ *     [ method, arg1, arg2, method, arg1, arg2, ... ].
+ * @private
+ */
+JstProcessor.prototype.push_ = function(args) {
+  this.calls_.push(args);
+  this.queueIndices_.push(0);
+};
+
+
+/**
+ * Enable/disable debugging.
+ * @param {boolean} debugging New state.
+ */
+JstProcessor.prototype.setDebugging = function(debugging) {
+  if (MAPS_DEBUG) {
+    this.debugging_ = debugging;
+  }
+};
+
+/**
+ * @private
+ */
+JstProcessor.prototype.createArray_ = function() {
+  if (this.arrayPool_.length) {
+    return this.arrayPool_.pop();
+  } else {
+    return [];
+  }
+};
+
+
+/**
+ * @private
+ */
+JstProcessor.prototype.recycleArray_ = function(array) {
+  arrayClear(array);
+  this.arrayPool_.push(array);
+};
+
+/**
+ * Implements internals of jstProcess. This processes the two
+ * attributes transclude and jsselect, which replace or multiply
+ * elements, hence the name "outer". The remainder of the attributes
+ * is processed in jstProcessInner_(), below. That function
+ * jsProcessInner_() only processes attributes that affect an existing
+ * node, but doesn't create or destroy nodes, hence the name
+ * "inner". jstProcessInner_() is called through jstSelect_() if there
+ * is a jsselect attribute (possibly for newly created clones of the
+ * current template node), or directly from here if there is none.
+ *
+ * @param {JsEvalContext} context
+ *
+ * @param {Element} template
+ * @private
+ */
+JstProcessor.prototype.jstProcessOuter_ = function(context, template) {
+  var me = this;
+
+  var jstAttributes = me.jstAttributes_(template);
+  if (MAPS_DEBUG && me.debugging_) {
+    me.logState_('Outer', template, jstAttributes.jstAttributeValues);
+  }
+
+  var transclude = jstAttributes[ATT_transclude];
+  if (transclude) {
+    var tr = jstGetTemplate(transclude);
+    if (tr) {
+      domReplaceChild(tr, template);
+      var call = me.createArray_();
+      call.push(me.jstProcessOuter_, context, tr);
+      me.push_(call);
+    } else {
+      domRemoveNode(template);
+    }
+    return;
+  }
+
+  var select = jstAttributes[ATT_select];
+  if (select) {
+    me.jstSelect_(context, template, select);
+  } else {
+    me.jstProcessInner_(context, template);
+  }
+};
+
+
+/**
+ * Implements internals of jstProcess. This processes all attributes
+ * except transclude and jsselect. It is called either from
+ * jstSelect_() for nodes that have a jsselect attribute so that the
+ * jsselect attribute will not be processed again, or else directly
+ * from jstProcessOuter_(). See the comment on jstProcessOuter_() for
+ * an explanation of the name.
+ *
+ * @param {JsEvalContext} context
+ *
+ * @param {Element} template
+ * @private
+ */
+JstProcessor.prototype.jstProcessInner_ = function(context, template) {
+  var me = this;
+
+  var jstAttributes = me.jstAttributes_(template);
+  if (MAPS_DEBUG && me.debugging_) {
+    me.logState_('Inner', template, jstAttributes.jstAttributeValues);
+  }
+
+  // NOTE: See NOTE on ATT_content why this is a separate
+  // attribute, and not a special value in ATT_values.
+  var display = jstAttributes[ATT_display];
+  if (display) {
+    var shouldDisplay = context.jsexec(display, template);
+    if (MAPS_DEBUG && me.debugging_) {
+      me.logs_.push(ATT_display + ': ' + shouldDisplay + '<br/>');
+    }
+    if (!shouldDisplay) {
+      displayNone(template);
+      return;
+    }
+    displayDefault(template);
+  }
+
+  // NOTE: jsvars is evaluated before jsvalues, because it's
+  // more useful to be able to use var values in attribute value
+  // expressions than vice versa.
+  var values = jstAttributes[ATT_vars];
+  if (values) {
+    me.jstVars_(context, template, values);
+  }
+
+  values = jstAttributes[ATT_values];
+  if (values) {
+    me.jstValues_(context, template, values);
+  }
+
+  // Evaluate expressions immediately. Useful for hooking callbacks
+  // into jstemplates.
+  //
+  // NOTE: Evaluation order is sometimes significant, e.g. when
+  // the expression evaluated in jseval relies on the values set in
+  // jsvalues, so it needs to be evaluated *after*
+  // jsvalues. TODO: This is quite arbitrary, it would be
+  // better if this would have more necessity to it.
+  var expressions = jstAttributes[ATT_eval];
+  if (expressions) {
+    for (var i = 0, I = jsLength(expressions); i < I; ++i) {
+      context.jsexec(expressions[i], template);
+    }
+  }
+
+  var skip = jstAttributes[ATT_skip];
+  if (skip) {
+    var shouldSkip = context.jsexec(skip, template);
+    if (MAPS_DEBUG && me.debugging_) {
+      me.logs_.push(ATT_skip + ': ' + shouldSkip + '<br/>');
+    }
+    if (shouldSkip) return;
+  }
+
+  // NOTE: content is a separate attribute, instead of just a
+  // special value mentioned in values, for two reasons: (1) it is
+  // fairly common to have only mapped content, and writing
+  // content="expr" is shorter than writing values="content:expr", and
+  // (2) the presence of content actually terminates traversal, and we
+  // need to check for that. Display is a separate attribute for a
+  // reason similar to the second, in that its presence *may*
+  // terminate traversal.
+  var content = jstAttributes[ATT_content];
+  if (content) {
+    me.jstContent_(context, template, content);
+
+  } else {
+    // Newly generated children should be ignored, so we explicitly
+    // store the children to be processed.
+    var queue = me.createArray_();
+    var ctx = null;
+    for (var c = template.firstChild; c; c = c.nextSibling) {
+      if (c.nodeType == DOM_ELEMENT_NODE) {
+        // Construct a new context if needed, lazily.
+        if (!ctx) {
+          ctx = context;
+          var selectInner = jstAttributes[ATT_innerselect];
+          if (selectInner && selectInner != VAR_this) {
+            ctx = context.clone(context.jsexec(selectInner, template), 0, 0);
+          }
+        }
+        queue.push(me.jstProcessOuter_, ctx, c);
+      }
+    }
+    if (queue.length) me.push_(queue);
+  }
+};
+
+
+/**
+ * Implements the jsselect attribute: evalutes the value of the
+ * jsselect attribute in the current context, with the current
+ * variable bindings (see JsEvalContext.jseval()). If the value is an
+ * array, the current template node is multiplied once for every
+ * element in the array, with the array element being the context
+ * object. If the array is empty, or the value is undefined, then the
+ * current template node is dropped. If the value is not an array,
+ * then it is just made the context object.
+ *
+ * @param {JsEvalContext} context The current evaluation context.
+ *
+ * @param {Element} template The currently processed node of the template.
+ *
+ * @param {Function} select The javascript expression to evaluate.
+ *
+ * @notypecheck FIXME(hmitchell): See OCL6434950. instance and value need
+ * type checks.
+ * @private
+ */
+JstProcessor.prototype.jstSelect_ = function(context, template, select) {
+  var me = this;
+
+  var value = context.jsexec(select, template);
+
+  // Enable reprocessing: if this template is reprocessed, then only
+  // fill the section instance here. Otherwise do the cardinal
+  // processing of a new template.
+  var instance = domGetAttribute(template, ATT_instance);
+
+  var instanceLast = false;
+  if (instance) {
+    if (instance.charAt(0) == CHAR_asterisk) {
+      instance = parseInt10(instance.substr(1));
+      instanceLast = true;
+    } else {
+      instance = parseInt10(/** @type {string} */(instance));
+    }
+  }
+
+  // The expression value instanceof Array is occasionally false for
+  // arrays, seen in Firefox. Thus we recognize an array as an object
+  // which is not null that has a length property. Notice that this
+  // also matches input data with a length property, so this property
+  // name should be avoided in input data.
+  var multiple = isArray(value);
+  var count = multiple ? jsLength(value) : 1;
+  var multipleEmpty = (multiple && count == 0);
+
+  if (multiple) {
+    if (multipleEmpty) {
+      // For an empty array, keep the first template instance and mark
+      // it last. Remove all other template instances.
+      if (!instance) {
+        domSetAttribute(template, ATT_instance, STRING_asteriskzero);
+        displayNone(template);
+      } else {
+        domRemoveNode(template);
+      }
+
+    } else {
+      displayDefault(template);
+      // For a non empty array, create as many template instances as
+      // are needed. If the template is first processed, as many
+      // template instances are needed as there are values in the
+      // array. If the template is reprocessed, new template instances
+      // are only needed if there are more array values than template
+      // instances. Those additional instances are created by
+      // replicating the last template instance.
+      //
+      // When the template is first processed, there is no jsinstance
+      // attribute. This is indicated by instance === null, except in
+      // opera it is instance === "". Notice also that the === is
+      // essential, because 0 == "", presumably via type coercion to
+      // boolean.
+      if (instance === null || instance === STRING_empty ||
+          (instanceLast && instance < count - 1)) {
+        // A queue of calls to push.
+        var queue = me.createArray_();
+
+        var instancesStart = instance || 0;
+        var i, I, clone;
+        for (i = instancesStart, I = count - 1; i < I; ++i) {
+          var node = domCloneNode(template);
+          domInsertBefore(node, template);
+
+          jstSetInstance(/** @type {Element} */(node), value, i);
+          clone = context.clone(value[i], i, count);
+
+          queue.push(me.jstProcessInner_, clone, node,
+                     JsEvalContext.recycle, clone, null);
+
+        }
+        // Push the originally present template instance last to keep
+        // the order aligned with the DOM order, because the newly
+        // created template instances are inserted *before* the
+        // original instance.
+        jstSetInstance(template, value, i);
+        clone = context.clone(value[i], i, count);
+        queue.push(me.jstProcessInner_, clone, template,
+                   JsEvalContext.recycle, clone, null);
+        me.push_(queue);
+      } else if (instance < count) {
+        var v = value[instance];
+
+        jstSetInstance(template, value, instance);
+        var clone = context.clone(v, instance, count);
+        var queue = me.createArray_();
+        queue.push(me.jstProcessInner_, clone, template,
+                   JsEvalContext.recycle, clone, null);
+        me.push_(queue);
+      } else {
+        domRemoveNode(template);
+      }
+    }
+  } else {
+    if (value == null) {
+      displayNone(template);
+    } else {
+      displayDefault(template);
+      var clone = context.clone(value, 0, 1);
+      var queue = me.createArray_();
+      queue.push(me.jstProcessInner_, clone, template,
+                 JsEvalContext.recycle, clone, null);
+      me.push_(queue);
+    }
+  }
+};
+
+
+/**
+ * Implements the jsvars attribute: evaluates each of the values and
+ * assigns them to variables in the current context. Similar to
+ * jsvalues, except that all values are treated as vars, independent
+ * of their names.
+ *
+ * @param {JsEvalContext} context Current evaluation context.
+ *
+ * @param {Element} template Currently processed template node.
+ *
+ * @param {Array} values Processed value of the jsvalues attribute: a
+ * flattened array of pairs. The second element in the pair is a
+ * function that can be passed to jsexec() for evaluation in the
+ * current jscontext, and the first element is the variable name that
+ * the value returned by jsexec is assigned to.
+ * @private
+ */
+JstProcessor.prototype.jstVars_ = function(context, template, values) {
+  for (var i = 0, I = jsLength(values); i < I; i += 2) {
+    var label = values[i];
+    var value = context.jsexec(values[i + 1], template);
+    context.setVariable(label, value);
+  }
+};
+
+
+/**
+ * Implements the jsvalues attribute: evaluates each of the values and
+ * assigns them to variables in the current context (if the name
+ * starts with '$', javascript properties of the current template node
+ * (if the name starts with '.'), or DOM attributes of the current
+ * template node (otherwise). Since DOM attribute values are always
+ * strings, the value is coerced to string in the latter case,
+ * otherwise it's the uncoerced javascript value.
+ *
+ * @param {JsEvalContext} context Current evaluation context.
+ *
+ * @param {Element} template Currently processed template node.
+ *
+ * @param {Array} values Processed value of the jsvalues attribute: a
+ * flattened array of pairs. The second element in the pair is a
+ * function that can be passed to jsexec() for evaluation in the
+ * current jscontext, and the first element is the label that
+ * determines where the value returned by jsexec is assigned to.
+ * @private
+ */
+JstProcessor.prototype.jstValues_ = function(context, template, values) {
+  for (var i = 0, I = jsLength(values); i < I; i += 2) {
+    var label = values[i];
+    var value = context.jsexec(values[i + 1], template);
+
+    if (label.charAt(0) == CHAR_dollar) {
+      // A jsvalues entry whose name starts with $ sets a local
+      // variable.
+      context.setVariable(label, value);
+
+    } else if (label.charAt(0) == CHAR_period) {
+      // A jsvalues entry whose name starts with . sets a property of
+      // the current template node. The name may have further dot
+      // separated components, which are translated into namespace
+      // objects. This specifically allows to set properties on .style
+      // using jsvalues. NOTE: Setting the style attribute has
+      // no effect in IE and hence should not be done anyway.
+      var nameSpaceLabel = label.substr(1).split(CHAR_period);
+      var nameSpaceObject = template;
+      var nameSpaceDepth = jsLength(nameSpaceLabel);
+      for (var j = 0, J = nameSpaceDepth - 1; j < J; ++j) {
+        var jLabel = nameSpaceLabel[j];
+        if (!nameSpaceObject[jLabel]) {
+          nameSpaceObject[jLabel] = {};
+        }
+        nameSpaceObject = nameSpaceObject[jLabel];
+      }
+      nameSpaceObject[nameSpaceLabel[nameSpaceDepth - 1]] = value;
+
+    } else if (label) {
+      // Any other jsvalues entry sets an attribute of the current
+      // template node.
+      if (typeof value == TYPE_boolean) {
+        // Handle boolean values that are set as attributes specially,
+        // according to the XML/HTML convention.
+        if (value) {
+          domSetAttribute(template, label, label);
+        } else {
+          domRemoveAttribute(template, label);
+        }
+      } else {
+        domSetAttribute(template, label, STRING_empty + value);
+      }
+    }
+  }
+};
+
+
+/**
+ * Implements the jscontent attribute. Evalutes the expression in
+ * jscontent in the current context and with the current variables,
+ * and assigns its string value to the content of the current template
+ * node.
+ *
+ * @param {JsEvalContext} context Current evaluation context.
+ *
+ * @param {Element} template Currently processed template node.
+ *
+ * @param {Function} content Processed value of the jscontent
+ * attribute.
+ * @private
+ */
+JstProcessor.prototype.jstContent_ = function(context, template, content) {
+  // NOTE: Profiling shows that this method costs significant
+  // time. In jstemplate_perf.html, it's about 50%. I tried to replace
+  // by HTML escaping and assignment to innerHTML, but that was even
+  // slower.
+  var value = STRING_empty + context.jsexec(content, template);
+  // Prevent flicker when refreshing a template and the value doesn't
+  // change.
+  if (template.innerHTML == value) {
+    return;
+  }
+  while (template.firstChild) {
+    domRemoveNode(template.firstChild);
+  }
+  var t = domCreateTextNode(this.document_, value);
+  domAppendChild(template, t);
+};
+
+
+/**
+ * Caches access to and parsing of template processing attributes. If
+ * domGetAttribute() is called every time a template attribute value
+ * is used, it takes more than 10% of the time.
+ *
+ * @param {Element} template A DOM element node of the template.
+ *
+ * @return {Object} A javascript object that has all js template
+ * processing attribute values of the node as properties.
+ * @private
+ */
+JstProcessor.prototype.jstAttributes_ = function(template) {
+  if (template[PROP_jstcache]) {
+    return template[PROP_jstcache];
+  }
+
+  var jstid = domGetAttribute(template, ATT_jstcache);
+  if (jstid) {
+    return template[PROP_jstcache] = JstProcessor.jstcache_[jstid];
+  }
+
+  return JstProcessor.prepareNode_(template);
+};
+
+
+/**
+ * Helps to implement the transclude attribute, and is the initial
+ * call to get hold of a template from its ID.
+ *
+ * If the ID is not present in the DOM, and opt_loadHtmlFn is specified, this
+ * function will call that function and add the result to the DOM, before
+ * returning the template.
+ *
+ * @param {string} name The ID of the HTML element used as template.
+ * @param {Function=} opt_loadHtmlFn A function which, when called, will return
+ *   HTML that contains an element whose ID is 'name'.
+ *
+ * @return {?Element} The DOM node of the template. (Only element nodes
+ * can be found by ID, hence it's a Element.).
+ */
+function jstGetTemplate(name, opt_loadHtmlFn) {
+  var doc = document;
+  var section;
+  if (opt_loadHtmlFn) {
+    section = jstLoadTemplateIfNotPresent(doc, name, opt_loadHtmlFn);
+  } else {
+    section = domGetElementById(doc, name);
+  }
+  if (section) {
+    JstProcessor.prepareTemplate_(section);
+    var ret = domCloneElement(section);
+    domRemoveAttribute(ret, STRING_id);
+    return ret;
+  } else {
+    return null;
+  }
+}
+
+/**
+ * This function is the same as 'jstGetTemplate' but, if the template
+ * does not exist, throw an exception.
+ *
+ * @param {string} name The ID of the HTML element used as template.
+ * @param {Function=} opt_loadHtmlFn A function which, when called, will return
+ *   HTML that contains an element whose ID is 'name'.
+ *
+ * @return {Element} The DOM node of the template. (Only element nodes
+ * can be found by ID, hence it's a Element.).
+ */
+function jstGetTemplateOrDie(name, opt_loadHtmlFn) {
+  var x = jstGetTemplate(name, opt_loadHtmlFn);
+  //check(x !== null);
+  return /** @type {Element} */(x);
+}
+
+
+/**
+ * If an element with id 'name' is not present in the document, call loadHtmlFn
+ * and insert the result into the DOM.
+ *
+ * @param {Document} doc
+ * @param {string} name
+ * @param {Function} loadHtmlFn A function that returns HTML to be inserted
+ * into the DOM.
+ * @param {string=} opt_target The id of a DOM object under which to attach the
+ *   HTML once it's inserted.  An object with this id is created if it does not
+ *   exist.
+ * @return {Element} The node whose id is 'name'.
+ */
+function jstLoadTemplateIfNotPresent(doc, name, loadHtmlFn, opt_target) {
+  var section = domGetElementById(doc, name);
+  if (section) {
+    return section;
+  }
+  // Load any necessary HTML and try again.
+  jstLoadTemplate_(doc, loadHtmlFn(), opt_target || STRING_jsts);
+  var section = domGetElementById(doc, name);
+  if (!section) {
+    log('Error: jstGetTemplate was provided with opt_loadHtmlFn, ' +
+	"but that function did not provide the id '" + name + "'.");
+  }
+  return /** @type {Element} */(section);
+}
+
+
+/**
+ * Loads the given HTML text into the given document, so that
+ * jstGetTemplate can find it.
+ *
+ * We append it to the element identified by targetId, which is hidden.
+ * If it doesn't exist, it is created.
+ *
+ * @param {Document} doc The document to create the template in.
+ *
+ * @param {string} html HTML text to be inserted into the document.
+ *
+ * @param {string} targetId The id of a DOM object under which to attach the
+ *   HTML once it's inserted.  An object with this id is created if it does not
+ *   exist.
+ */
+function jstLoadTemplate_(doc, html, targetId) {
+  var existing_target = domGetElementById(doc, targetId);
+  var target;
+  if (!existing_target) {
+    target = domCreateElement(doc, STRING_div);
+    target.id = targetId;
+    displayNone(target);
+    positionAbsolute(target);
+    domAppendChild(doc.body, target);
+  } else {
+    target = existing_target;
+  }
+  var div = domCreateElement(doc, STRING_div);
+  target.appendChild(div);
+  div.innerHTML = html;
+}
+
+
+/**
+ * Sets the jsinstance attribute on a node according to its context.
+ *
+ * @param {Element} template The template DOM node to set the instance
+ * attribute on.
+ *
+ * @param {Array} values The current input context, the array of
+ * values of which the template node will render one instance.
+ *
+ * @param {number} index The index of this template node in values.
+ */
+function jstSetInstance(template, values, index) {
+  if (index == jsLength(values) - 1) {
+    domSetAttribute(template, ATT_instance, CHAR_asterisk + index);
+  } else {
+    domSetAttribute(template, ATT_instance, STRING_empty + index);
+  }
+}
+
+
+/**
+ * Log the current state.
+ * @param {string} caller An identifier for the caller of .log_.
+ * @param {Element} template The template node being processed.
+ * @param {Object} jstAttributeValues The jst attributes of the template node.
+ * @private
+ */
+JstProcessor.prototype.logState_ = function(
+    caller, template, jstAttributeValues) {
+  if (MAPS_DEBUG) {
+    var msg = '<table>';
+    msg += '<caption>' + caller + '</caption>';
+    msg += '<tbody>';
+    if (template.id) {
+      msg += '<tr><td>' + 'id:' + '</td><td>' + template.id + '</td></tr>';
+    }
+    if (template.name) {
+      msg += '<tr><td>' + 'name:' + '</td><td>' + template.name + '</td></tr>';
+    }
+    if (jstAttributeValues) {
+      msg += '<tr><td>' + 'attr:' +
+          '</td><td>' + /*jsToSource*/(jstAttributeValues) + '</td></tr>';
+    }
+    msg += '</tbody></table><br/>';
+    this.logs_.push(msg);
+  }
+};
+
+
+/**
+ * Retrieve the processing logs.
+ * @return {Array.<string>} The processing logs.
+ */
+JstProcessor.prototype.getLogs = function() {
+  return this.logs_;
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/util.js b/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/util.js
new file mode 100644
index 0000000..d9dd0f9
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/jsTemplate/util.js
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Miscellaneous constants and functions referenced in
+ * the main source files.
+ *
+ * @author Steffen Meschkat (mesch@google.com)
+ */
+
+var MAPS_DEBUG = false;
+
+function log(msg) {}
+
+// String literals defined globally and not to be inlined. (IE6 perf)
+/** @const */ var STRING_empty = '';
+
+/** @const */ var CSS_display = 'display';
+/** @const */ var CSS_position = 'position';
+
+// Constants for possible values of the typeof operator.
+var TYPE_boolean = 'boolean';
+var TYPE_number = 'number';
+var TYPE_object = 'object';
+var TYPE_string = 'string';
+var TYPE_function = 'function';
+var TYPE_undefined = 'undefined';
+
+
+/**
+ * Wrapper for the eval() builtin function to evaluate expressions and
+ * obtain their value. It wraps the expression in parentheses such
+ * that object literals are really evaluated to objects. Without the
+ * wrapping, they are evaluated as block, and create syntax
+ * errors. Also protects against other syntax errors in the eval()ed
+ * code and returns null if the eval throws an exception.
+ *
+ * @param {string} expr
+ * @return {?Object}
+ */
+function jsEval(expr) {
+  try {
+    // NOTE: An alternative idiom would be:
+    //
+    //   eval('(' + expr + ')');
+    //
+    // Note that using the square brackets as below, "" evals to undefined.
+    // The alternative of using parentheses does not work when evaluating
+    // function literals in IE.
+    // e.g. eval("(function() {})") returns undefined, and not a function
+    // object, in IE.
+    return eval('[' + expr + '][0]');
+  } catch (e) {
+    log('EVAL FAILED ' + expr + ': ' + e);
+    return null;
+  }
+}
+
+function jsLength(obj) {
+  return obj.length;
+}
+
+function assert(obj) {}
+
+/**
+ * Copies all properties from second object to the first.  Modifies to.
+ *
+ * @param {Object} to  The target object.
+ * @param {Object} from  The source object.
+ */
+function copyProperties(to, from) {
+  for (var p in from) {
+    to[p] = from[p];
+  }
+}
+
+
+/**
+ * @param {Object|null|undefined} value The possible value to use.
+ * @param {Object} defaultValue The default if the value is not set.
+ * @return {Object} The value, if it is
+ * defined and not null; otherwise the default.
+ */
+function getDefaultObject(value, defaultValue) {
+  if (typeof value != TYPE_undefined && value != null) {
+    return /** @type {Object} */(value);
+  } else {
+    return defaultValue;
+  }
+}
+
+/**
+ * Detect if an object looks like an Array.
+ * Note that instanceof Array is not robust; for example an Array
+ * created in another iframe fails instanceof Array.
+ * @param {?Object} value Object to interrogate.
+ * @return {boolean} Is the object an array?
+ */
+function isArray(value) {
+  return value != null &&
+      typeof value == TYPE_object &&
+      typeof value.length == TYPE_number;
+}
+
+
+/**
+ * Finds a slice of an array.
+ *
+ * @param {Array} array  Array to be sliced.
+ * @param {number} start  The start of the slice.
+ * @param {number=} opt_end  The end of the slice (optional).
+ * @return {Array} array  The slice of the array from start to end.
+ */
+function arraySlice(array, start, opt_end) {
+  // Use
+  //   return Function.prototype.call.apply(Array.prototype.slice, arguments);
+  // instead of the simpler
+  //   return Array.prototype.slice.call(array, start, opt_end);
+  // here because of a bug in the FF and IE implementations of
+  // Array.prototype.slice which causes this function to return an empty list
+  // if opt_end is not provided.
+  return Function.prototype.call.apply(Array.prototype.slice, arguments);
+}
+
+
+/**
+ * Jscompiler wrapper for parseInt() with base 10.
+ *
+ * @param {string} s string repersentation of a number.
+ *
+ * @return {number} The integer contained in s, converted on base 10.
+ */
+function parseInt10(s) {
+  return parseInt(s, 10);
+}
+
+
+/**
+ * Clears the array by setting the length property to 0. This usually
+ * works, and if it should turn out not to work everywhere, here would
+ * be the place to implement the browser specific workaround.
+ *
+ * @param {Array} array  Array to be cleared.
+ */
+function arrayClear(array) {
+  array.length = 0;
+}
+
+
+/**
+ * Prebinds "this" within the given method to an object, but ignores all
+ * arguments passed to the resulting function.
+ * I.e. var_args are all the arguments that method is invoked with when
+ * invoking the bound function.
+ *
+ * @param {?Object} object  The object that the method call targets.
+ * @param {Function} method  The target method.
+ * @return {Function}  Method with the target object bound to it and curried by
+ *                     the provided arguments.
+ */
+function bindFully(object, method, var_args) {
+  var args = arraySlice(arguments, 2);
+  return function() {
+    return method.apply(object, args);
+  };
+}
+
+function domGetElementById(document, id) {
+  return document.getElementById(id);
+}
+
+/**
+ * Creates a new node in the given document
+ *
+ * @param {Document} doc  Target document.
+ * @param {string} name  Name of new element (i.e. the tag name)..
+ * @return {Element}  Newly constructed element.
+ */
+function domCreateElement(doc, name) {
+  return doc.createElement(name);
+}
+
+/**
+ * Traverses the element nodes in the DOM section underneath the given
+ * node and invokes the given callback as a method on every element
+ * node encountered.
+ *
+ * @param {Element} node  Parent element of the subtree to traverse.
+ * @param {Function} callback  Called on each node in the traversal.
+ */
+function domTraverseElements(node, callback) {
+  var traverser = new DomTraverser(callback);
+  traverser.run(node);
+}
+
+/**
+ * A class to hold state for a dom traversal.
+ * @param {Function} callback  Called on each node in the traversal.
+ * @constructor
+ * @class
+ */
+function DomTraverser(callback) {
+  this.callback_ = callback;
+}
+
+/**
+ * Processes the dom tree in breadth-first order.
+ * @param {Element} root  The root node of the traversal.
+ */
+DomTraverser.prototype.run = function(root) {
+  var me = this;
+  me.queue_ = [root];
+  while (jsLength(me.queue_)) {
+    me.process_(me.queue_.shift());
+  }
+};
+
+/**
+ * Processes a single node.
+ * @param {Element} node  The current node of the traversal.
+ * @private
+ */
+DomTraverser.prototype.process_ = function(node) {
+  var me = this;
+
+  me.callback_(node);
+
+  for (var c = node.firstChild; c; c = c.nextSibling) {
+    if (c.nodeType == DOM_ELEMENT_NODE) {
+      me.queue_.push(c);
+    }
+  }
+};
+
+/**
+ * Get an attribute from the DOM.  Simple redirect, exists to compress code.
+ *
+ * @param {Element} node  Element to interrogate.
+ * @param {string} name  Name of parameter to extract.
+ * @return {?string}  Resulting attribute.
+ */
+function domGetAttribute(node, name) {
+  return node.getAttribute(name);
+  // NOTE: Neither in IE nor in Firefox, HTML DOM attributes
+  // implement namespaces. All items in the attribute collection have
+  // null localName and namespaceURI attribute values. In IE, we even
+  // encounter DIV elements that don't implement the method
+  // getAttributeNS().
+}
+
+
+/**
+ * Set an attribute in the DOM.  Simple redirect to compress code.
+ *
+ * @param {Element} node  Element to interrogate.
+ * @param {string} name  Name of parameter to set.
+ * @param {string|number} value  Set attribute to this value.
+ */
+function domSetAttribute(node, name, value) {
+  node.setAttribute(name, value);
+}
+
+/**
+ * Remove an attribute from the DOM.  Simple redirect to compress code.
+ *
+ * @param {Element} node  Element to interrogate.
+ * @param {string} name  Name of parameter to remove.
+ */
+function domRemoveAttribute(node, name) {
+  node.removeAttribute(name);
+}
+
+/**
+ * Clone a node in the DOM.
+ *
+ * @param {Node} node  Node to clone.
+ * @return {Node}  Cloned node.
+ */
+function domCloneNode(node) {
+  return node.cloneNode(true);
+  // NOTE: we never so far wanted to use cloneNode(false),
+  // hence the default.
+}
+
+/**
+ * Clone a element in the DOM.
+ *
+ * @param {Element} element  Element to clone.
+ * @return {Element}  Cloned element.
+ */
+function domCloneElement(element) {
+  return /** @type {Element} */(domCloneNode(element));
+}
+
+/**
+ * Returns the document owner of the given element. In particular,
+ * returns window.document if node is null or the browser does not
+ * support ownerDocument.  If the node is a document itself, returns
+ * itself.
+ *
+ * @param {Node|null|undefined} node  The node whose ownerDocument is required.
+ * @return {Document}  The owner document or window.document if unsupported.
+ */
+function ownerDocument(node) {
+  if (!node) {
+    return document;
+  } else if (node.nodeType == DOM_DOCUMENT_NODE) {
+    return /** @type {Document} */(node);
+  } else {
+    return node.ownerDocument || document;
+  }
+}
+
+/**
+ * Creates a new text node in the given document.
+ *
+ * @param {Document} doc  Target document.
+ * @param {string} text  Text composing new text node.
+ * @return {Text}  Newly constructed text node.
+ */
+function domCreateTextNode(doc, text) {
+  return doc.createTextNode(text);
+}
+
+/**
+ * Appends a new child to the specified (parent) node.
+ *
+ * @param {Element} node  Parent element.
+ * @param {Node} child  Child node to append.
+ * @return {Node}  Newly appended node.
+ */
+function domAppendChild(node, child) {
+  return node.appendChild(child);
+}
+
+/**
+ * Sets display to default.
+ *
+ * @param {Element} node  The dom element to manipulate.
+ */
+function displayDefault(node) {
+  node.style[CSS_display] = '';
+}
+
+/**
+ * Sets display to none. Doing this as a function saves a few bytes for
+ * the 'style.display' property and the 'none' literal.
+ *
+ * @param {Element} node  The dom element to manipulate.
+ */
+function displayNone(node) {
+  node.style[CSS_display] = 'none';
+}
+
+
+/**
+ * Sets position style attribute to absolute.
+ *
+ * @param {Element} node  The dom element to manipulate.
+ */
+function positionAbsolute(node) {
+  node.style[CSS_position] = 'absolute';
+}
+
+
+/**
+ * Inserts a new child before a given sibling.
+ *
+ * @param {Node} newChild  Node to insert.
+ * @param {Node} oldChild  Sibling node.
+ * @return {Node}  Reference to new child.
+ */
+function domInsertBefore(newChild, oldChild) {
+  return oldChild.parentNode.insertBefore(newChild, oldChild);
+}
+
+/**
+ * Replaces an old child node with a new child node.
+ *
+ * @param {Node} newChild  New child to append.
+ * @param {Node} oldChild  Old child to remove.
+ * @return {Node}  Replaced node.
+ */
+function domReplaceChild(newChild, oldChild) {
+  return oldChild.parentNode.replaceChild(newChild, oldChild);
+}
+
+/**
+ * Removes a node from the DOM.
+ *
+ * @param {Node} node  The node to remove.
+ * @return {Node}  The removed node.
+ */
+function domRemoveNode(node) {
+  return domRemoveChild(node.parentNode, node);
+}
+
+/**
+ * Remove a child from the specified (parent) node.
+ *
+ * @param {Element} node  Parent element.
+ * @param {Node} child  Child node to remove.
+ * @return {Node}  Removed node.
+ */
+function domRemoveChild(node, child) {
+  return node.removeChild(child);
+}
+
+
+/**
+ * Trim whitespace from begin and end of string.
+ *
+ * @see testStringTrim();
+ *
+ * @param {string} str  Input string.
+ * @return {string}  Trimmed string.
+ */
+function stringTrim(str) {
+  return stringTrimRight(stringTrimLeft(str));
+}
+
+/**
+ * Trim whitespace from beginning of string.
+ *
+ * @see testStringTrimLeft();
+ *
+ * @param {string} str  Input string.
+ * @return {string}  Trimmed string.
+ */
+function stringTrimLeft(str) {
+  return str.replace(/^\s+/, '');
+}
+
+/**
+ * Trim whitespace from end of string.
+ *
+ * @see testStringTrimRight();
+ *
+ * @param {string} str  Input string.
+ * @return {string}  Trimmed string.
+  */
+function stringTrimRight(str) {
+  return str.replace(/\s+$/, '');
+}
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/loader.js b/trunk/features/src/main/javascript/features/opensocial-templates/loader.js
new file mode 100644
index 0000000..4904e84
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/loader.js
@@ -0,0 +1,307 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview OpenSocial Template loader. Can be used to load template
+ * libraries via URL. Supports Javascript and CSS injection.
+ *
+ * Usage:
+ *   os.Loader.loadUrl("/path/templatelib.xml", function() { doSomething(); });
+ *
+ * or
+ *   os.Loader.loadContent(
+ *       "<Templates><Template tag="foo:bar">...</Template></Templates>");
+ *
+ * The Template Library should have the following structure:
+ *
+ *   <Templates xmlns:foo="http://foo.com/">
+ *     <Namspace prefix="foo" url="http://foo.com/"/>
+ *     <Template tag="foo:bar">[Template Markup Here]</Template>
+ *     <Style>[CSS for all templates]</Style>
+ *     <JavaScript>
+ *       function usedByAllTemplates() { ... };
+ *     </JavaScript>
+ *     <TemplateDef tag="foo:baz">
+ *       <Template>[Markup for foo:baz]</Template>
+ *       <Style>[CSS for foo:baz]</Style>
+ *       <JavaScript>
+ *         function usedByFooBaz() { ... };
+ *       </JavaScript>
+ *     </TemplateDef>
+ *   </Templates>
+ *
+ * TODO(levik): Implement dependency support - inject JS and CSS lazily.
+ * TODO(levik): More error handling and reporting of ill-formed XML files.
+ */
+
+os.Loader = {};
+
+/**
+ * A map of URLs which were already loaded.
+ * @private
+ */
+os.Loader.loadedUrls_ = {};
+
+/**
+ * Load a remote URL via XMLHttpRequest or gadgets.io.makeRequest API
+ *     when in context of a gadget.
+ * @param {string} url The URL that is to be fetched.
+ * @param {Function} callback Function to call once loaded.
+ */
+os.Loader.loadUrl = function(url, callback) {
+  if (typeof(window['gadgets']) != 'undefined') {
+    os.Loader.requestUrlGadgets_(url, callback);
+  } else {
+    os.Loader.requestUrlXHR_(url, callback);
+  }
+};
+
+/**
+ * Loads a Template Library from a URL via XMLHttpRequest. Once the library is
+ * loaded, the callback function is called. A map is kept to prevent loading
+ * the same URL twice.
+ * @param {string} url The URL of the Template Library.
+ * @param {Function} callback Function to call once loaded.
+ * @private
+ */
+os.Loader.requestUrlXHR_ = function(url, callback) {
+  if (os.Loader.loadedUrls_[url]) {
+    window.setTimeout(callback, 0);
+    return;
+  }
+  var req = null;
+  if (typeof shindig != 'undefined' &&
+      shindig.xhrwrapper &&
+      shindig.xhrwrapper.createXHR) {
+    req = shindig.xhrwrapper.createXHR();
+  } else if (typeof XMLHttpRequest != 'undefined') {
+    req = new XMLHttpRequest();
+  } else {
+    req = new ActiveXObject('MSXML2.XMLHTTP');
+  }
+  req.open('GET', url, true);
+  req.onreadystatechange = function() {
+    if (req.readyState == 4) {
+      os.Loader.loadContent(req.responseText, url);
+      callback();
+    }
+  };
+  req.send(null);
+};
+
+/**
+ * Fetch content remotely using the gadgets.io.makeRequest API.
+ * @param {string} url The URL where the content is located.
+ * @param {Function} callback Function to call with the data from the URL
+ *     once it is fetched.
+ * @private
+ */
+os.Loader.requestUrlGadgets_ = function(url, callback) {
+  var params = {};
+  var gadgets = window['gadgets'];
+
+  if (os.Loader.loadedUrls_[url]) {
+    window.setTimeout(callback, 0);
+    return;
+  }
+  params[gadgets.io.RequestParameters.CONTENT_TYPE] =
+      gadgets.io.ContentType.TEXT;
+  gadgets.io.makeRequest(url, function(obj) {
+    os.Loader.loadContent(obj.data, url);
+    callback();
+  }, params);
+};
+
+/**
+ * Loads a number of Template libraries, specified by an array of URLs. Once
+ * all the libraries have been loaded, the callback is called.
+ * @param {Array.<string>} urls An array of URLs of Template Libraries to load.
+ * @param {Function} callback Function to call once all libraries are loaded.
+ */
+os.Loader.loadUrls = function(urls, callback) {
+  var loadOne = function() {
+    if (urls.length == 0) {
+      callback();
+    } else {
+      os.Loader.loadUrl(urls.pop(), loadOne);
+    }
+  };
+  loadOne();
+};
+
+/**
+ * Processes the XML markup of a Template Library.
+ */
+os.Loader.loadContent = function(xmlString, url) {
+  var doc = gadgets.jsondom.parse(xmlString, url);
+  var templatesNode = doc.firstChild;
+  os.Loader.processTemplatesNode(templatesNode);
+  os.Loader.loadedUrls_[url] = true;
+};
+
+/**
+ * Gets the function that should be used for processing a tag.
+ * @param {string} tagName Name of the tag.
+ * @return {?Function} The function for processing such tags.
+ * @private
+ */
+os.Loader.getProcessorFunction_ = function(tagName) {
+  // TODO(levik): This won't work once compiler does name mangling.
+  return os.Loader['process' + tagName + 'Node'] || null;
+};
+
+/**
+ * Processes the <Templates> node.
+ */
+os.Loader.processTemplatesNode = function(node) {
+  // since the ie domparse does not return a general parent element
+  // we check here if firstChild is really present
+  if (node.firstChild) {
+    node = node.firstChild;
+  }
+  for (var child = node; child; child = child.nextSibling) {
+    if (child.nodeType == DOM_ELEMENT_NODE) {
+      var handler = os.Loader.getProcessorFunction_(child.tagName);
+      if (handler) {
+        handler(child);
+      }
+    }
+  }
+};
+
+/**
+ * Processes the <Namespace> node.
+ */
+os.Loader.processNamespaceNode = function(node) {
+  var prefix = node.getAttribute('prefix');
+  var url = node.getAttribute('url');
+  os.createNamespace(prefix, url);
+};
+
+/**
+ * Processes the <TemplateDef> node
+ */
+os.Loader.processTemplateDefNode = function(node) {
+  var tag = node.getAttribute('tag');
+  var name = node.getAttribute('name');
+  for (var child = node.firstChild; child; child = child.nextSibling) {
+    if (child.nodeType == DOM_ELEMENT_NODE) {
+      // TODO(levik): This won't work once compiler does name mangling.
+      var handler = os.Loader.getProcessorFunction_(child.tagName);
+      if (handler) {
+        handler(child, tag, name);
+      }
+    }
+  }
+};
+
+/**
+ * Processes the <Template> node
+ */
+os.Loader.processTemplateNode = function(node, opt_tag, opt_name) {
+  var tag = opt_tag || node.getAttribute('tag');
+  var name = opt_name || node.getAttribute('name');
+  if (tag) {
+    var tagParts = tag.split(':');
+    if (tagParts.length != 2) {
+      throw Error('Invalid tag name: ' + tag);
+    }
+    var nsObj = os.getNamespace(tagParts[0]);
+    if (!nsObj) {
+      throw Error('Namespace not registered: ' + tagParts[0] +
+          ' while trying to define ' + tag);
+    }
+    var template = os.compileXMLNode(node, tag);
+    nsObj[tagParts[1]] = os.createTemplateCustomTag(template);
+    os.registerTemplate(template);
+  } else if (name) {
+    var template = os.compileXMLNode(node);
+    template.id = name;
+    os.registerTemplate(template);
+  }
+};
+
+/**
+ * Processes the <JavaScript> node
+ */
+os.Loader.processJavaScriptNode = function(node, opt_tag) {
+  for (var contentNode = node.firstChild; contentNode;
+      contentNode = contentNode.nextSibling) {
+    // TODO(levik): Skip empty text nodes (with whitespace and newlines)
+    os.Loader.injectJavaScript(contentNode.nodeValue);
+  }
+};
+
+/**
+ * Processes the <Style> node
+ */
+os.Loader.processStyleNode = function(node, opt_tag) {
+  for (var contentNode = node.firstChild; contentNode;
+      contentNode = contentNode.nextSibling) {
+    // TODO(levik): Skip empty text nodes (with whitespace and newlines)
+    os.Loader.injectStyle(contentNode.nodeValue);
+  }
+};
+
+/**
+ * @type {Element} DOM node used for dynamic injection of JavaScript.
+ * @private
+ * TODO(davidbyttow): Only retrieve this once if JavaScript injection was
+ * actually requested.
+ */
+os.Loader.headNode_ = document.getElementsByTagName('head')[0] ||
+    document.getElementsByTagName('*')[0];
+
+/**
+ * Injects and evaluates JavaScript code synchronously in the global scope.
+ */
+os.Loader.injectJavaScript = function(jsCode) {
+  var scriptNode = document.createElement('script');
+  scriptNode.type = 'text/javascript';
+  scriptNode.text = jsCode;
+  os.Loader.headNode_.appendChild(scriptNode);
+};
+
+/**
+ * Injects CSS Style code into the page.
+ */
+os.Loader.injectStyle = function(cssCode) {
+  var sheet;
+  if (document.styleSheets.length == 0) {
+    document.getElementsByTagName('head')[0].appendChild(
+        document.createElement('style'));
+  }
+  sheet = document.styleSheets[0];
+  var rules = cssCode.split('}');
+  for (var i = 0; i < rules.length; i++) {
+    var rule = rules[i].replace(/\n/g, '').replace(/\s+/g, ' ');
+    try {
+      if (rule.length > 2) {
+        if (sheet.insertRule) {
+          rule = rule + '}';
+          sheet.insertRule(rule, sheet.cssRules.length);
+        } else {
+          var ruleParts = rule.split('{');
+          sheet.addRule(ruleParts[0], ruleParts[1]);
+        }
+      }
+    } catch (err) {
+      gadgets.error('Error in stylesheet: ' + rule + ' - ' + err.name + ' - ' + err.message);
+    }
+  }
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/namespaces.js b/trunk/features/src/main/javascript/features/opensocial-templates/namespaces.js
new file mode 100644
index 0000000..b85c76a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/namespaces.js
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Implements namespace support for custom tags.
+ *
+ * TODO(davidbyttow): Refactor this.
+ */
+
+/**
+ * Map of namespace collections.
+ *
+ * Each namespace collection is either a map of tag handlers, or an object
+ * that has a getTag(tagName) method that will return a tag handler based on
+ * name.
+ *
+ * A tag handler function should be have the following signature:
+ * function({Element} node, {Object} data, {JSEvalContext} context)
+ * where context is the JSEvalContext used to wrap data.
+ *
+ * For simpler implementations,
+ * function({Element} node, {Object} data)
+ * can be used, omitting the third param.
+ *
+ * Handler functions can return a string, a DOM Element or an Object with
+ * {Element} root and, optionally, {Function} onAttach properties.
+ *
+ * @type {Object}
+ * @private
+ */
+os.nsmap_ = {};
+
+/***
+ * Registers the given namespace with a specified URL. Throws an error if it
+ * already exists as a different URL.
+ * @param {string} ns Namespace tag.
+ * @param {string} url URI Reference for namespace.
+ * @return {Object} The object map of registered tags.
+ */
+os.createNamespace = function(ns, url) {
+  var tags = os.nsmap_[ns];
+  if (! os.nsmap_.hasOwnProperty(ns)) {
+    tags = {};
+    os.nsmap_[ns] = tags;
+    opensocial.xmlutil.NSMAP[ns] = url;
+  } else if (opensocial.xmlutil.NSMAP[ns] == null) {
+    // Lazily register an auto-created namespace.
+    opensocial.xmlutil.NSMAP[ns] = url;
+  } else if (opensocial.xmlutil.NSMAP[ns] != url) {
+    throw ('Namespace ' + ns + ' already defined with url ' +
+        opensocial.xmlutil.NSMAP[ns]);
+  }
+  return tags;
+};
+
+/**
+ * Returns the namespace object for a given prefix.
+ */
+os.getNamespace = function(prefix) {
+  return os.nsmap_[prefix];
+};
+
+os.addNamespace = function(ns, url, nsObj) {
+  if (! os.nsmap_.hasOwnProperty(ns)) {
+    if (opensocial.xmlutil.NSMAP[ns] == null) {
+      // Lazily register an auto-created namespace.
+      opensocial.xmlutil.NSMAP[ns] = url;
+      return;
+    } else {
+      throw ("Namespace '" + ns + "' already exists!");
+    }
+  }
+  os.nsmap_[ns] = nsObj;
+  opensocial.xmlutil.NSMAP[ns] = url;
+};
+
+os.getCustomTag = function(ns, tag) {
+  if (! os.nsmap_.hasOwnProperty(ns)) {
+    return null;
+  }
+  var nsObj = os.nsmap_[ns];
+  if (nsObj.getTag) {
+    return nsObj.getTag(tag);
+  } else {
+    return nsObj[tag];
+  }
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/os.js b/trunk/features/src/main/javascript/features/opensocial-templates/os.js
new file mode 100644
index 0000000..8aef8fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/os.js
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Implements os:Render tag and OpenSocial-specific
+ * identifier resolver.
+ */
+
+/**
+ * Define 'os:Render' and 'os:Html' tags and the @onAttach attribute
+ */
+os.defineBuiltinTags = function() {
+  var osn = os.getNamespace('os') ||
+      os.createNamespace('os', 'http://ns.opensocial.org/2008/markup');
+
+  /**
+   * <os:Var> custom tag for variable assignment
+   */
+  osn.Var = function(node, data, context) {
+    var value = os.getValueFromNode_(node,'value');
+
+    if (! value && node.innerHTML) {
+      value = node.innerHTML;
+    }
+    
+    var parsedValue = gadgets.json.parse(value);
+     
+    if (parsedValue) {
+      value = parsedValue;
+    }
+
+    context['vars_'][VAR_top][node.getAttribute('key')] = value;
+    return '';
+  };
+
+  /**
+   * <os:Render> custom tag renders the specified child nodes of the current
+   * context.
+   */
+  osn.Render = function(node, data, context) {
+    var parent = context.getVariable(os.VAR_parentnode);
+    var exp = node.getAttribute('content') || '*';
+    var result = os.getValueFromNode_(parent, exp);
+    if (!result) {
+      return '';
+    } else if (typeof(result) == 'string') {
+      var textNode = document.createTextNode(result);
+      result = [];
+      result.push(textNode);
+    } else if (!os.isArray(result)) {
+      var resultArray = [];
+      for (var i = 0; i < result.childNodes.length; i++) {
+        resultArray.push(result.childNodes[i]);
+      }
+      result = resultArray;
+    } else if (exp != '*' && result.length == 1 &&
+        result[0].nodeType == DOM_ELEMENT_NODE) {
+      // When we call <os:renderAll content="tag"/>, render the inner content
+      // of the tag returned, not the tag itself.
+      var resultArray = [];
+      for (var child = result[0].firstChild; child; child = child.nextSibling) {
+        resultArray.push(child);
+      }
+      result = resultArray;
+    }
+
+    // Trim away leading and trailing spaces on IE, which interprets them
+    // literally.
+    if (os.isIe) {
+      for (var i = 0; i < result.length; i++) {
+        if (result[i].nodeType == DOM_TEXT_NODE) {
+          var trimmed = os.trimWhitespaceForIE_(
+              result[i].nodeValue, (i == 0), (i == result.length - 1));
+          if (trimmed != result[i].nodeValue) {
+            result[i].parentNode.removeChild(result[i]);
+            result[i] = document.createTextNode(trimmed);
+          }
+        }
+      }
+    }
+
+    return result;
+  };
+  // TODO: Remove legacy names.
+  osn.render = osn.RenderAll = osn.renderAll = osn.Render;
+
+  /**
+   * <os:Html> custom tag renders HTML content (as opposed to HTML code), so
+   * <os:Html code="<b>Hello</b>"/> would result in the bold string "Hello",
+   * rather than the text of the markup.
+   */
+  osn.Html = function(node) {
+    var html = node.code ? '' + node.code : node.getAttribute('code') || '';
+    // TODO(levik): Sanitize the HTML here to avoid script injection issues.
+    // Perhaps use the gadgets sanitizer if available.
+    return html;
+  };
+
+  function createClosure(object, method) {
+    return function() {
+      method.apply(object);
+    };
+  }
+
+  /**
+   * Custom attribute handler for @onAttach attribute, which allows deferred
+   * execution of a snippet of JS when a template is attached to the DOM.
+   * This is useful for when geometry needs to be available for post-processing
+   * (such as with Google Maps).
+   * The code will have "this" bounde to the DOM node on which the attribute was
+   * found.
+   */
+  function processOnAttach(node, code, data, context) {
+    var callbacks = context.getVariable(os.VAR_callbacks);
+    var func = new Function(code);
+    callbacks.push(createClosure(node, func));
+  }
+  os.registerAttribute_('onAttach', processOnAttach);
+  os.registerAttribute_('onCreate', processOnAttach);
+  os.registerAttribute_('oncreate', processOnAttach);
+  os.registerAttribute_('x-oncreate', processOnAttach);
+  os.registerAttribute_('x-onCreate', processOnAttach);
+};
+
+os.defineBuiltinTags();
+
+/**
+ * Identifier Resolver function for OpenSocial objects.
+ * Checks for:
+ * <ul>
+ *   <li>Simple property</li>
+ *   <li>JavaBean-style getter</li>
+ *   <li>OpenSocial Field</li>
+ *   <li>Data result set</li>
+ * </ul>
+ * @param {Object} object The object in the scope of which to get a named
+ * property.
+ * @param {string} name The name of the property to get.
+ * @return {Object?} The property requested.
+ */
+os.resolveOpenSocialIdentifier = function(object, name) {
+  // Simple property from object.
+  if (typeof(object[name]) != 'undefined') {
+    return object[name];
+  }
+
+  // JavaBean-style getter method.
+  var functionName = os.getPropertyGetterName(name);
+  if (object[functionName]) {
+    return object[functionName]();
+  }
+
+  // Check OpenSocial field by dictionary mapping
+  if (object.getField) {
+    var fieldData = object.getField(name);
+    if (fieldData) {
+      return fieldData;
+    }
+  }
+
+  // Multi-purpose get() method
+  if (object.get) {
+    var responseItem = object.get(name);
+
+    // ResponseItem is a data set
+    if (responseItem && responseItem.getData) {
+      var data = responseItem.getData();
+      // Return array payload where appropriate
+      return data.array_ || data;
+    }
+    return responseItem;
+  }
+
+  // Return undefined value, to avoid confusing with existing value of "null".
+  var und;
+  return und;
+};
+
+os.setIdentifierResolver(os.resolveOpenSocialIdentifier);
+
+/**
+ * Create methods for an object based upon a field map for OpenSocial.
+ * @param {Object} object Class object to have methods created for.
+ * @param {Object} fields A key-value map object to retrieve fields (keys) and
+ * method names (values) from.
+ * @private
+ */
+os.createOpenSocialGetMethods_ = function(object, fields) {
+  if (object && fields) {
+    for (var key in fields) {
+      var value = fields[key];
+      var getter = os.getPropertyGetterName(value);
+      object.prototype[getter] = function() {
+        this.getField(key);
+      };
+    }
+  }
+};
+
+/**
+ * Automatically register JavaBean-style methods for various OpenSocial objects.
+ * @private
+ */
+os.registerOpenSocialFields_ = function() {
+  var fields = os.resolveOpenSocialIdentifier.FIELDS;
+  if (opensocial) {
+    // TODO: Add more OpenSocial objects.
+    if (opensocial.Person) {
+      //os.createOpenSocialGetMethods_(opensocial.Person,  opensocial.Person.Field);
+    }
+  }
+};
+
+os.registerOpenSocialFields_();
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/template.js b/trunk/features/src/main/javascript/features/opensocial-templates/template.js
new file mode 100644
index 0000000..e960014
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/template.js
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Provides the Template class used to represent a single
+ * compiled template that can be rendered into any DOM node.
+ */
+
+
+/**
+ * Creates a context object out of a json data object.
+ * @param {Object} data
+ * @param {Object=} opt_globals
+ */
+os.createContext = function(data, opt_globals) {
+  var context = JsEvalContext.create(data);
+  context.setVariable(os.VAR_callbacks, []);
+  var defaults = os.getContextDefaults_();
+  for (var def in defaults) {
+    if (defaults.hasOwnProperty(def)) {
+      context.setVariable(def, defaults[def]);
+    }
+  }
+  context.setVariable(os.VAR_emptyArray, os.EMPTY_ARRAY);
+  if (opt_globals) {
+    for (var global in opt_globals) {
+      if (opt_globals.hasOwnproperty(global)) {
+        context.setVariable(global, opt_globals[global]);
+      }
+    }
+  }
+  return context;
+};
+
+os.contextDefaults_ = null;
+
+os.getContextDefaults_ = function() {
+  if (!os.contextDefaults_) {
+    os.contextDefaults_ = {};
+    os.contextDefaults_[os.VAR_emptyArray] = os.EMPTY_ARRAY;
+    os.contextDefaults_[os.VAR_identifierresolver] = os.getFromContext;
+    if (window['JSON'] && JSON.parse) {
+      os.contextDefaults_['osx:parseJson'] = JSON.parse;
+    } else if (window['gadgets'] && gadgets.json && gadgets.json.parse) {
+      os.contextDefaults_['osx:parseJson'] = gadgets.json.parse;
+    }
+  }
+  return os.contextDefaults_;
+};
+
+/**
+ * A renderable compiled Template. A template can contain one or more
+ * compiled nodes pre-processed for JST operation.
+ * @constructor
+ */
+os.Template = function(opt_id) {
+  this.templateRoot_ = document.createElement('span');
+  this.id = opt_id || ('template_' + os.Template.idCounter_++);
+};
+
+/**
+ * A global counter for template IDs.
+ * @type {number}
+ * @private
+ */
+os.Template.idCounter_ = 0;
+
+/**
+ * A Map of registered templates by keyed ID.
+ * @type {Object.<string, os.Template>}
+ * @private
+ */
+os.registeredTemplates_ = {};
+
+/**
+ * Registers a compiled template by its ID.
+ * @param {os.Template} template List of template nodes.
+ */
+os.registerTemplate = function(template) {
+  os.registeredTemplates_[template.id] = template;
+};
+
+/**
+ * De-registers a compiled template..
+ * @param {os.Template} template List of template nodes.
+ */
+os.unRegisterTemplate = function(template) {
+  delete os.registeredTemplates_[template.id];
+};
+
+/**
+ * Gets a registered template by ID.
+ * @param {string} templateId The ID of a registered Template.
+ * @return {os.Template} A Template object.
+ */
+os.getTemplate = function(templateId) {
+  return os.registeredTemplates_[templateId];
+};
+
+/**
+ * Sets a single compiled node into this template.
+ * @param {Element} node - A compiled node.
+ * @private
+ */
+os.Template.prototype.setCompiledNode_ = function(node) {
+  os.removeChildren(this.templateRoot_);
+  this.templateRoot_.appendChild(node);
+};
+
+/**
+ * Sets a list of compiled nodes into this template.
+ * @param {Array.<Element>} nodes An array of compiled nodes.
+ * @private
+ */
+os.Template.prototype.setCompiledNodes_ = function(nodes) {
+  os.removeChildren(this.templateRoot_);
+  for (var i = 0; i < nodes.length; i++) {
+    this.templateRoot_.appendChild(nodes[i]);
+  }
+};
+
+/**
+ * Renders the template and returns the result.
+ * Does not fire callbacks.
+ * @return {Element} a DOM element containing the result of template processing.
+ */
+os.Template.prototype.render = function(opt_data, opt_context) {
+  if (!opt_context) {
+    opt_context = os.createContext(opt_data);
+  }
+  return os.renderTemplateNode_(this.templateRoot_, opt_context);
+};
+
+/**
+ * Renders the template and puts the result into the specified element, then
+ * fires callbacks.
+ */
+os.Template.prototype.renderInto = function(root, opt_data, opt_context) {
+  if (!opt_context) {
+    opt_context = os.createContext(opt_data);
+  }
+  var result = this.render(opt_data, opt_context);
+  os.removeChildren(root);
+  os.appendChildren(result, root);
+  os.fireCallbacks(opt_context);
+};
diff --git a/trunk/features/src/main/javascript/features/opensocial-templates/util.js b/trunk/features/src/main/javascript/features/opensocial-templates/util.js
new file mode 100644
index 0000000..354b234
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/opensocial-templates/util.js
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * @fileoverview Provides various utility functions used throughout the library.
+ */
+
+
+/**
+ * Trims leading and trailing whitespace from a string.
+ * @param {string} string The input string.
+ * @return {string} Input with leading and trailing whitespace removed.
+ */
+os.trim = function(string) {
+  return string.replace(/^\s+|\s+$/g, '');
+};
+
+
+/**
+ * Checks whether or not a given character is alpha-numeric. *
+ * @param {string} ch Character to check.
+ * @return {boolean} This character is alpha-numeric.
+ */
+os.isAlphaNum = function(ch) {
+  // TODO: Try with ch.charCodeAt() to see if faster.
+  return ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
+      (ch >= '0' && ch <= '9') || ch == '_');
+};
+
+/**
+ * Clears the children of a given DOM node.
+ * @param {Node} node DOM node to clear.
+ */
+os.removeChildren = function(node) {
+  while (node.firstChild) {
+    node.removeChild(node.firstChild);
+  }
+};
+
+/**
+ * Copies all children from one node to another.
+ * @param {Node} sourceNode DOM node with children to append.
+ * @param {Node} targetNode DOM node to append sourceNode's children to.
+ */
+os.appendChildren = function(sourceNode, targetNode) {
+  if (sourceNode == targetNode) {
+    return;
+  }
+  while (sourceNode.firstChild) {
+    targetNode.appendChild(sourceNode.firstChild);
+  }
+};
+
+/**
+ * Given a property name (e.g. 'foo') will create a JavaBean-style getter
+ * (e.g. 'getFoo').
+ * @param {string} propertyName Name of the property.
+ * @return {string} The name of the getter function.
+ */
+os.getPropertyGetterName = function(propertyName) {
+  var getter = 'get' + propertyName.charAt(0).toUpperCase() +
+      propertyName.substring(1);
+  return getter;
+};
+
+
+/**
+ * Given a constant-style string (e.g., 'FOO_BAR'), will return a camel-cased
+ * string (e.g., fooBar).
+ * @param {string} str String to convert to camelCase.
+ * @return {string} The camel-cased string.
+ */
+os.convertToCamelCase = function(str) {
+  // Preserve the upper case string, to work around problems in some locales
+  // (such as Turkish, where 'I'.toLowerCase().toUpperCase() != 'I')
+  var upper = str.toUpperCase();
+  var words = str.toLowerCase().split('_');
+  var out = [];
+  var index = words[0].length + 1;
+  out.push(words[0]);
+  for (var i = 1; i < words.length; ++i) {
+    if (words[i].length > 0) {
+      var piece = upper.charAt(index) + words[i].substring(1);
+      out.push(piece);
+    }
+    index += words[i].length + 1;
+  }
+  return out.join('');
+};
diff --git a/trunk/features/src/main/javascript/features/osapi.base/batch.js b/trunk/features/src/main/javascript/features/osapi.base/batch.js
new file mode 100644
index 0000000..9d7a6de
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi.base/batch.js
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+(function() {
+
+  /**
+   * It is common to batch requests together to make them more efficient.
+   *
+   * Note: the container config specified endpoints at which services are to be
+   * found. When creating a batch, the calls are split out into separate
+   * requests based on the transport, as it may get sent to a different server
+   * on the backend.
+   */
+  var newBatch = function() {
+    var that = {};
+
+    // An array of requests where each request is
+    // { key : <key>
+    //   request : {
+    //     method : <service-method>
+    //     rpc  : <request params>
+    //     transport : <rpc dispatcher>
+    //  }
+    // }
+
+    /** @type {Array.<Object>} */
+    var keyedRequests = [];
+
+    /**
+     * Create a new request in the batch
+     * @param {string} key id for the request.
+     * @param {Object} request the opensocial request object which is of the form
+     * { method : <service-method>
+     *   rpc  : <request>
+     *   transport : <rpc dispatcher>
+     * }.
+     */
+    var add = function(key, request) {
+      if (request && key) {
+        keyedRequests.push({'key' : key, 'request' : request});
+      }
+      return that;
+    };
+
+    /**
+     * Convert our internal request format into a JSON-RPC
+     * @param {Object} request
+     */
+    var toJsonRpc = function(request) {
+      var jsonRpc = { 'method': request['request']['method'], 'id': request['key'] };
+      if (request['request']['rpc']) {
+        jsonRpc['params'] = request['request']['rpc'];
+      }
+      return jsonRpc;
+    };
+
+    /**
+     * Call to make a batch execute its requests. Batch will distribute calls over their
+     * bound transports and then merge them before calling the userCallback. If the result
+     * of an rpc is another rpc request then it will be chained and executed.
+     *
+     * @param {function(Object)} userCallback the callback to the gadget where results are passed.
+     */
+    var execute = function(userCallback) {
+      var batchResult = {};
+
+      var perTransportBatch = {};
+
+      // Break requests into their per-transport batches in call order
+      /** @type {number} */
+      var latchCount = 0;
+      var transports = [];
+      for (var i = 0; i < keyedRequests.length; i++) {
+        // Batch requests per-transport
+        var transport = keyedRequests[i]['request']['transport'];
+        if (!perTransportBatch[transport['name']]) {
+          transports.push(transport);
+          latchCount++;
+        }
+        perTransportBatch[transport['name']] = perTransportBatch[transport['name']] || [];
+
+        // Transform the request into JSON-RPC form before sending to the transport
+        perTransportBatch[transport['name']].push(toJsonRpc(keyedRequests[i]));
+      }
+
+      // Define callback for transports
+      var transportCallback = function(transportBatchResult) {
+        if (transportBatchResult['error']) {
+          batchResult['error'] = transportBatchResult['error'];
+        }
+        // Merge transport results into overall result and hoist data.
+        // All transport results are required to be of the format
+        // { <key> : <JSON-RPC response>, ...}
+        for (var i = 0; i < keyedRequests.length; i++) {
+          var key = keyedRequests[i]['key'];
+          var response = transportBatchResult[key];
+          if (response) {
+            if (response['error']) {
+              // No need to hoist error responses
+              batchResult[key] = response;
+            } else {
+              // Handle both compliant and non-compliant JSON-RPC data responses.
+              batchResult[key] = response['data'] || response['result'];
+            }
+          }
+        }
+
+        // Latch on no. of transports before calling user callback
+        latchCount--;
+        if (latchCount === 0) {
+          userCallback(batchResult);
+        }
+      };
+
+      // For each transport execute its local batch of requests
+      for (var j = 0; j < transports.length; j++) {
+        transports[j].execute(perTransportBatch[transports[j]['name']], transportCallback);
+      }
+
+      // Force the callback to occur asynchronously even if there were no actual calls
+      if (latchCount == 0) {
+        window.setTimeout(function() {userCallback(batchResult)}, 0);
+      }
+    };
+
+    that.execute = execute;
+    that.add = add;
+    return that;
+  };
+
+  osapi.newBatch = newBatch;
+})();
diff --git a/trunk/features/src/main/javascript/features/osapi.base/feature.xml b/trunk/features/src/main/javascript/features/osapi.base/feature.xml
new file mode 100644
index 0000000..1649a70
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi.base/feature.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>osapi.base</name>
+  <dependency>globals</dependency>
+  <dependency>core.log</dependency>
+  <dependency>taming</dependency>
+  <all>
+    <script src="batch.js"></script>
+    <script src="osapi.js"></script>
+    <script src="taming.js" caja="1"></script>    
+    <api>
+      <exports type="js">osapi.newBatch.add</exports>
+      <exports type="js">osapi.newBatch.execute</exports>
+      <exports type="js">osapi._registerMethod</exports>
+      <exports type="js">osapi._BoundCall</exports>
+      <exports type="js">osapi._BoundCall.prototype.execute</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/osapi.base/osapi.js b/trunk/features/src/main/javascript/features/osapi.base/osapi.js
new file mode 100644
index 0000000..05b9248
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi.base/osapi.js
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Called by the transports for each service method that they expose
+ * @param {string} method  The method to expose e.g. "people.get".
+ * @param {Object.<string,Object>} transport The transport used to
+ *    execute a call for the method.
+ */
+osapi._registerMethod = function(method, transport) {
+  // Skip registration of local newBatch implementation.
+  if (method === 'newBatch') {
+    return;
+  }
+
+  // Lookup last method value.
+  var parts = method.split('.');
+  var last = osapi;
+  for (var i = 0; i < parts.length - 1; i++) {
+    last[parts[i]] = last[parts[i]] || {};
+    last = last[parts[i]];
+  }
+  var basename = parts[parts.length - 1], old;
+
+  // registered methods are now 'last one in wins'.  See tamings__ below.
+  if (last[basename]) {
+    old = last[basename];
+  }
+
+  last[basename] = function(rpc) {
+    // TODO: This shouldn't really be necessary. The spec should be clear
+    // enough about defaults that we dont have to populate this.
+    rpc = rpc || {};
+    rpc['userId'] = rpc['userId'] || '@viewer';
+    rpc['groupId'] = rpc['groupId'] || '@self';
+    var boundCall = new osapi._BoundCall(method, transport, rpc);
+    return boundCall;
+  };
+
+  if (typeof tamings___ !== 'undefined') {
+    var newTaming = function() {
+      caja___.markFunction(last[basename], method);
+    };
+
+    // Remove the old taming if we are replacing it, no sense in growing the
+    // array needlessly. This function (_registerMethod) gets called a lot.
+    if (old && old.__taming_index) {
+      last[basename].__taming_index = old.__taming_index;
+      tamings___[old.__taming_index] = newTaming;
+    }
+    else {
+      last[basename].__taming_index = tamings___.length;
+      tamings___.push(newTaming);
+    }
+  }
+};
+
+// This was formerly an anonymous ad-hoc object, but that triggers a caja
+// bug: http://code.google.com/p/google-caja/issues/detail?id=1355
+// Workaround is to make it a class.
+osapi._BoundCall = function(method, transport, rpc) {
+  this['method'] = method;
+  this['transport'] = transport;
+  this['rpc'] = rpc;
+};
+
+osapi._BoundCall.prototype.execute = function(callback) {
+  var cajaReady = (typeof caja___ !== 'undefined'
+                   && caja___.getUseless
+                   && caja___.getUseless());
+  var that = cajaReady ? caja___.getUseless() : this;
+  var feralCallback = cajaReady ? caja___.untame(callback) : callback;
+  var batch = osapi.newBatch();
+  batch.add(this.method, this);
+  batch.execute(function(batchResult) {
+    if (batchResult.error) {
+      // Forward the error back.
+      feralCallback.call(that, { error: batchResult.error });
+    } else {
+      feralCallback.call(that, batchResult[that.method]);
+    }
+  });
+};
+
diff --git a/trunk/features/src/main/javascript/features/osapi.base/taming.js b/trunk/features/src/main/javascript/features/osapi.base/taming.js
new file mode 100644
index 0000000..ef447fe
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi.base/taming.js
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core osapi.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  function newBatch() {
+    var batch = osapi.newBatch();
+    caja___.whitelistFuncs([
+      [batch, 'add'],
+      [batch, 'execute']
+    ]);
+    return caja___.tame(batch);
+  }
+  caja___.markFunction(newBatch, 'newBatch');
+  caja___.tamesTo(osapi.newBatch, newBatch);
+  caja___.whitelistCtors([
+    [osapi, '_BoundCall', Object]
+  ]);
+  caja___.whitelistMeths([
+    [osapi._BoundCall, 'execute']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/osapi/feature.xml b/trunk/features/src/main/javascript/features/osapi/feature.xml
new file mode 100644
index 0000000..4c947cf
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi/feature.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>osapi</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>shindig.auth</dependency>
+  <dependency>core.config</dependency>
+  <dependency>core.io</dependency>
+  <dependency>core.json</dependency>
+  <dependency>core.util</dependency>
+  <dependency>osapi.base</dependency>
+  <dependency>rpc</dependency>
+  <dependency>security-token</dependency>
+  <container>
+    <script src="jsonrpctransport.js"></script>
+    <script src="peoplehelpers.js"></script>
+    <script src="taming.js" caja="1"></script>    
+  </container>
+  <gadget>
+    <script src="jsonrpctransport.js"></script>
+    <script src="gadgetsrpctransport.js"></script>
+    <script src="peoplehelpers.js"></script>    
+    <script src="taming.js" caja="1"></script>
+    <api>
+      <uses type="rpc">osapi._handleGadgetRpcMethod</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/osapi/gadgetsrpctransport.js b/trunk/features/src/main/javascript/features/osapi/gadgetsrpctransport.js
new file mode 100644
index 0000000..7861fe5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi/gadgetsrpctransport.js
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * A transport for osapi based on gadgets.rpc. Allows osapi to expose APIs requiring container
+ * and user UI mediation in addition to allowing data oriented APIs to be implemented using
+ * gadgets.rpc instead of XHR/JSON-RPC/REST etc..
+ */
+if (gadgets && gadgets.rpc) { //Dont bind if gadgets.rpc not defined
+  (function() {
+
+    /**
+     * Execute the JSON-RPC batch of gadgets.rpc. The container is expected to implement
+     * the method osapi._handleGadgetRpcMethod(<JSON-RPC batch>)
+     *
+     * @param {Object} requests the opensocial JSON-RPC request batch.
+     * @param {function(Object)} callback to the osapi batch with either an error response or
+     * a JSON-RPC batch result.
+     * @private
+     */
+    function execute(requests, callback) {
+      var rpcCallback = function(response) {
+        if (!response) {
+          callback({ 'code': 500, 'message': 'Container refused the request' });
+        } else if (response['error']) {
+          callback(response);
+        } else {
+          var responseMap = {};
+          for (var i = 0; i < response.length; i++) {
+            responseMap[response[i]['id']] = response[i];
+          }
+          callback(responseMap);
+        }
+      };
+      gadgets.rpc.call('..', 'osapi._handleGadgetRpcMethod', rpcCallback, requests);
+      // TODO - Timeout handling if rpc silently fails?
+    }
+
+    function init(config) {
+      var transport = { 'name': 'gadgets.rpc', 'execute' : execute };
+      var services = config['osapi.services'];
+      if (services) {
+        // Iterate over the defined services, extract the gadget.rpc endpoint and
+        // bind to it
+        for (var endpointName in services) {
+          if (services.hasOwnProperty(endpointName)) {
+            if (endpointName === 'gadgets.rpc') {
+              var methods = services[endpointName];
+              for (var i = 0; i < methods.length; i++) {
+                osapi._registerMethod(methods[i], transport);
+              }
+            }
+          }
+        }
+      }
+
+      // Check if the container.listMethods is bound? If it is then use it to
+      // introspect the container services for available methods and bind them
+      // Because the call is asynchronous we delay the execution of the gadget onLoad
+      // handler until the callback has completed. Containers wishing to avoid this
+      // behavior should not specify a binding for container.listMethods in their
+      // container config but rather list out all the container methods they want to
+      // expose directly which is the preferred option for production environments
+      if (osapi.container && osapi.container.listMethods) {
+
+        // Intercept the onload handler so that it is not called until
+        // - gadgets.util.runOnLoadHandlers called at end of gadget,
+        //   and either
+        //   - callback from container.listMethods
+        //   - callback from window.setTimeout
+        var originalRunOnLoadHandlers = gadgets.util.runOnLoadHandlers;
+        var gadgetFlag = false;
+        var listMethodsFlag = false;
+        var triggered = false;
+        var trigger = function() {
+          if (!triggered && gadgetFlag && listMethodsFlag) {
+            triggered = true;
+            originalRunOnLoadHandlers();
+          }
+        };
+        gadgets.util.runOnLoadHandlers = function() {
+          gadgetFlag = true;
+          trigger();
+        };
+
+        // Call for the container methods and bind them to osapi.
+        osapi.container.listMethods({}).execute(function(response) {
+          if (!response['error']) {
+            for (var i = 0; i < response.length; i++) {
+              // do not rebind container.listMethods implementation
+              if (response[i] != 'container.listMethods') {
+                osapi._registerMethod(response[i], transport);
+              }
+            }
+          }
+          listMethodsFlag = true;
+          trigger();
+        });
+
+        // Wait 500ms for the rpc. This should be a reasonable upper bound
+        // even for slow transports while still allowing for reasonable testing
+        // in a development environment
+        window.setTimeout(function() {
+          listMethodsFlag = true;
+          trigger();
+        }, 500);
+      }
+    }
+
+    // Do not run this in container mode.
+    if (gadgets.config) {
+      gadgets.config.register('osapi.services', null, init);
+    }
+  })();
+}
diff --git a/trunk/features/src/main/javascript/features/osapi/jsonrpctransport.js b/trunk/features/src/main/javascript/features/osapi/jsonrpctransport.js
new file mode 100644
index 0000000..cc118e3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi/jsonrpctransport.js
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Provide a transport of osapi requests over JSON-RPC. Exposed JSON-RPC endpoints and
+ * their associated methods are available from config in the "osapi.services" field.
+ */
+(function() {
+
+  var useOAuth2;
+
+  /**
+   * Called by a batch to execute all requests
+   * @param {Object} requests
+   * @param {function(Object)} callback
+   */
+  function execute(requests, callback) {
+    function processResponse(response) {
+      // Convert an XHR failure to a JSON-RPC error
+      if (response['errors'][0]) {
+        callback({
+          error: {
+            'code': response['rc'],
+            'message': response['text']
+          }
+        });
+      } else {
+        var jsonResponse = response['result'] || response['data'];
+        if (jsonResponse['error']) {
+          callback(jsonResponse);
+        } else {
+          var responseMap = {};
+          for (var i = 0; i < jsonResponse.length; i++) {
+            responseMap[jsonResponse[i]['id']] = jsonResponse[i];
+          }
+          callback(responseMap);
+        }
+      }
+    }
+
+    var request = {
+      'POST_DATA' : gadgets.json.stringify(requests),
+      'CONTENT_TYPE' : 'JSON',
+      'METHOD' : 'POST',
+      'AUTHORIZATION' : 'SIGNED'
+    };
+    var headers = {'Content-Type': 'application/json'};
+
+    var url = this.name;
+    var token = shindig.auth.getSecurityToken();
+    if (token) {
+      if (useOAuth2) {
+        headers['Authorization'] = 'OAuth2 ' + token;
+      } else {
+        url += '?st=';
+        url += encodeURIComponent(token);
+      }
+    }
+    gadgets.io.makeNonProxiedRequest(url, processResponse, request, headers);
+  }
+
+  function init(config) {
+    var services = config['osapi.services'];
+    useOAuth2 = config['osapi.useOAuth2'];
+    if (services) {
+      // Iterate over the defined services, extract the http endpoints and
+      // create a transport per-endpoint
+      for (var endpointName in services) {
+        if (services.hasOwnProperty(endpointName)) {
+          if (endpointName.indexOf('http') == 0 ||
+              endpointName.indexOf('//') == 0) {
+            // Expand the host & append the security token
+            var endpointUrl = endpointName.replace('%host%', document.location.host);
+            var transport = { 'name' : endpointUrl, 'execute' : execute };
+            var methods = services[endpointName];
+            for (var i = 0; i < methods.length; i++) {
+              osapi._registerMethod(methods[i], transport);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  // We do run this in the container mode in the new common container
+  if (gadgets.config) {
+    gadgets.config.register('osapi.services', null, init);
+  }
+
+})();
diff --git a/trunk/features/src/main/javascript/features/osapi/peoplehelpers.js b/trunk/features/src/main/javascript/features/osapi/peoplehelpers.js
new file mode 100644
index 0000000..82db3a5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi/peoplehelpers.js
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Service to retrieve People via JSON RPC opensocial calls.
+ * Called in onLoad handler as osapi.people.get could be defined by
+ * the container over the gadgets.rpc transport.
+ */
+gadgets.util.registerOnLoadHandler(function() {
+
+  // No point defining these if osapi.people.get doesn't exist
+  if (osapi && osapi.people && osapi.people.get) {
+    /**
+    * Helper functions to get People.
+    * Options specifies parameters to the call as outlined in the
+    * JSON RPC Opensocial Spec
+    * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/rpc-protocol
+    * @param {object.<JSON>} The JSON object of parameters for the specific request.
+    */
+    /**
+      * Function to get Viewer profile.
+      * Options specifies parameters to the call as outlined in the
+      * JSON RPC Opensocial Spec
+      * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/rpc-protocol
+      * @param {object.<JSON>} The JSON object of parameters for the specific request.
+      */
+    osapi.people.getViewer = function(options) {
+      options = options || {};
+      options.userId = '@viewer';
+      options.groupId = '@self';
+      return osapi.people.get(options);
+    };
+
+    /**
+      * Function to get Viewer's friends'  profiles.
+      * Options specifies parameters to the call as outlined in the
+      * JSON RPC Opensocial Spec
+      * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/rpc-protocol
+      * @param {object.<JSON>} The JSON object of parameters for the specific request.
+      */
+    osapi.people.getViewerFriends = function(options) {
+      options = options || {};
+      options.userId = '@viewer';
+      options.groupId = '@friends';
+      return osapi.people.get(options);
+    };
+
+    /**
+      * Function to get Owner profile.
+      * Options specifies parameters to the call as outlined in the
+      * JSON RPC Opensocial Spec
+      * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/rpc-protocol
+      * @param {object.<JSON>} The JSON object of parameters for the specific request.
+      */
+    osapi.people.getOwner = function(options) {
+      options = options || {};
+      options.userId = '@owner';
+      options.groupId = '@self';
+      return osapi.people.get(options);
+    };
+
+    /**
+      * Function to get Owner's friends' profiles.
+      * Options specifies parameters to the call as outlined in the
+      * JSON RPC Opensocial Spec
+      * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/rpc-protocol
+      * @param {object.<JSON>} The JSON object of parameters for the specific request.
+      */
+    osapi.people.getOwnerFriends = function(options) {
+      options = options || {};
+      options.userId = '@owner';
+      options.groupId = '@friends';
+      return osapi.people.get(options);
+    };
+  }
+});
diff --git a/trunk/features/src/main/javascript/features/osapi/taming.js b/trunk/features/src/main/javascript/features/osapi/taming.js
new file mode 100644
index 0000000..cbec475
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osapi/taming.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose people osapi.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+
+  // Forced to tame in an onload handler because peoplehelpers does
+  // not define some functions till runOnLoadHandlers runs
+  var savedImports = imports;
+  gadgets.util.registerOnLoadHandler(function() {
+    if (osapi && osapi.people && osapi.people.get) {
+      caja___.whitelistFuncs([
+        [osapi.people, 'getViewer'],
+        [osapi.people, 'getViewerFriends'],
+        [osapi.people, 'getOwner'],
+        [osapi.people, 'getOwnerFriends']
+      ]);
+    }
+  });
+});
diff --git a/trunk/features/src/main/javascript/features/osml/feature.xml b/trunk/features/src/main/javascript/features/osml/feature.xml
new file mode 100644
index 0000000..73c3677
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/osml/feature.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <!--
+    The osml feature signals TemplateRewriter to only process OSML tags, a strict
+    subset of OpenSocial Templating. This is defined by section 3 of the
+    OpenSocial Markup Language Tags Specification v0.9
+  -->
+  <name>osml</name>
+  <dependency>opensocial-data</dependency>
+  <dependency>opensocial-templates</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml b/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml
new file mode 100644
index 0000000..2f870a2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/proxied-form-post/feature.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+<!--
+  Required configuration:
+
+  jsonProxyUrl: A url pointing to the JSON proxy endpoint, used by
+      gadgets.io.makeRequest. All data passed to this end point will be
+      encoded inside of the POST body.
+-->
+  <name>proxied-form-post</name>
+  <dependency>core.io</dependency>
+  <dependency>security-token</dependency>
+  <dependency>taming</dependency>
+  <gadget>
+    <script src="post.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.io.proxiedMultipartFormPost</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/proxied-form-post/post.js b/trunk/features/src/main/javascript/features/proxied-form-post/post.js
new file mode 100644
index 0000000..a15c07b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/proxied-form-post/post.js
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This provides the ability to upload a file through the shindig
+ *         proxy by posting a form element.
+ */
+
+(function () {
+  var config,
+      iframe,
+      work,
+      workQ = [],
+      workTimout;
+
+  /**
+   * @param {Object} configuration Configuration settings.
+   * @private
+   */
+  function init(configuration) {
+    config = configuration['core.io'] || {};
+  }
+  gadgets.config.register('core.io', {
+    "jsonProxyUrl": gadgets.config.NonEmptyStringValidator
+  }, init);
+
+  // IE and FF3.6 only
+  function getIFrame() {
+    if (!iframe) {
+      var container = gadgets.util.createElement('div');
+      container.innerHTML =
+          '<iframe name="os-xhrframe"'
+        + ' style="position:absolute;left:1px;top:1px;height:1px;width:1px;visibility:hidden"'
+        + ' onload="gadgets.io.proxiedMultipartFormPostCB_();"></iframe>';
+      gadgets.util.getBodyElement().appendChild(iframe = container.firstChild);
+    }
+    return iframe;
+  }
+
+  gadgets.io.proxiedMultipartFormPostCB_ = function(event) {
+    if (!work) {
+      return;
+    }
+
+    try {
+      var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document) || iframe.document,
+          data = doc.getElementsByTagName('textarea')[0].value;
+    } catch (e) {}
+    var xhrobj = {
+      readyState: 4,
+      status: data ? 200 : 500,
+      responseText: data ? data : 'Unknown error.'
+    };
+    work.form.setAttribute('action', work.url);
+
+    gadgets.io.processResponse_.call(null, work.url, work.onresult, work.params, xhrobj);
+    work = 0;
+    if (workQ.length) {
+      work = workQ.shift();
+      work.form.submit();
+    }
+  };
+
+  /**
+   * Posts a form through the proxy to a remote service.
+   *
+   * @param {Element} form The form element to be posted. This form element must
+   *           include the action attribute for where you want the data to be posted.
+   * @param {Object} params The request options. Similar to gadgets.io.makeRequest
+   * @param {function} onresult The callback to process the success or failure of
+   *           the post.  Similar to gadgets.io.makeRequest's callback param.
+   * @param {function=} opt_onprogress The callback to call with progress updates.
+   *           This callback may not be called if the browser does not
+   *           support the api. Please note that this only reflects the progress of
+   *           uploading to the shindig proxy and does not account for progress of
+   *           the request from the shindig proxy to the remote server.
+   *           This callback takes 2 arguments:
+   *             {Event} event The progress event.
+   *             {function} abort Function to call to abort the post.
+   * @param {FormData=} opt_formdata The FormData object to post. If provided, this
+   *           object will be used as the data to post the provided form and the form
+   *           element provided should contain the action attribute of where to post
+   *           the form. If ommitted and the browser supports it, a FormData object
+   *           will be created from the provided form element.
+   */
+  gadgets.io.proxiedMultipartFormPost = function (form, params, onresult, onprogress, formdata) {
+    params = params || {};
+
+    var auth, signOwner,
+        signViewer = signOwner = true,
+        st = shindig.auth.getSecurityToken(),
+        url = form.getAttribute('action'),
+        contentType = 'multipart/form-data',
+        headers = params['HEADERS'] || (params['HEADERS'] = {}),
+        urlParams = gadgets.util.getUrlParameters();
+
+    if (params['AUTHORIZATION'] && params['AUTHORIZATION'] !== 'NONE') {
+      auth = params['AUTHORIZATION'].toLowerCase();
+    }
+    // Include owner information?
+    if (typeof params['SIGN_OWNER'] !== 'undefined') {
+      signOwner = params['SIGN_OWNER'];
+    }
+    // Include viewer information?
+    if (typeof params['SIGN_VIEWER'] !== 'undefined') {
+      signViewer = params['SIGN_VIEWER'];
+    }
+
+    if (!url) {
+      throw new Error('Form missing action attribute.');
+    }
+    if (!st) {
+      throw new Error('Something went wrong, security token is unavailable.');
+    }
+
+    form.setAttribute('enctype', headers['Content-Type'] = contentType);
+
+    // Info that the proxy endpoint needs.
+    var query = {
+      'MPFP': 1, // This will force an alternate route in the makeRequest proxy endpoint
+      'url': url,
+      'httpMethod': 'POST',
+      'headers': gadgets.io.encodeValues(headers, false),
+      'authz': auth || '',
+      'st': st,
+      'contentType': params['CONTENT_TYPE'] || 'TEXT',
+      'signOwner': signOwner,
+      'signViewer': signViewer,
+      // should we bypass gadget spec cache (e.g. to read OAuth provider URLs)
+      'bypassSpecCache': gadgets.util.getUrlParameters()['nocache'] || '',
+      'getFullHeaders': !!params['GET_FULL_HEADERS']
+    };
+
+    delete params['OAUTH_RECEIVED_CALLBACK'];
+    // OAuth goodies
+    if (auth === 'oauth' || auth === 'signed' || auth === 'oauth2') {
+      // Just copy the OAuth parameters into the req to the server
+      for (var opt in params) {
+        if (params.hasOwnProperty(opt)) {
+          if (opt.indexOf('OAUTH_') === 0 || opt === 'code') {
+            query[opt] = params[opt];
+          }
+        }
+      }
+    }
+
+    var proxyUrl = config['jsonProxyUrl'].replace('%host%', document.location.host)
+      + '?' + gadgets.io.encodeValues(query);
+
+    if (window.FormData) {
+      var xhr = new XMLHttpRequest(),
+          data = formdata || new FormData(form);
+
+      if (xhr.upload) {
+        xhr.upload.onprogress = function(event) {
+          onprogress.call(null, event, function(){ xhr.abort(); });
+        };
+      }
+      xhr.onreadystatechange = gadgets.util.makeClosure(
+        null, gadgets.io.processResponse_, url, onresult, params, xhr
+      );
+      xhr.open("POST", proxyUrl);
+      xhr.send(data);
+    } else {
+      // IE and FF3.6 only
+      proxyUrl += '&iframe=1';
+      form.setAttribute('action', proxyUrl);
+      form.setAttribute('target', getIFrame().name);
+      form.setAttribute('method', 'POST');
+
+      // This transport can only support 1 request at a time, so we serialize
+      // them.
+      var job = {
+        form: form,
+        onresult: onresult,
+        params: params,
+        url: url
+      };
+      if (work) {
+        workQ.push(job);
+      } else {
+        work = job;
+        form.submit();
+      }
+    }
+  };
+
+})();
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/proxied-form-post/taming.js b/trunk/features/src/main/javascript/features/proxied-form-post/taming.js
new file mode 100644
index 0000000..0a4e2c6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/proxied-form-post/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.io.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.io, 'proxiedMultipartFormPost']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/pubsub/feature.xml b/trunk/features/src/main/javascript/features/pubsub/feature.xml
new file mode 100644
index 0000000..63c01c8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/pubsub/feature.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>pubsub</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="pubsub.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="rpc">pubsub</exports>
+      <uses type="rpc">pubsub</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="pubsub-router.js"/>
+    <api>
+      <exports type="rpc">pubsub</exports>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/pubsub/pubsub-router.js b/trunk/features/src/main/javascript/features/pubsub/pubsub-router.js
new file mode 100644
index 0000000..5711ffc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/pubsub/pubsub-router.js
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side message router for PubSub, a gadget-to-gadget
+ * communication library.
+ */
+
+/**
+ * @static
+ * @class Routes PubSub messages.
+ * @name gadgets.pubsubrouter
+ */
+gadgets.pubsubrouter = function() {
+  var gadgetIdToSpecUrl;
+  var subscribers = {};
+  var onSubscribe;
+  var onUnsubscribe;
+  var onPublish;
+
+  function router(command, channel, message) {
+    var gadgetId = this.f;
+    var sender = gadgetId === '..' ? 'container' : gadgetIdToSpecUrl(gadgetId);
+    if (sender) {
+      switch (command) {
+        case 'subscribe':
+          if (onSubscribe && onSubscribe(gadgetId, channel)) {
+            break;
+          }
+          if (!subscribers[channel]) {
+            subscribers[channel] = {};
+          }
+          subscribers[channel][gadgetId] = true;
+          break;
+        case 'unsubscribe':
+          if (onUnsubscribe && onUnsubscribe(gadgetId, channel)) {
+            break;
+          }
+          if (subscribers[channel]) {
+            delete subscribers[channel][gadgetId];
+          }
+          break;
+        case 'publish':
+          if (onPublish && onPublish(gadgetId, channel, message)) {
+            break;
+          }
+          var channelSubscribers = subscribers[channel];
+          if (channelSubscribers) {
+            for (var subscriber in channelSubscribers) {
+              if (channelSubscribers.hasOwnProperty(subscriber)) {
+                gadgets.rpc.call(subscriber, 'pubsub', null, channel, sender, message);
+              }
+            }
+          }
+          break;
+        default:
+          throw new Error('Unknown pubsub command');
+      }
+    }
+  }
+
+  return /** @scope gadgets.pubsubrouter */ {
+    /**
+     * Initializes the PubSub message router.
+     * @param {function(number)} gadgetIdToSpecUrlHandler Function that returns the full
+     *                   gadget spec URL of a given gadget id. For example:
+     *                   function(id) { return idToUrlMap[id]; }.
+     * @param {Object=} opt_callbacks Optional event handlers. Supported handlers:
+     *                 opt_callbacks.onSubscribe: function(gadgetId, channel)
+     *                   Called when a gadget tries to subscribe to a channel.
+     *                 opt_callbacks.onUnsubscribe: function(gadgetId, channel)
+     *                   Called when a gadget tries to unsubscribe from a channel.
+     *                 opt_callbacks.onPublish: function(gadgetId, channel, message)
+     *                   Called when a gadget tries to publish a message.
+     *                 All these event handlers may reject a particular PubSub
+     *                 request by returning true.
+     */
+    init: function(gadgetIdToSpecUrlHandler, opt_callbacks) {
+      if (typeof gadgetIdToSpecUrlHandler !== 'function') {
+        throw new Error('Invalid handler');
+      }
+      if (typeof opt_callbacks === 'object') {
+        onSubscribe = opt_callbacks.onSubscribe;
+        onUnsubscribe = opt_callbacks.onUnsubscribe;
+        onPublish = opt_callbacks.onPublish;
+      }
+      gadgetIdToSpecUrl = gadgetIdToSpecUrlHandler;
+      gadgets.rpc.register('pubsub', router);
+    },
+
+    /**
+     * Publishes a message to a channel.
+     * @param {string} channel Channel name.
+     * @param {string} message Message to publish.
+     */
+    publish: function(channel, message) {
+      router.call({f: '..'}, 'publish', channel, message);
+    }
+  };
+}();
+
diff --git a/trunk/features/src/main/javascript/features/pubsub/pubsub.js b/trunk/features/src/main/javascript/features/pubsub/pubsub.js
new file mode 100644
index 0000000..01254c1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/pubsub/pubsub.js
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Gadget-side PubSub library for gadget-to-gadget communication.
+ */
+
+/**
+ * @static
+ * @class Provides operations for making rpc calls.
+ * @name gadgets.pubsub
+ */
+gadgets.pubsub = function() {
+  var listeners = {};
+
+  function router(channel, sender, message) {
+    if (this.f !== '..') {
+      return;
+    }
+    var listener = listeners[channel];
+    if (typeof listener === 'function') {
+      listener(sender, message);
+    }
+  }
+
+  return /** @scope gadgets.pubsub */ {
+    /**
+     * Publishes a message to a channel.
+     * @param {string} channel Channel name.
+     * @param {string} message Message to publish.
+     */
+    publish: function(channel, message) {
+      gadgets.rpc.call('..', 'pubsub', null, 'publish', channel, message);
+    },
+
+    /**
+     * Subscribes to a channel.
+     * @param {string} channel Channel name.
+     * @param {function(Object,Object)} callback Callback function that receives messages.
+     *                   For example:
+     *                   function(sender, message) {
+     *                     if (isTrustedGadgetSpecUrl(sender)) {
+     *                       processMessage(message);
+     *                     }
+     *                   }.
+     */
+    subscribe: function(channel, callback) {
+      listeners[channel] = callback;
+      gadgets.rpc.register('pubsub', router);
+      gadgets.rpc.call('..', 'pubsub', null, 'subscribe', channel);
+    },
+
+    /**
+     * Unsubscribes from a channel.
+     * @param {string} channel Channel name.
+     */
+    unsubscribe: function(channel) {
+      delete listeners[channel];
+      gadgets.rpc.call('..', 'pubsub', null, 'unsubscribe', channel);
+    }
+
+  };
+}();
+
diff --git a/trunk/features/src/main/javascript/features/pubsub/taming.js b/trunk/features/src/main/javascript/features/pubsub/taming.js
new file mode 100644
index 0000000..af978f5
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/pubsub/taming.js
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.pubsub.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.pubsub, 'publish'],
+    [gadgets.pubsub, 'subscribe'],
+    [gadgets.pubsub, 'unsubscribe']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/rpc/fe.transport.js b/trunk/features/src/main/javascript/features/rpc/fe.transport.js
new file mode 100644
index 0000000..1304d40
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/fe.transport.js
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.rpctx = gadgets.rpctx || {};
+
+/*
+ * For Gecko-based browsers, the security model allows a child to call a
+ * function on the frameElement of the iframe, even if the child is in
+ * a different domain. This method is dubbed "frameElement" (fe).
+ *
+ * The ability to add and call such functions on the frameElement allows
+ * a bidirectional channel to be setup via the adding of simple function
+ * references on the frameElement object itself. In this implementation,
+ * when the container sets up the authentication information for that gadget
+ * (by calling setAuth(...)) it as well adds a special function on the
+ * gadget's iframe. This function can then be used by the gadget to send
+ * messages to the container. In turn, when the gadget tries to send a
+ * message, it checks to see if this function has its own function stored
+ * that can be used by the container to call the gadget. If not, the
+ * function is created and subsequently used by the container.
+ * Note that as a result, FE can only be used by a container to call a
+ * particular gadget *after* that gadget has called the container at
+ * least once via FE.
+ *
+ *   fe: Gecko-specific frameElement trick.
+ *      - Firefox 1+
+ */
+if (!gadgets.rpctx.frameElement) {  // make lib resilient to double-inclusion
+
+  gadgets.rpctx.frameElement = function() {
+    // Consts for FrameElement.
+    var FE_G2C_CHANNEL = '__g2c_rpc';
+    var FE_C2G_CHANNEL = '__c2g_rpc';
+    var process;
+    var ready;
+
+    function callFrameElement(targetId, from, rpc) {
+      try {
+        if (from !== '..') {
+          // Call from gadget to the container.
+          var fe = window.frameElement;
+
+          if (typeof fe[FE_G2C_CHANNEL] === 'function') {
+            // Complete the setup of the FE channel if need be.
+            if (typeof fe[FE_G2C_CHANNEL][FE_C2G_CHANNEL] !== 'function') {
+              fe[FE_G2C_CHANNEL][FE_C2G_CHANNEL] = function(args) {
+                process(gadgets.json.parse(args));
+              };
+            }
+
+            // Conduct the RPC call.
+            fe[FE_G2C_CHANNEL](gadgets.json.stringify(rpc));
+            return true;
+          }
+        } else {
+          // Call from container to gadget[targetId].
+          var frame = document.getElementById(targetId);
+
+          if (typeof frame[FE_G2C_CHANNEL] === 'function' &&
+              typeof frame[FE_G2C_CHANNEL][FE_C2G_CHANNEL] === 'function') {
+
+            // Conduct the RPC call.
+            frame[FE_G2C_CHANNEL][FE_C2G_CHANNEL](gadgets.json.stringify(rpc));
+            return true;
+          }
+        }
+      } catch (e) {
+      }
+      return false;
+    }
+
+    return {
+      getCode: function() {
+        return 'fe';
+      },
+
+      isParentVerifiable: function() {
+        return false;
+      },
+
+      init: function(processFn, readyFn) {
+        // No global setup.
+        process = processFn;
+        ready = readyFn;
+        return true;
+      },
+
+      setup: function(receiverId, token) {
+        // Indicate OK to call to container. This will be true
+        // by the end of this method.
+        if (receiverId !== '..') {
+          try {
+            var frame = document.getElementById(receiverId);
+            frame[FE_G2C_CHANNEL] = function(args) {
+              process(gadgets.json.parse(args));
+            };
+          } catch (e) {
+            return false;
+          }
+        }
+        if (receiverId === '..') {
+          ready('..', true);
+          var ackFn = function() {
+            window.setTimeout(function() {
+              gadgets.rpc.call(receiverId, gadgets.rpc.ACK);
+            }, 500);
+          };
+          // Setup to container always happens before onload.
+          // If it didn't, the correct fix would be in gadgets.util.
+          gadgets.util.registerOnLoadHandler(ackFn);
+        }
+        return true;
+      },
+
+      call: function(targetId, from, rpc) {
+        return callFrameElement(targetId, from, rpc);
+      }
+
+    };
+  }();
+
+} // !end of double-inclusion guard
diff --git a/trunk/features/src/main/javascript/features/rpc/feature.xml b/trunk/features/src/main/javascript/features/rpc/feature.xml
new file mode 100644
index 0000000..cb81e7a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/feature.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>rpc</name>
+  <dependency>globals</dependency>
+  <dependency>core.config.base</dependency>
+  <dependency>core.json</dependency>
+  <dependency>core.util.onload</dependency>
+  <dependency>core.util.urlparams</dependency>
+  <all>
+    <api>
+      <exports type="js">gadgets.rpc.config</exports>
+      <exports type="js">gadgets.rpc.register</exports>
+      <exports type="js">gadgets.rpc.unregister</exports>
+      <exports type="js">gadgets.rpc.registerDefault</exports>
+      <exports type="js">gadgets.rpc.unregisterDefault</exports>
+      <exports type="js">gadgets.rpc.forceParentVerifiable</exports>
+      <exports type="js">gadgets.rpc.call</exports>
+      <exports type="js">gadgets.rpc.getRelayUrl</exports>
+      <exports type="js">gadgets.rpc.setRelayUrl</exports>
+      <exports type="js">gadgets.rpc.setAuthToken</exports>
+      <exports type="js">gadgets.rpc.setupReceiver</exports>
+      <exports type="js">gadgets.rpc.getAuthToken</exports>
+      <exports type="js">gadgets.rpc.removeReceiver</exports>
+      <exports type="js">gadgets.rpc.getRelayChannel</exports>
+      <exports type="js">gadgets.rpc.receive</exports>
+      <exports type="js">gadgets.rpc.receiveSameDomain</exports>
+      <exports type="js">gadgets.rpc.getOrigin</exports>
+      <exports type="js">gadgets.rpc.getTargetOrigin</exports>
+      <uses type="rpc">__ack</uses>
+      <uses type="rpc">__cb</uses>
+    </api>
+    <script src="wpm.transport.js"/>
+    <script src="flash.transport.js"/>
+    <script src="fe.transport.js"/>
+    <script src="nix.transport.js"/>
+    <script src="rmr.transport.js"/>
+    <script src="ifpc.transport.js"/>
+    <script src="rpc.js"/>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/rpc/flash.transport.js b/trunk/features/src/main/javascript/features/rpc/flash.transport.js
new file mode 100644
index 0000000..7fa94c7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/flash.transport.js
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.rpctx = gadgets.rpctx || {};
+
+/**
+ * Transport for browsers that utilizes a small Flash bridge and
+ * Flash's ExternalInterface methods to transport messages securely,
+ * and with guarantees provided on sender identity. This largely emulates wpm.
+ *
+ *   flash: postMessage on the window object.
+ *        - Internet Explorer 6/7
+ *        - In theory, any browser supporting Flash 8 and above,
+ *          though embed code is tailored to IE only to reduce size.
+ *        + (window.postMessage takes precedence where available)
+ */
+if (!gadgets.rpctx.flash) {  // make lib resilient to double-inclusion
+
+  gadgets.rpctx.flash = function() {
+    var swfId = '___xpcswf';
+    var swfUrl = null;
+    var usingFlash = false;
+    var processFn = null;
+    var readyFn = null;
+    var relayHandle = null;
+
+    var LOADER_TIMEOUT_MS = 100;
+    var MAX_LOADER_RETRIES = 50;
+
+    var pendingHandshakes = [];
+    var flushHandshakesHandle = null;
+    var FLUSH_HANDSHAKES_TIMEOUT_MS = 500;
+
+    var setupHandle = null;
+    var setupAttempts = 0;
+
+    var SWF_CHANNEL_READY = '_scr';
+    var SWF_CONFIRMED_PARENT = '_pnt';
+    var READY_TIMEOUT_MS = 100;
+    var MAX_READY_RETRIES = 50;
+    var readyAttempts = 0;
+    var readyHandle = null;
+    var readyMsgs = {};
+
+    var myLoc = window.location.protocol + '//' + window.location.host;
+    var JSL_NS = '___jsl';
+    var METHODS_NS = '_fm';
+    var bucketNs;
+
+    function setupMethodBucket() {
+      window[JSL_NS] = window[JSL_NS] || {};
+      var container = window[JSL_NS];
+      var bucket = container[METHODS_NS] = {};
+      bucketNs = JSL_NS + '.' + METHODS_NS;
+      return bucket;
+    }
+
+    var methodBucket = setupMethodBucket();
+
+    function exportMethod(method, requestedName) {
+      var exported = function() {
+        method.apply({}, arguments);
+      };
+      methodBucket[requestedName] = methodBucket[requestedName] || exported;
+      return bucketNs + '.' + requestedName;
+    }
+
+    function getChannelId(receiverId) {
+      return receiverId === '..' ? gadgets.rpc.RPC_ID : receiverId;
+    }
+
+    function getRoleId(targetId) {
+      return targetId === '..' ? 'INNER' : 'OUTER';
+    }
+
+    function init(config) {
+      if (usingFlash) {
+        swfUrl = config['rpc']['commSwf'] || '//xpc.googleusercontent.com/gadgets/xpc.swf';
+      }
+    }
+    gadgets.config.register('rpc', null, init);
+
+    function relayLoader() {
+      if (relayHandle === null && document.body && swfUrl) {
+        var theSwf = swfUrl + '?cb=' + Math.random() + '&origin=' + myLoc + '&jsl=1';
+
+        var containerDiv = document.createElement('div');
+        containerDiv.style.height = '1px';
+        containerDiv.style.width = '1px';
+        var html = '<object height="1" width="1" id="' + swfId +
+            '" type="application/x-shockwave-flash">' +
+            '<param name="allowScriptAccess" value="always"></param>' +
+            '<param name="movie" value="' + theSwf + '"></param>' +
+            '<embed type="application/x-shockwave-flash" allowScriptAccess="always" ' +
+            'src="' + theSwf + '" height="1" width="1"></embed>' +
+            '</object>';
+
+        document.body.appendChild(containerDiv);
+        containerDiv.innerHTML = html;
+
+        relayHandle = containerDiv.firstChild;
+      }
+      ++setupAttempts;
+      if (setupHandle !== null &&
+          (relayHandle !== null || setupAttempts >= MAX_LOADER_RETRIES)) {
+        window.clearTimeout(setupHandle);
+      } else {
+        // Either document.body doesn't yet exist or config doesn't.
+        // In either case the relay handle isn't set up properly yet, and
+        // so should be retried.
+        setupHandle = window.setTimeout(relayLoader, LOADER_TIMEOUT_MS);
+      }
+    }
+
+    function childReadyPoller() {
+      // Attempt sending a message to parent indicating that child is ready
+      // to receive messages. This only occurs after the SWF indicates that
+      // its setup() method has been successfully called and completed, and
+      // only in child context.
+      if (readyMsgs['..']) return;
+      sendChannelReady('..');
+      readyAttempts++;
+      if (readyAttempts >= MAX_READY_RETRIES && readyHandle !== null) {
+        window.clearTimeout(readyHandle);
+        readyHandle = null;
+      } else {
+        // Try again later. The handle will be cleared during receipt of
+        // the setup ACK.
+        readyHandle = window.setTimeout(childReadyPoller, READY_TIMEOUT_MS);
+      }
+    }
+
+    function flushHandshakes() {
+      if (relayHandle !== null && relayHandle['setup']) {
+        while (pendingHandshakes.length > 0) {
+          var shake = pendingHandshakes.shift();
+          var targetId = shake.targetId;
+          relayHandle['setup'](shake.token, getChannelId(targetId), getRoleId(targetId));
+        }
+        if (flushHandshakesHandle !== null) {
+          window.clearTimeout(flushHandshakesHandle);
+          flushHandshakesHandle = null;
+        }
+      } else if (flushHandshakesHandle === null && pendingHandshakes.length > 0) {
+        flushHandshakesHandle = window.setTimeout(flushHandshakes, FLUSH_HANDSHAKES_TIMEOUT_MS);
+      }
+    }
+
+    function ready() {
+      flushHandshakes();
+      if (setupHandle !== null) {
+        window.clearTimeout(setupHandle);
+      }
+      setupHandle = null;
+    }
+    exportMethod(ready, 'ready');
+
+    function setupDone() {
+      // Called by SWF only for role_id = "INNER" ie when initializing to parent.
+      // Instantiates a polling handshake mechanism which ensures that any enqueued
+      // messages remain so until each side is ready to send.
+      if (!readyMsgs['..'] && readyHandle === null) {
+        readyHandle = window.setTimeout(childReadyPoller, READY_TIMEOUT_MS);
+      }
+    }
+    exportMethod(setupDone, 'setupDone');
+
+    function call(targetId, from, rpc) {
+      var targetOrigin = gadgets.rpc.getTargetOrigin(targetId);
+      var rpcKey = gadgets.rpc.getAuthToken(targetId);
+      var handleKey = 'sendMessage_' + getChannelId(targetId) + '_' + rpcKey + '_' + getRoleId(targetId);
+      var messageHandler = relayHandle[handleKey];
+      messageHandler.call(relayHandle, gadgets.json.stringify(rpc), targetOrigin);
+      return true;
+    }
+
+    function receiveMessage(message, fromOrigin, toOrigin) {
+      var jsonMsg = gadgets.json.parse(message);
+      var channelReady = jsonMsg[SWF_CHANNEL_READY];
+      if (channelReady) {
+        // Special message indicating that a ready message has been received, indicating
+        // the sender is now prepared to receive messages. This type of message is instigated
+        // by child context in polling fashion, and is responded-to by parent context(s).
+        // If readyHandle is non-null, then it should first be cleared.
+        // This method is OK to call twice, if it occurs in a race.
+        readyFn(channelReady, true);
+        readyMsgs[channelReady] = true;
+        if (channelReady !== '..') {
+          // Child-to-parent: immediately signal that parent is ready.
+          // Now that we know that child can receive messages, it's enough to send once.
+          sendChannelReady(channelReady, true);
+        }
+        return;
+      }
+      window.setTimeout(function() { processFn(jsonMsg, fromOrigin); }, 0);
+    }
+    exportMethod(receiveMessage, 'receiveMessage');
+
+    function sendChannelReady(receiverId, opt_isParentConfirmation) {
+      var myId = gadgets.rpc.RPC_ID;
+      var readyAck = {};
+      readyAck[SWF_CHANNEL_READY] = opt_isParentConfirmation ? '..' : myId;
+      readyAck[SWF_CONFIRMED_PARENT] = myId;
+      call(receiverId, myId, readyAck);
+    }
+
+    return {
+      // "core" transport methods
+      getCode: function() {
+        return 'flash';
+      },
+
+      isParentVerifiable: function() {
+        return true;
+      },
+
+      init: function(processIn, readyIn) {
+        processFn = processIn;
+        readyFn = readyIn;
+        usingFlash = true;
+        return true;
+      },
+
+      setup: function(receiverId, token) {
+        // Perform all setup, including embedding of relay SWF (a one-time
+        // per Window operation), in this method. We cannot assume document.body
+        // exists however, since child-to-parent setup is often done in head.
+        // Thus we simply enqueue a setup pair and attempt to complete them all.
+        // If body already exists then this enqueueing will immediately flush;
+        // otherwise polling will occur until the SWF has completed loading, at
+        // which point all connections will complete their handshake.
+        pendingHandshakes.push({ token: token, targetId: receiverId });
+        if (relayHandle === null && setupHandle === null) {
+          setupHandle = window.setTimeout(relayLoader, LOADER_TIMEOUT_MS);
+        }
+        // Lets try to flush any registered handshakes
+        flushHandshakes();
+        return true;
+      },
+
+      call: call,
+
+      // Methods called by relay SWF. Should be considered private.
+      _receiveMessage: receiveMessage,
+      _ready: ready,
+      _setupDone: setupDone
+    };
+  }();
+
+} // !end of double-inclusion guard
diff --git a/trunk/features/src/main/javascript/features/rpc/ifpc.transport.js b/trunk/features/src/main/javascript/features/rpc/ifpc.transport.js
new file mode 100644
index 0000000..4d96c72
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/ifpc.transport.js
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.rpctx = gadgets.rpctx || {};
+
+/*
+ * For all others, we have a fallback mechanism known as "ifpc". IFPC
+ * exploits the fact that while same-origin policy prohibits a frame from
+ * accessing members on a window not in the same domain, that frame can,
+ * however, navigate the window heirarchy (via parent). This is exploited by
+ * having a page on domain A that wants to talk to domain B create an iframe
+ * on domain B pointing to a special relay file and with a message encoded
+ * after the hash (#). This relay, in turn, finds the page on domain B, and
+ * can call a receipt function with the message given to it. The relay URL
+ * used by each caller is set via the gadgets.rpc.setRelayUrl(..) and
+ * *must* be called before the call method is used.
+ *
+ *   ifpc: Iframe-based method, utilizing a relay page, to send a message.
+ *      - No known major browsers still use this method, but it remains
+ *        useful as a catch-all fallback for the time being.
+ */
+if (!gadgets.rpctx.ifpc) {  // make lib resilient to double-inclusion
+
+  gadgets.rpctx.ifpc = function() {
+    var iframePool = [];
+    var callId = 0;
+    var ready;
+
+    var URL_LIMIT = 2000;
+    var messagesIn = {};
+
+    /**
+   * Encodes arguments for the legacy IFPC wire format.
+   *
+   * @param {Object} args
+   * @return {string} the encoded args.
+   */
+    function encodeLegacyData(args) {
+      var argsEscaped = [];
+      for (var i = 0, j = args.length; i < j; ++i) {
+        argsEscaped.push(encodeURIComponent(gadgets.json.stringify(args[i])));
+      }
+      return argsEscaped.join('&');
+    }
+
+    /**
+   * Helper function to emit an invisible IFrame.
+   * @param {string} src SRC attribute of the IFrame to emit.
+   * @private
+   */
+    function emitInvisibleIframe(src) {
+      var iframe;
+      // Recycle IFrames
+      for (var i = iframePool.length - 1; i >= 0; --i) {
+        var ifr = iframePool[i];
+        try {
+          if (ifr && (ifr.recyclable || ifr.readyState === 'complete')) {
+            ifr.parentNode.removeChild(ifr);
+            if (window.ActiveXObject) {
+              // For MSIE, delete any iframes that are no longer being used. MSIE
+              // cannot reuse the IFRAME because a navigational click sound will
+              // be triggered when we set the SRC attribute.
+              // Other browsers scan the pool for a free iframe to reuse.
+              iframePool[i] = ifr = null;
+              iframePool.splice(i, 1);
+            } else {
+              ifr.recyclable = false;
+              iframe = ifr;
+              break;
+            }
+          }
+        } catch (e) {
+          // Ignore; IE7 throws an exception when trying to read readyState and
+          // readyState isn't set.
+        }
+      }
+      // Create IFrame if necessary
+      if (!iframe) {
+        iframe = document.createElement('iframe');
+        iframe.style.border = iframe.style.width = iframe.style.height = '0px';
+        iframe.style.visibility = 'hidden';
+        iframe.style.position = 'absolute';
+        iframe.onload = function() { this.recyclable = true; };
+        iframePool.push(iframe);
+      }
+      iframe.src = src;
+      window.setTimeout(function() { document.body.appendChild(iframe); }, 0);
+    }
+
+    function isMessageComplete(arr, total) {
+      for (var i = total - 1; i >= 0; --i) {
+        if (typeof arr[i] === 'undefined') {
+          return false;
+        }
+      }
+      return true;
+    }
+
+    return {
+      getCode: function() {
+        return 'ifpc';
+      },
+
+      isParentVerifiable: function() {
+        return true;
+      },
+
+      init: function(processFn, readyFn) {
+        // No global setup.
+        ready = readyFn;
+        ready('..', true);  // Ready immediately.
+        return true;
+      },
+
+      setup: function(receiverId, token) {
+        // Indicate readiness to send to receiver.
+        ready(receiverId, true);
+        return true;
+      },
+
+      call: function(targetId, from, rpc) {
+        // Retrieve the relay file used by IFPC. Note that
+        // this must be set before the call, and so we conduct
+        // an extra check to ensure it is not blank.
+        var relay = gadgets.rpc.getRelayUrl(targetId);
+        ++callId;
+
+        if (!relay) {
+          gadgets.warn('No relay file assigned for IFPC');
+          return false;
+        }
+
+        // The RPC mechanism supports two formats for IFPC (legacy and current).
+        var src = null,
+            queueOut = [];
+        if (rpc['l']) {
+          // Use legacy protocol.
+          // Format: #iframe_id&callId&num_packets&packet_num&block_of_data
+          var callArgs = rpc['a'];
+          src = [relay, '#', encodeLegacyData([from, callId, 1, 0,
+            encodeLegacyData([from, rpc['s'], '', '', from].concat(
+                callArgs))])].join('');
+          queueOut.push(src);
+        } else {
+          // Format: #targetId & sourceId@callId & packetNum & packetId & packetData
+          src = [relay, '#', targetId, '&', from, '@', callId, '&'].join('');
+          var message = encodeURIComponent(gadgets.json.stringify(rpc)),
+              payloadLength = URL_LIMIT - src.length,
+              numPackets = Math.ceil(message.length / payloadLength),
+              packetIdx = 0,
+              part;
+          while (message.length > 0) {
+            part = message.substring(0, payloadLength);
+            message = message.substring(payloadLength);
+            queueOut.push([src, numPackets, '&', packetIdx, '&', part].join(''));
+            packetIdx += 1;
+          }
+        }
+
+        // Conduct the IFPC call by creating the Iframe with
+        // the relay URL and appended message.
+        do {
+          emitInvisibleIframe(queueOut.shift());
+        } while (queueOut.length > 0);
+        return true;
+      },
+
+      /** Process message from invisible iframe, merging message parts if necessary. */
+      _receiveMessage: function(fragment, process) {
+        var from = fragment[1],   // in the form of "<from>@<callid>"
+            numPackets = parseInt(fragment[2], 10),
+            packetIdx = parseInt(fragment[3], 10),
+            payload = fragment[fragment.length - 1],
+            completed = numPackets === 1;
+
+        // if message is multi-part, store parts in the proper order
+        if (numPackets > 1) {
+          if (!messagesIn[from]) {
+            messagesIn[from] = [];
+          }
+          messagesIn[from][packetIdx] = payload;
+          // check if all parts have been sent
+          if (isMessageComplete(messagesIn[from], numPackets)) {
+            payload = messagesIn[from].join('');
+            delete messagesIn[from];
+            completed = true;
+          }
+        }
+
+        // complete message sent
+        if (completed) {
+          process(gadgets.json.parse(decodeURIComponent(payload)));
+        }
+      }
+    };
+  }();
+
+} // !end of double inclusion guard
diff --git a/trunk/features/src/main/javascript/features/rpc/nix.transport.js b/trunk/features/src/main/javascript/features/rpc/nix.transport.js
new file mode 100644
index 0000000..8ecce70
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/nix.transport.js
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.rpctx = gadgets.rpctx || {};
+
+/**
+ * For Internet Explorer before version 8, the security model allows anyone
+ * parent to set the value of the "opener" property on another window,
+ * with only the receiving window able to read it.
+ * This method is dubbed "Native IE XDC" (NIX).
+ *
+ * This method works by placing a handler object in the "opener" property
+ * of a gadget when the container sets up the authentication information
+ * for that gadget (by calling setAuthToken(...)). At that point, a NIX
+ * wrapper is created and placed into the gadget by calling
+ * theframe.contentWindow.opener = wrapper. Note that as a result, NIX can
+ * only be used by a container to call a particular gadget *after* that
+ * gadget has called the container at least once via NIX.
+ *
+ * The NIX wrappers in this RPC implementation are instances of a VBScript
+ * class that is created when this implementation loads. The reason for
+ * using a VBScript class stems from the fact that any object can be passed
+ * into the opener property.
+ * While this is a good thing, as it lets us pass functions and setup a true
+ * bidirectional channel via callbacks, it opens a potential security hole
+ * by which the other page can get ahold of the "window" or "document"
+ * objects in the parent page and in turn wreak havok. This is due to the
+ * fact that any JS object useful for establishing such a bidirectional
+ * channel (such as a function) can be used to access a function
+ * (eg. obj.toString, or a function itself) created in a specific context,
+ * in particular the global context of the sender. Suppose container
+ * domain C passes object obj to gadget on domain G. Then the gadget can
+ * access C's global context using:
+ * var parentWindow = (new obj.toString.constructor("return window;"))();
+ * Nulling out all of obj's properties doesn't fix this, since IE helpfully
+ * restores them to their original values if you do something like:
+ * delete obj.toString; delete obj.toString;
+ * Thus, we wrap the necessary functions and information inside a VBScript
+ * object. VBScript objects in IE, like DOM objects, are in fact COM
+ * wrappers when used in JavaScript, so we can safely pass them around
+ * without worrying about a breach of context while at the same time
+ * allowing them to act as a pass-through mechanism for information
+ * and function calls. The implementation details of this VBScript wrapper
+ * can be found in the setupChannel() method below.
+ *
+ *   nix: Internet Explorer-specific window.opener trick.
+ *     - Internet Explorer 6
+ *     - Internet Explorer 7
+ */
+if (!gadgets.rpctx.nix) {  // make lib resilient to double-inclusion
+
+gadgets.rpctx.nix = function() {
+  // Consts for NIX. VBScript doesn't
+  // allow items to start with _ for some reason,
+  // so we need to make these names quite unique, as
+  // they will go into the global namespace.
+  var NIX_WRAPPER = 'GRPC____NIXVBS_wrapper';
+  var NIX_GET_WRAPPER = 'GRPC____NIXVBS_get_wrapper';
+  var NIX_HANDLE_MESSAGE = 'GRPC____NIXVBS_handle_message';
+  var NIX_CREATE_CHANNEL = 'GRPC____NIXVBS_create_channel';
+  var MAX_NIX_SEARCHES = 10;
+  var NIX_SEARCH_PERIOD = 500;
+
+  // JavaScript reference to the NIX VBScript wrappers.
+  // Gadgets will have but a single channel under
+  // nix_channels['..'] while containers will have a channel
+  // per gadget stored under the gadget's ID.
+  var nix_channels = {};
+
+  // Store the ready signal method for use on handshake complete.
+  var ready;
+  var numHandlerSearches = 0;
+
+  // Search for NIX handler to parent. Tries MAX_NIX_SEARCHES times every
+  // NIX_SEARCH_PERIOD milliseconds.
+  function conductHandlerSearch() {
+    // Call from gadget to the container.
+    var handler = nix_channels['..'];
+    if (handler) {
+      return;
+    }
+
+    if (++numHandlerSearches > MAX_NIX_SEARCHES) {
+      // Handshake failed. Will fall back.
+      gadgets.warn('Nix transport setup failed, falling back...');
+      ready('..', false);
+      return;
+    }
+
+    // If the gadget has yet to retrieve a reference to
+    // the NIX handler, try to do so now. We don't do a
+    // typeof(window.opener.GetAuthToken) check here
+    // because it means accessing that field on the COM object, which,
+    // being an internal function reference, is not allowed.
+    // "in" works because it merely checks for the prescence of
+    // the key, rather than actually accessing the object's property.
+    // This is just a sanity check, not a validity check.
+    if (!handler && window.opener && 'GetAuthToken' in window.opener) {
+      handler = window.opener;
+
+      // Create the channel to the parent/container.
+      // First verify that it knows our auth token to ensure it's not
+      // an impostor.
+      if (handler.GetAuthToken() == gadgets.rpc.getAuthToken('..')) {
+        // Auth match - pass it back along with our wrapper to finish.
+        // own wrapper and our authentication token for co-verification.
+        var token = gadgets.rpc.getAuthToken('..');
+        handler.CreateChannel(window[NIX_GET_WRAPPER]('..', token),
+                              token);
+        // Set channel handler
+        nix_channels['..'] = handler;
+        window.opener = null;
+
+        // Signal success and readiness to send to parent.
+        // Container-to-gadget bit flipped in CreateChannel.
+        ready('..', true);
+        return;
+      }
+    }
+
+    // Try again.
+    window.setTimeout(function() { conductHandlerSearch(); },
+                      NIX_SEARCH_PERIOD);
+  }
+
+  return {
+    getCode: function() {
+      return 'nix';
+    },
+
+    isParentVerifiable: function() {
+      return false;
+    },
+
+    init: function(processFn, readyFn) {
+      ready = readyFn;
+
+      // Ensure VBScript wrapper code is in the page and that the
+      // global Javascript handlers have been set.
+      // VBScript methods return a type of 'unknown' when
+      // checked via the typeof operator in IE. Fortunately
+      // for us, this only applies to COM objects, so we
+      // won't see this for a real Javascript object.
+      if (typeof window[NIX_GET_WRAPPER] !== 'unknown') {
+        window[NIX_HANDLE_MESSAGE] = function(data) {
+          window.setTimeout(
+              function() { processFn(gadgets.json.parse(data)); }, 0);
+        };
+
+        window[NIX_CREATE_CHANNEL] = function(name, channel, token) {
+          // Verify the authentication token of the gadget trying
+          // to create a channel for us.
+          if (gadgets.rpc.getAuthToken(name) === token) {
+            nix_channels[name] = channel;
+            ready(name, true);
+          }
+        };
+
+        // Inject the VBScript code needed.
+        var vbscript =
+          // We create a class to act as a wrapper for
+          // a Javascript call, to prevent a break in of
+          // the context.
+          'Class ' + NIX_WRAPPER + '\n '
+
+          // An internal member for keeping track of the
+          // name of the document (container or gadget)
+          // for which this wrapper is intended. For
+          // those wrappers created by gadgets, this is not
+          // used (although it is set to "..")
+          + 'Private m_Intended\n'
+
+          // Stores the auth token used to communicate with
+          // the gadget. The GetChannelCreator method returns
+          // an object that returns this auth token. Upon matching
+          // that with its own, the gadget uses the object
+          // to actually establish the communication channel.
+          + 'Private m_Auth\n'
+
+          // Method for internally setting the value
+          // of the m_Intended property.
+          + 'Public Sub SetIntendedName(name)\n '
+          + 'If isEmpty(m_Intended) Then\n'
+          + 'm_Intended = name\n'
+          + 'End If\n'
+          + 'End Sub\n'
+
+          // Method for internally setting the value of the m_Auth property.
+          + 'Public Sub SetAuth(auth)\n '
+          + 'If isEmpty(m_Auth) Then\n'
+          + 'm_Auth = auth\n'
+          + 'End If\n'
+          + 'End Sub\n'
+
+          // A wrapper method which actually causes a
+          // message to be sent to the other context.
+          + 'Public Sub SendMessage(data)\n '
+          + NIX_HANDLE_MESSAGE + '(data)\n'
+          + 'End Sub\n'
+
+          // Returns the auth token to the gadget, so it can
+          // confirm a match before initiating the connection
+          + 'Public Function GetAuthToken()\n '
+          + 'GetAuthToken = m_Auth\n'
+          + 'End Function\n'
+
+          // Method for setting up the container->gadget
+          // channel. Not strictly needed in the gadget's
+          // wrapper, but no reason to get rid of it. Note here
+          // that we pass the intended name to the NIX_CREATE_CHANNEL
+          // method so that it can save the channel in the proper place
+          // *and* verify the channel via the authentication token passed
+          // here.
+          + 'Public Sub CreateChannel(channel, auth)\n '
+          + 'Call ' + NIX_CREATE_CHANNEL + '(m_Intended, channel, auth)\n'
+          + 'End Sub\n'
+          + 'End Class\n'
+
+          // Function to get a reference to the wrapper.
+          + 'Function ' + NIX_GET_WRAPPER + '(name, auth)\n'
+          + 'Dim wrap\n'
+          + 'Set wrap = New ' + NIX_WRAPPER + '\n'
+          + 'wrap.SetIntendedName name\n'
+          + 'wrap.SetAuth auth\n'
+          + 'Set ' + NIX_GET_WRAPPER + ' = wrap\n'
+          + 'End Function';
+
+        try {
+          window.execScript(vbscript, 'vbscript');
+        } catch (e) {
+          return false;
+        }
+      }
+      return true;
+    },
+
+    setup: function(receiverId, token) {
+      if (receiverId === '..') {
+        conductHandlerSearch();
+        return true;
+      }
+      try {
+        var frame = document.getElementById(receiverId);
+        var wrapper = window[NIX_GET_WRAPPER](receiverId, token);
+        frame.contentWindow.opener = wrapper;
+      } catch (e) {
+        return false;
+      }
+      return true;
+    },
+
+    call: function(targetId, from, rpc) {
+      try {
+        // If we have a handler, call it.
+        if (nix_channels[targetId]) {
+          nix_channels[targetId].SendMessage(gadgets.json.stringify(rpc));
+        }
+      } catch (e) {
+        return false;
+      }
+      return true;
+    }
+  };
+}();
+
+} // !end of double-inclusion guard
diff --git a/trunk/features/src/main/javascript/features/rpc/noop.transport.js b/trunk/features/src/main/javascript/features/rpc/noop.transport.js
new file mode 100644
index 0000000..2e6dff2
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/noop.transport.js
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.rpctx = gadgets.rpctx || {};
+
+/**
+ * Transport that does nothing.
+ */
+if (!gadgets.rpctx.noop) {  // make lib resilient to double-inclusion
+  gadgets.rpctx.noop = function() {
+    return {
+      getCode: function() {
+        return 'noop';
+      },
+
+      isParentVerifiable: function() {
+        return false;
+      },
+
+      init: function(processFn, readyFn) {
+        return true;
+      },
+
+      setup: function(receiverId, token) {
+        return true;
+      },
+
+      call: function(targetId, from, rpc) {
+        return false;
+      }
+    };
+  }();
+
+} // !end of double-inclusion guard
diff --git a/trunk/features/src/main/javascript/features/rpc/rmr.transport.js b/trunk/features/src/main/javascript/features/rpc/rmr.transport.js
new file mode 100644
index 0000000..1a6cf0a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/rmr.transport.js
@@ -0,0 +1,556 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.rpctx = gadgets.rpctx || {};
+
+/*
+ * For older WebKit-based browsers, the security model does not allow for any
+ * known "native" hacks for conducting cross browser communication. However,
+ * a variation of the IFPC (see below) can be used, entitled "RMR". RMR is
+ * a technique that uses the resize event of the iframe to indicate that a
+ * message was sent (instead of the much slower/performance heavy polling
+ * technique used when a defined relay page is not avaliable). Simply put,
+ * RMR uses the same "pass the message by the URL hash" trick that IFPC
+ * uses to send a message, but instead of having an active relay page that
+ * runs a piece of code when it is loaded, RMR merely changes the URL
+ * of the relay page (which does not even have to exist on the domain)
+ * and then notifies the other party by resizing the relay iframe. RMR
+ * exploits the fact that iframes in the dom of page A can be resized
+ * by page A while the onresize event will be fired in the DOM of page B,
+ * thus providing a single bit channel indicating "message sent to you".
+ * This method has the added benefit that the relay need not be active,
+ * nor even exist: a 404 suffices just as well. Note that the technique
+ * doesn't actually strictly require WebKit; it just so happens that these
+ * browsers have no known alternatives (but are very ill-used right now).
+ * The technique's implementation accounts for timing issues through
+ * a packet-ack'ing protocol, so should work on just about any browser.
+ * This may be of value in scenarios where neither wpm nor Flash are
+ * available for some reason.
+ *
+ *   rmr: Resizing trick, works particularly well on WebKit.
+ *      - Safari 2+
+ *      - Chrome 1
+ */
+if (!gadgets.rpctx.rmr) {  // make lib resilient to double-inclusion
+
+  gadgets.rpctx.rmr = function() {
+    // Consts for RMR, including time in ms RMR uses to poll for
+    // its relay frame to be created, and the max # of polls it does.
+    var RMR_SEARCH_TIMEOUT = 500;
+    var RMR_MAX_POLLS = 10;
+
+    // JavaScript references to the channel objects used by RMR.
+    // Gadgets will have but a single channel under
+    // rmr_channels['..'] while containers will have a channel
+    // per gadget stored under the gadget's ID.
+    var rmr_channels = {};
+
+    var parentParam = gadgets.util.getUrlParameters()['parent'];
+
+    var process;
+    var ready;
+
+    /**
+   * Append an RMR relay frame to the document. This allows the receiver
+   * to start receiving messages.
+   *
+   * @param {Node} channelFrame Relay frame to add to the DOM body.
+   * @param {string} relayUri Base URI for the frame.
+   * @param {string} data to pass along to the frame.
+   * @param {string=} opt_frameId ID of frame for which relay is being appended (optional).
+   */
+    function appendRmrFrame(channelFrame, relayUri, data, opt_frameId) {
+      var appendFn = function() {
+        // Append the iframe.
+        document.body.appendChild(channelFrame);
+
+        // Set the src of the iframe to 'about:blank' first and then set it
+        // to the relay URI. This prevents the iframe from maintaining a src
+        // to the 'old' relay URI if the page is returned to from another.
+        // In other words, this fixes the bfcache issue that causes the iframe's
+        // src property to not be updated despite us assigning it a new value here.
+        channelFrame.src = 'about:blank';
+        if (opt_frameId) {
+          // Process the initial sent payload (typically sent by container to
+          // child/gadget) only when the relay frame has finished loading. We
+          // do this to ensure that, in processRmrData(...), the ACK sent due
+          // to processing can actually be sent. Before this time, the frame's
+          // contentWindow is null, making it impossible to do so.
+          channelFrame.onload = function() {
+            processRmrData(opt_frameId);
+          };
+        }
+        channelFrame.src = relayUri + '#' + data;
+      };
+
+      if (document.body) {
+        appendFn();
+      } else {
+        // Common gadget case: attaching header during in-gadget handshake,
+        // when we may still be in script in head. Attach onload.
+        gadgets.util.registerOnLoadHandler(function() { appendFn(); });
+      }
+    }
+
+    /**
+   * Sets up the RMR transport frame for the given frameId. For gadgets
+   * calling containers, the frameId should be '..'.
+   *
+   * @param {string} frameId The ID of the frame.
+   */
+    function setupRmr(frameId) {
+      if (typeof rmr_channels[frameId] === 'object') {
+        // Sanity check. Already done.
+        return;
+      }
+
+      var channelFrame = document.createElement('iframe');
+      var frameStyle = channelFrame.style;
+      frameStyle.position = 'absolute';
+      frameStyle.top = '0px';
+      frameStyle.border = '0';
+      frameStyle.opacity = '0';
+
+      // The width here is important as RMR
+      // makes use of the resize handler for the frame.
+      // Do not modify unless you test thoroughly!
+      frameStyle.width = '10px';
+      frameStyle.height = '1px';
+      channelFrame.id = 'rmrtransport-' + frameId;
+      channelFrame.name = channelFrame.id;
+
+      // Use the explicitly set relay, if one exists. Otherwise,
+      // Construct one using the parent parameter plus robots.txt
+      // as a synthetic relay. This works since browsers using RMR
+      // treat 404s as legitimate for the purposes of cross domain
+      // communication.
+      var relayUri = gadgets.rpc.getRelayUrl(frameId);
+      var relayOrigin = gadgets.rpc.getOrigin(parentParam);
+      if (!relayUri) {
+        relayUri = relayOrigin + '/robots.txt';
+      }
+
+      rmr_channels[frameId] = {
+        frame: channelFrame,
+        receiveWindow: null,
+        relayUri: relayUri,
+        relayOrigin: relayOrigin,
+        searchCounter: 0,
+        width: 10,
+
+        // Waiting means "waiting for acknowledgement to be received."
+        // Acknowledgement always comes as a special ACK
+        // message having been received. This message is received
+        // during handshake in different ways by the container and
+        // gadget, and by normal RMR message passing once the handshake
+        // is complete.
+        waiting: true,
+        queue: [],
+
+        // Number of non-ACK messages that have been sent to the recipient
+        // and have been acknowledged.
+        sendId: 0,
+
+        // Number of messages received and processed from the sender.
+        // This is the number that accompanies every ACK to tell the
+        // sender to clear its queue.
+        recvId: 0,
+
+        // Token sent to target to verify domain.
+        // TODO: switch to shindig.random()
+        verifySendToken: String(Math.random()),
+
+        // Token received from target during handshake. Stored in
+        // order to send back to the caller for verification.
+        verifyRecvToken: null,
+        originVerified: false
+      };
+
+      if (frameId !== '..') {
+        // Container always appends a relay to the gadget, before
+        // the gadget appends its own relay back to container. The
+        // gadget, in the meantime, refuses to attach the container
+        // relay until it finds this one. Thus, the container knows
+        // for certain that gadget to container communication is set
+        // up by the time it finds its own relay. In addition to
+        // establishing a reliable handshake protocol, this also
+        // makes it possible for the gadget to send an initial batch
+        // of messages to the container ASAP.
+        appendRmrFrame(channelFrame, relayUri, getRmrData(frameId));
+      }
+
+      // Start searching for our own frame on the other page.
+      conductRmrSearch(frameId);
+    }
+
+    /**
+   * Searches for a relay frame, created by the sender referenced by
+   * frameId, with which this context receives messages. Once
+   * found with proper permissions, attaches a resize handler which
+   * signals messages to be sent.
+   *
+   * @param {string} frameId Frame ID of the prospective sender.
+   */
+    function conductRmrSearch(frameId) {
+      var channelWindow = null;
+
+      // Increment the search counter.
+      rmr_channels[frameId].searchCounter++;
+
+      try {
+        var targetWin = gadgets.rpc._getTargetWin(frameId);
+        if (frameId === '..') {
+          // We are a gadget.
+          channelWindow = targetWin.frames['rmrtransport-' + gadgets.rpc.RPC_ID];
+        } else {
+          // We are a container.
+          channelWindow = targetWin.frames['rmrtransport-..'];
+        }
+      } catch (e) {
+        // Just in case; may happen when relay is set to about:blank or unset.
+        // Catching exceptions here ensures that the timeout to continue the
+        // search below continues to work.
+      }
+
+      var status = false;
+
+      if (channelWindow) {
+        // We have a valid reference to "our" RMR transport frame.
+        // Register the proper event handlers.
+        status = registerRmrChannel(frameId, channelWindow);
+      }
+
+      if (!status) {
+        // Not found yet. Continue searching, but only if the counter
+        // has not reached the threshold.
+        if (rmr_channels[frameId].searchCounter > RMR_MAX_POLLS) {
+          // If we reach this point, then RMR has failed and we
+          // fall back to IFPC.
+          return;
+        }
+
+        window.setTimeout(function() {
+          conductRmrSearch(frameId);
+        }, RMR_SEARCH_TIMEOUT);
+      }
+    }
+
+    /**
+   * Attempts to conduct an RPC call to the specified
+   * target with the specified data via the RMR
+   * method. If this method fails, the system attempts again
+   * using the known default of IFPC.
+   *
+   * @param {string} targetId Module Id of the RPC service provider.
+   * @param {string} serviceName Name of the service to call.
+   * @param {string} from Module Id of the calling provider.
+   * @param {Object} rpc The RPC data for this call.
+   */
+    function callRmr(targetId, serviceName, from, rpc) {
+      var handler = null;
+
+      if (from !== '..') {
+        // Call from gadget to the container.
+        handler = rmr_channels['..'];
+      } else {
+        // Call from container to the gadget.
+        handler = rmr_channels[targetId];
+      }
+
+      if (handler) {
+        // Queue the current message if not ACK.
+        // ACK is always sent through getRmrData(...).
+        if (serviceName !== gadgets.rpc.ACK) {
+          handler.queue.push(rpc);
+        }
+
+        if (handler.waiting ||
+            (handler.queue.length === 0 &&
+            !(serviceName === gadgets.rpc.ACK && rpc && rpc['ackAlone'] === true))) {
+          // If we are awaiting a response from any previously-sent messages,
+          // or if we don't have anything new to send, just return.
+          // Note that we don't short-return if we're ACKing just-received
+          // messages.
+          return true;
+        }
+
+        if (handler.queue.length > 0) {
+          handler.waiting = true;
+        }
+
+        var url = handler.relayUri + '#' + getRmrData(targetId);
+
+        try {
+          // Update the URL with the message.
+          handler.frame.contentWindow.location = url;
+
+          // Resize the frame.
+          var newWidth = handler.width == 10 ? 20 : 10;
+          handler.frame.style.width = newWidth + 'px';
+          handler.width = newWidth;
+
+          // Done!
+        } catch (e) {
+          // Something about location-setting or resizing failed.
+          // This should never happen, but if it does, fall back to
+          // the default transport.
+          return false;
+        }
+      }
+
+      return true;
+    }
+
+    /**
+   * Returns as a string the data to be appended to an RMR relay frame,
+   * constructed from the current request queue plus an ACK message indicating
+   * the currently latest-processed message ID.
+   *
+   * @param {string} toFrameId Frame whose sendable queued data to retrieve.
+   */
+    function getRmrData(toFrameId) {
+      var channel = rmr_channels[toFrameId];
+      var rmrData = {id: channel.sendId};
+      if (channel) {
+        rmrData['d'] = Array.prototype.slice.call(channel.queue, 0);
+        var ackPacket = { 's': gadgets.rpc.ACK, 'id': channel.recvId };
+        if (!channel.originVerified) {
+          ackPacket['sendToken'] = channel.verifySendToken;
+        }
+        if (channel.verifyRecvToken) {
+          ackPacket['recvToken'] = channel.verifyRecvToken;
+        }
+        rmrData['d'].push(ackPacket);
+      }
+      return gadgets.json.stringify(rmrData);
+    }
+
+    /**
+   * Retrieve data from the channel keyed by the given frameId,
+   * processing it as a batch. All processed data is assumed to have been
+   * generated by getRmrData(...), pairing that method with this.
+   *
+   * @param {string} fromFrameId Frame from which data is being retrieved.
+   */
+    function processRmrData(fromFrameId) {
+      var channel = rmr_channels[fromFrameId];
+      var data = channel.receiveWindow.location.hash.substring(1);
+
+      // Decode the RPC object array.
+      var rpcObj = gadgets.json.parse(decodeURIComponent(data)) || {};
+      var rpcArray = rpcObj['d'] || [];
+
+      var nonAckReceived = false;
+      var noLongerWaiting = false;
+
+      var numBypassed = 0;
+      var numToBypass = (channel.recvId - rpcObj['id']);
+      for (var i = 0; i < rpcArray.length; ++i) {
+        var rpc = rpcArray[i];
+
+        // If we receive an ACK message, then mark the current
+        // handler as no longer waiting and send out the next
+        // queued message.
+        if (rpc['s'] === gadgets.rpc.ACK) {
+          // ACK received - whether this came from a handshake or
+          // an active call, in either case it indicates readiness to
+          // send messages to the from frame.
+          ready(fromFrameId, true);
+
+          // Store sendToken if challenge was passed.
+          // This will cause the token to be sent back to the sender
+          // to prove origin verification.
+          channel.verifyRecvToken = rpc['sendToken'];
+
+          // If a recvToken came back, check to see if it matches the
+          // sendToken originally sent as a challenge. If so, mark
+          // origin as having been verified.
+          if (!channel.originVerified && rpc['recvToken'] &&
+              String(rpc['recvToken']) == String(channel.verifySendToken)) {
+            channel.originVerified = true;
+          }
+
+          if (channel.waiting) {
+            noLongerWaiting = true;
+          }
+
+          channel.waiting = false;
+          var newlyAcked = Math.max(0, rpc['id'] - channel.sendId);
+          channel.queue.splice(0, newlyAcked);
+          channel.sendId = Math.max(channel.sendId, rpc['id'] || 0);
+          continue;
+        }
+
+        // If we get here, we've received > 0 non-ACK messages to
+        // process. Indicate this bit for later.
+        nonAckReceived = true;
+
+        // Bypass any messages already received.
+        if (++numBypassed <= numToBypass) {
+          continue;
+        }
+
+        ++channel.recvId;
+
+        // Send along the origin if it's been verified during handshake.
+        // In either case, dispatch the message.
+        process(rpc, channel.originVerified ? channel.relayOrigin : undefined);
+      }
+
+      // Send an ACK indicating that we got/processed the message(s).
+      // Do so if we've received a message to process or if we were waiting
+      // before but a received ACK has cleared our waiting bit, and we have
+      // more messages to send. Performing this operation causes additional
+      // messages to be sent.
+      if (nonAckReceived ||
+          (noLongerWaiting && channel.queue.length > 0)) {
+        var from = (fromFrameId === '..') ? gadgets.rpc.RPC_ID : '..';
+        callRmr(fromFrameId, gadgets.rpc.ACK, from, {'ackAlone': nonAckReceived});
+      }
+    }
+
+    /**
+   * Registers the RMR channel handler for the given frameId and associated
+   * channel window.
+   *
+   * @param {string} frameId The ID of the frame for which this channel is being
+   *   registered.
+   * @param {Object} channelWindow The window of the receive frame for this
+   *   channel, if any.
+   *
+   * @return {boolean} True if the frame was setup successfully, false
+   *   otherwise.
+   */
+    function registerRmrChannel(frameId, channelWindow) {
+      var channel = rmr_channels[frameId];
+
+      // Verify that the channel is ready for receiving.
+      try {
+        var canAccess = false;
+
+        // Check to see if the document is in the window. For Chrome, this
+        // will return 'false' if the channelWindow is inaccessible by this
+        // piece of JavaScript code, meaning that the URL of the channelWindow's
+        // parent iframe has not yet changed from 'about:blank'. We do this
+        // check this way because any true *access* on the channelWindow object
+        // will raise a security exception, which, despite the try-catch, still
+        // gets reported to the debugger (it does not break execution, the try
+        // handles that problem, but it is still reported, which is bad form).
+        // This check always succeeds in Safari 3.1 regardless of the state of
+        // the window.
+        canAccess = 'document' in channelWindow;
+
+        if (!canAccess) {
+          return false;
+        }
+
+        // Check to see if the document is an object. For Safari 3.1, this will
+        // return undefined if the page is still inaccessible. Unfortunately, this
+        // *will* raise a security issue in the debugger.
+        // TODO Find a way around this problem.
+        canAccess = typeof channelWindow['document'] == 'object';
+
+        if (!canAccess) {
+          return false;
+        }
+
+        // Once we get here, we know we can access the document (and anything else)
+        // on the window object. Therefore, we check to see if the location is
+        // still about:blank (this takes care of the Safari 3.2 case).
+        var loc = channelWindow.location.href;
+
+        // Check if this is about:blank for Safari.
+        if (loc === 'about:blank') {
+          return false;
+        }
+      } catch (ex) {
+        // For some reason, the iframe still points to about:blank. We try
+        // again in a bit.
+        return false;
+      }
+
+      // Save a reference to the receive window.
+      channel.receiveWindow = channelWindow;
+
+      // Register the onresize handler.
+      function onresize() {
+        processRmrData(frameId);
+      };
+
+      if (typeof channelWindow.attachEvent === 'undefined') {
+        channelWindow.onresize = onresize;
+      } else {
+        channelWindow.attachEvent('onresize', onresize);
+      }
+
+      if (frameId === '..') {
+        // Gadget to container. Signal to the container that the gadget
+        // is ready to receive messages by attaching the g -> c relay.
+        // As a nice optimization, pass along any gadget to container
+        // queued messages that have backed up since then. ACK is enqueued in
+        // getRmrData to ensure that the container's waiting flag is set to false
+        // (this happens in the below code run on the container side).
+        appendRmrFrame(channel.frame, channel.relayUri, getRmrData(frameId), frameId);
+      } else {
+        // Process messages that the gadget sent in its initial relay payload.
+        // We can do this immediately because the container has already appended
+        // and loaded a relay frame that can be used to ACK the messages the gadget
+        // sent. In the preceding if-block, however, the processRmrData(...) call
+        // must wait. That's because appendRmrFrame may not actually append the
+        // frame - in the context of a gadget, this code may be running in the
+        // head element, so it cannot be appended to body. As a result, the
+        // gadget cannot ACK the container for messages it received.
+        processRmrData(frameId);
+      }
+
+      return true;
+    }
+
+    return {
+      getCode: function() {
+        return 'rmr';
+      },
+
+      isParentVerifiable: function() {
+        return true;
+      },
+
+      init: function(processFn, readyFn) {
+        // No global setup.
+        process = processFn;
+        ready = readyFn;
+        return true;
+      },
+
+      setup: function(receiverId, token) {
+        try {
+          setupRmr(receiverId);
+        } catch (e) {
+          gadgets.warn('Caught exception setting up RMR: ' + e);
+          return false;
+        }
+        return true;
+      },
+
+      call: function(targetId, from, rpc) {
+        return callRmr(targetId, rpc['s'], from, rpc);
+      }
+    };
+  }();
+
+} // !end of double-inclusion guard
diff --git a/trunk/features/src/main/javascript/features/rpc/rpc.js b/trunk/features/src/main/javascript/features/rpc/rpc.js
new file mode 100644
index 0000000..1be5cfa
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/rpc.js
@@ -0,0 +1,1092 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Remote procedure call library for gadget-to-container,
+ * container-to-gadget, and gadget-to-gadget (thru container) communication.
+ */
+
+/**
+ * gadgets.rpc Transports
+ *
+ * All transports are stored in object gadgets.rpctx, and are provided
+ * to the core gadgets.rpc library by various build rules.
+ *
+ * Transports used by core gadgets.rpc code to actually pass messages.
+ * Each transport implements the same interface exposing hooks that
+ * the core library calls at strategic points to set up and use
+ * the transport.
+ *
+ * The methods each transport must implement are:
+ * + getCode(): returns a string identifying the transport. For debugging.
+ * + isParentVerifiable(): indicates (via boolean) whether the method
+ *     has the property that its relay URL verifies for certain the
+ *     receiver's protocol://host:port.
+ * + init(processFn, readyFn): Performs any global initialization needed. Called
+ *     before any other gadgets.rpc methods are invoked. processFn is
+ *     the function in gadgets.rpc used to process an rpc packet. readyFn is
+ *     a function that must be called when the transport is ready to send
+ *     and receive messages bidirectionally. Returns
+ *     true if successful, false otherwise.
+ * + setup(receiverId, token): Performs per-receiver initialization, if any.
+ *     receiverId will be '..' for gadget-to-container. Returns true if
+ *     successful, false otherwise.
+ * + call(targetId, from, rpc): Invoked to send an actual
+ *     message to the given targetId, with the given serviceName, from
+ *     the sender identified by 'from'. Payload is an rpc packet. Returns
+ *     true if successful, false otherwise.
+ */
+
+if (!window['gadgets']['rpc']) { // make lib resilient to double-inclusion
+
+  /**
+   * @static
+   * @namespace Provides operations for making rpc calls.
+   * @name gadgets.rpc
+   */
+  gadgets.rpc = function() {
+    /**
+     * @const
+     * @private
+     */
+    var CALLBACK_NAME = '__cb';
+
+    /**
+     * @const
+     * @private
+     */
+    var DEFAULT_NAME = '';
+
+    /** Exported constant, for use by transports only.
+     * @const
+     * @type {string}
+     * @member gadgets.rpc
+     */
+    var ACK = '__ack';
+
+    /**
+     * Timeout and number of attempts made to setup a transport receiver.
+     * @const
+     * @private
+     */
+    var SETUP_FRAME_TIMEOUT = 500;
+
+    /**
+     * @const
+     * @private
+     */
+    var SETUP_FRAME_MAX_TRIES = 10;
+
+    /**
+     * @const
+     * @private
+     */
+    var ID_ORIGIN_DELIMITER = '|';
+
+    /**
+     * @const
+     * @private
+     */
+    var RPC_KEY_CALLBACK = 'callback';
+
+    /**
+     * @const
+     * @private
+     */
+    var RPC_KEY_ORIGIN = 'origin';
+    var RPC_KEY_REFERRER = 'referer';
+
+    var services = {};
+    var relayUrl = {};
+    var useLegacyProtocol = {};
+    var authToken = {};
+    var callId = 0;
+    var callbacks = {};
+    var setup = {};
+    var sameDomain = {};
+    var params = {};
+    var receiverTx = {};
+    var earlyRpcQueue = {};
+    var passReferrerDirection = null;
+    var passReferrerContents = null;
+
+    // isGadget =~ isChild for the purposes of rpc (used only in setup).
+    var isChild = !!gadgets.util.getUrlParameters().parent &&
+      (window.top !== window.self);
+
+    // Set the current rpc ID from window.name immediately, to prevent
+    // shadowing of window.name by a "var name" declaration, or similar.
+    var rpcId = window.name;
+
+    var securityCallback = function() {};
+    var arbitrator = null;
+    var LOAD_TIMEOUT = 0;
+    var FRAME_PHISH = 1;
+    var FORGED_MSG = 2;
+
+    // Fallback transport is simply a dummy impl that emits no errors
+    // and logs info on calls it receives, to avoid undesired side-effects
+    // from falling back to IFPC or some other transport.
+    var console = window['console'];
+    var clog = console && console.log ? function(msg) { console.log(msg); } : function(){};
+    var fallbackTransport = (function() {
+      function logFn(name) {
+        return function() {
+          clog(name + ': call ignored');
+        };
+      }
+      return {
+        'getCode': function() { return 'noop'; },
+        // Not really, but prevents transport assignment to IFPC.
+        'isParentVerifiable': function() { return true; },
+        'init': logFn('init'),
+        'setup': logFn('setup'),
+        'call': logFn('call')
+      };
+    })();
+
+    // Load the authentication token for speaking to the container
+    // from the gadget's parameters, or default to '0' if not found.
+    if (gadgets.util) {
+      params = gadgets.util.getUrlParameters();
+    }
+
+    /**
+     * Return a transport representing the best available cross-domain
+     * message-passing mechanism available to the browser.
+     *
+     * <p>Transports are selected on a cascading basis determined by browser
+     * capability and other checks. The order of preference is:
+     * <ol>
+     * <li> wpm: Uses window.postMessage standard.
+     * <li> dpm: Uses document.postMessage, similar to wpm but pre-standard.
+     * <li> nix: Uses IE-specific browser hacks.
+     * <li> rmr: Signals message passing using relay file's onresize handler.
+     * <li> fe: Uses FF2-specific window.frameElement hack.
+     * <li> ifpc: Sends messages via active load of a relay file.
+     * </ol>
+     * <p>See each transport's commentary/documentation for details.
+     * @return {Object}
+     * @member gadgets.rpc
+     */
+    function getTransport() {
+      if (params['rpctx'] == 'flash') return gadgets.rpctx.flash;
+      if (params['rpctx'] == 'rmr') return gadgets.rpctx.rmr;
+      return typeof window.postMessage === 'function' ? gadgets.rpctx.wpm :
+          typeof window.postMessage === 'object' ? gadgets.rpctx.wpm :
+          window.ActiveXObject ? (gadgets.rpctx.flash ? gadgets.rpctx.flash : gadgets.rpctx.nix) :
+          navigator.userAgent.indexOf('WebKit') > 0 ? gadgets.rpctx.rmr :
+          navigator.product === 'Gecko' ? gadgets.rpctx.frameElement :
+          gadgets.rpctx.ifpc;
+    }
+
+    /**
+     * Function passed to, and called by, a transport indicating it's ready to
+     * send and receive messages.
+     */
+    function transportReady(receiverId, readySuccess) {
+      if (receiverTx[receiverId]) return;
+      var tx = transport;
+      if (!readySuccess) {
+        tx = fallbackTransport;
+      }
+      receiverTx[receiverId] = tx;
+
+      // If there are any early-queued messages, send them now directly through
+      // the needed transport.
+      var earlyQueue = earlyRpcQueue[receiverId] || [];
+      for (var i = 0; i < earlyQueue.length; ++i) {
+        var rpc = earlyQueue[i];
+        // There was no auth/rpc token set before, so set it now.
+        rpc['t'] = getAuthToken(receiverId);
+        tx.call(receiverId, rpc['f'], rpc);
+      }
+
+      // Clear the queue so it won't be sent again.
+      earlyRpcQueue[receiverId] = [];
+    }
+
+    //  Track when this main page is closed or navigated to a different location
+    // ("unload" event).
+    //  NOTE: The use of the "unload" handler here and for the relay iframe
+    // prevents the use of the in-memory page cache in modern browsers.
+    // See: https://developer.mozilla.org/en/using_firefox_1.5_caching
+    // See: http://webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/
+    var mainPageUnloading = false,
+        hookedUnload = false;
+
+    function hookMainPageUnload() {
+      if (hookedUnload) {
+        return;
+      }
+      function onunload() {
+        mainPageUnloading = true;
+      }
+
+      // TODO: use common helper
+      if (typeof window.addEventListener != 'undefined') {
+        window.addEventListener('unload', onunload, false);
+      } else if (typeof window.attachEvent != 'undefined') {
+        window.attachEvent('onunload', onunload);
+      }
+
+      hookedUnload = true;
+    }
+
+    function relayOnload(targetId, sourceId, token, data, relayWindow) {
+      // Validate auth token.
+      if (!authToken[sourceId] || authToken[sourceId] !== token) {
+        gadgets.error('Invalid auth token. ' + authToken[sourceId] + ' vs ' + token);
+        securityCallback(sourceId, FORGED_MSG);
+      }
+
+      relayWindow.onunload = function() {
+        if (setup[sourceId] && !mainPageUnloading) {
+          securityCallback(sourceId, FRAME_PHISH);
+          gadgets.rpc.removeReceiver(sourceId);
+        }
+      };
+      hookMainPageUnload();
+
+      data = gadgets.json.parse(decodeURIComponent(data));
+    }
+
+    /**
+     * Helper function that performs actual processing of an RPC request.
+     * Origin is passed in separately to ensure that it cannot be spoofed,
+     * and guard code in the method ensures the same before dispatching
+     * any service handler.
+     * @param {Object} rpc RPC request object.
+     * @param {String} opt_sender RPC sender, if available and with a verified origin piece.
+     * @private
+     */
+    function process(rpc, opt_sender) {
+      //
+      // RPC object contents:
+      //   s: Service Name
+      //   f: From
+      //   c: The callback ID or 0 if none.
+      //   a: The arguments for this RPC call.
+      //   t: The authentication token.
+      //
+      if (rpc && typeof rpc['s'] === 'string' && typeof rpc['f'] === 'string' &&
+          rpc['a'] instanceof Array) {
+
+        if (typeof arbitrate === 'function' && !arbitrate(rpc['s'], rpc['f'])) {
+          return;
+        }
+
+        // Validate auth token.
+        if (authToken[rpc['f']]) {
+          // We don't do type coercion here because all entries in the authToken
+          // object are strings, as are all url params. See setupReceiver(...).
+          if (authToken[rpc['f']] !== rpc['t']) {
+            gadgets.error('Invalid auth token. ' + authToken[rpc['f']] + ' vs ' + rpc['t']);
+            securityCallback(rpc['f'], FORGED_MSG);
+          }
+        }
+
+        if (rpc['s'] === ACK) {
+          // Acknowledgement API, used to indicate a receiver is ready.
+          window.setTimeout(function() { transportReady(rpc['f'], true); }, 0);
+          return;
+        }
+
+        // If there is a callback for this service, attach a callback function
+        // to the rpc context object for asynchronous rpc services.
+        //
+        // Synchronous rpc request handlers should simply ignore it and return a
+        // value as usual.
+        // Asynchronous rpc request handlers, on the other hand, should pass its
+        // result to this callback function and not return a value on exit.
+        //
+        // For example, the following rpc handler passes the first parameter back
+        // to its rpc client with a one-second delay.
+        //
+        // function asyncRpcHandler(param) {
+        //   var me = this;
+        //   setTimeout(function() {
+        //     me.callback(param);
+        //   }, 1000);
+        // }
+        if (rpc['c']) {
+          rpc[RPC_KEY_CALLBACK] = function(result) {
+            gadgets.rpc.call(rpc['f'], CALLBACK_NAME, null, rpc['c'], result);
+          };
+        }
+
+        // Set the requestor origin.
+        // If not passed by the transport, then this simply sets to undefined.
+        if (opt_sender) {
+          var origin = getOrigin(opt_sender);
+          rpc[RPC_KEY_ORIGIN] = opt_sender;
+          var referrer = rpc['r'];
+          if (!referrer || getOrigin(referrer) != origin) {
+            // Transports send along as much info as they can about the sender
+            // of the message; 'origin' is the origin component alone, while
+            // 'referrer' is a best-effort field set from available information.
+            // The second clause simply verifies that referrer is valid.
+            referrer = opt_sender;
+          }
+          rpc[RPC_KEY_REFERRER] = referrer;
+        }
+
+        // Call the requested RPC service.
+        var result = (services[rpc['s']] ||
+            services[DEFAULT_NAME]).apply(rpc, rpc['a']);
+
+        // If the rpc request handler returns a value, immediately pass it back
+        // to the callback. Otherwise, do nothing, assuming that the rpc handler
+        // will make an asynchronous call later.
+        if (rpc['c'] && typeof result !== 'undefined') {
+          gadgets.rpc.call(rpc['f'], CALLBACK_NAME, null, rpc['c'], result);
+        }
+      }
+    }
+
+    /**
+     * Helper method returning a canonicalized protocol://host[:port] for
+     * a given input URL, provided as a string. Used to compute convenient
+     * relay URLs and to determine whether a call is coming from the same
+     * domain as its receiver (bypassing the try/catch capability detection
+     * flow, thereby obviating Firebug and other tools reporting an exception).
+     *
+     * @param {string} url Base URL to canonicalize.
+     * @memberOf gadgets.rpc
+     */
+    function getOrigin(url) {
+      if (!url) {
+        return '';
+      }
+      url = url.toLowerCase();
+      if (url.indexOf('//') == 0) {
+        url = window.location.protocol + url;
+      }
+      if (url.indexOf('://') == -1) {
+        // Assumed to be schemaless. Default to current protocol.
+        url = window.location.protocol + '//' + url;
+      }
+      // At this point we guarantee that "://" is in the URL and defines
+      // current protocol. Skip past this to search for host:port.
+      var host = url.substring(url.indexOf('://') + 3);
+
+      // Find the first slash char, delimiting the host:port.
+      var slashPos = host.indexOf('/');
+      if (slashPos != -1) {
+        host = host.substring(0, slashPos);
+      }
+
+      var protocol = url.substring(0, url.indexOf('://'));
+
+      // Use port only if it's not default for the protocol.
+      var portStr = '';
+      var portPos = host.indexOf(':');
+      if (portPos != -1) {
+        var port = host.substring(portPos + 1);
+        host = host.substring(0, portPos);
+        if ((protocol === 'http' && port !== '80') ||
+            (protocol === 'https' && port !== '443')) {
+          portStr = ':' + port;
+        }
+      }
+
+      // Return <protocol>://<host>[<port>]
+      return protocol + '://' + host + portStr;
+    }
+
+    /*
+     * Makes a sibling id in the format of "/<siblingFrameId>|<siblingOrigin>".
+     */
+    function makeSiblingId(id, opt_origin) {
+      return '/' + id + (opt_origin ? ID_ORIGIN_DELIMITER + opt_origin : '');
+    }
+
+    /*
+     * Parses an iframe id.  Returns null if not a sibling id or
+     *   {id: <siblingId>, origin: <siblingOrigin>} otherwise.
+     */
+    function parseSiblingId(id) {
+      if (id.charAt(0) == '/') {
+        var delimiter = id.indexOf(ID_ORIGIN_DELIMITER);
+        var siblingId = delimiter > 0 ? id.substring(1, delimiter) : id.substring(1);
+        var origin = delimiter > 0 ? id.substring(delimiter + 1) : null;
+        return {id: siblingId, origin: origin};
+      } else {
+        return null;
+      }
+    }
+
+    function getTargetWin(id) {
+      if (typeof id === 'undefined' ||
+          id === '..') {
+        return window.parent;
+      }
+
+      var siblingId = parseSiblingId(id);
+      if (siblingId) {
+        var currentWindow = window;
+        while (!currentWindow.frames[siblingId.id]) {
+          if (currentWindow === window.top) {
+            break;
+          }
+          currentWindow = currentWindow.parent;
+        }
+        return currentWindow.frames[siblingId.id];
+      }
+
+      // Cast to a String to avoid an index lookup.
+      id = String(id);
+      
+      // Try getElementById() first
+      var target = document.getElementById(id);
+      if (target && target.contentWindow) {
+        return target.contentWindow;
+      }
+
+      // Fallback to window.frames
+      target = window.frames[id];
+      if (target && !target.closed) {
+        return target;
+      }
+
+      return null;
+    }
+
+    function getTargetOrigin(id) {
+      var targetRelay = null;
+      var relayUrl = getRelayUrl(id);
+      if (relayUrl) {
+        targetRelay = relayUrl;
+      } else {
+        var siblingId = parseSiblingId(id);
+        if (siblingId) {
+          // sibling
+          targetRelay = siblingId.origin;
+        } else if (id == '..') {
+          // parent
+          targetRelay = params['parent'];
+        } else {
+          // child
+          targetRelay = document.getElementById(id).src;
+        }
+      }
+
+      return getOrigin(targetRelay);
+    }
+
+    // Pick the most efficient RPC relay mechanism.
+    var transport = getTransport();
+
+    // Create the Default RPC handler.
+    services[DEFAULT_NAME] = function() {
+      clog('Unknown RPC service: ' + this.s);
+    };
+
+    // Create a Special RPC handler for callbacks.
+    services[CALLBACK_NAME] = function(callbackId, result) {
+      var callback = callbacks[callbackId];
+      if (callback) {
+        delete callbacks[callbackId];
+        callback.call(this, result);
+      }
+    };
+
+    /**
+     * Conducts any frame-specific work necessary to setup
+     * the channel type chosen. This method is called when
+     * the container page first registers the gadget in the
+     * RPC mechanism. Gadgets, in turn, will complete the setup
+     * of the channel once they send their first messages.
+     */
+    function setupFrame(frameId, token) {
+      if (setup[frameId] === true) {
+        return;
+      }
+
+      if (typeof setup[frameId] === 'undefined') {
+        setup[frameId] = 0;
+      }
+
+      var tgtFrame = getTargetWin(frameId);
+      if (frameId === '..' || tgtFrame != null) {
+        if (transport.setup(frameId, token) === true) {
+          setup[frameId] = true;
+          return;
+        }
+      }
+
+      if (setup[frameId] !== true && setup[frameId]++ < SETUP_FRAME_MAX_TRIES) {
+        // Try again in a bit, assuming that frame will soon exist.
+        window.setTimeout(function() { setupFrame(frameId, token); },
+                        SETUP_FRAME_TIMEOUT);
+      } else {
+        // Fail: fall back for this gadget.
+        receiverTx[frameId] = fallbackTransport;
+        setup[frameId] = true;
+      }
+    }
+
+    var _flagCrossOrigin = {};
+    /**
+     * Attempts to make an rpc by calling the target's receive method directly.
+     * This works when gadgets are rendered on the same domain as their container,
+     * a potentially useful optimization for trusted content which keeps
+     * RPC behind a consistent interface.
+     *
+     * @param {string} target Module id of the rpc service provider.
+     * @param {Object} rpc RPC data.
+     * @return {boolean}
+     */
+    function callSameDomain(target, rpc) {
+      var targetEl = getTargetWin(target);
+      if (!sameDomain[target] || (sameDomain[target] !== _flagCrossOrigin &&
+              targetEl.Function.prototype !== sameDomain[target].constructor.prototype)) {
+
+        var targetRelay = getRelayUrl(target);
+        if (getOrigin(targetRelay) !== getOrigin(window.location.href)) {
+          // Not worth trying -- avoid the error and just return.
+          sameDomain[target] = _flagCrossOrigin; // never try this again
+          return false;
+        }
+
+        try {
+          // If this succeeds, then same-domain policy applied
+          var targetGadgets = targetEl['gadgets'];
+          sameDomain[target] = targetGadgets.rpc.receiveSameDomain;
+        } catch (e) {
+          // Shouldn't happen due to origin check. Caught to emit more
+          // meaningful error to the caller. Consider emitting in non-opt mode.
+          // gadgets.log('Same domain call failed: parent= incorrectly set.');
+          sameDomain[target] = _flagCrossOrigin; // never try this again
+          return false;
+        }
+      }
+
+      // Cross window functions in IE often look like objects in nearly every way
+      // (typeof() will lie to you)
+      if (sameDomain[target] && sameDomain[target] !== _flagCrossOrigin) {
+        // Call target's receive method
+        sameDomain[target](rpc);
+        return true;
+      }
+
+      return false;
+    }
+
+    /**
+     * Gets the relay URL of a target frame.
+     * @param {string} targetId Name of the target frame.
+     * @return {string|undefined} Relay URL of the target frame.
+     *
+     * @member gadgets.rpc
+     */
+    function getRelayUrl(targetId) {
+      var url = relayUrl[targetId];
+      // Some RPC methods (wpm, for one) are unhappy with schemeless URLs.
+      if (url && url.substring(0, 1) === '/') {
+        if (url.substring(1, 2) === '/') {    // starts with '//'
+          url = document.location.protocol + url;
+        } else {    // relative URL, starts with '/'
+          url = document.location.protocol + '//' + document.location.host + url;
+        }
+      }
+      return url;
+    }
+
+    /**
+     * Sets the relay URL of a target frame.
+     * @param {string} targetId Name of the target frame.
+     * @param {string} url Full relay URL of the target frame.
+     *
+     * @member gadgets.rpc
+     * @deprecated
+     */
+    function setRelayUrl(targetId, url, opt_useLegacy) {
+      // Make URL absolute if necessary
+      if (!/http(s)?:\/\/.+/.test(url)) {
+        if (url.indexOf('//') == 0) {
+          url = window.location.protocol + url;
+        } else if (url.charAt(0) == '/') {
+          url = window.location.protocol + '//' + window.location.host + url;
+        } else if (url.indexOf('://') == -1) {
+          // Assumed to be schemaless. Default to current protocol.
+          url = window.location.protocol + '//' + url;
+        }
+      }
+      relayUrl[targetId] = url;
+      if (typeof opt_useLegacy !== 'undefined') {
+        useLegacyProtocol[targetId] = !!opt_useLegacy;
+      }
+    }
+
+    /**
+     * Helper method to retrieve the authToken for a given gadget.
+     * Not to be used directly.
+     * @member gadgets.rpc
+     * @return {string}
+     */
+    function getAuthToken(targetId) {
+      return authToken[targetId];
+    }
+
+    /**
+     * Sets the auth token of a target frame.
+     * @param {string} targetId Name of the target frame.
+     * @param {string} token The authentication token to use for all
+     *     calls to or from this target id.
+     *
+     * @member gadgets.rpc
+     * @deprecated
+     */
+    function setAuthToken(targetId, token) {
+      token = token || '';
+
+      // Coerce token to a String, ensuring that all authToken values
+      // are strings. This ensures correct comparison with URL params
+      // in the process(rpc) method.
+      authToken[targetId] = String(token);
+
+      setupFrame(targetId, token);
+    }
+
+    function setReferrerConfig(cfg) {
+      var passReferrer = cfg['passReferrer'] || '';
+      var prParts = passReferrer.split(':', 2);
+      passReferrerDirection = prParts[0] || 'none';
+      passReferrerContents = prParts[1] || 'origin';
+    }
+
+    function setLegacyProtocolConfig(cfg) {
+      if (isLegacyProtocolConfig(cfg)) {
+        transport = gadgets.rpctx.ifpc;
+        transport.init(process, transportReady);
+      }
+    }
+
+    function isLegacyProtocolConfig(cfg) {
+      return String(cfg['useLegacyProtocol']) === 'true';
+    }
+
+    function setupContainedContext(rpctoken, opt_parent) {
+      function init(config) {
+        var cfg = config ? config['rpc'] : {};
+        setReferrerConfig(cfg);
+
+        // Parent-relative only.
+        var parentRelayUrl = cfg['parentRelayUrl'] || '';
+        parentRelayUrl = getOrigin(params['parent'] || opt_parent) + parentRelayUrl;
+        setRelayUrl('..', parentRelayUrl, isLegacyProtocolConfig(cfg));
+
+        setLegacyProtocolConfig(cfg);
+
+        setAuthToken('..', rpctoken);
+      }
+
+      // Check to see if we know the parent yet.
+      // In almost all cases we will, since the parent param is provided.
+      // However, it's possible that the lib doesn't yet know, but is
+      // initialized in forced fashion later.
+      if (!params['parent'] && opt_parent) {
+        // Handles the forced initialization case.
+        init({});
+        return;
+      }
+
+      // Handles the standard gadgets.config.init() case.
+      gadgets.config.register('rpc', null, init);
+    }
+
+    function setupChildIframe(gadgetId, opt_frameurl, opt_authtoken) {
+      var childIframe = null;
+      if (gadgetId.charAt(0) != '/') {
+        // only set up child (and not sibling) iframe
+        if (!gadgets.util) {
+          return;
+        }
+        childIframe = document.getElementById(gadgetId);
+        if (!childIframe) {
+          throw new Error('Cannot set up gadgets.rpc receiver with ID: ' + gadgetId +
+              ', element not found.');
+        }
+      }
+
+      // The "relay URL" can either be explicitly specified or is set as
+      // the child IFRAME URL's origin
+      var childSrc = childIframe && childIframe.src;
+      var relayUrl = opt_frameurl || gadgets.rpc.getOrigin(childSrc);
+      setRelayUrl(gadgetId, relayUrl);
+
+      // The auth token is parsed from child params (rpctoken) or overridden.
+      var childParams = gadgets.util.getUrlParameters(childSrc);
+      var rpctoken = opt_authtoken || childParams['rpctoken'];
+      setAuthToken(gadgetId, rpctoken);
+    }
+
+    /**
+     * Sets up the gadgets.rpc library to communicate with the receiver.
+     * <p>This method replaces setRelayUrl(...) and setAuthToken(...)
+     *
+     * <p>Simplified instructions - highly recommended:
+     * <ol>
+     * <li> Generate &lt;iframe id="&lt;ID&gt;" src="...#parent=&lt;PARENTURL&gt;&rpctoken=&lt;RANDOM&gt;"/&gt;
+     *      and add to DOM.
+     * <li> Call gadgets.rpc.setupReceiver("&lt;ID>");
+     *      <p>All parent/child communication initializes automatically from here.
+     *         Naturally, both sides need to include the library.
+     * </ol>
+     *
+     * <p>Detailed container/parent instructions:
+     * <ol>
+     * <li> Create the target IFRAME (eg. gadget) with a given &lt;ID> and params
+     *    rpctoken=<token> (eg. #rpctoken=1234), which is a random/unguessbable
+     *    string, and parent=&lt;url>, where &lt;url> is the URL of the container.
+     * <li> Append IFRAME to the document.
+     * <li> Call gadgets.rpc.setupReceiver(&lt;ID>)
+     * <p>[Optional]. Strictly speaking, you may omit rpctoken and parent. This
+     *             practice earns little but is occasionally useful for testing.
+     *             If you omit parent, you MUST pass your container URL as the 2nd
+     *             parameter to this method.
+     * </ol>
+     *
+     * <p>Detailed gadget/child IFRAME instructions:
+     * <ol>
+     * <li> If your container/parent passed parent and rpctoken params (query string
+     *    or fragment are both OK), you needn't do anything. The library will self-
+     *    initialize.
+     * <li> If "parent" is omitted, you MUST call this method with targetId '..'
+     *    and the second param set to the parent URL.
+     * <li> If "rpctoken" is omitted, but the container set an authToken manually
+     *    for this frame, you MUST pass that ID (however acquired) as the 3rd param
+     *    to this method.
+     * </ol>
+     *
+     * @member gadgets.rpc
+     * @param {string} targetId
+     * @param {string=} opt_receiverurl
+     * @param {string=} opt_authtoken
+     */
+    function setupReceiver(targetId, opt_receiverurl, opt_authtoken) {
+      if (targetId === '..') {
+        // Gadget/IFRAME to container.
+        var rpctoken = opt_authtoken || params['rpctoken'] || params['ifpctok'] || '';
+        setupContainedContext(rpctoken, opt_receiverurl);
+      } else {
+        // Container to child.
+        setupChildIframe(targetId, opt_receiverurl, opt_authtoken);
+      }
+    }
+
+    function getReferrer(targetId) {
+      if (passReferrerDirection === 'bidir' ||
+          (passReferrerDirection === 'c2p' && targetId === '..') ||
+          (passReferrerDirection === 'p2c' && targetId !== '..')) {
+        var href = window.location.href;
+        var lopOff = '?';  // default = origin
+        if (passReferrerContents === 'query') {
+          lopOff = '#';
+        } else if (passReferrerContents === 'hash') {
+          return href;
+        }
+        var lastIx = href.lastIndexOf(lopOff);
+        lastIx = lastIx === -1 ? href.length : lastIx;
+        return href.substring(0, lastIx);
+      }
+      return null;
+    }
+
+    return /** @scope gadgets.rpc */ {
+      config: function(config) {
+        if (typeof config.securityCallback === 'function') {
+          securityCallback = config.securityCallback;
+        }
+        if (typeof config.arbitrator === 'function') {
+          arbitrate = config.arbitrator;
+        }
+      },
+
+      /**
+       * Registers an RPC service.
+       * @param {string} serviceName Service name to register.
+       * @param {function(Object,Object)} handler Service handler.
+       *
+       * @return The old service handler for serviceName, if any.
+       * @member gadgets.rpc
+       */
+      register: function(serviceName, handler) {
+        if (serviceName === CALLBACK_NAME || serviceName === ACK) {
+          throw new Error('Cannot overwrite callback/ack service');
+        }
+
+        if (serviceName === DEFAULT_NAME) {
+          throw new Error('Cannot overwrite default service:'
+                        + ' use registerDefault');
+        }
+
+        var old = services[serviceName];
+        services[serviceName] = handler;
+        return old;
+      },
+
+      /**
+       * Unregisters an RPC service.
+       * @param {string} serviceName Service name to unregister.
+       *
+       * @member gadgets.rpc
+       */
+      unregister: function(serviceName) {
+        if (serviceName === CALLBACK_NAME || serviceName === ACK) {
+          throw new Error('Cannot delete callback/ack service');
+        }
+
+        if (serviceName === DEFAULT_NAME) {
+          throw new Error('Cannot delete default service:'
+                        + ' use unregisterDefault');
+        }
+
+        delete services[serviceName];
+      },
+
+      /**
+       * Registers a default service handler to processes all unknown
+       * RPC calls which raise an exception by default.
+       * @param {function(Object,Object)} handler Service handler.
+       *
+       * @member gadgets.rpc
+       */
+      registerDefault: function(handler) {
+        services[DEFAULT_NAME] = handler;
+      },
+
+      /**
+       * Unregisters the default service handler. Future unknown RPC
+       * calls will fail silently.
+       *
+       * @member gadgets.rpc
+       */
+      unregisterDefault: function() {
+        delete services[DEFAULT_NAME];
+      },
+
+      /**
+       * Forces all subsequent calls to be made by a transport
+       * method that allows the caller to verify the message receiver
+       * (by way of the parent parameter, through getRelayUrl(...)).
+       * At present this means IFPC or WPM.
+       * @member gadgets.rpc
+       */
+      forceParentVerifiable: function() {
+        if (!transport.isParentVerifiable()) {
+          transport = gadgets.rpctx.ifpc;
+        }
+      },
+
+      /**
+       * Calls an RPC service.
+       * @param {string} targetId Module Id of the RPC service provider.
+       *                          Empty if calling the parent container.
+       * @param {string} serviceName Service name to call.
+       * @param {function()|null} callback Callback function (if any) to process
+       *                                 the return value of the RPC request.
+       * @param {*} var_args Parameters for the RPC request.
+       *
+       * @member gadgets.rpc
+       */
+      call: function(targetId, serviceName, callback, var_args) {
+        targetId = targetId || '..';
+        // Default to the container calling.
+        var from = '..';
+
+        if (targetId === '..') {
+          from = rpcId;
+        } else if (targetId.charAt(0) == '/') {
+          // sending to sibling
+          from = makeSiblingId(rpcId, gadgets.rpc.getOrigin(window.location.href));
+        }
+
+        ++callId;
+        if (callback) {
+          callbacks[callId] = callback;
+        }
+
+        var rpc = {
+          's': serviceName,
+          'f': from,
+          'c': callback ? callId : 0,
+          'a': Array.prototype.slice.call(arguments, 3),
+          't': authToken[targetId],
+          'l': !!useLegacyProtocol[targetId]
+        };
+
+        var referrer = getReferrer(targetId);
+        if (referrer) {
+          rpc['r'] = referrer;
+        }
+
+        if (targetId !== '..' &&
+            parseSiblingId(targetId) == null &&  // sibling never in the document
+            !document.getElementById(targetId)) {
+          // The target has been removed from the DOM. Don't even try.
+          return;
+        }
+
+        // If target is on the same domain, call method directly
+        if (callSameDomain(targetId, rpc)) {
+          return;
+        }
+
+        // Attempt to make call via a cross-domain transport.
+        // Retrieve the transport for the given target - if one
+        // target is misconfigured, it won't affect the others.
+        // In the case of a sibling relay, channel is not found
+        // in the receiverTx map but in the transport itself.
+        var channel = receiverTx[targetId];
+        if (!channel && parseSiblingId(targetId) !== null) {
+          // Sibling-to-sibling communication; use default trasport
+          // (in practice, wpm) despite not being ready()-indicated.
+          channel = transport;
+        }
+
+        if (!channel) {
+          // Not set up yet. Enqueue the rpc for such time as it is.
+          if (!earlyRpcQueue[targetId]) {
+            earlyRpcQueue[targetId] = [rpc];
+          } else {
+            earlyRpcQueue[targetId].push(rpc);
+          }
+          return;
+        }
+
+        // If we are told to use the legacy format, then we must
+        // default to IFPC.
+        if (useLegacyProtocol[targetId]) {
+          channel = gadgets.rpctx.ifpc;
+        }
+
+        if (channel.call(targetId, from, rpc) === false) {
+          // Fall back to IFPC. This behavior may be removed as IFPC is as well.
+          receiverTx[targetId] = fallbackTransport;
+          transport.call(targetId, from, rpc);
+        }
+      },
+
+      getRelayUrl: getRelayUrl,
+      setRelayUrl: setRelayUrl,
+      setAuthToken: setAuthToken,
+      setupReceiver: setupReceiver,
+      getAuthToken: getAuthToken,
+
+      // Note: Does not delete iframe
+      removeReceiver: function(receiverId) {
+        delete relayUrl[receiverId];
+        delete useLegacyProtocol[receiverId];
+        delete authToken[receiverId];
+        delete setup[receiverId];
+        delete sameDomain[receiverId];
+        delete receiverTx[receiverId];
+      },
+
+      /**
+       * Gets the RPC relay mechanism.
+       * @return {string} RPC relay mechanism. See above for
+       *   a list of supported types.
+       *
+       * @member gadgets.rpc
+       */
+      getRelayChannel: function() {
+        return transport.getCode();
+      },
+
+      /**
+       * Receives and processes an RPC request. (Not to be used directly.)
+       * Only used by IFPC.
+       * @param {Array.<string>} fragment An RPC request fragment encoded as
+       *        an array. The first 4 elements are target id, source id & call id,
+       *        total packet number, packet id. The last element stores the actual
+       *        JSON-encoded and URI escaped packet data.
+       *
+       * @member gadgets.rpc
+       * @deprecated
+       */
+      receive: function(fragment, otherWindow) {
+        if (fragment.length > 4) {
+          transport._receiveMessage(fragment, process);
+        } else {
+          relayOnload.apply(null, fragment.concat(otherWindow));
+        }
+      },
+
+      /**
+       * Receives and processes an RPC request sent via the same domain.
+       * (Not to be used directly). Converts the inbound rpc object's
+       * Array into a local Array to pass the process() Array test.
+       * @param {Object} rpc RPC object containing all request params.
+       * @member gadgets.rpc
+       */
+      receiveSameDomain: function(rpc) {
+        // Pass through to local process method but converting to a local Array
+        rpc['a'] = Array.prototype.slice.call(rpc['a']);
+        window.setTimeout(function() { process(rpc); }, 0);
+      },
+
+      // Helper method to get the protocol://host:port of an input URL.
+      // see docs above
+      getOrigin: getOrigin,
+      getTargetOrigin: getTargetOrigin,
+
+      /**
+       * Internal-only method used to initialize gadgets.rpc.
+       * @member gadgets.rpc
+       */
+      init: function() {
+        // Conduct any global setup necessary for the chosen transport.
+        // Do so after gadgets.rpc definition to allow transport to access
+        // gadgets.rpc methods.
+        if (transport.init(process, transportReady) === false) {
+          transport = fallbackTransport;
+        }
+        if (isChild) {
+          setupReceiver('..');
+        } else {
+          gadgets.config.register('rpc', null, function(config) {
+            var cfg = config['rpc'] || {};
+            setReferrerConfig(cfg);
+            setLegacyProtocolConfig(cfg);
+          });
+        }
+      },
+
+      /** Returns the window keyed by the ID. null/".." for parent, else child */
+      _getTargetWin: getTargetWin,
+
+      /** Parses a sibling id into {id: <siblingId>, origin: <siblingOrigin>} */
+      _parseSiblingId: parseSiblingId,
+
+      ACK: ACK,
+
+      RPC_ID: rpcId || '..',
+
+      SEC_ERROR_LOAD_TIMEOUT: LOAD_TIMEOUT,
+      SEC_ERROR_FRAME_PHISH: FRAME_PHISH,
+      SEC_ERROR_FORGED_MSG: FORGED_MSG
+    };
+  }();
+
+  // Initialize library/transport.
+  gadgets.rpc.init();
+
+} // !end of double-inclusion guard
diff --git a/trunk/features/src/main/javascript/features/rpc/wpm.transport.js b/trunk/features/src/main/javascript/features/rpc/wpm.transport.js
new file mode 100644
index 0000000..0f9be75
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/rpc/wpm.transport.js
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+gadgets.rpctx = gadgets.rpctx || {};
+
+/**
+ * Transport for browsers that support native messaging (various implementations
+ * of the HTML5 postMessage method). Officially defined at
+ * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html.
+ *
+ * postMessage is a native implementation of XDC. A page registers that
+ * it would like to receive messages by listening the the "message" event
+ * on the window (document in DPM) object. In turn, another page can
+ * raise that event by calling window.postMessage (document.postMessage
+ * in DPM) with a string representing the message and a string
+ * indicating on which domain the receiving page must be to receive
+ * the message. The target page will then have its "message" event raised
+ * if the domain matches and can, in turn, check the origin of the message
+ * and process the data contained within.
+ *
+ *   wpm: postMessage on the window object.
+ *      - Internet Explorer 8+
+ *      - Safari 4+
+ *      - Chrome 2+
+ *      - Webkit nightlies
+ *      - Firefox 3+
+ *      - Opera 9+
+ */
+if (!gadgets.rpctx.wpm) {  // make lib resilient to double-inclusion
+
+  gadgets.rpctx.wpm = function() {
+    var process, ready;
+    var forceSecure = true;
+
+    function attachBrowserEvent(eventName, callback, useCapture) {
+      if (typeof window.addEventListener != 'undefined') {
+        window.addEventListener(eventName, callback, useCapture);
+      } else if (typeof window.attachEvent != 'undefined') {
+        window.attachEvent('on' + eventName, callback);
+      }
+    }
+
+    function removeBrowserEvent(eventName, callback, useCapture) {
+      if (window.removeEventListener) {
+        window.removeEventListener(eventName, callback, useCapture);
+      } else if (window.detachEvent) {
+        window.detachEvent('on' + eventName, callback);
+      }
+    }
+
+    function onmessage(packet) {
+      var rpc = gadgets.json.parse(packet.data);
+      if (!rpc || !rpc['f']) {
+        return;
+      }
+
+      // for security, check origin against expected value
+      var origin = gadgets.rpc.getTargetOrigin(rpc['f']);
+
+      // Opera's "message" event does not have an "origin" property (at least,
+      // it doesn't in version 9.64;  presumably, it will in version 10).  If
+      // event.origin does not exist, use event.domain.  The other difference is that
+      // while event.origin looks like <scheme>://<hostname>:<port>, event.domain
+      // consists only of <hostname>.
+      if (forceSecure && (typeof packet.origin !== 'undefined'
+          ? packet.origin !== origin
+          : packet.domain !== /^.+:\/\/([^:]+).*/.exec(origin)[1])) {
+        return;
+      }
+      process(rpc, packet.origin);
+    }
+
+    return {
+      getCode: function() {
+        return 'wpm';
+      },
+
+      isParentVerifiable: function() {
+        return true;
+      },
+
+      init: function(processFn, readyFn) {
+        function configure(config) {
+          var cfg = config ? config['rpc'] : {};
+          if (String(cfg['disableForceSecure']) === 'true') {
+            forceSecure = false;
+          }
+        }
+        gadgets.config.register('rpc', null, configure);
+
+        process = processFn;
+        ready = readyFn;
+
+        // Set up native postMessage handler.
+        attachBrowserEvent('message', onmessage, false);
+
+        ready('..', true);  // Immediately ready to send to parent.
+        return true;
+      },
+
+      setup: function(receiverId, token) {
+        // Indicate that we're ready to send to the given receiver.
+        ready(receiverId, true);
+        return true;
+      },
+
+      call: function(targetId, from, rpc) {
+        // targetOrigin = canonicalized relay URL
+        var origin = gadgets.rpc.getTargetOrigin(targetId);
+        var targetWin = gadgets.rpc._getTargetWin(targetId);
+        if (origin) {
+          // Some browsers (IE, Opera) have an implementation of postMessage that is
+          // synchronous, although HTML5 specifies that it should be asynchronous.  In
+          // order to make all browsers behave consistently, we wrap all postMessage
+          // calls in a setTimeout with a timeout of 0.
+          window.setTimeout(function() {
+              targetWin.postMessage(gadgets.json.stringify(rpc), origin); }, 0);
+        } else {
+          gadgets.error('No relay set (used as window.postMessage targetOrigin)' +
+              ', cannot send cross-domain message');
+        }
+        return true;
+      }
+    };
+  }();
+
+} // !end of double-inclusion guard
diff --git a/trunk/features/src/main/javascript/features/security-token/feature.xml b/trunk/features/src/main/javascript/features/security-token/feature.xml
new file mode 100644
index 0000000..2e73dc0
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/security-token/feature.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <!--
+    security-token is implemented in server code.
+
+    It serves as a placeholder that signals when a gadget requires a security
+    token for proper operation. It does not indicate where the token is needed
+    (fragment or query string).
+
+    This feature is referenced and used in a few ways:
+    a. Other features that require a security token, such as opensocial,
+       will depend on it. The transitive closure of the dependency tree thus
+       indicates such requests require a security token.
+    b. As noted in (a), metadata requests may be formed for a gadget which
+       request whether or not a security token is needed for rendering the
+       gadget. This makes it possible to intelligently choose when to mint
+       and include a security token during rendering.
+    c. As a corollary to (a) and (b), this feature depends on locked-domain,
+       again to provide a clear mechanism for containers to render gadgets
+       on the locked-domain when rendered in an IFRAME. This ensures token security.
+    d. Another corollary to (a) and (b), a dep on auth-refresh ensures it too
+       is pulled in when necessary. This obviates the need for containers to
+       manually append &libs=auth-refresh to support this.
+    e. GadgetSpec processing code automatically includes this feature when
+       OAuth tags are included in the gadget, signaling the token's need.
+  -->
+  <name>security-token</name>
+  <dependency>locked-domain</dependency>
+  <dependency>auth-refresh</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/selection/feature.xml b/trunk/features/src/main/javascript/features/selection/feature.xml
new file mode 100644
index 0000000..2c09e64
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/selection/feature.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>selection</name>
+  <dependency>globals</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="selection.js"/>
+    <script src="taming.js"/>
+    <api>
+      <exports type="js">gadgets.selection.setSelection</exports>
+      <exports type="js">gadgets.selection.getSelection</exports>
+      <exports type="js">gadgets.selection.addListener</exports>
+      <exports type="js">gadgets.selection.removeListener</exports>
+      <exports type="rpc">gadgets.selection.selectionChanged</exports>
+      <uses type="rpc">gadgets.selection.set</uses>
+      <uses type="rpc">gadgets.selection.register</uses>
+    </api>
+  </gadget>
+  <container>
+    <script src="selection_container.js"/>
+    <api>
+      <exports type="rpc">osapi.container.Container.selection</exports>
+      <exports type="rpc">osapi.container.Container.selection.setSelection</exports>
+      <exports type="rpc">osapi.container.Container.selection.getSelection</exports>
+      <exports type="rpc">osapi.container.Container.selection.addListener</exports>
+      <exports type="rpc">osapi.container.Container.selection.removeListener</exports>
+      <uses type="rpc">gadgets.selection.selectionChanged</uses>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/selection/selection.js b/trunk/features/src/main/javascript/features/selection/selection.js
new file mode 100644
index 0000000..9cf9e44
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/selection/selection.js
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Gadget-side library for participating in selection eventing.
+ */
+
+/**
+ * @static
+ * @class Selection class for gadgets.
+ * Provides framework for selection eventing.
+ * @name gadgets.selection
+ */
+gadgets['selection'] = function() {
+  var listeners,
+      currentSelection;
+
+  function addListener(listener) {
+    if (!listeners) {
+      listeners  = [];
+      gadgets.rpc.call('..', 'gadgets.selection.register', function(selection) {
+        currentSelection = selection;
+      });
+    }
+    if (typeof listener === 'function') {
+      listeners.push(listener); // add the listener to the list
+    }
+  }
+
+  gadgets.util.registerOnLoadHandler(function() {
+    gadgets.rpc.register('gadgets.selection.selectionChanged', function(selection) {
+      if (this.f !== '..') {
+        return;
+      }
+      currentSelection = selection;
+      for (var i=0, currentListener; currentListener=listeners[i]; i++) {
+        listeners[i](selection);
+      }
+    });
+
+    // Start watching selection.
+    // TODO: change getSelection api to be async so we don't need to do this.
+    addListener(function(){});
+  });
+
+  return /** @scope gadgets.selection */ {
+    /**
+     * Sets the current selection.
+     * @param {string} selection Selected object.
+     */
+    setSelection: function(selection) {
+      currentSelection = selection;
+      gadgets.rpc.call('..', 'gadgets.selection.set', null, selection);
+    },
+
+    /**
+     * Gets the current selection.
+     * @return {Object} the current selection.
+     */
+    getSelection: function() {
+      return currentSelection;
+    },
+
+    /**
+     * Registers a listener for selection.
+     * @param {function} listener The listener to remove.
+     */
+    addListener: addListener,
+
+    /**
+     * Removes a listener for selection.
+     * @param {function} listener The listener to remove.
+     */
+    removeListener: function(listener) {
+      for (var i = 0, currentListener; currentListener=listeners[i]; i++) {
+        if (currentListener === listener) {
+          listeners.splice(i, 1);
+          break;
+        }
+      }
+    }
+  };
+}();
diff --git a/trunk/features/src/main/javascript/features/selection/selection_container.js b/trunk/features/src/main/javascript/features/selection/selection_container.js
new file mode 100644
index 0000000..eb76ffb
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/selection/selection_container.js
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Container-side selection manager.
+ */
+
+/**
+ * @static
+ * @class Manages selection and selection listeners.
+ * @name gadgets.selectionmanager
+ */
+(function() {
+
+  var _selection,
+      listeners = [],
+      listeningGadgets = {};
+
+  function addSelectionListener(listener) {
+    if (typeof listener === 'function') {
+      listeners.push(listener);
+    }
+  }
+
+  function removeSelectionListener(listener) {
+    for (var i = 0, currentListener; currentListener=listeners[i]; i++) {
+      if (currentListener === listener) {
+        listeners.splice(i, 1);
+        break;
+      }
+    }
+  }
+
+  osapi.container.Container.addMixin('selection', function(context) {
+
+    function notifySelection(selection) {
+      _selection = selection;
+      for(var i=0, currentListener; currentListener=listeners[i]; i++) {
+        listeners[i](selection);
+      }
+
+      // Call rpc endpoint in all gadgets that have registered
+      for (var to in listeningGadgets) {
+        if (!context.getGadgetSiteByIframeId_(to)) {
+          delete listeningGadgets[to];  // Remove sites that are no longer with us
+        }
+        else {
+          gadgets.rpc.call(to, 'gadgets.selection.selectionChanged', null, selection);
+        }
+      }
+    }
+
+    context.rpcRegister('gadgets.selection.set', function(rpcArgs, selection) {
+      notifySelection(selection);
+    });
+
+    context.rpcRegister('gadgets.selection.register', function(rpcArgs) {
+      listeningGadgets[rpcArgs.f] = 1;
+      return _selection;
+    });
+
+    return /** @scope gadgets.selection */ {
+      /**
+       * Sets the current selection.
+       * @param {string} selection Selected object.
+       */
+      setSelection: notifySelection,
+
+      /**
+       * Gets the current selection.
+       * @return {Object} the current selection.
+       */
+      getSelection: function() {
+        return _selection;
+      },
+
+      /**
+       * Registers a listener for selection.
+       * @param {function} listener The listener to remove.
+       */
+      addListener: addSelectionListener,
+
+      /**
+       * Removes a listener for selection.
+       * @param {function} listener The listener to remove.
+       */
+      removeListener: removeSelectionListener
+    };
+  });
+
+})();
diff --git a/trunk/features/src/main/javascript/features/selection/taming.js b/trunk/features/src/main/javascript/features/selection/taming.js
new file mode 100644
index 0000000..c117272
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/selection/taming.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+var tamings___ = tamings___ || [];
+tamings___.push(function(imports) {
+  ___.grantRead(gadgets.selection, 'setSelection');
+  ___.grantRead(gadgets.selection, 'getSelection');
+  ___.grantRead(gadgets.selection, 'addListener');
+  ___.grantRead(gadgets.selection, 'removeListener');
+});
diff --git a/trunk/features/src/main/javascript/features/setprefs/feature.xml b/trunk/features/src/main/javascript/features/setprefs/feature.xml
new file mode 100644
index 0000000..6a60da8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/setprefs/feature.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>setprefs</name>
+  <dependency>core.prefs</dependency>
+  <dependency>core.util</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="setprefs.js"/>
+    <api>
+      <exports type="js">gadgets.Prefs</exports>
+      <exports type="js">gadgets.Prefs.prototype.set</exports>
+      <exports type="js">gadgets.Prefs.prototype.setArray</exports>
+      <uses type="rpc">set_pref</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/setprefs/setprefs.js b/trunk/features/src/main/javascript/features/setprefs/setprefs.js
new file mode 100644
index 0000000..62c9b96
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/setprefs/setprefs.js
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/*global gadgets */
+
+/**
+ * @fileoverview This library augments gadgets.Prefs with functionality
+ * to store prefs dynamically.
+ */
+
+/**
+ * Stores a preference.
+ * @param {string | Object} key The pref to store.
+ * @param {string} value The values to store.
+ * @private This feature is documented in prefs.js
+ */
+gadgets.Prefs.prototype.set = function(key, value) {
+  var needUpdate = false;
+  if (arguments.length > 2) {
+    // For backwards compatibility. This can take the form:
+    // prefs.set(key0, value0, key1, value1, key2, value2);
+
+    // prefs.set({key0: value0, key1: value1, key2: value2});
+    var obj = {};
+    for (var i = 0, j = arguments.length; i < j; i += 2) {
+      obj[arguments[i]] = arguments[i + 1];
+    }
+    needUpdate = gadgets.Prefs.setInternal_(obj);
+  } else {
+    needUpdate = gadgets.Prefs.setInternal_(key, value);
+  }
+  if (!needUpdate) {
+    return;
+  }
+
+  var args = [
+    null, // go to parent
+    'set_pref', // service name
+    null, // no callback
+    gadgets.util.getUrlParameters()['ifpctok'] ||
+        gadgets.util.getUrlParameters()['rpctoken'] || 0 // Legacy IFPC "security".
+  ].concat(Array.prototype.slice.call(arguments));
+
+  gadgets.rpc.call.apply(gadgets.rpc, args);
+};
+
+/**
+ * Stores a preference from the given list.
+ * @param {string} key The pref to store.
+ * @param {Array.<string | number>} val The values to store.
+ * @private This feature is documented in prefs.js
+ */
+gadgets.Prefs.prototype.setArray = function(key, val) {
+  // We must escape pipe (|) characters to ensure that decoding in
+  // getArray actually works properly.
+  for (var i = 0, j = val.length; i < j; ++i) {
+    if (typeof val[i] !== 'number') {
+      val[i] = val[i].replace(/\|/g, '%7C');
+    }
+  }
+  this.set(key, val.join('|'));
+};
+
diff --git a/trunk/features/src/main/javascript/features/settitle/feature.xml b/trunk/features/src/main/javascript/features/settitle/feature.xml
new file mode 100644
index 0000000..6ac8b85
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/settitle/feature.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>settitle</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>rpc</dependency>
+  <gadget>
+    <script src="settitle.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.window.setTitle</exports>
+      <exports type="js">_IG_SetTitle</exports>
+      <uses type="rpc">set_title</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/settitle/settitle.js b/trunk/features/src/main/javascript/features/settitle/settitle.js
new file mode 100644
index 0000000..b38e319
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/settitle/settitle.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library augments gadets.window with functionality
+ * to set the title of a gadget dynamically.
+ */
+
+
+/* NOTE: no class comment because one already exists in dynamic-height */
+gadgets.window = gadgets.window || {};
+
+/**
+ * Sets the gadget title.
+ * @param {string} title The preferred title.
+ * @scope gadgets.window
+ */
+gadgets.window.setTitle = function(title) {
+  gadgets.rpc.call(null, 'set_title', null, title);
+};
+
+// Alias for legacy code
+var _IG_SetTitle = gadgets.window.setTitle;
+
diff --git a/trunk/features/src/main/javascript/features/settitle/taming.js b/trunk/features/src/main/javascript/features/settitle/taming.js
new file mode 100644
index 0000000..b530f90
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/settitle/taming.js
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose gadgets.window.setTitle to cajoled gadgets
+ */
+
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.window, 'setTitle']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/shared-script-frame/feature.xml b/trunk/features/src/main/javascript/features/shared-script-frame/feature.xml
new file mode 100644
index 0000000..e341bc6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shared-script-frame/feature.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>shared-script-frame</name>
+  <dependency>container</dependency>
+  <dependency>container.util</dependency>
+  <dependency>rpc</dependency>
+  <container>
+    <script src="shared-script-frame-container.js"/>
+    <api>
+      <exports type="rpc">get_script_frame_name</exports>
+    </api>
+  </container>
+  <gadget>
+    <script src="shared-script-frame.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.script.getScriptFrame</exports>
+      <uses type="rpc">get_script_frame_name</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shared-script-frame/shared-script-frame-container.js b/trunk/features/src/main/javascript/features/shared-script-frame/shared-script-frame-container.js
new file mode 100644
index 0000000..6a6e650
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shared-script-frame/shared-script-frame-container.js
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This feature provides a means for multiple instances of a gadget to share
+ *   a single frame for loading script code.
+ *
+ *   This is the container specific code.
+ */
+
+osapi.container.Container.addMixin('SharedScriptFrame', function(container) {
+
+  /**
+   * Map of gadgetUrls to script frame sites.
+   * @type {!Object.<string, osapi.container.GadgetSite>}
+   * @constant
+   */
+  var siteMap = {};
+
+  /**
+   * Generates a key to hash the script frame by for this gadget.  If the
+   * gadget uses locked domains and specifies participants in the locked domain,
+   * those other participants will be figured into this key so that they in turn
+   * generate the same key.
+   *
+   * @param {!string} url The gadget URL that requested the script frame.
+   * @param {?Object} ldFeature The feature segment of the gadget for the
+   *   locked-domain feature.
+   */
+  var getFrameKey = function(url, ldFeature) {
+    var participants, filtered = {};
+    filtered[url.toLowerCase()] = 1;
+
+    if (ldFeature && ldFeature.params && (participants = ldFeature.params.participant)) {
+      if (typeof(participants) == 'string') {
+        filtered[participants.toLowerCase()] = 1;
+      }
+      else {
+        for (var i = 0, participant; participant = participants[i]; i++) {
+          filtered[participant.toLowerCase()] = 1;
+        }
+      }
+    }
+
+    var ret = [];
+    for (i in filtered) {
+      ret.push(i);
+    }
+    return ret.sort().join('');
+  }
+
+  /**
+   * Creates a new shared script frame gadget instance on the page.
+   *
+   * @param {!string} url The gadget URL that requested the script frame.
+   * @param {!Object} feature The feature segment of the gadget for the
+   *   shared-script-frame feature.
+   * @param {?Object} ldFeature The feature segment of the gadget for the
+   *   locked-domain feature.
+   */
+  var createScriptFrame = function(url, feature, ldFeature) {
+    var key = getFrameKey(url, ldFeature);
+    if (siteMap[key]) {
+      return;
+    }
+
+    var view = osapi.container.GadgetSite.DEFAULT_VIEW_;
+    if (feature.params && feature.params.view) {
+      view = feature.params.view[0];
+    }
+
+    var elem = document.createElement('div');
+    elem.style.display = 'none';
+    document.body.appendChild(elem);
+
+    var site = siteMap[key] = container.newGadgetSite(elem);
+    var params = {};
+    params[osapi.container.RenderParam.VIEW] = view;
+    container.navigateGadget(site, url, undefined, params);
+  };
+
+  /**
+   * Searches the map for the script frame being requested, if not found
+   * it will create it.  Will call the rpc callback with the found or created
+   * script frame name.
+   *
+   * @param {!Object} rpcArgs The arguments from the RPC call
+   * @returns {?string} The name of the script frame
+   */
+  var getScriptFrameName = function(rpcArgs) {
+    var info = rpcArgs.gs.getActiveSiteHolder().getGadgetInfo(),
+        key = getFrameKey(info.url, info.modulePrefs.features['locked-domain']);
+
+    var name, scriptSite = siteMap[key];
+    if (scriptSite) {
+      name = scriptSite.getActiveSiteHolder().getIframeId();
+    }
+    return name;
+  };
+
+  /**
+   * Holds functions to respond to life-cycle events
+   *
+   * @type {!Object}
+   * @constant
+   */
+  var lifeCycleHandlers = {};
+
+  /**
+   * Respond to the ON_BEFORE_RENDER event by creating a script frame for the
+   * loading gadget, but only if we need one.
+   *
+   * @param {!Object} metadata The gadget metadata
+   */
+  lifeCycleHandlers[osapi.container.CallbackType.ON_BEFORE_RENDER] = function(metadata) {
+    var url = metadata.url;
+    try {
+      var feature = metadata.modulePrefs.features['shared-script-frame'];
+      var ldFeature = metadata.modulePrefs.features['locked-domain'];
+    } catch(e) {}
+    if (feature) {
+      createScriptFrame(url, feature, ldFeature);
+    }
+  };
+
+  container.addGadgetLifecycleCallback('shared-script-frame-setup', lifeCycleHandlers);
+  container.rpcRegister('get_script_frame_name', getScriptFrameName);
+  return {};
+});
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/shared-script-frame/shared-script-frame.js b/trunk/features/src/main/javascript/features/shared-script-frame/shared-script-frame.js
new file mode 100644
index 0000000..b55678d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shared-script-frame/shared-script-frame.js
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This feature provides a means for multiple instances of a gadget to share
+ *   a single frame for loading script code.
+ *
+ *   This is the gadget specific code.
+ */
+
+gadgets.script = (function(){
+
+  /**
+   * Obtain a reference to the script frame by name.
+   *
+   * @param {string} name The name of the frame to request.
+   * @return {?Window} The requested frame, or null if not available.
+   */
+  var getFrameByName = function(name) {
+    return name ? window.open('', name) : null;
+  };
+
+  /**
+   * Make RPC call to obtain the script frame name, then call the callback with the
+   * passing in a reference to the script frame.
+   *
+   * @param {function(?Window)} callback The callback function that the gadget passes in.
+   *   This function is passed the shared-script-frame window.  The param may be undefined
+   *   if something goes wrong, gadgets should verify.
+   */
+  var getScriptFrame = function(callback) {
+    gadgets.rpc.call(null, 'get_script_frame_name', function(name) {
+      callback(getFrameByName(name));
+    });
+  };
+
+  return {
+    getScriptFrame: getScriptFrame
+  };
+})();
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/shared-script-frame/taming.js b/trunk/features/src/main/javascript/features/shared-script-frame/taming.js
new file mode 100644
index 0000000..a0fda1e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shared-script-frame/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.script.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.script, 'getScriptFrame']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/shindig.auth/auth-init.js b/trunk/features/src/main/javascript/features/shindig.auth/auth-init.js
new file mode 100644
index 0000000..718bd9e
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.auth/auth-init.js
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Bootstraps auth.js.
+ */
+
+shindig.auth = new shindig.Auth();
diff --git a/trunk/features/src/main/javascript/features/shindig.auth/auth.js b/trunk/features/src/main/javascript/features/shindig.auth/auth.js
new file mode 100644
index 0000000..02966e3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.auth/auth.js
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*global gadgets */
+
+/**
+ * @fileoverview
+ *
+ * Manages the gadget security token AKA the gadget auth token AKA the
+ * social token.  Also provides an API for the container server to
+ * efficiently pass authenticated data to the gadget at render time.
+ *
+ * The shindig.auth package is not part of the opensocial or gadgets spec,
+ * and gadget authors should never use these functions or the security token
+ * directly.  These APIs are an implementation detail and are for shindig
+ * internal use only.
+ *
+ * Passing authenticated data into the gadget at render time:
+ *
+ * The gadget auth token is the only way for the container to allow the
+ * gadget access to authenticated data.  gadgets.io.makeRequest for SIGNED
+ * or OAUTH requests relies on the authentication token.  Access to social data
+ * also relies on the authentication token.
+ *
+ * The authentication token is normally passed into the gadget on the URL
+ * fragment (after the #), and so is not visible to the gadget rendering
+ * server.  This keeps the token from being leaked in referer headers, but at
+ * the same time limits the amount of authenticated data the gadget can view
+ * quickly: fetching authenticated data requires an extra round trip.
+ *
+ * If the authentication token is passed to the gadget as a query parameter,
+ * the gadget rendering server gets an opportunity to view the token during
+ * the rendering process.  This allows the rendering server to quickly inject
+ * authenticated data into the gadget, at the price of potentially leaking
+ * the authentication token in referer headers.  That risk can be mitigated
+ * by using a short-lived authentication token on the query string, which
+ * the gadget server can swap for a longer lived token at render time.
+ *
+ * If the rendering server injects authenticated data into the gadget in the
+ * form of a JSON string, the resulting javascript object can be accessed via
+ * shindig.auth.getTrustedData.
+ *
+ * To access the security token:
+ *   var st = shindig.auth.getSecurityToken();
+ *
+ * To update the security token with new data from the gadget server:
+ *   shindig.auth.updateSecurityToken(newToken);
+ *
+ * To quickly access a javascript object that has been authenticated by the
+ * container and the rendering server:
+ *   var trusted = shindig.auth.getTrustedData();
+ *   doSomething(trusted.foo.bar);
+ */
+
+/**
+ * Class used to mange the gadget auth token.  Singleton initialized from
+ * auth-init.js.
+ *
+ * @constructor
+ */
+shindig.Auth = function() {
+  /**
+   * The authentication token.
+   */
+  var authToken = null;
+
+  /**
+   * Trusted object from container.
+   */
+  var trusted = null;
+
+  /**
+   * Copy URL parameters into the auth token
+   *
+   * The initial auth token can look like this:
+   *    t=abcd&url=$&foo=
+   *
+   * If any of the values in the token are '$', a matching parameter
+   * from the URL will be inserted, for example:
+   *    t=abcd&url=http%3A%2F%2Fsome.gadget.com&foo=
+   *
+   * Why do this at all?  The only currently known use case for this is
+   * efficiently including the gadget URL in the auth token.  If you embed
+   * the entire URL in the security token, you effectively double the size
+   * of the URL passed on the gadget rendering request:
+   *   /gadgets/ifr?url=<gadget-url>#st=<encrypted-gadget-url>
+   *
+   * This can push the gadget render URL beyond the max length supported
+   * by browsers, and then things break.  To work around this, the
+   * security token can include only a (much shorter) hash of the gadget-url:
+   *  /gadgets/ifr?url=<gadget-url>#st=<xyz>
+   *
+   * However, we still want the proxy that handles gadgets.io.makeRequest
+   * to be able to look up the gadget URL efficiently, without requring
+   * a database hit.  To do that, we modify the auth token here to fill
+   * in any blank values.  The auth token then becomes:
+   *    t=<xyz>&url=<gadget-url>
+   *
+   * We send the expanded auth token in the body of post requests, so we
+   * don't run into problems with length there.  (But people who put
+   * several hundred characters in their gadget URLs are still lame.)
+   * @param {Object} urlParams
+   */
+  function addParamsToToken(urlParams) {
+    var args = authToken.split('&');
+    for (var i = 0; i < args.length; i++) {
+      var nameAndValue = args[i].split('=');
+      if (nameAndValue.length === 2) {
+        var name = nameAndValue[0];
+        var value = nameAndValue[1];
+        if (value === '$') {
+          value = encodeURIComponent(urlParams[name]);
+          args[i] = name + '=' + value;
+        }
+      }
+    }
+    authToken = args.join('&');
+  }
+
+  function init(configuration) {
+    var urlParams = gadgets.util.getUrlParameters();
+    var config = configuration['shindig.auth'] || {};
+
+    // Auth token - might be injected into the gadget directly, or might
+    // be on the URL (hopefully on the fragment).
+    if (config['authToken']) {
+      authToken = config['authToken'];
+    } else if (urlParams['st']) {
+      authToken = urlParams['st'];
+    }
+    if (authToken !== null) {
+      addParamsToToken(urlParams);
+    }
+
+    // Trusted JSON.  We use eval directly because this was injected by the
+    // container server and json parsing is slow in IE.
+    if (config['trustedJson']) {
+      trusted = eval('(' + config['trustedJson'] + ')');
+    }
+  }
+
+  gadgets.config.register('shindig.auth', null, init);
+
+  return /** @scope shindig.auth */ {
+
+    /**
+     * Gets the auth token.
+     *
+     * @return {string} the gadget authentication token.
+     *
+     * @member shindig.auth
+     */
+    getSecurityToken: function() {
+      return authToken;
+    },
+
+    /**
+     * Updates the security token with new data from the gadget server.
+     *
+     * @param {string} newToken the new auth token data.
+     *
+     * @member shindig.auth
+     */
+    updateSecurityToken: function(newToken) {
+      authToken = newToken;
+    },
+
+    /**
+     * Quickly retrieves data that is known to have been injected by
+     * a trusted container server.
+     * @return {Object}
+     */
+    getTrustedData: function() {
+      return trusted;
+    }
+  };
+};
diff --git a/trunk/features/src/main/javascript/features/shindig.auth/feature.xml b/trunk/features/src/main/javascript/features/shindig.auth/feature.xml
new file mode 100644
index 0000000..c85f3ee
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.auth/feature.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements. See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership. The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>shindig.auth</name>
+  <dependency>core.config</dependency>
+  <dependency>core.util</dependency>
+  <all>
+    <script src="auth.js"/>
+    <script src="auth-init.js"/>
+    <api>
+      <exports type="js">shindig.Auth.getSecurityToken</exports>
+      <exports type="js">shindig.Auth.updateSecurityToken</exports>
+      <exports type="js">shindig.Auth.getTrustedData</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.container-1.0/feature.xml b/trunk/features/src/main/javascript/features/shindig.container-1.0/feature.xml
new file mode 100644
index 0000000..67dd56b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.container-1.0/feature.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>shindig.container-1.0</name>
+  <dependency>container</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.container/cookiebaseduserprefstore.js b/trunk/features/src/main/javascript/features/shindig.container/cookiebaseduserprefstore.js
new file mode 100644
index 0000000..a092699
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.container/cookiebaseduserprefstore.js
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Implements the gadgets.UserPrefStore interface using a cookies
+ * based implementation. Depends on cookies.js. This code should not be used in
+ * a production environment.
+ */
+
+/**
+ * Cookie-based user preference store.
+ * @constructor
+ */
+shindig.CookieBasedUserPrefStore = function() {
+  gadgets.UserPrefStore.call(this);
+};
+
+shindig.CookieBasedUserPrefStore.inherits(shindig.UserPrefStore);
+
+shindig.CookieBasedUserPrefStore.prototype.USER_PREFS_PREFIX =
+    'gadgetUserPrefs-';
+
+shindig.CookieBasedUserPrefStore.prototype.getPrefs = function(gadget) {
+  var userPrefs = {};
+  var cookieName = this.USER_PREFS_PREFIX + gadget.id;
+  var cookie = shindig.cookies.get(cookieName);
+  if (cookie) {
+    var pairs = cookie.split('&');
+    for (var i = 0; i < pairs.length; i++) {
+      var nameValue = pairs[i].split('=');
+      var name = decodeURIComponent(nameValue[0]);
+      var value = decodeURIComponent(nameValue[1]);
+      userPrefs[name] = value;
+    }
+  }
+
+  return userPrefs;
+};
+
+shindig.CookieBasedUserPrefStore.prototype.savePrefs = function(gadget) {
+  var pairs = [];
+  for (var name in gadget.getUserPrefs()) {
+    var value = gadget.getUserPref(name);
+    var pair = encodeURIComponent(name) + '=' + encodeURIComponent(value);
+    pairs.push(pair);
+  }
+
+  var cookieName = this.USER_PREFS_PREFIX + gadget.id;
+  var cookieValue = pairs.join('&');
+  shindig.cookies.set(cookieName, cookieValue);
+};
+
+shindig.Container.prototype.userPrefStore =
+    new shindig.CookieBasedUserPrefStore();
diff --git a/trunk/features/src/main/javascript/features/shindig.container/cookies.js b/trunk/features/src/main/javascript/features/shindig.container/cookies.js
new file mode 100644
index 0000000..f919640
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.container/cookies.js
@@ -0,0 +1,271 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Functions for setting, getting and deleting cookies.
+ */
+
+/**
+ * Namespace for cookie functions
+ */
+
+// TODO: find the official solution for a cookies library
+shindig.cookies = {};
+
+
+shindig.cookies.JsType_ = {
+  UNDEFINED: 'undefined'
+};
+
+shindig.cookies.isDef = function(val) {
+  return typeof val != shindig.cookies.JsType_.UNDEFINED;
+};
+
+
+/**
+ * Sets a cookie.
+ * The max_age can be -1 to set a session cookie. To remove and expire cookies,
+ * use remove() instead.
+ *
+ * @param {string} name The cookie name.
+ * @param {string} value The cookie value.
+ * @param {number} opt_maxAge The max age in seconds (from now). Use -1 to set
+ *                            a session cookie. If not provided, the default is
+ *                            -1 (i.e. set a session cookie).
+ * @param {string} opt_path The path of the cookie, or null to not specify a
+ *                          path attribute (browser will use the full request
+ *                          path). If not provided, the default is '/' (i.e.
+ *                          path=/).
+ * @param {string} opt_domain The domain of the cookie, or null to not specify
+ *                            a domain attribute (browser will use the full
+ *                            request host name). If not provided, the default
+ *                            is null (i.e. let browser use full request host
+ *                            name).
+ */
+shindig.cookies.set = function(name, value, opt_maxAge, opt_path, opt_domain) {
+  // we do not allow '=' or ';' in the name
+  if (/;=/g.test(name)) {
+    throw new Error('Invalid cookie name "' + name + '"');
+  }
+  // we do not allow ';' in value
+  if (/;/g.test(value)) {
+    throw new Error('Invalid cookie value "' + value + '"');
+  }
+
+  if (!shindig.cookies.isDef(opt_maxAge)) {
+    opt_maxAge = -1;
+  }
+
+  var domainStr = opt_domain ? ';domain=' + opt_domain : '';
+  var pathStr = opt_path ? ';path=' + opt_path : '';
+
+  var expiresStr;
+
+  // Case 1: Set a session cookie.
+  if (opt_maxAge < 0) {
+    expiresStr = '';
+
+  // Case 2: Expire the cookie.
+  // Note: We don't tell people about this option in the function doc because
+  // we prefer people to use ExpireCookie() to expire cookies.
+  } else if (opt_maxAge === 0) {
+    // Note: Don't use Jan 1, 1970 for date because NS 4.76 will try to convert
+    // it to local time, and if the local time is before Jan 1, 1970, then the
+    // browser will ignore the Expires attribute altogether.
+    var pastDate = new Date(1970, 1 /*Feb*/, 1);  // Feb 1, 1970
+    expiresStr = ';expires=' + pastDate.toUTCString();
+
+  // Case 3: Set a persistent cookie.
+  } else {
+    var futureDate = new Date((new Date).getTime() + opt_maxAge * 1000);
+    expiresStr = ';expires=' + futureDate.toUTCString();
+  }
+
+  document.cookie = name + '=' + value + domainStr + pathStr + expiresStr;
+};
+
+
+/**
+ * Returns the value for the first cookie with the given name
+ * @param {string} name The name of the cookie to get.
+ * @param {string} opt_default If not found this is returned instead.
+ * @return {string|undefined} The value of the cookie. If no cookie is set this
+ *                            returns opt_default or undefined if opt_default is
+ *                            not provided.
+ */
+shindig.cookies.get = function(name, opt_default) {
+  var nameEq = name + '=';
+  var cookie = String(document.cookie);
+  for (var pos = -1; (pos = cookie.indexOf(nameEq, pos + 1)) >= 0;) {
+    var i = pos;
+    // walk back along string skipping whitespace and looking for a ; before
+    // the name to make sure that we don't match cookies whose name contains
+    // the given name as a suffix.
+    while (--i >= 0) {
+      var ch = cookie.charAt(i);
+      if (ch == ';') {
+        i = -1;  // indicate success
+        break;
+      }
+    }
+    if (i == -1) {  // first cookie in the string or we found a ;
+      var end = cookie.indexOf(';', pos);
+      if (end < 0) {
+        end = cookie.length;
+      }
+      return cookie.substring(pos + nameEq.length, end);
+    }
+  }
+  return opt_default;
+};
+
+
+/**
+ * Removes and expires a cookie.
+ *
+ * @param {string} name The cookie name.
+ * @param {string} opt_path The path of the cookie, or null to expire a cookie
+ *                          set at the full request path. If not provided, the
+ *                          default is '/' (i.e. path=/).
+ * @param {string} opt_domain The domain of the cookie, or null to expire a
+ *                            cookie set at the full request host name. If not
+ *                            provided, the default is null (i.e. cookie at
+ *                            full request host name).
+ */
+shindig.cookies.remove = function(name, opt_path, opt_domain) {
+  var rv = shindig.cookies.containsKey(name);
+  shindig.cookies.set(name, '', 0, opt_path, opt_domain);
+  return rv;
+};
+
+
+/**
+ * Gets the names and values for all the cookies
+ * @private
+ * @return {Object} An object with keys and values.
+ */
+shindig.cookies.getKeyValues_ = function() {
+  var cookie = String(document.cookie);
+  var parts = cookie.split(/\s*;\s*/);
+  var keys = [], values = [], index, part;
+  for (var i = 0; part = parts[i]; i++) {
+    index = part.indexOf('=');
+
+    if (index == -1) { // empty name
+      keys.push('');
+      values.push(part);
+    } else {
+      keys.push(part.substring(0, index));
+      values.push(part.substring(index + 1));
+    }
+  }
+  return {keys: keys, values: values};
+};
+
+
+/**
+ * Gets the names for all the cookies
+ * @return {Array} An array with the names of the cookies.
+ */
+shindig.cookies.getKeys = function() {
+  return shindig.cookies.getKeyValues_().keys;
+};
+
+
+/**
+ * Gets the values for all the cookies
+ * @return {Array} An array with the values of the cookies.
+ */
+shindig.cookies.getValues = function() {
+  return shindig.cookies.getKeyValues_().values;
+};
+
+
+/**
+ * Whether there are any cookies for this document
+ * @return {boolean}
+ */
+shindig.cookies.isEmpty = function() {
+  return document.cookie === '';
+};
+
+
+/**
+ * Returns the number of cookies for this document
+ * @return {number}
+ */
+shindig.cookies.getCount = function() {
+  var cookie = String(document.cookie);
+  if (cookie === '') {
+    return 0;
+  }
+  var parts = cookie.split(/\s*;\s*/);
+  return parts.length;
+};
+
+
+/**
+ * Returns whether there is a cookie with the given name
+ * @param {string} key The name of the cookie to test for.
+ * @return {boolean}
+ */
+shindig.cookies.containsKey = function(key) {
+  var sentinel = {};
+  // if get does not find the key it returns the default value. We therefore
+  // compare the result with an object to ensure we do not get any false
+  // positives.
+  return shindig.cookies.get(key, sentinel) !== sentinel;
+};
+
+
+/**
+ * Returns whether there is a cookie with the given value. (This is an O(n)
+ * operation.)
+ * @param {string} value The value to check for.
+ * @return {boolean}
+ */
+shindig.cookies.containsValue = function(value) {
+  // this O(n) in any case so lets do the trivial thing.
+  var values = shindig.cookies.getKeyValues_().values;
+  for (var i = 0; i < values.length; i++) {
+    if (values[i] == value) {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+/**
+ * Removes all cookies for this document
+ */
+shindig.cookies.clear = function() {
+  var keys = shindig.cookies.getKeyValues_().keys;
+  for (var i = keys.length - 1; i >= 0; i--) {
+    shindig.cookies.remove(keys[i]);
+  }
+};
+
+/**
+ * Static constant for the size of cookies. Per the spec, there's a 4K limit
+ * to the size of a cookie. To make sure users can't break this limit, we
+ * should truncate long cookies at 3950 bytes, to be extra careful with dumb
+ * browsers/proxies that interpret 4K as 4000 rather than 4096
+ * @type {number}
+ */
+shindig.cookies.MAX_COOKIE_LENGTH = 3950;
diff --git a/trunk/features/src/main/javascript/features/shindig.container/feature.xml b/trunk/features/src/main/javascript/features/shindig.container/feature.xml
new file mode 100644
index 0000000..a9f3203
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.container/feature.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+<!--
+Required configuration:
+A map of view names to view attributes. Examples:
+
+
+-->
+  <name>shindig-container</name>
+  <dependency>globals</dependency>
+  <dependency>core.config</dependency>
+  <dependency>rpc</dependency>
+  <dependency>osapi</dependency>
+  <dependency>shindig.uri.ext</dependency>
+  <container>
+    <script src="util.js"/>
+    <script src="cookies.js"/>
+    <script src="shindig-container.js"/>
+    <script src="osapi.js"/>
+    <api>
+      <exports type="rpc">osapi._handleGadgetRpcMethod</exports>
+      <exports type="rpc">resize_iframe</exports>
+      <exports type="rpc">set_pref</exports>
+      <exports type="rpc">set_title</exports>
+      <exports type="rpc">requestNavigateTo</exports>
+      <exports type="rpc">requestSendMessage</exports>
+    </api>
+  </container>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.container/osapi.js b/trunk/features/src/main/javascript/features/shindig.container/osapi.js
new file mode 100644
index 0000000..d66258d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.container/osapi.js
@@ -0,0 +1,108 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Base OSAPI binding.
+ */
+
+/**
+ * Container-side binding for the gadgetsrpctransport used by osapi. Containers
+ * add services to the client-side osapi implementation by defining them in the osapi
+ * namespace
+ */
+if (gadgets && gadgets.rpc) { //Only define if gadgets rpc exists
+
+  /**
+   * Dispatch a JSON-RPC batch request to services defined in the osapi namespace
+   * @param {Array} requests
+   */
+  osapi._handleGadgetRpcMethod = function(requests) {
+    var responses = new Array(requests.length);
+    var callCount = 0;
+    var callback = this.callback;
+    var dummy = function(params, apiCallback) {
+      apiCallback({});
+    };
+    for (var i = 0; i < requests.length; i++) {
+      // Don't allow underscores in any part of the method name as a convention
+      // for restricted methods
+      var current = osapi;
+      if (requests[i].method.indexOf('_') == -1) {
+        var path = requests[i].method.split('.');
+        for (var j = 0; j < path.length; j++) {
+          if (current.hasOwnProperty(path[j])) {
+            current = current[path[j]];
+          } else {
+            // No matching api
+            current = dummy;
+            break;
+          }
+        }
+      } else {
+        current = dummy;
+      }
+
+      // Execute the call and latch the rpc callback until all
+      // complete
+      current(requests[i].params, function(i) {
+        return function(response) {
+          // Put back in json-rpc format
+          responses[i] = { id: requests[i].id, data: response};
+          callCount++;
+          if (callCount == requests.length) {
+            callback(responses);
+          }
+        };
+      }(i));
+    }
+  };
+
+  osapi.container = {};
+
+  /**
+   * Basic implementation of system.listMethods which can be used to introspect
+   * available services
+   * @param request
+   * @param callback
+   */
+  osapi.container['listMethods'] = function(request, callback) {
+    var names = [];
+    recurseNames(osapi, '', 5, names);
+    callback(names);
+  };
+
+  /**
+   * Recurse the object paths to a limited depth
+   */
+  function recurseNames(base, path, depth, accumulated) {
+    if (depth == 0) return;
+    for (var prop in base) if (base.hasOwnProperty(prop)) {
+      if (prop.indexOf('_') == -1) {
+        var type = typeof(base[prop]);
+        if (type == 'function') {
+          accumulated.push(path + prop);
+        } else if (type == 'object') {
+          recurseNames(base[prop], path + prop + '.', depth - 1, accumulated);
+        }
+      }
+    }
+  }
+
+  // Register the osapi RPC dispatcher.
+  gadgets.rpc.register('osapi._handleGadgetRpcMethod', osapi._handleGadgetRpcMethod);
+}
diff --git a/trunk/features/src/main/javascript/features/shindig.container/shindig-container.js b/trunk/features/src/main/javascript/features/shindig.container/shindig-container.js
new file mode 100644
index 0000000..40afac3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.container/shindig-container.js
@@ -0,0 +1,986 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Open Gadget Container.
+ */
+
+shindig.errors = {};
+shindig.errors.SUBCLASS_RESPONSIBILITY = 'subclass responsibility';
+shindig.errors.TO_BE_DONE = 'to be done';
+
+/**
+ * Calls an array of asynchronous functions and calls the continuation
+ * function when all are done.
+ * @param {Array} functions Array of asynchronous functions, each taking
+ *     one argument that is the continuation function that handles the result
+ *     That is, each function is something like the following:
+ *     function(continuation) {
+ *       // compute result asynchronously
+ *       continuation(result);
+ *     }.
+ * @param {Function} continuation Function to call when all results are in.  It
+ *     is pass an array of all results of all functions.
+ * @param {Object} opt_this Optional object used as "this" when calling each
+ *     function.
+ */
+shindig.callAsyncAndJoin = function(functions, continuation, opt_this) {
+  var pending = functions.length;
+  var results = [];
+  for (var i = 0; i < functions.length; i++) {
+    // we need a wrapper here because i changes and we need one index
+    // variable per closure
+    var wrapper = function(index) {
+      var fn = functions[index];
+      if (typeof fn === 'string') {
+        fn = opt_this[fn];
+      }
+      fn.call(opt_this, function(result) {
+        results[index] = result;
+        if (--pending === 0) {
+          continuation(results);
+        }
+      });
+    };
+    wrapper(i);
+  }
+};
+
+
+// ----------
+// Extensible
+
+shindig.Extensible = function() {
+};
+
+/**
+ * Sets the dependencies.
+ * @param {Object} dependencies Object whose properties are set on this
+ *     container as dependencies.
+ */
+shindig.Extensible.prototype.setDependencies = function(dependencies) {
+  for (var p in dependencies) {
+    this[p] = dependencies[p];
+  }
+};
+
+/**
+ * Returns a dependency given its name.
+ * @param {String} name Name of dependency.
+ * @return {Object} Dependency with that name or undefined if not found.
+ */
+shindig.Extensible.prototype.getDependencies = function(name) {
+  return this[name];
+};
+
+
+
+// -------------
+// UserPrefStore
+
+/**
+ * User preference store interface.
+ * @constructor
+ */
+shindig.UserPrefStore = function() {
+};
+
+/**
+ * Gets all user preferences of a gadget.
+ * @param {Object} gadget Gadget object.
+ * @return {Object} All user preference of given gadget.
+ */
+shindig.UserPrefStore.prototype.getPrefs = function(gadget) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+/**
+ * Saves user preferences of a gadget in the store.
+ * @param {Object} gadget Gadget object.
+ * @param {Object} prefs User preferences.
+ */
+shindig.UserPrefStore.prototype.savePrefs = function(gadget) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+
+// -------------
+// DefaultUserPrefStore
+
+/**
+ * User preference store implementation.
+ * TODO: Turn this into a real implementation that is production safe
+ * @constructor
+ */
+shindig.DefaultUserPrefStore = function() {
+  shindig.UserPrefStore.call(this);
+};
+shindig.DefaultUserPrefStore.inherits(shindig.UserPrefStore);
+
+shindig.DefaultUserPrefStore.prototype.getPrefs = function(gadget) { };
+
+shindig.DefaultUserPrefStore.prototype.savePrefs = function(gadget) { };
+
+
+// -------------
+// GadgetService
+
+/**
+ * Interface of service provided to gadgets for resizing gadgets,
+ * setting title, etc.
+ * @constructor
+ */
+shindig.GadgetService = function() {
+};
+
+shindig.GadgetService.prototype.setHeight = function(elementId, height) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+shindig.GadgetService.prototype.setTitle = function(gadget, title) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+shindig.GadgetService.prototype.setUserPref = function(id) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+
+// ----------------
+// IfrGadgetService
+
+/**
+ * Base implementation of GadgetService.
+ * @constructor
+ */
+shindig.IfrGadgetService = function() {
+  shindig.GadgetService.call(this);
+  gadgets.rpc.register('resize_iframe', this.setHeight);
+  gadgets.rpc.register('set_pref', this.setUserPref);
+  gadgets.rpc.register('set_title', this.setTitle);
+  gadgets.rpc.register('requestNavigateTo', this.requestNavigateTo);
+  gadgets.rpc.register('requestSendMessage', this.requestSendMessage);
+};
+
+shindig.IfrGadgetService.inherits(shindig.GadgetService);
+
+shindig.IfrGadgetService.prototype.setHeight = function(height) {
+  if (height > shindig.container.maxheight_) {
+    height = shindig.container.maxheight_;
+  }
+
+  var element = document.getElementById(this.f);
+  if (element) {
+    element.style.height = height + 'px';
+  }
+};
+
+shindig.IfrGadgetService.prototype.setTitle = function(title) {
+  var element = document.getElementById(this.f + '_title');
+  if (element) {
+    element.innerHTML = title.replace(/&/g, '&amp;').replace(/</g, '&lt;');
+  }
+};
+
+/**
+ * Sets one or more user preferences
+ * @param {String} editToken
+ * @param {String} name Name of user preference.
+ * @param {String} value Value of user preference
+ * More names and values may follow.
+ */
+shindig.IfrGadgetService.prototype.setUserPref = function(editToken, name,
+    value) {
+  var id = shindig.container.gadgetService.getGadgetIdFromModuleId(this.f);
+  var gadget = shindig.container.getGadget(id);
+  for (var i = 1, j = arguments.length; i < j; i += 2) {
+    this.userPrefs[arguments[i]].value = arguments[i + 1];
+  }
+  gadget.saveUserPrefs();
+};
+
+/**
+ * Requests the container to send a specific message to the specified users.
+ * @param {Array.<String>|String} recipients An ID, array of IDs, or a group reference;
+ * the supported keys are VIEWER, OWNER, VIEWER_FRIENDS, OWNER_FRIENDS, or a
+ * single ID within one of those groups.
+ * @param {opensocial.Message} message The message to send to the specified users.
+ * @param {Function} opt_callback The function to call once the request has been
+ * processed; either this callback will be called or the gadget will be reloaded
+ * from scratch.
+ * @param {opensocial.NavigationParameters} opt_params The optional parameters
+ * indicating where to send a user when a request is made, or when a request
+ * is accepted; options are of type  NavigationParameters.DestinationType.
+ */
+shindig.IfrGadgetService.prototype.requestSendMessage = function(recipients,
+    message, opt_callback, opt_params) {
+  if (opt_callback) {
+    window.setTimeout(function() {
+      opt_callback(new opensocial.ResponseItem(
+          null, null, opensocial.ResponseItem.Error.NOT_IMPLEMENTED, null));
+    }, 0);
+  }
+};
+
+/**
+ * Navigates the page to a new url based on a gadgets requested view and
+ * parameters.
+ */
+shindig.IfrGadgetService.prototype.requestNavigateTo = function(view,
+    opt_params) {
+  var id = shindig.container.gadgetService.getGadgetIdFromModuleId(this.f);
+  var url = shindig.container.gadgetService.getUrlForView(view);
+
+  if (opt_params) {
+    var paramStr = gadgets.json.stringify(opt_params);
+    if (paramStr.length > 0) {
+      url += '&appParams=' + encodeURIComponent(paramStr);
+    }
+  }
+
+  if (url && document.location.href.indexOf(url) == -1) {
+    document.location.href = url;
+  }
+};
+
+/**
+ * This is a silly implementation that will need to be overriden by almost all
+ * real containers.
+ * TODO: Find a better default for this function
+ *
+ * @param {string} view The view name to get the url for.
+ */
+shindig.IfrGadgetService.prototype.getUrlForView = function(view) {
+  if (view === 'canvas') {
+    return '/canvas';
+  } else if (view === 'profile') {
+    return '/profile';
+  } else {
+    return null;
+  }
+};
+
+shindig.IfrGadgetService.prototype.getGadgetIdFromModuleId = function(
+    moduleId) {
+  // Quick hack to extract the gadget id from module id
+  return parseInt(moduleId.match(/_([0-9]+)$/)[1], 10);
+};
+
+
+// -------------
+// LayoutManager
+
+/**
+ * Layout manager interface.
+ * @constructor
+ */
+shindig.LayoutManager = function() {
+};
+
+/**
+ * Gets the HTML element that is the chrome of a gadget into which the content
+ * of the gadget can be rendered.
+ * @param {Object} gadget Gadget instance.
+ * @return {Object} HTML element that is the chrome for the given gadget.
+ */
+shindig.LayoutManager.prototype.getGadgetChrome = function(gadget) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+// -------------------
+// StaticLayoutManager
+
+/**
+ * Static layout manager where gadget ids have a 1:1 mapping to chrome ids.
+ * @constructor
+ */
+shindig.StaticLayoutManager = function() {
+  shindig.LayoutManager.call(this);
+};
+
+shindig.StaticLayoutManager.inherits(shindig.LayoutManager);
+
+/**
+ * Sets chrome ids, whose indexes are gadget instance ids (starting from 0).
+ * @param {Array} gadgetChromeIds Gadget id to chrome id map.
+ */
+shindig.StaticLayoutManager.prototype.setGadgetChromeIds =
+    function(gadgetChromeIds) {
+  this.gadgetChromeIds_ = gadgetChromeIds;
+};
+
+shindig.StaticLayoutManager.prototype.getGadgetChrome = function(gadget) {
+  var chromeId = this.gadgetChromeIds_[gadget.id];
+  return chromeId ? document.getElementById(chromeId) : null;
+};
+
+
+// ----------------------
+// FloatLeftLayoutManager
+
+/**
+ * FloatLeft layout manager where gadget ids have a 1:1 mapping to chrome ids.
+ * @constructor
+ * @param {String} layoutRootId Id of the element that is the parent of all
+ *     gadgets.
+ */
+shindig.FloatLeftLayoutManager = function(layoutRootId) {
+  shindig.LayoutManager.call(this);
+  this.layoutRootId_ = layoutRootId;
+};
+
+shindig.FloatLeftLayoutManager.inherits(shindig.LayoutManager);
+
+shindig.FloatLeftLayoutManager.prototype.getGadgetChrome =
+    function(gadget) {
+  var layoutRoot = document.getElementById(this.layoutRootId_);
+  if (layoutRoot) {
+    var chrome = document.createElement('div');
+    chrome.className = 'gadgets-gadget-chrome';
+    chrome.style.cssFloat = 'left';
+    layoutRoot.appendChild(chrome);
+    return chrome;
+  } else {
+    return null;
+  }
+};
+
+
+// ------
+// Gadget
+
+/**
+ * Creates a new instance of gadget.  Optional parameters are set as instance
+ * variables.
+ * @constructor
+ * @param {Object} params Parameters to set on gadget.  Common parameters:
+ *    "specUrl": URL to gadget specification
+ *    "private": Whether gadget spec is accessible only privately, which means
+ *        browser can load it but not gadget server
+ *    "spec": Gadget Specification in XML
+ *    "userPrefs": a javascript object containing attribute value pairs of user
+ *        preferences for this gadget with the value being a preference object
+ *    "viewParams": a javascript object containing attribute value pairs
+ *        for this gadgets
+ *    "secureToken": an encoded token that is passed on the URL hash
+ *    "hashData": Query-string like data that will be added to the
+ *        hash portion of the URL.
+ *    "specVersion": a hash value used to add a v= param to allow for better caching
+ *    "title": the default title to use for the title bar.
+ *    "height": height of the gadget
+ *    "width": width of the gadget
+ *    "debug": send debug=1 to the gadget server, gets us uncompressed
+ *        javascript.
+ */
+shindig.Gadget = function(params) {
+  this.userPrefs = {};
+
+  if (params) {
+    for (var name in params) if (params.hasOwnProperty(name)) {
+      this[name] = params[name];
+    }
+  }
+  if (!this.secureToken) {
+    // Assume that the default security token implementation is
+    // in use on the server.
+    this.secureToken = 'john.doe:john.doe:appid:cont:url:0:default';
+  }
+};
+
+shindig.Gadget.prototype.setServerBase = function(url) {
+  this.serverBase_ = url;
+}
+
+shindig.Gadget.prototype.getServerBase = function() {
+  return this.serverBase_;
+};
+
+
+shindig.Gadget.prototype.getUserPrefs = function() {
+  return this.userPrefs;
+};
+
+shindig.Gadget.prototype.saveUserPrefs = function() {
+  shindig.container.userPrefStore.savePrefs(this);
+};
+
+shindig.Gadget.prototype.getUserPrefValue = function(name) {
+  var pref = this.userPrefs[name];
+  return typeof(pref.value) != 'undefined' && pref.value != null ?
+      pref.value : pref['default'];
+};
+
+shindig.Gadget.prototype.render = function(chrome) {
+  if (chrome) {
+    var gadget = this;
+    this.getContent(function(content) {
+      chrome.innerHTML = content;
+      gadget.finishRender(chrome);
+    });
+  }
+};
+
+shindig.Gadget.prototype.getContent = function(continuation) {
+  shindig.callAsyncAndJoin([
+    'getTitleBarContent', 'getUserPrefsDialogContent',
+    'getMainContent'], function(results) {
+    continuation(results.join(''));
+  }, this);
+};
+
+/**
+ * Gets title bar content asynchronously or synchronously.
+ * @param {Function} continuation Function that handles title bar content as
+ *     the one and only argument.
+ */
+shindig.Gadget.prototype.getTitleBarContent = function(continuation) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+/**
+ * Gets user preferences dialog content asynchronously or synchronously.
+ * @param {Function} continuation Function that handles user preferences
+ *     content as the one and only argument.
+ */
+shindig.Gadget.prototype.getUserPrefsDialogContent = function(continuation) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+/**
+ * Gets gadget content asynchronously or synchronously.
+ * @param {Function} continuation Function that handles gadget content as
+ *     the one and only argument.
+ */
+shindig.Gadget.prototype.getMainContent = function(continuation) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+shindig.Gadget.prototype.finishRender = function(chrome) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+/*
+ * Gets additional parameters to append to the iframe url
+ * Override this method if you need any custom params.
+ */
+shindig.Gadget.prototype.getAdditionalParams = function() {
+  return '';
+};
+
+
+// ---------
+// IfrGadget
+
+shindig.BaseIfrGadget = function(opt_params) {
+  shindig.Gadget.call(this, opt_params);
+  
+  if (!this.serverBase_){
+    this.serverBase_ = '/gadgets/'; // default gadget server
+  } else if (this.serverBase_.indexOf('/gadgets')<0) {
+    this.serverBase_ += '/gadgets/';
+  }
+  this.queryIfrGadgetType_();
+};
+
+shindig.BaseIfrGadget.inherits(shindig.Gadget);
+
+shindig.BaseIfrGadget.prototype.GADGET_IFRAME_PREFIX_ = 'remote_iframe_';
+
+shindig.BaseIfrGadget.prototype.CONTAINER = 'default';
+
+shindig.BaseIfrGadget.prototype.cssClassGadget = 'gadgets-gadget';
+shindig.BaseIfrGadget.prototype.cssClassTitleBar = 'gadgets-gadget-title-bar';
+shindig.BaseIfrGadget.prototype.cssClassTitle = 'gadgets-gadget-title';
+shindig.BaseIfrGadget.prototype.cssClassTitleButtonBar =
+    'gadgets-gadget-title-button-bar';
+shindig.BaseIfrGadget.prototype.cssClassGadgetUserPrefsDialog =
+    'gadgets-gadget-user-prefs-dialog';
+shindig.BaseIfrGadget.prototype.cssClassGadgetUserPrefsDialogActionBar =
+    'gadgets-gadget-user-prefs-dialog-action-bar';
+shindig.BaseIfrGadget.prototype.cssClassTitleButton = 'gadgets-gadget-title-button';
+shindig.BaseIfrGadget.prototype.cssClassGadgetContent = 'gadgets-gadget-content';
+shindig.BaseIfrGadget.prototype.rpcToken = (0x7FFFFFFF * Math.random()) | 0;
+shindig.BaseIfrGadget.prototype.rpcRelay = '../container/rpc_relay.html';
+
+shindig.BaseIfrGadget.prototype.getTitleBarContent = function(continuation) {
+  var settingsButton = this.hasViewablePrefs_() ?
+      '<a href="#" onclick="shindig.container.getGadget(' + this.id +
+          ').handleOpenUserPrefsDialog();return false;" class="' + this.cssClassTitleButton +
+          '">settings</a> '
+      : '';
+  continuation('<div id="' + this.cssClassTitleBar + '-' + this.id +
+      '" class="' + this.cssClassTitleBar + '"><span id="' +
+      this.getIframeId() + '_title" class="' +
+      this.cssClassTitle + '">' + (this.title ? this.title : 'Title') + '</span> | <span class="' +
+      this.cssClassTitleButtonBar + '">' + settingsButton +
+      '<a href="#" onclick="shindig.container.getGadget(' + this.id +
+      ').handleToggle();return false;" class="' + this.cssClassTitleButton +
+      '">toggle</a></span></div>');
+};
+
+shindig.BaseIfrGadget.prototype.getUserPrefsDialogContent = function(continuation) {
+  continuation('<div id="' + this.getUserPrefsDialogId() + '" class="' +
+      this.cssClassGadgetUserPrefsDialog + '"></div>');
+};
+
+
+
+shindig.BaseIfrGadget.prototype.getMainContent = function(continuation) {
+  // proper sub-class has not been mixed-in yet
+  var gadget = this;
+  window.setTimeout(function() {
+    gadget.getMainContent(continuation);
+  }, 0);
+};
+
+shindig.BaseIfrGadget.prototype.getIframeId = function() {
+  return this.GADGET_IFRAME_PREFIX_ + this.id;
+};
+
+shindig.BaseIfrGadget.prototype.getUserPrefsDialogId = function() {
+  return this.getIframeId() + '_userPrefsDialog';
+};
+
+shindig.BaseIfrGadget.prototype.getUserPrefsParams = function() {
+  var params = '';
+  for (var name in this.getUserPrefs()) {
+    params += '&up_' + encodeURIComponent(name) + '=' +
+        encodeURIComponent(this.getUserPrefValue(name));
+  }
+  return params;
+};
+
+shindig.BaseIfrGadget.prototype.handleToggle = function() {
+  var gadgetIframe = document.getElementById(this.getIframeId());
+  if (gadgetIframe) {
+    var gadgetContent = gadgetIframe.parentNode;
+    var display = gadgetContent.style.display;
+    gadgetContent.style.display = display ? '' : 'none';
+  }
+};
+
+
+shindig.BaseIfrGadget.prototype.hasViewablePrefs_ = function() {
+  for (var name in this.getUserPrefs()) {
+    var pref = this.userPrefs[name];
+    if (pref.type != 'hidden') {
+      return true;
+    }
+  }
+  return false;
+};
+
+
+shindig.BaseIfrGadget.prototype.handleOpenUserPrefsDialog = function() {
+  if (this.userPrefsDialogContentLoaded) {
+    this.showUserPrefsDialog();
+  } else {
+    var gadget = this;
+    var igCallbackName = 'ig_callback_' + this.id;
+    window[igCallbackName] = function(userPrefsDialogContent) {
+      gadget.userPrefsDialogContentLoaded = true;
+      gadget.buildUserPrefsDialog(userPrefsDialogContent);
+      gadget.showUserPrefsDialog();
+    };
+
+    var script = document.createElement('script');
+    script.src = 'http://www.gmodules.com/ig/gadgetsettings?mid=' + this.id +
+        '&output=js' + this.getUserPrefsParams() + '&url=' + this.specUrl;
+    document.body.appendChild(script);
+  }
+};
+
+shindig.BaseIfrGadget.prototype.buildUserPrefsDialog = function(content) {
+  var userPrefsDialog = document.getElementById(this.getUserPrefsDialogId());
+  userPrefsDialog.innerHTML = content +
+      '<div class="' + this.cssClassGadgetUserPrefsDialogActionBar +
+      '"><input type="button" value="Save" onclick="shindig.container.getGadget(' +
+      this.id + ').handleSaveUserPrefs()"> <input type="button" value="Cancel" onclick="shindig.container.getGadget(' +
+      this.id + ').handleCancelUserPrefs()"></div>';
+  userPrefsDialog.childNodes[0].style.display = '';
+};
+
+shindig.BaseIfrGadget.prototype.showUserPrefsDialog = function(opt_show) {
+  var userPrefsDialog = document.getElementById(this.getUserPrefsDialogId());
+  userPrefsDialog.style.display = (opt_show || typeof opt_show == 'undefined')
+      ? '' : 'none';
+};
+
+shindig.BaseIfrGadget.prototype.hideUserPrefsDialog = function() {
+  this.showUserPrefsDialog(false);
+};
+
+shindig.BaseIfrGadget.prototype.handleSaveUserPrefs = function() {
+  this.hideUserPrefsDialog();
+
+  var numFields = document.getElementById('m_' + this.id +
+      '_numfields').value;
+  for (var i = 0; i < numFields; i++) {
+    var input = document.getElementById('m_' + this.id + '_' + i);
+    var userPrefNamePrefix = 'm_' + this.id + '_up_';
+    var userPrefName = input.name.substring(userPrefNamePrefix.length);
+    var userPrefValue = input.value;
+    this.userPrefs[userPrefName].value = userPrefValue;
+  }
+
+  this.saveUserPrefs();
+  this.refresh();
+};
+
+shindig.BaseIfrGadget.prototype.handleCancelUserPrefs = function() {
+  this.hideUserPrefsDialog();
+};
+
+shindig.BaseIfrGadget.prototype.refresh = function() {
+  var iframeId = this.getIframeId();
+  // we have to add a random value to the iframe url because otherwise
+  // some browsers would not refresh the since the iframe src would remain the
+  // same
+  document.getElementById(iframeId).src = this.getIframeUrl(Math.random());
+};
+
+shindig.BaseIfrGadget.prototype.queryIfrGadgetType_ = function() {
+  // Get the gadget metadata and check if the gadget requires the 'pubsub-2'
+  // feature.  If so, then we use OpenAjax Hub in order to create and manage
+  // the iframe.  Otherwise, we create the iframe ourselves.
+  var request = {
+    context: {
+      country: 'default',
+      language: 'default',
+      view: 'default',
+      container: 'default'
+    },
+    gadgets: [{
+      url: this.specUrl,
+      moduleId: 1
+    }]
+  };
+
+  var makeRequestParams = {
+    'CONTENT_TYPE' : 'JSON',
+    'METHOD' : 'POST',
+    'POST_DATA' : gadgets.json.stringify(request)
+  };
+
+  var url = this.serverBase_ + 'metadata?st=' + this.secureToken;
+
+  gadgets.io.makeNonProxiedRequest(url,
+      handleJSONResponse,
+      makeRequestParams,
+      {'Content-Type':'application/javascript'}
+  );
+
+  var gadget = this;
+  function handleJSONResponse(obj) {
+    var requiresPubSub2 = false;
+    var arr = obj.data.gadgets[0].features;
+    for (var i = 0; i < arr.length; i++) {
+      if (arr[i] === 'pubsub-2') {
+        requiresPubSub2 = true;
+        break;
+      }
+    }
+    var subClass = requiresPubSub2 ? shindig.OAAIfrGadget : shindig.IfrGadget;
+    for (var name in subClass) if (subClass.hasOwnProperty(name)) {
+      gadget[name] = subClass[name];
+    }
+  }
+};
+
+// ---------
+// IfrGadget
+
+shindig.IfrGadget = {
+  getMainContent: function(continuation) {
+    var iframeId = this.getIframeId();
+    gadgets.rpc.setRelayUrl(iframeId, this.serverBase_ + this.rpcRelay);
+    gadgets.rpc.setAuthToken(iframeId, this.rpcToken);
+    continuation('<div class="' + this.cssClassGadgetContent + '"><iframe id="' +
+        iframeId + '" name="' + iframeId + '" class="' + this.cssClassGadget +
+        '" src="about:blank' +
+        '" frameborder="no" scrolling="no"' +
+        (this.height ? ' height="' + this.height + '"' : '') +
+        (this.width ? ' width="' + this.width + '"' : '') +
+        '></iframe></div>');
+  },
+
+  finishRender: function(chrome) {
+    window.frames[this.getIframeId()].location = this.getIframeUrl();
+  },
+
+  getIframeUrl: function(random) {
+    return this.serverBase_ + 'ifr?' +
+        'container=' + this.CONTAINER +
+        '&mid=' + this.id +
+        '&nocache=' + shindig.container.nocache_ +
+        '&country=' + shindig.container.country_ +
+        '&lang=' + shindig.container.language_ +
+        '&view=' + shindig.container.view_ +
+        (this.specVersion ? '&v=' + this.specVersion : '') +
+        (shindig.container.parentUrl_ ? '&parent=' + encodeURIComponent(shindig.container.parentUrl_) : '') +
+        (this.debug ? '&debug=1' : '') +
+        this.getAdditionalParams() +
+        this.getUserPrefsParams() +
+        (this.secureToken ? '&st=' + this.secureToken : '') +
+        '&url=' + encodeURIComponent(this.specUrl) +
+        (this.viewParams ?
+            '&view-params=' + encodeURIComponent(gadgets.json.stringify(this.viewParams)) : '') +
+        (random ? '&r=' + random : '') +
+        '#rpctoken=' + this.rpcToken +
+        (this.hashData ? '&' + this.hashData : '');
+  }
+};
+
+
+// ---------
+// OAAIfrGadget
+
+shindig.OAAIfrGadget = {
+  getMainContent: function(continuation) {
+    continuation('<div id="' + this.cssClassGadgetContent + '-' + this.id +
+        '" class="' + this.cssClassGadgetContent + '"></div>');
+  },
+
+  finishRender: function(chrome) {
+    var iframeAttrs = {
+      className: this.cssClassGadget,
+      frameborder: 'no',
+      scrolling: 'no'
+    };
+    if (this.height) {
+      iframeAttrs.height = this.height;
+    }
+    if (this.width) {
+      iframeAttrs.width = this.width;
+    }
+
+    new OpenAjax.hub.IframeContainer(
+        gadgets.pubsub2router.hub,
+        this.getIframeId(),
+        {
+          Container: {
+            onSecurityAlert: function(source, alertType) {
+              gadgets.error('Security error for container ' + source.getClientID() + ' : ' + alertType);
+              source.getIframe().src = 'about:blank';
+              // for debugging
+              //          },
+              //          onConnect: function( container ) {
+              //            gadgets.log("++ connected: " + container.getClientID());
+            }
+          },
+          IframeContainer: {
+            parent: document.getElementById(this.cssClassGadgetContent + '-' + this.id),
+            uri: this.getIframeUrl(),
+            tunnelURI: shindig.uri(this.serverBase_ + this.rpcRelay).resolve(shindig.uri(window.location.href)),
+            iframeAttrs: iframeAttrs
+          }
+        }
+    );
+  },
+
+  getIframeUrl: function(random) {
+    return this.serverBase_ + 'ifr?' +
+        'container=' + this.CONTAINER +
+        '&mid=' + this.id +
+        '&nocache=' + shindig.container.nocache_ +
+        '&country=' + shindig.container.country_ +
+        '&lang=' + shindig.container.language_ +
+        '&view=' + shindig.container.view_ +
+        (this.specVersion ? '&v=' + this.specVersion : '') +
+        //      (shindig.container.parentUrl_ ? '&parent=' + encodeURIComponent(shindig.container.parentUrl_) : '') +
+        (this.debug ? '&debug=1' : '') +
+        this.getAdditionalParams() +
+        this.getUserPrefsParams() +
+        (this.secureToken ? '&st=' + this.secureToken : '') +
+        '&url=' + encodeURIComponent(this.specUrl) +
+        //      '#rpctoken=' + this.rpcToken +
+        (this.viewParams ?
+            '&view-params=' + encodeURIComponent(gadgets.json.stringify(this.viewParams)) : '') +
+        (random ? '&r=' + random : '') +
+        //      (this.hashData ? '&' + this.hashData : '');
+        (this.hashData ? '#' + this.hashData : '');
+  }
+};
+
+
+// ---------
+// Container
+
+/**
+ * Container interface.
+ * @constructor
+ */
+shindig.Container = function() {
+  this.gadgets_ = {};
+  this.parentUrl_ = document.location.href + '://' + document.location.host;
+  this.country_ = 'ALL';
+  this.language_ = 'ALL';
+  this.view_ = 'default';
+  this.nocache_ = 1;
+
+  // signed max int
+  this.maxheight_ = 0x7FFFFFFF;
+};
+
+shindig.Container.inherits(shindig.Extensible);
+
+/**
+ * Known dependencies:
+ *     gadgetClass: constructor to create a new gadget instance
+ *     userPrefStore: instance of a subclass of shindig.UserPrefStore
+ *     gadgetService: instance of a subclass of shindig.GadgetService
+ *     layoutManager: instance of a subclass of shindig.LayoutManager
+ */
+
+shindig.Container.prototype.gadgetClass = shindig.Gadget;
+
+shindig.Container.prototype.userPrefStore = new shindig.DefaultUserPrefStore();
+
+shindig.Container.prototype.gadgetService = new shindig.GadgetService();
+
+shindig.Container.prototype.layoutManager =
+    new shindig.StaticLayoutManager();
+
+shindig.Container.prototype.setParentUrl = function(url) {
+  this.parentUrl_ = url;
+};
+
+shindig.Container.prototype.setCountry = function(country) {
+  this.country_ = country;
+};
+
+shindig.Container.prototype.setNoCache = function(nocache) {
+  this.nocache_ = nocache;
+};
+
+shindig.Container.prototype.setLanguage = function(language) {
+  this.language_ = language;
+};
+
+shindig.Container.prototype.setView = function(view) {
+  this.view_ = view;
+};
+
+shindig.Container.prototype.setMaxHeight = function(maxheight) {
+  this.maxheight_ = maxheight;
+};
+
+shindig.Container.prototype.getGadgetKey_ = function(instanceId) {
+  return 'gadget_' + instanceId;
+};
+
+shindig.Container.prototype.getGadget = function(instanceId) {
+  return this.gadgets_[this.getGadgetKey_(instanceId)];
+};
+
+shindig.Container.prototype.createGadget = function(opt_params) {
+  return new this.gadgetClass(opt_params);
+};
+
+shindig.Container.prototype.addGadget = function(gadget) {
+  gadget.id = this.getNextGadgetInstanceId();
+  this.gadgets_[this.getGadgetKey_(gadget.id)] = gadget;
+};
+
+shindig.Container.prototype.addGadgets = function(gadgets) {
+  for (var i = 0; i < gadgets.length; i++) {
+    this.addGadget(gadgets[i]);
+  }
+};
+
+/**
+ * Renders all gadgets in the container.
+ */
+shindig.Container.prototype.renderGadgets = function() {
+  for (var key in this.gadgets_) {
+    this.renderGadget(this.gadgets_[key]);
+  }
+};
+
+/**
+ * Renders a gadget.  Gadgets are rendered inside their chrome element.
+ * @param {Object} gadget Gadget object.
+ */
+shindig.Container.prototype.renderGadget = function(gadget) {
+  throw Error(shindig.errors.SUBCLASS_RESPONSIBILITY);
+};
+
+shindig.Container.prototype.nextGadgetInstanceId_ = 0;
+
+shindig.Container.prototype.getNextGadgetInstanceId = function() {
+  return this.nextGadgetInstanceId_++;
+};
+
+/**
+ * Refresh all the gadgets in the container.
+ */
+shindig.Container.prototype.refreshGadgets = function() {
+  for (var key in this.gadgets_) {
+    this.gadgets_[key].refresh();
+  }
+};
+
+
+// ------------
+// IfrContainer
+
+/**
+ * Container that renders gadget using ifr.
+ * @constructor
+ */
+shindig.IfrContainer = function() {
+  shindig.Container.call(this);
+};
+
+shindig.IfrContainer.inherits(shindig.Container);
+
+shindig.IfrContainer.prototype.gadgetClass = shindig.BaseIfrGadget;
+
+shindig.IfrContainer.prototype.gadgetService = new shindig.IfrGadgetService();
+
+shindig.IfrContainer.prototype.setParentUrl = function(url) {
+  if (!url.match(/^http[s]?:\/\//)) {
+    url = document.location.href.match(/^[^?#]+\//)[0] + url;
+  }
+
+  this.parentUrl_ = url;
+};
+
+/**
+ * Renders a gadget using ifr.
+ * @param {Object} gadget Gadget object.
+ */
+shindig.IfrContainer.prototype.renderGadget = function(gadget) {
+  var chrome = this.layoutManager.getGadgetChrome(gadget);
+  gadget.render(chrome);
+};
+
+function init(config) {
+    var sbase = config['shindig-container'];
+    shindig.Gadget.prototype.setServerBase(sbase.serverBase);
+}
+
+// We do run this in the container mode in the new common container
+if (gadgets.config) {
+  gadgets.config.register('shindig-container', null, init);
+};
+
+/**
+ * Default container.
+ */
+shindig.container = new shindig.IfrContainer();
diff --git a/trunk/features/src/main/javascript/features/shindig.container/util.js b/trunk/features/src/main/javascript/features/shindig.container/util.js
new file mode 100644
index 0000000..f1cdac8
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.container/util.js
@@ -0,0 +1,29 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Utility functions for the Open Gadget Container.
+ */
+
+Function.prototype.inherits = function(parentCtor) {
+  function tempCtor() {};
+  tempCtor.prototype = parentCtor.prototype;
+  this.superClass_ = parentCtor.prototype;
+  this.prototype = new tempCtor();
+  this.prototype.constructor = this;
+};
diff --git a/trunk/features/src/main/javascript/features/shindig.random/feature.xml b/trunk/features/src/main/javascript/features/shindig.random/feature.xml
new file mode 100644
index 0000000..066a411
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.random/feature.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+<!--
+A JavaScript "more-secure-random" implementation.
+-->
+  <name>shindig.random</name>
+  <dependency>globals</dependency>
+  <dependency>shindig.sha1</dependency>
+  <all>
+    <api>
+      <exports type="js">shindig.random</exports>
+    </api>
+    <script src="random.js"/>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.random/random.js b/trunk/features/src/main/javascript/features/shindig.random/random.js
new file mode 100644
index 0000000..2dc16d6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.random/random.js
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/*
+ * @fileoverview
+ * This code implements a safer random() method that is seeded from
+ * screen width/height and (presumably random/unguessable) mouse
+ * movement, in an effort to create a better seed for random().
+ *
+ * Its aim is to solve the problem of gadgets that are relying on
+ * secret RPC tokens to validate identity.
+ *
+ * Another possible solution is to use XHR to get a real random number
+ * from the server, though this is not feasible or may be too slow in
+ * some circumstances.
+ */
+shindig.random = (function() {
+  var oth = Math.random();
+  var hex = '0123456789ABCDEF';
+  var start = 1;
+  var m = ((screen.width * screen.width) + screen.height) * 1e6;
+  var sliceFn = [].slice;
+
+  // TODO: consider using addEventListener
+  var orig_onmousemove = window.onmousemove || function() { };
+
+  window.onmousemove = function(e) {
+    if (window.event) {
+      e = window.event;
+    }
+
+    var ac = (e.screenX + e.clientX) << 16;
+    ac += (e.screenY + e.clientY);
+    ac *= new Date().getTime() % 1e6;
+    start = (start * ac) % m;
+    return orig_onmousemove.apply(window, sliceFn.call(arguments, 0));
+  };
+
+  function sha1(str) {
+    var sha1 = shindig.sha1();
+    sha1.update(str);
+    return sha1.digestString();
+  }
+
+  var seed = sha1(
+      document.cookie + '|' + document.location + '|' + (new Date()).getTime() + '|' + oth);
+
+  return function() {
+    var rnd = start;
+    rnd += parseInt(seed.substr(0, 20), 16);
+    seed = sha1(seed);
+    return rnd / (m + Math.pow(16, 20));
+  }
+})();
diff --git a/trunk/features/src/main/javascript/features/shindig.sha1/feature.xml b/trunk/features/src/main/javascript/features/shindig.sha1/feature.xml
new file mode 100644
index 0000000..18adf07
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.sha1/feature.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+<!--
+A JavaScript sha1 imlementation.
+-->
+  <name>shindig.sha1</name>
+  <dependency>globals</dependency>
+  <all>
+    <api>
+      <exports type="js">shindig.sha1.reset</exports>
+      <exports type="js">shindig.sha1.update</exports>
+      <exports type="js">shindig.sha1.digest</exports>
+      <exports type="js">shindig.sha1.digestString</exports>
+    </api>
+    <script src="sha1.js"/>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.sha1/sha1.js b/trunk/features/src/main/javascript/features/shindig.sha1/sha1.js
new file mode 100644
index 0000000..a28b2ba
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.sha1/sha1.js
@@ -0,0 +1,279 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+// File copied directly from Closure Library (http://code.google.com/p/closure-library)
+// Imported into Shindig w/ slight namespacing modifications and change from
+// prototype-style to closure (ironically) style JS objects.
+
+/**
+ * @fileoverview SHA-1 cryptographic hash.
+ * Variable names follow the notation in FIPS PUB 180-3:
+ * http://csrc.nist.gov/publications/fips/fips180-3/fips180-3_final.pdf.
+ *
+ * Usage:
+ *   var sha1 = shindig.sha1();
+ *   sha1.update(bytes);
+ *   var hash = sha1.digest();
+ */
+
+/**
+ * SHA-1 cryptographic hash constructor.
+ *
+ * The properties declared here are discussed in the above algorithm document.
+ * @constructor
+ */
+shindig.sha1 = (function() {
+  var hex = '0123456789ABCDEF';
+
+  /**
+   * Holds the previous values of accumulated variables a-e in the compress_
+   * function.
+   * @type {Array.<number>}
+   * @private
+   */
+  var chain_ = [];
+
+  /**
+   * A buffer holding the partially computed hash result.
+   * @type {Array.<number>}
+   * @private
+   */
+  var buf_ = [];
+
+  /**
+   * An array of 80 bytes, each a part of the message to be hashed.  Referred to
+   * as the message schedule in the docs.
+   * @type {Array.<number>}
+   * @private
+   */
+  var W_ = [];
+
+  /**
+   * Contains data needed to pad messages less than 64 bytes.
+   * @type {Array.<number>}
+   * @private
+   */
+  var pad_ = [];
+
+  pad_[0] = 128;
+  for (var i = 1; i < 64; ++i) {
+    pad_[i] = 0;
+  }
+
+  /**
+   * @type {number}
+   * @private
+   */
+  var inbuf_;
+
+  /**
+   * @type {number}
+   * @private
+   */
+  var total_;
+
+  /**
+   * Resets the internal accumulator.
+   */
+  function reset() {
+    chain_[0] = 0x67452301;
+    chain_[1] = 0xefcdab89;
+    chain_[2] = 0x98badcfe;
+    chain_[3] = 0x10325476;
+    chain_[4] = 0xc3d2e1f0;
+
+    inbuf_ = 0;
+    total_ = 0;
+  }
+
+  /**
+   * Internal helper performing 32 bit left rotate.
+   * @param {number} w 32-bit integer to rotate.
+   * @param {number} r Bits to rotate left by.
+   * @return {number} w rotated left by r bits.
+   * @private
+   */
+  function rotl_(w, r) {
+    return ((w << r) | (w >>> (32 - r))) & 0xffffffff;
+  }
+
+  /**
+   * Internal compress helper function.
+   * @param {Array} buf containing block to compress.
+   * @private
+   */
+  function compress_(buf) {
+    var W = W_;
+
+    // get 16 big endian words
+    for (var i = 0; i < 64; i += 4) {
+      var w = (buf[i] << 24) |
+              (buf[i + 1] << 16) |
+              (buf[i + 2] << 8) |
+              (buf[i + 3]);
+      W[i / 4] = w;
+    }
+
+    // expand to 80 words
+    for (var i = 16; i < 80; i++) {
+      W[i] = rotl_(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
+    }
+
+    var a = chain_[0];
+    var b = chain_[1];
+    var c = chain_[2];
+    var d = chain_[3];
+    var e = chain_[4];
+    var f, k;
+
+    for (var i = 0; i < 80; i++) {
+      if (i < 40) {
+        if (i < 20) {
+          f = d ^ (b & (c ^ d));
+          k = 0x5a827999;
+        } else {
+          f = b ^ c ^ d;
+          k = 0x6ed9eba1;
+        }
+      } else {
+        if (i < 60) {
+          f = (b & c) | (d & (b | c));
+          k = 0x8f1bbcdc;
+        } else {
+          f = b ^ c ^ d;
+          k = 0xca62c1d6;
+        }
+      }
+
+      var t = (rotl_(a, 5) + f + e + k + W[i]) & 0xffffffff;
+      e = d;
+      d = c;
+      c = rotl_(b, 30);
+      b = a;
+      a = t;
+    }
+
+    chain_[0] = (chain_[0] + a) & 0xffffffff;
+    chain_[1] = (chain_[1] + b) & 0xffffffff;
+    chain_[2] = (chain_[2] + c) & 0xffffffff;
+    chain_[3] = (chain_[3] + d) & 0xffffffff;
+    chain_[4] = (chain_[4] + e) & 0xffffffff;
+  }
+
+  /**
+   * Adds a byte array to internal accumulator.
+   * @param {Array.<number>} bytes to add to digest.
+   * @param {number=} opt_length is # of bytes to compress.
+   */
+  function update(bytes, opt_length) {
+    if (typeof(bytes) === 'string') {
+      // convert Unicode to UTF-8 bytes
+      bytes = unescape(encodeURIComponent(bytes));
+      var byteArray = [];
+      for (var i = 0, maxi = bytes.length; i < maxi; ++i) {
+        byteArray.push(bytes.charCodeAt(i));
+      }
+      bytes = byteArray;
+    }
+
+    if (!opt_length) {
+      opt_length = bytes.length;
+    }
+
+    var n = 0;
+
+    // Optimize for 64 byte chunks at 64 byte boundaries.
+    if (inbuf_ == 0) {
+      while (n + 64 < opt_length) {
+        compress_(bytes.slice(n, n + 64));
+        n += 64;
+        total_ += 64;
+      }
+    }
+
+    while (n < opt_length) {
+      buf_[inbuf_++] = bytes[n++];
+      total_++;
+
+      if (inbuf_ == 64) {
+        inbuf_ = 0;
+        compress_(buf_);
+
+        // Pick up 64 byte chunks.
+        while (n + 64 < opt_length) {
+          compress_(bytes.slice(n, n + 64));
+          n += 64;
+          total_ += 64;
+        }
+      }
+    }
+  }
+
+  /**
+   * @return {Array} byte[20] containing finalized hash.
+   */
+  function digest() {
+    var digest = [];
+    var totalBits = total_ * 8;
+
+    // Add pad 0x80 0x00*.
+    if (inbuf_ < 56) {
+      update(pad_, 56 - inbuf_);
+    } else {
+      update(pad_, 64 - (inbuf_ - 56));
+    }
+
+    // Add # bits.
+    for (var i = 63; i >= 56; i--) {
+      buf_[i] = totalBits & 255;
+      totalBits >>>= 8;
+    }
+
+    compress_(buf_);
+
+    var n = 0;
+    for (var i = 0; i < 5; i++) {
+      for (var j = 24; j >= 0; j -= 8) {
+        digest[n++] = (chain_[i] >> j) & 255;
+      }
+    }
+
+    return digest;
+  }
+
+  /**
+   * @return {String} Hex-encoded string containing finalized hash.
+   */
+  function digestString() {
+    var arr = digest();
+    var hash = '';
+    for (var i = 0; i < arr.length; i++) {
+      hash += hex.charAt(Math.floor(arr[i] / 16)) + hex.charAt(arr[i] % 16);
+    }
+    return hash;
+  }
+
+  reset();
+
+  return {
+    reset: reset,
+    update: update,
+    digest: digest,
+    digestString: digestString
+  };
+});
diff --git a/trunk/features/src/main/javascript/features/shindig.uri.ext/feature.xml b/trunk/features/src/main/javascript/features/shindig.uri.ext/feature.xml
new file mode 100644
index 0000000..e2a739a
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.uri.ext/feature.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <!--
+  Augmented version of shindig.uri with various non-core, but useful, helper methods.
+  -->
+  <name>shindig.uri.ext</name>
+  <dependency>globals</dependency>
+  <dependency>shindig.uri</dependency>
+  <all>
+    <script src="util.js"/>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.uri.ext/util.js b/trunk/features/src/main/javascript/features/shindig.uri.ext/util.js
new file mode 100644
index 0000000..0e5c4b3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.uri.ext/util.js
@@ -0,0 +1,75 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Augments shindig.uri class with various useful helper methods.
+ */
+
+shindig._uri = shindig.uri;
+shindig.uri = (function() {
+  var oldCtor = shindig._uri;
+  shindig._uri = null;
+
+  /**
+   * Checks that a Uri has the same origin as this Uri.
+   *
+   * Two Uris have the same origin if they point to the same schema, server
+   * and port.
+   *
+   * @param {Url} other The Uri to compare to this Uri.
+   * @return {boolean} Whether the Uris have the same origin.
+   */
+  function hasSameOrigin(self, other) {
+    return self.getOrigin() == other.getOrigin();
+  }
+
+  /**
+   * Fully qualifies this Uri if it is relative, using a given base Uri.
+   *
+   * @param {Uri} self The base Uri.
+   * @param {Uri} base The Uri to resolve.
+   */
+  function resolve(self, base) {
+    if (self.getSchema() == '') {
+      self.setSchema(base.getSchema());
+    }
+    if (self.getAuthority() == '') {
+      self.setAuthority(base.getAuthority());
+    }
+    var selfPath = self.getPath();
+    if (selfPath == '' || selfPath.charAt(0) != '/') {
+      var basePath = base.getPath();
+      var lastSlash = basePath.lastIndexOf('/');
+      if (lastSlash != -1) {
+        basePath = basePath.substring(0, lastSlash + 1);
+      }
+      self.setPath(base.getPath() + selfPath);
+    }
+  }
+
+  return function(opt_in) {
+    var self = oldCtor(opt_in);
+    self.hasSameOrigin = function(other) {
+      return hasSameOrigin(self, other);
+    };
+    self.resolve = function(other) {
+      return resolve(self, other);
+    };
+    return self;
+  };
+})();
diff --git a/trunk/features/src/main/javascript/features/shindig.uri/feature.xml b/trunk/features/src/main/javascript/features/shindig.uri/feature.xml
new file mode 100644
index 0000000..c6d7f6c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.uri/feature.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <!--
+  A Pure-JS Uri implementation.
+  -->
+  <name>shindig.uri</name>
+  <dependency>globals</dependency>
+  <all>
+    <script src="uri.js"/>
+    <api>
+      <exports type="js">shindig.uri.getSchema</exports>
+      <exports type="js">shindig.uri.getAuthority</exports>
+      <exports type="js">shindig.uri.getOrigin</exports>
+      <exports type="js">shindig.uri.getPath</exports>
+      <exports type="js">shindig.uri.getQuery</exports>
+      <exports type="js">shindig.uri.getFragment</exports>
+      <exports type="js">shindig.uri.getQP</exports>
+      <exports type="js">shindig.uri.getFP</exports>
+      <exports type="js">shindig.uri.setSchema</exports>
+      <exports type="js">shindig.uri.setAuthority</exports>
+      <exports type="js">shindig.uri.setPath</exports>
+      <exports type="js">shindig.uri.setQuery</exports>
+      <exports type="js">shindig.uri.setFragment</exports>
+      <exports type="js">shindig.uri.setQP</exports>
+      <exports type="js">shindig.uri.setFP</exports>
+      <exports type="js">shindig.uri.setExistingP</exports>
+      <exports type="js">shindig.uri.toString</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.uri/uri.js b/trunk/features/src/main/javascript/features/shindig.uri/uri.js
new file mode 100644
index 0000000..7563b66
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.uri/uri.js
@@ -0,0 +1,257 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Pure JS code for processing Uris.
+ *
+ * Unlike Java Shindig and other code, these Uris are mutable. While
+ * this introduces some challenges, ultimately the confusion introduced
+ * by passing around a Uri versus a UriBuilder in an untyped language
+ * is too great.
+ *
+ * The class only implements core methods associated with Uris -
+ * essentially the minimum required for various helper routines. Lazy evalution
+ * of query and fragment params is chosen to avoid undue performance hit.
+ * Further, only set operations are provided for query/fragment params,
+ * in order to keep the API relatively small, yet sufficiently flexible. Values set to
+ * null are equivalent to being removed, for instance.
+ *
+ * Limitations include, but are not limited to:
+ * + Multiple params with the same key not supported via set APIs.
+ * + Full RPC-compliant parsing not supported. A "highly useful" subset is impl'd.
+ * + Helper methods are all provided in the shindig.uri.full feature.
+ * + Query and fragment do not contain their preceding ? and # chars.
+ *
+ * Example usage:
+ * var uri = shindig.uri("http://www.apache.org");
+ * uri.setPath("random.xml");
+ * alert(uri.toString());  // Emits "http://www.apache.org/random.xml"
+ * var other =  // resolve() provided in shindig.uri.full
+ *     shindig.uri("http://www.other.com/foo").resolve("/bar").setQP({ hi: "bye" });
+ * alert(other);  // Emits "http://other.com/bar?hi=bye"
+ */
+shindig.uri = (function() {
+  var PARSE_REGEX = new RegExp('^(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\\?([^#]*))?(?:#(.*))?');
+
+  return function(opt_in) {
+    var schema_ = '';
+    var authority_ = '';
+    var path_ = '';
+    var query_ = '';
+    var qparms_ = null;
+    var fragment_ = '';
+    var fparms_ = null;
+    var unesc = window.decodeURIComponent ? decodeURIComponent : unescape;
+    var esc = window.encodeURIComponent ? encodeURIComponent : escape;
+    var bundle = null;
+
+    function parseFrom(url) {
+      if (url.match(PARSE_REGEX) === null) {
+        throw 'Malformed URL: ' + url;
+      }
+      schema_ = RegExp.$1;
+      authority_ = RegExp.$2;
+      path_ = RegExp.$3;
+      query_ = RegExp.$4;
+      fragment_ = RegExp.$5;
+    }
+
+    function serializeParams(params) {
+      var str = [];
+      for (var i = 0, j = params.length; i < j; ++i) {
+        var key = params[i][0];
+        var val = params[i][1];
+        if (typeof val == 'undefined') {
+          continue;
+        }
+        str.push(esc(key) + (val !== null ? '=' + esc(val) : ''));
+      }
+      return str.join('&');
+    }
+
+    function getQuery() {
+      if (qparms_) {
+        query_ = serializeParams(qparms_);
+        qparms_ = null;
+      }
+      return query_;
+    }
+
+    function getFragment() {
+      if (fparms_) {
+        fragment_ = serializeParams(fparms_);
+        fparms_ = null;
+      }
+      return fragment_;
+    }
+
+    function getQP(key) {
+      qparms_ = qparms_ || parseParams(query_);
+      return getParam(qparms_, key);
+    }
+
+    function getFP(key) {
+      fparms_ = fparms_ || parseParams(fragment_);
+      return getParam(fparms_, key);
+    }
+
+    function setQP(argOne, argTwo) {
+      qparms_ = setParams(qparms_ || parseParams(query_), argOne, argTwo);
+      return bundle;
+    }
+
+    function setFP(argOne, argTwo) {
+      fparms_ = setParams(fparms_ || parseParams(fragment_), argOne, argTwo);
+      return bundle;
+    }
+
+    function getOrigin() {
+      return [
+        schema_,
+        schema_ !== '' ? ':' : '',
+        authority_ !== '' ? '//' : '',
+        authority_
+      ].join('');
+    }
+
+    /**
+     * Returns a readable representation of the URL.
+     *
+     * @return {string} A readable URL.
+     */
+    function toString() {
+      var query = getQuery();
+      var fragment = getFragment();
+      return [
+        getOrigin(),
+        path_,
+        query !== '' ? '?' : '',
+        query,
+        fragment !== '' ? '#' : '',
+        fragment
+      ].join('');
+    }
+
+    function parseParams(str) {
+      var params = [];
+      // When the string is empty, split returns an array containing one empty string,
+      // rather than an empty array.
+      if(str === "") {
+        return params;
+      }
+
+      var pairs = str.split('&');
+      for (var i = 0, j = pairs.length; i < j; ++i) {
+        var kv = pairs[i].split('=');
+        var key = kv.shift();
+        var value = null;
+        if (kv.length > 0) {
+          value = kv.join('').replace(/\+/g, ' ');
+        }
+        params.push([key, value != null ? unesc(value) : null]);
+      }
+      return params;
+    }
+
+    function getParam(pmap, key) {
+      for (var i = 0, j = pmap.length; i < j; ++i) {
+        if (pmap[i][0] == key) {
+          return pmap[i][1];
+        }
+      }
+      // undefined = no key set
+      // vs. null = no value set and "" = value is empty
+      return undefined;
+    }
+
+    function setParams(pset, argOne, argTwo) {
+      // Assume by default that we're setting by map (full replace).
+      var newParams = argOne;
+      if (typeof argOne === 'string') {
+        // N/V set (single param override)
+        newParams = {};
+        newParams[argOne] = argTwo;
+      }
+      for (var key in newParams) {
+        var found = false;
+        for (var i = 0, j = pset.length; !found && i < j; ++i) {
+          if (pset[i][0] == key) {
+            pset[i][1] = newParams[key];
+            found = true;
+          }
+        }
+        if (!found) {
+          pset.push([key, newParams[key]]);
+        }
+      }
+      return pset;
+    }
+
+    function stripPrefix(str, pfx) {
+      str = str || '';
+      if (str[0] === pfx) {
+        str = str.substr(pfx.length);
+      }
+      return str;
+    }
+
+    // CONSTRUCTOR
+    if (typeof opt_in === 'object' &&
+        typeof opt_in.toString === 'function') {
+      // Assume it's another shindig.uri, or something that can be parsed from one.
+      parseFrom(opt_in.toString());
+    } else if (opt_in) {
+      parseFrom(opt_in);
+    }
+
+    bundle = {
+      // Getters
+      getSchema: function() { return schema_; },
+      getAuthority: function() { return authority_; },
+      getOrigin: getOrigin,
+      getPath: function() { return path_; },
+      getQuery: getQuery,
+      getFragment: getFragment,
+      getQP: getQP,
+      getFP: getFP,
+
+      // Setters
+      setSchema: function(schema) { schema_ = schema; return bundle; },
+      setAuthority: function(authority) { authority_ = authority; return bundle; },
+      setPath: function(path) { if (typeof path !== 'undefined' && path != null) { path_ = (path[0] === '/' ? '' : '/') + path; } return bundle; },
+      setQuery: function(query) { qparms_ = null; query_ = stripPrefix(query, '?'); return bundle; },
+      setFragment: function(fragment) { fparms_ = null; fragment_ = stripPrefix(fragment, '#'); return bundle; },
+      setQP: setQP,
+      setFP: setFP,
+      setExistingP: function(key, val) {
+        if (typeof(getQP(key, val)) != 'undefined') {
+          setQP(key, val);
+        }
+        if (typeof(getFP(key, val)) != 'undefined') {
+          setFP(key, val);
+        }
+        return bundle;
+      },
+
+      // Core utility methods.
+      toString: toString
+    };
+
+    return bundle;
+  }
+})();
diff --git a/trunk/features/src/main/javascript/features/shindig.xhrwrapper/feature.xml b/trunk/features/src/main/javascript/features/shindig.xhrwrapper/feature.xml
new file mode 100644
index 0000000..69120ce
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.xhrwrapper/feature.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>shindig.xhrwrapper</name>
+  <dependency>core.io</dependency>
+  <dependency>xmlutil</dependency>
+  <gadget>
+    <script src="xhrwrapper.js"/>
+    <api>
+      <exports type="js">shindig.xhrwrapper.createXHR</exports>
+      <exports type="js">shindig.xhrwrapper.XhrWrapper</exports>
+      <exports type="js">shindig.xhrwrapper.XhrWrapper.prototype.abort</exports>
+      <exports type="js">shindig.xhrwrapper.XhrWrapper.prototype.getAllResponseHeaders</exports>
+      <exports type="js">shindig.xhrwrapper.XhrWrapper.prototype.getResponseHeader</exports>
+      <exports type="js">shindig.xhrwrapper.XhrWrapper.prototype.open</exports>
+      <exports type="js">shindig.xhrwrapper.XhrWrapper.prototype.send</exports>
+      <exports type="js">shindig.xhrwrapper.XhrWrapper.prototype.setRequestHeader</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/shindig.xhrwrapper/xhrwrapper.js b/trunk/features/src/main/javascript/features/shindig.xhrwrapper/xhrwrapper.js
new file mode 100644
index 0000000..0f46d2c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/shindig.xhrwrapper/xhrwrapper.js
@@ -0,0 +1,487 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Emulate XMLHttpRequest using gadgets.io.makeRequest.
+ *
+ * This is not a complete implementation of XMLHttpRequest:
+ * - synchronous send() is unsupported;
+ * - the callback function will not get full header information, as makeRequest
+ *   only provides the Set-Cookie and Location headers.
+ */
+
+shindig.xhrwrapper = shindig.xhrwrapper || {};
+
+(function() {
+
+  // Save the browser's XMLHttpRequest and ActiveXObject constructors.
+  var RealXMLHttpRequest = window.XMLHttpRequest;
+  var RealActiveXObject = window.ActiveXObject;
+
+  /**
+   * Creates a real XMLHttpRequest object.
+   *
+   * This function is to be used by code that needs access to the browser's
+   * XMLHttpRequest functionality, such as the code that implements
+   * gadgets.io.makeRequest itself.
+   *
+   * @return {Object|undefined} A XMLHttpRequest object, if one could
+   *     be created.
+   */
+  shindig.xhrwrapper.createXHR = function() {
+    var activeXIdents =
+        ['MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP'];
+    if (typeof RealActiveXObject != 'undefined') {
+      for (var i = 0; i < activeXIdents.length; i++) {
+        try {
+          return new RealActiveXObject(activeXIdents[i]);
+        } catch (x) {
+          // do nothing, if none exists we'll do something later
+        }
+      }
+    }
+    if (typeof RealXMLHttpRequest != 'undefined') {
+      return new RealXMLHttpRequest();
+    }
+    return undefined;
+  };
+
+  /**
+   * @class XhrWrapper class.
+   *
+   * @constructor
+   * @description Implements the XMLHttpRequest interface, using
+   *     gadgets.io.makeRequest to make the actual network accesses.
+   */
+  shindig.xhrwrapper.XhrWrapper = function() {
+    this.config_ = originalNS.gadgets.config.get('shindig.xhrwrapper');
+
+    // XMLHttpRequest event listeners
+    this.onreadystatechange = null;
+
+    // XMLHttpRequest properties
+    this.readyState = 0;
+  };
+
+  /**
+   * Aborts the request if it has already been sent.
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.abort = function() {
+    this.aborted_ = true;
+  };
+
+  /**
+   * Returns all response headers as a string.
+   *
+   * @return {?string} The text of all response headers, or null if no response
+   *     has been received.
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.getAllResponseHeaders = function() {
+    if (!this.responseHeaders_) {
+      return null;
+    }
+
+    var allHeaders = '';
+    for (var header in this.responseHeaders_) {
+      allHeaders += header + ': ' + this.responseHeaders_[header] + '\n';
+    }
+    return allHeaders;
+  };
+
+  /**
+   * Returns the value of a particular response header.
+   *
+   * @param {string} The name of the header to return.
+   * @return {?string} The value of the header, or null if no response has
+   *     been received or the header doesn't exist in the response.
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.getResponseHeader = function(header) {
+    if (!this.responseHeaders_) {
+      return null;
+    }
+
+    var value = this.responseHeaders_[header.toLowerCase()];
+    return value ? value : null;
+  };
+
+  /**
+   * Initializes a request.
+   *
+   * @param {string} method The HTTP method to use ('POST' or 'GET').
+   * @param {string} url The URL to which to send the request.
+   * @param {boolean=} opt_async Whether to perform the operation
+   *     asynchronously (defaults to true). Synchronous operations are not
+   *     supported, so it must presently be omitted or true.
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.open =
+      function(method, url, opt_async) {
+    this.method_ = method;
+    this.url_ = new Url(url);
+    this.aborted_ = false;
+    this.requestHeaders_ = {};
+    this.responseHeaders_ = {};
+
+    this.baseUrl_ = new Url(this.config_['contentUrl']);
+
+    this.fixRequestUrl_();
+
+    if (!this.baseUrl_.hasSameOrigin(this.url_)) {
+      throw new Error('A gadget at ' + this.config_['contentUrl'] +
+                      ' tried to access ' + url + ' via XMLHttpRequest.');
+    }
+
+    if (opt_async === false) {
+      throw new Error('xhrwrapper does not support synchronous XHR.');
+    }
+
+    // XMLHttpRequest properties
+    this.multipart = false;
+    this.readyState = 1;
+    this.responseText = null;
+    this.responseXML = null;
+    this.status = 0;
+    this.statusText = null;
+  };
+
+  /**
+   * Sends the request.
+   *
+   * @param {string=} opt_data The data used to populate the body of a POST
+   *     request.
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.send = function(opt_data) {
+    // Switch to the original namespaces for call to gadgets.io.makeRequest.
+    switchOriginalNS_();
+    try {
+      this.aborted_ = false;
+      var that = this;
+      var params = {};
+      params[gadgets.io.RequestParameters.METHOD] = this.method_;
+      params[gadgets.io.RequestParameters.HEADERS] = this.requestHeaders_;
+      params[gadgets.io.RequestParameters.GET_FULL_HEADERS] = true;
+      params[gadgets.io.RequestParameters.POST_DATA] = opt_data;
+      if (this.config_['authorization']) {
+        if (this.config_['authorization'] == 'oauth' || this.config_['authorization'] == 'oauth2') {
+          params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.OAUTH;
+          params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = this.config_['oauthService'];
+          if (this.config_['oauthTokenName']) {
+            params[gadgets.io.RequestParameters.OAUTH_TOKEN_NAME] = this.config_['oauthTokenName'];
+          }
+        } else if (this.config_['authorization'] == 'oauth2') {
+            params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.OAUTH2;
+            params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = this.config_['oauthService'];
+        } else if (this.config_['authorization'] == 'signed') {
+          params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.SIGNED;
+        }
+      }
+
+      gadgets.io.makeRequest(this.url_.toString(),
+                             function(response) { that.callback_(response); },
+                             params);
+    } catch (e) {
+      throw e;
+    } finally {
+      switchGadgetNS_();
+    }
+  };
+
+  /**
+   * Sets the value of an HTTP request header.
+   *
+   * @param {string} header The name of the header to set.
+   * @param {string} value The value for the header.
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.setRequestHeader =
+      function(header, value) {
+    this.requestHeaders_[header] = value;
+  };
+
+  /**
+   * Processes the results from makeRequest and calls onreadystatechange.
+   *
+   * @param {Object} response The response from makeRequest.
+   * @private
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.callback_ = function(response) {
+    if (this.aborted_) {
+      return;
+    }
+    this.readyState = 4;
+    this.responseHeaders_ = response.headers;
+    this.responseText = response.text;
+    // Switch to the original namespaces for call to opensocial.xmlutil.parseXML
+    switchOriginalNS_();
+    try {
+      this.responseXML = opensocial.xmlutil.parseXML(response.text);
+    } catch (x) {
+      this.responseXML = null;
+    } finally {
+      switchGadgetNS_();
+    }
+    this.status = response.rc;
+    if (response.errors) {
+      this.statusText = response.errors[0];
+    }
+    if (this.onreadystatechange) {
+      var event = {};
+      event.type = 'readystatechange';
+      event.srcElement = this;
+      event.target = this;
+      this.onreadystatechange(event);
+    }
+  };
+
+  /**
+   * Points the request URL to the correct server.
+   *
+   * If the URL is pointing to the gadget server, this function assumes the
+   * gadget's author wanted to point to the gadget contents location and
+   * changes it so that it points to the right place.
+   *
+   * For example, if the gadget is rendered in https://shindig/gadgets/ifr
+   * and the gadget's contents are at http://foo.com/bar/baz.html:
+   *
+   * - foo.xml gets turned into http://foo.com/bar/foo.xml
+   * - /foo/bar.xml gets turned into http://foo.com/foo/bar.xml
+   * - //foo.com/bar.xml gets turned into http://foo.com/bar.xml
+   * - http://foo.com/bar.xml is untouched
+   * - https://shindig/bar.xml is turned into http://foo.com/bar.xml
+   * - https://shindig/gadgets/bar.xml is turned into
+   *     http://foo.com/bar/bar.xml
+   */
+  shindig.xhrwrapper.XhrWrapper.prototype.fixRequestUrl_ = function() {
+    this.url_.fullyQualify(this.baseUrl_);
+    var loc = new Url(window.location.href);
+    if (this.url_.hasSameOrigin(loc)) {
+      this.url_.schema = this.baseUrl_.schema;
+      this.url_.authority = this.baseUrl_.authority;
+      var pathLen = loc.path.length;
+      if (this.url_.path.substr(0, pathLen) == loc.path) {
+        this.url_.path = this.baseUrl_.path + this.url_.path.substr(pathLen);
+      }
+    }
+  };
+
+  /**
+   * @class A class for processing URLs.
+   *
+   * @constructor
+   * @description Pries apart the components of a URL, so it can be sliced
+   * and diced and combined with other URLs as needed.
+   */
+  function Url(url) {
+    this.schema = '';
+    this.authority = '';
+    this.path = '';
+    this.filename = '';
+    this.query = '';
+    this.fragment = '';
+
+    var parse = url;
+    var sharp = parse.indexOf('#');
+    if (sharp != -1) {
+      this.fragment = parse.substr(sharp);
+      parse = parse.substr(0, sharp);
+    }
+    var question = parse.indexOf('?');
+    if (question != -1) {
+      this.query = parse.substr(question);
+      parse = parse.substr(0, question);
+    }
+    var doubleSlash = parse.indexOf('//');
+    if (doubleSlash != -1) {
+      this.schema = parse.substr(0, doubleSlash);
+      parse = parse.substr(doubleSlash + 2);
+      var firstSlash = parse.indexOf('/');
+      if (firstSlash != -1) {
+        this.authority = parse.substr(0, firstSlash);
+        parse = parse.substr(firstSlash);
+      } else {
+        this.authority = parse;
+        parse = '';
+      }
+    }
+    var lastSlash = parse.lastIndexOf('/');
+    if (lastSlash != -1) {
+      this.path = parse.substr(0, lastSlash + 1);
+      parse = parse.substr(lastSlash + 1);
+    }
+    this.filename = parse;
+  };
+
+  /**
+   * Checks that a URL has the same origin as this URL.
+   *
+   * Two URLs have the same origin if they point to the same schema, server
+   * and port.
+   *
+   * @param {Url} other The URL to compare to this URL.
+   * @return {boolean} Whether the URLs have the same origin.
+   */
+  Url.prototype.hasSameOrigin = function(other) {
+    return this.schema == other.schema && this.authority == other.authority;
+  };
+
+  /**
+   * Fully qualifies this URL if it is relative, using a given base URL.
+   *
+   * @param {Url} base The base URL.
+   */
+  Url.prototype.fullyQualify = function(base) {
+    if (this.schema == '') {
+      this.schema = base.schema;
+    }
+    if (this.authority == '') {
+      this.authority = base.authority;
+      if (this.path == '' || this.path[0] != '/') {
+        this.path = base.path + this.path;
+      }
+    }
+  };
+
+  /**
+   * Returns a readable representation of the URL.
+   *
+   * @return {string} A readable URL.
+   */
+  Url.prototype.toString = function() {
+    var url = '';
+    if (this.schema) {
+      url += this.schema;
+    }
+    if (this.authority) {
+      url += '//' + this.authority;
+    }
+    if (this.path) {
+      url += this.path;
+    }
+    if (this.filename) {
+      url += this.filename;
+    }
+    if (this.query) {
+      url += this.query;
+    }
+    if (this.fragment) {
+      url += this.fragment;
+    }
+    return url;
+  };
+
+  /**
+   * Acts as a drop-in replacement for IE's ActiveXObject.
+   * @param {string} className The name of the class to create.
+   */
+  function ActiveXObjectReplacement(className) {
+    var obj;
+    if (typeof className == 'string' &&
+        (className.substr(0, 14).toLowerCase() == 'msxml2.xmlhttp' ||
+         className.toLowerCase() == 'microsoft.xmlhttp')) {
+      obj = new shindig.xhrwrapper.XhrWrapper();
+    } else {
+      obj = new RealActiveXObject(className);
+    }
+    for (var f in obj) {
+      this[f] = obj[f];
+    }
+  };
+
+  /*
+   * XhrWrapper is designed to take type=url gadgets and convert them to
+   * type=html gadgets with minimal changes. Some of those gadgets, instead
+   * of loading the feature JavaScript on demand, have it hardcoded.
+   * When such a gadget is loaded as a type=html gadget, the code hardcoded
+   * in the gadget will overwrite the code from Shindig.
+   *
+   * This is bad because when this code tries to call gadgets.io.makeRequest,
+   * it will call the wrong function, or it might even be undefined.
+   *
+   * Therefore, we save the original namespaces before the gadget has a chance
+   * to overwrite them, then switch between them as necessary.
+   *
+   * This works like this:
+   *
+   * switchOriginalNS_();
+   * try {
+   *   functionThatNeedsTheOriginalNamespaces();
+   * } catch (e) {
+   *   throw e;
+   * } finally {
+   *   // Make sure we switch back to the gadget namespaces.
+   *   switchGadgetNS_();
+   * }
+   */
+  var originalNS = {};
+  var gadgetNS = {};
+  var namespaces = ['gadgets', 'opensocial', 'shindig'];
+
+  /**
+   * Copies the Shindig namespaces between two objects.
+   *
+   * @param {Object} from Object to copy from, or null for the global object.
+   * @param {Object} to Object to copy to, or null for the global object.
+   * @private
+   */
+  function copyNS_(from, to) {
+    // NB "this" is the global object.
+    var orig = from ? from : this;
+    var dest = to ? to : this;
+    for (var i in namespaces) {
+      var nsName = namespaces[i];
+      if (typeof orig[nsName] != 'undefined') {
+        dest[nsName] = orig[nsName];
+      }
+    }
+  };
+
+  /**
+   * Switches from the gadget's namespaces to the original namespaces.
+   * @private
+   */
+  function switchOriginalNS_() {
+    copyNS_(null, gadgetNS);
+    copyNS_(originalNS, null);
+  };
+
+  /**
+   * Switches from the original namespaces to the gadget's namespaces.
+   * @private
+   */
+  function switchGadgetNS_() {
+    copyNS_(gadgetNS, null);
+  };
+
+  // Save the original namespaces.
+  copyNS_(null, originalNS);
+
+  // Replace the browser's XMLHttpRequest and ActiveXObject constructors with
+  // xhrwrapper's.
+  if (window.XMLHttpRequest) {
+    window.XMLHttpRequest = shindig.xhrwrapper.XhrWrapper;
+  }
+  if (window.ActiveXObject) {
+    window.ActiveXObject = ActiveXObjectReplacement;
+  }
+
+  var config = {
+    contentUrl: gadgets.config.NonEmptyStringValidator
+  };
+  gadgets.config.register('shindig.xhrwrapper', config);
+
+})();
+
diff --git a/trunk/features/src/main/javascript/features/skins/feature.xml b/trunk/features/src/main/javascript/features/skins/feature.xml
new file mode 100644
index 0000000..89830c1
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/skins/feature.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>skins</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>core.config</dependency>
+  <dependency>core.util</dependency>
+  <gadget>
+    <script src="skins.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.skins.init</exports>
+      <exports type="js">gadgets.skins.getProperty</exports>
+      <exports type="js">gadgets.skins.Property</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/skins/skins.js b/trunk/features/src/main/javascript/features/skins/skins.js
new file mode 100644
index 0000000..058d7fc
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/skins/skins.js
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library provides functions for getting skin information.
+ */
+
+/**
+ * @static
+ * @class Provides operations for getting display information about the
+ *     currently shown skin.
+ * @name gadgets.skins
+ */
+gadgets.skins = function() {
+  var skinProperties = {};
+
+  var requiredConfig = {
+    'properties': gadgets.config.ExistsValidator
+  };
+
+  gadgets.config.register('skins', requiredConfig, function(config) {
+    skinProperties = config['skins']['properties'];
+  });
+
+
+  return {
+    /**
+     * Override the default properties with a new set of properties.
+     *
+     * @param {Object} properties The mapping of property names to values.
+     */
+    init: function(properties) {
+      skinProperties = properties;
+    },
+
+    /**
+     * Fetches the display property mapped to the given key.
+     *
+     * @param {string} propertyKey The key to get data for;
+     *    keys are defined in <a href="gadgets.skins.Property.html"><code>
+     *    gadgets.skins.Property.</code></a>
+     * @return {string} The data.
+     *
+     * @member gadgets.skins
+     */
+    getProperty: function(propertyKey) {
+      return skinProperties[propertyKey] || '';
+    }
+  };
+}();
+/**
+ * @static
+ * @class
+ * All of the display values that can be fetched and used in the gadgets UI.
+ * These are the supported keys for the
+ * <a href="gadgets.skins.html#getProperty">gadgets.skins.getProperty()</a>
+ * method.
+ * @name gadgets.skins.Property
+ */
+gadgets.skins.Property = gadgets.util.makeEnum([
+  /**
+   * An image to use in the background of the gadget.
+   * @member gadgets.skins.Property
+   */
+  'BG_IMAGE',
+
+  /**
+   * The color of the background of the gadget.
+   * @member gadgets.skins.Property
+   */
+  'BG_COLOR',
+
+  /**
+   * The color that the main font should use.
+   * @member gadgets.skins.Property
+   */
+  'FONT_COLOR',
+
+  /**
+   * The positioning of the background image
+   * @member gadgets.skins.Property
+   */
+  'BG_POSITION',
+
+  /**
+   * The repeat characteristics for the background image
+   * @member gadgets.skins.Property
+   */
+  'BG_REPEAT',
+
+  /**
+   * The color that anchor tags should use.
+   * @member gadgets.skins.Property
+   */
+  'ANCHOR_COLOR'
+]);
diff --git a/trunk/features/src/main/javascript/features/skins/taming.js b/trunk/features/src/main/javascript/features/skins/taming.js
new file mode 100644
index 0000000..3facac7
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/skins/taming.js
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.skin.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistFuncs([
+    [gadgets.skins, 'getProperty']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/tabs/feature.xml b/trunk/features/src/main/javascript/features/tabs/feature.xml
new file mode 100644
index 0000000..a922bbd
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/tabs/feature.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>tabs</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>core.prefs</dependency>
+  <dependency>core.config</dependency>
+  <gadget>
+    <script src="tabs.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.Tab</exports>
+      <exports type="js">gadgets.Tab.prototype.getName</exports>
+      <exports type="js">gadgets.Tab.prototype.getNameContainer</exports>
+      <exports type="js">gadgets.Tab.prototype.getContentContainer</exports>
+      <exports type="js">gadgets.Tab.prototype.getCallback</exports>
+      <exports type="js">gadgets.Tab.prototype.getIndex</exports>
+      <exports type="js">gadgets.TabSet</exports>
+      <exports type="js">gadgets.TabSet.prototype.addTab</exports>
+      <exports type="js">gadgets.TabSet.prototype.removeTab</exports>
+      <exports type="js">gadgets.TabSet.prototype.getSelectedTab</exports>
+      <exports type="js">gadgets.TabSet.prototype.setSelectedTab</exports>
+      <exports type="js">gadgets.TabSet.prototype.swapTabs</exports>
+      <exports type="js">gadgets.TabSet.prototype.getTabs</exports>
+      <exports type="js">gadgets.TabSet.prototype.alignTabs</exports>
+      <exports type="js">gadgets.TabSet.prototype.displayTabs</exports>
+      <exports type="js">gadgets.TabSet.prototype.getHeaderContainer</exports>
+      <exports type="js">_IG_Tabs</exports>
+      <exports type="js">_IG_Tabs.prototype.addDynamicTab</exports>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/tabs/tabs.js b/trunk/features/src/main/javascript/features/tabs/tabs.js
new file mode 100644
index 0000000..19ee567
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/tabs/tabs.js
@@ -0,0 +1,666 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Tabs library for gadgets.
+ */
+
+/**
+ * @class Tab class for gadgets.
+ * You create tabs using the TabSet addTab() method.
+ * To get Tab objects,
+ * use the TabSet getSelectedTab() or getTabs() methods.
+ *
+ * <p>
+ * <b>See also:</b>
+ * <a href="gadgets.TabSet.html">TabSet</a>
+ * </p>
+ *
+ * @name gadgets.Tab
+ * @description Creates a new Tab.
+ */
+
+/**
+ * @param {gadgets.TabSet} handle The associated gadgets.TabSet instance.
+ * @private
+ * @constructor
+ */
+gadgets.Tab = function(handle) {
+  this.handle_ = handle;
+  this.td_ = null;
+  this.contentContainer_ = null;
+  this.callback_ = null;
+};
+
+/**
+ * Returns the label of the tab as a string (may contain HTML).
+ * @return {string} Label of the tab.
+ */
+gadgets.Tab.prototype.getName = function() {
+  return this.td_.innerHTML;
+};
+
+/**
+ * Returns the HTML element that contains the tab's label.
+ * @return {Element} The HTML element of the tab's label.
+ */
+gadgets.Tab.prototype.getNameContainer = function() {
+  return this.td_;
+};
+
+/**
+ * Returns the HTML element where the tab content is rendered.
+ * @return {Element} The HTML element of the content container.
+ */
+gadgets.Tab.prototype.getContentContainer = function() {
+  return this.contentContainer_;
+};
+
+/**
+ * Returns the callback function that is executed when the tab is selected.
+ * @return {function(string)} The callback function of the tab.
+ */
+gadgets.Tab.prototype.getCallback = function() {
+  return this.callback_;
+};
+
+/**
+ * Returns the tab's index.
+ * @return {number} The tab's index.
+ */
+gadgets.Tab.prototype.getIndex = function() {
+  var tabs = this.handle_.getTabs();
+  for (var i = 0; i < tabs.length; ++i) {
+    if (this === tabs[i]) {
+      return i;
+    }
+  }
+  return -1;
+};
+
+/**
+ * @class A class gadgets can use to make tabs.
+ * @description Creates a new TabSet object
+ *
+ * @constructor
+ * @param {string=} opt_moduleId Optional suffix for the ID of tab container.
+ * @param {string=} opt_defaultTab Optional tab name that specifies the name of
+ *                   of the tab that is selected after initialization.
+ *                   If this parameter is omitted, the first tab is selected by
+ *                   default.
+ * @param {Element=} opt_container The HTML element to contain the tabs.  If
+ *                    omitted, a new div element is created and inserted at the
+ *                    very top.
+ */
+gadgets.TabSet = function(opt_moduleId, opt_defaultTab, opt_container) {
+  this.moduleId_ = opt_moduleId || 0;
+  this.domIdFilter_ = new RegExp('^[A-Za-z]([0-9a-zA-Z_:.-]+)?$');
+  this.selectedTab_ = null;
+  this.tabs_ = [];
+  this.tabsAdded_ = 0;
+  this.defaultTabName_ = opt_defaultTab || '';
+  this.leftNavContainer_ = null;
+  this.rightNavContainer_ = null;
+  this.navTable_ = null;
+  this.tabsContainer_ = null;
+  this.rtl_ = document.body.dir === 'rtl';
+  this.prefs_ = new gadgets.Prefs();
+  this.selectedTabIndex_ = this.prefs_.getString('selectedTab');
+  this.mainContainer_ = this.createMainContainer_(opt_container);
+  this.tabTable_ = this.createTabTable_();
+  this.displayTabs(false);
+  //  gadgets.TabSet.addCSS_([  ].join(''));
+};
+
+gadgets.config.register('tabset', {}, function(configuration) {
+  // Injects the default stylesheet for tabs
+  gadgets.TabSet.addCSS_(configuration['tabs']['css'].join(''));
+});
+
+
+/**
+ * Adds a new tab based on the name-value pairs specified in opt_params.
+ * @param {string} tabName Label of the tab to create.
+ * @param {string|Object=} opt_params Optional parameter object. The following
+ *                   properties are supported:
+ *                   .contentContainer An existing HTML element to be used as
+ *                     the tab content container. If omitted, the tabs
+ *                     library creates one.
+ *                   .callback A callback function to be executed when the tab
+ *                     is selected.
+ *                   .tooltip A tooltip description that pops up when user moves
+ *                     the mouse cursor over the tab.
+ *                   .index The index at which to insert the tab. If omitted,
+ *                     the new tab is appended to the end.
+ * @return {string} DOM id of the tab container.
+ */
+gadgets.TabSet.prototype.addTab = function(tabName, opt_params) {
+  if (typeof opt_params === 'string') {
+    opt_params = {contentContainer: document.getElementById(arguments[1]),
+      callback: arguments[2]};
+  }
+
+  var params = opt_params || {};
+
+  var tabIndex = -1;
+  if (params.index >= 0 && params.index < this.tabs_.length) {
+    tabIndex = params.index;
+  }
+  var tab = this.createTab_(tabName, {
+    contentContainer: params.contentContainer,
+    callback: params.callback,
+    tooltip: params.tooltip
+  });
+
+  var tr = this.tabTable_.rows[0];
+  if (this.tabs_.length > 0) {
+    var filler = document.createElement('td');
+    filler.className = this.cascade_('tablib_spacerTab');
+    filler.appendChild(document.createTextNode(' '));
+
+    var ref = tabIndex < 0 ? tr.cells[tr.cells.length - 1] : this.tabs_[tabIndex].td_;
+    tr.insertBefore(filler, ref);
+    tr.insertBefore(tab.td_, tabIndex < 0 ? ref : filler);
+  } else {
+    tr.insertBefore(tab.td_, tr.cells[tr.cells.length - 1]);
+  }
+
+  if (tabIndex < 0) {
+    tabIndex = this.tabs_.length;
+    this.tabs_.push(tab);
+  } else {
+    this.tabs_.splice(tabIndex, 0, tab);
+
+    // Inserting may change selected tab's index
+    this.saveSelectedTabIndex_();
+  }
+
+  var selectedIndex = parseInt(this.selectedTabIndex_, 10);
+  if (isNaN(selectedIndex)) {
+    if (tabName == this.defaultTabName_ || (!this.defaultTabName_ && tabIndex === 0)) {
+      this.selectTab_(tab);
+    }
+  } else if (selectedIndex == tabIndex) {
+    this.selectTab_(tab, true);
+  }
+
+  this.tabsAdded_++;
+  this.displayTabs(true);
+  this.adjustNavigation_();
+
+  return tab.contentContainer_.id;
+};
+
+/**
+ * Removes a tab at tabIndex and all of its associated content.
+ * @param {number} tabIndex Index of the tab to remove.
+ */
+gadgets.TabSet.prototype.removeTab = function(tabIndex) {
+  var tab = this.tabs_[tabIndex];
+  if (tab) {
+    if (tab === this.selectedTab_) {
+      var maxIndex = this.tabs_.length - 1;
+      if (maxIndex > 0) {
+        this.selectTab_(tabIndex < maxIndex ?
+            this.tabs_[tabIndex + 1] :
+            this.tabs_[tabIndex - 1]);
+      }
+    }
+    var tr = this.tabTable_.rows[0];
+    if (this.tabs_.length > 1) {
+      tr.removeChild(tabIndex ? tab.td_.previousSibling : tab.td_.nextSibling);
+    }
+    tr.removeChild(tab.td_);
+    this.mainContainer_.removeChild(tab.contentContainer_);
+    this.tabs_.splice(tabIndex, 1);
+    this.adjustNavigation_();
+    if (this.tabs_.length === 0) {
+      this.displayTabs(false);
+      this.selectedTab_ = null;
+    }
+  }
+};
+
+/**
+ * Returns the currently selected tab object.
+ * @return {gadgets.Tab} The currently selected tab object.
+ */
+gadgets.TabSet.prototype.getSelectedTab = function() {
+  return this.selectedTab_;
+};
+
+/**
+ * Selects the tab at tabIndex and fires the tab's callback function if it
+ * exists. If the tab is already selected, the callback is not fired.
+ * @param {number} tabIndex Index of the tab to select.
+ */
+gadgets.TabSet.prototype.setSelectedTab = function(tabIndex) {
+  if (this.tabs_[tabIndex]) {
+    this.selectTab_(this.tabs_[tabIndex]);
+  }
+};
+
+/**
+ * Swaps the positions of tabs at tabIndex1 and tabIndex2. The selected tab
+ * does not change, and no callback functions are called.
+ * @param {number} tabIndex1 Index of the first tab to swap.
+ * @param {number} tabIndex2 Index of the secnod tab to swap.
+ */
+gadgets.TabSet.prototype.swapTabs = function(tabIndex1, tabIndex2) {
+  var tab1 = this.tabs_[tabIndex1];
+  var tab2 = this.tabs_[tabIndex2];
+  if (tab1 && tab2) {
+    var tr = tab1.td_.parentNode;
+    var slot = tab1.td_.nextSibling;
+    tr.insertBefore(tab1.td_, tab2.td_);
+    tr.insertBefore(tab2.td_, slot);
+    this.tabs_[tabIndex1] = tab2;
+    this.tabs_[tabIndex2] = tab1;
+  }
+};
+
+
+/**
+ * Returns an array of all existing tab objects.
+ * @return {Array.<gadgets.Tab>} Array of all existing tab objects.
+ */
+gadgets.TabSet.prototype.getTabs = function() {
+  return this.tabs_;
+};
+
+/**
+ * Sets the alignment of tabs. Tabs are center-aligned by default.
+ * @param {string} align 'left', 'center', or 'right'.
+ * @param {number=} opt_offset Optional parameter to set the number of pixels
+ *                   to offset tabs from the left or right edge. The default
+ *                   value is 3px.
+ */
+gadgets.TabSet.prototype.alignTabs = function(align, opt_offset) {
+  var tr = this.tabTable_.rows[0];
+  var left = tr.cells[0];
+  var right = tr.cells[tr.cells.length - 1];
+  var offset = isNaN(opt_offset) ? '3px' : opt_offset + 'px';
+  left.style.width = align === 'left' ? offset : '';
+  right.style.width = align === 'right' ? offset : '';
+  // In Opera and potentially some other browsers, changes to the width of
+  // table cells aren't rendered.  To fix this, we force to re-render the
+  // table by hiding and showing it again.
+  this.tabTable_.style.display = 'none';
+  this.tabTable_.style.display = '';
+};
+
+/**
+ * Shows or hides tabs and all associated content.
+ * @param {boolean} display true to show tabs; false to hide tabs.
+ */
+gadgets.TabSet.prototype.displayTabs = function(display) {
+  this.mainContainer_.style.display = display ? 'block' : 'none';
+};
+
+/**
+ * Returns the tab headers container element.
+ * @return {Element} The tab headers container element.
+ */
+gadgets.TabSet.prototype.getHeaderContainer = function() {
+  return this.tabTable_;
+};
+
+/**
+ * Helper method that returns an HTML container element to which all tab-related
+ * content will be appended.
+ * This container element is created and inserted as the first child of the
+ * gadget if opt_element is not specified.
+ * @param {Element=} opt_element Optional HTML container element.
+ * @return {Element} HTML container element.
+ * @private
+ */
+gadgets.TabSet.prototype.createMainContainer_ = function(opt_element) {
+  var newId = 'tl_' + this.moduleId_;
+  var container = opt_element || document.getElementById(newId);
+
+  if (!container) {
+    container = document.createElement('div');
+    container.id = newId;
+    document.body.insertBefore(container, document.body.firstChild);
+  }
+
+  container.className = this.cascade_('tablib_main_container') + ' ' +
+      container.className;
+
+  return container;
+};
+
+/**
+ * Helper method that expands a class name into two class names.
+ * @param {string} label CSS class.
+ * @return {string} Expanded class names.
+ * @private
+ */
+gadgets.TabSet.prototype.cascade_ = function(label) {
+  return label + ' ' + label + this.moduleId_;
+};
+
+/**
+ * Helper method that creates the tabs table and inserts it into the main
+ * container as the first child.
+ * @return {Element} HTML element of the tab container table.
+ * @private
+ */
+gadgets.TabSet.prototype.createTabTable_ = function() {
+  var table = document.createElement('table');
+  table.id = this.mainContainer_.id + '_header';
+  table.className = this.cascade_('tablib_table');
+  table.cellSpacing = '0';
+  table.cellPadding = '0';
+
+  var tbody = document.createElement('tbody');
+  var tr = document.createElement('tr');
+  tbody.appendChild(tr);
+  table.appendChild(tbody);
+
+  var emptyTd = document.createElement('td');
+  emptyTd.className = this.cascade_('tablib_emptyTab');
+  emptyTd.appendChild(document.createTextNode(' '));
+  tr.appendChild(emptyTd);
+  tr.appendChild(emptyTd.cloneNode(true));
+
+  // Construct a wrapper table around our tab table to house the navigation
+  // elements. These elements will appear if the tab table overflows.
+  var navTable = document.createElement('table');
+  navTable.id = this.mainContainer_.id + '_navTable';
+  navTable.style.width = '100%';
+  navTable.cellSpacing = '0';
+  navTable.cellPadding = '0';
+  navTable.style.tableLayout = 'fixed';
+  var navTbody = document.createElement('tbody');
+  var navTr = document.createElement('tr');
+  navTbody.appendChild(navTr);
+  navTable.appendChild(navTbody);
+
+  // Create the left navigation element.
+  var leftNavTd = document.createElement('td');
+  leftNavTd.className = this.cascade_('tablib_emptyTab') + ' ' +
+                        this.cascade_('tablib_navContainer');
+  leftNavTd.style.textAlign = 'left';
+  leftNavTd.style.display = '';
+  var leftNav = document.createElement('a');
+  leftNav.href = 'javascript:void(0)';
+  leftNav.innerHTML = '&laquo;';
+  leftNavTd.appendChild(leftNav);
+  navTr.appendChild(leftNavTd);
+
+  // House the actual tab table in the middle, hiding any overflow.
+  var tabNavTd = document.createElement('td');
+  navTr.appendChild(tabNavTd);
+  var wrapper = document.createElement('div');
+  wrapper.style.width = '100%';
+  wrapper.style.overflow = 'hidden';
+  wrapper.appendChild(table);
+  tabNavTd.appendChild(wrapper);
+
+  // Create the right navigation element.
+  var rightNavTd = document.createElement('td');
+  rightNavTd.className = this.cascade_('tablib_emptyTab') + ' ' +
+                         this.cascade_('tablib_navContainer');
+  rightNavTd.style.textAlign = 'right';
+  rightNavTd.style.display = '';
+  var rightNav = document.createElement('a');
+  rightNav.href = 'javascript:void(0)';
+  rightNav.innerHTML = '&raquo;';
+  rightNavTd.appendChild(rightNav);
+  navTr.appendChild(rightNavTd);
+
+  // Register onclick event handlers for smooth scrolling.
+  var me = this;
+  leftNav.onclick = function(event) {
+    me.smoothScroll_(wrapper, -120);
+
+    event = event || window.event;
+    if (event.stopPropagation) {
+      event.stopPropagation();
+    } else {
+      event.cancelBubble = true;
+    }
+    if (event.preventDefault) {
+      event.preventDefault();
+    } else {
+      event.returnValue = false;
+    }
+  };
+  rightNav.onclick = function(event) {
+    me.smoothScroll_(wrapper, 120);
+
+    event = event || window.event;
+    if (event.stopPropagation) {
+      event.stopPropagation();
+    } else {
+      event.cancelBubble = true;
+    }
+    if (event.preventDefault) {
+      event.preventDefault();
+    } else {
+      event.returnValue = false;
+    }
+  };
+
+  // Swap left and right scrolling if direction is RTL.
+  if (this.rtl_) {
+    var temp = leftNav.onclick;
+    leftNav.onclick = rightNav.onclick;
+    rightNav.onclick = temp;
+  }
+
+  // If we're already displaying tabs, then remove them.
+  if (this.navTable_) {
+    this.mainContainer_.replaceChild(navTable, this.navTable_);
+  } else {
+    this.mainContainer_.insertBefore(navTable, this.mainContainer_.firstChild);
+    var adjustNavigationFn = function() {
+      me.adjustNavigation_();
+    };
+    gadgets.util.attachBrowserEvent(window, 'resize', adjustNavigationFn, false);
+  }
+
+  this.navTable_ = navTable;
+  this.leftNavContainer_ = leftNavTd;
+  this.rightNavContainer_ = rightNavTd;
+  this.tabsContainer_ = wrapper;
+
+  return table;
+};
+
+/**
+ * Helper method that shows or hides the navigation elements.
+ * @private
+ */
+gadgets.TabSet.prototype.adjustNavigation_ = function() {
+  this.leftNavContainer_.style.display = 'none';
+  this.rightNavContainer_.style.display = 'none';
+  if (this.tabsContainer_.scrollWidth <= this.tabsContainer_.offsetWidth) {
+    if (this.tabsContainer_.scrollLeft) {
+      // to avoid JS error in IE
+      this.tabsContainer_.scrollLeft = 0;
+    }
+    return;
+  }
+
+  this.leftNavContainer_.style.display = '';
+  this.rightNavContainer_.style.display = '';
+  if (this.tabsContainer_.scrollLeft + this.tabsContainer_.offsetWidth >
+      this.tabsContainer_.scrollWidth) {
+    this.tabsContainer_.scrollLeft = this.tabsContainer_.scrollWidth -
+                                     this.tabsContainer_.offsetWidth;
+  } else if (this.rtl_) {
+    this.tabsContainer_.scrollLeft = this.tabsContainer_.scrollWidth;
+  }
+};
+
+/**
+ * Helper method that smoothly scrolls the tabs container.
+ * @param {Element} container The tabs container element.
+ * @param {number} distance The amount of pixels to scroll right.
+ * @private
+ */
+gadgets.TabSet.prototype.smoothScroll_ = function(container, distance) {
+  var scrollAmount = 10;
+  if (!distance) {
+    return;
+  } else {
+    container.scrollLeft += (distance < 0) ? -scrollAmount : scrollAmount;
+  }
+
+  var nextScroll = Math.min(scrollAmount, Math.abs(distance));
+  var me = this;
+  var timeoutFn = function() {
+    me.smoothScroll_(container, (distance < 0) ? distance + nextScroll :
+                                                 distance - nextScroll);
+  };
+  setTimeout(timeoutFn, 10);
+};
+
+/**
+ * Helper function that dynamically inserts CSS rules to the page.
+ * @param {string} cssText CSS rules to inject.
+ * @private
+ */
+gadgets.TabSet.addCSS_ = function(cssText) {
+  var head = document.getElementsByTagName('head')[0];
+  if (head) {
+    var styleElement = document.createElement('style');
+    styleElement.type = 'text/css';
+    if (styleElement.styleSheet) {
+      styleElement.styleSheet.cssText = cssText;
+    } else {
+      styleElement.appendChild(document.createTextNode(cssText));
+    }
+    head.insertBefore(styleElement, head.firstChild);
+  }
+};
+
+/**
+ * Helper method that creates a new gadgets.Tab object.
+ * @param {string} tabName Label of the tab to create.
+ * @param {Object} params Parameter object. The following properties
+ *                   are supported:
+ *                   .contentContainer An existing HTML element to be used as
+ *                     the tab content container. If omitted, the tabs
+ *                     library creates one.
+ *                   .callback A callback function to be executed when the tab
+ *                     is selected.
+ *                   .tooltip A tooltip description that pops up when user moves
+ *                     the mouse cursor over the tab.
+ * @return {gadgets.Tab} A new gadgets.Tab object.
+ * @private
+ */
+gadgets.TabSet.prototype.createTab_ = function(tabName, params) {
+  var tab = new gadgets.Tab(this);
+  tab.contentContainer_ = params.contentContainer;
+  tab.callback_ = params.callback;
+  tab.td_ = document.createElement('td');
+  tab.td_.title = params.tooltip || '';
+  tab.td_.innerHTML = html_sanitize(tabName);
+  tab.td_.className = this.cascade_('tablib_unselected');
+  tab.td_.onclick = this.setSelectedTabGenerator_(tab);
+
+  if (!tab.contentContainer_) {
+    tab.contentContainer_ = document.createElement('div');
+    tab.contentContainer_.id = this.mainContainer_.id + '_' + this.tabsAdded_;
+    this.mainContainer_.appendChild(tab.contentContainer_);
+  } else if (tab.contentContainer_.parentNode !== this.mainContainer_) {
+    this.mainContainer_.appendChild(tab.contentContainer_);
+  }
+  tab.contentContainer_.style.display = 'none';
+  tab.contentContainer_.className = this.cascade_('tablib_content_container') +
+      ' ' + tab.contentContainer_.className;
+  return tab;
+};
+
+/**
+ * Helper method that creates a function to select the specified tab.
+ * @param {gadgets.Tab} tab The tab to select.
+ * @return {function()} Callback function to select the tab.
+ * @private
+ */
+gadgets.TabSet.prototype.setSelectedTabGenerator_ = function(tab) {
+  return function() { tab.handle_.selectTab_(tab); };
+};
+
+/**
+ * Helper method that selects a tab and unselects the previously selected.
+ * If the tab is already selected, then callback is not executed.
+ * @param {gadgets.Tab} tab The tab to select.
+ * @private
+ */
+gadgets.TabSet.prototype.selectTab_ = function(tab, opt_inhibit_save) {
+  if (this.selectedTab_ === tab) {
+    return;
+  }
+
+  if (this.selectedTab_) {
+    this.selectedTab_.td_.className = this.cascade_('tablib_unselected');
+    this.selectedTab_.td_.onclick =
+        this.setSelectedTabGenerator_(this.selectedTab_);
+    this.selectedTab_.contentContainer_.style.display = 'none';
+  }
+
+  tab.td_.className = this.cascade_('tablib_selected');
+  tab.td_.onclick = null;
+  tab.contentContainer_.style.display = 'block';
+  this.selectedTab_ = tab;
+
+  // Remember which tab is selected only if nosave is not true.
+  var nosave = (opt_inhibit_save === true) ? true : false;
+  if (!nosave) {
+    this.saveSelectedTabIndex_();
+  }
+
+  if (typeof tab.callback_ === 'function') {
+    tab.callback_(tab.contentContainer_.id);
+  }
+};
+
+gadgets.TabSet.prototype.saveSelectedTabIndex_ = function() {
+  try {
+    var currentTabIndex = this.selectedTab_.getIndex();
+    if (currentTabIndex >= 0) {
+      this.selectedTabIndex_ = currentTabIndex;
+      this.prefs_.set('selectedTab', currentTabIndex);
+    }
+  } catch (e) {
+    // ignore.  setprefs is optional for tablib.
+  }
+};
+
+// Aliases for legacy code
+
+/**
+ * @type {gadgets.TabSet}
+ * @deprecated
+ */
+var _IG_Tabs = gadgets.TabSet;
+_IG_Tabs.prototype.moveTab = _IG_Tabs.prototype.swapTabs;
+
+/**
+ * @param {string} tabName
+ * @param {function()} callback
+ * @deprecated
+ */
+_IG_Tabs.prototype.addDynamicTab = function(tabName, callback) {
+  return this.addTab(tabName, {callback: callback});
+};
+
diff --git a/trunk/features/src/main/javascript/features/tabs/taming.js b/trunk/features/src/main/javascript/features/tabs/taming.js
new file mode 100644
index 0000000..d4cc584
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/tabs/taming.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose gadgets.Tabs and gadgets.TabSet API to cajoled gadgets
+ */
+
+tamings___.push(function(imports) {
+  caja___.whitelistCtors([
+    [gadgets, 'Tab'],
+    [gadgets, 'TabSet']
+  ]);
+  caja___.whitelistMeths([
+    [gadgets.Tab, 'getCallback'],
+    [gadgets.Tab, 'getContentContainer'],
+    [gadgets.Tab, 'getIndex'],
+    [gadgets.Tab, 'getName'],
+    [gadgets.Tab, 'getNameContainer'],
+
+    [gadgets.TabSet, 'addTab'],
+    [gadgets.TabSet, 'alignTabs'],
+    [gadgets.TabSet, 'displayTabs'],
+    [gadgets.TabSet, 'getHeaderContainer'],
+    [gadgets.TabSet, 'getSelectedTab'],
+    [gadgets.TabSet, 'getTabs'],
+    [gadgets.TabSet, 'removeTab'],
+    [gadgets.TabSet, 'setSelectedTab'],
+    [gadgets.TabSet, 'swapTabs']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/taming/feature.xml b/trunk/features/src/main/javascript/features/taming/feature.xml
new file mode 100644
index 0000000..9b9f3c3
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/taming/feature.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>taming</name>
+  <all>
+    <script src="taming.js"/>
+    <api>
+      <exports type="js">tamings___</exports>
+    </api>
+  </all>
+</feature>
\ No newline at end of file
diff --git a/trunk/features/src/main/javascript/features/taming/taming.js b/trunk/features/src/main/javascript/features/taming/taming.js
new file mode 100644
index 0000000..4b3221d
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/taming/taming.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @namespace The global safeJSON namespace
+ * @type {Object}
+ */
+var safeJSON = window['safeJSON'];
+
+/**
+ * @namespace The global tamings___ namespace
+ * @type {Array.<Function>}
+ */
+var tamings___ = window['tamings___'] || [];
+
+/**
+ * @namespace The global bridge___ namespace
+ * @type {Object}
+ */
+var bridge___;
+
+/**
+ * @namespace The global caja___ namespace
+ * @type {Object}
+ */
+var caja___ = window['caja___'];
+
+/**
+ * @namespace The global ___ namespace
+ * @type {Object}
+ */
+var ___ = window['___'];
diff --git a/trunk/features/src/main/javascript/features/views/feature.xml b/trunk/features/src/main/javascript/features/views/feature.xml
new file mode 100644
index 0000000..08f9a3c
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/views/feature.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+<!--
+Required configuration:
+A map of view names to view attributes. Examples:
+
+
+-->
+  <name>views</name>
+  <dependency>globals</dependency>
+  <dependency>taming</dependency>
+  <dependency>core.config</dependency>
+  <dependency>core.json</dependency>
+  <dependency>core.util</dependency>
+  <dependency>rpc</dependency>
+  <dependency>domnode</dependency>
+  <gadget>
+    <script src="views.js"/>
+    <script src="taming.js" caja="1"/>
+    <api>
+      <exports type="js">gadgets.views.bind</exports>
+      <exports type="js">gadgets.views.requestNavigateTo</exports>
+      <exports type="js">gadgets.views.getCurrentView</exports>
+      <exports type="js">gadgets.views.getSupportedViews</exports>
+      <exports type="js">gadgets.views.getParams</exports>
+      <exports type="js">gadgets.views.ViewType</exports>
+      <exports type="js">gadgets.views.View</exports>
+      <exports type="js">gadgets.views.View.prototype.getName</exports>
+      <exports type="js">gadgets.views.View.prototype.getUrlTemplate</exports>
+      <exports type="js">gadgets.views.View.prototype.bind</exports>
+      <exports type="js">gadgets.views.View.prototype.isOnlyVisibleGadget</exports>
+      <uses type="rpc">requestNavigateTo</uses>
+    </api>
+  </gadget>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/views/taming.js b/trunk/features/src/main/javascript/features/views/taming.js
new file mode 100644
index 0000000..69768e6
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/views/taming.js
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @class
+ * Tame and expose core gadgets.views.* API to cajoled gadgets
+ */
+tamings___.push(function(imports) {
+  caja___.whitelistCtors([
+    [gadgets.views, 'View', Object]
+  ]);
+  caja___.whitelistMeths([
+    [gadgets.views.View, 'bind'],
+    [gadgets.views.View, 'getUrlTemplate'],
+    [gadgets.views.View, 'isOnlyVisibleGadget'],
+    [gadgets.views.View, 'getName']
+  ]);
+  caja___.whitelistFuncs([
+    [gadgets.views, 'getCurrentView'],
+    [gadgets.views, 'getParams'],
+    [gadgets.views, 'requestNavigateTo']
+  ]);
+});
diff --git a/trunk/features/src/main/javascript/features/views/views.js b/trunk/features/src/main/javascript/features/views/views.js
new file mode 100644
index 0000000..8378285
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/views/views.js
@@ -0,0 +1,401 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview This library provides functions for navigating to and dealing
+ *     with views of the current gadget.
+ */
+
+/**
+ * Implements the gadgets.views API spec. See
+ * http://code.google.com/apis/gadgets/docs/reference/gadgets.views.html
+ */
+gadgets.views = function() {
+
+  /**
+   * all view constants
+   */
+  var _viewType = {};
+
+  /**
+   * Reference to the current view object.
+   */
+  var currentView = null;
+
+  /**
+   * Map of all supported views for this container.
+   */
+  var supportedViews = {};
+
+  /**
+   * Map of parameters passed to the current request.
+   */
+  var params = {};
+
+  /**
+   * Forces navigation via requestNavigateTo.
+   */
+  function forceNavigate(e) {
+    if (!e) {
+      e = window.event;
+    }
+
+    var target;
+    if (e.target) {
+      target = e.target;
+    } else if (e.srcElement) {
+      target = e.srcElement;
+    }
+
+    if (target.nodeType === DOM_TEXT_NODE) {
+      target = target.parentNode;
+    }
+
+    if (target.nodeName.toLowerCase() === 'a') {
+      // We use getAttribute() instead of .href to avoid automatic relative path resolution.
+      var href = target.getAttribute('href');
+      if (href && href[0] !== '#' && href.indexOf('://') === -1) {
+        gadgets.views.requestNavigateTo(currentView, href);
+        if (e.stopPropagation) {
+          e.stopPropagation();
+        }
+        if (e.preventDefault) {
+          e.preventDefault();
+        }
+        e.returnValue = false;
+        e.cancelBubble = true;
+        return false;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * Initializes views. Assumes that the current view is the "view"
+   * url parameter (or default if "view" isn't supported), and that
+   * all view parameters are in the form view-<name>
+   * TODO: Use unified configuration when it becomes available.
+   *
+   */
+  function init(config) {
+    var conf = config['views'] || {};
+    for (var s in conf) {
+      if (conf.hasOwnProperty(s)) {
+        // TODO: Fix this by moving view names / config into a sub property.
+        if (s != 'rewriteLinks') {
+          var obj = conf[s];
+          var constantName = s.toUpperCase();
+          _viewType[constantName] = constantName;
+          if (!obj) {
+            continue;
+          }
+          supportedViews[s] = new gadgets.views.View(s, obj.isOnlyVisible);
+          var aliases = obj['aliases'] || [];
+          for (var i = 0, alias; (alias = aliases[i]); ++i) {
+            supportedViews[alias] = new gadgets.views.View(s, obj['isOnlyVisible']);
+          }
+        }
+      }
+    }
+
+    var urlParams = gadgets.util.getUrlParameters();
+    // View parameters are passed as a single parameter.
+    if (urlParams['view-params']) {
+      params = gadgets.json.parse(urlParams['view-params']) || params;
+    }
+
+    var viewName = urlParams['view'] || "";
+    // check for subview
+    var viewNameArray = viewName.split(".");
+    if(viewNameArray.length > 1) {
+      var viewNameMajor = viewNameArray[0];
+      if (supportedViews.hasOwnProperty(viewNameMajor)) {
+        var viewMajor = supportedViews[viewNameMajor];
+        currentView = new gadgets.views.View(viewName, viewMajor.isOnlyVisibleGadget());
+      } else {
+        currentView = supportedViews['default'];
+      }
+    } else {
+      currentView = supportedViews[viewName] || supportedViews['default'];
+    }
+
+    if (conf.rewriteLinks) {
+      gadgets.util.attachBrowserEvent(document, 'click', forceNavigate, false);
+    }
+  }
+
+  gadgets.config.register('views', null, init);
+
+  return {
+
+    /**
+     * Binds a URL template with variables in the passed environment
+     * to produce a URL string.
+     *
+     * The URL template conforms to the IETF draft spec:
+     * http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html
+     *
+     * @param {string} urlTemplate A URL template for a container view.
+     * @param {Object.<string, string>} environment A set of named variables.
+     * @return {string} A URL string with substituted variables.
+     */
+    bind: function(urlTemplate, environment) {
+      if (typeof urlTemplate !== 'string') {
+        throw new Error('Invalid urlTemplate');
+      }
+
+      if (typeof environment !== 'object') {
+        throw new Error('Invalid environment');
+      }
+
+      var varRE = /^([a-zA-Z0-9][a-zA-Z0-9_\.\-]*)(=([a-zA-Z0-9\-\._~]|(%[0-9a-fA-F]{2}))*)?$/,
+          expansionRE = new RegExp('\\{([^}]*)\\}', 'g'),
+          opRE = /^-([a-zA-Z]+)\|([^|]*)\|(.+)$/,
+          result = [],
+          textStart = 0,
+          group,
+          match,
+          varName,
+          defaultValue,
+          op,
+          arg,
+          vars,
+          flag;
+
+      /**
+       * @param {string} varName
+       * @param {string=} defaultVal
+       */
+      function getVar(varName, defaultVal) {
+        return environment.hasOwnProperty(varName) ?
+               environment[varName] : defaultVal;
+      }
+
+      function matchVar(v) {
+        if (!(match = v.match(varRE))) {
+          throw new Error('Invalid variable : ' + v);
+        }
+      }
+
+      function matchVars(vs, j, map) {
+        var i, va = vs.split(',');
+        for (i = 0; i < va.length; ++i) {
+          matchVar(va[i]);
+          if (map(j, getVar(match[1]), match[1])) {
+            break;
+          }
+        }
+        return j;
+      }
+
+      function objectIsEmpty(v) {
+        if ((typeof v === 'object') || (typeof v === 'function')) {
+          for (var i in v) {
+            if (v.hasOwnProperty(i)) {
+              return false;
+            }
+          }
+          return true;
+        }
+        return false;
+      }
+
+      while ((group = expansionRE.exec(urlTemplate))) {
+        result.push(urlTemplate.substring(textStart, group.index));
+        textStart = expansionRE.lastIndex;
+        if ((match = group[1].match(varRE))) {
+          varName = match[1];
+          defaultValue = match[2] ? match[2].substr(1) : '';
+          result.push(getVar(varName, defaultValue));
+        } else {
+          if ((match = group[1].match(opRE))) {
+            op = match[1];
+            arg = match[2];
+            vars = match[3];
+            flag = 0;
+            switch (op) {
+              case 'neg':
+                flag = 1;
+              case 'opt':
+                if (matchVars(vars, {flag: flag}, function(j, v) {
+                  if (typeof v !== 'undefined' && !objectIsEmpty(v)) {
+                    j.flag = !j.flag;
+                    return 1;
+                  }
+                  return 0;
+                }).flag) {
+                  result.push(arg);
+                }
+                break;
+              case 'join':
+                result.push(matchVars(vars, [], function(j, v, k) {
+                  if (typeof v === 'string') {
+                    j.push(k + '=' + v);
+                  } else if (typeof v === 'object') {
+                    for (var i in v) {
+                      if (v.hasOwnProperty(i)) {
+                        j.push(i + '=' + v[i]);
+                      }
+                    }
+                  }
+                }).join(arg));
+                break;
+              case 'list':
+                matchVar(vars);
+                var value = getVar(match[1]);
+                if (typeof value === 'object' && typeof value.join === 'function') {
+                  result.push(value.join(arg));
+                }
+                break;
+              case 'prefix':
+                flag = 1;
+              case 'suffix':
+                matchVar(vars);
+                value = getVar(match[1], match[2] && match[2].substr(1));
+                if (typeof value === 'string') {
+                  result.push(flag ? arg + value : value + arg);
+                } else if (typeof value === 'object' && typeof value.join === 'function') {
+                  result.push(flag ? arg + value.join(arg) : value.join(arg) + arg);
+                }
+                break;
+              default:
+                throw new Error('Invalid operator : ' + op);
+            }
+          } else {
+            throw new Error('Invalid syntax : ' + group[0]);
+          }
+        }
+      }
+
+      result.push(urlTemplate.substr(textStart));
+
+      return result.join('');
+    },
+
+    /**
+     * Attempts to navigate to this gadget in a different view. If the container
+     * supports parameters will pass the optional parameters along to the gadget
+     * in the new view.
+     *
+     * @param {string | gadgets.views.View} view The view to navigate to.
+     * @param {Object.<string, string>=} opt_params Parameters to pass to the
+     *     gadget after it has been navigated to on the surface.
+     * @param {string=} opt_ownerId The ID of the owner of the page to navigate to;
+     *                 defaults to the current owner.
+     */
+    requestNavigateTo: function(view, opt_params, opt_ownerId) {
+      if (typeof view !== 'string') {
+        view = view.getName();
+      }
+      // TODO If we want to implement a POPUP view we'll have to do it here,
+      // The parent frame's attempts to use window.open will fail since it's not
+      // directly initiated from the onclick handler
+      gadgets.rpc.call(null, 'requestNavigateTo', null, view, opt_params, opt_ownerId);
+    },
+
+    /**
+     * Returns the current view.
+     *
+     * @return {gadgets.views.View} The current view.
+     */
+    getCurrentView: function() {
+      return currentView;
+    },
+
+    /**
+     * Returns a map of all the supported views. Keys each gadgets.view.View by
+     * its name.
+     *
+     * @return {Object.<gadgets.views.ViewType | string, gadgets.views.View>}
+     *   All supported views, keyed by their name attribute.
+     */
+    getSupportedViews: function() {
+      return supportedViews;
+    },
+
+    /**
+     * Returns the parameters passed into this gadget for this view. Does not
+     * include all url parameters, only the ones passed into
+     * gadgets.views.requestNavigateTo
+     *
+     * @return {Object.<string, string>} The parameter map.
+     */
+    getParams: function() {
+      return params;
+    },
+
+    ViewType: _viewType
+  };
+}();
+
+
+/**
+ * @class
+ * View Class
+ * @name gadgets.views.View
+ */
+
+/**
+ * View Representation
+ * @constructor
+ * @param {string} name - the name of the view.
+ * @param {boolean=} opt_isOnlyVisible - is this view devoted to this gadget.
+ */
+
+gadgets.views.View = function(name, opt_isOnlyVisible) {
+  this.name_ = name;
+  this.isOnlyVisible_ = !!opt_isOnlyVisible;
+};
+
+/**
+ * @return {string} The view name.
+ */
+gadgets.views.View.prototype.getName = function() {
+  return this.name_;
+};
+
+/**
+ * Returns the associated URL template of the view.
+ * The URL template conforms to the IETF draft spec:
+ * http://bitworking.org/projects/URI-Templates/spec/draft-gregorio-uritemplate-03.html
+ * @return {string} A URL template.
+ */
+gadgets.views.View.prototype.getUrlTemplate = function() {
+  return gadgets.config &&
+         gadgets.config.views &&
+         gadgets.config.views[this.name_] &&
+         gadgets.config.views[this.name_].urlTemplate;
+};
+
+/**
+ * Binds the view's URL template with variables in the passed environment
+ * to produce a URL string.
+ * @param {Object.<string, string>} environment A set of named variables.
+ * @return {string} A URL string with substituted variables.
+ */
+gadgets.views.View.prototype.bind = function(environment) {
+  return gadgets.views.bind(this.getUrlTemplate(), environment);
+};
+
+/**
+ * @return {boolean} True if this is the only visible gadget on the page.
+ */
+gadgets.views.View.prototype.isOnlyVisibleGadget = function() {
+  return this.isOnlyVisible_;
+};
diff --git a/trunk/features/src/main/javascript/features/xhrwrapper/feature.xml b/trunk/features/src/main/javascript/features/xhrwrapper/feature.xml
new file mode 100644
index 0000000..7848e76
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/xhrwrapper/feature.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you 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.
+-->
+<feature>
+  <name>xhrwrapper</name>
+  <!-- deprecated usage -->
+  <dependency>shindig.xhrwrapper</dependency>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/xmlutil/feature.xml b/trunk/features/src/main/javascript/features/xmlutil/feature.xml
new file mode 100644
index 0000000..de3514b
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/xmlutil/feature.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<feature>
+  <name>xmlutil</name>
+  <dependency>globals</dependency>
+  <all>
+    <script src="xmlutil.js"></script>
+    <api>
+      <exports type="js">opensocial.xmlutil.parseXML</exports>
+      <exports type="js">opensocial.xmlutil.NSMAP</exports>
+      <exports type="js">opensocial.xmlutil.getRequiredNamespaces</exports>
+      <exports type="js">opensocial.xmlutil.ENTITIES</exports>
+      <exports type="js">opensocial.xmlutil.prepareXML</exports>
+    </api>
+  </all>
+</feature>
diff --git a/trunk/features/src/main/javascript/features/xmlutil/xmlutil.js b/trunk/features/src/main/javascript/features/xmlutil/xmlutil.js
new file mode 100644
index 0000000..3e94560
--- /dev/null
+++ b/trunk/features/src/main/javascript/features/xmlutil/xmlutil.js
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+var opensocial = opensocial || {};
+
+opensocial.xmlutil = opensocial.xmlutil || {};
+
+/**
+ * Cached DOMParser objects on browsers that support it.
+ * @private
+ */
+opensocial.xmlutil.parser_ = null;
+
+
+/**
+ * Parses an XML string into an XML Document.
+ * @param {string} str A string of well-formed XML.
+ * @return {Document} XML document.
+ */
+opensocial.xmlutil.parseXML = function(str) {
+  if (typeof(DOMParser) != 'undefined') {
+    opensocial.xmlutil.parser_ = opensocial.xmlutil.parser_ || new DOMParser();
+    var doc = opensocial.xmlutil.parser_.parseFromString(str, 'text/xml');
+    if (doc.firstChild && doc.firstChild.tagName == 'parsererror') {
+      throw Error(doc.firstChild.firstChild.nodeValue);
+    }
+    return doc;
+  } else if (typeof(ActiveXObject) != 'undefined') {
+    var doc = new ActiveXObject('MSXML2.DomDocument');
+    doc.validateOnParse = false;
+    doc.loadXML(str);
+    if (doc.parseError && doc.parseError.errorCode) {
+      throw Error(doc.parseError.reason);
+    }
+    return doc;
+  }
+  throw Error('No XML parser found in this browser.');
+};
+
+
+/**
+ * Map of Namespace prefixes to their respective URLs.
+ * @type {Object.<string,string>}
+ */
+opensocial.xmlutil.NSMAP = {
+  'os': 'http://opensocial.org/'
+};
+
+
+/**
+ * Returns the XML namespace declarations that need to be injected into a
+ * particular XML-like snippet to make it valid XML. Uses the defined
+ * namespaces to see which are available, and checks that they are used in
+ * the supplied code. An empty string is returned if no injection is needed.
+ *
+ * @param {string} xml XML-like source code.
+ * @param {Element=} opt_container Optional container node to look for namespace
+ * declarations.
+ * @return {string} A string of xmlns delcarations required for this XML.
+ */
+opensocial.xmlutil.getRequiredNamespaces = function(xml, opt_container) {
+  var namespaces = opt_container ?
+      opensocial.xmlutil.getNamespaceDeclarations_(opt_container) : {};
+  for (var prefix in opensocial.xmlutil.NSMAP) {
+    if (opensocial.xmlutil.NSMAP.hasOwnProperty(prefix) &&
+        !namespaces.hasOwnProperty(prefix) &&
+        xml.indexOf('<' + prefix + ':') >= 0 &&
+        xml.indexOf('xmlns:' + prefix + ':') < 0) {
+      namespaces[prefix] = opensocial.xmlutil.NSMAP[prefix];
+    }
+  }
+  return opensocial.xmlutil.serializeNamespaces_(namespaces);
+};
+
+/**
+ * @private
+ * @return {string}
+ */
+opensocial.xmlutil.serializeNamespaces_ = function(namespaces) {
+  var buffer = [];
+  for (var prefix in namespaces) {
+    if (namespaces.hasOwnProperty(prefix)) {
+      buffer.push(' xmlns:', prefix, '=\"', namespaces[prefix], '\"');
+    }
+  }
+  return buffer.join('');
+};
+
+
+/**
+ * Returns a map of XML namespaces declared on an DOM Element.
+ * @param {Element} el The Element to inspect.
+ * @return {Object.<string, string>} A Map of keyed by prefix of declared
+ *     namespaces.
+ * @private
+ */
+opensocial.xmlutil.getNamespaceDeclarations_ = function(el) {
+  var namespaces = {};
+  for (var i = 0; i < el.attributes.length; i++) {
+    var name = el.attributes[i].nodeName;
+    if (name.substring(0, 6) != 'xmlns:') {
+      continue;
+    }
+    namespaces[name.substring(6, name.length)] = el.getAttribute(name);
+  }
+  return namespaces;
+};
+
+
+/**
+ * XHTML Entities we need to support in XML, definted in DOCTYPE declaration.
+ *
+ * TODO: A better way to do this.
+ */
+opensocial.xmlutil.ENTITIES = '<!ENTITY nbsp \"&#160;\">';
+
+
+/**
+ * Prepares an XML-like string to be parsed by browser parser. Injects a DOCTYPE
+ * with entities and a top-level <root> element to encapsulate the code.
+ * @param {string} xml XML string to be prepared.
+ * @param {Element=} opt_container Optional container Element with namespace
+ * declarations.
+ * @return {string} XML string prepared for client-side parsing.
+ */
+opensocial.xmlutil.prepareXML = function(xml, opt_container) {
+  var namespaces = opensocial.xmlutil.getRequiredNamespaces(xml, opt_container);
+  return '<!DOCTYPE root [' + opensocial.xmlutil.ENTITIES +
+      ']><root xml:space=\"preserve\"' +
+      namespaces + '>' + xml + '</root>';
+};
diff --git a/trunk/features/src/site/site.xml b/trunk/features/src/site/site.xml
new file mode 100644
index 0000000..1ce5fd3
--- /dev/null
+++ b/trunk/features/src/site/site.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+
+<project xmlns="http://maven.apache.org/DECORATION/1.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/DECORATION/1.0.0 http://maven.apache.org/xsd/decoration-1.0.0.xsd"
+  name="Apache Shindig Features">
+
+  <body>
+    <menu ref="parent"/>
+
+    <menu name="JavaScriptDoc">
+      <item name="Index" href="jsdoc/index.html"/>
+    </menu>
+
+    <menu ref="reports" inherit="bottom" />
+  </body>
+</project>
diff --git a/trunk/features/src/test/javascript/features/actions/actions_test.js b/trunk/features/src/test/javascript/features/actions/actions_test.js
new file mode 100644
index 0000000..74ce764
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/actions/actions_test.js
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Tests for actions feature
+ */
+
+function DeclarativeActionsTest(name) {
+  TestCase.call(this, name);
+}
+
+DeclarativeActionsTest.inherits(TestCase);
+
+(function() {
+
+  DeclarativeActionsTest.prototype.setUp = function() {
+    this.apiUri = window.__API_URI;
+    window.__API_URI = shindig.uri('http://shindig.com');
+    this.containerUri = window.__CONTAINER_URI;
+    window.__CONTAINER_URI = shindig.uri('http://container.com');
+
+    this.gadgetsRpc = gadgets.rpc;
+    var self = this;
+    gadgets.rpc = {};
+    gadgets.rpc.register = function(service, callback) {
+      if (self.captures && self.captures.hasOwnProperty(service)) {
+        self.captures[service] = callback;
+      }
+    };
+    gadgets.rpc.call = function() {
+      self.rpcArguments = Array.prototype.slice.call(arguments);
+    };
+  };
+
+  DeclarativeActionsTest.prototype.tearDown = function() {
+    window.__API_URI = this.apiUri;
+    window.__CONTAINER_URI = this.containerUri;
+
+    gadgets.rpc = this.gadgetsRpc;
+    delete this.rpcArguments;
+    delete this.captures;
+  };
+
+  DeclarativeActionsTest.prototype.testGadgetsAddAction = function() {
+    var actionId = "testAction";
+    var callbackFn = function(){};
+    var _actionObj = {
+        id: actionId,
+        label:"Test Action",
+        path:"container/navigationLinks",
+        callback: callbackFn
+      };
+    gadgets.actions.addAction(_actionObj);
+    this.assertRpcCalled('..', 'actions.bindAction', null, _actionObj);
+  };
+
+  DeclarativeActionsTest.prototype.testGadgetsRemoveAction = function() {
+    var actionId = "testAction";
+    gadgets.actions.removeAction(actionId);
+    this.assertRpcCalled('..', 'actions.removeAction', null, actionId);
+  };
+
+  DeclarativeActionsTest.prototype.testGadgetsRunAction = function() {
+    var actionId = "testAction";
+    var opt_selection = "testSelection";
+    gadgets.actions.runAction(actionId, opt_selection);
+    this.assertRpcCalled('..', 'actions.runAction', null,
+      actionId, opt_selection
+    );
+  };
+
+
+  DeclarativeActionsTest.prototype.testContainerGetAction = function() {
+    var container = new osapi.container.Container({});
+    var actionId = "testAction";
+    var actionObj = container.actions.getAction(actionId);
+    // registry is empty
+    this.assertUndefined(actionObj);
+  };
+
+
+  DeclarativeActionsTest.prototype.testContainerGetActionsByPath = function() {
+    var container = new osapi.container.Container();
+    var actionId = "testAction";
+    var actionsArray = container.actions
+      .getActionsByPath("container/navigationLinks");
+    //registry is empty
+    this.assertEquals(actionsArray, []);
+  };
+
+  DeclarativeActionsTest.prototype.testContainerGetActionsByDataType = function() {
+    var container = new osapi.container.Container();
+    var actionId = "testAction";
+    var actionsArray = container.actions.getActionsByDataType("opensocial.Person");
+    // registry is empty
+    this.assertEquals(actionsArray, []);
+  };
+
+  DeclarativeActionsTest.prototype.testBindInvalidAction = function() {
+    var undef, captures = this.captures = {
+      'actions.bindAction': undef
+    };
+
+    var container = new osapi.container.Container(),
+        showActionCalled = false,
+        actionId = 'testAction',
+        actionObj = {
+          id: actionId
+        };
+
+    this.assertNotUndefined('RPC endpoint "actions.bindAction" was not registered.', captures['actions.bindAction']);
+    container.actions.registerShowActionsHandler(function() {
+      showActionCalled = true;
+    });
+    captures['actions.bindAction'](actionObj);
+
+    this.assertUndefined(container.actions.getAction(actionId));
+    this.assertFalse(showActionCalled);
+  };
+
+  DeclarativeActionsTest.prototype.testContainerGetAction_Full = function() {
+    var undef, captures = this.captures = {
+      'actions.bindAction': undef,
+      'actions.removeAction': undef
+    };
+
+    var container = new osapi.container.Container({}),
+        actionId = "testAction",
+        actionObj = {
+          id: actionId,
+          label: "Test Action",
+          path: "container/navigationLinks"
+        };
+
+    this.assertNotUndefined('RPC endpoint "actions.bindAction" was not registered.', captures['actions.bindAction']);
+    this.assertNotUndefined('RPC endpoint "actions.removeAction" was not registered.', captures['actions.removeAction']);
+    captures['actions.bindAction'](actionObj);
+
+    this.assertEquals(actionObj, container.actions.getAction(actionId));
+
+    captures['actions.removeAction'](actionId);
+    this.assertUndefined(container.actions.getAction(actionId));
+  };
+
+
+  DeclarativeActionsTest.prototype.testContainerGetActions_Full = function() {
+    var undef, captures = this.captures = {
+      'actions.bindAction': undef,
+      'actions.removeAction': undef
+    };
+
+    var container = new osapi.container.Container({}),
+        actions = [
+          { id: "test1", label: "Test Action1", path: "container/navigationLinks" },
+          { id: "test2", label: "Test Action2", path: "container/navigationLinks" },
+          { id: "test3", label: "Test Action3", dataType: "opensocial.Person" },
+          { id: "test4", label: "Test Action4", dataType: "opensocial.Person" }
+        ];
+    this.assertNotUndefined('RPC endpoint "actions.bindAction" was not registered.', captures['actions.bindAction']);
+    this.assertNotUndefined('RPC endpoint "actions.removeAction" was not registered.', captures['actions.removeAction']);
+
+    for (var i = 0; i < actions.length; i++) {
+      captures['actions.bindAction'](actions[i]);
+    }
+    this.assertEquals(actions, container.actions.getAllActions());
+
+    for (var i = 0; i < actions.length; i++) {
+      captures['actions.removeAction'](actions[i].id);
+    }
+    this.assertEquals([], container.actions.getAllActions());
+
+  };
+
+
+  DeclarativeActionsTest.prototype.testContainerGetActionsByPath_Full = function() {
+    var undef, captures = this.captures = {
+      'actions.bindAction': undef,
+      'actions.removeAction': undef
+    };
+
+    var container = new osapi.container.Container(),
+        actionObj = {
+          id: "testAction",
+          label: "Test Action",
+          path: "container/navigationLinks"
+        };
+    this.assertNotUndefined('RPC endpoint "actions.bindAction" was not registered.', captures['actions.bindAction']);
+    this.assertNotUndefined('RPC endpoint "actions.removeAction" was not registered.', captures['actions.removeAction']);
+
+    captures['actions.bindAction'](actionObj);
+    this.assertEquals([actionObj], container.actions.getActionsByPath("container/navigationLinks"));
+
+    captures['actions.removeAction'](actionObj.id);
+    this.assertEquals([], container.actions.getActionsByPath("container/navigationLinks"));
+  };
+
+  DeclarativeActionsTest.prototype.testContainerGetActionsByDataType_Full = function() {
+    var undef, captures = this.captures = {
+      'actions.bindAction': undef,
+      'actions.removeAction': undef
+    };
+
+    var container = new osapi.container.Container();
+        actionObj = {
+          id: "testAction",
+          label: "Test Action",
+          dataType: "opensocial.Person"
+        };
+    this.assertNotUndefined('RPC endpoint "actions.bindAction" was not registered.', captures['actions.bindAction']);
+    this.assertNotUndefined('RPC endpoint "actions.removeAction" was not registered.', captures['actions.removeAction']);
+
+    captures['actions.bindAction'](actionObj);
+    this.assertEquals([actionObj], container.actions.getActionsByDataType("opensocial.Person"));
+
+    captures['actions.removeAction'](actionObj.id);
+    this.assertEquals([], container.actions.getActionsByDataType("opensocial.Person"));
+  };
+
+  /**
+   * Asserts gadgets.rpc.call() is called with the expected arguments given.
+   */
+  DeclarativeActionsTest.prototype.assertRpcCalled = function() {
+    this.assertNotUndefined("RPC was not called.", this.rpcArguments);
+    this.assertEquals("RPC argument list not valid length.", arguments.length,
+        this.rpcArguments.length);
+
+    for ( var i = 0; i < arguments.length; i++) {
+      this.assertEquals(arguments[i], this.rpcArguments[i]);
+    }
+    this.rpcArguments = undefined;
+  };
+})();
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/alltests.js b/trunk/features/src/test/javascript/features/alltests.js
new file mode 100644
index 0000000..fb8a845
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/alltests.js
@@ -0,0 +1,174 @@
+/*
+JsUnit - a JUnit port for JavaScript
+Copyright (C) 1999,2000,2001,2002,2003,2006 Joerg Schaible
+
+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.
+*/
+// util for our unit tests
+// the runner runs from the features directory. These are all relative to that.
+// TODO: figure out how to set the working directory in Rhino.
+
+var testSrcDir = "src/test/javascript/features";
+var testToolsDir = "src/test/javascript/lib";
+var srcDir = "src/main/javascript/features";
+
+if (!this.JsUtil) {
+  if (this.WScript) {
+    var fso = new ActiveXObject("Scripting.FileSystemObject");
+    var file = fso.OpenTextFile(testToolsDir + "/JsUtil.js", 1);
+    var all = file.ReadAll();
+    file.Close();
+    eval( all );
+  } else {
+    load(testToolsDir + "/JsUtil.js");
+  }
+
+  eval(JsUtil.prototype.include(testSrcDir + '/mocks/env.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/mocks/window.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/mocks/xhr.js'));
+  eval(JsUtil.prototype.include(srcDir + '/globals/globals.js'));
+  eval(JsUtil.prototype.include(srcDir + '/cloo/cloo.js'));
+  eval(JsUtil.prototype.include(srcDir + '/domnode/constants.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.config.base/config.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.config/validators.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.json/json-native.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.json/json-jsimpl.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.json/json-flatten.js'));
+  eval(JsUtil.prototype.include(srcDir + '/shindig.auth/auth.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.util.dom/dom.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.util.string/string.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.util.urlparams/urlparams.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.util/util.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.prefs/prefs.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.log/log.js'));
+  eval(JsUtil.prototype.include(srcDir + '/core.io/io.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.util/constant.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.util/util.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container/service.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.site/site.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.site/site_holder.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.site.gadget/gadget_holder.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.site.gadget/gadget_site.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.site.url/url_holder.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container.site.url/url_site.js'));
+  eval(JsUtil.prototype.include(srcDir + '/container/container.js'));
+  eval(JsUtil.prototype.include(srcDir + '/i18n/currencycodemap.js'));
+  eval(JsUtil.prototype.include(srcDir + '/i18n/datetimeformat.js'));
+  eval(JsUtil.prototype.include(srcDir + '/i18n/datetimeparse.js'));
+  eval(JsUtil.prototype.include(srcDir + '/i18n/formatting.js'));
+  eval(JsUtil.prototype.include(srcDir + '/i18n/numberformat.js'));
+  eval(JsUtil.prototype.include(srcDir + '/setprefs/setprefs.js'));
+  eval(JsUtil.prototype.include(srcDir + '/views/views.js'));
+  eval(JsUtil.prototype.include(srcDir + '/shindig.uri/uri.js'));
+  eval(JsUtil.prototype.include(srcDir + '/shindig.xhrwrapper/xhrwrapper.js'));
+  eval(JsUtil.prototype.include(srcDir + '/xmlutil/xmlutil.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-data-context/datacontext.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-data/data.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/opensocial.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/activity.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/address.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/album.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/bodytype.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/collection.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/container.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/datarequest.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/dataresponse.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/email.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/enum.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/environment.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/idspec.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/mediaitem.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/message.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/name.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/navigationparameters.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/organization.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/person.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/phone.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/responseitem.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-reference/url.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-base/fieldtranslations.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-base/jsonactivity.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-base/jsonalbum.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-base/jsonmediaitem.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-base/jsonperson.js'));
+  eval(JsUtil.prototype.include(srcDir + '/opensocial-jsonrpc/jsonrpccontainer.js'));
+  eval(JsUtil.prototype.include(srcDir + '/osapi.base/osapi.js'));
+  eval(JsUtil.prototype.include(srcDir + '/osapi.base/batch.js'));
+  eval(JsUtil.prototype.include(srcDir + '/osapi/jsonrpctransport.js'));
+  eval(JsUtil.prototype.include(srcDir + '/osapi/peoplehelpers.js'));
+  eval(JsUtil.prototype.include(srcDir + '/gadgets.json.ext/json-xmltojson.js;));
+  eval(JsUtil.prototype.include(testToolsDir + "/JsUnit.js"));
+  eval(JsUtil.prototype.include(testToolsDir + '/testutils.js'));
+  eval(JsUtil.prototype.include(testSrcDir + "/core/authtest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/core/config-test.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/core/prefstest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/core.io/iotest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-base/jsonactivitytest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-base/jsonalbumtest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-base/jsonmediaitemtest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-reference/activitytest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-templates/compiler_test.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-templates/container_test.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-templates/loader_test.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-templates/os_test.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-templates/template_test.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/opensocial-templates/util_test.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/osapi/osapitest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/osapi/batchtest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/osapi/jsonrpctransporttest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/views/urltemplatetest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + "/xhrwrapper/xhrwrappertest.js"));
+  eval(JsUtil.prototype.include(testSrcDir + '/shindig.uri/uritest.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/container/util_test.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/container/service_test.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/container/gadget_holder_test.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/container/gadget_site_test.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/container/container_test.js'));
+  eval(JsUtil.prototype.include(testSrcDir + '/json-xmltojson/jsonxmltojson-test.js'));
+}
+
+function AllTests() {
+  TestSuite.call(this, "AllTests");
+}
+
+AllTests.inherits(TestSuite);
+
+
+function AllTests_suite() {
+  var suite = new AllTests();
+  suite.addTest(JsonRpcTransportTestSuite.prototype.suite());
+  suite.addTest(BatchTestSuite.prototype.suite());
+  return suite;
+}
+
+
+AllTests.prototype = new TestSuite();
+AllTests.prototype.suite = AllTests_suite;
+AllTests.glue();
+
+var args;
+if (this.WScript) {
+  args = new Array();
+  for (var i = 0; i < WScript.Arguments.Count(); ++i) {
+    args[i] = WScript.Arguments(i);
+  }
+} else if (this.arguments) {
+  args = arguments;
+} else {
+  args = new Array();
+  args.push("AllTests");
+}
+
+
+var result = TextTestRunner.prototype.main(args);
+JsUtil.prototype.quit(result);
diff --git a/trunk/features/src/test/javascript/features/container.url/container_url_test.js b/trunk/features/src/test/javascript/features/container.url/container_url_test.js
new file mode 100644
index 0000000..d783349
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container.url/container_url_test.js
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Tests for container APIs for URL sites.
+ */
+
+function UrlContainerTest(name) {
+  TestCase.call(this, name);
+}
+
+UrlContainerTest.inherits(TestCase);
+UrlContainerTest.prototype.setUp = function() {
+  this.apiUri = window.__API_URI;
+  window.__API_URI = shindig.uri('http://shindig.com');
+  this.containerUri = window.__CONTAINER_URI;
+  window.__CONTAINER_URI = shindig.uri('http://container.com');
+  this.shindigContainerGadgetSite = osapi.container.GadgetSite;
+  this.shindigContainerUrlSite = osapi.container.UrlSite;
+  this.gadgetsRpc = gadgets.rpc;
+};
+
+UrlContainerTest.prototype.tearDown = function() {
+  window.__API_URI = this.apiUri;
+  window.__CONTAINER_URI = this.containerUri;
+  osapi.container.GadgetSite = this.shindigContainerGadgetSite;
+  osapi.container.UrlSite = this.shindigContainerUrlSite;
+  gadgets.rpc = this.gadgetsRpc;
+};
+
+UrlContainerTest.prototype.testNewUrlSite = function() {
+  this.setupGadgetsRpcRegister();
+  this.setupUrlSite(5, "http://example.com", null);
+  var container = new osapi.container.Container();
+  var site = container.newUrlSite({});
+  this.assertEquals(5, site.getId());
+};
+
+UrlContainerTest.prototype.testNavigateUrl = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container({
+    'allowDefaultView' : true,
+    'renderCajole' : true,
+    'renderDebug' : true,
+    'renderTest' : true
+  });
+
+  this.setupUrlSite(2, "http://example.com", null);
+  container.navigateUrl(osapi.container.UrlSite(), "http://example.com/index.html", {});
+  this.assertEquals("http://example.com/index.html", this.urlsite_render_url);
+};
+
+UrlContainerTest.prototype.setupGadgetsRpcRegister = function() {
+  gadgets.rpc = {
+    register : function() {}
+  };
+};
+
+UrlContainerTest.prototype.setupUrlSite = function(id, url, urlHolder) {
+  var self = this;
+  osapi.container.UrlSite = function() {
+    return {
+      "getId" : function() {
+        return id;
+      },
+      "render" : function(url, renderParams) {
+        self.urlsite_render_url = url;
+        self.urlsite_render_renderParams = renderParams;
+      }
+    };
+  };
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/container.url/url_holder_test.js b/trunk/features/src/test/javascript/features/container.url/url_holder_test.js
new file mode 100644
index 0000000..f594aad
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container.url/url_holder_test.js
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Tests for the containers URL holder.
+ */
+
+function UrlHolderTest(name) {
+  TestCase.call(this, name);
+}
+
+UrlHolderTest.inherits(TestCase);
+UrlHolderTest.prototype.setUp = function() {
+
+};
+
+UrlHolderTest.prototype.tearDown = function() {
+
+};
+
+UrlHolderTest.prototype.testNew = function() {
+  var element = {};
+  var holder = new osapi.container.UrlHolder({getId: function(){return 123;}}, element);
+  this.assertEquals(element, holder.getElement());
+  this.assertUndefined(holder.getIframeId());
+  this.assertUndefined(holder.getUrl());
+};
+
+UrlHolderTest.prototype.testRenderWithoutParams = function() {
+  var element = {};
+  var url = "http://example.com";
+  var holder = new osapi.container.UrlHolder({getId: function(){return 123;}, getTitle: function(){return "default title"}}, element);
+  this.assertUndefined(holder.getUrl());
+  this.assertUndefined(holder.getIframeId());
+  holder.render(url, {});
+  this.assertEquals('<iframe' + ' marginwidth="0"' + ' hspace="0"' + ' title="default title"' + ' frameborder="0"'
+          + ' scrolling="auto"' + ' marginheight="0"' + ' vspace="0"' + ' id="__url_123"'
+          + ' name="__url_123"' + ' src="http://example.com"' + ' ></iframe>', element.innerHTML);
+  this.assertEquals(url, holder.getUrl());
+  this.assertEquals("__url_123", holder.getIframeId());
+};
+
+UrlHolderTest.prototype.testRenderWithParams = function() {
+  var element = {};
+  var url = "http://example.com";
+  var holder = new osapi.container.UrlHolder({getId: function(){return 123;}, getTitle: function(){return "default title"}}, element);
+  this.assertUndefined(holder.getUrl());
+  this.assertUndefined(holder.getIframeId());
+  holder.render(url, {
+          "class" : "myClass",
+          "width" : 54,
+          "height" : 104
+  });
+  this.assertEquals('<iframe' + ' marginwidth="0"' + ' hspace="0"' + ' height="104"'
+          + ' title="default title"' + ' frameborder="0"' + ' scrolling="auto"' + ' class="myClass"' + ' marginheight="0"'
+          + ' vspace="0"' + ' id="__url_123"' + ' width="54"' + ' name="__url_123"'
+          + ' src="http://example.com"' + ' ></iframe>', element.innerHTML);
+  this.assertEquals(url, holder.getUrl());
+  this.assertEquals("__url_123", holder.getIframeId());
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/container.url/url_site_test.js b/trunk/features/src/test/javascript/features/container.url/url_site_test.js
new file mode 100644
index 0000000..f9e607f
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container.url/url_site_test.js
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Tests for the containers URL site.
+ */
+
+function UrlSiteTest(name) {
+  TestCase.call(this, name);
+}
+
+UrlSiteTest.inherits(TestCase);
+
+UrlSiteTest.prototype.setUp = function() {
+
+};
+
+UrlSiteTest.prototype.tearDown = function() {
+  
+};
+
+UrlSiteTest.prototype.testNew = function() {
+  var args = {
+    "urlEl" : {}
+  };
+  var site = new osapi.container.UrlSite(null, null, args);
+  this.assertEquals(osapi.container.Site.prototype.nextUniqueSiteId_ - 1, site.getId());
+  this.assertTrue(!site.getActiveSiteHolder());
+  var site2 = new osapi.container.UrlSite(null, null, args);
+  this.assertEquals(osapi.container.Site.prototype.nextUniqueSiteId_ - 1, site2.getId());
+  this.assertTrue(!site.getActiveSiteHolder());
+};
+
+UrlSiteTest.prototype.testRenderNoParams = function() {
+  var el = {};
+  var args = {
+    "urlEl" : el
+  };
+  var url = "http://example.com";
+  var site = new osapi.container.UrlSite(null, null, args);
+  site.render(url, {});
+  this.assertNotNull(site.getActiveSiteHolder());
+  this.assertEquals('<iframe' + ' marginwidth="0"' + ' hspace="0"' + ' title="default title"' + ' frameborder="0"'
+          + ' scrolling="auto"' + ' marginheight="0"' + ' vspace="0"' + ' id="__url_' + site.getId() + '"'
+          + ' name="__url_' + site.getId() + '"' + ' src="http://example.com"' + ' ></iframe>', el.innerHTML);
+};
+
+UrlSiteTest.prototype.testRenderWithParams = function() {
+  var el = {};
+  var args = {
+    "urlEl" : el
+  };
+  var url = "http://example.com";
+  var site = new osapi.container.UrlSite(null, null, args);
+  site.render(url, {
+          "class" : "myClass",
+          "width" : 54,
+          "height" : 104
+  });
+  this.assertNotNull(site.getActiveSiteHolder());
+  this.assertEquals('<iframe' + ' marginwidth="0"' + ' hspace="0"' + ' height="104"'
+          + ' title="default title"' + ' frameborder="0"' + ' scrolling="auto"' + ' class="myClass"' + ' marginheight="0"'
+          + ' vspace="0"' + ' id="__url_' + site.getId() + '"' + ' width="54"' + ' name="__url_' + site.getId() + '"'
+          + ' src="http://example.com"' + ' ></iframe>', el.innerHTML);
+};
+
+UrlSiteTest.prototype.testClose = function() {
+  var el = {
+          "firstChild" : "firstChild",
+          "removeChild" : function(child) {
+            this.firstChild = "removedFirstChild"
+          }
+  };
+  var args = {
+    "urlEl" : el
+  };
+  var site = new osapi.container.UrlSite(null, null, args);
+  site.render("http://example.com", {});
+  site.close();
+  this.assertEquals("removedFirstChild", el.firstChild);
+}
+
+UrlSiteTest.prototype.testParentId = function() {
+  var el = {};
+  var args = {
+    "urlEl" : el
+  };
+  var site = new osapi.container.UrlSite(null, null, args);
+  site.setParentId(1);
+  this.assertEquals(1, site.getParentId());
+}
+
+UrlSiteTest.prototype.testSetWidth = function() {
+  var el = {};
+  var args = {
+    "urlEl" : el
+  };
+  var site = new osapi.container.UrlSite(null, null, args);
+  this.assertEquals(site, site.setWidth(50));
+
+  el = {};
+  args.urlEl = el;
+  site = new osapi.container.UrlSite(null, null, args);
+  site.render("http://example.com", {});
+  this.assertEquals(site, site.setWidth(50));
+
+  el = {
+    "firstChild" : null
+  };
+  args.urlEl = el;
+  site = new osapi.container.UrlSite(null, null, args);
+  site.render("http://example.com", {});
+  this.assertEquals(site, site.setWidth(50));
+
+  el = {
+    "firstChild" : {
+      "style" : {
+        "width" : 0
+      }
+    }
+  };
+  args.urlEl = el;
+  site = new osapi.container.UrlSite(null, null, args);
+  site.render("http://example.com", {});
+  site.setWidth(50);
+  this.assertEquals("50px", el.firstChild.style.width);
+}
+
+UrlSiteTest.prototype.testSetHeight = function() {
+  var el = {};
+  var args = {
+    "urlEl" : el
+  };
+  var site = new osapi.container.UrlSite(null, null, args);
+  this.assertEquals(site, site.setHeight(50));
+
+  el = {};
+  args.urlEl = el;
+  site = new osapi.container.UrlSite(null, null, args);
+  site.render("http://example.com", {});
+  this.assertEquals(site, site.setHeight(50));
+
+  el = {
+    "firstChild" : null
+  };
+  args.urlEl = el;
+  site = new osapi.container.UrlSite(null, null, args);
+  site.render("http://example.com", {});
+  this.assertEquals(site, site.setHeight(50));
+
+  el = {
+    "firstChild" : {
+      "style" : {
+        "height" : 0
+      }
+    }
+  };
+  args.urlEl = el;
+  site = new osapi.container.UrlSite(null, null, args);
+  site.render("http://example.com", {});
+  site.setHeight(50);
+  this.assertEquals("50px", el.firstChild.style.height);
+}
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/container/container_test.js b/trunk/features/src/test/javascript/features/container/container_test.js
new file mode 100644
index 0000000..8bc88dd
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container/container_test.js
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the container library.
+ */
+
+function ContainerTest(name) {
+  TestCase.call(this, name);
+}
+
+ContainerTest.inherits(TestCase);
+
+ContainerTest.prototype.setUp = function() {
+  this.apiUri = window.__API_URI;
+  window.__API_URI = shindig.uri('http://shindig.com');
+  this.containerUri = window.__CONTAINER_URI;
+  window.__CONTAINER_URI = shindig.uri('http://container.com');
+  this.shindigContainerGadgetSite = osapi.container.GadgetSite;
+  this.gadgetsRpc = gadgets.rpc;
+  window._setTimeout = window.setTimeout;
+  window.setTimeout = function(fn, time) {};
+  setTimeout = window.setTimeout;
+};
+
+ContainerTest.prototype.tearDown = function() {
+  window.__API_URI = this.apiUri;
+  window.__CONTAINER_URI = this.containerUri;
+  osapi.container.GadgetSite = this.shindigContainerGadgetSite;
+  gadgets.rpc = this.gadgetsRpc;
+  window.setTimeout = window._setTimeout;
+};
+
+ContainerTest.prototype.testUnloadGadget = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container();
+  container.preloadedGadgetUrls_ = {
+    'preloaded1.xml' : {},
+    'preloaded2.xml' : {}
+  };
+  container.unloadGadget('preloaded1.xml');
+  this.assertTrue('1', container.preloadedGadgetUrls_['preloaded1.xml'] == null);
+  this.assertTrue('2', container.preloadedGadgetUrls_['preloaded2.xml'] != null);
+};
+
+ContainerTest.prototype.testUnloadGadgets = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container();
+  container.preloadedGadgetUrls_ = {
+    'preloaded1.xml' : {},
+    'preloaded2.xml' : {},
+    'preloaded3.xml' : {}
+  };
+  container.unloadGadgets(['preloaded1.xml', 'preloaded2.xml']);
+  this.assertTrue('1', container.preloadedGadgetUrls_['preloaded1.xml'] == null);
+  this.assertTrue('2', container.preloadedGadgetUrls_['preloaded2.xml'] == null);
+  this.assertTrue('3', container.preloadedGadgetUrls_['preloaded3.xml'] != null);
+};
+
+ContainerTest.prototype.testPreloadConfigGadgets = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container( 
+    { 'preloadMetadatas' : { 'preloaded1.xml' : {}}});
+  var test = null;
+  this.assertTrue('1', 'preloaded1.xml' in container.preloadedGadgetUrls_);
+  this.assertFalse('2', 'preloaded2.xml' in container.preloadedGadgetUrls_);
+};
+
+ContainerTest.prototype.testPreloadCaches = function() {
+  var self = this;
+  this.setupGadgetsRpcRegister();
+  var mockMetadata = {'preloaded1.xml' : {}};
+  var container = new osapi.container.Container();
+  container.service_.addGadgetMetadatas = function(gadgets, refTime) {
+    self.assertEquals(mockMetadata, gadgets);
+    self.assertNull(refTime);
+  };
+  container.service_.addGadgetTokens = function(tokens, refTime) {
+    self.assertEquals(mockMetadata, tokens);
+    self.assertNull(refTime);
+  };
+  container.addPreloadGadgets_ = function(gadgets) {
+    self.assertEquals(mockMetadata, gadgets);
+  };
+  container.applyLifecycleCallbacks_ = function() {};
+  container.preloadCaches({
+    'preloadMetadatas' : mockMetadata,
+    'preloadTokens' : mockMetadata
+  });
+};
+
+ContainerTest.prototype.testPreloadConfigTokens = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container(
+          { 'preloadMetadatas' : { 'preloaded1.xml' : {'tokenTTL' : 200, 'needsTokenRefresh' : true}},
+            'preloadTokens' : { 'preloaded1.xml' : {'tokenTTL' : 100}}});
+  this.assertEquals(100*1000*0.8, container.tokenRefreshInterval_);
+
+  container = new osapi.container.Container(
+          { 'preloadMetadatas' : { 'preloaded1.xml' : {'tokenTTL' : 200, 'needsTokenRefresh' : true}}});
+  this.assertEquals(200*1000*0.8, container.tokenRefreshInterval_);
+
+  container = new osapi.container.Container(
+          { 'preloadMetadatas' : { 'preloaded1.xml' : {'tokenTTL' : 200, 'needsTokenRefresh' : true}},
+            'preloadTokens' : { 'preloaded1.xml' : {'tokenTTL' : 300}}});
+  this.assertEquals(300*1000*0.8, container.tokenRefreshInterval_);
+};
+
+ContainerTest.prototype.testNavigateGadget = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container({
+    'allowDefaultView' : true,
+    'renderCajole' : true,
+    'renderDebug' : true,
+    'renderTest' : true
+  });
+
+  this.setupGadgetSite(1, {}, null);
+  var site = container.newGadgetSite(null);
+  container.navigateGadget(site, 'gadget.xml', {}, {});
+  this.assertEquals('gadget.xml', this.site_navigateTo_gadgetUrl);
+  this.assertTrue(this.site_navigateTo_renderParams['allowDefaultView']);
+  this.assertTrue(this.site_navigateTo_renderParams['cajole']);
+  this.assertTrue(this.site_navigateTo_renderParams['debug']);
+  this.assertTrue(this.site_navigateTo_renderParams['nocache']);
+  this.assertTrue(this.site_navigateTo_renderParams['testmode']);
+};
+
+ContainerTest.prototype.testNewGadgetSite = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container();
+  this.setupGadgetSite(1, {}, null);
+  var site1 = container.newGadgetSite(null);
+  this.setupGadgetSite(2, {}, null);
+  var site2 = container.newGadgetSite(null);
+  this.assertTrue(container.sites_[1] != null);
+  this.assertTrue(container.sites_[2] != null);
+};
+
+ContainerTest.prototype.testMixinViaPrototype = function() {
+  this.setupGadgetsRpcRegister();
+  osapi.container.Container.prototype.mixins_['test'] = function(context) {
+    return {
+      'getSitesLength' : function() {
+        return context.sites_.length;
+      }
+    };
+  };
+  osapi.container.Container.prototype.mixinsOrder_.push('test');
+  var container = new osapi.container.Container();
+  this.setupGadgetSite(1, {}, null);
+  container.newGadgetSite(null);
+  this.assertTrue(container.sites_[1] != null);
+  this.assertEquals(container.sites_.length, container.test.getSitesLength());
+};
+
+ContainerTest.prototype.testMixinViaAdd = function() {
+  this.setupGadgetsRpcRegister();
+  osapi.container.Container.addMixin('test2', function(context) {
+    return {
+      'getSitesLength' : function() {
+        return context.sites_.length;
+      }
+    };
+  });
+  var container = new osapi.container.Container();
+  this.setupGadgetSite(1, {}, null);
+  container.newGadgetSite(null);
+  this.assertTrue(container.sites_[1] != null);
+  this.assertEquals(container.sites_.length, container.test2.getSitesLength());
+};
+
+ContainerTest.prototype.setupGadgetSite = function(id, gadgetInfo, gadgetHolder) {
+  var self = this;
+  osapi.container.GadgetSite = function() {
+    return {
+      'getId' : function() {
+        return id;
+      },
+      'navigateTo' : function(gadgetUrl, viewParams, renderParams, func) {
+        self.site_navigateTo_gadgetUrl = gadgetUrl;
+        self.site_navigateTo_viewParams = viewParams;
+        self.site_navigateTo_renderParams = renderParams;
+        func(gadgetInfo);
+      },
+      'getActiveSiteHolder' : function() {
+        return gadgetHolder;
+      }
+    };
+  };
+};
+
+ContainerTest.prototype.setupGadgetsRpcRegister = function() {
+  gadgets.rpc = {
+    register: function() {
+    }
+  };
+};
diff --git a/trunk/features/src/test/javascript/features/container/gadget_holder_test.js b/trunk/features/src/test/javascript/features/container/gadget_holder_test.js
new file mode 100644
index 0000000..4638a1e
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container/gadget_holder_test.js
@@ -0,0 +1,180 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the gadget_holder library.
+ */
+
+function GadgetHolderTest(name) {
+  TestCase.call(this, name);
+}
+
+GadgetHolderTest.inherits(TestCase);
+
+GadgetHolderTest.prototype.setUp = function() {
+  this.containerUri = window.__CONTAINER_URI;
+  window.__CONTAINER_URI = shindig.uri('http://container.com');
+  this.gadgetsRpc = gadgets.rpc;
+  this.pubsub2router = gadgets.pubsub2router;
+};
+
+GadgetHolderTest.prototype.tearDown = function() {
+  window.__CONTAINER_URI = this.containerUri;
+  gadgets.rpc = this.gadgetsRpc;
+  gadgets.pubsub2router = this.pubsub2router;
+};
+
+GadgetHolderTest.prototype.testNew = function() {
+  var element = {
+    getAttribute: function() {
+      return '0';
+    },
+    id: '123'
+  };
+  var site = new osapi.container.GadgetSite(null, null, {gadgetEl: element});
+  var holder = new osapi.container.GadgetHolder(site, element);
+  this.assertEquals(element, holder.getElement());
+  this.assertUndefined(holder.getIframeId());
+  this.assertUndefined(holder.getGadgetInfo());
+  this.assertUndefined(holder.getUrl());
+};
+
+GadgetHolderTest.prototype.testRenderWithoutRenderParams = function() {
+  var element = {};
+  var gadgetInfo = {
+      'iframeUrls' : {'default' : 'http://shindig/gadgets/ifr?url=gadget.xml&lang=en&country=US#rpctoken=1234'},
+      'url' : 'gadget.xml'
+  };
+  this.setupGadgetsRpcSetupReceiver();
+  var element = {
+    id: '123'
+  };
+  var service = {};
+  service.getCountry = function(){return "ZH";};
+  service.getLanguage = function(){return "cn"};
+  var site = new osapi.container.GadgetSite(null, service, {gadgetEl: element});
+  var holder = new osapi.container.GadgetHolder(site, element, '__gadgetOnLoad');
+  holder.render(gadgetInfo, {}, {'view' : 'default'});
+  this.assertEquals('<iframe' +
+      ' marginwidth="0"' +
+      ' hspace="0"' +
+      ' title="default title"' +
+      ' frameborder="0"' +
+      ' scrolling="no"' +
+      ' onload="window.__gadgetOnLoad(\'gadget.xml\', \'123\');"' +
+      ' marginheight="0"' +
+      ' vspace="0"' +
+      ' id="__gadget_123"' +
+      ' name="__gadget_123"' +
+      ' src="http://shindig/gadgets/ifr?url=gadget.xml&lang=en&country=US&debug=0&nocache=0&testmode=0' +
+          '&view=default&parent=http%3A//container.com&mid=0#rpctoken=1234"' +
+      ' ></iframe>',
+      element.innerHTML);
+};
+
+GadgetHolderTest.prototype.testRenderWithRenderRequests = function() {
+  var gadgetInfo = {
+      'iframeUrls' : {'default' : 'http://shindig/gadgets/ifr?url=gadget.xml&lang=%lang%&country=%country%#rpctoken=1234'},
+      'url' : 'gadget.xml'
+  };
+  var renderParams = {
+      'cajole' : true,
+      'class' : 'xyz',
+      'debug' : true,
+      'height' : 111,
+      'nocache' : true,
+      'testmode' : true,
+      'width' : 222,
+      'view' : 'default'
+  };
+  this.setupGadgetsRpcSetupReceiver();
+  var element = {
+    id: '123'
+  };
+  var service = {};
+  service.getCountry = function(){return "US";};
+  service.getLanguage = function(){return "en"};
+  var site = new osapi.container.GadgetSite(null, service, {gadgetEl: element, moduleId: 123});
+  var holder = new osapi.container.GadgetHolder(site, element, '__gadgetOnLoad');
+  holder.render(gadgetInfo, {}, renderParams);
+  this.assertEquals('<iframe' +
+      ' marginwidth="0"' +
+      ' hspace="0"' +
+      ' height="111"' +
+      ' title="default title"' +
+      ' frameborder="0"' +
+      ' scrolling="no"' +
+      ' onload="window.__gadgetOnLoad(\'gadget.xml\', \'123\');"' +
+      ' class="xyz"' +
+      ' marginheight="0"' +
+      ' vspace="0"' +
+      ' id="__gadget_123"' +
+      ' width="222"' +
+      ' name="__gadget_123"' +
+      ' src="http://shindig/gadgets/ifr?url=gadget.xml&lang=en&country=US&debug=1&nocache=1&testmode=1' +
+          '&view=default&libs=caja&caja=1&parent=http%3A//container.com&mid=0#rpctoken=1234"' +
+      ' ></iframe>',
+      element.innerHTML);
+};
+GadgetHolderTest.prototype.testRemoveOaContainer_exisiting = function() {
+    var hub = this.setupMockPubsub2router(true);
+    var holder = new osapi.container.GadgetHolder();
+    var answer = 42;
+    holder.removeOaaContainer_(answer);
+    this.assertEquals(answer, hub.getCallArgs().g.id);
+    this.assertEquals(answer, hub.getCallArgs().r.container.passedId);
+};
+GadgetHolderTest.prototype.testRemoveOaContainer_nonexisting = function() {
+    var hub = this.setupMockPubsub2router(false);
+    var holder = new osapi.container.GadgetHolder();
+    var answer = 42;
+    holder.removeOaaContainer_(answer);
+    this.assertEquals(answer, hub.getCallArgs().g.id);
+    this.assertEquals("undefined", typeof hub.getCallArgs().r.container);
+};
+
+GadgetHolderTest.prototype.setupGadgetsRpcSetupReceiver = function() {
+  gadgets.rpc = {
+    setupReceiver: function(iframeId, relayUri, rpcToken) {
+    }
+  };
+};
+
+GadgetHolderTest.prototype.setupMockPubsub2router = function(existing) {
+    gadgets.pubsub2router = {
+        hub:(function () {
+            var getArgs = {}, removeArgs = {};
+            return{
+                getContainer:function (id) {
+                    getArgs.id = id;
+                    return existing ? {passedId:id} : null;
+                },
+                removeContainer:function (container) {
+                    removeArgs.container = container;
+                },
+                getCallArgs:function () {
+                    return {g:getArgs, r:removeArgs}
+                }
+            }
+        })()
+    };
+    return gadgets.pubsub2router.hub;
+};
diff --git a/trunk/features/src/test/javascript/features/container/gadget_site_test.js b/trunk/features/src/test/javascript/features/container/gadget_site_test.js
new file mode 100644
index 0000000..6444c71
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container/gadget_site_test.js
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the gadget_site library.
+ */
+
+function GadgetSiteTest(name) {
+  TestCase.call(this, name);
+}
+
+GadgetSiteTest.inherits(TestCase);
+
+GadgetSiteTest.prototype.NAVIGATE_CALLBACK = 'nc';
+GadgetSiteTest.prototype.GADGET_URL = 'http://gadget/abc.xml';
+
+GadgetSiteTest.prototype.setUp = function() {
+  var self = this;
+
+  this.util_getCurrentTimeMs_value = 100;
+  this.util_getCurrentTimeMs_func = osapi.container.util.getCurrentTimeMs;
+  osapi.container.util.getCurrentTimeMs = function() {
+    return self.util_getCurrentTimeMs_value++;
+  };
+
+  this.util_warn_value = null;
+  this.util_warn_func = gadgets.warn;
+  gadgets.warn = function(value) {
+    self.util_warn_value = value;
+  };
+
+  window[this.NAVIGATE_CALLBACK] = function(timingInfo) {
+    self.window_navigateCallback_timingInfo = timingInfo;
+  };
+};
+
+GadgetSiteTest.prototype.tearDown = function() {
+  osapi.container.util.getCurrentTimeMs = this.util_getCurrentTimeMs_func;
+  gadgets.warn = this.util_warn_func;
+  delete window[this.NAVIGATE_CALLBACK];
+};
+
+GadgetSiteTest.prototype.testGetId = function() {
+  var site = new osapi.container.GadgetSite(null, null, {});
+  this.assertEquals(osapi.container.Site.prototype.nextUniqueSiteId_ - 1, site.getId());
+  site = new osapi.container.GadgetSite(null, null, {});
+  this.assertEquals(osapi.container.Site.prototype.nextUniqueSiteId_ - 1, site.getId());
+};
+
+GadgetSiteTest.prototype.testNavigateToWithUncachedError = function() {
+  var self = this;
+  var service = {
+    getCachedGadgetMetadata : function(url) {
+      self.service_getCachedGadgetMetadata_url = url;
+      return null; // not cached
+    },
+    getGadgetMetadata : function(request, callback) {
+      self.service_getGadgetMetadata_request = request;
+      self.service_getGadgetMetadata_callback = callback;
+      callback(self.newMetadataError(self.GADGET_URL, 'na'));
+    }
+  };
+  var navigateToCallback = function(gadgetInfo) {
+    self.site_navigateToCallback_gadgetInfo = gadgetInfo;
+  };
+  var site = this.newGadgetSite(service, 'nc');
+  site.navigateTo(this.GADGET_URL, {}, {}, navigateToCallback);
+  this.assertEquals('Failed to navigate for gadget ' + this.GADGET_URL + '.',
+      this.util_warn_value);
+  this.assertTrue(this.window_navigateCallback_timingInfo['id'] >= 0);
+  this.assertEquals(this.GADGET_URL, this.window_navigateCallback_timingInfo['url']);
+  this.assertEquals(100, this.window_navigateCallback_timingInfo['start']);
+  this.assertEquals(1, this.window_navigateCallback_timingInfo['xrt']); // not cached
+  this.assertEquals(this.GADGET_URL, this.service_getCachedGadgetMetadata_url);
+  this.assertTrue(this.site_navigateToCallback_gadgetInfo['error'] != null);
+};
+
+GadgetSiteTest.prototype.testNavigateToWithCachedError = function() {
+  var self = this;
+  var service = {
+    getCachedGadgetMetadata : function(url) {
+      self.service_getCachedGadgetMetadata_url = url;
+      return self.newMetadataError(self.GADGET_URL, 'na'); // cached
+    },
+    getGadgetMetadata : function(request, callback) {
+      self.service_getGadgetMetadata_request = request;
+      self.service_getGadgetMetadata_callback = callback;
+      callback(self.newMetadataError(self.GADGET_URL, 'na'));
+    }
+  };
+  var navigateToCallback = function(gadgetInfo) {
+    self.site_navigateToCallback_gadgetInfo = gadgetInfo;
+  };
+  var site = this.newGadgetSite(service, this.NAVIGATE_CALLBACK);
+  site.navigateTo(this.GADGET_URL, {}, {}, navigateToCallback);
+  this.assertEquals('Failed to navigate for gadget ' + this.GADGET_URL + '.',
+      this.util_warn_value);
+  this.assertTrue(this.window_navigateCallback_timingInfo['id'] >= 0);
+  this.assertEquals(this.GADGET_URL, this.window_navigateCallback_timingInfo['url']);
+  this.assertEquals(100, this.window_navigateCallback_timingInfo['start']);
+  this.assertEquals(0, this.window_navigateCallback_timingInfo['xrt']); // cached
+  this.assertEquals(this.GADGET_URL, this.service_getCachedGadgetMetadata_url);
+  this.assertTrue(this.site_navigateToCallback_gadgetInfo['error'] != null);
+};
+
+GadgetSiteTest.prototype.newMetadataError = function(url, message) {
+  var response = {};
+  response[url] = {};
+  response[url]['error'] = message;
+  return response;
+};
+
+GadgetSiteTest.prototype.newGadgetSite = function(service, navigateCallback) {
+  return new osapi.container.GadgetSite(null, service, {
+    'navigateCallback' : navigateCallback
+  });
+};
diff --git a/trunk/features/src/test/javascript/features/container/service_test.js b/trunk/features/src/test/javascript/features/container/service_test.js
new file mode 100644
index 0000000..b802009
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container/service_test.js
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the service library.
+ */
+
+function ServiceTest(name) {
+  TestCase.call(this, name);
+}
+
+ServiceTest.inherits(TestCase);
+
+ServiceTest.prototype.setUp = function() {
+  this.apiUri = window.__API_URI;
+  window.__API_URI = shindig.uri('http://shindig.com');
+  this.containerUri = window.__CONTAINER_URI;
+  window.__CONTAINER_URI = shindig.uri('http://container.com');
+  this.osapiGadgets = osapi.gadgets;
+  this.shindigContainerGadgetSite = osapi.container.GadgetSite;
+  this.gadgetsRpc = gadgets.rpc;
+  gadgets.rpc = {
+    register: function() {
+    }
+  };
+  
+  this.self = {};
+  var response = {};
+  response.error = {};
+};
+
+ServiceTest.prototype.tearDown = function() {
+  window.__API_URI = this.apiUri;
+  window.__CONTAINER_URI = this.containerUri;
+  osapi.container.GadgetSite = this.shindigContainerGadgetSite;
+  gadgets.rpc = this.gadgetsRpc;
+  osapi.gadgets = this.osapiGadgets;
+};
+
+ServiceTest.prototype.setupOsapiGadgetsMetadata = function(response) {
+  osapi.gadgets = {};
+  osapi.gadgets.metadata = function(request) {
+    return {
+      execute: function(func) {
+        func(response);
+      }
+    };
+  };
+};
+
+ServiceTest.prototype.setupUtilCurrentTimeMs = function(time) {
+  osapi.container.util.getCurrentTimeMs = function() {
+    return time;
+  };
+};
+
+ServiceTest.prototype.testGetGadgetMetadata = function() {
+  var service = new osapi.container.Service(new osapi.container.Container({
+    GET_LANGUAGE: function() {
+      return 'pt'; 
+    },
+    GET_COUNTRY: function() {
+      return 'BR';
+    }
+  }));
+  service.cachedMetadatas_ = {
+    'cached1.xml' : {
+      'url' : 'cached1.xml',
+      'responseTimeMs' : 80,
+      'expireTimeMs' : 85,
+      'localExpireTimeMs' : 100
+    }
+  };
+
+  var request = osapi.container.util.newMetadataRequest([
+      'cached1.xml', 'resp1.xml', 'resp2.xml', 'resp3.xml'
+  ]);
+
+  var response = {
+    'resp1.xml' : {
+      'responseTimeMs' : 90,
+      'expireTimeMs' : 91
+    },
+    'resp2.xml' : {
+      'responseTimeMs' : 110,
+      'expireTimeMs' : 112
+    },
+    'resp3.xml' : {
+      'responseTimeMs' : 97,
+      'expireTimeMs' : 103
+    }
+  };
+
+  var self = this;
+  var callback = function(response) {
+    self.response = response;
+  };
+  
+  this.setupUtilCurrentTimeMs(100);
+  this.setupOsapiGadgetsMetadata(response);
+  var metadata = service.getGadgetMetadata(request, callback);
+  var response = self.response;
+  
+  this.assertEquals('cached1.xml', response['cached1.xml'].url);
+  this.assertEquals(80, response['cached1.xml'].responseTimeMs);
+  this.assertEquals(85, response['cached1.xml'].expireTimeMs);
+  this.assertEquals(100, response['cached1.xml'].localExpireTimeMs);
+
+  this.assertEquals('resp1.xml', response['resp1.xml'].url);
+  this.assertEquals(90, response['resp1.xml'].responseTimeMs);
+  this.assertEquals(91, response['resp1.xml'].expireTimeMs);
+  this.assertEquals(101, response['resp1.xml'].localExpireTimeMs);
+
+  this.assertEquals('resp2.xml', response['resp2.xml'].url);
+  this.assertEquals(110, response['resp2.xml'].responseTimeMs);
+  this.assertEquals(112, response['resp2.xml'].expireTimeMs);
+  this.assertEquals(102, response['resp2.xml'].localExpireTimeMs);
+
+  this.assertEquals('resp3.xml', response['resp3.xml'].url);
+  this.assertEquals(97, response['resp3.xml'].responseTimeMs);
+  this.assertEquals(103, response['resp3.xml'].expireTimeMs);
+  this.assertEquals(106, response['resp3.xml'].localExpireTimeMs);
+  
+  this.assertTrue(service.cachedMetadatas_['cached1.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['resp1.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['resp2.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['resp3.xml'] != null);
+  
+  this.assertEquals('pt', service.getLanguage());
+  this.assertEquals('BR', service.getCountry());
+  this.assertEquals('pt', request.language);
+  this.assertEquals('BR', request.country);
+};
+
+ServiceTest.prototype.testUncacheStaleGadgetMetadataExcept = function() {
+  var service = new osapi.container.Service(new osapi.container.Container());
+  service.cachedMetadatas_ = {
+      'cached1.xml' : { 'localExpireTimeMs' : 100 },
+      'cached2.xml' : { 'localExpireTimeMs' : 200 },
+      'except1.xml' : { 'localExpireTimeMs' : 100 },
+      'except2.xml' : { 'localExpireTimeMs' : 200 }
+  };
+  this.setupUtilCurrentTimeMs(150);
+  service.uncacheStaleGadgetMetadataExcept({
+      'except1.xml' : null,
+      'except2.xml' : null
+  });
+  this.assertTrue(service.cachedMetadatas_['cached1.xml'] == null);
+  this.assertTrue(service.cachedMetadatas_['cached2.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['except1.xml'] != null);
+  this.assertTrue(service.cachedMetadatas_['except2.xml'] != null);
+};
+
+ServiceTest.prototype.testUpdateResponse = function() {
+  var service = new osapi.container.Service(new osapi.container.Container());
+  this.setupUtilCurrentTimeMs(120);
+
+  var data = {responseTimeMs : 100, expireTimeMs : 105};
+  service.addGadgetMetadatas({'id' : data});
+  this.assertEquals("id", data.url);
+  this.assertEquals(125, data.localExpireTimeMs);
+  
+  data = {responseTimeMs : 100, expireTimeMs : 105};
+  service.addGadgetMetadatas({'id' : data}, 104);
+  this.assertEquals("id", data.url);
+  this.assertEquals(121, data.localExpireTimeMs);
+};
+
+ServiceTest.prototype.testAddToCache = function() {
+  var service = new osapi.container.Service(new osapi.container.Container());
+  this.setupUtilCurrentTimeMs(120);
+
+  var cache = {};
+  service.addToCache_(
+    { "id1": { responseTimeMs : 100, expireTimeMs : 105 },
+      "id2": { responseTimeMs : 100, expireTimeMs : 135 }},
+    103, cache);
+  
+  this.assertTrue(122, cache["id1"].localExpireTimeMs);
+  this.assertTrue(152, cache["id2"].localExpireTimeMs);
+};
+  
+
diff --git a/trunk/features/src/test/javascript/features/container/util_test.js b/trunk/features/src/test/javascript/features/container/util_test.js
new file mode 100644
index 0000000..6f37a48
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/container/util_test.js
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the util library.
+ */
+
+function UtilTest(name) {
+  TestCase.call(this, name);
+}
+
+UtilTest.inherits(TestCase);
+
+
+UtilTest.prototype.setUp = function() {
+  this.container = window.__CONTAINER;
+  window.__CONTAINER = 'abc';
+};
+
+UtilTest.prototype.tearDown = function() {
+  window.__CONTAINER = this.container;
+};
+
+UtilTest.prototype.testIsEmptyJson = function() {
+  this.assertEquals(true, osapi.container.util.isEmptyJson({}));
+  this.assertFalse(osapi.container.util.isEmptyJson({ 'a' : 'b' }));
+};
+
+UtilTest.prototype.testNewMetadataRequest = function() {
+  var req = osapi.container.util.newMetadataRequest(['a.xml', 'b.xml']);
+  this.assertEquals('abc', req.container);
+  this.assertEquals(2, req.ids.length);
+  this.assertEquals('a.xml', req.ids[0]);
+  this.assertEquals('b.xml', req.ids[1]);
+};
diff --git a/trunk/features/src/test/javascript/features/core.io/iotest.js b/trunk/features/src/test/javascript/features/core.io/iotest.js
new file mode 100644
index 0000000..3a98356
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/core.io/iotest.js
@@ -0,0 +1,1293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+var gadgets = gadgets || {};
+
+function IoTest(name) {
+  TestCase.call(this, name);
+};
+IoTest.inherits(TestCase);
+
+IoTest.prototype.setUp = function() {
+  this.getUrlParameters = gadgets.util.getUrlParameters;
+  gadgets.util.getUrlParameters = function() {
+    return { "st" : "authtoken", "url" : "http://www.gadget.com/gadget.xml", "container" : "foo" };
+  };
+  if (!shindig.auth) {
+    shindig.auth = new shindig.Auth();
+  }
+
+  this.fakeXhrs = new fakeXhr.Factory(this);
+  this.oldXMLHttpRequest = window.XMLHTTPRequest;
+  this.oldXhrWrapper = shindig.xhrwrapper;
+  window.XMLHttpRequest = this.fakeXhrs.getXhrConstructor();
+  shindig.xhrwrapper = undefined;
+
+  document.scripts = [];
+  gadgets.config.init({ "core.io" : {
+      "proxyUrl" : "http://example.com/proxy?url=%url%&refresh=%refresh%&g=%gadget%&c=%container%",
+      "jsonProxyUrl" : "http://example.com/json",
+      "unparseableCruft" : "throw 1; < don't be evil' >"}});
+  gadgets.io.preloaded_ = [];
+};
+IoTest.prototype.tearDown = function() {
+  gadgets.util.getUrlParameters = this.getUrlParameters;
+};
+
+IoTest.prototype.setSchemaless = function() {
+  gadgets.config.init({ "core.io" : {
+      "proxyUrl" : "//example.com/proxy?url=%url%&refresh=%refresh%&g=%gadget%&c=%container%",
+      "jsonProxyUrl" : "http://example.com/json",
+      "unparseableCruft" : "throw 1; < don't be evil' >"}});
+  gadgets.io.preloaded_ = [];
+};
+
+IoTest.prototype.setWithFileName = function() {
+  gadgets.config.init({ "core.io" : {
+      "proxyUrl" : "http://example.com/proxy%filename%?url=%url%&refresh=%refresh%&g=%gadget%&c=%container%",
+      "jsonProxyUrl" : "http://example.com/json",
+      "unparseableCruft" : "throw 1; < don't be evil' >"}});
+  gadgets.io.preloaded_ = [];
+};
+
+IoTest.prototype.setOAuthSupportEnabled = function() {
+  gadgets.config.init({ "core.io" : {
+      "proxyUrl" : "http://example.com/proxy?url=%url%&refresh=%refresh%&g=%gadget%&c=%container%%authz%",
+      "jsonProxyUrl" : "http://example.com/json",
+      "unparseableCruft" : "throw 1; < don't be evil' >"}});
+  gadgets.io.preloaded_ = [];
+};
+
+IoTest.prototype.tearDown = function() {
+  window.XMLHttpRequest = this.oldXMLHTTPRequest;
+  shindig.xhrwrapper = this.oldXhrWrapper;
+};
+
+IoTest.prototype.testGetProxyUrl = function() {
+  var proxied = gadgets.io.getProxyUrl("http://target.example.com/image.gif");
+  this.assertEquals(
+      "http://example.com/proxy?url=http%3a%2f%2ftarget.example.com%2fimage.gif" +
+          "&refresh=3600" +
+          "&g=http%3a%2f%2fwww.gadget.com%2fgadget.xml" +
+          "&c=foo",
+      proxied);
+};
+
+IoTest.prototype.testGetProxyUrl_OAuthSupportEnabled = function() {
+  this.setOAuthSupportEnabled();
+  var proxied = gadgets.io.getProxyUrl("http://target.example.com/image.gif", { 'AUTHORIZATION': "OAUTH2", 'OAUTH_SERVICE_NAME' : "some-service"});
+  this.assertEquals(
+      "http://example.com/proxy?url=http%3a%2f%2ftarget.example.com%2fimage.gif" +
+          "&refresh=3600" +
+          "&g=http%3a%2f%2fwww.gadget.com%2fgadget.xml" +
+          "&c=foo&authz=oauth2&st=authtoken&OAUTH_SERVICE_NAME=some-service",
+      proxied);
+};
+
+IoTest.prototype.testGetProxyUrl_nondefaultRefresh = function() {
+  var proxied = gadgets.io.getProxyUrl("http://target.example.com/image.gif",
+      { 'REFRESH_INTERVAL' : 30 });
+  this.assertEquals(
+      "http://example.com/proxy?url=http%3a%2f%2ftarget.example.com%2fimage.gif" +
+          "&refresh=30" +
+          "&g=http%3a%2f%2fwww.gadget.com%2fgadget.xml" +
+          "&c=foo",
+      proxied);
+};
+
+IoTest.prototype.testGetProxyUrl_disableCache = function() {
+  var proxied = gadgets.io.getProxyUrl("http://target.example.com/image.gif",
+      { 'REFRESH_INTERVAL' : 0 });
+  this.assertEquals(
+      "http://example.com/proxy?url=http%3a%2f%2ftarget.example.com%2fimage.gif" +
+          "&refresh=0" +
+          "&g=http%3a%2f%2fwww.gadget.com%2fgadget.xml" +
+          "&c=foo",
+      proxied);
+};
+
+IoTest.prototype.testGetProxyUrl_schemaless = function() {
+  this.setSchemaless();
+  window.location = { protocol: "https:" };
+  var proxied = gadgets.io.getProxyUrl("http://target.example.com/image.gif");
+  this.assertEquals(
+      "https://example.com/proxy?url=http%3a%2f%2ftarget.example.com%2fimage.gif" +
+          "&refresh=3600" +
+          "&g=http%3a%2f%2fwww.gadget.com%2fgadget.xml" +
+          "&c=foo",
+      proxied);
+};
+
+IoTest.prototype.testGetProxyUrl_withFileName = function() {
+  this.setWithFileName();
+  var proxied = gadgets.io.getProxyUrl("http://target.example.com/image.gif");
+  this.assertEquals(
+      "http://example.com/proxy/image.gif?url=http%3a%2f%2ftarget.example.com%2fimage.gif" +
+          "&refresh=3600" +
+          "&g=http%3a%2f%2fwww.gadget.com%2fgadget.xml" +
+          "&c=foo",
+      proxied);
+};
+
+IoTest.prototype.testEncodeValues = function() {
+  var x = gadgets.io.encodeValues({ 'foo' : 'bar' });
+  this.assertEquals("foo=bar", x);
+};
+
+IoTest.prototype.setArg = function(req, inBody, name, value) {
+  if (inBody) {
+    req.setBodyArg(name, value);
+  } else {
+    req.setQueryArg(name, value);
+  }
+};
+
+IoTest.prototype.setStandardArgs = function(req, inBody) {
+  this.setArg(req, inBody, "st", "");
+  this.setArg(req, inBody, "contentType", "TEXT");
+  this.setArg(req, inBody, "authz", "");
+  this.setArg(req, inBody, "bypassSpecCache", "");
+  this.setArg(req, inBody, "signViewer", "true");
+  this.setArg(req, inBody, "signOwner", "true");
+  this.setArg(req, inBody, "getSummaries", "false");
+  this.setArg(req, inBody, "gadget", "http://www.gadget.com/gadget.xml");
+  this.setArg(req, inBody, "getFullHeaders", "false");
+  this.setArg(req, inBody, "container", "foo");
+  this.setArg(req, inBody, "headers", "");
+  this.setArg(req, inBody, "numEntries", "3");
+  this.setArg(req, inBody, "postData", "");
+  this.setArg(req, inBody, "httpMethod", "GET");
+  req.setHeader( 'X-Shindig-ST', shindig.auth.getSecurityToken() );
+};
+
+IoTest.prototype.makeFakeResponse = function(text, rc) {
+  return new fakeXhr.Response("throw 1; < don't be evil' >" + text, (rc ? rc : 200));
+};
+
+IoTest.prototype.testNoMethod = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      });
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testNoMethod_nonDefaultRefresh = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("refresh", "1800");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      {
+        "REFRESH_INTERVAL" : 1800
+      });
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testNoMethod_disableRefresh = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("refresh", "0");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      {
+        "REFRESH_INTERVAL" : 0
+      });
+  this.assertEquals('some data', resp.text);
+};
+
+// Make sure we don't accidentally include any cache-busting parameters
+// in our GET requests
+IoTest.prototype.testRepeatGet = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      });
+  this.assertEquals('some data', resp.text);
+
+  resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      });
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testPost = function() {
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("httpMethod", "POST");
+  req.setBodyArg("postData", "foo=bar");
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("headers", "Content-Type=application%2fx-www-form-urlencoded");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params[gadgets.io.RequestParameters.METHOD] = "POST";
+  params[gadgets.io.RequestParameters.POST_DATA] = "foo=bar";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testPost_noBody = function() {
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("httpMethod", "POST");
+  req.setBodyArg("postData", "");
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("headers", "Content-Type=application%2fx-www-form-urlencoded");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params[gadgets.io.RequestParameters.METHOD] = "POST";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testPost_emptyBody = function() {
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("httpMethod", "POST");
+  req.setBodyArg("postData", "");
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("headers", "Content-Type=application%2fx-www-form-urlencoded");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params[gadgets.io.RequestParameters.METHOD] = "POST";
+  params[gadgets.io.RequestParameters.POST_DATA] = "";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testPut = function() {
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("httpMethod", "PUT");
+  req.setBodyArg("postData", "abcd");
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("refresh", null);
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params[gadgets.io.RequestParameters.METHOD] = "PUT";
+  params[gadgets.io.RequestParameters.POST_DATA] = "abcd";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+/**
+ * Tests using makeRequest on IE with ActiveX disabled, which results in ActiveXObject throwing an
+ * exception. We should then fall back to XMLHttpRequest.
+ *
+ * See https://issues.apache.org/jira/browse/SHINDIG-1808 for details.
+ */
+IoTest.prototype.testPut_IEActiveXDisabled = function() {
+  ActiveXObject = function() {
+    throw "error";
+  }
+
+  try {
+    this.testPut();
+  } finally {
+    ActiveXObject = undefined;
+  }
+};
+
+/**
+ * Tests using makeRequest on IE with ActiveX enabled, which results in an ActiveXObject being
+ * created instead of XMLHttpRequest.
+ */
+IoTest.prototype.testPut_IEActiveXEnabled = function() {
+  ActiveXObject = function() {
+    var xhrConstructor = this.fakeXhrs.getXhrConstructor();
+    return new xhrConstructor();
+  }
+
+  try {
+    this.testPut();
+  } finally {
+    ActiveXObject = undefined;
+  }
+};
+
+/**
+ * Tests using makeRequest if there is no available mechanism for performing XML HTTP requests.
+ */
+IoTest.prototype.testPut_NoXhrAvailable = function() {
+  var error = null;
+
+  window.XMLHttpRequest = undefined;
+  try {
+    this.testPut();
+  } catch (e) {
+    error = e;
+  } finally {
+    window.XMLHttpRequest = this.fakeXhrs.getXhrConstructor();
+  }
+
+  this.assertEquals('no xhr available', error);
+};
+
+IoTest.prototype.testPut_noBody = function() {
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("httpMethod", "PUT");
+  req.setBodyArg("postData", "");
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("refresh", null);
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params[gadgets.io.RequestParameters.METHOD] = "PUT";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testSignedGet = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("signOwner", "true");
+  req.setBodyArg("signViewer", "true");
+  req.setBodyArg("authz", "signed");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("refresh", null);
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "SIGNED";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testSignedPost = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("signOwner", "true");
+  req.setBodyArg("signViewer", "true");
+  req.setBodyArg("authz", "signed");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("httpMethod", "POST");
+  req.setBodyArg("headers", "Content-Type=application%2fx-www-form-urlencoded");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "SIGNED";
+  params["METHOD"] = "POST";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testSignedGet_noViewerBoolean = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("signOwner", "true");
+  req.setBodyArg("signViewer", "false");
+  req.setBodyArg("authz", "signed");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("refresh", null);
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "SIGNED";
+  params[gadgets.io.RequestParameters.SIGN_VIEWER] = false;
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testSignedGet_noViewerString = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("signOwner", "true");
+  req.setBodyArg("signViewer", "false");
+  req.setBodyArg("authz", "signed");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("refresh", null);
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "SIGNED";
+  params[gadgets.io.RequestParameters.SIGN_VIEWER] = "false";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testSignedGet_withNoOwnerAndViewerString = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("signOwner", "false");
+  req.setBodyArg("signViewer", "true");
+  req.setBodyArg("authz", "signed");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("refresh", null);
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "SIGNED";
+  params[gadgets.io.RequestParameters.SIGN_VIEWER] = "true";
+  params[gadgets.io.RequestParameters.SIGN_OWNER] = false;
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testOAuth = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "oauth");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("oauthState", "");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'oauthApprovalUrl' : 'http://sp.example.com/authz?oauth_token=foo',
+          'oauthState' : 'newState'
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "OAUTH";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals("http://sp.example.com/authz?oauth_token=foo",
+      resp.oauthApprovalUrl);
+
+  gadgets.io.oauthReceivedCallbackUrl_ = "http://shindig?oauth_verifier=12345";
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "oauth");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("oauthState", "newState");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("OAUTH_RECEIVED_CALLBACK",
+      "http://shindig?oauth_verifier=12345");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'personal data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "OAUTH";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals("personal data", resp.text);
+  this.assertEquals(null, gadgets.io.oauthReceivedCallbackUrl_);
+};
+
+IoTest.prototype.testSignedEquivalentToOAuth = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "signed");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("OAUTH_USE_TOKEN", "always");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'oauthApprovalUrl' : 'http://sp.example.com/authz?oauth_token=foo',
+          'oauthState' : 'newState'
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "SIGNED";
+  params["OAUTH_USE_TOKEN"] = "always";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals("http://sp.example.com/authz?oauth_token=foo",
+      resp.oauthApprovalUrl);
+};
+
+IoTest.prototype.testOAuth_error = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "oauth");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("oauthState", "");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'oauthError' : 'SOME_ERROR_CODE',
+          'oauthErrorText' : 'Some helpful error message',
+          'oauthState' : 'newState'
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "OAUTH";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertUndefined(resp.oauthApprovalUrl);
+  this.assertEquals("SOME_ERROR_CODE", resp.oauthError);
+  this.assertEquals("Some helpful error message", resp.oauthErrorText);
+};
+
+IoTest.prototype.testOAuth_serviceAndToken = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "oauth");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("OAUTH_SERVICE_NAME", "some-service");
+  req.setBodyArg("OAUTH_TOKEN_NAME", "some-token");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'oauthApprovalUrl' : 'http://sp.example.com/authz?oauth_token=foo',
+          'oauthState' : 'newState'
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "OAUTH";
+  params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "some-service";
+  params[gadgets.io.RequestParameters.OAUTH_TOKEN_NAME] = "some-token";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals("http://sp.example.com/authz?oauth_token=foo",
+      resp.oauthApprovalUrl);
+
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "oauth");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("oauthState", "newState");
+  req.setBodyArg("OAUTH_SERVICE_NAME", "some-service");
+  req.setBodyArg("OAUTH_TOKEN_NAME", "some-token");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'personal data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "OAUTH";
+  params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME] = "some-service";
+  params[gadgets.io.RequestParameters.OAUTH_TOKEN_NAME] = "some-token";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals("personal data", resp.text);
+};
+
+IoTest.prototype.testOAuth_preapprovedToken = function() {
+  gadgets.io.clearOAuthState();
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "oauth");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("oauthState", "");
+  req.setBodyArg("OAUTH_REQUEST_TOKEN", "reqtoken");
+  req.setBodyArg("OAUTH_REQUEST_TOKEN_SECRET", "abcd1234");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'personal data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params["AUTHORIZATION"] = "OAUTH";
+  params[gadgets.io.RequestParameters.OAUTH_REQUEST_TOKEN] = "reqtoken";
+  params[gadgets.io.RequestParameters.OAUTH_REQUEST_TOKEN_SECRET] = "abcd1234";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+
+  this.assertEquals("personal data", resp.text);
+};
+
+IoTest.prototype.testServerFailure = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("contentType", "JSON");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : 'Internal Server Failure.',
+          'rc' : 500
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      {
+        "CONTENT_TYPE" : "JSON"
+      });
+  this.assertEquals(500, resp.rc);
+  this.assertEquals(gadgets.json.stringify(["500 Error"]), gadgets.json.stringify(resp.errors));
+  this.assertEquals("Internal Server Failure.", resp.text);
+};
+
+IoTest.prototype.testJsonNonAuthoritative = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("contentType", "JSON");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : '{ "somejsonparam" : 3 }',
+          'rc' : 203
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      {
+        "CONTENT_TYPE" : "JSON"
+      });
+  this.assertEquals(3, resp.data.somejsonparam);
+};
+
+IoTest.prototype.testJson = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("contentType", "JSON");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : '{ "somejsonparam" : 3 }',
+          'rc' : 200
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      {
+        "CONTENT_TYPE" : "JSON"
+      });
+  this.assertEquals(3, resp.data.somejsonparam);
+};
+
+IoTest.prototype.testJson_malformed = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("contentType", "JSON");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : '{ bogus : 3 }',
+          'rc' : 200
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      {
+        "CONTENT_TYPE" : "JSON"
+      });
+  this.assertEquals("500 Failed to parse JSON", resp.errors[0]);
+};
+
+/**
+ * Tests parsing XML if the DOMParser class is defined.
+ */
+IoTest.prototype.testDom_DomParserDefined = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("contentType", "DOM");
+
+  var body = '<data>some text</data>';
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : body,
+          'rc' : 200
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var test = this;
+  DOMParser = function() {
+    this.parseFromString = function(content, contentType) {
+      test.assertEquals(body, content);
+      test.assertEquals("text/xml", contentType);
+
+      return {documentElement: {nodeName: "data", text: "some text"}};
+    }
+  }
+
+  try {
+    var resp = null;
+    gadgets.io.makeRequest("http://target.example.com/somepage",
+        function(data) {
+          resp = data;
+        },
+        {
+          "CONTENT_TYPE" : "DOM"
+        });
+  } finally {
+    DOMParser = undefined;
+  }
+
+  this.assertEquals(200, resp.rc);
+  this.assertEquals("some text", resp.data.documentElement.text);
+};
+
+/**
+ * Tests parsing XML if the DOMParser and ActiveXObject classes are both defined - DOMParser
+ * should be used.
+ */
+IoTest.prototype.testDom_DomParserAndActiveXObjectDefined = function() {
+  ActiveXObject = function() {
+    if (type === "Microsoft.XMLDOM") {
+      throw "should not be called";
+    } else {
+      return new window.XMLHttpRequest();
+    }
+  };
+
+  try {
+    this.testDom_DomParserDefined();
+  } finally {
+    ActiveXObject = undefined;
+  }
+};
+
+/**
+ * Tests parsing XML if the ActiveXObject class is defined.
+ */
+IoTest.prototype.testDom_ActiveXObjectDefined = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("contentType", "DOM");
+
+  var body = '<data>some text</data>';
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : body,
+          'rc' : 200
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var test = this;
+  ActiveXObject = function(type) {
+    if (type === "Microsoft.XMLDOM") {
+      this.loadXML = function(content) {
+        test.assertEquals(body, content);
+
+        this.documentElement = {nodeName: "data", text: "some text"};
+        return true;
+      }
+    } else {
+      return new window.XMLHttpRequest();
+    }
+  }
+
+  try {
+    var resp = null;
+    gadgets.io.makeRequest("http://target.example.com/somepage",
+        function(data) {
+          resp = data;
+        },
+        {
+          "CONTENT_TYPE" : "DOM"
+        });
+  } finally {
+    ActiveXObject = undefined;
+  }
+
+  this.assertEquals(200, resp.rc);
+  this.assertEquals("some text", resp.data.documentElement.text);
+};
+
+/**
+ * Tests parsing XML if there is no available mechanism for doing so.
+ */
+IoTest.prototype.testDom_NoParser = function() {
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+  req.setQueryArg("contentType", "DOM");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : '<wrapper><text>some text</text></wrapper>',
+          'rc' : 200
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      {
+        "CONTENT_TYPE" : "DOM"
+      });
+  this.assertEquals(500, resp.rc);
+  this.assertEquals(["500 Failed to parse XML because no DOM parser was available"], resp.errors);
+};
+
+IoTest.prototype.testPreload = function() {
+  gadgets.io.preloaded_ = [
+    {
+      "id": "http://target.example.com/somepage",
+      "rc" : 200,
+      "body" : "preloadedbody",
+      "headers": {
+        "set-cookie": ["foo=bar","baz=quux"],
+        "location": ["somewhere"]
+      }
+    }
+  ];
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      });
+
+  this.assertEquals("preloadedbody", resp.text);
+  this.assertEquals("somewhere", resp.headers["location"][0]);
+  this.assertEquals("foo=bar", resp.headers["set-cookie"][0]);
+  this.assertEquals("baz=quux", resp.headers["set-cookie"][1]);
+
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : 'not preloaded',
+          'rc' : 200
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      });
+  this.assertEquals("not preloaded", resp.text);
+};
+
+IoTest.prototype.testPreloadMiss_postRequest = function() {
+  gadgets.io.preloaded_ = [
+    {
+      "id": "http://target.example.com/somepage",
+      "rc" : 200,
+      "body" : "preloadedbody"
+    }
+  ];
+
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("httpMethod", "POST");
+  req.setBodyArg("postData", "foo=bar");
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("headers", "Content-Type=application%2fx-www-form-urlencoded");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  params[gadgets.io.RequestParameters.METHOD] = "POST";
+  params[gadgets.io.RequestParameters.POST_DATA] = "foo=bar";
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testPreloadMiss_wrongUrl = function() {
+  gadgets.io.preloaded_ = [
+    {
+      "id": "http://target.example.com/somepage2",
+      "rc" : 200,
+      "body" : "preloadedbody"
+    }
+  ];
+
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+
+  var resp = this.makeFakeResponse(
+      "{ 'http://target.example.com/somepage' : { 'body' : 'some data', 'rc' : 200 }}");
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  var params = {};
+  gadgets.io.makeRequest(
+      "http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals('some data', resp.text);
+};
+
+IoTest.prototype.testPreload_error404 = function() {
+  gadgets.io.preloaded_ = [
+    {
+      "id": "http://target.example.com/somepage",
+      "rc" : 404
+    }
+  ];
+
+  var req = new fakeXhr.Expectation("GET", "http://example.com/json");
+  this.setStandardArgs(req, false);
+  req.setQueryArg("url", "http://target.example.com/somepage");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : 'not preloaded', 
+          'rc' : 200
+         }
+      }));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      });
+  this.assertEquals("404 Error", resp.errors[0]);
+
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      });
+  this.assertEquals("not preloaded", resp.text);
+};
+
+IoTest.prototype.testPreload_oauthApproval = function() {
+  gadgets.io.clearOAuthState();
+  gadgets.io.preloaded_ = [
+    {
+      "id": "http://target.example.com/somepage",
+      "rc" : 200,
+      "oauthState" : "stateinfo",
+      "oauthApprovalUrl" : "http://example.com/approve"
+    }
+  ];
+
+  var req = new fakeXhr.Expectation("POST", "http://example.com/json");
+  this.setStandardArgs(req, true);
+  req.setBodyArg("url", "http://target.example.com/somepage");
+  req.setBodyArg("authz", "oauth");
+  req.setBodyArg("st", "authtoken");
+  req.setBodyArg("refresh", null);
+  req.setBodyArg("oauthState", "stateinfo");
+  req.setHeader("Content-Type", "application/x-www-form-urlencoded");
+
+  var resp = this.makeFakeResponse(gadgets.json.stringify(
+      { 'http://target.example.com/somepage' : {
+          'body' : 'not preloaded', 
+          'rc' : 200
+         }
+      }
+      ));
+
+  this.fakeXhrs.expect(req, resp);
+
+  var params = {};
+  params["AUTHORIZATION"] = "OAUTH";
+  var resp = null;
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals("http://example.com/approve", resp.oauthApprovalUrl);
+
+  gadgets.io.makeRequest("http://target.example.com/somepage",
+      function(data) {
+        resp = data;
+      },
+      params);
+  this.assertEquals("not preloaded", resp.text);
+};
diff --git a/trunk/features/src/test/javascript/features/core/authtest.js b/trunk/features/src/test/javascript/features/core/authtest.js
new file mode 100644
index 0000000..7cf1b3b
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/core/authtest.js
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+var gadgets = gadgets || {};
+
+function AuthTest(name) {
+  TestCase.call(this, name);
+};
+AuthTest.inherits(TestCase);
+
+AuthTest.prototype.setUp = function() {
+  // Prepare for mocks
+  gadgets.util = gadgets.util || {};
+  gadgets.config = gadgets.config || {};
+  this.oldConfigRegister = gadgets.config.register;
+  this.getUrlParameters = gadgets.util.getUrlParameters;
+};
+
+AuthTest.prototype.tearDown = function() {
+  // Remove mocks
+  gadgets.config.register = this.oldConfigRegister;
+  gadgets.util.getUrlParameters = this.getUrlParameters;
+};
+
+AuthTest.prototype.testTokenOnFragment = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 'st' : 'authtoken' };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals('authtoken', auth.getSecurityToken());
+  this.assertNull(auth.getTrustedData());
+  auth.updateSecurityToken('newtoken');
+  this.assertEquals('newtoken', auth.getSecurityToken());
+};
+
+AuthTest.prototype.testTokenInConfig = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({ 'shindig.auth' : { 'authToken' : 'configAuthToken' }});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 'st' : 'fragmentAuthToken' };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals('configAuthToken', auth.getSecurityToken());
+  this.assertNull(auth.getTrustedData());
+  auth.updateSecurityToken('newtoken');
+  this.assertEquals('newtoken', auth.getSecurityToken());
+};
+
+AuthTest.prototype.testNoToken = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({ 'shindig.auth' : null });
+  };
+  gadgets.util.getUrlParameters = function() {
+    return {};
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals(null, auth.getSecurityToken());
+  this.assertNull(auth.getTrustedData());
+  auth.updateSecurityToken('newtoken');
+  this.assertEquals('newtoken', auth.getSecurityToken());
+};
+
+AuthTest.prototype.testAddParamsToToken_normal = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 
+      'st' : 't=abcd&url=$',
+      'url' : 'http://www.example.com/gadget.xml'
+    };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals(
+      't=abcd&url=http%3a%2f%2fwww.example.com%2fgadget.xml',
+      auth.getSecurityToken());
+  auth.updateSecurityToken('newtoken');
+  this.assertEquals('newtoken', auth.getSecurityToken());
+};
+
+AuthTest.prototype.testAddParamsToToken_blankvalue = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 
+      'st' : 't=abcd&url=$&url=',
+      'url' : 'http://www.example.com/gadget.xml'
+    };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals(
+      't=abcd&url=http%3a%2f%2fwww.example.com%2fgadget.xml&url=',
+      auth.getSecurityToken());
+};
+
+AuthTest.prototype.testAddParamsToToken_dupname = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 
+      'st' : 't=abcd&url=$&url=$',
+      'url' : 'http://www.example.com/gadget.xml'
+    };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals(
+      't=abcd&url=http%3a%2f%2fwww.example.com%2fgadget.xml&url=' + 
+          'http%3a%2f%2fwww.example.com%2fgadget.xml',
+      auth.getSecurityToken());
+};
+
+AuthTest.prototype.testAddParamsToToken_blankname = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 
+      'st' : 't=abcd&=&url=$',
+      'url' : 'http://www.example.com/gadget.xml'
+    };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals(
+      't=abcd&=&url=http%3a%2f%2fwww.example.com%2fgadget.xml',
+      auth.getSecurityToken());
+};
+
+AuthTest.prototype.testAddParamsToToken_nonpaired = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 
+      'st' : 't=abcd&foo&url=$',
+      'url' : 'http://www.example.com/gadget.xml'
+    };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals(
+      't=abcd&foo&url=http%3a%2f%2fwww.example.com%2fgadget.xml',
+      auth.getSecurityToken());
+};
+
+AuthTest.prototype.testAddParamsToToken_extraequals = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 
+      'st' : 't=abcd&foo=$bar$=$baz$&url=$',
+      'url' : 'http://www.example.com/gadget.xml'
+    };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals(
+      't=abcd&foo=$bar$=$baz$&url=http%3a%2f%2fwww.example.com%2fgadget.xml',
+      auth.getSecurityToken());
+};
+
+AuthTest.prototype.testTrustedJson = function() {
+  gadgets.config.register = function(name, validator, callback) {
+    callback({ 'shindig.auth' : { 'trustedJson' : '{ "foo" : "bar" }' }});
+  };
+  gadgets.util.getUrlParameters = function() {
+    return { 
+      'st' : 't=abcd&foo=$bar$=$baz$&url=$',
+      'url' : 'http://www.example.com/gadget.xml'
+    };
+  };
+  var auth = new shindig.Auth();
+  this.assertEquals('bar', auth.getTrustedData().foo);
+};
diff --git a/trunk/features/src/test/javascript/features/core/config-test.js b/trunk/features/src/test/javascript/features/core/config-test.js
new file mode 100644
index 0000000..b231b83
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/core/config-test.js
@@ -0,0 +1,402 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+function ConfigTest(name) {
+  TestCase.call(this, name);
+}
+
+ConfigTest.inherits(TestCase);
+
+ConfigTest.prototype.setUp = function() {
+  gadgets.config.clear();
+  this.script1 = { nodeType: 3, src: "http://www.1.com/js/1.js?c=0" };
+  this.script2 = { nodeType: 3, src: "http://www.2.com/js/2.js?c=1" };
+  this.script3 = { nodeType: 3, src: "http://www.3.com/js/3.js?blah&c=0" };
+  document.scripts = [ this.script1, this.script2, this.script3 ];
+  this.defaultConfig = {
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    testBasic: { data: "Hello, World!", untouched: "Goodbye" },
+    testSecond: { foo: "Bar" }
+  };
+};
+
+ConfigTest.prototype.tearDown = function() {
+  gadgets.config.update({}, true);  // "reset" gadgets lib
+  window["___jsl"] = undefined;
+  window["___config"] = undefined;
+};
+
+ConfigTest.prototype.testBasic = function() {
+  var testBasicConfig;
+  gadgets.config.register("testBasic", null, function(config) {
+    testBasicConfig = config.testBasic;
+  });
+
+  gadgets.config.init(this.defaultConfig);
+
+  this.assertEquals("Hello, World!", testBasicConfig.data);
+};
+
+ConfigTest.prototype.testMultiple = function() {
+  var testMultiple0;
+  gadgets.config.register("testMultiple", null, function(config) {
+    testMultiple0 = config.testMultiple;
+  });
+
+  var testMultiple1;
+  gadgets.config.register("testMultiple", null, function(config) {
+    testMultiple1 = config.testMultiple;
+  });
+
+  gadgets.config.init({
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    testMultiple: {data: "Hello, World!"}
+  });
+
+  this.assertEquals("Hello, World!", testMultiple0.data);
+  this.assertEquals("Hello, World!", testMultiple1.data);
+};
+
+ConfigTest.prototype.testFindScriptLastNoHint = function() {
+  var testListen;
+  gadgets.config.register("testBasic", null, function(config) {
+    testListen = config;
+  });
+  this.script3.nodeValue = "{ testBasic: { data: 'News' } }";
+
+  gadgets.config.init(this.defaultConfig);
+
+  this.assertEquals("News", testListen.testBasic.data);
+  this.assertEquals("Goodbye", testListen.testBasic.untouched);
+};
+
+ConfigTest.prototype.testFindScriptLastHintMismatch = function() {
+  var testListen;
+  gadgets.config.register("testBasic", null, function(config) {
+    testListen = config;
+  });
+  this.script3.nodeValue = " testBasic: { data: 'AgainNew' } ";
+
+  window["___jsl"] = { u: "http://nomatch.com/foo.js" };
+  gadgets.config.init(this.defaultConfig);
+
+  this.assertEquals("AgainNew", testListen.testBasic.data);
+  this.assertEquals("Goodbye", testListen.testBasic.untouched);
+};
+
+ConfigTest.prototype.testFindScriptHintExact = function() {
+  var testListen;
+  gadgets.config.register("testBasic", null, function(config) {
+    testListen = config;
+  });
+  this.script2.nodeValue = "data: 'Override'";
+  window["___jsl"] = {f: [ "testBasic" ]};
+  gadgets.config.init(this.defaultConfig);
+  this.assertEquals("Override", testListen.testBasic.data);
+  this.assertEquals("Goodbye", testListen.testBasic.untouched);
+};
+
+ConfigTest.prototype.testFindScriptHintPrefixMatch = function() {
+  var testListen;
+  gadgets.config.register("testBasic", null, function(config) {
+    testListen = config;
+  });
+  this.script2.src += "#hash=1";
+  this.script2.nodeValue = "testBasic: { data: 'Override' }, testSecond: [ 'difftype' ]";
+  window["___jsl"] = {f: [ "testBasic", "testSecond" ]};
+  gadgets.config.init(this.defaultConfig);
+  this.assertEquals("Override", testListen.testBasic.data);
+  this.assertEquals("Goodbye", testListen.testBasic.untouched);
+  this.assertEquals("difftype", testListen.testSecond[0]);
+};
+
+ConfigTest.prototype.testInitMergesNotOverwrites = function() {
+  var testListen;
+  gadgets.config.register("testBasic", null, function(config) {
+    testListen = config;
+  });
+  gadgets.config.init(this.defaultConfig);
+  this.assertEquals("Hello, World!", testListen.testBasic.data);
+  this.assertEquals("Goodbye", testListen.testBasic.untouched);
+  this.assertEquals("Bar", testListen.testSecond.foo);
+
+  gadgets.config.init({ testBasic: { data: "Override" } });
+  this.assertEquals("Override", testListen.testBasic.data);
+  this.assertEquals("Goodbye", testListen.testBasic.untouched);
+  this.assertEquals("Bar", testListen.testSecond.foo);
+};
+
+ConfigTest.prototype.testUpdateMerge = function() {
+  var testListen;
+  gadgets.config.register("one", null, function(config) {
+    testListen = config;
+  });
+  gadgets.config.init({
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    one: { oneKey1: { oneSubkey1: "oneVal1" }, oneKey2: "data" },
+    two: "twoVal1"
+  });
+  this.assertEquals("oneVal1", testListen.one.oneKey1.oneSubkey1);
+  this.assertEquals("data", testListen.one.oneKey2);
+  this.assertEquals("twoVal1", testListen.two);
+  gadgets.config.update({
+    one: { oneKey1: { oneSubkey1: "updated", oneSubkey2: "newpair" } },
+    two: [ "newtype" ],
+    three: { foo: 123 }
+  });
+  testListen = gadgets.config.get();
+  this.assertEquals("updated", testListen.one.oneKey1.oneSubkey1);
+  this.assertEquals("newpair", testListen.one.oneKey1.oneSubkey2);
+  this.assertEquals("data", testListen.one.oneKey2);
+  this.assertEquals("newtype", testListen.two[0]);
+  this.assertEquals(123, testListen.three.foo);
+};
+
+ConfigTest.prototype.testUpdateBeforeInit = function() {
+  var testListen = null;
+  gadgets.config.register("one", null, function(config) {
+    testListen = config;
+  });
+  gadgets.config.update({
+    one: { oneKey1: { oneSubkey1: "oneVal1", sticks: "stones" }, breaks: "bones" }
+  });
+  this.assertTrue(testListen === null);
+  gadgets.config.init({
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    one: { oneKey1: { oneSubkey1: "overwrite" } }
+  });
+  this.assertEquals("overwrite", testListen.one.oneKey1.oneSubkey1);
+  this.assertEquals("stones", testListen.one.oneKey1.sticks);
+  this.assertEquals("bones", testListen.one.breaks);
+};
+
+ConfigTest.prototype.testMergeFromInlineConfig = function() {
+  var testListen;
+  gadgets.config.register("one", null, function(config) {
+    testListen = config;
+  });
+  window["___config"] = { one: { oneKey1: { oneSubkey1: "override" } } };
+  gadgets.config.init({
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    one: { oneKey1: { oneSubkey1: "oneVal1" }, oneKey2: "data" },
+    two: "twoVal1"
+  });
+  this.assertEquals("override", testListen.one.oneKey1.oneSubkey1);
+  this.assertEquals("data", testListen.one.oneKey2);
+  this.assertEquals("twoVal1", testListen.two);
+};
+
+ConfigTest.prototype.testValidator = function() {
+  var validatorValue;
+  gadgets.config.register("testValidator", {data: function(value) {
+    validatorValue = value;
+    return true;
+  }});
+
+  gadgets.config.init({
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    testValidator: {data: "Hello, World!"}
+  });
+
+  this.assertEquals("Hello, World!", validatorValue);
+};
+
+ConfigTest.prototype.testValidatorMultiple = function() {
+  var validatorValue0;
+  gadgets.config.register("testValidator", {key0: function(value) {
+    validatorValue0 = value;
+    return true;
+  }});
+
+  var validatorValue1;
+  gadgets.config.register("testValidator", {key1: function(value) {
+    validatorValue1 = value;
+    return true;
+  }});
+
+  gadgets.config.init({
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    testValidator: {key0: "Hello, World!", key1: "Goodbye, World!"}
+  });
+
+  this.assertEquals("Hello, World!", validatorValue0);
+  this.assertEquals("Goodbye, World!", validatorValue1);
+};
+
+ConfigTest.prototype.testValidatorRejection = function() {
+  gadgets.config.register("testValidatorRejection", {data: function(value) {
+    return false;
+  }});
+
+  try {
+    gadgets.config.init({testValidatorRejection: {data: "Hello, World!"}});
+    this.fail("Did not throw an exception when validation failed.");
+  } catch (e) {
+    // Expected.
+  }
+};
+
+ConfigTest.prototype.testValidatorDisabled = function() {
+  var testValidatorDisabledConfig;
+  gadgets.config.register("testValidatorDisabled", {data: function(value) {
+    return false;
+  }},
+  function(config) {
+    testValidatorDisabledConfig = config.testValidatorDisabled;
+  });
+
+  gadgets.config.init({
+    'core.io': {
+      jsPath: '/js',
+      proxyUrl: '',
+      jsonProxyUrl: 'a',
+      unparseableCruft: ''
+    },
+    testValidatorDisabled: {data: "Hello, World!"}
+  }, true);
+
+  this.assertEquals("Hello, World!", testValidatorDisabledConfig.data);
+};
+
+ConfigTest.prototype.testEnumValidator = function() {
+  var validator = gadgets.config.EnumValidator("foo", "bar", "baz");
+
+  this.assertTrue(validator("foo"));
+  this.assertTrue(validator("bar"));
+  this.assertTrue(validator("baz"));
+  this.assertFalse(validator("junk"));
+};
+
+ConfigTest.prototype.testRegExValidator = function() {
+  var validator = gadgets.config.RegExValidator(/^hello.*$/);
+
+  this.assertTrue(validator("hello"));
+  this.assertTrue(validator("hello, world"));
+  this.assertTrue(validator("hellothere"));
+  this.assertFalse(validator("not hello"));
+};
+
+ConfigTest.prototype.testExistsValidator = function() {
+  var validator = gadgets.config.ExistsValidator;
+
+  this.assertTrue(validator("hello"));
+  this.assertTrue(validator(0));
+  this.assertTrue(validator(false));
+  this.assertTrue(validator(null));
+  this.assertTrue(validator(""));
+
+  this.assertFalse(validator({}.foo));
+};
+
+ConfigTest.prototype.testNonEmptyStringValidator = function() {
+  var validator = gadgets.config.NonEmptyStringValidator;
+
+  this.assertTrue(validator("hello"));
+
+  this.assertFalse(validator(0));
+  this.assertFalse(validator(false));
+  this.assertFalse(validator(null));
+  this.assertFalse(validator(""));
+  this.assertFalse(validator(undefined));
+};
+
+ConfigTest.prototype.testBooleanValidator = function() {
+  var validator = gadgets.config.BooleanValidator;
+
+  this.assertTrue(validator(true));
+  this.assertTrue(validator(false));
+
+  this.assertFalse(validator("hello"));
+  this.assertFalse(validator(0));
+  this.assertFalse(validator(null));
+  this.assertFalse(validator(undefined));
+};
+
+ConfigTest.prototype.testLikeValidator = function() {
+  var key0value, key1value;
+
+  var validator = gadgets.config.LikeValidator({
+    key0: function(data) {
+      key0value = data;
+      return true;
+    },
+    key1: function(data) {
+      key1value = data;
+      return true;
+    }
+  });
+
+  this.assertTrue(validator({key0:"Key0", key1: "Key1"}));
+  this.assertEquals("Key0", key0value);
+  this.assertEquals("Key1", key1value);
+};
+
+ConfigTest.prototype.testLikeValidatorWithFailure = function() {
+  var key0value, key1value;
+
+  var validator = gadgets.config.LikeValidator({
+    key0: function(data) {
+      key0value = data;
+      return false;
+    },
+    key1: function(data) {
+      key1value = data;
+      return true;
+    }
+  });
+
+  this.assertFalse(validator({key0:"Key0", key1: "Key1"}));
+  this.assertEquals("Key0", key0value);
+  this.assertEquals(null, key1value);
+};
diff --git a/trunk/features/src/test/javascript/features/core/prefstest.js b/trunk/features/src/test/javascript/features/core/prefstest.js
new file mode 100644
index 0000000..5ecd7f6
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/core/prefstest.js
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for gadgets.prefs.
+ * TODO cover more gadgets.prefs functions.
+ */
+
+function PrefsTest(name) {
+  TestCase.call(this, name);
+}
+
+PrefsTest.inherits(TestCase);
+
+PrefsTest.prototype.setUp = function() {
+  this.params = {myCounter: 100, myString: '15.3', myUndefined: undefined,
+      myObject: {}, myFloat: 3.3, myBool: true, myArray: ['one', 'two'],
+      boolString: 'true'};
+};
+
+PrefsTest.prototype.tearDown = function() {
+  this.params = undefined;
+};
+
+PrefsTest.prototype.testSetInternal_ = function() {
+  var pref = new gadgets.Prefs();
+
+  // First time set the prefs, expect returning true.
+  this.assertTrue(gadgets.Prefs.setInternal_(this.params, 100));
+  this.assertEquals(true, pref.getBool('boolString'));
+
+  // Set the same prefs, expect returning false.
+  this.assertFalse(gadgets.Prefs.setInternal_(this.params, 100));
+  this.assertEquals(true, pref.getBool('boolString'));
+
+  // Modify 'boolString' value, expect returning true.
+  this.assertTrue(gadgets.Prefs.setInternal_('boolString', false));
+  this.assertEquals(false, pref.getBool('boolString'));
+};
+
+PrefsTest.prototype.testGetInt = function() {
+    var expectedResults = {myCounter: 100, myString: 15, myUndefined: 0,
+        myObject: 0, myFloat: 3};
+
+    var pref = new gadgets.Prefs();
+
+    gadgets.Prefs.setInternal_(this.params, 100);
+
+    for (var userPref in expectedResults) {
+      this.assertEquals(expectedResults[userPref], pref.getInt(userPref));
+    }
+};
+
+PrefsTest.prototype.testGetFloat = function() {
+    var expectedResults = {myCounter: 100, myString: 15.3, myUndefined: 0,
+        myObject: 0, myFloat: 3.3};
+
+    var pref = new gadgets.Prefs();
+
+    gadgets.Prefs.setInternal_(this.params, 100);
+
+    for (var userPref in expectedResults) {
+      this.assertEquals(expectedResults[userPref], pref.getFloat(userPref));
+    }
+};
+
+PrefsTest.prototype.testGetBool = function() {
+    var expectedResults = {myCounter: true, myString: true, myUndefined: false,
+        myObject: false, myFloat: true, myBool: true, boolString: true,
+        myArray: false};
+
+    var pref = new gadgets.Prefs();
+
+    gadgets.Prefs.setInternal_(this.params, 100);
+
+    for (var userPref in expectedResults) {
+      this.assertEquals(expectedResults[userPref], pref.getBool(userPref));
+    }
+};
+
diff --git a/trunk/features/src/test/javascript/features/core/utiltest.js b/trunk/features/src/test/javascript/features/core/utiltest.js
new file mode 100644
index 0000000..44b5e3e
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/core/utiltest.js
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for gadgets.util.
+ * TODO cover more gadgets.util functions.
+ */
+
+function UtilTest(name) {
+  TestCase.call(this, name);
+}
+
+UtilTest.inherits(TestCase);
+
+UtilTest.prototype.setUp = function() {
+  this.oldDocument = document;
+};
+
+UtilTest.prototype.tearDown = function() {
+  document = this.oldDocument;
+};
+
+UtilTest.prototype.testMakeEnum = function() {
+  var val = ['Foo', 'BAR', 'baz'];
+  var obj = gadgets.util.makeEnum(val);
+  this.assertEquals('Foo', obj['Foo']);
+  this.assertEquals('BAR', obj['BAR']);
+  this.assertEquals('baz', obj['baz']);
+};
+
+UtilTest.prototype.testIsDebug = function() {
+  document = {
+    getElementsByTagName: function () {
+      return [
+        {src: 'http://www.example.com/foobar.js?debug=1'},
+        {src: 'http://www.example.com/js/features/foobar.js'},
+      ];
+    }
+  };
+
+  gadgets.config.init({'core.io':{jsPath: '/js/features', jsonProxyUrl: '/blah'}});
+  this.assertFalse('isDebug not set on the injected feature js.', gadgets.util.isDebug());
+};
+
+UtilTest.prototype.testIsDebug2 = function() {
+  document = {
+    getElementsByTagName: function () {
+      return [
+        {src: 'http://www.example.com/foobar.js?debug=0'},
+        {src: 'http://www.example.com/js/features/foobar.js?debug=1'},
+      ];
+    }
+  };
+
+  gadgets.config.init({'core.io':{jsPath: '/js/features', jsonProxyUrl: '/blah'}});
+  this.assertTrue('isDebug set on the injected feature js.', gadgets.util.isDebug());
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/embeddedexperiences/embedded_experiences_container_test.js b/trunk/features/src/test/javascript/features/embeddedexperiences/embedded_experiences_container_test.js
new file mode 100644
index 0000000..117dc48
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/embeddedexperiences/embedded_experiences_container_test.js
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Tests for container APIs for embedded experiences.
+ */
+
+function EEContainerTest(name) {
+  TestCase.call(this, name);
+}
+
+EEContainerTest.inherits(TestCase);
+EEContainerTest.prototype.setUp = function() {
+      this.apiUri = window.__API_URI;
+      window.__API_URI = shindig.uri('http://shindig.com');
+      this.containerUri = window.__CONTAINER_URI;
+      window.__CONTAINER_URI = shindig.uri('http://container.com');
+      this.shindigContainerGadgetSite = osapi.container.GadgetSite;
+      this.shindigContainerUrlSite = osapi.container.UrlSite;
+      this.shindigContainerPreload = osapi.container.Container.preloadGadget;
+      this.gadgetsRpc = gadgets.rpc;
+};
+
+EEContainerTest.prototype.tearDown = function() {
+      window.__API_URI = this.apiUri;
+      window.__CONTAINER_URI = this.containerUri;
+      osapi.container.GadgetSite = this.shindigContainerGadgetSite;
+      osapi.container.UrlSite = this.shindigContainerUrlSite;
+      osapi.container.Container.preloadGadget = this.shindigContainerPreload;
+      gadgets.rpc = this.gadgetsRpc;
+};
+
+EEContainerTest.prototype.testNavigateGadget = function() {
+      this.setupGadgetsRpcRegister();
+      var container = new osapi.container.Container({
+        'allowDefaultView' : true,
+        'renderCajole' : true,
+        'renderDebug' : true,
+        'renderTest' : true
+      });
+
+      var eeDataModel = {'gadget' : 'http://example.com/gadget.xml', 'context' : '123'};
+
+      this.setupGadgetSite(1, {}, null);
+      this.setupPreload();
+      container.ee.navigate({}, eeDataModel, {});
+      var renderParamDataModel = this.site_navigateTo_renderParams['eeDataModel'];
+      this.assertEquals('http://example.com/gadget.xml', renderParamDataModel.gadget);
+      this.assertEquals('123', renderParamDataModel.context);
+      this.assertEquals('embedded', this.site_navigateTo_renderParams['view']);
+      this.assertEquals('http://example.com/gadget.xml', this.site_navigateTo_gadgetUrl);
+      this.assertTrue(this.site_navigateTo_renderParams['allowDefaultView']);
+      this.assertTrue(this.site_navigateTo_renderParams['cajole']);
+      this.assertTrue(this.site_navigateTo_renderParams['debug']);
+      this.assertTrue(this.site_navigateTo_renderParams['nocache']);
+      this.assertTrue(this.site_navigateTo_renderParams['testmode']);
+
+};
+
+EEContainerTest.prototype.testNavigateGadgetWithAssociatedContext = function() {
+      this.setupGadgetsRpcRegister();
+      var container = new osapi.container.Container({});
+
+      var eeDataModel =
+        {'gadget' : 'http://example.com/gadget.xml', 'context' : {'label' : '123'}};
+
+      var container_context = {'associatedContext' : {'id': "123abc",
+                                                      'type': 'opensocial.ActivityEntry',
+                                                      'objectReference' : {}}};
+
+      this.setupGadgetSite(1, {}, null);
+      this.setupPreload();
+      container.ee.navigate({}, eeDataModel, {}, function() {}, container_context);
+
+      // verify
+      var renderParamDataModel = this.site_navigateTo_renderParams['eeDataModel'];
+      this.assertEquals('http://example.com/gadget.xml', renderParamDataModel.gadget);
+      var expectedContext = {"openSocial": {"associatedContext":{"id":"123abc","type":"opensocial.ActivityEntry","objectReference":{}}}, "label":"123"};
+      this.assertEquals(expectedContext, renderParamDataModel.context);
+};
+
+
+EEContainerTest.prototype.testNavigateGadgetWithPreferredExperience = function() {
+      this.setupGadgetsRpcRegister();
+      var container = new osapi.container.Container({});
+      var eeDataModel =
+        {'gadget' : 'http://example.com/gadget.xml', 'context' : {'objectid' : '123'},
+          'preferredExperience' : {'target' : {'type' : 'gadget','view' : 'my-ee-view', 'viewTarget' : 'DIALOG'},
+          'display' : {'type' : 'text', 'label' : 'Click me to say Hello World'}}};
+
+      this.setupGadgetSite(1, {}, null);
+      this.setupPreload();
+      container.ee.navigate({}, eeDataModel, {});
+
+      // verify
+      var renderParamDataModel = this.site_navigateTo_renderParams['eeDataModel'];
+      this.assertEquals({"objectid":"123"}, renderParamDataModel.context);
+      this.assertEquals('my-ee-view', this.site_navigateTo_renderParams['view']);
+
+};
+
+EEContainerTest.prototype.testNavigateGadgetWithBadGadgetTarget = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container({});
+  var eeDataModel =
+    {'url' : 'http://example.com/myee.html',
+      'preferredExperience' : {'target' : {'type' : 'gadget','view' : 'my-ee-view', 'viewTarget' : 'DIALOG'},
+      'display' : {'type' : 'text', 'label' : 'Click me to say Hello World'}}};
+
+  this.setupUrlSite(1, null, null);
+  this.setupPreload();
+  container.ee.navigate({}, eeDataModel, {});
+
+  // verify
+  this.assertEquals('http://example.com/myee.html', this.urlsite_render_url);
+
+};
+
+EEContainerTest.prototype.testNavigateGadgetWithBadUrlTarget = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container({});
+  var eeDataModel =
+    {'gadget' : 'http://example.com/myee.xml', 'context' : {'objectid' : '123'},
+      'preferredExperience' : {'target' : {'type' : 'url', 'viewTarget' : 'DIALOG'},
+      'display' : {'type' : 'text', 'label' : 'Click me to say Hello World'}}};
+
+  this.setupGadgetSite(1, {}, null);
+  this.setupPreload();
+  container.ee.navigate({}, eeDataModel, {});
+
+  // verify
+  var renderParamDataModel = this.site_navigateTo_renderParams['eeDataModel'];
+  this.assertEquals('http://example.com/myee.xml', renderParamDataModel.gadget);
+  this.assertEquals({"objectid":"123"}, renderParamDataModel.context);
+  this.assertEquals('embedded', this.site_navigateTo_renderParams['view']);
+
+};
+
+EEContainerTest.prototype.testGadgetNavigateWithCustomEENavigation = function() {
+      this.setupGadgetsRpcRegister();
+      var customEENavigate = function(dataModel) {
+        customNavigate = true;
+        if(dataModel.gadget)
+          return 'gadget';
+        else
+          return 'url';
+      };
+      var customNavigate = false;
+
+      var container = new osapi.container.Container({'GET_EE_NAVIGATION_TYPE' : customEENavigate});
+      var eeDataModel = {'gadget' : 'http://example.com/gadget.xml', 'context' : '123'};
+
+      this.setupGadgetSite(1, {}, null);
+      this.setupPreload();
+      container.ee.navigate({}, eeDataModel, {});
+
+      // verify
+      this.assertTrue(customNavigate);
+      var renderParamDataModel = this.site_navigateTo_renderParams['eeDataModel'];
+      this.assertEquals('http://example.com/gadget.xml', renderParamDataModel.gadget);
+      this.assertEquals('123', renderParamDataModel.context);
+      this.assertEquals('embedded', this.site_navigateTo_renderParams['view']);
+      this.assertEquals('http://example.com/gadget.xml', this.site_navigateTo_gadgetUrl);
+};
+
+EEContainerTest.prototype.setupGadgetsRpcRegister = function() {
+      gadgets.rpc = {
+        register: function() {
+        }
+      };
+};
+
+EEContainerTest.prototype.setupGadgetSite = function(id, gadgetInfo, gadgetHolder) {
+    var self = this;
+    osapi.container.GadgetSite = function() {
+        return {
+            'getId' : function() {
+                return id;
+            },
+            'navigateTo' : function(gadgetUrl, viewParams, renderParams, func) {
+                self.site_navigateTo_gadgetUrl = gadgetUrl;
+                self.site_navigateTo_viewParams = viewParams;
+                self.site_navigateTo_renderParams = renderParams;
+                func(gadgetInfo);
+            },
+            'getActiveSiteHolder' : function() {
+                return gadgetHolder;
+            }
+        };
+    };
+};
+
+EEContainerTest.prototype.setupUrlSite = function(id, url, urlHolder) {
+    var self = this;
+    osapi.container.UrlSite = function() {
+        return {
+            'getId' : function() {
+                return id;
+            },
+            'render' : function(url, renderParams) {
+                self.urlsite_render_url = url;
+                self.urlsite_render_renderParams = renderParams;
+            }
+        };
+    };
+};
+
+EEContainerTest.prototype.setupPreload = function() {
+  osapi.container.Container.prototype.preloadGadget = function(gadgetUrl, func) {
+    var ret = [];
+    ret[gadgetUrl] = {};
+    func(ret);
+  };
+};
diff --git a/trunk/features/src/test/javascript/features/json-xmltojson/jsonxmltojson-test.js b/trunk/features/src/test/javascript/features/json-xmltojson/jsonxmltojson-test.js
new file mode 100644
index 0000000..06e2257
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/json-xmltojson/jsonxmltojson-test.js
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+function JsonXmlToJsonTest(name) {
+  TestCase.call(this, name); // super
+}
+JsonXmlToJsonTest.inherits(TestCase);
+JsonXmlToJsonTest.prototype.setUp = function() {};
+JsonXmlToJsonTest.prototype.tearDown = function() {};
+
+JsonXmlToJsonTest.prototype.testConvertXmlToJson = function() {
+  var fakeDom = {
+    nodeName: '#document',
+    nodeType: 9,
+    attributes: [],
+    childNodes: [{
+      nodeName: 'embed',
+      nodeType: 1,
+      attributes: [],
+      childNodes: [{
+        nodeName: 'url',
+        nodeType: 1,
+        attributes: [],
+        childNodes: [{
+          nodeName: '#text',
+          attributes: [],
+          nodeType: 3,
+          nodeValue: 'http://www.example.com'
+        }]
+      }]
+    }]
+  };
+  fakeDom.firstChild = fakeDom.childNodes[0];
+  fakeDom.firstChild.firstChild = fakeDom.firstChild.childNodes[0];
+  fakeDom.firstChild.firstChild.firstChild = fakeDom.firstChild.firstChild.childNodes[0];
+
+  var result = gadgets.json.xml.convertXmlToJson(fakeDom),
+      expected = {embed:{url:'http://www.example.com'}};
+  this.assertEquals(expected, result);
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/jsondom/jsondom-test.js b/trunk/features/src/test/javascript/features/jsondom/jsondom-test.js
new file mode 100644
index 0000000..8766c8a
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/jsondom/jsondom-test.js
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+function JsonDomTest(name) {
+  TestCase.call(this, name);
+}
+
+JsonDomTest.inherits(TestCase);
+
+JsonDomTest.prototype.setUp = function() {
+};
+
+JsonDomTest.prototype.tearDown = function() {
+};
+
+JsonDomTest.prototype.testParsePrecached = function() {
+  // TODO: implement
+};
+
+JsonDomTest.prototype.testParseNotPreloadedDelegatesToBrowser = function() {
+  // TODO: implement
+};
+
+JsonDomTest.prototype.testPreloadSingleString = function() {
+  // TODO: implement
+};
+
+JsonDomTest.prototype.testPreloadSingleElement = function() {
+  // TODO: implement
+};
+
+JsonDomTest.prototype.testPreloadError = function() {
+  // TODO: implement
+};
+
+JsonDomTest.prototype.testPreloadFullTemplate = function() {
+  // TODO: implement
+};
diff --git a/trunk/features/src/test/javascript/features/mocks/env.js b/trunk/features/src/test/javascript/features/mocks/env.js
new file mode 100644
index 0000000..0d20804
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/mocks/env.js
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Provides a simulated browser environment.
+ * TODO Port John Resig's env.js.
+ */
+
+var document = {
+  location: {
+    href: "http://localhost"
+  },
+  getElementsByTagName: function (name) { return []; }
+};
+
+// See mocks.FakeWindow if you need something more full featured.
+var window = {};
+window['gadgets'] = {};
+
+var alert = function() {};
diff --git a/trunk/features/src/test/javascript/features/mocks/window.js b/trunk/features/src/test/javascript/features/mocks/window.js
new file mode 100644
index 0000000..153b644
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/mocks/window.js
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Provides a simulated window object.  Recommended usage is to reset
+ * the global window object in each test case that uses the window.
+ *
+ * Example:
+ * 
+ * ExampleTest.prototype.testSomething = function() {
+ *   window = new mocks.FakeWindow();
+ *   // Test things
+ * };
+ */
+
+var mocks = mocks || {};
+
+/**
+ * @constructor
+ * @description creates a new fake window object.
+ * @param {string} url Destination url.
+ * @param {string} target Name of window
+ * @param {string} options Options for window, such as size.
+ */
+mocks.FakeWindow = function(url, target, options) {
+  // Properties passed to window.open
+  this.url_ = url;
+  this.target_ = target;
+  this.options_ = options;
+
+  // Whether the window has been closed.
+  this.closed = false;
+
+  // Event handling.  Events array is always sorted in order of ascending
+  // execution time.
+  this.now_ = 1000000;
+  this.events_ = [];
+  this.nextEventId_ = 1000;
+};
+
+/**
+ * Replacement for window.open.
+ */
+mocks.FakeWindow.prototype.open = function(url, target, options) {
+  return new mocks.FakeWindow(url, target, options);
+};
+
+/**
+ * Replacement for window.close.
+ */
+mocks.FakeWindow.prototype.close = function() {
+  this.closed = true;
+};
+
+/**
+ * Replacement for window.setInterval
+ */
+mocks.FakeWindow.prototype.setInterval = function(callback, millis) {
+  var event = {
+    id: this.nextEventId_,
+    when: this.now_ += millis,
+    interval: millis,
+    callback: callback
+  };
+  this.events_.push(event);
+  this.sortEvents_();
+  ++this.nextEventId_;
+  return event.id;
+};
+
+mocks.FakeWindow.prototype.sortEvents_ = function(event) {
+  this.events_.sort(function (a, b) {
+    return a.when - b.when;
+  });
+};
+
+/**
+ * Replacement for window.clearInterval
+ */
+mocks.FakeWindow.prototype.clearInterval = function(id) {
+  // Removes a single event by copying everything but that event.
+  var remaining = [];
+  for (var i = 0; i < this.events_.length; ++i) {
+    e = this.events_[i];
+    if (e.id !== id) {
+      remaining.push(e);
+    }
+  }
+  if (this.events_.length === remaining.length) {
+    throw 'window.clearInterval failed, no event with id ' + id;
+  }
+  this.events_ = remaining;
+};
+
+/**
+ * Moves the clock forward, running any associated events.
+ */
+mocks.FakeWindow.prototype.incrementTime = function(millis) {
+  if (this.active) {
+    throw 'recursive invocation of window.incrementTime.  Cut that out';
+  }
+  this.active = true;
+
+  // Each iteration bumps the time just enough to run a single event, or
+  // else ends the loop.
+  var finish = this.now_ + millis;
+  do {
+    var ranEvent = false;
+    if (this.events_.length > 0) {
+      var e = this.events_[0];
+      if (e.when <= finish) {
+        this.now_ = e.when;
+        e.when += e.interval;
+        this.sortEvents_();
+        // Deliberately let exceptions propagate, it's probably a bug if
+        // a timer throws an exception.
+        e.callback();
+        ranEvent = true;
+      }
+    }
+  } while (ranEvent);
+  this.now_ = finish;
+  this.active = false;
+};
diff --git a/trunk/features/src/test/javascript/features/mocks/xhr.js b/trunk/features/src/test/javascript/features/mocks/xhr.js
new file mode 100644
index 0000000..3409772
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/mocks/xhr.js
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Provides a simulated XMLHttpRequest object.
+ *
+ * To use, create a FakeXhrFactory, then populate the factory
+ * with FakeXhrExpectation and FakeXhrResponse objects.
+ */
+
+var fakeXhr = fakeXhr || {};
+
+/**
+ * @class
+ * What XHR to expect.  Requests are matched based on method, url, headers
+ * query string parameters, and body parameters.  Parameter ordering does
+ * not matter.
+ *
+ * Parameters are assumed to be HTML form encoded.
+ *
+ * @name fakeXhr.Expectation
+ */
+
+/**
+ * Create a request.
+ *
+ * @constructor
+ */
+fakeXhr.Expectation = function(method, url) {
+  this.method = method;
+  this.url = url;
+  this.queryArgs = {};
+  this.bodyArgs = {};
+  this.headers = {};
+};
+
+fakeXhr.Expectation.prototype.setMethod = function(method) {
+  this.method = method;
+};
+
+fakeXhr.Expectation.prototype.setUrl = function(url) {
+  this.url = url;
+  var query = url.indexOf("?");
+  if (query !== -1) {
+    this.queryArgs = this.parseForm(url.substr(query+1));
+    this.url = url.substr(0, query);
+  }
+};
+
+fakeXhr.Expectation.prototype.setBodyArg = function(name, value) {
+  if (value !== null) {
+    this.bodyArgs[name] = value;
+  } else {
+    delete this.bodyArgs[name];
+  }
+};
+
+fakeXhr.Expectation.prototype.setQueryArg = function(name, value) {
+  if (value !== null) {
+    this.queryArgs[name] = value;
+  } else {
+    delete this.queryArgs[name];
+  }
+};
+
+fakeXhr.Expectation.prototype.setHeader = function(name, value) {
+  if (value !== null) {
+    this.headers[name] = value;
+  } else {
+    delete this.headers[name];
+  }
+};
+
+fakeXhr.Expectation.prototype.parseForm = function(form) {
+  var result = {};
+  if (form) {
+    var pairs = form.split("&");
+    for (var i=0; i < pairs.length; ++i) {
+      var arg = pairs[i].split("=");
+      // We use unescape here instead of decodeURIComponent because of a bug
+      // in Rhino: https://bugzilla.mozilla.org/show_bug.cgi?id=217257.
+      // Rhino fixed this ages ago, but there hasn't been a new release of
+      // the Berlios jsunit package that incorporates the fix.
+      var name = unescape(arg[0]);
+      var value = unescape(arg[1]);
+      result[name] = value;
+    }
+  }
+  return result;
+};
+
+fakeXhr.Expectation.prototype.toString = function() {
+  return gadgets.json.stringify(this);
+};
+
+fakeXhr.Expectation.prototype.checkMatch = function(testcase, other) {
+  testcase.assertEquals(this.method, other.method);
+  testcase.assertEquals(this.url, other.url);
+  this.checkTableEquals(testcase, "query", this.queryArgs, other.queryArgs);
+  this.checkTableEquals(testcase, "body", this.bodyArgs, other.bodyArgs);
+  this.checkTableEquals(testcase, "header", this.headers, other.headers);
+};
+
+fakeXhr.Expectation.prototype.checkTableEquals = function(testcase, type, x, y) {
+  // Check for things in x that aren't in y
+  var member;
+  for (member in x) if (x.hasOwnProperty(member)) {
+    testcase.assertEquals(
+        "wrong value for " + type + " parameter " + member,
+        x[member], y[member]);
+  }
+  // Check for things in y that aren't in x
+  for (member in y) if (y.hasOwnProperty(member)) {
+    testcase.assertEquals(
+        "extra value for " + type + " parameter " + member,
+        x[member], y[member]);
+  }
+};
+
+
+/**
+ * @class
+ * What data to return for an XMLHttpRequest.
+ *
+ * @name fakeXhr.Response
+ */
+
+/**
+ * Create a response.
+ *
+ * @constructor
+ */
+fakeXhr.Response = function(responseText, status) {
+  this.responseText = responseText;
+  this.status = status || 200;
+};
+
+fakeXhr.Response.prototype.getResponseText = function() {
+  return this.responseText;
+};
+
+fakeXhr.Response.prototype.getStatus = function() {
+  return this.status;
+};
+
+
+/**
+ * @class
+ * Holds a list of expected XMLHTTPRequests and responses.
+ *
+ * @name fakeXhr.Factory
+ */
+
+/**
+ * Create a factory to collect requests and responses.
+ *
+ * @constructor
+ */
+fakeXhr.Factory = function(testcase) {
+  this.testcase = testcase;
+  this.expectations = [];
+};
+
+/**
+ * Expect a certain request and return the specified response.
+ */
+fakeXhr.Factory.prototype.expect = function(expect, response) {
+  this.expectations.push({ "expect" : expect, "response" : response});
+};
+
+/**
+ * Finds the response matching a particular request.
+ */
+fakeXhr.Factory.prototype.find = function(req) {
+  this.testcase.assertTrue(this.expectations.length > 0);
+  var next = this.expectations.shift();
+  next.expect.checkMatch(this.testcase, req);
+  return next.response;
+};
+
+/**
+ * Create a new XMLHttpRequest that will be fed from the expectations
+ * associated with this factory.
+ */
+fakeXhr.Factory.prototype.getXhrConstructor = function() {
+  var factory = this;
+  return function() {
+    return new fakeXhr.Request(factory);
+  };
+};
+
+
+/**
+ * @class
+ * An XMLHTTPRequest object
+ *
+ * @name fakeXhr.Factory
+ */
+
+/**
+ * Create a new XMLHTTPRequest, with response data returned from the
+ * specified factory.
+ *
+ * @constructor
+ */
+fakeXhr.Request = function(factory) {
+  this.factory = factory;
+  this.actual = new fakeXhr.Expectation(null, null);
+  this.response = null;
+  this.onreadystatechange = null;
+};
+
+fakeXhr.Request.prototype.open = function(method, url, async) {
+  this.actual.setMethod(method);
+  this.actual.setUrl(url);
+};
+
+fakeXhr.Request.prototype.setRequestHeader = function(name, value) {
+  this.actual.setHeader(name, value);
+};
+
+fakeXhr.Request.prototype.send = function(body) {
+  this.actual.bodyArgs = this.actual.parseForm(body);
+  var response = this.factory.find(this.actual);
+  this.readyState = 4;
+  this.status = response.status;
+  this.responseText = response.responseText;
+  this.onreadystatechange();
+};
diff --git a/trunk/features/src/test/javascript/features/oauthpopup/oauthpopup-test.js b/trunk/features/src/test/javascript/features/oauthpopup/oauthpopup-test.js
new file mode 100644
index 0000000..edfca81
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/oauthpopup/oauthpopup-test.js
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+var gadgets = gadgets || {};
+
+function PopupTest(name) {
+  TestCase.call(this, name);
+};
+PopupTest.inherits(TestCase);
+
+PopupTest.prototype.setUp = function() {
+  this.oldWindow = window;
+  window = new mocks.FakeWindow();
+};
+
+PopupTest.prototype.tearDown = function() {
+  window = this.oldWindow;
+};
+
+PopupTest.prototype.testPopup = function() {
+  var opened = false;
+  var open = function() {
+    opened = true;
+  };
+  var closed = false;
+  var close = function() {
+    closed = true;
+  };
+  // Create the popup
+  var popup = new gadgets.oauth.Popup('destination', 'options', open, close);
+  var openerOnClick = popup.createOpenerOnClick();
+  var closerOnClick = popup.createApprovedOnClick();
+  this.assertNull('Window opened prematurely', popup.win_);
+  this.assertFalse('Opener callback was called', opened);
+
+  // Open the window
+  var ranDefaultAction = openerOnClick();
+  this.assertTrue('Window not opened', opened);
+  this.assertFalse('Ran browser default action on open', ranDefaultAction);
+  this.assertNotNull('Window was null', popup.win_);
+  this.assertEquals('Url incorrect', 'destination', popup.win_.url_);
+  this.assertEquals('Target incorrect', '_blank', popup.win_.target_);
+  this.assertEquals('Options incorrect', 'options', popup.win_.options_);
+
+  // Wait a bit for our events to run
+  window.incrementTime(1000);
+  this.assertFalse('closer callback called early', closed);
+
+  // User or site closes window
+  popup.win_.close();
+  window.incrementTime(100);
+  this.assertTrue('Closer callback not called', closed);
+};
+
+PopupTest.prototype.testPopup_userClick = function() {
+  var opened = false;
+  var open = function() {
+    opened = true;
+  };
+  var closed = false;
+  var close = function() {
+    closed = true;
+  };
+  // Create the popup
+  var popup = new gadgets.oauth.Popup('destination', 'options', open, close);
+  var openerOnClick = popup.createOpenerOnClick();
+  var closerOnClick = popup.createApprovedOnClick();
+
+  // Open the window
+  openerOnClick();
+
+  // Wait a bit for our events to run
+  window.incrementTime(1000);
+  this.assertFalse('closer callback called early', closed);
+
+  // User clicks link
+  var ranDefaultAction = closerOnClick();
+  this.assertFalse(ranDefaultAction);
+  this.assertTrue('Closer callback not called', closed);
+};
+
+PopupTest.prototype.testTimerCancelled = function() {
+  var open = function() {};
+  var closeCount = 0;
+  var close = function() {
+    ++closeCount;
+  };
+
+  // Create the popup
+  var popup = new gadgets.oauth.Popup('destination', 'options', open, close);
+  var openerOnClick = popup.createOpenerOnClick();
+  var closerOnClick = popup.createApprovedOnClick();
+
+  // Open the window
+  openerOnClick();
+
+  // Close the window
+  popup.win_.close();
+
+  // Wait a bit for our events to run
+  window.incrementTime(1000);
+  this.assertEquals('Wrong number of calls to close', 1, closeCount);
+  window.incrementTime(1000);
+  this.assertEquals('timer not cancelled', 1, closeCount);
+};
diff --git a/trunk/features/src/test/javascript/features/open-views/viewEnhancements-test.js b/trunk/features/src/test/javascript/features/open-views/viewEnhancements-test.js
new file mode 100644
index 0000000..0358a49
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/open-views/viewEnhancements-test.js
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+function ViewEnhancementsTest(name) {
+  TestCase.call(this, name);
+}
+
+ViewEnhancementsTest.inherits(TestCase);
+
+(function() {
+
+  var rpcs, oldRpc = gadgets.rpc;
+
+  ViewEnhancementsTest.prototype.setUp = function() {
+    rpcs = [];
+    gadgets.rpc = {
+      call: function() {
+        rpcs = [];
+        rpcs.push(arguments);
+      }
+    };
+  };
+
+  ViewEnhancementsTest.prototype.tearDown = function() {
+    gadgets.rpc.call = oldRpc;
+  };
+
+  ViewEnhancementsTest.prototype.testOpenGadget = function() {
+    var resultCallback = function() {};
+    var navigateCallback = function() {};
+    var params = {coordinates: {top: 100, left: 100}};
+
+    gadgets.views.openGadget(resultCallback, navigateCallback);
+
+    this.assertEquals('..', rpcs[0][0]);
+    this.assertEquals('gadgets.views.openGadget', rpcs[0][1]);
+    this.assertNotNull('Assert not null error', rpcs[0][2]);
+    this.assertNotNull('Assert not null error', rpcs[0][3]);
+    this.assertUndefined('Assert undefined error', rpcs[0][4]);
+
+    gadgets.views.openGadget(resultCallback, navigateCallback, params);
+    this.assertEquals(params, rpcs[0][4]);
+  };
+
+  ViewEnhancementsTest.prototype.testOpenEmbeddedExperience = function() {
+    var resultCallback = function() {};
+    var navigateCallback = function() {};
+    var dataModel = {'url': 'http://www.example.com'};
+    var params = {coordinates: {top: 100, left: 100}};
+
+    gadgets.views.openEmbeddedExperience(resultCallback, navigateCallback,
+        dataModel, params);
+
+    this.assertEquals('..', rpcs[0][0]);
+    this.assertEquals('gadgets.views.openEmbeddedExperience', rpcs[0][1]);
+    this.assertNotNull('Assert not null error', rpcs[0][2]);
+    this.assertNotNull('Assert not null error', rpcs[0][3]);
+    this.assertEquals('http://www.example.com', rpcs[0][4]['url']);
+    this.assertEquals(params, rpcs[0][5]);
+  };
+
+  ViewEnhancementsTest.prototype.testOpenUrl = function() {
+    var url = 'www...';
+    var navigateCallback = function() {};
+    var viewTarget = 'dialog';
+
+    gadgets.views.openUrl(url, navigateCallback);
+
+    this.assertEquals('..', rpcs[0][0]);
+    this.assertEquals('gadgets.views.openUrl', rpcs[0][1]);
+    this.assertNotNull('Assert not null error', rpcs[0][2]);
+    this.assertEquals(url, rpcs[0][3]);
+    this.assertUndefined('Assert undefined error', rpcs[0][4]);
+
+    gadgets.views.openUrl(url, navigateCallback, viewTarget);
+    this.assertEquals(viewTarget, rpcs[0][4]);
+
+    var coordinates = {coordinates: {top: 100, left: 100}};
+    gadgets.views.openUrl(url, navigateCallback, viewTarget, coordinates);
+    this.assertEquals(coordinates, rpcs[0][5]);
+  };
+
+  ViewEnhancementsTest.prototype.testClose = function() {
+
+    var site = {};
+
+    gadgets.views.close();
+
+    this.assertEquals('..', rpcs[0][0]);
+    this.assertEquals('gadgets.views.close', rpcs[0][1]);
+    this.assertNull('Assert null error', rpcs[0][2]);
+    this.assertUndefined('Assert undefined error', rpcs[0][3]);
+
+    gadgets.views.close(site);
+    this.assertEquals(site, rpcs[0][3]);
+  };
+
+  ViewEnhancementsTest.prototype.testSetReturnValue = function() {
+
+    var returnValue = {};
+
+    gadgets.views.setReturnValue(returnValue);
+
+    this.assertEquals('..', rpcs[0][0]);
+    this.assertEquals('gadgets.views.setReturnValue', rpcs[0][1]);
+    this.assertNull('Assert null error', rpcs[0][2]);
+    this.assertEquals(returnValue, rpcs[0][3]);
+  };
+
+  ViewEnhancementsTest.prototype.testGetContainerDimensions = function() {
+    var resultCallback = function() {};
+
+    gadgets.window.getContainerDimensions(resultCallback);
+
+    this.assertEquals('..', rpcs[0][0]);
+    this.assertEquals('gadgets.window.getContainerDimensions', rpcs[0][1]);
+    this.assertEquals(resultCallback, rpcs[0][2]);
+  };
+
+})();
diff --git a/trunk/features/src/test/javascript/features/opensearch/opensearch_test.js b/trunk/features/src/test/javascript/features/opensearch/opensearch_test.js
new file mode 100644
index 0000000..8ee29ac
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensearch/opensearch_test.js
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ * 
+ * Unittests for the opensearch feature
+ */
+function OpenSearchTest(name) {
+  TestCase.call(this, name);
+}
+
+OpenSearchTest.inherits(TestCase);
+
+OpenSearchTest.prototype.setUp = function() {
+  this.apiUri = window.__API_URI;
+  window.__API_URI = shindig.uri('http://shindig.com');
+  this.containerUri = window.__CONTAINER_URI;
+  window.__CONTAINER_URI = shindig.uri('http://container.com');
+  this.shindigContainerGadgetSite = osapi.container.GadgetSite;
+  this.shindigContainerUrlSite = osapi.container.UrlSite;
+  this.gadgetsRpc = gadgets.rpc;
+};
+
+OpenSearchTest.prototype.tearDown = function() {
+  window.__API_URI = this.apiUri;
+  window.__CONTAINER_URI = this.containerUri;
+  osapi.container.GadgetSite = this.shindigContainerGadgetSite;
+  osapi.container.UrlSite = this.shindigContainerUrlSite;
+  gadgets.rpc = this.gadgetsRpc;
+};
+
+setDescriptions = function(container) {
+  container.opensearch
+  .setDescriptions_( {
+    "http://hosting.gmodules.com/ig/gadgets/file/109228598702359180066/twitterfinal.xml" : {
+    "OpenSearchDescription" : {
+    "@xmlns" : "http://a9.com/-/spec/opensearch/1.1/",
+    "#text" : [ "\u000a ", "\u000a ", "\u000a ", "\u000a ", "\u000a ",
+                "\u000a ", "\u000a\u000a" ],
+                "ShortName" : "Twitter Search",
+                "Description" : "Realtime Twitter Search",
+                "Url" : {
+    "@type" : "application/atom+xml",
+    "@method" : "get",
+    "@template" : "http://search.twitter.com/search.atom?q={searchTerms}"
+  },
+  "Image" : {
+    "@width" : "16",
+    "@height" : "16",
+    "#text" : "http://search.twitter.com/favicon.png"
+  },
+  "InputEncoding" : "UTF-8",
+  "SearchForm" : "http://search.twitter.com/"
+  }
+  },
+  "http://hosting.gmodules.com/ig/gadgets/file/109228598702359180066/myspacefinal.xml" : {
+    "OpenSearchDescription" : {
+    "@xmlns" : "http://a9.com/-/spec/opensearch/1.1/",
+    "#text" : [ "\u000a ", "\u000a ", "\u000a ", "\u000a ", "\u000a ",
+                "\u000a ", "\u000a" ],
+                "ShortName" : "MySpace Video",
+                "Description" : "Search MySpace videos.",
+                "Tags" : "myspace opensearch search video",
+                "Image" : {
+    "@height" : "16",
+    "@width" : "16",
+    "@type" : "image/x-icon",
+    "#text" : "http://www.myspace.com/favicon.ico"
+  },
+  "Url" : {
+    "@type" : "application/atom+xml",
+    "@xmlns:myspace" : "http://api.myspace.com/-/opensearch/extensions/1.0/",
+    "@template" : "http://api.myspace.com/opensearch/videos?format=xml&searchTerms={searchTerms}"
+  },
+  "Attribution" : "Search data Copyright 2003-2010 MySpace.com. All Rights Reserved."
+  }
+  },
+  "http://hosting.gmodules.com/ig/gadgets/file/109228598702359180066/myspacemultitemplate.xml" : {
+    "OpenSearchDescription" : {
+    "@xmlns" : "http://a9.com/-/spec/opensearch/1.1/",
+    "#text" : [ "\u000a ", "\u000a ", "\u000a ", "\u000a ", "\u000a ",
+                "\u000a ", "\u000a\u000a ", "\u000a ", "\u000a" ],
+                "ShortName" : "MySpace Video",
+                "Description" : "Search MySpace videos.",
+                "Tags" : "myspace opensearch search video",
+                "Image" : {
+    "@height" : "16",
+    "@width" : "16",
+    "@type" : "image/x-icon",
+    "#text" : "http://www.myspace.com/favicon.ico"
+  },
+  "Url" : [
+           {
+             "@type" : "text/html",
+             "@xmlns:myspace" : "http://api.myspace.com/-/opensearch/extensions/1.0/",
+             "@template" : "http://searchservice.myspace.com/index.cfm?fuseaction=sitesearch.results&type=MySpaceTV&qry={searchTerms}&pg={startPage?}"
+           },
+           {
+             "@type" : "application/atom+json",
+             "@xmlns:myspace" : "http://api.myspace.com/-/opensearch/extensions/1.0/",
+             "@template" : "http://api.myspace.com/opensearch/videos?format=json&searchTerms={searchTerms}&count={count?}&startPage={startPage?}&tag={myspace:tag?}&videoMode={myspace:videoMode?}&culture={myspace:culture?}"
+           },
+           {
+             "@type" : "application/atom+xml",
+             "@xmlns:myspace" : "http://api.myspace.com/-/opensearch/extensions/1.0/",
+             "@template" : "http://api.myspace.com/opensearch/videos?format=xml&searchTerms={searchTerms}&count={count?}&startPage={startPage?}&tag={myspace:tag?}&videoMode={myspace:videoMode?}&culture={myspace:culture?}"
+           } ],
+           "Attribution" : "Search data Copyright 2003-2010 MySpace.com. All Rights Reserved."
+  }
+  }
+  });
+}
+
+//The following two methods should be uncommented for unit testing after 
+//uncommenting the setDescriptions method in the opensearch.js code.
+/*OpenSearchTest.prototype.testUrls = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container();
+  setDescriptions(container);
+  urls = container.opensearch.getOpenSearchURLs();
+  this.assertEquals('all urls', 5, urls.length);
+  urls = container.opensearch.getOpenSearchURLs("application/atom+xml");
+  this.assertEquals('atom urls', 3, urls.length);
+  urls = container.opensearch.getOpenSearchURLs("text/html");
+  this.assertEquals('text/html urls', 1, urls.length);
+  urls = container.opensearch.getOpenSearchURLs("application/shindig");
+  this.assertEquals('bad type urls', 0, urls.length);
+
+};*/
+
+/*OpenSearchTest.prototype.testDescriptions = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container();
+
+  setDescriptions(container);
+
+  descriptions = container.opensearch.getOpenSearchDescriptions();
+  this.assertEquals('all descriptions', 3, descriptions.length);
+  descriptions = container.opensearch
+  .getOpenSearchDescriptions("application/atom+xml");
+  this.assertEquals('atom descriptions', 3, descriptions.length);
+  descriptions = container.opensearch.getOpenSearchDescriptions("text/html");
+  this.assertEquals('text/html descriptions', 1, descriptions.length);
+  descriptions = container.opensearch
+  .getOpenSearchDescriptions("application/shindig");
+  this.assertEquals('bad type descriptions', 0, descriptions.length);
+};*/
+
+OpenSearchTest.prototype.testCallbacks = function() {
+  this.setupGadgetsRpcRegister();
+  var container = new osapi.container.Container();
+  callback = function() {
+  };
+  //add callback to the container
+  container.opensearch.addOpenSearchCallback(callback);
+  //remove callback
+  this.assertTrue(container.opensearch.removeOpenSearchCallback(callback));
+  //check that the removed callback is no longer there. 
+  this.assertFalse(container.opensearch.removeOpenSearchCallback(callback));
+};
+
+OpenSearchTest.prototype.setupGadgetSite = function(id, gadgetInfo,
+    gadgetHolder) {
+  var self = this;
+  osapi.container.GadgetSite = function() {
+    return {
+      'getId' : function() {
+      return id;
+    },
+    'navigateTo' : function(gadgetUrl, viewParams, renderParams, func) {
+      self.site_navigateTo_gadgetUrl = gadgetUrl;
+      self.site_navigateTo_viewParams = viewParams;
+      self.site_navigateTo_renderParams = renderParams;
+      func(gadgetInfo);
+    },
+    'getActiveSiteHolder' : function() {
+      return gadgetHolder;
+    }
+    };
+  };
+};
+
+OpenSearchTest.prototype.setupGadgetsRpcRegister = function() {
+  gadgets.rpc = {
+      register : function() {
+      }
+  }
+};
diff --git a/trunk/features/src/test/javascript/features/opensocial-base/jsonactivitytest.js b/trunk/features/src/test/javascript/features/opensocial-base/jsonactivitytest.js
new file mode 100644
index 0000000..23cdfbb
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-base/jsonactivitytest.js
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+var gadgets = gadgets || {};
+
+function JsonActivityTest(name) {
+  TestCase.call(this, name);
+};
+JsonActivityTest.inherits(TestCase);
+
+JsonActivityTest.prototype.setUp = function() {
+  // Prepare for mocks
+  this.oldGetField = opensocial.Container.getField;
+  opensocial.Container.getField = function(fields, key, opt_params) {
+    return fields[key];
+  };
+};
+
+JsonActivityTest.prototype.tearDown = function() {
+  // Remove mocks
+  opensocial.Container.getField = this.oldGetField;
+};
+
+JsonActivityTest.prototype.testConstructArrayObject = function() {
+  var map = {'fakeClass' : [{'field1' : 'value1'}, {'field2' : 'value2'}]};
+  FakeClass = function(opt_params) {
+    this.fields = opt_params;
+  };
+
+  JsonActivity.constructArrayObject(map, 'fakeClass', FakeClass);
+
+  var result = map['fakeClass'];
+  this.assertTrue(result instanceof Array);
+  this.assertTrue(result[0] instanceof FakeClass);
+  this.assertTrue(result[1] instanceof FakeClass);
+  this.assertEquals('value1', result[0].fields['field1']);
+  this.assertEquals('value2', result[1].fields['field2']);
+};
+
+JsonActivityTest.prototype.testJsonActivityConstructor = function() {
+  var activity = new JsonActivity({'title' : 'green',
+    'mediaItems' : [{'mimeType' : 'black', 'url' : 'white',
+      'type' : 'orange'}]});
+
+  var fields = opensocial.Activity.Field;
+  this.assertEquals('green', activity.getField(fields.TITLE));
+
+  var mediaItems = activity.getField(fields.MEDIA_ITEMS);
+  this.assertTrue(mediaItems instanceof Array);
+  this.assertTrue(mediaItems[0] instanceof JsonMediaItem);
+
+  var mediaItemFields = opensocial.MediaItem.Field;
+  this.assertEquals('black', mediaItems[0].getField(mediaItemFields.MIME_TYPE));
+  this.assertEquals('white', mediaItems[0].getField(mediaItemFields.URL));
+  this.assertEquals('orange', mediaItems[0].getField(mediaItemFields.TYPE));
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-base/jsonalbumtest.js b/trunk/features/src/test/javascript/features/opensocial-base/jsonalbumtest.js
new file mode 100644
index 0000000..9b89331
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-base/jsonalbumtest.js
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+var gadgets = gadgets || {};
+
+function JsonAlbumTest(name) {
+  TestCase.call(this, name);
+};
+JsonAlbumTest.inherits(TestCase);
+
+JsonAlbumTest.prototype.setUp = function() {
+  // Prepare for mocks
+  this.oldGetField = opensocial.Container.getField;
+  opensocial.Container.getField = function(fields, key, opt_params) {
+    return fields[key];
+  };
+};
+
+JsonAlbumTest.prototype.tearDown = function() {
+  // Remove mocks
+  opensocial.Container.getField = this.oldGetField;
+};
+
+JsonAlbumTest.prototype.testJsonAlbumConstructor = function() {
+	var album = new JsonAlbum( {
+		'id' : 1,
+		'ownerId' : 2,
+		'title' : 'test-title',
+		'description' : 'test-description',
+		'location' : {'locality' : 'test-locality', 'country' : 'test-country'},
+		'mediaItemCount' : 3,
+		'mediaMimeType' : [ 'jpg' ],
+		'mediaType' : 'image',
+		'thumbnailUrl' : 'test-thumbnailUrl'
+	});
+	
+	var fields = opensocial.Album.Field;
+	this.assertEquals(1, album.getField(fields.ID));
+	this.assertEquals(2, album.getField(fields.OWNER_ID));
+	this.assertEquals(3, album.getField(fields.MEDIA_ITEM_COUNT));
+	this.assertEquals('test-title', album.getField(fields.TITLE));
+	this.assertEquals('test-description', album.getField(fields.DESCRIPTION));
+	
+	var location = album.getField(fields.LOCATION);
+	this.assertTrue(location instanceof opensocial.Address);
+	this.assertEquals('test-locality', location.getField(opensocial.Address.Field.LOCALITY));
+	this.assertEquals('test-country', location.getField(opensocial.Address.Field.COUNTRY));
+	
+	var mimeTypes = album.getField(fields.MEDIA_MIME_TYPE);
+	this.assertTrue(mimeTypes instanceof Array);
+	this.assertEquals('jpg', mimeTypes[0]);
+	
+	this.assertEquals(opensocial.MediaItem.Type.IMAGE, album.getField(fields.MEDIA_TYPE));
+	this.assertEquals('test-thumbnailUrl', album.getField(fields.THUMBNAIL_URL));
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-base/jsonmediaitemtest.js b/trunk/features/src/test/javascript/features/opensocial-base/jsonmediaitemtest.js
new file mode 100644
index 0000000..d3a370d
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-base/jsonmediaitemtest.js
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+var gadgets = gadgets || {};
+
+function JsonMediaItemTest(name) {
+  TestCase.call(this, name);
+};
+JsonMediaItemTest.inherits(TestCase);
+
+JsonMediaItemTest.prototype.setUp = function() {
+  // Prepare for mocks
+  this.oldGetField = opensocial.Container.getField;
+  opensocial.Container.getField = function(fields, key, opt_params) {
+    return fields[key];
+  };
+};
+
+JsonMediaItemTest.prototype.tearDown = function() {
+  // Remove mocks
+  opensocial.Container.getField = this.oldGetField;
+};
+
+JsonMediaItemTest.prototype.testJsonMediaItemConstructor = function() {
+  var mediaItem = new JsonMediaItem({'mimeType' : 'black', 'url' : 'white',
+      'type' : 'orange'});
+
+  var fields = opensocial.MediaItem.Field;
+  this.assertEquals('black', mediaItem.getField(fields.MIME_TYPE));
+  this.assertEquals('white', mediaItem.getField(fields.URL));
+  this.assertEquals('orange', mediaItem.getField(fields.TYPE));
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-data-context/datacontexttest.js b/trunk/features/src/test/javascript/features/opensocial-data-context/datacontexttest.js
new file mode 100644
index 0000000..8a995a7
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-data-context/datacontexttest.js
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the opensocial-data-context feature.
+ */
+function DataContextTest(name) {
+  TestCase.call(this, name);
+}
+
+DataContextTest.inherits(TestCase);
+
+DataContextTest.prototype.setUp = function() {
+};
+
+DataContextTest.prototype.tearDown = function() {
+  var dataSets = opensocial.data.getDataContext().dataSets;
+  for (var key in dataSets) {
+    if (dataSets.hasOwnProperty(key)) {
+      delete dataSets[key];
+    }
+  }
+};
+
+DataContextTest.prototype.testPutDataSet = function() {
+  var context = opensocial.data.getDataContext();
+
+  context.putDataSet('key', 'value');
+  this.assertEquals('value', context.getDataSet('key'));
+  
+  // Test that putting null and undefined don't change the value.
+  // TODO: this seems wrong;  why not support removing data?
+  context.putDataSet('key', null);
+  this.assertEquals('value', context.getDataSet('key'));
+
+  context.putDataSet('key', undefined);
+  this.assertEquals('value', context.getDataSet('key'));
+};
+
+/**
+ * Test registerListener()
+ */
+DataContextTest.prototype.testRegisterListener = function() {
+  var context = opensocial.data.getDataContext();
+  var listenerCalledWithKey = null;
+  var that = this;
+  var listener = function(key) {
+    listenerCalledWithKey = key;
+    that.assertNotNull(key);
+  };
+  
+  context.registerListener('key', listener);
+  this.assertNull(listenerCalledWithKey);
+  
+  context.putDataSet('other', 1);
+  this.assertNull(listenerCalledWithKey);
+
+  context.putDataSet('key', 2);
+  this.assertEquals('key', listenerCalledWithKey[0]);
+
+  listenerCalledWithKey = null;
+  context.putDataSet('key', 3);
+  this.assertEquals('key', listenerCalledWithKey[0]);
+};
+
+/**
+ * Test registerListener()
+ */
+DataContextTest.prototype.testRegisterListenerWithArray = function() {
+  var context = opensocial.data.getDataContext();
+  var listenerCalledWithKey = null;
+  var that = this;
+  var listener = function(key) {
+    listenerCalledWithKey = key;
+    that.assertNotNull(key);
+  };
+  
+  context.registerListener(['aone', 'atwo'], listener);
+  this.assertNull(listenerCalledWithKey);
+
+  context.putDataSet('aone', 1);
+  this.assertNull(listenerCalledWithKey);
+
+  context.putDataSet('atwo', 2);
+  this.assertEquals('atwo', listenerCalledWithKey[0]);
+
+  context.putDataSet('aone', 3);
+  this.assertEquals('aone', listenerCalledWithKey[0]);
+};
+
+/**
+ * Test registerListener() with '*'
+ */
+DataContextTest.prototype.testRegisterListenerWithStar = function() {
+  var context = opensocial.data.getDataContext();
+  var listenerCalledWithKey = null;
+  var that = this;
+  var listener = function(key) {
+    listenerCalledWithKey = key;
+    that.assertNotNull(key);
+  };
+  
+  context.registerListener('*', listener);
+  this.assertNull(listenerCalledWithKey);
+  
+  context.putDataSet('one', 1);
+  this.assertEquals('one', listenerCalledWithKey[0]);
+
+  context.putDataSet('two', 2);
+  this.assertEquals('two', listenerCalledWithKey[0]);
+};
+
+/**
+ * Test getData()
+ */
+DataContextTest.prototype.testGetData = function() {
+  var context = opensocial.data.getDataContext();
+  context.putDataSet('key', 'value');
+  this.assertEquals('value', context.getData()['key']);
+  context.putDataSet('key', 'value2');
+  this.assertEquals('value2', context.getData()['key']);
+  
+  // Test that altering the result of getData doesn't change the context
+  var data = context.getData();
+  data['key'] = 'ball';
+  this.assertEquals('value2', context.getDataSet('key'));
+};
+
+/**
+ * Test putDataSets()
+ */
+DataContextTest.prototype.testPutDataSets = function() {
+  var context = opensocial.data.getDataContext();
+  var counter = 0;
+  var passedKeys = null;
+  var listener = function(keys) {
+    counter++;
+    passedKeys = keys;
+  };
+  context.registerListener(['sets1', 'sets2'], listener);
+  context.putDataSets({ sets1: 'a', sets2: 'b' });
+  this.assertEquals('a', context.getDataSet('sets1'));
+  this.assertEquals('b', context.getDataSet('sets2'));
+  
+  // Test that listener was only called once.
+  this.assertEquals(1, counter);
+  
+  // Test that listener was passed both keys.
+  this.assertEquals(2, passedKeys.length);
+};
+
+/**
+ * Test registerOneTimeListener_()
+ */
+DataContextTest.prototype.testOneTimeListener = function() {
+  var context = opensocial.data.getDataContext();
+  var counter = 0;
+  var listener = function(keys) {
+    counter++;
+  };
+  context.registerOneTimeListener_('oneTime', listener);
+  context.putDataSet('oneTime', 'foo');
+  this.assertEquals(1, counter);
+  context.putDataSet('oneTime', 'bar');
+  this.assertEquals(1, counter);
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-data-context/index.html b/trunk/features/src/test/javascript/features/opensocial-data-context/index.html
new file mode 100644
index 0000000..c6178d6
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-data-context/index.html
@@ -0,0 +1,86 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+<head>
+  <title>Opensocial Data Context Tests</title>
+  <script>
+    function TestCase() {};
+    Function.prototype.inherits = function() {};
+  </script>
+  <script src="../../../../main/javascript/features/opensocial-data-context/datacontext.js"></script>  
+  <script src="datacontexttest.js"></script>
+  <script type="text/javascript">
+      DataContextTest.prototype.assertNotNull = function(a) {
+        if (a === null) {
+          throw("Null: " + a);
+        }
+      };
+      DataContextTest.prototype.assertNull = function(a) {
+        if (a !== null) {
+          throw("Not null: " + a);
+        }
+      };
+      DataContextTest.prototype.assertEquals = function(a, b) {
+        if (a !== b) {
+          throw("Not equal: " + typeof(a) + "[" + a + "] and " + typeof(b) + "[" + b + "]");
+        }
+      };
+      
+      function exposeTestFunctionNames(obj) {
+        var testSource = obj ? obj.prototype : 
+            (typeof RuntimeObject != 'undefined' ? RuntimeObject('test' + '*') : self);
+        var testFunctionNames = [];
+        for (var i in testSource) {
+          if (i.substring(0, 4) == 'test' && typeof(testSource[i]) == 'function')
+           testFunctionNames.push(i);
+        }
+        return testFunctionNames;
+      }
+
+      function runAllTests() {
+        var log = function(msg, forcePage) {
+          if (window.console && !forcePage) {
+            console.log(msg);
+            return;
+          } 
+          var div = document.createElement("div");
+          div.appendChild(document.createTextNode(msg));
+          document.body.appendChild(div); 
+        };
+        var obj = new DataContextTest();
+        var tests = exposeTestFunctionNames(DataContextTest);
+        var failed = 0;
+        for (var i = 0; i < tests.length; i++) {
+          log(tests[i]);
+          try {
+            obj[tests[i]]();
+            log("OK");
+          } catch (e) {
+            log("FAIL: " + e);
+            failed++;
+          }
+        }
+        log("All finished. " + i + " run. " + failed + " failed.", true);
+      }
+    </script>  
+</head>
+<body>
+    <input type="button" onclick="runAllTests()" value="Run tests"/>
+</body>
+</html>
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-data/datatest.js b/trunk/features/src/test/javascript/features/opensocial-data/datatest.js
new file mode 100644
index 0000000..8fb1d57
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-data/datatest.js
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the opensocial-data-context feature.
+ */
+function DataTest(name) {
+  TestCase.call(this, name);
+}
+
+DataTest.inherits(TestCase);
+
+DataTest.prototype.setUp = function() {
+};
+
+DataTest.prototype.testParseExpression = function() {
+  var expressions = [
+    [ "Hello world", null ],
+    [ "${foo}", "(foo)" ],
+    [ "Hello ${foo} world", "'Hello '+(foo)+' world'" ],
+    [ "${foo} ${bar}", "(foo)+' '+(bar)" ]
+  ];
+  for (var i = 0; i < expressions.length; i++) {
+    this.assertEquals(
+        expressions[i][1],
+        opensocial.data.parseExpression_(expressions[i][0])
+    );
+  }
+};
+
+DataTest.prototype.testEvalExpression = function() {
+  var data = {
+    'foo': 'Hello',
+    'bar': 'World'
+  };
+  data.name = {
+    'first': 'John',
+    'last': 'Doe'
+  };
+  opensocial.data.DataContext.putDataSet("test", data);
+  var expressions = [
+    [ opensocial.data.parseExpression_("Test: ${test.foo}"), "Test: Hello" ],
+    [ opensocial.data.parseExpression_("${test.foo} ${test.bar}!"), "Hello World!" ],
+    [ opensocial.data.parseExpression_("${test.name.first} ${test.name.last}"), "John Doe" ]
+  ];
+  for (var i = 0; i < expressions.length; i++) {
+    this.assertEquals(
+        expressions[i][1],
+        opensocial.data.DataContext.evalExpression(expressions[i][0])
+    );
+  }
+};
+
+/**
+ * Unit test to test data result handlers.
+ */
+DataTest.prototype.testPutDataSet = function() {
+  var key = 'test1';
+  var value = 'foo';
+  opensocial.data.DataContext.putDataSet(key, value);
+  this.assertEquals(value, opensocial.data.DataContext.getDataSet(key));
+};
+
+function registerNS(prefix) {
+  opensocial.xmlutil.NSMAP[prefix] = "#" + prefix;
+};
+
+/**
+ * Unit test to check full functionality of a request handler.
+ */
+DataTest.prototype.testRequestHandler = function() {
+  registerNS("test");
+  var results = {};
+  
+  opensocial.data.registerRequestHandler('test:request', function(descriptor) {
+    results[descriptor.key] = descriptor.getAttribute('data');
+  });
+  
+  var xmlData =
+      '<test:request key="first" data="testData"/>' +
+      '<test:request key="second" data="${foo}"/>';
+  
+  try {
+    opensocial.data.loadRequests(xmlData);
+  } catch (e) {
+    // TODO: This test breaks because Mavan's JSUnit doesn't implement neither 
+    // IE's nor FF's XML parsing interface.
+    return;
+  }
+
+  this.assertNotNull(opensocial.data.requests_['first']);
+  this.assertNotNull(opensocial.data.requests_['second']);
+
+  opensocial.data.DataContext.putDataSet("foo", "bar");
+  opensocial.data.executeRequests();
+
+  this.assertEquals('testData', results['first']);
+  this.assertEquals('bar', results['second']);
+};
+
+/**
+ * Unit test to test listener functionality when a data key is put.
+ */
+DataTest.prototype.testListener = function() {
+  var fired = false;
+  opensocial.data.DataContext.registerListener('testKey', function() {
+    fired = true;
+  });
+  opensocial.data.DataContext.putDataSet('testKey', {});
+  this.assertEquals(true, fired);
+};
+
diff --git a/trunk/features/src/test/javascript/features/opensocial-data/index.html b/trunk/features/src/test/javascript/features/opensocial-data/index.html
new file mode 100644
index 0000000..fb4ee50
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-data/index.html
@@ -0,0 +1,88 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html>
+<head>
+  <title>Opensocial Data Tests</title>
+  <script>
+    function TestCase() {};
+    Function.prototype.inherits = function() {};
+  </script>
+  <script src="../../../../main/javascript/features/opensocial-data-context/datacontext.js"></script>
+  <script src="../../../../main/javascript/features/xmlutil/xmlutil.js"></script>
+  <script src="../../../../main/javascript/features/opensocial-data/data.js"></script>  
+  <script src="datatest.js"></script>
+  <script type="text/javascript">
+      DataTest.prototype.assertNotNull = function(a) {
+        if (a === null) {
+          throw("Null: " + a);
+        }
+      };
+      DataTest.prototype.assertNull = function(a) {
+        if (a !== null) {
+          throw("Not null: " + a);
+        }
+      };
+      DataTest.prototype.assertEquals = function(a, b) {
+        if (a !== b) {
+          throw("Not equal: " + typeof(a) + "[" + a + "] and " + typeof(b) + "[" + b + "]");
+        }
+      };
+      
+      function exposeTestFunctionNames(obj) {
+        var testSource = obj ? obj.prototype : 
+            (typeof RuntimeObject != 'undefined' ? RuntimeObject('test' + '*') : self);
+        var testFunctionNames = [];
+        for (var i in testSource) {
+          if (i.substring(0, 4) == 'test' && typeof(testSource[i]) == 'function')
+           testFunctionNames.push(i);
+        }
+        return testFunctionNames;
+      }
+
+      function runAllTests() {
+        var log = function(msg, forcePage) {
+          if (window.console && !forcePage) {
+            console.log(msg);
+            return;
+          } 
+          var div = document.createElement("div");
+          div.appendChild(document.createTextNode(msg));
+          document.body.appendChild(div); 
+        };
+        var obj = new DataTest();
+        var tests = exposeTestFunctionNames(DataTest);
+        var failed = 0;
+        for (var i = 0; i < tests.length; i++) {
+          log(tests[i]);
+          try {
+            obj[tests[i]]();
+            log("OK");
+          } catch (e) {
+            log("FAIL: " + e);
+            failed++;
+          }
+        }
+        log("All finished. " + i + " run. " + failed + " failed.", true);
+      }
+    </script>  
+</head>
+<body>
+    <input type="button" onclick="runAllTests()" value="Run tests"/>
+</body>
+</html>
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-reference/activitytest.js b/trunk/features/src/test/javascript/features/opensocial-reference/activitytest.js
new file mode 100644
index 0000000..f0a4464
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-reference/activitytest.js
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+var gadgets = gadgets || {};
+
+function ActivityTest(name) {
+  TestCase.call(this, name);
+};
+ActivityTest.inherits(TestCase);
+
+ActivityTest.prototype.setUp = function() {
+  // Prepare for mocks
+  gadgets.util = gadgets.util || {};
+  this.oldEscape = gadgets.util.escape;
+  gadgets.util.escape = function(param) {
+    return param;
+  };
+};
+
+ActivityTest.prototype.tearDown = function() {
+  // Remove mocks
+  gadgets.util.escape = this.oldEscape;
+};
+
+ActivityTest.prototype.testSetField = function() {
+  var activity = new opensocial.Activity({'title' : 'yellow'});
+  this.assertEquals('yellow', activity.getField('title'));
+
+  activity.setField('title', 'purple');
+  this.assertEquals('purple', activity.getField('title'));
+};
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/compiler_test.js b/trunk/features/src/test/javascript/features/opensocial-templates/compiler_test.js
new file mode 100644
index 0000000..9ae0248
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/compiler_test.js
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function testSubstitution() {
+  var values = [
+    [ "hello world", null ],
+    [ "hello $world", null ],
+    [ "hello ${Cur} world", "'hello '+($this)+' world'" ],
+    [ "${Cur} hello", "($this)+' hello'" ],
+    [ "hello ${Cur}", "'hello '+($this)" ],
+    [ "$ ${Cur}", "'$ '+($this)" ],
+    [ "$${Cur}", "'$'+($this)" ],
+    [ "${Cur} ${Context.Index}", "($this)+' '+($_ir($loop, 'Index'))"],
+    [ "a ${Cur} b ${Context.Index} c", "'a '+($this)+' b '+($_ir($loop, 'Index'))+' c'"]
+  ];
+  for (var i = 0; i < values.length; i++) {
+    var compiled = os.parseAttribute_(values[i][0]);
+    assertEquals(values[i][1], compiled);
+  }
+};
+
+/**
+ * Unit test for compiler identifier wrapping.
+ * TODO(kjin): test all of the following:
+ *    "'a'",
+ *    "foo",
+ *    "foo + bar",
+ *    "foo||bar",
+ *    "foo.bar",
+ *    "foo().bar",
+ *    "foo.bar(baz)",
+ *    "foo.bar().baz",
+ *    "foo('a').bar",
+ *    "foo[bar].baz",
+ *    "foo.bar.baz",
+ *    "$my('foo').bar",
+ *    "$cur($context, 'person').ProfileName",
+ *    "foo(bar)[baz]"
+ */
+function testWrapIdentifiers() {
+  assertEquals("$_ir($_ir($context, 'foo'), 'bar')",
+      os.wrapIdentifiersInExpression("foo.bar"));
+
+  assertEquals("$_ir($_ir($context, 'data'), 'array')()",
+      os.wrapIdentifiersInExpression("data.array()"));
+
+  assertEquals("$_ir($_ir($context, 'data')(), 'array')",
+      os.wrapIdentifiersInExpression('data().array'));
+
+  // Check that namespaced tags are treated as single identifiers.
+  assertEquals("$_ir($context, 'os:Item')",
+      os.wrapIdentifiersInExpression("os:Item"));
+
+  // Check that a colon surrounded by spaces is not treated as
+  // part of identifier
+  assertEquals("$_ir($context, 'foo') ? $_ir($context, 'bar') : " +
+      "$_ir($context, 'baz')",
+      os.wrapIdentifiersInExpression("foo ? bar : baz"));
+
+  assertEquals("$_ir($_ir($context, 'viewer'), 'foo', $_ea)",
+      os.wrapIdentifiersInExpression("viewer.foo", "$_ea"));
+}
+
+function testTransformVariables() {
+  assertEquals("$this.foo", os.transformVariables_('$cur.foo'));
+}
+
+/**
+ * Unit test for JSP operator support.
+ */
+function testOperators() {
+  var data = {A:42, B:101};
+
+  var testData = [
+    { template:"${A lt B}", expected:"true" },
+    { template:"${A gt B}", expected:"false" },
+    { template:"${A eq A}", expected:"true" },
+    { template:"${A neq A}", expected:"false" },
+    { template:"${A lte A}", expected:"true" },
+    { template:"${A lte B}", expected:"true" },
+    { template:"${A gte B}", expected:"false" },
+    { template:"${A gte A}", expected:"true" },
+    { template:"${A eq " + data.A + "}", expected:"true" },
+    { template:"${(A eq A) ? 'PASS' : 'FAIL'}", expected:"PASS" },
+    { template:"${not true}", expected:"false" },
+    { template:"${A eq A and B eq B}", expected:"true" },
+    { template:"${A eq A and false}", expected:"false" },
+    { template:"${false or A eq A}", expected:"true" },
+    { template:"${false or false}", expected:"false" }
+    //TODO: precedence, parenthesis
+  ];
+
+  for (var i = 0; i < testData.length; i++) {
+    var testEntry = testData[i];
+    var template = os.compileTemplateString(testEntry.template);
+    var resultNode = template.render(data);
+    var resultStr = resultNode.firstChild.innerHTML;
+    assertEquals(resultStr, testEntry.expected);
+  }
+}
+
+function testCopyAttributes() {
+  var src = document.createElement('div');
+  var dst = document.createElement('div');
+  src.setAttribute('attr', 'test');
+  src.setAttribute('class', 'foo');
+  os.copyAttributes_(src, dst);
+  assertEquals('test', dst.getAttribute('attr'));
+  assertEquals('foo', dst.getAttribute('className'));
+  assertEquals('foo', dst.className);
+};
+
+/**
+ * Tests TBODY injection.
+ */
+function testTbodyInjection() {
+  var src, check, template, output;
+
+  // One row.
+  src = "<table><tr><td>foo</td></tr></table>";
+  check = "<table><tbody><tr><td>foo</td></tr></tbody></table>";
+  template = os.compileTemplateString(src);
+  output = template.templateRoot_.innerHTML;
+  output = output.toLowerCase();
+  output = output.replace(/\s/g, '');
+  assertEquals(check, output);
+
+  // Two rows.
+  src = "<table><tr><td>foo</td></tr><tr><td>bar</td></tr></table>";
+  check = "<table><tbody><tr><td>foo</td></tr>" +
+      "<tr><td>bar</td></tr></tbody></table>";
+  template = os.compileTemplateString(src);
+  output = template.templateRoot_.innerHTML;
+  output = output.toLowerCase();
+  output = output.replace(/\s/g, '');
+  assertEquals(check, output);
+};
+
+function testEventHandlers() {
+  var src, template, output;
+
+  window['testEvent'] = function(value) {
+    window['testValue'] = value;
+  };
+
+  // Static handler
+  src = "<button onclick=\"testEvent(true)\">Foo</button>";
+  template = os.compileTemplateString(src);
+  output = template.render();
+  // Append to document to enable events
+  document.body.appendChild(output);
+  window['testValue'] = false;
+  output.firstChild.click();
+  document.body.removeChild(output);
+  assertEquals(true, window['testValue']);
+
+  // Dynamic handler
+  src = "<button onclick=\"testEvent('${title}')\">Foo</button>";
+  template = os.compileTemplateString(src);
+  output = template.render({ title: 'foo' });
+  // Append to document to enable events
+  document.body.appendChild(output);
+  window['testValue'] = false;
+  output.firstChild.click();
+  document.body.removeChild(output);
+  assertEquals('foo', window['testValue']);
+};
+
+function testNestedIndex() {
+  var src, template, output;
+
+  src = '<table><tr repeat="${list}" var="row" context="x">' +
+      '<td repeat="${row}" context="y">${x.Index},${y.Index}</td></tr></table>';
+  template = os.compileTemplateString(src);
+  output = template.render({ list: [ ['a', 'b'], ['c', 'd'] ] });
+  //                           table  /  tbody  /   tr    /   td
+  assertEquals('1,1', output.lastChild.lastChild.lastChild.lastChild.innerHTML);
+};
+
+function testLoopNullDefaultValue() {
+  var src = '<div repeat="foo">a</div>';
+  var template = os.compileTemplateString(src);
+  var select = template.templateRoot_.firstChild.getAttribute("jsselect");
+  assertEquals("$_ir($context, 'foo', $_ea)", select);
+};
+/*
+function testEmbed() {
+  var src, template, output;
+
+  src = '<embed sRc="http://www.youtube.com/v/${$my.movie}&amp;hl=en" type="application/x-shockwave-flash" wmode="transparent" height="${$my.height}" width="${$my.width}"/>';
+  template = os.compileTemplateString(src);
+  src = '<img sRc="http://www.youtube.com/v/${$my.movie}&amp;hl=en" type="application/x-shockwave-flash" wmode="transparent" height="${$my.height}" width="${$my.width}"/>';
+  var template2 = os.compileTemplateString(src);
+  output = template.render();
+}
+*/
+
+function testGetFromContext() {
+  // JSON context
+  var context = { foo: 'bar' };
+  assertEquals('bar', os.getFromContext(context, 'foo'));
+
+  // JsEvalContext
+  context = os.createContext(context);
+  assertEquals('bar', os.getFromContext(context, 'foo'));
+
+  // Variable from context
+  context.setVariable('baz', 'bing');
+  assertEquals('bing', os.getFromContext(context, 'baz'));
+
+  // Non-existent value
+  assertEquals('', os.getFromContext(context, 'title'));
+
+  // Non-existent value with default
+  assertEquals(null, os.getFromContext(context, 'title', null));
+};
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/container_test.js b/trunk/features/src/test/javascript/features/opensocial-templates/container_test.js
new file mode 100644
index 0000000..db24f1c
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/container_test.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function testTemplateType() {
+  assertTrue(os.Container.isTemplateType_('text/template'));
+  assertTrue(os.Container.isTemplateType_('text/os-template'));
+  assertTrue(!os.Container.isTemplateType_('os-template'));
+}
+
+function testRegisterTemplates() {
+  os.Container.registerDocumentTemplates();
+  assertNotNull(os.getTemplate('os:Test'));
+  os.Container.processInlineTemplates();
+  var el = document.getElementById('test');
+  assertNotNull(el);
+  assertEquals('tag template', domutil.getVisibleText(el));
+}
+
+function testRequireLibrary() {
+  var params = {};
+  var oldGadgets = window.gadgets;
+  
+  window.gadgets = {};
+  window.gadgets.io = {};
+  window.gadgets.io.makeRequest = function() {};
+  window.gadgets.io.RequestParameters = { CONTENT_TYPE: 1 };
+  window.gadgets.io.ContentType = { TEXT: 1 };
+  window.gadgets.util = {};  
+  window.gadgets.util.getFeatureParameters = function() {
+    return params;
+  };
+  
+  params.requireLibrary = "foo";
+  os.Container.requiredLibraries_ = 0;
+  os.Container.processGadget();
+  assertEquals(1, os.Container.requiredLibraries_);
+
+  params.requireLibrary = [ "baz", "bing" ];
+  os.Container.requiredLibraries_ = 0;
+  os.Container.processGadget();
+  assertEquals(2, os.Container.requiredLibraries_);
+
+  
+  window.gadgets = oldGadgets;
+}
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/domutil.js b/trunk/features/src/test/javascript/features/opensocial-templates/domutil.js
new file mode 100644
index 0000000..c893d80
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/domutil.js
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+// A pseudo namespace.
+var domutil = {};
+
+/**
+ * Helper functions adapted from Selenium code.
+ *
+ * Determines if the specified element is visible. An element can be rendered
+ * invisible by setting the CSS "visibility" property to "hidden", or the
+ * "display" property to "none", either for the element itself or one if its
+ * ancestors. This method will fail if the element is not present.
+ * @param {Node} node The node to check.
+ * @return {boolean} Whether the node is visible.
+ */
+domutil.isVisible = function(node) {
+  // This test is necessary because getComputedStyle returns nothing
+  // for WebKit-based browsers if node not in document. See comment below.
+  if (node.style.display == 'none' || node.style.visibility == 'hidden') {
+    return false;
+  }
+  var visibility = this.findEffectiveStyleProperty(node, 'visibility');
+  var display = this.findEffectiveStyleProperty(node, 'display');
+  return visibility != 'hidden' && display != 'none';
+};
+
+
+/**
+ * Returns the value of the effective style specified by {@code property}.
+ * @param {Node} element The node to query.
+ * @param {string} property The name of a style that is of interest.
+ * @return {string} The value of style {@code property}.
+ */
+domutil.findEffectiveStyleProperty = function(element, property) {
+  var effectiveStyle = this.findEffectiveStyle(element);
+  var propertyValue = effectiveStyle[property];
+  if (propertyValue == 'inherit' && element.parentNode.style) {
+    return this.findEffectiveStyleProperty(element.parentNode, property);
+  }
+  return propertyValue;
+};
+
+
+/**
+ * Returns the effective style object.
+ * @param {Node} element The node to query.
+ * @return {CSSStyleDeclaration|undefined} The style object.
+ */
+domutil.findEffectiveStyle = function(element) {
+  if (!element.style) {
+    return undefined; // not a styled element
+  }
+  if (window.getComputedStyle) {
+    // DOM-Level-2-CSS
+    // WebKit-based browsers (Safari included) return nothing if the element
+    // is not a descendent of document ...
+    return window.getComputedStyle(element, null);
+  }
+  if (element.currentStyle) {
+    // non-standard IE alternative
+    return element.currentStyle;
+    // TODO: this won't really work in a general sense, as
+    //   currentStyle is not identical to getComputedStyle()
+    //   ... but it's good enough for 'visibility'
+  }
+  throw new Error('cannot determine effective stylesheet in this browser');
+};
+
+
+/**
+ * Returns the text content of the current node, without markup and invisible
+ * symbols. New lines are stripped and whitespace is collapsed,
+ * such that each character would be visible.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @return {string} The text content.
+ */
+domutil.getVisibleText = function(node) {
+  var textContent;
+  // NOTE(kjin): IE innerText is more like Firefox textContent -- visibility
+  // is not concerned. Safari 3 and Chrome innerText is just the visible text.
+  var buf = [];
+  domutil.getVisibleText_(node, buf, true);
+  textContent = buf.join('');
+
+  textContent = textContent.replace(/\xAD/g, '');
+
+  textContent = textContent.replace(/ +/g, ' ');
+  if (textContent != ' ') {
+    textContent = textContent.replace(/^\s*/, '');
+  }
+
+  return textContent;
+};
+
+
+/**
+ * Returns the domutil.getVisibleText without trailing space, if any.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @return {string} The text content.
+ */
+domutil.getVisibleTextTrim = function(node) {
+  return domutil.getVisibleText(node).replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
+};
+
+
+/**
+ * Recursive support function for text content retrieval.
+ *
+ * @param {Node} node The node from which we are getting content.
+ * @param {Array} buf string buffer.
+ * @param {boolean} normalizeWhitespace Whether to normalize whitespace.
+ * @private
+ */
+domutil.getVisibleText_ = function(node, buf, normalizeWhitespace) {
+  var TAGS_TO_IGNORE_ = {
+    'SCRIPT': 1,
+    'STYLE': 1,
+    'HEAD': 1,
+    'IFRAME': 1,
+    'OBJECT': 1
+  };
+  var PREDEFINED_TAG_VALUES_ = {'IMG': ' ', 'BR': '\n'};
+
+  if (node.nodeName in TAGS_TO_IGNORE_) {
+    // ignore certain tags
+  } else if (node.nodeType == 3) {
+    if (normalizeWhitespace) {
+      buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, ''));
+    } else {
+      buf.push(node.nodeValue);
+    }
+  } else if (!domutil.isVisible(node)) {
+    // ignore invisible node
+    // this has to be after the check for NodeType.TEXT because text node
+    // does not have style.
+  } else if (node.nodeName in PREDEFINED_TAG_VALUES_) {
+    buf.push(PREDEFINED_TAG_VALUES_[node.nodeName]);
+  } else {
+    var child = node.firstChild;
+    while (child) {
+      domutil.getVisibleText_(child, buf, normalizeWhitespace);
+      child = child.nextSibling;
+    }
+  }
+};
+
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/index.html b/trunk/features/src/test/javascript/features/opensocial-templates/index.html
new file mode 100644
index 0000000..4f92533
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/index.html
@@ -0,0 +1,190 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+<html>
+  <head>
+    <title>OpenSocial templates JsUnit tests</title>
+    <script src="../../lib/JsUtil.js"></script>
+    <script src="../../lib/JsUnit.js"></script>
+    <script>
+        var gadgets = {};
+      // TODO: These adapters are needed because the tests were originally
+      // created with a different version of JSUnit in mind. Refactor this file
+      // and the various test files to use the TestCase class infrastructure.
+      var _assert = new Assert();
+      function assertTrue(a) {
+        _assert.assertTrue(a);
+      }
+      function assertEquals(a,b) {
+        _assert.assertEquals(a,b);
+      }
+      function assertNotNull(a) {
+        _assert.assertNotNull(a);
+      }
+      function assertContains(a,b) {
+        _assert.assertTrue(b.indexOf(a) >= 0);
+      }
+    </script>
+    
+    <!-- the set of js files that make up opensocial-templates feature, as they appear in feature.xml -->
+    <script src="../../../../main/javascript/features/opensocial-templates/jsTemplate/util.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/jsTemplate/jsevalcontext.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/jsTemplate/jstemplate.js"></script>
+    
+    <script src="../../../../main/javascript/features/opensocial-data-context/datacontext.js"></script>
+    <script src="../../../../main/javascript/features/jsondom/jsondom.js"></script>
+    <script src="../../../../main/javascript/features/xmlutil/xmlutil.js"></script>
+    <script src="../../../../main/javascript/features/core.json/json-native.js"></script>
+    <script src="../../../../main/javascript/features/core.json/json-jsimpl.js"></script>
+    <script src="../../../../main/javascript/features/core.json/json-flatten.js"></script>
+    <script src="../../../../main/javascript/features/core.util/util.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-data/data.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/base.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/namespaces.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/util.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/template.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/compiler.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/loader.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/container.js"></script>
+    <script src="../../../../main/javascript/features/opensocial-templates/os.js"></script>
+    <!-- the JsUnit tests -->
+    <script src="domutil.js"></script>
+    <script type="text/javascript" src="compiler_test.js"></script>
+    <script type="text/javascript" src="container_test.js"></script>
+    <script type="text/javascript" src="loader_test.js"></script>
+    <script type="text/javascript" src="os_test.js"></script>
+    <script type="text/javascript" src="util_test.js"></script>
+    <script type="text/javascript" src="template_test.js"></script>
+    <!-- JsUnit work-around for non-FireFox browsers -->
+    <script type="text/javascript">
+      function exposeTestFunctionNames() {
+        var testSource = typeof RuntimeObject != 'undefined' ?
+                         RuntimeObject('test' + '*') : self;
+        var testFunctionNames = [];
+        for (var i in testSource) {
+          if (i.substring(0, 4) == 'test' && typeof(testSource[i]) == 'function')
+           testFunctionNames.push(i);
+        }
+        return testFunctionNames;
+      }
+
+      function runAllTests() {
+        var log = function(msg, forcePage) {
+          if (window.console && !forcePage) {
+            console.log(msg);
+            return;
+          } 
+          var div = document.createElement("div");
+          div.appendChild(document.createTextNode(msg));
+          document.body.appendChild(div); 
+        };
+        var tests = exposeTestFunctionNames();
+        var failed = 0;
+        for (var i = 0; i < tests.length; i++) {
+          log(tests[i]);
+          try {
+            window[tests[i]]();
+            log("OK");
+          } catch (e) {
+            log("FAIL: " + e);
+            failed++;
+          }
+        }
+        log("All finished. " + i + " run. " + failed + " failed.", true);
+      }
+      
+      os.createNamespace("test", "http://www.google.com/#test");
+    </script>
+  </head>
+  <body>
+    <input type="button" onclick="runAllTests()" value="Run tests"/>
+    <script type="text/os-template" tag="os:Test">tag template</script>
+    <script type="text/os-template">
+      <div id="test"><os:Test/></div>
+    </script>
+
+    <div style="display: none">
+      <div id="domSource">
+        <ul>
+          <li>one</li>
+          <li>two</li>
+        </ul>
+        <b>bold</b>
+      </div>
+      <div id="domTarget">
+      </div>
+    </div>
+
+    <xmp id="_T_Substitution_attribute" style="display: none">
+      <button id="${id}" style="color: ${color}" a1="value ${A1}">${text}</button>
+    </xmp>
+    <xmp id="my:user" style="display: none">
+      <a href="${My.dat.url}">${My.dat.name}</a> (${My.foo})
+    </xmp>
+    <xmp id="my:record" style="display: none">
+      <b style="color: ${My.color}">${My.dat.title}</b>: <my:user foo="${My.foo}" dat="${My.dat.user}"/>
+    </xmp>
+    <xmp id="_T_Substitution_nested" style="display: none">
+      <div repeat="users">
+        <my:record color="${color}" foo="${user.id}" dat="${Cur}"/>
+      </div>
+    </xmp>
+
+    <xmp id="_T_Conditional_Number" style="display: none">
+      <span if="42==42">TRUE</span>
+      <span if="!(42==42)">FALSE</span>
+    </xmp>
+    <xmp id="_T_Conditional_String" style="display: none">
+      <span if="'101'=='101'">TRUE</span>
+      <span if="'101'!='101'">FALSE</span>
+    </xmp>
+    <xmp id="_T_Conditional_Mixed" style="display: none">
+      <span if="'101' gt 42">TRUE</span>
+      <span if="'101' lt 42">FALSE</span>
+    </xmp>
+
+    <xmp id="_T_Repeat" style="display: none">
+      <div repeat="entries">
+        ${data}
+      </div>
+    </xmp>
+
+    <xmp id="_T_Options" style="display: none">
+      <select id="options">
+        <option repeat="options" value="${value}">${value}</option>
+      </select>
+    </xmp>
+
+    <xmp id="custom:list" style="display: none">
+      <div repeat="$my.item"><os:renderAll content="header"/><os:renderAll content="body"/></div>
+    </xmp>
+
+    <xmp id="_T_List" style="display: none">
+      <custom:list>
+        <item>
+          <header>hello</header>
+          <body>world</body>
+        </item>
+      </custom:list>
+    </xmp>
+
+    <xmp id="_T_Tag_blink" style="display: none">
+      <custom:blink>blink text</custom:blink>
+    </xmp>
+  </body>
+</html>
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/loader_test.js b/trunk/features/src/test/javascript/features/opensocial-templates/loader_test.js
new file mode 100644
index 0000000..8063391
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/loader_test.js
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Unit test for injecting JavaScript into the global scope with the 
+ * os.Loader.
+ */
+function testInjectJavaScript() {
+  var jsCode = "function testFunction() { return 'foo'; }";
+  os.Loader.injectJavaScript(jsCode);
+  assertTrue(window.testFunction instanceof Function);
+  assertEquals(window.testFunction(), 'foo');
+}
+
+/**
+ * Unit test for injecting CSS through the os.Loader.
+ */
+function testInjectStyle() {
+  var cssCode = '.testCSS { width: 100px; height: 200px; }';
+  os.Loader.injectStyle(cssCode);
+  var rule = getStyleRule('.testCSS');
+  assertNotNull(rule);
+  assertEquals(rule.style.width, '100px');
+  assertEquals(rule.style.height, '200px');
+}
+
+/**
+ * @type {String} Template XML data for testLoadContent.
+ */
+var testContentXML =
+    '<Templates xmlns:test="http://www.google.com/#test">' +
+    '  <Namespace prefix="test" url="http://www.google.com/#test"/>' +
+    '  <Template tag="test:tag">' +
+    '    <div id="tag"></div>' +
+    '  </Template>' +
+    '  <JavaScript>' +
+    '    function testJavaScript() {' +
+    '      return "testJavaScript";' +
+    '    }' +
+    '  </JavaScript>' +
+    '  <Style>' +
+    '    .testStyle {' +
+    '      width: 24px;' +
+    '    }' +
+    '  </Style>' +
+    '  <TemplateDef tag="test:tagDef">' +
+    '    <Template>' +
+    '      <div id="tagDef"></div>' +
+    '    </Template>' +
+    '    <JavaScript>' +
+    '      function testJavaScriptDef() {' +
+    '        return "testJavaScriptDef";' +
+    '      }' +
+    '    </JavaScript>' +
+    '    <Style>' +
+    '      .testStyleDef {' +
+    '        height: 42px;' +
+    '      }' +
+    '    </Style>' +
+    '  </TemplateDef>' +
+    '</Templates>';
+
+/**
+ * System test for os.loadContent functionality. This tests
+ * all functionality except for XHR.
+ */
+function testLoadContent() {
+  os.Loader.loadContent(testContentXML);
+
+  // Verify registered tags.
+  var ns = os.nsmap_['test'];
+  assertNotNull(ns);
+  assertTrue(ns['tag'] instanceof Function);
+  assertTrue(ns['tagDef'] instanceof Function);
+
+  // Verify JavaScript functions.
+  assertTrue(window['testJavaScript'] instanceof Function);
+  assertEquals(window.testJavaScript(), 'testJavaScript');
+  assertTrue(window['testJavaScriptDef'] instanceof Function);
+  assertEquals(window.testJavaScriptDef(), 'testJavaScriptDef');
+
+  // Verify styles.
+  var rule = getStyleRule('.testStyle');
+  assertNotNull(rule);
+  assertEquals(rule.style.width, '24px');
+  var ruleDef = getStyleRule('.testStyleDef');
+  assertNotNull(ruleDef);
+  assertEquals(ruleDef.style.height, '42px');
+}
+
+/**
+ * Utility function for retrieving a style rule by selector text
+ * if its available.
+ * @param {string} name Selector text name.
+ * @return {Object} CSSRule object.
+ */
+function getStyleRule(name) {
+  var sheets = document.styleSheets;
+  for (var i = 0; i < sheets.length; ++i) {
+    var rules = sheets[i].cssRules || sheets[i].rules;
+    if (rules) {
+      for (var j = 0; j < rules.length; ++j) {
+        if (rules[j].selectorText == name
+            //hack for WebKit Quirks mode
+            || rules[j].selectorText == name.toLowerCase()) {
+          return rules[j];
+        }
+      }
+    }
+  }
+  return null;
+}
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/os_test.js b/trunk/features/src/test/javascript/features/opensocial-templates/os_test.js
new file mode 100644
index 0000000..bd51749
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/os_test.js
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Unit test for testing the behavior of the OpenSocial identifier resolver.
+ */
+function testResolveOpenSocialIdentifier() {
+  /**
+   * Sample class with a property, property getter, custom getField and a get()
+   * method.
+   */
+  var TestClass = function() {
+    this.foo = 'fooData';
+    this.bar_ = 'barData';
+    this.thumbnailUrl_ = 'thumbnailUrlData';
+    this.responseItem_ = {};
+    this.responseItem_.getData = function() {
+      return 'responseItemData';
+    };
+  };
+  TestClass.prototype.getBar = function() {
+    return this.bar_;
+  };
+  TestClass.prototype.getField = function(field) {
+    if (field == 'THUMBNAIL_URL') {
+      return this.thumbnailUrl_;
+    }
+    return null;
+  };
+  TestClass.prototype.get = function(field) {
+    if (field == 'responseItem') {
+      return this.responseItem_;
+    }
+    return null;
+  };
+  
+  var obj = new TestClass(); 
+  
+  assertEquals('fooData', os.resolveOpenSocialIdentifier(obj, 'foo'));
+  assertEquals('barData', os.resolveOpenSocialIdentifier(obj, 'bar'));
+  assertEquals('thumbnailUrlData',
+      os.resolveOpenSocialIdentifier(obj, 'THUMBNAIL_URL'));
+  assertEquals('responseItemData',
+      os.resolveOpenSocialIdentifier(obj, 'responseItem'));
+}
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/template_test.js b/trunk/features/src/test/javascript/features/opensocial-templates/template_test.js
new file mode 100644
index 0000000..3a58c91
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/template_test.js
@@ -0,0 +1,790 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Helper functions.
+ * @param {string} templateId The id of template definition node.
+ * @param {Object} opt_context The context data.
+ * @return {Element} The rendered HTML element.
+ * @private
+ */
+function compileAndRender_(templateId, opt_context) {
+  var template = os.compileTemplate(document.getElementById(templateId));
+
+  // Process the template and output the result
+  var outputNode = document.createElement("div");
+  template.renderInto(outputNode, opt_context);
+  return outputNode;
+}
+
+/**
+ * Takes a string representing a the markup of single DOM node, and returns
+ * the corresponding DOM node.
+ *
+ * @param {string} markup Markup text of the DOM node
+ * @return {Node} The DOM node
+ */
+function markupToNode(markup) {
+  var node = document.createElement('div');
+  node.innerHTML = markup;
+  return node.firstChild;
+}
+
+/**
+ * Finds if an Attr object is real (specified and not inserted by JST).
+ * The JST attributes start with 'js', but IE also has a special '__jstcache'
+ * attribute.
+ * @param {Node} attr The Attr node object.
+ */
+function isRealAttribute(attr) {
+  return attr.specified &&
+         attr.name.indexOf('js') != 0 &&
+         attr.name != '__jstcache';
+};
+
+/**
+ * Normalize a node to the corresponding markup text, ignoring
+ *     JsTemplate artifacts:
+ * - Removes attributes starting with "js"
+ * - Removes the SPAN tag if it has a "customtag" attribute or has no attributes
+ *   that don't start with "js". This leaves the contents of the tag
+ * - Removes nodes with style="display: none;", which is what JsTemplate does
+ *   to nodes that aren't officially output.
+ *
+ * @param {Node} node The DOM node
+ * @return {string} The normalized markup
+ */
+function nodeToNormalizedMarkup(node) {
+  if (node.nodeType == 3) {
+    return node.nodeValue;
+  } else if (node.nodeType == 1) {
+    var hasRealAttributes = false;
+
+    for (var i = 0; i < node.attributes.length; i++) {
+      if (isRealAttribute(node.attributes[i])) {
+        hasRealAttributes = true;
+      }
+    }
+
+    if (node.getAttribute('customtag') != null) {
+      hasRealAttributes = false;
+    }
+
+    if (node.nodeName == 'SPAN' && !hasRealAttributes) {
+      var text = '';
+      for (var i = 0; i < node.childNodes.length; i++) {
+        text += nodeToNormalizedMarkup(node.childNodes[i]);
+      }
+      return text;
+    }
+    if (node.style.display == 'none') {
+      return '';
+    }
+
+    var text = '<' + node.nodeName;
+    for (var i = 0; i < node.attributes.length; i++) {
+      var att = node.attributes[i];
+      if (isRealAttribute(att)) {
+        text += ' ' + att.name + '="' + att.value + '"';
+      }
+    }
+    text += '>';
+    for (var i = 0; i < node.childNodes.length; i++) {
+      text += nodeToNormalizedMarkup(node.childNodes[i]);
+    }
+    text += '</' + node.nodeName + '>';
+    return text;
+  }
+  return '';
+}
+
+/**
+ * Normalizes a node or text string to normalized text of the DOM node
+ *
+ * @param {Node|string} nodeOrText The DOM node or text of the DOM node
+ * @return {string} The normalized markup text
+ *
+ */
+function normalizeNodeOrMarkup(nodeOrText) {
+  var node = typeof nodeOrText == 'string'
+    ? markupToNode(nodeOrText) : nodeOrText;
+  return nodeToNormalizedMarkup(node);
+}
+
+
+/*
+ * Checks if two DOM node are equal, ingoring template artefacts.
+ *
+ * @param {Node|string} lhs First DOM node or string of markup contents
+ * @param {Node|string} rhs Second DOM node or string of markup contents
+ */
+function assertTemplateDomEquals(lhs, rhs) {
+  lhs = normalizeNodeOrMarkup(lhs);
+  rhs = normalizeNodeOrMarkup(rhs);
+
+  assertEquals(lhs, rhs);
+}
+
+
+/**
+ * Allow testing of templates passed in a strings. Allows for calling
+ * a template, passing in a map of named templates, and passing in the data
+ * context.
+ *
+ * @param {string} templateText The text of the inline template to evaluate
+ * @param {string} output The expected output
+ * @param {Object=} context The data context
+ * @param {Array<String>=} namedTemplates Array of text of namedTemplates
+ */
+function assertTemplateOutput(templateText, output, context,
+    namedTemplates) {
+
+  // Parse and register named templates
+  if (namedTemplates instanceof Array) {
+    for (var i = 0; i < namedTemplates.length; i++) {
+      var text = '<Templates xmlns:os="uri:unused">' + namedTemplates[i] +
+          '</Templates>';
+      var dom = opensocial.xmlutil.parseXML(text);
+      os.Loader.processTemplatesNode(dom);
+    }
+  }
+
+  var template = os.compileTemplateString(templateText);
+
+  // Process the template and output the result
+  var outputNode = document.createElement("div");
+  template.renderInto(outputNode, context);
+  assertTemplateDomEquals(output, outputNode.firstChild);
+}
+
+/**
+ * Tests Namespace.
+ */
+function testNamespace() {
+  // Create the "custom" namespace
+  var custom = os.createNamespace("custom", "http://google.com/#custom");
+  assertEquals(custom, os.getNamespace("custom"));
+
+  var custom_sameUrl =
+    os.createNamespace("custom", "http://google.com/#custom");
+  assertEquals(custom_sameUrl, os.getNamespace("custom"));
+
+  try {
+    var custom_newUrl =
+      os.createNamespace("custom", "http://google.com/#custom_new");
+    fail("no exception thrown with new URL for the same namespace");
+  }
+  catch (e) {
+    // We expect os to throw an exception for namespace conflict.
+    // But if e is a JsUnitException (thrown from fail), throw it again.
+    if (e.isJsUnitException) {
+      throw e;
+    }
+  }
+}
+
+/**
+ * Tests Substitution.
+ */
+function testSubstitution_text() {
+  var data = {
+    title: "count",
+    value: 0
+  };
+  assertTemplateOutput('<div>${title}:${value}</div>',
+    '<div>' + data.title + ":" + data.value + '</div>',
+    data);
+}
+
+function testSubstitution_attribute() {
+  var data = {
+    id: "varInAttr",
+    color: "red",
+    A1: 111,
+    text: "click me"
+  };
+  var outputNode = compileAndRender_("_T_Substitution_attribute", data);
+  var contentNode = outputNode.firstChild;
+
+  assertEquals(data.id, contentNode.id);
+  assertEquals(data.color, contentNode.style.color);
+  assertEquals("value " + data.A1, contentNode.getAttribute("a1"));
+  assertEquals(data.text, contentNode.innerHTML);
+}
+
+function testSubstitution_nested() {
+  var data = {
+    title: "Users",
+    users: [
+      { title: "President", color: 'red',
+        user: { name: "Bob", id: "101", url: "http://www.bob.com" }},
+      { title: "Manager", color: 'green',
+        user: { name: "Rob", id: "102", url: "http://www.rob.com" }},
+      { title: "Peon", color: 'blue',
+        user: { name: "Jeb", id: "103", url: "http://www.jeb.com" }}
+    ]
+  };
+
+  os.createNamespace("my", "www.google.com/#my");
+  os.Container.registerTag('my:user');
+  os.Container.registerTag('my:record');
+
+  var outputNode = compileAndRender_("_T_Substitution_nested", data);
+
+  assertEquals(data.users.length, outputNode.childNodes.length);
+  for (var i = 0; i < data.users.length; i++) {
+    var user = data.users[i];
+    var divNode = outputNode.childNodes[i];
+    assertEquals("DIV", divNode.tagName);
+
+    // Find first Element child. FF creates an empty #text node, IE does not,
+    // so we need to look.
+    var spanNode = divNode.firstChild;
+    while (!spanNode.tagName) {
+      spanNode = spanNode.nextSibling;
+    }
+
+    assertEquals(user.color, spanNode.color);
+    assertEquals(user.user.id, spanNode.foo);
+
+    var recordNode = spanNode.childNodes[0];
+    assertEquals(user.color, recordNode.firstChild.style.color);
+    assertEquals(user.title, recordNode.firstChild.innerHTML);
+
+    var userNode = recordNode.lastChild;
+    assertEquals(user.user.id, userNode.foo);
+    var anchorNode = userNode.firstChild.childNodes[0];
+    assertEquals(user.user.name, anchorNode.innerHTML);
+    assertContains(user.user.url, anchorNode.href);
+
+    var fooNode = userNode.firstChild.childNodes[2];
+    assertEquals(user.user.id, fooNode.innerHTML);
+  }
+}
+
+/**
+ * Tests if attribute.
+ */
+function testConditional_Number() {
+  var outputNode = os.compileTemplateString(
+      '<span if="42==42">TRUE</span><span if="!(42==42)">FALSE</span>'
+      ).render();
+  assertEquals("TRUE", domutil.getVisibleTextTrim(outputNode));
+}
+
+function testConditional_String() {
+  var outputNode = os.compileTemplateString(
+      "<span if=\"'101'=='101'\">TRUE</span><span if=\"'101'!='101'\">FALSE</span>"
+      ).render();
+  assertEquals("TRUE", domutil.getVisibleTextTrim(outputNode));
+}
+
+function testConditional_Mixed() {
+  var outputNode = os.compileTemplateString(
+      "<span if=\"'101' gt 42\">TRUE</span><span if=\"'101' lt 42\">FALSE</span>"
+      ).render();
+  assertEquals("TRUE", domutil.getVisibleTextTrim(outputNode));
+}
+
+/**
+ * Tests repeat attribute.
+ */
+function testRepeat() {
+  var data = {
+    entries : [
+      { data: "This" },
+      { data: "is" },
+      { data: "an" },
+      { data: "array" },
+      { data: "of" },
+      { data: "data." }
+    ]
+  };
+  var outputNode = compileAndRender_("_T_Repeat", data);
+
+  assertEquals(data.entries.length, outputNode.childNodes.length);
+  for (var i = 0; i < data.entries.length; i++) {
+    var entry = data.entries[i];
+    assertEquals("DIV", outputNode.childNodes[i].tagName);
+    assertEquals(entry.data,
+      domutil.getVisibleTextTrim(outputNode.childNodes[i]));
+  }
+}
+
+/**
+ * Tests select elements.
+ */
+function testSelect() {
+  var data = {
+    options : [
+      { value: "one" },
+      { value: "two" },
+      { value: "three" }
+    ]
+  };
+  var outputNode = compileAndRender_("_T_Options", data);
+  var selectNode = outputNode.firstChild;
+
+  assertEquals(data.options.length, selectNode.options.length);
+  for (var i = 0; i < data.options.length; i++) {
+    var entry = data.options[i];
+    var optionNode = selectNode.options[i];
+    assertEquals("OPTION", optionNode.tagName);
+    assertEquals(entry.value, optionNode.getAttribute('value'));
+  }
+}
+
+function testList() {
+  os.Container.registerTag('custom:list');
+  var output = compileAndRender_('_T_List');
+  assertEquals('helloworld', domutil.getVisibleText(output));
+}
+
+/**
+ * Tests JS custom tags.
+ */
+function testTag_input() {
+  var custom = os.createNamespace("custom", "http://google.com/#custom");
+  /**
+   * Custom controller that uses the value of any input fields as a key to
+   * replace itself with in the context data.
+   */
+  custom.input = function(node, context) { // return HTML;
+    var inputNode = document.createElement('input');
+
+    // Use the "value" attribute from the tag to index the context data
+    inputNode.value = context[node.getAttribute('value')];
+    return inputNode;
+  };
+
+  var data = {
+    data: "Some default data"
+  };
+  var template = os.compileTemplateString("<custom:input value=\"data\" cur=\"Cur\"/>");
+  var output = template.render(data);
+
+  // extract contentNode
+  var contentNode = output.getElementsByTagName("input")[0];
+
+  assertEquals(contentNode.value, data.data);
+}
+
+function testHelloWorld() {
+  assertTemplateOutput(
+    '<div>Hello world!</div>',
+    '<div>Hello world!</div>');
+}
+
+function testSimpleExpression() {
+  assertTemplateOutput(
+    '<div>${HelloWorld}</div>',
+    '<div>Hello world!</div>',
+    {HelloWorld: 'Hello world!'});
+}
+
+function testNamedTemplate() {
+  assertTemplateOutput(
+    '<div><os:HelloWorld/></div>',
+    '<div>Hello world!</div>',
+    null,
+    ['<Template tag="os:HelloWorld">Hello world!</Template>']);
+}
+
+function testParameter() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<div><os:HelloWorldWithParam text="Hello world!"/></div>',
+      '<div>Hello world!</div>',
+      null,
+      ['<Template tag="os:HelloWorldWithParam">' + content + '</Template>']);
+  };
+
+  tryTemplateContent('${$my.text}');
+  tryTemplateContent('${My.text}');
+  tryTemplateContent('${my.text}');
+
+  // Not working yet:
+  /*
+  tryTemplateContent('${text}');
+  */
+}
+
+function testContent() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<div><os:HelloWorldWithContent>Hello world!' +
+      '</os:HelloWorldWithContent></div>',
+      '<div>Hello world!</div>',
+      null,
+      ['<Template tag="os:HelloWorldWithContent">' + content + '</Template>']);
+  };
+
+  tryTemplateContent('<os:Render/>');
+}
+
+function testNamedContent() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<div>' +
+      '<os:HelloWorldWithNamedContent>' +
+        '<os:DontShowThis>Don\'t show this</os:DontShowThis>' +
+        '<os:Foo>Hello <b>world!</b></os:Foo>' +
+        '<Content>Hello <b>world!</b></Content>' +
+      '</os:HelloWorldWithNamedContent>' +
+      '</div>',
+
+      '<div>Hello <b>world!</b></div>',
+      null,
+      ['<Template tag="os:HelloWorldWithNamedContent">' + content + '</Template>']);
+  };
+  tryTemplateContent('<os:Render content="os:Foo"/>');
+  tryTemplateContent('<os:Render content="Content"/>');
+
+  // Not working yet:
+  /*
+  tryTemplateContent('<os:Render content="${my.os:Content}"/>');
+  tryTemplateContent('<os:Render content="my.os:Content"/>');
+  tryTemplateContent('<os:Render content="${My.os:Content}"/>');
+  tryTemplateContent('<os:Render content="${my.Content}"/>');
+  tryTemplateContent('<os:Render content="my.Content"/>');
+  tryTemplateContent('<os:Render content="${My.Content}"/>');
+  tryTemplateContent('<os:Render content="${Content}"/>');
+  */
+}
+
+function testRepeatedContent() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      '<os:HelloWorldRepeatedContent>' +
+        '<Word>Hello</Word>' +
+        '<os:Word>world!</os:Word>' +
+      '</os:HelloWorldRepeatedContent>',
+
+      '<div>Helloworld!</div>',
+      null,
+      ['<Template tag="os:HelloWorldRepeatedContent">' + content + '</Template>']);
+  };
+
+  tryTemplateContent('<div><span repeat="${My.Word}"><os:Render/></span></div>');
+  tryTemplateContent('<div><span repeat="${Word}"><os:Render/></span></div>');
+
+  // Not working yet because $my must be explicit:
+  /*
+    tryTemplateContent('<div><span repeat="${Word}"><os:Render/></span></div>');
+    tryTemplateContent('<div><span repeat="${os:Word}"><os:Render/></span></div>');
+    tryTemplateContent('<div><span repeat="Word"><os:Render/></span></div>');
+    tryTemplateContent('<div><span repeat="os:Word"><os:Render/></span></div>');
+  */
+};
+
+/**
+ * Bug when calling a repeat twice - 2nd time fails
+ *
+ * This is because <os:Render> moves the child tags out to destination, so
+ * the second loop is empty.
+ */
+function testRepeatedContentTwice() {
+  /*
+  assertTemplateOutput(
+    '<os:HelloWorldRepeatedContent>' +
+      '<os:Word>Hello</os:Word>' +
+      '<os:Word>world!</os:Word>' +
+    '</os:HelloWorldRepeatedContent>',
+
+    '<div><div>Helloworld!</div><div>Helloworld!</div></div>',
+    null,
+    ['<Template tag="os:HelloWorldRepeatedContent">
+      '<div>' +
+      '<div><span repeat="$my.os:Word"><os:Render/></span></div>' +
+      '<div><span repeat="$my.os:Word"><os:Render/></span></div>' +
+      '</Template>']
+    );
+  */
+};
+
+/**
+ * Currently, expression inside "content" attribute is equiv of no attribute.
+ * Probably should just include no data.
+ *
+ * I.e.
+ * <os:Render content="${os:NoMatch}"/> currently has same output as
+ * <os:Render/>
+ */
+function testRenderAllBadExprInContent() {
+  /*
+  assertTemplateOutput(
+    '<div>' +
+    '<os:HelloWorldBadExpr>' +
+      '<os:Content>Hello world!</os:Content>' +
+    '</os:HelloWorldBadExpr>' +
+    '</div>',
+
+    '<div></div>',
+    null,
+    ['<Template tag="os:HelloWorldBadExpr">' +
+     '<os:Render content="${os:NoMatch}"/>' +
+     '</Template>']);
+  */
+}
+
+
+function testBooleanTrue() {
+  assertTemplateOutput(
+    '<span if="${BooleanTrue}">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanTrue: true});
+
+  assertTemplateOutput(
+    '<span if="BooleanTrue">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanTrue: true});
+
+  assertTemplateOutput(
+    '<span if="!BooleanTrue">Hello world!</span>',
+    '<span></span>',
+    {BooleanTrue: true});
+
+  assertTemplateOutput(
+    '<span if="${!BooleanTrue}">Hello world!</span>',
+    '<span></span>',
+    {BooleanTrue: true});
+}
+
+
+function testBooleanFalse() {
+  assertTemplateOutput(
+    '<span if="BooleanFalse">Hello world!</span>',
+    '<span></span>',
+    {BooleanFalse: false});
+
+  assertTemplateOutput(
+    '<span if="!BooleanFalse">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanFalse: false});
+
+  assertTemplateOutput(
+    '<span if="${!BooleanFalse}">Hello world!</span>',
+    '<span>Hello world!</span>',
+    {BooleanFalse: false});
+
+  assertTemplateOutput(
+    '<span if="${BooleanFalse}">Hello world!</span>',
+    '<span></span>',
+    {BooleanFalse: false});
+}
+
+
+function testRepeatedNode() {
+  var tryTemplateContent = function(content) {
+    assertTemplateOutput(
+      content,
+      '<div>Helloworld!</div>',
+      {
+        Words: ['Hello', 'world!'],
+        WordObjects: [{value: 'Hello'}, {value: 'world!'}]
+      });
+  };
+
+  tryTemplateContent('<div><span repeat="WordObjects">${$cur.value}</span></div>');
+  tryTemplateContent('<div><span repeat="WordObjects">${value}</span></div>');
+  tryTemplateContent('<div><span repeat="WordObjects">${cur.value}</span></div>');
+  tryTemplateContent('<div><span repeat="Words">${cur}</span></div>');
+  tryTemplateContent('<div><span repeat="Words">${$cur}</span></div>');
+  tryTemplateContent('<div><span repeat="Words">${Cur}</span></div>');
+
+  // Do we want to continue to support this?
+  tryTemplateContent('<div><span repeat="Words">${$this}</span></div>');
+};
+
+function testDynamicRepeatedContent() {
+
+  assertTemplateOutput(
+    '<os:DynamicRepeat>' +
+      '<Word repeat="WordObjects">${Cur.value}</Word>' +
+    '</os:DynamicRepeat>',
+    '<div>Helloworld!</div>',
+    {WordObjects: [{value: 'Hello'}, {value: 'world!'}]},
+
+    ['<Template tag="os:DynamicRepeat">' +
+     '<div><span repeat="My.Word"><os:Render/></span></div>' +
+     '</Template>']);
+
+};
+
+function testReplaceTopLevelVars() {
+  function test(src, dest) {
+    assertEquals(dest, os.replaceTopLevelVars_(src));
+  }
+
+  // Basic substitution for each replacement
+  test('my.man', '$my.man');
+  test('my', '$my');
+  test('My.man', '$my.man');
+  test('My', '$my');
+  test('cur.man', '$this.man');
+  test('cur', '$this');
+  test('Cur.man', '$this.man');
+  test('Cur', '$this');
+
+  // Basic no sustitution
+  test('$my.man', '$my.man');
+  test('$my', '$my');
+  test('ns.My', 'ns.My');
+  test('Cur/2', '$this/2');
+  test('Cur*2', '$this*2');
+  test('Cur[My.name]', '$this[$my.name]');
+  test('Cur||\'Nothing\'', '$this||\'Nothing\'');
+
+  // Single operator, both fist and last expression
+  test('My.man+your.man', '$my.man+your.man');
+  test('your.man>My.man', 'your.man>$my.man');
+
+  // Tests a specific operator
+  function testOperator(operator) {
+    test('My.man' + operator + 'your.man',
+        '$my.man' + operator + 'your.man');
+
+    test('your.man' + operator + 'My.man',
+        'your.man' + operator + '$my.man');
+
+    test('My' + operator + 'My',
+        '$my' + operator + '$my');
+  }
+
+  // All operators
+  testOperator('+');
+  testOperator(' + ');
+  testOperator('-');
+  testOperator('<');
+  testOperator(' lt ');
+  testOperator('>');
+  testOperator(' gt ');
+  testOperator('=');
+  testOperator('!=');
+  testOperator('==');
+  testOperator('&&');
+  testOperator(' and ');
+  testOperator('||');
+  testOperator(' or ');
+  testOperator(' and !');
+  testOperator('/');
+  testOperator('*');
+  testOperator('|');
+  testOperator('(');
+  testOperator('[');
+};
+
+function testHtmlTag() {
+  var template = os.compileTemplateString('<os:Html code="${foo}"/>');
+  var output = template.render({foo: 'Hello <b>world</b>!'});
+  var boldNodes = output.getElementsByTagName("b");
+  assertEquals(1, boldNodes.length);
+};
+
+function testOnAttachAttribute() {
+  var template = os.compileTemplateString(
+      '<div onAttach="this.title=\'bar\'"/>');
+  var output = document.createElement('div');
+  template.renderInto(output);
+  assertEquals('bar', output.firstChild.title);
+};
+
+function testSpacesAmongTags() {
+  var tryTemplateContent = function(templateText) {
+   var output = os.compileTemplateString(templateText).render();
+    assertEquals('Hello world!', domutil.getVisibleTextTrim(output));
+  };
+
+  os.Loader.loadContent('<Templates xmlns:os="uri:unused">' +
+    '<Template tag="os:msg">${My.text}</Template></Templates>');
+
+  tryTemplateContent('<div><os:msg text="Hello"/>\n' +
+      ' <os:msg text="world!"/></div>');
+  tryTemplateContent('<div><os:msg text="Hello"/>  ' +
+      '<os:msg text="world!"/></div>');
+  tryTemplateContent('<div> <os:msg text="Hello"/>  ' +
+      '<os:msg text="world!"/>\n</div>');
+
+  os.Loader.loadContent('<Templates xmlns:os="uri:unused">' +
+    '<Template tag="os:msg"><os:Render/></Template></Templates>');
+
+  tryTemplateContent('<div><os:msg>Hello</os:msg>\n' +
+      ' <os:msg>world!</os:msg>\n</div>');
+  tryTemplateContent('<div><os:msg>Hello</os:msg>' +
+      '  <os:msg>world!</os:msg></div>');
+  tryTemplateContent('<div>\n  <os:msg>Hello</os:msg>' +
+      '  <os:msg>world!</os:msg>\n</div>');
+};
+
+function testVariablePrecedence() { 
+  // TODO: Update tests to reflect @cur not propagating into custom tags  
+  // Precedence should be ${Cur} -> ${My} -> ${Top}  
+  var tryTemplateContent = function(templateText, data) {
+    var output = os.compileTemplateString(templateText).render(data);
+    assertEquals('Right', domutil.getVisibleTextTrim(output));
+  };
+  os.Loader.loadContent('<Templates xmlns:os="uri:unused">' + 
+    '<Template tag="os:msg">${Value}</Template></Templates>');
+  
+  tryTemplateContent('<os:msg/>', { Value: 'Right' } );
+  tryTemplateContent('<os:msg Value="Right"/>', { } );
+  tryTemplateContent('<os:msg Value="Right"/>', { Value: 'Wrong' } );
+  tryTemplateContent('<os:msg Value="Right" cur="${Top}"/>', { Value: 'Wrong' } );
+  tryTemplateContent('<os:msg Value="${Value}"/>', { Value: 'Right' } );
+};
+
+function testOsRepeat() {
+  var data = { list : [ { name: 'a' }, { name: 'b' }, { name: 'c' } ] };
+  var output = os.compileTemplateString('<os:Repeat expression="${list}"><b>${name}</b> <b>${name}</b> </os:Repeat>').render(data);
+  assertEquals("a a b b c c", domutil.getVisibleTextTrim(output));
+  
+  output = os.compileTemplateString('<select><os:Repeat expression="${list}"><option>${name}</option><option>${name} again</option></os:Repeat></select>').render(data);
+  assertEquals(6, output.firstChild.options.length);
+  
+  output = os.compileTemplateString('<table><os:Repeat expression="${list}"><tr><td>${name}</td></tr><tr><td>${name} again</td></tr></os:Repeat></table>').render(data);
+  assertEquals(6, output.firstChild.rows.length);
+};
+
+function testOsIf() {
+  var data = { list : [ { name: 'a' }, { name: 'b' }, { name: 'c' } ] };
+  var output = os.compileTemplateString('<os:Repeat expression="${list}"><os:If condition="${name != \'b\'}"><b>${name}</b> <b>${name}</b> </os:If></os:Repeat>').render(data);
+  assertEquals("a a c c", domutil.getVisibleTextTrim(output));
+};
+
+function testOsVar() {
+
+  assertTemplateOutput(
+  '<div><os:Var key="counter" value="1" />${counter}</div>',
+  '<div>1</div>',
+  {});
+
+  assertTemplateOutput(
+  '<div><os:Var key="counter" value="1" /><os:Var key="counter" value="${counter + 1}" />${counter}</div>',
+  '<div>2</div>',
+  {});
+
+  assertTemplateOutput(
+  '<div><os:Var key="counter" value="[1,3,5,7]" />${counter[1]}</div>',
+  '<div>3</div>',
+  {});
+
+  assertTemplateOutput(
+  '<div><os:Var key="counter">{"key" : "value"}</os:Var>${counter.key}</div>',
+  '<div>value</div>',
+  {});
+}
diff --git a/trunk/features/src/test/javascript/features/opensocial-templates/util_test.js b/trunk/features/src/test/javascript/features/opensocial-templates/util_test.js
new file mode 100644
index 0000000..f4a3391
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/opensocial-templates/util_test.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Unit test for various DOM utils.
+ */
+function testDomUtils() {
+  var sourceNode = document.getElementById('domSource');
+  var targetNode = document.getElementById('domTarget');
+  var html = sourceNode.innerHTML;
+  targetNode.innerHTML = '';
+
+  // test appendChildren
+  os.appendChildren(sourceNode, targetNode);
+  assertEquals(html, targetNode.innerHTML);
+
+  // test removeChildren
+  os.removeChildren(targetNode);
+  assertEquals(0, targetNode.childNodes.length);
+}
+
+/**
+ * Unit test for createPropertyGetter
+ */
+function testGetPropertyGetterName() {
+  assertEquals('getFoo', os.getPropertyGetterName('foo'));
+  assertEquals('getFooBar', os.getPropertyGetterName('fooBar'));
+}
+
+/**
+ * Unit test for convertConstantToCamelCase.
+ */
+function testConvertToCamelCase() {
+  assertEquals('foo', os.convertToCamelCase('FOO'));
+  assertEquals('fooBar', os.convertToCamelCase('FOO_BAR'));
+  assertEquals('fooBarBaz', os.convertToCamelCase('FOO_BAR__BAZ'));
+}
diff --git a/trunk/features/src/test/javascript/features/osapi/batchtest.js b/trunk/features/src/test/javascript/features/osapi/batchtest.js
new file mode 100644
index 0000000..dcf3a7d
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/osapi/batchtest.js
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function BatchTest(name) {
+  TestCase.call(this, name);
+};
+
+BatchTest.inherits(TestCase);
+
+BatchTest.prototype.setUp = function() {
+  shindig = shindig || {};
+  shindig.auth = {};
+  shindig.auth.getSecurityToken =  function() {
+    return 'dsjk452487sdf7sdf865%&^*&^8cjhsdf';
+  };
+
+  window._setTimeout = window.setTimeout;
+  window.setTimeout = function(fn, time) { fn.call()};
+
+  document.scripts = [];
+  gadgets.config.init({ "osapi.services" : {
+      "http://%host%/social/rpc" : ["system.listMethods", "people.get", "activities.get", 
+        "activities.create", "appdata.get", "appdata.update", "appdata.delete"] }
+  });
+};
+
+BatchTest.prototype.tearDown = function() {
+  shindig.auth = undefined;
+  window.setTimeout = window._setTimeout;
+};
+
+BatchTest.prototype.testAddAndExecuteOneRequests = function() {
+  var batch = osapi.newBatch();
+  this.assertBatchMembers(batch);
+  batch.add('friends', osapi.people.get());
+  var expectedJson = [{method:"people.get",params:
+    {userId:"@viewer",groupId:"@self"},
+      id:"friends"}
+    ];
+
+  var argsInCallToMakeNonProxiedRequest;
+  var oldMakeRequest = gadgets.io.makeNonProxiedRequest;
+  try {
+    gadgets.io.makeNonProxiedRequest = function(url, callback, params, headers) {
+      argsInCallToMakeNonProxiedRequest = { url : url, callback : callback, params : params,
+        headers : headers};
+    };
+    batch.execute(function() {});
+
+    this.assertArgsToMakeNonProxiedRequest(argsInCallToMakeNonProxiedRequest, expectedJson);
+
+
+  } finally {
+    gadgets.io.makeNonProxiedRequest = oldMakeRequest;
+  }
+};
+
+BatchTest.prototype.testAddAndExecuteTwoRequests = function() {
+  var batch = osapi.newBatch();
+  this.assertBatchMembers(batch);
+
+  batch.add('friends', osapi.people.get()).
+      add('activities', osapi.activities.get());
+
+  var expectedJson = [{method:"people.get",params:
+    {userId:"@viewer",groupId:"@self"},
+      id:"friends"},
+    {method:"activities.get",params:
+      {userId:"@viewer",groupId:"@self"},id:"activities"}
+    ];
+
+  var argsInCallToMakeNonProxiedRequest;
+  var oldMakeRequest = gadgets.io.makeNonProxiedRequest;
+  try {
+    gadgets.io.makeNonProxiedRequest = function(url, callback, params, headers) {
+      argsInCallToMakeNonProxiedRequest = { url : url, callback : callback, params : params,
+        headers : headers};
+    };
+    batch.execute(function() {});
+    this.assertArgsToMakeNonProxiedRequest(argsInCallToMakeNonProxiedRequest, expectedJson);
+  } finally {
+    gadgets.io.makeNonProxiedRequest = oldMakeRequest;
+  }
+};
+
+BatchTest.prototype.testEmptyBatch = function() {
+  var batch = osapi.newBatch();
+  this.assertBatchMembers(batch);
+
+  var that = this;
+  batch.execute(function(data) {
+    that.assertTrue("Data should be returned", data);
+    that.assertTrue("Data should be empty", typeof data.length == 'undefined');
+  });
+};
+
+/**
+ * Checks to see if generated batch function has the correct members. 
+ *
+ * @param fn (Function) The function which should have these properties
+ */
+BatchTest.prototype.assertBatchMembers = function(fn) {
+  this.assertTrue('Should have produced a batch', typeof fn != 'undefined');
+  this.assertTrue('Should have an execute method', fn.execute);
+  this.assertTrue('Should have an add method', fn.add);
+};
+
+
+
+function BatchTestSuite() {
+  TestSuite.call(this, 'BatchTestSuite');
+  this.addTestSuite(BatchTest);
+}
+
+BatchTestSuite.inherits(TestSuite);
diff --git a/trunk/features/src/test/javascript/features/osapi/jsonrpctransporttest.js b/trunk/features/src/test/javascript/features/osapi/jsonrpctransporttest.js
new file mode 100644
index 0000000..742bc69
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/osapi/jsonrpctransporttest.js
@@ -0,0 +1,423 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function JsonRpcTransportTest(name) {
+  TestCase.call(this, name);
+};
+
+JsonRpcTransportTest.inherits(TestCase);
+
+var lastXhr = {};
+
+JsonRpcTransportTest.prototype.dummyXhr = function(url, callback, params, headers) {
+  lastXhr.url = url;
+  lastXhr.callback = callback;
+  lastXhr.params = params;
+  lastXhr.headers = headers;
+  callback(lastXhr.result);
+};
+
+
+JsonRpcTransportTest.prototype.setUp = function() {
+  shindig = shindig || {};
+  shindig.auth = {};
+  shindig.auth.getSecurityToken = function() {
+    return 'dsjk452487sdf7sdf865%&^*&^8cjhsdf';
+  };
+
+  gadgets.io._makeNonProxiedRequest = gadgets.io.makeNonProxiedRequest;
+  gadgets.io.makeNonProxiedRequest = this.dummyXhr;
+  lastXhr = {};
+  document.scripts = [];
+  gadgets.config.init({ "osapi.services" : {
+      "http://%host%/social/rpc" : ["system.listMethods", "people.get", "activities.get", 
+        "activities.create", "appdata.get", "appdata.update", "appdata.delete"] }
+  });
+
+  window._setTimeout = window.setTimeout;
+  window.setTimeout = function(fn, time) { fn.call()};
+
+};
+
+JsonRpcTransportTest.prototype.tearDown = function() {
+  shindig.auth = undefined;
+  gadgets.io.makeNonProxiedRequest = gadgets.io._makeNonProxiedRequest;
+  window.setTimeout = window._setTimeout;
+};
+
+JsonRpcTransportTest.prototype.testJsonBuilding = function() {
+  var getFn = osapi.activities.get({ userId : '@viewer', groupId : '@self'});
+  this.assertRequestPropertiesForService(getFn);
+
+  var expectedJson = [{ method : 'activities.get', id : "activities.get",
+    params : {
+      groupId : '@self',
+      userId : '@viewer'
+    }
+  }];
+
+  lastXhr.result = {data : [{ id : "activities.get", result : {}}], errors : []};
+
+  getFn.execute(function() {});
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+};
+
+JsonRpcTransportTest.prototype.testPluralGet = function() {
+  var getVieweractivitiesFn = osapi.activities.get({ userId : '@viewer', groupId : '@self'});
+  this.assertRequestPropertiesForService(getVieweractivitiesFn);
+
+  var expectedJson = [{ method : "activities.get",
+      id : "activities.get",
+      params : { userId : '@viewer',
+      groupId : '@self'
+    }
+  }];
+
+  lastXhr.result = { data :
+      [{id : "activities.get",
+        data:
+         {list:
+    	  [{title:"yellow",userId:"john.doe",id:"1",body:"what a color!"}], 
+    	  totalResults :1, startIndex:0}}], 
+    	  errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have one entry", 1, response.list.length);
+    that.assertEquals("Should match title of activity", "yellow", response.list[0].title);
+  });
+
+
+  getVieweractivitiesFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+JsonRpcTransportTest.prototype.testNoParamGetsUsesDefaults = function() {
+  var getVieweractivitiesFn = osapi.activities.get();
+  this.assertRequestPropertiesForService(getVieweractivitiesFn);
+
+  var expectedJson = [{ method : "activities.get",
+      id : "activities.get",
+      params : { userId : '@viewer',
+      groupId : '@self'
+    }
+  }];
+
+  lastXhr.result = { data :
+      [{id : "activities.get",
+        data:
+         {list:
+    	  [{title:"yellow",userId:"john.doe",id:"1",body:"what a color!"}],
+    	  totalResults :1, startIndex:0}}], 
+    	  errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have one entry", 1, response.list.length);
+    that.assertEquals("Should match title of activity", "yellow", response.list[0].title);
+  });
+
+  getVieweractivitiesFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+JsonRpcTransportTest.prototype.testNonDefaultGroupGet = function() {
+  var getViewerFriendActivitiesFn = osapi.activities.get({ userId : '@viewer',
+    groupId : '@friends'});
+  this.assertRequestPropertiesForService(getViewerFriendActivitiesFn);
+
+  var expectedJson = [{ method : "activities.get",
+      id : "activities.get",
+      params : { userId : '@viewer',
+      groupId : '@friends'}
+  }];
+
+  lastXhr.result = { data :
+      [{id : "activities.get",
+        data:
+         {list:
+    	  [{title:"yellow",userId:"john.doe",id:"1",body:"what a color!"}, 
+    	   {title:"Your New Activity",id:"1234396143857", body:"Blah Blah"}],
+    	   totalResults:2,startIndex:0}}],
+    	   errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have two activities", 2, response.list.length);
+    that.assertEquals("Should match title of activity", "yellow", response.list[0].title);
+    that.assertEquals("Should match title of activity", "Your New Activity",
+        response.list[1].title);
+  });
+
+  getViewerFriendActivitiesFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+JsonRpcTransportTest.prototype.testCreate = function() {
+  var createActivityFn = osapi.activities.create({ userId : '@viewer',
+    activity : { title : "New Activity", body : "Blah blah blah." }});
+  this.assertRequestPropertiesForService(createActivityFn);
+
+  var expectedJson = [{ method : "activities.create",
+      id : "activities.create",
+      params : { userId : '@viewer',
+        groupId : '@self',
+        activity : { title : "New Activity", body : "Blah blah blah."}}
+  }];
+
+  lastXhr.result = { data : [{id : "activities.create", data: {}}], errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have no activities", undefined, response.length);
+  });
+
+  createActivityFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+
+function JsonRpcTransportTestSuite() {
+  TestSuite.call(this, 'JsonRpcTransportTestSuite');
+  this.addTestSuite(JsonRpcTransportTest);
+}
+
+JsonRpcTransportTestSuite.inherits(TestSuite);
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function JsonRpcTransportTest(name) {
+  TestCase.call(this, name);
+};
+
+JsonRpcTransportTest.inherits(TestCase);
+
+var lastXhr = {};
+
+JsonRpcTransportTest.prototype.dummyXhr = function(url, callback, params, headers) {
+  lastXhr.url = url;
+  lastXhr.callback = callback;
+  lastXhr.params = params;
+  lastXhr.headers = headers;
+  callback(lastXhr.result);
+};
+
+
+JsonRpcTransportTest.prototype.setUp = function() {
+  shindig = shindig || {};
+  shindig.auth = {};
+  shindig.auth.getSecurityToken = function() {
+    return 'dsjk452487sdf7sdf865%&^*&^8cjhsdf';
+  };
+
+  gadgets.io._makeNonProxiedRequest = gadgets.io.makeNonProxiedRequest;
+  gadgets.io.makeNonProxiedRequest = this.dummyXhr;
+  lastXhr = {};
+  gadgets.config.init({ "osapi.services" : {
+      "http://%host%/social/rpc" : ["system.listMethods", "people.get", "activities.get", 
+        "activities.create", "appdata.get", "appdata.update", "appdata.delete"] }
+  });
+
+  window._setTimeout = window.setTimeout;
+  window.setTimeout = function(fn, time) { fn.call()};
+
+};
+
+JsonRpcTransportTest.prototype.tearDown = function() {
+  shindig.auth = undefined;
+  gadgets.io.makeNonProxiedRequest = gadgets.io._makeNonProxiedRequest;
+  window.setTimeout = window._setTimeout;
+};
+
+JsonRpcTransportTest.prototype.testJsonBuilding = function() {
+  var getFn = osapi.activities.get({ userId : '@viewer', groupId : '@self'});
+  this.assertRequestPropertiesForService(getFn);
+
+  var expectedJson = [{ method : 'activities.get', id : "activities.get",
+    params : {
+      groupId : '@self',
+      userId : '@viewer'
+    }
+  }];
+
+  lastXhr.result = {data : [{ id : "activities.get", result : {}}], errors : []};
+
+  getFn.execute(function() {});
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+};
+
+JsonRpcTransportTest.prototype.testPluralGet = function() {
+  var getVieweractivitiesFn = osapi.activities.get({ userId : '@viewer', groupId : '@self'});
+  this.assertRequestPropertiesForService(getVieweractivitiesFn);
+
+  var expectedJson = [{ method : "activities.get",
+      id : "activities.get",
+      params : { userId : '@viewer',
+      groupId : '@self'
+    }
+  }];
+
+  lastXhr.result = { data :
+      [{id : "activities.get",
+        data:
+         {list:
+    	  [{title:"yellow",userId:"john.doe",id:"1",body:"what a color!"}], 
+    	  totalResults :1, startIndex:0}}], 
+    	  errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have one entry", 1, response.list.length);
+    that.assertEquals("Should match title of activity", "yellow", response.list[0].title);
+  });
+
+
+  getVieweractivitiesFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+JsonRpcTransportTest.prototype.testNoParamGetsUsesDefaults = function() {
+  var getVieweractivitiesFn = osapi.activities.get();
+  this.assertRequestPropertiesForService(getVieweractivitiesFn);
+
+  var expectedJson = [{ method : "activities.get",
+      id : "activities.get",
+      params : { userId : '@viewer',
+      groupId : '@self'
+    }
+  }];
+
+  lastXhr.result = { data :
+      [{id : "activities.get",
+        data:
+         {list:
+    	  [{title:"yellow",userId:"john.doe",id:"1",body:"what a color!"}],
+    	  totalResults :1, startIndex:0}}], 
+    	  errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have one entry", 1, response.list.length);
+    that.assertEquals("Should match title of activity", "yellow", response.list[0].title);
+  });
+
+  getVieweractivitiesFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+JsonRpcTransportTest.prototype.testNonDefaultGroupGet = function() {
+  var getViewerFriendActivitiesFn = osapi.activities.get({ userId : '@viewer',
+    groupId : '@friends'});
+  this.assertRequestPropertiesForService(getViewerFriendActivitiesFn);
+
+  var expectedJson = [{ method : "activities.get",
+      id : "activities.get",
+      params : { userId : '@viewer',
+      groupId : '@friends'}
+  }];
+
+  lastXhr.result = { data :
+      [{id : "activities.get",
+        data:
+         {list:
+    	  [{title:"yellow",userId:"john.doe",id:"1",body:"what a color!"}, 
+    	   {title:"Your New Activity",id:"1234396143857", body:"Blah Blah"}],
+    	   totalResults:2,startIndex:0}}],
+    	   errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have two activities", 2, response.list.length);
+    that.assertEquals("Should match title of activity", "yellow", response.list[0].title);
+    that.assertEquals("Should match title of activity", "Your New Activity",
+        response.list[1].title);
+  });
+
+  getViewerFriendActivitiesFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+JsonRpcTransportTest.prototype.testCreate = function() {
+  var createActivityFn = osapi.activities.create({ userId : '@viewer',
+    activity : { title : "New Activity", body : "Blah blah blah." }});
+  this.assertRequestPropertiesForService(createActivityFn);
+
+  var expectedJson = [{ method : "activities.create",
+      id : "activities.create",
+      params : { userId : '@viewer',
+        groupId : '@self',
+        activity : { title : "New Activity", body : "Blah blah blah."}}
+  }];
+
+  lastXhr.result = { data : [{id : "activities.create", data: {}}], errors : []};
+
+  var that = this;
+  var inspectableCallback = makeInspectableCallback(function (response) {
+    that.assertTrue("callback from execute should have gotten a response", response);
+    that.assertFalse("should not be an error in callback response", response.error);
+    that.assertEquals("Should have no activities", undefined, response.length);
+  });
+
+  createActivityFn.execute(inspectableCallback.callback);
+  this.assertArgsToMakeNonProxiedRequest(lastXhr, expectedJson);
+  this.assertTrue("should have called the callback", inspectableCallback.wasCalled());
+};
+
+
+function JsonRpcTransportTestSuite() {
+  TestSuite.call(this, 'JsonRpcTransportTestSuite');
+  this.addTestSuite(JsonRpcTransportTest);
+}
+
+JsonRpcTransportTestSuite.inherits(TestSuite);
diff --git a/trunk/features/src/test/javascript/features/osapi/osapitest.js b/trunk/features/src/test/javascript/features/osapi/osapitest.js
new file mode 100644
index 0000000..086d057
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/osapi/osapitest.js
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+function OsapiTest(name) {
+  TestCase.call(this, name);
+};
+
+OsapiTest.inherits(TestCase);
+
+OsapiTest.prototype.setUp = function() {
+  window._setTimeout = window.setTimeout;
+  window.setTimeout = function() {};
+};
+
+OsapiTest.prototype.tearDown = function() {
+  window.setTimeout = window._setTimeout;
+};
+
+OsapiTest.prototype.testCall = function() {
+  var transport = {};
+  osapi._registerMethod("test.method", transport);
+  var transportCalled = false;
+  transport.execute = function(requests, callback) {
+    transportCalled = true;
+    callback([
+      {id:"test.method",result:{a:"b"}}
+    ]);
+  };
+  var callbackCalled = false;
+  osapi.test.method({}).execute(function(result) {
+    callbackCalled = true;
+  });
+  this.assertTrue("osapi transport correctly called", transportCalled);
+  this.assertTrue("osapi callback correctly called", callbackCalled);
+};
+
+
+function OsapiTestSuite() {
+  TestSuite.call(this, 'OsapiTestSuite');
+  this.addTestSuite(OsapiTest);
+}
+
+OsapiTestSuite.inherits(TestSuite);
diff --git a/trunk/features/src/test/javascript/features/selection/selection_test.js b/trunk/features/src/test/javascript/features/selection/selection_test.js
new file mode 100644
index 0000000..b646969
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/selection/selection_test.js
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview Tests for selection
+ */
+
+function SelectionTest(name) {
+  TestCase.call(this, name);
+}
+
+SelectionTest.inherits(TestCase);
+
+(function() {
+
+  SelectionTest.prototype.setUp = function() {
+    this.apiUri = window.__API_URI;
+    window.__API_URI = shindig.uri('http://shindig.com');
+    this.containerUri = window.__CONTAINER_URI;
+    window.__CONTAINER_URI = shindig.uri('http://container.com');
+    this.shindigContainerGadgetSite = osapi.container.GadgetSite;
+
+    this.gadgetsRpc = gadgets.rpc;
+    var that = this;
+    gadgets.rpc = {};
+    gadgets.rpc.register = function() {
+    };
+    gadgets.rpc.call = function() {
+      that.rpcArguments = Array.prototype.slice.call(arguments);
+    };
+  };
+
+  SelectionTest.prototype.tearDown = function() {
+    window.__API_URI = this.apiUri;
+    window.__CONTAINER_URI = this.containerUri;
+    osapi.container.GadgetSite = this.shindigContainerGadgetSite;
+    gadgets.rpc = this.gadgetsRpc;
+    this.rpcArguments = undefined;
+  };
+
+  SelectionTest.prototype.testContainerSetGetSelection = function() {
+    var container = new osapi.container.Container({});
+    var _token = "hello";
+    container.selection.setSelection(_token);
+    var token = container.selection.getSelection();
+    this.assertEquals(_token, token);
+  };
+
+  SelectionTest.prototype.testGadgetSetGetSelection = function() {
+    var container = new osapi.container.Container({});
+    var token = "hello";
+    gadgets.selection.setSelection(token);
+    this.assertRpcCalled("..", "gadgets.selection.set", null, token);
+  };
+
+  SelectionTest.prototype.testGadgetGetSelection = function() {
+    var container = new osapi.container.Container({});
+    var token = "hello";
+    gadgets.selection.setSelection(token);
+    var _token = gadgets.selection.getSelection();
+    this.assertEquals(token, _token);
+  };
+
+  SelectionTest.prototype.testGadgetAddSelectionListener = function() {
+    var container = new osapi.container.Container({});
+    gadgets.selection.addListener(function(){});
+    this.assertRpcCalled("..", "gadgets.selection.register", function(){});
+    gadgets.selection.addListener(function(){});
+    this.assertNoRpcCalled();
+  };
+
+  /**
+   * Asserts gadgets.rpc.call() is called with the expected arguments given.
+   * Note that it resets this.rpcArguments for next RPC call assertion.
+   */
+  SelectionTest.prototype.assertRpcCalled = function() {
+    this.assertNotUndefined("RPC was not called.", this.rpcArguments);
+    this.assertEquals("RPC argument list not valid length.", arguments.length,
+        this.rpcArguments.length);
+
+    for ( var i = 0; i < arguments.length; i++) {
+      this.assertEquals(arguments[i], this.rpcArguments[i]);
+    }
+    this.resetRpc();
+  };
+
+  /**
+   * Resets this.rpcArguments.
+   */
+  SelectionTest.prototype.resetRpc = function() {
+    this.rpcArguments = undefined;
+  };
+
+  /**
+   * Asserts that no gadgets.rpc.call() is called.
+   */
+  SelectionTest.prototype.assertNoRpcCalled = function() {
+    this.assertUndefined("RPC was called.", this.rpcArguments);
+  };
+
+})();
\ No newline at end of file
diff --git a/trunk/features/src/test/javascript/features/setprefs/setprefstest.js b/trunk/features/src/test/javascript/features/setprefs/setprefstest.js
new file mode 100644
index 0000000..e020470
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/setprefs/setprefstest.js
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the setprefs feature.
+ */
+
+function SetPrefsTest(name) {
+  TestCase.call(this, name);
+}
+
+SetPrefsTest.inherits(TestCase);
+
+SetPrefsTest.prototype.setUp = function() {
+  var that = this;
+  this.savedRpc = gadgets.rpc;
+  gadgets.rpc = {};
+  gadgets.rpc.call = function() {
+    that.rpcArguments = Array.prototype.slice.call(arguments);
+  };
+};
+
+SetPrefsTest.prototype.tearDown = function() {
+  this.rpcArguments = undefined;
+  gadgets.rpc = this.savedRpc;
+};
+
+SetPrefsTest.prototype.testSet = function() {
+  var pref = new gadgets.Prefs();
+  gadgets.Prefs.setInternal_('key', 100);
+
+  // Clear the RPC call history before pref.set() is called.
+  this.resetRpc();
+
+  // The same group of values, should not invoke a RPC call.
+  pref.set('key', 100);
+  this.assertNoRpcCalled();
+
+  // Value altered, should invoke a RPC call.
+  pref.set('key', 200);
+  this.assertEquals(200, pref.getInt('key'));
+  this.assertRpcCalled(null, 'set_pref', null, 0, 'key', 200);
+
+  // Set the same value again, should not invoke a RPC call.
+  pref.set('key', 200);
+  this.assertNoRpcCalled();
+};
+
+SetPrefsTest.prototype.testSetArray = function() {
+  var pref = new gadgets.Prefs();
+  pref.setArray('array', ['foo', 'bar']);
+  var array = pref.getArray('array');
+  this.assertEquals(2, array.length);
+  this.assertEquals('foo', array[0]);
+  this.assertEquals('bar', array[1]);
+
+  this.assertRpcCalled(null, 'set_pref', null, 0, 'array', 'foo|bar');
+};
+
+SetPrefsTest.prototype.testSetArrayWithPipe = function() {
+  var pref = new gadgets.Prefs();
+  pref.setArray('array', ['foo', 'b|ar']);
+  var array = pref.getArray('array');
+  this.assertEquals(2, array.length);
+  this.assertEquals('foo', array[0]);
+  this.assertEquals('b|ar', array[1]);
+
+  this.assertRpcCalled(null, 'set_pref', null, 0, 'array', 'foo|b%7Car');
+};
+
+SetPrefsTest.prototype.testSetArrayWithNumbers = function() {
+  var pref = new gadgets.Prefs();
+  pref.setArray('array', [1, 2]);
+  var array = pref.getArray('array');
+  this.assertEquals(2, array.length);
+  this.assertEquals('1', array[0]);
+  this.assertEquals('2', array[1]);
+
+  this.assertRpcCalled(null, 'set_pref', null, 0, 'array', '1|2');
+};
+
+/**
+ * Asserts gadgets.rpc.call() is called with the expected arguments given.
+ * Note that it resets this.rpcArguments for next RPC call assertion.
+ */
+SetPrefsTest.prototype.assertRpcCalled = function() {
+  this.assertNotUndefined("RPC was not called.", this.rpcArguments);
+  this.assertEquals("RPC argument list not valid length.",
+      arguments.length, this.rpcArguments.length);
+    
+  for (var i = 0; i < arguments.length; i++) {
+    this.assertEquals(arguments[i], this.rpcArguments[i]);
+  }
+  this.resetRpc();
+};
+
+/**
+ * Resets this.rpcArguments.
+ */
+SetPrefsTest.prototype.resetRpc = function() {
+  this.rpcArguments = undefined;
+};
+
+/**
+ * Asserts that no gadgets.rpc.call() is called.
+ */
+SetPrefsTest.prototype.assertNoRpcCalled = function() {
+  this.assertUndefined("RPC was called.", this.rpcArguments);
+};
diff --git a/trunk/features/src/test/javascript/features/shindig.uri/uritest.js b/trunk/features/src/test/javascript/features/shindig.uri/uritest.js
new file mode 100644
index 0000000..8cdf13d
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/shindig.uri/uritest.js
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the shindig.uri library.
+ */
+
+function ShindigUriTest(name) {
+  TestCase.call(this, name);
+}
+
+ShindigUriTest.inherits(TestCase);
+
+ShindigUriTest.prototype.testParseFullUri = function() {
+  var str = "http://www.example.com/my/path?qk1=qv1&qk2=qv2#fk1=fv1&fk2=fv2";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("http", uri.getSchema());
+  this.assertEquals("www.example.com", uri.getAuthority());
+  this.assertEquals("http://www.example.com", uri.getOrigin());
+  this.assertEquals("/my/path", uri.getPath());
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+  this.assertEquals("qv1", uri.getQP("qk1"));
+  this.assertEquals("qv2", uri.getQP("qk2"));
+  this.assertEquals("fv1", uri.getFP("fk1"));
+  this.assertEquals("fv2", uri.getFP("fk2"));
+
+  // Check query and fragment again to ensure ordering doesn't matter.
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+
+  // Re-serialize all.
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParseQuerylessUri = function() {
+  var str = "http://www.example.com/my/path#fk1=fv1&fk2=fv2";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("http", uri.getSchema());
+  this.assertEquals("www.example.com", uri.getAuthority());
+  this.assertEquals("http://www.example.com", uri.getOrigin());
+  this.assertEquals("/my/path", uri.getPath());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+  this.assertEquals("fv1", uri.getFP("fk1"));
+  this.assertEquals("fv2", uri.getFP("fk2"));
+
+  // Check query and fragment again to ensure ordering doesn't matter.
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+
+  // Re-serialize all.
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParseFragmentlessUri = function() {
+  var str = "http://www.example.com/my/path?qk1=qv1&qk2=qv2";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("http", uri.getSchema());
+  this.assertEquals("www.example.com", uri.getAuthority());
+  this.assertEquals("http://www.example.com", uri.getOrigin());
+  this.assertEquals("/my/path", uri.getPath());
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("qv1", uri.getQP("qk1"));
+  this.assertEquals("qv2", uri.getQP("qk2"));
+
+  // Check query and fragment again to ensure ordering doesn't matter.
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+
+  // Re-serialize all.
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParseSchemalessUri = function() {
+  var str = "//www.example.com/my/path?qk1=qv1&qk2=qv2#fk1=fv1&fk2=fv2";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("", uri.getSchema());
+  this.assertEquals("www.example.com", uri.getAuthority());
+  this.assertEquals("//www.example.com", uri.getOrigin());
+  this.assertEquals("/my/path", uri.getPath());
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+  this.assertEquals("qv1", uri.getQP("qk1"));
+  this.assertEquals("qv2", uri.getQP("qk2"));
+  this.assertEquals("fv1", uri.getFP("fk1"));
+  this.assertEquals("fv2", uri.getFP("fk2"));
+
+  // Check query and fragment again to ensure ordering doesn't matter.
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+
+  // Re-serialize all.
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParseAuthoritylessUri = function() {
+  var str = "/my/path?qk1=qv1&qk2=qv2#fk1=fv1&fk2=fv2";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("", uri.getSchema());
+  this.assertEquals("", uri.getAuthority());
+  this.assertEquals("", uri.getOrigin());
+  this.assertEquals("/my/path", uri.getPath());
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+  this.assertEquals("qv1", uri.getQP("qk1"));
+  this.assertEquals("qv2", uri.getQP("qk2"));
+  this.assertEquals("fv1", uri.getFP("fk1"));
+  this.assertEquals("fv2", uri.getFP("fk2"));
+
+  // Check query and fragment again to ensure ordering doesn't matter.
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+
+  // Re-serialize all.
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParsePathlessUri = function() {
+  var str = "http://www.example.com?qk1=qv1&qk2=qv2#fk1=fv1&fk2=fv2";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("http", uri.getSchema());
+  this.assertEquals("www.example.com", uri.getAuthority());
+  this.assertEquals("http://www.example.com", uri.getOrigin());
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+  this.assertEquals("qv1", uri.getQP("qk1"));
+  this.assertEquals("qv2", uri.getQP("qk2"));
+  this.assertEquals("fv1", uri.getFP("fk1"));
+  this.assertEquals("fv2", uri.getFP("fk2"));
+
+  // Check query and fragment again to ensure ordering doesn't matter.
+  this.assertEquals("qk1=qv1&qk2=qv2", uri.getQuery());
+  this.assertEquals("fk1=fv1&fk2=fv2", uri.getFragment());
+
+  // Re-serialize all.
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParsePathOnly = function() {
+  var str = "/my/path";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("", uri.getSchema());
+  this.assertEquals("", uri.getAuthority());
+  this.assertEquals("", uri.getOrigin());
+  this.assertEquals("/my/path", uri.getPath());
+
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParseQueryOnly = function() {
+  var str = "?foo=bar&baz=bak&boo=hiss";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("bar", uri.getQP("foo"));
+  this.assertEquals("bak", uri.getQP("baz"));
+  this.assertEquals("hiss", uri.getQP("boo"));
+
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParseFragmentOnly = function() {
+  var str = "#foo=bar&baz=bak";
+  var uri = shindig.uri(str);
+
+  this.assertEquals("bar", uri.getFP("foo"));
+  this.assertEquals("bak", uri.getFP("baz"));
+
+  this.assertEquals(str, uri.toString());
+};
+
+ShindigUriTest.prototype.testParseWithContextualOddities = function() {
+  var uri = shindig.uri("//www.example.com/my//path?#");
+
+  this.assertEquals("", uri.getSchema());
+  this.assertEquals("www.example.com", uri.getAuthority());
+  this.assertEquals("/my//path", uri.getPath());
+  this.assertEquals("", uri.getQuery());
+  this.assertEquals("", uri.getFragment());
+
+  this.assertEquals("//www.example.com/my//path", uri.toString());
+};
+
+ShindigUriTest.prototype.testParseQueryNullAndMissing = function() {
+  var uri = shindig.uri("?&one=two&three&&four=five");
+  this.assertEquals("two", uri.getQP("one"));
+  this.assertTrue(null === uri.getQP("three"));
+  this.assertTrue(undefined === uri.getQP("nonexistent"));
+  this.assertEquals("five", uri.getQP("four"));
+};
+
+ShindigUriTest.prototype.testParseFragmentNullAndMissing = function() {
+  var uri = shindig.uri("#&one=two&three&&four=five");
+  this.assertEquals("two", uri.getFP("one"));
+  this.assertTrue(null === uri.getFP("three"));
+  this.assertTrue(undefined === uri.getQP("nonexistent"));
+  this.assertEquals("five", uri.getFP("four"));
+};
+
+ShindigUriTest.prototype.testBuildFullUri = function() {
+  var uri = shindig.uri().setSchema("http")
+                         .setAuthority("www.example.com")
+                         .setPath("/my/path")
+                         .setQuery("?one=two&three=four")
+                         .setFragment("#five=six");
+  this.assertEquals("http://www.example.com/my/path?one=two&three=four#five=six", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildSchemalessUri = function() {
+  var uri = shindig.uri().setAuthority("www.example.com")
+                         .setPath("/my/path")
+                         .setQuery("?one=two&three=four")
+                         .setFragment("#five=six");
+  this.assertEquals("//www.example.com/my/path?one=two&three=four#five=six", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildAuthoritylessUri = function() {
+  var uri = shindig.uri().setPath("/my/path")
+                         .setQuery("?one=two&three=four")
+                         .setFragment("#five=six");
+  this.assertEquals("/my/path?one=two&three=four#five=six", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildPathlessUri = function() {
+  var uri = shindig.uri().setSchema("http")
+                         .setAuthority("www.example.com")
+                         .setQuery("?one=two&three=four")
+                         .setFragment("#five=six");
+  this.assertEquals("http://www.example.com?one=two&three=four#five=six", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildQuerylessUri = function() {
+  var uri = shindig.uri().setSchema("http")
+                         .setAuthority("www.example.com")
+                         .setPath("/my/path")
+                         .setFragment("#five=six");
+  this.assertEquals("http://www.example.com/my/path#five=six", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildFragmentlessUri = function() {
+  var uri = shindig.uri().setSchema("http")
+                         .setAuthority("www.example.com")
+                         .setPath("/my/path")
+                         .setQuery("?one=two&three=four");
+  this.assertEquals("http://www.example.com/my/path?one=two&three=four", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildPath = function() {
+  var uri = shindig.uri().setPath("/my/path");
+  this.assertEquals("/my/path", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildAuthority = function() {
+  var uri = shindig.uri().setAuthority("www.example.com");
+  this.assertEquals("//www.example.com", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildDirectQuery = function() {
+  var uri = shindig.uri().setQuery("one=two&three&&four=five");
+  this.assertEquals("two", uri.getQP("one"));
+  this.assertTrue(null === uri.getQP("three"));
+  this.assertTrue(undefined === uri.getQP("nonexistent"));
+  this.assertEquals("five", uri.getQP("four"));
+};
+
+ShindigUriTest.prototype.testBuildDirectFragment = function() {
+  var uri = shindig.uri().setFragment("one=two&three&&four=five");
+  this.assertEquals("two", uri.getFP("one"));
+  this.assertTrue(null === uri.getFP("three"));
+  this.assertTrue(undefined === uri.getQP("nonexistent"));
+  this.assertEquals("five", uri.getFP("four"));
+};
+
+ShindigUriTest.prototype.testBuildQuery = function() {
+  var uri = shindig.uri().setQuery("one=two&three=four");
+  this.assertEquals("two", uri.getQP("one"));
+  this.assertEquals("four", uri.getQP("three"));
+  uri.setQP("three", "five");
+  this.assertEquals("two", uri.getQP("one"));
+  this.assertEquals("five", uri.getQP("three"));
+  uri.setQP({ one: "one", two: "two", three: "three" });
+  this.assertEquals("one", uri.getQP("one"));
+  this.assertEquals("two", uri.getQP("two"));
+  this.assertEquals("three", uri.getQP("three"));
+  uri.setQP("two", null);
+  this.assertEquals("?one=one&three=three&two", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildFragment = function() {
+  var uri = shindig.uri().setFragment("one=two&three=four");
+  this.assertEquals("two", uri.getFP("one"));
+  this.assertEquals("four", uri.getFP("three"));
+  uri.setFP("three", "five");
+  this.assertEquals("two", uri.getFP("one"));
+  this.assertEquals("five", uri.getFP("three"));
+  uri.setFP({ one: "one", two: "two", three: "three" });
+  this.assertEquals("one", uri.getFP("one"));
+  this.assertEquals("two", uri.getFP("two"));
+  this.assertEquals("three", uri.getFP("three"));
+  uri.setFP("two", null);
+  this.assertEquals("#one=one&three=three&two", uri.toString());
+};
+
+ShindigUriTest.prototype.testReplaceExistingQuery = function() {
+  var uri = shindig.uri().setQuery("one=two")
+                         .setFragment("three=four")
+                         .setExistingP("one", "111")
+                         .setExistingP("three", "333")
+                         .setExistingP("xxx", "yyy");
+  this.assertEquals("?one=111#three=333", uri.toString());
+};
+
+ShindigUriTest.prototype.testBuildWithOverrides = function() {
+  var uri =
+      shindig.uri("http://www.example.com/my/path?one=two&baz#three=four")
+        .setAuthority("www.foo.com")
+        .setQP("one", "five")
+        .setFragment("foo=bar");
+  this.assertEquals(null, uri.getQP("baz"));
+  this.assertEquals(uri.toString(), "http://www.foo.com/my/path?one=five&baz#foo=bar", uri.toString());
+};
diff --git a/trunk/features/src/test/javascript/features/views/requestnavigateto-test.js b/trunk/features/src/test/javascript/features/views/requestnavigateto-test.js
new file mode 100644
index 0000000..967bffd
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/views/requestnavigateto-test.js
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+
+function RequestNavigateToTest(name) {
+  TestCase.call(this, name);
+}
+
+RequestNavigateToTest.inherits(TestCase);
+
+(function() {
+
+var rpcs, oldRpc = gadgets.rpc;
+
+RequestNavigateToTest.prototype.setUp = function() {
+  rpcs = [];
+  gadgets.rpc = {
+    call: function() {
+      rpcs.push(arguments);
+    }
+  };
+};
+
+RequestNavigateToTest.prototype.tearDown = function() {
+  gadgets.rpc.call = oldRpc;
+};
+
+RequestNavigateToTest.prototype.testBasic = function() {
+  gadgets.views.requestNavigateTo("canvas");
+
+  this.assertEquals("requestNavigateTo", rpcs[0][1]);
+  this.assertEquals("canvas", rpcs[0][3]);
+};
+
+RequestNavigateToTest.prototype.testViewObject = function() {
+  gadgets.views.requestNavigateTo(new gadgets.views.View("canvas"));
+
+  this.assertEquals("requestNavigateTo", rpcs[0][1]);
+  this.assertEquals("canvas", rpcs[0][3]);
+};
+
+RequestNavigateToTest.prototype.testKeyValueParams = function() {
+  gadgets.views.requestNavigateTo("canvas", {foo:"bar"});
+
+  this.assertEquals("requestNavigateTo", rpcs[0][1]);
+  this.assertEquals("canvas", rpcs[0][3]);
+  this.assertEquals("bar", rpcs[0][4].foo);
+};
+
+RequestNavigateToTest.prototype.testUriParams = function() {
+  gadgets.views.requestNavigateTo("canvas", "/foo/bar?blah");
+
+  this.assertEquals("requestNavigateTo", rpcs[0][1]);
+  this.assertEquals("canvas", rpcs[0][3]);
+  this.assertEquals("/foo/bar?blah", rpcs[0][4]);
+};
+
+})();
+
diff --git a/trunk/features/src/test/javascript/features/views/urltemplatetest.js b/trunk/features/src/test/javascript/features/views/urltemplatetest.js
new file mode 100644
index 0000000..d626252
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/views/urltemplatetest.js
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for URL template functions of gadgets.views.
+ */
+
+function UrlTemplateTest(name) {
+  TestCase.call(this, name);
+}
+
+UrlTemplateTest.inherits(TestCase);
+
+UrlTemplateTest.prototype.setUp = function() {
+};
+
+UrlTemplateTest.prototype.tearDown = function() {
+};
+
+UrlTemplateTest.prototype.batchTest = function(testcases) {
+  for (var i = 0; i < testcases.length; ++i) {
+    var testcase = testcases[i];
+    var urlTemplate = testcase[0];
+    var environment = testcase[1];
+    var expected = testcase[2];
+
+    if (typeof expected === 'string') {
+      this.assertEquals(expected, gadgets.views.bind(urlTemplate, environment));
+    } else {
+      var fallenThrough = false;
+      try {
+        gadgets.views.bind(urlTemplate, environment);
+        fallenThrough = true;
+      } catch (e) {
+        this.assertEquals(expected.message, e.message);
+      }
+      this.assertFalse(fallenThrough);
+    }
+  }
+};
+
+UrlTemplateTest.prototype.testVariableSubstitution = function() {
+  this.batchTest([
+    [
+      'http://host/path/{A=65}{66=B}',
+      {
+        'A': 'a'
+      },
+      'http://host/path/aB'
+    ],
+
+    [
+      'http://host/path/{open}{social}{0.8}{d-_-b}',
+      {
+        'open': 'O',
+        'social': 'S',
+        '0.8': 'v0.8',
+        'd-_-b': '!'
+      },
+      'http://host/path/OSv0.8!'
+    ],
+
+    [
+      'http://host/path/{undefined_value}/suffix',
+      {
+        'value': 'undefined'
+      },
+      'http://host/path//suffix'
+    ],
+
+    [
+      'http://host/path/{recurring}{recurring}{recurring}',
+      {
+        'recurring': '.'
+      },
+      'http://host/path/...'
+    ],
+
+    [
+      null,
+      null,
+      new Error('Invalid urlTemplate')
+    ],
+
+    [
+      'http://host/path/{var}',
+      'string',
+      new Error('Invalid environment')
+    ],
+
+    [
+      'http://host/path/{invalid definition!!!}',
+      {
+        'value': 'defined'
+      },
+      new Error('Invalid syntax : {invalid definition!!!}')
+    ],
+
+    [
+      'http://host/path/{} is also invalid',
+      {
+        'value': 'defined'
+      },
+      new Error('Invalid syntax : {}')
+    ]
+
+  ]);
+};
+
+UrlTemplateTest.prototype.testPrefixOperator = function() {
+  this.batchTest([
+    [
+      'http://host/path/{-prefix|/|foo}{-prefix|/|bar}{-prefix|-|baz}',
+      {
+        'foo': 'bar',
+        'baz': ['b', 'a', 'z']
+      },
+      'http://host/path//bar-b-a-z'
+    ],
+
+    [
+      'http://host/path/{-prefix|/|foo=bar}',
+      {
+      },
+      'http://host/path//bar'
+    ]
+  ]);
+};
+
+UrlTemplateTest.prototype.testSuffixOperator = function() {
+  this.batchTest([
+    [
+      'http://host/path/{-suffix|/|foo}{-suffix|/|bar}{-suffix|-|baz}',
+      {
+        'foo': 'bar',
+        'baz': ['b', 'a', 'z']
+      },
+      'http://host/path/bar/b-a-z-'
+    ],
+
+    [
+      'http://host/path/{-suffix|/|foo=bar}',
+      {
+      },
+      'http://host/path/bar/'
+    ]
+  ]);
+};
+
+UrlTemplateTest.prototype.testListOperator = function() {
+  this.batchTest([
+    [
+      'http://host/path/{-list|/|foo}{-list|-|bar}{-list|-|baz}{-list|*|BAZ}',
+      {
+        'foo': ['f', 'o', 'o'],
+        'bar': [],
+        'BAZ': ['baz']
+      },
+      'http://host/path/f/o/obaz'
+    ]
+  ]);
+};
+
+UrlTemplateTest.prototype.testJoinOperator = function() {
+  this.batchTest([
+    [
+      'http://host/path/{-join|*|spam}/{-join|&|foo,bar,baz}{-join|-|b}',
+      {
+        'spam': 'eggs',
+        'foo': 'FOO',
+        'baz': 'BAZ'
+      },
+      'http://host/path/spam=eggs/foo=FOO&baz=BAZ'
+    ]
+  ]);
+};
+
+UrlTemplateTest.prototype.testOptOperator = function() {
+  this.batchTest([
+    [
+      'http://host/path/{-opt|spam|foo}/{-opt|eggs|foo,bar}/{-opt|ham|foo,bar,baz}',
+      {
+        'bar': [],
+        'baz': 'BAZ'
+      },
+      'http://host/path///ham'
+    ]
+  ]);
+};
+
+UrlTemplateTest.prototype.testNegOperator = function() {
+  this.batchTest([
+    [
+      'http://host/path/{-neg|spam|foo}/{-neg|eggs|foo,bar}/{-neg|ham|foo,bar,baz}',
+      {
+        'bar': [],
+        'baz': 'BAZ'
+      },
+      'http://host/path/spam/eggs/'
+    ]
+  ]);
+};
diff --git a/trunk/features/src/test/javascript/features/views/views-init-test.js b/trunk/features/src/test/javascript/features/views/views-init-test.js
new file mode 100644
index 0000000..240a068
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/views/views-init-test.js
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+function ViewsInitTest(name) {
+  TestCase.call(this, name);
+}
+
+ViewsInitTest.inherits(TestCase);
+
+(function() {
+
+var callback;
+
+ViewsInitTest.prototype.setUp = function() {
+  this.document = document;
+  this.getUrlParameters = gadgets.util.getUrlParameters;
+};
+ViewsInitTest.prototype.tearDown = function() {
+  document = this.document;
+  gadgets.util.getUrlParameters = this.getUrlParameters;
+};
+
+ViewsInitTest.prototype.testObjectParams = function() {
+  gadgets.util.getUrlParameters = function() {
+    return {"view-params": gadgets.json.stringify({foo: "bar"})};
+  };
+
+  document.scripts = [];
+  gadgets.config.init({views:{}});
+
+  this.assertEquals("bar", gadgets.views.getParams().foo);
+};
+
+ViewsInitTest.prototype.testStringParams = function() {
+  // In practice, containers should actually be passing this as the 'path' query string param
+  // to the gadget renderer, but we want to be sure that we can handle it just in case.
+  var path = "/foo/bar/baz.html?blah=blah&foo=bar";
+  gadgets.util.getUrlParameters = function() {
+    return {"view-params": gadgets.json.stringify(path)};
+  };
+
+  document.scripts = [];
+  gadgets.config.init({views:{}});
+
+  this.assertEquals(path, gadgets.views.getParams());
+};
+
+
+ViewsInitTest.prototype.testRewriteLinksStandards = function() {
+  var name, func, bubble;
+  document = {
+    addEventListener: function() {
+      name = arguments[0];
+      func = arguments[1];
+      bubble = arguments[2];
+    },
+    getElementsByTagName: function (name) { return []; }
+  };
+
+  document.scripts = [];
+  gadgets.config.init({views:{rewriteLinks: true}});
+
+  this.assertEquals("click", name);
+  this.assertTrue(typeof func === "function");
+  this.assertFalse(bubble);
+  this.assertTrue(typeof gadgets.views.getSupportedViews().rewriteLinks === "undefined");
+};
+
+ViewsInitTest.prototype.testRewriteLinksIe = function() {
+  var name, func, self = this;
+  document = {
+    attachEvent: function() {
+      name = arguments[0];
+      func = arguments[1];
+
+    },
+    addEventListener: undefined,
+    getElementsByTagName: function (name) { return []; }
+  };
+
+  document.scripts = [];
+  gadgets.config.init({views:{rewriteLinks: true}});
+
+  this.assertEquals("onclick", name);
+  this.assertTrue(typeof func === "function");
+  this.assertTrue(typeof gadgets.views.getSupportedViews().rewriteLinks === "undefined");
+};
+
+ViewsInitTest.prototype.testCurrentSubView = function() {
+  var viewName = "home.default";
+  gadgets.util.getUrlParameters = function() {
+    return {"view": viewName};
+  };
+
+  document.scripts = [];
+  gadgets.config.init({"views":{"home":{"isOnlyVisible": false},"default":{"isOnlyVisible": false}}});
+
+  this.assertTrue(gadgets.views.getCurrentView());
+  this.assertEquals(viewName, gadgets.views.getCurrentView().getName());
+};
+
+// TODO: Verify behavior of onclick.
+
+})();
diff --git a/trunk/features/src/test/javascript/features/xhrwrapper/xhrwrappertest.js b/trunk/features/src/test/javascript/features/xhrwrapper/xhrwrappertest.js
new file mode 100644
index 0000000..d79475e
--- /dev/null
+++ b/trunk/features/src/test/javascript/features/xhrwrapper/xhrwrappertest.js
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * @fileoverview
+ *
+ * Unittests for the xhrwrapper feature.
+ */
+
+var shindig = shindig || {};
+
+function XhrWrapperTest(name) {
+  TestCase.call(this, name);
+}
+XhrWrapperTest.inherits(TestCase);
+
+XhrWrapperTest.prototype.setUp = function() {
+  // prepare mocks
+  gadgets.io = gadgets.io || {};
+  window.location = window.location || {};
+  opensocial.xmlutil = opensocial.xmlutil || {};
+  this.oldMakeRequest = gadgets.io.makeRequest;
+  this.oldWindowLocation = window.location;
+  this.oldParseXML = opensocial.xmlutil.parseXML;
+  this.madeRequest = {};
+  gadgets.io.makeRequest = this.mockMakeRequest(this.madeRequest);
+  window.location = { 'href': 'http://shindig/gadgets/ifr?url=blah' };
+  opensocial.xmlutil.parseXML = XhrWrapperTest.mockParseXML;
+  document.scripts = [];
+  gadgets.config.init(
+    {"shindig.xhrwrapper": {"contentUrl": "http://foo.bar/baz/bax.html"}});
+};
+
+XhrWrapperTest.prototype.tearDown = function() {
+  // remove mocks
+  gadgets.io.makeRequest = this.oldMakeRequest;
+  window.location = this.oldWindowLocation;
+  opensocial.xmlutil.parseXML = this.oldParseXML;
+};
+
+XhrWrapperTest.prototype.testBasicWorking = function() {
+  var that = this;
+  var calledCallback = false;
+  var xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', 'http://foo.bar');
+  xhr.onreadystatechange = function(e) {
+    that.assertEquals('readystatechange', e.type);
+    that.assertEquals(xhr, e.target);
+    calledCallback = true;
+  };
+  xhr.send();
+
+  this.madeRequest.doCallback();
+
+  this.checkRequest('GET', 'http://foo.bar');
+  this.assertTrue(calledCallback);
+  this.assertEquals(4, xhr.readyState);
+  this.assertEquals('some text', xhr.responseText);
+  this.assertEquals('this would normally be XML', xhr.responseXML);
+  this.assertEquals(200, xhr.status);
+  this.assertEquals('no error', xhr.statusText);
+  this.assertEquals('v1', xhr.getResponseHeader('h1'));
+  this.assertEquals('v2', xhr.getResponseHeader('h2'));
+  this.assertEquals('h1: v1\nh2: v2\n', xhr.getAllResponseHeaders());
+};
+
+XhrWrapperTest.prototype.testAddRequestHeaders = function() {
+  var xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', 'http://foo.bar');
+  xhr.setRequestHeader('header', 'value');
+  xhr.send();
+
+  this.assertEquals('value', this.madeRequest.params.HEADERS['header']);
+};
+
+XhrWrapperTest.prototype.testSameOriginViolation = function() {
+  var thrown;
+  var xhr;
+
+  // Different schema
+  gadgets.config.init(
+    {"shindig.xhrwrapper": {"contentUrl": "https://foo.bar/baz/bax.html"}});  
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  try {
+    xhr.open('GET', 'http://foo.bar/thing');
+    thrown = false;
+  } catch (x) {
+    thrown = true;
+  }
+  this.assertTrue('Should have thrown an error.', thrown);
+
+  // Different authority
+  gadgets.config.init(
+    {"shindig.xhrwrapper": {"contentUrl": "http://baw.net/bax.html"}});  
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  try {
+    xhr.open('GET', 'http://foo.bar/thing');
+    thrown = false;
+  } catch (x) {
+    thrown = true;
+  }
+  this.assertTrue('Should have thrown an error.', thrown);
+
+  // Same schema and authority
+  gadgets.config.init(
+    {"shindig.xhrwrapper": {"contentUrl": "http://foo.bar/some/bax.html"}});
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  try {
+    xhr.open('GET', 'http://foo.bar/thing');
+    thrown = false;
+  } catch (x) {
+    thrown = true;
+  }
+  this.assertFalse('Should not have thrown an error.', thrown);
+};
+
+XhrWrapperTest.prototype.testResolveRelativeUrl = function() {
+  var xhr;
+
+  // Only path provided
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', '/foo/bar/baz.xml');
+  xhr.send();
+  this.checkRequest('GET', 'http://foo.bar/foo/bar/baz.xml');
+
+  // Schema missing
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', '//foo.bar/baz.xml');
+  xhr.send();
+  this.checkRequest('GET', 'http://foo.bar/baz.xml');
+};
+
+XhrWrapperTest.prototype.testRepointWrongUrls = function() {
+  var xhr;
+
+  // Only schema and hostname match
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', 'http://shindig/foo/bar/baz.xml');
+  xhr.send();
+  this.checkRequest('GET', 'http://foo.bar/foo/bar/baz.xml');
+
+  // Schema, hostname and first part of path match
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', 'http://shindig/gadgets/foo/bar/baz.xml');
+  xhr.send();
+  this.checkRequest('GET', 'http://foo.bar/baz/foo/bar/baz.xml');
+};
+
+XhrWrapperTest.prototype.testOauthParamsUsed = function() {
+  gadgets.config.init({
+      'shindig.xhrwrapper': {
+          'contentUrl': 'http://foo.bar/baz/bax.html',
+          'authorization': 'oauth',
+          'oauthService': 'testOauthService'
+    	}
+  });
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', '/foo/bar/baz.xml');
+  xhr.send();
+  this.checkOAuth('testOauthService');
+
+  gadgets.config.init({
+      'shindig.xhrwrapper': {
+          'contentUrl': 'http://foo.bar/baz/bax.html',
+          'authorization': 'oauth',
+          'oauthService': 'testOauthService',
+          'oauthTokenName': 'testOauthToken'
+    	}
+  });
+  xhr = new shindig.xhrwrapper.XhrWrapper();
+  xhr.open('GET', '/foo/bar/baz.xml');
+  xhr.send();
+  this.checkOAuth('testOauthService', 'testOauthToken');
+};
+
+XhrWrapperTest.prototype.testSignedAuthParamsUsed = function() {
+	  gadgets.config.init({
+	      'shindig.xhrwrapper': {
+	          'contentUrl': 'http://foo.bar/baz/bax.html',
+	          'authorization': 'signed'
+	    	}
+	  });
+	  xhr = new shindig.xhrwrapper.XhrWrapper();
+	  xhr.open('GET', '/foo/bar/baz.xml');
+	  xhr.send();
+
+	  this.assertEquals('SIGNED', this.madeRequest.params['AUTHORIZATION']);
+	};
+
+XhrWrapperTest.prototype.mockMakeRequest = function(info) {
+  var that = this;
+  return function(url, callback, opt_params) {
+    info.url = url;
+    info.callback = callback;
+    info.params = opt_params;
+    info.doCallback = function() {
+      var response = {
+        data: 'some data',
+        errors: [ 'no error' ],
+        headers: { h1: 'v1', h2: 'v2' },
+        rc: 200,
+        text: 'some text'
+      };
+      info.callback.call(null, response);
+    };
+  };
+};
+
+XhrWrapperTest.mockParseXML = function(t) {
+  return 'this would normally be XML';
+};
+
+XhrWrapperTest.prototype.checkRequest = function(method, url) {
+  this.assertEquals(method, this.madeRequest.params['METHOD']);
+  this.assertEquals(url, this.madeRequest.url);
+};
+
+XhrWrapperTest.prototype.checkOAuth = function(service, opt_token) {
+  this.assertEquals('OAUTH', this.madeRequest.params['AUTHORIZATION']);
+  this.assertEquals(service, this.madeRequest.params['OAUTH_SERVICE_NAME']);
+  this.assertEquals(opt_token, this.madeRequest.params['OAUTH_TOKEN_NAME']);
+};
diff --git a/trunk/features/src/test/javascript/lib/JsUnit.js b/trunk/features/src/test/javascript/lib/JsUnit.js
new file mode 100644
index 0000000..822ded4
--- /dev/null
+++ b/trunk/features/src/test/javascript/lib/JsUnit.js
@@ -0,0 +1,2521 @@
+/*
+JsUnit - a JUnit port for JavaScript
+Copyright (C) 1999,2000,2001,2002,2003,2006,2007 Joerg Schaible
+
+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.
+*/
+
+/**
+ * @file
+ * Test unit classes for JavaScript.
+ * This file contains a port of the JUnit Java package of Kent Beck and 
+ * Erich Gamma for JavaScript.
+ *
+ * If this file is loaded within a browser, an onLoad event handler is set.
+ * This event handler will set the global variable isJsUnitPageLoaded to true.
+ * Any previously set onLoad event handler is restored and called.
+ */
+
+
+// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+// JUnit framework classes
+// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+/**
+ * Thrown when a test assertion fails.
+ * @ctor
+ * Constructor.
+ * An AssertionFailedMessage needs a message and a call stack for construction.
+ * @tparam String msg Failure message.
+ * @tparam CallStack stack The call stack of the assertion.
+ */
+function AssertionFailedError( msg, stack )
+{
+    JsUnitError.call( this, msg );
+    /**
+     * The call stack for the message.
+     */
+    this.mCallStack = stack;
+}
+AssertionFailedError.prototype = new JsUnitError();
+/**
+ * The name of the AssertionFailedError class as String.
+ * @type String
+ */
+AssertionFailedError.prototype.name = "AssertionFailedError";
+
+
+/**
+ * Thrown when a test assert comparing equal strings fail.
+ * @ctor
+ * Constructor.
+ * An AssertionFailedMessage needs a message and a call stack for construction.
+ * @tparam String msg Failure message (optional).
+ * @tparam String expected The expected string value.
+ * @tparam String actual The actual string value.
+ * @tparam CallStack stack The call stack of the assertion.
+ */
+function ComparisonFailure( msg, expected, actual, stack )
+{
+    AssertionFailedError.call( 
+        this, ( msg ? msg + " " : "" ) + "expected", stack );
+    this.mExpected = new String( expected );
+    this.mActual = new String( actual );
+}
+/**
+ * Returns the error message.
+ * @treturn String Returns the formatted error message.
+ * Returns "..." in place of common prefix and "..." in
+ * place of common suffix between expected and actual.
+ */
+function ComparisonFailure_toString()
+{
+    var str = AssertionFailedError.prototype.toString.call( this );
+    
+    var end = Math.min( this.mExpected.length, this.mActual.length );
+    var i = 0;
+    for( ; i < end; ++i )
+        if( this.mExpected.charAt( i ) != this.mActual.charAt( i ))
+            break;
+    var j = this.mExpected.length - 1;
+    var k = this.mActual.length - 1;
+    for( ; k >= i && j >= i; --k, --j )
+        if( this.mExpected.charAt( j ) != this.mActual.charAt( k ))
+            break;
+
+    var expected;
+    var actual;
+
+    if( j < i && k < i )
+    {
+        expected = this.mExpected;
+        actual = this.mActual;
+    }
+    else
+    {
+        expected = this.mExpected.substring( i, j + 1 );
+        actual = this.mActual.substring( i, k + 1 );
+        if( i <= end && i > 0 )
+        {
+            expected = "..." + expected;
+            actual = "..." + actual;
+        }
+        if( j < this.mExpected.length - 1 )
+            expected += "...";
+        if( k < this.mActual.length - 1 )
+            actual += "...";
+    }
+    
+    return str + ":<" + expected + ">, but was:<" + actual + ">";
+}
+ComparisonFailure.prototype = new AssertionFailedError();
+ComparisonFailure.glue();
+/**
+ * The name of the ComparisonFailure class as String.
+ * @type String
+ */
+ComparisonFailure.prototype.name = "ComparisonFailure";
+
+
+/**
+ * A test can be run and collect its results.
+ * @note Additional to JsUnit 3.8 the test has always a name. The interface
+ * requires a getter and a setter and a method to search for tests.
+ */
+function Test()
+{
+}
+/**
+ * Counts the number of test cases that will be run by this test.
+ * @treturn Number The number of test cases.
+ */
+Test.prototype.countTestCases = function() {}
+/**
+ * Search a test by name.
+ * The function compares the given name with the name of the test and 
+ * returns its own instance if the name is equal.
+ * @note This is an enhancement to JUnit 3.8
+ * @tparam String testName The name of the searched test.
+ * @treturn Test The test instance itself of null.
+ */
+Test.prototype.findTest = function( testName ) {}
+/**
+ * Retrieves the name of the test.
+ * @note This is an enhancement to JUnit 3.8
+ * @treturn String The name of test.
+ */
+Test.prototype.getName = function() {}
+/**
+ * Runs the test.
+ * @tparam TestResult result The result to fill.
+ * @treturn TestResult The result of test cases.
+ */
+Test.prototype.run = function( result ) {}
+/**
+ * Sets the name of the test.
+ * @note This is an enhancement to JUnit 3.8
+ * @tparam String testName The new name of the test.
+ */
+Test.prototype.setName = function( testName ) {}
+
+
+/**
+ * A TestFailure collects a failed test together with the caught exception.
+ * @ctor
+ * Constructor.
+ * @tparam Test test The failed test.
+ * @tparam Error except The thrown error of the exception
+ * @see TestResult
+ */
+function TestFailure( test, except )
+{
+    this.mException = except;
+    this.mTest = test;
+}
+/**
+ * Retrieve the exception message.
+ * @treturn String Returns the exception message.
+ */
+function TestFailure_exceptionMessage()
+{ 
+    var ex = this.thrownException(); 
+    return ex ? ex.toString() : "";
+}
+/**
+ * Retrieve the failed test.
+ * @treturn Test Returns the failed test.
+ */
+function TestFailure_failedTest() { return this.mTest; }
+/**
+ * Test for a JsUnit failure.
+ * @treturn Boolean Returns true if the exception is a failure.
+ */
+function TestFailure_isFailure() 
+{ 
+    return this.thrownException() instanceof AssertionFailedError; 
+}
+/**
+ * Retrieve the thrown exception.
+ * @treturn Error Returns the thrown exception.
+ */
+function TestFailure_thrownException() { return this.mException; }
+/**
+ * Retrieve failure as string.
+ * Slightly enhanced message format compared to JsUnit 3.7.
+ * @treturn String Returns the error message.
+ */
+function TestFailure_toString() 
+{ 
+    return "Test " + this.mTest + " failed: " + this.exceptionMessage();
+}
+/**
+ * Retrieve the stack trace.
+ * @treturn String Returns stack trace (if available).
+ */
+function TestFailure_trace() 
+{ 
+    var ex = this.thrownException();
+    if( ex && ex.mCallStack )
+        return ex.mCallStack.toString();
+    else
+        return "";
+}
+TestFailure.glue();
+
+
+/**
+ * A protectable can be run and throw an Error.
+ */
+function Protectable()
+{
+}
+/**
+ * Runs a test.
+ * @tparam Test test The test to run.
+ */
+Protectable.prototype.protect = function( test ) {}
+
+
+/**
+ * A listener for test progress.
+ */
+function TestListener()
+{
+}
+/**
+ * An occurred error was added.
+ * @tparam Test test The failed test.
+ * @tparam Error except The thrown error.
+ */
+TestListener.prototype.addError = function( test, except ) {}
+/**
+ * An occured failure was added.
+ * @tparam Test test The failed test.
+ * @tparam AssertionFailedError afe The thrown assertion failure.
+ */
+TestListener.prototype.addFailure = function( test, afe ) {}
+/**
+ * A test ended.
+ * @tparam Test test The ended test.
+ */
+TestListener.prototype.endTest = function( test ) {}
+/**
+ * A test started
+ * @tparam Test test The started test.
+ */
+TestListener.prototype.startTest = function( test ) {}
+
+
+/**
+ * A TestResult collects the results of executing a test case.
+ * The test framework distinguishes between <i>failures</i> and <i>errors</i>.
+ * A failure is anticipated and checked for with assertions. Errors are
+ * unanticipated problems like a JavaScript run-time error.
+ *
+ * @see Test
+ */
+function TestResult()
+{
+    this.mErrors = new Array();
+    this.mFailures = new Array();
+    this.mListeners = new Array();
+    this.mRunTests = 0;
+    this.mStop = 0;
+}
+/**
+ * Add an occurred error.
+ * Add an occurred error and call the registered listeners.
+ * @tparam Test test The failed test.
+ * @tparam Error except The thrown error.
+ */
+function TestResult_addError( test, except )
+{
+    this.mErrors.push( new TestFailure( test, except ));
+    for( var i = 0; i < this.mListeners.length; ++i )
+        this.mListeners[i].addError( test, except );
+}
+/**
+ * Add an occurred failure.
+ * Add an occurred failure and call the registered listeners.
+ * @tparam Test test The failed test.
+ * @tparam AssertionFailedError afe The thrown assertion failure.
+ */
+function TestResult_addFailure( test, afe )
+{
+    this.mFailures.push( new TestFailure( test, afe ));
+    for( var i = 0; i < this.mListeners.length; ++i )
+        this.mListeners[i].addFailure( test, afe );
+}
+/**
+ * Add a listener.
+ * @tparam TestListener listener The listener.
+ */
+function TestResult_addListener( listener ) 
+{ 
+    this.mListeners.push( listener ); 
+}
+/**
+ * Returns a copy of the listeners.
+ * @treturn Array A copy of the listeners.
+ */
+function TestResult_cloneListeners() 
+{ 
+    var listeners = new Array();
+    for( var i = 0; i < this.mListeners.length; ++i )
+        listeners[i] = this.mListeners[i];
+    return listeners;
+}
+/**
+ * A test ended.
+ * A test ended, inform the listeners.
+ * @tparam Test test The ended test.
+ */
+function TestResult_endTest( test )
+{
+    for( var i = 0; i < this.mListeners.length; ++i )
+        this.mListeners[i].endTest( test );
+}
+/**
+ * Retrieve the number of occurred errors.
+ * @type Number
+ */
+function TestResult_errorCount() { return this.mErrors.length; }
+/**
+ * Retrieve the number of occurred failures.
+ * @type Number
+ */
+function TestResult_failureCount() { return this.mFailures.length; }
+/**
+ * Remove a listener.
+ * @tparam TestListener listener The listener.
+ */
+function TestResult_removeListener( listener ) 
+{ 
+    for( var i = 0; i < this.mListeners.length; ++i )
+    {
+        if( this.mListeners[i] == listener )
+        {
+            this.mListeners.splice( i, 1 );
+            break;
+        }
+    }
+}
+/**
+ * Runs a test case.
+ * @tparam Test test The test case to run.
+ */
+function TestResult_run( test )
+{
+    this.startTest( test );
+
+    function OnTheFly() {}
+    OnTheFly.prototype.protect = function() { this.mTest.runBare(); }
+    OnTheFly.prototype.mTest = test;
+    OnTheFly.fulfills( Protectable );
+    
+    this.runProtected( test, new OnTheFly());
+    this.endTest( test );
+}
+/**
+ * Retrieve the number of run tests.
+ * @type Number
+ */
+function TestResult_runCount() { return this.mRunTests; }
+/**
+ * Runs a test case protected.
+ * @tparam Test test The test case to run.
+ * @tparam Protectable p The protectable block running the test.
+ * To implement your own protected block that logs thrown exceptions, 
+ * pass a Protectable to TestResult.runProtected().
+ */
+function TestResult_runProtected( test, p )
+{
+    try
+    {
+        p.protect();
+    }
+    catch( ex )
+    {
+        if( ex instanceof AssertionFailedError )
+            this.addFailure( test, ex );
+        else
+            this.addError( test, ex );
+    }
+}
+/**
+ * Checks whether the test run should stop.
+ * @type Boolean
+ */
+function TestResult_shouldStop() { return this.mStop; }
+/**
+ * A test starts.
+ * A test starts, inform the listeners.
+ * @tparam Test test The test to start.
+ */
+function TestResult_startTest( test )
+{
+    ++this.mRunTests;
+
+    for( var i = 0; i < this.mListeners.length; ++i )
+        this.mListeners[i].startTest( test );
+}
+/**
+ * Marks that the test run should stop.
+ */
+function TestResult_stop() { this.mStop = 1; }
+/**
+ * Returns whether the entire test was successful or not.
+ * @type Boolean
+ */
+function TestResult_wasSuccessful() 
+{ 
+    return this.mErrors.length + this.mFailures.length == 0; 
+}
+TestResult.glue();
+TestResult.fulfills( TestListener );
+
+
+/**
+ * A set of assert methods.
+ */
+function Assert()
+{
+}
+/**
+ * Asserts that two values are equal.
+ * @tparam String msg An optional error message.
+ * @tparam Object expected The expected value.
+ * @tparam Object actual The actual value.
+ * @exception AssertionFailedError Thrown if the expected value is not the 
+ * actual one.
+ */
+function Assert_assertEquals( msg, expected, actual )
+{
+    if( arguments.length == 2 )
+    {
+        actual = expected;
+        expected = msg;
+        msg = null;
+    }
+    if( expected != actual )
+        if( typeof( expected ) == "string" && typeof( actual ) == "string" )
+            throw new ComparisonFailure( msg, expected, actual, new CallStack());
+        else
+            this.fail( "Expected:<" + expected + ">, but was:<" + actual + ">"
+                , new CallStack(), msg );
+}
+/**
+ * Asserts that a regular expression matches a string.
+ * @tparam String msg An optional error message.
+ * @tparam Object expected The regular expression.
+ * @tparam Object actual The actual value.
+ * @exception AssertionFailedError Thrown if the actual value does not match 
+ * the regular expression.
+ * @note This is an enhancement to JUnit 3.8
+ * @since 1.3
+ */
+function Assert_assertMatches( msg, expected, actual )
+{
+    if( arguments.length == 2 )
+    {
+        actual = expected;
+        expected = msg;
+        msg = null;
+    }
+    if( expected instanceof RegExp && typeof( actual ) == "string" )
+    {
+        if( !actual.match( expected ))
+            this.fail( "RegExp:<" + expected + "> did not match:<" + actual + ">", new CallStack(), msg );
+    }
+    else
+        this.fail( "Expected:<" + expected + ">, but was:<" + actual + ">"
+            , new CallStack(), msg );
+}
+/**
+ * Asserts that a condition is false.
+ * @tparam String msg An optional error message.
+ * @tparam String cond The condition to evaluate.
+ * @exception AssertionFailedError Thrown if the evaluation was not false.
+ */
+function Assert_assertFalse( msg, cond )
+{
+    if( arguments.length == 1 )
+    {
+        cond = msg;
+        msg = null;
+    }
+    if( eval( cond ))
+        this.fail( "Condition should have failed \"" + cond + "\""
+            , new CallStack(), msg );
+}
+/**
+ * Asserts that two floating point values are equal to within a given tolerance.
+ * @tparam String msg An optional error message.
+ * @tparam Object expected The expected value.
+ * @tparam Object actual The actual value.
+ * @tparam Object tolerance The maximum difference allowed to make equality check pass.
+ * @note This is an enhancement to JUnit 3.8
+ * @exception AssertionFailedError Thrown if the expected value is not within 
+ * the tolerance of the actual one.
+ */
+function Assert_assertFloatEquals( msg, expected, actual, tolerance)
+{
+    if( arguments.length == 3 )
+    {
+        tolerance = actual;
+        actual = expected;
+        expected = msg;
+        msg = null;
+    }
+    if(    typeof( actual ) != "number" 
+        || typeof( expected ) != "number" 
+        || typeof( tolerance ) != "number" )
+    {
+        this.fail( "Cannot compare " + expected + " and " + actual 
+            + " with tolerance " + tolerance + " (must all be numbers).");
+    }
+ 
+    if( Math.abs(expected - actual) > tolerance)
+    {
+        this.fail( "Expected:<" + expected + ">, but was:<" + actual + ">"
+            , new CallStack(), msg );
+    }
+}
+/**
+ * Asserts that an object is not null.
+ * @tparam String msg An optional error message.
+ * @tparam Object object The valid object.
+ * @exception AssertionFailedError Thrown if the object is not null.
+ */
+function Assert_assertNotNull( msg, object )
+{
+    if( arguments.length == 1 )
+    {
+        object = msg;
+        msg = null;
+    }
+    if( object === null )
+        this.fail( "Object was null.", new CallStack(), msg );
+}
+/**
+ * Asserts that two values are not the same.
+ * @tparam String msg An optional error message.
+ * @tparam Object expected The expected value.
+ * @tparam Object actual The actual value.
+ * @exception AssertionFailedError Thrown if the expected value is not the 
+ * actual one.
+ */
+function Assert_assertNotSame( msg, expected, actual )
+{
+    if( arguments.length == 2 )
+    {
+        actual = expected;
+        expected = msg;
+        msg = null;
+    }
+    if( expected === actual )
+        this.fail( "Not the same expected:<" + expected + ">"
+            , new CallStack(), msg );
+}
+/**
+ * Asserts that an object is not undefined.
+ * @tparam String msg An optional error message.
+ * @tparam Object object The defined object.
+ * @exception AssertionFailedError Thrown if the object is undefined.
+ */
+function Assert_assertNotUndefined( msg, object )
+{
+    if( arguments.length == 1 )
+    {
+        object = msg;
+        msg = null;
+    }
+    if( object === undefined )
+        this.fail( "Object <" + object + "> was undefined."
+            , new CallStack(), msg );
+}
+/**
+ * Asserts that an object is null.
+ * @tparam String msg An optional error message.
+ * @tparam Object object The null object.
+ * @exception AssertionFailedError Thrown if the object is not null.
+ */
+function Assert_assertNull( msg, object )
+{
+    if( arguments.length == 1 )
+    {
+        object = msg;
+        msg = null;
+    }
+    if( object !== null )
+        this.fail( "Object <" + object + "> was not null."
+            , new CallStack(), msg );
+}
+/**
+ * Asserts that two values are the same.
+ * @tparam String msg An optional error message.
+ * @tparam Object expected The expected value.
+ * @tparam Object actual The actual value.
+ * @exception AssertionFailedError Thrown if the expected value is not the 
+ * actual one.
+ */
+function Assert_assertSame( msg, expected, actual )
+{
+    if( arguments.length == 2 )
+    {
+        actual = expected;
+        expected = msg;
+        msg = null;
+    }
+    if( expected === actual )
+        return;
+    else
+        this.fail( "Same expected:<" + expected + ">, but was:<" + actual + ">"
+            , new CallStack(), msg );
+}
+/**
+ * Asserts that a condition is true.
+ * @tparam String msg An optional error message.
+ * @tparam String cond The condition to evaluate.
+ * @exception AssertionFailedError Thrown if the evaluation was not true.
+ */
+function Assert_assertTrue( msg, cond )
+{
+    if( arguments.length == 1 )
+    {
+        cond = msg;
+        msg = null;
+    }
+    if( !eval( cond ))
+        this.fail( "Condition failed \"" + cond + "\"", new CallStack(), msg );
+}
+/**
+ * Asserts that an object is undefined.
+ * @tparam String msg An optional error message.
+ * @tparam Object object The undefined object.
+ * @exception AssertionFailedError Thrown if the object is not undefined.
+ */
+function Assert_assertUndefined( msg, object )
+{
+    if( arguments.length == 1 )
+    {
+        object = msg;
+        msg = null;
+    }
+    if( object !== undefined )
+        this.fail( "Object <" + object + "> was not undefined."
+            , new CallStack(), msg );
+}
+/**
+ * Fails a test with a give message.
+ * @tparam String msg The error message.
+ * @tparam CallStack stack The call stack of the error.
+ * @tparam String usermsg The message part of the user.
+ * @exception AssertionFailedError Is always thrown.
+ */
+function Assert_fail( msg, stack, usermsg )
+{
+    var afe = new AssertionFailedError(
+        ( usermsg ? usermsg + " " : "" ) + msg, stack );
+    throw afe;
+}
+Assert.glue();
+
+
+/**
+ * A test case defines the fixture to run multiple tests. 
+ * To define a test case
+ * -# implement a subclass of TestCase
+ * -# define instance variables that store the state of the fixture
+ * -# initialize the fixture state by overriding <code>setUp</code>
+ * -# clean-up after a test by overriding <code>tearDown</code>.
+ * Each test runs in its own fixture so there can be no side effects among 
+ * test runs.
+ *
+ * For each test implement a method which interacts
+ * with the fixture. Verify the expected results with assertions specified
+ * by calling <code>assertTrue</code> with a boolean or one of the other assert 
+ * functions.
+ *
+ * Once the methods are defined you can run them. The framework supports
+ * both a static and more generic way to run a test.
+ * In the static way you override the runTest method and define the method to
+ * be invoked.
+ * The generic way uses the JavaScript functionality to enumerate a function's
+ * methods to implement <code>runTest</code>. In this case the name of the case
+ * has to correspond to the test method to be run.
+ *
+ * The tests to be run can be collected into a TestSuite. JsUnit provides
+ * several <i>test runners</i> which can run a test suite and collect the
+ * results.
+ * A test runner expects a function <code><i>FileName</i>Suite</code> as the 
+ * entry point to get a test to run.
+ *
+ * @see TestResult
+ * @see TestSuite
+ * @ctor
+ * Constructs a test case with the given name.
+ * @tparam String name The name of the test case.
+ */
+function TestCase( name )
+{
+    Assert.call( this );
+    this.mName = name;
+}
+/**
+ * Counts the number of test cases that will be run by this test.
+ * @treturn Number Returns 1.
+ */
+function TestCase_countTestCases() { return 1; }
+/**
+ * Creates a default TestResult object.
+ * @treturn TestResult Returns the new object.
+ */
+function TestCase_createResult() { return new TestResult(); }
+/**
+ * Find a test by name.
+ * @note This is an enhancement to JUnit 3.8
+ * @tparam String testName The name of the searched test.
+ * @treturn Test Returns this if the test's name matches or null.
+ */
+function TestCase_findTest( testName ) 
+{ 
+    return testName == this.mName ? this : null; 
+}
+/**
+ * Retrieves the name of the test.
+ * @treturn String The name of test cases.
+ */
+function TestCase_getName() { return this.mName; }
+/**
+ * Runs a test and collects its result in a TestResult instance.
+ * The function can be called with or without argument. If no argument is
+ * given, the function will create a default result set and return it.
+ * Otherwise the return value can be omitted.
+ * @tparam TestResult result The test result to fill.
+ * @treturn TestResult Returns the test result.
+ */
+function TestCase_run( result )
+{
+    if( !result )
+        result = this.createResult();
+    result.run( this );
+    return result;
+}
+/**
+ * \internal
+ */
+function TestCase_runBare()
+{
+    this.setUp();
+    try
+    {
+        this.runTest();
+        this.tearDown();
+    }
+    catch( ex )
+    {
+        this.tearDown();
+        throw ex;
+    }
+}
+/**
+ * Override to run the test and assert its state.
+ */
+function TestCase_runTest()
+{
+    var method = this.getName();
+    this.assertNotNull( method );
+    method = method.substring( method.lastIndexOf( "." ) + 1 );
+    method = this[method];
+    if( method )
+        method.call( this );
+    else
+        this.fail( "Method '" + this.getName() + "' not found!" );
+}
+/**
+ * Sets the name of the test case.
+ * @tparam String name The new name of test cases.
+ */
+function TestCase_setName( name ) { this.mName = name; }
+/**
+ * Retrieve the test case as string.
+ * @treturn String Returns the name of the test case.
+ */
+function TestCase_toString() 
+{ 
+    /*
+    var className = new String( this.constructor ); 
+    var regex = /function (\w+)/;
+    regex.exec( className );
+    className = new String( RegExp.$1 );
+    */
+    return this.mName; // + "(" + className + ")"; 
+}
+TestCase.prototype = new Assert();
+TestCase.glue();
+TestCase.fulfills( Test );
+/**
+ * Set up the environment of the fixture.
+ */
+TestCase.prototype.setUp = function() {};
+/**
+ * Clear up the environment of the fixture.
+ */
+TestCase.prototype.tearDown = function() {};
+
+
+/**
+ * A TestSuite is a composition of Tests.
+ * It runs a collection of test cases.
+ * In despite of the JUnit implementation, this class has also functionality of
+ * TestSetup of the extended JUnit framework. This is because of &quot;recursion
+ * limits&quot; of the JavaScript implementation of BroadVision's One-to-one
+ * Server (an OEM version of Netscape Enterprise Edition).
+ * @see Test
+ * @ctor
+ * Constructor.
+ * The constructor collects all test methods of the given object and adds them
+ * to the array of tests.
+ * @tparam Object obj if obj is an instance of a TestCase, the suite is filled 
+ * with the fixtures automatically. Otherwise obj's string value is treated as 
+ * name.
+ */
+function TestSuite( obj )
+{
+    this.mTests = new Array();
+
+    var name, str;
+    switch( typeof obj )
+    {
+        case "function":
+            if( !str )
+                str = new String( obj );
+            name = str.substring( str.indexOf( " " ) + 1, str.indexOf( "(" ));
+            if( name == "" )
+                name = "[anonymous]";
+            break;
+        case "string": name = obj; break;
+        case "object": 
+            if( obj !== null )
+            {
+                if( obj.getName && typeof( obj.getName ) == "function" )
+                {
+                    var tname = obj.getName();
+                    if( tname )
+                    {
+                        var idx = tname.indexOf( "." );
+                        if( idx == tname.lastIndexOf( "." ))
+                            obj = eval( name = tname.substring( 0, idx ));
+                    }
+                }
+                if( typeof( obj ) != "function" )
+                    this.addTest( 
+                        this.warning( "Cannot instantiate test class for " 
+                            + "object '" + obj + "'" ));
+            }
+            // fall through
+        case "undefined":   
+            // fall through
+        default: 
+            if( typeof( name ) == "undefined" )
+                name = null; 
+            break;
+    }
+
+    this.setName( name );
+
+    // collect all testXXX methods
+    if( typeof( obj ) == "function" && obj.prototype )
+    {
+        for( var member in obj.prototype )
+        {
+            if(    member.indexOf( "test" ) == 0 
+                && typeof( obj.prototype[member] ) == "function" )
+            {
+                this.addTest( new ( obj )( member ));
+            }
+        }
+    }
+}
+/**
+ * Add a test to the suite.
+ * @tparam Test test The test to add.
+ * The test suite will add the given \a test to the suite and prepends the
+ * name of a TestCase with the name of the suite.
+ */
+function TestSuite_addTest( test ) 
+{ 
+    if( test instanceof TestCase )
+    {
+        var name = test.getName();
+        test.setName( this.getName() + "." + name );
+    }
+    this.mTests.push( test ); 
+}
+/**
+ * Add a test suite to the current suite.
+ * All fixtures of the test case will be collected in a suite which
+ * will be added.
+ * @tparam TestCase testCase The TestCase object to add.
+ */
+function TestSuite_addTestSuite( testCase ) 
+{ 
+    this.addTest( new TestSuite( testCase )); 
+}
+/**
+ * Counts the number of test cases that will be run by this test suite.
+ * @treturn Number The number of test cases.
+ */
+function TestSuite_countTestCases()
+{
+    var tests = 0;
+    for( var i = 0; i < this.testCount(); ++i )
+        tests += this.mTests[i].countTestCases();
+    return tests;
+}
+/**
+ * Search a test by name.
+ * @note This is an enhancement to JUnit 3.8
+ * The function compares the given name with the name of the test and 
+ * returns its own instance if the name is equal.
+ * @tparam String name The name of the searched test.
+ * @treturn Test The instance itself or null.
+ */
+function TestSuite_findTest( name )
+{
+    if( name == this.mName )
+        return this;
+
+    for( var i = 0; i < this.testCount(); ++i )
+    {
+        var test = this.mTests[i].findTest( name );
+        if( test != null )
+            return test;
+    }
+    return null;
+}
+/**
+ * Retrieves the name of the test suite.
+ * @treturn String The name of test suite.
+ */
+function TestSuite_getName() { return this.mName ? this.mName : ""; }
+/**
+ * Runs the tests and collects their result in a TestResult instance.
+ * @note As an enhancement to JUnit 3.8 the method calls also startTest
+ * and endTest of the TestResult.
+ * @tparam TestResult result The test result to fill.
+ */
+function TestSuite_run( result )
+{
+    --result.mRunTests;
+    result.startTest( this );
+
+    for( var i = 0; i < this.testCount(); ++i )
+    {
+        if( result.shouldStop())
+            break;
+        var test = this.mTests[i];
+        this.runTest( test, result );
+    }
+
+    if( i == 0 )
+    {
+        var ex = new AssertionFailedError( 
+            "Test suite with no tests.", new CallStack());
+        result.addFailure( this, ex );
+    }
+
+    result.endTest( this );
+}
+/**
+ * Runs a single test test and collect its result in a TestResult instance.
+ * @tparam Test test The test to run.
+ * @tparam TestResult result The test result to fill.
+ */
+function TestSuite_runTest( test, result )
+{
+    test.run( result );
+}
+/**
+ * Sets the name of the suite.
+ * @tparam String name The name to set.
+ */
+function TestSuite_setName( name )
+{ 
+    this.mName = name;
+}
+/**
+ * Runs the test at the given index.
+ * @tparam Number index The index.
+ * @type Test
+ */
+function TestSuite_testAt( index )
+{
+    return this.mTests[index];
+}
+/**
+ * Returns the number of tests in this suite.
+ * @type Number
+ */
+function TestSuite_testCount() { return this.mTests.length; }
+/**
+ * Retrieve the test suite as string.
+ * @treturn String Returns the name of the test case.
+ */
+function TestSuite_toString() 
+{ 
+    return "Suite '" + this.mName + "'";
+}
+/**
+ * Returns a test which will fail and log a warning message.
+ * @tparam String message The warning message.
+ * @type Test
+ */
+function TestSuite_warning( message )
+{
+    function Warning() { TestCase.call( this, "warning" ); }
+    Warning.prototype = new TestCase();
+    Warning.prototype.runTest = function() { this.fail( this.mMessage ); }
+    Warning.prototype.mMessage = message;
+
+    return new Warning();
+}
+TestSuite.glue();
+TestSuite.fulfills( Test );
+
+
+// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+// JUnit extension classes
+// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+/**
+ * A Decorator for Tests. Use TestDecorator as the base class
+ * for defining new test decorators. Test decorator subclasses
+ * can be introduced to add behavior before or after a test
+ * is run.
+ * @see Test
+ * @ctor
+ * Constructor.
+ * The constructor saves the test.
+ * @tparam Test test The test to decorate.
+ */
+function TestDecorator( test )
+{
+    Assert.call( this );
+    this.mTest = test;
+}
+/**
+ * The basic run behavior. The function calls the run method of the decorated
+ * test.
+ * @tparam TestResult result The test result.
+ */
+function TestDecorator_basicRun( result ) { this.mTest.run( result ); }
+/**
+ * Returns the number of the test cases.
+ * @type Number.
+ */
+function TestDecorator_countTestCases() { return this.mTest.countTestCases(); }
+/** 
+ * Returns the test if it matches the name. 
+ * @tparam String name The searched test name.
+ * @type Test
+ */
+function TestDecorator_findTest( name ) { return this.mTest.findTest( name ); }
+/** 
+ * Returns name of the test.
+ * @note This is an enhancement to JUnit 3.8
+ * @type String
+ */
+function TestDecorator_getName() { return this.mTest.getName(); }
+/** 
+ * Returns name the decorated test.
+ * @note This is an enhancement to JUnit 3.8
+ * @type Test
+ */
+function TestDecorator_getTest() { return this.mTest; }
+/**
+ * Run the test.
+ * @tparam TestResult result The test result.
+ */
+function TestDecorator_run( result ) { this.basicRun( result ); }
+/** 
+ * Sets name of the test.
+ * @tparam String name The new name of the test.
+ */
+function TestDecorator_setName( name ) { this.mTest.setName( name ); }
+/** 
+ * Returns the test as string. 
+ * @note This is an enhancement to JUnit 3.8
+ * @type String
+ */
+function TestDecorator_toString() { return this.mTest.toString(); }
+TestDecorator.prototype = new Assert();
+TestDecorator.glue();
+TestDecorator.fulfills( Test );
+
+
+/**
+ * A Decorator to set up and tear down additional fixture state.
+ * Subclass TestSetup and insert it into your tests when you want
+ * to set up additional state once before the tests are run.
+ * @see TestCase
+ * @ctor
+ * Constructor.
+ * The constructor saves the test.
+ * @tparam Test test The test to decorate.
+ */
+function TestSetup( test )
+{
+    TestDecorator.call( this, test );
+}
+/**
+ * Runs a test case with additional set up and tear down.
+ * @tparam TestResult result The result set.
+ */
+function TestSetup_run( result )
+{
+    function OnTheFly() {}
+    OnTheFly.prototype.protect = function()
+    {   
+        this.mTestSetup.setUp();
+        this.mTestSetup.basicRun( this.result );
+        this.mTestSetup.tearDown();
+    }
+    OnTheFly.prototype.result = result;
+    OnTheFly.prototype.mTestSetup = this;
+    OnTheFly.fulfills( Protectable );
+    
+    result.runProtected( this.mTest, new OnTheFly());
+}
+TestSetup.prototype = new TestDecorator();
+TestSetup.glue();
+/**
+ * Sets up the fixture. Override to set up additional fixture
+ * state.
+ */
+TestSetup.prototype.setUp = function() {}
+/**
+ * Tears down the fixture. Override to tear down the additional
+ * fixture state.
+ */
+TestSetup.prototype.tearDown = function() {}
+
+
+/**
+ * A Decorator that runs a test repeatedly.
+ * @ctor
+ * Constructor.
+ * @tparam Test test The test to repeat.
+ * @tparam Number repeat The number of repeats.
+ */
+function RepeatedTest( test, repeat )
+{
+    TestDecorator.call( this, test );
+    this.mTimesRepeat = repeat;
+}
+function RepeatedTest_countTestCases()
+{
+    var tests = TestDecorator.prototype.countTestCases.call( this );
+    return tests * this.mTimesRepeat;
+}
+/**
+ * Runs a test case with additional set up and tear down.
+ * @tparam TestResult result The result set.
+ */
+function RepeatedTest_run( result )
+{
+    for( var i = 0; i < this.mTimesRepeat; i++ )
+    {
+        if( result.shouldStop())
+            break;
+        TestDecorator.prototype.run.call( this, result );
+    }
+}
+function RepeatedTest_toString()
+{
+    return TestDecorator.prototype.toString.call( this ) + " (repeated)";
+}
+RepeatedTest.prototype = new TestDecorator();
+RepeatedTest.glue();
+
+
+/**
+ * A TestCase that expects an exception of class mClass to be thrown.
+ * The other way to check that an expected exception is thrown is:
+ * <pre>
+ * try {
+ *   this.shouldThrow();
+ * }
+ * catch (ex) {
+ *   if (ex instanceof SpecialException)
+ *      return;
+ *   else
+ *      throw ex;
+ * }
+ * this.fail("Expected SpecialException");
+ * </pre>
+ *
+ * To use ExceptionTestCase, create a TestCase like:
+ * <pre>
+ * new ExceptionTestCase("testShouldThrow", SpecialException);
+ * </pre>
+ * @ctor
+ * Constructor.
+ * The constructor is initialized with the name of the test and the expected
+ * class to be thrown.
+ * @tparam String name The name of the test case.
+ * @tparam Function clazz The class to be thrown.
+ */
+function ExceptionTestCase( name, clazz )
+{
+    TestCase.call( this, name )
+    /**
+     * Save the class.
+     * @type Function
+     */
+    this.mClass = clazz;
+}
+/**
+ * Execute the test method expecting that an exception of
+ * class mClass or one of its subclasses will be thrown
+ */
+function ExceptionTestCase_runTest()
+{
+    try
+    {
+        TestCase.prototype.runTest.call( this );
+    }
+    catch( ex )
+    {
+        if(    ex instanceof this.mClass 
+            && !( ex instanceof AssertionFailedError ))
+            return;
+        else
+            throw ex;
+    }
+    this.fail( "Expected exception " + this.mClass.toString());
+}
+ExceptionTestCase.prototype = new TestCase();
+ExceptionTestCase.glue();
+
+
+// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+// JUnit runner classes
+// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+/**
+ * A listener interface for observing the execution of a test run.
+ * @note This class is an &quot;initial version&quot; in JUnit 3.8.1
+ * and might be replace TestListener some day.
+ */
+function TestRunListener()
+{
+}
+/**
+ * Status for an error.
+ * @type Number
+ */
+TestRunListener.prototype.STATUS_ERROR = 1;
+/**
+ * Status for a failure.
+ * @type Number
+ */
+TestRunListener.prototype.STATUS_FAILURE = 2;
+/**
+ * A test run was started.
+ * @tparam String suiteName The name of the test suite.
+ * @tparam Number testCount The number of tests in the suite.
+ */
+TestRunListener.prototype.testRunStarted = function( suiteName, testCount ) {}
+/**
+ * A test run was ended.
+ * @tparam Number elapsedTime The number of elapsed milliseconds.
+ */
+TestRunListener.prototype.testRunEnded = function( elapsedTime ) {}
+/**
+ * A test run was stopped.
+ * @tparam Number elapsedTime The number of elapsed milliseconds.
+ */
+TestRunListener.prototype.testRunStopped = function( elapsedTime ) {}
+/**
+ * A test started.
+ * @tparam String testName The name of the started test.
+ */
+TestRunListener.prototype.testStarted = function( testName ) {}
+/**
+ * A test ended.
+ * @tparam String testName The name of the ended test.
+ */
+TestRunListener.prototype.testEnded = function( testName ) {}
+/**
+ * A test failed.
+ * @tparam Number status The status of the test.
+ * @tparam String testName The name of the failed test.
+ * @tparam String trace The stack trace as String.
+ */
+TestRunListener.prototype.testFailed = function( status, testName, trace ) {}
+
+
+/**
+ * General base class for an application running test suites.
+ */
+function BaseTestRunner()
+{
+    this.mElapsedTime = 0;
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that had an error.
+ * @tparam Error except The thrown error.
+ */
+function BaseTestRunner_addError( test, except ) 
+{ 
+    this.testFailed( TestRunListener.prototype.STATUS_ERROR, 
+        test.toString(), except.toString()); 
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that had a failure.
+ * @tparam AssertionFailedError afe The thrown failure.
+ */
+function BaseTestRunner_addFailure( test, afe ) 
+{ 
+    this.testFailed( TestRunListener.prototype.STATUS_ERROR, 
+        test.toString(), afe.toString()); 
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The ended test.
+ */
+function BaseTestRunner_endTest( test ) 
+{ 
+    this.testEnded( test.toString()); 
+}
+/**
+ * Retrieve the value of a global preference key.
+ * @tparam String key The key of the preference.
+ * @tparam Object value The default value.
+ * @treturn Object The value of the key or the default value.
+ */
+function BaseTestRunner_getPreference( key, value ) 
+{ 
+    var v = BaseTestRunner.prototype.getPreferences()[key];
+    if( !v )
+        v = value;
+    return v;
+}
+/**
+ * Retrieves the Object with the global preferences of any runner.
+ * @treturn Object Returns the runner's global preferences.
+ */
+function BaseTestRunner_getPreferences() 
+{ 
+    return BaseTestRunner.prototype.mPreferences; 
+}
+/**
+ * Returns the Test corresponding to the given suite.
+ * @tparam String name The name of the test.
+ * This is a template method, subclasses override runFailed(), 
+ * clearStatus().
+ */
+function BaseTestRunner_getTest( name ) 
+{ 
+    if( typeof( name ) != "string" )
+    {
+        this.clearStatus();
+        return null;
+    }
+    var test;
+    try
+    {
+        var testFunc = eval( name );
+        if( typeof( testFunc ) == "function" && testFunc.prototype )
+        {
+            if( testFunc.prototype.suite )
+                test = testFunc.prototype.suite();
+            else if(   name.match( /Test$/ ) 
+                     && testFunc.prototype instanceof TestCase )
+            {
+                test = new TestSuite( testFunc );
+            }
+        }
+    }
+    catch( ex )
+    {
+    }
+    if( test === undefined || !( test instanceof TestSuite ))
+    {
+        this.runFailed( "Test not found \"" + name + "\"" );
+        return null;
+    }
+    else
+    {
+        this.clearStatus();
+        return test;
+    }
+}
+/**
+ * Set a global preference.
+ * @tparam String key The key of the preference.
+ * @tparam Object value The value of the preference.
+ */
+function BaseTestRunner_setPreference( key, value ) 
+{ 
+    BaseTestRunner.prototype.getPreferences()[key] = value;
+}
+/**
+ * Set any runner's global preferences.
+ * @tparam Array prefs The new preferences.
+ */
+function BaseTestRunner_setPreferences( prefs ) 
+{ 
+    BaseTestRunner.prototype.mPreferences = prefs;
+}
+/**
+ * Retrieve the flag for raw stack output.
+ * @treturn Boolean Flag for an unfiltered stack output.
+ */
+function BaseTestRunner_showStackRaw() 
+{ 
+    return BaseTestRunner.prototype.getPreference( "filterStack", false ) != true;
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The started test.
+ */
+function BaseTestRunner_startTest( test ) 
+{ 
+    this.testStarted( test.toString()); 
+}
+/**
+ * Truncates string to maximum length.
+ * @tparam String str The string to truncate.
+ * @treturn String The truncated string.
+ */
+function BaseTestRunner_truncate( str ) 
+{
+    var max = BaseTestRunner.prototype.getPreference( "maxMessageLength" );
+    if( max < str.length )
+        str = str.substring( 0, max ) + "...";
+    return str; 
+}
+BaseTestRunner.glue();
+BaseTestRunner.prototype.mPreferences = new Object();
+BaseTestRunner.prototype.clearStatus = function() {}
+BaseTestRunner.prototype.runFailed = function( msg ) {}
+BaseTestRunner.prototype.testEnded = function( test ) {}
+BaseTestRunner.prototype.testFailed = function( test ) {}
+BaseTestRunner.prototype.testStarted = function( test ) {}
+BaseTestRunner.fulfills( TestListener );
+BaseTestRunner.prototype.setPreference( "filterStack", true );
+BaseTestRunner.prototype.setPreference( "maxMessageLength", 500 );
+
+
+/**
+ * TestRunner of JsUnit 1.1
+ * @see TextTestRunner
+ * @deprecated since 1.2 in favor of TextTestRunner
+ */
+function TestRunner()
+{
+    BaseTestRunner.call( this );
+    this.mSuites = new TestSuite();
+}
+/**
+ * Add a test suite to the application.
+ * @tparam TestSuite suite The suite to add.
+ */
+function TestRunner_addSuite( suite ) 
+{ 
+    this.mSuites.addTest( suite ); 
+}
+/**
+ * Counts the number of test cases that will be run by this test 
+ * application.
+ * @treturn Number The number of test cases.
+ */
+function TestRunner_countTestCases() 
+{ 
+    return this.mSuites.countTestCases(); 
+}
+/**
+ * Runs all test of all suites and collects their results in a TestResult 
+ * instance.
+ * @tparam String name The name of the test.
+ * @tparam TestResult result The test result to fill.
+ */
+function TestRunner_run( name, result )
+{
+    var test = this.mSuites.findTest( name );
+    if( test == null )
+    {
+        var ex = new AssertionFailedError( 
+            "Test \"" + name + "\" not found.", new CallStack());
+        result.addFailure( new Test( name ), ex );
+    }
+    else
+    {
+        this.mElapsedTime = new Date();
+        test.run( result );
+        this.mElapsedTime = new Date() - this.mElapsedTime;
+    }
+}
+/**
+ * Runs all test of all suites and collects their results in a TestResult 
+ * instance.
+ * @tparam TestResult result The test result to fill.
+ */
+function TestRunner_runAll( result ) 
+{ 
+    this.mElapsedTime = new Date();
+    this.mSuites.run( result ); 
+    this.mElapsedTime = new Date() - this.mElapsedTime;
+}
+TestRunner.prototype = new BaseTestRunner();
+TestRunner.glue();
+
+
+/**
+ * Class to print the result of a TextTestRunner.
+ * @ctor
+ * Constructor.
+ * @tparam PrinterWriter writer The writer for the report.
+ * Initialization of the ResultPrinter. If no \a writer is provided the 
+ * instance uses the SystemWriter.
+ */
+function ResultPrinter( writer )
+{
+    this.setWriter( writer );
+    this.mColumn = 0;
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that had an error.
+ * @tparam Error except The thrown error.
+ */
+function ResultPrinter_addError( test, except )
+{
+    this.getWriter().print( "E" );
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that had a failure.
+ * @tparam AssertionFailedError afe The thrown failure.
+ */
+function ResultPrinter_addFailure( test, afe )
+{
+    this.getWriter().print( "F" );
+}
+/**
+ * Returns the elapsed time in seconds as String.
+ * @tparam Number runTime The elapsed time in ms.
+ * @type String
+ */
+function ResultPrinter_elapsedTimeAsString( runTime )
+{
+    return new String( runTime / 1000 );
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that ends.
+ */
+function ResultPrinter_endTest( test )
+{
+}
+/**
+ * Returns the associated writer to this instance.
+ * @type PrinterWriter
+ */
+function ResultPrinter_getWriter()
+{
+    return this.mWriter;
+}
+/**
+ * Print the complete test result.
+ * @tparam TestResult result The complete test result.
+ * @tparam Number runTime The elapsed time in ms.
+ */
+function ResultPrinter_print( result, runTime )
+{
+    this.printHeader( runTime );
+    this.printErrors( result );
+    this.printFailures( result );
+    this.printFooter( result );
+}
+/**
+ * Print a defect of the test result.
+ * @tparam TestFailure defect The defect to print.
+ * @tparam Number count The counter for this defect type.
+ */
+function ResultPrinter_printDefect( defect, count )
+{
+    this.printDefectHeader( defect, count );
+    this.printDefectTrace( defect );
+    this.getWriter().println();
+}
+/**
+ * \internal
+ */
+function ResultPrinter_printDefectHeader( defect, count )
+{
+    this.getWriter().print( count + ") " + defect.toString());
+}
+/**
+ * Print the defects of a special type of the test result.
+ * @tparam Array<TestFailure> array The array with the defects.
+ * @tparam String type The type of the defects.
+ */
+function ResultPrinter_printDefects( array, type )
+{
+    if( array.length == 0 )
+        return;
+    if( array.length == 1 )
+        this.getWriter().println( "There was 1 " + type + ":" );
+    else
+        this.getWriter().println( 
+            "There were " + array.length + " " + type + "s:" );
+    for( var i = 0; i < array.length; )
+        this.printDefect( array[i], ++i );
+}
+/**
+ * \internal
+ */
+function ResultPrinter_printDefectTrace( defect )
+{
+    var trace = defect.trace();
+    if( trace )
+    {
+        this.getWriter().println();
+        this.getWriter().println( trace );
+    }
+}
+/**
+ * Print the errors of the test result.
+ * @tparam TestResult result The complete test result.
+ */
+function ResultPrinter_printErrors( result )
+{
+    this.printDefects( result.mErrors, "error" );
+}
+/**
+ * Print the failures of the test result.
+ * @tparam TestResult result The complete test result.
+ */
+function ResultPrinter_printFailures( result )
+{
+    this.printDefects( result.mFailures, "failure" );
+}
+/**
+ * Print the footer of the test result.
+ * @tparam TestResult result The complete test result.
+ */
+function ResultPrinter_printFooter( result )
+{
+    var writer = this.getWriter();
+    if( result.wasSuccessful())
+    {
+        var count = result.runCount();
+        writer.println();
+        writer.print( "OK" );
+        writer.println( 
+            " (" + count + " test" + ( count == 1 ? "" : "s" ) + ")" );
+    }
+    else
+    {
+        writer.println();
+        writer.println( "FAILURES!!!" );
+        writer.println( "Tests run: " + result.runCount()
+            + ", Failures: " + result.failureCount()
+            + ", Errors: " + result.errorCount());
+    }
+    writer.println();
+}
+/**
+ * Print the header of the test result.
+ * @tparam Number runTime The elapsed time in ms.
+ */
+function ResultPrinter_printHeader( runTime )
+{
+    var writer = this.getWriter();
+    writer.println();
+    writer.println( "Time: " + this.elapsedTimeAsString( runTime ));
+}
+/**
+ * Sets the PrinterWriter.
+ * @note This is an enhancement to JUnit 3.8
+ * @tparam PrinterWriter writer The writer for the report.
+ * Initialization of the ResultPrinter. If no \a writer is provided the 
+ * instance uses the SystemWriter.
+ */
+function ResultPrinter_setWriter( writer )
+{
+    this.mWriter = writer ? writer : JsUtil.prototype.getSystemWriter();
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that starts.
+ */
+function ResultPrinter_startTest( test )
+{
+    if( !( test instanceof TestSuite ))
+    {
+        this.getWriter().print( "." );
+        if( this.mColumn++ >= 39 )
+        {
+            this.getWriter().println();
+            this.mColumn = 0;
+        }
+    }
+}
+ResultPrinter.glue();
+ResultPrinter.fulfills( TestListener );
+
+
+/**
+ * Class for an application running test suites with a test based status 
+ * report.
+ * @ctor
+ * The constructor.
+ * @tparam Object outdev Output device
+ * The TestRunner is initialized with the given output device. This may be an
+ * instance of a ResultPrinter, a PrinterWriter or undefined. For a 
+ * PrinterWriter the constructor creates a new instance of a standard 
+ * ResultPrinter with this PrinterWriter. If \a outdev is undefined it creates
+ * a ResultPrinter with the SystemWriter.
+ */
+function TextTestRunner( outdev )
+{
+    BaseTestRunner.call( this );
+    this.setPrinter( outdev );
+}
+/**
+ * Creates an instance of a TestResult to be used for the test run.
+ * @treturn TestResult Returns the new TestResult instance.
+ */
+function TextTestRunner_createTestResult() 
+{
+    return new TestResult(); 
+}
+/**
+ * Retrieve the currently used ResultPrinter.
+ * @treturn ResultPrinter Returns the ResultPrinter.
+ * @since 1.3
+ */
+function TextTestRunner_getPrinter()
+{
+    return this.mPrinter;
+}
+/**
+ * Executes a test run with the given test.
+ * @tparam Test test The test.
+ * @treturn TestResult The result of the test.
+ */
+function TextTestRunner_doRun( test ) 
+{
+    var result = this.createTestResult();
+    result.addListener( this.mPrinter );
+    var startTime = new Date();
+    test.run( result );
+    var endTime = new Date();
+    this.mPrinter.print( result, endTime - startTime );
+    return result;
+}
+/**
+ * Runs a single test or a suite extracted from a TestCase subclass.
+ * @tparam Object test The class to test or a test.
+ * This static method can be used to start a test run from your program.
+ * @treturn TestResult The result of the test.
+ */
+function TextTestRunner_run( test )
+{
+    if( test instanceof Function )
+        test = new TestSuite( test );
+    var runner = new TextTestRunner();
+    return runner.doRun( test );
+}
+/**
+ * Program entry point.
+ * @tparam Array<String> args Program arguments.
+ * The function will create a TextTestRunner or the TestRunner given by the
+ * preference "TestRunner" and run the tests given by the arguments. The 
+ * function will exit the program with an error code indicating the type of
+ * success.
+ */
+function TextTestRunner_main( args )
+{
+    var ex;
+    var runner = BaseTestRunner.prototype.getPreference( "TestRunner" );
+    if( runner )
+        runner = new runner();
+    if( !runner )
+         runner = new TextTestRunner();
+    try
+    {
+        var result = runner.start( args );
+        JsUtil.prototype.quit( 
+              result.wasSuccessful() 
+            ? TextTestRunner.prototype.SUCCESS_EXIT
+            : TextTestRunner.prototype.FAILURE_EXIT );
+    }
+    catch( ex )
+    {
+        runner.runFailed( ex.toString());
+    }
+}
+/**
+ * Run failed.
+ * @tparam String msg The failure message.
+ * @treturn Number TextTestRunner.FAILURE_EXIT.
+ */
+function TextTestRunner_runFailed( msg )
+{
+    JsUtil.prototype.getSystemWriter().println( msg );
+    JsUtil.prototype.quit( TextTestRunner.prototype.EXCEPTION_EXIT );
+}
+/**
+ * Set printer.
+ * @tparam Object outdev Output device
+ */
+function TextTestRunner_setPrinter( outdev )
+{
+    if( typeof( outdev ) == "object" )
+    {
+        if( outdev instanceof PrinterWriter )
+            outdev = new ResultPrinter( outdev );
+        if( !( outdev instanceof ResultPrinter ))
+            outdev = new ResultPrinter();
+    }
+    else
+        outdev = new ResultPrinter();
+
+    this.mPrinter = outdev;
+}
+/**
+ * Starts a test run.
+ * @tparam Object args The (optional) arguments as Array or String
+ * @type TestResult
+ * @exception Usage If an unknown option is used
+ * Analyzes the command line arguments and runs the given test suite. If
+ * no argument was given, the function tries to run AllTests.suite().
+ */
+function TextTestRunner_start( args )
+{
+    function Usage( msg )
+    {
+        JsUnitError.call( this, 
+            "[JavaScript engine] [TestScript] TestName [TestName2]" + msg );
+    }
+    Usage.prototype = new JsUnitError();
+    Usage.prototype.name = "Usage";
+
+    var testCases = new Array();
+
+    if( typeof( args ) == "undefined" )
+        args = new Array();
+    else if( typeof( args ) == "string" )
+        args = args.split( /[ ,;]/ );
+
+    var optionsPossible = true;
+    var msg = "";
+    for( var i = 0; i < args.length; ++i )
+    {
+        args[i] = args[i].trim();
+        if( optionsPossible && args[i].match( /^-/ ))
+        {
+            if( args[i] == "--" )
+            {
+                optionsPossible = false;
+                continue;
+            }
+            switch( args[i] )
+            {
+                case "--xml" :
+                    this.setPrinter( 
+                        new XMLResultPrinter( 
+                            this.mPrinter.getWriter()));
+                    continue;
+                    
+                case "--classic" :
+                    this.setPrinter( 
+                        new ClassicResultPrinter( 
+                            this.mPrinter.getWriter()));
+                    continue;
+                    
+                case "--html" :
+                    this.mPrinter.setWriter(
+                        new HTMLWriterFilter(
+                            this.mPrinter.getWriter()));
+                    continue;
+                    
+                case "--run" :
+                    if( args[i + 1] )
+                    {
+                        var optArg = args[++i].trim();
+                        var collector;
+                        if( optArg == "TESTCASES" )
+                            collector = new TestCaseCollector( 
+                                JsUtil.prototype.global );
+                        else if( optArg == "TESTSUITES" )
+                            collector = new TestSuiteCollector( 
+                                JsUtil.prototype.global );
+                        else if( optArg == "ALLTESTS" )
+                            collector = new AllTestsCollector( 
+                                JsUtil.prototype.global );
+                        if( collector )
+                        {
+                            var collection = collector.collectTests();
+                            for( var test in collection)
+                            {
+                                var testName = this.getTest( collection[test] );
+                                if( testName )
+                                    testCases.push( testName );
+                            }
+                            continue;
+                        }
+                        else
+                            msg = "\nUnknown argument for option \"" + args[i - 1] + "\"";
+                    }
+                    else
+                    {
+                        msg = "\nMissing argument for option \"" + args[i] + "\"";
+                    }
+                    
+                default:
+                    if( msg.length == 0 )
+                    {
+                        msg = "\nUnknown option \"" + args[i] + "\"";
+                    }
+                case "-?" :
+                    throw new Usage( msg );
+            }
+        }
+        var testName = this.getTest( args[i] );
+        if( testName )
+            testCases.push( testName );
+    }
+
+    var test;
+    if( testCases.length == 0 )
+        test = this.getTest( "AllTests" );
+    else if( testCases.length > 1 )
+    {
+        test = new TestSuite( "Start" );
+        for( i = 0; i < testCases.length; ++i )
+            test.addTest( testCases[i] );
+    }
+    else
+        test = testCases[0];
+
+    if( test )
+        return this.doRun( test );
+    else
+        return new TestResult();
+}
+TextTestRunner.prototype = new BaseTestRunner();
+TextTestRunner.glue();
+/**
+ * Exit code, when all tests succeed
+ * @type Number
+ */
+TextTestRunner.prototype.SUCCESS_EXIT = 0;
+/**
+ * Exit code, when at least one test fails with a failure.
+ * @type Number
+ */
+TextTestRunner.prototype.FAILURE_EXIT = 1;
+/**
+ * Exit code, when at least one test fails with an error.
+ * @type Number
+ */
+TextTestRunner.prototype.EXCEPTION_EXIT = 2;
+
+
+/**
+ * The ResultPrinter of JsUnit 1.1. It prints for every test a single row
+ * and reports at the end of a test a small summary.
+ * @ctor
+ * Constructor.
+ * @tparam PrinterWriter writer The writer for the report.
+ * Initialization of the ClassicResultPrinter. If no \a writer is provided the 
+ * instance uses the SystemWriter.
+ */
+function ClassicResultPrinter( writer )
+{
+    ResultPrinter.call( this, writer );
+}
+/**
+ * An occurred error was added.
+ * @tparam Test test The failed test.
+ * @tparam Error except The thrown exception.
+ */
+function ClassicResultPrinter_addError( test, except )
+{
+    var str = "";
+    if( except.description )
+    {
+        if( except.name )
+            str = except.name + ": ";
+        str += except.description;
+    }
+    else
+        str = except;
+    this.writeLn( "ERROR in " + test + ": " + str );
+}
+/**
+ * An occurred failure was added.
+ * @tparam Test test The failed test.
+ * @tparam Error except The thrown exception.
+ */
+function ClassicResultPrinter_addFailure( test, except )
+{
+    this.writeLn( "FAILURE in " + test + ": " + except );
+    if( except.mCallStack )
+        this.writeLn( except.mCallStack.toString());
+}
+/**
+ * A test ended
+ * @tparam Test test The ended test.
+ */
+function ClassicResultPrinter_endTest( test )
+{
+    if( test instanceof TestSuite )
+    {
+        this.mNest = this.mNest.substr( 1 );
+        this.writeLn( 
+              "<" + this.mNest.replace( /-/g, "=" ) 
+            + " Completed test suite \"" + test.getName() + "\"" );
+    }
+}
+/**
+ * Print the complete test result.
+ * @tparam TestResult result The complete test result.
+ * @tparam Number runTime The elapsed time in ms.
+ * Overloaded, because only the footer is needed.
+ */
+function ClassicResultPrinter_print( result, runTime )
+{
+    this.printFooter( result, runTime );
+}
+/**
+ * Write a header starting the application.
+ * @tparam Test test The top level test.
+ */
+function ClassicResultPrinter_printHeader( test )
+{
+    this.mRunTests = 0;
+    this.mInReport = true;
+    this.mNest = "";
+    this.writeLn( 
+          "TestRunner (" + test.countTestCases() + " test cases available)" );
+}
+/**
+ * Write a footer at application end with a summary of the tests.
+ * @tparam TestResult result The result of the test run.
+ * @tparam Number runTime The elapsed time in ms.
+ */
+function ClassicResultPrinter_printFooter( result, runTime )
+{
+    if( result.wasSuccessful() == 0 )
+    {
+        var error = result.errorCount() != 1 ? " errors" : " error";
+        var failure = result.failureCount() != 1 ? " failures" : " failure";
+        this.writeLn( 
+              result.errorCount() + error + ", " 
+            + result.failureCount() + failure + "." );
+    }
+    else
+        this.writeLn( 
+              result.runCount() + " tests successful in " 
+            + this.elapsedTimeAsString( runTime ) + " seconds." );
+    this.mInReport = false;
+}
+/**
+ * A test started
+ * @tparam Test test The started test.
+ */
+function ClassicResultPrinter_startTest( test )
+{
+    if( !this.mInReport )
+        this.printHeader( test );
+    if( !( test instanceof TestSuite ))
+    {
+        ++this.mRunTests;
+        this.writeLn( 
+              this.mNest + " Running test " 
+            + this.mRunTests + ": \"" + test + "\"" );
+    }
+    else
+    {
+        this.writeLn( 
+              this.mNest.replace(/-/g, "=") + "> Starting test suite \"" 
+            + test.getName() + "\"" );
+        this.mNest += "-";
+    }
+}
+/**
+ * Write a line of text.
+ * @tparam String str The text to print on the line.
+ * The method of this object does effectively nothing. It must be 
+ * overloaded with a proper version, that knows how to print a line,
+ * if the script engine cannot be detected (yet).
+ */
+function ClassicResultPrinter_writeLn( str )
+{
+    this.getWriter().println( str );
+}
+ClassicResultPrinter.prototype = new ResultPrinter();
+ClassicResultPrinter.glue();
+
+
+/**
+ * Convert the result of a TextTestPrinter into XML to be used by JUnitReport.
+ * @ctor
+ * Constructor.
+ * @tparam PrinterWriter writer The writer for the report.
+ * Initialization of the XMLResultPrinter. If no \a writer is provided the 
+ * instance uses the SystemWriter.
+ * @since upcoming
+ */
+function XMLResultPrinter( writer )
+{
+    ResultPrinter.call( this, writer );
+    this.mTests = new Array();
+    this.mCurrentTest = null;
+    this.mSuite = null;
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that had an error.
+ * @tparam Error except The thrown error.
+ */
+function XMLResultPrinter_addError( test, except )
+{
+    this.mCurrentTest.mError = except;
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that had a failure.
+ * @tparam AssertionFailedError afe The thrown failure.
+ */
+function XMLResultPrinter_addFailure( test, afe )
+{
+    this.mCurrentTest.mFailure = afe;
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that ends.
+ */
+function XMLResultPrinter_endTest( test )
+{
+    if( this.mCurrentTest != null ) 
+    {
+        var endTime = new Date();
+        this.mCurrentTest.mTime = this.elapsedTimeAsString( 
+            endTime - this.mCurrentTest.mTime );
+        this.mTests.push( this.mCurrentTest );
+        this.mCurrentTest = null;
+    }
+}
+/**
+ * Print the complete test result as XML report to be used by JUnitReport.
+ * @tparam TestResult result The complete test result.
+ * @tparam Number runTime The elapsed time in ms.
+ */
+function XMLResultPrinter_print( result, runTime )
+{
+    var writer = this.getWriter();
+    writer.println( '<?xml version="1.0" encoding="ISO-8859-1" ?>' );
+    writer.print( '<testsuite errors="' );
+    writer.print( result.errorCount());
+    writer.print( '" failures="' );
+    writer.print( result.failureCount());
+    writer.print( '" name="' );
+    writer.print( this.mSuite );
+    writer.print( '" tests="' );
+    writer.print( result.runCount());
+    writer.print( '" time="' );
+    writer.print( this.elapsedTimeAsString( runTime ));
+    writer.println( '">' );
+    for( var i = 0; i < this.mTests.length; ++i )
+    {
+        var test = this.mTests[i];
+        writer.print( '    <testcase name="' );
+        writer.print( test.mName );
+        writer.print( '" time="' );
+        writer.print( test.mTime );
+        writer.print( '"' );
+        if( test.mError || test.mFailure )
+        {
+            writer.println( '>' );
+            writer.print( '        <' );
+            var defect;
+            var tag;
+            if( test.mError )
+            {
+                defect = test.mError;
+                tag = "error";
+            }
+            else
+            {
+                defect = test.mFailure;
+                tag = "failure";
+            }
+            writer.print( tag );
+            writer.print( ' message="' );
+            var htmlWriter = new HTMLWriterFilter();
+            htmlWriter.println( defect.toString());
+            var message = htmlWriter.getWriter().get().replace( /<br>/g, " " );
+            writer.print( message.trim());
+            writer.print( '" type=""' );
+            var trace = defect.mCallStack ? defect.mCallStack.toString() : null;
+            if( trace )
+            {
+                writer.print( '>' );
+                writer.print( trace );
+                writer.print( '</' );
+                writer.print( tag );
+            }
+            else
+                writer.print( '/' );
+            writer.println( '>' );
+            writer.print( '    </testcase' );
+        }
+        else
+            writer.print( '/' );
+        writer.println( '>' );
+    }
+    writer.println( '</testsuite>' );
+}
+/**
+ * Implementation of TestListener.
+ * @tparam Test test The test that starts.
+ */
+function XMLResultPrinter_startTest( test )
+{
+    if( this.mSuite == null )
+        this.mSuite = test.getName();
+    this.mCurrentTest = new Object();
+    this.mCurrentTest.mName = test.getName();
+    this.mCurrentTest.mTime = new Date();
+}
+XMLResultPrinter.prototype = new ResultPrinter();
+XMLResultPrinter.glue();
+XMLResultPrinter.prototype.printDefect = function() {}
+XMLResultPrinter.prototype.printDefectHeader = function() {}
+XMLResultPrinter.prototype.printDefects = function() {}
+XMLResultPrinter.prototype.printDefectTrace = function() {}
+XMLResultPrinter.prototype.printErrors = function() {}
+XMLResultPrinter.prototype.printFailures = function() {}
+XMLResultPrinter.prototype.printFooter = function() {}
+XMLResultPrinter.prototype.printHeader = function() {}
+
+
+/**
+ * Class for an application running test suites reporting in HTML.
+ * @see TextTestRunner
+ * @see HTMLWriterFilter
+ * @deprecated since 1.2 in favor of TextTestRunner in combination with a
+ * HTMLWriterFilter wrapping an arbitrary PrinterWriter.
+ */
+function HTMLTestRunner( outdev )
+{
+    TextTestRunner.call( this, outdev );
+}
+/**
+ * Set printer.
+ * @tparam Object outdev Output device
+ * @treturn Number TextTestRunner.FAILURE_EXIT.
+ * The function wraps the PrinterWriter of the new ResultPrinter with a 
+ * HTMLWriterFilter.
+ * @deprecated since 1.2
+ */
+function HTMLTestRunner_setPrinter( outdev )
+{
+    TextTestRunner.prototype.setPrinter.call( this, outdev );
+    this.mPrinter.setWriter( 
+        new HTMLWriterFilter( this.mPrinter.getWriter()));
+}
+HTMLTestRunner.prototype = new TextTestRunner();
+HTMLTestRunner.glue();
+
+
+/**
+ * A collector for Test classes.
+ * In contrast to JUnit this interface returns an Array and not an enumeration.
+ * @since upcoming
+ */
+function TestCollector() 
+{
+}
+/**
+ * Collect Test classes.
+ * @treturn Array Returns an Array with classes.
+ */
+TestCollector.prototype.collectTests = function() {}
+
+
+/**
+ * A collector for the AllTests class.
+ * @ctor
+ * Constructs an AllTestsCollector.
+ * @tparam Object scope The object defining the scope the \c AllTests class is
+ * searched for.
+ * @since upcoming
+ */
+function AllTestsCollector( scope ) 
+{
+    this.mScope = scope;
+}
+/**
+ * Collect Test class \a AllTests.
+ * @treturn Array Returns an Array with the class named \c AllTests.
+ */
+function AllTestsCollector_collectTests() 
+{
+    var tests = new Array();
+    var testFunc = this.mScope.AllTests;
+    if( typeof( testFunc ) == "function" && typeof( testFunc.prototype.suite ) == "function" ) 
+        tests.push( testFunc );
+    return tests;
+}
+AllTestsCollector.glue();
+AllTestsCollector.fulfills( TestCollector );
+
+
+/**
+ * A generic collector for all Test classes within a scope.
+ * @ctor
+ * Constructs a GenericTestCollector.
+ * @note This is an equivalent to the ClassPathTestCollector.
+ * @tparam Object scope The object defining the scope of the search.
+ * @tparam RegExp pattern The regular expression the function name must match.
+ * @tparam Function type The class type the function must be an instance of.
+ * @since upcoming
+ */
+function GenericTestCollector( scope, pattern, type ) 
+{
+    this.mScope = scope;
+    this.mPattern = pattern;
+    this.mType = type;
+}
+/**
+ * Collect the Test classes.
+ * @treturn Array Returns an Array with the found Test classes.
+ */
+function GenericTestCollector_collectTests() 
+{
+    var tests = new Array();
+    for( var testName in this.mScope ) 
+        if( testName.match( this.mPattern ))
+        {
+            var testFunc = this.mScope[testName];
+            if(    typeof( testFunc ) == "function" 
+                && testFunc.prototype 
+                && this.isTest( testFunc )) 
+            {
+                tests.push( testName );
+            }
+        }
+    return tests;
+}
+/**
+ * Test the function to be collected.
+ * The method tests the \a testFunc for the class \a type given in the 
+ * constructor. A derived implementation may add additional criteria like the
+ * existence of a specific method of the class type.
+ * @tparam Function testFunc The Function to be tested.
+ * @treturn Boolean Returns \c true if the function is a Test.
+ */
+function GenericTestCollector_isTest( testFunc ) 
+{
+    return testFunc.prototype instanceof this.mType;
+}
+GenericTestCollector.glue();
+GenericTestCollector.fulfills( TestCollector );
+
+
+/**
+ * A TestCase collector for all test cases within a scope.
+ * @ctor
+ * Constructs a TestCaseCollector.
+ * @tparam Object scope The object defining the scope of the search.
+ * @tparam RegExp pattern The regular expression the function name must match.
+ * Defaults to <code>/.+Test$/</code>
+ * @since upcoming
+ */
+function TestCaseCollector( scope, pattern ) {
+    GenericTestCollector.call( 
+        this, 
+        scope, 
+        typeof( pattern ) == "undefined" ? /.+Test$/ : pattern, 
+        TestCase );
+}
+TestCaseCollector.prototype = new GenericTestCollector();
+
+
+/**
+ * A TestSuite collector for all test suites within a scope.
+ * @ctor
+ * Constructs a TestSuiteCollector.
+ * @tparam Object scope The object defining the scope of the search.
+ * @tparam RegExp pattern The regular expression the function name must match.
+ * Defaults to <code>/.+TestSuite$/</code>
+ * @since upcoming
+ */
+function TestSuiteCollector( scope, pattern ) {
+    GenericTestCollector.call( 
+        this, 
+        scope, 
+        typeof( pattern ) == "undefined" ? /.+TestSuite$/ : pattern, 
+        TestSuite );
+}
+TestSuiteCollector.prototype = new GenericTestCollector();
+
+
+/**
+ * Class for an embeddable text-oriented TestRunner used in other applications
+ * with a status report.
+ * @ctor
+ * The constructor.
+ * @tparam ResultPrinter printer The ResultPrinter used to present the test 
+ * results.
+ */
+function EmbeddedTextTestRunner( printer )
+{
+    BaseTestRunner.call( this );
+    this.setPrinter( printer );
+}
+/**
+ * Creates an instance of a TestResult to be used for the test run.
+ * @treturn TestResult Returns the new TestResult instance.
+ */
+function EmbeddedTextTestRunner_createTestResult() 
+{
+    return new TestResult(); 
+}
+/**
+ * Executes the given tests in the array.
+ * @tparam Array<String> testNames The name of the tests to execute.
+ * @tparam String suiteName The name of the generated TestSuite (may be undefined).
+ * @treturn TestResult The result of the test.
+ */
+function EmbeddedTextTestRunner_run( testNames, suiteName ) 
+{
+    var result = this.createTestResult();
+    result.addListener( this.mPrinter );
+
+    var tests = new Array();
+    for( var test in testNames )
+    {
+        var suite = this.getTest( testNames[test] );
+        if( suite )
+            tests.push( suite );
+    }
+    
+    var test;
+    if( tests.length == 0 ) {
+        if( typeof( suiteName ) != "string" )
+            suiteName = "AllTests";
+        test = this.getTest( suiteName );
+        if( !test ) {
+            test = new TestSuite();
+            test.setName( suiteName );
+        }
+    }
+    else if( tests.length > 1 )
+    {
+        test = new TestSuite( typeof( suiteName ) != "string" ? "TestCollection" : suiteName );
+        for( i = 0; i < tests.length; ++i )
+            test.addTest( tests[i] );
+    }
+    else
+        test = tests[0];
+
+    var startTime = new Date();
+    if( test )
+        test.run( result );
+    var endTime = new Date();
+    this.mPrinter.print( result, endTime - startTime );
+    return result;
+}
+/**
+ * Set printer.
+ * @tparam ResultPrinter printer The ResultPrinter
+ */
+function EmbeddedTextTestRunner_setPrinter( printer )
+{
+    this.mPrinter = printer;
+}
+EmbeddedTextTestRunner.prototype = new BaseTestRunner();
+EmbeddedTextTestRunner.glue();
diff --git a/trunk/features/src/test/javascript/lib/JsUnitBV.js b/trunk/features/src/test/javascript/lib/JsUnitBV.js
new file mode 100644
index 0000000..9094e04
--- /dev/null
+++ b/trunk/features/src/test/javascript/lib/JsUnitBV.js
@@ -0,0 +1,64 @@
+/*
+JsUnit - a JUnit port for JavaScript
+Copyright (C) 1999,2000,2001,2002,2003,2006 Joerg Schaible
+
+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.
+*/
+
+/**
+ * @file
+ * Test unit classes for BroadVision environment.
+ * This file contains extensions for the test unit framework especially 
+ * for BroadVision
+ */
+
+/**
+ * Class for an application running test suites with the BroadVision ctxdriver
+ * and console output.
+ */
+function CtxWriter()
+{
+}
+/** 
+ * \internal 
+ */
+function CtxWriter__flush( str )
+{
+    print( str.substring( 0, str.length - 1 )); 
+}
+CtxWriter.prototype = new PrinterWriter();
+CtxWriter.prototype._flush = CtxWriter__flush;
+
+
+/**
+ * Class for an application running test suites with the BroadVision ctxdriver 
+ * and console output.
+ * @see TextTestRunner
+ * @see CtxWriter
+ * @deprecated since 1.2 in favour of TextTestRunner in combination with a 
+ * CtxWriter.
+ */
+function CtxTestRunner()
+{
+    TextTestRunner.call( this );
+}
+/**
+ * Write a line of text to the browser window.
+ * @tparam String str The text to print on the line.
+ * @deprecated since 1.2
+ */
+function CtxTestRunner_writeLn( str ) { print( str ); }
+
+CtxTestRunner.prototype = new TextTestRunner();
+CtxTestRunner.prototype.writeLn = CtxTestRunner_writeLn;
+
diff --git a/trunk/features/src/test/javascript/lib/JsUnitNSServer.js b/trunk/features/src/test/javascript/lib/JsUnitNSServer.js
new file mode 100644
index 0000000..14aa08f
--- /dev/null
+++ b/trunk/features/src/test/javascript/lib/JsUnitNSServer.js
@@ -0,0 +1,84 @@
+/*
+JsUnit - a JUnit port for JavaScript
+Copyright (C) 1999,2000,2001,2002,2003,2006 Joerg Schaible
+
+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.
+*/
+
+/**
+ * @file
+ * Test unit classes for a Netscape Server environment.
+ * This file contains extensions for the test unit framework especially for 
+ * output of the results at a Netscape Server.
+ */
+
+/**
+ * PrinterWriter for an application running test suites with the Netscape 
+ * Server.
+ */
+function NSServerWriter()
+{
+}
+/** 
+ * \internal 
+ */
+function NSServerWriter__flush( str )
+{
+    print( str ); 
+}
+NSServerWriter.prototype = new PrinterWriter();
+NSServerWriter.prototype._flush = NSServerWriter__flush;
+
+
+/**
+ * Class for an application running test suites with a Netscape Server.
+ * @see TextTestRunner
+ * @see NSServerWriter
+ * @deprecated since 1.2 in favour of TextTestRunner in combination with a 
+ * NSServerWriter.
+ */
+function NSServerTestRunner()
+{
+    TextTestRunner.call( this );
+}
+/**
+ * Write a header starting the application.
+ * @deprecated since 1.2
+ */
+function NSServerTestRunner_printHeader()
+{
+    write( "<pre>" );
+    TextTestRunner.prototype.printHeader.call( this );
+}
+/**
+ * Write a footer at application end with a summary of the tests.
+ * @tparam TestResult result The result of the test run.
+ * @deprecated since 1.2
+ */
+function NSServerTestRunner_printFooter( result )
+{
+    TextTestRunner.prototype.printFooter.call( this, result );
+    write( "</pre>" );
+}
+/**
+ * Write a line of text to the console to the browser window.
+ * @tparam String str The text to print on the line.
+ * @deprecated since 1.2
+ */
+function NSServerTestRunner_writeLn( str ) { write( str + "\n" ); }
+
+NSServerTestRunner.prototype = new TextTestRunner();
+NSServerTestRunner.prototype.printHeader = NSServerTestRunner_printHeader;
+NSServerTestRunner.prototype.printFooter = NSServerTestRunner_printFooter;
+NSServerTestRunner.prototype.writeLn = NSServerTestRunner_writeLn;
+
diff --git a/trunk/features/src/test/javascript/lib/JsUtil.js b/trunk/features/src/test/javascript/lib/JsUtil.js
new file mode 100644
index 0000000..f478923
--- /dev/null
+++ b/trunk/features/src/test/javascript/lib/JsUtil.js
@@ -0,0 +1,914 @@
+/*
+JsUnit - a JUnit port for JavaScript
+Copyright (C) 1999,2000,2001,2002,2003,2006,2007 Joerg Schaible
+
+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.
+*/
+
+/**
+ * @file
+ * Utility classes needed for the JsUnit classes.
+ * JsUnit need several helper classes to work properly. This file contains
+ * anything that is not related directly to JsUnit, but may be useful in other
+ * environments, too.
+ */
+
+
+if( !this.Error )
+{
+    /**
+     * Error class according ECMA specification.
+     * This class is only active, if the ECMA implementation of the current
+     * engine does not support it.
+     * @ctor
+     * Constructor.
+     * The constructor initializes the \c message member with the argument 
+     * \a msg.
+     * \attention The ECMA standard does not ensure, that the constructor
+     * of the internal Error class may be called by derived objects. It will
+     * normally return a new Error instance if called as function.
+     * @tparam String msg The error message.
+     */
+    function Error( msg )
+    {
+        if( this instanceof Error )
+        {
+            /**
+             * The error message.
+             * @type String
+             */
+            this.message = msg || "";
+            return;
+        }
+        else
+        {
+            return new Error( msg );
+        }
+    }
+    /**
+     * String representation of the error.
+     * @treturn String Returns a \c String containing the Error class name 
+     * and the error message.
+     * \attention The format of the returned string is not defined by ECMA
+     * and is up to the vendor only. This implementation follows the behavior
+     * of Mozilla.org's SpiderMonkey.
+     */
+    function Error_toString()
+    {
+        var msg = this.message;
+        return this.name + ": " + msg;
+    }
+    Error.prototype = new Object();
+    Error.prototype.toString = Error_toString;
+    /**
+     * The name of the Error class as String.
+     * @type String
+     */
+    Error.prototype.name = "Error";
+    /**
+     * \internal
+     */
+    Error.prototype.testable = true;
+}
+else 
+{
+    /**
+     * \internal
+     */
+    Error.prototype.testable = false;
+}
+
+
+/**
+ * JsUnitError class.
+ * Since ECMA does not define any inheritability of the Error class and the
+ * class itself is highly vender specific, JsUnit uses its own base class for
+ * all errors in the framework.
+ * @ctor
+ * Constructor.
+ * The constructor initializes the \c message member with the argument 
+ * \a msg.
+ * \attention The ECMA standard does not ensure, that the constructor
+ * of the internal Error class may be called by derived objects. It will
+ * normally return a new Error instance if called as function.
+ * @tparam String msg The error message.
+ * \attention This constructor may <b>not</b> be called as normal function.
+ */
+function JsUnitError( msg )
+{
+    this.message = msg || "";   
+}
+/**
+ * String representation of the error.
+ * The format of the returned string is not defined by ECMA
+ * and is up to the vendor only. This implementation follows the behavior
+ * of Mozilla.org's SpiderMonkey.
+ * @treturn String Returns a \c String containing the Error class name 
+ * and the error message.
+ */
+function JsUnitError_toString()
+{
+    var msg = this.message;
+    return this.name + ": " + msg;
+}
+JsUnitError.prototype = new Error();
+JsUnitError.prototype.toString = JsUnitError_toString;
+/**
+ * The name of the Error class as String.
+ * @type String
+ */
+JsUnitError.prototype.name = "JsUnitError";
+
+
+/**
+ * InterfaceDefinitionError class.
+ * This error class is used for interface definitions. Such definitions are 
+ * simulated using Function::fulfills. The class has no explicit functionality
+ * despite the separate type
+ * @see Function::fulfills
+ * @ctor
+ * Constructor.
+ * The constructor initializes the \c message member with the argument 
+ * \a msg.
+ * @tparam String msg The error message.
+ **/
+function InterfaceDefinitionError( msg )
+{
+    JsUnitError.call( this, msg );
+}
+InterfaceDefinitionError.prototype = new JsUnitError();
+/**
+ * The name of the InterfaceDefinitionError class as String.
+ * @type String
+ **/
+InterfaceDefinitionError.prototype.name = "InterfaceDefinitionError";
+
+
+/**
+ * FunctionGluingError class.
+ * This error class is used for gluing member functions to a class. This convenience
+ * functionality of Function::glue ensures by throwing an instance of this class, that
+ * only valid functions are injected to the prototype. The class has no explicit 
+ * functionality despite the separate type
+ * @see Function::glue
+ * @ctor
+ * Constructor.
+ * The constructor initializes the \c message member with the argument 
+ * \a msg.
+ * @tparam String msg The error message.
+ **/
+function FunctionGluingError( msg )
+{
+    JsUnitError.call( this, msg );
+}
+FunctionGluingError.prototype = new JsUnitError();
+/**
+ * The name of the FunctionGluingError class as String.
+ * @type String
+ **/
+FunctionGluingError.prototype.name = "FunctionGluingError";
+
+
+/**
+ * \class Function
+ * Standard ECMA class.
+ * \docgen function Function() {}
+ */
+/**
+ * Ensures that a function fulfills an interface.
+ * Since with ECMA 262 (3rd edition) interfaces are not supported yet, this
+ * function will simulate the functionality. The arguments for the functions
+ * are all classes that the current class will implement. The function checks
+ * whether the current class fulfills the interface of the given classes or not.
+ * @exception TypeError If the current object is not a class or the interface
+ * is not a Function object with a prototype.
+ * @exception InterfaceDefinitionError If an interface is not fulfilled or the 
+ * interface has invalid members.
+ */
+function Function_fulfills()
+{
+    for( var i = 0; i < arguments.length; ++i )
+    {
+        var I = arguments[i];
+        if( typeof I != "function" || !I.prototype )
+            throw new InterfaceDefinitionError( 
+                I.toString() + " is not an Interface" );
+        if( !this.prototype )
+            throw new InterfaceDefinitionError( 
+                "Current instance is not a Function definition" );
+        for( var f in I.prototype )
+        {
+            if( typeof I.prototype[f] != "function" )
+                throw new InterfaceDefinitionError( f.toString() 
+                    + " is not a method in Interface " + I.toString());
+            if(    typeof this.prototype[f] != "function" 
+                && typeof this[f] != "function" )
+            {
+                if(    typeof this.prototype[f] == "undefined" 
+                    && typeof this[f] == "undefined" )
+                    throw new InterfaceDefinitionError( 
+                        f.toString() + " is not defined" );
+                else
+                    throw new InterfaceDefinitionError( 
+                        f.toString() + " is not a function" );
+            }
+        }
+    }
+}
+/**
+ * Glue functions to a JavaScript class as member functions.
+ * The method attaches the functions given as arguments to the prototype of the
+ * current instance.
+ * @exception InterfaceDefinitionError If the current instance of a given
+ * argument is not a Function object with a prototype.
+ */
+function Function_glue( scope )
+{
+    if( !this.prototype )
+        throw new FunctionGluingError( 
+            "Current instance is not a Function definition" );
+    var r = /function (\w+)[^\{\}]*\)/;
+    if( !r.exec( this.toString()))
+        throw new FunctionGluingError( "Cannot glue to anonymous function" );
+    var className = new String( RegExp.$1 );
+    if( scope  === undefined )
+        scope = JsUtil.prototype.global;
+    for( var name in scope ) 
+    {
+        if( name.indexOf( className + "_" ) == 0 )
+        {
+            var fnName = name.substr( className.length + 1 );
+            var fn = scope[name];
+            if( typeof( fn ) == "function" ) 
+            {
+                if( ! /^[a-z_][\w]*$/.test( fnName ))
+                    throw new FunctionGluingError( 
+                        "Not a valid method name: " + fnName );
+                this.prototype[fnName] = fn;
+            }
+        }
+    }
+}
+Function.prototype.fulfills = Function_fulfills;
+Function.prototype.glue = Function_glue;
+
+
+// MS engine does not implement Array.push and Array.pop until JScript 5.6
+if( !Array.prototype.pop )
+{
+    /**
+     * \class Array
+     * Standard ECMA class.
+     * \docgen function Array() {}
+     */
+    /**
+     * Pops last element from Array.
+     * The function is an implementation of the Array::pop method described
+     * in the ECMA standard. It removes the last element of the Array and
+     * returns it.
+     *
+     * The function is active if the ECMA implementation does not implement
+     * it (like Microsoft JScript engine up to version 5.5).
+     * @treturn Object Last element or undefined
+     */
+    function Array_pop()
+    {
+        var obj;
+        if( this instanceof Array && this.length > 0 )
+        {
+            var last = parseInt( this.length ) - 1;
+            obj = this[last];
+            this.length = last;
+        }
+        return obj;
+    }
+    Array.prototype.pop = Array_pop;
+}   
+if( !Array.prototype.push )
+{ 
+    /**
+     * Pushes elements into Array.
+     * The function is an implementation of the Array::push method described
+     * in the ECMA standard. It adds all given parameters at the end of the
+     * array.
+     *
+     * The function is active if the ECMA implementation does not implement
+     * it (like Microsoft JScript engine up to version 5.5).
+     * @treturn Object Number of added elements
+     */
+    function Array_push()
+    {
+        var i = 0;
+        if( this instanceof Array )
+        {
+            i = this.length;
+            
+            // Preallocation of array
+            if( arguments.length > 0 )
+                this[arguments.length + this.length - 1] = null;
+            
+            for( ; i < this.length; ++i )
+                this[i] = arguments[i - this.length + arguments.length];
+        }       
+        return i;
+    }
+    Array.prototype.push = Array_push;
+}
+
+
+/**
+ * \class String
+ * Standard ECMA class.
+ * \docgen function String() {}
+ */
+/**
+ * Trims characters from string.
+ * @tparam String chars String with characters to remove.  The character may
+ * also be a regular expression character class like "\\s" (which is the 
+ * default).
+ *
+ * The function removes the given characters \a chars from the beginning an 
+ * the end from the current string and returns the result. The function will 
+ * not modify the current string.
+ *
+ * The function is written as String enhancement and available as new member 
+ * function of the class String.
+ * @treturn String String without given characters at start or end.
+ */
+function String_trim( chars )
+{
+    if( !chars )
+        chars = "\\s";
+    var re = new RegExp( "^[" + chars + "]*(.*?)[" + chars + "]*$" );
+    var s = this.replace( re, "$1" );
+    return s;
+}
+String.prototype.trim = String_trim;
+
+
+/**
+ * Helper class with static flags.
+ */
+function JsUtil()
+{
+}
+/** 
+ * Retrieve the caller of a function.
+ * @tparam Function fn The function to examine.
+ * @treturn Function The caller as Function or undefined.
+ **/
+function JsUtil_getCaller( fn )
+{
+    switch( typeof( fn ))
+    {
+        case "undefined":
+            return JsUtil_getCaller( JsUtil_getCaller );
+            
+        case "function":
+            if( fn.caller )
+                return fn.caller;
+            if( fn.arguments && fn.arguments.caller )
+                return fn.arguments.caller;
+    }
+    return undefined;
+}
+/**
+ * Includes a JavaScript file.
+ * @tparam String fname The file name.
+ * Loads the content of a JavaScript file into a String that has to be
+ * evaluated. Works for command line shells WSH, Rhino and SpiderMonkey.
+ * @note This function is highly quirky. While WSH works as expected, the
+ * Mozilla shells will evaluate the file immediately and add any symbols to
+ * the global name space and return just "true". Therefore you have to 
+ * evaluate the returned string for WSH at global level also. Otherwise the
+ * function is not portable.
+ * @treturn String The JavaScript code to be evaluated.
+ */
+function JsUtil_include( fname )
+{
+    var ret = "true";
+    if( JsUtil.prototype.isMozillaShell || JsUtil.prototype.isKJS )
+    {
+        load( fname );
+    }
+    else if( JsUtil.prototype.isWSH )
+    {
+        var fso = new ActiveXObject( "Scripting.FileSystemObject" );
+        var file = fso.OpenTextFile( fname, 1 );
+        ret = file.ReadAll();
+        file.Close();
+    }
+    return ret;
+}
+/**
+ * Returns the SystemWriter.
+ * Instantiates a SystemWriter depending on the current JavaScript engine.
+ * Works for command line shells WSH, Rhino and SpiderMonkey.
+ * @type SystemWriter
+ */
+function JsUtil_getSystemWriter()
+{
+    if( !JsUtil.prototype.mWriter )
+        JsUtil.prototype.mWriter = new SystemWriter();
+    return JsUtil.prototype.mWriter;
+}
+/**
+ * Quits the JavaScript engine.
+ * @tparam Number ret The exit code.
+ * Stops current JavaScript engine and returns an exit code. Works for 
+ * command line shells WSH, Rhino and SpiderMonkey.
+ */
+function JsUtil_quit( ret )
+{
+    if( JsUtil.prototype.isMozillaShell )
+        quit( ret );
+    else if( JsUtil.prototype.isKJS )
+        exit( ret );
+    else if( JsUtil.prototype.isWSH )
+        WScript.Quit( ret );
+}
+JsUtil.prototype.getCaller = JsUtil_getCaller;
+JsUtil.prototype.getSystemWriter = JsUtil_getSystemWriter;
+JsUtil.prototype.include = JsUtil_include;
+JsUtil.prototype.quit = JsUtil_quit;
+/**
+ * The SystemWriter.
+ * @type SystemWriter
+ * @see getSystemWriter
+ */
+JsUtil.prototype.mWriter = null;
+/**
+ * Flag for a browser.
+ * @type Boolean
+ * The member is true, if the script runs within a browser environment.
+ */
+JsUtil.prototype.isBrowser = this.window != null;
+/**
+ * Flag for Microsoft JScript.
+ * @type Boolean
+ * The member is true, if the script runs in the Microsoft JScript engine.
+ */
+JsUtil.prototype.isJScript = this.ScriptEngine != null;
+/**
+ * Flag for Microsoft Windows Scripting Host.
+ * @type Boolean
+ * The member is true, if the script runs in the Microsoft Windows Scripting
+ * Host.
+ */
+JsUtil.prototype.isWSH = this.WScript != null;
+/**
+ * Flag for Microsoft IIS.
+ * @type Boolean
+ * The member is true, if the script runs in the Microsoft JScript engine.
+ */
+JsUtil.prototype.isIIS = 
+       JsUtil.prototype.isJScript
+    && this.Server != null;
+/**
+ * Flag for Netscape Enterprise Server (iPlanet) engine.
+ * @type Boolean
+ * The member is true, if the script runs in the iPlanet as SSJS.
+ */
+JsUtil.prototype.isNSServer = 
+       this.Packages != null 
+    && !this.importPackage 
+    && !JsUtil.prototype.isBrowser;
+/**
+ * Flag for Rhino.
+ * @type Boolean
+ * The member is true, if the script runs in an embedded Rhino of Mozilla.org.
+ */
+JsUtil.prototype.isRhino = 
+       this.java != null 
+    && this.java.lang != null 
+    && this.java.lang.System != null;
+/**
+ * Flag for a Mozilla JavaScript shell.
+ * @type Boolean
+ * The member is true, if the script runs in a command line shell of a
+ * Mozilla.org script engine (either SpiderMonkey or Rhino).
+ */
+JsUtil.prototype.isMozillaShell = this.quit != null;
+/**
+ * Flag for a KJS shell.
+ * @type Boolean
+ * The member is true, if the script runs in a command line shell of a
+ * KDE's script engine.
+ */
+JsUtil.prototype.isKJS = this.exit != null;
+/**
+ * Flag for a command line shell.
+ * @type Boolean
+ * The member is true, if the script runs in a command line shell.
+ */
+JsUtil.prototype.isShell = 
+       JsUtil.prototype.isMozillaShell 
+    || JsUtil.prototype.isKJS 
+    || JsUtil.prototype.isWSH;
+/**
+ * Flag for Obtree C4.
+ * @type Boolean
+ * The member is true, if the script runs in Obtree C4 of IXOS.
+ */
+JsUtil.prototype.isObtree = this.WebObject != null;
+/**
+ * Flag for call stack support.
+ * @type Boolean
+ * The member is true, if the engine provides call stack info.
+ */
+JsUtil.prototype.hasCallStackSupport = 
+       JsUtil.prototype.getCaller() !== undefined;
+/**
+ * The global object.
+ * @type Object
+ * The member keeps the execution scope of this file, which is normally the 
+ * global object.
+ */
+JsUtil.prototype.global = this;
+
+
+/**
+ * CallStack object.
+ * The object is extremely system dependent, since its functionality is not
+ * within the range of ECMA 262, 3rd edition. It is supported by JScript
+ * and SpiderMonkey and was supported in Netscape Enterprise Server 2.x, 
+ * but not in the newer version 4.x.
+ * @ctor
+ * Constructor.
+ * The object collects the current call stack up to the JavaScript engine.
+ * Most engines will not support call stack information with a recursion.
+ * Therefore the collection is stopped when the stack has two identical
+ * functions in direct sequence.
+ * @tparam Number depth Maximum recorded stack depth (defaults to 10).
+ **/
+function CallStack( depth )
+{
+    /**
+     * The array with the stack. 
+     * @type Array<String>
+     */
+    this.mStack = null;
+    if( JsUtil.prototype.hasCallStackSupport )
+        this._fill( depth );
+}
+
+/**
+ * \internal
+ */
+function CallStack__fill( depth )
+{
+    this.mStack = new Array();
+    
+    // set stack depth to default
+    if( depth == null )
+        depth = 10;
+
+    ++depth;
+    var fn = JsUtil.prototype.getCaller( CallStack__fill );
+    while( fn != null && depth > 0 )
+    {
+        var s = new String( fn );
+        --depth;
+
+        // Extract function name and argument list
+        var r = /function (\w+)([^\{\}]*\))/;
+        r.exec( s );
+        var f = new String( RegExp.$1 );
+        var args = new String( RegExp.$2 );
+        this.mStack.push(( f + args ).replace( /\s/g, "" ));
+
+        // Retrieve caller function
+        if( fn == JsUtil.prototype.getCaller( fn ))
+        {
+            // Some interpreter's caller use global objects and may start
+            // an endless recursion.
+            this.mStack.push( "[JavaScript recursion]" );
+            break;
+        }
+        else
+            fn = JsUtil.prototype.getCaller( fn );
+    }
+
+    if( fn == null )
+        this.mStack.push( "[JavaScript engine]" );
+
+    // remove direct calling function CallStack or CallStack_fill
+    this.mStack.shift();
+}
+/**
+ * Fills the object with the current call stack info.
+ * The function collects the current call stack up to the JavaScript engine.
+ * Any previous data of the instance is lost.
+ * Most engines will not support call stack information with a recursion.
+ * Therefore the collection is stopped when the stack has two identical
+ * functions in direct sequence.
+ * @tparam Number depth Maximum recorded stack depth (defaults to 10).
+ **/
+function CallStack_fill( depth )
+{
+    this.mStack = null;
+    if( JsUtil.prototype.hasCallStackSupport )
+        this._fill( depth );
+}
+/**
+ * Retrieve call stack as array.
+ * The function returns the call stack as Array of Strings. 
+ * @treturn Array<String> The call stack as array of strings.
+ **/
+function CallStack_getStack()
+{
+    var a = new Array();
+    if( this.mStack != null )
+        for( var i = this.mStack.length; i--; )
+            a[i] = this.mStack[i];
+    return a;
+}
+/**
+ * Retrieve call stack as string.
+ * The function returns the call stack as string. Each stack frame has an 
+ * own line and is prepended with the call stack depth.
+ * @treturn String The call stack as string.
+ **/
+function CallStack_toString()
+{
+    var s = "";
+    if( this.mStack != null )
+        for( var i = 1; i <= this.mStack.length; ++i )
+        {
+            if( s.length != 0 )
+                s += "\n";
+            s += i.toString() + ": " + this.mStack[i-1];
+        }
+    return s;
+}
+CallStack.prototype._fill = CallStack__fill;
+CallStack.prototype.fill = CallStack_fill;
+CallStack.prototype.getStack = CallStack_getStack;
+CallStack.prototype.toString = CallStack_toString;
+
+
+/**
+ * PrinterWriterError class.
+ * This error class is used for errors in the PrinterWriter.
+ * @see PrinterWriter::close
+ * @ctor
+ * Constructor.
+ * The constructor initializes the \c message member with the argument 
+ * \a msg.
+ * @tparam String msg The error message.
+ **/
+function PrinterWriterError( msg )
+{
+    JsUnitError.call( this, msg );
+}
+PrinterWriterError.prototype = new JsUnitError();
+/**
+ * The name of the PrinterWriterError class as String.
+ * @type String
+ **/
+PrinterWriterError.prototype.name = "PrinterWriterError";
+
+
+/**
+ * A PrinterWriter is an abstract base class for printing text.
+ * @note This is a helper construct to support different writers in 
+ * ResultPrinter e.g. depending on the JavaScript engine.
+ */
+function PrinterWriter()
+{
+    this.mBuffer = null;    
+    this.mClosed = false;
+}
+/**
+ * Closes the writer.
+ * After closing the steam no further writing is allowed. Multiple calls to
+ * close should be allowed.
+ */
+function PrinterWriter_close() 
+{
+    this.flush();
+    this.mClosed = true;
+}
+/**
+ * Flushes the writer.
+ * Writes any buffered data to the underlaying output stream system immediately.
+ * @exception PrinterWriterError If flush was called after closing.
+ */
+function PrinterWriter_flush()
+{
+    if( !this.mClosed )
+    {
+        if( this.mBuffer !== null )
+        {
+            this._flush( this.mBuffer + "\n" );
+            this.mBuffer = null;    
+        }
+    }
+    else    
+        throw new PrinterWriterError( 
+            "'flush' called for closed PrinterWriter." );
+}
+/**
+ * Prints into the writer.
+ * @tparam Object data The data to print as String.
+ * @exception PrinterWriterError If print was called after closing.
+ */
+function PrinterWriter_print( data )
+{
+    if( !this.mClosed )
+    {
+        var undef;
+        if( data === undef || data == null )
+            data = "";
+        if( this.mBuffer )
+            this.mBuffer += data.toString();
+        else
+            this.mBuffer = data.toString();
+    }
+    else    
+        throw new PrinterWriterError( 
+            "'print' called for closed PrinterWriter." );
+}
+/**
+ * Prints a line into the writer.
+ * @tparam Object data The data to print as String.
+ * @exception PrinterWriterError If println was called after closing.
+ */
+function PrinterWriter_println( data )
+{
+    this.print( data );
+    this.flush();
+}
+PrinterWriter.prototype.close = PrinterWriter_close;
+PrinterWriter.prototype.flush = PrinterWriter_flush;
+PrinterWriter.prototype.print = PrinterWriter_print;
+PrinterWriter.prototype.println = PrinterWriter_println;
+/** 
+ * \internal 
+ */
+PrinterWriter.prototype._flush = function() {};
+
+
+/**
+ * The PrinterWriter of the JavaScript engine.
+ */
+function SystemWriter() 
+{
+    PrinterWriter.call( this );
+} 
+/**
+ * Closes the writer.
+ * Function just flushes the writer. Closing the system writer is not possible.
+ */
+function SystemWriter_close() 
+{
+    this.flush();
+}
+/** 
+ * \internal 
+ */
+function SystemWriter__flush( str ) 
+{
+    /* self-modifying code ... */
+    if( JsUtil.prototype.isMozillaShell )
+        this._flush = 
+            function SystemWriter__flush( str ) 
+            { 
+                print( str.substring( 0, str.length - 1 )); 
+            }
+    else if( JsUtil.prototype.isKJS )
+        this._flush = 
+            function SystemWriter__flush( str ) 
+            { 
+                print( str ); 
+            }
+    else if( JsUtil.prototype.isBrowser )
+        this._flush = 
+            function SystemWriter__flush( str ) 
+            { 
+                document.write( str );
+            }
+    else if( JsUtil.prototype.isWSH )
+        this._flush = 
+            function SystemWriter__flush( str ) 
+            { 
+                WScript.Echo( str.substring( 0, str.length - 1 )); 
+            }
+    else if( JsUtil.prototype.isIIS )
+        this._flush = 
+            function SystemWriter__flush( str ) 
+            { 
+                Response.write( str ); 
+            }
+    /*
+    else if( JsUtil.prototype.isNSServer )
+        this._flush = 
+            function SystemWriter__flush( str ) 
+            { 
+                write( str );
+            }
+    */
+    else if( JsUtil.prototype.isObtree )
+        this._flush = 
+            function SystemWriter__flush( str ) 
+            { 
+                write( str ); 
+            }
+    else
+        this._flush = function() {}
+
+    this._flush( str );
+}
+SystemWriter.prototype = new PrinterWriter();
+SystemWriter.prototype.close = SystemWriter_close;
+SystemWriter.prototype._flush = SystemWriter__flush;
+
+
+/**
+ * The PrinterWriter into a String.
+ */
+function StringWriter() 
+{
+    PrinterWriter.call( this );
+    this.mString = "";
+} 
+/**
+ * Returns the written String.
+ * The function will close also the stream if it is still open.
+ * @type String
+ */
+function StringWriter_get() 
+{
+    if( !this.mClosed )
+        this.close();
+    return this.mString;
+}
+/** 
+ * \internal 
+ */
+function StringWriter__flush( str )
+{
+    this.mString += str;
+}
+StringWriter.prototype = new PrinterWriter();
+StringWriter.prototype.get = StringWriter_get;
+StringWriter.prototype._flush = StringWriter__flush;
+
+
+/**
+ * A filter for a PrinterWriter encoding HTML.
+ * @ctor
+ * Constructor.
+ * @tparam PrinterWriter writer The writer to filter.
+ * The constructor accepts the writer to wrap.
+ */
+function HTMLWriterFilter( writer )
+{
+    PrinterWriter.call( this );
+    this.setWriter( writer );
+}
+/**
+ * Returns the wrapped PrinterWriter.
+ * @type PrinterWriter
+ */
+function HTMLWriterFilter_getWriter() 
+{
+    return this.mWriter;
+}
+/**
+ * Sets the PrinterWriter to wrap.
+ * @tparam PrinterWriter writer The writer to filter.
+ * If the argument is omitted a StringWriter is created and wrapped.
+ */
+function HTMLWriterFilter_setWriter( writer ) 
+{
+    this.mWriter = writer ? writer : new StringWriter();
+}
+/** 
+ * \internal 
+ */
+function HTMLWriterFilter__flush( str )
+{
+    str = str.toString();
+    str = str.replace( /&/g, "&amp;" ); 
+    str = str.replace( /</g, "&lt;" ); 
+    str = str.replace( />/g, "&gt;" ); 
+    str = str.replace( /\'/g, "&apos;" ); 
+    str = str.replace( /\"/g, "&quot;" ); 
+    str = str.replace( /\n/g, "<br>" );
+    this.mWriter._flush( str );
+}
+HTMLWriterFilter.prototype = new PrinterWriter();
+HTMLWriterFilter.prototype.getWriter = HTMLWriterFilter_getWriter;
+HTMLWriterFilter.prototype.setWriter = HTMLWriterFilter_setWriter;
+HTMLWriterFilter.prototype._flush = HTMLWriterFilter__flush;
diff --git a/trunk/features/src/test/javascript/lib/testutils.js b/trunk/features/src/test/javascript/lib/testutils.js
new file mode 100644
index 0000000..c059527
--- /dev/null
+++ b/trunk/features/src/test/javascript/lib/testutils.js
@@ -0,0 +1,203 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Verifies members for a single service request function
+ * @param fn (Function) Service Request function
+ */
+TestCase.prototype.assertRequestPropertiesForService = function(fn) {
+  this.assertTrue('Should have produced a result', fn);
+  this.assertTrue('Should have an execute method', fn.execute);
+  this.assertTrue('Should have a json-rpc method', !!fn.method);
+  this.assertTrue('Should have a json-rpc', fn.rpc);
+};
+
+
+/**
+ * Verify that arguments sent out of system (to non-proxied xhr) are built properly
+ * @param argsInCall
+ * @param expectedJson
+ */
+TestCase.prototype.assertArgsToMakeNonProxiedRequest = function(argsInCall, expectedJson) {
+  this.assertTrue('url should be passed to makeNonProxiedRequest',
+      argsInCall.hasOwnProperty('url'));
+  this.assertTrue('callback should be passed to makeNonProxiedRequest',
+      argsInCall.callback);
+  this.assertTrue('params should be passed to makeNonProxiedRequest',
+      argsInCall.params);
+  this.assertEquals('Content type should match', 'application/json',
+      argsInCall.headers['Content-Type']);
+  this.assertEquals('Json for batch should match', expectedJson,
+      gadgets.json.parse(argsInCall.params.POST_DATA));
+
+};
+
+/**
+ * Verify that arguments sent out of system (to proxied xhr) are built properly
+ * @param argsInCall
+ * @param expectedJson
+ */
+TestCase.prototype.assertArgsToMakeRequest = function(argsInCall) {
+  this.assertTrue('url should be passed to makeNonProxiedRequest',
+      argsInCall.hasOwnProperty('url'));
+  this.assertTrue('callback should be passed to makeNonProxiedRequest',
+      argsInCall.callback);
+  this.assertTrue('options should be passed to makeNonProxiedRequest',
+      argsInCall.options);
+
+};
+
+/**
+ * Make a callback that can be asserted that it was called, and that still calls
+ * a callback.
+ * @return (Function) A callback function
+ */
+var makeInspectableCallback = function(realCallback) {
+  var called = false;
+
+  return {
+    callback : function(result) {
+      called = true;
+      if (realCallback != null) {
+        if (typeof result !== 'array') {
+          result = [result];
+        }
+        realCallback.apply(this, result);
+      }
+    },
+    wasCalled : function() {
+      return called;
+    }
+  };
+};
+
+
+/**
+ * An assert equals that compares correctly and prints useful output for object types, like json.
+ * @param msg
+ * @param expected
+ * @param actual
+ */
+TestCase.prototype.assertEquals = function(msg, expected, actual) {
+  if (arguments.length == 2) {
+    actual = expected;
+    expected = msg;
+    msg = null;
+  }
+
+  if (!deepEquals(expected, actual)) {
+    if (typeof( expected ) == 'string' && typeof( actual ) == 'string') {
+      throw new ComparisonFailure(msg, expected, actual, new CallStack());
+    } else {
+      this.fail('Expected:<' + getSourceMessage(expected) +
+                '>\n, but was:<' + getSourceMessage(actual) + '>'
+          , new CallStack(), msg);
+    }
+  }
+};
+
+/**
+ * Print an operand to an assert operation.
+ * @param expected
+ * @param actual
+ */
+var getSourceMessage = function(operand) {
+  if (operand === undefined) {
+    return "undefined";
+  } else if (operand === null) {
+    return "null";
+  } else {
+    return operand.toSource();
+  }
+};
+
+/**
+ * Implements deep equality for JSON objects;  the two objects
+ * are equal if they have the same properties with the same values.
+ *
+ * @param {Object} a first object
+ * @param {Object} b second object
+ * @return {boolean} true if the objects are equal
+ * @private
+ */
+function deepEquals(expected, actual) {
+  if (expected === actual) {
+    return true;
+  }
+
+  // Undefined/null can be treated as equal here, I believe
+  if (expected == null) {
+    return actual == null;
+  }
+
+  // If the types are different, the objects are different
+  var typeOfExpected = typeof expected;
+  if (typeOfExpected != typeof actual) {
+    return false;
+  }
+
+  // A few types that can handle a straight === check
+  if ((typeOfExpected == 'string') || (typeOfExpected == 'boolean') ||
+      (typeOfExpected == 'number')) {
+    return expected === actual;
+  }
+
+  // If it's an array, use deepEquals on each entry
+  if (typeOfExpected == 'array') {
+    if (expected.length != actual.length) {
+      return false;
+    }
+
+    for (var i = 0; i < expected.length; i++) {
+      if (!deepEquals(expected[i], actual[i])) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  // OK, we figure it's just an object.
+
+  // Make sure everything in a matches the value in b
+  for (var aKey in expected) {
+    if (!expected.hasOwnProperty(aKey)) {
+      continue;
+    }
+
+    if (!deepEquals(expected[aKey], actual[aKey])) {
+      return false;
+    }
+  }
+
+  // And make sure everything in b is in a
+  for (var bKey in actual) {
+    if (!actual.hasOwnProperty(bKey)) {
+      continue;
+    }
+    if (!(bKey in expected)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+;
+
+
diff --git a/trunk/java/LICENSE b/trunk/java/LICENSE
new file mode 100644
index 0000000..fb2a837
--- /dev/null
+++ b/trunk/java/LICENSE
@@ -0,0 +1,267 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+
+===============================================================================
+
+The Apache Shindig distribution includes a number of subcomponents
+with separate copyright notices and license terms. Your use of the
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+===============================================================================
+OpenSocial Specification 0.8:
+
+Copyright (c) 2008 OpenSocial Foundation (http://www.opensocial.org)
+Released under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+===============================================================================
+Code Mirror:
+ Copyright (c) 2007-2010 Marijn Haverbeke
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any
+ damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any
+ purpose, including commercial applications, and to alter it and
+ redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software. If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+    distribution.
+
+===============================================================================
+swfobject:
+
+The MIT License
+
+Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
+
+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.
+
+
+ Marijn Haverbeke
+ marijnh@gmail.com
diff --git a/trunk/java/NOTICE b/trunk/java/NOTICE
new file mode 100644
index 0000000..873cca8
--- /dev/null
+++ b/trunk/java/NOTICE
@@ -0,0 +1,16 @@
+Apache Shindig
+Copyright 2012 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+-----------------------------------------------------------
+
+This product includes software (Gadget Server, Gadget Container)
+originally developed by Google Inc. (http://code.google.com/) and licensed
+to the ASF as initial contribution for Shindig.
+
+This product contains software (sha1 JS impl) developed by Google Inc.
+
+This product includes software (wave) developed by Google, Inc
+Copyright 2010 Google Inc.
diff --git a/trunk/java/README b/trunk/java/README
new file mode 100644
index 0000000..3b82b9f
--- /dev/null
+++ b/trunk/java/README
@@ -0,0 +1,95 @@
+                          Apache Shindig Java
+
+  What is it?
+  -----------
+
+  Shindig is a JavaScript container and implementations of the backend APIs
+  and proxy required for hosting OpenSocial applications.
+
+  This is the Java implementation of Shindig. If you are looking to the PHP 
+  implementation, please visit our website.
+
+  Documentation
+  -------------
+
+  The most up-to-date documentation can be found at http://shindig.apache.org/
+  and at http://shindig.apache.org/developers/java/index.html for specific 
+  Java documentation.
+
+  Release Notes
+  -------------
+
+  The full list of changes can be found at https://issues.apache.org/jira/browse/SHINDIG.
+
+  System Requirements
+  -------------------
+
+  Java:
+    1.5 or above.
+  Servlet container:
+    Apache Tomcat or other compatible Java Servlet containers.
+  Memory:
+    128MB minimum requirement.
+  Disk:
+    32MB minimum requirement. 
+  Operating System:
+    No minimum requirement. On Windows, Windows NT and above or Cygwin is 
+    required for the startup scripts. Tested on Windows XP, Fedora Core 
+    and Mac OS X.
+
+  Installing Shindig Java
+  -----------------------
+
+  Unzip the distribution archive, i.e. shindig-${project.version}-java.zip to 
+  the directory you wish to install Shindig. 
+
+  The following explains how to deploy the Shindig war file to Apache 
+  Tomcat. If you are using an other container, please read its documentation
+  on how to proceed. You could download the war from:
+  http://repo1.maven.org/maven2/org/apache/shindig/shindig-server/${project.version}/shindig-server-${project.version}.war  
+
+  The easiest way to deploy Shindig on Apache Tomcat is to rename the 
+  shindig-server-${project.version}.war file to ROOT.war and drop it in the 
+  Tomcat webapps.
+  Be sure to delete $TOMCAT_HOME/webapps/ROOT dir before starting Tomcat.
+
+  Licensing
+  ---------
+
+  Please see the file called LICENSE.
+
+  Shindig URLS
+  ------------
+
+  Home Page:          http://shindig.apache.org/
+  Downloads:          http://shindig.apache.org/download/index.html
+  Mailing Lists:      http://shindig.apache.org/mail-lists.html
+  Source Code:        http://svn.apache.org/repos/asf/shindig/
+  Issue Tracking:     https://issues.apache.org/jira/browse/SHINDIG
+  Wiki:               http://cwiki.apache.org/confluence/display/SHINDIG/
+
+This distribution includes cryptographic software.  The country in
+which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of
+encryption software.  BEFORE using any encryption software, please
+check your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software, to
+see if this is permitted.  See <http://www.wassenaar.org/> for more
+information.
+
+The U.S. Government Department of Commerce, Bureau of Industry and
+Security (BIS), has classified this software as Export Commodity
+Control Number (ECCN) 5D002.C.1, which includes information security
+software using or performing cryptographic functions with asymmetric
+algorithms.  The form and manner of this Apache Software Foundation
+distribution makes it eligible for export under the License Exception
+ENC Technology Software Unrestricted (TSU) exception (see the BIS
+Export Administration Regulations, Section 740.13) for both object
+code and source code.
+
+The following provides more details on the included cryptographic
+software:
+
+    Apache Shindig interfaces with the Java JCE APIs to provide
+    encryption of messages using the AES standard.
+
diff --git a/trunk/java/common/conf/shindig.properties b/trunk/java/common/conf/shindig.properties
new file mode 100644
index 0000000..9285694
--- /dev/null
+++ b/trunk/java/common/conf/shindig.properties
@@ -0,0 +1,226 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# Location of feature manifests (comma separated)
+shindig.features.default=res://features/features.txt
+
+# Location of container configurations (comma separated)
+shindig.containers.default=res://containers/default/container.js
+
+### Inbound OAuth support
+# The URL base to use for full OAuth support (three-legged)
+shindig.oauth.base-url=/oauth
+shindig.oauth.authorize-action=/WEB-INF/authorize.jsp
+# The range to the past and future of timestamp for OAuth token validation. Default to 5 minutes
+shindig.oauth.validator-max-timestamp-age-ms=300000
+
+### Outbound OAuth support
+shindig.signing.state-key=
+shindig.signing.key-name=
+shindig.signing.key-file=
+shindig.signing.global-callback-url=http://%authority%%contextRoot%/gadgets/oauthcallback
+shindig.signing.enable-signed-callbacks=true
+
+### If a OAuth2Client does not specify a redirect uri it will default here
+shindig.oauth2.global-redirect-uri=http://%authority%%contextRoot%/gadgets/oauth2callback
+### Setting to true will cause the registered OAuth2Persistence plugin to load it's values
+### with what's in config/oauth2.json, no meaning without a second persistence implementation.
+shindig.oauth2.import=false
+### Determines if the import will start by removing everything currently in persistence.
+shindig.oauth2.import.clean=false
+# Set to true if you want to allow the use of 3-party (authorization_code) OAuth 2.0 flow when viewer != owner.
+# This setting is not recommended for pages that allow user-controlled javascript, since
+# that javascript could be used to make unauthorized requests on behalf of the viewer of the page
+shindig.oauth2.viewer-access-tokens-enabled=true
+# Set to true to send extended trace messages to the client.  Probably want this to be false for
+# production systems and true for test/development.
+shindig.oauth2.send-trace-to-client=true
+shindig.signing.oauth2.state-key=
+
+# Set to true if you want to allow the use of 3-legged OAuth tokens when viewer != owner.
+# This setting is not recommended for pages that allow user-controlled javascript, since
+# that javascript could be used to make unauthorized requests on behalf of the viewer of the page
+shindig.signing.viewer-access-tokens-enabled=false
+
+# If enabled here, configuration values can be found in container configuration files.
+shindig.locked-domain.enabled=false
+
+# Enable or disable referrer check.
+shindig.locked-domain.refererCheck.enabled=false
+
+# TODO: This needs to be moved to container configuration.
+shindig.content-rewrite.only-allow-excludes=false
+shindig.content-rewrite.include-urls=.*
+shindig.content-rewrite.exclude-urls=
+shindig.content-rewrite.include-tags=body,embed,img,input,link,script,style
+shindig.content-rewrite.expires=86400
+shindig.content-rewrite.enable-split-js-concat=true
+shindig.content-rewrite.enable-single-resource-concat=false
+
+#
+# Default set of forced libs to allow for better caching
+#
+# NOTE: setting this causes the EndToEnd test to fail the opensocial-templates test
+shindig.gadget-rewrite.default-forced-libs=core:rpc
+#shindig.gadget-rewrite.default-forced-libs=
+
+#
+# Allow supported JavaScript features required by a gadget to be externalized on demand
+shindig.gadget-rewrite.externalize-feature-libs=true
+
+# Configuration for image rewriter
+shindig.image-rewrite.max-inmem-bytes = 1048576
+shindig.image-rewrite.max-palette-size = 256
+shindig.image-rewrite.allow-jpeg-conversion = true
+shindig.image-rewrite.jpeg-compression = 0.90
+shindig.image-rewrite.min-threshold-bytes = 200
+shindig.image-rewrite.jpeg-retain-subsampling = false
+# Huffman optimization reduces the images size by addition 4-6% without
+# any loss in the quality of the image, but takes extra cpu cycles for
+# computing the optimized huffman tables.
+shindig.image-rewrite.jpeg-huffman-optimization = false
+
+# Configuration for the os:Flash tag
+shindig.flash.min-version = 9.0.115
+
+# Configuration for template rewriter
+shindig.template-rewrite.extension-tag-namespace=http://ns.opensocial.org/2009/extensions
+
+# These values provide default TTLs (in ms) for HTTP responses that don't use caching headers.
+shindig.cache.http.defaultTtl=3600000
+shindig.cache.http.negativeCacheTtl=60000
+
+# Amount of time after which the entry in cache should be considered for a refetch for a
+# non-userfacing internal fetch when the response is strict-no-cache.
+shindig.cache.http.strict-no-cache-resource.refetch-after-ms=-1
+
+# A default refresh interval for XML files, since there is no natural way for developers to
+# specify this value, and most HTTP responses don't include good cache control headers.
+shindig.cache.xml.refreshInterval=300000
+
+# Add entries in the form shindig.cache.lru.<name>.capacity to specify capacities for different
+# caches when using the LruCacheProvider.
+# It is highly recommended that the EhCache implementation be used instead of the LRU cache.
+shindig.cache.lru.default.capacity=1000
+shindig.cache.lru.expressions.capacity=1000
+shindig.cache.lru.gadgetSpecs.capacity=1000
+shindig.cache.lru.messageBundles.capacity=1000
+shindig.cache.lru.httpResponses.capacity=10000
+
+# The location of the EhCache configuration file.
+shindig.cache.ehcache.config=res://org/apache/shindig/common/cache/ehcache/ehcacheConfig.xml
+
+# The location of the filter file for EhCache's SizeOfEngine
+# This gets set as a system property to be consumed by EhCache.
+# Can be a resource on the classpath or a path on the file system.
+shindig.cache.ehcache.sizeof.filter=res://org/apache/shindig/common/cache/ehcache/SizeOfFilter.txt
+
+# true to enable JMX integration.
+shindig.cache.ehcache.jmx.enabled=true
+
+# true to enable JMX stats.
+shindig.cache.ehcache.jmx.stats=true
+
+# true to skip expensive encoding detection.
+# if true, will only attempt to validate utf-8. Assumes all other encodings are ISO-8859-1.
+shindig.http.fast-encoding-detection=true
+
+# Configuration for the HttpFetcher
+# Connection timeout, in milliseconds, for requests.
+shindig.http.client.connection-timeout-ms=5000
+
+# Maximum size, in bytes, of the object we fetched, 0 == no limit
+shindig.http.client.max-object-size-bytes=0
+
+# Strict-mode parsing for proxy and concat URIs ensures that the authority/host and path
+# for the URIs match precisely what is found in the container config for it. This is
+# useful where statistics and traffic routing patterns, typically in large installations,
+# key on hostname (and occasionally path). Enforcing this does come at the cost that
+# mismatches break, which in turn mandates that URI generation always happen in consistent
+# fashion, ie. by the class itself or tightly controlled code.
+shindig.uri.proxy.use-strict-parsing=false
+shindig.uri.concat.use-strict-parsing=false
+
+# Host:port of the proxy to use while fetching urls. Leave blank if proxy is
+# not to be used.
+org.apache.shindig.gadgets.http.basicHttpFetcherProxy=
+
+org.apache.shindig.serviceExpirationDurationMinutes=60
+
+#
+# Older versions of shindig used 'data' in the json-rpc response format
+# The spec calls for using 'result' instead, however to avoid breakage we
+# allow you to set it back to the old way here
+#
+# valid values are
+#  result  - new form
+#  data    - old broken form
+#  both    - return both fields for full compatibility
+#
+shindig.json-rpc.result-field=result
+
+# Remap "Internal server error"s received from the basicHttpFetcherProxy server to
+# "Bad Gateway error"s, so that it is clear to the user that the proxy server is
+# the one that threw the exception.
+shindig.accelerate.remapInternalServerError=true
+shindig.proxy.remapInternalServerError=true
+
+# Add debug data when using VanillaCajaHtmlParser.
+vanillaCajaParser.needsDebugData=true
+
+# Allow non-SSL OAuth 2.0 bearer tokens
+org.apache.shindig.auth.oauth2-require-ssl=false
+
+# Set gadget param in proxied uri as authority if this is true
+org.apache.shindig.gadgets.uri.setAuthorityAsGadgetParam=false
+
+# Maximum Get Url size limit
+org.apache.shindig.gadgets.uri.urlMaxLength=2048
+
+# Default cachettl value for versioned url in seconds. Here default value is 1 year.
+org.apache.shindig.gadgets.servlet.longLivedRefreshSec=31536000
+
+# Closure compiler optimization level.  One of advanced|simple|whitespace_only|none.
+# Defaults to simple.
+shindig.closure.compile.level=simple
+
+# Size of the compiler thread pool
+shindig.closure.compile.threadPoolSize=5
+
+# OAuth 2.0 authorization code, access token, and refresh token expiration times.
+# 5 * 60 * 1000 = 300000 = 5 minutes
+# 5 * 60 * 60 * 1000 = 18000000 = 5 hours
+# 5 * 60 * 60 * 1000 * 24 = 432000000 = 5 days
+shindig.oauth2.authCodeExpiration=300000
+shindig.oauth2.accessTokenExpiration=18000000
+shindig.oauth2.refreshTokenExpiration=432000000
+
+# Allows unauthenticated requests to Shindig
+shindig.allowUnauthenticated=true
+
+# Allows JSONP requests to Shindig
+shindig.allowJSONP=true
+
+# Comma separated tags that need to have its relative path to be resolved as absolute.
+# Possible values are RESOURCES and HYPERLINKS
+shindig.gadgets.rewriter.absolutePath.tags=RESOURCES
+
+# Configure cache characteristics of js content (max-age in seconds)
+# where -1 caches "forever, 0 means "no-cache"
+shindig.jscontent.unversioned.maxage=3600
+shindig.jscontent.versioned.maxage=-1
+shindig.jscontent.invalid.maxage=0
diff --git a/trunk/java/common/pom.xml b/trunk/java/common/pom.xml
new file mode 100644
index 0000000..f09ed6c
--- /dev/null
+++ b/trunk/java/common/pom.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-common</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Apache Shindig Common Code</name>
+  <description>Common java code for Shindig</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/common</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/common</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/common</url>
+  </scm>
+
+  <build>
+    <resources>
+      <resource>
+        <targetPath>containers/default</targetPath>
+        <directory>${basedir}/../../config</directory>
+        <includes>
+          <include>container.js</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>src/main/resources</directory>
+        <includes>
+          <include>**/*</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>conf</directory>
+        <includes>
+          <include>**/*</include>
+        </includes>
+      </resource>
+    </resources>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>reporting</id>
+      <reporting>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>clirr-maven-plugin</artifactId>
+            <configuration>
+              <comparisonVersion>${shindig.api.previous}</comparisonVersion>
+            </configuration>
+          </plugin>
+        </plugins>
+      </reporting>
+    </profile>
+  </profiles>
+
+  <dependencies>
+    <!-- external dependencies -->
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-fileupload</groupId>
+      <artifactId>commons-fileupload</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+    </dependency>
+    <dependency>
+      <artifactId>commons-lang3</artifactId>
+      <groupId>org.apache.commons</groupId>
+    </dependency>
+    <dependency>
+      <groupId>joda-time</groupId>
+      <artifactId>joda-time</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.oauth.core</groupId>
+      <artifactId>oauth</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.thoughtworks.xstream</groupId>
+      <artifactId>xstream</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.sf.ehcache</groupId>
+      <artifactId>ehcache-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>de.odysseus.juel</groupId>
+      <artifactId>juel-impl</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>el-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>jasper-el</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>xml-apis</groupId>
+      <artifactId>xml-apis</artifactId>
+    </dependency>
+
+    <!-- needed for ehcache's use of slf4j -->
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-jdk14</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AbstractSecurityToken.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AbstractSecurityToken.java
new file mode 100644
index 0000000..9c8bce3
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AbstractSecurityToken.java
@@ -0,0 +1,392 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+import org.apache.shindig.common.crypto.BlobExpiredException;
+import org.apache.shindig.common.util.TimeSource;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+
+/**
+ * A base class for SecurityToken Implementations.
+ * Currently provides an isExpired() method and getters/setters for nearly
+ * every field of the token.
+ *
+ * @since 2.0.0
+ */
+public abstract class AbstractSecurityToken implements SecurityToken {
+  /** allow three minutes for clock skew */
+  private static final long CLOCK_SKEW_ALLOWANCE = 180;
+
+  public static final int DEFAULT_MAX_TOKEN_TTL = 3600; // 1 hour
+
+  private static final TimeSource TIME_SOURCE = new TimeSource();
+
+  public enum Keys {
+    OWNER("o") {
+      public String getValue(SecurityToken token) {
+        return token.getOwnerId();
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        token.setOwnerId(map.get(key));
+      }
+    },
+    VIEWER("v") {
+      public String getValue(SecurityToken token) {
+        return token.getViewerId();
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        token.setViewerId(map.get(key));
+      }
+    },
+    APP_ID("i") {
+      public String getValue(SecurityToken token) {
+        return token.getAppId();
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        token.setAppId(map.get(key));
+      }
+    },
+    DOMAIN("d") {
+      public String getValue(SecurityToken token) {
+        return token.getDomain();
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        token.setDomain(map.get(key));
+      }
+    },
+    CONTAINER("c") {
+      public String getValue(SecurityToken token) {
+        return token.getContainer();
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        token.setContainer(map.get(key));
+      }
+    },
+    APP_URL("u") {
+      public String getValue(SecurityToken token) {
+        return token.getAppUrl();
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        token.setAppUrl(map.get(key));
+      }
+    },
+    MODULE_ID("m") {
+      public String getValue(SecurityToken token) {
+        long value = token.getModuleId();
+        if (value == 0) {
+          return null;
+        }
+        return Long.toString(token.getModuleId(), 10);
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        String value = map.get(key);
+        if (value != null) {
+          token.setModuleId(Long.parseLong(value, 10));
+        }
+      }
+    },
+    EXPIRES("x") {
+      public String getValue(SecurityToken token) {
+        Long value = token.getExpiresAt();
+        if (value == null) {
+          return null;
+        }
+        return Long.toString(token.getExpiresAt(), 10);
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        String value = map.get(key);
+        if (value != null) {
+          token.setExpiresAt(Long.parseLong(value, 10));
+        }
+      }
+    },
+    TRUSTED_JSON("j") {
+      public String getValue(SecurityToken token) {
+        return token.getTrustedJson();
+      }
+      public void loadFromMap(AbstractSecurityToken token, Map<String, String> map) {
+        token.setTrustedJson(map.get(key));
+      }
+    };
+
+    protected String key;
+    private Keys(String key) {
+      this.key = key;
+    }
+
+    /**
+     * @return The key this {@link Keys} is bound to.
+     */
+    public String getKey() {
+      return key;
+    }
+
+    /**
+     * Gets the {@link String} value from the {@link SecurityToken} using the getter that
+     * this {@link Keys} is bound to.
+     *
+     * @param token The token to get the value from.
+     * @return The value
+     */
+    public abstract String getValue(SecurityToken token);
+
+    /**
+     * Loads from the map the value bound to this {@link Keys} and sets it on the
+     * {@link SecurityToken}
+     *
+     * @param token The token to insert set the value on.
+     * @param map The map to read the value from.
+     */
+    public abstract void loadFromMap(AbstractSecurityToken token, Map<String, String> map);
+  }
+
+  private String ownerId;
+  private String viewerId;
+  private String appId;
+  private String domain;
+  private String container;
+  private String appUrl;
+  private long moduleId = 0;
+  private Long expiresAt;
+  private String trustedJson;
+  private String activeUrl;
+  private TimeSource timeSource = AbstractSecurityToken.TIME_SOURCE;
+  private int tokenTTL;
+
+  /**
+   * This method is mostly used for test code to test the expire methods.
+   *
+   * @param timeSource The new {@link TimeSource} for this token to use.
+   * @return This object.
+   */
+  @VisibleForTesting
+  protected AbstractSecurityToken setTimeSource(TimeSource timeSource) {
+    this.timeSource = timeSource;
+    return this;
+  }
+
+  protected TimeSource getTimeSource() {
+    return timeSource;
+  }
+
+  public String getOwnerId() {
+    return ownerId;
+  }
+
+  protected AbstractSecurityToken setOwnerId(String ownerId) {
+    this.ownerId = ownerId;
+    return this;
+  }
+
+  public String getViewerId() {
+    return viewerId;
+  }
+
+  protected AbstractSecurityToken setViewerId(String viewerId) {
+    this.viewerId = viewerId;
+    return this;
+  }
+
+  public String getAppId() {
+    return appId;
+  }
+
+  protected AbstractSecurityToken setAppId(String appId) {
+    this.appId = appId;
+    return this;
+  }
+
+  public String getDomain() {
+    return domain;
+  }
+
+  protected AbstractSecurityToken setDomain(String domain) {
+    this.domain = domain;
+    return this;
+  }
+
+  public String getContainer() {
+    return container;
+  }
+
+  protected AbstractSecurityToken setContainer(String container) {
+    this.container = container;
+    return this;
+  }
+
+  public String getAppUrl() {
+    return appUrl;
+  }
+
+  protected AbstractSecurityToken setAppUrl(String appUrl) {
+    this.appUrl = appUrl;
+    return this;
+  }
+
+  public long getModuleId() {
+    return moduleId;
+  }
+
+  protected AbstractSecurityToken setModuleId(long moduleId) {
+    this.moduleId = moduleId;
+    return this;
+  }
+
+  public Long getExpiresAt() {
+    return expiresAt;
+  }
+
+  /**
+   * Compute and set the expiration time for this token using the default TTL.
+   *
+   * @return This security token.
+   * @see #setExpires(int)
+   */
+  protected AbstractSecurityToken setExpires() {
+    return setExpires(DEFAULT_MAX_TOKEN_TTL);
+  }
+
+  /**
+   * Compute and set the expiration time for this token using the provided TTL.
+   *
+   * @param tokenTTL the time to live (in seconds) of the token
+   * @return This security token.
+   */
+  protected AbstractSecurityToken setExpires(int tokenTTL) {
+    this.tokenTTL = tokenTTL;
+    return setExpiresAt((getTimeSource().currentTimeMillis() / 1000) + getMaxTokenTTL());
+  }
+
+  /**
+   * Set the expiration time for this token.
+   *
+   * @param expiresAt When this token expires, in seconds since epoch.
+   * @return This security token.
+   */
+  protected AbstractSecurityToken setExpiresAt(Long expiresAt) {
+    this.expiresAt = expiresAt;
+    return this;
+  }
+
+  public String getTrustedJson() {
+    return trustedJson;
+  }
+
+  protected AbstractSecurityToken setTrustedJson(String trustedJson) {
+    this.trustedJson = trustedJson;
+    return this;
+  }
+
+  public boolean isExpired() {
+    try {
+      enforceNotExpired();
+    } catch (BlobExpiredException e) {
+      return true;
+    }
+    return false;
+  }
+
+  public AbstractSecurityToken enforceNotExpired() throws BlobExpiredException {
+    Long expiresAt = getExpiresAt();
+    if (expiresAt != null) {
+      long maxTime = expiresAt + CLOCK_SKEW_ALLOWANCE;
+      long now = getTimeSource().currentTimeMillis() / 1000;
+
+      if (!(now < maxTime)) {
+        throw new BlobExpiredException(now, maxTime);
+      }
+    }
+    return this;
+  }
+
+  public String getActiveUrl() {
+    return activeUrl;
+  }
+
+  protected AbstractSecurityToken setActiveUrl(String activeUrl) {
+    this.activeUrl = activeUrl;
+    return this;
+  }
+
+  /**
+   * A {@link Map} representation of this {@link SecurityToken}.  Implementors that
+   * handle additional keys not contained in {@link Keys} should override and
+   * supplement the functionality of this method.
+   *
+   * @return A map of serialized token values keyed according to {@link Keys}.
+   * @see #getMapKeys()
+   * @see #loadFromMap(Map)
+   */
+  public Map<String, String> toMap() {
+    Map<String, String> map = Maps.newHashMap();
+    for (Keys key : getMapKeys()) {
+      String value = key.getValue(this);
+      if (value != null) {
+        map.put(key.getKey(), key.getValue(this));
+      }
+    }
+    return map;
+  }
+
+  /**
+   * Returns the maximum allowable time (in seconds) for this token to live. Override this method
+   * only if you are internal token that doesn't get serialized via
+   * {@link SecurityTokenCodec#encodeToken(SecurityToken)}, e.g., OAuth state tokens. For all other
+   * cases, the SecurityTokenCodec will handle the time to live of the token.
+   *
+   * @return Maximum allowable time in seconds for a token to live.
+   * @see SecurityTokenCodec#getTokenTimeToLive(String)
+   */
+  protected int getMaxTokenTTL() {
+    return this.tokenTTL;
+  }
+
+  /**
+   * A helper to help load known supported keys from a provided map.
+   *
+   * @param map The map of values.
+   * @see #getMapKeys()
+   * @see #toMap()
+   */
+  protected AbstractSecurityToken loadFromMap(Map<String, String> map) {
+    for (Keys key : getMapKeys()) {
+      key.loadFromMap(this, map);
+    }
+    return this;
+  }
+
+  /**
+   * This method will govern the effectiveness of the protected {@link #toMap()} and
+   * {@link #loadFromMap(SecurityToken, Map)} helper methods.
+   * <br><br>
+   * If your implementation throws an exception on any of the get methods, you
+   * should not include the associated key here, and those values should be handled
+   * in an overridden implementation of {@link #toMap()} if they might contain
+   * useful information.
+   *
+   * @return An EnumSet of the Enums supported by the implementation of this
+   * {@link AbstractSecurityToken}.
+   */
+  protected abstract EnumSet<Keys> getMapKeys();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AnonymousAuthenticationHandler.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AnonymousAuthenticationHandler.java
new file mode 100644
index 0000000..f9d4728
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AnonymousAuthenticationHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Handled Anonymous Authentication, including returning an "anonymous" security token.
+ */
+public class AnonymousAuthenticationHandler implements AuthenticationHandler {
+  public static final String ALLOW_UNAUTHENTICATED = "shindig.allowUnauthenticated";
+  private final boolean allowUnauthenticated;
+
+  @Inject
+  public AnonymousAuthenticationHandler(@Named(ALLOW_UNAUTHENTICATED)
+      boolean allowUnauthenticated) {
+    this.allowUnauthenticated = allowUnauthenticated;
+  }
+
+  public String getName() {
+    return AuthenticationMode.UNAUTHENTICATED.name();
+  }
+
+  public SecurityToken getSecurityTokenFromRequest(HttpServletRequest request) {
+    if (allowUnauthenticated) {
+      return new AnonymousSecurityToken();
+    }
+    return null;
+  }
+
+  public String getWWWAuthenticateHeader(String realm) {
+    return null;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AnonymousSecurityToken.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AnonymousSecurityToken.java
new file mode 100644
index 0000000..eddebe2
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AnonymousSecurityToken.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import java.util.EnumSet;
+
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.config.ContainerConfig;
+
+/**
+ * A special class of Token representing the anonymous viewer/owner
+ */
+public class AnonymousSecurityToken extends AbstractSecurityToken implements SecurityToken {
+
+  /**
+   * The user ID for anonymous users.
+   */
+  public static final String ANONYMOUS_ID = "-1";
+
+  private static final EnumSet<Keys> MAP_KEYS = EnumSet.of(
+    Keys.OWNER, Keys.VIEWER, Keys.APP_URL, Keys.MODULE_ID, Keys.TRUSTED_JSON
+  );
+
+  public AnonymousSecurityToken() {
+    this(ContainerConfig.DEFAULT_CONTAINER);
+  }
+
+  public AnonymousSecurityToken(String container) {
+    this(container, 0L, "");
+  }
+
+  public AnonymousSecurityToken(String container, Long moduleId, String appUrl) {
+    setContainer(container).setModuleId(moduleId).setAppUrl(appUrl)
+      .setOwnerId(ANONYMOUS_ID)
+      .setViewerId(ANONYMOUS_ID)
+      .setDomain("*")
+      .setTrustedJson("");
+  }
+
+  @Override
+  public String getAppId() {
+    return getAppUrl();
+  }
+
+  // Anon Security Tokens have no need to expire
+  @Override
+  protected AbstractSecurityToken setExpires() {
+    return this;
+  }
+
+  // Anon Security Tokens have no need to expire
+  @Override
+  protected AbstractSecurityToken setExpiresAt(Long expiresAt) {
+    return this;
+  }
+
+  public boolean isAnonymous() {
+    return true;
+  }
+
+  public String getUpdatedToken() {
+    return "";
+  }
+
+  public String getAuthenticationMode() {
+    return AuthenticationMode.UNAUTHENTICATED.name();
+  }
+
+  public String getActiveUrl() {
+    throw new UnsupportedOperationException("No active URL available");
+  }
+
+  protected EnumSet<Keys> getMapKeys() {
+    return MAP_KEYS;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthInfo.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthInfo.java
new file mode 100644
index 0000000..f3575ef
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthInfo.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import com.google.inject.Inject;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Wrapper class for AuthInfoUtil to provide injection with in request scope.
+ */
+public class AuthInfo {
+  private final HttpServletRequest req;
+
+  /**
+   * Create AuthInfo from a given HttpServletRequest
+   * @param req
+   */
+  @Inject
+  public AuthInfo(HttpServletRequest req) {
+    this.req = req;
+  }
+
+  /** Export attribute names in current name space */
+  public static class Attribute {
+    public static final AuthInfoUtil.Attribute SECURITY_TOKEN =
+        AuthInfoUtil.Attribute.SECURITY_TOKEN;
+    public static final AuthInfoUtil.Attribute AUTH_TYPE =
+        AuthInfoUtil.Attribute.AUTH_TYPE;
+  }
+
+  /**
+   * Get the security token for this request.
+   *
+   * @return The security token
+   */
+  public SecurityToken getSecurityToken() {
+    return AuthInfoUtil.getSecurityTokenFromRequest(req);
+  }
+
+  /**
+   * Get the hosted domain for this request.
+   *
+   * @return The domain, or {@code null} if no domain was found
+   */
+  public String getAuthType() {
+    return AuthInfoUtil.getAuthTypeFromRequest(req);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthInfoUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthInfoUtil.java
new file mode 100644
index 0000000..f2f07fc
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthInfoUtil.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Class to get authorization information on a servlet request.
+ *
+ * Information is set by adding an AuthentiationServletFilter, and there
+ * is no way to set in a public API. This can be added in the future for testing
+ * purposes.
+ */
+public final class AuthInfoUtil {
+  private AuthInfoUtil() {}
+
+  /**
+   * Constants for request attribute keys
+   */
+  @VisibleForTesting
+  public enum Attribute {
+    /** The security token */
+    SECURITY_TOKEN,
+    /** The named auth type */
+    AUTH_TYPE;
+
+    public String getId() {
+      return Attribute.class.getName() + '.' + this.name();
+    }
+  }
+
+  /**
+   * Get the security token for this request.
+   *
+   * @return The security token
+   */
+  public static SecurityToken getSecurityTokenFromRequest(HttpServletRequest req) {
+    return getRequestAttribute(req, Attribute.SECURITY_TOKEN);
+  }
+
+  /**
+   * Get the hosted domain for this request.
+   *
+   * @return The domain, or {@code null} if no domain was found
+   */
+  public static String getAuthTypeFromRequest(HttpServletRequest req) {
+    return getRequestAttribute(req, Attribute.AUTH_TYPE);
+  }
+
+  /**
+   * Set the security token for the request.
+   *
+   * @param req The request object
+   * @param token The security token
+   */
+  public static void setSecurityTokenForRequest(HttpServletRequest req, SecurityToken token) {
+    setRequestAttribute(req, Attribute.SECURITY_TOKEN, token);
+  }
+
+  /**
+   * Set the auth type for the request.
+   *
+   * @param req The request object
+   * @param authType The named auth type
+   */
+  public static void setAuthTypeForRequest(HttpServletRequest req, String authType) {
+    setRequestAttribute(req, Attribute.AUTH_TYPE, authType);
+  }
+
+  /**
+   * Set a standard request attribute.
+   *
+   * @param req The request
+   * @param att The attribute
+   * @param value The value
+   */
+  private static<T> void setRequestAttribute(HttpServletRequest req, Attribute att, T value) {
+    req.setAttribute(att.getId(), value);
+  }
+
+  /**
+   * Get a standard attribute
+   *
+   * @param req The request
+   * @param att The attribute
+   * @return The value
+   */
+  @SuppressWarnings("unchecked")
+  private static<T> T getRequestAttribute(HttpServletRequest req, Attribute att) {
+    return (T)req.getAttribute(att.getId());
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationHandler.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationHandler.java
new file mode 100644
index 0000000..5541332
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationHandler.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Implements a specific authentication mechanism and produces a SecurityToken when authentication
+ * is successful.
+ */
+public interface AuthenticationHandler {
+
+  /**
+   * Some authentication handlers need to read the request body to perform verification. Because
+   * the servlet stream can only be read once, making the content unavailable to the receiving
+   * servlet. An authentication handler that fully reads the body should stash the raw content
+   * byte array using request.setAttribute(STASHED_BODY, <body byte array>)
+   */
+  public static final String STASHED_BODY = "STASHED_BODY";
+
+  /**
+   * @return The name of the authentication handler. This value is bound to the security token
+   * and can be used to determine the authentication mechanism by which the security token was
+   * created. The value is expected to be one of the values in AuthenticationMode but string
+   * is used here to allow containers to have custom authentication modes
+   */
+  String getName();
+
+  /**
+   * Produce a security token extracted from the HTTP request.
+   *
+   * @param request The request to extract a token from.
+   * @return A valid security token for the request, or null if it wasn't possible to authenticate.
+   */
+  SecurityToken getSecurityTokenFromRequest(HttpServletRequest request)
+      throws InvalidAuthenticationException;
+
+    /**
+     * Return a String to be used for a WWW-Authenticate header. This will be called if the
+     * call to getSecurityTokenFromRequest returns null.
+     *
+     * If non-null/non-blank it will be added to the Response.
+     * See Section 6.1.3 of the Portable Contacts Specification
+     *
+     * @param realm the name of the realm to use for the authenticate header
+     * @return Header value for a WWW-Authenticate Header
+     */
+  String getWWWAuthenticateHeader(String realm);
+
+  /**
+   * An exception thrown by an AuthenticationHandler in the situation where
+   * a malformed credential or token is passed. A handler which throws this exception
+   * is required to include the appropriate error state in the servlet response
+   */
+  public static final class InvalidAuthenticationException extends Exception {
+
+     private Map<String,String> additionalHeaders;
+     private String redirect;
+
+     /**
+      * @param message Message to output in error response
+      * @param cause Underlying exception
+      */
+     public InvalidAuthenticationException(String message, Throwable cause) {
+       this(message, cause, null, null);
+     }
+
+     /**
+      * @param message Message to output in error response
+      * @param cause Underlying exception
+      * @param additionalHeaders Headers to add to error response
+      * @param redirect URL to redirect to on error
+      */
+     public InvalidAuthenticationException(String message, Throwable cause,
+         Map<String,String> additionalHeaders, String redirect) {
+       super(message, cause);
+       this.additionalHeaders = additionalHeaders;
+       this.redirect = redirect;
+     }
+
+     public Map<String, String> getAdditionalHeaders() {
+       return additionalHeaders;
+     }
+
+     public String getRedirect() {
+       return redirect;
+     }
+   }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationMode.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationMode.java
new file mode 100644
index 0000000..a67588c
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationMode.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+/**
+ * Enumeration of known authentication modes
+ */
+public enum AuthenticationMode {
+
+  /**
+   * The request has no authentication associated with it. Used for anonymous requests
+   */
+  UNAUTHENTICATED,
+
+  /**
+   * Used by rendered gadgets to authenticate calls to the container
+   */
+  SECURITY_TOKEN_URL_PARAMETER,
+
+  /**
+   * A fully validated 3-legged OAuth call by a 3rd party on behalf of a user of the
+   * receiving domain. viewerid should always be available
+   */
+  OAUTH,
+
+  /**
+   * A call by a validated 3rd party on its own behalf. Can emulate a call on behalf of a user
+   * of the receiving domain subject to ACL checking but is not required to do so. viewerid may or
+   * may not be available
+   */
+  OAUTH_CONSUMER_REQUEST,
+
+  /**
+   * The request is from a logged in user of the receiving domain
+   */
+  COOKIE
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationServletFilter.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationServletFilter.java
new file mode 100644
index 0000000..8b17e43
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/AuthenticationServletFilter.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.InjectedFilter;
+
+import com.google.inject.name.Named;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Filter that attempts to authenticate an incoming HTTP request. It uses the guice injected
+ * AuthenticationHandlers to try and generate a SecurityToken from the request. Once it finds a non
+ * null token it passes that on the chain.
+ *
+ * If you wish to add a container specific type of auth system simply register an
+ * additional handler.
+ */
+public class AuthenticationServletFilter extends InjectedFilter {
+  public static final String WWW_AUTHENTICATE_HEADER = "WWW-Authenticate";
+
+  //class name for logging purpose
+  private static final String CLASSNAME = AuthenticationServletFilter.class.getName();
+  private static final Logger LOG = Logger.getLogger(CLASSNAME, MessageKeys.MESSAGES);
+
+  private String realm = "shindig";
+  private List<AuthenticationHandler> handlers;
+
+  @Inject(optional = true)
+  public void setAuthenticationRealm(@Named("shindig.authentication.realm") String realm) {
+    this.realm = realm;
+  }
+
+  @Inject
+  public void setAuthenticationHandlers(List<AuthenticationHandler> handlers) {
+    this.handlers = handlers;
+  }
+
+  public void destroy() { }
+
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) {
+      throw new ServletException("Auth filter can only handle HTTP");
+    }
+
+    HttpServletRequest req = (HttpServletRequest) request;
+    HttpServletResponse resp = (HttpServletResponse) response;
+    String authHeader = null;
+
+    try {
+      for (AuthenticationHandler handler : handlers) {
+        authHeader = handler.getWWWAuthenticateHeader(getRealm(req));
+        SecurityToken token = handler.getSecurityTokenFromRequest(req);
+        if (token != null) {
+          AuthInfoUtil.setAuthTypeForRequest(req, handler.getName());
+          AuthInfoUtil.setSecurityTokenForRequest(req, token);
+          callChain(chain, req, resp);
+          return;
+        } else {
+          // Set auth header
+          setAuthHeader(authHeader, resp);
+        }
+      }
+
+      // We did not find a security token so we will just pass null
+      callChain(chain, req, resp);
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      Throwable cause = iae.getCause();
+
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "doFilter", MessageKeys.ERROR_PARSING_SECURE_TOKEN, cause);
+      }
+
+      if (iae.getAdditionalHeaders() != null) {
+        for (Map.Entry<String,String> entry : iae.getAdditionalHeaders().entrySet()) {
+          resp.addHeader(entry.getKey(), entry.getValue());
+        }
+      }
+      if (iae.getRedirect() != null) {
+        onRedirect(req, resp, iae);
+      } else {
+        // Set auth header
+        setAuthHeader(authHeader, resp);
+        onError(req, resp, iae);
+      }
+    }
+  }
+
+  /**
+   * Override this to return container server specific realm.
+   * @return The authentication realm for this server.
+   */
+  protected String getRealm(HttpServletRequest request) {
+    return realm;
+  }
+
+  /**
+   * Override this to perform extra error processing. Headers will have already been set on the
+   * response.
+   *
+   * @param req
+   *          the current http request for this filter
+   * @param resp
+   *          the current http response for this filter
+   * @param iae
+   *          the exception that caused the error path
+   * @throws IOException
+   */
+  protected void onError(HttpServletRequest req, HttpServletResponse resp,
+          AuthenticationHandler.InvalidAuthenticationException iae) throws IOException {
+    Throwable cause = iae.getCause();
+
+    // For now append the cause message if set, this allows us to send any underlying oauth errors
+    String message = (cause == null) ? iae.getMessage() : iae.getMessage() + cause.getMessage();
+    resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, message);
+  }
+
+  /**
+   * Override this to perform extra processing on redirect. Headers will have already been set on
+   * the response.
+   *
+   * @param req
+   *          the current http request for this filter
+   * @param resp
+   *          the current http response for this filter
+   * @param iae
+   *          the exception that caused the redirect path
+   * @throws IOException
+   */
+  protected void onRedirect(HttpServletRequest req, HttpServletResponse resp,
+          AuthenticationHandler.InvalidAuthenticationException iae) throws IOException {
+    resp.sendRedirect(iae.getRedirect());
+  }
+
+  private void setAuthHeader(@Nullable String authHeader, HttpServletResponse response) {
+    if (authHeader != null) {
+      response.addHeader(WWW_AUTHENTICATE_HEADER, authHeader);
+    }
+  }
+
+  private void callChain(FilterChain chain, HttpServletRequest request,
+      HttpServletResponse response) throws IOException, ServletException {
+    if (request.getAttribute(AuthenticationHandler.STASHED_BODY) != null) {
+      chain.doFilter(new StashedBodyRequestwrapper(request), response);
+    } else {
+      chain.doFilter(request, response);
+    }
+  }
+
+  private static class StashedBodyRequestwrapper extends HttpServletRequestWrapper {
+    final InputStream rawStream;
+    ServletInputStream stream;
+    BufferedReader reader;
+
+    StashedBodyRequestwrapper(HttpServletRequest wrapped) {
+      super(wrapped);
+      rawStream = new ByteArrayInputStream(
+          (byte[])wrapped.getAttribute(AuthenticationHandler.STASHED_BODY));
+    }
+
+    @Override
+    public ServletInputStream getInputStream() throws IOException {
+      Preconditions.checkState(reader == null,
+          "The methods getInputStream() and getReader() are mutually exclusive.");
+
+      if (stream == null) {
+        stream = new ServletInputStream() {
+          public int read() throws IOException {
+            return rawStream.read();
+          }
+        };
+      }
+      return stream;
+    }
+
+    @Override
+    public BufferedReader getReader() throws IOException {
+      Preconditions.checkState(stream == null,
+          "The methods getInputStream() and getReader() are mutually exclusive.");
+
+      if (reader == null) {
+        Charset charset = Charset.forName(getCharacterEncoding());
+        if (charset == null) {
+          charset =  Charsets.UTF_8;
+        }
+        reader = new BufferedReader(new InputStreamReader(rawStream, charset));
+      }
+      return reader;
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/BasicSecurityToken.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/BasicSecurityToken.java
new file mode 100644
index 0000000..8df6099
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/BasicSecurityToken.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import java.util.EnumSet;
+
+/**
+ * Primitive token implementation that uses strings as tokens.
+ */
+public class BasicSecurityToken extends AbstractSecurityToken {
+  private static final EnumSet<Keys> SUPPORTED = EnumSet.noneOf(Keys.class);
+
+  public BasicSecurityToken(String owner, String viewer, String app,
+      String domain, String appUrl, String moduleId, String container, String activeUrl, Long expiresAt) {
+    setOwnerId(owner).setViewerId(viewer).setAppId(app).setDomain(domain).setAppUrl(appUrl);
+    if (moduleId != null)
+      setModuleId(Long.parseLong(moduleId));
+    setContainer(container).setActiveUrl(activeUrl).setExpiresAt(expiresAt);
+  }
+
+  public BasicSecurityToken() { }
+
+  public String getUpdatedToken() {
+    return null;
+  }
+
+  public String getAuthenticationMode() {
+    return AuthenticationMode.SECURITY_TOKEN_URL_PARAMETER.name();
+  }
+
+  public boolean isAnonymous() {
+    return false;
+  }
+
+  /* (non-Javadoc)
+   * @see org.apache.shindig.auth.AbstractSecurityToken#getSupportedKeys()
+   *
+   * The codec for this token does not use a BlobCrypter, so we don't need the
+   * toMap and loadFromMap functionality.
+   */
+  protected EnumSet<Keys> getMapKeys() {
+    return SUPPORTED;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/BasicSecurityTokenCodec.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/BasicSecurityTokenCodec.java
new file mode 100644
index 0000000..47f4cd0
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/BasicSecurityTokenCodec.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.config.ContainerConfig;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Maps;
+import com.google.inject.Singleton;
+
+/**
+ * A SecurityTokenCodec implementation that just provides dummy data to satisfy
+ * tests and API calls. Do not use this for any security applications.
+ *
+ * @since 2.0.0
+ */
+@Singleton
+public class BasicSecurityTokenCodec implements SecurityTokenCodec, ContainerConfig.ConfigObserver {
+
+  // Logging
+  private static final String CLASSNAME = BasicSecurityTokenCodec.class.getName();
+  private static final Logger LOG = Logger.getLogger(CLASSNAME);
+
+  private static final int OWNER_INDEX = 0;
+  private static final int VIEWER_INDEX = 1;
+  private static final int APP_ID_INDEX = 2;
+  private static final int DOMAIN_INDEX = 3;
+  private static final int APP_URL_INDEX = 4;
+  private static final int MODULE_ID_INDEX = 5;
+  private static final int CONTAINER_ID_INDEX = 6;
+  private static final int EXPIRY_INDEX = 7; // for back compat, conditionally check later
+  private static final int TOKEN_COUNT = CONTAINER_ID_INDEX + 1;
+  private Map<String, Integer> tokenTTLs = Maps.newHashMap();
+
+  /**
+   * Encodes a token using the a plaintext dummy format.
+   * @param token token to encode
+   * @return token with values separated by colons
+   */
+  public String encodeToken(SecurityToken token) {
+    Long expires = null;
+    Integer tokenTTL = this.tokenTTLs.get(token.getContainer());
+    if (token instanceof AbstractSecurityToken) {
+      if (tokenTTL != null) {
+        ((AbstractSecurityToken) token).setExpires(tokenTTL);
+      } else {
+        ((AbstractSecurityToken) token).setExpires();
+      }
+      expires = token.getExpiresAt();
+    } else {
+      // Quick and dirty token expire calculation.
+      AbstractSecurityToken localToken = new BasicSecurityToken();
+      if (tokenTTL != null) {
+        localToken.setExpires(tokenTTL);
+      } else {
+        localToken.setExpires();
+      }
+      expires = localToken.getExpiresAt();
+    }
+
+    String encoded = Joiner.on(":").join(
+        Utf8UrlCoder.encode(token.getOwnerId()),
+        Utf8UrlCoder.encode(token.getViewerId()),
+        Utf8UrlCoder.encode(token.getAppId()),
+        Utf8UrlCoder.encode(token.getDomain()),
+        Utf8UrlCoder.encode(token.getAppUrl()),
+        Long.toString(token.getModuleId(), 10),
+        Utf8UrlCoder.encode(token.getContainer()));
+
+    if (expires != null) {
+      encoded = Joiner.on(':').join(encoded, Long.toString(expires, 10));
+    }
+
+    return encoded;
+  }
+
+
+  /**
+   * {@inheritDoc}
+   *
+   * Returns a token with some faked out values.
+   */
+  public SecurityToken createToken(Map<String, String> parameters)
+      throws SecurityTokenException {
+
+    final String token = parameters.get(SecurityTokenCodec.SECURITY_TOKEN_NAME);
+    if (token == null || token.trim().length() == 0) {
+      // No token is present, assume anonymous access
+      return new AnonymousSecurityToken();
+    }
+
+    try {
+      String[] tokens = StringUtils.split(token, ':');
+      if (tokens.length < TOKEN_COUNT) {
+        throw new SecurityTokenException("Malformed security token");
+      }
+
+      Long expires = null;
+      if (tokens.length > TOKEN_COUNT && !tokens[EXPIRY_INDEX].equals("")) {
+        expires = Long.parseLong(Utf8UrlCoder.decode(tokens[EXPIRY_INDEX]), 10);
+      }
+
+      BasicSecurityToken basicToken = new BasicSecurityToken(
+          Utf8UrlCoder.decode(tokens[OWNER_INDEX]),
+          Utf8UrlCoder.decode(tokens[VIEWER_INDEX]),
+          Utf8UrlCoder.decode(tokens[APP_ID_INDEX]),
+          Utf8UrlCoder.decode(tokens[DOMAIN_INDEX]),
+          Utf8UrlCoder.decode(tokens[APP_URL_INDEX]),
+          Utf8UrlCoder.decode(tokens[MODULE_ID_INDEX]),
+          Utf8UrlCoder.decode(tokens[CONTAINER_ID_INDEX]),
+          parameters.get(SecurityTokenCodec.ACTIVE_URL_NAME),
+          expires);
+      return basicToken.enforceNotExpired();
+    } catch (BlobCrypterException e) {
+      throw new SecurityTokenException(e);
+    } catch (ArrayIndexOutOfBoundsException e) {
+      throw new SecurityTokenException(e);
+    }
+  }
+
+  public int getTokenTimeToLive() {
+    return AbstractSecurityToken.DEFAULT_MAX_TOKEN_TTL;
+  }
+
+  public int getTokenTimeToLive(String container) {
+    Integer tokenTTL = this.tokenTTLs.get(container);
+    if (tokenTTL == null) {
+      return getTokenTimeToLive();
+    }
+    return tokenTTL;
+  }
+
+  /**
+   * Creates a basic signer
+   */
+  public BasicSecurityTokenCodec() {}
+
+  /**
+   * Creates a basic signer that can observe container configuration changes
+   * @param config the container config to observe
+   */
+  public BasicSecurityTokenCodec(ContainerConfig config) {
+    config.addConfigObserver(this, true);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public void containersChanged(ContainerConfig config, Collection<String> changed,
+          Collection<String> removed) {
+    for (String container : removed) {
+      this.tokenTTLs.remove(container);
+    }
+
+    for (String container : changed) {
+      int tokenTTL = config.getInt(container, SECURITY_TOKEN_TTL_CONFIG);
+      // 0 means the value was not defined or NaN.  0 shouldn't be a valid TTL anyway.
+      if (tokenTTL > 0) {
+        this.tokenTTLs.put(container, tokenTTL);
+      } else {
+        LOG.logp(Level.WARNING, CLASSNAME, "containersChanged",
+                "Token TTL for container \"{0}\" was {1} and will be ignored.",
+                new Object[] { container, tokenTTL });
+      }
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/BlobCrypterSecurityToken.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/BlobCrypterSecurityToken.java
new file mode 100644
index 0000000..7d9b3f5
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/BlobCrypterSecurityToken.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import java.util.EnumSet;
+import java.util.Map;
+/**
+ * Authentication based on a provided BlobCrypter.
+ *
+ * Wire format is "&lt;container&gt;:&lt;encrypted-and-signed-token&gt;"
+ *
+ * Container is included so different containers can use different security tokens if necessary.
+ */
+public class BlobCrypterSecurityToken extends AbstractSecurityToken {
+  private static final EnumSet<Keys> MAP_KEYS = EnumSet.of(
+    Keys.OWNER, Keys.VIEWER, Keys.APP_URL, Keys.MODULE_ID, Keys.EXPIRES, Keys.TRUSTED_JSON
+  );
+
+  /**
+   * Create a new security token.
+   *
+   * @param container container that is issuing the token
+   * @param domain domain to use for signed fetch with default signed fetch key.
+   * @param activeUrl
+   * @param values Other values to init into the token.
+   */
+  public BlobCrypterSecurityToken(String container, String domain, String activeUrl, Map<String, String> values) {
+    if (values != null) {
+      loadFromMap(values);
+    }
+    setContainer(container).setDomain(domain).setActiveUrl(activeUrl);
+  }
+
+  // Our tokens are static, we could change this to periodically update the token.
+  public String getUpdatedToken() {
+    return null;
+  }
+
+  public String getAuthenticationMode() {
+    return AuthenticationMode.SECURITY_TOKEN_URL_PARAMETER.name();
+  }
+
+  public boolean isAnonymous() {
+    return false;
+  }
+
+  // Legacy value for signed fetch, opensocial 0.8 prefers opensocial_app_url
+  @Override
+  public String getAppId() {
+    return getAppUrl();
+  }
+
+  protected EnumSet<Keys> getMapKeys() {
+    return MAP_KEYS;
+  }
+
+  public static BlobCrypterSecurityToken fromToken(SecurityToken token) {
+    BlobCrypterSecurityToken interpretedToken = new BlobCrypterSecurityToken(token.getContainer(),
+        token.getDomain(), token.getActiveUrl(), null);
+    interpretedToken
+        .setAppId(token.getAppId())
+        .setAppUrl(token.getAppUrl())
+        .setExpiresAt(token.getExpiresAt())
+        .setModuleId(token.getModuleId())
+        .setOwnerId(token.getOwnerId())
+        .setTrustedJson(token.getTrustedJson())
+        .setViewerId(token.getViewerId());
+
+    return interpretedToken;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/BlobCrypterSecurityTokenCodec.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/BlobCrypterSecurityTokenCodec.java
new file mode 100644
index 0000000..a17e75b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/BlobCrypterSecurityTokenCodec.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.config.ContainerConfig;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Provides security token decoding services.  Configuration is via containers.js.  Each container
+ * should specify (or inherit)
+ *
+ * securityTokenKeyFile: path to file containing a key to use for verifying tokens.
+ * signedFetchDomain: oauth_consumer_key value to use for signed fetch using default key.
+ *
+ * Creating a key is best done with a command line like this:
+ * <pre>
+ *     dd if=/dev/random bs=32 count=1  | openssl base64 > /tmp/key.txt
+ * </pre>
+ * Wire format is "&lt;container&gt;:&lt;encrypted-and-signed-token&gt;"
+ *
+ * @since 2.0.0
+ */
+@Singleton
+public class BlobCrypterSecurityTokenCodec implements SecurityTokenCodec, ContainerConfig.ConfigObserver {
+
+  // Logging
+  private static final String CLASSNAME = BlobCrypterSecurityTokenCodec.class.getName();
+  private static final Logger LOG = Logger.getLogger(CLASSNAME);
+
+  public static final String SECURITY_TOKEN_KEY = "gadgets.securityTokenKey";
+
+  public static final String SIGNED_FETCH_DOMAIN = "gadgets.signedFetchDomain";
+
+  /**
+   * Keys are container ids, values are crypters
+   */
+  protected Map<String, BlobCrypter> crypters = Maps.newHashMap();
+
+  /**
+   * Keys are container ids, values are domains used for signed fetch.
+   */
+  protected Map<String, String> domains = Maps.newHashMap();
+
+  private Map<String, Integer> tokenTTLs = Maps.newHashMap();
+
+  @Inject
+  public BlobCrypterSecurityTokenCodec(ContainerConfig config) {
+    try {
+      config.addConfigObserver(this, false);
+      loadContainers(config, config.getContainers(), crypters, domains, tokenTTLs);
+    } catch (IOException e) {
+      // Someone specified securityTokenKeyFile, but we couldn't load the key.  That merits killing
+      // the server.
+      LOG.log(Level.SEVERE, "Error while initializing BlobCrypterSecurityTokenCodec", e);
+      throw new RuntimeException(e);
+    }
+  }
+
+  public void containersChanged(
+      ContainerConfig config, Collection<String> changed, Collection<String> removed) {
+    Map<String, BlobCrypter> newCrypters = Maps.newHashMap(crypters);
+    Map<String, String> newDomains = Maps.newHashMap(domains);
+    Map<String, Integer> newTokenTTLs = Maps.newHashMap(tokenTTLs);
+    try {
+      loadContainers(config, changed, newCrypters, newDomains, newTokenTTLs);
+      for (String container : removed) {
+        newCrypters.remove(container);
+        newDomains.remove(container);
+        newTokenTTLs.remove(container);
+      }
+    } catch (IOException e) {
+      // Someone specified securityTokenKeyFile, but we couldn't load the key.
+      // Keep the old configuration.
+      LOG.log(Level.WARNING, "There was an error loading an updated container configuration. "
+          + "Keeping old configuration.", e);
+      return;
+    }
+    crypters = newCrypters;
+    domains = newDomains;
+    tokenTTLs = newTokenTTLs;
+  }
+
+  private void loadContainers(ContainerConfig config, Collection<String> containers,
+          Map<String, BlobCrypter> crypters, Map<String, String> domains,
+          Map<String, Integer> tokenTTLs) throws IOException {
+    for (String container : containers) {
+      String key = config.getString(container, SECURITY_TOKEN_KEY);
+      if (key != null) {
+        BlobCrypter crypter = loadCrypter(key);
+        crypters.put(container, crypter);
+      }
+      String domain = config.getString(container, SIGNED_FETCH_DOMAIN);
+      domains.put(container, domain);
+
+      // Process tokenTTLs
+      int tokenTTL = config.getInt(container, SECURITY_TOKEN_TTL_CONFIG);
+      // 0 means the value was not defined or NaN.  0 shouldn't be a valid TTL anyway.
+      if (tokenTTL > 0) {
+        tokenTTLs.put(container, tokenTTL);
+      } else {
+        LOG.logp(Level.WARNING, CLASSNAME, "loadContainers",
+                "Token TTL for container \"{0}\" was {1} and will be ignored.",
+                new Object[] { container, tokenTTL });
+      }
+    }
+  }
+
+  /**
+   * Load a BlobCrypter using the specified key.  Override this if you have your own BlobCrypter
+   * implementation.
+   *
+   * @param key The security token key.
+   * @return The BlobCrypter.
+   */
+  protected BlobCrypter loadCrypter(String key) {
+    return new BasicBlobCrypter(key);
+  }
+
+  /**
+   * Decrypt and verify the provided security token.
+   */
+  public SecurityToken createToken(Map<String, String> tokenParameters)
+      throws SecurityTokenException {
+    String token = tokenParameters.get(SecurityTokenCodec.SECURITY_TOKEN_NAME);
+    if (StringUtils.isBlank(token)) {
+      // No token is present, assume anonymous access
+      return new AnonymousSecurityToken();
+    }
+    String[] fields = StringUtils.split(token, ':');
+    if (fields.length != 2) {
+      throw new SecurityTokenException("Invalid security token " + token);
+    }
+    String container = fields[0];
+    BlobCrypter crypter = crypters.get(container);
+    if (crypter == null) {
+      throw new SecurityTokenException("Unknown container " + token);
+    }
+    String domain = domains.get(container);
+    String activeUrl = tokenParameters.get(SecurityTokenCodec.ACTIVE_URL_NAME);
+    String crypted = fields[1];
+    try {
+      BlobCrypterSecurityToken st = new BlobCrypterSecurityToken(container, domain, activeUrl,
+          crypter.unwrap(crypted));
+      return st.enforceNotExpired();
+    } catch (BlobCrypterException e) {
+      throw new SecurityTokenException(e);
+    }
+  }
+
+  /**
+   * Encrypt and sign the token.  The returned value is *not* web safe, it should be URL
+   * encoded before being used as a form parameter.
+   */
+  public String encodeToken(SecurityToken token) throws SecurityTokenException {
+    if (!token.getAuthenticationMode().equals(
+            AuthenticationMode.SECURITY_TOKEN_URL_PARAMETER.name())) {
+      throw new SecurityTokenException("Can only encode BlobCrypterSecurityTokens");
+    }
+
+    // Test code sends in real AbstractTokens, they have modified time sources in them so
+    // that we can test token expiration, production tokens are proxied via the SecurityToken interface.
+    AbstractSecurityToken aToken = token instanceof AbstractSecurityToken ?
+        (AbstractSecurityToken)token : BlobCrypterSecurityToken.fromToken(token);
+
+    BlobCrypter crypter = crypters.get(aToken.getContainer());
+    if (crypter == null) {
+      throw new SecurityTokenException("Unknown container " + aToken.getContainer());
+    }
+
+    try {
+      Integer tokenTTL = this.tokenTTLs.get(aToken.getContainer());
+      if (tokenTTL != null) {
+        aToken.setExpires(tokenTTL);
+      } else {
+        aToken.setExpires();
+      }
+      return aToken.getContainer() + ':' + crypter.wrap(aToken.toMap());
+    } catch (BlobCrypterException e) {
+      throw new SecurityTokenException(e);
+    }
+  }
+
+  public int getTokenTimeToLive() {
+    return AbstractSecurityToken.DEFAULT_MAX_TOKEN_TTL;
+  }
+
+  public int getTokenTimeToLive(String container) {
+    Integer tokenTTL = this.tokenTTLs.get(container);
+    if (tokenTTL == null) {
+      return getTokenTimeToLive();
+    }
+    return tokenTTL;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/DefaultSecurityTokenCodec.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/DefaultSecurityTokenCodec.java
new file mode 100644
index 0000000..177aed6
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/DefaultSecurityTokenCodec.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.shindig.config.ContainerConfig;
+
+import java.util.Map;
+
+/**
+ * Default implementation of security tokens.  Decides based on default container configuration
+ * whether to use real crypto for security tokens or to use a simple insecure implementation that
+ * is useful for testing.
+ *
+ * Example configuration in container.js for insecure security tokens:
+ *    gadgets.securityTokenType = insecure
+ *
+ * Example configuration in container.js for blob crypter based security tokens:
+ *    gadgets.securityTokenType = secure
+ *
+ * The insecure implementation is BasicSecurityTokenCodec.
+ *
+ * The secure implementation is BlobCrypterSecurityTokenCodec.
+ *
+ * @since 2.0.0
+ */
+@Singleton
+public class DefaultSecurityTokenCodec implements SecurityTokenCodec {
+  private static final String SECURITY_TOKEN_TYPE = "gadgets.securityTokenType";
+
+  private final SecurityTokenCodec codec;
+
+  @Inject
+  public DefaultSecurityTokenCodec(ContainerConfig config) {
+    String tokenType = config.getString(ContainerConfig.DEFAULT_CONTAINER, SECURITY_TOKEN_TYPE);
+
+    if ("insecure".equals(tokenType)) {
+      codec = new BasicSecurityTokenCodec(config);
+    } else if ("secure".equals(tokenType)) {
+      codec = new BlobCrypterSecurityTokenCodec(config);
+    } else {
+      throw new RuntimeException("Unknown security token type specified in " +
+          ContainerConfig.DEFAULT_CONTAINER + " container configuration. " +
+          SECURITY_TOKEN_TYPE + ": " + tokenType);
+    }
+  }
+
+  public SecurityToken createToken(Map<String, String> tokenParameters)
+      throws SecurityTokenException {
+    return codec.createToken(tokenParameters);
+  }
+
+  public String encodeToken(SecurityToken token) throws SecurityTokenException {
+    if (token == null) {
+      return null;
+    }
+    return codec.encodeToken(token);
+  }
+
+  @VisibleForTesting
+  protected SecurityTokenCodec getCodec() {
+    return codec;
+  }
+
+  public int getTokenTimeToLive() {
+    return codec.getTokenTimeToLive();
+  }
+
+  public int getTokenTimeToLive(String container) {
+    return codec.getTokenTimeToLive(container);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/ForwardingSecurityToken.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/ForwardingSecurityToken.java
new file mode 100644
index 0000000..fee9dcf
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/ForwardingSecurityToken.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+/**
+ * A SecurityToken that forwards all methods calls to another token. Subclasses should override
+ * one or more methods to change or add behavior.
+ *
+ * @since 2.0.0
+ */
+public abstract class ForwardingSecurityToken implements SecurityToken {
+  SecurityToken delegate;
+  protected ForwardingSecurityToken(SecurityToken delegate) {
+    this.delegate = delegate;
+  }
+
+  public String getOwnerId() {
+    return delegate.getOwnerId();
+  }
+
+  public String getViewerId() {
+    return delegate.getViewerId();
+  }
+
+  public String getAppId() {
+    return delegate.getAppId();
+  }
+
+  public String getDomain() {
+    return delegate.getDomain();
+  }
+
+  public String getContainer() {
+    return delegate.getContainer();
+  }
+
+  public String getAppUrl() {
+    return delegate.getAppUrl();
+  }
+
+  public long getModuleId() {
+    return delegate.getModuleId();
+  }
+
+  public Long getExpiresAt() {
+    return delegate.getExpiresAt();
+  }
+
+  public boolean isExpired() {
+    return delegate.isExpired();
+  }
+
+  public String getUpdatedToken() {
+    return delegate.getUpdatedToken();
+  }
+
+  public String getAuthenticationMode() {
+    return delegate.getAuthenticationMode();
+  }
+
+  public String getTrustedJson() {
+    return delegate.getTrustedJson();
+  }
+
+  public boolean isAnonymous() {
+    return delegate.isAnonymous();
+  }
+
+  public String getActiveUrl() {
+    return delegate.getActiveUrl();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java
new file mode 100644
index 0000000..08fc4f6
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthConstants.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+/**
+ * OAuth constants not found in the upstream OAuth library
+ */
+public final class OAuthConstants {
+  private OAuthConstants() {}
+  public static final String OAUTH_SESSION_HANDLE = "oauth_session_handle";
+  public static final String OAUTH_EXPIRES_IN = "oauth_expires_in";
+  public static final String OAUTH_BODY_HASH = "oauth_body_hash";
+
+  public static final String PROBLEM_ACCESS_TOKEN_EXPIRED = "access_token_expired";
+  public static final String PROBLEM_PARAMETER_MISSING = "parameter_missing";
+  public static final String PROBLEM_TOKEN_INVALID = "token_invalid";
+  public static final String PROBLEM_BAD_VERIFIER = "bad_verifier";
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthUtil.java
new file mode 100644
index 0000000..333ef5a
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/OAuthUtil.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthException;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuth.Parameter;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Wrapper for the OAuth.net utility functions.  Some of them are declared as throwing IOException
+ * for cases that are extremely unlikely to happen.  We turn those IOExceptions in to
+ * RuntimeExceptions since the caller can't do anything about them anyway.
+ */
+public final class OAuthUtil {
+  private OAuthUtil() {}
+
+  public static String getParameter(OAuthMessage message, String name) {
+    try {
+      return message.getParameter(name);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static List<Entry<String, String>> getParameters(OAuthMessage message) {
+    try {
+      return message.getParameters();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static String formEncode(Iterable<? extends Entry<String, String>> parameters) {
+    try {
+      return OAuth.formEncode(parameters);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static String addParameters(String url, List<Entry<String, String>> parameters) {
+    try {
+      return OAuth.addParameters(url, parameters);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static OAuthMessage newRequestMessage(OAuthAccessor accessor, String method, String url,
+      List<Parameter> parameters) throws OAuthException {
+    try {
+      return accessor.newRequestMessage(method, url, parameters);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    } catch (URISyntaxException e) {
+      throw new OAuthException(e);
+    }
+  }
+
+  public static enum SignatureType {
+    URL_ONLY,
+    URL_AND_FORM_PARAMS,
+    URL_AND_BODY_HASH,
+  }
+
+  /**
+   * @param tokenEndpoint true if this is a request token or access token request.  We don't check
+   * oauth_body_hash on those.
+   */
+  public static SignatureType getSignatureType(boolean tokenEndpoint, String contentType) {
+    if (OAuth.isFormEncoded(contentType)) {
+      return SignatureType.URL_AND_FORM_PARAMS;
+    }
+    if (tokenEndpoint) {
+      return SignatureType.URL_ONLY;
+    }
+    return SignatureType.URL_AND_BODY_HASH;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityToken.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityToken.java
new file mode 100644
index 0000000..19b936f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityToken.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+/**
+ * An abstract representation of a signing token.
+ * Use in conjunction with @code SecurityTokenCodec.
+ */
+public interface SecurityToken {
+
+  /**
+   * @return the owner from the token, or null if there is none.
+   */
+  String getOwnerId();
+
+  /**
+   * @return the viewer from the token, or null if there is none.
+   */
+  String getViewerId();
+
+  /**
+   * @return the application id from the token, or null if there is none.
+   */
+  String getAppId();
+
+  /**
+   * @return the domain from the token, or null if there is none.
+   */
+  String getDomain();
+
+  /**
+   * @return The container.
+   */
+  String getContainer();
+
+  /**
+   * @return the URL of the application
+   */
+  String getAppUrl();
+
+  /**
+   * @return the module ID of the application
+   */
+  long getModuleId();
+
+  /**
+   * @return The time in seconds since epoc that this token expires or
+   *   <code>null</code> if unknown or indeterminate.
+   */
+  Long getExpiresAt();
+
+  /**
+   * @return true if the token is no longer valid.
+   */
+  boolean isExpired();
+
+  /**
+   * @return an updated version of the token to return to the gadget, or null
+   * if there is no need to update the token.
+   */
+  String getUpdatedToken();
+
+  /**
+   * @return the authentication mechanism used to generate this security token
+   * @see AuthenticationMode
+   */
+  String getAuthenticationMode();
+
+  /**
+   * @return a string formatted JSON object from the container, or null if there
+   * is no JSON from the container.
+   */
+  String getTrustedJson();
+
+  /**
+   * @return true if the token is for an anonymous viewer/owner
+   */
+  boolean isAnonymous();
+
+  /**
+   * @return the URL being used by the current request and null if not specified.
+   *
+   * The returned URL must contain at least protocol, host, and port.
+   *
+   * The returned URL may contain path or query parameters.
+   *
+   */
+  String getActiveUrl();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityTokenCodec.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityTokenCodec.java
new file mode 100644
index 0000000..36a6d19
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityTokenCodec.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.Map;
+
+/**
+ *  Handles verification of gadget security tokens.
+ *
+ * @since 2.0.0
+ */
+@ImplementedBy(DefaultSecurityTokenCodec.class)
+public interface SecurityTokenCodec {
+
+  /**
+   * The security token value must be passed on a map value referenced by this key. Additional
+   * parameters can be passed as seen fit.
+   */
+  String SECURITY_TOKEN_NAME = "token";
+
+  /**
+   * Active URL for the request.  Must include protocol, host, and port.  May include path
+   * and may include query.
+   */
+  String ACTIVE_URL_NAME = "activeUrl";
+
+  /**
+   * The configuration parameter for security token time-to-lives.
+   */
+  String SECURITY_TOKEN_TTL_CONFIG = "gadgets.securityTokenTTL";
+
+  /**
+   * Decrypts and verifies a gadget security token to return a gadget token.
+   *
+   * @param tokenParameters Map containing a entry 'token' in wire format (probably encrypted.)
+   * @return the decrypted and verified token.
+   * @throws SecurityTokenException If tokenString is not a valid token
+   */
+  SecurityToken createToken(Map<String, String> tokenParameters)
+      throws SecurityTokenException;
+
+  String encodeToken(SecurityToken token) throws SecurityTokenException;
+
+  /**
+   * This method is deprecated in favor of {@link SecurityTokenCodec#getTokenTimeToLive(String)}.
+   * Implementations should only rely on this method to return the default time-to-live of tokens
+   * generated by this codec in the case where <code>getTokenTimeToLive(String)</code> fails.
+   *
+   * @return The default amount of time a token generated by this codec can be expected to live.
+   * @see SecurityTokenCodec#getTokenTimeToLive(String)
+   */
+  @Deprecated
+  int getTokenTimeToLive();
+
+  /**
+   * @param container
+   *          The container the token is for
+   * @return The amount of time a token generated by this codec within the given container can be
+   *         expected to live.
+   */
+  int getTokenTimeToLive(String container);
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityTokenException.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityTokenException.java
new file mode 100644
index 0000000..a037b9d
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/SecurityTokenException.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+/**
+ * Exceptions thrown by SecurityTokenCodec implementations.
+ */
+public class SecurityTokenException extends Exception {
+  public SecurityTokenException(String message) {
+    super(message);
+  }
+  public SecurityTokenException(Exception cause) {
+    super(cause);
+  }
+  public SecurityTokenException(String message, Exception cause) {
+    super(message, cause);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/auth/UrlParameterAuthenticationHandler.java b/trunk/java/common/src/main/java/org/apache/shindig/auth/UrlParameterAuthenticationHandler.java
new file mode 100644
index 0000000..9865bf1
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/auth/UrlParameterAuthenticationHandler.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import net.oauth.OAuth;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Produces security tokens by extracting the "st" parameter from the request url or post body.
+ */
+public class UrlParameterAuthenticationHandler implements AuthenticationHandler {
+  private static final String SECURITY_TOKEN_PARAM = "st";
+
+  private final SecurityTokenCodec securityTokenCodec;
+  private final Boolean oauthSSLrequired;
+
+  @Inject
+  public UrlParameterAuthenticationHandler(SecurityTokenCodec securityTokenCodec,
+                                           @Named("org.apache.shindig.auth.oauth2-require-ssl")
+                                           Boolean oauthSSLrequired) {
+    this.securityTokenCodec = securityTokenCodec;
+    this.oauthSSLrequired = oauthSSLrequired;
+  }
+
+  public String getName() {
+    return AuthenticationMode.SECURITY_TOKEN_URL_PARAMETER.name();
+  }
+
+  public SecurityToken getSecurityTokenFromRequest(HttpServletRequest request)
+      throws InvalidAuthenticationException {
+    Map<String, String> parameters = getMappedParameters(request);
+    try {
+      if (parameters.get(SecurityTokenCodec.SECURITY_TOKEN_NAME) == null) {
+        return null;
+      }
+      return securityTokenCodec.createToken(parameters);
+    } catch (SecurityTokenException e) {
+      throw new InvalidAuthenticationException("Malformed security token " +
+          parameters.get(SecurityTokenCodec.SECURITY_TOKEN_NAME), e);
+    }
+  }
+
+  public String getWWWAuthenticateHeader(String realm) {
+    return null;
+  }
+
+  protected SecurityTokenCodec getSecurityTokenCodec() {
+    return this.securityTokenCodec;
+  }
+
+  private static final Pattern AUTHORIZATION_REGEX = Pattern.compile("\\s*OAuth2\\s+(\\S*)\\s*.*");
+
+  protected Map<String, String> getMappedParameters(final HttpServletRequest request) {
+    Map<String, String> params = Maps.newHashMap();
+    boolean isSecure = this.oauthSSLrequired ? request.isSecure() : true;
+
+    // old style security token
+    String token = request.getParameter(SECURITY_TOKEN_PARAM);
+
+    // OAuth2 token as a param
+    // NOTE: if oauth_signature_method is present then we have a OAuth 1.0 request
+    // See OAuth 2.0 Bearer Tokens Draft 01 -- 2.3  URI Query Parameter
+    // http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-01
+    if (token == null && isSecure && request.getParameter(OAuth.OAUTH_SIGNATURE_METHOD) == null) {
+      token = request.getParameter(OAuth.OAUTH_TOKEN);
+    }
+
+    // token in authorization header
+    // See OAuth 2.0 Bearer Tokens Draft 01 -- 2.1 The Authorization Request Header Field
+   if (token == null && isSecure) {
+      for (Enumeration<String> headers = request.getHeaders("Authorization"); headers != null && headers.hasMoreElements();) {
+        String authorization = headers.nextElement();
+        if (authorization != null) {
+          Matcher m = AUTHORIZATION_REGEX.matcher(authorization);
+          if (m.matches()) {
+            token = m.group(1);
+          }
+        }
+      }
+    }
+
+    // no token yet, see if it was attached as a header
+    if (StringUtils.isEmpty(token)) {
+      String t = request.getHeader( "X-Shindig-ST" );
+      if (StringUtils.isNotBlank(t)) {
+        token = t;
+      }
+    }
+
+    params.put(SecurityTokenCodec.SECURITY_TOKEN_NAME, token);
+    params.put(SecurityTokenCodec.ACTIVE_URL_NAME, getActiveUrl(request));
+    return params;
+  }
+
+  protected String getActiveUrl(HttpServletRequest request) {
+    return request.getRequestURL().toString();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/JsonProperty.java b/trunk/java/common/src/main/java/org/apache/shindig/common/JsonProperty.java
new file mode 100644
index 0000000..3385a0b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/JsonProperty.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for specifying a property name other than the default when using JsonSerializer.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface JsonProperty {
+  String value();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java b/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java
new file mode 100644
index 0000000..a6c74b5
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/JsonSerializer.java
@@ -0,0 +1,457 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.joda.time.DateTime;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import com.google.common.collect.Multimap;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Serializes a JSONObject.
+ *
+ * The methods here are designed to be substantially more CPU and memory efficient than those found
+ * in org.json or net.sf.json. In profiling, the performance of both of these libraries has been
+ * found to be woefully inadequate for large scale deployments.
+ *
+ * The append*() methods can be used to serialize directly into an Appendable, such as an output
+ * stream. This avoids unnecessary copies to intermediate objects.
+ *
+ * To reduce output size, null values in json arrays and objects will always be removed.
+ */
+public final class JsonSerializer {
+  // Multiplier to use for allocating the buffer.
+  private static final int BASE_MULTIPLIER = 256;
+
+  private static final char[] HEX_DIGITS = {
+    '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'
+  };
+
+  private JsonSerializer() {}
+
+  public static String serialize(Object object) {
+    StringBuilder buf = new StringBuilder(1024);
+    try {
+      append(buf, object);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Serialize a JSONObject. Does not guard against cyclical references.
+   */
+  public static String serialize(JSONObject object) {
+    StringBuilder buf = new StringBuilder(object.length() * BASE_MULTIPLIER);
+    try {
+      appendJsonObject(buf, object);
+    } catch (IOException e) {
+      // Shouldn't ever happen unless someone adds something to append*.
+      throw new RuntimeException(e);
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Serializes a Map as a JSON object. Does not guard against cyclical references.
+   */
+  public static String serialize(Map<String, ?> map) {
+    StringBuilder buf = new StringBuilder(map.size() * BASE_MULTIPLIER);
+    try {
+      appendMap(buf, map);
+    } catch (IOException e) {
+      // Shouldn't ever happen unless someone adds something to append*.
+      throw new RuntimeException(e);
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Serializes a Collection as a JSON array. Does not guard against cyclical references.
+   */
+  public static String serialize(Collection<?> collection) {
+    StringBuilder buf = new StringBuilder(collection.size() * BASE_MULTIPLIER);
+    try {
+      appendCollection(buf, collection);
+    } catch (IOException e) {
+      // Shouldn't ever happen unless someone adds something to append*.
+      throw new RuntimeException(e);
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Serializes an array as a JSON array. Does not guard against cyclical references
+   */
+  public static String serialize(Object[] array) {
+    StringBuilder buf = new StringBuilder(array.length * BASE_MULTIPLIER);
+    try {
+      appendArray(buf, array);
+    } catch (IOException e) {
+      // Shouldn't ever happen unless someone adds something to append*.
+      throw new RuntimeException(e);
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Serializes a JSON array. Does not guard against cyclical references
+   */
+  public static String serialize(JSONArray array) {
+    StringBuilder buf = new StringBuilder(array.length() * BASE_MULTIPLIER);
+    try {
+      appendJsonArray(buf, array);
+    } catch (IOException e) {
+      // Shouldn't ever happen unless someone adds something to append*.
+      throw new RuntimeException(e);
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Appends a value to the buffer.
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  @SuppressWarnings("unchecked")
+  public static void append(Appendable buf, Object value) throws IOException {
+    if (value == null || value == JSONObject.NULL) {
+      buf.append("null");
+    } else if (value instanceof Number ||
+               value instanceof Boolean) {
+      // Primitives
+      buf.append(value.toString());
+    } else if (value instanceof CharSequence ||
+               value instanceof DateTime ||
+               value instanceof Locale ||
+               value instanceof Uri ||
+               value.getClass().isEnum()) {
+      // String-like Primitives
+      appendString(buf, value.toString());
+    } else if (value instanceof Date) {
+      appendString(buf, DateUtil.formatIso8601Date((Date)value));
+    } else if (value instanceof JSONObject) {
+      appendJsonObject(buf, (JSONObject) value);
+    } else if (value instanceof JSONArray) {
+      appendJsonArray(buf, (JSONArray) value);
+    } else if (value instanceof Map) {
+      appendMap(buf, (Map<String, Object>) value);
+    } else if (value instanceof Multimap) {
+      appendMultimap(buf, (Multimap<String, Object>) value);
+    } else if (value instanceof Collection) {
+      appendCollection(buf, (Collection<Object>) value);
+    } else if (value.getClass().isArray()) {
+      appendArray(buf, (Object[]) value);
+    } else {
+      // Try getter conversion
+      appendPojo(buf, value);
+    }
+  }
+
+  /**
+   * Appends a java object using getters
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  public static void appendPojo(Appendable buf, Object pojo) throws IOException {
+    Map<String, Method> methods = JsonUtil.getGetters(pojo);
+    buf.append('{');
+    boolean firstDone = false;
+    for (Map.Entry<String, Method> entry : methods.entrySet()) {
+      try {
+        Object value = entry.getValue().invoke(pojo);
+        if (value != null) {
+          String attribute = entry.getKey();
+
+          // Common use case isOwner/isViewer should not be set unless true
+          if (!(("isOwner".equals(attribute) || "isViewer".equals(attribute)) && value.equals(Boolean.FALSE))) {
+            // Drop null values.
+            if (firstDone) {
+              buf.append(',');
+            } else {
+              firstDone = true;
+            }
+            appendString(buf, attribute);
+            buf.append(':');
+            append(buf, value);
+          }
+        }
+      } catch (IllegalArgumentException e) {
+        // Shouldn't be possible.
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        // Bad class.
+        throw new RuntimeException(e);
+      } catch (InvocationTargetException e) {
+        // Bad class.
+        throw new RuntimeException(e);
+      }
+    }
+    buf.append('}');
+  }
+
+  /**
+   * Appends an array to the buffer.
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  public static void appendArray(Appendable buf, Object[] array) throws IOException {
+    buf.append('[');
+    boolean firstDone = false;
+    for (Object o : array) {
+      if (o != null) {
+        if (firstDone) {
+          buf.append(',');
+        } else {
+          firstDone = true;
+        }
+        append(buf, o);
+      }
+    }
+    buf.append(']');
+  }
+
+  /**
+   * Append a JSONArray to the buffer.
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  public static void appendJsonArray(Appendable buf, JSONArray array) throws IOException {
+    buf.append('[');
+    boolean firstDone = false;
+    for (int i = 0, j = array.length(); i < j; ++i) {
+      Object value = array.opt(i);
+      if (value != null) {
+        if (firstDone) {
+          buf.append(',');
+        } else {
+          firstDone = true;
+        }
+        append(buf, value);
+      }
+    }
+    buf.append(']');
+  }
+
+  /**
+   * Appends a Collection to the buffer.
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  public static void appendCollection(Appendable buf, Collection<?> collection)
+      throws IOException {
+    buf.append('[');
+    boolean firstDone = false;
+    for (Object o : collection) {
+      if (o != null) {
+        if (firstDone) {
+          buf.append(',');
+        } else {
+          firstDone = true;
+        }
+        append(buf, o);
+      }
+    }
+    buf.append(']');
+  }
+
+  /**
+   * Appends a Map to the buffer.
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  public static void appendMap(final Appendable buf, final Map<String, ?> map) throws IOException {
+    buf.append('{');
+    boolean firstDone = false;
+    for (Map.Entry<String, ?> entry : map.entrySet()) {
+      Object value = entry.getValue();
+      if (value != null) {
+        if (firstDone) {
+          buf.append(',');
+        } else {
+          firstDone = true;
+        }
+        Object key = entry.getKey();
+
+        appendString(buf, key.toString());
+        buf.append(':');
+        append(buf, value);
+      }
+    }
+    buf.append('}');
+  }
+
+  /**
+   * Appends a Map to the buffer.
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  public static void appendMultimap(Appendable buf, Multimap<String, Object> map) throws IOException {
+    appendMap(buf, map.asMap());
+  }
+
+  /**
+   * Appends a JSONObject to the buffer.
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  @SuppressWarnings("unchecked")
+  public static void appendJsonObject(Appendable buf, JSONObject object) throws IOException {
+    buf.append('{');
+    Iterator<String> keys = object.keys();
+    boolean firstDone = false;
+    while (keys.hasNext()) {
+      String key = keys.next();
+      Object value = object.opt(key);
+      if (value != null) {
+        if (firstDone) {
+          buf.append(',');
+        } else {
+          firstDone = true;
+        }
+        appendString(buf, key);
+        buf.append(':');
+        append(buf, value);
+      }
+    }
+    buf.append('}');
+  }
+
+  /**
+   * Appends a string to the buffer. The string will be JSON encoded and enclosed in quotes.
+   *
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  public static void appendString(Appendable buf, CharSequence string) throws IOException {
+    if (string == null || string.length() == 0) {
+      buf.append("\"\"");
+      return;
+    }
+
+    char current = 0;
+    buf.append('"');
+    for (int i = 0, j = string.length(); i < j; ++i) {
+      current = string.charAt(i);
+      switch (current) {
+        case '\\':
+        case '"':
+          buf.append('\\');
+          buf.append(current);
+          break;
+        // We escape angle brackets in order to prevent content sniffing in user agents like IE.
+        // This content sniffing can potentially be used to bypass other security restrictions.
+        case '<':
+          buf.append("\\u003c");
+          break;
+        case '>':
+          buf.append("\\u003e");
+          break;
+        default:
+          if (current < ' ' || (current >= '\u0080' && current < '\u00a0') ||
+              (current >= '\u2000' && current < '\u2100')) {
+            buf.append('\\');
+            switch (current) {
+              case '\b':
+                buf.append('b');
+                break;
+              case '\t':
+                buf.append('t');
+                break;
+              case '\n':
+                buf.append('n');
+                break;
+              case '\f':
+                buf.append('f');
+                break;
+              case '\r':
+                buf.append('r');
+                break;
+              default:
+                // The possible alternative approaches for dealing with unicode characters are
+                // as follows:
+                // Method 1 (from json.org.JSONObject)
+                // 1. Append "000" + Integer.toHexString(current)
+                // 2. Truncate this value to 4 digits by using value.substring(value.length() - 4)
+                //
+                // Method 2 (from net.sf.json.JSONObject)
+                // This method is fairly unique because the entire thing uses an intermediate fixed
+                // size buffer of 1KB. It's an interesting approach, but overall performs worse than
+                // org.json
+                // 1. Append "000" + Integer.toHexString(current)
+                // 2. Append value.charAt(value.length() - 4)
+                // 2. Append value.charAt(value.length() - 3)
+                // 2. Append value.charAt(value.length() - 2)
+                // 2. Append value.charAt(value.length() - 1)
+                //
+                // Method 3 (previous experiment)
+                // 1. Calculate Integer.hexString(current)
+                // 2. for (int i = 0; i < 4 - value.length(); ++i) { buf.append('0'); }
+                // 3. buf.append(value)
+                //
+                // Method 4 (Sun conversion from java.util.Properties)
+                // 1. Append '\'
+                // 2. Append 'u'
+                // 3. Append each of 4 octets by indexing into a hex array.
+                //
+                // Method 5
+                // Index into a single lookup table of all relevant lookup values.
+                buf.append('u');
+                buf.append(HEX_DIGITS[(current >> 12) & 0xF]);
+                buf.append(HEX_DIGITS[(current >>  8) & 0xF]);
+                buf.append(HEX_DIGITS[(current >>  4) & 0xF]);
+                buf.append(HEX_DIGITS[current & 0xF]);
+             }
+          } else {
+            buf.append(current);
+          }
+      }
+    }
+    buf.append('"');
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/JsonUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/JsonUtil.java
new file mode 100644
index 0000000..cf9d2aa
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/JsonUtil.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.lang3.text.WordUtils;
+import org.json.JSONObject;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * JSON utilities that are not specific to either serialization or conversion.
+ */
+public final class JsonUtil {
+  private JsonUtil() {}
+
+  private static final Set<String> EXCLUDE_METHODS
+      = ImmutableSet.of("getClass", "getDeclaringClass");
+
+  private static final LoadingCache<Class<?>, Map<String, Method>> GETTERS = CacheBuilder
+      .newBuilder()
+      .build(new CacheLoader<Class<?>, Map<String, Method>>() {
+        public Map<String, Method> load(Class<?> clazz) {
+          ImmutableMap.Builder<String,Method> methods = ImmutableMap.builder();
+
+          for (Method method : clazz.getMethods()) {
+            if (method.getParameterTypes().length == 0 && !method.isSynthetic()) {
+              String name = getPropertyName(method);
+              if (name != null) {
+                methods.put(name, method);
+              }
+            }
+          }
+          return methods.build();
+        }
+      });
+
+  /**
+   * Gets a property of an Object.  Will return a property value if
+   * serializing the value would produce a JSON object containing that
+   * property, otherwise returns null.
+   */
+  public static Object getProperty(Object value, String propertyName) {
+    Preconditions.checkNotNull(value);
+    Preconditions.checkNotNull(propertyName);
+
+    if (value instanceof JSONObject) {
+      return ((JSONObject) value).opt(propertyName);
+    } else if (value instanceof Map<?, ?>) {
+      return ((Map<?, ?>) value).get(propertyName);
+    } else {
+      // Try getter conversion
+      Method method = GETTERS.getUnchecked(value.getClass()).get(propertyName);
+      if (method != null) {
+        try {
+          return method.invoke(value);
+        } catch (IllegalArgumentException e) {
+          // Shouldn't be possible.
+          throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+          // Bad class.
+          throw new RuntimeException(e);
+        } catch (InvocationTargetException e) {
+          // Bad class.
+          throw new RuntimeException(e);
+        }
+      }
+    }
+
+    return null;
+  }
+
+  static Map<String, Method> getGetters(Object pojo) {
+    return GETTERS.getUnchecked(pojo.getClass()) ;
+  }
+
+  private static String getPropertyName(Method method) {
+    JsonProperty property = method.getAnnotation(JsonProperty.class);
+    if (property == null) {
+      String name = method.getName();
+      if (name.startsWith("get") && (!EXCLUDE_METHODS.contains(name))) {
+        return WordUtils.uncapitalize(name.substring(3));
+      } else if (name.startsWith("is") && name.length() > 2 &&
+          Character.isUpperCase(name.charAt(2))) {
+        return WordUtils.uncapitalize(name.substring(2));
+      }
+      return null;
+    } else {
+      return property.value();
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/Nullable.java b/trunk/java/common/src/main/java/org/apache/shindig/common/Nullable.java
new file mode 100644
index 0000000..5af10fc
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/Nullable.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * Temporary package for use until we the jsr people release to maven central.
+ * http://code.google.com/p/jsr-305/issues/detail?id=13
+ *
+ * This allows us to remove the jsr305 jar from findbugs which is LGPL and
+ * moved from 'provided' to 'runtime' scope due to use by Guice.
+ */
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Nullable {
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java b/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java
new file mode 100644
index 0000000..bd2bb89
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/Pair.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+/**
+ * A pair of any two objects.
+ */
+public class Pair<T1, T2> {
+  public final T1 one;
+  public final T2 two;
+
+  public Pair(T1 one, T2 two) {
+    this.one = one;
+    this.two = two;
+  }
+
+  public static <T1, T2> Pair<T1, T2> of(T1 one, T2 two) {
+    return new Pair<T1, T2>(one, two);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/PropertiesModule.java b/trunk/java/common/src/main/java/org/apache/shindig/common/PropertiesModule.java
new file mode 100644
index 0000000..1d26028
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/PropertiesModule.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.CreationException;
+import com.google.inject.name.Names;
+import com.google.inject.spi.Message;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.util.ResourceLoader;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Properties;
+
+/**
+ * Injects everything from the a property file as a Named value
+ * Uses the default shindig.properties file if no other is provided
+ */
+public class PropertiesModule extends AbstractModule {
+
+  private final static String DEFAULT_PROPERTIES = "shindig.properties";
+
+  private final Properties properties;
+
+  public PropertiesModule() {
+    super();
+    this.properties = readPropertyFile(getDefaultPropertiesPath());
+  }
+
+  public PropertiesModule(String propertyFile) {
+    this.properties = readPropertyFile(propertyFile);
+  }
+
+  public PropertiesModule(Properties properties) {
+    this.properties = properties;
+  }
+
+  @Override
+  protected void configure() {
+    this.binder().bindConstant().annotatedWith(Names.named("shindig.contextroot")).to(getContextRoot());
+    Names.bindProperties(this.binder(), getProperties());
+    // This could be generalized to inject any system property...
+    this.binder().bindConstant().annotatedWith(Names.named("shindig.port")).to(getServerPort());
+    this.binder().bindConstant().annotatedWith(Names.named("shindig.host")).to(getServerHostname());
+  }
+
+  /**
+   * Should return the context root where the current web module is deployed with.  Useful for testing and working out of the box configs.
+   * If not set uses fixed value of "".
+   * @return an context path as a string.
+   */
+  protected String getContextRoot() {
+    return System.getProperty("shindig.contextroot") != null ? System.getProperty("shindig.contextroot") : "";
+  }
+
+  /**
+   * Should return the default port set as system property. Return empty string if it's not set.
+   * @return an integer port number as a string.
+   */
+  protected String getServerPort() {
+    return System.getProperty("shindig.port") != null ? System.getProperty("shindig.port") : "";
+  }
+
+  /*
+   * Should return the hostname set as system property. Return empty string  if its' not set.
+   * @return a hostname
+   */
+  protected String getServerHostname() {
+    return System.getProperty("shindig.host") != null ? System.getProperty("shindig.host") : "";
+  }
+
+  protected static String getDefaultPropertiesPath() {
+    return DEFAULT_PROPERTIES;
+  }
+
+  protected Properties getProperties() {
+    return properties;
+  }
+
+  protected Properties readPropertyFile(String propertyFile) {
+    Properties properties = new Properties();
+    InputStream is = null;
+    String contextRoot = getContextRoot();
+    try {
+      is = ResourceLoader.openResource(propertyFile);
+      properties.load(is);
+
+      for (Object key : properties.keySet()) {
+        String value = (String)properties.get((String)key);
+        if (value != null && value.contains("%contextRoot%")){
+          properties.put(key, value.replace(("%contextRoot%"),contextRoot));
+        }
+      }
+    } catch (IOException e) {
+      throw new CreationException(Arrays.asList(
+          new Message("Unable to load properties: " + propertyFile)));
+    } finally {
+      IOUtils.closeQuietly(is);
+    }
+    return properties;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/Cache.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/Cache.java
new file mode 100644
index 0000000..559fcab
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/Cache.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+/**
+ * A basic cache interface. If necessary, we can always move to the commons
+ * cache for the future.
+ */
+public interface Cache<K, V> {
+
+  /**
+   * Retrieves an entry for the cache.
+   *
+   * @return The entry stored under the given key, or null if it doesn't exist.
+   */
+  V getElement(K key);
+
+  /**
+   * Stores an entry into the cache.
+   */
+  void addElement(K key, V value);
+
+  /**
+   * Removes an entry from the cache.
+   *
+   * @param key The entry to return.
+   * @return The entry stored under the given key, or null if it doesn't exist.
+   */
+  V removeElement(K key);
+
+  /**
+   * Returns the capacity of the cache.
+   *
+   * @return a positive integer indicating the upper bound on the number of allowed elements
+   * in the cace, -1 signifies that the capacity is unbounded
+   */
+  long getCapacity();
+
+  /**
+   * @return The current size of the cache, or -1 if the cache does not support returning sizes.
+   */
+  long getSize();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/CacheProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/CacheProvider.java
new file mode 100644
index 0000000..8491267
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/CacheProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Interface for Shindig caches.
+ */
+@ImplementedBy(LruCacheProvider.class)
+public interface CacheProvider {
+  /**
+   * Create a named single instance cache in this cache manager, if the cache
+   * already exists, return it if the name is null, a new anonymous cache is
+   * created
+   *
+   * @param <K>  The Key type for the cache
+   * @param <V>  The pay-load type
+   * @param name The non-null name of the cache.
+   * @return A Cache configured to the required specification.
+   */
+  <K, V> Cache<K, V> createCache(String name);
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/LruCache.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/LruCache.java
new file mode 100644
index 0000000..8db638f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/LruCache.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * A basic LRU cache. Prefer using EhCache for most purposes to this class.
+ */
+public class LruCache<K, V> extends LinkedHashMap<K, V> implements Cache<K, V> {
+  final int capacity;
+
+  public LruCache(int capacity) {
+    super(capacity, 0.75f, true);
+    this.capacity = capacity;
+  }
+
+  public synchronized V getElement(K key) {
+    return super.get(key);
+  }
+
+  public synchronized void addElement(K key, V value) {
+    super.put(key, value);
+  }
+
+  public synchronized V removeElement(K key) {
+    return super.remove(key);
+  }
+
+  public long getCapacity() {
+    return capacity;
+  }
+
+  public long getSize() {
+    return size();
+  }
+
+  @Override
+  protected synchronized boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+    return size() > capacity;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/LruCacheProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/LruCacheProvider.java
new file mode 100644
index 0000000..c11e6da
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/LruCacheProvider.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.MapMaker;
+import com.google.inject.ConfigurationException;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+
+/**
+ * A cache provider that always produces LRU caches.
+ *
+ * LRU cache sizes can be configured by specifying property names in the form
+ *
+ * shindig.cache.lru.<cache name>.capacity=foo
+ *
+ * The default value is expected under shindig.cache.lru.default.capacity
+ *
+ * An in memory LRU cache only scales so far. For a production-worthy cache, use
+ * {@code EhCacheCacheProvider}.
+ */
+public class LruCacheProvider implements CacheProvider {
+  private static final String classname = LruCacheProvider.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+  private final int defaultCapacity;
+  private final Injector injector;
+  private final Map<String, Cache<?, ?>> caches = new MapMaker().makeMap();
+
+  @Inject
+  public LruCacheProvider(Injector injector,
+      @Named("shindig.cache.lru.default.capacity") int defaultCapacity) {
+    this.injector = injector;
+    this.defaultCapacity = defaultCapacity;
+  }
+
+  public LruCacheProvider(int capacity) {
+    this(null, capacity);
+  }
+
+  private int getCapacity(String name) {
+    if (injector != null && name != null) {
+      String key = "shindig.cache.lru." + name + ".capacity";
+      Key<String> guiceKey = Key.get(String.class, Names.named(key));
+      try {
+        if (injector.getBinding(guiceKey) == null) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "getCapacity(String name)", MessageKeys.LRU_CAPACITY,new Object[] {"No",name});
+          }
+        } else {
+          String value = injector.getInstance(guiceKey);
+          try {
+            return Integer.parseInt(value);
+          } catch (NumberFormatException e) {
+            if (LOG.isLoggable(Level.WARNING)) {
+              LOG.logp(Level.WARNING, classname, "getCapacity(String name)", MessageKeys.LRU_CAPACITY,new Object[] {"Invalid",name});
+            }
+          }
+        }
+      } catch ( ConfigurationException e ) {
+        return defaultCapacity;
+      }
+    }
+    return defaultCapacity;
+  }
+
+  @SuppressWarnings("unchecked")
+  public <K, V> Cache<K, V> createCache(String name) {
+    int capacity = getCapacity(Preconditions.checkNotNull(name));
+    Cache<K, V> cache = (Cache<K, V>) caches.get(name);
+    if (cache == null) {
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("Creating cache named " + name);
+      }
+      cache = new LruCache<K, V>(capacity);
+      caches.put(name, cache);
+    }
+    return cache;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/NullCache.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/NullCache.java
new file mode 100644
index 0000000..e3ad64d
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/NullCache.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+/**
+ * Cache implementation that does nothing.
+ */
+public class NullCache<K, V> implements Cache<K, V>{
+
+  public void addElement(K key, V value) {
+  }
+
+  public long getCapacity() {
+    return 0;
+  }
+
+  public V getElement(K key) {
+    return null;
+  }
+
+  public long getSize() {
+    return 0;
+  }
+
+  public V removeElement(K key) {
+    return null;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java
new file mode 100644
index 0000000..0064199
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/SoftExpiringCache.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+import org.apache.shindig.common.util.TimeSource;
+
+import com.google.common.collect.MapMaker;
+
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A cache that uses a soft expiration policy. Entries will be kept around for potentially as long
+ * as the underlying cache permits, but we keep a timestamp around to retain a notion of the actual
+ * age. This provides users of this class with the option of keeping an "expired" entry beyond the
+ * normal lifetime.
+ *
+ * As soon as an entry expires from the underlying cache, it disappears from here as well.
+ *
+ * Note that this isn't actually a cache itself, but rather a wrapper for one. It differs in the
+ * getElement method substantially, since the returned objects are not the same as the V parameter.
+ */
+public class SoftExpiringCache<K, V> {
+  private final Cache<K, V> cache;
+
+  // We keep a weak reference to the value stored in the cache so that when the value in the actual
+  // cache is removed, we should lose it here as well.
+  private final ConcurrentMap<V, Long> expirationTimes;
+  private TimeSource timeSource;
+
+  /**
+   * Create a new TtlCache with the given capacity and TTL values.
+   * The cache provider provides an implementation of the actual storage.
+   *
+   * @param cache The underlying cache that will store actual data.
+   */
+  public SoftExpiringCache(Cache<K, V>  cache) {
+    this.cache = cache;
+    expirationTimes = new MapMaker().weakKeys().makeMap();
+    timeSource = new TimeSource();
+  }
+
+  /**
+   * Retrieve an element from the cache by key. If there is no such element
+   * for that key in the cache, or if the element has timed out, null is returned.
+   * @param key Key whose element to look up.
+   * @return Element in the cache, if present and not timed out.
+   */
+  public CachedObject<V> getElement(K key) {
+    V value = cache.getElement(key);
+    if (value == null) {
+      return null;
+    }
+
+    Long expiration = expirationTimes.get(value);
+
+    if (expiration == null) {
+      return null;
+    }
+
+    return new CachedObject<V>(value, expiration < timeSource.currentTimeMillis());
+  }
+
+  /**
+   * Add an element to the cache, with the intended max age for its cache entry provided in
+   * milliseconds.
+   *
+   * @param key The key to store the entry for.
+   * @param value The value to store.
+   * @param maxAge The maximum age for this entry before it is deemed expired.
+   */
+  public void addElement(K key, V value, long maxAge) {
+    long now = timeSource.currentTimeMillis();
+    cache.addElement(key, value);
+    expirationTimes.put(value, now + maxAge);
+  }
+
+  /**
+   * Set a new time source. For use in testing.
+   *
+   * @param timeSource New time source to use.
+   */
+  public void setTimeSource(TimeSource timeSource) {
+    this.timeSource = timeSource;
+  }
+
+  public static class CachedObject<V> {
+    public final V obj;
+    public final boolean isExpired;
+
+    protected CachedObject(V obj, boolean isExpired) {
+      this.obj = obj;
+      this.isExpired = isExpired;
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhCacheCacheProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhCacheCacheProvider.java
new file mode 100644
index 0000000..f8c8e7a
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhCacheCacheProvider.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache.ehcache;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.servlet.GuiceServletContextListener;
+import org.apache.shindig.common.util.ResourceLoader;
+
+import com.google.common.collect.MapMaker;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.config.Configuration;
+import net.sf.ehcache.config.ConfigurationFactory;
+import net.sf.ehcache.management.ManagementService;
+
+import javax.management.MBeanServer;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.management.ManagementFactory;
+import java.util.concurrent.ConcurrentMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Cache interface based on ehcache.
+ *
+ * @see <a href="http://www.ehcache.org">http://www.ehcache.org</a>
+ */
+public class EhCacheCacheProvider implements CacheProvider,
+        GuiceServletContextListener.CleanupCapable {
+  private static final Logger LOG = Logger.getLogger(EhCacheCacheProvider.class.getName());
+  private final CacheManager cacheManager;
+  private final ConcurrentMap<String, Cache<?, ?>> caches = new MapMaker().makeMap();
+
+  /**
+   * @param configPath
+   *          the path to the EhCache configuration file
+   * @param filterPath
+   *          the path to the EhCache SizeOf engine filter file
+   * @param jmxEnabled
+   *          true if JMX should be enabled for EhCache, false otherwise
+   * @param withCacheStats
+   *          true if cache statistics should be enabled globally, false otherwise
+   * @param cleanupHandler
+   *          cleanup handler with which to register to ensure proper cache shutdown via
+   *          {@link #cleanup()}
+   * @throws IOException
+   *           if there was an issue parsing the given configuration
+   */
+  @Inject
+  public EhCacheCacheProvider(@Named("shindig.cache.ehcache.config") String configPath,
+                              @Named("shindig.cache.ehcache.sizeof.filter") String filterPath,
+                              @Named("shindig.cache.ehcache.jmx.enabled") boolean jmxEnabled,
+                              @Named("shindig.cache.ehcache.jmx.stats") boolean withCacheStats,
+                              GuiceServletContextListener.CleanupHandler cleanupHandler)
+      throws IOException {
+    // TODO: Setting this system property is currently the only way to hook in our own filter
+    // https://jira.terracotta.org/jira/browse/EHC-938
+    // https://jira.terracotta.org/jira/browse/EHC-924
+    // Remove res:// and file:// prefixes.  EhCache can't understand them.
+    String normalizedFilterPath = filterPath.replaceFirst(ResourceLoader.RESOURCE_PREFIX, "");
+    normalizedFilterPath = normalizedFilterPath.replaceFirst(ResourceLoader.FILE_PREFIX, "");
+    System.setProperty("net.sf.ehcache.sizeof.filter", normalizedFilterPath);
+
+    // If ehcache.disk.store.dir isn't already set, set it to java.io.tmpdir.
+    // See http://ehcache.org/documentation/user-guide/storage-options#diskstore-configuration-element
+    String diskStoreProperty = System.getProperty("ehcache.disk.store.dir");
+    if (Strings.isNullOrEmpty(diskStoreProperty)) {
+      System.setProperty("ehcache.disk.store.dir", System.getProperty("java.io.tmpdir"));
+    }
+
+    cacheManager = CacheManager.create(getConfiguration(configPath));
+    create(jmxEnabled, withCacheStats);
+    cleanupHandler.register(this);
+  }
+
+  /**
+   * Read the cache configuration from the specified resource.
+   *
+   * This function is intended to be overrideable to allow for programmatic cache configuration.
+   *
+   * @param configPath
+   *          the path to the configuration file
+   * @return Configuration the configuration object parsed from the configuration file
+   * @throws IOException
+   *           if there was an error parsing the given configuration
+   */
+  protected Configuration getConfiguration(String configPath) throws IOException {
+    InputStream configStream = ResourceLoader.open(configPath);
+    return ConfigurationFactory.parseConfiguration(configStream);
+  }
+
+  private void create(boolean jmxEnabled, boolean withCacheStats) {
+    if (jmxEnabled) {
+      // register the cache manager with JMX
+      MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
+      ManagementService.registerMBeans(cacheManager, mBeanServer, true, true, true, withCacheStats);
+    }
+  }
+
+  /**
+   * Perform a shutdown of the underlying cache manager.
+   */
+  public void cleanup() {
+    cacheManager.shutdown();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @SuppressWarnings("unchecked")
+  public <K, V> Cache<K, V> createCache(String name) {
+    if (!caches.containsKey(Preconditions.checkNotNull(name))) {
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("Creating cache named " + name);
+      }
+      caches.putIfAbsent(name, new EhConfiguredCache<K, V>(name, cacheManager));
+    }
+    return (Cache<K, V>) caches.get(Preconditions.checkNotNull(name));
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhCacheModule.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhCacheModule.java
new file mode 100644
index 0000000..65a5ee8
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhCacheModule.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache.ehcache;
+
+import org.apache.shindig.common.cache.CacheProvider;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+
+/**
+ * Creates a module to supply a EhCache Provider
+ */
+public class EhCacheModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(CacheProvider.class).to(EhCacheCacheProvider.class).in(Scopes.SINGLETON);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhConfiguredCache.java b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhConfiguredCache.java
new file mode 100644
index 0000000..0b83924
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/cache/ehcache/EhConfiguredCache.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache.ehcache;
+
+import com.google.common.base.Preconditions;
+import org.apache.shindig.common.cache.Cache;
+
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+
+
+/**
+ * Produces a cache configured from ehcache.
+ *
+ * @param <K> the type of key used to cache elements
+ * @param <V> the type of element stored in this cache
+ */
+public class EhConfiguredCache<K, V> implements Cache<K, V> {
+
+  private net.sf.ehcache.Cache cache;
+
+  /**
+   * Create a new EhCache cache with the given name for the given cache manager if one does not
+   * already exist.
+   *
+   * @param cacheName
+   *          the name to use for the cache
+   * @param cacheManager
+   *          the cache manager in which to create the cache
+   */
+  public EhConfiguredCache(String cacheName, CacheManager cacheManager) {
+    synchronized (cacheManager) {
+      cache = cacheManager.getCache(Preconditions.checkNotNull(cacheName));
+      if (cache == null) {
+        cacheManager.addCache(cacheName);
+        cache = cacheManager.getCache(cacheName);
+        if (cache == null) {
+          throw new RuntimeException("Failed to create Cache with name " + cacheName);
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public void addElement(K key, V value) {
+    cache.put(new Element(key, value));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @SuppressWarnings("unchecked")
+  public V getElement(K key) {
+    Element cacheElement = cache.get(key);
+    if (cacheElement != null) {
+      return (V) cacheElement.getObjectValue();
+    }
+    return null;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @SuppressWarnings("unchecked")
+  public V removeElement(K key) {
+    Object value = getElement(key);
+    cache.remove(key);
+    return (V) value;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public long getCapacity() {
+    // EhCache returns 0 to represent an unbounded cache, where the Cache interface expects -1
+    // EhCache also returns 0 when using resource pooling as a count-based capacity does not apply
+    long totalCapacity = cache.getCacheConfiguration().getMaxEntriesLocalHeap()
+            + cache.getCacheConfiguration().getMaxEntriesLocalDisk();
+    return totalCapacity == 0 ? -1 : totalCapacity;
+  }
+
+  /**
+   * @return The current size of the cache.
+   *
+   * Note that this does not call getSize on the underlying cache, which is very expensive. This
+   * will not include the size of remote caches.
+   */
+  public long getSize() {
+    return cache.getMemoryStoreSize() + cache.getDiskStoreSize();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BasicBlobCrypter.java b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BasicBlobCrypter.java
new file mode 100644
index 0000000..b5fab4f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BasicBlobCrypter.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.crypto;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Maps;
+import com.google.common.base.Preconditions;
+
+import com.google.common.primitives.Bytes;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.common.util.TimeSource;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.security.GeneralSecurityException;
+import java.util.Map;
+
+/**
+ * Simple implementation of BlobCrypter.
+ */
+public class BasicBlobCrypter implements BlobCrypter {
+
+  // Labels for key derivation
+  private static final byte CIPHER_KEY_LABEL = 0;
+  private static final byte HMAC_KEY_LABEL = 1;
+
+  /** minimum length of master key */
+  public static final int MASTER_KEY_MIN_LEN = 16;
+
+  public TimeSource timeSource = new TimeSource();
+  private byte[] cipherKey;
+  private byte[] hmacKey;
+
+  /**
+   * Creates a crypter based on a key in a file.  The key is the first line
+   * in the file, whitespace trimmed from either end, as UTF-8 bytes.
+   *
+   * The following *nix command line will create an excellent key:
+   * <pre>
+   * dd if=/dev/random bs=32 count=1  | openssl base64 > /tmp/key.txt
+   * </pre>
+   *
+   * @throws IOException if the file can't be read.
+   */
+  public BasicBlobCrypter(File keyfile) throws IOException {
+    BufferedReader reader = null;
+    try {
+      FileInputStream openFile = new FileInputStream(keyfile);
+      reader = new BufferedReader(
+          new InputStreamReader(openFile, Charsets.UTF_8));
+      init(reader.readLine());
+    } finally {
+      try {
+        if (reader != null) {
+          reader.close();
+        }
+      } catch (IOException e) {
+        // oh well.
+      }
+    }
+  }
+
+  /**
+   * Builds a BlobCrypter from the specified master key
+   *
+   * @param masterKey
+   */
+  public BasicBlobCrypter(byte[] masterKey) {
+    init(masterKey);
+  }
+
+  /**
+   * Builds a BlobCrypter from the specified master key
+   *
+   * @param masterKey
+   */
+  public BasicBlobCrypter(String masterKey) {
+    init(masterKey);
+  }
+
+  private void init(String masterKey) {
+    if (masterKey == null) {
+      throw new IllegalArgumentException("Unexpectedly empty masterKey:" + masterKey);
+    }
+    masterKey = masterKey.trim();
+    byte[] keyBytes = CharsetUtil.getUtf8Bytes(masterKey);
+    init(keyBytes);
+  }
+
+  private void init(byte[] masterKey) {
+    Preconditions.checkArgument(masterKey.length >= MASTER_KEY_MIN_LEN,
+        "Master key needs at least %s bytes", MASTER_KEY_MIN_LEN);
+
+    cipherKey = deriveKey(CIPHER_KEY_LABEL, masterKey, Crypto.CIPHER_KEY_LEN);
+    hmacKey = deriveKey(HMAC_KEY_LABEL, masterKey, 0);
+  }
+
+  /**
+   * Generates unique keys from a master key.
+   *
+   * @param label type of key to derive
+   * @param masterKey master key
+   * @param len length of key needed, less than 20 bytes.  20 bytes are
+   * returned if len is 0.
+   *
+   * @return a derived key of the specified length
+   */
+  private byte[] deriveKey(byte label, byte[] masterKey, int len) {
+    byte[] base = Bytes.concat(new byte[] { label }, masterKey);
+    byte[] hash = DigestUtils.sha(base);
+    if (len == 0) {
+      return hash;
+    }
+    byte[] out = new byte[len];
+    System.arraycopy(hash, 0, out, 0, out.length);
+    return out;
+  }
+
+  /* (non-Javadoc)
+   * @see org.apache.shindig.util.BlobCrypter#wrap(java.util.Map)
+   */
+  public String wrap(Map<String, String> in) throws BlobCrypterException {
+    try {
+      byte[] encoded = serialize(in);
+      byte[] cipherText = Crypto.aes128cbcEncrypt(cipherKey, encoded);
+      byte[] hmac = Crypto.hmacSha1(hmacKey, cipherText);
+      byte[] b64 = Base64.encodeBase64URLSafe(Bytes.concat(cipherText, hmac));
+      return CharsetUtil.newUtf8String(b64);
+    } catch (GeneralSecurityException e) {
+      throw new BlobCrypterException(e);
+    }
+  }
+
+  /**
+   * Encode the input for transfer.  We use something a lot like HTML form
+   * encodings.
+   * @param in map of parameters to encode
+   */
+  private byte[] serialize(Map<String, String> in) {
+    StringBuilder sb = new StringBuilder();
+
+    for (Map.Entry<String, String> val : in.entrySet()) {
+      sb.append(Utf8UrlCoder.encode(val.getKey()));
+      sb.append('=');
+      sb.append(Utf8UrlCoder.encode(val.getValue()));
+      sb.append('&');
+    }
+    if (sb.length() > 0) {
+      sb.deleteCharAt(sb.length() - 1);  // Remove the last &
+    }
+    return CharsetUtil.getUtf8Bytes(sb.toString());
+  }
+
+  /* (non-Javadoc)
+   * @see org.apache.shindig.util.BlobCrypter#unwrap(java.lang.String, int)
+   */
+  public Map<String, String> unwrap(String in) throws BlobCrypterException {
+    try {
+      byte[] bin = Base64.decodeBase64(CharsetUtil.getUtf8Bytes(in));
+      byte[] hmac = new byte[Crypto.HMAC_SHA1_LEN];
+      byte[] cipherText = new byte[bin.length-Crypto.HMAC_SHA1_LEN];
+      System.arraycopy(bin, 0, cipherText, 0, cipherText.length);
+      System.arraycopy(bin, cipherText.length, hmac, 0, hmac.length);
+      Crypto.hmacSha1Verify(hmacKey, cipherText, hmac);
+      byte[] plain = Crypto.aes128cbcDecrypt(cipherKey, cipherText);
+      Map<String, String> out = deserialize(plain);
+      return out;
+    } catch (GeneralSecurityException e) {
+      throw new BlobCrypterException("Invalid token signature", e);
+    } catch (ArrayIndexOutOfBoundsException e) {
+      throw new BlobCrypterException("Invalid token format", e);
+    } catch (NegativeArraySizeException e) {
+      throw new BlobCrypterException("Invalid token format", e);
+    }
+
+  }
+
+  private Map<String, String> deserialize(byte[] plain) {
+    String base = CharsetUtil.newUtf8String(plain);
+    // replaces [&=] regex
+    String[] items = StringUtils.splitPreserveAllTokens(base, "&=");
+    Map<String, String> map = Maps.newHashMapWithExpectedSize(items.length);
+    for (int i=0; i < items.length; ) {
+      String key = Utf8UrlCoder.decode(items[i++]);
+      String val = Utf8UrlCoder.decode(items[i++]);
+      map.put(key, val);
+    }
+    return map;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobCrypter.java b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobCrypter.java
new file mode 100644
index 0000000..28b299b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobCrypter.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.crypto;
+
+import java.util.Map;
+
+/**
+ * Utility interface for managing signed and encrypted blobs.
+ * Blobs are made up of name/value pairs.
+ *
+ * Thread safe.
+ */
+public interface BlobCrypter {
+
+  /**
+   * Encrypts and signs a blob.
+   *
+   * @param in name/value pairs to encrypt
+   * @return a base64 encoded blob
+   *
+   * @throws BlobCrypterException when crypto errors occur
+   */
+  String wrap(Map<String, String> in) throws BlobCrypterException;
+
+  /**
+   * Unwraps a blob.
+   *
+   * @param in blob
+   * @return the name/value pairs.
+   *
+   * @throws BlobCrypterException if the blob can't be decoded.
+   */
+  Map<String, String> unwrap(String in) throws BlobCrypterException;
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobCrypterException.java b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobCrypterException.java
new file mode 100644
index 0000000..981ec0a
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobCrypterException.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.crypto;
+
+/**
+ * For all exceptions thrown by BlobCrypter
+ */
+public class BlobCrypterException extends Exception {
+  public BlobCrypterException(Throwable cause) {
+    super(cause);
+  }
+
+  public BlobCrypterException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
+
+  protected BlobCrypterException(String msg) {
+    super(msg);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobExpiredException.java b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobExpiredException.java
new file mode 100644
index 0000000..7195606
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/BlobExpiredException.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.crypto;
+
+import java.util.Date;
+
+/**
+ * Thrown when a blob has expired.
+ */
+public class BlobExpiredException extends BlobCrypterException {
+
+  public final Date used;
+  public final Date maxDate;
+
+  public BlobExpiredException(long now, long maxTime) {
+    this(new Date(now*1000), new Date(maxTime*1000));
+  }
+
+  public BlobExpiredException(Date now, Date maxTime) {
+    super("Blob expired. Was valid until " + maxTime + ", attempted use at " + now);
+    this.used = now;
+    this.maxDate = maxTime;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/Crypto.java b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/Crypto.java
new file mode 100644
index 0000000..984cee4
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/crypto/Crypto.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.crypto;
+
+import com.google.common.primitives.Bytes;
+import org.apache.commons.codec.binary.Hex;
+
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Cryptographic utility functions.
+ */
+public final class Crypto {
+
+  /**
+   * Use this random number generator instead of creating your own.  This is
+   * thread-safe.
+   */
+  public static final SecureRandom RAND = new SecureRandom();
+
+  /**
+   * HMAC algorithm to use
+   */
+  private final static String HMAC_TYPE = "HMACSHA1";
+
+  /**
+   * minimum safe length for hmac keys (this is good practice, but not
+   * actually a requirement of the algorithm
+   */
+  private final static int MIN_HMAC_KEY_LEN = 8;
+
+  /**
+   * Encryption algorithm to use
+   */
+  private final static String CIPHER_TYPE = "AES/CBC/PKCS5Padding";
+
+  private final static String CIPHER_KEY_TYPE = "AES";
+
+  /**
+   * Use keys of this length for encryption operations
+   */
+  public final static int CIPHER_KEY_LEN = 16;
+
+  private static final int CIPHER_BLOCK_SIZE = 16;
+
+  /**
+   * Length of HMAC SHA1 output
+   */
+  public final static int HMAC_SHA1_LEN = 20;
+
+  private final static char[] DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };
+
+  // everything is static, no instantiating this class
+  private Crypto() {
+  }
+
+  /**
+   * Gets a hex encoded random string.
+   *
+   * @param numBytes number of bytes of randomness.
+   */
+  public static String getRandomString(int numBytes) {
+    return new String(Hex.encodeHex(getRandomBytes(numBytes)));
+  }
+
+  /**
+   * @return a random string of digits of the specified length.
+   */
+  public static String getRandomDigits(int len) {
+    byte[] random = getRandomBytes(len);
+    StringBuilder out = new StringBuilder(len);
+    for (int i = 0; i < len; ++i) {
+      out.append(DIGITS[Math.abs(random[i] % DIGITS.length)]);
+    }
+    return out.toString();
+  }
+
+  /**
+   * Returns strong random bytes.
+   *
+   * @param numBytes number of bytes of randomness
+   */
+  public static byte[] getRandomBytes(int numBytes) {
+    byte[] out = new byte[numBytes];
+    RAND.nextBytes(out);
+    return out;
+  }
+
+  /**
+   * HMAC sha1
+   *
+   * @param key the key must be at least 8 bytes in length.
+   * @param in byte array to HMAC.
+   * @return the hash
+   *
+   * @throws GeneralSecurityException
+   */
+  public static byte[] hmacSha1(byte[] key, byte[] in) throws GeneralSecurityException {
+    if (key.length < MIN_HMAC_KEY_LEN) {
+      throw new GeneralSecurityException("HMAC key should be at least "
+          + MIN_HMAC_KEY_LEN + " bytes.");
+    }
+    Mac hmac = Mac.getInstance(HMAC_TYPE);
+    Key hmacKey = new SecretKeySpec(key, HMAC_TYPE);
+    hmac.init(hmacKey);
+    hmac.update(in);
+    return hmac.doFinal();
+  }
+
+  /**
+   * Verifies an HMAC SHA1 hash.  Throws if the verification fails.
+   *
+   * @param key
+   * @param in
+   * @param expected
+   * @throws GeneralSecurityException
+   */
+  public static void hmacSha1Verify(byte[] key, byte[] in, byte[] expected)
+  throws GeneralSecurityException {
+    Mac hmac = Mac.getInstance(HMAC_TYPE);
+    Key hmacKey = new SecretKeySpec(key, HMAC_TYPE);
+    hmac.init(hmacKey);
+    hmac.update(in);
+    byte actual[] = hmac.doFinal();
+    if (actual.length != expected.length) {
+      throw new GeneralSecurityException("HMAC verification failure");
+    }
+    for (int i=0; i < actual.length; i++) {
+      if (actual[i] != expected[i]) {
+        throw new GeneralSecurityException("HMAC verification failure");
+      }
+    }
+  }
+
+  /**
+   * AES-128-CBC encryption.  The IV is returned as the first 16 bytes
+   * of the cipher text.
+   *
+   * @param key
+   * @param plain
+   *
+   * @return the IV and cipher text
+   *
+   * @throws GeneralSecurityException
+   */
+  public static byte[] aes128cbcEncrypt(byte[] key, byte[] plain)
+  throws GeneralSecurityException {
+    Cipher cipher = Cipher.getInstance(CIPHER_TYPE);
+    byte iv[] = getRandomBytes(cipher.getBlockSize());
+    return Bytes.concat(iv, aes128cbcEncryptWithIV(key, iv, plain));
+  }
+
+  /**
+   * AES-128-CBC encryption with a given IV.
+   *
+   * @param key
+   * @param iv
+   * @param plain
+   *
+   * @return the cipher text
+   *
+   * @throws GeneralSecurityException
+   */
+  public static byte[] aes128cbcEncryptWithIV(byte[] key, byte[] iv, byte[] plain)
+  throws GeneralSecurityException {
+    Cipher cipher = Cipher.getInstance(CIPHER_TYPE);
+    Key cipherKey = new SecretKeySpec(key, CIPHER_KEY_TYPE);
+    IvParameterSpec ivSpec = new IvParameterSpec(iv);
+    cipher.init(Cipher.ENCRYPT_MODE, cipherKey, ivSpec);
+    return cipher.doFinal(plain);
+  }
+
+
+  /**
+   * AES-128-CBC decryption.  The IV is assumed to be the first 16 bytes
+   * of the cipher text.
+   *
+   * @param key
+   * @param cipherText
+   *
+   * @return the plain text
+   *
+   * @throws GeneralSecurityException
+   */
+  public static byte[] aes128cbcDecrypt(byte[] key, byte[] cipherText)
+  throws GeneralSecurityException {
+    byte iv[] = new byte[CIPHER_BLOCK_SIZE];
+    System.arraycopy(cipherText, 0, iv, 0, iv.length);
+    return aes128cbcDecryptWithIv(key, iv, cipherText, iv.length);
+  }
+
+  /**
+   * AES-128-CBC decryption with a particular IV.
+   *
+   * @param key decryption key
+   * @param iv initial vector for decryption
+   * @param cipherText cipher text to decrypt
+   * @param offset offset into cipher text to begin decryption
+   *
+   * @return the plain text
+   *
+   * @throws GeneralSecurityException
+   */
+  public static byte[] aes128cbcDecryptWithIv(byte[] key, byte[] iv,
+      byte[] cipherText, int offset) throws GeneralSecurityException {
+    Cipher cipher = Cipher.getInstance(CIPHER_TYPE);
+    Key cipherKey = new SecretKeySpec(key, CIPHER_KEY_TYPE);
+    IvParameterSpec ivSpec = new IvParameterSpec(iv);
+    cipher.init(Cipher.DECRYPT_MODE, cipherKey, ivSpec);
+    return cipher.doFinal(cipherText, offset, cipherText.length-offset);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/logging/i18n/MessageKeys.java b/trunk/java/common/src/main/java/org/apache/shindig/common/logging/i18n/MessageKeys.java
new file mode 100644
index 0000000..c0d4fd4
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/logging/i18n/MessageKeys.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.logging.i18n;
+
+/**
+ * Unique [key, value] pairs for i18n logging messages.
+ * The key is used as message key input in the log method and its value matched the key specified in the localized resource.properties file
+ */
+public interface MessageKeys {
+	public static final String MESSAGES = "org.apache.shindig.common.logging.i18n.resource";
+
+	//AuthenticationServletFilter
+	public static final String ERROR_PARSING_SECURE_TOKEN = "errorParsingSecureToken";
+	//XmlUtil
+	public static final String ERROR_PARSING_XML="commonErrorParsingXML";
+	public static final String ERROR_PARSING_EXTERNAL_GENERAL_ENTITIES="errorParsingExternalGeneralEntities";
+	public static final String ERROR_PARSING_EXTERNAL_PARAMETER_ENTITIES="errorParsingExternalParameterEntities";
+	public static final String ERROR_PARSING_EXTERNAL_DTD="errorParsingExternalDTD";
+	public static final String ERROR_PARSING_SECURE_XML="errorNotUsingSecureXML";
+	public static final String REUSE_DOC_BUILDERS="reuseDocumentBuilders";
+	public static final String NOT_REUSE_DOC_BUILDERS="notReuseDocBuilders";
+	//LruCacheProvier
+	public static final String LRU_CAPACITY="LRUCapacity";
+	//DynamicConfigProperty
+	public static final String EVAL_EL_FAILED="evalExpressionFailed";
+	//JsonContainerConfigLoader
+	public static final String READING_CONFIG="readingContainerConfig";
+	public static final String LOAD_FR_STRING="loadFromString";
+	public static final String LOAD_RESOURCES_FROM="loadResourcesFrom";
+	public static final String LOAD_FILES_FROM="loadFilesFrom";
+	//ApiServlet
+	public static final String API_SERVLET_PROTOCOL_EXCEPTION="apiServletProtocolException";
+	public static final String API_SERVLET_EXCEPTION="apiServletException";
+	//RegistryFeature
+	public static final String OVERRIDING_FEATURE="overridingFeature";
+	//XSDValidator
+	public static final String RESOLVE_RESOURCE="resolveResource";
+	public static final String FAILED_TO_VALIDATE="failedToValidate";
+
+
+	//FeatureResourceLoader
+	public static final String MISSING_FILE="missingFile";
+	public static final String UNABLE_RETRIEVE_LIB="unableRetrieveLib";
+	//BasicHttpFetcher
+	public static final String TIMEOUT_EXCEPTION="timeoutException";
+	public static final String EXCEPTION_OCCURRED="exceptionOccurred";
+	public static final String SLOW_RESPONSE="slowResponse";
+	//HttpResponseMetadataHelper
+	public static final String ERROR_GETTING_MD5="errorGettingMD5";
+	public static final String ERROR_PARSING_MD5="errorParsingMD5";
+	//OAuthModule
+	public static final String USING_RANDOM_KEY="usingRandomKey";
+	public static final String USING_FILE="usingFile";
+	public static final String LOAD_KEY_FILE_FROM="loadKeyFileFrom";
+	public static final String COULD_NOT_LOAD_KEY_FILE="couldNotLoadKeyFile";
+	public static final String COULD_NOT_LOAD_SIGN_KEY="couldNotLoadSignedKey";
+	public static final String FAILED_TO_INIT="failedToInit";
+	//OauthRequest
+	public static final String OAUTH_FETCH_FATAL_ERROR="oauthFetchFatalError";
+	public static final String BOGUS_EXPIRED="bogusExpired";
+	public static final String OAUTH_FETCH_ERROR_REPROMPT="oauthFetchErrorReprompt";
+	public static final String OAUTH_FETCH_UNEXPECTED_ERROR="oauthFetchUnexpectedError";
+	public static final String UNAUTHENTICATED_OAUTH="unauthenticatedOauth";
+	public static final String INVALID_OAUTH="invalidOauth";
+	//CajaCssSanitizer
+	public static final String FAILED_TO_PARSE="failedToParse";
+	public static final String UNABLE_TO_CONVERT_SCRIPT="unableToConvertScript";
+	//PipelineExecutor
+	public static final String ERROR_PRELOADING="errorPreloading";
+	//Processor
+	public static final String RENDER_NON_WHITELISTED_GADGET="renderNonWhitelistedGadget";
+	//CajaResponseRewriter
+	public static final String FAILED_TO_RETRIEVE="failedToRetrieve";
+	public static final String FAILED_TO_READ="failedToRead";
+	//DefaultServiceFetcher
+	public static final String HTTP_ERROR_FETCHING="httpErrorFetching";
+	public static final String FAILED_TO_FETCH_SERVICE="failedToFetchService";
+	public static final String FAILED_TO_PARSE_SERVICE="failedToParseService";
+	//Renderer
+	public static final String FAILED_TO_RENDER="FailedToRender";
+	//RenderingGadgetRewriter
+	public static final String UNKNOWN_FEATURES="unknownFeatures";
+	public static final String UNEXPECTED_ERROR_PRELOADING="unexpectedErrorPreloading";
+	//SanitizingResponseRewriter
+	public static final String REQUEST_TO_SANITIZE_WITHOUT_CONTENT="requestToSanitizeWithoutContent";
+	public static final String REQUEST_TO_SANITIZE_UNKNOW_CONTENT="requestToSanitizeUnknownContent";
+	public static final String UNABLE_SANITIZE_UNKNOWN_IMG="unableToSanitizeUnknownImg";
+	public static final String UNABLE_DETECT_IMG_TYPE="unableToDetectImgType";
+	//BasicImageRewriter
+	public static final String IO_ERROR_REWRITING_IMG="ioErrorRewritingImg";
+	public static final String UNKNOWN_ERROR_REWRITING_IMG="unknownErrorRewritingImg";
+	public static final String FAILED_TO_READ_IMG="failedToReadImg";
+	//CssResponseRewriter
+	public static final String CAJA_CSS_PARSE_FAILURE="cajaCssParseFailure";
+	//ImageAttributeRewriter
+	public static final String UNABLE_TO_PROCESS_IMG="unableToProcessImg";
+	public static final String UNABLE_TO_READ_RESPONSE="unableToReadResponse";
+	public static final String UNABLE_TO_FETCH_IMG="unableToFetchImg";
+	public static final String UNABLE_TO_PARSE_IMG="unableToParseImg";
+	//MutableContent
+	public static final String EXCEPTION_PARSING_CONTENT="exceptionParsingContent";
+	//PipelineDataGadgetRewriter
+	public static final String FAILED_TO_PARSE_PRELOAD="failedToParsePreload";
+	//ProxyingVisitor
+	public static final String URI_EXCEPTION_PARSING="uriExceptionParsing";
+	//TemplateRewriter
+	public static final String 	MALFORMED_TEMPLATE_LIB="malformedTemplateLib";
+	//CajaContentRewriter
+	public static final String CAJOLED_CACHE_CREATED="cajoledCacheCreated";
+	public static final String RETRIEVE_REFERENCE="retrieveReference";
+	public static final String UNABLE_TO_CAJOLE="unableToCajole";
+	//ConcatProxyServlet
+	public static final String CONCAT_PROXY_REQUEST_FAILED="concatProxyRequestFailed";
+	//GadgetRenderingServlet
+	public static final String MALFORMED_TTL_VALUE="malformedTtlValue";
+	//HttpRequestHandler
+	public static final String GADGET_CREATION_ERROR="gadgetCreationError";
+	//ProxyServlet
+	public static final String EMBEDED_IMG_WRONG_DOMAIN="embededImgWrongDomain";
+	//RpcServlet
+	public static final String BAD_REQUEST_400="badRequest400";
+	//DefaultTemplateProcessor
+	public static final String EL_FAILURE="elFailure";
+	//UriUtils
+	public static final String SKIP_ILLEGAL_HEADER="skipIllegalHeader";
+	//AbstractSpecFactory
+	public static final String UPDATE_SPEC_FAILURE_USE_CACHE_VERSION="updateSpecFailureUseCacheVersion";
+	public static final String UPDATE_SPEC_FAILURE_APPLY_NEG_CACHE="updateSpecFailureApplyNegCache";
+	//HashLockedDomainService
+	public static final String NO_LOCKED_DOMAIN_CONFIG="noLockedDomainConfig";
+	//Bootstrap
+	public static final String STARTING_CONN_MANAGER_WITH="startingConnManagerWith";
+	//DefaultRequestPipeline
+	public static final String CACHED_RESPONSE="cachedResponse";
+	public static final String STALE_RESPONSE="staleResponse";
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/Authority.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/Authority.java
new file mode 100644
index 0000000..4f6ad97
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/Authority.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+/**
+ * Interface to return Authority and Origin of a hierarchical URI . Shindig server components
+ * would use the information to construct URI.
+ *
+ */
+public interface Authority {
+
+  /**
+   * The authority part of the hierarchical URI. Eg "localhost:8080"
+   * @return the authority part of the hierarchical URI
+   */
+  public String getAuthority();
+
+  /**
+   * The scheme and authority part of the hierarchical URI. Eg "http://localhost:8080"
+   * @return the scheme and authority part of the hierarchical URI
+   */
+  public String getOrigin();
+
+  /**
+   * The scheme part of the hierarchical URI. Eg "http://localhost:8080"
+   * @return the scheme a of the hierarchical URI
+   */
+  public String getScheme();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/BasicAuthority.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/BasicAuthority.java
new file mode 100644
index 0000000..abd9b67
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/BasicAuthority.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.Nullable;
+
+/**
+ * Basic implementation for Authority Interface.
+ *
+ * Authority information is calculated based on following procedure.
+ * 1. Optionally default host and default port can be provided as ServletContext Parameters in web.xml.
+ *    Once it's provided, default host and default port are used to construct authority information.
+ * 2. If default host and default port are not provided, host and port from current HttpServletRequest
+ *    will be used if available.
+ * 3. If HttpServletRequest is not available, jetty host/port will be used.
+ *    This is required for junit tests.
+ */
+public class BasicAuthority implements Authority {
+  private final String host;
+  private final String port;
+  public final static String JETTY_HOST = "jetty.host";
+  public final static String JETTY_PORT = "jetty.port";
+
+  @Inject
+  public BasicAuthority(@Nullable @Named("shindig.host") String defaultHost,
+      @Nullable @Named("shindig.port") String defaultPort) {
+    this.host = StringUtils.isNotBlank(defaultHost) ? defaultHost : null;
+    this.port =  StringUtils.isNotBlank(defaultPort) ? defaultPort : null;
+  }
+
+  public String getAuthority() {
+    return Joiner.on(':').join(
+        Objects.firstNonNull(host, getServerHostname()),
+        Objects.firstNonNull(port, getServerPort()));
+  }
+
+  public String getScheme(){
+    return Objects.firstNonNull(ServletRequestContext.getScheme(), "http");
+  }
+
+  public String getOrigin(){
+	return getScheme() + "://" + getAuthority();
+  }
+
+  private String getServerPort() {
+    return Objects.firstNonNull(ServletRequestContext.getPort(),
+        Objects.firstNonNull(System.getProperty(JETTY_PORT), "8080"));
+  }
+
+  private String getServerHostname() {
+    return Objects.firstNonNull(ServletRequestContext.getHost(),
+        Objects.firstNonNull(System.getProperty(JETTY_HOST), "localhost"));
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/GuiceServletContextListener.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/GuiceServletContextListener.java
new file mode 100644
index 0000000..d9c660c
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/GuiceServletContextListener.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.Stage;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletContextEvent;
+import javax.servlet.ServletContextListener;
+
+/**
+ * Creates a global guice injector and stores it in a servlet context parameter
+ * for injecting servlets.
+ */
+public class GuiceServletContextListener implements ServletContextListener {
+  public static final String INJECTOR_ATTRIBUTE = "guice-injector";
+  public static final String MODULES_ATTRIBUTE = "guice-modules";
+
+  // From guice-servlet-2.0
+  public static final String INJECTOR_NAME = Injector.class.getName();
+
+  // HNN- constant name matched system.properties <contextparam> specified in the web.xml
+  private static final String SYSTEM_PROPERTIES = "system.properties";
+
+  public void contextInitialized(ServletContextEvent event) {
+    ServletContext context = event.getServletContext();
+    setSystemProperties(context);
+    String moduleNames = context.getInitParameter(MODULES_ATTRIBUTE);
+    List<Module> modules = Lists.newLinkedList();
+
+    if (moduleNames != null) {
+      for (String moduleName : Splitter.on(':').split(moduleNames)) {
+        try {
+          moduleName = moduleName.trim();
+          if (moduleName.length() > 0) {
+            try {
+              modules.add((Module)Class.forName(moduleName).newInstance());
+            } catch (Throwable t) {
+              // If we cannot find the class using forName try the current
+              // threads class loader
+              modules.add((Module)Thread.currentThread().getContextClassLoader()
+                  .loadClass(moduleName).newInstance());
+            }
+          }
+        } catch (InstantiationException e) {
+          throw new RuntimeException(e);
+        } catch (ClassNotFoundException e) {
+          throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    }
+    Injector injector = Guice.createInjector(Stage.PRODUCTION, modules);
+    context.setAttribute(INJECTOR_ATTRIBUTE, injector);
+  }
+
+  public void contextDestroyed(ServletContextEvent event) {
+    ServletContext context = event.getServletContext();
+    Injector injector = (Injector) context.getAttribute(INJECTOR_ATTRIBUTE);
+    if (injector != null) {
+        CleanupHandler cleanups = injector.getInstance(CleanupHandler.class);
+        cleanups.cleanup();
+    }
+
+    context.removeAttribute(INJECTOR_ATTRIBUTE);
+  }
+
+
+  /**
+   * This method sets all the (key,value) properties specified in the web.xml <contextparam> system.properties element
+   * if they are not empty.
+   * @param context the ServletContext
+   */
+  private void setSystemProperties(ServletContext context){
+    String contextRoot = context.getContextPath();
+    System.setProperty("shindig.contextroot", contextRoot);
+    String systemProperties = context.getInitParameter(SYSTEM_PROPERTIES);
+
+    if (systemProperties!=null && systemProperties.trim().length() > 0){
+      for (String prop : Splitter.on('\n').trimResults().split(systemProperties)){
+        String[] keyAndvalue = StringUtils.split(prop, "=", 2);
+        if (keyAndvalue.length == 2) {
+          String key = keyAndvalue[0];
+          String value = keyAndvalue[1];
+          //set the system property if they are not empty
+          if (key!=null && key.trim().length() > 0 && value!=null && value.trim().length() > 0){
+            System.setProperty(key,value);
+          }
+        }
+      }
+    }
+  }
+
+
+  /**
+   * Interface for classes that need to run cleanup code without
+   * using Runtime ShutdownHooks (which leaks memory on redeploys)
+   */
+  public interface CleanupCapable {
+    /** Execute the cleanup code. */
+    void cleanup();
+  }
+
+  /**
+   * Injectable handler that allows Guice classes to make themselves cleanup capable.
+   */
+  @Singleton
+  public static class CleanupHandler {
+    private List<CleanupCapable> cleanupHandlers = Lists.newArrayList();
+
+    public CleanupHandler() { }
+    /**
+     * Add a new class instance for running cleanup code.
+     *
+     * Best way
+     *
+     * @param cleanupCapable class instance implementing CleanupCapable.
+     */
+    public void register(CleanupCapable cleanupCapable) {
+      cleanupHandlers.add(cleanupCapable);
+    }
+
+    public void cleanup() {
+      for (CleanupCapable cleanupHandler : cleanupHandlers) {
+        cleanupHandler.cleanup();
+      }
+    }
+  }
+}
+
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HostFilter.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HostFilter.java
new file mode 100644
index 0000000..ecdfe71
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HostFilter.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterConfig;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * A Filter that can cache ServletRequest information in ThreadLocal variable
+ */
+public class HostFilter implements Filter {
+
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    ServletRequestContext.setRequestInfo(request);
+    chain.doFilter(request, response);
+  }
+
+  public void destroy() {
+  }
+
+  public void init(FilterConfig filterConfig) throws ServletException {
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpServletUserAgentProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpServletUserAgentProvider.java
new file mode 100644
index 0000000..12efc3f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpServletUserAgentProvider.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Simple provider of UserAgent information from an HttpServletRequest.
+ * Uses an injected UserAgent.Parser to generate a UserAgent.Entry.
+ */
+public class HttpServletUserAgentProvider implements Provider<UserAgent> {
+  private final UserAgent.Parser uaParser;
+  private final Provider<HttpServletRequest> reqProvider;
+
+  @Inject
+  public HttpServletUserAgentProvider(UserAgent.Parser uaParser,
+      Provider<HttpServletRequest> reqProvider) {
+    this.uaParser = uaParser;
+    this.reqProvider = reqProvider;
+  }
+
+  public UserAgent get() {
+    HttpServletRequest req = reqProvider.get();
+    if (req != null) {
+      String userAgent = req.getHeader("User-Agent");
+      if (userAgent != null) {
+        return uaParser.parse(userAgent);
+      }
+    }
+    return null;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java
new file mode 100644
index 0000000..dfc418d
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/HttpUtil.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import com.google.common.base.Preconditions;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.common.util.TimeSource;
+
+import com.google.common.collect.Lists;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Collection of HTTP utilities
+ */
+public final class HttpUtil {
+  private HttpUtil() {}
+
+  // 1 year.
+  private static int defaultTtl = 60 * 60 * 24 * 365;
+
+  private static TimeSource timeSource;
+
+  static {
+    setTimeSource(new TimeSource());
+  }
+
+  public static void setTimeSource(TimeSource timeSource) {
+    HttpUtil.timeSource = timeSource;
+  }
+
+  public static TimeSource getTimeSource() {
+    return timeSource;
+  }
+
+  /**
+   * Sets HTTP headers that instruct the browser to cache content. Implementations should take care
+   * to use cache-busting techniques on the url if caching for a long period of time.
+   *
+   * @param response The HTTP response
+   */
+  public static void setCachingHeaders(HttpServletResponse response) {
+    setCachingHeaders(response, defaultTtl, false);
+  }
+
+  /**
+   * Sets HTTP headers that instruct the browser to cache content. Implementations should take care
+   * to use cache-busting techniques on the url if caching for a long period of time.
+   *
+   * @param response The HTTP response
+   * @param noProxy True if you don't want the response to be cacheable by proxies.
+   */
+  public static void setCachingHeaders(HttpServletResponse response, boolean noProxy) {
+    setCachingHeaders(response, defaultTtl, noProxy);
+  }
+
+  /**
+   * Sets HTTP headers that instruct the browser to cache content. Implementations should take care
+   * to use cache-busting techniques on the url if caching for a long period of time.
+   *
+   * @param response The HTTP response
+   * @param ttl The time to cache for, in seconds. If 0, then insure that
+   *            this object is not cached.
+   */
+  public static void setCachingHeaders(HttpServletResponse response, int ttl) {
+    setCachingHeaders(response, ttl, false);
+  }
+
+  public static void setNoCache(HttpServletResponse response) {
+    setCachingHeaders(response, 0, false);
+  }
+
+  /**
+   * Sets HTTP headers that instruct the browser to cache content. Implementations should take care
+   * to use cache-busting techniques on the url if caching for a long period of time.
+   *
+   * @param response The HTTP response
+   * @param ttl The time to cache for, in seconds. If 0, then insure that
+   *            this object is not cached.
+   * @param noProxy True if you don't want the response to be cacheable by proxies.
+   */
+  public static void setCachingHeaders(HttpServletResponse response, int ttl, boolean noProxy) {
+    for (Pair<String, String> header : getCachingHeadersToSet(ttl, noProxy)) {
+      response.setHeader(header.one, header.two);
+    }
+  }
+
+  public static List<Pair<String, String>> getCachingHeadersToSet(int ttl, boolean noProxy) {
+    return getCachingHeadersToSet(ttl, null, null, noProxy);
+  }
+
+  public static List<Pair<String, String>> getCachingHeadersToSet(int ttl, String cacheControl, String pragma, boolean noProxy) {
+    List<Pair<String, String>> cachingHeaders = Lists.newArrayListWithExpectedSize(3);
+    cachingHeaders.add(Pair.of("Expires",
+        DateUtil.formatRfc1123Date(timeSource.currentTimeMillis() + (1000L * ttl))));
+
+    if (ttl <= 0) {
+      cachingHeaders.add(Pair.of("Pragma", pragma == null ? "no-cache" : pragma));
+      cachingHeaders.add(Pair.of("Cache-Control", cacheControl == null ? "no-cache" : cacheControl));
+    } else {
+      if (noProxy) {
+        cachingHeaders.add(Pair.of("Cache-Control", "private,max-age=" + Integer.toString(ttl)));
+      } else {
+        cachingHeaders.add(Pair.of("Cache-Control", "public,max-age=" + Integer.toString(ttl)));
+      }
+    }
+
+    return cachingHeaders;
+  }
+
+  public static int getDefaultTtl() {
+    return defaultTtl;
+  }
+
+  public static void setDefaultTtl(int defaultTtl) {
+    HttpUtil.defaultTtl = defaultTtl;
+  }
+
+
+  static final Pattern GET_REQUEST_CALLBACK_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z0-9_\\.]+");
+
+  public static boolean isJSONP(HttpServletRequest request) throws IllegalArgumentException {
+    String callback = request.getParameter("callback");
+
+    // Must be a GET
+    if (!"GET".equals(request.getMethod()))
+      return false;
+
+    // No callback specified
+    if (callback == null) return false;
+
+    Preconditions.checkArgument(GET_REQUEST_CALLBACK_PATTERN.matcher(callback).matches(),
+        "Wrong format for parameter 'callback' specified. Must match: " +
+            GET_REQUEST_CALLBACK_PATTERN.toString());
+
+    return true;
+  }
+
+
+  public static final String ACCESS_CONTROL_ALLOW_ORIGIN_HEADER = "Access-Control-Allow-Origin";
+
+  /**
+   * Set the header for Cross-Site Resource Sharing.
+   * @param resp HttpServletResponse to modify
+   * @param validOrigins a space separated list of Origins as defined by the html5 spec
+   * @see <a href="http://dev.w3.org/html5/spec/browsers.html#origin-0">html 5 spec, section 5.3</a>
+   */
+  public static void setCORSheader(HttpServletResponse resp, Collection<String> validOrigins) {
+    if (validOrigins == null) {
+      return;
+    }
+    for (String origin : validOrigins) {
+      resp.addHeader(ACCESS_CONTROL_ALLOW_ORIGIN_HEADER, origin);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/InjectedFilter.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/InjectedFilter.java
new file mode 100644
index 0000000..a7f168a
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/InjectedFilter.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import com.google.inject.Injector;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.UnavailableException;
+
+/**
+ * A Filter that can use Guice for injecting. Complements InjectedServlet.
+ */
+public abstract class InjectedFilter implements Filter {
+  protected Injector injector;
+
+  public void init(FilterConfig config) throws ServletException {
+    ServletContext context = config.getServletContext();
+    injector = (Injector) context.getAttribute(GuiceServletContextListener.INJECTOR_ATTRIBUTE);
+    if (injector == null) {
+      injector = (Injector)
+        context.getAttribute(GuiceServletContextListener.INJECTOR_NAME);
+      if (injector == null) {
+        throw new UnavailableException(
+            "Guice Injector not found! Make sure you registered " +
+            GuiceServletContextListener.class.getName() + " as a listener");
+      }
+    }
+    injector.injectMembers(this);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/InjectedServlet.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/InjectedServlet.java
new file mode 100644
index 0000000..8fc215e
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/InjectedServlet.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Injector;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.UnavailableException;
+import javax.servlet.http.HttpServlet;
+
+/**
+ * Supports DI for servlets. Can't handle ctor injection since
+ * the servlet spec requires configuration being done in init().
+ */
+public abstract class InjectedServlet extends HttpServlet {
+  protected Injector injector;
+  protected transient boolean initialized = false;
+
+  @Override
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
+    ServletContext context = config.getServletContext();
+    injector = (Injector) context.getAttribute(GuiceServletContextListener.INJECTOR_ATTRIBUTE);
+    if (injector == null) {
+      injector = (Injector) context.getAttribute(GuiceServletContextListener.INJECTOR_NAME);
+      if (injector == null) {
+        throw new UnavailableException(
+            "Guice Injector not found! Make sure you registered " +
+                GuiceServletContextListener.class.getName() + " as a listener");
+      }
+    }
+    injector.injectMembers(this);
+    initialized = true;
+  }
+
+  /**
+   * Called in each guice injected method to insure we are not initialized twice
+   */
+  protected void checkInitialized() {
+    Preconditions.checkState(!initialized, "Servlet already initialized");
+  }
+
+  /**
+   * Writes the state of this InjectedServlet during serialization.
+   *
+   * @param out The stream to which to save the state
+   *
+   * @throws IOException if an error occurs
+   */
+  private void writeObject(ObjectOutputStream out) throws IOException {
+    out.defaultWriteObject();
+  }
+
+  /**
+   * Restores the state of this InjectedServlet during deserialization.
+   *
+   * <p>Upon deserialization, this InjectedServlet will not be functional
+   * until after its init method was called, which will cause all necessary
+   * injection to happen.
+   *
+   * @param in The stream from which to restore the state
+   *
+   * @throws IOException if an error occurs
+   * @throws ClassNotFoundException if a class cannot be found
+   */
+  private void readObject(ObjectInputStream in)
+          throws IOException, ClassNotFoundException {
+    in.defaultReadObject();
+    initialized = false;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/ParameterFetcher.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/ParameterFetcher.java
new file mode 100644
index 0000000..45db07b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/ParameterFetcher.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Maps parameters from a Servlet request into a map. This is used to control which
+ * parameters are passed on to other parts of the container. Using this Adapter allows
+ * e.g. to pass multiple parameters into the secure token generation methods.
+ */
+public interface ParameterFetcher {
+    Map<String, String> fetch(HttpServletRequest req);
+}
+
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/ServletRequestContext.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/ServletRequestContext.java
new file mode 100644
index 0000000..3f1707c
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/ServletRequestContext.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import javax.servlet.ServletRequest;
+
+public class ServletRequestContext {
+
+  public final static String HOST = "host";
+  public final static String PORT = "port";
+  public final static String SCHEME = "scheme";
+
+  public static void setRequestInfo(ServletRequest req) {
+    host.set(req.getServerName());
+    port.set("" + req.getServerPort());
+    scheme.set(req.getScheme());
+
+    // Temporary solution since variables are not available in forked thread during js processing
+    System.setProperty(HOST, req.getServerName());
+    System.setProperty(PORT, "" + req.getServerPort());
+    System.setProperty(SCHEME, req.getScheme());
+  }
+
+  /**
+   * A Thread Local holder for host
+   */
+  private static ThreadLocal<String> host = new ThreadLocal<String>();
+
+  /**
+   * A Thread Local holder for port
+   */
+  private static ThreadLocal<String> port = new ThreadLocal<String>();
+
+  /**
+   * A Thread Local holder for scheme
+   */
+  private static ThreadLocal<String> scheme = new ThreadLocal<String>();
+
+
+  public static String getHost(){
+    return host.get() != null ? host.get() : System.getProperty(HOST);
+  }
+
+  public static String getPort(){
+    return port.get() != null ? port.get() : System.getProperty(PORT);
+  }
+
+  public static String getScheme(){
+    return scheme.get() != null ? scheme.get() : System.getProperty(SCHEME);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/UserAgent.java b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/UserAgent.java
new file mode 100644
index 0000000..2c6ba10
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/servlet/UserAgent.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Simple class defining basic User-Agent parsing.
+ * Defines an interface for a Parser, a list of common Browsers,
+ * and an Entry that is consumed by code providing UA-specific behavior.
+ */
+public final class UserAgent {
+  private final Browser browser;
+  private final String version;
+  private static final Pattern VERSION_NUMBER_REGEX = Pattern.compile(".*?([0-9]+(\\.[0-9]+)?).*");
+
+  public UserAgent(Browser browser, String version) {
+    this.browser = browser;
+    this.version = version;
+  }
+
+  /**
+   * @return Identifying browser string.
+   */
+  public Browser getBrowser() {
+    return browser;
+  }
+
+  /**
+   * @return Version string of user agent.
+   */
+  public String getVersion() {
+    return version != null ? version.trim() : null;
+  }
+
+  /**
+   * @return Numeric version number, if parseable. Otherwise -1.
+   */
+  public double getVersionNumber() {
+    // Attempt to retrieve the numeric part of a version string.
+    Matcher matcher = VERSION_NUMBER_REGEX.matcher(getVersion());
+    if (!matcher.matches()) {
+      return -1;
+    }
+    String matched = matcher.group(1);
+    return Double.parseDouble(matched);
+  }
+
+  public interface Parser {
+    UserAgent parse(String userAgent);
+  }
+
+  public enum Browser {
+    MSIE,
+    FIREFOX,
+    SAFARI,
+    WEBKIT,
+    CHROME,
+    OPERA,
+    HTML5,  // A faux-Browser useful for directly referencing HTML5 JS capability vs. "legacy".
+    OTHER
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/uri/DefaultUriParser.java b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/DefaultUriParser.java
new file mode 100644
index 0000000..9266a2b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/DefaultUriParser.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.uri;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+/**
+ * Uri parser using java.net.URI as its basis, enforcing RFC 2396 restrictions.
+ */
+public class DefaultUriParser implements UriParser {
+  /**
+   * Produces a new Uri from a text representation.
+   *
+   * @param text The text uri.
+   * @return A new Uri, parsed into components.
+   */
+  public Uri parse(String text) {
+    try {
+      return Uri.fromJavaUri(new URI(text));
+    } catch (URISyntaxException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/uri/Uri.java b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/Uri.java
new file mode 100644
index 0000000..1ba8c5e
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/Uri.java
@@ -0,0 +1,413 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.uri;
+
+import com.google.common.base.Strings;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a Uniform Resource Identifier (URI) reference as defined by <a
+ * href="http://tools.ietf.org/html/rfc3986">RFC 3986</a>.
+ *
+ * Assumes that all url components are UTF-8 encoded.
+ */
+public final class Uri {
+  private final String text;
+  private final String scheme;
+  private final String authority;
+  private final String path;
+  private final String query;
+  private final String fragment;
+
+  private final Map<String, List<String>> queryParameters;
+  private final Map<String, List<String>> fragmentParameters;
+
+  private static UriParser parser = new DefaultUriParser();
+
+  @Inject(optional = true)
+  public static void setUriParser(UriParser uriParser) {
+    parser = uriParser;
+  }
+
+  Uri(UriBuilder builder) {
+    scheme = builder.getScheme();
+    authority = builder.getAuthority();
+    path = builder.getPath();
+    query = builder.getQuery();
+    fragment = builder.getFragment();
+    queryParameters = ImmutableMap.copyOf(builder.getQueryParameters());
+    fragmentParameters = ImmutableMap.copyOf(builder.getFragmentParameters());
+
+    StringBuilder out = new StringBuilder();
+
+    if (scheme != null) {
+      out.append(scheme).append(':');
+    }
+    if (authority != null) {
+      out.append("//").append(authority);
+      // insure that there's a separator between authority/path
+      if (path != null && path.length() > 1 && !path.startsWith("/")) {
+        out.append('/');
+      }
+    }
+    if (path != null) {
+      out.append(path);
+    }
+    if (query != null) {
+      out.append('?').append(query);
+    }
+    if (fragment != null) {
+      out.append('#').append(fragment);
+    }
+    text = out.toString();
+  }
+
+  /**
+   * Produces a new Uri from a text representation.
+   *
+   * @param text The text uri.
+   * @return A new Uri, parsed into components.
+   */
+  public static Uri parse(String text) {
+    try {
+      return parser.parse(text);
+    } catch (IllegalArgumentException e) {
+      // This occurs all the time. Wrap the exception in a Uri-specific
+      // exception, yet one that remains a RuntimeException, so that
+      // callers may catch a specific exception rather than a blanket
+      // Exception, as a compromise between throwing a checked exception
+      // here (forcing wide-scale refactoring across the code base) and
+      // forcing users to simply catch abstract Exceptions here and there.
+      throw new UriException(e);
+    }
+  }
+
+  /**
+   * Convert a java.net.URI to a Uri.
+   * @param uri the uri to convert
+   * @return a shindig Uri
+   */
+  public static Uri fromJavaUri(URI uri) {
+    if (uri.isOpaque()) {
+      throw new UriException("No support for opaque Uris " + uri.toString());
+    }
+    return new UriBuilder()
+        .setScheme(uri.getScheme())
+        .setAuthority(uri.getRawAuthority())
+        .setPath(uri.getRawPath())
+        .setQuery(uri.getRawQuery())
+        .setFragment(uri.getRawFragment())
+        .toUri();
+  }
+
+  /**
+   * @return a java.net.URI equal to this Uri.
+   */
+  public URI toJavaUri() {
+    try {
+      return new URI(toString());
+    } catch (URISyntaxException e) {
+      // Shouldn't ever happen.
+      throw new UriException(e);
+    }
+  }
+
+  /**
+   * Derived from Harmony
+   * Resolves a given url relative to this url. Resolution rules are the same as for
+   * {@code java.net.URI.resolve(URI)}
+   */
+  public Uri resolve(Uri relative) {
+    if (relative == null) {
+      return null;
+    }
+    if (relative.isAbsolute()) {
+      return relative;
+    }
+
+    UriBuilder result;
+    if (Strings.isNullOrEmpty(relative.path) && relative.scheme == null
+        && relative.authority == null && relative.query == null
+        && relative.fragment != null) {
+      // if the relative URI only consists of fragment,
+      // the resolved URI is very similar to this URI,
+      // except that it has the fragement from the relative URI.
+      result = new UriBuilder(this);
+      result.setFragment(relative.fragment);
+    } else if (relative.scheme != null) {
+      result = new UriBuilder(relative);
+    } else if (relative.authority != null) {
+      // if the relative URI has authority,
+      // the resolved URI is almost the same as the relative URI,
+      // except that it has the scheme of this URI.
+      result = new UriBuilder(relative);
+      result.setScheme(scheme);
+    } else {
+      // since relative URI has no authority,
+      // the resolved URI is very similar to this URI,
+      // except that it has the query and fragment of the relative URI,
+      // and the path is different.
+      result = new UriBuilder(this);
+      result.setFragment(relative.fragment);
+      result.setQuery(relative.query);
+      String relativePath = Objects.firstNonNull(relative.path, "");
+      if (relativePath.startsWith("/")) { //$NON-NLS-1$
+        result.setPath(relativePath);
+      } else {
+        // resolve a relative reference
+        String basePath = path != null ? path : "/";
+        int endindex = basePath.lastIndexOf('/') + 1;
+        result.setPath(normalizePath(basePath.substring(0, endindex) + relativePath));
+      }
+    }
+    Uri resolved = result.toUri();
+    validate(resolved);
+    return resolved;
+  }
+
+  private static void validate(Uri uri) {
+    if (Strings.isNullOrEmpty(uri.authority) &&
+        Strings.isNullOrEmpty(uri.path) &&
+        Strings.isNullOrEmpty(uri.query)) {
+      throw new UriException("Invalid scheme-specific part");
+    }
+  }
+
+  /**
+   * Dervived from harmony
+   * normalize path, and return the resulting string
+   */
+  private static String normalizePath(String path) {
+    // count the number of '/'s, to determine number of segments
+    int index = -1;
+    int pathlen = path.length();
+    int size = 0;
+    if (pathlen > 0 && path.charAt(0) != '/') {
+      size++;
+    }
+    while ((index = path.indexOf('/', index + 1)) != -1) {
+      if (index + 1 < pathlen && path.charAt(index + 1) != '/') {
+        size++;
+      }
+    }
+
+    String[] seglist = new String[size];
+    boolean[] include = new boolean[size];
+
+    // break the path into segments and store in the list
+    int current = 0;
+    int index2 = 0;
+    index = (pathlen > 0 && path.charAt(0) == '/') ? 1 : 0;
+    while ((index2 = path.indexOf('/', index + 1)) != -1) {
+      seglist[current++] = path.substring(index, index2);
+      index = index2 + 1;
+    }
+
+    // if current==size, then the last character was a slash
+    // and there are no more segments
+    if (current < size) {
+      seglist[current] = path.substring(index);
+    }
+
+    // determine which segments get included in the normalized path
+    for (int i = 0; i < size; i++) {
+      include[i] = true;
+      if (seglist[i].equals("..")) { //$NON-NLS-1$
+        int remove = i - 1;
+        // search back to find a segment to remove, if possible
+        while (remove > -1 && !include[remove]) {
+          remove--;
+        }
+        // if we find a segment to remove, remove it and the ".."
+        // segment
+        if (remove > -1 && !seglist[remove].equals("..")) { //$NON-NLS-1$
+          include[remove] = false;
+          include[i] = false;
+        }
+      } else if (seglist[i].equals(".")) { //$NON-NLS-1$
+        include[i] = false;
+      }
+    }
+
+    // put the path back together
+    StringBuilder newpath = new StringBuilder();
+    if (path.startsWith("/")) { //$NON-NLS-1$
+      newpath.append('/');
+    }
+
+    for (int i = 0; i < seglist.length; i++) {
+      if (include[i]) {
+        newpath.append(seglist[i]);
+        newpath.append('/');
+      }
+    }
+
+    // if we used at least one segment and the path previously ended with
+    // a slash and the last segment is still used, then delete the extra
+    // trailing '/'
+    if (!path.endsWith("/") && seglist.length > 0 //$NON-NLS-1$
+        && include[seglist.length - 1]) {
+      newpath.deleteCharAt(newpath.length() - 1);
+    }
+
+    String result = newpath.toString();
+
+    // check for a ':' in the first segment if one exists,
+    // prepend "./" to normalize
+    index = result.indexOf(':');
+    index2 = result.indexOf('/');
+    if (index != -1 && (index < index2 || index2 == -1)) {
+      newpath.insert(0, "./"); //$NON-NLS-1$
+      result = newpath.toString();
+    }
+    return result;
+  }
+
+  /**
+   * @return True if the Uri is absolute.
+   */
+  public boolean isAbsolute() {
+    return scheme != null && authority != null;
+  }
+
+  /**
+   * @return The scheme part of the uri, or null if none was specified.
+   */
+  public String getScheme() {
+    return scheme;
+  }
+
+  /**
+   * @return The authority part of the uri, or null if none was specified.
+   */
+  public String getAuthority() {
+    return authority;
+  }
+
+  /**
+   * @return The path part of the uri, or null if none was specified.
+   */
+  public String getPath() {
+    return path;
+  }
+
+  /**
+   * @return The query part of the uri, or null if none was specified.
+   */
+  public String getQuery() {
+    return query;
+  }
+
+  /**
+   * @return The query part of the uri, separated into component parts.
+   */
+  public Map<String, List<String>> getQueryParameters() {
+    return queryParameters;
+  }
+
+  /**
+   * @return All query parameters with the given name.
+   */
+  public Collection<String> getQueryParameters(String name) {
+    return queryParameters.get(name);
+  }
+
+  /**
+   * @return The first query parameter value with the given name.
+   */
+  public String getQueryParameter(String name) {
+    Collection<String> values = queryParameters.get(name);
+    if (values == null || values.isEmpty()) {
+      return null;
+    }
+    return values.iterator().next();
+  }
+
+  /**
+   * @return The uri fragment.
+   */
+  public String getFragment() {
+    return fragment;
+  }
+
+  /**
+   * @return The fragment part of the uri, separated into component parts.
+   */
+  public Map<String, List<String>> getFragmentParameters() {
+    return fragmentParameters;
+  }
+
+  /**
+   * @return All query parameters with the given name.
+   */
+  public Collection<String> getFragmentParameters(String name) {
+    return fragmentParameters.get(name);
+  }
+
+  /**
+   * @return The first query parameter value with the given name.
+   */
+  public String getFragmentParameter(String name) {
+    Collection<String> values = fragmentParameters.get(name);
+    if (values == null || values.isEmpty()) {
+      return null;
+    }
+    return values.iterator().next();
+  }
+
+  @Override
+  public String toString() {
+    return text;
+  }
+
+  @Override
+  public int hashCode() {
+    return text.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {return true;}
+    if (!(obj instanceof Uri)) {return false;}
+    return Objects.equal(text, ((Uri)obj).text);
+  }
+
+  /**
+   * Interim typed, but not checked, exception facilitating migration
+   * of Uri methods to throwing a checked UriException later.
+   */
+  public static final class UriException extends IllegalArgumentException {
+    private UriException(Exception e) {
+      super(e);
+    }
+
+    private UriException(String msg) {
+      super(msg);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/uri/UriBuilder.java b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/UriBuilder.java
new file mode 100644
index 0000000..09b9c3f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/UriBuilder.java
@@ -0,0 +1,439 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.uri;
+
+import org.apache.shindig.common.util.Utf8UrlCoder;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Constructs Uris from inputs.
+ *
+ * Note that the builder will only automatically encode query parameters that are added. Other
+ * parameters must be encoded explicitly.
+ */
+public final class UriBuilder {
+  private static final Pattern QUERY_PATTERN = Pattern.compile("([^&=]+)=([^&=]*)");
+
+  private String scheme;
+  private String authority;
+  private String path;
+  private final ParamString query;
+  private final ParamString fragment;
+
+  /**
+   * Construct a new builder from an existing uri.
+   */
+  public UriBuilder(Uri uri) {
+    scheme = uri.getScheme();
+    authority = uri.getAuthority();
+    path = uri.getPath();
+    query = new ParamString(uri.getQuery());
+    fragment = new ParamString(uri.getFragment());
+  }
+
+  /**
+   * Construct a new builder from a servlet request.
+   */
+  public UriBuilder(HttpServletRequest req) {
+    scheme = req.getScheme().toLowerCase();
+    int serverPort = req.getServerPort();
+    authority = req.getServerName() +
+        ((serverPort == 80 && "http".equals(scheme)) ||
+         (serverPort == 443 && "https".equals(scheme)) ||
+         (serverPort <= 0) ? "" :
+           ":" + serverPort);
+    path = req.getRequestURI();
+    query = new ParamString(req.getQueryString());
+    fragment = new ParamString();
+  }
+
+  /**
+   * Create an empty builder.
+   */
+  public UriBuilder() {
+    query =  new ParamString();
+    fragment = new ParamString();
+  }
+
+  /**
+   * Construct a builder by parsing a string.
+   */
+  public static UriBuilder parse(String text) {
+    return new UriBuilder(Uri.parse(text));
+  }
+
+  /**
+   * Convert the builder to a Uri.
+   */
+  public Uri toUri() {
+    return new Uri(this);
+  }
+
+  /**
+   * @return The scheme part of the uri, or null if none was specified.
+   */
+  public String getScheme() {
+    return scheme;
+  }
+
+  public UriBuilder setScheme(String scheme) {
+    this.scheme = scheme;
+    return this;
+  }
+
+  /**
+   * @return The authority part of the uri, or null if none was specified.
+   */
+  public String getAuthority() {
+    return authority;
+  }
+
+  public UriBuilder setAuthority(String authority) {
+    this.authority = authority;
+    return this;
+  }
+
+  /**
+   * @return The path part of the uri, or null if none was specified.
+   */
+  public String getPath() {
+    return path;
+  }
+
+  /**
+   * Sets the path component of the Uri.
+   */
+  public UriBuilder setPath(String path) {
+    this.path = path;
+    return this;
+  }
+
+  /**
+   * @return The queryParameters fragment.
+   */
+  public String getQuery() {
+    return query.getString();
+  }
+
+  /**
+   * Assigns the specified query string as the query portion of the uri, automatically decoding
+   * parameters to populate the parameter map for calls to getParameter.
+   */
+  public UriBuilder setQuery(String str) {
+    query.setString(str);
+    return this;
+  }
+
+  public UriBuilder addQueryParameter(String name, String value) {
+    query.add(name, value);
+    return this;
+  }
+
+  public UriBuilder addQueryParameters(Map<String, String> parameters) {
+    query.addAll(parameters);
+    return this;
+  }
+
+  /**
+   * Force overwrites a given query parameter with the given value.
+   */
+  public UriBuilder putQueryParameter(String name, String... values) {
+    query.put(name, values);
+    return this;
+  }
+
+  /**
+   * Force overwrites a given query parameter with the given value.
+   */
+  public UriBuilder putQueryParameter(String name, Iterable<String> values) {
+    query.put(name, values);
+    return this;
+  }
+
+  /**
+   * Removes a query parameter.
+   */
+  public UriBuilder removeQueryParameter(String name) {
+    query.remove(name);
+    return this;
+  }
+
+  /**
+   * @return The queryParameters part of the uri, separated into component parts.
+   */
+  public Map<String, List<String>> getQueryParameters() {
+    return query.getParams();
+  }
+
+  /**
+   * @return All queryParameters parameters with the given name.
+   */
+  public List<String> getQueryParameters(String name) {
+    return query.getParams(name);
+  }
+
+  /**
+   * @return The first queryParameters parameter value with the given name.
+   */
+  public String getQueryParameter(String name) {
+    return query.get(name);
+  }
+
+  /**
+   * @return The queryParameters fragment.
+   */
+  public String getFragment() {
+    return fragment.getString();
+  }
+
+  public UriBuilder setFragment(String str) {
+    fragment.setString(str);
+    return this;
+  }
+
+  public UriBuilder addFragmentParameter(String name, String value) {
+    fragment.add(name, value);
+    return this;
+  }
+
+  public UriBuilder addFragmentParameters(Map<String, String> parameters) {
+    fragment.addAll(parameters);
+    return this;
+  }
+
+  /**
+   * Force overwrites a given fragment parameter with the given value.
+   */
+  public UriBuilder putFragmentParameter(String name, String... values) {
+    fragment.put(name, values);
+    return this;
+  }
+
+  /**
+   * Force overwrites a given fragment parameter with the given value.
+   */
+  public UriBuilder putFragmentParameter(String name, Iterable<String> values) {
+    fragment.put(name, values);
+    return this;
+  }
+
+  /**
+   * Removes a fragment parameter.
+   */
+  public UriBuilder removeFragmentParameter(String name) {
+    fragment.remove(name);
+    return this;
+  }
+
+  /**
+   * @return The fragmentParameters part of the uri, separated into component parts.
+   */
+  public Map<String, List<String>> getFragmentParameters() {
+    return fragment.getParams();
+  }
+
+  /**
+   * @return All fragmentParameters parameters with the given name.
+   */
+  public List<String> getFragmentParameters(String name) {
+    return fragment.getParams(name);
+  }
+
+  /**
+   * @return The first fragmentParameters parameter value with the given name.
+   */
+  public String getFragmentParameter(String name) {
+    return fragment.get(name);
+  }
+
+  /**
+   * Utility method for joining key / value pair parameters into a url-encoded string.
+   */
+  public static String joinParameters(Map<String, List<String>> query) {
+    if (query.isEmpty()) {
+      return null;
+    }
+    StringBuilder buf = new StringBuilder();
+    boolean firstDone = false;
+    for (Map.Entry<String, List<String>> entry : query.entrySet()) {
+      String name = Utf8UrlCoder.encode(entry.getKey());
+      for (String value : entry.getValue()) {
+        if (firstDone) {
+          buf.append('&');
+        }
+        firstDone = true;
+
+        buf.append(name)
+           .append('=')
+           .append(Utf8UrlCoder.encode(value));
+      }
+    }
+    return buf.toString();
+  }
+
+  /**
+   * Utility method for splitting a parameter string into key / value pairs.
+   */
+  public static Map<String, List<String>> splitParameters(String query) {
+    if (query == null) {
+      return Collections.emptyMap();
+    }
+    Map<String, List<String>> params = Maps.newLinkedHashMap();
+    Matcher paramMatcher = QUERY_PATTERN.matcher(query);
+    while (paramMatcher.find()) {
+      String name = Utf8UrlCoder.decode(paramMatcher.group(1));
+      String value = Utf8UrlCoder.decode(paramMatcher.group(2));
+      List<String> values = params.get(name);
+      if (values == null) {
+        values = Lists.newArrayList();
+        params.put(name, values);
+      }
+      values.add(value);
+    }
+    return Collections.unmodifiableMap(params);
+  }
+
+  @Override
+  public String toString() {
+    return toUri().toString();
+  }
+
+  @Override
+  public int hashCode() {
+    return toUri().hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {return true;}
+    if (!(obj instanceof UriBuilder)) {return false;}
+
+    return toString().equals(obj.toString());
+  }
+
+  private static final class ParamString {
+    private final Map<String, List<String>> params;
+    private String str;
+
+    private ParamString() {
+      this.params = Maps.newLinkedHashMap();
+    }
+
+    private ParamString(String str) {
+      this();
+      setString(str);
+    }
+
+    /**
+     * @return The queryParameters fragment.
+     */
+    public String getString() {
+      if (str == null) {
+        str = joinParameters(params);
+      }
+      return str;
+    }
+
+    /**
+     * Assigns the specified query string as the query portion of the uri, automatically decoding
+     * parameters to populate the parameter map for calls to getParameter.
+     */
+    public void setString(String str) {
+      params.clear();
+      params.putAll(splitParameters(str));
+      this.str = str;
+    }
+
+    public void add(String name, String value) {
+      str = null;
+      List<String> values = params.get(name);
+      if (values == null) {
+        values = Lists.newArrayList();
+        params.put(name, values);
+      }
+      values.add(value);
+    }
+
+    public void addAll(Map<String, String> parameters) {
+      str = null;
+      for (Map.Entry<String, String> entry : parameters.entrySet()) {
+        add(entry.getKey(), entry.getValue());
+      }
+    }
+
+    /**
+     * Force overwrites a given query parameter with the given value.
+     */
+    public void put(String name, String... values) {
+      str = null;
+      params.put(name, Lists.newArrayList(values));
+    }
+
+    /**
+     * Force overwrites a given query parameter with the given value.
+     */
+    public void put(String name, Iterable<String> values) {
+      str = null;
+      params.put(name, Lists.newArrayList(values));
+    }
+
+    /**
+     * Removes a query parameter.
+     */
+    public void remove(String name) {
+      str = null;
+      params.remove(name);
+    }
+
+    /**
+     * @return The queryParameters part of the uri, separated into component parts.
+     */
+    public Map<String, List<String>> getParams() {
+      return params;
+    }
+
+    /**
+     * @return All queryParameters parameters with the given name.
+     */
+    public List<String> getParams(String name) {
+      return params.get(name);
+    }
+
+    /**
+     * @return The first queryParameters parameter value with the given name.
+     */
+    public String get(String name) {
+      Collection<String> values = params.get(name);
+      if (values == null || values.isEmpty()) {
+        return null;
+      }
+      return values.iterator().next();
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/uri/UriParser.java b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/UriParser.java
new file mode 100644
index 0000000..fe6e4bb
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/uri/UriParser.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.uri;
+
+/**
+ * An injectable interface for parsing Uris out of String text.
+ */
+public interface UriParser {
+  /**
+   * Produces a new Uri from a text representation.
+   *
+   * @param text The text uri.
+   * @return A new Uri, parsed into components.
+   */
+  Uri parse(String text);
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/Base32.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/Base32.java
new file mode 100644
index 0000000..200dcdd
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/Base32.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import org.apache.commons.codec.BinaryDecoder;
+import org.apache.commons.codec.BinaryEncoder;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.EncoderException;
+
+/**
+ * Implements Base32 encoding using 0-9a-v, with no padding for partial bytes.
+ *
+ * This is suitable for base32 encoding any binary data that needs to be passed
+ * in a web-safe context, but it differs from base32 implementations used in
+ * other contexts.
+ */
+public class Base32 implements BinaryDecoder, BinaryEncoder {
+
+  private static final StringEncoding ENCODER =
+      new StringEncoding("0123456789abcdefghijklmnopqrstuv".toCharArray());
+
+  public static byte[] encodeBase32(byte[] arg0) {
+    return ENCODER.encode(arg0).getBytes();
+  }
+
+  public static byte[] decodeBase32(byte[] arg0) {
+    return ENCODER.decode(new String(arg0));
+  }
+
+  public byte[] decode(byte[] arg0) throws DecoderException {
+    return decodeBase32(arg0);
+  }
+
+  public byte[] encode(byte[] arg0) throws EncoderException {
+    return encodeBase32(arg0);
+  }
+
+  public Object decode(Object object) throws DecoderException {
+    if (!(object instanceof byte[])) {
+      throw new DecoderException(
+          "Parameter supplied to Base32 decode is not a byte[]");
+    }
+    return decodeBase32((byte[]) object);
+  }
+
+  public Object encode(Object object) throws EncoderException {
+    if (!(object instanceof byte[])) {
+      throw new EncoderException(
+          "Parameter supplied to Base32 encode is not a byte[]");
+    }
+    return encodeBase32((byte[]) object);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/CharsetUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/CharsetUtil.java
new file mode 100644
index 0000000..9284a04
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/CharsetUtil.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import com.google.common.base.Charsets;
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Utilities for dealing with character set encoding.
+ */
+public final class CharsetUtil {
+  private CharsetUtil() {}
+
+  /**
+   * A clean version of String#getBytes that does not throw exceptions for
+   * the UTF-8 Charset.
+   *
+   * Replace all Callers with getBytes(Charsets.UTF_8) once
+   * we move to Java 6.
+   *
+   * @param s a string to convert
+   * @return UTF-8 byte array for the input string.
+   */
+  public static byte[] getUtf8Bytes(String s) {
+    if (s == null) {
+      return ArrayUtils.EMPTY_BYTE_ARRAY;
+    }
+    ByteBuffer bb = Charsets.UTF_8.encode(s);
+    return ArrayUtils.subarray(bb.array(), 0, bb.limit());
+
+  }
+
+  /**
+   * A clean version of new String(byte[], "UTF-8")
+   *
+   * Replace all callers with new String(b, Charsets.UTF_8) when
+   * we move to Java 6.
+   *
+   * @param b a byte array of UTF-8 bytes
+   * @return a UTF-8 encoded string
+   */
+  public static String newUtf8String(byte[] b) {
+    return Charsets.UTF_8.decode(ByteBuffer.wrap(b)).toString();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/DateUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/DateUtil.java
new file mode 100644
index 0000000..b4ffbd9
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/DateUtil.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.joda.time.format.DateTimeFormat;
+import org.joda.time.format.DateTimeFormatter;
+import org.joda.time.format.ISODateTimeFormat;
+
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Date parsing and writing utilities.
+ */
+public final class DateUtil {
+
+  private static final DateTimeFormatter RFC1123_DATE_FORMAT = DateTimeFormat
+      .forPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'")
+      .withLocale(Locale.US)
+      .withZone(DateTimeZone.UTC);
+
+  private static final DateTimeFormatter ISO8601_DATE_FORMAT = ISODateTimeFormat.dateTime()
+      .withZone(DateTimeZone.UTC);
+
+  private DateUtil() {}
+
+  /**
+   * Parses an RFC1123 format date.  Returns null if the date fails to parse for
+   * any reason.
+   *
+   * @param dateStr
+   * @return the date
+   */
+  public static Date parseRfc1123Date(String dateStr) {
+    try {
+      return RFC1123_DATE_FORMAT.parseDateTime(dateStr).toDate();
+    } catch (Exception e) {
+      // Don't care.
+      return null;
+    }
+  }
+
+  /**
+   * Parses an ISO8601 formatted datetime into a Date or null
+   * is parsing fails.
+   *
+   * @param dateStr A datetime string in ISO8601 format
+   * @return the date
+   */
+   public static Date parseIso8601DateTime(String dateStr) {
+      try {
+          // joda does our ISO 8601 parsing
+          return new DateTime(dateStr).toDate();
+      } catch(Exception e) {
+          return null;
+      }
+  }
+
+  /**
+   * Formats an ISO 8601 format date.
+   */
+  public static String formatIso8601Date(Date date) {
+      return formatIso8601Date(date.getTime());
+  }
+
+  /**
+   * Formats an ISO 8601 format date.
+   */
+  public static String formatIso8601Date(long time) {
+      return ISO8601_DATE_FORMAT.print(time);
+  }
+
+  /**
+   * Formats an RFC 1123 format date.
+   */
+  public static String formatRfc1123Date(Date date) {
+    return formatRfc1123Date(date.getTime());
+  }
+
+  /**
+   * Formats an RFC 1123 format date.
+   */
+  public static String formatRfc1123Date(long timeStamp) {
+    return RFC1123_DATE_FORMAT.print(timeStamp);
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java
new file mode 100644
index 0000000..392b768
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import com.google.common.base.Preconditions;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Routines for producing hashes.
+ */
+public final class HashUtil {
+  private static final char[] HEX_CHARS = "0123456789abcdef".toCharArray();
+
+  private HashUtil() {}
+  /**
+   * Produces a checksum for the given input data. Currently uses a hexified
+   * message digest.
+   *
+   * @param data
+   * @return The checksum.
+   */
+  public static String checksum(byte[] data) {
+    byte[] hashBytes = getMessageDigest().digest(Preconditions.checkNotNull(data));
+    return bytesToHex(hashBytes);
+  }
+
+  /**
+   * Converts a byte array into a hex string.
+   *
+   * @param hashBytes The byte array to convert.
+   * @return The hex string.
+   */
+  public static String bytesToHex(byte[] hashBytes) {
+    char[] hex = new char[2 * hashBytes.length];
+
+    // Convert to hex. possibly change to base64 in the future for smaller
+    // signatures.
+
+    int offset = 0;
+    for (byte b : hashBytes) {
+      hex[offset++] = HEX_CHARS[(b & 0xF0) >>> 4]; // upper 4 bits
+      hex[offset++] = HEX_CHARS[(b & 0x0F)];       // lower 4 bits
+    }
+    return new String(hex);
+  }
+
+  /**
+   * Produces a raw checksum for the given input data.  Currently uses a message digest
+   *
+   * @param data
+   * @return The checksum.
+   */
+  public static String rawChecksum(byte[] data) {
+    return new String(getMessageDigest().digest(Preconditions.checkNotNull(data)));
+  }
+
+  /**
+   * Provides a {@link MessageDigest} object for calculating checksums.
+   *
+   * @return A MessageDigest object.
+   */
+  public static MessageDigest getMessageDigest() {
+    MessageDigest md;
+    try {
+      md = MessageDigest.getInstance("MD5");
+    } catch (NoSuchAlgorithmException noMD5) {
+      try {
+        md = MessageDigest.getInstance("SHA");
+      } catch (NoSuchAlgorithmException noSha) {
+        throw new RuntimeException("No suitable MessageDigest found!", noSha);
+      }
+    }
+    return md;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java
new file mode 100644
index 0000000..471c46e
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/JsonConversionUtil.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import org.apache.commons.lang3.StringUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utilities for converting a JSON object to and from a URL encoding
+ */
+public final class JsonConversionUtil {
+  private JsonConversionUtil() {}
+
+  private static final Pattern ARRAY_MATCH = Pattern.compile("(\\w+)\\((\\d+)\\)");
+
+  private static final Set<String> RESERVED_PARAMS = ImmutableSet.of("method", "id", "st", "oauth_token");
+
+  @SuppressWarnings("unchecked")
+  public static JSONObject fromRequest(HttpServletRequest request) throws JSONException {
+    Map<String, String[]> params = request.getParameterMap();
+
+    if (!params.containsKey("method")) {
+      return null;
+    }
+
+    JSONObject root = new JSONObject();
+    root.put("method", params.get("method")[0]);
+    if (params.containsKey("id")) {
+      root.put("id", params.get("id")[0]);
+    }
+    JSONObject paramsRoot = new JSONObject();
+    for (Map.Entry<String, String[]> entry : params.entrySet()) {
+      if (!RESERVED_PARAMS.contains(entry.getKey().toLowerCase())) {
+        String[] path = StringUtils.splitPreserveAllTokens(entry.getKey(), '.');
+        JSONObject holder = buildHolder(paramsRoot, path, 0);
+        holder.put(path[path.length - 1], convertToJsonValue(entry.getValue()[0]));
+      }
+    }
+    if (paramsRoot.length() > 0) {
+      root.put("params", paramsRoot);
+    }
+    return root;
+  }
+
+  public static Map<String, String> fromJson(JSONObject obj) throws JSONException {
+    Map<String, String> result = Maps.newHashMap();
+    collect(obj, "", result);
+    return result;
+  }
+
+  private static void collect(Object current, String prefix, Map<String, String> result)
+      throws JSONException {
+    if (current == null) {
+      result.put(prefix, "null");
+      return;
+    }
+
+    if (current instanceof JSONObject) {
+      JSONObject json = (JSONObject) current;
+      Iterator<?> keys = json.keys();
+      while (keys.hasNext()) {
+        String key = (String) keys.next();
+        if (json.isNull(key)) {
+          result.put(prefix + '.' + key, "null");
+        } else {
+          collect(json.get(key), prefix + '.' + key, result);
+        }
+      }
+    } else if (current instanceof JSONArray) {
+      JSONArray jsonArr = (JSONArray) current;
+      if (isAllLiterals(jsonArr)) {
+        // The array is all simple value types
+        String jsonArrayString = jsonArr.toString();
+        //Strip [ & ]
+        jsonArrayString = jsonArrayString.substring(1, jsonArrayString.length() - 1);
+        if (jsonArr.length() == 1) {
+          jsonArrayString = '(' + jsonArrayString + ')';
+        }
+        result.put(prefix, jsonArrayString);
+      } else {
+        for (int i = 0; i < jsonArr.length(); i++) {
+          if (jsonArr.isNull(i)) {
+            result.put(prefix + '(' + i + ')', "null");
+          } else {
+            collect(jsonArr.get(i), prefix + '(' + i + ')', result);
+          }
+        }
+      }
+    } else {
+      result.put(prefix, current.toString());
+    }
+  }
+
+  public static boolean isAllLiterals(JSONArray jsonArr) throws JSONException {
+    for (int i = 0; i < jsonArr.length(); i++) {
+      if (!jsonArr.isNull(i) &&
+          (jsonArr.get(i) instanceof JSONObject ||
+              jsonArr.get(i) instanceof JSONArray)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  static JSONObject parametersToJsonObject(Map<String, String> params) throws JSONException {
+    JSONObject root = new JSONObject();
+
+    for (Map.Entry<String, String> entry : params.entrySet()) {
+      String[] path = StringUtils.splitPreserveAllTokens(entry.getKey(), '.');
+      JSONObject holder = buildHolder(root, path, 0);
+      if (path.length > 1) {
+        holder.put(path[path.length - 1], convertToJsonValue(entry.getValue()));
+      } else {
+        holder.put(path[0], convertToJsonValue(entry.getValue()));
+      }
+    }
+
+    return root;
+  }
+
+  /**
+   * Parse the steps in the path into JSON Objects.
+   */
+  static JSONObject buildHolder(JSONObject root, String[] steps, int currentStep)
+      throws JSONException {
+    if (currentStep > steps.length - 2) {
+      return root;
+    } else {
+      Matcher matcher = ARRAY_MATCH.matcher(steps[currentStep]);
+      if (matcher.matches()) {
+        // Handle as array
+        String fieldName = matcher.group(1);
+        int index = Integer.parseInt(matcher.group(2));
+        JSONArray newArrayStep;
+        if (root.has(fieldName)) {
+          newArrayStep = root.getJSONArray(fieldName);
+        } else {
+          newArrayStep = new JSONArray();
+          root.put(fieldName, newArrayStep);
+        }
+        JSONObject newStep = new JSONObject();
+        newArrayStep.put(index, newStep);
+        return buildHolder(newStep, steps, ++currentStep);
+      } else {
+        JSONObject newStep;
+        if (root.has(steps[currentStep])) {
+          newStep = root.getJSONObject(steps[currentStep]);
+        } else {
+          newStep = new JSONObject();
+          root.put(steps[currentStep], newStep);
+        }
+        return buildHolder(newStep, steps, ++currentStep);
+      }
+    }
+  }
+
+  static Object convertToJsonValue(String value) throws JSONException {
+    if (value == null) {
+      return null;
+    } else if (value.startsWith("(") && value.endsWith(")")) {
+      // explicit form of literal array
+      return new JSONArray('[' + value.substring(1, value.length() - 1) + ']');
+    } else {
+      try {
+        // inferred parsing of literal array
+        // Attempt to parse as an array of literals
+        JSONArray parsedArray = new JSONArray('[' + value + ']');
+        if (parsedArray.length() == 1) {
+          // Not an array.
+          Object obj = parsedArray.get(0);
+          if (obj instanceof Double && !obj.toString().equals(value)) {
+            // Numeric overflow or truncation occurred. ie. large int/long
+            // converted to Double with exponent or Double truncated.
+            // In Shindig we return this as a verbatim String to avoid
+            // loss of data on input, as with lengthy ID values consisting
+            // of only numbers.
+            return value;
+          }
+          return obj;
+        }
+        return parsedArray;
+      } catch (JSONException je) {
+        return value;
+      }
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/OpenSocialVersion.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/OpenSocialVersion.java
new file mode 100644
index 0000000..5633021
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/OpenSocialVersion.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import com.google.common.base.Objects;
+
+import java.util.Comparator;
+import java.util.StringTokenizer;
+
+/**
+ * Convenience class for working with OpenSocial Specification and Feature versions.
+ * Applies the rules specified in the OS specification
+ * http://opensocial-resources.googlecode.com/svn/spec/1.0/Core-Gadget.xml#Versioning
+ *
+ */
+public class OpenSocialVersion {
+
+  public static Comparator<OpenSocialVersion> COMPARATOR = new VersionComparator();
+
+  public int major = -1;
+  public int minor = -1;
+  public int patch = -1;
+
+  /**
+   * Create a new OpenSocialVersion based upon a versionString
+   * @param versionString Version string
+   */
+  public OpenSocialVersion(String versionString){
+    StringTokenizer tokens = new StringTokenizer(versionString,".");
+    try{
+      if(tokens.hasMoreTokens()){
+        major = Integer.parseInt(tokens.nextToken());
+      }
+      if(tokens.hasMoreTokens()){
+        minor = Integer.parseInt(tokens.nextToken());
+      }
+      if(tokens.hasMoreTokens()){
+        patch = Integer.parseInt(tokens.nextToken());
+      }
+    } catch(NumberFormatException ex){
+      //Revert if we couldn't parse
+      major = -1;
+      minor = -1;
+      patch = -1;
+    }
+  }
+
+
+  /**
+   * Same version number matches same version number
+   */
+  @Override
+  public boolean equals(Object o) {
+    if(o instanceof OpenSocialVersion){
+      OpenSocialVersion ver = (OpenSocialVersion)o;
+      return (ver.major == major) && (ver.minor == minor) && (ver.patch == patch);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(major, minor, patch);
+  }
+
+  /**
+   * Tests if OpenSocialVersion is equivalent to the parameter version
+   * @param version Compare with this version
+   * @return TRUE if is equivalent to version
+   */
+  public boolean isEquivalent(OpenSocialVersion version){
+    int cmp = version.major - major;
+    if(cmp == 0 && version.minor > -1 && minor > -1){
+      cmp = version.minor - minor;
+    }
+    if(cmp == 0 && version.patch > -1 && patch > -1){
+      cmp = version.patch - patch;
+    }
+    return cmp == 0;
+  }
+
+  /**
+   * Tests if OpenSocialVersion is equivalent to the parameter version
+   * @param version Compare with this version string
+   * @return TRUE if is equivalent to version
+   */
+  public boolean isEquivalent(String version){
+    return isEquivalent(new OpenSocialVersion(version));
+  }
+
+  /**
+   * Tests if OpenSocialVersion is equal to or greater than parameter version
+   * @param version Compare with this version
+   * @return TRUE if is equal or greater than version
+   */
+  public boolean isEqualOrGreaterThan(OpenSocialVersion version){
+    int cmp = version.major - major;
+    if(cmp == 0){
+      if(version.minor > -1 && minor > -1){
+        cmp = version.minor - minor;
+      } else {
+        cmp = version.minor;
+      }
+    }
+    if(cmp == 0){
+      if(version.patch > -1 && patch > -1){
+        cmp = version.patch - patch;
+      } else {
+        cmp = version.patch;
+      }
+    }
+    return cmp <= 0;
+  }
+
+  /**
+   * Tests if OpenSocialVersion is equal to or greater than parameter version
+   * @param version Compare with this version string
+   * @return TRUE if is equal or greater than version
+   */
+  public boolean isEqualOrGreaterThan(String version){
+    return isEqualOrGreaterThan(new OpenSocialVersion(version));
+  }
+
+}
+
+/**
+ * Utility class for sorting OpenSocialVersion objects
+ *
+ */
+class VersionComparator implements java.util.Comparator<OpenSocialVersion>{
+
+  public int compare(OpenSocialVersion object1, OpenSocialVersion object2) {
+    int cmp = object1.major - object2.major;
+    if(cmp == 0){
+      cmp = object1.minor - object2.minor;
+    }
+    if(cmp == 0){
+      cmp = object1.patch - object2.patch;
+    }
+    return cmp;
+  }
+
+}
+
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/ResourceLoader.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/ResourceLoader.java
new file mode 100644
index 0000000..ed58a32
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/ResourceLoader.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Handles loading contents from resource and file system files.
+ */
+public final class ResourceLoader {
+  public static final String RESOURCE_PREFIX = "res://";
+  public static final String FILE_PREFIX = "file://";
+
+  private ResourceLoader() {}
+  /**
+   * Opens a given path as either a resource or a file, depending on the path
+   * name.
+   *
+   * If path starts with res://, we interpret it as a resource.
+   * If path starts with file://, or path has no prefix, we interpret it as a file.
+   * @param path
+   * @return The opened input stream
+   */
+  public static InputStream open(String path) throws IOException {
+    if (path.startsWith(RESOURCE_PREFIX)) {
+      return openResource(path.substring(RESOURCE_PREFIX.length()));
+    } else if (path.startsWith(FILE_PREFIX)) {
+      path = path.substring(FILE_PREFIX.length());
+    }
+    File file = new File(path);
+    return new FileInputStream(file);
+  }
+
+  /**
+   * Opens a resource
+   * @param resource
+   * @return An input stream for the given named resource
+   * @throws FileNotFoundException
+   */
+  public static InputStream openResource(String resource) throws FileNotFoundException  {
+    ClassLoader cl = ResourceLoader.class.getClassLoader();
+    try {
+      return openResource(cl, resource);
+    } catch (FileNotFoundException e) {
+      // If we cannot find the resource using the current classes class loader
+      // try the current threads
+      cl = Thread.currentThread().getContextClassLoader();
+      return openResource(cl, resource);
+    }
+  }
+
+  /**
+   * Opens a resource
+   * @param cl The classloader to use to find the resource
+   * @param resource The resource to open
+   * @return An input stream for the given named resource
+   * @throws FileNotFoundException
+   */
+
+  private static InputStream openResource(ClassLoader cl, String resource)
+      throws FileNotFoundException {
+    InputStream is = cl.getResourceAsStream(resource.trim());
+    if (is == null) {
+      throw new FileNotFoundException("Can not locate resource: " + resource);
+    }
+    return is;
+  }
+
+  /**
+   * Reads the contents of a resource as a string.
+   *
+   * @param resource
+   * @return Contents of the resource.
+   * @throws IOException
+   */
+  public static String getContent(String resource) throws IOException {
+    InputStream is = openResource(resource);
+    try {
+      return IOUtils.toString(is, "UTF-8");
+    } finally {
+      IOUtils.closeQuietly(is);
+    }
+  }
+
+  /**
+   * @param file
+   * @return The contents of the file (assumed to be UTF-8).
+   * @throws IOException
+   */
+  public static String getContent(File file) throws IOException {
+    InputStream is = new FileInputStream(file);
+    try {
+      return IOUtils.toString(is, "UTF-8");
+    } finally {
+      IOUtils.closeQuietly(is);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/StringEncoding.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/StringEncoding.java
new file mode 100644
index 0000000..a9732e7
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/StringEncoding.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import com.google.common.collect.Sets;
+
+import java.util.Arrays;
+import java.util.TreeSet;
+
+/**
+ * Utility class for encoding strings to and from byte arrays.
+ */
+public class StringEncoding {
+  private final char[] DIGITS;
+  private final int SHIFT;
+  private final int MASK;
+
+  /**
+   * Creates a new encoding based on the supplied set of digits.
+   * @param userDigits set of characters to map bytes to
+   */
+  public StringEncoding(final char[] userDigits) {
+    TreeSet<Character> t = Sets.newTreeSet();
+    for (char c : userDigits) {
+      t.add(c);
+    }
+    char[] digits = new char[t.size()];
+    int i = 0;
+    for (char c : t) {
+      digits[i++] = c;
+    }
+    this.DIGITS = digits;
+    this.MASK = digits.length - 1;
+    this.SHIFT = Integer.numberOfTrailingZeros(MASK+1);
+    if ((MASK+1) != (1<<SHIFT) || digits.length >= 256) {
+      throw new AssertionError(Arrays.toString(digits));
+    }
+  }
+
+  /**
+   * Returns the given bytes in their encoded form.
+   * @param data bytes to convert to string
+   * @return the encoded string
+   */
+  public String encode(byte[] data) {
+    if (data.length == 0) {
+      return "";
+    }
+    StringBuilder result =
+      new StringBuilder(1 + data.length * 8 / DIGITS.length);
+    int buffer = data[0];
+    int next = 1;
+    int bitsLeft = 8;
+    while (bitsLeft > 0 || next < data.length) {
+      if (bitsLeft < SHIFT) {
+        if (next < data.length) {
+          buffer <<= 8;
+          buffer |= (data[next++] & 0xff);
+          bitsLeft += 8;
+        } else {
+          int pad = SHIFT - bitsLeft;
+          buffer <<= pad;
+          bitsLeft += pad;
+        }
+      }
+      int index = MASK & (buffer >> (bitsLeft - SHIFT));
+      bitsLeft -= SHIFT;
+      result.append(DIGITS[index]);
+    }
+    return result.toString();
+  }
+
+  /**
+   * Decodes the given encoded string and returns the original raw bytes.
+   * @param encoded String to encode
+   * @return bytes matching the string
+   */
+  public byte[] decode(String encoded) {
+    if (encoded.length() == 0) {
+      return ArrayUtils.EMPTY_BYTE_ARRAY;
+    }
+    int encodedLength = encoded.length();
+    int outLength = encodedLength * SHIFT / 8;
+    byte[] result = new byte[outLength];
+    int buffer = 0;
+    int next = 0;
+    int bitsLeft = 0;
+    for (char c : encoded.toCharArray()) {
+      buffer <<= SHIFT;
+      buffer |= Arrays.binarySearch(DIGITS, c) & MASK;
+      bitsLeft += SHIFT;
+      if (bitsLeft >= 8) {
+        result[next++] = (byte) (buffer >> (bitsLeft - 8));
+        bitsLeft -= 8;
+      }
+    }
+    assert next == outLength && bitsLeft < SHIFT;
+    return result;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/TimeSource.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/TimeSource.java
new file mode 100644
index 0000000..8fbfaf6
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/TimeSource.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+/**
+ * Simple source of current time to use for dependency injection.
+ */
+public class TimeSource {
+
+  public long currentTimeMillis() {
+    return System.currentTimeMillis();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/util/Utf8UrlCoder.java b/trunk/java/common/src/main/java/org/apache/shindig/common/util/Utf8UrlCoder.java
new file mode 100644
index 0000000..c90b7f7
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/util/Utf8UrlCoder.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+
+/**
+ * Performs url encoding / decoding with forced utf-8. Automatically takes care
+ * of boilerplate exception handling.
+ */
+public final class Utf8UrlCoder {
+
+  private Utf8UrlCoder() {}
+
+  public static String encode(String input) {
+    try {
+      return URLEncoder.encode(input, "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public static String decode(String input) {
+    try {
+      return URLDecoder.decode(input, "UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/xml/DomUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/xml/DomUtil.java
new file mode 100644
index 0000000..852a3d2
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/xml/DomUtil.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.xml;
+
+import com.google.common.collect.Lists;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.traversal.DocumentTraversal;
+import org.w3c.dom.traversal.NodeFilter;
+import org.w3c.dom.traversal.NodeIterator;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility functions for navigating DOM
+ */
+public final class DomUtil {
+
+  private DomUtil() {}
+
+  /**
+   * @return first child node matching the specified name
+   */
+  public static Node getFirstNamedChildNode(Node root, String nodeName) {
+    Node current = root.getFirstChild();
+    while (current != null) {
+      if (current.getNodeName().equalsIgnoreCase(nodeName)) {
+        return current;
+      }
+      current = current.getNextSibling();
+    }
+    return null;
+  }
+
+  /**
+   * @return last child node matching the specified name.
+   */
+  public static Node getLastNamedChildNode(Node root, String nodeName) {
+    Node current = root.getLastChild();
+    while (current != null) {
+      if (current.getNodeName().equalsIgnoreCase(nodeName)) {
+        return current;
+      }
+      current = current.getPreviousSibling();
+    }
+    return null;
+  }
+
+  public static List<Element> getElementsByTagNameCaseInsensitive(Document doc,
+      final Set<String> lowerCaseNames) {
+    final List<Element> result = Lists.newArrayList();
+    NodeIterator nodeIterator = ((DocumentTraversal) doc)
+        .createNodeIterator(doc, NodeFilter.SHOW_ELEMENT,
+            new NodeFilter() {
+              public short acceptNode(Node n) {
+                if (lowerCaseNames.contains(n.getNodeName().toLowerCase())) {
+                  return NodeFilter.FILTER_ACCEPT;
+                }
+                return NodeFilter.FILTER_REJECT;
+              }
+            }, false);
+    for (Node n = nodeIterator.nextNode(); n != null ; n = nodeIterator.nextNode()) {
+      result.add((Element)n);
+    }
+    return result;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlException.java b/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlException.java
new file mode 100644
index 0000000..0d3b7f2
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlException.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.xml;
+
+/**
+ * Exception throw by shindig XML parsing utility routines
+ */
+public class XmlException extends Exception {
+  public XmlException(String message, Exception cause) {
+    super(message, cause);
+  }
+  public XmlException(Exception cause) {
+    super(cause);
+  }
+  public XmlException(String message) {
+    super(message);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java b/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java
new file mode 100644
index 0000000..74af4fd
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/common/xml/XmlUtil.java
@@ -0,0 +1,350 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.xml;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Utility class for simplifying parsing of xml documents. Documents are not validated, and
+ * loading of external files (xinclude, external entities, DTDs, etc.) are disabled.
+ */
+public final class XmlUtil {
+  private static final String CLASSNAME = XmlUtil.class.getName();
+  private static final Logger LOG = Logger.getLogger(CLASSNAME, MessageKeys.MESSAGES);
+
+  // Handles xml errors so that they're not logged to stderr.
+  private static final ErrorHandler ERROR_HANDLER = new ErrorHandler() {
+    public void error(SAXParseException exception) throws SAXException {
+      throw exception;
+    }
+    public void fatalError(SAXParseException exception) throws SAXException {
+      throw exception;
+    }
+    public void warning(SAXParseException exception) {
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "warning", MessageKeys.ERROR_PARSING_XML, exception);
+      }
+    }
+  };
+
+  private static boolean canReuseBuilders = false;
+
+  private static final DocumentBuilderFactory BUILDER_FACTORY
+      = DocumentBuilderFactory.newInstance();
+
+  private static final ThreadLocal<DocumentBuilder> REUSABLE_BUILDER
+      = new ThreadLocal<DocumentBuilder>() {
+          @Override
+          protected DocumentBuilder initialValue() {
+            try {
+              if (LOG.isLoggable(Level.FINE))
+                LOG.fine("Created a new document builder");
+              return BUILDER_FACTORY.newDocumentBuilder();
+            } catch (ParserConfigurationException e) {
+              throw new RuntimeException(e);
+            }
+          }
+        };
+
+  static {
+    // Namespace support is required for <os:> elements
+    BUILDER_FACTORY.setNamespaceAware(true);
+
+    // Disable various insecure and/or expensive options.
+    BUILDER_FACTORY.setValidating(false);
+
+    // Can't disable doctypes entirely because they're usually harmless. External entity
+    // resolution, however, is both expensive and insecure.
+    try {
+      BUILDER_FACTORY.setAttribute(
+          "http://xml.org/sax/features/external-general-entities", false);
+    } catch (IllegalArgumentException e) {
+      // Not supported by some very old parsers.
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "static block", MessageKeys.ERROR_PARSING_EXTERNAL_GENERAL_ENTITIES);
+      }
+    }
+
+    try {
+      BUILDER_FACTORY.setAttribute(
+          "http://xml.org/sax/features/external-parameter-entities", false);
+    } catch (IllegalArgumentException e) {
+      // Not supported by some very old parsers.
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "static block", MessageKeys.ERROR_PARSING_EXTERNAL_PARAMETER_ENTITIES);
+      }
+    }
+
+    try {
+      BUILDER_FACTORY.setAttribute(
+          "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+    } catch (IllegalArgumentException e) {
+      // Only supported by Apache's XML parsers.
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "static block", MessageKeys.ERROR_PARSING_EXTERNAL_DTD);
+      }
+    }
+
+    try {
+      BUILDER_FACTORY.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+    } catch (IllegalArgumentException e) {
+      // Not supported by older parsers.
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "static block", MessageKeys.ERROR_PARSING_SECURE_XML);
+      }
+    }
+
+    try {
+      DocumentBuilder builder = BUILDER_FACTORY.newDocumentBuilder();
+      builder.reset();
+      canReuseBuilders = true;
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "static block", MessageKeys.REUSE_DOC_BUILDERS);
+      }
+    } catch (UnsupportedOperationException e) {
+      // Only supported by newer parsers (xerces 2.8.x+ for instance).
+      canReuseBuilders = false;
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "static block", MessageKeys.NOT_REUSE_DOC_BUILDERS);
+      }
+    } catch (ParserConfigurationException e) {
+      // Only supported by newer parsers (xerces 2.8.x+ for instance).
+      canReuseBuilders = false;
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, CLASSNAME, "static block", MessageKeys.NOT_REUSE_DOC_BUILDERS);
+      }
+    }
+  }
+
+  private XmlUtil() {}
+
+  /**
+   * Extracts an attribute from a node.
+   *
+   * @param node
+   * @param attr
+   * @param def
+   * @return The value of the attribute, or def
+   */
+  public static String getAttribute(Node node, String attr, String def) {
+    NamedNodeMap attrs = node.getAttributes();
+    Node val = attrs.getNamedItem(attr);
+    if (val != null) {
+      return val.getNodeValue();
+    }
+    return def;
+  }
+
+  /**
+   * @param node
+   * @param attr
+   * @return The value of the given attribute, or null if not present.
+   */
+  public static String getAttribute(Node node, String attr) {
+    return getAttribute(node, attr, null);
+  }
+
+  /**
+   * Retrieves an attribute as a URI.
+   * @param node
+   * @param attr
+   * @return The parsed uri, or def if the attribute doesn't exist or can not
+   *     be parsed as a URI.
+   */
+  public static Uri getUriAttribute(Node node, String attr, Uri def) {
+    String uri = getAttribute(node, attr);
+    if (uri != null) {
+      try {
+        return Uri.parse(uri);
+      } catch (IllegalArgumentException e) {
+        return def;
+      }
+    }
+    return def;
+  }
+
+  /**
+   * Retrieves an attribute as a URI.
+   * @param node
+   * @param attr
+   * @return The parsed uri, or null.
+   */
+  public static Uri getUriAttribute(Node node, String attr) {
+    return getUriAttribute(node, attr, null);
+  }
+
+  /**
+   * Retrieves an attribute as a URI, and verifies that the URI is an http
+   * or https URI.
+   * @param node
+   * @param attr
+   * @param base
+   *@param def  @return the parsed uri, or def if the attribute is not a valid http or
+   * https URI.
+   */
+  public static Uri getHttpUriAttribute(Node node, String attr, Uri base, Uri def) {
+    Uri uri = getUriAttribute(node, attr, def);
+    if (uri == null) {
+      return def;
+    }
+    // resolve to base before checking
+    if (base != null) {
+      uri = base.resolve(uri);
+    }
+    if (!"http".equalsIgnoreCase(uri.getScheme()) && !"https".equalsIgnoreCase(uri.getScheme())) {
+      return def;
+    }
+    return uri;
+  }
+
+  /**
+   * Retrieves an attribute as a URI, and verifies that the URI is an http or https URI.
+   * @param node
+   * @param attr
+   * @param base
+   * @return the parsed uri, or null if the attribute is not a valid http or
+   * https URI.
+   */
+  public static Uri getHttpUriAttribute(Node node, String attr, Uri base) {
+    return getHttpUriAttribute(node, attr, base, null);
+  }
+
+  /**
+   * Retrieves an attribute as a boolean.
+   *
+   * @param node
+   * @param attr
+   * @param def
+   * @return True if the attribute exists and is not equal to "false"
+   *    false if equal to "false", and def if not present.
+   */
+  public static boolean getBoolAttribute(Node node, String attr, boolean def) {
+    String value = getAttribute(node, attr);
+    if (value == null) {
+      return def;
+    }
+    return Boolean.parseBoolean(value);
+  }
+
+  /**
+   * @param node
+   * @param attr
+   * @return True if the attribute exists and is not equal to "false"
+   *    false otherwise.
+   */
+  public static boolean getBoolAttribute(Node node, String attr) {
+    return getBoolAttribute(node, attr, false);
+  }
+
+  /**
+   * @return An attribute coerced to an integer.
+   */
+  public static int getIntAttribute(Node node, String attr, int def) {
+    String value = getAttribute(node, attr);
+    if (value == null) {
+      return def;
+    }
+    try {
+      return Integer.parseInt(value);
+    } catch (NumberFormatException e) {
+      return def;
+    }
+  }
+
+  /**
+   * @return An attribute coerced to an integer.
+   */
+  public static int getIntAttribute(Node node, String attr) {
+    return getIntAttribute(node, attr, 0);
+  }
+
+  /**
+   * Fetch a builder from the pool, creating a new one only if necessary.
+   */
+  private static DocumentBuilder getBuilder() throws ParserConfigurationException {
+    DocumentBuilder builder;
+    if (canReuseBuilders) {
+      builder = REUSABLE_BUILDER.get();
+      builder.reset();
+    } else {
+      builder = BUILDER_FACTORY.newDocumentBuilder();
+    }
+    builder.setErrorHandler(ERROR_HANDLER);
+    return builder;
+  }
+
+  /**
+   * Attempts to parse the input xml into a single element.
+   * @param xml
+   * @return The document object
+   * @throws XmlException if a parse error occured.
+   */
+  public static Element parse(String xml) throws XmlException {
+    DocumentBuilder builder = null;
+    try {
+      builder = getBuilder();
+      InputSource is = new InputSource(new StringReader(xml.trim()));
+      return builder.parse(is).getDocumentElement();
+    } catch (SAXParseException e) {
+      throw new XmlException(
+          e.getMessage() + " At: (" + e.getLineNumber() + ',' + e.getColumnNumber() + ')', e);
+    } catch (SAXException e) {
+      throw new XmlException(e);
+    } catch (ParserConfigurationException e) {
+      throw new XmlException(e);
+    } catch (IOException e) {
+      throw new XmlException(e);
+    } finally {
+      // Remove reference to XmlUtils class to insure classes can be unloaded
+      if (builder != null) {
+        builder.setErrorHandler(null);
+      }
+    }
+  }
+
+  /**
+   * Same as {@link #parse(String)}, but throws a RuntimeException instead of XmlException.
+   */
+  public static Element parseSilent(String xml) {
+    try {
+      return parse(xml);
+    } catch (XmlException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/BasicContainerConfig.java b/trunk/java/common/src/main/java/org/apache/shindig/config/BasicContainerConfig.java
new file mode 100644
index 0000000..724395b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/BasicContainerConfig.java
@@ -0,0 +1,367 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import org.apache.shindig.common.JsonSerializer;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+/**
+ * Basic container configuration class, without expression support.
+ *
+ * We use a cascading model, so you only have to specify attributes in your
+ * config that you actually want to change.
+ *
+ * Configurations can be added/modified/removed using transactions. The
+ * configuration is protected with a read/write lock.
+ */
+public class BasicContainerConfig implements ContainerConfig {
+
+  protected final Set<ConfigObserver> observers =
+      Sets.newSetFromMap(new WeakHashMap<ConfigObserver, Boolean>());
+  protected Map<String, Map<String, Object>> config = Maps.newHashMap();
+
+  public Collection<String> getContainers() {
+    return Collections.unmodifiableSet(config.keySet());
+  }
+
+  public Map<String, Object> getProperties(String container) {
+    return config.get(container);
+  }
+
+  public Object getProperty(String container, String name) {
+    Map<String, Object> containerData = config.get(container);
+    if (containerData == null) {
+      return null;
+    }
+    return containerData.get(name);
+  }
+
+  public String getString(String container, String property) {
+    Object value = getProperty(container, property);
+    if (value == null) {
+      return null;
+    }
+    return value.toString();
+  }
+
+  public int getInt(String container, String property) {
+    Object value = getProperty(container, property);
+    if (value instanceof Number) {
+      return ((Number) value).intValue();
+    } else if (value instanceof String) {
+      try {
+        return Integer.parseInt((String) value);
+      } catch (NumberFormatException nfe) {
+        return 0;
+      }
+    }
+    return 0;
+  }
+
+  public boolean getBool(String container, String property) {
+    Object value = getProperty(container, property);
+    if (value instanceof Boolean) {
+      return ((Boolean) value).booleanValue();
+    } else if (value instanceof String) {
+      return "true".equalsIgnoreCase((String) value);
+    }
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T> List<T> getList(String container, String property) {
+    Object value = getProperty(container, property);
+    if (value instanceof List) {
+      return (List<T>) value;
+    }
+    return Collections.emptyList();
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T> Map<String, T> getMap(String container, String property) {
+    Object value = getProperty(container, property);
+    if (value instanceof Map) {
+      return (Map<String, T>) value;
+    }
+    return Collections.emptyMap();
+  }
+
+  public void addConfigObserver(ConfigObserver observer, boolean notifyNow) {
+    observers.add(observer);
+    if (notifyNow) {
+      notifyObservers(getContainers(), ImmutableSet.<String>of());
+    }
+  }
+
+  public Transaction newTransaction() {
+    return new BasicTransaction();
+  }
+
+  /**
+   * Notifies the configuration observers that some containers' configurations
+   * have been changed.
+   *
+   * @param changed The names of the containers that have been added or changed.
+   * @param removed The names of the containers that have been removed.
+   */
+  protected void notifyObservers(Collection<String> changed, Collection<String> removed) {
+    for (ConfigObserver observer : observers) {
+      observer.containersChanged(this, changed, removed);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return JsonSerializer.serialize(config);
+  }
+
+  protected class BasicTransaction implements Transaction {
+    protected boolean clear = false;
+    protected Map<String, Map<String, Object>> setContainers = Maps.newHashMap();
+    protected Set<String> removeContainers = Sets.newHashSet();
+    protected ContainerConfigException throwException = null;
+
+    public Transaction clearContainers() {
+      clear = true;
+      return this;
+    }
+
+    public Transaction addContainer(Map<String, Object> container) {
+      Object names = container.get(CONTAINER_KEY);
+      if (names instanceof Collection<?>) {
+        for (Object name : (Collection<?>) names) {
+          setContainers.put(name.toString(), container);
+        }
+      } else if (names != null) {
+        setContainers.put(names.toString(), container);
+      } else {
+        throwException = new ContainerConfigException(
+            "A container configuration doesn't have the " + CONTAINER_KEY + " property");
+      }
+      return this;
+    }
+
+    public Transaction removeContainer(String name) {
+      removeContainers.add(name);
+      return this;
+    }
+
+    public void commit() throws ContainerConfigException {
+      if (throwException != null) {
+        throw throwException;
+      }
+      Set<String> removed = Sets.newHashSet();
+      Set<String> changed = Sets.newHashSet();
+      synchronized (BasicContainerConfig.this) {
+        BasicContainerConfig tmpConfig = getTemporaryConfig(!clear);
+        changeContainersInConfig(tmpConfig, setContainers, removeContainers);
+        // This point will not be reached if an exception was thrown.
+        diffConfiguration(tmpConfig, changed, removed);
+        setNewConfig(tmpConfig);
+      }
+      notifyObservers(changed, removed);
+    }
+
+    /**
+     * Creates a temporary ContainerConfig object that optionally contains a
+     * copy of the current configuration.
+     *
+     * If you subclass {@link BasicContainerConfig} and you change its
+     * internals, you must generally override this method to generate an object
+     * of the same type as your subclass, and to fill its contents correctly.
+     *
+     * @param copyValues Whether the current configuration should be copied.
+     * @return A new ContainerConfig object of the appropriate type.
+     */
+    protected BasicContainerConfig getTemporaryConfig(boolean copyValues) {
+      BasicContainerConfig tmp = new BasicContainerConfig();
+      if (copyValues) {
+        tmp.config = deepCopyConfig(config);
+      }
+      return tmp;
+    }
+
+    /**
+     * Applies the requested changes in a container configuration.
+     *
+     * @param newConfig The container configuration object to modify.
+     * @param setContainers A map from container name to container to
+     *        add/modify.
+     * @param removeContainers A set of names of containers to remove.
+     * @throws ContainerConfigException If there was a problem setting the new
+     *         configuration.
+     */
+    protected void changeContainersInConfig(BasicContainerConfig newConfig,
+        Map<String, Map<String, Object>> setContainers, Set<String> removeContainers)
+        throws ContainerConfigException {
+      newConfig.config.putAll(setContainers);
+      for (String container : removeContainers) {
+        newConfig.config.remove(container);
+      }
+      for (String container : newConfig.config.keySet()) {
+        newConfig.config.put(container, mergeParents(container, newConfig.config));
+      }
+    }
+
+    /**
+     * Replaces the old configuration with the new configuration.
+     *
+     * @param newConfig The map that contains the new configuration.
+     */
+    protected void setNewConfig(BasicContainerConfig newConfig) {
+      config = newConfig.config;
+    }
+
+    /**
+     * Recursively merge values from parent containers in the prototype chain.
+     *
+     * For example, for the following two containers:
+     * { 'gadgets.container': ['default'],
+     *   'base': '/gadgets/foo',
+     *   'user': 'peter',
+     *   'map': { 'latitude': 42, 'longitude': -8 },
+     *   'data': [ 'foo', 'bar' ] }
+     * { 'gadgets.container': ['new'],
+     *   'user': 'anne',
+     *   'colour': 'green',
+     *   'map': { 'longitude': 130 },
+     *   'data': null }
+     *
+     * It would result in a merged "new" container that looks like this:
+     * { 'gadgets.container': ['new'],
+     *   'base': '/gadgets/foo',
+     *   'user': 'anne',
+     *   'colour': 'green',
+     *   'map': { 'latitude': 42, 'longitude': 130 },
+     *   'data': null }
+     *
+     * @return The container merged with all parents.
+     * @throws ContainerConfigException If there is an invalid parent parameter
+     *         in the prototype chain.
+     */
+    protected Map<String, Object> mergeParents(String name, Map<String, Map<String, Object>> config)
+        throws ContainerConfigException {
+      Map<String, Object> container = config.get(name);
+      if (ContainerConfig.DEFAULT_CONTAINER.equals(name)) {
+        return container;
+      }
+
+      String parent = container.get(PARENT_KEY) != null
+          ? container.get(PARENT_KEY).toString() : ContainerConfig.DEFAULT_CONTAINER;
+      if (!config.containsKey(parent)) {
+        throw new ContainerConfigException(
+            "Unable to locate parent '" + parent + "' required by " + container.get(CONTAINER_KEY));
+      }
+      return mergeObjects(mergeParents(parent, config), container);
+    }
+
+    /**
+     * Merges two container configurations together (recursively), adding values
+     * from "parentValues" into "container" if "container" doesn't already
+     * define them.
+     *
+     * @param parentValues The values that will be added if absent.
+     * @param container The container to merge the values into.
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> mergeObjects(
+        Map<String, Object> parentValues, Map<String, Object> container) {
+      // Clone the object with the parent values
+      Map<String, Object> clone = Maps.newHashMap(parentValues);
+      // Walk parameter list for the container and merge recursively.
+      for (Map.Entry<String, Object> entry : container.entrySet()) {
+        String field = entry.getKey();
+        Object fromParents = clone.get(field);
+        Object fromContainer = entry.getValue();
+        // Merge if object type is Map
+        if (fromContainer instanceof Map<?, ?> && fromParents instanceof Map<?, ?>) {
+          clone.put(field, mergeObjects(
+              (Map<String, Object>) fromParents, (Map<String, Object>) fromContainer));
+        } else {
+          // Otherwise we just overwrite it.
+          clone.put(field, fromContainer);
+        }
+      }
+      return clone;
+    }
+
+    /**
+     * Calculates the difference between the current and new configurations.
+     *
+     * @param newConfig The object containing the new configuration.
+     * @param changed A set that will be populated with the names of the
+     *        added/modified containers.
+     * @param removed A set that will be populated with the names of the removed
+     *        containers.
+     */
+    private void diffConfiguration(
+        BasicContainerConfig newConfig, Set<String> changed, Set<String> removed) {
+      removed.addAll(Sets.difference(config.keySet(), newConfig.config.keySet()));
+      for (String container : newConfig.config.keySet()) {
+        if (!newConfig.config.get(container).equals(config.get(container))) {
+          changed.add(container);
+        }
+      }
+    }
+
+    /**
+     * Returns a deep copy of a configuration object.
+     *
+     * @param config The configuration object to copy.
+     * @return A copy of the configuration object.
+     */
+    @SuppressWarnings("unchecked")
+    protected Map<String, Map<String, Object>> deepCopyConfig(
+        Map<String, Map<String, Object>> config) {
+      return (Map<String, Map<String, Object>>) deepCopyObject(config);
+    }
+
+    private Object deepCopyObject(Object obj) {
+      if (obj instanceof Map<?, ?>) {
+        Map<?, ?> objMap = (Map<?, ?>) obj;
+        Map<Object, Object> map = Maps.newHashMap();
+        for (Entry<?, ?> entry : objMap.entrySet()) {
+          map.put(entry.getKey(), deepCopyObject(entry.getValue()));
+        }
+        return map;
+      } else if (obj instanceof List<?>) {
+        List<?> objList = (List<?>) obj;
+        List<Object> list = Lists.newArrayList();
+        for (Object elem : objList) {
+          list.add(deepCopyObject(elem));
+        }
+        return list;
+      } else {
+        return obj;
+      }
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfig.java b/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfig.java
new file mode 100644
index 0000000..ef4edcf
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfig.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a container configuration.
+ *
+ * Container configurations are used to support multiple, independent
+ * configurations in the same server instance. Global configuration values are
+ * handled via traditional mechanisms such as properties files or command-line
+ * flags bound through Guice's @Named annotation.
+ *
+ * This interface is implemented by two classes:
+ *
+ * - {@link BasicContainerConfig} provides configuration inheritance;
+ * - {@link ExpressionContainerConfig} extends {@link BasicContainerConfig} and
+ * also provides expression evaluation in string values and properties.
+ *
+ * Container configurations are stored by default in JSON format, for easy
+ * sharing with the code found in the PHP implementation of Shindig, and for
+ * easy readability. They can be loaded with the methods in
+ * {@link JsonContainerConfigLoader}.
+ */
+@ImplementedBy(JsonContainerConfig.class)
+public interface ContainerConfig {
+
+  public static final String PARENT_KEY = "parent";
+  // TODO: Rename this to simply "container", gadgets.container is unnecessary.
+  public static final String CONTAINER_KEY = "gadgets.container";
+  public static final String DEFAULT_CONTAINER = "default";
+
+  /**
+   * @return The set of all containers that are currently registered.
+   */
+  Collection<String> getContainers();
+
+  /**
+   * Fetch all properties for the given container configuration.
+   */
+  Map<String, Object> getProperties(String container);
+
+  /**
+   * @return The configuration property stored under the given name for the
+   *         given container.
+   */
+  Object getProperty(String container, String name);
+
+  /**
+   * @return The configuration property stored under the given name for the
+   *         given container, or null if it is not defined or not a string.
+   */
+  String getString(String container, String name);
+
+  /**
+   * @return The configuration property stored under the given name for the
+   *         given container, or 0 if it is not defined or not a number.
+   */
+  int getInt(String container, String name);
+
+
+  /**
+   * @return The configuration property stored under the given name for the
+   *         given container, or false if it is not defined or not a boolean.
+   */
+  boolean getBool(String container, String name);
+
+  /**
+   * @return The configuration property stored under the given name for the
+   *         given container, or an empty list if it is not defined or not a
+   *         list.
+   */
+  <T> List<T> getList(String container, String name);
+
+  /**
+   * @return The configuration property stored under the given name for the
+   *         given container, or an empty map if it is not defined or not a map.
+   */
+  <T> Map<String, T> getMap(String container, String name);
+
+  /**
+   * Creates a new transaction to create, modify or remove containers.
+   *
+   * @return The new transaction object.
+   */
+  Transaction newTransaction();
+
+  /**
+   * Adds an observer that will be notified when the configuration changes.
+   *
+   * @param observer The observer to be notified.
+   * @param notifyNow If true, the observer will receive an immediate
+   *        notification for the current configuration.
+   */
+  void addConfigObserver(ConfigObserver observer, boolean notifyNow);
+
+  /**
+   * A transaction object allows to create, modify and remove one or more
+   * containers at a time.
+   */
+  interface Transaction {
+
+    /**
+     * Clears the container configuration before performing the other operations
+     * in the transaction.
+     *
+     * @return The transaction object, to allow chaining operations.
+     */
+    Transaction clearContainers();
+
+    /**
+     * Adds or modifies a container configuration.
+     *
+     * A container's names are specified in the gadgets.container property. If
+     * it is an array, a copy of the container will be created for each name in
+     * that property.
+     *
+     * @param container The container's new configuration, as a map from
+     *        property name to property contents.
+     * @return The transaction object, to allow chaining operations.
+     */
+    Transaction addContainer(Map<String, Object> container);
+
+    /**
+     * Removes a container configuration.
+     *
+     * @param name The name of the container to remove.
+     * @return The transaction object, to allow chaining operations.
+     */
+    Transaction removeContainer(String name);
+
+    /**
+     * Performs all the transaction operations on the container configuration.
+     *
+     * @throws ContainerConfigException If there was a problem applying the new
+     *         configuration. If this exception is thrown, the existing
+     *         configuration will not be modified.
+     */
+    void commit() throws ContainerConfigException;
+  }
+
+  /**
+   * Interface for objects that get notified when container configurations are
+   * changed.
+   */
+  interface ConfigObserver {
+
+    /**
+     * Notifies the object that some container configurations have been added or
+     * modified.
+     *
+     * @param config The ContainerConfig object where the configuration was
+     *        changed.
+     * @param changed The names of the containers that have been added or
+     *        modified.
+     * @param removed The names of the containers that have been removed.
+     */
+    void containersChanged(
+        ContainerConfig config, Collection<String> changed, Collection<String> removed);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfigELResolver.java b/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfigELResolver.java
new file mode 100644
index 0000000..ea4a45f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfigELResolver.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+
+import javax.el.ELContext;
+import javax.el.ELResolver;
+import javax.el.PropertyNotWritableException;
+
+/**
+ * ELResolver that handles adds support for:
+ *   - the "Cur" property, for explicit reference to the current container config
+ *   - the "parent" property, for explicit and recursive reference to config parents
+ *   - implicit reference to top-level properties in the current container config
+ *     or inside any parents
+ */
+public class ContainerConfigELResolver extends ELResolver {
+  /** Key for the current container. */
+  public static final String CURRENT_CONFIG_KEY = "Cur";
+
+  private final ContainerConfig config;
+  private final String currentContainer;
+
+  public ContainerConfigELResolver(ContainerConfig config, String currentContainer) {
+    this.config = config;
+    this.currentContainer = currentContainer;
+  }
+
+  @Override
+  public Class<?> getCommonPropertyType(ELContext context, Object base) {
+    if ((base == null) || (base instanceof ContainerReference)) {
+      return String.class;
+    }
+
+    return null;
+  }
+
+  @Override
+  public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+      Object base) {
+    return null;
+  }
+
+  @Override
+  public Class<?> getType(ELContext context, Object base, Object property) {
+    if ((base == null) || (base instanceof ContainerReference)) {
+      context.setPropertyResolved(true);
+      Object value = getValue(context, base, property);
+      return (value == null) ? null : value.getClass();
+    }
+
+    return null;
+  }
+
+  @Override
+  public Object getValue(ELContext context, Object base, Object property) {
+    // Handle all requests off the base, and anything that is a reference to
+    // a container
+    String container;
+    if (base == null) {
+      container = currentContainer;
+    } else if (base instanceof ContainerReference) {
+      container = ((ContainerReference) base).containerName;
+    } else {
+      // Not ours - return without setPropertyResolved(true)
+      return null;
+    }
+
+    context.setPropertyResolved(true);
+    if (JsonContainerConfig.PARENT_KEY.equals(property)) {
+      // "parent": find the parent of the base, and return a ContainerReference
+      String parent = config.getString(container, JsonContainerConfig.PARENT_KEY);
+      if (parent == null) {
+        return null;
+      } else {
+        ContainerReference reference = new ContainerReference();
+        reference.containerName = parent;
+        return reference;
+      }
+    } else if (CURRENT_CONFIG_KEY.equals(property) && base == null) {
+      // "Cur": return a reference to the current container
+      ContainerReference reference = new ContainerReference();
+      reference.containerName = currentContainer;
+      return reference;
+    } else {
+      // Referring to a property of an existing container
+      return config.getProperty(container, property.toString());
+    }
+  }
+
+  @Override
+  public boolean isReadOnly(ELContext context, Object base, Object property) {
+    if ((base == null) || (base instanceof ContainerReference)) {
+      context.setPropertyResolved(true);
+      return true;
+    }
+
+    return false;
+  }
+
+  @Override
+  public void setValue(ELContext context, Object base, Object property, Object value) {
+    // No support for mutating container configs
+    if ((base == null) || (base instanceof ContainerReference)) {
+      throw new PropertyNotWritableException();
+    }
+  }
+
+  /** A reference to the container, for later EL evaluation */
+  private static class ContainerReference {
+    public String containerName;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfigException.java b/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfigException.java
new file mode 100644
index 0000000..cad2735
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/ContainerConfigException.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+/**
+ * Thrown for problems encountered with the container configuration.
+ */
+public class ContainerConfigException extends Exception {
+
+  public ContainerConfigException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
+
+  public ContainerConfigException(String msg) {
+    super(msg);
+  }
+
+  public ContainerConfigException(Throwable cause) {
+    super(cause);
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/DynamicConfigProperty.java b/trunk/java/common/src/main/java/org/apache/shindig/config/DynamicConfigProperty.java
new file mode 100644
index 0000000..95c09c5
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/DynamicConfigProperty.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.expressions.Expressions;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ValueExpression;
+
+
+/**
+ * String property that can be interpreted using a container context.
+ *
+ * Implements CharSequence strictly as a marker. Only toString is supported.
+ */
+public class DynamicConfigProperty implements CharSequence {
+  private static final String classname = DynamicConfigProperty.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+  private final ELContext context;
+  private final ValueExpression expression;
+
+  public DynamicConfigProperty(String value, Expressions expressions, ELContext context) {
+    this.context = context;
+    this.expression = expressions.parse(value, String.class);
+  }
+
+  @Override
+  public String toString() {
+    try {
+      return (String) expression.getValue(context);
+    } catch (ELException e) {
+        if (LOG.isLoggable(Level.WARNING)) {
+          //log the i18n expression
+          LOG.logp(Level.WARNING, classname, "toString", MessageKeys.EVAL_EL_FAILED, new Object[] {expression.getExpressionString()});
+          //now log the stacktrace
+          LOG.logp(Level.WARNING, classname, "toString", MessageKeys.EVAL_EL_FAILED, e);
+        }
+      return "";
+    }
+  }
+
+  public char charAt(int index) {
+    throw new UnsupportedOperationException();
+  }
+
+  public int length() {
+    throw new UnsupportedOperationException();
+  }
+
+  public CharSequence subSequence(int start, int end) {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/ExpressionContainerConfig.java b/trunk/java/common/src/main/java/org/apache/shindig/config/ExpressionContainerConfig.java
new file mode 100644
index 0000000..56f7094
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/ExpressionContainerConfig.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Singleton;
+
+import org.apache.shindig.expressions.Expressions;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ValueExpression;
+
+/**
+ * Represents a container configuration that uses expressions in values.
+ *
+ * We use a cascading model, so you only have to specify attributes in your
+ * config that you actually want to change.
+ *
+ * String values may use expressions. The variable context defaults to the
+ * 'current' container, but parent values may be accessed through the special
+ * "parent" property.
+ *
+ * get* can take either a simple property name (foo), or an EL expression
+ * (${foo.bar}).
+ */
+@Singleton
+public class ExpressionContainerConfig extends BasicContainerConfig {
+
+  protected Map<String, Map<String, Object>> rawConfig;
+  private final Expressions expressions;
+
+  public ExpressionContainerConfig(Expressions expressions) {
+    this.expressions = expressions;
+    this.rawConfig = Maps.newHashMap();
+  }
+
+  /**
+   * Creates a new transaction to create, modify or remove containers.
+   *
+   * @return The new transaction object.
+   */
+  @Override
+  public Transaction newTransaction() {
+    return new ExpressionTransaction();
+  }
+
+  @Override
+  public Object getProperty(String container, String property) {
+    if (property.startsWith("${")) {
+      // An expression!
+      try {
+        ValueExpression expression = expressions.parse(property, Object.class);
+        return expression.getValue(createExpressionContext(container));
+      } catch (ELException e) {
+        return null;
+      }
+    }
+
+    return super.getProperty(container, property);
+  }
+
+  protected Expressions getExpressions() {
+    return expressions;
+  }
+
+  protected ELContext createExpressionContext(String container) {
+    return getExpressions().newELContext(new ContainerConfigELResolver(this, container));
+  }
+
+  public class ExpressionTransaction extends BasicTransaction {
+
+    @Override
+    protected BasicContainerConfig getTemporaryConfig(boolean copyValues) {
+      ExpressionContainerConfig tmp = new ExpressionContainerConfig(getExpressions());
+      if (copyValues) {
+        tmp.rawConfig = deepCopyConfig(rawConfig);
+        tmp.config = deepCopyConfig(config);
+      }
+      return tmp;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    protected void changeContainersInConfig(BasicContainerConfig config,
+        Map<String, Map<String, Object>> setContainers, Set<String> removeContainers)
+        throws ContainerConfigException {
+      ExpressionContainerConfig tmp = (ExpressionContainerConfig) config;
+      tmp.rawConfig.putAll(setContainers);
+      for (String container : removeContainers) {
+        tmp.rawConfig.remove(container);
+      }
+      tmp.config.clear();
+      for (String container : tmp.rawConfig.keySet()) {
+        Map<String, Object> merged = mergeParents(container, tmp.rawConfig);
+        tmp.rawConfig.put(container, merged);
+        Map<String, Object> value =
+            (Map<String, Object>) parseAll(merged, tmp.createExpressionContext(container));
+        tmp.config.put(container, value);
+      }
+      for (String container : tmp.config.keySet()) {
+        Map<String, Object> value = (Map<String, Object>) evaluateAll(tmp.config.get(container));
+        tmp.config.put(container, value);
+      }
+    }
+
+    @Override
+    protected void setNewConfig(BasicContainerConfig newConfig) {
+      ExpressionContainerConfig tmp = (ExpressionContainerConfig) newConfig;
+      rawConfig = tmp.rawConfig;
+      config = tmp.config;
+    }
+
+    private Object parseAll(Object value, ELContext context) {
+      if (value instanceof String) {
+        return new DynamicConfigProperty((String) value, expressions, context);
+      } else if (value instanceof Map<?, ?>) {
+        Map<?, ?> mapValue = (Map<?, ?>) value;
+        Map<Object, Object> newMap = Maps.newHashMap();
+        for (Map.Entry<?, ?> entry : mapValue.entrySet()) {
+          newMap.put(entry.getKey(), parseAll(entry.getValue(), context));
+        }
+        return Collections.unmodifiableMap(newMap);
+      } else if (value instanceof List<?>) {
+        List<Object> newList = Lists.newArrayList();
+        for (Object entry : (List<?>) value) {
+          newList.add(parseAll(entry, context));
+        }
+        return Collections.unmodifiableList(newList);
+      } else {
+        return value;
+      }
+    }
+
+    private Object evaluateAll(Object value) {
+      if (value instanceof CharSequence) {
+        return value.toString();
+      } else if (value instanceof Map<?, ?>) {
+        Map<?, ?> mapValue = (Map<?, ?>) value;
+        Map<Object, Object> newMap = Maps.newHashMap();
+        for (Map.Entry<?, ?> entry : mapValue.entrySet()) {
+          newMap.put(entry.getKey(), evaluateAll(entry.getValue()));
+        }
+        return Collections.unmodifiableMap(newMap);
+      } else if (value instanceof List<?>) {
+        List<Object> newList = Lists.newArrayList();
+        for (Object entry : (List<?>) value) {
+          newList.add(evaluateAll(entry));
+        }
+        return Collections.unmodifiableList(newList);
+      } else {
+        return value;
+      }
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/JsonContainerConfig.java b/trunk/java/common/src/main/java/org/apache/shindig/config/JsonContainerConfig.java
new file mode 100644
index 0000000..45ad856
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/JsonContainerConfig.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.expressions.Expressions;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+
+// Temporary replacement of javax.annotation.Nullable
+import org.apache.shindig.common.Nullable;
+
+/**
+ * Represents a container configuration using JSON notation.
+ *
+ * See config/container.js for an example configuration.
+ *
+ * We use a cascading model, so you only have to specify attributes in
+ * your config that you actually want to change.
+ *
+ * String values may use expressions. The variable context defaults to the 'current' container,
+ * but parent values may be accessed through the special "parent" property.
+ */
+@Singleton
+public class JsonContainerConfig extends ExpressionContainerConfig {
+
+  // Used by tests
+  public JsonContainerConfig(String containers, Expressions expressions) throws ContainerConfigException {
+    this(containers, "localhost", "8080", "",expressions);
+  }
+  /**
+   * Creates a new configuration from files.
+   * @throws ContainerConfigException
+   */
+  @Inject
+  public JsonContainerConfig(@Named("shindig.containers.default") String containers,
+                             @Nullable @Named("shindig.host") String host,
+                             @Nullable @Named("shindig.port") String port,
+                             @Nullable @Named("shindig.contextroot") String contextRoot,
+                             Expressions expressions)
+      throws ContainerConfigException {
+    super(expressions);
+    JsonContainerConfigLoader.getTransactionFromFile(containers, host, port, contextRoot, this).commit();
+  }
+
+  /**
+   * Creates a new configuration from a JSON Object, for use in testing.
+   * @throws ContainerConfigException
+   */
+  public JsonContainerConfig(JSONObject json, Expressions expressions) throws ContainerConfigException {
+    super(expressions);
+    Transaction transaction = newTransaction();
+    Iterator<?> keys = json.keys();
+    while (keys.hasNext()) {
+      JSONObject optJSONObject = json.optJSONObject((String) keys.next());
+      if (optJSONObject != null) {
+        transaction.addContainer(JsonContainerConfigLoader.parseJsonContainer(optJSONObject));
+      }
+    }
+    transaction.commit();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/config/JsonContainerConfigLoader.java b/trunk/java/common/src/main/java/org/apache/shindig/config/JsonContainerConfigLoader.java
new file mode 100644
index 0000000..144b604
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/config/JsonContainerConfigLoader.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.config.ContainerConfig.Transaction;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A class to build container configurations from JSON notation.
+ *
+ * See config/container.js for an example configuration.
+ *
+ * We use a cascading model, so you only have to specify attributes in your
+ * config that you actually want to change.
+ *
+ * String values may use expressions. The variable context defaults to the
+ * 'current' container, but parent values may be accessed through the special
+ * "parent" property.
+ */
+public class JsonContainerConfigLoader {
+
+  private static final String classname = JsonContainerConfigLoader.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+  private static final Splitter CRLF_SPLITTER = Splitter.onPattern("[\r\n]+");
+
+  public static final char FILE_SEPARATOR = ',';
+  public static final String SERVER_PORT = "SERVER_PORT";
+  public static final String SERVER_HOST = "SERVER_HOST";
+  public static final String CONTEXT_ROOT = "CONTEXT_ROOT";
+
+  private JsonContainerConfigLoader() {
+  }
+
+  /**
+   * Creates a transaction to append the contents of one or more files or
+   * resources to an existing configuration.
+   *
+   * @param containers The comma-separated list of files or resources to load
+   *        the container configurations from.
+   * @param host The hostname where Shindig is running.
+   * @param port The port number where Shindig is receiving requests.
+   * @param contextRoot contextRoot where Shindig module is deployed
+   * @param containerConfig The container configuration to add the contents of
+   *        the file to.
+   * @return A transaction to add the new containers to the configuration.
+   * @throws ContainerConfigException If there was a problem reading the files.
+   */
+  public static Transaction getTransactionFromFile(
+      String containers, String host, String port, String contextRoot,ContainerConfig containerConfig)
+      throws ContainerConfigException {
+    return addToTransactionFromFile(containers, host, port, contextRoot,containerConfig.newTransaction());
+  }
+
+  /**
+   * Appends the contents of one or more files or resources to an transaction.
+   *
+   * @param containers The comma-separated list of files or resources to load
+   *        the container configurations from.
+   * @param host The hostname where Shindig is running.
+   * @param port The port number where Shindig is receiving requests.
+   * @param transaction The transaction to add the contents of the file to.
+   * @return The transaction, to allow chaining.
+   * @throws ContainerConfigException If there was a problem reading the files.
+   */
+  public static Transaction addToTransactionFromFile(
+      String containers, String host, String port, String contextRoot, Transaction transaction)
+      throws ContainerConfigException {
+    List<Map<String, Object>> config = loadContainers(containers);
+    addHostAndPortToDefaultContainer(config, host, port,contextRoot);
+    addContainersToTransaction(transaction, config);
+    return transaction;
+  }
+
+  /**
+   * Parses a container in JSON notation.
+   *
+   * @param json The container configuration in JSON notation.
+   * @return A parsed container configuration.
+   */
+  public static Map<String, Object> parseJsonContainer(JSONObject json) {
+    return jsonToMap(json);
+  }
+
+  /**
+   * Parses a container in JSON notation.
+   *
+   * @param json The container configuration in JSON notation.
+   * @return A parsed container configuration.
+   * @throws JSONException If there was a problem parsing the container.
+   */
+  public static Map<String, Object> parseJsonContainer(String json) throws JSONException {
+    return parseJsonContainer(new JSONObject(json));
+  }
+
+  /**
+   * Loads containers from the specified resource. Follows the same rules as
+   * {@code JsFeatureLoader.loadFeatures} for locating resources.
+   *
+   * @param path
+   * @throws ContainerConfigException
+   */
+  private static List<Map<String, Object>> loadContainers(String path)
+      throws ContainerConfigException {
+    List<Map<String, Object>> all = Lists.newArrayList();
+    try {
+      for (String location : Splitter.on(FILE_SEPARATOR).split(path)) {
+        if (location.startsWith("res://")) {
+          location = location.substring(6);
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "loadContainers", MessageKeys.LOAD_RESOURCES_FROM, new Object[] {location});
+          }
+          if (path.endsWith(".txt")) {
+            loadResources(CRLF_SPLITTER.split(ResourceLoader.getContent(location)), all);
+          } else {
+            loadResources(ImmutableList.of(location), all);
+          }
+        } else {
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "loadContainers", MessageKeys.LOAD_FILES_FROM, new Object[] {location});
+          }
+          File file = new File(location);
+          loadFiles(new File[] {file}, all);
+        }
+      }
+
+      return all;
+    } catch (IOException e) {
+      throw new ContainerConfigException(e);
+    }
+  }
+
+  /**
+   * Loads containers from directories recursively.
+   *
+   * Only files with a .js or .json extension will be loaded.
+   *
+   * @param files The files to examine.
+   * @throws ContainerConfigException when IO exceptions occur
+   */
+  private static void loadFiles(File[] files, List<Map<String, Object>> all)
+      throws ContainerConfigException {
+    for (File file : files) {
+      try {
+        if (file == null) continue;
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "loadFiles", MessageKeys.READING_CONFIG, new Object[] {file.getName()});
+        }
+        if (file.isDirectory()) {
+          loadFiles(file.listFiles(), all);
+        } else if (file.getName().toLowerCase(Locale.ENGLISH).endsWith(".js")
+            || file.getName().toLowerCase(Locale.ENGLISH).endsWith(".json")) {
+          if (!file.exists()) {
+            throw new ContainerConfigException(
+                "The file '" + file.getAbsolutePath() + "' doesn't exist.");
+          }
+          all.add(loadFromString(ResourceLoader.getContent(file)));
+        } else {
+          if (LOG.isLoggable(Level.FINEST))
+            LOG.finest(file.getAbsolutePath() + " doesn't seem to be a JS or JSON file.");
+        }
+      } catch (IOException e) {
+        throw new ContainerConfigException(
+            "The file '" + file.getAbsolutePath() + "' has errors", e);
+      }
+    }
+  }
+
+  /**
+   * Loads resources recursively.
+   *
+   * @param files The base paths to look for container.xml
+   * @throws ContainerConfigException when IO errors occur
+   */
+  private static void loadResources(Iterable<String> files, List<Map<String, Object>> all)
+      throws ContainerConfigException {
+    try {
+      for (String entry : files) {
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "loadResources", MessageKeys.READING_CONFIG, new Object[] {entry});
+        }
+        String content = ResourceLoader.getContent(entry);
+        if (content == null || content.length() == 0)
+          throw new IOException("The file " + entry + "is empty");
+        all.add(loadFromString(content));
+      }
+    } catch (IOException e) {
+      throw new ContainerConfigException(e);
+    }
+  }
+
+  /**
+   * Processes a container file.
+   *
+   * @param json json to parse and load
+   * @throws ContainerConfigException when invalid json is encountered
+   */
+  private static Map<String, Object> loadFromString(String json) throws ContainerConfigException {
+    try {
+      return jsonToMap(new JSONObject(json));
+    } catch (JSONException e) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.logp(Level.WARNING, classname, "loadFromString", MessageKeys.READING_CONFIG, new Object[] {json});
+      }
+      throw new ContainerConfigException("Trouble parsing " + json, e);
+    }
+  }
+
+  /**
+   * Convert a JSON value to a configuration value.
+   */
+  private static Object jsonToConfig(Object json) {
+    if (JSONObject.NULL.equals(json)) {
+      return null;
+    } else if (json instanceof CharSequence) {
+      return json.toString();
+    } else if (json instanceof JSONArray) {
+      JSONArray jsonArray = (JSONArray) json;
+      ImmutableList.Builder<Object> values = ImmutableList.builder();
+      for (int i = 0, j = jsonArray.length(); i < j; ++i) {
+        values.add(jsonToConfig(jsonArray.opt(i)));
+      }
+      return values.build();
+    } else if (json instanceof JSONObject) {
+      return jsonToMap((JSONObject) json);
+    }
+
+    // A (boxed) primitive.
+    return json;
+  }
+
+  private static Map<String, Object> jsonToMap(JSONObject json) {
+    String[] keys = JSONObject.getNames(json);
+    if (keys == null) {
+      return ImmutableMap.of();
+    }
+    Map<String, Object> values = new HashMap<String, Object>(json.length(), 1);
+    for (String key : keys) {
+      Object val = jsonToConfig(json.opt(key));
+      //If this is a string see if its a pointer to an external resource, and if so, load the resource
+      if (val instanceof String) {
+        String stringVal = (String) val;
+        if (stringVal.startsWith(ResourceLoader.RESOURCE_PREFIX) ||
+            stringVal.startsWith(ResourceLoader.FILE_PREFIX)) {
+          try {
+            val = IOUtils.toString(ResourceLoader.open(stringVal), "UTF-8");
+          } catch (IOException e) {
+            if (LOG.isLoggable(Level.WARNING)) {
+              LOG.logp(Level.WARNING, classname, "jsonToMap", MessageKeys.READING_CONFIG, e);
+            }
+          }
+        }
+      }
+      values.put(key, val);
+    }
+    return Collections.unmodifiableMap(values);
+  }
+
+  private static void addHostAndPortToDefaultContainer(
+      List<Map<String, Object>> config, String host, String port,String contextRoot) {
+    for (int i = 0, j = config.size(); i < j; ++i) {
+      Map<String, Object> container = config.get(i);
+      @SuppressWarnings("unchecked")
+      List<String> names = (List<String>) container.get(ContainerConfig.CONTAINER_KEY);
+      if (names != null && names.contains(ContainerConfig.DEFAULT_CONTAINER)) {
+        Map<String, Object> newContainer = Maps.newHashMap();
+        newContainer.putAll(container);
+        newContainer.put(SERVER_PORT, port);
+        newContainer.put(SERVER_HOST, host);
+        newContainer.put(CONTEXT_ROOT, contextRoot);
+        config.set(i, Collections.unmodifiableMap(newContainer));
+      }
+    }
+  }
+
+  private static void addContainersToTransaction(
+      Transaction transaction, List<Map<String, Object>> config) {
+    for (Map<String, Object> container : config) {
+      transaction.addContainer(container);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/ELTypeConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/ELTypeConverter.java
new file mode 100644
index 0000000..ac15c85
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/ELTypeConverter.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+/**
+ * Expression Language type conversion interface.
+ *
+ * @since 2.0.0
+ */
+import javax.el.ELException;
+
+import org.apache.shindig.expressions.juel.JuelTypeConverter;
+
+import com.google.inject.ImplementedBy;
+
+@ImplementedBy(JuelTypeConverter.class)
+public interface ELTypeConverter {
+
+  /**
+   * for some EL without custom type conversion (Jasper), we want to delay
+   * conversion until after expression has been evaluated (with minimal amount of coercion).
+   */
+  public boolean isPostConvertible(Class<?> type);
+
+  /**
+   *
+   */
+  public <T> T convert(Object obj, Class<T> type) throws ELException;
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/ExpressionProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/ExpressionProvider.java
new file mode 100644
index 0000000..c4f4ad8
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/ExpressionProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import javax.el.ExpressionFactory;
+
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.expressions.juel.JuelProvider;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Provider for an expression handler Juel or Jasper
+ * @since 2.0.0
+ */
+@ImplementedBy(JuelProvider.class)
+public interface ExpressionProvider {
+
+  /**
+   *
+   * @param cacheProvider - the cache provider - not used by all EL implementations.
+   * @param converter - type conversion class -
+   * @return
+   */
+  ExpressionFactory newExpressionFactory(CacheProvider cacheProvider,
+      ELTypeConverter converter);
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/Expressions.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/Expressions.java
new file mode 100644
index 0000000..a931b8b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/Expressions.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.expressions.juel.JuelProvider;
+import org.apache.shindig.expressions.juel.JuelTypeConverter;
+
+import java.util.Map;
+
+import javax.el.ArrayELResolver;
+import javax.el.CompositeELResolver;
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.ExpressionFactory;
+import javax.el.FunctionMapper;
+import javax.el.ListELResolver;
+import javax.el.MapELResolver;
+import javax.el.PropertyNotFoundException;
+import javax.el.PropertyNotWritableException;
+import javax.el.ValueExpression;
+import javax.el.VariableMapper;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+
+/**
+ * A facade to the expressions functionality.
+ */
+@Singleton
+public class Expressions {
+
+  private final ExpressionFactory factory;
+  private final ELContext parseContext;
+  private final ELResolver defaultELResolver;
+  private final Functions functions;
+  private final ELTypeConverter typeConverter;
+
+  /**
+   * Returns an instance of Expressions that doesn't require
+   * any functions or perform any caching.  Use only for testing.
+   */
+  @VisibleForTesting
+  public static Expressions forTesting(Functions functions) {
+    return new Expressions(functions, null, new JuelTypeConverter(), new JuelProvider());
+  }
+
+  /**
+   * Returns an instance of Expressions that doesn't require
+   * any functions or perform any caching.  Use only for testing.
+   */
+  @VisibleForTesting
+  public static Expressions forTesting() {
+    return new Expressions(null, null, new JuelTypeConverter(), new JuelProvider());
+  }
+
+  @Inject
+  public Expressions(Functions functions, CacheProvider cacheProvider,
+      ELTypeConverter typeConverter, ExpressionProvider expProvider) {
+    this.functions = functions;
+    this.typeConverter = typeConverter;
+    factory = newExpressionFactory(expProvider, cacheProvider);
+    // Stub context with no FunctionMapper, used only to parse expressions
+    parseContext = new Context(null);
+    defaultELResolver = createDefaultELResolver();
+
+
+  }
+
+  /**
+   * Creates an ELContext.
+   * @param customResolvers resolvers to be added to the chain
+   */
+  public ELContext newELContext(ELResolver... customResolvers) {
+    CompositeELResolver composite = new CompositeELResolver();
+    for (ELResolver customResolver : customResolvers) {
+      composite.add(customResolver);
+    }
+
+    composite.add(defaultELResolver);
+    return new Context(composite);
+  }
+
+  /**
+   * Parse a value expression.
+   * @param expression the string expression.  This may be a literal
+   *     without any expressions.
+   * @param type the desired coercion type.
+   * @return a ValueExpression corresponding to the expression
+   */
+  public ValueExpression parse(String expression, Class<?> type) {
+    boolean shouldConvert = typeConverter.isPostConvertible(type);
+    if (shouldConvert) {
+      return new ValueExpressionWrapper(factory.createValueExpression(
+          parseContext, expression, Object.class), typeConverter, type);
+    }
+    else {
+      return factory.createValueExpression(parseContext, expression, type);
+    }
+  }
+
+  public ValueExpression constant(Object value, Class<?> type) {
+    boolean shouldConvert = typeConverter.isPostConvertible(type);
+    if (shouldConvert) {
+      return new ValueExpressionWrapper(factory.createValueExpression(value, Object.class), typeConverter, type);
+    }
+    else {
+      return factory.createValueExpression(value, type);
+    }
+
+  }
+
+
+  private ExpressionFactory newExpressionFactory(
+      ExpressionProvider expProvider, CacheProvider cacheProvider) {
+    return expProvider.newExpressionFactory(cacheProvider, typeConverter);
+  }
+
+  /**
+   * @return a default ELResolver with functionality needed by all
+   * expression evaluation.
+   */
+  private ELResolver createDefaultELResolver() {
+    CompositeELResolver resolver = new CompositeELResolver();
+    // Resolvers, in the order they will be most commonly accessed.
+    // Moving JsonELResolver to the end makes JSON property resolution twice
+    // as slow, so this is quite important.
+    resolver.add(new JsonELResolver());
+    resolver.add(new MapELResolver());
+    resolver.add(new ListELResolver());
+    resolver.add(new ArrayELResolver());
+    // TODO: bean el resolver?
+
+    return resolver;
+  }
+
+  /**
+   * ELContext implementation, like SimpleContext but using an injected
+   * FunctionMapper.
+   */
+  private class Context extends ELContext {
+    private final ELResolver resolver;
+    private VariableMapper variables;
+
+    public Context(ELResolver resolver) {
+      this.resolver = resolver;
+    }
+
+    @Override
+    public ELResolver getELResolver() {
+      return resolver;
+    }
+
+    @Override
+    public FunctionMapper getFunctionMapper() {
+      return functions;
+    }
+
+    @Override
+    public VariableMapper getVariableMapper() {
+      if (variables == null) {
+        variables = new Variables();
+      }
+
+      return variables;
+    }
+
+  }
+
+  static private class Variables extends VariableMapper {
+    private final Map<String, ValueExpression> variables = Maps.newHashMap();
+    @Override
+    public ValueExpression resolveVariable(String var) {
+      return variables.get(var);
+    }
+
+    @Override
+    public ValueExpression setVariable(String var, ValueExpression expression) {
+      return variables.put(var, expression);
+    }
+
+  }
+
+  private static class ValueExpressionWrapper extends ValueExpression {
+
+    private static final long serialVersionUID = 2135607228206570229L;
+    private ValueExpression expression = null;
+    private Class<?> type = null;
+    private ELTypeConverter converter = null;
+
+    public ValueExpressionWrapper(ValueExpression ve,
+        ELTypeConverter converter, Class<?> type) {
+      expression = ve;
+      this.type = type;
+      this.converter = converter;
+    }
+
+    @Override
+    public Object getValue(ELContext context) throws NullPointerException,
+        PropertyNotFoundException, ELException {
+        return converter.convert(expression.getValue(context), type);
+    }
+
+    @Override
+    public Class<?> getExpectedType() {
+      return expression.getExpectedType();
+    }
+
+    @Override
+    public Class<?> getType(ELContext context) throws NullPointerException,
+        PropertyNotFoundException, ELException {
+      return expression.getType(context);
+    }
+
+    @Override
+    public boolean isReadOnly(ELContext context) throws NullPointerException,
+        PropertyNotFoundException, ELException {
+      return expression.isReadOnly(context);
+    }
+
+    @Override
+    public void setValue(ELContext context, Object value)
+        throws NullPointerException, PropertyNotFoundException,
+        PropertyNotWritableException, ELException {
+      expression.setValue(context, value);
+
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      return expression.equals(obj);
+    }
+
+    @Override
+    public String getExpressionString() {
+      return expression.getExpressionString();
+    }
+
+    @Override
+    public int hashCode() {
+      return expression.hashCode();
+    }
+
+    @Override
+    public boolean isLiteralText() {
+      return expression.isLiteralText();
+    }
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/Functions.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/Functions.java
new file mode 100644
index 0000000..9da8577
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/Functions.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Map;
+
+import javax.el.FunctionMapper;
+
+import com.google.common.collect.Maps;
+import com.google.inject.ImplementedBy;
+import com.google.inject.Inject;
+
+/**
+ * An implementation of FunctionMapper that uses annotated static methods
+ * on classes to implement EL functions.
+ * <p>
+ * Each class passed to the constructor will have EL functions added
+ * for any static method annotated with the @Expose annotation.
+ * Each method can be exposed in one namespace prefix, with any number
+ * of method names.
+ * <p>
+ * The default Guice instance of the Functions class has the
+ * {@link OpensocialFunctions} methods registered.
+ */
+@ImplementedBy(Functions.DefaultFunctions.class)
+public class Functions extends FunctionMapper {
+  private final Map<String, Map<String, Method>> functions = Maps.newHashMap();
+
+  /** Annotation for static methods to be exposed as functions. */
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target(ElementType.METHOD)
+  public @interface Expose {
+    /**
+     * The prefix to bind functions to.
+     */
+    String prefix();
+
+    /**
+     * The prefix to bind functions to.
+     */
+    String[] names() default {};
+  }
+
+  /**
+   * Creates a Functions class with the specified
+   */
+  public Functions(Class<?>... functionClasses) {
+    for (Class<?> functionClass : functionClasses) {
+      for (Method m : functionClass.getMethods()) {
+        if ((m.getModifiers() & Modifier.STATIC) == 0) {
+          continue;
+        }
+
+        addMethod(m);
+      }
+    }
+  }
+
+  @Override
+  public Method resolveFunction(String prefix, String methodName) {
+    Map<String, Method> prefixMap = functions.get(prefix);
+    if (prefixMap == null) {
+      return null;
+    }
+
+    return prefixMap.get(methodName);
+  }
+
+  /** Register functions for a single Method */
+  private void addMethod(Method m) {
+    Expose annotation = m.getAnnotation(Expose.class);
+    if (m.isAnnotationPresent(Expose.class)) {
+      String prefix = annotation.prefix();
+      Map<String, Method> prefixMap = functions.get(prefix);
+      if (prefixMap == null) {
+        prefixMap = Maps.newHashMap();
+        functions.put(prefix, prefixMap);
+      }
+
+      for (String methodName : annotation.names()) {
+        Method priorMethod = prefixMap.put(methodName, m);
+        if (priorMethod != null) {
+          throw new IllegalStateException(
+              "Method " + prefix + ':' + methodName + " re-defined.");
+        }
+      }
+    }
+  }
+
+  /**
+   * A default version for Guice;  includes the Opensocial functions.
+   */
+  static class DefaultFunctions extends Functions {
+    @Inject
+    public DefaultFunctions() {
+      super(OpensocialFunctions.class);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/JsonELResolver.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/JsonELResolver.java
new file mode 100644
index 0000000..688ccae
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/JsonELResolver.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+
+/**
+ * ELResolver implementation for JSONArray and JSONObject.
+ */
+class JsonELResolver extends ELResolver {
+
+  @Override
+  public Class<?> getCommonPropertyType(ELContext context, Object base) {
+    if (base instanceof JSONArray) {
+      return Integer.class;
+    }
+
+    if (base instanceof JSONObject) {
+      return String.class;
+    }
+
+    return null;
+  }
+
+  @Override
+  public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+      Object base) {
+    return null;
+  }
+
+  @Override
+  public Class<?> getType(ELContext context, Object base, Object property) {
+    if (isJson(base)) {
+      context.setPropertyResolved(true);
+      Object value = getValue(context, base, property);
+      return value == null ? null : value.getClass();
+    }
+
+    return null;
+  }
+
+  @Override
+  public Object getValue(ELContext context, Object base, Object property) {
+    if (base instanceof JSONObject) {
+      context.setPropertyResolved(true);
+      return ((JSONObject) base).opt(String.valueOf(property));
+    }
+
+    if (base instanceof JSONArray) {
+      context.setPropertyResolved(true);
+      int index = toInt(property);
+      return ((JSONArray) base).opt(index);
+    }
+
+    return null;
+  }
+
+  @Override
+  public boolean isReadOnly(ELContext context, Object base, Object property) {
+    if (isJson(base)) {
+      context.setPropertyResolved(true);
+    }
+
+    return false;
+  }
+
+  @Override
+  public void setValue(ELContext context, Object base, Object property, Object value) {
+    if (base instanceof JSONObject) {
+      context.setPropertyResolved(true);
+      try {
+        ((JSONObject) base).put(String.valueOf(property), value);
+      } catch (JSONException e) {
+        throw new ELException(e);
+      }
+      context.setPropertyResolved(true);
+    }
+
+    if (base instanceof JSONArray) {
+      context.setPropertyResolved(true);
+      int index = toInt(property);
+      try {
+        ((JSONArray) base).put(index, value);
+      } catch (JSONException e) {
+        throw new ELException(e);
+      }
+      context.setPropertyResolved(true);
+    }
+  }
+
+  private int toInt(Object property) {
+    if (property instanceof Number) {
+      return ((Number) property).intValue();
+    }
+
+    try {
+      return Integer.parseInt(String.valueOf(property));
+    } catch (NumberFormatException nfe) {
+      throw new ELException(nfe);
+    }
+  }
+
+  private boolean isJson(Object base) {
+    return (base instanceof JSONObject || base instanceof JSONArray);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/OpensocialFunctions.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/OpensocialFunctions.java
new file mode 100644
index 0000000..9fc8803
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/OpensocialFunctions.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.el.ELException;
+
+/**
+ * Default functions in the OpenSocial-Templating spec are prefixed with "os:"
+ * All other functions are prefixed with "osx:"
+ */
+public final class OpensocialFunctions {
+  private OpensocialFunctions() {
+  }
+
+  /**
+   * Convert a string to a JSON Object or JSON Array.
+   */
+  @Functions.Expose(prefix = "osx", names = {"parseJson"})
+  public static Object parseJson(String text) {
+    if ((text == null) || "".equals(text)) {
+      return null;
+    }
+
+    try {
+      if (text.startsWith("[")) {
+        return new JSONArray(text);
+      } else {
+        return new JSONObject(text);
+      }
+    } catch (JSONException je) {
+      throw new ELException(je);
+    }
+  }
+
+  /**
+   * Decode a base-64 encoded string.
+   */
+  @Functions.Expose(prefix = "osx", names = {"decodeBase64"})
+  public static String decodeBase64(String text) {
+    if (text == null) {
+      return null;
+    }
+
+    // TODO: allow a charset to be passed in?
+    return CharsetUtil.newUtf8String(Base64.decodeBase64(CharsetUtil.getUtf8Bytes(text)));
+  }
+
+  /**
+   * Form encode a string
+   */
+  @Functions.Expose(prefix = "os", names = {"urlEncode"})
+  public static String formEncode(String text) {
+    if (text == null) {
+      return null;
+    }
+
+    return Utf8UrlCoder.encode(text);
+  }
+
+  /**
+   * Form decode a string
+   * @param text
+   * @return
+   */
+  @Functions.Expose(prefix = "os", names = {"urlDecode"})
+  public static String formDecode(String text) {
+    if (text == null) {
+      return null;
+    }
+
+    return Utf8UrlCoder.decode(text);
+  }
+
+  /**
+   * Escape HTML entities in a string
+   */
+  @Functions.Expose(prefix = "os", names = {"htmlEncode"})
+  public static String htmlEncode(String text) {
+    if (text == null) {
+      return null;
+    }
+
+    return StringEscapeUtils.escapeHtml4(text);
+  }
+
+  /**
+   * Unescape HTML entities in a string
+   * @param text
+   * @return
+   */
+  @Functions.Expose(prefix = "os", names = {"htmlDecode"})
+  public static String htmlDecode(String text) {
+    if (text == null) {
+      return null;
+    }
+
+    return StringEscapeUtils.unescapeHtml4(text);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java
new file mode 100644
index 0000000..05df8f5
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/RootELResolver.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.el.ELContext;
+import javax.el.ELResolver;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * ELResolver implementation that adds a map of top-level variables.
+ * New variables can be inserted after creation with:
+ * {@code context.getELResolver().setValue(context, null, name, value);}
+ *
+ * TODO: should this be read-only?
+ *
+ * @see Expressions#newELContext(ELResolver...)
+ */
+public class RootELResolver extends ELResolver {
+  private final Map<String, ? extends Object> map;
+
+  public RootELResolver() {
+    this(ImmutableMap.<String, Object>of());
+  }
+
+  public RootELResolver(Map<String, ? extends Object> base) {
+    this.map = base;
+  }
+
+  @Override
+  public Class<?> getCommonPropertyType(ELContext context, Object base) {
+    if (base == null) {
+      return String.class;
+    }
+
+    return null;
+  }
+
+  @Override
+  public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+      Object base) {
+    return null;
+  }
+
+  @Override
+  public Class<?> getType(ELContext context, Object base, Object property) {
+    if (base == null && map.containsKey(property)) {
+      context.setPropertyResolved(true);
+      Object value = map.get(property);
+      return value == null ? null : value.getClass();
+    }
+
+    return null;
+  }
+
+  @Override
+  public Object getValue(ELContext context, Object base, Object property) {
+    if (base == null && map.containsKey(property)) {
+      context.setPropertyResolved(true);
+      return map.get(property);
+    }
+
+    return null;
+  }
+
+  @Override
+  public boolean isReadOnly(ELContext context, Object base, Object property) {
+    if (base == null && map.containsKey(property)) {
+      context.setPropertyResolved(true);
+      return true;
+    }
+
+    return false;
+  }
+
+  @Override
+  public void setValue(ELContext context, Object base, Object property, Object value) {
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/ShindigTypeConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/ShindigTypeConverter.java
new file mode 100644
index 0000000..fb53ffc
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/ShindigTypeConverter.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+
+import javax.el.ELException;
+
+import com.google.common.collect.ImmutableList;
+
+
+/**
+ * Custom type converter class that overrides the default EL coercion rules
+ * where necessary.  Specifically, Booleans are handled differently,
+ * and JSONArray is supported.
+ */
+public class ShindigTypeConverter implements ELTypeConverter {
+
+
+  public  boolean isPostConvertible(Class<?> type) {
+    return false;
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T> T convert(Object obj, Class<T> type) throws ELException {
+    // Handle boolean specially
+    if (type == Boolean.class || type == Boolean.TYPE) {
+      return (T) coerceToBoolean(obj);
+    }
+
+    if (type == JSONArray.class) {
+      return (T) coerceToJsonArray(obj);
+    }
+
+    if (type == Iterable.class) {
+      return (T) coerceToIterable(obj);
+    }
+
+    //  Nothing more we can do.
+    return null;
+  }
+
+  /**
+   * Coerce objects to iterables.  Iterables and JSONArrays have the obvious
+   * coercion.  JSONObjects are coerced to single-element lists, unless
+   * they have a "list" property that is in array, in which case that's used.
+   */
+  private Iterable<?> coerceToIterable(Object obj) {
+    if (obj == null) {
+      return ImmutableList.of();
+    }
+
+    if (obj instanceof Iterable<?>) {
+      return ((Iterable<?>) obj);
+    }
+
+    if (obj instanceof JSONArray) {
+      final JSONArray array = (JSONArray) obj;
+      // TODO: Extract JSONArrayIterator class?
+      return new Iterable<Object>() {
+        public Iterator<Object> iterator() {
+          return new Iterator<Object>() {
+            private int i = 0;
+
+            public boolean hasNext() {
+              return i < array.length();
+            }
+
+            public Object next() {
+              if (i >= array.length()) {
+                throw new NoSuchElementException();
+              }
+
+              try {
+                return array.get(i++);
+              } catch (Exception e) {
+                throw new ELException(e);
+              }
+            }
+
+            public void remove() {
+              throw new UnsupportedOperationException();
+            }
+          };
+        }
+      };
+    }
+
+    if (obj instanceof JSONObject) {
+      JSONObject json = (JSONObject) obj;
+
+      // Does this object have a "list" property that is an array?
+      // TODO: add to specification
+      Object childList = json.opt("list");
+      if (childList instanceof JSONArray) {
+        return coerceToIterable(childList);
+      }
+
+      // A scalar JSON value is treated as a single element list.
+      return ImmutableList.of(json);
+    }
+
+    return ImmutableList.of(obj);
+  }
+
+  private JSONArray coerceToJsonArray(Object obj) {
+    if (obj == null) {
+      return null;
+    }
+
+    if (obj instanceof JSONArray) {
+      return (JSONArray) obj;
+    }
+
+    if (obj instanceof String) {
+      JSONArray array = new JSONArray();
+      StringTokenizer tokenizer = new StringTokenizer(obj.toString(), ",");
+      while (tokenizer.hasMoreTokens()) {
+        array.put(tokenizer.nextToken());
+      }
+
+      return array;
+    }
+
+    throw new ELException("Could not coerce " + obj.getClass().getName() + " to JSONArray");
+  }
+
+  /**
+   * Coerce the following booleans:
+   *
+   * null -> false
+   * empty string, and "false" -> false
+   * boolean false -> false
+   * number 0 -> false
+   *
+   * All else is true.
+   */
+  private Boolean coerceToBoolean(Object obj) {
+    if (obj == null) {
+      return false;
+    }
+
+    if (obj instanceof String) {
+      return !("".equals(obj) || "false".equals(obj));
+    }
+
+    if (obj instanceof Boolean) {
+      return (Boolean) obj;
+    }
+
+    if (obj instanceof Number) {
+      return 0 != ((Number) obj).intValue();
+    }
+
+    return true;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperConversionModule.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperConversionModule.java
new file mode 100644
index 0000000..7ac170f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperConversionModule.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.jasper;
+
+
+import org.apache.shindig.expressions.ELTypeConverter;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+
+/**
+ * Creates a module to supply a Jasper Type Converter
+ *
+ * @since 2.0.0
+ */
+public class JasperConversionModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(ELTypeConverter.class).to(JasperTypeConverter.class).in(Scopes.SINGLETON);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperModule.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperModule.java
new file mode 100644
index 0000000..518890d
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperModule.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.jasper;
+
+import org.apache.shindig.expressions.ExpressionProvider;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+
+/**
+ * Creates a module to supply a Jasper Provider
+ *
+ * @since 2.0.0
+ */
+public class JasperModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(ExpressionProvider.class).to(JasperProvider.class)
+        .in(Scopes.SINGLETON);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperProvider.java
new file mode 100644
index 0000000..7bc2619
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.jasper;
+
+import javax.el.ExpressionFactory;
+import org.apache.el.ExpressionFactoryImpl;
+
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.expressions.ELTypeConverter;
+import org.apache.shindig.expressions.ExpressionProvider;
+
+/**
+ * A Provider for Jasper Expression processing
+ * @since 2.0.0
+ */
+public class JasperProvider implements ExpressionProvider {
+
+  public ExpressionFactory newExpressionFactory(CacheProvider cacheProvider,
+      ELTypeConverter converter) {
+    return new ExpressionFactoryImpl();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperTypeConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperTypeConverter.java
new file mode 100644
index 0000000..f6889ea
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/jasper/JasperTypeConverter.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.jasper;
+
+import org.apache.shindig.expressions.ShindigTypeConverter;
+import org.json.JSONArray;
+
+/**
+ * Jasper Implementation of a ShindigTypeConverter
+ * @since 2.0.0
+ */
+public class JasperTypeConverter extends ShindigTypeConverter {
+
+  @Override
+  public boolean isPostConvertible(Class<?> type) {
+    return type == Boolean.class || type == Boolean.TYPE
+        || type == JSONArray.class || type == Iterable.class;
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelModule.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelModule.java
new file mode 100644
index 0000000..ab038f7
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelModule.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.juel;
+
+import org.apache.shindig.expressions.ExpressionProvider;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Scopes;
+
+/**
+ * Creates a module to supply a Juel Provider
+ *
+ * @since 2.0.0
+ */
+public class JuelModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(ExpressionProvider.class).to(JuelProvider.class).in(Scopes.SINGLETON);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelProvider.java
new file mode 100644
index 0000000..9f6c461
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelProvider.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.juel;
+
+import javax.el.ExpressionFactory;
+
+import de.odysseus.el.ExpressionFactoryImpl;
+import de.odysseus.el.misc.TypeConverter;
+import de.odysseus.el.tree.Tree;
+import de.odysseus.el.tree.TreeCache;
+import de.odysseus.el.tree.TreeStore;
+import de.odysseus.el.tree.impl.Builder;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.NullCache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.expressions.ELTypeConverter;
+import org.apache.shindig.expressions.ExpressionProvider;
+
+/**
+ * A provider for a Juel based Expression Implementation
+ * @since 2.0.0
+ */
+public class JuelProvider implements ExpressionProvider {
+
+  private static final String EXPRESSION_CACHE = "expressions";
+
+  /**
+   * Any provided JUEL converter must implement both JUEL TypeConverter impl and ELTypeConverter
+   */
+  public ExpressionFactory newExpressionFactory(CacheProvider cacheProvider,
+      ELTypeConverter converter) {
+    TreeStore store = new TreeStore(new Builder(),
+        createTreeCache(cacheProvider));
+    return new ExpressionFactoryImpl(store, (TypeConverter) converter);
+  }
+
+  /**
+   * Create a JUEL cache of expressions.
+   */
+  private TreeCache createTreeCache(CacheProvider cacheProvider) {
+    Cache<String, Tree> treeCache;
+    if (cacheProvider == null) {
+      treeCache = new NullCache<String, Tree>();
+    } else {
+      treeCache = cacheProvider.createCache(EXPRESSION_CACHE);
+    }
+
+    final Cache<String, Tree> resolvedTreeCache = treeCache;
+    return new TreeCache() {
+      public Tree get(String expression) {
+        return resolvedTreeCache.getElement(expression);
+      }
+
+      public void put(String expression, Tree tree) {
+        resolvedTreeCache.addElement(expression, tree);
+      }
+    };
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelTypeConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelTypeConverter.java
new file mode 100644
index 0000000..4691ef4
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/expressions/juel/JuelTypeConverter.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.juel;
+
+import javax.el.ELException;
+
+import org.apache.shindig.expressions.ShindigTypeConverter;
+
+import de.odysseus.el.misc.TypeConverter;
+
+/**
+ * A converter used by Juel
+ * @since 2.0.0
+ */
+public class JuelTypeConverter extends ShindigTypeConverter implements
+    TypeConverter {
+
+  private static final long serialVersionUID = -4382092735987940726L;
+
+  @Override
+  public <T> T convert(Object obj, Class<T> type) throws ELException {
+    T retValue = super.convert(obj, type);
+    if (retValue == null) {
+      retValue = TypeConverter.DEFAULT.convert(obj, type);
+    }
+
+    return retValue;
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/ApiServlet.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ApiServlet.java
new file mode 100644
index 0000000..91960d6
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ApiServlet.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+/**
+ * Common base class for API servlets.
+ */
+public abstract class ApiServlet extends InjectedServlet {
+  private static final String classname = ApiServlet.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  protected static final String FORMAT_PARAM = "format";
+  protected static final String JSON_FORMAT = "json";
+  protected static final String ATOM_FORMAT = "atom";
+  protected static final String XML_FORMAT = "xml";
+
+  protected static final String DEFAULT_ENCODING = "UTF-8";
+
+  /** ServletConfig parameter set to provide an explicit named binding for handlers */
+  public static final String HANDLERS_PARAM = "handlers";
+
+  /** The default key used to look up handlers if the servlet config parameter is not available */
+  public static final Key<Set<Object>> DEFAULT_HANDLER_KEY =
+       Key.get(new TypeLiteral<Set<Object>>(){}, Names.named("org.apache.shindig.handlers"));
+
+  protected HandlerRegistry dispatcher;
+  protected BeanConverter jsonConverter;
+  protected BeanConverter xmlConverter;
+  protected BeanConverter atomConverter;
+  protected ContainerConfig containerConfig;
+  protected Boolean isJSONPAllowed;
+
+  @Override
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
+
+    // Lookup the set of handlers to bind to this api endpoint and
+    // populate the registry with them
+    String handlers = config.getInitParameter(HANDLERS_PARAM);
+    Key<Set<Object>> handlerKey;
+    if (handlers == null || "".equals(handlers)) {
+      handlerKey = DEFAULT_HANDLER_KEY;
+    } else {
+      handlerKey = Key.get(new TypeLiteral<Set<Object>>(){}, Names.named(handlers));
+    }
+    this.dispatcher.addHandlers(injector.getInstance(handlerKey));
+    this.dispatcher.addHandlers(Collections.<Object>singleton(new SystemHandler(dispatcher)));
+  }
+
+  @Inject
+  public void setHandlerRegistry(HandlerRegistry dispatcher) {
+    this.dispatcher = dispatcher;
+  }
+
+  @Inject
+  public void setContainerConfig(ContainerConfig containerConfig) {
+    this.containerConfig = containerConfig;
+  }
+
+  @Inject
+  public void setJSONPAllowed(
+      @Named("shindig.allowJSONP") Boolean isJSONPAllowed) {
+    this.isJSONPAllowed = isJSONPAllowed;
+  }
+
+  @Inject
+  public void setBeanConverters(
+      @Named("shindig.bean.converter.json") BeanConverter jsonConverter,
+      @Named("shindig.bean.converter.xml") BeanConverter xmlConverter,
+      @Named("shindig.bean.converter.atom") BeanConverter atomConverter) {
+    this.jsonConverter = jsonConverter;
+    this.xmlConverter = xmlConverter;
+    this.atomConverter = atomConverter;
+  }
+
+  protected SecurityToken getSecurityToken(HttpServletRequest servletRequest) {
+    return AuthInfoUtil.getSecurityTokenFromRequest(servletRequest);
+  }
+
+  protected abstract void sendError(HttpServletResponse servletResponse, ResponseItem responseItem)
+      throws IOException;
+
+  protected void sendSecurityError(HttpServletResponse servletResponse) throws IOException {
+    sendError(servletResponse, new ResponseItem(HttpServletResponse.SC_UNAUTHORIZED,
+        "The request did not have a proper security token nor oauth message and unauthenticated "
+            + "requests are not allowed"));
+  }
+
+  protected ResponseItem getResponseItem(Future<?> future) {
+    try {
+      // TODO: use timeout methods?
+      Object result = future != null ? future.get() : null;
+      // TODO: null is now a supported return value for post/delete, but
+      // is bad for get().
+      return new ResponseItem(result != null ? result : Collections.emptyMap());
+    } catch (InterruptedException ie) {
+      return responseItemFromException(ie);
+    } catch (ExecutionException ee) {
+      return responseItemFromException(ee.getCause());
+    }
+  }
+
+  protected ResponseItem responseItemFromException(Throwable t) {
+    if (t instanceof ProtocolException) {
+      ProtocolException pe = (ProtocolException) t;
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, classname, "responseItemFromException", MessageKeys.API_SERVLET_PROTOCOL_EXCEPTION,pe);
+      }
+      return new ResponseItem(pe.getCode(), pe.getMessage(), pe.getResponse());
+    }
+    if (LOG.isLoggable(Level.INFO)) {
+      LOG.logp(Level.INFO, classname, "responseItemFromException", MessageKeys.API_SERVLET_EXCEPTION,t);
+    }
+    return new ResponseItem(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, t.getMessage());
+  }
+
+  protected void setCharacterEncodings(HttpServletRequest servletRequest,
+      HttpServletResponse servletResponse) throws IOException {
+    if (servletRequest.getCharacterEncoding() == null) {
+      servletRequest.setCharacterEncoding(DEFAULT_ENCODING);
+    }
+    servletResponse.setCharacterEncoding(DEFAULT_ENCODING);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/BaseRequestItem.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/BaseRequestItem.java
new file mode 100644
index 0000000..5557005
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/BaseRequestItem.java
@@ -0,0 +1,333 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.model.FilterOperation;
+import org.apache.shindig.protocol.model.SortOrder;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.base.Preconditions;
+import org.joda.time.DateTime;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Default implementation of RequestItem
+ */
+public class BaseRequestItem implements RequestItem {
+
+  protected final SecurityToken token;
+  final BeanConverter converter;
+  final Map<String,Object> parameters;
+  final Map<String, FormDataItem> formItems;
+  final BeanJsonConverter jsonConverter;
+  Map<String,Object> attributes;
+
+  public BaseRequestItem(Map<String, String[]> parameters,
+      SecurityToken token,
+      BeanConverter converter,
+      BeanJsonConverter jsonConverter) {
+    this.token = token;
+    this.converter = converter;
+    this.parameters = Maps.newHashMap();
+
+    for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
+      if  (entry.getValue() == null) {
+        setParameter(entry.getKey(), null);
+      } else if (entry.getValue().length == 1) {
+        setParameter(entry.getKey(), entry.getValue()[0]);
+      } else {
+        setParameter(entry.getKey(), Lists.newArrayList(entry.getValue()));
+      }
+    }
+    this.jsonConverter = jsonConverter;
+    this.formItems = null;
+  }
+
+  public BaseRequestItem(JSONObject parameters,
+      Map<String, FormDataItem> formItems,
+      SecurityToken token,
+      BeanConverter converter,
+      BeanJsonConverter jsonConverter) {
+    try {
+      this.parameters = Maps.newHashMap();
+      @SuppressWarnings("unchecked")
+      // JSONObject keys are always strings
+      Iterator<String> keys = parameters.keys();
+      while (keys.hasNext()) {
+        String key = keys.next();
+        this.parameters.put(key, parameters.get(key));
+      }
+      this.token = token;
+      this.converter = converter;
+      this.formItems = formItems;
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+    this.jsonConverter = jsonConverter;
+  }
+
+  public String getAppId() {
+    String appId = getParameter(APP_ID);
+    if (appId != null && appId.equals(APP_SUBSTITUTION_TOKEN)) {
+      return token.getAppId();
+    } else {
+      return appId;
+    }
+  }
+
+  public Date getUpdatedSince() {
+    String updatedSince = getParameter("updatedSince");
+    if (updatedSince == null)
+      return null;
+
+    DateTime date = new DateTime(updatedSince);
+
+    return date.toDate();
+  }
+
+  public String getSortBy() {
+    return getParameter(SORT_BY);
+  }
+
+  public SortOrder getSortOrder() {
+    String sortOrder = getParameter(SORT_ORDER);
+    try {
+      return sortOrder == null
+            ? SortOrder.ascending
+            : SortOrder.valueOf(sortOrder);
+    } catch (IllegalArgumentException iae) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+           "Parameter " + SORT_ORDER + " (" + sortOrder + ") is not valid.");
+    }
+  }
+
+  public String getFilterBy() {
+    return getParameter(FILTER_BY);
+  }
+
+  public int getStartIndex() {
+    String startIndex = getParameter(START_INDEX);
+    try {
+      return startIndex == null ? DEFAULT_START_INDEX
+          : Integer.valueOf(startIndex);
+    } catch (NumberFormatException nfe) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+          "Parameter " + START_INDEX + " (" + startIndex + ") is not a number.");
+    }
+  }
+
+  public int getCount() {
+    String count = getParameter(COUNT);
+    try {
+      return count == null ? DEFAULT_COUNT : Integer.valueOf(count);
+    } catch (NumberFormatException nfe) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+           "Parameter " + COUNT + " (" + count + ") is not a number.");
+    }
+  }
+
+  public FilterOperation getFilterOperation() {
+    String filterOp = getParameter(FILTER_OPERATION);
+    try {
+      return filterOp == null
+          ? FilterOperation.contains
+          : FilterOperation.valueOf(filterOp);
+    } catch (IllegalArgumentException iae) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+           "Parameter " + FILTER_OPERATION + " (" + filterOp + ") is not valid.");
+    }
+  }
+
+  public String getFilterValue() {
+    String filterValue = getParameter(FILTER_VALUE);
+    return Objects.firstNonNull(filterValue, "");
+  }
+
+  public Set<String> getFields() {
+    return getFields(Collections.<String>emptySet());
+  }
+
+  public Set<String> getFields(Set<String> defaultValue) {
+    Set<String> result = ImmutableSet.copyOf(getListParameter(FIELDS));
+    if (result.isEmpty()) {
+      return defaultValue;
+    }
+    return result;
+  }
+
+
+  public SecurityToken getToken() {
+    return token;
+  }
+
+  public <T> T getTypedParameter(String parameterName, Class<T> dataTypeClass) {
+    try {
+      String json = getParameter(parameterName);
+      if (json == null) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "missing data for " + parameterName);
+      }
+      return converter.convertToObject(json, dataTypeClass);
+    } catch (RuntimeException e) {
+      if (e.getCause() instanceof JSONException)
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
+      throw e;
+    }
+  }
+
+  public <T> T getTypedRequest(Class<T> dataTypeClass) {
+    try {
+      return jsonConverter.convertToObject(new JSONObject(this.parameters).toString(),
+          dataTypeClass);
+    } catch (RuntimeException e) {
+      if (e.getCause() instanceof JSONException)
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
+      throw e;
+    }
+  }
+
+  public String getParameter(String paramName) {
+    Object param = this.parameters.get(paramName);
+    if (param instanceof List<?>) {
+      if (((List<?>)param).isEmpty()) {
+        return null;
+      } else {
+        param = ((List<?>)param).get(0);
+      }
+    }
+    if (param == null) {
+      return null;
+    }
+    return param.toString();
+  }
+
+  public String getParameter(String paramName, String defaultValue) {
+    String param = getParameter(paramName);
+    if (param == null) {
+      return defaultValue;
+    }
+    return param;
+  }
+
+  public Map<String, Object> getParameters() {
+    return Collections.unmodifiableMap(this.parameters);
+  }
+
+  public List<String> getListParameter(String paramName) {
+    Object param = this.parameters.get(paramName);
+    if (param == null) {
+      return Collections.emptyList();
+    }
+    if (param instanceof String && ((String)param).indexOf(',') != -1) {
+      List<String> listParam = ImmutableList.copyOf(Splitter.on(',').split((String) param));
+      this.parameters.put(paramName, listParam);
+      return listParam;
+    }
+    else if (param instanceof List<?>) {
+      // Assume it's a list of strings.  This is not type-safe.
+      return (List<String>) param;
+    } else if (param instanceof JSONArray) {
+      try {
+        JSONArray jsonArray = (JSONArray)param;
+        List<String> returnVal = Lists.newArrayListWithCapacity(jsonArray.length());
+        for (int i = 0; i < jsonArray.length(); i++) {
+          returnVal.add(jsonArray.getString(i));
+        }
+        return returnVal;
+      } catch (JSONException je) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, je.getMessage(), je);
+      }
+    } else {
+      // Allow up-conversion of non-array to array params.
+      return Lists.newArrayList(param.toString());
+    }
+  }
+
+  @VisibleForTesting
+  public void setParameter(String paramName, Object paramValue) {
+    if (paramValue instanceof String[]) {
+      String[] arr = (String[])paramValue;
+      if (arr.length == 1) {
+        this.parameters.put(paramName, arr[0]);
+      } else {
+        this.parameters.put(paramName, Lists.newArrayList(arr));
+      }
+    } else if (paramValue instanceof String) {
+      String stringValue = (String)paramValue;
+      if (stringValue.length() > 0) {
+        this.parameters.put(paramName, stringValue);
+      }
+    } else {
+      this.parameters.put(paramName, paramValue);
+    }
+  }
+
+  public FormDataItem getFormMimePart(String partName) {
+    if (formItems != null) {
+      return formItems.get(partName);
+    } else {
+      return null;
+    }
+  }
+
+  private Map<String,Object> getAttributeMap() {
+     if (this.attributes == null){
+       this.attributes = Maps.newHashMap();
+     }
+     return attributes;
+  }
+
+  public Object getAttribute(String val) {
+    Preconditions.checkNotNull(val);
+    return getAttributeMap().get(val);
+  }
+
+  public void setAttribute(String val, Object obj) {
+    Preconditions.checkNotNull(val);
+    if (obj == null) {
+      getAttributeMap().remove(val);
+    } else {
+      getAttributeMap().put(val, obj);
+    }
+  }
+
+  public Set<String> getParameterNames() {
+    return this.parameters.keySet();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/ContentTypes.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ContentTypes.java
new file mode 100644
index 0000000..4a49132
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ContentTypes.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.base.Strings;
+import com.google.common.base.Joiner;
+
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
+
+/**
+ * Common mime content types and utilities
+ */
+public final class ContentTypes {
+  private ContentTypes() {}
+
+  /**
+   * Allowed alternatives to application/json, including types listed
+   * in JSON-RPC spec.
+   */
+  public static final Set<String> ALLOWED_JSON_CONTENT_TYPES =
+      ImmutableSet.of("application/json", "text/x-json", "application/javascript",
+          "application/x-javascript", "text/javascript", "text/ecmascript",
+          "application/json-rpc", "application/jsonrequest");
+
+  /**
+   * Allowed alternatives to application/xml
+   */
+  public static final Set<String> ALLOWED_XML_CONTENT_TYPES =
+      ImmutableSet.of("text/xml", "application/xml");
+
+  /**
+   * Allowed alternatives to application/atom+xml
+   */
+  public static final Set<String> ALLOWED_ATOM_CONTENT_TYPES =
+      ImmutableSet.of("application/atom+xml");
+
+  /**
+   * Content types that are forbidden for REST & RPC calls
+   */
+  public static final Set<String> FORBIDDEN_CONTENT_TYPES =
+      ImmutableSet.of(
+          "application/x-www-form-urlencoded" // Not allowed because of OAuth body signing issues
+      );
+
+  public static final String MULTIPART_FORM_CONTENT_TYPE = "multipart/form-data";
+
+  public static final Set<String> ALLOWED_MULTIPART_CONTENT_TYPES =
+      ImmutableSet.of(MULTIPART_FORM_CONTENT_TYPE);
+
+  public static final String OUTPUT_JSON_CONTENT_TYPE = "application/json";
+  public static final String OUTPUT_XML_CONTENT_TYPE = "application/xml";
+  public static final String OUTPUT_ATOM_CONTENT_TYPE = "application/atom+xml";
+
+  /**
+   * Extract the mime part from an Http Content-Type header
+   */
+  public static String extractMimePart(String contentType) {
+    contentType = contentType.trim();
+    int separator = contentType.indexOf(';');
+    if (separator != -1) {
+      contentType = contentType.substring(0, separator);
+    }
+    return contentType.trim().toLowerCase();
+  }
+
+  public static void checkContentTypes(Set<String> allowedContentTypes,
+      String contentType) throws InvalidContentTypeException {
+
+    if (Strings.isNullOrEmpty(contentType)) {
+      throw new InvalidContentTypeException(
+          "No Content-Type specified. One of "
+              + Joiner.on(", ").join(allowedContentTypes) + " is required");
+    }
+
+    contentType = ContentTypes.extractMimePart(contentType);
+
+    if (ContentTypes.FORBIDDEN_CONTENT_TYPES.contains(contentType)) {
+      throw new InvalidContentTypeException(
+          "Cannot use disallowed Content-Type " + contentType);
+    }
+    if (allowedContentTypes.contains(contentType)) {
+      return;
+    }
+    throw new InvalidContentTypeException(
+        "Unsupported Content-Type "
+            + contentType
+            + ". One of "
+            + Joiner.on(", ").join(allowedContentTypes)
+            + " is required");
+  }
+
+  public static class InvalidContentTypeException extends Exception {
+    public InvalidContentTypeException(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataCollection.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataCollection.java
new file mode 100644
index 0000000..c105f75
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataCollection.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import java.util.Map;
+
+/**
+ * Wrapper class used for data responses.
+ */
+public class DataCollection {
+  private Map<String, Map<String, String>> entry;
+
+  public DataCollection(Map<String, Map<String, String>> entry) {
+    this.entry = entry;
+  }
+
+  public Map<String, Map<String, String>> getEntry() {
+    return entry;
+  }
+
+  public void setEntry(Map<String, Map<String, String>> entry) {
+    this.entry = entry;
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataServiceServlet.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataServiceServlet.java
new file mode 100644
index 0000000..d6e605d
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataServiceServlet.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Servlet used to process REST requests (/rest/* etc.)
+ */
+public class DataServiceServlet extends ApiServlet {
+
+  private static final Logger LOG = Logger.getLogger(DataServiceServlet.class.getName());
+
+  public static final Set<String> ALLOWED_CONTENT_TYPES =
+      new ImmutableSet.Builder<String>().addAll(ContentTypes.ALLOWED_JSON_CONTENT_TYPES)
+          .addAll(ContentTypes.ALLOWED_XML_CONTENT_TYPES)
+          .addAll(ContentTypes.ALLOWED_ATOM_CONTENT_TYPES).build();
+
+  protected static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
+
+  @Override
+  protected void doGet(HttpServletRequest servletRequest,
+      HttpServletResponse servletResponse)
+      throws ServletException, IOException {
+    executeRequest(servletRequest, servletResponse);
+  }
+
+  @Override
+  protected void doPut(HttpServletRequest servletRequest,
+      HttpServletResponse servletResponse)
+      throws ServletException, IOException {
+    try {
+      ContentTypes.checkContentTypes(ALLOWED_CONTENT_TYPES, servletRequest.getContentType());
+      executeRequest(servletRequest, servletResponse);
+    } catch (ContentTypes.InvalidContentTypeException icte) {
+      sendError(servletResponse,
+          new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, icte.getMessage()));
+    }
+  }
+
+  @Override
+  protected void doDelete(HttpServletRequest servletRequest,
+      HttpServletResponse servletResponse)
+      throws ServletException, IOException {
+    executeRequest(servletRequest, servletResponse);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest servletRequest,
+      HttpServletResponse servletResponse)
+      throws ServletException, IOException {
+    try {
+      ContentTypes.checkContentTypes(ALLOWED_CONTENT_TYPES, servletRequest.getContentType());
+      executeRequest(servletRequest, servletResponse);
+    } catch (ContentTypes.InvalidContentTypeException icte) {
+      sendError(servletResponse,
+          new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, icte.getMessage()));
+    }
+  }
+
+  /**
+   * Actual dispatch handling for servlet requests
+   */
+  void executeRequest(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
+      throws IOException {
+    if (LOG.isLoggable(Level.FINEST)) {
+      LOG.finest("Handling restful request for " + servletRequest.getPathInfo());
+    }
+
+    setCharacterEncodings(servletRequest, servletResponse);
+
+    SecurityToken token = getSecurityToken(servletRequest);
+    if (token == null) {
+      sendSecurityError(servletResponse);
+      return;
+    }
+
+    HttpUtil.setCORSheader(servletResponse, containerConfig.<String>getList(token.getContainer(),
+        "gadgets.parentOrigins"));
+
+    handleSingleRequest(servletRequest, servletResponse, token);
+  }
+
+  @Override
+  protected void sendError(HttpServletResponse servletResponse, ResponseItem responseItem)
+      throws IOException {
+    String errorMessage = responseItem.getErrorMessage();
+    int errorCode = responseItem.getErrorCode();
+    if (errorCode < 0) {
+      // Map JSON-RPC error codes into HTTP error codes as best we can
+      // TODO: Augment the error message (if missing) with a default
+      switch (errorCode) {
+        case -32700:
+        case -32602:
+        case -32600:
+          // Parse error, invalid params, and invalid request
+          errorCode = HttpServletResponse.SC_BAD_REQUEST;
+          break;
+        case -32601:
+          // Procedure doesn't exist
+          errorCode = HttpServletResponse.SC_NOT_IMPLEMENTED;
+          break;
+        case -32603:
+        default:
+          // Internal server error, or any application-defined error
+          errorCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+          break;
+      }
+    }
+
+    servletResponse.sendError(errorCode, errorMessage);
+  }
+
+  /**
+   * Handler for non-batch requests.
+   */
+  private void handleSingleRequest(HttpServletRequest servletRequest,
+      HttpServletResponse servletResponse, SecurityToken token) throws IOException {
+
+    // Always returns a non-null handler.
+    RestHandler handler = getRestHandler(servletRequest);
+
+    // Get Content-Type and format
+    String format = null;
+    String contentType = null;
+
+    try {
+      format = servletRequest.getParameter(FORMAT_PARAM);
+    } catch (Throwable t) {
+      // this happens while testing
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("Unexpected error : format param is null " + t.toString());
+      }
+    }
+    try {
+      // TODO: First implementation causes bug when Content-Type is application/atom+xml. Fix is applied.
+      contentType = ContentTypes.extractMimePart(servletRequest.getContentType());
+    } catch (Throwable t) {
+      //this happens while testing
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("Unexpected error : content type is null " + t.toString());
+      }
+    }
+
+    // Get BeanConverter for Request payload.
+    BeanConverter requestConverter = getConverterForRequest(contentType, format);
+
+    // Get BeanConverter for Response body.
+    BeanConverter responseConverter = getConverterForFormat(format);
+
+    Reader bodyReader = null;
+    if (!servletRequest.getMethod().equals("GET") && !servletRequest.getMethod().equals("HEAD")) {
+      bodyReader = servletRequest.getReader();
+    }
+
+    // Execute the request
+    @SuppressWarnings("unchecked")
+    Map<String, String[]> parameterMap = servletRequest.getParameterMap();
+    Future<?> future = handler.execute(parameterMap, bodyReader, token, requestConverter);
+    ResponseItem responseItem = getResponseItem(future);
+
+    servletResponse.setContentType(responseConverter.getContentType());
+    if (responseItem.getErrorCode() >= 200 && responseItem.getErrorCode() < 400) {
+      PrintWriter writer = servletResponse.getWriter();
+      Object response = responseItem.getResponse();
+      // TODO: ugliness resulting from not using RestfulItem
+      if (!(response instanceof DataCollection) && !(response instanceof RestfulCollection)) {
+        response = ImmutableMap.of("entry", response);
+      }
+
+      // JSONP style callbacks
+      String callback = (this.isJSONPAllowed && HttpUtil.isJSONP(servletRequest) &&
+          ContentTypes.OUTPUT_JSON_CONTENT_TYPE.equals(responseConverter.getContentType())) ?
+          servletRequest.getParameter("callback") : null;
+
+      if (callback != null) writer.write(callback + '(');
+      writer.write(responseConverter.convertToString(response));
+      if (callback != null) writer.write(");\n");
+    } else {
+      sendError(servletResponse, responseItem);
+    }
+  }
+
+  protected RestHandler getRestHandler(HttpServletRequest servletRequest) {
+    // TODO Rework to allow sub-services
+    String path = servletRequest.getPathInfo();
+
+    // TODO - This shouldnt be on BaseRequestItem
+    String method = servletRequest.getParameter(X_HTTP_METHOD_OVERRIDE);
+    if (method == null) {
+      method = servletRequest.getMethod();
+    }
+
+    // Always returns a non-null handler.
+    return dispatcher.getRestHandler(path, method.toUpperCase());
+  }
+
+  /*
+   * Return the right BeanConverter to convert the request payload.
+   */
+  public BeanConverter getConverterForRequest(@Nullable String contentType, String format) {
+    if (StringUtils.isNotBlank(contentType)) {
+      return getConverterForContentType(contentType);
+    } else {
+      return getConverterForFormat(format);
+    }
+  }
+
+  /**
+   * Return BeanConverter based on content type.
+   * @param contentType the content type for the converter.
+   * @return BeanConverter based on the contentType input param. Will default to JSON
+   */
+  protected BeanConverter getConverterForContentType(String contentType) {
+    return ContentTypes.ALLOWED_ATOM_CONTENT_TYPES.contains(contentType) ? atomConverter :
+        ContentTypes.ALLOWED_XML_CONTENT_TYPES.contains(contentType) ? xmlConverter : jsonConverter;
+  }
+
+  /**
+   * Return BeanConverter based on format request parameter.
+   * @param format the format for the converter.
+   * @return BeanConverter based on the format input param. Will default to JSON
+   */
+  protected BeanConverter getConverterForFormat(String format) {
+    return ATOM_FORMAT.equals(format) ? atomConverter : XML_FORMAT.equals(format) ? xmlConverter :
+        jsonConverter;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataServiceServletFetcher.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataServiceServletFetcher.java
new file mode 100644
index 0000000..dddcce3
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DataServiceServletFetcher.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.common.servlet.ParameterFetcher;
+
+import com.google.common.collect.ImmutableMap;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+/**
+ * Default implementation for the GadgetDataServlet parameter fetcher. Do not change unless you have
+ * a compelling need to pass more parameters into the createResponse method.
+ */
+public class DataServiceServletFetcher implements ParameterFetcher {
+
+  public Map<String, String> fetch(HttpServletRequest req) {
+    return ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, req.getParameter("st"));
+  }
+}
+
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/DefaultHandlerRegistry.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DefaultHandlerRegistry.java
new file mode 100644
index 0000000..7550621
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/DefaultHandlerRegistry.java
@@ -0,0 +1,659 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.Reader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Default implementation of HandlerRegistry. Bind to appropriately
+ * annotated handlers.
+ */
+public class DefaultHandlerRegistry implements HandlerRegistry {
+
+  private static final Logger LOG = Logger.getLogger(DefaultHandlerRegistry.class.getName());
+
+  // Map service - > method -> { handler, ...}
+  private final Map<String, Map<String, SortedSet<RestPath>>> serviceMethodPathMap =
+      Maps.newHashMap();
+  private final Map<String, RpcInvocationHandler> rpcOperations = Maps.newHashMap();
+
+  private final Injector injector;
+  private final BeanJsonConverter beanJsonConverter;
+  private final HandlerExecutionListener executionListener;
+
+  /**
+   * Creates a dispatcher with the specified handler classes
+   *
+   * @param injector Used to create instance if handler is a Class
+   */
+  @Inject
+  public DefaultHandlerRegistry(Injector injector,
+                                BeanJsonConverter beanJsonConverter,
+                                HandlerExecutionListener executionListener) {
+    this.injector = injector;
+    this.beanJsonConverter = beanJsonConverter;
+    this.executionListener = executionListener;
+  }
+
+  /**
+   * Add handlers to the registry
+   * @param handlers
+   */
+  public void addHandlers(Set<Object> handlers) {
+    for (final Object handler : handlers) {
+      Class<?> handlerType;
+      Provider<?> handlerProvider;
+      if (handler instanceof Class<?>) {
+        handlerType = (Class<?>) handler;
+        handlerProvider = injector.getProvider(handlerType);
+      } else {
+        handlerType = handler.getClass();
+        handlerProvider = new Provider<Object>() {
+          public Object get() {
+            return handler;
+          }
+        };
+      }
+      Preconditions.checkState(handlerType.isAnnotationPresent(Service.class),
+          "Attempt to bind unannotated service implementation %s",handlerType.getName());
+
+      Service service = handlerType.getAnnotation(Service.class);
+
+      for (Method m : handlerType.getMethods()) {
+        if (m.isAnnotationPresent(Operation.class)) {
+          Operation op = m.getAnnotation(Operation.class);
+          createRpcHandler(handlerProvider, service, op, m);
+          createRestHandler(handlerProvider, service, op, m);
+        }
+      }
+    }
+  }
+
+  /**
+   * Get an RPC handler
+   */
+  public RpcHandler getRpcHandler(JSONObject rpc) {
+    try {
+      String key = rpc.getString("method");
+      RpcInvocationHandler rpcHandler = rpcOperations.get(key);
+      if (rpcHandler == null) {
+        return new ErrorRpcHandler(new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+            "The method " + key + " is not implemented"));
+      }
+      return new RpcInvocationWrapper(rpcHandler, rpc);
+    } catch (JSONException je) {
+      return new ErrorRpcHandler(new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+          "No method requested in RPC"));
+    }
+  }
+
+  /**
+   * Get a REST request handler
+   */
+  public RestHandler getRestHandler(String path, String method) {
+    method = method.toUpperCase();
+    if (path != null) {
+      if (path.startsWith("/")) {
+        path = path.substring(1);
+      }
+      String[] pathParts = StringUtils.splitPreserveAllTokens(path, '/');
+      Map<String, SortedSet<RestPath>> methods = serviceMethodPathMap.get(pathParts[0]);
+      if (methods != null) {
+        SortedSet<RestPath> paths = methods.get(method);
+        if (paths != null) {
+          for (RestPath restPath : paths) {
+            RestHandler handler = restPath.accept(pathParts);
+            if (handler != null) {
+              return handler;
+            }
+          }
+        }
+      }
+    }
+    return new ErrorRestHandler(new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+        "No service defined for path " + path));
+  }
+
+  public Set<String> getSupportedRestServices() {
+    Set<String> result = Sets.newTreeSet();
+    for (Map<String, SortedSet<RestPath>> methods : serviceMethodPathMap.values()) {
+      for (Map.Entry<String, SortedSet<RestPath>> method : methods.entrySet()) {
+        for (RestPath path : method.getValue()) {
+          result.add(method.getKey() + ' ' + path.operationPath);
+        }
+      }
+    }
+    return Collections.unmodifiableSet(result);
+  }
+
+  public Set<String> getSupportedRpcServices() {
+    return Collections.unmodifiableSet(rpcOperations.keySet());
+  }
+
+  private void createRestHandler(Provider<?> handlerProvider,
+      Service service, Operation op, Method m) {
+    try {
+      MethodCaller methodCaller = new MethodCaller(m, true);
+      String opName = m.getName();
+      if (!Strings.isNullOrEmpty(op.name())) {
+        opName = op.name();
+      }
+      RestInvocationHandler restHandler = new RestInvocationHandler(op, methodCaller,
+          handlerProvider, beanJsonConverter,
+          new ExecutionListenerWrapper(service.name(), opName, executionListener));
+      String serviceName = service.name();
+
+      Map<String, SortedSet<RestPath>> methods = serviceMethodPathMap.get(serviceName);
+      if (methods == null) {
+        methods = Maps.newHashMap();
+        serviceMethodPathMap.put(serviceName, methods);
+      }
+
+      for (String httpMethod : op.httpMethods()) {
+        if (!Strings.isNullOrEmpty(httpMethod)) {
+          httpMethod = httpMethod.toUpperCase();
+          SortedSet<RestPath> sortedSet = methods.get(httpMethod);
+          if (sortedSet == null) {
+            sortedSet = Sets.newTreeSet();
+            methods.put(httpMethod, sortedSet);
+          }
+
+          if (Strings.isNullOrEmpty(op.path())) {
+            sortedSet.add(new RestPath('/' + serviceName +  service.path(), restHandler));
+          } else {
+            // Use the standard service name and constant prefix as the key
+            sortedSet.add(new RestPath('/' + serviceName + op.path(), restHandler));
+          }
+        }
+      }
+    } catch (NoSuchMethodException nme) {
+      LOG.log(Level.INFO, "No REST binding for " + service.name() + '.' + m.getName());
+    }
+
+  }
+
+  private void createRpcHandler(Provider<?> handlerProvider, Service service,
+      Operation op, Method m) {
+    try {
+      MethodCaller methodCaller = new MethodCaller(m, false);
+
+      String opName = m.getName();
+      // Use the override if its defined
+      if (op.name().length() > 0) {
+        opName = op.name();
+      }
+      RpcInvocationHandler rpcHandler =
+          new RpcInvocationHandler(methodCaller, handlerProvider, beanJsonConverter,
+              new ExecutionListenerWrapper(service.name(), opName, executionListener));
+      rpcOperations.put(service.name() + '.' + opName, rpcHandler);
+    } catch (NoSuchMethodException nme) {
+      LOG.log(Level.INFO, "No RPC binding for " + service.name() + '.' + m.getName());
+    }
+  }
+
+  /**
+   * Utility wrapper for the HandlerExecutionListener
+   */
+  private static class ExecutionListenerWrapper {
+    final String service;
+    final String operation;
+    final HandlerExecutionListener listener;
+
+    ExecutionListenerWrapper(String service, String operation,
+        HandlerExecutionListener listener) {
+      this.service = service;
+      this.operation = operation;
+      this.listener = listener;
+    }
+
+    private void executing(RequestItem req) {
+      listener.executing(service, operation, req);
+    }
+
+    private void executed(RequestItem req) {
+      listener.executed(service, operation, req);
+    }
+  }
+
+
+  /**
+   * Proxy binding for an RPC operation. We allow binding to methods that
+   * return non-Future types by wrapping them in ImmediateFuture.
+   */
+  static final class RpcInvocationHandler  {
+
+    private Provider<?> handlerProvider;
+    private BeanJsonConverter beanJsonConverter;
+    private ExecutionListenerWrapper listener;
+    private MethodCaller methodCaller;
+
+    private RpcInvocationHandler(MethodCaller methodCaller,
+                                 Provider<?> handlerProvider,
+                                 BeanJsonConverter beanJsonConverter,
+                                 ExecutionListenerWrapper listener) {
+      this.handlerProvider = handlerProvider;
+      this.beanJsonConverter = beanJsonConverter;
+      this.listener = listener;
+      this.methodCaller = methodCaller;
+    }
+
+    public Future<?> execute(JSONObject rpc, Map<String, FormDataItem> formItems,
+        SecurityToken token, BeanConverter converter) {
+      RequestItem item;
+      try {
+        JSONObject params = rpc.has("params") ? (JSONObject)rpc.get("params") : new JSONObject();
+        item = methodCaller.getRpcRequestItem(params, formItems, token, beanJsonConverter);
+      } catch (Exception e) {
+        return Futures.immediateFailedFuture(e);
+      }
+
+      try {
+        listener.executing(item);
+        return methodCaller.call(handlerProvider.get(), item);
+      } catch (Exception e) {
+        return Futures.immediateFailedFuture(e);
+      } finally {
+        listener.executed(item);
+      }
+    }
+  }
+
+  /**
+   * Encapsulate the dispatch of a single RPC
+   */
+  static final class RpcInvocationWrapper implements RpcHandler {
+
+    final RpcInvocationHandler handler;
+    final JSONObject rpc;
+
+    RpcInvocationWrapper(RpcInvocationHandler handler, JSONObject rpc) {
+      this.handler = handler;
+      this.rpc = rpc;
+    }
+
+    public Future<?> execute(Map<String, FormDataItem> formItems, SecurityToken st,
+        BeanConverter converter) {
+      return handler.execute(rpc, formItems, st, converter);
+    }
+  }
+
+  /**
+   * Proxy binding for a REST operation. We allow binding to methods that
+   * return non-Future types by wrapping them in ImmediateFuture.
+   */
+  static final class RestInvocationHandler  {
+    final Provider<?> handlerProvider;
+    final Operation operation;
+    final BeanJsonConverter beanJsonConverter;
+    final ExecutionListenerWrapper listener;
+    final MethodCaller methodCaller;
+
+    private RestInvocationHandler(Operation operation,
+        MethodCaller methodCaller,
+        Provider<?> handlerProvider,
+        BeanJsonConverter beanJsonConverter,
+        ExecutionListenerWrapper listener) {
+      this.operation = operation;
+      this.handlerProvider = handlerProvider;
+      this.beanJsonConverter = beanJsonConverter;
+      this.listener = listener;
+      this.methodCaller = methodCaller;
+    }
+
+    public Future<?> execute(Map<String, String[]> parameters, Reader body,
+                             SecurityToken token, BeanConverter converter) {
+
+      RequestItem item;
+      try {
+        // bind the body contents if available
+        if (body != null) {
+          parameters.put(operation.bodyParam(), new String[]{IOUtils.toString(body)});
+        }
+        item = methodCaller.getRestRequestItem(parameters, token, converter, beanJsonConverter);
+      } catch (Exception e) {
+        return Futures.immediateFailedFuture(e);
+      }
+
+      try {
+        listener.executing(item);
+        return methodCaller.call(handlerProvider.get(), item);
+      } catch (Exception e) {
+        return Futures.immediateFailedFuture(e);
+      } finally {
+        listener.executed(item);
+      }
+    }
+  }
+
+  /**
+   * Encapsulate the dispatch of a single REST call.
+   * Augment the executed parameters with those extracted from the path
+   */
+  static class RestInvocationWrapper implements RestHandler {
+    RestInvocationHandler handler;
+    Map<String, String[]> pathParams;
+
+    RestInvocationWrapper(Map<String, String[]> pathParams, RestInvocationHandler handler) {
+      this.pathParams = pathParams;
+      this.handler = handler;
+    }
+
+    public Future<?> execute(Map<String, String[]> parameters, Reader body,
+                             SecurityToken token, BeanConverter converter) {
+      pathParams.putAll(parameters);
+      return handler.execute(pathParams, body, token, converter);
+    }
+  }
+
+  /**
+   * Calls methods annotated with {@link Operation} and appropriately translates
+   * RequestItem to the actual input class of the method.
+   */
+  private static class MethodCaller {
+    /** Type of object to create for this method, or null if takes no args */
+    private Class<?> inputClass;
+
+    /** Constructors for request item class that will be used */
+    private final Constructor<?> restRequestItemConstructor;
+    private final Constructor<?> rpcRequestItemConstructor;
+
+    /** The method */
+    private final Method method;
+
+    /**
+     * Create information needed to call a method
+     * @param method The method
+     * @param isRest True if REST method (affects which RequestItem constructor to return)
+     * @throws NoSuchMethodException
+     */
+    public MethodCaller(Method method, boolean isRest) throws NoSuchMethodException {
+      this.method = method;
+
+      inputClass = method.getParameterTypes().length > 0 ? method.getParameterTypes()[0] : null;
+
+      // Methods that need RequestItem interface should automatically get a BaseRequestItem
+      if (RequestItem.class.equals(inputClass)) {
+        inputClass = BaseRequestItem.class;
+      }
+      boolean inputIsRequestItem = (inputClass != null) &&
+          RequestItem.class.isAssignableFrom(inputClass);
+
+      Class<?> requestItemType = inputIsRequestItem ? inputClass : BaseRequestItem.class;
+
+      restRequestItemConstructor = requestItemType.getConstructor(Map.class,
+          SecurityToken.class, BeanConverter.class, BeanJsonConverter.class);
+      rpcRequestItemConstructor = requestItemType.getConstructor(JSONObject.class,
+          Map.class, SecurityToken.class, BeanConverter.class, BeanJsonConverter.class);
+    }
+
+    public RequestItem getRestRequestItem(Map<String, String[]> params, SecurityToken token,
+        BeanConverter converter, BeanJsonConverter jsonConverter) {
+      return getRequestItem(params, token, converter, jsonConverter, restRequestItemConstructor);
+    }
+
+    public RequestItem getRpcRequestItem(JSONObject params, Map<String, FormDataItem> formItems,
+        SecurityToken token, BeanJsonConverter converter) {
+      return getRequestItem(params, formItems, token, converter, converter, rpcRequestItemConstructor);
+    }
+
+    private RequestItem getRequestItem(Object params, Map<String, FormDataItem> formItems,
+        SecurityToken token, BeanConverter converter, BeanJsonConverter jsonConverter,
+        Constructor<?> constructor) {
+      try {
+        return (RequestItem) constructor.newInstance(params, formItems, token,  converter,
+            jsonConverter);
+      } catch (InstantiationException e) {
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    private RequestItem getRequestItem(Object params, SecurityToken token, BeanConverter converter,
+        BeanJsonConverter jsonConverter, Constructor<?> constructor) {
+      try {
+        return (RequestItem) constructor.newInstance(params, token,  converter, jsonConverter);
+      } catch (InstantiationException e) {
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    public Future<?> call(Object handler, RequestItem item) {
+      try {
+        Object result;
+        if (inputClass == null) {
+          result = method.invoke(handler);
+        } else if (RequestItem.class.isAssignableFrom(inputClass)) {
+          result = method.invoke(handler, item);
+        } else {
+          result = method.invoke(handler, item.getTypedRequest(inputClass));
+        }
+
+        if (result instanceof Future<?>) {
+          return (Future<?>) result;
+        }
+        return Futures.immediateFuture(result);
+      } catch (IllegalAccessException e) {
+        return Futures.immediateFailedFuture(e);
+      } catch (InvocationTargetException e) {
+        // Unwrap the internal exception
+        return Futures.immediateFailedFuture(e.getTargetException());
+      }
+    }
+  }
+
+  /**
+   * Standard REST handler to wrap errors
+   */
+  private static final class ErrorRestHandler implements RestHandler {
+
+    private final ProtocolException error;
+
+    public ErrorRestHandler(ProtocolException error) {
+      this.error = error;
+    }
+
+    public Future<?> execute(Map<String, String[]> parameters, Reader body,
+                          SecurityToken token, BeanConverter converter) {
+      return Futures.immediateFailedFuture(error);
+    }
+  }
+
+  /**
+   * Standard RPC handler to wrap errors
+   */
+  private static final class ErrorRpcHandler implements RpcHandler {
+
+    private final ProtocolException error;
+
+    public ErrorRpcHandler(ProtocolException error) {
+      this.error = error;
+    }
+
+    public Future<?> execute(Map<String, FormDataItem> formItems, SecurityToken token,
+        BeanConverter converter) {
+      return Futures.immediateFailedFuture(error);
+    }
+  }
+
+  /**
+   * Path matching and parameter extraction for REST.
+   */
+  static class RestPath implements Comparable<RestPath> {
+
+    enum PartType {
+      CONST, SINGULAR_PARAM, PLURAL_PARAM
+    }
+
+    static class Part {
+      String partName;
+      PartType type;
+      Part(String partName, PartType type) {
+        this.partName = partName;
+        this.type = type;
+      }
+    }
+
+    final String operationPath;
+    final List<Part> parts;
+    final RestInvocationHandler handler;
+    final int constCount;
+    final int lastConstIndex;
+
+    public RestPath(String path, RestInvocationHandler handler) {
+      int tmpConstCount = 0;
+      int tmpConstIndex = -1;
+      this.operationPath = path;
+      String[] partArr = StringUtils.split(path.substring(1), '/');
+      parts = Lists.newArrayList();
+      for (int i = 0; i < partArr.length; i++) {
+        String part = partArr[i];
+        if (part.startsWith("{")) {
+          if (part.endsWith("}+")) {
+            parts.add(new Part(part.substring(1, part.length() - 2), PartType.PLURAL_PARAM));
+          } else if (part.endsWith("}")) {
+            parts.add(new Part(part.substring(1, part.length() - 1), PartType.SINGULAR_PARAM));
+          } else {
+            throw new IllegalStateException("Invalid REST path part format " + part);
+          }
+        } else {
+          parts.add(new Part(part, PartType.CONST));
+          tmpConstCount++;
+          tmpConstIndex = i;
+        }
+      }
+      constCount = tmpConstCount;
+      lastConstIndex = tmpConstIndex;
+      this.handler = handler;
+    }
+
+    /**
+     * See if this Rest path is a match for the requested path
+     * Requested path is offset by 1 as it includes service name
+     * @return A handler with the path parameters decoded, null if not a match for the path
+     */
+    public RestInvocationWrapper accept(String[] requestPathParts) {
+      if (constCount > 0) {
+        if (lastConstIndex >= requestPathParts.length) {
+          // Last required constant match is not possible with
+          // this request
+          return null;
+        }
+        for (int i = 0; i <= lastConstIndex; i++) {
+          if (parts.get(i).type == PartType.CONST &&
+              !parts.get(i).partName.equals(requestPathParts[i])) {
+            // Constant part does not match request
+            return null;
+          }
+        }
+      }
+
+      // All constant parts matched, extract the parameters
+      Map<String, String[]> parsedParams = Maps.newHashMap();
+      for (int i = 0; i < Math.min(requestPathParts.length, parts.size()); i++) {
+        if (parts.get(i).type == PartType.SINGULAR_PARAM) {
+          if (requestPathParts[i].indexOf(',') != -1) {
+            throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+                "Cannot expect plural value " + requestPathParts[i]
+                    + " for singular field " + parts.get(i) + " for path " + operationPath);
+          }
+          parsedParams.put(parts.get(i).partName, new String[]{requestPathParts[i]});
+        } else if (parts.get(i).type == PartType.PLURAL_PARAM) {
+          parsedParams.put(parts.get(i).partName, StringUtils.splitPreserveAllTokens(requestPathParts[i], ','));
+        }
+      }
+      return new RestInvocationWrapper(parsedParams, handler);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof RestPath) {
+        RestPath that = (RestPath)other;
+        return (this.constCount == that.constCount &&
+            this.lastConstIndex == that.lastConstIndex &&
+            Objects.equal(this.operationPath, that.operationPath));
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return this.constCount ^ this.lastConstIndex ^ operationPath.hashCode();
+    }
+
+    /**
+     * Rank based on the number of consant parts they accept, where the constant parts occur
+     * and lexical ordering.
+     */
+    public int compareTo(RestPath other) {
+      // Rank first by number of constant elements in the path
+      int result = other.constCount - this.constCount;
+      if (result == 0) {
+        // Rank second by the position of the last constant element
+        // (lower index is better)
+        result = this.lastConstIndex - other.lastConstIndex;
+      }
+      if (result == 0) {
+        // Rank lastly by lexical order
+        result = this.operationPath.compareTo(other.operationPath);
+      }
+      return result;
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerExecutionListener.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerExecutionListener.java
new file mode 100644
index 0000000..dee5e96
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerExecutionListener.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.logging.Logger;
+
+/**
+ * Called by the handler dispatcher prior to executing a handler. Used to allow
+ * containers to implement cross-cutting features such as request logging.
+ */
+@ImplementedBy(HandlerExecutionListener.NoOpHandler.class)
+public interface HandlerExecutionListener {
+
+  /**
+   * Called prior to executing a REST or RPC handler
+   * @param service Name of the service being called
+   * @param operation Name of operation being called
+   * @param request being executed
+   */
+  void executing(String service, String operation, RequestItem request);
+  void executed(String service, String operation, RequestItem request);
+
+  /**
+   * Default no-op implementation
+   */
+  public static class NoOpHandler implements HandlerExecutionListener {
+
+    public void executing(String service, String operation, RequestItem request) {
+      // No-op
+    }
+    public void executed(String service, String operation, RequestItem request) {
+      // No-op
+    }
+  }
+
+  /**
+   * A simple implementation that logs the start/stop times of requests
+   *
+   * You can configure this for use by adding a binding in your Guice Module like this:
+   *   bind(HandlerExecutionListener.class).to(HandlerExecutionListener.LoggingHandler.class);
+   */
+
+  public static class LoggingHandler implements HandlerExecutionListener {
+    public static final Logger LOG = Logger.getLogger(HandlerExecutionListener.class.toString());
+
+    public void executing(String service, String operation, RequestItem request) {
+      LOG.info("start - " + service + ' ' + operation);
+    }
+    public void executed(String service, String operation, RequestItem request) {
+      LOG.info("  end - " + service + ' ' + operation);
+    }
+  }
+}
+
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerPreconditions.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerPreconditions.java
new file mode 100644
index 0000000..dcfe75c
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerPreconditions.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+
+import java.util.Collection;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Utility class for common API call preconditions
+ */
+public final class HandlerPreconditions {
+
+  private HandlerPreconditions() {}
+
+  public static void requireNotEmpty(Collection<?> coll, String message)
+      throws ProtocolException {
+    if (coll.isEmpty()) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, message);
+    }
+  }
+
+  public static void requireEmpty(Collection<?> coll, String message) throws ProtocolException {
+    if (!coll.isEmpty()) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, message);
+    }
+  }
+
+  public static void requireSingular(Collection<?> coll, String message)
+      throws ProtocolException {
+    if (coll.size() != 1) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, message);
+    }
+  }
+
+  public static void requirePlural(Collection<?> coll, String message) throws ProtocolException {
+    if (coll.size() <= 1) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, message);
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerRegistry.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerRegistry.java
new file mode 100644
index 0000000..5f22edf
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/HandlerRegistry.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.inject.ImplementedBy;
+
+import org.json.JSONObject;
+
+import java.util.Set;
+
+/**
+ * Registry of REST and RPC handlers for the set of available services
+ */
+@ImplementedBy(DefaultHandlerRegistry.class)
+public interface HandlerRegistry {
+
+  /**
+   * Add a set of handlers to the registry
+   * @param handlers
+   */
+  void addHandlers(Set<Object> handlers);
+
+  /**
+   * @param rpc The rpc to dispatch
+   * @return the handler
+   */
+  RpcHandler getRpcHandler(JSONObject rpc);
+
+  /**
+   * @param path Path of the service
+   * @param method The HTTP method
+   * @return the handler
+   */
+  RestHandler getRestHandler(String path, String method);
+
+  /**
+   * @return The list of available services
+   */
+  Set<String> getSupportedRestServices();
+
+  /**
+   * @return The list of available services
+   */
+  Set<String> getSupportedRpcServices();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java
new file mode 100644
index 0000000..ab3d245
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/JsonRpcServlet.java
@@ -0,0 +1,356 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.base.Strings;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.util.JsonConversionUtil;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+import org.apache.shindig.protocol.multipart.MultipartFormParser;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+/**
+ * JSON-RPC handler servlet.
+ */
+public class JsonRpcServlet extends ApiServlet {
+
+  public static final Set<String> ALLOWED_CONTENT_TYPES =
+      new ImmutableSet.Builder<String>().addAll(ContentTypes.ALLOWED_JSON_CONTENT_TYPES)
+          .addAll(ContentTypes.ALLOWED_MULTIPART_CONTENT_TYPES).build();
+
+  /**
+   * In a multipart request, the form item with field name "request" will contain the
+   * actual request, per the proposed Opensocial 0.9 specification.
+   */
+  public static final String REQUEST_PARAM = "request";
+
+  private MultipartFormParser formParser;
+
+  @Inject
+  void setMultipartFormParser(MultipartFormParser formParser) {
+    this.formParser = formParser;
+  }
+
+  private String jsonRpcResultField = "result";
+  private boolean jsonRpcBothFields = false;
+
+  @Inject(optional = true)
+  void setJsonRpcResultField(@Named("shindig.json-rpc.result-field")String jsonRpcResultField) {
+    this.jsonRpcResultField = jsonRpcResultField;
+    jsonRpcBothFields = "both".equals(jsonRpcResultField);
+  }
+
+  @Override
+  protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
+      throws IOException {
+    setCharacterEncodings(servletRequest, servletResponse);
+    servletResponse.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    // only GET/POST
+    String method = servletRequest.getMethod();
+
+    if (!("GET".equals(method) || "POST".equals(method))) {
+      sendError(servletResponse,
+                new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "Only POST/GET Allowed"));
+      return;
+    }
+
+    SecurityToken token = getSecurityToken(servletRequest);
+    if (token == null) {
+      sendSecurityError(servletResponse);
+      return;
+    }
+
+    HttpUtil.setCORSheader(servletResponse, containerConfig.<String>getList(token.getContainer(), "gadgets.parentOrigins"));
+
+    try {
+      String content = null;
+      String callback = null; // for JSONP
+      Map<String,FormDataItem> formData = Maps.newHashMap();
+
+      // Get content or deal with JSON-RPC GET
+      if ("POST".equals(method)) {
+        content = getPostContent(servletRequest, formData);
+      } else if (this.isJSONPAllowed && HttpUtil.isJSONP(servletRequest)) {
+        content = servletRequest.getParameter("request");
+        callback = servletRequest.getParameter("callback");
+      } else {
+        // GET request, fromRequest() creates the json objects directly.
+        JSONObject request = JsonConversionUtil.fromRequest(servletRequest);
+
+        if (request != null) {
+          dispatch(request, formData, servletRequest, servletResponse, token, null);
+          return;
+        }
+      }
+
+      if (content == null) {
+        sendError(servletResponse, new ResponseItem(HttpServletResponse.SC_BAD_REQUEST, "No content specified"));
+        return;
+      }
+
+      if (isContentJsonBatch(content)) {
+        JSONArray batch = new JSONArray(content);
+        dispatchBatch(batch, formData, servletRequest, servletResponse, token, callback);
+      } else {
+        JSONObject request = new JSONObject(content);
+        dispatch(request, formData, servletRequest, servletResponse, token, callback);
+      }
+    } catch (JSONException je) {
+      sendJsonParseError(je, servletResponse);
+    } catch (IllegalArgumentException e) {
+      // a bad jsonp request..
+      sendBadRequest(e, servletResponse);
+    }  catch (ContentTypes.InvalidContentTypeException icte) {
+      sendBadRequest(icte, servletResponse);
+    }
+  }
+
+  protected String getPostContent(HttpServletRequest request, Map<String,FormDataItem> formItems)
+      throws ContentTypes.InvalidContentTypeException, IOException {
+    String content = null;
+
+    ContentTypes.checkContentTypes(ALLOWED_CONTENT_TYPES, request.getContentType());
+
+    if (formParser.isMultipartContent(request)) {
+      for (FormDataItem item : formParser.parse(request)) {
+        if (item.isFormField() && REQUEST_PARAM.equals(item.getFieldName()) && content == null) {
+          // As per spec, in case of a multipart/form-data content, there will be one form field
+          // with field name as "request". It will contain the json request. Any further form
+          // field or file item will not be parsed out, but will be exposed via getFormItem
+          // method of RequestItem.
+          if (!Strings.isNullOrEmpty(item.getContentType())) {
+            ContentTypes.checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES, item.getContentType());
+          }
+          content = item.getAsString();
+        } else {
+          formItems.put(item.getFieldName(), item);
+        }
+      }
+    } else {
+      content = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
+    }
+    return content;
+  }
+
+  protected void dispatchBatch(JSONArray batch, Map<String, FormDataItem> formItems ,
+      HttpServletRequest servletRequest, HttpServletResponse servletResponse,
+      SecurityToken token, String callback) throws JSONException, IOException {
+    // Use linked hash map to preserve order
+    List<Future<?>> responses = Lists.newArrayListWithCapacity(batch.length());
+
+    // Gather all Futures.  We do this up front so that
+    // the first call to get() comes after all futures are created,
+    // which allows for implementations that batch multiple Futures
+    // into single requests.
+    for (int i = 0; i < batch.length(); i++) {
+      JSONObject batchObj = batch.getJSONObject(i);
+      responses.add(getHandler(batchObj, servletRequest).execute(formItems, token, jsonConverter));
+    }
+
+    // Resolve each Future into a response.
+    // TODO: should use shared deadline across each request
+    List<Object> result = new ArrayList<Object>(batch.length());
+    for (int i = 0; i < batch.length(); i++) {
+      JSONObject batchObj = batch.getJSONObject(i);
+      String key = null;
+      if (batchObj.has("id")) {
+        key = batchObj.getString("id");
+      }
+      result.add(getJSONResponse(key, getResponseItem(responses.get(i))));
+    }
+
+    // Generate the output
+    Writer writer = servletResponse.getWriter();
+    if (callback != null) writer.append(callback).append('(');
+    jsonConverter.append(writer, result);
+    if (callback != null) writer.append(");\n");
+  }
+
+  protected void dispatch(JSONObject request, Map<String, FormDataItem> formItems,
+      HttpServletRequest servletRequest, HttpServletResponse servletResponse,
+      SecurityToken token, String callback) throws JSONException, IOException {
+    String key = null;
+
+    if (request.has("id")) {
+      key = request.getString("id");
+    }
+
+    // getRpcHandler never returns null
+    Future<?> future = getHandler(request, servletRequest).execute(formItems, token, jsonConverter);
+
+    // Resolve each Future into a response.
+    // TODO: should use shared deadline across each request
+    ResponseItem response = getResponseItem(future);
+    Object result = getJSONResponse(key, response);
+
+    // Generate the output
+    Writer writer = servletResponse.getWriter();
+    if (callback != null) writer.append(callback).append('(');
+    jsonConverter.append(writer, result);
+    if (callback != null) writer.append(");\n");
+  }
+
+  /**
+   *
+   */
+  protected void addResult(Map<String,Object> result, Object data) {
+    if (jsonRpcBothFields) {
+      result.put("result", data);
+      result.put("data", data);
+    } else {
+      result.put(jsonRpcResultField, data);
+    }
+  }
+
+  /**
+   * Determine if the content contains a batch request
+   *
+   * @param content json content or null
+   * @return true if content contains is a json array, not a json object or null
+   */
+  private boolean isContentJsonBatch(String content) {
+    if (content == null) return false;
+    return ((content.indexOf('[') != -1) && content.indexOf('[') < content.indexOf('{'));
+  }
+  /**
+   * Wrap call to dispatcher to allow for implementation specific overrides
+   * and servlet-request contextual handling
+   */
+  protected RpcHandler getHandler(JSONObject rpc, HttpServletRequest request) {
+    return dispatcher.getRpcHandler(rpc);
+  }
+
+  protected Object getJSONResponse(String key, ResponseItem responseItem) {
+    Map<String, Object> result = Maps.newHashMap();
+    if (key != null) {
+      result.put("id", key);
+    }
+    if (responseItem.getErrorCode() < 200 ||
+        responseItem.getErrorCode() >= 400) {
+      result.put("error", getErrorJson(responseItem));
+    } else {
+      Object response = responseItem.getResponse();
+      if (response instanceof DataCollection) {
+        addResult(result, ((DataCollection) response).getEntry());
+      } else if (response instanceof RestfulCollection) {
+        Map<String, Object> map = Maps.newHashMap();
+        RestfulCollection<?> collection = (RestfulCollection<?>) response;
+        // Return sublist info
+        if (collection.getTotalResults() != collection.getList().size()) {
+          map.put("startIndex", collection.getStartIndex());
+          map.put("itemsPerPage", collection.getItemsPerPage());
+        }
+        // always put in totalResults
+        map.put("totalResults", collection.getTotalResults());
+
+        // always add metadata for collections
+        map.put("filtered", collection.isFiltered());
+        map.put("updatedSince", collection.isUpdatedSince());
+        map.put("sorted", collection.isSorted());
+
+        map.put("list", collection.getList());
+        addResult(result, map);
+      } else {
+        addResult(result, response);
+      }
+
+      // TODO: put "code" for != 200?
+    }
+    return result;
+  }
+
+  /** Map of old-style error titles */
+  protected static final Map<Integer, String> errorTitles = ImmutableMap.<Integer, String> builder()
+     .put(HttpServletResponse.SC_NOT_IMPLEMENTED, "notImplemented")
+     .put(HttpServletResponse.SC_UNAUTHORIZED, "unauthorized")
+     .put(HttpServletResponse.SC_FORBIDDEN, "forbidden")
+     .put(HttpServletResponse.SC_BAD_REQUEST, "badRequest")
+     .put(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "internalError")
+     .put(HttpServletResponse.SC_EXPECTATION_FAILED, "limitExceeded")
+     .build();
+
+  // TODO(doll): Refactor the responseItem so that the fields on it line up with this format.
+  // Then we can use the general converter to output the response to the client and we won't
+  // be harcoded to json.
+  protected Object getErrorJson(ResponseItem responseItem) {
+    Map<String, Object> error = new HashMap<String, Object>(2, 1);
+    error.put("code", responseItem.getErrorCode());
+
+    String message = errorTitles.get(responseItem.getErrorCode());
+    if (message == null) {
+      message = responseItem.getErrorMessage();
+    } else {
+      if (StringUtils.isNotBlank(responseItem.getErrorMessage())) {
+        message += ": " + responseItem.getErrorMessage();
+      }
+    }
+
+    if (StringUtils.isNotBlank(message)) {
+      error.put("message", message);
+    }
+
+    if (responseItem.getResponse() != null) {
+      error.put("data", responseItem.getResponse());
+    }
+
+    return error;
+  }
+
+  @Override
+  protected void sendError(HttpServletResponse servletResponse, ResponseItem responseItem)
+      throws IOException {
+    jsonConverter.append(servletResponse.getWriter(), getErrorJson(responseItem));
+
+    servletResponse.setStatus(responseItem.getErrorCode());
+  }
+
+  protected void sendBadRequest(Throwable t, HttpServletResponse response) throws IOException {
+    sendError(response, new ResponseItem(HttpServletResponse.SC_BAD_REQUEST,
+        "Invalid input - " + t.getMessage()));
+  }
+
+  protected void sendJsonParseError(JSONException e, HttpServletResponse response) throws IOException {
+    sendError(response, new ResponseItem(HttpServletResponse.SC_BAD_REQUEST,
+        "Invalid JSON - " + e.getMessage()));
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/Operation.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/Operation.java
new file mode 100644
index 0000000..756a5fd
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/Operation.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotates a method on a ServiceHandler which expose a REST/RPC operation
+ * The name of the annotated method is the literal name of the method for JSON-RPC
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface Operation {
+  /**
+   * The HTTP methods to bind this operation to.
+   */
+  String[] httpMethods();
+
+  /**
+   * The parameter name to bind the body content to in the RequestItem
+   * passed to the REST/RPC handler.
+   */
+  String bodyParam() default "body";
+
+  /**
+   * The path to match for the operation to override the service
+   * path matching and parameter binding. This is useful for situations
+   * such as /<service>/@supportedFields where the path determines the
+   * operation rather than the HTTP method in REST
+   */
+  String path() default "";
+
+  /**
+   * The name to match for the RPC operation to override the default behavior
+   * which is to use the name of the annotated method
+   */
+  String name() default "";
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/ProtocolException.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ProtocolException.java
new file mode 100644
index 0000000..184229d
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ProtocolException.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Unchecked exception class for errors thrown by request handlers
+ */
+public class ProtocolException extends RuntimeException {
+  private final int errorCode;
+
+  /**
+   * The application specific response value associated with this exception.
+   */
+  private final Object response;
+
+  public ProtocolException(int errorCode, String errorMessage, Throwable cause) {
+    super(errorMessage, cause);
+    checkErrorCode(errorCode);
+    this.errorCode = errorCode;
+    this.response = null;
+  }
+
+  public ProtocolException(int errorCode, String errorMessage) {
+    this(errorCode, errorMessage, null);
+  }
+
+  public ProtocolException(int errorCode, String errorMessage, Object response) {
+    super(errorMessage);
+    checkErrorCode(errorCode);
+    this.errorCode = errorCode;
+    this.response = response;
+  }
+
+  public int getCode() {
+    return errorCode;
+  }
+
+  public Object getResponse() {
+    return response;
+  }
+
+  private void checkErrorCode(int code) {
+    // 200 is not a legit use of ProtocolExceptions.
+    Preconditions.checkArgument(code != HttpServletResponse.SC_OK,
+        "May not use OK error code with ProtocolException");
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/RequestItem.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RequestItem.java
new file mode 100644
index 0000000..c583894
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RequestItem.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.model.FilterOperation;
+import org.apache.shindig.protocol.model.SortOrder;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A request to pass to a bound service handler
+ */
+public interface RequestItem {
+
+  // Common OpenSocial API fields
+  String APP_ID = "appId";
+  String START_INDEX = "startIndex";
+  String COUNT = "count";
+  String SORT_BY = "sortBy";
+  String SORT_ORDER = "sortOrder";
+  String FILTER_BY = "filterBy";
+  String FILTER_OPERATION = "filterOp";
+  String FILTER_VALUE = "filterValue";
+  String FIELDS = "fields";// Opensocial defaults
+  int DEFAULT_START_INDEX = 0;
+  int DEFAULT_COUNT = 20;
+  String APP_SUBSTITUTION_TOKEN = "@app";
+
+  /**
+   * Gets the Opensocial App ID for this request
+   * @return an app ID
+   */
+  String getAppId();
+
+  /**
+   * Gets the value of the updatedSince parameter
+   * @return A Date representing the updatedSince value
+   */
+  Date getUpdatedSince();
+
+  /**
+   * Gets the value of the startIndex parameter
+   * @return An integer containing the value of startIndex
+   */
+  int getStartIndex();
+
+  /**
+   * Gets the value of the count parameter
+   * @return An integer containing the value of count
+   */
+  int getCount();
+
+  /**
+   * Gets the value of the sortBy parameter
+   * @return the value of the sortBy parameter
+   */
+  String getSortBy();
+
+  /**
+   * Gets the value of the sortOrder parameter
+   * @return a SortOrder enum value representing the sortOrder parameter
+   */
+  SortOrder getSortOrder();
+
+  /**
+   * Gets the value of the filterBy parameter
+   * @return the value of the filterBy parameter
+   */
+  String getFilterBy();
+
+  /**
+   * Gets the value of the filterOperation parameter
+   * @return a SortOrder enum value representing the filterOperation parameter
+   */
+  FilterOperation getFilterOperation();
+
+  /**
+   * Gets the value of the filterValue parameter
+   * @return the value of the filterValue parameter
+   */
+  String getFilterValue();
+
+  /**
+   * Gets the unique set of fields from the request
+   *
+   * @return Set of field names, empty if no fields specified.
+   */
+  Set<String> getFields();
+
+  /**
+   * Get the unique set of fields from the request with defaults
+   * @param defaultValue returned if no fields are specified in the request.
+   * @return specified set of fields or default value
+   */
+  Set<String> getFields(Set<String> defaultValue);
+
+  /**
+   * Returns the security token of this request
+   * @return the token
+   */
+  SecurityToken getToken();
+
+  /**
+   * Converts a parameter into an object using a converter
+   * @param parameterName the name of the parameter with data to convert
+   * @param dataTypeClass The class to make
+   * @param <T> The type of this object
+   * @return A Valid Object of the given type
+   */
+
+  <T> T getTypedParameter(String parameterName, Class<T> dataTypeClass);
+
+  /**
+   * Assume that all the parameters in the request belong to single aggregate
+   * type and convert to it.
+   * @param dataTypeClass the class to convert to
+   * @return Typed request object
+   */
+
+  <T> T getTypedRequest(Class<T> dataTypeClass);
+
+  /**
+   * Gets the specified parameter as a string
+   * @param paramName the param name to get
+   * @return the paramName value or null if the parameter is not found
+   */
+  String getParameter(String paramName);
+
+  /**
+   * Gets the specified parameter as a string, with a default value
+   * @param paramName the param name to get
+   * @param defaultValue the default value of the parameter
+   * @return the paramName value or defaultValue if the parameter is not found
+   */
+  String getParameter(String paramName, String defaultValue);
+
+  /**
+   * Tries to get a list of values for a specified parameter.  This can include splitting
+   * text on commas, dereferencing a json array and more.
+   * @param paramName The parameter
+   * @return A list of strings for the given parameter
+   */
+  List<String> getListParameter(String paramName);
+
+  /**
+   * Returns MIME content data for multipart/mixed form submissions
+   * @param partName the part name to retrieve
+   * @return The FormDataItem for this part.
+   */
+  FormDataItem getFormMimePart(String partName);
+
+  /**
+   * Gets an attribute for this request.  Attributes are a place to store per-request values that persist across the
+   * life cycle.
+   *
+   * @param val the localized string variable for this request
+   * @return the object associated with this requested string value or null if not found
+   */
+  Object getAttribute(String val);
+
+  /**
+   * Sets an attribute on this request object
+   * @param val string value
+   * @param obj an object
+   */
+  void setAttribute(String val, Object obj);
+
+  /**
+   * Get the list of parameter names for this request object.
+   * @return A set of Parameter Names.
+   */
+   Set<String> getParameterNames();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/ResponseItem.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ResponseItem.java
new file mode 100644
index 0000000..1cb4f69
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/ResponseItem.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.base.Objects;
+
+/**
+ * Represents the response items that get handed back as json within the
+ * DataResponse.
+ */
+public final class ResponseItem {
+  /**
+   * The error code associated with the item.
+   */
+  private final int errorCode;
+
+  /**
+   * The error message.
+   */
+  private final String errorMessage;
+
+  /**
+   * The response value.
+   */
+  private final Object response;
+
+  /**
+   * Create a ResponseItem specifying the ResponseError and error Message.
+   * @param errorCode an RPC error code
+   * @param errorMessage the Error Message
+   */
+  public ResponseItem(int errorCode, String errorMessage) {
+    this(errorCode, errorMessage, null);
+  }
+
+  /**
+   * Create a ResponseItem specifying the ResponseError and error Message.
+   * @param errorCode an RPC error code
+   * @param errorMessage the Error Message
+   * @param response the application specific value that will be sent as
+   *     as part of "data" section of the error.
+   */
+  public ResponseItem(int errorCode, String errorMessage, Object response) {
+    this.errorCode = errorCode;
+    this.errorMessage = errorMessage;
+    this.response = response;
+  }
+
+  /**
+   * Create a ResponseItem specifying a value.
+   */
+  public ResponseItem(Object response) {
+    this.errorCode = 200;
+    this.errorMessage = null;
+    this.response = response;
+  }
+
+  /**
+   * Get the response value.
+   */
+  public Object getResponse() {
+    return response;
+  }
+
+  /**
+   * Get the error code associated with this ResponseItem.
+   * @return the error code associated with this ResponseItem
+   */
+  public int getErrorCode() {
+    return errorCode;
+  }
+
+  /**
+   * Get the Error Message associated with this Response Item.
+   * @return the Error Message
+   */
+  public String getErrorMessage() {
+    return errorMessage;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+
+    if (!(o instanceof ResponseItem)) {
+      return false;
+    }
+
+    ResponseItem that = (ResponseItem) o;
+    return (errorCode == that.errorCode)
+        && Objects.equal(errorMessage, that.errorMessage)
+        && Objects.equal(response, that.response);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(errorCode, errorMessage, response);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/RestHandler.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RestHandler.java
new file mode 100644
index 0000000..2a5c5d7
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RestHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+
+import java.io.Reader;
+import java.util.Map;
+import java.util.concurrent.Future;
+
+/**
+ * Interface exposed by a REST handler
+ */
+public interface RestHandler {
+
+  /**
+   * Handle the request and return a Future from which the response object
+   * can be retrieved
+   */
+  Future<?> execute(Map<String, String[]> parameters, Reader body,
+                    SecurityToken token, BeanConverter converter);
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/RestfulCollection.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RestfulCollection.java
new file mode 100644
index 0000000..edcae16
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RestfulCollection.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Data structure representing a Rest response.
+ */
+public class RestfulCollection<T> extends HashMap<String, Object>{
+  private List<T> list;
+  private int startIndex;
+  private int totalResults;
+  private int itemsPerPage;
+
+  private boolean filtered = false;
+  private boolean sorted = false;
+  private boolean updatedSince = false;
+
+  /**
+   * Creates a new RestfulCollection that includes a complete set of entries.
+   *
+   * Default values for startIndex, totalResults, itemsPerPage and filtering parameters are automatically set.
+   *
+   * @param entry a list of entries
+   */
+  public RestfulCollection(List<T> entry) {
+    this(entry, 0, entry.size(), entry.size());
+    this.filtered=true;
+    this.sorted=true;
+    this.updatedSince=true;
+    put("filtered", true);
+    put("sorted", true);
+    put("updatedSince", true);
+  }
+
+  /**
+   * Create a paginated collection response.
+   *
+   * @param list paginated entries
+   * @param startIndex the index corresponding to the first element of {entry}
+   * @param totalResults the total size of the resultset
+   * @param itemsPerPage the size of the pagination, generally set to the user-specified count parameter. Clamped to the totalResults size automatically
+   *
+   * @since 1.1-BETA4
+   */
+  public RestfulCollection(List<T> list, int startIndex, int totalResults, int itemsPerPage) {
+    this.list = list;
+    this.startIndex = startIndex;
+    this.totalResults = totalResults;
+    this.itemsPerPage = Math.min(itemsPerPage, totalResults);
+    put("list", list);
+    put("startIndex", startIndex);
+    put("totalResults", totalResults);
+    put("itemsPerPage", this.itemsPerPage);
+  }
+
+
+  /**
+   * Helper constructor for un-paged collection,
+   * Use {@link #RestfulCollection(java.util.List, int, int, int)} in paginated context
+   */
+  public RestfulCollection(List<T> entry, int startIndex, int totalResults) {
+    this(entry, startIndex, totalResults, entry.size());
+  }
+
+  public List<T> getList() {
+    return list;
+  }
+
+  public void setList(List<T> list) {
+    this.list = list;
+    put("list", list);
+  }
+
+  public int getStartIndex() {
+    return startIndex;
+  }
+
+  public void setStartIndex(int startIndex) {
+    this.startIndex = startIndex;
+    put("startIndex", startIndex);
+  }
+
+  public int getTotalResults() {
+    return totalResults;
+  }
+
+  public void setItemsPerPage(int itemsPerPage) {
+    this.itemsPerPage = itemsPerPage;
+    put("itemsPerPage", itemsPerPage);
+  }
+
+  public int getItemsPerPage() {
+    return itemsPerPage;
+  }
+
+  public void setTotalResults(int totalResults) {
+    this.totalResults = totalResults;
+    put("totalResults", totalResults);
+  }
+
+  public boolean isFiltered() {
+    return filtered;
+  }
+
+  public void setFiltered(boolean filtered) {
+    this.filtered = filtered;
+    put("filtered", filtered);
+  }
+
+  public boolean isSorted() {
+    return sorted;
+  }
+
+  public void setSorted(boolean sorted) {
+    this.sorted = sorted;
+    put("sorted", sorted);
+  }
+
+  public boolean isUpdatedSince() {
+    return updatedSince;
+  }
+
+  public void setUpdatedSince(boolean updatedSince) {
+    this.updatedSince = updatedSince;
+    put("updatedSince", updatedSince);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/RpcHandler.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RpcHandler.java
new file mode 100644
index 0000000..48e9f3f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/RpcHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+
+/**
+ * Interface exposed by an RPC handler
+ */
+public interface RpcHandler {
+
+  /**
+   * Handle the request and return a Future from which the response object
+   * can be retrieved.
+   */
+  Future<?> execute(Map<String, FormDataItem> formItems, SecurityToken st, BeanConverter converter);
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/Service.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/Service.java
new file mode 100644
index 0000000..7e7d1aa
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/Service.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates the base path for REST calls or the RPC service name a RequestHandler
+ * can dispatch to. Define parameter binding for REST path variables
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Service {
+  /**
+   * The name of the service this handler exports. This is also the name of the
+   * root path element of the REST endpoint. E.g. The "activities" service
+   * consumes all paths under /activities/...
+   */
+  String name();
+
+  /**
+   * The structure of the REST paths used to address this service. Paths
+   * can contain placeholders delimited by {...} that bind a named parameter or
+   * set of parameters to the request. A plural parameter is denoted by appending '+'
+   * after the named parameter. Parameters are bound in order left-to-right and
+   * missing path segments are bound to null or empty sets
+   *
+   * E.g.
+   *
+   * /{userId}+/{group}/{personId}+ will parameterize the following URLs
+   * /1/@self            => { userId : [1], group : @self, personId : []}
+   * /1/@self            => { userId : [1], group : @self, personId : []}
+   * /1,2/@friends       => { userId : [1,2], group : @friends, personId : []}
+   * /1,2/@friends/2,3   => { userId : [1,2], group : @friends, personId : [2,3]}
+   *
+   */
+  String path() default "";
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/SystemHandler.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/SystemHandler.java
new file mode 100644
index 0000000..777512b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/SystemHandler.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.apache.shindig.protocol.model.FilterOperation;
+
+import java.util.Set;
+
+
+/**
+ * Implements the 'system' service operations for JSON/XML RPC
+ */
+@Service(name = "system")
+public class SystemHandler {
+
+  HandlerRegistry registry;
+
+  public SystemHandler(HandlerRegistry registry) {
+    this.registry = registry;
+  }
+
+  @Operation(httpMethods = "GET")
+  public Set<String> listMethods(RequestItem request) {
+    if ("protocol".equalsIgnoreCase(request.getFilterBy()) &&
+        FilterOperation.equals == request.getFilterOperation() &&
+      "REST".equalsIgnoreCase(request.getFilterValue())) {
+      return registry.getSupportedRestServices();
+    }
+    return registry.getSupportedRpcServices();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanConverter.java
new file mode 100644
index 0000000..7251a31
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanConverter.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import java.io.IOException;
+
+/**
+ * Interface for bean conversion classes
+ */
+public interface BeanConverter {
+  <T> T convertToObject(String string, Class<T> className);
+
+  String convertToString(Object pojo);
+
+  /** @return the content type of the converted data */
+  String getContentType();
+
+  /**
+   * Serialize object to a buffer. Useful for high performance output.
+   * @param buf Buffer to append to
+   * @param pojo Object to serialize
+   * @throws IOException If {@link Appendable#append(char)} throws an exception.
+   */
+  void append(Appendable buf, Object pojo) throws IOException;
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java
new file mode 100644
index 0000000..bc32b72
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanDelegator.java
@@ -0,0 +1,410 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class to create a delegator (proxy) from an interface to a class.
+ * It is used by the GadgetHandler to provide easy separation from interface
+ * to actual implementation classes.
+ * It uses Java reflection which require the usage of interfaces.
+ * The validate function should be used in the test code to validate
+ * that all API functions are implemented by the actual data, and it will
+ * warn us if actual implementation change and break the API.
+ * Delegation support composition, and will create a proxy for fields according
+ * To table of classes to proxy.
+ *
+ * @since 2.0.0
+ */
+public class BeanDelegator {
+
+  /** Indicate NULL value for a field (To overcome shortcome of immutable map) */
+  public static final String NULL = "<NULL sentinel>";
+
+  /** Gate a value to use NULL constant instead of null pointer */
+  public static Object nullable(Object o) {
+    return (o != null ? o : NULL);
+  }
+
+  /**
+   * Convert field names to common name - no underscore and lower case
+   */
+  public static String normalizeName(String name) {
+    return StringUtils.remove(name, '_').toLowerCase();
+  }
+
+  /**
+   * Convert map of fields to common names and nullable values
+   */
+  public static Map<String, Object> normalizeFields(Map<String, Object> original) {
+    ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
+    if (original != null) {
+      for (Map.Entry<String, Object> entry : original.entrySet()) {
+        builder.put(normalizeName(entry.getKey()), nullable(entry.getValue()));
+      }
+    }
+    return builder.build();
+  }
+
+  /** List of Classes that are considered primitives and are not proxied **/
+  public static final ImmutableSet<Class<?>> PRIMITIVE_TYPE_CLASSES = ImmutableSet.of(
+    String.class, Integer.class, Long.class, Boolean.class, Uri.class);
+
+  /** Map from classes to proxy to the interface they are proxied by */
+  private final Map<Class<?>, Class<?>> delegatedClasses;
+
+  private final Map<Enum<?>, Enum<?>> enumConvertionMap;
+
+  public BeanDelegator() {
+    this(ImmutableMap.<Class<?>, Class<?>>of(),
+         ImmutableMap.<Enum<?>, Enum<?>>of());
+  }
+
+  public BeanDelegator(Map<Class<?>, Class<?>> delegatedClasses,
+                       Map<Enum<?>, Enum<?>> enumConvertionMap) {
+    this.delegatedClasses = delegatedClasses;
+    this.enumConvertionMap = enumConvertionMap;
+  }
+
+  /**
+   * Create a proxy for the real object.
+   * @param source item to proxy
+   * @return proxied object according to map of classes to proxy
+   */
+  public Object createDelegator(Object source) {
+    if (source == null || delegatedClasses == null) {
+      return source;
+    }
+
+    if (delegatedClasses.containsKey(source.getClass())) {
+      Class<?> apiInterface = delegatedClasses.get(source.getClass());
+
+      return createDelegator(source, apiInterface);
+    }
+    return source;
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T> T createDelegator(Object source, Class<T> apiInterface) {
+    return createDelegator(source, apiInterface, null);
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T> T createDelegator(Object source, Class<T> apiInterface,
+                               Map<String, Object> extraFields) {
+
+    extraFields = normalizeFields(extraFields);
+    if (source == null && !extraFields.isEmpty()) {
+      // Create delegator that is based only on fields, so use dummy object
+      source = new NullClass();
+    }
+
+    if (source == null) {
+      return null;
+    }
+
+    if (apiInterface.isPrimitive() || apiInterface.isAssignableFrom(source.getClass())) {
+      return (T) source;
+    }
+
+    // For enum, return the converted enum
+    if (source instanceof Enum<?> && delegatedClasses.containsKey(source.getClass())) {
+      return (T) convertEnum((Enum<?>) source);
+    }
+
+    // Proxy each item in a map (map key is not proxied)
+    if (source instanceof Map<?, ?>) {
+      Map<?, ?> mapSource = (Map<?, ?>) source;
+      if (!mapSource.isEmpty() && delegatedClasses.containsKey(
+          mapSource.values().iterator().next().getClass())) {
+        // Convert Map:
+        ImmutableMap.Builder<Object, Object> mapBuilder = ImmutableMap.builder();
+        for (Map.Entry<?, ?> entry : mapSource.entrySet()) {
+          mapBuilder.put(entry.getKey(), createDelegator(entry.getValue(), apiInterface));
+        }
+        return (T) mapBuilder.build();
+      } else {
+        return (T) source;
+      }
+    }
+
+    // Proxy each item in a map (map key is not proxied)
+    if (source instanceof Multimap<?, ?>) {
+      Multimap<?, ?> mapSource = (Multimap<?, ?>) source;
+      if (!mapSource.isEmpty() && delegatedClasses.containsKey(
+          mapSource.values().iterator().next().getClass())) {
+        // Convert Map:
+        ImmutableMultimap.Builder<Object, Object> mapBuilder = ImmutableMultimap.builder();
+        for (Map.Entry<?, ?> entry : mapSource.entries()) {
+          mapBuilder.put(entry.getKey(), createDelegator(entry.getValue(), apiInterface));
+        }
+        return (T) mapBuilder.build();
+      } else {
+        return (T) source;
+      }
+    }
+    // Proxy each item in a list
+    if (source instanceof List<?>) {
+      List<?> listSource = (List<?>) source;
+      if (!listSource.isEmpty() && delegatedClasses.containsKey(
+        listSource.get(0).getClass())) {
+        // Convert Map:
+        ImmutableList.Builder<Object> listBuilder = ImmutableList.builder();
+        for (Object entry : listSource) {
+          listBuilder.add(createDelegator(entry, apiInterface));
+        }
+        return (T) listBuilder.build();
+      } else {
+        return (T) source;
+      }
+    }
+    return (T) Proxy.newProxyInstance( apiInterface.getClassLoader(),
+      new Class[] { apiInterface }, new DelegateInvocationHandler(source, extraFields));
+  }
+
+  public Enum<?> convertEnum(Enum<?> value) {
+    if (enumConvertionMap.containsKey(value)) {
+      return enumConvertionMap.get(value);
+    }
+    throw new UnsupportedOperationException("Unknown enum value " + value.name());
+  }
+
+  protected class DelegateInvocationHandler implements InvocationHandler {
+    /** Proxied object */
+    private final Object source;
+    /** Use the next values instead of proxying source */
+    private final Map<String, Object> extraFields;
+
+    public DelegateInvocationHandler(Object source) {
+      this(source, null);
+    }
+
+    public DelegateInvocationHandler(Object source, Map<String, Object> extraFields) {
+      Preconditions.checkNotNull(source);
+
+      this.source = source;
+      this.extraFields = extraFields;
+    }
+
+    /**
+     * Proxy the interface function to the source object
+     * @throws UnsupportedOperationException if method is not supported by source
+     */
+    public Object invoke(Object proxy, Method method, Object[] args) {
+      Class<?> sourceClass = source.getClass();
+      // Return proxy fields if available
+      if (!extraFields.isEmpty() && method.getName().startsWith("get")) {
+        String field = method.getName().substring(3).toLowerCase();
+        if (extraFields.containsKey(field)) {
+          Object data = extraFields.get(field);
+          return (data == NULL ? null : data);
+        }
+      }
+      Exception exc;
+      try {
+        Method sourceMethod = sourceClass.getMethod(
+            method.getName(), method.getParameterTypes());
+        Object result = sourceMethod.invoke(source, args);
+        return createDelegator(result, getParameterizedReturnType(method));
+      } catch (NoSuchMethodException e) {
+        // Will throw unsupported method below
+        exc = e;
+      } catch (IllegalArgumentException e) {
+        // Will throw unsupported method below
+        exc = e;
+      } catch (IllegalAccessException e) {
+        // Will throw unsupported method below
+        exc = e;
+      } catch (InvocationTargetException e) {
+        // Will throw unsupported method below
+        exc = e;
+      }
+      throw new UnsupportedOperationException("Unsupported function: " + method.getName(), exc);
+    }
+  }
+
+  private Class<?> getParameterizedReturnType(Method method) {
+    Type type = method.getGenericReturnType();
+    if (type instanceof ParameterizedType) {
+      ParameterizedType paramType = (ParameterizedType) type;
+
+      if (List.class.isAssignableFrom((Class<?>) paramType.getRawType())) {
+        type = paramType.getActualTypeArguments()[0];
+      } else if (Map.class.isAssignableFrom((Class<?>) paramType.getRawType())) {
+        type = paramType.getActualTypeArguments()[1];
+      } else if (Multimap.class.isAssignableFrom((Class<?>) paramType.getRawType())) {
+        type = paramType.getActualTypeArguments()[1];
+      }
+    }
+    return (Class<?>) type;
+  }
+
+
+  /**
+   * Validate all proxied classes to see that all required functions are implemented.
+   * Throws exception if failed validation.
+   * Note that it ignore the extra fields support.
+   * @throws SecurityException
+   * @throws NoSuchMethodException
+   * @throws NoSuchFieldException
+   */
+  public void validate() throws SecurityException, NoSuchMethodException, NoSuchFieldException {
+    for (Map.Entry<Class<?>, Class<?>> entry : delegatedClasses.entrySet()) {
+      if (!entry.getKey().isEnum()) {
+        validate(entry.getKey(), entry.getValue());
+      }
+    }
+  }
+
+  public void validate(Class<?> dataClass, Class<?> interfaceClass)
+      throws SecurityException, NoSuchMethodException, NoSuchFieldException {
+    for (Method method : interfaceClass.getMethods()) {
+      Method dataMethod = dataClass.getMethod(method.getName(), method.getParameterTypes());
+      if (dataMethod == null) {
+        throw new NoSuchMethodException("Method " + method.getName()
+            + " is not implemented by " + dataClass.getName());
+      }
+      if (!validateTypes(dataMethod.getGenericReturnType(), method.getGenericReturnType())) {
+        throw new NoSuchMethodException("Method " + method.getName()
+          + " has wrong return type by " + dataClass.getName());
+      }
+    }
+  }
+
+  private boolean validateTypes(Type dataType, Type interfaceType)
+      throws NoSuchFieldException {
+
+    // Handle Map and List parameterized types
+    if (dataType instanceof ParameterizedType) {
+      ParameterizedType dataParamType = (ParameterizedType) dataType;
+      ParameterizedType interfaceParamType = (ParameterizedType) interfaceType;
+
+      if (List.class.isAssignableFrom((Class<?>) dataParamType.getRawType()) &&
+          List.class.isAssignableFrom((Class<?>) interfaceParamType.getRawType())) {
+
+        dataType = dataParamType.getActualTypeArguments()[0];
+        interfaceType = interfaceParamType.getActualTypeArguments()[0];
+        return validateTypes(dataType, interfaceType);
+      }
+      if (Map.class.isAssignableFrom((Class<?>) dataParamType.getRawType()) &&
+          Map.class.isAssignableFrom((Class<?>) interfaceParamType.getRawType())) {
+        Type dataKeyType = dataParamType.getActualTypeArguments()[0];
+        Type interfaceKeyType = interfaceParamType.getActualTypeArguments()[0];
+        if (dataKeyType != interfaceKeyType || !PRIMITIVE_TYPE_CLASSES.contains(dataKeyType)) {
+          return false;
+        }
+        dataType = dataParamType.getActualTypeArguments()[1];
+        interfaceType = interfaceParamType.getActualTypeArguments()[1];
+        return validateTypes(dataType, interfaceType);
+      }
+
+      if (Multimap.class.isAssignableFrom((Class<?>) dataParamType.getRawType()) &&
+          Multimap.class.isAssignableFrom((Class<?>) interfaceParamType.getRawType())) {
+        Type dataKeyType = dataParamType.getActualTypeArguments()[0];
+        Type interfaceKeyType = interfaceParamType.getActualTypeArguments()[0];
+        if (dataKeyType != interfaceKeyType || !PRIMITIVE_TYPE_CLASSES.contains(dataKeyType)) {
+          return false;
+        }
+        dataType = dataParamType.getActualTypeArguments()[1];
+        interfaceType = interfaceParamType.getActualTypeArguments()[1];
+        return validateTypes(dataType, interfaceType);
+      }
+      // Only support Multimap, Map and List generics
+      return false;
+    }
+
+    // Primitive types
+    if (dataType == interfaceType) {
+        return !(!PRIMITIVE_TYPE_CLASSES.contains(dataType) && !((Class<?>) dataType).isPrimitive());
+    }
+
+    // Check all enum values are accounted for
+    Class<?> dataClass = (Class<?>)dataType;
+    if (dataClass.isEnum()) {
+      for (Object f : dataClass.getEnumConstants()) {
+        if (!enumConvertionMap.containsKey(f) ||
+            enumConvertionMap.get(f).getClass() != interfaceType) {
+          throw new NoSuchFieldException("Enum " + dataClass.getName()
+            + " don't have mapping for value " + f.toString());
+        }
+      }
+    }
+    return (delegatedClasses.get(dataType) == interfaceType);
+  }
+
+  /**
+   * Validate a delegator object has all fields defined.
+   * With the field list option, classes can be delegated without being complete.
+   * This helper method should be used to verify that no field was missed.
+   */
+  public static void validateDelegator(Object o)
+      throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {
+    for (Method method : o.getClass().getInterfaces()[0].getMethods()) {
+      if (method.getName().startsWith("get")) {
+        Object val = method.invoke(o);
+      }
+    }
+  }
+
+  /**
+   * Utility function to auto generate mapping between two enums that have same values (name)
+   * All values in the sourceEnum must have values in targetEnum,
+   *  otherwise {@link RuntimeException} is thrown
+   */
+  public static Map<Enum<?>, Enum<?>> createDefaultEnumMap(
+      Class<? extends Enum<?>> sourceEnum, Class<? extends Enum<?>> targetEnum) {
+   Map<String, Enum<?>> values2Map = Maps.newHashMap();
+   for (Enum<?> val2 : targetEnum.getEnumConstants()) {
+     values2Map.put(val2.name(), val2);
+   }
+
+   ImmutableMap.Builder<Enum<?>, Enum<?>> mapBuilder = ImmutableMap.builder();
+   for (Enum<?> val1 : sourceEnum.getEnumConstants()) {
+     if (values2Map.containsKey(val1.name())) {
+       mapBuilder.put(val1, values2Map.get(val1.name()));
+     } else {
+       throw new RuntimeException("Missing enum value " + val1.name()
+           + " for enum " + targetEnum.getName());
+     }
+   }
+   return mapBuilder.build();
+  }
+
+  /** Fake class that does not have fields or method for field base delegator */
+  public static class NullClass {}
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java
new file mode 100644
index 0000000..cdf05b0
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanFilter.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Filter content of a bean according to fields list.
+ * Fields list should be in lower case. And support sub objects using dot notation.
+ * For example to get only the "name" field of the object in the "view" field,
+ * specify "view.name" (and also specify "view" to get the view itself).
+ * Use "*" to get all fields, or "view.*" all sub fields of view (see tests).
+ * Note that specifying "view" does NOT imply "view.*" and that
+ * specifying "view.*" require specifying "view" in order to get the view itself.
+ * (Note that the processBeanFilter resolve the last limitation)
+ *
+ * Note this code create a new object for each filtered object.
+ * Filtering can be done also using cglib.InterfaceMaker and reflect.Proxy.makeProxyInstance
+ * That results with an object that have same finger print as source, but cannot be cast to it.
+ *
+ * @since 2.0.0
+ */
+public class BeanFilter {
+
+  public static final String ALL_FIELDS = "*";
+  public static final String DELIMITER = ".";
+
+  /** Annotation for required field that should not be filtered */
+  @Target(ElementType.METHOD)
+  @Retention(RetentionPolicy.RUNTIME)
+  public @interface Unfiltered {}
+
+  /**
+   * Create a proxy object that filter object fields according to set of fields.
+   * If a field is not specified in the set, the get method will return null.
+   * (Primitive returned type cannot be filtered)
+   * The filter is done recursively on sub items.
+   * @param data the object to filter
+   * @param fields list of fields to pass through.
+   */
+  public Object createFilteredBean(Object data, Set<String> fields) {
+    return createFilteredBean(data, fields, "");
+  }
+
+  @SuppressWarnings("unchecked")
+  private Object createFilteredBean(Object data, Set<String> fields, String fieldName) {
+    // For null, atomic object or for all fields just return original.
+    if (data == null || fields == null
+        || BeanDelegator.PRIMITIVE_TYPE_CLASSES.contains(data.getClass())
+        || fields.contains(ALL_FIELDS)) {
+      return data;
+    }
+
+    // For map, generate a new map with filtered objects
+    if (data instanceof Map<? ,?>) {
+      Map<Object, Object> oldMap = (Map<Object, Object>) data;
+      Map<Object, Object> newMap = Maps.newHashMapWithExpectedSize(oldMap.size());
+      for (Map.Entry<Object, Object> entry : oldMap.entrySet()) {
+        newMap.put(entry.getKey(), createFilteredBean(entry.getValue(), fields, fieldName));
+      }
+      return newMap;
+    }
+
+    // For list, generate a new list of filtered objects
+    if (data instanceof List<?>) {
+      List<Object> oldList = (List<Object>) data;
+      List<Object> newList = Lists.newArrayListWithCapacity(oldList.size());
+      for (Object entry : oldList) {
+        newList.add(createFilteredBean(entry, fields, fieldName));
+      }
+      return newList;
+    }
+
+    // Create a new intercepted object:
+    return Proxy.newProxyInstance( data.getClass().getClassLoader(),
+        data.getClass().getInterfaces(), new FilterInvocationHandler(data, fields, fieldName));
+  }
+
+  /**
+   * Invocation handler to filter fields. It return null to fields that are not in the list.
+   * It invokes method on original object. It does not filter primitive types.
+   * And it create bean filter proxy for return objects
+   */
+  private class FilterInvocationHandler implements InvocationHandler {
+    private final String prefix;
+    private final Set<String> fields;
+    private final Object origData;
+
+    FilterInvocationHandler(Object origData, Set<String> fields, String fieldName) {
+      this.fields = fields;
+      this.prefix = Strings.isNullOrEmpty(fieldName) ? "" : fieldName + DELIMITER;
+      this.origData = origData;
+    }
+
+    public Object invoke(Object data, Method method, Object[] args) {
+      String fieldName = null;
+      Object result;
+      if (method.getName().startsWith("get")
+          // Do not filter out primitive types, it will result in NPE
+          && !method.getReturnType().isPrimitive()) {
+        // Look for Required annotation
+        boolean required = (method.getAnnotation(Unfiltered.class) != null);
+        fieldName = prefix + method.getName().substring(3).toLowerCase();
+        if (!required && !fields.contains(fieldName)) {
+          return null;
+        }
+      }
+      try {
+        result = method.invoke(origData, args);
+      } catch (IllegalArgumentException e) {
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      } catch (InvocationTargetException e) {
+        throw new RuntimeException(e);
+      }
+      if (result != null && fieldName != null
+          // if the request ask for all fields, we don't need to filter them
+          && !fields.contains(fieldName + DELIMITER + ALL_FIELDS)) {
+        return createFilteredBean(result, fields, fieldName);
+        // TODO: Consider improving the above by saving the filtered bean in a local map for reuse
+        // for current use the get is called once, so it would actually create overhead
+      }
+      return result;
+    }
+  }
+
+  public Set<String> processBeanFields(Collection<String> fields) {
+    ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+    for (String field : fields) {
+      builder.add(field.toLowerCase());
+      while (field.contains(DELIMITER)) {
+        field = field.substring(0, field.lastIndexOf(DELIMITER));
+        builder.add(field.toLowerCase());
+      }
+    }
+    return builder.build();
+  }
+
+  /**
+   * Provide list of all fields for a specific bean
+   * @param bean the class to list fields for
+   * @param depth maximum depth of recursive (mainly for infinite loop protection)
+   */
+  public List<String> getBeanFields(Class<?> bean, int depth) {
+    List<String> fields = Lists.newLinkedList();
+    for (Method method : bean.getMethods()) {
+      if (method.getName().startsWith("get")) {
+        String fieldName = method.getName().substring(3);
+        fields.add(fieldName);
+        Class<?> returnType = method.getReturnType();
+        // Get the type of list:
+        if (List.class.isAssignableFrom(returnType)) {
+          ParameterizedType aType = (ParameterizedType) method.getGenericReturnType();
+          Type[] parameterArgTypes = aType.getActualTypeArguments();
+          if (parameterArgTypes.length > 0) {
+            returnType = (Class<?>) parameterArgTypes[0];
+          } else {
+            returnType = null;
+          }
+        }
+        // Get the type of map value
+        if (Map.class.isAssignableFrom(returnType)) {
+          ParameterizedType aType = (ParameterizedType) method.getGenericReturnType();
+          Type[] parameterArgTypes = aType.getActualTypeArguments();
+          if (parameterArgTypes.length > 1) {
+            returnType = (Class<?>) parameterArgTypes[1];
+          } else {
+            returnType = null;
+          }
+        }
+        // Get member fields and append fields using dot notation
+        if (depth > 1 && returnType != null && !returnType.isPrimitive()
+            && !returnType.isEnum()
+            && !BeanDelegator.PRIMITIVE_TYPE_CLASSES.contains(returnType)) {
+          List<String> subFields = getBeanFields(returnType, depth - 1);
+          for (String field : subFields) {
+            fields.add(fieldName + DELIMITER + field);
+          }
+        }
+      }
+    }
+    return fields;
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanJsonConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanJsonConverter.java
new file mode 100644
index 0000000..1d5717a
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanJsonConverter.java
@@ -0,0 +1,290 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import org.apache.shindig.common.JsonProperty;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.protocol.ContentTypes;
+import org.apache.shindig.protocol.model.Enum;
+import org.apache.shindig.protocol.model.EnumImpl;
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.joda.time.DateTime;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+/**
+ * Converts between JSON and java objects.
+ *
+ * TODO: Eliminate BeanConverter interface.
+ */
+public class BeanJsonConverter implements BeanConverter {
+
+  // Only compute the filtered SETTERS once per-class
+  private static final LoadingCache<Class<?>, Map<String, Method>> SETTERS = CacheBuilder
+      .newBuilder()
+      .build(new CacheLoader<Class<?>, Map<String, Method>>() {
+        public Map<String, Method> load(Class<?> type) {
+          ImmutableMap.Builder<String, Method> builder = ImmutableMap.builder();
+          for (Method method : type.getMethods()) {
+            if (method.getParameterTypes().length == 1) {
+              String name = getPropertyName(method);
+              if (name != null) {
+                builder.put(name, method);
+              }
+            }
+          }
+          return builder.build();
+        }
+      });
+
+  private final Injector injector;
+
+  @Inject
+  public BeanJsonConverter(Injector injector) {
+    this.injector = injector;
+  }
+
+  public String getContentType() {
+    return ContentTypes.OUTPUT_JSON_CONTENT_TYPE;
+  }
+
+  /**
+   * Convert the passed in object to a string.
+   *
+   * @param pojo The object to convert
+   * @return An object whose toString method will return json
+   */
+  public String convertToString(final Object pojo) {
+    return JsonSerializer.serialize(pojo);
+  }
+
+  public void append(Appendable buf, Object pojo) throws IOException {
+    JsonSerializer.append(buf, pojo);
+  }
+
+  private static String getPropertyName(Method setter) {
+    JsonProperty property = setter.getAnnotation(JsonProperty.class);
+    if (property == null) {
+      String name = setter.getName();
+      if (name.startsWith("set") && !Modifier.isStatic(setter.getModifiers())) {
+        return name.substring(3, 4).toLowerCase() + name.substring(4);
+      }
+      return null;
+    } else {
+      return property.value();
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  // Class.cast() would be better - but the Class object may be null
+  public <T> T convertToObject(String string, Class<T> clazz) {
+    return (T)convertToObject(string, (Type) clazz);
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T> T convertToObject(String json, Type type) {
+    try {
+      return (T) convertToObject(new JSONObject(json), type);
+    } catch (JSONException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public Object convertToObject(Object value, Type type) {
+    if (type == null || type.equals(Object.class)) {
+      // Use the source type instead.
+      if (value instanceof JSONObject) {
+        return convertToMap((JSONObject) value, null);
+      } else if (value instanceof JSONArray) {
+        return convertToList((JSONArray) value, null);
+      }
+      return value;
+    } else if (type instanceof ParameterizedType) {
+      return convertGeneric(value, (ParameterizedType) type);
+    } else if (type.equals(String.class)) {
+      return String.valueOf(value);
+    } else if (type.equals(Boolean.class) || type.equals(Boolean.TYPE)) {
+      return value instanceof String ? Boolean.valueOf((String) value) : Boolean.TRUE.equals(value);
+    } else if (type.equals(Integer.class) || type.equals(Integer.TYPE)) {
+      return value instanceof String ? Integer.valueOf((String) value) : ((Number) value).intValue();
+    } else if (type.equals(Long.class) || type.equals(Long.TYPE)) {
+      return value instanceof String ? Long.valueOf((String) value) : ((Number) value).longValue();
+    } else if (type.equals(Double.class) || type.equals(Double.TYPE)) {
+      return value instanceof String ? Double.valueOf((String) value) : ((Number) value).doubleValue();
+    } else if (type.equals(Float.class) || type.equals(Float.TYPE)) {
+      return value instanceof String ? Float.valueOf((String) value) : ((Number) value).floatValue();
+    } else if (type.equals(Date.class)) {
+      return new DateTime(String.valueOf(value)).toDate();
+    } else if (type.equals(Uri.class)) {
+      return Uri.parse(String.valueOf(value));
+    } else if (type.equals(Map.class)) {
+      return convertToMap((JSONObject) value, null);
+    } else if (type.equals(List.class) || type.equals(Collection.class)) {
+      return convertToList((JSONArray) value, null);
+    } else if (type.equals(Set.class)) {
+      return convertToSet((JSONArray) value, null);
+    }
+
+    Class<?> clazz = (Class<?>) type;
+
+    if (clazz.isEnum()) {
+      return convertToEnum((String) value, clazz);
+    }
+
+    return convertToClass((JSONObject) value, clazz);
+  }
+
+  private Object convertGeneric(Object value, ParameterizedType type) {
+    Type[] typeArgs = type.getActualTypeArguments();
+    Class<?> clazz = (Class<?>) type.getRawType();
+
+    if (Set.class.isAssignableFrom(clazz)) {
+      return convertToSet((JSONArray) value, typeArgs[0]);
+    } else if (Collection.class.isAssignableFrom(clazz)) {
+      return convertToList((JSONArray) value, typeArgs[0]);
+    } else if (Map.class.isAssignableFrom(clazz)) {
+      return convertToMap((JSONObject) value, typeArgs[1]);
+    } else if (org.apache.shindig.protocol.model.Enum.class.isAssignableFrom(clazz)) {
+      // Special case for opensocial Enum objects. These really need to be refactored to not require
+      // this handling.
+      return convertToOsEnum((JSONObject) value, (Class<?>) typeArgs[0]);
+    }
+    return convertToClass((JSONObject) value, clazz);
+  }
+
+  private Enum<Enum.EnumKey> convertToOsEnum(JSONObject json, Class<?> enumKeyType) {
+    Enum<Enum.EnumKey> value;
+    String val = Enum.Field.VALUE.toString();
+    String display = Enum.Field.DISPLAY_VALUE.toString();
+    if (json.has(val)) {
+      Enum.EnumKey enumKey;
+      try {
+        enumKey = (Enum.EnumKey) enumKeyType.getField(json.optString(val)).get(null);
+      } catch (IllegalArgumentException e) {
+        throw new RuntimeException(e);
+      } catch (SecurityException e) {
+        throw new RuntimeException(e);
+      } catch (IllegalAccessException e) {
+        throw new RuntimeException(e);
+      } catch (NoSuchFieldException e) {
+        throw new RuntimeException(e);
+      }
+      String displayValue = null;
+      if (json.has(display)) {
+        displayValue = json.optString(display);
+      }
+      value = new EnumImpl<Enum.EnumKey>(enumKey,displayValue);
+    } else {
+      value = new EnumImpl<Enum.EnumKey>(null, json.optString(display));
+    }
+    return value;
+  }
+
+  private Object convertToEnum(String value, Class<?> type) {
+    for (Object o : type.getEnumConstants()) {
+      if (o.toString().equals(value)) {
+        return o;
+      }
+    }
+    throw new IllegalArgumentException("No enum value " + value + " in " + type.getName());
+  }
+
+  private Map<String, Object> convertToMap(JSONObject in, Type type) {
+    Map<String, Object> out = new HashMap<String, Object>(in.length(), 1);
+    if(in.length() == 0)
+      return Collections.emptyMap();
+
+    for (String name : JSONObject.getNames(in)) {
+      out.put(name, convertToObject(in.opt(name), type));
+    }
+    return out;
+  }
+
+  private List<Object> convertToList(JSONArray in, Type type) {
+    ArrayList<Object> out = Lists.newArrayListWithCapacity(in.length());
+
+    for (int i = 0, j = in.length(); i < j; ++i) {
+      out.add(convertToObject(in.opt(i), type));
+    }
+    return out;
+  }
+
+  private Set<Object> convertToSet(JSONArray in, Type type) {
+    return ImmutableSet.copyOf(convertToList(in, type));
+  }
+
+  private Object convertToClass(JSONObject in, Class<?> type) {
+    Object out = injector.getInstance(type);
+
+    /*
+     * Simple hack to add support for arbitrary extensions to Shindig's data
+     * model.  It initializes keys/values of an ExtendableBean class, which is
+     * a Map under the covers.  If a class implements ExtendableBean.java, it
+     * will support arbitrary mappings to JSON & XML.
+     */
+    if (ExtendableBean.class.isAssignableFrom(type)) {
+      for (String name : JSONObject.getNames(in)) {
+        ((ExtendableBean) out).put(name, convertToObject(in.opt(name), null));
+      }
+    }
+
+    for (Map.Entry<String, Method> entry : SETTERS.getUnchecked(out.getClass()).entrySet()) {
+      Object value = in.opt(entry.getKey());
+      if (value != null) {
+        Method method = entry.getValue();
+        try {
+          method.invoke(out, convertToObject(value, method.getGenericParameterTypes()[0]));
+        } catch (IllegalArgumentException e) {
+          throw new RuntimeException(e);
+        } catch (IllegalAccessException e) {
+          throw new RuntimeException(e);
+        } catch (InvocationTargetException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    }
+    return out;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanXStreamConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanXStreamConverter.java
new file mode 100644
index 0000000..abe692e
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/BeanXStreamConverter.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import org.apache.shindig.protocol.ContentTypes;
+import org.apache.shindig.protocol.DataCollection;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.conversion.xstream.StackDriver;
+import org.apache.shindig.protocol.conversion.xstream.ThreadSafeWriterStack;
+import org.apache.shindig.protocol.conversion.xstream.WriterStack;
+import org.apache.shindig.protocol.conversion.xstream.XStreamConfiguration;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
+import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
+import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
+import com.thoughtworks.xstream.io.xml.XppDriver;
+import com.thoughtworks.xstream.mapper.DefaultMapper;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+/**
+ * Converts to/from XML format using XStream
+ */
+public class BeanXStreamConverter implements BeanConverter {
+  public static final String XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
+  private static final XStreamConfiguration.ConverterSet[] MAPPER_SCOPES = {
+      XStreamConfiguration.ConverterSet.MAP,
+      XStreamConfiguration.ConverterSet.COLLECTION,
+      XStreamConfiguration.ConverterSet.DEFAULT };
+  private static final Logger LOG = Logger.getLogger(BeanXStreamConverter.class.getName());
+
+  protected WriterStack writerStack;
+
+
+  protected Map<XStreamConfiguration.ConverterSet, XStreamConfiguration.ConverterConfig> converterMap = Maps.newHashMap();
+
+  @Inject
+  public BeanXStreamConverter(XStreamConfiguration configuration) {
+    ReflectionProvider rp = new PureJavaReflectionProvider();
+    Mapper dmapper = new DefaultMapper(this.getClass().getClassLoader());
+    /*
+     * Putting this here means only one conversion per thread may be active at
+     * any one time, but since the conversion process is atomic this will not
+     * matter unless the class is extended.
+     */
+    writerStack = new ThreadSafeWriterStack();
+
+
+    /*
+     * create a driver that wires into a standard driver, and updates the stack
+     * position.
+     */
+    HierarchicalStreamDriver driver = new StackDriver(new XppDriver(), writerStack, configuration.getNameSpaces());
+    /*
+     * Create an interface class mapper that understands class hierarchy for
+     * single items
+     */
+    for (XStreamConfiguration.ConverterSet c : MAPPER_SCOPES) {
+      converterMap.put(c, configuration.getConverterConfig(c, rp,dmapper, driver,writerStack));
+    }
+  }
+
+  public String getContentType() {
+    return ContentTypes.OUTPUT_XML_CONTENT_TYPE;
+  }
+
+  public String convertToString(Object pojo) {
+    return convertToXml(pojo);
+  }
+
+  /**
+   * convert an Object to XML, but make certain that only one of these is run on
+   * a thread at any one time. This only matters if this class is extended.
+   *
+   * @param obj
+   * @return The XML as a string
+   */
+  private String convertToXml(Object obj) {
+
+    writerStack.reset();
+    if (obj instanceof RestfulCollection) {
+      XStreamConfiguration.ConverterConfig cc = converterMap
+          .get(XStreamConfiguration.ConverterSet.COLLECTION);
+      cc.mapper.setBaseObject(obj); // thread safe method
+      String result = cc.xstream.toXML(obj);
+
+      if (LOG.isLoggable(Level.FINE))
+        LOG.fine("Result is " + result);
+
+      return XML_DECL + result;
+    } else if (obj instanceof Map<?, ?>) {
+      Map<?, ?> m = (Map<?, ?>) obj;
+      XStreamConfiguration.ConverterConfig cc = converterMap
+          .get(XStreamConfiguration.ConverterSet.MAP);
+      if (m.size() == 1) {
+        Object s = m.values().iterator().next();
+        cc.mapper.setBaseObject(s); // thread safe method
+        String result = cc.xstream.toXML(s);
+
+        if (LOG.isLoggable(Level.FINE))
+          LOG.fine("Result is " + result);
+
+        return XML_DECL + "<response xmlns=\"http://ns.opensocial.org/2008/opensocial\">" + result + "</response>";
+      }
+    } else if (obj instanceof DataCollection) {
+      XStreamConfiguration.ConverterConfig cc = converterMap
+          .get(XStreamConfiguration.ConverterSet.MAP);
+      cc.mapper.setBaseObject(obj); // thread safe method
+      String result = cc.xstream.toXML(obj);
+
+      if (LOG.isLoggable(Level.FINE))
+        LOG.fine("Result is " + result);
+
+      return XML_DECL + result;
+    }
+    XStreamConfiguration.ConverterConfig cc = converterMap
+        .get(XStreamConfiguration.ConverterSet.DEFAULT);
+
+    cc.mapper.setBaseObject(obj); // thread safe method
+    String result = cc.xstream.toXML(obj);
+
+    if (LOG.isLoggable(Level.FINE))
+      LOG.fine("Result is " + result);
+    return XML_DECL + "<response xmlns=\"http://ns.opensocial.org/2008/opensocial\">" + result + "</response>";
+  }
+
+  @SuppressWarnings("unchecked")
+  public <T> T convertToObject(String xml, Class<T> className) {
+    XStreamConfiguration.ConverterConfig cc = converterMap.get(XStreamConfiguration.ConverterSet.DEFAULT);
+    return (T) cc.xstream.fromXML(xml);
+  }
+
+  public void append(Appendable buf, Object pojo) throws IOException {
+    buf.append(convertToString(pojo));
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ClassFieldMapping.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ClassFieldMapping.java
new file mode 100644
index 0000000..41fc02e
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ClassFieldMapping.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+/**
+ * This represents the mapping between a class and a field, potentially with a
+ * parent element. It is used to define the element names that are used to
+ * serialize the contents of a class.
+ *
+ * eg
+ * <pre>
+ * &lt;outerobject&gt;
+ * &lt;listcontainer&gt;
+ *    &lt;listelement&gt;
+ *       &lt;objectcontent&gt;
+ *    &lt;/listelement&gt;
+ * &lt;/listcontainer&gt;
+ * ...
+ * &lt;/outerobject&gt;
+ * </pre>
+ * or (not currently used in OS)
+ * <pre>
+ * &lt;person&gt;
+ *    &lt;emails&gt;
+ *       &lt;email&gt;
+ *          &lt;type&gt;&lt;/type&gt;
+ *          &lt;value&gt;&lt;/value&gt;
+ *       &lt;/email&gt;
+ *       ...
+ *    &lt;/emails&gt;
+ *    ...
+ * &lt;/person&gt;
+ * </pre>
+ *
+ */
+public class ClassFieldMapping {
+
+  /**
+   * The name of the element to map the class to.
+   */
+  private String elementName;
+
+  /**
+   * The class being mapped.
+   */
+  private Class<?> mappedClazz;
+  /**
+   * An optional parent element name.
+   */
+  private String fieldParentName;
+
+  /**
+   * Create a simple element class mapping, applicable to all parent elements.
+   *
+   * @param elementName
+   *          the name of the element
+   * @param mappedClazz
+   *          the class to map to the name of the element
+   */
+  public ClassFieldMapping(String elementName, Class<?> mappedClazz) {
+    this.elementName = elementName;
+    this.mappedClazz = mappedClazz;
+    this.fieldParentName = null;
+  }
+
+  /**
+   * Create a element class mapping, that only applies to one parent element
+   * name.
+   *
+   * @param parentName
+   *          the name of the parent element that this mapping applies to
+   * @param elementName
+   *          the name of the element
+   * @param mappedClazz
+   *          the class to map to the name of the element
+   */
+  public ClassFieldMapping(String parentName, String elementName, Class<?> mappedClazz) {
+    this.elementName = elementName;
+    this.mappedClazz = mappedClazz;
+    this.fieldParentName = parentName;
+  }
+
+  /**
+   * @return get the element name.
+   */
+  public String getElementName() {
+    return elementName;
+  }
+
+  /**
+   * @return get the mapped class.
+   */
+  public Class<?> getMappedClass() {
+    return mappedClazz;
+  }
+
+  /**
+   * Does this ClassFieldMapping match the supplied parent and type.
+   *
+   * @param parent
+   *          the parent element, which may be null
+   * @param type
+   *          the type of the field being stored
+   * @return true if this mapping is a match for the combination
+   */
+  public boolean matches(String parent, Class<?> type) {
+    if (fieldParentName == null) {
+      return mappedClazz.isAssignableFrom(type);
+    }
+    return fieldParentName.equals(parent)
+        && mappedClazz.isAssignableFrom(type);
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/DataCollectionConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/DataCollectionConverter.java
new file mode 100644
index 0000000..b576918
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/DataCollectionConverter.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import org.apache.shindig.protocol.DataCollection;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * This converter changes the way in which a collection is serialized
+ */
+public class DataCollectionConverter extends AbstractCollectionConverter {
+
+  /**
+   * @param mapper
+   */
+  public DataCollectionConverter(Mapper mapper) {
+    super(mapper);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter#canConvert(java.lang.Class)
+   */
+  @Override
+  // Base API is inherently unchecked
+  @SuppressWarnings("unchecked")
+  public boolean canConvert(Class clazz) {
+    return DataCollection.class.isAssignableFrom(clazz);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter#marshal(java.lang.Object,
+   *      com.thoughtworks.xstream.io.HierarchicalStreamWriter,
+   *      com.thoughtworks.xstream.converters.MarshallingContext)
+   */
+  @Override
+  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+
+    DataCollection collection = (DataCollection) source;
+    Map<String, Map<String, String>> internalMap = collection.getEntry();
+
+    for (Entry<String, Map<String, String>> eo : internalMap.entrySet()) {
+      writer.startNode("entry");
+      writer.startNode("key");
+      writer.setValue(eo.getKey());
+      writer.endNode();
+      writer.startNode("value");
+      for (Entry<String, String> ei : eo.getValue().entrySet()) {
+        writer.startNode("entry");
+        writer.startNode("key");
+        writer.setValue(ei.getKey());
+        writer.endNode();
+        writer.startNode("value");
+        writer.setValue(ei.getValue());
+        writer.endNode();
+        writer.endNode();
+      }
+
+      writer.endNode();
+      writer.endNode();
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader,
+   *      com.thoughtworks.xstream.converters.UnmarshallingContext)
+   */
+  @SuppressWarnings("unchecked")
+  @Override
+  public Object unmarshal(HierarchicalStreamReader reader,
+      UnmarshallingContext context) {
+    Preconditions.checkNotNull(reader);
+    reader.moveDown();
+    Map<String, Object> m = Maps.newHashMap();
+    while (reader.hasMoreChildren()) {
+      reader.moveDown(); // entry
+      String ok = null;
+      Object ov = null;
+      while (reader.hasMoreChildren()) {
+        reader.moveDown(); // key or value
+        String elname = reader.getNodeName();
+        if ("key".equals(elname)) {
+          ok = reader.getValue();
+        } else if ("value".equals(elname)) {
+          ov = reader.getValue();
+          if (reader.hasMoreChildren()) {
+            Map<String, String> innerMap = Maps.newHashMap();
+            while (reader.hasMoreChildren()) {
+              reader.moveDown();// entry
+              String k = null;
+              String v = null;
+              while (reader.hasMoreChildren()) {
+                reader.moveDown(); // key or value
+                if ("key".equals(elname)) {
+                  k = reader.getValue();
+                } else if ("value".equals(elname)) {
+                  v = reader.getValue();
+                }
+                reader.moveUp();
+              }
+              innerMap.put(k, v);
+              reader.moveUp();
+            }
+            ov = innerMap;
+          } else {
+          }
+        }
+        reader.moveUp();
+      }
+      reader.moveUp();
+      m.put(ok, ov);
+    }
+    reader.moveUp();
+    // scan the map, if there are any maps, then everything should be in maps.
+    boolean nonmap = false;
+    for (Entry<String, Object> e : m.entrySet()) {
+      if (e.getValue() instanceof String) {
+        nonmap = true;
+      }
+    }
+    Map<String, Map<String, String>> fm = Maps.newHashMap();
+    if (nonmap) {
+      for (Entry<String, Object> e : m.entrySet()) {
+        if (e.getValue() instanceof Map) {
+          fm.put(e.getKey(), (Map<String, String>) e.getValue());
+        } else {
+          // not certain that this makes sense, but can't see how else.
+          Map<String, String> mv = Maps.newHashMap();
+          mv.put(e.getKey(), (String) e.getValue());
+          fm.put(e.getKey(), mv);
+        }
+      }
+
+    } else {
+      for (Entry<String, Object> e : m.entrySet()) {
+        fm.put(e.getKey(), (Map<String, String>) e.getValue());
+      }
+    }
+    return new DataCollection(fm);
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ExtendableBeanConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ExtendableBeanConverter.java
new file mode 100644
index 0000000..c6422aa
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ExtendableBeanConverter.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.shindig.protocol.model.ExtendableBeanImpl;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+/**
+ * Serializes an ExtendableBeanImpl type as a Map instead of a POJO.
+ *
+ * Note: For an ExtendableBean field within a POJO to be properly serialized to
+ * XML, the field must still be hardcoded into the POJO, even if the POJO itself
+ * is an ExtendableBean. This is because POJOs are still serialized to XML
+ * normally to preserve custom formatting (hence why this class ONLY converts
+ * ExtendableBeanImpl). If xstream does not expect the ExtendableBean field, it
+ * will not serialize it. This does not, however, affect JSON serialization,
+ * which always works without predefining the field when extending
+ * ExtendableBean.
+ */
+public class ExtendableBeanConverter implements Converter {
+
+	@SuppressWarnings("rawtypes")
+	public boolean canConvert(Class clazz) {
+		return clazz.equals(ExtendableBeanImpl.class);
+	}
+
+	@SuppressWarnings("rawtypes")
+	public void marshal(Object value, HierarchicalStreamWriter writer,
+			MarshallingContext context) {
+		AbstractMap map = (AbstractMap) value;
+		for (Object obj : map.entrySet()) {
+			Entry entry = (Entry) obj;
+			writer.startNode(entry.getKey().toString());
+			if (entry.getValue() instanceof String) {
+				writer.setValue(entry.getValue().toString());
+			} else if (entry.getValue() instanceof Map) {
+				marshal(entry.getValue(), writer, context);
+			} else {
+				context.convertAnother(entry.getValue());
+			}
+			writer.endNode();
+		}
+	}
+
+	public Object unmarshal(HierarchicalStreamReader reader,
+			UnmarshallingContext context) {
+		return null; // XML POST not supported
+	}
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/GuiceBeanConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/GuiceBeanConverter.java
new file mode 100644
index 0000000..cc80e14
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/GuiceBeanConverter.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+
+import com.google.inject.Injector;
+import com.thoughtworks.xstream.converters.ConversionException;
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.ExtendedHierarchicalStreamWriterHelper;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+import java.util.Collection;
+
+/**
+ * Bean converter that uses Guice bindings to correctly convert
+ */
+public class GuiceBeanConverter implements Converter {
+  private Mapper mapper;
+  private GuiceBeanProvider beanProvider;
+
+  public GuiceBeanConverter(Mapper mapper, Injector injector) {
+    this(mapper, new GuiceBeanProvider(injector));
+  }
+
+  public GuiceBeanConverter(Mapper mapper, GuiceBeanProvider beanProvider) {
+    this.mapper = mapper;
+    this.beanProvider = beanProvider;
+  }
+
+  /**
+   * Only checks for the availability of a public default constructor. If you
+   * need stricter checks, subclass JavaBeanConverter
+   */
+  // Base API is inherently unchecked
+
+  public boolean canConvert(Class type) {
+    while (true) {
+      if (type == null) {
+        return false;
+      }
+      if (Object.class.equals(type)) {
+        return false;
+      }
+      if (type.isInterface()) {
+        return true;
+      }
+      for (Class<?> iff : type.getInterfaces()) {
+        if (iff.isAnnotationPresent(Exportablebean.class)) {
+          return true;
+        }
+      }
+      type = type.getSuperclass();
+    }
+  }
+
+  public void marshal(final Object source,
+      final HierarchicalStreamWriter writer, final MarshallingContext context) {
+    beanProvider.visitSerializableProperties(source,
+        new GuiceBeanProvider.Visitor() {
+          public boolean shouldVisit(String name, Class<?> definedIn) {
+            return mapper.shouldSerializeMember(definedIn, name);
+          }
+
+          public void visit(String propertyName, Class<?> fieldType,
+              Class<?> definedIn, Object newObj) {
+            if (newObj != null) {
+              Mapper.ImplicitCollectionMapping mapping = mapper
+                  .getImplicitCollectionDefForFieldName(source.getClass(),
+                      propertyName);
+              if (mapping != null) {
+                if (mapping.getItemFieldName() != null) {
+                  Collection<?> list = (Collection<?>) newObj;
+                  for (Object obj : list) {
+                    writeField(propertyName, mapping.getItemFieldName(),
+                        mapping.getItemType(), definedIn, obj);
+                  }
+                } else {
+                  context.convertAnother(newObj);
+                }
+              } else {
+                writeField(propertyName, propertyName, fieldType, definedIn,
+                    newObj);
+              }
+            }
+          }
+
+          private void writeField(String propertyName, String aliasName,
+              Class<?> fieldType, Class<?> definedIn, Object newObj) {
+            ExtendedHierarchicalStreamWriterHelper.startNode(writer, mapper
+                .serializedMember(source.getClass(), aliasName), fieldType);
+            context.convertAnother(newObj);
+            writer.endNode();
+
+          }
+        });
+  }
+
+  public Object unmarshal(final HierarchicalStreamReader reader,
+      final UnmarshallingContext context) {
+    final Object result = instantiateNewInstance(context);
+
+    while (reader.hasMoreChildren()) {
+      reader.moveDown();
+
+      String propertyName = mapper.realMember(result.getClass(), reader
+          .getNodeName());
+
+      boolean propertyExistsInClass = beanProvider.propertyDefinedInClass(
+          propertyName, result.getClass());
+
+      if (propertyExistsInClass) {
+        Class<?> type = determineType(reader, result, propertyName);
+        Object value = context.convertAnother(result, type);
+        beanProvider.writeProperty(result, propertyName, value);
+      } else if (mapper.shouldSerializeMember(result.getClass(), propertyName)) {
+        throw new ConversionException("Property '" + propertyName
+            + "' not defined in class " + result.getClass().getName());
+      }
+
+      reader.moveUp();
+    }
+
+    return result;
+  }
+
+  private Object instantiateNewInstance(UnmarshallingContext context) {
+    Object result = context.currentObject();
+    if (result == null) {
+      result = beanProvider.newInstance(context.getRequiredType());
+    }
+    return result;
+  }
+
+  private Class<?> determineType(HierarchicalStreamReader reader,
+      Object result, String fieldName) {
+    final String classAttributeName = mapper.attributeForAlias("class");
+    String classAttribute = reader.getAttribute(classAttributeName);
+    if (classAttribute != null) {
+      return mapper.realClass(classAttribute);
+    } else {
+      return mapper.defaultImplementationOf(beanProvider.getPropertyType(
+          result, fieldName));
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/GuiceBeanProvider.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/GuiceBeanProvider.java
new file mode 100644
index 0000000..ff15e01
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/GuiceBeanProvider.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import java.beans.BeanInfo;
+import java.beans.IntrospectionException;
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.inject.Injector;
+import com.thoughtworks.xstream.converters.reflection.ObjectAccessException;
+
+/**
+ * GuiceBeanProvider class.
+ */
+public class GuiceBeanProvider {
+
+  protected static final Object[] NO_PARAMS = new Object[0];
+  private final Comparator<String> propertyNameComparator;
+
+  private final transient LoadingCache<Class<?>, Map<String, PropertyDescriptor>> propertyNameCache = CacheBuilder
+      .newBuilder().weakKeys().build(
+          new CacheLoader<Class<?>, Map<String, PropertyDescriptor>>() {
+            public Map<String, PropertyDescriptor> load(Class<?> type) {
+
+              BeanInfo beanInfo;
+              try {
+                beanInfo = Introspector.getBeanInfo(type, Object.class);
+              } catch (IntrospectionException e) {
+                throw new ObjectAccessException("Cannot get BeanInfo of type " + type.getName(), e);
+              }
+
+              ImmutableMap.Builder<String, PropertyDescriptor> nameMapBuilder = ImmutableMap.builder();
+              for (PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors()) {
+                nameMapBuilder.put(descriptor.getName(), descriptor);
+              }
+              return nameMapBuilder.build();
+            }
+          }
+      );
+  private Injector injector;
+
+  public GuiceBeanProvider(Injector injector) {
+    this(injector, null);
+  }
+
+  public GuiceBeanProvider(Injector injector,
+      final Comparator<String> propertyNameComparator) {
+    this.propertyNameComparator = propertyNameComparator;
+    this.injector = injector;
+  }
+
+  public Object newInstance(Class<?> type) {
+    return injector.getInstance(type);
+  }
+
+  public void visitSerializableProperties(Object object, Visitor visitor) {
+    for (PropertyDescriptor property : getSerializableProperties(object)) {
+      try {
+        Method readMethod = property.getReadMethod();
+        String name = property.getName();
+        Class<?> definedIn = readMethod.getDeclaringClass();
+        if (visitor.shouldVisit(name, definedIn)) {
+          Object value = readMethod.invoke(object);
+          visitor.visit(name, property.getPropertyType(), definedIn, value);
+        }
+      } catch (IllegalArgumentException e) {
+        throw new ObjectAccessException("Could not get property "
+            + object.getClass() + '.' + property.getName(), e);
+      } catch (IllegalAccessException e) {
+        throw new ObjectAccessException("Could not get property "
+            + object.getClass() + '.' + property.getName(), e);
+      } catch (InvocationTargetException e) {
+        throw new ObjectAccessException("Could not get property "
+            + object.getClass() + '.' + property.getName(), e);
+      }
+    }
+  }
+
+  public void writeProperty(Object object, String propertyName, Object value) {
+    PropertyDescriptor property = getProperty(propertyName, object.getClass());
+    try {
+      property.getWriteMethod().invoke(object, value);
+    } catch (IllegalArgumentException e) {
+      throw new ObjectAccessException("Could not set property "
+          + object.getClass() + '.' + property.getName(), e);
+    } catch (IllegalAccessException e) {
+      throw new ObjectAccessException("Could not set property "
+          + object.getClass() + '.' + property.getName(), e);
+    } catch (InvocationTargetException e) {
+      throw new ObjectAccessException("Could not set property "
+          + object.getClass() + '.' + property.getName(), e);
+    }
+  }
+
+  public Class<?> getPropertyType(Object object, String name) {
+    return getProperty(name, object.getClass()).getPropertyType();
+  }
+
+  public boolean propertyDefinedInClass(String name, Class<?> type) {
+    return getProperty(name, type) != null;
+  }
+
+  private List<PropertyDescriptor> getSerializableProperties(Object object) {
+    Map<String, PropertyDescriptor> nameMap = propertyNameCache.getUnchecked(object.getClass());
+
+    Set<String> names = (propertyNameComparator == null) ? nameMap.keySet() :
+      ImmutableSortedSet.orderedBy(propertyNameComparator).addAll(nameMap.keySet()).build();
+
+    List<PropertyDescriptor> result = Lists.newArrayListWithCapacity(nameMap.size());
+
+    for (final String name : names) {
+      final PropertyDescriptor descriptor = nameMap.get(name);
+      if (canStreamProperty(descriptor)) {
+        result.add(descriptor);
+      }
+    }
+    return result;
+  }
+
+  protected boolean canStreamProperty(PropertyDescriptor descriptor) {
+    return descriptor.getReadMethod() != null
+        && descriptor.getWriteMethod() != null;
+  }
+
+  public boolean propertyWriteable(String name, Class<?> type) {
+    PropertyDescriptor property = getProperty(name, type);
+    return property.getWriteMethod() != null;
+  }
+
+  private PropertyDescriptor getProperty(String name, Class<?> type) {
+    return propertyNameCache.getUnchecked(type).get(name);
+  }
+
+  interface Visitor {
+    boolean shouldVisit(String name, Class<?> definedIn);
+
+    void visit(String name, Class<?> type, Class<?> definedIn, Object value);
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ImplicitCollectionFieldMapping.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ImplicitCollectionFieldMapping.java
new file mode 100644
index 0000000..20943f1
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ImplicitCollectionFieldMapping.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.thoughtworks.xstream.mapper.Mapper.ImplicitCollectionMapping;
+
+/**
+ * <p>
+ * ItemFieldMapping defines a mapping of a class within a class to an element
+ * name. Where classes are tested, the must implement or extend the specified
+ * classes, unlike the standard behaviour of XStream they don't need to be the
+ * classes in question.
+ * </p>
+ * <p>
+ * The structure is used for implicit collections of the form *
+ * </p>
+ *
+ * <pre>
+ * &lt;outerobject&gt;
+ *    &lt;listelement&gt;
+ *       &lt;objectcontent&gt;
+ *    &lt;/listelement&gt;
+ *    &lt;listelement&gt;
+ *       &lt;objectcontent&gt;
+ *    &lt;/listelement&gt;
+ * ...
+ * &lt;/outerobject&gt;
+ * </pre>
+ * <p>
+ * or
+ * </p>
+ *
+ * <pre>
+ * &lt;person&gt;
+ *     &lt;emails&gt;
+ *        &lt;type&gt;&lt;/type&gt;
+ *        &lt;value&gt;&lt;/value&gt;
+ *     &lt;/emails&gt;
+ *     &lt;emails&gt;
+ *        &lt;type&gt;&lt;/type&gt;
+ *        &lt;value&gt;&lt;/value&gt;
+ *     &lt;/emails&gt;
+ *     &lt;emails&gt;
+ *        &lt;type&gt;&lt;/type&gt;
+ *        &lt;value&gt;&lt;/value&gt;
+ *     &lt;/emails&gt;
+ *     ...
+ * &lt;/person&gt;
+ * </pre>
+ * <p>
+ * would be specified with NewItemFieldMapping(Person.class, "emails",
+ * ListField.class, "emails");
+ * </p>
+ */
+public class ImplicitCollectionFieldMapping implements ImplicitCollectionMapping {
+
+  /**
+   * The Class that the field is defined in.
+   */
+  private Class<?> definedIn;
+  /**
+   * The class of the item that is being defined.
+   */
+  private Class<?> itemType;
+  /**
+   * The name of the fields in the class (get and set methods)
+   */
+  private String fieldName;
+  /**
+   * The name of the element that should be used for this field.
+   */
+  private String itemFieldName;
+
+  /**
+   * Create a Item Field Mapping object specifying that where the class itemType
+   * appears in the Class definedIn, the elementName should be used for the
+   * Element Name.
+   *
+   * @param definedIn
+   *          the class which contains the method
+   * @param fieldName
+   *          the name of the method/field in the class.
+   * @param itemType
+   *          the type of the method/field in the class.
+   * @param itemFieldName
+   *          the name of element in the xml
+   *
+   */
+  public ImplicitCollectionFieldMapping(Class<?> definedIn, String fieldName,
+      Class<?> itemType, String itemFieldName) {
+    this.definedIn = definedIn;
+    this.itemType = itemType;
+    this.itemFieldName = itemFieldName;
+    this.fieldName = fieldName;
+  }
+
+  /**
+   * Does this ItemFieldMapping match the supplied classes.
+   *
+   * @param definedIn
+   *          the class that the target test class is defined in, this is a real
+   *          class
+   * @param itemType
+   *          the target class, the real class
+   * @return true if the definedIn class implements the definedIn class of this
+   *         ItemFieldMapping and the itemType class implements the itemType
+   *         class of this ItemFieldMapping.
+   */
+  public boolean matches(Class<?> definedIn, Class<?> itemType) {
+    return (this.definedIn.isAssignableFrom(definedIn) && this.itemType
+        .isAssignableFrom(itemType));
+  }
+
+  public boolean matches(Class<?> definedIn, String fieldName) {
+    return (this.definedIn.isAssignableFrom(definedIn) && this.fieldName
+        .equals(fieldName));
+  }
+
+  /**
+   * @return
+   */
+  public String getFieldName() {
+    return fieldName;
+  }
+
+  /**
+   * @return
+   */
+  public String getItemFieldName() {
+    return itemFieldName;
+  }
+
+  /**
+   * @return
+   */
+  public Class<?> getItemType() {
+    return itemType;
+  }
+
+  public String getKeyFieldName() {
+    return null;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceClassMapper.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceClassMapper.java
new file mode 100644
index 0000000..e2806e1
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceClassMapper.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.thoughtworks.xstream.mapper.Mapper;
+import com.thoughtworks.xstream.mapper.MapperWrapper;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+/**
+ * The InterfaceClassMapper provides the central mapping of the XStream bean
+ * converter. It is used by XStream to determine the element names and classes
+ * being mapped. This is driven by a number of read only data structures that
+ * are injected on creation. The resolution of classes follow the inheritance
+ * model. To map all collections to an element we would use the Collection.class
+ * as the reference class and then ArrayList and Set would both be mapped to the
+ * same element.
+ */
+public class InterfaceClassMapper extends MapperWrapper {
+
+  /**
+   * A logger.
+   */
+  private static final Logger LOG = Logger.getLogger(InterfaceClassMapper.class.getName());
+  /**
+   * A map of element names to classes.
+   */
+  private Map<String, Class<?>> elementClassMap = Maps.newHashMap();
+  /**
+   * The first child of the root object. If the root object is not a collection,
+   * this is null. If the root object is a collection and all the elements are
+   * the same this is set to the class of those elements. Once the first call
+   * has been made to the mapper, this is set back to null. Note its a thread
+   * local enabling this class to remain multi threaded.
+   */
+  private ThreadLocal<Class<?>> firstChild = new ThreadLocal<Class<?>>();
+  /**
+   * A map of classed to ommit, the key is the field name, and the value is an
+   * array of classes where that field is ommitted from the output.
+   */
+  private Multimap<String, Class<?>> omitMMap;
+  /**
+   * A map of elements, where the ClassMapping object defines how the classes
+   * are mapped to elements.
+   */
+  private List<ClassFieldMapping> elementMappingList;
+  /**
+   * A map of parent elements used where there is a collection as the root
+   * object being serialized. This ensures that the root obejct gets the right
+   * element name rather than list a generic &gt;list&lt;
+   */
+  private List<ClassFieldMapping> listElementMappingList;
+  /**
+   * An implementation of a tracking stack for the writer. If this class is to
+   * be thread safe, the implementation of this field must also be thread safe
+   * as it is shared over multiple threads.
+   */
+  private WriterStack writerStack;
+
+  /**
+   * A list of explicit mapping specifications.
+   */
+  private List<ImplicitCollectionFieldMapping> itemFieldMappings;
+
+  /**
+   * Create an Interface Class Mapper with a configuration.
+   *
+   * @param writerStack
+   *          A thread safe WriterStack implementation connected to the XStream
+   *          driver and hence all the writers.
+   * @param wrapped
+   *          the base mapper to be wrapped by this wrapper. All mappers must
+   *          wrap the default mapper.
+   * @param elementMappingList
+   *          A list of element to class mappings specified by ClassFieldMapping
+   *          Object. This list is priority ordered with the highest priority
+   *          mappings coming first.
+   * @param listElementMappingList
+   *          A list of element names to use as the base element where there is
+   *          a collection of the same type objects being serialized.
+   * @param omitMMap
+   *          A Multimap of fields in classes to omit from serialization.
+   * @param elementClassMap
+   *          a map of element names to class types.
+   */
+  public InterfaceClassMapper(WriterStack writerStack,
+      Mapper wrapped,
+      List<ClassFieldMapping> elementMappingList,
+      List<ClassFieldMapping> listElementMappingList,
+      List<ImplicitCollectionFieldMapping> itemFieldMappings,
+      Multimap<String, Class<?>> omitMMap,
+      Map<String, Class<?>> elementClassMap) {
+    super(wrapped);
+    this.elementClassMap = elementClassMap;
+    this.elementMappingList = elementMappingList;
+    this.listElementMappingList = listElementMappingList;
+    this.omitMMap = omitMMap;
+    this.writerStack = writerStack;
+    this.itemFieldMappings = itemFieldMappings;
+  }
+
+  /**
+   * Set the base object at the start of a serialization, this ensures that the
+   * base element type is appropriate for the elements that are contained within
+   * the object. This method only has any effect if the base object is a
+   * Collection of some form. other wise this method has no effect on the state.
+   * The method is thread safe attaching state to the thread for later
+   * retrieval.
+   *
+   * @param base
+   *          the base object being serialized.
+   */
+  public void setBaseObject(Object base) {
+    firstChild.set(null);
+    if (Collection.class.isAssignableFrom(base.getClass())) {
+      Collection<?> c = (Collection<?>) base;
+      Class<?> clazz = null;
+
+      for (Object o : c) {
+        if (clazz == null) {
+          clazz = o.getClass();
+        } else {
+          if (!clazz.equals(o.getClass())) {
+            clazz = null;
+            break;
+          }
+        }
+      }
+      firstChild.set(clazz);
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("First Child set to " + clazz);
+      }
+    }
+  }
+
+  /**
+   * <p>
+   * Get the serialized element name for a specific class. If this is the first
+   * object to be serialized, and it is a collection, then the elements of the
+   * collection will have been inspected to determine if the container should
+   * have a special name. These names are specified in the
+   * listElementMappingList which is specified on construction. If the first
+   * element is not found a standard list.container element name is used to
+   * contain all the others, this list type is only ever used in the unit tests.
+   * </p>
+   * <p>
+   * For subsequent elements, the class is mapped directly to a element name at
+   * the same level, specified via the elementMappingList which is injected in
+   * the constructor. This mapping looks to see if the class in question
+   * inherits of extends the classes in the list and uses the element name
+   * associated wit the first match.
+   * </p>
+   *
+   * @see com.thoughtworks.xstream.mapper.MapperWrapper#serializedClass(java.lang.Class)
+   * @param type
+   *          the type of the class to the serialized
+   * @return the name of the element that that should be used to contain the
+   *         class when serialized.
+   */
+  @SuppressWarnings("unchecked")
+  // the API is not generic
+  @Override
+  public String serializedClass(Class type) {
+    String parentElementName = writerStack.peek();
+    if (Collection.class.isAssignableFrom(type) && firstChild.get() != null) {
+      // empty list, if this is the first one, then we need to look at the
+      // first child setup on startup.
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("Converting Child to " + firstChild.get());
+      }
+      type = firstChild.get();
+      firstChild.set(null);
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("serializedClass(" + type + ") is a collection member "
+            + Collection.class.isAssignableFrom(type));
+      }
+      for (ClassFieldMapping cfm : listElementMappingList) {
+        if (cfm.matches(parentElementName, type)) {
+          return cfm.getElementName();
+        }
+      }
+      return "list.container";
+    } else {
+      // but after we have been asked once, then clear
+      firstChild.set(null);
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.fine("serializedClass(" + type + ')');
+      }
+      for (ClassFieldMapping cfm : elementMappingList) {
+        if (cfm.matches(parentElementName, type)) {
+          if (LOG.isLoggable(Level.FINE)) {
+            LOG.fine("From MAP serializedClass(" + type + ")  =="
+                + cfm.getElementName());
+          }
+          return cfm.getElementName();
+        }
+      }
+
+    }
+
+    String fieldName = super.serializedClass(type);
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.fine("--- From Super serializedClass(" + type + ")  ==" + fieldName);
+    }
+    return fieldName;
+
+  }
+
+  /**
+   * Checks to see if the field in a class should be serialized. This is
+   * controlled buy the omitMMap Multimap which is keyed by the field name. Each entry
+   * in the map contains a list of classes where the field name should be
+   * excluded from the output.
+   *
+   * @param definedIn
+   *          the class the field is defined in
+   * @param fieldName
+   *          the field being considered
+   * @return true of the field should be serialized false if it should be
+   *         ignored.
+   * @see com.thoughtworks.xstream.mapper.MapperWrapper#shouldSerializeMember(java.lang.Class,
+   *      java.lang.String)
+   *
+   */
+  @SuppressWarnings("unchecked")
+  // API is not generic
+  @Override
+  public boolean shouldSerializeMember(Class definedIn, String fieldName) {
+    for (Class<?> omit : omitMMap.get(fieldName)) {
+      if (omit.isAssignableFrom(definedIn)) {
+        return false;
+      }
+    }
+    return super.shouldSerializeMember(definedIn, fieldName);
+  }
+
+  /**
+   * Get the real class associated with an element name from the
+   * elementMappingList.
+   *
+   * @param elementName
+   *          the name of the element being read.
+   * @see com.thoughtworks.xstream.mapper.MapperWrapper#realClass(java.lang.String)
+   */
+  @SuppressWarnings("unchecked")
+  @Override
+  public Class realClass(String elementName) {
+    Class<?> clazz = elementClassMap.get(elementName);
+    if (clazz == null) {
+      clazz = super.realClass(elementName);
+    }
+    return clazz;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   * @see com.thoughtworks.xstream.mapper.MapperWrapper#getImplicitCollectionDefForFieldName(java.lang.Class, java.lang.String)
+   */
+  @SuppressWarnings("unchecked")
+  @Override
+  public ImplicitCollectionMapping getImplicitCollectionDefForFieldName(Class itemType, String fieldName) {
+    for ( ImplicitCollectionFieldMapping ifm : itemFieldMappings) {
+      if ( ifm.matches(itemType, fieldName) ) {
+        return ifm;
+      }
+    }
+    return super.getImplicitCollectionDefForFieldName(itemType, fieldName);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceFieldAliasMapping.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceFieldAliasMapping.java
new file mode 100644
index 0000000..b2f7481
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceFieldAliasMapping.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+/**
+ * Maps of Interfaces to Aliases used by {@link org.apache.shindig.protocol.conversion.BeanXStreamConverter}
+ */
+public class InterfaceFieldAliasMapping {
+
+  private String alias;
+  private Class<?> type;
+  private String fieldName;
+  private String parent;
+
+  /**
+   * @param alias
+   *          the name of the element to be used in the xml
+   * @param type
+   *          the type containing the field
+   * @param fieldName
+   *          the field name.
+   */
+  public InterfaceFieldAliasMapping(String alias, Class<?> type,
+      String fieldName) {
+    this.alias = alias;
+    this.type = type;
+    this.fieldName = fieldName;
+  }
+
+  /**
+   * @param alias
+   *          the name of the element to be used in the xml
+   * @param type
+   *          the type containing the field
+   * @param fieldName
+   *          the field name.
+   * @param parent
+   *          the parent element
+   */
+  public InterfaceFieldAliasMapping(String alias, Class<?> type,
+      String fieldName, String parent) {
+    this.alias = alias;
+    this.type = type;
+    this.fieldName = fieldName;
+    this.parent = parent;
+  }
+
+  /**
+   * @return
+   */
+  public Class<?> getType() {
+    return type;
+  }
+
+  /**
+   * @return
+   */
+  public String getAlias() {
+    return alias;
+  }
+
+  /**
+   * @return
+   */
+  public String getFieldName() {
+    return fieldName;
+  }
+
+  /**
+   * @return the parent
+   */
+  public String getParent() {
+    return parent;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see java.lang.Object#toString()
+   */
+  @Override
+  public String toString() {
+    if (parent == null) {
+      return type + ".get" + fieldName + "() <-> <" + alias + '>';
+    } else {
+      return type + ".get" + fieldName + "() <-> <" + alias
+          + "> inside parent <" + parent + '>';
+    }
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceFieldAliasingMapper.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceFieldAliasingMapper.java
new file mode 100644
index 0000000..0cd92d9
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/InterfaceFieldAliasingMapper.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.thoughtworks.xstream.mapper.Mapper;
+import com.thoughtworks.xstream.mapper.MapperWrapper;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Maps Interfaces to Aliases used by {@link org.apache.shindig.protocol.conversion.BeanXStreamConverter}
+ */
+public class InterfaceFieldAliasingMapper extends MapperWrapper {
+
+  private Map<String, List<InterfaceFieldAliasMapping>> serializedMap = Maps.newHashMap();
+  private Map<String, List<InterfaceFieldAliasMapping>> membersMap = Maps.newHashMap();
+  private WriterStack writerStack;
+
+  /**
+   * @param wrapped
+   */
+  public InterfaceFieldAliasingMapper(Mapper wrapped, WriterStack writerStack,
+      List<InterfaceFieldAliasMapping> ifaList) {
+    super(wrapped);
+    this.writerStack = writerStack;
+    for (InterfaceFieldAliasMapping ifa : ifaList) {
+
+      List<InterfaceFieldAliasMapping> serializedMatches = serializedMap.get(ifa.getFieldName());
+      if (serializedMatches == null) {
+        serializedMatches = Lists.newArrayList();
+        serializedMap.put(ifa.getFieldName(), serializedMatches);
+      }
+      serializedMatches.add(ifa);
+      List<InterfaceFieldAliasMapping> memberMatches = membersMap.get(ifa.getAlias());
+      if (memberMatches == null) {
+        memberMatches = Lists.newArrayList();
+        membersMap.put(ifa.getAlias(), memberMatches);
+      }
+      memberMatches.add(ifa);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.mapper.MapperWrapper#realMember(java.lang.Class,
+   *      java.lang.String)
+   */
+  @SuppressWarnings("unchecked")
+  @Override
+  public String realMember(Class type, String serialized) {
+    // get the possible member spec, using the serialized elment as the key.
+    // comes from the map of members.
+    List<InterfaceFieldAliasMapping> serializedMatches = membersMap
+        .get(serialized);
+    if (serializedMatches != null) {
+      for (InterfaceFieldAliasMapping ifa : serializedMatches) {
+        if (ifa.getType().isAssignableFrom(type)) {
+          return ifa.getFieldName();
+        }
+      }
+    }
+    return super.realMember(type, serialized);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.mapper.MapperWrapper#serializedMember(java.lang.Class,
+   *      java.lang.String)
+   */
+  @SuppressWarnings("unchecked")
+  @Override
+  public String serializedMember(Class type, String memberName) {
+    // get the possible serialized spec, using the memberName elment as the key.
+    // comes from the map of serialized elements.
+    List<InterfaceFieldAliasMapping> memberMatches = serializedMap
+        .get(memberName);
+    if (memberMatches != null) {
+      for (InterfaceFieldAliasMapping ifa : memberMatches) {
+        if (ifa.getParent() == null) {
+          if (ifa.getType().isAssignableFrom(type)) {
+            return ifa.getAlias();
+          }
+        } else {
+          if (ifa.getType().isAssignableFrom(type)
+              && ifa.getParent().equals(writerStack.peek())) {
+            return ifa.getAlias();
+          }
+        }
+      }
+    }
+    return super.serializedMember(type, memberName);
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/MapConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/MapConverter.java
new file mode 100644
index 0000000..8b796b0
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/MapConverter.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.google.common.collect.MapMaker;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+/**
+ * converts a map to and from the form &lt;container&gt;
+ * &lt;key&gt;value&lt;/key&gt; &lt;key&gt;value&lt;/key&gt; <container>.
+ */
+public class MapConverter extends AbstractCollectionConverter {
+
+  /**
+   * Create a MapConverter that use use the supplied mapper.
+   *
+   * @param mapper
+   *          the mapped to base the conversion on.
+   */
+  public MapConverter(Mapper mapper) {
+    super(mapper);
+  }
+
+  /**
+   * output the Map in the simplified form.
+   *
+   * @param source
+   *          the object to be output
+   * @param writer
+   *          the writer to use to perform the output.
+   * @param context
+   *          the context in which to perform the output.
+   *
+   * @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object,
+   *      com.thoughtworks.xstream.io.HierarchicalStreamWriter,
+   *      com.thoughtworks.xstream.converters.MarshallingContext)
+   */
+  @Override
+  public void marshal(Object source, HierarchicalStreamWriter writer,
+      MarshallingContext context) {
+    Map<?, ?> map = (Map<?, ?>) source;
+    for (Entry<?, ?> e : map.entrySet()) {
+      writer.startNode("entry");
+      writer.startNode("key");
+      writer.setValue(String.valueOf(e.getKey()));
+      writer.endNode();
+      writer.startNode("value");
+      context.convertAnother(e.getValue());
+      writer.endNode();
+      writer.endNode();
+    }
+  }
+
+  /**
+   * Convert a suitably positioned reader stream into a Map object.
+   *
+   * @param reader
+   *          the stream reader positioned at the start of the object.
+   * @param context
+   *          the unmarshalling context.
+   * @return the object representing the stream.
+   * @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader,
+   *      com.thoughtworks.xstream.converters.UnmarshallingContext)
+   */
+  @Override
+  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+    Map<String, Object> m = new MapMaker().makeMap();
+    reader.moveDown();
+    while (reader.hasMoreChildren()) {
+      String key = reader.getNodeName();
+      if ("entry".equals(key)) {
+        Object value = null;
+        reader.moveDown();
+        String type = reader.getNodeName();
+        if ("key".equals(type)) {
+          key = reader.getValue();
+        } else {
+          if (reader.hasMoreChildren()) {
+            value = readItem(reader, context, m);
+          } else {
+            value = reader.getValue();
+          }
+        }
+        reader.moveUp();
+        reader.moveDown();
+        type = reader.getNodeName();
+        if ("key".equals(type)) {
+          key = reader.getValue();
+        } else {
+          if (reader.hasMoreChildren()) {
+            value = readItem(reader, context, m);
+          } else {
+            value = reader.getValue();
+          }
+        }
+        m.put(key, value);
+        reader.moveUp();
+      } else {
+        reader.moveDown();
+        if (reader.hasMoreChildren()) {
+          m.put(key, readItem(reader, context, m));
+        } else {
+          m.put(key, reader.getValue());
+        }
+        reader.moveUp();
+      }
+    }
+    reader.moveUp();
+    return m;
+  }
+
+  /**
+   * Can this Converter convert the type supplied.
+   *
+   * @param clazz
+   *          the type being converted.
+   * @return true if the type supplied is a form of Map.
+   * @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class)
+   */
+  @Override
+  @SuppressWarnings("unchecked")
+  // API is not generic
+  public boolean canConvert(Class clazz) {
+    return Map.class.isAssignableFrom(clazz);
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/NamespaceSet.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/NamespaceSet.java
new file mode 100644
index 0000000..22d2748
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/NamespaceSet.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * A container class that defines namespaces and subsequent element named for an
+ * element in the output stack. A set of active namespaces are defined by
+ * addNamespace and a name to prefixed name translation is specified by
+ * addPrefixedElement.
+ */
+public class NamespaceSet {
+
+  /**
+   * A map namespace attributes to the namespace uri.
+   */
+  private Map<String, String> namespaces = Maps.newHashMap();
+  /**
+   * A map of localElement names to prefixed element names.
+   */
+  private Map<String, String> elementNames = Maps.newHashMap();
+
+  /**
+   * Add a namespace to the list.
+   *
+   * @param nsAttribute
+   *          the attribute to be used to specify the namespace
+   * @param namespace
+   *          the namespace URI
+   */
+  public void addNamespace(String nsAttribute, String namespace) {
+    namespaces.put(nsAttribute, namespace);
+  }
+
+  /**
+   * Add a localname translation.
+   *
+   * @param elementName
+   *          the local name of the element
+   * @param namespacedElementName
+   *          the final name of the element with prefix.
+   */
+  public void addPrefixedElement(String elementName, String namespacedElementName) {
+    elementNames.put(elementName, namespacedElementName);
+  }
+
+  /**
+   * Convert an element name, if necessary.
+   *
+   * @param name
+   *          the name to be converted.
+   * @return the converted name, left as is if no conversion was required.
+   */
+  public String getElementName(String name) {
+    return elementNames.get(name) != null ? elementNames.get(name) : name;
+  }
+
+  /**
+   * @return an Set of entries containing the namespace attributes and uris,
+   *         attributes in the key, uri's in the value or each entry.
+   */
+  public Set<Entry<String, String>> nameSpaceEntrySet() {
+    return namespaces.entrySet();
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/RestfullCollectionConverter.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/RestfullCollectionConverter.java
new file mode 100644
index 0000000..746011f
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/RestfullCollectionConverter.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import org.apache.shindig.protocol.RestfulCollection;
+
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+/**
+ * This converter changes the way in which a collection is serialized
+ */
+public class RestfullCollectionConverter extends AbstractCollectionConverter {
+
+  /**
+   * @param mapper
+   */
+  public RestfullCollectionConverter(Mapper mapper) {
+    super(mapper);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter#canConvert(java.lang.Class)
+   */
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean canConvert(Class clazz) {
+    return RestfulCollection.class.isAssignableFrom(clazz);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter#marshal(java.lang.Object,
+   *      com.thoughtworks.xstream.io.HierarchicalStreamWriter,
+   *      com.thoughtworks.xstream.converters.MarshallingContext)
+   */
+  @Override
+  public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+    RestfulCollection<?> collection = (RestfulCollection<?>) source;
+
+    writer.startNode("itemsPerPage");
+    writer.setValue(String.valueOf(collection.getItemsPerPage()));
+    writer.endNode();
+    writer.startNode("startIndex");
+    writer.setValue(String.valueOf(collection.getStartIndex()));
+    writer.endNode();
+    writer.startNode("totalResults");
+    writer.setValue(String.valueOf(collection.getTotalResults()));
+    writer.endNode();
+    writer.startNode("filtered");
+    writer.setValue(String.valueOf(collection.isFiltered()));
+    writer.endNode();
+    writer.startNode("sorted");
+    writer.setValue(String.valueOf(collection.isSorted()));
+    writer.endNode();
+    writer.startNode("updatedSince");
+    writer.setValue(String.valueOf(collection.isUpdatedSince()));
+    writer.endNode();
+
+    // TODO: resolve if entry is the container or the name of the object.
+    writer.startNode("list");
+    for (Object o : collection.getList()) {
+      writer.startNode("entry");
+      writeItem(o, context, writer);
+      writer.endNode();
+    }
+    writer.endNode();
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader,
+   *      com.thoughtworks.xstream.converters.UnmarshallingContext)
+   */
+  @Override
+  public Object unmarshal(HierarchicalStreamReader arg0, UnmarshallingContext arg1) {
+    // TODO Auto-generated method stub
+    return null;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/StackDriver.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/StackDriver.java
new file mode 100644
index 0000000..c5492d9
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/StackDriver.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.Map;
+
+/**
+ * A StackDriver wraps other forms of Drivers and updates a WriterStack with the
+ * path into the writer hierarchy.
+ */
+public class StackDriver implements HierarchicalStreamDriver {
+
+  /**
+   * The parent Stream Driver that does the work.
+   */
+  private HierarchicalStreamDriver parent;
+  /**
+   * A Writer Stack implementation that records where the writer is.
+   */
+  private WriterStack writerStack;
+  private Map<String, NamespaceSet> namespaces;
+
+  /**
+   * Create a {@link StackDriver}, wrapping a {@link HierarchicalStreamDriver}
+   * and updating a {@link WriterStack}.
+   *
+   * @param parent
+   *          the driver to be wrapped
+   * @param writerStack
+   *          the thread safe writer stack that records where the writer is.
+   * @param map
+   */
+  public StackDriver(HierarchicalStreamDriver parent, WriterStack writerStack, Map<String, NamespaceSet> map) {
+    this.parent = parent;
+    this.writerStack = writerStack;
+    this.namespaces = map;
+  }
+
+  /**
+   * Create a {@link HierarchicalStreamReader}, using the wrapped
+   * {@link HierarchicalStreamDriver}.
+   *
+   * @param reader
+   *          the Reader that will be used to read from the underlying stream
+   * @return the reader
+   * @see com.thoughtworks.xstream.io.HierarchicalStreamDriver#createReader(java.io.Reader)
+   */
+  public HierarchicalStreamReader createReader(Reader reader) {
+    return parent.createReader(reader);
+  }
+
+  /**
+   * Create a {@link HierarchicalStreamReader}, using the wrapped
+   * {@link HierarchicalStreamDriver}.
+   *
+   * @param inputStream
+   *          the input stream that will be used to read from the underlying
+   *          stream
+   * @return the reader
+   * @see com.thoughtworks.xstream.io.HierarchicalStreamDriver#createReader(java.io.InputStream)
+   */
+  public HierarchicalStreamReader createReader(InputStream inputStream) {
+    return parent.createReader(inputStream);
+  }
+
+  /**
+   * Create a {@link HierarchicalStreamWriter} that tracks the path to the
+   * current element based on a {@link java.io.Writer}.
+   *
+   * @param writer
+   *          the underlying writer that will perform the writes.
+   * @return the writer
+   * @see com.thoughtworks.xstream.io.HierarchicalStreamDriver#createWriter(java.io.Writer)
+   */
+  public HierarchicalStreamWriter createWriter(Writer writer) {
+    HierarchicalStreamWriter parentWriter = parent.createWriter(writer);
+    return new StackWriterWrapper(parentWriter, writerStack, namespaces);
+  }
+
+  /**
+   * Create a {@link HierarchicalStreamWriter} that tracks the path to the
+   * current element based on a {@link OutputStream}.
+   *
+   * @param outputStream
+   *          the underlying output stream that will perform the writes.
+   * @return the writer
+   * @see com.thoughtworks.xstream.io.HierarchicalStreamDriver#createWriter(java.io.Writer)
+   */
+  public HierarchicalStreamWriter createWriter(OutputStream outputStream) {
+    HierarchicalStreamWriter parentWriter = parent.createWriter(outputStream);
+    return new StackWriterWrapper(parentWriter, writerStack, namespaces);
+  }
+
+  public HierarchicalStreamReader createReader(URL url) {
+    return parent.createReader(url);
+  }
+
+  public HierarchicalStreamReader createReader(File file) {
+    return parent.createReader(file);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/StackWriterWrapper.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/StackWriterWrapper.java
new file mode 100644
index 0000000..b3cf371
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/StackWriterWrapper.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.io.WriterWrapper;
+
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * A Writer that provides a Stack based tracking of the location of the
+ * underlying writer.
+ */
+public class StackWriterWrapper extends WriterWrapper {
+
+  /**
+   * The stack that keeps track of current node.
+   */
+  private WriterStack writerStack;
+  private Map<String, NamespaceSet> namespaces;
+
+  /**
+   * Create a {@link StackWriterWrapper} that wraps a
+   * {@link HierarchicalStreamWriter} and tracks where that writer is in the
+   * hierarchy.
+   *
+   * @param wrapped
+   *          the underlying writer
+   * @param writerStack
+   *          the stack that will record where the writer is.
+   * @param namespaces
+   */
+  public StackWriterWrapper(HierarchicalStreamWriter wrapped,
+      WriterStack writerStack, Map<String, NamespaceSet> namespaces) {
+    super(wrapped);
+    this.writerStack = writerStack;
+    this.namespaces = namespaces;
+  }
+
+  /**
+   * Set attribute values on the current node, but filter out class attributes
+   * from the writer, this is not strictly a feature of this class, but is
+   * required (for shindig to meet the XSD requirements.
+   *
+   * @param name
+   *          the name of attribute
+   * @param value
+   *          the value of the attribute.
+   * @see com.thoughtworks.xstream.io.WriterWrapper#addAttribute(java.lang.String,
+   *      java.lang.String)
+   */
+  @Override
+  public void addAttribute(String name, String value) {
+    if (!"class".equals(name)) {
+      super.addAttribute(name, value);
+    }
+  }
+
+  /**
+   * Begin a new element or node of the supplied name.
+   *
+   * @param name
+   *          the name of the node.
+   *
+   * @see com.thoughtworks.xstream.io.WriterWrapper#startNode(java.lang.String )
+   */
+  @Override
+  public void startNode(String name) {
+    super.startNode(translateName(name));
+    addNamespace(name);
+  }
+
+  /**
+   * Start a node with a specific class. This may invoke
+   * {@link StackWriterWrapper#startNode(String)} so we might have double
+   * recording of the position in the stack. This would be a bug.
+   *
+   * @see com.thoughtworks.xstream.io.WriterWrapper#startNode(java.lang.String ,
+   *      java.lang.Class)
+   */
+  @SuppressWarnings("unchecked")
+  // API is not generic
+  @Override
+  public void startNode(String name, Class clazz) {
+    super.startNode(translateName(name), clazz);
+    addNamespace(name);
+  }
+
+  /**
+   * @param name
+   * @return
+   */
+  private String translateName(String name) {
+    NamespaceSet namespaceSet = namespaces.get(name);
+    NamespaceSet currentNamespace = writerStack.peekNamespace();
+    // specified by not current
+    if (namespaceSet != null &&  namespaceSet != currentNamespace) {
+        return namespaceSet.getElementName(name);
+    }
+    // current has been specified
+    if ( currentNamespace != null ) {
+      return currentNamespace.getElementName(name);
+    }
+    return name;
+
+  }
+
+  /**
+   * @param name
+   */
+  private void addNamespace(String name) {
+    NamespaceSet namespaceSet = namespaces.get(name);
+    NamespaceSet currentNamespace = writerStack.peekNamespace();
+    // specified and not current
+    if ( namespaceSet != null && namespaceSet != currentNamespace ) {
+      for (Entry<String, String> e : namespaceSet.nameSpaceEntrySet()) {
+        super.addAttribute(e.getKey(), e.getValue());
+      }
+      currentNamespace = namespaceSet;
+    }
+    writerStack.push(name, currentNamespace);
+  }
+
+  /**
+   * End the current node, making the parent node the active node.
+   *
+   * @see com.thoughtworks.xstream.io.WriterWrapper#endNode()
+   */
+  @Override
+  public void endNode() {
+    writerStack.pop();
+    super.endNode();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ThreadSafeWriterStack.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ThreadSafeWriterStack.java
new file mode 100644
index 0000000..9291433
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/ThreadSafeWriterStack.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * A simple implementation of a WriterStack that can be shared amongst multiple
+ * threads and will record the state of each thread. This cannot however be
+ * shared amongst multiple writers on multiple threads as this would lead to an
+ * inconsistent state. In the shindig implementation this is not an issue as the
+ * serialization process is atomic below the API.
+ */
+public class ThreadSafeWriterStack implements WriterStack {
+  /**
+   * A thread local holder for the stack.
+   */
+  private ThreadLocal<List<Object[]>> stackHolder = new ThreadLocal<List<Object[]>>() {
+    @Override
+    protected List<Object[]> initialValue() {
+      return Lists.newArrayList();
+    }
+  };
+
+  /**
+   * Create a {@link WriterStack} that is thread safe. The stack will store its
+   * contents on the thread so this class can be shared amongst multiple
+   * threads, but obviously there must be only one instance of the class per
+   * writer per thread.
+   */
+  public ThreadSafeWriterStack() {
+  }
+
+  /**
+   * Add an element name to the stack on the current thread.
+   *
+   * @param name
+   *          the node name just added.
+   */
+  public void push(String name, NamespaceSet namespaceSet) {
+    stackHolder.get().add(new Object[]{name, namespaceSet});
+  }
+
+  /**
+   * Remove a node name from the stack on the current thread.
+   *
+   * @return the node name just ended.
+   */
+  public String pop() {
+    List<Object[]> stack = stackHolder.get();
+    if (stack.isEmpty()) {
+      return null;
+    } else {
+      Object[] o =  stack.remove(stack.size() - 1);
+      if ( o != null && o.length > 0 ) {
+        return (String) o[0];
+      }
+      return null;
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see WriterStack#peek()
+   */
+  public String peek() {
+    return (String) peek(0);
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see WriterStack#peekNamespace()
+   */
+  public NamespaceSet peekNamespace() {
+    return (NamespaceSet) peek(1);
+  }
+  /**
+   * Look at the node name on the top of the stack on the current thread.
+   *
+   * @return the current node name.
+   */
+  public Object peek(int i) {
+    List<Object[]> stack = stackHolder.get();
+    if (stack.isEmpty()) {
+      return null;
+    } else {
+      Object[] o = stack.get(stack.size() - 1);
+      if ( o != null && o.length > i ) {
+        return o[i];
+      }
+      return null;
+    }
+  }
+
+  /**
+   * Reset the stack back to the default state.
+   *
+   * @see WriterStack#reset()
+   */
+  public void reset() {
+    stackHolder.get().clear();
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see WriterStack#size()
+   */
+  public int size() {
+    List<Object[]> s = stackHolder.get();
+    if ( s == null ) {
+      return 0;
+    } else {
+      return s.size();
+    }
+  }
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/WriterStack.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/WriterStack.java
new file mode 100644
index 0000000..45d2026
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/WriterStack.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+/**
+ * A writer stack is a simple stack that tracks the current location of the
+ * writer.
+ */
+public interface WriterStack {
+
+  /**
+   * Peek into the current location of the writer.
+   *
+   * @return the name of the current node.
+   */
+  String peek();
+
+  /**
+   * @return the current namespace.
+   */
+  NamespaceSet peekNamespace();
+
+  /**
+   * Reset the stack to its default state.
+   */
+  void reset();
+
+  /**
+   * add a node name into the stack indicating that the writer has moved into a
+   * new child element.
+   *
+   * @param name
+   *          the name of the new child element.
+   * @param namespace
+   *          the namespace set associated with the current element.
+   */
+  void push(String name, NamespaceSet namespace);
+
+  /**
+   * Remove and return the current node name, making the parent node the active
+   * node name.
+   *
+   * @return the node name just removed from the stack.
+   */
+  String pop();
+
+  /**
+   * @return the size of the statck
+   */
+  int size();
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/XStreamConfiguration.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/XStreamConfiguration.java
new file mode 100644
index 0000000..5772e6c
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/conversion/xstream/XStreamConfiguration.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion.xstream;
+
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
+import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+import java.util.Map;
+
+/**
+ * The configuration for the XStream converter, this class encapsulates the
+ * lists and maps that define the how xstream converts the model.
+ */
+public interface XStreamConfiguration {
+  public static enum ConverterSet {
+    MAP(), COLLECTION(), DEFAULT()
+  }
+
+  public class ConverterConfig {
+    public InterfaceClassMapper mapper;
+    public XStream xstream;
+
+    public ConverterConfig(InterfaceClassMapper mapper, XStream xstream) {
+      this.mapper = mapper;
+      this.xstream = xstream;
+    }
+  }
+
+  /**
+   * Generate the converter config.
+   *
+   * @param c
+   *          which converter set.
+   * @param rp
+   *          an XStream reflection provider.
+   * @param dmapper
+   *          the XStream mapper.
+   * @param driver
+   *          the XStream driver
+   * @param writerStack
+   *          a hirachical stack recorder.
+   * @return the converter config, used for serialization.
+   */
+  ConverterConfig getConverterConfig(ConverterSet c, ReflectionProvider rp,
+      Mapper dmapper, HierarchicalStreamDriver driver, WriterStack writerStack);
+
+  /**
+   * @return get the namespace mappings used by the driver.
+   */
+  Map<String, NamespaceSet> getNameSpaces();
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/Enum.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/Enum.java
new file mode 100644
index 0000000..22d6664
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/Enum.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+/**
+ * see <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Enum">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Enum</a>
+ *
+ * Base class for all Enum objects. This class allows containers to use constants for fields that
+ * have a common set of values.
+ */
+@Exportablebean
+public interface Enum<E extends Enum.EnumKey> {
+
+  /**
+   * Set of fields associated with an Enum object.
+   */
+  public static enum Field {
+    /**
+     * The value of the field.
+     */
+    VALUE("value"),
+    /**
+     * The display value of the field.
+     */
+    DISPLAY_VALUE("displayValue");
+
+    /**
+     * The json representation of the feild enum.
+     */
+    private final String jsonString;
+
+    /**
+     * Create a field enum.
+     * @param jsonString the json value of the enum.
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see java.lang.Enum#toString()
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * Gets the value of this Enum. This is the string displayed to the user. If the container
+   * supports localization, the string should be localized.
+   *
+   * @return the Enum's user visible value
+   */
+  String getDisplayValue();
+
+  /**
+   * Sets the value of this Enum. This is the string displayed to the user. If the container
+   * supports localization, the string should be localized.
+   *
+   * @param displayValue The value to set.
+   */
+  void setDisplayValue(String displayValue);
+
+  /**
+   * Gets the key for this Enum. Use this for logic within your gadget.
+   *
+   * @return java.lang.Enum key object for this Enum.
+   */
+  E getValue();
+
+  /**
+   * Sets the key for this Enum. Use this for logic within your gadget.
+   *
+   * @param value The value to set.
+   */
+  void setValue(E value);
+
+  /**
+ * base interface for keyed Enumerators.
+   */
+  public static interface EnumKey {
+    String getDisplayValue();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/EnumImpl.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/EnumImpl.java
new file mode 100644
index 0000000..1e4c9bb
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/EnumImpl.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+/**
+ * Implementation of the Enum interface
+ */
+public final class EnumImpl<E extends Enum.EnumKey> implements Enum<E> {
+  private String displayValue;
+  private E value = null;
+
+  /**
+   * Constructs a Enum object.
+   * @param value EnumKey The key to use
+   * @param displayValue String The display value
+   */
+  public EnumImpl(E value, String displayValue) {
+    this.value = value;
+    this.displayValue = displayValue;
+  }
+
+  /**
+   * Constructs a Enum object.
+   * @param value The key to use. Will use the value from getDisplayValue() as
+   *     the display value.
+   */
+  public EnumImpl(E value) {
+    this(value, value.getDisplayValue());
+  }
+
+  public String getDisplayValue() {
+    return displayValue;
+  }
+
+  public void setDisplayValue(String displayValue) {
+    this.displayValue = displayValue;
+  }
+
+  public E getValue() {
+    return value;
+  }
+
+  public void setValue(E value) {
+    this.value = value;
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/Exportablebean.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/Exportablebean.java
new file mode 100644
index 0000000..daca947
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/Exportablebean.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation used by {@link org.apache.shindig.protocol.conversion.xstream.GuiceBeanConverter} to
+ * determine whether data from a getter is exposed.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited()
+public @interface Exportablebean {}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/ExtendableBean.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/ExtendableBean.java
new file mode 100644
index 0000000..1cc909b
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/ExtendableBean.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+import java.util.Map;
+
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * A generic bucket to store arbitrary extensions.
+ */
+@ImplementedBy(ExtendableBeanImpl.class)
+@Exportablebean
+public interface ExtendableBean extends Map<String, Object> {
+
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/ExtendableBeanImpl.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/ExtendableBeanImpl.java
new file mode 100644
index 0000000..1fb7a4e
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/ExtendableBeanImpl.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+import java.util.HashMap;
+
+
+/**
+ * A generic bucket to store arbitrary extensions.
+ */
+public class ExtendableBeanImpl extends HashMap<String, Object> implements ExtendableBean {
+	private static final long serialVersionUID = 1L;
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/FilterOperation.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/FilterOperation.java
new file mode 100644
index 0000000..edc1f69
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/FilterOperation.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+/**
+ * Standard filter operations
+ */
+public enum FilterOperation {
+  contains, equals, startsWith, present
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/SortOrder.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/SortOrder.java
new file mode 100644
index 0000000..7b52b59
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/model/SortOrder.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+/**
+ * Common sort order definitions
+ */
+public enum SortOrder {
+  ascending, descending
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/CommonsFormDataItem.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/CommonsFormDataItem.java
new file mode 100644
index 0000000..85e66c0
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/CommonsFormDataItem.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.multipart;
+
+import org.apache.commons.fileupload.FileItem;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Implementation of FormDataItem using Apache commons FileItem.
+ */
+class CommonsFormDataItem implements FormDataItem {
+  private final FileItem fileItem;
+
+  CommonsFormDataItem(FileItem fileItem) {
+    this.fileItem = fileItem;
+  }
+
+  public byte[] get() {
+    return fileItem.get();
+  }
+
+  public String getAsString() {
+    return fileItem.getString();
+  }
+
+  public String getContentType() {
+    return fileItem.getContentType();
+  }
+
+  public String getFieldName() {
+    return fileItem.getFieldName();
+  }
+
+  public InputStream getInputStream() throws IOException {
+    return fileItem.getInputStream();
+  }
+
+  public String getName() {
+    return fileItem.getName();
+  }
+
+  public long getSize() {
+    return fileItem.getSize();
+  }
+
+  public boolean isFormField() {
+    return fileItem.isFormField();
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/DefaultMultipartFormParser.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/DefaultMultipartFormParser.java
new file mode 100644
index 0000000..c370d84
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/DefaultMultipartFormParser.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.multipart;
+
+import org.apache.commons.fileupload.FileItem;
+import org.apache.commons.fileupload.FileItemFactory;
+import org.apache.commons.fileupload.FileUploadException;
+import org.apache.commons.fileupload.disk.DiskFileItemFactory;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+
+import java.io.IOException;
+import java.net.UnknownServiceException;
+import java.util.Collection;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Implementation of MultipartFormParser using Apache Commons file upload.
+ */
+public class DefaultMultipartFormParser implements MultipartFormParser {
+  private static final String MULTIPART = "multipart/";
+
+  public Collection<FormDataItem> parse(HttpServletRequest servletRequest)
+      throws IOException  {
+    FileItemFactory factory = new DiskFileItemFactory();
+    ServletFileUpload upload = new ServletFileUpload(factory);
+
+    try {
+      @SuppressWarnings("unchecked")
+      List<FileItem> fileItems = upload.parseRequest(servletRequest);
+      return convertToFormData(fileItems);
+    } catch (FileUploadException e) {
+      UnknownServiceException use = new UnknownServiceException("File upload error.");
+      use.initCause(e);
+      throw use;
+    }
+  }
+
+  private Collection<FormDataItem> convertToFormData(List<FileItem> fileItems) {
+    List<FormDataItem> formDataItems =
+        Lists.newArrayListWithCapacity(fileItems.size());
+    for (FileItem item : fileItems) {
+      formDataItems.add(new CommonsFormDataItem(item));
+    }
+
+    return formDataItems;
+  }
+
+  public boolean isMultipartContent(HttpServletRequest request) {
+    if (!"POST".equals(request.getMethod())) {
+      return false;
+    }
+    String contentType = request.getContentType();
+    if (contentType == null) {
+      return false;
+    }
+    return contentType.toLowerCase().startsWith(MULTIPART);
+  }
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/FormDataItem.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/FormDataItem.java
new file mode 100644
index 0000000..c00229a
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/FormDataItem.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.multipart;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Interface to represent an field item in multipart/form-data.
+ */
+public interface FormDataItem {
+
+  /**
+   * Returns the Content type of the field item.
+   *
+   * @return content type
+   */
+  String getContentType();
+
+  /**
+   * The size of the content stored in this field item.
+   *
+   * @return size of the content
+   */
+  long getSize();
+
+  /**
+   * Returns an InputStream from which the content of the field item can be
+   * read.
+   *
+   * @return InputStream to the content of the field item.
+   * @throws IOException
+   */
+  InputStream getInputStream() throws IOException;
+
+  /**
+   * Returns the content of the field item.
+   *
+   * @return content of the field item
+   */
+  byte[] get();
+
+  /**
+   * Returns the content of the field item as text.
+   *
+   * @return content of the field item as text
+   */
+  String getAsString();
+
+  /**
+   * Name of the uploaded file, if the item represents file upload.
+   * This will be only valid when {@link #isFormField()} returns false.
+   *
+   * @return name of the uploaded file
+   */
+  String getName();
+
+  /**
+   * Field name of this field item. Can be used to identify a field by name but
+   * as per RFC this need not be unique.
+   *
+   * @return name of the field
+   */
+  String getFieldName();
+
+  /**
+   * Used to identify if the field item represents a file upload or a regular
+   * form field.
+   *
+   * @return true if it is a regular form field
+   */
+  boolean isFormField();
+}
diff --git a/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/MultipartFormParser.java b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/MultipartFormParser.java
new file mode 100644
index 0000000..a3be1ac
--- /dev/null
+++ b/trunk/java/common/src/main/java/org/apache/shindig/protocol/multipart/MultipartFormParser.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.multipart;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Class providing a facade over multipart form handling.
+ */
+@ImplementedBy(DefaultMultipartFormParser.class)
+public interface MultipartFormParser {
+  /** Parse a request into a list of data items */
+  Collection<FormDataItem> parse(HttpServletRequest request) throws IOException;
+
+  /**
+   * @return true if the request requires multipart parsing.
+   */
+  boolean isMultipartContent(HttpServletRequest request);
+}
diff --git a/trunk/java/common/src/main/resources/cache.ccf b/trunk/java/common/src/main/resources/cache.ccf
new file mode 100644
index 0000000..e2293d2
--- /dev/null
+++ b/trunk/java/common/src/main/resources/cache.ccf
@@ -0,0 +1,31 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+jcs.default=
+jcs.default.cacheattributes=org.apache.jcs.engine.CompositeCacheAttributes
+jcs.default.cacheattributes.MaxObjects=10000
+jcs.default.cacheattributes.MemoryCacheName=org.apache.jcs.engine.memory.lru.LRUMemoryCache
+jcs.default.cacheattributes.UseMemoryShrinker=false
+jcs.default.cacheattributes.MaxMemoryIdleTimeSeconds=3600
+jcs.default.cacheattributes.ShrinkerIntervalSeconds=60
+jcs.default.elementattributes=org.apache.jcs.engine.ElementAttributes
+jcs.default.elementattributes.IsEternal=false
+jcs.default.elementattributes.MaxLifeSeconds=21600
+jcs.default.elementattributes.IdleTime=1800
+jcs.default.elementattributes.IsSpool=true
+jcs.default.elementattributes.IsRemote=true
+jcs.default.elementattributes.IsLateral=true
diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/cache/ehcache/SizeOfFilter.txt b/trunk/java/common/src/main/resources/org/apache/shindig/common/cache/ehcache/SizeOfFilter.txt
new file mode 100644
index 0000000..b6548fb
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/cache/ehcache/SizeOfFilter.txt
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# Instead of using the @IgnoreSizeOf annotation to mark resources that should be ignored by
+# EhCache's sizeof engine, one can list those references in this file.
+# See http://ehcache.org/documentation/configuration/cache-size#sizing-of-cached-entries
+
+# FeatureBundle and FeatureResources that are shared and should be ignored.
+org.apache.shindig.gadgets.js.JsContent.bundle
+org.apache.shindig.gadgets.js.JsContent.resource
+
+# This is the injected singleton instance of the HttpFetcher.
+org.apache.shindig.gadgets.features.FeatureResourceLoader$UriResource.fetcher
diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/cache/ehcache/ehcacheConfig.xml b/trunk/java/common/src/main/resources/org/apache/shindig/common/cache/ehcache/ehcacheConfig.xml
new file mode 100644
index 0000000..1d2070a
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/cache/ehcache/ehcacheConfig.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+
+<!--
+  The one and only cache manager for Shindig.
+
+  Allot a modest amount of heap to be shared by all caches.
+
+  By default only the CompiledJs caches will overflow to disk.  No caches will persist
+  to disk by default as a development aid.  It is recommended that deployments tune
+  caches to their needs.
+
+  Statistics are turned on for every cache by default.  This affects cache performance.
+  To turn stats off you can use shindig.cache.ehcache.jmx.stats in shindig.properties.
+
+  See http://ehcache.org/ehcache.xsd for more information on valid configuration.
+-->
+<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:noNamespaceSchemaLocation="ehcache.xsd"
+  updateCheck="false"
+  maxBytesLocalHeap="50m"
+  name="ShindigCM">
+
+  <!--
+    The directory where any caches configured as diskPersistent or overflowToDisk
+    will end up.
+  -->
+  <diskStore path="ehcache.disk.store.dir"/>
+
+  <!-- Default sizeOfPolicy for computing the size of cache elements in memory -->
+  <sizeOfPolicy maxDepth="1000" maxDepthExceededBehavior="abort"/>
+
+
+  <!--
+    Mandatory Default Cache configuration. These settings will be applied to caches
+    created programmatically using CacheManager.add(String cacheName).
+
+    The defaultCache has an implicit name "default" which is a reserved cache name.
+  -->
+  <defaultCache
+    statistics="true"
+    eternal="false"
+    timeToIdleSeconds="300"
+    timeToLiveSeconds="600"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!--
+    Compiled feature code, 10Mb disk overflow
+  -->
+  <cache name="CompiledJs"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="true"
+    diskPersistent="false"
+    maxBytesLocalDisk="10m"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!--
+    gadget specs and message bundles have additional caching policies that
+    allow us to cache the objects indefinitely when using an LFU or LRU cache.
+  -->
+  <cache name="gadgetSpecs"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <cache name="messageBundles"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!-- Used to cache parsed HTML DOMs based on their content -->
+  <cache name="parsedDocuments"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!-- Used to cache parsed CSS DOMs based on their content -->
+  <cache name="parsedCss"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!--
+    This configuration is only suitable for a modest sized HTTP cache.
+    You should configure a shared cache for production use.
+    Give this cache 30% of the local heap by default.
+  -->
+  <cache name="httpResponses"
+    maxBytesLocalHeap="30%"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!-- Used to cache parsed expressions based on their content -->
+  <cache name="expressions"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!-- Used to cache cajoled modules based on their content -->
+  <cache name="cajoledModules"
+    statistics="true"
+    eternal="false"
+    timeToIdleSeconds="300"
+    timeToLiveSeconds="600"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU"/>
+
+  <!-- Used to cache Gadget Feature JS -->
+  <cache name="FeatureJsCache"
+    statistics="true"
+    eternal="true"
+    overflowToDisk="false"
+    diskPersistent="false"
+    memoryStoreEvictionPolicy="LFU">
+    <!--
+      The elements stored in this cache are complex and the default sizeOfPolicy
+      is insufficient
+    -->
+    <sizeOfPolicy maxDepth="5000" maxDepthExceededBehavior="abort"/>
+  </cache>
+</ehcache>
diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource.properties
new file mode 100644
index 0000000..83d9ec8
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource.properties
@@ -0,0 +1,180 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.  
+
+
+##AuthenticationServletFilter
+errorParsingSecureToken=The security token or credential is malformed and cannot be parsed.
+
+##XmlUtil
+errorParsingXML=Error parsing the XML. This can be ignored.
+errorParsingExternalGeneralEntities=The XML processor being used will load external general entities.
+errorParsingExternalParameterEntities=The XML processor being used will load external parameter entities.
+errorParsingExternalDTD=The XML processor being used will load Document Type Definitions (DTD).
+errorNotUsingSecureXML=The XML processor being used does not support secure parsing.
+reuseDocumentBuilders=Document builders are being reused.
+notReuseDocBuilders=Document builders are not being reused.
+
+##LruCacheProvider
+LRUCapacity=The least recently used (LRU) capacity {0} is configured for {1}.
+
+##DynamicConfigProperty
+evalExpressionFailed={0} cannot be evaluated.
+
+##JsonContainerConfigLoader
+readingContainerConfig=Container configuration {0} is being read.
+loadFromString={0} cannot be parsed.
+
+##JsonContainerConfigLoader, FeatureRegistry
+loadResourcesFrom=Resources from {0} are loading.
+
+##JsonContainerConfigLoader, FeatureRegistry
+loadFilesFrom=Files from {0} are loading.
+
+##ApiServlet
+apiServletProtocolException=A response error is being returned because a protocol exception occurred.
+apiServletException=A response error is being returned because a protocol exception occurred.occurred.
+
+##FeatureRegistry
+overridingFeature=The {0} feature with definition at {1} is being overridden.
+
+##FeatureResourceLoader
+missingFile=The file {0} used to exist but is now missing.
+unableRetrieveLib=The remote library from {0} cannot be retrieved.
+
+##BasicHttpFetcher
+timeoutException={0} has timed out because of the following exception: {1} - {2} - {3} ms.
+exceptionOccurred=The following exception occurred when fetching {0}: {1} ms elapsed.
+slowResponse={0} is responding slowly. {1} ms elapsed.
+
+##HttpResponseMetadataHelper
+errorGettingMD5=An error occurred when getting Message Digest 5 (MD5). The error was ignored.
+errorParsingMD5=An error occurred when parsing the Message Digest 5 (MD5) string in UTF-8 format.
+ 
+##OAuthModule     
+usingRandomKey=A random key for OAuth client-side state encryption is being used.
+usingFile=The {0} file for OAuth client-side state encryption is being used.
+loadKeyFileFrom=The OAuth signing key from {0} is loading.
+couldNotLoadKeyFile= The {0} key file could not be loaded.
+couldNotLoadSignedKey=The OAuth signing key did not load correctly. To create a key: \n 1. Run the following command: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj '/CN=mytestkey'\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Edit the shindig.properties file by adding these lines: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.
+failedToInit=OAuth consumers from {0} failed to initialize.
+
+##OAuthRequest
+oauthFetchFatalError=The following fatal error occurred when OAuth was fetching content: \n {0}.
+oauthFetchErrorReprompt=The following error occurred when OAuth was fetching content: \n {0}. The user is being reprompted for approval.
+bogusExpired=The server returned an invalid expiration:\n {0}.
+oauthFetchUnexpectedError=The following fatal error occurred when OAuth was fetching content: \n {0}.
+unauthenticatedOauth=OAuth cannot fetch content because the user authentication does not exist. The following error occurred: \n {0}.
+invalidOauth=OAuth cannot fetch content because the request is invalid. The following error occurred: \n {0}.
+
+##CajaCssSanitizer
+failedToParse=The style sheet cannot be parsed.
+unableToConvertScript=The script node cannot be converted to an OpenSocial Markup Language (OSML) tag.
+
+##PipelineExecutor
+errorPreloading=An unexpected error occurred when preloading.
+
+##Processor
+renderBlacklistedGadget=The system attempted to render the following blacklisted gadget: {0}.
+
+##CajaResponseRewriter, CajaContentRewriter
+failedToRetrieve={0} cannot be retrieved.
+failedToRead={0} cannot be read.
+
+##DefaultServiceFetcher
+httpErrorFetching=An HTTP {0} error occurred when fetching service methods from the {1} endpoint.
+failedToFetchService=Services methods from the {0} endpoint could not be fetched. The following error occurred: {1}.
+failedToParseService=Services methods from the {0} endpoint could not be parsed. The following error occurred: {1}.
+
+##Renderer
+FailedToRender=The gadget at {0} did not render. The following error occurred: {1}.
+
+##RenderingGadgetRewriter
+unknownFeatures=One or more unknown features exist in the following extern &libs=: {0}.
+unexpectedErrorPreloading=An unexpected error occurred when preloading the gadget.
+
+##SanitizingResponseRewriter
+requestToSanitizeWithoutContent=A request to sanitize was issued without a content type for {0}.
+requestToSanitizeUnknownContent=A request to sanitize was issued without a known content type {0} for {1}.
+unableToSanitizeUnknownImg=The image type {0} is unknown and cannot be sanitized.
+unableToDetectImgType=The image type for {0} cannot be detected when sanitizing content.
+
+##BasicImageRewriter
+ioErrorRewritingImg=The following input/output error occurred when rewriting the {0} image: {1}.
+unknownErrorRewritingImg=The following error occurred when rewriting the {0} image: {1}.
+failedToReadImg=The {0} image cannot be read and is being skipped. The following error occurred: {1}.
+
+##CssResponseRewriter
+cajaCssParseFailure=The following error occurred when parsing the Caja CSS: {0} for {1}.
+
+##ImageAttributeRewriter
+unableToProcessImg=The {0} image resource cannot be processed.
+unableToReadResponse=The response for the {0} image resource cannot be read.
+unableToFetchImg=The {0} image resource cannot be fetched.
+unableToParseImg=The {0} image resource cannot be parsed.
+
+##MutableContent
+exceptionParsingContent=An exception occurred when parsing content for the gadget.
+
+##PipelineDataGadgetRewriter
+failedToParsePreload=The gadget at {0} could not be parsed for preloading.
+
+##ProxyingVisitor
+uriExceptionParsing=A Uniform Resource Identifier (URI) exception occurred when parsing the gadget at {0}.
+
+##TemplateRewriter
+malformedTemplateLib=Exceptions occurred because of malformed template libraries.
+
+##CajaContnetRewriter
+cajoledCacheCreated=A cajoled cache was created.
+retrieveReference={0} is being retrieved.
+unableToCajole=The gadget at {0} cannot be cajoled.
+
+##ConcatProxyServlet
+concatProxyRequestFailed=The following error occurred when requesting a concatenated proxy: {0}.
+
+##GadgetRenderingServlet
+malformedTtlValue=An invalid Time To Live (TTL) value of {0} was ignored.
+
+##HttpRequestHandler
+gadgetCreationError=An error occurred creating the gadget for rewriting.  Rewriting the response without the gadget.
+
+##ProxyServlet
+embededImgWrongDomain=The request to embed the {0} URL was made to the wrong domain {1}.
+
+##DefaultTemplateProcessor
+elFailure=The following EL error occurred for the gadget at {0}: {1}.
+
+##UriUtils
+skipIllegalHeader=The {0} header is illegal and is being skipped. The following error occurred: {1}.
+
+##AbstractSpecFactory
+updateSpecFailureUseCacheVersion=An error occurred when updating {0}. Status code {1} was returned. Exception: {2}. A cached version is being used instead.
+updateSpecFailureApplyNegCache=An error occurred when updating {0}. Status code {1} was returned. Exception: {2}. A negative cache is being applied.
+
+##HashLockedDomainService
+noLockedDomainConfig=A locked domain configuration for {0} does not exist.
+
+##Bootstrap
+startingConnManagerWith=Connection manager with {0} properties is starting.
+
+##XSDValidator
+resolveResource=The following resources are being resolved: {0}, {1}, {2}, {3}.
+failedToValidate=An error occurred when validating {0}.
+
+##DefaultRequestPipeline
+cachedResponse=Returning cached response for the request to {0}.
+staleResponse="There was an error requesting the resource at {0} but we have a prior response in the cache.  Returning a possibly stale response from the cache.
diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ar.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ar.properties
new file mode 100644
index 0000000..6c2fbf4
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ar.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u064a\u0648\u062c\u062f \u062a\u0644\u0641 \u0641\u064a \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u0633\u0631\u064a\u0629 \u0623\u0648 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0627\u0639\u062a\u0645\u0627\u062f \u0648\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u0644\u064a\u0644\u0647\u0627 \u0644\u063a\u0648\u064a\u0627.

+

+##XmlUtil

+errorParsingXML=\u062d\u062f\u062b \u062e\u0637\u0623 \u0641\u064a \u062a\u062d\u0644\u064a\u0644 XML \u0644\u063a\u0648\u064a\u0627. \u0633\u064a\u062a\u0645 \u062a\u062c\u0627\u0647\u0644 \u0630\u0644\u0643.

+errorParsingExternalGeneralEntities=\u0633\u064a\u0642\u0648\u0645 \u0645\u0639\u0627\u0644\u062c XML \u0627\u0644\u0630\u064a \u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647 \u0628\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0639\u0627\u0645\u0629 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629.

+errorParsingExternalParameterEntities=\u0633\u064a\u0642\u0648\u0645 \u0645\u0639\u0627\u0644\u062c XML \u0627\u0644\u0630\u064a \u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647 \u0628\u062a\u062d\u0645\u064a\u0644 \u0639\u0646\u0627\u0635\u0631 \u0627\u0644\u0645\u0639\u0627\u0645\u0644 \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629.

+errorParsingExternalDTD=\u0633\u064a\u0642\u0648\u0645 \u0645\u0639\u0627\u0644\u062c XML \u0627\u0644\u0630\u064a \u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647 \u0628\u062a\u062d\u0645\u064a\u0644 \u062a\u0639\u0631\u064a\u0641\u0627\u062a \u0646\u0648\u0639 \u0627\u0644\u0648\u062b\u064a\u0642\u0629 \u200f(DTD)\u200f.

+errorNotUsingSecureXML=\u0645\u0639\u0627\u0644\u062c XML \u0627\u0644\u0630\u064a \u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647 \u0644\u0627 \u064a\u062f\u0639\u0645 \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0644\u063a\u0648\u064a \u0627\u0644\u0645\u0624\u0645\u0646.

+reuseDocumentBuilders=\u0633\u064a\u062a\u0645 \u0627\u0639\u0627\u062f\u0629 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0623\u062f\u0648\u0627\u062a \u0628\u0646\u0627\u0621 \u0627\u0644\u0648\u062b\u064a\u0642\u0629.

+notReuseDocBuilders=\u0644\u0646 \u064a\u062a\u0645 \u0627\u0639\u0627\u062f\u0629 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0623\u062f\u0648\u0627\u062a \u0628\u0646\u0627\u0621 \u0627\u0644\u0648\u062b\u064a\u0642\u0629.

+

+##LruCacheProvider

+LRUCapacity=\u062a\u0645 \u062a\u0648\u0635\u064a\u0641 (LRU) \u0628\u0627\u0644\u0642\u062f\u0631\u0629 {0} \u0627\u0644\u0649 {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed=\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u0642\u064a\u064a\u0645 {0}.

+

+##JsonContainerConfigLoader

+readingContainerConfig=\u062c\u0627\u0631\u064a \u0642\u0631\u0627\u0621\u0627\u062a \u062a\u0648\u0635\u064a\u0641 \u0627\u0644\u062d\u0627\u0648\u064a\u0629 {0}.

+loadFromString=\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u0644\u064a\u0644 {0} \u0644\u063a\u0648\u064a\u0627.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=\u062c\u0627\u0631\u064a \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0635\u0627\u062f\u0631 \u0645\u0646 {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=\u062c\u0627\u0631\u064a \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u0641\u0627\u062a \u0645\u0646 {0}.

+

+##ApiServlet

+apiServletProtocolException=\u062c\u0627\u0631\u064a \u0627\u0631\u062c\u0627\u0639 \u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0628\u0633\u0628\u0628 \u062d\u062f\u0648\u062b \u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u0628\u0631\u0648\u062a\u0648\u0643\u0648\u0644.

+apiServletException=\u062c\u0627\u0631\u064a \u0627\u0631\u062c\u0627\u0639 \u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0628\u0633\u0628\u0628 \u062d\u062f\u0648\u062b \u062e\u0637\u0623 \u0641\u064a \u0627\u0644\u0628\u0631\u0648\u062a\u0648\u0643\u0648\u0644.

+

+##FeatureRegistry

+overridingFeature=\u062c\u0627\u0631\u064a \u0627\u062d\u0644\u0627\u0644 \u0627\u0644\u062e\u0627\u0635\u064a\u0629 {0} \u0628\u0627\u0644\u062a\u0639\u0631\u064a\u0641 \u0639\u0644\u0649 {1}.

+

+##FeatureResourceLoader

+missingFile=\u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0645\u0644\u0641 {0} \u0644\u0644\u062e\u0631\u0648\u062c \u0648\u0644\u0643\u0646\u0647 \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f \u062d\u0627\u0644\u064a\u0627.

+unableRetrieveLib=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u0627\u0644\u0645\u0643\u062a\u0628\u0629 \u0639\u0646 \u0628\u0639\u062f \u0645\u0646 {0}.

+

+##BasicHttpFetcher

+timeoutException=\u0627\u0646\u062a\u0647\u0649 \u0648\u0642\u062a {0} \u0628\u0633\u0628\u0628 \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: {1} - {2} - {3} \u0645\u0644\u064a \u062b\u0627\u0646\u064a\u0629.

+exceptionOccurred=\u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0627\u062d\u0636\u0627\u0631 {0}: \u0627\u0646\u0642\u0636\u0649 {1} \u0645\u0644\u064a \u062b\u0627\u0646\u064a\u0629.

+slowResponse=\u0627\u0633\u062a\u062c\u0627\u0628\u0629 {0} \u0628\u0637\u064a\u0626\u0629. \u0627\u0646\u0642\u0636\u0649 {1} \u0645\u0644\u064a \u062b\u0627\u0646\u064a\u0629.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u0627\u062d\u0636\u0627\u0631 Message Digest 5 \u200f(MD5)\u200f. \u062a\u0645 \u062a\u062c\u0627\u0647\u0644 \u0627\u0644\u062e\u0637\u0623.

+errorParsingMD5=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u062a\u062d\u0644\u064a\u0644 Message Digest 5 \u200f(MD5)\u200f \u0644\u063a\u0648\u064a\u0627 \u0628\u062f\u0621\u0627 \u0645\u0646 \u0627\u0644\u0646\u0633\u0642 UTF-8.

+

+##OAuthModule

+usingRandomKey=\u0627\u0644\u0645\u0641\u062a\u0627\u062d \u0627\u0644\u0639\u0634\u0648\u0627\u0626\u064a \u0627\u0644\u0649 OAuth \u0645\u0646 \u062c\u0627\u0646\u0628 \u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 \u064a\u062d\u062f\u062f \u0623\u0646\u0647 \u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u062a\u0634\u0641\u064a\u0631.

+usingFile=\u0627\u0644\u0645\u0644\u0641 {0} \u0627\u0644\u0649 OAuth \u0645\u0646 \u062c\u0627\u0646\u0628 \u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 \u064a\u062d\u062f\u062f \u0623\u0646\u0647 \u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u062a\u0634\u0641\u064a\u0631.

+loadKeyFileFrom=\u062c\u0627\u0631\u064a \u062a\u062d\u0645\u064a\u0644 \u0645\u0641\u062a\u0627\u062d \u062a\u0648\u0642\u064a\u0639 OAuth \u0645\u0646 {0}.

+couldNotLoadKeyFile= \u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0631\u0626\u064a\u0633\u064a {0}.

+couldNotLoadSignedKey=\u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u0645\u064a\u0644 \u0645\u0641\u062a\u0627\u062d \u062a\u0648\u0642\u064a\u0639 OAuth \u0628\u0637\u0631\u064a\u0642\u0629 \u0635\u062d\u064a\u062d\u0629. \u0644\u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u0645\u0641\u062a\u0627\u062d: \n 1. \u0642\u0645 \u0628\u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0623\u0645\u0631 \u0627\u0644\u062a\u0627\u0644\u064a: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. \u200f2.\u200f \u0642\u0645 \u0628\u062a\u062d\u0631\u064a\u0631 \u0627\u0644\u0645\u0644\u0641 \u200f\u200eshindig.properties\u200e\u200f \u0645\u0646 \u062e\u0644\u0627\u0644 \u0627\u0636\u0627\u0641\u0629 \u0647\u0630\u0647 \u0627\u0644\u0623\u0633\u0637\u0631: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=\u0641\u0634\u0644 \u0641\u064a \u0627\u0639\u062f\u0627\u062f \u0645\u0633\u062a\u0647\u0644\u0643\u064a OAuth \u0645\u0646 {0}.

+

+##OAuthRequest

+oauthFetchFatalError=\u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062c\u0633\u064a\u0645 \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0642\u064a\u0627\u0645 OAuth \u0628\u0627\u062d\u0636\u0627\u0631 \u0645\u062d\u062a\u0648\u064a\u0627\u062a: \n {0}.

+oauthFetchErrorReprompt=\u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0642\u064a\u0627\u0645 OAuth \u0628\u0627\u062d\u0636\u0627\u0631 \u0645\u062d\u062a\u0648\u064a\u0627\u062a: \n {0}. \u062c\u0627\u0631\u064a \u0627\u0639\u0627\u062f\u0629 \u062d\u062b \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0644\u0644\u0627\u0639\u062a\u0645\u0627\u062f.

+bogusExpired=\u0642\u0627\u0645\u062a \u0648\u062d\u062f\u0629 \u0627\u0644\u062e\u062f\u0645\u0629 \u0628\u0627\u0631\u062c\u0627\u0639 \u0627\u0646\u062a\u0647\u0627\u0621 \u0635\u0644\u0627\u062d\u064a\u0629 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d:\n {0}.

+oauthFetchUnexpectedError=\u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062c\u0633\u064a\u0645 \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0642\u064a\u0627\u0645 OAuth \u0628\u0627\u062d\u0636\u0627\u0631 \u0645\u062d\u062a\u0648\u064a\u0627\u062a: \n {0}.

+unauthenticatedOauth=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0623\u0646 \u064a\u0642\u0648\u0645 OAuth \u0628\u0627\u062d\u0636\u0627\u0631 \u0627\u0644\u0645\u062d\u062a\u0648\u064a\u0627\u062a \u0644\u0623\u0646 \u062a\u0648\u062b\u064a\u0642 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f. \u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: \n {0}.

+invalidOauth=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0623\u0646 \u064a\u0642\u0648\u0645 OAuth \u0628\u0627\u062d\u0636\u0627\u0631 \u0627\u0644\u0645\u062d\u062a\u0648\u064a\u0627\u062a \u0644\u0623\u0646 \u0627\u0644\u0637\u0644\u0628 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d. \u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u0644\u064a\u0644 \u0635\u0641\u062d\u0629 \u0627\u0644\u0623\u0646\u0645\u0627\u0637 \u0644\u063a\u0648\u064a\u0627.

+unableToConvertScript=\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u062d\u0648\u064a\u0644 \u0639\u0642\u062f\u0629 \u0627\u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0646\u0635\u064a \u0627\u0644\u0649 \u0634\u0627\u0631\u0629 OpenSocial Markup Language \u200f(OSML)\u200f.

+

+##PipelineExecutor

+errorPreloading=\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639 \u0639\u0646\u062f \u0627\u0644\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0633\u0628\u0642.

+

+##Processor

+renderBlacklistedGadget=\u062d\u0627\u0648\u0644 \u0627\u0644\u0646\u0638\u0627\u0645 \u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0623\u062f\u0648\u0627\u062a \u0627\u0644\u0645\u062d\u0638\u0648\u0631\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0633\u062a\u0631\u062c\u0627\u0639 {0}.

+failedToRead=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0642\u0631\u0627\u0621\u0629 {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=\u062d\u062f\u062b \u062e\u0637\u0623 HTTP {0} \u0639\u0646\u062f \u0627\u062d\u0636\u0627\u0631 \u0637\u0631\u0642 \u0627\u0644\u062e\u062f\u0645\u0629 \u0645\u0646 \u0627\u0644\u0646\u0642\u0637\u0629 \u0627\u0644\u0637\u0631\u0641\u064a\u0629 {1}.

+failedToFetchService=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u062d\u0636\u0627\u0631 \u0637\u0631\u0642 \u0627\u0644\u062e\u062f\u0645\u0627\u062a \u0645\u0646 \u0627\u0644\u0646\u0642\u0637\u0629 \u0627\u0644\u0637\u0631\u0641\u064a\u0629 {0}. \u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: {1}.

+failedToParseService=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0644\u063a\u0648\u064a \u0644\u0637\u0631\u0642 \u0627\u0644\u062e\u062f\u0645\u0627\u062a \u0645\u0646 \u0627\u0644\u0646\u0642\u0637\u0629 \u0627\u0644\u0637\u0631\u0641\u064a\u0629 {0}. \u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: {1}.

+

+##Renderer

+FailedToRender=\u0644\u0645 \u064a\u062a\u0645 \u062a\u062d\u0648\u064a\u0644 \u0627\u0644\u0623\u062f\u0627\u0629 \u0639\u0644\u0649 {0} \u0628\u064a\u0627\u0646\u064a\u0627. \u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=\u0647\u0646\u0627\u0643 \u0648\u0627\u062d\u062f\u0629 \u0623\u0648 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0627\u0644\u062e\u0635\u0627\u0626\u0635 \u0627\u0644\u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641\u0629 \u0641\u064a &libs= \u0627\u0644\u062e\u0627\u0631\u062c\u064a\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629: {0}.

+unexpectedErrorPreloading=\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u062a\u0648\u0642\u0639 \u0639\u0646\u062f \u0627\u0644\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0633\u0628\u0642 \u0644\u0644\u0623\u062f\u0627\u0629.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u062a\u0645 \u0627\u0635\u062f\u0627\u0631 \u0637\u0644\u0628 \u0627\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0646\u0638\u064a\u0645 \u0628\u062f\u0648\u0646 \u0646\u0648\u0639 \u0645\u062d\u062a\u0648\u064a\u0627\u062a {0}.

+requestToSanitizeUnknownContent=\u062a\u0645 \u0627\u0635\u062f\u0627\u0631 \u0637\u0644\u0628 \u0627\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0646\u0638\u064a\u0645 \u0628\u062f\u0648\u0646 \u0646\u0648\u0639 \u0645\u062d\u062a\u0648\u064a\u0627\u062a \u0645\u0639\u0631\u0648\u0641 {0} \u0627\u0644\u0649 {1}.

+unableToSanitizeUnknownImg=\u0646\u0648\u0639 \u0627\u0644\u0635\u0648\u0631\u0629 {0} \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641 \u0648\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0639\u0627\u062f\u0629 \u062a\u0646\u0638\u064a\u0645\u0647.

+unableToDetectImgType=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0643\u062a\u0634\u0627\u0641 \u0646\u0648\u0639 \u0627\u0644\u0635\u0648\u0631\u0629 \u0627\u0644\u0649 {0} \u0639\u0646\u062f \u0627\u0639\u0627\u062f\u0629 \u062a\u0646\u0638\u064a\u0645 \u0627\u0644\u0645\u062d\u062a\u0648\u064a\u0627\u062a.

+

+##BasicImageRewriter

+ioErrorRewritingImg=\u062d\u062f\u062b \u062e\u0637\u0623 \u0627\u0644\u0645\u062f\u062e\u0644\u0627\u062a/\u0627\u0644\u0645\u062e\u0631\u062c\u0627\u062a \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0627\u0639\u0627\u062f\u0629 \u0643\u062a\u0627\u0628\u0629 \u0635\u0648\u0631\u0629 {0}: {1}.

+unknownErrorRewritingImg=\u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0627\u0639\u0627\u062f\u0629 \u0643\u062a\u0627\u0628\u0629 \u0635\u0648\u0631\u0629 {0}: {1}.

+failedToReadImg=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0635\u0648\u0631\u0629 {0} \u0648\u062c\u0627\u0631\u064a \u062a\u062e\u0637\u064a\u0647\u0627. \u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=\u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0644\u063a\u0648\u064a \u0627\u0644\u0649 Caja CSS: {0} \u0627\u0644\u0649 {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=\u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u0634\u063a\u064a\u0644 \u0645\u0635\u062f\u0631 \u0627\u0644\u0635\u0648\u0631\u0629 {0}.

+unableToReadResponse=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0644\u0645\u0635\u062f\u0631 \u0627\u0644\u0635\u0648\u0631\u0629 {0}.

+unableToFetchImg=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u062d\u0636\u0627\u0631 \u0645\u0635\u062f\u0631 \u0627\u0644\u0635\u0648\u0631\u0629 {0}.

+unableToParseImg=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0644\u063a\u0648\u064a \u0644\u0645\u0635\u062f\u0631 \u0627\u0644\u0635\u0648\u0631\u0629 {0}.

+

+##MutableContent

+exceptionParsingContent=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0644\u063a\u0648\u064a \u0644\u0645\u062d\u062a\u0648\u064a\u0627\u062a \u0627\u0644\u0623\u062f\u0627\u0629.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0644\u063a\u0648\u064a \u0644\u0644\u0623\u062f\u0627\u0629 \u0639\u0644\u0649 {0} \u0644\u0644\u062a\u062d\u0645\u064a\u0644 \u0627\u0644\u0645\u0633\u0628\u0642.

+

+##ProxyingVisitor

+uriExceptionParsing=\u062d\u062f\u062b \u062e\u0637\u0623 \u0641\u064a Uniform Resource Identifier \u200f(URI)\u200f \u0639\u0646\u062f \u0627\u0644\u062a\u062d\u0644\u064a\u0644 \u0627\u0644\u0644\u063a\u0648\u064a \u0644\u0644\u0623\u062f\u0627\u0629 \u0639\u0644\u0649 {0}.

+

+##TemplateRewriter

+malformedTemplateLib=\u062d\u062f\u062b\u062a \u0623\u062e\u0637\u0627\u0621 \u0628\u0633\u0628\u0628 \u062a\u0644\u0641 \u0641\u064a \u0645\u0643\u062a\u0628\u0627\u062a \u0627\u0644\u0642\u0627\u0644\u0628.

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u062a\u0645 \u062a\u0643\u0648\u064a\u0646 \u0630\u0627\u0643\u0631\u0629 \u062a\u062e\u0632\u064a\u0646 \u0645\u0624\u0642\u062a \u0632\u0627\u0626\u0641\u0629.

+retrieveReference=\u062c\u0627\u0631\u064a \u0627\u0633\u062a\u0631\u062c\u0627\u0639 {0}.

+unableToCajole=\u0627\u0644\u0623\u062f\u0627\u0629 \u0639\u0644\u0649 {0} \u0644\u0627 \u064a\u0645\u0643\u0646 \u0623\u0646 \u062a\u0643\u0648\u0646 \u0632\u0627\u0626\u0641\u0629.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a \u0639\u0646\u062f \u0637\u0644\u0628 proxy \u0645\u062a\u0633\u0644\u0633\u0644: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=\u062a\u0645 \u062a\u062c\u0627\u0647\u0644 \u0642\u064a\u0645\u0629 \u200f(TTL)\u200f \u0628\u0627\u0644\u0642\u064a\u0645\u0629 {0}.

+

+##HttpRequestHandler

+gadgetCreationError=\u062d\u062f\u062b \u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u062a\u0643\u0648\u064a\u0646 \u0627\u0644\u0623\u062f\u0627\u0629 \u0644\u0627\u0639\u0627\u062f\u0629 \u0627\u0644\u0643\u062a\u0627\u0628\u0629.  \u0633\u064a\u062a\u0645 \u0627\u0639\u0627\u062f\u0629 \u0643\u062a\u0627\u0628\u0629 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0628\u062f\u0648\u0646 \u0627\u0644\u0623\u062f\u0627\u0629.

+

+##ProxyServlet

+embededImgWrongDomain=\u062a\u0645 \u0639\u0645\u0644 \u0637\u0644\u0628 \u0628\u062a\u0636\u0645\u064a\u0646 {0} URL \u0641\u064a \u0646\u0637\u0627\u0642 \u062e\u0637\u0623 {1}.

+

+##DefaultTemplateProcessor

+elFailure=\u062d\u062f\u062b \u062e\u0637\u0623 EL \u0627\u0644\u062a\u0627\u0644\u064a \u0644\u0644\u0623\u062f\u0627\u0629 \u0639\u0644\u0649 {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=\u0627\u0644\u0639\u0646\u0648\u0627\u0646 {0} \u063a\u064a\u0631 \u0635\u062d\u064a\u062d \u0648\u062c\u0627\u0631\u064a \u062a\u062e\u0637\u064a\u0647. \u062d\u062f\u062b \u0627\u0644\u062e\u0637\u0623 \u0627\u0644\u062a\u0627\u0644\u064a: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u062a\u062d\u062f\u064a\u062b {0}. \u062a\u0645 \u0627\u0631\u062c\u0627\u0639 \u0643\u0648\u062f \u0627\u0644\u062d\u0627\u0644\u0629 {1}. \u0627\u0633\u062a\u062b\u0646\u0627\u0621: {2}. \u0633\u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0627\u0644\u0646\u0633\u062e\u0629 \u0627\u0644\u0645\u0633\u062c\u0644\u0629 \u0641\u064a \u0627\u0644\u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u0648\u0633\u064a\u0637\u0629 \u0628\u062f\u0644\u0627 \u0645\u0646\u0647\u0627.

+updateSpecFailureApplyNegCache=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u062a\u062d\u062f\u064a\u062b {0}. \u062a\u0645 \u0627\u0631\u062c\u0627\u0639 \u0643\u0648\u062f \u0627\u0644\u062d\u0627\u0644\u0629 {1}. \u0627\u0633\u062a\u062b\u0646\u0627\u0621: {2}. \u062c\u0627\u0631\u064a \u062a\u0637\u0628\u064a\u0642 \u0627\u0644\u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u0648\u0633\u064a\u0637\u0629 \u0627\u0644\u0633\u0644\u0628\u064a\u0629.

+

+##HashLockedDomainService

+noLockedDomainConfig=\u062a\u0648\u0635\u064a\u0641 \u0627\u0644\u0646\u0637\u0627\u0642 \u0627\u0644\u0645\u0624\u0645\u0646 \u0627\u0644\u0649 {0} \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f.

+

+##Bootstrap

+startingConnManagerWith=\u062c\u0627\u0631\u064a \u0628\u062f\u0621 Connection manager \u0644\u0644\u062e\u0635\u0627\u0626\u0635 {0}.

+

+##XSDValidator

+resolveResource=\u062c\u0627\u0631\u064a \u062d\u0644 \u0627\u0644\u0645\u0635\u0627\u062f\u0631 \u0627\u0644\u062a\u0627\u0644\u064a\u0629: {0}\u060c {1}\u060c {2}\u060c {3}.

+failedToValidate=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u0627\u0644\u062a\u062d\u0642\u0642 \u0645\u0646 {0}.

+

+##DefaultRequestPipeline

+cachedResponse=\u062c\u0627\u0631\u064a \u0627\u0631\u062c\u0627\u0639 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0627\u0644\u062a\u064a \u062a\u0645 \u062a\u0633\u062c\u064a\u0644\u0647\u0627 \u0641\u064a \u0627\u0644\u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u0648\u0633\u064a\u0637\u0629 \u0644\u0644\u0637\u0644\u0628 \u0627\u0644\u0649 {0}.

+staleResponse="\u062d\u062f\u062b \u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u0637\u0644\u0628 \u0627\u0644\u0645\u0635\u062f\u0631 \u0641\u064a {0} \u0648\u0644\u0643\u0646 \u0647\u0646\u0627\u0643 \u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0633\u0627\u0628\u0642\u0629 \u0641\u064a \u0627\u0644\u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u0648\u0633\u064a\u0637\u0629.  \u064a\u062a\u0645 \u0627\u0631\u062c\u0627\u0639 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0627\u0644\u0642\u062f\u064a\u0645\u0629 \u0627\u0644\u0645\u062a\u0648\u0642\u0639\u0629 \u0645\u0646 \u0627\u0644\u0630\u0627\u0643\u0631\u0629 \u0627\u0644\u0648\u0633\u064a\u0637\u0629.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ca.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ca.properties
new file mode 100644
index 0000000..aed5b82
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ca.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=El testimoni o la credencial de seguretat no s'ha format correctament i no es pot analitzar.

+

+##XmlUtil

+errorParsingXML=Error en analitzar l'XML. Es pot ignorar.

+errorParsingExternalGeneralEntities=El processador XML que s'est\u00e0 utilitzant carregar\u00e0 entitats generals externes.

+errorParsingExternalParameterEntities=El processador XML que s'est\u00e0 utilitzant carregar\u00e0 entitats de par\u00e0metre extern.

+errorParsingExternalDTD=El processador XML que s'est\u00e0 utilitzant carregar\u00e0 DTD (Document Type Definitions).

+errorNotUsingSecureXML=El processador XML que s'est\u00e0 utilitzant no admet l'an\u00e0lisi segur.

+reuseDocumentBuilders=S'estan tornant a utilitzar els generadors de documents.

+notReuseDocBuilders=No s'estan tornant a utilitzar els generadors de documents.

+

+##LruCacheProvider

+LRUCapacity=La capacitat LRU (least recently used) {0} s''ha configurat per a {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} no es pot avaluar.

+

+##JsonContainerConfigLoader

+readingContainerConfig=S''est\u00e0 llegint la configuraci\u00f3 del contenidor {0}.

+loadFromString={0} no es pot analitzar.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=S''estan carregant recursos des de {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=S''estan carregant fitxers des de {0}.

+

+##ApiServlet

+apiServletProtocolException=S'ha tornat un error de resposta perqu\u00e8 s'ha produ\u00eft una excepci\u00f3 de protocol.

+apiServletException=S'ha tornat un error de resposta perqu\u00e8 s'ha produ\u00eft una excepci\u00f3 de protocol.

+

+##FeatureRegistry

+overridingFeature=S''est\u00e0 alterant temporalment la funci\u00f3 {0} amb definici\u00f3 a {1}.

+

+##FeatureResourceLoader

+missingFile=El fitxer {0} existia per\u00f2 ara falta.

+unableRetrieveLib=No es pot recuperar la biblioteca remota des de {0}.

+

+##BasicHttpFetcher

+timeoutException={0} ha superat el temps d''espera a causa de l''excepci\u00f3 seg\u00fcent: {1} - {2} - {3} ms.

+exceptionOccurred=S''ha produ\u00eft l''excepci\u00f3 seg\u00fcent mentre s''obtenia {0}: {1} ms transcorreguts.

+slowResponse={0} est\u00e0 responent a poc a poc. {1} ms transcorreguts.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=S'ha produ\u00eft un error en obtenir el resum de missatge 5 (MD5). L'error s'ha ignorat.

+errorParsingMD5=S'ha produ\u00eft un error en analitzar la cadena del resum de missatge 5 (MD5) en format UTF-8.

+

+##OAuthModule

+usingRandomKey=S'est\u00e0 utilitzant un xifratge d'estat de client OAuth.

+usingFile=S''est\u00e0 utilitzant el fitxer {0} de xifratge d''estat de client OAuth.

+loadKeyFileFrom=S''est\u00e0 carregant la clau de signatura OAuth de {0}.

+couldNotLoadKeyFile= No s''ha pogut carregar el fitxer de claus {0}.

+couldNotLoadSignedKey=La clau de signatura OAuth no s''ha carregat correctament. Per crear una clau: \n 1. Executeu les ordres seg\u00fcents: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Editeu el fitxer shindig.properties afegint les l\u00ednies seg\u00fcents: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=No s''han pogut inicialitzar els clients OAuth de {0}.

+

+##OAuthRequest

+oauthFetchFatalError=S''ha produ\u00eft el seg\u00fcent error fatal quan OAuth estava obtenint contingut: \n {0}.

+oauthFetchErrorReprompt=S''ha produ\u00eft el seg\u00fcent error quan OAuth estava obtenint contingut: \n {0}. S''est\u00e0 tornant a demanar l''aprovaci\u00f3 de l''usuari.

+bogusExpired=El servidor ha tornat una expiraci\u00f3 no v\u00e0lida:\n {0}.

+oauthFetchUnexpectedError=S''ha produ\u00eft un error greu quan OAuth estava obtenint contingut: \n \ {0}.

+unauthenticatedOauth=OAuth no pot obtenir contingut perqu\u00e8 l''autenticaci\u00f3 d''usuari no existeix. S''ha produ\u00eft el seg\u00fcent error: \n {0}.

+invalidOauth=OAuth no pot obtenir contingut perqu\u00e8 la sol\u00b7licitud no \u00e9s v\u00e0lida. S''ha produ\u00eft el seg\u00fcent error: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=El full d'estil no es pot analitzar.

+unableToConvertScript=El node d'script no es pot convertir a una etiqueta OpenSocial Markup Language (OSML).

+

+##PipelineExecutor

+errorPreloading=S'ha produ\u00eft un error inesperat durant la prec\u00e0rrega.

+

+##Processor

+renderBlacklistedGadget=El sistema ha intentat representar el seg\u00fcent gadget de la llista negra: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} no es pot recuperar.

+failedToRead={0} no es pot llegir.

+

+##DefaultServiceFetcher

+httpErrorFetching=S''ha produ\u00eft un error {0} d''HTTP quan s''obtenien m\u00e8todes de servei del punt final de {1}.

+failedToFetchService=No s''han pogut obtenir els m\u00e8todes de serveis del punt final de {0}. S''ha produ\u00eft el seg\u00fcent error: {1}.

+failedToParseService=No s''han pogut analitzar els m\u00e8todes de serveis del punt final de {0}. S''ha produ\u00eft el seg\u00fcent error: {1}.

+

+##Renderer

+FailedToRender=No s''ha representant el gadget a {0}. S''ha produ\u00eft el seg\u00fcent error: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Una o m\u00e9s caracter\u00edstiques desconegudes existeixen al seg\u00fcent &libs= extern: {0}.

+unexpectedErrorPreloading=S'ha produ\u00eft un error inesperat durant la prec\u00e0rrega del gadget.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=S''ha em\u00e8s una sol\u00b7licitud de depuraci\u00f3 sense el tipus de contingut de {0}.

+requestToSanitizeUnknownContent=S''ha em\u00e8s una sol\u00b7licitud de depuraci\u00f3 sense el tipus de contingut {0} de {1}.

+unableToSanitizeUnknownImg=El tipus d''imatge {0} \u00e9s desconegut i no es pot depurar.

+unableToDetectImgType=El tipus d''imatge de {0} no es pot detectar quan es depura el contingut.

+

+##BasicImageRewriter

+ioErrorRewritingImg=S''ha produ\u00eft el seg\u00fcent error d''entrada/sortida en tornar a escriure la {0} imatge: {1}.

+unknownErrorRewritingImg=S''ha produ\u00eft l''error seg\u00fcent en tornar a escriure la {0} imatge: {1}.

+failedToReadImg=La imatge {0} no es pot llegir i s''ometr\u00e0. S''ha produ\u00eft el seg\u00fcent error: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=S''ha produ\u00eft el seg\u00fcent error en analitzar Caja CSS: {0} de {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=No es pot processar el recurs d''imatge {0}.

+unableToReadResponse=No es pot llegir la resposta del recurs d''imatge {0}.

+unableToFetchImg=No es pot obtenir el recurs d''imatge {0}.

+unableToParseImg=No es pot analitzar el recurs d''imatge {0}.

+

+##MutableContent

+exceptionParsingContent=S'ha produ\u00eft una excepci\u00f3 en analitzar el contingut del gadget.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=El gadget a {0} no s''ha pogut analitzar per a la prec\u00e0rrega.

+

+##ProxyingVisitor

+uriExceptionParsing=S''ha produ\u00eft una excepci\u00f3 URI (Uniform Resource Identifier) en analitzar el gadget a {0}.

+

+##TemplateRewriter

+malformedTemplateLib=S'han produ\u00eft excepcions a causa de biblioteques de plantilla amb format incorrecte.

+

+##CajaContnetRewriter

+cajoledCacheCreated=S'ha creat una mem\u00f2ria cau de cajole.

+retrieveReference={0} s''est\u00e0 recuperant.

+unableToCajole=El gadget a {0} no es pot incloure en cajole.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=S''ha produ\u00eft el seg\u00fcent error en sol\u00b7licitar un servidor intermedi concatenat: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=S''ha ignorat un valor TTL (Time To Live) no v\u00e0lid de {0}.

+

+##HttpRequestHandler

+gadgetCreationError=S'ha produ\u00eft un error en crear el gadget per tornar a escriure.  Es torna a escriure la resposta sense gadget.

+

+##ProxyServlet

+embededImgWrongDomain=La sol\u00b7licitud d''incorporaci\u00f3 de l''URL {0} s''ha realitzat al domini incorrecte {1}.

+

+##DefaultTemplateProcessor

+elFailure=S''ha produ\u00eft el seg\u00fcent error EL per al gadget a {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=La cap\u00e7alera {0} no \u00e9s v\u00e0lida i s''est\u00e0 ometent. S''ha produ\u00eft el seg\u00fcent error: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=S''ha produ\u00eft un error en actualitzar {0}. S''ha tornat el codi d''estat {1}. Excepci\u00f3: {2}. En lloc seu, s''est\u00e0 utilitzant una versi\u00f3 emmagatzemada a la mem\u00f2ria cau.

+updateSpecFailureApplyNegCache=S''ha produ\u00eft un error en actualitzar {0}. S''ha tornat el codi d''estat {1}. Excepci\u00f3: {2}. S''est\u00e0 aplicant una mem\u00f2ria cau negativa.

+

+##HashLockedDomainService

+noLockedDomainConfig=No existeix cap configuraci\u00f3 de domini bloquejada per a {0}.

+

+##Bootstrap

+startingConnManagerWith=S''est\u00e0 iniciant el gestor de connexions amb les propietats {0}.

+

+##XSDValidator

+resolveResource=S''estan resolent els recursos seg\u00fcents: {0}, {1}, {2}, {3}.

+failedToValidate=S''ha produ\u00eft un error en validar {0}.

+

+##DefaultRequestPipeline

+cachedResponse=S''est\u00e0 tornant la resposta emmagatzemada a la mem\u00f2ria cau per a la sol\u00b7licitud de {0}.

+staleResponse="S''ha produ\u00eft un error en sol\u00b7licitar el recurs a {0} per\u00f2 tenim una resposta anterior a la mem\u00f2ria cau. S''est\u00e0 tornant una resposta possiblement obsoleta des de la mem\u00f2ria cau.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_cs.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_cs.properties
new file mode 100644
index 0000000..eeed0b6
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_cs.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Token nebo pov\u011b\u0159en\u00ed zabezpe\u010den\u00ed je po\u0161kozen\u00e9 a nelze je analyzovat.

+

+##XmlUtil

+errorParsingXML=Chyba p\u0159i anal\u00fdze XML. Tuto v\u00fdjimku lze ignorovat.

+errorParsingExternalGeneralEntities=Pou\u017e\u00edvan\u00fd analyz\u00e1tor XML nyn\u00ed na\u010dte obecn\u00e9 extern\u00ed entity.

+errorParsingExternalParameterEntities=Pou\u017e\u00edvan\u00fd analyz\u00e1tor XML na\u010dte entity extern\u00edch parametr\u016f.

+errorParsingExternalDTD=Pou\u017e\u00edvan\u00fd analyz\u00e1tor XML na\u010dte DTD (Document Type Definitions).

+errorNotUsingSecureXML=Pou\u017e\u00edvan\u00fd analyz\u00e1tor XML nepodporuje bezpe\u010dnou syntaktickou anal\u00fdzu.

+reuseDocumentBuilders=Tv\u016frci dokument\u016f budou znovu pou\u017eiti.

+notReuseDocBuilders=Tv\u016frci dokument\u016f nebudou znovu pou\u017eiti.

+

+##LruCacheProvider

+LRUCapacity=Ned\u00e1vno pou\u017eit\u00e1 kapacita {0} (LRU) je nakonfigurov\u00e1na pro {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} nelze vyhodnotit.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Prob\u00edh\u00e1 \u010dten\u00ed konfigurace kontejneru {0}.

+loadFromString={0} nelze analyzovat.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Na\u010d\u00edtaj\u00ed se prost\u0159edky z {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Na\u010d\u00edtaj\u00ed se soubory z {0}.

+

+##ApiServlet

+apiServletProtocolException=Byla vr\u00e1cena chyba odpov\u011bdi, jeliko\u017e do\u0161lo k v\u00fdjimce protokolu.

+apiServletException=Byla vr\u00e1cena chyba odpov\u011bdi, jeliko\u017e do\u0161lo k v\u00fdjimce protokolu.

+

+##FeatureRegistry

+overridingFeature=Funkce {0} s definic\u00ed v {1} bude p\u0159eps\u00e1na.

+

+##FeatureResourceLoader

+missingFile=Soubor {0} d\u0159\u00edve existoval, ale nyn\u00ed chyb\u00ed.

+unableRetrieveLib=Nelze na\u010d\u00edst vzd\u00e1lenou knihovnu z um\u00edst\u011bn\u00ed {0}.

+

+##BasicHttpFetcher

+timeoutException=Platnost {0} vypr\u0161ela kv\u016fli n\u00e1sleduj\u00edc\u00ed v\u00fdjimce: {1} - {2} - {3} ms.

+exceptionOccurred=P\u0159i na\u010dten\u00ed{0} do\u0161lo k n\u00e1sleduj\u00edc\u00ed v\u00fdjimce: {1} ms.

+slowResponse={0} odpov\u00edd\u00e1 pomalu. Uplynulo {1} ms.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed k\u00f3du digest zpr\u00e1vy (MD5) do\u0161lo k chyb\u011b. Chyba byla ignorov\u00e1na.

+errorParsingMD5=P\u0159i anal\u00fdze \u0159et\u011bzce k\u00f3du digest zpr\u00e1vy (MD5) ve form\u00e1tu UTF-8 do\u0161lo k chyb\u011b.

+

+##OAuthModule

+usingRandomKey=Pro \u0161ifrov\u00e1n\u00ed OAuth na stran\u011b klienta bude pou\u017eit n\u00e1hodn\u00fd kl\u00ed\u010d.

+usingFile=Pro \u0161ifrov\u00e1n\u00ed OAuth na stran\u011b klienta bude pou\u017eit soubor {0}.

+loadKeyFileFrom=Na\u010d\u00edt\u00e1 se podpisov\u00fd kl\u00ed\u010d OAuth z um\u00edst\u011bn\u00ed {0}.

+couldNotLoadKeyFile= Soubor s kl\u00ed\u010dem {0} nelze na\u010d\u00edst.

+couldNotLoadSignedKey=Podpisov\u00fd kl\u00ed\u010d OAuth se nepoda\u0159ilo spr\u00e1vn\u011b na\u010d\u00edst. Jak vytvo\u0159it kl\u00ed\u010d: \n 1. Spus\u0165te n\u00e1sleduj\u00edc\u00ed p\u0159\u00edkaz: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Upravte soubor shindig.properties p\u0159id\u00e1n\u00edm n\u00e1sleduj\u00edc\u00edch \u0159\u00e1dk\u016f: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=Nepoda\u0159ilo se inicializovat odb\u011bratele z um\u00edst\u011bn\u00ed {0}.

+

+##OAuthRequest

+oauthFetchFatalError=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed obsahu pomoc\u00ed OAuth do\u0161lo k z\u00e1va\u017en\u00e9 chyb\u011b: \n {0}.

+oauthFetchErrorReprompt=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed obsahu pomoc\u00ed OAuth do\u0161lo k chyb\u011b: \n {0}. Je po\u017eadov\u00e1no schv\u00e1len\u00ed u\u017eivatele.

+bogusExpired=Server vr\u00e1til neplatn\u00e9 vypr\u0161en\u00ed:\n {0}.

+oauthFetchUnexpectedError=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed obsahu pomoc\u00ed OAuth do\u0161lo k z\u00e1va\u017en\u00e9 chyb\u011b: \n {0}.

+unauthenticatedOauth=OAuth se nepoda\u0159ilo z\u00edskat obsah, proto\u017ee neexistuje ov\u011b\u0159en\u00ed u\u017eivatele. Do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: \n {0}.

+invalidOauth=OAuth se nepoda\u0159ilo z\u00edskat obsah, proto\u017ee je po\u017eadavek neplatn\u00fd. Do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=Anal\u00fdza \u0161ablony styl\u016f se nezda\u0159ila.

+unableToConvertScript=P\u0159evod uzlu skriptu na zna\u010dku OSML (OpenSocial Markup Language) se nezda\u0159il.

+

+##PipelineExecutor

+errorPreloading=P\u0159i na\u010d\u00edt\u00e1n\u00ed do\u0161lo k neo\u010dek\u00e1van\u00e9 chyb\u011b.

+

+##Processor

+renderBlacklistedGadget=Syst\u00e9m se pokusil vykreslit v seznamu zak\u00e1zan\u00fd modul gadget: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} se nepoda\u0159ilo z\u00edskat.

+failedToRead={0} nelze p\u0159e\u010d\u00edst.

+

+##DefaultServiceFetcher

+httpErrorFetching=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed metod slu\u017eby z koncov\u00e9ho bodu {1} do\u0161lo k chyb\u011b HTTP {0}.

+failedToFetchService=Nepoda\u0159ilo se z\u00edskat metody slu\u017eeb z koncov\u00e9ho bodu {0}. Do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: {1}.

+failedToParseService=Anal\u00fdza metod slu\u017eeb z koncov\u00e9ho bodu {0} se nezda\u0159ila. Do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: {1}.

+

+##Renderer

+FailedToRender=Modul gadget na adrese {0} se nepoda\u0159ilo vykreslit. Do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=V extern\u00edch knihovn\u00e1ch &libs=: {0} byla nalezena jedna nebo v\u00edce nezn\u00e1m\u00fdch funkc\u00ed.

+unexpectedErrorPreloading=P\u0159i na\u010d\u00edt\u00e1n\u00ed modulu gadget do\u0161lo k neo\u010dek\u00e1van\u00e9 chyb\u011b.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Po\u017eadavek na vy\u010di\u0161t\u011bn\u00ed byl zad\u00e1n bez specifikace typu obsahu pro {0}.

+requestToSanitizeUnknownContent=Po\u017eadavek na vy\u010di\u0161t\u011bn\u00ed byl zad\u00e1n bez specifikace typu obsahu  {0} pro {1}.

+unableToSanitizeUnknownImg=Typ obrazu {0} je nezn\u00e1m\u00fd a nelze jej vy\u010distit.

+unableToDetectImgType=Typ obrazu pro {0} nelze p\u0159i o\u0161et\u0159en\u00ed obsahu zjistit.

+

+##BasicImageRewriter

+ioErrorRewritingImg=P\u0159i p\u0159episov\u00e1n\u00ed {0} v obraze: {1} do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b vstupu a v\u00fdstupu.

+unknownErrorRewritingImg=P\u0159i p\u0159episov\u00e1n\u00ed {0} v obraze: {1} do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b.

+failedToReadImg=Obraz {0} nelze na\u010d\u00edst a bude p\u0159esko\u010den. Do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=P\u0159i anal\u00fdze souboru Caja CSS do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: {0} pro {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=Zdroj obr\u00e1zku {0} nelze zpracovat.

+unableToReadResponse=Odpov\u011b\u010f na zdroj obr\u00e1zku {0} nelze p\u0159e\u010d\u00edst.

+unableToFetchImg=Zdroj obr\u00e1zku {0} nelze z\u00edskat.

+unableToParseImg=Anal\u00fdzu zdroje obr\u00e1zku {0} nelze prov\u00e9st.

+

+##MutableContent

+exceptionParsingContent=P\u0159i anal\u00fdze obsahu pro modul gadget do\u0161lo k v\u00fdjimce.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Modul gadget v um\u00edst\u011bn\u00ed {0} nelze analyzovat pro na\u010dten\u00ed.

+

+##ProxyingVisitor

+uriExceptionParsing=P\u0159i anal\u00fdze modulu gadget v um\u00edst\u011bn\u00ed {0} do\u0161lo k v\u00fdjimce URI (Uniform Resource Identifier) .

+

+##TemplateRewriter

+malformedTemplateLib=K v\u00fdjimk\u00e1m do\u0161lo kv\u016fli po\u0161kozen\u00fdm knihovn\u00e1m \u0161ablon.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Byla vytvo\u0159ena na\u010dten\u00e1 mezipam\u011b\u0165.

+retrieveReference=Prob\u00edh\u00e1 z\u00edsk\u00e1v\u00e1n\u00ed {0}.

+unableToCajole=Modul gadget {0} nelze explicitn\u011b vytvo\u0159it.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed z\u0159et\u011bzen\u00e9 proxy do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Neplatn\u00e1 hodnota TTL (Time To Live) {0} byla ignorov\u00e1na.

+

+##HttpRequestHandler

+gadgetCreationError=Do\u0161lo k chyb\u011b p\u0159i vytv\u00e1\u0159en\u00ed modulu gadget pro p\u0159eps\u00e1n\u00ed. Odezva bude p\u0159eps\u00e1na bez modulu gadget.

+

+##ProxyServlet

+embededImgWrongDomain=Po\u017eadavek na vlo\u017een\u00ed URL {0} byl proveden v chybn\u00e9 dom\u00e9n\u011b {1}.

+

+##DefaultTemplateProcessor

+elFailure=Pro modul gadget v um\u00edst\u011bn\u00ed {0}: {1} do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b EL.

+

+##UriUtils

+skipIllegalHeader=Z\u00e1hlav\u00ed{0} je neplatn\u00e9 a bude p\u0159esko\u010deno. Do\u0161lo k n\u00e1sleduj\u00edc\u00ed chyb\u011b: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=P\u0159i aktualizaci {0} do\u0161lo k chyb\u011b. Byl vr\u00e1cen stavov\u00fd k\u00f3d {1}. V\u00fdjimka: {2}. Bude pou\u017eita verze z mezipam\u011bti.

+updateSpecFailureApplyNegCache=P\u0159i aktualizaci {0} do\u0161lo k chyb\u011b. Byl vr\u00e1cen stavov\u00fd k\u00f3d {1}. V\u00fdjimka: {2}. Pou\u017e\u00edv\u00e1 se z\u00e1porn\u00e1 mezipam\u011b\u0165.

+

+##HashLockedDomainService

+noLockedDomainConfig=Uzamknut\u00e1 konfigurace dom\u00e9ny pro {0} neexistuje.

+

+##Bootstrap

+startingConnManagerWith=Spou\u0161t\u00ed se spr\u00e1vce p\u0159ipojen\u00ed s vlastnostmi {0}.

+

+##XSDValidator

+resolveResource=\u0158e\u0161\u00ed se n\u00e1sleduj\u00edc\u00ed prost\u0159edky: {0}, {1}, {2}, {3}.

+failedToValidate=P\u0159i ov\u011b\u0159ov\u00e1n\u00ed {0} do\u0161lo k chyb\u011b.

+

+##DefaultRequestPipeline

+cachedResponse=Prob\u00edh\u00e1 vracen\u00ed odpov\u011bdi z mezipam\u011bti pro po\u017eadavek na prost\u0159edek {0}.

+staleResponse=P\u0159i zpracov\u00e1n\u00ed po\u017eadavku na prost\u0159edek v um\u00edst\u011bn\u00ed {0} do\u0161lo k chyb\u011b. V mezipam\u011bti je v\u0161ak ulo\u017eena p\u0159edchoz\u00ed odpov\u011b\u010f. Prob\u00edh\u00e1 vracen\u00ed potenci\u00e1ln\u011b zastaral\u00e9 odpov\u011bdi z mezipam\u011bti.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_da.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_da.properties
new file mode 100644
index 0000000..ff860e5
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_da.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Sikkerhedstoken eller legitimationsoplysning er forkert udformet og kan ikke analyseres.

+

+##XmlUtil

+errorParsingXML=Fejl under analyse af XML. Den ignoreres.

+errorParsingExternalGeneralEntities=Den anvendte XML-processor indl\u00e6ser eksterne, generelle entiteter.

+errorParsingExternalParameterEntities=Den anvendte XML-processor indl\u00e6ser eksterne parameterentiteter.

+errorParsingExternalDTD=Den anvendte XML-processor DTD'er (Document Type Definitions).

+errorNotUsingSecureXML=Den anvendte XML-processor underst\u00f8tter ikke sikker parsing.

+reuseDocumentBuilders=Dokumentbyggere genbruges.

+notReuseDocBuilders=Dokumentbyggere genbruges ikke.

+

+##LruCacheProvider

+LRUCapacity=Den mindst nyligt brugte (LRU) kapacitet {0} er konfigureret for {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} kan ikke evalueres.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Konfiguration af opbevaringsstedet {0} l\u00e6ses.

+loadFromString={0} kan ikke analyseres.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Ressourcer fra {0} indl\u00e6ses.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Filer fra {0} indl\u00e6ses.

+

+##ApiServlet

+apiServletProtocolException=Der returneres en svarfejl, fordi der er opst\u00e5et en protokolundtagelse.

+apiServletException=Der returneres en svarfejl, fordi der er opst\u00e5et en protokolundtagelse.

+

+##FeatureRegistry

+overridingFeature=Funktionen {0} med definition p\u00e5 {1} tilsides\u00e6ttes.

+

+##FeatureResourceLoader

+missingFile=Filen {0} fandtes tidligere, men mangler nu.

+unableRetrieveLib=Det eksterne bibliotek fra {0} kan ikke hentes.

+

+##BasicHttpFetcher

+timeoutException=Tidsfristen for {0} er udl\u00f8bet pga. f\u00f8lgende undtagelse: {1} - {2} - {3} ms.

+exceptionOccurred=Der opstod f\u00f8lgende undtagelse under hentning af {0}: {1} ms medg\u00e5et.

+slowResponse={0} svarer langsomt. Der er g\u00e5et {1} ms.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Der er opst\u00e5et en fejl under hentning af MD5 (Message Digest 5). Fejlen blev ignoreret.

+errorParsingMD5=Der er opst\u00e5et en fejl under analyse af MD5-strengen (Message Digest 5) i UTF-8-format.

+

+##OAuthModule

+usingRandomKey=Der anvendes en tilf\u00e6ldig n\u00f8gle til tilstandskryptering p\u00e5 OAuth-klienten.

+usingFile={0}-filen til tilstandskryptering p\u00e5 OAuth-klienten anvendes.

+loadKeyFileFrom=OAuth-signeringsn\u00f8glen fra {0} indl\u00e6ses.

+couldNotLoadKeyFile= N\u00f8glefilen {0} kan ikke indl\u00e6ses.

+couldNotLoadSignedKey=OAuth-signeringsn\u00f8glen er ikke indl\u00e6st korrekt. S\u00e5dan opretter du en n\u00f8gle: \n 1. Udf\u00f8r f\u00f8lgende kommando: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mintestnoegle''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Redig\u00e9r filen shindig.properties ved at tilf\u00f8je disse linjer: \n{0} =<sti-til-oauthkey.pem>\n {1} =minnoegle\n.

+failedToInit=OAuth-forbrugere fra {0} er ikke initialiseret.

+

+##OAuthRequest

+oauthFetchFatalError=Der er opst\u00e5et f\u00f8lgende alvorlige fejl, mens OAuth hentede indhold: \n {0}.

+oauthFetchErrorReprompt=Der er opst\u00e5et f\u00f8lgende fejl, mens OAuth hentede indhold: \n {0}. Brugeren bliver bedt om at godkende igen.

+bogusExpired=Serveren har returneret et ugyldigt udl\u00f8b:\n {0}.

+oauthFetchUnexpectedError=Der er opst\u00e5et f\u00f8lgende alvorlige fejl, mens OAuth hentede indhold: \n {0}.

+unauthenticatedOauth=OAuth kan ikke hente indhold, fordi brugervalideringen ikke findes. Der er opst\u00e5et f\u00f8lgende fejl: \n \ {0}.

+invalidOauth=OAuth kan ikke hente indhold, fordi anmodningen er ugyldig. Der er opst\u00e5et f\u00f8lgende fejl: \n \ {0}.

+

+##CajaCssSanitizer

+failedToParse=Typografifilen kan ikke analyseres.

+unableToConvertScript=Scriptnoden kan ikke konverteres til et OSML-emneord (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Der er opst\u00e5et en uventet fejl under forudindl\u00e6sning.

+

+##Processor

+renderBlacklistedGadget=Systemet har fors\u00f8gt at gengive f\u00f8lgende sortlistede gadget: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} kan ikke hentes.

+failedToRead={0} kan ikke l\u00e6ses.

+

+##DefaultServiceFetcher

+httpErrorFetching=Der er opst\u00e5et en HTTP-baseret {0}-fejl under hentning af servicemetoder fra slutpunktet {1}.

+failedToFetchService=Servicemetoder fra slutpunktet {0} kan ikke hentes. Der er opst\u00e5et f\u00f8lgende fejl: {1}.

+failedToParseService=Servicemetoder fra slutpunktet {0} kan ikke analyseres. Der er opst\u00e5et f\u00f8lgende fejl: {1}.

+

+##Renderer

+FailedToRender=Gadgetten p\u00e5 {0} er ikke gengivet. Der er opst\u00e5et f\u00f8lgende fejl: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Der findes \u00e9n eller flere ukendte funktioner i f\u00f8lgende eksterne &libs=: {0}.

+unexpectedErrorPreloading=Der er opst\u00e5et en uventet fejl under forudindl\u00e6sning af gadgetten.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Der er udsendt en anmodning om rensning uden en indholdstype for {0}.

+requestToSanitizeUnknownContent=Der er udsendt en anmodning om rensning uden en kendt indholdstype {0} for {1}.

+unableToSanitizeUnknownImg=Billedtypen {0} er ukendt og kan ikke renses.

+unableToDetectImgType=Billedtypen for {0} kan ikke registreres ved rensning af indhold.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Der er opst\u00e5et f\u00f8lgende input/output-fejl under skrivning af {0}-billedet: {1}.

+unknownErrorRewritingImg=Der er opst\u00e5et f\u00f8lgende fejl under skrivning af {0}-billedet: {1}.

+failedToReadImg=Billedet {0} kan ikke l\u00e6ses og springes over. Der er opst\u00e5et f\u00f8lgende fejl: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Der er opst\u00e5et f\u00f8lgende fejl under analyse af skrivning af Caja-typografifilerne: {0} for {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=Billedressourcen {0} kan ikke behandles.

+unableToReadResponse=Svaret for billedressourcen {0} kan ikke l\u00e6ses.

+unableToFetchImg=Billedressourcen {0} kan ikke hentes.

+unableToParseImg=Billedressourcen {0} kan ikke analyseres.

+

+##MutableContent

+exceptionParsingContent=Der er opst\u00e5et en undtagelse under analyse af indhold for gadgetten.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Gadgetten p\u00e5 {0} kan ikke analyseres til forudindl\u00e6sning.

+

+##ProxyingVisitor

+uriExceptionParsing=Der er opst\u00e5et en URI-undtagelse (Uniform Resource Identifier) under analyse af gadgetten p\u00e5 {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Der er opst\u00e5et undtagelser p\u00e5 grund af forkert formaterede skabelonbiblioteker.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Der er oprettet en kompileret cache.

+retrieveReference={0} hentes.

+unableToCajole=Gadgetten p\u00e5 {0} kan ikke kompileres.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Der er opst\u00e5et f\u00f8lgende fejl ved anmodning om en sammenk\u00e6det proxy: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=En ugyldig TTL-v\u00e6rdi (Time To Live) p\u00e5 {0} blev ignoreret.

+

+##HttpRequestHandler

+gadgetCreationError=Der er opst\u00e5et en fejl oprettelse af gadgetten for genskrivning. Skriver svaret igen uden gadgetten.

+

+##ProxyServlet

+embededImgWrongDomain=Anmodningen om at inds\u00e6tte URL''en {0} er foretaget til det forkerte dom\u00e6ne {1}.

+

+##DefaultTemplateProcessor

+elFailure=Der er opst\u00e5et f\u00f8lgende EL-fejl for gadgetten p\u00e5 {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=Overskriften {0} er ugyldig og springes over. Der er opst\u00e5et f\u00f8lgende fejl: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Der er opst\u00e5et en fejl under opdatering af {0}. Statuskoden {1} blev returneret. Undtagelse: {2}. Der bruges en cachelagret version i stedet for.

+updateSpecFailureApplyNegCache=Der er opst\u00e5et en fejl under opdatering af {0}. Statuskoden {1} blev returneret. Undtagelse: {2}. Der anvendes en negativ cache.

+

+##HashLockedDomainService

+noLockedDomainConfig=Der findes ikke en l\u00e5st dom\u00e6nekonfiguration for {0}.

+

+##Bootstrap

+startingConnManagerWith=Tilslutningsstyring starter med {0}-egenskaber.

+

+##XSDValidator

+resolveResource=F\u00f8lgende ressourcer fortolkes: {0}, {1}, {2}, {3}.

+failedToValidate=Der er opst\u00e5et en fejl under validering af {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Returnerer cachelagret svar til anmodningen om {0}.

+staleResponse=Der er opst\u00e5et en fejl under anmodning om ressourcen p\u00e5 {0}, men der findes et tidligere svar i cachen. Returnerer et svar, der muligvis er for\u00e6ldet, fra cachen.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_de.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_de.properties
new file mode 100644
index 0000000..3f73bb4
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_de.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Das Sicherheitstoken oder der Berechtigungsnachweis ist fehlerhaft und kann nicht analysiert werden.

+

+##XmlUtil

+errorParsingXML=Fehler beim Parsing der XML. Dies kann ignoriert werden.

+errorParsingExternalGeneralEntities=Der verwendete XML-Prozessor l\u00e4dt externe allgemeine Entit\u00e4ten.

+errorParsingExternalParameterEntities=Der verwendete XML-Prozessor l\u00e4dt externe Parameterentit\u00e4ten.

+errorParsingExternalDTD=Der verwendete XML-Prozessor l\u00e4dt Dokumenttypdefinitionen (DTD).

+errorNotUsingSecureXML=Der verwendete XML-Prozessor unterst\u00fctzt kein sicheres Parsing.

+reuseDocumentBuilders=Die Dokumenterstellungsprogramme werden wiederverwendet.

+notReuseDocBuilders=Die Dokumenterstellungsprogramme werden nicht wiederverwendet.

+

+##LruCacheProvider

+LRUCapacity=Die LRU-Kapazit\u00e4t {0} wird konfiguriert f\u00fcr {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} kann nicht ausgewertet werden.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Containerkonfiguration {0} wird gelesen.

+loadFromString={0} kann nicht analysiert werden.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Ressourcen aus {0} werden geladen.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Dateien aus {0} werden geladen.

+

+##ApiServlet

+apiServletProtocolException=Es wird ein Antwortfehler ausgegeben, da eine Protokollausnahmebedingung aufgetreten ist.

+apiServletException=Es wird ein Antwortfehler ausgegeben, da eine Protokollausnahmebedingung aufgetreten ist.

+

+##FeatureRegistry

+overridingFeature=Die Funktion {0} mit ihrer Definition unter {1} wird au\u00dfer Kraft gesetzt.

+

+##FeatureResourceLoader

+missingFile=Die Datei {0} ist nicht mehr vorhanden.

+unableRetrieveLib=Die ferne Bibliothek von {0} kann nicht abgerufen werden.

+

+##BasicHttpFetcher

+timeoutException={0} hat wegen der folgenden Ausnahmebedingung das zul\u00e4ssige Zeitlimit \u00fcberschritten: {1} - {2} - {3} ms.

+exceptionOccurred=Die folgende Ausnahmebedingung ist beim Abrufen von {0} aufgetreten: {1} ms abgelaufen.

+slowResponse={0} reagiert langsam. {1} ms abgelaufen.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Beim Abrufen von Message Digest 5 (MD5) ist ein Fehler aufgetreten. Der Fehler wurde ignoriert.

+errorParsingMD5=Beim Auswerten der MD5-Zeichenfolge (MD5 - Message Digest 5) im UTF-8-Format ist ein Fehler aufgetreten.

+

+##OAuthModule

+usingRandomKey=Es wird ein Zufallsschl\u00fcssel f\u00fcr die clientseitige OAuth-Statusverschl\u00fcsselung verwendet.

+usingFile=Es wird die Datei {0} f\u00fcr die clientseitige OAuth-Statusverschl\u00fcsselung verwendet.

+loadKeyFileFrom=Der OAuth-Signierschl\u00fcssel von {0} wird geladen.

+couldNotLoadKeyFile= Die Schl\u00fcsseldatei {0} konnte nicht geladen werden.

+couldNotLoadSignedKey=Der OAuth-Signierschl\u00fcssel wurde nicht ordnungsgem\u00e4\u00df geladen. So erstellen Sie einen Schl\u00fcssel: \n \ 1. F\u00fchren Sie den folgenden Befehl aus: \n \ openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n \ -out testkey.pem -subj ''/CN=mytestkey''\n \ openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n \n. 2. Bearbeiten Sie die Datei "shindig.properties" durch Hinzuf\u00fcgen der folgenden Zeilen: \n {0} =<path-to-oauthkey.pem>\n \ {1} =mykey\n .

+failedToInit=OAuth-Konsumenten von {0} konnten nicht initialisiert werden.

+

+##OAuthRequest

+oauthFetchFatalError=Der folgende schwerwiegende Fehler ist aufgetreten, als OAuth Inhalt abrief: \n \ {0}.

+oauthFetchErrorReprompt=Der folgende Fehler ist aufgetreten, als OAuth Inhalt abrief: \n \ {0}. Der Benutzer wird erneut aufgefordert, den Vorgang zu genehmigen.

+bogusExpired=Der Server hat einen ung\u00fcltigen Ablauf ausgegeben:\n \ {0}.

+oauthFetchUnexpectedError=Der folgende schwerwiegende Fehler ist aufgetreten, als OAuth Inhalt abrief: \n \ {0}.

+unauthenticatedOauth=OAuth kann keinen Inhalt abrufen, weil keine Benutzerauthentifizierung vorhanden ist. Der folgende Fehler ist aufgetreten: \n \ {0}.

+invalidOauth=OAuth kann keinen Inhalt abrufen, weil die Anforderung ung\u00fcltig ist. Der folgende Fehler ist aufgetreten: \n \ {0}.

+

+##CajaCssSanitizer

+failedToParse=Das Style-Sheet kann nicht analysiert werden.

+unableToConvertScript=Der Scriptknoten kann nicht in ein OSML-Tag (OSML - OpenSocial Markup Language) konvertiert werden.

+

+##PipelineExecutor

+errorPreloading=Bei der Vorinstallation ist ein unerwarteter Fehler aufgetreten.

+

+##Processor

+renderBlacklistedGadget=Das System hat versucht, das folgende Gadget mit Sperrvermerk bereitzustellen: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} kann nicht abgerufen werden.

+failedToRead={0} kann nicht gelesen werden.

+

+##DefaultServiceFetcher

+httpErrorFetching=Ein HTTP-{0}-Fehler ist beim Abrufen von Servicemethoden vom {1}-Endpunkt aufgetreten.

+failedToFetchService=Die Servicemethoden vom {0}-Endpunkt konnten nicht abgerufen werden. Der folgende Fehler ist aufgetreten: {1}.

+failedToParseService=Die Servicemethoden vom {0}-Endpunkt konnten nicht analysiert werden. Der folgende Fehler ist aufgetreten: {1}.

+

+##Renderer

+FailedToRender=Das Gadget unter {0} wurde nicht bereitgestellt. Der folgende Fehler ist aufgetreten: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Mindestens eine unbekannte Funktion ist in den folgenden externen &libs= vorhanden: {0}.

+unexpectedErrorPreloading=Bei der Vorinstallation des Gadgets ist ein unerwarteter Fehler aufgetreten.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Es wurde eine Bereinigungsanforderung ohne Inhaltstyp f\u00fcr {0} ausgegeben.

+requestToSanitizeUnknownContent=Es wurde eine Bereinigungsanforderung ohne bekannten Inhaltstyp {0} f\u00fcr {1} ausgegeben.

+unableToSanitizeUnknownImg=Der Imagetyp {0} ist unbekannt und kann nicht bereinigt werden.

+unableToDetectImgType=Der Imagetyp f\u00fcr {0} kann beim Bereinigen von Inhalt nicht erkannt werden.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Beim Neuerstellen des {0}-Images ist der folgende Ein-/Ausgabefehler aufgetreten: {1}.

+unknownErrorRewritingImg=Beim Neuerstellen des {0}-Images ist der folgende Fehler aufgetreten: {1}.

+failedToReadImg=Das {0}-Image kann nicht gelesen werden und wird \u00fcbersprungen. Der folgende Fehler ist aufgetreten: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Beim Parsen des Caja-CSS ist der folgende Fehler aufgetreten: {0} f\u00fcr {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=Die {0}-Imageressource kann nicht verarbeitet werden.

+unableToReadResponse=Die Antwort f\u00fcr die {0}-Imageressource kann nicht gelesen werden.

+unableToFetchImg=Die {0}-Imageressource kann nicht abgerufen werden.

+unableToParseImg=Die {0}-Imageressource kann nicht analysiert werden.

+

+##MutableContent

+exceptionParsingContent=Beim Parsen von Inhalt f\u00fcr das Gadget ist eine Ausnahmebedingung aufgetreten.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Das Gadget unter {0} konnte f\u00fcr die Vorinstallation nicht analysiert werden.

+

+##ProxyingVisitor

+uriExceptionParsing=Es ist eine URI-Ausnahmebedingung (URI - Uniform Resource Identifier) aufgetreten, als das Gadget unter {0} analysiert wurde.

+

+##TemplateRewriter

+malformedTemplateLib=Aufgrund der fehlerhaften Vorlagenbibliotheken sind Ausnahmebedingungen aufgetreten.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Es wurde ein optimierter Cache erstellt.

+retrieveReference={0} wird abgerufen.

+unableToCajole=Das Gadget unter {0} kann nicht optimiert werden.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Der folgende Fehler ist beim Anfordern eines verk\u00fcrzten Proxys aufgetreten: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Ein ung\u00fcltiger Lebensdauerwert von {0} wurde ignoriert.

+

+##HttpRequestHandler

+gadgetCreationError=Beim Erstellen des Gadgets zum Neuschreiben ist ein Fehler aufgetreten. Die Antwort wird ohne das Gadget neu geschrieben.

+

+##ProxyServlet

+embededImgWrongDomain=Die Anforderung f\u00fcr die Integrierung der {0}-URL wurde f\u00fcr die falsche Dom\u00e4ne durchgef\u00fchrt: {1}.

+

+##DefaultTemplateProcessor

+elFailure=Der folgende EL-Fehler ist f\u00fcr das Gadget unter {0} aufgetreten: {1}.

+

+##UriUtils

+skipIllegalHeader=Der {0}-Header ist unzul\u00e4ssig und wird \u00fcbersprungen. Der folgende Fehler ist aufgetreten: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Beim Aktualisieren von {0} ist ein Fehler aufgetreten. Der Statuscode {1} wurde zur\u00fcckgegeben. Ausnahme: {2}. Es wird stattdessen eine zwischengespeicherte Version verwendet.

+updateSpecFailureApplyNegCache=Beim Aktualisieren von {0} ist ein Fehler aufgetreten. Der Statuscode {1} wurde zur\u00fcckgegeben. Ausnahme: {2}. Es wird ein negativer Cache verwendet.

+

+##HashLockedDomainService

+noLockedDomainConfig=Es ist keine gesperrte Dom\u00e4nenkonfiguration f\u00fcr {0} vorhanden.

+

+##Bootstrap

+startingConnManagerWith=Ein Verbindungsmanager mit {0} Eigenschaften wird gestartet.

+

+##XSDValidator

+resolveResource=Die folgenden Ressourcen werden aufgel\u00f6st: {0}, {1}, {2}, {3}.

+failedToValidate=Beim Pr\u00fcfen von {0} ist ein Fehler aufgetreten.

+

+##DefaultRequestPipeline

+cachedResponse=R\u00fcckgabe der in den Cache gestellten Antwort f\u00fcr die Anforderung an {0}.

+staleResponse="Beim Anfordern der Ressource bei {0} ist ein Fehler aufgetreten, jedoch ist eine vorherige Antwort im Cache vorhanden. R\u00fcckgabe einer m\u00f6glicherweise veralteten Antwort aus dem Cache.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_el.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_el.properties
new file mode 100644
index 0000000..a3819b4
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_el.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03b1\u03c3\u03c6\u03ac\u03bb\u03b5\u03b9\u03b1\u03c2 \u03ae \u03c4\u03bf\u03c5 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf\u03c5 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03b3\u03b9\u03b1\u03c4\u03af \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae.

+

+##XmlUtil

+errorParsingXML=\u03a3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03ba\u03ce\u03b4\u03b9\u03ba\u03b1 XML. \u0391\u03c5\u03c4\u03cc \u03c4\u03bf \u03bc\u03ae\u03bd\u03c5\u03bc\u03b1 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03b3\u03bd\u03bf\u03b7\u03b8\u03b5\u03af.

+errorParsingExternalGeneralEntities=\u039f \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03c2 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03ae\u03c2 XML \u03b8\u03b1 \u03c6\u03bf\u03c1\u03c4\u03ce\u03c3\u03b5\u03b9 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ad\u03c2 \u03b3\u03b5\u03bd\u03b9\u03ba\u03ad\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2.

+errorParsingExternalParameterEntities=\u039f \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03c2 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03ae\u03c2 XML \u03b8\u03b1 \u03c6\u03bf\u03c1\u03c4\u03ce\u03c3\u03b5\u03b9 \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03ad\u03c2 \u03bf\u03bd\u03c4\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03c9\u03bd.

+errorParsingExternalDTD=\u039f \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03c2 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03ae\u03c2 XML \u03b8\u03b1 \u03c6\u03bf\u03c1\u03c4\u03ce\u03c3\u03b5\u03b9 \u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 DTD (Document Type Definition).

+errorNotUsingSecureXML=\u039f \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bc\u03b5\u03bd\u03bf\u03c2 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03ae\u03c2 XML \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03b1\u03c3\u03c6\u03b1\u03bb\u03ae \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7.

+reuseDocumentBuilders=\u0395\u03c0\u03b1\u03bd\u03b1\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b5\u03c1\u03b3\u03b1\u03bb\u03b5\u03af\u03b1 \u03b4\u03cc\u03bc\u03b7\u03c3\u03b7\u03c2 \u03b5\u03b3\u03b3\u03c1\u03ac\u03c6\u03c9\u03bd.

+notReuseDocBuilders=\u0394\u03b5\u03bd \u03b5\u03c0\u03b1\u03bd\u03b1\u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03bf\u03cd\u03bd\u03c4\u03b1\u03b9 \u03b5\u03c1\u03b3\u03b1\u03bb\u03b5\u03af\u03b1 \u03b4\u03cc\u03bc\u03b7\u03c3\u03b7\u03c2 \u03b5\u03b3\u03b3\u03c1\u03ac\u03c6\u03c9\u03bd.

+

+##LruCacheProvider

+LRUCapacity=\u0397 \u03bb\u03b9\u03b3\u03cc\u03c4\u03b5\u03c1\u03bf \u03c0\u03c1\u03cc\u03c3\u03c6\u03b1\u03c4\u03b1 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b7\u03bc\u03ad\u03bd\u03b7 (LRU) \u03c7\u03c9\u03c1\u03b7\u03c4\u03b9\u03ba\u03cc\u03c4\u03b7\u03c4\u03b1 {0} \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b3\u03b9\u03b1 \u03c4\u03bf {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03c0\u03bf\u03c4\u03af\u03bc\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}.

+

+##JsonContainerConfigLoader

+readingContainerConfig=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03c4\u03c9\u03bd \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03c9\u03bd \u03b8\u03ad\u03c3\u03b7\u03c2 \u03c5\u03c0\u03bf\u03b4\u03bf\u03c7\u03ae\u03c2 {0}.

+loadFromString=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=\u03a6\u03bf\u03c1\u03c4\u03ce\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03c0\u03cc\u03c1\u03bf\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=\u03a6\u03bf\u03c1\u03c4\u03ce\u03bd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03c4\u03b1 \u03b1\u03c1\u03c7\u03b5\u03af\u03b1 \u03b1\u03c0\u03cc \u03c4\u03bf {0}.

+

+##ApiServlet

+apiServletProtocolException=\u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1\u03c4\u03af \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5.

+apiServletException=\u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03ad\u03c6\u03b5\u03c4\u03b1\u03b9 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1\u03c4\u03af \u03c0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03c0\u03c1\u03c9\u03c4\u03bf\u03ba\u03cc\u03bb\u03bb\u03bf\u03c5.

+

+##FeatureRegistry

+overridingFeature=\u0397 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 {0} \u03bc\u03b5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc \u03c3\u03c4\u03bf {1} \u03b8\u03b1 \u03b1\u03bd\u03c4\u03b9\u03ba\u03b1\u03c4\u03b1\u03c3\u03c4\u03b1\u03b8\u03b5\u03af.

+

+##FeatureResourceLoader

+missingFile=\u03a4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf {0} \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd.

+unableRetrieveLib=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03c0\u03bf\u03bc\u03b1\u03ba\u03c1\u03c5\u03c3\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b2\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03ae\u03ba\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf {0}.

+

+##BasicHttpFetcher

+timeoutException=\u0397 \u03c0\u03c1\u03bf\u03b8\u03b5\u03c3\u03bc\u03af\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 {0} \u03ad\u03bb\u03b7\u03be\u03b5 \u03b5\u03be\u03b1\u03b9\u03c4\u03af\u03b1\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7\u03c2 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7\u03c2: {1} - {2} - {3} \u03c7\u03b9\u03bb\u03b9\u03bf\u03c3\u03c4\u03ac \u03c4\u03bf\u03c5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03bf\u03bb\u03ad\u03c0\u03c4\u03bf\u03c5.

+exceptionOccurred=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b7 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}: \u03c0\u03b1\u03c1\u03ae\u03bb\u03b8\u03b1\u03bd {1} \u03c7\u03b9\u03bb\u03b9\u03bf\u03c3\u03c4\u03ac \u03c4\u03bf\u03c5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03bf\u03bb\u03ad\u03c0\u03c4\u03bf\u03c5.

+slowResponse=\u039f \u03c7\u03c1\u03cc\u03bd\u03bf\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 {0} \u03b5\u03af\u03bd\u03b1\u03b9 \u03b5\u03c1\u03b3\u03cc\u03c2. \u03a0\u03b1\u03c1\u03ae\u03bb\u03b8\u03b1\u03bd {1} \u03c7\u03b9\u03bb\u03b9\u03bf\u03c3\u03c4\u03ac \u03c4\u03bf\u03c5 \u03b4\u03b5\u03c5\u03c4\u03b5\u03c1\u03bf\u03bb\u03ad\u03c0\u03c4\u03bf\u03c5.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03bb\u03ae\u03c8\u03b7 \u03c4\u03bf\u03c5 MD5 (Message Digest 5). \u03a4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03b1\u03b3\u03bd\u03bf\u03ae\u03b8\u03b7\u03ba\u03b5.

+errorParsingMD5=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03c3\u03b5\u03b9\u03c1\u03ac\u03c2 \u03c7\u03b1\u03c1\u03b1\u03ba\u03c4\u03ae\u03c1\u03c9\u03bd MD5 (Message Digest 5) \u03c3\u03b5 \u03bc\u03bf\u03c1\u03c6\u03ae UTF-8.

+

+##OAuthModule

+usingRandomKey=\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c4\u03c5\u03c7\u03b1\u03af\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 OAuth \u03c3\u03c4\u03bf\u03bd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7.

+usingFile=\u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf {0} \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 OAuth \u03c3\u03c4\u03bf\u03bd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7.

+loadKeyFileFrom=\u03a6\u03bf\u03c1\u03c4\u03ce\u03bd\u03b5\u03c4\u03b1\u03b9 \u03c4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c5\u03c0\u03bf\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 OAuth \u03b1\u03c0\u03cc \u03c4\u03bf {0}.

+couldNotLoadKeyFile= \u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b1\u03c1\u03c7\u03b5\u03af\u03bf\u03c5 \u03ba\u03bb\u03b5\u03b9\u03b4\u03b9\u03ce\u03bd {0}.

+couldNotLoadSignedKey=\u03a4\u03bf \u03ba\u03bb\u03b5\u03b9\u03b4\u03af \u03c5\u03c0\u03bf\u03b3\u03c1\u03b1\u03c6\u03ae\u03c2 OAuth \u03b4\u03b5\u03bd \u03c6\u03bf\u03c1\u03c4\u03ce\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5 \u03b5\u03c0\u03b9\u03c4\u03c5\u03c7\u03af\u03b1. \u0393\u03b9\u03b1 \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03c4\u03b5 \u03ad\u03bd\u03b1 \u03ba\u03bb\u03b5\u03b9\u03b4\u03af: \n 1. \u0395\u03ba\u03c4\u03b5\u03bb\u03ad\u03c3\u03c4\u03b5 \u03c4\u03b7\u03bd \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. \u03a4\u03c1\u03bf\u03c0\u03bf\u03c0\u03bf\u03b9\u03ae\u03c3\u03c4\u03b5 \u03c4\u03bf \u03b1\u03c1\u03c7\u03b5\u03af\u03bf shindig.properties \u03c0\u03c1\u03bf\u03c3\u03b8\u03ad\u03c4\u03bf\u03bd\u03c4\u03b1\u03c2 \u03c4\u03b9\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03b3\u03c1\u03b1\u03bc\u03bc\u03ad\u03c2: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=\u0391\u03c0\u03ad\u03c4\u03c5\u03c7\u03b5 \u03b7 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ce\u03bd \u03c4\u03b9\u03bc\u03ce\u03bd \u03c3\u03c4\u03bf\u03c5\u03c2 \u03ba\u03b1\u03c4\u03b1\u03bd\u03b1\u03bb\u03c9\u03c4\u03ad\u03c2 OAuth \u03b1\u03c0\u03cc \u03c4\u03bf {0}.

+

+##OAuthRequest

+oauthFetchFatalError=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03b1\u03bd\u03b5\u03c0\u03b1\u03bd\u03cc\u03c1\u03b8\u03c9\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b1\u03c0\u03cc \u03c4\u03bf OAuth: \n {0}.

+oauthFetchErrorReprompt=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b1\u03c0\u03cc \u03c4\u03bf OAuth: \n {0}. \u0396\u03b7\u03c4\u03b5\u03af\u03c4\u03b1\u03b9 \u03be\u03b1\u03bd\u03ac \u03ad\u03b3\u03ba\u03c1\u03b9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7.

+bogusExpired=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b5\u03c0\u03ad\u03c3\u03c4\u03c1\u03b5\u03c8\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03bb\u03ae\u03be\u03b7:\n {0}.

+oauthFetchUnexpectedError=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03b1\u03bd\u03b5\u03c0\u03b1\u03bd\u03cc\u03c1\u03b8\u03c9\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b1\u03c0\u03cc \u03c4\u03bf OAuth: \n {0}.

+unauthenticatedOauth=\u03a4\u03bf OAuth \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03b3\u03b9\u03b1\u03c4\u03af \u03b4\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7 \u03b3\u03b9\u03b1 \u03b5\u03be\u03b1\u03ba\u03c1\u03af\u03b2\u03c9\u03c3\u03b7. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: \n {0}.

+invalidOauth=\u03a4\u03bf OAuth \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ae\u03c3\u03b5\u03b9 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03cc\u03bc\u03b5\u03bd\u03bf \u03b3\u03b9\u03b1\u03c4\u03af \u03b7 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c6\u03cd\u03bb\u03bb\u03bf\u03c5 \u03c3\u03c4\u03c5\u03bb.

+unableToConvertScript=\u039f \u03ba\u03cc\u03bc\u03b2\u03bf\u03c2 \u03c3\u03b5\u03bd\u03b1\u03c1\u03af\u03bf\u03c5 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03bc\u03b5\u03c4\u03b1\u03c4\u03c1\u03b1\u03c0\u03b5\u03af \u03c3\u03b5 \u03c0\u03c1\u03bf\u03c3\u03b4\u03b9\u03bf\u03c1\u03b9\u03c3\u03c4\u03b9\u03ba\u03cc OSML (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7.

+

+##Processor

+renderBlacklistedGadget=\u03a4\u03bf \u03c3\u03cd\u03c3\u03c4\u03b7\u03bc\u03b1 \u03b5\u03c0\u03b9\u03c7\u03b5\u03af\u03c1\u03b7\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7 \u03bc\u03b7 \u03b5\u03c0\u03b9\u03c4\u03c1\u03b5\u03c0\u03cc\u03bc\u03b5\u03bd\u03b7 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}.

+failedToRead=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 HTTP {0} \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03bc\u03b5\u03b8\u03cc\u03b4\u03c9\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf {1}.

+failedToFetchService=\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03bc\u03b5\u03b8\u03cc\u03b4\u03c9\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf {0}. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {1}.

+failedToParseService=\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03bc\u03b5\u03b8\u03cc\u03b4\u03c9\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u03b1\u03c0\u03cc \u03c4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf {0}. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {1}.

+

+##Renderer

+FailedToRender=\u0397 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c3\u03c4\u03b7 \u03b8\u03ad\u03c3\u03b7 {0} \u03b4\u03b5\u03bd \u03b5\u03bc\u03c6\u03b1\u03bd\u03af\u03c3\u03c4\u03b7\u03ba\u03b5. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=\u03a5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03bc\u03af\u03b1 \u03ae \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03b5\u03c2 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03b5\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b5\u03c2 \u03c3\u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03b5\u03be\u03c9\u03c4\u03b5\u03c1\u03b9\u03ba\u03cc &libs=: {0}.

+unexpectedErrorPreloading=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c0\u03c1\u03bf\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u03a5\u03c0\u03bf\u03b2\u03bb\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b5\u03be\u03c5\u03b3\u03af\u03b1\u03bd\u03c3\u03b7\u03c2 \u03c7\u03c9\u03c1\u03af\u03c2 \u03b5\u03af\u03b4\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b3\u03b9\u03b1 {0}.

+requestToSanitizeUnknownContent=\u03a5\u03c0\u03bf\u03b2\u03bb\u03ae\u03b8\u03b7\u03ba\u03b5 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b5\u03be\u03c5\u03b3\u03af\u03b1\u03bd\u03c3\u03b7\u03c2 \u03c7\u03c9\u03c1\u03af\u03c2 \u03b3\u03bd\u03c9\u03c3\u03c4\u03cc \u03b5\u03af\u03b4\u03bf\u03c2 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 {0} \u03b3\u03b9\u03b1 {1}.

+unableToSanitizeUnknownImg=\u03a4\u03bf \u03b5\u03af\u03b4\u03bf\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 {0} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b3\u03bd\u03c9\u03c3\u03c4\u03cc \u03ba\u03b1\u03b9 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03be\u03c5\u03b3\u03b9\u03b1\u03bd\u03b8\u03b5\u03af.

+unableToDetectImgType=\u03a4\u03bf \u03b5\u03af\u03b4\u03bf\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 \u03b3\u03b9\u03b1 {0} \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03b5\u03bd\u03c4\u03bf\u03c0\u03b9\u03c3\u03c4\u03b5\u03af \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03be\u03c5\u03b3\u03af\u03b1\u03bd\u03c3\u03b7 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5.

+

+##BasicImageRewriter

+ioErrorRewritingImg=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 I/O \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bd\u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 {0}: {1}.

+unknownErrorRewritingImg=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bd\u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 {0}: {1}.

+failedToReadImg=\u0397 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1 {0} \u03b8\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bb\u03b5\u03b9\u03c6\u03b8\u03b5\u03af \u03b3\u03b9\u03b1\u03c4\u03af \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03ae \u03c4\u03b7\u03c2. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 Caja CSS: {0} \u03b3\u03b9\u03b1 {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 \u03c0\u03cc\u03c1\u03bf\u03c5 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 {0}.

+unableToReadResponse=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03b3\u03bd\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c0\u03cc\u03c1\u03bf \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 {0}.

+unableToFetchImg=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03cc\u03c1\u03bf\u03c5 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 {0}.

+unableToParseImg=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03cc\u03c1\u03bf\u03c5 \u03b5\u03b9\u03ba\u03cc\u03bd\u03b1\u03c2 {0}.

+

+##MutableContent

+exceptionParsingContent=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c0\u03b5\u03c1\u03b9\u03b5\u03c7\u03bf\u03bc\u03ad\u03bd\u03bf\u03c5 \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c0\u03c1\u03bf\u03c6\u03cc\u03c1\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c3\u03c4\u03b7 \u03b8\u03ad\u03c3\u03b7 {0}.

+

+##ProxyingVisitor

+uriExceptionParsing=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03b5\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7 URI (Uniform Resource Identifier) \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03ae \u03b1\u03bd\u03ac\u03bb\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c3\u03c4\u03b7 \u03b8\u03ad\u03c3\u03b7 {0}.

+

+##TemplateRewriter

+malformedTemplateLib=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b1\u03bd \u03b5\u03be\u03b1\u03b9\u03c1\u03ad\u03c3\u03b5\u03b9\u03c2 \u03bb\u03cc\u03b3\u03c9 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7\u03c2 \u03bc\u03bf\u03c1\u03c6\u03ae\u03c2 \u03c4\u03c9\u03bd \u03b2\u03b9\u03b2\u03bb\u03b9\u03bf\u03b8\u03b7\u03ba\u03ce\u03bd \u03c0\u03c1\u03bf\u03c4\u03cd\u03c0\u03c9\u03bd.

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u0394\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03b8\u03b7\u03ba\u03b5 \u03bb\u03b1\u03bd\u03b8\u03ac\u03bd\u03bf\u03c5\u03c3\u03b1 \u03bc\u03bd\u03ae\u03bc\u03b7 \u03c4\u03cd\u03c0\u03bf\u03c5 "cajoled cache".

+retrieveReference=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}.

+unableToCajole=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03c3\u03c4\u03bf {0} \u03c3\u03b5 \u03bb\u03b1\u03bd\u03b8\u03ac\u03bd\u03bf\u03c5\u03c3\u03b1 \u03bc\u03bd\u03ae\u03bc\u03b7 \u03c4\u03cd\u03c0\u03bf\u03c5 "cajoled cache".

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03b4\u03b9\u03ac\u03bc\u03b5\u03c3\u03bf \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=\u0391\u03b3\u03bd\u03bf\u03ae\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03c4\u03b9\u03bc\u03ae TTL (Time To Live) \u03b3\u03b9\u03b1 \u03c4\u03bf {0}.

+

+##HttpRequestHandler

+gadgetCreationError=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03c4\u03b7\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b1\u03bd\u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae.  \u0398\u03b1 \u03b3\u03af\u03bd\u03b5\u03b9 \u03b5\u03c0\u03b1\u03bd\u03b5\u03b3\u03b3\u03c1\u03b1\u03c6\u03ae \u03c4\u03b7\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03c7\u03c9\u03c1\u03af\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae.

+

+##ProxyServlet

+embededImgWrongDomain=\u0397 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03b5\u03bd\u03c3\u03c9\u03bc\u03ac\u03c4\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 URL {0} \u03c5\u03c0\u03bf\u03b2\u03bb\u03ae\u03b8\u03b7\u03ba\u03b5 \u03c3\u03b5 \u03bb\u03b1\u03bd\u03b8\u03b1\u03c3\u03bc\u03ad\u03bd\u03bf \u03c4\u03bf\u03bc\u03ad\u03b1 {1}.

+

+##DefaultTemplateProcessor

+elFailure=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 EL \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03c3\u03c4\u03b7 \u03b8\u03ad\u03c3\u03b7 {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=\u0397 \u03ba\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03b1 {0} \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03ba\u03b1\u03b9 \u03b8\u03b1 \u03c0\u03b1\u03c1\u03b1\u03bb\u03b5\u03b9\u03c6\u03b8\u03b5\u03af. \u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}. \u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03ac\u03c6\u03b7\u03ba\u03b5 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 {1}. \u0395\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7: {2}. \u03a7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03bc\u03b9\u03b1 \u03b5\u03ba\u03b4\u03bf\u03c7\u03ae \u03c0\u03bf\u03c5 \u03b5\u03af\u03c7\u03b5 \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03c4\u03b5\u03af \u03c3\u03c4\u03b7 \u03bb\u03b1\u03bd\u03b8\u03ac\u03bd\u03bf\u03c5\u03c3\u03b1 \u03bc\u03bd\u03ae\u03bc\u03b7.

+updateSpecFailureApplyNegCache=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03bd\u03b7\u03bc\u03ad\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}. \u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03ac\u03c6\u03b7\u03ba\u03b5 \u03bf \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc\u03c2 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7\u03c2 {1}. \u0395\u03be\u03b1\u03af\u03c1\u03b5\u03c3\u03b7: {2}. \u0395\u03c6\u03b1\u03c1\u03bc\u03cc\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c1\u03bd\u03b7\u03c4\u03b9\u03ba\u03ae \u03bb\u03b1\u03bd\u03b8\u03ac\u03bd\u03bf\u03c5\u03c3\u03b1 \u03bc\u03bd\u03ae\u03bc\u03b7.

+

+##HashLockedDomainService

+noLockedDomainConfig=\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03ba\u03bb\u03b5\u03b9\u03b4\u03c9\u03bc\u03ad\u03bd\u03b5\u03c2 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9\u03c2 \u03c4\u03bf\u03bc\u03ad\u03b1 \u03b3\u03b9\u03b1 \u03c4\u03bf {0}.

+

+##Bootstrap

+startingConnManagerWith=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7 \u03c4\u03b7\u03c2 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1\u03c2 \u03b4\u03b9\u03b1\u03c7\u03b5\u03af\u03c1\u03b9\u03c3\u03b7\u03c2 \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03b5\u03c9\u03bd \u03bc\u03b5 \u03b9\u03b4\u03b9\u03cc\u03c4\u03b7\u03c4\u03b5\u03c2 {0}.

+

+##XSDValidator

+resolveResource=\u0391\u03bd\u03b1\u03bb\u03cd\u03bf\u03bd\u03c4\u03b1\u03b9 \u03bf\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf\u03b9 \u03c0\u03cc\u03c1\u03bf\u03b9: {0}, {1}, {2}, {3}.

+failedToValidate=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03c0\u03b9\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 \u03c4\u03bf\u03c5 {0}.

+

+##DefaultRequestPipeline

+cachedResponse=\u0395\u03c0\u03b9\u03c3\u03c4\u03c1\u03bf\u03c6\u03ae \u03b1\u03c0\u03bf\u03b8\u03b7\u03ba\u03b5\u03c5\u03bc\u03ad\u03bd\u03b7\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03c3\u03c4\u03bf {0}.

+staleResponse="\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03c4\u03b7\u03c2 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c0\u03cc\u03c1\u03bf \u03c3\u03c4\u03bf {0}, \u03b1\u03bb\u03bb\u03ac \u03c5\u03c0\u03ac\u03c1\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03c0\u03c1\u03bf\u03b7\u03b3\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03c3\u03c4\u03b7 \u03bb\u03b1\u03bd\u03b8\u03ac\u03bd\u03bf\u03c5\u03c3\u03b1 \u03bc\u03bd\u03ae\u03bc\u03b7. \u0398\u03b1 \u03b5\u03c0\u03b9\u03c3\u03c4\u03c1\u03b1\u03c6\u03b5\u03af \u03bc\u03b9\u03b1 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03b7 \u03bb\u03b1\u03bd\u03b8\u03ac\u03bd\u03bf\u03c5\u03c3\u03b1 \u03bc\u03bd\u03ae\u03bc\u03b7, \u03b7 \u03bf\u03c0\u03bf\u03af\u03b1 \u03b5\u03bd\u03b4\u03ad\u03c7\u03b5\u03c4\u03b1\u03b9 \u03bd\u03b1 \u03bc\u03b7\u03bd \u03b9\u03c3\u03c7\u03cd\u03b5\u03b9 \u03c0\u03bb\u03ad\u03bf\u03bd.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_en_US.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_en_US.properties
new file mode 100644
index 0000000..b36b2f7
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_en_US.properties
@@ -0,0 +1,176 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.  
+
+
+##AuthenticationServletFilter
+errorParsingSecureToken=The security token or credential is malformed and cannot be parsed.
+
+##XmlUtil
+errorParsingXML=Error parsing the XML. This can be ignored.
+errorParsingExternalGeneralEntities=The XML processor being used will load external general entities.
+errorParsingExternalParameterEntities=The XML processor being used will load external parameter entities.
+errorParsingExternalDTD=The XML processor being used will load Document Type Definitions (DTD).
+errorNotUsingSecureXML=The XML processor being used does not support secure parsing.
+reuseDocumentBuilders=Document builders are being reused.
+notReuseDocBuilders=Document builders are not being reused.
+
+##LruCacheProvider
+LRUCapacity=The least recently used (LRU) capacity {0} is configured for {1}.
+
+##DynamicConfigProperty
+evalExpressionFailed={0} cannot be evaluated.
+
+##JsonContainerConfigLoader
+readingContainerConfig=Container configuration {0} is being read.
+loadFromString={0} cannot be parsed.
+
+##JsonContainerConfigLoader, FeatureRegistry
+loadResourcesFrom=Resources from {0} are loading.
+
+##JsonContainerConfigLoader, FeatureRegistry
+loadFilesFrom=Files from {0} are loading.
+
+##ApiServlet
+apiServletProtocolException=A response error is being returned because a protocol exception occurred.
+apiServletException=A response error is being returned because a protocol exception occurred.occurred.
+
+##FeatureRegistry
+overridingFeature=The {0} feature with definition at {1} is being overridden.
+
+##FeatureResourceLoader
+missingFile=The file {0} used to exist but is now missing.
+unableRetrieveLib=The remote library from {0} cannot be retrieved.
+
+##BasicHttpFetcher
+timeoutException={0} has timed out because of the following exception: {1} - {2} - {3} ms.
+exceptionOccurred=The following exception occurred when fetching {0}: {1} ms elapsed.
+slowResponse={0} is responding slowly. {1} ms elapsed.
+
+##HttpResponseMetadataHelper
+errorGettingMD5=An error occurred when getting Message Digest 5 (MD5). The error was ignored.
+errorParsingMD5=An error occurred when parsing the Message Digest 5 (MD5) string in UTF-8 format.
+ 
+##OAuthModule     
+usingRandomKey=A random key for OAuth client-side state encryption is being used.
+usingFile=The {0} file for OAuth client-side state encryption is being used.
+loadKeyFileFrom=The OAuth signing key from {0} is loading.
+couldNotLoadKeyFile= The {0} key file could not be loaded.
+couldNotLoadSignedKey=The OAuth signing key did not load correctly. To create a key: \n 1. Run the following command: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj '/CN=mytestkey'\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Edit the shindig.properties file by adding these lines: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.
+failedToInit=OAuth consumers from {0} failed to initialize.
+
+##OAuthRequest
+oauthFetchFatalError=The following fatal error occurred when OAuth was fetching content: \n {0}.
+oauthFetchErrorReprompt=The following error occurred when OAuth was fetching content: \n {0}. The user is being reprompted for approval.
+bogusExpired=The server returned an invalid expiration:\n {0}.
+oauthFetchUnexpectedError=The following fatal error occurred when OAuth was fetching content: \n {0}.
+unauthenticatedOauth=OAuth cannot fetch content because the user authentication does not exist. The following error occurred: \n {0}.
+invalidOauth=OAuth cannot fetch content because the request is invalid. The following error occurred: \n {0}.
+
+##CajaCssSanitizer
+failedToParse=The style sheet cannot be parsed.
+unableToConvertScript=The script node cannot be converted to an OpenSocial Markup Language (OSML) tag.
+
+##PipelineExecutor
+errorPreloading=An unexpected error occurred when preloading.
+
+##Processor
+renderBlacklistedGadget=The system attempted to render the following blacklisted gadget: {0}.
+
+##CajaResponseRewriter, CajaContentRewriter
+failedToRetrieve={0} cannot be retrieved.
+failedToRead={0} cannot be read.
+
+##DefaultServiceFetcher
+httpErrorFetching=An HTTP {0} error occurred when fetching service methods from the {1} endpoint.
+failedToFetchService=Services methods from the {0} endpoint could not be fetched. The following error occurred: {1}.
+failedToParseService=Services methods from the {0} endpoint could not be parsed. The following error occurred: {1}.
+
+##Renderer
+FailedToRender=The gadget at {0} did not render. The following error occurred: {1}.
+
+##RenderingGadgetRewriter
+unknownFeatures=One or more unknown features exist in the following extern &libs=: {0}.
+unexpectedErrorPreloading=An unexpected error occurred when preloading the gadget.
+
+##SanitizingResponseRewriter
+requestToSanitizeWithoutContent=A request to sanitize was issued without a content type for {0}.
+requestToSanitizeUnknownContent=A request to sanitize was issued without a known content type {0} for {1}.
+unableToSanitizeUnknownImg=The image type {0} is unknown and cannot be sanitized.
+unableToDetectImgType=The image type for {0} cannot be detected when sanitizing content.
+
+##BasicImageRewriter
+ioErrorRewritingImg=The following input/output error occurred when rewriting the {0} image: {1}.
+unknownErrorRewritingImg=The following error occurred when rewriting the {0} image: {1}.
+failedToReadImg=The {0} image cannot be read and is being skipped. The following error occurred: {1}.
+
+##CssResponseRewriter
+cajaCssParseFailure=The following error occurred when parsing the Caja CSS: {0} for {1}.
+
+##ImageAttributeRewriter
+unableToProcessImg=The {0} image resource cannot be processed.
+unableToReadResponse=The response for the {0} image resource cannot be read.
+unableToFetchImg=The {0} image resource cannot be fetched.
+unableToParseImg=The {0} image resource cannot be parsed.
+
+##MutableContent
+exceptionParsingContent=An exception occurred when parsing content for the gadget.
+
+##PipelineDataGadgetRewriter
+failedToParsePreload=The gadget at {0} could not be parsed for preloading.
+
+##ProxyingVisitor
+uriExceptionParsing=A Uniform Resource Identifier (URI) exception occurred when parsing the gadget at {0}.
+
+##TemplateRewriter
+malformedTemplateLib=Exceptions occurred because of malformed template libraries.
+
+##CajaContnetRewriter
+cajoledCacheCreated=A cajoled cache was created from en_US properties file.
+retrieveReference={0} is being retrieved.
+unableToCajole=The gadget at {0} cannot be cajoled.
+
+##ConcatProxyServlet
+concatProxyRequestFailed=The following error occurred when requesting a concatenated proxy: {0}.
+
+##GadgetRenderingServlet
+malformedTtlValue=An invalid Time To Live (TTL) value of {0} was ignored.
+
+##HttpRequestHandler
+gadgetCreationError=An error occurred creating the gadget for rewriting.  Rewriting the response without the gadget.
+
+##ProxyServlet
+embededImgWrongDomain=The request to embed the {0} URL was made to the wrong domain {1}.
+
+##DefaultTemplateProcessor
+elFailure=The following EL error occurred for the gadget at {0}: {1}.
+
+##UriUtils
+skipIllegalHeader=The {0} header is illegal and is being skipped. The following error occurred: {1}.
+
+##AbstractSpecFactory
+updateSpecFailureUseCacheVersion=An error occurred when updating {0}. Status code {1} was returned. Exception: {2}. A cached version is being used instead.
+updateSpecFailureApplyNegCache=An error occurred when updating {0}. Status code {1} was returned. Exception: {2}. A negative cache is being applied.
+
+##HashLockedDomainService
+noLockedDomainConfig=A locked domain configuration for {0} does not exist.
+
+##Bootstrap
+startingConnManagerWith=Connection manager with {0} properties is starting.
+
+##XSDValidator
+resolveResource=The following resources are being resolved: {0}, {1}, {2}, {3}.
+failedToValidate=An error occurred when validating {0}.
diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_es.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_es.properties
new file mode 100644
index 0000000..e6201ba
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_es.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=La se\u00f1al de seguridad o credencial no tiene formato correcto y no se puede analizar.

+

+##XmlUtil

+errorParsingXML=Error al analizar el XML. Se puede ignorar.

+errorParsingExternalGeneralEntities=El procesador XML que se est\u00e1 usando cargar\u00e1 entidades generales externas.

+errorParsingExternalParameterEntities=El procesador XML que se est\u00e1 usando cargar\u00e1 entidades de par\u00e1metros externos.

+errorParsingExternalDTD=El procesador XML que se est\u00e1 usando cargar\u00e1 Definiciones de tipos de documento (DTD).

+errorNotUsingSecureXML=El procesador XML que se est\u00e1 usando no tiene soporte para el an\u00e1lisis seguro.

+reuseDocumentBuilders=Los constructores de documentos se est\u00e1n reutilizando.

+notReuseDocBuilders=Los constructores de documentos no se est\u00e1n reutilizando.

+

+##LruCacheProvider

+LRUCapacity=La capacidad Menos usada recientemente (LRU, por sus siglas en ingl\u00e9s) {0} est\u00e1 configurada para {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} no se puede evaluar.

+

+##JsonContainerConfigLoader

+readingContainerConfig=La configuraci\u00f3n del contenedor {0} se est\u00e1 leyendo.

+loadFromString={0} no se puede analizar.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Los recursos de {0} se est\u00e1n cargando.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Los archivos de {0} se est\u00e1n cargando.

+

+##ApiServlet

+apiServletProtocolException=Se devuelve un error de respuesta porque se ha producido una excepci\u00f3n de protocolo.

+apiServletException=Se devuelve un error de respuesta porque se ha producido una excepci\u00f3n de protocolo.

+

+##FeatureRegistry

+overridingFeature=La caracter\u00edstica {0} con definici\u00f3n en {1} se sobrescribe.

+

+##FeatureResourceLoader

+missingFile=El archivo {0} exist\u00eda antes, pero ahora falta.

+unableRetrieveLib=La biblioteca remota desde {0} no se puede recuperar.

+

+##BasicHttpFetcher

+timeoutException={0} ha superado el tiempo de espera debido a la excepci\u00f3n siguiente: {1} - {2} - {3} ms.

+exceptionOccurred=Se ha producido la excepci\u00f3n siguiente al captar {0}: {1} ms transcurridos.

+slowResponse={0} est\u00e1 respondiendo muy lentamente. {1} ms transcurridos.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Se ha producido un error al obtener el Resumen de mensajes 5 (Message Digest 5, MD5). El error ha sido ignorado.

+errorParsingMD5=Se ha producido un error al analizar la serie Message Digest 5 (MD5) en formato UTF-8.

+

+##OAuthModule

+usingRandomKey=Se est\u00e1 usando una clave aleatoria para el cifrado del estado del lado del cliente de OAuth.

+usingFile=Se est\u00e1 usando el archivo {0} para el cifrado del estado del lado del cliente de OAuth.

+loadKeyFileFrom=La clave de firma de OAuth de {0} se est\u00e1 cargando.

+couldNotLoadKeyFile= El archivo de claves {0} no se ha podido cargar.

+couldNotLoadSignedKey=La clave de firma de OAuth no se ha cargado correctamente. Para crear una clave: \n 1. Ejecute el mandato siguiente: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Edite el archivo shindig.properties y a\u00f1ada las l\u00edneas siguientes: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=Los consumidores de OAuth desde {0} no se han podido inicializar.

+

+##OAuthRequest

+oauthFetchFatalError=Se ha producido el error muy grave siguiente cuando OAuth captaba contenido: \n {0}.

+oauthFetchErrorReprompt=Se ha producido el error siguiente cuando OAuth captaba contenido: \n \ {0}. Se vuelve a preguntar al usuario por aprobaci\u00f3n.

+bogusExpired=El servidor ha devuelto una caducidad no v\u00e1lida: \n {0}.

+oauthFetchUnexpectedError=Se ha producido el error muy grave siguiente cuando OAuth captaba contenido: \n {0}.

+unauthenticatedOauth=OAuth no puede captar contenido porque la autenticaci\u00f3n de usuario no existe. Se ha producido el error siguiente: \n \ {0}.

+invalidOauth=OAuth no puede captar contenido porque la solicitud no es v\u00e1lida. Se ha producido el error siguiente: \n \ {0}.

+

+##CajaCssSanitizer

+failedToParse=La hoja de estilo no se puede analizar.

+unableToConvertScript=El nodo de script no se puede convertir a una etiqueta OpenSocial Markup Language (OSML).

+

+##PipelineExecutor

+errorPreloading=Se ha producido un error no esperado en la precarga.

+

+##Processor

+renderBlacklistedGadget=El sistema ha intentado representar el gadget siguiente en la lista negra: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} no se puede recuperar.

+failedToRead={0} no se puede leer.

+

+##DefaultServiceFetcher

+httpErrorFetching=Se ha producido un error HTTP {0} al captar m\u00e9todos de servicio desde el punto final {1}.

+failedToFetchService=Los m\u00e9todos del servicio del punto final {0} no se han podido captar. Se ha producido el error siguiente: {1}.

+failedToParseService=Los m\u00e9todos de servicios del punto final {0} no se han podido analizar. Se ha producido el error siguiente: {1}.

+

+##Renderer

+FailedToRender=El gadget en {0} no se ha representado. Se ha producido el error siguiente: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Hay una o m\u00e1s caracter\u00edsticas desconocidas en las extern &libs= siguientes: {0}.

+unexpectedErrorPreloading=Se ha producido un error no esperado en la precarga del gadget.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Se ha emitido una solicitud de saneamiento sin un tipo de contenido para {0}.

+requestToSanitizeUnknownContent=Se ha emitido una solicitud de saneamiento sin un tipo de contenido conocido {0} para {1}.

+unableToSanitizeUnknownImg=El tipo de imagen {0} es desconocido y no se puede sanear.

+unableToDetectImgType=El tipo de imagen para {0} no se puede detectar al sanear el contenido.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Se ha producido el error de entrada/salida siguiente al reescribir la imagen {0}: {1}.

+unknownErrorRewritingImg=Se ha producido el error siguiente al reescribir la imagen {0}: {1}.

+failedToReadImg=La imagen {0} no se puede leer y se omite. Se ha producido el error siguiente: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Se ha producido el error siguiente al analizar la CSS de Caja: {0} para {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=El recurso de imagen {0} no se puede procesar.

+unableToReadResponse=La respuesta para el recurso de imagen {0} no se puede leer.

+unableToFetchImg=El recurso de imagen {0} no se puede captar.

+unableToParseImg=El recurso de imagen {0} no se puede analizar.

+

+##MutableContent

+exceptionParsingContent=Se ha producido una excepci\u00f3n al analizar el contenido del gadget.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=El gadget en {0} no se ha podido analizar para precarga.

+

+##ProxyingVisitor

+uriExceptionParsing=Se ha producido una excepci\u00f3n de identificador universal de recursos (URI) al analizar el gadget en {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Las excepciones se han producido porque las bibliotecas de plantillas no tiene un formato correcto.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Se ha creado una memoria cach\u00e9 de caja.

+retrieveReference={0} se est\u00e1 recuperando.

+unableToCajole=El gadget en {0} no se puede poner en caja.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Se ha producido el error siguiente al solicitar un proxy concatenado: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Un valor de Tiempo de vida (TTL) no v\u00e1lido de {0} se ha ignorado.

+

+##HttpRequestHandler

+gadgetCreationError=Se ha producido un error al crear el gadget para volver a grabar.  Se est\u00e1 volviendo a grabar la respuesta sin el gadget.

+

+##ProxyServlet

+embededImgWrongDomain=La solicitud de incluir el URL {0} se ha realizado en el dominio incorrecto {1}.

+

+##DefaultTemplateProcessor

+elFailure=Se ha producido el error EL siguiente para el gadget en {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=La cabecera de {0} no est\u00e1 permitida y se est\u00e1 saltando. Se ha producido el error siguiente: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Se ha producido un error al actualizar {0}. Se ha devuelto el c\u00f3digo de estado {1}. Excepci\u00f3n: {2}. En su lugar, se est\u00e1 usando una versi\u00f3n en la memoria cach\u00e9.

+updateSpecFailureApplyNegCache=Se ha producido un error al actualizar {0}. Se ha devuelto el c\u00f3digo de estado {1}. Excepci\u00f3n: {2}. Se est\u00e1 aplicando una memoria cach\u00e9 negativa.

+

+##HashLockedDomainService

+noLockedDomainConfig=Una configuraci\u00f3n de dominio bloqueada para {0} no existe.

+

+##Bootstrap

+startingConnManagerWith=El Gestor de conexiones con propiedades {0} se est\u00e1 iniciando.

+

+##XSDValidator

+resolveResource=Los recursos siguientes se est\u00e1n resolviendo: {0}, {1}, {2}, {3}.

+failedToValidate=Se ha producido un error al validar {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Devolviendo respuesta almacenada en memoria cach\u00e9 para la respuesta a {0}.

+staleResponse="Se ha producido un error al solicitar el recurso en {0}, pero tenemos una respuesta anterior en la memoria cach\u00e9. Devolviendo una respuesta posiblemente obsoleta desde la memoria cach\u00e9.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_fi.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_fi.properties
new file mode 100644
index 0000000..bf9a5e1
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_fi.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Suojaussanake tai valtuustieto on muotoiltu virheellisesti, eik\u00e4 sen j\u00e4sennys onnistu.

+

+##XmlUtil

+errorParsingXML=XML-koodin j\u00e4sennyksess\u00e4 on ilmennyt virhe. Virhe voidaan ohittaa.

+errorParsingExternalGeneralEntities=K\u00e4yt\u00f6ss\u00e4 oleva XML-k\u00e4sittelij\u00e4 lataa ulkoisia yleisolioita.

+errorParsingExternalParameterEntities=K\u00e4yt\u00f6ss\u00e4 oleva XML-k\u00e4sittelij\u00e4 lataa ulkoisia parametriolioita.

+errorParsingExternalDTD=K\u00e4yt\u00f6ss\u00e4 oleva XML-k\u00e4sittelij\u00e4 lataa Document Type Definition (DTD) -m\u00e4\u00e4rityksi\u00e4.

+errorNotUsingSecureXML=Suojattu j\u00e4sennys ei ole tuettua k\u00e4yt\u00f6ss\u00e4 olevassa XML-k\u00e4sittelij\u00e4ss\u00e4.

+reuseDocumentBuilders=Asiakirjojen muodostustoimintoja k\u00e4ytet\u00e4\u00e4n uudelleen.

+notReuseDocBuilders=Asiakirjojen muodostustoimintoja ei k\u00e4ytet\u00e4 uudelleen.

+

+##LruCacheProvider

+LRUCapacity=LRU-kapasiteetti {0} (pisimm\u00e4n aikaa sitten k\u00e4ytetty kapasiteetti) on m\u00e4\u00e4ritetty kohteelle {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed=Kohteen {0} arviointi ei onnistu.

+

+##JsonContainerConfigLoader

+readingContainerConfig=J\u00e4rjestelm\u00e4 lukee s\u00e4il\u00f6kokoonpanoa {0}.

+loadFromString=Kohteen {0} j\u00e4sennys ei onnistu.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Kohteen {0} resurssien lataus on meneill\u00e4\u00e4n.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Kohteen {0} tiedostojen lataus on meneill\u00e4\u00e4n.

+

+##ApiServlet

+apiServletProtocolException=J\u00e4rjestelm\u00e4 palauttaa vastausvirheen, koska on ilmennyt yhteysk\u00e4yt\u00e4nt\u00f6\u00f6n liittyv\u00e4 poikkeus.

+apiServletException=J\u00e4rjestelm\u00e4 palauttaa vastausvirheen, koska on ilmennyt yhteysk\u00e4yt\u00e4nt\u00f6\u00f6n liittyv\u00e4 poikkeus.

+

+##FeatureRegistry

+overridingFeature=J\u00e4rjestelm\u00e4 ohittaa ominaisuuden {0}, jonka m\u00e4\u00e4ritys on kohteessa {1}.

+

+##FeatureResourceLoader

+missingFile=Tiedosto {0} oli j\u00e4rjestelm\u00e4ss\u00e4 aiemmin, mutta nyt tiedosto puuttuu.

+unableRetrieveLib=Et\u00e4kirjaston nouto kohteesta {0} ei onnistu.

+

+##BasicHttpFetcher

+timeoutException={0} on aikakatkaistu seuraavan poikkeuksen takia: {1} \u2013 {2} \u2013 {3} ms.

+exceptionOccurred=Kohdetta {0} noudettaessa on ilmennyt seuraava poikkeus: toiminnon toteutus kesti {1} ms.

+slowResponse={0} vastaa hitaasti. Toteutus on kest\u00e4nyt {1} ms.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Message Digest 5 (MD5) -koosteen noudossa on ilmennyt virhe. J\u00e4rjestelm\u00e4 ohitti virheen.

+errorParsingMD5=Message Digest 5 (MD5) -koostemerkkijonon j\u00e4sennyksess\u00e4 UTF-8-muotoon on ilmennyt virhe.

+

+##OAuthModule

+usingRandomKey=J\u00e4rjestelm\u00e4 k\u00e4ytt\u00e4\u00e4 ty\u00f6aseman puolen OAuth-tilansalauksen satunnaista avainta.

+usingFile=J\u00e4rjestelm\u00e4 k\u00e4ytt\u00e4\u00e4 ty\u00f6aseman puolen OAuth-tilansalauksen tiedostoa {0}.

+loadKeyFileFrom=OAuth-allekirjoitusavaimen lataus kohteesta {0} on meneill\u00e4\u00e4n.

+couldNotLoadKeyFile= Avaintiedoston {0} lataus ei onnistunut.

+couldNotLoadSignedKey=OAuth-allekirjoitusavaimen lataus ei onnistunut. Luo avain seuraavasti: \n 1. Aja seuraava komento: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=omatestiavain''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Muokkaa shindig.properties-tiedostoa ja lis\u00e4\u00e4 siihen seuraavat rivit: \n{0} =<oauthkey.pem-tiedoston_polku>\n {1} =mykey\n.

+failedToInit=Kohteen {0} OAuth-kuluttajas\u00e4ikeiden alustus ep\u00e4onnistui.

+

+##OAuthRequest

+oauthFetchFatalError=On ilmennyt seuraava vakava virhe, kun OAuth nouti sis\u00e4lt\u00f6\u00e4: \n {0}.

+oauthFetchErrorReprompt=On ilmennyt seuraava virhe, kun OAuth nouti sis\u00e4lt\u00f6\u00e4: \n {0}. Hyv\u00e4ksynt\u00e4\u00e4 pyydet\u00e4\u00e4n k\u00e4ytt\u00e4j\u00e4lt\u00e4 uudelleen.

+bogusExpired=Palvelin on palauttanut virheellisen vanhentumistiedon:\n {0}.

+oauthFetchUnexpectedError=On ilmennyt seuraava vakava virhe, kun OAuth nouti sis\u00e4lt\u00f6\u00e4: \n {0}.

+unauthenticatedOauth=OAuth ei voi noutaa sis\u00e4lt\u00f6\u00e4, koska k\u00e4ytt\u00e4j\u00e4n todennusta ei ole. On ilmennyt seuraava virhe: \n \ {0}.

+invalidOauth=OAuth ei voi noutaa sis\u00e4lt\u00f6\u00e4, koska pyynt\u00f6 on virheellinen. On ilmennyt seuraava virhe: \n \ {0}.

+

+##CajaCssSanitizer

+failedToParse=Tyylitiedoston j\u00e4sennys ei onnistu.

+unableToConvertScript=Komentosarjan solmun muunto OpenSocial Markup Language (OSML) -tunnisteeksi ei onnistu.

+

+##PipelineExecutor

+errorPreloading=Esilatauksessa on ilmennyt odottamaton virhe.

+

+##Processor

+renderBlacklistedGadget=J\u00e4rjestelm\u00e4 on yritt\u00e4nyt hahmontaa seuraavaa estolistaan lis\u00e4tty\u00e4 gadget-komponenttia: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=Kohteen {0} nouto ei onnistu.

+failedToRead=Kohteen {0} lukeminen ei onnistu.

+

+##DefaultServiceFetcher

+httpErrorFetching=On ilmennyt HTTP-virhe {0} noudettaessa palvelumetodeja p\u00e4\u00e4tepisteest\u00e4 {1}.

+failedToFetchService=Palvelumetodien nouto p\u00e4\u00e4tepisteest\u00e4 {0} ei onnistunut. On ilmennyt seuraava virhe: {1}.

+failedToParseService=P\u00e4\u00e4tepisteest\u00e4 {0} per\u00e4isin olevien palvelumetodien j\u00e4sennys ei onnistunut. On ilmennyt seuraava virhe: {1}.

+

+##Renderer

+FailedToRender=Kohteessa {0} sijaitsevan gadget-komponentin hahmonnus ei onnistunut. On ilmennyt seuraava virhe: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Seuraavissa ulkoisissa kirjastoissa on ainakin yksi tuntematon ominaisuus: {0}.

+unexpectedErrorPreloading=Gadget-komponentin esilatauksessa on ilmennyt odottamaton virhe.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Vaarallisen tai viallisen koodin poistopyynt\u00f6 on annettu ilman kohteen {0} sis\u00e4lt\u00f6lajia.

+requestToSanitizeUnknownContent=Vaarallisen tai viallisen koodin poistopyynt\u00f6 on annettu ilman kohteen {1} tunnettua sis\u00e4lt\u00f6lajia {0}.

+unableToSanitizeUnknownImg=Kuvalaji {0} on tuntematon, eik\u00e4 siit\u00e4 voida poistaa vaarallista tai viallista koodia.

+unableToDetectImgType=J\u00e4rjestelm\u00e4 ei tunnista kohteen {0} kuvalajia poistettaessa vaarallista tai viallista koodia.

+

+##BasicImageRewriter

+ioErrorRewritingImg=J\u00e4rjestelm\u00e4ss\u00e4 on ilmennyt seuraava siirr\u00e4nt\u00e4virhe noudettaessa kuvaa {0}: {1}.

+unknownErrorRewritingImg=J\u00e4rjestelm\u00e4ss\u00e4 on ilmennyt seuraava virhe kirjoitettaessa kuvaa {0} uudelleen: {1}.

+failedToReadImg=Kuvaa {0} ei voi lukea, ja j\u00e4rjestelm\u00e4 ohittaa sen. On ilmennyt seuraava virhe: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Caja CSS -tietoja j\u00e4sennett\u00e4ess\u00e4 on ilmennyt seuraava virhe: {0} ({1}).

+

+##ImageAttributeRewriter

+unableToProcessImg=Kuvaresurssin {0} k\u00e4sittely ei onnistu.

+unableToReadResponse=Kuvaresurssin {0} vastausta ei voi lukea.

+unableToFetchImg=Kuvaresurssin {0} nouto ei onnistu.

+unableToParseImg=Kuvaresurssin {0} j\u00e4sennys ei onnistu.

+

+##MutableContent

+exceptionParsingContent=Gadget-komponentin sis\u00e4lt\u00f6\u00e4 j\u00e4sennett\u00e4ess\u00e4 on ilmennyt poikkeus.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Kohteessa {0} sijaitsevan gadget-komponentin j\u00e4sennys esilatausta varten ei onnistunut.

+

+##ProxyingVisitor

+uriExceptionParsing=On ilmennyt Uniform Resource Identifier (URI) -poikkeus j\u00e4sennett\u00e4ess\u00e4 kohteessa {0} sijaitsevaa gadget-komponenttia.

+

+##TemplateRewriter

+malformedTemplateLib=Virheellisesti muotoillut mallipohjakirjastot ovat aiheuttaneet poikkeuksia.

+

+##CajaContnetRewriter

+cajoledCacheCreated=K\u00e4\u00e4nnetty v\u00e4limuisti on luotu.

+retrieveReference=Kohteen {0} nouto on meneill\u00e4\u00e4n.

+unableToCajole=Kohteessa {0} sijaitsevaa gadget-komponenttia ei voi tallentaa k\u00e4\u00e4nnettyyn v\u00e4limuistiin.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=J\u00e4rjestelm\u00e4ss\u00e4 on ilmennyt seuraava virhe pyydett\u00e4ess\u00e4 liitetty\u00e4 v\u00e4lityspalvelinta: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=J\u00e4rjestelm\u00e4 on ohittanut virheellisen julkaisuarvon (TTL-arvon) {0}.

+

+##HttpRequestHandler

+gadgetCreationError=Gadget-komponentin luonnissa uudelleenkirjoitusta varten on ilmennyt virhe.  J\u00e4rjestelm\u00e4 kirjoittaa vastauksen uudelleen ilman gadget-komponenttia.

+

+##ProxyServlet

+embededImgWrongDomain=URL-osoitteen {0} sis\u00e4llytyspyynt\u00f6 on annettu v\u00e4\u00e4r\u00e4\u00e4n verkkoalueeseen {1}.

+

+##DefaultTemplateProcessor

+elFailure=Kohteessa {0} sijaitsevassa gadget-komponentissa on ilmennyt seuraava EL-virhe: {1}.

+

+##UriUtils

+skipIllegalHeader=Yl\u00e4tunniste {0} ei ole sallittu, ja j\u00e4rjestelm\u00e4 ohittaa sen. On ilmennyt seuraava virhe: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Kohdetta {0} p\u00e4ivitett\u00e4ess\u00e4 on ilmennyt virhe. Palautettu tilakoodi on {1}. Poikkeus: {2}. J\u00e4rjestelm\u00e4 k\u00e4ytt\u00e4\u00e4 sen sijasta v\u00e4limuistiin tallennettua versiota.

+updateSpecFailureApplyNegCache=Kohdetta {0} p\u00e4ivitett\u00e4ess\u00e4 on ilmennyt virhe. Palautettu tilakoodi on {1}. Poikkeus: {2}. J\u00e4rjestelm\u00e4 ottaa k\u00e4ytt\u00f6\u00f6n negatiivisen v\u00e4limuistin.

+

+##HashLockedDomainService

+noLockedDomainConfig=Kohteen {0} lukittua verkkoalueen kokoonpanoa ei ole.

+

+##Bootstrap

+startingConnManagerWith=J\u00e4rjestelm\u00e4 aloittaa yhteyksien hallintaohjelman, johon on m\u00e4\u00e4ritetty seuraavat ominaisuudet: {0}.

+

+##XSDValidator

+resolveResource=Seuraavien resurssien selvitys on meneill\u00e4\u00e4n: {0}, {1}, {2}, {3}.

+failedToValidate=Kohteen {0} tarkistuksessa on ilmennyt virhe.

+

+##DefaultRequestPipeline

+cachedResponse=Pyynn\u00f6n v\u00e4limuistiin tallennettu vastaus palautetaan kohteeseen {0}.

+staleResponse=Pyydett\u00e4ess\u00e4 kohteen {0} resurssia ilmeni virhe, mutta v\u00e4limuistiin on tallennettu aiempi vastaus. J\u00e4rjestelm\u00e4 palauttaa v\u00e4limuistista mahdollisesti vanhentuneen vastauksen.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_fr.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_fr.properties
new file mode 100644
index 0000000..ac8e80d
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_fr.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Le jeton de s\u00e9curit\u00e9 ou les donn\u00e9e d'identification sont syntaxiquement incorrects et ne peuvent \u00eatre analys\u00e9s.

+

+##XmlUtil

+errorParsingXML=Erreur d'analyse syntaxique du code XML. Cette erreur peut \u00eatre ignor\u00e9e.

+errorParsingExternalGeneralEntities=Le processeur XML en cours d'utilisation va charger les entit\u00e9s g\u00e9n\u00e9rales externes.

+errorParsingExternalParameterEntities=Le processeur XML en cours d'utilisation va charger les entit\u00e9s de param\u00e8tre externes.

+errorParsingExternalDTD=Le processeur XML en cours d'utilisation va charger les d\u00e9finitions DTD (Document Type Definitions).

+errorNotUsingSecureXML=Le processeur XML utilis\u00e9 ne prend pas en charge l'analyse s\u00e9curis\u00e9.

+reuseDocumentBuilders=Des g\u00e9n\u00e9rateurs de documents sont r\u00e9utilis\u00e9s.

+notReuseDocBuilders=Des g\u00e9n\u00e9rateurs de documents ne sont pas r\u00e9utilis\u00e9s.

+

+##LruCacheProvider

+LRUCapacity=La capacit\u00e9 {0} la moins r\u00e9cemment utilis\u00e9e (LRU) est configur\u00e9e pour {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} ne peut pas \u00eatre \u00e9valu\u00e9e.

+

+##JsonContainerConfigLoader

+readingContainerConfig=La configuration du conteneur {0} est en cours de lecture.

+loadFromString={0} ne peut pas \u00eatre analys\u00e9.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Les ressources \u00e0 partir de {0} sont en cours de chargement.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Les fichiers \u00e0 partir de {0} sont en cours de chargement.

+

+##ApiServlet

+apiServletProtocolException=Une erreur de r\u00e9ponse est renvoy\u00e9e car une exception de protocole s'est produite.

+apiServletException=Une erreur de r\u00e9ponse est renvoy\u00e9e car une exception de protocole s'est produite.

+

+##FeatureRegistry

+overridingFeature=La fonction {0} avec une d\u00e9finition de {1} est remplac\u00e9e.

+

+##FeatureResourceLoader

+missingFile=Le fichier {0} existait mais il est manquant \u00e0 pr\u00e9sent.

+unableRetrieveLib=Impossible d''extraire la biblioth\u00e8que distante de {0}.

+

+##BasicHttpFetcher

+timeoutException={0} a expir\u00e9 en raison de l''exception suivante : {1}-{2}-{3} ms.

+exceptionOccurred=L''exception suivante s''est produite lors de l''extraction de {0} : {1} ms se sont \u00e9coul\u00e9es.

+slowResponse={0} r\u00e9pond lentement. {1} ms se sont \u00e9coul\u00e9es.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Une erreur s'est produite lors de l'obtention de MD5 (Message Digest 5). Cette erreur a \u00e9t\u00e9 ignor\u00e9e.

+errorParsingMD5=Une erreur s'est produite lors de l'analyse de la cha\u00eene du MD5 (Message Digest 5) au format UTF-8.

+

+##OAuthModule

+usingRandomKey=Une cl\u00e9 al\u00e9atoire pour le chiffrement d'\u00e9tat c\u00f4t\u00e9 client d'OAuth est utilis\u00e9e.

+usingFile=Le fichier {0} pour le chiffrement d''\u00e9tat c\u00f4t\u00e9 client d''OAuth est utilis\u00e9.

+loadKeyFileFrom=La cl\u00e9 de signature OAuth de {0} est en cours de chargement.

+couldNotLoadKeyFile= Le fichier de cl\u00e9s {0} n''a pas pu \u00eatre charg\u00e9.

+couldNotLoadSignedKey=La cl\u00e9 de signature OAuth ne s''est pas charg\u00e9e correctement. Pour cr\u00e9er une cl\u00e9: \n \ 1. Ex\u00e9cutez la commande suivante : \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Editez le fichier shindig.properties en ajoutant les lignes suivantes : \n {0} =<path-to-oauthkey.pem>\n \ {1} = \n " mykey ".

+failedToInit=Les consommateurs OAuth de {0} n''ont pas pu s''initialiser.

+

+##OAuthRequest

+oauthFetchFatalError=L''erreur fatale suivante s''est produite lors de l''extraction de contenu par OAuth : \n \ {0}.

+oauthFetchErrorReprompt=L''erreur suivante s''est produite lors de l''extraction de contenu par OAuth : \n \ {0}. L''utilisateur est invit\u00e9 de nouveau \u00e0 donner son approbation.

+bogusExpired=Le serveur a renvoy\u00e9 une expiration non valide :\n {0}.

+oauthFetchUnexpectedError=L''erreur fatale suivante s''est produite lors de l''extraction de contenu par OAuth : \n \ {0}.

+unauthenticatedOauth=OAuth ne parvient pas \u00e0 extraire le contenu car l''authentification de l''utilisateur n''existe pas. L''erreur suivante s''est produite : \n {0}.

+invalidOauth=OAuth ne parvient pas \u00e0 extraire le contenu car la demande n''est pas valide. L''erreur suivante s''est produite : \n {0}.

+

+##CajaCssSanitizer

+failedToParse=La feuille de style ne peut pas \u00eatre analys\u00e9e.

+unableToConvertScript=Le noeud du script ne peut pas \u00eatre converti en balise OSML (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Une erreur impr\u00e9vue s'est produite lors du pr\u00e9chargement.

+

+##Processor

+renderBlacklistedGadget=Le syst\u00e8me a tent\u00e9 d''afficher le gadget sur liste noire suivant : {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=Impossible d''extraire {0}.

+failedToRead=Impossible de lire {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=Une erreur HTTP {0} s''est produite lors de l''extraction des m\u00e9thodes de service du noeud final {1}.

+failedToFetchService=Les m\u00e9thodes des services du noeud final {0} n''ont pas pu \u00eatre extraites. L''erreur suivante s''est produite : {1}.

+failedToParseService=Les m\u00e9thodes des services du noeud final {0} n''ont pas pu \u00eatre analys\u00e9es. L''erreur suivante s''est produite : {1}.

+

+##Renderer

+FailedToRender=Le gadget sur {0} ne s''est pas affich\u00e9. L''erreur suivante s''est produite : {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Une ou plusieurs fonctions inconnues existent dans le &libs=: {0} externe suivant.

+unexpectedErrorPreloading=Une erreur impr\u00e9vue s'est produite lors du pr\u00e9chargement du gadget.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Une demande d''assainissement a \u00e9t\u00e9 \u00e9mise sans type de contenu pour {0}.

+requestToSanitizeUnknownContent=Une demande d''assainissement a \u00e9t\u00e9 \u00e9mise sans type de contenu connu {0} pour {1}.

+unableToSanitizeUnknownImg=Le type d''image {0} est inconnu et ne peut pas \u00eatre s\u00e9curis\u00e9.

+unableToDetectImgType=Le type d''image pour {0} ne peut pas \u00eatre d\u00e9tect\u00e9 lors de l''assainissement du contenu.

+

+##BasicImageRewriter

+ioErrorRewritingImg=L''erreur d''entr\u00e9e-sortie suivante s''est produite lors de la r\u00e9\u00e9criture de l''image {0} : {1}.

+unknownErrorRewritingImg=L''erreur suivante s''est produite lors de la r\u00e9\u00e9criture de l''image {0} : {1}.

+failedToReadImg=L''image {0} ne peut pas \u00eatre lue et est ignor\u00e9e. L''erreur suivante s''est produite : {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=L''erreur suivante s''est produite lors de l''analyse syntaxique de Caja CSS : {0} pour {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=La ressource image {0} ne peut pas \u00eatre trait\u00e9e.

+unableToReadResponse=La r\u00e9ponse pour la ressource image {0} ne peut pas \u00eatre lue.

+unableToFetchImg=La ressource image {0} ne peut pas \u00eatre extraite.

+unableToParseImg=La ressource image {0} ne peut pas \u00eatre analys\u00e9e.

+

+##MutableContent

+exceptionParsingContent=Une erreur s'est produite lors de l'analyse de contenu du gadget.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Le gadget sur {0} n''a pas pu \u00eatre analys\u00e9 pour le pr\u00e9chargement.

+

+##ProxyingVisitor

+uriExceptionParsing=Une exception URI (Uniform Resource Identifier) s''est produite lors de l''analyse syntaxique du gadget \u00e0 {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Des exceptions se sont produites en raison des biblioth\u00e8ques de mod\u00e8les syntaxiquement incorrectes.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Un cache Caja a \u00e9t\u00e9 cr\u00e9\u00e9.

+retrieveReference={0} est en cours d''extraction.

+unableToCajole=Le gadget sur {0} ne peut pas \u00eatre de type Caja.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=L''erreur suivante s''est produite lors de la demande d''un proxy concat\u00e9n\u00e9 : {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Une valeur de dur\u00e9e de vie (Time To Live - TTL) {0} a \u00e9t\u00e9 ignor\u00e9e.

+

+##HttpRequestHandler

+gadgetCreationError=Une erreur s'est produite lors de la cr\u00e9ation du gadget pour la r\u00e9\u00e9criture.  R\u00e9\u00e9criture de la r\u00e9ponse sans le gadget.

+

+##ProxyServlet

+embededImgWrongDomain=La demande d''imbriquer l''URL {0} a \u00e9t\u00e9 effectu\u00e9e sur le domaine incorrect {1}.

+

+##DefaultTemplateProcessor

+elFailure=L''erreur EL suivante s''est produite pour le gadget \u00e0 {0} : {1}.

+

+##UriUtils

+skipIllegalHeader=L''en-t\u00eate {0} est ill\u00e9gal et est ignor\u00e9. L''erreur suivante s''est produite : {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Une erreur s''est produite lors de la mise \u00e0 jour de {0}. Le code d''\u00e9tat {1} a \u00e9t\u00e9 renvoy\u00e9. Exception : {2}. Une version mise en cache est utilis\u00e9e \u00e0 la place.

+updateSpecFailureApplyNegCache=Une erreur s''est produite lors de la mise \u00e0 jour de {0}. Le code d''\u00e9tat {1} a \u00e9t\u00e9 renvoy\u00e9. Exception : {2}. Un cache n\u00e9gatif est appliqu\u00e9.

+

+##HashLockedDomainService

+noLockedDomainConfig=Une configuration de domaine verrouill\u00e9e pour {0} n''existe pas.

+

+##Bootstrap

+startingConnManagerWith=Le gestionnaire de connexions avec {0} propri\u00e9t\u00e9s est lanc\u00e9.

+

+##XSDValidator

+resolveResource=Les ressources suivantes sont en cours de r\u00e9solution : {0}, {1}, {2}, {3}.

+failedToValidate=Une erreur s''est produite lors de la validation de {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Renvoi de la r\u00e9ponse mise en cache pour la demande sur {0}.

+staleResponse=Une erreur s''est produite lors de la demande de la ressource sur {0} mais une r\u00e9ponse ant\u00e9rieure est disponible en m\u00e9moire cache. Renvoi d''une r\u00e9ponse pouvant \u00eatre p\u00e9rim\u00e9e \u00e0 partir de la m\u00e9moire cache.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_hu.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_hu.properties
new file mode 100644
index 0000000..f9653f1
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_hu.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=A biztons\u00e1gi jelsor vagy hiteles\u00edt\u00e9si adatok hib\u00e1s form\u00e1tum\u00faak \u00e9s nem \u00e9rtelmezhet\u0151k.

+

+##XmlUtil

+errorParsingXML=Hiba az XML \u00e9rtelmez\u00e9sekor. Ez figyelmen k\u00edv\u00fcl hagyhat\u00f3.

+errorParsingExternalGeneralEntities=A haszn\u00e1lt XML feldolgoz\u00f3 k\u00fcls\u0151 \u00e1ltal\u00e1nos entit\u00e1sokat fog bet\u00f6lteni.

+errorParsingExternalParameterEntities=A haszn\u00e1lt XML feldolgoz\u00f3 k\u00fcls\u0151 param\u00e9ter entit\u00e1sokat fog bet\u00f6lteni.

+errorParsingExternalDTD=A haszn\u00e1lt XML feldolgoz\u00f3 k\u00fcls\u0151 dokumentumt\u00edpus meghat\u00e1roz\u00e1sokat (DTD) fog bet\u00f6lteni.

+errorNotUsingSecureXML=A haszn\u00e1lt XML feldolgoz\u00f3 nem t\u00e1mogatja a biztons\u00e1gos \u00e9rtelmez\u00e9st.

+reuseDocumentBuilders=A dokumentum\u00e9p\u00edt\u0151k \u00fajrafelhaszn\u00e1l\u00e1sa.

+notReuseDocBuilders=A dokumentum\u00e9p\u00edt\u0151k nincsenek \u00fajrafelhaszn\u00e1lva.

+

+##LruCacheProvider

+LRUCapacity=A legr\u00e9gebben haszn\u00e1lt (LRU) kapacit\u00e1s {0} a k\u00f6vetkez\u0151h\u00f6z van be\u00e1ll\u00edtva: {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} nem \u00e9rt\u00e9kelhet\u0151 ki.

+

+##JsonContainerConfigLoader

+readingContainerConfig={0} t\u00e1rol\u00f3be\u00e1ll\u00edt\u00e1s olvas\u00e1sa.

+loadFromString={0} nem \u00e9rtelmezhet\u0151.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom={0} er\u0151forr\u00e1sainak bet\u00f6lt\u00e9se folyamatban.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom={0} f\u00e1jljainak bet\u00f6lt\u00e9se folyamatban.

+

+##ApiServlet

+apiServletProtocolException=A program v\u00e1laszhib\u00e1t ad vissza, mert protokollkiv\u00e9tel t\u00f6rt\u00e9nt.

+apiServletException=A program v\u00e1laszhib\u00e1t ad vissza, mert protokollkiv\u00e9tel t\u00f6rt\u00e9nt.

+

+##FeatureRegistry

+overridingFeature=A(z) {1} helyen meghat\u00e1rozott {0} funkci\u00f3 fel\u00fclb\u00edr\u00e1l\u00e1sa folyamatban.

+

+##FeatureResourceLoader

+missingFile=A(z) {0} f\u00e1jl l\u00e9tezett, de most hi\u00e1nyzik.

+unableRetrieveLib=Nem lehet beolvasni a t\u00e1voli k\u00f6nyvt\u00e1rt a k\u00f6vetkez\u0151b\u0151l: {0}.

+

+##BasicHttpFetcher

+timeoutException={0} t\u00fall\u00e9pte az id\u0151korl\u00e1tot a k\u00f6vetkez\u0151 kiv\u00e9tel miatt: {1} - {2} - {3} ms.

+exceptionOccurred=A k\u00f6vetkez\u0151 kiv\u00e9tel t\u00f6rt\u00e9nt {0} lek\u00e9r\u00e9se sor\u00e1n: {1} ms telt el.

+slowResponse={0} lassan v\u00e1laszol. {1} ms telt el.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Hiba t\u00f6rt\u00e9nt az MD5 \u00fczenetkivonat beolvas\u00e1sakor. A program figyelmen k\u00edv\u00fcl hagyta a hib\u00e1t.

+errorParsingMD5=Hiba t\u00f6rt\u00e9nt az UTF-8 form\u00e1tum\u00fa MD5 \u00fczenetkivonat karaktersorozat\u00e1nak \u00e9rtelmez\u00e9sekor.

+

+##OAuthModule

+usingRandomKey=Az OAuth \u00fcgyf\u00e9loldali \u00e1llapot titkos\u00edt\u00e1shoz v\u00e9letlenszer\u0171 kulcsot haszn\u00e1l a program.

+usingFile=Az OAuth \u00fcgyf\u00e9loldali \u00e1llapot titkos\u00edt\u00e1shoz {0} f\u00e1jlt haszn\u00e1l a program.

+loadKeyFileFrom=Az OAuth al\u00e1\u00edr\u00e1si kulcs bet\u00f6lt\u00e9se folyamatban a k\u00f6vetkez\u0151b\u0151l: {0}.

+couldNotLoadKeyFile= A(z) {0} kulcsf\u00e1jlt nem lehetett bet\u00f6lteni.

+couldNotLoadSignedKey=Az OAuth al\u00e1\u00edr\u00e1si kulcsot nem lehetett bet\u00f6lteni megfelel\u0151en. Kulcs l\u00e9trehoz\u00e1s\u00e1hoz: \n 1. Futtassa a k\u00f6vetkez\u0151 parancsot: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. M\u00f3dos\u00edtsa a shindig.properties f\u00e1jlt \u00e9s adja hozz\u00e1 az al\u00e1bbi sorokat: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=Nem siker\u00fclt az OAuth fogyaszt\u00f3k inicializ\u00e1l\u00e1sa a k\u00f6vetkez\u0151b\u0151l: {0}.

+

+##OAuthRequest

+oauthFetchFatalError=Az al\u00e1bbi v\u00e9gzetes hiba t\u00f6rt\u00e9nt, amikor az OAuth tartalmat k\u00e9rt le: \n {0}.

+oauthFetchErrorReprompt=Az al\u00e1bbi hiba t\u00f6rt\u00e9nt, amikor az OAuth tartalmat k\u00e9rt le: \n {0}. A program \u00fajb\u00f3l k\u00e9ri a felhaszn\u00e1l\u00f3 j\u00f3v\u00e1hagy\u00e1s\u00e1t.

+bogusExpired=A szerver \u00e9rv\u00e9nytelen lej\u00e1ratot adott vissza:\n {0}.

+oauthFetchUnexpectedError=Az al\u00e1bbi v\u00e9gzetes hiba t\u00f6rt\u00e9nt, amikor az OAuth tartalmat k\u00e9rt le: \n {0}.

+unauthenticatedOauth=Az OAuth nem tud tartalmat lek\u00e9rni, mert a felhaszn\u00e1l\u00f3i hiteles\u00edt\u00e9s nem l\u00e9tezik. Az al\u00e1bbi hiba t\u00f6rt\u00e9nt: \n {0}.

+invalidOauth=Az OAuth nem tud tartalmat lek\u00e9rni, mert a k\u00e9r\u00e9s \u00e9rv\u00e9nytelen. Az al\u00e1bbi hiba t\u00f6rt\u00e9nt: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=A st\u00edluslapot nem lehet \u00e9rtelmezni.

+unableToConvertScript=A parancsf\u00e1jl-csom\u00f3pontot nem lehet \u00e1talak\u00edtani OpenSocial Markup Language (OSML) c\u00edmk\u00e9re.

+

+##PipelineExecutor

+errorPreloading=Nem v\u00e1rt hiba t\u00f6rt\u00e9nt az el\u0151bet\u00f6lt\u00e9skor.

+

+##Processor

+renderBlacklistedGadget=A rendszer megpr\u00f3b\u00e1lta el\u0151\u00e1ll\u00edtani az al\u00e1bbi feketelist\u00e1s vez\u00e9rl\u0151elemet: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} nem olvashat\u00f3 be.

+failedToRead={0} nem olvashat\u00f3.

+

+##DefaultServiceFetcher

+httpErrorFetching=HTTP {0} hiba t\u00f6rt\u00e9nt a szolg\u00e1ltat\u00e1smet\u00f3dusok lek\u00e9r\u00e9sekor {1} v\u00e9gpontb\u00f3l.

+failedToFetchService=A(z) {0} v\u00e9gpont szolg\u00e1ltat\u00e1smet\u00f3dusait nem lehetett lek\u00e9rni. A k\u00f6vetkez\u0151 hiba t\u00f6rt\u00e9nt: {1}.

+failedToParseService=A(z) {0} v\u00e9gpont szolg\u00e1ltat\u00e1smet\u00f3dusait nem lehetett \u00e9rtelmezni. Az al\u00e1bbi hiba t\u00f6rt\u00e9nt: {1}.

+

+##Renderer

+FailedToRender=Nem siker\u00fclt a vez\u00e9rl\u0151elem el\u0151\u00e1ll\u00edt\u00e1sa a k\u00f6vetkez\u0151 helyen: {0}. Az al\u00e1bbi hiba t\u00f6rt\u00e9nt: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=N\u00e9h\u00e1ny ismeretlen funkci\u00f3 l\u00e9tezik a k\u00f6vetkez\u0151 k\u00fcls\u0151 k\u00f6nyvt\u00e1rakban=: {0}.

+unexpectedErrorPreloading=Nem v\u00e1rt hiba t\u00f6rt\u00e9nt a vez\u00e9rl\u0151elem el\u0151bet\u00f6lt\u00e9sekor.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Megtiszt\u00edt\u00e1si k\u00e9r\u00e9st adtak ki tartalomt\u00edpus n\u00e9lk\u00fcl a k\u00f6vetkez\u0151re: {0}.

+requestToSanitizeUnknownContent=Megtiszt\u00edt\u00e1si k\u00e9r\u00e9st adtak ki ismert {0} tartalomt\u00edpus n\u00e9lk\u00fcl a k\u00f6vetkez\u0151re: {1}.

+unableToSanitizeUnknownImg=A(z) {0} k\u00e9pt\u00edpus ismeretlen \u00e9s nem lehet megtiszt\u00edtani.

+unableToDetectImgType={0} k\u00e9pt\u00edpusa nem \u00e9szlelhet\u0151 a tartalom megtiszt\u00edt\u00e1sakor.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Az al\u00e1bbi bemeneti/kimeneti hiba t\u00f6rt\u00e9nt a(z) {0} k\u00e9p \u00fajra\u00edr\u00e1sakor: {1}.

+unknownErrorRewritingImg=Az al\u00e1bbi hiba t\u00f6rt\u00e9nt a(z) {0} k\u00e9p \u00fajra\u00edr\u00e1sakor: {1}.

+failedToReadImg=A(z) {0} k\u00e9p nem olvashat\u00f3 \u00e9s kihagyja a program. Az al\u00e1bbi hiba t\u00f6rt\u00e9nt: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Az al\u00e1bbi hiba t\u00f6rt\u00e9nt a Caja CSS \u00e9rtelmez\u00e9sekor: {0}: {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=A(z) {0} k\u00e9p er\u0151forr\u00e1st nem lehet feldolgozni.

+unableToReadResponse=A(z) {0} k\u00e9p er\u0151forr\u00e1s v\u00e1lasza nem olvashat\u00f3.

+unableToFetchImg=A(z) {0} k\u00e9p er\u0151forr\u00e1st nem lehet lek\u00e9rni.

+unableToParseImg=A(z) {0} k\u00e9p er\u0151forr\u00e1st nem lehet \u00e9rtelmezni.

+

+##MutableContent

+exceptionParsingContent=Kiv\u00e9tel t\u00f6rt\u00e9nt a tartalom \u00e9rtelmez\u00e9sekor a vez\u00e9rl\u0151elemhez.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Nem lehetett el\u0151bet\u00f6lt\u00e9shez \u00e9rtelmezni a vez\u00e9rl\u0151elemet a k\u00f6vetkez\u0151 helyen: {0}

+

+##ProxyingVisitor

+uriExceptionParsing=URI kiv\u00e9tel t\u00f6rt\u00e9nt a k\u00f6vetkez\u0151 helyen l\u00e9v\u0151 vez\u00e9rl\u0151elem \u00e9rtelmez\u00e9sekor: {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Kiv\u00e9telek t\u00f6rt\u00e9ntek hib\u00e1s form\u00e1tum\u00fa sablonk\u00f6nyvt\u00e1rak miatt.

+

+##CajaContnetRewriter

+cajoledCacheCreated=L\u00e9trej\u00f6tt egy caja gyors\u00edt\u00f3t\u00e1r.

+retrieveReference={0} beolvas\u00e1sa folyamatban.

+unableToCajole=Nem lehets\u00e9ges a k\u00f6vetkez\u0151 helyen l\u00e9v\u0151 vez\u00e9rl\u0151elem caja feldolgoz\u00e1sa: {0}.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Az al\u00e1bbi hiba t\u00f6rt\u00e9nt \u00f6sszef\u0171z\u00f6tt proxy k\u00e9r\u00e9sekor: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Egy \u00e9rv\u00e9nytelen {0} \u00e9lettartam (TTL) \u00e9rt\u00e9ket figyelmen k\u00edv\u00fcl hagyott a program.

+

+##HttpRequestHandler

+gadgetCreationError=Hiba t\u00f6rt\u00e9nt az \u00fajra\u00edr\u00e1si vez\u00e9rl\u0151elem l\u00e9trehoz\u00e1sakor.  A v\u00e1lasz \u00e1t\u00edr\u00e1s\u00e1ra a vez\u00e9rl\u0151elem n\u00e9lk\u00fcl ker\u00fcl sor.

+

+##ProxyServlet

+embededImgWrongDomain=A(z) {0} URL be\u00e1gyaz\u00e1si k\u00e9r\u00e9se rossz tartom\u00e1nyba ({1}) lett elk\u00fcldve.

+

+##DefaultTemplateProcessor

+elFailure=Az al\u00e1bbi EL hiba t\u00f6rt\u00e9nt a k\u00f6vetkez\u0151 helyen l\u00e9v\u0151 vez\u00e9rl\u0151elemn\u00e9l: {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=A(z) {0} fejl\u00e9c \u00e9rv\u00e9nytelen \u00e9s kihagyja a program. Az al\u00e1bbi hiba t\u00f6rt\u00e9nt: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Hiba t\u00f6rt\u00e9nt {0} friss\u00edt\u00e9sekor. {1} \u00e1llapotk\u00f3d \u00e9rkezett vissza. Kiv\u00e9tel: {2}. A program ehelyett egy gyors\u00edt\u00f3t\u00e1rban l\u00e9v\u0151 v\u00e1ltozatot haszn\u00e1l.

+updateSpecFailureApplyNegCache=Hiba t\u00f6rt\u00e9nt {0} friss\u00edt\u00e9sekor. {1} \u00e1llapotk\u00f3d \u00e9rkezett vissza. Kiv\u00e9tel: {2}. Negat\u00edv gyors\u00edt\u00f3t\u00e1r alkalmaz\u00e1s\u00e1ra ker\u00fcl sor.

+

+##HashLockedDomainService

+noLockedDomainConfig=Nem l\u00e9tezik {0} z\u00e1rolt tartom\u00e1nybe\u00e1ll\u00edt\u00e1sa.

+

+##Bootstrap

+startingConnManagerWith={0} tulajdons\u00e1ggal rendelkez\u0151 kapcsolatkezel\u0151 ind\u00edt\u00e1sa folyamatban.

+

+##XSDValidator

+resolveResource=Az al\u00e1bbi er\u0151forr\u00e1sok felold\u00e1sa folyamatban: {0}, {1}, {2}, {3}.

+failedToValidate=Hiba t\u00f6rt\u00e9nt {0} \u00e9rv\u00e9nyes\u00edt\u00e9sekor.

+

+##DefaultRequestPipeline

+cachedResponse=Gyors\u00edt\u00f3t\u00e1rban l\u00e9v\u0151 v\u00e1lasz visszaad\u00e1sa a k\u00e9r\u00e9sre {0} sz\u00e1m\u00e1ra.

+staleResponse="Hiba t\u00f6rt\u00e9nt {0} er\u0151forr\u00e1s\u00e1nak k\u00e9r\u00e9sekor, de egy kor\u00e1bbi v\u00e1lasz megtal\u00e1lhat\u00f3 a gyors\u00edt\u00f3t\u00e1rban. A program visszaadja a gyors\u00edt\u00f3t\u00e1rb\u00f3l az esetlegesen elavult v\u00e1laszt.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_it.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_it.properties
new file mode 100644
index 0000000..400300e
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_it.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Il token di sicurezza o le credenziali non hanno il formato corretto e non possono essere analizzati.

+

+##XmlUtil

+errorParsingXML=Errore durante l'analisi di XML. Questo errore pu\u00f2 essere ignorato.

+errorParsingExternalGeneralEntities=Il processore XML utilizzato caricher\u00e0 le entit\u00e0 generali esterne.

+errorParsingExternalParameterEntities=Il processore XML utilizzato caricher\u00e0 le entit\u00e0 dei parametri esterne.

+errorParsingExternalDTD=Il processore XML utilizzato caricher\u00e0 le DTD (Document Type Definitions).

+errorNotUsingSecureXML=Il processore XML utilizzato non supporta l'analisi sicura.

+reuseDocumentBuilders=I builder di documenti stanno per essere riutilizzati.

+notReuseDocBuilders=I builder di documenti non stanno per essere riutilizzati.

+

+##LruCacheProvider

+LRUCapacity=La capacit\u00e0 LRU (least recently used) {0} \u00e8 configurata per {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed=Impossibile valutare {0}.

+

+##JsonContainerConfigLoader

+readingContainerConfig=La configurazione del contenitore {0} sta per essere letta.

+loadFromString=Impossibile analizzare {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Le risorse da {0} stanno per essere caricate.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=I file da {0} stanno per essere caricati.

+

+##ApiServlet

+apiServletProtocolException=\u00c8 stato restituito un errore di risposta in quanto si \u00e8 verificata un'eccezione del protocollo.

+apiServletException=\u00c8 stato restituito un errore di risposta in quanto si \u00e8 verificata un'eccezione del protocollo.

+

+##FeatureRegistry

+overridingFeature=La funzione {0} con definizione su {1} sta per essere ignorata.

+

+##FeatureResourceLoader

+missingFile=Il file {0} esisteva ma adesso non \u00e8 presente.

+unableRetrieveLib=La libreria remota da {0} non pu\u00f2 essere richiamata.

+

+##BasicHttpFetcher

+timeoutException={0} \u00e8 scaduto a causa della seguente eccezione: {1} - {2} - {3} ms.

+exceptionOccurred=Si \u00e8 verificata la seguente eccezione durante il caricamento di {0}: {1} ms trascorsi.

+slowResponse={0} sta rispondendo troppo lentamente. {1} ms trascorsi.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Si \u00e8 verificato un errore durante l'ottenimento di Message Digest 5 (MD5). L'errore \u00e8 stato ignorato.

+errorParsingMD5=Si \u00e8 verificato un errore durante l'analisi della stringa Message Digest 5 (MD5) in formato UTF-8.

+

+##OAuthModule

+usingRandomKey=Sta per essere utilizzata una chiave casuale per la crittografia dello stato del client OAuth.

+usingFile=Sta per essere utilizzato il file {0} per la crittografia dello stato del client OAuth.

+loadKeyFileFrom=La chiave di firma OAuth da {0} sta per essere caricata.

+couldNotLoadKeyFile= Il file di chiavi di {0} non \u00e8 stato caricato.

+couldNotLoadSignedKey=La chiave di firma OAuth non \u00e8 stata caricata correttamente. Per creare una chiave: \n 1. Emettere il seguente comando: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Modificare il file shindig.properties aggiungendo le seguenti righe: \n{0} =<percorso-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=I consumer OAuth da {0} non sono stati inizializzati.

+

+##OAuthRequest

+oauthFetchFatalError=Si \u00e8 verificato il seguente errore grave durante il caricamento del contenuto da parte di OAuth: \n {0}.

+oauthFetchErrorReprompt=Si \u00e8 verificato il seguente errore durante il caricamento del contenuto da parte di OAuth: \n {0}. All''utente verr\u00e0 richiesta l''approvazione.

+bogusExpired=Il server ha restituito una scadenza non valida: \n \ {0}.

+oauthFetchUnexpectedError=Si \u00e8 verificato il seguente errore grave durante il caricamento del contenuto da parte di OAuth: \n {0}.

+unauthenticatedOauth=OAuth non pu\u00f2 caricare il contenuto in quanto l''autenticazione utente non esiste. Si \u00e8 verificato il seguente errore: \n {0}.

+invalidOauth=OAuth non \u00e8 in grado di caricare il contenuto in quanto la richiesta non \u00e8 valida. Si \u00e8 verificato il seguente errore: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=Impossibile analizzare il foglio di stile.

+unableToConvertScript=Il nodo di script non pu\u00f2 essere convertito in un tag OSML (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Si \u00e8 verificato un errore imprevisto durante il precaricamento.

+

+##Processor

+renderBlacklistedGadget=Il sistema ha provato a eseguire il rendering del seguente gadget nella blacklist: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=Impossibile richiamare {0}.

+failedToRead=Impossibile leggere {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=Si \u00e8 verificato un errore HTTP {0} durante il caricamento dei metodi di servizio dall''endpoint {1}.

+failedToFetchService=I metodi dei servizi dall''endpoint {0} non sono stati caricati. Si \u00e8 verificato il seguente errore: {1}.

+failedToParseService=I metodi dei servizi dall''endpoint {0} non sono stati analizzati. Si \u00e8 verificato il seguente errore: {1}.

+

+##Renderer

+FailedToRender=Non \u00e8 stato eseguito il rendering del gadget su {0}. Si \u00e8 verificato il seguente errore: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Una o pi\u00f9 funzioni sconosciute esistono nelle seguenti &librerie esterne=: {0}.

+unexpectedErrorPreloading=Si \u00e8 verificato un errore non previsto durante il precaricamento del gadget.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u00c8 stata emessa una richiesta di ripulitura senza un tipo di contenuto per {0}.

+requestToSanitizeUnknownContent=\u00c8 stata emessa una richiesta di ripulitura senza un tipo di contenuto noto {0} per {1}.

+unableToSanitizeUnknownImg=Il tipo di immagine {0} non \u00e8 noto e non pu\u00f2 essere ripulito.

+unableToDetectImgType=Il tipo di immagine per {0} non pu\u00f2 essere rilevato senza una ripulitura del contenuto.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Si \u00e8 verificato il seguente errore di input/output durante la riscrittura dell''immagine {0}: {1}.

+unknownErrorRewritingImg=Si \u00e8 verificato il seguente errore durante la riscrittura dell''immagine {0}: {1}.

+failedToReadImg=Impossibile leggere l''immagine {0} e pertanto verr\u00e0 ignorata. Si \u00e8 verificato il seguente errore: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Si \u00e8 verificato il seguente errore durante l''analisi del CSS Caja {0} per {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=Impossibile elaborare la risorsa dell''immagine {0}.

+unableToReadResponse=La risposta per la risorsa dell''immagine {0} non pu\u00f2 essere letta.

+unableToFetchImg=Impossibile caricare la risorsa dell''immagine {0}.

+unableToParseImg=Impossibile analizzare la risorsa dell''immagine {0}.

+

+##MutableContent

+exceptionParsingContent=Si \u00e8 verificata un'eccezione durante l'analisi del contenuto per il gadget.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Il gadget su {0} non \u00e8 stato analizzato per il precaricamento.

+

+##ProxyingVisitor

+uriExceptionParsing=Si \u00e8 verificata un''eccezione URI (Uniform Resource Identifier) durante l''analisi del gadget su {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Si sono verificate delle eccezioni a causa di librerie di modello strutturate non correttamente.

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u00c8 stata creata una cache cajol.

+retrieveReference={0} sta per essere richiamato.

+unableToCajole=Il gadget su {0} non pu\u00f2 essere inserito nel cajol.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Si \u00e8 verificato il seguente errore durante la richiesta di un proxy concatenato: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Un valore TTL (Time To Live) non valido di {0} \u00e8 stato ignorato.

+

+##HttpRequestHandler

+gadgetCreationError=Si \u00e8 verificato un errore durante la creazione del gadget per la riscrittura.  La risposta verr\u00e0 riscritta senza il gadget.

+

+##ProxyServlet

+embededImgWrongDomain=La richiesta di integrazione dell''URL {0} \u00e8 stata effettuata al dominio non corretto {1}.

+

+##DefaultTemplateProcessor

+elFailure=Si \u00e8 verificato il seguente errore EL per il gadget su {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=L''intestazione {0} non \u00e8 consentita e pertanto verr\u00e0 ignorata. Si \u00e8 verificato il seguente errore: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Si \u00e8 verificato un errore durante l''aggiornamento di {0}. \u00c8 stato restituito il codice di stato {1}. Eccezione: {2}. Verr\u00e0 utilizzata una versione memorizzata nella cache.

+updateSpecFailureApplyNegCache=Si \u00e8 verificato un errore durante l''aggiornamento di {0}. \u00c8 stato restituito il codice di stato {1}. Eccezione: {2}. Verr\u00e0 applicata una cache negativa.

+

+##HashLockedDomainService

+noLockedDomainConfig=Una configurazione del dominio bloccato per {0} non esiste.

+

+##Bootstrap

+startingConnManagerWith=Il gestore connessioni con le propriet\u00e0 {0} sta per essere avviato.

+

+##XSDValidator

+resolveResource=Le seguenti risorse stanno per essere risolte: {0}, {1}, {2}, {3}.

+failedToValidate=Si \u00e8 verificato un errore durante la convalida di {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Restituzione in corso della risposta memorizzata nella cache per la richiesta a {0}.

+staleResponse="Si \u00e8 verificato un errore durante la richiesta della risorsa su {0} ma \u00e8 presente una risposta precedente nella cache.  Verr\u00e0 restituita una risposta di possibile stallo dalla cache.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_iw.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_iw.properties
new file mode 100644
index 0000000..7e982a2
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_iw.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=The security token or credential is malformed and cannot be parsed.

+

+##XmlUtil

+errorParsingXML=Error parsing the XML. This can be ignored.

+errorParsingExternalGeneralEntities=The XML processor being used will load external general entities.

+errorParsingExternalParameterEntities=The XML processor being used will load external parameter entities.

+errorParsingExternalDTD=The XML processor being used will load Document Type Definitions (DTD).

+errorNotUsingSecureXML=The XML processor being used does not support secure parsing.

+reuseDocumentBuilders=Document builders are being reused.

+notReuseDocBuilders=Document builders are not being reused.

+

+##LruCacheProvider

+LRUCapacity=The least recently used (LRU) capacity {0} is configured for {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} cannot be evaluated.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Container configuration {0} is being read.

+loadFromString={0} cannot be parsed.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Resources from {0} are loading.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Files from {0} are loading.

+

+##ApiServlet

+apiServletProtocolException=A response error is being returned because a protocol exception occurred.

+apiServletException=A response error is being returned because a protocol exception occurred.occurred.

+

+##FeatureRegistry

+overridingFeature=The {0} feature with definition at {1} is being overridden.

+

+##FeatureResourceLoader

+missingFile=The file {0} used to exist but is now missing.

+unableRetrieveLib=The remote library from {0} cannot be retrieved.

+

+##BasicHttpFetcher

+timeoutException={0} has timed out because of the following exception: {1} - {2} - {3} ms.

+exceptionOccurred=The following exception occurred when fetching {0}: {1} ms elapsed.

+slowResponse={0} is responding slowly. {1} ms elapsed.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=An error occurred when getting Message Digest 5 (MD5). The error was ignored.

+errorParsingMD5=An error occurred when parsing the Message Digest 5 (MD5) string in UTF-8 format.

+

+##OAuthModule

+usingRandomKey=A random key for OAuth client-side state encryption is being used.

+usingFile=The {0} file for OAuth client-side state encryption is being used.

+loadKeyFileFrom=The OAuth signing key from {0} is loading.

+couldNotLoadKeyFile= The {0} key file could not be loaded.

+couldNotLoadSignedKey=The OAuth signing key did not load correctly. To create a key: \n 1. Run the following command: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Edit the shindig.properties file by adding these lines: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=OAuth consumers from {0} failed to initialize.

+

+##OAuthRequest

+oauthFetchFatalError=The following fatal error occurred when OAuth was fetching content: \n {0}.

+oauthFetchErrorReprompt=The following error occurred when OAuth was fetching content: \n {0}. The user is being reprompted for approval.

+bogusExpired=The server returned an invalid expiration:\n {0}.

+oauthFetchUnexpectedError=The following fatal error occurred when OAuth was fetching content: \n {0}.

+unauthenticatedOauth=OAuth cannot fetch content because the user authentication does not exist. The following error occurred: \n {0}.

+invalidOauth=OAuth cannot fetch content because the request is invalid. The following error occurred: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=The style sheet cannot be parsed.

+unableToConvertScript=The script node cannot be converted to an OpenSocial Markup Language (OSML) tag.

+

+##PipelineExecutor

+errorPreloading=An unexpected error occurred when preloading.

+

+##Processor

+renderBlacklistedGadget=The system attempted to render the following blacklisted gadget: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} cannot be retrieved.

+failedToRead={0} cannot be read.

+

+##DefaultServiceFetcher

+httpErrorFetching=An HTTP {0} error occurred when fetching service methods from the {1} endpoint.

+failedToFetchService=Services methods from the {0} endpoint could not be fetched. The following error occurred: {1}.

+failedToParseService=Services methods from the {0} endpoint could not be parsed. The following error occurred: {1}.

+

+##Renderer

+FailedToRender=The gadget at {0} did not render. The following error occurred: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=One or more unknown features exist in the following extern &libs=: {0}.

+unexpectedErrorPreloading=An unexpected error occurred when preloading the gadget.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=A request to sanitize was issued without a content type for {0}.

+requestToSanitizeUnknownContent=A request to sanitize was issued without a known content type {0} for {1}.

+unableToSanitizeUnknownImg=The image type {0} is unknown and cannot be sanitized.

+unableToDetectImgType=The image type for {0} cannot be detected when sanitizing content.

+

+##BasicImageRewriter

+ioErrorRewritingImg=The following input/output error occurred when rewriting the {0} image: {1}.

+unknownErrorRewritingImg=The following error occurred when rewriting the {0} image: {1}.

+failedToReadImg=The {0} image cannot be read and is being skipped. The following error occurred: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=The following error occurred when parsing the Caja CSS: {0} for {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=The {0} image resource cannot be processed.

+unableToReadResponse=The response for the {0} image resource cannot be read.

+unableToFetchImg=The {0} image resource cannot be fetched.

+unableToParseImg=The {0} image resource cannot be parsed.

+

+##MutableContent

+exceptionParsingContent=An exception occurred when parsing content for the gadget.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=The gadget at {0} could not be parsed for preloading.

+

+##ProxyingVisitor

+uriExceptionParsing=A Uniform Resource Identifier (URI) exception occurred when parsing the gadget at {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Exceptions occurred because of malformed template libraries.

+

+##CajaContnetRewriter

+cajoledCacheCreated=A cajoled cache was created.

+retrieveReference={0} is being retrieved.

+unableToCajole=The gadget at {0} cannot be cajoled.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=The following error occurred when requesting a concatenated proxy: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=An invalid Time To Live (TTL) value of {0} was ignored.

+

+##HttpRequestHandler

+gadgetCreationError=An error occurred creating the gadget for rewriting.  Rewriting the response without the gadget.

+

+##ProxyServlet

+embededImgWrongDomain=The request to embed the {0} URL was made to the wrong domain {1}.

+

+##DefaultTemplateProcessor

+elFailure=The following EL error occurred for the gadget at {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=The {0} header is illegal and is being skipped. The following error occurred: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=An error occurred when updating {0}. Status code {1} was returned. Exception: {2}. A cached version is being used instead.

+updateSpecFailureApplyNegCache=An error occurred when updating {0}. Status code {1} was returned. Exception: {2}. A negative cache is being applied.

+

+##HashLockedDomainService

+noLockedDomainConfig=A locked domain configuration for {0} does not exist.

+

+##Bootstrap

+startingConnManagerWith=Connection manager with {0} properties is starting.

+

+##XSDValidator

+resolveResource=The following resources are being resolved: {0}, {1}, {2}, {3}.

+failedToValidate=An error occurred when validating {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Returning cached response for the request to {0}.

+staleResponse="There was an error requesting the resource at {0} but we have a prior response in the cache.  Returning a possibly stale response from the cache.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ja.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ja.properties
new file mode 100644
index 0000000..2938c35
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ja.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fc\u30fb\u30c8\u30fc\u30af\u30f3\u307e\u305f\u306f\u8cc7\u683c\u60c5\u5831\u306e\u5f62\u5f0f\u304c\u6b63\u3057\u304f\u306a\u3044\u305f\u3081\u3001\u69cb\u6587\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3002

+

+##XmlUtil

+errorParsingXML=XML \u306e\u69cb\u6587\u89e3\u6790\u6642\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002 \u3053\u308c\u306f\u7121\u8996\u3057\u3066\u304b\u307e\u3044\u307e\u305b\u3093\u3002

+errorParsingExternalGeneralEntities=\u4f7f\u7528\u4e2d\u306e XML \u30d7\u30ed\u30bb\u30c3\u30b5\u30fc\u306b\u3088\u308a\u3001\u5916\u90e8\u306e\u4e00\u822c\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30fc\u304c\u30ed\u30fc\u30c9\u3055\u308c\u307e\u3059\u3002

+errorParsingExternalParameterEntities=\u4f7f\u7528\u4e2d\u306e XML \u30d7\u30ed\u30bb\u30c3\u30b5\u30fc\u306b\u3088\u308a\u3001\u5916\u90e8\u306e\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u30fb\u30a8\u30f3\u30c6\u30a3\u30c6\u30a3\u30fc\u304c\u30ed\u30fc\u30c9\u3055\u308c\u307e\u3059\u3002

+errorParsingExternalDTD=\u4f7f\u7528\u4e2d\u306e XML \u30d7\u30ed\u30bb\u30c3\u30b5\u30fc\u306b\u3088\u308a\u3001\u6587\u66f8\u30bf\u30a4\u30d7\u5b9a\u7fa9 (DTD) \u304c\u30ed\u30fc\u30c9\u3055\u308c\u307e\u3059\u3002

+errorNotUsingSecureXML=\u4f7f\u7528\u4e2d\u306e XML \u30d7\u30ed\u30bb\u30c3\u30b5\u30fc\u306f\u3001\u30bb\u30ad\u30e5\u30a2\u69cb\u6587\u89e3\u6790\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093\u3002

+reuseDocumentBuilders=\u6587\u66f8\u30d3\u30eb\u30c0\u30fc\u3092\u518d\u5229\u7528\u3057\u3066\u3044\u307e\u3059\u3002

+notReuseDocBuilders=\u6587\u66f8\u30d3\u30eb\u30c0\u30fc\u306f\u518d\u5229\u7528\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002

+

+##LruCacheProvider

+LRUCapacity=\u6700\u9577\u672a\u4f7f\u7528\u6642\u9593 (LRU) \u5bb9\u91cf {0} \u304c {1} \u7528\u306b\u69cb\u6210\u3055\u308c\u3066\u3044\u307e\u3059\u3002

+

+##DynamicConfigProperty

+evalExpressionFailed={0} \u3092\u8a55\u4fa1\u3067\u304d\u307e\u305b\u3093\u3002

+

+##JsonContainerConfigLoader

+readingContainerConfig=\u30b3\u30f3\u30c6\u30ca\u30fc\u69cb\u6210 {0} \u3092\u8aad\u307f\u53d6\u3063\u3066\u3044\u307e\u3059\u3002

+loadFromString={0} \u3092\u69cb\u6587\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3002

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom={0} \u304b\u3089\u306e\u30ea\u30bd\u30fc\u30b9\u3092\u30ed\u30fc\u30c9\u3057\u3066\u3044\u307e\u3059\u3002

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom={0} \u304b\u3089\u306e\u30d5\u30a1\u30a4\u30eb\u3092\u30ed\u30fc\u30c9\u3057\u3066\u3044\u307e\u3059\u3002

+

+##ApiServlet

+apiServletProtocolException=\u30d7\u30ed\u30c8\u30b3\u30eb\u4f8b\u5916\u304c\u767a\u751f\u3057\u305f\u305f\u3081\u3001\u5fdc\u7b54\u30a8\u30e9\u30fc\u304c\u8fd4\u3055\u308c\u3066\u3044\u307e\u3059\u3002

+apiServletException=\u30d7\u30ed\u30c8\u30b3\u30eb\u4f8b\u5916\u304c\u767a\u751f\u3057\u305f\u305f\u3081\u3001\u5fdc\u7b54\u30a8\u30e9\u30fc\u304c\u8fd4\u3055\u308c\u3066\u3044\u307e\u3059\u3002

+

+##FeatureRegistry

+overridingFeature={1} \u3067\u306e\u5b9a\u7fa9\u3092\u6301\u3064 {0} \u6a5f\u80fd\u304c\u30aa\u30fc\u30d0\u30fc\u30e9\u30a4\u30c9\u3055\u308c\u3066\u3044\u307e\u3059\u3002

+

+##FeatureResourceLoader

+missingFile=\u30d5\u30a1\u30a4\u30eb {0} \u306f\u4ee5\u524d\u306f\u5b58\u5728\u3057\u3066\u3044\u307e\u3057\u305f\u304c\u3001\u73fe\u5728\u306f\u5b58\u5728\u3057\u3066\u3044\u307e\u305b\u3093\u3002

+unableRetrieveLib={0} \u304b\u3089\u306e\u30ea\u30e2\u30fc\u30c8\u30fb\u30e9\u30a4\u30d6\u30e9\u30ea\u30fc\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002

+

+##BasicHttpFetcher

+timeoutException={0} \u306f\u3001\u6b21\u306e\u4f8b\u5916\u306e\u305f\u3081\u306b\u30bf\u30a4\u30e0\u30a2\u30a6\u30c8\u306b\u306a\u308a\u307e\u3057\u305f: {1} - {2} - {3} \u30df\u30ea\u79d2\u3002

+exceptionOccurred={0} \u3092\u30d5\u30a7\u30c3\u30c1\u3059\u308b\u969b\u306b\u3001\u6b21\u306e\u4f8b\u5916\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1} \u30df\u30ea\u79d2\u7d4c\u904e\u3002

+slowResponse={0} \u304b\u3089\u306e\u5fdc\u7b54\u306b\u6642\u9593\u304c\u304b\u304b\u3063\u3066\u3044\u307e\u3059\u3002 {1} \u30df\u30ea\u79d2\u7d4c\u904e\u3002

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Message Digest 5 (MD5) \u306e\u53d6\u5f97\u6642\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002 \u30a8\u30e9\u30fc\u306f\u7121\u8996\u3055\u308c\u307e\u3057\u305f\u3002

+errorParsingMD5=UTF-8 \u5f62\u5f0f\u306e Message Digest 5 (MD5) \u30b9\u30c8\u30ea\u30f3\u30b0\u306e\u69cb\u6587\u89e3\u6790\u6642\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##OAuthModule

+usingRandomKey=OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30fb\u30b5\u30a4\u30c9\u72b6\u614b\u6697\u53f7\u5316\u7528\u306e\u30e9\u30f3\u30c0\u30e0\u9375\u304c\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002

+usingFile=OAuth \u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30fb\u30b5\u30a4\u30c9\u72b6\u614b\u6697\u53f7\u5316\u7528\u306e {0} \u30d5\u30a1\u30a4\u30eb\u304c\u4f7f\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002

+loadKeyFileFrom={0} \u304b\u3089\u306e OAuth \u7f72\u540d\u9375\u3092\u30ed\u30fc\u30c9\u3057\u3066\u3044\u307e\u3059\u3002

+couldNotLoadKeyFile= {0} \u9375\u30d5\u30a1\u30a4\u30eb\u3092\u30ed\u30fc\u30c9\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+couldNotLoadSignedKey=OAuth \u7f72\u540d\u9375\u304c\u6b63\u3057\u304f\u30ed\u30fc\u30c9\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u9375\u3092\u4f5c\u6210\u3059\u308b\u306b\u306f\u3001\u4ee5\u4e0b\u306e\u624b\u9806\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\n 1. \u4ee5\u4e0b\u306e\u30b3\u30de\u30f3\u30c9\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002 \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. \u4ee5\u4e0b\u306e\u884c\u3092\u8ffd\u52a0\u3057\u3066\u3001shindig.properties \u30d5\u30a1\u30a4\u30eb\u3092\u7de8\u96c6\u3057\u307e\u3059\u3002 \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit={0} \u306e OAuth \u30b3\u30f3\u30b7\u30e5\u30fc\u30de\u30fc\u306e\u521d\u671f\u5316\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002

+

+##OAuthRequest

+oauthFetchFatalError=OAuth \u306b\u3088\u308b\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u53d6\u308a\u51fa\u3059\u3068\u304d\u306b\u3001\u4ee5\u4e0b\u306e\u81f4\u547d\u7684\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: \n {0}\u3002

+oauthFetchErrorReprompt=OAuth \u306b\u3088\u308b\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u53d6\u308a\u51fa\u3059\u3068\u304d\u306b\u3001\u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: \n {0}\u3002 \u30e6\u30fc\u30b6\u30fc\u306b\u5bfe\u3057\u3066\u3001\u627f\u8a8d\u3092\u6c42\u3081\u308b\u30d7\u30ed\u30f3\u30d7\u30c8\u304c\u518d\u5ea6\u8868\u793a\u3055\u308c\u307e\u3059\u3002

+bogusExpired=\u30b5\u30fc\u30d0\u30fc\u304b\u3089\u7121\u52b9\u306a\u6709\u52b9\u671f\u9650\u304c\u8fd4\u3055\u308c\u307e\u3057\u305f:\n {0}\u3002

+oauthFetchUnexpectedError=OAuth \u306b\u3088\u308b\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u53d6\u308a\u51fa\u3059\u3068\u304d\u306b\u3001\u4ee5\u4e0b\u306e\u81f4\u547d\u7684\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: \n {0}\u3002

+unauthenticatedOauth=\u30e6\u30fc\u30b6\u30fc\u8a8d\u8a3c\u304c\u5b58\u5728\u3057\u306a\u3044\u305f\u3081\u3001OAuth \u306f\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u53d6\u308a\u51fa\u3059\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002 \u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: \n {0}\u3002

+invalidOauth=\u8981\u6c42\u304c\u7121\u52b9\u3067\u3042\u308b\u305f\u3081\u3001OAuth \u306f\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u53d6\u308a\u51fa\u3059\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002 \u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: \n {0}\u3002

+

+##CajaCssSanitizer

+failedToParse=\u30b9\u30bf\u30a4\u30eb\u30fb\u30b7\u30fc\u30c8\u3092\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3002

+unableToConvertScript=\u30b9\u30af\u30ea\u30d7\u30c8\u30fb\u30ce\u30fc\u30c9\u3092 OpenSocial Markup Language (OSML) \u30bf\u30b0\u306b\u5909\u63db\u3067\u304d\u307e\u305b\u3093\u3002

+

+##PipelineExecutor

+errorPreloading=\u30d7\u30ea\u30ed\u30fc\u30c9\u6642\u306b\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##Processor

+renderBlacklistedGadget=\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u6b21\u306e\u30ac\u30b8\u30a7\u30c3\u30c8\u3092\u3001\u30b7\u30b9\u30c6\u30e0\u304c\u30ec\u30f3\u30c0\u30ea\u30f3\u30b0\u3057\u3088\u3046\u3068\u3057\u307e\u3057\u305f: {0}\u3002

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} \u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3002

+failedToRead={0} \u3092\u8aad\u307f\u53d6\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002

+

+##DefaultServiceFetcher

+httpErrorFetching={1} \u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u304b\u3089\u30b5\u30fc\u30d3\u30b9\u30fb\u30e1\u30bd\u30c3\u30c9\u3092\u53d6\u308a\u51fa\u3059\u3068\u304d\u306b HTTP {0} \u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+failedToFetchService={0} \u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u304b\u3089\u306e\u30b5\u30fc\u30d3\u30b9\u30fb\u30e1\u30bd\u30c3\u30c9\u3092\u53d6\u308a\u51fa\u3059\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+failedToParseService={0} \u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u304b\u3089\u306e\u30b5\u30fc\u30d3\u30b9\u30fb\u30e1\u30bd\u30c3\u30c9\u3092\u69cb\u6587\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+

+##Renderer

+FailedToRender={0} \u306b\u3042\u308b\u30ac\u30b8\u30a7\u30c3\u30c8\u304c\u30ec\u30f3\u30c0\u30ea\u30f3\u30b0\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f\u3002 \u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+

+##RenderingGadgetRewriter

+unknownFeatures=1 \u3064\u4ee5\u4e0a\u306e\u4e0d\u660e\u306a\u6a5f\u80fd\u304c\u4ee5\u4e0b\u306e extern &libs= \u306b\u5b58\u5728\u3057\u307e\u3059: {0}\u3002

+unexpectedErrorPreloading=\u30ac\u30b8\u30a7\u30c3\u30c8\u306e\u30d7\u30ea\u30ed\u30fc\u30c9\u6642\u306b\u4e88\u671f\u3057\u306a\u3044\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u30b5\u30cb\u30bf\u30a4\u30ba\u8981\u6c42\u304c\u767a\u884c\u3055\u308c\u307e\u3057\u305f\u304c\u3001{0} \u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30fb\u30bf\u30a4\u30d7\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3002

+requestToSanitizeUnknownContent=\u30b5\u30cb\u30bf\u30a4\u30ba\u8981\u6c42\u304c\u767a\u884c\u3055\u308c\u307e\u3057\u305f\u304c\u3001{1} \u306e\u65e2\u77e5\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u30fb\u30bf\u30a4\u30d7 {0} \u304c\u542b\u307e\u308c\u3066\u3044\u307e\u305b\u3093\u3002

+unableToSanitizeUnknownImg=\u30a4\u30e1\u30fc\u30b8\u30fb\u30bf\u30a4\u30d7 {0} \u304c\u4e0d\u660e\u3067\u3042\u308b\u305f\u3081\u3001\u30b5\u30cb\u30bf\u30a4\u30ba\u3067\u304d\u307e\u305b\u3093\u3002

+unableToDetectImgType=\u30b3\u30f3\u30c6\u30f3\u30c4\u3092\u30b5\u30cb\u30bf\u30a4\u30ba\u3059\u308b\u3068\u304d\u306b\u3001{0} \u306e\u30a4\u30e1\u30fc\u30b8\u30fb\u30bf\u30a4\u30d7\u3092\u691c\u51fa\u3067\u304d\u307e\u305b\u3093\u3002

+

+##BasicImageRewriter

+ioErrorRewritingImg={0} \u30a4\u30e1\u30fc\u30b8\u306e\u518d\u66f8\u304d\u8fbc\u307f\u4e2d\u306b\u4ee5\u4e0b\u306e\u5165\u51fa\u529b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+unknownErrorRewritingImg={0} \u30a4\u30e1\u30fc\u30b8\u306e\u518d\u66f8\u304d\u8fbc\u307f\u4e2d\u306b\u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+failedToReadImg={0} \u30a4\u30e1\u30fc\u30b8\u3092\u8aad\u307f\u53d6\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002\u3053\u306e\u30a4\u30e1\u30fc\u30b8\u3092\u30b9\u30ad\u30c3\u30d7\u3057\u3066\u3044\u307e\u3059\u3002 \u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+

+##CssResponseRewriter

+cajaCssParseFailure={1} \u306e Caja CSS {0} \u306e\u69cb\u6587\u89e3\u6790\u6642\u306b\u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##ImageAttributeRewriter

+unableToProcessImg={0} \u30a4\u30e1\u30fc\u30b8\u30fb\u30ea\u30bd\u30fc\u30b9\u3092\u51e6\u7406\u3067\u304d\u307e\u305b\u3093\u3002

+unableToReadResponse={0} \u30a4\u30e1\u30fc\u30b8\u30fb\u30ea\u30bd\u30fc\u30b9\u306e\u5fdc\u7b54\u3092\u8aad\u307f\u53d6\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002

+unableToFetchImg={0} \u30a4\u30e1\u30fc\u30b8\u30fb\u30ea\u30bd\u30fc\u30b9\u3092\u53d6\u308a\u51fa\u3059\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002

+unableToParseImg={0} \u30a4\u30e1\u30fc\u30b8\u30fb\u30ea\u30bd\u30fc\u30b9\u3092\u69cb\u6587\u89e3\u6790\u3067\u304d\u307e\u305b\u3093\u3002

+

+##MutableContent

+exceptionParsingContent=\u30ac\u30b8\u30a7\u30c3\u30c8\u306e\u30b3\u30f3\u30c6\u30f3\u30c4\u306e\u69cb\u6587\u89e3\u6790\u6642\u306b\u4f8b\u5916\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##PipelineDataGadgetRewriter

+failedToParsePreload={0} \u306b\u3042\u308b\u30ac\u30b8\u30a7\u30c3\u30c8\u3092\u3001\u30d7\u30ea\u30ed\u30fc\u30c9\u7528\u306b\u69cb\u6587\u89e3\u6790\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+

+##ProxyingVisitor

+uriExceptionParsing={0} \u306b\u3042\u308b\u30ac\u30b8\u30a7\u30c3\u30c8\u306e\u69cb\u6587\u89e3\u6790\u4e2d\u306b Uniform Resource Identifier (URI) \u4f8b\u5916\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##TemplateRewriter

+malformedTemplateLib=\u30c6\u30f3\u30d7\u30ec\u30fc\u30c8\u30fb\u30e9\u30a4\u30d6\u30e9\u30ea\u30fc\u306e\u5f62\u5f0f\u304c\u6b63\u3057\u304f\u306a\u3044\u305f\u3081\u3001\u4f8b\u5916\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u30b3\u30f3\u30d1\u30a4\u30eb\u3055\u308c\u305f\u30ad\u30e3\u30c3\u30b7\u30e5\u304c\u4f5c\u6210\u3055\u308c\u307e\u3057\u305f\u3002

+retrieveReference={0} \u3092\u53d6\u5f97\u3057\u3066\u3044\u307e\u3059\u3002

+unableToCajole={0} \u306b\u3042\u308b\u30ac\u30b8\u30a7\u30c3\u30c8\u3092\u30b3\u30f3\u30d1\u30a4\u30eb\u3067\u304d\u307e\u305b\u3093\u3002

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u9023\u7d50\u3055\u308c\u305f\u30d7\u30ed\u30ad\u30b7\u30fc\u306e\u8981\u6c42\u6642\u306b\u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {0}\u3002

+

+##GadgetRenderingServlet

+malformedTtlValue=\u7121\u52b9\u306a\u5b58\u7d9a\u6642\u9593 (TTL) \u306e\u5024 {0} \u306f\u7121\u8996\u3055\u308c\u307e\u3057\u305f\u3002

+

+##HttpRequestHandler

+gadgetCreationError=\u518d\u66f8\u304d\u8fbc\u307f\u30a6\u30a3\u30b8\u30a7\u30c3\u30c8\u306e\u4f5c\u6210\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002  \u30ac\u30b8\u30a7\u30c3\u30c8\u306a\u3057\u3067\u5fdc\u7b54\u3092\u518d\u66f8\u304d\u8fbc\u307f\u3057\u3066\u3044\u307e\u3059\u3002

+

+##ProxyServlet

+embededImgWrongDomain=URL {0} \u3092\u57cb\u3081\u8fbc\u3080\u8981\u6c42\u304c\u3001\u8aa4\u3063\u305f\u30c9\u30e1\u30a4\u30f3 {1} \u306b\u5bfe\u3057\u3066\u767a\u884c\u3055\u308c\u307e\u3057\u305f\u3002

+

+##DefaultTemplateProcessor

+elFailure={0} \u306b\u3042\u308b\u30ac\u30b8\u30a7\u30c3\u30c8\u3067\u3001\u4ee5\u4e0b\u306e EL \u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+

+##UriUtils

+skipIllegalHeader={0} \u30d8\u30c3\u30c0\u30fc\u306f\u6b63\u3057\u304f\u306a\u3044\u305f\u3081\u30b9\u30ad\u30c3\u30d7\u3055\u308c\u307e\u3059\u3002 \u4ee5\u4e0b\u306e\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {1}\u3002

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion={0} \u306e\u66f4\u65b0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002 \u72b6\u6cc1\u30b3\u30fc\u30c9 {1} \u304c\u8fd4\u3055\u308c\u307e\u3057\u305f\u3002 \u4f8b\u5916: {2} \u4ee3\u308f\u308a\u306b\u3001\u30ad\u30e3\u30c3\u30b7\u30e5\u3055\u308c\u305f\u30d0\u30fc\u30b8\u30e7\u30f3\u304c\u4f7f\u7528\u3055\u308c\u307e\u3059\u3002

+updateSpecFailureApplyNegCache={0} \u306e\u66f4\u65b0\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002 \u72b6\u6cc1\u30b3\u30fc\u30c9 {1} \u304c\u8fd4\u3055\u308c\u307e\u3057\u305f\u3002 \u4f8b\u5916: {2} \u30cd\u30ac\u30c6\u30a3\u30d6\u30fb\u30ad\u30e3\u30c3\u30b7\u30e5\u304c\u9069\u7528\u3055\u308c\u3066\u3044\u307e\u3059\u3002

+

+##HashLockedDomainService

+noLockedDomainConfig={0} \u306e\u30ed\u30c3\u30af\u6e08\u307f\u30c9\u30e1\u30a4\u30f3\u69cb\u6210\u304c\u5b58\u5728\u3057\u307e\u305b\u3093\u3002

+

+##Bootstrap

+startingConnManagerWith={0} \u30d7\u30ed\u30d1\u30c6\u30a3\u30fc\u3092\u6301\u3064\u63a5\u7d9a\u30de\u30cd\u30fc\u30b8\u30e3\u30fc\u3092\u958b\u59cb\u3057\u3066\u3044\u307e\u3059\u3002

+

+##XSDValidator

+resolveResource=\u4ee5\u4e0b\u306e\u30ea\u30bd\u30fc\u30b9\u3092\u89e3\u6c7a\u3057\u3066\u3044\u307e\u3059: {0}\u3001{1}\u3001{2}\u3001{3}\u3002

+failedToValidate={0} \u306e\u691c\u8a3c\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+

+##DefaultRequestPipeline

+cachedResponse={0} \u3078\u306e\u8981\u6c42\u306b\u5bfe\u3057\u3001\u30ad\u30e3\u30c3\u30b7\u30e5\u3055\u308c\u305f\u5fdc\u7b54\u3092\u8fd4\u3057\u3066\u3044\u307e\u3059\u3002

+staleResponse="{0} \u306e\u30ea\u30bd\u30fc\u30b9\u306b\u5bfe\u3059\u308b\u8981\u6c42\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u304c\u3001\u30ad\u30e3\u30c3\u30b7\u30e5\u5185\u306b\u4ee5\u524d\u306e\u5fdc\u7b54\u304c\u3042\u308a\u307e\u3059\u3002\u6574\u5408\u3057\u3066\u3044\u306a\u3044\u53ef\u80fd\u6027\u306e\u3042\u308b\u5fdc\u7b54\u3092\u30ad\u30e3\u30c3\u30b7\u30e5\u304b\u3089\u8fd4\u3057\u3066\u3044\u307e\u3059\u3002

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_kk.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_kk.properties
new file mode 100644
index 0000000..8b6015b
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_kk.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u049a\u0430\u0443\u0456\u043f\u0441\u0456\u0437\u0434\u0456\u043a \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b \u043d\u0435\u043c\u0435\u0441\u0435 \u0442\u0456\u0440\u043a\u0435\u043b\u0433\u0456 \u0434\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0456 \u0434\u04b1\u0440\u044b\u0441 \u049b\u04b1\u0440\u044b\u043b\u043c\u0430\u0493\u0430\u043d \u0436\u04d9\u043d\u0435 \u043e\u043b\u0430\u0440\u0434\u044b \u0442\u0430\u043b\u0434\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##XmlUtil

+errorParsingXML=XML \u0442\u0430\u043b\u0434\u0430\u0443\u0434\u0430\u0493\u044b \u049b\u0430\u0442\u0435. \u041e\u0441\u044b\u043d\u044b \u0435\u043b\u0435\u043c\u0435\u0443\u0433\u0435 \u0431\u043e\u043b\u0430\u0434\u044b.

+errorParsingExternalGeneralEntities=\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0493\u0430\u043d XML \u04e9\u04a3\u0434\u0435\u0433\u0456\u0448\u0456 \u0441\u044b\u0440\u0442\u049b\u044b \u0436\u0430\u043b\u043f\u044b \u043d\u044b\u0441\u0430\u043d\u0434\u0430\u0440\u0434\u044b \u0436\u04af\u043a\u0442\u0435\u0439\u0434\u0456.

+errorParsingExternalParameterEntities=\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0493\u0430\u043d XML \u04e9\u04a3\u0434\u0435\u0433\u0456\u0448\u0456 \u0441\u044b\u0440\u0442\u049b\u044b \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043d\u044b\u0441\u0430\u043d\u0434\u0430\u0440\u044b\u043d \u0436\u04af\u043a\u0442\u0435\u0439\u0434\u0456.

+errorParsingExternalDTD=\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0493\u0430\u043d XML \u04e9\u04a3\u0434\u0435\u0433\u0456\u0448\u0456 \u049a\u04b1\u0436\u0430\u0442 \u0442\u04af\u0440\u0456 \u0430\u043d\u044b\u049b\u0442\u0430\u043c\u0430\u043b\u0430\u0440\u044b\u043d (DTD) \u0436\u04af\u043a\u0442\u0435\u0439\u0434\u0456.

+errorNotUsingSecureXML=\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0493\u0430\u043d XML \u04e9\u04a3\u0434\u0435\u0433\u0456\u0448\u0456 \u049b\u0430\u0443\u0456\u043f\u0441\u0456\u0437 \u0442\u0430\u043b\u0434\u0430\u0443\u0493\u0430 \u049b\u043e\u043b\u0434\u0430\u0443 \u043a\u04e9\u0440\u0441\u0435\u0442\u043f\u0435\u0439\u0434\u0456.

+reuseDocumentBuilders=\u049a\u04b1\u0436\u0430\u0442 \u049b\u04b1\u0440\u0430\u0441\u0442\u044b\u0440\u0493\u044b\u0448\u0442\u0430\u0440\u044b \u049b\u0430\u0439\u0442\u0430 \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0443\u0434\u0430.

+notReuseDocBuilders=\u049a\u04b1\u0436\u0430\u0442 \u049b\u04b1\u0440\u0430\u0441\u0442\u044b\u0440\u0493\u044b\u0448\u0442\u0430\u0440\u044b \u049b\u0430\u0439\u0442\u0430 \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u044b\u043f \u0436\u0430\u0442\u049b\u0430\u043d \u0436\u043e\u049b.

+

+##LruCacheProvider

+LRUCapacity=\u0415\u04a3 \u0430\u0437 \u0441\u043e\u04a3\u0493\u044b \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0493\u0430\u043d (LRU) \u0441\u044b\u0439\u044b\u043c\u0434\u044b\u043b\u044b\u049b {0} {1} \u04af\u0448\u0456\u043d \u0442\u0435\u04a3\u0448\u0435\u043b\u0433\u0435\u043d.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} \u0431\u0430\u0493\u0430\u043b\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##JsonContainerConfigLoader

+readingContainerConfig={0} \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440 \u0442\u0435\u04a3\u0448\u0435\u043b\u0456\u043c\u0456 \u043e\u049b\u044b\u043b\u0443\u0434\u0430.

+loadFromString={0} \u0442\u0430\u043b\u0434\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom={0} \u0440\u0435\u0441\u0443\u0440\u0441\u0442\u0430\u0440\u044b \u0436\u04af\u043a\u0442\u0435\u043b\u0443\u0434\u0435.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom={0} \u0444\u0430\u0439\u043b\u0434\u0430\u0440\u044b \u0436\u04af\u043a\u0442\u0435\u043b\u0443\u0434\u0435.

+

+##ApiServlet

+apiServletProtocolException=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0434\u044b\u04a3 \u049b\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439\u044b \u043e\u0440\u044b\u043d \u0430\u043b\u0493\u0430\u043d \u0441\u0435\u0431\u0435\u0431\u0456\u043d\u0435\u043d \u0436\u0430\u0443\u0430\u043f \u049b\u0430\u0442\u0435\u0441\u0456 \u049b\u0430\u0439\u0442\u0430\u0440\u044b\u043b\u0443\u0434\u0430.

+apiServletException=\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0434\u0430 \u049b\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439 \u043e\u0440\u044b\u043d \u0430\u043b\u0493\u0430\u043d\u0434\u044b\u049b\u0442\u0430\u043d, \u0436\u0430\u0443\u0430\u043f \u0431\u0435\u0440\u0443 \u049b\u0430\u0442\u0435\u0441\u0456 \u049b\u0430\u0439\u0442\u0430\u0440\u044b\u043b\u0430\u0434\u044b.

+

+##FeatureRegistry

+overridingFeature={0} \u043c\u04af\u043c\u043a\u0456\u043d\u0434\u0456\u0433\u0456 {1} \u043c\u04af\u043c\u043a\u0456\u043d\u0434\u0456\u043a\u043f\u0435\u043d \u0430\u043b\u0434\u044b\u043d \u0430\u043b\u0430 \u0430\u043d\u044b\u049b\u0442\u0430\u043b\u0443\u0434\u0430.

+

+##FeatureResourceLoader

+missingFile={0} \u0444\u0430\u0439\u043b \u0431\u04b1\u0440\u044b\u043d\u0434\u0430 \u0431\u0430\u0440 \u0431\u043e\u043b\u0493\u0430\u043d, \u0431\u0456\u0440\u0430\u049b \u049b\u0430\u0437\u0456\u0440\u0433\u0456 \u0443\u0430\u049b\u044b\u0442\u0442\u0430 \u0436\u043e\u049b.

+unableRetrieveLib={0} \u0456\u0448\u0456\u043d\u0435\u043d \u049b\u0430\u0448\u044b\u049b\u0442\u044b\u049b \u043a\u0456\u0442\u0430\u043f\u0445\u0430\u043d\u0430\u0441\u044b\u043d \u0448\u044b\u0493\u0430\u0440\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##BasicHttpFetcher

+timeoutException=\u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439\u0434\u044b\u04a3 \u0441\u0435\u0431\u0435\u0431\u0456\u043d\u0435\u043d {0} \u0443\u0430\u049b\u044b\u0442\u044b \u0431\u0456\u0442\u0442\u0456: {1} - {2} - {3} \u043c\u0441.

+exceptionOccurred={0}: {1} \u043c\u0441 \u0442\u0430\u04a3\u0434\u0430\u043f \u0430\u043b\u0443 \u0430\u044f\u049b\u0442\u0430\u043b\u0493\u0430\u043d \u043a\u0435\u0437\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u049b\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+slowResponse={0} \u0431\u0430\u044f\u0443 \u0436\u0430\u0443\u0430\u043f \u0431\u0435\u0440\u0443\u0434\u0435. {1} \u043c\u0441 \u04e9\u0442\u0442\u0456.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Message Digest 5 (MD5) \u0430\u043b\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b. \u049a\u0430\u0442\u0435 \u0435\u043b\u0435\u043d\u0431\u0435\u0434\u0456.

+errorParsingMD5=Message Digest 5 (MD5) \u0436\u043e\u043b\u044b\u043d UTF-8 \u043f\u0456\u0448\u0456\u043c\u0456\u043d\u0434\u0435 \u0442\u0430\u043b\u0434\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+

+##OAuthModule

+usingRandomKey=\u041eAuth \u043a\u043b\u0438\u0435\u043d\u0442\u0442\u0456\u043a \u043a\u04af\u0439\u0456\u043d\u0456\u04a3 \u0448\u0438\u0444\u0440\u043b\u0430\u043d\u0443\u044b \u04af\u0448\u0456\u043d \u043a\u0435\u0437\u0434\u0435\u0439\u0441\u043e\u049b \u043a\u0456\u043b\u0442 \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0443\u0434\u0430.

+usingFile=\u041eAuth \u043a\u043b\u0438\u0435\u043d\u0442\u0442\u0456\u043a \u043a\u04af\u0439\u0456\u043d\u0456\u04a3 \u0448\u0438\u0444\u0440\u043b\u0430\u043d\u0443\u044b \u04af\u0448\u0456\u043d {0} \u0444\u0430\u0439\u043b\u044b \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0443\u0434\u0430.

+loadKeyFileFrom={0} \u0456\u0448\u0456\u043d\u0435\u043d OAuth \u043a\u0456\u0440\u0443 \u043a\u0456\u043b\u0442\u0456 \u0436\u04af\u043a\u0442\u0435\u043b\u0443\u0434\u0435.

+couldNotLoadKeyFile= {0} \u043d\u0435\u0433\u0456\u0437\u0433\u0456 \u0444\u0430\u0439\u043b\u044b\u043d \u0436\u04af\u043a\u0442\u0435\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0431\u043e\u043b\u043c\u0430\u0434\u044b.

+couldNotLoadSignedKey=OAuth \u043a\u0456\u0440\u0443 \u043a\u0456\u043b\u0442\u0456 \u0434\u04b1\u0440\u044b\u0441 \u0436\u04af\u043a\u0442\u0435\u043b\u043c\u0435\u0434\u0456. \u041a\u0456\u043b\u0442\u0442\u0456 \u0436\u0430\u0441\u0430\u0443 \u04af\u0448\u0456\u043d: \n 1. \u041a\u0435\u043b\u0435\u0441\u0456 \u043f\u04d9\u0440\u043c\u0435\u043d\u0434\u0456 \u0456\u0441\u043a\u0435 \u049b\u043e\u0441\u044b\u04a3\u044b\u0437: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. \u041a\u0435\u043b\u0435\u0441\u0456 \u0436\u043e\u043b\u0434\u0430\u0440\u0434\u044b \u049b\u043e\u0441\u0443 \u0430\u0440\u049b\u044b\u043b\u044b shindig.properties \u0444\u0430\u0439\u043b\u044b\u043d \u04e9\u04a3\u0434\u0435\u04a3\u0456\u0437: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit={0} \u0456\u0448\u0456\u043d\u0435\u043d OAuth \u0442\u04b1\u0442\u044b\u043d\u0443\u0448\u044b\u043b\u0430\u0440\u044b \u0431\u0430\u043f\u0442\u0430\u043d\u0434\u044b\u0440\u044b\u043b\u043c\u0430\u0434\u044b.

+

+##OAuthRequest

+oauthFetchFatalError=OAuth \u043c\u0430\u0437\u043c\u04b1\u043d\u0434\u044b \u0430\u043b\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u0436\u04e9\u043d\u0434\u0435\u043b\u043c\u0435\u0441 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: \n {0}.

+oauthFetchErrorReprompt=OAuth \u043c\u0430\u0437\u043c\u04b1\u043d\u0434\u044b \u0430\u043b\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: \n {0}. \u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b\u0493\u0430 \u0431\u0435\u043a\u0456\u0442\u0443\u0433\u0435 \u0445\u0430\u0431\u0430\u0440 \u049b\u0430\u0439\u0442\u0430 \u0436\u0456\u0431\u0435\u0440\u0456\u043b\u0443\u0434\u0435.

+bogusExpired=\u0421\u0435\u0440\u0432\u0435\u0440\u0434\u0435\u043d \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437 \u043c\u0435\u0440\u0437\u0456\u043c \u049b\u0430\u0439\u0442\u0430\u0440\u044b\u043b\u0434\u044b:\n {0}.

+oauthFetchUnexpectedError=OAuth \u043c\u0430\u0437\u043c\u04b1\u043d\u0434\u044b \u0430\u043b\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u0436\u04e9\u043d\u0434\u0435\u043b\u043c\u0435\u0441 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: \n {0}.

+unauthenticatedOauth=\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b \u0442\u04af\u043f\u043d\u04b1\u0441\u049b\u0430\u043b\u044b\u0493\u044b \u0436\u043e\u049b \u0441\u0435\u0431\u0435\u0431\u0456\u043d\u0435\u043d OAuth \u043c\u0430\u0437\u043c\u04af\u043d\u0434\u044b \u0430\u043b\u0443\u044b \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441. \u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: \n {0}.

+invalidOauth=\u0421\u04b1\u0440\u0430\u0443 \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437 \u0431\u043e\u043b\u0493\u0430\u043d\u0434\u044b\u049b\u0442\u0430\u043d OAuth \u043c\u0430\u0437\u043c\u04b1\u043d\u0434\u044b \u0430\u043b\u0443\u044b \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441. \u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=\u041c\u04d9\u043d\u0435\u0440\u043b\u0435\u0440 \u043a\u0435\u0441\u0442\u0435\u0441\u0456\u043d \u0442\u0430\u043b\u0434\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+unableToConvertScript=\u0422\u0430\u04a3\u0431\u0430 \u0442\u04af\u0439\u0456\u043d\u0456\u043d OpenSocial Markup Language (OSML) \u0442\u0435\u0433\u0456\u043d\u0435 \u0442\u04af\u0440\u043b\u0435\u043d\u0434\u0456\u0440\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##PipelineExecutor

+errorPreloading=\u0410\u043b\u0434\u044b\u043d \u0430\u043b\u0430 \u0436\u04af\u043a\u0442\u0435\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u04af\u0442\u0456\u043b\u043c\u0435\u0433\u0435\u043d \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+

+##Processor

+renderBlacklistedGadget=\u0416\u04af\u0439\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0440\u0430 \u0442\u0456\u0437\u0456\u043c\u0434\u0435\u0433\u0456 \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u043d\u044b \u043a\u04e9\u0440\u0441\u0435\u0442\u0443\u0433\u0435 \u04d9\u0440\u0435\u043a\u0435\u0442\u0442\u0435\u043d\u0434\u0456: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} \u0448\u044b\u0493\u0430\u0440\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+failedToRead={0} \u043e\u049b\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##DefaultServiceFetcher

+httpErrorFetching={1} \u0441\u043e\u04a3\u0493\u044b \u043d\u04af\u043a\u0442\u0435\u0441\u0456\u043d\u0435\u043d \u049b\u044b\u0437\u043c\u0435\u0442 \u04d9\u0434\u0456\u0441\u0442\u0435\u0440\u0456\u043d \u0430\u043b\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 HTTP {0} \u049b\u0430\u0442\u0435\u0441\u0456 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+failedToFetchService={0} \u0441\u043e\u04a3\u0493\u044b \u043d\u04af\u043a\u0442\u0435\u0441\u0456\u043d\u0435\u043d \u049b\u044b\u0437\u043c\u0435\u0442 \u04d9\u0434\u0456\u0441\u0442\u0435\u0440\u0456\u043d \u0430\u043b\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0431\u043e\u043b\u043c\u0430\u0434\u044b. \u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+failedToParseService={0} \u0441\u043e\u04a3\u0493\u044b \u043d\u04af\u043a\u0442\u0435\u0441\u0456\u043d\u0435\u043d \u049b\u044b\u0437\u043c\u0435\u0442 \u04d9\u0434\u0456\u0441\u0442\u0435\u0440\u0456\u043d \u0442\u0430\u043b\u0434\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0431\u043e\u043b\u043c\u0430\u0434\u044b. \u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+

+##Renderer

+FailedToRender={0} \u043c\u0435\u043a\u0435\u043d\u0436\u0430\u0439\u044b\u043d\u0434\u0430\u0493\u044b \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u043a\u04e9\u0440\u0441\u0435\u0442\u0456\u043b\u043c\u0435\u0434\u0456. \u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=\u0411\u0456\u0440 \u043d\u0435\u043c\u0435\u0441\u0435 \u043e\u0434\u0430\u043d \u0430\u0440\u0442\u044b\u049b \u0431\u0435\u043b\u0433\u0456\u0441\u0456\u0437 \u043c\u04af\u043c\u043a\u0456\u043d\u0434\u0456\u043a \u043a\u0435\u043b\u0435\u0441\u0456 \u0441\u044b\u0440\u0442\u049b\u044b &libs= \u0456\u0448\u0456\u043d\u0434\u0435 \u0431\u0430\u0440: {0}.

+unexpectedErrorPreloading=\u0428\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u043d\u044b \u0430\u043b\u0434\u044b\u043d \u0430\u043b\u0430 \u0436\u04af\u043a\u0442\u0435\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u04af\u0442\u043f\u0435\u0433\u0435\u043d \u049b\u0430\u0442\u0435 \u043f\u0430\u0439\u0434\u0430 \u0431\u043e\u043b\u0434\u044b.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u0422\u0430\u0437\u0430\u0440\u0442\u0443 \u0441\u04b1\u0440\u0430\u0443\u044b {0} \u04af\u0448\u0456\u043d \u043c\u0430\u0437\u043c\u04b1\u043d \u0442\u04af\u0440\u0456\u043d\u0441\u0456\u0437 \u0448\u044b\u0493\u0430\u0440\u044b\u043b\u0434\u044b.

+requestToSanitizeUnknownContent=\u0422\u0430\u0437\u0430\u0440\u0442\u0443 \u0441\u04b1\u0440\u0430\u0443\u044b {1} \u04af\u0448\u0456\u043d \u0431\u0435\u043b\u0433\u0456\u043b\u0456 {0} \u043c\u0430\u0437\u043c\u04b1\u043d \u0442\u04af\u0440\u0456\u043d\u0441\u0456\u0437 \u0448\u044b\u0493\u0430\u0440\u044b\u043b\u0434\u044b.

+unableToSanitizeUnknownImg={0} \u043a\u0435\u0441\u043a\u0456\u043d \u0442\u04af\u0440\u0456 \u0431\u0435\u043b\u0433\u0456\u0441\u0456\u0437 \u0436\u04d9\u043d\u0435 \u0442\u0430\u0437\u0430\u0440\u0442\u044b\u043b\u043c\u0430\u0439\u0434\u044b.

+unableToDetectImgType=\u041c\u0430\u0437\u043c\u04b1\u043d\u0434\u044b \u0442\u0430\u0437\u0430\u0440\u0442\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 {0} \u04af\u0448\u0456\u043d \u043a\u0435\u0441\u043a\u0456\u043d \u0442\u04af\u0440\u0456\u043d \u0430\u043d\u044b\u049b\u0442\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##BasicImageRewriter

+ioErrorRewritingImg={0} \u043a\u0435\u0441\u043a\u0456\u043d\u0456\u043d \u049b\u0430\u0439\u0442\u0430 \u0436\u0430\u0437\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u0435\u043d\u0433\u0456\u0437\u0443/\u0448\u044b\u0493\u0430\u0440\u0443 \u049b\u0430\u0442\u0435\u0441\u0456 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+unknownErrorRewritingImg={0} \u043a\u0435\u0441\u043a\u0456\u043d\u0456\u043d \u049b\u0430\u0439\u0442\u0430 \u0436\u0430\u0437\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+failedToReadImg={0} \u043a\u0435\u0441\u043a\u0456\u043d\u0456\u043d \u043e\u049b\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441 \u0436\u04d9\u043d\u0435 \u043e\u043b \u04e9\u0442\u043a\u0456\u0437\u0456\u043b\u0443\u0434\u0435. \u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Caja CSS \u0442\u0430\u043b\u0434\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1} \u04af\u0448\u0456\u043d {0}.

+

+##ImageAttributeRewriter

+unableToProcessImg={0} \u043a\u0435\u0441\u043a\u0456\u043d \u0440\u0435\u0441\u0443\u0440\u0441\u044b\u043d \u04e9\u04a3\u0434\u0435\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+unableToReadResponse={0} \u043a\u0435\u0441\u043a\u0456\u043d \u0440\u0435\u0441\u0443\u0440\u0441\u044b \u04af\u0448\u0456\u043d \u0436\u0430\u0443\u0430\u043f\u0442\u044b \u043e\u049b\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+unableToFetchImg={0} \u043a\u0435\u0441\u043a\u0456\u043d \u0440\u0435\u0441\u0443\u0440\u0441\u044b\u043d \u0430\u043b\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+unableToParseImg={0} \u043a\u0435\u0441\u043a\u0456\u043d \u0440\u0435\u0441\u0443\u0440\u0441\u044b\u043d \u0442\u0430\u043b\u0434\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##MutableContent

+exceptionParsingContent=\u0428\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u04af\u0448\u0456\u043d \u043c\u0430\u0437\u043c\u04b1\u043d\u0434\u044b \u0442\u0430\u043b\u0434\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload={0} \u043c\u0435\u043a\u0435\u043d\u0436\u0430\u0439\u044b\u043d\u0434\u0430\u0493\u044b \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u043d\u044b \u0430\u043b\u0434\u044b\u043d \u0430\u043b\u0430 \u0436\u04af\u043a\u0442\u0435\u0443 \u04af\u0448\u0456\u043d \u0442\u0430\u043b\u0434\u0430\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##ProxyingVisitor

+uriExceptionParsing=Uniform Resource Identifier (URI) \u049b\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439\u044b {0} \u043c\u0435\u043a\u0435\u043d\u0436\u0430\u0439\u044b\u043d\u0434\u0430\u0493\u044b \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u043d\u044b \u0442\u0430\u043b\u0434\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+

+##TemplateRewriter

+malformedTemplateLib=\u049a\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439\u043b\u0430\u0440 \u049b\u0430\u0442\u0435 \u049b\u04b1\u0440\u044b\u043b\u0493\u0430\u043d \u04af\u043b\u0433\u0456 \u043a\u0456\u0442\u0430\u043f\u0445\u0430\u043d\u0430\u043b\u0430\u0440\u044b\u043d\u044b\u04a3 \u0441\u0435\u0431\u0435\u0431\u0456\u043d\u0435\u043d \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u0421\u04b1\u0440\u0430\u043b\u0493\u0430\u043d \u043a\u044d\u0448 \u0436\u0430\u0441\u0430\u043b\u0434\u044b.

+retrieveReference={0} \u0448\u044b\u0493\u0430\u0440\u044b\u043b\u0443\u0434\u0430.

+unableToCajole={0} \u043c\u0435\u043a\u0435\u043d\u0436\u0430\u0439\u044b\u043d\u0434\u0430\u0493\u044b \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u043d\u044b \u0441\u04b1\u0440\u0430\u0442\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u0411\u0430\u0439\u043b\u0430\u043d\u044b\u0441\u0442\u044b\u0440\u044b\u043b\u0493\u0430\u043d \u043f\u0440\u043e\u043a\u0441\u0438\u0433\u0435 \u0441\u04b1\u0440\u0430\u0443 \u0441\u0430\u043b\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u043a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue={0} \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437 Time To Live (TTL) \u043c\u04d9\u043d\u0456 \u0435\u043b\u0435\u043d\u0431\u0435\u0434\u0456.

+

+##HttpRequestHandler

+gadgetCreationError=\u049a\u0430\u0439\u0442\u0430 \u0436\u0430\u0437\u0443 \u04af\u0448\u0456\u043d \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u0436\u0430\u0441\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435\u043b\u0456\u043a \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.  \u0416\u0430\u0443\u0430\u043f\u0442\u044b \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u0441\u044b\u0437 \u049b\u0430\u0439\u0442\u0430 \u0436\u0430\u0437\u0443.

+

+##ProxyServlet

+embededImgWrongDomain={0} URL \u0435\u043d\u0434\u0456\u0440\u0443 \u0441\u04b1\u0440\u0430\u0443\u044b \u0434\u04b1\u0440\u044b\u0441 \u0435\u043c\u0435\u0441 {1} \u0434\u043e\u043c\u0435\u043d\u0433\u0435 \u0441\u0430\u043b\u044b\u043d\u0434\u044b.

+

+##DefaultTemplateProcessor

+elFailure=\u041a\u0435\u043b\u0435\u0441\u0456 EL \u049b\u0430\u0442\u0435\u0441\u0456 {0} \u043c\u0435\u043a\u0435\u043d\u0436\u0430\u0439\u044b\u043d\u0434\u0430 \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u04af\u0448\u0456\u043d \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+

+##UriUtils

+skipIllegalHeader={0} \u0442\u0430\u049b\u044b\u0440\u044b\u0431\u044b \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437 \u0436\u04d9\u043d\u0435 \u04e9\u0442\u043a\u0456\u0437\u0456\u043b\u0443\u0434\u0435. \u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion={0} \u0436\u0430\u04a3\u0430\u0440\u0442\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b. \u041a\u04af\u0439 \u043a\u043e\u0434\u044b {1} \u049b\u0430\u0439\u0442\u0430\u0440\u044b\u043b\u0434\u044b. \u049a\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439: {2}. \u041e\u0440\u043d\u044b\u043d\u0430 \u043a\u044d\u0448 \u043d\u04b1\u0441\u049b\u0430\u0441\u044b \u049b\u043e\u043b\u0434\u0430\u043d\u044b\u043b\u0443\u0434\u0430.

+updateSpecFailureApplyNegCache={0} \u0436\u0430\u04a3\u0430\u0440\u0442\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b. \u041a\u04af\u0439 \u043a\u043e\u0434\u044b {1} \u049b\u0430\u0439\u0442\u0430\u0440\u044b\u043b\u0434\u044b. \u049a\u0438\u044b\u0441 \u0436\u0430\u0493\u0434\u0430\u0439: {2}. \u0422\u0435\u0440\u0456\u0441 \u043a\u044d\u0448 \u049b\u043e\u043b\u0434\u0430\u043d\u044b\u043b\u0443\u0434\u0430.

+

+##HashLockedDomainService

+noLockedDomainConfig={0} \u04af\u0448\u0456\u043d \u049b\u04b1\u043b\u044b\u043f\u0442\u0430\u043d\u0493\u0430\u043d \u0434\u043e\u043c\u0435\u043d \u0442\u0435\u04a3\u0448\u0435\u043b\u0456\u043c\u0456 \u0436\u043e\u049b.

+

+##Bootstrap

+startingConnManagerWith=\u049a\u043e\u0441\u044b\u043b\u044b\u043c\u0434\u044b \u0440\u0435\u0442\u0442\u0435\u0443\u0448\u0456 {0} \u0441\u0438\u043f\u0430\u0442\u0442\u0430\u0440\u044b\u043c\u0435\u043d \u0456\u0441\u043a\u0435 \u049b\u043e\u0441\u044b\u043b\u0443\u0434\u0430.

+

+##XSDValidator

+resolveResource=\u041a\u0435\u043b\u0435\u0441\u0456 \u0440\u0435\u0441\u0443\u0440\u0441\u0442\u0430\u0440 \u0448\u0435\u0448\u0456\u043b\u0443\u0434\u0435: {0}, {1}, {2}, {3}.

+failedToValidate={0} \u0431\u0430\u0493\u0430\u043b\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+

+##DefaultRequestPipeline

+cachedResponse={0} \u0456\u0448\u0456\u043d\u0434\u0435\u0433\u0456 \u0441\u04b1\u0440\u0430\u0443\u0493\u0430 \u0430\u0440\u043d\u0430\u043b\u0493\u0430\u043d \u043a\u044d\u0448\u0442\u0435\u043b\u0433\u0435\u043d \u0436\u0430\u0443\u0430\u043f \u049b\u0430\u0439\u0442\u0430\u0440\u044b\u04a3\u044b\u0437.

+staleResponse="{0} \u0456\u0448\u0456\u043d\u0434\u0435\u0433\u0456 \u0440\u0435\u0441\u0443\u0440\u0441\u0442\u044b \u0441\u04b1\u0440\u0430\u0443 \u049b\u0430\u0442\u0435\u0441\u0456 \u0431\u043e\u043b\u0434\u044b, \u0431\u0456\u0440\u0430\u049b \u0431\u0456\u0437\u0434\u0435 \u043a\u044d\u0448 \u0456\u0448\u0456\u043d\u0434\u0435\u0433\u0456 \u0430\u043b\u0434\u044b\u04a3\u0493\u044b \u0436\u0430\u0443\u0430\u043f \u0431\u0430\u0440. \u041a\u044d\u0448\u0442\u0435\u043d \u0435\u0441\u043a\u0456\u0440\u0433\u0435\u043d \u0436\u0430\u0443\u0430\u043f \u049b\u0430\u0439\u0442\u0430\u0440\u0443 \u043c\u04af\u043c\u043a\u0456\u043d.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ko.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ko.properties
new file mode 100644
index 0000000..23e063f
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ko.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\ubcf4\uc548 \ud1a0\ud070 \ub610\ub294 \uc2e0\uc784 \uc815\ubcf4\uc758 \ud615\uc2dd\uc774 \uc798\ubabb \ub418\uc5b4\uc11c \uad6c\ubb38 \ubd84\uc11d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##XmlUtil

+errorParsingXML=XML\uc744 \uad6c\ubb38 \ubd84\uc11d\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \ubb34\uc2dc\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.

+errorParsingExternalGeneralEntities=\uc0ac\uc6a9 \uc911\uc778 XML \ud504\ub85c\uc138\uc11c\ub294 \uc678\ubd80 \uc77c\ubc18 \uc5d4\ud2f0\ud2f0\ub97c \ub85c\ub4dc\ud569\ub2c8\ub2e4.

+errorParsingExternalParameterEntities=\uc0ac\uc6a9 \uc911\uc778 XML \ud504\ub85c\uc138\uc11c\ub294 \uc678\ubd80 \ub9e4\uac1c\ubcc0\uc218 \uc5d4\ud2f0\ud2f0\ub97c \ub85c\ub4dc\ud569\ub2c8\ub2e4.

+errorParsingExternalDTD=\uc0ac\uc6a9 \uc911\uc778 XML \ud504\ub85c\uc138\uc11c\ub294 DTD(Document Type Definitions)\ub97c \ub85c\ub4dc\ud569\ub2c8\ub2e4.

+errorNotUsingSecureXML=\uc0ac\uc6a9 \uc911\uc778 XML \ud504\ub85c\uc138\uc11c\ub294 \ubcf4\uc548 \uad6c\ubb38 \ubd84\uc11d\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.

+reuseDocumentBuilders=\ubb38\uc11c \ube4c\ub354\ub97c \ub2e4\uc2dc \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4.

+notReuseDocBuilders=\ubb38\uc11c \ube4c\ub354\ub97c \ub2e4\uc2dc \uc0ac\uc6a9\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.

+

+##LruCacheProvider

+LRUCapacity=LRU(Least Recently Used) \uc6a9\ub7c9 {0}\uc774(\uac00) {1}\uc5d0 \ub300\ud574 \uad6c\uc131\ub429\ub2c8\ub2e4.

+

+##DynamicConfigProperty

+evalExpressionFailed={0}\uc744(\ub97c) \ud3c9\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##JsonContainerConfigLoader

+readingContainerConfig=\ucee8\ud14c\uc774\ub108 \uad6c\uc131 {0}\uc744(\ub97c) \uc77d\ub294 \uc911\uc785\ub2c8\ub2e4.

+loadFromString={0}\uc744(\ub97c) \uad6c\ubb38 \ubd84\uc11d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom={0}\uc5d0\uc11c \uc790\uc6d0\uc744 \ub85c\ub4dc \uc911\uc785\ub2c8\ub2e4.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom={0}\uc5d0\uc11c \ud30c\uc77c\uc744 \ub85c\ub4dc \uc911\uc785\ub2c8\ub2e4.

+

+##ApiServlet

+apiServletProtocolException=\ud504\ub85c\ud1a0\ucf5c \uc608\uc678\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc5d0 \uc751\ub2f5 \uc624\ub958\uac00 \ub9ac\ud134 \uc911\uc785\ub2c8\ub2e4.

+apiServletException=\ud504\ub85c\ud1a0\ucf5c \uc608\uc678\uac00 \ubc1c\uc0dd\ud588\uae30 \ub54c\ubb38\uc5d0 \uc751\ub2f5 \uc624\ub958\uac00 \ub9ac\ud134 \uc911\uc785\ub2c8\ub2e4.

+

+##FeatureRegistry

+overridingFeature={1}\uc5d0 \uc815\uc758\uac00 \uc788\ub294 {0} \uae30\ub2a5\uc744 \uacb9\uccd0\uc501\ub2c8\ub2e4.

+

+##FeatureResourceLoader

+missingFile={0} \ud30c\uc77c\uc774 \uc874\uc7ac\ud588\uc9c0\ub9cc \uc9c0\uae08\uc740 \ub204\ub77d\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+unableRetrieveLib={0}\uc5d0\uc11c \uc6d0\uaca9 \ub77c\uc774\ube0c\ub7ec\ub9ac\ub97c \uac80\uc0c9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##BasicHttpFetcher

+timeoutException={1} - {2} - {3}\ubc00\ub9ac\ucd08 \uc608\uc678\ub85c {0}\uc774(\uac00) \uc81c\ud55c\uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+exceptionOccurred={0}\uc744(\ub97c) \uac00\uc838\uc624\ub294 \uc911 {1}\ubc00\ub9ac\ucd08\uac00 \uacbd\uacfc\ub418\ub294 \uc608\uc678\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+slowResponse={0}\uc774(\uac00) \ub290\ub9ac\uac8c \uc751\ub2f5 \uc911\uc785\ub2c8\ub2e4. {1}\ubc00\ub9ac\ucd08\uac00 \uacbd\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=MD5(Message Digest 5)\ub97c \uac00\uc838\uc624\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc624\ub958\uac00 \ubb34\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+errorParsingMD5=UTF-8 \ud615\uc2dd\uc758 MD5(Message Digest 5) \ubb38\uc790\uc5f4\uc744 \uad6c\ubb38 \ubd84\uc11d\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+

+##OAuthModule

+usingRandomKey=OAuth \ud074\ub77c\uc774\uc5b8\ud2b8 \uce21 \uc0c1\ud0dc \uc554\ud638\ud654\ub97c \uc704\ud55c \ubb34\uc791\uc704 \ud0a4\uac00 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4.

+usingFile=OAuth \ud074\ub77c\uc774\uc5b8\ud2b8 \uce21 \uc0c1\ud0dc \uc554\ud638\ud654\ub97c \uc704\ud55c {0} \ud30c\uc77c\uc774 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4.

+loadKeyFileFrom=OAuth \ubd80\ud638 \ud0a4\ub97c {0}\uc5d0\uc11c \ub85c\ub4dc \uc911\uc785\ub2c8\ub2e4.

+couldNotLoadKeyFile= {0} \ud0a4 \ud30c\uc77c\uc744 \ub85c\ub4dc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+couldNotLoadSignedKey=OAuth \ubd80\ud638 \ud0a4\uac00 \uc62c\ubc14\ub974\uac8c \ub85c\ub4dc\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ud0a4\ub97c \uc791\uc131\ud558\ub824\uba74 \ub2e4\uc74c\uc744 \uc218\ud589\ud558\uc2ed\uc2dc\uc624. \n 1. \ub2e4\uc74c \uba85\ub839\uc744 \uc2e4\ud589\ud558\uc2ed\uc2dc\uc624. \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. \ub2e4\uc74c \ud589\uc744 \ucd94\uac00\ud558\uc5ec shindig.properties \ud30c\uc77c\uc744 \ud3b8\uc9d1\ud558\uc2ed\uc2dc\uc624. \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=OAuth \ucee8\uc288\uba38\ub97c {0}\uc5d0\uc11c \ucd08\uae30\ud654\ud558\ub294 \ub370 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4.

+

+##OAuthRequest

+oauthFetchFatalError=OAuth\uac00 \ucee8\ud150\uce20\ub97c \uac00\uc838\uc624\ub294 \uc911\uc5d0 \ub2e4\uc74c\uc758 \uc2ec\uac01\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4 \n {0}.

+oauthFetchErrorReprompt=OAuth\uac00 \ucee8\ud150\uce20\ub97c \uac00\uc838\uc624\ub294 \uc911\uc5d0 \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4 \n {0}. \uc2b9\uc778\uc744 \uc704\ud574 \uc0ac\uc6a9\uc790\uac00 \ub2e4\uc2dc \ud504\ub86c\ud504\ud2b8\ub429\ub2c8\ub2e4.

+bogusExpired=\uc11c\ubc84\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc740 \ub9cc\uae30\ub97c \ub9ac\ud134\ud588\uc2b5\ub2c8\ub2e4.\n {0}.

+oauthFetchUnexpectedError=OAuth\uac00 \ucee8\ud150\uce20\ub97c \uac00\uc838\uc624\ub294 \uc911\uc5d0 \ub2e4\uc74c\uc758 \uc2ec\uac01\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \n {0}.

+unauthenticatedOauth=\uc0ac\uc6a9\uc790 \uc778\uc99d\uc774 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c OAuth\uac00 \ucee8\ud150\uce20\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \n {0}.

+invalidOauth=\uc694\uccad\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc73c\ubbc0\ub85c OAuth\uac00 \ucee8\ud150\uce20\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \n {0}.

+

+##CajaCssSanitizer

+failedToParse=\uc2a4\ud0c0\uc77c\uc2dc\ud2b8\ub97c \uad6c\ubb38 \ubd84\uc11d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+unableToConvertScript=\uc2a4\ud06c\ub9bd\ud2b8 \ub178\ub4dc\ub97c OSML(OpenSocial Markup Language) \ud0dc\uadf8\ub85c \ubcc0\ud658\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##PipelineExecutor

+errorPreloading=\uc0ac\uc804 \ub85c\ub4dc\ud558\ub294 \uc911\uc5d0 \uc608\uae30\uce58 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+

+##Processor

+renderBlacklistedGadget=\uc2dc\uc2a4\ud15c\uc774 \ube14\ub799\ub9ac\uc2a4\ud2b8 \uac00\uc82f {0}\uc744(\ub97c) \ub80c\ub354\ub9c1\ud558\ub824\uace0 \uc2dc\ub3c4\ud588\uc2b5\ub2c8\ub2e4.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0}\uc744(\ub97c) \uac80\uc0c9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+failedToRead={0}\uc744(\ub97c) \uc77d\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##DefaultServiceFetcher

+httpErrorFetching={1} \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc5d0\uc11c \uc11c\ube44\uc2a4 \uba54\uc18c\ub4dc\ub97c \uac00\uc838\uc624\ub294 \uc911\uc5d0 HTTP {0} \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+failedToFetchService={0} \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc758 \uc11c\ube44\uc2a4 \uba54\uc18c\ub4dc\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+failedToParseService={0} \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc758 \uc11c\ube44\uc2a4 \uba54\uc18c\ub4dc\ub97c \uad6c\ubb38 \ubd84\uc11d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+

+##Renderer

+FailedToRender={0}\uc758 \uac00\uc82f\uc744 \ub80c\ub354\ub9c1\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=\ub2e4\uc74c extern &libs=\uc5d0 \ud558\ub098 \uc774\uc0c1\uc758 \uc54c \uc218 \uc5c6\ub294 \uae30\ub2a5\uc774 \uc874\uc7ac\ud569\ub2c8\ub2e4. {0}.

+unexpectedErrorPreloading=\uac00\uc82f\uc744 \uc0ac\uc804 \ub85c\ub4dc\ud558\ub294 \uc911\uc5d0 \uc608\uae30\uce58 \uc54a\uc740 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent={0}\uc5d0 \ub300\ud55c \ucee8\ud150\uce20 \uc720\ud615 \uc5c6\uc774 \uc81c\uac70 \uc694\uccad\uc774 \ubc1c\ud589\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+requestToSanitizeUnknownContent={1}\uc5d0 \ub300\ud574 \uc54c\ub824\uc9c4 \ucee8\ud150\uce20 \uc720\ud615 {0}\uc774(\uac00) \uc544\ub2cc \uc81c\uac70 \uc694\uccad\uc774 \ubc1c\ud589\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+unableToSanitizeUnknownImg=\uc774\ubbf8\uc9c0 \uc720\ud615 {0}\uc740(\ub294) \uc54c \uc218 \uc5c6\ub294 \uc720\ud615\uc73c\ub85c \uc81c\uac70\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+unableToDetectImgType=\ucee8\ud150\uce20 \uc81c\uac70 \uc2dc {0}\uc5d0 \ub300\ud55c \uc774\ubbf8\uc9c0 \uc720\ud615\uc744 \ubc1c\uacac\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##BasicImageRewriter

+ioErrorRewritingImg={0} \uc774\ubbf8\uc9c0\ub97c \ub2e4\uc2dc \uc4f0\ub294 \uc911\uc5d0 \ub2e4\uc74c \uc785/\ucd9c\ub825(I/O) \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+unknownErrorRewritingImg={0} \uc774\ubbf8\uc9c0\ub97c \ub2e4\uc2dc \uc4f0\ub294 \uc911\uc5d0 \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+failedToReadImg={0} \uc774\ubbf8\uc9c0\ub97c \uc77d\uc744 \uc218 \uc5c6\uc5b4 \uc0dd\ub7b5 \uc911\uc785\ub2c8\ub2e4. \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Caja CSS\ub97c \uad6c\ubb38 \ubd84\uc11d\ud558\ub294 \uc911\uc5d0 \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}\uc5d0 \ub300\ud574 {0}.

+

+##ImageAttributeRewriter

+unableToProcessImg={0} \uc774\ubbf8\uc9c0 \uc790\uc6d0\uc744 \ucc98\ub9ac\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+unableToReadResponse={0} \uc774\ubbf8\uc9c0 \uc790\uc6d0\uc5d0 \ub300\ud55c \uc751\ub2f5\uc744 \uc77d\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+unableToFetchImg={0} \uc774\ubbf8\uc9c0 \uc790\uc6d0\uc744 \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+unableToParseImg={0} \uc774\ubbf8\uc9c0 \uc790\uc6d0\uc744 \uad6c\ubb38 \ubd84\uc11d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##MutableContent

+exceptionParsingContent=\uac00\uc82f\uc5d0 \ub300\ud55c \ucee8\ud150\uce20\ub97c \uad6c\ubb38 \ubd84\uc11d\ud558\ub294 \uc911\uc5d0 \uc608\uc678\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload={0}\uc758 \uac00\uc82f\uc744 \uc0ac\uc804 \ub85c\ub4dc \uc2dc \uad6c\ubb38 \ubd84\uc11d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##ProxyingVisitor

+uriExceptionParsing={0}\uc758 \uac00\uc82f\uc744 \uad6c\ubb38 \ubd84\uc11d\ud558\ub294 \uc911\uc5d0 URI(Uniform Resource Identifier) \uc608\uc678\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+

+##TemplateRewriter

+malformedTemplateLib=\uc798\ubabb\ub41c \ud615\uc2dd\uc758 \ud15c\ud50c\ub9ac\ud2b8 \ub77c\uc774\ube0c\ub7ec\ub9ac \ub54c\ubb38\uc5d0 \uc608\uc678\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+

+##CajaContnetRewriter

+cajoledCacheCreated=\ucef4\ud30c\uc77c\ub41c \uce90\uc2dc\uac00 \uc791\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+retrieveReference={0}\uc744(\ub97c) \uac80\uc0c9 \uc911\uc785\ub2c8\ub2e4.

+unableToCajole={0}\uc758 \uac00\uc82f\uc744 \ucef4\ud30c\uc77c\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\uc5f0\uacb0\ub41c \ud504\ub85d\uc2dc\ub97c \uc694\uccad\ud558\ub294 \uc911\uc5d0 \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue={0}\uc758 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc740 TTL(Time To Live) \uac12\uc774 \ubb34\uc2dc\ub429\ub2c8\ub2e4.

+

+##HttpRequestHandler

+gadgetCreationError=\ub2e4\uc2dc \uc4f0\uae30 \uc704\ud574 \uac00\uc82f\uc744 \uc791\uc131\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uac00\uc82f\uc774 \uc5c6\uc774 \uc751\ub2f5\uc744 \ub2e4\uc2dc \uc791\uc131\ud558\ub294 \uc911\uc785\ub2c8\ub2e4.

+

+##ProxyServlet

+embededImgWrongDomain={0} URL \uc784\ubca0\ub4dc \uc694\uccad\uc774 \uc798\ubabb\ub41c \ub3c4\uba54\uc778 {1}\uc5d0 \uc791\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+

+##DefaultTemplateProcessor

+elFailure={0}\uc758 \uac00\uc82f\uc5d0 \ub300\ud574 \ub2e4\uc74c EL \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+

+##UriUtils

+skipIllegalHeader={0} \ud5e4\ub354\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc544 \uc0dd\ub7b5 \uc911\uc785\ub2c8\ub2e4. \ub2e4\uc74c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion={0}\uc744(\ub97c) \uc5c5\ub370\uc774\ud2b8\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc0c1\ud0dc \ucf54\ub4dc {1}\uc774(\uac00) \ub9ac\ud134\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc608\uc678: {2}. \uce90\uc2dc\ub41c \ubc84\uc804\uc744 \ub300\uc2e0 \uc0ac\uc6a9\ud569\ub2c8\ub2e4.

+updateSpecFailureApplyNegCache={0}\uc744(\ub97c) \uc5c5\ub370\uc774\ud2b8\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uc0c1\ud0dc \ucf54\ub4dc {1}\uc774(\uac00) \ub9ac\ud134\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \uc608\uc678: {2}. \ub124\uac70\ud2f0\ube0c \uce90\uc2dc\uac00 \uc801\uc6a9\ub429\ub2c8\ub2e4

+

+##HashLockedDomainService

+noLockedDomainConfig={0}\uc5d0 \ub300\ud574 \uc7a0\uae34 \ub3c4\uba54\uc778 \uad6c\uc131\uc774 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.

+

+##Bootstrap

+startingConnManagerWith={0} \ud2b9\uc131\uc774 \uc788\ub294 \uc5f0\uacb0 \uad00\ub9ac\uc790\uac00 \uc2dc\uc791 \uc911\uc785\ub2c8\ub2e4.

+

+##XSDValidator

+resolveResource=\ub2e4\uc74c \uc790\uc6d0\uc744 \ubd84\uc11d \uc911\uc785\ub2c8\ub2e4. {0}, {1}, {2}, {3}.

+failedToValidate={0}\uc744(\ub97c) \uc720\ud6a8\uc131 \uac80\uc99d\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+

+##DefaultRequestPipeline

+cachedResponse=\uc694\uccad\uc5d0 \ub300\ud574 \uce90\uc2dc\ub41c \uc751\ub2f5\uc744 {0}(\uc73c)\ub85c \ub418\ub3cc\ub9bd\ub2c8\ub2e4.

+staleResponse="{0}\uc5d0\uc11c \uc790\uc6d0\uc744 \uc694\uccad\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc73c\ub098 \uce90\uc2dc\uc5d0 \uc774\uc804 \uc751\ub2f5\uc774 \uc788\uc2b5\ub2c8\ub2e4. \uce90\uc2dc\ub85c\ubd80\ud130 \uac00\ub2a5\ud55c \uc2dc\uac04\uc774 \uacbd\uacfc\ub41c(stale) \uc751\ub2f5\uc744 \ub418\ub3cc\ub9bd\ub2c8\ub2e4.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_nl.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_nl.properties
new file mode 100644
index 0000000..47ed60b
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_nl.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Het beveiligingstoken of legitimatiegegeven onjuist geformuleerd en kan niet worden ontleed.

+

+##XmlUtil

+errorParsingXML=Fout bij ontleden van XML. Dit kunt u negeren.

+errorParsingExternalGeneralEntities=Met de gebruikte XML-processor worden de externe algemene entiteiten geladen.

+errorParsingExternalParameterEntities=Met de gebruikte XML-processor worden de externe parameterentiteiten geladen.

+errorParsingExternalDTD=Met de gebruikte XML-processor worden DTD's (Document Type Definitions) geladen.

+errorNotUsingSecureXML=De gebruikte XML-processor biedt geen ondersteuning voor beveiligd ontleden.

+reuseDocumentBuilders=Documentbuilders worden opnieuw gebruikt.

+notReuseDocBuilders=Documentbuilders worden niet opnieuw gebruikt.

+

+##LruCacheProvider

+LRUCapacity=De kleinste recente capaciteit {0} is geconfigureerd voor {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} kan niet worden ge\u00ebvalueerd.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Containerconfiguratie {0} wordt gelezen.

+loadFromString={0} kan niet worden ontleed.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Resources van {0} worden geladen.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Bestanden van {0} worden geladen.

+

+##ApiServlet

+apiServletProtocolException=Omdat een protocoluitzondering is opgetreden, wordt een responsfout gemeld.

+apiServletException=Omdat een protocoluitzondering is opgetreden, wordt een responsfout gemeld.

+

+##FeatureRegistry

+overridingFeature=De functie {0} met definitie op {1}, wordt genegeerd.

+

+##FeatureResourceLoader

+missingFile=Het bestand {0} was eerder wel aanwezig, maar ontbreekt nu.

+unableRetrieveLib=De bibliotheek op afstand van {0} kan niet worden opgehaald.

+

+##BasicHttpFetcher

+timeoutException=Voor {0} is  de timeout verstreken vanwege de volgende uitzondering: {1} - {2} - {3} ms.

+exceptionOccurred=De volgende uitzondering is opgetreden bij het ophalen van {0}: {1} ms verstreken.

+slowResponse={0} is traag met reageren. {1} ms zijn verstreken.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Een fout is opgetreden bij het ophalen van MD5 (Message Digest 5). De fout wordt genegeerd.

+errorParsingMD5=Een fout is opgetreden tijdens het ontleden van de MD5 (Message Digest 5)-tekenreeks met UTF-8-indeling.

+

+##OAuthModule

+usingRandomKey=Er wordt een willekeurige sleutel voor versleuteling van de OAuth client-sidestatus gebruikt.

+usingFile=Het bestand {0} voor versleuteling van de OAuth client-sidestatus wordt gebruikt.

+loadKeyFileFrom=De OAuth-handtekeningsleutel van {0} wordt geladen.

+couldNotLoadKeyFile= Het sleutelbestand {0} kan niet worden geladen.

+couldNotLoadSignedKey=De OAuth-handtekeningsleutel is incorrect geladen. Om een sleutel te maken: \n 1. Voer de volgende opdracht uit: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Wijzig het bestand shindig.properties door de volgende regels toe te voegen: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=Initialiseren van OAuth-consumenten van {0} is mislukt.

+

+##OAuthRequest

+oauthFetchFatalError=De volgende fatale fout is opgetreden bij het ophalen van content door OAuth: \n \ {0}.

+oauthFetchErrorReprompt=De volgende fout is opgetreden bij het ophalen van content door OAuth: \n \ {0}. De gebruiker wordt opnieuw gevraagd om goedkeuring.

+bogusExpired=De server heeft een ongeldige vervaldatum teruggezonden:\n {0}.

+oauthFetchUnexpectedError=De volgende fatale fout is opgetreden bij het ophalen van content door OAuth: \n \ {0}.

+unauthenticatedOauth=OAuth kan geen content ophalen omdat de gebruikersverificatie niet aanwezig is. De volgende fout is opgetreden: \n {0}.

+invalidOauth=OAuth kan geen content ophalen omdat de aanvraag ongeldig is. De volgende fout is opgetreden: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=Het stijlblad kan niet worden ontleed.

+unableToConvertScript=Het scriptknooppunt an niet worden geconverteerd naar een OSML (OpenSocial Markup Language)-tag.

+

+##PipelineExecutor

+errorPreloading=Een onverwachte fout is opgetreden tijdens het vooraf laden.

+

+##Processor

+renderBlacklistedGadget=Het systeem probeerde de volgende op de zwarte lijst geplaatste gadget weer te geven: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} kan niet worden opgehaald.

+failedToRead={0} kan niet worden gelezen.

+

+##DefaultServiceFetcher

+httpErrorFetching=Een HTTP-fout {0} is opgetreden bij het ophalen van servicemethoden vanaf het eindpunt {1}.

+failedToFetchService=Servicemethoden vanaf eindpunt {0} kunnen niet worden opgehaald. De volgende fout is opgetreden: {1}.

+failedToParseService=Servicemethoden vanaf eindpunt {0} kunnen niet worden ontleed. De volgende fout is opgetreden: {1}.

+

+##Renderer

+FailedToRender=De gadget op {0} is niet weergegeven. De volgende fout is opgetreden: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Een of meer onbekende functies zijn aanwezig in de volgende externe &libs=: {0}.

+unexpectedErrorPreloading=Een onverwachte fout is opgetreden bij het vooraf laden van de gadget.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Een aanvraag voor opschonen is opgegeven zonder een contenttype voor {0}.

+requestToSanitizeUnknownContent=Een aanvraag voor opschonen is opgegeven zonder een bekend contenttype {0} voor {1}.

+unableToSanitizeUnknownImg=Het afbeeldingstype {0} is onbekend en kan niet worden opgeschoond.

+unableToDetectImgType=Het afbeeldingstype voor {0} kan niet worden gevonden bij het opschonen van content.

+

+##BasicImageRewriter

+ioErrorRewritingImg=De volgende invoer/uitvoer-fout is opgetreden bij het opnieuw schrijven van de afbeelding {0}: {1}.

+unknownErrorRewritingImg=De volgende fout is opgetreden bij het opnieuw schrijven van de afbeelding {0}: {1}.

+failedToReadImg=De afbeelding {0} kan niet worden gelezen en wordt overgeslagen. De volgende fout is opgetreden: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=De volgende fout is opgetreden bij het ontleden van de Caja-CSS: {0} voor {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=De afbeeldingsresource {0} kan niet worden verwerkt.

+unableToReadResponse=De respons voor de afbeeldingsresource {0} kan niet worden gelezen.

+unableToFetchImg=De afbeeldingsresource {0} kan niet worden opgehaald.

+unableToParseImg=De afbeeldingsresource {0} kan niet worden ontleed.

+

+##MutableContent

+exceptionParsingContent=Een uitzondering is opgetreden bij het ontleden van content voor de gadget.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=De gadget op {0} kan niet worden ontleed voor vooraf laden.

+

+##ProxyingVisitor

+uriExceptionParsing=Een URI (Uniform Resource Identifier)-uitzondering is opgetreden bij het ontleden van de gadget op {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Uitzonderingen zijn opgetreden vanwege onjuist geconfigureerde sjablonenbibliotheken.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Een gecompileerde cache is gemaakt.

+retrieveReference={0} wordt is opgehaald.

+unableToCajole=De gadget op {0} kan niet worden gecompileerd.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=De volgende fout is opgetreden bij het aanvragen van een aaneengeschakelde proxy: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Een ongeldige TTL (Time To Live)-waarde van {0} is genegeerd.

+

+##HttpRequestHandler

+gadgetCreationError=Er is een fout opgetreden bij het genereren van de gadget voor opnieuw schrijven. Het antwoord opnieuw schrijven zonder de gadget.

+

+##ProxyServlet

+embededImgWrongDomain=De aanvraag voor het inbedden van de URL {0} is gedaan voor het verkeerde domein {1}.

+

+##DefaultTemplateProcessor

+elFailure=De volgende EL-fout is opgetreden voor de gadget op {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=De kop {0} is niet toegestaan en wordt overgeslagen. De volgende fout is opgetreden: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Een fout is opgetreden tijdens het bijwerken van {0}. Statuscode {1} is geretourneerd. Uitzondering: {2}. In plaats hiervan wordt een gecachete versie gebruikt.

+updateSpecFailureApplyNegCache=Er is een fout opgetreden tijdens het bijwerken van {0}. Statuscode {1} is geretourneerd. Uitzondering: {2}. Er wordt een negatieve cache toegepast.

+

+##HashLockedDomainService

+noLockedDomainConfig=Er bestaat geen vergrendelde domeinconfiguratie voor {0}.

+

+##Bootstrap

+startingConnManagerWith=Verbindingsmanager met {0} eigenschappen wordt gestart.

+

+##XSDValidator

+resolveResource=De volgende resources worden omgezet: {0}, {1}, {2}, {3}.

+failedToValidate=Er is een fout opgetreden tijdens het valideren van {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Cacherespons voor de aanvraag wordt geretourneerd naar {0}.

+staleResponse="Er is een fout opgetreden bij het aanvragen van de resource op {0}, maar er bevindt zich een oudere respons in de cache. Er wordt mogelijk een verouderde respons uit de cache geretourneerd.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_no.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_no.properties
new file mode 100644
index 0000000..1806904
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_no.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Sikkerhetstokenet eller legitimasjoner har feil format og kan ikke analyseres.

+

+##XmlUtil

+errorParsingXML=Feil ved analyse av XML. Dette kan ignoreres.

+errorParsingExternalGeneralEntities=XML-prosessoren som blir brukt, vil laste inn eksterne generelle enheter.

+errorParsingExternalParameterEntities=XML-prosessoren som blir brukt, vil laste inn eksterne parameterenheter.

+errorParsingExternalDTD=XML-prosessoren som blir brukt, vil laste inn dokumenttypedefinisjoner (DTD).

+errorNotUsingSecureXML=XML-prosessoren som blir brukt, st\u00f8tter ikke sikker analyse.

+reuseDocumentBuilders=Dokumentbyggere blir brukt p\u00e5 nytt.

+notReuseDocBuilders=Dokumentbyggere blir ikke brukt p\u00e5 nytt.

+

+##LruCacheProvider

+LRUCapacity=LRU-kapasitet {0} (LRU=Least Recently Used) er konfigurert for {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} kan ikke evalueres.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Containerkonfigurasjon {0} blir lest.

+loadFromString={0} kan ikke analyseres.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Ressurser fra {0} blir lastet inn.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Filer fra {0} blir lastet inn.

+

+##ApiServlet

+apiServletProtocolException=Det blir returnert en svarfeil fordi det oppstod et protokollunntak.

+apiServletException=Det blir returnert en svarfeil fordi det oppstod et protokollunntak.

+

+##FeatureRegistry

+overridingFeature=Funksjonen {0} med definisjon i {1} blir overskrevet.

+

+##FeatureResourceLoader

+missingFile=Filen {0} fantes tidligere, men mangler n\u00e5.

+unableRetrieveLib=Det eksterne biblioteket fra {0} kan ikke hentes.

+

+##BasicHttpFetcher

+timeoutException={0} har blitt tidsavbrutt p\u00e5 grunn av f\u00f8lgende unntak: {1} - {2} - {3} ms.

+exceptionOccurred=F\u00f8lgende unntak oppstod ved henting av {0}: {1} ms ble brukt.

+slowResponse={0} svarer tregt. {1} ms ble brukt.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Det oppstod en feil ved henting av MD5 (Message Digest 5). Feilen ble ignorert.

+errorParsingMD5=Det oppstod en feil ved analysering av MD5-streng (Message Digest 5) i UTF-8-format.

+

+##OAuthModule

+usingRandomKey=Det blir brukt en tilfeldig n\u00f8kkel for kryptering av OAuth-tilstand p\u00e5 klientsiden.

+usingFile=Filen {0} blir brukt for kryptering av OAuth-tilstand p\u00e5 klientsiden.

+loadKeyFileFrom=OAuth-signeringsn\u00f8kkelen fra {0} blir lastet inn.

+couldNotLoadKeyFile= {0}-n\u00f8kkelfilen kunne ikke lastes inn.

+couldNotLoadSignedKey=OAuth-signeringsn\u00f8kkelen ble ikke lastet inn p\u00e5 riktig m\u00e5te. Slik oppretter du en n\u00f8kkel: \n 1. Kj\u00f8r f\u00f8lgende kommando: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Rediger filen shindig.properties ved \u00e5 legge til disse linjene: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=OAuth-forbrukere fra {0} kunne ikke initialiseres.

+

+##OAuthRequest

+oauthFetchFatalError=F\u00f8lgende alvorlige feil oppstod da OAuth hentet innhold: \n {0}.

+oauthFetchErrorReprompt=F\u00f8lgende feil oppstod da OAuth hentet innhold: \n {0}. Brukeren f\u00e5r ny foresp\u00f8rsel om \u00e5 godkjenne.

+bogusExpired=Serveren returnerte et ugyldig utl\u00f8p:\n {0}.

+oauthFetchUnexpectedError=F\u00f8lgende alvorlige feil oppstod da OAuth hentet innhold: \n {0}.

+unauthenticatedOauth=OAuth kan ikke hente innhold fordi brukerautentiseringen ikke finnes. F\u00f8lgende feil oppstod: \n {0}.

+invalidOauth=OAuth kan ikke hente innhold fordi foresp\u00f8rselen er ugyldig. F\u00f8lgende feil oppstod: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=Stilarket kan ikke analyseres.

+unableToConvertScript=Skriptnoden kan ikke konverteres til en OSML-kode (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Det oppstod en uventet feil ved forh\u00e5ndslasting.

+

+##Processor

+renderBlacklistedGadget=Systemet fors\u00f8kte \u00e5 gjengi f\u00f8lgende svartelistede gadget: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} kan ikke hentes.

+failedToRead={0} kan ikke leses.

+

+##DefaultServiceFetcher

+httpErrorFetching=Det oppstod en HTTP {0}-feil ved henting av servicemetoder fra {1}-sluttpunktet.

+failedToFetchService=Servicemetoder fra {0}-sluttpunktet kunne ikke hentes. F\u00f8lgende feil oppstod: {1}.

+failedToParseService=Servicemetoder fra {0}-sluttpunktet kunne ikke analyseres. F\u00f8lgende feil oppstod: {1}.

+

+##Renderer

+FailedToRender=Gadgeten i {0} kunne ikke gjengis. F\u00f8lgende feil oppstod: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Det finnes en eller flere ukjente funksjoner i f\u00f8lgende eksterne &libs=: {0}.

+unexpectedErrorPreloading=Det oppstod en uventet feil ved forh\u00e5ndslasting av gadgeten.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Det ble gitt en foresp\u00f8rsel om rensing uten noen innholdstype for {0}.

+requestToSanitizeUnknownContent=Det ble gitt en foresp\u00f8rsel om rensing uten en kjent innholdstype {0} for {1}.

+unableToSanitizeUnknownImg=Bildetypen {0} er ukjent og kan ikke renses.

+unableToDetectImgType=Bildetypen for {0} kan ikke oppdages ved rensing av innhold.

+

+##BasicImageRewriter

+ioErrorRewritingImg=F\u00f8lgende inn/ut-feil oppstod ved ny skriving av {0}-bildet: {1}.

+unknownErrorRewritingImg=F\u00f8lgende feil oppstod ved ny skriving av {0}-bildet: {1}.

+failedToReadImg={0}-bildet kan ikke leses og blir hoppet over. F\u00f8lgende feil oppstod: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=F\u00f8lgende feil oppstod ved analysering av Caja CSS: {0} for {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg={0}-bilderessursen kan ikke behandles.

+unableToReadResponse=Svaret for {0}-bilderessursen kan ikke leses.

+unableToFetchImg={0}-bilderessursen kan ikke hentes.

+unableToParseImg={0}-bilderessursen kan ikke analyseres.

+

+##MutableContent

+exceptionParsingContent=Det oppstod et unntak ved analysering av innhold for gadgeten.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Gadgeten i {0} kunne ikke analyseres for forh\u00e5ndslasting.

+

+##ProxyingVisitor

+uriExceptionParsing=Det oppstod et URI-unntak (Uniform Resource Identifier) ved analysering av gadget i {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Det oppstod unntak p\u00e5 grunn av malbiblioteker med feil format.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Det ble opprettet en kompilert hurtigbuffer.

+retrieveReference={0} blir hentet.

+unableToCajole=Gadgeten i {0} kan ikke kompileres.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=F\u00f8lgende feil oppstod ved foresp\u00f8rsel etter en sammenkjedet proxy: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=En ugyldig TTL-verdi (Time To Live) p\u00e5 {0} ble ignorert.

+

+##HttpRequestHandler

+gadgetCreationError=Det oppstod en feil ved opprettelse av gadgeten for omskriving. Skriver om svaret uten gadgeten.

+

+##ProxyServlet

+embededImgWrongDomain=Foresp\u00f8rselen om \u00e5 bygge inn URLen {0} ble gitt til feil domene {1}.

+

+##DefaultTemplateProcessor

+elFailure=F\u00f8lgende EL-feil oppstod for gadgeten i {0}: {1}.

+

+##UriUtils

+skipIllegalHeader={0}-toppteksten er ugyldig og blir hoppet over. F\u00f8lgende feil oppstod: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Det oppstod en feil ved oppdatering av {0}. Statuskode {1} ble returnert. Unntak: {2}. Det blir brukt en hurtigbufret versjon i stedet.

+updateSpecFailureApplyNegCache=Det oppstod en feil ved oppdatering av {0}. Statuskode {1} ble returnert. Unntak: {2}. Det blir brukt en negativ hurtigbuffer.

+

+##HashLockedDomainService

+noLockedDomainConfig=Det finnes ingen l\u00e5st domenekonfigurasjon for {0}.

+

+##Bootstrap

+startingConnManagerWith=Tilkoblingsstyrer med {0}-egenskaper starter.

+

+##XSDValidator

+resolveResource=F\u00f8lgende ressurser blir behandlet: {0}, {1}, {2}, {3}.

+failedToValidate=Det oppstod en feil ved validering av {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Returnerer hurtigbufret svar p\u00e5 foresp\u00f8rsel til {0}.

+staleResponse="Det oppstod en feil ved foresp\u00f8rsel om ressurs p\u00e5 {0} men vi har et tidligere svar i hurtigbufferen. Returnerer et mulig foreldet svar fra hurtigbufferen.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pl.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pl.properties
new file mode 100644
index 0000000..48ba238
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pl.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Znacznik zabezpiecze\u0144 lub referencja jest zniekszta\u0142cona i nie mo\u017cna jej przeanalizowa\u0107.

+

+##XmlUtil

+errorParsingXML=B\u0142\u0105d podczas analizowania kodu XML. Ten b\u0142\u0105d mo\u017cna zignorowa\u0107.

+errorParsingExternalGeneralEntities=U\u017cywany procesor XML za\u0142aduje zewn\u0119trzne encje og\u00f3lne.

+errorParsingExternalParameterEntities=U\u017cywany procesor XML za\u0142aduje zewn\u0119trzne encje parametr\u00f3w.

+errorParsingExternalDTD=U\u017cywany procesor XML za\u0142aduje definicje DTD (Document Type Definition).

+errorNotUsingSecureXML=U\u017cywany procesor XML nie obs\u0142uguje bezpiecznego analizowania.

+reuseDocumentBuilders=Programy buduj\u0105ce dokumenty s\u0105 ponownie wykorzystywane.

+notReuseDocBuilders=Programy buduj\u0105ce dokumenty nie s\u0105 ponownie wykorzystywane.

+

+##LruCacheProvider

+LRUCapacity=Wielko\u015b\u0107 na potrzeby algorytmu LRU (Least Recently Used) {0} zosta\u0142a skonfigurowana dla elementu {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed=Nie mo\u017cna warto\u015bciowa\u0107 elementu {0}.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Odczytywanie konfiguracji kontenera {0}.

+loadFromString=Nie mo\u017cna przeanalizowa\u0107 elementu {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=\u0141adowanie zasob\u00f3w z {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=\u0141adowanie plik\u00f3w z {0}.

+

+##ApiServlet

+apiServletProtocolException=Zwracany jest b\u0142\u0105d odpowiedzi, poniewa\u017c wyst\u0105pi\u0142 wyj\u0105tek protoko\u0142u.

+apiServletException=Zwracany jest b\u0142\u0105d odpowiedzi, poniewa\u017c wyst\u0105pi\u0142 wyj\u0105tek protoko\u0142u.

+

+##FeatureRegistry

+overridingFeature=Nadpisywanie funkcji {0} z definicj\u0105 w {1}.

+

+##FeatureResourceLoader

+missingFile=Plik {0} istnia\u0142, ale obecnie nie istnieje.

+unableRetrieveLib=Nie mo\u017cna pobra\u0107 biblioteki zdalnej z {0}.

+

+##BasicHttpFetcher

+timeoutException=Operacja {0} przekroczy\u0142a limit czasu z powodu wyst\u0105pienia nast\u0119puj\u0105cego wyj\u0105tku: {1} - {2} - {3} ms.

+exceptionOccurred=Wyst\u0105pi\u0142 nast\u0119puj\u0105cy wyj\u0105tek podczas pobierania elementu {0}: up\u0142yn\u0119\u0142o {1} ms.

+slowResponse=Serwer {0} zbyt d\u0142ugo odpowiada. Up\u0142yn\u0119\u0142o: {1} ms.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Wyst\u0105pi\u0142 b\u0142\u0105d podczas uzyskiwania skr\u00f3tu MD5 (Message Digest 5). B\u0142\u0105d zosta\u0142 zignorowany.

+errorParsingMD5=Wyst\u0105pi\u0142 b\u0142\u0105d podczas analizowania \u0142a\u0144cucha skr\u00f3tu MD5 (Message Digest 5) w formacie UTF-8.

+

+##OAuthModule

+usingRandomKey=Na potrzeby szyfrowania stanu po stronie klienta OAuth u\u017cywany jest losowy klucz.

+usingFile=Na potrzeby szyfrowania stanu po stronie klienta OAuth u\u017cywany jest plik {0}.

+loadKeyFileFrom=\u0141adowanie klucza podpisywania OAuth z {0}.

+couldNotLoadKeyFile= Nie mo\u017cna za\u0142adowa\u0107 pliku kluczy {0}.

+couldNotLoadSignedKey=Klucz podpisywania OAuth nie zosta\u0142 poprawnie za\u0142adowany. Aby utworzy\u0107 klucz: \n 1. Uruchom nast\u0119puj\u0105c\u0105 komend\u0119: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n 2. Edytuj plik shindig.properties, dodaj\u0105c nast\u0119puj\u0105ce wiersze: \n{0} =<\u015bcie\u017cka_do_klucza_OAuth.pem>\n {1} =mykey\n

+failedToInit=Nie powiod\u0142o si\u0119 zainicjowanie konsument\u00f3w OAuth z {0}.

+

+##OAuthRequest

+oauthFetchFatalError=Podczas pobierania tre\u015bci przez protok\u00f3\u0142 OAuth wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d krytyczny: \n {0}.

+oauthFetchErrorReprompt=Podczas pobierania tre\u015bci przez protok\u00f3\u0142 OAuth wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: \n {0}. U\u017cytkownik zostanie ponownie poproszony o zatwierdzenie.

+bogusExpired=Serwer zwr\u00f3ci\u0142 niepoprawn\u0105 dat\u0119 wa\u017cno\u015bci: \n {0}.

+oauthFetchUnexpectedError=Podczas pobierania tre\u015bci przez protok\u00f3\u0142 OAuth wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d krytyczny: \n {0}.

+unauthenticatedOauth=Protok\u00f3\u0142 OAuth nie mo\u017ce pobra\u0107 tre\u015bci, poniewa\u017c u\u017cytkownik nie jest uwierzytelniony. Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: \n {0}

+invalidOauth=Protok\u00f3\u0142 OAuth nie mo\u017ce pobra\u0107 tre\u015bci, poniewa\u017c \u017c\u0105danie jest niepoprawne. Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: \n {0}

+

+##CajaCssSanitizer

+failedToParse=Nie mo\u017cna przeanalizowa\u0107 arkusza styl\u00f3w.

+unableToConvertScript=W\u0119z\u0142a skryptu nie mo\u017cna przekszta\u0142ci\u0107 w znacznik OSML (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas wst\u0119pnego \u0142adowania.

+

+##Processor

+renderBlacklistedGadget=System pr\u00f3bowa\u0142 wy\u015bwietli\u0107 nast\u0119puj\u0105cy gad\u017cet znajduj\u0105cy si\u0119 na czarnej li\u015bcie: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=Nie mo\u017cna pobra\u0107 elementu {0}.

+failedToRead=Nie mo\u017cna odczyta\u0107 elementu {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=Wyst\u0105pi\u0142 b\u0142\u0105d HTTP {0} podczas pobierania metod us\u0142ugi z punktu ko\u0144cowego {1}.

+failedToFetchService=Nie mo\u017cna pobra\u0107 metod us\u0142ug z punktu ko\u0144cowego {0}. Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: {1}.

+failedToParseService=Nie mo\u017cna przeanalizowa\u0107 metod us\u0142ug z punktu ko\u0144cowego {0}. Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: {1}.

+

+##Renderer

+FailedToRender=Gad\u017cet w {0} nie zosta\u0142 wy\u015bwietlony. Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=W nast\u0119puj\u0105cych bibliotekach zewn\u0119trznych istnieje co najmniej jeden nieznany sk\u0142adnik: {0}.

+unexpectedErrorPreloading=Wyst\u0105pi\u0142 nieoczekiwany b\u0142\u0105d podczas wst\u0119pnego \u0142adowania gad\u017cetu.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Wys\u0142ano \u017c\u0105danie czyszczenia bez typu tre\u015bci dla elementu {0}.

+requestToSanitizeUnknownContent=Wys\u0142ano \u017c\u0105danie czyszczenia bez znanego typu tre\u015bci {0} dla elementu {1}.

+unableToSanitizeUnknownImg=Typ obrazu {0} jest nieznany i nie mo\u017cna go wyczy\u015bci\u0107.

+unableToDetectImgType=Podczas czyszczenia tre\u015bci nie mo\u017cna wykry\u0107 typu obrazu {0}.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Podczas ponownego zapisywania obrazu {0} wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d we/wy: {1}.

+unknownErrorRewritingImg=Podczas ponownego zapisywania obrazu {0} wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: {1}.

+failedToReadImg=Nie mo\u017cna odczyta\u0107 obrazu {0}. Zostanie on pomini\u0119ty. Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d podczas analizowania arkusza CSS Caja: {0} dla {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=Nie mo\u017cna przetworzy\u0107 zasobu obrazu {0}.

+unableToReadResponse=Nie mo\u017cna odczyta\u0107 odpowiedzi dla zasobu obrazu {0}.

+unableToFetchImg=Nie mo\u017cna pobra\u0107 zasobu obrazu {0}.

+unableToParseImg=Nie mo\u017cna przeanalizowa\u0107 zasobu obrazu {0}.

+

+##MutableContent

+exceptionParsingContent=Wyst\u0105pi\u0142 wyj\u0105tek podczas analizowania tre\u015bci gad\u017cetu.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Gad\u017cet w {0} nie m\u00f3g\u0142 zosta\u0107 przeanalizowany w celu wst\u0119pnego \u0142adowania.

+

+##ProxyingVisitor

+uriExceptionParsing=Wyst\u0105pi\u0142 wyj\u0105tek identyfikatora URI (Uniform Resource Identifier) podczas analizowania gad\u017cetu w {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Wyst\u0105pi\u0142y wyj\u0105tki z powodu zniekszta\u0142conych bibliotek szablon\u00f3w.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Utworzono pami\u0119\u0107 podr\u0119czn\u0105 danych, dla kt\u00f3rych dokonano konwersji w skrypt Caja.

+retrieveReference=Pobieranie elementu {0}.

+unableToCajole=Nie mo\u017cna dokona\u0107 konwersji gad\u017cetu w {0} w skrypt Caja.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Podczas \u017c\u0105dania skonkatenowanego proxy wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Niepoprawna warto\u015b\u0107 czasu \u017cycia (Time To Live - TTL) {0} zosta\u0142a zignorowana.

+

+##HttpRequestHandler

+gadgetCreationError=Wyst\u0105pi\u0142 b\u0142\u0105d podczas tworzenia gad\u017cetu na potrzeby przebudowywania. Odpowied\u017a zostanie przebudowana bez u\u017cycia gad\u017cetu.

+

+##ProxyServlet

+embededImgWrongDomain=\u017b\u0105danie osadzenia adresu URL {0} zosta\u0142o wys\u0142ane do niepoprawnej domeny {1}.

+

+##DefaultTemplateProcessor

+elFailure=Dla gad\u017cetu w {0} wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d j\u0119zyka wyra\u017ce\u0144: {1}.

+

+##UriUtils

+skipIllegalHeader=Nag\u0142\u00f3wek {0} jest niedozwolony i zostanie pomini\u0119ty. Wyst\u0105pi\u0142 nast\u0119puj\u0105cy b\u0142\u0105d: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Wyst\u0105pi\u0142 b\u0142\u0105d podczas aktualizowania elementu {0}. Zwr\u00f3cony kod statusu: {1}. Wyj\u0105tek: {2}. Zostanie u\u017cyta wersja z pami\u0119ci podr\u0119cznej.

+updateSpecFailureApplyNegCache=Wyst\u0105pi\u0142 b\u0142\u0105d podczas aktualizowania elementu {0}. Zwr\u00f3cony kod statusu: {1}. Wyj\u0105tek: {2}. Zostanie zastosowana pami\u0119\u0107 podr\u0119czna niepowodze\u0144.

+

+##HashLockedDomainService

+noLockedDomainConfig=Nie istnieje zablokowana konfiguracja domeny dla elementu {0}.

+

+##Bootstrap

+startingConnManagerWith=Uruchamianie mened\u017cera po\u0142\u0105cze\u0144 z nast\u0119puj\u0105c\u0105 liczb\u0105 w\u0142a\u015bciwo\u015bci: {0}.

+

+##XSDValidator

+resolveResource=Rozstrzyganie nast\u0119puj\u0105cych zasob\u00f3w: {0}, {1}, {2}, {3}.

+failedToValidate=Wyst\u0105pi\u0142 b\u0142\u0105d podczas sprawdzania poprawno\u015bci elementu {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Z pami\u0119ci podr\u0119cznej jest zwracana odpowied\u017a na \u017c\u0105danie dotycz\u0105ce zasobu {0}.

+staleResponse=Wyst\u0105pi\u0142 b\u0142\u0105d podczas \u017c\u0105dania zasobu {0}, ale w pami\u0119ci podr\u0119cznej znajdowa\u0142a si\u0119 poprzednia odpowied\u017a. Zwracana jest odpowied\u017a (prawdopodobnie nieaktualna) pochodz\u0105ca z pami\u0119ci podr\u0119cznej.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pt.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pt.properties
new file mode 100644
index 0000000..9f33d4e
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pt.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=A credencial ou o s\u00edmbolo de seguran\u00e7a est\u00e1 incorrecto e n\u00e3o pode ser analisado.

+

+##XmlUtil

+errorParsingXML=Erro ao analisar o XML. Este pode ser ignorado.

+errorParsingExternalGeneralEntities=O processador de XML a ser utilizado ir\u00e1 carregar entidades gerais externas.

+errorParsingExternalParameterEntities=O processador de XML a ser utilizado ir\u00e1 carregar entidades de par\u00e2metros externas.

+errorParsingExternalDTD=O processador de XML a ser utilizado ir\u00e1 carregar as DTDs (Document Type Definitions).

+errorNotUsingSecureXML=O processador de XML a ser utilizado n\u00e3o suporta uma an\u00e1lise segura.

+reuseDocumentBuilders=Os criadores de documentos est\u00e3o a ser reutilizados.

+notReuseDocBuilders=Os criadores de documentos n\u00e3o est\u00e3o a ser reutilizados.

+

+##LruCacheProvider

+LRUCapacity=A capacidade do LRU (Least Recently Used) {0} est\u00e1 configurada para {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed=N\u00e3o \u00e9 poss\u00edvel avaliar {0}.

+

+##JsonContainerConfigLoader

+readingContainerConfig=A configura\u00e7\u00e3o do contentor {0} est\u00e1 a ser lida.

+loadFromString=N\u00e3o \u00e9 poss\u00edvel analisar {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Os recursos de {0} est\u00e3o a ser carregados.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Os ficheiros de {0} est\u00e3o a ser carregados.

+

+##ApiServlet

+apiServletProtocolException=Est\u00e1 a ser devolvido um erro de resposta, uma vez que ocorreu uma excep\u00e7\u00e3o de protocolo.

+apiServletException=Est\u00e1 a ser devolvido um erro de resposta, uma vez que ocorreu uma excep\u00e7\u00e3o de protocolo.

+

+##FeatureRegistry

+overridingFeature=A fun\u00e7\u00e3o {0} com defini\u00e7\u00e3o {1} est\u00e1 a ser substitu\u00edda.

+

+##FeatureResourceLoader

+missingFile=O ficheiro {0} existia, mas agora est\u00e1 em falta.

+unableRetrieveLib=N\u00e3o \u00e9 poss\u00edvel obter a biblioteca remota de {0}.

+

+##BasicHttpFetcher

+timeoutException=O tempo limite de {0} foi excedido, devido \u00e0 seguinte excep\u00e7\u00e3o: {1} - {2} - {3} ms.

+exceptionOccurred=Ocorreu a seguinte excep\u00e7\u00e3o ao obter {0}: decorreram {1} ms.

+slowResponse={0} est\u00e1 a responder lentamente. Decorreram {1} ms.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Ocorreu um erro ao obter a MD5 (Message Digest 5). O erro foi ignorado.

+errorParsingMD5=Ocorreu um erro ao analisar a cadeia da MD5 (Message Digest 5) no formato UTF-8.

+

+##OAuthModule

+usingRandomKey=A chave aleat\u00f3ria para a encripta\u00e7\u00e3o de estado do lado do cliente de OAuth est\u00e1 a ser utilizada.

+usingFile=O ficheiro {0} para a encripta\u00e7\u00e3o de estado do lado do cliente de OAuth est\u00e1 a ser utilizada.

+loadKeyFileFrom=A chave de assinatura de OAuth de {0} est\u00e1 a ser carregada.

+couldNotLoadKeyFile= N\u00e3o foi poss\u00edvel carregar o ficheiro de chave {0}.

+couldNotLoadSignedKey=A chave de assinatura de OAuth n\u00e3o foi correctamente carregada. Para criar uma chave: \n 1. Execute o seguinte comando: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Edite o ficheiro shindig.properties adicionando estas linhas: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=Falha ao inicializar os consumidores de OAuth de {0}.

+

+##OAuthRequest

+oauthFetchFatalError=Ocorreu o seguinte erro fatal quando o OAuth estava a obter conte\u00fado: \n {0}.

+oauthFetchErrorReprompt=Ocorreu o seguinte erro quando o OAuth estava a obter conte\u00fado: \n {0}. A aprova\u00e7\u00e3o est\u00e1 a ser novamente solicitada ao utilizador.

+bogusExpired=O servidor devolveu um limite de validade n\u00e3o v\u00e1lido:\n {0}.

+oauthFetchUnexpectedError=Ocorreu o seguinte erro fatal quando o OAuth estava a obter conte\u00fado: \n {0}.

+unauthenticatedOauth=N\u00e3o \u00e9 poss\u00edvel ao OAuth obter conte\u00fado, uma vez que a autentica\u00e7\u00e3o do utilizador n\u00e3o existe. Ocorreu o seguinte erro: \n {0}.

+invalidOauth=N\u00e3o \u00e9 poss\u00edvel ao OAuth obter conte\u00fado, uma vez que o pedido n\u00e3o \u00e9 v\u00e1lido. Ocorreu o seguinte erro: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=N\u00e3o \u00e9 poss\u00edvel analisar a folha de estilos.

+unableToConvertScript=O n\u00f3 do script n\u00e3o pode ser convertido num controlo de OSML (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Ocorreu um erro inesperado ao pr\u00e9-carregar.

+

+##Processor

+renderBlacklistedGadget=O sistema tentou apresentar o seguinte gadget da lista negra: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=N\u00e3o \u00e9 poss\u00edvel obter {0}.

+failedToRead=N\u00e3o \u00e9 poss\u00edvel ler {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=Ocorreu um erro de HTTP {0} ao obter os m\u00e9todos de servi\u00e7o a partir do ponto final {1}.

+failedToFetchService=N\u00e3o foi poss\u00edvel obter os m\u00e9todos de servi\u00e7os a partir do ponto final {0}. Ocorreu o seguinte erro: {1}.

+failedToParseService=N\u00e3o foi poss\u00edvel analisar os m\u00e9todos de servi\u00e7os a partir do ponto final {0}. Ocorreu o seguinte erro: {1}.

+

+##Renderer

+FailedToRender=O gadget em {0} n\u00e3o foi apresentado. Ocorreu o seguinte erro: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Existe uma ou mais fun\u00e7\u00f5es desconhecidas no seguinte externo &libs=: {0}.

+unexpectedErrorPreloading=Ocorreu um erro inesperado ao pr\u00e9-carregar o gadget.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Foi emitido um pedido para depurar sem um tipo de conte\u00fado para {0}.

+requestToSanitizeUnknownContent=Foi emitido um pedido para depurar sem um tipo de conte\u00fado conhecido {0} para {1}.

+unableToSanitizeUnknownImg=O tipo de imagem {0} \u00e9 desconhecido e n\u00e3o pode ser depurado.

+unableToDetectImgType=N\u00e3o \u00e9 poss\u00edvel detectar o tipo de imagem para {0} ao depurar conte\u00fado.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Ocorreu o seguinte erro de entrada/sa\u00edda ao gravar novamente a imagem {0}: {1}.

+unknownErrorRewritingImg=Ocorreu o seguinte erro ao gravar novamente a imagem {0}: {1}.

+failedToReadImg=A imagem {0} n\u00e3o pode ser lida e vai ser ignorada. Ocorreu o seguinte erro: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Ocorre o seguinte erro ao analisar a CSS de Caja: {0} para {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=N\u00e3o \u00e9 poss\u00edvel processar o recurso de imagem {0}.

+unableToReadResponse=N\u00e3o \u00e9 poss\u00edvel ler a resposta para o recurso de imagem {0}.

+unableToFetchImg=N\u00e3o \u00e9 poss\u00edvel obter o recurso de imagem {0}.

+unableToParseImg=N\u00e3o \u00e9 poss\u00edvel analisar o recurso de imagem {0}.

+

+##MutableContent

+exceptionParsingContent=Ocorreu uma excep\u00e7\u00e3o ao analisar o conte\u00fado do gadget.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=N\u00e3o foi poss\u00edvel analisar o gadget em {0} para pr\u00e9-carregamento.

+

+##ProxyingVisitor

+uriExceptionParsing=Ocorreu uma excep\u00e7\u00e3o de URI (Uniform Resource Identifier) ao analisar o gadget em {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Ocorreram excep\u00e7\u00f5es, devido a bibliotecas de modelos incorrectas.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Foi criada uma cache compilada em Caja.

+retrieveReference=A obter {0}.

+unableToCajole=O gadget em {0} n\u00e3o pode ser compilado em Caja.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Ocorreu o seguinte erro ao solicitar um proxy concatenado: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Um valor de TTL (Time To Live) n\u00e3o v\u00e1lido de {0} foi ignorado.

+

+##HttpRequestHandler

+gadgetCreationError=Ocorreu um erro ao criar o gadget para gravar novamente.  A resposta est\u00e1 a ser gravada novamente sem o gadget.

+

+##ProxyServlet

+embededImgWrongDomain=O pedido para incorporar o URL {0} foi efectuado no dom\u00ednio incorrecto {1}.

+

+##DefaultTemplateProcessor

+elFailure=Ocorreu o seguinte erro de EL para o gadget em {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=O cabe\u00e7alho {0} n\u00e3o \u00e9 permitido e vai ser ignorado. Ocorreu o seguinte erro: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Ocorreu um erro ao actualizar {0}. Foi devolvido o c\u00f3digo de estado {1}. Excep\u00e7\u00e3o: {2}. Em alternativa, est\u00e1 a ser utilizada a vers\u00e3o colocada em cache.

+updateSpecFailureApplyNegCache=Ocorreu um erro ao actualizar {0}. Foi devolvido o c\u00f3digo de estado {1}. Excep\u00e7\u00e3o: {2}. Est\u00e1 a ser aplicada uma cache negativa.

+

+##HashLockedDomainService

+noLockedDomainConfig=N\u00e3o existe uma configura\u00e7\u00e3o de dom\u00ednio bloqueado para {0}.

+

+##Bootstrap

+startingConnManagerWith=O gestor de liga\u00e7\u00f5es com as propriedades {0} est\u00e1 a iniciar.

+

+##XSDValidator

+resolveResource=Os seguintes recursos est\u00e3o a ser processados: {0}, {1}, {2}, {3}.

+failedToValidate=Ocorreu um erro ao validar {0}.

+

+##DefaultRequestPipeline

+cachedResponse=A devolver a resposta colocada em cache para o pedido para {0}.

+staleResponse="Ocorreu um erro ao solicitar o recurso em {0}, mas existe uma resposta anterior na cache. A devolver uma resposta possivelmente obsoleta a partir da cache.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pt_BR.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pt_BR.properties
new file mode 100644
index 0000000..89a8c8a
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_pt_BR.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=O token ou a credencial de seguran\u00e7a est\u00e1 malformado e n\u00e3o pode ser analisado.

+

+##XmlUtil

+errorParsingXML=Erro ao analisar o XML. Isso pode ser ignorado.

+errorParsingExternalGeneralEntities=O processador XML sendo usado carregar\u00e1 entidades gerais externas.

+errorParsingExternalParameterEntities=O processador XML sendo usado carregar\u00e1 entidades de par\u00e2metro externo.

+errorParsingExternalDTD=O processador XML sendo usado carregar\u00e1 Defini\u00e7\u00f5es de Tipo de Documento (DTD).

+errorNotUsingSecureXML=O processador XML sendo usado n\u00e3o suporta an\u00e1lise segura.

+reuseDocumentBuilders=Construtores de documento est\u00e3o sendo reutilizados.

+notReuseDocBuilders=Construtores de documento n\u00e3o est\u00e3o sendo reutilizados.

+

+##LruCacheProvider

+LRUCapacity=A capacidade menos utilizada recentemente (LRU) {0} est\u00e1 configurada para {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} n\u00e3o pode ser avaliado.

+

+##JsonContainerConfigLoader

+readingContainerConfig=A configura\u00e7\u00e3o de cont\u00eainer {0} est\u00e1 sendo lida.

+loadFromString={0} n\u00e3o pode ser analisado.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Recursos de {0} est\u00e3o sendo carregados.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Arquivos de {0} est\u00e3o sendo carregados.

+

+##ApiServlet

+apiServletProtocolException=Um erro de resposta est\u00e1 sendo retornado porque ocorreu uma exce\u00e7\u00e3o de protocolo.

+apiServletException=Um erro de resposta est\u00e1 sendo retornado porque ocorreu uma exce\u00e7\u00e3o de protocolo.

+

+##FeatureRegistry

+overridingFeature=O recurso {0} com a defini\u00e7\u00e3o em {1} est\u00e1 sendo substitu\u00eddo.

+

+##FeatureResourceLoader

+missingFile=O arquivo {0} costumava existir, mas agora est\u00e1 ausente.

+unableRetrieveLib=A biblioteca remota de {0} n\u00e3o pode ser recuperada.

+

+##BasicHttpFetcher

+timeoutException={0} atingiu o tempo limite por causa da seguinte exce\u00e7\u00e3o: {1} - {2} - {3} ms.

+exceptionOccurred=A seguinte exce\u00e7\u00e3o ocorreu ao buscar {0}: {1} ms decorrido.

+slowResponse={0} est\u00e1 respondendo lentamente. {1} ms decorrido.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Ocorreu um erro ao obter Message Digest 5 (MD5). O erro foi ignorado.

+errorParsingMD5=Ocorreu um erro ao analisar a sequ\u00eancia Message Digest 5 (MD5) no formato UTF-8.

+

+##OAuthModule

+usingRandomKey=Uma chave aleat\u00f3ria para criptografia de estado no lado do cliente OAuth est\u00e1 sendo usada.

+usingFile=O arquivo {0} para criptografia de estado no lado do cliente OAuth est\u00e1 sendo usado.

+loadKeyFileFrom=A chave de assinatura OAuth de {0} est\u00e1 sendo carregada.

+couldNotLoadKeyFile= O arquivo-chave {0} n\u00e3o p\u00f4de ser carregado.

+couldNotLoadSignedKey=A chave de assinatura OAuth n\u00e3o foi carregada corretamente. Para criar uma chave: \n 1. Execute o seguinte comando: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Edite o arquivo shindig.properties incluindo estas linhas: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=Falha ao inicializar consumidores OAuth de {0}.

+

+##OAuthRequest

+oauthFetchFatalError=O seguinte erro fatal ocorreu quando OAuth estava buscando conte\u00fado: \n {0}.

+oauthFetchErrorReprompt=O seguinte erro ocorreu quando OAuth esta buscando conte\u00fado: \n {0}. O usu\u00e1rio est\u00e1 sendo avisado novamente quanto \u00e0 aprova\u00e7\u00e3o.

+bogusExpired=O servidor retornou uma expira\u00e7\u00e3o inv\u00e1lida:\n {0}.

+oauthFetchUnexpectedError=O seguinte erro fatal ocorreu quando OAuth estava buscando conte\u00fado: \n {0}.

+unauthenticatedOauth=O OAuth n\u00e3o pode buscar conte\u00fado porque n\u00e3o existe autentica\u00e7\u00e3o do usu\u00e1rio. O seguinte erro ocorreu: \n {0}.

+invalidOauth=O OAuth n\u00e3o pode buscar conte\u00fado porque a solicita\u00e7\u00e3o \u00e9 inv\u00e1lida. O seguinte erro ocorreu: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=A folha de estilo n\u00e3o pode ser analisada.

+unableToConvertScript=O n\u00f3 de script n\u00e3o pode ser convertido em uma identifica\u00e7\u00e3o OpenSocial Markup Language (OSML).

+

+##PipelineExecutor

+errorPreloading=Ocorreu um erro inesperado durante o pr\u00e9-carregamento.

+

+##Processor

+renderBlacklistedGadget=O sistema tentou renderizar o seguinte dispositivo inclu\u00eddo na lista de bloqueio: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} n\u00e3o pode ser recuperado.

+failedToRead={0} n\u00e3o pode ser lido.

+

+##DefaultServiceFetcher

+httpErrorFetching=Ocorreu um erro HTTP {0} ao buscar m\u00e9todos de servi\u00e7o do terminal {1}.

+failedToFetchService=N\u00e3o foi poss\u00edvel buscar m\u00e9todos de servi\u00e7os do terminal {0}. O seguinte erro ocorreu: {1}.

+failedToParseService=N\u00e3o foi poss\u00edvel analisar m\u00e9todos de servi\u00e7os do terminal {0}. O seguinte erro ocorreu: {1}.

+

+##Renderer

+FailedToRender=O dispositivo em {0} n\u00e3o foi renderizado. O seguinte erro ocorreu: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=Um ou mais recursos desconhecidos existem no seguinte extern &libs=: {0}.

+unexpectedErrorPreloading=Ocorreu um erro inesperado durante o pr\u00e9-carregamento do dispositivo.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Uma solicita\u00e7\u00e3o de limpeza foi emitida sem um tipo de conte\u00fado para {0}.

+requestToSanitizeUnknownContent=Uma solicita\u00e7\u00e3o de limpeza foi emitida sem um tipo de conte\u00fado conhecido {0} para {1}.

+unableToSanitizeUnknownImg=O tipo de imagem {0} \u00e9 desconhecido e n\u00e3o pode ser limpo.

+unableToDetectImgType=O tipo de imagem para {0} n\u00e3o pode ser detectado ao limpar conte\u00fado.

+

+##BasicImageRewriter

+ioErrorRewritingImg=O seguinte erro de entrada/sa\u00edda ocorreu ao regravar a imagem {0}: {1}.

+unknownErrorRewritingImg=O seguinte erro ocorreu ao regravar a imagem {0}: {1}.

+failedToReadImg=A imagem {0} n\u00e3o pode ser lida e est\u00e1 sendo ignorada. O seguinte erro ocorreu: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=O seguinte erro ocorreu ao analisar o Caja CSS {0} para {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=O recurso de imagem {0} n\u00e3o pode ser processado.

+unableToReadResponse=A resposta para o recurso de imagem {0} n\u00e3o pode ser lida.

+unableToFetchImg=O recurso de imagem {0} n\u00e3o pode ser buscado.

+unableToParseImg=O recurso de imagem {0} n\u00e3o pode ser analisado.

+

+##MutableContent

+exceptionParsingContent=Ocorreu uma exce\u00e7\u00e3o ao analisar conte\u00fado para o dispositivo.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=O dispositivo em {0} n\u00e3o p\u00f4de ser analisado para pr\u00e9-carregamento.

+

+##ProxyingVisitor

+uriExceptionParsing=Uma exce\u00e7\u00e3o de Identificador Uniforme de Recursos (URI) ocorreu ao analisar o dispositivo em {0}.

+

+##TemplateRewriter

+malformedTemplateLib=Ocorreram exce\u00e7\u00f5es por causa de bibliotecas de modelo malformadas.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Um cache persuadido foi criado.

+retrieveReference={0} est\u00e1 sendo recuperado.

+unableToCajole=O dispositivo em {0} n\u00e3o pode ser persuadido.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Ocorreu o seguinte erro ao solicitar um proxy concatenado: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Um valor de Tempo de Vida (TTL) inv\u00e1lido de {0} foi ignorado.

+

+##HttpRequestHandler

+gadgetCreationError=Ocorreu um erro ao criar o dispositivo para regrava\u00e7\u00e3o.  Regravando a resposta sem o dispositivo.

+

+##ProxyServlet

+embededImgWrongDomain=A solicita\u00e7\u00e3o para integrar a URL {0} foi feita ao dom\u00ednio errado {1}.

+

+##DefaultTemplateProcessor

+elFailure=O seguinte erro de EL ocorreu para o dispositivo em {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=O cabe\u00e7alho {0} \u00e9 ilegal e est\u00e1 sendo ignorado. O seguinte erro ocorreu: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Ocorreu um erro ao atualizar {0}. O c\u00f3digo de status {1} foi retornado. Exce\u00e7\u00e3o: {2}. Uma vers\u00e3o em cache est\u00e1 sendo usada em seu lugar.

+updateSpecFailureApplyNegCache=Ocorreu um erro ao atualizar {0}. O c\u00f3digo de status {1} foi retornado. Exce\u00e7\u00e3o: {2}. Um cache negativo est\u00e1 sendo aplicado.

+

+##HashLockedDomainService

+noLockedDomainConfig=Uma configura\u00e7\u00e3o de dom\u00ednio bloqueado para {0} n\u00e3o existe.

+

+##Bootstrap

+startingConnManagerWith=O gerenciador de configura\u00e7\u00e3o com propriedades {0} est\u00e1 sendo iniciado.

+

+##XSDValidator

+resolveResource=Os recursos a seguir est\u00e3o sendo resolvidos: {0}, {1}, {2}, {3}.

+failedToValidate=Ocorreu um erro ao validar {0}.

+

+##DefaultRequestPipeline

+cachedResponse=Retornando resposta em cache para a solicita\u00e7\u00e3o para {0}.

+staleResponse="Houve um erro ao solicitar o recurso em {0}, mas temos uma resposta anterior no cache. Retornando uma poss\u00edvel resposta antiga do cache.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ru.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ru.properties
new file mode 100644
index 0000000..c30024d
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_ru.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u041c\u0430\u0440\u043a\u0435\u0440 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0438\u043b\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u043e\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0438\u043c\u0435\u0435\u0442 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u043d\u043e.

+

+##XmlUtil

+errorParsingXML=\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0430\u043d\u0430\u043b\u0438\u0437\u0435 XML. \u042d\u0442\u043e \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u0435 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c.

+errorParsingExternalGeneralEntities=\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440 XML \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 \u043e\u0431\u0449\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b.

+errorParsingExternalParameterEntities=\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440 XML \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442 \u0432\u043d\u0435\u0448\u043d\u0438\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430.

+errorParsingExternalDTD=\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440 XML \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442 \u041e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0442\u0438\u043f\u0430 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 (DTD).

+errorNotUsingSecureXML=\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u044b\u0439 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u043e\u0440 XML \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0430\u043d\u0430\u043b\u0438\u0437 \u0437\u0430\u0449\u0438\u0442\u044b.

+reuseDocumentBuilders=\u041a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u0449\u0438\u043a\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f.

+notReuseDocBuilders=\u041a\u043e\u043c\u043f\u043e\u043d\u043e\u0432\u0449\u0438\u043a\u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430 \u043d\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e.

+

+##LruCacheProvider

+LRUCapacity=\u041d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u0434\u0430\u0432\u043d\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0432\u0448\u0430\u044f\u0441\u044f (LRU) \u0432\u043c\u0435\u0441\u0442\u0438\u043c\u043e\u0441\u0442\u044c {0} \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0434\u043b\u044f {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u043e.

+

+##JsonContainerConfigLoader

+readingContainerConfig=\u0427\u0438\u0442\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043a\u043e\u043d\u0442\u0435\u0439\u043d\u0435\u0440\u0430 {0}.

+loadFromString=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432 \u0438\u0437 {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=\u0417\u0430\u0433\u0440\u0443\u0437\u043a\u0430 \u0444\u0430\u0439\u043b\u043e\u0432 \u0438\u0437 {0}.

+

+##ApiServlet

+apiServletProtocolException=\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0435\u043d\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u0432\u0435\u0442\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f \u043f\u0440\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430.

+apiServletException=\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0435\u043d\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043e\u0442\u0432\u0435\u0442\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f \u043f\u0440\u0438 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430.

+

+##FeatureRegistry

+overridingFeature=\u0424\u0443\u043d\u043a\u0446\u0438\u044f {0} \u0441 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u0435\u043c \u0432 {1} \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0430.

+

+##FeatureResourceLoader

+missingFile=\u0424\u0430\u0439\u043b {0} \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043e\u0432\u0430\u043b, \u043d\u043e \u0441\u0435\u0439\u0447\u0430\u0441 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442.

+unableRetrieveLib=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u0432\u043b\u0435\u0447\u044c \u0443\u0434\u0430\u043b\u0435\u043d\u043d\u0443\u044e \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0438\u0437 {0}.

+

+##BasicHttpFetcher

+timeoutException=\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 {0} \u0438\u0437-\u0437\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0438: {1} - {2} - {3} \u043c\u0441.

+exceptionOccurred=\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f \u043f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u043a\u0435 {0}: \u043f\u0440\u043e\u0448\u043b\u043e {1} \u043c\u0441.

+slowResponse={0} \u043e\u0442\u0432\u0435\u0447\u0430\u0435\u0442 \u043c\u0435\u0434\u043b\u0435\u043d\u043d\u043e. \u041f\u0440\u043e\u0448\u043b\u043e {1} \u043c\u0441.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0438 \u041e\u043f\u0438\u0441\u0430\u0442\u0435\u043b\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f 5 (MD5). \u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.

+errorParsingMD5=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0430\u043d\u0430\u043b\u0438\u0437\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u041e\u043f\u0438\u0441\u0430\u0442\u0435\u043b\u044f \u0441\u043e\u043e\u0431\u0449\u0435\u043d\u0438\u044f 5 (MD5) \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UTF-8.

+

+##OAuthModule

+usingRandomKey=\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0434\u043b\u044f \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.

+usingFile=\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0444\u0430\u0439\u043b {0} \u0434\u043b\u044f \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u043d\u0430 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth.

+loadKeyFileFrom=\u0417\u0430\u0433\u0440\u0443\u0436\u0430\u0435\u0442\u0441\u044f \u043a\u043b\u044e\u0447 \u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u0438\u044f OAuth \u0438\u0437 {0}.

+couldNotLoadKeyFile= \u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0444\u0430\u0439\u043b \u043a\u043b\u044e\u0447\u0430 {0}.

+couldNotLoadSignedKey=\u041a\u043b\u044e\u0447 \u043f\u043e\u0434\u043f\u0438\u0441\u0430\u043d\u0438\u044f OAuth \u043d\u0435 \u0431\u044b\u043b \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043d. \u0414\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u043a\u043b\u044e\u0447\u0430: \n 1. \u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u043a\u043e\u043c\u0430\u043d\u0434\u0443: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u0444\u0430\u0439\u043b shindig.properties, \u0434\u043e\u0431\u0430\u0432\u0438\u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0441\u0442\u0440\u043e\u043a\u0438: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0435\u043c\u043d\u0438\u043a\u0438 OAuth \u0438\u0437 {0}.

+

+##OAuthRequest

+oauthFetchFatalError=\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043d\u0435\u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u0438 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0438 OAuth \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e: \n {0}.

+oauthFetchErrorReprompt=\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u0438 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0438 OAuth \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e: \n {0}. \u0423 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043e \u0443\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435.

+bogusExpired=\u0421\u0435\u0440\u0432\u0435\u0440 \u0432\u0435\u0440\u043d\u0443\u043b \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044f \u0441\u0440\u043e\u043a\u0430:\n {0}.

+oauthFetchUnexpectedError=\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043d\u0435\u0443\u0441\u0442\u0440\u0430\u043d\u0438\u043c\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u0438 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0438 OAuth \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e: \n {0}.

+unauthenticatedOauth=OAuth \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0438\u0437\u0432\u043b\u0435\u0447\u044c \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0434\u0430\u043d\u043d\u044b\u0445 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442. \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \n {0}.

+invalidOauth=OAuth \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0438\u0437\u0432\u043b\u0435\u0447\u044c \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435, \u0442\u0430\u043a \u043a\u0430\u043a \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0435\u0432\u0435\u0440\u0435\u043d. \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0430\u0431\u043b\u0438\u0446\u0443 \u0441\u0442\u0438\u043b\u0435\u0439.

+unableToConvertScript=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0443\u0437\u0435\u043b \u0441\u0446\u0435\u043d\u0430\u0440\u0438\u044f \u0432 \u0442\u0435\u0433 OpenSocial Markup Language (OSML).

+

+##PipelineExecutor

+errorPreloading=\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.

+

+##Processor

+renderBlacklistedGadget=\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u044b\u0442\u0430\u043b\u0430\u0441\u044c \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u0433\u0430\u0434\u0436\u0435\u0442 \u0438\u0437 \u0447\u0435\u0440\u043d\u043e\u0433\u043e \u0441\u043f\u0438\u0441\u043a\u0430: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u0432\u043b\u0435\u0447\u044c {0}.

+failedToRead=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 HTTP {0} \u043f\u0440\u0438 \u0432\u044b\u0431\u043e\u0440\u043a\u0435 \u043c\u0435\u0442\u043e\u0434\u043e\u0432 \u0441\u043b\u0443\u0436\u0431\u044b \u0438\u0437 \u043a\u043e\u043d\u0435\u0447\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438 {1}.

+failedToFetchService=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u0432\u043b\u0435\u0447\u044c \u043c\u0435\u0442\u043e\u0434\u044b \u0441\u043b\u0443\u0436\u0431\u044b \u0438\u0437 \u043a\u043e\u043d\u0435\u0447\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438 {0}. \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: {1}.

+failedToParseService=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0442\u043e\u0434\u044b \u0441\u043b\u0443\u0436\u0431\u044b \u0438\u0437 \u043a\u043e\u043d\u0435\u0447\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438 {0}. \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: {1}.

+

+##Renderer

+FailedToRender=\u0413\u0430\u0434\u0436\u0435\u0442 \u0432 {0} \u043d\u0435 \u0432\u044b\u0432\u0435\u0434\u0435\u043d. \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=\u041e\u0434\u043d\u0430 \u0438\u043b\u0438 \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u044b\u0445 \u0444\u0443\u043d\u043a\u0446\u0438\u0439 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0442 \u0432 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0445 \u0432\u043d\u0435\u0448\u043d\u0438\u0445 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u0445: {0}.

+unexpectedErrorPreloading=\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0433\u0430\u0434\u0436\u0435\u0442\u0430 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u0417\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0431\u0435\u0437 \u0442\u0438\u043f\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0434\u043b\u044f {0}.

+requestToSanitizeUnknownContent=\u0417\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d \u0431\u0435\u0437 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e {0} \u0434\u043b\u044f {1}.

+unableToSanitizeUnknownImg=\u0422\u0438\u043f \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f {0} \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d \u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d\u0430.

+unableToDetectImgType=\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0442\u0438\u043f \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0434\u043b\u044f {0} \u043f\u0440\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u0438\u0437 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e.

+

+##BasicImageRewriter

+ioErrorRewritingImg=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u0432\u043e\u0434\u0430/\u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f {0}: {1}.

+unknownErrorRewritingImg=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f {0}: {1}.

+failedToReadImg=\u0418\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u0435 {0} \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u0438 \u043e\u043d\u043e \u0431\u044b\u043b\u043e \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d\u043e. \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0430\u043d\u0430\u043b\u0438\u0437\u0435 Caja CSS: {0} \u0434\u043b\u044f {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f {0}.

+unableToReadResponse=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0447\u0438\u0442\u0430\u0442\u044c \u043e\u0442\u0432\u0435\u0442 \u0434\u043b\u044f \u0440\u0435\u0441\u0443\u0440\u0441\u0430 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f {0}.

+unableToFetchImg=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0438\u0437\u0432\u043b\u0435\u0447\u044c \u0440\u0435\u0441\u0443\u0440\u0441 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f {0}.

+unableToParseImg=\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441 \u0438\u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f {0}.

+

+##MutableContent

+exceptionParsingContent=\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f \u043f\u0440\u0438 \u0430\u043d\u0430\u043b\u0438\u0437\u0435 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0433\u043e \u0433\u0430\u0434\u0436\u0435\u0442\u0430.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=\u0413\u0430\u0434\u0436\u0435\u0442 \u0432 {0} \u043d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u0440\u043e\u0430\u043d\u0430\u043b\u0438\u0437\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043b\u044f \u043f\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0439 \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0438.

+

+##ProxyingVisitor

+uriExceptionParsing=\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f \u0423\u043d\u0438\u0432\u0435\u0440\u0441\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u0430 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 (URI) \u043f\u0440\u0438 \u0430\u043d\u0430\u043b\u0438\u0437\u0435 \u0433\u0430\u0434\u0436\u0435\u0442\u0430 \u0432 {0}.

+

+##TemplateRewriter

+malformedTemplateLib=\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0438 \u0438\u0437-\u0437\u0430 \u043d\u0435\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a \u0448\u0430\u0431\u043b\u043e\u043d\u043e\u0432.

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u0421\u043e\u0437\u0434\u0430\u043d \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u043a\u044d\u0448.

+retrieveReference=\u0418\u0437\u0432\u043b\u0435\u0447\u0435\u043d {0}.

+unableToCajole=\u0413\u0430\u0434\u0436\u0435\u0442 {0} \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u043c.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u0421\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u0438 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u043a\u0441\u0438: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0412\u0440\u0435\u043c\u0435\u043d\u0438 \u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0432 \u043a\u044d\u0448\u0435 (TTL) \u0434\u043b\u044f {0} \u043f\u0440\u043e\u0438\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e.

+

+##HttpRequestHandler

+gadgetCreationError=\u041e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0438. \u041f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u044c \u043e\u0442\u0432\u0435\u0442\u0430 \u0431\u0435\u0437 \u0433\u0430\u0434\u0436\u0435\u0442\u0430.

+

+##ProxyServlet

+embededImgWrongDomain=\u0417\u0430\u043f\u0440\u043e\u0441 \u043d\u0430 \u0432\u0441\u0442\u0430\u0432\u043a\u0443 URL {0} \u043f\u043e\u0441\u043b\u0430\u043d \u043d\u0430 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0434\u043e\u043c\u0435\u043d {1}.

+

+##DefaultTemplateProcessor

+elFailure=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 EL \u0434\u043b\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430 \u0432 {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043e\u043a {0} \u043d\u0435\u0432\u0435\u0440\u0435\u043d \u0438 \u0431\u044b\u043b \u043f\u0440\u043e\u043f\u0443\u0449\u0435\u043d. \u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 {0}. \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0435\u043d \u043a\u043e\u0434 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f {1}. \u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f: {2}. \u0412\u043c\u0435\u0441\u0442\u043e \u044d\u0442\u043e\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f \u0438\u0437 \u043a\u044d\u0448\u0430.

+updateSpecFailureApplyNegCache=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0438 {0}. \u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0435\u043d \u043a\u043e\u0434 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f {1}. \u0418\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u0435\u043b\u044c\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f: {2}. \u041f\u0440\u0438\u043c\u0435\u043d\u0435\u043d \u043d\u0435\u0433\u0430\u0442\u0438\u0432\u043d\u044b\u0439 \u043a\u044d\u0448.

+

+##HashLockedDomainService

+noLockedDomainConfig=\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u043e\u043c\u0435\u043d\u0430 \u0434\u043b\u044f {0} \u043d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.

+

+##Bootstrap

+startingConnManagerWith=\u0417\u0430\u043f\u0443\u0449\u0435\u043d \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0439 \u0441\u043e \u0441\u0432\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 {0}.

+

+##XSDValidator

+resolveResource=\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u044b \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044b: {0}, {1}, {2}, {3}.

+failedToValidate=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0435 {0}.

+

+##DefaultRequestPipeline

+cachedResponse=\u0412\u043e\u0437\u0432\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u043a\u044d\u0448\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043d\u0430 \u0437\u0430\u043f\u0440\u043e\u0441 \u043a {0}.

+staleResponse="\u041e\u0448\u0438\u0431\u043a\u0430 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 \u0432 {0}, \u043d\u043e \u043f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0438\u0439 \u043e\u0442\u0432\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d \u0432 \u043a\u044d\u0448\u0435.  \u0411\u0443\u0434\u0435\u0442 \u0432\u043e\u0437\u0432\u0440\u0430\u0449\u0435\u043d \u043e\u0442\u0432\u0435\u0442 \u0438\u0437 \u043a\u044d\u0448\u0430 (\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e, \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0439).

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_sl.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_sl.properties
new file mode 100644
index 0000000..2e048bc
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_sl.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Varnostni \u017eeton ali poverilnica ni pravilno oblikovana in je ni mogo\u010de raz\u010dleniti.

+

+##XmlUtil

+errorParsingXML=Napaka pri raz\u010dlenjevanju XML-a. To lahko prezrete.

+errorParsingExternalGeneralEntities=Procesor XML, ki ga uporabljate, bo nalo\u017eil zunanje splo\u0161ne vnose.

+errorParsingExternalParameterEntities=Procesor XML, ki ga uporabljate, bo nalo\u017eil zunanje parametrske vnose.

+errorParsingExternalDTD=Procesor XML, ki ga uporabljate, bo nalo\u017eil definicije tipov dokumentov (DTD).

+errorNotUsingSecureXML=Procesor XML, ki ga uporabljate, ne podpira varnega raz\u010dlenjevanja.

+reuseDocumentBuilders=Graditelji dokumentov se znova uporabljajo.

+notReuseDocBuilders=Graditelji dokumentov se ne uporabljajo znova.

+

+##LruCacheProvider

+LRUCapacity=Najmanj uporabljana (LRU) kapaciteta {0} je konfigurirana za {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} ni mogo\u010de ovrednotiti.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Poteka branje konfiguracije vsebnika {0}.

+loadFromString={0} ni mogo\u010de raz\u010dleniti.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Poteka nalaganje virov iz {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Poteka nalaganje datotek iz {0}.

+

+##ApiServlet

+apiServletProtocolException=Vrnjena je bila napaka v odgovoru, ker je pri\u0161lo do izjemnega stanja protokola.

+apiServletException=Vrnjena je bila napaka v odgovoru, ker je pri\u0161lo do izjemnega stanja protokola.

+

+##FeatureRegistry

+overridingFeature=Poteka prepisovanje funkcije {0} z definicijo v {1}.

+

+##FeatureResourceLoader

+missingFile=Datoteka {0} je obstajala, zdaj pa manjka.

+unableRetrieveLib=Oddaljene knji\u017enice iz {0} ni mogo\u010de priklicati.

+

+##BasicHttpFetcher

+timeoutException={0} se je za\u010dasno prekinil zaradi naslednjega izjemnega stanja: {1} - {2} - {3} ms.

+exceptionOccurred=Med pridobivanjem {0} je pri\u0161lo do izjemnega stanja: poteklo je {1} ms.

+slowResponse={0} se odziva po\u010dasi. Poteklo je {1} ms.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Med pridobivanjem izvle\u010dka sporo\u010dila 5 (MD5) je pri\u0161lo do napake. Napaka je bila prezrta.

+errorParsingMD5=Med raz\u010dlenjevanjem niza izvle\u010dka sporo\u010dila 5 (MD5) v formatu UTF-8 je pri\u0161lo do napake.

+

+##OAuthModule

+usingRandomKey=Uporabljen je naklju\u010dni klju\u010d za \u0161ifriranje stanja na strani odjemalca OAuth.

+usingFile=Uporabljena je datoteka {0} za \u0161ifriranje stanja na strani odjemalca OAuth.

+loadKeyFileFrom=Poteka nalaganje klju\u010da za prijavo OAuth iz {0}.

+couldNotLoadKeyFile= Datoteke klju\u010dev {0} ni bilo mogo\u010de nalo\u017eiti.

+couldNotLoadSignedKey=Klju\u010d za prijavo OAuth se ni pravilno nalo\u017eil. Postopek za ustvarjanje klju\u010da: \n 1. Za\u017eenite naslednji ukaz: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Uredite datoteko shindig.properties, tako da dodate te vrstice: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=Porabnikov OAuth iz {0} ni bilo mogo\u010de inicializirati.

+

+##OAuthRequest

+oauthFetchFatalError=Ko je OAuth pridobival vsebino, je pri\u0161lo do naslednje usodne napake: \n {0}.

+oauthFetchErrorReprompt=Ko je OAuth pridobival vsebino, je pri\u0161lo do naslednje napake: \n {0}. Uporabniku je znova prikazan poziv za odobritev.

+bogusExpired=Stre\u017enik je vrnil neveljaven potek:\n \ {0}.

+oauthFetchUnexpectedError=Ko je OAuth pridobival vsebino, je pri\u0161lo do naslednje usodne napake: \n {0}.

+unauthenticatedOauth=OAuth ne more pridobiti vsebine, ker overjanje uporabnika ne obstaja. Zgodila se je naslednja napaka: \n {0}.

+invalidOauth=OAuth ne more pridobiti vsebine, ker zahteva ni veljavna. Zgodila se je naslednja napaka: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=Slogovnega lista ni mogo\u010de raz\u010dleniti.

+unableToConvertScript=Vozli\u0161\u010da skripta ni mogo\u010de pretvoriti v oznako OSML (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Med vnaprej\u0161njim nalaganjem se je zgodila nepri\u010dakovana napaka.

+

+##Processor

+renderBlacklistedGadget=Sistem je poskusil upodobiti naslednji pripomo\u010dek s \u010drne liste: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} ni mogo\u010de priklicati.

+failedToRead={0} ni mogo\u010de prebrati.

+

+##DefaultServiceFetcher

+httpErrorFetching=Napaka HTTP {0} med pridobivanjem storitvenih na\u010dinov iz zaklju\u010dne to\u010dke {1}.

+failedToFetchService=Storitvenih na\u010dinov iz zaklju\u010dne to\u010dke {0} ni bilo mogo\u010de pridobiti. Zgodila se je naslednja napaka: {1}.

+failedToParseService=Storitvenih na\u010dinov iz zaklju\u010dne to\u010dke {0} ni bilo mogo\u010de raz\u010dleniti. Zgodila se je naslednja napaka: {1}.

+

+##Renderer

+FailedToRender=Pripomo\u010dek v {0} ni bil upodobljen. Zgodila se je naslednja napaka: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=V naslednjih zunanjih &knji\u017enicah obstaja ena ali ve\u010d neznanih komponent=: {0}.

+unexpectedErrorPreloading=Med vnaprej\u0161njim nalaganjem pripomo\u010dka se je zgodila nepri\u010dakovana napaka.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=Zahteva za saniranje je bila izdana brez vrste vsebine za {0}.

+requestToSanitizeUnknownContent=Zahteva za saniranje je bila izdana brez znane vrste vsebine {0} za {1}.

+unableToSanitizeUnknownImg=Vrsta slike {0} ni znana in je ni mogo\u010de sanirati.

+unableToDetectImgType=Pri saniranju vsebine ni mogo\u010de odkriti vrste slike za {0}.

+

+##BasicImageRewriter

+ioErrorRewritingImg=Med vnovi\u010dnim zapisovanjem slike {0} se je zgodila naslednja napaka vhoda/izhoda: {1}.

+unknownErrorRewritingImg=Med vnovi\u010dnim zapisovanjem slike {0} se je zgodila naslednja napaka: {1}.

+failedToReadImg=Slike {0} ni mogo\u010de prebrati in bo presko\u010dena. Zgodila se je naslednja napaka: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Med raz\u010dlenjevanjem CSS-ja Caja se je zgodila naslednja napaka: {0} za {1}.

+

+##ImageAttributeRewriter

+unableToProcessImg=Vira slike {0} ni mogo\u010de obdelati.

+unableToReadResponse=Odgovora za vir slike {0} ni mogo\u010de prebrati.

+unableToFetchImg=Vira slike {0} ni mogo\u010de pridobiti.

+unableToParseImg=Vira slike {0} ni mogo\u010de raz\u010dleniti.

+

+##MutableContent

+exceptionParsingContent=Med raz\u010dlenjevanjem vsebine za pripomo\u010dek je pri\u0161lo do nepri\u010dakovanega izjemnega stanja.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Pripomo\u010dka v {0} ni bilo mogo\u010de raz\u010dleniti za vnaprej\u0161nje nalaganje.

+

+##ProxyingVisitor

+uriExceptionParsing=Pri raz\u010dlenjevanju pripomo\u010dka v {0} je pri\u0161lo do izjemnega stanja URI-ja.

+

+##TemplateRewriter

+malformedTemplateLib=Zaradi napa\u010dno oblikovanih knji\u017enic predlog je pri\u0161lo do izjemnih stanj.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Ustvarjen je bil nepravi predpomnilnik.

+retrieveReference=Poteka pridobivanje {0}.

+unableToCajole=Pripomo\u010dka v {0} ni mogo\u010de pretentati.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Med zahtevanjem veri\u017enega proxyja se je zgodila naslednja napaka: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Neveljavna vrednost TTL (Time To Live - \u010das \u017eivljenja) {0} je bila prezrta.

+

+##HttpRequestHandler

+gadgetCreationError=Med ustvarjanjem gradnika za vnovi\u010dno pisanje je pri\u0161lo do napake. Vnovi\u010dno pisanje odgovora brez gradnika.

+

+##ProxyServlet

+embededImgWrongDomain=Zahteva za vdelavo URL-ja {0} je bila izdana za napa\u010dno domeno {1}.

+

+##DefaultTemplateProcessor

+elFailure=Za pripomo\u010dek v {0} se je zgodila naslednja napaka: {1}.

+

+##UriUtils

+skipIllegalHeader=Glava {0} ni veljavna in bo presko\u010dena. Zgodila se je naslednja napaka: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Med posodabljanjem {0} se je zgodila napaka. Vrnjena je bila statusna koda {1}. Izjemno stanje: {2}. Uporabljena bo predpomnjena razli\u010dica.

+updateSpecFailureApplyNegCache=Med posodabljanjem {0} se je zgodila napaka. Vrnjena je bila statusna koda {1}. Izjemno stanje: {2}. Uporabljen je negativni predpomnilnik.

+

+##HashLockedDomainService

+noLockedDomainConfig=Konfiguracija zaklenjene domene za {0} ne obstaja.

+

+##Bootstrap

+startingConnManagerWith=Upravitelj povezav z lastnostmi {0} se zaganja.

+

+##XSDValidator

+resolveResource=Poteka razre\u0161evanje naslednjih virov: {0}, {1}, {2}, {3}.

+failedToValidate=Med preverjanjem {0} se je zgodila napaka.

+

+##DefaultRequestPipeline

+cachedResponse=Vra\u010danje predpomnjenega odgovora za zahtevo v {0}.

+staleResponse="Pri zahtevanju vira v lokaciji {0} je pri\u0161lo od napake, vendar je predpomnilniku prej\u0161nji odgovor. Vra\u010danje potencialno zastarelega odgovora iz pomnilnika.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_sv.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_sv.properties
new file mode 100644
index 0000000..27a4a19
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_sv.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=Det gick inte att tolka s\u00e4kerhetselementet eller anv\u00e4ndarinformationen. S\u00e4kerhetselementet eller anv\u00e4ndarinformationen \u00e4r felformaterat/felformaterad.

+

+##XmlUtil

+errorParsingXML=Det uppstod ett fel n\u00e4r XML-koden skulle tolkas. Du kan ignorera det h\u00e4r.

+errorParsingExternalGeneralEntities=Externa allm\u00e4nna entiteter kommer att l\u00e4sas in med hj\u00e4lp av den XML-bearbetningsfunktion som anv\u00e4nds.

+errorParsingExternalParameterEntities=Externa parameterentiteter kommer att l\u00e4sas in med hj\u00e4lp av den XML-bearbetningsfunktion som anv\u00e4nds.

+errorParsingExternalDTD=DTD:er (Document Type Definition) kommer att l\u00e4sas in med hj\u00e4lp av den XML-bearbetningsfunktion som anv\u00e4nds.

+errorNotUsingSecureXML=Det finns inga funktioner f\u00f6r s\u00e4ker tolkning i den XML-bearbetningsfunktion som anv\u00e4nds.

+reuseDocumentBuilders=Dokumentbyggfunktionerna \u00e5teranv\u00e4nds.

+notReuseDocBuilders=Dokumentbyggfunktionerna \u00e5teranv\u00e4nds inte.

+

+##LruCacheProvider

+LRUCapacity=Den senast anv\u00e4nda kapaciteten, {0}, \u00e4r konfigurerad f\u00f6r {1}.

+

+##DynamicConfigProperty

+evalExpressionFailed=Det gick inte att utv\u00e4rdera {0}.

+

+##JsonContainerConfigLoader

+readingContainerConfig=Beh\u00e5llarkonfigurationen {0} l\u00e4ses.

+loadFromString=Det gick inte att tolka {0}.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=Resurserna fr\u00e5n {0} l\u00e4ses in.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=Filerna fr\u00e5n {0} l\u00e4ses in.

+

+##ApiServlet

+apiServletProtocolException=Det uppstod ett protokollundantag. Ett svarsfel returneras.

+apiServletException=Det uppstod ett protokollundantag. Ett svarsfel returneras.

+

+##FeatureRegistry

+overridingFeature=Funktionen {0} med definitionen p\u00e5 {1} \u00e5sidos\u00e4tts.

+

+##FeatureResourceLoader

+missingFile=Filen {0} finns inte l\u00e4ngre.

+unableRetrieveLib=Det gick inte att h\u00e4mta fj\u00e4rrbiblioteket fr\u00e5n {0}.

+

+##BasicHttpFetcher

+timeoutException=Tidsgr\u00e4nsen f\u00f6r {0} \u00f6verskreds p\u00e5 grund av f\u00f6ljande undantag: {1} - {2} - {3} ms.

+exceptionOccurred=F\u00f6ljande undantag uppstod n\u00e4r {0} skulle h\u00e4mtas: anv\u00e4nd tid: {1} ms.

+slowResponse={0} svarar l\u00e5ngsamt. Anv\u00e4nd tid: {1} ms.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=Det uppstod ett fel n\u00e4r MD5-str\u00e4ngen (Message Digest 5) skulle h\u00e4mtas. Felet ignoreras.

+errorParsingMD5=Det uppstod ett fel n\u00e4r MD5-str\u00e4ngen (Message Digest 5) i UTF-8-format skulle tolkas.

+

+##OAuthModule

+usingRandomKey=En slumpm\u00e4ssig nyckel f\u00f6r OAuth-klientl\u00e4geskryptering anv\u00e4nds.

+usingFile=Filen {0} f\u00f6r OAuth-klientl\u00e4geskryptering anv\u00e4nds.

+loadKeyFileFrom=OAuth-signeringsnyckeln fr\u00e5n {0} l\u00e4ses in.

+couldNotLoadKeyFile= Det gick inte att l\u00e4sa in nyckelfilen {0}.

+couldNotLoadSignedKey=Det gick inte att l\u00e4sa in OAuth-signeringsnyckeln p\u00e5 r\u00e4tt s\u00e4tt. S\u00e5 h\u00e4r skapar du en nyckel: \n 1. K\u00f6r f\u00f6ljande kommando: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testnyckel.pem \\\n -out testnyckel.pem -subj ''/CN=mintestnyckel''\n openssl pkcs8 -in testnyckel.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. Redigera filen shindig.properties genom att l\u00e4gga till f\u00f6ljande rader: \n{0} =<s\u00f6kv\u00e4g-till-OAuth-nyckeln.pem>\n {1} =minnyckel \n.

+failedToInit=Det gick inte att initiera OAuth-konsumenterna fr\u00e5n {0}.

+

+##OAuthRequest

+oauthFetchFatalError=F\u00f6ljande allvarliga fel uppstod n\u00e4r OAuth-inneh\u00e5ll skulle h\u00e4mtas: \n {0}.

+oauthFetchErrorReprompt=F\u00f6ljande allvarliga fel uppstod n\u00e4r OAuth-inneh\u00e5ll skulle h\u00e4mtas: \n {0}. Ett nytt meddelande om godk\u00e4nnande visas f\u00f6r anv\u00e4ndaren.

+bogusExpired=En ogiltig tidsgr\u00e4ns returnerades fr\u00e5n servern: \n {0}.

+oauthFetchUnexpectedError=F\u00f6ljande allvarliga fel uppstod n\u00e4r OAuth-inneh\u00e5ll skulle h\u00e4mtas: \n {0}.

+unauthenticatedOauth=Det gick inte att h\u00e4mta OAuth-inneh\u00e5ll eftersom anv\u00e4ndarautentiseringen inte finns. F\u00f6ljande fel uppstod: \n {0}.

+invalidOauth=Det gick inte att h\u00e4mta OAuth-inneh\u00e5ll eftersom beg\u00e4ran \u00e4r ogiltig. F\u00f6ljande fel uppstod: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=Det gick inte att tolka formatmallen.

+unableToConvertScript=Det gick inte att konvertera skriptnoden till ett OSML-m\u00e4rkord (OpenSocial Markup Language).

+

+##PipelineExecutor

+errorPreloading=Det uppstod ett ov\u00e4ntat fel vid f\u00f6rinl\u00e4sningen.

+

+##Processor

+renderBlacklistedGadget=Ett f\u00f6rs\u00f6k att \u00e5terge f\u00f6ljande svartlistade gadgetprogram utf\u00f6rdes: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=Det gick inte att h\u00e4mta {0}.

+failedToRead=Det gick inte att l\u00e4sa {0}.

+

+##DefaultServiceFetcher

+httpErrorFetching=Det uppstod ett HTTP {0}-fel n\u00e4r tj\u00e4nstemetoderna skulle h\u00e4mtas fr\u00e5n {1}-slutpunkten.

+failedToFetchService=Det gick inte att h\u00e4mta tj\u00e4nstemetoderna fr\u00e5n {0}-slutpunkten. F\u00f6ljande fel uppstod: {1}.

+failedToParseService=Det gick inte att tolka tj\u00e4nstemetoderna fr\u00e5n {0}-slutpunkten. F\u00f6ljande fel uppstod: {1}.

+

+##Renderer

+FailedToRender=Det gick inte att \u00e5terge gadgetprogrammet p\u00e5 {0}. F\u00f6ljande fel uppstod: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=En eller flera ok\u00e4nda funktioner finns i f\u00f6ljande externa bibliotek: {0}.

+unexpectedErrorPreloading=Det uppstod ett ov\u00e4ntat fel n\u00e4r gadgetprogrammet skulle f\u00f6rinl\u00e4sas.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=En beg\u00e4ran om att rensa utf\u00e4rdades utan n\u00e5gon inneh\u00e5llstyp f\u00f6r {0}.

+requestToSanitizeUnknownContent=En beg\u00e4ran om att rensa utf\u00e4rdades utan n\u00e5gon k\u00e4nd inneh\u00e5llstyp, {0}, f\u00f6r {1}.

+unableToSanitizeUnknownImg=Bildtypen {0} \u00e4r ok\u00e4nd. Det g\u00e5r inte att rensa den.

+unableToDetectImgType=Det gick inte att k\u00e4nna igen bildtypen f\u00f6r {0} vid rensning av inneh\u00e5ll.

+

+##BasicImageRewriter

+ioErrorRewritingImg=F\u00f6ljande I/O-fel uppstod n\u00e4r bilden {0} skulle skrivas: {1}.

+unknownErrorRewritingImg=F\u00f6ljande fel uppstod n\u00e4r bilden {0} skulle skrivas: {1}.

+failedToReadImg=Det gick inte att l\u00e4sa bilden {0}. Bilden hoppas \u00f6ver. F\u00f6ljande fel uppstod: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=F\u00f6ljande fel uppstod n\u00e4r Caja-CSS-formatmallen {0} f\u00f6r {1} skulle tolkas.

+

+##ImageAttributeRewriter

+unableToProcessImg=Det gick inte att bearbeta bildresursen {0}.

+unableToReadResponse=Det gick inte att l\u00e4sa svaret f\u00f6r bildresursen {0}.

+unableToFetchImg=Det gick inte att h\u00e4mta bildresursen {0}.

+unableToParseImg=Det gick inte att tolka bildresursen {0}.

+

+##MutableContent

+exceptionParsingContent=Det uppstod ett undantag n\u00e4r inneh\u00e5llet f\u00f6r gadgetprogrammet skulle tolkas.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=Det gick inte att tolka gadgetprogrammet p\u00e5 {0} f\u00f6r f\u00f6rinl\u00e4sning.

+

+##ProxyingVisitor

+uriExceptionParsing=Det uppstod ett URI-adressundantag (Uniform Resource Identifier) n\u00e4r gadgetprogrammet p\u00e5 {0} skulle tolkas.

+

+##TemplateRewriter

+malformedTemplateLib=Det uppstod undantag p\u00e5 grund av felformaterade mallbibliotek.

+

+##CajaContnetRewriter

+cajoledCacheCreated=En kompilerad cache skapades.

+retrieveReference={0} h\u00e4mtas.

+unableToCajole=Det gick inte att kompilera gadgetprogrammet p\u00e5 {0}.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=F\u00f6ljande fel uppstod n\u00e4r en konkatenerad proxy skulle beg\u00e4ras: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Det ogiltiga TTL-v\u00e4rdet (Time To Live) {0} ignorerades.

+

+##HttpRequestHandler

+gadgetCreationError=Det uppstod ett fel n\u00e4r gadgetprogrammet f\u00f6r omskrivning skulle skapas. Svaret skrivs om utan gadgetprogrammet.

+

+##ProxyServlet

+embededImgWrongDomain=Beg\u00e4ran om att b\u00e4dda in URL-adressen {0} gjordes till fel dom\u00e4n, {1}.

+

+##DefaultTemplateProcessor

+elFailure=F\u00f6ljande EL-fel uppstod f\u00f6r gadgetprogrammet p\u00e5 {0}: {1}.

+

+##UriUtils

+skipIllegalHeader=Huvudet {0} \u00e4r ogiltigt. Huvudet hoppas \u00f6ver. F\u00f6ljande fel uppstod: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=Det uppstod ett fel n\u00e4r {0} skulle uppdateras. Statusen {1} returnerades. Undantag: {2}. En cachad version anv\u00e4nds i st\u00e4llet.

+updateSpecFailureApplyNegCache=Det uppstod ett fel n\u00e4r {0} skulle uppdateras. Statusen {1} returnerades. Undantag: {2}. En negative cache anv\u00e4nds.

+

+##HashLockedDomainService

+noLockedDomainConfig=Det finns ingen l\u00e5st dom\u00e4nkonfiguration f\u00f6r {0}.

+

+##Bootstrap

+startingConnManagerWith=Anslutningshanteraren med {0}-egenskaper startas.

+

+##XSDValidator

+resolveResource=F\u00f6ljande resurser l\u00f6ses ut: {0}, {1}, {2} och {3}.

+failedToValidate=Det uppstod ett fel n\u00e4r {0} skulle valideras.

+

+##DefaultRequestPipeline

+cachedResponse=Det cachade svaret p\u00e5 beg\u00e4ran till {0} returneras.

+staleResponse="Det uppstod ett fel n\u00e4r resursen p\u00e5 {0} skulle beg\u00e4ras men det finns ett tidigare svar i cachen. Ett svar som eventuellt \u00e4r inaktuellt returneras fr\u00e5n cachen.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_th.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_th.properties
new file mode 100644
index 0000000..6bc1152
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_th.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e04\u0e27\u0e32\u0e21\u0e1b\u0e25\u0e2d\u0e14\u0e20\u0e31\u0e22\u0e2b\u0e23\u0e37\u0e2d\u0e2b\u0e19\u0e31\u0e07\u0e2a\u0e37\u0e2d\u0e23\u0e31\u0e1a\u0e23\u0e2d\u0e07\u0e1c\u0e34\u0e14\u0e23\u0e39\u0e1b\u0e41\u0e1a\u0e1a\u0e41\u0e25\u0e30\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e44\u0e14\u0e49

+

+##XmlUtil

+errorParsingXML=\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e43\u0e19\u0e01\u0e32\u0e23\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c XML \u0e0b\u0e36\u0e48\u0e07\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e25\u0e30\u0e40\u0e27\u0e49\u0e19\u0e44\u0e14\u0e49

+errorParsingExternalGeneralEntities=\u0e15\u0e31\u0e27\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25 XML \u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e43\u0e0a\u0e49\u0e08\u0e30\u0e42\u0e2b\u0e25\u0e14\u0e40\u0e2d\u0e19\u0e17\u0e34\u0e15\u0e35\u0e17\u0e31\u0e48\u0e27\u0e44\u0e1b\u0e20\u0e32\u0e22\u0e19\u0e2d\u0e01

+errorParsingExternalParameterEntities=\u0e15\u0e31\u0e27\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25 XML \u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e43\u0e0a\u0e49\u0e08\u0e30\u0e42\u0e2b\u0e25\u0e14\u0e40\u0e2d\u0e19\u0e17\u0e34\u0e15\u0e35\u0e1e\u0e32\u0e23\u0e32\u0e21\u0e34\u0e40\u0e15\u0e2d\u0e23\u0e4c\u0e20\u0e32\u0e22\u0e19\u0e2d\u0e01

+errorParsingExternalDTD=\u0e15\u0e31\u0e27\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25 XML \u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e43\u0e0a\u0e49\u0e08\u0e30\u0e42\u0e2b\u0e25\u0e14 Document Type Definitions (DTD)

+errorNotUsingSecureXML=\u0e15\u0e31\u0e27\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25 XML \u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e43\u0e0a\u0e49\u0e08\u0e30\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e2a\u0e19\u0e31\u0e1a\u0e2a\u0e19\u0e38\u0e19\u0e43\u0e19\u0e01\u0e32\u0e23\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e27\u0e32\u0e21\u0e1b\u0e25\u0e2d\u0e14\u0e20\u0e31\u0e22

+reuseDocumentBuilders=\u0e15\u0e31\u0e27\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e40\u0e2d\u0e01\u0e2a\u0e32\u0e23\u0e16\u0e39\u0e01\u0e19\u0e33\u0e01\u0e25\u0e31\u0e1a\u0e21\u0e32\u0e43\u0e0a\u0e49

+notReuseDocBuilders=\u0e15\u0e31\u0e27\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e40\u0e2d\u0e01\u0e2a\u0e32\u0e23\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e16\u0e39\u0e01\u0e19\u0e33\u0e01\u0e25\u0e31\u0e1a\u0e21\u0e32\u0e43\u0e0a\u0e49

+

+##LruCacheProvider

+LRUCapacity=\u0e04\u0e27\u0e32\u0e21\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e17\u0e35\u0e48\u0e43\u0e0a\u0e49\u0e19\u0e49\u0e2d\u0e22\u0e17\u0e35\u0e48\u0e2a\u0e38\u0e14\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e40\u0e23\u0e47\u0e27\u0e46 \u0e19\u0e35\u0e49 (LRU) {0} \u0e16\u0e39\u0e01\u0e01\u0e33\u0e2b\u0e19\u0e14\u0e44\u0e27\u0e49\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {1}

+

+##DynamicConfigProperty

+evalExpressionFailed={0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e1b\u0e23\u0e30\u0e40\u0e21\u0e34\u0e19\u0e1c\u0e25\u0e44\u0e14\u0e49

+

+##JsonContainerConfigLoader

+readingContainerConfig=\u0e04\u0e2d\u0e19\u0e1f\u0e34\u0e01\u0e39\u0e40\u0e23\u0e0a\u0e31\u0e19\u0e04\u0e2d\u0e19\u0e40\u0e17\u0e19\u0e40\u0e19\u0e2d\u0e23\u0e4c {0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e2d\u0e48\u0e32\u0e19

+loadFromString={0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e44\u0e14\u0e49

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a\u0e08\u0e32\u0e01 {0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e42\u0e2b\u0e25\u0e14

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=\u0e44\u0e1f\u0e25\u0e4c\u0e08\u0e32\u0e01 {0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e42\u0e2b\u0e25\u0e14

+

+##ApiServlet

+apiServletProtocolException=\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e43\u0e19\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e16\u0e39\u0e01\u0e2a\u0e48\u0e07\u0e04\u0e37\u0e19\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e1b\u0e23\u0e42\u0e15\u0e04\u0e2d\u0e25

+apiServletException=\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e43\u0e19\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e16\u0e39\u0e01\u0e2a\u0e48\u0e07\u0e04\u0e37\u0e19\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e42\u0e1b\u0e23\u0e42\u0e15\u0e04\u0e2d\u0e25

+

+##FeatureRegistry

+overridingFeature=\u0e04\u0e38\u0e13\u0e25\u0e31\u0e01\u0e29\u0e13\u0e30 {0} \u0e14\u0e49\u0e27\u0e22\u0e19\u0e34\u0e22\u0e32\u0e21\u0e17\u0e35\u0e48 {1} \u0e16\u0e39\u0e01\u0e41\u0e17\u0e19\u0e17\u0e31\u0e1a

+

+##FeatureResourceLoader

+missingFile=\u0e44\u0e1f\u0e25\u0e4c {0} \u0e16\u0e39\u0e01\u0e43\u0e0a\u0e49\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e43\u0e2b\u0e49\u0e21\u0e35\u0e2d\u0e22\u0e39\u0e48\u0e41\u0e15\u0e48\u0e15\u0e2d\u0e19\u0e19\u0e35\u0e49\u0e2b\u0e32\u0e22\u0e44\u0e1b

+unableRetrieveLib=\u0e44\u0e25\u0e1a\u0e23\u0e32\u0e23\u0e35\u0e41\u0e1a\u0e1a\u0e23\u0e35\u0e42\u0e21\u0e15\u0e08\u0e32\u0e01 {0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e36\u0e07\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e44\u0e14\u0e49

+

+##BasicHttpFetcher

+timeoutException={0} \u0e2b\u0e21\u0e14\u0e40\u0e27\u0e25\u0e32\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: {1} - {2} - {3} ms

+exceptionOccurred=\u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e40\u0e01\u0e34\u0e14\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e14\u0e36\u0e07 {0}: {1} ms \u0e17\u0e35\u0e48\u0e43\u0e0a\u0e49

+slowResponse={0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e15\u0e2d\u0e1a\u0e2a\u0e19\u0e2d\u0e07\u0e0a\u0e49\u0e32\u0e25\u0e07 {1} ms \u0e17\u0e35\u0e48\u0e43\u0e0a\u0e49

+

+##HttpResponseMetadataHelper

+errorGettingMD5=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a Message Digest 5 (MD5) \u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e16\u0e39\u0e01\u0e25\u0e30\u0e40\u0e27\u0e49\u0e19

+errorParsingMD5=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e2a\u0e15\u0e23\u0e34\u0e07 Message Digest 5 (MD5) \u0e43\u0e19\u0e23\u0e39\u0e1b\u0e41\u0e1a\u0e1a UTF-8

+

+##OAuthModule

+usingRandomKey=\u0e04\u0e35\u0e22\u0e4c\u0e41\u0e1a\u0e1a\u0e2a\u0e38\u0e48\u0e21\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e23\u0e2b\u0e31\u0e2a\u0e2a\u0e16\u0e32\u0e19\u0e30 OAuth \u0e43\u0e19\u0e1d\u0e31\u0e48\u0e07\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e16\u0e39\u0e01\u0e19\u0e33\u0e21\u0e32\u0e43\u0e0a\u0e49

+usingFile=\u0e44\u0e1f\u0e25\u0e4c {0} \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e23\u0e2b\u0e31\u0e2a\u0e2a\u0e16\u0e32\u0e19\u0e30 OAuth \u0e43\u0e19\u0e1d\u0e31\u0e48\u0e07\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e16\u0e39\u0e01\u0e19\u0e33\u0e21\u0e32\u0e43\u0e0a\u0e49

+loadKeyFileFrom=\u0e04\u0e35\u0e22\u0e4c\u0e01\u0e32\u0e23\u0e25\u0e07\u0e19\u0e32\u0e21 OAuth \u0e08\u0e32\u0e01 {0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e42\u0e2b\u0e25\u0e14

+couldNotLoadKeyFile= \u0e04\u0e35\u0e22\u0e4c\u0e44\u0e1f\u0e25\u0e4c {0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e42\u0e2b\u0e25\u0e14\u0e44\u0e14\u0e49

+couldNotLoadSignedKey=\u0e04\u0e35\u0e22\u0e4c\u0e01\u0e32\u0e23\u0e25\u0e07\u0e19\u0e32\u0e21 OAuth \u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e42\u0e2b\u0e25\u0e14\u0e44\u0e27\u0e49\u0e2d\u0e22\u0e48\u0e32\u0e07\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 \u0e2b\u0e32\u0e01\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e04\u0e35\u0e22\u0e4c: \n 1. \u0e23\u0e31\u0e19\u0e04\u0e33\u0e2a\u0e31\u0e48\u0e07\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. \u0e41\u0e01\u0e49\u0e44\u0e02\u0e44\u0e1f\u0e25\u0e4c shindig.properties \u0e42\u0e14\u0e22\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e1a\u0e23\u0e23\u0e17\u0e31\u0e14\u0e40\u0e2b\u0e25\u0e48\u0e32\u0e19\u0e35\u0e49: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=\u0e04\u0e2d\u0e19\u0e0b\u0e39\u0e21\u0e40\u0e21\u0e2d\u0e23\u0e4c OAuth \u0e08\u0e32\u0e01 {0} \u0e25\u0e49\u0e21\u0e40\u0e2b\u0e25\u0e27\u0e43\u0e19\u0e01\u0e32\u0e23\u0e40\u0e15\u0e23\u0e35\u0e22\u0e21\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e40\u0e1a\u0e37\u0e49\u0e2d\u0e07\u0e15\u0e49\u0e19

+

+##OAuthRequest

+oauthFetchFatalError=\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e23\u0e38\u0e19\u0e41\u0e23\u0e07\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e40\u0e01\u0e34\u0e14\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d OAuth \u0e01\u0e33\u0e25\u0e31\u0e07\u0e14\u0e36\u0e07\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32: \n {0}

+oauthFetchErrorReprompt=\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e40\u0e01\u0e34\u0e14\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d OAuth \u0e01\u0e33\u0e25\u0e31\u0e07\u0e14\u0e36\u0e07\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32: \n {0} \u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e15\u0e4c\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e2d\u0e19\u0e38\u0e21\u0e31\u0e15\u0e34

+bogusExpired=\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e2a\u0e48\u0e07\u0e04\u0e37\u0e19\u0e01\u0e32\u0e23\u0e2b\u0e21\u0e14\u0e2d\u0e32\u0e22\u0e38\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07:\n {0}

+oauthFetchUnexpectedError=\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e23\u0e38\u0e19\u0e41\u0e23\u0e07\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e40\u0e01\u0e34\u0e14\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d OAuth \u0e01\u0e33\u0e25\u0e31\u0e07\u0e14\u0e36\u0e07\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32: \n {0}.

+unauthenticatedOauth=OAuth \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e36\u0e07\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e44\u0e14\u0e49\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49\u0e44\u0e21\u0e48\u0e21\u0e35\u0e2d\u0e22\u0e39\u0e48 \u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: \n {0}.

+invalidOauth=OAuth \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e36\u0e07\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e44\u0e14\u0e49\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 \u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=\u0e2a\u0e44\u0e15\u0e25\u0e4c\u0e0a\u0e35\u0e15\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e44\u0e14\u0e49

+unableToConvertScript=\u0e42\u0e2b\u0e19\u0e14\u0e2a\u0e04\u0e23\u0e34\u0e1b\u0e15\u0e4c\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e41\u0e1b\u0e25\u0e07\u0e40\u0e1b\u0e47\u0e19\u0e41\u0e17\u0e47\u0e01 OpenSocial Markup Language (OSML) \u0e44\u0e14\u0e49

+

+##PipelineExecutor

+errorPreloading=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e04\u0e32\u0e14\u0e04\u0e34\u0e14\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e17\u0e33\u0e01\u0e32\u0e23\u0e42\u0e2b\u0e25\u0e14\u0e01\u0e48\u0e2d\u0e19

+

+##Processor

+renderBlacklistedGadget=\u0e23\u0e30\u0e1a\u0e1a\u0e1e\u0e22\u0e32\u0e22\u0e32\u0e21\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e01\u0e32\u0e23\u0e41\u0e2a\u0e14\u0e07\u0e1c\u0e25\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e17\u0e35\u0e48\u0e2d\u0e22\u0e39\u0e48\u0e43\u0e19\u0e23\u0e32\u0e22\u0e0a\u0e37\u0e48\u0e2d\u0e04\u0e27\u0e32\u0e21\u0e44\u0e21\u0e48\u0e19\u0e48\u0e32\u0e40\u0e0a\u0e37\u0e48\u0e2d\u0e16\u0e37\u0e2d: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e36\u0e07\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e44\u0e14\u0e49

+failedToRead={0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e2d\u0e48\u0e32\u0e19\u0e44\u0e14\u0e49

+

+##DefaultServiceFetcher

+httpErrorFetching=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a HTTP {0} \u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e14\u0e36\u0e07\u0e40\u0e21\u0e18\u0e2d\u0e14\u0e02\u0e2d\u0e07\u0e40\u0e0b\u0e2d\u0e23\u0e4c\u0e27\u0e34\u0e2a\u0e08\u0e32\u0e01\u0e08\u0e38\u0e14\u0e1b\u0e25\u0e32\u0e22 {1}

+failedToFetchService=\u0e40\u0e21\u0e18\u0e2d\u0e14\u0e02\u0e2d\u0e07\u0e40\u0e0b\u0e2d\u0e23\u0e4c\u0e27\u0e34\u0e2a\u0e08\u0e32\u0e01\u0e08\u0e38\u0e14\u0e1b\u0e25\u0e32\u0e22 {0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e36\u0e07\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e44\u0e14\u0e49 \u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: {1}.

+failedToParseService=\u0e40\u0e21\u0e18\u0e2d\u0e14\u0e02\u0e2d\u0e07\u0e40\u0e0b\u0e2d\u0e23\u0e4c\u0e27\u0e34\u0e2a\u0e08\u0e32\u0e01\u0e08\u0e38\u0e14\u0e1b\u0e25\u0e32\u0e22 {0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e44\u0e14\u0e49 \u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: {1}

+

+##Renderer

+FailedToRender=\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e17\u0e35\u0e48 {0} \u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e01\u0e32\u0e23\u0e41\u0e2a\u0e14\u0e07\u0e1c\u0e25 \u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: {1}

+

+##RenderingGadgetRewriter

+unknownFeatures=\u0e04\u0e38\u0e13\u0e25\u0e31\u0e01\u0e29\u0e13\u0e30\u0e15\u0e31\u0e49\u0e07\u0e41\u0e15\u0e48\u0e2b\u0e19\u0e36\u0e48\u0e07\u0e02\u0e49\u0e2d\u0e02\u0e36\u0e49\u0e19\u0e44\u0e1b\u0e21\u0e35\u0e2d\u0e22\u0e39\u0e48\u0e43\u0e19 extern &libs=: {0}

+unexpectedErrorPreloading=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e04\u0e32\u0e14\u0e04\u0e34\u0e14\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e17\u0e33\u0e01\u0e32\u0e23\u0e42\u0e2b\u0e25\u0e14\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e01\u0e48\u0e2d\u0e19

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u0e2d\u0e2d\u0e01\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19\u0e44\u0e21\u0e48\u0e43\u0e2b\u0e49\u0e40\u0e01\u0e34\u0e14\u0e1b\u0e31\u0e0d\u0e2b\u0e32\u0e42\u0e14\u0e22\u0e44\u0e21\u0e48\u0e21\u0e35\u0e0a\u0e19\u0e34\u0e14\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {0}

+requestToSanitizeUnknownContent=\u0e2d\u0e2d\u0e01\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19\u0e44\u0e21\u0e48\u0e43\u0e2b\u0e49\u0e40\u0e01\u0e34\u0e14\u0e1b\u0e31\u0e0d\u0e2b\u0e32\u0e42\u0e14\u0e22\u0e44\u0e21\u0e48\u0e21\u0e35\u0e0a\u0e19\u0e34\u0e14\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e17\u0e35\u0e48\u0e23\u0e39\u0e49\u0e08\u0e31\u0e01 {0} \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {1}

+unableToSanitizeUnknownImg=\u0e44\u0e21\u0e48\u0e23\u0e39\u0e49\u0e08\u0e31\u0e01\u0e0a\u0e19\u0e34\u0e14\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0} \u0e41\u0e25\u0e30\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19\u0e44\u0e21\u0e48\u0e43\u0e2b\u0e49\u0e40\u0e01\u0e34\u0e14\u0e1b\u0e31\u0e0d\u0e2b\u0e32\u0e44\u0e14\u0e49

+unableToDetectImgType=\u0e0a\u0e19\u0e34\u0e14\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e15\u0e23\u0e27\u0e08\u0e1e\u0e1a\u0e44\u0e14\u0e49\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e1b\u0e49\u0e2d\u0e07\u0e01\u0e31\u0e19\u0e44\u0e21\u0e48\u0e43\u0e2b\u0e49\u0e40\u0e01\u0e34\u0e14\u0e1b\u0e31\u0e0d\u0e2b\u0e32\u0e01\u0e31\u0e1a\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32

+

+##BasicImageRewriter

+ioErrorRewritingImg=\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e2d\u0e34\u0e19\u0e1e\u0e38\u0e15/\u0e40\u0e2d\u0e32\u0e15\u0e4c\u0e1e\u0e38\u0e15\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e40\u0e01\u0e34\u0e14\u0e02\u0e36\u0e49\u0e19\u0e02\u0e13\u0e30\u0e40\u0e02\u0e35\u0e22\u0e19\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0} \u0e43\u0e2b\u0e21\u0e48\u0e2d\u0e35\u0e01\u0e04\u0e23\u0e31\u0e49\u0e07 : {1}

+unknownErrorRewritingImg=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e40\u0e02\u0e35\u0e22\u0e19\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0}: {1}

+failedToReadImg=\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0} \u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e2d\u0e48\u0e32\u0e19\u0e44\u0e14\u0e49\u0e41\u0e25\u0e30\u0e16\u0e39\u0e01\u0e02\u0e49\u0e32\u0e21 \u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e02\u0e36\u0e49\u0e19: {1}

+

+##CssResponseRewriter

+cajaCssParseFailure=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e02\u0e36\u0e49\u0e19\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33 Caja CSS: {0} \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {1}

+

+##ImageAttributeRewriter

+unableToProcessImg=\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e33\u0e40\u0e19\u0e34\u0e19\u0e01\u0e32\u0e23\u0e01\u0e31\u0e1a\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a\u0e02\u0e2d\u0e07\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0} \u0e44\u0e14\u0e49

+unableToReadResponse=\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e2d\u0e48\u0e32\u0e19\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a\u0e02\u0e2d\u0e07\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0} \u0e44\u0e14\u0e49

+unableToFetchImg=\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e14\u0e36\u0e07\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a\u0e02\u0e2d\u0e07\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0} \u0e44\u0e14\u0e49

+unableToParseImg=\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a\u0e02\u0e2d\u0e07\u0e2d\u0e34\u0e21\u0e40\u0e21\u0e08 {0} \u0e44\u0e14\u0e49

+

+##MutableContent

+exceptionParsingContent=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19\u0e02\u0e36\u0e49\u0e19\u0e02\u0e13\u0e30\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e40\u0e19\u0e37\u0e49\u0e2d\u0e2b\u0e32\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e17\u0e35\u0e48 {0} \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e42\u0e2b\u0e25\u0e14\u0e01\u0e48\u0e2d\u0e19\u0e44\u0e14\u0e49

+

+##ProxyingVisitor

+uriExceptionParsing=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a Uniform Resource Identifier (URI) \u0e40\u0e21\u0e37\u0e48\u0e2d\u0e27\u0e34\u0e40\u0e04\u0e23\u0e32\u0e30\u0e2b\u0e4c\u0e04\u0e33\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e17\u0e35\u0e48 {0}

+

+##TemplateRewriter

+malformedTemplateLib=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19\u0e02\u0e36\u0e49\u0e19\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e44\u0e25\u0e1a\u0e23\u0e32\u0e23\u0e35\u0e40\u0e17\u0e47\u0e21\u0e40\u0e1e\u0e25\u0e15\u0e1c\u0e34\u0e14\u0e23\u0e39\u0e1b\u0e41\u0e1a\u0e1a

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u0e41\u0e04\u0e0a\u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e42\u0e19\u0e49\u0e21\u0e19\u0e49\u0e32\u0e27\u0e16\u0e39\u0e01\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e02\u0e36\u0e49\u0e19

+retrieveReference={0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e14\u0e36\u0e07\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25

+unableToCajole=\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e42\u0e19\u0e49\u0e21\u0e19\u0e49\u0e32\u0e27\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e17\u0e35\u0e48 {0} \u0e44\u0e14\u0e49

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e1e\u0e23\u0e47\u0e2d\u0e01\u0e0b\u0e35\u0e41\u0e1a\u0e1a\u0e15\u0e48\u0e2d\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07: {0}

+

+##GadgetRenderingServlet

+malformedTtlValue=\u0e04\u0e48\u0e32 Time To Live (TTL) \u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e07 {0} \u0e16\u0e39\u0e01\u0e25\u0e30\u0e40\u0e27\u0e49\u0e19

+

+##HttpRequestHandler

+gadgetCreationError=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e23\u0e30\u0e2b\u0e27\u0e48\u0e32\u0e07\u0e01\u0e32\u0e23\u0e2a\u0e23\u0e49\u0e32\u0e07 gadget \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e02\u0e35\u0e22\u0e19\u0e43\u0e2b\u0e21\u0e48  \u0e01\u0e32\u0e23\u0e40\u0e02\u0e35\u0e22\u0e19\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e43\u0e2b\u0e21\u0e48\u0e42\u0e14\u0e22\u0e44\u0e21\u0e48\u0e21\u0e35 gadget

+

+##ProxyServlet

+embededImgWrongDomain=\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e1d\u0e31\u0e07 {0} URL \u0e16\u0e39\u0e01\u0e17\u0e33\u0e43\u0e2b\u0e49\u0e1c\u0e34\u0e14\u0e42\u0e14\u0e40\u0e21\u0e19 {1}

+

+##DefaultTemplateProcessor

+elFailure=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14 EL \u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e17\u0e35\u0e48 {0}: {1}

+

+##UriUtils

+skipIllegalHeader=\u0e2a\u0e48\u0e27\u0e19\u0e2b\u0e31\u0e27 {0} \u0e1c\u0e34\u0e14\u0e01\u0e0e\u0e40\u0e01\u0e13\u0e11\u0e4c\u0e41\u0e25\u0e30\u0e16\u0e39\u0e01\u0e02\u0e49\u0e32\u0e21 \u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49: {1}

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e36\u0e49\u0e19\u0e02\u0e13\u0e30\u0e2d\u0e31\u0e1e\u0e40\u0e14\u0e15 {0} \u0e42\u0e04\u0e49\u0e14\u0e2a\u0e16\u0e32\u0e19\u0e30 {1} \u0e16\u0e39\u0e01\u0e2a\u0e48\u0e07\u0e04\u0e37\u0e19 \u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19: {2} \u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e0a\u0e31\u0e19\u0e17\u0e35\u0e48\u0e41\u0e04\u0e0a\u0e41\u0e25\u0e49\u0e27\u0e08\u0e30\u0e16\u0e39\u0e01\u0e43\u0e0a\u0e49\u0e41\u0e17\u0e19

+updateSpecFailureApplyNegCache=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e36\u0e49\u0e19\u0e02\u0e13\u0e30\u0e2d\u0e31\u0e1e\u0e40\u0e14\u0e15 {0} \u0e42\u0e04\u0e49\u0e14\u0e2a\u0e16\u0e32\u0e19\u0e30 {1} \u0e16\u0e39\u0e01\u0e2a\u0e48\u0e07\u0e04\u0e37\u0e19 \u0e02\u0e49\u0e2d\u0e22\u0e01\u0e40\u0e27\u0e49\u0e19: {2} \u0e41\u0e04\u0e0a\u0e17\u0e35\u0e48\u0e15\u0e34\u0e14\u0e04\u0e48\u0e32\u0e25\u0e1a\u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e43\u0e0a\u0e49

+

+##HashLockedDomainService

+noLockedDomainConfig=\u0e04\u0e2d\u0e19\u0e1f\u0e34\u0e01\u0e39\u0e40\u0e23\u0e0a\u0e31\u0e19\u0e02\u0e2d\u0e07\u0e42\u0e14\u0e40\u0e21\u0e19\u0e17\u0e35\u0e48\u0e25\u0e47\u0e2d\u0e01\u0e44\u0e27\u0e49\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {0} \u0e44\u0e21\u0e48\u0e21\u0e35\u0e2d\u0e22\u0e39\u0e48

+

+##Bootstrap

+startingConnManagerWith=\u0e1c\u0e39\u0e49\u0e08\u0e31\u0e14\u0e01\u0e32\u0e23\u0e40\u0e0a\u0e37\u0e48\u0e2d\u0e21\u0e15\u0e48\u0e2d\u0e14\u0e49\u0e27\u0e22\u0e04\u0e38\u0e13\u0e2a\u0e21\u0e1a\u0e31\u0e15\u0e34 {0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e34\u0e48\u0e21\u0e17\u0e33\u0e07\u0e32\u0e19

+

+##XSDValidator

+resolveResource=\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e41\u0e01\u0e49\u0e1b\u0e31\u0e0d\u0e2b\u0e32: {0}, {1}, {2}, {3}

+failedToValidate=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e13\u0e30\u0e15\u0e23\u0e27\u0e08\u0e2a\u0e2d\u0e1a\u0e04\u0e27\u0e32\u0e21\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 {0}

+

+##DefaultRequestPipeline

+cachedResponse=\u0e2a\u0e48\u0e07\u0e04\u0e37\u0e19\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e17\u0e35\u0e48\u0e41\u0e04\u0e0a\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e1b\u0e22\u0e31\u0e07 {0}

+staleResponse="\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e43\u0e19\u0e01\u0e32\u0e23\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a\u0e17\u0e35\u0e48 {0} \u0e41\u0e15\u0e48\u0e40\u0e23\u0e32\u0e21\u0e35\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e01\u0e48\u0e2d\u0e19\u0e2b\u0e19\u0e49\u0e32\u0e43\u0e19\u0e41\u0e04\u0e0a  \u0e2a\u0e48\u0e07\u0e04\u0e37\u0e19\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e17\u0e35\u0e48\u0e2d\u0e32\u0e08\u0e04\u0e49\u0e32\u0e07\u0e2d\u0e22\u0e39\u0e48\u0e08\u0e32\u0e01\u0e41\u0e04\u0e0a

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_tr.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_tr.properties
new file mode 100644
index 0000000..5607807
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_tr.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=G\u00fcvenlik belirteci ya da kimlik bilgisi bozuk ve ayr\u0131\u015ft\u0131r\u0131lam\u0131yor.

+

+##XmlUtil

+errorParsingXML=XML ayr\u0131\u015ft\u0131r\u0131l\u0131rken hata olu\u015ftu. Bu hata yoksay\u0131labilir.

+errorParsingExternalGeneralEntities=Kullan\u0131lmakta olan XML i\u015flemcisi d\u0131\u015f genel varl\u0131klar\u0131 y\u00fckleyecek.

+errorParsingExternalParameterEntities=Kullan\u0131lmakta olan XML i\u015flemcisi d\u0131\u015f parametre varl\u0131klar\u0131n\u0131 y\u00fckleyecek.

+errorParsingExternalDTD=Kullan\u0131lmakta olan XML i\u015flemcisi Belge T\u00fcr\u00fc Tan\u0131mlar\u0131'n\u0131 (DTD) y\u00fckleyecek.

+errorNotUsingSecureXML=Kullan\u0131lmakta olan XML i\u015flemcisi g\u00fcvenli ayr\u0131\u015ft\u0131rmay\u0131 desteklemiyor.

+reuseDocumentBuilders=Belge olu\u015fturucular yeniden kullan\u0131l\u0131yor.

+notReuseDocBuilders=Belge olu\u015fturucular yeniden kullan\u0131lm\u0131yor.

+

+##LruCacheProvider

+LRUCapacity=En \u00f6nce kullan\u0131lan (LRU) kapasitesi {0}, {1} i\u00e7in yap\u0131land\u0131r\u0131ld\u0131.

+

+##DynamicConfigProperty

+evalExpressionFailed={0} de\u011ferlendirilemiyor.

+

+##JsonContainerConfigLoader

+readingContainerConfig={0} kapsay\u0131c\u0131 yap\u0131land\u0131rmas\u0131 okunuyor.

+loadFromString={0} ayr\u0131\u015ft\u0131r\u0131lam\u0131yor.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom={0} konumundaki kaynaklar y\u00fckleniyor.

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom={0} konumundaki dosyalar y\u00fckleniyor.

+

+##ApiServlet

+apiServletProtocolException=Bir ileti\u015fim kural\u0131 \u00f6zel durumu olu\u015ftu\u011fundan bir yan\u0131t hatas\u0131 d\u00f6nd\u00fcr\u00fcl\u00fcyor.

+apiServletException=Bir ileti\u015fim kural\u0131 \u00f6zel durumu olu\u015ftu\u011fundan bir yan\u0131t hatas\u0131 d\u00f6nd\u00fcr\u00fcl\u00fcyor.

+

+##FeatureRegistry

+overridingFeature={1} olana\u011f\u0131nda tan\u0131m\u0131 bulunan {0} \u00f6zelli\u011fi ge\u00e7ersiz k\u0131l\u0131n\u0131yor.

+

+##FeatureResourceLoader

+missingFile={0} dosyas\u0131 \u00f6nceden vard\u0131, ancak \u015fimdi yok.

+unableRetrieveLib={0} konumundan uzak kitapl\u0131k al\u0131nam\u0131yor.

+

+##BasicHttpFetcher

+timeoutException={0}, \u015fu \u00f6zel durum nedeniyle zamana\u015f\u0131m\u0131na u\u011frad\u0131: {1} - {2} - {3} ms.

+exceptionOccurred={0} getirilirken \u015fu \u00f6zel durum olu\u015ftu: {1} ms ge\u00e7ti.

+slowResponse={0} yava\u015f yan\u0131t veriyor. {1} ms ge\u00e7ti.

+

+##HttpResponseMetadataHelper

+errorGettingMD5=\u0130leti \u00d6zeti 5 (MD5) al\u0131n\u0131rken bir hata olu\u015ftu. Hata yoksay\u0131ld\u0131.

+errorParsingMD5=UTF-8 bi\u00e7imindeki \u0130leti \u00d6zeti 5 (MD5) dizesi ayr\u0131\u015ft\u0131r\u0131l\u0131rken bir hata olu\u015ftu.

+

+##OAuthModule

+usingRandomKey=OAuth istemci taraf\u0131 durum \u015fifrelemesi i\u00e7in rasgele bir anahtar kullan\u0131l\u0131yor.

+usingFile=OAuth istemci taraf\u0131 durum \u015fifrelemesi i\u00e7in {0} dosyas\u0131 kullan\u0131l\u0131yor.

+loadKeyFileFrom={0} OAuth imza anahtar\u0131 y\u00fckleniyor.

+couldNotLoadKeyFile= {0} anahtar dosyas\u0131 y\u00fcklenemedi.

+couldNotLoadSignedKey=OAuth imza anahtar\u0131 do\u011fru bir \u015fekilde y\u00fcklenmedi. Bir anahtar olu\u015fturmak i\u00e7in: \n 1. A\u015fa\u011f\u0131daki komutu \u00e7al\u0131\u015ft\u0131r\u0131n: \n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. A\u015fa\u011f\u0131daki sat\u0131rlar\u0131 ekleyerek shindig.properties dosyas\u0131n\u0131 d\u00fczenleyin: \n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit={0} konumundan OAuth t\u00fcketicileri ba\u015flat\u0131lamad\u0131.

+

+##OAuthRequest

+oauthFetchFatalError=OAuth i\u00e7eri\u011fi getirirken a\u015fa\u011f\u0131daki \u00f6nemli hata olu\u015ftu: \n \ {0}.

+oauthFetchErrorReprompt=OAuth i\u00e7eri\u011fi getirirken a\u015fa\u011f\u0131daki hata olu\u015ftu: \n \ {0}. Kullan\u0131c\u0131n\u0131n onay\u0131 yeniden isteniyor.

+bogusExpired=Sunucu ge\u00e7ersiz bir s\u00fcre sonu d\u00f6nd\u00fcrd\u00fc:\n {0}.

+oauthFetchUnexpectedError=OAuth i\u00e7eri\u011fi getirirken a\u015fa\u011f\u0131daki \u00f6nemli hata olu\u015ftu: \n \ {0}.

+unauthenticatedOauth=Kullan\u0131c\u0131 kimlik do\u011frulamas\u0131 var olmad\u0131\u011f\u0131ndan OAuth i\u00e7eri\u011fi getiremiyor. A\u015fa\u011f\u0131daki hata olu\u015ftu: \n {0}.

+invalidOauth=\u0130stek ge\u00e7ersiz oldu\u011fundan OAuth i\u00e7eri\u011fi getiremiyor. A\u015fa\u011f\u0131daki hata olu\u015ftu: \n {0}.

+

+##CajaCssSanitizer

+failedToParse=Stil sayfas\u0131 ayr\u0131\u015ft\u0131r\u0131lam\u0131yor.

+unableToConvertScript=Komut dosyas\u0131 d\u00fc\u011f\u00fcm\u00fc, bir OpenSocial Bi\u00e7imlendirme Dili (OSML) etiketine d\u00f6n\u00fc\u015ft\u00fcr\u00fclemiyor.

+

+##PipelineExecutor

+errorPreloading=\u00d6nceden y\u00fckleme s\u0131ras\u0131nda beklenmedik bir hata olu\u015ftu.

+

+##Processor

+renderBlacklistedGadget=Sistem, kara listedeki \u015fu arac\u0131 olu\u015fturmay\u0131 denedi: {0}.

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve={0} al\u0131nam\u0131yor.

+failedToRead={0} okunam\u0131yor.

+

+##DefaultServiceFetcher

+httpErrorFetching={1} u\u00e7 noktas\u0131ndan hizmet y\u00f6ntemleri getirilirken bir HTTP {0} hatas\u0131 olu\u015ftu.

+failedToFetchService={0} u\u00e7 noktas\u0131ndaki hizmet y\u00f6ntemleri getirilemedi. \u015eu hata olu\u015ftu: {1}.

+failedToParseService={0} u\u00e7 noktas\u0131ndaki hizmet y\u00f6ntemleri ayr\u0131\u015ft\u0131r\u0131lamad\u0131. \u015eu hata olu\u015ftu: {1}.

+

+##Renderer

+FailedToRender={0} konumundaki ara\u00e7 g\u00f6rsel olarak olu\u015fturulamad\u0131. \u015eu hata olu\u015ftu: {1}.

+

+##RenderingGadgetRewriter

+unknownFeatures=\u015eu extern &libs= \u00f6\u011fesinde bir ya da daha fazla bilinmeyen \u00f6zellik var: {0}.

+unexpectedErrorPreloading=Ara\u00e7 \u00f6nceden y\u00fcklenirken beklenmedik bir hata olu\u015ftu.

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent={0} i\u00e7in bir i\u00e7erik t\u00fcr\u00fc belirtilmeden bir temizleme iste\u011fi yay\u0131nland\u0131.

+requestToSanitizeUnknownContent={1} i\u00e7in bir {0} bilinen i\u00e7erik t\u00fcr\u00fc olmadan temizleme iste\u011fi yay\u0131nland\u0131.

+unableToSanitizeUnknownImg={0} resim t\u00fcr\u00fc bilinmiyor ve temizlenemez.

+unableToDetectImgType=\u0130\u00e7erik temizlenirken {0} i\u00e7in resim t\u00fcr\u00fc alg\u0131lanam\u0131yor.

+

+##BasicImageRewriter

+ioErrorRewritingImg={0} resmi yeniden yaz\u0131l\u0131rken \u015fu giri\u015f/\u00e7\u0131k\u0131\u015f hatas\u0131 olu\u015ftu: {1}.

+unknownErrorRewritingImg={0} resmi yeniden yaz\u0131l\u0131rken \u015fu hata olu\u015ftu: {1}.

+failedToReadImg={0} resmi okunam\u0131yor ve atlan\u0131yor. \u015eu hata olu\u015ftu: {1}.

+

+##CssResponseRewriter

+cajaCssParseFailure=Caja CSS ayr\u0131\u015ft\u0131r\u0131l\u0131rken \u015fu hata olu\u015ftu: {1} i\u00e7in {0}.

+

+##ImageAttributeRewriter

+unableToProcessImg={0} resim kayna\u011f\u0131 i\u015flenemiyor.

+unableToReadResponse={0} resim kayna\u011f\u0131na ili\u015fkin yan\u0131t okunam\u0131yor.

+unableToFetchImg={0} resim kayna\u011f\u0131 getirilemiyor.

+unableToParseImg={0} resim kayna\u011f\u0131 ayr\u0131\u015ft\u0131r\u0131lam\u0131yor.

+

+##MutableContent

+exceptionParsingContent=Araca ili\u015fkin i\u00e7erik ayr\u0131\u015ft\u0131r\u0131l\u0131rken bir \u00f6zel durum olu\u015ftu.

+

+##PipelineDataGadgetRewriter

+failedToParsePreload={0} konumundaki ara\u00e7 \u00f6nceden y\u00fckleme i\u00e7in ayr\u0131\u015ft\u0131r\u0131lamad\u0131.

+

+##ProxyingVisitor

+uriExceptionParsing={0} konumundaki ara\u00e7 ayr\u0131\u015ft\u0131r\u0131l\u0131rken bir Tek Bi\u00e7imli Kaynak Tan\u0131mlay\u0131c\u0131 (URI) \u00f6zel durumu olu\u015ftu.

+

+##TemplateRewriter

+malformedTemplateLib=Bozuk \u015fablon kitapl\u0131klar\u0131 nedeniyle \u00f6zel durumlar olu\u015ftu.

+

+##CajaContnetRewriter

+cajoledCacheCreated=Derlenmi\u015f bir \u00f6nbellek olu\u015fturuldu.

+retrieveReference={0} al\u0131n\u0131yor.

+unableToCajole={0} konumundaki ara\u00e7 derlenemiyor.

+

+##ConcatProxyServlet

+concatProxyRequestFailed=Birle\u015ftirilmi\u015f yetkili sunucu istenirken \u015fu hata olu\u015ftu: {0}.

+

+##GadgetRenderingServlet

+malformedTtlValue=Ge\u00e7ersiz bir Kullan\u0131m S\u00fcresi (TTL) de\u011feri olan {0} yoksay\u0131ld\u0131.

+

+##HttpRequestHandler

+gadgetCreationError=Yeniden yazma i\u00e7in ara\u00e7 olu\u015fturulurken bir hata ortaya \u00e7\u0131kt\u0131.  Yan\u0131t ara\u00e7 olmadan yeniden yaz\u0131l\u0131yor.

+

+##ProxyServlet

+embededImgWrongDomain={0} URL''sini yerle\u015ftirme iste\u011fi yanl\u0131\u015f etki alan\u0131na yap\u0131ld\u0131: {1}.

+

+##DefaultTemplateProcessor

+elFailure={0} konumundaki ara\u00e7la ilgili olarak \u015fu EL hatas\u0131 olu\u015ftu: {1}.

+

+##UriUtils

+skipIllegalHeader={0} \u00fcstbilgisi ge\u00e7ersiz ve atlan\u0131yor. \u015eu hata olu\u015ftu: {1}.

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion={0} g\u00fcncelle\u015ftirilirken bir hata olu\u015ftu. {1} durum kodu d\u00f6nd\u00fcr\u00fcld\u00fc. \u00d6zel durum: {2}. Bunun yerine \u00f6nbelle\u011fe al\u0131nm\u0131\u015f bir s\u00fcr\u00fcm kullan\u0131l\u0131yor.

+updateSpecFailureApplyNegCache={0} g\u00fcncelle\u015ftirilirken bir hata olu\u015ftu. {1} durum kodu d\u00f6nd\u00fcr\u00fcld\u00fc. \u00d6zel durum: {2}. Bir negatif \u00f6nbellek uygulan\u0131yor.

+

+##HashLockedDomainService

+noLockedDomainConfig={0} i\u00e7in kilitli bir etki alan\u0131 yap\u0131land\u0131rmas\u0131 yok.

+

+##Bootstrap

+startingConnManagerWith={0} \u00f6zelliklerine sahip ba\u011flant\u0131 y\u00f6neticisi ba\u015flat\u0131l\u0131yor.

+

+##XSDValidator

+resolveResource=\u015eu kaynaklar \u00e7\u00f6z\u00fcmleniyor: {0}, {1}, {2}, {3}.

+failedToValidate={0} do\u011frulan\u0131rken bir hata olu\u015ftu.

+

+##DefaultRequestPipeline

+cachedResponse=\u0130ste\u011fin \u00f6nbelle\u011fe al\u0131nan yan\u0131t\u0131 {0} kayna\u011f\u0131na d\u00f6nd\u00fcr\u00fcl\u00fcyor.

+staleResponse="{0} konumundaki kaynak istenirken hata olu\u015ftu, ancak \u00f6nbellekte \u00f6nceden bir yan\u0131t\u0131m\u0131z var. \u00d6nbellekten b\u00fcy\u00fck olas\u0131l\u0131kla eski bir yan\u0131t d\u00f6nd\u00fcr\u00fcl\u00fcyor.

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_zh.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_zh.properties
new file mode 100644
index 0000000..9f7b2dc
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_zh.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u5b89\u5168\u6027\u4ee4\u724c\u6216\u51ed\u8bc1\u683c\u5f0f\u9519\u8bef\u6216\u65e0\u6cd5\u89e3\u6790\u3002

+

+##XmlUtil

+errorParsingXML=\u89e3\u6790 XML \u65f6\u53d1\u751f\u9519\u8bef\u3002 \u60a8\u53ef\u4ee5\u5ffd\u7565\u6b64\u9519\u8bef\u3002

+errorParsingExternalGeneralEntities=\u6b63\u5728\u4f7f\u7528\u7684 XML \u5904\u7406\u5668\u5c06\u88c5\u5165\u5916\u90e8\u5e38\u89c4\u5b9e\u4f53\u3002

+errorParsingExternalParameterEntities=\u6b63\u5728\u4f7f\u7528\u7684 XML \u5904\u7406\u5668\u5c06\u88c5\u5165\u5916\u90e8\u53c2\u6570\u5b9e\u4f53\u3002

+errorParsingExternalDTD=\u6b63\u5728\u4f7f\u7528\u7684 XML \u5904\u7406\u5668\u5c06\u88c5\u5165\u6587\u6863\u7c7b\u578b\u5b9a\u4e49 (DTD)\u3002

+errorNotUsingSecureXML=\u6b63\u5728\u4f7f\u7528\u7684 XML \u5904\u7406\u5668\u4e0d\u652f\u6301\u5b89\u5168\u89e3\u6790\u3002

+reuseDocumentBuilders=\u6b63\u5728\u590d\u7528\u6587\u6863\u6784\u5efa\u5668\u3002

+notReuseDocBuilders=\u672a\u5728\u590d\u7528\u6587\u6863\u6784\u5efa\u5668\u3002

+

+##LruCacheProvider

+LRUCapacity=\u4e3a {1} \u914d\u7f6e\u4e86\u6700\u8fd1\u6700\u5c11\u4f7f\u7528 (LRU) \u5bb9\u91cf {0}\u3002

+

+##DynamicConfigProperty

+evalExpressionFailed=\u65e0\u6cd5\u5bf9 {0} \u8fdb\u884c\u6c42\u503c\u3002

+

+##JsonContainerConfigLoader

+readingContainerConfig=\u6b63\u5728\u8bfb\u53d6\u5bb9\u5668\u914d\u7f6e {0}\u3002

+loadFromString=\u65e0\u6cd5\u89e3\u6790 {0}\u3002

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=\u6b63\u5728\u88c5\u5165 {0} \u4e2d\u7684\u8d44\u6e90\u3002

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=\u6b63\u5728\u88c5\u5165 {0} \u4e2d\u7684\u6587\u4ef6\u3002

+

+##ApiServlet

+apiServletProtocolException=\u7531\u4e8e\u53d1\u751f\u534f\u8bae\u5f02\u5e38\uff0c\u6b63\u5728\u8fd4\u56de\u54cd\u5e94\u9519\u8bef\u3002

+apiServletException=\u7531\u4e8e\u53d1\u751f\u534f\u8bae\u5f02\u5e38\uff0c\u6b63\u5728\u8fd4\u56de\u54cd\u5e94\u9519\u8bef\u3002

+

+##FeatureRegistry

+overridingFeature=\u6b63\u5728\u8986\u76d6\u5728 {1} \u5904\u5b9a\u4e49\u7684\u529f\u80fd\u90e8\u4ef6 {0}\u3002

+

+##FeatureResourceLoader

+missingFile=\u6587\u4ef6 {0} \u66fe\u7ecf\u5b58\u5728\uff0c\u4f46\u73b0\u5728\u4e22\u5931\u3002

+unableRetrieveLib=\u65e0\u6cd5\u68c0\u7d22 {0} \u4e2d\u7684\u8fdc\u7a0b\u5e93\u3002

+

+##BasicHttpFetcher

+timeoutException=\u7531\u4e8e\u4ee5\u4e0b\u5f02\u5e38\uff0c{0} \u5df2\u8d85\u65f6\uff1a{1} - {2} - {3} ms\u3002

+exceptionOccurred=\u63d0\u53d6 {0} \u65f6\u53d1\u751f\u4e86\u4ee5\u4e0b\u5f02\u5e38\uff1a\u8017\u65f6 {1} ms\u3002

+slowResponse={0} \u54cd\u5e94\u7f13\u6162\u3002 \u8017\u65f6 {1} ms\u3002

+

+##HttpResponseMetadataHelper

+errorGettingMD5=\u83b7\u53d6\u6d88\u606f\u6458\u8981 5 (MD5) \u65f6\u53d1\u751f\u9519\u8bef\u3002 \u5df2\u5ffd\u7565\u6b64\u9519\u8bef\u3002

+errorParsingMD5=\u89e3\u6790 UTF-8 \u683c\u5f0f\u7684\u6d88\u606f\u6458\u8981 5 (MD5) \u5b57\u7b26\u4e32\u65f6\u53d1\u751f\u9519\u8bef\u3002

+

+##OAuthModule

+usingRandomKey=\u6b63\u5728\u4f7f\u7528 OAuth \u5ba2\u6237\u673a\u7aef\u72b6\u6001\u52a0\u5bc6\u7684\u968f\u673a\u5bc6\u94a5\u3002

+usingFile=\u6b63\u5728\u4f7f\u7528 OAuth \u5ba2\u6237\u673a\u7aef\u72b6\u6001\u52a0\u5bc6\u7684 {0} \u6587\u4ef6\u3002

+loadKeyFileFrom=\u6b63\u5728\u88c5\u5165 {0} \u4e2d\u7684 OAuth \u7b7e\u540d\u5bc6\u94a5\u3002

+couldNotLoadKeyFile= \u65e0\u6cd5\u88c5\u5165 {0} \u5bc6\u94a5\u6587\u4ef6\u3002

+couldNotLoadSignedKey=\u672a\u6b63\u786e\u88c5\u5165 OAuth \u7b7e\u540d\u5bc6\u94a5\u3002 \u8981\u521b\u5efa\u5bc6\u94a5\uff1a\n 1. \u8fd0\u884c\u4ee5\u4e0b\u547d\u4ee4\uff1a\n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n. 2. \u7f16\u8f91 shindig.properties \u6587\u4ef6\u5e76\u6dfb\u52a0\u4e0b\u5217\u5404\u884c\uff1a\n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n.

+failedToInit=\u672a\u80fd\u521d\u59cb\u5316 {0} \u4e2d\u7684 OAuth \u4f7f\u7528\u8005\u3002

+

+##OAuthRequest

+oauthFetchFatalError=OAuth \u63d0\u53d6\u5185\u5bb9\u65f6\u53d1\u751f\u4ee5\u4e0b\u81f4\u547d\u9519\u8bef\uff1a\n {0}\u3002

+oauthFetchErrorReprompt=OAuth \u63d0\u53d6\u5185\u5bb9\u65f6\u53d1\u751f\u4ee5\u4e0b\u9519\u8bef\uff1a\n {0}\u3002 \u7cfb\u7edf\u5c06\u91cd\u65b0\u63d0\u793a\u7528\u6237\u8fdb\u884c\u6838\u51c6\u3002

+bogusExpired=\u670d\u52a1\u5668\u8fd4\u56de\u4e86\u65e0\u6548\u7684\u5230\u671f\u65e5\u671f\uff1a\n {0}.

+oauthFetchUnexpectedError=OAuth \u63d0\u53d6\u5185\u5bb9\u65f6\u53d1\u751f\u4ee5\u4e0b\u81f4\u547d\u9519\u8bef\uff1a\n {0}\u3002

+unauthenticatedOauth=\u7531\u4e8e\u4e0d\u5b58\u5728\u7528\u6237\u8ba4\u8bc1\u4fe1\u606f\uff0c\u56e0\u6b64 OAuth \u65e0\u6cd5\u63d0\u53d6\u5185\u5bb9\u3002 \u53d1\u751f\u4e86\u4ee5\u4e0b\u9519\u8bef\uff1a\n {0}\u3002

+invalidOauth=\u7531\u4e8e\u8bf7\u6c42\u65e0\u6548\uff0c\u56e0\u6b64 OAuth \u65e0\u6cd5\u63d0\u53d6\u5185\u5bb9\u3002 \u53d1\u751f\u4e86\u4ee5\u4e0b\u9519\u8bef\uff1a\n {0}\u3002

+

+##CajaCssSanitizer

+failedToParse=\u65e0\u6cd5\u89e3\u6790\u6837\u5f0f\u8868\u3002

+unableToConvertScript=\u65e0\u6cd5\u5c06\u811a\u672c\u8282\u70b9\u8f6c\u6362\u4e3a OpenSocial \u6807\u7b7e\u8bed\u8a00 (OSML) \u6807\u7b7e\u3002

+

+##PipelineExecutor

+errorPreloading=\u6267\u884c\u9884\u88c5\u5165\u65f6\u53d1\u751f\u610f\u5916\u7684\u9519\u8bef\u3002

+

+##Processor

+renderBlacklistedGadget=\u7cfb\u7edf\u8bd5\u56fe\u5448\u793a\u4ee5\u4e0b\u5df2\u52a0\u5165\u9ed1\u540d\u5355\u7684\u5c0f\u914d\u4ef6\uff1a{0}\u3002

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=\u65e0\u6cd5\u68c0\u7d22 {0}\u3002

+failedToRead=\u65e0\u6cd5\u8bfb\u53d6 {0}\u3002

+

+##DefaultServiceFetcher

+httpErrorFetching=\u63d0\u53d6 {1} \u7aef\u70b9\u4e2d\u7684\u670d\u52a1\u65b9\u6cd5\u65f6\u53d1\u751f HTTP {0} \u9519\u8bef\u3002

+failedToFetchService=\u65e0\u6cd5\u63d0\u53d6 {0} \u7aef\u70b9\u4e2d\u7684\u670d\u52a1\u65b9\u6cd5\u3002 \u53d1\u751f\u4e86\u4ee5\u4e0b\u9519\u8bef\uff1a{1}\u3002

+failedToParseService=\u65e0\u6cd5\u89e3\u6790 {0} \u7aef\u70b9\u4e2d\u7684\u670d\u52a1\u65b9\u6cd5\u3002 \u53d1\u751f\u4e86\u4ee5\u4e0b\u9519\u8bef\uff1a{1}\u3002

+

+##Renderer

+FailedToRender=\u672a\u5448\u793a {0} \u5904\u7684\u5c0f\u914d\u4ef6\u3002 \u53d1\u751f\u4e86\u4ee5\u4e0b\u9519\u8bef\uff1a{1}\u3002

+

+##RenderingGadgetRewriter

+unknownFeatures=\u4ee5\u4e0b extern &libs= \u4e2d\u5b58\u5728\u4e00\u4e2a\u6216\u591a\u4e2a\u672a\u77e5\u7684\u529f\u80fd\u90e8\u4ef6\uff1a{0}\u3002

+unexpectedErrorPreloading=\u9884\u88c5\u5165\u5c0f\u914d\u4ef6\u65f6\u53d1\u751f\u610f\u5916\u7684\u9519\u8bef\u3002

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u53d1\u51fa\u4e86\u6e05\u9664\u8bf7\u6c42\uff0c\u4f46\u6ca1\u6709 {0} \u7684\u5185\u5bb9\u7c7b\u578b\u3002

+requestToSanitizeUnknownContent=\u53d1\u51fa\u4e86\u6e05\u9664\u8bf7\u6c42\uff0c\u4f46\u6ca1\u6709 {1} \u7684\u5df2\u77e5\u5185\u5bb9\u7c7b\u578b {0}\u3002

+unableToSanitizeUnknownImg=\u56fe\u50cf\u7c7b\u578b {0} \u672a\u77e5\u5e76\u4e14\u4e0d\u53ef\u6e05\u9664\u3002

+unableToDetectImgType=\u6e05\u9664\u5185\u5bb9\u65f6\u65e0\u6cd5\u68c0\u6d4b\u5230 {0} \u7684\u56fe\u50cf\u7c7b\u578b\u3002

+

+##BasicImageRewriter

+ioErrorRewritingImg=\u91cd\u5199 {0} \u56fe\u50cf\u65f6\u53d1\u751f\u4ee5\u4e0b\u8f93\u5165/\u8f93\u51fa\u9519\u8bef\uff1a{1}\u3002

+unknownErrorRewritingImg=\u91cd\u5199 {0} \u56fe\u50cf\u65f6\u53d1\u751f\u4ee5\u4e0b\u9519\u8bef\uff1a{1}\u3002

+failedToReadImg=\u65e0\u6cd5\u8bfb\u53d6 {0} \u56fe\u50cf\uff0c\u6b63\u5728\u5c06\u5176\u8df3\u8fc7\u3002 \u53d1\u751f\u4e86\u4ee5\u4e0b\u9519\u8bef\uff1a{1}\u3002

+

+##CssResponseRewriter

+cajaCssParseFailure=\u5bf9\u4e8e {1}\uff0c\u89e3\u6790 Caja CSS \u65f6\u53d1\u751f\u4ee5\u4e0b\u9519\u8bef\uff1a{0}\u3002

+

+##ImageAttributeRewriter

+unableToProcessImg=\u65e0\u6cd5\u5904\u7406 {0} \u56fe\u50cf\u8d44\u6e90\u3002

+unableToReadResponse=\u65e0\u6cd5\u8bfb\u53d6 {0} \u56fe\u50cf\u8d44\u6e90\u7684\u54cd\u5e94\u3002

+unableToFetchImg=\u65e0\u6cd5\u63d0\u53d6 {0} \u56fe\u50cf\u8d44\u6e90\u3002

+unableToParseImg=\u65e0\u6cd5\u89e3\u6790 {0} \u56fe\u50cf\u8d44\u6e90\u3002

+

+##MutableContent

+exceptionParsingContent=\u89e3\u6790\u5c0f\u914d\u4ef6\u7684\u5185\u5bb9\u65f6\u53d1\u751f\u5f02\u5e38\u3002

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=\u65e0\u6cd5\u89e3\u6790 {0} \u5904\u7684\u5c0f\u914d\u4ef6\u4ee5\u8fdb\u884c\u9884\u88c5\u5165\u3002

+

+##ProxyingVisitor

+uriExceptionParsing=\u89e3\u6790 {0} \u5904\u7684\u5c0f\u914d\u4ef6\u65f6\u53d1\u751f\u7edf\u4e00\u8d44\u6e90\u6807\u8bc6 (URI) \u5f02\u5e38\u3002

+

+##TemplateRewriter

+malformedTemplateLib=\u56e0\u4e3a\u6a21\u677f\u5e93\u683c\u5f0f\u9519\u8bef\u800c\u53d1\u751f\u5f02\u5e38\u3002

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u521b\u5efa\u4e86\u865a\u62df\u7684\u9ad8\u901f\u7f13\u5b58\u3002

+retrieveReference=\u6b63\u5728\u68c0\u7d22 {0}\u3002

+unableToCajole=\u65e0\u6cd5\u865a\u62df {0} \u5904\u7684\u5c0f\u914d\u4ef6\u3002

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u8bf7\u6c42\u5408\u5e76\u7684\u4ee3\u7406\u65f6\u53d1\u751f\u4ee5\u4e0b\u9519\u8bef\uff1a{0}\u3002

+

+##GadgetRenderingServlet

+malformedTtlValue=\u5df2\u5ffd\u7565\u65e0\u6548\u7684\u751f\u5b58\u65f6\u95f4 (TTL) \u503c {0}\u3002

+

+##HttpRequestHandler

+gadgetCreationError=\u521b\u5efa\u7528\u4e8e\u91cd\u5199\u7684\u5c0f\u914d\u4ef6\u65f6\u53d1\u751f\u9519\u8bef\u3002  \u6b63\u5728\u4e0d\u4f7f\u7528\u5c0f\u914d\u4ef6\u7684\u60c5\u51b5\u4e0b\u91cd\u5199\u54cd\u5e94\u3002

+

+##ProxyServlet

+embededImgWrongDomain=\u5411\u9519\u8bef\u7684\u57df {1} \u53d1\u51fa\u4e86\u8981\u5d4c\u5165 {0} URL \u7684\u8bf7\u6c42\u3002

+

+##DefaultTemplateProcessor

+elFailure={0} \u5904\u7684\u5c0f\u914d\u4ef6\u53d1\u751f\u4ee5\u4e0b EL \u9519\u8bef\uff1a{1}\u3002

+

+##UriUtils

+skipIllegalHeader={0} \u5934\u4e0d\u5408\u6cd5\uff0c\u56e0\u6b64\u6b63\u5728\u5c06\u5176\u8df3\u8fc7\u3002 \u53d1\u751f\u4e86\u4ee5\u4e0b\u9519\u8bef\uff1a{1}\u3002

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=\u66f4\u65b0 {0} \u65f6\u53d1\u751f\u9519\u8bef\u3002 \u8fd4\u56de\u4e86\u72b6\u6001\u7801 {1}\u3002 \u5f02\u5e38\uff1a{2}\u3002 \u6b63\u5728\u6539\u4e3a\u4f7f\u7528\u9ad8\u901f\u7f13\u5b58\u7684\u7248\u672c\u3002

+updateSpecFailureApplyNegCache=\u66f4\u65b0 {0} \u65f6\u53d1\u751f\u9519\u8bef\u3002 \u8fd4\u56de\u4e86\u72b6\u6001\u7801 {1}\u3002 \u5f02\u5e38\uff1a{2}\u3002 \u6b63\u5728\u5e94\u7528\u6d88\u6781\u9ad8\u901f\u7f13\u5b58\u3002

+

+##HashLockedDomainService

+noLockedDomainConfig={0} \u7684\u9501\u5b9a\u57df\u914d\u7f6e\u4e0d\u5b58\u5728\u3002

+

+##Bootstrap

+startingConnManagerWith=\u6b63\u5728\u542f\u52a8\u5177\u6709 {0} \u5c5e\u6027\u7684\u8fde\u63a5\u7ba1\u7406\u5668\u3002

+

+##XSDValidator

+resolveResource=\u6b63\u5728\u89e3\u6790\u4ee5\u4e0b\u8d44\u6e90\uff1a{0}\u3001{1}\u3001{2} \u548c {3}\u3002

+failedToValidate=\u9a8c\u8bc1 {0} \u65f6\u53d1\u751f\u9519\u8bef\u3002

+

+##DefaultRequestPipeline

+cachedResponse=\u5c06\u8bf7\u6c42\u7684\u9ad8\u901f\u7f13\u5b58\u54cd\u5e94\u8fd4\u56de\u7ed9 {0}\u3002

+staleResponse=\u5728 {0} \u4e0a\u8bf7\u6c42\u8d44\u6e90\u65f6\u53d1\u751f\u4e86\u9519\u8bef\uff0c\u4f46\u9ad8\u901f\u7f13\u5b58\u4e2d\u6709\u4e4b\u524d\u7684\u54cd\u5e94\u3002  \u4ece\u9ad8\u901f\u7f13\u5b58\u8fd4\u56de\u53ef\u80fd\u7684\u65e7\u54cd\u5e94\u3002

diff --git a/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_zh_TW.properties b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_zh_TW.properties
new file mode 100644
index 0000000..b2d9904
--- /dev/null
+++ b/trunk/java/common/src/main/resources/org/apache/shindig/common/logging/i18n/resource_zh_TW.properties
@@ -0,0 +1,184 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+

+

+##AuthenticationServletFilter

+errorParsingSecureToken=\u5b89\u5168\u8a18\u865f\u6216\u8a8d\u8b49\u5f62\u614b\u7570\u5e38\uff0c\u7121\u6cd5\u5256\u6790\u3002

+

+##XmlUtil

+errorParsingXML=\u5256\u6790 XML \u6642\u767c\u751f\u932f\u8aa4\u3002 \u9019\u5247\u8a0a\u606f\u53ef\u4ee5\u5ffd\u7565\u3002

+errorParsingExternalGeneralEntities=\u8981\u4f7f\u7528\u7684 XML \u8655\u7406\u5668\u6703\u8f09\u5165\u5916\u90e8\u4e00\u822c\u5be6\u9ad4\u3002

+errorParsingExternalParameterEntities=\u8981\u4f7f\u7528\u7684 XML \u8655\u7406\u5668\u6703\u8f09\u5165\u5916\u90e8\u53c3\u6578\u5be6\u9ad4\u3002

+errorParsingExternalDTD=\u8981\u4f7f\u7528\u7684 XML \u8655\u7406\u5668\u6703\u8f09\u5165\u300c\u6587\u4ef6\u985e\u578b\u5b9a\u7fa9 (DTD)\u300d\u3002

+errorNotUsingSecureXML=\u8981\u4f7f\u7528\u7684 XML \u8655\u7406\u5668\u4e0d\u652f\u63f4\u5b89\u5168\u5256\u6790\u3002

+reuseDocumentBuilders=\u8981\u91cd\u8907\u4f7f\u7528\u6587\u4ef6\u5efa\u7f6e\u5668\u3002

+notReuseDocBuilders=\u4e0d\u91cd\u8907\u4f7f\u7528\u6587\u4ef6\u5efa\u7f6e\u5668\u3002

+

+##LruCacheProvider

+LRUCapacity=\u5df2\u91dd\u5c0d {1} \u914d\u7f6e\u8fd1\u671f\u6700\u5c11\u4f7f\u7528 (LRU) \u5bb9\u91cf {0}\u3002

+

+##DynamicConfigProperty

+evalExpressionFailed=\u7121\u6cd5\u8a55\u4f30 {0}\u3002

+

+##JsonContainerConfigLoader

+readingContainerConfig=\u6b63\u5728\u8b80\u53d6\u5132\u5b58\u5668\u914d\u7f6e {0}\u3002

+loadFromString=\u7121\u6cd5\u5256\u6790 {0}\u3002

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadResourcesFrom=\u6b63\u5728\u8f09\u5165\u4f86\u81ea {0} \u7684\u8cc7\u6e90\u3002

+

+##JsonContainerConfigLoader, FeatureRegistry

+loadFilesFrom=\u6b63\u5728\u8f09\u5165\u4f86\u81ea {0} \u7684\u6a94\u6848\u3002

+

+##ApiServlet

+apiServletProtocolException=\u7531\u65bc\u767c\u751f\u901a\u8a0a\u5354\u5b9a\u7570\u5e38\u72c0\u6cc1\uff0c\u56e0\u6b64\u6b63\u5728\u50b3\u56de\u56de\u61c9\u932f\u8aa4\u3002

+apiServletException=\u7531\u65bc\u767c\u751f\u901a\u8a0a\u5354\u5b9a\u7570\u5e38\u72c0\u6cc1\uff0c\u56e0\u6b64\u6b63\u5728\u50b3\u56de\u56de\u61c9\u932f\u8aa4\u3002

+

+##FeatureRegistry

+overridingFeature=\u6b63\u5728\u7f6e\u63db {1} \u8655\u5177\u6709\u5b9a\u7fa9\u7684 {0} \u529f\u80fd\u3002

+

+##FeatureResourceLoader

+missingFile=\u6a94\u6848 {0} \u904e\u53bb\u5b58\u5728\uff0c\u4f46\u73fe\u5728\u907a\u6f0f\u3002

+unableRetrieveLib=\u7121\u6cd5\u64f7\u53d6\u4f86\u81ea {0} \u7684\u9060\u7aef\u7a0b\u5f0f\u5eab\u3002

+

+##BasicHttpFetcher

+timeoutException=\u7531\u65bc\u767c\u751f\u4e0b\u5217\u7570\u5e38\u72c0\u6cc1\uff0c\u56e0\u6b64 {0} \u5df2\u903e\u6642\uff1a{1} - {2} - {3} \u6beb\u79d2\u3002

+exceptionOccurred=\u63d0\u53d6 {0} \u6642\u767c\u751f\u4e0b\u5217\u7570\u5e38\u72c0\u6cc1\uff1a\u5df2\u7d93\u6b77 {1} \u6beb\u79d2\u3002

+slowResponse={0} \u56de\u61c9\u7de9\u6162\u3002 \u5df2\u7d93\u6b77 {1} \u6beb\u79d2\u3002

+

+##HttpResponseMetadataHelper

+errorGettingMD5=\u53d6\u5f97\u300c\u8a0a\u606f\u6458\u8981 5 (MD5)\u300d\u6642\u767c\u751f\u932f\u8aa4\u3002 \u8a72\u932f\u8aa4\u88ab\u5ffd\u7565\u3002

+errorParsingMD5=\u5256\u6790 UTF-8 \u683c\u5f0f\u7684\u300c\u8a0a\u606f\u6458\u8981 5 (MD5)\u300d\u5b57\u4e32\u6642\u767c\u751f\u932f\u8aa4\u3002

+

+##OAuthModule

+usingRandomKey=\u6b63\u5728\u4f7f\u7528 OAuth \u7528\u6236\u7aef\u72c0\u614b\u52a0\u5bc6\u7684\u96a8\u6a5f\u91d1\u9470\u3002

+usingFile=\u6b63\u5728\u4f7f\u7528 OAuth \u7528\u6236\u7aef\u72c0\u614b\u52a0\u5bc6\u7684 {0} \u6a94\u6848\u3002

+loadKeyFileFrom=\u6b63\u5728\u8f09\u5165\u4f86\u81ea {0} \u7684 OAuth \u7c3d\u7ae0\u91d1\u9470\u3002

+couldNotLoadKeyFile= \u7121\u6cd5\u8f09\u5165 {0} \u91d1\u9470\u6a94\u3002

+couldNotLoadSignedKey=\u672a\u6b63\u78ba\u8f09\u5165 OAuth \u7c3d\u7ae0\u91d1\u9470\u3002 \u82e5\u8981\u5efa\u7acb\u91d1\u9470\uff0c\u8acb\u57f7\u884c\u4e0b\u5217\u52d5\u4f5c\uff1a\n 1. \u57f7\u884c\u4e0b\u5217\u6307\u4ee4\uff1a\n openssl req -newkey rsa:1024 -days 365 -nodes -x509 -keyout testkey.pem \\\n -out testkey.pem -subj ''/CN=mytestkey''\n openssl pkcs8 -in testkey.pem -out oauthkey.pem -topk8 -nocrypt -outform PEM\n\n\u3002 2. \u900f\u904e\u65b0\u589e\u4e0b\u5217\u884c\u4f86\u7de8\u8f2f shindig.properties \u6a94\u6848\uff1a\n{0} =<path-to-oauthkey.pem>\n {1} =mykey\n\u3002

+failedToInit=\u4f86\u81ea {0} \u7684 OAuth \u6d88\u8cbb\u8005\u7121\u6cd5\u8d77\u59cb\u8a2d\u5b9a\u3002

+

+##OAuthRequest

+oauthFetchFatalError=OAuth \u63d0\u53d6\u5167\u5bb9\u6642\u767c\u751f\u4e0b\u5217\u56b4\u91cd\u932f\u8aa4\uff1a\n {0}\u3002

+oauthFetchErrorReprompt=OAuth \u63d0\u53d6\u5167\u5bb9\u6642\u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a\n {0}\u3002 \u6b63\u5728\u91cd\u65b0\u63d0\u793a\u4f7f\u7528\u8005\u9032\u884c\u6838\u51c6\u3002

+bogusExpired=\u4f3a\u670d\u5668\u50b3\u56de\u7121\u6548\u7684\u6709\u6548\u671f\u9650\uff1a\n {0}\u3002

+oauthFetchUnexpectedError=OAuth \u63d0\u53d6\u5167\u5bb9\u6642\u767c\u751f\u4e0b\u5217\u56b4\u91cd\u932f\u8aa4\uff1a\n {0}\u3002

+unauthenticatedOauth=\u7531\u65bc\u4f7f\u7528\u8005\u9451\u5225\u4e0d\u5b58\u5728\uff0c\u56e0\u6b64 OAuth \u7121\u6cd5\u63d0\u53d6\u5167\u5bb9\u3002 \u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a\n {0}\u3002

+invalidOauth=\u7531\u65bc\u8981\u6c42\u7121\u6548\uff0c\u56e0\u6b64 OAuth \u7121\u6cd5\u63d0\u53d6\u5167\u5bb9\u3002 \u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a\n {0}\u3002

+

+##CajaCssSanitizer

+failedToParse=\u7121\u6cd5\u5256\u6790\u6a23\u5f0f\u8868\u3002

+unableToConvertScript=Script \u7bc0\u9ede\u7121\u6cd5\u8f49\u63db\u70ba\u300cOpenSocial \u6a19\u8a18\u8a9e\u8a00 (OSML)\u300d\u6a19\u7c64\u3002

+

+##PipelineExecutor

+errorPreloading=\u9810\u8f09\u6642\u767c\u751f\u975e\u9810\u671f\u7684\u932f\u8aa4\u3002

+

+##Processor

+renderBlacklistedGadget=\u7cfb\u7d71\u5617\u8a66\u5448\u73fe\u4e0b\u5217\u5df2\u52a0\u5165\u9ed1\u540d\u55ae\u7684\u5c0f\u5de5\u5177\uff1a{0}\u3002

+

+##CajaResponseRewriter, CajaContentRewriter

+failedToRetrieve=\u7121\u6cd5\u64f7\u53d6 {0}\u3002

+failedToRead=\u7121\u6cd5\u8b80\u53d6 {0}\u3002

+

+##DefaultServiceFetcher

+httpErrorFetching=\u5f9e {1} \u7aef\u9ede\u63d0\u53d6\u670d\u52d9\u65b9\u6cd5\u6642\u767c\u751f HTTP {0} \u932f\u8aa4\u3002

+failedToFetchService=\u7121\u6cd5\u63d0\u53d6\u4f86\u81ea {0} \u7aef\u9ede\u7684\u670d\u52d9\u65b9\u6cd5\u3002 \u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{1}\u3002

+failedToParseService=\u7121\u6cd5\u5256\u6790\u4f86\u81ea {0} \u7aef\u9ede\u7684\u670d\u52d9\u65b9\u6cd5\u3002 \u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{1}\u3002

+

+##Renderer

+FailedToRender=\u7121\u6cd5\u5448\u73fe {0} \u8655\u7684\u5c0f\u5de5\u5177\u3002 \u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{1}\u3002

+

+##RenderingGadgetRewriter

+unknownFeatures=\u5728\u4e0b\u5217\u5916\u90e8\u6a94 &libs={0} \u4e2d\u5b58\u5728\u4e00\u6216\u591a\u500b\u4e0d\u660e\u529f\u80fd\u3002

+unexpectedErrorPreloading=\u9810\u8f09\u5c0f\u5de5\u5177\u6642\u767c\u751f\u975e\u9810\u671f\u7684\u932f\u8aa4\u3002

+

+##SanitizingResponseRewriter

+requestToSanitizeWithoutContent=\u5be9\u67e5\u8981\u6c42\u5728\u6c92\u6709 {0} \u7684\u5167\u5bb9\u985e\u578b\u7684\u60c5\u6cc1\u4e0b\u767c\u51fa\u3002

+requestToSanitizeUnknownContent=\u5be9\u67e5\u8981\u6c42\u5728\u6c92\u6709 {1} \u7684\u5df2\u77e5\u5167\u5bb9\u985e\u578b {0} \u7684\u60c5\u6cc1\u4e0b\u767c\u51fa\u3002

+unableToSanitizeUnknownImg=\u5f71\u50cf\u985e\u578b {0} \u4e0d\u660e\uff0c\u7121\u6cd5\u5be9\u67e5\u3002

+unableToDetectImgType=\u5be9\u67e5\u5167\u5bb9\u6642\uff0c\u5075\u6e2c\u4e0d\u5230 {0} \u7684\u5f71\u50cf\u985e\u578b\u3002

+

+##BasicImageRewriter

+ioErrorRewritingImg=\u91cd\u65b0\u5beb\u5165 {0} \u5f71\u50cf\u6642\u767c\u751f\u4e0b\u5217\u8f38\u5165/\u8f38\u51fa\u932f\u8aa4\uff1a{1}\u3002

+unknownErrorRewritingImg=\u91cd\u65b0\u5beb\u5165 {0} \u5f71\u50cf\u6642\u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{1}\u3002

+failedToReadImg=\u7121\u6cd5\u8b80\u53d6 {0} \u5f71\u50cf\uff0c\u6b63\u5728\u8df3\u904e\u3002 \u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{1}\u3002

+

+##CssResponseRewriter

+cajaCssParseFailure=\u5256\u6790 Caja CSS \u6642\u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{1} \u7684 {0}\u3002

+

+##ImageAttributeRewriter

+unableToProcessImg=\u7121\u6cd5\u8655\u7406 {0} \u5f71\u50cf\u8cc7\u6e90\u3002

+unableToReadResponse=\u7121\u6cd5\u8b80\u53d6 {0} \u5f71\u50cf\u8cc7\u6e90\u7684\u56de\u61c9\u3002

+unableToFetchImg=\u7121\u6cd5\u63d0\u53d6 {0} \u5f71\u50cf\u8cc7\u6e90\u3002

+unableToParseImg=\u7121\u6cd5\u5256\u6790 {0} \u5f71\u50cf\u8cc7\u6e90\u3002

+

+##MutableContent

+exceptionParsingContent=\u5256\u6790\u5c0f\u5de5\u5177\u7684\u5167\u5bb9\u6642\u767c\u751f\u7570\u5e38\u72c0\u6cc1\u3002

+

+##PipelineDataGadgetRewriter

+failedToParsePreload=\u7121\u6cd5\u5256\u6790 {0} \u8655\u7684\u5c0f\u5de5\u5177\u4ee5\u9032\u884c\u9810\u8f09\u3002

+

+##ProxyingVisitor

+uriExceptionParsing=\u5256\u6790 {0} \u8655\u7684\u5c0f\u5de5\u5177\u6642\u767c\u751f\u300c\u7d71\u4e00\u8cc7\u6e90\u8b58\u5225\u78bc (URI)\u300d\u7570\u5e38\u72c0\u6cc1\u3002

+

+##TemplateRewriter

+malformedTemplateLib=\u7531\u65bc\u7bc4\u672c\u5eab\u5f62\u614b\u7570\u5e38\uff0c\u56e0\u6b64\u767c\u751f\u7570\u5e38\u72c0\u6cc1\u3002

+

+##CajaContnetRewriter

+cajoledCacheCreated=\u5df2\u5efa\u7acb\u8a98\u9a19\u5feb\u53d6\u3002

+retrieveReference=\u6b63\u5728\u64f7\u53d6 {0}\u3002

+unableToCajole=\u7121\u6cd5\u8a98\u9a19 {0} \u8655\u7684\u5c0f\u5de5\u5177\u3002

+

+##ConcatProxyServlet

+concatProxyRequestFailed=\u8981\u6c42\u9023\u7d50\u7684 Proxy \u6642\u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{0}\u3002

+

+##GadgetRenderingServlet

+malformedTtlValue=\u5df2\u5ffd\u7565\u7121\u6548\u7684\u300c\u5b58\u6d3b\u6642\u9593 (TTL)\u300d\u503c {0}\u3002

+

+##HttpRequestHandler

+gadgetCreationError=\u5efa\u7acb\u5c0f\u5de5\u5177\u4ee5\u91cd\u65b0\u64b0\u5beb\u6642\u767c\u751f\u932f\u8aa4\u3002  \u8acb\u5728\u6c92\u6709\u5c0f\u5de5\u5177\u4e0b\u91cd\u65b0\u64b0\u5beb\u56de\u61c9\u3002

+

+##ProxyServlet

+embededImgWrongDomain=\u5c0d\u932f\u8aa4\u7684\u7db2\u57df {1} \u767c\u51fa\u5167\u542b {0} URL \u7684\u8981\u6c42\u3002

+

+##DefaultTemplateProcessor

+elFailure={0} \u8655\u7684\u5c0f\u5de5\u5177\u767c\u751f\u4e0b\u5217 EL \u932f\u8aa4\uff1a{1}\u3002

+

+##UriUtils

+skipIllegalHeader={0} \u6a19\u982d\u7121\u6548\uff0c\u6b63\u5728\u8df3\u904e\u3002 \u767c\u751f\u4e0b\u5217\u932f\u8aa4\uff1a{1}\u3002

+

+##AbstractSpecFactory

+updateSpecFailureUseCacheVersion=\u66f4\u65b0 {0} \u6642\u767c\u751f\u932f\u8aa4\u3002 \u5df2\u50b3\u56de\u72c0\u614b\u78bc {1}\u3002 \u7570\u5e38\u72c0\u6cc1\uff1a{2}\u3002 \u6b63\u5728\u6539\u7528\u5feb\u53d6\u7684\u7248\u672c\u3002

+updateSpecFailureApplyNegCache=\u66f4\u65b0 {0} \u6642\u767c\u751f\u932f\u8aa4\u3002 \u5df2\u50b3\u56de\u72c0\u614b\u78bc {1}\u3002 \u7570\u5e38\u72c0\u6cc1\uff1a{2}\u3002 \u6b63\u5728\u5957\u7528\u8ca0\u5feb\u53d6\u3002

+

+##HashLockedDomainService

+noLockedDomainConfig={0} \u7684\u9396\u5b9a\u7db2\u57df\u914d\u7f6e\u4e0d\u5b58\u5728\u3002

+

+##Bootstrap

+startingConnManagerWith=\u6b63\u5728\u555f\u52d5\u5177\u6709 {0} \u5167\u5bb9\u7684\u9023\u7dda\u7ba1\u7406\u7a0b\u5f0f\u3002

+

+##XSDValidator

+resolveResource=\u6b63\u5728\u89e3\u6790\u4e0b\u5217\u8cc7\u6e90\uff1a{0}\u3001{1}\u3001{2}\u3001{3}\u3002

+failedToValidate=\u9a57\u8b49 {0} \u6642\u767c\u751f\u932f\u8aa4\u3002

+

+##DefaultRequestPipeline

+cachedResponse=\u5c07\u8981\u6c42\u7684\u5feb\u53d6\u56de\u61c9\u50b3\u56de {0}\u3002

+staleResponse=\u5728 {0} \u8981\u6c42\u8cc7\u6e90\u6642\u767c\u751f\u932f\u8aa4\uff0c\u4f46\u662f\u6211\u5011\u5df2\u7d93\u5728\u5feb\u53d6\u4e2d\u6709\u4e8b\u5148\u56de\u61c9\u3002  \u5f9e\u5feb\u53d6\u50b3\u56de\u53ef\u80fd\u7684\u820a\u56de\u61c9\u3002

diff --git a/trunk/java/common/src/test/java/org/apache/shindig/auth/AuthInfoUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/auth/AuthInfoUtilTest.java
new file mode 100644
index 0000000..6737c6f
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/auth/AuthInfoUtilTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class AuthInfoUtilTest extends Assert {
+
+  @Test
+  public void testToken() throws Exception {
+    HttpServletRequest req = new FakeHttpServletRequest();
+    SecurityToken token = new FakeGadgetToken();
+
+    AuthInfoUtil.setSecurityTokenForRequest(req, token);
+
+    assertEquals(token, AuthInfoUtil.getSecurityTokenFromRequest(req));
+  }
+
+  @Test
+  public void testAuthType() throws Exception {
+    HttpServletRequest req = new FakeHttpServletRequest();
+
+    AuthInfoUtil.setAuthTypeForRequest(req, "FakeAuth");
+
+    assertEquals("FakeAuth", AuthInfoUtil.getAuthTypeFromRequest(req));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/auth/AuthenticationServletFilterTest.java b/trunk/java/common/src/test/java/org/apache/shindig/auth/AuthenticationServletFilterTest.java
new file mode 100644
index 0000000..c3e9492
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/auth/AuthenticationServletFilterTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.servlet.HttpServletResponseRecorder;
+
+import com.google.common.collect.ImmutableList;
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.ServletException;
+
+public class AuthenticationServletFilterTest extends EasyMockTestCase {
+  private static final String TEST_AUTH_HEADER = "Test Authentication Header";
+
+  private AuthenticationServletFilter filter;
+
+  private HttpServletRequest request;
+  private HttpServletResponse response;
+  private HttpServletResponseRecorder recorder;
+  private FilterChain chain;
+  private AuthenticationHandler nullStHandler;
+
+  @Before
+  public void setup() {
+    request = mock(HttpServletRequest.class);
+    response  = mock(HttpServletResponse.class);
+    recorder = new HttpServletResponseRecorder(response);
+    chain = mock(FilterChain.class);
+    filter = new AuthenticationServletFilter();
+    nullStHandler = new NullSecurityTokenAuthenticationHandler();
+  }
+
+  @Test(expected = ServletException.class)
+  public void testDoFilter_BadArgs() throws Exception {
+    filter.doFilter(null, null, null);
+  }
+
+  @Test
+  public void testNullSecurityToken() throws Exception {
+    filter.setAuthenticationHandlers(ImmutableList.<AuthenticationHandler>of(nullStHandler));
+    filter.doFilter(request, recorder, chain);
+    assertEquals(TEST_AUTH_HEADER,
+        recorder.getHeader(AuthenticationServletFilter.WWW_AUTHENTICATE_HEADER));
+  }
+
+  private static class NullSecurityTokenAuthenticationHandler implements AuthenticationHandler {
+    public String getName() {
+      return "TestAuth";
+    }
+
+    public SecurityToken getSecurityTokenFromRequest(HttpServletRequest request)
+        throws InvalidAuthenticationException {
+      return null;
+    }
+
+    public String getWWWAuthenticateHeader(String realm) {
+      return TEST_AUTH_HEADER;
+    }
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/auth/BasicSecurityTokenCodecTest.java b/trunk/java/common/src/test/java/org/apache/shindig/auth/BasicSecurityTokenCodecTest.java
new file mode 100644
index 0000000..b081160
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/auth/BasicSecurityTokenCodecTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Map;
+
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+
+public class BasicSecurityTokenCodecTest {
+
+  private BasicSecurityTokenCodec codec;
+  private ContainerConfig config;
+
+  @Before
+  public void setUp() throws Exception {
+    config = new BasicContainerConfig();
+    codec = new BasicSecurityTokenCodec(config);
+  }
+
+  @Test
+  public void testGetTokenTimeToLive() throws Exception {
+    Builder<String, Object> builder = ImmutableMap.builder();
+    Map<String, Object> container = builder
+            .put(ContainerConfig.CONTAINER_KEY, ImmutableList.of("default", "tokenTest"))
+            .put(SecurityTokenCodec.SECURITY_TOKEN_TTL_CONFIG, Integer.valueOf(300)).build();
+
+    config.newTransaction().addContainer(container).commit();
+    assertEquals("Token TTL matches what is set in the container config", 300,
+            codec.getTokenTimeToLive("tokenTest"));
+    assertEquals("Token TTL matches the default TTL", AbstractSecurityToken.DEFAULT_MAX_TOKEN_TTL,
+            codec.getTokenTimeToLive());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/auth/BlobCrypterSecurityTokenCodecTest.java b/trunk/java/common/src/test/java/org/apache/shindig/auth/BlobCrypterSecurityTokenCodecTest.java
new file mode 100644
index 0000000..e0fbf2d
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/auth/BlobCrypterSecurityTokenCodecTest.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.shindig.auth.AbstractSecurityToken.Keys;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
+
+/**
+ * Tests for BlobCrypterSecurityTokenCodec
+ */
+public class BlobCrypterSecurityTokenCodecTest {
+
+  private BlobCrypterSecurityTokenCodec codec;
+  private FakeTimeSource timeSource;
+  private ContainerConfig config;
+
+  @Before
+  public void setUp() throws Exception {
+    config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(makeContainer("default"))
+        .addContainer(makeContainer("container"))
+        .addContainer(makeContainer("example"))
+        .commit();
+    codec = new BlobCrypterSecurityTokenCodec(config);
+    timeSource = new FakeTimeSource();
+  }
+
+  protected Map<String, Object> makeContainer(String container) {
+    return ImmutableMap.<String, Object>of(ContainerConfig.CONTAINER_KEY,
+        ImmutableList.of(container),
+        BlobCrypterSecurityTokenCodec.SECURITY_TOKEN_KEY,
+        getContainerKey(container),
+        BlobCrypterSecurityTokenCodec.SIGNED_FETCH_DOMAIN,
+        container + ".com");
+  }
+
+  protected String getContainerKey(String container) {
+    return "KEY FOR CONTAINER " + container;
+  }
+
+  protected BlobCrypter getBlobCrypter(String key) {
+    BasicBlobCrypter c = new BasicBlobCrypter(key);
+    c.timeSource = timeSource;
+    return c;
+  }
+
+  @Test
+  public void testCreateToken() throws Exception {
+    Map<String, String> values = new HashMap<String, String>();
+    values.put(Keys.APP_URL.getKey(), "http://www.example.com/gadget.xml");
+    values.put(Keys.MODULE_ID.getKey(), Long.toString(12345L, 10));
+    values.put(Keys.OWNER.getKey(), "owner");
+    values.put(Keys.VIEWER.getKey(), "viewer");
+    values.put(Keys.TRUSTED_JSON.getKey(), "trusted");
+
+    BlobCrypterSecurityToken t = new BlobCrypterSecurityToken("container", null, null, values);
+    String encrypted = t.getContainer() + ":" + getBlobCrypter(getContainerKey("container")).wrap(t.toMap());
+
+    SecurityToken t2 = codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, encrypted));
+
+    assertEquals("http://www.example.com/gadget.xml", t2.getAppId());
+    assertEquals("http://www.example.com/gadget.xml", t2.getAppUrl());
+    assertEquals("container.com", t2.getDomain());
+    assertEquals(12345L, t2.getModuleId());
+    assertEquals("owner", t2.getOwnerId());
+    assertEquals("viewer", t2.getViewerId());
+    assertEquals("trusted", t2.getTrustedJson());
+  }
+
+  @Test
+  public void testUnknownContainer() throws Exception {
+    Map<String, String> values = new HashMap<String, String>();
+    values.put(Keys.APP_URL.getKey(), "http://www.example.com/gadget.xml");
+    values.put(Keys.MODULE_ID.getKey(), Long.toString(12345L, 10));
+    values.put(Keys.OWNER.getKey(), "owner");
+    values.put(Keys.VIEWER.getKey(), "viewer");
+    values.put(Keys.TRUSTED_JSON.getKey(), "trusted");
+
+    BlobCrypterSecurityToken t = new BlobCrypterSecurityToken("container", null, null, values);
+    String encrypted = t.getContainer() + ":" + getBlobCrypter(getContainerKey("container")).wrap(t.toMap());
+    encrypted = encrypted.replace("container:", "other:");
+
+    try {
+      codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, encrypted));
+      fail("should have reported that container was unknown");
+    } catch (SecurityTokenException e) {
+      assertTrue(e.getMessage(), e.getMessage().contains("Unknown container"));
+    }
+  }
+
+  @Test
+  public void testWrongContainer() throws Exception {
+    Map<String, String> values = new HashMap<String, String>();
+    values.put(Keys.APP_URL.getKey(), "http://www.example.com/gadget.xml");
+    values.put(Keys.MODULE_ID.getKey(), Long.toString(12345L, 10));
+    values.put(Keys.OWNER.getKey(), "owner");
+    values.put(Keys.VIEWER.getKey(), "viewer");
+    values.put(Keys.TRUSTED_JSON.getKey(), "trusted");
+
+    BlobCrypterSecurityToken t = new BlobCrypterSecurityToken("container", null, null, values);
+    String encrypted = t.getContainer() + ":" + getBlobCrypter(getContainerKey("container")).wrap(t.toMap());
+    encrypted = encrypted.replace("container:", "example:");
+
+    try {
+      codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, encrypted));
+      fail("should have tried to decrypt with wrong key");
+    } catch (SecurityTokenException e) {
+      assertTrue(e.getMessage(), e.getMessage().contains("Invalid token signature"));
+    }
+  }
+
+  @Test
+  public void testExpired() throws Exception {
+    Map<String, String> values = new HashMap<String, String>();
+    values.put(Keys.APP_URL.getKey(), "http://www.example.com/gadget.xml");
+    values.put(Keys.MODULE_ID.getKey(), Long.toString(12345L, 10));
+    values.put(Keys.OWNER.getKey(), "owner");
+    values.put(Keys.VIEWER.getKey(), "viewer");
+    values.put(Keys.TRUSTED_JSON.getKey(), "trusted");
+
+    BlobCrypterSecurityToken token = new BlobCrypterSecurityToken("container", null, null, values);
+    token.setTimeSource(timeSource);
+    timeSource.incrementSeconds(-1 * (codec.getTokenTimeToLive("container") + 181)); // one hour plus clock skew
+    String encrypted = codec.encodeToken(token);
+    try {
+      codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, encrypted));
+      fail("should have expired");
+    } catch (SecurityTokenException e) {
+      assertTrue(e.getMessage(), e.getMessage().contains("Blob expired"));
+    }
+  }
+
+  @Test
+  public void testMalformed() throws Exception {
+    try {
+      codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, "foo"));
+      fail("should have tried to decrypt with wrong key");
+    } catch (SecurityTokenException e) {
+      assertTrue(e.getMessage(), e.getMessage().contains("Invalid security token foo"));
+    }
+  }
+
+  @Test
+  public void testAnonymous() throws Exception {
+    SecurityToken t = codec.createToken(
+        ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, "   "));
+    assertTrue(t.isAnonymous());
+
+    Map<String, String> empty = ImmutableMap.of();
+    t = codec.createToken(empty);
+    assertTrue(t.isAnonymous());
+  }
+
+  @Test
+  public void testChangingContainers() throws Exception {
+    String newContainer = "newcontainer";
+    Map<String, String> values = new HashMap<String, String>();
+    values.put(Keys.APP_URL.getKey(), "http://www.example.com/gadget.xml");
+    values.put(Keys.MODULE_ID.getKey(), Long.toString(12345L, 10));
+    values.put(Keys.OWNER.getKey(), "owner");
+    values.put(Keys.VIEWER.getKey(), "viewer");
+    values.put(Keys.TRUSTED_JSON.getKey(), "trusted");
+
+    BlobCrypterSecurityToken t = new BlobCrypterSecurityToken(newContainer, null, null, values);
+    String encrypted = t.getContainer() + ":" + getBlobCrypter(getContainerKey(newContainer)).wrap(t.toMap());
+
+    // fails when trying to create a token for a non-existing container
+    try {
+      codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, encrypted));
+      fail("Should have thrown a SecurityTokenException");
+    } catch (SecurityTokenException e) {
+      // pass
+    }
+    // add the container, now it should succeed
+    config.newTransaction().addContainer(makeContainer(newContainer)).commit();
+    codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, encrypted));
+    // remove the token, now it should fail again
+    config.newTransaction().removeContainer(newContainer).commit();
+    try {
+      codec.createToken(ImmutableMap.of(SecurityTokenCodec.SECURITY_TOKEN_NAME, encrypted));
+      fail("Should have thrown a SecurityTokenException");
+    } catch (SecurityTokenException e) {
+      // pass
+    }
+  }
+
+  @Test
+  public void testGetTokenTimeToLive() throws Exception {
+    Builder<String, Object> builder = ImmutableMap.builder();
+    Map<String, Object> container = builder.putAll(makeContainer("tokenTest"))
+            .put(SecurityTokenCodec.SECURITY_TOKEN_TTL_CONFIG, Integer.valueOf(300)).build();
+
+    config.newTransaction().addContainer(container).commit();
+    assertEquals("Token TTL matches what is set in the container config", 300,
+            codec.getTokenTimeToLive("tokenTest"));
+    assertEquals("Token TTL matches the default TTL", AbstractSecurityToken.DEFAULT_MAX_TOKEN_TTL,
+            codec.getTokenTimeToLive());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/auth/BlobCrypterSecurityTokenTest.java b/trunk/java/common/src/test/java/org/apache/shindig/auth/BlobCrypterSecurityTokenTest.java
new file mode 100644
index 0000000..77ed906
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/auth/BlobCrypterSecurityTokenTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.AbstractSecurityToken.Keys;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests BlobCrypterSecurityToken
+ */
+public class BlobCrypterSecurityTokenTest {
+
+  private static final String CONTAINER = "container";
+  private static final String DOMAIN = "example.com";
+
+  private FakeTimeSource timeSource = new FakeTimeSource();
+  private BasicBlobCrypter crypter;
+
+  @Before
+  public void setUp() {
+    crypter = new BasicBlobCrypter(Crypto.getRandomBytes(20));
+    crypter.timeSource = timeSource;
+  }
+
+  @Test
+  public void testNullValues() throws Exception {
+    BlobCrypterSecurityToken t = new BlobCrypterSecurityToken(CONTAINER, DOMAIN, null, null);
+    String token = t.getContainer() + ":" + crypter.wrap(t.toMap());
+    assertTrue("should start with container: " + token, token.startsWith("container:"));
+    String[] fields = StringUtils.split(token, ':');
+    BlobCrypterSecurityToken t2 = new BlobCrypterSecurityToken(CONTAINER, DOMAIN, null, crypter.unwrap(fields[1]));
+
+    assertNull(t2.getAppId(), t2.getAppId());
+    assertNull(t2.getAppUrl(), t2.getAppUrl());
+    assertEquals(DOMAIN, t2.getDomain());
+    assertEquals(0, t2.getModuleId());
+    assertNull(t2.getOwnerId(), t2.getOwnerId());
+    assertNull(t2.getViewerId(), t2.getViewerId());
+    assertNull(t2.getTrustedJson(), t2.getTrustedJson());
+    assertNull(t2.getUpdatedToken(), t2.getUpdatedToken());
+    assertEquals(CONTAINER, t2.getContainer());
+    assertNull(t2.getActiveUrl(), t2.getActiveUrl());
+  }
+
+  @Test
+  public void testRealValues() throws Exception {
+    Map<String, String> values = new HashMap<String, String>();
+    values.put(Keys.APP_URL.getKey(), "http://www.example.com/gadget.xml");
+    values.put(Keys.MODULE_ID.getKey(), Long.toString(12345L, 10));
+    values.put(Keys.OWNER.getKey(), "owner");
+    values.put(Keys.VIEWER.getKey(), "viewer");
+    values.put(Keys.TRUSTED_JSON.getKey(), "trusted");
+
+    BlobCrypterSecurityToken t = new BlobCrypterSecurityToken(CONTAINER, DOMAIN, null, values);
+    String token = t.getContainer() + ":" + crypter.wrap(t.toMap());
+    assertTrue("should start with container: " + token, token.startsWith("container:"));
+    String[] fields = StringUtils.split(token, ':');
+    BlobCrypterSecurityToken t2 = new BlobCrypterSecurityToken(CONTAINER, DOMAIN, "active", crypter.unwrap(fields[1]));
+    assertEquals("http://www.example.com/gadget.xml", t2.getAppId());
+    assertEquals("http://www.example.com/gadget.xml", t2.getAppUrl());
+    assertEquals(DOMAIN, t2.getDomain());
+    assertEquals(12345L, t2.getModuleId());
+    assertEquals("owner", t2.getOwnerId());
+    assertEquals("viewer", t2.getViewerId());
+    assertEquals("trusted", t2.getTrustedJson());
+    assertEquals(CONTAINER, t2.getContainer());
+    assertEquals("active", t2.getActiveUrl());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/auth/DefaultSecurityTokenCodecTest.java b/trunk/java/common/src/test/java/org/apache/shindig/auth/DefaultSecurityTokenCodecTest.java
new file mode 100644
index 0000000..f491501
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/auth/DefaultSecurityTokenCodecTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.shindig.config.BasicContainerConfig;
+import org.junit.Test;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Tests of DefaultSecurityTokenCodec
+ */
+public class DefaultSecurityTokenCodecTest {
+
+  private static class FakeContainerConfig extends BasicContainerConfig {
+    private final String tokenType;
+
+    public FakeContainerConfig(String tokenType) {
+      this.tokenType = tokenType;
+    }
+
+    @Override
+    public Object getProperty(String container, String parameter) {
+      if ("gadgets.securityTokenType".equals(parameter)) {
+        if ("default".equals(container)) {
+          return tokenType;
+        }
+      } else if ("gadgets.securityTokenKey".equals(parameter)) {
+        return "container key file: " + container;
+      }
+      return null;
+    }
+
+    @Override
+    public Collection<String> getContainers() {
+      return Lists.newArrayList("somecontainer");
+    }
+  }
+
+  @Test
+  public void testBasicDecoder() throws Exception {
+    DefaultSecurityTokenCodec codec = new DefaultSecurityTokenCodec(
+        new FakeContainerConfig("insecure"));
+    Long expires = System.currentTimeMillis() / 1000 + 500; // 50 seconds in the future
+    String token = "o:v:app:domain:appurl:12345:container:" +  Long.toString(expires, 10);
+    Map<String, String> parameters = Collections.singletonMap(
+        SecurityTokenCodec.SECURITY_TOKEN_NAME, token);
+    SecurityToken st = codec.createToken(parameters);
+    assertEquals("o", st.getOwnerId());
+    assertEquals("v", st.getViewerId());
+    assertEquals("appurl", st.getAppUrl());
+    assertEquals("container", st.getContainer());
+    assertEquals(expires, st.getExpiresAt());
+  }
+
+  @Test
+  public void testInvalidDecoder() throws Exception {
+    try {
+      new DefaultSecurityTokenCodec(new FakeContainerConfig("garbage"));
+      fail("Should have thrown");
+    } catch (RuntimeException e) {
+      assertTrue("exception should contain garbage: " + e, e.getMessage().contains("garbage"));
+    }
+  }
+
+  @Test
+  public void testNullDecoder() throws Exception {
+    try {
+      new DefaultSecurityTokenCodec(new FakeContainerConfig(null));
+      fail("Should have thrown");
+    } catch (RuntimeException e) {
+      assertTrue("exception should contain null: " + e, e.getMessage().contains("null"));
+    }
+  }
+
+  @Test
+  public void testRealDecoder() throws Exception {
+    // Just verifies that "secure" tokens get routed to the right decoder class.
+    DefaultSecurityTokenCodec securityTokenCodec = new DefaultSecurityTokenCodec(new FakeContainerConfig("secure"));
+    assertTrue(securityTokenCodec.getCodec() instanceof BlobCrypterSecurityTokenCodec);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/auth/UrlParameterAuthenticationHandlerTest.java b/trunk/java/common/src/test/java/org/apache/shindig/auth/UrlParameterAuthenticationHandlerTest.java
new file mode 100644
index 0000000..a121f5a
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/auth/UrlParameterAuthenticationHandlerTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.auth;
+
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+public class UrlParameterAuthenticationHandlerTest {
+  SecurityToken expectedToken;
+  UrlParameterAuthenticationHandler authHandler;
+  SecurityTokenCodec codec;
+  HttpServletRequest req;
+
+  @Before
+  public void setup() throws Exception {
+    expectedToken = new BasicSecurityToken(
+        "owner", "viewer", "app",
+        "domain", "appUrl", "0", "container", "activeUrl", 1000L);
+    // Mock token codec
+    codec = new SecurityTokenCodec() {
+      public SecurityToken createToken(Map<String, String> tokenParameters) throws SecurityTokenException {
+        return tokenParameters == null ? null :
+               "1234".equals(tokenParameters.get(SecurityTokenCodec.SECURITY_TOKEN_NAME)) ? expectedToken : null;
+      }
+
+      public String encodeToken(SecurityToken token) throws SecurityTokenException {
+        return null;
+      }
+
+      public int getTokenTimeToLive() {
+        return 0; // Not used.
+      }
+
+      public int getTokenTimeToLive(String container) {
+        return 0; // Not used.
+      }
+    };
+
+    authHandler = new UrlParameterAuthenticationHandler(codec, true);
+  }
+
+  @Test
+  public void testGetSecurityTokenFromRequest() throws Exception {
+    Assert.assertEquals(authHandler.getName(), AuthenticationMode.SECURITY_TOKEN_URL_PARAMETER.name());
+  }
+
+  @Test
+  public void testInvalidRequests() throws Exception {
+    // Empty request
+    req = new FakeHttpServletRequest();
+    Assert.assertNull(authHandler.getSecurityTokenFromRequest(req));
+
+    // Old behavior, no longer supported
+    req = new FakeHttpServletRequest().setHeader("Authorization", "Token token=\"1234\"");
+    Assert.assertNull(authHandler.getSecurityTokenFromRequest(req));
+
+    req = new FakeHttpServletRequest().setHeader("Authorization", "OAuth 1234");
+    Assert.assertNull(authHandler.getSecurityTokenFromRequest(req));
+  }
+
+  @Test
+  public void testSecurityToken() throws Exception {
+    // security token in request
+    req = new FakeHttpServletRequest("http://example.org/rpc?st=1234");
+    Assert.assertEquals(expectedToken, authHandler.getSecurityTokenFromRequest(req));
+  }
+
+  @Test
+  public void testOAuth1() throws Exception {
+    // An OAuth 1.0 request, we should not process this.
+    req = new FakeHttpServletRequest()
+        .setHeader("Authorization", "OAuth oauth_signature_method=\"RSA-SHA1\"");
+    SecurityToken token = authHandler.getSecurityTokenFromRequest(req);
+    Assert.assertNull(token);
+  }
+
+  @Test
+  public void testOAuth2Header() throws Exception {
+    req = new FakeHttpServletRequest("https://www.example.org/")
+        .setHeader("Authorization", "OAuth2  1234");
+    Assert.assertEquals(expectedToken, authHandler.getSecurityTokenFromRequest(req));
+
+    req = new FakeHttpServletRequest("https://www.example.org/")
+        .setHeader("Authorization", "   OAuth2    1234 ");
+    Assert.assertEquals(expectedToken, authHandler.getSecurityTokenFromRequest(req));
+
+    req = new FakeHttpServletRequest("https://www.example.org/")
+        .setHeader("Authorization", "OAuth2 1234 x=1,y=\"2 2 2\"");
+    Assert.assertEquals(expectedToken, authHandler.getSecurityTokenFromRequest(req));
+
+    req = new FakeHttpServletRequest("http://www.example.org/")
+        .setHeader("Authorization", "OAuth2 1234");
+    Assert.assertNull(authHandler.getSecurityTokenFromRequest(req));
+  }
+
+  @Test
+  public void testOAuth2Param() throws Exception
+  {
+    req = new FakeHttpServletRequest("https://www.example.com?oauth_token=1234");
+    Assert.assertEquals(expectedToken, authHandler.getSecurityTokenFromRequest(req));
+
+    req = new FakeHttpServletRequest("https://www.example.com?oauth_token=1234&oauth_signature_method=RSA-SHA1");
+    Assert.assertNull(authHandler.getSecurityTokenFromRequest(req));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/AllTests.java b/trunk/java/common/src/test/java/org/apache/shindig/common/AllTests.java
new file mode 100644
index 0000000..f076eb7
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/AllTests.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+import junitx.util.DirectorySuiteBuilder;
+import junitx.util.SimpleTestFilter;
+
+/**
+ * Run all gadgets tests.
+ */
+public class AllTests extends TestSuite {
+
+  public static Test suite() throws Exception {
+    DirectorySuiteBuilder builder = new DirectorySuiteBuilder(
+      new SimpleTestFilter());
+    return builder.suite("target/test-classes");
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/EasyMockTestCase.java b/trunk/java/common/src/test/java/org/apache/shindig/common/EasyMockTestCase.java
new file mode 100644
index 0000000..de8c35c
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/EasyMockTestCase.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import com.google.common.collect.Lists;
+
+import org.easymock.EasyMock;
+import org.easymock.IMockBuilder;
+import org.junit.Assert;
+
+import java.lang.reflect.Method;
+import java.util.List;
+
+
+public abstract class EasyMockTestCase extends Assert {
+  /** Tracks all EasyMock objects created for a test. */
+  private final List<Object> mocks = Lists.newArrayList();
+
+  /**
+   * Creates a strict mock object for the given class, adds it to the internal
+   * list of all mocks, and returns it.
+   *
+   * @param clazz Class to be mocked.
+   * @return A mock instance of the given type.
+   **/
+  protected <T> T mock(Class<T> clazz) {
+    return mock(clazz, false);
+  }
+
+  /**
+   * Creates a strict or nice mock object for the given class, adds it to the internal
+   * list of all mocks, and returns it.
+   *
+   * @param clazz Class to be mocked.
+   * @param strict whether or not to make a strict mock
+   * @return A mock instance of the given type.
+   **/
+  protected <T> T mock(Class<T> clazz, boolean strict) {
+    T m = strict ? EasyMock.createMock(clazz) : EasyMock.createNiceMock(clazz);
+    mocks.add(m);
+    return m;
+  }
+
+  /**
+   * Creates a nice mock object for the given class, adds it to the internal
+   * list of all mocks, and returns it.
+   *
+   * @param clazz Class to be mocked.
+   * @return A mock instance of the given type.
+   **/
+
+
+  protected <T> T mock(Class<T> clazz, Method[] methods) {
+    return mock(clazz, methods, false);
+  }
+
+
+  /**
+   * Creates a strict mock object for the given class, adds it to the internal
+   * list of all mocks, and returns it.
+   *
+   * @param clazz Class to be mocked.
+   * @return A mock instance of the given type.
+   **/
+
+  protected <T> T mock(Class<T> clazz, Method[] methods, boolean strict) {
+    IMockBuilder<T> builder = EasyMock.createMockBuilder(clazz).addMockedMethods(methods);
+
+    T m = strict ? builder.createMock() : builder.createNiceMock();
+    mocks.add(m);
+
+    return m;
+  }
+
+  /**
+   * Sets each mock to replay mode in the order they were created. Call this after setting
+   * all of the mock expectations for a test.
+   */
+  protected void replay() {
+    EasyMock.replay(mocks.toArray());
+  }
+
+  protected void replay(Object mock) {
+    EasyMock.replay(mock);
+  }
+
+  /**
+   * Verifies each mock in the order they were created. Call this at the end of each test
+   * to verify the expectations were satisfied.
+   */
+  protected void verify() {
+    EasyMock.verify(mocks.toArray());
+  }
+
+  /**
+   * Resets all of the mocks.
+   */
+  protected void reset() {
+    EasyMock.reset(mocks.toArray());
+  }
+
+  protected void reset(Object mock) {
+    EasyMock.reset(mock);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/JsonAssert.java b/trunk/java/common/src/test/java/org/apache/shindig/common/JsonAssert.java
new file mode 100644
index 0000000..64755c9
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/JsonAssert.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+public final class JsonAssert {
+  private JsonAssert() {}
+
+  public static void assertJsonArrayEquals(JSONArray expected, JSONArray actual) throws Exception {
+    assertJsonArrayEquals(null, expected, actual);
+  }
+
+  public static void assertJsonArrayEquals(String message, JSONArray expected, JSONArray actual)
+          throws Exception {
+    if (expected.length() != actual.length()) {
+      assertEquals("Arrays are not of equal length", expected.toString(), actual.toString());
+    }
+
+    for (int i = 0; i < expected.length(); ++i) {
+      Object expectedValue = expected.opt(i);
+      Object actualValue = actual.opt(i);
+
+      assertSame(expected.toString() + " != " + actual.toString(), expectedValue.getClass(),
+              actualValue.getClass());
+
+      if (expectedValue instanceof JSONObject) {
+        assertJsonObjectEquals(message, (JSONObject) expectedValue, (JSONObject) actualValue);
+      } else if (expectedValue instanceof JSONArray) {
+        assertJsonArrayEquals(message, (JSONArray) expectedValue, (JSONArray) actualValue);
+      } else {
+        assertEquals(expectedValue, actualValue);
+      }
+    }
+  }
+
+  public static void assertJsonObjectEquals(JSONObject expected, JSONObject actual)
+          throws Exception {
+    assertJsonObjectEquals(null, expected, actual);
+  }
+
+  public static void assertJsonObjectEquals(String message, JSONObject expected, JSONObject actual)
+          throws Exception {
+    if (expected.length() != actual.length()) {
+      assertEquals("Objects are not of equal size", expected.toString(2), actual.toString(2));
+    }
+
+    // Both are empty so skip
+    if (JSONObject.getNames(expected) == null && JSONObject.getNames(actual) == null) {
+      return;
+    }
+    for (String name : JSONObject.getNames(expected)) {
+      Object expectedValue = expected.opt(name);
+      Object actualValue = actual.opt(name);
+
+      if (expectedValue != null) {
+        assertNotNull(expected.toString() + " != " + actual.toString(), actualValue);
+      }
+      assertSame(expected.toString() + " != " + actual.toString(), expectedValue.getClass(),
+              actualValue.getClass());
+
+      if (expectedValue instanceof JSONObject) {
+        assertJsonObjectEquals(message, (JSONObject) expectedValue, (JSONObject) actualValue);
+      } else if (expectedValue instanceof JSONArray) {
+        assertJsonArrayEquals(message, (JSONArray) expectedValue, (JSONArray) actualValue);
+      } else {
+        assertEquals(expectedValue, actualValue);
+      }
+    }
+  }
+
+  public static void assertJsonEquals(String expected, String actual) throws Exception {
+    assertJsonEquals(null, expected, actual);
+  }
+
+  public static void assertJsonEquals(String message, String expected, String actual)
+          throws Exception {
+    switch (expected.charAt(0)) {
+    case '{':
+      assertJsonObjectEquals(message, new JSONObject(expected), new JSONObject(actual));
+      break;
+    case '[':
+      assertJsonArrayEquals(message, new JSONArray(expected), new JSONArray(actual));
+      break;
+    default:
+      assertEquals(expected, actual);
+      break;
+    }
+  }
+
+  public static void assertObjectEquals(Object expected, Object actual) throws Exception {
+    assertObjectEquals(null, expected, actual);
+  }
+
+  public static void assertObjectEquals(String message, Object expected, Object actual)
+          throws Exception {
+    if (!(expected instanceof String)) {
+      expected = JsonSerializer.serialize(expected);
+    }
+
+    if (!(actual instanceof String)) {
+      actual = JsonSerializer.serialize(actual);
+    }
+
+    assertJsonEquals(message, (String) expected, (String) actual);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java
new file mode 100644
index 0000000..3e1959d
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/JsonSerializerTest.java
@@ -0,0 +1,333 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import static org.apache.shindig.common.JsonAssert.assertJsonEquals;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.base.Strings;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.LinkedHashMultimap;
+
+/**
+ * Tests for JsonSerializer.
+ *
+ * This class may be executed to perform micro benchmarks comparing the performance of the
+ * serializer with that of json.org and net.sf.json.
+ */
+public class JsonSerializerTest {
+
+  private static final String JSON_POJO_AS_JSON = "{string:'string-value',integer:100,'simple!':3}";
+
+  @Test
+  public void serializeSimpleJsonObject() throws Exception {
+    String json = "{foo:'bar'}";
+    assertJsonEquals(json, JsonSerializer.serialize(new JSONObject(json)));
+  }
+
+  @Test
+  public void serializeSimpleMap() throws Exception {
+    Map<String, String> map = new HashMap<String, String>(3, 1);
+    map.put("hello", "world");
+    map.put("foo", "bar");
+    map.put("remove", null);
+    assertJsonEquals("{hello:'world',foo:'bar'}", JsonSerializer.serialize(map));
+  }
+
+  @Test
+  public void serializeSimpleMultimap() throws Exception {
+    Multimap<String, String> map = LinkedHashMultimap.create();
+    Set<String> methods = ImmutableSet.of("system.listMethods", "people.get");
+    map.putAll("hostEndpoint", methods);
+    assertJsonEquals("{hostEndpoint : ['system.listMethods', 'people.get']}",
+        JsonSerializer.serialize(map));
+  }
+
+  @Test
+  public void serializeSimpleCollection() throws Exception {
+    Collection<String> collection = Arrays.asList("foo", null, "bar", "baz", null);
+    assertJsonEquals("['foo','bar','baz']", JsonSerializer.serialize(collection));
+  }
+
+  @Test
+  public void serializeArray() throws Exception {
+    String[] array = {"foo", null, "bar", "baz"};
+    assertJsonEquals("['foo','bar','baz']", JsonSerializer.serialize(array));
+  }
+
+  @Test
+  public void serializeJsonArray() throws Exception {
+    JSONArray array = new JSONArray(new String[] {"foo", null, "bar", "baz"});
+    assertJsonEquals("['foo','bar','baz']", JsonSerializer.serialize(array));
+  }
+
+  @Test
+  public void serializeJsonObjectWithComplexArray() throws Exception {
+    JSONArray array = new JSONArray();
+    array.put(new JsonPojo());
+    JSONObject object = new JSONObject();
+    object.put("array", array);
+    assertJsonEquals("{'array': [" + JSON_POJO_AS_JSON + "]}", JsonSerializer.serialize(object));
+  }
+
+  @Test
+  public void serializeJsonObjectWithNullPropertyValue() throws Exception {
+    String json = "{foo:null}";
+    assertJsonEquals(json, JsonSerializer.serialize(new JSONObject(json)));
+  }
+
+  @Test
+  public void serializePrimitives() throws Exception {
+    assertEquals("null", JsonSerializer.serialize((Object) null));
+    assertEquals("\"hello\"", JsonSerializer.serialize("hello"));
+    assertEquals("100", JsonSerializer.serialize(100));
+    assertEquals("125.0", JsonSerializer.serialize(125.0f));
+    assertEquals("126.0", JsonSerializer.serialize(126.0));
+    assertEquals("1", JsonSerializer.serialize(1L));
+    assertEquals("\"RUNTIME\"", JsonSerializer.serialize(RetentionPolicy.RUNTIME));
+    assertEquals("\"string buf\"",
+        JsonSerializer.serialize(new StringBuilder().append("string").append(' ').append("buf")));
+  }
+
+  public static class JsonPojo {
+    public String getString() {
+      return "string-value";
+    }
+
+    @SuppressWarnings("unused")
+    private String getPrivateString() {
+      throw new UnsupportedOperationException();
+    }
+
+    public int getInteger() {
+      return 100;
+    }
+
+    @JsonProperty("simple!")
+    public int getSimpleName() {
+      return 3;
+    }
+
+    public Object getNullValue() {
+      return null;
+    }
+    @JsonProperty("simple!")
+    public void setSimpleName(int foo) {
+
+    }
+    @JsonProperty("invalid-setter-two-args")
+    public void setInvalidSetterTwoArgs(String foo, String bar) {
+    }
+
+    @JsonProperty("invalid-setter-no-args")
+    public void setInvalidSetterNoArgs() {
+    }
+
+    @JsonProperty("invalid-getter-args")
+    public String getInvalidGetterWithArgs(String foo) {
+       return "invalid";
+    }
+  }
+
+  @Test
+  public void serializePojo() throws Exception {
+    JsonPojo pojo = new JsonPojo();
+
+    assertJsonEquals(JSON_POJO_AS_JSON,
+        JsonSerializer.serialize(pojo));
+  }
+
+  @Test
+  public void serializeMixedObjects() throws Exception {
+    Map<String, ?> map = ImmutableMap.of(
+        "int", Integer.valueOf(3),
+        "double", Double.valueOf(2.7d),
+        "bool", Boolean.TRUE,
+        "map", ImmutableMap.of("hello", "world", "foo", "bar"),
+        "string", "hello!");
+    assertJsonEquals(
+        "{int:3,double:2.7,bool:true,map:{hello:'world',foo:'bar'},string:'hello!'}",
+        JsonSerializer.serialize(map));
+  }
+
+  @Test
+  public void serializeMixedArray() throws Exception {
+    Collection<Object> data = Arrays.asList(
+        Integer.valueOf(3),
+        Double.valueOf(2.7d),
+        Boolean.TRUE,
+        Arrays.asList("one", "two", "three"),
+        new JSONArray(new String[] {"foo", "bar"}),
+        "hello!");
+    assertJsonEquals(
+        "[3,2.7,true,['one','two','three'],['foo','bar'],'hello!']",
+        JsonSerializer.serialize(data));
+  }
+
+  @Test
+  public void emptyString() throws Exception {
+    StringBuilder builder = new StringBuilder();
+    JsonSerializer.appendString(builder, "");
+
+    assertEquals("\"\"", builder.toString());
+  }
+
+  @Test
+  public void escapeSequences() throws Exception {
+    StringBuilder builder = new StringBuilder();
+    JsonSerializer.appendString(builder, "\t\r value \\\foo\b\uFFFF\uBCAD\n\u0083");
+
+    assertEquals("\"\\t\\r value \\\\\\foo\\b\uFFFF\uBCAD\\n\\u0083\"", builder.toString());
+  }
+
+  @Test
+  public void escapeBrackets() throws Exception {
+    StringBuilder builder = new StringBuilder();
+    JsonSerializer.appendString(builder, "Hello<world>foo < bar");
+
+    assertEquals("\"Hello\\u003cworld\\u003efoo \\u003c bar\"", builder.toString());
+
+    // Quick sanity check to make sure that this converts back cleanly.
+    JSONObject obj = new JSONObject("{foo:" + builder + '}');
+    assertEquals("Hello<world>foo < bar", obj.get("foo"));
+  }
+
+  private static String avg(long start, long end, long runs) {
+    double delta = end - start;
+    return String.format("%f5", delta / runs);
+  }
+
+  private static String runJsonOrgTest(Map<String, Object> data, int iterations) {
+    org.json.JSONObject object = new org.json.JSONObject(data);
+    long start = System.currentTimeMillis();
+    String result = null;
+    for (int i = 0; i < iterations; ++i) {
+      result = object.toString();
+    }
+    System.out.println("json.org: " + avg(start, System.currentTimeMillis(), iterations) + "ms");
+    return result;
+  }
+
+  private static String runSerializerTest(Map<String, Object> data, int iterations) {
+    long start = System.currentTimeMillis();
+    String result = null;
+    for (int i = 0; i < iterations; ++i) {
+      result = JsonSerializer.serialize(data);
+    }
+    System.out.println("serializer: " + avg(start, System.currentTimeMillis(), iterations) + "ms");
+    return result;
+  }
+
+
+  // private static String runNetSfJsonTest(Map<String, Object> data, int iterations) {
+  //   net.sf.json.JSONObject object = net.sf.json.JSONObject.fromObject(data);
+  //   long start = System.currentTimeMillis();
+  //   String result = null;
+  //   for (int i = 0; i < iterations; ++i) {
+  //     result = object.toString();
+  //   }
+  //   System.out.println("net.sf.json: " + avg(start, System.currentTimeMillis(), iterations) + "ms");
+  //   return result;
+  // }
+
+  public static Map<String, Object> perfComparison100SmallValues() {
+    Map<String, Object> data = Maps.newHashMap();
+    for (int i = 0; i < 100; ++i) {
+      data.put("key-" + i, "small value");
+    }
+
+    return data;
+  }
+
+  public static Map<String, Object> perfComparison1000SmallValues() {
+    Map<String, Object> data = Maps.newHashMap();
+    for (int i = 0; i < 1000; ++i) {
+      data.put("key-" + i, "small value");
+    }
+
+    return data;
+  }
+
+  public static Map<String, Object> perfComparison100LargeValues() {
+    Map<String, Object> data = Maps.newHashMap();
+    for (int i = 0; i < 100; ++i) {
+      data.put("key-" + i, Strings.repeat("small value", 100));
+    }
+    return data;
+  }
+
+  public static Map<String, Object> perfComparison10LargeValuesAndEscapes() {
+    Map<String, Object> data = Maps.newHashMap();
+    for (int i = 0; i < 10; ++i) {
+      data.put("key-" + i, Strings.repeat("\tsmall\r value \\foo\b\uFFFF\uBCAD\n\u0083", 100));
+    }
+    return data;
+  }
+
+  public static Map<String, Object> perfComparison100Arrays() {
+    Map<String, Object> data = Maps.newHashMap();
+    String[] array = {
+      "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"
+    };
+
+    for (int i = 0; i < 100; ++i) {
+      data.put("key-" + i, array);
+    }
+
+    return data;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static void main(String[] args) throws Exception {
+    int iterations = args.length > 0 ? Integer.parseInt(args[0]) : 1000;
+    System.out.println("Running tests with " + iterations + " iterations.");
+
+    for (Method method : JsonSerializerTest.class.getMethods()) {
+      if (method.getName().startsWith("perfComparison")) {
+        Map<String, Object> data = (Map<String, Object>)method.invoke(null);
+        System.out.println("Running: " + method.getName());
+
+        runJsonOrgTest(data, iterations);
+        runSerializerTest(data, iterations);
+
+        // if (!jsonEquals(jsonOrg, netSfJson)) {
+        //   System.out.println("net.sf.json did not produce results matching the reference impl.");
+        // }
+        System.out.println("-----------------------");
+      }
+    }
+    System.out.println("Done");
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/JsonUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/JsonUtilTest.java
new file mode 100644
index 0000000..a029d0c
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/JsonUtilTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.common.JsonSerializerTest.JsonPojo;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+
+public class JsonUtilTest {
+  @Test
+  public void getPropertyOfJsonObject() throws Exception {
+    JSONObject json = new JSONObject("{a: 1, b: '2'}");
+    assertEquals(1, JsonUtil.getProperty(json, "a"));
+    assertEquals("2", JsonUtil.getProperty(json, "b"));
+    assertNull(JsonUtil.getProperty(json, "c"));
+  }
+
+  @Test
+  public void getPropertyOfMap() throws Exception {
+    Map<String, Object> map = ImmutableMap.of("a", (Object) 1, "b", "2");
+        assertEquals(1, JsonUtil.getProperty(map, "a"));
+    assertEquals("2", JsonUtil.getProperty(map, "b"));
+    assertNull(JsonUtil.getProperty(map, "c"));
+  }
+
+  @Test
+  public void getPropertyOfPojo() throws Exception {
+    JsonPojo pojo = new JsonPojo();
+    assertEquals("string-value", JsonUtil.getProperty(pojo, "string"));
+    assertEquals(100, JsonUtil.getProperty(pojo, "integer"));
+    assertEquals(3, JsonUtil.getProperty(pojo, "simple!"));
+    assertNull(JsonUtil.getProperty(pojo, "not"));
+  }
+
+  @Test
+  public void excludedPropertiesOfPojo() throws Exception {
+    JsonPojo pojo = new JsonPojo();
+    // These exist as getters on all objects, but not as properties
+    assertNull(JsonUtil.getProperty(pojo, "class"));
+    assertNull(JsonUtil.getProperty(pojo, "declaringClass"));
+  }
+
+  private class DuplicateBase<type> {
+    public type getValue() {
+      return null;
+    }
+  }
+
+  private class Duplicate extends DuplicateBase<String> {
+    public String getValue() {
+      return "duplicate";
+    }
+  }
+
+  @Test
+  public void duplicateMethodPojo() throws Exception {
+    Duplicate pojo = new Duplicate();
+    assertEquals("duplicate", JsonUtil.getProperty(pojo, "value"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/OpenSocialVersionTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/OpenSocialVersionTest.java
new file mode 100644
index 0000000..08ca63c
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/OpenSocialVersionTest.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import org.apache.shindig.common.util.OpenSocialVersion;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import junit.framework.Assert;
+
+/**
+ * Tests utility class for Version strings
+ *
+ */
+public class OpenSocialVersionTest {
+
+  @Test
+  public void createOpenSocialVersion(){
+    OpenSocialVersion version = new OpenSocialVersion("1.2.3");
+    Assert.assertEquals(1, version.major);
+    Assert.assertEquals(2, version.minor);
+    Assert.assertEquals(3, version.patch);
+    Assert.assertEquals(version, new OpenSocialVersion("1.2.3"));
+  }
+
+  @Test
+  public void testEquivalence(){
+    OpenSocialVersion version = new OpenSocialVersion("1.2.3");
+    OpenSocialVersion version2 = new OpenSocialVersion("1.2");
+    Assert.assertTrue(version.isEquivalent(version2));
+
+    version = new OpenSocialVersion("2");
+    Assert.assertTrue(version.isEquivalent("2.2"));
+
+    version = new OpenSocialVersion("3");
+    Assert.assertTrue(!version.isEquivalent("2.2"));
+  }
+
+  @Test
+  public void testEqualOrGreaterThan(){
+    OpenSocialVersion version = new OpenSocialVersion("1.2.3");
+    OpenSocialVersion version2 = new OpenSocialVersion("1.2");
+    Assert.assertTrue(version.isEqualOrGreaterThan(version2));
+    Assert.assertTrue(!version2.isEqualOrGreaterThan(version));
+
+    version = new OpenSocialVersion("2");
+    version2 = new OpenSocialVersion("2.2");
+    Assert.assertTrue(!version.isEqualOrGreaterThan(version2));
+    Assert.assertTrue(version2.isEqualOrGreaterThan(version));
+
+    version = new OpenSocialVersion("2.2.48");
+    version2 = new OpenSocialVersion("2.2.49");
+    Assert.assertTrue(!version.isEqualOrGreaterThan(version2));
+    Assert.assertTrue(version2.isEqualOrGreaterThan(version));
+
+    version = new OpenSocialVersion("3");
+    Assert.assertTrue(version.isEqualOrGreaterThan("2.2"));
+
+    version = new OpenSocialVersion("3.1.18");
+    Assert.assertTrue(version.isEqualOrGreaterThan("2.2"));
+  }
+
+  @Test
+  public void testVersionSorting(){
+    ArrayList<OpenSocialVersion> list = new ArrayList<OpenSocialVersion>();
+    list.add(new OpenSocialVersion("2.2.48"));
+    list.add(new OpenSocialVersion("9.0.1"));
+    list.add(new OpenSocialVersion("1.2.48"));
+    list.add(new OpenSocialVersion("2.3.48"));
+    list.add(new OpenSocialVersion("2.2.455"));
+    list.add(new OpenSocialVersion("9.0.0"));
+    Collections.sort(list, OpenSocialVersion.COMPARATOR);
+    for(int i =0;i < list.size()-1;i++){
+      Assert.assertTrue(list.get(i+1).isEqualOrGreaterThan(list.get(i)));
+    }
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java
new file mode 100644
index 0000000..6373e9d
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/PairTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class PairTest {
+
+  @Test
+  public void testPair() {
+    Pair<String, Integer> p = Pair.of("one", new Integer(1));
+    assertEquals("one", p.one);
+    assertEquals(new Integer(1), p.two);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/cache/LruCacheProviderTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/LruCacheProviderTest.java
new file mode 100644
index 0000000..47f83ab
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/LruCacheProviderTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.name.Names;
+
+import org.junit.Test;
+
+public class LruCacheProviderTest {
+
+  private LruCache<Object, Object> getCache(CacheProvider provider, String name) {
+    Cache<Object, Object> base = provider.createCache(name);
+    return (LruCache<Object, Object>)base;
+  }
+
+  @Test
+  public void defaultCapacityForNamedCache() throws Exception {
+    LruCacheProvider provider = new LruCacheProvider(10);
+    assertEquals(10, getCache(provider, "foo").capacity);
+  }
+
+  LruCacheProvider createProvider(final String name, final String capacity, int defaultCapacity) {
+    Module module = new AbstractModule() {
+      @Override
+      public void configure() {
+        binder().bindConstant()
+            .annotatedWith(Names.named("shindig.cache.lru." + name + ".capacity"))
+            .to(capacity);
+      }
+    };
+
+    Injector injector = Guice.createInjector(module);
+
+    return new LruCacheProvider(injector, defaultCapacity);
+  }
+
+  @Test
+  public void configuredMultipleCalls() throws Exception {
+    LruCacheProvider provider = createProvider("foo", "100", 10);
+    assertSame(getCache(provider, "foo"), getCache(provider, "foo"));
+  }
+
+  @Test
+  public void configuredCapacity() throws Exception {
+    LruCacheProvider provider = createProvider("foo", "100", 10);
+    assertEquals(100, getCache(provider, "foo").capacity);
+  }
+
+  @Test
+  public void missingConfiguredCapacity() throws Exception {
+    LruCacheProvider provider = createProvider("foo", "100", 10);
+    assertEquals(10, getCache(provider, "bar").capacity);
+  }
+
+  @Test
+  public void malformedConfiguredCapacity() throws Exception {
+    LruCacheProvider provider = createProvider("foo", "adfdf", 10);
+    assertEquals(10, getCache(provider, "foo").capacity);
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/cache/LruCacheTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/LruCacheTest.java
new file mode 100644
index 0000000..3e9ca39
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/LruCacheTest.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+public class LruCacheTest {
+  private static final int TEST_CAPACITY = 2;
+
+  private final LruCache<String, String> cache
+      = new LruCache<String, String>(TEST_CAPACITY);
+
+  @Test
+  public void normalCapacityOk() {
+    for (int i = 0; i < TEST_CAPACITY; ++i) {
+      cache.addElement(Integer.toString(i), Integer.toString(i));
+    }
+    assertEquals(TEST_CAPACITY, cache.size());
+    assertEquals(TEST_CAPACITY, cache.getSize());
+    assertEquals(TEST_CAPACITY, cache.getCapacity());
+    assertEquals("0", cache.getElement("0"));
+  }
+
+  @Test
+  public void exceededCapacityRemoved() {
+    for (int i = 0; i < TEST_CAPACITY + 1; ++i) {
+      cache.addElement(Integer.toString(i), Integer.toString(i));
+    }
+    assertEquals(TEST_CAPACITY, cache.size());
+    assertEquals(TEST_CAPACITY, cache.getSize());
+    assertEquals(TEST_CAPACITY, cache.getCapacity());
+    assertNull(cache.getElement("0"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/cache/SoftExpiringCacheTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/SoftExpiringCacheTest.java
new file mode 100644
index 0000000..06d08c5
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/SoftExpiringCacheTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache;
+
+import org.apache.shindig.common.util.FakeTimeSource;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class SoftExpiringCacheTest extends Assert {
+  private FakeTimeSource timeSource;
+  private Cache<String, String> cache;
+
+  @Before
+  public void setUp() throws Exception {
+    timeSource = new FakeTimeSource(0);
+    cache = new LruCache<String, String>(5);
+  }
+
+  private SoftExpiringCache<String, String> makeSoftExpiringCache() {
+    SoftExpiringCache<String, String> expiringCache = new SoftExpiringCache<String, String>(cache);
+    expiringCache.setTimeSource(timeSource);
+    return expiringCache;
+  }
+
+  @Test
+  public void testGeneralCacheExpiration() {
+    SoftExpiringCache<String, String> expiringCache = makeSoftExpiringCache();
+    String key = "key1", val = "val1";
+    expiringCache.addElement(key, val, 240 * 1000);
+
+    // Time is still 0: should be in the cache.
+    assertEquals(val, expiringCache.getElement(key).obj);
+    assertFalse(expiringCache.getElement(key).isExpired);
+
+    // Time = 300 seconds: out of cache.
+    timeSource.setCurrentTimeMillis(300 * 1000);
+    assertEquals(val, expiringCache.getElement(key).obj);
+    assertTrue(expiringCache.getElement(key).isExpired);
+  }
+
+  @Test
+  public void testMissingValue() {
+    SoftExpiringCache<String, String> expiringCache = makeSoftExpiringCache();
+    assertNull(expiringCache.getElement("not set"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/cache/ehcache/EhCacheCacheProviderTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/ehcache/EhCacheCacheProviderTest.java
new file mode 100644
index 0000000..1bc1972
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/cache/ehcache/EhCacheCacheProviderTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.cache.ehcache;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+
+import org.apache.shindig.common.servlet.GuiceServletContextListener;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class EhCacheCacheProviderTest {
+
+  static CacheProvider defaultProvider;
+  @BeforeClass
+  public static void setup() throws Exception {
+    defaultProvider = new EhCacheCacheProvider(
+        "res://org/apache/shindig/common/cache/ehcache/ehcacheConfig.xml",
+        "org/apache/shindig/common/cache/ehcache/SizeOfFilter.txt",
+        true,
+        true,
+        new GuiceServletContextListener.CleanupHandler());
+  }
+
+  @Test
+  public void getNamedCache() throws Exception {
+    Cache<String, String> cache = defaultProvider.createCache("testcache");
+    Cache<String, String> cache2 = defaultProvider.createCache("testcache");
+    Assert.assertNotNull(cache);
+    Assert.assertEquals(cache, cache2);
+    Assert.assertNull(cache.getElement("test"));
+    cache.addElement("test", "value1");
+    Assert.assertEquals("value1", cache.getElement("test"));
+    cache.removeElement("test");
+    Assert.assertNull(cache.getElement("test"));
+    Assert.assertEquals(cache.getCapacity(), cache2.getCapacity());
+    Assert.assertEquals(cache.getSize(), cache2.getSize());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/crypto/BlobCrypterTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/crypto/BlobCrypterTest.java
new file mode 100644
index 0000000..05e980e
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/crypto/BlobCrypterTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.crypto;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.util.FakeTimeSource;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.apache.commons.codec.binary.Base64;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class BlobCrypterTest {
+
+  private BasicBlobCrypter crypter;
+  private FakeTimeSource timeSource;
+
+  public BlobCrypterTest() {
+    crypter = new BasicBlobCrypter("0123456789abcdef".getBytes());
+    timeSource = new FakeTimeSource();
+    crypter.timeSource = timeSource;
+  }
+
+  @Test
+  public void testEncryptAndDecrypt() throws Exception {
+    checkString("");
+    checkString("a");
+    checkString("ab");
+    checkString("dfkljdasklsdfklasdjfklajsdfkljasdklfjasdkljfaskldjf");
+    checkString(Crypto.getRandomString(500));
+    checkString("foo bar baz");
+    checkString("foo\nbar\nbaz");
+  }
+
+  private void checkString(String string) throws Exception {
+    Map<String, String> in = Maps.newHashMap();
+    if (string != null) {
+      in.put("a", string);
+    }
+    String blob = crypter.wrap(in);
+    Map<String, String> out = crypter.unwrap(blob);
+    assertEquals(string, out.get("a"));
+  }
+
+  @Test
+  public void testDecryptGarbage() throws Exception {
+    StringBuilder sb = new StringBuilder();
+    for (int i=0; i < 100; ++i) {
+      assertThrowsBlobCrypterException(sb.toString());
+      sb.append('a');
+    }
+  }
+
+  private void assertThrowsBlobCrypterException(String in) {
+    try {
+      crypter.unwrap(in);
+      fail("Should have thrown BlobCrypterException for input " + in);
+    } catch (BlobCrypterException e) {
+      // Good.
+    }
+  }
+
+  @Test
+  public void testManyEntries() throws Exception {
+    Map<String, String> in = Maps.newHashMap();
+    for (int i=0; i < 1000; i++) {
+      in.put(Integer.toString(i), Integer.toString(i));
+    }
+    String blob = crypter.wrap(in);
+    Map<String, String> out = crypter.unwrap(blob);
+    for (int i=0; i < 1000; i++) {
+      assertEquals(out.get(Integer.toString(i)), Integer.toString(i));
+    }
+  }
+
+  @Test(expected=BlobCrypterException.class)
+  public void testTamperIV() throws Exception {
+    Map<String, String> in = ImmutableMap.of("a","b");
+
+    String blob = crypter.wrap(in);
+    byte[] blobBytes = Base64.decodeBase64(blob.getBytes());
+    blobBytes[0] ^= 0x01;
+    String tampered = new String(Base64.encodeBase64(blobBytes));
+    crypter.unwrap(tampered);
+  }
+
+  @Test(expected=BlobCrypterException.class)
+  public void testTamperData() throws Exception {
+    Map<String, String> in = ImmutableMap.of("a","b");
+    String blob = crypter.wrap(in);
+    byte[] blobBytes = Base64.decodeBase64(blob.getBytes());
+    blobBytes[30] ^= 0x01;
+    String tampered = new String(Base64.encodeBase64(blobBytes));
+    crypter.unwrap(tampered);
+  }
+
+  @Test(expected=BlobCrypterException.class)
+  public void testTamperMac() throws Exception {
+    Map<String, String> in = ImmutableMap.of("a","b");
+
+    String blob = crypter.wrap(in);
+    byte[] blobBytes = Base64.decodeBase64(blob.getBytes());
+    blobBytes[blobBytes.length-1] ^= 0x01;
+    String tampered = new String(Base64.encodeBase64(blobBytes));
+    crypter.unwrap(tampered);
+  }
+
+  @Test
+  public void testFixedKey() throws Exception {
+    BlobCrypter alt = new BasicBlobCrypter("0123456789abcdef".getBytes());
+    Map<String, String> in = ImmutableMap.of("a","b");
+
+    String blob = crypter.wrap(in);
+    Map<String, String> out = alt.unwrap(blob);
+    assertEquals("b", out.get("a"));
+  }
+
+  @Test(expected=BlobCrypterException.class)
+  public void testBadKey() throws Exception {
+    BlobCrypter alt = new BasicBlobCrypter("1123456789abcdef".getBytes());
+    Map<String, String> in = ImmutableMap.of("a","b");
+
+    String blob = crypter.wrap(in);
+    alt.unwrap(blob);
+  }
+
+  @Test(expected=IllegalArgumentException.class)
+  public void testShortKeyFails() throws Exception {
+    new BasicBlobCrypter("0123456789abcde".getBytes());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/crypto/CryptoTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/crypto/CryptoTest.java
new file mode 100644
index 0000000..c8855ba
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/crypto/CryptoTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.crypto;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.security.GeneralSecurityException;
+import java.util.regex.Pattern;
+
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.junit.Test;
+
+public class CryptoTest {
+  private BasicBlobCrypter crypter;
+
+  public CryptoTest() {
+    crypter = new BasicBlobCrypter("0123456789abcdef".getBytes());
+    crypter.timeSource = new FakeTimeSource();
+  }
+
+  @Test
+  public void testHmacSha1() throws Exception {
+    String key = "abcd1234";
+    String val = "your mother is a hedgehog";
+    byte[] expected = {
+        -21, 2, 47, -101, 9, -40, 18, 43, 76, 117,
+        -51, 115, -122, -91, 39, 26, -18, 122, 30, 90,
+    };
+    byte[] hmac = Crypto.hmacSha1(key.getBytes(), val.getBytes());
+    assertArrayEquals(expected, hmac);
+  }
+
+  @Test
+  public void testHmacSha1Verify() throws Exception {
+    String key = "abcd1234";
+    String val = "your mother is a hedgehog";
+    byte[] expected = {
+        -21, 2, 47, -101, 9, -40, 18, 43, 76, 117,
+        -51, 115, -122, -91, 39, 26, -18, 122, 30, 90,
+    };
+    Crypto.hmacSha1Verify(key.getBytes(), val.getBytes(), expected);
+  }
+
+
+  @Test(expected = GeneralSecurityException.class)
+  public void testHmacSha1VerifyTampered() throws Exception {
+    String key = "abcd1234";
+    String val = "your mother is a hedgehog";
+    byte[] expected = {
+        -21, 2, 47, -101, 9, -40, 18, 43, 76, 117,
+        -51, 115, -122, -91, 39, 0, -18, 122, 30, 90,
+    };
+    Crypto.hmacSha1Verify(key.getBytes(), val.getBytes(), expected);
+  }
+
+  @Test
+  public void testAes128Cbc() throws Exception {
+    byte[] key = Crypto.getRandomBytes(Crypto.CIPHER_KEY_LEN);
+    for (byte i=0; i < 50; i++) {
+      byte[] orig = new byte[i];
+      for (byte j=0; j < i; j++) {
+        orig[j] = j;
+      }
+      byte[] cipherText = Crypto.aes128cbcEncrypt(key, orig);
+      byte[] plainText = Crypto.aes128cbcDecrypt(key, cipherText);
+      assertArrayEquals("Array of length " + i, orig, plainText);
+    }
+  }
+
+  @Test
+  public void testRandomDigits() throws Exception {
+    Pattern digitPattern = Pattern.compile("^\\d+$");
+    String digits = Crypto.getRandomDigits(100);
+    assertEquals(100, digits.length());
+    assertTrue("Should be only digits: " + digits, digitPattern.matcher(digits).matches());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/BasicAuthorityTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/BasicAuthorityTest.java
new file mode 100644
index 0000000..d2ad29e
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/BasicAuthorityTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import org.apache.shindig.common.EasyMockTestCase;
+
+import org.junit.Test;
+
+/**
+ * Simple test for BasicAuthority.
+ */
+public class BasicAuthorityTest extends EasyMockTestCase {
+
+  @Test
+  public void testBasicAuthorityWorks() {
+    String host = "myhost";
+    String port = "9080";
+    BasicAuthority authority = new BasicAuthority(host,port);
+    assertEquals( "myhost:9080", authority.getAuthority());
+  }
+
+  @Test
+  public void testDefaultHostAndPort() {
+    String host = "";
+    String port = "";
+    BasicAuthority authority = new BasicAuthority(host,port);
+    assertEquals("localhost:8080", authority.getAuthority());
+  }
+
+  @Test
+  public void testJettyHostAndPort() {
+    String host = "";
+    String port = "";
+    System.setProperty("jetty.host", "localhost");
+    System.setProperty("jetty.port", "9003");
+    BasicAuthority authority = new BasicAuthority(host,port);
+    assertEquals("localhost:9003", authority.getAuthority() );
+    System.clearProperty("jetty.host");
+    System.clearProperty("jetty.port");
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpServletResponseRecorder.java b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpServletResponseRecorder.java
new file mode 100644
index 0000000..d7aa7c3
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpServletResponseRecorder.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import org.apache.shindig.common.util.DateUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * Captures output from an HttpServletResponse.
+ */
+public class HttpServletResponseRecorder extends HttpServletResponseWrapper {
+  protected final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+  private PrintWriter writer;
+  private final Map<String, String> headers = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+  private int httpStatusCode = HttpServletResponse.SC_OK;
+  private String encoding = Charset.defaultCharset().name();
+
+  public HttpServletResponseRecorder(HttpServletResponse response) {
+    super(response);
+  }
+
+  public String getResponseAsString() {
+    try {
+      getWriter().close();
+      return new String(baos.toByteArray(), "UTF-8");
+    } catch (IOException e) {
+      return null;
+    }
+  }
+
+  public byte[] getResponseAsBytes() {
+    return baos.toByteArray();
+  }
+
+  public int getHttpStatusCode() {
+    return httpStatusCode;
+  }
+
+  public String getHeader(String name) {
+    return headers.get(name);
+  }
+
+  @Override
+  public PrintWriter getWriter() throws UnsupportedEncodingException {
+    if (writer == null) {
+      writer = new PrintWriter(new OutputStreamWriter(baos, encoding));
+    }
+    return writer;
+  }
+
+  @Override
+  public ServletOutputStream getOutputStream() {
+    return new ServletOutputStream() {
+      @Override
+      public void write(int b) {
+        baos.write(b);
+      }
+    };
+  }
+
+  @Override
+  public void addHeader(String name, String value) {
+    headers.put(name, value);
+  }
+
+  @Override
+  public void setHeader(String name, String value) {
+    addHeader(name, value);
+  }
+
+  @Override
+  public void addDateHeader(String name, long date) {
+    headers.put(name, DateUtil.formatRfc1123Date(date));
+  }
+
+  @Override
+  public void setDateHeader(String name, long date) {
+    addDateHeader(name, date);
+  }
+
+  @Override
+  public void setStatus(int httpStatusCode) {
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  @Override
+  public void sendError(int httpStatusCode) {
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  @Override
+  public void sendRedirect(String location) {
+    setStatus(302);
+    setHeader("Location", location);
+  }
+
+  @Override
+  public void setStatus(int httpStatusCode, String msg)  {
+    try {
+      getWriter().write(msg);
+      this.httpStatusCode = httpStatusCode;
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public void sendError(int httpStatusCode, String msg) {
+    try {
+      getWriter().write(msg);
+      this.httpStatusCode = httpStatusCode;
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public void setContentType(String type) {
+    headers.put("Content-Type", type);
+  }
+
+  @Override
+  public String getContentType() {
+    return headers.get("Content-Type");
+  }
+
+  @Override
+  public void setCharacterEncoding(String encoding) {
+    this.encoding = encoding;
+  }
+
+  @Override
+  public String getCharacterEncoding() {
+    return encoding;
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpServletUserAgentProviderTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpServletUserAgentProviderTest.java
new file mode 100644
index 0000000..4087cf8
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpServletUserAgentProviderTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import com.google.inject.Provider;
+
+import static org.easymock.EasyMock.expect;
+
+import org.apache.shindig.common.EasyMockTestCase;
+
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Simple test for HttpServletUserAgentProvider.
+ */
+public class HttpServletUserAgentProviderTest extends EasyMockTestCase {
+  private UserAgent.Parser parser = new PassThroughUAParser();
+
+  @Test
+  public void testProviderWorks() {
+    String agentVersion = "AGENT_VERSION";
+    HttpServletRequest req = mock(HttpServletRequest.class);
+    expect(req.getHeader("User-Agent")).andReturn(agentVersion).once();
+    replay();
+    HttpServletUserAgentProvider provider = new HttpServletUserAgentProvider(
+        parser, new HttpServletRequestProvider(req));
+    UserAgent entry = provider.get();
+    assertEquals(UserAgent.Browser.OTHER, entry.getBrowser());
+    assertEquals(agentVersion, entry.getVersion());
+    verify();
+  }
+
+  @Test
+  public void testNoRequestGetsNull() {
+    HttpServletUserAgentProvider provider = new HttpServletUserAgentProvider(
+        parser, new HttpServletRequestProvider(null));
+    assertNull(provider.get());
+  }
+
+  @Test
+  public void testNoUserAgentGetsNull() {
+    HttpServletRequest req = mock(HttpServletRequest.class);
+    expect(req.getHeader("User-Agent")).andReturn(null).once();
+    replay();
+    HttpServletUserAgentProvider provider = new HttpServletUserAgentProvider(
+        parser, new HttpServletRequestProvider(req));
+    assertNull(provider.get());
+    verify();
+  }
+
+  private static class HttpServletRequestProvider implements Provider<HttpServletRequest> {
+    private HttpServletRequest req;
+
+    private HttpServletRequestProvider(HttpServletRequest req) {
+      this.req = req;
+    }
+
+    public HttpServletRequest get() {
+      return req;
+    }
+  }
+
+  private static class PassThroughUAParser implements UserAgent.Parser {
+    public UserAgent parse(String agentVersion) {
+      return new UserAgent(UserAgent.Browser.OTHER, agentVersion);
+    }
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpUtilTest.java
new file mode 100644
index 0000000..1a5f532
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/HttpUtilTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import static junitx.framework.ComparableAssert.assertGreater;
+import static junitx.framework.ComparableAssert.assertLesser;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.easymock.EasyMock;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.http.HttpServletResponse;
+
+public class HttpUtilTest {
+
+  public static final FakeTimeSource timeSource = new FakeTimeSource();
+  public static final long testStartTime = timeSource.currentTimeMillis();
+
+  static {
+    HttpUtil.setTimeSource(timeSource);
+  }
+
+  private HttpServletResponse mockResponse = EasyMock.createMock(HttpServletResponse.class);
+  private HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(mockResponse);
+
+  @Test
+  public void testSetCachingHeaders() {
+    HttpUtil.setCachingHeaders(recorder);
+    checkCacheControlHeaders(testStartTime, recorder, HttpUtil.getDefaultTtl(), false);
+  }
+
+  @Test
+  public void testSetCachingHeadersNoProxy() {
+    HttpUtil.setCachingHeaders(recorder, true);
+
+    checkCacheControlHeaders(testStartTime, recorder, HttpUtil.getDefaultTtl(), true);
+  }
+
+  @Test
+  public void testSetCachingHeadersAllowProxy() {
+    HttpUtil.setCachingHeaders(recorder, false);
+    checkCacheControlHeaders(testStartTime, recorder, HttpUtil.getDefaultTtl(), false);
+  }
+
+  @Test
+  public void testSetCachingHeadersFixedTtl() {
+    int ttl = 10;
+    HttpUtil.setCachingHeaders(recorder, ttl);
+    checkCacheControlHeaders(testStartTime, recorder, ttl, false);
+  }
+
+  @Test
+  public void testSetCachingHeadersWithTtlAndNoProxy() {
+    int ttl = 20;
+    HttpUtil.setCachingHeaders(recorder, ttl, true);
+    checkCacheControlHeaders(testStartTime, recorder, ttl, true);
+  }
+
+  @Test
+  public void testSetCachingHeadersNoCache() {
+    HttpUtil.setCachingHeaders(recorder, 0);
+    checkCacheControlHeaders(testStartTime, recorder, 0, true);
+  }
+
+  @Test
+  public void testSetNoCche() {
+    HttpUtil.setNoCache(recorder);
+    checkCacheControlHeaders(testStartTime, recorder, 0, true);
+  }
+
+  @Test
+  public void testCORSstar() {
+    HttpUtil.setCORSheader(recorder, Collections.singleton("*"));
+    assertEquals(recorder.getHeader(HttpUtil.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER), "*");
+  }
+
+  @Test
+  public void testCORSnull() {
+     HttpUtil.setCORSheader(recorder, null);
+     assertEquals(recorder.getHeader(HttpUtil.ACCESS_CONTROL_ALLOW_ORIGIN_HEADER), null);
+   }
+
+   @Test
+   @Ignore("HttpServletResponseRecorder doesn't support multiple headers")
+   public void testCORSmultiple() {
+     HttpUtil.setCORSheader(recorder, Arrays.asList("http://foo.example.com", "http://bar.example.com"));
+     // TODO fix HttpServletResponseRecorder and add multi-header test here
+   }
+
+  public static void checkCacheControlHeaders(long testStartTime,
+      HttpServletResponseRecorder response, int ttl, boolean noProxy) {
+
+    long expires = DateUtil.parseRfc1123Date(response.getHeader("Expires")).getTime();
+
+    long lowerBound = testStartTime + (1000L * (ttl - 1));
+    long upperBound = lowerBound + 2000L;
+
+    assertGreater("Expires should be at least " + ttl + " seconds more than start time.",
+        lowerBound, expires);
+
+    assertLesser("Expires should be within 2 seconds of the requested value.",
+        upperBound, expires);
+
+    if (ttl == 0) {
+      assertEquals("no-cache", response.getHeader("Pragma"));
+      assertEquals("no-cache", response.getHeader("Cache-Control"));
+    } else {
+      List<String> directives
+          = Arrays.asList(StringUtils.split(response.getHeader("Cache-Control"), ','));
+
+      assertTrue("Incorrect max-age set.", directives.contains("max-age=" + ttl));
+      if (noProxy) {
+        assertTrue("No private Cache-Control directive was set.", directives.contains("private"));
+      } else {
+        assertTrue("No public Cache-Control directive was set.", directives.contains("public"));
+      }
+    }
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/UserAgentTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/UserAgentTest.java
new file mode 100644
index 0000000..0c35667
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/servlet/UserAgentTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.servlet;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class UserAgentTest extends Assert {
+  private UserAgent getUaEntry(String version) {
+    return new UserAgent(UserAgent.Browser.OTHER, version);
+  }
+
+  @Test
+  public void testVersionNumberParsingStandard() {
+    assertEquals(3D, getUaEntry("3").getVersionNumber(), 0);
+  }
+
+  @Test
+  public void testVersionNumberParsingStandardDecimal() {
+    assertEquals(3.1415, getUaEntry("3.1415").getVersionNumber(), 0);
+  }
+
+  @Test
+  public void testVersionNumberParsingMultiPart() {
+    assertEquals(3.1, getUaEntry("3.1.5").getVersionNumber(), 0);
+  }
+
+  @Test
+  public void testVersionNumberParsingAlphaSuffix() {
+    assertEquals(4.5, getUaEntry("4.5beta2").getVersionNumber(), 0);
+  }
+
+  @Test
+  public void testVersionNumberParsingEmbeddedInTheMiddle() {
+    assertEquals(1.5, getUaEntry("beta 1.5 rc 5").getVersionNumber(), 0);
+  }
+
+  @Test
+  public void testVersionNumberParsingNoMatch() {
+    assertEquals(-1, getUaEntry("invalid").getVersionNumber(), 0);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/testing/FakeGadgetToken.java b/trunk/java/common/src/test/java/org/apache/shindig/common/testing/FakeGadgetToken.java
new file mode 100644
index 0000000..e73ffb3
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/testing/FakeGadgetToken.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.testing;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+import org.apache.shindig.auth.AbstractSecurityToken;
+import org.apache.shindig.auth.AuthenticationMode;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenException;
+
+/**
+ * A fake SecurityToken implementation to help testing.
+ */
+public class FakeGadgetToken extends AbstractSecurityToken {
+
+  private String authMode = AuthenticationMode.SECURITY_TOKEN_URL_PARAMETER.name();
+  private String updated;
+
+  public String getAuthenticationMode() {
+    return authMode;
+  }
+
+  public boolean isAnonymous() {
+    return false;
+  }
+
+  public FakeGadgetToken() {}
+  /**
+   * Create a fake security token from a map of parameter strings, keys are one of:
+   * ownerId, viewerId, domain, appUrl, appId, trustedJson, module
+   *
+   * @param paramMap
+   * @return The fake token
+   */
+  public FakeGadgetToken(Map<String, String> paramMap) {
+    this(
+      paramMap.get("appId"),
+      paramMap.get("appUrl"),
+      paramMap.get("domain"),
+      paramMap.get("ownerId"),
+      paramMap.get("trustedJson"),
+      paramMap.get("viewerId"),
+      paramMap.get("module")
+    );
+  }
+
+  public FakeGadgetToken(String appId, String appUrl, String domain, String ownerId, String trustedJson, String viewerId, String moduleId) {
+    setAppId(appId);
+    setAppUrl(appUrl);
+    setDomain(domain);
+    setOwnerId(ownerId);
+    setTrustedJson(trustedJson);
+    setViewerId(viewerId);
+
+    if (moduleId != null) {
+      setModuleId(Long.parseLong(moduleId));
+    }
+  }
+
+  /**
+   * SecurityTokenCodec for testing - this allows passing around a
+   * security token of format key=value&key2=value2, where key is one of:
+   * ownerId, viewerId, domain, appUrl, appId, trustedJson, module
+   */
+  public static class Codec implements SecurityTokenCodec {
+    public SecurityToken createToken(Map<String, String> tokenParameters)  {
+      return new FakeGadgetToken(tokenParameters);
+    }
+
+    public String encodeToken(SecurityToken token) throws SecurityTokenException {
+      return null; // NOT USED
+    }
+
+    public int getTokenTimeToLive() {
+      return 0; // Not used.
+    }
+
+    public int getTokenTimeToLive(String container) {
+      return 0; // Not used.
+    }
+  }
+
+  public FakeGadgetToken setAuthenticationMode(String authMode) {
+    this.authMode = authMode;
+    return this;
+  }
+
+  public FakeGadgetToken setUpdatedToken(String updated) {
+    this.updated = updated;
+    return this;
+  }
+
+  public String getUpdatedToken() {
+    return updated;
+  }
+
+  @Override
+  protected EnumSet<Keys> getMapKeys() {
+    return EnumSet.noneOf(Keys.class);
+  }
+
+  public FakeGadgetToken setAppUrl(String appUrl) {
+    return (FakeGadgetToken)super.setAppUrl(appUrl);
+  }
+
+  public FakeGadgetToken setOwnerId(String ownerId) {
+    return (FakeGadgetToken)super.setOwnerId(ownerId);
+  }
+
+  public FakeGadgetToken setViewerId(String viewerId) {
+    return (FakeGadgetToken)super.setViewerId(viewerId);
+  }
+
+  public FakeGadgetToken setAppId(String appId) {
+    return (FakeGadgetToken)super.setAppId(appId);
+  }
+
+  public FakeGadgetToken setDomain(String domain) {
+    return (FakeGadgetToken)super.setDomain(domain);
+  }
+
+  public FakeGadgetToken setContainer(String container) {
+    return (FakeGadgetToken)super.setContainer(container);
+  }
+
+  public FakeGadgetToken setModuleId(long moduleId) {
+    return (FakeGadgetToken)super.setModuleId(moduleId);
+  }
+
+  public FakeGadgetToken setExpiresAt(Long expiresAt) {
+    return (FakeGadgetToken)super.setExpiresAt(expiresAt);
+  }
+
+  public FakeGadgetToken setTrustedJson(String trustedJson) {
+    return (FakeGadgetToken)super.setTrustedJson(trustedJson);
+  }
+
+  public FakeGadgetToken setActiveUrl(String activeUrl) {
+    return (FakeGadgetToken)super.setActiveUrl(activeUrl);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/testing/FakeHttpServletRequest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/testing/FakeHttpServletRequest.java
new file mode 100644
index 0000000..22604f1
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/testing/FakeHttpServletRequest.java
@@ -0,0 +1,877 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.testing;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TimeZone;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+/**
+ * This class fakes a HttpServletRequest for unit test purposes. Currently, it
+ * supports servlet API 2.4.
+ *
+ * <p>
+ * To use this class, you specify the request info (URL, parameters) in the
+ * constructors.
+ *
+ * <p>
+ * Lots of stuff are still not implemented here. Feel free to implement them.
+ */
+public class FakeHttpServletRequest implements HttpServletRequest {
+  protected static final String DEFAULT_HOST = "localhost";
+  protected static final int DEFAULT_PORT = 80;
+  private static final String COOKIE_HEADER = "Cookie";
+  private static final String HOST_HEADER = "Host";
+  private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
+
+  protected String scheme = "http";
+  protected String host;
+  protected int port;
+  protected boolean secure = false;
+  protected String method = "GET";
+  protected String protocol = "HTTP/1.0";
+  protected String contextPath;
+  protected String servletPath;
+  protected String pathInfo = null;
+  protected String queryString;
+  protected String ip = "127.0.0.1";
+  protected String contentType;
+
+  protected Hashtable<String, String> headers =
+      new Hashtable<String, String>();
+
+  // Use a LinkedHashMap so we can generate a query string that is in the same
+  // order that we set the parameters
+  protected Map<String, String[]> parameters = Maps.newLinkedHashMap();
+
+  protected Set<String> postParameters = Sets.newHashSet();
+
+  protected Map<String, Cookie> cookies = new Hashtable<String, Cookie>();
+
+
+  // Use a Map rather than a table since the specified behavior of
+  // setAttribute() allows null values.
+  protected Map<String, Object> attributes = Maps.newHashMap();
+
+  protected Locale locale = Locale.US;
+  protected List<Locale> locales = null;
+
+  // used by POST methods
+  protected byte[] postData;
+  protected String characterEncoding;
+
+  // the following two booleans ensure that either getReader() or
+  // getInputStream is called, but not both, to conform to specs for the
+  // HttpServletRequest class.
+  protected boolean getReaderCalled = false;
+  protected boolean getInputStreamCalled = false;
+
+  private HttpSession session;
+
+  static final String METHOD_POST = "POST";
+
+  /**
+   * Example: http://www.example.com:1234/foo/bar?abc=xyz "www.example.com" is
+   * the host 1234 is the port "/foo" is the contextPath "/bar" is the
+   * servletPath "abc=xyz" is the queryString
+   */
+  public FakeHttpServletRequest(String host, int port, String contextPath,
+      String servletPath, String queryString) {
+    constructor(host, port, contextPath, servletPath, queryString);
+  }
+
+  public FakeHttpServletRequest(String host, String port, String contextPath,
+      String servletPath, String queryString) {
+    this(host, Integer.parseInt(port), contextPath, servletPath, queryString);
+  }
+
+  public FakeHttpServletRequest(String contextPath, String servletPath,
+      String queryString) {
+    this(DEFAULT_HOST, -1, contextPath, servletPath, queryString);
+  }
+
+  public FakeHttpServletRequest() {
+    this(DEFAULT_HOST, DEFAULT_PORT, "", null, null);
+  }
+
+  public FakeHttpServletRequest(String urlStr) throws MalformedURLException {
+    URL url = new URL(urlStr);
+    String contextPath;
+    String servletPath;
+    String path = url.getPath();
+    if (path.length() <= 1) {
+      // path must be either empty string or "/"
+      contextPath = path;
+      servletPath = null;
+    } else {
+      // Look for the second slash which separates the servlet path from the
+      // context path. e.g. "/foo/bar"
+      int secondSlash = path.indexOf('/', 1);
+      if (secondSlash < 0) {
+        // No second slash
+        contextPath = path;
+        servletPath = null;
+      } else {
+        contextPath = path.substring(0, secondSlash);
+        servletPath = path.substring(secondSlash);
+      }
+    }
+
+    // Set the scheme
+    scheme = url.getProtocol();
+    if (scheme.equalsIgnoreCase("https")) {
+      secure = true;
+    }
+
+    int port = url.getPort();
+
+    // Call constructor() instead of this() because the later is only allowed
+    // at the begining of a constructor
+    constructor(url.getHost(), port, contextPath, servletPath, url.getQuery());
+  }
+
+  public FakeHttpServletRequest setLocale(Locale locale) {
+    this.locale = locale;
+    return this;
+  }
+
+  public FakeHttpServletRequest setLocales(List<Locale> locales) {
+    this.locales = locales;
+    return this;
+  }
+
+  public FakeHttpServletRequest setProtocol(String prot) {
+    this.protocol = prot;
+    return this;
+  }
+
+  public FakeHttpServletRequest setSecure(boolean secure) {
+    this.secure = secure;
+    return this;
+  }
+
+  /*
+   * Set a header on this request. Note that if the header implies other
+   * attributes of the request I will set them accordingly. Specifically:
+   *
+   * If the header is "Cookie:" then I will automatically call setCookie on all
+   * of the name-value pairs found therein.
+   *
+   * This makes the object easier to use because you can just feed it headers
+   * and the object will remain consistent with the behavior you'd expect from a
+   * request.
+   */
+  public FakeHttpServletRequest setHeader(String name, String value) {
+    if (name.equals(COOKIE_HEADER)) {
+      String[] pairs = splitAndTrim(value, ";");
+      for (String pair : pairs) {
+        int equalsPos = pair.indexOf('=');
+        if (equalsPos != -1) {
+          String cookieName = pair.substring(0, equalsPos);
+          String cookieValue = pair.substring(equalsPos + 1);
+          addToCookieMap(new Cookie(cookieName, cookieValue));
+        }
+      }
+      setCookieHeader();
+      return this;
+    }
+
+    addToHeaderMap(name, value);
+
+    if (name.equals(HOST_HEADER)) {
+      host = value;
+    }
+    return this;
+  }
+
+  private void addToHeaderMap(String name, String value) {
+    headers.put(name.toLowerCase(), value);
+  }
+
+  /**
+   * Associates a set of cookies with this fake request.
+   *
+   * @param cookies the cookies associated with this request.
+   */
+  public FakeHttpServletRequest setCookies(Cookie... cookies) {
+    for (Cookie cookie : cookies) {
+      addToCookieMap(cookie);
+    }
+    setCookieHeader();
+    return this;
+  }
+
+  /**
+   * Sets a single cookie associated with this fake request. Cookies are
+   * cumulative, but ones with the same name will overwrite one another.
+   *
+   * @param c the cookie to associate with this request.
+   */
+  public FakeHttpServletRequest setCookie(Cookie c) {
+    addToCookieMap(c);
+    setCookieHeader();
+    return this;
+  }
+
+  private void addToCookieMap(Cookie c) {
+    cookies.put(c.getName(), c);
+  }
+
+  /**
+   * Sets the "Cookie" HTTP header based on the current cookies.
+   */
+  private void setCookieHeader() {
+    StringBuilder sb = new StringBuilder();
+    boolean isFirst = true;
+    for (Cookie c : cookies.values()) {
+      if (!isFirst) {
+        sb.append("; ");
+      }
+      sb.append(c.getName());
+      sb.append('=');
+      sb.append(c.getValue());
+      isFirst = false;
+    }
+
+    // We cannot use setHeader() here, because setHeader() calls this method
+    addToHeaderMap(COOKIE_HEADER, sb.toString());
+  }
+
+  /**
+   * Sets the a parameter in this fake request.
+   *
+   * @param name the string key
+   * @param values the string array value
+   * @param isPost if the paramenter comes in the post body.
+   */
+  public FakeHttpServletRequest setParameter(String name, boolean isPost, String... values) {
+    if (isPost) {
+      postParameters.add(name);
+    }
+    parameters.put(name, values);
+    // Old query string no longer matches up, so set it to null so it can be
+    // regenerated on the next call of getQueryString()
+    queryString = null;
+    return this;
+  }
+
+  /**
+   * Sets the a parameter in this fake request.
+   *
+   * @param name the string key
+   * @param values the string array value
+   */
+  public FakeHttpServletRequest setParameter(String name, String... values) {
+    setParameter(name, false, values);
+    return this;
+  }
+
+
+  /** Set the path info field. */
+  public FakeHttpServletRequest setPathInfo(String path) {
+    pathInfo = path;
+    return this;
+  }
+
+  /**
+   * Specify the mock POST data.
+   *
+   * @param postString the mock post data
+   * @param encoding format with which to encode mock post data
+   */
+  public FakeHttpServletRequest setPostData(String postString, String encoding)
+      throws UnsupportedEncodingException {
+    setPostData(postString.getBytes(encoding));
+    characterEncoding = encoding;
+    return this;
+  }
+
+  /**
+   * Specify the mock POST data in raw binary format.
+   *
+   * This implicitly sets character encoding to not specified.
+   *
+   * @param data the mock post data; this is owned by the caller, so
+   *        modifications made after this call will show up when the post data
+   *        is read
+   */
+  public FakeHttpServletRequest setPostData(byte[] data) {
+    postData = data;
+    characterEncoding = null;
+    method = METHOD_POST;
+    return this;
+  }
+
+  /**
+   * Set a new value for the query string. The query string will be parsed and
+   * all parameters reset.
+   *
+   * @param queryString representing the new value. i.e.: "bug=1&id=23"
+   */
+  public FakeHttpServletRequest setQueryString(String queryString) {
+    this.queryString = queryString;
+    parameters.clear();
+    decodeQueryString(queryString, parameters);
+    return this;
+  }
+
+  /**
+   * Sets the session for this request.
+   *
+   * @param session the new session
+   */
+  public FakeHttpServletRequest setSession(HttpSession session) {
+    this.session = session;
+    return this;
+  }
+
+  /**
+   * Sets the content type.
+   *
+   * @param contentType of the request.
+   */
+  public FakeHttpServletRequest setContentType(String contentType) {
+    this.contentType = contentType;
+    return this;
+  }
+
+  // ///////////////////////////////////////////////////////////////////////////
+  // Implements methods from HttpServletRequest
+  // ///////////////////////////////////////////////////////////////////////////
+
+  public String getAuthType() {
+    throw new UnsupportedOperationException();
+  }
+
+  public java.lang.String getContextPath() {
+    return contextPath;
+  }
+
+  public Cookie[] getCookies() {
+    if (cookies.isEmpty()) {
+      // API promises null return if no cookies
+      return null;
+    }
+    return cookies.values().toArray(new Cookie[cookies.size()]);
+  }
+
+  public long getDateHeader(String name) {
+    String value = getHeader(name);
+    if (value == null) return -1;
+
+    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT, Locale.US);
+    format.setTimeZone(TimeZone.getTimeZone("GMT"));
+    try {
+      return format.parse(value).getTime();
+    } catch (ParseException e) {
+      throw new IllegalArgumentException("Cannot parse number from header "
+          + name + ':' + value, e);
+    }
+  }
+
+  public FakeHttpServletRequest setDateHeader(String name, long value) {
+    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT, Locale.US);
+    format.setTimeZone(TimeZone.getTimeZone("GMT"));
+    setHeader(name, format.format(new Date(value)));
+    return this;
+  }
+
+  public String getHeader(String name) {
+    return headers.get(name.toLowerCase());
+  }
+
+  public Enumeration<String> getHeaderNames() {
+    return headers.keys();
+  }
+
+  public Enumeration<?> getHeaders(String name) {
+    List<String> values = Lists.newArrayList();
+    for (Map.Entry<String, String> entry : headers.entrySet()) {
+      if (name.equalsIgnoreCase(entry.getKey())) {
+        values.add(entry.getValue());
+      }
+    }
+    return Collections.enumeration(values);
+  }
+
+  public int getIntHeader(String name) {
+    return Integer.parseInt(getHeader(name));
+  }
+
+  public String getMethod() {
+    return method;
+  }
+
+  public FakeHttpServletRequest setMethod(String method) {
+    this.method = method;
+    return this;
+  }
+
+  public String getPathInfo() {
+    return pathInfo;
+  }
+
+  public String getPathTranslated() {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getQueryString() {
+    try {
+      if (queryString == null && !parameters.isEmpty()) {
+        boolean hasPrevious = false;
+        StringBuilder queryString = new StringBuilder();
+        for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
+          // We're not interested in blank keys
+          if (StringUtils.isBlank(entry.getKey()) || postParameters.contains(entry.getKey())) {
+            continue;
+          }
+          if (hasPrevious) {
+            queryString.append('&');
+          }
+
+          String[] values = entry.getValue();
+          // Append the parameters to the query string
+          if (values.length == 0) {
+            queryString.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
+          } else {
+            for (int i = 0; i < values.length; i++) {
+              queryString.append(URLEncoder.encode(entry.getKey(), "UTF-8")).append('=').append(
+                      URLEncoder.encode(values[i], "UTF-8"));
+              if (i < values.length - 1) {
+                queryString.append('&');
+              }
+            }
+          }
+          hasPrevious = true;
+
+        }
+        this.queryString = queryString.toString();
+      }
+      return queryString;
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException("Should always support UTF-8", e);
+    }
+  }
+
+  public String getRemoteUser() {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getRequestedSessionId() {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getRequestURI() {
+    StringBuilder buf = new StringBuilder();
+    if (StringUtils.isNotBlank(contextPath)) {
+      buf.append(contextPath);
+    }
+
+    if (servletPath != null && !"".equals(servletPath)) {
+      buf.append(servletPath);
+    }
+
+    if (buf.length() == 0) {
+      buf.append('/');
+    }
+
+    return buf.toString();
+  }
+
+  public StringBuffer getRequestURL() {
+    StringBuffer buf =
+        secure ? new StringBuffer("https://") : new StringBuffer("http://");
+    buf.append(host);
+    if (port >= 0) {
+      buf.append(':');
+      buf.append(port);
+    }
+    buf.append(getRequestURI()); // always begins with '/'
+    return buf;
+  }
+
+  public String getServletPath() {
+    return servletPath;
+  }
+
+  public FakeHttpServletRequest setServletPath(String servletPath) {
+    this.servletPath = servletPath;
+    return this;
+  }
+
+  public HttpSession getSession() {
+    return getSession(true);
+  }
+
+  public HttpSession getSession(boolean create) {
+    // TODO return fake session if create && session == null
+    return session;
+  }
+
+  public java.security.Principal getUserPrincipal() {
+    throw new UnsupportedOperationException();
+  }
+
+  public boolean isRequestedSessionIdFromCookie() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Deprecated
+  public boolean isRequestedSessionIdFromUrl() {
+    throw new UnsupportedOperationException("This method is deprecated");
+  }
+
+  public boolean isRequestedSessionIdFromURL() {
+    throw new UnsupportedOperationException();
+  }
+
+  public boolean isRequestedSessionIdValid() {
+    throw new UnsupportedOperationException();
+  }
+
+  public boolean isUserInRole(String role) {
+    throw new UnsupportedOperationException();
+  }
+
+  // Implements methods from ServletRequest ///////////////////////////////////
+
+  public Object getAttribute(String name) {
+    return attributes.get(name);
+  }
+
+  public Enumeration<?> getAttributeNames() {
+    return Collections.enumeration(attributes.keySet());
+  }
+
+  public String getCharacterEncoding() {
+    return characterEncoding;
+  }
+
+  public int getContentLength() {
+    return (postData == null) ? 0 : postData.length;
+  }
+
+  public String getContentType() {
+    return contentType;
+  }
+
+  /**
+   * Get the body of the request (i.e. the POST data) as a binary stream. As per
+   * Java docs, this OR getReader() may be called, but not both (attempting that
+   * will result in an IllegalStateException)
+   *
+   */
+  public ServletInputStream getInputStream() {
+    if (getReaderCalled) {
+      throw new IllegalStateException(
+          "getInputStream() called after getReader()");
+    }
+    getInputStreamCalled = true; // so that getReader() can no longer be called
+
+    final InputStream in = new ByteArrayInputStream(postData);
+    return new ServletInputStream() {
+      @Override public int read() throws IOException {
+        return in.read();
+      }
+    };
+  }
+
+  public Locale getLocale() {
+    return locale;
+  }
+
+  public Enumeration<?> getLocales() {
+    return Collections.enumeration(locales);
+  }
+
+  public String getParameter(String name) {
+    String[] parameters = getParameterValues(name);
+    if (parameters == null || parameters.length < 1) {
+      return null;
+    } else {
+      return parameters[0];
+    }
+  }
+
+  public Map<String, String[]> getParameterMap() {
+    return parameters;
+  }
+
+  public Enumeration<String> getParameterNames() {
+    return Collections.enumeration(parameters.keySet());
+  }
+
+  public String[] getParameterValues(String name) {
+    return parameters.get(name);
+  }
+
+  public String getProtocol() {
+    return protocol;
+  }
+
+  public BufferedReader getReader() throws IOException {
+    if (getInputStreamCalled) {
+      throw new IllegalStateException(
+          "getReader() called after getInputStream()");
+    }
+
+    getReaderCalled = true;
+    BufferedReader br = null;
+    ByteArrayInputStream bais = new ByteArrayInputStream(postData);
+    InputStreamReader isr;
+    if (characterEncoding != null) {
+      isr = new InputStreamReader(bais, characterEncoding);
+    } else {
+      isr = new InputStreamReader(bais);
+    }
+    br = new BufferedReader(isr);
+    return br;
+  }
+
+  @Deprecated
+  public String getRealPath(String path) {
+    throw new UnsupportedOperationException("This method is deprecated");
+  }
+
+  public String getRemoteAddr() {
+    return ip;
+  }
+
+  /**
+   * Sets the remote IP address for this {@code FakeHttpServletRequest}.
+   *
+   * @param ip the IP to set
+   * @return this {@code FakeHttpServletRequest} object
+   */
+  public FakeHttpServletRequest setRemoteAddr(String ip) {
+    this.ip = ip;
+    return this;
+  }
+
+  public String getRemoteHost() {
+    return "localhost";
+  }
+
+
+  /*
+   * (non-Javadoc)
+   *
+   * New Servlet 2.4 method
+   *
+   * @see javax.servlet.ServletRequest#getLocalPort()
+   */
+  public int getLocalPort() {
+    return 8080;
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * New Servlet 2.4 method
+   *
+   * @see javax.servlet.ServletRequest#getLocalAddr()
+   */
+  public String getLocalAddr() {
+    return "127.0.0.1";
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * New Servlet 2.4 method
+   *
+   * @see javax.servlet.ServletRequest#getLocalName()
+   */
+  public String getLocalName() {
+    return "localhost";
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * New Servlet 2.4 method
+   *
+   * @see javax.servlet.ServletRequest#getRemotePort()
+   */
+  public int getRemotePort() {
+    throw new UnsupportedOperationException();
+  }
+
+
+  public RequestDispatcher getRequestDispatcher(String path) {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getScheme() {
+    return scheme;
+  }
+
+  public String getServerName() {
+    return host;
+  }
+
+  public int getServerPort() {
+    return (port < 0) ? DEFAULT_PORT : port;
+  }
+
+  public boolean isSecure() {
+    return secure;
+  }
+
+  public void removeAttribute(String name) {
+    attributes.remove(name);
+  }
+
+  public void setAttribute(String name, Object value) {
+    attributes.put(name, value);
+  }
+
+  /**
+   * @inheritDoc
+   *
+   * For POST requests, this affects interpretation of POST body.
+   *
+   * For non-POST requests (original author's comment): Do nothing - all request
+   * components were created as unicode Strings, so this can't affect how
+   * they're interpreted anyway.
+   */
+  public void setCharacterEncoding(String env) {
+    if (method.equals(METHOD_POST)) {
+      characterEncoding = env;
+    }
+  }
+
+  // Helper methods ///////////////////////////////////////////////////////////
+
+  /**
+   * This method serves as the central constructor of this class. The reason it
+   * is not an actual constructor is that Java doesn't allow calling another
+   * constructor at the end of a constructor. e.g.
+   *
+   * <pre>
+   * public FakeHttpServletRequest(String foo) {
+   *   // Do something here
+   *   this(foo, bar); // calling another constructor here is not allowed
+   * }
+   * </pre>
+   */
+  protected void constructor(String host, int port, String contextPath,
+      String servletPath, String queryString) {
+    setHeader(HOST_HEADER, host);
+    this.port = port;
+    this.contextPath = contextPath;
+    this.servletPath = servletPath;
+    this.queryString = queryString;
+    if (queryString != null) {
+      decodeQueryString(queryString, parameters);
+    }
+  }
+
+  protected void decodeQueryString(String queryString,
+      Map<String, String[]> parameters) {
+    for (String param : queryString.split("&")) {
+      // The first '=' separates the name and value
+      int sepPos = param.indexOf('=');
+      String name, value;
+      if (sepPos < 0) {
+        // if no equal is present, assume a blank value
+        name = param;
+        value = "";
+      } else {
+        name = param.substring(0, sepPos);
+        value = param.substring(sepPos + 1);
+      }
+
+      addParameter(parameters, decodeParameterPart(name),
+          decodeParameterPart(value));
+    }
+  }
+
+  private String decodeParameterPart(String str) {
+    // borrowed from FormUrlDecoder
+    try {
+      // we could infer proper encoding from headers, but setCharacterEncoding
+      // is a noop.
+      return URLDecoder.decode(str, "UTF-8");
+    } catch (IllegalArgumentException iae) {
+      // According to the javadoc of URLDecoder, when the input string is
+      // illegal, it could either leave the illegal characters alone or throw
+      // an IllegalArgumentException! To deal with both consistently, we
+      // ignore IllegalArgumentException and just return the original string.
+      return str;
+    } catch (UnsupportedEncodingException e) {
+      return str;
+    }
+  }
+
+  protected void addParameter(Map<String, String[]> parameters, String name,
+      String value) {
+    if (parameters.containsKey(name)) {
+      String[] existingParamValues = parameters.get(name);
+      String[] newParamValues = new String[existingParamValues.length + 1];
+      System.arraycopy(existingParamValues, 0, newParamValues, 0,
+          existingParamValues.length);
+      newParamValues[newParamValues.length - 1] = value;
+      parameters.put(name, newParamValues);
+    } else {
+      String[] paramValues = {value,};
+      parameters.put(name, paramValues);
+    }
+  }
+
+  private static String[] splitAndTrim(String str, String delims) {
+    StringTokenizer tokenizer = new StringTokenizer(str, delims);
+    int n = tokenizer.countTokens();
+    String[] list = new String[n];
+    for (int i = 0; i < n; i++) {
+      list[i] = tokenizer.nextToken().trim();
+    }
+    return list;
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/testing/ImmediateExecutorService.java b/trunk/java/common/src/test/java/org/apache/shindig/common/testing/ImmediateExecutorService.java
new file mode 100644
index 0000000..768e492
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/testing/ImmediateExecutorService.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.testing;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.AbstractExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * ExecutorService used for testing. Executes all tasks immediately.
+ */
+public class ImmediateExecutorService extends AbstractExecutorService {
+    private boolean shutdown;
+
+    public void execute(Runnable command) {
+      command.run();
+    }
+
+    public boolean isTerminated() {
+      return shutdown;
+    }
+
+    public boolean isShutdown() {
+      return shutdown;
+    }
+
+    public boolean awaitTermination(long timeout, TimeUnit unit) {
+      return true;
+    }
+
+    public void shutdown() {
+      shutdown = true;
+    }
+
+    public List<Runnable> shutdownNow() {
+      shutdown();
+      return Collections.emptyList();
+    }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/uri/UriBuilderTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/uri/UriBuilderTest.java
new file mode 100644
index 0000000..9a6c6a5
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/uri/UriBuilderTest.java
@@ -0,0 +1,472 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.uri;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static junitx.framework.Assert.assertNotEquals;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Tests for UriBuilder
+ */
+public class UriBuilderTest {
+
+  @Test
+  public void allPartsUsed() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .setQuery("hello=world")
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void noSchemeUsed() {
+    UriBuilder builder = new UriBuilder()
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .setQuery("hello=world")
+        .setFragment("foo");
+    assertEquals("//apache.org/shindig?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void noAuthorityUsed() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setPath("/shindig")
+        .setQuery("hello=world")
+        .setFragment("foo");
+    assertEquals("http:/shindig?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void noPathUsed() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setQuery("hello=world")
+        .setFragment("foo");
+    assertEquals("http://apache.org?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void noQueryUsed() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig#foo", builder.toString());
+  }
+
+  @Test
+  public void noFragmentUsed() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .setQuery("hello=world");
+    assertEquals("http://apache.org/shindig?hello=world", builder.toString());
+  }
+
+  @Test
+  public void hostRelativePaths() {
+    UriBuilder builder = new UriBuilder()
+        .setPath("/shindig")
+        .setQuery("hello=world")
+        .setFragment("foo");
+    assertEquals("/shindig?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void relativePaths() {
+    UriBuilder builder = new UriBuilder()
+        .setPath("foo")
+        .setQuery("hello=world")
+        .setFragment("foo");
+    assertEquals("foo?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void noPathNoHostNoAuthority() {
+    UriBuilder builder = new UriBuilder()
+        .setQuery("hello=world")
+        .setFragment("foo");
+    assertEquals("?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void justSchemeAndAuthority() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org");
+    assertEquals("http://apache.org", builder.toString());
+  }
+
+  @Test
+  public void justPath() {
+    UriBuilder builder = new UriBuilder()
+        .setPath("/shindig");
+    assertEquals("/shindig", builder.toString());
+  }
+
+  @Test
+  public void justAuthorityAndPath() {
+    UriBuilder builder = new UriBuilder()
+        .setAuthority("apache.org")
+        .setPath("/shindig");
+    assertEquals("//apache.org/shindig", builder.toString());
+  }
+
+  @Test
+  public void justQuery() {
+    UriBuilder builder = new UriBuilder()
+        .setQuery("hello=world");
+    assertEquals("?hello=world", builder.toString());
+  }
+
+  @Test
+  public void justFragment() {
+    UriBuilder builder = new UriBuilder()
+        .setFragment("foo");
+    assertEquals("#foo", builder.toString());
+  }
+
+  @Test
+  public void addSingleQueryParameter() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addQueryParameter("hello", "world")
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig?hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void addTwoQueryParameters() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addQueryParameter("hello", "world")
+        .addQueryParameter("foo", "bar")
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig?hello=world&foo=bar#foo", builder.toString());
+  }
+
+  @Test
+  public void iterableQueryParameters() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .putQueryParameter("hello", Lists.newArrayList("world", "monde"))
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig?hello=world&hello=monde#foo", builder.toString());
+  }
+
+  @Test
+  public void removeQueryParameter() {
+    UriBuilder uri = UriBuilder.parse("http://www.example.com/foo?bar=baz&quux=baz");
+    uri.removeQueryParameter("bar");
+    assertEquals("http://www.example.com/foo?quux=baz", uri.toString());
+    uri.removeQueryParameter("quux");
+    assertEquals("http://www.example.com/foo", uri.toString());
+  }
+
+  @Test
+  public void addIdenticalParameters() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addQueryParameter("hello", "world")
+        .addQueryParameter("hello", "goodbye")
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig?hello=world&hello=goodbye#foo", builder.toString());
+  }
+
+  @Test
+  public void addBatchParameters() {
+    Map<String, String> params = Maps.newLinkedHashMap();
+    params.put("foo", "bar");
+    params.put("hello", "world");
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addQueryParameters(params)
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig?foo=bar&hello=world#foo", builder.toString());
+  }
+
+  @Test
+  public void queryStringIsUnescaped() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .setQuery("hello+world=world%26bar");
+    assertEquals("world&bar", builder.getQueryParameter("hello world"));
+  }
+
+  @Test
+  public void queryParamsAreEscaped() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addQueryParameter("hello world", "foo&bar")
+        .setFragment("foo");
+    assertEquals("http://apache.org/shindig?hello+world=foo%26bar#foo", builder.toString());
+    assertEquals("hello+world=foo%26bar", builder.getQuery());
+  }
+
+  @Test
+  public void addSingleFragmentParameter() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addFragmentParameter("hello", "world")
+        .setQuery("foo");
+    assertEquals("http://apache.org/shindig?foo#hello=world", builder.toString());
+  }
+
+  @Test
+  public void addTwoFragmentParameters() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addFragmentParameter("hello", "world")
+        .addFragmentParameter("foo", "bar")
+        .setQuery("foo");
+    assertEquals("http://apache.org/shindig?foo#hello=world&foo=bar", builder.toString());
+  }
+
+  @Test
+  public void iterableFragmentParameters() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .putFragmentParameter("hello", Lists.newArrayList("world", "monde"))
+        .setQuery("foo");
+    assertEquals("http://apache.org/shindig?foo#hello=world&hello=monde", builder.toString());
+  }
+
+  @Test
+  public void removeFragmentParameter() {
+    UriBuilder uri = UriBuilder.parse("http://www.example.com/foo#bar=baz&quux=baz");
+    uri.removeFragmentParameter("bar");
+    assertEquals("http://www.example.com/foo#quux=baz", uri.toString());
+    uri.removeFragmentParameter("quux");
+    assertEquals("http://www.example.com/foo", uri.toString());
+  }
+
+  @Test
+  public void addIdenticalFragmentParameters() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addFragmentParameter("hello", "world")
+        .addFragmentParameter("hello", "goodbye")
+        .setQuery("foo");
+    assertEquals("http://apache.org/shindig?foo#hello=world&hello=goodbye", builder.toString());
+  }
+
+  @Test
+  public void addBatchFragmentParameters() {
+    Map<String, String> params = Maps.newLinkedHashMap();
+    params.put("foo", "bar");
+    params.put("hello", "world");
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addFragmentParameters(params)
+        .setQuery("foo");
+    assertEquals("http://apache.org/shindig?foo#foo=bar&hello=world", builder.toString());
+  }
+
+  @Test
+  public void fragmentStringIsUnescaped() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .setFragment("hello+world=world%26bar");
+    assertEquals("world&bar", builder.getFragmentParameter("hello world"));
+  }
+
+  @Test
+  public void fragmentParamsAreEscaped() {
+    UriBuilder builder = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("apache.org")
+        .setPath("/shindig")
+        .addFragmentParameter("hello world", "foo&bar")
+        .setQuery("foo");
+    assertEquals("http://apache.org/shindig?foo#hello+world=foo%26bar", builder.toString());
+    assertEquals("hello+world=foo%26bar", builder.getFragment());
+  }
+
+  @Test
+  public void parse() {
+    UriBuilder builder = UriBuilder.parse("http://apache.org/shindig?foo=bar%26baz&foo=three#blah");
+
+    assertEquals("http", builder.getScheme());
+    assertEquals("apache.org", builder.getAuthority());
+    assertEquals("/shindig", builder.getPath());
+    assertEquals("foo=bar%26baz&foo=three", builder.getQuery());
+    assertEquals("blah", builder.getFragment());
+
+    assertEquals("bar&baz", builder.getQueryParameter("foo"));
+
+    List<String> values = Arrays.asList("bar&baz", "three");
+    assertEquals(values, builder.getQueryParameters("foo"));
+  }
+
+  @Test
+  public void constructFromUriAndBack() {
+    Uri uri = Uri.parse("http://apache.org/foo/bar?foo=bar&a=b&c=d&y=z&foo=zoo#foo");
+    UriBuilder builder = new UriBuilder(uri);
+
+    assertEquals(uri, builder.toUri());
+  }
+
+  @Test
+  public void constructFromUriAndModify() {
+    Uri uri = Uri.parse("http://apache.org/foo/bar?foo=bar#foo");
+    UriBuilder builder = new UriBuilder(uri);
+
+    builder.setAuthority("example.org");
+    builder.addQueryParameter("bar", "foo");
+
+    assertEquals("http://example.org/foo/bar?foo=bar&bar=foo#foo", builder.toString());
+  }
+
+  @Test
+  public void equalsAndHashCodeOk() {
+    UriBuilder uri = UriBuilder.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    UriBuilder uri2 = new UriBuilder(Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo"));
+
+    assertEquals(uri, uri2);
+    assertEquals(uri2, uri);
+
+    assertEquals(uri, uri);
+
+    assertNotNull(uri);
+    assertNotEquals(uri, "http://example.org/foo/bar/baz?blah=blah#boo");
+    assertNotEquals(uri, Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo"));
+
+    assertEquals(uri.hashCode(), uri2.hashCode());
+  }
+
+  @Test
+  public void constructFromServletRequestHttpStandardPortAndModify() {
+    HttpServletRequest req = createMock(HttpServletRequest.class);
+    expect(req.getScheme()).andReturn("http").once();
+    expect(req.getServerName()).andReturn("example.com");
+    expect(req.getServerPort()).andReturn(80).once();
+    expect(req.getRequestURI()).andReturn("/my/path");
+    expect(req.getQueryString()).andReturn("foo=bar&baz=bak");
+    replay(req);
+
+    UriBuilder builder = new UriBuilder(req);
+    verify(req);
+
+    assertEquals("http://example.com/my/path?foo=bar&baz=bak", builder.toString());
+    assertEquals("bar", builder.getQueryParameter("foo"));  // sanity check on a single param
+    assertEquals(0, builder.getFragmentParameters().size());  // shouldn't NPE
+
+    // Simple modification
+    builder.setPath("/other/path");
+    assertEquals("http://example.com/other/path?foo=bar&baz=bak", builder.toString());
+  }
+
+  @Test
+  public void constructFromServletRequestHttpsStandardPort() {
+    HttpServletRequest req = createMock(HttpServletRequest.class);
+    expect(req.getScheme()).andReturn("https").once();
+    expect(req.getServerName()).andReturn("example.com");
+    expect(req.getServerPort()).andReturn(443).once();
+    expect(req.getRequestURI()).andReturn("/my/path");
+    expect(req.getQueryString()).andReturn("foo=bar&baz=bak");
+    replay(req);
+
+    UriBuilder builder = new UriBuilder(req);
+    verify(req);
+
+    assertEquals("https://example.com/my/path?foo=bar&baz=bak", builder.toString());
+  }
+
+  @Test
+  public void constructFromServletRequestNonStandardPort() {
+    HttpServletRequest req = createMock(HttpServletRequest.class);
+    expect(req.getScheme()).andReturn("HtTp").once();  // Shouldn't happen but try anyway.
+    expect(req.getServerName()).andReturn("example.com");
+    expect(req.getServerPort()).andReturn(5000).once();
+    expect(req.getRequestURI()).andReturn("/my/path");
+    expect(req.getQueryString()).andReturn("one=two&three=four");
+    replay(req);
+
+    UriBuilder builder = new UriBuilder(req);
+    verify(req);
+
+    assertEquals("http://example.com:5000/my/path?one=two&three=four", builder.toString());
+  }
+
+  @Test
+  public void constructFromServletRequestNonePort() {
+    HttpServletRequest req = createMock(HttpServletRequest.class);
+    expect(req.getScheme()).andReturn("http").once();  // Shouldn't happen but try anyway.
+    expect(req.getServerName()).andReturn("example.com");
+    expect(req.getServerPort()).andReturn(-1).once();  // No port specified (0 or -1)
+    expect(req.getRequestURI()).andReturn("/my/path");
+    expect(req.getQueryString()).andReturn("one=two&three=four");
+    replay(req);
+
+    UriBuilder builder = new UriBuilder(req);
+    verify(req);
+
+    assertEquals("http://example.com/my/path?one=two&three=four", builder.toString());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/uri/UriTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/uri/UriTest.java
new file mode 100644
index 0000000..cac3a1e
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/uri/UriTest.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.uri;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.junit.Ignore;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+
+/**
+ * Tests for Uri.
+ */
+public class UriTest {
+  @Test
+  public void parseFull() {
+    Uri uri = Uri.parse("http://apache.org/foo?a=b&a=c&b=d+e#blah");
+
+    assertEquals("http", uri.getScheme());
+    assertEquals("apache.org", uri.getAuthority());
+    assertEquals("/foo", uri.getPath());
+    assertEquals("a=b&a=c&b=d+e", uri.getQuery());
+    Collection<String> params = Arrays.asList("b", "c");
+    assertEquals(params, uri.getQueryParameters("a"));
+    assertEquals("b", uri.getQueryParameter("a"));
+    assertEquals("d e", uri.getQueryParameter("b"));
+    assertEquals("blah", uri.getFragment());
+
+    assertEquals("http://apache.org/foo?a=b&a=c&b=d+e#blah", uri.toString());
+  }
+
+  @Test
+  public void parseNoScheme() {
+    Uri uri = Uri.parse("//apache.org/foo?a=b&a=c&b=d+e#blah");
+
+    assertNull(uri.getScheme());
+    assertEquals("apache.org", uri.getAuthority());
+    assertEquals("/foo", uri.getPath());
+    assertEquals("a=b&a=c&b=d+e", uri.getQuery());
+    Collection<String> params = Arrays.asList("b", "c");
+    assertEquals(params, uri.getQueryParameters("a"));
+    assertEquals("b", uri.getQueryParameter("a"));
+    assertEquals("d e", uri.getQueryParameter("b"));
+    assertEquals("blah", uri.getFragment());
+  }
+
+  @Test
+  public void parseNoAuthority() {
+    Uri uri = Uri.parse("http:/foo?a=b&a=c&b=d+e#blah");
+
+    assertEquals("http", uri.getScheme());
+    assertNull(uri.getAuthority());
+    assertEquals("/foo", uri.getPath());
+    assertEquals("a=b&a=c&b=d+e", uri.getQuery());
+    Collection<String> params = Arrays.asList("b", "c");
+    assertEquals(params, uri.getQueryParameters("a"));
+    assertEquals("b", uri.getQueryParameter("a"));
+    assertEquals("d e", uri.getQueryParameter("b"));
+    assertEquals("blah", uri.getFragment());
+  }
+
+  @Test
+  public void parseNoPath() {
+    Uri uri = Uri.parse("http://apache.org?a=b&a=c&b=d+e#blah");
+
+    assertEquals("http", uri.getScheme());
+    assertEquals("apache.org", uri.getAuthority());
+    // Path is never null.
+    assertEquals("", uri.getPath());
+    assertEquals("a=b&a=c&b=d+e", uri.getQuery());
+    Collection<String> params = Arrays.asList("b", "c");
+    assertEquals(params, uri.getQueryParameters("a"));
+    assertEquals("b", uri.getQueryParameter("a"));
+    assertEquals("d e", uri.getQueryParameter("b"));
+    assertEquals("blah", uri.getFragment());
+  }
+
+  @Test
+  public void parseNoQuery() {
+    Uri uri = Uri.parse("http://apache.org/foo#blah");
+
+    assertEquals("http", uri.getScheme());
+    assertEquals("apache.org", uri.getAuthority());
+    assertEquals("/foo", uri.getPath());
+    assertNull(uri.getQuery());
+    assertEquals(0, uri.getQueryParameters().size());
+    assertNull(uri.getQueryParameter("foo"));
+    assertEquals("blah", uri.getFragment());
+  }
+
+  @Test
+  public void parseNoFragment() {
+    Uri uri = Uri.parse("http://apache.org/foo?a=b&a=c&b=d+e");
+
+    assertEquals("http", uri.getScheme());
+    assertEquals("apache.org", uri.getAuthority());
+    assertEquals("/foo", uri.getPath());
+    assertEquals("a=b&a=c&b=d+e", uri.getQuery());
+    assertNull(uri.getFragment());
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseInvalidHost() {
+    Uri.parse("http://A&E%#%#%/foo?a=b#blah");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseInvalidScheme() {
+    Uri.parse("----://apache.org/foo?a=b#blah");
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseInvalidPath() {
+    Uri.parse("http://apache.org/foo\\---(&%?a=b#blah");
+  }
+
+  @Test
+  public void toJavaUri() {
+    URI javaUri = URI.create("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri uri = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+
+    assertEquals(javaUri, uri.toJavaUri());
+  }
+
+  @Test
+  public void toJavaUriWithSpecialChars() {
+    URI javaUri = URI.create("http://example.org/foo/bar/baz?blah=bl%25ah#boo");
+    Uri uri = Uri.parse("http://example.org/foo/bar/baz?blah=bl%25ah#boo");
+
+    assertEquals(javaUri, uri.toJavaUri());
+  }
+
+  @Test
+  public void fromJavaUri() throws Exception {
+    URI javaUri = URI.create("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri uri = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+
+    assertEquals(uri, Uri.fromJavaUri(javaUri));
+  }
+
+  @Test
+  public void resolveFragment() throws Exception {
+    Uri base = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("#bar");
+
+    assertEquals("http://example.org/foo/bar/baz?blah=blah#bar", base.resolve(other).toString());
+  }
+
+  @Test
+  public void resolveQuery() throws Exception {
+    Uri base = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("?hello=world");
+
+    assertEquals("http://example.org/foo/bar/?hello=world", base.resolve(other).toString());
+  }
+
+  @Test
+  public void resolvePathIncludesSubdirs() throws Exception {
+    Uri base = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("fez/../huey/./dewey/../louis");
+
+    assertEquals("http://example.org/foo/bar/huey/louis", base.resolve(other).toString());
+  }
+
+  // Ignore for now..
+  @Ignore
+  public void resolvePathSubdirsExtendsBeyondRoot() throws Exception {
+    Uri base = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("../random/../../../../../home");
+
+    assertEquals("http://example.org/home", base.resolve(other).toString());
+  }
+
+  @Test
+  public void resolvePathRelative() throws Exception {
+    Uri base = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("wee");
+
+    assertEquals("http://example.org/foo/bar/wee", base.resolve(other).toString());
+  }
+
+  @Test
+  public void resolvePathRelativeToNullPath() throws Exception {
+    Uri base = new UriBuilder().setScheme("http").setAuthority("example.org").toUri();
+    Uri other = Uri.parse("dir");
+
+    assertEquals("http://example.org/dir", base.resolve(other).toString());
+  }
+
+  @Test
+  public void resolvePathAbsolute() throws Exception {
+    Uri base = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("/blah");
+
+    assertEquals("http://example.org/blah", base.resolve(other).toString());
+  }
+
+  @Test
+  public void resolveAuthority() throws Exception {
+    Uri base = Uri.parse("https://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("//example.com/blah");
+
+    assertEquals("https://example.com/blah", base.resolve(other).toString());
+  }
+
+  @Test
+  public void resolveAbsolute() throws Exception {
+    Uri base = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri other = Uri.parse("http://www.ietf.org/rfc/rfc2396.txt");
+
+    assertEquals("http://www.ietf.org/rfc/rfc2396.txt", base.resolve(other).toString());
+  }
+
+  @Test
+  public void absoluteUrlIsAbsolute() {
+    assertTrue("Url with scheme not reported absolute.",
+        Uri.parse("http://example.org/foo").isAbsolute());
+  }
+
+  @Test
+  public void relativeUrlIsNotAbsolute() {
+    assertFalse("Url without scheme reported absolute.",
+        Uri.parse("//example.org/foo").isAbsolute());
+  }
+
+  @Test
+  public void parseWithSpecialCharacters() {
+    String original = "http://example.org/?foo%25pbar=baz+blah";
+
+    assertEquals(original, Uri.parse(original).toString());
+  }
+
+  @Test
+  public void equalsAndHashCodeOk() {
+    Uri uri = Uri.parse("http://example.org/foo/bar/baz?blah=blah#boo");
+    Uri uri2 = new UriBuilder()
+        .setScheme("http")
+        .setAuthority("example.org")
+        .setPath("/foo/bar/baz")
+        .addQueryParameter("blah", "blah")
+        .setFragment("boo")
+        .toUri();
+
+    assertEquals(uri, uri2);
+    assertEquals(uri2, uri);
+
+    assertEquals(uri.hashCode(), uri2.hashCode());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/util/CharsetUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/util/CharsetUtilTest.java
new file mode 100644
index 0000000..a880f53
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/util/CharsetUtilTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.junit.Test;
+
+import junitx.framework.ArrayAssert;
+
+/**
+ * Tests for CharsetUtil.
+ */
+public class CharsetUtilTest {
+
+  @Test
+  public void testGetUtf8String() {
+    ArrayAssert.assertEquals(new byte[] { 0x69, 0x6e }, CharsetUtil.getUtf8Bytes("in"));
+    ArrayAssert.assertEquals(ArrayUtils.EMPTY_BYTE_ARRAY, CharsetUtil.getUtf8Bytes(null));
+    testStringOfLength(0);
+    testStringOfLength(10);
+    testStringOfLength(100);
+    testStringOfLength(1000);
+  }
+
+  private void testStringOfLength(int len) {
+    StringBuilder sb = new StringBuilder();
+    for (int i=0; i < len; ++i) {
+      sb.append('a');
+    }
+    byte[] out = CharsetUtil.getUtf8Bytes(sb.toString());
+    assertEquals(len, out.length);
+    for (int i=0; i < len; ++i) {
+      assertEquals('a', out[i]);
+    }
+  }
+
+
+  private static final byte[] LATIN1_UTF8_DATA = {
+    'G', 'a', 'm', 'e', 's', ',', ' ', 'H', 'Q', ',', ' ', 'M', 'a', 'n', 'g', (byte)0xC3,
+    (byte) 0xA1, ',', ' ', 'A', 'n', 'i', 'm', 'e', ' ', 'e', ' ', 't', 'u', 'd', 'o', ' ',
+    'q', 'u', 'e', ' ', 'u', 'm', ' ', 'b', 'o', 'm', ' ', 'n', 'e', 'r', 'd', ' ', 'a', 'm', 'a'
+  };
+
+  private static final String LATIN1_STRING
+      = "Games, HQ, Mang\u00E1, Anime e tudo que um bom nerd ama";
+
+  @Test
+  public void testLatin1() {
+    ArrayAssert.assertEquals(LATIN1_UTF8_DATA, CharsetUtil.getUtf8Bytes(LATIN1_STRING));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/util/DateUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/util/DateUtilTest.java
new file mode 100644
index 0000000..5feca0f
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/util/DateUtilTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Test;
+
+import java.util.Date;
+import java.util.Locale;
+
+public class DateUtilTest {
+
+  String[] rfc1123text = new String[] {
+    "Tue, 27 May 2008 05:12:50 GMT",
+    "Wed, 28 May 2008 04:40:48 GMT",
+    "Mon, 30 Jun 3090 03:29:55 GMT",
+    "Fri, 06 Jun 1670 01:57:27 GMT",
+  };
+
+  String[] iso8601text = new String[] {
+          "2008-05-27T05:12:50.000Z",
+          "2008-05-28T04:40:48.000Z",
+          "3090-06-30T03:29:55.000Z",
+          "1670-06-06T01:57:27.000Z"
+   };
+
+  Date[] timeStamps = new Date[] {
+    new Date(1211865170000L),
+    new Date(1211949648000L),
+    new Date(35359385395000L),
+    new Date(-9453535353000L),
+  };
+
+  @Test
+  public void parse() {
+    for (int i = 0, j = rfc1123text.length; i < j; ++i) {
+      assertEquals(timeStamps[i].getTime(), DateUtil.parseRfc1123Date(rfc1123text[i]).getTime());
+    }
+  }
+
+  @Test
+  public void format() {
+    for (int i = 0, j = timeStamps.length; i < j; ++i) {
+      assertEquals(rfc1123text[i], DateUtil.formatRfc1123Date(timeStamps[i].getTime()));
+    }
+  }
+
+  @Test
+  public void formatIso8601() {
+      for (int i = 0, j = timeStamps.length; i < j; ++i) {
+          assertEquals(iso8601text[i], DateUtil.formatIso8601Date(timeStamps[i].getTime()));
+      }
+  }
+
+  @Test
+  public void formatRfc1123Date() {
+    for (int i = 0, j = timeStamps.length; i < j; ++i) {
+      assertEquals(rfc1123text[i], DateUtil.formatRfc1123Date(timeStamps[i]));
+    }
+  }
+
+  @Test
+  public void formatIso8601Date() {
+      for (int i = 0, j = timeStamps.length; i < j; ++i) {
+          assertEquals(iso8601text[i], DateUtil.formatIso8601Date(timeStamps[i]));
+      }
+  }
+
+  @Test
+  public void parseMalformedRfc1123() {
+    assertNull(DateUtil.parseRfc1123Date("Invalid date format"));
+  }
+
+  @Test
+  public void parseMalformedIso8691() {
+      assertNull(DateUtil.parseIso8601DateTime("invalid date format"));
+  }
+
+  @Test
+  public void parseWrongTimeZone() {
+    String expires = "Mon, 12 May 2008 09:23:29 PDT";
+    assertNull(DateUtil.parseRfc1123Date(expires));
+  }
+
+  @Test
+  public void parseRfc1036() {
+    // We don't support this, though RFC 2616 suggests we should
+    String expires = "Sunday, 06-Nov-94 08:49:37 GMT";
+    assertNull(DateUtil.parseRfc1123Date(expires));
+  }
+
+  @Test
+  public void parseAsctime() {
+    // We don't support this, though RFC 2616 suggests we should
+    String expires = "Sun Nov  6 08:49:37 1994";
+    assertNull(DateUtil.parseRfc1123Date(expires));
+  }
+
+  @Test
+  public void formatInWrongLocale() {
+    Locale orig = Locale.getDefault();
+    try {
+      Locale.setDefault(Locale.ITALY);
+      formatRfc1123Date();
+    } finally {
+      Locale.setDefault(orig);
+    }
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/util/FakeTimeSource.java b/trunk/java/common/src/test/java/org/apache/shindig/common/util/FakeTimeSource.java
new file mode 100644
index 0000000..1fbbcb9
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/util/FakeTimeSource.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import org.apache.shindig.common.util.TimeSource;
+
+/**
+ * Fake time source for dependency injection.
+ */
+public class FakeTimeSource extends TimeSource {
+
+  public long now;
+
+  public FakeTimeSource() {
+    this(System.currentTimeMillis());
+  }
+
+  public FakeTimeSource(long now) {
+    this.now = now;
+  }
+
+  @Override
+  public long currentTimeMillis() {
+    return now;
+  }
+
+  public void setCurrentTimeMillis(long now) {
+    this.now = now;
+  }
+
+  public void incrementSeconds(int seconds) {
+    now += seconds*1000;
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/util/HashUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/util/HashUtilTest.java
new file mode 100644
index 0000000..fe7898d
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/util/HashUtilTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class HashUtilTest {
+  @Test
+  public void testChecksum() throws Exception {
+    byte[] data = new byte[] {Byte.MAX_VALUE, Byte.MIN_VALUE};
+    assertEquals("d41d8cd98f00b204e9800998ecf8427e", HashUtil.checksum(new byte[]{}));
+    assertEquals("a494a8690b72391b13d3cbe27edb5c58", HashUtil.checksum(new byte[]{Byte.MAX_VALUE, Byte.MIN_VALUE}));
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testChecksumNPE() {
+    HashUtil.checksum(null);
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void testRawChecksumNPE() {
+    HashUtil.rawChecksum(null);
+  }
+
+  @Test
+  public void testRawChecksum() throws Exception {
+    // this is lame, but different platforms/charsets mangle this.
+    assertNotNull(HashUtil.rawChecksum(new byte[]{}));
+    assertNotNull(HashUtil.rawChecksum(new byte[]{Byte.MAX_VALUE, Byte.MIN_VALUE}));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java
new file mode 100644
index 0000000..c03516a
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/util/JsonConversionUtilTest.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Map;
+
+/**
+ * Test for conversion of a structured key-value set to a JSON object
+ */
+public class JsonConversionUtilTest extends Assert {
+
+  @Test
+  public void testSimplePathToJsonParsing()
+      throws Exception {
+    JSONObject root = new JSONObject();
+    JsonConversionUtil.buildHolder(root, "a.a.a".split("\\."), 0);
+    assertJsonEquals(root, new JSONObject("{a:{a:{}}}"));
+  }
+
+  @Test
+  public void testArrayPathToJsonParsing()
+      throws Exception {
+    JSONObject root = new JSONObject();
+    JsonConversionUtil.buildHolder(root, "a.a(0).a".split("\\."), 0);
+    JsonConversionUtil.buildHolder(root, "a.a(1).a".split("\\."), 0);
+    JsonConversionUtil.buildHolder(root, "a.a(2).a".split("\\."), 0);
+    assertJsonEquals(root, new JSONObject("{a:{a:[{},{},{}]}}"));
+  }
+
+  @Test
+  public void testValueToJsonParsing()
+      throws Exception {
+    String longNumber = "108502345354398668456";
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue(longNumber), longNumber);
+    String longDoubleOverflow = "108502345354398668456.1234";
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue(longDoubleOverflow),
+        longDoubleOverflow);
+    String longDoubleFractionPart = "1.108502345354398668456108502345354398668456";
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue(longDoubleFractionPart),
+        longDoubleFractionPart);
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("12345"), 12345);
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("12.345"), 12.345);
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("abc"), "abc");
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("\"a,b,c\""), "a,b,c");
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("true"), true);
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("false"), false);
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("null"), JSONObject.NULL);
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("'abc'"), "abc");
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("a,b,c"),
+        new JSONArray(Lists.newArrayList("a", "b", "c")));
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("1,2,3,true,false,null"),
+        new JSONArray(Lists.<Object>newArrayList(1, 2, 3, true,
+            false, null)));
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("(1)"),
+        new JSONArray(Lists.newArrayList(1)));
+    assertJsonEquals(JsonConversionUtil.convertToJsonValue("(true)"),
+        new JSONArray(Lists.newArrayList(true)));
+  }
+
+  @Test
+  public void testParameterMapToJsonParsing()
+      throws Exception {
+    assertJsonEquals(JsonConversionUtil.parametersToJsonObject(ImmutableMap.of("a.b.c", "1")),
+        new JSONObject("{a:{b:{c:1}}}"));
+    assertJsonEquals(
+        JsonConversionUtil.parametersToJsonObject(ImmutableMap.of("a.b.c", "\"1\"")),
+        new JSONObject("{a:{b:{c:\"1\"}}}"));
+    assertJsonEquals(JsonConversionUtil.parametersToJsonObject(ImmutableMap.of("a.b.c", "true")),
+        new JSONObject("{a:{b:{c:true}}}"));
+    assertJsonEquals(
+        JsonConversionUtil.parametersToJsonObject(ImmutableMap.of("a.b.c", "false")),
+        new JSONObject("{a:{b:{c:false}}}"));
+    assertJsonEquals(JsonConversionUtil.parametersToJsonObject(ImmutableMap.of("a.b.c", "null")),
+        new JSONObject("{a:{b:{c:null}}}"));
+    assertJsonEquals(JsonConversionUtil.parametersToJsonObject(
+        ImmutableMap.of("a.b(0).c", "hello", "a.b(1).c", "hello")),
+        new JSONObject("{a:{b:[{c:\"hello\"},{c:\"hello\"}]}}"));
+    assertJsonEquals(JsonConversionUtil.parametersToJsonObject(
+        ImmutableMap.of("a.b.c", "hello, true, false, null, 1,2, \"null\", \"()\"")),
+        new JSONObject("{a:{b:{c:[\"hello\",true,false,null,1,2,\"null\",\"()\"]}}}"));
+    assertJsonEquals(JsonConversionUtil.parametersToJsonObject(
+        ImmutableMap.of("a.b.c", "\"hello, true, false, null, 1,2\"")),
+        new JSONObject("{a:{b:{c:\"hello, true, false, null, 1,2\"}}}"));
+  }
+
+  @Test
+  public void testJSONToParameterMapParsing()
+      throws Exception {
+    Map<String, String> resultMap = JsonConversionUtil
+        .fromJson(new JSONObject("{a:{b:[{c:\"hello\"},{c:\"hello\"}]}}"));
+    assertEquals(2, resultMap.size());
+    assertEquals("hello", resultMap.get(".a.b(0).c"));
+    assertEquals("hello", resultMap.get(".a.b(1).c"));
+  }
+
+  @Test
+  public void testJsonFromRequest() throws Exception {
+    HttpServletRequest fakeRequest;
+    for (String badParms : ImmutableList.of("x=1", "x=1&callback=")) {
+      fakeRequest = new FakeHttpServletRequest("http://foo.com/gadgets/rpc?" + badParms);
+      assertNull(JsonConversionUtil.fromRequest(fakeRequest));
+    }
+   }
+
+  public static void assertJsonEquals(Object expected, Object actual)
+      throws JSONException {
+    if (expected == null) {
+      assertNull(actual);
+      return;
+    }
+    assertNotNull(actual);
+    if (expected instanceof JSONObject) {
+      JSONObject expectedObject = (JSONObject) expected;
+      JSONObject actualObject = (JSONObject) actual;
+      if (expectedObject.length() == 0) {
+        assertEquals(expectedObject.length(), actualObject.length());
+        return;
+      }
+      assertEquals(expectedObject.names().length(), actualObject.names().length());
+
+      for (String key : JSONObject.getNames(expectedObject)) {
+        assertTrue("missing key " + key, actualObject.has(key));
+        assertJsonEquals(expectedObject.get(key), actualObject.get(key));
+      }
+    } else if (expected instanceof JSONArray) {
+      JSONArray expectedArray = (JSONArray) expected;
+      JSONArray actualArray = (JSONArray) actual;
+      assertEquals(expectedArray.length(), actualArray.length());
+      for (int i = 0; i < expectedArray.length(); i++) {
+        if (expectedArray.isNull(i)) {
+          assertTrue(actualArray.isNull(i));
+        } else {
+          assertJsonEquals(expectedArray.get(i), actualArray.get(i));
+        }
+      }
+
+    } else {
+      assertEquals(expected, actual);
+    }
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/util/StringEncodingTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/util/StringEncodingTest.java
new file mode 100644
index 0000000..93828f7
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/util/StringEncodingTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.util.StringEncoding;
+
+import org.junit.Test;
+
+public class StringEncodingTest {
+
+  @Test
+  public void testBase32() throws Exception {
+    StringEncoding encoder = new StringEncoding(
+        "0123456789abcdefghijklmnopqrstuv".toCharArray());
+    testEncoding(encoder, new byte[] { 0 }, "00");
+    testEncoding(encoder, new byte[] { 0, 0 }, "0000");
+    testEncoding(encoder, new byte[] { 10, 0 }, "1800");
+    testRoundTrip(encoder, Crypto.getRandomBytes(1));
+    testRoundTrip(encoder, Crypto.getRandomBytes(2));
+    testRoundTrip(encoder, Crypto.getRandomBytes(3));
+    testRoundTrip(encoder, Crypto.getRandomBytes(20));
+    testRoundTrip(encoder, Crypto.getRandomBytes(30));
+  }
+
+  private void testRoundTrip(StringEncoding encoder, byte[] bytes) {
+    String encoded = encoder.encode(bytes);
+    byte[] decoded = encoder.decode(encoded);
+    assertArrayEquals(bytes, decoded);
+  }
+
+  private void testEncoding(StringEncoding encoder, byte[] b, String s) {
+    String encoded = encoder.encode(b);
+    assertEquals(s, encoded);
+    byte[] decoded = encoder.decode(encoded);
+    assertArrayEquals(b, decoded);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/util/Utf8UrlCoderTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/util/Utf8UrlCoderTest.java
new file mode 100644
index 0000000..aa7aff7
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/util/Utf8UrlCoderTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.util;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class Utf8UrlCoderTest {
+  private static final String RAW_SIMPLE = "Hello, world!";
+  private static final String ENCODED_SIMPLE = "Hello%2C+world%21";
+  private static final String RAW_COMPLEX = "\u4F60\u597D";
+  private static final String ENCODED_COMPLEX = "%E4%BD%A0%E5%A5%BD";
+
+  @Test
+  public void encodeSimple() {
+    assertEquals(ENCODED_SIMPLE, Utf8UrlCoder.encode(RAW_SIMPLE));
+  }
+
+  @Test
+  public void decodeSimple() {
+    assertEquals(RAW_SIMPLE, Utf8UrlCoder.decode(ENCODED_SIMPLE));
+  }
+
+  @Test
+  public void encodeComplex() {
+    assertEquals(ENCODED_COMPLEX, Utf8UrlCoder.encode(RAW_COMPLEX));
+  }
+
+  @Test
+  public void decodeComplex() {
+    assertEquals(RAW_COMPLEX, Utf8UrlCoder.decode(ENCODED_COMPLEX));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/xml/DomUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/xml/DomUtilTest.java
new file mode 100644
index 0000000..7a85ea3
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/xml/DomUtilTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.xml;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+public class DomUtilTest {
+  private static final String XML =
+      "<root>" +
+      "  <other>whatever</other>" +
+      "  <element>zero</element>" +
+      "  <ElEmEnT>one</ElEmEnT>" +
+      "  <element>two</element>" +
+      "  <other>not real</other>" +
+      "</root>";
+
+  private static Element root;
+
+  @BeforeClass
+  public static void createRoot() throws XmlException {
+    root = XmlUtil.parse(XML);
+  }
+
+  @Test
+  public void getFirstNamedChildNode() {
+    assertEquals("zero", DomUtil.getFirstNamedChildNode(root, "element").getTextContent());
+    assertEquals("whatever", DomUtil.getFirstNamedChildNode(root, "other").getTextContent());
+    assertNull("Did not return null for missing element.",
+        DomUtil.getFirstNamedChildNode(root, "fake"));
+  }
+
+  @Test
+  public void getLastNamedChildNode() {
+    assertEquals("two", DomUtil.getLastNamedChildNode(root, "element").getTextContent());
+    assertEquals("not real", DomUtil.getLastNamedChildNode(root, "other").getTextContent());
+    assertNull("Did not return null for missing element.",
+        DomUtil.getLastNamedChildNode(root, "fake"));
+  }
+
+  @Test
+  public void getElementsByTagNameCaseInsensitive() {
+    Document doc = root.getOwnerDocument();
+    List<Element> elements
+        = DomUtil.getElementsByTagNameCaseInsensitive(doc, ImmutableSet.of("element"));
+    assertEquals("zero", elements.get(0).getTextContent());
+    assertEquals("one", elements.get(1).getTextContent());
+    assertEquals("two", elements.get(2).getTextContent());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/common/xml/XmlUtilTest.java b/trunk/java/common/src/test/java/org/apache/shindig/common/xml/XmlUtilTest.java
new file mode 100644
index 0000000..ba1a6a5
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/common/xml/XmlUtilTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.common.xml;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+/**
+ * Tests for XmlUtil
+ */
+public class XmlUtilTest {
+  private final static String STRING_ATTR = "string";
+  private final static String STRING_VALUE = "some string value";
+  private final static String INT_ATTR = "int";
+  private final static int INT_VALUE = 10;
+  private final static String BOOL_TRUE_ATTR = "bool-true";
+  private final static String BOOL_FALSE_ATTR = "bool-false";
+  private final static String URI_ATTR = "uri";
+  private final static Uri URI_VALUE = Uri.parse("http://example.org/file");
+  private final static String URI_MALFORMED_ATTR = "uri-malformed";
+  private final static String FAKE_ATTR = "fake";
+  private final static String HTTPS_URI_ATTR = "httpsuri";
+  private final static Uri HTTPS_URI_VALUE = Uri.parse("https://example.org");
+  private final static String FTP_URI_ATTR = "ftpuri";
+  private final static Uri FTP_URI_VALUE = Uri.parse("ftp://ftp.example.org");
+
+  private final static String XML
+      = "<Element " +
+      STRING_ATTR + "='" + STRING_VALUE + "' " +
+      INT_ATTR + "='" + INT_VALUE + "' " +
+      BOOL_TRUE_ATTR + "='true' " +
+      BOOL_FALSE_ATTR + "='false' " +
+      URI_ATTR + "='" + URI_VALUE + "' " +
+      URI_MALFORMED_ATTR + "='$#%$^$^$^$%$%!! ' " +
+      HTTPS_URI_ATTR + "='" + HTTPS_URI_VALUE + "' " +
+      FTP_URI_ATTR + "='" + FTP_URI_VALUE + "' " +
+      "/>";
+
+  private Element node;
+
+  @Before
+  public void makeElement() throws XmlException {
+    node = XmlUtil.parse(XML);
+  }
+
+  @Test
+  public void getAttribute() {
+    assertEquals(STRING_VALUE, XmlUtil.getAttribute(node, STRING_ATTR));
+    assertEquals(STRING_VALUE, XmlUtil.getAttribute(node, FAKE_ATTR, STRING_VALUE));
+    assertNull("getAttribute must return null for undefined attributes.",
+        XmlUtil.getAttribute(node, FAKE_ATTR));
+  }
+
+  @Test
+  public void getIntAttribute() {
+    assertEquals(INT_VALUE, XmlUtil.getIntAttribute(node, INT_ATTR));
+    assertEquals(INT_VALUE, XmlUtil.getIntAttribute(node, FAKE_ATTR, INT_VALUE));
+    assertEquals(INT_VALUE, XmlUtil.getIntAttribute(node, STRING_ATTR, INT_VALUE));
+    assertEquals("getIntAttribute must return 0 for undefined attributes.",
+        0, XmlUtil.getIntAttribute(node, FAKE_ATTR));
+  }
+
+  @Test
+  public void getBoolAttribute() {
+    assertTrue(XmlUtil.getBoolAttribute(node, BOOL_TRUE_ATTR));
+    assertFalse(XmlUtil.getBoolAttribute(node, BOOL_FALSE_ATTR));
+    assertTrue(XmlUtil.getBoolAttribute(node, FAKE_ATTR, true));
+    assertFalse(XmlUtil.getBoolAttribute(node, FAKE_ATTR, false));
+    assertFalse("getBoolAttribute must return false for undefined attributes.",
+        XmlUtil.getBoolAttribute(node, FAKE_ATTR));
+  }
+
+  @Test
+  public void getUriAttribute() {
+    assertEquals(URI_VALUE, XmlUtil.getUriAttribute(node, URI_ATTR));
+    assertEquals(URI_VALUE, XmlUtil.getUriAttribute(node, FAKE_ATTR, URI_VALUE));
+    assertEquals(URI_VALUE, XmlUtil.getUriAttribute(node, URI_MALFORMED_ATTR, URI_VALUE));
+    assertNull("getUriAttribute must return null for undefined attributes.",
+        XmlUtil.getUriAttribute(node, FAKE_ATTR));
+    assertEquals(FTP_URI_VALUE, XmlUtil.getUriAttribute(node, FTP_URI_ATTR));
+  }
+
+  @Test
+  public void testHttpUriAttribute() {
+    assertEquals(HTTPS_URI_VALUE, XmlUtil.getHttpUriAttribute(node, HTTPS_URI_ATTR, null));
+    assertNull(XmlUtil.getHttpUriAttribute(node, FTP_URI_ATTR, null));
+    assertEquals(HTTPS_URI_VALUE, XmlUtil.getHttpUriAttribute(node, FTP_URI_ATTR, null, HTTPS_URI_VALUE));
+  }
+
+  @Test(expected=XmlException.class)
+  public void parseBadXmlThrows() throws XmlException {
+    XmlUtil.parse("malformed xml");
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/config/BasicContainerConfigTest.java b/trunk/java/common/src/test/java/org/apache/shindig/config/BasicContainerConfigTest.java
new file mode 100644
index 0000000..01982cb
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/config/BasicContainerConfigTest.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import static org.junit.Assert.*;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.config.ContainerConfig.ConfigObserver;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests for BasicContainerConfig
+ */
+public class BasicContainerConfigTest {
+
+  protected static final Map<String, Object> DEFAULT_CONTAINER =
+      makeContainer("default", "inherited", "yes");
+  protected static final Map<String, Object> MODIFIED_DEFAULT_CONTAINER =
+      makeContainer("default", "inherited", "si");
+  protected static final Map<String, Object> EXTRA_CONTAINER = makeContainer("extra");
+  protected static final Map<String, Object> MODIFIED_EXTRA_CONTAINER =
+      makeContainer("extra", "inherited", "no");
+
+  protected ContainerConfig config;
+
+  protected static Map<String, Object> makeContainer(String name, Object... values) {
+    // Not using ImmutableMap to allow null values
+    Map<String, Object> newCtr = Maps.newHashMap();
+    newCtr.put("gadgets.container", ImmutableList.of(name));
+    for (int i = 0; i < values.length / 2; ++i) {
+      newCtr.put(values[i * 2].toString(), values[i * 2 + 1]);
+    }
+    return Collections.unmodifiableMap(newCtr);
+  }
+
+  protected static Map<String, Object> makeContainer(List<String> name, Object... values) {
+    // Not using ImmutableMap to allow null values
+    Map<String, Object> newCtr = Maps.newHashMap();
+    newCtr.put("gadgets.container", name);
+    for (int i = 0; i < values.length / 2; ++i) {
+      newCtr.put(values[i * 2].toString(), values[i * 2 + 1]);
+    }
+    return Collections.unmodifiableMap(newCtr);
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    config = new BasicContainerConfig();
+    config.newTransaction().clearContainers().addContainer(DEFAULT_CONTAINER).commit();
+  }
+
+  @Test
+  public void testGetContainers() throws Exception {
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+    assertEquals(ImmutableSet.of("default", "extra"), config.getContainers());
+  }
+
+  @Test
+  public void testGetProperties() throws Exception {
+    assertEquals(ImmutableSet.of("gadgets.container", "inherited"),
+        config.getProperties("default").keySet());
+  }
+
+  @Test
+  public void testPropertyTypes() throws Exception {
+    String container = "misc";
+    config.newTransaction().addContainer(makeContainer("misc",
+        "bool", Boolean.valueOf(true),
+        "bool2", "true",
+        "badbool", Integer.valueOf(1234),
+        "badbool2", "notabool",
+        "int", Integer.valueOf(1234),
+        "int2", "1234",
+        "badint", "notanint",
+        "string", "abcd",
+        "list", ImmutableList.of("a"),
+        "badlist", "notalist",
+        "map", ImmutableMap.of("a", "b"),
+        "badmap", "notamap")).commit();
+    assertEquals(true, config.getBool(container, "bool"));
+    assertEquals(true, config.getBool(container, "bool2"));
+    assertEquals(false, config.getBool(container, "badbool"));
+    assertEquals(false, config.getBool(container, "badbool2"));
+    assertEquals(1234, config.getInt(container, "int"));
+    assertEquals(1234, config.getInt(container, "int2"));
+    assertEquals(0, config.getInt(container, "badint"));
+    assertEquals("abcd", config.getString(container, "string"));
+    assertEquals(ImmutableList.of("a"), config.getList(container, "list"));
+    assertTrue(config.getList(container, "badlist").isEmpty());
+    assertEquals(ImmutableMap.of("a", "b"), config.getMap(container, "map"));
+    assertTrue(config.getMap(container, "badmap").isEmpty());
+  }
+
+  @Test
+  public void testInheritance() throws Exception {
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+    assertEquals("yes", config.getString("default", "inherited"));
+    assertEquals("yes", config.getString("extra", "inherited"));
+    config.newTransaction().addContainer(MODIFIED_EXTRA_CONTAINER).commit();
+    assertEquals("no", config.getString("extra", "inherited"));
+    config.newTransaction().addContainer(MODIFIED_DEFAULT_CONTAINER).commit();
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+    assertEquals("si", config.getString("extra", "inherited"));
+    assertEquals("si", config.getString("extra", "inherited"));
+  }
+
+  @Test
+  public void testContainersAreMergedRecursively() throws Exception {
+    // Data taken from the documentation for BasicContainerConfig#mergeParents
+    Map<String, Object> defaultContainer = makeContainer("default",
+        "base", "/gadgets/foo",
+        "user", "peter",
+        "map", ImmutableMap.of("latitude", 42, "longitude", -8),
+        "data", ImmutableList.of("foo", "bar"));
+    Map<String, Object> newContainer = makeContainer("new",
+        "user", "anne",
+        "colour", "green",
+        "map", ImmutableMap.of("longitude", 130),
+        "data", null);
+    Map<String, Object> expectedContainer = makeContainer("new",
+        "base", "/gadgets/foo",
+        "user", "anne",
+        "colour", "green",
+        "map", ImmutableMap.of("latitude", 42, "longitude", 130),
+        "data", null);
+    config.newTransaction().addContainer(defaultContainer).addContainer(newContainer).commit();
+    assertEquals(expectedContainer, config.getProperties("new"));
+  }
+
+  @Test
+  public void testNulledPropertiesRemainNulledAfterSeveralTransactions() throws Exception {
+    Map<String, Object> defaultContainer = makeContainer("default", "o1", "v1", "o2", "v2", "o3", "v3");
+    Map<String, Object> parentContainer = makeContainer("parent", "o3", null);
+    Map<String, Object> childContainer = makeContainer("child", "parent", "parent", "o2", null);
+    config.newTransaction().addContainer(defaultContainer).commit();
+    config.newTransaction().addContainer(parentContainer).commit();
+    config.newTransaction().addContainer(childContainer).commit();
+    assertNull(config.getProperty("child", "o2"));
+    assertNull(config.getProperty("child", "o3"));
+    assertNull(config.getProperty("parent", "o3"));
+  }
+
+  @Test
+  public void testAddNewContainer() throws Exception {
+    ConfigObserver observer = EasyMock.createMock(ContainerConfig.ConfigObserver.class);
+    observer.containersChanged(EasyMock.isA(ContainerConfig.class),
+        EasyMock.eq(ImmutableSet.of("extra")), EasyMock.eq(ImmutableSet.<String>of()));
+    EasyMock.replay(observer);
+    config.addConfigObserver(observer, false);
+
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+    assertTrue(config.getContainers().contains("extra"));
+    assertEquals("yes", config.getString("extra", "inherited"));
+    EasyMock.verify(observer);
+  }
+
+  @Test
+  public void testReplaceContainer() throws Exception {
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+
+    ConfigObserver observer = EasyMock.createMock(ContainerConfig.ConfigObserver.class);
+    observer.containersChanged(EasyMock.isA(ContainerConfig.class),
+        EasyMock.eq(ImmutableSet.of("extra")), EasyMock.eq(ImmutableSet.<String>of()));
+    EasyMock.replay(observer);
+    config.addConfigObserver(observer, false);
+
+    config.newTransaction().addContainer(MODIFIED_EXTRA_CONTAINER).commit();
+    assertTrue(config.getContainers().contains("extra"));
+    assertEquals("no", config.getString("extra", "inherited"));
+    EasyMock.verify(observer);
+  }
+
+  @Test
+  public void testReadSameContainer() throws Exception {
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+
+    ConfigObserver observer = EasyMock.createMock(ContainerConfig.ConfigObserver.class);
+    observer.containersChanged(EasyMock.isA(ContainerConfig.class),
+        EasyMock.eq(ImmutableSet.<String>of()), EasyMock.eq(ImmutableSet.<String>of()));
+    EasyMock.replay(observer);
+    config.addConfigObserver(observer, false);
+
+
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+    assertTrue(config.getContainers().contains("extra"));
+    assertEquals("yes", config.getString("extra", "inherited"));
+    EasyMock.verify(observer);
+  }
+
+  @Test
+  public void testRemoveContainer() throws Exception {
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+
+    ConfigObserver observer = EasyMock.createMock(ContainerConfig.ConfigObserver.class);
+    observer.containersChanged(EasyMock.isA(ContainerConfig.class),
+        EasyMock.eq(ImmutableSet.<String>of()), EasyMock.eq(ImmutableSet.of("extra")));
+    EasyMock.replay(observer);
+    config.addConfigObserver(observer, false);
+
+    config.newTransaction().removeContainer("extra").commit();
+    assertFalse(config.getContainers().contains("extra"));
+    EasyMock.verify(observer);
+  }
+
+  @Test
+  public void testClearContainerConfig() throws Exception {
+    ConfigObserver observer = EasyMock.createMock(ContainerConfig.ConfigObserver.class);
+    observer.containersChanged(EasyMock.isA(ContainerConfig.class),
+        EasyMock.eq(ImmutableSet.of("additional")), EasyMock.eq(ImmutableSet.of("extra")));
+    EasyMock.replay(observer);
+    config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .clearContainers()
+        .addContainer(DEFAULT_CONTAINER)
+        .addContainer(EXTRA_CONTAINER)
+        .commit();
+    config.addConfigObserver(observer, false);
+
+    config
+        .newTransaction()
+        .clearContainers()
+        .addContainer(DEFAULT_CONTAINER)
+        .addContainer(makeContainer("additional"))
+        .commit();
+
+    assertFalse(config.getContainers().contains("extra"));
+    assertTrue(config.getContainers().contains("additional"));
+
+    EasyMock.verify(observer);
+  }
+
+  @Test
+  public void testAddObserverNotifiesImmediately() throws Exception {
+    ConfigObserver observer = EasyMock.createMock(ContainerConfig.ConfigObserver.class);
+    observer.containersChanged(EasyMock.isA(ContainerConfig.class),
+        EasyMock.eq(ImmutableSet.of("default", "extra")), EasyMock.eq(ImmutableSet.<String>of()));
+    EasyMock.replay(observer);
+
+    config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(DEFAULT_CONTAINER)
+        .addContainer(EXTRA_CONTAINER)
+        .commit();
+    config.addConfigObserver(observer, true);
+
+    EasyMock.verify(observer);
+  }
+
+  @Test
+  public void testAliasesArePopulated() throws Exception {
+    Map<String, Object> container =
+        makeContainer(ImmutableList.of("original", "alias"), "property", "value");
+    config.newTransaction().addContainer(container).commit();
+    assertEquals(ImmutableSet.of("default", "original", "alias"), config.getContainers());
+    assertEquals("value", config.getString("original", "property"));
+    assertEquals("value", config.getString("alias", "property"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/config/ExpressionContainerConfigTest.java b/trunk/java/common/src/test/java/org/apache/shindig/config/ExpressionContainerConfigTest.java
new file mode 100644
index 0000000..692022c
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/config/ExpressionContainerConfigTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import static org.junit.Assert.*;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.shindig.expressions.Expressions;
+import org.junit.Test;
+
+import java.util.Map;
+
+/**
+ * Tests for ExpressionContainerConfig.
+ */
+public class ExpressionContainerConfigTest extends BasicContainerConfigTest {
+
+  private static final Map<String, Object> DEFAULT_EXPR_CONTAINER =
+      makeContainer("default", "expr", "${inherited}", "inherited", "yes");
+  private static final Map<String, Object> MODIFIED_DEFAULT_EXPR_CONTAINER =
+      makeContainer("default", "expr", "${inherited}", "inherited", "si");
+
+  @Override
+  public void setUp() throws Exception {
+    config = new ExpressionContainerConfig(Expressions.forTesting());
+    config.newTransaction().addContainer(DEFAULT_EXPR_CONTAINER).commit();
+  }
+
+  @Override
+  public void testGetProperties() throws Exception {
+    assertEquals(ImmutableSet.of("gadgets.container", "inherited", "expr"),
+        config.getProperties("default").keySet());
+  }
+
+  @Test
+  public void testExpressionValues() throws Exception {
+    assertEquals("yes", config.getString("default", "expr"));
+  }
+
+  @Test
+  public void testExpressionInheritance() throws Exception {
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+    assertEquals("yes", config.getString("default", "expr"));
+    assertEquals("yes", config.getString("extra", "expr"));
+    config.newTransaction().addContainer(MODIFIED_EXTRA_CONTAINER).commit();
+    assertEquals("no", config.getString("extra", "expr"));
+    config.newTransaction().addContainer(MODIFIED_DEFAULT_EXPR_CONTAINER).commit();
+    config.newTransaction().addContainer(EXTRA_CONTAINER).commit();
+    assertEquals("si", config.getString("extra", "expr"));
+    assertEquals("si", config.getString("extra", "expr"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/config/JsonContainerConfigLoaderTest.java b/trunk/java/common/src/test/java/org/apache/shindig/config/JsonContainerConfigLoaderTest.java
new file mode 100644
index 0000000..3c29767
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/config/JsonContainerConfigLoaderTest.java
@@ -0,0 +1,341 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import static org.apache.shindig.config.ContainerConfig.DEFAULT_CONTAINER;
+import static org.apache.shindig.config.ContainerConfig.CONTAINER_KEY;
+import static org.apache.shindig.config.ContainerConfig.PARENT_KEY;
+import static org.junit.Assert.*;
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.expressions.Expressions;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+public class JsonContainerConfigLoaderTest {
+
+  private static final String TOP_LEVEL_NAME = "Top level name";
+  private static final String TOP_LEVEL_VALUE = "Top level value";
+
+  private static final String NESTED_KEY = "ne$ted";
+  private static final String NESTED_NAME = "Nested name";
+  private static final String NESTED_VALUE = "Nested value";
+  private static final String NESTED_ALT_VALUE = "Nested value alt";
+
+  private static final String CHILD_CONTAINER = "child";
+  private static final String CONTAINER_A = "container-a";
+  private static final String CONTAINER_B = "container-b";
+
+  private static final String ARRAY_NAME = "array value";
+  private static final String[] ARRAY_VALUE = {"Hello", "World"};
+  private static final String ARRAY_ALT_VALUE = "Not an array";
+
+  public static final String DYNAMICALLY_LOADED_VALUE_KEY = "dynamicallyLoadedValueKey";
+
+  private ExpressionContainerConfig config;
+
+  private File createTemporaryFile(Object content, String extension) throws Exception {
+    File file = File.createTempFile(getClass().getName(), extension);
+    file.deleteOnExit();
+    BufferedWriter out = new BufferedWriter(new FileWriter(file));
+    out.write(content.toString());
+    out.close();
+    return file;
+  }
+
+  private File createDefaultContainer() throws Exception {
+
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put(TOP_LEVEL_NAME, TOP_LEVEL_VALUE);
+    json.put(ARRAY_NAME, ARRAY_VALUE);
+
+    // small nested data.
+    JSONObject nested = new JSONObject();
+    nested.put(NESTED_NAME, NESTED_VALUE);
+
+    json.put(NESTED_KEY, nested);
+    return createTemporaryFile(json, ".json");
+  }
+
+  private void createConfigForTest(String containers) throws ContainerConfigException {
+    JsonContainerConfigLoader
+        .getTransactionFromFile(containers, "localhost", "8080", "",config).commit();
+  }
+
+  @Before
+  public void setUp() {
+    config = new ExpressionContainerConfig(Expressions.forTesting());
+  }
+
+  @Test
+  public void parseBasicConfig() throws Exception {
+    createConfigForTest(createDefaultContainer().getAbsolutePath());
+
+    assertEquals(1, config.getContainers().size());
+    for (String container : config.getContainers()) {
+      assertEquals(DEFAULT_CONTAINER, container);
+    }
+
+    String value = config.getString(DEFAULT_CONTAINER, TOP_LEVEL_NAME);
+    assertEquals(TOP_LEVEL_VALUE, value);
+
+    Map<String, Object> nested = config.getMap(DEFAULT_CONTAINER, NESTED_KEY);
+    String nestedValue = nested.get(NESTED_NAME).toString();
+    assertEquals(NESTED_VALUE, nestedValue);
+  }
+
+
+  @Test
+  public void aliasesArePopulated() throws Exception {
+    JSONObject json = new JSONObject()
+        .put(CONTAINER_KEY, new String[]{CONTAINER_A, CONTAINER_B})
+        .put(NESTED_KEY, NESTED_VALUE);
+
+    File parentFile = createDefaultContainer();
+    File childFile = createTemporaryFile(json, ".json");
+
+    createConfigForTest(childFile.getAbsolutePath() +
+        JsonContainerConfigLoader.FILE_SEPARATOR + parentFile.getAbsolutePath());
+
+    assertEquals(NESTED_VALUE, config.getString(CONTAINER_A, NESTED_KEY));
+    assertEquals(NESTED_VALUE, config.getString(CONTAINER_B, NESTED_KEY));
+  }
+
+  @Test
+  public void parseWithDefaultInheritance() throws Exception {
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{CHILD_CONTAINER});
+    json.put(PARENT_KEY, DEFAULT_CONTAINER);
+    json.put(ARRAY_NAME, ARRAY_ALT_VALUE);
+
+    // small nested data.
+    JSONObject nested = new JSONObject();
+    nested.put(NESTED_NAME, NESTED_ALT_VALUE);
+
+    json.put(NESTED_KEY, nested);
+
+    File childFile = createTemporaryFile(json, ".json");
+    File parentFile = createDefaultContainer();
+    createConfigForTest(childFile.getAbsolutePath() +
+        JsonContainerConfigLoader.FILE_SEPARATOR + parentFile.getAbsolutePath());
+
+    String value = config.getString(CHILD_CONTAINER, TOP_LEVEL_NAME);
+    assertEquals(TOP_LEVEL_VALUE, value);
+
+    Map<String, Object> nestedObj = config.getMap(CHILD_CONTAINER, NESTED_KEY);
+    String nestedValue = nestedObj.get(NESTED_NAME).toString();
+    assertEquals(NESTED_ALT_VALUE, nestedValue);
+
+    String arrayValue = config.getString(CHILD_CONTAINER, ARRAY_NAME);
+    assertEquals(ARRAY_ALT_VALUE, arrayValue);
+
+    // Verify that the parent value wasn't overwritten as well.
+
+    List<String> actual = new ArrayList<String>();
+    for (Object val : config.getList(DEFAULT_CONTAINER, ARRAY_NAME)) {
+      actual.add(val.toString());
+    }
+
+    List<String> expected = Arrays.asList(ARRAY_VALUE);
+
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  public void invalidContainerReturnsNull() throws Exception {
+    createConfigForTest(createDefaultContainer().getAbsolutePath());
+    assertNull("Did not return null for invalid container.", config.getString("fake", PARENT_KEY));
+  }
+
+  @Test(expected = ContainerConfigException.class)
+  public void badConfigThrows() throws Exception {
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{CHILD_CONTAINER});
+    json.put(PARENT_KEY, "bad bad bad parent!");
+    json.put(ARRAY_NAME, ARRAY_ALT_VALUE);
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+  }
+
+  @Test
+  public void pathQuery() throws Exception {
+    createConfigForTest(createDefaultContainer().getAbsolutePath());
+    String path = "${" + NESTED_KEY + "['" + NESTED_NAME + "']}";
+    String data = config.getString(DEFAULT_CONTAINER, path);
+    assertEquals(NESTED_VALUE, data);
+  }
+
+  @Test
+  public void expressionEvaluation() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put("expression", "Hello, ${world}!");
+    json.put("world", "Earth");
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+
+    assertEquals("Hello, Earth!", config.getString(DEFAULT_CONTAINER, "expression"));
+  }
+
+  @Test
+  public void shindigPortTest() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put("expression", "port=${SERVER_PORT}");
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+
+    assertEquals("port=8080", config.getString(DEFAULT_CONTAINER, "expression"));
+  }
+
+  @Test
+  public void testCommonEnvironmentAddedToAllContainers() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER, "testContainer"});
+    json.put("port", "${SERVER_PORT}");
+    json.put("host", "${SERVER_HOST}");
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+
+    assertEquals("8080", config.getString(DEFAULT_CONTAINER, "port"));
+    assertEquals("8080", config.getString("testContainer", "port"));
+    assertEquals("localhost", config.getString(DEFAULT_CONTAINER, "host"));
+    assertEquals("localhost", config.getString("testContainer", "host"));
+  }
+
+  @Test
+  public void expressionEvaluationUsingParent() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{CHILD_CONTAINER});
+    json.put(PARENT_KEY, DEFAULT_CONTAINER);
+    json.put("parentExpression", "${parent['" + TOP_LEVEL_NAME + "']}");
+
+    File childFile = createTemporaryFile(json, ".json");
+    File parentFile = createDefaultContainer();
+    createConfigForTest(childFile.getAbsolutePath() +
+        JsonContainerConfigLoader.FILE_SEPARATOR + parentFile.getAbsolutePath());
+
+    assertEquals(TOP_LEVEL_VALUE, config.getString(CHILD_CONTAINER, "parentExpression"));
+  }
+
+  @Test
+  public void nullEntryEvaluation() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject("{ 'gadgets.container' : ['default'], features : { osapi : null }}");
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+    assertNull(config.getMap("default", "features").get("osapi"));
+  }
+
+  @Test
+  public void testNullEntriesOverrideEntriesInParent() throws Exception {
+    // We use JSON Objects here to guarantee that we're well formed up front.
+    JSONObject parent = new JSONObject("{ 'gadgets.container' : ['default'], features : { osapi : 'foo' }}");
+    JSONObject child = new JSONObject("{ 'gadgets.container' : ['child'], features : null}");
+    JSONObject grand = new JSONObject("{ 'gadgets.container' : ['grand'], parent : 'child'}");
+    createConfigForTest(createTemporaryFile(parent, ".json").getAbsolutePath());
+    createConfigForTest(createTemporaryFile(child, ".json").getAbsolutePath());
+    createConfigForTest(createTemporaryFile(grand, ".json").getAbsolutePath());
+    assertEquals("foo", config.getMap("default", "features").get("osapi"));
+    assertNull(config.getProperty("child", "features"));
+    assertNull(config.getProperty("grand", "features"));
+  }
+
+  @Test
+  public void resourceLoaderClasspathTest() throws Exception {
+    // Pointer to a file that we'll load from the classpath
+    String testFile = "classpath-accessible-test-file.txt";
+
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put(DYNAMICALLY_LOADED_VALUE_KEY, ResourceLoader.RESOURCE_PREFIX + testFile);
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+
+    //Make sure that the file was properly loaded from the classpath...  If it doesnt start with the
+    //resource prefix and it does contain the expected text then it was loaded properly.
+    assertFalse(config.getString(DEFAULT_CONTAINER, DYNAMICALLY_LOADED_VALUE_KEY).
+        startsWith(ResourceLoader.RESOURCE_PREFIX));
+    assertTrue(config.getString(DEFAULT_CONTAINER, DYNAMICALLY_LOADED_VALUE_KEY).contains(testFile));
+  }
+
+  @Test
+  public void resourceLoaderFileTest() throws Exception {
+    // Create a temporary file that we can load from
+    String dynamicValue = "dynamic value";
+    File temporaryFile = createTemporaryFile(dynamicValue, ".txt");
+
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put(DYNAMICALLY_LOADED_VALUE_KEY, ResourceLoader.FILE_PREFIX + temporaryFile.getAbsolutePath());
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+
+    assertEquals(dynamicValue, config.getString(DEFAULT_CONTAINER, DYNAMICALLY_LOADED_VALUE_KEY).trim());
+  }
+
+  @Test
+  public void resourceLoaderClasspathFailureTest() throws Exception {
+    // Pointer to an invalid resource reference
+    String invalidResource = ResourceLoader.RESOURCE_PREFIX + "does-not-exist";
+
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put(DYNAMICALLY_LOADED_VALUE_KEY, invalidResource);
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+
+    // If we fail to load a resource a warning is logged and we just end up with the raw value back
+    assertEquals(invalidResource, config.getString(DEFAULT_CONTAINER, DYNAMICALLY_LOADED_VALUE_KEY));
+  }
+
+  @Test
+  public void resourceLoaderFileFailureTest() throws Exception {
+    // Pointer to an invalid resource reference
+    String invalidResource = ResourceLoader.FILE_PREFIX + "does-not-exist";
+
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put(DYNAMICALLY_LOADED_VALUE_KEY, invalidResource);
+
+    createConfigForTest(createTemporaryFile(json, ".json").getAbsolutePath());
+
+    // If we fail to load a resource a warning is logged and we just end up with the raw value back
+    assertEquals(invalidResource, config.getString(DEFAULT_CONTAINER, DYNAMICALLY_LOADED_VALUE_KEY));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/config/JsonContainerConfigTest.java b/trunk/java/common/src/test/java/org/apache/shindig/config/JsonContainerConfigTest.java
new file mode 100644
index 0000000..5c8874e
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/config/JsonContainerConfigTest.java
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.config;
+
+import org.apache.shindig.expressions.Expressions;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.shindig.config.ContainerConfig.DEFAULT_CONTAINER;
+import static org.apache.shindig.config.JsonContainerConfig.CONTAINER_KEY;
+import static org.apache.shindig.config.JsonContainerConfig.PARENT_KEY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class JsonContainerConfigTest {
+
+  private static final String TOP_LEVEL_NAME = "Top level name";
+  private static final String TOP_LEVEL_VALUE = "Top level value";
+
+  private static final String NESTED_KEY = "ne$ted";
+  private static final String NESTED_NAME = "Nested name";
+  private static final String NESTED_VALUE = "Nested value";
+  private static final String NESTED_ALT_VALUE = "Nested value alt";
+
+  private static final String CHILD_CONTAINER = "child";
+  private static final String CONTAINER_A = "container-a";
+  private static final String CONTAINER_B = "container-b";
+
+  private static final String ARRAY_NAME = "array value";
+  private static final String[] ARRAY_VALUE = {"Hello", "World"};
+  private static final String ARRAY_ALT_VALUE = "Not an array";
+
+  private File createContainer(JSONObject json) throws Exception {
+    File file = File.createTempFile(getClass().getName(), ".json");
+    file.deleteOnExit();
+    BufferedWriter out = new BufferedWriter(new FileWriter(file));
+    out.write(json.toString());
+    out.close();
+    return file;
+  }
+
+  private File createDefaultContainer() throws Exception {
+
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put(TOP_LEVEL_NAME, TOP_LEVEL_VALUE);
+    json.put(ARRAY_NAME, ARRAY_VALUE);
+
+    // small nested data.
+    JSONObject nested = new JSONObject();
+    nested.put(NESTED_NAME, NESTED_VALUE);
+
+    json.put(NESTED_KEY, nested);
+    return createContainer(json);
+  }
+
+  @Test
+  public void parseBasicConfig() throws Exception {
+    ContainerConfig config = new JsonContainerConfig(createDefaultContainer().getAbsolutePath(),
+        Expressions.forTesting());
+
+    assertEquals(1, config.getContainers().size());
+    for (String container : config.getContainers()) {
+      assertEquals(DEFAULT_CONTAINER, container);
+    }
+
+    String value = config.getString(DEFAULT_CONTAINER, TOP_LEVEL_NAME);
+    assertEquals(TOP_LEVEL_VALUE, value);
+
+    Map<String, Object> nested = config.getMap(DEFAULT_CONTAINER, NESTED_KEY);
+    String nestedValue = nested.get(NESTED_NAME).toString();
+    assertEquals(NESTED_VALUE, nestedValue);
+  }
+
+  @Test
+  public void aliasesArePopulated() throws Exception {
+    JSONObject json = new JSONObject()
+        .put(CONTAINER_KEY, new String[]{CONTAINER_A, CONTAINER_B})
+        .put(NESTED_KEY, NESTED_VALUE);
+
+    File parentFile = createDefaultContainer();
+    File childFile = createContainer(json);
+
+    ContainerConfig config = new JsonContainerConfig(childFile.getAbsolutePath() +
+        JsonContainerConfigLoader.FILE_SEPARATOR + parentFile.getAbsolutePath(), Expressions.forTesting());
+
+    assertEquals(NESTED_VALUE, config.getString(CONTAINER_A, NESTED_KEY));
+    assertEquals(NESTED_VALUE, config.getString(CONTAINER_B, NESTED_KEY));
+  }
+
+  @Test
+  public void parseWithDefaultInheritance() throws Exception {
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{CHILD_CONTAINER});
+    json.put(PARENT_KEY, DEFAULT_CONTAINER);
+    json.put(ARRAY_NAME, ARRAY_ALT_VALUE);
+
+    // small nested data.
+    JSONObject nested = new JSONObject();
+    nested.put(NESTED_NAME, NESTED_ALT_VALUE);
+
+    json.put(NESTED_KEY, nested);
+
+    File childFile = createContainer(json);
+    File parentFile = createDefaultContainer();
+    ContainerConfig config = new JsonContainerConfig(childFile.getAbsolutePath() +
+        JsonContainerConfigLoader.FILE_SEPARATOR + parentFile.getAbsolutePath(), Expressions.forTesting());
+
+    String value = config.getString(CHILD_CONTAINER, TOP_LEVEL_NAME);
+    assertEquals(TOP_LEVEL_VALUE, value);
+
+    Map<String, Object> nestedObj = config.getMap(CHILD_CONTAINER, NESTED_KEY);
+    String nestedValue = nestedObj.get(NESTED_NAME).toString();
+    assertEquals(NESTED_ALT_VALUE, nestedValue);
+
+    String arrayValue = config.getString(CHILD_CONTAINER, ARRAY_NAME);
+    assertEquals(ARRAY_ALT_VALUE, arrayValue);
+
+    // Verify that the parent value wasn't overwritten as well.
+
+    List<String> actual = new ArrayList<String>();
+    for (Object val : config.getList(DEFAULT_CONTAINER, ARRAY_NAME)) {
+      actual.add(val.toString());
+    }
+
+    List<String> expected = Arrays.asList(ARRAY_VALUE);
+
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  public void invalidContainerReturnsNull() throws Exception {
+    ContainerConfig config = new JsonContainerConfig(createDefaultContainer().getAbsolutePath(),
+        Expressions.forTesting());
+    assertNull("Did not return null for invalid container.", config.getString("fake", PARENT_KEY));
+  }
+
+  @Test(expected = ContainerConfigException.class)
+  public void badConfigThrows() throws Exception {
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{CHILD_CONTAINER});
+    json.put(PARENT_KEY, "bad bad bad parent!");
+    json.put(ARRAY_NAME, ARRAY_ALT_VALUE);
+
+    new JsonContainerConfig(createContainer(json).getAbsolutePath(), Expressions.forTesting());
+  }
+
+  @Test
+  public void pathQuery() throws Exception {
+    ContainerConfig config = new JsonContainerConfig(createDefaultContainer().getAbsolutePath(), Expressions.forTesting());
+    String path = "${" + NESTED_KEY + "['" + NESTED_NAME + "']}";
+    String data = config.getString(DEFAULT_CONTAINER, path);
+    assertEquals(NESTED_VALUE, data);
+  }
+
+  @Test
+  public void expressionEvaluation() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put("expression", "Hello, ${world}!");
+    json.put("world", "Earth");
+
+    ContainerConfig config = new JsonContainerConfig(createContainer(json).getAbsolutePath(), Expressions.forTesting());
+
+    assertEquals("Hello, Earth!", config.getString(DEFAULT_CONTAINER, "expression"));
+  }
+
+  @Test
+  public void shindigPortTest() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER});
+    json.put("expression", "port=${SERVER_PORT}");
+
+    ContainerConfig config = new JsonContainerConfig(createContainer(json).getAbsolutePath(),
+        Expressions.forTesting());
+
+    assertEquals("port=8080", config.getString(DEFAULT_CONTAINER, "expression"));
+  }
+
+  @Test
+  public void testCommonEnvironmentAddedToAllContainers() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{DEFAULT_CONTAINER, "testContainer"});
+    json.put("port", "${SERVER_PORT}");
+    json.put("host", "${SERVER_HOST}");
+
+    ContainerConfig config = new JsonContainerConfig(createContainer(json).getAbsolutePath(),
+        Expressions.forTesting());
+
+    assertEquals("8080", config.getString(DEFAULT_CONTAINER, "port"));
+    assertEquals("8080", config.getString("testContainer", "port"));
+    assertEquals("localhost", config.getString(DEFAULT_CONTAINER, "host"));
+    assertEquals("localhost", config.getString("testContainer", "host"));
+  }
+
+  @Test
+  public void expressionEvaluationUsingParent() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject();
+    json.put(CONTAINER_KEY, new String[]{CHILD_CONTAINER});
+    json.put(PARENT_KEY, DEFAULT_CONTAINER);
+    json.put("parentExpression", "${parent['" + TOP_LEVEL_NAME + "']}");
+
+    File childFile = createContainer(json);
+    File parentFile = createDefaultContainer();
+    ContainerConfig config = new JsonContainerConfig(childFile.getAbsolutePath() +
+        JsonContainerConfigLoader.FILE_SEPARATOR + parentFile.getAbsolutePath(), Expressions.forTesting());
+
+    assertEquals(TOP_LEVEL_VALUE, config.getString(CHILD_CONTAINER, "parentExpression"));
+  }
+
+  @Test
+  public void nullEntryEvaluation() throws Exception {
+    // We use a JSON Object here to guarantee that we're well formed up front.
+    JSONObject json = new JSONObject("{ 'gadgets.container' : ['default'], features : { osapi : null }}");
+    JsonContainerConfig config = new JsonContainerConfig(createContainer(json).getAbsolutePath(),
+        Expressions.forTesting());
+    assertNull(config.getMap("default", "features").get("osapi"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java b/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java
new file mode 100644
index 0000000..83cd702
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/expressions/ExpressionsTest.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.el.ELContext;
+import javax.el.PropertyNotFoundException;
+import javax.el.ValueExpression;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+public class ExpressionsTest {
+  public Expressions expressions;
+  private ELContext context;
+  private Map<String, Object> variables;
+
+  @Before
+  public void setUp() {
+    expressions = Expressions.forTesting();
+    variables = Maps.newHashMap();
+    context = expressions.newELContext(new RootELResolver(variables));
+  }
+
+  @Test
+  public void arraySupport() {
+    addVariable("array", new String[]{"foo", "bar"});
+    String result = evaluate("${array[0]}${array[1]}", String.class);
+    assertEquals("foobar", result);
+  }
+
+  @Test
+  public void listSupport() {
+    addVariable("list", ImmutableList.of("foo", "bar"));
+    String result = evaluate("${list[0]}${list[1]}", String.class);
+    assertEquals("foobar", result);
+  }
+
+  @Test
+  public void mapSupport() {
+    addVariable("map", ImmutableMap.of("foo", "bar"));
+    String result = evaluate("${map.foo}${map['foo']}", String.class);
+    assertEquals("barbar", result);
+  }
+
+  @Test
+  public void jsonObjectSupport() throws Exception {
+    addVariable("object", new JSONObject("{foo: 125}"));
+    int result = evaluate("${object.foo}", Integer.class);
+    assertEquals(125, result);
+  }
+
+  @Test
+  public void jsonArraySupport() throws Exception {
+    addVariable("array", new JSONArray("[1, 2]"));
+    int result = evaluate("${array[0] + array[1]}", Integer.class);
+    assertEquals(3, result);
+  }
+
+  @Test
+  public void jsonArrayCoercionOfStatic() throws Exception {
+    JSONArray result = evaluate("first,second", JSONArray.class);
+    JSONArray expected = new JSONArray("['first', 'second']");
+    assertEquals(expected.toString(), result.toString());
+  }
+
+  @Test
+  public void jsonArrayCoercion() throws Exception {
+    addVariable("foo", "first,second");
+    JSONArray result = evaluate("${foo}", JSONArray.class);
+    JSONArray expected = new JSONArray("['first', 'second']");
+    assertEquals(expected.toString(), result.toString());
+  }
+
+  @Test
+  public void missingJsonSubproperty() throws Exception {
+    addVariable("object", new JSONObject("{foo: 125}"));
+    assertNull(evaluate("${object.bar.baz}", Object.class));
+  }
+
+  @Test
+  public void missingMapSubproperty() throws Exception {
+    addVariable("map", ImmutableMap.of("key", "value"));
+    assertNull(evaluate("${map.bar.baz}", Object.class));
+  }
+
+  @Test(expected = PropertyNotFoundException.class)
+  public void missingTopLevelVariable() throws Exception {
+    // Top-level properties must throw a PropertyNotFoundException when
+    // failing;  other properties must not.  Pipeline data batching
+    // relies on this
+    assertNull(evaluate("${map.bar.baz}", Object.class));
+  }
+
+  @Test
+  public void booleanCoercionOfBooleans() throws Exception{
+    addVariable("bool", false);
+    assertFalse(evaluate("${bool}", Boolean.class));
+    assertTrue(evaluate("${!bool}", Boolean.class));
+
+    addVariable("bool", true);
+    assertTrue(evaluate("${bool}", Boolean.class));
+    assertFalse(evaluate("${!bool}", Boolean.class));
+  }
+
+  @Test
+  public void booleanCoercionOfNumbers() throws Exception{
+    // Negation tests have been moved to EL subdir
+    addVariable("bool", 0);
+    assertFalse(evaluate("${bool}", Boolean.class));
+
+    addVariable("bool", 1);
+    assertTrue(evaluate("${bool}", Boolean.class));
+  }
+
+  @Test
+  public void booleanCoercionOfNull() throws Exception{
+    addVariable("bool", null);
+    assertFalse(evaluate("${bool}", Boolean.class));
+    assertTrue(evaluate("${!bool}", Boolean.class));
+  }
+
+  @Test
+  public void booleanCoercionOfStrings() throws Exception{
+    // Negation tests for FALSE and any String have been moved El subdir
+    addVariable("bool", "");
+    assertFalse(evaluate("${bool}", Boolean.class));
+    assertTrue(evaluate("${!bool}", Boolean.class));
+
+    addVariable("bool", "false");
+    assertFalse(evaluate("${bool}", Boolean.class));
+    assertTrue(evaluate("${!bool}", Boolean.class));
+
+    // Case-sensitive coercion:  FALSE is true
+    addVariable("bool", "FALSE");
+    assertTrue(evaluate("${bool}", Boolean.class));
+
+    addVariable("bool", "true");
+    assertTrue(evaluate("${bool}", Boolean.class));
+    assertFalse(evaluate("${!bool}", Boolean.class));
+
+    addVariable("bool", "booga");
+    assertTrue(evaluate("${bool}", Boolean.class));
+  }
+
+
+  @Test
+  public void iterableCoercionOfScalar() throws Exception {
+    addVariable("iter", "foo");
+    assertEquals(ImmutableList.of("foo"),
+        evaluate("${iter}", Iterable.class));
+  }
+
+  @Test
+  public void iterableCoercionOfNull() throws Exception {
+    addVariable("iter", null);
+    assertEquals(ImmutableList.of(),
+        evaluate("${iter}", Iterable.class));
+  }
+
+  @Test
+  public void iterableCoercionOfCollection() throws Exception {
+    addVariable("iter", ImmutableList.of(1, 2, 3));
+    assertEquals(ImmutableList.of(1, 2, 3),
+        evaluate("${iter}", Iterable.class));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void iterableCoercionOfJSONArray() throws Exception {
+    addVariable("iter", new JSONArray("[1,2,3]"));
+    assertEquals(ImmutableList.of(1, 2, 3),
+        ImmutableList.copyOf(evaluate("${iter}", Iterable.class)));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void iterableCoercionOfJSONObjectWithListProperty() throws Exception {
+    addVariable("iter", new JSONObject("{list: [1,2,3]}"));
+    assertEquals(ImmutableList.of(1, 2, 3),
+        ImmutableList.copyOf(evaluate("${iter}", Iterable.class)));
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void iterableCoercionOfJSONObjectWithoutListProperty() throws Exception {
+    JSONObject json = new JSONObject("{foo: [1,2,3]}");
+    addVariable("iter", json);
+    assertEquals(ImmutableList.of(json),
+        ImmutableList.copyOf(evaluate("${iter}", Iterable.class)));
+  }
+
+  public <T> T evaluate(String expression, Class<T> type) {
+    ValueExpression expr = expressions.parse(expression, type);
+    return type.cast(expr.getValue(context));
+  }
+
+  public void addVariable(String key, Object value) {
+    variables.put(key, value);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/expressions/FunctionsTest.java b/trunk/java/common/src/test/java/org/apache/shindig/expressions/FunctionsTest.java
new file mode 100644
index 0000000..9acd411
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/expressions/FunctionsTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import org.json.JSONObject;
+
+import java.lang.reflect.Method;
+
+import javax.el.ELContext;
+import javax.el.ValueExpression;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FunctionsTest extends Assert {
+  private Functions functions;
+
+  @Before
+  public void setUp() throws Exception {
+    functions = new Functions(FunctionsTest.class);
+  }
+
+  @Test
+  public void testExpose() throws Exception {
+    Method hi = functions.resolveFunction("test", "hi");
+    assertEquals("hi", hi.invoke(null));
+
+    Method hiAlternate = functions.resolveFunction("test", "hola");
+    assertEquals("hi", hiAlternate.invoke(null));
+
+    Method bonjour = functions.resolveFunction("other", "bonjour");
+    assertEquals("French hello", bonjour.invoke(null));
+  }
+
+  @Test
+  public void testNonStaticNotExposed() {
+    assertNull(functions.resolveFunction("test", "goodbye"));
+  }
+
+  @Test
+  public void testDefaultBinding() throws Exception {
+    Injector injector = Guice.createInjector();
+    functions = injector.getInstance(Functions.class);
+
+    Method toJson = functions.resolveFunction("osx", "parseJson");
+    Object o = toJson.invoke(null, "{a : 1}");
+    assertTrue(o instanceof JSONObject);
+    assertEquals(1, ((JSONObject) o).getInt("a"));
+  }
+
+  @Test
+  public void testExpressionEvaluation() {
+    Expressions expressions = Expressions.forTesting(functions);
+    ELContext context = expressions.newELContext();
+    ValueExpression expression = expressions.parse("${other:bonjour()}", String.class);
+
+    assertEquals("French hello", expression.getValue(context));
+
+    expression = expressions.parse("${test:add(1, 2)}", Integer.class);
+    assertEquals(3, expression.getValue(context));
+  }
+
+  /**
+   * Static function, should be exposed under two names.
+   */
+  @Functions.Expose(prefix="test", names={"hi", "hola"})
+  public static String sayHi() {
+    return "hi";
+  }
+
+  /**
+   * Test with some arguments.
+   */
+  @Functions.Expose(prefix="test", names={"add"})
+  public static int add(int i, int j) {
+    return i + j;
+  }
+
+  /**
+   * Static function, should be exposed under two names.
+   */
+  @Functions.Expose(prefix="other", names={"bonjour"})
+  public static String sayHi2() {
+    return "French hello";
+  }
+
+  /**
+   * Non-static: shouldn't be exposed.
+   */
+  @Functions.Expose(prefix="test", names={"goodbye"})
+  public String sayGoodbye() {
+    return "goodbye";
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/expressions/OpensocialFunctionsTest.java b/trunk/java/common/src/test/java/org/apache/shindig/expressions/OpensocialFunctionsTest.java
new file mode 100644
index 0000000..2584438
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/expressions/OpensocialFunctionsTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.util.Map;
+
+import javax.el.ELContext;
+import javax.el.ValueExpression;
+
+import com.google.common.collect.Maps;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OpensocialFunctionsTest extends Assert {
+  private Expressions expressions;
+  private ELContext context;
+  private Map<String, Object> vars = Maps.newHashMap();
+
+  @Before
+  public void setUp() {
+    Functions functions = new Functions(OpensocialFunctions.class);
+    expressions = Expressions.forTesting(functions);
+    context = expressions.newELContext(new RootELResolver(vars));
+  }
+
+  @Test
+  public void testParseJsonObject() {
+    ValueExpression testParseJsonObject =
+      expressions.parse("${osx:parseJson('{a: 1}').a}", Integer.class);
+    assertEquals(1, testParseJsonObject.getValue(context));
+  }
+
+  @Test
+  public void testParseJsonArray() {
+    ValueExpression testParseJsonArray =
+      expressions.parse("${osx:parseJson('[1, 2, 3]')[1]}", Integer.class);
+    assertEquals(2, testParseJsonArray.getValue(context));
+  }
+
+  @Test
+  public void testDecodeBase64() throws Exception {
+    String test = "12345";
+    String encoded = new String(Base64.encodeBase64(test.getBytes("UTF-8")), "UTF-8");
+    vars.put("encoded", encoded);
+
+    ValueExpression testDecodeBase64 =
+      expressions.parse("${osx:decodeBase64(encoded)}", String.class);
+    assertEquals("12345", testDecodeBase64.getValue(context));
+  }
+
+  @Test
+  public void testUrlEncode() throws Exception {
+    String test = "He He";
+    vars.put("test", test);
+
+    ValueExpression testUrlEncode =
+      expressions.parse("${os:urlEncode(test)}", String.class);
+    assertEquals("He+He", testUrlEncode.getValue(context));
+  }
+
+  @Test
+  public void testUrlDecode() throws Exception {
+    String test = "He+He";
+    vars.put("encoded", test);
+
+    ValueExpression testUrlDecode =
+      expressions.parse("${os:urlDecode(encoded)}", String.class);
+    assertEquals("He He", testUrlDecode.getValue(context));
+  }
+
+  @Test
+  public void testHtmlEncode() throws Exception {
+    String test = "<test>";
+    vars.put("test", test);
+
+    ValueExpression testHtmlEncode =
+      expressions.parse("${os:htmlEncode(test)}", String.class);
+    assertEquals("&lt;test&gt;", testHtmlEncode.getValue(context));
+  }
+
+  @Test
+  public void testHtmlDecode() throws Exception {
+    String test = "&lt;1+1>3&gt;";
+    vars.put("encoded", test);
+
+    ValueExpression testHtmlDecode =
+      expressions.parse("${os:htmlDecode(encoded)}", String.class);
+    assertEquals("<1+1>3>", testHtmlDecode.getValue(context));
+  }
+
+  @Test
+  public void testParseJsonNull() throws Exception {
+    ValueExpression testUrlEncode =
+      expressions.parse("${osx:parseJson(null)}", String.class);
+    assertEquals("", testUrlEncode.getValue(context));
+  }
+
+  @Test
+  public void testDecodeBase64Null() throws Exception {
+    ValueExpression testUrlEncode =
+      expressions.parse("${osx:decodeBase64(null)}", String.class);
+    assertEquals("", testUrlEncode.getValue(context));
+  }
+
+  @Test
+  public void testUrlEncodeNull() throws Exception {
+    ValueExpression testUrlEncode =
+      expressions.parse("${os:urlEncode(null)}", String.class);
+    assertEquals("", testUrlEncode.getValue(context));
+  }
+
+  @Test
+  public void testUrlDecodeNull() throws Exception {
+    ValueExpression testUrlDecode =
+      expressions.parse("${os:urlDecode(null)}", String.class);
+    assertEquals("", testUrlDecode.getValue(context));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/expressions/jasper/JasperExpressionsTest.java b/trunk/java/common/src/test/java/org/apache/shindig/expressions/jasper/JasperExpressionsTest.java
new file mode 100644
index 0000000..837315c
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/expressions/jasper/JasperExpressionsTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.jasper;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.ExpressionsTest;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class JasperExpressionsTest  extends ExpressionsTest{
+
+  @Before
+  @Override
+  public void setUp() {
+    super.setUp();
+    expressions = new Expressions(null, null, new JasperTypeConverter(), new JasperProvider());
+  }
+
+  @Ignore
+  @Test
+  public void booleanCoercionOfStringsFails() throws Exception{
+    // Case-sensitive coercion:  FALSE is true
+    // Test fails because Jasper type conversion routines does not recognize FALSE.
+    addVariable("bool", "FALSE");
+    assertFalse(evaluate("${!bool}", Boolean.class));
+
+    // Jasper cannot handle this
+    addVariable("bool", "booga");
+    assertFalse(evaluate("${!bool}", Boolean.class));
+  }
+
+  @Ignore
+  @Test
+  public void booleanCoercionOfNumbersFails() throws Exception {
+    // These test cases will not pass with Jasper due to ELSupport exceptions
+    // thrown when coercing Integer to Boolean
+    addVariable("bool", 0);
+    assertTrue(evaluate("${!bool}", Boolean.class));
+
+    addVariable("bool", 1);
+    assertFalse(evaluate("${!bool}", Boolean.class));
+
+    evaluate("${true && 5}", String.class);
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/expressions/juel/JuelExpressionsTest.java b/trunk/java/common/src/test/java/org/apache/shindig/expressions/juel/JuelExpressionsTest.java
new file mode 100644
index 0000000..b652be6
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/expressions/juel/JuelExpressionsTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.expressions.juel;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Map;
+
+import javax.el.ELContext;
+import javax.el.ValueExpression;
+
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Maps;
+
+public class JuelExpressionsTest {
+
+  private Expressions expressions;
+  private ELContext context;
+  private Map<String, Object> variables;
+
+  @Before
+  public void setUp() {
+    expressions = Expressions.forTesting(null);
+    variables = Maps.newHashMap();
+    context = expressions.newELContext(new RootELResolver(variables));
+  }
+
+  @Test
+  public void booleanCoercionOfStringsFails() throws Exception {
+
+    addVariable("bool", "FALSE");
+    assertFalse(evaluate("${!bool}", Boolean.class));
+
+    addVariable("bool", "booga");
+    assertFalse(evaluate("${!bool}", Boolean.class));
+  }
+
+  @Test
+  public void booleanCoercionOfNumbersFails() throws Exception {
+    addVariable("bool", 0);
+    assertTrue(evaluate("${!bool}", Boolean.class));
+
+    addVariable("bool", 1);
+    assertFalse(evaluate("${!bool}", Boolean.class));
+
+    evaluate("${true && 5}", String.class);
+  }
+
+  private <T> T evaluate(String expression, Class<T> type) {
+
+    ValueExpression expr = expressions.parse(expression, type);
+    return type.cast(expr.getValue(context));
+  }
+
+  private void addVariable(String key, Object value) {
+    variables.put(key, value);
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/BaseRequestItemTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/BaseRequestItemTest.java
new file mode 100644
index 0000000..e509b3a
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/BaseRequestItemTest.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.model.SortOrder;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.Guice;
+
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+/**
+ * Test BaseRequestItem
+ */
+public class BaseRequestItemTest extends Assert {
+
+  private static final FakeGadgetToken FAKE_TOKEN = new FakeGadgetToken();
+
+  protected BaseRequestItem request;
+  protected BeanJsonConverter converter;
+
+  @Before
+  public void setUp() throws Exception {
+    FAKE_TOKEN.setAppId("12345");
+    FAKE_TOKEN.setOwnerId("someowner");
+    FAKE_TOKEN.setViewerId("someowner");
+    converter = new BeanJsonConverter(Guice.createInjector());
+    request = new BaseRequestItem(
+        Maps.<String,String[]>newHashMap(),
+        FAKE_TOKEN, converter, converter);
+  }
+
+  @Test
+  public void testParseCommaSeparatedList() throws Exception {
+    request.setParameter("fields", "huey,dewey,louie");
+    assertEquals(Lists.newArrayList("huey", "dewey", "louie"), request.getListParameter("fields"));
+  }
+
+  @Test
+  public void testGetAppId() throws Exception {
+    request.setParameter("appId", "100");
+    assertEquals("100", request.getAppId());
+
+    request.setParameter("appId", "@app");
+    assertEquals(FAKE_TOKEN.getAppId(), request.getAppId());
+  }
+
+  @Test
+  public void testStartIndex() throws Exception {
+    request.setParameter("startIndex", null);
+    assertEquals(RequestItem.DEFAULT_START_INDEX, request.getStartIndex());
+
+    request.setParameter("startIndex", "5");
+    assertEquals(5, request.getStartIndex());
+  }
+
+  @Test
+  public void testCount() throws Exception {
+    request.setParameter("count", null);
+    assertEquals(RequestItem.DEFAULT_COUNT, request.getCount());
+
+    request.setParameter("count", "5");
+    assertEquals(5, request.getCount());
+  }
+
+  @Test
+  public void testSortOrder() throws Exception {
+    request.setParameter("sortOrder", null);
+    assertEquals(SortOrder.ascending, request.getSortOrder());
+
+    request.setParameter("sortOrder", "descending");
+    assertEquals(SortOrder.descending, request.getSortOrder());
+  }
+
+  @Test
+  public void testFields() throws Exception {
+    request.setParameter("fields", "");
+    assertEquals(Sets.<String>newHashSet(), request.getFields());
+
+    request.setParameter("fields", "happy,sad,grumpy");
+    assertEquals(Sets.newHashSet("happy", "sad", "grumpy"), request.getFields());
+  }
+
+  @Test
+  public void testGetTypedParameter() throws Exception {
+    request.setParameter("anykey", "{name: 'Bob', id: '1234'}");
+    InputData input = request.getTypedParameter("anykey", InputData.class);
+    assertEquals("Bob", input.name);
+    assertEquals(1234, input.id);
+  }
+
+  @Test(expected = ProtocolException.class)
+  public void testGetTypedParameterEmpty() throws Exception {
+    request.getTypedParameter("empty", InputData.class);
+  }
+
+  @Test
+  public void testGetParameters() throws Exception {
+    request.setParameter("anykey", "{name: 'Bob', id: '1234'}");
+    Map<String, Object> params = request.getParameters();
+    assertEquals(1, params.size());
+    assertTrue(params.containsKey("anykey"));
+    try {
+      params.put("this", "is bad");
+      fail("Params should be immutable");
+    } catch (UnsupportedOperationException e) {
+      // As expected
+    }
+  }
+
+  @Test
+  public void testGetInvalidJsonTypedParameter() throws Exception {
+    request.setParameter("anykey", "{name: 'Bob");
+    int code = 0;
+    try {
+      request.getTypedParameter("anykey", InputData.class);
+    } catch(ProtocolException e) {
+      code = e.getCode();
+    }
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, code);
+  }
+
+  @Test
+  public void testJSONConstructor() throws Exception {
+    request = new BaseRequestItem(new JSONObject('{' +
+            "userId:john.doe," +
+            "groupId:@self," +
+            "fields:[huey,dewey,louie]" +
+        '}'), null, FAKE_TOKEN, converter, converter);
+    assertEquals(Lists.newArrayList("huey", "dewey", "louie"), request.getListParameter("fields"));
+  }
+
+  @Test
+  public void testAttributes() throws Exception {
+    assertNull(request.getAttribute("undefined"));
+    request.setAttribute("test", "value");
+    assertEquals("value", request.getAttribute("test"));
+    request.setAttribute("test", null);
+    assertNull(request.getAttribute("undefined"));
+  }
+
+  public static class InputData {
+    String name;
+    int id;
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    public void setId(int id) {
+      this.id = id;
+    }
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/ContentTypesTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/ContentTypesTest.java
new file mode 100644
index 0000000..4f7d9b5
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/ContentTypesTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test content type checks
+ */
+public class ContentTypesTest extends Assert {
+
+  @Test
+  public void testAllowJson() throws Exception {
+    ContentTypes.checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+  }
+
+  @Test
+  public void testAllowJsonRpc() throws Exception {
+    ContentTypes.checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES,
+        "application/json-rpc");
+  }
+
+  @Test
+  public void testAllowAtom() throws Exception {
+    ContentTypes.checkContentTypes(ContentTypes.ALLOWED_ATOM_CONTENT_TYPES,
+        ContentTypes.OUTPUT_ATOM_CONTENT_TYPE);
+  }
+
+  @Test
+  public void testAllowXml() throws Exception {
+    ContentTypes.checkContentTypes(ContentTypes.ALLOWED_XML_CONTENT_TYPES,
+        ContentTypes.OUTPUT_XML_CONTENT_TYPE);
+  }
+
+  @Test
+  public void testAllowMultipart() throws Exception {
+    ContentTypes.checkContentTypes(ContentTypes.ALLOWED_MULTIPART_CONTENT_TYPES,
+        ContentTypes.MULTIPART_FORM_CONTENT_TYPE);
+  }
+
+  @Test(expected=ContentTypes.InvalidContentTypeException.class)
+  public void testForbidden() throws Exception {
+    ContentTypes.checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES,
+        "application/x-www-form-urlencoded");
+  }
+
+  @Test(expected=ContentTypes.InvalidContentTypeException.class)
+  public void testStrictDisallowUnknown() throws Exception {
+    ContentTypes.checkContentTypes(ContentTypes.ALLOWED_JSON_CONTENT_TYPES,
+        "text/plain");
+  }
+
+  @Test
+  public void textExtractMimePart() throws Exception {
+    assertEquals("text/xml", ContentTypes.extractMimePart("Text/Xml ; charset = ISO-8859-1;x=y"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/DataCollectionTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/DataCollectionTest.java
new file mode 100644
index 0000000..a75f448
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/DataCollectionTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.collect.Maps;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class DataCollectionTest extends Assert {
+
+  @Test
+  public void testBasicMethods() throws Exception {
+    Map<String, Map<String, String>> entry = Maps.newHashMap();
+    DataCollection collection = new DataCollection(entry);
+    assertEquals(entry, collection.getEntry());
+
+    Map<String, Map<String, String>> newEntry = Maps.newHashMap();
+    Map<String, String> value = Maps.newHashMap();
+    value.put("knock knock", "who's there?");
+    value.put("banana", "banana who?");
+    value.put("banana!", "banana who?");
+    value.put("orange!", "?");
+    newEntry.put("orange you glad I didn't type banana", value);
+    collection.setEntry(newEntry);
+    assertEquals(newEntry, collection.getEntry());
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/DataServiceServletTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/DataServiceServletTest.java
new file mode 100644
index 0000000..3bce3a6
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/DataServiceServletTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.base.Strings;
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.easymock.IMocksControl;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Sets;
+
+public class DataServiceServletTest extends Assert {
+
+  private static final FakeGadgetToken FAKE_GADGET_TOKEN = new FakeGadgetToken()
+      .setOwnerId("john.doe").setViewerId("john.doe");
+
+  private HttpServletRequest req;
+  private HttpServletResponse res;
+  private DataServiceServlet servlet;
+  private BeanJsonConverter jsonConverter;
+  private BeanConverter xmlConverter;
+  private BeanConverter atomConverter;
+  private ContainerConfig containerConfig;
+
+  private IMocksControl mockControl = EasyMock.createNiceControl();
+
+  @Before
+  public void setUp() throws Exception {
+    servlet = new DataServiceServlet();
+    req = mockControl.createMock(HttpServletRequest.class);
+    res = mockControl.createMock(HttpServletResponse.class);
+    jsonConverter = mockControl.createMock(BeanJsonConverter.class);
+    xmlConverter = mockControl.createMock(BeanConverter.class);
+    atomConverter = mockControl.createMock(BeanConverter.class);
+    containerConfig = mockControl.createMock(ContainerConfig.class);
+
+    EasyMock.expect(jsonConverter.getContentType()).andReturn(
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE).anyTimes();
+    EasyMock.expect(xmlConverter.getContentType()).andReturn(
+        ContentTypes.OUTPUT_XML_CONTENT_TYPE).anyTimes();
+    EasyMock.expect(atomConverter.getContentType()).andReturn(
+        ContentTypes.OUTPUT_ATOM_CONTENT_TYPE).anyTimes();
+
+    HandlerRegistry registry = new DefaultHandlerRegistry(null, jsonConverter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(Sets.<Object>newHashSet(new TestHandler()));
+
+    servlet.setHandlerRegistry(registry);
+    servlet.setContainerConfig(containerConfig);
+    servlet.setJSONPAllowed(true);
+
+    servlet.setBeanConverters(jsonConverter, xmlConverter, atomConverter);
+  }
+
+  @Test
+  public void testUriRecognition() throws Exception {
+    verifyHandlerWasFoundForPathInfo("/test/5/@self");
+  }
+
+  private void verifyHandlerWasFoundForPathInfo(String peoplePathInfo)
+      throws Exception {
+    String post = "POST";
+    verifyHandlerWasFoundForPathInfo(peoplePathInfo, post, post);
+  }
+
+  private void verifyHandlerWasFoundForPathInfo(String pathInfo,
+    String actualMethod, String overrideMethod) throws Exception {
+    setupRequest(pathInfo, actualMethod, overrideMethod);
+
+    String method = Strings.isNullOrEmpty(overrideMethod) ? actualMethod : overrideMethod;
+
+    EasyMock.expect(jsonConverter.convertToString(
+        ImmutableMap.of("entry", TestHandler.REST_RESULTS.get(method))))
+        .andReturn("{ 'entry' : " + TestHandler.REST_RESULTS.get(method) + " }");
+
+    PrintWriter writerMock = EasyMock.createMock(PrintWriter.class);
+    EasyMock.expect(res.getWriter()).andReturn(writerMock);
+    writerMock.write(TestHandler.GET_RESPONSE);
+    EasyMock.expectLastCall();
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+    mockControl.reset();
+  }
+
+  @Test
+  public void testDisallowJSONP() throws Exception {
+    servlet.setJSONPAllowed(false);
+    String route = "/test";
+    verifyHandlerWasFoundForPathInfo(route, "POST", "GET");
+    servlet.setJSONPAllowed(true);
+  }
+
+  @Test
+  public void testOverridePostWithGet() throws Exception {
+    String route = "/test";
+    verifyHandlerWasFoundForPathInfo(route, "POST", "GET");
+  }
+
+  @Test
+  public void  testOverrideGetWithPost() throws Exception {
+    String route = "/test";
+    verifyHandlerWasFoundForPathInfo(route, "GET", "POST");
+  }
+
+  /**
+   * Tests a data handler that returns a failed Future
+   */
+  @Test
+  public void testFailedRequest() throws Exception {
+    String route = "/test";
+    setupRequest(route, "DELETE", null);
+
+    // Shouldnt these be expectations
+    res.sendError(HttpServletResponse.SC_BAD_REQUEST, TestHandler.FAILURE_MESSAGE);
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+    mockControl.reset();
+  }
+
+  private void setupRequest(String pathInfo, String actualMethod, String overrideMethod)
+      throws IOException {
+    FakeHttpServletRequest fakeReq = new FakeHttpServletRequest("/social/rest", pathInfo, "");
+    fakeReq.setPathInfo(pathInfo);
+    fakeReq.setParameter(DataServiceServlet.X_HTTP_METHOD_OVERRIDE, overrideMethod);
+    fakeReq.setCharacterEncoding("UTF-8");
+    if (!("GET").equals(actualMethod) && !("HEAD").equals(actualMethod)) {
+      fakeReq.setPostData("", "UTF-8");
+    }
+    fakeReq.setMethod(actualMethod);
+    fakeReq.setAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId(), FAKE_GADGET_TOKEN);
+    fakeReq.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    req = fakeReq;
+  }
+
+  @Test
+  public void testGetConverterForFormat() throws Exception {
+    assertConverterForFormat(atomConverter, "atom");
+    assertConverterForFormat(xmlConverter, "xml");
+    assertConverterForFormat(jsonConverter, "");
+    assertConverterForFormat(jsonConverter, null);
+    assertConverterForFormat(jsonConverter, "ahhhh!");
+  }
+
+  @Test
+  public void testGetConverterForContentType() throws Exception {
+    assertConverterForContentType(atomConverter, ContentTypes.OUTPUT_ATOM_CONTENT_TYPE);
+    assertConverterForContentType(xmlConverter, ContentTypes.OUTPUT_XML_CONTENT_TYPE);
+    assertConverterForContentType(xmlConverter, "text/xml");
+    assertConverterForContentType(jsonConverter, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertConverterForContentType(jsonConverter, "application/json");
+    assertConverterForContentType(jsonConverter, "");
+    assertConverterForContentType(jsonConverter, null);
+    assertConverterForContentType(jsonConverter, "abcd!");
+  }
+
+  private void assertConverterForFormat(BeanConverter converter, String format) {
+    assertEquals(converter, servlet.getConverterForFormat(format));
+  }
+
+  private void assertConverterForContentType(BeanConverter converter, String contentType) {
+    assertEquals(converter, servlet.getConverterForContentType(contentType));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/DefaultHandlerRegistryTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/DefaultHandlerRegistryTest.java
new file mode 100644
index 0000000..8719dda
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/DefaultHandlerRegistryTest.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.inject.Guice;
+
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests BasicHandleRregistry
+ */
+public class DefaultHandlerRegistryTest extends Assert {
+
+  private DefaultHandlerRegistry registry;
+  private BeanJsonConverter converter;
+
+  @Before
+  public void setUp() throws Exception {
+    converter = new BeanJsonConverter(Guice.createInjector());
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(Sets.<Object>newHashSet(new TestHandler()));
+  }
+
+  @Test
+  public void testGetHandlerRPC() throws Exception {
+    assertNotNull(registry.getRpcHandler(new JSONObject("{method : test.get}")));
+  }
+
+  @Test
+  public void testGetHandlerRest() throws Exception {
+    assertNotNull(registry.getRestHandler("/test/", "GET"));
+  }
+
+  @Test
+  public void testOverrideHandlerRPC() throws Exception {
+    assertNotNull(registry.getRpcHandler(new JSONObject("{method : test.overidden}")));
+  }
+
+  @Test
+  public void testOverrideHandlerRPCName() throws Exception {
+    assertNotNull(registry.getRpcHandler(new JSONObject("{method : test.override.rpcname}")));
+  }
+
+  @Test
+  public void testOverrideHandlerRest() throws Exception {
+    assertNotNull(registry.getRestHandler("/test/overidden/method/", "GET"));
+  }
+
+  @Test
+  public void testGetForAliasHandler() {
+    assertNotNull(registry.getRestHandler("/test", "GET"));
+  }
+
+  @Test
+  public void testRpcHandler_serviceDoesntExist() throws Exception {
+    JSONObject rpc = new JSONObject("{method : makebelieve.get}");
+    RpcHandler rpcHandler = registry.getRpcHandler(rpc);
+    try {
+      Future<?> future = rpcHandler.execute(null, null, null);
+      future.get();
+      fail("Expect exception for missing method");
+    } catch (ExecutionException t) {
+      assertSame(t.getCause().getClass(), ProtocolException.class);
+      Assert.assertEquals(HttpServletResponse.SC_NOT_IMPLEMENTED, ((ProtocolException) t.getCause()).getCode());
+    } catch (Throwable t) {
+      fail("Unexpected exception " + t.toString());
+    }
+  }
+
+  @Test
+  public void testRestHandler_serviceDoesntExist() {
+    RestHandler restHandler = registry.getRestHandler("/makebelieve", "GET");
+    try {
+      Future<?> future = restHandler.execute(Maps.<String, String[]>newHashMap(), null, null, null);
+      future.get();
+      fail("Expect exception for missing method");
+    } catch (ExecutionException t) {
+      assertSame(t.getCause().getClass(), ProtocolException.class);
+      Assert.assertEquals(HttpServletResponse.SC_NOT_IMPLEMENTED, ((ProtocolException) t.getCause()).getCode());
+    } catch (Throwable t) {
+      fail("Unexpected exception " + t.toString());
+    }
+  }
+
+  @Test
+  public void testNonFutureDispatch() throws Exception {
+    // Test calling a handler method which does not return a future
+    RestHandler handler = registry.getRestHandler("/test", "GET");
+    Future<?> future = handler.execute(Maps.<String, String[]>newHashMap(), null, null, null);
+    assertEquals(TestHandler.GET_RESPONSE, future.get());
+  }
+
+  @Test
+  public void testFutureDispatch() throws Exception {
+    // Test calling a handler method which does not return a future
+    RestHandler handler = registry.getRestHandler("/test", "POST");
+    Future<?> future = handler.execute(Maps.<String, String[]>newHashMap(), null, null, null);
+    assertEquals(TestHandler.CREATE_RESPONSE, future.get());
+  }
+
+  @Test
+  public void testRpcWithInputClassThatIsntRequestItem() throws Exception {
+    JSONObject rpc = new JSONObject("{ method : test.echo, params: {value: 'Bob' }}");
+    RpcHandler handler = registry.getRpcHandler(rpc);
+    Future<?> future = handler.execute(null, null, converter);
+    assertEquals(future.get(), TestHandler.ECHO_PREFIX + "Bob");
+  }
+
+  @Test
+  public void testRestWithInputClassThatIsntRequestItem() throws Exception {
+    RestHandler handler = registry.getRestHandler("/test/echo", "GET");
+    String[] value = {"Bob"};
+    Future<?> future = handler.execute(ImmutableMap.of("value", value), null, null, converter);
+    assertEquals(future.get(), TestHandler.ECHO_PREFIX + "Bob");
+  }
+
+  @Test
+  public void testNoArgumentClass() throws Exception {
+    JSONObject rpc = new JSONObject("{ method : test.noArg }");
+    RpcHandler handler = registry.getRpcHandler(rpc);
+    Future<?> future = handler.execute(null, null, converter);
+    assertEquals(TestHandler.NO_ARG_RESPONSE, future.get());
+  }
+
+  @Test
+  public void testNonFutureException() throws Exception {
+    // Test calling a handler method which does not return a future
+    JSONObject rpc = new JSONObject("{ method : test.exception }");
+    RpcHandler handler = registry.getRpcHandler(rpc);
+    Future<?> future = handler.execute(null, null, null);
+    try {
+      future.get();
+      fail("Service method did not produce NullPointerException from Future");
+    } catch (ExecutionException ee) {
+      assertSame(ee.getCause().getClass(), NullPointerException.class);
+    }
+  }
+
+  @Test
+  public void testFutureException() throws Exception {
+    // Test calling a handler method which does not return a future
+    JSONObject rpc = new JSONObject("{ method : test.futureException }");
+    RpcHandler handler = registry.getRpcHandler(rpc);
+    Future<?> future = handler.execute(null, null, null);
+    try {
+      future.get();
+      fail("Service method did not produce ExecutionException from Future");
+    } catch (ExecutionException ee) {
+      assertSame(ee.getCause().getClass(), ProtocolException.class);
+    }
+  }
+
+  @Test
+  public void testSupportedRpcServices() throws Exception {
+    assertEquals(registry.getSupportedRpcServices(),
+        Sets.newHashSet("test.create", "test.get", "test.overridden", "test.exception",
+            "test.futureException", "test.override.rpcname", "test.echo", "test.noArg"));
+  }
+
+  @Test
+  public void testSupportedRestServices() throws Exception {
+    assertEquals(registry.getSupportedRestServices(),
+        Sets.newHashSet("GET /test/{someParam}/{someOtherParam}",
+            "PUT /test/{someParam}/{someOtherParam}",
+            "DELETE /test/{someParam}/{someOtherParam}",
+            "POST /test/{someParam}/{someOtherParam}",
+            "GET /test/overridden/method",
+            "GET /test/echo"));
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testAddNonService() {
+    registry.addHandlers(Sets.newHashSet(new Object()));
+  }
+
+  @Test
+  public void testRestPath() {
+    DefaultHandlerRegistry.RestPath restPath =
+        new DefaultHandlerRegistry.RestPath("/service/const1/{p1}/{p2}+/const2/{p3}", null);
+    DefaultHandlerRegistry.RestInvocationWrapper wrapper =
+        restPath.accept("service/const1/a/b,c/const2/d".split("/"));
+    assertArrayEquals(wrapper.pathParams.get("p1"), new String[]{"a"});
+    assertArrayEquals(wrapper.pathParams.get("p2"), new String[]{"b","c"});
+    assertArrayEquals(wrapper.pathParams.get("p3"), new String[]{"d"});
+    wrapper = restPath.accept("service/const1/a/b/const2".split("/"));
+    assertArrayEquals(wrapper.pathParams.get("p1"), new String[]{"a"});
+    assertArrayEquals(wrapper.pathParams.get("p2"), new String[]{"b"});
+    assertNull(wrapper.pathParams.get("p3"));
+    assertNull(restPath.accept("service/const1/{p1}/{p2}+".split("/")));
+    assertNull(restPath.accept("service/constmiss/{p1}/{p2}+/const2".split("/")));
+  }
+
+  @Test
+  public void testRestPathOrdering() {
+    DefaultHandlerRegistry.RestPath restPath1 =
+        new DefaultHandlerRegistry.RestPath("/service/const1/{p1}/{p2}+/const2/{p3}", null);
+    DefaultHandlerRegistry.RestPath restPath2 =
+        new DefaultHandlerRegistry.RestPath("/service/{p1}/{p2}+/const2/{p3}", null);
+    DefaultHandlerRegistry.RestPath restPath3 =
+        new DefaultHandlerRegistry.RestPath("/service/const1/const2/{p1}/{p2}+/{p3}", null);
+    Set<DefaultHandlerRegistry.RestPath> sortedSet = ImmutableSortedSet.of(restPath1, restPath2, restPath3);
+    Iterator<DefaultHandlerRegistry.RestPath> itr = sortedSet.iterator();
+    assertEquals(itr.next(), restPath3);
+    assertEquals(itr.next(), restPath1);
+    assertEquals(itr.next(), restPath2);
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/JsonRpcServletTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/JsonRpcServletTest.java
new file mode 100644
index 0000000..88efede
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/JsonRpcServletTest.java
@@ -0,0 +1,450 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.reset;
+
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+import org.apache.shindig.protocol.multipart.MultipartFormParser;
+import org.easymock.IMocksControl;
+import org.easymock.EasyMock;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.inject.Guice;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class JsonRpcServletTest extends Assert {
+
+  private static final FakeGadgetToken FAKE_GADGET_TOKEN = new FakeGadgetToken()
+      .setOwnerId("john.doe").setViewerId("john.doe");
+
+  private static final String IMAGE_FIELDNAME = "profile-photo";
+  private static final String IMAGE_DATA = "image data";
+  private static final byte[] IMAGE_DATA_BYTES = IMAGE_DATA.getBytes();
+  private static final String IMAGE_TYPE = "image/jpeg";
+
+  private HttpServletRequest req;
+  private HttpServletResponse res;
+  private JsonRpcServlet servlet;
+  private MultipartFormParser multipartFormParser;
+  private ContainerConfig containerConfig;
+
+  private final IMocksControl mockControl = EasyMock.createNiceControl();
+
+  private final ByteArrayOutputStream stream = new ByteArrayOutputStream();
+  private final PrintWriter writer = new PrintWriter(stream);
+  private final TestHandler handler = new TestHandler();
+
+  @Before
+  public void setUp() throws Exception {
+    servlet = new JsonRpcServlet();
+    req = mockControl.createMock(HttpServletRequest.class);
+    res = mockControl.createMock(HttpServletResponse.class);
+    containerConfig = mockControl.createMock(ContainerConfig.class);
+
+    multipartFormParser = mockControl.createMock(MultipartFormParser.class);
+    EasyMock.expect(multipartFormParser.isMultipartContent(req)).andStubReturn(false);
+    servlet.setMultipartFormParser(multipartFormParser);
+
+    BeanJsonConverter converter = new BeanJsonConverter(Guice.createInjector());
+
+    HandlerRegistry registry = new DefaultHandlerRegistry(null, null,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(Collections.<Object>singleton(handler));
+
+    servlet.setHandlerRegistry(registry);
+    servlet.setBeanConverters(converter, null, null);
+    servlet.setContainerConfig(containerConfig);
+    servlet.setJSONPAllowed(true);
+
+    handler.setMock(new TestHandler() {
+      @Override
+      public Object get(RequestItem req) {
+        return ImmutableMap.of("foo", "bar");
+      }
+    });
+  }
+
+  private String getOutput() throws IOException {
+    writer.close();
+    return stream.toString("UTF-8");
+  }
+
+  @Test
+  public void testMethodRecognition() throws Exception {
+    setupRequest("{method:test.get,id:id,params:{userId:5,groupId:@self}}");
+
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals("{id: 'id', result: {foo:'bar'}}", getOutput());
+  }
+
+  @Test
+  public void testPostMultipartFormData() throws Exception {
+    reset(multipartFormParser);
+
+    handler.setMock(new TestHandler() {
+      @Override
+      public Object get(RequestItem req) {
+        FormDataItem item = req.getFormMimePart(IMAGE_FIELDNAME);
+        return ImmutableMap.of("image-data", new String(item.get()),
+            "image-type", item.getContentType(),
+            "image-ref", req.getParameter("image-ref"));
+      }
+    });
+    expect(req.getMethod()).andStubReturn("POST");
+    expect(req.getAttribute(isA(String.class))).andReturn(FAKE_GADGET_TOKEN);
+    expect(req.getCharacterEncoding()).andStubReturn("UTF-8");
+    expect(req.getContentType()).andStubReturn(ContentTypes.MULTIPART_FORM_CONTENT_TYPE);
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    List<FormDataItem> formItems = Lists.newArrayList();
+    String request = "{method:'test.get',id:'id',params:" +
+        "{userId:5,groupId:'@self',image-ref:'@" + IMAGE_FIELDNAME + "'}}";
+    formItems.add(mockFormDataItem(JsonRpcServlet.REQUEST_PARAM,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE, request.getBytes(), true));
+    formItems.add(mockFormDataItem(IMAGE_FIELDNAME, IMAGE_TYPE, IMAGE_DATA_BYTES, false));
+    expect(multipartFormParser.isMultipartContent(req)).andReturn(true);
+    expect(multipartFormParser.parse(req)).andReturn(formItems);
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals("{id: 'id', result: {image-data:'" + IMAGE_DATA +
+        "', image-type:'" + IMAGE_TYPE + "', image-ref:'@" + IMAGE_FIELDNAME + "'}}", getOutput());
+  }
+
+  /**
+   * Test that it passes even when content-type is not set for "request" parameter. This would
+   * be the case where the request is published via webform.
+   */
+  @Test
+  public void testPostMultipartFormDataWithRequestFieldHavingNoContentType() throws Exception {
+    reset(multipartFormParser);
+
+    handler.setMock(new TestHandler() {
+      @Override
+      public Object get(RequestItem req) {
+        FormDataItem item = req.getFormMimePart(IMAGE_FIELDNAME);
+        return ImmutableMap.of("image-data", new String(item.get()),
+            "image-type", item.getContentType(),
+            "image-ref", req.getParameter("image-ref"));
+      }
+    });
+    expect(req.getMethod()).andStubReturn("POST");
+    expect(req.getAttribute(isA(String.class))).andReturn(FAKE_GADGET_TOKEN);
+    expect(req.getCharacterEncoding()).andStubReturn("UTF-8");
+    expect(req.getContentType()).andStubReturn(ContentTypes.MULTIPART_FORM_CONTENT_TYPE);
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    List<FormDataItem> formItems = Lists.newArrayList();
+    String request = "{method:'test.get',id:'id',params:" +
+        "{userId:5,groupId:'@self',image-ref:'@" + IMAGE_FIELDNAME + "'}}";
+    formItems.add(mockFormDataItem(IMAGE_FIELDNAME, IMAGE_TYPE, IMAGE_DATA_BYTES, false));
+    formItems.add(mockFormDataItem("request", null, request.getBytes(), true));
+    expect(multipartFormParser.isMultipartContent(req)).andReturn(true);
+    expect(multipartFormParser.parse(req)).andReturn(formItems);
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals("{id: 'id', result: {image-data:'" + IMAGE_DATA +
+        "', image-type:'" + IMAGE_TYPE + "', image-ref:'@" + IMAGE_FIELDNAME + "'}}", getOutput());
+  }
+
+
+  /**
+   * Test that any form-data other than "request" does not undergo any content type check.
+   */
+  @Test
+  public void testPostMultipartFormDataOnlyRequestFieldHasContentTypeChecked()
+      throws Exception {
+    reset(multipartFormParser);
+
+    handler.setMock(new TestHandler() {
+      @Override
+      public Object get(RequestItem req) {
+        FormDataItem item = req.getFormMimePart(IMAGE_FIELDNAME);
+        return ImmutableMap.of("image-data", new String(item.get()),
+            "image-type", item.getContentType(),
+            "image-ref", req.getParameter("image-ref"));
+      }
+    });
+    expect(req.getMethod()).andStubReturn("POST");
+    expect(req.getAttribute(isA(String.class))).andReturn(FAKE_GADGET_TOKEN);
+    expect(req.getCharacterEncoding()).andStubReturn("UTF-8");
+    expect(req.getContentType()).andStubReturn(ContentTypes.MULTIPART_FORM_CONTENT_TYPE);
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    List<FormDataItem> formItems = Lists.newArrayList();
+    String request = "{method:'test.get',id:'id',params:" +
+        "{userId:5,groupId:'@self',image-ref:'@" + IMAGE_FIELDNAME + "'}}";
+    formItems.add(mockFormDataItem(IMAGE_FIELDNAME, IMAGE_TYPE, IMAGE_DATA_BYTES, false));
+    formItems.add(mockFormDataItem("oauth_hash", "application/octet-stream",
+        "oauth-hash".getBytes(), true));
+    formItems.add(mockFormDataItem("request", null, request.getBytes(), true));
+    formItems.add(mockFormDataItem("oauth_signature", "application/octet-stream",
+        "oauth_signature".getBytes(), true));
+    expect(multipartFormParser.isMultipartContent(req)).andReturn(true);
+    expect(multipartFormParser.parse(req)).andReturn(formItems);
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals("{id: 'id', result: {image-data:'" + IMAGE_DATA +
+        "', image-type:'" + IMAGE_TYPE + "', image-ref:'@" + IMAGE_FIELDNAME + "'}}", getOutput());
+  }
+
+  /**
+   * Test that "request" field undergoes contentType check, and error is thrown if wrong content
+   * type is present.
+   */
+  @Test
+  public void testPostMultipartFormDataRequestFieldIsSubjectedToContentTypeCheck()
+      throws Exception {
+    reset(multipartFormParser);
+
+    handler.setMock(new TestHandler() {
+      @Override
+      public Object get(RequestItem req) {
+        FormDataItem item = req.getFormMimePart(IMAGE_FIELDNAME);
+        return ImmutableMap.of("image-data", item.get(),
+            "image-type", item.getContentType(),
+            "image-ref", req.getParameter("image-ref"));
+      }
+    });
+    expect(req.getMethod()).andStubReturn("POST");
+    expect(req.getAttribute(isA(String.class))).andReturn(FAKE_GADGET_TOKEN);
+    expect(req.getCharacterEncoding()).andStubReturn("UTF-8");
+    expect(req.getContentType()).andStubReturn(ContentTypes.MULTIPART_FORM_CONTENT_TYPE);
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    List<FormDataItem> formItems = Lists.newArrayList();
+    String request = "{method:'test.get',id:'id',params:" +
+        "{userId:5,groupId:'@self',image-ref:'@" + IMAGE_FIELDNAME + "'}}";
+    formItems.add(mockFormDataItem(IMAGE_FIELDNAME, IMAGE_TYPE, IMAGE_DATA_BYTES, false));
+    formItems.add(mockFormDataItem("request", "application/octet-stream", request.getBytes(),
+        true));
+    expect(multipartFormParser.isMultipartContent(req)).andReturn(true);
+    expect(multipartFormParser.parse(req)).andReturn(formItems);
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    String output = getOutput();
+    assertTrue(output.contains("Unsupported Content-Type application/octet-stream"));
+  }
+
+  @Test
+  public void testInvalidService() throws Exception {
+    setupRequest("{method:junk.get,id:id,params:{userId:5,groupId:@self}}");
+
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals(
+        "{id:id,error:{message:'notImplemented: The method junk.get is not implemented',code:501}}",
+        getOutput());
+  }
+
+
+  /**
+   * Tests a data handler that returns a failed Future.
+   * @throws Exception on failure
+   */
+  @Test
+  public void testFailedRequest() throws Exception {
+    setupRequest("{id:id,method:test.futureException}");
+
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals(
+        "{id:id,error:{message:'badRequest: FAILURE_MESSAGE',code:400}}", getOutput());
+  }
+
+  @Test
+  public void testBasicBatch() throws Exception {
+    setupRequest("[{method:test.get,id:'1'},{method:test.get,id:'2'}]");
+
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals("[{id:'1',result:{foo:'bar'}},{id:'2',result:{foo:'bar'}}]",
+        getOutput());
+  }
+
+  @Test
+  public void testDisallowJSONP() throws Exception {
+    servlet.setJSONPAllowed(false);
+    setupRequest("[{method:test.get,id:'1'},{method:test.get,id:'2'}]");
+
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals("[{id:'1',result:{foo:'bar'}},{id:'2',result:{foo:'bar'}}]",
+        getOutput());
+    servlet.setJSONPAllowed(true);
+  }
+
+  @Test
+  public void testGetExecution() throws Exception {
+    expect(req.getParameterMap()).andStubReturn(
+        ImmutableMap.of("method", new String[]{"test.get"}, "id", new String[]{"1"}));
+    expect(req.getMethod()).andStubReturn("GET");
+    expect(req.getAttribute(isA(String.class))).andReturn(FAKE_GADGET_TOKEN);
+    expect(req.getCharacterEncoding()).andStubReturn("UTF-8");
+    res.setCharacterEncoding("UTF-8");
+
+    expect(res.getWriter()).andReturn(writer);
+    expectLastCall();
+
+    mockControl.replay();
+    servlet.service(req, res);
+    mockControl.verify();
+
+    JsonAssert.assertJsonEquals("{id:'1',result:{foo:'bar'}}", getOutput());
+  }
+
+  @Test
+  public void testGetJsonResponseWithKey() throws Exception {
+    ResponseItem responseItem = new ResponseItem("Name");
+    Object result = servlet.getJSONResponse("my-key", responseItem);
+    JsonAssert.assertObjectEquals("{id: 'my-key', result: 'Name'}", result);
+  }
+
+  @Test
+  public void testGetJsonResponseWithoutKey() throws Exception {
+    ResponseItem responseItem = new ResponseItem("Name");
+    Object result = servlet.getJSONResponse(null, responseItem);
+    JsonAssert.assertObjectEquals("{result: 'Name'}", result);
+  }
+
+  @Test
+  public void testGetJsonResponseErrorWithData() throws Exception {
+    ResponseItem responseItem = new ResponseItem(401, "Error Message", "Optional Data");
+    Object result = servlet.getJSONResponse(null, responseItem);
+    JsonAssert.assertObjectEquals(
+        "{error: {message: 'unauthorized: Error Message', data: 'Optional Data', code: 401}}",
+        result);
+  }
+
+  @Test
+  public void testGetJsonResponseErrorWithoutData() throws Exception {
+    ResponseItem responseItem = new ResponseItem(401, "Error Message");
+    Object result = servlet.getJSONResponse(null, responseItem);
+    JsonAssert.assertObjectEquals(
+        "{error: {message:'unauthorized: Error Message', code:401}}",
+        result);
+  }
+
+  private void setupRequest(String json) throws IOException {
+    final InputStream in = new ByteArrayInputStream(json.getBytes());
+    ServletInputStream stream = new ServletInputStream() {
+      @Override
+      public int read() throws IOException {
+        return in.read();
+      }
+    };
+
+    expect(req.getInputStream()).andStubReturn(stream);
+    expect(req.getMethod()).andStubReturn("POST");
+    expect(req.getAttribute(isA(String.class))).andReturn(FAKE_GADGET_TOKEN);
+    expect(req.getCharacterEncoding()).andStubReturn("UTF-8");
+    expect(req.getContentType()).andStubReturn(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+  }
+
+  private FormDataItem mockFormDataItem(String fieldName, String contentType, byte content[],
+      boolean isFormField) throws IOException {
+    InputStream in = new ByteArrayInputStream(content);
+    FormDataItem formDataItem = mockControl.createMock(FormDataItem.class);
+    expect(formDataItem.getContentType()).andStubReturn(contentType);
+    expect(formDataItem.getSize()).andStubReturn((long) content.length);
+    expect(formDataItem.get()).andStubReturn(content);
+    expect(formDataItem.getAsString()).andStubReturn(new String(content));
+    expect(formDataItem.getFieldName()).andStubReturn(fieldName);
+    expect(formDataItem.isFormField()).andStubReturn(isFormField);
+    expect(formDataItem.getInputStream()).andStubReturn(in);
+    return formDataItem;
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/RestfulCollectionTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/RestfulCollectionTest.java
new file mode 100644
index 0000000..85582ab
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/RestfulCollectionTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.collect.Lists;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.List;
+
+public class RestfulCollectionTest extends Assert {
+  @Test
+  public void testBasicMethods() throws Exception {
+    RestfulCollection<String> collection
+        = new RestfulCollection<String>(Lists.<String>newArrayList());
+
+    List<String> entryList = Lists.newArrayList("banana");
+    int startIndex = 5;
+    int totalResults = 8675309;
+
+    collection.setList(entryList);
+    collection.setStartIndex(startIndex);
+    collection.setTotalResults(totalResults);
+
+    assertEquals(entryList, collection.getList());
+    assertEquals(startIndex, collection.getStartIndex());
+    assertEquals(totalResults, collection.getTotalResults());
+  }
+
+  @Test
+  public void testConstructors() throws Exception {
+    List<String> entry = Lists.newArrayList("banana", "who");
+    RestfulCollection<String> collection = new RestfulCollection<String>(entry);
+
+    assertEquals(entry, collection.getList());
+    assertEquals(0, collection.getStartIndex());
+    assertEquals(entry.size(), collection.getTotalResults());
+    assertEquals(entry.size(), collection.getItemsPerPage());
+
+    int startIndex = 9;
+    int totalResults = 10;
+    int resultsPerPage = 1;
+    collection = new RestfulCollection<String>(entry, startIndex, totalResults, resultsPerPage);
+
+    assertEquals(entry, collection.getList());
+    assertEquals(startIndex, collection.getStartIndex());
+    assertEquals(totalResults, collection.getTotalResults());
+    assertEquals(resultsPerPage, collection.getItemsPerPage());
+  }
+
+  @Test
+  public void testMapMethods() throws Exception {
+    RestfulCollection<String> collection
+        = new RestfulCollection<String>(Lists.<String>newArrayList());
+
+    List<String> entryList = Lists.newArrayList("banana");
+    int startIndex = 5;
+    int totalResults = 8675309;
+    String anyItem = "anyvalue";
+
+    collection.put("list", entryList);
+    collection.put("startIndex", startIndex);
+    collection.put("totalResults", totalResults);
+    collection.put("anyItem",anyItem);
+
+    assertEquals(entryList, collection.get("list"));
+    assertEquals(startIndex, collection.get("startIndex"));
+    assertEquals(totalResults, collection.get("totalResults"));
+    assertEquals(anyItem, collection.get("anyItem"));
+
+    int resultsPerPage = 1;
+    List<String> entry = Lists.newArrayList("banana", "who");
+    collection = new RestfulCollection<String>(entry, startIndex, totalResults, resultsPerPage);
+
+    assertEquals(entry, collection.get("list"));
+    assertEquals(startIndex, collection.get("startIndex"));
+    assertEquals(totalResults, collection.get("totalResults"));
+    assertEquals(resultsPerPage, collection.get("itemsPerPage"));
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/TestHandler.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/TestHandler.java
new file mode 100644
index 0000000..6cae9db
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/TestHandler.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.Futures;
+
+
+import org.junit.Ignore;
+
+import java.util.Map;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Simple test handler implementation. Can be used standalone or to wrap a mock
+ * delegate.
+ */
+@Ignore
+@Service(name = "test", path = "/{someParam}/{someOtherParam}" )
+public class TestHandler {
+
+  public static final String GET_RESPONSE = "GET_RESPONSE";
+  public static final String CREATE_RESPONSE = "CREATE_RESPONSE";
+  public static final String FAILURE_MESSAGE = "FAILURE_MESSAGE";
+  public static final String ECHO_PREFIX = "ECHO: ";
+  public static final String NO_ARG_RESPONSE = "No arguments from me!";
+
+  public static Map<String,String> REST_RESULTS = ImmutableMap.of(
+      "POST", CREATE_RESPONSE, "GET", GET_RESPONSE, "DELETE", FAILURE_MESSAGE);
+
+  private TestHandler mock;
+
+  public TestHandler() {
+  }
+
+  public void setMock(TestHandler mock) {
+    this.mock = mock;
+  }
+
+  @Operation(httpMethods = "GET")
+  public Object get(RequestItem req) {
+    if (mock != null) {
+      return mock.get(req);
+    }
+    return GET_RESPONSE;
+  }
+
+  @Operation(httpMethods = "GET", path = "/overridden/method")
+  public Object overridden(RequestItem req) {
+    if (mock != null) {
+      return mock.get(req);
+    }
+    return GET_RESPONSE;
+  }
+
+  @Operation(name="override.rpcname", httpMethods = "")
+  public Object overriddenRpc(RequestItem req) {
+    if (mock != null) {
+      return mock.get(req);
+    }
+    return GET_RESPONSE;
+  }
+
+  @Operation(httpMethods = {"POST", "PUT"})
+  public Future<?> create(RequestItem req) {
+    if (mock != null) {
+      return mock.create(req);
+    }
+    return Futures.immediateFuture(CREATE_RESPONSE);
+  }
+
+  @Operation(httpMethods = "DELETE")
+  public Future<?> futureException(RequestItem req) {
+    if (mock != null) {
+      return mock.futureException(req);
+    }
+    return Futures.immediateFailedFuture(new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+        FAILURE_MESSAGE, new Throwable()));
+  }
+
+  @Operation(httpMethods = {})
+  public void exception(RequestItem req) {
+    if (mock != null) {
+      mock.exception(req);
+    } else {
+      throw new NullPointerException(FAILURE_MESSAGE);
+    }
+  }
+
+  @Operation(httpMethods = "GET", path = "/echo")
+  public String echo(Input input) {
+    return ECHO_PREFIX + input.value;
+  }
+
+  @Ignore
+  public static class Input {
+    public String value;
+    public void setValue(String value) { this.value = value; }
+  }
+
+  @Operation(httpMethods = "")
+  public String noArg() {
+    return NO_ARG_RESPONSE;
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java
new file mode 100644
index 0000000..781e4e9
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanDelegatorTest.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Assert;
+
+import org.apache.shindig.protocol.conversion.BeanFilter.Unfiltered;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.Map;
+
+public class BeanDelegatorTest extends Assert {
+
+  // Note, this classes also used by the BeanFilter tests
+  public static interface SimpleBeanInterface {
+    public int getI();
+    public SimpleBeanInterface setI(int i);
+    public String getS();
+    // Test list conversions:
+    public List<String> getList();
+    public List<SimpleBeanInterface> getBeanList();
+    // Test Map conversion
+    public Map<String, String> getMap();
+    public Map<String, SimpleBeanInterface> getBeanMap();
+    // Test error cases
+    public String getUnknown(); // delegated class doesn't have this
+    public int getWrongType(); // delegated class return different type
+    public String getPrivateData(); // delegated class method is private
+
+    // Test enum
+    public enum Style { A, B }
+    public Style getStyle();
+
+    // Test of required
+    @Unfiltered
+    public String getRequired();
+  }
+
+  public static class SimpleBean {
+    private int i;
+    private String s;
+    private List<String> l;
+    private List<SimpleBean> beanList;
+    private Map<String, String> stringMap;
+    private Map<String, SimpleBean> beanMap;
+
+    public int getI() { return i; }
+    public SimpleBean setI(int ni) { i = ni; return this; }
+
+    public String getS() { return s; }
+    public SimpleBean setS(String ns) { s = ns; return this; }
+
+    public List<String> getList() { return l; }
+    public SimpleBean setList(List<String> nl) { l = nl; return this; }
+
+    public List<SimpleBean> getBeanList() { return beanList; }
+    public SimpleBean setBeanList(List<SimpleBean> nl) { beanList = nl; return this; }
+
+    public Map<String, String> getMap() { return stringMap; }
+    public SimpleBean setMap(Map<String, String> nm) { stringMap = nm; return this; }
+
+    public Map<String, SimpleBean> getBeanMap() { return beanMap; }
+    public SimpleBean setBeanMap(Map<String, SimpleBean> nm) { beanMap = nm; return this; }
+
+    public String getWrongType() { return "this is string"; }
+
+    @SuppressWarnings("unused")
+    private String getPrivateData() { return "this is private"; }
+
+    // Enum data:
+    public enum RealStyle { R_A, R_B
+    }
+    RealStyle style;
+    public RealStyle getStyle() { return style; }
+    public SimpleBean setStyle(RealStyle style) { this.style = style; return this; }
+
+    // Test of required
+    public String getRequired() { return "required"; }
+  }
+
+  private BeanDelegator beanDelegator;
+  private SimpleBean source;
+  private SimpleBeanInterface proxy;
+
+  public static BeanDelegator createSimpleDelegator() {
+    BeanDelegator beanDelegator = new BeanDelegator(
+        ImmutableMap.<Class<?>, Class<?>>of(SimpleBean.class, SimpleBeanInterface.class,
+            SimpleBean.RealStyle.class, SimpleBeanInterface.Style.class),
+        ImmutableMap.<Enum<?>, Enum<?>>of(SimpleBean.RealStyle.R_A, SimpleBeanInterface.Style.A,
+            SimpleBean.RealStyle.R_B, SimpleBeanInterface.Style.B));
+    return beanDelegator;
+  }
+
+  @Before
+  public void setUp() {
+    beanDelegator = createSimpleDelegator();
+    source = new SimpleBean();
+    proxy = (SimpleBeanInterface) beanDelegator.createDelegator(source);
+  }
+
+  @Test
+  public void testSimpleBean() {
+    String s = "test";
+    source.setS(s);
+    assertEquals(s, proxy.getS());
+
+    proxy.setI(5);
+    assertEquals(5, proxy.getI());
+    assertEquals(5, source.getI());
+
+    source.setStyle(SimpleBean.RealStyle.R_A);
+    assertEquals(SimpleBeanInterface.Style.A, proxy.getStyle());
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testUnimplementedFunction() {
+    proxy.getUnknown();
+  }
+
+  @Test(expected = ClassCastException.class)
+  public void testWrontType() {
+    proxy.getWrongType();
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void testPrivateAccess() {
+    proxy.getPrivateData();
+  }
+
+  @Test
+  public void testStringList() {
+    assertNull(proxy.getList());
+    List<String> stringList = ImmutableList.of("item1", "item2");
+    source.setList(stringList);
+    assertEquals(stringList, proxy.getList());
+    stringList = ImmutableList.of();
+    source.setList(stringList);
+    assertEquals(stringList, proxy.getList());
+  }
+
+  @Test
+  public void testBeanList() {
+    List<SimpleBean> beanList = ImmutableList.of();
+    source.setBeanList(beanList);
+    assertEquals(beanList, proxy.getBeanList());
+
+    SimpleBean item = new SimpleBean().setS("item");
+    beanList = ImmutableList.of(item);
+    source.setBeanList(beanList);
+    List<SimpleBeanInterface> interList = proxy.getBeanList();
+    assertEquals(1, interList.size());
+    assertEquals(item.getS(), interList.get(0).getS());
+  }
+
+  @Test
+  public void testStringMap() {
+    assertNull(proxy.getMap());
+    Map<String, String> stringMap = ImmutableMap.of("item1", "v1", "item2", "v2");
+    source.setMap(stringMap);
+    assertEquals(stringMap, proxy.getMap());
+    stringMap = ImmutableMap.of();
+    source.setMap(stringMap);
+    assertEquals(stringMap, proxy.getMap());
+  }
+
+  @Test
+  public void testBeanMap() {
+    Map<String, SimpleBean> beanMap = ImmutableMap.of();
+    source.setBeanMap(beanMap);
+    assertEquals(beanMap, proxy.getBeanMap());
+
+    SimpleBean item = new SimpleBean().setS("item");
+    beanMap = ImmutableMap.of("item", item);
+    source.setBeanMap(beanMap);
+    Map<String, SimpleBeanInterface> interMap = proxy.getBeanMap();
+    assertEquals(1, interMap.size());
+    assertEquals(item.getS(), interMap.get("item").getS());
+  }
+
+  class TokenData {
+    public String getId() { return "id"; }
+  }
+
+  interface TokenInter {
+    public String getId();
+    public String getContainer();
+  }
+
+  @Test
+  public void testExtraFields() {
+    TokenData data = new TokenData();
+    String container = "data";
+    TokenInter p = beanDelegator.createDelegator(data, TokenInter.class,
+        ImmutableMap.<String, Object>of("container", container));
+
+    assertSame(data.getId(), p.getId());
+    assertSame(container, p.getContainer());
+  }
+
+  @Test
+  public void testExtraFieldsBadCase() {
+    TokenData data = new TokenData();
+    String container = "data";
+    TokenInter p = beanDelegator.createDelegator(data, TokenInter.class,
+        ImmutableMap.<String, Object>of("Cont_Ainer", container));
+
+    assertSame(data.getId(), p.getId());
+    assertSame(container, p.getContainer());
+  }
+
+  // Make sure validate will actually fail
+  @Test(expected = NoSuchMethodException.class)
+  public void tesValidate() throws Exception {
+    beanDelegator.validate();
+  }
+
+  @Test
+  public void testValidateGoodBean() throws Exception {
+    TokenInter p = beanDelegator.createDelegator(null, TokenInter.class,
+        ImmutableMap.<String, Object>of("container", "open", "id", "test"));
+    BeanDelegator.validateDelegator(p);
+  }
+
+  @Test(expected = InvocationTargetException.class)
+  public void testValidateWrongtype() throws Exception {
+    TokenInter p = beanDelegator.createDelegator(null, TokenInter.class,
+        ImmutableMap.<String, Object>of("container", "open", "id", new Integer(5)));
+    BeanDelegator.validateDelegator(p);
+  }
+
+  @Test(expected = InvocationTargetException.class)
+  public void testValidateMissingField() throws Exception {
+    TokenInter p = beanDelegator.createDelegator(null, TokenInter.class,
+        ImmutableMap.<String, Object>of("container", "open"));
+    BeanDelegator.validateDelegator(p);
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java
new file mode 100644
index 0000000..48804ba
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanFilterTest.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.shindig.protocol.conversion.BeanDelegatorTest.SimpleBean;
+import org.apache.shindig.protocol.conversion.BeanDelegatorTest.SimpleBeanInterface;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class BeanFilterTest extends Assert {
+
+  private BeanFilter beanFilter;
+  private BeanDelegator beanDelegator;
+
+  @Before
+  public void setUp() {
+    beanFilter = new BeanFilter();
+    beanDelegator = BeanDelegatorTest.createSimpleDelegator();
+  }
+
+  @Test
+  public void testNull() throws Exception {
+    assertNull(beanFilter.createFilteredBean(null, ImmutableSet.<String>of("s")));
+  }
+
+  @Test
+  public void testSimple() throws Exception {
+    String data = "data";
+
+    String newData = (String) beanFilter.createFilteredBean(data, null);
+    assertSame(data, newData);
+  }
+
+  @Test
+  public void testInt() throws Exception {
+    SimpleBean data = new SimpleBean().setI(5);
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("i"));
+    assertEquals(5, newData.getI());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("s"));
+    // Filter is ignored for primitive types:
+    assertEquals(5, newData.getI());
+  }
+
+  @Test
+  public void testString() throws Exception {
+    SimpleBean data = new SimpleBean().setS("data");
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("s"));
+    assertEquals("data", newData.getS());
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("i"));
+    assertNull("S is filtered out", newData.getS());
+    assertNotNull("Required field", newData.getRequired());
+  }
+
+  @Test
+  public void testList() throws Exception {
+    SimpleBean data = new SimpleBean().setList(ImmutableList.<String>of("d1", "d2"));
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+      dataBean, ImmutableSet.<String>of("s"));
+    assertEquals(null, newData.getList());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(
+        dataBean, ImmutableSet.<String>of("list"));
+    assertArrayEquals(data.getList().toArray(), newData.getList().toArray());
+  }
+
+  @Test
+  public void testMap() throws Exception {
+    List<String> list = ImmutableList.of("test");
+    SimpleBean data = new SimpleBean().setS("Main").setBeanMap(
+        ImmutableMap.<String, SimpleBean>of( "s1", new SimpleBean().setS("sub1").setList(list),
+          "s2", new SimpleBean().setS("sub2").setList(list).setBeanMap(
+              ImmutableMap.of("s2s1", new SimpleBean().setS("sub2-sub1"))
+        )));
+    SimpleBeanInterface dataBean = (SimpleBeanInterface) beanDelegator.createDelegator(data);
+
+    SimpleBeanInterface newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+        ImmutableSet.<String>of("beanmap"));
+    assertEquals(2, newData.getBeanMap().size());
+    assertEquals(null, newData.getBeanMap().get("s1").getS());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+      ImmutableSet.<String>of("beanmap", "beanmap.s"));
+    assertNotSame(dataBean.getBeanMap().getClass(), newData.getBeanMap().getClass());
+    assertEquals(2, newData.getBeanMap().size());
+    assertEquals("sub1", newData.getBeanMap().get("s1").getS());
+    assertNull("List is filtered out", newData.getBeanMap().get("s1").getList());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+      ImmutableSet.<String>of("beanmap", "beanmap.*"));
+    // Verify filter is a simple pass through.
+    // can only check class since each time different delegator is created
+    assertSame(dataBean.getBeanMap().getClass(), newData.getBeanMap().getClass());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+        ImmutableSet.<String>of("beanmap", "beanmap.beanmap", "beanmap.beanmap.s"));
+    assertEquals(2, newData.getBeanMap().size());
+    Map<String, SimpleBeanInterface> subSubMap = newData.getBeanMap().get("s2").getBeanMap();
+    assertEquals(1, subSubMap.size());
+    assertEquals("sub2-sub1", subSubMap.get("s2s1").getS());
+    assertNull("list is filtered", subSubMap.get("s2s1").getList());
+
+    newData = (SimpleBeanInterface) beanFilter.createFilteredBean(dataBean,
+        ImmutableSet.<String>of("beanmap", "beanmap.beanmap", "beanmap.beanmap.*"));
+    assertEquals(2, newData.getBeanMap().size());
+    assertNotSame(dataBean.getBeanMap().getClass(), newData.getBeanMap().getClass());
+    assertSame(data.getBeanMap().get("s2").getBeanMap().getClass(),
+        newData.getBeanMap().get("s2").getBeanMap().getClass());
+  }
+
+  @Test
+  public void testProcessFields() {
+    Set<String> srcFields = ImmutableSet.of("A", "b", "c.d.e.f", "Case", "cAse", "CASE");
+    Set<String> newFields = beanFilter.processBeanFields(srcFields);
+    assertEquals(7, newFields.size());
+    assertTrue(newFields.contains("a"));
+    assertTrue(newFields.contains("b"));
+    assertTrue(newFields.contains("c"));
+    assertTrue(newFields.contains("c.d"));
+    assertTrue(newFields.contains("c.d.e"));
+    assertTrue(newFields.contains("c.d.e.f"));
+    assertTrue(newFields.contains("case"));
+  }
+
+  @Test
+  public void testListFields() {
+    List<String> fields = beanFilter.getBeanFields(SimpleBeanInterface.class, 3);
+    assertTrue(fields.contains("Map"));
+    assertTrue(fields.contains("I"));
+    assertTrue(fields.contains("S"));
+    assertTrue(fields.contains("Style"));
+    assertTrue(fields.contains("List"));
+    assertTrue(fields.contains("BeanList.List"));
+    assertTrue(fields.contains("Map"));
+    assertTrue(fields.contains("BeanMap.List"));
+    assertTrue(fields.contains("BeanMap.BeanMap.BeanMap"));
+    assertFalse(fields.contains("BeanMap.BeanMap.BeanMap.BeanMap"));
+    assertEquals(77, fields.size());
+    // If failed use next prints to verify and fix
+    // System.out.println(fields.size());
+    // System.out.println(fields.toString());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanJsonConverterInjectedClassTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanJsonConverterInjectedClassTest.java
new file mode 100644
index 0000000..4aa3dd9
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanJsonConverterInjectedClassTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * The Class BeanJsonConverterInjectedClassTest.
+ */
+public class BeanJsonConverterInjectedClassTest extends Assert {
+
+  /** The bean json converter. */
+  private BeanJsonConverter beanJsonConverter;
+
+  @Before
+  public void setUp() throws Exception {
+    this.beanJsonConverter = new BeanJsonConverter(Guice.createInjector(new TestModule()));
+  }
+
+  /**
+   * Test json conversion of a TestInterface into a TestObject
+   *
+   * @throws Exception the exception
+   */
+  @Test
+  public void testJsonToObject() throws Exception {
+    String json = "{x:'xValue',y:'yValue'}";
+    TestObject object = (TestObject) beanJsonConverter.convertToObject(json, TestInterface.class);
+    assertNotNull("expected 'x' field not set after json conversion", object.getX());
+    assertNotNull("expected 'y' field not set after json conversion", object.getY());
+  }
+
+  /**
+   * TestModule that binds TestObject to TestInterface
+   */
+  private static class TestModule extends AbstractModule {
+    /* (non-Javadoc)
+     * @see com.google.inject.AbstractModule#configure()
+     */
+    @Override
+    protected void configure() {
+      bind(TestInterface.class).to(TestObject.class);
+    }
+  }
+
+  /**
+   * TestInterface.
+   */
+  public interface TestInterface {
+    String getX();
+    void setX(String x);
+  }
+
+  /**
+   * TestObject.
+   */
+  public static class TestObject implements TestInterface {
+
+    private String x;
+    private String y;
+
+    public String getX() {
+      return x;
+    }
+    public void setX(String x) {
+      this.x = x;
+    }
+
+    public String getY() {
+      return y;
+    }
+    public void setY(String y) {
+      this.y = y;
+    }
+  }
+
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanJsonConverterTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanJsonConverterTest.java
new file mode 100644
index 0000000..469d1d2
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/conversion/BeanJsonConverterTest.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.conversion;
+
+import org.apache.shindig.protocol.model.Model;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Guice;
+import com.google.inject.TypeLiteral;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+public class BeanJsonConverterTest extends Assert {
+  private BeanJsonConverter beanJsonConverter;
+
+  @Before
+  public void setUp() throws Exception {
+    beanJsonConverter = new BeanJsonConverter(Guice.createInjector());
+  }
+
+  public static class TestObject {
+    static String staticValue;
+    String hello;
+    int count;
+    List<TestObject> children;
+    TestEnum testEnum;
+
+    public static void setSomeStatic(String staticValue) {
+      TestObject.staticValue = staticValue;
+    }
+
+    public void setHello(String hello) {
+      this.hello = hello;
+    }
+
+    public void setCount(int count) {
+      this.count = count;
+    }
+
+    public void setChildren(List<TestObject> children) {
+      this.children = children;
+    }
+
+    public enum TestEnum {
+      foo, bar, baz
+    }
+
+    public void setTestEnum(TestEnum testEnum) {
+      this.testEnum = testEnum;
+    }
+  }
+
+  @Test
+  public void testJsonToObject() throws Exception {
+    String json = '{' +
+        "hello:'world'," +
+        "count:10," +
+        "someStatic:'foo'," +
+        "testEnum:'bar'," +
+        "children:[{hello:'world-2',count:11},{hello:'world-3',count:12}]}";
+
+    TestObject object = beanJsonConverter.convertToObject(json, TestObject.class);
+
+    assertEquals("world", object.hello);
+    assertEquals(10, object.count);
+    assertEquals("world-2", object.children.get(0).hello);
+    assertEquals(11, object.children.get(0).count);
+    assertEquals("world-3", object.children.get(1).hello);
+    assertEquals(12, object.children.get(1).count);
+    assertNull("Should not set static values", TestObject.staticValue);
+    assertEquals(TestObject.TestEnum.bar, object.testEnum);
+  }
+
+  @Test
+  public void testJsonToPrimitives() throws Exception {
+    String simpleJson = "{hello:'world',count:10}";
+
+    Object object = beanJsonConverter.convertToObject(simpleJson, null);
+
+    Map<?, ?> map = (Map<?, ?>) object;
+
+    assertEquals("world", map.get("hello"));
+    assertEquals(10, map.get("count"));
+  }
+
+  @Test
+  public void testJsonToCar() throws Exception {
+    String carJson = "{engine:[{value:DIESEL},{value:TURBO}],parkingTickets:{SF:$137,NY:'$301'}," +
+            "passengers:[{gender:female,name:'Mum'}, {gender:male,name:'Dad'}]}";
+
+    Model.Car car = beanJsonConverter.convertToObject(carJson, Model.Car.class);
+    ArrayList<Model.Engine> engineInfo = Lists.newArrayList(Model.Engine.DIESEL,
+        Model.Engine.TURBO);
+    for (int i = 0; i < car.getEngine().size(); i++) {
+      assertEquals(car.getEngine().get(i).getValue(), engineInfo.get(i));
+    }
+
+    assertEquals(car.getParkingTickets(), ImmutableMap.of("SF", "$137", "NY", "$301"));
+    Model.Passenger mum = car.getPassengers().get(0);
+    assertEquals(Model.Gender.female, mum.getGender());
+    assertEquals("Mum", mum.getName());
+    Model.Passenger dad = car.getPassengers().get(1);
+    assertEquals(Model.Gender.male, dad.getGender());
+    assertEquals("Dad", dad.getName());
+  }
+
+  @Test
+  public void testJsonToMap() throws Exception {
+    String jsonActivity = "{count : 0, favoriteColor : 'yellow'}";
+    Map<String, Object> data = beanJsonConverter.convertToObject(jsonActivity,
+        new TypeLiteral<Map<String, Object>>(){}.getType());
+
+    assertEquals(2, data.size());
+
+    for (Entry<String, Object> entry : data.entrySet()) {
+      String key = entry.getKey();
+      Object value = entry.getValue();
+      if (key.equals("count")) {
+        assertEquals(0, value);
+      } else if (key.equals("favoriteColor")) {
+        assertEquals("yellow", value);
+      }
+    }
+  }
+
+  @Test
+  public void testJsonToMapWithConversion() throws Exception {
+    String jsonActivity = "{count : 0, favoriteColor : 'yellow'}";
+    Map<String, String> data = Maps.newHashMap();
+    data = beanJsonConverter.convertToObject(jsonActivity,
+        new TypeLiteral<Map<String, String>>(){}.getType());
+
+    assertEquals(2, data.size());
+
+    for (Entry<String, String> entry : data.entrySet()) {
+      String key = entry.getKey();
+      String value = entry.getValue();
+      if (key.equals("count")) {
+        assertEquals("0", value);
+      } else if (key.equals("favoriteColor")) {
+        assertEquals("yellow", value);
+      }
+    }
+  }
+
+  @Test
+  public void testJsonToNestedGeneric() throws Exception {
+    String jsonActivity = "{key0:[0,1,2],key1:[3,4,5]}";
+    Map<String, List<Integer>> data =  beanJsonConverter.convertToObject(jsonActivity,
+        new TypeLiteral<Map<String, List<Integer>>>(){}.getType());
+
+    assertEquals(2, data.size());
+
+    assertEquals(Arrays.asList(0, 1, 2), data.get("key0"));
+    assertEquals(Arrays.asList(3, 4, 5), data.get("key1"));
+  }
+
+  @Test
+  public void testEmptyJsonMap() throws Exception {
+    String emptyMap = "{}";
+    Map<String, String> data = beanJsonConverter.convertToObject(emptyMap,
+         new TypeLiteral<Map<String,String>>(){}.getType());
+    assertTrue(data.isEmpty());
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/model/Model.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/model/Model.java
new file mode 100644
index 0000000..c939aa5
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/model/Model.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.model;
+
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * Limited model to fully exercise data binding
+ */
+public class Model {
+
+  public static class Car {
+    public static final String DEFAULT_JSON =
+        "{\"engine\":[{\"value\":\"GAS\"},{\"value\":\"HYBRID\"}]," +
+            "\"parkingTickets\":{\"TOKYO\":\"250Y\",\"BERKELEY\":\"$120\"}," +
+            "\"passengers\":[{\"gender\":\"male\",\"name\":\"Dick Dastardly\"},{\"gender\":\"female\",\"name\":\"Speed Racer\"}]}";
+
+    public static final String DEFAULT_XML =
+        "<response>" +
+            "<engine>" +
+              "<EnumImpl><value><declaringClass>org.apache.shindig.protocol.model.Model$Engine</declaringClass><displayValue>Gas</displayValue></value></EnumImpl>" +
+              "<EnumImpl><value><declaringClass>org.apache.shindig.protocol.model.Model$Engine</declaringClass><displayValue>Hybrid</displayValue></value></EnumImpl>" +
+            "</engine>" +
+            "<parkingTickets>" +
+              "<entry><key>TOKYO</key><value>250Y</value></entry>" +
+              "<entry><key>BERKELEY</key><value>$120</value></entry>" +
+            "</parkingTickets>" +
+            "<passengers>" +
+              "<ModelPassenger>" +
+                "<gender><declaringClass>org.apache.shindig.protocol.model.Model$Gender</declaringClass></gender>" +
+                "<name>Dick Dastardly</name>" +
+              "</ModelPassenger>" +
+              "<ModelPassenger>" +
+                "<gender><declaringClass>org.apache.shindig.protocol.model.Model$Gender</declaringClass></gender>" +
+                "<name>Speed Racer</name>" +
+              "</ModelPassenger>" +
+            "</passengers></response>";
+
+    private List<Enum<Engine>> engine;
+    private Map<String, String> parkingTickets;
+    private List<Passenger> passengers;
+
+    public Car() {
+      List<Enum<Engine>> engines = Lists.newArrayList();
+      engines.add(new EnumImpl<Engine>(Engine.GAS, null));
+      engines.add(new EnumImpl<Engine>(Engine.HYBRID, null));
+      engine = engines;
+      parkingTickets = Maps.newHashMap();
+      parkingTickets.put("BERKELEY", "$120");
+      parkingTickets.put("TOKYO", "250Y");
+      passengers = Lists.newArrayList();
+      passengers.add(new Passenger("Dick Dastardly", Gender.male));
+      passengers.add(new Passenger("Speed Racer", Gender.female));
+    }
+
+    public Car(List<Enum<Engine>> engine, Map<String, String> parkingTickets,
+               List<Passenger> passengers) {
+      this.engine = engine;
+      this.parkingTickets = parkingTickets;
+      this.passengers = passengers;
+    }
+
+    public List<Enum<Engine>> getEngine() {
+      return engine;
+    }
+
+    public void setEngine(List<Enum<Engine>> engine) {
+      this.engine = engine;
+    }
+
+    public Map<String, String> getParkingTickets() {
+      return parkingTickets;
+    }
+
+    public void setParkingTickets(Map<String, String> parkingTickets) {
+      this.parkingTickets = parkingTickets;
+    }
+
+    public List<Passenger> getPassengers() {
+      return passengers;
+    }
+
+    public void setPassengers(List<Passenger> passengers) {
+      this.passengers = passengers;
+    }
+  }
+
+  public static class ExpensiveCar extends Car {
+    private int cost = 100000;
+
+    public int getCost() {
+      return cost;
+    }
+
+    public void setCost(int cost) {
+      this.cost = cost;
+    }
+  }
+
+  public static class Passenger {
+    private String name;
+    private Gender gender;
+
+    public Passenger() {
+      name = "Speed Racer";
+      gender = Gender.female;
+    }
+
+    public Passenger(String name, Gender gender) {
+      this.name = name;
+      this.gender = gender;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    public Gender getGender() {
+      return gender;
+    }
+
+    public void setGender(Gender gender) {
+      this.gender = gender;
+    }
+  }
+
+  public enum Engine implements org.apache.shindig.protocol.model.Enum.EnumKey {
+    DIESEL("DIESEL", "Diesel"),
+    GAS("GAS", "Gas"),
+    HYBRID("HYBRID", "Hybrid"),
+    TURBO("TURBO", "Turbo");
+
+    private final String jsonString;
+    private final String displayValue;
+
+    private Engine(String jsonString, String displayValue) {
+      this.jsonString = jsonString;
+      this.displayValue = displayValue;
+    }
+
+    public String toString() {
+      return this.jsonString;
+    }
+
+    public String getDisplayValue() {
+      return displayValue;
+    }
+  }
+
+  public enum Gender {
+    male,
+    female
+  }
+}
diff --git a/trunk/java/common/src/test/java/org/apache/shindig/protocol/multipart/DefaultMultipartFormParserTest.java b/trunk/java/common/src/test/java/org/apache/shindig/protocol/multipart/DefaultMultipartFormParserTest.java
new file mode 100644
index 0000000..4484b52
--- /dev/null
+++ b/trunk/java/common/src/test/java/org/apache/shindig/protocol/multipart/DefaultMultipartFormParserTest.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.protocol.multipart;
+
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.google.common.collect.Lists;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DefaultMultipartFormParserTest extends Assert {
+
+  private static final String REQUEST_FIELDNAME = "request";
+  private static final String REQUEST_DATA = "{name: 'HelloWorld'}";
+
+  private static final String ALBUM_IMAGE_FIELDNAME = "album-image";
+  private static final String ALBUM_IMAGE_FILENAME = "album-image.jpg";
+  private static final String ALBUM_IMAGE_DATA = "album image data";
+  private static final String ALBUM_IMAGE_TYPE = "image/jpeg";
+
+  private static final String PROFILE_IMAGE_FIELDNAME = "profile-image";
+  private static final String PROFILE_IMAGE_FILENAME = "profile-image.jpg";
+  private static final String PROFILE_IMAGE_DATA = "profile image data";
+  private static final String PROFILE_IMAGE_TYPE = "image/png";
+
+  private MultipartFormParser multipartFormParser;
+  private HttpServletRequest request;
+
+  @Before
+  public void setUp() throws Exception {
+    multipartFormParser = new DefaultMultipartFormParser();
+  }
+
+  /**
+   * Test that requests must be both POST and have a multipart
+   * content type.
+   */
+  @Test
+  public void testIsMultipartContent() {
+    FakeHttpServletRequest request = new FakeHttpServletRequest();
+
+    request.setMethod("GET");
+    assertFalse(multipartFormParser.isMultipartContent(request));
+
+    request.setMethod("POST");
+    assertFalse(multipartFormParser.isMultipartContent(request));
+
+    request.setContentType("multipart/form-data");
+    assertTrue(multipartFormParser.isMultipartContent(request));
+
+    request.setMethod("GET");
+    assertFalse(multipartFormParser.isMultipartContent(request));
+}
+
+  /**
+   * Helper class to create the multipart/form-data body of the POST request.
+   */
+  private static class MultipartFormBuilder {
+    private final String boundary;
+    private final StringBuilder packet = new StringBuilder();
+    private static final String BOUNDARY = "--abcdefgh";
+
+    public MultipartFormBuilder() {
+      this(BOUNDARY);
+    }
+
+    public MultipartFormBuilder(String boundary) {
+      this.boundary = boundary;
+    }
+
+    public String getContentType() {
+      return "multipart/form-data; boundary=" + boundary;
+    }
+
+    public byte[] build() {
+      write("--");
+      write(boundary);
+      write("--");
+      return packet.toString().getBytes();
+    }
+
+    public void addFileItem(String fieldName, String fileName, String content,
+        String contentType) {
+      writeBoundary();
+
+      write("Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" +
+          fileName + '\"');
+      write("\r\n");
+      write("Content-Type: " + contentType);
+      write("\r\n\r\n");
+      write(content);
+      write("\r\n");
+    }
+
+    public void addFormField(String fieldName, String content) {
+      addFormField(fieldName, content, null);
+    }
+
+    public void addFormField(String fieldName, String content, String contentType) {
+      writeBoundary();
+
+      write("Content-Disposition: form-data; name=\"" + fieldName + '\"');
+      if (contentType != null) {
+        write("\r\n");
+        write("Content-Type: " + contentType);
+      }
+      write("\r\n\r\n");
+      write(content);
+      write("\r\n");
+    }
+
+    private void writeBoundary() {
+      write("--");
+      write(boundary);
+      write("\r\n");
+    }
+
+    private void write(String content) {
+      packet.append(content);
+    }
+  }
+
+  private void setupRequest(byte[] postData, String contentType) throws IOException {
+    FakeHttpServletRequest fakeReq = new FakeHttpServletRequest("/social/rest", "", "");
+    fakeReq.setPostData(postData);
+    fakeReq.setContentType(contentType);
+    request = fakeReq;
+  }
+
+  @Test
+  public void testSingleFileItem() throws Exception {
+    MultipartFormBuilder builder = new MultipartFormBuilder();
+    builder.addFileItem(ALBUM_IMAGE_FIELDNAME, ALBUM_IMAGE_FILENAME, ALBUM_IMAGE_DATA,
+        ALBUM_IMAGE_TYPE);
+    setupRequest(builder.build(), builder.getContentType());
+
+    List<FormDataItem> formItems =
+      Lists.newArrayList(multipartFormParser.parse(request));
+
+    assertEquals(1, formItems.size());
+    FormDataItem formItem = formItems.get(0);
+    assertFalse(formItem.isFormField());
+    assertEquals(ALBUM_IMAGE_FIELDNAME, formItem.getFieldName());
+    assertEquals(ALBUM_IMAGE_FILENAME, formItem.getName());
+    assertEquals(ALBUM_IMAGE_TYPE, formItem.getContentType());
+    assertEquals(ALBUM_IMAGE_DATA, new String(formItem.get()));
+  }
+
+  @Test
+  public void testSingleRequest() throws Exception {
+    MultipartFormBuilder builder = new MultipartFormBuilder();
+    builder.addFormField(REQUEST_FIELDNAME, REQUEST_DATA);
+    setupRequest(builder.build(), builder.getContentType());
+
+    List<FormDataItem> formItems =
+      Lists.newArrayList(multipartFormParser.parse(request));
+
+    assertEquals(1, formItems.size());
+    FormDataItem formItem = formItems.get(0);
+    assertTrue(formItem.isFormField());
+    assertEquals(REQUEST_FIELDNAME, formItem.getFieldName());
+    assertEquals(REQUEST_DATA, new String(formItem.get()));
+  }
+
+  @Test
+  public void testSingleFileItemAndRequest() throws Exception {
+    MultipartFormBuilder builder = new MultipartFormBuilder();
+    builder.addFileItem(ALBUM_IMAGE_FIELDNAME, ALBUM_IMAGE_FILENAME, ALBUM_IMAGE_DATA,
+        ALBUM_IMAGE_TYPE);
+    builder.addFormField(REQUEST_FIELDNAME, REQUEST_DATA);
+    setupRequest(builder.build(), builder.getContentType());
+
+    List<FormDataItem> formItems =
+      Lists.newArrayList(multipartFormParser.parse(request));
+
+    assertEquals(2, formItems.size());
+    FormDataItem formItem = formItems.get(0);
+    assertFalse(formItem.isFormField());
+    assertEquals(ALBUM_IMAGE_FIELDNAME, formItem.getFieldName());
+    assertEquals(ALBUM_IMAGE_FILENAME, formItem.getName());
+    assertEquals(ALBUM_IMAGE_TYPE, formItem.getContentType());
+    assertEquals(ALBUM_IMAGE_DATA, new String(formItem.get()));
+
+    formItem = formItems.get(1);
+    assertTrue(formItem.isFormField());
+    assertEquals(REQUEST_FIELDNAME, formItem.getFieldName());
+    assertEquals(REQUEST_DATA, new String(formItem.get()));
+  }
+
+  @Test
+  public void testMultipleFileItemAndRequest() throws Exception {
+    MultipartFormBuilder builder = new MultipartFormBuilder();
+    builder.addFileItem(ALBUM_IMAGE_FIELDNAME, ALBUM_IMAGE_FILENAME, ALBUM_IMAGE_DATA,
+        ALBUM_IMAGE_TYPE);
+    builder.addFormField(REQUEST_FIELDNAME, REQUEST_DATA);
+    builder.addFileItem(PROFILE_IMAGE_FIELDNAME, PROFILE_IMAGE_FILENAME, PROFILE_IMAGE_DATA,
+        PROFILE_IMAGE_TYPE);
+    setupRequest(builder.build(), builder.getContentType());
+
+    List<FormDataItem> formItems =
+      Lists.newArrayList(multipartFormParser.parse(request));
+
+    assertEquals(3, formItems.size());
+    FormDataItem formItem = formItems.get(0);
+    assertFalse(formItem.isFormField());
+    assertEquals(ALBUM_IMAGE_FIELDNAME, formItem.getFieldName());
+    assertEquals(ALBUM_IMAGE_FILENAME, formItem.getName());
+    assertEquals(ALBUM_IMAGE_TYPE, formItem.getContentType());
+    assertEquals(ALBUM_IMAGE_DATA, new String(formItem.get()));
+
+    formItem = formItems.get(1);
+    assertTrue(formItem.isFormField());
+    assertEquals(REQUEST_FIELDNAME, formItem.getFieldName());
+    assertEquals(REQUEST_DATA, new String(formItem.get()));
+
+    formItem = formItems.get(2);
+    assertFalse(formItem.isFormField());
+    assertEquals(PROFILE_IMAGE_FIELDNAME, formItem.getFieldName());
+    assertEquals(PROFILE_IMAGE_FILENAME, formItem.getName());
+    assertEquals(PROFILE_IMAGE_TYPE, formItem.getContentType());
+    assertEquals(PROFILE_IMAGE_DATA, new String(formItem.get()));
+  }
+}
diff --git a/trunk/java/common/src/test/resources/classpath-accessible-test-file.txt b/trunk/java/common/src/test/resources/classpath-accessible-test-file.txt
new file mode 100644
index 0000000..39e35c8
--- /dev/null
+++ b/trunk/java/common/src/test/resources/classpath-accessible-test-file.txt
@@ -0,0 +1,18 @@
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+
+classpath-accessible-test-file.txt
\ No newline at end of file
diff --git a/trunk/java/common/src/test/resources/logging.properties b/trunk/java/common/src/test/resources/logging.properties
new file mode 100644
index 0000000..a2dc12e
--- /dev/null
+++ b/trunk/java/common/src/test/resources/logging.properties
@@ -0,0 +1,30 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+handlers=java.util.logging.ConsoleHandler
+java.util.logging.ConsoleHandler.level=ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+.level=INFO
+
+# Shindig commons
+org.apache.shindig.common.JsonContainerConfig.level=${java.util.logging.test.level}
+org.apache.shindig.common.cache.LruCacheProvider.level=${java.util.logging.test.level}
+org.apache.shindig.common.cache.ehcache.EhCacheCacheProvider.level=${java.util.logging.test.level}
+org.apache.shindig.common.xml.XmlUtil.level=${java.util.logging.test.level}
+
+# others
+net.sf.ehcache.Cache.level=${java.util.logging.test.level}
\ No newline at end of file
diff --git a/trunk/java/gadgets/README b/trunk/java/gadgets/README
new file mode 100644
index 0000000..5fa34ef
--- /dev/null
+++ b/trunk/java/gadgets/README
@@ -0,0 +1,12 @@
+Installing and Running Shindig Gadget Server
+============================================
+
+Please see BUILD-JAVA in the base project directory for information.
+
+Once the server is running, you can hit the server at
+http://localhost:<port>/gadgets/ifr?url=<gadget-url>
+
+Example: http://localhost:<port>/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
+
+For more information, see http://shindig.apache.org/
diff --git a/trunk/java/gadgets/pom.xml b/trunk/java/gadgets/pom.xml
new file mode 100644
index 0000000..9d42c9c
--- /dev/null
+++ b/trunk/java/gadgets/pom.xml
@@ -0,0 +1,251 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-gadgets</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Apache Shindig Gadget Renderer</name>
+  <description>Renders gadgets, provides the gadget metadata service, and serves
+    all javascript required by the OpenSocial specification.</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/gadgets</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/gadgets</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/gadgets</url>
+  </scm>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins </groupId>
+        <artifactId>maven-eclipse-plugin</artifactId>
+      </plugin>
+    </plugins>
+    <resources>
+      <resource>
+        <targetPath>config</targetPath>
+        <directory>${basedir}/../../config</directory>
+        <includes>
+          <include>oauth.json</include>
+          <include>oauth2.json</include>
+          <include>OSML_library.xml</include>
+          <include>gadget-admin.json</include>
+        </includes>
+      </resource>
+
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>false</filtering>
+      </resource>
+      <resource>
+        <!-- TODO: eliminate this inclusion -->
+        <!-- this is relative to the pom.xml directory -->
+        <directory>${basedir}/../../content/</directory>
+        <targetPath>files</targetPath>
+        <includes>
+          <include>xpc.swf</include>
+          <include>container/rpc_relay.html</include>
+          <include>container/cookiebaseduserprefstore.js</include>
+        </includes>
+      </resource>
+    </resources>
+    <pluginManagement>
+    	<plugins>
+    		<!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.-->
+    		<plugin>
+    			<groupId>org.eclipse.m2e</groupId>
+    			<artifactId>lifecycle-mapping</artifactId>
+    			<version>1.0.0</version>
+    			<configuration>
+    				<lifecycleMappingMetadata>
+    					<pluginExecutions>
+    						<pluginExecution>
+    							<pluginExecutionFilter>
+    								<groupId>org.codehaus.mojo</groupId>
+    								<artifactId>
+    									build-helper-maven-plugin
+    								</artifactId>
+    								<versionRange>[1.7,)</versionRange>
+    								<goals>
+    									<goal>add-source</goal>
+    								</goals>
+    							</pluginExecutionFilter>
+    							<action>
+    								<ignore />
+    							</action>
+    						</pluginExecution>
+    					</pluginExecutions>
+    				</lifecycleMappingMetadata>
+    			</configuration>
+    		</plugin>
+    	</plugins>
+    </pluginManagement>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>reporting</id>
+      <reporting>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>clirr-maven-plugin</artifactId>
+            <configuration>
+              <comparisonVersion>${shindig.api.previous&gt;}</comparisonVersion>
+            </configuration>
+            <version>2.2.3</version>
+          </plugin>
+        </plugins>
+      </reporting>
+    </profile>
+  </profiles>
+
+  <dependencies>
+    <!-- project dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+
+    <!--
+      external dependencies
+      where the depedency version is defined in dependency Management,
+      there is no need for the version, and it should not be put in
+      so we have a single place to change the version
+    -->
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>diff_match_patch</groupId>
+      <artifactId>diff_match_patch</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>caja</groupId>
+      <artifactId>caja</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>caja</groupId>
+      <artifactId>htmlparser</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.oauth.core</groupId>
+      <artifactId>oauth</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.oauth.core</groupId>
+      <artifactId>oauth-httpclient4</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.oauth.core</groupId>
+      <artifactId>oauth-provider</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-multibindings</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-lang3</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>rome</groupId>
+      <artifactId>rome</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>rome</groupId>
+      <artifactId>modules</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.ibm.icu</groupId>
+      <artifactId>icu4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.sourceforge.nekohtml</groupId>
+      <artifactId>nekohtml</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>xerces</groupId>
+      <artifactId>xercesImpl</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sanselan</groupId>
+      <artifactId>sanselan</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>xml-apis</groupId>
+      <artifactId>xml-apis</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.apache.tomcat</groupId>
+        <artifactId>el-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.javascript</groupId>
+      <artifactId>closure-compiler</artifactId>
+      <version>v20130227</version>
+    </dependency>
+
+    <!-- test -->
+    <dependency>
+      <groupId>org.mortbay.jetty</groupId>
+      <artifactId>jetty</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AbstractLockedDomainService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AbstractLockedDomainService.java
new file mode 100644
index 0000000..9a60943
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AbstractLockedDomainService.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.config.ContainerConfig;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Provides a default implementation of much of the basic functionality for managing locked domains.
+ *
+ * @since 2.5.0
+ */
+public abstract class AbstractLockedDomainService implements LockedDomainService {
+
+  /**
+   * Used to observer locked domain required in the config. Doing this instead of having
+   * AbstractLockedDomainService implement ConfigObserver so that subclasses don't need to know what
+   * AbstractLockedDomainService is observing. In order to add the config observer in the constructor
+   * this needed to be broken out to avoid initialization order problems when subclassing and
+   * calling super()
+   */
+  private class LockedDomainObserver implements ContainerConfig.ConfigObserver {
+    /*
+     * (non-Javadoc)
+     *
+     * @see org.apache.shindig.config.ContainerConfig.ConfigObserver#containersChanged
+     * (org.apache.shindig.config.ContainerConfig, java.util.Collection, java.util.Collection)
+     */
+    public void containersChanged(ContainerConfig config, Collection<String> changed,
+            Collection<String> removed) {
+      for (String container : changed) {
+        required.put(container, config.getBool(container, LOCKED_DOMAIN_REQUIRED_KEY));
+        permittedRefererDomains.put(container, config.getList(container, PERMITTED_REFERER_DOMAINS_KEY));
+      }
+      for (String container : removed) {
+        required.remove(container);
+        permittedRefererDomains.remove(container);
+      }
+    }
+  }
+
+  protected static final String LOCKED_DOMAIN_REQUIRED_KEY = "gadgets.uri.iframe.lockedDomainRequired";
+
+  protected static final String PERMITTED_REFERER_DOMAINS_KEY = "shindig.locked-domain.permittedRefererDomains";
+
+  protected static final String LOCKED_DOMAIN_FEATURE = "locked-domain";
+  private final boolean enabled;
+
+  private boolean refererCheckEnabled;
+
+  protected final Map<String, Boolean> required;
+  private boolean lockSecurityTokens = false;
+
+  private LockedDomainObserver ldObserver;
+
+  protected final Map<String, List<Object>> permittedRefererDomains;
+
+  private static final String classname = HashLockedDomainService.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  /**
+   * Create a LockedDomainService. This constructor should be called by implementors.
+   *
+   * @param config
+   *          the container config that will be observed
+   * @param enabled
+   *          true if locked domains are enabled; false otherwise
+   */
+  protected AbstractLockedDomainService(ContainerConfig config, boolean enabled) {
+    this.enabled = enabled;
+    this.required = Maps.newHashMap();
+    this.permittedRefererDomains = Maps.newHashMap();
+    if (enabled) {
+      this.ldObserver = new LockedDomainObserver();
+      config.addConfigObserver(this.ldObserver, true);
+    }
+  }
+
+  @Inject
+  public void setRefererCheckEnabled(@Named("shindig.locked-domain.refererCheck.enabled") boolean refererCheckEnabled) {
+      this.refererCheckEnabled = refererCheckEnabled;
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see org.apache.shindig.gadgets.LockedDomainService#getLockedDomainForGadget
+   * (org.apache.shindig.gadgets.Gadget, java.lang.String)
+   */
+  public abstract String getLockedDomainForGadget(Gadget gadget, String container)
+          throws GadgetException;
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see org.apache.shindig.gadgets.LockedDomainService#isEnabled()
+   */
+  public boolean isEnabled() {
+    return this.enabled;
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see org.apache.shindig.gadgets.LockedDomainService#isGadgetValidForHost(java .lang.String,
+   * org.apache.shindig.gadgets.Gadget, java.lang.String)
+   */
+  public abstract boolean isGadgetValidForHost(String host, Gadget gadget, String container);
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see org.apache.shindig.gadgets.LockedDomainService#isHostUsingLockedDomain (java.lang.String)
+   */
+  public abstract boolean isHostUsingLockedDomain(String host);
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see org.apache.shindig.gadgets.LockedDomainService#isSafeForOpenProxy(java .lang.String)
+   */
+  public boolean isSafeForOpenProxy(String host) {
+    if (isEnabled()) {
+      return !isHostUsingLockedDomain(host);
+    }
+    return true;
+  }
+
+  /*
+   * (non-Javadoc)
+   *
+   * @see org.apache.shindig.gadgets.LockedDomainService#isRefererCheckEnabled()
+   */
+  public boolean isRefererCheckEnabled() {
+    return this.refererCheckEnabled;
+  }
+
+  /**
+   * Allows a renderer to render all gadgets that require a security token on a locked domain. This
+   * is recommended security practice, as it secures the token from other gadgets, but because the
+   * "security-token" dependency on "locked-domain" is both implicit (added by GadgetSpec code for
+   * OAuth elements) and/or transitive (included by opensocial and opensocial-templates features),
+   * turning this behavior by default may take some by surprise. As such, we provide this flag. If
+   * false (by default), locked-domain will apply only when the gadget's Requires/Optional sections
+   * include it. Otherwise, the transitive dependency tree will be traversed to make this decision.
+   *
+   * @param lockSecurityTokens
+   *          If true, locks domains for all gadgets requiring security-token.
+   */
+  @Inject(optional = true)
+  public void setLockSecurityTokens(
+          @Named("shindig.locked-domain.lock-security-tokens") Boolean lockSecurityTokens) {
+    this.lockSecurityTokens = lockSecurityTokens;
+  }
+
+  /**
+   * Returns true if domain locking is enforced for every gadget by the container
+   *
+   * @param container
+   *          the container configuration, e.g., "default"
+   * @return true if domain locking is enforced by the container
+   */
+  protected boolean isDomainLockingEnforced(String container) {
+    return this.required.get(container);
+  }
+
+  /**
+   * Override methods for custom behavior Allows you to override locked domain feature requests from
+   * a gadget.
+   */
+  protected boolean isExcludedFromLockedDomain(Gadget gadget, String container) {
+    return false;
+  }
+
+  /**
+   * Returns true if the gadget is requesting to be on a locked domain. If security token locking
+   * has been enabled via {@link #setLockSecurityTokens(Boolean)}, this method will return true if
+   * the gadget is explicitly or implicitly requesting locked domains; otherwise, this will return
+   * true only if the gadget is explicitly requesting locked domains.
+   *
+   * @param gadget
+   *          the gadget
+   * @return true if the gadget is requesting to be on a locked domain
+   */
+  protected boolean isGadgetReqestingLocking(Gadget gadget) {
+    if (this.lockSecurityTokens) {
+      return gadget.getAllFeatures().contains(LOCKED_DOMAIN_FEATURE);
+    }
+    return gadget.getViewFeatures().keySet().contains(LOCKED_DOMAIN_FEATURE);
+  }
+
+  /**
+   * Check whether the referer value in the request head is in the permitted referer domain list or not.
+   * @param gadget
+   *     the gadget
+   * @param container
+   *     the container
+   * @return true if the referer is valid, otherwise return false.
+   */
+  protected boolean isValidReferer(Gadget gadget, String container) {
+    String referer = gadget.getContext().getReferer();
+    List<Object> domainList = this.permittedRefererDomains.get(container);
+    if (null != referer && !"".equals(referer.trim())) {
+      try {
+        URL url = new URL(referer);
+        if (!domainList.isEmpty()) {
+          boolean matched = false;
+          String refererHost = url.getHost().toLowerCase();
+          for (Object permittedDomain: domainList) {
+            if (refererHost.endsWith(((String) permittedDomain).toLowerCase())) {
+              matched = true;
+              break;
+            }
+          }
+          if (!matched) {
+            LOG.logp(Level.SEVERE, classname, "Referer check failed.",
+                MessageKeys.FAILED_TO_VALIDATE, new Object[] { referer });
+            return false;
+          }
+        }
+      } catch (MalformedURLException e) {
+        LOG.logp(Level.SEVERE, classname, "Referer check failed, malformed referer url.",
+            MessageKeys.FAILED_TO_VALIDATE, new Object[] { referer });
+        return false;
+      }
+    } else {
+      if (!domainList.isEmpty()) {
+        LOG.logp(Level.SEVERE, classname, "Referer check failed, referer url is not valid.",
+          MessageKeys.FAILED_TO_VALIDATE, new Object[] { referer });
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AbstractSpecFactory.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AbstractSpecFactory.java
new file mode 100644
index 0000000..d623ae4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AbstractSpecFactory.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.SoftExpiringCache;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Basis for implementing GadgetSpec and MessageBundle factories.
+ *
+ * Automatically updates objects as needed asynchronously to provide optimal throughput.
+ */
+public abstract class AbstractSpecFactory<T> {
+  //class name for logging purpose
+  private static final String classname = AbstractSpecFactory.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+  private final Class<T> clazz;
+  private final ExecutorService executor;
+  private final RequestPipeline pipeline;
+  final SoftExpiringCache<String, Object> cache;
+  private final long refresh;
+
+  /**
+   * @param clazz the class for spec objects.
+   * @param executor for asynchronously updating specs
+   * @param pipeline the request pipeline for fetching new specs
+   * @param cache a cache for parsed spec objects
+   * @param refresh the frequency at which to update specs, independent of cache expiration policy
+   */
+  public AbstractSpecFactory(Class<T> clazz, ExecutorService executor, RequestPipeline pipeline,
+      Cache<String, Object> cache, long refresh) {
+    this.clazz = clazz;
+    this.executor = executor;
+    this.pipeline = pipeline;
+    this.cache = new SoftExpiringCache<String, Object>(cache);
+    this.refresh = refresh;
+  }
+
+  /**
+   * Attempt to fetch a spec, either from cache or from the network.
+   *
+   * Note that the {@code query} passed here will always be passed, unmodified, to
+   * {@link #parse(String, Query)}. This can be used to carry additional context information
+   * during parsing.
+   */
+  protected T getSpec(Query query) throws GadgetException {
+    Object obj = null;
+    if (!query.ignoreCache) {
+      SoftExpiringCache.CachedObject<Object> cached = cache.getElement(query.specUri.toString());
+      if (cached != null) {
+        obj = cached.obj;
+        if (cached.isExpired) {
+          // We write to the cache to avoid any race conditions with multiple writers.
+          // This causes a double write, but that's better than a write per thread or synchronizing
+          // this block.
+          cache.addElement(query.specUri.toString(), obj, refresh);
+          executor.execute(new SpecUpdater(query, obj));
+        }
+      }
+    }
+
+    if (obj == null) {
+      boolean bypassCache = false;
+      try {
+        obj = fetchFromNetwork(query);
+      } catch (SpecRetrievalFailedException e) {
+        // Don't cache the resulting exception.
+        // The underlying RequestPipeline may (and should) cache non-OK HTTP responses
+        // independently, and may do so for the same spec in different ways depending
+        // on context. There's no computational benefit to caching this exception in
+        // the spec cache since we won't try to re-parse the data anyway, as we would
+        // an OK response with a faulty spec.
+        bypassCache = true;
+        obj = e;
+      } catch (GadgetException e) {
+        obj = e;
+      }
+      if (!bypassCache) {
+        cache.addElement(query.specUri.toString(), obj, refresh);
+      }
+    }
+
+    if (obj instanceof GadgetException) {
+      throw (GadgetException) obj;
+    }
+
+    // If there's a bug that puts the wrong object in here, we'll get a ClassCastException.
+    return clazz.cast(obj);
+  }
+
+  /**
+   * Retrieves a spec from the network, parses, and adds it to the cache.
+   */
+  protected T fetchFromNetwork(Query query) throws SpecRetrievalFailedException, GadgetException {
+    HttpRequest request = new HttpRequest(query.specUri)
+        .setIgnoreCache(query.ignoreCache)
+        .setGadget(query.gadgetUri)
+        .setContainer(query.container)
+        .setSecurityToken( new AnonymousSecurityToken("", 0L, query.gadgetUri.toString()));
+
+    // Since we don't allow any variance in cache time, we should just force the cache time
+    // globally. This ensures propagation to shared caches when this is set.
+    request.setCacheTtl((int) (refresh / 1000));
+
+    HttpResponse response = pipeline.execute(request);
+    if (response.getHttpStatusCode() != HttpResponse.SC_OK) {
+      int retcode = response.getHttpStatusCode();
+      if (retcode == HttpResponse.SC_INTERNAL_SERVER_ERROR) {
+        // Convert external "internal error" to gateway error:
+        retcode = HttpResponse.SC_BAD_GATEWAY;
+      }
+      throw new SpecRetrievalFailedException(query.specUri, retcode);
+    }
+
+    try {
+      String content = response.getResponseAsString();
+      return parse(content, query);
+    } catch (XmlException e) {
+      throw new SpecParserException(e);
+    }
+  }
+
+  /**
+   * Parse and return a new spec object from the network.
+   *
+   * @param content the content located at specUri
+   * @param query same as was passed {@link #getSpec(Query)}
+   */
+  protected abstract T parse(String content, Query query) throws XmlException, GadgetException;
+
+  /**
+   * Holds information used to fetch a spec.
+   */
+  protected static class Query {
+    private Uri specUri = null;
+    private String container = ContainerConfig.DEFAULT_CONTAINER;
+    private Uri gadgetUri = null;
+    private boolean ignoreCache = false;
+
+    // Expose public constructor
+    public Query() {
+    }
+
+    public Query setSpecUri(Uri specUri) {
+      this.specUri = specUri;
+      return this;
+    }
+
+    public Query setContainer(String container) {
+      this.container = container;
+      return this;
+    }
+
+    public Query setGadgetUri(Uri gadgetUri) {
+      this.gadgetUri = gadgetUri;
+      return this;
+    }
+
+    public Query setIgnoreCache(boolean ignoreCache) {
+      this.ignoreCache = ignoreCache;
+      return this;
+    }
+
+    public Uri getSpecUri() {
+      return specUri;
+    }
+
+    public String getContainer() {
+      return container;
+    }
+
+    public Uri getGadgetUri() {
+      return gadgetUri;
+    }
+
+    public boolean getIgnoreCache() {
+      return ignoreCache;
+    }
+  }
+
+  private class SpecUpdater implements Runnable {
+    private final Query query;
+    private final Object old;
+
+    public SpecUpdater(Query query, Object old) {
+      this.query = query;
+      this.old = old;
+    }
+
+    public void run() {
+      try {
+        T newSpec = fetchFromNetwork(query);
+        cache.addElement(query.specUri.toString(), newSpec, refresh);
+      } catch (SpecRetrievalFailedException se) {
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, classname, "SpecUpdater", MessageKeys.UPDATE_SPEC_FAILURE_APPLY_NEG_CACHE, new Object[] {
+              query.specUri,
+              se.getHttpStatusCode(),
+              se.getMessage()
+          });
+        }
+      } catch (GadgetException e) {
+        if (old != null) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "SpecUpdater", MessageKeys.UPDATE_SPEC_FAILURE_USE_CACHE_VERSION, new Object[] {
+                query.specUri,
+                e.getHttpStatusCode(),
+                e.getMessage()
+            });
+          }
+          cache.addElement(query.specUri.toString(), old, refresh);
+        } else {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "SpecUpdater", MessageKeys.UPDATE_SPEC_FAILURE_APPLY_NEG_CACHE, new Object[] {
+                query.specUri,
+                e.getHttpStatusCode(),
+                e.getMessage()
+            });
+          }
+          cache.addElement(query.specUri.toString(), e, refresh);
+        }
+      }
+    }
+  }
+
+  protected static class SpecRetrievalFailedException extends GadgetException {
+    public SpecRetrievalFailedException(Uri specUri, int code) {
+      super(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
+            "Unable to retrieve spec for " + specUri + ". HTTP error " + code, code);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AuthType.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AuthType.java
new file mode 100644
index 0000000..43c44ba
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/AuthType.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+/**
+ * The supported auth modes for outbound requests.
+ */
+public enum AuthType {
+  NONE, SIGNED, OAUTH, OAUTH2;
+
+  /**
+   * @return The parsed value (defaults to NONE)
+   */
+  public static AuthType parse(String value) {
+    if (value != null) {
+      value = value.trim();
+      if (value.length() == 0) {
+        return NONE;
+      }
+      try {
+        return valueOf(value.toUpperCase());
+      } catch (IllegalArgumentException iae) {
+        return NONE;
+      }
+    } else {
+      return NONE;
+    }
+  }
+
+  /**
+   * Use lowercase as toString form
+   * @return string value of Auth type
+   */
+  @Override
+  public String toString() {
+    return super.toString().toLowerCase();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java
new file mode 100644
index 0000000..ac219df
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactory.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.w3c.dom.Element;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Default implementation of a gadget spec factory.
+ */
+@Singleton
+public class DefaultGadgetSpecFactory extends AbstractSpecFactory<GadgetSpec>
+    implements GadgetSpecFactory {
+  public static final String CACHE_NAME = "gadgetSpecs";
+  public static final String RAW_GADGETSPEC_XML_PARAM_NAME = "rawxml";
+  public static final Uri RAW_GADGET_URI = Uri.parse("http://localhost/raw.xml");
+
+  @Inject
+  public DefaultGadgetSpecFactory(ExecutorService executor,
+                                  RequestPipeline pipeline,
+                                  CacheProvider cacheProvider,
+                                  @Named("shindig.cache.xml.refreshInterval") long refresh) {
+    super(GadgetSpec.class, executor, pipeline, makeCache(cacheProvider), refresh);
+  }
+
+  public static Cache<String, Object> makeCache(CacheProvider cacheProvider) {
+    return cacheProvider.createCache(CACHE_NAME);
+  }
+
+  public GadgetSpec getGadgetSpec(GadgetContext context) throws GadgetException {
+    Uri gadgetUri = getGadgetUri(context);
+    if (RAW_GADGET_URI.equals(gadgetUri)) {
+      try {
+        String rawxml = context.getParameter(RAW_GADGETSPEC_XML_PARAM_NAME);
+        return new GadgetSpec(gadgetUri, XmlUtil.parse(rawxml), rawxml);
+      } catch (XmlException e) {
+        throw new SpecParserException(e);
+      }
+    }
+
+    Query query = new Query()
+        .setSpecUri(gadgetUri)
+        .setContainer(context.getContainer())
+        .setGadgetUri(gadgetUri)
+        .setIgnoreCache(context.getIgnoreCache());
+    return getSpec(query);
+  }
+
+  public Uri getGadgetUri(GadgetContext context) throws GadgetException {
+    String rawxml = context.getParameter(RAW_GADGETSPEC_XML_PARAM_NAME);
+    if (rawxml != null) {
+      // Set URI to a fixed, safe value (localhost), preventing a gadget rendered
+      // via raw XML (eg. via POST) to be rendered on a locked domain of any other
+      // gadget whose spec is hosted non-locally.
+      return RAW_GADGET_URI;
+    }
+    return context.getUrl();
+  }
+
+  private static final String BOM_ENTITY = "&#xFEFF;";
+
+  @Override
+  protected GadgetSpec parse(String content, Query query) throws XmlException, GadgetException {
+    // Allow BOM entity as first item on stream and ignore it:
+    if (content.length() >= BOM_ENTITY.length() &&
+        content.substring(0, BOM_ENTITY.length()).equalsIgnoreCase(BOM_ENTITY)) {
+      content = content.substring(BOM_ENTITY.length());
+    }
+    Element element = XmlUtil.parse(content);
+    return new GadgetSpec(query.getSpecUri(), element, content);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java
new file mode 100644
index 0000000..4933f4a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultGuiceModule.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.apache.shindig.common.servlet.BasicAuthority;
+import org.apache.shindig.common.servlet.GuiceServletContextListener;
+import org.apache.shindig.gadgets.config.DefaultConfigContributorModule;
+import org.apache.shindig.gadgets.http.AbstractHttpCache;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.InvalidationHandler;
+import org.apache.shindig.gadgets.js.JsCompilerModule;
+import org.apache.shindig.gadgets.js.JsServingPipelineModule;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.preload.PreloadModule;
+import org.apache.shindig.gadgets.render.RenderModule;
+import org.apache.shindig.gadgets.rewrite.RewriteModule;
+import org.apache.shindig.gadgets.servlet.GadgetsHandler;
+import org.apache.shindig.gadgets.servlet.HttpRequestHandler;
+import org.apache.shindig.gadgets.templates.TemplateModule;
+import org.apache.shindig.gadgets.uri.ProxyUriBase;
+import org.apache.shindig.gadgets.uri.UriModule;
+
+import org.apache.shindig.common.servlet.Authority;
+
+import org.apache.shindig.gadgets.variables.SubstituterModule;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.*;
+
+/**
+ * Creates a module to supply all of the core gadget classes.
+ *
+ * Instead of subclassing this consider adding features to the
+ * multibindings for features and rpc handlers.
+ */
+public class DefaultGuiceModule extends AbstractModule {
+
+  /** {@inheritDoc} */
+  @Override
+  protected void configure() {
+
+    bind(ExecutorService.class).to(ShindigExecutorService.class);
+    bind(Executor.class).annotatedWith(Names.named("shindig.concat.executor")).to(ShindigExecutorService.class);
+
+    bind(Authority.class).to(BasicAuthority.class);
+
+    bindConstant().annotatedWith(Names.named("shindig.jsload.ttl-secs")).to(60 * 60); // 1 hour
+    bindConstant().annotatedWith(Names.named("shindig.jsload.require-onload-with-jsload")).to(true);
+
+    install(new DefaultConfigContributorModule());
+    install(new ParseModule());
+    install(new PreloadModule());
+    install(new RenderModule());
+    install(new RewriteModule());
+    install(new SubstituterModule());
+    install(new TemplateModule());
+    install(new UriModule());
+    install(new JsCompilerModule());
+    install(new JsServingPipelineModule());
+
+    // We perform static injection on HttpResponse for cache TTLs.
+    requestStaticInjection(HttpResponse.class);
+    requestStaticInjection(AbstractHttpCache.class);
+    requestStaticInjection(ProxyUriBase.class);
+    registerGadgetHandlers();
+    registerFeatureHandlers();
+  }
+
+  /**
+   * Sets up multibinding for rpc handlers
+   */
+  protected void registerGadgetHandlers() {
+    Multibinder<Object> handlerBinder = Multibinder.newSetBinder(binder(), Object.class,
+        Names.named("org.apache.shindig.handlers"));
+    handlerBinder.addBinding().to(InvalidationHandler.class);
+    handlerBinder.addBinding().to(HttpRequestHandler.class);
+    handlerBinder.addBinding().to(GadgetsHandler.class);
+  }
+
+  /**
+   * Sets up the multibinding for extended feature resources
+   */
+  protected void registerFeatureHandlers() {
+    /*Multibinder<String> featureBinder = */
+        Multibinder.newSetBinder(binder(), String.class, Names.named("org.apache.shindig.features-extended"));
+  }
+
+  /**
+   * Merges the features provided in shindig.properties with the extended features from multibinding
+   * @param features Comma separated string from shindig.properties key 'shindig.features.default'
+   * @param extended Set of paths/resources from plugins
+   * @return the merged, list of all features to load.
+   */
+  @Provides
+  @Singleton
+  @Named("org.apache.shindig.features")
+  protected List<String> defaultFeatures(@Named("shindig.features.default")String features,
+                                         @Named("org.apache.shindig.features-extended")Set<String> extended) {
+    return ImmutableList.<String>builder()
+        .addAll(Splitter.on(',').split(features))
+        .addAll(extended)
+        .build();
+  }
+
+  /**
+   * A thread factory that sets the daemon flag to allow for clean servlet shutdown.
+   */
+  public static final ThreadFactory DAEMON_THREAD_FACTORY =
+    new ThreadFactory() {
+        private final ThreadFactory factory = Executors.defaultThreadFactory();
+
+        public Thread newThread(Runnable r) {
+            Thread t = factory.newThread(r);
+            t.setDaemon(true);
+            return t;
+        }
+    };
+
+  /**
+   * An Executor service that mimics Executors.newCachedThreadPool(DAEMON_THREAD_FACTORY);
+   * Registers a cleanup handler to shutdown the thread.
+   */
+  @Singleton
+  public static class ShindigExecutorService extends ThreadPoolExecutor implements GuiceServletContextListener.CleanupCapable {
+    @Inject
+    public ShindigExecutorService(GuiceServletContextListener.CleanupHandler cleanupHandler) {
+      super(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
+          new SynchronousQueue<Runnable>(),
+          DAEMON_THREAD_FACTORY);
+          cleanupHandler.register(this);
+    }
+
+    public void cleanup() {
+      this.shutdown();
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java
new file mode 100644
index 0000000..89e1acf
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/DefaultMessageBundleFactory.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LocaleSpec;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import java.util.Locale;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Default implementation of a message bundle factory.
+ */
+@Singleton
+public class DefaultMessageBundleFactory extends AbstractSpecFactory<MessageBundle>
+    implements MessageBundleFactory {
+  private static final Locale ALL_ALL = new Locale("all", "ALL");
+  public static final String CACHE_NAME = "messageBundles";
+
+  @Inject
+  public DefaultMessageBundleFactory(ExecutorService executor,
+                                     RequestPipeline pipeline,
+                                     CacheProvider cacheProvider,
+                                     @Named("shindig.cache.xml.refreshInterval") long refresh) {
+    super(MessageBundle.class, executor, pipeline, makeCache(cacheProvider), refresh);
+  }
+
+  private static Cache<String, Object> makeCache(CacheProvider cacheProvider) {
+    return cacheProvider.createCache(CACHE_NAME);
+  }
+
+  @Override
+  protected MessageBundle parse(String content, Query query) throws GadgetException {
+    return new MessageBundle(((LocaleQuery) query).locale, content);
+  }
+
+  public MessageBundle getBundle(GadgetSpec spec, Locale locale, boolean ignoreCache, String container, String view)
+      throws GadgetException {
+    MessageBundle exact = getBundleFor(spec, locale, ignoreCache, container, view);
+
+    // We don't want to fetch the same bundle multiple times, so we verify that the exact match
+    // has not already been fetched.
+    MessageBundle lang, country, all;
+
+    boolean isAllLanguage = locale.getLanguage().equalsIgnoreCase("all");
+    boolean isAllCountry = locale.getCountry().equalsIgnoreCase("ALL");
+
+    if (isAllCountry) {
+      lang = MessageBundle.EMPTY;
+    } else {
+      lang = getBundleFor(spec, new Locale(locale.getLanguage(), "ALL"), ignoreCache, container, view);
+    }
+
+    if (isAllLanguage) {
+      country = MessageBundle.EMPTY;
+    } else {
+      country = getBundleFor(spec, new Locale("all", locale.getCountry()), ignoreCache, container, view);
+    }
+
+    if (isAllCountry || isAllLanguage) {
+      // If either of these is true, we already picked up both anyway.
+      all = MessageBundle.EMPTY;
+    } else {
+      all = getBundleFor(spec, ALL_ALL, ignoreCache, container, view);
+    }
+
+    return new MessageBundle(all, country, lang, exact);
+  }
+
+  private MessageBundle getBundleFor(GadgetSpec spec, Locale locale, boolean ignoreCache, String container, String view)
+      throws GadgetException {
+    LocaleSpec localeSpec = spec.getModulePrefs().getLocale(locale, view);
+    if (localeSpec == null) {
+      return MessageBundle.EMPTY;
+    }
+
+    if (localeSpec.getMessages().toString().length() == 0) {
+      return localeSpec.getMessageBundle();
+    }
+
+    LocaleQuery query = new LocaleQuery();
+    query.setSpecUri(localeSpec.getMessages())
+         .setGadgetUri(spec.getUrl())
+         .setContainer(container)
+         .setIgnoreCache(ignoreCache);
+    query.locale = localeSpec;
+
+    return super.getSpec(query);
+  }
+
+  private static class LocaleQuery extends Query {
+    // We just use this to hold the locale used in the original query so that parsing can see it.
+    LocaleSpec locale;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FeedProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FeedProcessor.java
new file mode 100644
index 0000000..7a2f514
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FeedProcessor.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.json.JSONObject;
+import com.google.inject.ImplementedBy;
+
+/**
+ * Processes RSS & Atom Feeds and converts them into JSON output.
+ */
+@ImplementedBy(FeedProcessorImpl.class)
+public interface FeedProcessor {
+
+  /**
+   * Converts feed XML to JSON.
+   *
+   * @param feedUrl
+   *            The url that the feed was retrieved from.
+   * @param feedXml
+   *            The raw XML of the feed to be converted.
+   * @param getSummaries
+   *            True if summaries should be returned.
+   * @param numEntries
+   *            Number of entries to return.
+   * @return The JSON representation of the feed.
+   */
+  JSONObject process(String feedUrl, String feedXml, boolean getSummaries, int numEntries)
+      throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FeedProcessorImpl.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FeedProcessorImpl.java
new file mode 100644
index 0000000..36c294e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FeedProcessorImpl.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import java.io.StringReader;
+import java.util.List;
+
+import com.google.common.base.Strings;
+import com.sun.syndication.feed.module.mediarss.types.UrlReference;
+import com.sun.syndication.feed.module.mediarss.MediaEntryModule;
+import com.sun.syndication.feed.module.mediarss.MediaModule;
+import com.sun.syndication.feed.module.mediarss.types.MediaContent;
+import com.sun.syndication.feed.module.mediarss.types.Thumbnail;
+import com.sun.syndication.feed.synd.SyndContent;
+import com.sun.syndication.feed.synd.SyndEntry;
+import com.sun.syndication.feed.synd.SyndFeed;
+import com.sun.syndication.feed.synd.SyndLink;
+import com.sun.syndication.feed.synd.SyndImage;
+import com.sun.syndication.feed.synd.SyndPerson;
+import com.sun.syndication.io.FeedException;
+import com.sun.syndication.io.SyndFeedInput;
+
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Processes RSS/Atom Feeds and converts them into JSON output.
+ */
+public class FeedProcessorImpl implements FeedProcessor {
+
+  /**
+   * Converts feed XML to JSON.
+   *
+   * @param feedUrl      The url that the feed was retrieved from.
+   * @param feedXml      The raw XML of the feed to be converted.
+   * @param getSummaries True if summaries should be returned.
+   * @param numEntries   Number of entries to return.
+   * @return The JSON representation of the feed.
+   */
+  //@SuppressWarnings("unchecked")
+  public JSONObject process(String feedUrl, String feedXml, boolean getSummaries, int numEntries)
+      throws GadgetException {
+    try {
+      SyndFeed feed = new SyndFeedInput().build(new StringReader(feedXml));
+      JSONObject json = new JSONObject();
+      json.put("Title", Strings.nullToEmpty(feed.getTitle()));
+      json.put("URL", feedUrl);
+      json.put("Description", Strings.nullToEmpty(feed.getDescription()));
+      json.put("Link", Strings.nullToEmpty(feed.getLink()));
+
+      //Retrieve the feed image if it is available as well as an image url if the image is available.
+      if (feed.getImage() != null && !Strings.isNullOrEmpty(feed.getImage().getUrl())) {
+        SyndImage feedImage = feed.getImage();
+        JSONObject jsonImage = new JSONObject();
+        jsonImage.put("Url", feedImage.getUrl());
+
+        if (!Strings.isNullOrEmpty(feedImage.getTitle())) {
+          jsonImage.put("Title", feedImage.getTitle());
+        }
+        if (!Strings.isNullOrEmpty(feedImage.getDescription())) {
+          jsonImage.put("Description", feedImage.getDescription());
+        }
+        if (!Strings.isNullOrEmpty(feedImage.getLink())) {
+          jsonImage.put("Link", feedImage.getLink());
+        }
+        json.put("Image", jsonImage);
+      }
+
+
+      List<SyndPerson> authors = feed.getAuthors();
+      String jsonAuthor = null;
+      if (authors != null && !authors.isEmpty()) {
+        SyndPerson author = authors.get(0);
+        if (author.getName() != null) {
+          jsonAuthor = author.getName();
+        } else if (author.getEmail() != null) {
+          jsonAuthor = author.getEmail();
+        }
+      }
+      JSONArray entries = new JSONArray();
+      json.put("Entry", entries);
+
+      int entryCnt = 0;
+      for (Object obj : feed.getEntries()) {
+        SyndEntry e = (SyndEntry) obj;
+        if (entryCnt >= numEntries) {
+          break;
+        }
+        entryCnt++;
+
+        JSONObject entry = new JSONObject();
+        entry.put("Title", e.getTitle());
+        String link = e.getLink();
+        if (link == null) {
+          List<SyndLink> links = e.getLinks();
+          if (links != null && !links.isEmpty()) {
+            link = links.get(0).getHref();
+          }
+        }
+        entry.put("Link", link);
+        if (getSummaries) {
+          if (e.getContents() != null && !e.getContents().isEmpty()) {
+            entry.put("Summary", ((SyndContent) e.getContents().get(0)).getValue());
+          } else {
+            entry.put("Summary", e.getDescription() != null ? e.getDescription().getValue() : "");
+          }
+        }
+
+        if (e.getUpdatedDate() != null) {
+          entry.put("Date", e.getUpdatedDate().getTime());
+        } else if (e.getPublishedDate() != null) {
+          entry.put("Date", e.getPublishedDate().getTime());
+        } else {
+          entry.put("Date", 0);
+        }
+
+        // if no author at feed level, use the first entry author
+        if (jsonAuthor == null) {
+          jsonAuthor = e.getAuthor();
+        }
+
+        JSONObject media = new JSONObject();
+        MediaEntryModule mediaModule = (MediaEntryModule) e.getModule(MediaModule.URI);
+        if (mediaModule != null) {
+          if (mediaModule.getMediaContents().length > 0) {
+            JSONArray contents = new JSONArray();
+
+            for (MediaContent c : mediaModule.getMediaContents()) {
+              JSONObject content = new JSONObject();
+
+              if (c.getReference() instanceof UrlReference) {
+                content.put("URL", ((UrlReference) c.getReference()).getUrl().toString());
+              }
+
+              if (c.getType() != null) {
+                content.put("Type", c.getType());
+              }
+
+              if (c.getWidth() != null) {
+                content.put("Width", c.getWidth());
+              }
+
+              if (c.getHeight() != null) {
+                content.put("Height", c.getHeight());
+              }
+
+              contents.put(content);
+            }
+
+            media.put("Contents", contents);
+          }
+
+          if (mediaModule.getMetadata() != null) {
+            if (mediaModule.getMetadata().getThumbnail().length > 0) {
+              // "If multiple thumbnails are included, it is assumed that they are in order of importance"
+              // Only use the first thumbnail for simplicity's
+              // sake
+
+              JSONObject thumbnail = new JSONObject();
+
+              Thumbnail t = mediaModule.getMetadata().getThumbnail()[0];
+              thumbnail.put("URL", t.getUrl().toString());
+
+              if (t.getWidth() != null) {
+                thumbnail.put("Width", t.getWidth());
+              }
+
+              if (t.getHeight() != null) {
+                thumbnail.put("Height", t.getHeight());
+              }
+
+              media.put("Thumbnail", thumbnail);
+            }
+          }
+        }
+
+        entry.put("Media", media);
+
+        entries.put(entry);
+      }
+
+      json.put("Author", (jsonAuthor != null) ? jsonAuthor : "");
+      return json;
+    } catch (JSONException e) {
+      // This shouldn't ever happen.
+      throw new RuntimeException(e);
+    } catch (FeedException e) {
+      throw new GadgetException(GadgetException.Code.MALFORMED_XML_DOCUMENT, e, HttpResponse.SC_BAD_GATEWAY);
+    } catch (IllegalArgumentException e) {
+      throw new GadgetException(GadgetException.Code.MALFORMED_XML_DOCUMENT, e, HttpResponse.SC_BAD_GATEWAY);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FetchResponseUtils.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FetchResponseUtils.java
new file mode 100644
index 0000000..72ed7fd
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/FetchResponseUtils.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Handles converting HttpResponse objects to the format expected by the makeRequest javascript.
+ */
+public final class FetchResponseUtils {
+  private FetchResponseUtils() {}
+  /**
+   * Convert a response to a JSON object.
+   *
+   * The returned JSON object contains the following values:
+   * id: the id of the response
+   * rc: integer response code
+   * body: string response body
+   * headers: object, keys are header names, values are lists of header values
+   *
+   * The returned object is guaranteed to be mutable.
+   *
+   * @param response the response body
+   * @param id the response id, or null if not needed
+   * @param body string to use as the body of the response.
+   * @param getFullHeaders whether all response headers should be included,
+   *     or only a small set
+   * @return a JSONObject representation of the response body.
+   */
+  public static Map<String, Object> getResponseAsJson(HttpResponse response, String id,
+      String body, boolean getFullHeaders) {
+    Map<String, Object> resp = Maps.newHashMap();
+    if (id != null) {
+      resp.put("id", id);
+    }
+    resp.put("rc", response.getHttpStatusCode());
+    resp.put("body", body);
+    Map<String, Collection<String>> headers = Maps.newHashMap();
+    if (getFullHeaders) {
+      addAllHeaders(headers, response);
+    } else {
+      addHeaders(headers, response, "set-cookie");
+      addHeaders(headers, response, "location");
+    }
+    if (!headers.isEmpty()) {
+      resp.put("headers", headers);
+    }
+    // Merge in additional response data
+    resp.putAll(response.getMetadata());
+
+    return resp;
+  }
+
+  private static void addAllHeaders(Map<String, Collection<String>> headers,
+      HttpResponse response) {
+    Multimap<String, String> responseHeaders = response.getHeaders();
+    for (String name : responseHeaders.keySet()) {
+      headers.put(name.toLowerCase(), responseHeaders.get(name));
+    }
+  }
+
+  private static void addHeaders(Map<String, Collection<String>> headers, HttpResponse response,
+      String name) {
+    Collection<String> values = response.getHeaders(name);
+    if (!values.isEmpty()) {
+      headers.put(name.toLowerCase(), values);
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java
new file mode 100644
index 0000000..5044bc7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/Gadget.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.common.util.OpenSocialVersion;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LocaleSpec;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.uri.UriCommon;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+/**
+ * Intermediary representation of all state associated with processing
+ * of a single gadget request.
+ */
+public class Gadget {
+  private FeatureRegistry featureRegistry;
+  private GadgetContext context;
+  private GadgetSpec spec;
+  private Collection<PreloadedData> preloads;
+  private View currentView;
+  private Set<String> directFeatureDeps;
+
+  /**
+   * @param context The request that the gadget is being processed for.
+   */
+  public Gadget setContext(GadgetContext context) {
+    this.context = context;
+    directFeatureDeps = null;  //New context means View may have changed
+    allGadgetFeatures = null;
+    return this;
+  }
+
+  public GadgetContext getContext() {
+    return context;
+  }
+
+  /**
+   * @param registry The gadget feature registry to use to find dependent
+   *                 features.
+   */
+  public synchronized Gadget setGadgetFeatureRegistry(FeatureRegistry registry) {
+    this.featureRegistry = registry;
+    return this;
+  }
+
+  /**
+   * @param spec The spec for the gadget that is being processed.
+   */
+  public Gadget setSpec(GadgetSpec spec) {
+    this.spec = spec;
+    return this;
+  }
+
+  public GadgetSpec getSpec() {
+    return spec;
+  }
+
+  /**
+   * Returns open social specification version for this Gadget
+   * @return Version for this Gadget
+   */
+  public OpenSocialVersion getSpecificationVersion(){
+    if(spec != null){
+      return spec.getSpecificationVersion();
+    }
+    return null;
+  }
+
+  /**
+   * Returns if the doctype attribute is set to quirksmode.
+   * Needed to override default OpenSocial 2.0 behavior which is to render in standards mode,
+   * may not be possible to honor this attribute when inlining (caja)
+   *
+   * @return TRUE if this Gadget should be rendered in browser quirks mode
+   */
+  public boolean useQuirksMode(){
+    if(spec != null){
+      return GadgetSpec.DOCTYPE_QUIRKSMODE.equals(spec.getModulePrefs().getDoctype());
+    }
+    return false;
+  }
+
+  /**
+   * @param preloads The preloads for the gadget that is being processed.
+   */
+  public Gadget setPreloads(Collection<PreloadedData> preloads) {
+    this.preloads = preloads;
+    return this;
+  }
+
+  public Collection<PreloadedData> getPreloads() {
+    return preloads;
+  }
+
+  /**
+   * List of all features this spec depends on (including all transitive
+   * dependencies).
+   */
+  private List<String> allGadgetFeatures;
+  public synchronized List<String> getAllFeatures() {
+    if (allGadgetFeatures == null) {
+      Preconditions.checkState(featureRegistry != null, "setGadgetFeatureRegistry must be called before Gadget.getAllFeatures()");
+      allGadgetFeatures = featureRegistry.getFeatures(Lists.newArrayList(getDirectFeatureDeps()));
+    }
+    return allGadgetFeatures;
+  }
+
+  public Gadget setCurrentView(View currentView) {
+    this.currentView = currentView;
+    return this;
+  }
+
+  /**
+   * @return The View applicable for the current request.
+   */
+  public View getCurrentView() {
+    return currentView;
+  }
+
+  /**
+   * Convenience function for getting the locale spec for the current context.
+   *
+   * Identical to:
+   * Locale locale = gadget.getContext().getLocale();
+   * gadget.getSpec().getModulePrefs().getLocale(locale);
+   */
+  public LocaleSpec getLocale() {
+    View view = getCurrentView();
+    String viewName = (view == null) ? GadgetSpec.DEFAULT_VIEW : view.getName();
+    return spec.getModulePrefs().getLocale(context.getLocale(), viewName);
+  }
+
+  private void initializeFeatureDeps() {
+    if (directFeatureDeps == null) {
+      directFeatureDeps = Sets.newHashSet();
+      // If we have context, lets generate the correct set of views.
+      if (context != null) {
+        directFeatureDeps.addAll(spec.getModulePrefs()
+            .getViewFeatures(context.getView()).keySet());
+      } else {
+        directFeatureDeps.addAll(spec.getModulePrefs().getFeatures().keySet());
+      }
+    }
+  }
+
+  public void addFeature(String name) {
+    initializeFeatureDeps();
+    directFeatureDeps.add(name);
+  }
+
+  public void removeFeature(String name) {
+    initializeFeatureDeps();
+    directFeatureDeps.remove(name);
+  }
+
+  public Set<String> getDirectFeatureDeps() {
+    initializeFeatureDeps();
+    return Collections.unmodifiableSet(directFeatureDeps);
+  }
+
+  /**
+   * Convenience method that returns Map of features to load for gadget's current view
+   *
+   * @return a map of ModuleSpec/Require and ModuleSpec/Optional elements to Feature
+   */
+  public Map<String, Feature> getViewFeatures() {
+    View view = getCurrentView();
+    String name = (view == null) ? GadgetSpec.DEFAULT_VIEW : view.getName();
+
+    return spec.getModulePrefs().getViewFeatures(name);
+  }
+
+  /**
+   * Should the gadget content be sanitized on output
+   * @return
+   */
+  public boolean sanitizeOutput() {
+    return (getCurrentView() != null &&
+        getCurrentView().getType() == View.ContentType.HTML_SANITIZED) ||
+        "1".equals(getContext().getParameter(UriCommon.Param.SANITIZE.getKey()));
+  }
+
+  /**
+   * True if the gadget opts into caja or the container forces caja
+   */
+  public boolean requiresCaja() {
+    if ("1".equals(
+        getContext().getParameter(UriCommon.Param.CAJOLE.getKey()))) {
+      return true;
+    }
+    if (featureRegistry != null) {
+      return getAllFeatures().contains("caja");
+    } else {
+      return getViewFeatures().containsKey("caja");
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetContext.java
new file mode 100644
index 0000000..bf5c457
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetContext.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+import java.util.Locale;
+
+/**
+ * Bundles together context data for the current request with server config data.
+ *
+ * TODO: This should probably be called "GadgetRequest" instead of GadgetContext, since it is
+ * actually serving as abstraction over different request types.
+ */
+public class GadgetContext {
+  private final GadgetContext delegate;
+
+  public GadgetContext() {
+    this(null);
+  }
+
+  public GadgetContext(GadgetContext delegate) {
+    this.delegate = delegate;
+  }
+
+  /**
+   * @param name The parameter to get data for.
+   * @return The parameter set under the given name, or null.
+   */
+  public String getParameter(String name) {
+    return delegate == null ? null : delegate.getParameter(name);
+  }
+
+  /**
+   * @return The url for this gadget.
+   */
+  public Uri getUrl() {
+    return delegate == null ? null : delegate.getUrl();
+  }
+
+  /**
+   * @return The module id for this request.
+   */
+  public long getModuleId() {
+    return delegate == null ? 0 : delegate.getModuleId();
+  }
+
+  /**
+   * @return The locale for this request.
+   */
+  public Locale getLocale() {
+    return delegate == null ? GadgetSpec.DEFAULT_LOCALE : delegate.getLocale();
+  }
+
+  /**
+   * @return The rendering context for this request.
+   */
+  public RenderingContext getRenderingContext() {
+    return delegate == null ? RenderingContext.GADGET : delegate.getRenderingContext();
+  }
+
+  /**
+   * @return Whether or not to bypass caching behavior for the current request.
+   */
+  public boolean getIgnoreCache() {
+    return delegate != null && delegate.getIgnoreCache();
+  }
+
+  /**
+   * @return The container of the current request.
+   */
+  public String getContainer() {
+    return delegate == null ? ContainerConfig.DEFAULT_CONTAINER : delegate.getContainer();
+  }
+
+  /**
+   * @return The host for which the current request is being made.
+   */
+  public String getHost() {
+    return delegate == null ? null : delegate.getHost();
+  }
+
+  /**
+   * @return The host schema for which the current request is being made.
+   */
+  public String getHostSchema() {
+    return delegate == null ? null : delegate.getHostSchema();
+  }
+
+  /**
+   * @return The IP Address for the current user.
+   */
+  public String getUserIp() {
+    return delegate == null ? null : delegate.getUserIp();
+  }
+
+  /**
+   * @return Whether or not to show debug output.
+   */
+  public boolean getDebug() {
+    return delegate != null && delegate.getDebug();
+  }
+
+  /**
+   * @return Name of view to show
+   */
+  public String getView() {
+    return delegate == null ? GadgetSpec.DEFAULT_VIEW : delegate.getView();
+  }
+
+  /**
+   * @return The user prefs for the current request.
+   */
+  public UserPrefs getUserPrefs() {
+    return delegate == null ? UserPrefs.EMPTY : delegate.getUserPrefs();
+  }
+
+  /**
+   * @return The token associated with this request
+   */
+  public SecurityToken getToken() {
+    return delegate == null ? null : delegate.getToken();
+  }
+
+  /**
+   * @return The user agent string, or null if not present.
+   */
+  public String getUserAgent() {
+    return delegate == null ? null : delegate.getUserAgent();
+  }
+
+  /**
+   * @return Whether the gadget output should be sanitized.
+   */
+  public boolean getSanitize() {
+    return delegate == null ? false : delegate.getSanitize();
+  }
+
+  /**
+   * @return Whether the gadget output should be cajoled.
+   */
+  public boolean getCajoled() {
+    return delegate == null ? false : delegate.getCajoled();
+  }
+
+  /**
+   * @return return the feature js repository if available
+   */
+  public String getRepository() {
+    return delegate == null ? null : delegate.getRepository();
+  }
+
+  public String getReferer() {
+      return delegate == null ? null : delegate.getReferer();
+  }
+}
+
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetELResolver.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetELResolver.java
new file mode 100644
index 0000000..39a5b4f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetELResolver.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.PropertyNotWritableException;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * ELResolver for the built-in gadget properties:
+ * - UserPrefs: the user preferences
+ * - ViewParams: view params (as a JSON object)
+ */
+public class GadgetELResolver extends ELResolver {
+  public static final String USER_PREFS_PROPERTY = "UserPrefs";
+  public static final String VIEW_PARAMS_PROPERTY = "ViewParams";
+
+  private final GadgetContext gadgetContext;
+
+  public GadgetELResolver(GadgetContext context) {
+    this.gadgetContext = context;
+  }
+
+  @Override
+  public Class<?> getCommonPropertyType(ELContext context, Object base) {
+    if (base == null) {
+      return String.class;
+    }
+
+    return null;
+  }
+
+  @Override
+  public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+      Object base) {
+    return null;
+  }
+
+  @Override
+  public Class<?> getType(ELContext context, Object base, Object property) {
+    if (base == null) {
+      if (VIEW_PARAMS_PROPERTY.equals(property)) {
+        context.setPropertyResolved(true);
+        return Object.class;
+      } else if (USER_PREFS_PROPERTY.equals(property)) {
+        context.setPropertyResolved(true);
+        return Map.class;
+      }
+    }
+
+    return null;
+  }
+
+  @Override
+  public Object getValue(ELContext context, Object base, Object property) {
+    if (base == null) {
+      if (VIEW_PARAMS_PROPERTY.equals(property)) {
+        context.setPropertyResolved(true);
+        String params = gadgetContext.getParameter("view-params");
+        if (params != null && !"".equals(params)) {
+          try {
+            // TODO: immutable?
+            return new JSONObject(params);
+          } catch (JSONException e) {
+            throw new ELException(e);
+          }
+        }
+
+        // Return an empty map - this doesn't allocate anything, whereas an
+        // empty JSONObject would
+        return ImmutableMap.of();
+      } else if (USER_PREFS_PROPERTY.equals(property)) {
+        context.setPropertyResolved(true);
+        // TODO: immutable?
+        return gadgetContext.getUserPrefs().getPrefs();
+      }
+    }
+
+    return null;
+  }
+
+  @Override
+  public boolean isReadOnly(ELContext context, Object base, Object property) {
+    if ((base == null) &&
+        (VIEW_PARAMS_PROPERTY.equals(property)
+        || USER_PREFS_PROPERTY.equals(property))) {
+      context.setPropertyResolved(true);
+    }
+
+    return true;
+  }
+
+  @Override
+  public void setValue(ELContext context, Object base, Object property, Object value) {
+    if ((base == null) &&
+        (VIEW_PARAMS_PROPERTY.equals(property)
+        || USER_PREFS_PROPERTY.equals(property))) {
+      throw new PropertyNotWritableException("Cannot override " + property);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java
new file mode 100644
index 0000000..0bf7904
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetException.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+/**
+ * Base class for all Gadget exceptions. The bulk of the code uses
+ * this class directly, differentiating between error conditions by
+ * the Code enumeration.
+ */
+public class GadgetException extends Exception {
+  public enum Code {
+    // Catch-all for internal errors
+    INTERNAL_SERVER_ERROR,
+
+    // Configuration errors
+    INVALID_PATH,
+    INVALID_CONFIG,
+
+    // User-data related errors.
+    INVALID_USER_DATA,
+    INVALID_SECURITY_TOKEN,
+
+    // General xml
+    EMPTY_XML_DOCUMENT,
+    MALFORMED_XML_DOCUMENT,
+
+    // HTTP errors
+    FAILED_TO_RETRIEVE_CONTENT,
+
+    // Feature-related errors
+    UNSUPPORTED_FEATURE,
+
+    // General error, should be accompanied by message
+    INVALID_PARAMETER,
+    MISSING_PARAMETER,
+    UNRECOGNIZED_PARAMETER,
+    POST_TOO_LARGE,
+
+    // Interface component errors.
+    MISSING_FEATURE_REGISTRY,
+    MISSING_MESSAGE_BUNDLE_CACHE,
+    MISSING_REMOTE_OBJECT_FETCHER,
+    MISSING_SPEC_CACHE,
+
+    // Caja error
+    MALFORMED_FOR_SAFE_INLINING,
+
+    // Parsing errors
+    CSS_PARSE_ERROR,
+    HTML_PARSE_ERROR,
+    IMAGE_PARSE_ERROR,
+    JS_PARSE_ERROR,
+
+    // View errors
+    UNKNOWN_VIEW_SPECIFIED,
+
+    // Blacklisting
+    NON_WHITELISTED_GADGET,
+
+    // OAuth
+    OAUTH_STORAGE_ERROR,
+    OAUTH_APPROVAL_NEEDED,
+
+    // Signed fetch
+    REQUEST_SIGNING_FAILURE,
+
+    // Error in the JavaScript processing pipeline
+    JS_PROCESSING_ERROR,
+
+    // Error validating that the gadget supplied is correct for the locked domain the request came from.
+    GADGET_HOST_MISMATCH,
+
+    //Gadget Admin Error
+    GADGET_ADMIN_STORAGE_ERROR,
+    GADGET_ADMIN_FEATURE_NOT_ALLOWED
+  }
+
+  private final Code code;
+  private final int httpStatusCode;
+
+  public GadgetException(Code code, int httpStatusCode) {
+    this.code = code;
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public GadgetException(Code code, Throwable cause, int httpStatusCode) {
+    super(cause);
+    this.code = code;
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public GadgetException(Code code, String msg, Throwable cause, int httpStatusCode) {
+    super(msg, cause);
+    this.code = code;
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public GadgetException(Code code, String msg, int httpStatusCode) {
+    super(msg);
+    this.code = code;
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public GadgetException(Code code) {
+    this(code, HttpResponse.SC_INTERNAL_SERVER_ERROR);
+  }
+
+  public GadgetException(Code code, Throwable cause) {
+    this(code, cause, HttpResponse.SC_INTERNAL_SERVER_ERROR);
+  }
+
+  public GadgetException(Code code, String msg, Throwable cause) {
+    this(code, msg, cause, HttpResponse.SC_INTERNAL_SERVER_ERROR);
+  }
+
+  public GadgetException(Code code, String msg) {
+    this(code, msg, HttpResponse.SC_INTERNAL_SERVER_ERROR);
+  }
+
+  public Code getCode() {
+    return code;
+  }
+
+  public int getHttpStatusCode() {
+    return httpStatusCode;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java
new file mode 100644
index 0000000..0c634cc
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/GadgetSpecFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.common.uri.Uri;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Factory of gadget specs.
+ */
+@ImplementedBy(DefaultGadgetSpecFactory.class)
+public interface GadgetSpecFactory {
+
+  /** Return a gadget spec for a context */
+  GadgetSpec getGadgetSpec(GadgetContext context) throws GadgetException;
+
+  Uri getGadgetUri(GadgetContext context) throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java
new file mode 100644
index 0000000..fd5b125
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/HashLockedDomainService.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.Uri.UriException;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.uri.LockedDomainPrefixGenerator;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+/**
+ * Locked domain implementation based on sha1.
+ *
+ * The generated domain takes the form:
+ *
+ * base32(sha1(gadget url)).
+ *
+ * Other domain locking schemes are possible as well.
+ */
+@Singleton
+public class HashLockedDomainService extends AbstractLockedDomainService {
+
+  /**
+   * Used to observer locked domain suffixes for this class
+   */
+  private class HashLockedDomainObserver implements ContainerConfig.ConfigObserver {
+
+    public void containersChanged(ContainerConfig config, Collection<String> changed,
+            Collection<String> removed) {
+      for (String container : changed) {
+        String suffix = config.getString(container, LOCKED_DOMAIN_SUFFIX_KEY);
+        if (suffix == null) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "containersChanged",
+                    MessageKeys.NO_LOCKED_DOMAIN_CONFIG, new Object[] { container });
+          }
+        } else {
+          HashLockedDomainService.this.lockedSuffixes.put(container, checkSuffix(suffix));
+        }
+      }
+      for (String container : removed) {
+        HashLockedDomainService.this.lockedSuffixes.remove(container);
+      }
+    }
+  }
+
+  // class name for logging purpose
+  private static final String classname = HashLockedDomainService.class.getName();
+
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+  private final Map<String, String> lockedSuffixes;
+  private Authority authority;
+  private LockedDomainPrefixGenerator ldGen;
+  private final Pattern authpattern = Pattern.compile("%authority%");
+
+  private HashLockedDomainObserver ldObserver;
+
+  public static final String LOCKED_DOMAIN_SUFFIX_KEY = "gadgets.uri.iframe.lockedDomainSuffix";
+
+  /*
+   * Injected methods
+   */
+
+  /**
+   * Create a LockedDomainService
+   *
+   * @param config
+   *          per-container configuration
+   * @param enabled
+   *          whether this service should do anything at all.
+   */
+  @Inject
+  public HashLockedDomainService(ContainerConfig config,
+          @Named("shindig.locked-domain.enabled") boolean enabled, LockedDomainPrefixGenerator ldGen) {
+    super(config, enabled);
+    this.lockedSuffixes = Maps.newHashMap();
+    this.ldGen = ldGen;
+    if (enabled) {
+      this.ldObserver = new HashLockedDomainObserver();
+      config.addConfigObserver(this.ldObserver, true);
+    }
+  }
+
+  @Override
+  public String getLockedDomainForGadget(Gadget gadget, String container) throws GadgetException {
+    container = getContainer(container);
+    if (isEnabled() && !isExcludedFromLockedDomain(gadget, container)) {
+      if (isGadgetReqestingLocking(gadget) || isDomainLockingEnforced(container)) {
+        return getLockedDomain(gadget, container);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Generates a locked domain prefix given a gadget Uri.
+   *
+   * @param gadget The uri of the gadget.
+   * @return A locked domain prefix for the gadgetUri.
+   *         Returns empty string if locked domains are not enabled on the server.
+   */
+  private String getLockedDomainPrefix(Gadget gadget) throws GadgetException {
+    String ret = "";
+    if (isEnabled()) {
+      ret = this.ldGen.getLockedDomainPrefix(getLockedDomainParticipants(gadget));
+    }
+    // Lower-case to prevent casing from being relevant.
+    return ret.toLowerCase();
+  }
+
+  @Override
+  public boolean isGadgetValidForHost(String host, Gadget gadget, String container) {
+    container = getContainer(container);
+    if (isEnabled()) {
+      if (isGadgetReqestingLocking(gadget) || isHostUsingLockedDomain(host)
+              || isDomainLockingEnforced(container)) {
+        if (isRefererCheckEnabled() && !isValidReferer(gadget, container)) {
+          return false;
+        }
+        String neededHost;
+        try {
+          neededHost = getLockedDomain(gadget, container);
+        } catch (GadgetException e) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.log(Level.WARNING, "Invalid host for call.", e);
+          }
+          return false;
+        }
+        return host.equalsIgnoreCase(neededHost);
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public boolean isHostUsingLockedDomain(String host) {
+    if (isEnabled()) {
+      for (String suffix : this.lockedSuffixes.values()) {
+        if (host.toLowerCase().endsWith(suffix.toLowerCase())) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  @Inject(optional = true)
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  private String checkSuffix(String suffix) {
+    if (suffix != null) {
+      Matcher m = this.authpattern.matcher(suffix);
+      if (m.matches()) {
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.warning("You should not be using %authority% replacement in a running environment!");
+          LOG.warning("Check your config and specify an explicit locked domain suffix.");
+          LOG.warning("Found suffix: " + suffix);
+        }
+        if (this.authority != null) {
+          suffix = m.replaceAll(this.authority.getAuthority());
+        }
+      }
+    }
+    return suffix;
+  }
+
+  private String getContainer(String container) {
+    if (this.required.containsKey(container)) {
+      return container;
+    }
+    return ContainerConfig.DEFAULT_CONTAINER;
+  }
+
+  private String getLockedDomain(Gadget gadget, String container) throws GadgetException {
+    String suffix = this.lockedSuffixes.get(container);
+    if (suffix == null) {
+      return null;
+    }
+    return getLockedDomainPrefix(gadget) + suffix;
+  }
+
+  private String getLockedDomainParticipants(Gadget gadget) throws GadgetException {
+    Map<String, Feature> features = gadget.getSpec().getModulePrefs().getFeatures();
+    Feature ldFeature = features.get("locked-domain");
+
+    // This gadget is always a participant.
+    Set<String> filtered = new TreeSet<String>();
+    filtered.add(gadget.getSpec().getUrl().toString().toLowerCase());
+
+    if (ldFeature != null) {
+      Collection<String> participants = ldFeature.getParamCollection("participant");
+      for (String participant : participants) {
+        // be picky, this should be a valid uri
+        try {
+          Uri.parse(participant);
+        } catch (UriException e) {
+          throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+                  "Participant param must be a valid uri", e);
+        }
+        filtered.add(participant.toLowerCase());
+      }
+    }
+
+    StringBuilder buffer = new StringBuilder();
+    for (String participant : filtered) {
+      buffer.append(participant);
+    }
+    return buffer.toString();
+  }
+
+  @VisibleForTesting
+  ContainerConfig.ConfigObserver getConfigObserver() {
+    return this.ldObserver;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/JsCompileMode.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/JsCompileMode.java
new file mode 100644
index 0000000..34a11e5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/JsCompileMode.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+public enum JsCompileMode {
+  // Concats all build-time compile-version JS.
+  COMPILE_CONCAT("0"),
+  // Performs run-time compilation of concatenated built-time debug-version JS.
+  // All symbols exported (and as long as in transitive dependency) are
+  // retained/unbofuscated.
+  CONCAT_COMPILE_EXPORT_ALL("1"),
+  // Like run CONCAT_COMPILE_EXPORT_ALL, except the only retained/unobfuscated
+  // symbols are ones exported from the explicitly-requested features, ie: if
+  // feature=foo depends on feature=bar, /gadgets/js/foo will expose foo.*
+  // exported APIs, not bar.* exported APIs. This can potentially eliminate all
+  // un-used code.
+  CONCAT_COMPILE_EXPORT_EXPLICIT("2");
+
+  private final String paramValue;
+
+  private JsCompileMode(String paramValue) {
+    this.paramValue = paramValue;
+  }
+
+  public String getParamValue() {
+    return paramValue;
+  }
+
+  public static JsCompileMode valueOfParam(String param) {
+    for (JsCompileMode mode : JsCompileMode.values()) {
+      String modeParam = mode.getParamValue();
+      if (modeParam.equals(param)) {
+        return mode;
+      }
+    }
+    return getDefault();
+  }
+
+  public static JsCompileMode getDefault() {
+    return JsCompileMode.COMPILE_CONCAT;
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java
new file mode 100644
index 0000000..cdf73c1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/LockedDomainService.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Interface for locked domain, a security mechanism that ensures that
+ * a gadget is always registered on a fixed, unique domain. This prevents
+ * attacks from other gadgets that are rendered on the same domain, since all
+ * modern web browsers implement a same origin policy that prevents pages served
+ * from different hosts from accessing each other's data.
+ */
+@ImplementedBy(HashLockedDomainService.class)
+public interface LockedDomainService {
+  /**
+   * Check whether locked domains feature is enabled on the server.
+   *
+   * @return If locked domains is enabled on the server.
+   */
+  boolean isEnabled();
+
+  /**
+   * @return True if the host is safe for use with the open proxy.
+   */
+  boolean isSafeForOpenProxy(String host);
+
+  /**
+   * Check whether a gadget should be allowed to render on a particular
+   * host.
+   *
+   * @param host host name for the content
+   * @param gadget URL of the gadget
+   * @param container container
+   * @return true if the gadget can render
+   */
+  boolean isGadgetValidForHost(String host, Gadget gadget, String container);
+
+  /**
+   * Calculate the locked domain for a particular gadget on a particular
+   * container.
+   *
+   * @param gadget URL of the gadget
+   * @param container name of the container page
+   * @return the host name on which the gadget should render, or null if locked domain should not
+   * be used to render this gadget.
+   */
+  String getLockedDomainForGadget(Gadget gadget, String container) throws GadgetException;
+
+  /**
+   * Check whether a host is using a locked domain.
+   *
+   * @param host Host to inspect for locked domain suffix.
+   * @return If the supplied host is using a locked domain.
+   *         Returns false if locked domains are not enabled on the server.
+   */
+  boolean isHostUsingLockedDomain(String host);
+
+  /**
+   * @return If referrer check is enabled, return true. Otherwise, return false.
+   */
+  boolean isRefererCheckEnabled();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleFactory.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleFactory.java
new file mode 100644
index 0000000..cf32398
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/MessageBundleFactory.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.Locale;
+
+/**
+ * Factory of message bundles
+ */
+@ImplementedBy(DefaultMessageBundleFactory.class)
+public interface MessageBundleFactory {
+  /**
+   * Retrieves a messagMessageBundle for the provided GadgetSpec and Locale. Implementations must be
+   * sure to perform proper merging of message bundles of lower specifity with exact matches
+   * (exact &gt; lang only &gt; country only &gt; all / all)
+   *
+   * @param spec The gadget to inspect for Locales.
+   * @param locale The language and country to get a message bundle for.
+   * @param ignoreCache  True to bypass any caching of message bundles for debugging purposes.
+   * @param container The container that is requesting this message bundle
+   * @param view The view for which to return the Locale appropriate message bundle.  To retrieve only globally scoped bundles pass 'null'.
+   * @return The newly created MesageBundle.
+   * @throws GadgetException if retrieval fails for any reason.
+   */
+  MessageBundle getBundle(GadgetSpec spec, Locale locale, boolean ignoreCache, String container, String view)
+      throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RenderingContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RenderingContext.java
new file mode 100644
index 0000000..eedcd5a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/RenderingContext.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+/**
+ * Defines where the gadget is being rendered.
+ */
+public enum RenderingContext {
+  // Used when rendering gadgets of type=html|inline. gadgets.config.init is not
+  // injected into the gadget render, and container mediated.
+  // TODO: rename this to "RENDER_GADGET"?
+  GADGET("gadget", "0", false),
+
+  // Used when rendering gadgets of type=url. Unlike RenderingContext.GADGET,
+  // this special context is explicitly requested by the gadget (to include
+  // gadgets.config.init), while still considered a gadget render.
+  CONFIGURED_GADGET("gadget", "2", true),
+
+  // Used when rendering container data (not a gadget render)
+  CONTAINER("container", "1", true),
+
+  // Used when retrieving metadata about a gadget. Processing is generally
+  // identical to processing under GADGET, but some operations may be safely
+  // skipped, such as preload processing.
+  METADATA("gadget", null, null),
+
+  // Allows specification of feature JS with an <all> tag. Specially handled in
+  // FeatureRegistry: content specified in an <all> tag is chosen if there are
+  // no <gadget> or <container> sections. This avoids, for many libs where the JS
+  // is equivalent, copying sections all over the place.
+  ALL("all", "3", true);
+
+  private final String featureBundleTag;
+  private final String paramValue;
+  private final Boolean configurable;
+
+  private RenderingContext(String featureXmlTag, String paramValue, Boolean configurable) {
+    this.featureBundleTag = featureXmlTag;
+    this.paramValue = paramValue;
+    this.configurable = configurable;
+  }
+
+  public String getFeatureBundleTag() {
+    return featureBundleTag;
+  }
+
+  public boolean isConfigurable() {
+    return configurable;
+  }
+
+  public String getParamValue() {
+    return paramValue;
+  }
+
+  public static RenderingContext valueOfParam(String param) {
+    // Exception: when no &c= parameter provided or bad, default to GADGET.
+    if (param != null) {
+      for (RenderingContext rc : RenderingContext.values()) {
+        String rcParam = rc.getParamValue();
+        if (rcParam != null && rcParam.equals(param)) {
+          return rc;
+        }
+      }
+    }
+    return getDefault();
+  }
+
+  public static RenderingContext getDefault() {
+    return RenderingContext.GADGET;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/UnsupportedFeatureException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/UnsupportedFeatureException.java
new file mode 100644
index 0000000..1b34dd0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/UnsupportedFeatureException.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+
+/**
+ * Thrown whenever GadgetFeatureRegistry gets a request for a feature that is
+ * not registered.
+ */
+public class UnsupportedFeatureException extends GadgetException {
+  public UnsupportedFeatureException(String name) {
+    super(GadgetException.Code.UNSUPPORTED_FEATURE,
+        "Unsupported feature: " + name);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/UserPrefs.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/UserPrefs.java
new file mode 100644
index 0000000..907e783
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/UserPrefs.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Data structure representing gadget user preferences.
+ */
+public class UserPrefs {
+  /**
+   * Convenience representation of an empty pref set.
+   */
+  public static final UserPrefs EMPTY = new UserPrefs();
+  private final Map<String, String> prefs;
+
+  /**
+   * @return An immutable reference to all prefs.
+   */
+  public Map<String, String> getPrefs() {
+    return prefs;
+  }
+
+  /**
+   * @param name The pref to fetch.
+   * @return The pref specified by the given name.
+   */
+  public String getPref(String name) {
+    return prefs.get(name);
+  }
+
+  @Override
+  public String toString() {
+    return prefs.toString();
+  }
+
+  /**
+   * @param prefs The preferences to populate.
+   */
+  public UserPrefs(Map<String, String> prefs) {
+    this.prefs = ImmutableMap.copyOf(prefs);
+  }
+
+  /**
+   * Creates an empty user prefs object.
+   */
+  private UserPrefs() {
+    // just use the empty map.
+    this.prefs = Collections.emptyMap();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/BasicGadgetAdminStore.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/BasicGadgetAdminStore.java
new file mode 100644
index 0000000..8499090
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/BasicGadgetAdminStore.java
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.admin.FeatureAdminData.Type;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.caja.util.Sets;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * A simple implementation of a gadget administration store.
+ *
+ * @since 2.5.0
+ */
+@Singleton
+public class BasicGadgetAdminStore implements GadgetAdminStore {
+
+  // Key in the container config which indicated whether white-listing is turned on.
+  private static final String WHITELIST_KEY = "gadgets.admin.enableGadgetWhitelist";
+
+  // Key in the container config which indicates whether feature administration is turned on.
+  private static final String ENABLE_FEATURE_ADMIN = "gadgets.admin.enableFeatureAdministration";
+
+  private static final Logger LOG = Logger.getLogger(BasicGadgetAdminStore.class.getName());
+
+  private static final String GADGETS = "gadgets";
+  private static final String FEATURES = "features";
+  private static final String FEATURES_NAMES = "names";
+  private static final String TYPE = "type";
+  private static final String RPC = "rpc";
+  private static final String ADDITIONAL_RPC_SERVICE_IDS = "additionalServiceIds";
+  private static final String BLACKLIST = "blacklist";
+  private static final String CORE_FEATURE = "core";
+
+  private ServerAdminData serverAdminData;
+  private FeatureRegistryProvider featureRegistryProvider;
+  private ContainerConfig config;
+
+  /**
+   * Constructor.
+   */
+  @Inject
+  public BasicGadgetAdminStore(FeatureRegistryProvider featureRegistryProvider,
+          ContainerConfig config, ServerAdminData serverAdminData) {
+    this.serverAdminData = serverAdminData;
+    this.featureRegistryProvider = featureRegistryProvider;
+    this.config = config;
+  }
+
+  /**
+   * Inits the store from a JSON String representing the gadget administration information.
+   *
+   * @param store
+   *          a JSON String representing the gadget administration information.
+   * @throws GadgetException thrown when the store cannot be initiated.
+   */
+  public void init(String store) throws GadgetException {
+    try {
+      JSONObject json = new JSONObject(store);
+      Iterator<?> iter = json.keys();
+      String container;
+      while (iter.hasNext()) {
+        container = (String) iter.next();
+        serverAdminData.addContainerAdminData(container,
+                createContainerData(container, json.getJSONObject(container)));
+      }
+    } catch (JSONException e) {
+      throw new GadgetException(GadgetException.Code.GADGET_ADMIN_STORAGE_ERROR, e);
+    }
+  }
+
+  /**
+   * Creates container security information from a JSON object.
+   *
+   * @param container
+   *          the container the security information is for.
+   * @param containerJson
+   *          the JSON object representing the information.
+   * @return container admin data
+   * @throws JSONException
+   *           thrown when we cannot get the information from the JSON object
+   */
+  private ContainerAdminData createContainerData(String container, JSONObject containerJson)
+          throws JSONException, GadgetException {
+    ContainerAdminData containerData = new ContainerAdminData();
+    if (containerJson.has(GADGETS)) {
+      containerData = new ContainerAdminData(
+              createGadgetAdminDataMap(containerJson.getJSONObject(GADGETS)));
+    }
+    return containerData;
+  }
+
+  /**
+   * Creates an RpcAdminData object from a JSON object.
+   *
+   * @param rpcJson
+   *          the JSON object representing the RPC admin data.
+   * @return an RpcAdminData object.
+   * @throws JSONException thrown when the RpcAdminData object cannot be created.
+   */
+  private RpcAdminData createRpcAdminData(JSONObject rpcJson) throws JSONException {
+    RpcAdminData adminData = new RpcAdminData();
+    if(rpcJson.has(ADDITIONAL_RPC_SERVICE_IDS)) {
+      JSONArray ids = rpcJson.getJSONArray(ADDITIONAL_RPC_SERVICE_IDS);
+      for(int i = 0; i < ids.length(); i++) {
+        adminData.addAdditionalRpcServiceId(ids.getString(i));
+      }
+    }
+    return adminData;
+  }
+
+  /**
+   * Creates a map of gadget administration data.
+   *
+   * @param gadgetsJson
+   *          the JSON object representing the admin data.
+   * @return a map of gadget administration data.
+   * @throws JSONException
+   *           thrown when the map cannot be created.
+   */
+  private Map<String, GadgetAdminData> createGadgetAdminDataMap(JSONObject gadgetsJson)
+          throws JSONException {
+    Map<String, GadgetAdminData> map = Maps.newHashMap();
+    Iterator<?> keys = gadgetsJson.keys();
+    String gadgetUrl;
+    JSONObject gadgetJson;
+    while (keys.hasNext()) {
+      gadgetUrl = (String) keys.next();
+      gadgetJson = gadgetsJson.getJSONObject(gadgetUrl);
+      map.put(gadgetUrl, createGadgetAdminData(gadgetJson));
+    }
+    return map;
+  }
+
+  /**
+   * Creates a gadget administration data.
+   *
+   * @param gadgetJson
+   *          the gadget JSON object.
+   * @return gadget administration data.
+   * @throws JSONException
+   *           thrown when the information cannot found in the JSON object.
+   */
+  private GadgetAdminData createGadgetAdminData(JSONObject gadgetJson) throws JSONException {
+    FeatureAdminData featureData = new FeatureAdminData();
+    RpcAdminData rpcData = new RpcAdminData();
+    if(gadgetJson.has(FEATURES)) {
+      featureData = createFeatureAdminData(gadgetJson.getJSONObject(FEATURES));
+    }
+    if(gadgetJson.has(RPC)) {
+      rpcData = createRpcAdminData(gadgetJson.getJSONObject(RPC));
+    }
+    return new GadgetAdminData(featureData, rpcData);
+  }
+
+  /**
+   * Creates the feature admin data.
+   *
+   * @param featuresJson
+   *          The JSON object representing the feature admin data.
+   * @return Feature admin data.
+   * @throws JSONException Thrown when the JSON cannot be parsed.
+   */
+  private FeatureAdminData createFeatureAdminData(JSONObject featuresJson) throws JSONException {
+    FeatureAdminData data = new FeatureAdminData();
+    if (featuresJson.has(FEATURES_NAMES)) {
+      JSONArray features = featuresJson.getJSONArray(FEATURES_NAMES);
+      for (int i = 0; i < features.length(); i++) {
+        data.addFeature(features.getString(i));
+      }
+    }
+
+    data.setType(Type.WHITELIST);
+    if (!data.getFeatures().contains(CORE_FEATURE)) {
+      // Add the core feature since every gadget needs this and it can't be disabled
+      data.addFeature(CORE_FEATURE);
+    }
+    if (featuresJson.has(TYPE)) {
+      String type = featuresJson.getString(TYPE);
+      if (type.equalsIgnoreCase(BLACKLIST)) {
+        data.setType(Type.BLACKLIST);
+        //We need core for everything so remove it if it is blacklisted
+        data.removeFeature(CORE_FEATURE);
+      }
+    }
+    return data;
+  }
+
+  public GadgetAdminData getGadgetAdminData(String container, String gadgetUrl) {
+    GadgetAdminData data = null;
+    if (serverAdminData.hasContainerAdminData(container)) {
+      ContainerAdminData containerData = serverAdminData.getContainerAdminData(container);
+      if (containerData.hasGadgetAdminData(gadgetUrl)) {
+        data = containerData.getGadgetAdminData(gadgetUrl);
+      }
+    }
+    return data;
+  }
+
+  public void setGadgetAdminData(String container, String gadgetUrl, GadgetAdminData adminData) {
+    if (serverAdminData.hasContainerAdminData(container)) {
+      ContainerAdminData containerData = serverAdminData.getContainerAdminData(container);
+      containerData.addGadgetAdminData(gadgetUrl, adminData);
+    }
+  }
+
+  public ContainerAdminData getContainerAdminData(String container) {
+    ContainerAdminData data = null;
+    if (serverAdminData.hasContainerAdminData(container)) {
+      data = serverAdminData.getContainerAdminData(container);
+    }
+    return data;
+  }
+
+  public void setContainerAdminData(String container, ContainerAdminData containerAdminData) {
+    serverAdminData.addContainerAdminData(container, containerAdminData);
+  }
+
+  public ServerAdminData getServerAdminData() {
+    return serverAdminData;
+  }
+
+  /**
+   * Safely gets the container from the gadget by doing null checks.
+   *
+   * @param gadget
+   *          The gadget to get the container from.
+   * @return The container.
+   */
+  private String getSafeContainerFromGadget(Gadget gadget) {
+    GadgetContext context = gadget.getContext();
+    if (context != null) {
+      return context.getContainer();
+    }
+    return null;
+  }
+
+  /**
+   * Safely gets the gadget's URL from the gadget by doing null checks.
+   *
+   * @param gadget
+   *          The gadget to get the URL from.
+   * @return The gadget's URL.
+   */
+  private String getSafeGadgetUrlFromGadget(Gadget gadget) {
+    GadgetSpec spec = gadget.getSpec();
+    if (spec != null) {
+      Uri gadgetUri = spec.getUrl();
+      if (gadgetUri != null) {
+        return gadgetUri.toString();
+      }
+    }
+    return null;
+  }
+
+  public boolean checkFeatureAdminInfo(Gadget gadget) {
+    String container = getSafeContainerFromGadget(gadget);
+    String gadgetUrl = getSafeGadgetUrlFromGadget(gadget);
+    if (container == null || gadgetUrl == null) {
+      return false;
+    }
+
+    if (!isFeatureAdminEnabled(container)) {
+      return true;
+    }
+
+    GadgetContext context = gadget.getContext();
+    try {
+      FeatureRegistry featureRegistry = featureRegistryProvider.get(context.getRepository());
+      if (!hasGadgetAdminData(container, gadgetUrl)) {
+        return false;
+      }
+
+      FeatureAdminData featureAdminData = this.getGadgetAdminData(container, gadgetUrl)
+              .getFeatureAdminData();
+
+      Set<String> features = featureAdminData.getFeatures();
+      if(featureAdminData.getType() == Type.WHITELIST) {
+        //If the admin has specified a whitelist get all the dependencies for the features the admin
+        //has whitelisted and add them as well.  Blacklists need to be more specific.
+        features = Sets.newHashSet(featureRegistry.getFeatures(features));
+      }
+
+      List<String> gadgetFeatures = featureRegistry.getFeatures(getRequiredGadgetFeatures(gadget));
+
+      return areAllFeaturesAllowed(Sets.immutableSet(features),
+              gadgetFeatures, featureAdminData);
+    } catch (GadgetException e) {
+      LOG.log(Level.WARNING, "Exception while getting the FeatureRegistry.");
+      return false;
+    }
+
+  }
+
+  /**
+   * Gets all required gadget features.
+   *
+   * @param gadget
+   *          The gadget to get the gadget features for.
+   * @return The required gadget features.
+   */
+  private List<String> getRequiredGadgetFeatures(Gadget gadget) {
+    List<String> featureNames = Lists.newArrayList();
+    List<Feature> features = gadget.getSpec().getModulePrefs().getAllFeatures();
+    for (Feature feature : features) {
+      if (feature.getRequired()) {
+        featureNames.add(feature.getName());
+      }
+    }
+    return featureNames;
+  }
+
+  /**
+   * Checks the features for a gadget to see if they are allowed.
+   *
+   * @param featuresForGadget
+   *          a set of features that the admin has either whitelist or blacklisted.
+   * @param gadgetFeatures
+   *          a list of features required by the gadget.
+   * @param featureAdminData
+   *          the feature admin data for the gadget.
+   * @return true if all the features for the gadget are allowed, false otherwise.
+   */
+  private boolean areAllFeaturesAllowed(Set<String> featuresForGadget, List<String> gadgetFeatures,
+          FeatureAdminData featureAdminData) {
+    switch (featureAdminData.getType()) {
+    case BLACKLIST:
+      for (String feature : gadgetFeatures) {
+        if (featuresForGadget.contains(feature)) {
+          return false;
+        }
+      }
+
+      break;
+    case WHITELIST:
+    default:
+      return featuresForGadget.containsAll(gadgetFeatures);
+    }
+    return true;
+  }
+
+  public boolean isAllowedFeature(Feature feature, Gadget gadget) {
+    String container = getSafeContainerFromGadget(gadget);
+    String gadgetUrl = getSafeGadgetUrlFromGadget(gadget);
+    if (container == null || gadgetUrl == null) {
+      return false;
+    }
+    if (!isFeatureAdminEnabled(container)) {
+      return true;
+    }
+    if (!hasGadgetAdminData(container, gadgetUrl)) {
+      // If feature administration is not enabled assume the feature is allowed
+      return false;
+    }
+    GadgetAdminData gadgetAdminData = getGadgetAdminData(container, gadgetUrl);
+
+    FeatureAdminData featureAdminData = gadgetAdminData.getFeatureAdminData();
+    String featureName = feature.getName();
+    switch (featureAdminData.getType()) {
+    case BLACKLIST:
+      return !featureAdminData.getFeatures().contains(featureName);
+    case WHITELIST:
+    default:
+      return featureAdminData.getFeatures().contains(featureName);
+    }
+  }
+
+  /**
+   * Determines whether we have gadget administration data for a gadget.
+   *
+   * @param container
+   *          The container the gadget is in.
+   * @param gadgetUrl
+   *          The gadget to check.
+   * @return true if we do have gadget administration data false otherwise.
+   */
+  private boolean hasGadgetAdminData(String container, String gadgetUrl) {
+    return this.getGadgetAdminData(container, gadgetUrl) != null;
+  }
+
+  public boolean isWhitelisted(String container, String gadgetUrl) {
+    if (isWhitelistingEnabled(container)) {
+      return hasGadgetAdminData(container, gadgetUrl);
+    } else {
+      // If the white list checking is not enabled just assume it is there
+      return true;
+    }
+  }
+
+  /**
+   * Determines whether whitelisting is enabled for a container.
+   *
+   * @param container
+   *          The container to check.
+   * @return true if whitelisting is enabled for the container false otherwise.
+   */
+  private boolean isWhitelistingEnabled(String container) {
+    return config.getBool(container, WHITELIST_KEY);
+  }
+
+  /**
+   * Determines whether feature administration is enabled for a container.
+   *
+   * @param container
+   *          The container to check.
+   * @return true if feature administration is enabled for the container false otherwise.
+   */
+  private boolean isFeatureAdminEnabled(String container) {
+    return config.getBool(container, ENABLE_FEATURE_ADMIN);
+  }
+
+  public Set<String> getAdditionalRpcServiceIds(Gadget gadget) {
+    GadgetAdminData gadgetData = this.getGadgetAdminData(getSafeContainerFromGadget(gadget),
+            getSafeGadgetUrlFromGadget(gadget));
+    Set<String> ids = Sets.newHashSet();
+    if(gadgetData != null) {
+      ids.addAll(gadgetData.getRpcAdminData().getAdditionalRpcServiceIds());
+    }
+    return ids;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/ContainerAdminData.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/ContainerAdminData.java
new file mode 100644
index 0000000..3089b1e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/ContainerAdminData.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.caja.util.Maps;
+import com.google.common.base.Objects;
+
+/**
+ * Container's administration data.
+ *
+ * @version $Id: $
+ */
+public class ContainerAdminData {
+  private static final String STAR = "*";
+  private static final String HTTP = "http";
+  private static final String HTTPS = "https";
+  private static final int HTTP_PORT = 80;
+  private static final int HTTPS_PORT = 443;
+
+  private Map<String, GadgetAdminData> gadgetAdminMap;
+
+  /**
+   * Constructor
+   */
+  public ContainerAdminData() {
+    this(null);
+  }
+
+  /**
+   * Constructor
+   *
+   * @param gadgetAdminMap
+   *          map of gadget URLs to gadget admin data.
+   */
+  public ContainerAdminData(Map<String, GadgetAdminData> gadgetAdminMap) {
+    if (gadgetAdminMap == null) {
+      gadgetAdminMap = Maps.newHashMap();
+    }
+    this.gadgetAdminMap = gadgetAdminMap;
+  }
+
+  /**
+   * Adds gadget administration information for this container.
+   *
+   * @param gadgetUrl
+   *          the URL of the gadget the admin data is for.
+   * @param toAdd
+   *          the administration data for the gadget.
+   */
+  public void addGadgetAdminData(String gadgetUrl, GadgetAdminData toAdd) {
+    if (gadgetUrl != null) {
+      if (toAdd == null) {
+        toAdd = new GadgetAdminData();
+      }
+      this.gadgetAdminMap.put(gadgetUrl, toAdd);
+    }
+  }
+
+  /**
+   * Removes the gadget administration data.
+   *
+   * @param gadgetUrl
+   *          the gadget URL.
+   *
+   * @return The gadget administration data that was removed, or null if there was not gadget
+   *         administration data associated with that gadget URL.
+   */
+  public GadgetAdminData removeGadgetAdminData(String gadgetUrl) {
+    return this.gadgetAdminMap.remove(gadgetUrl);
+  }
+
+  /**
+   * Gets the gadget admin data for a given gadget.
+   *
+   * @param gadgetUrl
+   *          the URL to the gadget to get the administration data for.
+   * @return the gadget admin data.
+   */
+  public GadgetAdminData getGadgetAdminData(String gadgetUrl) {
+    GadgetAdminData match = this.gadgetAdminMap.get(gadgetUrl);
+    if(match != null) {
+      return match;
+    }
+
+    String key = gadgetUrl != null ? getGadgetAdminDataKey(gadgetUrl) : null;
+    return this.gadgetAdminMap.get(key);
+  }
+
+  /**
+   * Gets the gadget admin map.
+   *
+   * @return the gadget admin map.
+   */
+  public Map<String, GadgetAdminData> getGadgetAdminMap() {
+    return this.gadgetAdminMap;
+  }
+
+  /**
+   * Clears the gadget administration data.
+   */
+  public void clearGadgetAdminData() {
+    this.gadgetAdminMap.clear();
+  }
+
+  /**
+   * Determines whether there is administration data for a gadget.
+   *
+   * @param gadgetUrl
+   *          the gadget URL to check.
+   * @return true if there is administration data for a gadget false otherwise.
+   */
+  public boolean hasGadgetAdminData(String gadgetUrl) {
+    if (this.gadgetAdminMap.keySet().contains(gadgetUrl)) {
+      return true;
+    }
+
+    return gadgetUrl != null ? getGadgetAdminDataKey(gadgetUrl) != null : false;
+  }
+
+  /**
+   * Gets the key in the map for the gadget URL.
+   *
+   * @param gadgetUrl
+   *          The gadget URL.
+   * @return The key in the map for the gadget URL.
+   */
+  private String getGadgetAdminDataKey(String gadgetUrl) {
+    Set<String> gadgetUrls = this.gadgetAdminMap.keySet();
+    String normalizedGadgetUrl = createUrlWithPort(gadgetUrl);
+    String key = null;
+    for (String url : gadgetUrls) {
+      String normalizedUrl = createUrlWithPort(url);
+      if (normalizedUrl.endsWith(STAR)
+              && normalizedGadgetUrl.startsWith(normalizedUrl.substring(0,
+                      normalizedUrl.length() - 1))) {
+        if (key == null || (key != null && key.length() < normalizedUrl.length())) {
+          key = url;
+        }
+      } else if (normalizedUrl.equals(normalizedGadgetUrl)) {
+        key = url;
+        break;
+      }
+    }
+    return key;
+  }
+
+  /**
+   * Creates a new URL with the default port if one is not already there.
+   *
+   * @param gadgetUrl
+   *          The gadget URL to add the port to.
+   * @return A new URL with the default port.
+   */
+  private String createUrlWithPort(String gadgetUrl) {
+    try {
+      URL origUrl = new URL(gadgetUrl);
+      URL urlWithPort = null;
+      String origHost = origUrl.getHost();
+      if (origUrl.getPort() <= 0 && origHost != null && origHost.length() != 0
+              && !STAR.equals(origHost)) {
+        if (origUrl.getProtocol().equalsIgnoreCase(HTTP)) {
+          urlWithPort = new URL(origUrl.getProtocol(), origUrl.getHost(), HTTP_PORT, origUrl.getFile());
+        }
+        else if (origUrl.getProtocol().equalsIgnoreCase(HTTPS)) {
+          urlWithPort = new URL(origUrl.getProtocol(), origUrl.getHost(), HTTPS_PORT, origUrl.getFile());
+        }
+        return urlWithPort == null ? origUrl.toString() : urlWithPort.toString();
+      } else {
+        return origUrl.toString();
+      }
+    } catch (MalformedURLException e) {
+      return gadgetUrl;
+    }
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof ContainerAdminData) {
+      ContainerAdminData test = (ContainerAdminData) obj;
+      Map<String, GadgetAdminData> testGadgetAdminMap = test.getGadgetAdminMap();
+      return testGadgetAdminMap.equals(this.getGadgetAdminMap());
+
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.gadgetAdminMap);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/FeatureAdminData.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/FeatureAdminData.java
new file mode 100644
index 0000000..42ac3d7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/FeatureAdminData.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import java.util.Set;
+
+import com.google.caja.util.Sets;
+import com.google.common.base.Objects;
+
+/**
+ * Feature administration data for a gadget.
+ *
+ * @version $Id: $
+ */
+public class FeatureAdminData {
+
+  /**
+   * Enumerates the type of feature list.
+   *
+   * @version $Id: $
+   */
+  public enum Type {
+    WHITELIST, BLACKLIST
+  }
+
+  private Set<String> features;
+  private Type type;
+
+  /**
+   * Constructor
+   */
+  public FeatureAdminData() {
+    this(null, null);
+  }
+
+  /**
+   * Constructor
+   *
+   * @param features
+   *          a set of features
+   * @param type
+   *          determines which set takes priority over the other
+   */
+  public FeatureAdminData(Set<String> features, Type type) {
+    if (features == null) {
+      features = Sets.newHashSet();
+    }
+    if (type == null) {
+      type = Type.WHITELIST;
+    }
+    this.features = features;
+    this.type = type;
+  }
+
+  private void addFeatures(Set<String> toAdd, Set<String> features) {
+    for (String feature : toAdd) {
+      if (feature != null) {
+        features.add(feature);
+      }
+    }
+  }
+
+  /**
+   * Adds features for this gadget.
+   *
+   * @param toAdd
+   *          the features for this gadget.
+   */
+  public void addFeatures(Set<String> toAdd) {
+    addFeatures(toAdd, this.features);
+  }
+
+  private Set<String> createSingleFeatureSet(String feature) {
+    Set<String> features = Sets.newHashSet();
+    if (feature != null) {
+      features.add(feature);
+    }
+    return features;
+  }
+
+  /**
+   * Adds an feature for a gadget.
+   *
+   * @param toAdd
+   *          the feature to add.
+   */
+  public void addFeature(String toAdd) {
+    Set<String> features = createSingleFeatureSet(toAdd);
+    addFeatures(features);
+  }
+
+  /**
+   * Clears the set of features.
+   */
+  public void clearFeatures() {
+    this.features.clear();
+  }
+
+  private void removeFeatures(Set<String> toRemove, Set<String> features) {
+    if (toRemove != null && features != null) {
+      for (String feature : toRemove) {
+        features.remove(feature);
+      }
+    }
+  }
+
+  /**
+   * Removes the list of features for a gadget.
+   *
+   * @param toRemove
+   *          the features to remove.
+   */
+  public void removeFeatures(Set<String> toRemove) {
+    removeFeatures(toRemove, this.features);
+  }
+
+  /**
+   * Removes an feature for a gadget.
+   *
+   * @param toRemove
+   *          the feature to remove.
+   */
+  public void removeFeature(String toRemove) {
+    Set<String> features = createSingleFeatureSet(toRemove);
+    removeFeatures(features, this.features);
+  }
+
+  /**
+   * Gets the features.
+   *
+   * @return the features.
+   */
+  public Set<String> getFeatures() {
+    return this.features;
+  }
+
+  /**
+   * Gets the type of features list.
+   *
+   * @return the type of features list.
+   */
+  public Type getType() {
+    return this.type;
+  }
+
+  /**
+   * Sets the type. If this method is passed null than it will default to WHITELIST.
+   *
+   * @param type
+   *          the type to set.
+   */
+  public void setType(Type type) {
+    if (type == null) {
+      type = Type.WHITELIST;
+    }
+    this.type = type;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof FeatureAdminData) {
+      FeatureAdminData test = (FeatureAdminData) obj;
+      return this.getFeatures().equals(test.getFeatures())
+              && this.getType().equals(test.getType());
+    }
+    return false;
+
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.features, this.type);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminData.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminData.java
new file mode 100644
index 0000000..6e92c54
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminData.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import com.google.common.base.Objects;
+
+/**
+ * Information about the container's administration data.
+ *
+ * @since 2.5.0
+ */
+public class GadgetAdminData {
+  // In the future as more gadget admin data is created we
+  // should add it here.
+  private FeatureAdminData featureAdminData;
+  private RpcAdminData rpcAdminData;
+
+  /**
+   * Constructor
+   */
+  public GadgetAdminData() {
+    this.featureAdminData = new FeatureAdminData();
+    this.rpcAdminData = new RpcAdminData();
+  }
+
+  /**
+   * Constructor
+   *
+   * @param featureAdminData
+   *          Feature administration data for this gadget
+   * @param rpcAdminData
+   *          RPC administration data for this gadget
+   */
+  public GadgetAdminData(FeatureAdminData featureAdminData,
+          RpcAdminData rpcAdminData) {
+    if (featureAdminData == null) {
+      featureAdminData = new FeatureAdminData();
+    }
+    if (rpcAdminData == null) {
+      rpcAdminData = new RpcAdminData();
+    }
+    this.featureAdminData = featureAdminData;
+    this.rpcAdminData = rpcAdminData;
+  }
+
+  /**
+   * Gets the feature administration data for this gadget.
+   *
+   * @return
+   */
+  public FeatureAdminData getFeatureAdminData() {
+    return this.featureAdminData;
+  }
+
+  /**
+   * Sets the feature admin data.
+   *
+   * @param featureAdminData
+   *          the feature admin data to set.
+   */
+  public void setFeatureAdminData(FeatureAdminData featureAdminData) {
+    if(featureAdminData == null) {
+      featureAdminData = new FeatureAdminData();
+    }
+    this.featureAdminData = featureAdminData;
+  }
+
+  /**
+   * Gets the RPC administration data.
+   *
+   * @return
+   */
+  public RpcAdminData getRpcAdminData() {
+    return this.rpcAdminData;
+  }
+
+  /**
+   * Sets the RPC administration data.
+   *
+   * @param rpcAdminData
+   *          The RPC administration data to set.
+   */
+  public void setRpcAdminData(RpcAdminData rpcAdminData) {
+    if(rpcAdminData == null) {
+      rpcAdminData = new RpcAdminData();
+    }
+    this.rpcAdminData = rpcAdminData;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof GadgetAdminData) {
+      GadgetAdminData test = (GadgetAdminData) obj;
+      return this.getFeatureAdminData().equals(test.getFeatureAdminData()) &&
+      this.getRpcAdminData().equals(test.getRpcAdminData());
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.featureAdminData, this.rpcAdminData);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminModule.java
new file mode 100644
index 0000000..72ce8f2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminModule.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.util.ResourceLoader;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+/**
+ * Module to load the gadget administration information.
+ *
+ * @version $Id: $
+ */
+public class GadgetAdminModule extends AbstractModule {
+
+  private static final String GADGET_ADMIN_CONFIG = "config/gadget-admin.json";
+  private static final String classname = GadgetAdminModule.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  @Override
+  protected void configure() {
+    bind(GadgetAdminStore.class).toProvider(GadgetAdminStoreProvider.class);
+  }
+
+  @Singleton
+  public static class GadgetAdminStoreProvider implements Provider<GadgetAdminStore> {
+    private BasicGadgetAdminStore store;
+
+    @Inject
+    public GadgetAdminStoreProvider(BasicGadgetAdminStore store) {
+      this.store = store;
+      loadStore();
+    }
+
+    private void loadStore() {
+      try {
+        String gadgetAdminString = ResourceLoader.getContent(GADGET_ADMIN_CONFIG);
+        this.store.init(gadgetAdminString);
+      } catch (Throwable t) {
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, classname, "loadStore", MessageKeys.FAILED_TO_INIT,
+                  new Object[] { GADGET_ADMIN_CONFIG });
+          LOG.log(Level.WARNING, "", t);
+        }
+      }
+    }
+
+    public GadgetAdminStore get() {
+      return store;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminStore.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminStore.java
new file mode 100644
index 0000000..6c96c80
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/GadgetAdminStore.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import java.util.Set;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.spec.Feature;
+
+/**
+ * Interface for working with the store of gadget administration data.
+ *
+ * @since 2.5.0
+ */
+public interface GadgetAdminStore {
+
+  /**
+   * Gets the administration data for a gadget in a container.
+   *
+   * @param container
+   *          the container id.
+   * @param gadgetUrl
+   *          the gadget URL.
+   * @return the administration data for a gadget in a container
+   */
+  public GadgetAdminData getGadgetAdminData(String container, String gadgetUrl);
+
+  /**
+   * Sets gadget administration data for a gadget in a container.
+   *
+   * @param container
+   *          the container id.
+   * @param gadgetUrl
+   *          the gadget URL.
+   * @param adminData
+   *          administration data.
+   */
+  public void setGadgetAdminData(String container, String gadgetUrl, GadgetAdminData adminData);
+
+  /**
+   * Gets container administration data.
+   *
+   * @param container
+   *          the container to get the administration data for.
+   * @return container administration data.
+   */
+  public ContainerAdminData getContainerAdminData(String container);
+
+  /**
+   * Sets the container administration data..
+   *
+   * @param container
+   *          the container to set the administration data for.
+   * @param containerAdminData
+   *          the container administration data.
+   */
+  public void setContainerAdminData(String container, ContainerAdminData containerAdminData);
+
+  /**
+   * Gets the administration data for the server.
+   *
+   * @return the administration data for the server.
+   */
+  public ServerAdminData getServerAdminData();
+
+  /**
+   * Checks the feature administration data for a gadget.
+   *
+   * @param gadget
+   *          The gadget to check.
+   * @return true if the gadget is allowed to use all the features it requires false otherwise.
+   */
+  public boolean checkFeatureAdminInfo(Gadget gadget);
+
+  /**
+   * If feature administration is enabled for the given container then check to see if the feature
+   * is allowed. If it is not allowed the feature code will not be loaded in the container so we
+   * should not put it in the config.
+   *
+   * @param feature
+   *          The feature to check.
+   * @param gadget
+   *          The gadget to check.
+   * @return true if the feature is allowed to be used by the gadget in the given container.
+   */
+  public boolean isAllowedFeature(Feature feature, Gadget gadget);
+
+  /**
+   * Determines whether a gadget is on the whitelist of trusted gadgets set by the admin.
+   *
+   * @param container
+   *          The container id.
+   * @param gadgetUrl
+   *          The gadget URL.
+   * @return true if the gadget is on the whitelist, false otherwise.
+   */
+  public boolean isWhitelisted(String container, String gadgetUrl);
+
+  /**
+   * Gets additional RPC service IDs to allow for the gadget.
+   *
+   * @param gadget
+   *          The gadget to get the IDs for.
+   * @return The set of additional RPC service IDs to allow for the gadget.
+   */
+  public Set<String> getAdditionalRpcServiceIds(Gadget gadget);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/RpcAdminData.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/RpcAdminData.java
new file mode 100644
index 0000000..6d1c566
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/RpcAdminData.java
@@ -0,0 +1,109 @@
+/*

+ * Licensed to the Apache Software Foundation (ASF) under one

+ * or more contributor license agreements.  See the NOTICE file

+ * distributed with this work for additional information

+ * regarding copyright ownership.  The ASF licenses this file

+ * to you 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.

+ */

+package org.apache.shindig.gadgets.admin;

+

+import java.util.Set;

+

+import com.google.caja.util.Sets;

+import com.google.common.base.Objects;

+

+/**

+ * Represents RPC administration data.

+ *

+ * @since 2.5.0

+ */

+public class RpcAdminData {

+

+  private Set<String> additionalRpcServiceIds;

+

+  public RpcAdminData() {

+    this.additionalRpcServiceIds = Sets.newHashSet();

+  }

+

+  /**

+   * Constructor.

+   *

+   * @param additionalRpcServiceIds

+   *          Additional RPC service IDs to allow for the container.

+   */

+  public RpcAdminData(Set<String> additionalRpcServiceIds) {

+    if (additionalRpcServiceIds == null) {

+      additionalRpcServiceIds = Sets.newHashSet();

+    }

+    this.additionalRpcServiceIds = additionalRpcServiceIds;

+  }

+

+  /**

+   * Gets the additional RPC service IDs allowed for the container.

+   *

+   * @return The additional RPC service IDs allowed for the container.

+   */

+  public Set<String> getAdditionalRpcServiceIds() {

+    return additionalRpcServiceIds;

+  }

+

+  /**

+   * Sets the additional RPC service IDs allowed for the container.

+   *

+   * @param ids

+   *          The additional RPC service IDs to allow for the container.

+   */

+  public void setAdditionalRpcServiceIds(Set<String> ids) {

+    if(ids == null) {

+      ids = Sets.newHashSet();

+    }

+    this.additionalRpcServiceIds = ids;

+  }

+

+  /**

+   * Adds an additional RPC service ID for the container.

+   *

+   * @param id

+   *          The additional RPC service ID to allow for this container.

+   */

+  public void addAdditionalRpcServiceId(String id) {

+    if (id != null && id.length() > 0) {

+      this.additionalRpcServiceIds.add(id);

+    }

+  }

+

+  /**

+   * Removes a RPC service ID for the container.

+   *

+   * @param id

+   *          The RPC service ID to remove for this container.

+   */

+  public void removeAdditionalRpcServiceId(String id) {

+    this.additionalRpcServiceIds.remove(id);

+  }

+

+  @Override

+  public boolean equals(Object obj) {

+    if (obj instanceof RpcAdminData) {

+      RpcAdminData test = (RpcAdminData) obj;

+      return test.getAdditionalRpcServiceIds().equals(this.getAdditionalRpcServiceIds());

+    }

+    return false;

+  }

+

+  @Override

+  public int hashCode() {

+    return Objects.hashCode(this.additionalRpcServiceIds);

+  }

+}

diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/ServerAdminData.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/ServerAdminData.java
new file mode 100644
index 0000000..468638a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/admin/ServerAdminData.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import java.util.Map;
+
+import com.google.caja.util.Maps;
+import com.google.common.base.Objects;
+import com.google.inject.Inject;
+import org.apache.shindig.common.Nullable;
+
+/**
+ * Administration data for the server.
+ *
+ * @version $Id: $
+ */
+public class ServerAdminData {
+  private Map<String, ContainerAdminData> containerAdminDataMap;
+
+  /**
+   * Constructor.
+   */
+  @Inject
+  public ServerAdminData() {
+    this(null);
+  }
+
+  /**
+   * Constructor.
+   *
+   * @param containerAdminMap
+   *          a map of container IDs to container.
+   */
+  public ServerAdminData(@Nullable Map<String, ContainerAdminData> containerAdminMap) {
+    this.containerAdminDataMap = (containerAdminMap != null) ? containerAdminMap :
+        Maps.<String, ContainerAdminData>newHashMap();
+  }
+
+  /**
+   * Gets the given containers administration data.
+   *
+   * @param container
+   *          the id of the container.
+   * @return the administration data for the container.
+   */
+  public ContainerAdminData getContainerAdminData(String container) {
+    container = container != null ? container.toLowerCase() : container;
+    return this.containerAdminDataMap.get(container);
+  }
+
+  /**
+   * Removes container administration data.
+   *
+   * @param container
+   *          the container id.
+   */
+  public void removeContainerAdminData(String container) {
+    this.containerAdminDataMap.remove(container);
+  }
+
+  /**
+   * Adds administration data for a container.
+   *
+   * @param container
+   *          the container id the admin data is for.
+   * @param toAdd
+   *          the admin data to add.
+   */
+  public void addContainerAdminData(String container, ContainerAdminData toAdd) {
+    if (container != null) {
+      if (toAdd == null) {
+        toAdd = new ContainerAdminData();
+      }
+      this.containerAdminDataMap.put(container.toLowerCase(), toAdd);
+    }
+  }
+
+  /**
+   * Gets the map of container IDs to container admin data.
+   *
+   * @return the map of container IDs to container admin data.
+   */
+  public Map<String, ContainerAdminData> getContainerAdminDataMap() {
+    return this.containerAdminDataMap;
+  }
+
+  /**
+   * Clears all the container administration data.
+   */
+  public void clearContainerAdminData() {
+    this.containerAdminDataMap.clear();
+  }
+
+  /**
+   * Determines whether there is administration data for the container.
+   *
+   * @param container
+   *          the container to check.
+   * @return true if there is administration data false otherwise.
+   */
+  public boolean hasContainerAdminData(String container) {
+    container = container != null ? container.toLowerCase() : container;
+    return this.containerAdminDataMap.keySet().contains(container);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof ServerAdminData) {
+      ServerAdminData test = (ServerAdminData) obj;
+      return test.getContainerAdminDataMap().equals(this.containerAdminDataMap);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.containerAdminDataMap);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ConfigContributor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ConfigContributor.java
new file mode 100644
index 0000000..9fd52ea
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ConfigContributor.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import org.apache.shindig.gadgets.Gadget;
+
+import java.util.Map;
+
+/**
+ * Interface used by java classes that can inject javascript configuration information
+ * @since 2.0.0
+ */
+public interface ConfigContributor {
+  /**
+   * Contribute configuration values for a specific gadget in an iframe.
+   * @param config The config mapping of feature to value.
+   * @param gadget The gadget to contribute for.
+   */
+  public void contribute(Map<String,Object> config, Gadget gadget);
+
+  /**
+   * Contribute configuration for the container specific javascript. This interface
+   * should only support params used by JsServlet
+   *
+   * @param config The config to add to.
+   * @param container The container.
+   * @param host The hostname
+   */
+  public void contribute(Map<String,Object> config, String container, String host);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ConfigProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ConfigProcessor.java
new file mode 100644
index 0000000..2029195
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ConfigProcessor.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.shindig.gadgets.Gadget;
+
+@ImplementedBy(DefaultConfigProcessor.class)
+public interface ConfigProcessor {
+  // TODO: Clean up ConfigContributor interfaces so this lame uber-interface is not needed.
+  Map<String, Object> getConfig(String container, List<String> features, String host,
+      Gadget gadget);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/CoreUtilConfigContributor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/CoreUtilConfigContributor.java
new file mode 100644
index 0000000..36a0199
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/CoreUtilConfigContributor.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.rewrite.TemplateRewriter;
+import org.apache.shindig.gadgets.spec.Feature;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Populates the core.util configuration, which at present includes the list
+ * of features that are supported.
+ *
+ * @since 2.0.0
+ */
+@Singleton
+public class CoreUtilConfigContributor implements ConfigContributor {
+
+  private final FeatureRegistry registry;
+  private final GadgetAdminStore gadgetAdminStore;
+
+  @Inject
+  public CoreUtilConfigContributor(final FeatureRegistry registry,
+          GadgetAdminStore gadgetAdminStore) {
+    this.registry = registry;
+    this.gadgetAdminStore = gadgetAdminStore;
+  }
+
+  /** {@inheritDoc} */
+  public void contribute(Map<String, Object> config, Gadget gadget) {
+    // Add gadgets.util support. This is calculated dynamically based on request inputs.
+    Collection<Feature> features = gadget.getViewFeatures().values();
+    Map<String, Map<String, Object>> featureMap = Maps.newHashMapWithExpectedSize(features.size());
+    Set<String> allFeatureNames = registry.getAllFeatureNames();
+
+    for (Feature feature : features) {
+      // Skip unregistered features
+      if ((!allFeatureNames.contains(feature.getName())) ||
+              (!gadgetAdminStore.isAllowedFeature(feature, gadget))) {
+        continue;
+      }
+      // Flatten out the multimap a bit for backwards compatibility:  map keys
+      // with just 1 value into the string, treat others as arrays
+      Map<String, Object> paramFeaturesInConfig = Maps.newHashMap();
+      for (String paramName : feature.getParams().keySet()) {
+        Collection<String> paramValues = feature.getParams().get(paramName);
+        // Resolve the template URL to convert relative URL to absolute URL relative to gadget URL.
+        if (TemplateRewriter.TEMPLATES_FEATURE_NAME.equals(feature.getName())
+            && TemplateRewriter.REQUIRE_LIBRARY_PARAM.equals(paramName)) {
+          if (paramValues.size() == 1) {
+            Uri paramUri = Uri.parse(paramValues.iterator().next().trim());
+            paramUri = gadget.getContext().getUrl().resolve(paramUri);
+            paramFeaturesInConfig.put(paramName, paramUri.toString());
+          } else {
+            Collection<String> abReqLibs = Lists.newArrayList();
+            for (String libraryUrl : paramValues) {
+              Uri paramUri = Uri.parse(libraryUrl.trim());
+              paramUri = gadget.getContext().getUrl().resolve(paramUri);
+              abReqLibs.add(paramUri.toString());
+            }
+            paramFeaturesInConfig.put(paramName, abReqLibs);
+          }
+        } else {
+          if (paramValues.size() == 1) {
+            paramFeaturesInConfig.put(paramName, paramValues.iterator().next());
+          } else {
+            paramFeaturesInConfig.put(paramName, paramValues);
+          }
+        }
+      }
+
+      featureMap.put(feature.getName(), paramFeaturesInConfig);
+    }
+    config.put("core.util", featureMap);
+  }
+
+  /** {@inheritDoc} */
+  public void contribute(Map<String,Object> config, String container, String host) {
+    // not used for container configuration
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/DefaultConfigContributorModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/DefaultConfigContributorModule.java
new file mode 100644
index 0000000..ce93498
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/DefaultConfigContributorModule.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.MapBinder;
+
+/**
+ * Registers base config contribution bindings.
+ */
+public class DefaultConfigContributorModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    registerConfigContributors();
+    bind(ConfigProcessor.class).to(DefaultConfigProcessor.class);
+  }
+
+  protected void registerConfigContributors() {
+    MapBinder<String, ConfigContributor> configBinder = MapBinder.newMapBinder(binder(), String.class, ConfigContributor.class);
+    configBinder.addBinding("core.util").to(CoreUtilConfigContributor.class);
+    configBinder.addBinding("osapi").to(OsapiServicesConfigContributor.class);
+    configBinder.addBinding("shindig.auth").to(ShindigAuthConfigContributor.class);
+    configBinder.addBinding("shindig.xhrwrapper").to(XhrwrapperConfigContributor.class);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/DefaultConfigProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/DefaultConfigProcessor.java
new file mode 100644
index 0000000..2ed6273
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/DefaultConfigProcessor.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+
+public class DefaultConfigProcessor implements ConfigProcessor {
+  @VisibleForTesting
+  static final String GADGETS_FEATURES_KEY = "gadgets.features";
+
+  private final Map<String, ConfigContributor> featureContributors;
+  private final List<ConfigContributor> globalContributors;
+  private final ContainerConfig containerConfig;
+
+  @Inject
+  public DefaultConfigProcessor(
+      Map<String, ConfigContributor> featureContributors,
+      ContainerConfig containerConfig) {
+    this.featureContributors = featureContributors;
+    this.globalContributors = Lists.newLinkedList();
+    this.containerConfig = containerConfig;
+  }
+
+  @Inject(optional = true)
+  public void setGlobalContributors(List<ConfigContributor> globalContribs) {
+    globalContributors.addAll(globalContribs);
+  }
+
+  public Map<String, Object> getConfig(String container, List<String> features, String host,
+      Gadget gadget) {
+    Map<String, Object> config = Maps.newHashMap();
+
+    // Perform global config
+    for (ConfigContributor contrib : globalContributors) {
+      contribute(contrib, config, container, host, gadget);
+    }
+
+    // Append some container specific things
+    Map<String, Object> featureConfig = containerConfig.getMap(container, GADGETS_FEATURES_KEY);
+
+    if (featureConfig != null) {
+      // Discard what we don't care about.
+      for (String name : features) {
+        Object conf = featureConfig.get(name);
+        // Add from containerConfig.
+        if (conf != null) {
+          config.put(name, conf);
+        }
+        contribute(featureContributors.get(name), config, container, host, gadget);
+      }
+    }
+    return config;
+  }
+
+  private void contribute(ConfigContributor contrib, Map<String, Object> config, String container,
+      String host, Gadget gadget) {
+    if (contrib != null) {
+      if (host != null) {
+        contrib.contribute(config, container, host);
+      } else if (gadget != null) {
+        contrib.contribute(config, gadget);
+      }
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/OsapiServicesConfigContributor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/OsapiServicesConfigContributor.java
new file mode 100644
index 0000000..f6f3a53
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/OsapiServicesConfigContributor.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.render.RpcServiceLookup;
+
+import java.util.Map;
+
+/**
+ * Populates the osapi.services configuration, which includes
+ * the osapi endpoints this container supports.
+ *
+ * TODO osapi.services as a configuration parameter does not
+ * match a specific feature.  It would be better to store this as
+ * 'osapi:{services: {...}}}
+ *
+ * @since 2.0.0
+ */
+@Singleton
+public class OsapiServicesConfigContributor implements ConfigContributor {
+
+  protected final RpcServiceLookup rpcServiceLookup;
+
+  @Inject
+  public OsapiServicesConfigContributor(RpcServiceLookup rpcServiceLookup) {
+    this.rpcServiceLookup = rpcServiceLookup;
+  }
+
+  /** {@inheritDoc} */
+  public void contribute(Map<String, Object> config, Gadget gadget) {
+    GadgetContext ctx = gadget.getContext();
+    addServicesConfig(config, ctx.getContainer(), ctx.getHost());
+  }
+
+  /** {@inheritDoc} */
+  public void contribute(Map<String,Object> config, String container, String host) {
+    addServicesConfig(config, container, host);
+  }
+
+  /**
+   * Add osapi.services to the config
+   * @param config config map to add it to.
+   * @param container container to use to add osapi.services.
+   * @param host hostname to query from.
+   */
+  private void addServicesConfig(Map<String,Object> config, String container, String host) {
+    if (rpcServiceLookup != null) {
+      Multimap<String, String> endpoints = rpcServiceLookup.getServicesFor(container, host);
+      config.put("osapi.services", endpoints);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ShindigAuthConfigContributor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ShindigAuthConfigContributor.java
new file mode 100644
index 0000000..683121d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/ShindigAuthConfigContributor.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenException;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+
+import java.util.Map;
+
+@Singleton
+/**
+ * Injects auth configuration information for gadgets.config
+ *
+ * @since 2.0.0
+ */
+public class ShindigAuthConfigContributor implements ConfigContributor {
+
+  private SecurityTokenCodec securityTokenCodec;
+
+  @Inject
+  public ShindigAuthConfigContributor(SecurityTokenCodec codec) {
+    this.securityTokenCodec = codec;
+  }
+
+  /** {@inheritDoc} */
+  public void contribute(Map<String,Object> config, Gadget gadget) {
+    final GadgetContext context = gadget.getContext();
+    final SecurityToken authToken = context.getToken();
+    if (authToken != null) {
+      Map<String, String> authConfig = Maps.newHashMapWithExpectedSize(2);
+      String updatedToken = authToken.getUpdatedToken();
+      if (updatedToken != null) {
+        authConfig.put("authToken", updatedToken);
+      }
+      String trustedJson = authToken.getTrustedJson();
+      if (trustedJson != null) {
+        authConfig.put("trustedJson", trustedJson);
+      }
+      config.put("shindig.auth", authConfig);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public void contribute(Map<String,Object> config, String container, String host) {
+    // TODO: This currently will throw an exception when using BlobCrypterSecurityTokens...
+    // It seems to be ok for now, we need some good token cleanup.
+    SecurityToken containerToken = new AnonymousSecurityToken(container, 0L, "*");
+    Map<String, String> authConfig = Maps.newHashMapWithExpectedSize(2);
+
+    try {
+      config.put("shindig.auth", authConfig);
+      authConfig.put("authToken", securityTokenCodec.encodeToken(containerToken));
+
+    } catch (SecurityTokenException e) {
+      // ignore
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/XhrwrapperConfigContributor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/XhrwrapperConfigContributor.java
new file mode 100644
index 0000000..a5e35d1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/config/XhrwrapperConfigContributor.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Singleton;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.spec.View;
+
+import java.util.Map;
+
+/**
+ * Provides config support for the xhrwrapper feature.
+ *
+ * @since 2.0.0
+ */
+
+@Singleton
+public class XhrwrapperConfigContributor implements ConfigContributor {
+  /** {@inheritDoc} */
+  public void contribute(Map<String, Object> config, Gadget gadget) {
+    Map<String, String> xhrWrapperConfig = Maps.newHashMapWithExpectedSize(2);
+    View view = gadget.getCurrentView();
+    Uri contentsUri = view.getHref();
+    xhrWrapperConfig.put("contentUrl", contentsUri == null ? "" : contentsUri.toString());
+    if (AuthType.OAUTH.equals(view.getAuthType())) {
+      addOAuthConfig(xhrWrapperConfig, view);
+    } else if (AuthType.SIGNED.equals(view.getAuthType())) {
+      xhrWrapperConfig.put("authorization", "signed");
+    } else if (AuthType.OAUTH2.equals(view.getAuthType())) {
+      addOAuth2Config(xhrWrapperConfig, view);
+    }
+    config.put("shindig.xhrwrapper", xhrWrapperConfig);
+  }
+
+  /** {@inheritDoc} */
+  private void addOAuthConfig(Map<String, String> xhrWrapperConfig, View view) {
+    Map<String, String> oAuthConfig = Maps.newHashMapWithExpectedSize(3);
+    try {
+      OAuthArguments oAuthArguments = new OAuthArguments(view);
+      oAuthConfig.put("authorization", "oauth");
+      oAuthConfig.put("oauthService", oAuthArguments.getServiceName());
+      if (!"".equals(oAuthArguments.getTokenName())) {
+        oAuthConfig.put("oauthTokenName", oAuthArguments.getTokenName());
+      }
+      xhrWrapperConfig.putAll(oAuthConfig);
+    } catch (GadgetException e) {
+      // Do not add any OAuth configuration if an exception was thrown
+    }
+  }
+
+  private void addOAuth2Config(Map<String, String> xhrWrapperConfig, View view) {
+    Map<String, String> oAuth2Config = Maps.newHashMapWithExpectedSize(3);
+    OAuth2Arguments oAuth2Arguments = new OAuth2Arguments(view);
+    oAuth2Config.put("authorization", "oauth2");
+    oAuth2Config.put("oauthService", oAuth2Arguments.getServiceName());
+    xhrWrapperConfig.putAll(oAuth2Config);
+  }
+
+  public void contribute(Map<String, Object> config, String container, String host) {
+    // no-op, no container specific configuration
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/encoding/EncodingDetector.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/encoding/EncodingDetector.java
new file mode 100644
index 0000000..17a77cb
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/encoding/EncodingDetector.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.encoding;
+
+import java.nio.charset.Charset;
+
+import com.google.common.base.Charsets;
+import com.ibm.icu.text.CharsetDetector;
+import com.ibm.icu.text.CharsetMatch;
+
+/**
+ * Attempts to determine the encoding of a given string.
+ *
+ * Highly skewed towards common encodings (UTF-8 and Latin-1).
+ */
+public final class EncodingDetector {
+  private EncodingDetector() {}
+
+  public static class FallbackEncodingDetector {
+    public Charset detectEncoding(byte[] input) {
+      // Fall back to the incredibly slow ICU. It might be better to just skip this entirely.
+      CharsetDetector detector = new CharsetDetector();
+      detector.setText(input);
+      CharsetMatch match = detector.detect();
+      return Charset.forName(match.getName().toUpperCase());
+    }
+  }
+
+  /**
+   * Returns the detected encoding of the given byte array.
+   *
+   * @param input The data to detect the encoding for.
+   * @param assume88591IfNotUtf8 True to assume that the encoding is ISO-8859-1 (the standard
+   *     encoding for HTTP) if the bytes are not valid UTF-8. Only recommended if you can reasonably
+   *     expect that other encodings are going to be specified. Full encoding detection is very
+   *     expensive!
+   * @param alternateDecoder specify a fallback encoding detection.
+   *     Only used if assume88591IfNotUtf8 is false.
+   * @return The detected encoding.
+   */
+  public static Charset detectEncoding(byte[] input, boolean assume88591IfNotUtf8,
+      FallbackEncodingDetector alternateDecoder) {
+    if (looksLikeValidUtf8(input)) {
+      return Charsets.UTF_8;
+    }
+
+    if (assume88591IfNotUtf8) {
+      return Charsets.ISO_8859_1;
+    }
+
+    // Fall back encoding:
+    return alternateDecoder.detectEncoding(input);
+  }
+
+  /**
+   * A pretty good test that something is UTF-8. There are many sequences that will pass here that
+   * aren't valid UTF-8 due to the requirement that the shortest possible sequence always be used.
+   * We're ok with this behavior because the main goal is speed.
+   */
+  private static boolean looksLikeValidUtf8(byte[] input) {
+    int i = 0;
+    if (input.length >= 3 &&
+       (input[0] & 0xFF) == 0xEF &&
+       (input[1] & 0xFF) == 0xBB &&
+       (input[2] & 0xFF) == 0xBF) {
+      // Skip BOM.
+      i = 3;
+    }
+
+    int endOfSequence;
+    for (int j = input.length; i < j; ++i) {
+      int bite = input[i];
+      if ((bite & 0x80) == 0) {
+        continue; // ASCII
+      }
+
+      // Determine number of bytes in the sequence.
+      if ((bite & 0x0E0) == 0x0C0) {
+        endOfSequence = i + 1;
+      } else if ((bite & 0x0F0) == 0x0E0) {
+        endOfSequence = i + 2;
+      } else if ((bite & 0x0F8) == 0xF0) {
+        endOfSequence = i + 3;
+      } else {
+        // Not a valid utf-8 byte sequence. Skip.
+        return false;
+      }
+
+      if (endOfSequence >= j) {
+        // End of sequence reached, not a valid sequence
+        return false;
+      }
+
+      while (i < endOfSequence) {
+        i++;
+        bite = input[i];
+        if ((bite & 0xC0) != 0x80) {
+          // High bit not set, not a valid sequence
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/ApiDirective.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/ApiDirective.java
new file mode 100644
index 0000000..12a6063
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/ApiDirective.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+/**
+ * Represents a single &lt;exports&gt; or &lt;uses&gt; tag in a
+ * feature manifest. These in turn provide context to compiler/optimizer
+ * code and container code (for gadgets.rpc service IDs).
+ */
+public class ApiDirective {
+  public enum Type {
+    JS("js"),
+    RPC("rpc");
+
+    private final String code;
+
+    private Type(String code) {
+      this.code = code;
+    }
+
+    public static Type fromCode(String code) {
+      for (Type value : Type.values()) {
+        if (value.code.equals(code)) {
+          return value;
+        }
+      }
+      return null;
+    }
+  }
+
+  private final Type type;
+  private final String value;
+  private final boolean isUses;
+
+  ApiDirective(String type, String value, boolean isUses) {
+    this.type = Type.fromCode(type);
+    this.value = value;
+    this.isUses = isUses;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  public boolean isUses() {
+    return isUses;
+  }
+
+  public boolean isExports() {
+    return !isUses;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/BrowserSpecificFeatureResource.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/BrowserSpecificFeatureResource.java
new file mode 100644
index 0000000..43f224c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/BrowserSpecificFeatureResource.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.servlet.UserAgent;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A FeatureResource that supports being supplied only to certain browsers.
+ *
+ * This is optional functionality, activated by the browser="..." attribute on
+ * a &lt;script&gt; element. That attribute's value is interpreted as a
+ * comma-separated list of BROWSER-versionKey matchers.
+ *
+ * BROWSER must match (case-insensitive) the list of UserAgent.Browser enum values
+ * eg. "MSIE" or "FIREFOX".
+ *
+ * versionKey is OPERATORversionNumber, where OPERATOR may be one of:
+ * ^ - regex
+ * = - exact match
+ * >, >=, <, <= - greater than/less than matches
+ * [no operator] - exact match
+ *
+ * If no browser="..." attribute is specified, the resource always matches. Otherwise,
+ * if ANY of the browser-versionKey matchers match, the resource matches. In such case,
+ * the delegate FeatureResource's content methods are consulted. Otherwise, "" is returned
+ * for content.
+ *
+ * Example:
+ * browser="FireFox->=3, MSIE-6.0 would match FireFox 3.x.y (any) and IE 6.0 (only).
+ *
+ * To activate this capability, you may use the provided Loader class and bind it
+ * as your FeatureResourceLoader implementation; or build your own that wraps its resources
+ * in BrowserSpecificFeatureResource.
+ */
+public class BrowserSpecificFeatureResource implements FeatureResource {
+  private final Provider<UserAgent> uaProvider;
+  private final FeatureResource delegate;
+  private final Map<UserAgent.Browser, List<VersionMatcher>> browserMatch;
+
+  public BrowserSpecificFeatureResource(
+      Provider<UserAgent> uaProvider, FeatureResource delegate, String browserKey) {
+    this.uaProvider = uaProvider;
+    this.delegate = delegate;
+    this.browserMatch = populateBrowserMatchers(browserKey);
+  }
+
+  public String getContent() {
+    if (browserMatches()) {
+      return delegate.getContent();
+    }
+    return "";
+  }
+
+  public String getDebugContent() {
+    if (browserMatches()) {
+      return delegate.getDebugContent();
+    }
+    return "";
+  }
+
+  public boolean isExternal() {
+    return delegate.isExternal();
+  }
+
+  public boolean isProxyCacheable() {
+    // If browser-specific (ie. browserMatch has some qualifiers in it), not proxy cacheable
+    // (since the vast majority of browsers don't support Vary: User-Agent, we just say "false")
+    // Otherwise, delegate this call.
+    return browserMatch.isEmpty() ? delegate.isProxyCacheable() : false;
+  }
+
+  public String getName() {
+    return delegate.getName();
+  }
+
+  public Map<String, String> getAttribs() {
+    return delegate.getAttribs();
+  }
+
+  private boolean browserMatches() {
+    if (browserMatch.isEmpty()) {
+      // Not browser-sensitive.
+      return true;
+    }
+    UserAgent ua = uaProvider.get();
+    List<VersionMatcher> versionMatchers = browserMatch.get(ua.getBrowser());
+    if (versionMatchers != null) {
+      for (VersionMatcher matcher : versionMatchers) {
+        if (matcher.matches(ua.getVersion())) return true;
+      }
+    }
+    return false;
+  }
+
+  private static Map<UserAgent.Browser, List<VersionMatcher>> populateBrowserMatchers(
+      String browserKey) {
+    Map<UserAgent.Browser, List<VersionMatcher>> map = Maps.newHashMap();
+    if (browserKey == null || browserKey.length() == 0) {
+      return map;
+    }
+
+    // Comma-delimited list of <browser>-<versionKey> pairs.
+    String[] entries = StringUtils.split(browserKey, ',');
+    for (String entry : entries) {
+      entry = entry.trim();
+      String[] browserAndVersion = StringUtils.split(entry, '-');
+      String browser = browserAndVersion[0];
+      String versionKey = browserAndVersion.length == 2 ? browserAndVersion[1] : null;
+
+      // This may throw an IllegalArgumentException, (properly) indicating a faulty feature.xml
+      UserAgent.Browser browserEnum = UserAgent.Browser.valueOf(browser.toUpperCase());
+      if (!map.containsKey(browserEnum)) {
+        map.put(browserEnum, Lists.<VersionMatcher>newLinkedList());
+      }
+      map.get(browserEnum).add(new VersionMatcher(versionKey));
+    }
+
+    return map;
+  }
+
+  /**
+   * Simple FeatureResourceLoader implementation that wraps all resource loads in
+   * a browser-filtering delegator.
+   */
+  public static class Loader extends FeatureResourceLoader {
+    private final Provider<UserAgent> uaProvider;
+
+    @Inject
+    public Loader(HttpFetcher fetcher, TimeSource timeSource, FeatureFileSystem fileSystem,
+        Provider<UserAgent> uaProvider) {
+      super(fetcher, timeSource, fileSystem);
+      this.uaProvider = uaProvider;
+    }
+
+    @Override
+    public FeatureResource load(Uri uri, Map<String, String> attribs) throws GadgetException {
+      return new BrowserSpecificFeatureResource(
+          uaProvider, super.load(uri, attribs), attribs.get("browser"));
+    }
+  }
+
+  private static final class VersionMatcher {
+    private static final Op[] OPS = {
+      new Op("^") {
+        @Override
+        public boolean match(String in, String key) {
+          return in.matches(key);
+        }
+      },
+      new Op("=") {
+        @Override
+        public boolean match(String in, String key) {
+          return in.equals(key) || num(in).eq(num(key));
+        }
+      },
+      new Op(">") {
+        @Override
+        public boolean match(String in, String key) {
+          return num(in).gt(num(key));
+        }
+      },
+      new Op(">=") {
+        @Override
+        public boolean match(String in, String key) {
+          return in.equals(key) || num(in).eq(num(key)) || num(in).gt(num(key));
+        }
+      },
+      new Op("<") {
+        @Override
+        public boolean match(String in, String key) {
+          return num(in).lt(num(key));
+        }
+      },
+      new Op("<=") {
+        @Override
+        public boolean match(String in, String key) {
+          return in.equals(key) || num(in).eq(num(key)) || num(in).lt(num(key));
+        }
+      },
+    };
+
+    private final String versionKey;
+
+    private VersionMatcher(String versionKey) {
+      if (versionKey != null && versionKey.length() != 0) {
+        this.versionKey = versionKey;
+      } else {
+        // No qualifier = match all (shortcut)
+        this.versionKey = null;
+      }
+    }
+
+    public boolean matches(String version) {
+      if (versionKey == null || versionKey.equals(version)) {
+        // Match-all or exact-string-match.
+        return true;
+      }
+      for (Op op : OPS) {
+        if (op.apply(version, versionKey)) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    private static VersionNumber num(String str) {
+      return new VersionNumber(str);
+    }
+
+    private static abstract class Op {
+      private final String pfx;
+
+      private Op(String pfx) {
+        this.pfx = pfx;
+      }
+
+      private boolean apply(String version, String key) {
+        if (version.startsWith(pfx)) {
+          version = version.substring(pfx.length());
+          return match(version, key);
+        }
+        return false;
+      }
+
+      public abstract boolean match(String in, String key);
+    }
+
+    private static final class VersionNumber {
+      private final int[] parts;
+
+      private VersionNumber(String str) {
+        String[] strParts = StringUtils.split(str, '.');
+        int[] intParts = new int[strParts.length];
+        try {
+          for (int i = 0; i < strParts.length; ++i) {
+            intParts[i] = Integer.parseInt(strParts[i]);
+          }
+        } catch (NumberFormatException e) {
+          intParts = null;
+        }
+        this.parts = intParts;
+      }
+
+      public boolean eq(VersionNumber other) {
+        return Arrays.equals(this.parts, other.parts);
+      }
+
+      public boolean lt(VersionNumber other) {
+        for (int i = 0; i < this.parts.length; ++i) {
+          int otherVal = (i < other.parts.length) ? other.parts[i] : 0;  // 0's fill in the rest
+          if (this.parts[i] > otherVal) {
+            return false;
+          }
+        }
+        return true;
+      }
+
+      public boolean gt(VersionNumber other) {
+        for (int i = 0; i < this.parts.length; ++i) {
+          int otherVal = (i < other.parts.length) ? other.parts[i] : 0;  // 0's fill in the rest
+          if (this.parts[i] < otherVal) {
+            return false;
+          }
+        }
+        return true;
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureFile.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureFile.java
new file mode 100644
index 0000000..81cf87d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureFile.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import org.apache.shindig.common.util.ResourceLoader;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+
+/**
+ * DefaultFile delegate feature file interface to java.io.File object
+ */
+public class DefaultFeatureFile implements FeatureFile {
+
+  protected final File wrappedFile;
+
+  public DefaultFeatureFile(String path) {
+    this.wrappedFile = new File(path);
+  }
+
+  protected DefaultFeatureFile(File wrappedFile) {
+    this.wrappedFile = wrappedFile;
+  }
+
+  protected DefaultFeatureFile createFile(File wrappedFile) {
+    return new DefaultFeatureFile(wrappedFile);
+  }
+
+  public InputStream getInputStream() throws IOException {
+    return new FileInputStream(wrappedFile);
+  }
+
+  public boolean canRead() {
+    return wrappedFile.canRead();
+  }
+
+  public boolean exists() {
+    return wrappedFile.exists();
+  }
+
+  public String getName() {
+    return wrappedFile.getName();
+  }
+
+  public String getPath() {
+    return wrappedFile.getPath();
+  }
+
+  public String getAbsolutePath() {
+    return wrappedFile.getAbsolutePath();
+  }
+
+  public boolean isDirectory() {
+    return wrappedFile.isDirectory();
+  }
+
+  public FeatureFile[] listFiles() {
+    File[] wrappedFiles = wrappedFile.listFiles();
+    if (wrappedFiles == null) {
+      return null;
+    }
+    FeatureFile[] files = new FeatureFile[wrappedFiles.length];
+    for (int i = 0; i < wrappedFiles.length; i++) {
+      files[i] = createFile(wrappedFiles[i]);
+    }
+    return files;
+  }
+
+  public URI toURI() {
+    return wrappedFile.toURI();
+  }
+
+  public String getContent() throws IOException {
+    return ResourceLoader.getContent(wrappedFile);
+  }
+
+  public long lastModified() {
+    return wrappedFile.lastModified();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureFileSystem.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureFileSystem.java
new file mode 100644
index 0000000..2e45d93
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureFileSystem.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import org.apache.shindig.common.util.ResourceLoader;
+
+import java.io.IOException;
+
+/**
+ * Default file system class that generate default file objects
+ */
+public class DefaultFeatureFileSystem implements FeatureFileSystem {
+  public FeatureFile getFile(String path) {
+    return new DefaultFeatureFile(path);
+  }
+
+  public String getResourceContent(String resource) throws IOException {
+    return ResourceLoader.getContent(resource);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureRegistryProvider.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureRegistryProvider.java
new file mode 100644
index 0000000..d9e1fbc
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/DefaultFeatureRegistryProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetException.Code;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+/**
+ * Default feature registry provider that support only default (null) repository
+ */
+public class DefaultFeatureRegistryProvider implements FeatureRegistryProvider {
+
+  private final FeatureRegistry registry;
+
+  @Inject
+  public DefaultFeatureRegistryProvider(FeatureRegistry registry) {
+    this.registry = registry;
+  }
+
+  public FeatureRegistry get(String repository) throws GadgetException {
+    if (repository == null) {
+      return registry;
+    }
+    throw new GadgetException(Code.INVALID_PARAMETER, "Repository is not supported",
+        HttpResponse.SC_BAD_REQUEST);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureFile.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureFile.java
new file mode 100644
index 0000000..1b9be4e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureFile.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.inject.ImplementedBy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+
+/**
+ * A File Interface used by features to allow different storage then file system
+ */
+@ImplementedBy(DefaultFeatureFile.class)
+public interface FeatureFile {
+
+  /**
+   * @return file name
+   */
+  String getName();
+
+  /**
+   * @return file path
+   */
+  String getPath();
+
+  /**
+   * @return file absolute path
+   */
+  String getAbsolutePath();
+
+  /**
+   * @return true if file exists
+   */
+  boolean exists();
+
+  /**
+   * @return true if file can be read
+   */
+  boolean canRead();
+
+  /**
+   * @return true if file is a directory
+   */
+  boolean isDirectory();
+
+  /**
+   * @return list of files in the directory
+   */
+  FeatureFile[] listFiles() throws IOException;
+
+  /**
+   * @return file access in URI format
+   */
+  URI toURI();
+
+  /**
+   * @return InputStream to read file content
+   * @throws IOException
+   */
+  InputStream getInputStream() throws IOException;
+
+  /**
+   * @return Return the file content
+   * @throws IOException
+   */
+  String getContent() throws IOException;
+
+  /**
+   * @return return the time the file was last modified (miliseconds since epoch, or 0L for error)
+   */
+  long lastModified();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureFileSystem.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureFileSystem.java
new file mode 100644
index 0000000..689f22b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureFileSystem.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.inject.ImplementedBy;
+
+import java.io.IOException;
+
+/**
+ * Interface to handle file system and generate file objects
+ */
+@ImplementedBy(DefaultFeatureFileSystem.class)
+public interface FeatureFileSystem {
+  /**
+   * @param path file name and path
+   * @return new file object for specified file path
+   */
+  FeatureFile getFile(String path) throws IOException;
+
+  /**
+   * Load resource content
+   * @param resource
+   * @return resource content
+   * @throws IOException
+   */
+  String getResourceContent(String resource) throws IOException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureParser.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureParser.java
new file mode 100644
index 0000000..fdb31f1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureParser.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parses feature.xml files into an intermediary Java object for further processing.
+ * This is largely an implementation detail of FeatureRegistry.
+ */
+class FeatureParser {
+  public ParsedFeature parse(Uri parent, String xml) throws GadgetException {
+    Element doc;
+    try {
+      doc = XmlUtil.parse(xml);
+    } catch (XmlException e) {
+      throw new GadgetException(GadgetException.Code.MALFORMED_XML_DOCUMENT, e);
+    }
+
+    String name = null;
+    List<String> deps = Lists.newArrayList();
+    List<ParsedFeature.Bundle> bundles = Lists.newArrayList();
+    boolean supportDefer = false;
+
+    NodeList children = doc.getChildNodes();
+    for (int i = 0, j = children.getLength(); i < j; ++i) {
+      Node child = children.item(i);
+      if (child.getNodeType() == Node.ELEMENT_NODE) {
+        Element element = (Element)child;
+        if (element.getTagName().equals("name")) {
+          name = element.getTextContent();
+        } else if (element.getTagName().equals("dependency")) {
+          deps.add(element.getTextContent());
+        } else {
+          String type = element.getTagName().toLowerCase();
+          List<ParsedFeature.Resource> resources = Lists.newArrayList();
+          NodeList resourceKids = element.getElementsByTagName("script");
+          for (int x = 0, y = resourceKids.getLength(); x < y; ++x) {
+            Element resourceChild = (Element)resourceKids.item(x);
+            String src = resourceChild.getAttribute("src");
+            String content = resourceChild.getTextContent();
+            Map<String, String> attribs = getAttribs(resourceChild);
+            Uri source = null;
+            if (src != null && src.length() > 0) {
+              if (!"false".equals(attribs.get("inline"))) {
+                source = parent.resolve(FeatureRegistry.getComponentUri(src));
+              } else {
+                source = Uri.parse(src);
+              }
+            }
+            resources.add(new ParsedFeature.Resource(
+                source,
+                src != null && src.length() != 0 ? null : content,
+                getAttribs(resourceChild)));
+          }
+          List<ApiDirective> apiDirectives = Lists.newArrayList();
+          NodeList apiKids = element.getElementsByTagName("api");
+          for (int x = 0, y = apiKids.getLength(); x < y; ++x) {
+            Element apiChild = (Element)apiKids.item(x);
+            supportDefer = "true".equalsIgnoreCase(apiChild.getAttribute("supportDefer"));
+            NodeList apiElems = apiChild.getChildNodes();
+            for (int a = 0, b = apiElems.getLength(); a < b; ++a) {
+              Node apiElemNode = apiElems.item(a);
+              if (apiElemNode.getNodeType() == Node.ELEMENT_NODE) {
+                Element apiElem = (Element)apiElemNode;
+                boolean isImport = "uses".equals(apiElem.getNodeName());
+                boolean isExport = "exports".equals(apiElem.getNodeName());
+                if (isImport || isExport) {
+                  apiDirectives.add(new ApiDirective(
+                      apiElem.getAttribute("type"), apiElem.getTextContent(), isImport));
+                }
+              }
+            }
+          }
+          bundles.add(new ParsedFeature.Bundle(name, type, getAttribs(element),
+              resources, apiDirectives, supportDefer));
+        }
+      }
+    }
+
+    return new ParsedFeature(name, deps, bundles);
+  }
+
+  private Map<String, String> getAttribs(Element element) {
+    ImmutableMap.Builder<String, String> attribs = ImmutableMap.builder();
+    NamedNodeMap attribNodes = element.getAttributes();
+    for (int x = 0, y = attribNodes.getLength(); x < y; ++x) {
+      Attr attr = (Attr)attribNodes.item(x);
+      if (!attr.getName().equals("src")) {
+        attribs.put(attr.getName(), attr.getValue());
+      }
+    }
+    return attribs.build();
+  }
+
+  static final class ParsedFeature {
+    private final String name;
+    private final List<String> deps;
+    private final List<Bundle> bundles;
+
+    private ParsedFeature(String name, List<String> deps, List<Bundle> bundles) {
+      this.name = name;
+      this.deps = ImmutableList.copyOf(deps);
+      this.bundles = ImmutableList.copyOf(bundles);
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public List<String> getDeps() {
+      return deps;
+    }
+
+    public List<Bundle> getBundles() {
+      return bundles;
+    }
+
+    public final static class Bundle {
+      private final String name;
+      private final String type;
+      private final Map<String, String> attribs;
+      private final List<Resource> resources;
+      private final List<ApiDirective> apiDirectives;
+      private final boolean supportDefer;
+
+      private Bundle(String name, String type, Map<String, String> attribs,
+          List<Resource> resources, List<ApiDirective> apiDirectives, boolean supportDefer) {
+        this.name = name;
+        this.type = type;
+        this.attribs = attribs;
+        this.resources = resources;
+        this.apiDirectives = apiDirectives;
+        this.supportDefer = supportDefer;
+      }
+
+      public String getName() {
+        return name;
+      }
+
+      public String getType() {
+        return type;
+      }
+
+      public Map<String, String> getAttribs() {
+        return attribs;
+      }
+
+      public List<Resource> getResources() {
+        return resources;
+      }
+
+      public List<ApiDirective> getApis() {
+        return apiDirectives;
+      }
+
+      public boolean isSupportDefer() {
+        return supportDefer;
+      }
+    }
+
+    static final class Resource {
+      private final Uri source;
+      private final String content;
+      private final Map<String, String> attribs;
+
+      private Resource(Uri source, String content, Map<String, String> attribs) {
+        this.source = source;
+        this.content = content;
+        this.attribs = ImmutableMap.copyOf(attribs);
+      }
+
+      public Uri getSource() {
+        return source;
+      }
+
+      public String getContent() {
+        return content;
+      }
+
+      public Map<String, String> getAttribs() {
+        return attribs;
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistry.java
new file mode 100644
index 0000000..02ab7c9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistry.java
@@ -0,0 +1,753 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.RenderingContext;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+/**
+ * Mechanism for loading feature.xml files from a location keyed by a String.
+ * That String might be the location of a text file which in turn contains
+ * other feature file locations; a directory; or a feature.xml file itself.
+ */
+@Singleton
+public class FeatureRegistry {
+  public static final String CACHE_NAME = "FeatureJsCache";
+  public static final String RESOURCE_SCHEME = "res";
+  public static final String FILE_SCHEME = "file";
+  public static final Splitter CRLF_SPLITTER = Splitter.onPattern("[\r\n]+").trimResults().omitEmptyStrings();
+
+  //class name for logging purpose
+  private static final String classname = FeatureRegistry.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  // Map keyed by FeatureNode object created as a lookup for transitive feature deps.
+  private final Cache<String, LookupResult> cache;
+
+  private final FeatureParser parser;
+  private final FeatureResourceLoader resourceLoader;
+  private final ImmutableMap<String, FeatureNode> featureMap;
+  private final FeatureFileSystem fileSystem;
+  private final String repository;
+
+
+/**
+ * Construct a new FeatureRegistry, using resourceLoader to load actual resources,
+ * and loading data from the list of features provided, each of which points to a
+ * directory (whose .xml files will be scanned), file (interpreted as feature.xml),
+ * or resource (interpreted as feature.xml).
+ * @param resourceLoader
+ * @param features
+ * @throws GadgetException
+ */
+  @Inject
+  public FeatureRegistry(FeatureResourceLoader resourceLoader,
+                         CacheProvider cacheProvider,
+                         @Named("org.apache.shindig.features") List<String> features,
+                         FeatureFileSystem fileSystem) throws GadgetException {
+    this(resourceLoader, cacheProvider, features, fileSystem, null);
+  }
+
+  public FeatureRegistry(FeatureResourceLoader resourceLoader,
+      CacheProvider cacheProvider,
+      @Named("org.apache.shindig.features") List<String> features,
+      FeatureFileSystem fileSystem, String repository) throws GadgetException {
+
+    this.parser = new FeatureParser();
+    this.resourceLoader = resourceLoader;
+    this.fileSystem = fileSystem;
+    this.repository = repository;
+
+    this.featureMap = register(features);
+
+    // Connect the dependency graph made up of all features and validate there
+    // are no circular deps.
+    connectDependencyGraph();
+
+    this.cache = cacheProvider.createCache(CACHE_NAME);
+  }
+
+  public String getRepository() {
+    return repository;
+  }
+
+  /**
+   * Reads and registers all of the features in the directory, or the file, specified by
+   * the given resourceKey. Invalid features or invalid paths will yield a
+   * GadgetException.
+   *
+   * All features registered by this method must be valid (well-formed XML, resource
+   * references all return successfully), and each "batch" of registered features
+   * must be able to be assimilated into the current features tree in a valid fashion.
+   * That is, their dependencies must all be valid features as well, and the
+   * dependency tree must not contain circular dependencies.
+   *
+   * @param resourceList The files or directories to load the feature from. If feature.xml
+   *    is passed in directly, it will be loaded as a single feature. If a
+   *    directory is passed, any features in that directory (recursively) will
+   *    be loaded. If res://*.txt or res:*.txt is passed, we will look for named resources
+   *    in the text file. If path is prefixed with res:// or res:, the file
+   *    is treated as a resource, and all references are assumed to be
+   *    resources as well. Multiple locations may be specified by separating
+   *    them with a comma.
+   * @throws GadgetException If any of the files can't be read, are malformed, or invalid.
+   */
+  protected ImmutableMap<String, FeatureNode> register(List<String> resourceList)
+      throws GadgetException {
+    Map<String,FeatureNode> featureMapBuilder = Maps.newHashMap();
+
+    try {
+      for (String location : resourceList) {
+        Uri uriLoc = getComponentUri(location);
+
+        if (uriLoc.getScheme() != null && uriLoc.getScheme().equals(RESOURCE_SCHEME)) {
+          List<String> resources = Lists.newArrayList();
+
+          // Load as resource using ResourceLoader.
+          location = uriLoc.getPath();
+          if (location.startsWith("/")) {
+            // Accommodate res:// URIs.
+            location = location.substring(1);
+          }
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "register",
+                MessageKeys.LOAD_RESOURCES_FROM, new Object[] {uriLoc.toString()});
+          }
+          if (location.endsWith(".txt")) {
+            // Text file contains a list of other resource files to load
+            for (String resource : CRLF_SPLITTER.split(resourceLoader.getResourceContent(location))) {
+              if (resource.charAt(0) != '#') {
+                // Skip commented lines.
+                resource = getComponentUri(resource).getPath();
+                resources.add(resource);
+              }
+            }
+          } else {
+            resources.add(location);
+          }
+
+          loadResources(resources, featureMapBuilder);
+        } else {
+          // Load files in directory structure.
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "register",
+                MessageKeys.LOAD_FILES_FROM, new Object[] {location});
+          }
+          loadFile(fileSystem.getFile(uriLoc.getPath()), featureMapBuilder);
+        }
+      }
+      return ImmutableMap.copyOf(featureMapBuilder);
+    } catch (IOException e) {
+      throw new GadgetException(GadgetException.Code.INVALID_PATH, e);
+    }
+  }
+
+  /**
+   * For the given list of needed features, retrieves all the FeatureResource objects that
+   * contain their content and, if requested, that of their transitive dependencies.
+   *
+   * Resources are returned in order of their place in the dependency tree, with "bottom"/
+   * depended-on resources returned before those that depend on them. Resource objects
+   * within a given feature are returned in the order specified in their corresponding
+   * feature.xml file. In the case of a dependency tree "tie" eg. A depends on [B, C], B and C
+   * depend on D - resources are returned in the dependency order specified in feature.xml.
+   *
+   * Fills the "unsupported" list, if provided, with unknown features in the needed list.
+   *
+   * @param ctx Context for the request.
+   * @param needed List of all needed features.
+   * @param unsupported If non-null, a List populated with unknown features from the needed list.
+   * @return LookupResult object that may be used to render the needed features.
+   */
+  public LookupResult getFeatureResources(
+      GadgetContext ctx, Collection<String> needed, List<String> unsupported, boolean transitive) {
+    boolean useCache = (transitive && !ctx.getIgnoreCache());
+    String cacheKey = makeCacheKey(needed, ctx, unsupported);
+
+    if (useCache) {
+      LookupResult lookup = cache.getElement(cacheKey);
+      if (lookup != null) {
+        return lookup;
+      }
+    }
+
+    List<FeatureNode> featureNodes = transitive ?
+        getTransitiveDeps(needed, unsupported) : getRequestedNodes(needed, unsupported);
+
+    ImmutableList.Builder<FeatureBundle> bundlesBuilder =
+        new ImmutableList.Builder<FeatureBundle>();
+
+    for (FeatureNode entry : featureNodes) {
+      boolean specificContainerMatched = false;
+      List<FeatureBundle> noContainerBundles = Lists.newLinkedList();
+      for (FeatureBundle bundle : entry.getBundles(ctx.getRenderingContext())) {
+        String containerAttrib = bundle.getAttribs().get("container");
+        if (containerAttrib != null) {
+          if (containerMatch(containerAttrib, ctx.getContainer())) {
+            bundlesBuilder.add(bundle);
+            specificContainerMatched = true;
+          }
+        } else {
+          // Applies only if there were no specific container matches.
+          noContainerBundles.add(bundle);
+        }
+      }
+      if (!specificContainerMatched) {
+        for (FeatureBundle bundle : noContainerBundles) {
+          bundlesBuilder.add(bundle);
+        }
+      }
+    }
+
+    LookupResult result = new LookupResult(bundlesBuilder.build());
+    if (useCache && (unsupported == null || unsupported.isEmpty())) {
+      cache.addElement(cacheKey, result);
+    }
+
+    return result;
+  }
+
+  /**
+   * Helper method retrieving feature resources, including transitive dependencies.
+   * @param ctx Context for the request.
+   * @param needed List of all needed features.
+   * @param unsupported If non-null, a List populated with unknown features from the needed list.
+   * @return List of FeatureResources that may be used to render the needed features.
+   */
+  public LookupResult getFeatureResources(GadgetContext ctx, Collection<String> needed,
+      List<String> unsupported) {
+    return getFeatureResources(ctx, needed, unsupported, true);
+  }
+
+  /**
+   * Returns all known FeatureResources in dependency order, as described in getFeatureResources.
+   * Returns only GADGET-context resources. This is a convenience method largely for calculating
+   * JS checksum.
+   * @return List of all known (RenderingContext.GADGET) FeatureResources.
+   */
+  public LookupResult getAllFeatures() {
+    return getFeatureResources(new GadgetContext(), featureMap.keySet(), null);
+  }
+
+  /**
+   * Calculates and returns a dependency-ordered (as in getFeatureResources) list of features
+   * included directly or transitively from the specified list of needed features.
+   * This API ignores any unknown features among the needed list.
+   * @param needed List of features for which to obtain an ordered dep list.
+   * @return Ordered list of feature names, as described.
+   */
+  public List<String> getFeatures(Collection<String> needed) {
+    List<FeatureNode> fullTree = getTransitiveDeps(needed, Lists.<String>newLinkedList());
+    List<String> allFeatures = Lists.newLinkedList();
+    for (FeatureNode node : fullTree) {
+      allFeatures.add(node.name);
+    }
+    return allFeatures;
+  }
+
+  /**
+   * Helper method, returns all known feature names.
+   * @return All known feature names.
+   */
+  public Set<String> getAllFeatureNames() {
+    return featureMap.keySet();
+  }
+
+  // Provided for backward compatibility with existing feature loader configurations.
+  // res://-prefixed URIs are actually scheme = res, host = "", path = "/stuff". We want res:path.
+  // Package-private for use by FeatureParser as well.
+  static Uri getComponentUri(String str) {
+    return (str.startsWith("res://")) ?
+      new UriBuilder().setScheme(RESOURCE_SCHEME).setPath(str.substring(6)).toUri() :
+      Uri.parse(str);
+  }
+
+  private List<FeatureNode> getTransitiveDeps(
+      Collection<String> needed, List<String> unsupported) {
+    final List<FeatureNode> requested = getRequestedNodes(needed, unsupported);
+
+    Comparator<FeatureNode> nodeDepthComparator = new Comparator<FeatureNode>() {
+      public int compare(FeatureNode one, FeatureNode two) {
+        if (one.nodeDepth > two.nodeDepth ||
+            (one.nodeDepth == two.nodeDepth &&
+             requested.indexOf(one) < requested.indexOf(two))) {
+          return -1;
+        }
+        return 1;
+      }
+    };
+    // Before getTransitiveDeps() is called, all nodes and their graphs have been validated
+    // to have no circular dependencies, with their tree depth calculated. The requested
+    // features here may overlap in the tree, so we need to be sure not to double-include
+    // deps. Consider case where feature A depends on B and C, which both depend on D.
+    // If the requested features list is [A, C], we want to include A's tree in the appropriate
+    // order, and avoid double-including C (and its dependency D). Thus we sort by node depth
+    // first - A's tree is deeper than that of C, so *if* A's tree contains C, traversing
+    // it first guarantees that C is eventually included.
+    Collections.sort(requested, nodeDepthComparator);
+
+    Set<String> alreadySeen = Sets.newHashSet();
+    List<FeatureNode> fullDeps = Lists.newLinkedList();
+    for (FeatureNode requestedFeature : requested) {
+      for (FeatureNode toAdd : requestedFeature.getTransitiveDeps()) {
+        if (!alreadySeen.contains(toAdd.name)) {
+          alreadySeen.add(toAdd.name);
+          fullDeps.add(toAdd);
+        }
+      }
+    }
+
+    return fullDeps;
+  }
+
+  private List<FeatureNode> getRequestedNodes(
+      Collection<String> needed, List<String> unsupported) {
+    List<FeatureNode> requested = Lists.newArrayList();
+    for (String featureName : needed) {
+      if (featureMap.containsKey(featureName)) {
+        requested.add(featureMap.get(featureName));
+      } else {
+        if (unsupported != null) unsupported.add(featureName);
+      }
+    }
+    return requested;
+  }
+
+  private boolean containerMatch(String containerAttrib, String container) {
+    for (String attr : Splitter.on(',').trimResults().split(containerAttrib)) {
+      if (attr.equals(container)) return true;
+    }
+    return false;
+  }
+
+  private void connectDependencyGraph() throws GadgetException {
+    // Iterate through each raw dependency, adding the corresponding feature to the graph.
+    // Collect as many feature dep tree errors as possible before erroring out.
+    List<String> problems = Lists.newLinkedList();
+    List<FeatureNode> theFeatures = Lists.newLinkedList();
+
+    // First hook up all first-order dependencies.
+    for (Map.Entry<String, FeatureNode> featureEntry : featureMap.entrySet()) {
+      String name = featureEntry.getKey();
+      FeatureNode feature = featureEntry.getValue();
+
+      for (String rawDep : feature.getRawDeps()) {
+        if (!featureMap.containsKey(rawDep)) {
+          problems.add("Feature [" + name + "] has dependency on unknown feature: " + rawDep);
+        } else {
+          feature.addDep(featureMap.get(rawDep));
+          theFeatures.add(feature);
+        }
+      }
+    }
+
+    // Then hook up the transitive dependency graph to validate there are
+    // no loops present.
+    for (FeatureNode feature : theFeatures) {
+      try {
+        // Validates the dependency tree ensuring no circular dependencies,
+        // and calculates the depth of the dependency tree rooted at the node.
+        feature.completeNodeGraph();
+      } catch (GadgetException e) {
+        problems.add(e.getMessage());
+      }
+    }
+
+    if (!problems.isEmpty()) {
+      StringBuilder sb = new StringBuilder();
+      sb.append("Problems found processing features:\n");
+      Joiner.on('\n').appendTo(sb, problems);
+      throw new GadgetException(GadgetException.Code.INVALID_CONFIG, sb.toString());
+    }
+  }
+
+  private void loadResources(List<String> resources, Map<String,FeatureNode> featureMapBuilder)
+      throws GadgetException {
+    try {
+      for (String resource : resources) {
+        if (LOG.isLoggable(Level.FINE)) {
+          LOG.fine("Processing resource: " + resource);
+        }
+
+        String content = resourceLoader.getResourceContent(resource);
+        Uri parent = new UriBuilder().setScheme(RESOURCE_SCHEME).setPath(resource).toUri();
+        loadFeature(parent, content, featureMapBuilder);
+      }
+    } catch (IOException e) {
+      throw new GadgetException(GadgetException.Code.INVALID_PATH, e);
+    }
+  }
+
+  private void loadFile(FeatureFile file, Map<String,FeatureNode> featureMapBuilder)
+      throws GadgetException, IOException {
+    if (!file.exists() || !file.canRead()) {
+      throw new GadgetException(GadgetException.Code.INVALID_CONFIG,
+          "Feature file '" + file.getPath() + "' doesn't exist or can't be read");
+    }
+
+    FeatureFile[] toLoad = file.isDirectory() ? file.listFiles() : new FeatureFile[] { file };
+
+    for (FeatureFile featureFile : toLoad) {
+      if (featureFile.isDirectory()) {
+        // Traverse into subdirectories.
+        loadFile(featureFile, featureMapBuilder);
+      } else if (featureFile.getName().toLowerCase(Locale.ENGLISH).endsWith(".xml")) {
+        String content = featureFile.getContent();
+        Uri parent = Uri.fromJavaUri(featureFile.toURI());
+        loadFeature(parent, content, featureMapBuilder);
+      } else {
+        if (LOG.isLoggable(Level.FINEST)) {
+          LOG.finest(featureFile.getAbsolutePath() + " doesn't seem to be an XML file.");
+        }
+      }
+    }
+  }
+
+  /**
+   * Method that loads gadget features.
+   *
+   * @param parent uri of parent
+   * @param xml xml to parse
+   * @throws GadgetException
+   */
+  protected void loadFeature(Uri parent, String xml, Map<String,FeatureNode> featureMapBuilder)
+      throws GadgetException {
+    FeatureParser.ParsedFeature parsed = parser.parse(parent, xml);
+    // Duplicate feature = OK, just indicate it's being overridden.
+    if (featureMapBuilder.containsKey(parsed.getName())) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.logp(Level.WARNING, classname, "doFilter", MessageKeys.OVERRIDING_FEATURE,
+            new Object[] {parsed.getName(),parent});
+      }
+    }
+
+    // Walk through all parsed bundles, pulling resources and creating FeatureBundles/Nodes.
+    List<FeatureBundle> bundles = Lists.newArrayList();
+    for (FeatureParser.ParsedFeature.Bundle parsedBundle : parsed.getBundles()) {
+      List<FeatureResource> resources = Lists.newArrayList();
+      for (FeatureParser.ParsedFeature.Resource parsedResource : parsedBundle.getResources()) {
+        if (parsedResource.getSource() == null) {
+
+          resources.add(new InlineFeatureResource(parsed.getName() + ":inline.js",
+              parsedResource.getContent(), parsedResource.getAttribs()));
+        } else {
+          // Load using resourceLoader
+          resources.add(resourceLoader.load(parsedResource.getSource(),
+              getResourceAttribs(parsedBundle.getAttribs(), parsedResource.getAttribs())));
+        }
+      }
+      bundles.add(new FeatureBundle(parsedBundle, resources));
+    }
+
+    // Add feature to the master Map. The dependency tree isn't connected/validated/linked yet.
+    featureMapBuilder.put(parsed.getName(),
+        new FeatureNode(parsed.getName(), bundles, parsed.getDeps()));
+  }
+
+  protected String makeCacheKey(Collection<String> needed, GadgetContext ctx, List<String> unsupported) {
+    List<String> neededList = Lists.newArrayList(needed);
+    Collections.sort(neededList);
+    return new StringBuilder().append(StringUtils.join(neededList, ":"))
+        .append('|').append(ctx.getRenderingContext())
+        .append('|').append(ctx.getContainer())
+        .append('|').append(unsupported != null)
+        .append('|').append(Strings.nullToEmpty(repository))
+        .toString();
+  }
+
+  private Map<String, String> getResourceAttribs(Map<String, String> bundleAttribs,
+      Map<String, String> resourceAttribs) {
+    // For a given resource, attribs are a merge (by key, not by value) of bundle attribs and
+    // per-resource attribs, the latter serving as higher-precedence overrides.
+    return ImmutableMap.<String, String>builder()
+        .putAll(bundleAttribs)
+        .putAll(resourceAttribs).build();
+  }
+
+  private static final class InlineFeatureResource extends FeatureResource.Attribute {
+    private final String name;
+    private final String content;
+
+    private InlineFeatureResource(String name, String content, Map<String, String> attribs) {
+      super(attribs);
+      this.content = content;
+      this.name = name;
+    }
+
+    public String getContent() {
+      return content;
+    }
+
+    public String getDebugContent() {
+      return content;
+    }
+
+    public String getName() {
+      return name;
+    }
+  }
+
+  public static class LookupResult {
+    private final List<FeatureBundle> bundles;
+    private final List<FeatureResource> allResources;
+
+    public LookupResult(List<FeatureBundle> bundles) {
+      this.bundles = bundles;
+      ImmutableList.Builder<FeatureResource> resourcesBuilder = ImmutableList.builder();
+      for (FeatureBundle bundle : getBundles()) {
+        resourcesBuilder.addAll(bundle.getResources());
+      }
+      this.allResources = resourcesBuilder.build();
+    }
+
+    public List<FeatureBundle> getBundles() {
+      return bundles;
+    }
+
+    public List<FeatureResource> getResources() {
+      return allResources;
+    }
+  }
+
+  public static class FeatureBundle {
+    private final FeatureParser.ParsedFeature.Bundle bundle;
+    private final List<FeatureResource> resources;
+
+    public FeatureBundle(FeatureParser.ParsedFeature.Bundle bundle,
+                         List<FeatureResource> resources) {
+      this.bundle = bundle;
+      this.resources = ImmutableList.copyOf(resources);
+    }
+
+    public String getName() {
+      return bundle.getName();
+    }
+
+    public String getType() {
+      return bundle.getType();
+    }
+
+    public Map<String, String> getAttribs() {
+      return bundle.getAttribs();
+    }
+
+    public List<FeatureResource> getResources() {
+      return resources;
+    }
+
+    public List<ApiDirective> getApis() {
+      return bundle.getApis();
+    }
+
+    public boolean isSupportDefer() {
+      return bundle.isSupportDefer();
+    }
+
+    public List<String> getApis(ApiDirective.Type type, boolean isExports) {
+      ImmutableList.Builder<String> builder = ImmutableList.builder();
+      for (ApiDirective api : bundle.getApis()) {
+        if (api.getType() == type && api.isExports() == isExports) {
+          builder.add(api.getValue());
+        }
+      }
+      return builder.build();
+    }
+  }
+
+  private static final class FeatureNode {
+    private final String name;
+    private final List<FeatureBundle> bundles;
+    private final List<String> requestedDeps;
+    private final List<FeatureNode> depList;
+    private List<FeatureNode> transitiveDeps;
+    private boolean calculatedDepsStale;
+    private int nodeDepth = 0;
+
+    private FeatureNode(String name, List<FeatureBundle> bundles, List<String> rawDeps) {
+      this.name = name;
+      this.bundles = ImmutableList.copyOf(bundles);
+      this.requestedDeps = ImmutableList.copyOf(rawDeps);
+      this.depList = Lists.newLinkedList();
+      this.transitiveDeps = Lists.newArrayList(this);
+      this.calculatedDepsStale = false;
+    }
+
+    /**
+     * Returns all bundles matching the given rendering context.
+     * If there's >= 1 bundle matching a non-ALL context, return it.
+     * Otherwise, return any ALL bundles.
+     * @param rctx
+     */
+    public Iterable<FeatureBundle> getBundles(RenderingContext rctx) {
+      String tagMatch = null;
+      String directTag = rctx.getFeatureBundleTag();
+      for (FeatureBundle bundle : bundles) {
+        if (directTag != null && directTag.equalsIgnoreCase(bundle.getType())) {
+          tagMatch = directTag;
+        }
+      }
+      if (tagMatch == null) {
+        tagMatch = RenderingContext.ALL.getFeatureBundleTag();
+      }
+      final String useForMatching = tagMatch;
+
+      // Return an Iterator rather than coping a new
+      // list containing the types over which to iterate.
+      return new Iterable<FeatureBundle>() {
+        public Iterator<FeatureBundle> iterator() {
+          return new Iterator<FeatureBundle>() {
+            private FeatureBundle next;
+            private final Iterator<FeatureBundle> it = bundles.iterator();
+
+            public boolean hasNext() {
+              while (next == null && it.hasNext()) {
+                FeatureBundle candidate = it.next();
+                if (useForMatching.equalsIgnoreCase(candidate.getType())) {
+                  next = candidate;
+                }
+              }
+              return next != null;
+            }
+
+            public FeatureBundle next() {
+              hasNext();
+              FeatureBundle ret = next;
+              next = null;
+              return ret;
+            }
+
+            public void remove() {
+              throw new UnsupportedOperationException("Can't remove from bundle iterator");
+            }
+          };
+        }
+      };
+    }
+
+    public List<String> getRawDeps() {
+      return requestedDeps;
+    }
+
+    public void addDep(FeatureNode dep) {
+      depList.add(dep);
+      calculatedDepsStale = true;
+    }
+
+    private List<FeatureNode> getDepList() {
+      List<FeatureNode> revOrderDeps = Lists.newArrayList(depList);
+      Collections.reverse(revOrderDeps);
+      return ImmutableList.copyOf(revOrderDeps);
+    }
+
+    public void completeNodeGraph() throws GadgetException {
+      if (!calculatedDepsStale) {
+        return;
+      }
+
+      // Detect feature dependency loop
+      Set<String> tempList  = new HashSet<String>();
+      this.checkDependencyLoop(this, tempList);
+
+      this.nodeDepth = 0;
+      this.transitiveDeps = Lists.newLinkedList();
+      this.transitiveDeps.add(this);
+
+      Queue<Pair<FeatureNode, Pair<Integer, String>>> toTraverse = Lists.newLinkedList();
+      toTraverse.add(Pair.of(this, Pair.of(0, "")));
+
+      while (!toTraverse.isEmpty()) {
+        Pair<FeatureNode, Pair<Integer, String>> next = toTraverse.poll();
+        String debug = next.two.two + (next.two.one > 0 ? " -> " : "") + next.one.name;
+        // Breadth-first list of dependencies.
+        this.transitiveDeps.add(next.one);
+        this.nodeDepth = Math.max(this.nodeDepth, next.two.one);
+        for (FeatureNode nextDep : next.one.getDepList()) {
+          toTraverse.add(Pair.of(nextDep, Pair.of(next.two.one + 1, debug)));
+        }
+      }
+
+      Collections.reverse(this.transitiveDeps);
+      calculatedDepsStale = false;
+    }
+
+    public List<FeatureNode> getTransitiveDeps() {
+      return this.transitiveDeps;
+    }
+
+    /**
+     * Check whether current feature node has dependency loop
+     * @param featureNode
+     *     feature node which needs to check whether it has dependency loop.
+     * @param dependencyList
+     *     used to check whether current branch of feature dependency tree has dependency loop or not.
+     * @throws GadgetException
+     *     a new GadgetException will be thrown if a loop is detected.
+     */
+    private void checkDependencyLoop(FeatureNode featureNode, Set<String> dependencyList) throws GadgetException {
+        String featureNodeName = featureNode.name;
+        if (dependencyList.contains(featureNodeName)) {
+            // If dependencyList already contains this feature node, then current branch has dependency loop.
+            throw new GadgetException(GadgetException.Code.INVALID_CONFIG, "Feature " + featureNodeName + " has dependency loop problem.");
+        }
+        // Add the current dependency into branch.
+        dependencyList.add(featureNodeName);
+        List<FeatureNode> deps = featureNode.getDepList();
+        for (FeatureNode f : deps) {
+            checkDependencyLoop(f, dependencyList);
+            // Remove the part which already has been verified.
+            dependencyList.remove(f.name);
+        }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistryProvider.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistryProvider.java
new file mode 100644
index 0000000..ed2368f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureRegistryProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.inject.ImplementedBy;
+
+import org.apache.shindig.gadgets.GadgetException;
+
+/**
+ * Interface to provide feature registry for a repository
+ */
+@ImplementedBy(DefaultFeatureRegistryProvider.class)
+public interface FeatureRegistryProvider {
+  public FeatureRegistry get(String repository) throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResource.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResource.java
new file mode 100644
index 0000000..cf1990b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResource.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Map;
+
+/**
+ * Interface yielding content/code for JS features.
+ */
+public interface FeatureResource {
+  /**
+   * @return "Normal"-mode content for the feature, eg. optimized/obfuscated JS.
+   */
+  String getContent();
+
+  /**
+   * @return Debug-mode content for the feature.
+   */
+  String getDebugContent();
+
+  /**
+   * @return True if the content is actually a URL to be included via &lt;script src&gt;
+   */
+  boolean isExternal();
+
+  /**
+   * @return True if the JS can be cached by intermediary proxies or not.
+   */
+  boolean isProxyCacheable();
+
+  /**
+   * @return A descriptive name used to reference the JS in various debug and stats contexts.
+   */
+  String getName();
+
+  /**
+   * @return XML-defined attributes associated with this for resource selection/inclusion.
+   */
+  Map<String, String> getAttribs();
+
+  /**
+   * Helper base class to avoid having to implement rarely-overridden isExternal/isProxyCacheable
+   * functionality in FeatureResource.
+   */
+  public abstract class Default implements FeatureResource {
+    public boolean isExternal() {
+      return false;
+    }
+
+    public boolean isProxyCacheable() {
+      return true;
+    }
+
+    public Map<String, String> getAttribs() {
+      return ImmutableMap.of();
+    }
+  }
+
+  public abstract class Attribute extends Default {
+    private final Map<String, String> attribs;
+
+    public Attribute(Map<String, String> attribs) {
+      this.attribs = attribs;
+    }
+
+    @Override
+    public Map<String, String> getAttribs() {
+      return attribs;
+    }
+  }
+
+  public class Simple extends Default {
+    private final String content;
+    private final String debugContent;
+    private final String name;
+
+    public Simple(String content, String debugContent, String name) {
+      this.content = content;
+      this.debugContent = debugContent;
+      this.name = name;
+    }
+
+    public String getContent() {
+      return content;
+    }
+
+    public String getDebugContent() {
+      return debugContent;
+    }
+
+    public String getName() {
+      return name;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java
new file mode 100644
index 0000000..ff5cd6f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/FeatureResourceLoader.java
@@ -0,0 +1,330 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Class that loads FeatureResource objects used to populate JS feature code.
+ */
+public class FeatureResourceLoader {
+
+  // Class name for logging purpose
+  private static final String classname = FeatureResourceLoader.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  private final HttpFetcher fetcher;
+  private final TimeSource timeSource;
+  private final FeatureFileSystem fileSystem;
+  private int updateCheckFrequency = 0;  // <= 0 -> only load data once, don't check for updates.
+
+  @Inject
+  public FeatureResourceLoader(
+      HttpFetcher fetcher, TimeSource timeSource, FeatureFileSystem fileSystem) {
+    this.fetcher = fetcher;
+    this.timeSource = timeSource;
+    this.fileSystem = fileSystem;
+  }
+
+  @Inject(optional = true)
+  public void setSupportFileUpdates(
+      @Named("shindig.features.loader.file-update-check-frequency-ms") int updateCheckFrequency) {
+    this.updateCheckFrequency = updateCheckFrequency;
+  }
+
+  /**
+   * Primary, and only public, method of FeatureResourceLoader. Loads the resource
+   * keyed at the given {@code uri}, which was decorated with the provided list of attributes.
+   *
+   * The default implementation loads both file and res-schema resources using
+   * ResourceLoader, attempting to load optimized content for files named [file].js as [file].opt.js.
+   *
+   * Override this method to provide custom functionality. Basic loadFile, loadResource, and loadUri
+   * methods are kept protected for easy reuse.
+   *
+   * @param uri Uri of resource to be loaded.
+   * @param attribs Attributes decorating the resource in the corresponding feature.xml
+   * @return FeatureResource object providing content and debugContent loading capability.
+   * @throws GadgetException If any failure occurs during this process.
+   */
+  public FeatureResource load(Uri uri, Map<String, String> attribs) throws GadgetException {
+    try {
+      if ("file".equals(uri.getScheme())) {
+        return loadFile(uri.getPath(), attribs);
+      } else if ("res".equals(uri.getScheme())) {
+        return loadResource(uri.getPath(), attribs);
+      }
+      return loadUri(uri, attribs);
+    } catch (IOException e) {
+      throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, e);
+    }
+  }
+
+  @SuppressWarnings("unused")
+  protected FeatureResource loadFile(String path, Map<String, String> attribs) throws IOException {
+    return new DualModeFileResource(getOptPath(path), path, attribs);
+  }
+
+  protected String getFileContent(FeatureFile file) {
+    try {
+      return file.getContent();
+    } catch (IOException e) {
+      // This is fine; errors happen downstream.
+      return null;
+    }
+  }
+
+  @SuppressWarnings("unused")
+  protected FeatureResource loadResource(
+      String path, Map<String, String> attribs) throws IOException {
+    String optContent = null, debugContent = null;
+    try {
+      optContent = getResourceContent(getOptPath(path));
+    } catch (IOException e) {
+      // OK - optContent can be null. Error thrown downstream if both are null.
+    }
+    try {
+      debugContent = getResourceContent(path);
+    } catch (IOException e) {
+      // See above; OK for debugContent to be null.
+    }
+    return new DualModeStaticResource(path, optContent, debugContent, attribs);
+  }
+
+  public String getResourceContent(String resource) throws IOException {
+    return fileSystem.getResourceContent(resource);
+  }
+
+  /**
+   * @throws IOException if failed to load uri (by derived classes)
+   */
+  protected FeatureResource loadUri(Uri uri, Map<String, String> attribs) throws IOException {
+    String inline = attribs.get("inline");
+    inline = inline != null ? inline : "";
+    return new UriResource(fetcher, uri,
+        "1".equals(inline) || "true".equalsIgnoreCase(inline),
+        attribs);
+  }
+
+  protected String getOptPath(String orig) {
+    if (orig.endsWith(".js") && !orig.endsWith(".opt.js")) {
+      return orig.substring(0, orig.length() - 3) + ".opt.js";
+    }
+    return orig;
+  }
+
+  // Overridable for easier testing.
+  protected boolean fileHasChanged(FeatureFile file, long lastModified) {
+    return file.lastModified() > lastModified;
+  }
+
+  private class DualModeFileResource extends FeatureResource.Attribute {
+    private final FileContent optContent;
+    private final FileContent dbgContent;
+    private final String fileName;
+
+    protected DualModeFileResource(String optFilePath, String dbgFilePath,
+        Map<String, String> attribs) {
+      super(attribs);
+      this.optContent = new FileContent(optFilePath);
+      this.dbgContent = new FileContent(dbgFilePath);
+      this.fileName = dbgFilePath;
+      Preconditions.checkArgument(optContent.get() != null || dbgContent.get() != null,
+        "Problems reading resource: %s", dbgFilePath);
+    }
+
+    public String getContent() {
+      String opt = optContent.get();
+      return opt != null ? opt : dbgContent.get();
+    }
+
+    public String getDebugContent() {
+      String dbg = dbgContent.get();
+      return dbg != null ? dbg : optContent.get();
+    }
+
+    public String getName() {
+      return fileName;
+    }
+
+    private final class FileContent {
+      private final String filePath;
+      private long lastModified;
+      private long lastUpdateCheckTime;
+      private String content;
+
+      private FileContent(String filePath) {
+        this.filePath = filePath;
+        this.lastModified = 0;
+        this.lastUpdateCheckTime = 0;
+      }
+
+      private String get() {
+        long nowTime = timeSource.currentTimeMillis();
+        if (content == null ||
+            (updateCheckFrequency > 0 &&
+             (lastUpdateCheckTime + updateCheckFrequency) < nowTime)) {
+          // Only check for file updates at preconfigured intervals. This prevents
+          // overwhelming the file system while maintaining a reasonable update rate w/o
+          // implementing a full event-driven mechanism.
+          lastUpdateCheckTime = nowTime;
+          FeatureFile file;
+          try {
+            file = fileSystem.getFile(filePath);
+          } catch (IOException e) {
+            return null;
+          }
+          if (fileHasChanged(file, lastModified)) {
+            // Only reload file content if it's changed (or if it's the first
+            // load, when this check will succeed).
+            String newContent = getFileContent(file);
+            if (newContent != null) {
+              content = newContent;
+              lastModified = file.lastModified();
+            } else if (content != null) {
+              // Content existed before, file removed - log error.
+              if (LOG.isLoggable(Level.WARNING)) {
+                LOG.logp(Level.WARNING, classname, "get", MessageKeys.MISSING_FILE,
+                    new Object[] {filePath});
+              }
+            }
+          }
+        }
+        return content;
+      }
+    }
+  }
+
+  private static final class DualModeStaticResource extends FeatureResource.Attribute {
+    private final String content;
+    private final String debugContent;
+    private final String path;
+
+    private DualModeStaticResource(
+        String path, String content, String debugContent, Map<String, String> attribs) {
+      super(attribs);
+      this.content = content != null ? content : debugContent;
+      this.debugContent = debugContent != null ? debugContent : content;
+      this.path = path;
+      Preconditions.checkArgument(this.content != null, "Problems reading resource: %s", path);
+    }
+
+    public String getContent() {
+      return content;
+    }
+
+    public String getDebugContent() {
+      return debugContent;
+    }
+
+    public String getName() {
+      return path;
+    }
+  }
+
+  private static final class UriResource extends FeatureResource.Attribute {
+    private final HttpFetcher fetcher;
+    private final Uri uri;
+    private final boolean isInline;
+    private String content;
+    private long lastLoadTryMs;
+
+    private UriResource(HttpFetcher fetcher, Uri uri, boolean isInline,
+        Map<String, String> attribs) {
+      super(attribs);
+      this.fetcher = fetcher;
+      this.uri = uri;
+      this.isInline = isInline;
+      this.lastLoadTryMs = 0;
+      this.content = getContent();
+    }
+
+    public String getContent() {
+      if (isExternal()) {
+        return uri.toString();
+      } else if (content != null) {
+        // Variable content is a one-time content cache for inline JS features.
+        return content;
+      }
+
+      // Try to load the content. Ideally, and most of the time, this
+      // will happen immediately at startup. However, if the target server is
+      // down it shouldn't hose the entire server, so in that case we defer
+      // and try at most once per minute thereafter, the delay in place to
+      // avoid overwhelming a server down on its heels.
+      long now = System.currentTimeMillis();
+      if (fetcher != null && now > (lastLoadTryMs + (60 * 1000))) {
+        lastLoadTryMs = now;
+        try {
+          HttpRequest request = new HttpRequest(uri).setInternalRequest(true);
+          HttpResponse response = fetcher.fetch(request);
+          if (response.getHttpStatusCode() == HttpResponse.SC_OK) {
+            content = response.getResponseAsString();
+          } else {
+            if (LOG.isLoggable(Level.WARNING)) {
+              LOG.logp(Level.WARNING, classname, "getContent", MessageKeys.UNABLE_RETRIEVE_LIB,
+                  new Object[] {uri});
+            }
+          }
+        } catch (GadgetException e) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "getContent", MessageKeys.UNABLE_RETRIEVE_LIB,
+                new Object[] {uri});
+          }
+        }
+      }
+
+      return content;
+    }
+
+    public String getDebugContent() {
+      return getContent();
+    }
+
+    @Override
+    public boolean isExternal() {
+      return !isInline;
+    }
+
+    @Override
+    public boolean isProxyCacheable() {
+      return content != null;
+    }
+
+    public String getName() {
+      return uri.toString();
+    }
+
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/TestFeatureRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/TestFeatureRegistry.java
new file mode 100644
index 0000000..9716f0b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/features/TestFeatureRegistry.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.gadgets.GadgetException;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Maps;
+
+import java.util.List;
+
+/**
+ * Helper classes extending FeatureRegistry for use by test classes.
+ * Includes several helpers to load (fake) feature.xml and JS files from
+ * memory (or tempfile), but does not change FeatureRegistry logic.
+ */
+public class TestFeatureRegistry extends FeatureRegistry {
+  public static Builder newBuilder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private static String RESOURCE_BASE_PATH = "/resource/base/path";
+    private static int resourceIdx = 0;
+
+    private final ResourceMock resourceMock;
+    private final List<String> featureFiles;
+
+    private Builder() {
+      this.resourceMock = new ResourceMock();
+      this.featureFiles = Lists.newLinkedList();
+    }
+
+    public TestFeatureRegistry build(String useFeature) throws GadgetException {
+      return new TestFeatureRegistry(
+          new TestFeatureResourceLoader(resourceMock),
+          new TestCacheProvider(),
+          useFeature);
+    }
+
+    public TestFeatureRegistry build() throws GadgetException {
+      return build(Joiner.on(",").join(featureFiles));
+    }
+
+    public Builder addFeatureFile(String featureFile) {
+      featureFiles.add(featureFile);
+      return this;
+    }
+
+    /* Expectation methods and helpers */
+    public Uri expectResource(String content) {
+      return expectResource(content, ".xml");
+    }
+
+    public Uri expectResource(String content, String suffix) {
+      Uri res = makeResourceUri(suffix);
+      resourceMock.put(res.getPath(), content);
+      return res;
+    }
+
+    private static Uri makeResourceUri(String suffix) {
+      return Uri.parse("res://" + RESOURCE_BASE_PATH + "/file" + (++resourceIdx) + suffix);
+    }
+  }
+
+  /* Actual class contents here */
+  private final TestFeatureResourceLoader resourceLoader;
+  private final TestCacheProvider cacheProvider;
+  private TestFeatureRegistry(
+      TestFeatureResourceLoader resourceLoader,
+      TestCacheProvider cacheProvider,
+      String featureFiles) throws GadgetException {
+    super(resourceLoader, cacheProvider, ImmutableList.<String>of(featureFiles),
+        new DefaultFeatureFileSystem());
+    this.resourceLoader = resourceLoader;
+    this.cacheProvider = cacheProvider;
+  }
+
+  public Map<String, String> getLastAttribs() {
+    return Collections.unmodifiableMap(resourceLoader.lastAttribs);
+  }
+
+  @SuppressWarnings("unchecked")
+  public Cache<String, LookupResult> getLookupCache() {
+    Cache<?, ?> cacheEntry = cacheProvider.caches.get(FeatureRegistry.CACHE_NAME);
+    if (cacheEntry == null) {
+      return null;
+    }
+    return (Cache<String, LookupResult>)cacheEntry;
+  }
+
+  private static class TestFeatureResourceLoader extends FeatureResourceLoader {
+    private final ResourceMock resourceMock;
+    private Map<String, String> lastAttribs;
+
+    private TestFeatureResourceLoader(ResourceMock resourceMock) {
+      super(null, new TimeSource(), new DefaultFeatureFileSystem());
+      this.resourceMock = resourceMock;
+    }
+
+    @Override
+    public FeatureResource load(Uri uri, Map<String, String> attribs) throws GadgetException {
+      lastAttribs = ImmutableMap.copyOf(attribs);
+      return super.load(uri, attribs);
+    }
+
+    @Override
+    public String getResourceContent(String resource) throws IOException {
+      return resourceMock.get(resource);
+    }
+  }
+
+  private static class ResourceMock {
+    private final Map<String, String> resourceMap;
+
+    private ResourceMock() {
+      this.resourceMap = Maps.newHashMap();
+    }
+
+    private void put(String key, String value) {
+      resourceMap.put(clean(key), value);
+    }
+
+    private String get(String key) throws IOException {
+      key = clean(key);
+      if (!resourceMap.containsKey(key)) {
+        throw new IOException("Missing resource: " + key);
+      }
+      return resourceMap.get(key);
+    }
+
+    private String clean(String key) {
+      // Resource loading doesn't support leading '/'
+      return key.startsWith("/") ? key.substring(1) : key;
+    }
+  }
+
+  // TODO: generalize the below into common classes
+  private static class TestCacheProvider implements CacheProvider {
+    private final Map<String, Cache<?, ?>> caches = new MapMaker().makeMap();
+
+    @SuppressWarnings("unchecked")
+    public <K, V> Cache<K, V> createCache(String name) {
+      Cache<K, V> cache = (Cache<K, V>)caches.get(name);
+      if (cache == null) {
+        cache = new MapCache<K, V>();
+        caches.put(name, cache);
+      }
+      return cache;
+    }
+  }
+
+  private static class MapCache<K, V> implements Cache<K, V> {
+    private final Map<K, V> cache = new MapMaker().makeMap();
+
+    public void addElement(K key, V value) {
+      cache.put(key, value);
+    }
+
+    public long getCapacity() {
+      // Memory-bounded.
+      return Integer.MAX_VALUE;
+    }
+
+    public V getElement(K key) {
+      return cache.get(key);
+    }
+
+    public long getSize() {
+      return cache.size();
+    }
+
+    public V removeElement(K key) {
+      return cache.get(key);
+    }
+
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/AbstractHttpCache.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/AbstractHttpCache.java
new file mode 100644
index 0000000..6ee823a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/AbstractHttpCache.java
@@ -0,0 +1,318 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.uri.UriCommon;
+
+/**
+ * Base class for content caches. Defines cache expiration rules and
+ * and restrictions on allowed content.
+ *
+ * Note that in the case where refetchStrictNoCacheAfterMs is non-negative, strict no-cache
+ * resources are stored in the cache. In this case, only the Cache-Control/Pragma headers are stored
+ * and not the original content or other headers.
+ *
+ * This is used primarily for automatic fetches internal to shindig from triggering lots of
+ * back end fetches. Especially comes to play for fetch in CacheEnforcementVisitor. Now since this
+ * response is not usable for serving the content, we need to explicitly check if the content is
+ * strictNoCache. DefaultRequestPipeline does this correctly, and any implementation of
+ * RequestPipeline should do this as well. To prevent breakages for existing implementations, we
+ * are keeping the default value to -1.
+ *
+ * Example:
+ * GET /private.html \r\n
+ * Host: www.example.com \r\n
+ * Cache-Control: private, max-age=1000 \r\n
+ * \r\n
+ * My credit card number is 1234-5678-1234-5678
+ *
+ * And with refetch-after=3000, then we store the response as:
+ * GET /private.html \r\n
+ * Host: www.example.com \r\n
+ * Cache-Control: private, max-age=1000 \r\n
+ * \r\n
+ *
+ * For non user facing requests, www.example.com/private.html is considered as private and will not
+ * be refetched before 3000ms.
+ *
+ * For user facing requests, response.isStale() is always true, and will be fetched even before
+ * 1000ms. The max-age=1000 is completely ignored by shindig, because that value is for
+ * the client browser and not for proxies.
+ *
+ * isStale() = false always, for all time >= 0
+ * shouldRefetch() = false when time < date + 3000ms
+ * shouldRefetch() = true when time >= date + 3000ms
+ *
+ * Note that error cases are handled differently. (Even for strict no cache)
+ *
+ * Implementations that override this are discouraged from using custom cache keys unless there is
+ * actually customization in the request object itself. It is highly recommended that you still
+ * use {@link #createKey} in the base class and append any custom data to the end of the key instead
+ * of building your own keys from scratch.
+ */
+public abstract class AbstractHttpCache implements HttpCache {
+  private static final String RESIZE_HEIGHT = UriCommon.Param.RESIZE_HEIGHT.getKey();
+  private static final String RESIZE_WIDTH = UriCommon.Param.RESIZE_WIDTH.getKey();
+  private static final String RESIZE_QUALITY = UriCommon.Param.RESIZE_QUALITY.getKey();
+  private static final String NO_EXPAND = UriCommon.Param.NO_EXPAND.getKey();
+
+  // Amount of time after which the entry in cache should be considered for a refetch for a
+  // non-userfacing internal fetch when the response is strict-no-cache.
+  @Inject(optional = true) @Named("shindig.cache.http.strict-no-cache-resource.refetch-after-ms")
+  public static long REFETCH_STRICT_NO_CACHE_AFTER_MS_DEFAULT = -1L;
+
+  private long refetchStrictNoCacheAfterMs = REFETCH_STRICT_NO_CACHE_AFTER_MS_DEFAULT;
+
+  // Implement these methods to create a concrete HttpCache class.
+  protected abstract HttpResponse getResponseImpl(String key);
+  protected abstract void addResponseImpl(String key, HttpResponse response);
+  protected abstract void removeResponseImpl(String key);
+
+  public HttpResponse getResponse(HttpRequest request) {
+    if (isCacheable(request)) {
+      String keyString = createKey(request);
+      HttpResponse cached = getResponseImpl(keyString);
+      if (responseStillUsable(cached) &&
+          (!cached.isStrictNoCache() || refetchStrictNoCacheAfterMs >= 0)) {
+        return cached;
+      }
+    }
+    return null;
+  }
+
+  public HttpResponse addResponse(HttpRequest request, HttpResponse response) {
+    HttpResponseBuilder responseBuilder;
+    boolean storeStrictNoCacheResources = (refetchStrictNoCacheAfterMs >= 0);
+    if (isCacheable(request, response, storeStrictNoCacheResources)) {
+      if (storeStrictNoCacheResources && response.isStrictNoCache()) {
+        responseBuilder = buildStrictNoCacheHttpResponse(response);
+      } else {
+        responseBuilder = new HttpResponseBuilder(response);
+      }
+    } else {
+      return null;
+    }
+    int forcedTtl = request.getCacheTtl();
+    if (forcedTtl != -1 && !response.isError()) {
+      responseBuilder.setCacheTtl(forcedTtl);
+    }
+    response = responseBuilder.create();
+    String keyString = createKey(request);
+    addResponseImpl(keyString, response);
+    return response; // cached and possibly modified
+  }
+
+  @VisibleForTesting
+  public void setRefetchStrictNoCacheAfterMs(long refetchStrictNoCacheAfterMs) {
+    this.refetchStrictNoCacheAfterMs = refetchStrictNoCacheAfterMs;
+  }
+
+  @VisibleForTesting
+  HttpResponseBuilder buildStrictNoCacheHttpResponse(HttpResponse response) {
+    HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    copyHeaderIfPresent("Cache-Control", response, responseBuilder);
+    copyHeaderIfPresent("Pragma", response, responseBuilder);
+    responseBuilder.setRefetchStrictNoCacheAfterMs(refetchStrictNoCacheAfterMs);
+    return responseBuilder;
+  }
+
+  /**
+   * Copy the specified header from response into builder if it exists.
+   */
+  private void copyHeaderIfPresent(String header,
+                                   HttpResponse response,
+                                   HttpResponseBuilder builder) {
+    String headerValue = response.getHeader(header);
+    if (headerValue != null) {
+      builder.setHeader(header, headerValue);
+    }
+  }
+
+  public HttpResponse removeResponse(HttpRequest request) {
+    String keyString = createKey(request);
+    HttpResponse response = getResponseImpl(keyString);
+    removeResponseImpl(keyString);
+    if (responseStillUsable(response)) {
+      return response;
+    }
+    return null;
+  }
+
+  protected boolean isCacheable(HttpRequest request) {
+    if (request.getIgnoreCache()) {
+      return false;
+    }
+    return ("GET".equals(request.getMethod()) ||
+            "GET".equals(request.getHeader("X-Method-Override")));
+  }
+
+  protected boolean isCacheable(HttpRequest request, HttpResponse response,
+                                boolean allowStrictNoCacheResponses) {
+    if (!isCacheable(request)) {
+      return false;
+    }
+
+    if (request.getCacheTtl() != -1) {
+      // Caching was forced. Ignore what the response wants.
+      return true;
+    }
+
+    if (response.getHttpStatusCode() == HttpResponse.SC_NOT_MODIFIED) {
+      // Shindig server will serve 304s. Do not cache 304s from the origin server.
+      return false;
+    }
+
+    // If we allow strict no-cache responses or the HTTP response allows for it, we can cache.
+    return allowStrictNoCacheResponses || !response.isStrictNoCache();
+  }
+
+  /**
+   * Produce a key from the given request.
+   *
+   * Relevant pieces of the cache key:
+   *
+   * - request URI
+   * - authentication type
+   * - owner id
+   * - viewer id
+   * - owner of the token
+   * - gadget url (from security token; we don't trust what's on the URI itself)
+   * - instance id
+   * - oauth service name
+   * - oauth token name
+   * - the resize height parameter
+   * - the resize width parameter
+   * - the resize quality parameter
+   * - the no_expand parameter
+   * - the User-Agent request header
+   *
+   * Except for the first two, all of these may be unset or <code>null</code>,
+   * depending on authentication rules. See individual methods for details.  New cache key items
+   * should always be inserted using {@code CacheKeyBuilder#setParam(String, Object)}.
+   */
+  public String createKey(HttpRequest request) {
+    if ((request.getAuthType() != AuthType.NONE) &&
+        (request.getSecurityToken() == null)) {
+      throw new IllegalArgumentException(
+          "Cannot sign request without security token: [" + request + ']');
+    }
+
+    CacheKeyBuilder keyBuilder = new CacheKeyBuilder()
+        .setLegacyParam(0, request.getUri())
+        .setLegacyParam(1, request.getAuthType())
+        .setLegacyParam(2, getOwnerId(request))
+        .setLegacyParam(3, getViewerId(request))
+        .setLegacyParam(4, getTokenOwner(request))
+        .setLegacyParam(5, getAppUrl(request))
+        .setLegacyParam(6, getInstanceId(request))
+        .setLegacyParam(7, getServiceName(request))
+        .setLegacyParam(8, getTokenName(request))
+        .setParam("rh", request.getParam(RESIZE_HEIGHT))
+        .setParam("rw", request.getParam(RESIZE_WIDTH))
+        .setParam("rq", request.getParam(RESIZE_QUALITY))
+        .setParam("ne", request.getParam(NO_EXPAND))
+        .setParam("rm", request.getRewriteMimeType())
+        .setParam("ua", request.getHeader("User-Agent"));
+    return keyBuilder.build();
+  }
+
+  protected static String getOwnerId(HttpRequest request) {
+    if (request.getAuthType() != AuthType.NONE && request.getAuthType() != AuthType.OAUTH2
+        && request.getOAuthArguments().getSignOwner()) {
+      Preconditions.checkState(request.getSecurityToken() != null, "No Security Token set for request");
+      String ownerId = request.getSecurityToken().getOwnerId();
+      return Objects.firstNonNull(ownerId, "");
+    }
+    // Requests that don't use authentication can share the result.
+    return null;
+  }
+
+  protected static String getViewerId(HttpRequest request) {
+    if (request.getAuthType() != AuthType.NONE && request.getAuthType() != AuthType.OAUTH2
+        && request.getOAuthArguments().getSignViewer()) {
+      Preconditions.checkState(request.getSecurityToken() != null, "No Security Token set for request");
+      String viewerId = request.getSecurityToken().getViewerId();
+      return Objects.firstNonNull(viewerId, "");
+    }
+    // Requests that don't use authentication can share the result.
+    return null;
+  }
+
+  protected static String getTokenOwner(HttpRequest request) {
+    SecurityToken st = request.getSecurityToken();
+    if (request.getAuthType() != AuthType.NONE && request.getAuthType() != AuthType.OAUTH2
+        && st.getOwnerId() != null
+        && st.getOwnerId().equals(st.getViewerId())
+        && request.getOAuthArguments().mayUseToken()) {
+      return st.getOwnerId();
+    }
+    // Requests that don't use authentication can share the result.
+    return null;
+  }
+
+  protected static String getAppUrl(HttpRequest request) {
+    if (request.getAuthType() != AuthType.NONE) {
+      return request.getSecurityToken().getAppUrl();
+    }
+    // Requests that don't use authentication can share the result.
+    return null;
+  }
+
+  protected static String getInstanceId(HttpRequest request) {
+    if (request.getAuthType() != AuthType.NONE) {
+      return Long.toString(request.getSecurityToken().getModuleId());
+    }
+    // Requests that don't use authentication can share the result.
+    return null;
+  }
+
+  protected static String getServiceName(HttpRequest request) {
+    if ((request.getAuthType() != AuthType.NONE) && (request.getAuthType() != AuthType.OAUTH2)) {
+      return request.getOAuthArguments().getServiceName();
+    }
+    // Requests that don't use authentication can share the result.
+    return null;
+  }
+
+  protected static String getTokenName(HttpRequest request) {
+    if ((request.getAuthType() != AuthType.NONE) && (request.getAuthType() != AuthType.OAUTH2)) {
+      return request.getOAuthArguments().getTokenName();
+    }
+    // Requests that don't use authentication can share the result.
+    return null;
+  }
+
+  /**
+   * Utility function to verify that an entry is usable
+   * The cache still serve staled data, it is the responsible of the user
+   * to decide if to use it or not (use isStale).
+   * @return true If the response can be used.
+   */
+  protected boolean responseStillUsable(HttpResponse response) {
+    return response != null;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/BasicHttpFetcher.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/BasicHttpFetcher.java
new file mode 100644
index 0000000..5e66b9d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/BasicHttpFetcher.java
@@ -0,0 +1,552 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.Header;
+import org.apache.http.HeaderElement;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.HttpVersion;
+import org.apache.http.NoHttpResponseException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.params.ClientPNames;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.client.protocol.RequestAddCookies;
+import org.apache.http.client.protocol.ResponseProcessCookies;
+import org.apache.http.conn.ConnectionPoolTimeoutException;
+import org.apache.http.conn.HttpHostConnectException;
+import org.apache.http.conn.params.ConnPerRouteBean;
+import org.apache.http.conn.params.ConnRouteParams;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.entity.HttpEntityWrapper;
+import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpConnectionParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.ByteArrayBuffer;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.ProxySelector;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+// Temporary replacement of javax.annotation.Nullable
+import org.apache.shindig.common.Nullable;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A simple HTTP fetcher implementation based on Apache httpclient. Not recommended for production deployments until
+ * the following issues are addressed:
+ * <p/>
+ * 1. This class potentially allows access to resources behind an organization's firewall.
+ * 2. This class does not enforce any limits on what is fetched from remote hosts.
+ */
+@Singleton
+public class BasicHttpFetcher implements HttpFetcher {
+  private static final int DEFAULT_CONNECT_TIMEOUT_MS = 5000;
+  private static final int DEFAULT_READ_TIMEOUT_MS = 5000;
+  private static final int DEFAULT_MAX_OBJECT_SIZE = 0;  // no limit
+  private static final long DEFAULT_SLOW_RESPONSE_WARNING = 10000;
+
+  protected final HttpClient FETCHER;
+
+  // mutable fields must be volatile
+  private volatile int maxObjSize;
+  private volatile long slowResponseWarning;
+
+  //class name for logging purpose
+  private static final String classname = BasicHttpFetcher.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final Set<Class<?>> TIMEOUT_EXCEPTIONS = ImmutableSet.<Class<?>>of(ConnectionPoolTimeoutException.class,
+      SocketTimeoutException.class, SocketException.class, HttpHostConnectException.class, NoHttpResponseException.class,
+      InterruptedException.class, UnknownHostException.class);
+
+  /**
+   * Creates a new fetcher using the default maximum object size and timeout --
+   * no limit and 5 seconds.
+   * @param basicHttpFetcherProxy The http proxy to use.
+   */
+  @Inject
+  public BasicHttpFetcher(@Nullable @Named("org.apache.shindig.gadgets.http.basicHttpFetcherProxy")
+                          String basicHttpFetcherProxy) {
+    this(DEFAULT_MAX_OBJECT_SIZE, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_READ_TIMEOUT_MS,
+         basicHttpFetcherProxy);
+  }
+
+  /**
+   * Creates a new fetcher for fetching HTTP objects.  Not really suitable
+   * for production use. Use of an HTTP proxy for security is also necessary
+   * for production deployment.
+   *
+   * @param maxObjSize          Maximum size, in bytes, of the object we will fetch, 0 if no limit..
+   * @param connectionTimeoutMs timeout, in milliseconds, for connecting to hosts.
+   * @param readTimeoutMs       timeout, in millseconds, for unresponsive connections
+   * @param basicHttpFetcherProxy The http proxy to use.
+   */
+  public BasicHttpFetcher(int maxObjSize, int connectionTimeoutMs, int readTimeoutMs,
+                          String basicHttpFetcherProxy) {
+    // Create and initialize HTTP parameters
+    setMaxObjectSizeBytes(maxObjSize);
+    setSlowResponseWarning(DEFAULT_SLOW_RESPONSE_WARNING);
+
+    HttpParams params = new BasicHttpParams();
+
+    HttpConnectionParams.setConnectionTimeout(params, connectionTimeoutMs);
+
+    HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
+    HttpProtocolParams.setUserAgent(params, "Apache Shindig");
+    HttpProtocolParams.setContentCharset(params, "UTF-8");
+
+    HttpConnectionParams.setConnectionTimeout(params, connectionTimeoutMs);
+    HttpConnectionParams.setSoTimeout(params, readTimeoutMs);
+    HttpConnectionParams.setStaleCheckingEnabled(params, true);
+    HttpConnectionParams.setSoReuseaddr(params, true);
+
+    HttpClientParams.setRedirecting(params, true);
+    HttpClientParams.setAuthenticating(params, false);
+
+    // Create and initialize scheme registry
+    SchemeRegistry schemeRegistry = new SchemeRegistry();
+    schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));
+    schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory()));
+
+    ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(schemeRegistry);
+    // These are probably overkill for most sites.
+    cm.setMaxTotal(1152);
+    cm.setDefaultMaxPerRoute(256);
+
+    DefaultHttpClient client = new DefaultHttpClient(cm, params);
+
+    // Set proxy if set via guice.
+    if (!Strings.isNullOrEmpty(basicHttpFetcherProxy)) {
+      String[] splits = StringUtils.split(basicHttpFetcherProxy, ':');
+      ConnRouteParams.setDefaultProxy(
+          client.getParams(), new HttpHost(splits[0], Integer.parseInt(splits[1]), "http"));
+    }
+
+    // try resending the request once
+    client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(1, true));
+
+    // Add hooks for gzip/deflate
+    client.addRequestInterceptor(new HttpRequestInterceptor() {
+      public void process(
+          final org.apache.http.HttpRequest request,
+          final HttpContext context) throws HttpException, IOException {
+        if (!request.containsHeader("Accept-Encoding")) {
+          request.addHeader("Accept-Encoding", "gzip, deflate");
+        }
+      }
+    });
+    client.addResponseInterceptor(new HttpResponseInterceptor() {
+      public void process(
+          final org.apache.http.HttpResponse response,
+          final HttpContext context) throws HttpException, IOException {
+        HttpEntity entity = response.getEntity();
+        if (entity != null) {
+          Header ceheader = entity.getContentEncoding();
+          if (ceheader != null) {
+            for (HeaderElement codec : ceheader.getElements()) {
+              String codecname = codec.getName();
+              if ("gzip".equalsIgnoreCase(codecname)) {
+                response.setEntity(
+                    new GzipDecompressingEntity(response.getEntity()));
+                return;
+              } else if ("deflate".equals(codecname)) {
+                response.setEntity(new DeflateDecompressingEntity(response.getEntity()));
+                return;
+              }
+            }
+          }
+        }
+      }
+    });
+
+    // Disable automatic storage and sending of cookies (see SHINDIG-1382)
+    client.removeRequestInterceptorByClass(RequestAddCookies.class);
+    client.removeResponseInterceptorByClass(ResponseProcessCookies.class);
+
+    // Use Java's built-in proxy logic in case no proxy set via guice.
+    if (Strings.isNullOrEmpty(basicHttpFetcherProxy)) {
+      ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(
+          client.getConnectionManager().getSchemeRegistry(),
+          ProxySelector.getDefault());
+      client.setRoutePlanner(routePlanner);
+    }
+
+    FETCHER = client;
+  }
+
+  static class GzipDecompressingEntity extends HttpEntityWrapper {
+    public GzipDecompressingEntity(final HttpEntity entity) {
+      super(entity);
+    }
+
+    @Override
+    public InputStream getContent() throws IOException, IllegalStateException {
+      // the wrapped entity's getContent() decides about repeatability
+      InputStream wrappedin = wrappedEntity.getContent();
+
+      return new GZIPInputStream(wrappedin);
+    }
+
+    @Override
+    public long getContentLength() {
+      // length of ungzipped content is not known
+      return -1;
+    }
+  }
+
+  static class DeflateDecompressingEntity extends HttpEntityWrapper {
+    public DeflateDecompressingEntity(final HttpEntity entity) {
+      super(entity);
+    }
+
+    @Override
+    public InputStream getContent()
+        throws IOException, IllegalStateException {
+
+      // the wrapped entity's getContent() decides about repeatability
+      InputStream wrappedin = wrappedEntity.getContent();
+
+      return new InflaterInputStream(wrappedin, new Inflater(true));
+    }
+
+    @Override
+    public long getContentLength() {
+      // length of ungzipped content is not known
+      return -1;
+    }
+  }
+
+  public HttpResponse fetch(org.apache.shindig.gadgets.http.HttpRequest request)
+      throws GadgetException {
+    HttpUriRequest httpMethod = null;
+    Preconditions.checkNotNull(request);
+    final String methodType = request.getMethod();
+
+    final org.apache.http.HttpResponse response;
+    final long started = System.currentTimeMillis();
+
+    // Break the request Uri to its components:
+    Uri uri = request.getUri();
+    if (Strings.isNullOrEmpty(uri.getAuthority())) {
+      throw new GadgetException(GadgetException.Code.INVALID_USER_DATA,
+          "Missing domain name for request: " + uri,
+          HttpServletResponse.SC_BAD_REQUEST);
+    }
+    if (Strings.isNullOrEmpty(uri.getScheme())) {
+      throw new GadgetException(GadgetException.Code.INVALID_USER_DATA,
+          "Missing schema for request: " + uri,
+          HttpServletResponse.SC_BAD_REQUEST);
+    }
+    String[] hostparts = StringUtils.splitPreserveAllTokens(uri.getAuthority(),':');
+    int port = -1; // default port
+    if (hostparts.length > 2) {
+      throw new GadgetException(GadgetException.Code.INVALID_USER_DATA,
+          "Bad host name in request: " + uri.getAuthority(),
+          HttpServletResponse.SC_BAD_REQUEST);
+    }
+    if (hostparts.length == 2) {
+      try {
+        port = Integer.parseInt(hostparts[1]);
+      } catch (NumberFormatException e) {
+        throw new GadgetException(GadgetException.Code.INVALID_USER_DATA,
+            "Bad port number in request: " + uri.getAuthority(),
+            HttpServletResponse.SC_BAD_REQUEST);
+      }
+    }
+
+    String requestUri = uri.getPath();
+    // Treat path as / if set as null.
+    if (uri.getPath() == null) {
+      requestUri = "/";
+    }
+    if (uri.getQuery() != null) {
+      requestUri += '?' + uri.getQuery();
+    }
+
+    // Get the http host to connect to.
+    HttpHost host = new HttpHost(hostparts[0], port, uri.getScheme());
+
+    try {
+      if ("POST".equals(methodType) || "PUT".equals(methodType)) {
+        HttpEntityEnclosingRequestBase enclosingMethod = ("POST".equals(methodType))
+          ? new HttpPost(requestUri)
+          : new HttpPut(requestUri);
+
+        if (request.getPostBodyLength() > 0) {
+          enclosingMethod.setEntity(new InputStreamEntity(request.getPostBody(), request.getPostBodyLength()));
+        }
+        httpMethod = enclosingMethod;
+      } else if ("GET".equals(methodType)) {
+        httpMethod = new HttpGet(requestUri);
+      } else if ("HEAD".equals(methodType)) {
+        httpMethod = new HttpHead(requestUri);
+      } else if ("DELETE".equals(methodType)) {
+        httpMethod = new HttpDelete(requestUri);
+      }
+      for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
+        httpMethod.addHeader(entry.getKey(), Joiner.on(',').join(entry.getValue()));
+      }
+
+      // Disable following redirects.
+      if (!request.getFollowRedirects()) {
+        httpMethod.getParams().setBooleanParameter(ClientPNames.HANDLE_REDIRECTS, false);
+      }
+
+      // HttpClient doesn't handle all cases when breaking url (specifically '_' in domain)
+      // So lets pass it the url parsed:
+      response = FETCHER.execute(host, httpMethod);
+
+      if (response == null) {
+        throw new IOException("Unknown problem with request");
+      }
+
+      long now = System.currentTimeMillis();
+      if (now - started > slowResponseWarning) {
+        slowResponseWarning(request, started, now);
+      }
+
+      return makeResponse(response);
+
+    } catch (Exception e) {
+      long now = System.currentTimeMillis();
+
+      // Find timeout exceptions, respond accordingly
+      if (TIMEOUT_EXCEPTIONS.contains(e.getClass())) {
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "fetch", MessageKeys.TIMEOUT_EXCEPTION, new Object[] {request.getUri(),classname,e.getMessage(),now-started});
+        }
+        return HttpResponse.timeout();
+      }
+      if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "fetch", MessageKeys.EXCEPTION_OCCURRED, new Object[] {request.getUri(),now-started});
+          LOG.logp(Level.INFO, classname, "fetch", "", e);
+      }
+      // Separate shindig error from external error
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e,
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    } finally {
+      // cleanup any outstanding resources..
+      if (httpMethod != null) try {
+        httpMethod.abort();
+      } catch (UnsupportedOperationException e) {
+        // ignore
+      }
+    }
+  }
+
+  /**
+   * Called when a request takes too long.   Consider subclassing this if you want to do something other than logging
+   * a warning .
+   *
+   * @param request the request that generated the slowrequest
+   * @param started  the time the request started, in milliseconds.
+   * @param finished the time the request finished, in milliseconds.
+   */
+  protected void slowResponseWarning(HttpRequest request, long started, long finished) {
+    if (LOG.isLoggable(Level.WARNING)) {
+      LOG.logp(Level.WARNING, classname, "slowResponseWarning", MessageKeys.SLOW_RESPONSE, new Object[] {request.getUri(),finished-started});
+    }
+  }
+
+  /**
+   * Change the global maximum fetch size (in bytes) for all fetches.
+   *
+   * @param maxObjectSizeBytes value for maximum number of bytes, or 0 for no limit
+   */
+  @Inject(optional = true)
+  public void setMaxObjectSizeBytes(@Named("shindig.http.client.max-object-size-bytes") int maxObjectSizeBytes) {
+    this.maxObjSize = maxObjectSizeBytes;
+  }
+
+  /**
+   * Change the global threshold for warning about slow responses
+   *
+   * @param slowResponseWarning time in milliseconds after we issue a warning
+   */
+
+  @Inject(optional = true)
+  public void setSlowResponseWarning(@Named("shindig.http.client.slow-response-warning") long slowResponseWarning) {
+    this.slowResponseWarning = slowResponseWarning;
+  }
+
+  /**
+   * Change the global connection timeout for all new fetchs.
+   *
+   * @param connectionTimeoutMs new connection timeout in milliseconds
+   */
+  @Inject(optional = true)
+  public void setConnectionTimeoutMs(@Named("shindig.http.client.connection-timeout-ms") int connectionTimeoutMs) {
+    Preconditions.checkArgument(connectionTimeoutMs > 0, "connection-timeout-ms must be greater than 0");
+    FETCHER.getParams().setIntParameter(HttpConnectionParams.CONNECTION_TIMEOUT, connectionTimeoutMs);
+  }
+
+  /**
+   * Change the global read timeout for all new fetchs.
+   *
+   * @param readTimeoutMs new connection timeout in milliseconds
+   */
+  @Inject(optional = true)
+  public void setReadTimeoutMs(@Named("shindig.http.client.read-timeout-ms") int readTimeoutMs) {
+    Preconditions.checkArgument(readTimeoutMs > 0, "read-timeout-ms must be greater than 0");
+    FETCHER.getParams().setIntParameter(HttpConnectionParams.SO_TIMEOUT, readTimeoutMs);
+  }
+
+
+  /**
+   * @param response The response to parse
+   * @return A HttpResponse object made by consuming the response of the
+   *         given HttpMethod.
+   * @throws IOException when problems occur processing the body content
+   */
+  private HttpResponse makeResponse(org.apache.http.HttpResponse response) throws IOException {
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+
+    if (response.getAllHeaders() != null) {
+      for (Header h : response.getAllHeaders()) {
+        if (h.getName() != null)
+          builder.addHeader(h.getName(), h.getValue());
+      }
+    }
+
+    HttpEntity entity = response.getEntity();
+
+    if (maxObjSize > 0 && entity != null && entity.getContentLength() > maxObjSize) {
+      return HttpResponse.badrequest("Exceeded maximum number of bytes - " + maxObjSize);
+    }
+
+    byte[] responseBytes = (entity == null) ? null : toByteArraySafe(entity);
+
+    return builder
+        .setHttpStatusCode(response.getStatusLine().getStatusCode())
+        .setResponse(responseBytes)
+        .create();
+  }
+
+  /**
+   * This method is Safe replica version of org.apache.http.util.EntityUtils.toByteArray.
+   * The try block embedding 'instream.read' has a corresponding catch block for 'EOFException'
+   * (that's Ignored) and all other IOExceptions are let pass.
+   *
+   * @param entity
+   * @return byte array containing the entity content. May be empty/null.
+   * @throws IOException if an error occurs reading the input stream
+   */
+  public byte[] toByteArraySafe(final HttpEntity entity) throws IOException {
+    if (entity == null) {
+      return null;
+    }
+
+    InputStream instream = entity.getContent();
+    if (instream == null) {
+      return ArrayUtils.EMPTY_BYTE_ARRAY;
+    }
+    Preconditions.checkArgument(entity.getContentLength() < Integer.MAX_VALUE, "HTTP entity too large to be buffered in memory");
+
+    // The raw data stream (inside JDK) is read in a buffer of size '512'. The original code
+    // org.apache.http.util.EntityUtils.toByteArray reads the unzipped data in a buffer of
+    // 4096 byte. For any data stream that has a compression ratio lesser than 1/8, this may
+    // result in the buffer/array overflow. Increasing the buffer size to '16384'. It's highly
+    // unlikely to get data compression ratios lesser than 1/32 (3%).
+    final int bufferLength = 16384;
+    int i = (int)entity.getContentLength();
+    if (i < 0) {
+      i = bufferLength;
+    }
+    ByteArrayBuffer buffer = new ByteArrayBuffer(i);
+    try {
+      byte[] tmp = new byte[bufferLength];
+      int l;
+      while((l = instream.read(tmp)) != -1) {
+        buffer.append(tmp, 0, l);
+      }
+    } catch (EOFException eofe) {
+      /**
+       * Ref: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4040920
+       * Due to a bug in JDK ZLIB (InflaterInputStream), unexpected EOF error can occur.
+       * In such cases, even if the input stream is finished reading, the
+       * 'Inflater.finished()' call erroneously returns 'false' and
+       * 'java.util.zip.InflaterInputStream.fill' throws the 'EOFException'.
+       * So for such case, ignore the Exception in case Exception Cause is
+       * 'Unexpected end of ZLIB input stream'.
+       *
+       * Also, ignore this exception in case the exception has no message
+       * body as this is the case where {@link GZIPInputStream#readUByte}
+       * throws EOFException with empty message. A bug has been filed with Sun
+       * and will be mentioned here once it is accepted.
+       */
+      if (instream.available() == 0 &&
+          (eofe.getMessage() == null ||
+           eofe.getMessage().equals("Unexpected end of ZLIB input stream"))) {
+        LOG.log(Level.FINE, "EOFException: ", eofe);
+      } else {
+        throw eofe;
+      }
+    }
+    finally {
+      instream.close();
+    }
+    return buffer.toByteArray();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CacheKeyBuilder.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CacheKeyBuilder.java
new file mode 100644
index 0000000..a366d2b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/CacheKeyBuilder.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import java.util.Map;
+import java.util.SortedMap;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Builds the cache key object.
+ *
+ * <p>Takes extra care to build the cache keys that don't thrash persistent caches.
+ */
+public class CacheKeyBuilder {
+  private static final int NUM_LEGACY_PARAMS = 9;
+  private static final String DEFAULT_KEY_VALUE = "0";
+  private static final char KEY_SEPARATOR = ':';
+
+  /** The legacy parameters that need to appear in the cache key in a particular order. */
+  private Object[] legacyParams;
+
+  /** A sorted parameter map ensures an unique ordering of the hash keys */
+  private SortedMap<String, Object> paramMap;
+
+  public CacheKeyBuilder() {
+    this.paramMap = Maps.newTreeMap();
+    this.legacyParams = new Object[NUM_LEGACY_PARAMS];
+  }
+
+  private String getValueOrDefault(Object value) {
+    if (value == null) {
+      return DEFAULT_KEY_VALUE;
+    }
+    return String.valueOf(value);
+  }
+
+  /**
+   * Sets a legacy cache key parameter.
+   *
+   * <p>The legacy cache key parameters have a fixed order.  Since the cache key is not required
+   * to have all of the legacy parameters filled in, the index of the parameter must be given.
+   *
+   * @param index the index to place this parameter at; valid values are {@code 0} to
+   *        {@link CacheKeyBuilder#NUM_LEGACY_PARAMS - 1}
+   * @param value the object that determines the legacy parameter
+   */
+  public CacheKeyBuilder setLegacyParam(int index, Object value) {
+    legacyParams[index] = value;
+    return this;
+  }
+
+  /**
+   * Sets a cache key parameter.
+   *
+   * <p>Each parameter needs to be inserted in the cache key builder manually, so that
+   * the user has an option to select the parameters that need to become part of the key.
+   *
+   * @param name the parameter name; if <code>null</code>, the param will not be inserted
+   * @param value the object that determines the value of the parameter
+   */
+  public CacheKeyBuilder setParam(String name, Object value) {
+    if (value != null) {
+      paramMap.put(name, String.valueOf(value));
+    }
+    return this;
+  }
+
+  /**
+   * Inserting the keys from the parameter map only if the keys are defined, prevents thrashing
+   * the existing persistent caches at rolling restart of high-traffic servers.
+   * The cache keys of all URIs that don't set these parameters must not change.
+   *
+   * TODO: String parameters that have KEY_SEPARATOR appearing as a part of a string param need
+   * to be escaped (e.g. with a backslash like so: ":" -> "\:") to prevent weird cache key
+   * collisions.  Moreover the active character (backslash) needs to be escaped itself for similar
+   * reasons.
+   */
+  public String build() {
+    StringBuilder keyBuilder = new StringBuilder(2 * String.valueOf(legacyParams[0]).length());
+    appendLegacyKeys(keyBuilder);
+
+    if (!paramMap.isEmpty()) {
+      for (Map.Entry<String, Object> mapEntry : paramMap.entrySet()) {
+        keyBuilder.append(KEY_SEPARATOR);
+        keyBuilder.append(String.format("%s=%s", mapEntry.getKey(), mapEntry.getValue()));
+      }
+    }
+    return keyBuilder.toString();
+  }
+
+  private void appendLegacyKeys(StringBuilder key) {
+    boolean first = true;
+    for (Object legacyParam : legacyParams) {
+      if (!first) {
+        key.append(KEY_SEPARATOR);
+      } else {
+        first = false;
+      }
+      key.append(getValueOrDefault(legacyParam));
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultHttpCache.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultHttpCache.java
new file mode 100644
index 0000000..2e7a24b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultHttpCache.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Simple cache of HttpResponses. It is recommended that this cache be configured with a shared
+ * cache rather than a memory only cache.
+ */
+@Singleton
+public class DefaultHttpCache extends AbstractHttpCache {
+  public static final String CACHE_NAME = "httpResponses";
+
+  private final Cache<String, HttpResponse> cache;
+
+  @Inject
+  public DefaultHttpCache(CacheProvider cacheProvider) {
+    cache = cacheProvider.createCache(CACHE_NAME);
+  }
+
+  @Override
+  protected HttpResponse getResponseImpl(String key) {
+    return cache.getElement(key);
+  }
+
+  @Override
+  protected void addResponseImpl(String key, HttpResponse response) {
+    cache.addElement(key, response);
+  }
+
+  @Override
+  protected void removeResponseImpl(String key) {
+    cache.removeElement(key);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultInvalidationService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultInvalidationService.java
new file mode 100644
index 0000000..40d0b65
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultInvalidationService.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.base.Strings;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+
+import com.google.inject.Inject;
+
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Default implementation of the invalidation service. No security checks are applied when
+ * invalidating Urls. Invalidation marks are added to HttpResponse objects which are then cached.
+ * We do an exact equality check on the invalidation marks rather than trying to do something
+ * timestamp based.
+ *
+ * This implementation uses an invalidate-on-read technique. HttpResponses are 'marked' with a
+ * globally unique sequence value assigned to the users in who's context the signed/oauth request
+ * was made. When that same request is repeated later we ensure that the mark on the cached response
+ * is consistent with the current mark for the request.
+ *
+ * When the content for a user is invalidated a new unique sequence value is assigned to them and
+ * all the marks on cached content associated with that user will become invalid.
+ *
+ * This technique is reliable if the lifetime of the HttpCache is tied to the invalidation cache
+ * and when the invalidation cache is canonical. A non-canonical invalidation cache can be used
+ * but cached responses must become invalid if an invalidation entry is missing.
+ */
+public class DefaultInvalidationService implements InvalidationService {
+
+  public static final String CACHE_NAME = "invalidatedUsers";
+
+  private final HttpCache httpCache;
+  protected final Cache<String,Long> invalidationEntries;
+  private final AtomicLong marker;
+
+  private static final String TOKEN_PREFIX = "INV_TOK:";
+
+  @Inject
+  public DefaultInvalidationService(HttpCache httpCache, CacheProvider cacheProvider) {
+    // Initialize to current time to mimimize conflict with persistent caches
+    this(httpCache, cacheProvider, new AtomicLong(System.currentTimeMillis()));
+  }
+
+  DefaultInvalidationService(HttpCache httpCache, CacheProvider cacheProvider, AtomicLong marker) {
+    this.httpCache = httpCache;
+    invalidationEntries = cacheProvider.createCache(CACHE_NAME);
+    this.marker = marker;
+  }
+
+  public void invalidateApplicationResources(Set<Uri> uris, SecurityToken token) {
+    // TODO Add checks on content
+    for (Uri uri : uris) {
+      httpCache.removeResponse(new HttpRequest(uri));
+    }
+  }
+
+  /**
+   * Invalidate all fetched content that was signed on behalf of the specified set of users.
+   *
+   * @param opensocialIds
+   * @param token
+   */
+  public void invalidateUserResources(Set<String> opensocialIds, SecurityToken token) {
+    for (String userId : opensocialIds) {
+      // Allocate a new mark for each user
+      invalidationEntries.addElement(getKey(userId, token), marker.incrementAndGet());
+    }
+  }
+
+  public boolean isValid(HttpRequest request, HttpResponse response) {
+    if (request.getAuthType() == AuthType.NONE) {
+      // Always valid for unauthenticated requests
+      return true;
+    }
+    String invalidationHeader = response.getHeader(INVALIDATION_HEADER);
+    if (invalidationHeader == null) {
+      invalidationHeader = "";
+    }
+    return invalidationHeader.equals(getInvalidationMark(request));
+  }
+
+  public HttpResponse markResponse(HttpRequest request, HttpResponse response) {
+    if (request.getAuthType() == AuthType.NONE) {
+      return response;
+    }
+    String mark = getInvalidationMark(request);
+    if (mark.length() > 0) {
+      return new HttpResponseBuilder(response).setHeader(INVALIDATION_HEADER, mark).create();
+    }
+    return response;
+  }
+
+  /**
+   * Get the invalidation entry key for a user in the scope of a given
+   * application
+   */
+  private String getKey(String userId, SecurityToken token) {
+    // Convert the id to the container relative form
+    int colonIndex = userId.lastIndexOf(':');
+    if (colonIndex != -1) {
+      userId = userId.substring(colonIndex + 1);
+    }
+
+    // Assume the container is consistent in its use of either appId or appUrl.
+    // Use appId
+    if (!Strings.isNullOrEmpty(token.getAppId())) {
+      return TOKEN_PREFIX + token.getAppId() + ':' + userId;
+    }
+    return TOKEN_PREFIX + token.getAppUrl() + ':' + userId;
+  }
+
+  /**
+   * Calculate the invalidation mark for a request
+   */
+  private String getInvalidationMark(HttpRequest request) {
+    StringBuilder currentInvalidation = new StringBuilder();
+
+    Long ownerStamp = null;
+    if (request.getOAuthArguments() != null && request.getOAuthArguments().getSignOwner()) {
+      String ownerKey = getKey(request.getSecurityToken().getOwnerId(), request.getSecurityToken());
+      ownerStamp = invalidationEntries.getElement(ownerKey);
+    }
+    Long viewerStamp = null;
+    if (request.getOAuthArguments() != null && request.getOAuthArguments().getSignViewer()) {
+      if (ownerStamp != null &&
+          request.getSecurityToken().getOwnerId().equals(
+              request.getSecurityToken().getViewerId())) {
+        viewerStamp = ownerStamp;
+      } else {
+        String viewerKey = getKey(request.getSecurityToken().getViewerId(),
+            request.getSecurityToken());
+        viewerStamp = invalidationEntries.getElement(viewerKey);
+      }
+    }
+    if (ownerStamp != null) {
+      currentInvalidation.append("o=").append(ownerStamp).append(';');
+    }
+    if (viewerStamp != null) {
+      currentInvalidation.append("v=").append(viewerStamp).append(';');
+    }
+    return currentInvalidation.toString();
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java
new file mode 100644
index 0000000..ca5f353
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/DefaultRequestPipeline.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.collect.Multimap;
+import com.google.common.net.HttpHeaders;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.oauth.OAuthRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Request;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A standard implementation of a request pipeline. Performs request caching and
+ * signing on top of standard HTTP requests.
+ */
+@Singleton
+public class DefaultRequestPipeline implements RequestPipeline {
+  private final HttpFetcher httpFetcher;
+  private final HttpCache httpCache;
+  private final Provider<OAuthRequest> oauthRequestProvider;
+  private final Provider<OAuth2Request> oauth2RequestProvider;
+  private final ResponseRewriterRegistry responseRewriterRegistry;
+  private final InvalidationService invalidationService;
+  private final HttpResponseMetadataHelper metadataHelper;
+
+  // At what point you don't trust remote server date stamp on response (in milliseconds)
+  // (Should be less then DEFAULT_TTL)
+  static final long DEFAULT_DRIFT_LIMIT_MS = 3L * 60L * 1000L;
+
+  @Inject(optional = true) @Named("shindig.http.date-drift-limit-ms")
+  private static long responseDateDriftLimit = DEFAULT_DRIFT_LIMIT_MS;
+
+  //class name for logging purpose
+  private static final String classname = DefaultRequestPipeline.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  @Inject
+  public DefaultRequestPipeline(HttpFetcher httpFetcher,
+                                HttpCache httpCache,
+                                Provider<OAuthRequest> oauthRequestProvider,
+                                Provider<OAuth2Request> oauth2RequestProvider,
+                                @RewriterRegistry(rewriteFlow = RewriteFlow.REQUEST_PIPELINE)
+                                ResponseRewriterRegistry responseRewriterRegistry,
+                                InvalidationService invalidationService,
+                                @Nullable HttpResponseMetadataHelper metadataHelper) {
+    this.httpFetcher = httpFetcher;
+    this.httpCache = httpCache;
+    this.oauthRequestProvider = oauthRequestProvider;
+    this.oauth2RequestProvider = oauth2RequestProvider;
+    this.responseRewriterRegistry = responseRewriterRegistry;
+    this.invalidationService = invalidationService;
+    this.metadataHelper = metadataHelper;
+  }
+
+  public HttpResponse execute(HttpRequest request) throws GadgetException {
+    final String method = "execute";
+    normalizeProtocol(request);
+
+    HttpResponse cachedResponse = checkCachedResponse(request);
+
+    HttpResponse invalidatedResponse = null;
+    HttpResponse staleResponse = null;
+
+    // Note that we don't remove invalidated entries from the cache as we want them to be
+    // available in the event of a backend fetch failure.
+    // Note that strict no-cache entries are dummy responses and should not be used.
+    if (cachedResponse != null && !cachedResponse.isStrictNoCache()) {
+      if (!cachedResponse.isStale()) {
+        if (invalidationService.isValid(request, cachedResponse)) {
+          if(LOG.isLoggable(Level.FINEST)) {
+            LOG.logp(Level.FINEST, classname, method, MessageKeys.CACHED_RESPONSE,
+                    new Object[]{request.getUri().toString()});
+          }
+          return cachedResponse;
+        } else {
+          invalidatedResponse = cachedResponse;
+        }
+      } else {
+        if (!cachedResponse.isError()) {
+          // Remember good but stale cached response, to be served if server unavailable
+          staleResponse = cachedResponse;
+        }
+      }
+    }
+
+    // If we have a stale response, perform a conditional GET.
+    // Note: Fixing up the request with these headers will not affect http response caching. See
+    // org.apache.shindig.gadgets.http.AbstractHttpCache.createKey(HttpRequest)
+    if (staleResponse != null) {
+      final String lastModified = staleResponse.getHeader(HttpHeaders.LAST_MODIFIED);
+      if (lastModified != null) {
+        request.setHeader(HttpHeaders.IF_MODIFIED_SINCE, lastModified);
+      }
+
+      final String etag = staleResponse.getHeader(HttpHeaders.ETAG);
+      if (etag != null) {
+        request.setHeader(HttpHeaders.IF_NONE_MATCH, etag);
+      }
+    }
+
+    HttpResponse fetchedResponse = fetchResponse(request);
+    fetchedResponse = fixFetchedResponse(request, fetchedResponse, invalidatedResponse,
+        staleResponse);
+    return fetchedResponse;
+  }
+
+  /**
+   * Normalizing the HttpRequest object to verify the validity of the request.
+   *
+   * @param request
+   * @throws GadgetException
+   */
+  protected void normalizeProtocol(HttpRequest request) throws GadgetException {
+    // Normalize the protocol part of the URI
+    if (request.getUri().getScheme()== null) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "Url " + request.getUri().toString() + " does not include scheme",
+          HttpResponse.SC_BAD_REQUEST);
+    } else if (!"http".equals(request.getUri().getScheme()) &&
+        !"https".equals(request.getUri().getScheme())) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "Invalid request url scheme in url: " + Utf8UrlCoder.encode(request.getUri().toString()) +
+            "; only \"http\" and \"https\" supported.",
+            HttpResponse.SC_BAD_REQUEST);
+    }
+  }
+
+  /**
+   * Check the HttpRequest object has already allow caching and if do try to get it from cache.
+   * Override this if you want to add additional logic to get response for the request from cache.
+   *
+   * @param request
+   * @return HttpResponse object which either the cached response or null
+   */
+  protected HttpResponse checkCachedResponse(HttpRequest request) {
+    HttpResponse cachedResponse;
+    if (!request.getIgnoreCache()) {
+      cachedResponse = httpCache.getResponse(request);
+    } else {
+      cachedResponse = null;
+    }
+    return cachedResponse;
+  }
+
+  /**
+   * Fetch the response from the network using the right fetcher
+   * Override this if you need to extend the current behavior of supported auth type.
+   *
+   * @param request
+   * @return  HttpResponse object fetched from network
+   * @throws GadgetException
+   */
+  protected HttpResponse fetchResponse(HttpRequest request) throws GadgetException {
+    HttpResponse fetchedResponse;
+    switch (request.getAuthType()) {
+      case NONE:
+        fetchedResponse = httpFetcher.fetch(request);
+        break;
+      case SIGNED:
+      case OAUTH:
+        fetchedResponse = oauthRequestProvider.get().fetch(request);
+        break;
+      case OAUTH2:
+        fetchedResponse = oauth2RequestProvider.get().fetch(request);
+        break;
+      default:
+        return HttpResponse.error();
+    }
+    return fetchedResponse;
+  }
+
+  /**
+   * Attempt to "fix" the response by checking its validity and adding additional metadata
+   * Override this if you need to add more processing to the HttpResponse before being cached and
+   * returned.
+   *
+   * @param request
+   * @param fetchedResponse
+   * @param invalidatedResponse
+   * @param staleResponse
+   * @return HttpResponse object that has been updated with some metadata tags.
+   * @throws GadgetException
+   */
+  protected HttpResponse fixFetchedResponse(HttpRequest request, HttpResponse fetchedResponse,
+      @Nullable HttpResponse invalidatedResponse, @Nullable HttpResponse staleResponse)
+      throws GadgetException {
+    final String method = "fixFetchedResponse";
+    if (fetchedResponse.isError() && invalidatedResponse != null) {
+      // Use the invalidated cached response if it is not stale. We don't update its
+      // mark so it remains invalidated
+      return invalidatedResponse;
+    }
+
+    if (fetchedResponse.getHttpStatusCode() >= 500 && staleResponse != null) {
+      // If we have trouble accessing the remote server,
+      // Lets try the latest good but staled result
+      if(LOG.isLoggable(Level.FINEST)) {
+        LOG.logp(Level.FINEST, classname, method, MessageKeys.STALE_RESPONSE,
+            new Object[]{request.getUri().toString()});
+      }
+      return staleResponse;
+    }
+
+    fetchedResponse = maybeFixDriftTime(fetchedResponse);
+
+    // 304 Not Modified
+    // Return the stale response and update what is in the cache with any new headers per
+    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+    //
+    // "If a cache uses a received 304 response to update a cache entry,
+    //  the cache MUST update the entry to reflect any new field values
+    //  given in the response."
+    if (fetchedResponse.getHttpStatusCode() == HttpResponse.SC_NOT_MODIFIED) {
+
+      // Update the stale response's headers with the new headers from the fetched response.
+      // If the response from the remote server doesn't have an "Expires" or "Cache-Control" header,
+      // we should service the current request and remove the stale response from the cache. We rely
+      // on those headers to be present so that the response that is in the cache will return the
+      // correct value when isStale() is called. Without removing the stale response, we would get
+      // stuck in a loop of doing conditional GETs for the same stale resource every time it is
+      // requested.
+      final Multimap<String, String> fetchedResponseHeaders = fetchedResponse.getHeaders();
+      if (fetchedResponse.getCacheControlMaxAge() == -1
+              && fetchedResponse.getExpiresTime() == -1) {
+        // CONSIDER: We could try to recurse in this case and do a non-conditional-get for the resource.
+        return httpCache.removeResponse(request);
+      }
+
+      HttpResponseBuilder httpResponseBuilder = new HttpResponseBuilder(staleResponse);
+      for (String headerName : fetchedResponseHeaders.keySet()) {
+        httpResponseBuilder.removeHeader(headerName);
+      }
+
+      httpResponseBuilder.addAllHeaders(fetchedResponse.getHeaders());
+      return cacheFetchedResponse(request, httpResponseBuilder.create());
+    }
+
+    if (!fetchedResponse.isError() && !request.getIgnoreCache() && request.getCacheTtl() != 0) {
+      try {
+        fetchedResponse =
+            responseRewriterRegistry.rewriteHttpResponse(request, fetchedResponse, null);
+      } catch (RewritingException e) {
+        throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e,
+            e.getHttpStatusCode());
+      }
+    }
+
+    // Set response hash value in metadata (used for url versioning)
+    fetchedResponse = HttpResponseMetadataHelper.updateHash(fetchedResponse, metadataHelper);
+
+    // cache the fetched response if possible
+    fetchedResponse = cacheFetchedResponse(request, fetchedResponse);
+
+    return fetchedResponse;
+  }
+
+  /**
+   * Cache the HttpResponse object before being returned to caller.
+   * The default implementation also invalidate the response object by marking it as valid so the
+   * next request can detect if it has been invalidated.
+   * Override this if you need to add additional processing to cache the response.
+   *
+   * @param request
+   * @param fetchedResponse
+   * @return HttpResponse object that has been updated with some metadata tags.
+   */
+  protected HttpResponse cacheFetchedResponse(HttpRequest request, HttpResponse fetchedResponse) {
+    if (!request.getIgnoreCache()) {
+      // Mark the response with invalidation information prior to caching
+      if (fetchedResponse.getCacheTtl() > 0) {
+        fetchedResponse = invalidationService.markResponse(request, fetchedResponse);
+      }
+      HttpResponse cached = httpCache.addResponse(request, fetchedResponse);
+      if (cached != null) {
+        fetchedResponse = cached; // possibly modified response.
+      }
+    }
+
+    return fetchedResponse;
+  }
+
+  /**
+   * Verify response time, and if response time is off from current time by more then
+   * speficied time change response time to be current time.
+   * The function resolve cases that remote server time is wrong, which can cause
+   * resources to expire prematurly or served after they should be expired.
+   * The allowd drift time is configured by responseDateDriftLimit.
+   * @param response the response to fix
+   * @return new response with fix date or original reesponse
+   */
+  public static HttpResponse maybeFixDriftTime(HttpResponse response) {
+    Collection<String> dates = response.getHeaders("Date");
+
+    if (!dates.isEmpty()) {
+      Date d = DateUtil.parseRfc1123Date(dates.iterator().next());
+      if (d != null) {
+        long timestamp = d.getTime();
+        long currentTime = HttpUtil.getTimeSource().currentTimeMillis();
+        if (Math.abs(currentTime - timestamp) > responseDateDriftLimit) {
+          // Do not trust the date from response if it is too old (server time out of sync)
+          HttpResponseBuilder builder = new HttpResponseBuilder(response);
+          builder.setHeader("Date", DateUtil.formatRfc1123Date(currentTime));
+          response = builder.create();
+        }
+      }
+    }
+    return response;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpCache.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpCache.java
new file mode 100644
index 0000000..a2c6cf4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpCache.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Cache of HttpResponse.
+ *
+ * Keys are HttpRequest, values are the HttpResponse.
+ */
+@ImplementedBy(DefaultHttpCache.class)
+public interface HttpCache {
+
+  HttpResponse getResponse(HttpRequest request);
+
+  /**
+   * Add a request/response pair to the cache.
+   *
+   * @return The response that was cached, null otherwise.
+   */
+  HttpResponse addResponse(HttpRequest request, HttpResponse response);
+
+  HttpResponse removeResponse(HttpRequest key);
+
+  /**
+   * Create a string representation of the cache key.  If two requests are cache equivalent (a
+   * response to one request can be used to respond to the other request), their keys are
+   * guaranteed to be identical.
+   *
+   * Identical keys do not guarantee that two requests are cache equivalent.
+   */
+  String createKey(HttpRequest request);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpFetcher.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpFetcher.java
new file mode 100644
index 0000000..65b2c7d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpFetcher.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.shindig.gadgets.GadgetException;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Perform an request for the given resource. Does not perform caching, authentication, or stats.
+ * This class should only be used to implement network-level fetching of resources. While we use
+ * HTTP to represent the transport layer, it's important to note that this fetcher may be used for
+ * other types of URI-based resources and does not necessarily require HTTP.
+ */
+@ImplementedBy(BasicHttpFetcher.class)
+public interface HttpFetcher {
+
+  /**
+   * Fetch HTTP content.
+   *
+   * @param request The request to fetch.
+   * @return An HTTP response from the relevant resource, including error conditions.
+   * @throws GadgetException In the event of a failure that can't be mapped to an HTTP result code.
+   */
+  HttpResponse fetch(HttpRequest request) throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java
new file mode 100644
index 0000000..cfd7ea9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpRequest.java
@@ -0,0 +1,571 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.admin.BasicGadgetAdminStore;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Creates HttpRequests. A new HttpRequest should be created for every unique HttpRequest
+ * being constructed.
+ */
+public class HttpRequest {
+  private static final Logger LOG = Logger.getLogger(HttpRequest.class.getName());
+
+  /** Automatically added to every request so that we know that the request came from our server. */
+  public static final String DOS_PREVENTION_HEADER = "X-shindig-dos";
+  static final String DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8";
+
+  private String method = "GET";
+  private Uri uri;
+  private final Map<String, List<String>> headers = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
+
+  // Internal parameters which serve as extra information to pass along the
+  // chain of HttpRequest processing.
+  // NOTE: These are not get/post parameter equivalent of HttpServletRequest.
+  private final Map<String, String> params = Maps.newHashMap();
+
+  private byte[] postBody = ArrayUtils.EMPTY_BYTE_ARRAY;
+
+  // TODO: It might be useful to refactor these into a simple map of objects and use sub classes
+  // for more detailed data.
+
+  // Cache control.
+  private boolean ignoreCache;
+  private int cacheTtl = -1;
+
+  // Sanitization
+  private boolean sanitizationRequested;
+
+  // Caja
+  private boolean cajaRequested;
+
+  // Whether to follow redirects
+  private boolean followRedirects = true;
+
+  // Context for the request.
+  private Uri gadget;
+  private String container = ContainerConfig.DEFAULT_CONTAINER;
+
+  // For signed fetch & OAuth
+  private SecurityToken securityToken;
+
+  // TODO: Move this into OAuthRequest.
+  private OAuthArguments oauthArguments;
+  private OAuth2Arguments oauth2Arguments;
+  private AuthType authType;
+
+  private String rewriteMimeType;
+  private boolean internalRequest;
+
+  /**
+   * Construct a new request for the given uri.
+   */
+  public HttpRequest(Uri uri) {
+    this.uri = uri;
+    authType = AuthType.NONE;
+    addHeader(DOS_PREVENTION_HEADER, "on");
+  }
+
+  /**
+   * Clone an existing HttpRequest.
+   */
+  public HttpRequest(HttpRequest request) {
+    method = request.method;
+    uri = request.uri;
+    headers.putAll(request.headers);
+    postBody = request.postBody;
+    ignoreCache = request.ignoreCache;
+    cacheTtl = request.cacheTtl;
+    gadget = request.gadget;
+    container = request.container;
+    securityToken = request.securityToken;
+    if (request.oauthArguments != null) {
+      oauthArguments = new OAuthArguments(request.oauthArguments);
+    }
+    if (request.oauth2Arguments != null) {
+      oauth2Arguments = new OAuth2Arguments(request.oauth2Arguments);
+    }
+    authType = request.authType;
+    rewriteMimeType = request.rewriteMimeType;
+    followRedirects = request.followRedirects;
+    internalRequest = request.internalRequest;
+  }
+
+  public HttpRequest setMethod(String method) {
+    this.method = method;
+    return this;
+  }
+
+  public HttpRequest setUri(Uri uri) {
+    this.uri = uri;
+    return this;
+  }
+
+  /**
+   * Add a single header to the request. If a value for the given name is already set, a second
+   * value is added. If you wish to overwrite any possible values for a header, use
+   * {@link #setHeader(String, String)}.
+   */
+  public HttpRequest addHeader(String name, String value) {
+    List<String> values = headers.get(name);
+    if (values == null) {
+      values = Lists.newArrayList();
+      headers.put(name, values);
+    }
+    values.add(value);
+    return this;
+  }
+
+  /**
+   * Sets a single header value, overwriting any previously set headers with the same name.
+   */
+  public HttpRequest setHeader(String name, String value) {
+    headers.put(name, Lists.newArrayList(value));
+    return this;
+  }
+
+  /**
+   * Adds an entire map of headers to the request.
+   */
+  public HttpRequest addHeaders(Map<String, String> headers) {
+    for (Map.Entry<String, String> entry : headers.entrySet()) {
+      addHeader(entry.getKey(), entry.getValue());
+    }
+    return this;
+  }
+
+  /**
+   * Adds all headers in the provided map to the request.
+   */
+  public HttpRequest addAllHeaders(Map<String, ? extends List<String>> headers) {
+    this.headers.putAll(headers);
+    return this;
+  }
+
+  /**
+   * Remove all headers with the given name from the request.
+   *
+   * @return Any values that were removed from the request.
+   */
+  public List<String> removeHeader(String name) {
+    return headers.remove(name);
+  }
+
+  /**
+   * Assigns the specified body to the request, copying all input bytes.
+   */
+  public HttpRequest setPostBody(byte[] postBody) {
+    try {
+      setPostBody(postBody == null ? null : new ByteArrayInputStream(postBody));
+    } catch (IOException e){
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.log(Level.WARNING, e.getMessage(), e);  // Shouldn't ever happen.
+      }
+    }
+    return this;
+  }
+
+  /**
+   * Fills in the request body from an InputStream.
+   * @throws IOException
+   */
+  public HttpRequest setPostBody(InputStream is) throws IOException {
+    if (postBody == null) {
+      this.postBody = ArrayUtils.EMPTY_BYTE_ARRAY;
+    } else {
+      postBody = IOUtils.toByteArray(is);
+    }
+    return this;
+  }
+
+  /**
+   * @param ignoreCache Whether to ignore all caching for this request.
+   */
+  public HttpRequest setIgnoreCache(boolean ignoreCache) {
+    this.ignoreCache = ignoreCache;
+    if (ignoreCache) {
+      // Bypass any proxy caches as well.
+      headers.put("Pragma", Lists.newArrayList("no-cache"));
+    }
+    return this;
+  }
+
+  /**
+   * Should content fetched in response to this request
+   * be sanitized based on the specified mime-type
+   */
+  public boolean isSanitizationRequested() {
+    return sanitizationRequested;
+  }
+
+  public void setSanitizationRequested(boolean sanitizationRequested) {
+    this.sanitizationRequested = sanitizationRequested;
+  }
+
+    /**
+   * Should content fetched in response to this request
+   * be sanitized based on the specified mime-type
+   */
+  public boolean isCajaRequested() {
+    return cajaRequested;
+  }
+
+  public void setCajaRequested(boolean cajaRequested) {
+    this.cajaRequested = cajaRequested;
+  }
+
+  /**
+   * @param cacheTtl The amount of time to cache the result object for, in seconds. If set to -1,
+   * HTTP cache control headers will be honored. Otherwise objects will be cached for the time
+   * specified.
+   */
+  public HttpRequest setCacheTtl(int cacheTtl) {
+    this.cacheTtl = cacheTtl;
+    return this;
+  }
+
+  /**
+   * @param gadget The gadget that caused this HTTP request to be necessary. May be null if the
+   * request was not initiated by the actions of a gadget.
+   */
+  public HttpRequest setGadget(Uri gadget) {
+    this.gadget = gadget;
+    return this;
+  }
+
+  /**
+   * @param container The container that this request originated from.
+   */
+  public HttpRequest setContainer(String container) {
+    this.container = container;
+    return this;
+  }
+
+  /**
+   * Assign the security token to use for making any form of authenticated request.
+   */
+  public HttpRequest setSecurityToken(SecurityToken securityToken) {
+    this.securityToken = securityToken;
+    return this;
+  }
+
+  /**
+   * @param oauthArguments arguments for OAuth/signed fetched
+   */
+  public HttpRequest setOAuthArguments(OAuthArguments oauthArguments) {
+    this.oauthArguments = oauthArguments;
+    return this;
+  }
+
+  /**
+   * @param oauth2Arguments arguments for OAuth2/signed fetched
+   */
+  public HttpRequest setOAuth2Arguments(OAuth2Arguments oauth2Arguments) {
+    this.oauth2Arguments = oauth2Arguments;
+    return this;
+  }
+
+  /**
+   * @param followRedirects whether this request should automatically follow redirects.
+   */
+  public HttpRequest setFollowRedirects(boolean followRedirects) {
+    this.followRedirects = followRedirects;
+    return this;
+  }
+
+  /**
+   * @param authType The type of authentication being used for this request.
+   */
+  public HttpRequest setAuthType(AuthType authType) {
+    this.authType = authType;
+    return this;
+  }
+
+  /**
+   * @param rewriteMimeType The assumed content type of the response to be rewritten. Overrides
+   * any values set in the Content-Type response header.
+   *
+   * TODO: Move this to new rewriting facility.
+   */
+  public HttpRequest setRewriteMimeType(String rewriteMimeType) {
+    this.rewriteMimeType = rewriteMimeType;
+    return this;
+  }
+
+  public String getMethod() {
+    return method;
+  }
+
+  public Uri getUri() {
+    return uri;
+  }
+
+  /**
+   * @return All headers to be sent in this request.
+   */
+  public Map<String, List<String>> getHeaders() {
+    return headers;
+  }
+
+  /**
+   * @param name The header to fetch
+   * @return A list of headers with that name (may be empty).
+   */
+  public List<String> getHeaders(String name) {
+    List<String> match = headers.get(name);
+    if (match == null) {
+      return Collections.emptyList();
+    } else {
+      return match;
+    }
+  }
+
+  /**
+   * @return The first set header with the given name or null if not set. If
+   *         you need multiple values for the header, use getHeaders().
+   */
+  public String getHeader(String name) {
+    List<String> headerList = getHeaders(name);
+    if (headerList.isEmpty()) {
+      return null;
+    } else {
+      return headerList.get(0);
+    }
+  }
+
+  /**
+   * @return The content type of the request (determined from request headers)
+   */
+  public String getContentType() {
+    String type = getHeader("Content-Type");
+    if (type == null) {
+      return DEFAULT_CONTENT_TYPE;
+    }
+    return type;
+  }
+
+  /**
+   * @return An input stream that can be used to read the post body.
+   */
+  public InputStream getPostBody() {
+    return new ByteArrayInputStream(postBody);
+  }
+
+  /**
+   * @return The post body as a string, assuming UTF-8 encoding.
+   * TODO: We should probably tolerate other encodings, based on the
+   *     Content-Type header.
+   */
+  public String getPostBodyAsString() {
+    return CharsetUtil.newUtf8String(postBody);
+  }
+
+  /**
+   * Retrieves the total length of the post body.
+   *
+   * @return The length of the post body.
+   */
+  public int getPostBodyLength() {
+    return postBody.length;
+  }
+
+  /**
+   * @return True if caching should be ignored for this request.
+   */
+  public boolean getIgnoreCache() {
+    return ignoreCache;
+  }
+
+  /**
+   * @return The amount of time to cache any response objects for, in seconds.
+   */
+  public int getCacheTtl() {
+    return cacheTtl;
+  }
+
+  /**
+   * @return The uri of gadget responsible for making this request.
+   */
+  public Uri getGadget() {
+    return gadget;
+  }
+
+  public String getParam(String paramName) {
+    return params.get(paramName);
+  }
+
+  public Integer getParamAsInteger(String paramName) {
+    String value = params.get(paramName);
+    if (value == null) {
+      return null;
+    }
+    return NumberUtils.createInteger(value);
+  }
+
+  public <T> void setParam(String paramName, T paramValue) {
+    params.put(paramName,  (paramValue == null) ? null : String.valueOf(paramValue));
+  }
+
+  public Map<String, String> getParams() {
+    return params;
+  }
+
+  /**
+   * @return The container responsible for making this request.
+   */
+  public String getContainer() {
+    return container;
+  }
+
+  /**
+   * @return The security token used to make this request.
+   */
+  public SecurityToken getSecurityToken() {
+    return securityToken;
+  }
+
+  /**
+   * @return arguments for OAuth and signed fetch
+   */
+  public OAuthArguments getOAuthArguments() {
+    return oauthArguments;
+  }
+
+  /**
+   * @return arguments for OAuth2 and signed fetch
+   */
+  public OAuth2Arguments getOAuth2Arguments() {
+    return oauth2Arguments;
+  }
+
+
+  /**
+   * @return true if redirects should be followed.
+   */
+  public boolean getFollowRedirects() {
+    return followRedirects;
+  }
+
+  /**
+   * @return The type of authentication being used for this request.
+   */
+  public AuthType getAuthType() {
+    return authType;
+  }
+
+  /**
+   * @return The content type to assume when rewriting.
+   *
+   * TODO: Move this to new rewriting facility.
+   */
+  public String getRewriteMimeType() {
+    return rewriteMimeType;
+  }
+
+  /**
+   * @return true if this is an internal request, false otherwise
+   */
+  public boolean isInternalRequest() {
+    return internalRequest;
+  }
+
+  /**
+   * An internal request is one created by the server to satisfy global server requirements.
+   * Examples are retrieving the RPC methods, loading features, or rewriting requests pulling in
+   * external content (that are driven back through the proxy to be completed).  SecurityTokens would typically
+   * refer to a gadget as the source of the request, whereas the server initiated requests are occurring on behalf
+   * of the server, and not on behalf of a specific gadget.
+   * @param internalRequest Marks the request object as internal.
+   * @return HttpRequest A self-reference
+   */
+  public HttpRequest setInternalRequest(boolean internalRequest) {
+    this.internalRequest = internalRequest;
+    return this;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder(method);
+    buf.append(' ').append(uri.getPath())
+       .append(uri.getQuery() == null ? "" : '?' + uri.getQuery()).append("\n\n");
+    buf.append("Host: ").append(uri.getAuthority()).append('\n');
+    buf.append("X-Shindig-AuthType: ").append(authType).append('\n');
+    for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
+      String name = entry.getKey();
+      for (String value : entry.getValue()) {
+        buf.append(name).append(": ").append(value).append('\n');
+      }
+    }
+    buf.append('\n');
+    buf.append(getPostBodyAsString());
+
+    return buf.toString();
+  }
+
+  @Override
+  public int hashCode() {
+    return method.hashCode()
+      ^ uri.hashCode()
+      ^ authType.hashCode()
+      ^ Arrays.hashCode(postBody)
+      ^ headers.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof HttpRequest)) {
+      return false;
+    }
+    HttpRequest req = (HttpRequest)obj;
+    return method.equals(req.method) &&
+            uri.equals(req.uri) &&
+            authType == req.authType &&
+            Arrays.equals(postBody, req.postBody) &&
+            headers.equals(req.headers);
+    // TODO: Verify that other fields aren't meaningful. Especially important to check for oauth args.
+  }
+}
+
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java
new file mode 100644
index 0000000..ede402b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponse.java
@@ -0,0 +1,688 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Charsets;
+import com.google.common.base.Supplier;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.net.HttpHeaders;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.gadgets.encoding.EncodingDetector;
+
+import java.io.ByteArrayInputStream;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Represents the results of an HTTP content retrieval operation.
+ *
+ * HttpResponse objects are immutable in order to allow them to be safely used in concurrent
+ * caches and by multiple threads without worrying about concurrent modification.
+ */
+public final class HttpResponse implements Externalizable {
+  private static final long serialVersionUID = 7526471155622776147L;
+
+  public static final int SC_CONTINUE = 100;
+  public static final int SC_SWITCHING_PROTOCOLS = 101;
+
+  public static final int SC_OK = 200;
+  public static final int SC_CREATED = 201;
+  public static final int SC_ACCEPTED = 202;
+  public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
+  public static final int SC_NO_CONTENT = 204;
+  public static final int SC_RESET_CONTENT = 205;
+  public static final int SC_PARTIAL_CONTENT = 206;
+
+  public static final int SC_MULTIPLE_CHOICES = 300;
+  public static final int SC_MOVED_PERMANENTLY = 301;
+  public static final int SC_FOUND = 302;
+  public static final int SC_SEE_OTHER = 303;
+  public static final int SC_NOT_MODIFIED = 304;
+  public static final int SC_USE_PROXY = 305;
+  public static final int SC_TEMPORARY_REDIRECT = 307;
+
+  public static final int SC_BAD_REQUEST = 400;
+  public static final int SC_UNAUTHORIZED = 401;
+  public static final int SC_PAYMENT_REQUIRED = 402;
+  public static final int SC_FORBIDDEN = 403;
+  public static final int SC_NOT_FOUND = 404;
+  public static final int SC_METHOD_NOT_ALLOWED = 405;
+  public static final int SC_NOT_ACCEPTABLE = 406;
+  public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
+  public static final int SC_REQUEST_TIMEOUT = 408;
+  public static final int SC_CONFLICT = 409;
+  public static final int SC_GONE = 410;
+  public static final int SC_LENGTH_REQUIRED = 411;
+  public static final int SC_PRECONDITION_FAILED = 412;
+  public static final int SC_REQUEST_ENTITY_TOO_LARGE = 413;
+  public static final int SC_REQUEST_URI_TOO_LONG = 414;
+  public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
+  public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
+  public static final int SC_EXPECTATION_FAILED = 417;
+
+  public static final int SC_INTERNAL_SERVER_ERROR = 500;
+  public static final int SC_NOT_IMPLEMENTED = 501;
+  public static final int SC_BAD_GATEWAY = 502;
+  public static final int SC_SERVICE_UNAVAILABLE = 503;
+  public static final int SC_GATEWAY_TIMEOUT = 504;
+  public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;
+
+  // These content types can always skip encoding detection.
+  private static final Set<String> BINARY_CONTENT_TYPES = ImmutableSet.of(
+      "image/jpeg", "image/png", "image/gif", "image/jpg", "application/x-shockwave-flash",
+      "application/octet-stream", "application/ogg", "application/zip", "audio/mpeg",
+      "audio/x-ms-wma", "audio/vnd.rn-realaudio", "audio/x-wav", "video/mpeg", "video/mp4",
+      "video/quicktime", "video/x-ms-wmv", "video/x-flv", "video/flv",
+      "video/x-ms-asf", "application/pdf", "image/x-icon"
+  );
+
+  // These HTTP status codes should always honor the HTTP status returned by the remote host. All
+  // other error codes are treated as errors and will use the negativeCacheTtl value.
+  private static final Set<Integer> NEGATIVE_CACHING_EXEMPT_STATUS
+      = ImmutableSet.of(SC_UNAUTHORIZED, SC_FORBIDDEN);
+
+  // TTL to use when an error response is fetched. This should be non-zero to
+  // avoid high rates of requests to bad urls in high-traffic situations.
+  static final long DEFAULT_NEGATIVE_CACHE_TTL = 30 * 1000;
+
+  // Default TTL for an entry in the cache that does not have any cache control headers.
+  static final long DEFAULT_TTL = 5L * 60L * 1000L;
+
+  static final Charset DEFAULT_ENCODING = Charsets.UTF_8;
+
+  @Inject(optional = true) @Named("shindig.cache.http.negativeCacheTtl")
+  private static long negativeCacheTtl = DEFAULT_NEGATIVE_CACHE_TTL;
+
+  // Default TTL for resources that are public and has no explicit Cache-Control max-age
+  // and expires headers. Resources without cache-control are considered public by default.
+  @Inject(optional = true) @Named("shindig.cache.http.defaultTtl")
+  public static long defaultTtl = DEFAULT_TTL;
+
+  @Inject(optional = true) @Named("shindig.http.fast-encoding-detection")
+  private static boolean fastEncodingDetection = true;
+
+  // Support injection of smarter encoding detection
+  @Inject(optional = true)
+  private static EncodingDetector.FallbackEncodingDetector customEncodingDetector =
+      new EncodingDetector.FallbackEncodingDetector();
+
+  public static void setTimeSource(TimeSource timeSource) {
+    HttpUtil.setTimeSource(timeSource);
+  }
+
+  public static TimeSource getTimeSource() {
+    return HttpUtil.getTimeSource();
+  }
+
+  // Holds character sets for fast conversion
+  private static final LoadingCache<String, Charset> encodingToCharset = CacheBuilder
+    .newBuilder()
+    .build(new CacheLoader<String, Charset>() {
+      public Charset load(String encoding) throws ExecutionException {
+        try {
+          return Charset.forName(encoding);
+        } catch (UnsupportedCharsetException e) {
+          throw new ExecutionException(e);
+        } catch (IllegalCharsetNameException e) {
+          throw new ExecutionException(e);
+        }
+      }
+    });
+
+  private String responseString;
+  private long date;
+  private Charset encoding;
+  private Map<String, String> metadata;
+
+  private int httpStatusCode;
+  private Multimap<String, String> headers;
+  private byte[] responseBytes;
+
+  private long refetchStrictNoCacheAfterMs;
+
+  /**
+   * Needed for serialization. Do not use this for any other purpose.
+   */
+  public HttpResponse() {}
+
+  /**
+   * Construct an HttpResponse from a builder (called by HttpResponseBuilder.create).
+   * @param builder a valid builder
+   */
+  HttpResponse(HttpResponseBuilder builder) {
+    httpStatusCode = builder.getHttpStatusCode();
+    Multimap<String, String> headerCopy = HttpResponse.newHeaderMultimap();
+
+    // Always safe, HttpResponseBuilder won't modify the body.
+    responseBytes = builder.getResponse();
+
+    // Copy headers after builder.getResponse(), since that can modify Content-Type.
+    headerCopy.putAll(builder.getHeaders());
+
+    metadata = ImmutableMap.copyOf(builder.getMetadata());
+
+    // We want to modify the headers to ensure that the proper Content-Type and Date headers
+    // have been set. This allows us to avoid these expensive calculations from the cache.
+    date = getAndUpdateDate(headerCopy);
+    encoding = getAndUpdateEncoding(headerCopy, responseBytes);
+    headers = Multimaps.unmodifiableMultimap(headerCopy);
+    refetchStrictNoCacheAfterMs = builder.getRefetchStrictNoCacheAfterMs();
+  }
+
+  private HttpResponse(int httpStatusCode, String body) {
+    this(new HttpResponseBuilder()
+      .setHttpStatusCode(httpStatusCode)
+      .setResponseString(body));
+  }
+
+  public HttpResponse(String body) {
+    this(SC_OK, body);
+  }
+
+  public static HttpResponse error() {
+    return new HttpResponse(SC_INTERNAL_SERVER_ERROR, "");
+  }
+
+  public static HttpResponse badrequest(String msg) {
+    return new HttpResponse(SC_BAD_REQUEST, msg);
+  }
+
+  public static HttpResponse timeout() {
+    return new HttpResponse(SC_GATEWAY_TIMEOUT, "");
+  }
+
+  public static HttpResponse notFound() {
+    return new HttpResponse(SC_NOT_FOUND, "");
+  }
+
+  public int getHttpStatusCode() {
+    return httpStatusCode;
+  }
+
+  /**
+   * @return True if the status code is considered to be an error.
+   */
+  public boolean isError() {
+    return httpStatusCode >= 400;
+  }
+
+  /**
+   * @return The encoding of the response body, if we're able to determine it.
+   */
+  public String getEncoding() {
+    return encoding.name();
+  }
+
+  /**
+   * @return The Charset of the response body's encoding, if we were able to determine it.
+   */
+  public Charset getEncodingCharset() {
+    return encoding;
+  }
+
+  /**
+   * @return the content length
+   */
+  public int getContentLength() {
+    return responseBytes.length;
+  }
+
+  /**
+   * @return An input stream suitable for reading the entirety of the response.
+   */
+  public InputStream getResponse() {
+    return new ByteArrayInputStream(responseBytes);
+  }
+
+  /**
+   * Attempts to convert the response body to a string using the Content-Type header. If no
+   * Content-Type header is specified (or it doesn't include an encoding), we will assume it is
+   * DEFAULT_ENCODING.
+   *
+   * @return The body as a string.
+   */
+  public String getResponseAsString() {
+    if (responseString == null) {
+      responseString = encoding.decode(ByteBuffer.wrap(responseBytes)).toString();
+
+      // Strip BOM if present.
+      if (responseString.length() > 0 && responseString.codePointAt(0) == 0xFEFF) {
+        responseString = responseString.substring(1);
+      }
+    }
+    return responseString;
+  }
+
+  /**
+   * @return All headers for this object.
+   */
+  public Multimap<String, String> getHeaders() {
+    return headers;
+  }
+
+  /**
+   * @return All headers with the given name. If no headers are set for the given name, an empty
+   * collection will be returned.
+   */
+  public Collection<String> getHeaders(String name) {
+    return headers.get(name);
+  }
+
+  /**
+   * @return The first set header with the given name or null if not set. If you need multiple
+   *         values for the header, use getHeaders().
+   */
+  public String getHeader(String name) {
+    Collection<String> headerList = getHeaders(name);
+    if (headerList.isEmpty()) {
+      return null;
+    } else {
+      return headerList.iterator().next();
+    }
+  }
+
+  /**
+   * @return additional data to embed in responses sent from the JSON proxy.
+   */
+  public Map<String, String> getMetadata() {
+    return metadata;
+  }
+
+  /**
+   * Calculate the Cache Expiration for this response.
+   *
+   *
+   * For errors (rc >=400) we intentionally ignore cache-control headers for most HTTP error responses, because if
+   * we don't we end up hammering sites that have gone down with lots of requests. Certain classes
+   * of client errors (authentication) have more severe behavioral implications if we cache them.
+   *
+   * For errors if the server provides a Retry-After header we use that.
+   *
+   * We technically shouldn't be caching certain 300 class status codes either, such as 302, but
+   * in practice this is a better option for performance.
+   *
+   * @return consolidated cache expiration time or -1
+   */
+  public long getCacheExpiration() {
+    if (isError() && !NEGATIVE_CACHING_EXEMPT_STATUS.contains(httpStatusCode)) {
+      // If the server provides a Retry-After header use that as the cacheTtl
+      String retryAfter = this.getHeader("Retry-After");
+      if (retryAfter != null) {
+        if (StringUtils.isNumeric(retryAfter)) {
+          return date + Integer.valueOf(retryAfter) * 1000L;
+        } else {
+          Date expiresDate = DateUtil.parseRfc1123Date(retryAfter);
+          if (expiresDate != null)
+            return expiresDate.getTime();
+        }
+      }
+      // default value
+      return date + negativeCacheTtl;
+    }
+
+    if (isStrictNoCache()) {
+      return -1;
+    }
+    long maxAge = getCacheControlMaxAge();
+    if (maxAge != -1) {
+      return date + maxAge;
+    }
+    long expiration = getExpiresTime();
+    if (expiration != -1) {
+      return expiration;
+    }
+
+    if (isError()) {
+      return date + negativeCacheTtl;
+    }
+
+    return date + defaultTtl;
+  }
+
+  public long getRefetchStrictNoCacheAfterMs() {
+    return refetchStrictNoCacheAfterMs;
+  }
+
+  public boolean shouldRefetch() {
+    // Time after which resource should be refetched.
+    long refetchExpiration = isStrictNoCache() ?
+        date + getRefetchStrictNoCacheAfterMs() : getCacheExpiration();
+    return refetchExpiration <= getTimeSource().currentTimeMillis();
+  }
+
+  /**
+   * @return Consolidated ttl in milliseconds or -1.
+   */
+  public long getCacheTtl() {
+    long expiration = getCacheExpiration();
+    if (expiration != -1) {
+      return expiration - getTimeSource().currentTimeMillis();
+    }
+    return -1;
+  }
+
+  /**
+   * @return True if this result is stale.
+   */
+  public boolean isStale() {
+    if(getCacheControlMaxAge() == 0) {
+      return true;
+    }
+    return getCacheTtl() <= 0;
+  }
+
+  /**
+   * @return true if a strict no-cache header is set in Cache-Control or Pragma
+   */
+  public boolean isStrictNoCache() {
+    if (isError() && !NEGATIVE_CACHING_EXEMPT_STATUS.contains(httpStatusCode)) {
+      return false;
+    }
+    String cacheControl = getHeader(HttpHeaders.CACHE_CONTROL);
+    if (cacheControl != null) {
+      String[] directives = StringUtils.split(cacheControl, ',');
+      for (String directive : directives) {
+        directive = directive.trim();
+        if (directive.equalsIgnoreCase("no-cache")
+            || directive.equalsIgnoreCase("no-store")
+            || directive.equalsIgnoreCase("private")) {
+          return true;
+        }
+      }
+    }
+
+    for (String pragma : getHeaders(HttpHeaders.PRAGMA)) {
+      if ("no-cache".equalsIgnoreCase(pragma)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * @return the expiration time from the Expires header or -1 if not set
+   */
+  public long getExpiresTime() {
+    String expires = getHeader(HttpHeaders.EXPIRES);
+    if (expires != null) {
+      Date expiresDate = DateUtil.parseRfc1123Date(expires);
+      if (expiresDate != null) {
+        return expiresDate.getTime();
+      } else {
+        // Per RFC2616, 14.21 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21):
+        // "HTTP/1.1 clients and caches MUST treat other invalid date formats,
+        // especially including the value "0", as in the past (i.e., "already
+        // expired")."
+        return 0;
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * @return max-age value or -1 if invalid or not set
+   */
+  public long getCacheControlMaxAge() {
+    String cacheControl = getHeader(HttpHeaders.CACHE_CONTROL);
+    if (cacheControl != null) {
+      String[] directives = StringUtils.split(cacheControl, ',');
+      for (String directive : directives) {
+        directive = directive.trim();
+        if (directive.startsWith("max-age")) {
+          String[] parts = StringUtils.split(directive, '=');
+          if (parts.length == 2) {
+            try {
+              return Long.valueOf(parts[1]) * 1000;
+            } catch (NumberFormatException ignore) {
+              return -1;
+            }
+          }
+        }
+      }
+    }
+    return -1;
+  }
+
+  /**
+   * Tries to find a valid date from the input headers.
+   *
+   * @return The value of the date header, in milliseconds, or -1 if no Date could be determined.
+   */
+  private static long getAndUpdateDate(Multimap<String, String> headers) {
+    // Validate the Date header. Must conform to the HTTP date format.
+    long timestamp = -1;
+    long currentTime = getTimeSource().currentTimeMillis();
+    Collection<String> dates = headers.get(HttpHeaders.DATE);
+
+    if (!dates.isEmpty()) {
+      Date d = DateUtil.parseRfc1123Date(dates.iterator().next());
+      if (d != null) {
+        timestamp = d.getTime();
+      }
+    }
+    if (timestamp == -1) {
+      timestamp = currentTime;
+      headers.replaceValues(HttpHeaders.DATE, ImmutableList.of(DateUtil.formatRfc1123Date(timestamp)));
+    }
+    return timestamp;
+  }
+
+  /**
+   * returns the default TTL for responses.  Used mainly by tests because Guice static injects TTL values.
+   *
+   * @return milliseconds of the ttl
+   */
+  public long getDefaultTtl() {
+    return defaultTtl;
+  }
+
+  @VisibleForTesting
+  long getNegativeTtl() {
+    return negativeCacheTtl;
+  }
+
+  /**
+   * Attempts to determine the encoding of the body. If it can't be determined, we use
+   * DEFAULT_ENCODING instead.
+   *
+   * @return The detected encoding or DEFAULT_ENCODING.
+   */
+  private static Charset getAndUpdateEncoding(Multimap<String, String> headers, byte[] body) {
+    if (body == null || body.length == 0) {
+      return DEFAULT_ENCODING;
+    }
+
+    Collection<String> values = headers.get(HttpHeaders.CONTENT_TYPE);
+    if (!values.isEmpty()) {
+      String contentType = values.iterator().next();
+      String[] parts = StringUtils.split(contentType, ';');
+      if (parts == null
+          || parts.length == 0
+          || BINARY_CONTENT_TYPES.contains(parts[0])) {
+        return DEFAULT_ENCODING;
+      }
+      if (parts.length == 2) {
+        int offset = parts[1].toLowerCase().indexOf("charset=");
+        if (offset != -1) {
+          String charset = parts[1].substring(offset + 8).toUpperCase();
+          // Some servers include quotes around the charset:
+          //   Content-Type: text/html; charset="UTF-8"
+          if (charset.length() >= 2 && charset.startsWith("\"") && charset.endsWith("\"")) {
+            charset = charset.substring(1, charset.length() - 1);
+          }
+
+          try {
+            return encodingToCharset.get(charset);
+          } catch (ExecutionException e) {
+            // fall through to detection
+          }
+        }
+      }
+
+      Charset encoding = EncodingDetector.detectEncoding(body, fastEncodingDetection,
+          customEncodingDetector);
+      // Record the charset in the content-type header so that its value can be cached
+      // and re-used. This is a BIG performance win.
+      values.clear();
+      values.add(contentType + "; charset=" + encoding.name());
+
+      return encoding;
+    } else {
+      // If no content type was specified, we'll assume an unknown binary type.
+      return DEFAULT_ENCODING;
+    }
+  }
+
+
+  @Override
+  public int hashCode() {
+    return httpStatusCode
+      ^ headers.hashCode()
+      ^ Arrays.hashCode(responseBytes);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) { return true; }
+    if (!(obj instanceof HttpResponse)) { return false; }
+
+    HttpResponse response = (HttpResponse)obj;
+
+    return httpStatusCode == response.httpStatusCode &&
+           headers.equals(response.headers) &&
+           Arrays.equals(responseBytes, response.responseBytes);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder("HTTP/1.1 ").append(httpStatusCode).append("\r\n\r\n");
+    for (Map.Entry<String,String> entry : headers.entries()) {
+      buf.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
+    }
+    buf.append("\r\n").append(getResponseAsString()).append("\r\n");
+    return buf.toString();
+  }
+
+  /**
+   * @return The response as a byte array. Only visible to the package to avoid copying when
+   * making a new HttpResponseBuilder.
+   */
+  byte[] getResponseAsBytes() {
+    return responseBytes;
+  }
+
+  /**
+   * Expected layout:
+   *
+   * int - status code
+   * Map<String, List<String>> - headers
+   * int - length of body
+   * byte array - body, of previously specified length
+   */
+  @SuppressWarnings("unchecked")
+  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+    httpStatusCode = in.readInt();
+
+    // We store the multimap as a Map<String,List<String>> to insulate us from google-collections API churn
+    // And to remain backwards compatible
+
+    Map<String, List<String>> headerCopyMap = (Map<String, List<String>>)in.readObject();
+    Multimap headerCopy = newHeaderMultimap();
+
+    for (Map.Entry<String,List<String>> entry : headerCopyMap.entrySet()) {
+      headerCopy.putAll(entry.getKey(), entry.getValue());
+    }
+
+    int bodyLength = in.readInt();
+    responseBytes = new byte[bodyLength];
+    int cnt, offset = 0;
+    while ((cnt = in.read(responseBytes, offset, bodyLength)) > 0) {
+      offset += cnt;
+      bodyLength -= cnt;
+    }
+    if (offset != responseBytes.length) {
+      throw new IOException("Invalid body! Expected length = " + responseBytes.length + ", bytes readed = " + offset + '.');
+    }
+
+    date = getAndUpdateDate(headerCopy);
+    encoding = getAndUpdateEncoding(headerCopy, responseBytes);
+    headers = Multimaps.unmodifiableMultimap(headerCopy);
+    metadata = Collections.emptyMap();
+  }
+
+  public void writeExternal(ObjectOutput out) throws IOException {
+    out.writeInt(httpStatusCode);
+    // Write out multimap as a map (see above)
+    Map<String,List<String>> map = Maps.newHashMap();
+    for (String key : headers.keySet()) {
+      map.put(key, Lists.newArrayList(headers.get(key)));
+    }
+    out.writeObject(Maps.newHashMap(map));
+    out.writeInt(responseBytes.length);
+    out.write(responseBytes);
+  }
+
+
+  private static final Supplier<Collection<String>> HEADER_COLLECTION_SUPPLIER = new HeaderCollectionSupplier();
+
+  private static class HeaderCollectionSupplier implements Supplier<Collection<String>> {
+    public Collection<String> get() {
+      return new LinkedList<String>();  //To change body of implemented methods use File | Settings | File Templates.
+    }
+  }
+
+  // FIXME: Why isn't this a ListMultimap?  Headers should be ordered and we want to be able to do type checks on our Multimap.
+  public static Multimap<String,String> newHeaderMultimap() {
+    TreeMap<String,Collection<String>> map = new TreeMap<String,Collection<String>>(String.CASE_INSENSITIVE_ORDER);
+    return Multimaps.newMultimap(map, HEADER_COLLECTION_SUPPLIER);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
new file mode 100644
index 0000000..ffe7bff
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseBuilder.java
@@ -0,0 +1,355 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * Constructs HttpResponse objects.
+ */
+public class HttpResponseBuilder extends MutableContent {
+  private int httpStatusCode = HttpResponse.SC_OK;
+  private final Multimap<String, String> headers = HttpResponse.newHeaderMultimap();
+  private final Map<String, String> metadata = Maps.newHashMap();
+
+  // Stores the HttpResponse object, if any, from which this Builder is constructed.
+  // This allows us to avoid creating a new HttpResponse in create() when no changes
+  // have been made.
+  private HttpResponse responseObj;
+  private int responseObjNumChanges;
+
+  private long refetchStrictNoCacheAfterMs =
+      AbstractHttpCache.REFETCH_STRICT_NO_CACHE_AFTER_MS_DEFAULT;
+
+  public HttpResponseBuilder(GadgetHtmlParser parser, HttpResponse response) {
+    super(parser, response);
+    if (response != null) {
+      httpStatusCode = response.getHttpStatusCode();
+      headers.putAll(response.getHeaders());
+      metadata.putAll(response.getMetadata());
+      refetchStrictNoCacheAfterMs = response.getRefetchStrictNoCacheAfterMs();
+    } else {
+      setResponse(null);
+    }
+    responseObj = response;
+    responseObjNumChanges = getNumChanges();
+  }
+
+  public HttpResponseBuilder() {
+    this(unsupportedParser(), null);
+  }
+
+  public HttpResponseBuilder(HttpResponseBuilder builder) {
+    this(unsupportedParser(), builder.create());
+  }
+
+  public HttpResponseBuilder(HttpResponse response) {
+    this(unsupportedParser(), response);
+  }
+
+  /**
+   * @return A new HttpResponse.
+   */
+  public HttpResponse create() {
+    if (getNumChanges() != responseObjNumChanges || responseObj == null) {
+      // Short-circuit the creation process: no need to create a
+      // new (immutable) HttpResponse object when no modifications occurred.
+      responseObj = new HttpResponse(this);
+      responseObjNumChanges = getNumChanges();
+    }
+    return responseObj;
+  }
+
+  /**
+   * @param body The response string.  Converted to UTF-8 bytes and copied when set.
+   */
+  public HttpResponseBuilder setResponseString(String body) {
+    setContentBytes(CharsetUtil.getUtf8Bytes(body), Charsets.UTF_8);
+    return this;
+  }
+
+  public HttpResponseBuilder setEncoding(Charset charset) {
+    Collection<String> values = headers.get("Content-Type");
+    if (!values.isEmpty()) {
+      String contentType = values.iterator().next();
+      StringBuilder newContentTypeBuilder = new StringBuilder("");
+      // Remove previously set charset:
+      String[] parts = StringUtils.split(contentType, ';');
+      for (String part : parts) {
+        if (!part.contains("charset=")) {
+          if (newContentTypeBuilder.length() > 0) {
+            newContentTypeBuilder.append("; ");
+          }
+          newContentTypeBuilder.append(part);
+        }
+      }
+      if (charset != null) {
+        if (newContentTypeBuilder.length() > 0) {
+          newContentTypeBuilder.append("; ");
+        }
+        newContentTypeBuilder.append("charset=").append(charset.name());
+      }
+      values.clear();
+      String newContentType = newContentTypeBuilder.toString();
+      values.add(newContentType);
+      if (!(values.size() == 1 && !contentType.equals(newContentType))) {
+        incrementNumChanges();
+      }
+    }
+    return this;
+  }
+
+  /**
+   * @param responseBytes The response body. Copied when set.
+   */
+  public HttpResponseBuilder setResponse(byte[] responseBytes) {
+    if (responseBytes == null) {
+      responseBytes = ArrayUtils.EMPTY_BYTE_ARRAY;
+    }
+    byte[] newBytes = new byte[responseBytes.length];
+    System.arraycopy(responseBytes, 0, newBytes, 0, responseBytes.length);
+    setContentBytes(newBytes);
+    return this;
+  }
+
+  /**
+   * @param responseBytes The response body. Not copied when set.
+   */
+  public HttpResponseBuilder setResponseNoCopy(byte[] responseBytes) {
+    if (responseBytes == null) {
+      responseBytes = ArrayUtils.EMPTY_BYTE_ARRAY;
+    }
+    setContentBytes(responseBytes);
+    return this;
+  }
+
+  public HttpResponseBuilder setHttpStatusCode(int httpStatusCode) {
+    if (this.httpStatusCode != httpStatusCode) {
+      this.httpStatusCode = httpStatusCode;
+      incrementNumChanges();
+    }
+    return this;
+  }
+
+  public HttpResponseBuilder clearAllHeaders() {
+    incrementNumChanges();
+    headers.clear();
+    return this;
+  }
+
+  public HttpResponseBuilder addHeader(String name, String value) {
+    if (name != null) {
+      headers.put(name, value);
+      incrementNumChanges();
+    }
+    return this;
+  }
+
+  public HttpResponseBuilder setHeader(String name, String value) {
+    if (name != null) {
+      headers.replaceValues(name, Lists.newArrayList(value));
+      incrementNumChanges();
+    }
+    return this;
+  }
+
+  public String getHeader(String name) {
+    if (name != null && headers.containsKey(name)) {
+      return headers.get(name).iterator().next();
+    }
+    return null;
+  }
+
+  public HttpResponseBuilder addHeaders(Map<String, String> headers) {
+    for (Map.Entry<String,String> entry : headers.entrySet()) {
+      this.headers.put(entry.getKey(), entry.getValue());
+      incrementNumChanges();
+    }
+    return this;
+  }
+
+  public HttpResponseBuilder addAllHeaders(Map<String, ? extends List<String>> headers) {
+    for (Map.Entry<String,? extends List<String>> entry : headers.entrySet()) {
+      this.headers.putAll(entry.getKey(), entry.getValue());
+      incrementNumChanges();
+    }
+    return this;
+  }
+
+  /**
+   * Adds all the headers in the given multimap to the HttpResponse that is under construction.
+   *
+   * @param headers
+   *          A multimap of header keys and values. WARNING: This Multimap should be one constructed
+   *          by org.apache.shindig.gadgets.http.HttpResponse.newHeaderMultimap()
+   * @return <code>this</code>
+   */
+  public HttpResponseBuilder addAllHeaders(Multimap<String, String> headers) {
+    this.headers.putAll(headers);
+    incrementNumChanges();
+    return this;
+  }
+
+  public Collection<String> removeHeader(String name) {
+    Collection<String> ret = headers.removeAll(name);
+    if (ret != null) {
+      incrementNumChanges();
+    }
+    return ret;
+  }
+
+  public HttpResponseBuilder setCacheTtl(int cacheTtl) {
+    headers.removeAll("Pragma");
+    headers.removeAll("Expires");
+    headers.replaceValues("Cache-Control", ImmutableList.of("public,max-age=" + cacheTtl));
+    incrementNumChanges();
+    return this;
+  }
+
+  public HttpResponseBuilder setExpirationTime(long expirationTime) {
+    headers.removeAll("Cache-Control");
+    headers.removeAll("Pragma");
+    headers.put("Expires", DateUtil.formatRfc1123Date(expirationTime));
+    incrementNumChanges();
+    return this;
+  }
+
+  public HttpResponseBuilder setRefetchStrictNoCacheAfterMs(long refetchStrictNoCacheAfterMs) {
+    this.refetchStrictNoCacheAfterMs = refetchStrictNoCacheAfterMs;
+    incrementNumChanges();
+    return this;
+  }
+
+  public HttpResponseBuilder setCacheControlMaxAge(long expirationTime) {
+    String cacheControl = getHeader("Cache-Control");
+    List<String> directives = Lists.newArrayList();
+    if (cacheControl != null) {
+      for (String directive : StringUtils.split(cacheControl, ',')) {
+        directive = directive.trim();
+        if (!directive.startsWith("max-age") || StringUtils.split(directive, '=').length != 2) {
+          directives.add(directive);
+        }
+      }
+    }
+    directives.add("max-age=" + expirationTime);
+    setHeader("Cache-Control", StringUtils.join(directives, ','));
+    incrementNumChanges();
+    return this;
+  }
+
+  /**
+   * Sets cache-control headers indicating the response is not cacheable.
+   */
+  private final List<String> NO_CACHE_HEADER = ImmutableList.of("no-cache");
+  public HttpResponseBuilder setStrictNoCache() {
+    headers.replaceValues("Cache-Control", NO_CACHE_HEADER);
+    headers.replaceValues("Pragma", NO_CACHE_HEADER);
+    headers.removeAll("Expires");
+    incrementNumChanges();
+    return this;
+  }
+
+  public HttpResponseBuilder setMetadata(String key, String value) {
+    metadata.put(key, value);
+    incrementNumChanges();
+    return this;
+  }
+
+  public HttpResponseBuilder setMetadata(Map<String, String> metadata) {
+    this.metadata.putAll(metadata);
+    incrementNumChanges();
+    return this;
+  }
+
+  public int getContentLength() {
+    return getResponse().length;
+  }
+
+  Multimap<String, String> getHeaders() {
+    return headers;
+  }
+
+  Map<String, String> getMetadata() {
+    return metadata;
+  }
+
+  byte[] getResponse() {
+    // Supported to avoid copying data unnecessarily.
+    return getRawContentBytes();
+  }
+
+  public int getHttpStatusCode() {
+    return httpStatusCode;
+  }
+
+  public long getRefetchStrictNoCacheAfterMs() {
+    return refetchStrictNoCacheAfterMs;
+  }
+
+  /**
+   * Ensures that, when setting content bytes, the bytes' encoding is reflected
+   * in the current Content-Type header.
+   * Note, this method does NOT override existing Content-Type values if newEncoding is null.
+   * This allows charset to be set by header only, along with a byte array -- a very typical,
+   * and important, pattern when creating an HttpResponse in an HttpFetcher.
+   */
+  @Override
+  protected void setContentBytesState(byte[] newBytes, Charset newEncoding) {
+    super.setContentBytesState(newBytes, newEncoding);
+
+    // Set the new encoding of the raw bytes, in order to ensure that
+    // Content-Type headers are in sync w/ the content's encoding.
+    if (newEncoding != null) setEncoding(newEncoding);
+  }
+
+  private static GadgetHtmlParser unsupportedParser() {
+    return new GadgetHtmlParser(null) {
+      @Override
+      protected Document parseDomImpl(String source) throws GadgetException {
+        throw new UnsupportedOperationException("Using HttpResponseBuilder in non-rewriting context");
+      }
+
+      @Override
+      protected DocumentFragment parseFragmentImpl(String source)
+          throws GadgetException {
+        throw new UnsupportedOperationException("Using HttpResponseBuilder in non-rewriting context");
+      }
+    };
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseMetadataHelper.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseMetadataHelper.java
new file mode 100644
index 0000000..e435dd6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/HttpResponseMetadataHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.util.Base32;
+import org.apache.shindig.common.util.CharsetUtil;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Helper class to update HttpResponse metadata value.
+ *
+ * @since 2.0.0
+ */
+public class HttpResponseMetadataHelper {
+  public static final String DATA_HASH = "DataHash";
+  public static final String IMAGE_HEIGHT = "ImageHeight";
+  public static final String IMAGE_WIDTH = "ImageWidth";
+
+  //class name for logging purpose
+  private static final String classname = HttpResponseMetadataHelper.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  /**
+   * Return a copy of input response with additional metadata values.
+   * @param response source response
+   * @param values added metadata values
+   * @return copy of source response with updated metadata
+   */
+  public static HttpResponse updateMetadata(HttpResponse response, Map<String, String> values) {
+    Map<String, String> metadata = Maps.newHashMap(response.getMetadata());
+    // metadata.putAll(values);
+    for (Map.Entry<String, String> val : values.entrySet()) {
+      metadata.put(val.getKey(), val.getValue());
+    }
+    return new HttpResponseBuilder(response).setMetadata(metadata).create();
+  }
+
+  /**
+   * Calculate hash value for response and update metadata value (DATA_HASH)
+   * @return hash value
+   */
+  public String getHash(HttpResponse response) {
+    try {
+      MessageDigest md5 = MessageDigest.getInstance("MD5");
+      md5.update(response.getResponseAsBytes());
+      byte[] md5val = md5.digest();
+      return CharsetUtil.newUtf8String(Base32.encodeBase32(md5val));
+    } catch (NoSuchAlgorithmException e) {
+      // Should not happen
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, classname, "getHash", MessageKeys.ERROR_GETTING_MD5);
+      }
+    }
+    return null;
+  }
+
+  public static HttpResponse updateHash(HttpResponse response, HttpResponseMetadataHelper helper) {
+    if (helper != null) {
+      String hash = helper.getHash(response);
+      if (hash != null) {
+        return updateMetadata(response, ImmutableMap.<String, String>of(DATA_HASH, hash));
+      }
+    }
+    return response;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/InvalidationHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/InvalidationHandler.java
new file mode 100644
index 0000000..6724c16
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/InvalidationHandler.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.base.Strings;
+import org.apache.shindig.auth.AuthenticationMode;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.protocol.BaseRequestItem;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.Service;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+
+/**
+ * Handle cache invalidation API calls
+ *
+ * TODO : Sync with spec. Propose cache as service name, invalidate as operation
+ * viewer is always invalidated if available
+ */
+@Service(name = "cache")
+public class InvalidationHandler {
+
+  public static final String KEYS_PARAM = "invalidationKeys";
+
+  private final InvalidationService invalidation;
+
+
+  @Inject
+  public InvalidationHandler(InvalidationService invalidation) {
+    this.invalidation = invalidation;
+  }
+
+  @Operation(httpMethods = {"POST","GET"}, path = "/invalidate")
+  public void invalidate(BaseRequestItem request) {
+    if (Strings.isNullOrEmpty(request.getToken().getAppId()) &&
+        Strings.isNullOrEmpty(request.getToken().getAppUrl())) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+          "Cannot invalidate content without specifying application");
+    }
+
+    // Is the invalidation call from the application backend. If not we don't allow
+    // invalidation of resources or users other than @viewer
+    boolean isBackendInvalidation = AuthenticationMode.OAUTH_CONSUMER_REQUEST.name().equals(
+        request.getToken().getAuthenticationMode());
+
+    List<String> keys = request.getListParameter(KEYS_PARAM);
+    Set<String> userIds = Sets.newHashSet();
+    Set<Uri> resources = Sets.newHashSet();
+
+    // Assume the the viewer content is being invalidated if it is available
+    if (!Strings.isNullOrEmpty(request.getToken().getViewerId())) {
+      userIds.add(request.getToken().getViewerId());
+    }
+    if (keys != null) {
+      for (String key : keys) {
+        String lowerKey = key.toLowerCase();
+        if (lowerKey.startsWith("http")) {
+          // Assume key is a gadget spec, message bundle or other resource
+          // owned by the gadget
+          if (!isBackendInvalidation) {
+            throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+                "Cannot flush application resources from a gadget. " +
+                    "Must use OAuth consumer request");
+          }
+          resources.add(Uri.parse(key));
+        } else {
+          if ("@viewer".equals(key)) {
+            // Viewer is invalidated by default if available
+            continue;
+          }
+          if (!isBackendInvalidation && !key.equals(request.getToken().getViewerId())) {
+            throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+                "Cannot invalidate the content for a user other than the viewer from a gadget.");
+          }
+          userIds.add(key);
+        }
+      }
+    }
+    invalidation.invalidateApplicationResources(resources, request.getToken());
+    invalidation.invalidateUserResources(userIds, request.getToken());
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/InvalidationService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/InvalidationService.java
new file mode 100644
index 0000000..6d86dbb
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/InvalidationService.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.Set;
+
+/**
+ * Service implemented by the container to support content invalidation.
+ */
+@ImplementedBy(DefaultInvalidationService.class)
+public interface InvalidationService {
+
+  /**
+   * Header used to tag the content with its invalidation marker.
+   * Suppressed on output
+   */
+  public static final String INVALIDATION_HEADER = "X-Shindig-Invalidation";
+
+  /**
+   * Invalidate a set of cached resources that are part of the application specification itself.
+   * This includes gadget specs, manifests and message bundles
+   * @param uris of content to invalidate
+   * @param token identifying the calling application
+   */
+  void invalidateApplicationResources(Set<Uri> uris, SecurityToken token);
+
+  /**
+   * Invalidate all cached resources where the specified user ids were used as either the
+   * owner or viewer id when a signed or OAuth request was made for the content by the application
+   * identified in the security token.
+   * @param opensocialIds Set of user ids to invalidate authenticated/signed content for
+   * @param token identifying the calling application
+   */
+  void invalidateUserResources(Set<String> opensocialIds, SecurityToken token);
+
+  /**
+   * Is the specified HttpResponse still valid. If the request is signed or authenticated
+   * has its content been invalidated by a call to invalidateUserResource subsequent to the
+   * response being cached.
+   */
+  boolean isValid(HttpRequest request, HttpResponse response);
+
+  /**
+   * Mark the HttpResponse prior to caching it so that subsequent calls to isValid can detect
+   * if it has been invalidated.
+   */
+  HttpResponse markResponse(HttpRequest request, HttpResponse response);
+
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/MultipleResourceHttpFetcher.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/MultipleResourceHttpFetcher.java
new file mode 100644
index 0000000..0c37b31
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/MultipleResourceHttpFetcher.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.gadgets.GadgetException;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.FutureTask;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * This class provides simple way for doing parallel fetches for multiple
+ * resources using FutureTask's.
+ */
+public class MultipleResourceHttpFetcher {
+  private final RequestPipeline requestPipeline;
+  private final Executor executor;
+
+  public MultipleResourceHttpFetcher(RequestPipeline requestPipeline, Executor executor) {
+    this.requestPipeline = requestPipeline;
+    this.executor = executor;
+  }
+
+  /**
+   * Issue parallel requests to all resources that are needed.
+   *
+   * @param requests list of requests for which we want the resourses
+   * @return futureTasks List of Pairs of url,futureTask for all the requests
+   *    in same order as specified.
+   */
+  public List<Pair<Uri, FutureTask<RequestContext>>> fetchAll(List<HttpRequest> requests) {
+    List<Pair<Uri, FutureTask<RequestContext>>> futureTasks = Lists.newArrayList();
+    for (HttpRequest request : requests) {
+      futureTasks.add(Pair.of(request.getUri(), createHttpFetcher(request)));
+    }
+
+    return futureTasks;
+  }
+
+  /**
+   * Issue parallel requests to all the resources that are needed ignoring
+   * duplicates.
+   *
+   * @param requests list of urls for which we want the image resourses
+   * @return futureTasks map of url -> futureTask for all the requests sent.
+   */
+  public Map<Uri, FutureTask<RequestContext>> fetchUnique(List<HttpRequest> requests) {
+    Map<Uri, FutureTask<RequestContext>> futureTasks = Maps.newHashMap();
+    for (HttpRequest request : requests) {
+      Uri uri = request.getUri();
+      if (!futureTasks.containsKey(uri)) {
+        futureTasks.put(uri, createHttpFetcher(request));
+      }
+    }
+
+    return futureTasks;
+  }
+
+  // Fetch the content of the requested uri.
+  private FutureTask<RequestContext> createHttpFetcher(HttpRequest request) {
+    // Fetch the content of the requested uri.
+    FutureTask<RequestContext> httpFetcher =
+        new FutureTask<RequestContext>(new HttpFetchCallable(request, requestPipeline));
+    executor.execute(httpFetcher);
+    return httpFetcher;
+  }
+
+  private static class HttpFetchCallable implements Callable<RequestContext> {
+    private final HttpRequest httpReq;
+    private final RequestPipeline requestPipeline;
+
+    public HttpFetchCallable(HttpRequest httpReq, RequestPipeline requestPipeline) {
+      this.httpReq = httpReq;
+      this.requestPipeline = requestPipeline;
+    }
+
+    public RequestContext call() {
+      HttpResponse httpResp = null;
+      GadgetException gadgetException = null;
+      try {
+        httpResp = requestPipeline.execute(httpReq);
+      } catch (GadgetException e){
+        gadgetException = e;
+      }
+      return new RequestContext(httpReq, httpResp, gadgetException);
+    }
+  }
+
+  // Encapsulates the response context of a single resource fetch.
+  public static class RequestContext {
+    private final HttpRequest httpReq;
+    private final HttpResponse httpResp;
+    private final GadgetException gadgetException;
+
+    public HttpRequest getHttpReq() {
+      return httpReq;
+    }
+
+    public HttpResponse getHttpResp() {
+      return httpResp;
+    }
+
+    public GadgetException getGadgetException() {
+      return gadgetException;
+    }
+
+    public RequestContext(HttpRequest httpReq, HttpResponse httpResp, GadgetException ge) {
+      this.httpReq = httpReq;
+      this.httpResp = httpResp;
+      this.gadgetException = ge;
+    }
+
+    @Override
+    public int hashCode() {
+      return httpReq.hashCode()
+        ^ httpResp.hashCode()
+        ^ gadgetException.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof RequestContext)) {
+        return false;
+      }
+      RequestContext reqCxt = (RequestContext)obj;
+      return httpReq.equals(reqCxt.httpReq) &&
+          (httpResp != null ? httpResp.equals(reqCxt.httpResp) : reqCxt.httpResp == null) &&
+          (gadgetException != null ? gadgetException.equals(reqCxt.gadgetException) :
+              reqCxt.gadgetException == null);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/NoOpInvalidationService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/NoOpInvalidationService.java
new file mode 100644
index 0000000..b824e97
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/NoOpInvalidationService.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+
+import java.util.Set;
+
+/**
+ * No-Op implementation of the invalidation service
+ */
+public class NoOpInvalidationService implements InvalidationService {
+
+  public void invalidateApplicationResources(Set<Uri> uris, SecurityToken token) {
+    // No op
+  }
+
+  public void invalidateUserResources(Set<String> opensocialIds, SecurityToken token) {
+    // No op
+  }
+
+  public boolean isValid(HttpRequest request, HttpResponse response) {
+    return true;
+  }
+
+  public HttpResponse markResponse(HttpRequest request, HttpResponse response) {
+    return response;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RequestPipeline.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RequestPipeline.java
new file mode 100644
index 0000000..707b961
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/http/RequestPipeline.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.shindig.gadgets.GadgetException;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Implements a complete HTTP request pipeline. Performs caching, authentication, and serves as an
+ * injection point for any custom request pipeline injection.
+ *
+ * NOTE: When using cache, please ensure that you are checking response.isStrictNoCache() before
+ * serving out. Because cache may have private contents, even though marked stale.
+ * @see {AbstractHttpCache} for details.
+ */
+@ImplementedBy(DefaultRequestPipeline.class)
+public interface RequestPipeline {
+
+  /**
+   * Execute the given request.
+   *
+   * TODO: This should throw a custom exception type.
+   */
+  HttpResponse execute(HttpRequest request) throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddJslInfoVariableProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddJslInfoVariableProcessor.java
new file mode 100644
index 0000000..28d4767
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddJslInfoVariableProcessor.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+
+/**
+ * Injects a global ___jsl variable with information about the JS request.
+ *
+ * Used when loading embedded JS configuration in core.config/config.js.
+ */
+public class AddJslInfoVariableProcessor implements JsProcessor {
+  private static final Logger LOG = Logger.getLogger(AddJslInfoVariableProcessor.class.getName());
+  private static final String CODE_ID = "[jsload-code-info]";
+
+  @VisibleForTesting
+  static final String BASE_HINT_TEMPLATE = "window['___jsl'] = window['___jsl'] || {};";
+
+  @VisibleForTesting
+  static final String FEATURES_HINT_TEMPLATE = "window['___jsl']['f'] = [%s];";
+
+  private final FeatureRegistryProvider featureRegistryProvider;
+
+  @Inject
+  public AddJslInfoVariableProcessor(FeatureRegistryProvider featureRegistryProvider) {
+    this.featureRegistryProvider = featureRegistryProvider;
+  }
+
+  public boolean process(JsRequest jsRequest, JsResponseBuilder builder) {
+    JsUri jsUri = jsRequest.getJsUri();
+    if (!jsUri.isNohint()) {
+      String features = getFeatures(jsUri);
+      builder.prependJs(String.format(FEATURES_HINT_TEMPLATE, features), CODE_ID, true);
+      builder.prependJs(BASE_HINT_TEMPLATE, CODE_ID);
+    }
+    return true;
+  }
+
+  private String getFeatures(JsUri jsUri) {
+    FeatureRegistry registry = null;
+    String repository = jsUri.getRepository();
+    try {
+      registry = featureRegistryProvider.get(repository);
+    } catch (GadgetException e) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.log(Level.WARNING, "No registry found for repository: " + repository, e);
+      }
+    }
+
+    if (registry != null) {
+      List<String> features = registry.getFeatures(jsUri.getLibs());
+      Set<String> encoded = Sets.newTreeSet();
+      for (String feature : features) {
+        encoded.add('\'' + StringEscapeUtils.escapeEcmaScript(feature) + '\'');
+      }
+
+      return StringUtils.join(encoded, ",");
+    }
+
+    return "";
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddJslLoadedVariableProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddJslLoadedVariableProcessor.java
new file mode 100644
index 0000000..28219b0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddJslLoadedVariableProcessor.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+
+/**
+ * Injects a global ___jsl.l variable with information about the JS request.
+ *
+ * Used when loading embedded JS configuration in core.config/config.js.
+ */
+public class AddJslLoadedVariableProcessor implements JsProcessor {
+  private static final Logger LOG = Logger.getLogger(AddJslLoadedVariableProcessor.class.getName());
+  private static final String CODE_ID = "[jsload-loaded-info]";
+
+  @VisibleForTesting
+  static final String TEMPLATE =
+      "window['___jsl']['l'] = (window['___jsl']['l'] || []).concat(%s);";
+
+  private final FeatureRegistryProvider featureRegistryProvider;
+
+  @Inject
+  public AddJslLoadedVariableProcessor(FeatureRegistryProvider featureRegistryProvider) {
+    this.featureRegistryProvider = featureRegistryProvider;
+  }
+
+  public boolean process(JsRequest jsRequest, JsResponseBuilder builder) throws JsException {
+    JsUri jsUri = jsRequest.getJsUri();
+
+    FeatureRegistry registry = null;
+    String repository = jsUri.getRepository();
+    try {
+      registry = featureRegistryProvider.get(jsUri.getRepository());
+    } catch (GadgetException e) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.log(Level.WARNING, "No registry found for repository: " + repository, e);
+      }
+    }
+
+    if (registry != null && !jsUri.isNohint()) {
+      Set<String> allfeatures = registry.getAllFeatureNames();
+
+      Set<String> libs = Sets.newTreeSet();
+      libs.addAll(jsUri.getLibs());
+      libs.removeAll(jsUri.getLoadedLibs());
+      libs.retainAll(allfeatures);
+
+      String array = toArrayString(libs);
+      builder.appendJs(String.format(TEMPLATE, array), CODE_ID, true);
+    }
+    return true;
+  }
+
+  private String toArrayString(Set<String> bundles) {
+    StringBuilder builder = new StringBuilder();
+    for (String bundle : bundles) {
+      if (builder.length() > 0) builder.append(',');
+      builder.append('\'').append(StringEscapeUtils.escapeEcmaScript(bundle)).append('\'');
+    }
+    return '[' + builder.toString() + ']';
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddOnloadFunctionProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddOnloadFunctionProcessor.java
new file mode 100644
index 0000000..82dfdc5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AddOnloadFunctionProcessor.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Adds code to call an onload function after the Javascript code has been interpreted.
+ * The onload function can be injected in one of two ways:
+ * 1. Via the &onload query parameter. This is used for "first-stage" JS when the method
+ *    is directly injected.
+ * 2. Via ___jsl.c variable. This is set by "loader" JS which loads highly-cached
+ *    2nd-stage code that does not have onload injected.
+ */
+public class AddOnloadFunctionProcessor implements JsProcessor {
+  private static final String ONLOAD_CODE_ID = "[onload-processor]";
+  private static final String JSL_CODE_ID = "[jsload-callback]";
+
+  @VisibleForTesting
+  public static final String ONLOAD_FUNCTION_NAME_ERROR = "Invalid onload callback specified";
+
+  @VisibleForTesting
+  public static final String ONLOAD_JS_TPL = "(function() {" +
+      "var nm='%s';" +
+      "if (typeof window[nm]==='function') {" +
+      "window[nm]();" +
+      '}' +
+      "})();";
+
+  @VisibleForTesting
+  static final String JSL_CALLBACK_JS = "(function(){" +
+      "var j=window['___jsl'];" +
+      "if(j['c']&&--j['o']<=0){"+
+      "j['c']();" +
+      "delete j['c'];" +
+      "delete j['o'];" +
+      '}' +
+      "})();";
+
+  private static final Pattern ONLOAD_FN_PATTERN = Pattern.compile("[a-zA-Z0-9_]+");
+
+  public boolean process(JsRequest request, JsResponseBuilder builder)
+      throws JsException {
+    JsUri jsUri = request.getJsUri();
+
+    // Add onload handler to add callback function.
+    String onloadStr = jsUri.getOnload();
+    if (onloadStr != null) {
+      if (!ONLOAD_FN_PATTERN.matcher(onloadStr).matches()) {
+        throw new JsException(HttpServletResponse.SC_BAD_REQUEST, ONLOAD_FUNCTION_NAME_ERROR);
+      }
+      builder.appendJs(createOnloadScript(onloadStr), ONLOAD_CODE_ID);
+    } else if (jsUri.isNohint()) {
+      // "Second-stage" JS, which may have had a callback set by loader.
+      // This type of JS doesn't create a hint, but does attempt to use one.
+      builder.appendJs(JSL_CALLBACK_JS, JSL_CODE_ID, true);
+    }
+    return true;
+  }
+
+  @VisibleForTesting
+  protected String createOnloadScript(String function) {
+    return String.format(ONLOAD_JS_TPL, StringEscapeUtils.escapeEcmaScript(function));
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AnonFuncWrappingProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AnonFuncWrappingProcessor.java
new file mode 100644
index 0000000..766d271
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/AnonFuncWrappingProcessor.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.apache.shindig.gadgets.JsCompileMode;
+
+public class AnonFuncWrappingProcessor implements JsProcessor {
+  public boolean process(JsRequest jsRequest, JsResponseBuilder builder)
+      throws JsException {
+    if (jsRequest.getJsUri().getCompileMode() != JsCompileMode.COMPILE_CONCAT) {
+      builder.prependJs("(function(){", "[js-anon-wrapper]");
+      builder.appendJs("})();", "[js-anon-wrapper]");
+    }
+    return true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/BaseSurfaceJsProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/BaseSurfaceJsProcessor.java
new file mode 100644
index 0000000..0ff7045
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/BaseSurfaceJsProcessor.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Provider;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public abstract class BaseSurfaceJsProcessor implements JsProcessor {
+
+  protected final FeatureRegistryProvider featureRegistryProvider;
+  protected final Provider<GadgetContext> context;
+
+  protected BaseSurfaceJsProcessor(FeatureRegistryProvider featureRegistryProvider,
+      Provider<GadgetContext> context) {
+    this.featureRegistryProvider = featureRegistryProvider;
+    this.context = context;
+  }
+
+  protected final FeatureRegistry getFeatureRegistry(JsUri jsUri) throws JsException {
+    try {
+      return featureRegistryProvider.get(jsUri.getRepository());
+    } catch (GadgetException e) {
+      throw new JsException(e.getHttpStatusCode(), e.getMessage());
+    }
+  }
+
+  protected final List<String> getExports(FeatureBundle bundle, JsUri jsUri) {
+    // Add exports of bundle, regardless.
+    if (jsUri.getCompileMode() == JsCompileMode.CONCAT_COMPILE_EXPORT_ALL) {
+      return bundle.getApis(ApiDirective.Type.JS, true);
+
+    // Add exports of bundle if it is an explicitly-specified feature.
+    } else if (jsUri.getCompileMode() == JsCompileMode.CONCAT_COMPILE_EXPORT_EXPLICIT) {
+      if (jsUri.getLibs().contains(bundle.getName())) {
+        return bundle.getApis(ApiDirective.Type.JS, true);
+      }
+    }
+
+    return Lists.newArrayList();
+  }
+
+  protected final List<JsContent> getSurfaceJsContents(
+      FeatureRegistry featureRegistry, String featureName) {
+    ImmutableList.Builder<JsContent> result = ImmutableList.builder();
+    LookupResult lookup = featureRegistry.getFeatureResources(context.get(),
+        ImmutableList.of(featureName), null);
+    for (FeatureBundle bundle : lookup.getBundles()) {
+      for (FeatureResource resource : bundle.getResources()) {
+        result.add(JsContent.fromFeature(
+            resource.getDebugContent(), resource.getName(),
+            bundle, resource));
+      }
+    }
+    return result.build();
+  }
+
+  protected Collection<Input> generateInputs(List<String> symbols) {
+    Map<String, Input> result = Maps.newLinkedHashMap();
+    for (String symbol : symbols) {
+      String ns = getNamespace(symbol);
+      Input input = result.get(ns);
+      if (input == null) {
+        input = (ns != null) ? Input.newLocal(ns, expandNamespace(ns)) : Input.newGlobal();
+        result.put(ns, input);
+      }
+      String property = (ns != null) ? getProperty(symbol) : symbol;
+      input.properties.add(property);
+    }
+    return result.values();
+  }
+
+  private List<String> expandNamespace(String namespace) {
+    List<String> result = Lists.newArrayList();
+    for (int from = 0; ;) {
+      int idx = namespace.indexOf('.', from);
+      if (idx >= 0) {
+        result.add(namespace.substring(0, idx));
+        from = idx + 1;
+      } else {
+        result.add(namespace);
+        break;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Return the namespace for symbol (before last dot). If symbol is global,
+   * return null, to indicate "window" namespace.
+   */
+  private String getNamespace(String symbol) {
+    int idx = symbol.lastIndexOf('.');
+    return (idx >= 0) ? symbol.substring(0, idx) : null;
+  }
+
+  /**
+   * Return the property of symbol (after last dot). If symbol is global,
+   * return the original string.
+   */
+  private String getProperty(String symbol) {
+    int idx = symbol.lastIndexOf('.');
+    return (idx >= 0) ? symbol.substring(idx + 1) : symbol;
+  }
+
+  protected static class Input {
+    String namespace;
+    List<String> components;
+    List<String> properties;
+
+    private Input(String namespace, List<String> components) {
+      this.namespace = namespace;
+      this.components = components;
+      this.properties = Lists.newArrayList();
+    }
+
+    static Input newGlobal() {
+      return new Input(null, ImmutableList.<String>of());
+    }
+
+    static Input newLocal(String namespace, List<String> components) {
+      return new Input(namespace, components);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/CajaJsSubtractingProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/CajaJsSubtractingProcessor.java
new file mode 100644
index 0000000..321b9e6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/CajaJsSubtractingProcessor.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.UriCommon;
+
+import java.util.Map;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+public class CajaJsSubtractingProcessor implements JsProcessor {
+
+  @VisibleForTesting
+  static final String ATTRIB_VALUE = "1";
+
+  public boolean process(JsRequest jsRequest, JsResponseBuilder builder) {
+    if (!jsRequest.getJsUri().cajoleContent()) {
+      ImmutableList.Builder<JsContent> listBuilder = ImmutableList.builder();
+      for (JsContent js : builder.build().getAllJsContent()) {
+        if (!isCajole(js)) {
+          listBuilder.add(js);
+        }
+      }
+      builder.clearJs().appendAllJs(listBuilder.build());
+    }
+    return true;
+  }
+
+  private boolean isCajole(JsContent js) {
+    FeatureResource resource = js.getFeatureResource();
+    if (resource != null) {
+      Map<String, String> attribs = resource.getAttribs();
+      if (attribs != null) {
+        String attrib = attribs.get(UriCommon.Param.CAJOLE.getKey());
+        return ATTRIB_VALUE.equals(attrib);
+      }
+    }
+    return false;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/CompilationProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/CompilationProcessor.java
new file mode 100644
index 0000000..5252267
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/CompilationProcessor.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.rewrite.js.JsCompiler;
+
+public class CompilationProcessor implements JsProcessor {
+  private final JsCompiler compiler;
+
+  @Inject
+  public CompilationProcessor(JsCompiler compiler) {
+    this.compiler = compiler;
+  }
+
+  /**
+   * Compile content in the inbound JsResponseBuilder.
+   * TODO: Convert JsCompiler to take JsResponseBuilder directly rather than Iterable<JsContent>
+   * @throws JsException
+   */
+  public boolean process(JsRequest request, JsResponseBuilder builder) throws JsException {
+    Iterable<JsContent> jsContents = builder.build().getAllJsContent();
+    for (JsContent jsc : jsContents) {
+      FeatureBundle bundle = jsc.getFeatureBundle();
+      if (bundle != null) {
+        builder.appendExterns(bundle.getApis(ApiDirective.Type.JS, true));
+      }
+    }
+
+    JsResponse result = compiler.compile(request.getJsUri(), jsContents,
+        builder.build().getExterns());
+
+    builder.clearJs().appendAllJs(result.getAllJsContent());
+    builder.setStatusCode(result.getStatusCode());
+    builder.addErrors(result.getErrors());
+    int refresh = result.getCacheTtlSecs();
+    if (refresh > 0) {
+      // Allow ttl overwrite by compiler
+      builder.setCacheTtlSecs(refresh);
+    }
+    return true;
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/ConfigInjectionProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/ConfigInjectionProcessor.java
new file mode 100644
index 0000000..d422bb8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/ConfigInjectionProcessor.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.config.ConfigProcessor;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ConfigInjectionProcessor implements JsProcessor {
+  @VisibleForTesting
+  static final String GADGETS_FEATURES_KEY = "gadgets.features";
+  @VisibleForTesting
+  static final String CONFIG_FEATURE = "core.config.base";
+
+  protected static final String CONFIG_GLOBAL_KEY_TPL = "window['___cfg']=%s;\n";
+  protected static final String CONFIG_INIT_ID = "[config-injection]";
+  protected static final String CONFIG_INIT_TPL = "gadgets.config.init(%s);\n";
+  protected static final String CONFIG_INJECT_CODE =
+      "window['___jsl'] = window['___jsl'] || {};" +
+      "(window['___jsl']['ci'] = (window['___jsl']['ci'] || [])).push(%s);";
+
+  private final FeatureRegistryProvider registryProvider;
+  private final ConfigProcessor configProcessor;
+
+  @Inject
+  public ConfigInjectionProcessor(
+      FeatureRegistryProvider registryProvider,
+      ConfigProcessor configProcessor) {
+    this.registryProvider = registryProvider;
+    this.configProcessor = configProcessor;
+  }
+
+  public boolean process(JsRequest request, JsResponseBuilder builder) throws JsException {
+    JsUri jsUri = request.getJsUri();
+    GadgetContext ctx = new JsGadgetContext(jsUri);
+    FeatureRegistry registry;
+    try {
+      registry = registryProvider.get(jsUri.getRepository());
+    } catch (GadgetException e) {
+      throw new JsException(e.getHttpStatusCode(), e.getMessage());
+    }
+
+    // Append gadgets.config initialization if not in standard gadget mode.
+    if (ctx.getRenderingContext() != RenderingContext.GADGET) {
+      List<String> allReq = registry.getFeatures(jsUri.getLibs());
+      Collection<String> loaded = jsUri.getLoadedLibs();
+
+      // Only inject config for features not already present and configured.
+      List<String> newReq = subtractCollection(allReq, loaded);
+
+      Map<String, Object> config = configProcessor.getConfig(
+          ctx.getContainer(), newReq, request.getHost(), null);
+      if (!config.isEmpty()) {
+        String configJson = JsonSerializer.serialize(config);
+        if (allReq.contains(CONFIG_FEATURE) || loaded.contains(CONFIG_FEATURE)) {
+          // config lib is present: pass it data
+          injectBaseConfig(configJson, builder);
+        } else {
+          // config lib not available: use global variable
+          injectGlobalConfig(configJson, builder);
+        }
+      }
+    }
+    return true;
+  }
+
+  protected void injectBaseConfig(String configJson, JsResponseBuilder builder) {
+    builder.prependJs(String.format(CONFIG_INJECT_CODE, configJson), CONFIG_INIT_ID);
+    builder.appendJs(String.format(CONFIG_INIT_TPL, configJson), CONFIG_INIT_ID);
+  }
+
+  protected void injectGlobalConfig(String configJson, JsResponseBuilder builder) {
+    builder.appendJs(String.format(CONFIG_GLOBAL_KEY_TPL, configJson), CONFIG_INIT_ID);
+  }
+
+  private List<String> subtractCollection(Collection<String> root, Collection<String> subtracted) {
+    Set<String> result = Sets.newHashSet(root);
+    result.removeAll(subtracted);
+    return Lists.newArrayList(result);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DefaultJsProcessorRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DefaultJsProcessorRegistry.java
new file mode 100644
index 0000000..c1ba797
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DefaultJsProcessorRegistry.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import java.util.List;
+
+/**
+ * Default implementation of {@link JsProcessorRegistry}, using an injected list
+ * of processors.
+ */
+public class DefaultJsProcessorRegistry implements JsProcessorRegistry {
+
+  private final List<JsProcessor> preProcessors;
+  private final List<JsProcessor> optionalProcessors;
+  private final List<JsProcessor> requiredProcessors;
+
+  @Inject
+  public DefaultJsProcessorRegistry(
+      @Named("shindig.js.pre-processors") List<JsProcessor> preProcessors,
+      @Named("shindig.js.optional-processors") List<JsProcessor> optionalProcessors,
+      @Named("shindig.js.required-processors") List<JsProcessor> requiredProcessors) {
+    this.preProcessors = preProcessors;
+    this.optionalProcessors = optionalProcessors;
+    this.requiredProcessors = requiredProcessors;
+  }
+
+  public void process(JsRequest request, JsResponseBuilder response) throws JsException {
+    // JsProcessor defined in preProcessors can determine whether the js process really need to happen
+    // Typically, IfModifiedSinceProcessor is one of the preProcessors, if it sets a 304 status code,
+    // all the remaining JsProcessors in optional and required won't be started.
+    for (JsProcessor processor : preProcessors) {
+      if (!processor.process(request, response)){
+        return;
+      }
+    }
+    for (JsProcessor processor : optionalProcessors) {
+      if (!processor.process(request, response)) {
+        break;
+      }
+    }
+    // This pipeline sequentially executes JsProcessor, and can stop on any, bypassing
+    // the actual compilation process. This is put here so generated JS will still be
+    // compiled.
+    for (JsProcessor processor : requiredProcessors) {
+      processor.process(request, response);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DefaultJsServingPipeline.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DefaultJsServingPipeline.java
new file mode 100644
index 0000000..2fd484f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DefaultJsServingPipeline.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.inject.Inject;
+
+/**
+ * Default implementation of {@link JsServingPipeline}.
+ *
+ * The processing steps are executed by a {@link JsProcessorRegistry}, which can
+ * be configured or replaced to add and remove processing steps, or to execute
+ * different processing steps depending on the context.
+ */
+public class DefaultJsServingPipeline implements JsServingPipeline {
+
+  private final JsProcessorRegistry jsProcessorRegistry;
+
+  @Inject
+  public DefaultJsServingPipeline(JsProcessorRegistry jsProcessorRegistry) {
+    this.jsProcessorRegistry = jsProcessorRegistry;
+  }
+
+  public JsResponse execute(JsRequest jsRequest) throws JsException {
+    JsResponseBuilder resp = new JsResponseBuilder();
+    jsProcessorRegistry.process(jsRequest, resp);
+    final JsResponse response = resp.build();
+    if (response.isError()) {
+      throw new JsException(response.getStatusCode(), response.toErrorString());
+    }
+    return response;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DeferJsProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DeferJsProcessor.java
new file mode 100644
index 0000000..e4f1f5b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/DeferJsProcessor.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import java.util.List;
+
+public class DeferJsProcessor extends BaseSurfaceJsProcessor implements JsProcessor {
+
+  @VisibleForTesting
+  static final String FEATURE_NAME = "deferjs";
+
+  private static final String FUNCTION_NAME = "deferJs";
+
+  @Inject
+  public DeferJsProcessor(FeatureRegistryProvider featureRegistryProvider,
+      Provider<GadgetContext> context) {
+    super(featureRegistryProvider, context);
+  }
+
+  public boolean process(JsRequest jsRequest, JsResponseBuilder builder) throws JsException {
+    JsUri jsUri = jsRequest.getJsUri();
+    ImmutableList.Builder<JsContent> resp = ImmutableList.builder();
+    FeatureRegistry featureRegistry = getFeatureRegistry(jsUri);
+
+    boolean needDefers = false;
+    if (jsUri.isJsload()) {
+      // append all exports for deferred symbols
+      List<FeatureBundle> bundles = getSupportDeferBundles(featureRegistry, jsRequest);
+      for (FeatureBundle bundle : bundles) {
+        needDefers |= appendDeferJsStatements(resp, jsRequest.getJsUri(), bundle);
+      }
+    }
+
+    // TODO: Instead of clearing, do a replacement of feature impl with defer stubs.
+    // Clearing has an effect of ignoring previous processors work.
+    if (needDefers) {
+      builder.appendAllJs(getSurfaceJsContents(featureRegistry, FEATURE_NAME));
+    }
+    builder.appendAllJs(resp.build());
+    return true;
+  }
+
+  private boolean appendDeferJsStatements(ImmutableList.Builder<JsContent> builder,
+       JsUri jsUri, FeatureBundle bundle) {
+    List<String> exports = getExports(bundle, jsUri);
+    if (!exports.isEmpty()) {
+      StringBuilder sb = new StringBuilder();
+      for (Input input : generateInputs(exports)) {
+        sb.append(toDeferStatement(input));
+      }
+      builder.add(JsContent.fromFeature(sb.toString(), "[generated-symbol-exports]",
+          bundle, null));
+      return true;
+    }
+    return false;
+  }
+
+  private String toDeferStatement(Input input) {
+    StringBuilder result = new StringBuilder();
+
+    // Local namespace.
+    if (input.namespace != null) {
+      result.append(FUNCTION_NAME).append("('").append(input.namespace).append("',[");
+      for (int i = 0; i < input.properties.size(); i++) {
+        String prop = input.properties.get(i);
+        if (i > 0) result.append(',');
+        result.append('\'').append(prop).append('\'');
+      }
+      result.append("]);");
+
+    // Global/window namespace.
+    } else {
+      for (String prop : input.properties) {
+        result.append(FUNCTION_NAME).append("('").append(prop).append("');");
+      }
+    }
+    return result.toString();
+  }
+
+  private List<FeatureBundle> getSupportDeferBundles(FeatureRegistry registry, JsRequest jsRequest) {
+    List<FeatureBundle> result = Lists.newArrayList();
+    LookupResult lookup = registry.getFeatureResources(context.get(),
+      jsRequest.getNewFeatures(), null, false);
+    for (FeatureBundle bundle : lookup.getBundles()) {
+      if (bundle.isSupportDefer()) {
+        result.add(bundle);
+      }
+    }
+    return result;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/ExportJsProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/ExportJsProcessor.java
new file mode 100644
index 0000000..db51557
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/ExportJsProcessor.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import java.util.List;
+
+public class ExportJsProcessor extends BaseSurfaceJsProcessor implements JsProcessor {
+
+  @VisibleForTesting
+  static final String FEATURE_NAME = "exportjs";
+
+  private static final String FUNCTION_NAME = "exportJs";
+
+  @Inject
+  public ExportJsProcessor(FeatureRegistryProvider featureRegistryProvider,
+      Provider<GadgetContext> context) {
+    super(featureRegistryProvider, context);
+  }
+
+  public boolean process(JsRequest jsRequest, JsResponseBuilder builder) throws JsException {
+    JsUri jsUri = jsRequest.getJsUri();
+    ImmutableList.Builder<JsContent> resp = ImmutableList.builder();
+    FeatureRegistry featureRegistry = getFeatureRegistry(jsUri);
+
+    boolean needExports = false;
+    FeatureBundle last = null;
+    if (!jsUri.isJsload()) {
+      for (JsContent jsc : builder.build().getAllJsContent()) {
+        FeatureBundle current = jsc.getFeatureBundle();
+        if (last != null && current != last) {
+          needExports |= appendExportJsStatements(resp, jsUri, last);
+        }
+        resp.add(jsc);
+        last = current;
+      }
+      if (last != null) {
+        needExports |= appendExportJsStatements(resp, jsUri, last);
+      }
+    }
+
+    builder.clearJs();
+    if (needExports) {
+      builder.appendAllJs(getSurfaceJsContents(featureRegistry, FEATURE_NAME));
+    }
+    builder.appendAllJs(resp.build());
+    return true;
+  }
+
+  private boolean appendExportJsStatements(ImmutableList.Builder<JsContent> builder,
+      JsUri jsUri, FeatureBundle bundle) {
+    List<String> exports = getExports(bundle, jsUri);
+    if (!exports.isEmpty()) {
+      StringBuilder sb = new StringBuilder();
+      for (Input input : generateInputs(exports)) {
+        sb.append(toExportStatement(input));
+      }
+      builder.add(JsContent.fromFeature(sb.toString(), "[generated-symbol-exports]",
+          bundle, null));
+      return true;
+    }
+    return false;
+  }
+
+  private String toExportStatement(Input input) {
+    StringBuilder result = new StringBuilder();
+
+    // Local namespace.
+    if (input.namespace != null) {
+      result.append(FUNCTION_NAME).append("('").append(input.namespace).append("',[");
+      result.append(Joiner.on(',').join(input.components));
+      result.append("],{");
+      for (int i = 0; i < input.properties.size(); i++) {
+        String prop = input.properties.get(i);
+        if (i > 0) result.append(',');
+        result.append(prop).append(":'").append(prop).append('\'');
+      }
+      result.append("});");
+
+    // Global/window namespace.
+    } else {
+      for (String prop : input.properties) {
+        result.append(FUNCTION_NAME).append('(');
+        result.append('\'').append(prop).append("',[");
+        result.append(prop);
+        result.append("]);");
+      }
+    }
+    return result.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/GetJsContentProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/GetJsContentProcessor.java
new file mode 100644
index 0000000..cb6951f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/GetJsContentProcessor.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.rewrite.js.JsCompiler;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriStatus;
+
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Retrieves the requested Javascript code using a {@link JsProcessor}.
+ */
+public class GetJsContentProcessor implements JsProcessor {
+  public static final int DEFAULT_VERSIONED_MAXAGE = -1;
+  public static final int DEFAULT_UNVERSIONED_MAXAGE = 3600;
+  public static final int DEFAULT_INVALID_MAXAGE = 0;
+
+  private final FeatureRegistryProvider registryProvider;
+  private final JsCompiler compiler;
+
+  private int versionedMaxAge = DEFAULT_VERSIONED_MAXAGE;
+  private int unversionedMaxAge = DEFAULT_UNVERSIONED_MAXAGE;
+  private int invalidMaxAge = DEFAULT_INVALID_MAXAGE;
+
+  @Inject
+  public GetJsContentProcessor(
+      FeatureRegistryProvider registryProvider,
+      JsCompiler compiler) {
+    this.registryProvider = registryProvider;
+    this.compiler = compiler;
+  }
+
+  @Inject(optional=true)
+  public void setVersionedMaxAge(@Named("shindig.jscontent.versioned.maxage") Integer maxAge) {
+    if (maxAge != null) {
+      versionedMaxAge = maxAge;
+    }
+  }
+  @Inject(optional=true)
+  public void setUnversionedMaxAge(@Named("shindig.jscontent.unversioned.maxage") Integer maxAge) {
+    if (maxAge != null) {
+      unversionedMaxAge = maxAge;
+    }
+  }
+  @Inject(optional=true)
+  public void setInvalidMaxAge(@Named("shindig.jscontent.invalid.maxage") Integer maxAge) {
+    if (maxAge != null) {
+      invalidMaxAge = maxAge;
+    }
+  }
+
+  public boolean process(JsRequest request, JsResponseBuilder builder) throws JsException {
+    // Get JavaScript content from features aliases request.
+    JsUri jsUri = request.getJsUri();
+    GadgetContext ctx = new JsGadgetContext(jsUri);
+
+    FeatureRegistry registry;
+    try {
+      registry = registryProvider.get(jsUri.getRepository());
+    } catch (GadgetException e) {
+      throw new JsException(e.getHttpStatusCode(), e.getMessage());
+    }
+
+    // TODO: possibly warn on unknown/unrecognized libs.
+    List<FeatureBundle> requestedBundles = registry.getFeatureResources(
+        ctx, jsUri.getLibs(), null).getBundles();
+    List<FeatureBundle> loadedBundles = registry.getFeatureResources(
+        ctx, jsUri.getLoadedLibs(), null).getBundles();
+
+    Set<String> loadedFeatures = Sets.newHashSet();
+    for (FeatureBundle bundle : loadedBundles) {
+      loadedFeatures.add(bundle.getName());
+      builder.appendExterns(bundle.getApis(ApiDirective.Type.JS, true));
+      builder.appendExterns(bundle.getApis(ApiDirective.Type.JS, false));
+    }
+
+    // Collate all JS desired for the current request.
+    boolean isProxyCacheable = true;
+
+    for (FeatureBundle bundle : requestedBundles) {
+      // Exclude all transitively-dependent loaded features.
+      if (loadedFeatures.contains(bundle.getName())) {
+        continue;
+      }
+      builder.appendAllJs(compiler.getJsContent(jsUri, bundle));
+      for (FeatureResource featureResource : bundle.getResources()) {
+        isProxyCacheable = isProxyCacheable && featureResource.isProxyCacheable();
+      }
+    }
+
+    builder.setProxyCacheable(isProxyCacheable);
+    UriStatus uriStatus = jsUri.getStatus();
+    setResponseCacheTtl(builder, uriStatus != null ? uriStatus : UriStatus.VALID_UNVERSIONED);
+    return true;
+  }
+
+  /**
+   * Sets the cache TTL depending on the value of the {@link UriStatus} object.
+   *
+   * @param builder The {@link JsResponseBuilder} object.
+   * @param vstatus The {@link UriStatus} object.
+   */
+  protected void setResponseCacheTtl(JsResponseBuilder builder, UriStatus vstatus) {
+    switch (vstatus) {
+      case VALID_VERSIONED:
+        builder.setCacheTtlSecs(versionedMaxAge);
+        break;
+      case VALID_UNVERSIONED:
+        builder.setCacheTtlSecs(unversionedMaxAge);
+        break;
+      case INVALID_VERSION:
+        // URL is invalid in some way, likely version mismatch.
+        builder.setCacheTtlSecs(invalidMaxAge);
+        break;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/IfModifiedSinceProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/IfModifiedSinceProcessor.java
new file mode 100644
index 0000000..2000665
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/IfModifiedSinceProcessor.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.apache.shindig.gadgets.uri.UriStatus;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Returns a 304 Not Modified response if the request is for a versioned
+ * resource and contains an If-Modified-Since header. This works in this way
+ * because we rely on cache busting for versioned resources.
+ */
+public class IfModifiedSinceProcessor implements JsProcessor {
+
+  public static final int DEFAULT_VERSIONED_MAXAGE = -1;
+
+  private int versionedMaxAge = DEFAULT_VERSIONED_MAXAGE;
+
+  @Inject(optional=true)
+  public void setVersionedMaxAge(@Named("shindig.jscontent.versioned.maxage") Integer maxAge) {
+    if (maxAge != null) {
+      versionedMaxAge = maxAge;
+    }
+  }
+
+  public boolean process(JsRequest request, JsResponseBuilder builder) {
+    if (request.isInCache() &&
+        request.getJsUri().getStatus() == UriStatus.VALID_VERSIONED) {
+      builder.setStatusCode(HttpServletResponse.SC_NOT_MODIFIED);
+      builder.setCacheTtlSecs(versionedMaxAge);
+      return false;
+    }
+    return true;
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsCompilerModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsCompilerModule.java
new file mode 100644
index 0000000..3995ea5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsCompilerModule.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.apache.shindig.gadgets.rewrite.js.ClosureJsCompiler;
+import org.apache.shindig.gadgets.rewrite.js.JsCompiler;
+
+import com.google.inject.AbstractModule;
+
+/**
+ * Guice configuration for JS compilation.
+ */
+public class JsCompilerModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(JsCompiler.class).to(ClosureJsCompiler.class);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsContent.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsContent.java
new file mode 100644
index 0000000..752b166
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsContent.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureResource;
+
+/**
+ * Wrapper around JavaScript providing a way to track its provenance.
+ * Other metadata may be added as well, such as annotations regarding compilation,
+ * obfuscation, and so on.
+ */
+public class JsContent {
+  private final String content;
+  private final String source;
+  private final FeatureBundle bundle;
+  private final FeatureResource resource;
+  private final boolean noCompile;
+
+  public static JsContent fromText(String content, String source) {
+    return new JsContent(content, source, null, null, false);
+  }
+
+  public static JsContent fromText(String content, String source,
+      boolean noCompile) {
+    return new JsContent(content, source, null, null, noCompile);
+  }
+
+  public static JsContent fromFeature(String content, String source,
+      FeatureBundle bundle, FeatureResource resource) {
+    return new JsContent(content, source, bundle, resource, false);
+  }
+
+  public static JsContent fromFeature(String content, String source,
+      FeatureBundle bundle, FeatureResource resource, boolean noCompile) {
+    return new JsContent(content, source, bundle, resource, noCompile);
+  }
+
+  private JsContent(String content, String source,
+      FeatureBundle bundle, FeatureResource resource, boolean noCompile) {
+    this.content = content;
+    this.source = source;
+    this.bundle = bundle;
+    this.resource = resource;
+    this.noCompile = noCompile;
+  }
+
+  public String get() {
+    return content;
+  }
+
+  public String getSource() {
+    return source;
+  }
+
+  public FeatureBundle getFeatureBundle() {
+    return bundle;
+  }
+
+  public FeatureResource getFeatureResource() {
+    return resource;
+  }
+
+  /**
+   * This is usually only set to true for {@link JsProcessor} via {@link JsResponseBuilder}s
+   * that have dynamic output that is vulnerable to attacks where input variation
+   * would cause unique output.
+   *
+   * @return true if the content should not be run through an expensive compile
+   */
+  public boolean isNoCompile() {
+    return noCompile;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsException.java
new file mode 100644
index 0000000..73f5ee9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsException.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+/**
+ * Thrown when a step in the JavaScript processing pipeline results in an error.
+ */
+public class JsException extends Exception {
+
+  private final int statusCode;
+
+  public JsException(int statusCode, String msg) {
+    super(msg);
+    this.statusCode = statusCode;
+  }
+
+  public int getStatusCode() {
+    return statusCode;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsGadgetContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsGadgetContext.java
new file mode 100644
index 0000000..250b0ae
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsGadgetContext.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+/**
+ * GadgetContext for JsHandler called by FeatureRegistry when fetching the resources.
+ */
+public class JsGadgetContext extends GadgetContext {
+  private final RenderingContext renderingContext;
+  private final String container;
+  private final boolean debug;
+
+  public JsGadgetContext(JsUri ctx) {
+    this.renderingContext = ctx.getContext();
+    this.container = ctx.getContainer();
+    this.debug = ctx.isDebug();
+  }
+
+  @Override
+  public RenderingContext getRenderingContext() {
+    return renderingContext;
+  }
+
+  @Override
+  public String getContainer() {
+    return container;
+  }
+
+  @Override
+  public boolean getDebug() {
+    return debug;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsLoadProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsLoadProcessor.java
new file mode 100644
index 0000000..09b11f4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsLoadProcessor.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriCommon;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Emit JS loader code if the jsload query parameter is present in the request.
+ */
+public class JsLoadProcessor implements JsProcessor {
+  private static final String CODE_ID = "[jsloader-bootstrap]";
+
+  @VisibleForTesting
+  public static final String JSLOAD_ONLOAD_ERROR = "jsload requires onload";
+
+  @VisibleForTesting
+  public static final String JSLOAD_JS_TPL = "(function() {" +
+      "document.write('<scr' + 'ipt type=\"text/javascript\" src=\"%s\"></scr' + 'ipt>');" +
+      "})();"; // Concatenated to avoid some browsers do dynamic script injection.
+
+  @VisibleForTesting
+  public static final String ASYNC_JSLOAD_JS_TPL = "(function() {" +
+      "var s=document.createElement('script');" +
+      "s.src=\"%s\";" +
+      "document.getElementsByTagName('head')[0].appendChild(s);" +
+      "})();";
+
+  private final JsUriManager jsUriManager;
+  private final int jsloadTtlSecs;
+  private final boolean requireOnload;
+  private String template;
+
+  @Inject
+  public JsLoadProcessor(
+      JsUriManager jsUriManager,
+      @Named("shindig.jsload.ttl-secs") int jsloadTtlSecs,
+      @Named("shindig.jsload.require-onload-with-jsload") boolean requireOnload) {
+    this.jsUriManager = jsUriManager;
+    this.jsloadTtlSecs = jsloadTtlSecs;
+    this.requireOnload = requireOnload;
+    this.template = JSLOAD_JS_TPL;
+  }
+
+  @Inject(optional = true)
+  public void setUseAsync(@Named("shindig.jsload.async-mode") Boolean jsloadAsync) {
+    if (jsloadAsync) {
+      template = ASYNC_JSLOAD_JS_TPL;
+    }
+  }
+
+  public boolean process(JsRequest request, JsResponseBuilder builder)
+      throws JsException {
+    JsUri jsUri = request.getJsUri();
+
+    // Don't emit the JS itself; instead emit JS loader script that loads
+    // versioned JS. The loader script is much smaller and cacheable for a
+    // configurable amount of time.
+    if (jsUri.isJsload()) {
+      // Require users to specify &onload=. This ensures a reliable continuation
+      // of JS execution. IE asynchronously loads script, before it loads
+      // source-scripted and inlined JS.
+      if (requireOnload && jsUri.getOnload() == null) {
+        throw new JsException(HttpServletResponse.SC_BAD_REQUEST, JSLOAD_ONLOAD_ERROR);
+      }
+
+      int refresh = getCacheTtlSecs(jsUri);
+      builder.setCacheTtlSecs(refresh);
+      builder.setProxyCacheable(true);
+
+      doJsload(jsUri, builder);
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * @throws JsException
+   */
+  protected void doJsload(JsUri jsUri, JsResponseBuilder resp) throws JsException {
+    jsUri.setJsload(false);
+    jsUri.setNohint(true);
+    Uri incUri = jsUriManager.makeExternJsUri(jsUri);
+    // Make sure next fetch will get content:
+    incUri = new UriBuilder(incUri).addQueryParameter(UriCommon.Param.JSLOAD.getKey(), "0").toUri();
+    resp.appendJs(createJsloadScript(incUri), CODE_ID);
+  }
+
+  protected String createJsloadScript(Uri uri) {
+    String uriString = uri.toString();
+    return String.format(template, uriString);
+  }
+
+  private int getCacheTtlSecs(JsUri jsUri) {
+    if (jsUri.isNoCache()) {
+      return 0;
+    } else {
+      Integer jsUriRefresh = jsUri.getRefresh();
+      return (jsUriRefresh != null && jsUriRefresh >= 0)
+          ? jsUriRefresh : jsloadTtlSecs;
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsProcessor.java
new file mode 100644
index 0000000..83ba46d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsProcessor.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+/**
+ * A processing step to populate or modify a Javascript response.
+ */
+public interface JsProcessor {
+
+  /**
+   * Populates or modifies the Javascript response.
+   *
+   * @param jsRequest The JS request that originated this execution.
+   * @param builder The response builder to work on.
+   * @return Whether processing should continue after this processor.
+   * @throw JsException If an unrecoverable error occurred.
+   */
+  boolean process(JsRequest jsRequest, JsResponseBuilder builder)
+      throws JsException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsProcessorRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsProcessorRegistry.java
new file mode 100644
index 0000000..7375e51
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsProcessorRegistry.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * A class to run a series of processing steps on a JS response.
+ *
+ * The way the processing steps are registered is implementation-dependent.
+ */
+@ImplementedBy(DefaultJsProcessorRegistry.class)
+public interface JsProcessorRegistry {
+
+  /**
+   * Executes the processing steps.
+   *
+   * @param jsRequest The JS request that originated this execution.
+   * @param response A builder for the JS response.
+   * @throws JsException If any of the steps resulted in an error.
+   */
+  void process(JsRequest jsRequest, JsResponseBuilder response)
+      throws JsException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsRequest.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsRequest.java
new file mode 100644
index 0000000..3ae9d8c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsRequest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import com.google.common.collect.Lists;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Data about a JavaScript request.
+ *
+ * This class is instantiated via {@link JsRequestBuilder}.
+ */
+public class JsRequest {
+
+  private final JsUri jsUri;
+  private final String host;
+  private final boolean inCache;
+  private final FeatureRegistry registry;
+  private List<String> allFeatures;
+  private List<String> newFeatures;
+  private List<String> loadedFeatures;
+
+  JsRequest(JsUri jsUri, String host, boolean inCache, FeatureRegistry registry) {
+    this.jsUri = jsUri;
+    this.host = host;
+    this.inCache = inCache;
+    this.registry = registry;
+  }
+
+  /**
+   * @return this request's {@link JsUri}.
+   */
+  public JsUri getJsUri() {
+    return jsUri;
+  }
+
+  /**
+   * @return the host this request was directed to.
+   */
+  public String getHost() {
+    return host;
+  }
+
+  /**
+   * @return whether the client has this JS code in the cache.
+   */
+  public boolean isInCache() {
+    return inCache;
+  }
+
+  /**
+   * @return All features encapsulated by this request, including deps, in dep order.
+   */
+  public List<String> getAllFeatures() {
+    initFeaturesLists();
+    return allFeatures;
+  }
+
+  /**
+   * @return Features to be newly returned by this request (all - loaded), in dep order.
+   */
+  public List<String> getNewFeatures() {
+    initFeaturesLists();
+    return newFeatures;
+  }
+
+  /**
+   * @return Full list of all features previously loaded before this request, in dep order.
+   */
+  public List<String> getLoadedFeatures() {
+    initFeaturesLists();
+    return loadedFeatures;
+  }
+
+  private void initFeaturesLists() {
+    if (allFeatures == null) {
+      // Lazy-initialize these, to avoid computation where not needed.
+      allFeatures = registry.getFeatures(jsUri.getLibs());
+      loadedFeatures = registry.getFeatures(jsUri.getLoadedLibs());
+      newFeatures = Lists.newLinkedList();
+      for (String candidate : allFeatures) {
+        if (!loadedFeatures.contains(candidate)) {
+          newFeatures.add(candidate);
+        }
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsRequestBuilder.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsRequestBuilder.java
new file mode 100644
index 0000000..f728340
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsRequestBuilder.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Builds {@link JsRequest} instances.
+ */
+public class JsRequestBuilder {
+
+  private final JsUriManager jsUriManager;
+  private final FeatureRegistry registry;
+
+  @Inject
+  public JsRequestBuilder(JsUriManager jsUriManager,
+                          FeatureRegistry registry) {
+    this.jsUriManager = jsUriManager;
+    this.registry = registry;
+  }
+
+  /**
+   * Builds a {@link JsRequest} instance from the given request object.
+   *
+   * @param request The originating HTTP request object.
+   * @return The corresponding JsRequest object.
+   * @throws GadgetException If there was a problem parsing the URI.
+   */
+  public JsRequest build(HttpServletRequest request) throws GadgetException {
+    JsUri jsUri = jsUriManager.processExternJsUri(new UriBuilder(request).toUri());
+    String host = request.getHeader("Host");
+    boolean inCache = request.getHeader("If-Modified-Since") != null;
+    return build(jsUri, host, inCache);
+  }
+
+  /**
+   * Builds a {@link JsRequest} instance for a given JsUri/host pair.
+   * @param jsUri JsUri encapsulating the request.
+   * @param host Host context for the request.
+   * @return The corresponding JsRequest.
+   */
+  public JsRequest build(JsUri jsUri, String host) {
+    return build(jsUri, host, false);
+  }
+
+  protected JsRequest build(JsUri jsUri, String host, boolean inCache) {
+    return new JsRequest(jsUri, host, inCache, registry);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsResponse.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsResponse.java
new file mode 100644
index 0000000..50969ac
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsResponse.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An immutable object that contains the response for a JavaScript request.
+ * This object is used by JsHandler, JsProcessors, and JsCompiler alike.
+ */
+public class JsResponse {
+  private final List<JsContent> jsCode;
+  private final List<String> errors;
+  private final String externs;
+  private final int cacheTtlSecs;
+  private final int statusCode;
+  private final boolean proxyCacheable;
+  private String codeString;
+  private String errorString;
+
+  JsResponse(List<JsContent> jsCode, int statusCode, int cacheTtlSecs,
+      boolean proxyCacheable, List<String> errors, String externs) {
+    this.jsCode = Collections.unmodifiableList(jsCode);
+    this.errors = Collections.unmodifiableList(errors);
+    this.statusCode = statusCode;
+    this.cacheTtlSecs = cacheTtlSecs;
+    this.proxyCacheable = proxyCacheable;
+    this.externs = externs;
+  }
+
+  /**
+   * Returns the JavaScript code to serve.
+   */
+  public String toJsString() {
+    if (codeString == null) {
+      StringBuilder sb = new StringBuilder();
+      for (JsContent js : getAllJsContent()) {
+        sb.append(js.get());
+      }
+      codeString = sb.toString();
+    }
+    return codeString;
+  }
+
+  /**
+   * Returns an iterator starting at the beginning of all JS code in the response.
+   */
+  public Iterable<JsContent> getAllJsContent() {
+    return jsCode;
+  }
+
+  /**
+   * Returns the HTTP status code.
+   */
+  public int getStatusCode() {
+    return statusCode;
+  }
+
+  /**
+   * Returns whether the current response code is an error code.
+   */
+  public boolean isError() {
+    return statusCode >= 400;
+  }
+
+  /**
+   * Returns the cache TTL in seconds for this response.
+   *
+   * 0 seconds means "no cache"; a value below 0 means "cache forever".
+   */
+  public int getCacheTtlSecs() {
+    return cacheTtlSecs;
+  }
+
+  /**
+   * Returns whether the response can be cached by intermediary proxies.
+   */
+  public boolean isProxyCacheable() {
+    return proxyCacheable;
+  }
+
+  /**
+   * Returns a list of any error messages associated with this response.
+   */
+  public List<String> getErrors() {
+    return errors;
+  }
+
+  /**
+   * Returns a string of all error messages associated with this response.
+   */
+  public String toErrorString() {
+    if (errorString == null) {
+      StringBuilder sb = new StringBuilder();
+      for (String error : getErrors()) {
+        sb.append(error);
+      }
+      errorString = sb.toString();
+    }
+    return errorString;
+  }
+
+  /**
+   * Returns a string of generated externs.
+   */
+  public String getExterns() {
+    return externs;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsResponseBuilder.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsResponseBuilder.java
new file mode 100644
index 0000000..a5aa947
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsResponseBuilder.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A class with methods to create {@link JsResponse} objects.
+ */
+public class JsResponseBuilder {
+  private static final String EXTERN_DELIM = ";\n";
+
+  private List<JsContent> jsCode;
+  private final List<String> errors;
+  private int statusCode;
+  private int cacheTtlSecs;
+  private boolean proxyCacheable;
+  private final StringBuilder rawExterns;
+  private final List<String> externs;
+
+  public JsResponseBuilder() {
+    jsCode = Lists.newLinkedList();
+    statusCode = HttpServletResponse.SC_OK;
+    cacheTtlSecs = 0;
+    proxyCacheable = false;
+    errors = Lists.newLinkedList();
+    rawExterns = new StringBuilder();
+    externs = Lists.newLinkedList();
+  }
+
+  public JsResponseBuilder(JsResponse response) {
+    this();
+    if (response.getAllJsContent() != null) {
+      jsCode.addAll(Lists.newArrayList(response.getAllJsContent()));
+    }
+    if (response.getErrors() != null) {
+      errors.addAll(Lists.newArrayList(response.getErrors()));
+    }
+    if (response.getExterns() != null) {
+      rawExterns.append(response.getExterns());
+    }
+    statusCode = response.getStatusCode();
+    cacheTtlSecs = response.getCacheTtlSecs();
+    proxyCacheable = response.isProxyCacheable();
+  }
+
+  /**
+   * Prepend a JS to the response.
+   */
+  public JsResponseBuilder prependJs(JsContent jsContent) {
+    if (canAddContent(jsContent)) {
+      jsCode.add(0, jsContent);
+    }
+    return this;
+  }
+
+  /**
+   * Prepends JS to the output.
+   */
+  public JsResponseBuilder prependJs(String content, String name) {
+    return prependJs(JsContent.fromText(content, name));
+  }
+
+  /**
+   * Prepends JS to the output.
+   */
+  public JsResponseBuilder prependJs(String content, String name, boolean noCompile) {
+    return prependJs(JsContent.fromText(content, name, noCompile));
+  }
+
+  /**
+   * Insert a JS at a specific index.
+   */
+  public JsResponseBuilder insertJsAt(int index, JsContent jsContent) {
+    if (canAddContent(jsContent)) {
+      jsCode.add(index, jsContent);
+    }
+    return this;
+  }
+
+  /**
+   * Appends more JS to the response.
+   */
+  public JsResponseBuilder appendJs(JsContent jsContent) {
+    if (canAddContent(jsContent)) {
+      jsCode.add(jsContent);
+    }
+    return this;
+  }
+
+  /**
+   * Helper to append JS to the response w/ a name.
+   */
+  public JsResponseBuilder appendJs(String content, String name) {
+    return appendJs(JsContent.fromText(content, name));
+  }
+
+  /**
+   * Helper to append JS to the response w/ a name.
+   */
+  public JsResponseBuilder appendJs(String content, String name, boolean noCompile) {
+    return appendJs(JsContent.fromText(content, name, noCompile));
+  }
+
+  /**
+   * Helper to append a bunch of JS.
+   */
+  public JsResponseBuilder appendAllJs(Iterable<JsContent> jsBundle) {
+    for (JsContent content : jsBundle) {
+      appendJs(content);
+    }
+    return this;
+  }
+
+  /**
+   * Deletes all JavaScript code in the builder.
+   */
+  public JsResponseBuilder clearJs() {
+    this.jsCode = Lists.newLinkedList();
+    return this;
+  }
+
+  /**
+   * Sets the HTTP status code.
+   */
+  public JsResponseBuilder setStatusCode(int responseCode) {
+    this.statusCode = responseCode;
+    return this;
+  }
+
+  /**
+   * Returns the HTTP status code.
+   */
+  public int getStatusCode() {
+    return statusCode;
+  }
+
+  /**
+   * Adds an error to the response
+   */
+  public JsResponseBuilder addError(String error) {
+    this.errors.add(error);
+    return this;
+  }
+
+  /**
+   * Adds multiple errors to the response
+   */
+  public JsResponseBuilder addErrors(List<String> errs) {
+    this.errors.addAll(errs);
+    return this;
+  }
+
+  /**
+   * Sets the cache TTL in seconds for the response being built.
+   *
+   * 0 seconds means "no cache"; a value below 0 means "cache forever".
+   */
+  public JsResponseBuilder setCacheTtlSecs(int cacheTtlSecs) {
+    this.cacheTtlSecs = cacheTtlSecs;
+    return this;
+  }
+
+  /**
+   * Returns the cache TTL in seconds for the response.
+   */
+  public int getCacheTtlSecs() {
+    return cacheTtlSecs;
+  }
+
+  /**
+   * Sets whether the response can be cached by intermediary proxies.
+   */
+  public JsResponseBuilder setProxyCacheable(boolean proxyCacheable) {
+    this.proxyCacheable = proxyCacheable;
+    return this;
+  }
+
+  /**
+   * Returns whether the response can be cached by intermediary proxies.
+   */
+  public boolean isProxyCacheable() {
+    return proxyCacheable;
+  }
+
+  /**
+   * Appends a blob of raw extern.
+   */
+  public JsResponseBuilder appendRawExtern(String rawExtern) {
+    this.rawExterns.append(rawExtern).append(EXTERN_DELIM);
+    return this;
+  }
+
+  /**
+   * Appends a line of extern.
+   */
+  public JsResponseBuilder appendExtern(String extern) {
+    this.externs.add(extern);
+    return this;
+  }
+
+  /**
+   * Appends externs as from list of strings.
+   */
+  public JsResponseBuilder appendExterns(List<String> externs) {
+    for (String extern : externs) {
+      appendExtern(extern);
+    }
+    return this;
+  }
+
+  /**
+   * Deletes all externs in the builder.
+   */
+  public JsResponseBuilder clearExterns() {
+    int last = rawExterns.length();
+    this.rawExterns.delete(0, last);
+    this.externs.clear();
+    return this;
+  }
+
+  /**
+   * Builds a {@link JsResponse} object with the provided data.
+   */
+  public JsResponse build() {
+    return new JsResponse(jsCode, statusCode, cacheTtlSecs, proxyCacheable,
+        errors, rawExterns + buildExternString());
+  }
+
+  private String buildExternString() {
+    StringBuilder builder = new StringBuilder();
+    Set<String> set = Sets.newHashSet();
+    for (String ext : externs) {
+      List<String> expandedList = expand(ext);
+      for (String exp : expandedList) {
+        if (set.contains(exp)) continue;
+        if (exp.endsWith(".prototype")) continue;
+        if (!exp.contains(".")) builder.append("var ");
+        builder.append(exp).append(" = {}").append(EXTERN_DELIM);
+        set.add(exp);
+      }
+    }
+    return builder.toString();
+  }
+
+  private List<String> expand(String value) {
+    List<String> result = Lists.newArrayList();
+    StringBuilder cur = new StringBuilder();
+    for (String part : Splitter.on('.').split(value)) {
+      cur.append(cur.length() > 0 ? "." : "").append(part);
+      result.add(cur.toString());
+    }
+    return result;
+  }
+
+  private boolean canAddContent(JsContent jsContent) {
+    return jsContent.get().length() > 0;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsServingPipeline.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsServingPipeline.java
new file mode 100644
index 0000000..394988b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsServingPipeline.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * A pipeline to process Javascript requests.
+ *
+ * These requests go through several processing steps that may provide features
+ * like compilation, cajoling, etc.
+ */
+@ImplementedBy(DefaultJsServingPipeline.class)
+public interface JsServingPipeline {
+
+  /**
+   * Executes the steps in the pipeline and returns the resulting response.
+   *
+   * @param jsRequest The JS request.
+   * @return The JavaScript response generated by the pipeline.
+   * @throws JsException If any of the steps resulted in an error.
+   */
+  JsResponse execute(JsRequest jsRequest) throws JsException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsServingPipelineModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsServingPipelineModule.java
new file mode 100644
index 0000000..bd0ed29
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/JsServingPipelineModule.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.name.Named;
+
+import java.util.List;
+
+/**
+ * Guice configuration for the Javascript serving pipeline.
+ */
+public class JsServingPipelineModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    // nothing to configure here
+  }
+
+  @Provides
+  @Inject
+  @Named("shindig.js.pre-processors")
+  public List<JsProcessor> provideProcessors(
+      IfModifiedSinceProcessor ifModifiedSinceProcessor) {
+    return ImmutableList.<JsProcessor>of(ifModifiedSinceProcessor);
+  }
+
+  @Provides
+  @Inject
+  @Named("shindig.js.optional-processors")
+  public List<JsProcessor> provideProcessors(
+      AddJslInfoVariableProcessor addJslInfoVariableProcessor,
+      DeferJsProcessor deferJsProcessor,
+      JsLoadProcessor jsLoaderGeneratorProcessor,
+      GetJsContentProcessor getJsContentProcessor,
+      CajaJsSubtractingProcessor cajaJsSubtractingProcessor,
+      ExportJsProcessor exportJsProcessor,
+      SeparatorCommentingProcessor separatorCommentingProcessor,
+      ConfigInjectionProcessor configInjectionProcessor,
+      AddJslLoadedVariableProcessor addJslLoadedVariableProcessor,
+      AddOnloadFunctionProcessor addOnloadFunctionProcessor) {
+    jsLoaderGeneratorProcessor.setUseAsync(true);
+    return ImmutableList.of(
+        addJslInfoVariableProcessor,
+        deferJsProcessor,
+        jsLoaderGeneratorProcessor,
+        getJsContentProcessor,
+        cajaJsSubtractingProcessor,
+        exportJsProcessor,
+        separatorCommentingProcessor,
+        configInjectionProcessor,
+        addJslLoadedVariableProcessor,
+        addOnloadFunctionProcessor);
+  }
+
+  @Provides
+  @Inject
+  @Named("shindig.js.required-processors")
+  public List<JsProcessor> provideProcessors(
+      CompilationProcessor compilationProcessor,
+      AnonFuncWrappingProcessor anonFuncProcessor) {
+    return ImmutableList.of(
+        compilationProcessor,
+        anonFuncProcessor);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/SeparatorCommentingProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/SeparatorCommentingProcessor.java
new file mode 100644
index 0000000..81ce63c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/js/SeparatorCommentingProcessor.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import com.google.common.collect.ImmutableList;
+
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+
+public class SeparatorCommentingProcessor implements JsProcessor {
+
+  public boolean process(JsRequest jsRequest, JsResponseBuilder builder) {
+    ImmutableList.Builder<JsContent> jsBuilder = ImmutableList.builder();
+
+    FeatureBundle lastFeature = null;
+    for (JsContent js : builder.build().getAllJsContent()) {
+      FeatureBundle feature = js.getFeatureBundle();
+
+      // Entering a new feature, from none/text.
+      if (lastFeature == null && feature != null) {
+        jsBuilder.add(newComment(feature, true));
+
+      // Entering a new feature, from another feature.
+      } else if (lastFeature != null && feature != null && lastFeature != feature) {
+        jsBuilder.add(newComment(lastFeature, false));
+        jsBuilder.add(newComment(feature, true));
+
+      // Entering a text, from a feature
+      } else if (lastFeature != null && feature == null) {
+        jsBuilder.add(newComment(lastFeature, false));
+      }
+      jsBuilder.add(js);
+      lastFeature = feature;
+    }
+    // If there is a last feature.
+    if (lastFeature != null) {
+      jsBuilder.add(newComment(lastFeature, false));
+    }
+    builder.clearJs().appendAllJs(jsBuilder.build());
+    return true;
+  }
+
+  private JsContent newComment(FeatureBundle bundle, boolean start) {
+    String tag = start ? "start" : "end";
+    return JsContent.fromFeature(
+        "\n/* [" + tag + "] feature=" + bundle.getName() + " */\n",
+        "[comment-marker-" + tag + ']', bundle, null);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfo.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfo.java
new file mode 100644
index 0000000..9abecc7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfo.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import net.oauth.OAuthAccessor;
+
+import org.apache.shindig.gadgets.oauth.OAuthStore.ConsumerInfo;
+
+/**
+ * OAuth related data accessor
+ */
+public class AccessorInfo {
+
+  public static enum HttpMethod {
+    GET,
+    POST
+  }
+
+  public static enum OAuthParamLocation {
+    AUTH_HEADER,
+    POST_BODY,
+    URI_QUERY
+  }
+
+  private final OAuthAccessor accessor;
+  private final ConsumerInfo consumer;
+  private final HttpMethod httpMethod;
+  private final OAuthParamLocation paramLocation;
+  private String sessionHandle;
+  private long tokenExpireMillis;
+
+  public AccessorInfo(OAuthAccessor accessor, ConsumerInfo consumer, HttpMethod httpMethod,
+      OAuthParamLocation paramLocation, String sessionHandle, long tokenExpireMillis) {
+    this.accessor = accessor;
+    this.consumer = consumer;
+    this.httpMethod = httpMethod;
+    this.paramLocation = paramLocation;
+    this.sessionHandle = sessionHandle;
+    this.tokenExpireMillis = tokenExpireMillis;
+  }
+
+  public OAuthParamLocation getParamLocation() {
+    return paramLocation;
+  }
+
+  public OAuthAccessor getAccessor() {
+    return accessor;
+  }
+
+  public ConsumerInfo getConsumer() {
+    return consumer;
+  }
+
+  public HttpMethod getHttpMethod() {
+    return httpMethod;
+  }
+
+  public String getSessionHandle() {
+    return sessionHandle;
+  }
+
+  public void setSessionHandle(String sessionHandle) {
+    this.sessionHandle = sessionHandle;
+  }
+
+  public long getTokenExpireMillis() {
+    return tokenExpireMillis;
+  }
+
+  public void setTokenExpireMillis(long tokenExpireMillis) {
+    this.tokenExpireMillis = tokenExpireMillis;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java
new file mode 100644
index 0000000..216e32b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/AccessorInfoBuilder.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import net.oauth.OAuthAccessor;
+
+import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.OAuthStore.ConsumerInfo;
+
+/**
+ * Builder for AccessorInfo object.
+ */
+public class AccessorInfoBuilder {
+
+  private ConsumerInfo consumer;
+  private String requestToken;
+  private String accessToken;
+  private String tokenSecret;
+  private String sessionHandle;
+  private long tokenExpireMillis;
+  private OAuthParamLocation location;
+  private HttpMethod method;
+
+  public AccessorInfoBuilder() {
+  }
+
+  public AccessorInfo create(OAuthResponseParams responseParams) throws OAuthRequestException {
+    if (location == null) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM, "no location");
+    }
+    if (consumer == null) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM, "no consumer");
+    }
+
+    OAuthAccessor accessor = new OAuthAccessor(consumer.getConsumer());
+
+    // request token/access token/token secret can all be null, for signed fetch, or if the OAuth
+    // dance is just beginning
+    accessor.requestToken = requestToken;
+    accessor.accessToken = accessToken;
+    accessor.tokenSecret = tokenSecret;
+    return new AccessorInfo(accessor, consumer, method, location, sessionHandle, tokenExpireMillis);
+  }
+
+  public void setConsumer(ConsumerInfo consumer) {
+    this.consumer = consumer;
+  }
+
+  public void setRequestToken(String requestToken) {
+    this.requestToken = requestToken;
+  }
+
+  public void setAccessToken(String accessToken) {
+    this.accessToken = accessToken;
+  }
+
+  public void setTokenSecret(String tokenSecret) {
+    this.tokenSecret = tokenSecret;
+  }
+
+  public void setParameterLocation(OAuthParamLocation location) {
+    this.location = location;
+  }
+
+  public void setMethod(HttpMethod method) {
+    this.method = method;
+  }
+
+  public void setSessionHandle(String sessionHandle) {
+    this.sessionHandle = sessionHandle;
+  }
+
+  public void setTokenExpireMillis(long tokenExpireMillis) {
+    this.tokenExpireMillis = tokenExpireMillis;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStore.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStore.java
new file mode 100644
index 0000000..d523ddd
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStore.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.collect.Maps;
+
+import com.google.inject.Singleton;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthServiceProvider;
+import net.oauth.signature.RSA_SHA1;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret.KeyType;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Simple implementation of the {@link OAuthStore} interface. We use a
+ * in-memory hash map. If initialized with a private key, then the store will
+ * return an OAuthAccessor in {@code getOAuthAccessor} that uses that private
+ * key if no consumer key and secret could be found.
+ */
+@Singleton
+public class BasicOAuthStore implements OAuthStore {
+
+  private static final String CONSUMER_SECRET_KEY = "consumer_secret";
+  private static final String CONSUMER_KEY_KEY = "consumer_key";
+  private static final String KEY_TYPE_KEY = "key_type";
+  private static final String CALLBACK_URL = "callback_url";
+  private static final String OAUTH_BODY_HASH_KEY = "bodyHash";
+
+  /**
+   * HashMap of provider and consumer information. Maps BasicOAuthStoreConsumerIndexs (i.e.
+   * nickname of a service provider and the gadget that uses that nickname) to
+   * {@link BasicOAuthStoreConsumerKeyAndSecret}s.
+   */
+  private final Map<BasicOAuthStoreConsumerIndex, BasicOAuthStoreConsumerKeyAndSecret> consumerInfos;
+
+  /**
+   * HashMap of token information. Maps BasicOAuthStoreTokenIndexs (i.e. gadget id, token
+   * nickname, module id, etc.) to TokenInfos (i.e. access token and token
+   * secrets).
+   */
+  private final Map<BasicOAuthStoreTokenIndex, TokenInfo> tokens;
+
+  /**
+   * Key to use when no other key is found.
+   */
+  private BasicOAuthStoreConsumerKeyAndSecret defaultKey;
+
+  /**
+   * Callback to use when no per-key callback URL is found.
+   */
+  private String defaultCallbackUrl;
+
+  /** Number of times we looked up a consumer key */
+  private int consumerKeyLookupCount = 0;
+
+  /** Number of times we looked up an access token */
+  private int accessTokenLookupCount = 0;
+
+  /** Number of times we added an access token */
+  private int accessTokenAddCount = 0;
+
+  /** Number of times we removed an access token */
+  private int accessTokenRemoveCount = 0;
+
+  private Authority authority;
+
+  public BasicOAuthStore() {
+    consumerInfos = Maps.newHashMap();
+    tokens = Maps.newHashMap();
+  }
+
+  public void initFromConfigString(String oauthConfigStr) throws GadgetException {
+    try {
+      JSONObject oauthConfigs = new JSONObject(oauthConfigStr);
+      for (Iterator<?> i = oauthConfigs.keys(); i.hasNext();) {
+        String url = (String) i.next();
+        URI gadgetUri = new URI(url);
+        JSONObject oauthConfig = oauthConfigs.getJSONObject(url);
+        storeConsumerInfos(gadgetUri, oauthConfig);
+      }
+    } catch (JSONException e) {
+      throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR, e);
+    } catch (URISyntaxException e) {
+      throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR, e);
+    }
+  }
+
+  private void storeConsumerInfos(URI gadgetUri, JSONObject oauthConfig)
+      throws JSONException, GadgetException {
+    for (String serviceName : JSONObject.getNames(oauthConfig)) {
+      JSONObject consumerInfo = oauthConfig.getJSONObject(serviceName);
+      storeConsumerInfo(gadgetUri, serviceName, consumerInfo);
+    }
+  }
+
+  private void storeConsumerInfo(URI gadgetUri, String serviceName, JSONObject consumerInfo)
+      throws JSONException, GadgetException {
+    realStoreConsumerInfo(gadgetUri, serviceName, consumerInfo);
+  }
+
+  private void realStoreConsumerInfo(URI gadgetUri, String serviceName, JSONObject consumerInfo)
+      throws JSONException {
+    String callbackUrl = consumerInfo.optString(CALLBACK_URL, null);
+    String consumerSecret = consumerInfo.getString(CONSUMER_SECRET_KEY);
+    String consumerKey = consumerInfo.getString(CONSUMER_KEY_KEY);
+    String keyTypeStr = consumerInfo.getString(KEY_TYPE_KEY);
+    boolean oauthBodyHash = true;
+    String oauthBodyHashString = consumerInfo.optString(OAUTH_BODY_HASH_KEY);
+    if ("false".equalsIgnoreCase(oauthBodyHashString)) {
+      oauthBodyHash = false;
+    }
+    KeyType keyType = KeyType.HMAC_SYMMETRIC;
+
+    if ("RSA_PRIVATE".equals(keyTypeStr)) {
+      keyType = KeyType.RSA_PRIVATE;
+      consumerSecret = convertFromOpenSsl(consumerSecret);
+    } else if ("PLAINTEXT".equals(keyTypeStr)) {
+      keyType = KeyType.PLAINTEXT;
+    }
+
+    BasicOAuthStoreConsumerKeyAndSecret kas = new BasicOAuthStoreConsumerKeyAndSecret(
+        consumerKey, consumerSecret, keyType, null, callbackUrl, oauthBodyHash);
+
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(gadgetUri.toASCIIString());
+    index.setServiceName(serviceName);
+    setConsumerKeyAndSecret(index, kas);
+  }
+
+  // Support standard openssl keys by stripping out the headers and blank lines
+  public static String convertFromOpenSsl(String privateKey) {
+    return privateKey.replaceAll("-----[A-Z ]*-----", "").replace("\n", "");
+  }
+
+  public void setDefaultKey(BasicOAuthStoreConsumerKeyAndSecret defaultKey) {
+    this.defaultKey = defaultKey;
+  }
+
+  public void setDefaultCallbackUrl(String defaultCallbackUrl) {
+    this.defaultCallbackUrl = defaultCallbackUrl;
+  }
+
+  public void setConsumerKeyAndSecret(
+      BasicOAuthStoreConsumerIndex providerKey, BasicOAuthStoreConsumerKeyAndSecret keyAndSecret) {
+    consumerInfos.put(providerKey, keyAndSecret);
+  }
+
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  public ConsumerInfo getConsumerKeyAndSecret(
+      SecurityToken securityToken, String serviceName, OAuthServiceProvider provider)
+      throws GadgetException {
+    ++consumerKeyLookupCount;
+    BasicOAuthStoreConsumerIndex pk = new BasicOAuthStoreConsumerIndex();
+    pk.setGadgetUri(securityToken.getAppUrl());
+    pk.setServiceName(serviceName);
+    BasicOAuthStoreConsumerKeyAndSecret cks = consumerInfos.get(pk);
+    if (cks == null) {
+      cks = defaultKey;
+    }
+    if (cks == null) {
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+          "No key for gadget " + securityToken.getAppUrl() + " and service " + serviceName);
+    }
+    OAuthConsumer consumer;
+    final KeyType keyType = cks.getKeyType();
+    if (keyType == KeyType.RSA_PRIVATE) {
+      consumer = new OAuthConsumer(null, cks.getConsumerKey(), null, provider);
+      // The oauth.net java code has lots of magic.  By setting this property here, code thousands
+      // of lines away knows that the consumerSecret value in the consumer should be treated as
+      // an RSA private key and not an HMAC key.
+      consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.RSA_SHA1);
+      consumer.setProperty(RSA_SHA1.PRIVATE_KEY, cks.getConsumerSecret());
+    } else if  (keyType == KeyType.PLAINTEXT) {
+      consumer = new OAuthConsumer(null, cks.getConsumerKey(), cks.getConsumerSecret(), provider);
+      consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, "PLAINTEXT");
+    } else {
+      consumer = new OAuthConsumer(null, cks.getConsumerKey(), cks.getConsumerSecret(), provider);
+      consumer.setProperty(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
+    }
+    String callback = (cks.getCallbackUrl() != null ? cks.getCallbackUrl() : defaultCallbackUrl);
+
+    if (authority != null) {
+      callback = callback.replace("%authority%", authority.getAuthority());
+    }
+
+    return new ConsumerInfo(consumer, cks.getKeyName(), callback, cks.isOauthBodyHash());
+  }
+
+  private BasicOAuthStoreTokenIndex makeBasicOAuthStoreTokenIndex(
+      SecurityToken securityToken, String serviceName, String tokenName) {
+    BasicOAuthStoreTokenIndex tokenKey = new BasicOAuthStoreTokenIndex();
+    tokenKey.setGadgetUri(securityToken.getAppUrl());
+    tokenKey.setModuleId(securityToken.getModuleId());
+    tokenKey.setServiceName(serviceName);
+    tokenKey.setTokenName(tokenName);
+    tokenKey.setUserId(securityToken.getViewerId());
+    return tokenKey;
+  }
+
+  public TokenInfo getTokenInfo(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      String serviceName, String tokenName) {
+    ++accessTokenLookupCount;
+    BasicOAuthStoreTokenIndex tokenKey =
+        makeBasicOAuthStoreTokenIndex(securityToken, serviceName, tokenName);
+    return tokens.get(tokenKey);
+  }
+
+  public void setTokenInfo(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      String serviceName, String tokenName, TokenInfo tokenInfo) {
+    ++accessTokenAddCount;
+    BasicOAuthStoreTokenIndex tokenKey =
+        makeBasicOAuthStoreTokenIndex(securityToken, serviceName, tokenName);
+    tokens.put(tokenKey, tokenInfo);
+  }
+
+  public void removeToken(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      String serviceName, String tokenName) {
+    ++accessTokenRemoveCount;
+    BasicOAuthStoreTokenIndex tokenKey =
+        makeBasicOAuthStoreTokenIndex(securityToken, serviceName, tokenName);
+    tokens.remove(tokenKey);
+  }
+
+  public int getConsumerKeyLookupCount() {
+    return consumerKeyLookupCount;
+  }
+
+  public int getAccessTokenLookupCount() {
+    return accessTokenLookupCount;
+  }
+
+  public int getAccessTokenAddCount() {
+    return accessTokenAddCount;
+  }
+
+  public int getAccessTokenRemoveCount() {
+    return accessTokenRemoveCount;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreConsumerIndex.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreConsumerIndex.java
new file mode 100644
index 0000000..2f048c6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreConsumerIndex.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.base.Objects;
+
+/**
+ * Index into the token store by
+ */
+public class BasicOAuthStoreConsumerIndex {
+  private String gadgetUri;
+  private String serviceName;
+
+  public String getGadgetUri() {
+    return gadgetUri;
+  }
+  public void setGadgetUri(String gadgetUri) {
+    this.gadgetUri = gadgetUri;
+  }
+  public String getServiceName() {
+    return serviceName;
+  }
+  public void setServiceName(String serviceName) {
+    this.serviceName = serviceName;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(gadgetUri, serviceName);
+  }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof BasicOAuthStoreConsumerIndex)) {
+      return false;
+    }
+    final BasicOAuthStoreConsumerIndex other = (BasicOAuthStoreConsumerIndex) obj;
+    if (gadgetUri == null) {
+      if (other.gadgetUri != null) return false;
+    } else if (!gadgetUri.equals(other.gadgetUri)) return false;
+    if (serviceName == null) {
+      if (other.serviceName != null) return false;
+    } else if (!serviceName.equals(other.serviceName)) return false;
+    return true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreConsumerKeyAndSecret.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreConsumerKeyAndSecret.java
new file mode 100644
index 0000000..ee931d1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreConsumerKeyAndSecret.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+/**
+ * Data structure representing and OAuth consumer key and secret
+ */
+public class BasicOAuthStoreConsumerKeyAndSecret {
+
+  public static enum KeyType { HMAC_SYMMETRIC, RSA_PRIVATE, PLAINTEXT }
+
+  /** Value for oauth_consumer_key */
+  private final String consumerKey;
+
+  /** HMAC secret, or RSA private key, depending on keyType */
+  private final String consumerSecret;
+
+  /** Type of key */
+  private final KeyType keyType;
+
+  /** Name of public key to use with xoauth_public_key parameter.  May be null */
+  private final String keyName;
+
+  /** Callback URL associated with this consumer key */
+  private final String callbackUrl;
+
+  private final boolean oauthBodyHash;
+
+  public BasicOAuthStoreConsumerKeyAndSecret(String key, String secret, KeyType type, String name,
+          String callbackUrl) {
+    this(key, secret, type, name, callbackUrl, true);
+  }
+
+  public BasicOAuthStoreConsumerKeyAndSecret(String key, String secret, KeyType type, String name,
+      String callbackUrl, boolean oauthBodyHash) {
+    consumerKey = key;
+    consumerSecret = secret;
+    keyType = type;
+    keyName = name;
+    this.callbackUrl = callbackUrl;
+    this.oauthBodyHash = oauthBodyHash;
+  }
+
+  public String getConsumerKey() {
+    return consumerKey;
+  }
+
+  public String getConsumerSecret() {
+    return consumerSecret;
+  }
+
+  public KeyType getKeyType() {
+    return keyType;
+  }
+
+  public String getKeyName() {
+    return keyName;
+  }
+
+  public String getCallbackUrl() {
+    return callbackUrl;
+  }
+
+  public boolean isOauthBodyHash() {
+    return this.oauthBodyHash;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTokenIndex.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTokenIndex.java
new file mode 100644
index 0000000..d5400ac
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTokenIndex.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.base.Objects;
+
+/**
+ * Simple class representing oauth token store
+ */
+public class BasicOAuthStoreTokenIndex {
+
+  private String userId;
+  private String gadgetUri;
+  private long moduleId;
+  private String tokenName;
+  private String serviceName;
+
+  public String getUserId() {
+    return userId;
+  }
+  public void setUserId(String userId) {
+    this.userId = userId;
+  }
+  public String getGadgetUri() {
+    return gadgetUri;
+  }
+  public void setGadgetUri(String gadgetUri) {
+    this.gadgetUri = gadgetUri;
+  }
+  public long getModuleId() {
+    return moduleId;
+  }
+  public void setModuleId(long moduleId) {
+    this.moduleId = moduleId;
+  }
+  public String getTokenName() {
+    return tokenName;
+  }
+  public void setTokenName(String tokenName) {
+    this.tokenName = tokenName;
+  }
+  public String getServiceName() {
+    return serviceName;
+  }
+  public void setServiceName(String serviceName) {
+    this.serviceName = serviceName;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(gadgetUri, moduleId, serviceName, tokenName, userId);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (!(obj instanceof BasicOAuthStoreTokenIndex)) {
+      return false;
+    }
+    final BasicOAuthStoreTokenIndex other = (BasicOAuthStoreTokenIndex) obj;
+    if (gadgetUri == null) {
+      if (other.gadgetUri != null) return false;
+    } else if (!gadgetUri.equals(other.gadgetUri)) return false;
+    if (moduleId != other.moduleId) return false;
+    if (serviceName == null) {
+      if (other.serviceName != null) return false;
+    } else if (!serviceName.equals(other.serviceName)) return false;
+    if (tokenName == null) {
+      if (other.tokenName != null) return false;
+    } else if (!tokenName.equals(other.tokenName)) return false;
+    if (userId == null) {
+      if (other.userId != null) return false;
+    } else if (!userId.equals(other.userId)) return false;
+    return true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthCallbackGenerator.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthCallbackGenerator.java
new file mode 100644
index 0000000..bb6f07a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthCallbackGenerator.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.servlet.OAuthCallbackServlet;
+import org.apache.shindig.gadgets.uri.OAuthUriManager;
+
+/**
+ * Generates callback URLs for gadgets using OAuth 1.0a.  There are three relevant callback URLs:
+ *
+ * 1) The consumer key callback URL: registered with service providers when they issue OAuth
+ *    consumer keys.  Application authors will tell us the callback URL to send to the SP when they
+ *    provide us with their consumer key.
+ *
+ *    The SP will check that the callback URL we send them matches whatever was
+ *    preregistered.  It would be nice if they didn't do this, but enough do that we support it.
+ *
+ *    We don't control the consumer key callback URL.  Gadget authors need to make sure that it
+ *    always redirect to the shindig-deployment global callback URL.
+ *
+ * 2) Global callback URL: a single callback URL that can be whitelisted by service providers
+ *    and shared by all gadgets.  This keeps service providers (and gadget authors) from needing
+ *    to be aware of the complexities of which domain a particular gadget actually runs on.
+ *
+ *    The global callback URL always redirects to the gadget-domain callback URL.
+ *
+ * 3) Gadget domain callback URL: URL on the same hostname as the gadget.  This URL will pass
+ *    the oauth_verifier token into the gadget for reuse.  (It has to be on the same hostname
+ *    so that the same-origin policy allows communication.  We could use gadgets.rpc, except that
+ *    because the authorization happens in a popup we've got no good way to do all the gadgets.rpc
+ *    bootstrapping.)
+ *
+ * Here's an example of what you might see happen with these URLs:
+ *
+ * Shindig sends request token request to OAuth SP with callback URL of
+ *     http://gadgetauthor.com/oauthcallback?cs=<blob>
+ *
+ * User approves access.  OAuth SP redirects to
+ *     http://gadgetauthor.com/oauthcallback?cs=<blob>&oauth_verifier=<verifier>
+ *
+ * gadgauthor.com redirects to deployment global callback URL:
+ *     http://oauth.shindigexample.com/oauthcallback?cs=<blob>&oauth_verifier=<verifier>
+ *
+ * The global callback URL redirects to a gadget-specific callback URL:
+ *     http://12345.smodules.com/oauthcallback?oauth_verifier=<verifier>
+ *
+ * The gadget-specific callback will use window.opener to find the opening gadget and hand it
+ * the verified callback URL.
+ */
+public class GadgetOAuthCallbackGenerator implements OAuthCallbackGenerator {
+
+  private final Processor processor;
+  private final LockedDomainService lockedDomainService;
+  private final OAuthUriManager oauthUriManager;
+  private final BlobCrypter stateCrypter;
+
+  @Inject
+  public GadgetOAuthCallbackGenerator(Processor processor, LockedDomainService lockedDomainService,
+      OAuthUriManager oauthUriManager, @Named(OAuthFetcherConfig.OAUTH_STATE_CRYPTER)
+      BlobCrypter stateCrypter) {
+    this.processor = processor;
+    this.lockedDomainService = lockedDomainService;
+    this.oauthUriManager = oauthUriManager;
+    this.stateCrypter = stateCrypter;
+  }
+
+  public String generateCallback(OAuthFetcherConfig fetcherConfig, String baseCallback,
+      HttpRequest request, OAuthResponseParams responseParams) throws OAuthRequestException {
+    Uri activeUrl = checkGadgetCanRender(request.getSecurityToken(),
+        request.getOAuthArguments(), responseParams);
+    String gadgetDomainCallback = getGadgetDomainCallback(request.getSecurityToken(), activeUrl);
+    if (gadgetDomainCallback == null) {
+      return null;
+    }
+    return generateCallbackForProvider(responseParams, baseCallback, gadgetDomainCallback);
+  }
+
+  private Uri checkGadgetCanRender(SecurityToken securityToken, OAuthArguments arguments,
+      OAuthResponseParams responseParams) throws OAuthRequestException {
+    try {
+      GadgetContext context = new OAuthGadgetContext(securityToken, arguments);
+      // This feels really heavy-weight, is there a simpler way to figure out if a gadget requires
+      // a locked-domain?
+      Gadget gadget = processor.process(context);
+
+      Uri activeUrl = Uri.parse(securityToken.getActiveUrl());
+      String hostname = activeUrl.getAuthority();
+      if (!lockedDomainService.isGadgetValidForHost(hostname, gadget, securityToken.getContainer())) {
+        throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+            "Gadget should not be using URL " + activeUrl);
+      }
+      return activeUrl;
+    } catch (ProcessingException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Unable to check if gadget is using locked-domain", e);
+    }
+  }
+
+  private String getGadgetDomainCallback(SecurityToken securityToken, Uri activeUrl) {
+    Uri gadgetCallback = oauthUriManager.makeOAuthCallbackUri(
+        securityToken.getContainer(), activeUrl.getAuthority());
+    if (gadgetCallback == null) {
+      return null;
+    }
+    if (Strings.isNullOrEmpty(gadgetCallback.getScheme())) {
+      gadgetCallback = new UriBuilder(gadgetCallback).setScheme(activeUrl.getScheme()).toUri();
+    }
+    return gadgetCallback.toString();
+  }
+
+  private String generateCallbackForProvider(
+      OAuthResponseParams responseParams, String callbackForProvider, String gadgetDomainCallback)
+      throws OAuthRequestException {
+    OAuthCallbackState state = new OAuthCallbackState(stateCrypter);
+    state.setRealCallbackUrl(gadgetDomainCallback);
+    UriBuilder callback = UriBuilder.parse(callbackForProvider);
+    try {
+      callback.addQueryParameter(OAuthCallbackServlet.CALLBACK_STATE_PARAM,
+          state.getEncryptedState());
+    } catch (BlobCrypterException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Failure generating callback URL", e);
+    }
+    return callback.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java
new file mode 100644
index 0000000..ffc2d8d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.OAuthStore.ConsumerInfo;
+import org.apache.shindig.gadgets.oauth.OAuthStore.TokenInfo;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.OAuthService;
+import org.apache.shindig.gadgets.spec.OAuthSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.apache.shindig.gadgets.spec.BaseOAuthService.Location;
+import org.apache.shindig.gadgets.spec.BaseOAuthService.Method;
+
+import com.google.inject.Inject;
+import com.google.common.base.Joiner;
+
+import net.oauth.OAuthServiceProvider;
+
+/**
+ * Higher-level interface that allows callers to store and retrieve
+ * OAuth-related data directly from {@code GadgetSpec}s, {@code GadgetContext}s,
+ * etc. See {@link OAuthStore} for a more detailed explanation of the OAuth
+ * Data Store.
+ */
+public class GadgetOAuthTokenStore {
+
+  private final OAuthStore store;
+  private final GadgetSpecFactory specFactory;
+
+  /**
+   * Public constructor.
+   *
+   * @param store an {@link OAuthStore} that can store and retrieve OAuth
+   *              tokens, as well as information about service providers.
+   */
+  @Inject
+  public GadgetOAuthTokenStore(OAuthStore store, GadgetSpecFactory specFactory) {
+    this.store = store;
+    this.specFactory = specFactory;
+  }
+
+  /**
+   * Retrieve an AccessorInfo and OAuthAccessor that are ready for signing OAuthMessages.  To do
+   * this, we need to figure out:
+   *
+   * - what consumer key/secret to use for signing.
+   * - if an access token should be used for the request, and if so what it is.   *
+   * - the OAuth request/authorization/access URLs.
+   * - what HTTP method to use for request token and access token requests
+   * - where the OAuth parameters are located.
+   * - Information from the OAuth Fetcher config to determine if owner pages are secure
+   *
+   * Note that most of that work gets skipped for signed fetch, we just look up the consumer key
+   * and secret for that.  Signed fetch always sticks the parameters in the query string.
+   */
+  public AccessorInfo getOAuthAccessor(SecurityToken securityToken,
+      OAuthArguments arguments, OAuthClientState clientState, OAuthResponseParams responseParams,
+      OAuthFetcherConfig fetcherConfig)
+      throws OAuthRequestException {
+
+    AccessorInfoBuilder accessorBuilder = new AccessorInfoBuilder();
+
+    // Pick up any additional information needed about the format of the request, either from
+    // options to makeRequest, or options from the spec, or from sensible defaults.  This is how
+    // we figure out where to put the OAuth parameters and what methods to use for the OAuth
+    // URLs.
+    OAuthServiceProvider provider = null;
+    if (arguments.programmaticConfig()) {
+      provider = loadProgrammaticConfig(arguments, accessorBuilder, responseParams);
+    } else if (arguments.mayUseToken()) {
+      provider = lookupSpecInfo(securityToken, arguments, accessorBuilder, responseParams);
+    } else {
+      // This is plain old signed fetch.
+      accessorBuilder.setParameterLocation(AccessorInfo.OAuthParamLocation.URI_QUERY);
+    }
+
+    // What consumer key/secret should we use?
+    ConsumerInfo consumer;
+    try {
+      consumer = store.getConsumerKeyAndSecret(
+          securityToken, arguments.getServiceName(), provider);
+      accessorBuilder.setConsumer(consumer);
+    } catch (GadgetException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Unable to retrieve consumer key", e);
+    }
+
+    // Should we use the OAuth access token?  We never do this unless the client allows it, and
+    // if owner == viewer or owner pages are secure.
+    if (arguments.mayUseToken() && securityToken.getViewerId() != null) {
+      if ((fetcherConfig != null && fetcherConfig.isViewerAccessTokensEnabled()) ||
+          securityToken.getViewerId().equals(securityToken.getOwnerId())) {
+        lookupToken(securityToken, consumer, arguments, clientState, accessorBuilder, responseParams);
+      }
+    }
+
+    return accessorBuilder.create(responseParams);
+  }
+
+  /**
+   * Lookup information contained in the gadget spec.
+   */
+  private OAuthServiceProvider lookupSpecInfo(SecurityToken securityToken, OAuthArguments arguments,
+      AccessorInfoBuilder accessorBuilder, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    GadgetSpec spec = findSpec(securityToken, arguments, responseParams);
+    OAuthSpec oauthSpec = spec.getModulePrefs().getOAuthSpec();
+    if (oauthSpec == null) {
+      throw new OAuthRequestException(OAuthError.BAD_OAUTH_CONFIGURATION,
+          "Failed to retrieve OAuth URLs, spec for gadget " +
+          securityToken.getAppUrl() + " does not contain OAuth element.");
+    }
+    OAuthService service = oauthSpec.getServices().get(arguments.getServiceName());
+    if (service == null) {
+      throw new OAuthRequestException(OAuthError.BAD_OAUTH_CONFIGURATION,
+          "Failed to retrieve OAuth URLs, spec for gadget does not contain OAuth service " +
+          arguments.getServiceName() + ".  Known services: " +
+          Joiner.on(',').join(oauthSpec.getServices().keySet()) + '.');
+    }
+    // In theory some one could specify different parameter locations for request token and
+    // access token requests, but that's probably not useful.  We just use the request token
+    // rules for everything.
+    accessorBuilder.setParameterLocation(
+        getStoreLocation(service.getRequestUrl().location, responseParams));
+    accessorBuilder.setMethod(getStoreMethod(service.getRequestUrl().method, responseParams));
+    return new OAuthServiceProvider(
+        service.getRequestUrl().url.toJavaUri().toASCIIString(),
+        service.getAuthorizationUrl().toJavaUri().toASCIIString(),
+        service.getAccessUrl().url.toJavaUri().toASCIIString());
+  }
+
+  private OAuthServiceProvider loadProgrammaticConfig(OAuthArguments arguments,
+      AccessorInfoBuilder accessorBuilder, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    try {
+      String paramLocationStr = arguments.getRequestOption(OAuthArguments.PARAM_LOCATION_PARAM, "");
+      Location l = Location.parse(paramLocationStr);
+      accessorBuilder.setParameterLocation(getStoreLocation(l, responseParams));
+
+      String requestMethod = arguments.getRequestOption(OAuthArguments.REQUEST_METHOD_PARAM, "GET");
+      Method m = Method.parse(requestMethod);
+      accessorBuilder.setMethod(getStoreMethod(m, responseParams));
+
+      String requestTokenUrl = arguments.getRequestOption(OAuthArguments.REQUEST_TOKEN_URL_PARAM);
+      verifyUrl(requestTokenUrl, responseParams);
+      String accessTokenUrl = arguments.getRequestOption(OAuthArguments.ACCESS_TOKEN_URL_PARAM);
+      verifyUrl(accessTokenUrl, responseParams);
+
+      String authorizationUrl = arguments.getRequestOption(OAuthArguments.AUTHORIZATION_URL_PARAM);
+      verifyUrl(authorizationUrl, responseParams);
+      return new OAuthServiceProvider(requestTokenUrl, authorizationUrl, accessTokenUrl);
+    } catch (SpecParserException e) {
+      // these exceptions have decent programmer readable messages
+      throw new OAuthRequestException(OAuthError.BAD_OAUTH_CONFIGURATION,
+          e.getMessage());
+    }
+  }
+
+  private void verifyUrl(String url, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    if (url == null) {
+      return;
+    }
+    Uri uri;
+    try {
+      uri = Uri.parse(url);
+    } catch (Throwable t) {
+      throw new OAuthRequestException(OAuthError.INVALID_URL, url);
+    }
+    if (!uri.isAbsolute()) {
+      throw new OAuthRequestException(OAuthError.INVALID_URL, url);
+    }
+  }
+
+  /**
+   * Figure out the OAuth token that should be used with this request.  We check for this in three
+   * places.  In order of priority:
+   *
+   * 1) From information we cached on the client.
+   *    We encrypt the token and cache on the client for performance.
+   *
+   * 2) From information we have in our persistent state.
+   *    We persist the token server-side so we can look it up if necessary.
+   *
+   * 3) From information the gadget developer tells us to use (a preapproved request token.)
+   *    Gadgets can be initialized with preapproved request tokens.  If the user tells the service
+   *    provider they want to add a gadget to a gadget container site, the service provider can
+   *    create a preapproved request token for that site and pass it to the gadget as a user
+   *    preference.
+   */
+  private void lookupToken(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      OAuthArguments arguments, OAuthClientState clientState, AccessorInfoBuilder accessorBuilder,
+      OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    if (clientState.getRequestToken() != null) {
+      // We cached the request token on the client.
+      accessorBuilder.setRequestToken(clientState.getRequestToken());
+      accessorBuilder.setTokenSecret(clientState.getRequestTokenSecret());
+    } else if (clientState.getAccessToken() != null) {
+      // We cached the access token on the client
+      accessorBuilder.setAccessToken(clientState.getAccessToken());
+      accessorBuilder.setTokenSecret(clientState.getAccessTokenSecret());
+      accessorBuilder.setSessionHandle(clientState.getSessionHandle());
+      accessorBuilder.setTokenExpireMillis(clientState.getTokenExpireMillis());
+    } else {
+      // No useful client-side state, check persistent storage
+      TokenInfo tokenInfo;
+      try {
+        tokenInfo = store.getTokenInfo(securityToken, consumerInfo,
+            arguments.getServiceName(), arguments.getTokenName());
+      } catch (GadgetException e) {
+        throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+            "Unable to retrieve access token", e);
+      }
+      if (tokenInfo != null && tokenInfo.getAccessToken() != null) {
+        // We have an access token in persistent storage, use that.
+        accessorBuilder.setAccessToken(tokenInfo.getAccessToken());
+        accessorBuilder.setTokenSecret(tokenInfo.getTokenSecret());
+        accessorBuilder.setSessionHandle(tokenInfo.getSessionHandle());
+        accessorBuilder.setTokenExpireMillis(tokenInfo.getTokenExpireMillis());
+      } else {
+        // We don't have an access token yet, but the client sent us a (hopefully) preapproved
+        // request token.
+        accessorBuilder.setRequestToken(arguments.getRequestToken());
+        accessorBuilder.setTokenSecret(arguments.getRequestTokenSecret());
+      }
+    }
+  }
+
+  private OAuthParamLocation getStoreLocation(Location location,
+      OAuthResponseParams responseParams) throws OAuthRequestException {
+    switch(location) {
+    case HEADER:
+      return OAuthParamLocation.AUTH_HEADER;
+    case URL:
+      return OAuthParamLocation.URI_QUERY;
+    case BODY:
+      return OAuthParamLocation.POST_BODY;
+    }
+    throw new OAuthRequestException(OAuthError.UNKNOWN_PARAMETER_LOCATION);
+  }
+
+  private HttpMethod getStoreMethod(Method method, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    switch(method) {
+    case GET:
+      return HttpMethod.GET;
+    case POST:
+      return HttpMethod.POST;
+    }
+    throw new OAuthRequestException(OAuthError.UNSUPPORTED_HTTP_METHOD, method.toString());
+  }
+
+  private GadgetSpec findSpec(final SecurityToken securityToken, final OAuthArguments arguments,
+      OAuthResponseParams responseParams) throws OAuthRequestException {
+    try {
+      GadgetContext context = new OAuthGadgetContext(securityToken, arguments);
+      return specFactory.getGadgetSpec(context);
+    } catch (IllegalArgumentException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Could not fetch gadget spec, gadget URI invalid.", e);
+    } catch (GadgetException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Could not fetch gadget spec", e);
+    }
+  }
+
+  /**
+   * Store an access token for the given user/gadget/service/token name
+   */
+  public void storeTokenKeyAndSecret(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      OAuthArguments arguments, TokenInfo tokenInfo, OAuthResponseParams responseParams)
+      throws OAuthRequestException {
+    try {
+      store.setTokenInfo(securityToken, consumerInfo, arguments.getServiceName(),
+          arguments.getTokenName(), tokenInfo);
+    } catch (GadgetException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Unable to store access token", e);
+    }
+  }
+
+  /**
+   * Remove an access token for the given user/gadget/service/token name
+   */
+  public void removeToken(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      OAuthArguments arguments, OAuthResponseParams responseParams) throws OAuthRequestException {
+    try {
+      store.removeToken(securityToken, consumerInfo, arguments.getServiceName(),
+          arguments.getTokenName());
+    } catch (GadgetException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Unable to remove access token", e);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthArguments.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthArguments.java
new file mode 100644
index 0000000..08667b1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthArguments.java
@@ -0,0 +1,389 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.base.Objects;
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Arguments to an OAuth fetch sent by the client.
+ */
+public class OAuthArguments {
+  private static final String SERVICE_PARAM = "OAUTH_SERVICE_NAME";
+  private static final String TOKEN_PARAM = "OAUTH_TOKEN_NAME";
+  private static final String REQUEST_TOKEN_PARAM = "OAUTH_REQUEST_TOKEN";
+  private static final String REQUEST_TOKEN_SECRET_PARAM = "OAUTH_REQUEST_TOKEN_SECRET";
+  private static final String USE_TOKEN_PARAM = "OAUTH_USE_TOKEN";
+  private static final String CLIENT_STATE_PARAM = "oauthState";
+  private static final String BYPASS_SPEC_CACHE_PARAM = "bypassSpecCache";
+  private static final String SIGN_OWNER_PARAM = "signOwner";
+  private static final String SIGN_VIEWER_PARAM = "signViewer";
+  private static final String RECEIVED_CALLBACK_PARAM = "OAUTH_RECEIVED_CALLBACK";
+
+  // Experimental support for configuring OAuth without special parameters in the spec XML.
+  public static final String PROGRAMMATIC_CONFIG_PARAM = "OAUTH_PROGRAMMATIC_CONFIG";
+  public static final String REQUEST_METHOD_PARAM = "OAUTH_REQUEST_METHOD";
+  public static final String PARAM_LOCATION_PARAM = "OAUTH_PARAM_LOCATION";
+  public static final String REQUEST_TOKEN_URL_PARAM = "OAUTH_REQUEST_TOKEN_URL";
+  public static final String ACCESS_TOKEN_URL_PARAM = "OAUTH_ACCESS_TOKEN_URL";
+  public static final String AUTHORIZATION_URL_PARAM = "OAUTH_AUTHORIZATION_URL";
+
+  /**
+   * Should the OAuth access token be used?
+   */
+  public static enum UseToken {
+    /** Do not use the OAuth access token */
+    NEVER,
+    /** Use the access token if it exists already, but don't prompt for permission */
+    IF_AVAILABLE,
+    /** Use the access token if it exists, and prompt if it doesn't */
+    ALWAYS,
+  }
+
+  /** Should we attempt to use an access token for the request */
+  private UseToken useToken = UseToken.ALWAYS;
+
+  /** OAuth service nickname.  Signed fetch uses the empty string */
+  private String serviceName = "";
+
+  /** OAuth token nickname.  Signed fetch uses the empty string */
+  private String tokenName = "";
+
+  /** Request token the client wants us to use, may be null */
+  private String requestToken = null;
+
+  /** Token secret that goes with the request token */
+  private String requestTokenSecret = null;
+
+  /** Encrypted state blob stored on the client */
+  private String origClientState = null;
+
+  /** Whether we should bypass the gadget spec cache */
+  private boolean bypassSpecCache = false;
+
+  /** Include information about the owner? */
+  private boolean signOwner = false;
+
+  /** Include information about the viewer? */
+  private boolean signViewer = false;
+
+  /** Arbitrary name/value pairs associated with the request */
+  private final Map<String, String> requestOptions = new TreeMap<String,String>(String.CASE_INSENSITIVE_ORDER);
+
+  /** Whether the request is one for proxied content */
+  private boolean proxiedContentRequest = false;
+
+  /** Callback URL returned from service provider */
+  private String receivedCallbackUrl = null;
+
+  /**
+   * Parse OAuthArguments from parameters to the makeRequest servlet.
+   *
+   * @param auth authentication type for the request
+   * @param request servlet request
+   * @throws GadgetException if any parameters are invalid.
+   */
+  public OAuthArguments(AuthType auth, HttpServletRequest request) throws GadgetException {
+    useToken = parseUseToken(auth, getRequestParam(request, USE_TOKEN_PARAM, ""));
+    serviceName = getRequestParam(request, SERVICE_PARAM, "");
+    tokenName = getRequestParam(request, TOKEN_PARAM, "");
+    requestToken = getRequestParam(request, REQUEST_TOKEN_PARAM, null);
+    requestTokenSecret = getRequestParam(request, REQUEST_TOKEN_SECRET_PARAM, null);
+    origClientState = getRequestParam(request, CLIENT_STATE_PARAM, null);
+    bypassSpecCache = "1".equals(getRequestParam(request, BYPASS_SPEC_CACHE_PARAM, null));
+    signOwner = Boolean.parseBoolean(getRequestParam(request, SIGN_OWNER_PARAM, "true"));
+    signViewer = Boolean.parseBoolean(getRequestParam(request, SIGN_VIEWER_PARAM, "true"));
+    receivedCallbackUrl = getRequestParam(request, RECEIVED_CALLBACK_PARAM, null);
+    Enumeration<String> params = getParameterNames(request);
+    while (params.hasMoreElements()) {
+      String name = params.nextElement();
+      requestOptions.put(name, request.getParameter(name));
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private Enumeration<String> getParameterNames(HttpServletRequest request) {
+    return request.getParameterNames();
+  }
+
+  /**
+   * Parse OAuthArguments from parameters to Preload, proxied content rendering, and OSML tags.
+   */
+  public OAuthArguments(RequestAuthenticationInfo info) throws GadgetException {
+    this(info.getAuthType(), info.getAttributes());
+
+    origClientState = null;  // Client has no state for declarative calls
+    bypassSpecCache = false; // too much trouble to copy nocache=1 from the request context to here.
+
+    signOwner = info.isSignOwner();
+    signViewer = info.isSignViewer();
+  }
+
+  /**
+   * Parse OAuthArguments from a Map of settings
+   */
+  public OAuthArguments(AuthType auth,  Map<String, String> map) throws GadgetException {
+    requestOptions.putAll(map);
+    useToken = parseUseToken(auth, getAuthInfoParam(requestOptions, USE_TOKEN_PARAM, ""));
+    serviceName = getAuthInfoParam(requestOptions, SERVICE_PARAM, "");
+    tokenName = getAuthInfoParam(requestOptions, TOKEN_PARAM, "");
+    requestToken = getAuthInfoParam(requestOptions, REQUEST_TOKEN_PARAM, null);
+    requestTokenSecret = getAuthInfoParam(requestOptions, REQUEST_TOKEN_SECRET_PARAM, null);
+    origClientState = getAuthInfoParam(requestOptions, CLIENT_STATE_PARAM, null);
+    bypassSpecCache = "1".equals(getAuthInfoParam(requestOptions, BYPASS_SPEC_CACHE_PARAM, null));
+    signOwner =  Boolean.parseBoolean(getAuthInfoParam(requestOptions, SIGN_OWNER_PARAM, "true"));
+    signViewer = Boolean.parseBoolean(getAuthInfoParam(requestOptions, SIGN_VIEWER_PARAM, "true"));
+    receivedCallbackUrl = getAuthInfoParam(requestOptions, RECEIVED_CALLBACK_PARAM, null);
+  }
+
+
+  /**
+   * @return the named attribute from the Preload tag attributes, or default if the attribute is
+   * not present.
+   */
+  private static String getAuthInfoParam(Map<String, String> attrs, String name, String def) {
+    String val = attrs.get(name);
+    if (val == null) {
+      val = def;
+    }
+    return val;
+  }
+
+  /**
+   * @return the named parameter from the request, or default if the named parameter is not present.
+   */
+  private static String getRequestParam(HttpServletRequest request, String name, @Nullable String def) {
+    String val = request.getParameter(name);
+    if (val == null) {
+      val = def;
+    }
+    return val;
+  }
+
+
+  /**
+   * Figure out what the client wants us to do with the OAuth access token.
+   */
+  private static UseToken parseUseToken(AuthType auth, String useTokenStr) throws GadgetException {
+    if (useTokenStr.length() == 0) {
+      if (auth == AuthType.SIGNED) {
+        // signed fetch defaults to not using the token
+        return UseToken.NEVER;
+      } else {
+        // OAuth defaults to always using it.
+        return UseToken.ALWAYS;
+      }
+    }
+    useTokenStr = useTokenStr.toLowerCase();
+    if ("always".equals(useTokenStr)) {
+      return UseToken.ALWAYS;
+    }
+    if ("if_available".equals(useTokenStr)) {
+      return UseToken.IF_AVAILABLE;
+    }
+    if ("never".equals(useTokenStr)) {
+      return UseToken.NEVER;
+    }
+    throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+        "Unknown use token value " + useTokenStr, HttpResponse.SC_BAD_REQUEST);
+  }
+
+  /**
+   * Create an OAuthArguments object with all default values.  The details can be filled in later
+   * using the setters.
+   *
+   * Be careful using this in anything except test code.  If you find yourself wanting to use this
+   * method in real code, consider writing a new constructor instead.
+   */
+  public OAuthArguments() {
+  }
+
+
+  /**
+   * Copy constructor.
+   */
+  public OAuthArguments(OAuthArguments orig) {
+    useToken = orig.useToken;
+    serviceName = orig.serviceName;
+    tokenName = orig.tokenName;
+    requestToken = orig.requestToken;
+    requestTokenSecret = orig.requestTokenSecret;
+    origClientState = orig.origClientState;
+    bypassSpecCache = orig.bypassSpecCache;
+    signOwner = orig.signOwner;
+    signViewer = orig.signViewer;
+    requestOptions.putAll(orig.requestOptions);
+    proxiedContentRequest = orig.proxiedContentRequest;
+  }
+
+  public boolean mustUseToken() {
+    return (useToken == UseToken.ALWAYS);
+  }
+
+  public boolean mayUseToken() {
+    return (useToken == UseToken.IF_AVAILABLE || useToken == UseToken.ALWAYS);
+  }
+
+  public UseToken getUseToken() {
+    return useToken;
+  }
+
+  public void setUseToken(UseToken useToken) {
+    this.useToken = useToken;
+  }
+
+  public String getServiceName() {
+    return serviceName;
+  }
+
+  public void setServiceName(String serviceName) {
+    this.serviceName = serviceName;
+  }
+
+  public String getTokenName() {
+    return tokenName;
+  }
+
+  public void setTokenName(String tokenName) {
+    this.tokenName = tokenName;
+  }
+
+  public String getRequestToken() {
+    return requestToken;
+  }
+
+  public void setRequestToken(String requestToken) {
+    this.requestToken = requestToken;
+  }
+
+  public String getRequestTokenSecret() {
+    return requestTokenSecret;
+  }
+
+  public void setRequestTokenSecret(String requestTokenSecret) {
+    this.requestTokenSecret = requestTokenSecret;
+  }
+
+  public String getOrigClientState() {
+    return origClientState;
+  }
+
+  public void setOrigClientState(String origClientState) {
+    this.origClientState = origClientState;
+  }
+
+  public boolean getBypassSpecCache() {
+    return bypassSpecCache;
+  }
+
+  public void setBypassSpecCache(boolean bypassSpecCache) {
+    this.bypassSpecCache = bypassSpecCache;
+  }
+
+  public boolean getSignOwner() {
+    return signOwner;
+  }
+
+  public void setSignOwner(boolean signOwner) {
+    this.signOwner = signOwner;
+  }
+
+  public boolean getSignViewer() {
+    return signViewer;
+  }
+
+  public void setSignViewer(boolean signViewer) {
+    this.signViewer = signViewer;
+  }
+
+  public void setRequestOption(String name, String value) {
+    requestOptions.put(name, value);
+  }
+
+  public void removeRequestOption(String name) {
+    requestOptions.remove(name);
+  }
+
+  public String getRequestOption(String name) {
+    return requestOptions.get(name);
+  }
+
+  public String getRequestOption(String name, String def) {
+    String val = requestOptions.get(name);
+    return (val != null ? val : def);
+  }
+
+  public boolean isProxiedContentRequest() {
+    return proxiedContentRequest;
+  }
+
+  public void setProxiedContentRequest(boolean proxiedContentRequest) {
+    this.proxiedContentRequest = proxiedContentRequest;
+  }
+
+  public boolean programmaticConfig() {
+    return Boolean.parseBoolean(requestOptions.get(PROGRAMMATIC_CONFIG_PARAM));
+  }
+
+  public String getReceivedCallbackUrl() {
+    return receivedCallbackUrl;
+  }
+
+  public void setReceivedCallbackUrl(String receivedCallbackUrl) {
+    this.receivedCallbackUrl = receivedCallbackUrl;
+  }
+
+  @Override
+  public int hashCode() {
+      return Objects.hashCode(bypassSpecCache, origClientState, origClientState,
+          proxiedContentRequest, requestToken, requestTokenSecret, requestTokenSecret,
+          serviceName, serviceName, signOwner,
+          signViewer, tokenName, useToken);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj)
+      return true;
+
+    if (!(obj instanceof OAuthArguments)) {
+      return false;
+    }
+
+    OAuthArguments other = (OAuthArguments) obj;
+    return (bypassSpecCache == other.bypassSpecCache
+        && Objects.equal(origClientState, other.origClientState)
+        && proxiedContentRequest == other.proxiedContentRequest
+        && Objects.equal(requestToken, other.requestToken)
+        && Objects.equal(requestTokenSecret, other.requestTokenSecret)
+        && Objects.equal(tokenName, other.tokenName)
+        && signViewer == other.signViewer
+        && useToken == other.useToken);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackGenerator.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackGenerator.java
new file mode 100644
index 0000000..e772c9c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackGenerator.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.inject.ImplementedBy;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+
+/**
+ * Figures out the OAuth callback URL to send service providers.
+ */
+@ImplementedBy(GadgetOAuthCallbackGenerator.class)
+public interface OAuthCallbackGenerator {
+  String generateCallback(OAuthFetcherConfig fetcherConfig, String baseCallback,
+      HttpRequest request, OAuthResponseParams responseParams) throws OAuthRequestException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackState.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackState.java
new file mode 100644
index 0000000..a380278
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackState.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+
+import java.util.Map;
+
+/**
+ * Handles state passed on the OAuth callback URL.
+ *
+ * TODO: there's probably an abstract superclass that can be reused by OAuthClientState and this
+ * class.
+ */
+public class OAuthCallbackState {
+
+  private final BlobCrypter crypter;
+  private OAuthCallbackStateToken state;
+
+  public OAuthCallbackState(BlobCrypter crypter) {
+    this.crypter = crypter;
+    this.state = new OAuthCallbackStateToken();
+  }
+
+  public OAuthCallbackState(BlobCrypter crypter, String stateBlob) {
+    this.crypter = crypter;
+    Map<String, String> state = null;
+    if (stateBlob != null) {
+      try {
+        state = crypter.unwrap(stateBlob);
+        if (state == null) {
+          state = Maps.newHashMap();
+        }
+        this.state = new OAuthCallbackStateToken(state);
+        this.state.enforceNotExpired();
+      } catch (BlobCrypterException e) {
+        // Too old, or corrupt.  Ignore it.
+        state = null;
+      }
+    }
+    if (state == null) {
+      this.state = new OAuthCallbackStateToken();
+    }
+  }
+
+  public String getEncryptedState() throws BlobCrypterException {
+    return crypter.wrap(state.toMap());
+  }
+
+  public String getRealCallbackUrl() {
+    return state.getRealCallbackUrl();
+  }
+
+  public void setRealCallbackUrl(String realCallbackUrl) {
+    state.setRealCallbackUrl(realCallbackUrl);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackStateToken.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackStateToken.java
new file mode 100644
index 0000000..b954e74
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCallbackStateToken.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+import org.apache.shindig.auth.AbstractSecurityToken;
+
+
+/**
+ * Token used to persist information for the {@link OAuthCallbackState}
+ */
+public class OAuthCallbackStateToken extends AbstractSecurityToken {
+  private static final EnumSet<Keys> MAP_KEYS = EnumSet.of(Keys.EXPIRES);
+  private static final String REAL_CALLBACK_URL_KEY = "u";
+
+  private String realCallbackUrl;
+
+  public OAuthCallbackStateToken () {}
+
+  public OAuthCallbackStateToken (Map<String, String> values) {
+    loadFromMap(values);
+    setRealCallbackUrl(values.get(REAL_CALLBACK_URL_KEY));
+  }
+
+  public String getUpdatedToken() {
+    return null;
+  }
+
+  public String getAuthenticationMode() {
+    return null;
+  }
+
+  public boolean isAnonymous() {
+    return false;
+  }
+
+  protected EnumSet<Keys> getMapKeys() {
+    return MAP_KEYS;
+  }
+
+  public OAuthCallbackStateToken setRealCallbackUrl(String realCallbackUrl) {
+    this.realCallbackUrl = realCallbackUrl;
+    return this;
+  }
+
+  @Override
+  protected int getMaxTokenTTL() {
+    return 600;
+  }
+
+  public String getRealCallbackUrl() {
+    return realCallbackUrl;
+  }
+
+  @Override
+  public Map<String, String> toMap() {
+    Map<String, String> map = super.toMap();
+    map.put(REAL_CALLBACK_URL_KEY, getRealCallbackUrl());
+    return map;
+  }
+}
+
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java
new file mode 100644
index 0000000..83184e9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthClientState.java
@@ -0,0 +1,225 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.auth.AbstractSecurityToken;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.common.util.TimeSource;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+/**
+ * Class to handle OAuth fetcher state stored client side.  The state is
+ * stored as a signed, encrypted, time stamped blob.
+ */
+public class OAuthClientState extends AbstractSecurityToken {
+  private static final EnumSet<Keys> MAP_KEYS = EnumSet.of(Keys.EXPIRES, Keys.OWNER);
+
+  // Our client state is encrypted key/value pairs.  These are the key names.
+  private static final String REQ_TOKEN_KEY = "r";
+  private static final String REQ_TOKEN_SECRET_KEY = "rs";
+  private static final String ACCESS_TOKEN_KEY = "a";
+  private static final String ACCESS_TOKEN_SECRET_KEY = "as";
+  private static final String SESSION_HANDLE_KEY = "sh";
+  private static final String ACCESS_TOKEN_EXPIRATION_KEY = "e";
+
+  /** Name/value pairs */
+  private final Map<String, String> state;
+
+  /** Crypter to use when sending these to the client */
+  private final BlobCrypter crypter;
+
+  /**
+   * Create a new, empty client state blob.
+   *
+   * @param crypter
+   */
+  public OAuthClientState(BlobCrypter crypter) {
+    this.state = Maps.newHashMap();
+    this.crypter = crypter;
+  }
+
+  /**
+   * Initialize client state based on an encrypted blob passed by the
+   * client.
+   *
+   * @param crypter
+   * @param stateBlob
+   */
+  public OAuthClientState(BlobCrypter crypter, String stateBlob) {
+    this.crypter = crypter;
+    Map<String, String> state = null;
+    if (stateBlob != null) {
+      try {
+        state = crypter.unwrap(stateBlob);
+        if (state == null) {
+          state = Maps.newHashMap();
+        }
+        loadFromMap(state);
+        state.remove(Keys.EXPIRES.getKey());
+        state.remove(Keys.OWNER.getKey());
+        enforceNotExpired();
+      } catch (BlobCrypterException e) {
+        // Probably too old, pretend we never saw it at all.
+        state = null;
+      }
+    }
+    if (state == null) {
+      state = Maps.newHashMap();
+      setOwner(null);
+      setExpiresAt(null);
+    }
+    this.state = state;
+  }
+
+  /**
+   * @return true if there is no state to store with the client.
+   */
+  public boolean isEmpty() {
+    return (state.isEmpty() && getOwnerId() == null);
+  }
+
+  /**
+   * @return an encrypted blob of state to store with the client.
+   * @throws BlobCrypterException
+   */
+  public String getEncryptedState() throws BlobCrypterException {
+    setExpires();
+    Map<String, String> map = this.toMap();
+    map.putAll(state);
+    return crypter.wrap(map);
+  }
+
+  /**
+   * OAuth request token
+   */
+  public String getRequestToken() {
+    return state.get(REQ_TOKEN_KEY);
+  }
+
+  public void setRequestToken(String requestToken) {
+    setNullCheck(REQ_TOKEN_KEY, requestToken);
+  }
+
+  /**
+   * OAuth request token secret
+   */
+  public String getRequestTokenSecret() {
+    return state.get(REQ_TOKEN_SECRET_KEY);
+  }
+
+  public void setRequestTokenSecret(String requestTokenSecret) {
+    setNullCheck(REQ_TOKEN_SECRET_KEY, requestTokenSecret);
+  }
+
+  /**
+   * OAuth access token.
+   */
+  public String getAccessToken() {
+    return state.get(ACCESS_TOKEN_KEY);
+  }
+
+  public void setAccessToken(String accessToken) {
+    setNullCheck(ACCESS_TOKEN_KEY, accessToken);
+  }
+
+  /**
+   * OAuth access token secret.
+   */
+  public String getAccessTokenSecret() {
+    return state.get(ACCESS_TOKEN_SECRET_KEY);
+  }
+
+  public void setAccessTokenSecret(String accessTokenSecret) {
+    setNullCheck(ACCESS_TOKEN_SECRET_KEY, accessTokenSecret);
+  }
+
+  /**
+   * Session handle (http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html)
+   */
+  public String getSessionHandle() {
+    return state.get(SESSION_HANDLE_KEY);
+  }
+
+  public void setSessionHandle(String sessionHandle) {
+    setNullCheck(SESSION_HANDLE_KEY, sessionHandle);
+  }
+
+  /**
+   * Expiration of access token
+   * (http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html)
+   */
+  public long getTokenExpireMillis() {
+    String expiration = state.get(ACCESS_TOKEN_EXPIRATION_KEY);
+    if (expiration == null) {
+      return 0;
+    }
+    return Long.parseLong(expiration);
+  }
+
+  public void setTokenExpireMillis(long expirationMillis) {
+    setNullCheck(ACCESS_TOKEN_EXPIRATION_KEY, Long.toString(expirationMillis));
+  }
+
+  /**
+   * Owner of the OAuth token.
+   */
+  public String getOwner() {
+    return getOwnerId();
+  }
+
+  public void setOwner(String owner) {
+    setOwnerId(owner);
+  }
+
+  private void setNullCheck(String key, String value) {
+    if (value == null) {
+      state.remove(key);
+    } else {
+      state.put(key, value);
+    }
+  }
+
+  public String getUpdatedToken() {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getAuthenticationMode() {
+    throw new UnsupportedOperationException();
+  }
+
+  public boolean isAnonymous() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  protected EnumSet<Keys> getMapKeys() {
+    return MAP_KEYS;
+  }
+
+  @VisibleForTesting
+  protected AbstractSecurityToken setTimeSource(TimeSource timeSource) {
+    return super.setTimeSource(timeSource);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java
new file mode 100644
index 0000000..a73dd8c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthCommandLine.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+
+import org.apache.shindig.auth.OAuthConstants;
+import org.apache.shindig.auth.OAuthUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.http.BasicHttpFetcher;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+
+import java.io.FileInputStream;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *  Run a simple OAuth fetcher to execute a variety of OAuth fetches and output
+ *  the result
+ *
+ *  Arguments
+ *  --consumerKey <oauth_consumer_key>
+ *  --consumerSecret <oauth_consumer_secret>
+ *  --requestorId <xoauth_requestor_id>
+ *  --accessToken <oauth_access_token>
+ *  --method <GET | POST>
+ *  --url <url>
+ *  --contentType <contentType>
+ *  --postBody <encoded post body>
+ *  --postFile <file path of post body contents>
+ *  --paramLocation <URI_QUERY | POST_BODY | AUTH_HEADER>
+ *  --bodySigning hash|legacy|none
+ *  --httpProxy=<http proxy to use for fetching>
+ */
+public class OAuthCommandLine {
+
+  public static enum BodySigning {
+    none,
+    hash,
+    legacy
+  }
+
+  public static void main(String[] argv) throws Exception {
+    Map<String, String> params = Maps.newHashMap();
+    for (int i = 0; i < argv.length; i+=2) {
+      params.put(argv[i], argv[i+1]);
+    }
+    final String httpProxy = params.get("--httpProxy");
+    final String consumerKey = params.get("--consumerKey");
+    final String consumerSecret = params.get("--consumerSecret");
+    final String xOauthRequestor = params.get("--requestorId");
+    final String accessToken = params.get("--accessToken");
+    final String tokenSecret = params.get("--tokenSecret");
+    final String method = params.get("--method") == null ? "GET" :params.get("--method");
+    String url = params.get("--url");
+    String contentType = params.get("--contentType");
+    String postBody = params.get("--postBody");
+    String postFile = params.get("--postFile");
+    String paramLocation = params.get("--paramLocation");
+    String bodySigning = params.get("--bodySigning");
+
+    HttpRequest request = new HttpRequest(Uri.parse(url));
+    if (contentType != null) {
+      request.setHeader("Content-Type", contentType);
+    } else {
+      request.setHeader("Content-Type", OAuth.FORM_ENCODED);
+    }
+    if (postBody != null) {
+      request.setPostBody(postBody.getBytes());
+    }
+    if (postFile != null) {
+      request.setPostBody(IOUtils.toByteArray(new FileInputStream(postFile)));
+    }
+
+    OAuthParamLocation paramLocationEnum = OAuthParamLocation.URI_QUERY;
+    if (paramLocation != null) {
+      paramLocationEnum = OAuthParamLocation.valueOf(paramLocation);
+    }
+
+    BodySigning bodySigningEnum = BodySigning.none;
+    if (bodySigning != null) {
+      bodySigningEnum = BodySigning.valueOf(bodySigning);
+    }
+
+    List<OAuth.Parameter> oauthParams = Lists.newArrayList();
+    UriBuilder target = new UriBuilder(Uri.parse(url));
+    String query = target.getQuery();
+    target.setQuery(null);
+    oauthParams.addAll(OAuth.decodeForm(query));
+    if (OAuth.isFormEncoded(contentType) && request.getPostBodyAsString() != null) {
+      oauthParams.addAll(OAuth.decodeForm(request.getPostBodyAsString()));
+    } else if (bodySigningEnum == BodySigning.legacy) {
+      oauthParams.add(new OAuth.Parameter(request.getPostBodyAsString(), ""));
+    } else if (bodySigningEnum == BodySigning.hash) {
+      oauthParams.add(
+            new OAuth.Parameter(OAuthConstants.OAUTH_BODY_HASH,
+                new String(Base64.encodeBase64(
+                    DigestUtils.sha(request.getPostBodyAsString().getBytes())), "UTF-8")));
+    }
+
+    if (consumerKey != null) {
+      oauthParams.add(new OAuth.Parameter(OAuth.OAUTH_CONSUMER_KEY, consumerKey));
+    }
+    if (xOauthRequestor != null) {
+      oauthParams.add(new OAuth.Parameter("xoauth_requestor_id", xOauthRequestor));
+    }
+
+    OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, null);
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    accessor.accessToken = accessToken;
+    accessor.tokenSecret = tokenSecret;
+    OAuthMessage message = accessor.newRequestMessage(method, target.toString(), oauthParams);
+
+    List<Map.Entry<String, String>> entryList = OAuthRequest.selectOAuthParams(message);
+
+    switch (paramLocationEnum) {
+      case AUTH_HEADER:
+        request.addHeader("Authorization", OAuthRequest.getAuthorizationHeader(entryList));
+        break;
+
+      case POST_BODY:
+        if (!OAuth.isFormEncoded(contentType)) {
+          throw new RuntimeException(
+              "OAuth param location can only be post_body if post body if of " +
+                  "type x-www-form-urlencoded");
+        }
+        String oauthData = OAuthUtil.formEncode(message.getParameters());
+        request.setPostBody(CharsetUtil.getUtf8Bytes(oauthData));
+        break;
+
+      case URI_QUERY:
+        request.setUri(Uri.parse(OAuthUtil.addParameters(request.getUri().toString(),
+            entryList)));
+        break;
+    }
+    request.setMethod(method);
+
+    HttpFetcher fetcher = new BasicHttpFetcher(httpProxy);
+    HttpResponse response = fetcher.fetch(request);
+
+    System.out.println("Request ------------------------------");
+    System.out.println(request.toString());
+    System.out.println("Response -----------------------------");
+    System.out.println(response.toString());
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java
new file mode 100644
index 0000000..e48e605
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthError.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+/**
+ * Error strings to be returned to gadgets as "oauthError" data.
+ */
+public enum OAuthError {
+  /**
+   * The request cannot be completed because the gadget's OAuth configuration
+   * is incorrect. Generic message.
+   */
+  BAD_OAUTH_CONFIGURATION("%s"),
+
+  /**
+   * The request cannot be completed because the gadget didn't specify
+   * an endpoint required for redirection-based authorization.
+   */
+  BAD_OAUTH_TOKEN_URL("No %s URL specified"),
+
+  /**
+   * The request cannot be completed due to missing oauth field(s)
+   */
+  MISSING_OAUTH_PARAMETER("No %s returned from service provider"),
+
+  /**
+   * The request did not yield a response from the server
+   */
+  MISSING_SERVER_RESPONSE("No response from server"),
+
+  /**
+   * The requested HTTP method is not supported
+   */
+  UNSUPPORTED_HTTP_METHOD("Unknown method: %s"),
+
+  /**
+   * The request cannot be completed for an unspecified reason.
+   */
+  UNKNOWN_PROBLEM("%s"),
+
+  /**
+   * The user is not authenticated.
+   */
+  UNAUTHENTICATED("Unauthenticated OAuth fetch"),
+
+  /**
+   * The user is not the owner of the page.
+   */
+  NOT_OWNER("Non-Secure Owner Page. Only page owners can grant OAuth approval"),
+
+  /**
+   * The URL is invalid
+   */
+  INVALID_URL("Invalid URL: %s"),
+
+  /**
+   * The request contains an invalid parameter.
+   */
+  INVALID_PARAMETER("Invalid parameter name %s, applications may not override"
+      + " oauth, xoauth, or opensocial parameters"),
+
+  /**
+   * The request contains an invalid trusted parameter.
+   */
+  INVALID_TRUSTED_PARAMETER("Invalid trusted parameter name %s, parameter"
+      + " must start with oauth, xoauth, or opensocial"),
+
+  UNKNOWN_PARAMETER_LOCATION("Unknown parameter location: %s"),
+
+  /**
+   * The request cannot be completed because the request options were invalid.
+   * Generic message.
+   */
+  INVALID_REQUEST("%s"),
+  ;
+
+  private final String formatString;
+
+  OAuthError(String formatString) {
+    this.formatString = formatString;
+  }
+
+  @Override
+  public String toString() {
+    return formatString;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcherConfig.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcherConfig.java
new file mode 100644
index 0000000..02db84d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthFetcherConfig.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.util.TimeSource;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Configuration parameters for an OAuthRequest
+ */
+public class OAuthFetcherConfig {
+
+  public static final String OAUTH_STATE_CRYPTER = "shindig.oauth.state-crypter";
+
+  private final BlobCrypter stateCrypter;
+  private final GadgetOAuthTokenStore tokenStore;
+  private final TimeSource clock;
+  private final OAuthCallbackGenerator oauthCallbackGenerator;
+  private final boolean viewerAccessTokensEnabled;
+
+  @Inject
+  public OAuthFetcherConfig(
+      @Named(OAUTH_STATE_CRYPTER) BlobCrypter stateCrypter,
+      GadgetOAuthTokenStore tokenStore,
+      TimeSource clock,
+      OAuthCallbackGenerator oauthCallbackGenerator,
+      @Named("shindig.signing.viewer-access-tokens-enabled") boolean viewerAccessTokensEnabled) {
+    this.stateCrypter = stateCrypter;
+    this.tokenStore = tokenStore;
+    this.clock = clock;
+    this.oauthCallbackGenerator = oauthCallbackGenerator;
+    this.viewerAccessTokensEnabled = viewerAccessTokensEnabled;
+  }
+
+  /**
+   * @return A BlobCrypter Used to encrypt state stored on the client.
+   */
+  public BlobCrypter getStateCrypter() {
+    return stateCrypter;
+  }
+
+  /**
+   * @return the persistent token storage.
+   */
+  public GadgetOAuthTokenStore getTokenStore() {
+    return tokenStore;
+  }
+
+  /**
+   * @return the Clock
+   */
+  public TimeSource getClock() {
+    return clock;
+  }
+
+  /**
+   * @return callback Url generator
+   */
+  public OAuthCallbackGenerator getOAuthCallbackGenerator() {
+    return oauthCallbackGenerator;
+  }
+
+  /**
+   * @return true if the owner pages do not allow user controlled javascript
+   */
+  public boolean isViewerAccessTokensEnabled() {
+     return viewerAccessTokensEnabled;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthGadgetContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthGadgetContext.java
new file mode 100644
index 0000000..1011902
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthGadgetContext.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+
+/**
+ * GadgetContext for use when handling an OAuth request.
+ */
+public class OAuthGadgetContext extends GadgetContext {
+
+  private final SecurityToken securityToken;
+  private final String container;
+  private final Uri appUrl;
+  private final boolean bypassSpecCache;
+
+  public OAuthGadgetContext(SecurityToken securityToken, OAuthArguments arguments) {
+    this.securityToken = securityToken;
+    this.container = securityToken.getContainer();
+    this.appUrl = Uri.parse(securityToken.getAppUrl());
+    this.bypassSpecCache = arguments.getBypassSpecCache();
+  }
+
+  @Override
+  public String getContainer() {
+    return container;
+  }
+
+  @Override
+  public SecurityToken getToken() {
+    return securityToken;
+  }
+
+  @Override
+  public Uri getUrl() {
+    return appUrl;
+  }
+
+  @Override
+  public boolean getIgnoreCache() {
+    return bypassSpecCache;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java
new file mode 100644
index 0000000..1966268
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthModule.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret.KeyType;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Loads pre-reqs for OAuth.
+ */
+public class OAuthModule extends AbstractModule {
+
+  //class name for logging purpose
+  private static final String classname = OAuthModule.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+
+  private static final String OAUTH_CONFIG = "config/oauth.json";
+  private static final String OAUTH_SIGNING_KEY_FILE = "shindig.signing.key-file";
+  private static final String OAUTH_SIGNING_KEY_NAME = "shindig.signing.key-name";
+  private static final String OAUTH_CALLBACK_URL = "shindig.signing.global-callback-url";
+
+
+  @Override
+  protected void configure() {
+    // Used for encrypting client-side OAuth state.
+    bind(BlobCrypter.class).annotatedWith(Names.named(OAuthFetcherConfig.OAUTH_STATE_CRYPTER))
+        .toProvider(OAuthCrypterProvider.class);
+
+    // Used for persistent storage of OAuth access tokens.
+    bind(OAuthStore.class).toProvider(OAuthStoreProvider.class);
+    bind(OAuthRequest.class).toProvider(OAuthRequestProvider.class);
+  }
+
+  @Singleton
+  public static class OAuthCrypterProvider implements Provider<BlobCrypter> {
+
+    private final BlobCrypter crypter;
+
+    @Inject
+    public OAuthCrypterProvider(@Named("shindig.signing.state-key") String stateCrypterPath)
+        throws IOException {
+      if (StringUtils.isBlank(stateCrypterPath)) {
+        LOG.info("Using random key for OAuth client-side state encryption");
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "OAuthCrypterProvider constructor", MessageKeys.USING_RANDOM_KEY);
+        }
+        crypter = new BasicBlobCrypter(Crypto.getRandomBytes(BasicBlobCrypter.MASTER_KEY_MIN_LEN));
+      } else {
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "OAuthCrypterProvider constructor", MessageKeys.USING_FILE, new Object[] {stateCrypterPath});
+        }
+        crypter = new BasicBlobCrypter(new File(stateCrypterPath));
+      }
+    }
+
+    public BlobCrypter get() {
+      return crypter;
+    }
+  }
+
+  public static class OAuthRequestProvider implements Provider<OAuthRequest> {
+    private final HttpFetcher fetcher;
+    private final OAuthFetcherConfig config;
+
+    @Inject
+    public OAuthRequestProvider(HttpFetcher fetcher, OAuthFetcherConfig config) {
+      this.fetcher = fetcher;
+      this.config = config;
+    }
+
+    public OAuthRequest get() {
+      return new OAuthRequest(config, fetcher);
+    }
+  }
+
+  @Singleton
+  public static class OAuthStoreProvider implements Provider<OAuthStore> {
+
+    private final BasicOAuthStore store;
+
+    @Inject
+    public OAuthStoreProvider(
+        @Named(OAUTH_SIGNING_KEY_FILE) String signingKeyFile,
+        @Named(OAUTH_SIGNING_KEY_NAME) String signingKeyName,
+        @Named(OAUTH_CALLBACK_URL) String defaultCallbackUrl,
+        Authority authority) {
+      store = new BasicOAuthStore();
+      loadDefaultKey(signingKeyFile, signingKeyName);
+      store.setDefaultCallbackUrl(defaultCallbackUrl);
+      store.setAuthority(authority);
+      loadConsumers();
+    }
+
+    private void loadDefaultKey(String signingKeyFile, String signingKeyName) {
+      BasicOAuthStoreConsumerKeyAndSecret key = null;
+      if (!StringUtils.isBlank(signingKeyFile)) {
+        try {
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "loadDefaultKey", MessageKeys.LOAD_KEY_FILE_FROM, new Object[] {signingKeyFile});
+          }
+          String privateKey = IOUtils.toString(ResourceLoader.open(signingKeyFile), "UTF-8");
+          privateKey = BasicOAuthStore.convertFromOpenSsl(privateKey);
+          key = new BasicOAuthStoreConsumerKeyAndSecret(null, privateKey, KeyType.RSA_PRIVATE,
+              signingKeyName, null);
+        } catch (Throwable t) {
+           if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "loadDefaultKey", MessageKeys.COULD_NOT_LOAD_KEY_FILE, new Object[] {signingKeyFile});
+            LOG.logp(Level.WARNING, classname, "loadDefaultKey", "",t);
+          }
+        }
+      }
+      if (key != null) {
+        store.setDefaultKey(key);
+      } else {
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, classname, "loadDefaultKey", MessageKeys.COULD_NOT_LOAD_SIGN_KEY, new Object[] {OAUTH_SIGNING_KEY_FILE,OAUTH_SIGNING_KEY_NAME});
+        }
+      }
+    }
+
+    private void loadConsumers() {
+      try {
+        String oauthConfigString = ResourceLoader.getContent(OAUTH_CONFIG);
+        store.initFromConfigString(oauthConfigString);
+      } catch (Throwable t) {
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, classname, "loadConsumers", MessageKeys.FAILED_TO_INIT, new Object[] {OAUTH_CONFIG});
+          LOG.log(Level.WARNING, "", t);
+        }
+      }
+    }
+
+    public OAuthStore get() {
+      return store;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java
new file mode 100644
index 0000000..d7efc10
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthProtocolException.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.auth.OAuthUtil;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import com.google.common.collect.ImmutableSet;
+
+import net.oauth.OAuthMessage;
+import net.oauth.OAuthProblemException;
+
+import java.util.Set;
+
+/**
+ * Implements the
+ * <a href="http://wiki.oauth.net/ProblemReporting">
+ * OAuth problem reporting extension</a>
+ *
+ * We divide problems into three categories:
+ * - problems that cause us to abort the protocol.  For example, if we don't
+ *   have a consumer key that the service provider accepts, we give up.
+ *
+ * - problems that cause us to ask for the user's permission again.  For
+ *   example, if the service provider reports that an access token has been
+ *   revoked, we throw away the token and start over.
+ *
+ * - problems that require us to refresh our access token per the OAuth
+ *   session extension protocol
+ *
+ * By default we assume most service provider errors fall into the second
+ * category: we should ask for the user's permission again.
+ */
+class OAuthProtocolException extends Exception {
+
+  /**
+   * Problems that should force us to abort the protocol right away,
+   * and next time the user visits ask them for permission again.
+   */
+  private static Set<String> fatalProblems =
+      ImmutableSet.of("version_rejected",
+                      "signature_method_rejected",
+                      "consumer_key_unknown",
+                      "consumer_key_rejected",
+                      "timestamp_refused");
+
+  /**
+   * Problems that should force us to abort the protocol right away,
+   * but we can still try to use the access token again later.
+   */
+  private static Set<String> temporaryProblems =
+      ImmutableSet.of("consumer_key_refused");
+
+  /**
+   * Problems that should have us try to refresh the access token.
+   */
+  private static Set<String> extensionProblems =
+      ImmutableSet.of("access_token_expired");
+
+  private final boolean canRetry;
+
+  private final boolean startFromScratch;
+
+  private final boolean canExtend;
+
+  private final String problemCode;
+
+  public OAuthProtocolException(int status, OAuthMessage reply) {
+    String problem = OAuthUtil.getParameter(reply, OAuthProblemException.OAUTH_PROBLEM);
+    if (problem == null) {
+      throw new IllegalArgumentException("No problem reported for OAuthProtocolException");
+    }
+    this.problemCode = problem;
+    if (fatalProblems.contains(problem)) {
+      startFromScratch = true;
+      canRetry = false;
+      canExtend = false;
+    } else if (temporaryProblems.contains(problem)) {
+      startFromScratch = false;
+      canRetry = false;
+      canExtend = false;
+    } else if (extensionProblems.contains(problem)) {
+      startFromScratch = false;
+      canRetry = true;
+      canExtend = true;
+    } else {
+      // fallback to status to figure out behavior
+      if (status == HttpResponse.SC_UNAUTHORIZED) {
+        startFromScratch = true;
+        canRetry = true;
+      } else {
+        startFromScratch = false;
+        canRetry = false;
+      }
+      canExtend = false;
+    }
+  }
+
+  /**
+   * Handle OAuth protocol errors for SPs that don't support the problem
+   * reporting extension
+   * @param status HTTP status code, assumed to be between 400 and 499 inclusive
+   */
+  public OAuthProtocolException(int status) {
+    if (status == HttpResponse.SC_UNAUTHORIZED) {
+      startFromScratch = true;
+      canRetry = true;
+    } else {
+      startFromScratch = false;
+      canRetry = false;
+    }
+    canExtend = false;
+    problemCode = null;
+  }
+
+  /**
+   * @return true if we've gotten confused to the point where we should give
+   * up and ask the user for approval again.
+   */
+  public boolean startFromScratch() {
+    return startFromScratch;
+  }
+
+  /**
+   * @return true if we think we can make progress by attempting the protocol
+   * flow again (which may require starting from scratch).
+   */
+  public boolean canRetry() {
+    return canRetry;
+  }
+
+  /**
+   * @return true if we think we can make progress by attempting to extend the lifetime of the
+   * access token.
+   */
+  public boolean canExtend() {
+    return canExtend;
+  }
+
+  /**
+   * @return the OAuth problem code (from the problem reporting extension).
+   */
+  public String getProblemCode() {
+    return problemCode;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
new file mode 100644
index 0000000..572561a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequest.java
@@ -0,0 +1,971 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthException;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuthProblemException;
+import net.oauth.OAuth.Parameter;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.OAuthConstants;
+import org.apache.shindig.auth.OAuthUtil;
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.OAuthStore.ConsumerInfo;
+import org.apache.shindig.gadgets.oauth.OAuthStore.TokenInfo;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+
+/**
+ * Implements both signed fetch and full OAuth for gadgets, as well as a combination of the two that
+ * is necessary to build OAuth enabled gadgets for social sites.
+ *
+ * Signed fetch sticks identity information in the query string, signed either with the container's
+ * private key, or else with a secret shared between the container and the gadget.
+ *
+ * Full OAuth redirects the user to the OAuth service provider site to obtain the user's permission
+ * to access their data.  Read the example in the appendix to the OAuth spec for a summary of how
+ * this works (The spec is at http://oauth.net/core/1.0/).
+ *
+ * The combination protocol works by sending identity information in all requests, and allows the
+ * OAuth dance to happen as well when owner == viewer (by default) or for any viewer when the
+ * OAuthFetcherConfig#isViewerAccessTokensEnabled parameter is true. This lets OAuth service providers build up
+ * an identity mapping from ids on social network sites to their own local ids.
+ */
+public class OAuthRequest {
+
+  //class name for logging purpose
+  private static final String classname = OAuthRequest.class.getName();
+
+  // Maximum number of attempts at the protocol before giving up.
+  private static final int MAX_ATTEMPTS = 2;
+
+  // names of additional OAuth parameters we include in outgoing requests
+  // TODO(beaton): can we do away with this bit in favor of the opensocial param?
+  public static final String XOAUTH_APP_URL = "xoauth_app_url";
+
+  protected static final String OPENSOCIAL_OWNERID = "opensocial_owner_id";
+
+  protected static final String OPENSOCIAL_VIEWERID = "opensocial_viewer_id";
+
+  protected static final String OPENSOCIAL_APPID = "opensocial_app_id";
+
+  // TODO(beaton): figure out if this is the name in the 0.8 spec.
+  protected static final String OPENSOCIAL_APPURL = "opensocial_app_url";
+
+  protected static final String OPENSOCIAL_PROXIED_CONTENT = "opensocial_proxied_content";
+
+  // old and new parameters for the public key
+  // TODO remove OLD in a far future release
+  protected static final String XOAUTH_PUBLIC_KEY_OLD = "xoauth_signature_publickey";
+  protected static final String XOAUTH_PUBLIC_KEY_NEW = "xoauth_public_key";
+
+  protected static final Pattern ALLOWED_PARAM_NAME = Pattern.compile("[-:\\w~!@$*()_\\[\\]:,./ ]+");
+
+  private static final long ACCESS_TOKEN_EXPIRE_UNKNOWN = 0;
+  private static final long ACCESS_TOKEN_FORCE_EXPIRE = -1;
+
+
+  /**
+   * Configuration options for the fetcher.
+   */
+  protected final OAuthFetcherConfig fetcherConfig;
+
+  /**
+   * Next fetcher to use in chain.
+   */
+  private final HttpFetcher fetcher;
+
+  /**
+   * Additional trusted parameters to be included in the OAuth request.
+   */
+  private final List<Parameter> trustedParams;
+
+  /**
+   * State information from client
+   */
+  protected OAuthClientState clientState;
+
+  /**
+   * OAuth specific stuff to include in the response.
+   */
+  protected OAuthResponseParams responseParams;
+
+  /**
+   * The accessor we use for signing messages. This also holds metadata about
+   * the service provider, such as their URLs and the keys we use to access
+   * those URLs.
+   */
+  protected AccessorInfo accessorInfo;
+
+  /**
+   * The request the client really wants to make.
+   */
+  protected HttpRequest realRequest;
+
+  /**
+   * Data returned along with OAuth access token, null if this is not an access token request
+   */
+  protected Map<String, String> accessTokenData;
+
+  /**
+   * @param fetcherConfig configuration options for the fetcher
+   * @param fetcher fetcher to use for actually making requests
+   */
+  public OAuthRequest(OAuthFetcherConfig fetcherConfig, HttpFetcher fetcher) {
+    this(fetcherConfig, fetcher, null);
+  }
+
+  /**
+   * @param fetcherConfig configuration options for the fetcher
+   * @param fetcher fetcher to use for actually making requests
+   * @param trustedParams additional parameters to include in all outgoing OAuth requests, useful
+   *     for client data that can't be pulled from the security token but is still trustworthy.
+   */
+  public OAuthRequest(OAuthFetcherConfig fetcherConfig, HttpFetcher fetcher,
+      List<Parameter> trustedParams) {
+    this.fetcherConfig = fetcherConfig;
+    this.fetcher = fetcher;
+    this.trustedParams = trustedParams;
+  }
+
+  /**
+   * OAuth authenticated fetch.
+   */
+  public HttpResponse fetch(HttpRequest request) {
+    realRequest = request;
+    clientState = new OAuthClientState(
+        fetcherConfig.getStateCrypter(),
+        request.getOAuthArguments().getOrigClientState());
+    responseParams = new OAuthResponseParams(request.getSecurityToken(), request,
+        fetcherConfig.getStateCrypter());
+    try {
+      return fetchNoThrow();
+    } catch (RuntimeException e) {
+      // We log here to record the request/response pairs that created the failure.
+      responseParams.logDetailedWarning(classname,"fetch",MessageKeys.OAUTH_FETCH_UNEXPECTED_ERROR, e);
+      throw e;
+    }
+  }
+
+  /**
+   * Fetch data and build a response to return to the client.  We try to always return something
+   * reasonable to the calling app no matter what kind of madness happens along the way.  If an
+   * unchecked exception occurs, well, then the client is out of luck.
+   */
+  private HttpResponse fetchNoThrow() {
+    HttpResponseBuilder response;
+    try {
+      accessorInfo = fetcherConfig.getTokenStore().getOAuthAccessor(
+          realRequest.getSecurityToken(), realRequest.getOAuthArguments(), clientState,
+          responseParams, fetcherConfig);
+      response = fetchWithRetry();
+    } catch (OAuthRequestException e) {
+      // No data for us.
+      if (OAuthError.UNAUTHENTICATED.name().equals(e.getError())) {
+        responseParams.logDetailedInfo(classname,"fetchNoThrow",MessageKeys.UNAUTHENTICATED_OAUTH, e);
+      } else if (OAuthError.BAD_OAUTH_TOKEN_URL.name().equals(e.getError())) {
+        responseParams.logDetailedInfo(classname,"fetchNoThrow",MessageKeys.INVALID_OAUTH, e);
+      } else {
+        responseParams.logDetailedWarning(classname,"fetchNoThrow",MessageKeys.OAUTH_FETCH_FATAL_ERROR, e);
+      }
+      responseParams.setSendTraceToClient(true);
+      response = new HttpResponseBuilder()
+          .setHttpStatusCode(HttpResponse.SC_FORBIDDEN)
+          .setStrictNoCache();
+      responseParams.addToResponse(response, e);
+      return response.create();
+    }
+
+    // OK, got some data back, annotate it as necessary.
+    if (response.getHttpStatusCode() >= 400) {
+      responseParams.logDetailedWarning(classname,"fetchNoThrow",MessageKeys.OAUTH_FETCH_FATAL_ERROR);
+
+      responseParams.setSendTraceToClient(true);
+    } else if (responseParams.getAznUrl() != null && responseParams.sawErrorResponse()) {
+      responseParams.logDetailedWarning(classname,"fetchNoThrow",MessageKeys.OAUTH_FETCH_ERROR_REPROMPT);
+      responseParams.setSendTraceToClient(true);
+    }
+
+    responseParams.addToResponse(response, null);
+    return response.create();
+  }
+
+  /**
+   * Fetch data, retrying in the event that that the service provider returns an error and we think
+   * we can recover by restarting the protocol flow.
+   */
+  private HttpResponseBuilder fetchWithRetry() throws OAuthRequestException {
+    int attempts = 0;
+    boolean retry;
+    HttpResponseBuilder response = null;
+    do {
+      retry = false;
+      ++attempts;
+      try {
+        response = attemptFetch();
+      } catch (OAuthProtocolException pe) {
+        retry = handleProtocolException(pe, attempts);
+        if (!retry) {
+          if (pe.getProblemCode() != null) {
+            throw new OAuthRequestException(pe.getProblemCode(),
+                "Service provider rejected request", pe);
+          } else {
+            throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+                "Service provider rejected request", pe);
+          }
+        }
+      }
+    } while (retry);
+    return response;
+  }
+
+  private boolean handleProtocolException(OAuthProtocolException pe, int attempts)
+      throws OAuthRequestException {
+    if (pe.canExtend()) {
+      accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_FORCE_EXPIRE);
+    } else if (pe.startFromScratch()) {
+      fetcherConfig.getTokenStore().removeToken(realRequest.getSecurityToken(),
+          accessorInfo.getConsumer(), realRequest.getOAuthArguments(), responseParams);
+      accessorInfo.getAccessor().accessToken = null;
+      accessorInfo.getAccessor().requestToken = null;
+      accessorInfo.getAccessor().tokenSecret = null;
+      accessorInfo.setSessionHandle(null);
+      accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_EXPIRE_UNKNOWN);
+    }
+    return (attempts < MAX_ATTEMPTS && pe.canRetry());
+  }
+
+  /**
+   * Does one of the following:
+   * 1) Sends a request token request, and returns an approval URL to the calling app.
+   * 2) Sends an access token request to swap a request token for an access token, and then asks
+   *    for data from the service provider.
+   * 3) Asks for data from the service provider.
+   */
+  private HttpResponseBuilder attemptFetch() throws OAuthRequestException, OAuthProtocolException {
+    if (needApproval()) {
+      // This is section 6.1 of the OAuth spec.
+      checkCanApprove();
+      fetchRequestToken();
+      // This is section 6.2 of the OAuth spec.
+      buildClientApprovalState();
+      buildAznUrl();
+      // break out of the content fetching chain, we need permission from
+      // the user to do this
+      return new HttpResponseBuilder()
+         .setHttpStatusCode(HttpResponse.SC_OK)
+         .setStrictNoCache();
+    } else if (needAccessToken()) {
+      // This is section 6.3 of the OAuth spec
+      checkCanApprove();
+      exchangeRequestToken();
+      saveAccessToken();
+      buildClientAccessState();
+    }
+    return fetchData();
+  }
+
+  /**
+   * Do we need to get the user's approval to access the data?
+   */
+  private boolean needApproval() {
+    return (realRequest.getOAuthArguments().mustUseToken()
+            && accessorInfo.getAccessor().requestToken == null
+            && accessorInfo.getAccessor().accessToken == null);
+  }
+
+  /**
+   * Make sure the user is authorized to approve access tokens.  At the moment
+   * we restrict this to page owner's viewing their own pages.
+   */
+  private void checkCanApprove() throws OAuthRequestException {
+    String pageOwner = realRequest.getSecurityToken().getOwnerId();
+    String pageViewer = realRequest.getSecurityToken().getViewerId();
+    String stateOwner = clientState.getOwner();
+    if (pageOwner == null || pageViewer == null) {
+      throw new OAuthRequestException(OAuthError.UNAUTHENTICATED);
+    }
+    if (!fetcherConfig.isViewerAccessTokensEnabled() && !pageOwner.equals(pageViewer)) {
+      throw new OAuthRequestException(OAuthError.NOT_OWNER);
+    }
+    if (stateOwner != null && !stateOwner.equals(pageViewer)) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Client state belongs to a different person " +
+          "(state owner=" + stateOwner + ", pageViewer=" + pageViewer + ')');
+    }
+  }
+
+  private void fetchRequestToken() throws OAuthRequestException, OAuthProtocolException {
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+    HttpRequest request = createRequestTokenRequest(accessor);
+
+    List<Parameter> requestTokenParams = Lists.newArrayList();
+
+    addCallback(requestTokenParams);
+
+    HttpRequest signed = sanitizeAndSign(request, requestTokenParams, true, this.accessorInfo.getConsumer().isOauthBodyHash());
+
+    OAuthMessage reply = sendOAuthMessage(signed);
+
+    accessor.requestToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
+    accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
+  }
+
+  private HttpRequest createRequestTokenRequest(OAuthAccessor accessor)
+      throws OAuthRequestException {
+    if (accessor.consumer.serviceProvider.requestTokenURL == null) {
+      throw new OAuthRequestException(OAuthError.BAD_OAUTH_TOKEN_URL, "request token");
+    }
+    HttpRequest request = new HttpRequest(
+        Uri.parse(accessor.consumer.serviceProvider.requestTokenURL));
+    request.setMethod(accessorInfo.getHttpMethod().toString());
+    if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
+      request.setHeader("Content-Type", OAuth.FORM_ENCODED);
+    }
+
+    request.setSecurityToken( new AnonymousSecurityToken( "", 0L, this.realRequest.getSecurityToken().getAppUrl()));
+    return request;
+  }
+
+  private void addCallback(List<Parameter> requestTokenParams) throws OAuthRequestException {
+    // This will be either the consumer key callback URL or the global callback URL.
+    String baseCallback = StringUtils.trimToNull(accessorInfo.getConsumer().getCallbackUrl());
+    if (baseCallback != null) {
+      String callbackUrl = fetcherConfig.getOAuthCallbackGenerator().generateCallback(
+          fetcherConfig, baseCallback, realRequest, responseParams);
+      if (callbackUrl != null) {
+        requestTokenParams.add(new Parameter(OAuth.OAUTH_CALLBACK, callbackUrl));
+      }
+    }
+  }
+
+  /**
+   * Strip out any owner or viewer identity information passed by the client.
+   */
+  private List<Parameter> sanitize(List<Parameter> params) throws OAuthRequestException {
+    ArrayList<Parameter> list = Lists.newArrayList();
+    for (Parameter p : params) {
+      String name = p.getKey();
+      if (allowParam(name)) {
+        list.add(p);
+      } else {
+        throw new OAuthRequestException(OAuthError.INVALID_PARAMETER, name);
+      }
+    }
+    return list;
+  }
+
+  protected boolean allowParam(String paramName) {
+    String canonParamName = paramName.toLowerCase();
+    return (!(canonParamName.startsWith("oauth") ||
+        canonParamName.startsWith("xoauth") ||
+        canonParamName.startsWith("opensocial")) &&
+        ALLOWED_PARAM_NAME.matcher(canonParamName).matches());
+  }
+
+  /**
+   * This gives a chance to override parameters by passing trusted parameters.
+   *
+   */
+  private void overrideParameters(List<Parameter> authParams)
+    throws OAuthRequestException {
+    if (trustedParams == null) {
+      return;
+    }
+
+    Map<String, String> paramMap = Maps.newLinkedHashMap();
+    for (Parameter param : authParams) {
+      paramMap.put(param.getKey(), param.getValue());
+    }
+    for (Parameter param : trustedParams) {
+      if (!isContainerInjectedParameter(param.getKey())) {
+        throw new OAuthRequestException(OAuthError.INVALID_TRUSTED_PARAMETER, param.getKey());
+      }
+      paramMap.put(param.getKey(), param.getValue());
+    }
+
+    authParams.clear();
+    for (Entry<String, String> entry : paramMap.entrySet()) {
+      authParams.add(new Parameter(entry.getKey(), entry.getValue()));
+    }
+  }
+
+  /**
+   * Add identity information, such as owner/viewer/gadget.
+   */
+  private void addIdentityParams(List<Parameter> params) {
+    // If no owner or viewer information is required, don't add any identity params.  This lets
+    // us be compatible with strict OAuth service providers that reject extra parameters on
+    // requests.
+    if (!realRequest.getOAuthArguments().getSignOwner() &&
+        !realRequest.getOAuthArguments().getSignViewer()) {
+      return;
+    }
+
+    String owner = realRequest.getSecurityToken().getOwnerId();
+    if (owner != null && realRequest.getOAuthArguments().getSignOwner()) {
+      params.add(new Parameter(OPENSOCIAL_OWNERID, owner));
+    }
+
+    String viewer = realRequest.getSecurityToken().getViewerId();
+    if (viewer != null && realRequest.getOAuthArguments().getSignViewer()) {
+      params.add(new Parameter(OPENSOCIAL_VIEWERID, viewer));
+    }
+
+    String app = realRequest.getSecurityToken().getAppId();
+    if (app != null) {
+      params.add(new Parameter(OPENSOCIAL_APPID, app));
+    }
+
+    String appUrl = realRequest.getSecurityToken().getAppUrl();
+    if (appUrl != null) {
+      params.add(new Parameter(OPENSOCIAL_APPURL, appUrl));
+    }
+
+    if (realRequest.getOAuthArguments().isProxiedContentRequest()) {
+      params.add(new Parameter(OPENSOCIAL_PROXIED_CONTENT, "1"));
+    }
+  }
+
+  /**
+   * Add signature type to the message.
+   */
+  private void addSignatureParams(List<Parameter> params) {
+    if (accessorInfo.getConsumer().getConsumer().consumerKey == null) {
+      params.add(
+          new Parameter(OAuth.OAUTH_CONSUMER_KEY, realRequest.getSecurityToken().getDomain()));
+    }
+    if (accessorInfo.getConsumer().getKeyName() != null) {
+      params.add(new Parameter(XOAUTH_PUBLIC_KEY_OLD, accessorInfo.getConsumer().getKeyName()));
+      params.add(new Parameter(XOAUTH_PUBLIC_KEY_NEW, accessorInfo.getConsumer().getKeyName()));
+    }
+    params.add(new Parameter(OAuth.OAUTH_VERSION, OAuth.VERSION_1_0));
+    params.add(new Parameter(OAuth.OAUTH_TIMESTAMP,
+        Long.toString(fetcherConfig.getClock().currentTimeMillis() / 1000L)));
+    // the oauth.net java code uses a clock to generate nonces, which causes nonce collisions
+    // under heavy load.  A random nonce is more reliable.
+    params.add(new Parameter(OAuth.OAUTH_NONCE, String.valueOf(Math.abs(Crypto.RAND.nextLong()))));
+  }
+
+  static String getAuthorizationHeader(List<Map.Entry<String, String>> oauthParams) {
+    StringBuilder result = new StringBuilder("OAuth ");
+
+    boolean first = true;
+    for (Map.Entry<String, String> parameter : oauthParams) {
+      if (!first) {
+        result.append(", ");
+      } else {
+        first = false;
+      }
+      result.append(OAuth.percentEncode(parameter.getKey()))
+            .append("=\"")
+            .append(OAuth.percentEncode(parameter.getValue()))
+            .append('"');
+    }
+    return result.toString();
+  }
+
+
+  /**
+   * Start with an HttpRequest.
+   * Throw if there are any attacks in the query.
+   * Throw if there are any attacks in the post body.
+   * Build up OAuth parameter list.
+   * Sign it.
+   * Add OAuth parameters to new request.
+   * Send it.
+   */
+  public HttpRequest sanitizeAndSign(HttpRequest base, List<Parameter> params,
+      boolean tokenEndpoint, boolean addBodyHash) throws OAuthRequestException {
+    if (params == null) {
+      params = Lists.newArrayList();
+    }
+    UriBuilder target = new UriBuilder(base.getUri());
+    String query = target.getQuery();
+    target.setQuery(null);
+    params.addAll(sanitize(OAuth.decodeForm(query)));
+
+    switch(OAuthUtil.getSignatureType(tokenEndpoint, base.getHeader("Content-Type"))) {
+      case URL_ONLY:
+        break;
+      case URL_AND_FORM_PARAMS:
+        try {
+          params.addAll(sanitize(OAuth.decodeForm(base.getPostBodyAsString())));
+        } catch (IllegalArgumentException e) {
+          // Occurs if OAuth.decodeForm finds an invalid URL to decode.
+          throw new OAuthRequestException(OAuthError.INVALID_REQUEST,
+              "Could not decode body", e);
+        }
+        break;
+      case URL_AND_BODY_HASH:
+        if (addBodyHash) {
+          try {
+            byte[] body = IOUtils.toByteArray(base.getPostBody());
+            byte[] hash = DigestUtils.sha(body);
+            String b64 = CharsetUtil.newUtf8String(Base64.encodeBase64(hash));
+            params.add(new Parameter(OAuthConstants.OAUTH_BODY_HASH, b64));
+          } catch (IOException e) {
+            throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+                "Error taking body hash", e);
+          }
+        }
+        break;
+    }
+
+    // authParams are parameters prefixed with 'xoauth' 'oauth' or 'opensocial',
+    // trusted parameters have ability to override these parameters.
+    List<Parameter> authParams = Lists.newArrayList();
+
+    if (addBodyHash) {
+      addIdentityParams(authParams);
+    }
+
+    addSignatureParams(authParams);
+
+    overrideParameters(authParams);
+
+    params.addAll(authParams);
+
+    try {
+      OAuthMessage signed = OAuthUtil.newRequestMessage(accessorInfo.getAccessor(),
+          base.getMethod(), target.toString(), params);
+      HttpRequest oauthHttpRequest = createHttpRequest(base, selectOAuthParams(signed));
+      // Following 302s on OAuth responses is unlikely to be productive.
+      oauthHttpRequest.setFollowRedirects(false);
+      return oauthHttpRequest;
+    } catch (OAuthException e) {
+      throw new OAuthRequestException(OAuthError.UNKNOWN_PROBLEM,
+          "Error signing message", e);
+    }
+  }
+
+  private HttpRequest createHttpRequest(HttpRequest base,
+      List<Map.Entry<String, String>> oauthParams) throws OAuthRequestException {
+
+    OAuthParamLocation paramLocation = accessorInfo.getParamLocation();
+
+    // paramLocation could be overriden by a run-time parameter to fetchRequest
+
+    HttpRequest result = new HttpRequest(base);
+
+    // If someone specifies that OAuth parameters go in the body, but then sends a request for
+    // data using GET, we've got a choice.  We can throw some type of error, since a GET request
+    // can't have a body, or we can stick the parameters somewhere else, like, say, the header.
+    // We opt to put them in the header, since that stands some chance of working with some
+    // OAuth service providers.
+    if (paramLocation == OAuthParamLocation.POST_BODY &&
+        !result.getMethod().equals("POST")) {
+      paramLocation = OAuthParamLocation.AUTH_HEADER;
+    }
+
+    switch (paramLocation) {
+      case AUTH_HEADER:
+        result.addHeader("Authorization", getAuthorizationHeader(oauthParams));
+        break;
+
+      case POST_BODY:
+        String contentType = result.getHeader("Content-Type");
+        if (!OAuth.isFormEncoded(contentType)) {
+          throw new OAuthRequestException(OAuthError.INVALID_REQUEST,
+              "OAuth param location can only be post_body if it is of " +
+              "type x-www-form-urlencoded");
+        }
+        String oauthData = OAuthUtil.formEncode(oauthParams);
+        if (result.getPostBodyLength() == 0) {
+          result.setPostBody(CharsetUtil.getUtf8Bytes(oauthData));
+        } else {
+          StringBuilder postBody = new StringBuilder();
+          postBody.append(result.getPostBodyAsString());
+
+          if (!result.getPostBodyAsString().endsWith("&")) {
+            postBody.append('&');
+          }
+
+          postBody.append(oauthData);
+          result.setPostBody(postBody.toString().getBytes());
+        }
+        break;
+
+      case URI_QUERY:
+        result.setUri(Uri.parse(OAuthUtil.addParameters(result.getUri().toString(), oauthParams)));
+        break;
+    }
+
+    return result;
+  }
+
+  /**
+   * Sends OAuth request token and access token messages.
+   */
+  private OAuthMessage sendOAuthMessage(HttpRequest request)
+      throws OAuthRequestException, OAuthProtocolException {
+    HttpResponse response = fetchFromServer(request);
+    checkForProtocolProblem(response);
+    OAuthMessage reply = new OAuthMessage(null, null, null);
+
+    reply.addParameters(OAuth.decodeForm(response.getResponseAsString()));
+    reply = parseAuthHeader(reply, response);
+    if (OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN) == null) {
+      throw new OAuthRequestException(OAuthError.MISSING_OAUTH_PARAMETER,
+          OAuth.OAUTH_TOKEN);
+    }
+    if (OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET) == null) {
+      throw new OAuthRequestException(OAuthError.MISSING_OAUTH_PARAMETER,
+          OAuth.OAUTH_TOKEN_SECRET);
+    }
+    return reply;
+  }
+
+  /**
+   * Parse OAuth WWW-Authenticate header and either add them to an existing
+   * message or create a new message.
+   *
+   * @param msg
+   * @param resp
+   * @return the updated message.
+   */
+  private OAuthMessage parseAuthHeader(OAuthMessage msg, HttpResponse resp) {
+    if (msg == null) {
+      msg = new OAuthMessage(null, null, null);
+    }
+
+    for (String auth : resp.getHeaders("WWW-Authenticate")) {
+      msg.addParameters(OAuthMessage.decodeAuthorization(auth));
+    }
+
+    return msg;
+  }
+
+  /**
+   * Builds the data we'll cache on the client while we wait for approval.
+   */
+  private void buildClientApprovalState() {
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+    responseParams.getNewClientState().setRequestToken(accessor.requestToken);
+    responseParams.getNewClientState().setRequestTokenSecret(accessor.tokenSecret);
+    responseParams.getNewClientState().setOwner(realRequest.getSecurityToken().getOwnerId());
+  }
+
+  /**
+   * Builds the URL the client needs to visit to approve access.
+   */
+  private void buildAznUrl() throws OAuthRequestException {
+    // We add the token, gadget is responsible for the callback URL.
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+    if (accessor.consumer.serviceProvider.userAuthorizationURL == null) {
+      throw new OAuthRequestException(OAuthError.BAD_OAUTH_TOKEN_URL,
+          "authorization");
+    }
+    StringBuilder azn = new StringBuilder(
+        accessor.consumer.serviceProvider.userAuthorizationURL);
+    if (azn.indexOf("?") == -1) {
+      azn.append('?');
+    } else {
+      azn.append('&');
+    }
+    azn.append(OAuth.OAUTH_TOKEN);
+    azn.append('=');
+    azn.append(OAuth.percentEncode(accessor.requestToken));
+    responseParams.setAznUrl(azn.toString());
+  }
+
+  /**
+   * Do we need to exchange a request token for an access token?
+   */
+  private boolean needAccessToken() {
+    if (realRequest.getOAuthArguments().mustUseToken()
+        && accessorInfo.getAccessor().requestToken != null
+        && accessorInfo.getAccessor().accessToken == null) {
+      return true;
+    }
+    return realRequest.getOAuthArguments().mayUseToken() && accessTokenExpired();
+  }
+
+  private boolean accessTokenExpired() {
+    return (accessorInfo.getTokenExpireMillis() != ACCESS_TOKEN_EXPIRE_UNKNOWN
+        && accessorInfo.getTokenExpireMillis() < fetcherConfig.getClock().currentTimeMillis());
+  }
+
+  /**
+   * Implements section 6.3 of the OAuth spec.
+   */
+  private void exchangeRequestToken() throws OAuthRequestException, OAuthProtocolException {
+    if (accessorInfo.getAccessor().accessToken != null) {
+      // session extension per
+      // http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html
+      accessorInfo.getAccessor().requestToken = accessorInfo.getAccessor().accessToken;
+      accessorInfo.getAccessor().accessToken = null;
+    }
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+
+    if (accessor.consumer.serviceProvider.accessTokenURL == null) {
+      throw new OAuthRequestException(OAuthError.BAD_OAUTH_TOKEN_URL, "access token");
+    }
+    Uri accessTokenUri = Uri.parse(accessor.consumer.serviceProvider.accessTokenURL);
+    HttpRequest request = new HttpRequest(accessTokenUri);
+    request.setMethod(accessorInfo.getHttpMethod().toString());
+    request.setSecurityToken( new AnonymousSecurityToken( "", 0L, this.realRequest.getSecurityToken().getAppUrl()));
+    if (accessorInfo.getHttpMethod() == HttpMethod.POST) {
+      request.setHeader("Content-Type", OAuth.FORM_ENCODED);
+    }
+
+    List<Parameter> msgParams = Lists.newArrayList();
+    msgParams.add(new Parameter(OAuth.OAUTH_TOKEN, accessor.requestToken));
+    if (accessorInfo.getSessionHandle() != null) {
+      msgParams.add(new Parameter(OAuthConstants.OAUTH_SESSION_HANDLE,
+          accessorInfo.getSessionHandle()));
+    }
+    String receivedCallback = realRequest.getOAuthArguments().getReceivedCallbackUrl();
+    if (!StringUtils.isBlank(receivedCallback)) {
+      try {
+        Uri parsed = Uri.parse(receivedCallback);
+        String verifier = parsed.getQueryParameter(OAuth.OAUTH_VERIFIER);
+        if (verifier != null) {
+          msgParams.add(new Parameter(OAuth.OAUTH_VERIFIER, verifier));
+        }
+      } catch (IllegalArgumentException e) {
+        throw new OAuthRequestException(OAuthError.INVALID_REQUEST,
+            "Invalid received callback URL: " + receivedCallback, e);
+      }
+    }
+
+    HttpRequest signed = sanitizeAndSign(request, msgParams, true, this.accessorInfo.getConsumer().isOauthBodyHash());
+
+    OAuthMessage reply = sendOAuthMessage(signed);
+
+    accessor.accessToken = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN);
+    accessor.tokenSecret = OAuthUtil.getParameter(reply, OAuth.OAUTH_TOKEN_SECRET);
+    accessorInfo.setSessionHandle(OAuthUtil.getParameter(reply,
+        OAuthConstants.OAUTH_SESSION_HANDLE));
+    accessorInfo.setTokenExpireMillis(ACCESS_TOKEN_EXPIRE_UNKNOWN);
+    if (OAuthUtil.getParameter(reply, OAuthConstants.OAUTH_EXPIRES_IN) != null) {
+      try {
+        int expireSecs = Integer.parseInt(OAuthUtil.getParameter(reply,
+            OAuthConstants.OAUTH_EXPIRES_IN));
+        long expireMillis = fetcherConfig.getClock().currentTimeMillis() + expireSecs * 1000L;
+        accessorInfo.setTokenExpireMillis(expireMillis);
+      } catch (NumberFormatException e) {
+        // Hrm.  Bogus server.  We can safely ignore this, we'll just wait for the server to
+        // tell us when the access token has expired.
+        responseParams.logDetailedWarning(classname,"exchangeRequestToken",MessageKeys.BOGUS_EXPIRED);
+      }
+    }
+
+    // Clients may want to retrieve extra information returned with the access token.  Several
+    // OAuth service providers (e.g. Yahoo, NetFlix) return a user id along with the access
+    // token, and the user id is required to use their APIs.  Clients signal that they need this
+    // extra data by sending a fetch request for the access token URL.
+    //
+    // We don't return oauth* parameters from the response, because we know how to handle those
+    // ourselves and some of them (such as oauth_token_secret) aren't supposed to be sent to the
+    // client.
+    //
+    // Note that this data is not stored server-side.  Clients need to cache these user-ids or
+    // other data themselves, probably in user prefs, if they expect to need the data in the
+    // future.
+    if (accessTokenUri.equals(realRequest.getUri())) {
+      accessTokenData = Maps.newHashMap();
+      for (Entry<String, String> param : OAuthUtil.getParameters(reply)) {
+        if (!param.getKey().startsWith("oauth")) {
+          accessTokenData.put(param.getKey(), param.getValue());
+        }
+      }
+    }
+  }
+
+  /**
+   * Save off our new token and secret to the persistent store.
+   */
+  private void saveAccessToken() throws OAuthRequestException {
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+    TokenInfo tokenInfo = new TokenInfo(accessor.accessToken, accessor.tokenSecret,
+        accessorInfo.getSessionHandle(), accessorInfo.getTokenExpireMillis());
+    fetcherConfig.getTokenStore().storeTokenKeyAndSecret(realRequest.getSecurityToken(),
+        accessorInfo.getConsumer(), realRequest.getOAuthArguments(), tokenInfo, responseParams);
+  }
+
+  /**
+   * Builds the data we'll cache on the client while we make requests.
+   */
+  private void buildClientAccessState() {
+    OAuthAccessor accessor = accessorInfo.getAccessor();
+    responseParams.getNewClientState().setAccessToken(accessor.accessToken);
+    responseParams.getNewClientState().setAccessTokenSecret(accessor.tokenSecret);
+    responseParams.getNewClientState().setOwner(realRequest.getSecurityToken().getOwnerId());
+    responseParams.getNewClientState().setSessionHandle(accessorInfo.getSessionHandle());
+    responseParams.getNewClientState().setTokenExpireMillis(accessorInfo.getTokenExpireMillis());
+  }
+
+  /**
+   * Get honest-to-goodness user data.
+   *
+   * @throws OAuthProtocolException if the service provider returns an OAuth
+   * related error instead of user data.
+   */
+  private HttpResponseBuilder fetchData() throws OAuthRequestException, OAuthProtocolException {
+    HttpResponseBuilder builder;
+    if (accessTokenData != null) {
+      // This is a request for access token data, return it.
+      builder = formatAccessTokenData();
+    } else {
+      HttpRequest signed = sanitizeAndSign(realRequest, null, false, this.accessorInfo.getConsumer().isOauthBodyHash());
+
+      HttpResponse response = fetchFromServer(signed);
+
+      checkForProtocolProblem(response);
+      builder = new HttpResponseBuilder(response);
+    }
+    return builder;
+  }
+
+  private HttpResponse fetchFromServer(HttpRequest request) throws OAuthRequestException {
+    HttpResponse response = null;
+    try {
+      response = fetcher.fetch(request);
+      if (response == null) {
+        throw new OAuthRequestException(OAuthError.MISSING_SERVER_RESPONSE);
+      }
+      return response;
+    } catch (GadgetException e) {
+      throw new OAuthRequestException(OAuthError.MISSING_SERVER_RESPONSE, "", e);
+    } finally {
+      responseParams.addRequestTrace(request, response);
+    }
+  }
+
+  /**
+   * Access token data is returned to the gadget as json key/value pairs:
+   *
+   *    { "user_id": "12345678" }
+   */
+  private HttpResponseBuilder formatAccessTokenData() {
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+    builder.addHeader("Content-Type", "application/json; charset=utf-8");
+    builder.setHttpStatusCode(HttpResponse.SC_OK);
+    // no need to cache this, these requests should be fairly rare, and the results should be
+    // cached in gadget.
+    builder.setStrictNoCache();
+    JSONObject json = new JSONObject(accessTokenData);
+    builder.setResponseString(json.toString());
+    return builder;
+  }
+
+  /**
+   * Look for an OAuth protocol problem.  For cases where no access token is in play
+   * @param response
+   * @throws OAuthProtocolException
+   */
+  private void checkForProtocolProblem(HttpResponse response) throws OAuthProtocolException {
+    if (couldBeFullOAuthError(response)) {
+      // OK, might be OAuth related.
+      OAuthMessage message = parseAuthHeader(null, response);
+      if (OAuthUtil.getParameter(message, OAuthProblemException.OAUTH_PROBLEM) != null) {
+        // SP reported extended error information
+        throw new OAuthProtocolException(response.getHttpStatusCode(), message);
+      }
+      // No extended information, guess based on HTTP response code.
+      if (response.getHttpStatusCode() == HttpResponse.SC_UNAUTHORIZED) {
+        throw new OAuthProtocolException(response.getHttpStatusCode());
+      }
+    }
+  }
+
+  /**
+   * Check if a response might be due to an OAuth protocol error.  We don't want to intercept
+   * errors for signed fetch, we only care about places where we are dealing with OAuth request
+   * and/or access tokens.
+   */
+  private boolean couldBeFullOAuthError(HttpResponse response) {
+    // 400, 401 and 403 are likely to be authentication errors.  Unfortunately there is
+    // significant overlap with other types of server errors as well, so we can't just assume
+    // that the root cause of these errors is a bad token or a bad consumer key.
+    if (response.getHttpStatusCode() != HttpResponse.SC_BAD_REQUEST
+        && response.getHttpStatusCode() != HttpResponse.SC_UNAUTHORIZED
+        && response.getHttpStatusCode() != HttpResponse.SC_FORBIDDEN) {
+      return false;
+    }
+    // If the client forced us to use full OAuth, this might be OAuth related.
+    if (realRequest.getOAuthArguments().mustUseToken()) {
+      return true;
+    }
+    // If we're using an access token, this might be OAuth related.
+    if (accessorInfo.getAccessor().accessToken != null) {
+      return true;
+    }
+    // Not OAuth related.
+    return false;
+  }
+
+  /**
+   * Extracts only those parameters from an OAuthMessage that are OAuth-related.
+   * An OAuthMessage may hold a whole bunch of non-OAuth-related parameters
+   * because they were all needed for signing. But when constructing a request
+   * we need to be able to extract just the OAuth-related parameters because
+   * they, and only they, may have to be put into an Authorization: header or
+   * some such thing.
+   *
+   * @param message the OAuthMessage object, which holds non-OAuth parameters
+   * such as foo=bar (which may have been in the original URI query part, or
+   * perhaps in the POST body), as well as OAuth-related parameters (such as
+   * oauth_timestamp or oauth_signature).
+   *
+   * @return a list that contains only the oauth_related parameters.
+   */
+  static List<Map.Entry<String, String>> selectOAuthParams(OAuthMessage message) {
+    List<Map.Entry<String, String>> result = Lists.newArrayList();
+    for (Map.Entry<String, String> param : OAuthUtil.getParameters(message)) {
+      if (isContainerInjectedParameter(param.getKey())) {
+        result.add(param);
+      }
+    }
+    return result;
+  }
+
+  protected static boolean isContainerInjectedParameter(String key) {
+    key = key.toLowerCase();
+    return key.startsWith("oauth") || key.startsWith("xoauth") || key.startsWith("opensocial");
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequestException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequestException.java
new file mode 100644
index 0000000..d90ddac
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthRequestException.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Thrown by OAuth request routines.
+ * @since 2.0.0
+ */
+public class OAuthRequestException extends Exception {
+
+  /**
+   * Error code for the client.
+   */
+  private String error;
+
+  /**
+   * Error text for the client.
+   */
+  private String errorText;
+
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   * @param error
+   */
+  public OAuthRequestException (OAuthError error) {
+    this(error.name(), error.toString());
+  }
+
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   * @param error
+   * @param errorText
+   */
+  public OAuthRequestException (OAuthError error, String errorText) {
+    this(error.name(), String.format(error.toString(), errorText));
+  }
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   * @param error
+   * @param errorText
+   * @param cause
+   */
+  public OAuthRequestException(OAuthError error, String errorText, Throwable cause) {
+    this(error.name(), String.format(error.toString(), errorText), cause);
+  }
+
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   * @param error
+   * @param errorText
+   */
+  public OAuthRequestException(String error, String errorText) {
+    super('[' + error + ',' + errorText + ']');
+    this.error = Preconditions.checkNotNull(error);
+    this.errorText = Preconditions.checkNotNull(errorText);
+  }
+
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   * @param error
+   * @param errorText
+   * @param cause
+   */
+  public OAuthRequestException(String error, String errorText, Throwable cause) {
+    super('[' + error + ',' + errorText + ']', cause);
+    this.error = Preconditions.checkNotNull(error);
+    this.errorText = Preconditions.checkNotNull(errorText);
+  }
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   * @param message
+   */
+  public OAuthRequestException(String message) {
+    super(message);
+  }
+
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   * @param message
+   * @param cause
+   */
+  public OAuthRequestException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  /**
+   * Get the error code
+   * @return
+   */
+  public String getError() {
+    return error;
+  }
+
+  /**
+   * Get a meaningful description of the exception
+   * @return
+   */
+  public String getErrorText() {
+    return errorText;
+  }
+
+  @Override
+  public String getMessage() {
+    return errorText;
+  }
+
+  @Override
+  public String toString() {
+    return '[' + error + ',' + errorText + ']';
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java
new file mode 100644
index 0000000..83b5944
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthResponseParams.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Container for OAuth specific data to include in the response to the client.
+ */
+public class OAuthResponseParams {
+  private static final Logger LOG = Logger.getLogger(OAuthResponseParams.class.getName(),MessageKeys.MESSAGES);
+
+  // Finds the values of sensitive response params: oauth_token_secret and oauth_session_handle
+  private static final Pattern REMOVE_SECRETS =
+      Pattern.compile("(?<=(oauth_token_secret|oauth_session_handle)=)[^=& \t\r\n]*");
+
+  // names for the JSON values we return to the client
+  public static final String CLIENT_STATE = "oauthState";
+  public static final String APPROVAL_URL = "oauthApprovalUrl";
+  public static final String ERROR_CODE = "oauthError";
+  public static final String ERROR_TEXT = "oauthErrorText";
+
+  /**
+   * Transient state we want to cache client side.
+   */
+  private final OAuthClientState newClientState;
+
+  /**
+   * Security token used to authenticate request.
+   */
+  private final SecurityToken securityToken;
+
+  /**
+   * Original request from client.
+   */
+  private final HttpRequest originalRequest;
+
+  /**
+   * Request/response pairs we sent onward.
+   */
+  private final List<Pair<HttpRequest, HttpResponse>> requestTrace = Lists.newArrayList();
+
+  /**
+   * Authorization URL for the client.
+   */
+  private String aznUrl;
+
+  /**
+   * Whether we should include the request trace in the response to the application.
+   *
+   * It might be nice to make this configurable based on options passed to makeRequest.  For now
+   * we use some heuristics to figure it out.
+   */
+  private boolean sendTraceToClient;
+
+  /**
+   * Create response parameters.
+   */
+  public OAuthResponseParams(SecurityToken securityToken, HttpRequest originalRequest,
+      BlobCrypter stateCrypter) {
+    this.securityToken = securityToken;
+    this.originalRequest = originalRequest;
+    newClientState = new OAuthClientState(stateCrypter);
+  }
+
+  /**
+   * Log a warning message that includes the details of the request.
+   */
+  public void logDetailedWarning(String classname, String method, String msgKey) {
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.log(Level.FINE,getDetails(null));
+    } else if (LOG.isLoggable(Level.WARNING)) {
+    	LOG.logp(Level.WARNING, classname, method, msgKey, new Object[] {getDetails(null)});
+    }
+  }
+
+  /**
+   * Log a warning message that includes the details of the request and the thrown exception.
+   */
+  public void logDetailedWarning(String classname, String method, String msgKey, Throwable e) {
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.log(Level.FINE, getDetails(e), e);
+    } else if (LOG.isLoggable(Level.WARNING)) {
+       LOG.logp(Level.WARNING, classname, method, msgKey, new Object[] {e.getMessage()});
+    }
+  }
+
+  public void logDetailedInfo(String classname, String method, String msgKey, Throwable e) {
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.log(Level.FINE, getDetails(e), e);
+    } else if (LOG.isLoggable(Level.INFO)) {
+    	LOG.logp(Level.INFO, classname, method, msgKey, new Object[] {e.getMessage()});
+    }
+  }
+
+  /**
+   * Log a warning message that includes the details of the request.
+   */
+  public void logDetailedWarning(String note) {
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.log(Level.FINE, note + '\n' + getDetails(null));
+    } else if (LOG.isLoggable(Level.WARNING)) {
+      LOG.log(Level.WARNING, note);
+    }
+  }
+
+  /**
+   * Log a warning message that includes the details of the request and the thrown exception.
+   */
+  public void logDetailedWarning(String note, Throwable e) {
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.log(Level.FINE, note + '\n' + getDetails(e), e);
+    } else if (LOG.isLoggable(Level.WARNING)) {
+      LOG.log(Level.WARNING, note + ": " + e.getMessage());
+    }
+  }
+
+  public void logDetailedInfo(String note, Throwable e) {
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.log(Level.FINE, note + '\n' + getDetails(e), e);
+    } else if (LOG.isLoggable(Level.INFO)) {
+      LOG.log(Level.INFO, note + ": " + e.getMessage());
+    }
+  }
+
+  /**
+   * Add a request/response pair to our trace of actions associated with this request.
+   */
+  public void addRequestTrace(HttpRequest request, HttpResponse response) {
+    this.requestTrace.add(Pair.of(request, response));
+  }
+
+  /**
+   * @return true if the target server returned an error at some point during the request
+   */
+  public boolean sawErrorResponse() {
+    for (Pair<HttpRequest, HttpResponse> event : requestTrace) {
+      if (event.two == null || event.two.isError()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private String getDetails(Throwable e) {
+    String error = null;
+
+    if (null != e) {
+      if(e instanceof OAuthRequestException) {
+        OAuthRequestException reqException = ((OAuthRequestException) e);
+        error = reqException.getError() + ", " + reqException.getErrorText();
+      }
+      else {
+        error = e.getMessage();
+      }
+    }
+
+    return "OAuth error [" + error + "] for application "
+        + securityToken.getAppUrl() + ".  Request trace:" + getRequestTrace();
+  }
+
+  private String getRequestTrace() {
+    StringBuilder trace = new StringBuilder();
+    trace.append("\n==== Original request:\n");
+    trace.append(originalRequest);
+    trace.append("\n====");
+    int i = 1;
+    for (Pair<HttpRequest, HttpResponse> event : requestTrace) {
+      trace.append("\n==== Sent request ").append(i).append(":\n");
+      if (event.one != null) {
+        trace.append(filterSecrets(event.one.toString()));
+      }
+      trace.append("\n==== Received response ").append(i).append(":\n");
+      if (event.two != null) {
+        trace.append(filterSecrets(event.two.toString()));
+      }
+      trace.append("\n====");
+      ++i;
+    }
+    return trace.toString();
+  }
+
+  /**
+   * Removes security sensitive parameters from requests and responses.
+   */
+  static String filterSecrets(String in) {
+    Matcher m = REMOVE_SECRETS.matcher(in);
+    return m.replaceAll("REMOVED");
+  }
+
+  /**
+   * Update a response with additional data to be returned to the application.
+   */
+  public void addToResponse(HttpResponseBuilder response, OAuthRequestException e) {
+    if (!newClientState.isEmpty()) {
+      try {
+        response.setMetadata(CLIENT_STATE, newClientState.getEncryptedState());
+      } catch (BlobCrypterException cryptException) {
+        // Configuration error somewhere, this should never happen.
+        throw new RuntimeException(cryptException);
+      }
+    }
+    if (aznUrl != null) {
+      response.setMetadata(APPROVAL_URL, aznUrl);
+    }
+
+    if (e != null || sendTraceToClient) {
+      StringBuilder verboseError = new StringBuilder();
+
+      if (e != null) {
+        response.setMetadata(ERROR_CODE, e.getError());
+        verboseError.append(e.getErrorText());
+      }
+      if (sendTraceToClient) {
+        verboseError.append('\n');
+        verboseError.append(getRequestTrace());
+      }
+
+      response.setMetadata(ERROR_TEXT, verboseError.toString());
+    }
+  }
+
+  /**
+   * Get the state we will return to the client.
+   */
+  public OAuthClientState getNewClientState() {
+    return newClientState;
+  }
+
+  public String getAznUrl() {
+    return aznUrl;
+  }
+
+  /**
+   * Set the authorization URL we will return to the client.
+   */
+  public void setAznUrl(String aznUrl) {
+    this.aznUrl = aznUrl;
+  }
+
+  public boolean sendTraceToClient() {
+    return sendTraceToClient;
+  }
+
+  public void setSendTraceToClient(boolean sendTraceToClient) {
+    this.sendTraceToClient = sendTraceToClient;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthStore.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthStore.java
new file mode 100644
index 0000000..5887002
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth/OAuthStore.java
@@ -0,0 +1,174 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.gadgets.GadgetException;
+
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthServiceProvider;
+
+/**
+ * Interface to an OAuth Data Store. A shindig gadget server can act as an
+ * OAuth consumer, using OAuth tokens to talk to OAuth service providers on
+ * behalf of the gadgets it is proxying requests for. An OAuth consumer needs
+ * to permanently store gadgets it has collected, and retrieve the
+ * appropriate tokens when proxying a request for a gadget.
+ *
+ * An OAuth Data Store stores three things:
+ *  (1) information about OAuth service providers, including the three
+ *      URLs that define OAuth providers (defined in OAuthStore.ProviderInfo)
+ *  (2) information about consumer keys and secrets that gadgets might have
+ *      negotiated with OAuth service providers, or that containers might have
+ *      negotiated on behalf of the gadgets. This information is bound to
+ *      the service provider it pertains to and can only be stored if the
+ *      corresponding service provider information is already stored in the
+ *      OAuth store (defined in OAuthStore.ConsumerKeyAndSecret).
+ *  (3) OAuth access tokens and their corresponding token secrets. (defined
+ *      in OAuthStore.TokenInfo).
+ *
+ *  Note that we do not store request tokens in the OAuth store.
+ */
+public interface OAuthStore {
+
+  /**
+   * Information about an OAuth consumer.
+   */
+  public static class ConsumerInfo {
+    private final OAuthConsumer consumer;
+    private final String keyName;
+    private final String callbackUrl;
+    private final boolean oauthBodyHash;
+
+    /**
+     * @param consumer the OAuth consumer
+     * @param keyName the name of the key to use for this consumer (passed on query parameters to
+     * help with key rotation.)
+     * @param callbackUrl callback URL associated with this consumer, likely to point to the
+     * shindig server.
+     */
+    public ConsumerInfo(OAuthConsumer consumer, String keyName, String callbackUrl) {
+      this(consumer, keyName, callbackUrl, true);
+    }
+
+    /**
+     * @param consumer the OAuth consumer
+     * @param keyName the name of the key to use for this consumer (passed on query parameters to
+     * help with key rotation.)
+     * @param callbackUrl callback URL associated with this consumer, likely to point to the
+     * shindig server.
+     */
+    public ConsumerInfo(OAuthConsumer consumer, String keyName, String callbackUrl, boolean oauthBodyHash) {
+      this.consumer = consumer;
+      this.keyName = keyName;
+      this.callbackUrl = callbackUrl;
+      this.oauthBodyHash = oauthBodyHash;
+    }
+
+    public OAuthConsumer getConsumer() {
+      return consumer;
+    }
+
+    public String getKeyName() {
+      return keyName;
+    }
+
+    public String getCallbackUrl() {
+      return callbackUrl;
+    }
+
+    public boolean isOauthBodyHash() {
+      return this.oauthBodyHash;
+    }
+  }
+
+  /**
+   * Information about an access token.
+   */
+  public static class TokenInfo {
+    private final String accessToken;
+    private final String tokenSecret;
+    private final String sessionHandle;
+    private final long tokenExpireMillis;
+
+    /**
+     * @param accessToken the token
+     * @param tokenSecret the secret for the token
+     * @param sessionHandle the session handle
+     *     (http://oauth.googlecode.com/svn/spec/ext/session/1.0/drafts/1/spec.html)
+     * @param tokenExpireMillis time (milliseconds since epoch) when the token expires
+     */
+    public TokenInfo(String accessToken, String tokenSecret, String sessionHandle,
+        long tokenExpireMillis) {
+      this.accessToken = accessToken;
+      this.tokenSecret = tokenSecret;
+      this.sessionHandle = sessionHandle;
+      this.tokenExpireMillis = tokenExpireMillis;
+    }
+    public String getAccessToken() {
+      return accessToken;
+    }
+    public String getTokenSecret() {
+      return tokenSecret;
+    }
+    public String getSessionHandle() {
+      return sessionHandle;
+    }
+    public long getTokenExpireMillis() {
+      return tokenExpireMillis;
+    }
+  }
+
+  /**
+   * Retrieve OAuth consumer to use for requests.  The returned consumer is ready to use for signed
+   * fetch requests.
+   *
+   * @param securityToken token for user/gadget making request.
+   * @param serviceName gadget's nickname for the service being accessed.
+   * @param provider OAuth service provider info to be inserted into the returned consumer.
+   *
+   * @throws GadgetException if no OAuth consumer can be found (e.g. no consumer key can be used.)
+   */
+  ConsumerInfo getConsumerKeyAndSecret(SecurityToken securityToken, String serviceName,
+      OAuthServiceProvider provider) throws GadgetException;
+
+  /**
+   * Retrieve OAuth access token to use for the request.
+   * @param securityToken token for user/gadget making request.
+   * @param consumerInfo OAuth consumer that will be used for the request.
+   * @param serviceName gadget's nickname for the service being accessed.
+   * @param tokenName gadget's nickname for the token to use.
+   * @return the token and secret, or null if none exist
+   * @throws GadgetException if an error occurs during lookup
+   */
+  TokenInfo getTokenInfo(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      String serviceName, String tokenName) throws GadgetException;
+
+  /**
+   * Set the access token for the given user/gadget/service/token
+   */
+  void setTokenInfo(SecurityToken securityToken, ConsumerInfo consumerInfo, String serviceName,
+      String tokenName, TokenInfo tokenInfo) throws GadgetException;
+
+  /**
+   * Remove the access token for the given user/gadget/service/token
+   */
+  void removeToken(SecurityToken securityToken, ConsumerInfo consumerInfo,
+      String serviceName, String tokenName) throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Accessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Accessor.java
new file mode 100644
index 0000000..e744ee2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Accessor.java
@@ -0,0 +1,353 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.servlet.Authority;
+
+import java.util.Map;
+
+/**
+ *
+ * see {@link OAuth2Accessor}
+ */
+public class BasicOAuth2Accessor implements OAuth2Accessor {
+  private static final long serialVersionUID = 2050065428260384933L;
+  private OAuth2Token accessToken;
+  private final boolean allowModuleOverrides;
+  private boolean authorizationHeader;
+  private String authorizationUrl;
+  private String clientAuthenticationType;
+  private String clientId;
+  private byte[] clientSecret;
+  private OAuth2Error error;
+  private String errorContextMessage;
+  private Throwable errorException;
+  private boolean errorResponse;
+  private String errorUri;
+  private final String gadgetUri;
+  private final String globalRedirectUri;
+  private final transient Authority authority;
+  private final transient String contextRoot;
+  private String grantType;
+  private boolean redirecting;
+  private String redirectUri;
+  private OAuth2Token refreshToken;
+  private final String scope;
+  private final String serviceName;
+  private transient OAuth2CallbackState state;
+  private String tokenUrl;
+  private Type type;
+  private boolean urlParameter;
+  private final String user;
+  private Map<String, String> additionalRequestParams;
+  private String[] allowedDomains;
+
+  public BasicOAuth2Accessor() {
+    this(null, null, null, null, false, null, null, null, null);
+  }
+
+  BasicOAuth2Accessor(final Throwable exception, final OAuth2Error error,
+          final String contextMessage, final String errorUri) {
+    this.serviceName = null;
+    this.scope = null;
+    this.state = null;
+    this.tokenUrl = null;
+    this.type = null;
+    this.user = null;
+    this.gadgetUri = null;
+    this.globalRedirectUri = null;
+    this.authority = null;
+    this.contextRoot = null;
+    this.allowModuleOverrides = false;
+    this.additionalRequestParams = Maps.newHashMap();
+    this.setErrorResponse(exception, error, contextMessage, errorUri);
+  }
+
+  public BasicOAuth2Accessor(final OAuth2Accessor accessor) {
+    this.accessToken = accessor.getAccessToken();
+    this.authorizationUrl = accessor.getAuthorizationUrl();
+    this.clientAuthenticationType = accessor.getClientAuthenticationType();
+    this.authorizationHeader = accessor.isAuthorizationHeader();
+    this.urlParameter = accessor.isUrlParameter();
+    this.clientId = accessor.getClientId();
+    this.clientSecret = accessor.getClientSecret();
+    this.gadgetUri = accessor.getGadgetUri();
+    this.grantType = accessor.getGrantType();
+    this.redirectUri = accessor.getRedirectUri();
+    this.refreshToken = accessor.getRefreshToken();
+    this.serviceName = accessor.getServiceName();
+    this.scope = accessor.getScope();
+    this.state = accessor.getState();
+    this.tokenUrl = accessor.getTokenUrl();
+    this.type = accessor.getType();
+    this.user = accessor.getUser();
+    this.allowModuleOverrides = false;
+    this.globalRedirectUri = null;
+    this.authority = null;
+    this.contextRoot = null;
+    this.errorResponse = accessor.isErrorResponse();
+    this.redirecting = accessor.isRedirecting();
+    this.error = accessor.getError();
+    this.errorContextMessage = accessor.getErrorContextMessage();
+    this.errorException = accessor.getErrorException();
+    this.errorUri = accessor.getErrorUri();
+    this.additionalRequestParams = Maps.newHashMap();
+    this.allowedDomains = accessor.getAllowedDomains();
+  }
+
+  public BasicOAuth2Accessor(final String gadgetUri, final String serviceName, final String user,
+          final String scope, final boolean allowModuleOverrides, final OAuth2Store store,
+          final String globalRedirectUri, final Authority authority, final String contextRoot) {
+    this.gadgetUri = gadgetUri;
+    this.serviceName = serviceName;
+    this.user = user;
+    this.scope = scope;
+    this.allowModuleOverrides = allowModuleOverrides;
+    this.globalRedirectUri = globalRedirectUri;
+    if (store != null) {
+      this.state = new OAuth2CallbackState(store.getStateCrypter());
+    } else {
+      this.state = new OAuth2CallbackState();
+    }
+    this.state.setGadgetUri(gadgetUri);
+    this.state.setServiceName(serviceName);
+    this.state.setUser(user);
+    this.state.setScope(scope);
+    this.authority = authority;
+    this.contextRoot = contextRoot;
+    this.errorResponse = false;
+    this.redirecting = false;
+    this.additionalRequestParams = Maps.newHashMap();
+  }
+
+  public OAuth2Token getAccessToken() {
+    return this.accessToken;
+  }
+
+  public String getAuthorizationUrl() {
+    return this.authorizationUrl;
+  }
+
+  public String getClientAuthenticationType() {
+    return this.clientAuthenticationType;
+  }
+
+  public String getClientId() {
+    return this.clientId;
+  }
+
+  public byte[] getClientSecret() {
+    return this.clientSecret;
+  }
+
+  public OAuth2Error getError() {
+    return this.error;
+  }
+
+  public String getErrorContextMessage() {
+    return this.errorContextMessage;
+  }
+
+  public Throwable getErrorException() {
+    return this.errorException;
+  }
+
+  public String getErrorUri() {
+    return this.errorUri;
+  }
+
+  public String getGadgetUri() {
+    return this.gadgetUri;
+  }
+
+  public String getGrantType() {
+    return this.grantType;
+  }
+
+  public String getRedirectUri() {
+    if (this.redirectUri == null || this.redirectUri.length() == 0) {
+      String redirectUri2 = this.globalRedirectUri;
+      if (this.authority != null) {
+        redirectUri2 = redirectUri2.replace("%authority%", this.authority.getAuthority());
+        redirectUri2 = redirectUri2.replace("%contextRoot%", this.contextRoot);
+        redirectUri2 = redirectUri2.replace("%origin%", this.authority.getOrigin());
+      }
+
+      this.redirectUri = redirectUri2;
+    }
+
+    return this.redirectUri;
+  }
+
+  public OAuth2Token getRefreshToken() {
+    return this.refreshToken;
+  }
+
+  public Map<String, String> getAdditionalRequestParams() {
+    return this.additionalRequestParams;
+  }
+
+  public String getScope() {
+    return this.scope;
+  }
+
+  public String getServiceName() {
+    return this.serviceName;
+  }
+
+  public OAuth2CallbackState getState() {
+    if (this.state == null) {
+      return new OAuth2CallbackState(null);
+    }
+    return this.state;
+  }
+
+  public String getTokenUrl() {
+    return this.tokenUrl;
+  }
+
+  public Type getType() {
+    return this.type;
+  }
+
+  public String getUser() {
+    return this.user;
+  }
+
+  public void invalidate() {
+    this.accessToken = null;
+    this.authorizationUrl = null;
+    this.clientAuthenticationType = null;
+    this.clientId = null;
+    this.clientSecret = null;
+    this.grantType = null;
+    this.redirectUri = null;
+    this.refreshToken = null;
+    this.tokenUrl = null;
+    this.type = null;
+    this.errorResponse = false;
+    this.redirecting = false;
+    this.errorException = null;
+  }
+
+  public boolean isAllowModuleOverrides() {
+    return this.allowModuleOverrides;
+  }
+
+  public boolean isAuthorizationHeader() {
+    return this.authorizationHeader;
+  }
+
+  public boolean isErrorResponse() {
+    return this.errorResponse;
+  }
+
+  public boolean isRedirecting() {
+    return this.redirecting;
+  }
+
+  public boolean isUrlParameter() {
+    return this.urlParameter;
+  }
+
+  public boolean isValid() {
+    return this.grantType != null;
+  }
+
+  public void setAccessToken(final OAuth2Token accessToken) {
+    this.accessToken = accessToken;
+  }
+
+  public void setAuthorizationHeader(final boolean authorizationHeader) {
+    this.authorizationHeader = authorizationHeader;
+  }
+
+  public void setAuthorizationUrl(final String authorizationUrl) {
+    this.authorizationUrl = authorizationUrl;
+  }
+
+  public void setClientAuthenticationType(final String clientAuthenticationType) {
+    this.clientAuthenticationType = clientAuthenticationType;
+  }
+
+  public void setClientId(final String clientId) {
+    this.clientId = clientId;
+  }
+
+  public void setClientSecret(final byte[] clientSecret) {
+    this.clientSecret = clientSecret;
+  }
+
+  public void setErrorResponse(final Throwable exception, final OAuth2Error error,
+          final String contextMessage, final String errorUri) {
+    this.errorResponse = true;
+    this.errorException = exception;
+    if (error != null) {
+      this.error = error;
+      this.errorContextMessage = contextMessage;
+      this.errorUri = errorUri;
+    }
+  }
+
+  public void setErrorUri(final String errorUri) {
+    this.errorUri = errorUri;
+  }
+
+  public void setGrantType(final String grantType) {
+    this.grantType = grantType;
+  }
+
+  public void setRedirecting(final boolean redirecting) {
+    this.redirecting = redirecting;
+  }
+
+  public void setRedirectUri(final String redirectUri) {
+    this.redirectUri = redirectUri;
+  }
+
+  public void setRefreshToken(final OAuth2Token refreshToken) {
+    this.refreshToken = refreshToken;
+  }
+
+  public void setAdditionalRequestParams(final Map<String, String> additionalRequestParams) {
+    this.additionalRequestParams = additionalRequestParams;
+  }
+
+  public void setTokenUrl(final String tokenUrl) {
+    this.tokenUrl = tokenUrl;
+  }
+
+  public void setType(final Type type) {
+    this.type = type;
+  }
+
+  public void setUrlParameter(final boolean urlParameter) {
+    this.urlParameter = urlParameter;
+  }
+
+  public void setAllowedDomains(final String[] allowedDomains) {
+    this.allowedDomains = allowedDomains;
+  }
+
+  public String[] getAllowedDomains() {
+    return this.allowedDomains;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Message.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Message.java
new file mode 100644
index 0000000..1a1f8c0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Message.java
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.common.collect.Maps;
+
+/**
+ * See {@link OAuth2Message}
+ *
+ */
+public class BasicOAuth2Message implements OAuth2Message {
+  private final static String LOG_CLASS = BasicOAuth2Message.class.getName();
+  private final static FilteredLogger LOG = FilteredLogger
+      .getFilteredLogger(BasicOAuth2Message.LOG_CLASS);
+
+  private OAuth2Error error;
+  private final Map<String, String> params;
+  private final Map<String, String> unparsedProperties;
+
+  public BasicOAuth2Message() {
+    this.params = Maps.newHashMapWithExpectedSize(5);
+    this.unparsedProperties = Maps.newHashMapWithExpectedSize(0);
+  }
+
+  public String getAccessToken() {
+    return this.params.get(OAuth2Message.ACCESS_TOKEN);
+  }
+
+  public String getAuthorization() {
+    return this.params.get(OAuth2Message.AUTHORIZATION);
+  }
+
+  public OAuth2Error getError() {
+    if (this.error == null) {
+      final String errorParam = this.params.get(OAuth2Message.ERROR);
+      if (errorParam != null) {
+        if (OAuth2Message.INVALID_REQUEST.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_INVALID_REQUEST;
+        } else if (OAuth2Message.UNAUTHORIZED_CLIENT.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_UNAUTHORIZED_CLIENT;
+        } else if (OAuth2Message.ACCESS_DENIED.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_ACCESS_DENIED;
+        } else if (OAuth2Message.UNSUPPORTED_RESPONSE_TYPE.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_UNSUPPORTED_RESPONSE_TYPE;
+        } else if (OAuth2Message.INVALID_SCOPE.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_INVALID_SCOPE;
+        } else if (OAuth2Message.SERVER_ERROR.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_SERVER_ERROR;
+        } else if (OAuth2Message.TEMPORARILY_UNAVAILABLE.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_TEMPORARILY_UNAVAILABLE;
+        } else if (OAuth2Message.INVALID_CLIENT.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_INVALID_CLIENT;
+        } else if (OAuth2Message.INVALID_GRANT.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_INVALID_GRANT;
+        } else if (OAuth2Message.UNSUPPORTED_GRANT_TYPE.equalsIgnoreCase(errorParam)) {
+          this.error = OAuth2Error.SPEC_UNSUPPORTED_GRANT_TYPE;
+        } else {
+          this.error = OAuth2Error.UNKNOWN_PROBLEM;
+        }
+      }
+    }
+
+    return this.error;
+  }
+
+  public String getErrorDescription() {
+    return this.params.get(OAuth2Message.ERROR_DESCRIPTION);
+  }
+
+  public String getErrorUri() {
+    return this.params.get(OAuth2Message.ERROR_URI);
+  }
+
+  public String getExpiresIn() {
+    return this.params.get(OAuth2Message.EXPIRES_IN);
+  }
+
+  public String getMacAlgorithm() {
+    return this.params.get(OAuth2Message.MAC_ALGORITHM);
+  }
+
+  public String getMacSecret() {
+    return this.params.get(OAuth2Message.MAC_SECRET);
+  }
+
+  public Map<String, String> getParameters() {
+    return this.params;
+  }
+
+  public String getRefreshToken() {
+    return this.params.get(OAuth2Message.REFRESH_TOKEN);
+  }
+
+  public String getState() {
+    return this.params.get(OAuth2Message.STATE);
+  }
+
+  public String getTokenType() {
+    return this.params.get(OAuth2Message.TOKEN_TYPE);
+  }
+
+  public Map<String, String> getUnparsedProperties() {
+    // TODO ARC
+    return this.unparsedProperties;
+  }
+
+  public void parseFragment(final String fragment) {
+    final Uri uri = Uri.parse(fragment);
+    final Map<String, List<String>> _params = uri.getFragmentParameters();
+    for (final Entry<String, List<String>> entry : _params.entrySet()) {
+      this.params.put(entry.getKey(), entry.getValue().get(0));
+    }
+  }
+
+  public void parseJSON(final String response) {
+    try {
+      final JSONObject jsonObject = new JSONObject(response);
+      final String accessToken = jsonObject.optString(OAuth2Message.ACCESS_TOKEN, null);
+      if (accessToken != null) {
+        this.params.put(OAuth2Message.ACCESS_TOKEN, accessToken);
+      }
+
+      final String tokenType = jsonObject.optString(OAuth2Message.TOKEN_TYPE, null);
+      if (tokenType != null) {
+        this.params.put(OAuth2Message.TOKEN_TYPE, tokenType);
+      }
+
+      final String expiresIn = jsonObject.optString(OAuth2Message.EXPIRES_IN, null);
+      if (expiresIn != null) {
+        this.params.put(OAuth2Message.EXPIRES_IN, expiresIn);
+      }
+
+      final String refreshToken = jsonObject.optString(OAuth2Message.REFRESH_TOKEN, null);
+      if (refreshToken != null) {
+        this.params.put(OAuth2Message.REFRESH_TOKEN, refreshToken);
+      }
+
+      final String _error = jsonObject.optString(OAuth2Message.ERROR, null);
+      if (_error != null) {
+        this.params.put(OAuth2Message.ERROR, _error);
+      }
+
+      final String errorDescription = jsonObject.optString(OAuth2Message.ERROR_DESCRIPTION, null);
+      if (errorDescription != null) {
+        this.params.put(OAuth2Message.ERROR_DESCRIPTION, errorDescription);
+      }
+
+      final String errorUri = jsonObject.optString(OAuth2Message.ERROR_URI, null);
+      if (errorUri != null) {
+        this.params.put(OAuth2Message.ERROR_URI, errorUri);
+      }
+    } catch (final JSONException e) {
+      if (BasicOAuth2Message.LOG.isLoggable()) {
+        BasicOAuth2Message.LOG.log("JSONException parsing response", e);
+      }
+      this.params.put(OAuth2Message.ERROR, "JSONException parsing response");
+    }
+  }
+
+  public void parseQuery(final String query) {
+    final Uri uri = Uri.parse(query);
+    final Map<String, List<String>> _params = uri.getQueryParameters();
+    for (final Entry<String, List<String>> entry : _params.entrySet()) {
+      this.params.put(entry.getKey(), entry.getValue().get(0));
+    }
+    if ((!this.params.containsKey(OAuth2Message.EXPIRES_IN))
+        && (this.params.containsKey("expires"))) {
+      this.params.put(OAuth2Message.EXPIRES_IN, this.params.get("expires"));
+    }
+  }
+
+  public void parseRequest(final HttpServletRequest request) {
+    @SuppressWarnings("unchecked")
+    final Enumeration<String> paramNames = request.getParameterNames();
+    while (paramNames.hasMoreElements()) {
+      final String paramName = paramNames.nextElement();
+      final String param = request.getParameter(paramName);
+      this.params.put(paramName, param);
+    }
+  }
+
+  public void setError(OAuth2Error error) {
+    this.params.put(OAuth2Message.ERROR, error.getErrorCode());
+  }
+
+  public void setErrorDescription(String errorDescription) {
+    this.params.put(OAuth2Message.ERROR_DESCRIPTION, errorDescription);
+  }
+
+  public void setErrorUri(String errorUri) {
+    this.params.put(OAuth2Message.ERROR_URI, errorUri);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Request.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Request.java
new file mode 100644
index 0000000..530edc4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Request.java
@@ -0,0 +1,1043 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.oauth2.handler.AuthorizationEndpointResponseHandler;
+import org.apache.shindig.gadgets.oauth2.handler.ClientAuthenticationHandler;
+import org.apache.shindig.gadgets.oauth2.handler.GrantRequestHandler;
+import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError;
+import org.apache.shindig.gadgets.oauth2.handler.ResourceRequestHandler;
+import org.apache.shindig.gadgets.oauth2.handler.TokenEndpointResponseHandler;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+
+/**
+ * see {@link OAuth2Request}
+ *
+ */
+public class BasicOAuth2Request implements OAuth2Request {
+  private static final String LOG_CLASS = BasicOAuth2Request.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger
+          .getFilteredLogger(BasicOAuth2Request.LOG_CLASS);
+
+  private static final short MAX_ATTEMPTS = 3;
+
+  private OAuth2Accessor internalAccessor;
+
+  private OAuth2Arguments arguments;
+
+  private final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers;
+
+  private final List<ClientAuthenticationHandler> clientAuthenticationHandlers;
+
+  private final HttpFetcher fetcher;
+
+  private final OAuth2FetcherConfig fetcherConfig;
+
+  private final List<GrantRequestHandler> grantRequestHandlers;
+
+  private HttpRequest realRequest;
+
+  private final List<ResourceRequestHandler> resourceRequestHandlers;
+
+  private OAuth2ResponseParams responseParams;
+
+  private SecurityToken securityToken;
+
+  private final OAuth2Store store;
+
+  private final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers;
+
+  private final boolean sendTraceToClient;
+
+  private final OAuth2RequestParameterGenerator requestParameterGenerator;
+
+  private short attemptCounter = 0;
+
+  /**
+   * @param fetcherConfig
+   *          configuration options for the fetcher
+   * @param fetcher
+   *          fetcher to use for actually making requests
+   */
+  @Inject
+  public BasicOAuth2Request(final OAuth2FetcherConfig fetcherConfig, final HttpFetcher fetcher,
+          final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers,
+          final List<ClientAuthenticationHandler> clientAuthenticationHandlers,
+          final List<GrantRequestHandler> grantRequestHandlers,
+          final List<ResourceRequestHandler> resourceRequestHandlers,
+          final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers,
+          final boolean sendTraceToClient,
+          final OAuth2RequestParameterGenerator requestParameterGenerator) {
+    this.fetcherConfig = fetcherConfig;
+    if (this.fetcherConfig != null) {
+      this.store = this.fetcherConfig.getOAuth2Store();
+    } else {
+      this.store = null;
+    }
+    this.fetcher = fetcher;
+    this.authorizationEndpointResponseHandlers = authorizationEndpointResponseHandlers;
+    this.clientAuthenticationHandlers = clientAuthenticationHandlers;
+    this.grantRequestHandlers = grantRequestHandlers;
+    this.resourceRequestHandlers = resourceRequestHandlers;
+    this.tokenEndpointResponseHandlers = tokenEndpointResponseHandlers;
+    this.sendTraceToClient = sendTraceToClient;
+    this.requestParameterGenerator = requestParameterGenerator;
+
+    if (BasicOAuth2Request.LOG.isLoggable()) {
+      BasicOAuth2Request.LOG.log("this.fetcherConfig = {0}", this.fetcherConfig);
+      BasicOAuth2Request.LOG.log("this.store = {0}", this.store);
+      BasicOAuth2Request.LOG.log("this.fetcher = {0}", this.fetcher);
+      BasicOAuth2Request.LOG.log("this.authorizationEndpointResponseHandlers = {0}",
+              this.authorizationEndpointResponseHandlers);
+      BasicOAuth2Request.LOG.log("this.clientAuthenticationHandlers = {0}",
+              this.clientAuthenticationHandlers);
+      BasicOAuth2Request.LOG.log("this.grantRequestHandlers = {0}", this.grantRequestHandlers);
+      BasicOAuth2Request.LOG
+              .log("this.resourceRequestHandlers = {0}", this.resourceRequestHandlers);
+      BasicOAuth2Request.LOG.log("this.tokenEndpointResponseHandlers = {0}",
+              this.tokenEndpointResponseHandlers);
+      BasicOAuth2Request.LOG.log("this.sendTraceToClient = {0}", this.sendTraceToClient);
+    }
+  }
+
+  public HttpResponse fetch(final HttpRequest request) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "fetch", request);
+    }
+
+    OAuth2Accessor accessor = null;
+
+    HttpResponse response = null;
+
+    try {
+      // First step is to get an OAuth2Accessor for this request
+      if (request == null || request.getSecurityToken() == null) {
+        // Any errors before we have an accessor are special cases
+        response = this.sendErrorResponse(null, OAuth2Error.MISSING_FETCH_PARAMS,
+                "no request or security token");
+      } else {
+        this.realRequest = request;
+        this.securityToken = request.getSecurityToken();
+        this.responseParams = new OAuth2ResponseParams();
+        this.arguments = this.realRequest.getOAuth2Arguments();
+
+        if (BasicOAuth2Request.LOG.isLoggable()) {
+          BasicOAuth2Request.LOG.log("this.realRequest = {0}", this.realRequest);
+          BasicOAuth2Request.LOG.log("this.securityToken = {0}", this.securityToken);
+          BasicOAuth2Request.LOG.log("this.responseParams = {0}", this.responseParams);
+          BasicOAuth2Request.LOG.log("this.arguments = {0}", this.arguments);
+        }
+
+        if (this.responseParams == null || this.arguments == null) {
+          // Any errors before we have an accessor are special cases
+          return this.sendErrorResponse(null, OAuth2Error.FETCH_INIT_PROBLEM,
+                  "no responseParams or arguments");
+        }
+
+        accessor = this.getAccessor();
+
+        if (BasicOAuth2Request.LOG.isLoggable()) {
+          BasicOAuth2Request.LOG.log("accessor", accessor);
+        }
+
+        if (accessor == null) {
+          // Any errors before we have an accessor are special cases
+          response = this.sendErrorResponse(null, OAuth2Error.FETCH_INIT_PROBLEM,
+                  "accessor is null");
+        } else {
+          accessor.setRedirecting(false);
+
+          final Map<String, String> requestParams = this.requestParameterGenerator
+                  .generateParams(this.realRequest);
+          accessor.setAdditionalRequestParams(requestParams);
+
+          HttpResponseBuilder responseBuilder = null;
+          if (!accessor.isErrorResponse()) {
+            responseBuilder = this.attemptFetch(accessor);
+          }
+
+          response = this.processResponse(accessor, responseBuilder);
+        }
+      }
+    } catch (final Throwable t) {
+      BasicOAuth2Request.LOG.log(Level.SEVERE, "exception occurred during fetch", t);
+      if (accessor == null) {
+        accessor = new BasicOAuth2Accessor(t, OAuth2Error.FETCH_PROBLEM,
+                "exception occurred during fetch", "");
+      } else {
+        accessor.setErrorResponse(t, OAuth2Error.FETCH_PROBLEM, "exception occurred during fetch",
+                "");
+      }
+      response = this.processResponse(accessor, this.getErrorResponseBuilder(t,
+              OAuth2Error.FETCH_PROBLEM, "exception occurred during fetch"));
+    } finally {
+      if (accessor != null) {
+        if (!accessor.isRedirecting()) {
+          if (BasicOAuth2Request.LOG.isLoggable()) {
+            BasicOAuth2Request.LOG.log("accessor is not redirecting, remove it", accessor);
+          }
+          accessor.invalidate();
+          this.store.removeOAuth2Accessor(accessor);
+          this.internalAccessor = null;
+        } else {
+          if (!accessor.isValid()) {
+            if (BasicOAuth2Request.LOG.isLoggable()) {
+              BasicOAuth2Request.LOG.log("accesssor is not valid", accessor);
+            }
+          } else if (accessor.isErrorResponse()) {
+            if (BasicOAuth2Request.LOG.isLoggable()) {
+              BasicOAuth2Request.LOG.log("accessor isErrorResponse",
+                      accessor.getErrorContextMessage());
+            }
+          }
+          this.store.storeOAuth2Accessor(accessor);
+        }
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "fetch", response);
+    }
+
+    return response;
+  }
+
+  private HttpResponseBuilder attemptFetch(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "attemptFetch",
+              new Object[] { accessor });
+      BasicOAuth2Request.LOG.log("BasicOAuth2Request.haveAccessToken(accessor) = {0}",
+              BasicOAuth2Request.haveAccessToken(accessor) == null);
+      BasicOAuth2Request.LOG.log("BasicOAuth2Request.haveRefreshToken(accessor) = {0}",
+              BasicOAuth2Request.haveRefreshToken(accessor) == null);
+    }
+
+    if (this.attemptCounter > BasicOAuth2Request.MAX_ATTEMPTS) {
+      if (isLogging) {
+        BasicOAuth2Request.LOG.log("MAX_ATTEMPTS exceeded {0}", this.attemptCounter);
+        // This can be useful to diagnose the recursion
+        final StackTraceElement[] stackElements = Thread.currentThread().getStackTrace();
+        String stack = "";
+        for (final StackTraceElement element : stackElements) {
+          stack = stack + element.toString() + "\n";
+        }
+        BasicOAuth2Request.LOG.log("MAX_ATTEMPTS stack = {0}", stack);
+      }
+      return this.fetchData(accessor, true);
+    }
+
+    this.attemptCounter++;
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.log("attempt number {0}", this.attemptCounter);
+    }
+
+    HttpResponseBuilder ret = null;
+
+    if (accessor.isErrorResponse()) {
+      // If there's an error in the accessor don't continue.
+      return this.getErrorResponseBuilder(accessor.getErrorException(), accessor.getError(),
+              accessor.getErrorContextMessage(), accessor.getErrorUri(), accessor.getErrorContextMessage());
+    } else {
+      if (BasicOAuth2Request.haveAccessToken(accessor) != null) {
+        // We have an access_token, use it and stop!
+        // Don't try more than three times
+        ret = this.fetchData(accessor, this.attemptCounter > BasicOAuth2Request.MAX_ATTEMPTS);
+      } else {
+        // We don't have an access token, we need to try and get one.
+        // First step see if we have a refresh token
+        if (BasicOAuth2Request.haveRefreshToken(accessor) != null) {
+          if (BasicOAuth2Request.checkCanRefresh()) {
+            boolean attempt = false;
+            final String internedAccessor = getAccessorKey(accessor).intern();
+            if (isLogging) {
+              BasicOAuth2Request.LOG.log("about to synchronize on {0}", internedAccessor);
+            }
+            // This syncrhonized block is less than ideal.
+            // It is needed because if a gadget has multiple makeRequests that triggers
+            // multiple refreshes they can end up clobbering each other, and cause
+            // temporary failures until the gadget is refreshed.
+            // Syncrhonizing on the internedAccessor helps.  It is not cluster safe
+            // and could be problematic having so much code synchd.
+            // TODO : https://issues.apache.org/jira/browse/SHINDIG-1871
+            synchronized (internedAccessor) {
+              final OAuth2Accessor acc = this.getAccessorInternal();
+              if (isLogging) {
+                BasicOAuth2Request.LOG.log("acc = {0}", acc);
+                BasicOAuth2Request.LOG.log("BasicOAuth2Request.haveAccessToken(acc) = {0}",
+                        BasicOAuth2Request.haveAccessToken(acc) == null);
+                BasicOAuth2Request.LOG.log("BasicOAuth2Request.haveRefreshToken(acc) = {0}",
+                        BasicOAuth2Request.haveRefreshToken(acc) == null);
+              }
+              if (BasicOAuth2Request.haveAccessToken(acc) != null) {
+                // Another refresh must have won
+                if (isLogging) {
+                  BasicOAuth2Request.LOG.log("found an access token from another refresh",
+                          new Object[] {});
+                }
+                attempt = true;
+              } else {
+                final OAuth2HandlerError handlerError = this.refreshToken(accessor);
+                if (handlerError == null) {
+                  // No errors refreshing, attempt the fetch again.
+                  attempt = true;
+                  if (isLogging) {
+                    BasicOAuth2Request.LOG.log("no refresh errors reported", new Object[] {});
+                  }
+                } else {
+                  if (isLogging) {
+                    BasicOAuth2Request.LOG.log("refresh errors reported", new Object[] {});
+                  }
+                  // There was an error refreshing, stop.
+                  final OAuth2Error error = handlerError.getError();
+                  ret = this.getErrorResponseBuilder(handlerError.getCause(), error,
+                          handlerError.getContextMessage(), handlerError.getUri(),
+                          handlerError.getDescription());
+                }
+              }
+            }
+            if (attempt) {
+              if (isLogging) {
+                BasicOAuth2Request.LOG.log("going to re-attempt with a clean accesor",
+                        new Object[] {});
+              }
+              this.store.removeOAuth2Accessor(this.internalAccessor);
+              this.internalAccessor = null;
+              ret = this.attemptFetch(this.getAccessor());
+            }
+          } else {
+            // User cannot refresh, they'll have to try to authorize again.
+            accessor.setAccessToken(null);
+            accessor.setRefreshToken(null);
+            ret = this.attemptFetch(accessor);
+          }
+        } else {
+          // We have no access token and no refresh token.
+          // User needs to authorize again.
+          if (!accessor.isRedirecting() && this.checkCanAuthorize(accessor)) {
+            final String completeAuthUrl = this.authorize(accessor);
+            if (completeAuthUrl != null) {
+              // Send a response to redirect to the authorization url
+              this.responseParams.setAuthorizationUrl(completeAuthUrl);
+              accessor.setRedirecting(true);
+            } else {
+              // This wasn't a redirect type of authorization. try again
+              ret = this.attemptFetch(accessor);
+            }
+          }
+        }
+      }
+
+      if (ret == null) {
+        if (accessor.isRedirecting()) {
+          // Send redirect response to client
+          ret = new HttpResponseBuilder().setHttpStatusCode(HttpResponse.SC_OK).setStrictNoCache();
+        } else {
+          accessor.setAccessToken(null);
+          ret = this.attemptFetch(accessor);
+        }
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "attemptFetch", ret);
+    }
+
+    return ret;
+  }
+
+  private String authorize(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "authorize", accessor);
+    }
+
+    String ret = null;
+
+    final String grantType = accessor.getGrantType();
+
+    GrantRequestHandler grantRequestHandlerUsed = null;
+    for (final GrantRequestHandler grantRequestHandler : this.grantRequestHandlers) {
+      if (grantRequestHandler.getGrantType().equalsIgnoreCase(grantType)) {
+        grantRequestHandlerUsed = grantRequestHandler;
+        break;
+      }
+    }
+
+    if (grantRequestHandlerUsed == null) {
+      accessor.setErrorResponse(null, OAuth2Error.AUTHENTICATION_PROBLEM,
+              "no grantRequestHandler found for " + grantType, "");
+    } else {
+      String completeAuthUrl = null;
+      try {
+        completeAuthUrl = grantRequestHandlerUsed.getCompleteUrl(accessor);
+      } catch (final OAuth2RequestException e) {
+        if (isLogging) {
+          BasicOAuth2Request.LOG.log("error getting complete url", e);
+        }
+      }
+      if (grantRequestHandlerUsed.isRedirectRequired()) {
+        ret = completeAuthUrl;
+      } else {
+        final OAuth2HandlerError error = this.authorize(accessor, grantRequestHandlerUsed,
+                completeAuthUrl);
+        if (error != null) {
+          accessor.setErrorResponse(error.getCause(), OAuth2Error.AUTHENTICATION_PROBLEM,
+                  error.getContextMessage() + " , " + error.getDescription(), error.getUri());
+        }
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "authorize", ret);
+    }
+
+    return ret;
+  }
+
+  private OAuth2HandlerError authorize(final OAuth2Accessor accessor,
+          final GrantRequestHandler grantRequestHandler, final String completeAuthUrl) {
+
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "authorize", new Object[] {
+              accessor, grantRequestHandler, completeAuthUrl });
+    }
+
+    OAuth2HandlerError ret = null;
+
+    HttpRequest authorizationRequest;
+    try {
+      authorizationRequest = grantRequestHandler.getAuthorizationRequest(accessor, completeAuthUrl);
+    } catch (final OAuth2RequestException e) {
+      authorizationRequest = null;
+      ret = new OAuth2HandlerError(e.getError(), e.getErrorText(), e);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.log("authorizationRequest = {0}", authorizationRequest);
+    }
+
+    if (authorizationRequest != null) {
+      HttpResponse authorizationResponse;
+      try {
+        authorizationResponse = this.fetcher.fetch(authorizationRequest);
+      } catch (final GadgetException e) {
+        if (isLogging) {
+          BasicOAuth2Request.LOG.log("authorize()", e);
+        }
+        authorizationResponse = null;
+        ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZE_PROBLEM,
+                "exception thrown fetching authorization", e);
+      }
+
+      if (isLogging) {
+        BasicOAuth2Request.LOG.log("authorizationResponse = {0}", authorizationResponse);
+      }
+
+      if (authorizationResponse != null) {
+        if (grantRequestHandler.isAuthorizationEndpointResponse()) {
+          for (final AuthorizationEndpointResponseHandler authorizationEndpointResponseHandler : this.authorizationEndpointResponseHandlers) {
+            if (authorizationEndpointResponseHandler.handlesResponse(accessor,
+                    authorizationResponse)) {
+              if (isLogging) {
+                BasicOAuth2Request.LOG.log("using AuthorizationEndpointResponseHandler = {0}",
+                        authorizationEndpointResponseHandler);
+              }
+              ret = authorizationEndpointResponseHandler.handleResponse(accessor,
+                      authorizationResponse);
+              if (ret != null) {
+                // error occurred stop processing
+                break;
+              }
+            }
+          }
+        }
+
+        if (ret == null) {
+          if (grantRequestHandler.isTokenEndpointResponse()) {
+            for (final TokenEndpointResponseHandler tokenEndpointResponseHandler : this.tokenEndpointResponseHandlers) {
+              if (tokenEndpointResponseHandler.handlesResponse(accessor, authorizationResponse)) {
+                if (isLogging) {
+                  BasicOAuth2Request.LOG.log("using TokenEndpointResponseHandler = {0}",
+                          tokenEndpointResponseHandler);
+                }
+                ret = tokenEndpointResponseHandler.handleResponse(accessor, authorizationResponse);
+                if (ret != null) {
+                  // error occurred stop processing
+                  break;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "authorize", ret);
+    }
+
+    return ret;
+  }
+
+  private static String buildRefreshTokenUrl(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "buildRefreshTokenUrl",
+              accessor);
+    }
+
+    String ret = null;
+
+    final String refreshUrl = accessor.getTokenUrl();
+    if (refreshUrl != null) {
+      ret = BasicOAuth2Request.getCompleteRefreshUrl(refreshUrl);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "buildRefreshTokenUrl", ret);
+    }
+
+    return ret;
+  }
+
+  private boolean checkCanAuthorize(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "checkCanAuthorize", accessor);
+    }
+
+    boolean ret = true;
+
+    if (BasicOAuth2Request.LOG.isLoggable()) {
+      BasicOAuth2Request.LOG.log("securityToken = {0}", this.securityToken);
+    }
+
+    final String pageOwner = this.securityToken.getOwnerId();
+    final String pageViewer = this.securityToken.getViewerId();
+
+    if (BasicOAuth2Request.LOG.isLoggable()) {
+      BasicOAuth2Request.LOG.log("pageOwner = {0}", pageOwner);
+      BasicOAuth2Request.LOG.log("pageViewer = {0}", pageViewer);
+    }
+
+    if (pageOwner == null || pageViewer == null) {
+      accessor.setErrorResponse(null, OAuth2Error.AUTHORIZE_PROBLEM,
+              "pageOwner or pageViewer is null", "");
+      ret = false;
+    } else if (!this.fetcherConfig.isViewerAccessTokensEnabled() && !pageOwner.equals(pageViewer)) {
+      accessor.setErrorResponse(null, OAuth2Error.AUTHORIZE_PROBLEM, "pageViewer is not pageOwner",
+              "");
+      ret = false;
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "checkCanAuthorize", ret);
+    }
+
+    return ret;
+  }
+
+  private static boolean checkCanRefresh() {
+    // Everyone can try to refresh???
+    return true;
+  }
+
+  private HttpResponseBuilder fetchData(final OAuth2Accessor accessor, final boolean lastAttempt) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "fetchData", accessor);
+    }
+
+    HttpResponseBuilder ret = null;
+
+    try {
+      final HttpResponse response = this.fetchFromServer(accessor, this.realRequest, lastAttempt);
+      if (response != null) {
+        ret = new HttpResponseBuilder(response);
+
+        if (response.getHttpStatusCode() != HttpResponse.SC_OK && this.sendTraceToClient) {
+          this.responseParams.addRequestTrace(this.realRequest, response);
+        }
+      }
+    } catch (final OAuth2RequestException e) {
+      ret = this.getErrorResponseBuilder(e, e.getError(), e.getErrorText(), e.getErrorUri(),
+              e.getErrorDescription());
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "fetchData", ret);
+    }
+
+    return ret;
+  }
+
+  private HttpResponse fetchFromServer(final OAuth2Accessor accessor, final HttpRequest request,
+          final boolean lastAttempt) throws OAuth2RequestException {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "fetchFromServer",
+              new Object[] { accessor, "only log request once", lastAttempt });
+    }
+
+    HttpResponse ret;
+
+    final long currentTime = System.currentTimeMillis();
+
+    OAuth2Token accessToken = accessor.getAccessToken();
+    if (accessToken != null) {
+      final long expiresAt = accessToken.getExpiresAt();
+      if (expiresAt != 0) {
+        if (currentTime >= expiresAt) {
+          if (BasicOAuth2Request.LOG.isLoggable()) {
+            BasicOAuth2Request.LOG.log("accessToken has expired at {0}", expiresAt);
+          }
+          try {
+            this.store.removeToken(accessToken);
+          } catch (final GadgetException e) {
+            throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE,
+                    "error removing access_token", null);
+          }
+          accessToken = null;
+          accessor.setAccessToken(null);
+          if (!lastAttempt) {
+            return null;
+          }
+        }
+      }
+    }
+
+    OAuth2Token refreshToken = accessor.getRefreshToken();
+    if (refreshToken != null) {
+      final long expiresAt = refreshToken.getExpiresAt();
+      if (expiresAt != 0) {
+        if (currentTime >= expiresAt) {
+          if (BasicOAuth2Request.LOG.isLoggable()) {
+            BasicOAuth2Request.LOG.log("refreshToken has expired at {0}", expiresAt);
+          }
+          try {
+            this.store.removeToken(refreshToken);
+          } catch (final GadgetException e) {
+            throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE,
+                    "error removing refresh_token", null);
+          }
+          refreshToken = null;
+          accessor.setRefreshToken(null);
+          if (!lastAttempt) {
+            return null;
+          }
+        }
+      }
+    }
+
+    if (BasicOAuth2Request.LOG.isLoggable()) {
+      BasicOAuth2Request.LOG.log("accessToken = {0}", accessToken);
+      BasicOAuth2Request.LOG.log("refreshToken = {0}", refreshToken);
+    }
+
+    if (accessToken != null) {
+      final boolean isAllowed = OAuth2Utils.isUriAllowed(request.getUri(), accessor.getAllowedDomains());
+      if (isAllowed) {
+        String tokenType = accessToken.getTokenType();
+        if (tokenType == null || tokenType.length() == 0) {
+          tokenType = OAuth2Message.BEARER_TOKEN_TYPE;
+        }
+
+        for (final ResourceRequestHandler resourceRequestHandler : this.resourceRequestHandlers) {
+          if (tokenType.equalsIgnoreCase(resourceRequestHandler.getTokenType())) {
+            resourceRequestHandler.addOAuth2Params(accessor, request);
+          }
+        }
+      } else {
+        BasicOAuth2Request.LOG.log(Level.WARNING,
+                "Gadget {0} attempted to send OAuth2 Token to an unauthorized domain: {1}.",
+                new Object[] { accessor.getGadgetUri(), request.getUri() });
+      }
+    }
+
+    try {
+      ret = this.fetcher.fetch(request);
+    } catch (final GadgetException e) {
+      throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE,
+              "GadgetException fetchFromServer", e);
+    }
+
+    if (ret == null) {
+      throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE, "response is null",
+              null);
+    }
+
+    final int responseCode = ret.getHttpStatusCode();
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.log("responseCode = {0}", responseCode);
+    }
+
+    if (responseCode == HttpResponse.SC_UNAUTHORIZED) {
+      if (accessToken != null) {
+        try {
+          this.store.removeToken(accessToken);
+        } catch (final GadgetException e) {
+          throw new OAuth2RequestException(OAuth2Error.MISSING_SERVER_RESPONSE,
+                  "error removing access_token", null);
+        }
+        accessor.setAccessToken(null);
+      }
+
+      if (!lastAttempt) {
+        ret = null;
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "fetchFromServer", ret);
+    }
+
+    return ret;
+  }
+
+  private OAuth2Accessor getAccessorInternal() {
+    OAuth2Accessor ret = null;
+    if (this.fetcherConfig != null) {
+      final GadgetOAuth2TokenStore tokenStore = this.fetcherConfig.getTokenStore();
+      if (tokenStore != null) {
+        ret = tokenStore.getOAuth2Accessor(this.securityToken, this.arguments,
+                this.realRequest.getGadget());
+      }
+    }
+    return ret;
+  }
+
+  private OAuth2Accessor getAccessor() {
+    if (this.internalAccessor == null || !this.internalAccessor.isValid()) {
+      this.internalAccessor = this.getAccessorInternal();
+    }
+
+    return this.internalAccessor;
+  }
+
+  private static String getCompleteRefreshUrl(final String refreshUrl) {
+    return OAuth2Utils.buildUrl(refreshUrl, null, null);
+  }
+
+  private HttpResponseBuilder getErrorResponseBuilder(final Throwable t, final OAuth2Error error,
+          final String contextMessage) {
+    return this.getErrorResponseBuilder(t, error, contextMessage);
+  }
+
+  private HttpResponseBuilder getErrorResponseBuilder(final Throwable t, final OAuth2Error error,
+          final String contextMessage, final String errorUri, final String errorDescription) {
+
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "getErrorResponseBuilder",
+              new Object[] { t, error, contextMessage, errorUri, errorDescription });
+    }
+
+    final HttpResponseBuilder ret = new HttpResponseBuilder().setHttpStatusCode(
+            HttpResponse.SC_FORBIDDEN).setStrictNoCache();
+
+    if (t != null && this.sendTraceToClient) {
+      final StringWriter sw = new StringWriter();
+      t.printStackTrace(new PrintWriter(sw));
+      final String message = sw.toString();
+      this.responseParams.addDebug(message);
+    }
+
+    if (this.sendTraceToClient) {
+      this.responseParams.addToResponse(ret, error.getErrorCode(),
+              error.getErrorDescription(contextMessage) + " , " + errorDescription, errorUri,
+              error.getErrorExplanation());
+    } else {
+      this.responseParams.addToResponse(ret, error.getErrorCode(), "", "",
+              error.getErrorExplanation());
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "getErrorResponseBuilder", ret);
+    }
+
+    return ret;
+  }
+
+  private static String getRefreshBody(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "getRefreshBody", accessor);
+    }
+
+    String ret = "";
+
+    Map<String, String> queryParams;
+    try {
+      queryParams = Maps.newHashMap();
+      queryParams.put(OAuth2Message.GRANT_TYPE, OAuth2Message.REFRESH_TOKEN);
+      queryParams.put(OAuth2Message.REFRESH_TOKEN, new String(accessor.getRefreshToken()
+              .getSecret(), "UTF-8"));
+      if (accessor.getScope() != null && accessor.getScope().length() > 0) {
+        queryParams.put(OAuth2Message.SCOPE, accessor.getScope());
+      }
+
+      final String clientId = accessor.getClientId();
+      final byte[] secret = accessor.getClientSecret();
+      queryParams.put(OAuth2Message.CLIENT_ID, clientId);
+      queryParams.put(OAuth2Message.CLIENT_SECRET, new String(secret, "UTF-8"));
+
+      ret = OAuth2Utils.buildUrl(ret, queryParams, null);
+
+      final char firstChar = ret.charAt(0);
+      if (firstChar == '?' || firstChar == '&') {
+        ret = ret.substring(1);
+      }
+
+      if (isLogging) {
+        BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "getRefreshBody", ret);
+      }
+    } catch (final UnsupportedEncodingException e) {
+      if (isLogging) {
+        BasicOAuth2Request.LOG.log("error generating refresh body", e);
+        ret = null;
+      }
+    }
+
+    return ret;
+  }
+
+  private HttpResponse processResponse(final OAuth2Accessor accessor,
+          final HttpResponseBuilder responseBuilder) {
+
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "processResponse",
+              new Object[] { accessor, responseBuilder == null });
+    }
+
+    if (accessor.isErrorResponse() || responseBuilder == null) {
+      return this.sendErrorResponse(accessor.getErrorException(), accessor.getError(),
+              accessor.getErrorContextMessage(), accessor.getErrorUri(), "");
+    }
+
+    if (this.responseParams.getAuthorizationUrl() != null) {
+      responseBuilder.setMetadata(OAuth2ResponseParams.APPROVAL_URL,
+              this.responseParams.getAuthorizationUrl());
+      accessor.setRedirecting(true);
+    } else {
+      accessor.setRedirecting(false);
+    }
+
+    final HttpResponse ret = responseBuilder.create();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "processResponse",
+              "response logged in fetch()");
+    }
+
+    return ret;
+  }
+
+  private OAuth2HandlerError refreshToken(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Request.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Request.LOG.entering(BasicOAuth2Request.LOG_CLASS, "refreshToken",
+              new Object[] { accessor });
+    }
+
+    OAuth2HandlerError ret = null;
+
+    String refershTokenUrl;
+
+    refershTokenUrl = BasicOAuth2Request.buildRefreshTokenUrl(accessor);
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.log("refershTokenUrl = {0}", refershTokenUrl);
+    }
+
+    if (refershTokenUrl != null) {
+      HttpResponse response = null;
+      final HttpRequest request = new HttpRequest(Uri.parse(refershTokenUrl));
+      request.setSecurityToken(new AnonymousSecurityToken("", 0L, accessor.getGadgetUri()));
+      request.setMethod("POST");
+      request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
+
+      for (final ClientAuthenticationHandler clientAuthenticationHandler : this.clientAuthenticationHandlers) {
+        if (clientAuthenticationHandler.geClientAuthenticationType().equalsIgnoreCase(
+                accessor.getClientAuthenticationType())) {
+          clientAuthenticationHandler.addOAuth2Authentication(request, accessor);
+        }
+      }
+
+      try {
+        final byte[] body = BasicOAuth2Request.getRefreshBody(accessor).getBytes("UTF-8");
+        request.setPostBody(body);
+      } catch (final Exception e) {
+        if (isLogging) {
+          BasicOAuth2Request.LOG.log("refreshToken()", e);
+        }
+        ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM,
+                "error generating refresh body", e);
+      }
+
+      if (!OAuth2Utils.isUriAllowed(request.getUri(), accessor.getAllowedDomains())) {
+        ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM,
+                "error fetching refresh token - domain not allowed", null);
+      }
+
+      if (ret == null) {
+        try {
+          response = this.fetcher.fetch(request);
+        } catch (final GadgetException e) {
+          if (isLogging) {
+            BasicOAuth2Request.LOG.log("refreshToken()", e);
+          }
+          ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM,
+                  "error fetching refresh token", e);
+        }
+
+        if (isLogging) {
+          BasicOAuth2Request.LOG.log("response = {0}", response);
+        }
+
+        if (response == null) {
+          ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM, "response is null", null);
+        }
+
+        if (ret == null) {
+          // response is not null..
+          final int statusCode = response.getHttpStatusCode();
+          if (statusCode == HttpResponse.SC_UNAUTHORIZED
+                  || statusCode == HttpResponse.SC_BAD_REQUEST) {
+            try {
+              this.store.removeToken(accessor.getRefreshToken());
+            } catch (final GadgetException e) {
+              ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM,
+                      "failed to remove refresh token", e);
+            }
+            accessor.setRefreshToken(null);
+            if (isLogging) {
+              BasicOAuth2Request.LOG.log(Level.FINEST,
+                      "received {0} from provider, removed refresh token.  response = {1}",
+                      new Object[] { statusCode, response.getResponseAsString() });
+            }
+            return null;
+          } else if (statusCode != HttpResponse.SC_OK) {
+            ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM,
+                    "bad response from server : " + statusCode, null, "",
+                    response.getResponseAsString());
+          }
+
+          if (ret == null) {
+            for (final TokenEndpointResponseHandler tokenEndpointResponseHandler : this.tokenEndpointResponseHandlers) {
+              if (tokenEndpointResponseHandler.handlesResponse(accessor, response)) {
+                final OAuth2HandlerError error = tokenEndpointResponseHandler.handleResponse(
+                        accessor, response);
+                if (error != null) {
+                  try {
+                    this.store.removeToken(accessor.getRefreshToken());
+                  } catch (final GadgetException e) {
+                    ret = new OAuth2HandlerError(OAuth2Error.REFRESH_TOKEN_PROBLEM,
+                            error.getContextMessage(), e, error.getUri(), error.getDescription());
+                  }
+                  accessor.setRefreshToken(null);
+                  return error;
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Request.LOG.exiting(BasicOAuth2Request.LOG_CLASS, "refreshToken", ret);
+    }
+
+    return ret;
+  }
+
+  private HttpResponse sendErrorResponse(final Throwable t, final OAuth2Error error,
+          final String contextMessage) {
+    final HttpResponseBuilder responseBuilder = this.getErrorResponseBuilder(t, error,
+            contextMessage);
+    return responseBuilder.create();
+  }
+
+  private HttpResponse sendErrorResponse(final Throwable t, final OAuth2Error error,
+          final String contextMessage, final String errorUri, final String errorDescription) {
+    final HttpResponseBuilder responseBuilder = this.getErrorResponseBuilder(t, error,
+            contextMessage, errorUri, errorDescription);
+    return responseBuilder.create();
+  }
+
+  private static OAuth2Token haveAccessToken(final OAuth2Accessor accessor) {
+    OAuth2Token ret = accessor.getAccessToken();
+    if (ret != null) {
+      if (!BasicOAuth2Request.validateAccessToken(ret)) {
+        ret = null;
+      }
+    }
+    return ret;
+  }
+
+  private static OAuth2Token haveRefreshToken(final OAuth2Accessor accessor) {
+    OAuth2Token ret = accessor.getRefreshToken();
+    if (ret != null) {
+      if (!BasicOAuth2Request.validateRefreshToken(ret)) {
+        ret = null;
+      }
+    }
+    return ret;
+  }
+
+  private static boolean validateAccessToken(final OAuth2Token accessToken) {
+    return accessToken != null;
+  }
+
+  private static boolean validateRefreshToken(final OAuth2Token refreshToken) {
+    return refreshToken != null;
+  }
+
+  private static String getAccessorKey(final OAuth2Accessor accessor) {
+    if (accessor != null) {
+      return "accessor:" + accessor.getGadgetUri() + ':' + accessor.getServiceName() + ':'
+              + accessor.getUser() + ':' + accessor.getScope();
+    }
+
+    return null;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2RequestParameterGenerator.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2RequestParameterGenerator.java
new file mode 100644
index 0000000..a04ee03
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2RequestParameterGenerator.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.common.collect.Maps;
+import org.apache.shindig.gadgets.http.HttpRequest;
+
+import java.util.Map;
+
+/**
+ * Default implementation of a request parameter generator that return an empty map
+ */
+public class BasicOAuth2RequestParameterGenerator implements OAuth2RequestParameterGenerator {
+
+  public Map<String, String> generateParams(HttpRequest request) {
+
+    return Maps.newHashMap();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Store.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Store.java
new file mode 100644
index 0000000..59e66ef
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2Store.java
@@ -0,0 +1,534 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetException.Code;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Cache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2CacheException;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2PersistenceException;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Persister;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2TokenPersistence;
+
+import java.util.Set;
+
+/**
+ * see {@link OAuth2Store}
+ *
+ * Default OAuth2Store.  Handles a persistence scenario with a separate cache
+ * and persistence layer.
+ *
+ * Uses 3 Guice bindings to achieve storage implementation.
+ *
+ * 1) {@link OAuth2Persister} 2) {@link OAuth2Cache} 3) {@link OAuth2Encrypter}
+ *
+ */
+public class BasicOAuth2Store implements OAuth2Store {
+  private static final String LOG_CLASS = BasicOAuth2Store.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger
+          .getFilteredLogger(BasicOAuth2Store.LOG_CLASS);
+
+  private final OAuth2Cache cache;
+  private final String globalRedirectUri;
+  private final Authority authority;
+  private final String contextRoot;
+  private final OAuth2Persister persister;
+  private final OAuth2Encrypter encrypter;
+  private final BlobCrypter stateCrypter;
+
+  @Inject
+  public BasicOAuth2Store(final OAuth2Cache cache, final OAuth2Persister persister,
+          final OAuth2Encrypter encrypter, final String globalRedirectUri,
+          final Authority authority, final String contextRoot,
+          @Named(OAuth2FetcherConfig.OAUTH2_STATE_CRYPTER)
+          final BlobCrypter stateCrypter) {
+    this.cache = cache;
+    this.persister = persister;
+    this.globalRedirectUri = globalRedirectUri;
+    this.authority = authority;
+    this.contextRoot = contextRoot;
+    this.encrypter = encrypter;
+    this.stateCrypter = stateCrypter;
+    if (BasicOAuth2Store.LOG.isLoggable()) {
+      BasicOAuth2Store.LOG.log("this.cache = {0}", this.cache);
+      BasicOAuth2Store.LOG.log("this.persister = {0}", this.persister);
+      BasicOAuth2Store.LOG.log("this.globalRedirectUri = {0}", this.globalRedirectUri);
+      BasicOAuth2Store.LOG.log("this.encrypter = {0}", this.encrypter);
+      BasicOAuth2Store.LOG.log("this.stateCrypter = {0}", this.stateCrypter);
+    }
+  }
+
+  public boolean clearCache() throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "clearCache");
+    }
+
+    try {
+      this.cache.clearClients();
+      this.cache.clearTokens();
+      this.cache.clearAccessors();
+    } catch (final OAuth2PersistenceException e) {
+      if (isLogging) {
+        BasicOAuth2Store.LOG.log("Error clearing OAuth2 cache", e);
+      }
+      throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error clearing OAuth2 cache", e);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "clearCache", true);
+    }
+
+    return true;
+  }
+
+  public OAuth2Token createToken() {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "createToken");
+    }
+
+    final OAuth2Token ret = this.internalCreateToken();
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "clearCache", ret);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Client getClient(final String gadgetUri, final String serviceName)
+          throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "getClient", new Object[] {
+              gadgetUri, serviceName });
+    }
+
+    OAuth2Client client = this.cache.getClient(gadgetUri, serviceName);
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.log("client from cache = {0}", client);
+    }
+
+    if (client == null) {
+      try {
+        client = this.persister.findClient(gadgetUri, serviceName);
+        if (client != null) {
+          this.cache.storeClient(client);
+        }
+      } catch (final OAuth2PersistenceException e) {
+        if (isLogging) {
+          BasicOAuth2Store.LOG.log("Error loading OAuth2 client ", e);
+        }
+        throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error loading OAuth2 client "
+                + serviceName, e);
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "getClient", client);
+    }
+
+    return client;
+  }
+
+  public OAuth2Accessor getOAuth2Accessor(final OAuth2CallbackState state) {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "getOAuth2Accessor", state);
+    }
+
+    final OAuth2Accessor ret = this.cache.getOAuth2Accessor(state);
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "getOAuth2Accessor", ret);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Accessor getOAuth2Accessor(final String gadgetUri, final String serviceName,
+          final String user, final String scope) throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "getOAuth2Accessor", new Object[] {
+              gadgetUri, serviceName, user, scope });
+    }
+
+    final OAuth2CallbackState state = new OAuth2CallbackState(this.stateCrypter);
+    state.setGadgetUri(gadgetUri);
+    state.setServiceName(serviceName);
+    state.setUser(user);
+    state.setScope(scope);
+
+    OAuth2Accessor ret = this.cache.getOAuth2Accessor(state);
+
+    if (ret == null || !ret.isValid()) {
+      final OAuth2Client client = this.getClient(gadgetUri, serviceName);
+
+      if (client != null) {
+        final OAuth2Token accessToken = this.getToken(gadgetUri, serviceName, user, scope,
+                OAuth2Token.Type.ACCESS);
+        final OAuth2Token refreshToken = this.getToken(gadgetUri, serviceName, user, scope,
+                OAuth2Token.Type.REFRESH);
+
+        final BasicOAuth2Accessor newAccessor = new BasicOAuth2Accessor(gadgetUri, serviceName,
+                user, scope, client.isAllowModuleOverride(), this, this.globalRedirectUri,
+                this.authority, this.contextRoot);
+        newAccessor.setAccessToken(accessToken);
+        newAccessor.setAuthorizationUrl(client.getAuthorizationUrl());
+        newAccessor.setClientAuthenticationType(client.getClientAuthenticationType());
+        newAccessor.setAuthorizationHeader(client.isAuthorizationHeader());
+        newAccessor.setUrlParameter(client.isUrlParameter());
+        newAccessor.setClientId(client.getClientId());
+        newAccessor.setClientSecret(client.getClientSecret());
+        newAccessor.setGrantType(client.getGrantType());
+        newAccessor.setRedirectUri(client.getRedirectUri());
+        newAccessor.setRefreshToken(refreshToken);
+        newAccessor.setTokenUrl(client.getTokenUrl());
+        newAccessor.setType(client.getType());
+        newAccessor.setAllowedDomains(client.getAllowedDomains());
+        ret = newAccessor;
+
+        this.storeOAuth2Accessor(ret);
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "getOAuth2Accessor", ret);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Token getToken(final String gadgetUri, final String serviceName, final String user,
+          final String scope, final OAuth2Token.Type type) throws GadgetException {
+
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "getToken", new Object[] {
+              gadgetUri, serviceName, user, scope, type });
+    }
+
+    final String processedGadgetUri = this.getGadgetUri(gadgetUri, serviceName);
+    OAuth2Token token = this.cache.getToken(processedGadgetUri, serviceName, user, scope, type);
+    if (token == null) {
+      try {
+        token = this.persister.findToken(processedGadgetUri, serviceName, user, scope, type);
+        if (token != null) {
+          synchronized (token) {
+            try {
+              token.setGadgetUri(processedGadgetUri);
+              this.cache.storeToken(token);
+            } finally {
+              token.setGadgetUri(gadgetUri);
+            }
+          }
+        }
+      } catch (final OAuth2PersistenceException e) {
+        throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error loading OAuth2 token", e);
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "getToken", token);
+    }
+
+    return token;
+  }
+
+  public boolean init() throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "init");
+    }
+
+    if (this.cache.isPrimed()) {
+      if (isLogging) {
+        BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "init", false);
+      }
+      return false;
+    }
+
+    this.clearCache();
+
+    try {
+      final Set<OAuth2Client> clients = this.persister.loadClients();
+      if (isLogging) {
+        BasicOAuth2Store.LOG.log("clients = {0}", clients);
+      }
+      this.cache.storeClients(clients);
+    } catch (final OAuth2PersistenceException e) {
+      throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error loading OAuth2 clients", e);
+    }
+
+    try {
+      final Set<OAuth2Token> tokens = this.persister.loadTokens();
+      if (isLogging) {
+        BasicOAuth2Store.LOG.log("tokens = {0}", tokens);
+      }
+      this.cache.storeTokens(tokens);
+    } catch (final OAuth2PersistenceException e) {
+      throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error loading OAuth2 tokens", e);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "init", true);
+    }
+
+    return true;
+  }
+
+  public OAuth2Accessor removeOAuth2Accessor(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "removeOAuth2Accessor", accessor);
+    }
+
+    final OAuth2Accessor ret = null;
+
+    if (accessor != null) {
+      return this.cache.removeOAuth2Accessor(accessor);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "removeOAuth2Accessor", ret);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Token removeToken(final OAuth2Token token) throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "removeToken", token);
+    }
+
+    if (token != null) {
+      if (isLogging) {
+        BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "removeToken", token);
+      }
+
+      try {
+        synchronized (token) {
+          final String origGadgetApi = token.getGadgetUri();
+          final String processedGadgetUri = this.getGadgetUri(token.getGadgetUri(), token.getServiceName());
+          token.setGadgetUri(processedGadgetUri);
+          try {
+            // Remove token from the cache
+            this.cache.removeToken(token);
+            // Token is gone from the cache, also remove it from persistence
+            this.persister.removeToken(processedGadgetUri, token.getServiceName(), token.getUser(), token.getScope(), token.getType());
+          } finally {
+            token.setGadgetUri(origGadgetApi);
+          }
+        }
+
+        return token;
+      } catch (final OAuth2PersistenceException e) {
+        if (isLogging) {
+          BasicOAuth2Store.LOG.log("Error removing OAuth2 token ", e);
+        }
+        throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error removing OAuth2 token "
+                + token.getServiceName(), e);
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "removeToken", null);
+    }
+
+    return null;
+  }
+
+  public static boolean runImport(final OAuth2Persister source, final OAuth2Persister target,
+          final boolean clean) {
+    if (BasicOAuth2Store.LOG.isLoggable()) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "runImport", new Object[] { source,
+              target, clean });
+    }
+
+    // No import for default persistence
+    return false;
+  }
+
+  public void setToken(final OAuth2Token token) throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "setToken", token);
+    }
+
+    if (token != null) {
+      final String gadgetUri = token.getGadgetUri();
+      final String serviceName = token.getServiceName();
+
+      final String processedGadgetUri = this.getGadgetUri(gadgetUri, serviceName);
+      synchronized (token) {
+        token.setGadgetUri(processedGadgetUri);
+        try {
+          final OAuth2Token existingToken = this.getToken(gadgetUri, token.getServiceName(),
+                  token.getUser(), token.getScope(), token.getType());
+          try {
+            if (existingToken == null) {
+              this.persister.insertToken(token);
+            } else {
+              synchronized (existingToken) {
+                try {
+                  existingToken.setGadgetUri(processedGadgetUri);
+                  this.cache.removeToken(existingToken);
+                  this.persister.updateToken(token);
+                } finally {
+                  existingToken.setGadgetUri(gadgetUri);
+                }
+              }
+            }
+            this.cache.storeToken(token);
+          } catch (final OAuth2CacheException e) {
+            if (isLogging) {
+              BasicOAuth2Store.LOG.log("Error storing OAuth2 token", e);
+            }
+            throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error storing OAuth2 token", e);
+          } catch (final OAuth2PersistenceException e) {
+            if (isLogging) {
+              BasicOAuth2Store.LOG.log("Error storing OAuth2 token", e);
+            }
+            throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error storing OAuth2 token", e);
+          }
+        } finally {
+          token.setGadgetUri(gadgetUri);
+        }
+      }
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "setToken");
+    }
+  }
+
+  public void storeOAuth2Accessor(final OAuth2Accessor accessor) {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "storeOAuth2Accessor", accessor);
+    }
+
+    this.cache.storeOAuth2Accessor(accessor);
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "storeOAuth2Accessor");
+    }
+  }
+
+  protected String getGadgetUri(final String gadgetUri, final String serviceName)
+          throws GadgetException {
+    String ret = gadgetUri;
+    final OAuth2Client client = this.getClient(ret, serviceName);
+    if (client != null) {
+      if (client.isSharedToken()) {
+        ret = client.getClientId() + ':' + client.getServiceName();
+      }
+    }
+
+    return ret;
+  }
+
+  protected OAuth2Token internalCreateToken() {
+    return new OAuth2TokenPersistence(this.encrypter);
+  }
+
+  public BlobCrypter getStateCrypter() {
+    return this.stateCrypter;
+  }
+
+  public OAuth2Client invalidateClient(final OAuth2Client client) {
+    return this.cache.removeClient(client);
+  }
+
+  public OAuth2Token invalidateToken(final OAuth2Token token) {
+    return this.cache.removeToken(token);
+  }
+
+  public void clearAccessorCache() throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "clearAccessorCache");
+    }
+
+    try {
+      this.cache.clearAccessors();
+    } catch (final OAuth2CacheException e) {
+      if (isLogging) {
+        BasicOAuth2Store.LOG.log("Error clearing OAuth2 Accessor cache", e);
+      }
+      throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error clearing OAuth2Accessor cache", e);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "clearAccessorCache");
+    }
+  }
+
+  public void clearTokenCache() throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "clearTokenCache");
+    }
+
+    try {
+      this.cache.clearTokens();
+    } catch (final OAuth2CacheException e) {
+      if (isLogging) {
+        BasicOAuth2Store.LOG.log("Error clearing OAuth2 Token cache", e);
+      }
+      throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error clearing OAuth2Token cache", e);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "clearTokenCache");
+    }
+  }
+
+  public void clearClientCache() throws GadgetException {
+    final boolean isLogging = BasicOAuth2Store.LOG.isLoggable();
+    if (isLogging) {
+      BasicOAuth2Store.LOG.entering(BasicOAuth2Store.LOG_CLASS, "clearClientCache");
+    }
+
+    try {
+      this.cache.clearClients();
+    } catch (final OAuth2CacheException e) {
+      if (isLogging) {
+        BasicOAuth2Store.LOG.log("Error clearing OAuth2 Client cache", e);
+      }
+      throw new GadgetException(Code.OAUTH_STORAGE_ERROR, "Error clearing OAuth2Client cache", e);
+    }
+
+    if (isLogging) {
+      BasicOAuth2Store.LOG.exiting(BasicOAuth2Store.LOG_CLASS, "clearClientCache");
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/GadgetOAuth2TokenStore.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/GadgetOAuth2TokenStore.java
new file mode 100644
index 0000000..952b0a7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/GadgetOAuth2TokenStore.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+import org.apache.shindig.gadgets.spec.BaseOAuthService.EndPoint;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.OAuth2Service;
+import org.apache.shindig.gadgets.spec.OAuth2Spec;
+
+import com.google.common.base.Joiner;
+import com.google.inject.Inject;
+
+/**
+ * Higher-level interface that allows callers to store and retrieve
+ * OAuth2-related data directly from {@code GadgetSpec}s, {@code GadgetContext}
+ * s, etc. See {@link OAuth2Store} for a more detailed explanation of the OAuth
+ * 2.0 Data Store.
+ */
+public class GadgetOAuth2TokenStore {
+
+  private final static String LOG_CLASS = GadgetOAuth2TokenStore.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger
+      .getFilteredLogger(GadgetOAuth2TokenStore.LOG_CLASS);
+
+  private static class OAuth2SpecInfo {
+    private final String authorizationUrl;
+    private final String scope;
+    private final String tokenUrl;
+
+    public OAuth2SpecInfo(final String authorizationUrl, final String tokenUrl, final String scope) {
+      this.authorizationUrl = authorizationUrl;
+      this.tokenUrl = tokenUrl;
+      this.scope = scope;
+    }
+
+    public String getAuthorizationUrl() {
+      return this.authorizationUrl;
+    }
+
+    public String getScope() {
+      return this.scope;
+    }
+
+    public String getTokenUrl() {
+      return this.tokenUrl;
+    }
+  }
+
+  private final GadgetSpecFactory specFactory;
+
+  private final OAuth2Store store;
+
+  @Inject
+  public GadgetOAuth2TokenStore(final OAuth2Store store, final GadgetSpecFactory specFactory) {
+    this.store = store;
+    this.specFactory = specFactory;
+    if (GadgetOAuth2TokenStore.LOG.isLoggable()) {
+      GadgetOAuth2TokenStore.LOG.log("this.store = {0}", this.store);
+      GadgetOAuth2TokenStore.LOG.log("this.specFactory = {0}", this.specFactory);
+    }
+  }
+
+  private GadgetSpec findSpec(final SecurityToken securityToken, final OAuth2Arguments arguments,
+      final Uri gadgetUri) throws OAuth2RequestException {
+    final boolean isLogging = GadgetOAuth2TokenStore.LOG.isLoggable();
+    if (isLogging) {
+      GadgetOAuth2TokenStore.LOG.entering(GadgetOAuth2TokenStore.LOG_CLASS, "findSpec",
+          new Object[] { arguments, gadgetUri });
+    }
+
+    GadgetSpec ret;
+
+    try {
+      final GadgetContext context = new OAuth2GadgetContext(securityToken, arguments, gadgetUri);
+      ret = this.specFactory.getGadgetSpec(context);
+    } catch (final IllegalArgumentException e) {
+      if (isLogging) {
+        GadgetOAuth2TokenStore.LOG.log("Error finding GadgetContext " + gadgetUri.toString(), e);
+      }
+      throw new OAuth2RequestException(OAuth2Error.GADGET_SPEC_PROBLEM, gadgetUri.toString(), e);
+    } catch (final GadgetException e) {
+      if (isLogging) {
+        GadgetOAuth2TokenStore.LOG.log("Error finding GadgetContext " + gadgetUri.toString(), e);
+      }
+      throw new OAuth2RequestException(OAuth2Error.GADGET_SPEC_PROBLEM, gadgetUri.toString(), e);
+    }
+
+    if (isLogging) {
+      // this is cumbersome in the logs, just return whether or not it's null
+      if (ret == null) {
+        GadgetOAuth2TokenStore.LOG.exiting(GadgetOAuth2TokenStore.LOG_CLASS, "findSpec", null);
+      } else {
+        GadgetOAuth2TokenStore.LOG.exiting(GadgetOAuth2TokenStore.LOG_CLASS, "findSpec",
+            "non-null spec omitted from logs");
+      }
+    }
+
+    return ret;
+  }
+
+  /**
+   * Retrieves and merges the data from the {@link OAuth2Store}, the gadget spec
+   * and the request itself to populate the OAuth2 data for this requets.
+   *
+   * @param securityToken
+   *          {@link SecurityToken} from the request
+   * @param arguments
+   *          {@link OAuth2Arguments} from the request
+   * @param gadgetUri
+   *          gadget uri from the request
+   * @return the {@link OAuth2Accessor} for the request
+   * @throws OAuth2RequestException
+   */
+  public OAuth2Accessor getOAuth2Accessor(final SecurityToken securityToken,
+      final OAuth2Arguments arguments, final Uri gadgetUri) {
+
+    final boolean isLogging = GadgetOAuth2TokenStore.LOG.isLoggable();
+    if (isLogging) {
+      GadgetOAuth2TokenStore.LOG.entering(GadgetOAuth2TokenStore.LOG_CLASS, "getOAuth2Accessor",
+          new Object[] { securityToken, arguments, gadgetUri });
+    }
+
+    OAuth2Accessor ret = null;
+
+    if ((this.store == null) || (gadgetUri == null) || (securityToken == null)) {
+      ret = new BasicOAuth2Accessor(null, OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM,
+          "OAuth2Accessor missing a param --- store = " + this.store + " , gadgetUri = "
+              + gadgetUri + " , securityToken = " + securityToken, "");
+    } else {
+      final String serviceName = arguments != null ? arguments.getServiceName() : "";
+
+      OAuth2SpecInfo specInfo = null;
+      try {
+        specInfo = this.lookupSpecInfo(securityToken, arguments, gadgetUri);
+      } catch (final OAuth2RequestException e1) {
+        if (isLogging) {
+          GadgetOAuth2TokenStore.LOG.log("No gadget spec", e1);
+        }
+        ret = new BasicOAuth2Accessor(e1, OAuth2Error.NO_GADGET_SPEC, "gadgetUri = " + gadgetUri
+            + " , serviceName = " + serviceName, "");
+      }
+
+      if (specInfo == null) {
+        ret = new BasicOAuth2Accessor(null, OAuth2Error.NO_GADGET_SPEC, "gadgetUri = " + gadgetUri
+            + " , serviceName = " + serviceName, "");
+      }
+
+      if (ret == null && arguments != null) {
+        String scope = arguments.getScope();
+        if ((scope == null) || (scope.length() == 0)) {
+          // no scope on request, default to module prefs scope
+          scope = specInfo.getScope();
+        }
+
+        if ((scope == null) || (scope.length() == 0)) {
+          scope = "";
+        }
+
+        OAuth2Accessor persistedAccessor;
+        try {
+          persistedAccessor = this.store.getOAuth2Accessor(gadgetUri.toString(), serviceName,
+              securityToken.getViewerId(), scope);
+        } catch (final GadgetException e) {
+          if (isLogging) {
+            GadgetOAuth2TokenStore.LOG.log("Exception in getOAuth2Accessor", e);
+          }
+          persistedAccessor = null;
+        }
+
+        if (persistedAccessor == null) {
+          ret = new BasicOAuth2Accessor(null, OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM,
+              "gadgetUri = " + gadgetUri + " , serviceName = " + serviceName, "");
+        } else {
+          final OAuth2Accessor mergedAccessor = new BasicOAuth2Accessor(persistedAccessor);
+
+          if (persistedAccessor.isAllowModuleOverrides()) {
+            final String specAuthorizationUrl = specInfo.getAuthorizationUrl();
+            final String specTokenUrl = specInfo.getTokenUrl();
+
+            if ((specAuthorizationUrl != null) && (specAuthorizationUrl.length() > 0)) {
+              mergedAccessor.setAuthorizationUrl(specAuthorizationUrl);
+            }
+            if ((specTokenUrl != null) && (specTokenUrl.length() > 0)) {
+              mergedAccessor.setTokenUrl(specTokenUrl);
+            }
+          }
+
+          this.store.storeOAuth2Accessor(mergedAccessor);
+
+          ret = mergedAccessor;
+        }
+      }
+    }
+
+    if (isLogging) {
+      GadgetOAuth2TokenStore.LOG
+          .exiting(GadgetOAuth2TokenStore.LOG_CLASS, "getOAuth2Accessor", ret);
+    }
+
+    return ret;
+  }
+
+  /**
+   *
+   * @return the {@link OAuth2Store}, never <code>null</code>
+   */
+  public OAuth2Store getOAuth2Store() {
+    return this.store;
+  }
+
+  private OAuth2SpecInfo lookupSpecInfo(final SecurityToken securityToken,
+      final OAuth2Arguments arguments, final Uri gadgetUri) throws OAuth2RequestException {
+
+    final boolean isLogging = GadgetOAuth2TokenStore.LOG.isLoggable();
+    if (isLogging) {
+      GadgetOAuth2TokenStore.LOG.entering(GadgetOAuth2TokenStore.LOG_CLASS, "lookupSpecInfo",
+          new Object[] { securityToken, arguments, gadgetUri });
+    }
+
+    final GadgetSpec spec = this.findSpec(securityToken, arguments, gadgetUri);
+    final OAuth2Spec oauthSpec = spec.getModulePrefs().getOAuth2Spec();
+    if (oauthSpec == null) {
+      throw new OAuth2RequestException(OAuth2Error.LOOKUP_SPEC_PROBLEM,
+          "Failed to retrieve OAuth URLs, spec for gadget " + securityToken.getAppUrl()
+              + " does not contain OAuth element.", null);
+    }
+    final OAuth2Service service = oauthSpec.getServices().get(arguments.getServiceName());
+    if (service == null) {
+      throw new OAuth2RequestException(OAuth2Error.LOOKUP_SPEC_PROBLEM,
+          "Failed to retrieve OAuth URLs, spec for gadget does not contain OAuth service "
+              + arguments.getServiceName() + ".  Known services: "
+              + Joiner.on(',').join(oauthSpec.getServices().keySet()) + '.', null);
+    }
+
+    String authorizationUrl = null;
+    final EndPoint authorizationUrlEndpoint = service.getAuthorizationUrl();
+    if (authorizationUrlEndpoint != null) {
+      authorizationUrl = authorizationUrlEndpoint.url.toString();
+    }
+
+    String tokenUrl = null;
+    final EndPoint tokenUrlEndpoint = service.getTokenUrl();
+    if (tokenUrlEndpoint != null) {
+      tokenUrl = tokenUrlEndpoint.url.toString();
+    }
+
+    final OAuth2SpecInfo ret = new OAuth2SpecInfo(authorizationUrl, tokenUrl, service.getScope());
+
+    if (isLogging) {
+      GadgetOAuth2TokenStore.LOG.exiting(GadgetOAuth2TokenStore.LOG_CLASS, "lookupSpecInfo", ret);
+    }
+
+    return ret;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Accessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Accessor.java
new file mode 100644
index 0000000..32d243b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Accessor.java
@@ -0,0 +1,278 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.gadgets.oauth2.handler.ClientAuthenticationHandler;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * OAuth2 related data accessor.
+ *
+ * Every {@link OAuth2Request} will create an accessor and store it in the OAuth2Store while the
+ * request is being issued. It will be removed when the request is done (success or failure.)
+ *
+ * OAuth2Accessor implementations should be {@link Serializable} to facilitate cluster storage and
+ * caching across the various phases of OAuth 2.0 flows.
+ */
+
+public interface OAuth2Accessor extends Serializable {
+  /**
+   *
+   * Enumeration of possible accessor types
+   *
+   */
+  public enum Type {
+    CONFIDENTIAL, PUBLIC, UNKNOWN
+  }
+
+  /**
+   *
+   * @return the access {@link OAuth2Token} or <code>null</code>
+   */
+  OAuth2Token getAccessToken();
+
+  /**
+   *
+   * @return the authorization endpoint for this accessor.
+   */
+  String getAuthorizationUrl();
+
+  /**
+   * see {@link ClientAuthenticationHandler}
+   *
+   * @return the type of client authentication the service provider expects
+   */
+  String getClientAuthenticationType();
+
+  /**
+   *
+   * @return the "client_id" for this accessor
+   */
+  String getClientId();
+
+  /**
+   *
+   * @return the "client_secret" for this accessor
+   */
+  byte[] getClientSecret();
+
+  /**
+   *
+   *
+   */
+  OAuth2Error getError();
+
+  /**
+   *
+   */
+  String getErrorContextMessage();
+
+  /**
+   *
+   * @return the error exception, if this is an error, otherwise <code>null</code>
+   */
+  Throwable getErrorException();
+
+  /**
+   *
+   * @return the error uri, if this is an error, otherwise <code>null</code>
+   */
+  String getErrorUri();
+
+  /**
+   *
+   * @return the URI of the gadget issuing the request
+   */
+  String getGadgetUri();
+
+  /**
+   *
+   * @return grant_type of this client, e.g. "code" or "client_credentials"
+   */
+  String getGrantType();
+
+  /**
+   *
+   * @return redirect_uri of the client for this accessor
+   */
+  String getRedirectUri();
+
+  /**
+   *
+   * @return the refresh {@link OAuth2Token} or <code>null</code>
+   */
+  OAuth2Token getRefreshToken();
+
+  /**
+   *
+   * @return the additional oauth2 request params (never <code>null</code>)
+   */
+  Map<String, String> getAdditionalRequestParams();
+
+  /**
+   * if the gadget request or gadget spec specifies a scope it will be set here
+   *
+   * See {@link http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-3.3}
+   *
+   * @return scope of the request, or "" if none was specified
+   */
+  String getScope();
+
+  /**
+   *
+   * @return the service name from the gadget spec, defaults to ""
+   */
+  String getServiceName();
+
+  /**
+   *
+   * @return the state to include on authorization requests
+   */
+  OAuth2CallbackState getState();
+
+  /**
+   *
+   * @return the token endpoint for this accessor.
+   */
+  String getTokenUrl();
+
+  /**
+   *
+   * @return the {@link Type} of client for this accessor
+   */
+  Type getType();
+
+  /**
+   *
+   * @return of the page viewer
+   */
+  String getUser();
+
+  /**
+   * invalidates the accessor once the request is done.
+   *
+   */
+  void invalidate();
+
+  /**
+   *
+   * @return <code>true</code> if the gadget's <ModulePrefs> can override accessor settings
+   */
+  boolean isAllowModuleOverrides();
+
+  /**
+   * Indicates the service provider wants the access token in an "Authorization:" header, per the
+   * spec.
+   *
+   * @return
+   */
+  boolean isAuthorizationHeader();
+
+  /**
+   *
+   * @return if an error response needs to be sent to the client
+   */
+  boolean isErrorResponse();
+
+  /**
+   * is this accessor in the middle of a authorize redirect?
+   *
+   * @return
+   */
+  boolean isRedirecting();
+
+  /**
+   * Indicates the service provider wants the access token in an URL Parameter. This goes against
+   * the spec but Google, Facebook and Microsoft all expect it.
+   *
+   * @return
+   */
+  boolean isUrlParameter();
+
+  boolean isValid();
+
+  /**
+   * updates the access token for the request (does not add it to {@link OAuth2Store})
+   *
+   * @param accessToken
+   */
+  void setAccessToken(OAuth2Token accessToken);
+
+  /**
+   * updates the authorization endpoint url
+   *
+   * @param authorizationUrl
+   */
+  void setAuthorizationUrl(String authorizationUrl);
+
+  /**
+   *
+   * @param exception
+   * @param error
+   * @param contextMessage
+   * @param errorUri
+   */
+  void setErrorResponse(Throwable exception, OAuth2Error error, String contextMessage,
+          String errorUri);
+
+  /**
+   * Used to communicate that we are in a redirect authorization flow and the accessor should be
+   * preserved.
+   *
+   * @param redirecting
+   */
+  void setRedirecting(boolean redirecting);
+
+  /**
+   * updates the refresh token for the request (does not add it to {@link OAuth2Store})
+   *
+   * @param accessToken
+   */
+  void setRefreshToken(OAuth2Token refreshToken);
+
+  /**
+   * set the oauth2 request parameters
+   *
+   * @param requestParams
+   */
+  void setAdditionalRequestParams(Map<String, String> requestParams);
+
+  /**
+   * updates the token endpoint url
+   *
+   * @param tokenUrl
+   */
+  void setTokenUrl(String tokenUrl);
+
+  /**
+   * sets the domains of allowed resource servers
+   *
+   * @param allowedDomains
+   */
+  void setAllowedDomains(String[] allowedDomains);
+
+  /**
+   * gets the domains of allowed resource servers
+   *
+   * @return allowed domains, may be empty but never <code>null</code>
+   */
+  String[] getAllowedDomains();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Arguments.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Arguments.java
new file mode 100644
index 0000000..ec75214
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Arguments.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.util.Enumeration;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
+
+/**
+ * Arguments to an OAuth2 fetch sent by the client.
+ */
+public class OAuth2Arguments {
+  private static final String BYPASS_SPEC_CACHE_PARAM = "bypassSpecCache";
+  private static final String SCOPE_PARAM = "OAUTH_SCOPE";
+  private static final String SERVICE_PARAM = "OAUTH_SERVICE_NAME";
+
+  private final boolean bypassSpecCache;
+  private final Map<String, String> requestOptions = Maps.newTreeMap();
+  private final String scope;
+  private final String serviceName;
+
+  public OAuth2Arguments(final AuthType auth, final Map<String, String> map) {
+    if (AuthType.OAUTH2.equals(auth)) {
+      this.requestOptions.putAll(map);
+      this.serviceName = OAuth2Arguments.getAuthInfoParam(this.requestOptions,
+          OAuth2Arguments.SERVICE_PARAM, "");
+      this.scope = OAuth2Arguments.getAuthInfoParam(this.requestOptions,
+          OAuth2Arguments.SCOPE_PARAM, "");
+      this.bypassSpecCache = "1".equals(OAuth2Arguments.getAuthInfoParam(this.requestOptions,
+          OAuth2Arguments.BYPASS_SPEC_CACHE_PARAM, null));
+    } else {
+      this.serviceName = null;
+      this.scope = null;
+      this.bypassSpecCache = false;
+    }
+  }
+
+  /**
+   * Public constructor to parse OAuth2Arguments from a
+   * {@link HttpServletRequest}
+   *
+   * @param request
+   *          {@link HttpServletRequest} that came into the server
+   * @throws GadgetException
+   */
+  public OAuth2Arguments(final HttpServletRequest request) throws GadgetException {
+    this.serviceName = OAuth2Arguments.getRequestParam(request, OAuth2Arguments.SERVICE_PARAM, "");
+    this.scope = OAuth2Arguments.getRequestParam(request, OAuth2Arguments.SCOPE_PARAM, "");
+    this.bypassSpecCache = "1".equals(OAuth2Arguments.getRequestParam(request,
+        OAuth2Arguments.BYPASS_SPEC_CACHE_PARAM, null));
+    final Enumeration<String> params = OAuth2Arguments.getParameterNames(request);
+    while (params.hasMoreElements()) {
+      final String name = params.nextElement();
+      this.requestOptions.put(name, request.getParameter(name));
+    }
+  }
+
+  public OAuth2Arguments(final OAuth2Arguments orig) {
+    this.serviceName = orig.serviceName;
+    this.scope = orig.scope;
+    this.bypassSpecCache = orig.bypassSpecCache;
+    this.requestOptions.putAll(orig.requestOptions);
+  }
+
+  public OAuth2Arguments(final RequestAuthenticationInfo info) {
+    this(info.getAuthType(), info.getAttributes());
+  }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!(obj instanceof OAuth2Arguments)) {
+      return false;
+    }
+
+    final OAuth2Arguments other = (OAuth2Arguments) obj;
+    return (this.bypassSpecCache == other.getBypassSpecCache())
+        && this.serviceName.equals(other.getServiceName());
+  }
+
+  public boolean getBypassSpecCache() {
+    return this.bypassSpecCache;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Enumeration<String> getParameterNames(final HttpServletRequest request) {
+    return request.getParameterNames();
+  }
+
+  public String getRequestOption(String name) {
+    return this.requestOptions.get(name);
+  }
+
+  public String getRequestOption(String name, String def) {
+    final String val = this.requestOptions.get(name);
+    return (val != null ? val : def);
+  }
+
+  public String getScope() {
+    return this.scope;
+  }
+
+  public String getServiceName() {
+    return this.serviceName;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.bypassSpecCache, this.serviceName);
+  }
+
+  private static String getAuthInfoParam(final Map<String, String> attrs, final String name,
+      @Nullable final String def) {
+    String val = attrs.get(name);
+    if (val == null) {
+      val = def;
+    }
+    return val;
+  }
+
+  private static String getRequestParam(final HttpServletRequest request, final String name,
+      final String def) {
+    String val = request.getParameter(name);
+    if (val == null) {
+      val = def;
+    }
+    return val;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2CallbackState.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2CallbackState.java
new file mode 100644
index 0000000..d8c5a5b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2CallbackState.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import java.io.Serializable;
+import java.util.Map;
+
+public class OAuth2CallbackState implements Serializable {
+  private static final long serialVersionUID = 6591011719613609006L;
+  private static final String LOG_CLASS = OAuth2CallbackState.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger
+          .getFilteredLogger(OAuth2CallbackState.LOG_CLASS);
+
+  private final transient BlobCrypter crypter;
+  private OAuth2CallbackStateToken state;
+
+  public OAuth2CallbackState() {
+    this(null);
+  }
+
+  public OAuth2CallbackState(final BlobCrypter crypter) {
+    this.crypter = crypter;
+    this.state = new OAuth2CallbackStateToken();
+  }
+
+  public OAuth2CallbackState(final BlobCrypter crypter, final String stateBlob) {
+    this.crypter = crypter;
+
+    Map<String, String> state = null;
+    if (stateBlob != null && crypter != null) {
+      try {
+        state = crypter.unwrap(stateBlob);
+
+        if (state == null) {
+          state = Maps.newHashMap();
+        }
+        this.state = new OAuth2CallbackStateToken(state);
+        this.state.enforceNotExpired();
+      } catch (final BlobCrypterException e) {
+        // Too old, or corrupt. Ignore it.
+        state = null;
+        if (OAuth2CallbackState.LOG.isLoggable()) {
+          OAuth2CallbackState.LOG.log("OAuth2CallbackState stateBlob decryption failed", e);
+        }
+      }
+    }
+    if (state == null) {
+      this.state = new OAuth2CallbackStateToken();
+    }
+  }
+
+  public String getEncryptedState() throws BlobCrypterException {
+    String ret = null;
+    if (this.crypter != null) {
+      ret = this.crypter.wrap(this.state.toMap());
+    }
+
+    return ret;
+  }
+
+  public String getGadgetUri() {
+    return this.state.getGadgetUri();
+  }
+
+  public void setGadgetUri(final String gadgetUri) {
+    this.state.setGadgetUri(gadgetUri);
+  }
+
+  public String getServiceName() {
+    return this.state.getServiceName();
+  }
+
+  public void setServiceName(final String serviceName) {
+    this.state.setServiceName(serviceName);
+  }
+
+  public String getUser() {
+    return this.state.getUser();
+  }
+
+  public void setUser(final String user) {
+    this.state.setUser(user);
+  }
+
+  public String getScope() {
+    return this.state.getScope();
+  }
+
+  public void setScope(final String scope) {
+    this.state.setScope(scope);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2CallbackStateToken.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2CallbackStateToken.java
new file mode 100644
index 0000000..e30bead
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2CallbackStateToken.java
@@ -0,0 +1,163 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.auth.AbstractSecurityToken;
+
+import java.io.Serializable;
+import java.util.EnumSet;
+import java.util.Map;
+
+/**
+ *
+ */
+public class OAuth2CallbackStateToken extends AbstractSecurityToken implements Serializable {
+  private static final long serialVersionUID = -3913197153778386101L;
+  private static final EnumSet<Keys> MAP_KEYS = EnumSet.of(Keys.EXPIRES);
+  private static final String GADGET_URI = "g";
+  private static final String SERVICE_NAME = "sn";
+  private static final String USER = "u";
+  private static final String SCOPE = "sc";
+
+  private String gadgetUri;
+  private String serviceName;
+  private String user;
+  private String scope;
+
+  OAuth2CallbackStateToken() {
+    // used by OAuth2CallbackState
+  }
+
+  public OAuth2CallbackStateToken(final Map<String, String> values) {
+    this.loadFromMap(values);
+  }
+
+  @Override
+  protected AbstractSecurityToken loadFromMap(final Map<String, String> map) {
+    super.loadFromMap(map);
+    final String g = map.get(OAuth2CallbackStateToken.GADGET_URI);
+    if (g != null) {
+      this.setGadgetUri(g);
+    }
+
+    final String sn = map.get(OAuth2CallbackStateToken.SERVICE_NAME);
+    if (sn != null) {
+      this.setServiceName(sn);
+    }
+
+    final String u = map.get(OAuth2CallbackStateToken.USER);
+    if (u != null) {
+      this.setUser(u);
+    }
+
+    final String sc = map.get(OAuth2CallbackStateToken.SCOPE);
+    if (sc != null) {
+      this.setScope(sc);
+    }
+
+    return this;
+  }
+
+  public String getUpdatedToken() {
+    return null;
+  }
+
+  public String getAuthenticationMode() {
+    return null;
+  }
+
+  public boolean isAnonymous() {
+    return false;
+  }
+
+  @Override
+  protected EnumSet<Keys> getMapKeys() {
+    return OAuth2CallbackStateToken.MAP_KEYS;
+  }
+
+  public String getGadgetUri() {
+    return this.gadgetUri;
+  }
+
+  public String getServiceName() {
+    return this.serviceName;
+  }
+
+  public String getUser() {
+    return this.user;
+  }
+
+  public String getScope() {
+    return this.scope;
+  }
+
+  public OAuth2CallbackStateToken setGadgetUri(final String gadgetUri) {
+    this.gadgetUri = gadgetUri;
+    return this;
+  }
+
+  public OAuth2CallbackStateToken setServiceName(final String serviceName) {
+    this.serviceName = serviceName;
+    return this;
+  }
+
+  public OAuth2CallbackStateToken setUser(final String user) {
+    this.user = user;
+    return this;
+  }
+
+  public OAuth2CallbackStateToken setScope(final String scope) {
+    this.scope = scope;
+    return this;
+  }
+
+  /**
+   * Returns token time to live in seconds.
+   */
+  @Override
+  protected int getMaxTokenTTL() {
+    return 600;
+  }
+
+  @Override
+  public Map<String, String> toMap() {
+    final Map<String, String> map = super.toMap();
+    final String g = this.getGadgetUri();
+    if (g != null) {
+      map.put(OAuth2CallbackStateToken.GADGET_URI, g);
+    }
+
+    final String sn = this.getServiceName();
+    if (sn != null) {
+      map.put(OAuth2CallbackStateToken.SERVICE_NAME, sn);
+    }
+
+    final String u = this.getUser();
+    if (u != null) {
+      map.put(OAuth2CallbackStateToken.USER, u);
+    }
+
+    final String sc = this.getScope();
+    if (sc != null) {
+      map.put(OAuth2CallbackStateToken.SCOPE, sc);
+    }
+
+    return map;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Error.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Error.java
new file mode 100644
index 0000000..6074209
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Error.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import java.text.MessageFormat;
+import java.util.ResourceBundle;
+
+/**
+ * Any time there's an error in the OAuth2 layer it's reported with an OAuth2Error.
+ *
+ * errorCode - should correspond to an OAuth2Message errorCode when appropriate.
+ *
+ */
+public enum OAuth2Error {
+  AUTHORIZATION_CODE_PROBLEM("authorization_code_problem"),
+  AUTHORIZE_PROBLEM("authorize_problem"),
+  AUTHENTICATION_PROBLEM("authentication_problem"),
+  BEARER_TOKEN_PROBLEM("bearer_token_problem"),
+  CALLBACK_PROBLEM("callback_problem"),
+  CLIENT_CREDENTIALS_PROBLEM("client_credentials_problem"),
+  CODE_GRANT_PROBLEM("code_grant_problem"),
+  FETCH_INIT_PROBLEM("fetch_init_problem"),
+  FETCH_PROBLEM("fetch_problem"),
+  GADGET_SPEC_PROBLEM("gadget_spec_problem"),
+  GET_OAUTH2_ACCESSOR_PROBLEM("get_oauth2_accessor_problem"),
+  LOOKUP_SPEC_PROBLEM("lookup_spec_problem"),
+  MAC_TOKEN_PROBLEM("mac_token_problem"),
+  MISSING_FETCH_PARAMS("missing_fetch_params"),
+  MISSING_SERVER_RESPONSE("missing_server_response"),
+  NO_RESPONSE_HANDLER("no_response_handler"),
+  NO_GADGET_SPEC("no_gadget_spec"),
+  REFRESH_TOKEN_PROBLEM("refresh_token_problem"),
+  SECRET_ENCRYPTION_PROBLEM("secret_encryption_problem"),
+  SPEC_ACCESS_DENIED("access_denied"),
+  SPEC_INVALID_CLIENT("invalid_client"),
+  SPEC_INVALID_GRANT("invalid_grant"),
+  SPEC_INVALID_REQUEST("invalid_request"),
+  SPEC_INVALID_SCOPE("invalid_scope"),
+  SPEC_SERVER_ERROR("server_error"),
+  SPEC_TEMPORARILY_UNAVAILABLE("temporarily_unavailable"),
+  SPEC_UNAUTHORIZED_CLIENT("unauthorized_client"),
+  SPEC_UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"),
+  SPEC_UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"),
+  SERVER_REJECTED_REQUEST("server_rejected_request"),
+  TOKEN_RESPONSE_PROBLEM("token_response_problem"),
+  UNKNOWN_PROBLEM("unknown_problem");
+
+  public static final String MESSAGES = "org.apache.shindig.gadgets.oauth2.resource";
+
+  private static final String MESSAGE_HEADER = "message_header";
+
+  private final String errorCode;
+  private final String errorDescription;
+  private final String errorExplanation;
+
+  private OAuth2Error(final String errorCode) {
+    this.errorCode = errorCode;
+    String header = OAuth2Request.class.getName() + " encountered a problem: ";
+    String eDescription = errorCode;
+    String eExplanation = errorCode;
+
+    FilteredLogger log = null;
+    try {
+      log = FilteredLogger.getFilteredLogger("org.apache.shindig.gadgets.oauth2.OAuth2Error");
+      final ResourceBundle resourceBundle = log.getResourceBundle();
+      if (resourceBundle != null) {
+        final String bundleHeader = resourceBundle.getString("message_header");
+        if (bundleHeader != null) {
+          header = MessageFormat.format(bundleHeader, OAuth2Request.class.getName());
+        }
+
+        final String bundleErrorDescription = resourceBundle.getString(this.errorCode);
+        if ((bundleErrorDescription == null) || (bundleErrorDescription.length() == 0)) {
+          eDescription = header + this.errorCode;
+        } else {
+          eDescription = header + bundleErrorDescription;
+        }
+
+        final String bundleErrorExplanation = resourceBundle.getString(this.errorCode
+                + ".explanation");
+        if ((bundleErrorExplanation == null) || (bundleErrorExplanation.length() == 0)) {
+          eExplanation = eDescription;
+        } else {
+          eExplanation = bundleErrorExplanation;
+        }
+      }
+    } catch (final Exception e) {
+      if (log != null) {
+        if (log.isLoggable()) {
+          log.log("error loading OAuth2Error messages", e);
+        }
+      } else {
+        e.printStackTrace();
+      }
+    }
+
+    this.errorDescription = eDescription;
+    this.errorExplanation = eExplanation;
+  }
+
+  public String getErrorCode() {
+    return this.errorCode;
+  }
+
+  public String getErrorDescription(final Object... objects) {
+    return MessageFormat.format(this.errorDescription, objects);
+  }
+
+  public String getErrorExplanation() {
+    return this.errorExplanation;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2FetcherConfig.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2FetcherConfig.java
new file mode 100644
index 0000000..7badad6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2FetcherConfig.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Configuration parameters for an OAuth2Request
+ */
+public class OAuth2FetcherConfig {
+  private final GadgetOAuth2TokenStore tokenStore;
+  private final boolean viewerAccessTokensEnabled;
+  public static final String OAUTH2_STATE_CRYPTER = "shindig.oauth2.state-crypter";
+
+  @Inject
+  public OAuth2FetcherConfig(final GadgetOAuth2TokenStore tokenStore,
+          @Named("shindig.oauth2.viewer-access-tokens-enabled")
+          final boolean viewerAccessTokensEnabled) {
+    this.tokenStore = tokenStore;
+    this.viewerAccessTokensEnabled = viewerAccessTokensEnabled;
+  }
+
+  /**
+   * @return the store with persisted client and token information
+   */
+  public OAuth2Store getOAuth2Store() {
+    return this.tokenStore.getOAuth2Store();
+  }
+
+  /**
+   * @return the store with gadget spec information
+   */
+  public GadgetOAuth2TokenStore getTokenStore() {
+    return this.tokenStore;
+  }
+
+  /**
+   * @return true if the owner pages do not allow user controlled javascript
+   */
+  public boolean isViewerAccessTokensEnabled() {
+    return this.viewerAccessTokensEnabled;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2GadgetContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2GadgetContext.java
new file mode 100644
index 0000000..62aec8b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2GadgetContext.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+
+/**
+ * GadgetContext for use when handling an OAuth2 request.
+ */
+public class OAuth2GadgetContext extends GadgetContext {
+
+  private final Uri appUrl;
+  private final boolean bypassSpecCache;
+  private final String container;
+  private final String scope;
+  private final SecurityToken securityToken;
+
+  public OAuth2GadgetContext(final SecurityToken securityToken, final OAuth2Arguments arguments,
+      final Uri gadgetUri) {
+    this.securityToken = securityToken;
+    this.container = securityToken.getContainer();
+    this.appUrl = gadgetUri;
+    this.bypassSpecCache = arguments.getBypassSpecCache();
+    this.scope = arguments.getScope();
+  }
+
+  @Override
+  public String getContainer() {
+    return this.container;
+  }
+
+  @Override
+  public boolean getIgnoreCache() {
+    return this.bypassSpecCache;
+  }
+
+  public String getScope() {
+    return this.scope;
+  }
+
+  @Override
+  public SecurityToken getToken() {
+    return this.securityToken;
+  }
+
+  @Override
+  public Uri getUrl() {
+    return this.appUrl;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Message.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Message.java
new file mode 100644
index 0000000..466e532
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Message.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ *
+ * Interface representing an OAuth2Message parser that is injected into the {@link OAuth2Request}
+ * layer.
+ *
+ * It also contains the OAuth 2.0 constants.
+ *
+ * With the simplicity of the OAuth 2.0 client it is unlikely that another version of this class
+ * will need to be injected, but it can be with
+ * <code>com.google.inject.Provider<OAuth2Message></code>
+ *
+ */
+public interface OAuth2Message {
+  String ACCESS_DENIED = "access_denied";
+  String ACCESS_TOKEN = "access_token";
+  String AUTHORIZATION = "code";
+  String AUTHORIZATION_CODE = "authorization_code";
+  String AUTHORIZATION_HEADER = "Authorization";
+  String BASIC_AUTH_TYPE = "Basic";
+  String BEARER_TOKEN_TYPE = "Bearer";
+  String BODYHASH = "bodyhash";
+  String CLIENT_CREDENTIALS = "client_credentials";
+  String CLIENT_ID = "client_id";
+  String CLIENT_SECRET = "client_secret";
+  String CONFIDENTIAL_CLIENT_TYPE = "confidential";
+  String ERROR = "error";
+  String ERROR_DESCRIPTION = "error_description";
+  String ERROR_URI = "error_uri";
+  String EXPIRES_IN = "expires_in";
+  String GRANT_TYPE = "grant_type";
+  String HMAC_SHA_1 = "hmac-sha-1";
+  String HMAC_SHA_256 = "hmac-sha-256";
+  String ID = "id";
+  String INVALID_CLIENT = "invalid_client";
+  String INVALID_GRANT = "invalid_grant";
+  String INVALID_REQUEST = "invalid_request";
+  String INVALID_SCOPE = "invalid_scope";
+  String MAC = "mac";
+  String MAC_ALGORITHM = "algorithm";
+  String MAC_EXT = "ext";
+  String MAC_HEADER = "MAC";
+  String MAC_SECRET = "secret";
+  String MAC_TOKEN_TYPE = "mac";
+  String NO_GRANT_TYPE = "NONE";
+  String NONCE = "nonce";
+  String PUBLIC_CLIENT_TYPE = "public";
+  String REDIRECT_URI = "redirect_uri";
+  String REFRESH_TOKEN = "refresh_token";
+  String RESPONSE_TYPE = "response_type";
+  String SCOPE = "scope";
+  String SERVER_ERROR = "server_error";
+  String SHARED_TOKEN = "sharedToken";
+  String STANDARD_AUTH_TYPE = "STANDARD";
+  String STATE = "state";
+  String TEMPORARILY_UNAVAILABLE = "temporarily_unavailable";
+  String TOKEN_RESPONSE = "token";
+  String TOKEN_TYPE = "token_type";
+  String UNAUTHORIZED_CLIENT = "authorized_client";
+  String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
+  String UNSUPPORTED_RESPONSE_TYPE = "unsupported_response_type";
+
+  /**
+   * After a message is parsed it may contain an access token.
+   *
+   * @return the access_token in the message
+   */
+  String getAccessToken();
+
+  /**
+   * If this is an Authorization Code flow this method will return the authorization_code from the
+   * message.
+   *
+   * @return authorization_code in the message
+   */
+  String getAuthorization();
+
+  /**
+   * <code>null</code> error indicates the message parsed cleanly and the service provider did not
+   * return an error.
+   *
+   * @return the error from the service provider
+   */
+  OAuth2Error getError();
+
+  /**
+   *
+   * @return the optional error_description from the service provider
+   */
+  String getErrorDescription();
+
+  /**
+   *
+   * @return the optional error_uri from the service provider
+   */
+  String getErrorUri();
+
+  /**
+   *
+   * @return "expires_in" parameter in the message
+   */
+  String getExpiresIn();
+
+  /**
+   * The MAC Algorithm http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05#section-5
+   *
+   * @return
+   */
+  String getMacAlgorithm();
+
+  /**
+   * The MAC Secret http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05#section-5
+   *
+   * @return
+   */
+  String getMacSecret();
+
+  /**
+   *
+   * @return a general {@link Map} of all parameters in the message
+   */
+  Map<String, String> getParameters();
+
+  /**
+   *
+   * @return the "refresh_token" in the message
+   */
+  String getRefreshToken();
+
+  /**
+   *
+   * @return the optional state string in the message
+   */
+  String getState();
+
+  /**
+   *
+   * @return the "token_type" type in the message
+   */
+  String getTokenType();
+
+  /**
+   * Additional properties that went unparsed (i.e. aren't part of the core OAuth2, Bearer Token or
+   * MAC Token specs.
+   *
+   * @return
+   */
+  Map<String, String> getUnparsedProperties();
+
+  /**
+   * Populates an OAuth2Message from a query fragment. Not very useful in shindig.
+   *
+   * @param fragment
+   */
+  void parseFragment(String fragment);
+
+  /**
+   * Populates an OAuth2Message from a JSON response body.
+   *
+   * @param jsonString
+   *          returned from token endpoint request
+   */
+  void parseJSON(String jsonString);
+
+  /**
+   * Populates an OAuth2Message from a URL query string.
+   *
+   * @param queryString
+   *          from redirect_uri called by servcie provider
+   */
+  void parseQuery(String queryString);
+
+  /**
+   * Populates an OAuth2Message from the entire {@link HttpServletRequest}
+   *
+   *
+   * @param request
+   *          to parse
+   */
+  void parseRequest(HttpServletRequest request);
+
+  /**
+   *
+   * @param error
+   */
+  void setError(OAuth2Error error);
+
+  /**
+   *
+   * @param errorDescription
+   */
+  void setErrorDescription(String errorDescription);
+
+  /**
+   *
+   * @param errorUri
+   */
+  void setErrorUri(String errorUri);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2MessageModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2MessageModule.java
new file mode 100644
index 0000000..fd669a5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2MessageModule.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Injects the default {@link OAuth2Message} implmentation.
+ *
+ */
+public class OAuth2MessageModule extends AbstractModule {
+  public static class OAuth2MessageProvider implements Provider<OAuth2Message> {
+    @Inject
+    public OAuth2MessageProvider() {
+    }
+
+    public OAuth2Message get() {
+      return new BasicOAuth2Message();
+    }
+  }
+
+  @Override
+  protected void configure() {
+    this.bind(OAuth2Message.class).toProvider(OAuth2MessageProvider.class);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Module.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Module.java
new file mode 100644
index 0000000..78302a8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Module.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.oauth2.handler.AuthorizationEndpointResponseHandler;
+import org.apache.shindig.gadgets.oauth2.handler.ClientAuthenticationHandler;
+import org.apache.shindig.gadgets.oauth2.handler.GrantRequestHandler;
+import org.apache.shindig.gadgets.oauth2.handler.ResourceRequestHandler;
+import org.apache.shindig.gadgets.oauth2.handler.TokenEndpointResponseHandler;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Cache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2PersistenceException;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Persister;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.JSONOAuth2Persister;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Level;
+
+/**
+ * Injects the default OAuth2 implementation for {@link BasicOAuth2Request} and
+ * {@link BasicOAuth2Store}
+ *
+ *
+ */
+public class OAuth2Module extends AbstractModule {
+  private static final String CLASS_NAME = OAuth2Module.class.getName();
+  static final FilteredLogger LOG = FilteredLogger.getFilteredLogger(OAuth2Module.CLASS_NAME);
+
+  public static final String OAUTH2_IMPORT = "shindig.oauth2.import";
+  public static final String OAUTH2_IMPORT_CLEAN = "shindig.oauth2.import.clean";
+  public static final String OAUTH2_REDIRECT_URI = "shindig.oauth2.global-redirect-uri";
+  public static final String SEND_TRACE_TO_CLIENT = "shindig.oauth2.send-trace-to-client";
+
+  public static class OAuth2RequestProvider implements Provider<OAuth2Request> {
+    private final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers;
+    private final List<ClientAuthenticationHandler> clientAuthenticationHandlers;
+    private final OAuth2FetcherConfig config;
+    private final HttpFetcher fetcher;
+    private final List<GrantRequestHandler> grantRequestHandlers;
+    private final List<ResourceRequestHandler> resourceRequestHandlers;
+    private final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers;
+    private final boolean sendTraceToClient;
+    private final OAuth2RequestParameterGenerator requestParameterGenerator;
+
+    @Inject
+    public OAuth2RequestProvider(final OAuth2FetcherConfig config, final HttpFetcher fetcher,
+            final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers,
+            final List<ClientAuthenticationHandler> clientAuthenticationHandlers,
+            final List<GrantRequestHandler> grantRequestHandlers,
+            final List<ResourceRequestHandler> resourceRequestHandlers,
+            final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers,
+            @Named(OAuth2Module.SEND_TRACE_TO_CLIENT)
+            final boolean sendTraceToClient,
+            final OAuth2RequestParameterGenerator requestParameterGenerator) {
+      this.config = config;
+      this.fetcher = fetcher;
+      this.authorizationEndpointResponseHandlers = authorizationEndpointResponseHandlers;
+      this.clientAuthenticationHandlers = clientAuthenticationHandlers;
+      this.grantRequestHandlers = grantRequestHandlers;
+      this.resourceRequestHandlers = resourceRequestHandlers;
+      this.tokenEndpointResponseHandlers = tokenEndpointResponseHandlers;
+      this.sendTraceToClient = sendTraceToClient;
+      this.requestParameterGenerator = requestParameterGenerator;
+    }
+
+    public OAuth2Request get() {
+      return new BasicOAuth2Request(this.config, this.fetcher,
+              this.authorizationEndpointResponseHandlers, this.clientAuthenticationHandlers,
+              this.grantRequestHandlers, this.resourceRequestHandlers,
+              this.tokenEndpointResponseHandlers, this.sendTraceToClient,
+              this.requestParameterGenerator);
+    }
+  }
+
+  @Singleton
+  public static class OAuth2StoreProvider implements Provider<OAuth2Store> {
+
+    private final BasicOAuth2Store store;
+
+    @Inject
+    public OAuth2StoreProvider(
+        @Named(OAuth2Module.OAUTH2_REDIRECT_URI) final String globalRedirectUri,
+        @Named(OAuth2Module.OAUTH2_IMPORT) final boolean importFromConfig,
+        @Named(OAuth2Module.OAUTH2_IMPORT_CLEAN) final boolean importClean,
+        final Authority authority, final OAuth2Cache cache, final OAuth2Persister persister,
+        final OAuth2Encrypter encrypter,
+        @Nullable @Named("shindig.contextroot") final String contextRoot,
+        @Named(OAuth2FetcherConfig.OAUTH2_STATE_CRYPTER) final BlobCrypter stateCrypter) {
+
+      this.store = new BasicOAuth2Store(cache, persister, encrypter, globalRedirectUri, authority,
+              contextRoot, stateCrypter);
+
+      if (importFromConfig) {
+        try {
+          final OAuth2Persister source = new JSONOAuth2Persister(encrypter, authority,
+                  globalRedirectUri, contextRoot);
+          BasicOAuth2Store.runImport(source, persister, importClean);
+        } catch (final OAuth2PersistenceException e) {
+          if (OAuth2Module.LOG.isLoggable()) {
+            OAuth2Module.LOG.log("store init exception", e);
+          }
+        }
+      }
+
+      try {
+        this.store.init();
+      } catch (final GadgetException e) {
+        if (OAuth2Module.LOG.isLoggable()) {
+          OAuth2Module.LOG.log("store init exception", e);
+        }
+      }
+    }
+
+    public OAuth2Store get() {
+      return this.store;
+    }
+  }
+
+  @Singleton
+  public static class OAuth2CrypterProvider implements Provider<BlobCrypter> {
+
+    private final BlobCrypter crypter;
+
+    @Inject
+    public OAuth2CrypterProvider(@Named("shindig.signing.oauth2.state-key")
+    final String stateCrypterPath) throws IOException {
+      if (StringUtils.isBlank(stateCrypterPath)) {
+        OAuth2Module.LOG.log(Level.INFO,
+                "Using random key for OAuth2 client-side state encryption", new Object[] {});
+        if (OAuth2Module.LOG.isLoggable(Level.INFO)) {
+          OAuth2Module.LOG.log(Level.INFO, "OAuth2CrypterProvider constructor",
+                  MessageKeys.USING_RANDOM_KEY);
+        }
+        this.crypter = new BasicBlobCrypter(
+                Crypto.getRandomBytes(BasicBlobCrypter.MASTER_KEY_MIN_LEN));
+      } else {
+        if (OAuth2Module.LOG.isLoggable(Level.INFO)) {
+          OAuth2Module.LOG.log(Level.INFO, "OAuth2CrypterProvider constructor",
+                  new Object[] { stateCrypterPath });
+        }
+        this.crypter = new BasicBlobCrypter(new File(stateCrypterPath));
+      }
+    }
+
+    public BlobCrypter get() {
+      return this.crypter;
+    }
+  }
+
+  @Override
+  protected void configure() {
+    this.bind(OAuth2Store.class).toProvider(OAuth2StoreProvider.class);
+    this.bind(OAuth2Request.class).toProvider(OAuth2RequestProvider.class);
+    this.bind(OAuth2RequestParameterGenerator.class).to(BasicOAuth2RequestParameterGenerator.class);
+    // Used for encrypting client-side OAuth2 state.
+    this.bind(BlobCrypter.class)
+            .annotatedWith(Names.named(OAuth2FetcherConfig.OAUTH2_STATE_CRYPTER))
+            .toProvider(OAuth2CrypterProvider.class);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Request.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Request.java
new file mode 100644
index 0000000..be62246
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Request.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+/**
+ * Implements OAuth2 fetch for gadgets.
+ *
+ * OAuth 2.0 authorization_code flows will redirects the user to the OAuth 2.0
+ * service provider site to obtain the user's permission to access their data.
+ *
+ * See {@link http://oauth.net/2/}.
+ *
+ */
+public interface OAuth2Request {
+  /**
+   * OAuth 2.0 authenticated request
+   *
+   * @param request
+   *          gadget request
+   *
+   * @return the response to send to the client, never <code>null</code>
+   */
+  public HttpResponse fetch(final HttpRequest request);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestException.java
new file mode 100644
index 0000000..63f0cf2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestException.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+// Could probably gain something by making this more granular.
+/**
+ * Thrown by OAuth2 request routines.
+ *
+ */
+public class OAuth2RequestException extends Exception {
+  private static final long serialVersionUID = 7670892831898874835L;
+
+  /**
+   * Error code for the client.
+   */
+  private final OAuth2Error error;
+  private final String errorUri;
+  private final String errorDescription;
+
+  /**
+   * Error text for the client.
+   */
+  private final String errorText;
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   *
+   * @param error
+   *          {@link OAuth2Error} for this error
+   * @param errorText
+   *          String to help elaborate on the cause of this error
+   * @param cause
+   *          {@link Throwable} optional root cause of the error
+   */
+  public OAuth2RequestException(final OAuth2Error error, final String errorText,
+          final Throwable cause) {
+    this(error, errorText, cause, "", "");
+  }
+
+  /**
+   * Create an exception and record information about the exception to be returned to the gadget.
+   *
+   * @param error
+   *          {@link OAuth2Error} for this error
+   * @param errorText
+   *          String to help elaborate on the cause of this error
+   * @param cause
+   *          {@link Throwable} optional root cause of the error
+   * @param errorUri
+   *          optional errorUri from the OAuth2 spec
+   * @param errorDescription
+   *          optionally provide more details about the error
+   */
+  public OAuth2RequestException(final OAuth2Error error, final String errorText,
+          final Throwable cause, final String errorUri, final String errorDescription) {
+    super('[' + error.name() + ',' + String.format(error.toString(), errorText) + ']', cause);
+    this.error = error;
+    this.errorText = error.getErrorDescription(errorText);
+    this.errorUri = errorUri;
+    this.errorDescription = errorDescription;
+  }
+
+  /**
+   * Get the error code
+   *
+   * @return the {@link OAuth2Error}, never <code>null</code>
+   */
+  public OAuth2Error getError() {
+    return this.error;
+  }
+
+  /**
+   * Get a description of the exception
+   *
+   * @return, the error text never <code>null</code>
+   */
+  public String getErrorText() {
+    return this.errorText;
+  }
+
+  @Override
+  public String getMessage() {
+    return this.errorText;
+  }
+
+  /**
+   * Returns the errorUri, if it was provided by the OAuth2 service provider
+   *
+   * @return the errorUri, or "" or <code>null</code>
+   */
+  public String getErrorUri() {
+    return this.errorUri;
+  }
+
+  /**
+   * Returns the more meaningful description of the error
+   *
+   * @return the errorDescription, or "" or <code>null</code>
+   */
+  public String getErrorDescription() {
+    return this.errorDescription;
+  }
+
+  @Override
+  public String toString() {
+    return '[' + this.error.toString() + ',' + this.errorText + ']';
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestParameterGenerator.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestParameterGenerator.java
new file mode 100644
index 0000000..01c1de7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestParameterGenerator.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Enables injection of a new request parameter generator into the system.
+ */
+public interface OAuth2RequestParameterGenerator {
+
+  /**
+   * Generates additional parameters that are added to the request sent to the authorization server
+   *
+   * @return map of additional parameters. this should never be <code>null</code>
+   */
+  public Map<String, String> generateParams(HttpRequest request);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2ResponseParams.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2ResponseParams.java
new file mode 100644
index 0000000..a610243
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2ResponseParams.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.util.List;
+
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.oauth.OAuthResponseParams;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Container for OAuth2 specific data to include in the response to the client.
+ */
+public class OAuth2ResponseParams {
+  public static final String APPROVAL_URL = OAuthResponseParams.APPROVAL_URL;
+  public static final String ERROR_CODE = OAuthResponseParams.ERROR_CODE;
+  public static final String ERROR_TEXT = OAuthResponseParams.ERROR_TEXT;
+  public static final String ERROR_TRACE = "oauthErrorTrace";
+  public static final String ERROR_URI = "oauthErrorUri";
+  public static final String ERROR_EXPLANATION = "oauthErrorExplanation";
+
+  private String authorizationUrl;
+  private String requestTraceString;
+  private String message;
+
+  public OAuth2ResponseParams() {
+  }
+
+  /**
+   * Add a request/response pair to our trace of actions associated with this
+   * request.
+   */
+  public void addRequestTrace(final HttpRequest request, final HttpResponse response) {
+    final List<Pair<HttpRequest, HttpResponse>> requestTrace = Lists.newArrayList();
+    requestTrace.add(Pair.of(request, response));
+    this.requestTraceString = OAuth2ResponseParams.getRequestTrace(requestTrace);
+  }
+
+  public void addDebug(final String debugMessage) {
+    this.message = debugMessage;
+  }
+
+  public void addToResponse(final HttpResponseBuilder responseBuilder, final String errorCode,
+      final String errorDescription, final String errorUri, final String errorExplanation) {
+
+    if (errorCode != null) {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_CODE, errorCode);
+    } else {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_CODE, "");
+    }
+
+    if (errorUri != null) {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_URI, errorUri);
+    } else {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_URI, "");
+    }
+
+    if (errorDescription != null) {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_TEXT, errorDescription);
+    } else {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_TEXT, "");
+    }
+
+    if (errorExplanation != null) {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_EXPLANATION, errorExplanation);
+    } else {
+      responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_EXPLANATION, "");
+    }
+
+    String _message = "\n";
+    if (this.message != null) {
+      _message = _message + this.message + '\n';
+    }
+    if (this.requestTraceString != null) {
+      _message = _message + this.requestTraceString;
+    }
+    responseBuilder.setMetadata(OAuth2ResponseParams.ERROR_TRACE, _message);
+  }
+
+  public String getAuthorizationUrl() {
+    return this.authorizationUrl;
+  }
+
+  public void setAuthorizationUrl(final String authorizationUrl) {
+    this.authorizationUrl = authorizationUrl;
+  }
+
+  private static String getRequestTrace(final List<Pair<HttpRequest, HttpResponse>> requestTrace) {
+    final StringBuilder trace = new StringBuilder();
+    int i = 1;
+    for (final Pair<HttpRequest, HttpResponse> event : requestTrace) {
+      trace.append("\n==== Sent request ").append(i).append(":\n");
+      if (event.one != null) {
+        trace.append(FilteredLogger.filterSecrets(event.one.toString()));
+      }
+      trace.append("\n==== Received response ").append(i).append(":\n");
+      if (event.two != null) {
+        trace.append(FilteredLogger.filterSecrets(event.two.toString()));
+      }
+      trace.append("\n====");
+      ++i;
+    }
+    return trace.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Store.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Store.java
new file mode 100644
index 0000000..61b1cbb
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Store.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+import org.apache.shindig.gadgets.servlet.OAuth2CallbackServlet;
+
+/**
+ * Interface to an OAuth 2.0 Data Store. A shindig gadget server can act as an OAuth 2.0 consumer,
+ * using OAuth 2.0 tokens to talk to OAuth 2.0 service providers on behalf of the gadgets it is
+ * proxying requests for. An OAuth 2.0 consumer needs to permanently store gadgets it has collected,
+ * and retrieve the appropriate tokens when proxying a request for a gadget.
+ *
+ * Access and Refresh {@link OAuth2Token} may be store in memory or pesisted out to a file system or
+ * database.
+ *
+ * OAuth2Store implementors are responsible for handling the gadgeturi, serviceName, user, scope
+ * mappings in the manor most effective for their environment.
+ *
+ * {@link OAuth2Accessor} storage should be cluster safe so it can be referenced by
+ * {@link OAuth2CallbackServlet}
+ */
+public interface OAuth2Store {
+
+  /**
+   * Clears any in-memory caching of OAuth2Accessors or Tokens.
+   *
+   * @return <code>true</code> if the clear succeeded
+   *
+   * @throws GadgetException
+   *           if the clear could not happen
+   */
+  boolean clearCache() throws GadgetException;
+
+  /**
+   * Creates, but does not store, an {@link OAuth2Token}. The token can then be initialized and
+   * stored.
+   *
+   * @return a new {@link OAuth2Token}
+   */
+  OAuth2Token createToken();
+
+  /**
+   * Given an OAuth2CallbackState, see {@link OAuth2Store.getOAuth2CallbackState}, the store will
+   * return the {@link OAuth2Accessor} if it exists in storage but will not create a new one.
+   *
+   * @param state
+   *          {@link OAuth2CallbackState} index of the accessor to get
+   * @return the {@link OAuth2Accessor} or <code>null</code> if it cannot be located
+   */
+  OAuth2Accessor getOAuth2Accessor(OAuth2CallbackState state);
+
+  /**
+   * Will look for an accessor with the supplied mapping and return it. If one is not already stored
+   * a new one will be created and stored.
+   *
+   * @param gadgetUri
+   *          {@link String} URI of the gadget issuing the request
+   * @param serviceName
+   *          {@link String} name of the OAuth2 service from the gadget spec
+   * @param user
+   *          {@link String user} userid of the page viewer
+   * @param scope
+   *          {@link String} optional scope of the request. Supplied by the request or the gadget
+   *          spec
+   * @return the {@link OAuth2Accessor} , never <code>null</code>
+   * @throws GadgetException
+   *           if a lookup or creation error occurs
+   */
+  OAuth2Accessor getOAuth2Accessor(String gadgetUri, String serviceName, String user, String scope)
+          throws GadgetException;
+
+  /**
+   * Gets the OAuth2 state encrypter/decrypter
+   */
+  BlobCrypter getStateCrypter();
+
+  /**
+   * Gets a token, if it exists, from the store.
+   *
+   * @param gadgetUri
+   *          {@link String} URI of the gadget issuing the request
+   * @param serviceName
+   *          {@link String} name of the OAuth2 service from the gadget spec
+   * @param user
+   *          {@link String user} userid of the page viewer
+   * @param scope
+   *          {@link String} optional scope of the request. Supplied by the request or the gadget
+   *          spec
+   * @param type
+   *          {@link Type} if the token, ACCESS or REFRESH
+   * @return the {@link OAuth2Token} for the supplied mapping, <code>null</code> if it isn't stored
+   * @throws GadgetException
+   *           if something goes wrong
+   */
+  OAuth2Token getToken(String gadgetUri, String serviceName, String user, String scope,
+          OAuth2Token.Type type) throws GadgetException;
+
+  /**
+   * Cues the store to clear it's current state and reload from persistence.
+   *
+   * @return
+   * @throws GadgetException
+   */
+  boolean init() throws GadgetException;
+
+  /**
+   * Removes an {@link OAuth2Accessor} from the store.
+   *
+   * @param accessor
+   *          to remove
+   * @return the accessor that was removed, or <code>null</code> if the accessor was already removed
+   */
+  OAuth2Accessor removeOAuth2Accessor(OAuth2Accessor accessor);
+
+  /**
+   * Removes an {@link OAuth2Token} from the store.
+   *
+   * @param token
+   *          to remove
+   * @return the token that was removed, or <code>null</code> if the token was already removed\
+   * @throws GadgetException
+   *           if something goes wrong
+   */
+  OAuth2Token removeToken(OAuth2Token token) throws GadgetException;
+
+  /**
+   * Either inserts updates an {@link OAuth2Token} in the store.
+   *
+   * @param token
+   *          to store
+   * @throws GadgetException
+   *           if something goes wrong
+   */
+  void setToken(OAuth2Token token) throws GadgetException;
+
+  /**
+   * Either inserts updates an {@link OAuth2Accessor} in the store.
+   *
+   * @param accessor
+   *          to store
+   */
+  void storeOAuth2Accessor(OAuth2Accessor accessor);
+
+  /**
+   * Invalidate a cached client and force it to be reloaded from persistence.
+   *
+   * @param client
+   *          to be invalidated
+   *
+   * @return the client that was invalidated, or <code>null></code> if client could not be found
+   */
+  OAuth2Client invalidateClient(OAuth2Client client);
+
+  /**
+   * Invalidate a cached token and force it to be reloaded from persistence.
+   *
+   * @param token
+   *          to be invalidated
+   *
+   * @return the token that was invalidated, or <code>null</code> if token could not be found
+   */
+  OAuth2Token invalidateToken(OAuth2Token token);
+
+  /**
+   * Clears all currently cached {@link OAuth2Accessor}s.
+   */
+  void clearAccessorCache() throws GadgetException;
+
+  /**
+   * Clears all currently cached {@link OAuth2Token}s. Does not remove them from persistence.
+   */
+  void clearTokenCache() throws GadgetException;
+
+  /**
+   * Clears all currently cache {@link OAuth2Client}s. Does not remove the from persistence.
+   */
+  void clearClientCache() throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Token.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Token.java
new file mode 100644
index 0000000..a910a2c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Token.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * Contains all relevant data for a token.
+ *
+ * OAuth2Token implementations should be {@link Serializable} to facilitate cluster storage and
+ * caching across the various phases of OAuth 2.0 flows.
+ *
+ * OAuth2Tokens are stored in the {@link OAuth2Store}, they may be held in memory or in another
+ * persistence layer.
+ *
+ */
+public interface OAuth2Token extends Serializable {
+  public enum Type {
+    ACCESS, REFRESH
+  }
+
+  /**
+   * Used for creating MAC token nonces
+   *
+   * @return the time (in milliseconds) when the token was issued
+   */
+  long getIssuedAt();
+
+  /**
+   * issuedAt + expires_in or 0 if no expires_in was sent by server
+   *
+   * @return the time (in milliseconds) when the token expires
+   */
+  long getExpiresAt();
+
+  /**
+   *
+   * @return uri of the gadget the token applies to
+   */
+  String getGadgetUri();
+
+  /**
+   * For use with the MAC token specification.
+   *
+   * See See http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+   *
+   * @return the Mac algorithm
+   */
+  String getMacAlgorithm();
+
+  /**
+   * For use with the MAC token specification.
+   *
+   * See See http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+   *
+   * @return the mac ext
+   */
+  String getMacExt();
+
+  /**
+   * For use with the MAC token specification.
+   *
+   * See See http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+   *
+   * @return the mac secret
+   */
+  byte[] getMacSecret();
+
+  /**
+   * Contains any additional properties sent on the token.
+   *
+   * @return properties sent on the token
+   */
+  Map<String, String> getProperties();
+
+  /**
+   * See {@link http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-3.3}
+   *
+   * @return scope the token applies to, or "" for no scope
+   */
+  String getScope();
+
+  /**
+   *
+   * @return the token secret (unencrypted or signed)
+   */
+  byte[] getSecret();
+
+  /**
+   *
+   * @return serviceName (in gadget spec) the token applies to
+   */
+  String getServiceName();
+
+  /**
+   *
+   * @return type of this token e.g. "Bearer"
+   */
+  String getTokenType();
+
+  /**
+   *
+   * @return if this is an Type.ACCESS or Type.REFRESH token
+   */
+  Type getType();
+
+  /**
+   *
+   * @return shindig user the token was issued for
+   */
+  String getUser();
+
+  /**
+   * Setter for expiresAt field
+   *
+   * @param expiresIn
+   */
+  void setExpiresAt(long expiresAt);
+
+  /**
+   * Setter for gadgetUri field
+   *
+   * @param gadgetUri
+   */
+  void setGadgetUri(String gadgetUri);
+
+  /**
+   * Setter for issuedAt field
+   *
+   * @param expiresIn
+   */
+  void setIssuedAt(long issuedAt);
+
+  /**
+   * For use with the MAC token specification.
+   *
+   * See See http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+   *
+   */
+  void setMacAlgorithm(final String algorithm);
+
+  /**
+   * For use with the MAC token specification.
+   *
+   * See See http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+   *
+   */
+  void setMacSecret(final byte[] secret) throws OAuth2RequestException;
+
+  /**
+   * Set the properties on the token
+   *
+   */
+  void setProperties(Map<String, String> properties);
+
+  /**
+   * Setter for scope field
+   *
+   */
+  void setScope(String scope);
+
+  /**
+   * Setter for secret property
+   *
+   * @param secret
+   * @throws OAuth2RequestException
+   */
+  void setSecret(byte[] secret) throws OAuth2RequestException;
+
+  /**
+   * Setter for serviceName field
+   *
+   * @param serviceName
+   */
+  void setServiceName(String serviceName);
+
+  /**
+   * Setter for tokenType property
+   *
+   * @param tokenType
+   */
+  void setTokenType(String tokenType);
+
+  /**
+   * Setter for type property
+   *
+   * @param type
+   */
+  void setType(Type type);
+
+  /**
+   * Setter for user property
+   *
+   * @param user
+   */
+  void setUser(String user);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Utils.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Utils.java
new file mode 100644
index 0000000..f2bfe40
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/OAuth2Utils.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeSet;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Some common OAuth2 related utility methods
+ *
+ */
+public class OAuth2Utils {
+  private final static String LOG_CLASS = OAuth2Utils.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger.getFilteredLogger(OAuth2Utils.LOG_CLASS);
+
+  /**
+   * Normalizes a URL and parameters. If the URL already contains parameters,
+   * new parameters will be added properly.
+   *
+   * @param url2
+   *          is the base URL to normalize
+   * @param queryParams
+   *          query parameters to add to the URL
+   * @param fragmentParams
+   *          fragment params to add to the URL
+   * @return normalized url with parameter
+   */
+  public static String buildUrl(final String url2, final Map<String, String> queryParams,
+      final Map<String, String> fragmentParams) {
+    // Get any existing params
+    String url = url2;
+    if (url.endsWith("/")) {
+      url = url.substring(0, url.length() - 1);
+    }
+    final Uri uri = Uri.parse(url);
+    final Map<String, List<String>> existingQueryParams = uri.getQueryParameters();
+    final Map<String, List<String>> existingFragmentParams = uri.getFragmentParameters();
+    final int index = url.indexOf('?');
+    String urlNoParams = url;
+    if (index >= 0) {
+      urlNoParams = urlNoParams.substring(0, index);
+    }
+
+    final Map<String, String> queryParams2 = Maps.newHashMapWithExpectedSize(5);
+    if ((existingQueryParams != null) && !existingQueryParams.isEmpty()) {
+      for (final Entry<String, List<String>> entry : existingQueryParams.entrySet()) {
+        queryParams2.put(entry.getKey(), entry.getValue().get(0));
+      }
+    }
+
+    final Map<String, String> fragmentParams2 = Maps.newHashMapWithExpectedSize(5);
+    if ((existingFragmentParams != null) && !existingFragmentParams.isEmpty()) {
+      for (final Entry<String, List<String>> entry : existingFragmentParams.entrySet()) {
+        fragmentParams2.put(entry.getKey(), entry.getValue().get(0));
+      }
+    }
+
+    if (queryParams != null) {
+      queryParams2.putAll(queryParams);
+    }
+    if (fragmentParams != null) {
+      fragmentParams2.putAll(fragmentParams);
+    }
+
+    final StringBuilder buff = new StringBuilder(urlNoParams);
+    if ((queryParams != null) && !queryParams.isEmpty()) {
+      if (urlNoParams.contains("?")) {
+        buff.append('&');
+      } else {
+        buff.append('?');
+      }
+      buff.append(OAuth2Utils.convertQueryString(queryParams2));
+    }
+    if ((fragmentParams != null) && !fragmentParams.isEmpty()) {
+      if (urlNoParams.contains("#")) {
+        buff.append('&');
+      } else {
+        buff.append('#');
+      }
+      buff.append(OAuth2Utils.convertQueryString(fragmentParams2));
+    }
+    return buff.toString();
+  }
+
+  /**
+   * Converts a Map<String, String> to a URL query string.
+   *
+   * @param params
+   *          represents the Map of query parameters
+   *
+   * @return String is the URL encoded parameter String
+   */
+  public static String convertQueryString(final Map<String, String> params) {
+    if (params == null) {
+      return "";
+    }
+    final List<NameValuePair> nvp = new ArrayList<NameValuePair>();
+    for (final String key : new TreeSet<String>(params.keySet())) {
+      if (params.get(key) != null) {
+        nvp.add(new BasicNameValuePair(key, params.get(key)));
+      }
+    }
+    return URLEncodedUtils.format(nvp, "UTF-8");
+  }
+
+  /**
+   * Fetch bearer token from http request
+   *
+   * @param req httpServletRequest
+   *
+   * @return String bearer token from the request
+   */
+  public static String fetchBearerTokenFromHttpRequest(final HttpServletRequest req) {
+    String bearerToken = req.getParameter("access_token");
+    if ((bearerToken == null) || bearerToken.equals("")) {
+      final String header = req.getHeader("Authorization");
+      if ((header != null) && header.contains("Bearer")) {
+        final String[] parts = header.split("\\s+");
+        bearerToken = parts[parts.length - 1];
+      }
+    }
+    return bearerToken;
+  }
+
+  /**
+   * Fetch client secret from http request for a given client.
+   *
+   * @param req
+   *          httpServletRequest
+   * @param clientId
+   *          id of the client
+   *
+   * @return String client secret from the request
+   */
+  public static String fetchClientSecretFromHttpRequest(final String clientId,
+      final HttpServletRequest req) {
+    String secret = req.getParameter("client_secret");
+    if ((secret == null) || secret.equals("")) {
+      final String header = req.getHeader("Authorization");
+      if ((header != null) && header.contains("Basic")) {
+        final byte[] decodedSecret = Base64.decodeBase64(secret);
+        try {
+          String temp = new String(decodedSecret, "UTF-8");
+          String[] parts = StringUtils.split(temp, ':');
+          if ((parts != null) && (parts.length == 2) && (parts[0].equals(clientId))) {
+            secret = parts[1];
+          }
+        } catch (final UnsupportedEncodingException e) {
+          if (OAuth2Utils.LOG.isLoggable()) {
+            OAuth2Utils.LOG.log("UnsupportedEncodingException", e);
+          }
+          return null;
+        }
+      }
+    }
+    return secret;
+  }
+
+  /**
+   * Check if the given Uri is in the allowedDomains array.
+   *
+   * @param uri
+   *          The uri
+   * @param allowedDomains
+   *          allowed domains
+   *
+   * @return boolean true if uri is allowed
+   */
+  public static boolean isUriAllowed(final Uri uri, final String[] allowedDomains) {
+    if (allowedDomains == null || allowedDomains.length == 0) {
+      // if white list is not specified, allow client to access any domain
+      return true;
+    }
+    String host = uri.getAuthority();
+    final int pos = host.indexOf(':');
+    if (pos != -1) {
+      host = host.substring(0, pos);
+    }
+    host = host.toLowerCase();
+    for (String domain : allowedDomains) {
+      if (domain != null) {
+        domain = domain.trim();
+        domain = domain.toLowerCase();
+        if (domain.startsWith(".") && host.endsWith(domain) || domain.equalsIgnoreCase(host)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/AuthorizationEndpointResponseHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/AuthorizationEndpointResponseHandler.java
new file mode 100644
index 0000000..a2b75c9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/AuthorizationEndpointResponseHandler.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.servlet.OAuth2CallbackServlet;
+
+/**
+ * When an AuthorizationEndpointResponseHandler is injected into the system it
+ * will be called on every response from the authorization server that it claims
+ * to handle.
+ *
+ * It may not make sense for a single handler to support both requests and
+ * responses but it must support at least one.
+ *
+ * See {@link http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-4}
+ *
+ * By default shindig has handlers for the Authorization Code and Client
+ * Credential flows.
+ *
+ */
+public interface AuthorizationEndpointResponseHandler {
+  /**
+   * Let the handler do it's magic including any accessor/store updates.
+   *
+   *
+   * If the handler is executed and encountered an error that should stop the
+   * authorization process it should return the appropriate
+   * {@link OAuth2HandlerError}.
+   *
+   * Applies in particular to the Authorization Code flow redirect.
+   *
+   * See {@link http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-4.1.2}
+   *
+   * @param accessor
+   * @param response
+   * @return see above
+   */
+  public OAuth2HandlerError handleRequest(OAuth2Accessor accessor, HttpServletRequest request);
+
+  /**
+   * Let the handler do it's magic including any accessor/store updates.
+   *
+   *
+   * If the handler is executed and encountered an error that should stop the
+   * authorization process it should return the appropriate
+   * {@link OAuth2HandlerError}.
+   *
+   *
+   * Applies in particular to the client_credentials flow.
+   *
+   * See {@link http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-4.4.1}
+   *
+   * @param accessor
+   * @param response
+   * @return see above
+   */
+  public OAuth2HandlerError handleResponse(OAuth2Accessor accessor, HttpResponse response);
+
+  /**
+   * Does the handler support this {@link OAuth2Accessor} /
+   * {@link HttpServletRequest} response? The presumably has come from the
+   * {@link OAuth2CallbackServlet} or somewhere similar.
+   *
+   * @param accessor
+   * @param request
+   * @return <code>true</code> if handleRequest() should be invoked
+   */
+  public boolean handlesRequest(OAuth2Accessor accessor, HttpServletRequest request);
+
+  /**
+   * Does the handler support this{@link OAuth2Accessor} / {@link HttpResponse}
+   * response?
+   *
+   * @param accessor
+   * @param response
+   * @return <code>true</code> if handleRequest() should be invoked
+   */
+  public boolean handlesResponse(OAuth2Accessor accessor, HttpResponse response);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/BasicAuthenticationHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/BasicAuthenticationHandler.java
new file mode 100644
index 0000000..a2d20ac
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/BasicAuthenticationHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+
+import org.apache.commons.codec.binary.Base64;
+
+/**
+ *
+ * See {@link ClientAuthenticationHandler}
+ *
+ * Handler for Basic Authentication
+ *
+ */
+public class BasicAuthenticationHandler implements ClientAuthenticationHandler {
+  private static final OAuth2Error ERROR = OAuth2Error.AUTHENTICATION_PROBLEM;
+
+  public OAuth2HandlerError addOAuth2Authentication(final HttpRequest request,
+          final OAuth2Accessor accessor) {
+    try {
+      if (request == null) {
+        return BasicAuthenticationHandler.getError("request is null");
+      }
+
+      if (accessor == null || !accessor.isValid() || accessor.isErrorResponse()) {
+        return BasicAuthenticationHandler.getError("accessor is invalid " + accessor);
+      }
+
+      final String clientId = accessor.getClientId();
+
+      if (clientId == null) {
+        return BasicAuthenticationHandler.getError("client_id is null");
+      }
+
+      final byte[] secretBytes = accessor.getClientSecret();
+
+      if (secretBytes == null) {
+        return BasicAuthenticationHandler.getError("client_secret is secret");
+      }
+
+      final String secret = new String(secretBytes, "UTF-8");
+
+      final String authString = clientId + ':' + secret;
+      final byte[] authBytes = Base64.encodeBase64(authString.getBytes());
+      request.setHeader(OAuth2Message.AUTHORIZATION_HEADER, "Basic: " + new String(authBytes));
+
+      return null;
+    } catch (final Exception e) {
+      return BasicAuthenticationHandler.getError("Exception adding basic auth headers", e);
+    }
+  }
+
+  public String geClientAuthenticationType() {
+    return OAuth2Message.BASIC_AUTH_TYPE;
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage) {
+    return BasicAuthenticationHandler.getError(contextMessage, null);
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage, final Exception e) {
+    return new OAuth2HandlerError(BasicAuthenticationHandler.ERROR, contextMessage, e);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/BearerTokenHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/BearerTokenHandler.java
new file mode 100644
index 0000000..8cd4f32
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/BearerTokenHandler.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+import org.apache.shindig.gadgets.oauth2.OAuth2Utils;
+
+import java.util.Map;
+
+/**
+ *
+ * See {@link ResourceRequestHandler}
+ *
+ * Handles the mac token type
+ */
+public class BearerTokenHandler implements ResourceRequestHandler {
+  public static final String TOKEN_TYPE = OAuth2Message.BEARER_TOKEN_TYPE;
+  private static final OAuth2Error ERROR = OAuth2Error.BEARER_TOKEN_PROBLEM;
+
+  public OAuth2HandlerError addOAuth2Params(final OAuth2Accessor accessor, final HttpRequest request) {
+    try {
+      if (accessor == null || !accessor.isValid() || accessor.isErrorResponse()) {
+        return BearerTokenHandler.getError("accessor is invalid " + accessor);
+      }
+
+      if (request == null) {
+        return BearerTokenHandler.getError("request is null");
+      }
+
+      final Uri unAuthorizedRequestUri = request.getUri();
+
+      if (unAuthorizedRequestUri == null) {
+        return BearerTokenHandler.getError("unAuthorizedRequestUri is null");
+      }
+
+      final OAuth2Token accessToken = accessor.getAccessToken();
+
+      if (accessToken == null || accessToken.getTokenType().length() == 0) {
+        return BearerTokenHandler.getError("accessToken is invalid " + accessToken);
+      }
+
+      if (!BearerTokenHandler.TOKEN_TYPE.equalsIgnoreCase(accessToken.getTokenType())) {
+        return BearerTokenHandler.getError("token type mismatch expected "
+                + BearerTokenHandler.TOKEN_TYPE + " but got " + accessToken.getTokenType());
+      }
+
+      if (accessor.isUrlParameter()) {
+        final Map<String, String> queryParams = Maps.newHashMap();
+        final byte[] secretBytes = accessToken.getSecret();
+        final String secret = new String(secretBytes, "UTF-8");
+        queryParams.put(OAuth2Message.ACCESS_TOKEN, secret);
+        final String authorizedUriString = OAuth2Utils.buildUrl(unAuthorizedRequestUri.toString(),
+                queryParams, null);
+
+        request.setUri(Uri.parse(authorizedUriString));
+      }
+
+      if (accessor.isAuthorizationHeader()) {
+        request.setHeader("Authorization", BearerTokenHandler.TOKEN_TYPE + ' '
+                + new String(accessToken.getSecret(), "UTF-8"));
+      }
+
+      return null;
+    } catch (final Exception e) {
+      return BearerTokenHandler.getError("Exception occurred " + e.getMessage(), e);
+    }
+  }
+
+  public String getTokenType() {
+    return BearerTokenHandler.TOKEN_TYPE;
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage) {
+    return BearerTokenHandler.getError(contextMessage, null);
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage, final Exception e) {
+    return new OAuth2HandlerError(BearerTokenHandler.ERROR, contextMessage, e, "", "");
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ClientAuthenticationHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ClientAuthenticationHandler.java
new file mode 100644
index 0000000..66c5f8f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ClientAuthenticationHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+
+/**
+ * See {@link OAuth2Accessor#getClientAuthenticationType()} See {@link http
+ * ://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-2.3}
+ *
+ * Enables injection of new Client Authentication schemes into the system.
+ *
+ * If a {@link ClientAuthenticationHandler#geClientAuthenticationType()} matches a
+ * {@link OAuth2Accessor#getClientAuthenticationType()} it will be invoked for the outbound request
+ * to the service provider.
+ *
+ * By default "Basic" and "STANDARD" (client_id and client_secret added to request parameters) are
+ * supported.
+ */
+
+public interface ClientAuthenticationHandler {
+  /**
+   * Handler implementation will modify request as necessary.
+   *
+   * @param request
+   * @param accessor
+   * @return indicates failure by returning a {@link OAuth2HandlerError}
+   */
+  OAuth2HandlerError addOAuth2Authentication(HttpRequest request, OAuth2Accessor accessor);
+
+  /**
+   *
+   * @return the Client Authentication type for this handler
+   */
+  String geClientAuthenticationType();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ClientCredentialsGrantTypeHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ClientCredentialsGrantTypeHandler.java
new file mode 100644
index 0000000..307456d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ClientCredentialsGrantTypeHandler.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2RequestException;
+import org.apache.shindig.gadgets.oauth2.OAuth2Utils;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * See {@link GrantRequestHandler}
+ *
+ * Handles the "client_credentials" flow
+ */
+public class ClientCredentialsGrantTypeHandler implements GrantRequestHandler {
+  private static final OAuth2Error ERROR = OAuth2Error.CLIENT_CREDENTIALS_PROBLEM;
+
+  private final List<ClientAuthenticationHandler> clientAuthenticationHandlers;
+
+  @Inject
+  public ClientCredentialsGrantTypeHandler(
+          final List<ClientAuthenticationHandler> clientAuthenticationHandlers) {
+    this.clientAuthenticationHandlers = clientAuthenticationHandlers;
+  }
+
+  private String getAuthorizationBody(final OAuth2Accessor accessor) throws OAuth2RequestException {
+    String ret = "";
+
+    final Map<String, String> queryParams = Maps.newHashMap();
+    queryParams.put(OAuth2Message.GRANT_TYPE, this.getGrantType());
+
+    final String clientId = accessor.getClientId();
+    final byte[] secretBytes = accessor.getClientSecret();
+    String secret;
+    try {
+      secret = new String(secretBytes, "UTF-8");
+    } catch (final UnsupportedEncodingException e) {
+      throw new OAuth2RequestException(OAuth2Error.CLIENT_CREDENTIALS_PROBLEM,
+              "error getting authorization body", e);
+    }
+    queryParams.put(OAuth2Message.CLIENT_ID, clientId);
+    queryParams.put(OAuth2Message.CLIENT_SECRET, secret);
+
+    ret = OAuth2Utils.buildUrl(ret, queryParams, null);
+
+    final char firstChar = ret.charAt(0);
+    if (firstChar == '?' || firstChar == '&') {
+      ret = ret.substring(1);
+    }
+
+    return ret;
+  }
+
+  public HttpRequest getAuthorizationRequest(final OAuth2Accessor accessor,
+          final String completeAuthorizationUrl) throws OAuth2RequestException {
+
+    if (completeAuthorizationUrl == null || completeAuthorizationUrl.length() == 0) {
+      throw new OAuth2RequestException(ClientCredentialsGrantTypeHandler.ERROR,
+              "completeAuthorizationUrl is null", null);
+    }
+
+    if (accessor == null) {
+      throw new OAuth2RequestException(ClientCredentialsGrantTypeHandler.ERROR, "accessor is null",
+              null);
+    }
+
+    if (!accessor.isValid() || accessor.isErrorResponse() || accessor.isRedirecting()) {
+      throw new OAuth2RequestException(ClientCredentialsGrantTypeHandler.ERROR,
+              "accessor is invalid", null);
+    }
+
+    if (!accessor.getGrantType().equalsIgnoreCase(OAuth2Message.CLIENT_CREDENTIALS)) {
+      throw new OAuth2RequestException(ClientCredentialsGrantTypeHandler.ERROR,
+              "grant type is not client_credentials", null);
+    }
+
+    final HttpRequest request = new HttpRequest(Uri.parse(completeAuthorizationUrl));
+    request.setMethod("GET");
+    request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
+    request.setSecurityToken(new AnonymousSecurityToken("", 0L, accessor.getGadgetUri()));
+
+    for (final ClientAuthenticationHandler clientAuthenticationHandler : this.clientAuthenticationHandlers) {
+      if (clientAuthenticationHandler.geClientAuthenticationType().equalsIgnoreCase(
+              accessor.getClientAuthenticationType())) {
+        final OAuth2HandlerError error = clientAuthenticationHandler.addOAuth2Authentication(
+                request, accessor);
+        if (error != null) {
+          throw new OAuth2RequestException(error.getError(), error.getContextMessage(),
+                  error.getCause(), error.getUri(), error.getDescription());
+        }
+      }
+    }
+
+    try {
+      request.setPostBody(this.getAuthorizationBody(accessor).getBytes("UTF-8"));
+    } catch (final UnsupportedEncodingException e) {
+      throw new OAuth2RequestException(OAuth2Error.CLIENT_CREDENTIALS_PROBLEM,
+              "ClientCredentialsGrantTypeHandler - exception setting post body", e);
+    }
+
+    return request;
+  }
+
+  public String getCompleteUrl(final OAuth2Accessor accessor) throws OAuth2RequestException {
+
+    if (accessor == null) {
+      throw new OAuth2RequestException(ClientCredentialsGrantTypeHandler.ERROR, "accessor is null",
+              null);
+    }
+
+    if (!accessor.isValid() || accessor.isErrorResponse() || accessor.isRedirecting()) {
+      throw new OAuth2RequestException(ClientCredentialsGrantTypeHandler.ERROR,
+              "accessor is invalid", null);
+    }
+
+    if (!accessor.getGrantType().equalsIgnoreCase(OAuth2Message.CLIENT_CREDENTIALS)) {
+      throw new OAuth2RequestException(ClientCredentialsGrantTypeHandler.ERROR,
+              "grant type is not client_credentials", null);
+    }
+
+    String ret;
+    try {
+      final Map<String, String> queryParams = Maps.newHashMapWithExpectedSize(4);
+      queryParams.put(OAuth2Message.GRANT_TYPE, this.getGrantType());
+
+      final String clientId = accessor.getClientId();
+      final byte[] secretBytes = accessor.getClientSecret();
+      final String secret = new String(secretBytes, "UTF-8");
+      queryParams.put(OAuth2Message.CLIENT_ID, clientId);
+      queryParams.put(OAuth2Message.CLIENT_SECRET, secret);
+
+      final String scope = accessor.getScope();
+      if (scope != null && scope.length() > 0) {
+        queryParams.put(OAuth2Message.SCOPE, scope);
+      }
+
+      ret = OAuth2Utils.buildUrl(accessor.getTokenUrl(), queryParams, null);
+    } catch (final UnsupportedEncodingException e) {
+      throw new OAuth2RequestException(OAuth2Error.CLIENT_CREDENTIALS_PROBLEM,
+              "problem getting complete url", e);
+    }
+
+    return ret;
+  }
+
+  public String getGrantType() {
+    return OAuth2Message.CLIENT_CREDENTIALS;
+  }
+
+  public boolean isAuthorizationEndpointResponse() {
+    return false;
+  }
+
+  public boolean isRedirectRequired() {
+    return false;
+  }
+
+  public boolean isTokenEndpointResponse() {
+    return true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/CodeAuthorizationResponseHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/CodeAuthorizationResponseHandler.java
new file mode 100644
index 0000000..ede8023
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/CodeAuthorizationResponseHandler.java
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Utils;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ *
+ * See {@link AuthorizationEndpointResponseHandler}
+ *
+ * Handles the "code" flow
+ */
+public class CodeAuthorizationResponseHandler implements AuthorizationEndpointResponseHandler {
+  private static final String LOG_CLASS = CodeAuthorizationResponseHandler.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger
+          .getFilteredLogger(CodeAuthorizationResponseHandler.LOG_CLASS);
+
+  private final List<ClientAuthenticationHandler> clientAuthenticationHandlers;
+  private final HttpFetcher fetcher;
+  private final Provider<OAuth2Message> oauth2MessageProvider;
+  private final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers;
+
+  @Inject
+  public CodeAuthorizationResponseHandler(final Provider<OAuth2Message> oauth2MessageProvider,
+          final List<ClientAuthenticationHandler> clientAuthenticationHandlers,
+          final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers,
+          final HttpFetcher fetcher) {
+    this.oauth2MessageProvider = oauth2MessageProvider;
+    this.clientAuthenticationHandlers = clientAuthenticationHandlers;
+    this.tokenEndpointResponseHandlers = tokenEndpointResponseHandlers;
+    this.fetcher = fetcher;
+
+    if (CodeAuthorizationResponseHandler.LOG.isLoggable()) {
+      CodeAuthorizationResponseHandler.LOG.log("this.oauth2MessageProvider = {0}",
+              this.oauth2MessageProvider);
+      CodeAuthorizationResponseHandler.LOG.log("this.clientAuthenticationHandlers = {0}",
+              this.clientAuthenticationHandlers);
+      CodeAuthorizationResponseHandler.LOG.log("this.tokenEndpointResponseHandlers = {0}",
+              this.tokenEndpointResponseHandlers);
+      CodeAuthorizationResponseHandler.LOG.log("this.fetcher = {0}", this.fetcher);
+    }
+  }
+
+  private static String getAuthorizationBody(final OAuth2Accessor accessor,
+          final String authorizationCode) throws UnsupportedEncodingException {
+    final boolean isLogging = CodeAuthorizationResponseHandler.LOG.isLoggable();
+    if (isLogging) {
+      if (authorizationCode != null) {
+        CodeAuthorizationResponseHandler.LOG.entering(CodeAuthorizationResponseHandler.LOG_CLASS,
+                "getAuthorizationBody", "non-null authorizationCode");
+      } else {
+        CodeAuthorizationResponseHandler.LOG.entering(CodeAuthorizationResponseHandler.LOG_CLASS,
+                "getAuthorizationBody", null);
+      }
+    }
+
+    String ret = "";
+
+    final Map<String, String> queryParams = Maps.newHashMapWithExpectedSize(5);
+    queryParams.put(OAuth2Message.GRANT_TYPE, OAuth2Message.AUTHORIZATION_CODE);
+    if (authorizationCode != null) {
+      queryParams.put(OAuth2Message.AUTHORIZATION, authorizationCode);
+    }
+    queryParams.put(OAuth2Message.REDIRECT_URI, accessor.getRedirectUri());
+
+    final String clientId = accessor.getClientId();
+    final byte[] secretBytes = accessor.getClientSecret();
+    final String secret = new String(secretBytes, "UTF-8");
+    queryParams.put(OAuth2Message.CLIENT_ID, clientId);
+    queryParams.put(OAuth2Message.CLIENT_SECRET, secret);
+
+    // add any additional parameters
+    for (final Map.Entry<String, String> entry : accessor.getAdditionalRequestParams().entrySet()) {
+      queryParams.put(entry.getKey(), entry.getValue());
+    }
+
+    ret = OAuth2Utils.buildUrl(ret, queryParams, null);
+
+    final char firstChar = ret.charAt(0);
+    if (firstChar == '?' || firstChar == '&') {
+      ret = ret.substring(1);
+    }
+
+    if (isLogging) {
+      CodeAuthorizationResponseHandler.LOG.exiting(CodeAuthorizationResponseHandler.LOG_CLASS,
+              "getAuthorizationBody");
+    }
+    return ret;
+  }
+
+  private static String getCompleteTokenUrl(final String accessTokenUrl) {
+    return OAuth2Utils.buildUrl(accessTokenUrl, null, null);
+  }
+
+  public OAuth2HandlerError handleRequest(final OAuth2Accessor accessor,
+          final HttpServletRequest request) {
+    final boolean isLogging = CodeAuthorizationResponseHandler.LOG.isLoggable();
+    if (isLogging) {
+      CodeAuthorizationResponseHandler.LOG.entering(CodeAuthorizationResponseHandler.LOG_CLASS,
+              "handleRequest", new Object[] { accessor, request != null });
+    }
+
+    OAuth2HandlerError ret = null;
+
+    if (accessor == null) {
+      ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM, "accessor is null", null);
+    } else if (request == null) {
+      ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM, "request is null", null);
+    } else if (!accessor.isValid() || accessor.isErrorResponse() || !accessor.isRedirecting()) {
+      ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM, "accessor is invalid",
+              null);
+    } else if (!accessor.getGrantType().equalsIgnoreCase(OAuth2Message.AUTHORIZATION)) {
+      ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM,
+              "grant_type is not code", null);
+    }
+
+    if (ret == null) {
+      try {
+        final OAuth2Message msg = this.oauth2MessageProvider.get();
+        msg.parseRequest(request);
+        if (msg.getError() != null) {
+          ret = new OAuth2HandlerError(msg.getError(), "error parsing authorization response",
+                  null, msg.getErrorUri(), msg.getErrorDescription());
+        } else {
+          ret = this.setAuthorizationCode(msg.getAuthorization(), accessor);
+        }
+      } catch (final Exception e) {
+        if (CodeAuthorizationResponseHandler.LOG.isLoggable()) {
+          CodeAuthorizationResponseHandler.LOG.log(
+                  "Exception exchanging authorization code for access_token", e);
+        }
+        ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM,
+                "Exception exchanging authorization code for access_token", e);
+      }
+    }
+
+    if (isLogging) {
+      CodeAuthorizationResponseHandler.LOG.exiting(CodeAuthorizationResponseHandler.LOG_CLASS,
+              "handleRequest", ret);
+    }
+
+    return ret;
+  }
+
+  public OAuth2HandlerError handleResponse(final OAuth2Accessor accessor,
+          final HttpResponse response) {
+    return new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM,
+            "doesn't handle responses", null);
+  }
+
+  public boolean handlesRequest(final OAuth2Accessor accessor, final HttpServletRequest request) {
+
+    if (accessor == null) {
+      return false;
+    } else if (request == null) {
+      return false;
+    } else if (!accessor.isValid() || accessor.isErrorResponse() || !accessor.isRedirecting()) {
+      return false;
+    } else if (!accessor.getGrantType().equalsIgnoreCase(OAuth2Message.AUTHORIZATION)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  public boolean handlesResponse(final OAuth2Accessor accessor, final HttpResponse response) {
+    return false;
+  }
+
+  private OAuth2HandlerError setAuthorizationCode(final String authorizationCode,
+          final OAuth2Accessor accessor) {
+
+    final boolean isLogging = CodeAuthorizationResponseHandler.LOG.isLoggable();
+    if (isLogging) {
+      if (authorizationCode != null) {
+        CodeAuthorizationResponseHandler.LOG.entering(CodeAuthorizationResponseHandler.LOG_CLASS,
+                "setAuthorizationCode", new Object[] { "non-null authorizationCode", accessor });
+      } else {
+        CodeAuthorizationResponseHandler.LOG.entering(CodeAuthorizationResponseHandler.LOG_CLASS,
+                "setAuthorizationCode", new Object[] { null, accessor });
+      }
+    }
+
+    OAuth2HandlerError ret = null;
+
+    final String tokenUrl = CodeAuthorizationResponseHandler.getCompleteTokenUrl(accessor
+            .getTokenUrl());
+
+    final HttpRequest request = new HttpRequest(Uri.parse(tokenUrl));
+    request.setMethod("POST");
+    request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
+    request.setSecurityToken(new AnonymousSecurityToken("", 0L, accessor.getGadgetUri()));
+
+    if (!OAuth2Utils.isUriAllowed(request.getUri(), accessor.getAllowedDomains())) {
+      ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM,
+              "Exception exchanging authorization code for access_token - domain not allowed", null);
+    }
+
+    if (ret == null) {
+      for (final ClientAuthenticationHandler clientAuthenticationHandler : this.clientAuthenticationHandlers) {
+        if (clientAuthenticationHandler.geClientAuthenticationType().equalsIgnoreCase(
+                accessor.getClientAuthenticationType())) {
+          final OAuth2HandlerError error = clientAuthenticationHandler.addOAuth2Authentication(
+                  request, accessor);
+          if (error != null) {
+            ret = error;
+          }
+        }
+      }
+    }
+
+    if (ret == null) {
+      try {
+        final byte[] body = CodeAuthorizationResponseHandler.getAuthorizationBody(accessor,
+                authorizationCode).getBytes("UTF-8");
+        request.setPostBody(body);
+      } catch (final UnsupportedEncodingException e) {
+        if (CodeAuthorizationResponseHandler.LOG.isLoggable()) {
+          CodeAuthorizationResponseHandler.LOG.log(
+                  "UnsupportedEncodingException getting authorization body", e);
+        }
+        ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM,
+                "error getting authorization body", e);
+      }
+
+      HttpResponse response = null;
+      try {
+        response = this.fetcher.fetch(request);
+      } catch (final GadgetException e) {
+        if (CodeAuthorizationResponseHandler.LOG.isLoggable()) {
+          CodeAuthorizationResponseHandler.LOG.log("error exchanging code for access_token", e);
+        }
+        ret = new OAuth2HandlerError(OAuth2Error.AUTHORIZATION_CODE_PROBLEM,
+                "error exchanging code for access_token", e);
+      }
+
+      if (ret == null && response != null) {
+        if (response.getHttpStatusCode() != HttpResponse.SC_OK) {
+          final OAuth2Message msg = this.oauth2MessageProvider.get();
+          msg.parseJSON(response.getResponseAsString());
+          if (msg.getError() != null) {
+            ret = new OAuth2HandlerError(msg.getError(), "error exchanging code for access_token",
+                    null, msg.getErrorUri(), msg.getErrorDescription());
+          }
+        }
+
+        if (ret == null) {
+          for (final TokenEndpointResponseHandler tokenEndpointResponseHandler : this.tokenEndpointResponseHandlers) {
+            if (tokenEndpointResponseHandler.handlesResponse(accessor, response)) {
+              ret = tokenEndpointResponseHandler.handleResponse(accessor, response);
+              if (ret != null) {
+                // error occurred stop processing
+                break;
+              }
+            }
+          }
+        }
+      }
+    }
+
+    if (isLogging) {
+      CodeAuthorizationResponseHandler.LOG.exiting(CodeAuthorizationResponseHandler.LOG_CLASS,
+              "setAuthorizationCode", ret);
+    }
+
+    return ret;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/CodeGrantTypeHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/CodeGrantTypeHandler.java
new file mode 100644
index 0000000..da9f16a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/CodeGrantTypeHandler.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.crypto.BlobCrypterException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2CallbackState;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2RequestException;
+import org.apache.shindig.gadgets.oauth2.OAuth2Utils;
+
+import java.util.Map;
+
+/**
+ *
+ * See {@link GrantRequestHandler}
+ *
+ * Handles the "authorization_code" flow
+ */
+public class CodeGrantTypeHandler implements GrantRequestHandler {
+  private static final OAuth2Error ERROR = OAuth2Error.CODE_GRANT_PROBLEM;
+
+  public HttpRequest getAuthorizationRequest(final OAuth2Accessor accessor,
+          final String completeAuthorizationUrl) throws OAuth2RequestException {
+    throw new OAuth2RequestException(CodeGrantTypeHandler.ERROR,
+            "inappropriate call to CodeGrantTypeHandler.getAuthorizationRequest()", null);
+  }
+
+  public String getCompleteUrl(final OAuth2Accessor accessor) throws OAuth2RequestException {
+    if (accessor == null) {
+      throw new OAuth2RequestException(CodeGrantTypeHandler.ERROR, "accessor is null", null);
+    }
+
+    if (!accessor.isValid() || accessor.isErrorResponse() || accessor.isRedirecting()) {
+      throw new OAuth2RequestException(CodeGrantTypeHandler.ERROR, "accessor is invalid", null);
+    }
+
+    if (!accessor.getGrantType().equalsIgnoreCase(OAuth2Message.AUTHORIZATION)) {
+      throw new OAuth2RequestException(CodeGrantTypeHandler.ERROR, "grant type is not code", null);
+    }
+
+    final Map<String, String> queryParams = Maps.newHashMapWithExpectedSize(4);
+    queryParams.put(OAuth2Message.RESPONSE_TYPE, this.getGrantType());
+    queryParams.put(OAuth2Message.CLIENT_ID, accessor.getClientId());
+    final String redirectUri = accessor.getRedirectUri();
+    if (redirectUri != null && redirectUri.length() > 0) {
+      queryParams.put(OAuth2Message.REDIRECT_URI, redirectUri);
+    }
+
+    final OAuth2CallbackState state = accessor.getState();
+    if (state != null) {
+      try {
+        queryParams.put(OAuth2Message.STATE, state.getEncryptedState());
+      } catch (final BlobCrypterException e) {
+        throw new OAuth2RequestException(OAuth2Error.CODE_GRANT_PROBLEM, "encryption problem", e);
+      }
+    }
+
+    final String scope = accessor.getScope();
+    if (scope != null && scope.length() > 0) {
+      queryParams.put(OAuth2Message.SCOPE, scope);
+    }
+
+    // add any additional parameters
+    for (final Map.Entry<String, String> entry : accessor.getAdditionalRequestParams().entrySet()) {
+      queryParams.put(entry.getKey(), entry.getValue());
+    }
+
+    return OAuth2Utils.buildUrl(accessor.getAuthorizationUrl(), queryParams, null);
+  }
+
+  public String getGrantType() {
+    return OAuth2Message.AUTHORIZATION;
+  }
+
+  public static String getResponseType() {
+    return OAuth2Message.AUTHORIZATION_CODE;
+  }
+
+  public boolean isAuthorizationEndpointResponse() {
+    return true;
+  }
+
+  public boolean isRedirectRequired() {
+    return true;
+  }
+
+  public boolean isTokenEndpointResponse() {
+    return false;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/GrantRequestHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/GrantRequestHandler.java
new file mode 100644
index 0000000..8c8243b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/GrantRequestHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2RequestException;
+
+/**
+ * Enables injection of new Grant Type schemes into the system.
+ *
+ * If a {@link GrantRequestHandler#getGrantType()} matches a
+ * {@link OAuth2Accessor#getGrantType()} it will be invoked to initiate the
+ * grant request.
+ *
+ * By default "code" and "client_credentials" are supported.
+ *
+ * Only one GrantRequestHandler will be executed (first to match.)
+ *
+ */
+public interface GrantRequestHandler {
+  /**
+   * If {@link #isRedirectRequired()} is false the system will executes this
+   * request.
+   *
+   * @param accessor
+   * @param completeAuthorizationUrl
+   * @return HttpRequest
+   * @throws OAuth2RequestException
+   */
+  public HttpRequest getAuthorizationRequest(OAuth2Accessor accessor,
+      String completeAuthorizationUrl) throws OAuth2RequestException;
+
+  /**
+   * Url to send redirects to.
+   *
+   * @param accessor
+   * @return String complete url
+   * @throws OAuth2RequestException
+   */
+  public String getCompleteUrl(OAuth2Accessor accessor) throws OAuth2RequestException;
+
+  /**
+   *
+   * @return the grant_type this handler initiates
+   */
+  public String getGrantType();
+
+  /**
+   *
+   * @return true if the response is from the authorization endpoint, i.e. code
+   */
+  public boolean isAuthorizationEndpointResponse();
+
+  /**
+   *
+   * @return true to redirect the client to the
+   *         {@link #getCompleteUrl(OAuth2Accessor)}
+   */
+  public boolean isRedirectRequired();
+
+  /**
+   *
+   * @return true if the response is from the token endpoint i.e.
+   *         client_credentials
+   */
+  public boolean isTokenEndpointResponse();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/MacTokenHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/MacTokenHandler.java
new file mode 100644
index 0000000..bfe1496
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/MacTokenHandler.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+
+import org.apache.commons.codec.binary.Base64;
+
+import java.io.UnsupportedEncodingException;
+import java.security.GeneralSecurityException;
+
+/**
+ *
+ * See {@link ResourceRequestHandler}
+ *
+ * Handles the mac token type
+ */
+public class MacTokenHandler implements ResourceRequestHandler {
+  public static final String TOKEN_TYPE = OAuth2Message.MAC_TOKEN_TYPE;
+  private static final OAuth2Error ERROR = OAuth2Error.MAC_TOKEN_PROBLEM;
+
+  public OAuth2HandlerError addOAuth2Params(final OAuth2Accessor accessor, final HttpRequest request) {
+    try {
+      final OAuth2HandlerError handlerError = MacTokenHandler.validateOAuth2Params(accessor,
+              request);
+      if (handlerError != null) {
+        return handlerError;
+      }
+
+      final OAuth2Token accessToken = accessor.getAccessToken();
+
+      String ext = accessToken.getMacExt();
+      if (ext == null || ext.length() == 0) {
+        ext = "";
+      }
+
+      // REQUIRED. The MAC key identifier.
+      final String id = new String(accessToken.getSecret(), "UTF-8");
+
+      // REQUIRED. A unique string generated by the client to allow the
+      // server to verify that a request has never been made before and
+      // helps prevent replay attacks when requests are made over an
+      // insecure channel. The nonce value MUST be unique across all
+      // requests with the same MAC key identifier.
+      // The nonce value MUST consist of the age of the MAC credentials
+      // expressed as the number of seconds since the credentials were
+      // issued to the client, a colon character (%x25), and a unique
+      // string (typically random). The age value MUST be a positive
+      // integer and MUST NOT include leading zeros (e.g.
+      // "000137131200"). For example: "273156:di3hvdf8".
+      // To avoid the need to retain an infinite number of nonce values
+      // for future checks, the server MAY choose to restrict the time
+      // period after which a request with an old age is rejected. If
+      // such a restriction is enforced, the server SHOULD allow for a
+      // sufficiently large window to accommodate network delays which
+      // will affect the credentials issue time used by the client to
+      // calculate the credentials' age.
+      final long currentTime = System.currentTimeMillis() / 1000;
+      final String nonce = Long.toString(currentTime - accessToken.getIssuedAt()) + ':'
+              + String.valueOf(Math.abs(Crypto.RAND.nextLong()));
+
+      // OPTIONAL. The HTTP request payload body hash as described in
+      // Section 3.2.
+
+      String bodyHash = MacTokenHandler.getBodyHash(request, accessToken.getMacSecret(),
+              accessToken.getMacAlgorithm());
+      if (bodyHash == null) {
+        bodyHash = "";
+      }
+
+      // mac
+      // REQUIRED. The HTTP request MAC as described in Section 3.3.
+      final Uri uri = request.getUri();
+
+      String uriString = uri.getPath();
+      if (uri.getQuery() != null) {
+        uriString = uriString + '?' + uri.getQuery();
+      }
+
+      String host = uri.getAuthority();
+      String port = "80";
+      final int index = host.indexOf(':');
+      if (index > 0) {
+        port = host.substring(index + 1);
+        host = host.substring(0, index);
+      } else {
+        final String scheme = uri.getScheme();
+        if ("https".equals(scheme)) {
+          port = "443";
+        }
+      }
+
+      final String mac = MacTokenHandler.getMac(nonce, request.getMethod(), uriString, host, port,
+              bodyHash, ext, accessToken.getMacSecret(), accessToken.getMacAlgorithm());
+
+      final String headerString = buildHeaderString(id, nonce, bodyHash, ext, mac);
+
+      request.setHeader(OAuth2Message.AUTHORIZATION_HEADER, headerString);
+      return null;
+    } catch (final Exception e) {
+      return MacTokenHandler.getError("Exception occurred " + e.getMessage(), e);
+    }
+  }
+
+  private static String buildHeaderString(final String id, final String nonce,
+          final String bodyHash, final String ext, final String mac) {
+    final StringBuilder headerString = new StringBuilder();
+
+    headerString.append(OAuth2Message.MAC_HEADER);
+    headerString.append(" id = \"");
+    headerString.append(id);
+    headerString.append("\",");
+    headerString.append(OAuth2Message.NONCE);
+    headerString.append("=\"");
+    headerString.append(nonce);
+    if (bodyHash.length() > 0) {
+      headerString.append("\",");
+      headerString.append(OAuth2Message.BODYHASH);
+      headerString.append("=\"");
+      headerString.append(bodyHash);
+    }
+    if (ext.length() > 0) {
+      headerString.append("\",");
+      headerString.append(OAuth2Message.MAC_EXT);
+      headerString.append("=\"");
+      headerString.append(ext);
+    }
+    headerString.append("\",");
+    headerString.append(OAuth2Message.MAC);
+    headerString.append("=\"");
+    headerString.append(mac);
+    headerString.append('\"');
+    return headerString.toString();
+  }
+
+  private static OAuth2HandlerError validateOAuth2Params(final OAuth2Accessor accessor,
+          final HttpRequest request) {
+    if (accessor == null || !accessor.isValid() || accessor.isErrorResponse()) {
+      return MacTokenHandler.getError("accessor is invalid " + accessor);
+    }
+
+    if (request == null) {
+      return MacTokenHandler.getError("request is null");
+    }
+
+    final OAuth2Token accessToken = accessor.getAccessToken();
+
+    if (accessToken == null || accessToken.getTokenType().length() == 0) {
+      return MacTokenHandler.getError("accessToken is invalid " + accessToken);
+    }
+
+    if (!MacTokenHandler.TOKEN_TYPE.equalsIgnoreCase(accessToken.getTokenType())) {
+      return MacTokenHandler.getError("token type mismatch expected " + MacTokenHandler.TOKEN_TYPE
+              + " but got " + accessToken.getTokenType());
+    }
+
+    final String algorithm = accessToken.getMacAlgorithm();
+    if (algorithm == null || algorithm.length() == 0) {
+      return MacTokenHandler.getError("invalid mac algorithm " + algorithm);
+    }
+
+    if (!OAuth2Message.HMAC_SHA_1.equalsIgnoreCase(algorithm)) {
+      return MacTokenHandler.getError("unsupported algorithm " + algorithm);
+    }
+
+    final byte[] macSecret = accessToken.getMacSecret();
+    if (macSecret == null) {
+      return MacTokenHandler.getError("mac secret is null");
+    }
+
+    if (macSecret.length == 0) {
+      return MacTokenHandler.getError("invalid mac secret");
+    }
+
+    return null;
+  }
+
+  public String getTokenType() {
+    return MacTokenHandler.TOKEN_TYPE;
+  }
+
+  private static String getBodyHash(final HttpRequest request, final byte[] key,
+          final String algorithm) throws UnsupportedEncodingException, GeneralSecurityException {
+    if (request.getPostBodyLength() > 0) {
+      final byte[] text = MacTokenHandler.getBody(request);
+      final byte[] hashed = MacTokenHandler.hash(text, key, algorithm);
+      return new String(hashed, "UTF-8");
+    }
+
+    return "";
+  }
+
+  private static byte[] getBody(final HttpRequest request) throws UnsupportedEncodingException {
+    return request.getPostBodyAsString().getBytes("UTF-8");
+  }
+
+  private static String getMac(final String nonce, final String method, final String uri,
+          final String host, final String port, final String bodyHash, final String ext,
+          final byte[] key, final String algorithm) throws UnsupportedEncodingException,
+          GeneralSecurityException {
+    final StringBuilder normalizedRequestString = MacTokenHandler.getNormalizedRequestString(nonce,
+            method, uri, host, port, bodyHash, ext);
+    final byte[] normalizedRequestBytes = normalizedRequestString.toString().getBytes("UTF-8");
+    final byte[] mac = MacTokenHandler.hash(normalizedRequestBytes, key, algorithm);
+    final byte[] encodedBytes = Base64.encodeBase64(mac);
+    return new String(encodedBytes, "UTF-8");
+  }
+
+  private static byte[] hash(final byte[] text, final byte[] key, final String algorithm)
+          throws GeneralSecurityException {
+    if (OAuth2Message.HMAC_SHA_1.equalsIgnoreCase(algorithm)) {
+      return Crypto.hmacSha1(key, text);
+    }
+
+    return new byte[] {};
+  }
+
+  private static StringBuilder getNormalizedRequestString(final String nonce, final String method,
+          final String uri, final String host, final String port, final String bodyHash,
+          final String ext) {
+    final StringBuilder ret = new StringBuilder();
+    ret.append(nonce);
+    ret.append('\n');
+    ret.append(method);
+    ret.append('\n');
+    ret.append(uri);
+    ret.append('\n');
+    ret.append(host);
+    ret.append('\n');
+    ret.append(port);
+    ret.append('\n');
+    ret.append(bodyHash);
+    ret.append('\n');
+    ret.append(ext);
+    ret.append('\n');
+
+    return ret;
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage) {
+    return MacTokenHandler.getError(contextMessage, null);
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage, final Exception e) {
+    return new OAuth2HandlerError(MacTokenHandler.ERROR, contextMessage, e);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerError.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerError.java
new file mode 100644
index 0000000..97dcebd
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerError.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+
+import java.io.Serializable;
+
+/**
+ * Stores an error in the handler layer.
+ *
+ *
+ */
+public class OAuth2HandlerError implements Serializable {
+  private static final long serialVersionUID = 6533884367169476207L;
+
+  private final OAuth2Error error;
+  private final Exception cause;
+  private final String contextMessage;
+  private final String uri;
+  private final String description;
+
+  public OAuth2HandlerError(final OAuth2Error error, final String contextMessage,
+          final Exception cause) {
+    this(error, contextMessage, cause, "", "");
+  }
+
+  public OAuth2HandlerError(final OAuth2Error error, final String contextMessage,
+          final Exception cause, final String uri, final String description) {
+    this.error = error;
+    this.contextMessage = contextMessage;
+    this.cause = cause;
+    this.uri = uri;
+    this.description = description;
+  }
+
+  /**
+   *
+   * @return the {@link OAuth2Error} associated with this error
+   */
+  public OAuth2Error getError() {
+    return this.error;
+  }
+
+  /**
+   *
+   * @return underlying exception that caused is error or <code>null</code>
+   */
+  public Exception getCause() {
+    return this.cause;
+  }
+
+  /**
+   *
+   * @return non-translated message about the context of this error for debugging purposes
+   */
+  public String getContextMessage() {
+    return this.contextMessage;
+  }
+
+  public String getUri() {
+    return this.uri;
+  }
+
+  public String getDescription() {
+    return this.description;
+  }
+
+  @Override
+  public String toString() {
+    return OAuth2HandlerError.class.getName() + " : " + this.error + " : "
+            + this.getContextMessage() + " : " + this.uri + " : " + this.description + ":"
+            + this.cause;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerModule.java
new file mode 100644
index 0000000..38f4033
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerModule.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import java.util.List;
+
+/**
+ * Injects the default handlers.
+ *
+ */
+public class OAuth2HandlerModule extends AbstractModule {
+  private static final FilteredLogger LOG = FilteredLogger
+          .getFilteredLogger(OAuth2HandlerModule.class.getName());
+
+  @Override
+  protected void configure() {
+    if (OAuth2HandlerModule.LOG.isLoggable()) {
+      OAuth2HandlerModule.LOG.entering(OAuth2HandlerModule.class.getName(), "configure");
+    }
+  }
+
+  @Provides
+  @Singleton
+  public static List<AuthorizationEndpointResponseHandler> provideAuthorizationEndpointResponseHandlers(
+          final CodeAuthorizationResponseHandler codeAuthorizationResponseHandler) {
+    return ImmutableList
+            .of((AuthorizationEndpointResponseHandler) codeAuthorizationResponseHandler);
+  }
+
+  @Provides
+  @Singleton
+  public static List<ClientAuthenticationHandler> provideClientAuthenticationHandlers(
+          final BasicAuthenticationHandler basicAuthenticationHandler,
+          final StandardAuthenticationHandler standardAuthenticationHandler) {
+    return ImmutableList.of(basicAuthenticationHandler, standardAuthenticationHandler);
+  }
+
+  @Provides
+  @Singleton
+  public static List<GrantRequestHandler> provideGrantRequestHandlers(
+          final ClientCredentialsGrantTypeHandler clientCredentialsGrantTypeHandler,
+          final CodeGrantTypeHandler codeGrantTypeHandler) {
+    return ImmutableList.of(clientCredentialsGrantTypeHandler, codeGrantTypeHandler);
+  }
+
+  @Provides
+  @Singleton
+  public static List<TokenEndpointResponseHandler> provideTokenEndpointResponseHandlers(
+          final TokenAuthorizationResponseHandler tokenAuthorizationResponseHandler) {
+    return ImmutableList.of((TokenEndpointResponseHandler) tokenAuthorizationResponseHandler);
+  }
+
+  @Provides
+  @Singleton
+  public static List<ResourceRequestHandler> provideTokenHandlers(
+          final BearerTokenHandler bearerTokenHandler, final MacTokenHandler macTokenHandler) {
+    return ImmutableList.of(bearerTokenHandler, macTokenHandler);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ResourceRequestHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ResourceRequestHandler.java
new file mode 100644
index 0000000..f78c8a5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/ResourceRequestHandler.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+
+/**
+ *
+ * Enables inject of token type handlers to add OAuth2 auth data to resource
+ * requests.
+ *
+ * By default shindig supports "Bearer" token types.
+ *
+ * Matches on {@link OAuth2Token#getTokenType()}
+ *
+ * All matching handlers are executed.
+ *
+ */
+public interface ResourceRequestHandler {
+  /**
+   * Do the handler magic for the token type.
+   *
+   * @param accessor
+   * @param request
+   * @return {@link OAuth2HandlerError} if one occurs
+   */
+  public OAuth2HandlerError addOAuth2Params(final OAuth2Accessor accessor, final HttpRequest request);
+
+  /**
+   *
+   * @return the token type this handler handles
+   */
+  public String getTokenType();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/StandardAuthenticationHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/StandardAuthenticationHandler.java
new file mode 100644
index 0000000..c765252
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/StandardAuthenticationHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+
+/**
+ *
+ * See {@link ClientAuthenticationHandler}
+ *
+ * Handler for Basic Authentication
+ *
+ */
+public class StandardAuthenticationHandler implements ClientAuthenticationHandler {
+  private static final OAuth2Error ERROR = OAuth2Error.AUTHENTICATION_PROBLEM;
+
+  public OAuth2HandlerError addOAuth2Authentication(final HttpRequest request,
+          final OAuth2Accessor accessor) {
+    try {
+      if (request == null) {
+        return StandardAuthenticationHandler.getError("request is null");
+      }
+
+      if (accessor == null) {
+        return StandardAuthenticationHandler.getError("accessor is null");
+      }
+
+      if (!accessor.isValid() || accessor.isErrorResponse()) {
+        return StandardAuthenticationHandler.getError("accessor is invalid");
+      }
+
+      final String clientId = accessor.getClientId();
+
+      if (clientId == null) {
+        return StandardAuthenticationHandler.getError("client_id is null");
+      }
+
+      final byte[] secretBytes = accessor.getClientSecret();
+
+      if (secretBytes == null) {
+        return StandardAuthenticationHandler.getError("client_secret is secret");
+      }
+
+      final String secret = new String(secretBytes, "UTF-8");
+
+      request.setHeader(OAuth2Message.CLIENT_ID, clientId);
+      request.setParam(OAuth2Message.CLIENT_ID, clientId);
+      request.setHeader(OAuth2Message.CLIENT_SECRET, secret);
+      request.setParam(OAuth2Message.CLIENT_SECRET, secret);
+
+      return null;
+    } catch (final Exception e) {
+      return StandardAuthenticationHandler.getError("Exception adding standard auth headers", e);
+    }
+  }
+
+  public String geClientAuthenticationType() {
+    return OAuth2Message.STANDARD_AUTH_TYPE;
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage) {
+    return StandardAuthenticationHandler.getError(contextMessage, null);
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage, final Exception e) {
+    return new OAuth2HandlerError(StandardAuthenticationHandler.ERROR, contextMessage, e);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/TokenAuthorizationResponseHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/TokenAuthorizationResponseHandler.java
new file mode 100644
index 0000000..e2a5600
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/TokenAuthorizationResponseHandler.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Store;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+
+import org.json.JSONObject;
+
+import java.util.Map;
+
+/**
+ *
+ * See {@link TokenEndpointResponseHandler}
+ *
+ * Handles the "client_credentials" flow
+ */
+public class TokenAuthorizationResponseHandler implements TokenEndpointResponseHandler {
+  private static final String LOG_CLASS = CodeAuthorizationResponseHandler.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger
+          .getFilteredLogger(TokenAuthorizationResponseHandler.LOG_CLASS);
+
+  private static final OAuth2Error ERROR = OAuth2Error.TOKEN_RESPONSE_PROBLEM;
+
+  private final Provider<OAuth2Message> oauth2MessageProvider;
+  private final OAuth2Store store;
+
+  @Inject
+  public TokenAuthorizationResponseHandler(final Provider<OAuth2Message> oauth2MessageProvider,
+          final OAuth2Store store) {
+    this.oauth2MessageProvider = oauth2MessageProvider;
+    this.store = store;
+
+    if (TokenAuthorizationResponseHandler.LOG.isLoggable()) {
+      TokenAuthorizationResponseHandler.LOG.log("this.oauth2MessageProvider = {0}",
+              this.oauth2MessageProvider);
+      TokenAuthorizationResponseHandler.LOG.log("this.store = {0}", this.store);
+    }
+  }
+
+  public OAuth2HandlerError handleResponse(final OAuth2Accessor accessor,
+          final HttpResponse response) {
+    final boolean isLogging = TokenAuthorizationResponseHandler.LOG.isLoggable();
+
+    if (isLogging) {
+      if (response != null) {
+        TokenAuthorizationResponseHandler.LOG.entering(TokenAuthorizationResponseHandler.LOG_CLASS,
+                "getAuthorizationBody", new Object[] { accessor, "non-null response" });
+      } else {
+        TokenAuthorizationResponseHandler.LOG.entering(TokenAuthorizationResponseHandler.LOG_CLASS,
+                "getAuthorizationBody", new Object[] { accessor, null });
+      }
+    }
+
+    OAuth2HandlerError ret = null;
+
+    try {
+      if (response == null) {
+        ret = TokenAuthorizationResponseHandler.getError("response is null");
+      }
+
+      if (ret == null && (accessor == null || !accessor.isValid() || accessor.isErrorResponse())) {
+        ret = TokenAuthorizationResponseHandler.getError("accessor is invalid " + accessor);
+      }
+
+      if (ret == null && response != null) {
+        final int responseCode = response.getHttpStatusCode();
+        if (responseCode != HttpResponse.SC_OK) {
+          ret = TokenAuthorizationResponseHandler.getError("can't handle error response code "
+                  + responseCode);
+        }
+
+        if (ret == null) {
+          final long issuedAt = System.currentTimeMillis();
+
+          final String contentType = response.getHeader("Content-Type");
+          final String responseString = response.getResponseAsString();
+          final OAuth2Message msg = this.oauth2MessageProvider.get();
+
+          if (contentType.startsWith("text/plain")) {
+            // Facebook does this
+            msg.parseQuery('?' + responseString);
+          } else if (contentType.startsWith("application/json")) {
+            // Google does this
+            final JSONObject responseJson = new JSONObject(responseString);
+            msg.parseJSON(responseJson.toString());
+          } else {
+            if (isLogging) {
+              TokenAuthorizationResponseHandler.LOG.log("Unhandled Content-Type {0}", contentType);
+              TokenAuthorizationResponseHandler.LOG.exiting(
+                      TokenAuthorizationResponseHandler.LOG_CLASS, "handleResponse", null);
+            }
+            ret = TokenAuthorizationResponseHandler.getError("Unhandled Content-Type "
+                    + contentType);
+          }
+
+          final OAuth2Error error = msg.getError();
+          if (error != null) {
+            ret = getError("error parsing request", null, msg.getErrorUri(),
+                    msg.getErrorDescription());
+          } else if (error == null && accessor != null) {
+            final String accessToken = msg.getAccessToken();
+            final String refreshToken = msg.getRefreshToken();
+            final String expiresIn = msg.getExpiresIn();
+            final String tokenType = msg.getTokenType();
+            final String providerName = accessor.getServiceName();
+            final String gadgetUri = accessor.getGadgetUri();
+            final String scope = accessor.getScope();
+            final String user = accessor.getUser();
+            final String macAlgorithm = msg.getMacAlgorithm();
+            final String macSecret = msg.getMacSecret();
+            final Map<String, String> unparsedProperties = msg.getUnparsedProperties();
+
+            if (accessToken != null) {
+              final OAuth2Token storedAccessToken = this.store.createToken();
+              storedAccessToken.setIssuedAt(issuedAt);
+              if (expiresIn != null) {
+                storedAccessToken.setExpiresAt(issuedAt + Long.decode(expiresIn) * 1000);
+              } else {
+                storedAccessToken.setExpiresAt(0);
+              }
+              storedAccessToken.setGadgetUri(gadgetUri);
+              storedAccessToken.setServiceName(providerName);
+              storedAccessToken.setScope(scope);
+              storedAccessToken.setSecret(accessToken.getBytes("UTF-8"));
+              storedAccessToken.setTokenType(tokenType);
+              storedAccessToken.setType(OAuth2Token.Type.ACCESS);
+              storedAccessToken.setUser(user);
+              if (macAlgorithm != null) {
+                storedAccessToken.setMacAlgorithm(macAlgorithm);
+              }
+              if (macSecret != null) {
+                storedAccessToken.setMacSecret(macSecret.getBytes("UTF-8"));
+              }
+              storedAccessToken.setProperties(unparsedProperties);
+              this.store.setToken(storedAccessToken);
+              accessor.setAccessToken(storedAccessToken);
+            }
+
+            if (refreshToken != null) {
+              final OAuth2Token storedRefreshToken = this.store.createToken();
+              storedRefreshToken.setExpiresAt(0);
+              storedRefreshToken.setGadgetUri(gadgetUri);
+              storedRefreshToken.setServiceName(providerName);
+              storedRefreshToken.setScope(scope);
+              storedRefreshToken.setSecret(refreshToken.getBytes("UTF-8"));
+              storedRefreshToken.setTokenType(tokenType);
+              storedRefreshToken.setType(OAuth2Token.Type.REFRESH);
+              storedRefreshToken.setUser(user);
+              this.store.setToken(storedRefreshToken);
+              accessor.setRefreshToken(storedRefreshToken);
+            }
+          }
+        }
+      }
+    } catch (final Exception e) {
+      if (isLogging) {
+        TokenAuthorizationResponseHandler.LOG.log(
+                "exception thrown handling authorization response", e);
+      }
+      return TokenAuthorizationResponseHandler.getError(
+              "exception thrown handling authorization response", e, "", "");
+    }
+
+    if (isLogging) {
+      TokenAuthorizationResponseHandler.LOG.exiting(TokenAuthorizationResponseHandler.LOG_CLASS,
+              "handleResponse", ret);
+    }
+
+    return ret;
+  }
+
+  public boolean handlesResponse(final OAuth2Accessor accessor, final HttpResponse response) {
+    if (accessor == null || !accessor.isValid() || accessor.isErrorResponse()) {
+      return false;
+    }
+
+    return response != null;
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage) {
+    return TokenAuthorizationResponseHandler.getError(contextMessage, null, "", "");
+  }
+
+  private static OAuth2HandlerError getError(final String contextMessage, final Exception e,
+          final String uri, final String description) {
+    return new OAuth2HandlerError(TokenAuthorizationResponseHandler.ERROR, contextMessage, e, uri,
+            description);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/TokenEndpointResponseHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/TokenEndpointResponseHandler.java
new file mode 100644
index 0000000..5dc954b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/handler/TokenEndpointResponseHandler.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+
+/**
+ * When an TokenEndpointResponseHandler is injected into the system it will be
+ * called on every response from the token server that it claims to handle.
+ *
+ * See {@link http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-4}
+ *
+ * By default shindig has handlers for the Authorization Code and Client
+ * Credential flows.
+ *
+ */
+public interface TokenEndpointResponseHandler {
+  /**
+   * Let the handler do it's magic including any accessor/store updates.
+   *
+   * If the handler is executed and encountered an error that should stop the
+   * authorization process it should return the appropriate
+   * {@link OAuth2HandlerError}.
+   *
+   *
+   * Applies in particular to the client_credentials flow.
+   *
+   * See {@link http://tools.ietf.org/html/draft-ietf-oauth-v2-21#section-4.4.1}
+   *
+   * @param accessor
+   * @param response
+   * @return see above
+   */
+  public OAuth2HandlerError handleResponse(OAuth2Accessor accessor, HttpResponse response);
+
+  /**
+   * Does the handler support this {@link OAuth2Accessor} / {@link HttpResponse}
+   * response?
+   *
+   * @param accessor
+   * @param request
+   * @return <code>true</code> if handleRequest() should be invoked
+   */
+  public boolean handlesResponse(OAuth2Accessor accessor, HttpResponse response);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/logger/FilteredLogger.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/logger/FilteredLogger.java
new file mode 100644
index 0000000..4b3fec2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/logger/FilteredLogger.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.logger;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Wraps a {@link Logger} with functions to remove OAuth2 secrets so they don't show up in trace
+ * logs.
+ *
+ */
+public class FilteredLogger {
+  private static final Level DEFAULT_LOG_LEVEL = Level.FINEST;
+
+  private static final Pattern[] filters = new Pattern[] {
+          Pattern.compile("(?<=access_token=)[^=& \t\r\n]*"),
+          Pattern.compile("(?<=refresh_token=)[^=& \t\r\n]*"),
+          Pattern.compile("(?<=Authorization:)[^\t\r\n]*"),
+          Pattern.compile("(?<=client_id:)[^\t\r\n]*"),
+          Pattern.compile("(?<=client_id=)[^=& \t\r\n]*"),
+          Pattern.compile("(?<=client_secret=)[^=& \t\r\n]*"),
+          Pattern.compile("(?<=client_secret:)[^\t\r\n]*") };
+
+  private static String filteredParam(final Object param) {
+    final String paramString;
+    if (param != null) {
+      paramString = FilteredLogger.filterSecrets(param.toString());
+    } else {
+      paramString = "";
+    }
+
+    return paramString;
+  }
+
+  private static String[] filteredParams(final Object[] params) {
+    final String[] paramStrings;
+    if (params != null) {
+      paramStrings = new String[params.length];
+      int i = 0;
+      for (final Object param : params) {
+        if (param != null) {
+          paramStrings[i] = FilteredLogger.filteredParam(param.toString());
+        } else {
+          paramStrings[i] = "";
+        }
+        i++;
+      }
+    } else {
+      paramStrings = new String[] {};
+    }
+
+    return paramStrings;
+  }
+
+  public static String filterSecrets(final String in) {
+    String ret = in;
+    if (ret != null && ret.length() > 0) {
+      for (final Pattern pattern : FilteredLogger.filters) {
+        final Matcher m = pattern.matcher(ret);
+        ret = m.replaceAll("REMOVED");
+      }
+    }
+
+    if (ret == null) {
+      ret = "";
+    }
+
+    return ret;
+  }
+
+  public static FilteredLogger getFilteredLogger(final String className) {
+    return new FilteredLogger(className);
+  }
+
+  private final Logger logger;
+
+  protected FilteredLogger(final String className) {
+    this.logger = java.util.logging.Logger.getLogger(className, OAuth2Error.MESSAGES);
+  }
+
+  public void entering(final String sourceClass, final String sourceMethod) {
+    this.logger.entering(sourceClass, sourceMethod);
+  }
+
+  public void entering(final String sourceClass, final String sourceMethod, final Object param) {
+    this.logger.entering(sourceClass, sourceMethod, FilteredLogger.filteredParam(param));
+  }
+
+  public void entering(final String sourceClass, final String sourceMethod, final Object[] params) {
+    this.logger.entering(sourceClass, sourceMethod, FilteredLogger.filteredParams(params));
+  }
+
+  public ResourceBundle getResourceBundle() {
+    return this.logger.getResourceBundle();
+  }
+
+  public boolean isLoggable() {
+    return this.isLoggable(FilteredLogger.DEFAULT_LOG_LEVEL);
+  }
+
+  public boolean isLoggable(final Level logLevel) {
+    return this.logger.isLoggable(logLevel);
+  }
+
+  public void log(final Level logLevel, final String msg, final Object param) {
+    this.logger.log(logLevel, FilteredLogger.filterSecrets(msg),
+            FilteredLogger.filteredParam(param));
+  }
+
+  public void log(final Level logLevel, final String msg, final Object[] params) {
+    this.logger.log(logLevel, FilteredLogger.filterSecrets(msg),
+            FilteredLogger.filteredParams(params));
+  }
+
+  public void log(final Level logLevel, final String msg, final Throwable thrown) {
+    this.logger.log(logLevel, FilteredLogger.filterSecrets(msg), thrown);
+  }
+
+  public void log(final String msg, final Object param) {
+    this.log(FilteredLogger.DEFAULT_LOG_LEVEL, msg, param);
+  }
+
+  public void log(final String msg, final Object[] params) {
+    this.log(FilteredLogger.DEFAULT_LOG_LEVEL, msg, params);
+  }
+
+  public void log(final String msg, final Throwable thrown) {
+    this.logger.log(FilteredLogger.DEFAULT_LOG_LEVEL, msg, thrown);
+  }
+
+  public void exiting(final String sourceClass, final String sourceMethod) {
+    this.logger.exiting(sourceClass, sourceMethod);
+  }
+
+  public void exiting(final String sourceClass, final String sourceMethod, final Object result) {
+    this.logger.exiting(sourceClass, sourceMethod, FilteredLogger.filteredParam(result));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/MapCache.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/MapCache.java
new file mode 100644
index 0000000..98e3ae5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/MapCache.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2CallbackState;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token.Type;
+
+import java.util.Collection;
+import java.util.Map;
+
+public abstract class MapCache implements OAuth2Cache {
+  protected abstract Map<String, OAuth2Client> getClientMap();
+
+  protected abstract Map<String, OAuth2Token> getTokenMap();
+
+  protected abstract Map<String, OAuth2Accessor> getAccessorMap();
+
+  public void clearClients() throws OAuth2CacheException {
+    this.getClientMap().clear();
+  }
+
+  public void clearTokens() throws OAuth2CacheException {
+    this.getTokenMap().clear();
+  }
+
+  public void clearAccessors() {
+    this.getAccessorMap().clear();
+  }
+
+  public void storeTokens(final Collection<OAuth2Token> storeTokens) throws OAuth2CacheException {
+    if (storeTokens != null) {
+      for (final OAuth2Token token : storeTokens) {
+        this.storeToken(token);
+      }
+    }
+  }
+
+  public boolean isPrimed() {
+    return false;
+  }
+
+  public OAuth2Client getClient(final String gadgetUri, final String serviceName) {
+    OAuth2Client ret = null;
+    final String clientKey = this.getClientKey(gadgetUri, serviceName);
+    if (clientKey != null) {
+      ret = this.getClientMap().get(clientKey);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Accessor getOAuth2Accessor(final OAuth2CallbackState state) {
+    OAuth2Accessor ret = null;
+    final String accessorKey = this.getAccessorKey(state);
+    if (accessorKey != null) {
+      ret = this.getAccessorMap().get(accessorKey);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Token getToken(final String gadgetUri, final String serviceName, final String user,
+          final String scope, final Type type) {
+    OAuth2Token ret = null;
+    final String tokenKey = this.getTokenKey(gadgetUri, serviceName, user, scope, type);
+    if (tokenKey != null) {
+      ret = this.getTokenMap().get(tokenKey);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Client removeClient(final OAuth2Client client) {
+    OAuth2Client ret = null;
+    final String clientKey = this.getClientKey(client);
+    if (clientKey != null) {
+      ret = this.getClientMap().remove(clientKey);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Accessor removeOAuth2Accessor(final OAuth2Accessor accessor) {
+    OAuth2Accessor ret = null;
+    final String accessorKey = this.getAccessorKey(accessor);
+    if (accessorKey != null) {
+      ret = this.getAccessorMap().remove(accessorKey);
+    }
+
+    return ret;
+  }
+
+  public OAuth2Token removeToken(final OAuth2Token token) {
+    OAuth2Token ret = null;
+    final String tokenKey = this.getTokenKey(token);
+    if (tokenKey != null) {
+      ret = this.getTokenMap().remove(tokenKey);
+    }
+
+    return ret;
+  }
+
+  public void storeClient(final OAuth2Client client) throws OAuth2CacheException {
+    if (client != null) {
+      final String clientKey = this.getClientKey(client.getGadgetUri(), client.getServiceName());
+      this.getClientMap().put(clientKey, client);
+    }
+  }
+
+  public void storeClients(final Collection<OAuth2Client> clients) throws OAuth2CacheException {
+    if (clients != null) {
+      for (final OAuth2Client client : clients) {
+        this.storeClient(client);
+      }
+    }
+  }
+
+  public void storeOAuth2Accessor(final OAuth2Accessor accessor) {
+    if (accessor != null) {
+      final String accessorKey = this.getAccessorKey(accessor);
+      this.getAccessorMap().put(accessorKey, accessor);
+    }
+  }
+
+  public void storeToken(final OAuth2Token token) throws OAuth2CacheException {
+    if (token != null) {
+      final String tokenKey = this.getTokenKey(token);
+      this.getTokenMap().put(tokenKey, token);
+    }
+  }
+
+  protected String getClientKey(final OAuth2Client client) {
+    return this.getClientKey(client.getGadgetUri(), client.getServiceName());
+  }
+
+  protected String getClientKey(final String gadgetUri, final String serviceName) {
+    if (gadgetUri == null || serviceName == null) {
+      return null;
+    }
+    final StringBuilder buf = new StringBuilder(gadgetUri.length() + serviceName.length() + 1);
+    buf.append(gadgetUri);
+    buf.append(':');
+    buf.append(serviceName);
+    return buf.toString();
+  }
+
+  protected String getAccessorKey(final OAuth2CallbackState state) {
+    return this.getAccessorKey(state.getGadgetUri(), state.getServiceName(), state.getUser(),
+            state.getScope());
+  }
+
+  private String getAccessorKey(final String gadgetUri, final String serviceName,
+          final String user, final String scope) {
+    if (gadgetUri == null || serviceName == null || user == null) {
+      return null;
+    }
+
+    final String s;
+    if (scope == null) {
+      s = "";
+    } else {
+      s = scope;
+    }
+
+    final StringBuilder buf = new StringBuilder(gadgetUri.length() + serviceName.length()
+            + user.length() + s.length() + 3);
+    buf.append(gadgetUri);
+    buf.append(':');
+    buf.append(serviceName);
+    buf.append(':');
+    buf.append(user);
+    buf.append(':');
+    buf.append(s);
+
+    return buf.toString();
+  }
+
+  protected String getAccessorKey(final OAuth2Accessor accessor) {
+    return this.getAccessorKey(accessor.getGadgetUri(), accessor.getServiceName(),
+            accessor.getUser(), accessor.getScope());
+  }
+
+  protected String getTokenKey(final String gadgetUri, final String serviceName, final String user,
+          final String scope, final Type type) {
+    if (gadgetUri == null || serviceName == null || user == null) {
+      return null;
+    }
+
+    final String s;
+    if (scope == null) {
+      s = "";
+    } else {
+      s = scope;
+    }
+
+    final String t = type.name();
+
+    final StringBuilder buf = new StringBuilder(gadgetUri.length() + serviceName.length()
+            + user.length() + s.length() + t.length() + 4);
+    buf.append(gadgetUri);
+    buf.append(':');
+    buf.append(serviceName);
+    buf.append(':');
+    buf.append(user);
+    buf.append(':');
+    buf.append(s);
+    buf.append(':');
+    buf.append(t);
+
+    return buf.toString();
+  }
+
+  protected String getTokenKey(final OAuth2Token token) {
+    return this.getTokenKey(token.getGadgetUri(), token.getServiceName(), token.getUser(),
+            token.getScope(), token.getType());
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Cache.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Cache.java
new file mode 100644
index 0000000..938e451
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Cache.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2CallbackState;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+
+import java.util.Collection;
+
+/**
+ * Used by {@link OAuth2Store} to cache OAuth2 data.
+ *
+ * Default implementation is in-memory HashMaps for shindig.
+ *
+ */
+public interface OAuth2Cache {
+  /**
+   * Clears all cached {@link OAuth2Client}s.
+   *
+   * @throws OAuth2CacheException
+   */
+  void clearAccessors() throws OAuth2CacheException;
+
+  /**
+   * Clears all cached {@link OAuth2Client}s.
+   *
+   * @throws OAuth2CacheException
+   */
+  void clearClients() throws OAuth2CacheException;
+
+  /**
+   * Clears all cached {@link OAuth2Token}s.
+   *
+   * @throws OAuth2CacheException
+   */
+  void clearTokens() throws OAuth2CacheException;
+
+  /**
+   * Find an {@link OAuth2Client}.
+   *
+   * @param gadgetUri
+   * @param serviceName
+   * @return OAuth2Client
+   */
+  OAuth2Client getClient(String gadgetUri, String serviceName);
+
+  /**
+   * Find an {@link OAuth2Accessor} by state.
+   *
+   * @param state
+   * @return OAuth2Accessor
+   */
+  OAuth2Accessor getOAuth2Accessor(OAuth2CallbackState state);
+
+  /**
+   * Find an {@link OAuth2Token} based on index
+   *
+   * @param gadgetUri
+   * @param serviceName
+   * @param user
+   * @param scope
+   * @param type
+   * @return an OAuth2Token
+   */
+  OAuth2Token getToken(String gadgetUri, String serviceName, String user, String scope,
+          OAuth2Token.Type type);
+
+  /**
+   * @return true if the cache has already been primed. (presumably by another node.)
+   */
+  boolean isPrimed();
+
+  /**
+   * Remove the given client;
+   *
+   * @param client
+   * @return the client that was removed, or <code>null</code> if removal failed
+   */
+  OAuth2Client removeClient(OAuth2Client client);
+
+  /**
+   * Remove the given accessor.
+   *
+   * @param accessor
+   * @return the accessor that was removed, or <code>null</code> if removal failed
+   */
+  OAuth2Accessor removeOAuth2Accessor(OAuth2Accessor accessor);
+
+  /**
+   * Remove the given token;
+   *
+   * @param token
+   * @return the token that was removed, or <code>null</code> if removal failed
+   */
+  OAuth2Token removeToken(OAuth2Token token);
+
+  /**
+   * Stores the given client.
+   *
+   * @param index
+   * @param client
+   * @throws OAuth2CacheException
+   */
+  void storeClient(OAuth2Client client) throws OAuth2CacheException;
+
+  /**
+   * Store all clients in the collection.
+   *
+   * @param clients
+   * @throws OAuth2CacheException
+   */
+  void storeClients(Collection<OAuth2Client> clients) throws OAuth2CacheException;
+
+  /**
+   * Stores the given accessor.
+   *
+   * @param accessor
+   */
+  void storeOAuth2Accessor(OAuth2Accessor accessor);
+
+  /**
+   * Stores the given token.
+   */
+  void storeToken(OAuth2Token token) throws OAuth2CacheException;
+
+  /**
+   * Stores all tokens in the collection.
+   *
+   * @param tokens
+   * @throws OAuth2CacheException
+   */
+  void storeTokens(Collection<OAuth2Token> tokens) throws OAuth2CacheException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2CacheException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2CacheException.java
new file mode 100644
index 0000000..cb36d0f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2CacheException.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+/**
+ * Subclass of {@link OAuth2PersistenceException} for caching issues.
+ *
+ */
+public class OAuth2CacheException extends OAuth2PersistenceException {
+  private static final long serialVersionUID = -7722454386821962603L;
+
+  public OAuth2CacheException(final Exception cause) {
+    super(cause);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Client.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Client.java
new file mode 100644
index 0000000..50abe62
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Client.java
@@ -0,0 +1,312 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+/**
+ * Data class for client data stored in persistence.
+ *
+ * Uses the injected {@link OAuth2Encrypter} protect the client_secret in the persistence store.
+ *
+ */
+public class OAuth2Client implements Serializable {
+  private static final long serialVersionUID = -7374658882342619184L;
+  private boolean allowModuleOverride;
+  private boolean authorizationHeader;
+  private String authorizationUrl;
+  private String clientAuthenticationType;
+  private String clientId;
+  private byte[] clientSecret;
+  private byte[] encryptedSecret;
+  private final transient OAuth2Encrypter encrypter;
+  private String gadgetUri;
+  private String grantType = OAuth2Message.NO_GRANT_TYPE;
+  private String redirectUri;
+  private String serviceName;
+  private String tokenUrl;
+  private OAuth2Accessor.Type type = OAuth2Accessor.Type.UNKNOWN;
+  private boolean urlParameter;
+  private boolean sharedToken = false;
+  private String[] allowedDomains = new String[] {};
+
+  public OAuth2Client() {
+    this(null);
+  }
+
+  @Inject
+  public OAuth2Client(final OAuth2Encrypter encrypter) {
+    this.encrypter = encrypter;
+  }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof OAuth2Client)) {
+      return false;
+    }
+    final OAuth2Client other = (OAuth2Client) obj;
+    if (this.gadgetUri == null) {
+      if (other.gadgetUri != null) {
+        return false;
+      }
+    } else if (!this.gadgetUri.equals(other.gadgetUri)) {
+      return false;
+    }
+    if (this.serviceName == null) {
+      if (other.serviceName != null) {
+        return false;
+      }
+    } else if (!this.serviceName.equals(other.serviceName)) {
+      return false;
+    }
+    if (this.sharedToken != other.sharedToken) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Returns authorization endpoint
+   *
+   * @return authorization endpoint
+   */
+  public String getAuthorizationUrl() {
+    return this.authorizationUrl;
+  }
+
+  /**
+   * Returns client authentication type
+   *
+   * @return client authentication type
+   */
+  public String getClientAuthenticationType() {
+    return this.clientAuthenticationType;
+  }
+
+  /**
+   * Returns client id.
+   *
+   * @return client id
+   */
+  public String getClientId() {
+    return this.clientId;
+  }
+
+  /**
+   * Returns client secret
+   *
+   * @return client secret
+   */
+  public byte[] getClientSecret() {
+    return this.clientSecret;
+  }
+
+  /**
+   * Returns encrypted secret
+   *
+   * @return encrypted secret
+   */
+  public byte[] getEncryptedSecret() {
+    return this.encryptedSecret;
+  }
+
+  public OAuth2Encrypter getEncrypter() {
+    return this.encrypter;
+  }
+
+  public String getGadgetUri() {
+    return this.gadgetUri;
+  }
+
+  public String getGrantType() {
+    return this.grantType;
+  }
+
+  public String getRedirectUri() {
+    return this.redirectUri;
+  }
+
+  public String getServiceName() {
+    return this.serviceName;
+  }
+
+  public String getTokenUrl() {
+    return this.tokenUrl;
+  }
+
+  public OAuth2Accessor.Type getType() {
+    return this.type;
+  }
+
+  @Override
+  public int hashCode() {
+    if (this.serviceName != null && this.gadgetUri != null) {
+      return (this.serviceName + ':' + this.gadgetUri).hashCode();
+    }
+
+    return 0;
+  }
+
+  public boolean isAllowModuleOverride() {
+    return this.allowModuleOverride;
+  }
+
+  public boolean isAuthorizationHeader() {
+    return this.authorizationHeader;
+  }
+
+  public boolean isSharedToken() {
+    return this.sharedToken;
+  }
+
+  public boolean isUrlParameter() {
+    return this.urlParameter;
+  }
+
+  public void setAllowModuleOverride(final boolean alllowModuleOverride) {
+    this.allowModuleOverride = alllowModuleOverride;
+  }
+
+  public void setAuthorizationHeader(final boolean authorizationHeader) {
+    this.authorizationHeader = authorizationHeader;
+  }
+
+  public void setAuthorizationUrl(final String authorizationUrl) {
+    this.authorizationUrl = authorizationUrl;
+  }
+
+  public void setClientAuthenticationType(final String clientAuthenticationType) {
+    this.clientAuthenticationType = clientAuthenticationType;
+  }
+
+  public void setClientId(final String clientId) {
+    this.clientId = clientId;
+  }
+
+  public void setClientSecret(final byte[] secret) throws OAuth2EncryptionException {
+    this.clientSecret = secret;
+    if (this.encrypter != null) {
+      this.encryptedSecret = this.encrypter.encrypt(secret);
+    }
+  }
+
+  public void setEncryptedSecret(final byte[] encryptedSecret) throws OAuth2EncryptionException {
+    this.encryptedSecret = encryptedSecret;
+    if (this.encrypter != null) {
+      this.clientSecret = this.encrypter.decrypt(encryptedSecret);
+    }
+  }
+
+  public void setGadgetUri(final String gadgetUri) {
+    this.gadgetUri = gadgetUri;
+  }
+
+  public void setGrantType(final String grantType) {
+    this.grantType = grantType;
+  }
+
+  public void setRedirectUri(final String redirectUri) {
+    this.redirectUri = redirectUri;
+  }
+
+  public void setServiceName(final String serviceName) {
+    this.serviceName = serviceName;
+  }
+
+  public void setSharedToken(final boolean sharedToken) {
+    this.sharedToken = sharedToken;
+  }
+
+  public void setTokenUrl(final String tokenUrl) {
+    this.tokenUrl = tokenUrl;
+  }
+
+  public void setType(final OAuth2Accessor.Type type) {
+    this.type = type;
+  }
+
+  public void setUrlParameter(final boolean urlParameter) {
+    this.urlParameter = urlParameter;
+  }
+
+  /**
+   * sets the domains of allowed resource servers
+   *
+   * @param allowedDomains
+   */
+  public void setAllowedDomains(final String[] allowedDomains) {
+    this.allowedDomains = allowedDomains;
+  }
+
+  /**
+   * gets the domains of allowed resource servers
+   *
+   * @return allowed domains
+   */
+  public String[] getAllowedDomains() {
+    return this.allowedDomains;
+  }
+
+  @Override
+  public String toString() {
+    return "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2ClientImpl: serviceName = "
+            + this.serviceName + " , redirectUri = " + this.redirectUri + " , gadgetUri = "
+            + this.gadgetUri + " , clientId = " + this.clientId + " , grantType = "
+            + this.grantType + " , type = " + this.type.name() + " , grantType = " + this.grantType
+            + " , tokenUrl = " + this.tokenUrl + " , authorizationUrl = " + this.authorizationUrl
+            + " , this.clientAuthenticationType = " + this.clientAuthenticationType
+            + " , this.sharedToken = " + this.sharedToken + ", this.allowedDomains = "
+            + Arrays.asList(this.allowedDomains);
+  }
+
+  @Override
+  public OAuth2Client clone() {
+    final OAuth2Client ret = new OAuth2Client(this.encrypter);
+    ret.setAllowModuleOverride(this.allowModuleOverride);
+    ret.setAuthorizationHeader(this.authorizationHeader);
+    ret.setAuthorizationUrl(this.authorizationUrl);
+    ret.setClientAuthenticationType(this.clientAuthenticationType);
+    ret.setClientId(this.clientId);
+    try {
+      ret.setClientSecret(this.clientSecret);
+    } catch (final OAuth2EncryptionException e) {
+      // no op
+    }
+    ret.setGadgetUri(this.gadgetUri);
+    ret.setGrantType(this.grantType);
+    ret.setRedirectUri(this.redirectUri);
+    ret.setServiceName(this.serviceName);
+    ret.setSharedToken(this.sharedToken);
+    ret.setTokenUrl(this.tokenUrl);
+    ret.setType(this.type);
+    ret.setUrlParameter(this.urlParameter);
+    ret.setAllowedDomains(this.getAllowedDomains());
+
+    return ret;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Encrypter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Encrypter.java
new file mode 100644
index 0000000..778e80e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Encrypter.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+
+/**
+ * Injected into the system to encrypt/decrypt client and token secrets in the
+ * persistence layer.
+ *
+ * This does not apply to any broader concept of token signing or other signing
+ * from the OAuth 1.0 implementation.
+ *
+ */
+public interface OAuth2Encrypter {
+  /**
+   * Decrypts client and token secret
+   *
+   * @param encryptedSecret
+   * @return decryptedSecret
+   */
+  public byte[] decrypt(byte[] encryptedSecret) throws OAuth2EncryptionException;
+
+  /**
+   * Encrypts client and token secret
+   *
+   * @param plainSecret
+   * @return encryptedSecret
+   */
+  public byte[] encrypt(byte[] plainSecret) throws OAuth2EncryptionException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2EncryptionException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2EncryptionException.java
new file mode 100644
index 0000000..dcf92af
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2EncryptionException.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.apache.shindig.gadgets.GadgetException;
+
+/**
+ * Subclass of {@link OAuth2PersistenceException} for secret
+ * encryption/decryption issues.
+ *
+ */
+public class OAuth2EncryptionException extends GadgetException {
+  private static final long serialVersionUID = -3884237661767049433L;
+
+  public OAuth2EncryptionException(final Exception cause) {
+    super(Code.OAUTH_STORAGE_ERROR, cause);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2PersistenceException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2PersistenceException.java
new file mode 100644
index 0000000..6881a59
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2PersistenceException.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+/**
+ * Exception class for errors that occur in the persistence layer.
+ */
+public class OAuth2PersistenceException extends Exception {
+  private static final long serialVersionUID = -8550943441259921635L;
+
+  public OAuth2PersistenceException(final Exception cause) {
+    super(cause);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Persister.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Persister.java
new file mode 100644
index 0000000..d49efdb
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2Persister.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.apache.shindig.gadgets.oauth2.BasicOAuth2Store;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Store;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.JSONOAuth2Persister;
+
+import java.util.Set;
+
+/**
+ * Interface, used primarily by {@link OAuth2Store}, to manage {@link OAuth2Accessor} and
+ * {@link OAuth2Token} storage.
+ *
+ * An {@link OAuth2Accessor} has the same basic information as the {@link OAuth2Client}, merged with
+ * gadget spec and request prefs.
+ *
+ * {@link OAuth2Accessor} is short lived, for the life of one request.
+ *
+ * {@link OAuth2Clients} is intended to be persisted and cached.
+ *
+ * The default persister for shindig is {@link JSONOAuth2Persister}
+ *
+ */
+public interface OAuth2Persister {
+  /**
+   * Retrieves a client from the persistence layer. Returns <code>null</code> if not found.
+   *
+   * @param gadgetUri
+   * @param serviceName
+   * @return the client in the given mapping, must return <code>null</code> if the client is not
+   *         found
+   * @throws OAuth2PersistenceException
+   */
+  OAuth2Client findClient(String gadgetUri, String serviceName) throws OAuth2PersistenceException;
+
+  /**
+   *
+   * @param gadgetUri
+   * @param serviceName
+   * @param user
+   * @param scope
+   * @param type
+   * @return the token in the given mapping
+   * @throws OAuth2PersistenceException
+   */
+  OAuth2Token findToken(String gadgetUri, String serviceName, String user, String scope,
+          OAuth2Token.Type type) throws OAuth2PersistenceException;
+
+  /**
+   * Inserts a new {@link OAuth2Token} into the persistence layer.
+   *
+   * @param token
+   * @throws OAuth2PersistenceException
+   */
+  void insertToken(OAuth2Token token) throws OAuth2PersistenceException;
+
+  /**
+   * Load all the clients from the persistence layer. The {@link BasicOAuth2Store#init()} method
+   * will call this to prepopulate the cache.
+   *
+   * @return
+   * @throws OAuth2PersistenceException
+   */
+  Set<OAuth2Client> loadClients() throws OAuth2PersistenceException;
+
+  /**
+   * Load all the tokens from the persistence layer. The {@link BasicOAuth2Store#init()} method will
+   * call this to prepopulate the cache.
+   *
+   * @return
+   * @throws OAuth2PersistenceException
+   */
+  Set<OAuth2Token> loadTokens() throws OAuth2PersistenceException;
+
+  /**
+   * Removes a token from the persistence layer.
+   *
+   * @param gadgetUri
+   * @param serviceName
+   * @param user
+   * @param scope
+   * @param type
+   * @return
+   * @throws OAuth2PersistenceException
+   */
+  boolean removeToken(String gadgetUri, String serviceName, String user, String scope,
+          OAuth2Token.Type type) throws OAuth2PersistenceException;
+
+  /**
+   * Updates an existing {@link OAuth2Token} in the persistence layer.
+   *
+   * @param token
+   * @throws OAuth2PersistenceException
+   */
+  void updateToken(OAuth2Token token) throws OAuth2PersistenceException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2TokenPersistence.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2TokenPersistence.java
new file mode 100644
index 0000000..09115ee
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2TokenPersistence.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2RequestException;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+
+import java.util.Map;
+
+/**
+ * see {@link OAuth2Token}
+ *
+ */
+public class OAuth2TokenPersistence implements OAuth2Token {
+  private static final long serialVersionUID = -169781729667228661L;
+  private byte[] encryptedMacSecret;
+  private byte[] encryptedSecret;
+  private transient final OAuth2Encrypter encrypter;
+  private long expiresAt;
+  private String gadgetUri;
+  private long issuedAt;
+  private String macAlgorithm;
+  private String macExt;
+  private byte[] macSecret;
+  private final Map<String, String> properties;
+  private String scope;
+  private byte[] secret;
+  private String serviceName;
+  private String tokenType;
+  private Type type;
+  private String user;
+
+  public OAuth2TokenPersistence() {
+    this(null);
+  }
+
+  @Inject
+  public OAuth2TokenPersistence(final OAuth2Encrypter encrypter) {
+    this.encrypter = encrypter;
+    this.properties = Maps.newHashMap();
+  }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof OAuth2Token)) {
+      return false;
+    }
+    final OAuth2Token other = (OAuth2Token) obj;
+    if (this.gadgetUri == null) {
+      if (other.getGadgetUri() != null) {
+        return false;
+      }
+    } else if (!this.gadgetUri.equals(other.getGadgetUri())) {
+      return false;
+    }
+    if (this.serviceName == null) {
+      if (other.getServiceName() != null) {
+        return false;
+      }
+    } else if (!this.serviceName.equals(other.getServiceName())) {
+      return false;
+    }
+
+    if (this.user == null) {
+      if (other.getUser() != null) {
+        return false;
+      }
+    } else if (!this.user.equals(other.getUser())) {
+      return false;
+    }
+    if (this.scope == null) {
+      if (other.getScope() != null) {
+        return false;
+      }
+    } else if (!this.scope.equals(other.getScope())) {
+      return false;
+    }
+    if (this.type == null) {
+      if (other.getType() != null) {
+        return false;
+      }
+    } else if (!this.type.equals(other.getType())) {
+      return false;
+    }
+
+    return true;
+  }
+
+  public byte[] getEncryptedMacSecret() {
+    return this.encryptedMacSecret;
+  }
+
+  public byte[] getEncryptedSecret() {
+    return this.encryptedSecret;
+  }
+
+  public long getExpiresAt() {
+    return this.expiresAt;
+  }
+
+  public String getGadgetUri() {
+    return this.gadgetUri;
+  }
+
+  public long getIssuedAt() {
+    return this.issuedAt;
+  }
+
+  public String getMacAlgorithm() {
+    return this.macAlgorithm;
+  }
+
+  public String getMacExt() {
+    return this.macExt;
+  }
+
+  public byte[] getMacSecret() {
+    return this.macSecret;
+  }
+
+  public Map<String, String> getProperties() {
+    return this.properties;
+  }
+
+  public String getScope() {
+    return this.scope;
+  }
+
+  public byte[] getSecret() {
+    return this.secret;
+  }
+
+  public String getServiceName() {
+    return this.serviceName;
+  }
+
+  public String getTokenType() {
+    if (this.tokenType == null || this.tokenType.length() == 0) {
+      this.tokenType = OAuth2Message.BEARER_TOKEN_TYPE;
+    }
+    return this.tokenType;
+  }
+
+  public Type getType() {
+    return this.type;
+  }
+
+  public String getUser() {
+    return this.user;
+  }
+
+  @Override
+  public int hashCode() {
+    if (this.serviceName != null && this.gadgetUri != null) {
+      return (this.serviceName + ':' + this.gadgetUri + ':' + this.user + ':' + this.scope + ':' + this.type)
+              .hashCode();
+    }
+
+    return 0;
+  }
+
+  public void setEncryptedMacSecret(final byte[] encryptedSecret) throws OAuth2EncryptionException {
+    this.encryptedMacSecret = encryptedSecret;
+    this.macSecret = this.encrypter == null ? encryptedSecret : this.encrypter.decrypt(encryptedSecret);
+  }
+
+  public void setEncryptedSecret(final byte[] encryptedSecret) throws OAuth2EncryptionException {
+    this.encryptedSecret = encryptedSecret;
+    this.secret = this.encrypter == null ? encryptedSecret : this.encrypter.decrypt(encryptedSecret);
+  }
+
+  public void setExpiresAt(final long expiresAt) {
+    this.expiresAt = expiresAt;
+  }
+
+  public void setGadgetUri(final String gadgetUri) {
+    this.gadgetUri = gadgetUri;
+  }
+
+  public void setIssuedAt(final long issuedAt) {
+    this.issuedAt = issuedAt;
+  }
+
+  public void setMacAlgorithm(final String algorithm) {
+    this.macAlgorithm = algorithm;
+  }
+
+  public void setMacExt(final String macExt) {
+    this.macExt = macExt;
+  }
+
+  public void setMacSecret(final byte[] secret) throws OAuth2RequestException {
+    this.macSecret = secret;
+    try {
+      this.encryptedMacSecret = this.encrypter == null ? secret : this.encrypter.encrypt(secret);
+    } catch (final OAuth2EncryptionException e) {
+      throw new OAuth2RequestException(OAuth2Error.SECRET_ENCRYPTION_PROBLEM,
+              "OAuth2TokenPersistence could not encrypt the mac secret", e);
+    }
+  }
+
+  public void setProperties(final Map<String, String> properties) {
+    this.properties.clear();
+    if (properties != null) {
+      this.properties.putAll(properties);
+    }
+  }
+
+  public void setScope(final String scope) {
+    this.scope = scope;
+  }
+
+  public void setSecret(final byte[] secret) throws OAuth2RequestException {
+    this.secret = secret;
+    try {
+      this.encryptedSecret = this.encrypter == null ? secret : this.encrypter.encrypt(secret);
+    } catch (final OAuth2EncryptionException e) {
+      throw new OAuth2RequestException(OAuth2Error.SECRET_ENCRYPTION_PROBLEM,
+              "OAuth2TokenPersistence could not encrypt the token secret", e);
+    }
+  }
+
+  public void setServiceName(final String serviceName) {
+    this.serviceName = serviceName;
+  }
+
+  public void setTokenType(final String tokenType) {
+    this.tokenType = tokenType;
+  }
+
+  public void setType(final Type type) {
+    this.type = type;
+  }
+
+  public void setUser(final String user) {
+    this.user = user;
+  }
+
+  @Override
+  public String toString() {
+    return "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2TokenImpl: serviceName = "
+            + this.serviceName + " , user = " + this.user + " , gadgetUri = " + this.gadgetUri
+            + " , scope = " + this.scope + " , tokenType = " + this.getTokenType()
+            + " , issuedAt = " + this.issuedAt + " , expiresAt = " + this.expiresAt + " , type = "
+            + this.type;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/InMemoryCache.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/InMemoryCache.java
new file mode 100644
index 0000000..dcb7730
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/InMemoryCache.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2CallbackState;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token.Type;
+import org.apache.shindig.gadgets.oauth2.persistence.MapCache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Cache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ *
+ * {@link OAuth2Cache} implementation using in-memory {@link HashMap}s.
+ *
+ */
+@Singleton
+public class InMemoryCache extends MapCache {
+  private final Map<String, OAuth2Accessor> accessors;
+  private final Map<String, OAuth2Client> clients;
+  private final Map<String, OAuth2Token> tokens;
+
+  @Inject
+  public InMemoryCache() {
+    final Map<String, OAuth2Token> tMap = Maps.newHashMap();
+    this.tokens = Collections.synchronizedMap(tMap);
+    final Map<String, OAuth2Client> cMap = Maps.newHashMap();
+    this.clients = Collections.synchronizedMap(cMap);
+    final Map<String, OAuth2Accessor> aMap = Maps.newHashMap();
+    this.accessors = Collections.synchronizedMap(aMap);
+  }
+
+  @Override
+  protected Map<String, OAuth2Client> getClientMap() {
+    return this.clients;
+  }
+
+  @Override
+  protected Map<String, OAuth2Token> getTokenMap() {
+    return this.tokens;
+  }
+
+  @Override
+  protected Map<String, OAuth2Accessor> getAccessorMap() {
+    return this.accessors;
+  }
+
+  // getXXXKey() methods are overridden here even though they don't do anything
+  // Since this is a sample class it's to make it evident to other developers
+  // that they can override key management themselves.
+
+  @Override
+  protected String getClientKey(final OAuth2Client client) {
+    return super.getClientKey(client);
+  }
+
+  @Override
+  protected String getClientKey(final String gadgetUri, final String serviceName) {
+    return super.getClientKey(gadgetUri, serviceName);
+  }
+
+  @Override
+  protected String getAccessorKey(final OAuth2CallbackState state) {
+    return super.getAccessorKey(state);
+  }
+
+  @Override
+  protected String getAccessorKey(final OAuth2Accessor accessor) {
+    return super.getAccessorKey(accessor);
+  }
+
+  @Override
+  protected String getTokenKey(final String gadgetUri, final String serviceName, final String user,
+          final String scope, final Type type) {
+    return super.getTokenKey(gadgetUri, serviceName, user, scope, type);
+  }
+
+  @Override
+  protected String getTokenKey(final OAuth2Token token) {
+    return super.getTokenKey(token);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/JSONOAuth2Persister.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/JSONOAuth2Persister.java
new file mode 100644
index 0000000..08ef115
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/JSONOAuth2Persister.java
@@ -0,0 +1,373 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import com.google.caja.util.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token.Type;
+import org.apache.shindig.gadgets.oauth2.logger.FilteredLogger;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2EncryptionException;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2PersistenceException;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Persister;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2TokenPersistence;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Persistence implementation that reads <code>config/oauth2.json</code> on startup
+ *
+ */
+@Singleton
+public class JSONOAuth2Persister implements OAuth2Persister {
+  private static final String ALLOW_MODULE_OVERRIDE = "allowModuleOverride";
+  private static final String AUTHORIZATION_HEADER = "usesAuthorizationHeader";
+  private static final String AUTHORIZATION_URL = "authorizationUrl";
+  private static final String CLIENT_AUTHENTICATION = "client_authentication";
+  private static final String CLIENT_NAME = "clientName";
+  private static final String CLIENTS = "clients";
+  private static final String ENDPOINTS = "endpoints";
+  private static final String GADGET_BINDGINGS = "gadgetBindings";
+  private static final String NO_CLIENT_AUTHENTICATION = "NONE";
+  private static final String OAUTH2_CONFIG = "config/oauth2.json";
+  private static final String PROVIDER_NAME = "providerName";
+  private static final String PROVIDERS = "providers";
+  private static final String TOKEN_URL = "tokenUrl";
+  private static final String TYPE = "type";
+  private static final String URL_PARAMETER = "usesUrlParameter";
+  private static final String ALLOWED_DOMAINS = "allowedDomains";
+
+  private final JSONObject configFile;
+  private final String contextRoot;
+  private final OAuth2Encrypter encrypter;
+  private final String globalRedirectUri;
+
+  private final Authority authority;
+
+  private static final String LOG_CLASS = JSONOAuth2Persister.class.getName();
+  private static final FilteredLogger LOG = FilteredLogger
+          .getFilteredLogger(JSONOAuth2Persister.LOG_CLASS);
+
+  @Inject
+  public JSONOAuth2Persister(final OAuth2Encrypter encrypter, final Authority authority,
+          final String globalRedirectUri, @Nullable
+          @Named("shindig.contextroot")
+          final String contextRoot) throws OAuth2PersistenceException {
+    this.encrypter = encrypter;
+    this.authority = authority;
+    this.globalRedirectUri = globalRedirectUri;
+    this.contextRoot = contextRoot;
+    try {
+      this.configFile = new JSONObject(
+              JSONOAuth2Persister.getJSONString(JSONOAuth2Persister.OAUTH2_CONFIG));
+    } catch (final Exception e) {
+      if (JSONOAuth2Persister.LOG.isLoggable()) {
+        JSONOAuth2Persister.LOG.log("OAuth2PersistenceException", e);
+      }
+      throw new OAuth2PersistenceException(e);
+    }
+  }
+
+  public JSONOAuth2Persister(final OAuth2Encrypter encrypter, final Authority authority,
+          final String globalRedirectUri, @Nullable
+          @Named("shindig.contextroot")
+          final String contextRoot, final JSONObject configFile) {
+    this.encrypter = encrypter;
+    this.authority = authority;
+    this.globalRedirectUri = globalRedirectUri;
+    this.contextRoot = contextRoot;
+    this.configFile = configFile;
+  }
+
+  public OAuth2Token createToken() {
+    return new OAuth2TokenPersistence(this.encrypter);
+  }
+
+  public static OAuth2Client findClient(@SuppressWarnings("unused")
+  final Integer index) {
+    return null;
+  }
+
+  public OAuth2Client findClient(final String providerName, final String gadgetUri)
+          throws OAuth2PersistenceException {
+    return null;
+  }
+
+  public static OAuth2Provider findProvider(@SuppressWarnings("unused")
+  final Integer index) {
+    return null;
+  }
+
+  public static OAuth2Provider findProvider(@SuppressWarnings("unused")
+  final String providerName) {
+    return null;
+  }
+
+  public static OAuth2Token findToken(@SuppressWarnings("unused")
+  final Integer index) {
+    return null;
+  }
+
+  public OAuth2Token findToken(final String providerName, final String gadgetUri,
+          final String user, final String scope, final Type type) throws OAuth2PersistenceException {
+    return null;
+  }
+
+  public void insertToken(final OAuth2Token token) {
+    // does nothing
+  }
+
+  public Set<OAuth2Client> loadClients() throws OAuth2PersistenceException {
+    final Map<String, OAuth2GadgetBinding> gadgetBindings = this.loadGadgetBindings();
+    final Map<String, OAuth2Provider> providers = this.loadProviders();
+
+    final Map<String, OAuth2Client> internalMap = Maps.newHashMap();
+
+    try {
+      final JSONObject clients = this.configFile.getJSONObject(JSONOAuth2Persister.CLIENTS);
+      for (final Iterator<?> j = clients.keys(); j.hasNext();) {
+        final String clientName = (String) j.next();
+        final JSONObject settings = clients.getJSONObject(clientName);
+
+        final OAuth2Client client = new OAuth2Client(this.encrypter);
+
+        final String providerName = settings.getString(JSONOAuth2Persister.PROVIDER_NAME);
+        final OAuth2Provider provider = providers.get(providerName);
+        client.setAuthorizationUrl(provider.getAuthorizationUrl());
+        client.setClientAuthenticationType(provider.getClientAuthenticationType());
+        client.setAuthorizationHeader(provider.isAuthorizationHeader());
+        client.setUrlParameter(provider.isUrlParameter());
+        client.setTokenUrl(provider.getTokenUrl());
+
+        String redirectUri = settings.optString(OAuth2Message.REDIRECT_URI, null);
+        if (redirectUri == null) {
+          redirectUri = this.globalRedirectUri;
+        }
+        final String secret = settings.optString(OAuth2Message.CLIENT_SECRET);
+        final String clientId = settings.getString(OAuth2Message.CLIENT_ID);
+        final String typeS = settings.optString(JSONOAuth2Persister.TYPE, null);
+        String grantType = settings.optString(OAuth2Message.GRANT_TYPE, null);
+        final String sharedToken = settings.optString(OAuth2Message.SHARED_TOKEN, "false");
+        if ("true".equalsIgnoreCase(sharedToken)) {
+          client.setSharedToken(true);
+        }
+
+        try {
+          client.setEncryptedSecret(secret.getBytes("UTF-8"));
+        } catch (final OAuth2EncryptionException e) {
+          throw new OAuth2PersistenceException(e);
+        }
+
+        client.setClientId(clientId);
+
+        if (this.authority != null) {
+          redirectUri = redirectUri.replace("%authority%", this.authority.getAuthority());
+          redirectUri = redirectUri.replace("%contextRoot%", this.contextRoot);
+          redirectUri = redirectUri.replace("%origin%", this.authority.getOrigin());
+          redirectUri = redirectUri.replace("%scheme", this.authority.getScheme());
+        }
+        client.setRedirectUri(redirectUri);
+
+        if (grantType == null || grantType.length() == 0) {
+          grantType = OAuth2Message.AUTHORIZATION;
+        }
+
+        client.setGrantType(grantType);
+
+        OAuth2Accessor.Type type = OAuth2Accessor.Type.UNKNOWN;
+        if (OAuth2Message.CONFIDENTIAL_CLIENT_TYPE.equals(typeS)) {
+          type = OAuth2Accessor.Type.CONFIDENTIAL;
+        } else if (OAuth2Message.PUBLIC_CLIENT_TYPE.equals(typeS)) {
+          type = OAuth2Accessor.Type.PUBLIC;
+        }
+        client.setType(type);
+
+        final JSONArray dArray = settings.optJSONArray(JSONOAuth2Persister.ALLOWED_DOMAINS);
+        if (dArray != null) {
+          final ArrayList<String> domains = new ArrayList<String>();
+          for (int i = 0; i < dArray.length(); i++) {
+            domains.add(dArray.optString(i));
+          }
+          client.setAllowedDomains(domains.toArray(new String[0]));
+        }
+
+        internalMap.put(clientName, client);
+      }
+    } catch (final Exception e) {
+      if (JSONOAuth2Persister.LOG.isLoggable()) {
+        JSONOAuth2Persister.LOG.log("OAuth2PersistenceException", e);
+      }
+      throw new OAuth2PersistenceException(e);
+    }
+
+    final Set<OAuth2Client> ret = new HashSet<OAuth2Client>(gadgetBindings.size());
+    for (final OAuth2GadgetBinding binding : gadgetBindings.values()) {
+      final String clientName = binding.getClientName();
+      final OAuth2Client cachedClient = internalMap.get(clientName);
+      final OAuth2Client client = cachedClient.clone();
+      client.setGadgetUri(binding.getGadgetUri());
+      client.setServiceName(binding.getGadgetServiceName());
+      client.setAllowModuleOverride(binding.isAllowOverride());
+      ret.add(client);
+    }
+
+    return ret;
+  }
+
+  private Map<String, OAuth2GadgetBinding> loadGadgetBindings() throws OAuth2PersistenceException {
+    final Map<String, OAuth2GadgetBinding> ret = Maps.newHashMap();
+
+    try {
+      final JSONObject bindings = this.configFile
+              .getJSONObject(JSONOAuth2Persister.GADGET_BINDGINGS);
+      for (final Iterator<?> i = bindings.keys(); i.hasNext();) {
+        final String gadgetUriS = (String) i.next();
+        String gadgetUri = null;
+        if (this.authority != null) {
+          gadgetUri = gadgetUriS.replace("%authority%", this.authority.getAuthority());
+          gadgetUri = gadgetUri.replace("%contextRoot%", this.contextRoot);
+          gadgetUri = gadgetUri.replace("%origin%", this.authority.getOrigin());
+          gadgetUri = gadgetUri.replace("%scheme%", this.authority.getScheme());
+        }
+
+        final JSONObject binding = bindings.getJSONObject(gadgetUriS);
+        for (final Iterator<?> j = binding.keys(); j.hasNext();) {
+          final String gadgetServiceName = (String) j.next();
+          final JSONObject settings = binding.getJSONObject(gadgetServiceName);
+          final String clientName = settings.getString(JSONOAuth2Persister.CLIENT_NAME);
+          final boolean allowOverride = settings
+                  .getBoolean(JSONOAuth2Persister.ALLOW_MODULE_OVERRIDE);
+          final OAuth2GadgetBinding gadgetBinding = new OAuth2GadgetBinding(gadgetUri,
+                  gadgetServiceName, clientName, allowOverride);
+
+          ret.put(gadgetBinding.getGadgetUri() + ':' + gadgetBinding.getGadgetServiceName(),
+                  gadgetBinding);
+        }
+      }
+
+    } catch (final JSONException e) {
+      if (JSONOAuth2Persister.LOG.isLoggable()) {
+        JSONOAuth2Persister.LOG.log("OAuth2PersistenceException", e);
+      }
+      throw new OAuth2PersistenceException(e);
+    }
+
+    return ret;
+  }
+
+  private Map<String, OAuth2Provider> loadProviders() throws OAuth2PersistenceException {
+    final Map<String, OAuth2Provider> ret = Maps.newHashMap();
+
+    try {
+      final JSONObject providers = this.configFile.getJSONObject(JSONOAuth2Persister.PROVIDERS);
+      for (final Iterator<?> i = providers.keys(); i.hasNext();) {
+        final String providerName = (String) i.next();
+        final JSONObject provider = providers.getJSONObject(providerName);
+        final JSONObject endpoints = provider.getJSONObject(JSONOAuth2Persister.ENDPOINTS);
+
+        final String clientAuthenticationType = provider.optString(
+                JSONOAuth2Persister.CLIENT_AUTHENTICATION,
+                JSONOAuth2Persister.NO_CLIENT_AUTHENTICATION);
+
+        final boolean authorizationHeader = provider.optBoolean(
+                JSONOAuth2Persister.AUTHORIZATION_HEADER, false);
+
+        final boolean urlParameter = provider.optBoolean(JSONOAuth2Persister.URL_PARAMETER, false);
+
+        String authorizationUrl = endpoints.optString(JSONOAuth2Persister.AUTHORIZATION_URL, null);
+
+        if (this.authority != null && authorizationUrl != null) {
+          authorizationUrl = authorizationUrl.replace("%authority%", this.authority.getAuthority());
+          authorizationUrl = authorizationUrl.replace("%contextRoot%", this.contextRoot);
+          authorizationUrl = authorizationUrl.replace("%origin%", this.authority.getOrigin());
+          authorizationUrl = authorizationUrl.replace("%scheme%", this.authority.getScheme());
+        }
+
+        String tokenUrl = endpoints.optString(JSONOAuth2Persister.TOKEN_URL, null);
+        if (this.authority != null && tokenUrl != null) {
+          tokenUrl = tokenUrl.replace("%authority%", this.authority.getAuthority());
+          tokenUrl = tokenUrl.replace("%contextRoot%", this.contextRoot);
+          tokenUrl = tokenUrl.replace("%origin%", this.authority.getOrigin());
+          tokenUrl = tokenUrl.replace("%scheme%", this.authority.getScheme());
+        }
+
+        final OAuth2Provider oauth2Provider = new OAuth2Provider();
+
+        oauth2Provider.setName(providerName);
+        oauth2Provider.setAuthorizationUrl(authorizationUrl);
+        oauth2Provider.setTokenUrl(tokenUrl);
+        oauth2Provider.setClientAuthenticationType(clientAuthenticationType);
+        oauth2Provider.setAuthorizationHeader(authorizationHeader);
+        oauth2Provider.setUrlParameter(urlParameter);
+
+        ret.put(oauth2Provider.getName(), oauth2Provider);
+      }
+    } catch (final JSONException e) {
+      if (JSONOAuth2Persister.LOG.isLoggable()) {
+        JSONOAuth2Persister.LOG.log("OAuth2PersistenceException", e);
+      }
+      throw new OAuth2PersistenceException(e);
+    }
+
+    return ret;
+  }
+
+  public Set<OAuth2Token> loadTokens() throws OAuth2PersistenceException {
+    return Collections.emptySet();
+  }
+
+  public static boolean removeToken(@SuppressWarnings("unused")
+  final Integer index) {
+    // does nothing
+    return false;
+  }
+
+  public boolean removeToken(final String providerName, final String gadgetUri, final String user,
+          final String scope, final Type type) {
+    return false;
+  }
+
+  public void updateToken(final OAuth2Token token) {
+    // does nothing
+  }
+
+  private static String getJSONString(final String location) throws IOException {
+    return ResourceLoader.getContent(location);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/NoOpEncrypter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/NoOpEncrypter.java
new file mode 100644
index 0000000..d7d787a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/NoOpEncrypter.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2EncryptionException;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Sample of {@link OAuth2Encrypter} inteface that does nothing.
+ *
+ */
+@Singleton
+public class NoOpEncrypter implements OAuth2Encrypter {
+  @Inject
+  public NoOpEncrypter() {
+
+  }
+
+  public byte[] decrypt(final byte[] encryptedSecret) throws OAuth2EncryptionException {
+    return encryptedSecret;
+  }
+
+  public byte[] encrypt(final byte[] plainSecret) throws OAuth2EncryptionException {
+    return plainSecret;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2GadgetBinding.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2GadgetBinding.java
new file mode 100644
index 0000000..f4caa7c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2GadgetBinding.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+/**
+ * Binds a gadget to a client.
+ *
+ */
+public class OAuth2GadgetBinding {
+  private final boolean allowOverride;
+  private final String clientName;
+  private final String gadgetServiceName;
+  private final String gadgetUri;
+
+  public OAuth2GadgetBinding(final String gadgetUri, final String gadgetServiceName,
+      final String clientName, final boolean allowOverride) {
+    this.gadgetUri = gadgetUri;
+    this.gadgetServiceName = gadgetServiceName;
+    this.clientName = clientName;
+    this.allowOverride = allowOverride;
+  }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof OAuth2GadgetBinding)) {
+      return false;
+    }
+    final OAuth2GadgetBinding other = (OAuth2GadgetBinding) obj;
+    if (this.gadgetUri == null) {
+      if (other.gadgetUri != null) {
+        return false;
+      }
+    } else if (!this.gadgetUri.equals(other.gadgetUri)) {
+      return false;
+    }
+    if (this.gadgetServiceName == null) {
+      if (other.gadgetServiceName != null) {
+        return false;
+      }
+    } else if (!this.gadgetServiceName.equals(other.gadgetServiceName)) {
+      return false;
+    }
+    return true;
+  }
+
+  public String getClientName() {
+    return this.clientName;
+  }
+
+  public String getGadgetServiceName() {
+    return this.gadgetServiceName;
+  }
+
+  public String getGadgetUri() {
+    return this.gadgetUri;
+  }
+
+  @Override
+  public int hashCode() {
+    if ((this.gadgetUri != null) && (this.gadgetServiceName != null)) {
+      return (this.gadgetUri + ':' + this.gadgetServiceName).hashCode();
+    }
+
+    return 0;
+  }
+
+  public boolean isAllowOverride() {
+    return this.allowOverride;
+  }
+
+  @Override
+  public String toString() {
+    return "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2GadgetBinding: gadgetUri = "
+        + this.gadgetUri + " , gadgetServiceName = " + this.gadgetServiceName
+        + " , allowOverride = " + this.allowOverride;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2PersistenceModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2PersistenceModule.java
new file mode 100644
index 0000000..38a06d8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2PersistenceModule.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Cache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Persister;
+
+import com.google.inject.AbstractModule;
+
+/**
+ * Binds default persistence classes for shindig.
+ *
+ */
+public class OAuth2PersistenceModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    this.bind(OAuth2Persister.class).to(JSONOAuth2Persister.class);
+    this.bind(OAuth2Cache.class).to(InMemoryCache.class);
+    this.bind(OAuth2Encrypter.class).to(NoOpEncrypter.class);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2Provider.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2Provider.java
new file mode 100644
index 0000000..332eb5d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2Provider.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import java.io.Serializable;
+
+/**
+ * Representation of OAuth2 endpoints and other relevant endpoint data.
+ *
+ */
+public class OAuth2Provider implements Serializable {
+  private static final long serialVersionUID = -6539761759797255778L;
+
+  private boolean authorizationHeader = true;
+  private String authorizationUrl;
+  private String clientAuthenticationType;
+  private String name;
+  private String tokenUrl;
+  private boolean urlParameter = false;
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof OAuth2Provider)) {
+      return false;
+    }
+    final OAuth2Provider other = (OAuth2Provider) obj;
+    if (this.name == null) {
+      if (other.name != null) {
+        return false;
+      }
+    } else if (!this.name.equals(other.name)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  public String getAuthorizationUrl() {
+    return this.authorizationUrl;
+  }
+
+  public String getClientAuthenticationType() {
+    return this.clientAuthenticationType;
+  }
+
+  public String getName() {
+    return this.name;
+  }
+
+  public String getTokenUrl() {
+    return this.tokenUrl;
+  }
+
+  @Override
+  public int hashCode() {
+    if (this.name != null) {
+      return this.name.hashCode();
+    }
+
+    return 0;
+  }
+
+  public boolean isAuthorizationHeader() {
+    return this.authorizationHeader;
+  }
+
+  public boolean isUrlParameter() {
+    return this.urlParameter;
+  }
+
+  public void setAuthorizationHeader(boolean authorizationHeader) {
+    this.authorizationHeader = authorizationHeader;
+  }
+
+  public void setAuthorizationUrl(final String authorizationUrl) {
+    this.authorizationUrl = authorizationUrl;
+  }
+
+  public void setClientAuthenticationType(final String clientAuthenticationType) {
+    this.clientAuthenticationType = clientAuthenticationType;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public void setTokenUrl(final String tokenUrl) {
+    this.tokenUrl = tokenUrl;
+  }
+
+  public void setUrlParameter(boolean urlParameter) {
+    this.urlParameter = urlParameter;
+  }
+
+  @Override
+  public String toString() {
+    return "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2Provider: name = "
+        + this.name + " , authorizationUrl = " + this.authorizationUrl + " , tokenUrl = "
+        + this.tokenUrl + " , clientAuthenticationType = " + this.clientAuthenticationType;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/CompactHtmlSerializer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/CompactHtmlSerializer.java
new file mode 100644
index 0000000..23b1364
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/CompactHtmlSerializer.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import com.google.common.collect.ImmutableSortedSet;
+
+import org.w3c.dom.Node;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+
+/**
+ * Performs simple content compaction while writing HTML documents. The compaction includes:
+ * <ul>
+ * <li>Collapsing consecutive whitespaces while preserving those within style, pre and script tags
+ * <li>Removing HTML comments while preserving IE conditional comments
+ * </ul>
+ *
+ * TODO - Consider adding attribute quoting elimination, empty attribute elimination where safe
+ * end-tag elmination where safe.
+ */
+public class CompactHtmlSerializer extends DefaultHtmlSerializer {
+
+  private static final ImmutableSortedSet<String> SPECIAL_TAGS = ImmutableSortedSet
+      .orderedBy(String.CASE_INSENSITIVE_ORDER)
+      .add("style", "pre", "script", "textarea")
+      .build();
+  private static final String HTML_WHITESPACE = " \t\r\n";
+
+  @Override
+  protected void writeText(Node n, Appendable output) throws IOException {
+    if (isSpecialTag(n.getParentNode().getNodeName())) {
+      super.writeText(n, output);
+    } else {
+      collapseWhitespace(n.getTextContent(), output);
+    }
+  }
+
+  @Override
+  protected void writeComment(Node n, Appendable output) throws IOException {
+    if (isSpecialTag(n.getParentNode().getNodeName())) {
+      super.writeComment(n, output);
+    } else if (isIeConditionalComment(n)) {
+      super.writeComment(n, output);
+    }
+  }
+
+  /**
+   * See <a href="http://msdn.microsoft.com/en-us/library/ms537512(printer).aspx">MSDN</a>
+   * and <a href="http://www.quirksmode.org/css/condcom.html">PPK</a>
+   */
+  private boolean isIeConditionalComment(Node n) {
+    String comment = n.getTextContent();
+    return comment.contains("[if ") && comment.contains("[endif]");
+  }
+
+  /**
+   * Returns true if a tag with a given tagName should preserve any whitespaces
+   * in its children nodes.
+   */
+  static boolean isSpecialTag(String tagName) {
+    return SPECIAL_TAGS.contains(tagName);
+  }
+
+  /**
+   * Collapse any consecutive HTML whitespace characters inside a string into
+   * one space character (0x20). This method will not output any characters when
+   * the given string is entirely composed of whitespaces.
+   *
+   * References:
+   * <ul>
+   * <li>http://www.w3.org/TR/html401/struct/text.html#h-9.1</li>
+   * <li>http://java.sun.com/javase/6/docs/api/java/lang/Character.html#isWhitespace(char)</li>
+   * </ul>
+   */
+  static void collapseWhitespace(String str, Appendable output) throws IOException {
+    str = StringUtils.stripStart(str, HTML_WHITESPACE);
+
+    // Whitespaces between a sequence of non-whitespace characters
+    boolean seenWhitespace = false;
+    for (int i = 0; i < str.length(); i++) {
+      char c = str.charAt(i);
+
+      if (HTML_WHITESPACE.indexOf(c) != -1) {
+        seenWhitespace = true;
+      } else {
+        if (seenWhitespace) {
+          output.append(' ');
+        }
+        output.append(c);
+
+        seenWhitespace = false;
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/DefaultHtmlSerializer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/DefaultHtmlSerializer.java
new file mode 100644
index 0000000..f30e358
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/DefaultHtmlSerializer.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import org.cyberneko.html.HTMLElements;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+/**
+ * This parser does not try to escape entities in text content as it expects the parser
+ * to have retained the original entity references rather than its resolved form in text nodes.
+ */
+public class DefaultHtmlSerializer implements HtmlSerializer {
+
+  /** {@inheritDoc} */
+  public String serialize(Document doc) {
+    try {
+      StringWriter sw = HtmlSerialization.createWriter(doc);
+      if (doc.getDoctype() != null) {
+        HtmlSerialization.outputDocType(doc.getDoctype(), sw);
+      }
+      this.serialize(doc, sw);
+      return sw.toString();
+    } catch (IOException ioe) {
+      return null;
+    }
+  }
+
+  public void serialize(Node n, Appendable output) throws IOException {
+    serialize(n, output, false);
+  }
+
+  private void serialize(Node n, Appendable output, boolean xmlMode)
+      throws IOException {
+    if (n == null) return;
+    switch (n.getNodeType()) {
+      case Node.CDATA_SECTION_NODE: {
+        break;
+      }
+      case Node.COMMENT_NODE: {
+        writeComment(n, output);
+        break;
+      }
+      case Node.DOCUMENT_NODE: {
+        NodeList children = n.getChildNodes();
+        for (int i = 0; i < children.getLength(); i++) {
+          serialize(children.item(i), output, xmlMode);
+        }
+        break;
+      }
+      case Node.ELEMENT_NODE: {
+        Element elem = (Element) n;
+        NodeList children = elem.getChildNodes();
+        elem = substituteElement(elem);
+
+        HTMLElements.Element htmlElement =
+            HTMLElements.getElement(elem.getNodeName());
+
+        HtmlSerialization.printStartElement(elem, output, xmlMode && htmlElement.isEmpty());
+
+        // Special HTML elements - <script> in particular - will typically
+        // only have CDATA.  If they do have elements, that'd be data pipelining
+        // or templating kicking in, and we should use XML-format output.
+        boolean childXmlMode = xmlMode || htmlElement.isSpecial();
+        for (int i = 0; i < children.getLength(); i++) {
+          serialize(children.item(i), output, childXmlMode);
+        }
+        if (!htmlElement.isEmpty()) {
+          output.append("</").append(elem.getNodeName()).append('>');
+        }
+        break;
+      }
+      case Node.ENTITY_REFERENCE_NODE: {
+        output.append("&").append(n.getNodeName()).append(";");
+        break;
+      }
+      case Node.TEXT_NODE: {
+        writeText(n, output);
+        break;
+      }
+    }
+  }
+
+  /**
+   * Convert OSData and OSTemplate tags to script tags with the appropriate
+   * type attribute on output
+   */
+  private Element substituteElement(Element elem) {
+    String scriptType = SocialDataTags.SCRIPT_TYPE_TO_OSML_TAG.inverse().get(elem.getNodeName());
+    if (scriptType != null) {
+      Element replacement = elem.getOwnerDocument().createElement("script");
+      replacement.setAttribute("type", scriptType);
+
+      // Retain the remaining attributes of the node.
+      NamedNodeMap attribs = elem.getAttributes();
+      for (int i = 0; i < attribs.getLength(); ++i) {
+        Attr attr = (Attr)attribs.item(i);
+        if (!attr.getNodeName().equalsIgnoreCase("type")) {
+          Attr newAttr = replacement.getOwnerDocument().createAttribute(attr.getNodeName());
+          newAttr.setValue(attr.getValue());
+          replacement.setAttributeNode(newAttr);
+        }
+      }
+      return replacement;
+    }
+    return elem;
+  }
+
+  protected void writeText(Node n, Appendable output) throws IOException {
+    output.append(n.getTextContent());
+  }
+
+  protected void writeComment(Node n, Appendable output) throws IOException {
+    output.append("<!--").append(n.getNodeValue()).append("-->");
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/GadgetHtmlParser.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/GadgetHtmlParser.java
new file mode 100644
index 0000000..11ec5a3
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/GadgetHtmlParser.java
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.w3c.dom.Attr;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Parser for arbitrary HTML content
+ */
+
+public abstract class GadgetHtmlParser {
+
+  //class name for logging purpose
+  private static final String classname = GadgetHtmlParser.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+
+  public static final String PARSED_DOCUMENTS = "parsedDocuments";
+  public static final String PARSED_FRAGMENTS = "parsedFragments";
+
+  private Cache<String, Document> documentCache;
+  private Cache<String, DocumentFragment> fragmentCache;
+  private Provider<HtmlSerializer> serializerProvider = new DefaultSerializerProvider();
+  protected final DOMImplementation documentFactory;
+
+  protected GadgetHtmlParser(DOMImplementation documentFactory) {
+    this.documentFactory = documentFactory;
+  }
+
+  protected GadgetHtmlParser(DOMImplementation documentFactory,
+      final HtmlSerializer serializer) {
+    this.documentFactory = documentFactory;
+    this.serializerProvider = new Provider<HtmlSerializer>() {
+      public HtmlSerializer get() {
+        return serializer;
+      }
+    };
+  }
+
+  @Inject
+  public void setCacheProvider(CacheProvider cacheProvider) {
+    documentCache = cacheProvider.createCache(PARSED_DOCUMENTS);
+    fragmentCache = cacheProvider.createCache(PARSED_FRAGMENTS);
+  }
+
+  @Inject
+  public void setSerializerProvider(Provider<HtmlSerializer> serProvider) {
+    this.serializerProvider = serProvider;
+  }
+
+  /**
+   * @param content
+   * @return true if we detect a preamble of doctype or html
+   */
+  protected static boolean attemptFullDocParseFirst(String content) {
+    String normalized = content.substring(0, Math.min(100, content.length())).toUpperCase();
+    return normalized.contains("<!DOCTYPE") || normalized.contains("<HTML");
+  }
+
+  public Document parseDom(String source) throws GadgetException {
+    Document document = null;
+    String key = null;
+    // Avoid checksum overhead if we arent caching
+    boolean shouldCache = shouldCache();
+    if (shouldCache) {
+      // TODO - Consider using the source if its under a certain size
+      key = HashUtil.checksum(source.getBytes());
+      document = documentCache.getElement(key);
+    }
+
+    if (document == null) {
+      try {
+        document = parseDomImpl(source);
+      } catch (DOMException e) {
+        // DOMException is a RuntimeException
+        document = errorDom(e);
+        HtmlSerialization.attach(document, serializerProvider.get(), source);
+        return document;
+      } catch (NullPointerException e) {
+        throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+                                  "Caught exception in parseDomImpl", e);
+      }
+
+      HtmlSerialization.attach(document, serializerProvider.get(), source);
+
+      Node html = document.getDocumentElement();
+
+      Node head = null;
+      Node body = null;
+      LinkedList<Node> beforeHead = Lists.newLinkedList();
+      LinkedList<Node> beforeBody = Lists.newLinkedList();
+
+      while (html.hasChildNodes()) {
+        Node child = html.removeChild(html.getFirstChild());
+        if (child.getNodeType() == Node.ELEMENT_NODE &&
+            "head".equalsIgnoreCase(child.getNodeName())) {
+          if (head == null) {
+            head = child;
+          } else {
+            // Concatenate <head> elements together.
+            transferChildren(head, child);
+          }
+        } else if (child.getNodeType() == Node.ELEMENT_NODE &&
+                   "body".equalsIgnoreCase(child.getNodeName())) {
+          if (body == null) {
+            body = child;
+          } else {
+            // Concatenate <body> elements together.
+            transferChildren(body, child);
+          }
+        } else if (head == null) {
+          beforeHead.add(child);
+        } else if (body == null) {
+          beforeBody.add(child);
+        } else {
+          // Both <head> and <body> are present. Append to tail of <body>.
+          body.appendChild(child);
+        }
+      }
+
+      // Ensure head tag exists
+      if (head == null) {
+        // beforeHead contains all elements that should be prepended to <body>. Switch them.
+        LinkedList<Node> temp = beforeBody;
+        beforeBody = beforeHead;
+        beforeHead = temp;
+
+        // Add as first element
+        head = document.createElement("head");
+        html.insertBefore(head, html.getFirstChild());
+      } else {
+        // Re-append head node.
+        html.appendChild(head);
+      }
+
+      // Ensure body tag exists.
+      if (body == null) {
+        // Add immediately after head.
+        body = document.createElement("body");
+        html.insertBefore(body, head.getNextSibling());
+      } else {
+        // Re-append body node.
+        html.appendChild(body);
+      }
+
+      // Leftovers: nodes before the first <head> node found and the first <body> node found.
+      // Prepend beforeHead to the front of <head>, and beforeBody to beginning of <body>,
+      // in the order they were found in the document.
+      prependToNode(head, beforeHead);
+      prependToNode(body, beforeBody);
+
+      // One exception. <style>/<link rel="stylesheet" nodes from <body> end up at the end of <head>,
+      // since doing so is HTML compliant and can never break rendering due to ordering concerns.
+      LinkedList<Node> styleNodes = Lists.newLinkedList();
+      NodeList bodyKids = body.getChildNodes();
+      for (int i = 0; i < bodyKids.getLength(); ++i) {
+        Node bodyKid = bodyKids.item(i);
+        if (bodyKid.getNodeType() == Node.ELEMENT_NODE &&
+            isStyleElement((Element)bodyKid)) {
+          styleNodes.add(bodyKid);
+        }
+      }
+
+      for (Node styleNode : styleNodes) {
+        head.appendChild(body.removeChild(styleNode));
+      }
+
+      // Finally, reprocess all script nodes for OpenSocial purposes, as these
+      // may be interpreted (rightly, from the perspective of HTML) as containing text only.
+      reprocessScriptForOpenSocial(html);
+
+      if (shouldCache) {
+        documentCache.addElement(key, document);
+      }
+    }
+
+    if (shouldCache) {
+      Document copy = (Document)document.cloneNode(true);
+      HtmlSerialization.copySerializer(document, copy);
+      return copy;
+    }
+    return document;
+  }
+
+  protected void transferChildren(Node to, Node from) {
+    while (from.hasChildNodes()) {
+      to.appendChild(from.removeChild(from.getFirstChild()));
+    }
+  }
+
+  protected void prependToNode(Node to, LinkedList<Node> from) {
+    while (!from.isEmpty()) {
+      to.insertBefore(from.removeLast(), to.getFirstChild());
+    }
+  }
+
+  private boolean isStyleElement(Element elem) {
+    return "style".equalsIgnoreCase(elem.getNodeName()) ||
+           ("link".equalsIgnoreCase(elem.getNodeName()) &&
+            ("stylesheet".equalsIgnoreCase(elem.getAttribute("rel")) ||
+             elem.getAttribute("type").toLowerCase().contains("css")));
+  }
+
+  /**
+   * Parses a snippet of markup and appends the result as children to the
+   * provided node.
+   *
+   * @param source markup to be parsed
+   * @param result Node to append results to
+   * @throws GadgetException
+   */
+  public void parseFragment(String source, Node result) throws GadgetException {
+    boolean shouldCache = shouldCache();
+    String key = null;
+    if (shouldCache) {
+      key = HashUtil.checksum(source.getBytes());
+      DocumentFragment cachedFragment = fragmentCache.getElement(key);
+      if (cachedFragment != null) {
+        copyFragment(cachedFragment, result);
+        return;
+      }
+    }
+
+    DocumentFragment fragment;
+    try {
+      fragment = parseFragmentImpl(source);
+    } catch (DOMException e) {
+      // DOMException is a RuntimeException
+      appendParseException(result, e);
+      return;
+    }
+
+    reprocessScriptForOpenSocial(fragment);
+    if (shouldCache) {
+      fragmentCache.addElement(key, fragment);
+    }
+    copyFragment(fragment, result);
+  }
+
+  private void copyFragment(DocumentFragment source, Node dest) {
+    Document destDoc = dest.getOwnerDocument();
+    NodeList nodes = source.getChildNodes();
+    for (int i = 0; i < nodes.getLength(); i++) {
+      Node clone = destDoc.importNode(nodes.item(i), true);
+      dest.appendChild(clone);
+    }
+  }
+
+  protected Document errorDom(DOMException e) {
+    // Create a bare-bones DOM whose body is just error text.
+    // We do this to echo information to the developer that originally
+    // supplied the data, since doing so is more useful than simply
+    // returning a black-box HTML error code stemming from an NPE or other condition downstream.
+    // The method is protected to allow overriding of this behavior.
+    Document doc = documentFactory.createDocument(null, null, null);
+    Node html = doc.createElement("html");
+    html.appendChild(doc.createElement("head"));
+    Node body = doc.createElement("body");
+    appendParseException(body, e);
+    html.appendChild(body);
+    doc.appendChild(html);
+    return doc;
+  }
+
+  private void appendParseException(Node node, DOMException e) {
+    node.appendChild(node.getOwnerDocument().createTextNode(
+        GadgetException.Code.HTML_PARSE_ERROR.toString() + ": " + e.toString()));
+  }
+
+  protected boolean shouldCache() {
+    return documentCache != null && documentCache.getCapacity() != 0;
+  }
+
+  private void reprocessScriptForOpenSocial(Node root) throws GadgetException {
+    LinkedList<Node> nodeQueue = Lists.newLinkedList();
+    nodeQueue.add(root);
+    while (!nodeQueue.isEmpty()) {
+      Node next = nodeQueue.removeFirst();
+      if (next.getNodeType() == Node.ELEMENT_NODE &&
+          "script".equalsIgnoreCase(next.getNodeName())) {
+        Attr typeAttr = (Attr)next.getAttributes().getNamedItem("type");
+        if (typeAttr != null &&
+            SocialDataTags.SCRIPT_TYPE_TO_OSML_TAG.get(typeAttr.getValue()) != null) {
+          String osType = SocialDataTags.SCRIPT_TYPE_TO_OSML_TAG.get(typeAttr.getValue());
+
+          // The underlying parser impl may have already parsed these.
+          // Only re-parse with the coalesced text children wrapped within
+          // the corresponding OSData/OSTemplate tag.
+          boolean parseOs = true;
+          StringBuilder sb = new StringBuilder();
+
+          try {
+            // Convert the <script type="os/*" xmlns=""> node into an equivilant OSML tag
+            // while preserving all attributes (excluding 'type') in the original script node,
+            // including any xmlns attribute. This allows children to be reparsed within the
+            // correct xml namespace.
+            next.getAttributes().removeNamedItem("type");
+
+            HtmlSerialization.printStartElement(osType,
+                next.getAttributes(), sb, /*withXmlClose*/ false);
+
+          } catch (IOException e) {
+            if (LOG.isLoggable(Level.INFO)) {
+              LOG.logp(Level.INFO, classname, "reprocessScriptForOpenSocial", MessageKeys.UNABLE_TO_CONVERT_SCRIPT);
+            }
+          }
+
+          NodeList scriptKids = next.getChildNodes();
+          for (int i = 0; parseOs && i < scriptKids.getLength(); ++i) {
+            Node scriptKid = scriptKids.item(i);
+            if (scriptKid.getNodeType() != Node.TEXT_NODE) {
+              parseOs = false;
+            }
+            sb.append(scriptKid.getTextContent());
+          }
+
+          if (parseOs) {
+            // Clean out the script node.
+            while (next.hasChildNodes()) {
+              next.removeChild(next.getFirstChild());
+            }
+
+            sb.append("</").append(osType).append('>');
+            DocumentFragment osFragment = parseFragmentImpl(sb.toString());
+            while (osFragment.hasChildNodes()) {
+              Node osKid = osFragment.removeChild(osFragment.getFirstChild());
+              osKid = next.getOwnerDocument().adoptNode(osKid);
+              if (osKid.getNodeType() == Node.ELEMENT_NODE) {
+                next.getParentNode().appendChild(osKid);
+              }
+            }
+
+            next.getParentNode().removeChild(next);
+          }
+        }
+      }
+
+      // Enqueue children for inspection.
+      NodeList children = next.getChildNodes();
+      for (int i = 0; i < children.getLength(); ++i) {
+        nodeQueue.add(children.item(i));
+      }
+    }
+  }
+
+  /**
+   * TODO: remove the need for parseDomImpl as a parsing method. Gadget HTML is
+   * tag soup handled in custom fashion, or is a legitimate fragment. In either case,
+   * we can simply use the fragment parsing implementation and patch up in higher-level calls.
+   * @param source a piece of HTML
+   * @return a Document parsed from the HTML
+   * @throws GadgetException
+   */
+  protected abstract Document parseDomImpl(String source)
+      throws GadgetException;
+
+  /**
+   * @param source a snippet of HTML markup
+   * @return a DocumentFragment containing the parsed elements
+   * @throws GadgetException
+   */
+  protected abstract DocumentFragment parseFragmentImpl(String source)
+      throws GadgetException;
+
+  private static class DefaultSerializerProvider implements Provider<HtmlSerializer> {
+    public HtmlSerializer get() {
+      return new DefaultHtmlSerializer();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/HtmlSerialization.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/HtmlSerialization.java
new file mode 100644
index 0000000..b14fd82
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/HtmlSerialization.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import com.google.caja.lexer.escaping.Escaping;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.xerces.xni.QName;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Set;
+
+/**
+ * Static class with helpers to manage serialization of a Document.
+ * Binds an HtmlSerializer to a Document as user data, and pulls it out
+ * to achieve actual serialization.
+ */
+public class HtmlSerialization {
+
+  /**
+   * Used to key an instance of HtmlSerializer in
+   * document.getUserData
+   */
+  public static final String KEY = "serializer";
+
+  /**
+   * Used by a parser to record the original length of the content it parsed
+   * Can be used to optimize output buffers
+   */
+  private static final String ORIGINAL_LENGTH = "original-length";
+
+  public static final Set<String> URL_ATTRIBUTES = ImmutableSet.of("href", "src");
+
+  /**
+   * Attach a serializer instance to the document
+   * @param doc
+   * @param serializer
+   * @param originalContent may be null
+   */
+  public static void attach(Document doc, HtmlSerializer serializer, String originalContent) {
+    doc.setUserData(KEY, serializer, null);
+    if (originalContent != null) {
+      doc.setUserData(ORIGINAL_LENGTH, originalContent.length(), null);
+    }
+  }
+
+  /**
+   * Copy serializer from one document to another. Note this requires that
+   * serializers are thread safe
+   */
+  static void copySerializer(Document from, Document to) {
+    Integer length = (Integer)from.getUserData(ORIGINAL_LENGTH);
+    if (length != null) to.setUserData(ORIGINAL_LENGTH, length, null);
+    to.setUserData(KEY, from.getUserData(KEY), null);
+  }
+
+  /**
+   * Get the length of the original version of the document
+   * @param doc
+   * @return
+   */
+  private static int getOriginalLength(Document doc) {
+    Integer length = (Integer)doc.getUserData(ORIGINAL_LENGTH);
+    if (length == null) return -1;
+    return length;
+  }
+
+  /**
+   * Create a writer sized to the original length of the document
+   * @param doc
+   * @return
+   */
+  public static StringWriter createWriter(Document doc) {
+    int originalLength = getOriginalLength(doc);
+    if (originalLength == -1) {
+      return new StringWriter(8192);
+    } else {
+      // Typically rewriting makes a document larger
+      return new StringWriter((originalLength * 11) / 10);
+    }
+  }
+
+  /**
+   * Call the attached serializer and output the document
+   * @param doc
+   * @return
+   */
+  public static String serialize(Document doc) {
+    return ((HtmlSerializer) doc.getUserData(KEY)).serialize(doc);
+  }
+
+  public static void printEscapedText(CharSequence text, Appendable output) throws IOException {
+    Escaping.escapeXml(text, true, output);
+  }
+
+  /**
+   * Print the start of an HTML element.  If withXmlClose==true, this is an
+   * empty element that should have its content
+   */
+  public static void printStartElement(Element elem, Appendable output, boolean withXmlClose)
+      throws IOException {
+    printStartElement(elem.getTagName(), elem.getAttributes(), output, withXmlClose);
+  }
+
+  public static void printStartElement(String tagName, NamedNodeMap attributes, Appendable output,
+       boolean withXmlClose) throws IOException {
+    output.append("<").append(tagName);
+    for (int i = 0; i < attributes.getLength(); i++) {
+      Attr attr = (Attr)attributes.item(i);
+      String attrName = attr.getNodeName();
+      output.append(' ').append(attrName);
+      if (attr.getNodeValue() != null) {
+        output.append("=\"");
+        if (attr.getNodeValue().length() != 0) {
+          printEscapedText(attr.getNodeValue(), output);
+        }
+        output.append('"');
+      }
+    }
+
+    output.append(withXmlClose ? "/>" : ">");
+  }
+
+
+  public static void outputDocType(DocumentType docType, Appendable output) throws IOException {
+    output.append("<!DOCTYPE ");
+    // Use this so name matches case for XHTML
+    output.append(docType.getOwnerDocument().getDocumentElement().getNodeName());
+    if (docType.getPublicId() != null && docType.getPublicId().length() > 0) {
+      output.append(" ");
+      output.append("PUBLIC ").append('"').append(docType.getPublicId()).append('"');
+    }
+    if (docType.getSystemId() != null && docType.getSystemId().length() > 0) {
+      output.append(" ");
+      output.append('"').append(docType.getSystemId()).append('"');
+    }
+    output.append(">\n");
+  }
+
+  /**
+   * Returns true if the listed attribute is an URL attribute.
+   */
+  public static boolean isUrlAttribute(QName name, String attributeName) {
+    return name.uri == null && URL_ATTRIBUTES.contains(attributeName);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/HtmlSerializer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/HtmlSerializer.java
new file mode 100644
index 0000000..c57aac7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/HtmlSerializer.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import com.google.inject.ImplementedBy;
+
+import org.w3c.dom.Document;
+
+/**
+ * Interface for HTML serializers, which turn a Document into a String.
+ */
+@ImplementedBy(DefaultHtmlSerializer.class)
+public interface HtmlSerializer {
+  String serialize(Document doc);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/ParseModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/ParseModule.java
new file mode 100644
index 0000000..df546b1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/ParseModule.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provider;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.bootstrap.DOMImplementationRegistry;
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * Provide parse bindings
+ */
+public class ParseModule extends AbstractModule {
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  protected void configure() {
+    bind(GadgetHtmlParser.class).to(NekoSimplifiedHtmlParser.class);
+    bind(DOMImplementation.class).toProvider(DOMImplementationProvider.class);
+  }
+
+  /**
+   * Provider of new HTMLDocument implementations. Used to hide XML parser weirdness
+   */
+  public static class DOMImplementationProvider implements Provider<DOMImplementation> {
+
+    DOMImplementation domImpl;
+
+    public DOMImplementationProvider() {
+      try {
+        DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
+        // Require the traversal API
+        domImpl = registry.getDOMImplementation("XML 1.0 Traversal 2.0");
+      } catch (ClassNotFoundException e) {
+        // Try another
+      } catch (InstantiationException e) {
+        // Try another
+      } catch (IllegalAccessException e) {
+        // Try another
+      }
+      // This is ugly but effective
+      try {
+        if (domImpl == null) {
+          domImpl = (DOMImplementation)
+              Class.forName("org.apache.xerces.dom.DOMImplementationImpl").
+                  getMethod("getDOMImplementation").invoke(null);
+        }
+      } catch (ClassNotFoundException ex) {
+        // ignore, try another
+      } catch (IllegalAccessException ex) {
+        // ignore, try another
+      } catch (InvocationTargetException ex) {
+        // ignore, try another
+      } catch (NoSuchMethodException ex) {
+        // ignore, try another
+      }
+      try {
+        if (domImpl == null) {
+        domImpl = (DOMImplementation)
+          Class.forName("com.sun.org.apache.xerces.internal.dom.DOMImplementationImpl").
+              getMethod("getDOMImplementation").invoke(null);
+        }
+      } catch (Exception ex) {
+        throw new RuntimeException("Could not find HTML DOM implementation", ex);
+      }
+    }
+
+    public DOMImplementation get() {
+      return domImpl;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/SocialDataTags.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/SocialDataTags.java
new file mode 100644
index 0000000..11728f2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/SocialDataTags.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import java.util.List;
+
+import org.apache.shindig.common.xml.DomUtil;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+
+import org.w3c.dom.Document;
+
+/**
+ * Helper class containing all defs regarding social data tags,
+ * and one helper method to retrieve all such tags from the document.
+ * Neko's parser implementation disallows script tags with children,
+ * so as a workaround they convert such tags' name to OSData or OSTemplate.
+ * This class provides a helper to select all OSData or OSTemplate tags,
+ * irrespective whether this conversion occurred, ensuring that
+ * data pipelining and templating can work with any parser.
+ *
+ * @since 2.0.0
+ */
+public final class SocialDataTags {
+  private SocialDataTags() { }
+
+  /**
+   * Allowed tag names for OpenSocial Data and template blocks.
+   * Make the tag names lower case since they're normalized by
+   * the caja http://code.google.com/p/google-caja/issues/detail?id=1272
+   * Another approach is to namespace them but that causes other issues.
+   */
+  public static final String OSML_DATA_TAG = "osdata";
+  public static final String OSML_TEMPLATE_TAG = "ostemplate";
+
+  /**
+   * Bi-map of OpenSocial tags to their script type attribute values.
+   */
+  public static final BiMap<String, String> SCRIPT_TYPE_TO_OSML_TAG = ImmutableBiMap.of(
+      "text/os-data", OSML_DATA_TAG, "text/os-template", OSML_TEMPLATE_TAG);
+
+  public static List<Element> getTags(Document doc, String tagName) {
+    NodeList list = doc.getElementsByTagName(tagName);
+    List<Element> elements = Lists.newArrayListWithExpectedSize(list.getLength());
+    for (int i = 0; i < list.getLength(); i++) {
+      elements.add((Element) list.item(i));
+    }
+
+    // Add equivalent <script> elements
+    String scriptType = SCRIPT_TYPE_TO_OSML_TAG.inverse().get(tagName);
+    if (scriptType != null) {
+      List<Element> scripts =
+          DomUtil.getElementsByTagNameCaseInsensitive(doc, ImmutableSet.of("script"));
+      for (Element script : scripts) {
+        Attr typeAttr = (Attr)script.getAttributes().getNamedItem("type");
+        if (typeAttr != null && scriptType.equalsIgnoreCase(typeAttr.getValue())) {
+          elements.add(script);
+        }
+      }
+    }
+    return elements;
+  }
+
+  public static boolean isOpenSocialScript(Element script) {
+    Attr typeAttr = (Attr)script.getAttributes().getNamedItem("type");
+    return (typeAttr != null && typeAttr.getValue() != null &&
+            SCRIPT_TYPE_TO_OSML_TAG.containsKey(typeAttr.getValue()));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssLexerParser.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssLexerParser.java
new file mode 100644
index 0000000..1bcb27d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssLexerParser.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.CssLexer;
+import com.google.caja.lexer.CssTokenType;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.ParseException;
+import com.google.caja.lexer.Token;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.URI;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A parser that records the stream of CSS lexial tokens from the Caja lexer and creates a
+ * pseudo-DOM from that stream.
+ *
+ * TODO: Remove once Caja CSS DOM parser issues are resolved.
+ */
+public class CajaCssLexerParser {
+
+  private static final Pattern urlMatcher =
+      Pattern.compile("(url\\s*\\(\\s*['\"]?)([^\\)'\"]*)(['\"]?\\s*\\))",
+          Pattern.CASE_INSENSITIVE);
+
+  private static final URI DUMMY_SOURCE = URI.create("http://www.example.org");
+
+  public static final String CACHE_NAME = "parsedCss";
+
+  private Cache<String, List<Object>> parsedCssCache;
+
+  @Inject
+  public void setCacheProvider(CacheProvider cacheProvider) {
+    parsedCssCache = cacheProvider.createCache(CACHE_NAME);
+  }
+
+  public List<Object> parse(String content) throws GadgetException {
+    List<Object> parsedCss = null;
+    boolean shouldCache = shouldCache();
+    String key = null;
+    if (shouldCache) {
+      // TODO - Consider using the source if its under a certain size
+      key = HashUtil.checksum(content.getBytes());
+      parsedCss = parsedCssCache.getElement(key);
+    }
+    if (parsedCss == null) {
+      parsedCss = parseImpl(content);
+      if (shouldCache) {
+        parsedCssCache.addElement(key, parsedCss);
+      }
+    }
+
+    if (shouldCache) {
+      List<Object> cloned = Lists.newArrayListWithCapacity(parsedCss.size());
+      for (Object o : parsedCss) {
+        if (o instanceof ImportDecl) {
+          cloned.add(new ImportDecl(((ImportDecl) o).getUri()));
+        } else if (o instanceof UriDecl) {
+          cloned.add(new UriDecl(((UriDecl) o).getUri()));
+        } else {
+          cloned.add(o);
+        }
+      }
+      return cloned;
+    }
+    return parsedCss;
+  }
+
+  List<Object> parseImpl(String content) throws GadgetException {
+    List<Object> parsedCss = Lists.newArrayList();
+    CharProducer producer = CharProducer.Factory.create(new StringReader(content),
+        new InputSource(DUMMY_SOURCE));
+    CssLexer lexer = new CssLexer(producer);
+    try {
+      StringBuilder builder = new StringBuilder();
+      boolean inImport = false;
+      while (lexer.hasNext()) {
+        Token<CssTokenType> token = lexer.next();
+        if (token.type == CssTokenType.SYMBOL && token.text.equalsIgnoreCase("@import")) {
+          parsedCss.add(builder.toString());
+          builder.setLength(0);
+          inImport = true;
+        } else if (inImport) {
+          if (token.type == CssTokenType.URI) {
+            parsedCss.add(builder.toString());
+            builder.setLength(0);
+            Matcher matcher = urlMatcher.matcher(token.text);
+            if (matcher.find()) {
+              parsedCss.add(new ImportDecl(matcher.group(2).trim()));
+            }
+          } else if (token.type != CssTokenType.SPACE && token.type != CssTokenType.PUNCTUATION) {
+            inImport = false;
+            builder.append(token.text);
+          } else {
+            //builder.append(token.text);
+          }
+        } else if (token.type == CssTokenType.URI) {
+          Matcher matcher = urlMatcher.matcher(token.text);
+          if (!matcher.find()) {
+            builder.append(token.text);
+          } else {
+            parsedCss.add(builder.toString());
+            builder.setLength(0);
+            parsedCss.add(new UriDecl(matcher.group(2).trim()));
+          }
+        } else {
+          builder.append(token.text);
+        }
+      }
+      parsedCss.add(builder.toString());
+    } catch (ParseException pe) {
+      throw new GadgetException(GadgetException.Code.CSS_PARSE_ERROR, pe,
+          HttpResponse.SC_BAD_REQUEST);
+    }
+    return parsedCss;
+  }
+
+  /** Serialize a stylesheet to a String */
+  public String serialize(List<Object> styleSheet) {
+    StringWriter writer = new StringWriter();
+    serialize(styleSheet, writer);
+    return writer.toString();
+  }
+
+  /** Serialize a stylesheet to a Writer. */
+  public void serialize(List<Object> styleSheet, Appendable writer) {
+    try {
+      for (Object o : styleSheet) {
+        writer.append(o.toString());
+      }
+    } catch (IOException ioe) {
+      throw new RuntimeException(ioe);
+    }
+  }
+
+
+  private boolean shouldCache() {
+    return parsedCssCache != null && parsedCssCache.getCapacity() != 0;
+  }
+
+  public static class ImportDecl {
+
+    private String uri;
+
+    public ImportDecl(String uri) {
+      this.uri = uri;
+    }
+
+    public String getUri() {
+      return uri;
+    }
+
+    public void setUri(String uri) {
+      this.uri = uri;
+    }
+
+    @Override
+    public String toString() {
+      return "@import url('" + uri + "');\n";
+    }
+  }
+
+  public static class UriDecl {
+
+    private String uri;
+
+    public UriDecl(String uri) {
+      this.uri = uri;
+    }
+
+    public String getUri() {
+      return uri;
+    }
+
+    public void setUri(String uri) {
+      this.uri = uri;
+    }
+
+    @Override
+    public String toString() {
+      return "url('" + uri + "')";
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssParser.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssParser.java
new file mode 100644
index 0000000..2976906
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssParser.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.CssLexer;
+import com.google.caja.lexer.CssTokenType;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.ParseException;
+import com.google.caja.lexer.Token;
+import com.google.caja.lexer.TokenQueue;
+import com.google.caja.lexer.TokenStream;
+import com.google.caja.parser.css.CssParser;
+import com.google.caja.parser.css.CssTree;
+import com.google.caja.render.Concatenator;
+import com.google.caja.render.CssPrettyPrinter;
+import com.google.caja.reporting.MessageLevel;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.RenderContext;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.caja.util.Criterion;
+import com.google.inject.Inject;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Collections;
+
+/**
+ * A CSS DOM parser using Caja.
+ */
+public class CajaCssParser {
+
+  /**
+   * Fake URI source if one is not provided by the calling context.
+   */
+  private static final Uri FAKE_SOURCE = Uri.parse("http://a.dummy.url");
+
+  private static final String PARSED_CSS = "parsedCss";
+
+  private Cache<String, CssTree.StyleSheet> parsedCssCache;
+
+  @Inject
+  public void setCacheProvider(CacheProvider cacheProvider) {
+    parsedCssCache = cacheProvider.createCache(PARSED_CSS);
+  }
+
+  /**
+   * Parse CSS content into Caja's CSS DOM model
+   *
+   * @return A parsed stylesheet
+   */
+  public CssTree.StyleSheet parseDom(String content) throws GadgetException {
+    // Use a fake source if the real source is unknown
+    return parseDom(content, FAKE_SOURCE);
+  }
+
+  public CssTree.StyleSheet parseDom(String content, Uri source)
+      throws GadgetException {
+    CssTree.StyleSheet parsedCss = null;
+    boolean shouldCache = shouldCache();
+    String key = null;
+    if (shouldCache) {
+      // TODO - Consider using the source if its under a certain size
+      key = HashUtil.checksum(content.getBytes());
+      parsedCss = parsedCssCache.getElement(key);
+    }
+    if (parsedCss == null) {
+      try {
+        parsedCss = parseImpl(content, source);
+        if (shouldCache) {
+          parsedCssCache.addElement(key, parsedCss);
+        }
+      } catch (ParseException pe) {
+        // Bad input; not server's fault.
+        throw new GadgetException(GadgetException.Code.CSS_PARSE_ERROR, pe,
+            HttpResponse.SC_BAD_REQUEST);
+      }
+    }
+    if (shouldCache) {
+      return (CssTree.StyleSheet)parsedCss.clone();
+    }
+    return parsedCss;
+  }
+
+  private CssTree.StyleSheet parseImpl(String css, Uri source)
+      throws ParseException {
+    InputSource inputSource = new InputSource(source.toJavaUri());
+    CharProducer producer = CharProducer.Factory.create(new StringReader(css),
+        inputSource);
+    TokenStream<CssTokenType> lexer = new CssLexer(producer);
+    TokenQueue<CssTokenType> queue = new TokenQueue<CssTokenType>(lexer, inputSource,
+        new Criterion<Token<CssTokenType>>() {
+          public boolean accept(Token<CssTokenType> t) {
+            return CssTokenType.SPACE != t.type
+                && CssTokenType.COMMENT != t.type;
+          }
+        });
+    if (queue.isEmpty()) {
+      // Return empty stylesheet
+      return new CssTree.StyleSheet(null, Collections.<CssTree.CssStatement>emptyList());
+    }
+    MessageQueue mq = new SimpleMessageQueue();
+    CssParser parser = new CssParser(queue, mq, MessageLevel.WARNING);
+    return parser.parseStyleSheet();
+  }
+
+  /** Serialize a stylesheet to a String */
+  public String serialize(CssTree.StyleSheet styleSheet) {
+    StringWriter writer = new StringWriter();
+    serialize(styleSheet, writer);
+    return writer.toString();
+  }
+
+  /** Serialize a stylesheet to a Writer. */
+  public void serialize(CssTree.StyleSheet styleSheet, Writer writer) {
+    CssPrettyPrinter cssPrinter = new CssPrettyPrinter(new Concatenator(writer, null));
+    styleSheet.render(new RenderContext(cssPrinter));
+    cssPrinter.noMoreTokens();
+  }
+
+  private boolean shouldCache() {
+    return parsedCssCache != null && parsedCssCache.getCapacity() != 0;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java
new file mode 100644
index 0000000..7b9c946
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizer.java
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import com.google.common.base.Strings;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import com.google.caja.lang.css.CssSchema;
+import com.google.caja.parser.AbstractParseTreeNode;
+import com.google.caja.parser.AncestorChain;
+import com.google.caja.parser.Visitor;
+import com.google.caja.parser.css.CssTree;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+
+import org.w3c.dom.Element;
+
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Sanitize a CSS tree using Caja. Strip properties and functions that represent
+ * ways to execute script. Specifically
+ *
+ * - Use Caja's CSS property whitelist
+ * - Use Caja's CSS function whitelist
+ * - Force @import through the proxy and require sanitization. If they cant be parsed, remove them
+ * - Force @url references to have the HTTP/HTTPS protocol
+ */
+public class CajaCssSanitizer {
+  //class name for logging purpose
+  private static final String classname = CajaCssSanitizer.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private static final Set<String> ALLOWED_URI_SCHEMES = ImmutableSet.of("http", "https");
+
+  private final CajaCssParser parser;
+
+  private final CssSchema schema;
+
+  @Inject
+  public CajaCssSanitizer(CajaCssParser parser) {
+    this.parser = parser;
+    schema = CssSchema.getDefaultCss21Schema(new SimpleMessageQueue());
+  }
+
+  /**
+   * Sanitize the CSS content of a style tag.
+   * @param content to sanitize
+   * @param linkContext url of containing content
+   * @param gadgetContext The gadget context.
+   * @param importRewriter to rewrite @imports to sanitizing proxy
+   * @param imageRewriter to rewrite images to sanitizing proxy
+   * @return Sanitized css.
+   */
+  public String sanitize(String content, Uri linkContext, GadgetContext gadgetContext,
+                         ProxyUriManager importRewriter, ProxyUriManager imageRewriter) {
+    try {
+      CssTree.StyleSheet stylesheet = parser.parseDom(content, linkContext);
+      sanitize(stylesheet, linkContext, gadgetContext, importRewriter, imageRewriter);
+      // Write the rewritten CSS back into the element
+      return parser.serialize(stylesheet);
+    } catch (GadgetException ge) {
+      // Failed to parse stylesheet so log and continue
+      if (LOG.isLoggable(Level.INFO)) {
+        LOG.logp(Level.INFO, classname, "sanitize", MessageKeys.FAILED_TO_PARSE);
+        LOG.log(Level.INFO, ge.getMessage(), ge);
+      }
+      return "";
+    }
+  }
+
+  /**
+   * Sanitize the CSS content of a style tag.
+   * @param styleElem to sanitize
+   * @param linkContext url of containing content
+   * @param gadgetContext The gadget context.
+   * @param importRewriter to rewrite @imports to sanitizing proxy
+   * @param imageRewriter to rewrite images to sanitizing proxy
+   */
+  public void sanitize(Element styleElem, Uri linkContext, GadgetContext gadgetContext,
+                       ProxyUriManager importRewriter, ProxyUriManager imageRewriter) {
+    String content = null;
+    try {
+      CssTree.StyleSheet stylesheet =
+        parser.parseDom(styleElem.getTextContent(), linkContext);
+      sanitize(stylesheet, linkContext, gadgetContext, importRewriter, imageRewriter);
+      // Write the rewritten CSS back into the element
+      content = parser.serialize(stylesheet);
+    } catch (GadgetException ge) {
+      // Failed to parse stylesheet so log and continue
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "sanitize", MessageKeys.FAILED_TO_PARSE);
+          LOG.log(Level.INFO, ge.getMessage(), ge);
+        }
+    }
+    if (Strings.isNullOrEmpty(content)) {
+      // Remove the owning node
+      styleElem.getParentNode().removeChild(styleElem);
+    } else {
+      styleElem.setTextContent(content);
+    }
+  }
+
+  /**
+   * Sanitize the given CSS tree in-place by removing all non-whitelisted function calls
+   * @param css DOM root
+   * @param linkContext url of containing content
+   * @param gadgetContext The gadget context.
+   * @param importRewriter to rewrite links to sanitizing proxy
+   * @param imageRewriter to rewrite links to the sanitizing proxy
+   */
+  public void sanitize(CssTree css, final Uri linkContext, final GadgetContext gadgetContext,
+                       final ProxyUriManager importRewriter, final ProxyUriManager imageRewriter) {
+    css.acceptPreOrder(new Visitor() {
+      public boolean visit(AncestorChain<?> ancestorChain) {
+        if (ancestorChain.node instanceof CssTree.Property) {
+          if (!schema.isPropertyAllowed(((CssTree.Property) ancestorChain.node).
+              getPropertyName())) {
+            // Remove offending property
+            if (LOG.isLoggable(Level.FINE)) {
+              LOG.log(Level.FINE, "Removing property "
+                  + ((CssTree.Property) ancestorChain.node).getPropertyName());
+            }
+            clean(ancestorChain);
+          }
+        } else if (ancestorChain.node instanceof CssTree.FunctionCall) {
+          if (!schema.isFunctionAllowed(((CssTree.FunctionCall)ancestorChain.node).getName())) {
+            // Remove offending node
+            if (LOG.isLoggable(Level.FINE)) {
+              LOG.log(Level.FINE, "Removing function "
+                  + ((CssTree.FunctionCall) ancestorChain.node).getName());
+            }
+            clean(ancestorChain);
+          }
+        } else if (ancestorChain.node instanceof CssTree.UriLiteral &&
+            !(ancestorChain.getParentNode() instanceof CssTree.Import)) {
+          String uri = ((CssTree.UriLiteral)ancestorChain.node).getValue();
+          if (isValidUri(uri)) {
+            // Assume the URI is for an image. Rewrite it using the image link rewriter
+            ((CssTree.UriLiteral)ancestorChain.node).setValue(
+                rewriteUri(imageRewriter, uri, linkContext, gadgetContext));
+          } else {
+            // Remove offending node
+            if (LOG.isLoggable(Level.FINE)) {
+              LOG.log(Level.FINE, "Removing invalid URI " + uri);
+            }
+            clean(ancestorChain);
+          }
+        } else if (ancestorChain.node instanceof CssTree.Import) {
+          CssTree.Import importDecl = (CssTree.Import) ancestorChain.node;
+          String uri = importDecl.getUri().getValue();
+          if (isValidUri(uri)) {
+            importDecl.getUri().setValue(rewriteUri(importRewriter, uri, linkContext,
+                gadgetContext));
+          } else {
+            if (LOG.isLoggable(Level.FINE)) {
+              LOG.log(Level.FINE, "Removing invalid URI " + uri);
+            }
+            clean(ancestorChain);
+          }
+        }
+        return true;
+      }
+    }, null);
+  }
+
+  private static String rewriteUri(ProxyUriManager proxyUriManager, String input,
+                                   final Uri context, GadgetContext gadgetContext) {
+    Uri inboundUri;
+    try {
+      inboundUri = Uri.parse(input);
+    } catch (IllegalArgumentException e) {
+      // Don't rewrite at all.
+      return input;
+    }
+    if (context != null) {
+      inboundUri = context.resolve(inboundUri);
+    }
+
+    List<ProxyUriManager.ProxyUri> uris = ImmutableList.of(
+        new ProxyUriManager.ProxyUri(DomWalker.makeGadget(new GadgetContext(gadgetContext) {
+          @Override
+          public Uri getUrl() {
+            return context;
+          }
+        }), inboundUri));
+    List<Uri> rewritten = proxyUriManager.make(uris, null);
+    return rewritten.get(0).toString();
+  }
+
+  private boolean isValidUri(String uri) {
+    try {
+      String scheme = Uri.parse(uri).getScheme();
+      return Strings.isNullOrEmpty(scheme) ||
+          ALLOWED_URI_SCHEMES.contains(scheme.toLowerCase());
+    } catch (RuntimeException re) {
+      if (LOG.isLoggable(Level.FINE)) {
+        LOG.log(Level.FINE, "Failed to parse URI in CSS " + uri, re);
+      }
+    }
+    return false;
+  }
+
+  /**
+   * recurse up through chain to find a safe clean point
+   * @param chain chain of nodes
+   */
+  private static void clean(AncestorChain<?> chain) {
+    if (chain == null) {
+      return;
+    }
+    if (chain.node instanceof CssTree.Declaration ||
+        chain.node instanceof CssTree.Import) {
+      if (chain.getParentNode() instanceof CssTree.UserAgentHack) {
+        clean(chain.parent);
+      } else {
+        // Remove the entire subtree
+        ((AbstractParseTreeNode)chain.getParentNode()).removeChild(chain.node);
+      }
+    } else {
+      clean(chain.parent);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssUtils.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssUtils.java
new file mode 100644
index 0000000..645bd93
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaCssUtils.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import com.google.caja.parser.AncestorChain;
+import com.google.caja.parser.Visitor;
+import com.google.caja.parser.css.CssTree;
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+/**
+ * Utility functions for traversing Caja's CSS DOM
+ */
+public final class CajaCssUtils {
+  private CajaCssUtils() {}
+
+  /**
+   * Get the immediate children of the passed node with the specified node type
+   */
+  public static <T extends CssTree> List<T> children(CssTree node, Class<T> nodeType) {
+    List<T> result = Lists.newArrayList();
+    for (CssTree child : node.children()) {
+      if (nodeType.isAssignableFrom(child.getClass())) {
+        result.add(nodeType.cast(child));
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Get all descendants of the passed node with the specified node type
+   */
+  public static <T extends CssTree> List<T> descendants(CssTree node, final Class<T> nodeType) {
+    final List<T> descendants = Lists.newArrayList();
+    node.acceptPreOrder(new Visitor() {
+      public boolean visit(AncestorChain<?> ancestorChain) {
+        if (nodeType.isAssignableFrom(ancestorChain.node.getClass())) {
+          descendants.add(nodeType.cast(ancestorChain.node));
+        }
+        return true;
+      }
+    }, null);
+    return descendants;
+  }
+}
+
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaHtmlParser.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaHtmlParser.java
new file mode 100644
index 0000000..1c906b4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaHtmlParser.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import java.util.LinkedList;
+
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.HtmlLexer;
+import com.google.caja.lexer.HtmlTokenType;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.ParseException;
+import com.google.caja.lexer.TokenQueue;
+import com.google.caja.parser.html.DomParser;
+import com.google.caja.parser.html.Namespaces;
+import com.google.caja.reporting.Message;
+import com.google.caja.reporting.MessageLevel;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.apache.shindig.gadgets.parse.HtmlSerializer;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Node;
+
+public class CajaHtmlParser extends GadgetHtmlParser {
+  private static final String OSML_DATA_START = '<' + SocialDataTags.OSML_DATA_TAG;
+  private static final String OSML_TEMPLATE_START = '<' + SocialDataTags.OSML_TEMPLATE_TAG;
+
+  @Inject
+  public CajaHtmlParser(DOMImplementation documentFactory) {
+    super(documentFactory);
+  }
+
+  public CajaHtmlParser(DOMImplementation documentFactory,
+      HtmlSerializer serializer) {
+    super(documentFactory, serializer);
+  }
+
+  @Override
+  protected Document parseDomImpl(String source) throws GadgetException {
+    DocumentFragment fragment = parseFragmentImpl(source);
+
+    // TODO: remove parseDomImpl() altogether; only have subclasses
+    // support parseFragmentImpl() with base class cleaning up.
+    Document document = fragment.getOwnerDocument();
+    CajaHtmlSerializer serializer = new CajaHtmlSerializer();
+    HtmlSerialization.attach(document, serializer, null);
+    Node html = null;
+    LinkedList<Node> beforeHtml = Lists.newLinkedList();
+    while (fragment.hasChildNodes()) {
+      Node child = fragment.removeChild(fragment.getFirstChild());
+      if (child.getNodeType() == Node.ELEMENT_NODE &&
+          "html".equalsIgnoreCase(child.getNodeName())) {
+        if (html == null) {
+          html = child;
+        } else {
+          // Ignore the current (duplicated) html node but add its children
+          transferChildren(html, child);
+        }
+      } else if (html != null) {
+        html.appendChild(child);
+      } else {
+        beforeHtml.add(child);
+      }
+    }
+
+    if (html == null) {
+      html = document.createElement("html");
+    }
+
+    prependToNode(html, beforeHtml);
+
+    // Ensure document.getDocumentElement() is html node.
+    document.appendChild(html);
+
+    return document;
+  }
+
+  @Override
+  protected DocumentFragment parseFragmentImpl(String source)
+      throws GadgetException {
+    try {
+      MessageQueue mq = makeMessageQueue();
+
+      DomParser parser = getDomParser(source, mq);
+      DocumentFragment fragment = parser.parseFragment();
+
+      if (mq.hasMessageAtLevel(MessageLevel.ERROR)) {
+        StringBuilder err = new StringBuilder();
+        for (Message m : mq.getMessages()) {
+          err.append(m.toString()).append('\n');
+        }
+        throw new GadgetException(GadgetException.Code.HTML_PARSE_ERROR, err.toString(),
+            HttpResponse.SC_BAD_REQUEST);
+      }
+      return fragment;
+    } catch (ParseException e) {
+      throw new GadgetException(
+          GadgetException.Code.HTML_PARSE_ERROR, e.getCajaMessage().toString(),
+          HttpResponse.SC_BAD_REQUEST);
+    }
+  }
+
+  protected InputSource getInputSource() {
+    // Returns a default/dummy InputSource.
+    // We might consider adding the gadget URI to the GadgetHtmlParser API,
+    // but in the meantime this method is protected to allow overriding this
+    // with request-scoped retrieval of this same data.
+    return InputSource.UNKNOWN;
+  }
+
+  protected MessageQueue makeMessageQueue() {
+    return new SimpleMessageQueue();
+  }
+
+  protected boolean needsDebugData() {
+    return false;
+  }
+
+  private DomParser getDomParser(String source, final MessageQueue mq) throws ParseException {
+    InputSource is = getInputSource();
+    HtmlLexer lexer = new HtmlLexer(CharProducer.Factory.fromString(source, is));
+    TokenQueue<HtmlTokenType> tokenQueue = new TokenQueue<HtmlTokenType>(lexer, is);
+    final Namespaces ns = Namespaces.HTML_DEFAULT;  // Includes OpenSocial
+    final boolean needsDebugData = needsDebugData();
+
+    // OpenSocial Tempates need to be parsed as XML since tags can be self-closing.
+    final boolean asXml =
+        (source.startsWith(OSML_DATA_START) || source.startsWith(OSML_TEMPLATE_START));
+    DomParser parser = new DomParser(tokenQueue, asXml, mq);
+    parser.setDomImpl(documentFactory);
+    parser.setNeedsDebugData(needsDebugData);
+    return parser;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaHtmlSerializer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaHtmlSerializer.java
new file mode 100644
index 0000000..c2b44cb
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/CajaHtmlSerializer.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.gadgets.parse.HtmlSerializer;
+import org.w3c.dom.Document;
+
+import com.google.caja.parser.html.Nodes;
+import com.google.caja.reporting.MarkupRenderMode;
+
+/**
+ * HtmlSerializer using Caja's Nodes.render(...) method under the hood.
+ *
+ * @since 2.0.0
+ */
+public class CajaHtmlSerializer implements HtmlSerializer {
+  public String serialize(Document doc) {
+    if (doc.getDoctype() != null) {
+      return Nodes.render(doc.getDoctype(), doc, MarkupRenderMode.HTML);
+    } else {
+      return Nodes.render(doc, MarkupRenderMode.HTML);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlParser.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlParser.java
new file mode 100644
index 0000000..92739e2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlParser.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import com.google.caja.lexer.*;
+import com.google.caja.parser.html.DomParser;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+
+/**
+ * Simple html parser based on caja.
+ */
+public class VanillaCajaHtmlParser extends GadgetHtmlParser {
+  private final boolean needsDebugData;
+
+  @Inject
+  public VanillaCajaHtmlParser(DOMImplementation documentFactory,
+                               @Named("vanillaCajaParser.needsDebugData")
+                               boolean needsDebugData) {
+    super(documentFactory);
+    this.needsDebugData = needsDebugData;
+  }
+
+  @Override
+  public Document parseDom(String source) throws GadgetException {
+    // TODO: Add support for caching the DOM after evaluation.
+    return parseDomImpl(source);
+  }
+
+  private DomParser getDomParser(String source, final MessageQueue mq) throws ParseException {
+    InputSource is = InputSource.UNKNOWN;
+    HtmlLexer lexer = new HtmlLexer(CharProducer.Factory.fromString(source, is));
+    TokenQueue<HtmlTokenType> tokenQueue = new TokenQueue<HtmlTokenType>(
+        lexer, is);
+    DomParser parser = new DomParser(tokenQueue, /** asXml */ false, mq);
+
+    parser.setDomImpl(documentFactory);
+    parser.setNeedsDebugData(needsDebugData);
+    return parser;
+  }
+
+  @Override
+  protected Document parseDomImpl(String source) throws GadgetException {
+    MessageQueue mq = new SimpleMessageQueue();
+    try {
+      DomParser parser = getDomParser(source, mq);
+      Document doc = parser.parseDocument().getOwnerDocument();
+
+      VanillaCajaHtmlSerializer serializer = new VanillaCajaHtmlSerializer();
+      HtmlSerialization.attach(doc, serializer, null);
+      return doc;
+    } catch (ParseException e) {
+      throw new GadgetException(GadgetException.Code.HTML_PARSE_ERROR,
+          e.getCajaMessage().toString(), HttpResponse.SC_INTERNAL_SERVER_ERROR);
+    } catch (NullPointerException e) {
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e);
+    }
+  }
+
+  @Override
+  protected DocumentFragment parseFragmentImpl(String source)
+      throws GadgetException {
+    throw new UnsupportedOperationException("Use parseDom instead.");
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlSerializer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlSerializer.java
new file mode 100644
index 0000000..84a670a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlSerializer.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import com.google.caja.parser.html.Nodes;
+import com.google.caja.render.Concatenator;
+import com.google.caja.reporting.MarkupRenderMode;
+import com.google.caja.reporting.RenderContext;
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.apache.shindig.gadgets.parse.HtmlSerializer;
+import org.w3c.dom.Document;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+/**
+ * Serializer for VanillaCajaHtmlParser.
+ */
+public class VanillaCajaHtmlSerializer implements HtmlSerializer {
+  public String serialize(Document doc) {
+    try {
+      StringWriter sw = HtmlSerialization.createWriter(doc);
+      if (doc.getDoctype() != null) {
+        HtmlSerialization.outputDocType(doc.getDoctype(), sw);
+      }
+      RenderContext renderContext =
+          new RenderContext(new Concatenator(sw, null))
+              // More compact but needs charset set correctly.
+              .withAsciiOnly(false)
+              .withMarkupRenderMode(MarkupRenderMode.HTML);
+
+      // Use render unsafe in order to retain comments in the serialized HTML.
+      // TODO: This function is deprecated. Use a non-deprecated function.
+      Nodes.renderUnsafe(doc, renderContext);
+      return sw.toString();
+    } catch (IOException e) {
+      return null;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java
new file mode 100644
index 0000000..b6a7129
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/NekoSimplifiedHtmlParser.java
@@ -0,0 +1,521 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.nekohtml;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.apache.xerces.xni.Augmentations;
+import org.apache.xerces.xni.NamespaceContext;
+import org.apache.xerces.xni.QName;
+import org.apache.xerces.xni.XMLAttributes;
+import org.apache.xerces.xni.XMLDocumentHandler;
+import org.apache.xerces.xni.XMLLocator;
+import org.apache.xerces.xni.XMLResourceIdentifier;
+import org.apache.xerces.xni.XMLString;
+import org.apache.xerces.xni.XNIException;
+import org.apache.xerces.xni.parser.XMLDocumentSource;
+import org.apache.xerces.xni.parser.XMLInputSource;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import org.cyberneko.html.HTMLConfiguration;
+import org.cyberneko.html.HTMLElements;
+import org.cyberneko.html.HTMLEntities;
+import org.cyberneko.html.HTMLScanner;
+import org.cyberneko.html.HTMLTagBalancer;
+import org.cyberneko.html.filters.NamespaceBinder;
+import org.w3c.dom.DOMException;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Stack;
+
+/**
+ * Supports parsing of social markup blocks inside gadget content.
+ * &lt;script&gt; elements with types of either "text/os-template"
+ * or "text/os-data" are parsed inline into contained DOM hierarchies
+ * for subsequent processing by the pipeline and template rewriters.
+ */
+@Singleton
+public class NekoSimplifiedHtmlParser extends GadgetHtmlParser {
+
+  private static final HTMLElements.Element OSML_TEMPLATE_ELEMENT;
+  private static final HTMLElements.Element OSML_DATA_ELEMENT;
+
+  static {
+    HTMLElements.Element unknown = HTMLElements.getElement(HTMLElements.UNKNOWN);
+    OSML_TEMPLATE_ELEMENT = new HTMLElements.Element(unknown.code,
+        SocialDataTags.OSML_TEMPLATE_TAG, unknown.flags, HTMLElements.BODY, unknown.closes);
+    // Passing parent in constructor is ignored.
+    // Only allow template tags in BODY
+    OSML_TEMPLATE_ELEMENT.parent =
+        new HTMLElements.Element[]{HTMLElements.getElement(HTMLElements.BODY)};
+
+    // data tags are allowed in BODY only, since Neko disallows HEAD elements from
+    // having child elements of their own.
+    OSML_DATA_ELEMENT = new HTMLElements.Element(unknown.code,
+        SocialDataTags.OSML_TEMPLATE_TAG, unknown.flags, HTMLElements.BODY, unknown.closes);
+    OSML_DATA_ELEMENT.parent = new HTMLElements.Element[]{
+        HTMLElements.getElement(HTMLElements.BODY)};
+  }
+
+
+  @Inject
+  public NekoSimplifiedHtmlParser(DOMImplementation documentFactory) {
+    super(documentFactory);
+  }
+
+  @Override
+  protected Document parseDomImpl(String source) throws GadgetException {
+    DocumentHandler handler;
+
+    HTMLConfiguration config = newConfiguration();
+    try {
+      handler = parseHtmlImpl(source, config, new NormalizingTagBalancer());
+    } catch (IOException ioe) {
+      return null;
+    }
+
+    Document document = handler.getDocument();
+    document.appendChild(DomUtil.getFirstNamedChildNode(handler.getFragment(), "html"));
+    fixNekoWeirdness(document);
+    return document;
+  }
+
+  @Override
+  protected DocumentFragment parseFragmentImpl(String source) throws GadgetException {
+    DocumentHandler handler;
+
+    HTMLConfiguration config = newConfiguration();
+    // http://cyberneko.org/html/features/balance-tags/document-fragment
+    // deprecated http://cyberneko.org/html/features/document-fragment
+    config.setFeature("http://cyberneko.org/html/features/balance-tags/document-fragment", true);
+    config.setProperty("http://cyberneko.org/html/properties/balance-tags/fragment-context-stack",
+        new QName[]{new QName(null, "HTML", "HTML", null), new QName(null, "BODY", "BODY", null)});
+
+    try {
+      handler = parseHtmlImpl(source, config, new NekoPatchTagBalancer());
+    } catch (IOException ioe) {
+      return null;
+    }
+
+    return handler.getFragment();
+  }
+
+  /**
+   * Parse HTML source.
+   *
+   * @return a document handler containing the parsed source
+   */
+  private DocumentHandler parseHtmlImpl(String source, HTMLConfiguration config,
+      NormalizingTagBalancer tagBalancer)
+      throws IOException {
+
+    HTMLScanner htmlScanner = new HTMLScanner();
+    tagBalancer.setScanner(htmlScanner);
+
+    DocumentHandler handler = newDocumentHandler(source);
+
+    NamespaceBinder namespaceBinder = new NamespaceBinder();
+    namespaceBinder.setDocumentHandler(handler);
+    namespaceBinder.setDocumentSource(tagBalancer);
+    namespaceBinder.reset(config);
+    tagBalancer.setDocumentHandler(namespaceBinder);
+
+    // Order of filter is Scanner -> OSMLFilter -> Tag Balancer
+    tagBalancer.setDocumentSource(htmlScanner);
+    htmlScanner.setDocumentHandler(tagBalancer);
+
+    tagBalancer.reset(config);
+    htmlScanner.reset(config);
+
+    XMLInputSource inputSource = new XMLInputSource(null, null, null);
+    inputSource.setEncoding("UTF-8");
+    inputSource.setCharacterStream(new StringReader(source));
+    htmlScanner.setInputSource(inputSource);
+    htmlScanner.scanDocument(true);
+    return handler;
+  }
+
+  private void fixNekoWeirdness(Document document) {
+    // Neko as of versions > 1.9.13 stuffs all leading <script> nodes into <head>.
+    // This breaks all sorts of assumptions in gadgets, notably the existence of document.body.
+    // We can't tell Neko to avoid putting <script> into <head> however, since gadgets
+    // like <Content><script>...</script><style>...</style> will break due to both
+    // <script> and <style> ending up in <body> -- at which point Neko unceremoniously
+    // drops the <style> (and <link>) elements.
+    // Therefore we just search for <script> elements in <head> and stuff them all into
+    // the top of <body>.
+    // This method assumes a normalized document as input.
+    Node html = DomUtil.getFirstNamedChildNode(document, "html");
+    if (html.getNextSibling() != null &&
+        html.getNextSibling().getNodeName().equalsIgnoreCase("html")) {
+      // if a doctype is specified, then the desired root <html> node is wrapped by an <HTML> node
+      // Pull out the <html> root.
+      html = html.getNextSibling();
+    }
+    Node head = DomUtil.getFirstNamedChildNode(html, "head");
+    if (head == null) {
+      head = document.createElement("head");
+      html.insertBefore(head, html.getFirstChild());
+    }
+    NodeList headNodes = head.getChildNodes();
+    Stack<Node> headScripts = new Stack<Node>();
+    for (int i = 0; i < headNodes.getLength(); ++i) {
+      Node headChild = headNodes.item(i);
+      if (headChild.getNodeName().equalsIgnoreCase("script")) {
+        headScripts.add(headChild);
+      }
+    }
+
+    // Remove from head, add to top of <body> in <head> order.
+    Node body = DomUtil.getFirstNamedChildNode(html, "body");
+    if (body == null) {
+      body = document.createElement("body");
+      html.insertBefore(body, head.getNextSibling());
+    }
+    Node bodyFirst = body.getFirstChild();
+    while (!headScripts.isEmpty()) {
+      Node headScript = headScripts.pop();
+      head.removeChild(headScript);
+      body.insertBefore(headScript, bodyFirst);
+      bodyFirst = headScript;
+    }
+  }
+
+  protected HTMLConfiguration newConfiguration() {
+    HTMLConfiguration config = new HTMLConfiguration();
+    // Maintain original case for elements and attributes
+    config.setProperty("http://cyberneko.org/html/properties/names/elems", "match");
+    config.setProperty("http://cyberneko.org/html/properties/names/attrs", "no-change");
+    // Get notified of entity and character references
+    config.setFeature("http://apache.org/xml/features/scanner/notify-char-refs", true);
+    config.setFeature("http://cyberneko.org/html/features/scanner/notify-builtin-refs", true);
+    config.setFeature("http://xml.org/sax/features/namespaces", true);
+    return config;
+  }
+
+  protected DocumentHandler newDocumentHandler(String source) {
+    return new DocumentHandler(source);
+  }
+
+  /** Handler for XNI events from Neko */
+  protected class DocumentHandler implements XMLDocumentHandler {
+
+    private final Stack<Node> elementStack = new Stack<Node>();
+
+    private final StringBuilder builder;
+
+    private boolean inEntity = false;
+
+
+    private DocumentFragment documentFragment;
+
+    private Document document;
+
+    public DocumentHandler(String content) {
+      builder = new StringBuilder(content.length() / 10);
+    }
+
+    public DocumentFragment getFragment() {
+      return documentFragment;
+    }
+
+    public Document getDocument() {
+      return document;
+    }
+
+    public void startDocument(XMLLocator xmlLocator, String encoding,
+        NamespaceContext namespaceContext, Augmentations augs)
+        throws XNIException {
+      document = documentFactory.createDocument(null, null, null);
+      elementStack.clear();
+      documentFragment = document.createDocumentFragment();
+      elementStack.push(documentFragment);
+    }
+
+    public void xmlDecl(String version, String encoding, String standalone, Augmentations augs)
+        throws XNIException {
+      // Dont really do anything with this
+      builder.append("<?xml");
+      if (version != null) {
+        builder.append(" version=\"").append(version).append('\"');
+      }
+      if (encoding != null) {
+        builder.append(" encoding=\"").append(encoding).append('\"');
+      }
+      if (standalone != null) {
+        builder.append(" standalone=\"").append(standalone).append('\"');
+      }
+      builder.append('>');
+    }
+
+    public void doctypeDecl(String rootElement, String publicId, String systemId,
+        Augmentations augs) throws XNIException {
+      document = documentFactory.createDocument(null, null,
+          documentFactory.createDocumentType(rootElement, publicId, systemId));
+      elementStack.clear();
+      documentFragment = document.createDocumentFragment();
+      elementStack.push(documentFragment);
+    }
+
+    public void comment(XMLString text, Augmentations augs) throws XNIException {
+      flushTextBuffer();
+
+      // Add comments as comment nodes - needed to support sanitization
+      // of SocialMarkup-parsed content
+      Node comment = getDocument().createComment(new String(text.ch, text.offset, text.length));
+      appendChild(comment);
+    }
+
+    public void processingInstruction(String s, XMLString xmlString, Augmentations augs)
+        throws XNIException {
+      // No-op
+    }
+
+    public void startElement(QName qName, XMLAttributes xmlAttributes, Augmentations augs)
+        throws XNIException {
+      Element element = startElementImpl(qName, xmlAttributes);
+      // Not an empty element, so push on the stack
+      elementStack.push(element);
+    }
+
+    public void emptyElement(QName qName, XMLAttributes xmlAttributes, Augmentations augs)
+        throws XNIException {
+      startElementImpl(qName, xmlAttributes);
+    }
+
+    /** Flush any existing text content to the document.  Call this before appending any nodes. */
+    protected void flushTextBuffer() {
+      if (builder.length() > 0) {
+        appendChild(document.createTextNode(builder.toString()));
+        builder.setLength(0);
+      }
+    }
+
+    /** Create an Element in the DOM */
+    private Element startElementImpl(QName qName, XMLAttributes xmlAttributes) {
+      flushTextBuffer();
+
+      Element element;
+      // Preserve XML namespace if present
+      if (qName.uri != null) {
+        element = document.createElementNS(qName.uri, qName.rawname);
+      } else {
+        element = document.createElement(qName.rawname);
+      }
+
+      for (int i = 0; i < xmlAttributes.getLength(); i++) {
+        if (xmlAttributes.getURI(i) != null) {
+          element.setAttributeNS(xmlAttributes.getURI(i), xmlAttributes.getQName(i),
+              xmlAttributes.getValue(i));
+        } else {
+          try {
+            element.setAttribute(xmlAttributes.getLocalName(i), xmlAttributes
+                .getValue(i));
+          } catch (DOMException e) {
+            switch (e.code) {
+              case DOMException.INVALID_CHARACTER_ERR:
+                StringBuilder sb = new StringBuilder(e.getMessage());
+                sb.append("Around ...<");
+                if (qName.prefix != null) {
+                  sb.append(qName.prefix);
+                  sb.append(':');
+                }
+                sb.append(qName.localpart);
+                for (int j = 0; j < xmlAttributes.getLength(); j++) {
+                  if (StringUtils.isNotBlank(xmlAttributes.getLocalName(j))
+                      && StringUtils.isNotBlank(xmlAttributes.getValue(j))) {
+                    sb.append(' ');
+                    sb.append(xmlAttributes.getLocalName(j));
+                    sb.append("=\"");
+                    sb.append(xmlAttributes.getValue(j)).append('\"');
+                  }
+                }
+                sb.append("...");
+                throw new DOMException(DOMException.INVALID_CHARACTER_ERR, sb.toString());
+              default:
+                throw e;
+            }
+          }
+        }
+      }
+      appendChild(element);
+      return element;
+    }
+
+    public void startGeneralEntity(String name, XMLResourceIdentifier id, String encoding,
+        Augmentations augs) throws XNIException {
+      if (name.startsWith("#")) {
+        try {
+          boolean hex = name.startsWith("#x");
+          int offset = hex ? 2 : 1;
+          int base = hex ? 16 : 10;
+          int value = Integer.parseInt(name.substring(offset), base);
+          String entity = HTMLEntities.get(value);
+          if (entity != null) {
+            name = entity;
+          }
+        }
+        catch (NumberFormatException e) {
+          // ignore
+        }
+      }
+      printEntity(name);
+      inEntity = true;
+    }
+
+    private void printEntity(String name) {
+      builder.append('&');
+      builder.append(name);
+      builder.append(';');
+    }
+
+    public void textDecl(String s, String s1, Augmentations augs) throws XNIException {
+      builder.append(s);
+    }
+
+    public void endGeneralEntity(String s, Augmentations augs) throws XNIException {
+      inEntity = false;
+    }
+
+    public void characters(XMLString text, Augmentations augs) throws XNIException {
+      if (inEntity) {
+        return;
+      }
+      builder.append(text.ch, text.offset, text.length);
+    }
+
+    public void ignorableWhitespace(XMLString text, Augmentations augs) throws XNIException {
+      builder.append(text.ch, text.offset, text.length);
+    }
+
+    public void endElement(QName qName, Augmentations augs) throws XNIException {
+      flushTextBuffer();
+      elementStack.pop();
+    }
+
+    public void startCDATA(Augmentations augs) throws XNIException {
+      //No-op
+    }
+
+    public void endCDATA(Augmentations augs) throws XNIException {
+      //No-op
+    }
+
+    public void endDocument(Augmentations augs) throws XNIException {
+      flushTextBuffer();
+      elementStack.pop();
+    }
+
+    public void setDocumentSource(XMLDocumentSource xmlDocumentSource) {
+    }
+
+    public XMLDocumentSource getDocumentSource() {
+      return null;
+    }
+
+    private void appendChild(Node node) {
+      elementStack.peek().appendChild(node);
+    }
+  }
+
+  /**
+   * Used when parsing document fragments to correct a bug in Neko 1.9.13. We use the
+   * http://cyberneko.org/html/properties/balance-tags/fragment-context-stack
+   * property of Neko to force the fragment to be parsed as if it were already container in a body
+   * tag. This doesnt quite work together as without this fix it will still introduce head tags
+   * if the first parsed tags are allowed in a head tag.
+   * See https://sourceforge.net/tracker/?func=detail&atid=952178&aid=2870180&group_id=195122
+   */
+  private static class NekoPatchTagBalancer extends NormalizingTagBalancer {
+
+    /**
+     * Override the document start to record whether HTML, HEAD or BODY have been seen
+     */
+    @Override
+    public void startDocument(XMLLocator locator, String encoding,
+        NamespaceContext nscontext, Augmentations augs)
+        throws XNIException {
+
+      super.startDocument(locator, encoding, nscontext, augs);
+      for (int i = fElementStack.top - 1; i >= 0; i--) {
+        fSeenAnything = true;
+        if (fElementStack.data[i].element.code == HTMLElements.HTML) {
+          fSeenRootElement = true;
+        }
+        if (fElementStack.data[i].element.code == HTMLElements.HEAD) {
+          fSeenHeadElement = true;
+        }
+        if (fElementStack.data[i].element.code == HTMLElements.BODY) {
+          fSeenBodyElement = true;
+        }
+      }
+    }
+  }
+
+  /**
+   * Subclass of Neko's tag balancer that
+   * - Normalizes the case of forced html, head and body tags when they don't exist in the original
+   * content.
+   * -
+   */
+  private static class NormalizingTagBalancer extends HTMLTagBalancer {
+
+    private StringBuilder scriptContent;
+
+    private HTMLScanner scanner;
+
+    public NormalizingTagBalancer() {
+    }
+
+    public void setScanner(HTMLScanner scanner) {
+      this.scanner = scanner;
+    }
+
+    @Override
+    public void startElement(QName elem, XMLAttributes attrs, Augmentations augs)
+        throws XNIException {
+      // Normalize the case of forced-elements to lowercase for backward compatability
+      if (!fSeenRootElement && elem.rawname.equalsIgnoreCase("html")) {
+        elem.localpart = "html";
+        elem.rawname = "html";
+      } else if (!fSeenHeadElement && elem.rawname.equalsIgnoreCase("head")) {
+        elem.localpart = "head";
+        elem.rawname = "head";
+      } else if (!fSeenBodyElement && elem.rawname.equalsIgnoreCase("body")) {
+        elem.localpart = "body";
+        elem.rawname = "body";
+      }
+
+      super.startElement(elem, attrs, augs);
+    }
+
+
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/cajatest.html b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/cajatest.html
new file mode 100644
index 0000000..af82113
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/parse/nekohtml/cajatest.html
@@ -0,0 +1,102 @@
+<?xml version="not-even-close"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+
+<!-- a test input for HtmlLexer -->
+
+<html>
+<head>
+<title>Test File For HtmlLexer &amp; HtmlParser</title>
+<link rel=stylesheet type="text/css" src=foo/bar.css />
+<body
+ bgcolor=white
+ linkcolor = "blue"
+ onload="document.writeln(
+  &quot;&lt;p&gt;properly escaped code in a handler&lt;/p&gt;&quot;);"
+>
+
+<script type="text/javascript"><!--
+
+document.writeln("<p>Some initialization code in global context</p>");
+
+--></script>
+
+<script type="text/javascript">
+// hi there
+document.writeln("<p>More initialization</p>");
+</script>
+
+<div id=clickydiv onclick="handleClicky(event)"
+ ondblclick=this.onclick(event);return(false)>
+Clicky
+</div>
+
+<input id=foo>
+<gxp:attr name="onchange">alert("&lt;b&gt;hi&lt;/b&gt;");</gxp:attr>
+</input>
+
+<pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+
+<!-- some tokenization corner cases -->
+
+< notatag <atag/>
+
+</ notatag> </redundantlyclosed/>
+
+<messyattributes a=b=c d="e"f=g h =i j= k l = m checked n="o"/>
+
+< < < all in one text block > > >
+
+<xmp>Make sure that <!-- comments don't obscure the xmp close</xmp>
+
+<% # some php code here
+write("<pre>$horriblySyntacticConstruct1</pre>\n\n");
+%>
+
+<script type="text/javascript"><!--
+alert("hello world");
+// --></script>
+
+<script>/* </script> */alert('hi');</script>
+<script><!--/* </script> */alert('hi');--></script>
+
+<xmp style=color:blue><!--/* </xmp> */alert('hi');--></xmp>
+
+<style><!-- p { contentf: '</style>' } --></style>
+
+<title>Foo<!-- > </title> --></title>
+
+<textarea><!-- Zoicks </textarea>--></textarea>
+
+<!-- An escaping text span start may share its U+002D HYPHEN-MINUS characters
+   - with its corresponding escaping text span end. -->
+<script><!--></script>
+<script><!---></script>
+<script><!----></script>
+
+</body>
+</html>
+
+<![CDATA[ No such thing as a CDATA> section in HTML ]]>
+<script>a<b</script>
+
+<img src=foo.gif/><a href=><a href=/>
+
+<span title=malformed attribs' do=don't id=foo checked onclick="a<b">Bar</span>
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java
new file mode 100644
index 0000000..4c883c2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderService.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import org.apache.shindig.gadgets.Gadget;
+
+import java.util.Collection;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+
+import com.google.inject.Inject;
+
+/**
+ * Preloads will be fetched concurrently using the injected ExecutorService, and they can be read
+ * lazily using the returned map of futures.
+ *
+ * The last preloaded object always executes in the current thread to avoid creating unnecessary
+ * additional threads when we're blocking the current request anyway.
+ */
+public class ConcurrentPreloaderService implements PreloaderService {
+  private final ExecutorService executor;
+  private Preloader preloader;
+
+  @Inject
+  public ConcurrentPreloaderService(ExecutorService executor, Preloader preloader) {
+    this.executor = executor;
+    this.preloader = preloader;
+  }
+
+  public Collection<PreloadedData> preload(Gadget gadget) {
+    Collection<Callable<PreloadedData>> tasks =
+        preloader.createPreloadTasks(gadget);
+
+    return preload(tasks);
+  }
+
+  public Collection<PreloadedData> preload(Collection<Callable<PreloadedData>> tasks) {
+    ConcurrentPreloads preloads = new ConcurrentPreloads(tasks.size());
+    int processed = tasks.size();
+    for (Callable<PreloadedData> task : tasks) {
+      processed -= 1;
+      if (processed == 0) {
+        // The last preload fires in the current thread.
+        FutureTask<PreloadedData> futureTask = new FutureTask<PreloadedData>(task);
+        futureTask.run();
+        preloads.add(futureTask);
+      } else {
+        preloads.add(executor.submit(task));
+      }
+    }
+    return preloads;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloads.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloads.java
new file mode 100644
index 0000000..02c178c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/ConcurrentPreloads.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ForwardingCollection;
+import com.google.common.collect.Lists;
+
+/**
+ * Preloads data by evaluating Futures for PreloadedData.
+ * This class is not, however, thread-safe - tasks must be
+ * added and read from a single thread..
+ */
+class ConcurrentPreloads extends ForwardingCollection<PreloadedData> {
+  private final List<Future<PreloadedData>> tasks;
+  private Collection<PreloadedData> loaded;
+
+  ConcurrentPreloads() {
+    tasks = Lists.newArrayList();
+  }
+
+  ConcurrentPreloads(int size) {
+    tasks = Lists.newArrayListWithCapacity(size);
+  }
+
+  /**
+   * Add an active preloading process.
+   *
+   * @param futureData A future that will return the preloaded data.
+   */
+  ConcurrentPreloads add(Future<PreloadedData> futureData) {
+    tasks.add(futureData);
+    return this;
+  }
+
+  @Override
+  protected Collection<PreloadedData> delegate() {
+    if (loaded == null) {
+      loaded = getData();
+    }
+
+    return loaded;
+  }
+
+  private Collection<PreloadedData> getData() {
+    return Lists.transform(tasks, new Function<Future<PreloadedData>, PreloadedData>() {
+      public PreloadedData apply(Future<PreloadedData> preloadedDataFuture) {
+        return getPreloadedData(preloadedDataFuture);
+      }
+    });
+  }
+
+  /**
+   * Gets the preloaded data, handling any exceptions from Future processing.
+   */
+  protected PreloadedData getPreloadedData(Future<PreloadedData> preloadedDataFuture) {
+    try {
+     return preloadedDataFuture.get();
+    } catch (ExecutionException ee) {
+      return new FailedPreload(ee.getCause());
+    } catch (InterruptedException ie) {
+      // Do NOT Propagate the interrupt
+      throw new RuntimeException("Preloading was interrupted by thread termination", ie);
+    }
+  }
+
+  /** PreloadData implementation that reports failure */
+  private static class FailedPreload implements PreloadedData {
+    private final Throwable t;
+
+    public FailedPreload(Throwable t) {
+      this.t = t;
+    }
+
+    public Collection<Object> toJson() throws PreloadException {
+      if (t instanceof PreloadException) {
+        throw (PreloadException) t;
+      }
+
+      throw new PreloadException(t);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/HttpPreloader.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/HttpPreloader.java
new file mode 100644
index 0000000..6b072c9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/HttpPreloader.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import org.apache.shindig.gadgets.FetchResponseUtils;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.spec.Preload;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+/**
+ * Handles HTTP Preloading (/ModulePrefs/Preload elements).
+ *
+ * @see org.apache.shindig.gadgets.spec.Preload
+ */
+public class HttpPreloader implements Preloader {
+  private final RequestPipeline requestPipeline;
+
+  @Inject
+  public HttpPreloader(RequestPipeline requestPipeline) {
+    this.requestPipeline = requestPipeline;
+  }
+
+  public Collection<Callable<PreloadedData>> createPreloadTasks(Gadget gadget) {
+    List<Callable<PreloadedData>> preloads = Lists.newArrayList();
+
+    GadgetContext context = gadget.getContext();
+
+    for (Preload preload : gadget.getSpec().getModulePrefs().getPreloads()) {
+      Set<String> preloadViews = preload.getViews();
+      if (preloadViews.isEmpty() || preloadViews.contains(context.getView())) {
+        preloads.add(new PreloadTask(context, preload, preload.getHref().toString()));
+      }
+    }
+
+    return preloads;
+  }
+
+  // TODO: move somewhere more sensible
+  public static HttpRequest newHttpRequest(GadgetContext context,
+      RequestAuthenticationInfo authenticationInfo) throws GadgetException {
+    return new HttpRequest(authenticationInfo.getHref())
+        .setSecurityToken(context.getToken())
+        .setOAuthArguments(new OAuthArguments(authenticationInfo))
+        .setOAuth2Arguments(new OAuth2Arguments(authenticationInfo))
+        .setAuthType(authenticationInfo.getAuthType())
+        .setContainer(context.getContainer())
+        .setGadget(context.getUrl())
+        .setIgnoreCache(context.getIgnoreCache());
+  }
+
+  class PreloadTask implements Callable<PreloadedData> {
+    private final GadgetContext context;
+    private final Preload preload;
+    private final String key;
+
+    public PreloadTask(GadgetContext context, Preload preload, String key) {
+      this.context = context;
+      this.preload = preload;
+      this.key = key;
+    }
+
+    public PreloadedData call() throws Exception {
+      HttpRequest request = newHttpRequest(context, preload);
+
+      return new HttpPreloadData(requestPipeline.execute(request), key);
+    }
+  }
+
+  /**
+   * Implements PreloadData by returning a Map that matches the output format used by makeRequest.
+   */
+  private static class HttpPreloadData implements PreloadedData {
+    private final Map<String, Object> data;
+
+    public HttpPreloadData(HttpResponse response, String key) {
+      this.data = FetchResponseUtils.getResponseAsJson(response, key,
+          response.getResponseAsString(), false);
+    }
+
+    public Collection<Object> toJson() {
+      return ImmutableList.of((Object) data);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelineExecutor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelineExecutor.java
new file mode 100644
index 0000000..0f63368
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelineExecutor.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import org.apache.shindig.common.JsonUtil;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetELResolver;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.PipelinedData.Batch;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.el.CompositeELResolver;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+/**
+ * Runs data pipelining, chaining dependencies among batches as needed.
+ */
+public class PipelineExecutor {
+  // TODO: support configuration
+  private static final int MAX_BATCH_COUNT = 3;
+  //class name for logging purpose
+  private static final String classname = PipelineExecutor.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+
+  private final PipelinedDataPreloader preloader;
+  private final PreloaderService preloaderService;
+  private final Expressions expressions;
+
+  @Inject
+  public PipelineExecutor(PipelinedDataPreloader preloader,
+      PreloaderService preloaderService,
+      Expressions expressions) {
+    this.preloader = preloader;
+    this.preloaderService = preloaderService;
+    this.expressions = expressions;
+  }
+
+  /**
+   * Results from a full pipeline execution.
+   */
+  public static class Results {
+    /**
+     * A collection of the pipelines that could not be fully
+     * evaluated.
+     */
+    public final Collection<PipelinedData> remainingPipelines;
+
+    /**
+     * Results in the form of a full JSON-RPC batch response.
+     */
+    public final Collection<? extends Object> results;
+
+    /**
+     * Results in the form of a Map from id to a JSON-serializable object.
+     */
+    public final Map<String, ? extends Object> keyedResults;
+
+    public Results(Collection<PipelinedData> remainingPipelines,
+        Collection<? extends Object> results,
+        Map<String, ? extends Object> keyedResults) {
+      this.remainingPipelines = remainingPipelines;
+      this.results = results;
+      this.keyedResults = keyedResults;
+    }
+  }
+
+  /**
+   * Executes a pipeline, or set of pipelines.
+   * @param context the gadget context for the state in which the pipelines execute
+   * @param pipelines a collection of pipelines
+   * @return results from the pipeline, or null if there are no results
+   */
+  public Results execute(GadgetContext context, Collection<PipelinedData> pipelines) {
+    List<Object> results = Lists.newArrayList();
+    Map<String, Object> elResults = Maps.newHashMap();
+    CompositeELResolver rootObjects = new CompositeELResolver();
+    rootObjects.add(new GadgetELResolver(context));
+    rootObjects.add(new RootELResolver(elResults));
+
+    List<PipelineState> pipelineStates = Lists.newArrayList();
+    for (PipelinedData pipeline : pipelines) {
+      PipelinedData.Batch batch = pipeline.getBatch(expressions, rootObjects);
+      pipelineStates.add(new PipelineState(pipeline, batch));
+    }
+
+    int batchCount = 0;
+    while (true) {
+      List<Callable<PreloadedData>> tasks = Lists.newArrayList();
+      for (PipelineState pipeline : pipelineStates) {
+        if (pipeline.batch != null) {
+          tasks.addAll(preloader.createPreloadTasks(context, pipeline.batch));
+        }
+      }
+
+      if (tasks.isEmpty()) {
+        break;
+      }
+
+      Collection<PreloadedData> preloads = preloaderService.preload(tasks);
+      for (PreloadedData preloaded : preloads) {
+        try {
+          for (Object entry : preloaded.toJson()) {
+            results.add(entry);
+
+            String id = (String) JsonUtil.getProperty(entry, "id");
+
+            Object data = JsonUtil.getProperty(entry, "result");
+            if (data == null) {
+              // For backward compatiblity, check maybe return old 'data' field:
+              data = JsonUtil.getProperty(entry, "data");
+            }
+            if (data != null) {
+              elResults.put(id, data);
+            } else {
+              Object error = JsonUtil.getProperty(entry, "error");
+              if (error != null) {
+                elResults.put(id, error);
+              }
+            }
+          }
+        } catch (PreloadException pe) {
+          // This will be thrown in the event of some unexpected exception. We can move on.
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "execute", MessageKeys.ERROR_PRELOADING);
+            LOG.log(Level.WARNING, "", pe);
+          }
+        }
+      }
+
+      // Advance to the next batch
+      for (PipelineState pipeline : pipelineStates) {
+        if (pipeline.batch != null) {
+          pipeline.batch = pipeline.batch.getNextBatch(rootObjects);
+        }
+      }
+
+      batchCount++;
+      if (batchCount == MAX_BATCH_COUNT) {
+        break;
+      }
+    }
+
+    List<PipelinedData> remainingPipelines = Lists.newArrayList();
+    for (PipelineState pipeline : pipelineStates) {
+      if (pipeline.batch != null) {
+        remainingPipelines.add(pipeline.pipeline);
+      }
+    }
+
+    return new Results(remainingPipelines, results, elResults);
+  }
+
+  /** State of one of the pipelines */
+  static class PipelineState {
+    public PipelineState(PipelinedData pipeline, Batch batch) {
+      this.pipeline = pipeline;
+      this.batch = batch;
+    }
+
+    public final PipelinedData pipeline;
+    public PipelinedData.Batch batch;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java
new file mode 100644
index 0000000..7fb418d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloader.java
@@ -0,0 +1,399 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.JsonUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+/**
+ * Processes a single batch of pipeline data into tasks.
+ */
+public class PipelinedDataPreloader {
+  private final RequestPipeline requestPipeline;
+  private final ContainerConfig config;
+
+  private static final Set<String> HTTP_RESPONSE_HEADERS =
+    ImmutableSet.of("content-type", "location", "set-cookie");
+
+  @Inject
+  public PipelinedDataPreloader(RequestPipeline requestPipeline, ContainerConfig config) {
+    this.requestPipeline = requestPipeline;
+    this.config = config;
+  }
+
+  /** Create preload tasks from a batch of social and http preloads */
+  public Collection<Callable<PreloadedData>> createPreloadTasks(GadgetContext context,
+      PipelinedData.Batch batch) {
+    List<Callable<PreloadedData>> preloadList = Lists.newArrayList();
+
+    Collection<Object> socialRequest = Lists.newArrayList();
+    // Gather all the preload entries;  all social requests in one batch, each HTTP
+    // in its own
+    for (Map.Entry<String, PipelinedData.BatchItem> preloadEntry : batch.getPreloads().entrySet()) {
+      PipelinedData.BatchItem preloadItem = preloadEntry.getValue();
+      switch (preloadItem.getType()) {
+        case HTTP:
+          preloadList.add(new HttpPreloadTask(context, (RequestAuthenticationInfo) preloadItem.getData(),
+              preloadEntry.getKey()));
+          break;
+        case SOCIAL:
+          socialRequest.add(preloadItem.getData());
+          break;
+        case VARIABLE:
+          // TODO: this is rather crazy: these tasks don't need to execute on
+          // another thread.
+          preloadList.add(new VariableTask(preloadEntry.getKey(), preloadItem.getData()));
+          break;
+        default:
+          throw new IllegalArgumentException("Unknown pipeline type");
+      }
+    }
+
+    if (!socialRequest.isEmpty()) {
+      preloadList.add(new SocialPreloadTask(context, socialRequest));
+    }
+
+    return preloadList;
+  }
+
+  /**
+   * Hook for executing a JSON RPC fetch for social data.  Subclasses can override
+   * to provide special handling (e.g., directly invoking a local API)
+   *
+   * @param request the social request
+   * @return the response to the request
+   * @throws GadgetException if there are errors processing the gadget spec
+   */
+  protected HttpResponse executeSocialRequest(HttpRequest request) throws GadgetException {
+    return requestPipeline.execute(request);
+  }
+
+  private static class VariableTask implements Callable<PreloadedData> {
+    private ImmutableMap<String, Object> result;
+
+    public VariableTask(String key, Object data) {
+      this.result = (data == null) ? ImmutableMap.of("id", (Object) key)
+          : ImmutableMap.of("id", key, "result", data);
+    }
+
+    public PreloadedData call() throws Exception {
+      return new PreloadedData() {
+        public Collection<Object> toJson() throws PreloadException {
+          return ImmutableList.<Object>of(result);
+        }
+      };
+    }
+  }
+
+  /**
+   * Callable for issuing HttpRequests to JsonRpcServlet.
+   */
+  private class SocialPreloadTask implements Callable<PreloadedData> {
+
+    private final GadgetContext context;
+    private final Collection<? extends Object> socialRequests;
+
+    public SocialPreloadTask(GadgetContext context, Collection<? extends Object> socialRequests) {
+      this.context = context;
+      this.socialRequests = socialRequests;
+    }
+
+    public PreloadedData call() throws Exception {
+      HttpResponse response;
+
+      String token = context.getParameter("st");
+      if (token == null) {
+        response = new HttpResponseBuilder()
+           .setHttpStatusCode(HttpServletResponse.SC_FORBIDDEN)
+           .setResponseString("Security token missing")
+           .create();
+      } else {
+        Uri uri = getSocialUri(context, token);
+
+        String socialRequestsJson = JsonSerializer.serialize(socialRequests);
+        HttpRequest request = new HttpRequest(uri)
+            .setIgnoreCache(context.getIgnoreCache())
+            .setSecurityToken(context.getToken())
+            .setMethod("POST")
+            .setAuthType(AuthType.NONE)
+            .setPostBody(CharsetUtil.getUtf8Bytes(socialRequestsJson))
+            .addHeader("Content-Type", "application/json; charset=UTF-8")
+            .setContainer(context.getContainer())
+            .setGadget(context.getUrl());
+
+        response = executeSocialRequest(request);
+      }
+
+      // Unpack the response into a list of PreloadedData responses
+      String responseText;
+      if (response.getHttpStatusCode() < 400) {
+        responseText = response.getResponseAsString();
+      } else {
+        // For error responses, unpack into the same error format used
+        // for os:HttpRequest
+        responseText = JsonSerializer.serialize(
+            createJsonError(response.getHttpStatusCode(), null, response));
+      }
+
+      final List<Object> data = parseSocialResponse(socialRequests, responseText);
+
+      return new PreloadedData() {
+        public Collection<Object> toJson() {
+          return data;
+        }
+      };
+    }
+  }
+
+  /**
+   * Parse the response from a social request into a list of response objects
+   */
+  static List<Object> parseSocialResponse(Collection<? extends Object> requests,
+      String response) throws JSONException {
+    // Unpack the response into a list of PreloadedData responses
+    final List<Object> data = Lists.newArrayList();
+
+    if (response.startsWith("[")) {
+      // A non-error response is a JSON array
+      JSONArray array = new JSONArray(response);
+      for (int i = 0; i < array.length(); i++) {
+        data.add(array.get(i));
+      }
+    } else {
+      // But a global failure is a JSON object.  Per spec requirements, copy
+      // the overall error into per-id errors
+      JSONObject error = new JSONObject(response);
+      for (Object request : requests) {
+        JSONObject itemResponse = new JSONObject();
+        itemResponse.put("error", error);
+        itemResponse.put("id", JsonUtil.getProperty(request, "id"));
+        data.add(itemResponse);
+      }
+    }
+
+    return data;
+  }
+
+  /** A task for loading os:HttpRequest */
+  class HttpPreloadTask implements Callable<PreloadedData> {
+    private final GadgetContext context;
+    private final RequestAuthenticationInfo preload;
+    private final String key;
+
+    public HttpPreloadTask(GadgetContext context, RequestAuthenticationInfo preload, String key) {
+      this.context = context;
+      this.preload = preload;
+      this.key = key;
+    }
+
+    public PreloadedData call() throws Exception {
+      HttpRequest request = HttpPreloader.newHttpRequest(context, preload);
+      String refreshIntervalStr = preload.getAttributes().get("refreshInterval");
+      if (refreshIntervalStr != null) {
+        try {
+          int refreshInterval = Integer.parseInt(refreshIntervalStr);
+          request.setCacheTtl(refreshInterval);
+        } catch (NumberFormatException nfe) {
+          // Ignore, and use the HTTP response interval
+        }
+      }
+
+      String method = preload.getAttributes().get("method");
+      if (method != null) {
+        request.setMethod(method);
+      }
+
+      // TODO: params EL implementation is not yet properly escaped per spec
+      String params = preload.getAttributes().get("params");
+      if ((params != null) && !"".equals(params)) {
+        if ("POST".equalsIgnoreCase(request.getMethod())) {
+          request.setPostBody(CharsetUtil.getUtf8Bytes(params));
+          request.setHeader("Content-Type", "application/x-www-form-urlencoded; charset=utf-8");
+        } else {
+          UriBuilder uriBuilder = new UriBuilder(request.getUri());
+          String query = uriBuilder.getQuery();
+          query = query == null ? params : query + '&' + params;
+          uriBuilder.setQuery(query);
+          request.setUri(uriBuilder.toUri());
+        }
+      }
+
+      return new Data(requestPipeline.execute(request));
+    }
+
+    // TODO: change HttpPreloader to use this format
+    class Data implements PreloadedData {
+      private final JSONObject data;
+
+      public Data(HttpResponse response) {
+        String format = preload.getAttributes().get("format");
+        JSONObject wrapper = new JSONObject();
+
+        try {
+          wrapper.put("id", key);
+          if (response.getHttpStatusCode() >= 400) {
+            wrapper.put("error", createJsonError(response.getHttpStatusCode(), null, response));
+          } else {
+            // Create {data: {status: [CODE], content: {...}|[...]|"...", headers:{...}}}
+            JSONObject data = new JSONObject();
+            wrapper.put("result", data);
+
+            // Add the status
+            data.put("status", response.getHttpStatusCode());
+            String responseText = response.getResponseAsString();
+
+            // Add allowed headers
+            JSONObject headers = createJsonHeaders(response);
+            if (headers != null) {
+              data.put("headers", headers);
+            }
+
+            // And add the parsed content
+            if (format == null || "json".equals(format)) {
+              try {
+                if (responseText.startsWith("[")) {
+                  data.put("content", new JSONArray(responseText));
+                } else {
+                  data.put("content", new JSONObject(responseText));
+                }
+              } catch (JSONException je) {
+                // JSON parse failed: create a 406 error, and remove the "result" section
+                wrapper.remove("result");
+                wrapper.put("error", createJsonError(
+                    HttpResponse.SC_NOT_ACCEPTABLE, je.getMessage(), response));
+              }
+            } else {
+              data.put("content", responseText);
+            }
+          }
+        } catch (JSONException outerJe) {
+          throw new RuntimeException(outerJe);
+        }
+
+        this.data = wrapper;
+      }
+
+      public Collection<Object> toJson() {
+        return ImmutableList.<Object>of(data);
+      }
+    }
+  }
+
+  private static JSONObject createJsonHeaders(HttpResponse response)
+      throws JSONException {
+    JSONObject headers = null;
+
+    // Add allowed headers
+    for (String header: HTTP_RESPONSE_HEADERS) {
+      Collection<String> values = response.getHeaders(header);
+      if (values != null && !values.isEmpty()) {
+        JSONArray array = new JSONArray();
+        for (String value : values) {
+          array.put(value);
+        }
+
+        if (headers == null) {
+          headers = new JSONObject();
+        }
+
+        headers.put(header, array);
+      }
+    }
+
+    return headers;
+  }
+
+  /**
+   * Create {error: { code: [CODE], data: {content: "....", headers: {...}}}}
+   */
+  private static JSONObject createJsonError(int code, String message, HttpResponse response)
+      throws JSONException {
+    JSONObject error = new JSONObject();
+    error.put("code", code);
+    if (message != null) {
+      error.put("message", message);
+    }
+
+    JSONObject data = new JSONObject();
+    String responseText = response.getResponseAsString();
+    if (StringUtils.isNotEmpty(responseText)) {
+      data.put("content", responseText);
+    }
+
+    // Add allowed headers
+    JSONObject headers = createJsonHeaders(response);
+    if (headers != null) {
+      data.put("headers", headers);
+    }
+
+    if (data.length() > 0) {
+      error.put("data", data);
+    }
+
+    return error;
+  }
+
+  private Uri getSocialUri(GadgetContext context, String token) {
+    String jsonUri = config.getString(context.getContainer(), "gadgets.osDataUri");
+    Preconditions.checkNotNull(jsonUri, "No JSON URI available for social preloads");
+    Preconditions.checkNotNull(token, "No token available for social preloads");
+
+    UriBuilder builder = UriBuilder.parse(
+        jsonUri.replace("%host%", context.getHost()))
+        .addQueryParameter("st", token);
+    Uri uri = builder.toUri();
+    if(Strings.isNullOrEmpty(uri.getScheme()) && !Strings.isNullOrEmpty(context.getHostSchema())) {
+      uri = builder.setScheme(context.getHostSchema()).toUri();
+    }
+    return uri;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadException.java
new file mode 100644
index 0000000..aa7362f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadException.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+/**
+ * Exceptions thrown when preloading data.
+ */
+public class PreloadException extends Exception {
+  public PreloadException(String msg) {
+    super(msg);
+  }
+
+  public PreloadException(Throwable t) {
+    super(t);
+  }
+
+  public PreloadException(String msg, Throwable t) {
+    super(msg, t);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadModule.java
new file mode 100644
index 0000000..4fc585e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadModule.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import com.google.inject.AbstractModule;
+
+/**
+ * Guice bindings for the render package.
+ */
+public class PreloadModule extends AbstractModule {
+  @Override
+  protected void configure() {
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadedData.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadedData.java
new file mode 100644
index 0000000..775f33b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloadedData.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import java.util.Collection;
+
+
+/**
+ * Contains preloaded data and methods for manipulating it.
+ */
+public interface PreloadedData {
+
+  /**
+   * Serialize the preloaded data into json.
+   *
+   * @return A JSON object suitable for passing to org.json.JSONObject.put(String, Object).
+   */
+  Collection<Object> toJson() throws PreloadException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/Preloader.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/Preloader.java
new file mode 100644
index 0000000..e682972
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/Preloader.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import org.apache.shindig.gadgets.Gadget;
+
+import java.util.Collection;
+import java.util.concurrent.Callable;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Performs an individual preloading operation.
+ */
+@ImplementedBy(HttpPreloader.class)
+public interface Preloader {
+  /**
+   * Create new preload tasks for the provided gadget.
+   *
+   * @param gadget The gadget that the operations will be performed for.
+   * @return Preloading tasks that will be executed by
+   *  {@link PreloaderService#}.
+   */
+  Collection<Callable<PreloadedData>> createPreloadTasks(Gadget gadget);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java
new file mode 100644
index 0000000..a2b4f3e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/preload/PreloaderService.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import org.apache.shindig.gadgets.Gadget;
+
+import java.util.Collection;
+import java.util.concurrent.Callable;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Handles preloading operations, such as HTTP fetches, social data retrieval, or anything else that
+ * would benefit from preloading on the server instead of incurring a network request for users.
+ */
+@ImplementedBy(ConcurrentPreloaderService.class)
+public interface PreloaderService {
+  /**
+   * Begin all preload operations.
+   *
+   * @param gadget The gadget that the operations will be performed for.
+   * @return The preloads for the gadget.
+   */
+  Collection<PreloadedData> preload(Gadget gadget);
+
+  /**
+   * Execute preloads with a specific set of preload tasks.
+   */
+  Collection<PreloadedData> preload(Collection<Callable<PreloadedData>> tasks);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/ProcessingException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/ProcessingException.java
new file mode 100644
index 0000000..619b17a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/ProcessingException.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.process;
+
+/**
+ * Exceptions thrown during gadget processing.
+ */
+public class ProcessingException extends Exception {
+  private int statusCode;
+
+  public ProcessingException(Throwable t, int httpStatusCode) {
+    super(t);
+    statusCode = httpStatusCode;
+  }
+  public ProcessingException(String message, int httpStatusCode) {
+    super(message);
+    statusCode = httpStatusCode;
+  }
+
+  public ProcessingException(String message, Throwable t, int httpStatusCode) {
+    super(message, t);
+    statusCode = httpStatusCode;
+  }
+
+  public int getHttpStatusCode() {
+    return statusCode;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/Processor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/Processor.java
new file mode 100644
index 0000000..1b6e6b1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/process/Processor.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.process;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.variables.VariableSubstituter;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Converts an input Context into an output Gadget.
+ */
+@Singleton
+public class Processor {
+  //class name for logging purpose
+  private static final String classname = Processor.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+  private final GadgetSpecFactory gadgetSpecFactory;
+  private final VariableSubstituter substituter;
+  private final ContainerConfig containerConfig;
+  private final GadgetAdminStore gadgetAdminStore;
+  private final FeatureRegistryProvider featureRegistryProvider;
+
+  @Inject
+  public Processor(GadgetSpecFactory gadgetSpecFactory,
+                   VariableSubstituter substituter,
+                   ContainerConfig containerConfig,
+                   GadgetAdminStore gadgetAdminStore,
+                   FeatureRegistryProvider featureRegistryProvider) {
+    this.gadgetSpecFactory = gadgetSpecFactory;
+    this.substituter = substituter;
+    this.gadgetAdminStore = gadgetAdminStore;
+    this.containerConfig = containerConfig;
+    this.featureRegistryProvider = featureRegistryProvider;
+  }
+
+  protected void validateGadgetUrl(Uri url) throws ProcessingException {
+    if (!"http".equalsIgnoreCase(url.getScheme()) && !"https".equalsIgnoreCase(url.getScheme())) {
+      throw new ProcessingException("Unsupported scheme (must be http or https).",
+          HttpServletResponse.SC_FORBIDDEN);
+    }
+  }
+
+  /**
+   * Process a single gadget. Creates a gadget from a retrieved GadgetSpec and context object,
+   * automatically performing variable substitution on the spec for use elsewhere.
+   *
+   * @throws ProcessingException If there is a problem processing the gadget.
+   */
+  public Gadget process(GadgetContext context) throws ProcessingException {
+    GadgetSpec spec;
+    FeatureRegistry featureRegistry;
+
+    try {
+      Uri url = gadgetSpecFactory.getGadgetUri(context);
+
+      if (url == null) {
+        throw new ProcessingException("Missing or malformed url parameter",
+            HttpServletResponse.SC_BAD_REQUEST);
+      }
+
+      validateGadgetUrl(url);
+      if (!gadgetAdminStore.isWhitelisted(context.getContainer(), url.toString())) {
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "process", MessageKeys.RENDER_NON_WHITELISTED_GADGET, new Object[] {url});
+        }
+        throw new ProcessingException("The requested gadget is not authorized for this container",
+            HttpServletResponse.SC_FORBIDDEN);
+      }
+
+      spec = gadgetSpecFactory.getGadgetSpec(context);
+      spec = substituter.substitute(context, spec);
+
+      if (context.getSanitize()) {
+        spec = spec.removeUrlViews();
+      }
+
+      featureRegistry = featureRegistryProvider.get(context.getRepository());
+    } catch (GadgetException e) {
+      throw new ProcessingException(e.getMessage(), e, e.getHttpStatusCode());
+    }
+
+    return new Gadget()
+        .setContext(context)
+        .setGadgetFeatureRegistry(featureRegistry)
+        .setSpec(spec)
+        .setCurrentView(getView(context, spec));
+  }
+
+  /**
+   * Attempts to extract the "current" view for the given gadget.
+   *
+   * There is common container JavaScript code that performs this same type of aliasing check before
+   * render. If the common container is being used, the view should never have to be aliased here.
+   */
+  private View getView(GadgetContext context, GadgetSpec spec) {
+    String viewName = context.getView();
+    View view = spec.getView(viewName);
+    if (view == null) {
+      String container = context.getContainer();
+      String property = "${Cur['gadgets.features'].views['" + viewName + "'].aliases}";
+      for (Object alias : containerConfig.getList(container, property)) {
+        viewName = alias.toString();
+        view = spec.getView(viewName);
+        if (view != null) {
+          return view;
+        }
+      }
+    }
+    if (view == null) {
+      view = spec.getView(GadgetSpec.DEFAULT_VIEW);
+    }
+    return view;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/CajaResponseRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/CajaResponseRewriter.java
new file mode 100644
index 0000000..dd75b8c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/CajaResponseRewriter.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.ExternalReference;
+import com.google.caja.lexer.FetchedData;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.JsLexer;
+import com.google.caja.lexer.JsTokenQueue;
+import com.google.caja.lexer.ParseException;
+import com.google.caja.lexer.TokenConsumer;
+
+import com.google.caja.parser.AncestorChain;
+import com.google.caja.parser.ParseTreeNode;
+import com.google.caja.parser.js.CajoledModule;
+import com.google.caja.parser.js.Parser;
+import com.google.caja.plugin.PipelineMaker;
+import com.google.caja.plugin.PluginCompiler;
+import com.google.caja.plugin.PluginMeta;
+import com.google.caja.plugin.UriFetcher;
+import com.google.caja.plugin.LoaderType;
+import com.google.caja.plugin.UriEffect;
+import com.google.caja.plugin.UriPolicy;
+import com.google.caja.render.Concatenator;
+import com.google.caja.render.JsMinimalPrinter;
+import com.google.caja.render.JsPrettyPrinter;
+import com.google.caja.reporting.BuildInfo;
+import com.google.caja.reporting.MessageContext;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.RenderContext;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.inject.Inject;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.RewriterUtils;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Rewriter that cajoles Javascript.
+ *
+ * @since 2.0.0
+ */
+public class CajaResponseRewriter implements ResponseRewriter {
+  //class name for logging purpose
+  private static final String classname = CajaResponseRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final RequestPipeline requestPipeline;
+
+  @Inject
+  public CajaResponseRewriter(RequestPipeline requestPipeline) {
+    this.requestPipeline = requestPipeline;
+  }
+
+  public void rewrite(HttpRequest req, HttpResponseBuilder resp, Gadget gadget)
+          throws RewritingException {
+    if (!req.isCajaRequested()) { return; }
+
+    // Only accept Javascript for now
+    if (!RewriterUtils.isJavascript(req, resp)) {
+      resp.setContent("");
+      resp.setHttpStatusCode(HttpResponse.SC_BAD_REQUEST);
+      return;
+    }
+
+    boolean passed = false;
+
+    MessageQueue mq = new SimpleMessageQueue();
+    MessageContext mc = new MessageContext();
+    Uri contextUri = req.getUri();
+    InputSource is = new InputSource(contextUri.toJavaUri());
+
+    PluginMeta pluginMeta = new PluginMeta(
+            proxyFetcher(req, contextUri), proxyUriPolicy(req));
+    PluginCompiler compiler = new PluginCompiler(BuildInfo.getInstance(),
+            pluginMeta, mq);
+    compiler.setMessageContext(mc);
+
+    // Parse the javascript
+    try {
+      StringReader strReader = new StringReader(resp.getContent());
+      CharProducer cp = CharProducer.Factory.create(strReader, is);
+      JsTokenQueue tq = new JsTokenQueue(new JsLexer(cp), is);
+      ParseTreeNode input = new Parser(tq, mq).parse();
+      tq.expectEmpty();
+
+      compiler.addInput(AncestorChain.instance(input).node, contextUri.toJavaUri());
+    } catch (ParseException e) {
+      // Don't bother continuing.
+      resp.setContent("");
+      return;
+    }
+
+    try {
+      if (RewriterUtils.isJavascript(req, resp)) {
+        compiler.setGoals(
+            compiler.getGoals().without(PipelineMaker.HTML_SAFE_STATIC));
+      }
+      passed = compiler.run();
+
+      CajoledModule outputJs = passed ? compiler.getJavascript() : null;
+
+      StringBuilder jsOut = new StringBuilder();
+      TokenConsumer printer;
+      if ("1".equals(req.getParam("debug"))) {
+        printer = new JsPrettyPrinter(new Concatenator(jsOut));
+      } else {
+        printer = new JsMinimalPrinter(new Concatenator(jsOut));
+      }
+
+      RenderContext renderContext = new RenderContext(printer).withEmbeddable(true);
+
+      if (outputJs != null) {
+        outputJs.render(renderContext);
+      }
+
+      renderContext.getOut().noMoreTokens();
+      resp.setContent(jsOut.toString());
+    } finally {
+      if (!passed) {
+        resp.setContent("");
+      }
+    }
+  }
+
+  private UriPolicy proxyUriPolicy(HttpRequest request) {
+    final Uri contextUri = request.getUri();
+    final Gadget stubGadget = DomWalker.makeGadget(request);
+
+    return new UriPolicy() {
+      public String rewriteUri(ExternalReference ref, UriEffect effect,
+          LoaderType loader, Map<String, ?> hints) {
+
+        Uri resourceUri = Uri.fromJavaUri(ref.getUri());
+        if (contextUri != null) {
+          resourceUri = contextUri.resolve(resourceUri);
+        }
+
+        ProxyUriManager.ProxyUri proxyUri = new ProxyUriManager.ProxyUri(
+            stubGadget, resourceUri);
+        return proxyUri.getResource().toString();
+      }
+    };
+  }
+
+  private UriFetcher proxyFetcher(final HttpRequest req, final Uri contextUri) {
+    return new UriFetcher() {
+      public FetchedData fetch(ExternalReference ref, String mimeType) throws UriFetchException {
+        Uri resourceUri = Uri.fromJavaUri(ref.getUri());
+        if (contextUri != null) {
+          resourceUri = contextUri.resolve(resourceUri);
+        }
+
+        HttpRequest request = new HttpRequest(resourceUri)
+                .setContainer(req.getContainer())
+                .setGadget(req.getGadget())
+                .setInternalRequest( true );
+
+        try {
+          HttpResponse response = requestPipeline.execute(request);
+          byte[] responseBytes = IOUtils.toByteArray(response.getResponse());
+          return FetchedData.fromBytes(responseBytes, mimeType, response.getEncoding(),
+              new InputSource(ref.getUri()));
+        } catch (GadgetException e) {
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "proxyFetcher", MessageKeys.FAILED_TO_RETRIEVE, new Object[] {ref.toString()});
+          }
+          return null;
+        } catch (IOException e) {
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "proxyFetcher", MessageKeys.FAILED_TO_READ, new Object[] {ref.toString()});
+          }
+          return null;
+        }
+      }
+    };
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/DefaultRpcServiceLookup.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/DefaultRpcServiceLookup.java
new file mode 100644
index 0000000..5beb151
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/DefaultRpcServiceLookup.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.base.Objects;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+/**
+ * Simple storage for holding the various services offered by containers.
+ *
+ * This storage is keyed by container, and within each container, the services are stored, keyed by,
+ * the endpoint url.
+ *
+ * Here is a json structure that shows how data is stored:
+ *
+ * { container1 : { "http://.../endpoint1" : ["system.listMethods", "people.get", "people.create",
+ * "people.delete"], ... }, "http://.../endpoint2" : { "system.listMethods", "cache.invalidate"],
+ * ... } }, container 2 : ..... }
+ */
+@Singleton
+public class DefaultRpcServiceLookup implements RpcServiceLookup {
+
+  private final Cache<String, Multimap<String, String>> containerServices;
+
+  private final ServiceFetcher fetcher;
+
+  /**
+   * @param fetcher  RpcServiceFetcher to retrieve services available from endpoints
+   * @param duration in seconds service definitions should remain in the cache
+   */
+  @Inject
+  public DefaultRpcServiceLookup(ServiceFetcher fetcher,
+      @Named("org.apache.shindig.serviceExpirationDurationMinutes")Long duration) {
+    this.containerServices = CacheBuilder.newBuilder()
+        .expireAfterWrite(duration * 60, TimeUnit.SECONDS)
+        .build();
+    this.fetcher = fetcher;
+  }
+
+  /**
+   * @param container Syndicator param identifying the container for whom we want services
+   * @param host      Host for which gadget is being rendered, used to do substitution in endpoints
+   * @return Map of Services, by endpoint for the given container.
+   */
+  public Multimap<String, String> getServicesFor(final String container, final String host) {
+    // Support empty container or host by providing empty services:
+    if (container == null || container.length() == 0 || host == null) {
+      return ImmutableMultimap.of();
+    }
+    try {
+      return containerServices.get(container,
+        new Callable<Multimap<String, String>>() {
+          public Multimap<String, String> call() {
+            return Objects.firstNonNull(fetcher.getServicesForContainer(container, host),
+                ImmutableMultimap.<String,String>of());
+          }
+        }
+    );
+    } catch (ExecutionException e) {
+      return ImmutableMultimap.of();
+    }
+  }
+
+  /**
+   * Setup the services for a given container.
+   *
+   * @param container     The param identifying this container.
+   * @param foundServices Map of services, keyed by endpoint.
+   */
+  void setServicesFor(String container, Multimap<String, String> foundServices) {
+    containerServices.asMap().put(container, foundServices);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/DefaultServiceFetcher.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/DefaultServiceFetcher.java
new file mode 100644
index 0000000..a410a14
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/DefaultServiceFetcher.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.apache.shindig.auth.AbstractSecurityToken.Keys.APP_URL;
+import static org.apache.shindig.auth.AbstractSecurityToken.Keys.OWNER;
+import static org.apache.shindig.auth.AbstractSecurityToken.Keys.VIEWER;
+
+import static org.apache.shindig.auth.AnonymousSecurityToken.ANONYMOUS_ID;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+
+import org.apache.shindig.auth.BlobCrypterSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenException;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Default implementation for the ServiceFetcher the rpc services for a container by fetching
+ * them from the container's system.listMethods endpoints as defined in the container config.
+ */
+public class DefaultServiceFetcher implements ServiceFetcher {
+  public static final String JSON_RESPONSE_WRAPPER_ELEMENT = "result";
+
+  public static final String OSAPI_FEATURE_CONFIG = "osapi";
+
+  public static final String OSAPI_SERVICES = "osapi.services";
+
+  public static final String GADGETS_FEATURES_CONFIG = "gadgets.features";
+
+  public static final String SYSTEM_LIST_METHODS_METHOD = "system.listMethods";
+
+  /** Key in container config that lists the endpoints offering services */
+  public static final String OSAPI_BASE_ENDPOINTS = "endPoints";
+
+  //class name for logging purpose
+  private static final String classname = DefaultServiceFetcher.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final ContainerConfig containerConfig;
+
+  private final HttpFetcher fetcher;
+
+  private Authority authority;
+  private SecurityTokenCodec codec;
+
+  /** @param config Container Config for looking up endpoints */
+  @Inject
+  public DefaultServiceFetcher(ContainerConfig config, HttpFetcher fetcher) {
+    this.containerConfig = config;
+    this.fetcher = fetcher;
+  }
+
+  @Inject(optional = true)
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  @Inject
+  public void setSecurityTokenCodec(SecurityTokenCodec codec) {
+    this.codec = codec;
+  }
+
+  /**
+   * Returns the services, keyed by endpoint for the given container.
+   *
+   * @param container The particular container whose services we want.
+   * @return Map endpoints and their serviceMethod list
+   */
+  public Multimap<String, String> getServicesForContainer(String container, String host) {
+    if (containerConfig == null) {
+      return ImmutableMultimap.<String, String>builder().build();
+    }
+    LinkedHashMultimap<String, String> endpointServices = LinkedHashMultimap.create();
+
+    // First check services directly declared in container config
+    @SuppressWarnings("unchecked")
+    Map<String, Object> declaredServices = (Map<String, Object>) containerConfig.getMap(container,
+        GADGETS_FEATURES_CONFIG).get(OSAPI_SERVICES);
+    if (declaredServices != null) {
+      for (Map.Entry<String, Object> entry : declaredServices.entrySet()) {
+        @SuppressWarnings("unchecked")
+        Iterable<String> entryValue = (Iterable<String>) entry.getValue();
+        endpointServices.putAll(entry.getKey(), entryValue);
+      }
+    }
+
+    // Merge services lazily loaded from the endpoints if any
+    List<String> endpoints = getEndpointsFromContainerConfig(container, host);
+    for (String endpoint : endpoints) {
+      String endpointVal = endpoint;
+      if ( endpoint.startsWith("//") && authority != null ){
+        endpointVal = authority.getScheme() + ':' + endpoint;
+      }
+      endpointServices.putAll(endpoint, retrieveServices(container, endpointVal.replace("%host%", host)));
+    }
+    return ImmutableMultimap.copyOf(endpointServices);
+  }
+
+  @SuppressWarnings("unchecked")
+  protected List<String> getEndpointsFromContainerConfig(String container, String host) {
+    Map<String, Object> properties = (Map<String, Object>) containerConfig.getMap(container,
+        GADGETS_FEATURES_CONFIG).get(OSAPI_FEATURE_CONFIG);
+
+    if (properties != null) {
+      return (List<String>) properties.get(OSAPI_BASE_ENDPOINTS);
+    }
+    return ImmutableList.of();
+  }
+
+  protected Set<String> retrieveServices(String container, String endpoint) {
+    try {
+      StringBuilder sb = new StringBuilder( 250 );
+      sb.append(endpoint).append( "?method=" + SYSTEM_LIST_METHODS_METHOD );
+      Map<String, String> parms = Maps.newHashMap();
+      parms.put( OWNER.getKey(), ANONYMOUS_ID );
+      parms.put( VIEWER.getKey(), ANONYMOUS_ID );
+      parms.put( APP_URL.getKey(), "0" );
+      SecurityToken token = new BlobCrypterSecurityToken(container, "*", "0", parms);
+      sb.append( "&st=" ).append( codec.encodeToken( token ));
+      Uri url = Uri.parse(sb.toString());
+      HttpRequest request = new HttpRequest(url).setInternalRequest(true);
+
+      HttpResponse response = fetcher.fetch(request);
+      if (response.getHttpStatusCode() == HttpResponse.SC_OK) {
+        return getServicesFromJsonResponse(response.getResponseAsString());
+      } else {
+        if (LOG.isLoggable(Level.SEVERE)) {
+          LOG.logp(Level.SEVERE, classname, "retrieveServices", MessageKeys.HTTP_ERROR_FETCHING, new Object[] {response.getHttpStatusCode(),endpoint});
+        }
+      }
+    } catch (SecurityTokenException se) {
+      if (LOG.isLoggable(Level.SEVERE)) {
+        LOG.logp(Level.SEVERE, classname, "retrieveServices", MessageKeys.FAILED_TO_FETCH_SERVICE, new Object[] {endpoint,se.getMessage()});
+      }
+    } catch (GadgetException ge) {
+      if (LOG.isLoggable(Level.SEVERE)) {
+        LOG.logp(Level.SEVERE, classname, "retrieveServices", MessageKeys.FAILED_TO_FETCH_SERVICE, new Object[] {endpoint,ge.getMessage()});
+      }
+    } catch (JSONException je) {
+      if (LOG.isLoggable(Level.SEVERE)) {
+        LOG.logp(Level.SEVERE, classname, "retrieveServices", MessageKeys.FAILED_TO_PARSE_SERVICE, new Object[] {endpoint,je.getMessage()});
+      }
+    }
+    return ImmutableSet.of();
+  }
+
+  protected Set<String> getServicesFromJsonResponse(String content)
+      throws JSONException {
+    ImmutableSet.Builder<String> services = ImmutableSet.builder();
+    JSONObject js = new JSONObject(content);
+    JSONArray json = js.getJSONArray(JSON_RESPONSE_WRAPPER_ELEMENT);
+    for (int i = 0; i < json.length(); i++) {
+      String o = json.getString(i);
+      services.add(o);
+    }
+    return services.build();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/GadgetRewritersProvider.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/GadgetRewritersProvider.java
new file mode 100644
index 0000000..477e796
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/GadgetRewritersProvider.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import java.util.List;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Class to provide list of rewriters according to gadget request.
+ * Provide different list of rewriters for html accelerate request
+ *
+ * @since 2.0.0
+ */
+public class GadgetRewritersProvider {
+  private final List<GadgetRewriter> renderRewriters;
+
+  @Inject
+  public GadgetRewritersProvider(
+      @Named("shindig.rewriters.gadget") List<GadgetRewriter> renderRewriters) {
+    this.renderRewriters = renderRewriters;
+  }
+
+  public List<GadgetRewriter> getRewriters(GadgetContext context) {
+    return renderRewriters;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/HtmlRenderer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/HtmlRenderer.java
new file mode 100644
index 0000000..d7adbae
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/HtmlRenderer.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.preload.PreloaderService;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.spec.View;
+
+import java.util.Collection;
+
+import com.google.inject.Inject;
+
+/**
+ * Handles producing output markup for a gadget based on the provided context.
+ */
+public class HtmlRenderer {
+  public static final String PATH_PARAM = "path";
+  private final PreloaderService preloader;
+  private final ProxyRenderer proxyRenderer;
+  private final GadgetRewritersProvider gadgetRewritersProvider;
+  private final GadgetHtmlParser htmlParser;
+
+  @Inject
+  public HtmlRenderer(PreloaderService preloader,
+                      ProxyRenderer proxyRenderer,
+                      GadgetRewritersProvider gadgetRewritersProvider,
+                      GadgetHtmlParser htmlParser) {
+    this.preloader = preloader;
+    this.proxyRenderer = proxyRenderer;
+    this.gadgetRewritersProvider = gadgetRewritersProvider;
+    this.htmlParser = htmlParser;
+  }
+
+  /**
+   * Render the gadget into a string by performing the following steps:
+   *
+   * - Retrieve gadget specification information (GadgetSpec, MessageBundle, etc.)
+   *
+   * - Fetch any preloaded data needed to handle the request, as handled by Preloader.
+   *
+   * - Perform rewriting operations on the output content, handled by Rewriter.
+   *
+   * @param gadget The gadget for the rendering operation.
+   * @return The rendered gadget content
+   * @throws RenderingException if any issues arise that prevent rendering.
+   */
+  public String render(Gadget gadget) throws RenderingException {
+    try {
+      View view = gadget.getCurrentView();
+
+      // We always execute these preloads, they have nothing to do with the cache output.
+      Collection<PreloadedData> preloads = preloader.preload(gadget);
+      gadget.setPreloads(preloads);
+
+      String content;
+
+      if (view.getHref() == null) {
+        content = getViewContent(gadget);
+      } else {
+        content = proxyRenderer.render(gadget);
+      }
+
+      MutableContent mc = new MutableContent(htmlParser, content);
+      for (GadgetRewriter rewriter :
+          gadgetRewritersProvider.getRewriters(gadget.getContext())) {
+        rewriter.rewrite(gadget, mc);
+      }
+
+      return mc.getContent();
+    } catch (GadgetException e) {
+      throw new RenderingException(e.getMessage(), e, e.getHttpStatusCode());
+    } catch (RewritingException e) {
+      throw new RenderingException(e.getMessage(), e, e.getHttpStatusCode());
+    }
+  }
+
+  protected String getViewContent(Gadget gadget) {
+    View currentView = gadget.getCurrentView();
+    return currentView.getContent();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriter.java
new file mode 100644
index 0000000..3cf9ba8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriter.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Produce data constants that are needed by the opensocial-i18n
+ * feature based on user locale.
+ */
+public class OpenSocialI18NGadgetRewriter implements GadgetRewriter {
+  private static final String I18N_FEATURE_NAME = "opensocial-i18n";
+  private static final String DATA_PATH = "features/i18n/data/";
+  private Map<Locale, String> i18nConstantsCache = new ConcurrentHashMap<Locale, String>();
+
+  public void rewrite(Gadget gadget, MutableContent mutableContent) throws RewritingException {
+    // Don't touch sanitized gadgets.
+    if (gadget.sanitizeOutput()) {
+      return;
+    }
+    // Quickly return if opensocial-i18n feature is not needed.
+    if (!gadget.getAllFeatures().contains(I18N_FEATURE_NAME)) {
+      return;
+    }
+
+    try {
+      Document document = mutableContent.getDocument();
+      Element head = (Element)DomUtil.getFirstNamedChildNode(document.getDocumentElement(), "head");
+      injectI18NConstants(gadget, head);
+      mutableContent.documentChanged();
+    } catch (GadgetException e) {
+      throw new RewritingException(e, e.getHttpStatusCode());
+    }
+  }
+
+  private void injectI18NConstants(Gadget gadget, Node headTag) throws GadgetException {
+    StringBuilder inlineJs = new StringBuilder();
+    Locale locale = gadget.getContext().getLocale();
+    if (i18nConstantsCache.containsKey(locale)) {
+      inlineJs.append(i18nConstantsCache.get(locale));
+    } else {
+      // load gadgets.i18n.DateTimeConstants and gadgets.i18n.NumberFormatConstants
+      String localeName = getLocaleNameForLoadingI18NConstants(locale);
+      String dateTimeConstantsResource = "DateTimeConstants__" + localeName + ".js";
+      String numberConstantsResource = "NumberFormatConstants__" + localeName + ".js";
+      try {
+        inlineJs.append(attemptToLoadResource(dateTimeConstantsResource))
+            .append('\n').append(attemptToLoadResource(numberConstantsResource));
+        i18nConstantsCache.put(locale, inlineJs.toString());
+      } catch (IOException e) {
+        throw new GadgetException(GadgetException.Code.INVALID_CONFIG,
+            "Unexpected inability to load i18n data for locale: " + localeName,
+            HttpResponse.SC_INTERNAL_SERVER_ERROR);
+      }
+    }
+    Element inlineTag = headTag.getOwnerDocument().createElement("script");
+    headTag.appendChild(inlineTag);
+    inlineTag.appendChild(headTag.getOwnerDocument().createTextNode(inlineJs.toString()));
+  }
+
+  String getLocaleNameForLoadingI18NConstants(Locale locale) {
+    String localeName = "en";
+    String language = locale.getLanguage();
+    String country = locale.getCountry();
+    if (!language.equalsIgnoreCase("ALL")) {
+      try {
+        attemptToLoadDateConstants(language);
+        localeName = language;
+      } catch (IOException e) {
+        // ignore
+      }
+    }
+
+    if (!country.equalsIgnoreCase("ALL")) {
+      try {
+        attemptToLoadDateConstants(localeName + '_' + country);
+        localeName += '_' + country;
+      } catch (IOException e) {
+        // ignore
+      }
+    }
+    return localeName;
+  }
+
+  protected String attemptToLoadDateConstants(String localeName) throws IOException {
+    return attemptToLoadResource("DateTimeConstants__" + localeName + ".js");
+  }
+
+  private String attemptToLoadResource(String i18nRes) throws IOException {
+    return attemptToLoadResourceFullyQualified(DATA_PATH + i18nRes);
+  }
+
+  protected String attemptToLoadResourceFullyQualified(String resource) throws IOException {
+    return ResourceLoader.getContent(resource);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/ProxyRenderer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/ProxyRenderer.java
new file mode 100644
index 0000000..bfb00f5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/ProxyRenderer.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpCache;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.preload.PipelineExecutor;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.View;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Implements proxied rendering.
+ */
+public class ProxyRenderer {
+  public static final String PATH_PARAM = "path";
+  public static final String UA_IDENT = "Shindig";
+
+  private final RequestPipeline requestPipeline;
+  private final HttpCache httpCache;
+  private final PipelineExecutor pipelineExecutor;
+
+  /**
+   * @param requestPipeline Used for performing the proxy request. Always ignores caching because
+   *                        we want to skip preloading when the object is in the cache.
+   * @param httpCache The shared http cache. Used before checking the request pipeline to determine
+   *                  whether to perform the preload / fetch cycle.
+   */
+  @Inject
+  public ProxyRenderer(RequestPipeline requestPipeline,
+      HttpCache httpCache, PipelineExecutor pipelineExecutor) {
+    this.requestPipeline = requestPipeline;
+    this.httpCache = httpCache;
+    this.pipelineExecutor = pipelineExecutor;
+  }
+
+  public String render(Gadget gadget) throws RenderingException, GadgetException {
+    View view = gadget.getCurrentView();
+    Uri href = view.getHref();
+    Preconditions.checkArgument(href != null, "Gadget does not have href for the current view");
+
+    GadgetContext context = gadget.getContext();
+    String path = context.getParameter(PATH_PARAM);
+    if (path != null) {
+      try {
+        Uri relative = Uri.parse(path);
+        if (!relative.isAbsolute()) {
+          href = href.resolve(relative);
+        }
+      } catch (IllegalArgumentException e) {
+        // TODO: Spec does not say what to do for an invalid relative path.
+        // Just ignoring for now.
+      }
+    }
+
+    UriBuilder uri = new UriBuilder(href);
+    uri.addQueryParameter("lang", context.getLocale().getLanguage());
+    uri.addQueryParameter("country", context.getLocale().getCountry());
+
+    OAuthArguments oauthArgs = new OAuthArguments(view);
+    OAuth2Arguments oauth2Args = new OAuth2Arguments(view);
+    oauthArgs.setProxiedContentRequest(true);
+
+    HttpRequest request = new HttpRequest(uri.toUri())
+        .setIgnoreCache(context.getIgnoreCache())
+        .setOAuthArguments(oauthArgs)
+        .setOAuth2Arguments(oauth2Args)
+        .setAuthType(view.getAuthType())
+        .setSecurityToken(context.getToken())
+        .setContainer(context.getContainer())
+        .setGadget(gadget.getSpec().getUrl());
+    setUserAgent(request, context);
+
+    HttpResponse response = httpCache.getResponse(request);
+
+    if (response == null || response.isStale()) {
+      HttpRequest proxyRequest = createPipelinedProxyRequest(gadget, request);
+      response = requestPipeline.execute(proxyRequest);
+      httpCache.addResponse(request, response);
+    }
+
+    if (response.isError()) {
+      throw new RenderingException("Unable to reach remote host. HTTP status " +
+        response.getHttpStatusCode(), HttpServletResponse.SC_NOT_FOUND);
+    }
+
+    return response.getResponseAsString();
+  }
+
+  /**
+   * Creates a proxy request by fetching pipelined data and adding it to an existing request.
+   *
+   */
+  protected HttpRequest createPipelinedProxyRequest(Gadget gadget, HttpRequest original) {
+    HttpRequest request = new HttpRequest(original);
+    request.setIgnoreCache(true);
+
+    PipelinedData data = gadget.getCurrentView().getPipelinedData();
+    if (data != null) {
+      PipelineExecutor.Results results =
+        pipelineExecutor.execute(gadget.getContext(), ImmutableList.of(data));
+
+      if (results != null && !results.results.isEmpty()) {
+        String postContent = JsonSerializer.serialize(results.results);
+        // POST the preloaded content, with a method override of GET
+        // to enable caching
+        request.setMethod("POST")
+            .setPostBody(CharsetUtil.getUtf8Bytes(postContent))
+            .setHeader("Content-Type", "application/json;charset=utf-8");
+      }
+    }
+
+    return request;
+  }
+
+  /**
+   * Sets the User-Agent header in the new request to a variant of the original
+   * request's User-Agent, plus a small ident string for the gadget server.
+   */
+  private void setUserAgent(HttpRequest request, GadgetContext context) {
+    String userAgent = context.getUserAgent();
+    if (userAgent != null) {
+      String myIdent = getUAIdent();
+      if (myIdent != null) {
+        userAgent = userAgent + ' ' + myIdent;
+      }
+      request.setHeader("User-Agent", userAgent);
+    }
+  }
+
+  /**
+   * Returns the program name which will be added at the end of the User-Agent
+   * string, to identify the gadget server.
+   */
+  protected String getUAIdent() {
+    return UA_IDENT;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderModule.java
new file mode 100644
index 0000000..6c336f1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderModule.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import com.google.common.collect.ImmutableSet;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import java.util.Set;
+
+/**
+ * Guice bindings for the render package.
+ */
+public class RenderModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    // NOTE: Sanitization only works when using the "full" Neko HTML parser. It is not recommended
+    // that you attempt to use sanitization without it.
+  }
+
+  @Provides
+  @Singleton
+  @SanitizingGadgetRewriter.AllowedTags
+  protected Set<String> provideAllowedTags() {
+    return ImmutableSet.of("a", "abbr", "acronym", "area", "b", "bdo", "big", "blockquote",
+        "body", "br", "caption", "center", "cite", "code", "col", "colgroup", "dd", "del",
+        "dfn", "div", "dl", "dt", "em", "font", "h1", "h2", "h3", "h4", "h5", "h6", "head",
+        "hr", "html", "i", "img", "ins", "legend", "li", "link", "map", "ol", "p", "pre",
+        "q", "s", "samp", "small", "span", "strike", "strong", "style", "sub", "sup", "table",
+        "tbody", "td", "tfoot", "th", "thead", "tr", "tt", "u", "ul");
+  }
+
+  @Provides
+  @Singleton
+  @SanitizingGadgetRewriter.AllowedAttributes
+  protected Set<String> provideAllowedAttributes() {
+    return ImmutableSet.of("abbr", "align", "alt", "axis", "bgcolor", "border",
+        "cellpadding", "cellspacing", "char", "charoff", "cite", "class", "clear", "color",
+        "cols", "colspan", "compact", "coords", "datetime", "dir", "face", "headers", "height",
+        "href", "hreflang", "hspace", "id", "ismap", "lang", "longdesc", "name", "nohref",
+        "noshade", "nowrap", "rel", "rev", "rowspan", "rules", "scope", "shape", "size", "span",
+        "src", "start", "style", "summary", "title", "type", "usemap", "valign", "value",
+        "vspace", "width");
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/Renderer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/Renderer.java
new file mode 100644
index 0000000..9ac99e5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/Renderer.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.View;
+
+import com.google.inject.Inject;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+/**
+ * Validates a rendering request parameters before calling an appropriate renderer.
+ */
+public class Renderer {
+  //class name for logging purpose
+  private static final String classname = Renderer.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final Processor processor;
+  private final HtmlRenderer renderer;
+  private final ContainerConfig containerConfig;
+  private final LockedDomainService lockedDomainService;
+
+  @Inject
+  public Renderer(Processor processor,
+                  HtmlRenderer renderer,
+                  ContainerConfig containerConfig,
+                  LockedDomainService lockedDomainService) {
+    this.processor = processor;
+    this.renderer = renderer;
+    this.containerConfig = containerConfig;
+    this.lockedDomainService = lockedDomainService;
+  }
+
+  /**
+   * Attempts to render the requested gadget.
+   *
+   * @return The results of the rendering attempt.
+   *
+   * TODO: Localize error messages.
+   */
+  public RenderingResults render(GadgetContext context) {
+    if (!validateParent(context)) {
+      return RenderingResults.error("Unsupported parent parameter. Check your container code.",
+          HttpServletResponse.SC_BAD_REQUEST);
+    }
+
+    try {
+      Gadget gadget = processor.process(context);
+
+      GadgetSpec gadgetSpec = gadget.getSpec();
+      if (gadget.getCurrentView() == null) {
+        return RenderingResults.error("Unable to locate an appropriate view in this gadget. " +
+            "Requested: '" + gadget.getContext().getView() +
+            "' Available: " + gadgetSpec.getViews().keySet(), HttpServletResponse.SC_NOT_FOUND);
+      }
+
+      if (gadget.getCurrentView().getType() == View.ContentType.URL) {
+        if (gadget.requiresCaja()) {
+          return RenderingResults.error("Caja does not support url type gadgets.",
+            HttpServletResponse.SC_BAD_REQUEST);
+        } else if (gadget.sanitizeOutput()) {
+          return RenderingResults.error("Type=url gadgets cannot be sanitized.",
+            HttpServletResponse.SC_BAD_REQUEST);
+        }
+        return RenderingResults.mustRedirect(gadget.getCurrentView().getHref());
+      }
+
+      if (!lockedDomainService.isGadgetValidForHost(context.getHost(), gadget, context.getContainer())) {
+        return RenderingResults.error("Invalid domain for host (" + context.getHost()
+                + ") and gadget (" + gadgetSpec.getUrl() + ")",
+                HttpServletResponse.SC_BAD_REQUEST);
+      }
+
+      return RenderingResults.ok(renderer.render(gadget));
+    } catch (RenderingException e) {
+      return logError("render", context.getUrl(), e.getHttpStatusCode(), e);
+    } catch (ProcessingException e) {
+      return logError("render", context.getUrl(), e.getHttpStatusCode(), e);
+    } catch (RuntimeException e) {
+      if (e.getCause() instanceof GadgetException) {
+        return logError("render", context.getUrl(), ((GadgetException)e.getCause()).getHttpStatusCode(),
+            e.getCause());
+      }
+      throw e;
+    }
+  }
+
+  private RenderingResults logError(String methodname, Uri gadgetUrl, int statusCode, Throwable t) {
+    if (LOG.isLoggable(Level.INFO)) {
+      LOG.logp(Level.INFO, classname, methodname, MessageKeys.FAILED_TO_RENDER, new Object[] {gadgetUrl,t.getMessage()});
+    }
+    return RenderingResults.error(t.getMessage(), statusCode);
+  }
+
+  /**
+   * Validates that the parent parameter was acceptable.
+   *
+   * @return True if the parent parameter is valid for the current container.
+   */
+  private boolean validateParent(GadgetContext context) {
+    String container = context.getContainer();
+    String parent = context.getParameter("parent");
+
+    if (parent == null) {
+      // If there is no parent parameter, we are still safe because no
+      // dependent code ever has to trust it anyway.
+      return true;
+    }
+
+    List<Object> parents = containerConfig.getList(container, "gadgets.parent");
+    if (parents.isEmpty()) {
+      // Allow all.
+      return true;
+    }
+
+    // We need to check each possible parent parameter against this regex.
+    for (Object pattern : parents) {
+      if (Pattern.matches(pattern.toString(), parent)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingException.java
new file mode 100644
index 0000000..e8fbdb1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingException.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+/**
+ * Exceptions thrown during gadget rendering.
+ *
+ * These execeptions will usually translate directly into an end-user error message, so they should
+ * be easily localizable.
+ */
+public class RenderingException extends Exception {
+  private final int httpStatusCode;
+
+  public RenderingException(Throwable t, int httpStatusCode) {
+    super(t);
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public RenderingException(String message, int httpStatusCode) {
+    super(message);
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public RenderingException(String message, Throwable t, int httpStatusCode) {
+    super(message, t);
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public int getHttpStatusCode() {
+    return httpStatusCode;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriter.java
new file mode 100644
index 0000000..c66394f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriter.java
@@ -0,0 +1,571 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.el.ELContext;
+import javax.el.PropertyNotFoundException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetELResolver;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetException.Code;
+import org.apache.shindig.gadgets.MessageBundleFactory;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.UnsupportedFeatureException;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.config.ConfigProcessor;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.js.JsException;
+import org.apache.shindig.gadgets.js.JsRequest;
+import org.apache.shindig.gadgets.js.JsRequestBuilder;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.js.JsServingPipeline;
+import org.apache.shindig.gadgets.preload.PreloadException;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+import org.apache.shindig.gadgets.spec.UserPref;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.templates.MessageELResolver;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriCommon;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentType;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Text;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Produces a valid HTML document for the gadget output, automatically inserting appropriate HTML
+ * document wrapper data as needed.
+ *
+ * Currently, this is only invoked directly since the rewriting infrastructure doesn't properly
+ * deal with uncacheable rewrite operations.
+ *
+ * TODO: Break this up into multiple rewriters.
+ *
+ * Should be:
+ *
+ * - UserPrefs injection
+ * - Javascript injection (including configuration)
+ * - html document normalization
+ */
+public class RenderingGadgetRewriter implements GadgetRewriter {
+  //class name for logging purpose
+  private static final String classname = RenderingGadgetRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  protected static final String DEFAULT_CSS =
+      "body,td,div,span,p{font-family:arial,sans-serif;}" +
+      "a {color:#0000cc;}a:visited {color:#551a8b;}" +
+      "a:active {color:#ff0000;}" +
+      "body{margin: 0px;padding: 0px;background-color:white;}";
+  protected static final String SCROLLING_CSS =
+      "html,body{height:100%;width:100%;overflow:auto;}";
+  static final String IS_GADGET_BEACON = "window['__isgadget']=true;";
+  static final String INSERT_BASE_ELEMENT_KEY = "gadgets.insertBaseElement";
+  static final String REWRITE_DOCTYPE_QNAME = "gadgets.doctype_qname";
+  static final String REWRITE_DOCTYPE_PUBID = "gadgets.doctype_pubid";
+  static final String REWRITE_DOCTYPE_SYSID = "gadgets.doctype_sysid";
+  static final String FEATURES_KEY = "gadgets.features";
+
+  protected final MessageBundleFactory messageBundleFactory;
+  protected final ContainerConfig containerConfig;
+  protected final FeatureRegistryProvider featureRegistryProvider;
+  protected final JsServingPipeline jsServingPipeline;
+  protected final JsUriManager jsUriManager;
+  protected final ConfigProcessor configProcessor;
+  protected final GadgetAdminStore gadgetAdminStore;
+
+  protected Set<String> defaultExternLibs = ImmutableSet.of();
+
+  protected Boolean externalizeFeatures = false;
+
+  // DOCTYPE for HTML5, OpenSocial 2.0 default
+  private String defaultDoctypeQName = "html";
+  private String defaultDoctypePubId = null;
+  private String defaultDoctypeSysId = null;
+
+  private final Expressions expressions;
+  private ELContext elContext;
+  /**
+   * @param messageBundleFactory Used for injecting message bundles into gadget output.
+   */
+  @Inject
+  public RenderingGadgetRewriter(MessageBundleFactory messageBundleFactory,
+                                 Expressions expressions,
+                                 ContainerConfig containerConfig,
+                                 FeatureRegistryProvider featureRegistryProvider,
+                                 JsServingPipeline jsServingPipeline,
+                                 JsUriManager jsUriManager,
+                                 ConfigProcessor configProcessor,
+                                 GadgetAdminStore gadgetAdminStore) {
+    this.messageBundleFactory = messageBundleFactory;
+    this.expressions = expressions;
+    this.containerConfig = containerConfig;
+    this.featureRegistryProvider = featureRegistryProvider;
+    this.jsServingPipeline = jsServingPipeline;
+    this.jsUriManager = jsUriManager;
+    this.configProcessor = configProcessor;
+    this.gadgetAdminStore = gadgetAdminStore;
+  }
+
+  public void setDefaultDoctypeQName(String qname) {
+      this.defaultDoctypeQName = qname;
+  }
+
+  public void setDefaultDoctypePubId( String pubid) {
+      this.defaultDoctypePubId = pubid;
+  }
+
+  public void setDefaultDoctypeSysId( String sysid) {
+    this.defaultDoctypeSysId = sysid;
+  }
+
+  @Inject
+  public void setDefaultForcedLibs(@Named("shindig.gadget-rewrite.default-forced-libs")String forcedLibs) {
+    if (StringUtils.isNotBlank(forcedLibs)) {
+      defaultExternLibs = ImmutableSortedSet.copyOf(Splitter.on(':').split(forcedLibs));
+    }
+  }
+
+  @Inject(optional = true)
+  public void setExternalizeFeatureLibs(@Named("shindig.gadget-rewrite.externalize-feature-libs")Boolean externalizeFeatures) {
+    this.externalizeFeatures = externalizeFeatures;
+  }
+
+  /** Process the children of an element or document. */
+  public void processChildNodes(Node source) {
+    NodeList nodes = source.getChildNodes();
+    for (int i = 0; i < nodes.getLength(); i++) {
+      processNode(nodes.item(i));
+    }
+  }
+
+  /**
+   * Process a node.
+   *
+   * @param result the target node where results should be inserted
+   * @param source the source node of the template being processed
+   */
+  private void processNode(Node source) {
+    switch (source.getNodeType()) {
+    case Node.TEXT_NODE:
+      try {
+        source.setTextContent(String.valueOf(expressions.parse(source.getTextContent(), String.class)
+              .getValue(elContext)));
+      } catch (PropertyNotFoundException pe) {
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.log(Level.INFO, pe.getMessage(), pe);
+        }
+      }
+      break;
+    case Node.ELEMENT_NODE:
+      processChildNodes(source);
+      break;
+    case Node.DOCUMENT_NODE:
+      processChildNodes(source);
+      break;
+    }
+  }
+
+  public void rewrite(Gadget gadget, MutableContent mutableContent) throws RewritingException {
+    // Don't touch sanitized gadgets.
+    if (gadget.sanitizeOutput()) {
+      return;
+    }
+
+    try {
+      GadgetContext context = gadget.getContext();
+      MessageBundle bundle = messageBundleFactory.getBundle(gadget.getSpec(), context.getLocale(),
+              context.getIgnoreCache(), context.getContainer(), context.getView());
+
+      MessageELResolver messageELResolver = new MessageELResolver(expressions, bundle);
+
+      this.elContext = expressions.newELContext(messageELResolver,
+              new GadgetELResolver(gadget.getContext()));
+      this.elContext.putContext(GadgetContext.class, elContext);
+      Document document = mutableContent.getDocument();
+      processChildNodes(document);
+      Element head = (Element) DomUtil.getFirstNamedChildNode(document.getDocumentElement(), "head");
+
+      // Insert new content before any of the existing children of the head element
+      Node firstHeadChild = head.getFirstChild();
+
+      Element injectedStyle = document.createElement("style");
+      injectedStyle.setAttribute("type", "text/css");
+      head.insertBefore(injectedStyle, firstHeadChild);
+
+      // Inject default scrolling to the body
+      this.injectDefaultScrolling(injectedStyle);
+
+      // Only inject default styles if no doctype was specified.
+      if (document.getDoctype() == null) {
+        injectedStyle.appendChild(injectedStyle.getOwnerDocument().
+            createTextNode(DEFAULT_CSS));
+      }
+      // Override & insert DocType if Gadget is written for OpenSocial 2.0 or greater,
+      // if quirksmode is not set
+      if(gadget.getSpecificationVersion().isEqualOrGreaterThan("2.0.0")
+          && !gadget.useQuirksMode()){
+        String container = gadget.getContext().getContainer();
+        String doctype_qname = defaultDoctypeQName;
+        String doctype_sysid = defaultDoctypeSysId;
+        String doctype_pubid = defaultDoctypePubId;
+        String value = containerConfig.getString(container, REWRITE_DOCTYPE_QNAME);
+        if(value != null){
+          doctype_qname = value;
+        }
+        value = containerConfig.getString(container, REWRITE_DOCTYPE_SYSID);
+        if(value != null){
+          doctype_sysid = value;
+        }
+        value = containerConfig.getString(container, REWRITE_DOCTYPE_PUBID);
+        if(value != null){
+          doctype_pubid = value;
+        }
+        //Don't inject DOCTYPE if QName is null
+        if(doctype_qname != null){
+          DocumentType docTypeNode = document.getImplementation()
+              .createDocumentType(doctype_qname, doctype_pubid, doctype_sysid);
+          if(document.getDoctype() != null){
+            document.removeChild(document.getDoctype());
+          }
+          document.insertBefore(docTypeNode, document.getFirstChild());
+        }
+      }
+
+      Element html= (Element)document.getElementsByTagName("html").item(0);
+      if(html != null){
+        Locale locale = gadget.getContext().getLocale();
+        if (locale != null) {
+          String locStr = locale.toString();
+          String locValue = locStr.replace("_", "-");
+          html.setAttribute("lang", locValue);
+          html.setAttribute("xml:lang", locValue);
+        }
+      }
+
+      injectBaseTag(gadget, head);
+      injectGadgetBeacon(gadget, head, firstHeadChild);
+      injectFeatureLibraries(gadget, head, firstHeadChild);
+
+      // This can be one script block.
+      Element mainScriptTag = document.createElement("script");
+      injectMessageBundles(bundle, mainScriptTag);
+      injectDefaultPrefs(gadget, mainScriptTag);
+      injectPreloads(gadget, mainScriptTag);
+
+      // We need to inject our script before any developer scripts.
+      head.insertBefore(mainScriptTag, firstHeadChild);
+
+      Element body = (Element)DomUtil.getFirstNamedChildNode(document.getDocumentElement(), "body");
+
+      body.setAttribute("dir", bundle.getLanguageDirection());
+
+      // With Caja enabled, onloads are triggered by features/caja/taming.js
+      if (!gadget.requiresCaja()) {
+        injectOnLoadHandlers(body);
+      }
+
+      mutableContent.documentChanged();
+    } catch (GadgetException e) {
+      throw new RewritingException(e.getLocalizedMessage(), e, e.getHttpStatusCode());
+    }
+  }
+
+  protected void injectDefaultScrolling(Element injectedStyle) {
+    injectedStyle.appendChild(injectedStyle.getOwnerDocument().
+        createTextNode(SCROLLING_CSS));
+  }
+
+  protected void injectBaseTag(Gadget gadget, Node headTag) {
+    GadgetContext context = gadget.getContext();
+    if (containerConfig.getBool(context.getContainer(), INSERT_BASE_ELEMENT_KEY)) {
+      Uri base = gadget.getSpec().getUrl();
+      View view = gadget.getCurrentView();
+      if (view != null && view.getHref() != null) {
+        base = view.getHref();
+      }
+      Element baseTag = headTag.getOwnerDocument().createElement("base");
+      baseTag.setAttribute("href", base.toString());
+      headTag.insertBefore(baseTag, headTag.getFirstChild());
+    }
+  }
+
+  protected void injectOnLoadHandlers(Node bodyTag) {
+    Element onloadScript = bodyTag.getOwnerDocument().createElement("script");
+    bodyTag.appendChild(onloadScript);
+    onloadScript.appendChild(bodyTag.getOwnerDocument().createTextNode(
+        "gadgets.util.runOnLoadHandlers();"));
+  }
+
+
+  /**
+   * @throws GadgetException
+   */
+  protected void injectGadgetBeacon(Gadget gadget, Node headTag, Node firstHeadChild)
+          throws GadgetException {
+    Element beaconNode = headTag.getOwnerDocument().createElement("script");
+    beaconNode.setTextContent(IS_GADGET_BEACON);
+    headTag.insertBefore(beaconNode, firstHeadChild);
+  }
+
+  protected String getFeatureRepositoryId(Gadget gadget) {
+    GadgetContext context = gadget.getContext();
+    return context.getRepository();
+  }
+
+  /**
+   * Injects javascript libraries needed to satisfy feature dependencies.
+   */
+  protected void injectFeatureLibraries(Gadget gadget, Node headTag, Node firstHeadChild)
+          throws GadgetException {
+    // TODO: If there isn't any js in the document, we can skip this. Unfortunately, that means
+    // both script tags (easy to detect) and event handlers (much more complex).
+    GadgetContext context = gadget.getContext();
+    String repository = getFeatureRepositoryId(gadget);
+    FeatureRegistry featureRegistry = featureRegistryProvider.get(repository);
+
+    checkRequiredFeatures(gadget, featureRegistry);
+    //Check to make sure all the required features that are about to be injected are allowed
+    if(!gadgetAdminStore.checkFeatureAdminInfo(gadget)) {
+      throw new GadgetException(Code.GADGET_ADMIN_FEATURE_NOT_ALLOWED);
+    }
+
+    // Set of extern libraries requested by the container
+    Set<String> externForcedLibs = defaultExternLibs;
+
+    // gather the libraries we'll need to generate the extern script for
+    String externParam = context.getParameter("libs");
+    if (StringUtils.isNotBlank(externParam)) {
+      externForcedLibs = Sets.newTreeSet(Splitter.on(':').split(externParam));
+    }
+
+    // Inject extern script
+    if (!externForcedLibs.isEmpty()) {
+      injectScript(externForcedLibs, null, false, gadget, headTag, firstHeadChild, "");
+    }
+
+    Collection<String> gadgetLibs = Lists.newArrayList(gadget.getDirectFeatureDeps());
+    List<Feature> gadgetFeatures = gadget.getSpec().getModulePrefs().getAllFeatures();
+    for(Feature feature : gadgetFeatures) {
+      if(!feature.getRequired() &&
+              !gadgetAdminStore.isAllowedFeature(feature, gadget)) {
+        //If the feature is optional and the admin has not allowed it don't include it
+        gadgetLibs.remove(feature.getName());
+      }
+    }
+
+    // Get config for all features
+    Set<String> allLibs = ImmutableSet.<String>builder()
+        .addAll(externForcedLibs).addAll(gadgetLibs).build();
+    String libraryConfig =
+      getLibraryConfig(gadget, featureRegistry.getFeatures(allLibs));
+
+    // Inject internal script
+    injectScript(gadgetLibs, externForcedLibs, !externalizeFeatures,
+        gadget, headTag, firstHeadChild, libraryConfig);
+  }
+
+  /**
+   * Check that all gadget required features exists
+   */
+  protected void checkRequiredFeatures(Gadget gadget, FeatureRegistry featureRegistry)
+      throws GadgetException {
+    List<String> unsupported = Lists.newLinkedList();
+
+    // Get all resources requested by the gadget's requires/optional features.
+    Map<String, Feature> featureMap = gadget.getViewFeatures();
+    List<String> gadgetFeatureKeys = Lists.newLinkedList(gadget.getDirectFeatureDeps());
+    featureRegistry.getFeatureResources(gadget.getContext(), gadgetFeatureKeys, unsupported)
+                   .getResources();
+    if (!unsupported.isEmpty()) {
+      List<String> requiredUnsupported = Lists.newLinkedList();
+      for (String notThere : unsupported) {
+        if (!featureMap.containsKey(notThere) || featureMap.get(notThere).getRequired()) {
+          // if !containsKey, the lib was forced with Gadget.addFeature(...) so implicitly req'd.
+          requiredUnsupported.add(notThere);
+        }
+      }
+      if (!requiredUnsupported.isEmpty()) {
+        throw new UnsupportedFeatureException(requiredUnsupported.toString());
+      }
+    }
+  }
+
+  /**
+   * Get the JS content for a request (JsUri)
+   */
+  protected String getFeaturesContent(JsUri jsUri) throws GadgetException {
+    // Inject js content, fetched from JsPipeline
+    JsRequest jsRequest = new JsRequestBuilder(jsUriManager,
+        featureRegistryProvider.get(jsUri.getRepository())).build(jsUri, null);
+    JsResponse jsResponse;
+    try {
+      jsResponse = jsServingPipeline.execute(jsRequest);
+    } catch (JsException e) {
+      throw new GadgetException(Code.JS_PROCESSING_ERROR, e, e.getStatusCode());
+    }
+    return jsResponse.toJsString();
+  }
+
+  /**
+   * Add script tag with either js content (inline=true) or script src tag
+   */
+  protected void injectScript(Collection<String> libs, Collection<String> loaded, boolean inline,
+      Gadget gadget, Node headTag, Node firstHeadChild, String extraContent)
+      throws GadgetException {
+
+    GadgetContext context = gadget.getContext();
+    // Gadget is not specified in request in order to support better caching
+    JsUri jsUri = new JsUri(null, context.getDebug(), false, context.getContainer(), null,
+        libs, loaded, null, false, false, RenderingContext.getDefault(), null,
+        getFeatureRepositoryId(gadget));
+    jsUri.setCajoleContent(gadget.requiresCaja());
+
+    String content = "";
+    if (!inline) {
+      String jsUrl = new UriBuilder(jsUriManager.makeExternJsUri(jsUri))
+          // Avoid jsload by adding jsload=0
+          .addQueryParameter(UriCommon.Param.JSLOAD.getKey(), "0")
+          .toString();
+      Element libsTag = headTag.getOwnerDocument().createElement("script");
+      libsTag.setAttribute("src", jsUrl);
+      headTag.insertBefore(libsTag, firstHeadChild);
+    } else {
+      content = getFeaturesContent(jsUri);
+    }
+
+    content = content + extraContent;
+    if (content.length() > 0) {
+      Element inlineTag = headTag.getOwnerDocument().createElement("script");
+      headTag.insertBefore(inlineTag, firstHeadChild);
+      inlineTag.appendChild(headTag.getOwnerDocument().createTextNode(content));
+    }
+  }
+
+  /**
+   * Creates a set of all configuration needed to satisfy the requested feature set.
+   *
+   * Appends special configuration for gadgets.util.hasFeature and gadgets.util.getFeatureParams to
+   * the output js.
+   *
+   * This can't be handled via the normal configuration mechanism because it is something that
+   * varies per request.
+   *
+   * @param reqs The features needed to satisfy the request.
+   * @throws GadgetException If there is a problem with the gadget auth token
+   */
+  protected String getLibraryConfig(Gadget gadget, List<String> reqs)
+      throws GadgetException {
+    Map<String, Object> config =
+        configProcessor.getConfig(gadget.getContext().getContainer(), reqs, null, gadget);
+
+    if (!config.isEmpty()) {
+      return "gadgets.config.init(" + JsonSerializer.serialize(config) + ");\n";
+    }
+
+    return "";
+  }
+
+  /**
+   * Injects message bundles into the gadget output.
+   * @throws GadgetException If we are unable to retrieve the message bundle.
+   */
+  protected void injectMessageBundles(MessageBundle bundle, Node scriptTag) throws GadgetException {
+    String msgs = bundle.toJSONString();
+
+    Text text = scriptTag.getOwnerDocument().createTextNode("gadgets.Prefs.setMessages_(");
+    text.appendData(msgs);
+    text.appendData(");");
+    scriptTag.appendChild(text);
+  }
+
+  /**
+   * Injects default values for user prefs into the gadget output.
+   */
+  protected void injectDefaultPrefs(Gadget gadget, Node scriptTag) {
+    Collection<UserPref> prefs = gadget.getSpec().getUserPrefs().values();
+    Map<String, String> defaultPrefs = Maps.newHashMapWithExpectedSize(prefs.size());
+    for (UserPref up : prefs) {
+      defaultPrefs.put(up.getName(), up.getDefaultValue());
+    }
+    Text text = scriptTag.getOwnerDocument().createTextNode("gadgets.Prefs.setDefaultPrefs_(");
+    text.appendData(JsonSerializer.serialize(defaultPrefs));
+    text.appendData(");");
+    scriptTag.appendChild(text);
+  }
+
+  /**
+   * Injects preloads into the gadget output.
+   *
+   * If preloading fails for any reason, we just output an empty object.
+   */
+  protected void injectPreloads(Gadget gadget, Node scriptTag) {
+    List<Object> preload = Lists.newArrayList();
+    for (PreloadedData preloaded : gadget.getPreloads()) {
+      try {
+        preload.addAll(preloaded.toJson());
+      } catch (PreloadException pe) {
+        // This will be thrown in the event of some unexpected exception. We can move on.
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, classname, "injectPreloads", MessageKeys.UNEXPECTED_ERROR_PRELOADING);
+          LOG.log(Level.WARNING, pe.getMessage(), pe);
+        }
+      }
+    }
+    Text text = scriptTag.getOwnerDocument().createTextNode("gadgets.io.preloaded_=");
+    text.appendData(JsonSerializer.serialize(preload));
+    text.appendData(";");
+    scriptTag.appendChild(text);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingResults.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingResults.java
new file mode 100644
index 0000000..b04421f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RenderingResults.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import org.apache.shindig.common.uri.Uri;
+
+import com.google.common.base.Preconditions;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Contains the results of a rendering operation.
+ */
+public final class RenderingResults {
+  private final Status status;
+  private final String content;
+  private final String errorMessage;
+  private final int httpStatusCode;
+
+  private final Uri redirect;
+
+  private RenderingResults(Status status, String content, String errorMessage,
+      int httpStatusCode, Uri redirect) {
+    this.status = status;
+    this.content = content;
+    this.errorMessage = errorMessage;
+    this.httpStatusCode = httpStatusCode;
+
+    this.redirect = redirect;
+  }
+
+  public static RenderingResults ok(String content) {
+    return new RenderingResults(Status.OK, content, null, HttpServletResponse.SC_OK, null);
+  }
+
+  public static RenderingResults error(String errorMessage, int httpStatusCode) {
+    return new RenderingResults(Status.ERROR, null, errorMessage, httpStatusCode, null);
+  }
+
+  public static RenderingResults mustRedirect(Uri redirect) {
+    Preconditions.checkNotNull(redirect);
+    return new RenderingResults(Status.MUST_REDIRECT, null, null, HttpServletResponse.SC_FOUND,
+        redirect);
+  }
+
+  /**
+   * @return The status of the rendering operation.
+   */
+  public Status getStatus() {
+    return status;
+  }
+
+  /**
+   * @return The content to render. Only available when status is OK.
+   */
+  public String getContent() {
+    Preconditions.checkState(status == Status.OK, "Only available when status is OK.");
+    return content;
+  }
+
+  /**
+   * @return The error message for rendering. Only available when status is ERROR.
+   */
+  public String getErrorMessage() {
+    Preconditions.checkState(status == Status.ERROR, "Only available when status is ERROR.");
+    return errorMessage;
+  }
+
+  /**
+   * @return The HTTP status code for rendering. Only available when status is ERROR.
+   */
+  public int getHttpStatusCode() {
+    Preconditions.checkState(status == Status.ERROR, "Only available when status is ERROR.");
+    return httpStatusCode;
+  }
+
+  /**
+   * @return The error message for rendering. Only available when status is ERROR.
+   */
+  public Uri getRedirect() {
+    Preconditions.checkState(status == Status.MUST_REDIRECT, "Only available when status is MUST_REDIRECT.");
+    return redirect;
+  }
+
+  public enum Status {
+    OK, MUST_REDIRECT, ERROR
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RpcServiceLookup.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RpcServiceLookup.java
new file mode 100644
index 0000000..e606129
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/RpcServiceLookup.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import com.google.common.collect.Multimap;
+import com.google.inject.ImplementedBy;
+
+/**
+ * Provide information about the set of JSON-RPC services that are available to gadgets running in
+ * the context of a specific container
+ */
+@ImplementedBy(DefaultRpcServiceLookup.class)
+public interface RpcServiceLookup {
+  /**
+   * This result map is a map of the form
+   * { endpoint1 -> [services]}, endpoint2 -> [services]}
+   *
+   * Services are described using the names expected as a result of JSON-RPC call
+   * to system.listMethods
+   *
+   * When a gadget is rendered the container data is mapped into gadgets.config and used to
+   * initialize osapi, which typically results in output that looks like
+   *
+   * { "osapi.services" : { "http://.../endpoint1" : ["system.listMethods", "people.get", "people.create",
+   * "people.delete"], ... }, "http://.../endpoint2" : { "system.listMethods", "cache.invalidate"],
+   * ... } }}
+   * @param container
+   * @param host
+   * @return
+   */
+  Multimap<String, String> getServicesFor(String container, String host);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
new file mode 100644
index 0000000..41fd99b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriter.java
@@ -0,0 +1,404 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.BindingAnnotation;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.UserDataHandler;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A content rewriter that will sanitize output for simple 'badge' like display.
+ *
+ * This is intentionally not as robust as Caja. It is a simple element whitelist. It can not be used
+ * for sanitizing either javascript or CSS. CSS is desired in the long run, but it can't be proven
+ * safe in the short term.
+ *
+ * Generally used in conjunction with a gadget that gets its dynamic behavior externally (proxied
+ * rendering, OSML, etc.)
+ */
+public class SanitizingGadgetRewriter extends DomWalker.Rewriter {
+
+  /** Key stored as element user-data to bypass sanitization */
+  private static final String BYPASS_SANITIZATION_KEY = "shindig.bypassSanitization";
+
+  /**
+   * Is the Gadget to be rendered sanitized?
+   * @return true if sanitization will be enabled
+   */
+  public static boolean isSanitizedRenderingRequest(Gadget gadget) {
+    return "1".equals(gadget.getContext().getParameter("sanitize"));
+  }
+
+  /**
+   * Marks that an element and all its attributes are trusted content.
+   * This status is preserved across {@link Node#cloneNode} calls.  Be
+   * extremely careful when using this, especially with {@code includingChildren}
+   * set to {@code true}, as untrusted content that gets inserted (e.g, via
+   * os:RenderAll in templating) would become trusted.
+   *
+   * @param element the trusted element
+   * @param includingChildren if true, children of this element will are also
+   *     trusted.  Never set this to true on an element that will ever have
+   *     untrusted children inserted (e.g., if it contains or may contain os:Render).
+   */
+  public static void bypassSanitization(Element element, boolean includingChildren) {
+    element.setUserData(BYPASS_SANITIZATION_KEY,
+        includingChildren ? Bypass.ALL : Bypass.ONLY_SELF, copyOnClone);
+  }
+
+  // Public so it can be used by the old rewriter
+  public static enum Bypass { ALL, ONLY_SELF, NONE }
+
+    private static UserDataHandler copyOnClone = new UserDataHandler() {
+    public void handle(short operation, String key, Object data, Node src, Node dst) {
+      if (operation == NODE_IMPORTED || operation == NODE_CLONED) {
+        dst.setUserData(key, data, copyOnClone);
+      }
+    }
+  };
+
+  @Inject
+  public SanitizingGadgetRewriter(@AllowedTags Provider<Set<String>> allowedTags,
+      @AllowedAttributes Provider<Set<String>> allowedAttributes,
+      ContentRewriterFeature.Factory rewriterFeatureFactory,
+      CajaCssSanitizer cssSanitizer,
+      ProxyUriManager proxyUriManager) {
+    super(new BasicElementFilter(allowedTags, allowedAttributes),
+          new LinkSchemeCheckFilter(),
+          new StyleFilter(proxyUriManager, cssSanitizer),
+          new LinkFilter(proxyUriManager),
+          new ImageFilter(proxyUriManager),
+          new TargetFilter());
+  }
+
+
+  @Override
+  public void rewrite(Gadget gadget, MutableContent content) throws RewritingException {
+    if (gadget.sanitizeOutput()) {
+      boolean sanitized = false;
+      try {
+        super.rewrite(gadget, content);
+        sanitized = true;
+      } finally {
+        // Defensively clean the content in case of failure
+        if (!sanitized) {
+          content.setContent("");
+        }
+      }
+    }
+  }
+
+  /** Convert a NamedNodeMap to a list for easy and safe operations */
+  private static List<Attr> toList(NamedNodeMap nodes) {
+    List<Attr> list = new ArrayList<Attr>(nodes.getLength());
+
+    for (int i = 0, j = nodes.getLength(); i < j; ++i) {
+      list.add((Attr) nodes.item(i));
+    }
+
+    return list;
+  }
+
+  // Public so it can be used by the old rewriter
+  public static Bypass canBypassSanitization(Element element) {
+    Bypass bypass = (Bypass) element.getUserData(BYPASS_SANITIZATION_KEY);
+    if (bypass == null) {
+      bypass = Bypass.NONE;
+    }
+    return bypass;
+  }
+
+  private static abstract class SanitizingWalker implements DomWalker.Visitor {
+    protected abstract boolean removeTag(Gadget gadget, Element elem, Uri ctx);
+    protected abstract boolean removeAttr(Gadget gadget, Attr attr, Uri ctx);
+
+    public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+      Element elem;
+
+      switch (node.getNodeType()) {
+      case Node.CDATA_SECTION_NODE:
+      case Node.TEXT_NODE:
+      case Node.ENTITY_REFERENCE_NODE:
+        // Never modified.
+        return VisitStatus.BYPASS;
+      case Node.ELEMENT_NODE:
+      case Node.DOCUMENT_NODE:
+        // Continues through to follow-up logic.
+        elem = (Element)node;
+        break;
+      case Node.COMMENT_NODE:
+      default:
+        // Must remove all comments to avoid conditional comment evaluation.
+        // There might be other, unknown types as well. Don't trust them.
+        return VisitStatus.RESERVE_TREE;
+      }
+
+      Bypass bypass = canBypassSanitization(elem);
+      if (bypass == Bypass.ALL) {
+        // This is double-checked in revisit below to ensure no modification/removal occurs.
+        return VisitStatus.RESERVE_TREE;
+      } else if (bypass == Bypass.ONLY_SELF) {
+        return VisitStatus.BYPASS;
+      }
+
+      if (removeTag(gadget, elem, gadget.getSpec().getUrl())) {
+        // All reserved trees are removed in revisit.
+        return VisitStatus.RESERVE_TREE;
+      }
+
+      // Otherwise move on to attributes.
+      VisitStatus status = VisitStatus.MODIFY;
+      for (Attr attr : toList(elem.getAttributes())) {
+        if (removeAttr(gadget, attr, gadget.getSpec().getUrl())) {
+          elem.removeAttributeNode(attr);
+        }
+      }
+
+      return status;
+    }
+
+    public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+      // Remove all reserved nodes, since these are all for which removeTag returned true.
+      for (Node node : nodes) {
+        if (node.getNodeType() == Node.COMMENT_NODE ||
+            canBypassSanitization((Element)node) != Bypass.ALL) {
+          node.getParentNode().removeChild(node);
+        }
+      }
+      return true;
+    }
+  }
+
+  /**
+   * Restrict the set of allowed tags and attributes
+   */
+  static final class BasicElementFilter extends SanitizingWalker {
+    private final Provider<Set<String>> allowedTags;
+    private final Provider<Set<String>> allowedAttributes;
+
+    private BasicElementFilter(Provider<Set<String>> allowedTags,
+                               Provider<Set<String>> allowedAttributes) {
+      this.allowedTags = allowedTags;
+      this.allowedAttributes = allowedAttributes;
+    }
+
+    @Override
+    public boolean removeTag(Gadget gadget, Element elem, Uri context) {
+      return !allowedTags.get().contains(elem.getNodeName().toLowerCase());
+    }
+
+    @Override
+    public boolean removeAttr(Gadget gadget, Attr attr, Uri context) {
+      return !allowedAttributes.get().contains(attr.getName().toLowerCase());
+    }
+  }
+
+  /**
+   * Enfore that all uri's in the document have either http or https as
+   * their scheme
+   */
+  static class LinkSchemeCheckFilter extends SanitizingWalker {
+    private static final Set<String> URI_ATTRIBUTES = ImmutableSet.of("href", "src");
+
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      return false;
+    }
+
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      if (URI_ATTRIBUTES.contains(attr.getName().toLowerCase())) {
+        try {
+          Uri uri = Uri.parse(attr.getValue());
+          String scheme = uri.getScheme();
+          if (scheme != null && !scheme.equals("http") && !scheme.equals("https")) {
+            return true;
+          }
+        } catch (IllegalArgumentException iae) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Enfore that all images in the document are rewritten through the proxy.
+   * Prevents issues in IE where the image content contains script
+   */
+  static final class ImageFilter extends SanitizingWalker {
+    private final SanitizingProxyUriManager imageRewriter;
+
+    private ImageFilter(ProxyUriManager proxyUriManager) {
+      this.imageRewriter = new SanitizingProxyUriManager(proxyUriManager, "image/*");
+    }
+
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      return false;
+    }
+
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      if ("img".equalsIgnoreCase(attr.getOwnerElement().getNodeName()) &&
+          "src".equalsIgnoreCase(attr.getName())) {
+        try {
+          Uri uri = Uri.parse(attr.getValue());
+          ProxyUriManager.ProxyUri proxiedUri = ProxyUriManager.ProxyUri.fromList(
+              gadget, ImmutableList.of(uri)).get(0);
+          proxiedUri.setHtmlTagContext(attr.getOwnerElement().getNodeName().toLowerCase());
+          attr.setValue(imageRewriter.make(ImmutableList.of(proxiedUri), null)
+                .get(0).toString());
+        } catch (IllegalArgumentException e) {
+          // Invalid Uri, remove.
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  /**
+   * Pass the contents of style tags through the CSS sanitizer
+   */
+  static final class StyleFilter implements DomWalker.Visitor {
+    private final SanitizingProxyUriManager imageRewriter;
+    private final SanitizingProxyUriManager cssImportRewriter;
+    private final CajaCssSanitizer cssSanitizer;
+
+    private StyleFilter(ProxyUriManager proxyUriManager, CajaCssSanitizer cssSanitizer) {
+      this.imageRewriter = new SanitizingProxyUriManager(proxyUriManager, "image/*");
+      this.cssImportRewriter = new SanitizingProxyUriManager(proxyUriManager, "text/css");
+      this.cssSanitizer = cssSanitizer;
+    }
+
+    public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+      if (node.getNodeType() == Node.ELEMENT_NODE &&
+          "style".equalsIgnoreCase(node.getNodeName())) {
+        cssSanitizer.sanitize((Element) node, gadget.getSpec().getUrl(),
+            gadget.getContext(), cssImportRewriter, imageRewriter);
+        return VisitStatus.MODIFY;
+      }
+      return VisitStatus.BYPASS;
+    }
+
+    public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+      return false;
+    }
+  }
+
+  /**
+   * Restrict link tags to stylesheet content only and force the link to
+   * be rewritten through the proxy and sanitized
+   */
+  static final class LinkFilter extends SanitizingWalker {
+    private final SanitizingProxyUriManager cssImportRewriter;
+
+    private LinkFilter(ProxyUriManager proxyUriManager) {
+      this.cssImportRewriter = new SanitizingProxyUriManager(proxyUriManager, "text/css");
+    }
+
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      if (!elem.getNodeName().equalsIgnoreCase("link")) {
+        return false;
+      }
+      boolean hasType = false;
+      for (Attr attr : toList(elem.getAttributes())) {
+        if ("rel".equalsIgnoreCase(attr.getName())) {
+          hasType |= "stylesheet".equalsIgnoreCase(attr.getValue());
+        } else if ("type".equalsIgnoreCase(attr.getName())) {
+          hasType |= "text/css".equalsIgnoreCase(attr.getValue());
+        } else if ("href".equalsIgnoreCase(attr.getName())) {
+          try {
+            ProxyUriManager.ProxyUri proxiedUri = ProxyUriManager.ProxyUri.fromList(gadget,
+                  ImmutableList.of(Uri.parse(attr.getValue()))).get(0);
+            proxiedUri.setHtmlTagContext(elem.getNodeName().toLowerCase());
+            attr.setValue(cssImportRewriter.make(ImmutableList.of(proxiedUri), null)
+                .get(0).toString());
+          } catch (IllegalArgumentException e) {
+            return true;
+          }
+        }
+      }
+      return !hasType;
+    }
+
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      return false;
+    }
+  }
+
+  /**
+   * Restrict the value of the target attribute on anchors etc. to
+   * _blank or _self or remove the node
+   */
+  static class TargetFilter extends SanitizingWalker {
+    @Override
+    protected boolean removeTag(Gadget gadget, Element elem, Uri ctx) {
+      return false;
+    }
+
+    @Override
+    protected boolean removeAttr(Gadget gadget, Attr attr, Uri ctx) {
+      if ("target".equalsIgnoreCase(attr.getName())) {
+        String value = attr.getValue().toLowerCase();
+        if (!("_blank".equals(value) || "_self".equals(value))) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
+  @BindingAnnotation
+  public @interface AllowedTags { }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
+  @BindingAnnotation
+  public @interface AllowedAttributes { }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java
new file mode 100644
index 0000000..a696c44
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManager.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import java.util.List;
+
+/**
+ * Forcible rewrite the link through the proxy and force sanitization with
+ * an expected mime type.
+ *
+ * @since 2.0.0
+ */
+public class SanitizingProxyUriManager implements ProxyUriManager {
+  private final ProxyUriManager wrapped;
+  private final String expectedMime;
+
+  public SanitizingProxyUriManager(ProxyUriManager wrapped, String expectedMime) {
+    this.wrapped = wrapped;
+    this.expectedMime = expectedMime;
+  }
+
+  public ProxyUri process(Uri uri) throws GadgetException {
+    return wrapped.process(uri);
+  }
+
+  public List<Uri> make(List<ProxyUri> ctx, Integer forcedRefresh) {
+    // Just wraps the original ProxyUriManager and adds a few query params.
+    for (ProxyUri proxyUri : ctx) {
+      proxyUri.setSanitizeContent(true);
+      if (expectedMime != null) {
+        proxyUri.setRewriteMimeType(expectedMime);
+      }
+    }
+
+    return wrapped.make(ctx, forcedRefresh);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingResponseRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingResponseRewriter.java
new file mode 100644
index 0000000..26f3b1f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/SanitizingResponseRewriter.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.sanselan.ImageFormat;
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+
+/**
+ * Rewriter that sanitizes CSS and image content.
+ *
+ * @since 2.0.0
+ */
+public class SanitizingResponseRewriter implements ResponseRewriter {
+
+  //class name for logging purpose
+  private static final String classname = SanitizingResponseRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final ContentRewriterFeature.Factory featureConfigFactory;
+  private final CajaCssSanitizer cssSanitizer;
+  private final ProxyUriManager proxyUriManager;
+
+  @Inject
+  public SanitizingResponseRewriter(ContentRewriterFeature.Factory featureConfigFactory,
+      CajaCssSanitizer cssSanitizer,
+      ProxyUriManager proxyUriManager) {
+    this.featureConfigFactory = featureConfigFactory;
+    this.cssSanitizer = cssSanitizer;
+    this.proxyUriManager = proxyUriManager;
+  }
+
+  public void rewrite(HttpRequest request, HttpResponseBuilder resp, Gadget gadget) {
+    // Content fetched through the proxy can stipulate that it must be sanitized.
+    if (request.isSanitizationRequested() &&
+        featureConfigFactory.get(request).shouldRewriteURL(request.getUri().toString())) {
+      if (Strings.isNullOrEmpty(request.getRewriteMimeType())) {
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "rewrite", MessageKeys.REQUEST_TO_SANITIZE_WITHOUT_CONTENT,new Object[] {request.getUri()});
+        }
+        resp.setContent("");
+      } else if (request.getRewriteMimeType().equalsIgnoreCase("text/css")) {
+        rewriteProxiedCss(request, resp);
+      } else if (request.getRewriteMimeType().toLowerCase().startsWith("image/")) {
+        rewriteProxiedImage(request, resp);
+      } else {
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, classname, "rewrite",
+            MessageKeys.REQUEST_TO_SANITIZE_UNKNOW_CONTENT,
+            new Object[] {request.getRewriteMimeType(),request.getUri()});
+        }
+        resp.setContent("");
+      }
+    }
+  }
+
+  /**
+   * We don't actually rewrite the image we just ensure that it is in fact a valid
+   * and known image type.
+   */
+  private void rewriteProxiedImage(HttpRequest request, HttpResponseBuilder resp) {
+    boolean imageIsSafe = false;
+    try {
+      String contentType = resp.getHeader("Content-Type");
+      if (contentType == null || contentType.toLowerCase().startsWith("image/")) {
+        // Unspecified or unknown image mime type.
+        try {
+          ImageFormat imageFormat = Sanselan
+              .guessFormat(new ByteSourceInputStream(resp.getContentBytes(),
+                  request.getUri().getPath()));
+          if (imageFormat == ImageFormat.IMAGE_FORMAT_UNKNOWN) {
+            if (LOG.isLoggable(Level.INFO)) {
+              LOG.logp(Level.INFO, classname, "rewriteProxiedImage", MessageKeys.UNABLE_SANITIZE_UNKNOWN_IMG,new Object[] {request.getUri().toString()});
+            }
+            return;
+          }
+          imageIsSafe = true;
+          // Return false to indicate that no rewriting occurred
+        } catch (IOException ioe) {
+          throw new RuntimeException(ioe);
+        } catch (ImageReadException ire) {
+          // Unable to read the image so its not safe
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "rewriteProxiedImage", MessageKeys.UNABLE_DETECT_IMG_TYPE,new Object[] {request.getUri().toString()});
+            LOG.log(Level.INFO, ire.getMessage(), ire);
+          }
+        }
+      }
+    } finally {
+      if (!imageIsSafe) {
+        resp.setContent("");
+      }
+    }
+  }
+
+  /**
+   * Sanitize a CSS file.
+   */
+  private void rewriteProxiedCss(HttpRequest request, HttpResponseBuilder resp) {
+    String sanitized = "";
+    try {
+      String contentType = resp.getHeader("Content-Type");
+      if (contentType == null || contentType.toLowerCase().startsWith("text/")) {
+        SanitizingProxyUriManager cssImageRewriter =
+            new SanitizingProxyUriManager(proxyUriManager, "image/*");
+        SanitizingProxyUriManager cssImportRewriter =
+            new SanitizingProxyUriManager(proxyUriManager, "text/css");
+
+        GadgetContext gadgetContext = DomWalker.makeGadget(request).getContext();
+        sanitized = cssSanitizer.sanitize(resp.getContent(), request.getUri(),
+            gadgetContext, cssImportRewriter, cssImageRewriter);
+      }
+    } finally {
+      // Set sanitized content in finally to ensure it is always cleared in
+      // the case of errors
+      resp.setContent(sanitized);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/ServiceFetcher.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/ServiceFetcher.java
new file mode 100644
index 0000000..6db48e9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/render/ServiceFetcher.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import com.google.common.collect.Multimap;
+import com.google.inject.ImplementedBy;
+
+/**
+ * Retrieves the rpc services for a container.
+ */
+@ImplementedBy(DefaultServiceFetcher.class)
+public interface ServiceFetcher {
+  Multimap<String, String> getServicesForContainer(String container, String host);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceRewriter.java
new file mode 100644
index 0000000..4c4d120
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceRewriter.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Rewriter that converts all url's to absolute.
+ *
+ * @since 2.0.0
+ */
+public class AbsolutePathReferenceRewriter extends DomWalker.Rewriter {
+  private static final Logger LOG = Logger.getLogger(AbsolutePathReferenceRewriter.class.getName());
+
+  private AbsolutePathReferenceVisitor.Tags[] tags = {AbsolutePathReferenceVisitor.Tags.RESOURCES};
+
+  @Inject
+  public AbsolutePathReferenceRewriter() {}
+
+  @Inject(optional=true)
+  public void setAbsolutePathTags(@Named("shindig.gadgets.rewriter.absolutePath.tags")
+                                         String absolutePathTags) {
+    if(LOG.isLoggable(Level.FINE)) {
+      LOG.fine("Tags that should have the reference resolved to absolute path: " + absolutePathTags);
+    }
+    String[] tagsArray = absolutePathTags.split(",");
+    List<AbsolutePathReferenceVisitor.Tags> tagsList = Lists.newArrayList();
+    for(String tagValue : tagsArray) {
+      try {
+        AbsolutePathReferenceVisitor.Tags tag = AbsolutePathReferenceVisitor.Tags.valueOf(tagValue);
+        if(!tagsList.contains(tag)) {
+          tagsList.add(tag);
+        }
+      } catch (Exception ex) {
+        LOG.warning("Invalid absolute path tag name : " + tagValue);
+        continue;
+      }
+    }
+    this.tags = tagsList.toArray(new AbsolutePathReferenceVisitor.Tags[tagsList.size()]);
+  }
+
+  @Override
+  protected List<DomWalker.Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+    return ImmutableList.<DomWalker.Visitor>of(new AbsolutePathReferenceVisitor(tags));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceVisitor.java
new file mode 100644
index 0000000..2dc9278
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceVisitor.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Element;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Visitor that resolves relative paths relative to the
+ * base tag (only if present) / current page url and marks urls as absolute.
+ *
+ * @since 2.0.0
+ */
+public class AbsolutePathReferenceVisitor implements Visitor {
+  public enum Tags {
+    // Resources which would be fetched by the browser when rendering the page.
+    //TODO: Document the second parameter for clarity
+    // Does it make sense to factor this out into shindig properties?
+    RESOURCES(ImmutableMap.<String, String>builder()
+        .put("body", "background")
+        .put("img", "src")
+        .put("input", "src")
+        .put("link", "href")
+        .put("embed", "src")
+        .put("script", "src").build()),
+
+    // Hyperlinks that the user clicks on to navigate pages.
+    HYPERLINKS(ImmutableMap.<String, String>builder()
+        .put("a", "href")
+        .put("area", "href")
+        .put("q", "cite").build());
+
+    Map<String, String> resourceTags;
+    private Tags(Map<String, String> resourceTags) {
+      this.resourceTags = resourceTags;
+    }
+
+    public Map<String, String> getResourceTags() {
+      return resourceTags;
+    }
+  }
+
+  // Map of tag name -> attribute type describing uris to make absolute.
+  private final Map<String, String> tagsToMakeAbsolute;
+
+  // The base Uri used to absolutify relative uris in the document being visited.
+  private Uri baseUri;
+
+  @Inject
+  public AbsolutePathReferenceVisitor(Tags... resourceTags) {
+    Map<String, String> tagsToMakeAbsolute = new HashMap<String, String>();
+    for (Tags r : resourceTags) {
+      tagsToMakeAbsolute.putAll(r.getResourceTags());
+    }
+
+    this.tagsToMakeAbsolute = tagsToMakeAbsolute;
+  }
+
+  // @Override
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    Attr nodeAttr = getUriAttributeFromNode(node, tagsToMakeAbsolute);
+
+    if (nodeAttr != null) {
+      try {
+        Uri nodeUri = Uri.parse(nodeAttr.getValue());
+        Uri baseUri = getBaseResolutionUri(gadget, node);
+
+        Uri resolved = baseUri.resolve(nodeUri);
+
+        if (!resolved.equals(nodeUri)) {
+          nodeAttr.setValue(resolved.toString());
+          return VisitStatus.MODIFY;
+        }
+      } catch (Uri.UriException e) {
+        // UriException on illegal input. Ignore.
+      }
+    }
+    return VisitStatus.BYPASS;
+  }
+
+  // @Override
+  public boolean revisit(Gadget gadget, List<Node> node) throws RewritingException {
+    // Modification happens immediately.
+    return false;
+  }
+
+  /**
+   * Returns the uri attribute for the given node by looking up the
+   * tag name -> uri attribute map.
+   * NOTE: This function returns the node attribute only if the attribute has a
+   * non empty value.
+   * @param node The node to get uri attribute for.
+   * @param resourceTags Map from tag name -> uri attribute name.
+   * @return Uri attribute for the node.
+   */
+  public static Attr getUriAttributeFromNode(Node node, Map<String, String> resourceTags) {
+    String nodeName = node.getNodeName().toLowerCase();
+    if (node.getNodeType() == Node.ELEMENT_NODE &&
+        resourceTags.containsKey(nodeName)) {
+      if ("link".equals(nodeName)) {
+        // Rewrite link only when it is for css.
+        String type = ((Element)node).getAttribute("type");
+        String rel = ((Element)node).getAttribute("rel");
+        if (!"stylesheet".equalsIgnoreCase(rel) || !"text/css".equalsIgnoreCase(type)) {
+          return null;
+        }
+      }
+      Attr attr = (Attr) node.getAttributes().getNamedItem(resourceTags.get(nodeName));
+      String nodeUri = attr != null ? attr.getValue() : null;
+      if (!Strings.isNullOrEmpty(nodeUri)) {
+        return attr;
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Returns the uri to resolve any relative url on the current page to.
+   * This is equal to the base uri (in case the page has one) or the current
+   * page uri.
+   * @param gadget The gadget (container for page) being processed.
+   * @param node The current node being processed.
+   * @return The uri to resolve non absolute uri's relative to.
+   */
+  private Uri getBaseResolutionUri(Gadget gadget, Node node) {
+    if (baseUri == null) {
+      Uri pageUri = gadget.getSpec().getUrl();
+      Uri baseTagUri = getBaseUri(node.getOwnerDocument());
+      baseUri = baseTagUri != null ? baseTagUri : pageUri;
+    }
+    return baseUri;
+  }
+
+  /**
+   * Returns the base uri of the given document.
+   * Base uri is specified as &lt;base href="..."&gt;
+   * @param doc The document.
+   * @return Base uri of the document.
+   */
+  @VisibleForTesting
+  Uri getBaseUri(Document doc) {
+    String baseHref = getBaseHref(doc);
+    if (baseHref != null) {
+      try {
+        return Uri.parse(baseHref);
+      } catch (Uri.UriException e) {
+        // Ignore.
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Returns href value of the base tag.
+   * @param doc The document to process.
+   * @return Value of href attribute of the base tag.
+   */
+  @VisibleForTesting
+  String getBaseHref(Document doc) {
+    NodeList list = doc.getElementsByTagName("base");
+    if (list.getLength() == 0) {
+      return null;
+    }
+
+    NamedNodeMap nodeMap = list.item(0).getAttributes();
+    if (nodeMap == null) {
+      return null;
+    }
+    Attr attr = (Attr) nodeMap.getNamedItem("href");
+    return attr != null ? attr.getValue() : null;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/BaseTagRemoverRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/BaseTagRemoverRewriter.java
new file mode 100644
index 0000000..65803b5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/BaseTagRemoverRewriter.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Simple rewriter that deletes the base tag from the html document.
+ *
+ * @since 2.0.0
+ */
+public class BaseTagRemoverRewriter implements GadgetRewriter, ResponseRewriter {
+  private static final Logger logger = Logger.getLogger(BaseTagRemoverRewriter.class.getName());
+
+  public void rewrite(Gadget gadget, MutableContent mc) {
+    Document doc = mc.getDocument();
+
+    NodeList list = doc.getElementsByTagName("base");
+    for (int i = 0; i < list.getLength(); i++) {
+      Element baseElement = (Element) list.item(i);
+      baseElement.getParentNode().removeChild(baseElement);
+
+      if (baseElement.hasAttribute("href") && logger.isLoggable(Level.FINE)) {
+        logger.fine("Removing base tag pointing to: "
+                    + baseElement.getAttribute("href") + " for gadget: "
+                    + gadget.getContext().getUrl().toString());
+      }
+    }
+
+    mc.documentChanged();
+  }
+
+  public void rewrite(HttpRequest request, HttpResponseBuilder response, Gadget gadget)
+          throws RewritingException {
+    if (RewriterUtils.isHtml(request, response)) {
+      if(gadget == null) {
+        gadget = DomWalker.makeGadget(request);
+      }
+      rewrite(gadget, response);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CacheEnforcementVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CacheEnforcementVisitor.java
new file mode 100644
index 0000000..227c36e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CacheEnforcementVisitor.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpCache;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Visitor that walks over html tags as specified by {@code resourceTags} and
+ * reserves html tag nodes whose uri attributes are either not in cache, or are
+ * in cache, but the response in cache is either stale or an error response. In
+ * all the above mentioned cases except for the error case, we trigger a
+ * background fetch for the resource. This visitor should be used by a rewriter
+ * in conjuction with other visitors which depend on the uri of the html node
+ * being in cache.
+ *
+ * Note that in order to use the CacheEnforcementVisitor effectively, the
+ * shindig property shindig.cache.http.strict-no-cache-resource.max-age should
+ * be set to a positive value, so that strict no-cache resources are stored in
+ * cache with this ttl, and unnecessary fetches are not triggered each time for
+ * such resources.
+ *
+ */
+public class CacheEnforcementVisitor extends ResourceMutateVisitor {
+
+  private static final Logger logger = Logger.getLogger(CacheEnforcementVisitor.class.getName());
+  public static final String CACHE_ENFORCEMENT_FETCH_PARAM = "X-shindig-cache-enforcement-fetch";
+  private final HttpCache cache;
+  private final RequestPipeline requestPipeline;
+  private final Executor executor;
+
+
+  /**
+   * Constructs the cache enforcement visitor.
+   */
+  public CacheEnforcementVisitor(ContentRewriterFeature.Config featureConfig,
+                                 Executor executor,
+                                 HttpCache cache,
+                                 RequestPipeline requestPipeline,
+                                 Tags... resourceTags) {
+    super(featureConfig, resourceTags);
+    this.executor = executor;
+    this.cache = cache;
+    this.requestPipeline = requestPipeline;
+  }
+
+  /**
+   * Constructs a new HttpRequest in the context of the gadget.
+   * For example, the implementation may choose to copy User Agent or referer etc.
+   */
+  protected HttpRequest createNewHttpRequest(Gadget gadget, String uriStr) {
+    HttpRequest request = new HttpRequest(Uri.parse(uriStr));
+    if (gadget != null) {
+      GadgetSpec spec = gadget.getSpec();
+      if (spec != null) {
+        request.setGadget(spec.getUrl());
+      }
+      GadgetContext context = gadget.getContext();
+      if (context != null) {
+        request.setContainer(context.getContainer());
+      }
+      request.setParam(CACHE_ENFORCEMENT_FETCH_PARAM, "1");
+    }
+    return request;
+  }
+
+  @Override
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    if (super.visit(gadget, node).equals(VisitStatus.RESERVE_NODE)) {
+      Element element = (Element) node;
+      String nodeName = node.getNodeName().toLowerCase();
+      String uriStr = element.getAttribute(resourceTags.get(nodeName)).trim();
+      HttpRequest request = createNewHttpRequest(gadget, uriStr);
+      HttpResponse response = cache.getResponse(request);
+      if (response == null) {
+        return handleResponseNotInCache(request);
+      } else {
+        return handleResponseInCache(request, response);
+      }
+    }
+    return VisitStatus.BYPASS;
+  }
+
+  /**
+   * The action to be performed if the response is in cache.
+   *
+   * @param request HttpRequest to fetch the resource of the node.
+   * @param response The HttpResponse retrieved from cache.
+   * @return The visit status of the node.
+   */
+  protected VisitStatus handleResponseInCache(HttpRequest request, HttpResponse response) {
+    if (response.shouldRefetch()) {
+      // Reserve the node if the response should be refetched.
+      if (response.getCacheControlMaxAge() != 0) {
+        // If the cache-control max-age of the original response is zero, it doesn't make sense to
+        // trigger a pre-fetch for it, since by the time the request for it comes in, it will
+        // already be stale. Such resources will continuously be prefetched, but the fetched
+        // response will never be used.
+        // TODO: While we definitely should not be pre-fetching resources with a max-age of 0, other
+        // cases with a very small max-age(say 1s) should also probably not be pre-fetched either
+        // since the response might not be usable by the time the actual request comes in. Also
+        // we should consider the cases with no max-age, but instead an Expires header which is
+        // close to, or the same as the Date header.
+        triggerFetch(request);
+      }
+      return VisitStatus.RESERVE_NODE;
+    } else if (response.isStrictNoCache() || response.getHeader("Set-Cookie") != null ||
+               response.isError()) {
+      // If the response is strict no-cache, or has a Set-Cookie header or is an error response,
+      // reserve the node. Do not trigger a fetch, since pre-fetching the resource doesn't help as
+      // the response will not be cached. Also, for the error case, since we already set the
+      // ttl for such resources to 30 seconds, we should not keep pre-fetching these till they
+      // become stale.
+      return VisitStatus.RESERVE_NODE;
+    } else {
+      // Otherwise, we assume the cached response is valid and bypass the node.
+      return VisitStatus.BYPASS;
+    }
+  }
+
+  /**
+   * The action to be performed if the response is not in cache.
+   *
+   * @param request HttpRequest to fetch the resource of the node.
+   * @return The visit status of the node.
+   */
+  protected VisitStatus handleResponseNotInCache(HttpRequest request) {
+    triggerFetch(request);
+    return VisitStatus.RESERVE_NODE;
+  }
+
+  /**
+   * Triggers a background fetch for a resource.
+   *
+   * @param request HttpRequest to fetch the resource of the node.
+   */
+  protected void triggerFetch(final HttpRequest request) {
+
+    executor.execute(new Runnable() {
+
+      public void run() {
+        try {
+          requestPipeline.execute(request);
+        } catch (GadgetException e) {
+          logger.log(Level.WARNING, "Triggered fetch failed for " + request, e);
+        }
+      }
+    });
+  }
+
+  @Override
+  protected Collection<Pair<Node, Uri>> mutateUris(Gadget gadget, Collection<Node> nodes) {
+    return ImmutableList.of();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CaptureRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CaptureRewriter.java
new file mode 100644
index 0000000..11e12f5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CaptureRewriter.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+/**
+ * Utility rewriter for testing.
+ */
+public class CaptureRewriter implements ResponseRewriter, GadgetRewriter {
+  private boolean rewroteView = false;
+  private boolean rewroteResponse = false;
+
+  public boolean responseWasRewritten() {
+    return rewroteResponse;
+  }
+
+  public void rewrite(Gadget gadget, MutableContent content) {
+    rewroteView = true;
+  }
+
+  public boolean viewWasRewritten() {
+    return rewroteView;
+  }
+
+  public void rewrite(HttpRequest request, HttpResponseBuilder response, Gadget gadget) {
+    rewroteResponse = true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ConcatVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ConcatVisitor.java
new file mode 100644
index 0000000..cc300bd
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ConcatVisitor.java
@@ -0,0 +1,319 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Strings;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.uri.ConcatUriManager;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * DOM mutator that concatenates resources using the concat servlet
+ * @since 2.0.0
+ */
+public class ConcatVisitor implements DomWalker.Visitor {
+  public static class Js extends ConcatVisitor {
+    public Js(ContentRewriterFeature.Config config,
+              ConcatUriManager uriManager) {
+      super(config, uriManager, ConcatUriManager.Type.JS);
+    }
+  }
+
+  public static class Css extends ConcatVisitor {
+    public Css(ContentRewriterFeature.Config config,
+               ConcatUriManager uriManager) {
+      super(config, uriManager, ConcatUriManager.Type.CSS);
+    }
+  }
+
+  private final ConcatUriManager uriManager;
+  private final ConcatUriManager.Type type;
+  private final ContentRewriterFeature.Config config;
+  private final boolean split;
+  private final boolean singleResourceConcat;
+
+  private ConcatVisitor(ContentRewriterFeature.Config config,
+      ConcatUriManager uriManager, ConcatUriManager.Type type) {
+    this.uriManager = uriManager;
+    this.type = type;
+    this.config = config;
+    this.split = (type == ConcatUriManager.Type.JS && config.isSplitJsEnabled());
+    this.singleResourceConcat = config.isSingleResourceConcatEnabled();
+  }
+
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    // Reserve JS nodes; always if there's an adjacent rewritable JS node and also when
+    // directed to support split-resource concatenation
+    if (node.getNodeType() != Node.ELEMENT_NODE ||
+        !node.getNodeName().equalsIgnoreCase(type.getTagName())) {
+      return VisitStatus.BYPASS;
+    }
+
+    Element element = (Element)node;
+    if (isRewritableExternData(element)) {
+      if (split || singleResourceConcat ||
+          isRewritableExternData(getSibling(element, true)) ||
+          isRewritableExternData(getSibling(element, false))) {
+        return VisitStatus.RESERVE_NODE;
+      }
+    }
+
+    return VisitStatus.BYPASS;
+  }
+
+  /**
+   * For css:
+   * Link tags are first split into buckets separated by tags with mediaType == "all"
+   * / title attribute different from their previous link tag / nodes that are
+   * not 'link' tags.
+   * This ensures that the buckets can be processed separately without losing title /
+   * "all" mediaType information.
+   *
+   * Link tags with same mediaType are concatenated within each bucket.
+   * This exercise ensures that css information is loaded in the same relative order
+   * as that of the original html page, and that the css information within
+   * mediaType=="all" is retained and applies to all media types.
+   *
+   * Look at the areLinkNodesBucketable method for details on mediaType=="all" and
+   * title attribute
+   *
+   * Example: Assume we have the following node list. (all have same parent,
+   * nodes between Node6 and Node12 are non link nodes, and hence did not come
+   * to revisit() call)
+   *    <link href="1.css" rel="stylesheet" type="text/css" media="screen">       -- Node1
+   *    <link href="2.css" rel="stylesheet" type="text/css" media="print">        -- Node2
+   *    <link href="3.css" rel="stylesheet" type="text/css" media="screen">       -- Node3
+   *    <link href="4.css" rel="stylesheet" type="text/css" media="all">          -- Node4
+   *    <link href="5.css" rel="stylesheet" type="text/css" media="all">          -- Node5
+   *    <link href="6.css" rel="stylesheet" type="text/css" media="screen">       -- Node6
+   *    <link href="12.css" rel="stylesheet" type="text/css" media="screen">      -- Node12
+   *    <link href="13.css" rel="stylesheet" type="text/css" media="screen">      -- Node13
+   *
+   *    First we split to buckets bassed on the adjacency and other conditions.
+   *    buckets - [ [ Node1, Node2, Node3 ], [ Node4, Node 5 ], [ Node6 ], [ Node12, Node13 ]
+   *    Within each bucket we group them based on media type.
+   *    batches - [ Node1, Node2, Node3 ] --> [ [Node1, Node3], [Node2] ]
+   *            - [ Node4, Node 5 ] --> [ [ Node4, Node 5 ] ]
+   *            - [ Node6 ] --> [ [ Node6 ] ]
+   *            - [ Node12, Node13 ] --> [ [ Node12, Node13 ] ]
+   *
+   * Refer Tests for more examples.
+   */
+  public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+    // Collate Elements into Buckets.
+    List<List<Element>> concatBuckets = Lists.newLinkedList();
+    List<Element> curBucket = Lists.newLinkedList();
+    Iterator<Node> nodeIter = nodes.iterator();
+    Element cur = (Element)nodeIter.next();
+    curBucket.add(cur);
+    while (nodeIter.hasNext()) {
+      Element next = (Element)nodeIter.next();
+      if ((!split && cur != getSibling(next, true)) ||
+          (type == ConcatUriManager.Type.CSS && !areLinkNodesBucketable(cur, next))) {
+        // Break off current bucket and add to list of all.
+        concatBuckets.add(curBucket);
+        curBucket = Lists.newLinkedList();
+      }
+      curBucket.add(next);
+      cur = next;
+    }
+
+    // Add leftovers.
+    concatBuckets.add(curBucket);
+
+    // Split the existing buckets based on media types into concat batches.
+    List<List<Element>> concatBatches = Lists.newLinkedList();
+    Iterator<List<Element>> batchesIter = concatBuckets.iterator();
+    while (batchesIter.hasNext()) {
+      splitBatchOnMedia(batchesIter.next(), concatBatches);
+    }
+
+    // Prepare batches of Uris to send to generate concat Uris
+    List<List<Uri>> uriBatches = Lists.newLinkedList();
+    batchesIter = concatBatches.iterator();
+    while (batchesIter.hasNext()) {
+      List<Element> batch = batchesIter.next();
+      List<Uri> uris = Lists.newLinkedList();
+      if (batch.isEmpty() || !getUris(type, batch, uris)) {
+        batchesIter.remove();
+        continue;
+      }
+      uriBatches.add(uris);
+    }
+
+    if (uriBatches.isEmpty()) {
+      return false;
+    }
+
+    // Generate the ConcatUris, then correlate with original elements.
+    List<ConcatUriManager.ConcatData> concatUris =
+        uriManager.make(
+          ConcatUriManager.ConcatUri.fromList(gadget, uriBatches, type), !split);
+
+    Iterator<List<Element>> elemBatchIt = concatBatches.iterator();
+    Iterator<List<Uri>> uriBatchIt = uriBatches.iterator();
+    for (ConcatUriManager.ConcatData concatUri : concatUris) {
+      List<Element> sourceBatch = elemBatchIt.next();
+      List<Uri> sourceUris = uriBatchIt.next();
+
+      // Regardless what happens, inject as many copies of the first node
+      // as needed, with new (concat) URI, immediately ahead of the first elem.
+      Element firstElem = sourceBatch.get(0);
+      for (Uri uri : concatUri.getUris()) {
+        Element elemConcat = (Element)firstElem.cloneNode(true);
+        elemConcat.setAttribute(type.getSrcAttrib(), uri.toString());
+        firstElem.getParentNode().insertBefore(elemConcat, firstElem);
+      }
+
+      // Now for all Elements, either A) remove them or B) replace each
+      // with a <script> node with snippet of code configuring/evaluating
+      // the resultant inserted code. This is useful for split-JS in particular,
+      // and might also be used in spriting later.
+      Iterator<Uri> uriIt = sourceUris.iterator();
+      for (Element elem : sourceBatch) {
+        Uri elemOrigUri = uriIt.next();
+        String snippet = concatUri.getSnippet(elemOrigUri);
+        if (!Strings.isNullOrEmpty(snippet)) {
+          Node scriptNode = elem.getOwnerDocument().createElement("script");
+          scriptNode.setTextContent(snippet);
+          elem.getParentNode().insertBefore(scriptNode, elem);
+        }
+        elem.getParentNode().removeChild(elem);
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * Split the given batch of elements (assumed to be sibling nodes that can be concatenated)
+   * into batches with same media types.
+   *
+   * @param elements
+   * @param output
+   */
+  private void splitBatchOnMedia(List<Element> elements, List<List<Element>> output) {
+    // Multimap to hold the ordered list of elements encountered for a given media type.
+    Multimap<String, Element> mediaBatchMap = LinkedHashMultimap.create();
+    for (Element element : elements) {
+      String mediaType = element.getAttribute("media");
+      mediaBatchMap.put(Strings.isNullOrEmpty(mediaType) ? "screen" : mediaType, element);
+    }
+    Set<String> mediaTypes = mediaBatchMap.keySet();
+    for (String mediaType : mediaTypes) {
+      Collection<Element> elems = mediaBatchMap.get(mediaType);
+      output.add(new LinkedList<Element>(elems));
+    }
+  }
+
+  private boolean isRewritableExternData(Element elem) {
+    String uriStr = elem != null ? elem.getAttribute(type.getSrcAttrib()) : null;
+    if (Strings.isNullOrEmpty(uriStr) ||
+        !config.shouldRewriteURL(uriStr)) {
+      return false;
+    }
+    if (type == ConcatUriManager.Type.CSS) {
+      // rel="stylesheet" and type="text/css" also required.
+      return ("stylesheet".equalsIgnoreCase(elem.getAttribute("rel")) &&
+              "text/css".equalsIgnoreCase(elem.getAttribute("type")));
+    }
+    return true;
+  }
+
+  private Element getSibling(Element root, boolean isPrev) {
+    Node cur = root;
+    while ((cur = getNext(cur, isPrev)) != null) {
+      // Text nodes are safe to skip, as they won't effect styles or scripts.
+      // It is also safe to skip comment nodes except for conditional comments.
+      if (cur.getNodeType() == Node.TEXT_NODE ||
+          (cur.getNodeType() == Node.COMMENT_NODE && !isConditionalComment(cur))) {
+        continue;
+      }
+      break;
+    }
+    if (cur != null && cur.getNodeType() == Node.ELEMENT_NODE) {
+      return (Element)cur;
+    }
+    return null;
+  }
+
+  private Node getNext(Node node, boolean isPrev) {
+    return isPrev ? node.getPreviousSibling() : node.getNextSibling();
+  }
+
+  private boolean getUris(ConcatUriManager.Type type, List<Element> elems, List<Uri> uris) {
+    for (Element elem : elems) {
+      String uriStr = elem.getAttribute(type.getSrcAttrib());
+      try {
+        uris.add(Uri.parse(uriStr));
+      } catch (Uri.UriException e) {
+        // Invalid formatted Uri, batch failed.
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Checks if the css link tags can be put into the same bucket.
+   */
+  private boolean areLinkNodesBucketable(Element current, Element next) {
+    boolean areLinkNodesCompatible = false;
+    // All link tags with media='all' should be placed in their own buckets.
+    // Except for adjacent css links with media='all', which can belong to the
+    // same bucket.
+    String currMediaType = current.getAttribute("media");
+    String nextMediaType = next.getAttribute("media");
+    if (("all".equalsIgnoreCase(currMediaType) && "all".equalsIgnoreCase(nextMediaType)) ||
+        (!"all".equalsIgnoreCase(currMediaType) && !"all".equalsIgnoreCase(nextMediaType))) {
+      areLinkNodesCompatible = true;
+    }
+
+    // we can't keep the link tags with different 'title' attribute in same
+    // bucket.
+    // An example that proves the above comment.
+    // <link rel="stylesheet" type="text/css" href="a.css" />
+    // <link rel="stylesheet" type="text/css" href="b.css" title="small font"/>
+    // <link rel="stylesheet" type="text/css" href="c.css" />
+    // <link rel="alternate stylesheet" type="text/css" href="d.css" title="large font"/>
+    // Since browser allows to switch between the perferred styles 'small font' and 'large font',
+    // we should not batch across the links with title attribute, as it will lead to reordering of
+    // styles.
+    return areLinkNodesCompatible && current.getAttribute("title").equals(next.getAttribute("title"));
+  }
+
+  /**
+   * Checks if a given comment node is coditional comment.
+   */
+  private boolean isConditionalComment(Node node) {
+    return node.getNodeValue().trim().startsWith("[if");
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeature.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeature.java
new file mode 100644
index 0000000..96504a0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeature.java
@@ -0,0 +1,430 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for the "content-rewrite" feature. The supported params are
+ * include-url and exclude-url which honor multiple occurances of the parameter,
+ * these are simple case insensitive substrings, with "*" being the match-all
+ * wildcard. Additionally expires is the seconds for caching of the rewritten
+ * result. For legacy applications include-urls and exclude-urls, which are
+ * regular expressions as well as a common seperated list in include-tags.
+ * Default values are container specific.
+ */
+public class ContentRewriterFeature {
+  protected static final String INCLUDE_URLS = "include-urls";
+  protected static final String EXCLUDE_URLS = "exclude-urls";
+  protected static final String INCLUDE_URL = "include-url";
+  protected static final String EXCLUDE_URL = "exclude-url";
+  protected static final String INCLUDE_TAGS = "include-tags";
+  protected static final String EXPIRES = "expires";
+
+  public static final Integer EXPIRES_HTTP = -1;  // -1 = Use HTTP.
+
+  protected enum PatternOptions {
+    ALL, NONE, REGEX, STRINGS
+  }
+
+  /**
+   * Factory for content rewriter features.
+   */
+  @Singleton
+  public static class Factory {
+    private final GadgetSpecFactory specFactory;
+    private final Provider<DefaultConfig> defaultConfig;
+
+    private final LoadingCache<GadgetSpec, Config> rewriterConfigCache = CacheBuilder
+        .newBuilder()
+        .weakKeys()
+        .build(
+            new CacheLoader<GadgetSpec, Config>() {
+              @Override
+              public Config load(GadgetSpec spec) throws Exception {
+                return new Config(spec, defaultConfig.get());
+              }
+            }
+        );
+
+    @Inject
+    public Factory(GadgetSpecFactory specFactory, Provider<DefaultConfig> defaultConfig) {
+      this.specFactory = specFactory;
+      this.defaultConfig = defaultConfig;
+    }
+
+    public Config getDefault() {
+      return defaultConfig.get();
+    }
+
+    public Config get(HttpRequest request) {
+      GadgetSpec spec;
+      final Uri gadgetUrl = request.getGadget();
+      final boolean isIgnoreCache = request.getIgnoreCache();
+      if (gadgetUrl != null) {
+        try {
+          GadgetContext context = new GadgetContext() {
+            @Override
+            public Uri getUrl() {
+              return gadgetUrl;
+            }
+
+            @Override
+            public boolean getIgnoreCache() {
+              return isIgnoreCache;
+            }
+          };
+
+          spec = specFactory.getGadgetSpec(context);
+          if (spec != null) {
+            return get(spec);
+          }
+        } catch (GadgetException ge) {
+          // Falls through to default.
+        }
+      }
+      return defaultConfig.get();
+    }
+
+    public Config get(GadgetSpec spec) {
+      return rewriterConfigCache.getUnchecked(spec);
+    }
+
+    /**
+     * Create a rewriter feature that allows all URIs to be rewritten.
+     */
+    public Config createRewriteAllFeature(int ttl) {
+      return new Config(
+          ".*", "", (ttl == -1) ? "HTTP" : Integer.toString(ttl),
+          "", false, true, false);
+    }
+  }
+
+  @Singleton
+  public static class DefaultConfig extends Config {
+    @Inject
+    public DefaultConfig(
+        @Named("shindig.content-rewrite.include-urls")String includeUrls,
+        @Named("shindig.content-rewrite.exclude-urls")String excludeUrls,
+        @Named("shindig.content-rewrite.expires")String expires,
+        @Named("shindig.content-rewrite.include-tags")String includeTags,
+        @Named("shindig.content-rewrite.only-allow-excludes")boolean onlyAllowExcludes,
+        @Named("shindig.content-rewrite.enable-split-js-concat")boolean enableSplitJsConcat,
+        @Named("shindig.content-rewrite.enable-single-resource-concat")boolean
+            enableSingleResourceConcatenation) {
+      super(includeUrls, excludeUrls, expires, includeTags, onlyAllowExcludes,
+            enableSplitJsConcat, enableSingleResourceConcatenation);
+    }
+  }
+
+  public static class Config {
+    private final MatchBundle includes;
+    private final MatchBundle excludes;
+
+    // Use tree set to maintain order for fingerprint
+    private final Set<String> includeTags;
+
+    // If null then dont enforce a min TTL for proxied content.
+    // Use contents headers
+    private final Integer expires;
+    private final boolean onlyAllowExcludes;
+    private final boolean enableSplitJs;
+    private final boolean enableSingleResourceConcatenation;
+
+    // Lazily computed
+    private Integer fingerprint;
+
+    /**
+     * Constructor which takes a gadget spec and container settings
+     * as "raw" input strings.
+     *
+     * @param defaultInclude As a regex
+     * @param defaultExclude As a regex
+     * @param defaultExpires Either "HTTP" or a ttl in seconds
+     * @param defaultTags Set of default tags that can be rewritten
+     * @param onlyAllowExcludes If includes are always implicitly "all"
+     * @param enableSplitJs If split-JS technique is enabled
+     * @param enableSingleResourceConcatenation If single resource can be concatenated with itself
+     */
+    Config(String defaultInclude,
+        String defaultExclude, String defaultExpires, String defaultTags,
+        boolean onlyAllowExcludes, boolean enableSplitJs, boolean enableSingleResourceConcatenation) {
+      // Set up includes from defaultInclude param
+      this.includes = getMatchBundle(paramTrim(defaultInclude),
+          Collections.<String>emptyList());
+
+      // Set up excludes from defaultExclude param
+      this.excludes = getMatchBundle(paramTrim(defaultExclude),
+          Collections.<String>emptyList());
+
+      // Parse includeTags
+      ImmutableSet.Builder<String> includeTagsBuilder = ImmutableSet.builder();
+      for (String s : Splitter.on(',').trimResults().omitEmptyStrings().split(defaultTags.toLowerCase())) {
+        includeTagsBuilder.add(s);
+      }
+      this.includeTags = includeTagsBuilder.build();
+
+      // Parse expires field
+      int expiresVal = EXPIRES_HTTP;
+      try {
+        expiresVal = Integer.parseInt(paramTrim(defaultExpires));
+      } catch (NumberFormatException e) {
+        // Fall through to default.
+      }
+      this.expires = expiresVal;
+
+      // Save config for onlyAllowExcludes
+      this.onlyAllowExcludes = onlyAllowExcludes;
+      this.enableSplitJs = enableSplitJs;
+      this.enableSingleResourceConcatenation = enableSingleResourceConcatenation;
+    }
+
+    Config(GadgetSpec spec, Config defaultConfig) {
+      this.onlyAllowExcludes = defaultConfig.onlyAllowExcludes;
+
+      Feature f = spec.getModulePrefs().getFeatures().get("content-rewrite");
+
+      // Include overrides.
+      // Note: Shindig originally supported the plural versions with regular
+      // expressions. But the OpenSocial specification v0.9 allows for singular
+      // spellings, with multiple values. Plus they are case insensitive substrings.
+      // For backward compatibility, if the singular versions are present they
+      // will override the plural versions. 10/6/09
+      String includeRegex = defaultConfig.includes.param;
+      Collection<String> includeUrls = Lists.newArrayList();
+      if (f != null && !onlyAllowExcludes) {
+        if (f.getParams().containsKey(INCLUDE_URLS)) {
+          includeRegex = f.getParam(INCLUDE_URLS);
+        }
+        Collection<String> paramUrls = f.getParamCollection(INCLUDE_URL);
+        for (String url : paramUrls) {
+          includeUrls.add(url.trim().toLowerCase());
+        }
+      }
+      this.includes = getMatchBundle(includeRegex, includeUrls);
+
+      // Exclude overrides. Only use the exclude regex specified by the
+      // gadget spec if !onlyAllowExcludes.
+      String excludeRegex = defaultConfig.excludes.param;
+      Collection<String> excludeUrls = Lists.newArrayList();
+      if (f != null) {
+        if (f.getParams().containsKey(EXCLUDE_URLS)) {
+          excludeRegex = f.getParam(EXCLUDE_URLS);
+        }
+        Collection<String> eParamUrls = f.getParamCollection(EXCLUDE_URL);
+        for (String url : eParamUrls) {
+          excludeUrls.add(url.trim().toLowerCase());
+        }
+      }
+      this.excludes = getMatchBundle(excludeRegex, excludeUrls);
+
+      // Spec-specified include tags.
+      Set<String> tagsVal;
+      if (f != null && f.getParams().containsKey(INCLUDE_TAGS)) {
+        tagsVal = Sets.newTreeSet();
+        for (String tag : Splitter.on(',').trimResults().omitEmptyStrings().split(f.getParam(INCLUDE_TAGS))) {
+          tagsVal.add(tag.toLowerCase());
+        }
+        if (onlyAllowExcludes) {
+          // Only excludes are allowed. Keep only subset of
+          // specified tags that are in the defaults.
+          tagsVal.retainAll(defaultConfig.includeTags);
+        }
+      } else {
+        tagsVal = ImmutableSortedSet.copyOf(defaultConfig.includeTags);
+      }
+      this.includeTags = tagsVal;
+
+      // Let spec/feature override if present and smaller than default.
+      int expiresVal = defaultConfig.expires;
+      if (f != null && f.getParams().containsKey(EXPIRES)) {
+        try {
+          int overrideVal = Integer.parseInt(f.getParam(EXPIRES));
+          expiresVal = (expiresVal == EXPIRES_HTTP || overrideVal < expiresVal) ?
+              overrideVal : expiresVal;
+        } catch (NumberFormatException e) {
+          // Falls through to default.
+          if ("HTTP".equalsIgnoreCase(f.getParam(EXPIRES).trim())) {
+            expiresVal = EXPIRES_HTTP;
+          }
+        }
+      }
+      this.expires = expiresVal;
+      this.enableSplitJs = defaultConfig.enableSplitJs;
+      this.enableSingleResourceConcatenation = defaultConfig.enableSingleResourceConcatenation;
+    }
+
+    private String paramTrim(String param) {
+      if (param == null) {
+        return param;
+      }
+
+      return param.trim();
+    }
+
+    private MatchBundle getMatchBundle(String regex, Collection<String> matches) {
+      MatchBundle bundle = new MatchBundle();
+      bundle.param = regex;
+      bundle.matches = matches;
+
+      if (bundle.matches.isEmpty() && Strings.isNullOrEmpty(bundle.param)) {
+        bundle.options = PatternOptions.NONE;
+      } else if (bundle.matches.size() == 1) {
+        String firstVal = bundle.matches.iterator().next();
+        if ("*".equals(firstVal)) {
+          bundle.options = PatternOptions.ALL;
+        } else if ("".equals(firstVal)){
+          bundle.options = PatternOptions.NONE;
+        } else {
+          bundle.options = PatternOptions.STRINGS;
+        }
+      } else if (bundle.matches.size() > 1) {
+        bundle.options = PatternOptions.STRINGS;
+      } else {
+        if (".*".equals(bundle.param)) {
+          bundle.options = PatternOptions.ALL;
+        } else {
+          bundle.options = PatternOptions.REGEX;
+        }
+        bundle.pattern = Pattern.compile(bundle.param);
+      }
+      return bundle;
+    }
+
+    private static class MatchBundle {
+      private String param;
+      private PatternOptions options;
+      private Pattern pattern;
+      private Collection<String> matches;
+    }
+
+    protected boolean shouldInclude(String url) {
+      return matcherMatches(url, includes);
+    }
+
+    protected boolean shouldExclude(String url) {
+      return matcherMatches(url, excludes);
+    }
+
+    private static boolean matcherMatches(String url, MatchBundle bundle) {
+      switch (bundle.options) {
+      case NONE:
+        return false;
+      case ALL:
+        return true;
+      case REGEX:
+        return bundle.pattern.matcher(url).find();
+      case STRINGS:
+        // "*" is handled by ALL
+        String urllc = url.toLowerCase();
+        for (String substr : bundle.matches) {
+          if (urllc.contains(substr))
+            return true;
+        }
+        return false;
+      }
+      return false;
+    }
+
+    public boolean isRewriteEnabled() {
+      return includes.options != PatternOptions.NONE &&
+             excludes.options != PatternOptions.ALL;
+    }
+
+    public boolean shouldRewriteURL(String url) {
+      return shouldInclude(url) && !shouldExclude(url);
+    }
+
+    public boolean shouldRewriteTag(String tag) {
+      if (tag != null) {
+        return this.includeTags.contains(tag.toLowerCase());
+      }
+      return false;
+    }
+
+    public Set<String> getIncludedTags() {
+      return includeTags;
+    }
+
+    /**
+     * @return the min TTL to enforce or null if proxy should respect headers
+     */
+    public Integer getExpires() {
+      return expires;
+    }
+
+    public boolean isSplitJsEnabled() {
+      return enableSplitJs;
+    }
+
+    public boolean isSingleResourceConcatEnabled() {
+      return enableSingleResourceConcatenation;
+    }
+
+    /**
+     * @return fingerprint of rewriting rule for cache-busting
+     */
+    public int getFingerprint() {
+      if (fingerprint == null) {
+        int result =
+            (includes.pattern != null ?
+              includes.pattern.pattern().hashCode() : 0);
+        for (String s : includes.matches) {
+          result = 31 * result + s.hashCode();
+        }
+        result = 31 * result +
+            (excludes.pattern != null ?
+              excludes.pattern.pattern().hashCode() : 0);
+        for (String s : excludes.matches) {
+          result = 31 * result + s.hashCode();
+        }
+        for (String s : includeTags) {
+          result = 31 * result + s.hashCode();
+        }
+        fingerprint = result;
+      }
+      return fingerprint;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverRewriter.java
new file mode 100644
index 0000000..111cc78
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverRewriter.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+/**
+ * Removes charset information from &lt;meta http-equip="Content-Type"%gt; tags
+ *
+ * @since 2.0.0
+ */
+public class ContentTypeCharsetRemoverRewriter extends DomWalker.Rewriter {
+  @Inject
+  public ContentTypeCharsetRemoverRewriter() {
+    super(ImmutableList.<DomWalker.Visitor>of(new ContentTypeCharsetRemoverVisitor()));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverVisitor.java
new file mode 100644
index 0000000..f25c0d9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverVisitor.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.uri.UriUtils;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Removes charset information from &lt;meta http-equip="Content-Type"&gt;
+ *
+ * @since 2.0.0
+ */
+public class ContentTypeCharsetRemoverVisitor implements DomWalker.Visitor {
+  public final static String CONTENT = "content";
+  public final static String CONTENT_TYPE = "content-type";
+  public final static String HTTP_EQUIV = "http-equiv";
+  public final static String META = "meta";
+
+  // @Override
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    if (node.getNodeType() == Node.ELEMENT_NODE &&
+        META.equalsIgnoreCase(node.getNodeName())) {
+
+      Element elem = (Element) node;
+      String httpEquip = elem.getAttribute(HTTP_EQUIV);
+      String content = elem.getAttribute(CONTENT);
+      if (httpEquip != null && content != null &&
+          CONTENT_TYPE.equalsIgnoreCase(httpEquip)) {
+        elem.setAttribute(CONTENT, UriUtils.getContentTypeWithoutCharset(content));
+        return VisitStatus.MODIFY;
+      }
+    }
+    return VisitStatus.BYPASS;
+  }
+
+  // @Override
+  public boolean revisit(Gadget gadget, List<Node> nodes) {
+    // Edits in place.
+    return false;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContextAwareRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContextAwareRegistry.java
new file mode 100644
index 0000000..d15cc87
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ContextAwareRegistry.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Provider;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An implementation of ResponseRewriterRegistry which applies the list of
+ * rewriters based on the container and the rewrite flow id.
+ */
+public class ContextAwareRegistry implements ResponseRewriterRegistry {
+  protected final GadgetHtmlParser htmlParser;
+  protected final RewriteFlow rewriteFlow;
+  protected final Provider<Map<RewritePath, Provider<List<ResponseRewriter>>>>
+      rewritePathToRewriterList;
+
+  public ContextAwareRegistry(GadgetHtmlParser htmlParser,
+                              RewriteFlow rewriteFlow,
+                              Provider<Map<RewritePath, Provider<List<ResponseRewriter>>>>
+                                  rewritePathToRewriterList) {
+    this.rewriteFlow = rewriteFlow;
+    this.rewritePathToRewriterList = rewritePathToRewriterList;
+    this.htmlParser = htmlParser;
+  }
+
+  /**
+   * Returns the list of response rewriters for the given container. Falls back
+   * to the default container if no rewriters are present for the given
+   * container.
+   * @param container The container to return rewriters for.
+   * @return List of response rewriters for given container and rewrite flow.
+   */
+  List<ResponseRewriter> getResponseRewriters(String container) {
+    RewritePath rewritePath = new RewritePath(container,  rewriteFlow);
+    Provider<List<ResponseRewriter>> rewriterListProvider =
+        rewritePathToRewriterList.get().get(rewritePath);
+
+    if (rewriterListProvider == null) {
+      // Try default container if there are no rewriters provided for current container.
+      rewritePath = new RewritePath(ContainerConfig.DEFAULT_CONTAINER,  rewriteFlow);
+      rewriterListProvider = rewritePathToRewriterList.get().get(rewritePath);
+    }
+
+    return rewriterListProvider != null ? rewriterListProvider.get() :
+                                          ImmutableList.<ResponseRewriter>of();
+  }
+
+  public HttpResponse rewriteHttpResponse(HttpRequest req, HttpResponse resp,
+          Gadget gadget) throws RewritingException {
+    HttpResponseBuilder builder = new HttpResponseBuilder(htmlParser, resp);
+    for (ResponseRewriter rewriter : getResponseRewriters(req.getContainer())) {
+      rewriter.rewrite(req, builder, gadget);
+    }
+
+    // Returns the original HttpResponse if no changes have been made.
+    return builder.create();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CssResponseRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CssResponseRewriter.java
new file mode 100644
index 0000000..094859f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/CssResponseRewriter.java
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Strings;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.caja.CajaCssParser;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.w3c.dom.Element;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.caja.lexer.ParseException;
+import com.google.caja.parser.AbstractParseTreeNode;
+import com.google.caja.parser.AncestorChain;
+import com.google.caja.parser.Visitor;
+import com.google.caja.parser.css.CssTree;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+/**
+ * Rewrite links to referenced content in a stylesheet
+ *
+ * @since 2.0.0
+ */
+public class CssResponseRewriter implements ResponseRewriter {
+
+  //class name for logging purpose
+  private static final String classname = CssResponseRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final CajaCssParser cssParser;
+  protected final ProxyUriManager proxyUriManager;
+  protected final ContentRewriterFeature.Factory rewriterFeatureFactory;
+
+  @Inject
+  public CssResponseRewriter(CajaCssParser cssParser,
+      ProxyUriManager proxyUriManager, ContentRewriterFeature.Factory rewriterFeatureFactory) {
+    this.cssParser = cssParser;
+    this.proxyUriManager = proxyUriManager;
+    this.rewriterFeatureFactory = rewriterFeatureFactory;
+  }
+
+  public void rewrite(HttpRequest request, HttpResponseBuilder original, Gadget gadget)
+          throws RewritingException {
+    ContentRewriterFeature.Config config = rewriterFeatureFactory.get(request);
+    if (!RewriterUtils.isCss(request, original)) {
+      return;
+    }
+
+    String css = original.getContent();
+    StringWriter sw = new StringWriter((css.length() * 110) / 100);
+    rewrite(new StringReader(css), request.getUri(),
+        new UriMaker(proxyUriManager, config), sw, false,
+            DomWalker.makeGadget(request).getContext());
+    original.setContent(sw.toString());
+  }
+
+  /**
+   * Rewrite the given CSS content and optionally extract the import references.
+   * @param content CSS content
+   * @param source Uri of content
+   * @param uriMaker a Uri Maker
+   * @param writer Output
+   * @param extractImports If true remove the import statements from the output and return their
+   *            referenced URIs.
+   * @param gadgetContext The gadgetContext
+   *
+   * @return Empty list of extracted import URIs.
+   */
+  public List<String> rewrite(Reader content, Uri source, UriMaker uriMaker, Writer writer,
+      boolean extractImports, GadgetContext gadgetContext) throws RewritingException {
+    try {
+      String original = IOUtils.toString(content);
+      try {
+        CssTree.StyleSheet stylesheet = cssParser.parseDom(original, source);
+        List<String> stringList = rewrite(stylesheet, source, uriMaker, extractImports,
+            gadgetContext);
+        // Serialize the stylesheet
+        cssParser.serialize(stylesheet, writer);
+        return stringList;
+      } catch (GadgetException ge) {
+        if (ge.getCause() instanceof ParseException) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "rewrite", MessageKeys.CAJA_CSS_PARSE_FAILURE, new Object[] {ge.getCause().getMessage(),source});
+          }
+          writer.write(original);
+          return Collections.emptyList();
+        } else {
+          throw new RewritingException(ge, ge.getHttpStatusCode());
+        }
+      }
+    } catch (IOException ioe) {
+      throw new RewritingException(ioe, HttpResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+
+  /**
+   * Rewrite the CSS content in a style DOM node.
+   * @param styleNode Rewrite the CSS content of this node
+   * @param source Uri of content
+   * @param uriMaker a UriMaker
+   * @param extractImports If true remove the import statements from the output and return their
+   *            referenced URIs.
+   * @param gadgetContext The gadgetContext
+   * @return Empty list of extracted import URIs.
+   */
+  public List<String> rewrite(Element styleNode, Uri source, UriMaker uriMaker,
+      boolean extractImports, GadgetContext gadgetContext) throws RewritingException {
+    try {
+      CssTree.StyleSheet stylesheet =
+        cssParser.parseDom(styleNode.getTextContent(), source);
+      List<String> imports = rewrite(stylesheet, source, uriMaker, extractImports, gadgetContext);
+      // Write the rewritten CSS back into the element
+      String content = cssParser.serialize(stylesheet);
+      if (Strings.isNullOrEmpty(content) || StringUtils.isWhitespace(content)) {
+        // Remove the owning node
+        styleNode.getParentNode().removeChild(styleNode);
+      } else {
+        styleNode.setTextContent(content);
+      }
+      return imports;
+    } catch (GadgetException ge) {
+      if (ge.getCause() instanceof ParseException) {
+    	if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.WARNING, classname, "rewrite", MessageKeys.CAJA_CSS_PARSE_FAILURE, new Object[] {ge.getCause().getMessage(),source});
+        }
+        return Collections.emptyList();
+      } else {
+        throw new RewritingException(ge, ge.getHttpStatusCode());
+      }
+    }
+  }
+
+  /**
+   * Rewrite the CSS DOM in place.
+   * @param styleSheet To rewrite
+   * @param source  Uri of content
+   * @param uriMaker a UriMaker
+   * @param extractImports If true remove the import statements from the output and return their
+   *            referenced URIs.
+   * @return Empty list of extracted import URIs.
+   */
+  public static List<String> rewrite(CssTree.StyleSheet styleSheet, final Uri source,
+      final UriMaker uriMaker, final boolean extractImports, final GadgetContext gadgetContext) {
+    final List<String> imports = Lists.newLinkedList();
+    final List<CssTree.UriLiteral> skip = Lists.newLinkedList();
+
+    styleSheet.acceptPreOrder(new Visitor() {
+      public boolean visit(AncestorChain<?> chain) {
+        if (chain.node instanceof CssTree.Import) {
+          CssTree.Import importNode = (CssTree.Import) chain.node;
+          CssTree.UriLiteral uriLiteral = importNode.getUri();
+          skip.add(importNode.getUri());
+          if (extractImports) {
+            imports.add(uriLiteral.getValue());
+            ((AbstractParseTreeNode) chain.getParentNode()).removeChild(chain.node);
+          } else {
+            String rewritten = rewriteUri(uriMaker, uriLiteral.getValue(), source, gadgetContext);
+            uriLiteral.setValue(rewritten);
+          }
+        } else if (chain.node instanceof CssTree.UriLiteral &&
+            !skip.contains(chain.node)) {
+          CssTree.UriLiteral uriDecl = (CssTree.UriLiteral) chain.node;
+          String rewritten = rewriteUri(uriMaker, uriDecl.getValue(), source, gadgetContext);
+          uriDecl.setValue(rewritten);
+        }
+        return true;
+      }}, null);
+
+    return imports;
+  }
+
+  private static String rewriteUri(UriMaker uriMaker, String input, Uri context,
+      GadgetContext gadgetContext) {
+    Uri inboundUri;
+    try {
+      inboundUri = Uri.parse(input);
+    } catch (IllegalArgumentException e) {
+      // Don't rewrite at all.
+      return input;
+    }
+    if (context != null) {
+      inboundUri = context.resolve(inboundUri);
+    }
+    ProxyUriManager.ProxyUri proxyUri =
+        new ProxyUriManager.ProxyUri(DomWalker.makeGadget(gadgetContext), inboundUri);
+    return uriMaker.make(proxyUri, context).toString();
+  }
+
+  public static UriMaker uriMaker(ProxyUriManager wrapped, ContentRewriterFeature.Config config) {
+    return new UriMaker(wrapped, config);
+  }
+
+  public static class UriMaker {
+    protected final ProxyUriManager wrapped;
+    protected final ContentRewriterFeature.Config config;
+
+    public UriMaker(ProxyUriManager wrapped, ContentRewriterFeature.Config config) {
+      this.wrapped = wrapped;
+      this.config = config;
+    }
+
+    public Uri make(ProxyUriManager.ProxyUri uri, Uri context) {
+      if (config.shouldRewriteURL(uri.getResource().toString())) {
+        List<ProxyUriManager.ProxyUri> puris = Lists.newArrayList(uri);
+        List<Uri> returned = wrapped.make(puris, null);
+        return returned.get(0);
+      }
+      return context.resolve(uri.getResource());
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DefaultResponseRewriterRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DefaultResponseRewriterRegistry.java
new file mode 100644
index 0000000..86a5b57
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DefaultResponseRewriterRegistry.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+
+import java.util.Collections;
+import java.util.List;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+
+/**
+ * Basic registry -- just iterates over rewriters and invokes them sequentially.
+ *
+ * @since 2.0.0
+ */
+public class DefaultResponseRewriterRegistry implements ResponseRewriterRegistry {
+  protected final List<ResponseRewriter> rewriters;
+  protected final GadgetHtmlParser htmlParser;
+
+  @Inject
+  public DefaultResponseRewriterRegistry(List<ResponseRewriter> rewriters,
+      GadgetHtmlParser htmlParser) {
+    if (rewriters == null) {
+      rewriters = Collections.emptyList();
+    }
+    this.rewriters = Lists.newLinkedList(rewriters);
+    this.htmlParser = htmlParser;
+  }
+
+  /** {@inheritDoc} */
+  public HttpResponse rewriteHttpResponse(HttpRequest req, HttpResponse resp, Gadget gadget)
+          throws RewritingException {
+    HttpResponseBuilder builder = new HttpResponseBuilder(htmlParser, resp);
+
+    for (ResponseRewriter rewriter : rewriters) {
+      rewriter.rewrite(req, builder, gadget);
+    }
+
+    // Returns the original HttpResponse if no changes have been made.
+    return builder.create();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java
new file mode 100644
index 0000000..fcd808f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/DomWalker.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Framework-in-a-framework facilitating the common Visitor case
+ * in which a DOM tree is walked in order to manipulate it.
+ *
+ * See subclass doc for additional detail.
+ *
+ * @since 2.0.0
+ */
+public final class DomWalker {
+  private DomWalker() {}
+
+  /**
+   * Implemented by classes that do actual manipulation of the DOM
+   * while {@code DomWalker.ContentVisitor} walks it. {@code Visitor}
+   * instances are called for each {@code Node} in the DOM in the order
+   * they are registered with the {@code DomVisitor.ContentVisitor}
+   */
+  public interface Visitor {
+    /**
+     * Returned by the {@code visit(Gadget, Node)} method, signaling:
+     *
+     * BYPASS = Visitor doesn't care about the node.
+     * MODIFY = Visitor has modified the node.
+     * RESERVE_NODE = Visitor reserves exactly the node passed. No other
+     *   Visitor will visit the node.
+     * RESERVE_TREE = Visitor reserves the node passed and all its descendants
+     *   No other Visitor will visit them.
+     *
+     * Visitors are expected to be well-behaved in that they do not
+     * modify unreserved nodes: that is, in revisit(...) they do not access
+     * adjacent, parent, etc. nodes and modify them. visit(...) may return
+     * MODIFY to indicate a modification of the given node.
+     *
+     * Other append and delete operations are acceptable
+     * but only in revisit(). Reservations are supported in order to support
+     * "batched" lookups relating to a similar set of data retrieved from a
+     * backend.
+     */
+    public enum VisitStatus {
+      BYPASS,
+      MODIFY,
+      RESERVE_NODE,
+      RESERVE_TREE
+    }
+
+    /**
+     * Visit a particular Node in the DOM.
+     *
+     * @param gadget Context for the request.
+     * @param node Node being visited.
+     * @return Status, see {@code VisitStatus}
+     */
+    VisitStatus visit(Gadget gadget, Node node) throws RewritingException;
+
+    /**
+     * Revisit a node in the DOM that was marked by the
+     * {@code visit(Gadget, Node)} as reserved during DOM traversal.
+     *
+     * @param gadget Context for the request.
+     * @param nodes Nodes being revisited, previously marked as reserved.
+     * @return True if any node modified, false otherwise.
+     */
+    boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException;
+  }
+
+  /**
+   * Rewriter that traverses the DOM, passing each node to its
+   * list of {@code Visitor} instances in order. Each visitor
+   * may bypass, modify, or reserve the node. Reserved nodes
+   * will be revisited after the entire DOM tree is walked.
+   * The DOM tree is walked in depth-first order.
+   */
+  public static class Rewriter implements GadgetRewriter, ResponseRewriter {
+    private final List<Visitor> visitors;
+
+    public Rewriter(List<Visitor> visitors) {
+      this.visitors = visitors;
+    }
+
+    public Rewriter(Visitor... visitors) {
+      this.visitors = Arrays.asList(visitors);
+    }
+
+    public Rewriter() {
+      this.visitors = null;
+    }
+
+    // Override this to supply a list of Visitors generated using request context
+    // rather than supplied at construction time.
+    protected List<Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+      return visitors;
+    }
+
+    /**
+     * Performs the DomWalker rewrite operation described in class javadoc.
+     */
+    public void rewrite(Gadget gadget, MutableContent content)
+        throws RewritingException {
+      rewrite(makeVisitors(gadget, gadget.getSpec().getUrl()), gadget, content);
+    }
+
+    public void rewrite(HttpRequest request, HttpResponseBuilder builder, Gadget gadget)
+            throws RewritingException {
+      if (RewriterUtils.isHtml(request, builder)) {
+        if(gadget == null) {
+          gadget = makeGadget(request);
+        }
+        rewrite(makeVisitors(gadget, request.getGadget()), gadget, builder);
+      }
+    }
+
+    private boolean rewrite(List<Visitor> visitors, Gadget gadget, MutableContent content)
+        throws RewritingException {
+      Map<Visitor, List<Node>> reservations = Maps.newHashMap();
+
+      LinkedList<Node> toVisit = Lists.newLinkedList();
+      Document doc = content.getDocument();
+      if (doc == null) {
+        throw new RewritingException("content.getDocument is null. Content: "
+                                     + content.getContent(),
+                                     HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      }
+      toVisit.add(doc.getDocumentElement());
+      boolean mutated = false;
+      while (!toVisit.isEmpty()) {
+        Node visiting = toVisit.removeFirst();
+
+        // Iterate through all visitors evaluating their visitation status.
+        boolean treeReserved = false;
+        boolean nodeReserved = false;
+        for (Visitor visitor : visitors) {
+          switch(visitor.visit(gadget, visiting)) {
+          case MODIFY:
+            content.documentChanged();
+            mutated = true;
+            break;
+          case RESERVE_NODE:
+            nodeReserved = true;
+            break;
+          case RESERVE_TREE:
+            treeReserved = true;
+            break;
+          default:
+            // Aka BYPASS - do nothing.
+            break;
+          }
+
+          if (nodeReserved || treeReserved) {
+            // Reservation was made.
+            if (!reservations.containsKey(visitor)) {
+              reservations.put(visitor, Lists.<Node>newLinkedList());
+            }
+            reservations.get(visitor).add(visiting);
+            break;
+          }
+        }
+
+        if (!treeReserved && visiting.hasChildNodes()) {
+          // Tree wasn't reserved - walk children.
+          // In order to preserve DFS order, walk children in reverse.
+          for (Node child = visiting.getLastChild(); child != null;
+               child = child.getPreviousSibling()) {
+            toVisit.addFirst(child);
+          }
+        }
+      }
+
+      // Run through all reservations, revisiting as needed.
+      for (Visitor visitor : visitors) {
+        List<Node> nodesReserved = reservations.get(visitor);
+        if (nodesReserved != null && visitor.revisit(gadget, nodesReserved)) {
+          content.documentChanged();
+          mutated = true;
+        }
+      }
+
+      return mutated;
+    }
+  }
+
+  // TODO: Remove these lame hacks by changing Gadget to a proper general Context object.
+  public static Gadget makeGadget(GadgetContext context) {
+    try {
+      final GadgetSpec spec = new GadgetSpec(context.getUrl(),
+          "<Module><ModulePrefs author=\"a\" title=\"t\"></ModulePrefs>" +
+          "<Content></Content></Module>");
+      return new Gadget().setSpec(spec).setContext(context);
+    } catch (Exception e) {
+      throw new RuntimeException("Unexpected boilerplate parse failure");
+    }
+  }
+
+  public static Gadget makeGadget(final HttpRequest request) {
+    return makeGadget(new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        return request.getUri();
+      }
+
+      @Override
+      public String getParameter(String key) {
+        return request.getParam(key);
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return request.getIgnoreCache();
+      }
+
+      @Override
+      public String getContainer() {
+        return request.getContainer();
+      }
+
+      @Override
+      public boolean getDebug() {
+        return "1".equalsIgnoreCase(getParameter(Param.DEBUG.getKey()));
+      }
+    });
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/GadgetRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/GadgetRewriter.java
new file mode 100644
index 0000000..53d935b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/GadgetRewriter.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.Gadget;
+
+/**
+ * Interface for rewriters that modify gadget content.
+ */
+public interface GadgetRewriter {
+  /**
+   * Rewrite the gadget.
+   *
+   * @param gadget Gadget to rewrite.
+   * @param content the content of the gadget to be manipulated.
+   */
+  void rewrite(Gadget gadget, MutableContent content) throws RewritingException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ImageAttributeRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ImageAttributeRewriter.java
new file mode 100644
index 0000000..3394517
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ImageAttributeRewriter.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.MultipleResourceHttpFetcher;
+import org.apache.shindig.gadgets.http.MultipleResourceHttpFetcher.RequestContext;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.sanselan.ImageFormat;
+import org.apache.sanselan.Sanselan;
+import org.apache.sanselan.ImageInfo;
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
+import org.w3c.dom.Node;
+import org.w3c.dom.Element;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.FutureTask;
+
+import java.util.List;
+import java.util.Map;
+import java.io.IOException;
+
+/**
+ * Rewriter that adds height/width attributes to <img> tags.
+ */
+public class ImageAttributeRewriter extends DomWalker.Rewriter {
+  //class name for logging purpose
+    private static final String classname = ImageAttributeRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  @Inject
+  public ImageAttributeRewriter(RequestPipeline requestPipeline, ExecutorService executor) {
+    super(new ImageAttributeVisitor(requestPipeline, executor));
+  }
+
+  /**
+   * Visitor that injects height/width attributes for <img> tags, if needed to
+   * reduce the page reflows.
+   */
+  public static class ImageAttributeVisitor implements DomWalker.Visitor {
+    private final RequestPipeline requestPipeline;
+    private final ExecutorService executor;
+
+    private static final String IMG_ATTR_CLASS_NAME_PREFIX = "__shindig__image";
+
+    public ImageAttributeVisitor(RequestPipeline requestPipeline,
+                                 @Named("shindig.concat.executor") ExecutorService executor) {
+      this.requestPipeline = requestPipeline;
+      this.executor = executor;
+    }
+
+    public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+      if (node.getNodeType() == Node.ELEMENT_NODE &&
+          node.getNodeName().equalsIgnoreCase("img")) {
+        Element imageElement = (Element) node;
+
+        // we process the <img> tag when it does not have 'class' and 'id'
+        // attributes in order to avoid conflicts from css styles.
+        if ("".equals(imageElement.getAttribute("class")) &&
+            "".equals(imageElement.getAttribute("id")) &&
+            !"".equals(imageElement.getAttribute("src")) &&
+            "".equals(imageElement.getAttribute("height")) &&
+            "".equals(imageElement.getAttribute("width"))) {
+          return VisitStatus.RESERVE_NODE;
+        }
+      }
+      return VisitStatus.BYPASS;
+    }
+
+    public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+      if (nodes.isEmpty()) {
+        return false;
+      }
+      Node head = DomUtil.getFirstNamedChildNode(
+          nodes.get(0).getOwnerDocument().getDocumentElement(), "head");
+
+      if (head == null) {
+        // Should never occur; do for paranoia's sake.
+        return false;
+      }
+
+      List<HttpRequest> resourceRequests = Lists.newArrayList();
+      for (Node node : nodes) {
+        String imgSrc = ((Element) node).getAttribute("src");
+        Uri uri = UriBuilder.parse(imgSrc).toUri();
+        try {
+          resourceRequests.add(buildHttpRequest(gadget, uri));
+        } catch (GadgetException e) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "revisit", MessageKeys.UNABLE_TO_PROCESS_IMG, new Object[] {imgSrc});
+          }
+        }
+      }
+
+      MultipleResourceHttpFetcher fetcher =
+          new MultipleResourceHttpFetcher(requestPipeline, executor);
+      Map<Uri, FutureTask<RequestContext>> futureTasks = fetcher.fetchUnique(resourceRequests);
+      String cssContent = processAllImgResources(nodes, futureTasks);
+
+      if (cssContent.length() > 0) {
+        Element style = nodes.get(0).getOwnerDocument().createElement("style");
+        style.setAttribute("type", "text/css");
+        style.setTextContent(cssContent);
+        head.insertBefore(style, head.getFirstChild());
+      }
+      return true;
+    }
+
+    /**
+     * The method process all the images,  determine which of them are safe for
+     * injecting css styles for height/width extracted from the image metadata,
+     * and returns the string of css styles that needed to injected.
+     *
+     * @param nodes nodes list of nodes for this we want to height/width
+     *    attribute injection in css.
+     * @param futureTasks futureTasks map of url -> futureTask for all the requests sent.
+     * @return string contianing the css styles that needs to be injected.
+     */
+    private String processAllImgResources(List<Node> nodes,
+                                          Map<Uri, FutureTask<RequestContext>> futureTasks) {
+      StringBuilder cssContent = new StringBuilder("");
+
+      for (int i = 0; i < nodes.size(); i++) {
+        Element imageElement = (Element) nodes.get(i);
+        String src = imageElement.getAttribute("src");
+        RequestContext requestCxt;
+
+        // Fetch the content of the requested uri.
+        try {
+          Uri imgUri = UriBuilder.parse(src).toUri();
+
+          try {
+            requestCxt = futureTasks.get(imgUri).get();
+          } catch (InterruptedException ie) {
+            throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, ie);
+          } catch (ExecutionException ie) {
+            throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, ie);
+          }
+
+          if (requestCxt.getGadgetException() != null) {
+            throw requestCxt.getGadgetException();
+          }
+
+          HttpResponse response = requestCxt.getHttpResp();
+          // Content header checking is fast so this is fine to do for every
+          // response.
+          ImageFormat imageFormat = Sanselan.guessFormat(
+              new ByteSourceInputStream(response.getResponse(), imgUri.getPath()));
+
+          if (imageFormat == ImageFormat.IMAGE_FORMAT_UNKNOWN) {
+             // skip this node
+            continue;
+          }
+
+          // extract height and width from the actual image and set these
+          // attributes of the <img> tag.
+          ImageInfo imageInfo = Sanselan.getImageInfo(response.getResponse(),
+                                                      imgUri.getPath());
+
+          if (imageInfo == null) {
+            continue;
+          }
+
+          int imageHeight = imageInfo.getHeight();
+          int imageWidth = imageInfo.getWidth();
+
+          if (imageHeight > 0 && imageWidth > 0 && imageHeight * imageWidth > 1) {
+            imageElement.setAttribute("class", IMG_ATTR_CLASS_NAME_PREFIX + i);
+            cssContent.append('.').append(IMG_ATTR_CLASS_NAME_PREFIX).append(i).append(" {\n")
+              .append("  height: ").append(imageHeight).append("px;\n")
+              .append("  width: ").append(imageWidth).append("px;\n")
+              .append("}\n");
+          }
+        } catch (ImageReadException e) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "processAllImgResources", MessageKeys.UNABLE_TO_READ_RESPONSE, new Object[] {src});
+          }
+        } catch (GadgetException e) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "processAllImgResources", MessageKeys.UNABLE_TO_FETCH_IMG, new Object[] {src});
+          }
+        } catch (IOException e) {
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "processAllImgResources", MessageKeys.UNABLE_TO_PARSE_IMG, new Object[] {src});
+          }
+        }
+      }
+
+      return cssContent.toString();
+    }
+
+    // TODO(satya): Need to pass the request parameters as well ?
+    public static HttpRequest buildHttpRequest(Gadget gadget, Uri imgUri)
+        throws GadgetException {
+      HttpRequest req = new HttpRequest(imgUri);
+      req.setFollowRedirects(true);
+      return req;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ImageResizeRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ImageResizeRewriter.java
new file mode 100644
index 0000000..ef7ec94
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ImageResizeRewriter.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.Inject;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.w3c.dom.Node;
+import org.w3c.dom.Element;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This rewriter helps in appending the image size parameters (extracted from inline styles, height
+ * and width) to the proxied resource urls so that server side resizing can be done when ever
+ * possible. Non-proxied resource URLs are ignored by this rewriter.
+ */
+public class ImageResizeRewriter extends DomWalker.Rewriter {
+  private final ContentRewriterFeature.Factory featureConfigFactory;
+  private final ProxyUriManager proxyUriManager;
+
+  @Inject
+  public ImageResizeRewriter(ProxyUriManager proxyUriManager,
+                             ContentRewriterFeature.Factory featureConfigFactory) {
+    this.featureConfigFactory = featureConfigFactory;
+    this.proxyUriManager = proxyUriManager;
+  }
+
+  @Override
+  protected List<DomWalker.Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+    ContentRewriterFeature.Config config = featureConfigFactory.get(context.getSpec());
+    return Arrays.<DomWalker.Visitor>asList(new ImageResizeVisitor(proxyUriManager, config));
+  }
+
+  public static class ImageResizeVisitor implements DomWalker.Visitor {
+    protected final ProxyUriManager proxyUriManager;
+    protected final ContentRewriterFeature.Config featureConfig;
+
+    public ImageResizeVisitor(ProxyUriManager proxyUriManager,
+                              ContentRewriterFeature.Config featureConfig) {
+      this.proxyUriManager = proxyUriManager;
+      this.featureConfig = featureConfig;
+    }
+
+    public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+      if (node.getNodeType() == Node.ELEMENT_NODE &&
+          node.getNodeName().equalsIgnoreCase("img")) {
+        Element imageElement = (Element) node;
+
+        // We process the <img> tag in following cases
+        // a) it has 'height' and 'width' but no 'id' and 'class' attributes.
+        // b) it has inline style attribute.
+        // TODO(satya): please beware of max-height, etc fields.
+        if ((!isEmpty(imageElement, "height") && !isEmpty(imageElement, "width") &&
+             isEmpty(imageElement, "id") && isEmpty(imageElement, "class")) ||
+            (!isEmpty(imageElement, "style"))) {
+          return addHeightWidthParams(imageElement);
+        }
+      }
+      return VisitStatus.BYPASS;
+    }
+
+    private boolean isEmpty(Element element, String attribute) {
+      return "".equals(element.getAttribute(attribute));
+    }
+
+    public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+      return true;
+    }
+
+    private VisitStatus addHeightWidthParams(Element imgElement) {
+      // We want to append image resize params only to urls that are proxied through us.
+      String uriStr = imgElement.getAttribute("src").trim();
+      Uri uri = Uri.parse(uriStr);
+      ProxyUriManager.ProxyUri proxied;
+
+      // Try parsing this uri as a ProxyUri.
+      try {
+        proxied = proxyUriManager.process(uri);
+      } catch (GadgetException e) {
+        return VisitStatus.BYPASS;
+      }
+
+      if (null == proxied || proxied.getStatus() == UriStatus.BAD_URI) {
+        return VisitStatus.BYPASS;
+      }
+
+      VisitStatus status = VisitStatus.BYPASS;
+
+      // We consider only cases where both image dimensions are in 'px' format. As '%', 'em'
+      // units are relative to the parent, it is more difficult to infer those values.
+      // Specifically, we consider only the following cases:
+      //   i) style specifies both height and width
+      //   ii) height and width are both specified and style does not specify these attributes.
+      //   iii) height and width are both specified and style overrides one of these.
+      // All other cases are ignored.
+      Integer height = getIntegerPrefix(imgElement.getAttribute("height").trim());
+      Integer width = getIntegerPrefix(imgElement.getAttribute("width").trim());
+      if (null == height || null == width) {
+        height = null;
+        width = null;
+      }
+
+      // Inline style tags trump everything, including inline height/width attribute,
+      // height/width inherited from css.
+      if (!"".equals(imgElement.getAttribute("style"))) {
+        String styleStr = imgElement.getAttribute("style");
+
+        for (String attr : Splitter.on(';').split(styleStr)) {
+          String[] splits = StringUtils.split(attr, ':');
+          if (splits.length != 2) {
+            continue;
+          }
+
+          if ("height".equalsIgnoreCase(splits[0].trim())) {
+            Integer styleHeight = getIntegerPrefix(splits[1].trim());
+            if (null != styleHeight) {
+              height = styleHeight;
+            }
+          }
+
+          if ("width".equalsIgnoreCase(splits[0].trim())) {
+            Integer styleWidth = getIntegerPrefix(splits[1].trim());
+            if (null != styleWidth) {
+              width = styleWidth;
+            }
+          }
+        }
+      }
+
+      if (null != height && null != width) {
+        proxied.setResize(width, height, null, true);
+        List<Uri> updatedUri = proxyUriManager.make(Lists.newArrayList(proxied),
+                                                    featureConfig.getExpires());
+        if (updatedUri.size() == 1) {
+          imgElement.setAttribute("src", updatedUri.get(0).toString());
+          status = VisitStatus.MODIFY;
+        }
+      }
+
+      return status;
+    }
+
+    private Integer getIntegerPrefix(String input) {
+      String integerPrefix = "";
+      if (NumberUtils.isDigits(input)) {
+        integerPrefix = input;
+      } else if (input.endsWith("px") &&
+                 NumberUtils.isDigits(input.substring(0, input.length() - 2))) {
+        integerPrefix = input.substring(0, input.length() - 2);
+      }
+
+      Integer value = null;
+      if (!"".equals(integerPrefix)) {
+        try {
+          value = NumberUtils.createInteger(integerPrefix);
+        } catch (NumberFormatException e) {
+          // ignore
+        }
+      }
+      return value;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java
new file mode 100644
index 0000000..823dcce
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/MutableContent.java
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.w3c.dom.Document;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Object that maintains a String representation of arbitrary contents
+ * and a consistent view of those contents as an HTML parse tree.
+ */
+public class MutableContent {
+  private static final Map<String, Object> EMPTY_MAP = ImmutableMap.of();
+
+  // String representation of contentBytes taking into account the correct
+  // encoding of the content.
+  private String content;
+  private byte[] contentBytes;
+
+  // Encoding of the content bytes. UTF-8 by default.
+  private Charset contentEncoding;
+
+  private HttpResponse contentSource;
+
+  private Document document;
+  private int numChanges = 0;
+  private final GadgetHtmlParser contentParser;
+  private Map<String, Object> pipelinedData;
+
+  private static final String MUTABLE_CONTENT_LISTENER = "MutableContentListener";
+  //class name for logging purpose
+  private static final String classname = MutableContent.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  public static void notifyEdit(Document doc) {
+    MutableContent mc = (MutableContent) doc.getUserData(MUTABLE_CONTENT_LISTENER);
+    if (mc != null) {
+      mc.documentChanged();
+    }
+  }
+
+  /**
+   * Construct with decoded string content
+   */
+  public MutableContent(GadgetHtmlParser contentParser, String content) {
+    this.contentParser = contentParser;
+    this.content = content;
+    this.contentEncoding = Charsets.UTF_8;
+  }
+
+  /**
+   * Construct with HttpResponse so we can defer string decoding until we actually need
+   * the content. Given that we dont rewrite many mime types this is a performance advantage
+   */
+  public MutableContent(GadgetHtmlParser contentParser, HttpResponse contentSource) {
+    this.contentParser = contentParser;
+    this.contentSource = contentSource;
+    this.contentEncoding = contentSource != null ? contentSource.getEncodingCharset() : null;
+  }
+
+  /**
+   * Retrieves the current content for this object in String form.
+   * If content has been retrieved in parse tree form and has
+   * been edited, the String form is computed from the parse tree by
+   * rendering it. It is <b>strongly</b> encouraged to avoid switching
+   * between retrieval of parse tree (through {@code getParseTree}),
+   * with subsequent edits and retrieval of String contents to avoid
+   * repeated serialization and deserialization.
+   * As a final fallback, if content has been set as bytes, interprets
+   * them as a UTF8 String.
+   * @return Renderable/active content.
+   */
+  public String getContent() {
+    if (content == null) {
+      if (contentSource != null) {
+        content = contentSource.getResponseAsString();
+        // Clear on first use
+        contentSource = null;
+      } else if (document != null) {
+        content = HtmlSerialization.serialize(document);
+      } else if (contentBytes != null) {
+        Charset useEncoding = contentEncoding != null ? contentEncoding : Charsets.UTF_8;
+        content = useEncoding.decode(ByteBuffer.wrap(contentBytes)).toString();
+      }
+    }
+    return content;
+  }
+
+  /**
+   * Sets the object's content as a raw String. Note, this operation
+   * may clear the document if the content has changed
+   * @param newContent New content.
+   */
+  public void setContent(String newContent) {
+    // TODO - Equality check may be unnecessary overhead
+    if (content == null || !content.equals(newContent)) {
+      content = newContent;
+      document = null;
+      contentSource = null;
+      contentBytes = null;
+      incrementNumChanges();
+    }
+  }
+
+  /**
+   * Retrieves the current content for this object as an InputStream.
+   * @return Active content as InputStream.
+   */
+  public InputStream getContentBytes() {
+    return new ByteArrayInputStream(getRawContentBytes());
+  }
+
+  protected byte[] getRawContentBytes() {
+    if (contentBytes == null) {
+      if (contentSource != null) {
+        try {
+          setContentBytesState(IOUtils.toByteArray(contentSource.getResponse()),
+              contentSource.getEncodingCharset());
+          contentSource = null;
+        } catch (IOException e) {
+          // Doesn't occur; responseBytes wrapped as a ByteArrayInputStream.
+        }
+      } else if (content != null) {
+        // If retrieving a String here, we've already converted to UTF8.
+        // Be sure to reflect this when setting bytes.
+        // In the case of HttpResponseBuilder, this re-sets charset in Content-Type
+        // to UTF-8 rather than whatever it was before. We do this to standardize
+        // on UTF-8 for all String handling.
+        setContentBytesState(CharsetUtil.getUtf8Bytes(content), Charsets.UTF_8);
+      } else if (document != null) {
+        setContentBytesState(
+            CharsetUtil.getUtf8Bytes(HtmlSerialization.serialize(document)), Charsets.UTF_8);
+      }
+    }
+    return contentBytes;
+  }
+
+  /**
+   * Sets the object's contentBytes as the given raw input. If ever interpreted
+   * as a String, the data will be decoded as the encoding specified.
+   * Note, this operation may clear the document if the content has changed.
+   * Also note, it's mandated that the new bytes array will NOT be modified
+   * by the caller of this API. The array is not copied, for performance reasons.
+   * If the caller may modify a byte array, it MUST pass in a new copy.
+   * @param newBytes New content.
+   */
+  public void setContentBytes(byte[] newBytes, Charset newEncoding) {
+    if (contentBytes == null || !Arrays.equals(contentBytes, newBytes)) {
+      setContentBytesState(newBytes, newEncoding);
+      document = null;
+      contentSource = null;
+      content = null;
+      incrementNumChanges();
+    }
+  }
+
+  /**
+   * Sets content to new byte array, with unspecified charset. It is
+   * recommended to use the {@code setContentBytes(byte[], Charset)} API instead,
+   * where possible.
+   * @param newBytes New content.
+   */
+  public final void setContentBytes(byte[] newBytes) {
+    setContentBytes(newBytes, null);
+  }
+
+  /**
+   * Sets internal state having to do with content bytes, from the provided
+   * byte array and charset.
+   * This MUST be the only place in which MutableContent's notion of encoding is mutated.
+   * @param newBytes New content.
+   * @param newEncoding Encoding for the bytes, or null for unspecified.
+   */
+  protected void setContentBytesState(byte[] newBytes, Charset newEncoding) {
+    contentBytes = newBytes;
+    contentEncoding = newEncoding;
+  }
+
+  /**
+   * Notification that the content of the document has changed. Causes the content
+   * string and bytes to be cleared.
+   */
+  public void documentChanged() {
+    if (document != null) {
+      content = null;
+      contentSource = null;
+      contentBytes = null;
+      incrementNumChanges();
+    }
+  }
+
+  /**
+   * Retrieves the object contents in parsed form, if a
+   * {@code GadgetHtmlParser} is configured and is able to parse the string
+   * contents appropriately. To modify the object's
+   * contents by parse tree after setting new String contents,
+   * this method must be called again. However, this practice is highly
+   * discouraged, as parsing a tree from String is a costly operation and should
+   * be done at most once per rewrite.
+   */
+  public Document getDocument() {
+    // TODO - Consider actually imposing one parse limit on rewriter pipeline
+    if (document != null) {
+      return document;
+    }
+    try {
+      document = contentParser.parseDom(getContent());
+      document.setUserData(MUTABLE_CONTENT_LISTENER, this, null);
+    } catch (GadgetException e) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.logp(Level.WARNING, classname, "getDocument", MessageKeys.EXCEPTION_PARSING_CONTENT);
+        LOG.log(Level.WARNING, e.getMessage(), e);
+      }
+      return null;
+    }
+    return document;
+  }
+
+  public GadgetHtmlParser getContentParser() {
+    return contentParser;
+  }
+
+  public int getNumChanges() {
+    return numChanges;
+  }
+
+  protected void incrementNumChanges() {
+    ++numChanges;
+  }
+
+  /**
+   * True if current state has a parsed document. Allows rewriters to switch mode based on
+   * which content is most readily available
+   */
+  public boolean hasDocument() {
+    return (document != null);
+  }
+
+  public void addPipelinedData(String key, Object value) {
+    if (null == pipelinedData) {
+      pipelinedData = Maps.newHashMap();
+    }
+    pipelinedData.put(key, value);
+  }
+
+  public Map<String, Object> getPipelinedData() {
+    return (null == pipelinedData) ? EMPTY_MAP : pipelinedData;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/OsTemplateXmlLoaderRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/OsTemplateXmlLoaderRewriter.java
new file mode 100644
index 0000000..b95e85b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/OsTemplateXmlLoaderRewriter.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.w3c.dom.Attr;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Rewrites the gadget to include template and xml information
+ * @since 2.0.0
+ */
+public class OsTemplateXmlLoaderRewriter extends DomWalker.Rewriter {
+  public static final String OS_TEMPLATE_MIME = "os/template";
+  public static final String OS_TEMPLATES_FEATURE_NAME = "opensocial-templates";
+  private static final String PRELOAD_TPL = "gadgets.jsondom.preload_('%s',%s);";
+
+  private final Converter converter;
+
+  @Inject
+  public OsTemplateXmlLoaderRewriter(Converter converter) {
+    super(new GadgetHtmlVisitor(converter));
+    this.converter = converter;
+  }
+
+  // Override the HTTP rewrite method to provide custom type checking.
+  // The gadget rewrite method remains standard, using the Visitor pattern.
+  public boolean rewrite(HttpRequest request, HttpResponse original,
+      MutableContent content) throws RewritingException {
+    String mimeType = RewriterUtils.getMimeType(request, original);
+    if (OS_TEMPLATE_MIME.equalsIgnoreCase(mimeType)) {
+      content.setContent(converter.domToJson(content.getContent()));
+      return true;
+    }
+    return false;
+  }
+
+  public static class GadgetHtmlVisitor implements DomWalker.Visitor {
+    private final Converter converter;
+
+    public GadgetHtmlVisitor(Converter converter) {
+      this.converter = converter;
+    }
+
+    public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+      if (node.getNodeType() == Node.ELEMENT_NODE &&
+          "div".equalsIgnoreCase(((Element)node).getTagName()) &&
+          OS_TEMPLATE_MIME.equalsIgnoreCase(((Element)node).getAttribute("type")) &&
+          (!Strings.isNullOrEmpty(((Element) node).getAttribute("id")) ||
+           !Strings.isNullOrEmpty(((Element)node).getAttribute("name")))) {
+        return VisitStatus.RESERVE_NODE;
+      }
+      return VisitStatus.BYPASS;
+    }
+
+    public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+      if (!gadget.getAllFeatures().contains(OS_TEMPLATES_FEATURE_NAME)) {
+        return false;
+      }
+
+      Document doc = nodes.get(0).getOwnerDocument();
+      Element docElem = doc.getDocumentElement();
+      if (docElem == null) {
+        throw new RewritingException("Unexpected error, missing document element",
+            HttpResponse.SC_INTERNAL_SERVER_ERROR);
+      }
+
+      Node head = DomUtil.getFirstNamedChildNode(doc.getDocumentElement(), "head");
+      if (head == null) {
+        throw new RewritingException("Unexpected error, could not find <head> node",
+            HttpResponse.SC_INTERNAL_SERVER_ERROR);
+      }
+
+      StringBuilder preloadScript = new StringBuilder();
+
+      for (Node node : nodes) {
+        Element elem = (Element)node;
+        String value = elem.getTextContent();
+        String id = elem.getAttribute("name");
+        if (Strings.isNullOrEmpty(id)) {
+          id = elem.getAttribute("id");
+        }
+
+        preloadScript.append(String.format(PRELOAD_TPL, id, converter.domToJson(value)));
+      }
+
+      Node script = doc.createElement("script");
+      script.setTextContent(preloadScript.toString());
+      head.appendChild(script);
+
+      return true;
+    }
+  }
+
+  public static class Converter {
+    public static final String NAME_KEY = "n";
+    public static final String VALUE_KEY = "v";
+    public static final String CHILDREN_KEY = "c";
+    public static final String ATTRIBS_KEY = "a";
+    public static final String ERROR_KEY = "e";
+
+    private final GadgetHtmlParser parser;
+    private final DOMImplementation domImpl;
+
+    @Inject
+    public Converter(GadgetHtmlParser parser, DOMImplementation domImpl) {
+      this.parser = parser;
+      this.domImpl = domImpl;
+    }
+
+    public String domToJson(String xml) {
+      try {
+        Document doc = domImpl.createDocument(null, null, null);
+        Element container = doc.createElement("template");
+        parser.parseFragment(xml, container);
+        return jsonFromElement(container).toString();
+      } catch (GadgetException e) {
+        return jsonError("Gadget Exception: " + e).toString();
+      } catch (JSONException e) {
+        return jsonError("JSON Exception: " + e).toString();
+      }
+    }
+
+    public JSONObject jsonFromElement(Element elem) throws JSONException {
+      JSONObject json = new JSONObject();
+      json.put(NAME_KEY, elem.getTagName());
+
+      JSONArray attribs = new JSONArray();
+      NamedNodeMap attribMap = elem.getAttributes();
+      for (int i = 0; i < attribMap.getLength(); ++i) {
+        JSONObject attrib = new JSONObject();
+        Attr domAttrib = (Attr)attribMap.item(i);
+        attrib.put(NAME_KEY, domAttrib.getNodeName());
+        attrib.put(VALUE_KEY, domAttrib.getNodeValue());
+        attribs.put(attrib);
+      }
+      json.put(ATTRIBS_KEY, attribs);
+
+      JSONArray children = new JSONArray();
+      for (Node child = elem.getFirstChild(); child != null; child = child.getNextSibling()) {
+        switch (child.getNodeType()) {
+        case Node.TEXT_NODE:
+          children.put(child.getNodeValue());
+          break;
+        case Node.DOCUMENT_NODE:
+        case Node.ELEMENT_NODE:
+          children.put(jsonFromElement((Element)child));
+          break;
+        default:
+          // No other node types are supported.
+          break;
+        }
+      }
+      json.put(CHILDREN_KEY, children);
+
+      return json;
+    }
+
+    private JSONObject jsonError(String err) {
+      JSONObject json = new JSONObject();
+      try {
+        json.put(ERROR_KEY, err);
+      } catch (JSONException e) {
+        // Doesn't happen.
+      }
+      return json;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataGadgetRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataGadgetRewriter.java
new file mode 100644
index 0000000..d7392b1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/PipelineDataGadgetRewriter.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.apache.shindig.gadgets.preload.PipelineExecutor;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * ContentRewriter that resolves opensocial-data elements on the server.
+ *
+ * This rewriter cannot be used currently without the SocialMarkupHtmlParser.
+ */
+public class PipelineDataGadgetRewriter implements GadgetRewriter {
+
+  //class name for logging purpose
+  private static final String classname = PipelineDataGadgetRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final PipelineExecutor executor;
+
+  @Inject
+  public PipelineDataGadgetRewriter(PipelineExecutor executor) {
+    this.executor = executor;
+  }
+
+  public void rewrite(Gadget gadget, MutableContent content) {
+    // Only bother for gadgets using the opensocial-data feature
+    if (!gadget.getViewFeatures().containsKey("opensocial-data")) {
+      return;
+    }
+
+    Document doc = content.getDocument();
+    Map<PipelinedData, Node> pipelineNodes = parsePipelinedData(gadget, doc);
+
+    if (pipelineNodes.isEmpty()) {
+      return;
+    }
+
+    PipelineExecutor.Results results =
+        executor.execute(gadget.getContext(), pipelineNodes.keySet());
+
+    // Remove all pipeline entries that were fully evaluated
+    for (Map.Entry<PipelinedData, Node> nodeEntry : pipelineNodes.entrySet()) {
+      if (!results.remainingPipelines.contains(nodeEntry.getKey())) {
+        Node node = nodeEntry.getValue();
+        node.getParentNode().removeChild(node);
+        MutableContent.notifyEdit(doc);
+      }
+    }
+
+    // Insert script elements for all the successful results
+    if (!results.keyedResults.isEmpty()) {
+      Element head = (Element) DomUtil.getFirstNamedChildNode(doc.getDocumentElement(), "head");
+      Element pipelineScript = doc.createElement("script");
+      pipelineScript.setAttribute("type", "text/javascript");
+
+      StringBuilder script = new StringBuilder();
+      for (Map.Entry<String, ? extends Object> entry : results.keyedResults.entrySet()) {
+        String key = entry.getKey();
+
+        // TODO: escape key
+        content.addPipelinedData(key, entry.getValue());
+        script.append("opensocial.data.DataContext.putDataSet(\"")
+            .append(key)
+            .append("\",")
+            .append(JsonSerializer.serialize(entry.getValue()))
+            .append(");");
+      }
+
+      pipelineScript.appendChild(doc.createTextNode(script.toString()));
+      head.appendChild(pipelineScript);
+      MutableContent.notifyEdit(doc);
+    }
+
+    // And if no pipelines remain unexecuted, remove the opensocial-data feature
+    if (results.remainingPipelines.isEmpty()) {
+      gadget.addFeature("opensocial-data-context");
+      gadget.removeFeature("opensocial-data");
+    }
+  }
+
+  /**
+   * Parses pipelined data out of a Document.
+   */
+  Map<PipelinedData, Node> parsePipelinedData(Gadget gadget, Document doc) {
+    List<Element> dataTags = SocialDataTags.getTags(doc, SocialDataTags.OSML_DATA_TAG);
+    Map<PipelinedData, Node> pipelineNodes = Maps.newHashMap();
+    for (Element n : dataTags) {
+      try {
+        PipelinedData pipelineData = new PipelinedData(n, gadget.getSpec().getUrl());
+        pipelineNodes.put(pipelineData, n);
+      } catch (SpecParserException e) {
+        // Leave the element to the client
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "parsePipelinedData", MessageKeys.FAILED_TO_PARSE_PRELOAD, new Object[] {gadget.getSpec().getUrl()});
+          LOG.log(Level.INFO, e.getMessage(), e);
+        }
+      }
+    }
+    return pipelineNodes;
+  }
+
+  static class PipelineState {
+    public Node node;
+    public PipelinedData.Batch batch;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ProxyingContentRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ProxyingContentRewriter.java
new file mode 100644
index 0000000..badf59e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ProxyingContentRewriter.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor;
+import org.apache.shindig.gadgets.uri.ConcatUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import java.util.List;
+
+/**
+ * Rewrites gadget content to force resources through the /proxy endpoint.
+ *
+ * @since 2.0.0
+ */
+public class ProxyingContentRewriter extends DomWalker.Rewriter {
+  private final ContentRewriterFeature.Factory featureConfigFactory;
+  private final ProxyUriManager proxyUriManager;
+  private final ConcatUriManager concatUriManager;
+
+  @Inject
+  public ProxyingContentRewriter(ContentRewriterFeature.Factory featureConfigFactory,
+      ProxyUriManager proxyUriManager, ConcatUriManager concatUriManager) {
+    this.featureConfigFactory = featureConfigFactory;
+    this.proxyUriManager = proxyUriManager;
+    this.concatUriManager = concatUriManager;
+  }
+
+  @Override
+  protected List<Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+    ContentRewriterFeature.Config config = featureConfigFactory.get(context.getSpec());
+    // Note that concat is including with proxy in order to prevent
+    // proxying the rewritten concat url
+    // Basically Url rewritters should all be in one dom walker.
+    return ImmutableList.of(
+        new ConcatVisitor.Js(config, concatUriManager),
+        new ConcatVisitor.Css(config, concatUriManager),
+        new ProxyingVisitor(config, proxyUriManager,
+                            ProxyingVisitor.Tags.SCRIPT,
+                            ProxyingVisitor.Tags.STYLESHEET,
+                            ProxyingVisitor.Tags.EMBEDDED_IMAGES));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ProxyingVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ProxyingVisitor.java
new file mode 100644
index 0000000..448afd2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ProxyingVisitor.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.Lists;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.Uri.UriException;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Simple visitor that, when plugged into a DomWalker, rewrites
+ * resource links to proxied versions of the same.
+ *
+ * @since 2.0.0
+ */
+public class ProxyingVisitor extends ResourceMutateVisitor {
+  //class name for logging purpose
+  private static final String classname = ProxyingVisitor.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private final ProxyUriManager uriManager;
+
+  public ProxyingVisitor(ContentRewriterFeature.Config featureConfig,
+                         ProxyUriManager uriManager,
+                         Tags... resourceTags) {
+    super(featureConfig, resourceTags);
+    this.uriManager = uriManager;
+  }
+
+  @Override
+  protected Collection<Pair<Node, Uri>> mutateUris(Gadget gadget, Collection<Node> nodes) {
+    List<ProxyUriManager.ProxyUri> reservedUris =
+        Lists.newArrayListWithCapacity(nodes.size());
+    List<Node> reservedNodes = Lists.newArrayListWithCapacity(nodes.size());
+
+    for (Node node : nodes) {
+      Element element = (Element)node;
+      String nodeName = node.getNodeName().toLowerCase();
+      String uriStr = element.getAttribute(resourceTags.get(nodeName)).trim();
+      try {
+        ProxyUriManager.ProxyUri proxiedUri = new ProxyUriManager.ProxyUri(
+            gadget, Uri.parse(uriStr));
+
+        // Set the html tag context as the current node being processed.
+        proxiedUri.setHtmlTagContext(nodeName);
+        reservedUris.add(proxiedUri);
+        reservedNodes.add(node);
+      } catch (UriException e) {
+        // Uri parse exception, ignore.
+        if (LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, classname, "mutateUris", MessageKeys.URI_EXCEPTION_PARSING, new Object[] {uriStr});
+          LOG.log(Level.WARNING, e.getMessage(), e);
+        }
+      }
+    }
+
+    List<Uri> resourceUris = uriManager.make(reservedUris, featureConfig.getExpires());
+
+    // By contract, resourceUris matches by index with inbound Uris. Create an easy-access
+    // List with the results.
+    List<Pair<Node, Uri>> proxiedUris = Lists.newArrayListWithCapacity(nodes.size());
+
+    Iterator<Uri> uriIt = resourceUris.iterator();
+    for (Node node : reservedNodes) {
+      proxiedUris.add(Pair.of(node, uriIt.next()));
+    }
+
+    return proxiedUris;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResourceMutateVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResourceMutateVisitor.java
new file mode 100644
index 0000000..9672850
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResourceMutateVisitor.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Abstract visitor that walks over html tags as specified by
+ * {@code resourceTags} and prepares list of html tag nodes whose uri
+ * attributes can be mutated.
+ * Implementations can override {@link #mutateUris} for uses cases like
+ * proxying resources, making url's absolute, prefetching images etc.
+ *
+ * TODO: Refactor AbsolutePathReferenceVisitor to extend ResourceMutateVisitor.
+ *
+ * @since 2.0.0
+ */
+public abstract class ResourceMutateVisitor implements DomWalker.Visitor {
+  /**
+   * Enum for resource tags and associated attributes that should be mutated.
+   */
+  public enum Tags {
+    // Javascript resources requested by the current page.
+    SCRIPT(ImmutableMap.of("script", "src")),
+
+    // Css stylesheet resources requested by the current page.
+    STYLESHEET(ImmutableMap.of("link", "href")),
+
+    // Other embedded resources requested on the same page.
+    EMBEDDED_IMAGES(ImmutableMap.of("body", "background",
+                                    "img", "src",
+                                    "input", "src")),
+
+    // All resources that possibly be rewritten. Useful for testing.
+    ALL_RESOURCES(ImmutableMap.<String, String>builder()
+        .putAll(SCRIPT.getResourceTags())
+        .putAll(STYLESHEET.getResourceTags())
+        .putAll(EMBEDDED_IMAGES.getResourceTags())
+        .build());
+
+    private Map<String, String> resourceTags;
+    private Tags(Map<String, String> resourceTags) {
+      this.resourceTags = resourceTags;
+    }
+
+    public Map<String, String> getResourceTags() {
+      return resourceTags;
+    }
+  }
+
+  // Map of tag name to attribute of resources to rewrite.
+  protected final Map<String, String> resourceTags;
+  protected final ContentRewriterFeature.Config featureConfig;
+
+  public ResourceMutateVisitor(ContentRewriterFeature.Config featureConfig,
+                               Tags... resourceTags) {
+    this.featureConfig = featureConfig;
+
+    Map<String, String> rTags = Maps.newHashMap();
+    for (Tags r : resourceTags) {
+      rTags.putAll(r.getResourceTags());
+    }
+    this.resourceTags = ImmutableMap.<String, String>builder().putAll(rTags).build();
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    String nodeName = node.getNodeName().toLowerCase();
+    if (node.getNodeType() == Node.ELEMENT_NODE &&
+        resourceTags.containsKey(nodeName) &&
+        featureConfig.shouldRewriteTag(nodeName)) {
+      if ("link".equals(nodeName)) {
+        // Rewrite link only when it is for css.
+        String type = ((Element)node).getAttribute("type");
+        String rel = ((Element)node).getAttribute("rel");
+        if (!"stylesheet".equalsIgnoreCase(rel) || !"text/css".equalsIgnoreCase(type)) {
+          return VisitStatus.BYPASS;
+        }
+      }
+
+      Attr attr = (Attr) node.getAttributes().getNamedItem(
+          resourceTags.get(nodeName));
+      if (attr != null) {
+        String urlValue = attr.getValue();
+        if (!Strings.isNullOrEmpty(urlValue) && featureConfig.shouldRewriteURL(urlValue)) {
+          return VisitStatus.RESERVE_NODE;
+        }
+      }
+    }
+    return VisitStatus.BYPASS;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+    Collection<Pair<Node, Uri>> proxiedUris = mutateUris(gadget, nodes);
+
+    boolean mutated = false;
+    for (Pair<Node, Uri> proxyPair : proxiedUris) {
+      if (proxyPair.two == null) {
+        continue;
+      }
+      Element element = (Element) proxyPair.one;
+      String nodeName = element.getNodeName().toLowerCase();
+      element.setAttribute(resourceTags.get(nodeName), proxyPair.two.toString());
+      mutated = true;
+    }
+
+    return mutated;
+  }
+
+  // Mutate the list of nodes reserved by revisit().
+  protected abstract Collection<Pair<Node, Uri>> mutateUris(Gadget gadget, Collection<Node> nodes);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriter.java
new file mode 100644
index 0000000..ee9b708
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriter.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+/**
+ * Base interface used by response rewriter implementations.
+ *
+ * @since 2.0.0
+ */
+public interface ResponseRewriter {
+
+  /**
+   * Rewrites the response.
+   * @param request  The request that was made.
+   * @param response The response generated as a result of the request.
+   * @param gadget The gadget that made the request.  This parameter may be null.
+   * @throws RewritingException Thrown if something went wrong when rewriting the response.
+   */
+  public void rewrite(HttpRequest request, HttpResponseBuilder response, @Nullable Gadget gadget)
+          throws RewritingException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriterList.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriterList.java
new file mode 100644
index 0000000..753f813
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriterList.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.BindingAnnotation;
+
+import org.apache.shindig.config.ContainerConfig;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that specifies a list of rewriters with the rewriteFlow and
+ * container they are meant to be applied to.
+ */
+@BindingAnnotation
+@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ResponseRewriterList {
+
+  // Enum of rewrite flows being used.
+  public enum RewriteFlow {
+    DEFAULT,
+    REQUEST_PIPELINE,
+    ACCELERATE,
+    DUMMY_FLOW
+  }
+
+  // The flow id signifying what type of rewriting is done.
+  RewriteFlow rewriteFlow();
+
+  // The container context.
+  String container() default ContainerConfig.DEFAULT_CONTAINER;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriterRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriterRegistry.java
new file mode 100644
index 0000000..9fe9808
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ResponseRewriterRegistry.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Performs rewriting operations by invoking one or more {@link org.apache.shindig.gadgets.rewrite.ResponseRewriter}s.
+ *
+ * @since 2.0.0
+ */
+@ImplementedBy(DefaultResponseRewriterRegistry.class)
+public interface ResponseRewriterRegistry {
+
+  /**
+   * Rewrites an {@code HttpResponse} object with the given request as context,
+   * using the registered rewriters.
+   * @param req Request object for context.
+   * @param resp Original response object.
+   * @param gadget Gadget that may have been making the request.
+   * @return Rewritten response object, or resp if not modified.
+   * @throws RewritingException In case of errors.
+   */
+  HttpResponse rewriteHttpResponse(HttpRequest req, HttpResponse resp, Gadget gadget)
+    throws RewritingException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriteModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriteModule.java
new file mode 100644
index 0000000..a03e516
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriteModule.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.AbstractModule;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.multibindings.MapBinder;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.render.CajaResponseRewriter;
+import org.apache.shindig.gadgets.render.OpenSocialI18NGadgetRewriter;
+import org.apache.shindig.gadgets.render.RenderingGadgetRewriter;
+import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
+import org.apache.shindig.gadgets.render.SanitizingResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.image.BasicImageRewriter;
+import org.apache.shindig.gadgets.servlet.CajaContentRewriter;
+import org.apache.shindig.gadgets.uri.AccelUriManager;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Guice bindings for the rewrite package.
+ */
+public class RewriteModule extends AbstractModule {
+  public static final String ACCEL_CONTAINER = AccelUriManager.CONTAINER;
+  public static final String DEFAULT_CONTAINER = ContainerConfig.DEFAULT_CONTAINER;
+
+  // Mapbinder for the map from
+  // RewritePath -> [ List of response rewriters ].
+  protected MapBinder<RewritePath, List<ResponseRewriter>> mapbinder;
+
+  @Override
+  protected void configure() {
+    configureGadgetRewriters();
+    provideResponseRewriters();
+  }
+
+  protected void provideResponseRewriters() {
+    mapbinder = MapBinder.newMapBinder(binder(), new TypeLiteral<RewritePath>(){},
+                                       new TypeLiteral<List<ResponseRewriter>>() {});
+
+    Provider<List<ResponseRewriter>> accelRewriterList = getResponseRewriters(
+        ACCEL_CONTAINER, RewriteFlow.ACCELERATE);
+    Provider<List<ResponseRewriter>> requestPipelineRewriterList = getResponseRewriters(
+        DEFAULT_CONTAINER, RewriteFlow.REQUEST_PIPELINE);
+
+    addBindingForRewritePath(DEFAULT_CONTAINER, RewriteFlow.REQUEST_PIPELINE);
+    addBindingForRewritePath(DEFAULT_CONTAINER, RewriteFlow.DEFAULT);
+    addBindingForRewritePath(ACCEL_CONTAINER, RewriteFlow.ACCELERATE);
+    addBindingForRewritePath(ACCEL_CONTAINER, RewriteFlow.REQUEST_PIPELINE,
+                             requestPipelineRewriterList);
+    addBindingForRewritePath(ACCEL_CONTAINER, RewriteFlow.DEFAULT, accelRewriterList);
+  }
+
+  protected void addBindingForRewritePath(String container, RewriteFlow rewriteFlow,
+                                          Provider<List<ResponseRewriter>> list) {
+    RewritePath rewritePath = new RewritePath(container, rewriteFlow);
+    mapbinder.addBinding(rewritePath).toProvider(list);
+  }
+
+  protected void addBindingForRewritePath(String container, RewriteFlow rewriteFlow) {
+    addBindingForRewritePath(container, rewriteFlow, binder().getProvider(
+        getKey(container, rewriteFlow)));
+  }
+
+  protected Provider<List<ResponseRewriter>> getResponseRewriters(String container,
+                                                                  RewriteFlow flow) {
+    return binder().getProvider(getKey(container, flow));
+  }
+
+  protected Key<List<ResponseRewriter>> getKey(String container, RewriteFlow flow) {
+    return Key.get(new TypeLiteral<List<ResponseRewriter>>() {},
+                   new RewritePath(container, flow));
+  }
+
+
+  // Provides ResponseRewriterRegistry for DEFAULT flow.
+  @Provides
+  @Singleton
+  @RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT)
+  public ResponseRewriterRegistry provideDefaultList(GadgetHtmlParser parser,
+      Provider<Map<RewritePath, Provider<List<ResponseRewriter>>>> rewritePathToRewriterList) {
+    return new ContextAwareRegistry(parser, RewriteFlow.DEFAULT,
+                                    rewritePathToRewriterList);
+  }
+
+  // Provides ResponseRewriterRegistry for REQUEST_PIPELINE flow.
+  @Provides
+  @Singleton
+  @RewriterRegistry(rewriteFlow = RewriteFlow.REQUEST_PIPELINE)
+  public ResponseRewriterRegistry provideRequestPipelineList(GadgetHtmlParser parser,
+      Provider<Map<RewritePath, Provider<List<ResponseRewriter>>>> rewritePathToRewriterList) {
+    return new ContextAwareRegistry(parser, RewriteFlow.REQUEST_PIPELINE,
+                                    rewritePathToRewriterList);
+  }
+
+  // Provides ResponseRewriterRegistry for ACCELERATE flow.
+  @Provides
+  @Singleton
+  @RewriterRegistry(rewriteFlow = RewriteFlow.ACCELERATE)
+  public ResponseRewriterRegistry provideAccelerateList(GadgetHtmlParser parser,
+      Provider<Map<RewritePath, Provider<List<ResponseRewriter>>>> rewritePathToRewriterList) {
+    return new ContextAwareRegistry(parser, RewriteFlow.ACCELERATE,
+                                    rewritePathToRewriterList);
+  }
+
+  protected void configureGadgetRewriters() {
+    Multibinder<GadgetRewriter> multibinder = Multibinder.newSetBinder(binder(),
+        GadgetRewriter.class, Names.named("shindig.rewriters.gadget.set"));
+    multibinder.addBinding().to(PipelineDataGadgetRewriter.class);
+    multibinder.addBinding().to(TemplateRewriter.class);
+    multibinder.addBinding().to(AbsolutePathReferenceRewriter.class);
+    multibinder.addBinding().to(StyleTagExtractorContentRewriter.class);
+    multibinder.addBinding().to(StyleAdjacencyContentRewriter.class);
+    multibinder.addBinding().to(ProxyingContentRewriter.class);
+    multibinder.addBinding().to(CajaContentRewriter.class);
+    multibinder.addBinding().to(SanitizingGadgetRewriter.class);
+    multibinder.addBinding().to(RenderingGadgetRewriter.class);
+    multibinder.addBinding().to(OpenSocialI18NGadgetRewriter.class);
+  }
+
+  @Provides
+  @Singleton
+  @Named("shindig.rewriters.gadget")
+  protected List<GadgetRewriter> provideGadgetRewriters(
+      @Named("shindig.rewriters.gadget.set") Set<GadgetRewriter> gadgetRewritersSet) {
+    // Multibinding promise order within a binding module
+    return ImmutableList.copyOf(gadgetRewritersSet);
+  }
+
+  @Provides
+  @Singleton
+  @Named("shindig.rewriters.accelerate")
+  protected List<GadgetRewriter> provideAccelRewriters(
+      ProxyingContentRewriter proxyingContentRewriter,
+      CajaContentRewriter cajaRewriter) {
+    return ImmutableList.of(proxyingContentRewriter, cajaRewriter);
+  }
+
+  // Provides the list of rewriters to be applied for REQUEST_PIPELINE flow.
+  @Provides
+  @Singleton
+  @ResponseRewriterList(rewriteFlow = RewriteFlow.REQUEST_PIPELINE)
+  protected List<ResponseRewriter> providePreCacheResponseRewriters(
+      BasicImageRewriter imageRewriter) {
+    return ImmutableList.<ResponseRewriter>of(imageRewriter);
+  }
+
+  // Provides the list of rewriters to be applied for DEFAULT flow.
+  @Provides
+  @Singleton
+  @ResponseRewriterList(rewriteFlow = RewriteFlow.DEFAULT)
+  protected List<ResponseRewriter> provideDefaultRewriters(
+      AbsolutePathReferenceRewriter absolutePathRewriter,
+      StyleTagExtractorContentRewriter styleTagExtractorRewriter,
+      StyleAdjacencyContentRewriter styleAdjacencyRewriter,
+      ProxyingContentRewriter proxyingRewriter,
+      CssResponseRewriter cssRewriter,
+      SanitizingResponseRewriter sanitizedRewriter,
+      CajaResponseRewriter cajaRewriter) {
+    return ImmutableList.of(
+        absolutePathRewriter, styleTagExtractorRewriter, styleAdjacencyRewriter, proxyingRewriter,
+        cssRewriter, sanitizedRewriter, cajaRewriter);
+  }
+
+  // Provides the list of rewriters to be applied for ACCELERATE flow for
+  // accel container.
+  @Provides
+  @Singleton
+  @ResponseRewriterList(rewriteFlow = RewriteFlow.ACCELERATE,
+                        container = AccelUriManager.CONTAINER)
+  protected List<ResponseRewriter> provideAccelResponseRewriters(
+      AbsolutePathReferenceRewriter absolutePathReferenceRewriter,
+      StyleTagProxyEmbeddedUrlsRewriter styleTagProxyEmbeddedUrlsRewriter,
+      ProxyingContentRewriter proxyingContentRewriter) {
+    return ImmutableList.<ResponseRewriter>of(
+        absolutePathReferenceRewriter,
+        styleTagProxyEmbeddedUrlsRewriter,
+        proxyingContentRewriter);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewritePath.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewritePath.java
new file mode 100644
index 0000000..918e4aa
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewritePath.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * Implementation of ResponseRewriterList annotation interface.
+ */
+public class RewritePath implements ResponseRewriterList {
+  protected final String container;
+  protected final RewriteFlow rewriteFlow;
+
+  public RewritePath(String container, RewriteFlow rewriteFlow) {
+    this.container = container;
+    this.rewriteFlow = rewriteFlow;
+  }
+
+  public RewriteFlow rewriteFlow() {
+    return this.rewriteFlow;
+  }
+
+  public String container() {
+    return container;
+  }
+
+  public int hashCode() {
+    // This is specified in java.lang.Annotation.
+    return ((127* "container".hashCode()) ^ container.hashCode()) +
+           ((127 * "rewriteFlow".hashCode()) ^ rewriteFlow.hashCode());
+  }
+
+  public boolean equals(Object o) {
+    if (!(o instanceof ResponseRewriterList)) {
+      return false;
+    }
+    ResponseRewriterList other = (ResponseRewriterList) o;
+    return rewriteFlow.equals(other.rewriteFlow()) && container.equals(other.container());
+  }
+
+  public String toString() {
+    return '@' + ResponseRewriterList.class.getName()
+           + "(rewriteFlow=" + rewriteFlow + ','
+           + "container=" + container + ')';
+  }
+
+  public Class<? extends Annotation> annotationType() {
+    return ResponseRewriterList.class;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriterRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriterRegistry.java
new file mode 100644
index 0000000..feff767
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriterRegistry.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.BindingAnnotation;
+
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation that specifies an instance of ResponseRewriterRegistry to be used
+ * for a given rewriteFlow.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
+@BindingAnnotation
+public @interface RewriterRegistry {
+  // The flow id signifying what type of rewriting is done.
+  RewriteFlow rewriteFlow();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriterUtils.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriterUtils.java
new file mode 100644
index 0000000..1313a05
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewriterUtils.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.uri.UriCommon;
+
+// Temporary replacement of javax.annotation.Nullable
+import org.apache.shindig.common.Nullable;
+
+/**
+ * Various utility functions used by rewriters
+ */
+public final class RewriterUtils {
+  private RewriterUtils() {}
+
+  public static boolean isHtml(HttpRequest request, HttpResponse original) {
+    String mimeType = getMimeType(request, original);
+    return mimeType != null && (mimeType.contains("html"));
+  }
+
+  public static boolean isHtml(HttpRequest request, HttpResponseBuilder original) {
+    String mimeType = getMimeType(request, original);
+    return mimeType != null && (mimeType.contains("html")) &&
+           maybeAcceptHtml(request.getParam(UriCommon.Param.HTML_TAG_CONTEXT.getKey()));
+  }
+
+  /**
+   * Returns true if the given html tag can accept text/html data. For now,
+   * all html tags other than "script" are treated as html accepting tags.
+   *
+   * @param htmlTagName The html tag in question.
+   * @return True if {@code htmlTagName} accepts text/html data, false
+   *   otherwise.
+   */
+  public static boolean maybeAcceptHtml(@Nullable String htmlTagName) {
+    return !"script".equalsIgnoreCase(htmlTagName);
+  }
+
+  public static boolean isCss(HttpRequest request, HttpResponse original) {
+    String mimeType = getMimeType(request, original);
+    return mimeType != null && mimeType.contains("css");
+  }
+
+  public static boolean isCss(HttpRequest request, HttpResponseBuilder original) {
+    String mimeType = getMimeType(request, original);
+    return mimeType != null && mimeType.contains("css");
+  }
+
+  // TODO: Also check if the HTML_TAG_CONTEXT is script.
+  public static boolean isJavascript(HttpRequest request, HttpResponse original) {
+    String mimeType = getMimeType(request, original);
+    return mimeType != null && mimeType.contains("javascript");
+  }
+
+  public static boolean isJavascript(HttpRequest request, HttpResponseBuilder original) {
+    String mimeType = getMimeType(request, original);
+    return mimeType != null && mimeType.contains("javascript");
+  }
+
+  public static String getMimeType(HttpRequest request, HttpResponse original) {
+    String mimeType = request.getRewriteMimeType();
+    if (mimeType == null) {
+      mimeType = original.getHeader("Content-Type");
+    }
+    return mimeType != null ? mimeType.toLowerCase() : null;
+  }
+
+  public static String getMimeType(HttpRequest request, HttpResponseBuilder original) {
+    String mimeType = request.getRewriteMimeType();
+    if (mimeType == null) {
+      mimeType = original.getHeader("Content-Type");
+    }
+    return mimeType != null ? mimeType.toLowerCase() : null;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewritingException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewritingException.java
new file mode 100644
index 0000000..6bd30e5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/RewritingException.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+/**
+ * Exceptions thrown during content rewriting.
+ *
+ * These exceptions will usually translate directly into an end-user error message, so they should
+ * be easily localizable.
+ */
+public class RewritingException extends Exception {
+  private final int httpStatusCode;
+
+  public RewritingException(Throwable t, int httpStatusCode) {
+    super(t);
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public RewritingException(String message, int httpStatusCode) {
+    super(message);
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public RewritingException(String message, Throwable t, int httpStatusCode) {
+    super(message, t);
+    this.httpStatusCode = httpStatusCode;
+  }
+
+  public int getHttpStatusCode() {
+    return httpStatusCode;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ScriptConcatContentRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ScriptConcatContentRewriter.java
new file mode 100644
index 0000000..c01dd6f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/ScriptConcatContentRewriter.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpCache;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor;
+import org.apache.shindig.gadgets.uri.ConcatUriManager;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Concatenates non-private and cached script resources.
+ * CacheEnforcementVisitor is used to rewrite only non-private & cached scripts.
+ * @since 2.0.0
+ */
+public class ScriptConcatContentRewriter extends DomWalker.Rewriter {
+  private final ContentRewriterFeature.Factory featureConfigFactory;
+  private final ConcatUriManager concatUriManager;
+  private final Executor executor;
+  private final RequestPipeline requestPipeline;
+  private final HttpCache cache;
+
+  @Inject
+  public ScriptConcatContentRewriter(ConcatUriManager concatUriManager,
+                                     ContentRewriterFeature.Factory featureConfigFactory,
+                                     @Named("shindig.concat.executor") Executor executor,
+                                     HttpCache cache,
+                                     RequestPipeline requestPipeline) {
+    this.concatUriManager = concatUriManager;
+    this.featureConfigFactory = featureConfigFactory;
+    this.executor = executor;
+    this.cache = cache;
+    this.requestPipeline = requestPipeline;
+  }
+
+  @Override
+  protected List<Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+    ContentRewriterFeature.Config config = featureConfigFactory.get(context.getSpec());
+    return Arrays.asList(
+        new CacheEnforcementVisitor(config, executor, cache, requestPipeline,
+            CacheEnforcementVisitor.Tags.SCRIPT),
+        new ConcatVisitor.Js(config, concatUriManager));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyContentRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyContentRewriter.java
new file mode 100644
index 0000000..b93b845
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyContentRewriter.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+/**
+ * Merges adjacent style tags.
+ *
+ * @since 2.0.0
+ */
+public class StyleAdjacencyContentRewriter extends DomWalker.Rewriter {
+  public StyleAdjacencyContentRewriter() {
+    super(new StyleAdjacencyVisitor());
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyVisitor.java
new file mode 100644
index 0000000..a603e41
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyVisitor.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Objects;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Visitor that pulls all stylesheet nodes in a document to head, in
+ * the order they were found in the document. This maintains CSS semantics
+ * in all but the most pathological (JS manipulating CSS through stylesheets
+ * in an order-dependent way) cases while reducing browser reflows and making
+ * CSS concatenated-proxying more likely.
+ *
+ * @since 2.0.0
+ */
+public class StyleAdjacencyVisitor implements Visitor {
+
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    if (node.getNodeType() == Node.ELEMENT_NODE &&
+        ("style".equalsIgnoreCase(node.getNodeName()) ||
+         ("link".equalsIgnoreCase(node.getNodeName()) &&
+          ("stylesheet".equalsIgnoreCase(getAttrib(node, "rel")) ||
+           ("text/css".equalsIgnoreCase(getAttrib(node, "type"))))))) {
+      // Reserve <style...>, <link rel="stylesheet"...>, or <link type="text/css"...>
+      return VisitStatus.RESERVE_TREE;
+    }
+
+    return VisitStatus.BYPASS;
+  }
+
+  public boolean revisit(Gadget gadget, List<Node> nodes)
+      throws RewritingException {
+    Node head = DomUtil.getFirstNamedChildNode(
+        nodes.get(0).getOwnerDocument().getDocumentElement(), "head");
+
+    if (head == null) {
+      // Should never occur; do for paranoia's sake.
+      return false;
+    }
+
+    // Detach nodes
+    for (Node n : nodes) {
+      n.getParentNode().removeChild(n);
+    }
+
+    // Add nodes back to DOM
+    if (head.getFirstChild() == null) {
+      // add each node to head
+      for (Node n : nodes) {
+        head.appendChild(n);
+      }
+    } else {
+      // existing nodes in head, inject all nodes before the first one
+      Node firstChild = head.getFirstChild();
+      for (Node n : nodes) {
+        head.insertBefore(n, firstChild);
+      }
+    }
+
+    return true;
+  }
+
+  private String getAttrib(Node node, String key) {
+    String value = null;
+    NamedNodeMap attribs = node.getAttributes();
+    for (int i = 0; i < attribs.getLength(); ++i) {
+      Attr attr = (Attr)attribs.item(i);
+      if (key.equalsIgnoreCase(attr.getName())) {
+        value = attr.getValue();
+        break;
+      }
+    }
+    return Objects.firstNonNull(value, "");
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleConcatContentRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleConcatContentRewriter.java
new file mode 100644
index 0000000..8481be7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleConcatContentRewriter.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpCache;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor;
+import org.apache.shindig.gadgets.uri.ConcatUriManager;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Concatenates non-private and cached adjacent styles
+ * CacheEnforcementVisitor is used to merge only non-private & cached styles.
+ * @since 2.0.0
+ */
+public class StyleConcatContentRewriter extends DomWalker.Rewriter {
+  private final ContentRewriterFeature.Factory featureConfigFactory;
+  private final ConcatUriManager concatUriManager;
+  private final Executor executor;
+  private final RequestPipeline requestPipeline;
+  private final HttpCache cache;
+
+  @Inject
+  public StyleConcatContentRewriter(ConcatUriManager concatUriManager,
+                                    ContentRewriterFeature.Factory featureConfigFactory,
+                                    @Named("shindig.concat.executor") Executor executor,
+                                    HttpCache cache,
+                                    RequestPipeline requestPipeline) {
+    this.concatUriManager = concatUriManager;
+    this.featureConfigFactory = featureConfigFactory;
+    this.executor = executor;
+    this.cache = cache;
+    this.requestPipeline = requestPipeline;
+  }
+
+  @Override
+  protected List<Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+    ContentRewriterFeature.Config config = featureConfigFactory.get(context.getSpec());
+    return Arrays.asList(
+        new CacheEnforcementVisitor(config, executor, cache, requestPipeline,
+            CacheEnforcementVisitor.Tags.STYLESHEET),
+        new ConcatVisitor.Css(config, concatUriManager));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorContentRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorContentRewriter.java
new file mode 100644
index 0000000..953a8b0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorContentRewriter.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Extracts style tags
+ *
+ * @since 2.0.0
+ */
+public class StyleTagExtractorContentRewriter extends DomWalker.Rewriter {
+  private final ContentRewriterFeature.Factory featureConfigFactory;
+  private final ProxyUriManager proxyUriManager;
+  private final CssResponseRewriter cssRewriter;
+
+  @Inject
+  public StyleTagExtractorContentRewriter(ContentRewriterFeature.Factory featureConfigFactory,
+      ProxyUriManager proxyUriManager, CssResponseRewriter cssRewriter) {
+    this.featureConfigFactory = featureConfigFactory;
+    this.proxyUriManager = proxyUriManager;
+    this.cssRewriter = cssRewriter;
+  }
+
+  @Override
+  protected List<Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+    ContentRewriterFeature.Config config = featureConfigFactory.get(context.getSpec());
+    return Arrays.<Visitor>asList(
+        new StyleTagExtractorVisitor(config, cssRewriter, proxyUriManager));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorVisitor.java
new file mode 100644
index 0000000..214ab2b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorVisitor.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import java.util.List;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Visits nodes in the dom extracting style tags.
+ * @since 2.0.0
+ */
+public class StyleTagExtractorVisitor implements Visitor {
+  private final ContentRewriterFeature.Config config;
+  private final CssResponseRewriter cssRewriter;
+  private final ProxyUriManager proxyUriManager;
+
+  public StyleTagExtractorVisitor(ContentRewriterFeature.Config config,
+      CssResponseRewriter cssRewriter, ProxyUriManager proxyUriManager) {
+    this.config = config;
+    this.cssRewriter = cssRewriter;
+    this.proxyUriManager = proxyUriManager;
+  }
+
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    if (!config.isRewriteEnabled() || !config.getIncludedTags().contains("style")) {
+      return VisitStatus.BYPASS;
+    }
+
+    // Only process <style> elements.
+    if (node.getNodeType() != Node.ELEMENT_NODE ||
+        !node.getNodeName().equalsIgnoreCase("style")) {
+      return VisitStatus.BYPASS;
+    }
+
+    return VisitStatus.RESERVE_NODE;
+  }
+
+  public boolean revisit(Gadget gadget, List<Node> nodes)
+      throws RewritingException {
+    boolean mutated = false;
+    if (nodes.isEmpty()) {
+      return mutated;
+    }
+
+    Uri contentBase = gadget.getSpec().getUrl();
+    View view = gadget.getCurrentView();
+    if (view != null && view.getHref() != null) {
+      contentBase = view.getHref();
+    }
+
+    Element head = (Element)DomUtil.getFirstNamedChildNode(
+        nodes.get(0).getOwnerDocument().getDocumentElement(), "head");
+    for (Node node : nodes) {
+      // Guaranteed safe cast due to reservation logic.
+      Element elem = (Element)node;
+      List<String> extractedUrls = cssRewriter.rewrite(
+          elem, contentBase, CssResponseRewriter.uriMaker(proxyUriManager, config), true, gadget.getContext());
+      for (String extractedUrl : extractedUrls) {
+        // Add extracted urls as link elements to head
+        Element newLink = head.getOwnerDocument().createElement("link");
+        newLink.setAttribute("rel", "stylesheet");
+        newLink.setAttribute("type", "text/css");
+        newLink.setAttribute("href", extractedUrl);
+        head.appendChild(newLink);
+        mutated = true;
+      }
+    }
+
+    return mutated;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsRewriter.java
new file mode 100644
index 0000000..695c2df
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsRewriter.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Rewriter that replaces urls (@import + background) in
+ * &lt;style&gt; ... &lt;/style&gt; with their proxied versions.
+ *
+ * @since 2.0.0
+ */
+public class StyleTagProxyEmbeddedUrlsRewriter extends DomWalker.Rewriter {
+  protected final ContentRewriterFeature.Config config;
+  protected final ProxyUriManager proxyUriManager;
+  protected final CssResponseRewriter cssRewriter;
+
+  @Inject
+  public StyleTagProxyEmbeddedUrlsRewriter(ContentRewriterFeature.DefaultConfig config,
+                                           ProxyUriManager proxyUriManager,
+                                           CssResponseRewriter cssRewriter) {
+    this.config = config;
+    this.proxyUriManager = proxyUriManager;
+    this.cssRewriter = cssRewriter;
+  }
+
+  @Override
+  protected List<DomWalker.Visitor> makeVisitors(Gadget context, Uri gadgetUri) {
+    return Arrays.<DomWalker.Visitor>asList(
+        new StyleTagProxyEmbeddedUrlsVisitor(config, proxyUriManager,
+                                             cssRewriter));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsVisitor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsVisitor.java
new file mode 100644
index 0000000..f8cc78a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsVisitor.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.inject.Inject;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+
+/**
+ * Visitor that replaces urls (@import + background) in
+ * &lt;style&gt; ... &lt;/style&gt; with their proxied versions.
+ *
+ * @since 2.0.0
+ */
+public class StyleTagProxyEmbeddedUrlsVisitor implements DomWalker.Visitor {
+  protected final ContentRewriterFeature.Config config;
+  protected final ProxyUriManager proxyUriManager;
+  protected final CssResponseRewriter cssRewriter;
+
+  @Inject
+  public StyleTagProxyEmbeddedUrlsVisitor(ContentRewriterFeature.Config config,
+                                          ProxyUriManager proxyUriManager,
+                                          CssResponseRewriter cssRewriter) {
+    this.config = config;
+    this.proxyUriManager = proxyUriManager;
+    this.cssRewriter = cssRewriter;
+  }
+
+  public VisitStatus visit(Gadget gadget, Node node) throws RewritingException {
+    // Only process <style> elements.
+    if (node.getNodeType() != Node.ELEMENT_NODE ||
+        !node.getNodeName().equalsIgnoreCase("style")) {
+      return VisitStatus.BYPASS;
+    }
+
+    return VisitStatus.RESERVE_NODE;
+  }
+
+  public boolean revisit(Gadget gadget, List<Node> nodes) throws RewritingException {
+    Uri contentBase = gadget.getSpec().getUrl();
+
+    for (Node node: nodes) {
+      Element elem = (Element) node;
+      cssRewriter.rewrite(
+          elem, contentBase,
+          CssResponseRewriter.uriMaker(proxyUriManager, config), false, gadget.getContext());
+    }
+    return !nodes.isEmpty();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java
new file mode 100644
index 0000000..fcae9e7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/TemplateRewriter.java
@@ -0,0 +1,398 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.MessageBundleFactory;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+import org.apache.shindig.gadgets.templates.ContainerTagLibraryFactory;
+import org.apache.shindig.gadgets.templates.MessageELResolver;
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.apache.shindig.gadgets.templates.TemplateContext;
+import org.apache.shindig.gadgets.templates.TemplateLibrary;
+import org.apache.shindig.gadgets.templates.TemplateLibraryFactory;
+import org.apache.shindig.gadgets.templates.TemplateParserException;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.gadgets.templates.TemplateResource;
+import org.apache.shindig.gadgets.templates.tags.CompositeTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.apache.shindig.gadgets.templates.tags.TemplateBasedTagHandler;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * This ContentRewriter uses a TemplateProcessor to replace os-template
+ * tag contents of a gadget spec with their rendered equivalents.
+ *
+ * Only templates without the @name and @tag attributes are processed
+ * automatically.
+ */
+public class TemplateRewriter implements GadgetRewriter {
+
+  public final static Set<String> TAGS = ImmutableSet.of("script");
+  public static final String TEMPLATES_FEATURE_NAME = "opensocial-templates";
+  public static final String OSML_FEATURE_NAME = "osml";
+
+  /** Specifies what template libraries to load */
+  public static final String REQUIRE_LIBRARY_PARAM = "requireLibrary";
+
+  /** Set to true to block auto-processing of templates */
+  static final String DISABLE_AUTO_PROCESSING_PARAM = "disableAutoProcessing";
+
+  /** Enable client support? **/
+  static final String CLIENT_SUPPORT_PARAM = "client";
+
+  //class name for logging purpose
+  private static final String classname = TemplateRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+
+  /**
+   * Provider of the processor.  TemplateRewriters are stateless and multithreaded,
+   * processors are not.
+   */
+  private final Provider<TemplateProcessor> processor;
+  private final MessageBundleFactory messageBundleFactory;
+  private final Expressions expressions;
+  private final TagRegistry baseTagRegistry;
+  private final TemplateLibraryFactory libraryFactory;
+  private final ContainerTagLibraryFactory containerTagLibraryFactory;
+
+  @Inject
+  public TemplateRewriter(Provider<TemplateProcessor> processor,
+      MessageBundleFactory messageBundleFactory, Expressions expressions,
+      TagRegistry baseTagRegistry, TemplateLibraryFactory libraryFactory,
+      ContainerTagLibraryFactory containerTagLibraryFactory) {
+    this.processor = processor;
+    this.messageBundleFactory = messageBundleFactory;
+    this.expressions = expressions;
+    this.baseTagRegistry = baseTagRegistry;
+    this.libraryFactory = libraryFactory;
+    this.containerTagLibraryFactory = containerTagLibraryFactory;
+  }
+
+  public void rewrite(Gadget gadget, MutableContent content) throws RewritingException {
+    Map<String, Feature> directFeatures = gadget.getViewFeatures();
+
+    Feature feature = directFeatures.get(TEMPLATES_FEATURE_NAME);
+    if (feature == null && directFeatures.containsKey(OSML_FEATURE_NAME)) {
+      feature = directFeatures.get(OSML_FEATURE_NAME);
+    }
+
+    if (feature != null && isServerTemplatingEnabled(feature)) {
+      try {
+        rewriteImpl(gadget, feature, content);
+      } catch (GadgetException ge) {
+        throw new RewritingException(ge, ge.getHttpStatusCode());
+      }
+    }
+  }
+
+  /**
+   * Disable server-side templating when the feature contains:
+   * <pre>
+   *   &lt;Param name="disableAutoProcessing"&gt;true&lt;/Param&gt;
+   * </pre>
+   */
+  private boolean isServerTemplatingEnabled(Feature feature) {
+    return (!"true".equalsIgnoreCase(feature.getParam(DISABLE_AUTO_PROCESSING_PARAM)));
+  }
+
+  private void rewriteImpl(Gadget gadget, Feature feature, MutableContent content)
+      throws GadgetException {
+    List<TagRegistry> registries = Lists.newArrayList();
+    List<TemplateLibrary> libraries = Lists.newArrayList();
+
+    // TODO: Add View-specific library as Priority 0
+
+    // Built-in Java-based tags - Priority 1
+    registries.add(baseTagRegistry);
+
+    TemplateLibrary osmlLibrary = containerTagLibraryFactory.getLibrary(gadget.getContext().getContainer());
+
+    // OSML Built-in tags - Priority 2
+    registries.add(osmlLibrary.getTagRegistry());
+    libraries.add(osmlLibrary);
+
+    List<Element> templateElements = SocialDataTags.getTags(content.getDocument(),
+        SocialDataTags.OSML_TEMPLATE_TAG);
+    List<Element> templates = ImmutableList.copyOf(templateElements);
+
+    if (!OSML_FEATURE_NAME.equals(feature.getName())) {
+      // User-defined custom tags - Priority 3
+      registries.add(registerCustomTags(templates));
+
+      // User-defined libraries - Priority 4
+      loadTemplateLibraries(gadget.getContext(), feature, registries, libraries);
+    }
+
+    TagRegistry registry = new CompositeTagRegistry(registries);
+
+    TemplateContext templateContext = new TemplateContext(gadget, content.getPipelinedData());
+    boolean needsFeature = executeTemplates(templateContext, content, templates, registry);
+
+    // Check if a feature param overrides  our guess at whether the client-side
+    // feature is needed.
+    String clientOverride = feature.getParam(CLIENT_SUPPORT_PARAM);
+    if ("true".equalsIgnoreCase(clientOverride)) {
+      needsFeature = true;
+    } else if ("false".equalsIgnoreCase(clientOverride)) {
+      needsFeature = false;
+    }
+
+    Element head = (Element) DomUtil.getFirstNamedChildNode(
+        content.getDocument().getDocumentElement(), "head");
+    postProcess(templateContext, needsFeature, head, templates, libraries);
+  }
+
+  /**
+   * Post-processes the gadget content after rendering templates.
+   *
+   * @param templateContext TemplateContext to operate on
+   * @param needsFeature Should the templates feature be made available to
+   * client?
+   * @param head Head element of the gadget's document
+   * @param libraries Keeps track of all libraries, and which got used
+   * @param allTemplates A list of all the template nodes
+   * @param libraries A list of all registered libraries
+   */
+  private void postProcess(TemplateContext templateContext, boolean needsFeature, Element head,
+      List<Element> allTemplates, List<TemplateLibrary> libraries) {
+    // Inject all the needed library assets.
+    // TODO: inject library assets that aren't used on the server, but will
+    // be needed on the client
+    for (TemplateResource resource : templateContext.getResources()) {
+      injectTemplateLibraryAssets(resource, head);
+    }
+
+    // If we don't need the feature, remove it and all templates from the gadget
+    if (!needsFeature) {
+      templateContext.getGadget().removeFeature(TEMPLATES_FEATURE_NAME);
+      for (Element template : allTemplates) {
+        Node parent = template.getParentNode();
+        if (parent != null) {
+          parent.removeChild(template);
+        }
+      }
+    } else {
+      // If the feature is to be kept, inject the libraries.
+      // Library assets will be generated on the client.
+      // TODO: only inject the templates, not the full scripts/styles
+      for (TemplateLibrary library : libraries) {
+        injectTemplateLibrary(library, head);
+      }
+    }
+  }
+
+  private void loadTemplateLibraries(GadgetContext context, Feature feature,
+      List<TagRegistry> registries, List<TemplateLibrary> libraries)  throws GadgetException {
+    Collection<String> urls = feature.getParams().get(REQUIRE_LIBRARY_PARAM);
+    if (urls != null) {
+      for (String url : urls) {
+        Uri uri = Uri.parse(url.trim());
+        uri = context.getUrl().resolve(uri);
+
+        try {
+          TemplateLibrary library = libraryFactory.loadTemplateLibrary(context, uri);
+          registries.add(library.getTagRegistry());
+          libraries.add(library);
+        } catch (TemplateParserException te) {
+          // Suppress exceptions due to malformed template libraries
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "loadTemplateLibraries", MessageKeys.MALFORMED_TEMPLATE_LIB);
+            LOG.log(Level.WARNING, te.getMessage(),te);
+          }
+        }
+      }
+    }
+  }
+
+  private void injectTemplateLibraryAssets(TemplateResource resource, Element head) {
+    Element contentElement;
+    switch (resource.getType()) {
+      case JAVASCRIPT:
+        contentElement = head.getOwnerDocument().createElement("script");
+        contentElement.setAttribute("type", "text/javascript");
+        break;
+      case STYLE:
+        contentElement = head.getOwnerDocument().createElement("style");
+        contentElement.setAttribute("type", "text/css");
+        break;
+      default:
+        throw new IllegalStateException("Unhandled type");
+    }
+
+    if (resource.isSafe()) {
+      SanitizingGadgetRewriter.bypassSanitization(contentElement, false);
+    }
+    contentElement.setTextContent(resource.getContent());
+    head.appendChild(contentElement);
+  }
+
+  private void injectTemplateLibrary(TemplateLibrary library, Element head) {
+    try {
+      String libraryContent = library.serialize();
+      if (Strings.isNullOrEmpty(libraryContent)) {
+        return;
+      }
+
+      Element scriptElement = head.getOwnerDocument().createElement("script");
+      scriptElement.setAttribute("type", "text/javascript");
+      StringBuilder buffer = new StringBuilder();
+      buffer.append("opensocial.template.Loader.loadContent(");
+      JsonSerializer.appendString(buffer, library.serialize());
+      buffer.append(',');
+      JsonSerializer.appendString(buffer, library.getLibraryUri().toString());
+      buffer.append(");");
+      scriptElement.setTextContent(buffer.toString());
+      head.appendChild(scriptElement);
+    } catch (IOException ioe) {
+      // This should never happen.
+    }
+  }
+
+  /**
+   * Register templates with a "tag" attribute.
+   */
+  private TagRegistry registerCustomTags(List<Element> allTemplates) {
+    ImmutableSet.Builder<TagHandler> handlers = ImmutableSet.builder();
+    for (Element template : allTemplates) {
+      // Only process templates with a tag attribute
+      if (template.getAttribute("tag").length() == 0) {
+        continue;
+      }
+
+      Iterable<String> nameParts = Splitter.on(':').split(template.getAttribute("tag"));
+      // At this time, we only support
+      if (Iterables.size(nameParts) != 2) {
+        continue;
+      }
+      String namespaceUri = template.lookupNamespaceURI(Iterables.get(nameParts, 0));
+      if (namespaceUri != null) {
+        handlers.add(new TemplateBasedTagHandler(template, namespaceUri, Iterables.get(nameParts, 1)));
+      }
+    }
+
+    return new DefaultTagRegistry(handlers.build());
+  }
+
+  /**
+   * Processes and renders inline templates.
+   * @return Do we think the templates feature is still needed on the client?
+   */
+  private boolean executeTemplates(TemplateContext templateContext, MutableContent content,
+      List<Element> allTemplates, TagRegistry registry) throws GadgetException {
+    Map<String, Object> pipelinedData = content.getPipelinedData();
+
+    // If true, client-side processing will be needed
+    boolean needsFeature = false;
+    List<Element> templates = Lists.newArrayList();
+    for (Element element : allTemplates) {
+      String tag = element.getAttribute("tag");
+      String require = element.getAttribute("require");
+
+      if (!checkRequiredData(require, pipelinedData.keySet())) {
+        // Can't be processed on the server at all;  keep client-side processing
+        needsFeature = true;
+      } else if ("".equals(tag)) {
+        templates.add(element);
+      }
+    }
+
+    if (!templates.isEmpty()) {
+      Gadget gadget = templateContext.getGadget();
+
+      MessageBundle bundle = messageBundleFactory.getBundle(gadget.getSpec(),
+          gadget.getContext().getLocale(), gadget.getContext().getIgnoreCache(),
+          gadget.getContext().getContainer(), gadget.getContext().getView());
+      MessageELResolver messageELResolver = new MessageELResolver(expressions, bundle);
+
+      int autoUpdateID = 0;
+      for (Element template : templates) {
+        DocumentFragment result = processor.get().processTemplate(
+            template, templateContext, messageELResolver, registry);
+        // TODO: sanitized renders should ignore this value
+        if ("true".equals(template.getAttribute("autoUpdate"))) {
+          // autoUpdate requires client-side processing.
+          needsFeature = true;
+          Element span = template.getOwnerDocument().createElement("span");
+          String id = "template_auto" + (autoUpdateID++);
+          span.setAttribute("id", "_T_" + id);
+          template.setAttribute("name", id);
+          template.getParentNode().insertBefore(span, template);
+          span.appendChild(result);
+        } else {
+          template.getParentNode().insertBefore(result, template);
+          template.getParentNode().removeChild(template);
+        }
+      }
+      MutableContent.notifyEdit(content.getDocument());
+    }
+    return needsFeature;
+  }
+
+  /**
+   * Checks that all the required data is available at rewriting time.
+   * @param requiredData A string of comma-separated data set names
+   * @param availableData A map of available data sets
+   * @return true if all required data sets are present, false otherwise
+   */
+  private static boolean checkRequiredData(String requiredData, Set<String> availableData) {
+    if ("".equals(requiredData)) {
+      return true;
+    }
+    StringTokenizer st = new StringTokenizer(requiredData, ",");
+    while (st.hasMoreTokens()) {
+      if (!availableData.contains(st.nextToken().trim())) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BMPOptimizer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BMPOptimizer.java
new file mode 100644
index 0000000..882e0c5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BMPOptimizer.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriter;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Optimize BMP by converting to PNG
+ */
+public class BMPOptimizer extends PNGOptimizer {
+
+  public static BufferedImage readBmp(InputStream is)
+      throws ImageReadException, IOException {
+    return Sanselan.getBufferedImage(is);
+  }
+
+  public BMPOptimizer(OptimizerConfig config, HttpResponseBuilder response) {
+    super(config, response);
+    ImageWriter writer = ImageIO.getImageWritersByFormatName("png").next();
+    outputter = new ImageIOOutputter(writer, null);
+  }
+
+  @Override
+  protected String getOriginalContentType() {
+    return "image/bmp";
+  }
+
+  @Override
+  protected String getOriginalFormatName() {
+    return "bmp";
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java
new file mode 100644
index 0000000..23e97f4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizer.java
@@ -0,0 +1,293 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageFormat;
+import org.apache.sanselan.ImageWriteException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import com.sun.imageio.plugins.jpeg.JPEG;
+import com.sun.imageio.plugins.jpeg.JPEGImageWriter;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.imageio.IIOImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageTypeSpecifier;
+import javax.imageio.ImageWriteParam;
+import javax.imageio.ImageWriter;
+import javax.imageio.metadata.IIOInvalidTreeException;
+import javax.imageio.metadata.IIOMetadata;
+import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
+
+/**
+ * Base class for image optimizers
+ */
+abstract class BaseOptimizer {
+  static final Map<String, ImageFormat> FORMAT_NAME_TO_IMAGE_FORMAT = ImmutableMap.of(
+      "png", ImageFormat.IMAGE_FORMAT_PNG,
+      "gif", ImageFormat.IMAGE_FORMAT_GIF,
+      "jpeg", ImageFormat.IMAGE_FORMAT_JPEG);
+
+  final HttpResponseBuilder response;
+  final OptimizerConfig config;
+
+  protected ImageOutputter outputter;
+  protected byte[] minBytes;
+  protected int minLength;
+  protected JpegImageUtils.JpegImageParams sourceImageParams;
+  int reductionPct;
+
+  public BaseOptimizer(OptimizerConfig config, HttpResponseBuilder response) {
+    this(config, response, null);
+  }
+
+  public BaseOptimizer(OptimizerConfig config, HttpResponseBuilder response,
+                       JpegImageUtils.JpegImageParams sourceImageParams) {
+    this.config = config;
+    this.response = response;
+    this.minLength = response.getContentLength();
+    this.sourceImageParams = sourceImageParams;
+    this.outputter = getOutputter();
+  }
+
+  protected ImageOutputter getOutputter() {
+    Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName(getOriginalFormatName());
+    if (writers.hasNext()) {
+      ImageWriter writer = writers.next();
+      ImageWriteParam param = writer.getDefaultWriteParam();
+      if (getOriginalFormatName().equals("jpeg")) {
+        param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+        param.setCompressionQuality(config.getJpegCompression());
+        if (param instanceof JPEGImageWriteParam) {
+          ((JPEGImageWriteParam) param).setOptimizeHuffmanTables(
+                config.getJpegHuffmanOptimization());
+        }
+      }
+
+      JpegImageUtils.SamplingModes samplingMode = JpegImageUtils.SamplingModes.DEFAULT;
+      if (config.getJpegRetainSubsampling() && sourceImageParams != null) {
+        samplingMode = sourceImageParams.getSamplingMode();
+      }
+      return new ImageIOOutputter(writer, param, samplingMode);
+    }
+    return new SanselanOutputter(FORMAT_NAME_TO_IMAGE_FORMAT.get(getOriginalFormatName()));
+  }
+
+  /**
+   * Write the image using a specified write param
+   */
+  protected void write(BufferedImage image) throws IOException {
+    if (image == null) {
+      return;
+    }
+
+    byte[] bytes = outputter.toBytes(image);
+    if (minLength > bytes.length) {
+      minBytes = bytes;
+      minLength = minBytes.length;
+      reductionPct = ((response.getContentLength() - minLength) * 100) /
+          response.getContentLength();
+    }
+  }
+
+  public void rewrite(BufferedImage image) throws IOException {
+    if (outputter == null) {
+      return;
+    }
+
+    long time = System.currentTimeMillis();
+    rewriteImpl(image);
+    time = System.currentTimeMillis() - time;
+    if (minBytes != null && minBytes.length != 0) {
+      StringBuilder rewriteMsg = new StringBuilder(24);
+      rewriteMsg.append("c=").append(
+          ((minBytes.length * 100) / response.getContentLength()));
+      if (!getOutputContentType().equals(getOriginalContentType())) {
+        rewriteMsg.append(";o=").append(getOriginalContentType());
+      }
+      rewriteMsg.append(";t=").append(time);
+
+      // Removing the original 'Etag' header as we have updated the content.
+      response.removeHeader("ETag");
+      response
+          .setHeader("Content-Type", getOutputContentType())
+          .setHeader("X-Shindig-Rewrite", rewriteMsg.toString())
+          .setResponse(minBytes);
+    }
+  }
+
+  /**
+   * Get the rewritten image if available
+   */
+  protected final byte[] getRewrittenImage() {
+    return minBytes;
+  }
+
+  protected abstract void rewriteImpl(BufferedImage image) throws IOException;
+
+  protected abstract String getOutputContentType();
+
+  protected abstract String getOriginalContentType();
+
+  protected abstract String getOriginalFormatName();
+
+  /**
+   * Interface to allow for different serialization libraries to be used
+   */
+  public static interface ImageOutputter {
+    byte[] toBytes(BufferedImage image) throws IOException;
+  }
+
+  /**
+   * Standard ImageIO based image outputter
+   */
+  public static class ImageIOOutputter implements ImageOutputter {
+
+    ImageWriter writer;
+    ByteArrayOutputStream baos;
+    ImageWriteParam writeParam;
+    JpegImageUtils.SamplingModes jpegSamplingMode;
+
+    public ImageIOOutputter(ImageWriter writer, ImageWriteParam writeParam) {
+      this(writer, writeParam, JpegImageUtils.SamplingModes.DEFAULT);
+    }
+
+    public ImageIOOutputter(ImageWriter writer, ImageWriteParam writeParam,
+                            JpegImageUtils.SamplingModes jpegSamplingMode) {
+      this.writer = writer;
+      this.writeParam = Objects.firstNonNull(writeParam, writer.getDefaultWriteParam());
+      this.jpegSamplingMode = jpegSamplingMode;
+    }
+
+    public byte[] toBytes(BufferedImage image) throws IOException {
+      if (baos == null) {
+        baos = new ByteArrayOutputStream();
+      } else {
+        baos.reset();
+      }
+      writer.setOutput(ImageIO.createImageOutputStream(baos));
+
+      // Create a new empty metadata set
+      ImageWriteParam metaImageWriteParam = writeParam;
+      if (writer instanceof JPEGImageWriter) {
+        // There is an issue in the javax code because of which function call
+        // writer.getDefaultImageMetadata(new ImageTypeSpecifier(image.getColorModel(),
+        //    image.getSampleModel()), writeParam);
+        //
+        // does buggy processing for compression ratio parameter in ImageWriteParam.
+        // Hence passing null as ImageWriteParam here to ignore this processing and
+        // passing the ImageWriteParam later in the writer.write() call.
+        metaImageWriteParam = null;
+      }
+
+      IIOMetadata metadata = writer.getDefaultImageMetadata(
+          new ImageTypeSpecifier(image.getColorModel(), image.getSampleModel()),
+          metaImageWriteParam);
+
+      if (jpegSamplingMode.getModeValue() > 0 && writer instanceof JPEGImageWriter) {
+        setJpegSubsamplingMode(metadata);
+      }
+
+      writer.write(null, new IIOImage(image, Collections.<BufferedImage>emptyList(), metadata),
+                   metaImageWriteParam == null ? writeParam : null);
+
+      return baos.toByteArray();
+    }
+
+    private void setJpegSubsamplingMode(IIOMetadata metadata)
+        throws IIOInvalidTreeException {
+      // Tweaking the image metadata to override default subsampling(4:2:0) with
+      // 4:4:4.
+      Node rootNode = metadata.getAsTree(JPEG.nativeImageMetadataFormatName);
+      boolean metadataUpdated = false;
+      // The top level root node has two children, out of which the second one will
+      // contain all the information related to image markers.
+      if (rootNode.getLastChild() != null) {
+        Node markerNode = rootNode.getLastChild();
+        NodeList markers = markerNode.getChildNodes();
+        // Search for 'SOF' marker where subsampling information is stored.
+        for (int i = 0; i < markers.getLength(); i++) {
+          Node node = markers.item(i);
+          // 'SOF' marker can have
+          //   1 child node if the color representation is greyscale,
+          //   3 child nodes if the color representation is YCbCr, and
+          //   4 child nodes if the color representation is YCMK.
+          // This subsampling applies only to YCbCr.
+          if (node.getNodeName().equalsIgnoreCase("sof") && node.hasChildNodes() &&
+              node.getChildNodes().getLength() == 3) {
+            // In 'SOF' marker, first child corresponds to the luminance channel, and setting
+            // the HsamplingFactor and VsamplingFactor to 1, will imply 4:4:4 chroma subsampling.
+            NamedNodeMap attrMap = node.getFirstChild().getAttributes();
+            int samplingMode = jpegSamplingMode.getModeValue();
+            attrMap.getNamedItem("HsamplingFactor").setNodeValue((samplingMode & 0xf) + "");
+            attrMap.getNamedItem("VsamplingFactor").setNodeValue(((samplingMode >> 4) & 0xf) + "");
+            metadataUpdated = true;
+            break;
+          }
+        }
+      }
+
+      // Read the updated metadata from the metadata node tree.
+      if (metadataUpdated) {
+        metadata.setFromTree(JPEG.nativeImageMetadataFormatName, rootNode);
+      }
+    }
+  }
+
+  /**
+   * Sanselan based image outputter
+   */
+  public static class SanselanOutputter implements ImageOutputter {
+
+    ImageFormat format;
+    ByteArrayOutputStream baos;
+    public SanselanOutputter(ImageFormat format) {
+      this.format = format;
+    }
+
+    public byte[] toBytes(BufferedImage image) throws IOException {
+      if (baos == null) {
+        baos = new ByteArrayOutputStream();
+      } else {
+        baos.reset();
+      }
+      try {
+        Sanselan.writeImage(image, baos, format, Maps.newHashMap());
+        return baos.toByteArray();
+      } catch (ImageWriteException iwe) {
+        throw new IOException(iwe.getMessage());
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java
new file mode 100644
index 0000000..53217ef
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/BasicImageRewriter.java
@@ -0,0 +1,577 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import static java.awt.RenderingHints.KEY_INTERPOLATION;
+import static java.awt.RenderingHints.VALUE_INTERPOLATION_BICUBIC;
+import static java.lang.Math.abs;
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+
+import org.apache.sanselan.ImageFormat;
+import org.apache.sanselan.ImageInfo;
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.rewrite.image.BaseOptimizer.ImageIOOutputter;
+import org.apache.shindig.gadgets.rewrite.image.BaseOptimizer.ImageOutputter;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import java.awt.AlphaComposite;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageWriter;
+
+/**
+ * Rewrite images to more efficiently compress their content. Can output to a different format file
+ * for better efficiency.
+ *
+ * <p>Security Note: Uses the Sanselan library to parse image content and metadata to avoid security
+ * issues in the ImageIO library. Uses ImageIO for output.
+ */
+public class BasicImageRewriter implements ResponseRewriter {
+
+  static final String
+      CONTENT_TYPE_AND_EXTENSION_MISMATCH =
+        "Content is not an image but file extension asserts it is";
+  static final String
+      CONTENT_TYPE_AND_MIME_MISMATCH =
+          "Content is not an image but mime type asserts it is";
+
+  private static final String CONTENT_TYPE_IMAGE_PNG = "image/png";
+  /** Returned as the output message if a huge image is submitted to be scaled */
+  private static final String RESIZE_IMAGE_TOO_LARGE = "The image is too large to resize";
+  /** With resizing active, all images become PNGs */
+  private static final String RESIZE_OUTPUT_FORMAT = "png";
+
+  private static final String CONTENT_LENGTH = "Content-Length";
+
+  /** Parameter used to request image rendering quality */
+  private static final String PARAM_RESIZE_QUALITY = Param.RESIZE_QUALITY.getKey();
+  /** Parameter used to request image width change */
+  private static final String PARAM_RESIZE_WIDTH = Param.RESIZE_WIDTH.getKey();
+  /** Parameter used to request image height change */
+  private static final String PARAM_RESIZE_HEIGHT = Param.RESIZE_HEIGHT.getKey();
+  /** Parameter used to request resizing will not expand image */
+  private static final String PARAM_NO_EXPAND = Param.NO_EXPAND.getKey();
+
+  private static final int BITS_PER_BYTE = 8;
+  private static final Color COLOR_TRANSPARENT = new Color(255, 255, 255, 0);
+  public static final String CONTENT_TYPE = "Content-Type";
+  //class name for logging purpose
+  private static final String classname = BasicImageRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private static final Set<String> SUPPORTED_MIME_TYPES = ImmutableSet.of(
+      "image/gif", CONTENT_TYPE_IMAGE_PNG, "image/jpeg", "image/bmp");
+
+  private static final Set<String> SUPPORTED_FILE_EXTENSIONS = ImmutableSet.of(
+      ".gif", ".png", ".jpeg", ".jpg", ".bmp");
+
+  private final OptimizerConfig config;
+
+  private static class ImageResizeData {
+    private Integer requestedWidth;
+    private Integer requestedHeight;
+    private Integer widthDelta;
+    private Integer heightDelta;
+
+    protected ImageResizeData(Integer requestedWidth, Integer requestedHeight, Integer widthDelta,
+        Integer heightDelta) {
+      this.requestedWidth = requestedWidth;
+      this.requestedHeight = requestedHeight;
+      this.widthDelta = widthDelta;
+      this.heightDelta = heightDelta;
+    }
+
+    public Integer getWidth() {
+      return requestedWidth;
+    }
+
+    public Integer getHeight() {
+      return requestedHeight;
+    }
+
+    public Integer getWidthDelta() {
+      return widthDelta;
+    }
+
+    public Integer getHeightDelta() {
+      return heightDelta;
+    }
+  }
+
+  @Inject
+  public BasicImageRewriter(OptimizerConfig config) {
+    this.config = config;
+  }
+
+  /**
+   * Predicate check for validating the Image Rewrite step. Images that are either too huge or
+   * invalid resize URL parameters are specified are not fit for rewrite.
+   *
+   * @param request the HTTP request.
+   * @param response the HTTP response for the original image fetched.
+   * @param imageInfo the image information extracted via Apache's Sanselan APIs.
+   * @param isResizeRequested boolean flag to indicate whether Image resize is requested or not.
+   * Huge images for which resize is requested are not fit for rewrite.
+   * @return true if the specified image can be rewriten; else it's set to false.
+   */
+  private Boolean canRewrite(HttpRequest request, HttpResponseBuilder response,
+      ImageInfo imageInfo, Boolean isResizeRequested) {
+    Uri uri = request.getUri();
+    if (null == uri) return false;
+
+    // Don't handle very small images, but check after parsing format to
+    // detect attacks.
+    if (response.getContentLength() < config.getMinThresholdBytes()) {
+      return false;
+    }
+
+    // TODO(anyone): The following check has been retained to maintain the functional equivalency
+    // with the old code (and testcases). In case resize parameters are not specified, still one
+    // can apply optimization to the response (call to 'applyOptimizer').
+    Integer resizeQuality = request.getParamAsInteger(PARAM_RESIZE_QUALITY);
+    Integer requestedWidth = request.getParamAsInteger(PARAM_RESIZE_WIDTH);
+    Integer requestedHeight = request.getParamAsInteger(PARAM_RESIZE_HEIGHT);
+    if (!isUsableParameter(requestedWidth) || !isUsableParameter(requestedHeight) ||
+        !isUsableParameter(resizeQuality)) {
+      return false;
+    }
+
+    if (isResizeRequested && isImageTooLarge(imageInfo)) {
+      errorResponse(response, HttpResponse.SC_FORBIDDEN, RESIZE_IMAGE_TOO_LARGE);
+      return false;
+    }
+
+    // Don't handle animations.
+    // TODO: This doesn't work as current Sanselan doesn't return accurate image counts.
+    // See animated GIF detection below.
+    if (imageInfo.getNumberOfImages() > 1 || isImageTooLarge(imageInfo)) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Predicate check for validating the Image Resizing step. Images with improper resize parameters
+   * specified are not fit for resize.
+   *
+   * @param request the HTTP request.
+   * @param response the HTTP response for the original image fetched.
+   * @param imageInfo the image information extracted via Apache's Sanselan APIs.
+   * @return true if the specified image can be rewriten; else it's set to false.
+   */
+  private Boolean isResizeRequested(HttpRequest request, HttpResponseBuilder response,
+      ImageInfo imageInfo) {
+    Integer requestedWidth = request.getParamAsInteger(PARAM_RESIZE_WIDTH);
+    Integer requestedHeight = request.getParamAsInteger(PARAM_RESIZE_HEIGHT);
+
+    boolean resizeRequested = ((requestedWidth != null) && isUsableParameter(requestedWidth) ||
+                               (requestedHeight != null) && isUsableParameter(requestedHeight));
+    boolean noExpand = "1".equals(request.getParam(PARAM_NO_EXPAND));
+    if (noExpand &&
+        (requestedHeight == null || imageInfo.getHeight() <= requestedHeight) &&
+        (requestedWidth == null || imageInfo.getWidth() <= requestedWidth)) {
+      // Don't do anything, since the current image fits within the bounding area.
+      resizeRequested = false;
+    }
+
+    return resizeRequested;
+  }
+
+  /**
+   * Get Image Resize data by honoring resizing parameters specified in the request.
+   *
+   * @param request the HTTP request.
+   * @param response the HTTP response for the original image fetched.
+   * @param image the image to resize and format conversion.
+   * @param imageInfo the image information extracted via Apache's Sanselan APIs.
+   * @return image resize data corresponding to the transformed width and height. The return value
+   * is null for cases where image can't be resized.
+   */
+   private ImageResizeData getResizeData(HttpRequest request, HttpResponseBuilder response,
+       BufferedImage image, ImageInfo imageInfo) throws IOException {
+    int origWidth = imageInfo.getWidth();
+    int origHeight = imageInfo.getHeight();
+    int widthDelta = 0;
+    int heightDelta = 0;
+    Integer requestedWidth = request.getParamAsInteger(PARAM_RESIZE_WIDTH);
+    Integer requestedHeight = request.getParamAsInteger(PARAM_RESIZE_HEIGHT);
+
+    if (requestedWidth == null || requestedHeight == null) {
+      // It is enough to cast only one int to double, Java will coerce all others to double
+      // (JAVA spec, section 5.1.2).  In addition, interleave divisions and multiplications
+      // to keep the end result at bay, and clip the requested dimensions from below to
+      // compensate for small image dimensions.
+      if (requestedWidth == null) {
+        requestedWidth = max(1, (int) (origWidth / (double) origHeight * requestedHeight));
+      }
+      if (requestedHeight == null) {
+        requestedHeight = max(1, (int) (origHeight / (double) origWidth * requestedWidth));
+      }
+    } else {
+      // If both image dimensions are fixed, the two-step resizing process will need to know
+      // how much it has to fix up the image.
+      double ratio = getResizeRatio(requestedWidth, requestedHeight, origWidth, origHeight);
+      int widthAfterStep1 = max(1, (int) Math.round(ratio * origWidth));
+      widthDelta = requestedWidth - widthAfterStep1;
+
+      int heightAfterStep1 = max(1, (int) Math.round(ratio * origHeight));
+      heightDelta = requestedHeight - heightAfterStep1;
+
+      boolean noExpand = "1".equals(request.getParam(PARAM_NO_EXPAND));
+      if (noExpand) {
+        // No expansion requested: make sure not to expand the resulting image on either axis,
+        // even if both resize_[w,h] params are specified.
+        if (widthDelta == 0) {
+          requestedHeight = heightAfterStep1;
+          heightDelta = 0;
+        } else if (heightDelta == 0) {
+          requestedWidth = widthAfterStep1;
+          widthDelta = 0;
+        }
+      }
+    }
+
+    if (isResizeRequired(requestedWidth, requestedHeight, imageInfo)
+        && !isTargetImageTooLarge(requestedWidth, requestedHeight, imageInfo)) {
+      return new ImageResizeData(requestedWidth, requestedHeight, widthDelta, heightDelta);
+    } else {
+      return null;
+    }
+  }
+
+   public void rewrite(HttpRequest request, HttpResponseBuilder response, Gadget gadget) {
+     if (request == null || response == null) return;
+
+     Uri uri = request.getUri();
+     if (null == uri) return;
+
+     try {
+        // If the path or MIME type don't match, continue
+       if (!isSupportedImageResult(response, uri)) {
+         return;
+       }
+
+       // Content header checking is fast so this is fine to do for every response.
+       ImageFormat imageFormat = Sanselan
+           .guessFormat(new ByteSourceInputStream(response.getContentBytes(), uri.getPath()));
+
+       if (imageFormat == ImageFormat.IMAGE_FORMAT_UNKNOWN) {
+         enforceUnreadableImageRestrictions(uri, response);
+         return;
+       }
+
+       ImageInfo imageInfo = Sanselan.getImageInfo(response.getContentBytes(), uri.getPath());
+
+       Boolean resizeRequested = isResizeRequested(request, response, imageInfo);
+
+       // Return in case image can't be rewriten.
+       if (!canRewrite(request, response, imageInfo, resizeRequested)) {
+         return;
+       }
+
+       JpegImageUtils.JpegImageParams jpegImageParams = null;
+       if (imageFormat == ImageFormat.IMAGE_FORMAT_JPEG) {
+         jpegImageParams = JpegImageUtils.getJpegImageData(response.getContentBytes(), uri.getPath());
+       }
+
+       // Step#1: Read the image using appropriate readers for the corresponding image format.
+       BufferedImage image = readImage(imageFormat, response);
+
+       // Proceed to Resize in case image can be resized.
+       if (resizeRequested) {
+         // Step#2: Get the Resize Data
+         ImageResizeData resizeData = getResizeData(request, response, image, imageInfo);
+
+         if (resizeData != null) {
+           // Step#3: Resize (Scale+Stretch) Image using Java AWT Graphics2D package.
+           image = resizeImage(image, resizeData.getWidth(), resizeData.getHeight(),
+               resizeData.getWidthDelta(), resizeData.getHeightDelta());
+
+           // Step#4: Convert the image format (MIME_TYPE) using javax.imageio package.
+           updateResponse(response, image);
+         }
+       }
+
+       // Step#5: Optimize the supported image formats viz PNG, GIF, JPG & BMP using 'BaseOptimizer'
+       // and it's subclass implementations for the above four formats.
+       applyOptimizer(response, imageFormat, jpegImageParams, image, config);
+     } catch (IOException ioe) {
+       if (LOG.isLoggable(Level.WARNING)) {
+         LOG.logp(Level.WARNING, classname, "rewrite", MessageKeys.IO_ERROR_REWRITING_IMG, new Object[] {request.toString(),ioe.getMessage()});
+       }
+     } catch (RuntimeException re) {
+       // This is safe to recover from and necessary because the ImageIO/Sanselan calls can
+       // throw a very wide variety of exceptions
+       if (LOG.isLoggable(Level.INFO)) {
+         LOG.logp(Level.INFO, classname, "rewrite", MessageKeys.UNKNOWN_ERROR_REWRITING_IMG, new Object[] {request.toString(),re.getMessage()});
+       }
+     } catch (ImageReadException ire) {
+       if (LOG.isLoggable(Level.INFO)) {
+         LOG.logp(Level.INFO, classname, "rewrite", MessageKeys.FAILED_TO_READ_IMG, new Object[] {request.toString(),ire.getMessage()});
+       }
+     }
+   }
+
+  /**
+   * If the image is resized, the request needs to change so that the optimizer can
+   * make sensible image size-related decisions down the pipeline.  GIF images are rewritten
+   * as PNGs though, so as not to include the dependency on the GIF decoder.
+   *
+   * @param response the base response that will be modified with the resized image
+   * @param image the resized image that needs to be substituted for the original image from
+   *        the response
+   */
+  public void updateResponse(HttpResponseBuilder response, BufferedImage image) throws IOException {
+    ImageWriter imageWriter = ImageIO.getImageWritersByFormatName(RESIZE_OUTPUT_FORMAT).next();
+    ImageOutputter outputter = new ImageIOOutputter(imageWriter, null);
+    byte[] imageBytes = outputter.toBytes(image);
+    response
+        .setResponse(imageBytes)
+        .setHeader(CONTENT_TYPE, CONTENT_TYPE_IMAGE_PNG)
+        .setHeader(CONTENT_LENGTH, String.valueOf(imageBytes.length));
+  }
+
+  private boolean isUsableParameter(Integer parameterValue) {
+    if (parameterValue == null) {
+      return true;
+    }
+    return parameterValue.intValue() > 0;
+  }
+
+  /** Gets the feasible resize ratio. */
+  private double getResizeRatio(int requestedWidth, int requestedHeight, int origWidth,
+      int origHeight) {
+    return min(requestedWidth / (double) origWidth,
+              requestedHeight / (double) origHeight);
+  }
+
+  /**
+   * Two-step image resize.
+   *
+   * <p>The first step scales the image so that the smaller of the vertical and horizontal
+   * scaling ratios is satisfied.  For square images the two ratios are equal and we leave it
+   * at that.  For rectangular images, this leaves a part of the target image rectangle that is
+   * not covered, and we need to proceed to step 2.
+   *
+   * <p>The second step stretches the image along the dimension that came in short after the first
+   * step to fully cover the target image rectangle.
+   *
+   * @param image the image to resize
+   * @param requestedWidth the width in pixels of the requested resulting image
+   * @param requestedHeight the height in pixels of the requested resulting image
+   * @param extraWidth the width (in pixels) to add on top of the original image
+   * @param extraHeight the height (in pixels) to add on top of the original image
+   * @return the image obtained by stretching the original image so that its new dimensions
+   *        are {@code requestedWidth} and {@code requestedHeight}
+   */
+  public BufferedImage resizeImage(BufferedImage image, Integer requestedWidth,
+      Integer requestedHeight, int extraWidth, int extraHeight) {
+    int widthStretch = requestedWidth - extraWidth;
+    int heightStretch = requestedHeight - extraHeight;
+    int imageType = ImageUtils.isOpaque(image)
+        ? BufferedImage.TYPE_3BYTE_BGR
+        : BufferedImage.TYPE_INT_ARGB;
+
+    image = ImageUtils.getScaledInstance(image, widthStretch, heightStretch,
+        VALUE_INTERPOLATION_BICUBIC, true /* higherQuality */, imageType);
+
+    if (image.getWidth() != requestedWidth || image.getHeight() != requestedHeight) {
+      image = stretchImage(image, requestedWidth, requestedHeight, imageType);
+    }
+    return image;
+  }
+
+  private BufferedImage stretchImage(BufferedImage image, Integer requestedWidth,
+      Integer requestedHeight, int imageType) {
+    BufferedImage scaledImage = new BufferedImage(requestedWidth, requestedHeight, imageType);
+
+    Graphics2D g2d = scaledImage.createGraphics();
+    g2d.setRenderingHint(KEY_INTERPOLATION, VALUE_INTERPOLATION_BICUBIC);
+    fillWithTransparent(g2d, requestedWidth, requestedHeight);
+
+    g2d.drawImage(image, 0, 0, requestedWidth, requestedHeight, null);
+    image = scaledImage;
+    return image;
+  }
+
+  private void fillWithTransparent(Graphics2D g2d, Integer requestedWidth,
+      Integer requestedHeight) {
+    g2d.setComposite(AlphaComposite.Clear);
+    g2d.setColor(COLOR_TRANSPARENT);
+    g2d.fillRect(0, 0, requestedWidth, requestedHeight);
+    g2d.setComposite(AlphaComposite.SrcOver);
+  }
+
+  protected void applyOptimizer(HttpResponseBuilder response, ImageFormat imageFormat,
+      JpegImageUtils.JpegImageParams jpegImageParams, BufferedImage image,
+      OptimizerConfig config) throws IOException {
+    if (imageFormat == ImageFormat.IMAGE_FORMAT_GIF) {
+      // Detecting the existence of the NETSCAPE2.0 extension by string comparison
+      // is not exactly clean but is good enough to determine if a GIF is animated
+      // Remove once Sanselan returns image count
+      if (!response.create().getResponseAsString().contains("NETSCAPE2.0")) {
+        new GIFOptimizer(config, response).rewrite(image);
+      }
+    } else if (imageFormat == ImageFormat.IMAGE_FORMAT_PNG) {
+      new PNGOptimizer(config, response).rewrite(image);
+    } else if (imageFormat == ImageFormat.IMAGE_FORMAT_JPEG) {
+      new JPEGOptimizer(config, response, jpegImageParams).rewrite(image);
+    } else if (imageFormat == ImageFormat.IMAGE_FORMAT_BMP) {
+      new BMPOptimizer(config, response).rewrite(image);
+    }
+  }
+
+  private boolean isImageTooLarge(ImageInfo imageInfo) {
+    return isTargetImageTooLarge(imageInfo.getWidth(), imageInfo.getHeight(), imageInfo);
+  }
+
+  /**
+   * @param requestedHeight the requested image height, assumed always nonnegative
+   * @param requestedWidth the requested image width, assumed always nonnegative
+   * @param imageInfo the image information to analyze
+   * @return {@code true} if the image size given by the parameters is too large to be acceptable
+   *         for serving
+   */
+  private boolean isTargetImageTooLarge(int requestedHeight, int requestedWidth,
+      ImageInfo imageInfo) {
+    long imagePixels = abs(requestedHeight) * abs(requestedWidth);
+    long imageSizeBits = imagePixels * imageInfo.getBitsPerPixel();
+    return imageSizeBits > config.getMaxInMemoryBytes() * BITS_PER_BYTE;
+  }
+
+  protected boolean isSupportedImageResult(HttpResponseBuilder response, Uri uri) {
+    return isSupportedContent(response) || isImageUri(uri);
+  }
+
+  protected boolean isSupportedContent(HttpResponseBuilder response) {
+    return SUPPORTED_MIME_TYPES.contains(response.getHeader(CONTENT_TYPE));
+  }
+
+  /**
+   * Ensures that the URI points to an image, before continuing.
+   *
+   *  @param uri the URI to check
+   */
+  protected boolean isImageUri(Uri uri) {
+    boolean pathExtMatches = false;
+    for (String ext: SUPPORTED_FILE_EXTENSIONS) {
+      if (uri.getPath().endsWith(ext)) {
+        pathExtMatches = true;
+        break;
+      }
+    }
+    return pathExtMatches;
+  }
+
+  private boolean isResizeRequired(int resize_w, int resize_h, ImageInfo imageInfo) {
+    return resize_w != imageInfo.getWidth() || resize_h != imageInfo.getHeight();
+  }
+
+  /**
+   * An image could not be read from the content. Normally this is fine unless the content-type
+   * states that this is an image in which case it could be an attack. If either the filetype or the
+   * MIME-type indicate that image content should be available but we failed to read it, then return
+   * an error response.
+   */
+  protected void enforceUnreadableImageRestrictions(Uri uri, HttpResponseBuilder response) {
+    String contentType = response.getHeader(CONTENT_TYPE);
+    if (contentType != null) {
+      contentType = contentType.toLowerCase();
+      for (String expected : SUPPORTED_MIME_TYPES) {
+        if (contentType.contains(expected)) {
+          // MIME type says its a supported image but we can't read it. Reject.
+          errorResponse(response, HttpResponse.SC_UNSUPPORTED_MEDIA_TYPE,
+              CONTENT_TYPE_AND_MIME_MISMATCH);
+          return;
+        }
+      }
+    }
+
+    String path = uri.getPath().toLowerCase();
+    for (String supportedExtension : SUPPORTED_FILE_EXTENSIONS) {
+      if (path.endsWith(supportedExtension)) {
+        // The file extension says its a supported image but we can't read it. Reject.
+        errorResponse(response, HttpResponse.SC_UNSUPPORTED_MEDIA_TYPE,
+            CONTENT_TYPE_AND_EXTENSION_MISMATCH);
+        return;
+      }
+    }
+  }
+
+  private void errorResponse(HttpResponseBuilder response, int status, String msg) {
+    response.clearAllHeaders().setHttpStatusCode(status).setResponseString(msg);
+  }
+
+  protected BufferedImage readImage(ImageFormat imageFormat, HttpResponseBuilder response)
+      throws ImageReadException, IOException{
+    if (imageFormat == ImageFormat.IMAGE_FORMAT_GIF) {
+      return readGif(response);
+    } else if (imageFormat == ImageFormat.IMAGE_FORMAT_PNG) {
+      return readPng(response);
+    } else if (imageFormat == ImageFormat.IMAGE_FORMAT_JPEG) {
+      return readJpeg(response);
+    } else if (imageFormat == ImageFormat.IMAGE_FORMAT_BMP) {
+      return readBmp(response);
+    } else {
+      throw new ImageReadException("Unsupported format " + imageFormat.name);
+    }
+  }
+
+  // The following methods are intended to be overridden by implementors if they need to
+  // implement additional security constraints or use their own more efficient
+  // image reading mechanisms
+
+  protected BufferedImage readBmp(HttpResponseBuilder response) throws ImageReadException, IOException {
+    return BMPOptimizer.readBmp(response.getContentBytes());
+  }
+
+  protected BufferedImage readPng(HttpResponseBuilder response) throws ImageReadException, IOException {
+    return PNGOptimizer.readPng(response.getContentBytes());
+  }
+
+  protected BufferedImage readGif(HttpResponseBuilder response) throws ImageReadException, IOException {
+    return GIFOptimizer.readGif(response.getContentBytes());
+  }
+
+  protected BufferedImage readJpeg(HttpResponseBuilder response) throws ImageReadException, IOException {
+    return JPEGOptimizer.readJpeg(response.getContentBytes());
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/GIFOptimizer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/GIFOptimizer.java
new file mode 100644
index 0000000..264e26c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/GIFOptimizer.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Optimize GIF images by converting them to PNGs or even JPEGs depending on content
+ */
+public class GIFOptimizer extends PNGOptimizer {
+
+  public static BufferedImage readGif(InputStream is)
+      throws ImageReadException, IOException {
+    return Sanselan.getBufferedImage(is);
+  }
+
+  private boolean usePng;
+
+  public GIFOptimizer(OptimizerConfig config, HttpResponseBuilder response) {
+    super(config, response);
+  }
+
+  @Override
+  protected void rewriteImpl(BufferedImage image) throws IOException {
+    if (!ImageUtils.isOpaque(image)) {
+      // We can rewrite transparent GIFs to PNG but for IE6 it requires the use of
+      // the AlphaImageReader and some pain. Deferring this until that is proven to work
+
+      // Write to strip any metadata and re-compute the palette. We allow arbitrary large palettes
+      // here as if the image is already in a direct color model it will already have been
+      // constrained by the max in-mem constraint.
+      write(ImageUtils.palettize(image, Integer.MAX_VALUE));
+    } else {
+      usePng = true;
+      outputter = new ImageIOOutputter(ImageIO.getImageWritersByFormatName("png").next(), null);
+      super.rewriteImpl(image);
+    }
+  }
+
+  @Override
+  protected String getOriginalContentType() {
+    return "image/gif";
+  }
+
+  @Override
+  protected String getOutputContentType() {
+    if (usePng) {
+      return super.getOutputContentType();
+    }
+    return "image/gif";
+  }
+
+  @Override
+  protected String getOriginalFormatName() {
+    return "gif";
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/ImageUtils.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/ImageUtils.java
new file mode 100644
index 0000000..d87ecd1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/ImageUtils.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import com.google.common.collect.Maps;
+
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.awt.image.DataBuffer;
+import java.awt.image.IndexColorModel;
+import java.awt.image.WritableRaster;
+import java.util.Map;
+
+/**
+ * Utility functions for image processing and introspection.
+ */
+public final class ImageUtils {
+  private ImageUtils() {}
+  /**
+   * Convert an image to a palletized one. Will not create a palette above a fixed
+   * number of entries
+   */
+  public static BufferedImage palettize(BufferedImage img, int maxEntries) {
+    // Just because an image has a palette doesnt mean it has a good one
+    // so we re-index even if its an IndexColorModel
+    int addedCount = 0;
+    Map<Integer, Integer> added = Maps.newHashMap();
+    for (int y = 0; y < img.getHeight(); y++) {
+      for (int x = 0; x < img.getWidth(); x++) {
+        if (!added.containsKey(img.getRGB(x, y))) {
+          added.put(img.getRGB(x, y), addedCount++);
+        }
+        if (added.size() > maxEntries) {
+          // Bail if palette becomes too large
+          return null;
+        }
+      }
+    }
+    int[] cmap = new int[added.size()];
+    for (int c : added.keySet()) {
+      cmap[added.get(c)] = c;
+    }
+
+    int bitCount = 1;
+    while (added.size() >> bitCount != 0) {
+      bitCount *= 2;
+    }
+
+    IndexColorModel icm = new IndexColorModel(bitCount,
+        added.size(), cmap, 0, DataBuffer.TYPE_BYTE, null);
+
+    // Check if generated palette matched original
+    if (img.getColorModel() instanceof IndexColorModel) {
+      IndexColorModel originalModel = (IndexColorModel)img.getColorModel();
+      if (originalModel.getPixelSize() == icm.getPixelSize() &&
+          originalModel.getMapSize() == icm.getMapSize()) {
+        // Old model already had efficient palette
+        return null;
+      }
+    }
+
+    // Be careful to assign correctly assign byte packing method based on pixel size
+    BufferedImage dst =
+        new BufferedImage(img.getWidth(), img.getHeight(),
+            icm.getPixelSize() < 8 ? BufferedImage.TYPE_BYTE_BINARY :
+                BufferedImage.TYPE_BYTE_INDEXED, icm);
+
+    WritableRaster wr = dst.getRaster();
+    for (int y = 0; y < dst.getHeight(); y++) {
+      for (int x = 0; x < dst.getWidth(); x++) {
+        wr.setSample(x, y, 0, added.get(img.getRGB(x, y)));
+      }
+    }
+    return dst;
+  }
+
+  /**
+   * Convert an image to a direct color map
+   */
+  public static BufferedImage depalettize(BufferedImage img, int maxBytes) {
+    // Even is we use an RGB buffer instead of RGBA it still uses 4 bytes in memory
+    if (img.getWidth() * img.getHeight() * 4 > maxBytes) {
+      // Image is too large to depalettize
+      return null;
+    }
+    ColorModel colorModel = img.getColorModel();
+    if (colorModel instanceof IndexColorModel) {
+      IndexColorModel indexModel = (IndexColorModel)colorModel;
+      return indexModel.convertToIntDiscrete(img.getData(), false);
+    }
+    return null;
+  }
+
+
+  /**
+   * Check if an image is completely opaque
+   */
+  public static boolean isOpaque(BufferedImage img) {
+    for (int x = 0; x < img.getWidth(); x++) {
+      for (int y = 0; y < img.getHeight(); y++) {
+        if ((img.getRGB(x,y) & 0xff000000) != 0xff000000) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Convenience method that returns a scaled instance of the
+   * provided {@code BufferedImage}.
+   *
+   * NOTE: Adapted from code at
+   * http://today.java.net/pub/a/today/2007/04/03/perils-of-image-getscaledinstance.html
+   *
+   * @param img the original image to be scaled
+   * @param targetWidth the desired width of the scaled instance,
+   *    in pixels
+   * @param targetHeight the desired height of the scaled instance,
+   *    in pixels
+   * @param hint one of the rendering hints that corresponds to
+   *    {@code RenderingHints.KEY_INTERPOLATION} (e.g.
+   *    {@code RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR},
+   *    {@code RenderingHints.VALUE_INTERPOLATION_BILINEAR},
+   *    {@code RenderingHints.VALUE_INTERPOLATION_BICUBIC})
+   * @param higherQuality if true, this method will use a multi-step
+   *    scaling technique that provides higher quality than the usual
+   *    one-step technique (only useful in downscaling cases, where
+   *    {@code targetWidth} or {@code targetHeight} is
+   *    smaller than the original dimensions, and generally only when
+   *    the {@code BILINEAR} hint is specified)
+   * @return a scaled version of the original {@code BufferedImage}
+   */
+  public static BufferedImage getScaledInstance(BufferedImage img,
+      int targetWidth,
+      int targetHeight,
+      Object hint,
+      boolean higherQuality,
+      int imageType) {
+    BufferedImage ret = img;
+    int w, h;
+    if (higherQuality) {
+      // Use multi-step technique: start with original size, then
+      // scale down in multiple passes with drawImage()
+      // until the target size is reached
+      w = img.getWidth();
+      h = img.getHeight();
+    } else {
+      // Use one-step technique: scale directly from original
+      // size to target size with a single drawImage() call
+      w = targetWidth;
+      h = targetHeight;
+    }
+
+    do {
+      if (higherQuality && w > targetWidth) {
+        w /= 2;
+        if (w < targetWidth) {
+          w = targetWidth;
+        }
+      }
+
+      if (higherQuality && h > targetHeight) {
+        h /= 2;
+        if (h < targetHeight) {
+          h = targetHeight;
+        }
+      }
+
+      BufferedImage tmp = new BufferedImage(w, h, imageType);
+      Graphics2D g2 = tmp.createGraphics();
+      g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint);
+      g2.drawImage(ret, 0, 0, w, h, null);
+      g2.dispose();
+
+      ret = tmp;
+    } while (w > targetWidth || h > targetHeight);
+
+    return ret;
+  }
+}
+
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java
new file mode 100644
index 0000000..0e16706
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizer.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import javax.imageio.ImageIO;
+import java.awt.color.ICC_Profile;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Optimize JPEG images by either converting them to PNGs or re-encoding them with a more
+ * appropriate compression level.
+ */
+public class JPEGOptimizer extends BaseOptimizer {
+
+  public static BufferedImage readJpeg(InputStream is)
+      throws ImageReadException, IOException {
+    byte[] bytes = IOUtils.toByteArray(is);
+    // We cant use Sanselan to read JPEG but we can use it to read all the metadata which is
+    // where most security issues reside anyway in ImageIO
+    Sanselan.getMetadata(bytes, null);
+    byte[] iccBytes = Sanselan.getICCProfileBytes(bytes);
+    if (iccBytes != null && iccBytes.length > 0) {
+      ICC_Profile iccProfile = Sanselan.getICCProfile(bytes, null);
+      if (iccProfile == null) {
+        throw new ImageReadException("Image has ICC but it is corrupt and cannot be read");
+      }
+    }
+    return ImageIO.read(new ByteArrayInputStream(bytes));
+  }
+
+  private boolean usePng;
+
+  public JPEGOptimizer(OptimizerConfig config, HttpResponseBuilder response) {
+    super(config, response);
+  }
+
+  public JPEGOptimizer(OptimizerConfig config, HttpResponseBuilder response,
+                       JpegImageUtils.JpegImageParams sourceImageParams) {
+    super(config, response, sourceImageParams);
+  }
+
+  @Override
+  protected void rewriteImpl(BufferedImage image) throws IOException {
+    int pngLength = Integer.MAX_VALUE;
+    if (config.isJpegConversionAllowed()) {
+      // Create a new optimizer config and disable JPEG conversion
+      OptimizerConfig pngConfig = new OptimizerConfig(config.getMaxInMemoryBytes(),
+          config.getMaxPaletteSize(), false, config.getJpegCompression(),
+          config.getMinThresholdBytes(), config.getJpegHuffmanOptimization(),
+          config.getJpegRetainSubsampling());
+
+      // Output the image as PNG
+      PNGOptimizer pngOptimizer = new PNGOptimizer(pngConfig, response);
+      pngOptimizer.rewriteImpl(image);
+
+      if (pngOptimizer.getRewrittenImage()  != null) {
+        // PNG was better than original so use it
+        minBytes = pngOptimizer.getRewrittenImage();
+        minLength = minBytes.length;
+        pngLength = minLength;
+      }
+    }
+
+    // Write as standard JPEG using the configured default compression level
+    write(image);
+
+    // JPEG did not beat PNG
+    if (pngLength == minLength) {
+      usePng = true;
+    }
+  }
+
+  @Override
+  protected String getOutputContentType() {
+    if (usePng) {
+      return "image/png";
+    }
+    return "image/jpeg";
+  }
+
+  @Override
+  protected String getOriginalContentType() {
+    return "image/jpeg";
+  }
+
+  @Override
+  protected String getOriginalFormatName() {
+    return "jpeg";
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java
new file mode 100644
index 0000000..5594cc7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtils.java
@@ -0,0 +1,388 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.common.BinaryFileParser;
+import org.apache.sanselan.common.byteSources.ByteSourceInputStream;
+import org.apache.sanselan.formats.jpeg.JpegUtils;
+import org.apache.sanselan.formats.jpeg.JpegConstants;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.imageio.plugins.jpeg.JPEGQTable;
+
+/**
+ * Utility functions for jpeg image introspection.
+ */
+public class JpegImageUtils {
+  private static final Logger LOG = Logger.getLogger(ImageUtils.class.getName());
+  private static final int END_OF_IMAGE_MARKER = 0xffd9;
+  private static final String INVALID_JPEG_ERROR_MSG = "Not a Valid JPEG File";
+  private static final int HUFFMAN_TABLE_MARKER = 0xffc4;
+  private static final int QUANTIZATION_TABLE_MARKER = 0xffdb;
+  private static final int MAX_DC_SYMBOLS = 12;
+  private static final int MAX_AC_SYMBOLS = 162;
+
+  private JpegImageUtils() {}
+
+  /**
+   * Various subsampling modes supported by Jpeg and the corresponding values for
+   * this integer.
+   *   4:4:4 subsampling -> 0x11 -> 17
+   *   4:2:2 subsampling -> 0x21 -> 33
+   *   4:2:0 subsampling -> 0x22 -> 34
+   *   4:1:1 subsampling -> 0x41 -> 65
+   */
+  public static enum SamplingModes {
+    UNKNOWN(-2),
+    DEFAULT(-1),
+    YUV444(17),
+    YUV422(33),
+    YUV420(34),
+    YUV411(65);
+
+    private SamplingModes(int mode) {
+      this.mode = mode;
+    }
+
+    public int getModeValue() {
+      return mode;
+    }
+
+    private int mode;
+  }
+
+  public static class JpegImageParams {
+    private SamplingModes mode;
+    private boolean huffmanOptimized;
+    private float approxQualityFactor;
+    private float lumaQualityFactor = -1;
+    private float chromaQualityFactor = -1;
+
+    private final int[] k1LumaQuantTable = JPEGQTable.K1Luminance.getTable();
+    private final int[] k2ChromaQuantTable = JPEGQTable.K2Chrominance.getTable();
+
+    private int[][] tables = new int[2][64];
+    private int lumaIndex = -1;
+    private int chromaIndex = -1;
+
+    JpegImageParams(SamplingModes mode, boolean huffmanOptimized, float approxQualityFactor) {
+      this.mode = mode;
+      this.huffmanOptimized = huffmanOptimized;
+      this.approxQualityFactor = approxQualityFactor;
+    }
+
+    public SamplingModes getSamplingMode() {
+      return mode;
+    }
+
+    public void setSamplingMode(int samplingMode) {
+      for (SamplingModes mode : SamplingModes.values()) {
+        if (samplingMode == mode.getModeValue()) {
+          this.mode = mode;
+          return;
+        }
+      }
+
+      mode = SamplingModes.UNKNOWN;
+      LOG.log(Level.WARNING, "Unable to read subsampling information for Jpeg Image");
+    }
+
+    public boolean isHuffmanOptimized() {
+      return huffmanOptimized;
+    }
+
+    public void setHuffmanOptimized(boolean huffmanOptimized) {
+      this.huffmanOptimized = huffmanOptimized;
+    }
+
+    public void setLumaIndex(int index) {
+      this.lumaIndex = index;
+    }
+
+    public void setChromaIndex(int index) {
+      this.chromaIndex = index;
+    }
+
+    /**
+     * Quality is defined in terms of the base quantization tables used by encoder.
+     * Q = quant table, q = compression quality  and S = table used by encoder,
+     * Encoder does the following.
+     * if q > 0.5 then Q = 2 - 2*q*S otherwise Q = (0.5/q)*S.
+     *
+     * Since we dont have access to the table used by encoder. But it is generally close
+     * to the standard table defined by JPEG. Hence, we approx by taking sum of all values
+     * of the standard JPEG table and comparing with sum of all values of quant table.
+     *
+     * @param table quantization table specified in the jpeg header.
+     * @param stdTable reference quantization table specified in jpeg standard.
+     * @return approximate compression quality which lies in interval [0.0, 1.0].
+     */
+    public float approximateQuality(int[] table, int[] stdTable) {
+      int total = 0;
+      int stdTotal = 0;
+      for (int i = 0; i < 64; i++) {
+        total += table[i];
+        stdTotal += stdTable[i];
+      }
+
+      float scaleFactor = (total - 32F)/stdTotal;
+
+      float approxChannelQuality;
+      if (scaleFactor > 1.0) {
+        approxChannelQuality = 0.5F / scaleFactor;
+      } else {
+        approxChannelQuality = (2.0F - scaleFactor) / 2.0F;
+      }
+      return approxChannelQuality;
+    }
+
+    /**
+     * Adds quantization table to image data.
+     *
+     * @param tableIndex quantization table index.
+     * @param table quantization table that is used in while encoding.
+     */
+    public void addQTable(int tableIndex, int[] table) {
+      if (tableIndex == 0 || tableIndex == 1) {
+        System.arraycopy(table, 0, tables[tableIndex], 0, table.length);
+      }
+    }
+
+    public float getChromaQualityFactor() {
+      if (chromaQualityFactor < 0 && chromaIndex >= 0) {
+        chromaQualityFactor = approximateQuality(tables[chromaIndex], k2ChromaQuantTable);
+      }
+      return chromaQualityFactor;
+    }
+
+    public float getLumaQualityFactor() {
+     if (lumaQualityFactor < 0 && lumaIndex >= 0) {
+        lumaQualityFactor = approximateQuality(tables[lumaIndex], k1LumaQuantTable);
+      }
+      return lumaQualityFactor;
+    }
+
+    public float getApproxQualityFactor() {
+      if (approxQualityFactor < 0) {
+        approxQualityFactor = (getLumaQualityFactor() + 2 * getChromaQualityFactor()) / 3.0F;
+      }
+
+      return approxQualityFactor;
+    }
+  }
+
+  /**
+   * This function tries to extract various information from jpeg image like subsampling, jpeg
+   * compression quality and whether huffman optimzation is applied on the image data.
+   *
+   * @param is input stream comprisng the image data.
+   * @param filename of the image.
+   */
+  public static JpegImageParams getJpegImageData(InputStream is, String filename)
+      throws IOException, ImageReadException {
+    final JpegImageParams imageParams = new JpegImageParams(SamplingModes.UNKNOWN, false, -1);
+
+    JpegUtils.Visitor visitor = new JpegUtils.Visitor() {
+      BinaryFileParser binaryParser = new BinaryFileParser();
+
+      // return false to exit before reading image data.
+      public boolean beginSOS() {
+        return false;
+      }
+
+      public void visitSOS(int marker, byte markerBytes[],
+          byte imageData[]) {
+      }
+
+      // return false to exit traversal.
+      public boolean visitSegment(int marker, byte markerBytes[], int markerLength,
+          byte markerLengthBytes[], byte segmentData[]) throws ImageReadException, IOException {
+
+        if (marker == END_OF_IMAGE_MARKER)
+          return false;
+
+        if ((marker == JpegConstants.SOF0Marker) || (marker == JpegConstants.SOF2Marker)) {
+          parseSOFSegment(markerLength, segmentData);
+        } else if (marker == HUFFMAN_TABLE_MARKER) {
+          parseHuffmanTables(markerLength, segmentData);
+        } else if (marker == QUANTIZATION_TABLE_MARKER) {
+          parseQuantizationTables(markerLength, segmentData);
+        }
+
+        return true;
+      }
+
+      /**
+       * This function tries to extract the subsampling information from the JPEG image using
+       * either 'SOF0' or 'SOF2' segment.
+       * The structure of the 'SOF' marker is as follows.
+       *   - data precision (1 byte) in bits/sample,
+       *   - image height (2 bytes, little endian),
+       *   - image width (2 bytes, little endian),
+       *   - number of components (1 byte), usually 1 = grey scaled, 3 = color YCbCr
+       *     or YIQ, 4 = color CMYK)
+       *   - for each component: 3 bytes
+       *     - component id (1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q)
+       *     - sampling factors (bit 0-3 vertical sampling, 4-7 horizontal sampling)
+       *     - quantization table index
+       *
+       * @param markerLength length of the SOF marker.
+       * @param segmentData actual bytes representing the segment.
+       */
+      private void parseSOFSegment(int markerLength, byte[] segmentData)
+          throws IOException, ImageReadException {
+        // parse the SOF Marker.
+        int toBeProcessed = markerLength - 2;
+        int numComponents = 0;
+        InputStream is = new ByteArrayInputStream(segmentData);
+
+        // Skip precision(1 Byte), height(2 Bytes), width(2 bytes) bytes.
+        if (toBeProcessed > 6) {
+          binaryParser.skipBytes(is, 5, INVALID_JPEG_ERROR_MSG);
+          numComponents = binaryParser.readByte("Number_of_components", is,
+              "Unable to read Number of components from SOF marker");
+          toBeProcessed -= 6;
+        } else {
+          LOG.log(Level.WARNING, "Failed to SOF marker");
+          return;
+        }
+
+        // TODO(satya): Extend this library to gray scale images.
+        if (numComponents == 3 && toBeProcessed == 9) {
+          // Process 'Luma' Channel.
+          // Skipping the component Id field.
+          binaryParser.skipBytes(is, 1, INVALID_JPEG_ERROR_MSG);
+          imageParams.setSamplingMode(binaryParser.readByte("Sampling Factors", is,
+              "Unable to read the sampling factor from the 'Y' channel component spec"));
+          imageParams.setLumaIndex(binaryParser.readByte("Quantization Table Index", is,
+              "Unable to read Quantization table index of 'Y' channel"));
+
+          // Process 'Chroma' Channel.
+          // Skipping the component Id and sampling factor fields.
+          binaryParser.skipBytes(is, 2, INVALID_JPEG_ERROR_MSG);
+          imageParams.setChromaIndex(binaryParser.readByte("Quantization Table Index", is,
+              "Unable to read Quantization table index of 'Cb' Channel"));
+        } else {
+          LOG.log(Level.WARNING, "Failed to Component Spec from SOF marker");
+        }
+      }
+
+
+      /**
+       * This function tries to parse the Quantizations tables and adds them to JpegImageData
+       * object. If segmentData has more bytes after parsing first QT, that means DQT segment has
+       * multiple quantization tables. We allow multiple quant tables to have same tableIndex,
+       * and the latter one overrides the previous one. we currently parse upto 2 quantization
+       * tables.
+       * The structure of the DQT (Define Quantization Table) segment.
+       *   - QT information (1 byte): (bit 0 = LSB and bit 7 = MSB)
+       *     bit 3..0: index of QT (3..0, otherwise error)
+       *     bit 7..4: precision of QT, 0 means 8 bit, 1 means 16 bit, otherwise bad input
+       *   - n bytes QT values, n = 64*(precision+1)
+       *
+       * @param markerLength length of the DQT marker.
+       * @param segmentData actual bytes representing the segment.
+       */
+      private void parseQuantizationTables(int markerLength, byte[] segmentData)
+          throws ImageReadException, IOException {
+        InputStream is = new ByteArrayInputStream(segmentData);
+        int toBeProcessed = markerLength - 2;
+        while (toBeProcessed > 1) {
+          int tableInfo = binaryParser.readByte("Quantization Table Info", is,
+                                                "Not able to read Quantization Table Info");
+          toBeProcessed--;
+          int tableIndex = tableInfo & 0x0f;
+          int precision = tableInfo >> 4;
+          if (toBeProcessed < 64*(precision + 1)) {
+            return;
+          }
+
+          int[] quanTable = new int[64];
+          for (int i = 0; i < 64; i++) {
+            quanTable[i] = (precision == 0) ?
+                binaryParser.readByte("Reading", is, "Reading Quanization Table Failed") :
+                binaryParser.read2Bytes("Reading", is, "Reading Quantization Table Failed");
+          }
+          imageParams.addQTable(tableIndex, quanTable);
+          toBeProcessed -= 64*(precision + 1);
+        }
+      }
+
+      /**
+       * This functions parses the huffman table and try to figure out if huffman
+       * optimizations are applied on the image or not. If segmentData has more bytes after
+       * parsing first HT, that means DHT segment has multiple huffman tables.
+       * Structure of DHT (Define Huffman Table) segment.
+       *   - HT information (1 byte): (bit 0 = LSB and bit 7 = MSB)
+       *     bit 3..0: index of HT (3..0, otherwise error)
+       *     bit 4   : type of HT, 0 = DC table, 1 = AC table
+       *     bit 7..5: not used, must be 0
+       *   - 16 bytes: number of symbols with codes of length 1..16, the sum of these
+       *     bytes is the total number of codes, which must be <= 256
+       *   - n bytes: table containing the symbols in order of increasing code length
+       *     (n = total number of codes)
+       *
+       * @param markerLength length of the DHT marker.
+       * @param segmentData actual bytes representing the segment.
+       */
+      private void parseHuffmanTables(int markerLength, byte[] segmentData)
+          throws ImageReadException, IOException {
+        InputStream is = new ByteArrayInputStream(segmentData);
+
+        int toBeProcessed = markerLength -2;
+        while (toBeProcessed > 1) {
+          // Reading the table info byte.
+          int tableInfo = binaryParser.readByte("Huffman Table Info", is,
+                                                "Not able to read Huffman Table Info");
+          toBeProcessed--;
+
+          // Reading the counts of symbols from length 1...16.
+          if (toBeProcessed < 16) {
+            return;
+          }
+          int numSymbols =0;
+          for (int i = 0; i < 16; i++) {
+            numSymbols += binaryParser.readByte("Num symbols", is,
+                                                "Not able to read num symbols");
+          }
+          toBeProcessed -= 16 + numSymbols;
+
+          // It is highly unlikely that a huffman optimized image has same number of
+          // symbols as the standard huffman table. So, if DC tables has less than 12 symbols
+          // (OR) an AC table has less than 162 symbols it is most likely optimized.
+          int tableType = (tableInfo>>4) & 1;
+          if ((tableType == 0 && numSymbols != MAX_DC_SYMBOLS) ||
+              (tableType == 1 && numSymbols != MAX_AC_SYMBOLS)) {
+            imageParams.setHuffmanOptimized(true);
+            return;
+          }
+        }
+      }
+    };
+
+    new JpegUtils().traverseJFIF(new ByteSourceInputStream(is, filename), visitor);
+    return imageParams;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java
new file mode 100644
index 0000000..2886ad7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfig.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Configuration settings for the optimizer
+ */
+public class OptimizerConfig {
+
+  private final int maxInMemoryBytes;
+  private final int maxPaletteSize;
+  private final boolean jpegConversionAllowed;
+  private final float jpegCompression;
+  private final int minThresholdBytes;
+  private final boolean jpegHuffmanOptimization;
+  private final boolean jpegRetainSubsampling;
+
+  @Inject
+  public OptimizerConfig(
+      @Named("shindig.image-rewrite.max-inmem-bytes") int maxInMemoryBytes,
+      @Named("shindig.image-rewrite.max-palette-size") int maxPaletteSize,
+      @Named("shindig.image-rewrite.allow-jpeg-conversion") boolean jpegConversionAllowed,
+      @Named("shindig.image-rewrite.jpeg-compression") float jpegCompression,
+      @Named("shindig.image-rewrite.min-threshold-bytes") int minThresholdBytes,
+      @Named("shindig.image-rewrite.jpeg-huffman-optimization") boolean jpegHuffmanOptimization,
+      @Named("shindig.image-rewrite.jpeg-retain-subsampling") boolean jpegRetainSubsampling) {
+    this.maxInMemoryBytes = maxInMemoryBytes;
+    this.maxPaletteSize = maxPaletteSize;
+    this.jpegConversionAllowed = jpegConversionAllowed;
+    // Constrain jpeg compression to between 0.9 and 0.5 so its not pointless
+    // to attempt nor is it too lossy.
+    this.jpegCompression = Math.min(0.9f,Math.max(0.5f, jpegCompression));
+    this.minThresholdBytes = minThresholdBytes;
+    this.jpegHuffmanOptimization = jpegHuffmanOptimization;
+    this.jpegRetainSubsampling = jpegRetainSubsampling;
+  }
+
+  /**
+   * Defaults for usage in tests.
+   */
+  public OptimizerConfig() {
+    this(1024 * 1024, 256, true, 0.90f, 200, false, false);
+  }
+
+  /**
+   * The maximum allowed in-memory size of a parsed image. Used to protect system
+   * from very large memory allocations
+   */
+  public int getMaxInMemoryBytes() {
+    return maxInMemoryBytes;
+  }
+
+  /**
+   * The maximum no. of palette entries to create when attempting to palettize an
+   * image before quitting.
+   */
+  public int getMaxPaletteSize() {
+    return maxPaletteSize;
+  }
+
+  /**
+   * Allow conversion from and to JPEG for other image types that are fully opaque.
+   */
+  public boolean isJpegConversionAllowed() {
+    return jpegConversionAllowed;
+  }
+
+  /**
+   * The compression ratio to use when compressing JPEG images
+   * A value between 0.5 and 0.9.
+   */
+  public float getJpegCompression() {
+    return jpegCompression;
+  }
+
+  /**
+   * The threshold in bytes below which we do not attempt to rewite
+   * an image. Value should be chosen based on knowledge of MTU sizes
+   */
+  public int getMinThresholdBytes() {
+    return minThresholdBytes;
+  }
+
+  /**
+   * Indicate if we want to do huffman optimization while enocding the jpeg's.
+   */
+  public boolean getJpegHuffmanOptimization() {
+    return jpegHuffmanOptimization;
+  }
+
+  /**
+   * Indicate if we want to do retian original jpeg subsampling while encoding the jpeg's.
+   */
+  public boolean getJpegRetainSubsampling() {
+    return jpegRetainSubsampling;
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/PNGOptimizer.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/PNGOptimizer.java
new file mode 100644
index 0000000..aaa2270
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/image/PNGOptimizer.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import java.awt.image.BufferedImage;
+import java.awt.image.ColorModel;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Optimize a PNG image and possibly convert it to a JPEG.
+ */
+class PNGOptimizer extends BaseOptimizer {
+
+  public static BufferedImage readPng(InputStream is)
+      throws ImageReadException, IOException {
+    return Sanselan.getBufferedImage(is);
+  }
+
+  private boolean useJpeg;
+
+  public PNGOptimizer(OptimizerConfig config, HttpResponseBuilder response) {
+    super(config, response);
+  }
+
+  @Override
+  protected void rewriteImpl(BufferedImage bufferedImage) throws IOException {
+    BufferedImage palettized = ImageUtils.palettize(bufferedImage, config.getMaxPaletteSize());
+    if (palettized != null) {
+      write(palettized);
+    }
+
+    if (palettized == null) {
+      // If we are efficiently palletized then only JPEG can really win
+      if  (this.minBytes == null) {
+        // nothing has been written yet, so just strip metadata
+        write(bufferedImage);
+      }
+
+      // Depalettized images can win when a large palette has unused entries
+      BufferedImage depalettized = ImageUtils.depalettize(bufferedImage,
+          config.getMaxInMemoryBytes());
+      if (depalettized != null) {
+        write(depalettized);
+      }
+    }
+
+    // Try JPEG for truly opaque images
+    if (config.isJpegConversionAllowed()) {
+      boolean isOpaque;
+      if (palettized != null){
+        bufferedImage = palettized;
+        isOpaque = bufferedImage.getColorModel().getTransparency() == ColorModel.OPAQUE;
+      } else {
+        isOpaque = ImageUtils.isOpaque(bufferedImage);
+      }
+
+      if (isOpaque) {
+        byte[] lastBytes = minBytes;
+        int prevReductionPct = reductionPct;
+
+        // Workaround for bug in JPEG image writer
+        // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6444933
+        // Writer seems to think color space is CMYK and not RGBA. In this
+        // case the image is fully opaque so we can just down-convert to just RGB.
+        BufferedImage rgbOnlyImage = new BufferedImage(bufferedImage.getWidth(),
+            bufferedImage.getHeight(),
+            BufferedImage.TYPE_INT_RGB);
+        rgbOnlyImage.getGraphics().drawImage(bufferedImage, 0, 0, null);
+
+        JPEGOptimizer jpegOptimizer = new JPEGOptimizer(config, response);
+        outputter = jpegOptimizer.getOutputter();
+        write(rgbOnlyImage);
+        // Only use JPEG if it offers a significant reduction over other methods
+        if (reductionPct > prevReductionPct + 20) {
+          useJpeg = true;
+        } else {
+          minBytes = lastBytes;
+        }
+      }
+    }
+  }
+
+  @Override
+  protected String getOutputContentType() {
+    if (useJpeg) {
+      return "image/jpeg";
+    }
+    return "image/png";
+  }
+
+  @Override
+  protected String getOriginalContentType() {
+    return "image/png";
+  }
+
+  @Override
+  protected String getOriginalFormatName() {
+    return "png";
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/ClosureJsCompiler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/ClosureJsCompiler.java
new file mode 100644
index 0000000..fab4151
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/ClosureJsCompiler.java
@@ -0,0 +1,393 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.js;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.js.JsContent;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.js.JsResponseBuilder;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.javascript.jscomp.BasicErrorManager;
+import com.google.javascript.jscomp.CheckLevel;
+import com.google.javascript.jscomp.CommandLineRunner;
+import com.google.javascript.jscomp.CompilationLevel;
+import com.google.javascript.jscomp.Compiler;
+import com.google.javascript.jscomp.CompilerOptions;
+import com.google.javascript.jscomp.ErrorManager;
+import com.google.javascript.jscomp.JSError;
+import com.google.javascript.jscomp.Result;
+import com.google.javascript.jscomp.SourceFile;
+
+@Singleton
+public class ClosureJsCompiler implements JsCompiler {
+  // Default stack size for the compiler threads. The value was copied from closure compiler class.
+  private static final long DEFAULT_COMPILER_STACK_SIZE = 1048576L;
+
+  // Based on Closure Library's goog.exportSymbol implementation.
+  private static final JsContent EXPORTSYMBOL_CODE =
+      JsContent.fromText("var goog=goog||{};goog.exportSymbol=function(name,obj){"
+              + "var parts=name.split('.'),cur=window,part;"
+              + "for(;parts.length&&(part=parts.shift());){if(!parts.length){"
+              + "cur[part]=obj;}else{cur=cur[part]||(cur[part]={})}}};", "[goog.exportSymbol]");
+
+  //class name for logging purpose
+  private static final String classname = ClosureJsCompiler.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname);
+
+  @VisibleForTesting
+  static final String CACHE_NAME = "CompiledJs";
+
+  private final DefaultJsCompiler defaultCompiler;
+  private final Cache<String, CompileResult> cache;
+  private final List<SourceFile> defaultExterns;
+  private final String compileLevel;
+  private final Map<String, Future<CompileResult>> compiling;
+
+  private int threadPoolSize = 5;
+  private long compilerStackSize = DEFAULT_COMPILER_STACK_SIZE;
+  private ExecutorService compilerPool;
+
+  @Inject
+  public ClosureJsCompiler(DefaultJsCompiler defaultCompiler, CacheProvider cacheProvider,
+          @Named("shindig.closure.compile.level") String level) {
+    this(defaultCompiler, cacheProvider, level, null);
+  }
+  
+  public ClosureJsCompiler(DefaultJsCompiler defaultCompiler, CacheProvider cacheProvider,
+          String level, ExecutorService executorService) {
+    this.cache = cacheProvider.createCache(CACHE_NAME);
+    this.defaultCompiler = defaultCompiler;
+    List<SourceFile> externs = null;
+    try {
+      externs = Collections.unmodifiableList(CommandLineRunner.getDefaultExterns());
+    } catch(IOException e) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.log(Level.WARNING, "Unable to load default closure externs: " + e.getMessage(), e);
+      }
+    }
+    defaultExterns = externs;
+
+    compileLevel = level.toLowerCase().trim();
+    if(executorService != null) {
+      compilerPool = executorService;
+    }else {
+      compilerPool = createThreadPool();
+    }
+    Map<String, Future<CompileResult>> map = Maps.newHashMap();
+    compiling = new ConcurrentHashMap<String, Future<CompileResult>>(map);
+  }
+
+  @Inject(optional = true)
+  public void setThreadPoolSize(
+      @Named("shindig.closure.compile.threadPoolSize") Integer threadPoolSize) {
+
+    if (threadPoolSize != null && threadPoolSize != this.threadPoolSize) {
+      ExecutorService compilerPool = this.compilerPool;
+
+      this.threadPoolSize = threadPoolSize;
+      this.compilerPool = createThreadPool();
+
+      compilerPool.shutdown();
+    }
+  }
+
+  @Inject(optional = true)
+  public void setCompilerStackSize(
+      @Named("shindig.closure.compile.compilerStackSize") Long compilerStackSize) {
+    if (compilerStackSize > 0L) {
+      this.compilerStackSize = compilerStackSize;
+    }
+  }
+
+  /**
+   * Override this to provide your own {@link ExecutorService}
+   *
+   * @return An {@link ExecutorService} to use for the compiler pool.
+   */
+  protected ExecutorService createThreadPool() {
+    ThreadFactory threadFactory = new ClosureJSThreadFactory();
+    return Executors.newFixedThreadPool(threadPoolSize, threadFactory);
+  }
+
+  public CompilerOptions defaultCompilerOptions() {
+    CompilerOptions result = new CompilerOptions();
+    if (compileLevel.equals("advanced")) {
+      CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(result);
+    }
+    else if (compileLevel.equals("whitespace_only")) {
+      CompilationLevel.WHITESPACE_ONLY.setOptionsForCompilationLevel(result);
+    }
+    else {
+      // If 'none', this complier will not run, @see compile
+      CompilationLevel.SIMPLE_OPTIMIZATIONS.setOptionsForCompilationLevel(result);
+    }
+    return result;
+  }
+
+  @VisibleForTesting
+  protected CompilerOptions getCompilerOptions(JsUri uri) {
+    CompilerOptions options = defaultCompilerOptions();
+    return options;
+  }
+
+  public JsResponse compile(JsUri jsUri, Iterable<JsContent> content, String externs) {
+    JsResponseBuilder builder = new JsResponseBuilder();
+
+    CompilerOptions options = getCompilerOptions(jsUri);
+    StringBuilder compiled = new StringBuilder();
+    StringBuilder exports = new StringBuilder();
+    boolean useExterns = compileLevel.equals("advanced");
+    if (!useExterns) {
+      /*
+       * Kicking the can down the road.  Advanced optimizations doesn't currently work with the closure compiler in shindig.
+       * When it's fixed, we need to make sure all externs are included (not just externs for what was requested) otherwise
+       * the cache key will fluctuate with the url hit, and we will get massive cache churn and possible DDOS scenarios
+       * when we recompile all requested modules on the fly because the cache key was different.
+       */
+      externs = "";
+    }
+
+    // Add externs export to the list if set in options.
+    if (options.isExternExportsEnabled()) {
+      List<JsContent> allContent = Lists.newLinkedList(content);
+      allContent.add(EXPORTSYMBOL_CODE);
+      content = allContent;
+    }
+
+    try {
+      List<Future<CompileResult>> futures = Lists.newLinkedList();
+
+      // Process each content for work
+      for (JsContent code : content) {
+        JsResponse defaultCompiled = defaultCompiler.compile(jsUri, Lists.newArrayList(code), externs);
+
+        Future<CompileResult> future = null;
+        boolean compile = !code.isNoCompile() && !compileLevel.equals("none");
+        /*
+         *  isDebug usually will turn off all compilation, however, setting
+         *  isExternExportsEnabled and specifying an export path will keep the
+         *  closure compiler on and export the externs for debugging.
+         */
+        compile = compile && (!jsUri.isDebug() || options.isExternExportsEnabled());
+        if (compile) { // We should compile this code segment.
+          String cacheKey = makeCacheKey(defaultCompiled.toJsString(), externs, jsUri, options);
+
+          synchronized (compiling) {
+            CompileResult cached = cache.getElement(cacheKey);
+            if (cached == null) {
+              future = compiling.get(cacheKey);
+              if (future == null) {
+                // Don't pound on the compiler. Let the first thread queue the work,
+                // the rest of them will just wait on the futures later.
+                future = getCompileFuture(cacheKey, code, jsUri, externs);
+                compiling.put(cacheKey, future);
+              }
+            } else {
+              future = Futures.immediateFuture(cached);
+            }
+          }
+        }
+
+        if (future == null) {
+          future = Futures.immediateFuture(new CompileResult(code.get()));
+        }
+        futures.add(future);
+      }
+
+      // Wait on all work to be done.
+      for (Future<CompileResult> future : futures) {
+        CompileResult result = future.get();
+        compiled.append(result.getContent());
+        if (useExterns) {
+          String export = result.getExternExport();
+          if (export != null) {
+            exports.append(export);
+          }
+        }
+      }
+
+    } catch (Exception e) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.log(Level.WARNING, e.getMessage(), e);
+      }
+      Throwable cause = e.getCause();
+      if (cause instanceof CompilerException) {
+        return returnErrorResult(builder, HttpResponse.SC_NOT_FOUND, ((CompilerException)cause).getErrors());
+      } else {
+        return returnErrorResult(builder, HttpResponse.SC_NOT_FOUND, Lists.newArrayList(e.getMessage()));
+      }
+    }
+
+    builder.appendJs(compiled.toString(), "[compiled]");
+    builder.clearExterns().appendRawExtern(exports.toString());
+    return builder.build();
+  }
+
+  protected Future<CompileResult> getCompileFuture(final String cacheKey, final JsContent content,
+      final JsUri jsUri, final String externs) {
+
+    return compilerPool.submit(new Callable<CompileResult>() {
+      @Override
+      public CompileResult call() throws Exception {
+        // Create the options anew. Passing in the parent options, even cloning it, is not thread safe.
+        CompileResult result = doCompileContent(content, getCompilerOptions(jsUri), buildExterns(externs));
+        synchronized (compiling) {
+          // Other threads should pick this up in the cache now.
+          cache.addElement(cacheKey, result);
+          compiling.remove(cacheKey);
+        }
+
+        return result;
+      }
+    });
+  }
+
+  protected CompileResult doCompileContent(JsContent content, CompilerOptions options,
+      List<SourceFile> externs) throws CompilerException {
+
+    Compiler compiler = new Compiler(getErrorManager()); // We shouldn't reuse compilers
+
+    // disable JS Closure Compiler internal thread
+    compiler.disableThreads();
+
+    SourceFile source = SourceFile.fromCode(content.getSource(), content.get());
+    Result result = compiler.compile(externs, Lists.newArrayList(source), options);
+
+    if (result.errors.length > 0) {
+      throw new CompilerException(result.errors);
+    }
+
+    return new CompileResult(compiler, result);
+  }
+
+  protected List<SourceFile> buildExterns(String externs) {
+    List<SourceFile> allExterns = Lists.newArrayList();
+    allExterns.add(SourceFile.fromCode("externs", externs));
+    if (defaultExterns != null) {
+      allExterns.addAll(defaultExterns);
+    }
+    return allExterns;
+  }
+
+  private JsResponse returnErrorResult(
+      JsResponseBuilder builder, int statusCode, List<String> messages) {
+    builder.setStatusCode(statusCode);
+    builder.addErrors(messages);
+    JsResponse result = builder.build();
+    return result;
+  }
+
+  public Iterable<JsContent> getJsContent(JsUri jsUri, FeatureBundle bundle) {
+    jsUri = new JsUri(jsUri) {
+      @Override
+      public boolean isDebug() {
+        // Force debug JS in the raw JS content retrieved.
+        return true;
+      }
+    };
+    List<JsContent> builder = Lists.newLinkedList(defaultCompiler.getJsContent(jsUri, bundle));
+
+    CompilerOptions options = getCompilerOptions(jsUri);
+    if (options.isExternExportsEnabled()) {
+      List<String> exports = Lists.newArrayList(bundle.getApis(ApiDirective.Type.JS, true));
+      Collections.sort(exports);
+      String prevExport = null;
+      for (String export : exports) {
+        if (!export.equals(prevExport)) {
+          builder.add(JsContent.fromText(
+              "goog.exportSymbol('" + StringEscapeUtils.escapeEcmaScript(export) +
+              "', " + export + ");\n", "[export-symbol]"));
+          prevExport = export;
+        }
+      }
+    }
+    return builder;
+  }
+
+  protected String makeCacheKey(String code, String externs, JsUri uri, CompilerOptions options) {
+    // TODO: include compilation options in the cache key
+    return Joiner.on(":").join(
+        HashUtil.checksum(code.getBytes()),
+        HashUtil.checksum(externs.getBytes()),
+        uri.getCompileMode(),
+        uri.isDebug(),
+        options.isExternExportsEnabled());
+  }
+
+  private static ErrorManager getErrorManager() {
+    return new BasicErrorManager() {
+      @Override
+      protected void printSummary() { /* Do nothing */ }
+      @Override
+      public void println(CheckLevel checkLevel, JSError jsError) { /* Do nothing */ }
+    };
+  }
+
+  private class CompilerException extends Exception {
+    private static final long serialVersionUID = 1L;
+    private final JSError[] errors;
+    public CompilerException(JSError[] errors) {
+      this.errors = errors;
+    }
+
+    public List<String> getErrors() {
+      ImmutableList.Builder<String> builder = ImmutableList.builder();
+      for (JSError error : errors) {
+        builder.add(error.toString());
+      };
+
+      return builder.build();
+    }
+  }
+
+  private class ClosureJSThreadFactory implements ThreadFactory {
+    public Thread newThread(Runnable runnable) {
+      return new Thread(null, runnable, "shindigjscompiler", compilerStackSize);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/CompileResult.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/CompileResult.java
new file mode 100644
index 0000000..c5f2201
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/CompileResult.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.js;
+
+import java.io.Serializable;
+
+import com.google.javascript.jscomp.Compiler;
+import com.google.javascript.jscomp.Result;
+
+/**
+ * Serializable holder for cacheable compiler results.
+ */
+public class CompileResult implements Serializable {
+  private static final long serialVersionUID = 4824178999640746883L;
+
+  private String content;
+  private String externExport;
+
+  public CompileResult(Compiler compiler, Result result) {
+    content = compiler.toSource();
+    externExport = result.externExport;
+  }
+
+  public CompileResult(String content) {
+    this.content = content;
+    externExport = null;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public String getExternExport() {
+    return externExport;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/DefaultJsCompiler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/DefaultJsCompiler.java
new file mode 100644
index 0000000..b183fd8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/DefaultJsCompiler.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.js;
+
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.js.JsContent;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.js.JsResponseBuilder;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import java.util.List;
+
+/**
+ * Base for a JsCompiler implementation.
+ */
+public class DefaultJsCompiler implements JsCompiler {
+
+  public Iterable<JsContent> getJsContent(JsUri jsUri, FeatureBundle bundle) {
+    List<JsContent> jsContent = Lists.newLinkedList();
+    for (FeatureResource resource : bundle.getResources()) {
+      String content = getFeatureContent(jsUri, resource);
+      content = (content != null) ? content : "";
+      if (resource.isExternal()) {
+        // Support external/type=url feature serving through document.write()
+        jsContent.add(JsContent.fromFeature("document.write('<script src=\"" + content + "\"></script>')",
+            "[external:" + content + ']', bundle, resource));
+      } else {
+        jsContent.add(JsContent.fromFeature(content, resource.getName(), bundle, resource));
+      }
+      jsContent.add(JsContent.fromFeature(";\n", "[separator]", bundle, resource));
+    }
+    return jsContent;
+  }
+
+  public JsResponse compile(JsUri jsUri, Iterable<JsContent> content, String externs) {
+    return new JsResponseBuilder().appendAllJs(content).build();
+  }
+
+  protected String getFeatureContent(JsUri jsUri, FeatureResource resource) {
+    return jsUri.isDebug() ? resource.getDebugContent() : resource.getContent();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java
new file mode 100644
index 0000000..e95f356
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/rewrite/js/JsCompiler.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.js;
+
+import com.google.inject.ImplementedBy;
+
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.js.JsContent;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+/**
+ * Compiler to pre-process each feature independently and compile a
+ * concatenation of pre-processed data.
+ */
+@ImplementedBy(DefaultJsCompiler.class)
+public interface JsCompiler {
+
+  /**
+   * Pre-process feature JS.
+   * @param jsUri The JS uri making the request.
+   * @param bundle The feature bundle.
+   * @return Processed feature JS.
+   */
+  Iterable<JsContent> getJsContent(JsUri jsUri, FeatureBundle bundle);
+
+  /**
+   * Compiles the provided code with the provided list of external symbols.
+   * @param jsUri The JS uri making the request.
+   * @param content The raw/pre-processed JS code.
+   * @param externs The externs.
+   * @return A compilation result object.
+   */
+  JsResponse compile(JsUri jsUri, Iterable<JsContent> content, String externs);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java
new file mode 100644
index 0000000..f0f7680
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AccelHandler.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.AccelUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.UriCommon;
+import org.apache.shindig.gadgets.uri.UriUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Handles requests for accel servlet.
+ * The objective is to accelerate web pages.
+ *
+ * @since 2.0.0
+ */
+@Singleton
+public class AccelHandler {
+  private static final Logger logger = Logger.getLogger(
+      AccelHandler.class.getName());
+  static final String ERROR_FETCHING_DATA = "Error fetching data";
+  protected final RequestPipeline requestPipeline;
+  protected final ResponseRewriterRegistry contentRewriterRegistry;
+  protected final AccelUriManager uriManager;
+  protected final boolean remapInternalServerError;
+
+  @Inject
+  public AccelHandler(RequestPipeline requestPipeline,
+                      @RewriterRegistry(rewriteFlow = RewriteFlow.ACCELERATE)
+                      ResponseRewriterRegistry contentRewriterRegistry,
+                      AccelUriManager accelUriManager,
+                      @Named("shindig.accelerate.remapInternalServerError")
+                      Boolean remapInternalServerError) {
+    this.requestPipeline = requestPipeline;
+    this.contentRewriterRegistry = contentRewriterRegistry;
+    this.uriManager = accelUriManager;
+    this.remapInternalServerError = remapInternalServerError;
+  }
+
+  public HttpResponse fetch(HttpRequest request) throws IOException, GadgetException {
+    // TODO: Handle if modified since headers.
+
+    // Parse and normalize to get a proxied request uri.
+    ProxyUriManager.ProxyUri proxyUri = getProxyUri(request);
+
+    // Fetch the content of the requested uri.
+    HttpRequest req = buildHttpRequest(request, proxyUri);
+    HttpResponse results = requestPipeline.execute(req);
+
+    HttpResponse errorResponse = handleErrors(results);
+    if (errorResponse == null) {
+      // No error. Lets rewrite the content.
+      try {
+        results = contentRewriterRegistry.rewriteHttpResponse(req, results, null);
+      } catch (RewritingException e) {
+        logger.log(Level.WARNING, "Rewriting failed, serving original results", e);
+        // In case of exception continue using original results.
+      }
+    } else {
+      results = errorResponse;
+    }
+
+    // Copy the response headers and status code to the final http servlet
+    // response.
+    HttpResponseBuilder response = new HttpResponseBuilder();
+    UriUtils.copyResponseHeadersAndStatusCode(
+        results, response, remapInternalServerError, false,
+        UriUtils.DisallowedHeaders.OUTPUT_TRANSFER_DIRECTIVES,
+        UriUtils.DisallowedHeaders.AUTHENTICATION_DIRECTIVES);
+
+    // Override the content type of the final http response if the input request
+    // had the rewrite mime type header.
+    UriUtils.maybeRewriteContentType(req, response);
+
+    // Copy the content.
+    // TODO: replace this with streaming APIs when ready
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    IOUtils.copy(results.getResponse(), baos);
+    response.setResponseNoCopy(baos.toByteArray());
+    return response.create();
+  }
+
+  /**
+   * Returns the proxy uri encapsulating the request uri.
+   * @param httpRequest The http request.
+   * @return The proxy uri encapsulating the request uri.
+   * @throws GadgetException In case of errors.
+   */
+  public ProxyUriManager.ProxyUri getProxyUri(HttpRequest httpRequest) throws GadgetException {
+    Uri proxiedUri = uriManager.parseAndNormalize(httpRequest);
+    String uriString = proxiedUri.getQueryParameter(UriCommon.Param.URL.getKey());
+
+    // Throw BAD_GATEWAY in case parsing of url fails.
+    Uri requestedResource;
+    try {
+      requestedResource = Uri.parse(uriString);
+    } catch (Uri.UriException e) {
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+                                "Failed to parse uri: " + uriString,
+                                HttpResponse.SC_BAD_GATEWAY);
+    }
+
+    Gadget gadget = DomWalker.makeGadget(httpRequest);
+    ProxyUriManager.ProxyUri proxyUri = new ProxyUriManager.ProxyUri(gadget, requestedResource);
+    proxyUri.setHtmlTagContext(proxiedUri.getQueryParameter(
+        UriCommon.Param.HTML_TAG_CONTEXT.getKey()));
+    return proxyUri;
+  }
+
+  /**
+   * Build an HttpRequest object encapsulating the request details as requested
+   * by the user.
+   * @param httpRequest The http request.
+   * @param uriToProxyOrRewrite The parsed uri to proxy or rewrite through
+   *   accel servlet.
+   * @return Remote content request based on the parameters sent from the client.
+   * @throws GadgetException In case the data could not be fetched.
+   */
+  protected HttpRequest buildHttpRequest(HttpRequest httpRequest,
+                                         ProxyUriManager.ProxyUri uriToProxyOrRewrite)
+      throws GadgetException {
+    Uri tgt = uriToProxyOrRewrite.getResource();
+    HttpRequest req = uriToProxyOrRewrite.makeHttpRequest(tgt);
+    if (req == null) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "No url parameter in request", HttpResponse.SC_BAD_REQUEST);
+    }
+
+    // Copy the post body if it exists.
+    UriUtils.copyRequestData(httpRequest, req);
+
+    // Set and copy headers.
+    ServletUtil.setXForwardedForHeader(httpRequest, req);
+
+    UriUtils.copyRequestHeaders(
+        httpRequest, req,
+        UriUtils.DisallowedHeaders.POST_INCOMPATIBLE_DIRECTIVES,
+        UriUtils.DisallowedHeaders.HOST_HEADER);
+
+    // Since the Host header of httpRequest could be pointing to the shindig
+    // host (in case of a normalized request), we do not copy the Host header
+    // as is. Instead we explicitly set it to the authority of the resource
+    // being fetched as shown below.
+    req.setHeader("Host", tgt.getAuthority());
+
+    req.setFollowRedirects(false);
+    return req;
+  }
+
+  /**
+   * Process errors when fetching uri using request pipeline and return the
+   * error response to be returned to the user if any.
+   * @param results The http response returned by request pipeline.
+   * @return An HttpResponse instance encapsulating error message and status
+   *   code to be returned to the user in case of errors, null otherwise.
+   */
+  protected HttpResponse handleErrors(HttpResponse results) {
+    if (results == null) {
+      return new HttpResponseBuilder()
+          .setHttpStatusCode(HttpResponse.SC_NOT_FOUND)
+          .setResponse(ERROR_FETCHING_DATA.getBytes())
+          .create();
+    }
+    if (results.isError()) {
+      return results;
+    }
+
+    return null;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AuthenticationModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AuthenticationModule.java
new file mode 100644
index 0000000..5b1269c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/AuthenticationModule.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.collect.ImmutableList;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.apache.shindig.auth.AnonymousAuthenticationHandler;
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.auth.UrlParameterAuthenticationHandler;
+
+import java.util.List;
+
+/**
+ * Binds auth types used by gadget rendering. This should be used when running a stand-alone gadget
+ * renderer.
+ */
+public class AuthenticationModule extends AbstractModule {
+  @Override
+  protected void configure() {
+  }
+
+  @Provides
+  @Singleton
+  List<AuthenticationHandler> provideAuthenticationHandlers(UrlParameterAuthenticationHandler urlParameterAuthHandler,
+                                                        AnonymousAuthenticationHandler anonymoustAuthHandler) {
+      return ImmutableList.of(urlParameterAuthHandler, anonymoustAuthHandler);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java
new file mode 100644
index 0000000..0c3553d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/CajaContentRewriter.java
@@ -0,0 +1,427 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.ExternalReference;
+import com.google.caja.lexer.FetchedData;
+import com.google.caja.lexer.FilePosition;
+import com.google.caja.lexer.HtmlLexer;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.JsLexer;
+import com.google.caja.lexer.JsTokenQueue;
+import com.google.caja.lexer.ParseException;
+import com.google.caja.lexer.TokenConsumer;
+import com.google.caja.lexer.escaping.Escaping;
+import com.google.caja.parser.ParseTreeNode;
+import com.google.caja.parser.html.Dom;
+import com.google.caja.parser.html.DomParser;
+import com.google.caja.parser.js.CajoledModule;
+import com.google.caja.parser.js.Parser;
+import com.google.caja.plugin.Job;
+import com.google.caja.plugin.PipelineMaker;
+import com.google.caja.plugin.PluginCompiler;
+import com.google.caja.plugin.PluginMeta;
+import com.google.caja.plugin.LoaderType;
+import com.google.caja.plugin.UriEffect;
+import com.google.caja.plugin.UriFetcher;
+import com.google.caja.plugin.UriPolicy;
+import com.google.caja.plugin.UriFetcher.UriFetchException;
+import com.google.caja.render.Concatenator;
+import com.google.caja.render.JsMinimalPrinter;
+import com.google.caja.render.JsPrettyPrinter;
+import com.google.caja.reporting.BuildInfo;
+import com.google.caja.reporting.Message;
+import com.google.caja.reporting.MessageContext;
+import com.google.caja.reporting.MessageLevel;
+import com.google.caja.reporting.MessagePart;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.MessageType;
+import com.google.caja.reporting.RenderContext;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.caja.reporting.SnippetProducer;
+import com.google.caja.service.ServiceMessageType;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.apache.shindig.gadgets.parse.HtmlSerializer;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A GadgetRewriter based on caja technology
+ */
+public class CajaContentRewriter implements GadgetRewriter {
+  public static final String CAJOLED_MODULES = "cajoledModules";
+
+  //class name for logging purpose
+  private static final String CLASS_NAME = CajaContentRewriter.class.getName();
+  private static final Logger LOG = Logger.getLogger(CLASS_NAME, MessageKeys.MESSAGES);
+
+
+  private final Cache<ModuleCacheKey, ImmutableList<Job>> moduleCache;
+  private final RequestPipeline requestPipeline;
+  private final HtmlSerializer htmlSerializer;
+  private final ProxyUriManager proxyUriManager;
+
+  @Inject
+  public CajaContentRewriter(CacheProvider cacheProvider, RequestPipeline requestPipeline,
+                             HtmlSerializer htmlSerializer, ProxyUriManager proxyUriManager) {
+    if (null == cacheProvider) {
+      this.moduleCache = null;
+    } else {
+      this.moduleCache = cacheProvider.createCache(CAJOLED_MODULES);
+    }
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.logp(Level.FINE, CLASS_NAME, "CajaContentRewriter", MessageKeys.CAJOLED_CACHE_CREATED,
+               new Object[] {moduleCache});
+    }
+    this.requestPipeline = requestPipeline;
+    this.htmlSerializer = htmlSerializer;
+    this.proxyUriManager = proxyUriManager;
+  }
+
+  public class CajoledResult {
+    public final Node html;
+    public final CajoledModule js;
+    public final List<Message> messages;
+    public final boolean hasErrors;
+    CajoledResult(Node html, CajoledModule js, List<Message> messages, boolean hasErrors) {
+      this.html = html;
+      this.js = js;
+      this.messages = messages;
+      this.hasErrors= hasErrors;
+    }
+    @Override
+    public String toString() {
+      return "[html:'" + html + "', js: '" + js + "', messages: '" + messages + "']";
+    }
+  }
+
+  @VisibleForTesting
+  static ParseTreeNode parse(InputSource is, CharProducer cp, String mime, MessageQueue mq)
+      throws ParseException {
+    ParseTreeNode ptn;
+    if (mime.contains("javascript")) {
+      JsLexer lexer = new JsLexer(cp);
+      JsTokenQueue tq = new JsTokenQueue(lexer, is);
+      if (tq.isEmpty()) { return null; }
+      Parser p = new Parser(tq, mq);
+      ptn = p.parse();
+      tq.expectEmpty();
+    } else {
+      DomParser p = new DomParser(new HtmlLexer(cp), false, is, mq);
+      ptn = new Dom(p.parseFragment());
+      p.getTokenQueue().expectEmpty();
+    }
+    return ptn;
+  }
+
+  public CajoledResult rewrite(Uri uri, String container, String mime,
+      boolean es53, boolean debug) {
+    URI javaUri = uri.toJavaUri();
+    InputSource is = new InputSource(javaUri);
+    MessageQueue mq = new SimpleMessageQueue();
+    try {
+      UriFetcher fetcher = makeFetcher(uri, container);
+      ExternalReference extRef = new ExternalReference(javaUri,
+          FilePosition.instance(is, /*lineNo*/ 1, /*charInFile*/ 1, /*charInLine*/ 1));
+      // If the fetch fails, a UriFetchException is thrown and serialized as part of the
+      // message queue.
+      CharProducer cp = fetcher.fetch(extRef, mime).getTextualContent();
+      ParseTreeNode ptn = parse(is, cp, mime, mq);
+      return rewrite(uri, container, ptn, es53, debug);
+    } catch (UnsupportedEncodingException e) {
+      LOG.severe("Unexpected inability to recognize mime type: " + mime);
+      mq.addMessage(ServiceMessageType.UNEXPECTED_INPUT_MIME_TYPE,
+          MessagePart.Factory.valueOf(mime));
+    } catch (UriFetchException e) {
+      LOG.info("Failed to retrieve: " + e.toString());
+    } catch (ParseException e) {
+      mq.addMessage(MessageType.PARSE_ERROR, FilePosition.UNKNOWN);
+    }
+    return new CajoledResult(null, null, mq.getMessages(), /* hasErrors */ true);
+  }
+
+  public CajoledResult rewrite(Uri gadgetUri, String container,
+      ParseTreeNode root, boolean es53, boolean debug) {
+    UriFetcher fetcher = makeFetcher(gadgetUri, container);
+    UriPolicy policy = makePolicy(gadgetUri);
+    URI javaGadgetUri = gadgetUri.toJavaUri();
+    MessageQueue mq = new SimpleMessageQueue();
+    MessageContext context = new MessageContext();
+    PluginMeta meta = new PluginMeta(fetcher, policy);
+    PluginCompiler compiler = makePluginCompiler(meta, mq);
+    compiler.setMessageContext(context);
+    if (moduleCache != null) {
+      compiler.setJobCache(new ModuleCache(moduleCache));
+    }
+
+    if (debug) {
+      compiler.setGoals(compiler.getGoals()
+          .without(PipelineMaker.ONE_CAJOLED_MODULE)
+          .with(PipelineMaker.ONE_CAJOLED_MODULE_DEBUG));
+    }
+
+    compiler.addInput(root, javaGadgetUri);
+
+    boolean hasErrors = false;
+    if (!compiler.run()) {
+      hasErrors = true;
+    }
+
+    return new CajoledResult(compiler.getStaticHtml(),
+        compiler.getJavascript(),
+        compiler.getMessageQueue().getMessages(),
+        hasErrors);
+  }
+
+  public void rewrite(Gadget gadget, MutableContent mc) {
+    if (!gadget.requiresCaja()) return;
+
+    GadgetContext gadgetContext = gadget.getContext();
+    boolean debug = gadgetContext.getDebug();
+    Document doc = mc.getDocument();
+
+    // Serialize outside of MutableContent, to prevent a re-parse.
+    String docContent = HtmlSerialization.serialize(doc);
+    DocumentFragment root = doc.createDocumentFragment();
+    root.appendChild(doc.getDocumentElement());
+
+    if (debug) {
+      gadget.addFeature("caja-debug");
+    }
+
+    InputSource is = new InputSource(gadgetContext.getUrl().toJavaUri());
+    CajoledResult result =
+      rewrite(gadgetContext.getUrl(), gadgetContext.getContainer(),
+          new Dom(root), true, debug);
+
+    if (result.hasErrors) {
+      // Content is only used to produce useful snippets with error messages
+      List<Message> messages = result.messages;
+      createContainerFor(doc,
+          formatErrors(doc, is, docContent, messages, true /* visible */));
+      mc.documentChanged();
+      logException("rewrite", messages);
+      return;
+    }
+
+    Element cajoledOutput = doc.createElement("div");
+    cajoledOutput.setAttribute("id", "cajoled-output");
+
+    List<Message> messages = result.messages;
+    Element messagesNode = formatErrors(doc, is, docContent, messages,
+        /* invisible */ false);
+    cajoledOutput.appendChild(messagesNode);
+
+    Element outerDiv = doc.createElement("div");
+    outerDiv.setAttribute("id", "caja_outerContainer___");
+    outerDiv.setAttribute("style", "position: relative; overflow: hidden;");
+    cajoledOutput.appendChild(outerDiv);
+
+    Element innerDiv = doc.createElement("div");
+    innerDiv.setAttribute("id", "caja_innerContainer___");
+    innerDiv.setAttribute("class", "g___");
+    outerDiv.appendChild(innerDiv);
+
+    innerDiv.appendChild(doc.importNode(result.html, true));
+
+    String cajoledJs = renderJs(result.js, debug);
+    cajoledOutput.appendChild(cajaStart(doc, cajoledJs, debug));
+
+    createContainerFor(doc, cajoledOutput);
+    mc.documentChanged();
+    HtmlSerialization.attach(doc, htmlSerializer, null);
+  }
+
+  UriFetcher makeFetcher(final Uri gadgetUri, final String container) {
+    return new UriFetcher() {
+      public FetchedData fetch(ExternalReference ref, String mimeType)
+          throws UriFetchException {
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, CLASS_NAME, "makeFetcher", MessageKeys.RETRIEVE_REFERENCE,
+              new Object[] {ref.toString()});
+        }
+        Uri resourceUri = gadgetUri.resolve(Uri.fromJavaUri(ref.getUri()));
+        HttpRequest request =
+            new HttpRequest(resourceUri).setContainer(container).setGadget(gadgetUri).setInternalRequest( true );
+        try {
+          HttpResponse response = requestPipeline.execute(request);
+          byte[] responseBytes = IOUtils.toByteArray(response.getResponse());
+          return FetchedData.fromBytes(responseBytes, mimeType, response.getEncoding(),
+              new InputSource(ref.getUri()));
+        } catch (GadgetException e) {
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, CLASS_NAME, "makeFetcher", MessageKeys.FAILED_TO_RETRIEVE,
+                new Object[] {ref.toString()});
+          }
+          throw new UriFetchException(ref, mimeType, e);
+        } catch (IOException e) {
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, CLASS_NAME, "makeFetcher", MessageKeys.FAILED_TO_READ,
+                new Object[] {ref.toString()});
+          }
+          throw new UriFetchException(ref, mimeType, e);
+        }
+      }
+
+    };
+  }
+
+  protected UriPolicy makePolicy(final Uri gadgetUri) {
+    return new UriPolicy() {
+      public String rewriteUri(ExternalReference ref, UriEffect effect,
+          LoaderType loader, Map<String, ?> hints) {
+        try {
+          switch(effect) {
+            case SAME_DOCUMENT:
+                ProxyUriManager.ProxyUri proxyUri =
+                    new ProxyUriManager.ProxyUri(
+                        UriStatus.VALID_UNVERSIONED, Uri.fromJavaUri(ref.getUri()), null);
+                return proxyUriManager.make(ImmutableList.of(proxyUri), null).get(0).toString();
+            case NEW_DOCUMENT:
+            case NOT_LOADED:
+                return ref.getUri().toString();
+            default:
+                return null;
+          }
+        } catch (RuntimeException e) {
+          // if there are unexpected errors, fail safe - drop the uri
+        }
+        return null;
+      }
+    };
+  }
+
+  private void createContainerFor(Document doc, Node el) {
+    Element docEl = doc.createElement("html");
+    Element head = doc.createElement("head");
+    Element body = doc.createElement("body");
+    doc.appendChild(docEl);
+    docEl.appendChild(head);
+    docEl.appendChild(body);
+    body.appendChild(el);
+  }
+
+  private Element formatErrors(Document doc, InputSource is,
+      CharSequence orig, List<Message> messages, boolean visible) {
+    MessageContext mc = new MessageContext();
+    Map<InputSource, CharSequence> originalSrc = Maps.newHashMap();
+    originalSrc.put(is, orig);
+    mc.addInputSource(is);
+    SnippetProducer sp = new SnippetProducer(originalSrc, mc);
+
+    Element errElement = doc.createElement("ul");
+    // Style defined in gadgets.css
+    errElement.setAttribute("class", "gadgets-messages");
+    if (!visible) {
+      errElement.setAttribute("style", "display: none");
+    }
+    for (Message msg : messages) {
+      // Ignore LINT messages
+      if (MessageLevel.LINT.compareTo(msg.getMessageLevel()) <= 0) {
+        String snippet = sp.getSnippet(msg);
+        String messageText = msg.getMessageLevel().name() + ' ' +
+          html(msg.format(mc)) + ':' + snippet;
+        Element li = doc.createElement("li");
+        li.appendChild(doc.createTextNode(messageText));
+        errElement.appendChild(li);
+      }
+    }
+    return errElement;
+  }
+
+  private static String html(CharSequence s) {
+    StringBuilder sb = new StringBuilder();
+    Escaping.escapeXml(s, false, sb);
+    return sb.toString();
+  }
+
+  private String renderJs(CajoledModule cajoled, boolean debug) {
+    StringBuilder rendered = new StringBuilder();
+    TokenConsumer tc = debug
+        ? new JsPrettyPrinter(new Concatenator(rendered))
+        : new JsMinimalPrinter(new Concatenator(rendered));
+    cajoled.render(new RenderContext(tc)
+        .withAsciiOnly(true)
+        .withEmbeddable(true));
+    tc.noMoreTokens();
+    return rendered.toString();
+  }
+
+  private Element cajaStart(Document doc, String cajoledJs, boolean debug) {
+    Element scriptElement = doc.createElement("script");
+    scriptElement.setAttribute("type", "text/javascript");
+    StringBuilder start = new StringBuilder();
+    start.append("caja___.start(\n'");
+    Escaping.escapeJsString(cajoledJs, true, true, start);
+    start.append("', ");
+    start.append(debug ? "true" : "false");
+    start.append(");\n");
+    scriptElement.appendChild(doc.createTextNode(start.toString()));
+    return scriptElement;
+  }
+
+  private void logException(String methodname, List<Message> messages) {
+    StringBuilder errbuilder = new StringBuilder();
+    MessageContext mc = new MessageContext();
+    for (Message m : messages) {
+      errbuilder.append(m.format(mc)).append('\n');
+    }
+    if (LOG.isLoggable(Level.INFO)) {
+      LOG.logp(Level.INFO, CLASS_NAME, methodname, MessageKeys.UNABLE_TO_CAJOLE,
+          new Object[] {errbuilder});
+    }
+  }
+
+  protected PluginCompiler makePluginCompiler(
+      PluginMeta meta, MessageQueue mq) {
+    return new PluginCompiler(
+        BuildInfo.getInstance(), meta, mq);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ConcatProxyServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ConcatProxyServlet.java
new file mode 100644
index 0000000..e905a94
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ConcatProxyServlet.java
@@ -0,0 +1,405 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import com.google.common.collect.Lists;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.MultipleResourceHttpFetcher;
+import org.apache.shindig.gadgets.http.MultipleResourceHttpFetcher.RequestContext;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.ConcatUriManager;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Pattern;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet which concatenates the content of several proxied HTTP responses
+ */
+public class ConcatProxyServlet extends InjectedServlet {
+
+  private static final long serialVersionUID = -4390212150673709895L;
+
+  public static final String JSON_PARAM = Param.JSON.getKey();
+  private static final Pattern JSON_PARAM_PATTERN = Pattern.compile("^\\w*$");
+
+  static final Integer LONG_LIVED_REFRESH = (365 * 24 * 60 * 60);  // 1 year
+  static final Integer DEFAULT_REFRESH = (60 * 60);                // 1 hour
+
+  //class name for logging purpose
+  private static final String classname = ConcatProxyServlet.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private transient RequestPipeline requestPipeline;
+  private transient ConcatUriManager concatUriManager;
+  private transient ResponseRewriterRegistry contentRewriterRegistry;
+
+  // Sequential version of 'execute' by default.
+  private transient Executor executor = Executors.newSingleThreadExecutor();
+
+  private Integer longLivedRefreshSec = LONG_LIVED_REFRESH;
+
+  @Inject(optional = true)
+  public void setLongLivedRefresh(
+      @Named("org.apache.shindig.gadgets.servlet.longLivedRefreshSec") int longLivedRefreshSec) {
+    this.longLivedRefreshSec = longLivedRefreshSec;
+  }
+
+  @Inject
+  public void setRequestPipeline(RequestPipeline requestPipeline) {
+    checkInitialized();
+    this.requestPipeline = requestPipeline;
+  }
+
+  @Inject
+  public void setConcatUriManager(ConcatUriManager concatUriManager) {
+    checkInitialized();
+    this.concatUriManager = concatUriManager;
+  }
+
+  @Inject
+  public void setContentRewriterRegistry(@RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT)
+                                         ResponseRewriterRegistry contentRewriterRegistry) {
+    checkInitialized();
+    this.contentRewriterRegistry = contentRewriterRegistry;
+  }
+
+  @Inject
+  public void setExecutor(@Named("shindig.concat.executor") Executor executor) {
+    checkInitialized();
+    // Executor is independently named to allow separate configuration of
+    // concat fetch parallelism and other Shindig job execution.
+    this.executor = executor;
+  }
+
+  @SuppressWarnings("boxing")
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+    if (request.getHeader("If-Modified-Since") != null) {
+      response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+      return;
+    }
+
+    Uri uri = new UriBuilder(request).toUri();
+    ConcatUriManager.ConcatUri concatUri = concatUriManager.process(uri);
+
+    ConcatUriManager.Type concatType = concatUri.getType();
+    try {
+      if (concatType == null) {
+        throw new GadgetException(GadgetException.Code.MISSING_PARAMETER, "Missing type",
+            HttpResponse.SC_BAD_REQUEST);
+      }
+      } catch (GadgetException gex) {
+      response.sendError(HttpResponse.SC_BAD_REQUEST, formatError("doGet", gex, uri));
+      return;
+    }
+
+    // Throughout this class, wherever output is generated it's done as a UTF8 String.
+    // As such, we affirmatively state that UTF8 is being returned here.
+    response.setHeader("Content-Type", concatType.getMimeType() + "; charset=UTF8");
+    response.setHeader("Content-Disposition", "attachment;filename=p.txt");
+
+    ConcatOutputStream cos = createConcatOutputStream(response, concatUri);
+    if(cos == null) {
+      response.setStatus(HttpResponse.SC_BAD_REQUEST);
+      response.getOutputStream().println(
+              formatHttpError(HttpServletResponse.SC_BAD_REQUEST,
+                  "Bad json variable name " + concatUri.getSplitParam(), null));
+    } else {
+      if (doFetchConcatResources(response, concatUri, uri, cos)) {
+        response.setStatus(HttpResponse.SC_OK);
+      } else {
+        response.setStatus(HttpResponse.SC_BAD_REQUEST);
+      }
+      IOUtils.closeQuietly(cos);
+    }
+  }
+
+  /**
+   * Creates the correct ConcatOutputStream to use.  Will return null if there
+   * is a bad JSON varibale name.
+   * @param response HTTP response object.
+   * @param concatUri The concat URI.
+   * @return The correct ConcatOutputStream to use.
+   * @throws IOException thrown when the ConcatOutputStream cannot be created.
+   */
+  private ConcatOutputStream createConcatOutputStream(HttpServletResponse response,
+          ConcatUriManager.ConcatUri concatUri) throws IOException {
+    ConcatOutputStream cos;
+    String jsonVar = concatUri.getSplitParam();
+    if (jsonVar != null) {
+      // JSON-concat mode.
+      if (JSON_PARAM_PATTERN.matcher(jsonVar).matches()) {
+        cos = new JsonConcatOutputStream(response.getOutputStream(), jsonVar);
+      } else {
+        return null;
+      }
+    } else {
+      // Standard concat output mode.
+      cos = new VerbatimConcatOutputStream(response.getOutputStream());
+    }
+    return cos;
+  }
+
+  /**
+   * @param response HttpservletResponse.
+   * @param concatUri URI representing the concatenated list of resources requested.
+   * @param cos The ConcatOutputStream to write the response to.
+   * @return false for cases where concat resources could not be fetched, true for success cases.
+   * @throws IOException
+   */
+  private boolean doFetchConcatResources(HttpServletResponse response,
+      ConcatUriManager.ConcatUri concatUri, Uri uri, ConcatOutputStream cos) throws IOException {
+    // Check for json concat and set output stream.
+    Long minCacheTtl = Long.MAX_VALUE;
+    boolean isMinCacheTtlSet = false;
+
+    List<HttpRequest> requests = Lists.newArrayList();
+
+    try {
+      for (Uri resourceUri : concatUri.getBatch()) {
+        try {
+          requests.add(concatUri.makeHttpRequest(resourceUri));
+        } catch (GadgetException ge) {
+          if (cos.outputError(resourceUri, ge)) {
+            // True returned from outputError indicates a terminal error.
+            return false;
+          }
+        }
+      }
+
+      MultipleResourceHttpFetcher parallelFetcher =
+          new MultipleResourceHttpFetcher(requestPipeline, executor);
+      List<Pair<Uri, FutureTask<RequestContext>>> futureTasks = parallelFetcher.fetchAll(requests);
+
+      for (Pair<Uri, FutureTask<RequestContext>> futureTask : futureTasks) {
+        RequestContext requestCxt;
+        try {
+          try {
+            requestCxt = futureTask.two.get();
+          } catch (InterruptedException ie) {
+            throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, ie);
+          } catch (ExecutionException ee) {
+            throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, ee);
+          }
+          if (requestCxt.getGadgetException() != null) {
+            throw requestCxt.getGadgetException();
+          }
+          HttpResponse httpResp = requestCxt.getHttpResp();
+          if (httpResp != null) {
+            if (contentRewriterRegistry != null) {
+              try {
+                httpResp = contentRewriterRegistry.rewriteHttpResponse(requestCxt.getHttpReq(),
+                        httpResp, null);
+              } catch (RewritingException e) {
+                throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e,
+                        e.getHttpStatusCode());
+              }
+            }
+            minCacheTtl = Math.min(minCacheTtl, httpResp.getCacheTtl());
+            isMinCacheTtlSet = true;
+            cos.output(futureTask.one, httpResp);
+          } else {
+            return false;
+          }
+        } catch (GadgetException ge) {
+          if (cos.outputError(futureTask.one, ge)) {
+            return false;
+          }
+        }
+      }
+      // TODO: Investigate Chunked Encoding
+      minCacheTtl = isMinCacheTtlSet ? (minCacheTtl / 1000) : DEFAULT_REFRESH;
+      HttpUtil.setCachingHeaders(response,
+          concatUri.translateStatusRefresh(longLivedRefreshSec, minCacheTtl.intValue()), false);
+    } catch (GadgetException gex) {
+      cos.outputError(uri, gex);
+    }
+    return true;
+  }
+
+  private static String formatHttpError(int status, String errorMessage, Uri uri) {
+    StringBuilder err = new StringBuilder();
+    err.append("/* ---- Error ");
+    err.append(status);
+    if (!Strings.isNullOrEmpty(errorMessage)) {
+      err.append(", ");
+      err.append(errorMessage);
+    }
+    if (uri != null) {
+      err.append(" (").append(uri.toString()).append(')');
+    }
+
+    err.append(" ---- */");
+    return err.toString();
+  }
+
+  private static String formatError(String methodname, GadgetException excep, Uri uri)
+      throws IOException {
+    StringBuilder err = new StringBuilder();
+    err.append("/* ---- Error ");
+    err.append(excep.getCode().toString());
+    err.append(" concat(");
+    err.append(uri.toString());
+    err.append(") ");
+    err.append(excep.getMessage());
+    err.append(" ---- */");
+
+    // Log the errors here for now. We might want different severity levels
+    // for different error codes.
+    if (LOG.isLoggable(Level.INFO)) {
+      LOG.logp(Level.INFO, classname, methodname, MessageKeys.CONCAT_PROXY_REQUEST_FAILED, new Object[] {err.toString()});
+    }
+    return err.toString();
+  }
+
+  private static abstract class ConcatOutputStream extends ServletOutputStream {
+    private final ServletOutputStream wrapped;
+    private final StringBuilder stringBuilder;
+
+    protected ConcatOutputStream(ServletOutputStream wrapped) {
+      this.wrapped = wrapped;
+      stringBuilder = new StringBuilder();
+    }
+
+    protected abstract void outputJs(Uri uri, String data) throws IOException;
+
+    public void output(Uri uri, HttpResponse resp) throws IOException {
+      if (resp.getHttpStatusCode() != HttpServletResponse.SC_OK) {
+        println(formatHttpError(resp.getHttpStatusCode(), resp.getResponseAsString(), uri));
+      } else {
+        outputJs(uri, resp.getResponseAsString());
+      }
+    }
+
+    public boolean outputError(Uri uri, GadgetException e)
+        throws IOException {
+      println(formatError("outputError", e, uri));
+      return e.getHttpStatusCode() == HttpResponse.SC_INTERNAL_SERVER_ERROR;
+    }
+
+    @Override
+    public void write(int b) throws IOException {
+      wrapped.write(b);
+    }
+
+    @Override
+    public void write(byte b[], int off, int len) throws IOException {
+      wrapped.write(b, off, len);
+    }
+
+    @Override
+    public void write(byte b[]) throws IOException {
+      wrapped.write(b);
+    }
+
+    @Override
+    public void close() throws IOException {
+      wrapped.write(CharsetUtil.getUtf8Bytes(stringBuilder.toString()));
+      wrapped.close();
+    }
+
+    @Override
+    public void print(String data) throws IOException {
+      stringBuilder.append(data);
+    }
+
+    private String CRLF = "\r\n";
+
+    @Override
+    public void println(String data) throws IOException {
+      print(data + CRLF);
+    }
+  }
+
+  private static class VerbatimConcatOutputStream extends ConcatOutputStream {
+    public VerbatimConcatOutputStream(ServletOutputStream wrapped) {
+      super(wrapped);
+    }
+
+    @Override
+    protected void outputJs(Uri uri, String data) throws IOException {
+      println("/* ---- Start " + uri.toString() + " ---- */");
+      print(data);
+      println("/* ---- End " + uri.toString() + " ---- */");
+    }
+  }
+
+  private static class JsonConcatOutputStream extends ConcatOutputStream {
+    private boolean firstEntry;
+
+    public JsonConcatOutputStream(ServletOutputStream wrapped, String tok) throws IOException {
+      super(wrapped);
+      this.println(tok + "={");
+      this.firstEntry = true;
+    }
+
+    @Override
+    protected void outputJs(Uri uri, String data) throws IOException {
+      if (!firstEntry) {
+        println(",");
+      }
+      firstEntry = false;
+
+      print("\"");
+      print(uri.toString());
+      print("\":\"");
+      print(StringEscapeUtils.escapeEcmaScript(data));
+      print("\"");
+    }
+
+    @Override
+    public void close() throws IOException {
+      println("};");
+      super.close();
+    }
+
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java
new file mode 100644
index 0000000..fcf28af
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet filter to generate and check ETags in HTTP responses.
+ *
+ * An ETag is calculated for the servlet's output. If its value matches the
+ * value provided in the request's "If-None-Match" header, a 304 Not Modified
+ * response is returned; otherwise, the value is added to the response's "ETag"
+ * header.
+ *
+ * Note that when this filter is applied, the response body cannot be streamed.
+ */
+public class ETagFilter implements Filter {
+
+  public void init(FilterConfig filterConfig) {
+  }
+
+  public void destroy() {
+  }
+
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+      ETaggingHttpResponse taggingResponse = createTaggingResponse(request, response);
+      try {
+        chain.doFilter(request, taggingResponse);
+      } finally {
+        // Write to the output even if there was an exception, as it would have
+        // done without this filter.
+        taggingResponse.writeToOutput();
+      }
+    } else {
+      chain.doFilter(request, response);
+    }
+  }
+
+  protected ETaggingHttpResponse createTaggingResponse(
+      ServletRequest request, ServletResponse response) {
+    return new ETaggingHttpResponse((HttpServletRequest) request, (HttpServletResponse) response);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java
new file mode 100644
index 0000000..71c165b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+
+import org.apache.http.util.ByteArrayBuffer;
+import org.apache.shindig.common.util.HashUtil;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.security.MessageDigest;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * A class for generating and managing ETags for improved caching.
+ *
+ * Objects of this class have two modes: batching mode and streaming mode.
+ *
+ * In batching mode, the response body is stored in a buffer and, at the end,
+ * its ETag is calculated and everything is written to the output at once.
+ *
+ * In streaming mode, however, the response body is output as it's received
+ * from the servlet, and no ETag is calculated.
+ */
+public class ETaggingHttpResponse extends HttpServletResponseWrapper {
+
+  public static final String RESPONSE_HEADER = "ETag";
+  public static final String REQUEST_HEADER = "If-None-Match";
+
+  private static final Splitter IF_NONE_MATCH_SPLITTER =
+      Splitter.on(',').trimResults().trimResults(CharMatcher.is('"'));
+
+  protected final HttpServletRequest request;
+  protected final BufferServletOutputStream stream;
+  protected ServletOutputStream originalStream;
+  protected PrintWriter writer;
+  protected boolean batching;
+
+  public ETaggingHttpResponse(HttpServletRequest request, HttpServletResponse response) {
+    super(response);
+    this.request = request;
+    this.stream = new BufferServletOutputStream();
+    this.writer = null;
+    this.batching = true;
+  }
+
+  @Override
+  public ServletOutputStream getOutputStream() throws IOException {
+    if (originalStream == null) {
+      originalStream = getResponse().getOutputStream();
+    }
+    if (isCommitted()) {
+      batching = false;
+    }
+    return stream;
+  }
+
+  @Override
+  public PrintWriter getWriter() throws IOException {
+    if (writer == null) {
+      writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), getCharacterEncoding()));
+    }
+    return writer;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * The response object is also switched to streaming mode.
+   */
+  @Override
+  public void flushBuffer() throws IOException {
+    writeToOutput();
+    getResponse().flushBuffer();
+    batching = false;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * The response object is switched to batching mode if the response has not
+   * been committed yet.
+   */
+  @Override
+  public void reset() {
+    super.reset();
+    writer = null;
+    stream.reset();
+    batching = !isCommitted();
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * The response object is switched to batching mode if the response has not
+   * been committed yet.
+   */
+  @Override
+  public void resetBuffer() {
+    super.resetBuffer();
+    writer = null;
+    stream.reset();
+    batching = !isCommitted();
+  }
+
+  /**
+   * Switches this response object to streaming mode.
+   *
+   * The current buffer is written to the output, as are any subsequent writes.
+   *
+   * @throws IOException If flushing the buffer produced an exception.
+   */
+  public void startStreaming() throws IOException {
+    batching = false;
+    writeToOutput();
+  }
+
+  /**
+   * Outputs the response body.
+   *
+   * In batching mode, it outputs the full contents of the buffer with its
+   * corresponding ETag, or a NOT_MODIFIED response if the ETag matches the
+   * request's "If-None-Match" header.
+   *
+   * In streaming mode, output is only generated if the buffer is not empty;
+   * in that case, the buffer is flushed to the output.
+   *
+   * @throws IOException If there was a problem writing to the output.
+   */
+  protected void writeToOutput() throws IOException {
+    if (writer != null) {
+      writer.flush();
+    }
+    byte[] bytes = stream.getBuffer().toByteArray();
+    if (batching) {
+      String etag = stream.getContentHash();
+      ((HttpServletResponse) getResponse()).setHeader(RESPONSE_HEADER, '"' + etag + '"');
+      if (etagMatches(etag)) {
+        emitETagMatchedResult();
+      } else {
+        emitFullResponseBody(bytes);
+      }
+    } else if (bytes.length != 0) {
+      originalStream.write(bytes);
+      stream.getBuffer().clear();
+    }
+  }
+
+  protected boolean etagMatches(String etag) {
+    String ifNoneMatches = request.getHeader(REQUEST_HEADER);
+    if (Strings.isNullOrEmpty(ifNoneMatches)) {
+      return false;
+    }
+    return ImmutableList.copyOf(IF_NONE_MATCH_SPLITTER.split(ifNoneMatches)).contains(etag);
+  }
+
+  protected void emitETagMatchedResult() {
+    ((HttpServletResponse) getResponse()).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+    getResponse().setContentLength(0);
+  }
+
+  protected void emitFullResponseBody(byte[] bytes) throws IOException {
+    getResponse().setContentLength(bytes.length);
+    getResponse().getOutputStream().write(bytes);
+  }
+
+  /**
+   * A ServletOutputStream that stores the data in a byte array buffer.
+   */
+  @VisibleForTesting
+  class BufferServletOutputStream extends ServletOutputStream {
+    private static final int BUFFER_INITIAL_CAPACITY = 16384;
+
+    private MessageDigest digest = null;
+    private ByteArrayBuffer buffer = new ByteArrayBuffer(BUFFER_INITIAL_CAPACITY);
+
+    @Override
+    public void write(int b) throws IOException {
+      if (batching) {
+        updateDigest(b);
+        buffer.append(b);
+      } else {
+        originalStream.write(b);
+      }
+    }
+
+    public ByteArrayBuffer getBuffer() {
+      return buffer;
+    }
+
+    public void reset() {
+      buffer.clear();
+      digest = null;
+    }
+
+    public String getContentHash() {
+      ensureDigestObjectExists();
+      String hash = HashUtil.bytesToHex(digest.digest());
+      digest = null;
+      return hash;
+    }
+
+    private void updateDigest(int b) {
+      ensureDigestObjectExists();
+      digest.update((byte) b);
+    }
+
+    private void ensureDigestObjectExists() {
+      if (digest == null) {
+        digest = HashUtil.getMessageDigest();
+        digest.update(buffer.toByteArray());
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetRenderingServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetRenderingServlet.java
new file mode 100644
index 0000000..ff02a50
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetRenderingServlet.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.render.Renderer;
+import org.apache.shindig.gadgets.render.RenderingResults;
+import org.apache.shindig.gadgets.uri.IframeUriManager;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet for rendering Gadgets.
+ */
+public class GadgetRenderingServlet extends InjectedServlet {
+
+  private static final long serialVersionUID = -5634040113214794888L;
+
+  static final int DEFAULT_CACHE_TTL = 60 * 5;
+
+  //class name for logging purpose
+  private static final String classname = GadgetRenderingServlet.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  protected transient Renderer renderer;
+  protected transient IframeUriManager iframeUriManager;
+
+  @Inject
+  public void setRenderer(Renderer renderer) {
+    checkInitialized();
+    this.renderer = renderer;
+  }
+
+  @Inject
+  public void setIframeUriManager(IframeUriManager iframeUriManager) {
+    checkInitialized();
+    this.iframeUriManager = iframeUriManager;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+    // If an If-Modified-Since header is ever provided, we always say
+    // not modified. This is because when there actually is a change,
+    // cache busting should occur.
+    UriStatus urlStatus = getUrlStatus(req);
+    if (req.getHeader("If-Modified-Since") != null &&
+        !"1".equals(req.getParameter("nocache")) &&
+        urlStatus == UriStatus.VALID_VERSIONED) {
+      resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+      return;
+    }
+    render(req, resp, urlStatus);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+    render(req, resp, getUrlStatus(req));
+  }
+
+  private void render(HttpServletRequest req, HttpServletResponse resp, UriStatus urlstatus)
+      throws IOException {
+    if (req.getHeader(HttpRequest.DOS_PREVENTION_HEADER) != null) {
+      // Refuse to render for any request that came from us.
+      // TODO: Is this necessary for any other type of request? Rendering seems to be the only one
+      // that can potentially result in an infinite loop.
+      resp.sendError(HttpServletResponse.SC_FORBIDDEN);
+      return;
+    }
+
+    resp.setContentType("text/html");
+    resp.setCharacterEncoding("UTF-8");
+
+    GadgetContext context = new HttpGadgetContext(req);
+    RenderingResults results = renderer.render(context);
+
+    // process the rendering results
+    postGadgetRendering(new PostGadgetRenderingParams(req, resp, urlstatus, context, results));
+  }
+
+  /**
+   * Implementations that extend this class are strongly discouraged from overriding this method.
+   * To customize the behavior please override the hook methods for each of the
+   * RenderingResults.Status enum values instead.
+   */
+  protected void postGadgetRendering(PostGadgetRenderingParams params) throws IOException {
+    switch (params.getResults().getStatus()) {
+      case OK:
+        onOkRenderingResultsStatus(params);
+        break;
+      case ERROR:
+        onErrorRenderingResultsStatus(params);
+        break;
+      case MUST_REDIRECT:
+        onMustRedirectRenderingResultsStatus(params);
+        break;
+    }
+  }
+
+  protected void onOkRenderingResultsStatus(PostGadgetRenderingParams params)
+      throws IOException {
+    UriStatus urlStatus = params.getUrlStatus();
+    HttpServletResponse resp = params.getResponse();
+    if (params.getContext().getIgnoreCache() ||
+        urlStatus == UriStatus.INVALID_VERSION) {
+      HttpUtil.setCachingHeaders(resp, 0);
+    } else if (urlStatus == UriStatus.VALID_VERSIONED) {
+      // Versioned files get cached indefinitely
+      HttpUtil.setCachingHeaders(resp, true);
+    } else {
+      // Unversioned files get cached for 5 minutes by default, but this can be overridden
+      // with a query parameter.
+      int ttl = DEFAULT_CACHE_TTL;
+      String ttlStr = params.getRequest().getParameter(Param.REFRESH.getKey());
+      if (!Strings.isNullOrEmpty(ttlStr)) {
+        try {
+          ttl = Integer.parseInt(ttlStr);
+        } catch (NumberFormatException e) {
+          // Ignore malformed TTL value
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "onOkRenderingResultsStatus", MessageKeys.MALFORMED_TTL_VALUE, new Object[] {ttlStr});
+          }
+        }
+      }
+      HttpUtil.setCachingHeaders(resp, ttl, true);
+    }
+    resp.getWriter().print(params.getResults().getContent());
+  }
+
+  protected void onErrorRenderingResultsStatus(PostGadgetRenderingParams params)
+      throws IOException {
+    HttpServletResponse resp = params.getResponse();
+    resp.setStatus(params.getResults().getHttpStatusCode());
+    resp.getWriter().print(StringEscapeUtils.escapeHtml4(params.getResults().getErrorMessage()));
+  }
+
+  protected void onMustRedirectRenderingResultsStatus(PostGadgetRenderingParams params)
+      throws IOException {
+     params.getResponse().sendRedirect(params.getResults().getRedirect().toString());
+  }
+
+  private UriStatus getUrlStatus(HttpServletRequest req) {
+    return iframeUriManager.validateRenderingUri(new UriBuilder(req).toUri());
+  }
+
+  /**
+   * Contains the input parameters for post rendering methods.
+   */
+  protected static class PostGadgetRenderingParams {
+    private HttpServletRequest req;
+    private HttpServletResponse resp;
+    private UriStatus urlStatus;
+    private GadgetContext context;
+    private RenderingResults results;
+
+    public PostGadgetRenderingParams (HttpServletRequest req, HttpServletResponse resp,
+      UriStatus urlStatus, GadgetContext context, RenderingResults results) {
+      this.req = req;
+      this.resp = resp;
+      this.urlStatus = urlStatus;
+      this.context = context;
+      this.results = results;
+    }
+
+    public HttpServletRequest getRequest() {
+      return req;
+    }
+
+    public HttpServletResponse getResponse() {
+      return resp;
+    }
+
+    public UriStatus getUrlStatus() {
+      return urlStatus;
+    }
+
+    public GadgetContext getContext() {
+      return context;
+    }
+
+    public RenderingResults getResults() {
+      return results;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
new file mode 100644
index 0000000..6ea3287
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandler.java
@@ -0,0 +1,674 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.ExecutorService;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.Uri.UriException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.servlet.GadgetsHandlerApi.RenderingContext;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.uri.UriCommon;
+import org.apache.shindig.protocol.BaseRequestItem;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.protocol.conversion.BeanDelegator;
+import org.apache.shindig.protocol.conversion.BeanFilter;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+
+/**
+ * Provides endpoints for gadget metadata lookup and more.
+ *
+ * @since 2.0.0
+ */
+@Service(name = "gadgets")
+public class GadgetsHandler {
+  @VisibleForTesting
+  static final String FAILURE_METADATA = "Failed to get gadget metadata.";
+  @VisibleForTesting
+  static final String FAILURE_TOKEN = "Failed to get gadget token.";
+  @VisibleForTesting
+  static final String FAILURE_PROXY = "Failed to get proxy data.";
+  @VisibleForTesting
+  static final String FAILURE_CAJA = "Failed to cajole data.";
+  @VisibleForTesting
+  static final String FAILURE_JS = "Failed to get js data.";
+
+  private static final List<String> DEFAULT_METADATA_FIELDS =
+      ImmutableList.of("iframeUrls", "userPrefs.*", "modulePrefs.*", "views.*");
+
+  private static final List<String> DEFAULT_TOKEN_FIELDS = ImmutableList.of("token");
+
+  private static final List<String> DEFAULT_PROXY_FIELDS = ImmutableList.of("proxyUrl");
+
+  private static final List<String> DEFAULT_CAJA_FIELDS = ImmutableList.of("*");
+  private static final List<String> DEFAULT_JS_FIELDS = ImmutableList.of("jsUrl");
+
+  private static final Logger LOG = Logger.getLogger(GadgetsHandler.class.getName());
+
+  /**
+   *  Enum to list the used JSON/JSONP request parameters
+   *  It mostly reference the UriCommon fields for consistency,
+   *  This enum defined the API names, Do not change the names!
+   */
+  enum Param {
+    IDS("ids"),
+    CONTAINER(UriCommon.Param.CONTAINER.getKey()),
+    FIELDS("fields"),
+    DEBUG(UriCommon.Param.DEBUG),
+    NO_CACHE(UriCommon.Param.NO_CACHE),
+    REFRESH(UriCommon.Param.REFRESH),
+    LANG(UriCommon.Param.LANG),
+    COUNTRY(UriCommon.Param.COUNTRY),
+    VIEW(UriCommon.Param.VIEW),
+    RENDER_TYPE("render"),
+    SANITIZE(UriCommon.Param.SANITIZE),
+    GADGET(UriCommon.Param.GADGET),
+    FALLBACK_URL(UriCommon.Param.FALLBACK_URL_PARAM),
+    REWRITE_MIME(UriCommon.Param.REWRITE_MIME_TYPE),
+    NO_EXPAND(UriCommon.Param.NO_EXPAND),
+    RESIZE_HEIGHT(UriCommon.Param.RESIZE_HEIGHT),
+    RESIZE_WIDTH(UriCommon.Param.RESIZE_WIDTH),
+    RESIZE_QUALITY(UriCommon.Param.RESIZE_QUALITY),
+    FEATURES("features"),
+    LOADED_FEATURES("loadedFeatures"),
+    CONTAINER_MODE(UriCommon.Param.CONTAINER_MODE),
+    ONLOAD(UriCommon.Param.ONLOAD),
+    REPOSITORY(UriCommon.Param.REPOSITORY_ID),
+    MIME_TYPE("mime_type");
+
+    private final String name;
+    Param(String name) { this.name = name; }
+    Param(UriCommon.Param param) { this.name = param.getKey(); }
+    String getName() { return name; }
+  }
+
+  protected final ExecutorService executor;
+  protected final GadgetsHandlerService handlerService;
+
+  protected final BeanFilter beanFilter;
+  protected final BeanDelegator beanDelegator;
+
+  @Inject
+  public GadgetsHandler(ExecutorService executor,
+                        GadgetsHandlerService handlerService,
+                        BeanFilter beanFilter) {
+    this.executor = executor;
+    this.handlerService = handlerService;
+    this.beanFilter = beanFilter;
+
+    this.beanDelegator = new BeanDelegator();
+  }
+
+  @Operation(httpMethods = {"POST", "GET"}, path = "metadata")
+  public Map<String, GadgetsHandlerApi.BaseResponse> metadata(BaseRequestItem request)
+      throws ProtocolException {
+    return new AbstractExecutor() {
+      @Override
+      protected Callable<CallableData> createJob(String url, BaseRequestItem request)
+          throws ProcessingException {
+        return createMetadataJob(url, request);
+      }
+    }.execute(request);
+  }
+
+  @Operation(httpMethods = {"POST", "GET"}, path = "token")
+  public Map<String, GadgetsHandlerApi.BaseResponse> token(BaseRequestItem request)
+      throws ProtocolException {
+    return new AbstractExecutor() {
+      @Override
+      protected Callable<CallableData> createJob(String url, BaseRequestItem request)
+          throws ProcessingException {
+        return createTokenJob(url, request);
+      }
+    }.execute(request);
+  }
+
+  @Operation(httpMethods = {"POST", "GET"}, path = "js")
+  public GadgetsHandlerApi.BaseResponse js(BaseRequestItem request)
+      throws ProtocolException {
+    // No need for threading since it is one request
+    GadgetsHandlerApi.BaseResponse response;
+    try {
+      JsRequestData jsRequest = new JsRequestData(request);
+      response = handlerService.getJs(jsRequest);
+    } catch (ProcessingException e) {
+      response = handlerService.createErrorResponse(null, e.getHttpStatusCode(), e.getMessage());
+    } catch (Exception e) {
+      LOG.log(Level.INFO, "Error fetching JS", e);
+      response = handlerService.createErrorResponse(null, HttpResponse.SC_INTERNAL_SERVER_ERROR,
+          FAILURE_JS);
+    }
+    return response;
+  }
+
+  @Operation(httpMethods = {"POST", "GET"}, path = "proxy")
+  public Map<String, GadgetsHandlerApi.BaseResponse> proxy(BaseRequestItem request)
+      throws ProtocolException {
+    return new AbstractExecutor() {
+      @Override
+      protected Callable<CallableData> createJob(String url, BaseRequestItem request)
+          throws ProcessingException {
+        return createProxyJob(url, request);
+      }
+    }.execute(request);
+  }
+
+  @Operation(httpMethods = {"POST", "GET"}, path = "cajole")
+  public Map<String, GadgetsHandlerApi.BaseResponse> cajole(BaseRequestItem request)
+      throws ProtocolException {
+    return new AbstractExecutor() {
+      @Override
+      protected Callable<CallableData> createJob(String url, BaseRequestItem request)
+          throws ProcessingException {
+        return createCajaJob(url, request);
+      }
+    }.execute(request);
+  }
+
+  @Operation(httpMethods = "GET", path = "/@metadata.supportedFields")
+  public Set<String> supportedFields(RequestItem request) {
+    return ImmutableSet.copyOf(beanFilter
+        .getBeanFields(GadgetsHandlerApi.MetadataResponse.class, 5));
+  }
+
+  @Operation(httpMethods = "GET", path = "/@token.supportedFields")
+  public Set<String> tokenSupportedFields(RequestItem request) {
+    return ImmutableSet.copyOf(
+        beanFilter.getBeanFields(GadgetsHandlerApi.TokenResponse.class, 5));
+  }
+
+  @Operation(httpMethods = "GET", path = "/@js.supportedFields")
+  public Set<String> jsSupportedFields(RequestItem request) {
+    return ImmutableSet.copyOf(
+        beanFilter.getBeanFields(GadgetsHandlerApi.JsResponse.class, 5));
+  }
+
+  @Operation(httpMethods = "GET", path = "/@proxy.supportedFields")
+  public Set<String> proxySupportedFields(RequestItem request) {
+    return ImmutableSet.copyOf(
+        beanFilter.getBeanFields(GadgetsHandlerApi.ProxyResponse.class, 5));
+  }
+
+  @Operation(httpMethods = "GET", path = "/@cajole.supportedFields")
+  public Set<String> cajaSupportedFields(RequestItem request) {
+    return ImmutableSet.copyOf(beanFilter
+        .getBeanFields(GadgetsHandlerApi.CajaResponse.class, 5));
+  }
+
+  /**
+   * Class to handle threaded reply.
+   * Mainly it made to support filtering the id (url)
+   */
+  class CallableData {
+    private final String id;
+    private final GadgetsHandlerApi.BaseResponse data;
+    public CallableData(String id, GadgetsHandlerApi.BaseResponse data) {
+      this.id = id;
+      this.data = data;
+    }
+    public String getId() { return id; }
+    public GadgetsHandlerApi.BaseResponse getData() { return data; }
+  }
+
+  private abstract class AbstractExecutor {
+    public Map<String, GadgetsHandlerApi.BaseResponse> execute(BaseRequestItem request) {
+      Set<String> gadgetUrls = ImmutableSet.copyOf(request.getListParameter(Param.IDS.getName()));
+      if (gadgetUrls.isEmpty()) {
+        return ImmutableMap.of();
+      }
+
+      if (Strings.isNullOrEmpty(request.getParameter(Param.CONTAINER.getName()))) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+            "Missing container for request.");
+      }
+
+      ImmutableMap.Builder<String, GadgetsHandlerApi.BaseResponse> builder = ImmutableMap.builder();
+      int badReq = 0;
+      CompletionService<CallableData> completionService =
+          new ExecutorCompletionService<CallableData>(executor);
+      for (String gadgetUrl : gadgetUrls) {
+        try {
+          Callable<CallableData> job = createJob(gadgetUrl, request);
+          completionService.submit(job);
+        } catch (ProcessingException e) {
+          // Fail to create and submit job
+          builder.put(gadgetUrl, handlerService.createErrorResponse(null,
+              e.getHttpStatusCode(), e.getMessage()));
+          badReq++;
+        }
+      }
+
+      for (int numJobs = gadgetUrls.size() - badReq; numJobs > 0; numJobs--) {
+        CallableData response;
+        try {
+          response = completionService.take().get();
+          builder.put(response.getId(), response.getData());
+        } catch (InterruptedException e) {
+          throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+              "Processing interrupted.", e);
+        } catch (ExecutionException e) {
+          throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+              "Processing error.", e);
+        }
+      }
+      return builder.build();
+    }
+
+    protected abstract Callable<CallableData> createJob(String url, BaseRequestItem request)
+        throws ProcessingException;
+  }
+
+  // Hook to override in sub-class.
+  protected Callable<CallableData> createMetadataJob(final String url,
+      BaseRequestItem request) throws ProcessingException {
+    final MetadataRequestData metadataRequest = new MetadataRequestData(url, request);
+    return new Callable<CallableData>() {
+      public CallableData call() throws Exception {
+        try {
+          return new CallableData(url, handlerService.getMetadata(metadataRequest));
+        } catch (Exception e) {
+          return new CallableData(url,
+              handlerService.createErrorResponse(null, e, FAILURE_METADATA));
+        }
+      }
+    };
+  }
+
+  // Hook to override in sub-class.
+  protected Callable<CallableData> createTokenJob(final String url,
+      BaseRequestItem request) throws ProcessingException {
+    // TODO: Get token duration from requests
+    final TokenRequestData tokenRequest = new TokenRequestData(url, request);
+    return new Callable<CallableData>() {
+      public CallableData call() throws Exception {
+        try {
+          return new CallableData(url, handlerService.getToken(tokenRequest));
+        } catch (Exception e) {
+          return new CallableData(url,
+            handlerService.createErrorResponse(null, e, FAILURE_TOKEN));
+        }
+      }
+    };
+  }
+
+  // Hook to override in sub-class.
+  protected Callable<CallableData> createProxyJob(final String url,
+      BaseRequestItem request) throws ProcessingException {
+    final ProxyRequestData proxyRequest = new ProxyRequestData(url, request);
+    return new Callable<CallableData>() {
+      public CallableData call() throws Exception {
+        try {
+          return new CallableData(url, handlerService.getProxy(proxyRequest));
+        } catch (Exception e) {
+          return new CallableData(url,
+            handlerService.createErrorResponse(null, e, FAILURE_PROXY));
+        }
+      }
+    };
+  }
+
+  // Hook to override in sub-class.
+  protected Callable<CallableData> createCajaJob(final String url,
+      BaseRequestItem request) throws ProcessingException {
+    final CajaRequestData cajaRequest = new CajaRequestData(url, request);
+    return new Callable<CallableData>() {
+      public CallableData call() throws Exception {
+        try {
+          return new CallableData(url, handlerService.getCaja(cajaRequest));
+        } catch (Exception e) {
+          return new CallableData(url,
+            handlerService.createErrorResponse(null, e, FAILURE_CAJA));
+        }
+      }
+    };
+  }
+
+
+  /**
+   * Gadget context classes used to translate JSON BaseRequestItem into a more
+   * meaningful model objects that Java can work with.
+   */
+  private abstract class AbstractRequest implements GadgetsHandlerApi.BaseRequest {
+    protected final Uri uri;
+    protected final String container;
+    protected final List<String> fields;
+    protected final BaseRequestItem request;
+
+    public AbstractRequest(String url, BaseRequestItem request, List<String> defaultFields)
+        throws ProcessingException {
+      try {
+        this.uri = (url != null ? Uri.parse(url) : null);
+      } catch (UriException e) {
+        throw new ProcessingException("Bad url - " + url, HttpServletResponse.SC_BAD_REQUEST);
+      }
+      this.request = request;
+      this.container = request.getParameter(Param.CONTAINER.getName());
+      this.fields = processFields(request, defaultFields);
+    }
+
+    protected String getParam(BaseRequestItem request, Param field) {
+      return request.getParameter(field.getName());
+    }
+
+    protected String getParam(BaseRequestItem request, Param field, String defaultValue) {
+      return request.getParameter(field.getName(), defaultValue);
+    }
+
+    protected List<String> getListParam(BaseRequestItem request, Param field) {
+      return request.getListParameter(field.getName());
+    }
+
+    protected Boolean getBooleanParam(BaseRequestItem request, Param field) {
+      String val = request.getParameter(field.getName());
+      if (val != null) {
+        return "1".equals(val) || Boolean.valueOf(val);
+      }
+      return false;
+    }
+
+    protected Integer getIntegerParam(BaseRequestItem request, Param field)
+        throws ProcessingException {
+      String val = request.getParameter(field.getName());
+      Integer intVal = null;
+      if (val != null) {
+        try {
+          intVal = Integer.valueOf(val);
+        } catch (NumberFormatException e) {
+          throw new ProcessingException("Error parsing " + field + " parameter",
+              HttpServletResponse.SC_BAD_REQUEST);
+        }
+      }
+      return intVal;
+    }
+
+    public Uri getUrl() {
+      return uri;
+    }
+
+    public String getContainer() {
+      return container;
+    }
+
+    public List<String> getFields() {
+      return fields;
+    }
+
+    private List<String> processFields(BaseRequestItem request, List<String> defaultList) {
+      List<String> value = request.getListParameter(BaseRequestItem.FIELDS);
+      return ((value == null || value.isEmpty()) ? defaultList : value);
+    }
+  }
+
+  protected class JsRequestData extends AbstractRequest implements GadgetsHandlerApi.JsRequest {
+    private final Integer refresh;
+    private final boolean debug;
+    private final boolean ignoreCache;
+    private final List<String> features;
+    private final List<String> loadedFeatures;
+    private final RenderingContext context;
+    private final String onload;
+    private final String gadget;
+    private final String repository;
+
+    public JsRequestData(BaseRequestItem request) throws ProcessingException {
+      super(null, request, DEFAULT_JS_FIELDS);
+      this.ignoreCache = getBooleanParam(request, Param.NO_CACHE);
+      this.debug = getBooleanParam(request, Param.DEBUG);
+      this.refresh = getIntegerParam(request, Param.REFRESH);
+      this.features = getListParam(request, Param.FEATURES);
+      this.loadedFeatures = getListParam(request, Param.LOADED_FEATURES);
+      this.context = getRenderingContext(getParam(request, Param.CONTAINER_MODE));
+      this.onload = getParam(request, Param.ONLOAD);
+      this.gadget = getParam(request, Param.GADGET);
+      this.repository = getParam(request, Param.REPOSITORY);
+
+    }
+
+    public RenderingContext getContext() { return context; }
+    public boolean getDebug() { return debug; }
+    public List<String> getFeatures() { return features; }
+    public List<String> getLoadedFeatures() { return loadedFeatures; }
+    public boolean getIgnoreCache() { return ignoreCache; }
+    public String getOnload() { return onload; }
+    public Integer getRefresh() { return refresh; }
+    public String getGadget() { return gadget; }
+    public String getRepository() { return repository; }
+  }
+
+  private RenderingContext getRenderingContext(String param) {
+    RenderingContext context = RenderingContext.GADGET;
+    if ("1".equals(param)) {
+      context = RenderingContext.CONTAINER;
+    } else if ("2".equals(param)) {
+      context = RenderingContext.CONFIGURED_GADGET;
+    }
+    return context;
+  }
+
+  protected class ProxyRequestData extends AbstractRequest
+      implements GadgetsHandlerApi.ProxyRequest {
+
+    private final String gadget;
+    private final Integer refresh;
+    private final boolean debug;
+    private final boolean ignoreCache;
+    private final String fallbackUrl;
+    private final String mimetype;
+    private final boolean sanitize;
+    private final GadgetsHandlerApi.ImageParams imageParams;
+
+    public ProxyRequestData(String url, BaseRequestItem request) throws ProcessingException {
+      super(url, request, DEFAULT_PROXY_FIELDS);
+      this.ignoreCache = getBooleanParam(request, Param.NO_CACHE);
+      this.debug = getBooleanParam(request, Param.DEBUG);
+      this.sanitize = getBooleanParam(request, Param.SANITIZE);
+      this.gadget = getParam(request, Param.GADGET);
+      this.fallbackUrl = getParam(request, Param.FALLBACK_URL);
+      this.mimetype = getParam(request, Param.REWRITE_MIME);
+      this.refresh = getIntegerParam(request, Param.REFRESH);
+      imageParams = getImageParams(request);
+    }
+
+    private GadgetsHandlerApi.ImageParams getImageParams(BaseRequestItem request)
+        throws ProcessingException {
+      GadgetsHandlerApi.ImageParams params = null;
+      Boolean doNotExpand = getBooleanParam(request, Param.NO_EXPAND);
+      Integer height = getIntegerParam(request, Param.RESIZE_HEIGHT);
+      Integer width = getIntegerParam(request, Param.RESIZE_WIDTH);
+      Integer quality = getIntegerParam(request, Param.RESIZE_QUALITY);
+
+      if (height != null || width != null) {
+        return beanDelegator.createDelegator(null, GadgetsHandlerApi.ImageParams.class,
+            ImmutableMap.<String, Object>of(
+                "height", BeanDelegator.nullable(height),
+                "width", BeanDelegator.nullable(width),
+                "quality", BeanDelegator.nullable(quality),
+                "donotexpand", BeanDelegator.nullable(doNotExpand)));
+      }
+      return params;
+    }
+
+    public boolean getDebug() {
+      return debug;
+    }
+
+    public String getFallbackUrl() {
+      return fallbackUrl;
+    }
+
+    public boolean getIgnoreCache() {
+      return ignoreCache;
+    }
+
+    public GadgetsHandlerApi.ImageParams getImageParams() {
+      return imageParams;
+    }
+
+    public Integer getRefresh() {
+      return refresh;
+    }
+
+    public String getRewriteMimeType() {
+      return mimetype;
+    }
+
+    public boolean getSanitize() {
+      return sanitize;
+    }
+
+    public String getGadget() {
+      return gadget;
+    }
+  }
+
+  protected class TokenRequestData extends AbstractRequest
+      implements GadgetsHandlerApi.TokenRequest {
+
+    private Long moduleId;
+
+    public TokenRequestData(String url, BaseRequestItem request)
+        throws ProcessingException {
+      super(url, request, DEFAULT_TOKEN_FIELDS);
+
+      // The moduleId for the gadget (if it exists) is the fragment of the URI:
+      //  ex: http://example.com/gadget.xml#1 or http://example.com/gadget.xml
+      // zero is implied if missing.
+      String moduleId = this.uri.getFragmentParameter("moduleId");
+      this.moduleId = moduleId == null ? 0 : Long.valueOf(moduleId);
+    }
+
+    public GadgetsHandlerApi.AuthContext getAuthContext() {
+      return beanDelegator.createDelegator(
+          request.getToken(), GadgetsHandlerApi.AuthContext.class);
+    }
+
+    public Long getModuleId() {
+      return moduleId;
+    }
+  }
+
+  @VisibleForTesting
+  static GadgetsHandlerApi.RenderingType getRenderingType(String value)
+      throws ProcessingException {
+    GadgetsHandlerApi.RenderingType type = GadgetsHandlerApi.RenderingType.DEFAULT;
+    if (value != null) {
+      try {
+        type = GadgetsHandlerApi.RenderingType.valueOf(value.toUpperCase());
+      } catch (IllegalArgumentException e) {
+        throw new ProcessingException("Error parsing rendering type parameter",
+            HttpServletResponse.SC_BAD_REQUEST);
+      }
+    }
+    return type;
+  }
+
+  protected class CajaRequestData extends AbstractRequest
+      implements GadgetsHandlerApi.CajaRequest {
+    private final String mimeType;
+    private final boolean debug;
+
+    public CajaRequestData(String url, BaseRequestItem request)
+        throws ProcessingException {
+      super(url, request, DEFAULT_CAJA_FIELDS);
+      this.mimeType = getParam(request, Param.MIME_TYPE, "text/html");
+      this.debug = getBooleanParam(request, Param.DEBUG);
+    }
+
+    public String getMimeType() {
+      return mimeType;
+    }
+
+    public boolean getDebug() {
+      return debug;
+    }
+  }
+
+  protected class MetadataRequestData extends AbstractRequest
+      implements GadgetsHandlerApi.MetadataRequest {
+    protected final Locale locale;
+    protected final boolean ignoreCache;
+    protected final boolean debug;
+    protected final GadgetsHandlerApi.RenderingType renderingType;
+
+    public MetadataRequestData(String url, BaseRequestItem request)
+        throws ProcessingException {
+      super(url, request, DEFAULT_METADATA_FIELDS);
+      String lang = request.getParameter("language");
+      String country = request.getParameter("country");
+      this.locale =
+          (lang != null && country != null) ? new Locale(lang, country) : (lang != null)
+              ? new Locale(lang) : GadgetSpec.DEFAULT_LOCALE;
+      this.ignoreCache = getBooleanParam(request, Param.NO_CACHE);
+      this.debug = getBooleanParam(request, Param.DEBUG);
+      this.renderingType = GadgetsHandler.getRenderingType(getParam(request, Param.RENDER_TYPE));
+    }
+
+    public int getModuleId() {
+      return 1; // TODO calculate?
+    }
+
+    public Locale getLocale() {
+      return locale;
+    }
+
+    public boolean getIgnoreCache() {
+      return ignoreCache;
+    }
+
+    public boolean getDebug() {
+      return debug;
+    }
+
+    public String getView() {
+      return getParam(request, Param.VIEW, "default");
+    }
+
+    public GadgetsHandlerApi.AuthContext getAuthContext() {
+      return beanDelegator.createDelegator(
+        request.getToken(), GadgetsHandlerApi.AuthContext.class);
+    }
+
+    public GadgetsHandlerApi.RenderingType getRenderingType() {
+      return renderingType;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
new file mode 100644
index 0000000..e8fb975
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerApi.java
@@ -0,0 +1,335 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.protocol.conversion.BeanFilter.Unfiltered;
+
+import com.google.common.collect.Multimap;
+
+/**
+ * Gadget Handler Interface data.
+ * Classes in here specify the API data.
+ * Please do not reference run time classes, instead create new interface (keep imports clean!).
+ * Please avoid changes if possible, you might break external system that depend on the API.
+ *
+ * @since 2.0.0
+ */
+public class GadgetsHandlerApi {
+
+  public interface BaseRequest {
+    public String getContainer();
+    public List<String> getFields();
+    public Uri getUrl();
+  }
+
+  public interface Error {
+    public int getCode();
+    public String getMessage();
+  }
+
+  public interface BaseResponse {
+    /** Url of the request, optional (for example for bad url error) */
+    public Uri getUrl();
+    /** Error response (optional) */
+    @Unfiltered
+    public Error getError();
+    /** The response expiration time (miliseconds since epoch), -1 for no caching */
+    @Unfiltered
+    public Long getExpireTimeMs();
+    /** The response time (miliseconds since epoch) - usefull for misconfigured client time */
+    @Unfiltered
+    public Long getResponseTimeMs();
+  }
+
+  public interface MetadataRequest extends BaseRequest {
+    public Locale getLocale();
+    public boolean getIgnoreCache();
+    public boolean getDebug();
+    public String getView();
+    public AuthContext getAuthContext();
+    public RenderingType getRenderingType();
+  }
+
+  public enum RenderingType {
+    DEFAULT,
+    SANITIZED,
+    INLINE_CAJOLED,
+    IFRAME_CAJOLED
+  }
+
+  public interface AuthContext {
+    public String getOwnerId();
+    public String getViewerId();
+    public String getDomain();
+    public long getModuleId();
+    public String getAuthenticationMode();
+    public Long getExpiresAt();
+    public String getTrustedJson();
+  }
+
+  public interface MetadataResponse extends BaseResponse {
+    public Map<String, String> getIframeUrls();
+    public String getChecksum();
+    public ModulePrefs getModulePrefs();
+    public Map<String, UserPref> getUserPrefs();
+    public Map<String, View> getViews();
+    public Boolean getNeedsTokenRefresh();
+    public Set<String> getRpcServiceIds();
+    public Integer getTokenTTL();
+  }
+
+  public enum ViewContentType {
+    HTML("html"), URL("url"), HTML_SANITIZED("x-html-sanitized");
+
+    private final String name;
+    private ViewContentType(String name) {
+      this.name = name;
+    }
+    @Override
+    public String toString() {
+      return name;
+    }
+  }
+
+  public interface View {
+    public String getName();
+    public ViewContentType getType();
+    public Uri getHref();
+    public boolean getQuirks();
+    public int getPreferredHeight(); // Default to 0
+    public int getPreferredWidth();  // Default to 0
+  }
+
+  public enum UserPrefDataType {
+    STRING, HIDDEN, BOOL, ENUM, LIST, NUMBER
+  }
+
+  public interface UserPref {
+    public String getName();
+    public String getDisplayName();
+    public String getDefaultValue();
+    public boolean getRequired();
+    public UserPrefDataType getDataType();
+    public List<EnumValuePair> getOrderedEnumValues();
+  }
+
+  public interface EnumValuePair {
+    public String getValue();
+    public String getDisplayValue();
+  }
+
+  public interface ModulePrefs {
+    public String getTitle();
+    public Uri getTitleUrl();
+    public String getDescription();
+    public String getAuthor();
+    public String getAuthorEmail();
+    public Uri getScreenshot();
+    public Uri getThumbnail();
+    public String getDirectoryTitle();
+    public String getAuthorAffiliation();
+    public String getAuthorLocation();
+    public Uri getAuthorPhoto();
+    public String getAuthorAboutme();
+    public String getAuthorQuote();
+    public Uri getAuthorLink();
+    public boolean getScaling();
+    public boolean getScrolling();
+    public int getWidth();
+    public int getHeight();
+    public List<String> getCategories();
+    public Map<String, Feature> getFeatures();
+    public Map<String, LinkSpec> getLinks();
+    public OAuthSpec getOAuthSpec();
+    public OAuth2Spec getOAuth2Spec();
+    // TODO: Provide better interface for locale if needed
+    // public Map<Locale, LocaleSpec> getLocales();
+  }
+
+  public interface Feature {
+    public String getName();
+    public boolean getRequired();
+    public Multimap<String, String> getParams();
+  }
+
+  public interface LinkSpec {
+    public String getRel();
+    public Uri getHref();
+    public String getMethod();
+  }
+
+  public interface OAuthSpec {
+    public Map<String, OAuthService> getServices();
+  }
+
+  public interface OAuth2Spec {
+	    public Map<String, OAuth2Service> getServices();
+  }
+
+  public interface OAuthService {
+    public EndPoint getRequestUrl();
+    public EndPoint getAccessUrl();
+    public Uri getAuthorizationUrl();
+    public String getName();
+  }
+
+  public interface EndPoint {
+    public Uri getUrl();
+    public Method getMethod();
+    public Location getLocation();
+  }
+
+  public interface OAuth2Service {
+	  public EndPoint getAuthorizationUrl();
+	  public EndPoint getTokenUrl();
+	  public String getScope();
+	  public String getName();
+  }
+  public interface TokenRequest extends BaseRequest {
+    public AuthContext getAuthContext();
+    public Long getModuleId();
+  }
+
+  public interface TokenResponse extends BaseResponse {
+    public String getToken();
+    public Integer getTokenTTL();
+    public Long getModuleId();
+  }
+
+  // TODO(jasvir): Support getRefresh and noCache
+  public interface CajaRequest extends BaseRequest {
+    public String getMimeType();
+    public boolean getDebug();
+  }
+
+  public interface CajaResponse extends BaseResponse {
+    public String getHtml();
+    public String getJs();
+    public List<Message> getMessages();
+  }
+
+  public interface Message {
+    public MessageLevel getLevel();
+    public String getName();
+    public String getMessage();
+  }
+
+  public enum MessageLevel {
+    UNKNOWN,
+    // Fine grained info about internal progress
+    LOG,
+    // Broad info about internal progress
+    SUMMARY,
+    // Information inferred about source files
+    INFERENCE,
+    // Indicative of a possible problem in an input source file
+    LINT,
+    // Indicative of a probable problem in an input source file
+    WARNING,
+    // Indicative of a problem which prevents production of usable output
+    // but progress should continue in case further messages shed more info
+    ERROR,
+    // Indicative of a problem that prevents usable further processing
+    FATAL_ERROR
+  }
+
+  public enum Method {
+	    GET,
+	    POST
+  }
+
+  public enum Location {
+    HEADER("auth-header"),
+    URL("uri-query"),
+    BODY("post-body");
+
+    private String locationString;
+    private Location(String locationString) {
+      this.locationString = locationString;
+    }
+
+    @Override
+    public String toString() {
+      return locationString;
+    }
+  }
+
+  public interface ProxyRequest extends BaseRequest {
+    // The BaseRequest.url store the resource to proxy
+    public String getGadget();
+    public Integer getRefresh();
+    public boolean getDebug();
+    public boolean getIgnoreCache();
+    public String getFallbackUrl();
+    public String getRewriteMimeType();
+    public boolean getSanitize();
+    public ImageParams getImageParams();
+  }
+
+  public interface ImageParams {
+    public Integer getHeight();
+    public Integer getWidth();
+    public Integer getQuality();
+    public Boolean getDoNotExpand();
+  }
+
+  public interface ProxyResponse extends BaseResponse {
+    public Uri getProxyUrl();
+    public HttpResponse getProxyContent();
+  }
+
+  public interface HttpResponse {
+    public int getCode();
+    public String getEncoding();
+    public String getContentBase64();
+    public List<NameValuePair> getHeaders();
+  }
+
+  public interface NameValuePair {
+    public String getName();
+    public String getValue();
+  }
+
+  public interface JsRequest extends BaseRequest {
+    public String getGadget();
+    public Integer getRefresh();
+    public boolean getDebug();
+    public boolean getIgnoreCache();
+    public List<String> getFeatures();
+    public List<String> getLoadedFeatures();
+    public String getOnload();
+    public RenderingContext getContext();
+    public String getRepository();
+  }
+
+  public enum RenderingContext {
+    GADGET, CONTAINER, CONFIGURED_GADGET
+  }
+
+  public interface JsResponse extends BaseResponse {
+    public Uri getJsUrl();
+    public String getJsContent();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java
new file mode 100644
index 0000000..0b80ef9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerService.java
@@ -0,0 +1,751 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64InputStream;
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenException;
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.js.JsException;
+import org.apache.shindig.gadgets.js.JsRequestBuilder;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.js.JsServingPipeline;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.servlet.CajaContentRewriter.CajoledResult;
+import org.apache.shindig.gadgets.servlet.GadgetsHandlerApi.AuthContext;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LinkSpec;
+import org.apache.shindig.gadgets.spec.ModulePrefs;
+import org.apache.shindig.gadgets.spec.OAuth2Service;
+import org.apache.shindig.gadgets.spec.OAuth2Spec;
+import org.apache.shindig.gadgets.spec.OAuthService;
+import org.apache.shindig.gadgets.spec.OAuthSpec;
+import org.apache.shindig.gadgets.spec.UserPref;
+import org.apache.shindig.gadgets.spec.UserPref.EnumValuePair;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.uri.DefaultIframeUriManager;
+import org.apache.shindig.gadgets.uri.IframeUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager.ProxyUri;
+import org.apache.shindig.protocol.conversion.BeanDelegator;
+import org.apache.shindig.protocol.conversion.BeanFilter;
+
+import com.google.caja.lexer.TokenConsumer;
+import com.google.caja.parser.html.Nodes;
+import com.google.caja.render.Concatenator;
+import com.google.caja.render.JsMinimalPrinter;
+import com.google.caja.render.JsPrettyPrinter;
+import com.google.caja.reporting.MessageContext;
+import com.google.caja.reporting.RenderContext;
+import com.google.caja.util.Sets;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Service that interfaces with the system to provide information about gadgets.
+ *
+ * @since 2.0.0
+ */
+public class GadgetsHandlerService {
+
+  private static final Locale DEFAULT_LOCALE = new Locale("all", "all");
+
+  private static final Logger LOG = Logger.getLogger(GadgetsHandler.class.getName());
+
+  // Map shindig data class to API interfaces
+  @VisibleForTesting
+  static final Map<Class<?>, Class<?>> API_CLASSES =
+      new ImmutableMap.Builder<Class<?>, Class<?>>()
+          .put(View.class, GadgetsHandlerApi.View.class)
+          .put(UserPref.class, GadgetsHandlerApi.UserPref.class)
+          .put(EnumValuePair.class, GadgetsHandlerApi.EnumValuePair.class)
+          .put(ModulePrefs.class, GadgetsHandlerApi.ModulePrefs.class)
+          .put(Feature.class, GadgetsHandlerApi.Feature.class)
+          .put(LinkSpec.class, GadgetsHandlerApi.LinkSpec.class)
+          .put(OAuthSpec.class, GadgetsHandlerApi.OAuthSpec.class)
+          .put(OAuthService.class, GadgetsHandlerApi.OAuthService.class)
+          .put(OAuthService.EndPoint.class, GadgetsHandlerApi.EndPoint.class)
+          .put(OAuth2Spec.class, GadgetsHandlerApi.OAuth2Spec.class)
+          .put(OAuth2Service.class, GadgetsHandlerApi.OAuth2Service.class)
+          // Enums
+          .put(View.ContentType.class, GadgetsHandlerApi.ViewContentType.class)
+          .put(UserPref.DataType.class, GadgetsHandlerApi.UserPrefDataType.class)
+          .put(GadgetsHandlerApi.RenderingContext.class, RenderingContext.class)
+          .put(OAuthService.Method.class, GadgetsHandlerApi.Method.class)
+          .put(OAuthService.Location.class, GadgetsHandlerApi.Location.class)
+          .build();
+
+  // Provide mapping for internal enums to api enums
+  @VisibleForTesting
+  static final Map<Enum<?>, Enum<?>> ENUM_CONVERSION_MAP =
+      new ImmutableMap.Builder<Enum<?>, Enum<?>>()
+          // View.ContentType mapping
+          .putAll(BeanDelegator.createDefaultEnumMap(View.ContentType.class,
+              GadgetsHandlerApi.ViewContentType.class))
+          // UserPref.DataType mapping
+          .putAll(BeanDelegator.createDefaultEnumMap(UserPref.DataType.class,
+              GadgetsHandlerApi.UserPrefDataType.class))
+          .putAll(BeanDelegator.createDefaultEnumMap(OAuthService.Method.class,
+              GadgetsHandlerApi.Method.class))
+          .putAll(BeanDelegator.createDefaultEnumMap(OAuthService.Location.class,
+              GadgetsHandlerApi.Location.class))
+          .putAll(BeanDelegator.createDefaultEnumMap(GadgetsHandlerApi.RenderingContext.class,
+              RenderingContext.class))
+          .build();
+
+  protected final TimeSource timeSource;
+  protected final Processor processor;
+  protected final IframeUriManager iframeUriManager;
+  protected final SecurityTokenCodec securityTokenCodec;
+  protected final ProxyUriManager proxyUriManager;
+  protected final JsUriManager jsUriManager;
+  protected final JsServingPipeline jsPipeline;
+  protected final JsRequestBuilder jsRequestBuilder;
+  protected final ProxyHandler proxyHandler;
+  protected final BeanDelegator beanDelegator;
+  protected final long specRefreshInterval;
+  protected final BeanFilter beanFilter;
+  protected final CajaContentRewriter cajaContentRewriter;
+  protected final GadgetAdminStore gadgetAdminStore;
+  protected final FeatureRegistryProvider featureRegistryProvider;
+  protected final ModuleIdManager moduleIdManager;
+  private ContainerConfig config;
+
+  @Inject
+  public GadgetsHandlerService(TimeSource timeSource, Processor processor,
+      IframeUriManager iframeUriManager, SecurityTokenCodec securityTokenCodec,
+      ProxyUriManager proxyUriManager, JsUriManager jsUriManager, ProxyHandler proxyHandler,
+      JsServingPipeline jsPipeline, JsRequestBuilder jsRequestBuilder,
+      @Named("shindig.cache.xml.refreshInterval") long specRefreshInterval,
+      BeanFilter beanFilter, CajaContentRewriter cajaContentRewriter,
+      GadgetAdminStore gadgetAdminStore,
+      FeatureRegistryProvider featureRegistryProvider,
+      ModuleIdManager moduleIdManager,
+      ContainerConfig config) {
+    this.timeSource = timeSource;
+    this.processor = processor;
+    this.iframeUriManager = iframeUriManager;
+    this.securityTokenCodec = securityTokenCodec;
+    this.proxyUriManager = proxyUriManager;
+    this.jsUriManager = jsUriManager;
+    this.proxyHandler = proxyHandler;
+    this.jsPipeline = jsPipeline;
+    this.jsRequestBuilder = jsRequestBuilder;
+    this.specRefreshInterval = specRefreshInterval;
+    this.beanFilter = beanFilter;
+    this.cajaContentRewriter = cajaContentRewriter;
+    this.gadgetAdminStore = gadgetAdminStore;
+    this.featureRegistryProvider = featureRegistryProvider;
+    this.moduleIdManager = moduleIdManager;
+
+    this.beanDelegator = new BeanDelegator(API_CLASSES, ENUM_CONVERSION_MAP);
+    this.config = config;
+  }
+
+  /**
+   * Get gadget metadata information and iframe url. Support filtering of fields
+   * @param request request parameters
+   * @return gadget metadata and iframe urls
+   * @throws ProcessingException
+   */
+  public GadgetsHandlerApi.MetadataResponse getMetadata(GadgetsHandlerApi.MetadataRequest request)
+      throws ProcessingException {
+    verifyBaseParams(request, true);
+    Set<String> fields = beanFilter.processBeanFields(request.getFields());
+
+    GadgetContext context = new MetadataGadgetContext(request);
+    Gadget gadget = processor.process(context);
+
+    boolean needIfrUrls = isFieldIncluded(fields, "iframeurls");
+    if (needIfrUrls) {
+      if(!gadgetAdminStore.checkFeatureAdminInfo(gadget)) {
+        throw new ProcessingException("Gadget is not trusted to render in this container.",
+              HttpResponse.SC_BAD_REQUEST);
+      }
+    }
+    Map<String, Uri> uris = needIfrUrls ?
+            iframeUriManager.makeAllRenderingUris(gadget) : null;
+    Boolean needsTokenRefresh =
+        isFieldIncluded(fields, "needstokenrefresh") ?
+            gadget.getAllFeatures().contains("auth-refresh") : null;
+    boolean alwaysAppendSecurityToken = config.getBool(gadget.getContext().getContainer(),
+            DefaultIframeUriManager.SECURITY_TOKEN_ALWAYS_KEY);
+    if (alwaysAppendSecurityToken) {
+      needsTokenRefresh = Boolean.TRUE;
+    }
+    Set<String> rpcServiceIds = getRpcServiceIds(gadget);
+
+    Integer tokenTTL = isFieldIncluded(fields, "tokenTTL") ?
+        securityTokenCodec.getTokenTimeToLive(context.getContainer()) : null;
+
+    return createMetadataResponse(context.getUrl(), gadget.getSpec(), uris,
+        needsTokenRefresh, fields, timeSource.currentTimeMillis() + specRefreshInterval, tokenTTL,
+        rpcServiceIds);
+  }
+
+  /**
+   * Gets the set of allowed RPC service ids.
+   *
+   * @param gadget
+   *          the gadget to get the service ids for.
+   * @return the set of allowed RPC service ids.
+   */
+  private Set<String> getRpcServiceIds(Gadget gadget) {
+    GadgetContext context = gadget.getContext();
+    Set<String> rpcEndpoints = Sets.newHashSet(gadgetAdminStore.getAdditionalRpcServiceIds(gadget));
+    List<Feature> modulePrefFeatures = gadget.getSpec().getModulePrefs().getAllFeatures();
+    List<String> featureNames = Lists.newArrayList();
+    for(Feature feature : modulePrefFeatures) {
+      if(gadgetAdminStore.isAllowedFeature(feature, gadget)) {
+        featureNames.add(feature.getName());
+      }
+    }
+    try {
+      FeatureRegistry featureRegistry = featureRegistryProvider.get(context.getRepository());
+      LookupResult result = featureRegistry.getFeatureResources(context,
+          featureRegistry.getFeatures(featureNames), null);
+      List<FeatureBundle> bundles = result.getBundles();
+      for (FeatureBundle bundle : bundles) {
+        rpcEndpoints.addAll(bundle.getApis(ApiDirective.Type.RPC, false));
+      }
+    } catch (GadgetException e) {
+      LOG.log(Level.WARNING, "Error getting features from feature registry", e);
+    }
+    return rpcEndpoints;
+  }
+
+  private boolean isFieldIncluded(Set<String> fields, String name) {
+    return fields.contains(BeanFilter.ALL_FIELDS) || fields.contains(name.toLowerCase());
+  }
+  /**
+   * Create security token
+   * @param request token parameters (gadget, owner and viewer)
+   * @return Security token
+   * @throws SecurityTokenException
+   */
+  public GadgetsHandlerApi.TokenResponse getToken(GadgetsHandlerApi.TokenRequest request)
+      throws SecurityTokenException, ProcessingException {
+    verifyBaseParams(request, true);
+    Set<String> fields = beanFilter.processBeanFields(request.getFields());
+    AuthContext authContext = request.getAuthContext();
+
+    SecurityToken tokenData = null;
+    String token = null;
+
+    Long moduleId = request.getModuleId();
+    if (moduleId == null) {
+      // Zero means there's no persisted module instance and the container doesn't care to persist it.
+      moduleId = 0L;
+    } else if (moduleId < 0) {
+      // Please generate a module Id for me
+      moduleId = moduleIdManager.generate(request.getUrl(), authContext);
+    }
+    if (moduleId > 0) {
+      moduleId = moduleIdManager.validate(request.getUrl(), authContext, moduleId);
+    }
+
+    if (moduleId != null) {
+      tokenData = convertAuthContext(authContext, request.getContainer(),
+          request.getUrl().toString(), moduleId, request.getUrl().toString());
+      token = securityTokenCodec.encodeToken(tokenData);
+    }
+
+    Long expiryTimeMs = null;
+    Integer tokenTTL = null;
+    if (tokenData != null) {
+      expiryTimeMs = tokenData.getExpiresAt();
+      tokenTTL = isFieldIncluded(fields, "tokenTTL") ?
+              securityTokenCodec.getTokenTimeToLive(tokenData.getContainer())
+              : null;
+    }
+
+    moduleId = isFieldIncluded(fields, "moduleId") ? moduleId : null;
+
+    return createTokenResponse(request.getUrl(), token, fields, expiryTimeMs, tokenTTL, moduleId);
+  }
+
+  public GadgetsHandlerApi.JsResponse getJs(GadgetsHandlerApi.JsRequest request)
+      throws ProcessingException {
+    verifyBaseParams(request, false);
+    Set<String> fields = beanFilter.processBeanFields(request.getFields());
+
+    JsUri jsUri = createJsUri(request);
+    Uri servedUri = jsUriManager.makeExternJsUri(jsUri);
+
+    String content = null;
+    Long expireMs = null;
+    if (isFieldIncluded(fields, "jsContent")) {
+      JsResponse response;
+      try {
+        response = jsPipeline.execute(jsRequestBuilder.build(jsUri, servedUri.getAuthority()));
+      } catch (JsException e) {
+        throw new ProcessingException(e.getMessage(), e.getStatusCode());
+      }
+      content = response.toJsString();
+      if (response.isProxyCacheable()) {
+        expireMs = getDefaultExpiration();
+      }
+    } else {
+      expireMs = getDefaultExpiration();
+    }
+    return createJsResponse(request.getUrl(), servedUri, content, fields, expireMs);
+  }
+
+  public GadgetsHandlerApi.ProxyResponse getProxy(GadgetsHandlerApi.ProxyRequest request)
+      throws ProcessingException {
+    verifyBaseParams(request, true);
+    Set<String> fields = beanFilter.processBeanFields(request.getFields());
+
+    ProxyUri proxyUri = createProxyUri(request);
+    List<Uri> uris = proxyUriManager.make(ImmutableList.of(proxyUri), null);
+
+    HttpResponse httpResponse = null;
+    try {
+      if (isFieldIncluded(fields, "proxyContent")) {
+        httpResponse = proxyHandler.fetch(proxyUri);
+      }
+    } catch (IOException e) {
+      LOG.log(Level.INFO, "Failed to fetch resource " + proxyUri.getResource().toString(), e);
+      throw new ProcessingException("Error getting response content", HttpResponse.SC_BAD_GATEWAY);
+    } catch (GadgetException e) {
+      // TODO: Clean this log if it is too spammy
+      LOG.log(Level.INFO, "Failed to fetch resource " + proxyUri.getResource().toString(), e);
+      throw new ProcessingException("Error getting response content", HttpResponse.SC_BAD_GATEWAY);
+    }
+
+    try {
+      return createProxyResponse(uris.get(0), httpResponse, fields,
+          getProxyExpireMs(proxyUri, httpResponse));
+    } catch (IOException e) {
+      // Should never happen!
+      LOG.log(Level.WARNING, "Error creating proxy response", e);
+      throw new ProcessingException("Error getting response content",
+          HttpResponse.SC_INTERNAL_SERVER_ERROR);
+    }
+  }
+
+  /**
+   * Convert message level to Shindig's serializable message type
+   */
+  public static GadgetsHandlerApi.MessageLevel convertMessageLevel(String name) {
+    try {
+      return GadgetsHandlerApi.MessageLevel.valueOf(name);
+    }
+    catch (Exception ex) {
+      return GadgetsHandlerApi.MessageLevel.UNKNOWN;
+    }
+  }
+
+  /**
+   * Convert messages from Caja's internal message type to Shindig's serializable message type
+   */
+  private List<GadgetsHandlerApi.Message> convertMessages(
+      List<com.google.caja.reporting.Message> msgs, final MessageContext mc) {
+    List<GadgetsHandlerApi.Message> result = Lists.newArrayListWithExpectedSize(msgs.size());
+    for (final com.google.caja.reporting.Message m : msgs) {
+      MessageImpl msg = new MessageImpl(m.getMessageType().name(),
+          m.format(mc), convertMessageLevel(m.getMessageLevel().name()));
+      result.add(msg);
+    }
+    return result;
+  }
+
+  public GadgetsHandlerApi.CajaResponse getCaja(GadgetsHandlerApi.CajaRequest request)
+      throws ProcessingException {
+    verifyBaseParams(request, true);
+    Set<String> fields = beanFilter.processBeanFields(request.getFields());
+
+    MessageContext mc = new MessageContext();
+    CajoledResult result =
+      cajaContentRewriter.rewrite(request.getUrl(), request.getContainer(),
+          request.getMimeType(), true /* only support es53 */, request.getDebug());
+    String html = null;
+    String js = null;
+    if (!result.hasErrors && null != result.html) {
+      html = Nodes.render(result.html);
+    }
+
+    if (!result.hasErrors && null != result.js) {
+      StringBuilder builder = new StringBuilder();
+      TokenConsumer tc = request.getDebug() ?
+          new JsPrettyPrinter(new Concatenator(builder))
+          : new JsMinimalPrinter(new Concatenator(builder));
+      RenderContext rc = new RenderContext(tc)
+          .withAsciiOnly(true)
+          .withEmbeddable(true);
+      result.js.render(rc);
+      rc.getOut().noMoreTokens();
+      js = builder.toString();
+    }
+
+    // TODO(jasvir): Improve Caja responses expiration handling
+    return createCajaResponse(request.getUrl(),
+        html, js, convertMessages(result.messages, mc), fields,
+        timeSource.currentTimeMillis() + specRefreshInterval);
+  }
+
+  /**
+   * Verify request parameter are defined.
+   */
+  protected void verifyBaseParams(GadgetsHandlerApi.BaseRequest request, boolean checkUrl)
+      throws ProcessingException {
+    if (checkUrl && request.getUrl() == null) {
+      throw new ProcessingException("Missing url parameter", HttpResponse.SC_BAD_REQUEST);
+    }
+    if (request.getContainer() == null) {
+      throw new ProcessingException("Missing container parameter", HttpResponse.SC_BAD_REQUEST);
+    }
+    if (request.getFields() == null) {
+      throw new ProcessingException("Missing fields parameter", HttpResponse.SC_BAD_REQUEST);
+    }
+  }
+
+  protected Long getProxyExpireMs(ProxyUri proxyUri, @Nullable HttpResponse httpResponse) {
+    if (httpResponse != null) {
+      return httpResponse.getCacheExpiration();
+    } else if (proxyUri.getRefresh() != null) {
+      return timeSource.currentTimeMillis() + proxyUri.getRefresh() * 1000;
+    }
+
+    return getDefaultExpiration();
+  }
+
+  protected long getDefaultExpiration() {
+    return timeSource.currentTimeMillis() + (HttpUtil.getDefaultTtl() * 1000);
+  }
+
+  /**
+   * GadgetContext for metadata request. Used by the gadget processor
+   */
+  protected class MetadataGadgetContext extends GadgetContext {
+
+    private final GadgetsHandlerApi.MetadataRequest request;
+    private final SecurityToken authContext;
+
+    public MetadataGadgetContext(GadgetsHandlerApi.MetadataRequest request) {
+      this.request = request;
+      this.authContext = convertAuthContext(
+          request.getAuthContext(), request.getContainer(), request.getUrl().toString());
+    }
+
+    @Override
+    public Uri getUrl() {
+      return request.getUrl();
+    }
+
+    @Override
+    public String getContainer() {
+      return request.getContainer();
+    }
+
+    @Override
+    public RenderingContext getRenderingContext() {
+      return RenderingContext.METADATA;
+    }
+
+    @Override
+    public long getModuleId() {
+      return 1;
+    }
+
+    @Override
+    public Locale getLocale() {
+      return (request.getLocale() == null ? DEFAULT_LOCALE : request.getLocale());
+    }
+
+    @Override
+    public boolean getIgnoreCache() {
+      return request.getIgnoreCache();
+    }
+
+    @Override
+    public boolean getDebug() {
+      return request.getDebug();
+    }
+
+    @Override
+    public String getView() {
+      return request.getView();
+    }
+
+    @Override
+    public SecurityToken getToken() {
+      return authContext;
+    }
+
+    @Override
+    public boolean getSanitize() {
+      return (request.getRenderingType() == GadgetsHandlerApi.RenderingType.SANITIZED);
+    }
+
+    @Override
+    public boolean getCajoled() {
+      return (request.getRenderingType() == GadgetsHandlerApi.RenderingType.IFRAME_CAJOLED);
+    }
+  }
+
+  private SecurityToken convertAuthContext(GadgetsHandlerApi.AuthContext authContext,
+      String container, String url) {
+    return convertAuthContext(authContext, container, url, 0, url);
+  }
+
+  private SecurityToken convertAuthContext(GadgetsHandlerApi.AuthContext authContext,
+      String container, String url, long moduleId, String activeUrl) {
+    if (authContext == null) {
+      return null;
+    }
+    return beanDelegator.createDelegator(authContext, SecurityToken.class,
+        ImmutableMap.<String, Object>of("container", container,
+            "appid", url, "appurl", url, "moduleId", moduleId, "activeurl", activeUrl));
+  }
+
+  public GadgetsHandlerApi.BaseResponse createErrorResponse(Uri uri, Exception e,
+      String defaultMsg) {
+    if (e instanceof ProcessingException) {
+      ProcessingException processingExc = (ProcessingException) e;
+      return createErrorResponse(uri, processingExc.getHttpStatusCode(),
+          processingExc.getMessage());
+    }
+    LOG.log(Level.WARNING, "Error handling request: " + (uri != null ? uri.toString() : ""), e);
+    return createErrorResponse(uri, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, defaultMsg);
+  }
+
+  public GadgetsHandlerApi.BaseResponse createErrorResponse(Uri url, int code, String error) {
+    GadgetsHandlerApi.Error errorBean = beanDelegator.createDelegator(
+        null, GadgetsHandlerApi.Error.class, ImmutableMap.<String, Object>of(
+          "message", BeanDelegator.nullable(error), "code", code));
+
+    return beanDelegator.createDelegator(error, GadgetsHandlerApi.BaseResponse.class,
+        ImmutableMap.<String, Object>of("url", BeanDelegator.nullable(url), "error", errorBean,
+            "responsetimems", BeanDelegator.NULL, "expiretimems", BeanDelegator.NULL));
+  }
+
+  @VisibleForTesting
+  GadgetsHandlerApi.MetadataResponse createMetadataResponse(Uri url, GadgetSpec spec, Map<String,
+        Uri> iframeUris, Boolean needsTokenRefresh, Set<String> fields, Long expireTime,
+        Integer tokenTTL, Set<String> rpcServiceIds) {
+    return (GadgetsHandlerApi.MetadataResponse) beanFilter.createFilteredBean(
+        beanDelegator.createDelegator(spec, GadgetsHandlerApi.MetadataResponse.class,
+            ImmutableMap.<String, Object>builder()
+                .put("url", url)
+                .put("error", BeanDelegator.NULL)
+                .put("iframeurls", BeanDelegator.nullable(iframeUris))
+                .put("needstokenrefresh", BeanDelegator.nullable(needsTokenRefresh))
+                .put("responsetimems", timeSource.currentTimeMillis())
+                .put("expiretimems", BeanDelegator.nullable(expireTime))
+                .put("rpcserviceids", BeanDelegator.nullable(rpcServiceIds))
+                .put("tokenttl", BeanDelegator.nullable(tokenTTL)).build()),
+        fields);
+  }
+
+  @VisibleForTesting
+  GadgetsHandlerApi.TokenResponse createTokenResponse(Uri url, String token, Set<String> fields,
+      Long tokenExpire, Integer tokenTTL, Long moduleId) {
+    return (GadgetsHandlerApi.TokenResponse) beanFilter.createFilteredBean(
+        beanDelegator.createDelegator(null, GadgetsHandlerApi.TokenResponse.class,
+            ImmutableMap.<String, Object>builder()
+                .put("url", url)
+                .put("error", BeanDelegator.NULL)
+                .put("token", BeanDelegator.nullable(token))
+                .put("responsetimems", timeSource.currentTimeMillis())
+                .put("expiretimems", BeanDelegator.nullable(tokenExpire))
+                .put("tokenttl", BeanDelegator.nullable(tokenTTL))
+                .put("moduleid", BeanDelegator.nullable(moduleId))
+            .build()
+        ),
+        fields
+    );
+  }
+
+  protected JsUri createJsUri(GadgetsHandlerApi.JsRequest request) {
+    RenderingContext context = (RenderingContext)
+    (request.getContext() != null ?
+        // TODO: Figure out why maven complain about casting and clean the dummy cast
+        (Object) beanDelegator.convertEnum(request.getContext())
+        : RenderingContext.GADGET);
+
+    return new JsUri(request.getRefresh(), request.getDebug(), request.getIgnoreCache(),
+        request.getContainer(), request.getGadget(), request.getFeatures(),
+        request.getLoadedFeatures(), request.getOnload(), false, false, context, request.getUrl(),
+        request.getRepository());
+  }
+
+  @VisibleForTesting
+  GadgetsHandlerApi.JsResponse createJsResponse(Uri url, Uri jsUri, String content,
+      Set<String> fields, Long expireMs) {
+    return (GadgetsHandlerApi.JsResponse) beanFilter.createFilteredBean(
+        beanDelegator.createDelegator(null, GadgetsHandlerApi.JsResponse.class,
+            ImmutableMap.<String, Object>builder()
+                .put("url", BeanDelegator.nullable(url))
+                .put("error", BeanDelegator.NULL)
+                .put("jsurl", jsUri)
+                .put("jscontent", BeanDelegator.nullable(content))
+                .put("responsetimems", timeSource.currentTimeMillis())
+                .put("expiretimems", BeanDelegator.nullable(expireMs)).build()),
+        fields);
+  }
+
+  protected ProxyUri createProxyUri(GadgetsHandlerApi.ProxyRequest request) {
+    ProxyUriManager.ProxyUri proxyUri = new ProxyUriManager.ProxyUri(request.getRefresh(),
+        request.getDebug(), request.getIgnoreCache(), request.getContainer(),
+        request.getGadget(), request.getUrl());
+
+    proxyUri.setFallbackUrl(request.getFallbackUrl())
+        .setRewriteMimeType(request.getRewriteMimeType())
+        .setSanitizeContent(request.getSanitize());
+
+    GadgetsHandlerApi.ImageParams image = request.getImageParams();
+    if (image != null) {
+      proxyUri.setResize( image.getWidth(), image.getHeight(),
+          image.getQuality(), image.getDoNotExpand());
+    }
+    return proxyUri;
+  }
+
+  @VisibleForTesting
+  GadgetsHandlerApi.ProxyResponse createProxyResponse(Uri uri, HttpResponse httpResponse,
+      Set<String> fields, Long expireMs) throws IOException {
+
+    GadgetsHandlerApi.HttpResponse beanHttp = null;
+    if (httpResponse != null) {
+      String content = "";
+      if (httpResponse.getContentLength() > 0) {
+        // Stream out the base64-encoded data.
+        // Ctor args indicate to encode w/o line breaks.
+        Base64InputStream b64input =
+            new Base64InputStream(httpResponse.getResponse(), true, 0, null);
+        content = IOUtils.toString(b64input);
+      }
+
+      ImmutableList.Builder<GadgetsHandlerApi.NameValuePair> headersBuilder =
+          ImmutableList.builder();
+      for (final Map.Entry<String, String> entry : httpResponse.getHeaders().entries()) {
+        headersBuilder.add(
+            beanDelegator.createDelegator(null, GadgetsHandlerApi.NameValuePair.class,
+                ImmutableMap.<String, Object>of("name", entry.getKey(), "value", entry.getValue()))
+        );
+      }
+
+      beanHttp = beanDelegator.createDelegator(null, GadgetsHandlerApi.HttpResponse.class,
+          ImmutableMap.<String, Object>of(
+              "code", httpResponse.getHttpStatusCode(),
+              "encoding", httpResponse.getEncoding(),
+              "contentbase64", content,
+              "headers", headersBuilder.build()));
+    }
+
+    return (GadgetsHandlerApi.ProxyResponse) beanFilter.createFilteredBean(
+      beanDelegator.createDelegator(null, GadgetsHandlerApi.ProxyResponse.class,
+          ImmutableMap.<String, Object>builder()
+              .put("proxyurl", uri)
+              .put("proxycontent", BeanDelegator.nullable(beanHttp))
+              .put("url", BeanDelegator.NULL)
+              .put("error", BeanDelegator.NULL)
+              .put("responsetimems", timeSource.currentTimeMillis())
+              .put("expiretimems", BeanDelegator.nullable(expireMs))
+              .build()),
+      fields);
+  }
+
+  @VisibleForTesting
+  GadgetsHandlerApi.CajaResponse createCajaResponse(Uri uri, String html, String js,
+      List<GadgetsHandlerApi.Message> messages, Set<String> fields, Long expireMs) {
+    ImmutableList.Builder<GadgetsHandlerApi.Message> msgBuilder =
+      ImmutableList.builder();
+    for (final GadgetsHandlerApi.Message m : messages) {
+      msgBuilder.add(
+        beanDelegator.createDelegator(null, GadgetsHandlerApi.Message.class,
+            ImmutableMap.<String, Object>of("name", m.getName(),
+                "level", m.getLevel(), "message", m.getMessage())));
+    }
+
+    return (GadgetsHandlerApi.CajaResponse) beanFilter.createFilteredBean(
+        beanDelegator.createDelegator(null, GadgetsHandlerApi.CajaResponse.class,
+            ImmutableMap.<String, Object>builder()
+            .put("url", uri)
+            .put("html", BeanDelegator.nullable(html))
+            .put("js", BeanDelegator.nullable(js))
+            .put("messages", msgBuilder.build())
+            .put("error", BeanDelegator.NULL)
+            .put("responsetimems", timeSource.currentTimeMillis())
+            .put("expiretimems", BeanDelegator.nullable(expireMs))
+            .build()),
+            fields);
+  }
+
+  private static class MessageImpl implements GadgetsHandlerApi.Message {
+    private final GadgetsHandlerApi.MessageLevel level;
+    private final String message;
+    private final String name;
+
+    public MessageImpl(String name, String message, GadgetsHandlerApi.MessageLevel level) {
+      this.name = name;
+      this.message = message;
+      this.level = level;
+    }
+
+    public GadgetsHandlerApi.MessageLevel getLevel() {
+      return level;
+    }
+
+    public String getMessage() {
+      return message;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HtmlAccelServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HtmlAccelServlet.java
new file mode 100644
index 0000000..3d5cbe3
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HtmlAccelServlet.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.inject.Inject;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.uri.AccelUriManager;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Handles requests for accel servlet.
+ * The objective is to accelerate web pages.
+ *
+ * @since 2.0.0
+ */
+public class HtmlAccelServlet extends InjectedServlet {
+  private static final long serialVersionUID = -424353123863813052L;
+
+  private static final Logger logger = Logger.getLogger(
+      HtmlAccelServlet.class.getName());
+  private transient AccelHandler accelHandler;
+
+  @Inject
+  public void setHandler(AccelHandler accelHandler) {
+    checkInitialized();
+    this.accelHandler = accelHandler;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse servletResponse)
+      throws IOException {
+    if (logger.isLoggable(Level.FINE)) {
+      logger.fine("Accel request = " + request.toString());
+    }
+
+    HttpRequest req = ServletUtil.fromHttpServletRequest(request);
+    req.setContainer(AccelUriManager.CONTAINER);
+    HttpResponse response;
+    try {
+      response = accelHandler.fetch(req);
+    } catch (GadgetException e) {
+      response = ServletUtil.errorResponse(e);
+    }
+
+    ServletUtil.copyToServletResponse(response, servletResponse);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+    doGet(request, response);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java
new file mode 100644
index 0000000..3f1eaac
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpGadgetContext.java
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.UserPrefs;
+import org.apache.shindig.gadgets.uri.UriCommon;
+
+import com.google.common.collect.Maps;
+
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Implements GadgetContext using an HttpServletRequest
+ */
+public class HttpGadgetContext extends GadgetContext {
+  public static final String USERPREF_PARAM_PREFIX = "up_";
+
+  private final HttpServletRequest request;
+
+  private final String container;
+  private final Boolean debug;
+  private final Boolean ignoreCache;
+  private final Locale locale;
+  private final Integer moduleId;
+  private final RenderingContext renderingContext;
+  private final Uri url;
+  private final UserPrefs userPrefs;
+  private final String view;
+  private final String referer;
+
+  public HttpGadgetContext(HttpServletRequest request) {
+    this.request = request;
+
+    container = getContainer(request);
+    debug = getDebug(request);
+    ignoreCache = getIgnoreCache(request);
+    locale = getLocale(request);
+    moduleId = getModuleId(request);
+    renderingContext = getRenderingContext(request);
+    url = getUrl(request);
+    userPrefs = getUserPrefs(request);
+    view = getView(request);
+    referer = getReferer();
+  }
+
+  @Override
+  public String getParameter(String name) {
+    return request.getParameter(name);
+  }
+
+  @Override
+  public String getContainer() {
+    return container == null ? super.getContainer() : container;
+  }
+
+  @Override
+  public String getHost() {
+    String host = request.getHeader("Host");
+    return host == null ? super.getHost() : host;
+  }
+
+  @Override
+  public String getHostSchema() {
+    String schema = request.getScheme();
+    return schema == null ? super.getHostSchema() : schema;
+  }
+
+  @Override
+  public String getUserIp() {
+    String ip = request.getRemoteAddr();
+    return ip == null ? super.getUserIp() : ip;
+  }
+
+  @Override
+  public boolean getDebug() {
+    return debug == null ? super.getDebug(): debug;
+  }
+
+  @Override
+  public boolean getIgnoreCache() {
+    if (ignoreCache == null) {
+      return super.getIgnoreCache();
+    }
+    return ignoreCache;
+  }
+
+  @Override
+  public Locale getLocale() {
+    return locale == null ? super.getLocale() : locale;
+  }
+
+  @Override
+  public long getModuleId() {
+    return moduleId == null ? super.getModuleId() : moduleId;
+  }
+
+  @Override
+  public RenderingContext getRenderingContext() {
+    return renderingContext == null ? super.getRenderingContext() : renderingContext;
+  }
+
+  @Override
+  public SecurityToken getToken() {
+    return AuthInfoUtil.getSecurityTokenFromRequest(request);
+  }
+
+  @Override
+  public Uri getUrl() {
+    return url == null ? super.getUrl() : url;
+  }
+
+  @Override
+  public UserPrefs getUserPrefs() {
+    if (userPrefs == null) {
+      return super.getUserPrefs();
+    }
+    return userPrefs;
+  }
+
+  @Override
+  public String getView() {
+    if (view == null) {
+      return super.getView();
+    }
+    return view;
+  }
+
+  @Override
+  public String getUserAgent() {
+    String userAgent = request.getHeader("User-Agent");
+    if (userAgent == null) {
+      return super.getUserAgent();
+    }
+    return userAgent;
+  }
+
+  @Override
+  public String getRepository() {
+    String repository = request.getHeader(UriCommon.Param.REPOSITORY_ID.getKey());
+    if (repository == null) {
+      return super.getRepository();
+    }
+    return repository;
+  }
+
+  @Override
+  public String getReferer() {
+    String referer = request.getHeader("Referer");
+    return referer == null ? super.getReferer() : referer;
+  }
+
+  /**
+   * @param req
+   * @return The container, if set, or null.
+   */
+  @SuppressWarnings("deprecation")
+  private static String getContainer(HttpServletRequest req) {
+    String container = req.getParameter(UriCommon.Param.CONTAINER.getKey());
+    if (container == null) {
+      // The parameter used to be called 'synd' FIXME: schedule removal
+      container = req.getParameter(UriCommon.Param.SYND.getKey());
+    }
+    return container;
+  }
+
+  /**
+   * @param req
+   * @return Debug setting, if set, or null.
+   */
+  private static Boolean getDebug(HttpServletRequest req) {
+    String debug = req.getParameter(UriCommon.Param.DEBUG.getKey());
+    if (debug == null) {
+      return Boolean.FALSE;
+    } else if ("0".equals(debug)) {
+      return Boolean.FALSE;
+    }
+    return Boolean.TRUE;
+  }
+
+  /**
+   * @param req
+   * @return The ignore cache setting, if appropriate params are set, or null.
+   */
+  private static Boolean getIgnoreCache(HttpServletRequest req) {
+    String ignoreCache = req.getParameter(UriCommon.Param.NO_CACHE.getKey());
+    if (ignoreCache == null) {
+      return Boolean.FALSE;
+    } else if ("0".equals(ignoreCache)) {
+      return Boolean.FALSE;
+    }
+    return Boolean.TRUE;
+  }
+
+  /**
+   * @param req
+   * @return The locale, if appropriate parameters are set, or null.
+   */
+  private static Locale getLocale(HttpServletRequest req) {
+    String language = req.getParameter(UriCommon.Param.LANG.getKey());
+    String country = req.getParameter(UriCommon.Param.COUNTRY.getKey());
+    if (language == null && country == null) {
+      return null;
+    } else if (language == null) {
+      language = "all";
+    } else if (country == null) {
+      country = "ALL";
+    }
+    return new Locale(language, country);
+  }
+
+  /**
+   * @param req
+   * @return module id, if specified
+   */
+  @SuppressWarnings("boxing")
+  private static Integer getModuleId(HttpServletRequest req) {
+    String mid = req.getParameter("mid");
+    if (mid == null) {
+      return null;
+    }
+
+    try {
+      return Integer.parseInt(mid);
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  /**
+   * @param req
+   * @return The rendering context, if appropriate params are set, or null.
+   */
+  private static RenderingContext getRenderingContext(HttpServletRequest req) {
+    String c = req.getParameter(UriCommon.Param.CONTAINER_MODE.getKey());
+    if (c == null) {
+      return null;
+    }
+    return RenderingContext.valueOfParam(c);
+  }
+
+  /**
+   * @param req
+   * @return The ignore cache setting, if appropriate params are set, or null.
+   */
+  private static Uri getUrl(HttpServletRequest req) {
+    String url = req.getParameter(UriCommon.Param.URL.getKey());
+    if (url == null) {
+      return null;
+    }
+    try {
+      return Uri.parse(url);
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  /**
+   * @param req
+   * @return UserPrefs, if any are set for this request.
+   */
+  @SuppressWarnings("unchecked")
+  private static UserPrefs getUserPrefs(HttpServletRequest req) {
+    Map<String, String> prefs = Maps.newHashMap();
+    Enumeration<String> paramNames = req.getParameterNames();
+    if (paramNames == null) {
+      return null;
+    }
+    while (paramNames.hasMoreElements()) {
+      String paramName = paramNames.nextElement();
+      if (paramName.startsWith(USERPREF_PARAM_PREFIX)) {
+        String prefName = paramName.substring(USERPREF_PARAM_PREFIX.length());
+        prefs.put(prefName, req.getParameter(paramName));
+      }
+    }
+    return new UserPrefs(prefs);
+  }
+
+  /**
+   * @param req
+   * @return The view, if specified, or null.
+   */
+  private static String getView(HttpServletRequest req) {
+    return req.getParameter(UriCommon.Param.VIEW.getKey());
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpRequestHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpRequestHandler.java
new file mode 100644
index 0000000..ff334e8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpRequestHandler.java
@@ -0,0 +1,614 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.JsonProperty;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.FeedProcessor;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.apache.shindig.protocol.BaseRequestItem;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.Service;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * An alternate implementation of the Http proxy service using the standard API dispatcher for REST
+ * / JSON-RPC calls. The basic form of the request is as follows
+ * ...
+ * method : http.<HTTP method name>
+ * params : {
+ *    href : <endpoint to fetch content from>,
+ *    headers : { <header-name> : [<header-value>, ...]},
+ *    format : <"text", "json", "feed">
+ *    body : <request body>
+ *    gadget : <url of gadget spec for calling application>
+ *    authz: : <none | oauth | signed>,
+ *    sign_owner: <boolean, default true>
+ *    sign_viewer: <boolean, default true>
+ *    ...<additional auth arguments. See OAuthArguments>
+ *    refreshInterval : <Integer time in seconds to force as cache TTL. Default is to use response headers>
+ *    noCache : <Bypass container content cache. Default false>
+ *    sanitize : <Force sanitize fetched content. Default false>
+ *    summarize : <If contentType == "FEED" summarize the results. Default false>
+ *    entryCount : <If contentType == "FEED" limit results to specified no of items. Default 3>
+ * }
+ *
+ * A successful response response will have the form
+ *
+ * data : {
+ *    status : <HTTP status code.>
+ *    headers : { <header name> : [<header val1>, <header val2>, ...], ...}
+ *    content : <response body>: string if 'text', JSON is 'feed' or 'json' format
+ *    token : <If security token provides a renewed value.>
+ *    metadata : { <metadata entry> : <metadata value>, ...}
+ * }
+ *
+ * It's important to note that requests which generate HTTP error responses such as 500 are returned
+ * in the above format. The RPC itself succeeded in these cases. If an RPC error occurred the client
+ * should introspect the error message for information as to the cause.
+ *
+ * TODO: send errors using "result", not plain content
+ *
+ * @see MakeRequestHandler
+ */
+@Service(name = "http")
+public class HttpRequestHandler {
+
+  static final Set<String> BAD_HEADERS = ImmutableSet.of("HOST", "ACCEPT-ENCODING");
+
+  private static final String CLASSNAME = HttpRequestHandler.class.getName();
+  private static final Logger LOG = Logger.getLogger(CLASSNAME, MessageKeys.MESSAGES);
+
+  private final RequestPipeline requestPipeline;
+  private final ResponseRewriterRegistry contentRewriterRegistry;
+  private final Provider<FeedProcessor> feedProcessorProvider;
+  private final Processor processor;
+
+  @Inject
+  public HttpRequestHandler(RequestPipeline requestPipeline,
+      @RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT)
+      ResponseRewriterRegistry contentRewriterRegistry,
+      Provider<FeedProcessor> feedProcessorProvider,
+      Processor processor) {
+    this.requestPipeline = requestPipeline;
+    this.contentRewriterRegistry = contentRewriterRegistry;
+    this.feedProcessorProvider = feedProcessorProvider;
+    this.processor = processor;
+  }
+
+
+  /** Execute an HTTP GET request */
+  @Operation(httpMethods = {"POST","GET"}, path = "/get")
+  public HttpApiResponse get(BaseRequestItem request) {
+    HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
+    assertNoBody(httpReq, "GET");
+    return execute("GET", httpReq, request);
+  }
+
+  /** Execute an HTTP POST request */
+  @Operation(httpMethods = "POST", path = "/post")
+  public HttpApiResponse post(BaseRequestItem request) {
+    HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
+    return execute("POST", httpReq, request);
+  }
+
+  /** Execute an HTTP PUT request */
+  @Operation(httpMethods = "POST", path = "/put")
+  public HttpApiResponse put(BaseRequestItem request) {
+    HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
+    return execute("PUT", httpReq, request);
+  }
+
+  /** Execute an HTTP DELETE request */
+  @Operation(httpMethods = "POST", path = "/delete")
+  public HttpApiResponse delete(BaseRequestItem request) {
+    HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
+    assertNoBody(httpReq, "DELETE");
+    return execute("DELETE", httpReq, request);
+  }
+
+  /** Execute an HTTP HEAD request */
+  @Operation(httpMethods = {"POST","GET"}, path = "/head")
+  public HttpApiResponse head(BaseRequestItem request) {
+    HttpApiRequest httpReq = request.getTypedRequest(HttpApiRequest.class);
+    assertNoBody(httpReq, "HEAD");
+    return execute("HEAD", httpReq, request);
+  }
+
+  private void assertNoBody(HttpApiRequest httpRequest, String method) {
+    if (httpRequest.body != null) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+         "Request body not supported for " + method);
+    }
+  }
+
+  /**
+   * Dispatch the request
+   */
+  private HttpApiResponse execute(String method, HttpApiRequest httpApiRequest,
+      final BaseRequestItem requestItem) {
+    if (httpApiRequest.href == null) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "href parameter is missing");
+    }
+
+    // Canonicalize the path
+    Uri href = normalizeUrl(httpApiRequest.href);
+    try {
+      HttpRequest req = new HttpRequest(href);
+      req.setMethod(method);
+      if (httpApiRequest.body != null) {
+        req.setPostBody(httpApiRequest.body.getBytes());
+      }
+
+      // Copy over allowed headers
+      for (Map.Entry<String, List<String>> header : httpApiRequest.headers.entrySet()) {
+        if (!BAD_HEADERS.contains(header.getKey().trim().toUpperCase())) {
+          for (String value : header.getValue()) {
+            req.addHeader(header.getKey(), value);
+          }
+        }
+      }
+
+      // Extract the gadget URI from the request or the security token
+      final Uri gadgetUri = getGadgetUri(requestItem.getToken(), httpApiRequest);
+      if (gadgetUri == null) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+            "Gadget URI not specified in request");
+      }
+      req.setGadget(gadgetUri);
+
+      // Detect the authz parsing
+      if (httpApiRequest.authz != null) {
+        req.setAuthType(AuthType.parse(httpApiRequest.authz));
+      }
+
+      req.setSecurityToken(requestItem.getToken());
+
+      final AuthType authType = req.getAuthType();
+      if (authType != AuthType.NONE) {
+        if (authType == AuthType.OAUTH2) {
+          Map<String, String> authSettings = getAuthSettings(requestItem);
+          OAuth2Arguments oauth2Args = new OAuth2Arguments(req.getAuthType(), authSettings);
+
+          req.setOAuth2Arguments(oauth2Args);
+        } else {
+          Map<String, String> authSettings = getAuthSettings(requestItem);
+          OAuthArguments oauthArgs = new OAuthArguments(req.getAuthType(), authSettings);
+          oauthArgs.setSignOwner(httpApiRequest.signOwner);
+          oauthArgs.setSignViewer(httpApiRequest.signViewer);
+
+          req.setOAuthArguments(oauthArgs);
+        }
+      }
+
+      // TODO: Allow the rewriter to use an externally forced mime type. This is needed
+      // allows proper rewriting of <script src="x"/> where x is returned with
+      // a content type like text/html which unfortunately happens all too often
+
+      req.setIgnoreCache(httpApiRequest.noCache);
+      req.setSanitizationRequested(httpApiRequest.sanitize);
+
+      // If the proxy request specifies a refresh param then we want to force the min TTL for
+      // the retrieved entry in the cache regardless of the headers on the content when it
+      // is fetched from the original source.
+      if (httpApiRequest.refreshInterval != null) {
+        req.setCacheTtl(httpApiRequest.refreshInterval);
+      }
+
+      final HttpRequest request = req;
+      HttpResponse results = requestPipeline.execute(req);
+      GadgetContext context = new GadgetContext() {
+        @Override
+        public Uri getUrl() {
+          return gadgetUri;
+        }
+
+        @Override
+        public String getParameter(String key) {
+          return request.getParam(key);
+        }
+
+        @Override
+        public boolean getIgnoreCache() {
+          return request.getIgnoreCache();
+        }
+
+        @Override
+        public String getContainer() {
+          return requestItem.getToken().getContainer();
+        }
+
+        @Override
+        public boolean getDebug() {
+          return "1".equalsIgnoreCase(getParameter(Param.DEBUG.getKey()));
+        }
+      };
+      // TODO: os:HttpRequest and Preload do not use the content rewriter.
+      // Should we really do so here?
+      try {
+        Gadget gadget = processor.process(context);
+        results = contentRewriterRegistry.rewriteHttpResponse(req, results, gadget);
+      } catch (ProcessingException e) {
+        //If there is an error creating the gadget object just rewrite the content without
+        //the gadget object.  This will result in any content rewrite params not being
+        //honored, but its better than the request failing all together.
+        if(LOG.isLoggable(Level.WARNING)) {
+          LOG.logp(Level.WARNING, CLASSNAME, "execute", MessageKeys.GADGET_CREATION_ERROR, e);
+        }
+        results = contentRewriterRegistry.rewriteHttpResponse(req, results, null);
+      }
+
+      HttpApiResponse httpApiResponse = new HttpApiResponse(results,
+          transformBody(httpApiRequest, results),
+          httpApiRequest);
+
+      // Renew the security token if we can
+      if (requestItem.getToken() != null) {
+        String updatedAuthToken = requestItem.getToken().getUpdatedToken();
+        if (updatedAuthToken != null) {
+          httpApiResponse.token = updatedAuthToken;
+        }
+      }
+      return httpApiResponse;
+    } catch (GadgetException ge) {
+      throw new ProtocolException(ge.getHttpStatusCode(), ge.getMessage(), ge);
+    } catch (RewritingException re) {
+      throw new ProtocolException(re.getHttpStatusCode(),
+          re.getMessage(), re);
+    }
+  }
+
+  /**
+   * Extract all unknown keys into a map for extra auth params.
+   */
+  private Map<String, String> getAuthSettings(BaseRequestItem requestItem) {
+    // Keys in a request item are always Strings
+    @SuppressWarnings("unchecked")
+    Set<String> allParameters = requestItem.getTypedRequest(Map.class).keySet();
+
+    Map<String, String> authSettings = Maps.newHashMap();
+    for (String paramName : allParameters) {
+      if (!HttpApiRequest.KNOWN_PARAMETERS.contains(paramName)) {
+        authSettings.put(paramName, requestItem.getParameter(paramName));
+      }
+    }
+
+    return authSettings;
+  }
+
+  /** Helper method to normalize Uri that does not have scheme or empty path  */
+  protected Uri normalizeUrl(Uri url) {
+    if (url.getScheme() == null) {
+      // Assume http
+      url = new UriBuilder(url).setScheme("http").toUri();
+    }
+
+    if (url.getPath() == null || url.getPath().length() == 0) {
+      url = new UriBuilder(url).setPath("/").toUri();
+    }
+
+    return url;
+  }
+
+
+  /** Format a response as JSON, including additional JSON inserted by chained content fetchers. */
+  protected Object transformBody(HttpApiRequest request, HttpResponse results)
+      throws GadgetException {
+    String body = results.getResponseAsString();
+    if ("feed".equalsIgnoreCase(request.format)) {
+      return processFeed(request, body);
+    } else if ("json".equalsIgnoreCase(request.format)) {
+      try {
+        body = body.trim();
+        if(body.length() > 0 && body.charAt(0) == '[') {
+          return new JSONArray(body);
+        } else {
+          return new JSONObject(body);
+        }
+      } catch (JSONException e) {
+        // TODO: include data block with invalid JSON
+        throw new ProtocolException(HttpServletResponse.SC_NOT_ACCEPTABLE, "Response not valid JSON", e);
+      }
+    }
+
+    return body;
+  }
+
+  /** Processes a feed (RSS or Atom) using FeedProcessor. */
+  protected Object processFeed(HttpApiRequest req, String responseBody)
+      throws GadgetException {
+    return feedProcessorProvider.get().process(req.href.toString(), responseBody, req.summarize,
+        req.entryCount);
+  }
+
+  /** Extract the gadget URL from the request or the security token */
+  protected Uri getGadgetUri(SecurityToken token, HttpApiRequest httpApiRequest) {
+    if (token != null && token.getAppUrl() != null) {
+      return Uri.parse(token.getAppUrl());
+    }
+    return null;
+  }
+
+  /**
+   * Simple type that represents an Http request to execute on the callers behalf
+   */
+  public static class HttpApiRequest {
+    static final Set<String> KNOWN_PARAMETERS = ImmutableSet.of(
+        "alias", "href", "headers", "body", "gadget", "authz", "sign_owner",
+        "sign_viewer", "format", "refreshInterval", "noCache", "sanitize",
+        "summarize", "entryCount");
+
+    // Content to fetch / execute
+    Uri href;
+
+    Map<String, List<String>> headers = Maps.newHashMap();
+
+    /** POST body */
+    String body;
+
+    /** Authorization type ("none", "signed", "oauth") */
+    String authz = "none";
+
+    /** Should the request be signed by owner? */
+    boolean signOwner = true;
+
+    /** Should the request be signed by viewer? */
+    boolean signViewer = true;
+
+    // The format type to coerce the response into. Supported values are
+    // "text", "json", and "feed".
+    String format;
+
+    // Use Integer here to allow for null
+    Integer refreshInterval;
+
+    // Bypass http caches
+    boolean noCache;
+
+    // Use HTML/CSS sanitizer
+    boolean sanitize;
+
+    // Control feed handling
+    boolean summarize;
+    int entryCount = 3;
+
+    public Uri getHref() {
+      return href;
+    }
+
+    public void setHref(Uri url) {
+      this.href = url;
+    }
+
+    public Map<String, List<String>> getHeaders() {
+      return headers;
+    }
+
+    public void setHeaders(Map<String, List<String>> headers) {
+      this.headers = headers;
+    }
+
+    public String getBody() {
+      return body;
+    }
+
+    public void setBody(String body) {
+      this.body = body;
+    }
+
+    public Integer getRefreshInterval() {
+      return refreshInterval;
+    }
+
+    public void setRefreshInterval(Integer refreshInterval) {
+      this.refreshInterval = refreshInterval;
+    }
+
+    public boolean isNoCache() {
+      return noCache;
+    }
+
+    public void setNoCache(boolean noCache) {
+      this.noCache = noCache;
+    }
+
+    public boolean isSanitize() {
+      return sanitize;
+    }
+
+    public void setSanitize(boolean sanitize) {
+      this.sanitize = sanitize;
+    }
+
+    public String getFormat() {
+      return format;
+    }
+
+    public void setFormat(String format) {
+      this.format = format;
+    }
+
+    public String getAuthz() {
+      return authz;
+    }
+
+    public void setAuthz(String authz) {
+      this.authz = authz;
+    }
+
+    public boolean isSignViewer() {
+      return signViewer;
+    }
+
+    @JsonProperty("sign_viewer")
+    public void setSignViewer(boolean signViewer) {
+      this.signViewer = signViewer;
+    }
+
+    public boolean isSignOwner() {
+      return signOwner;
+    }
+
+    @JsonProperty("sign_owner")
+    public void setSignOwner(boolean signOwner) {
+      this.signOwner = signOwner;
+    }
+
+    public boolean isSummarize() {
+      return summarize;
+    }
+
+    public void setSummarize(boolean summarize) {
+      this.summarize = summarize;
+    }
+
+    public int getEntryCount() {
+      return entryCount;
+    }
+
+    public void setEntryCount(int entryCount) {
+      this.entryCount = entryCount;
+    }
+  }
+
+  /**
+   * Response to request for Http content
+   */
+  public static class HttpApiResponse {
+    // Http status code
+    int status;
+
+    // Returned headers
+    Map<String, Collection<String>> headers;
+
+    // Body content, either a String or a JSON-type structure
+    Object content;
+
+    // Renewed security token if available
+    String token;
+
+    // Metadata associated with the response.
+    Map<String, String> metadata;
+
+    public HttpApiResponse(int status) {
+      this.status = status;
+    }
+
+    /**
+     * Construct response based on HttpResponse from fetcher
+     */
+    public HttpApiResponse(HttpResponse response, Object content, HttpApiRequest httpApiRequest) {
+      this.status = response.getHttpStatusCode();
+      this.headers = new TreeMap<String, Collection<String>>(String.CASE_INSENSITIVE_ORDER);
+
+      if (response.getHeaders().containsKey("set-cookie")) {
+        this.headers.put("set-cookie", response.getHeaders("set-cookie"));
+      }
+      if (response.getHeaders().containsKey("location")) {
+        this.headers.put("location", response.getHeaders("location"));
+      }
+
+      this.content = content;
+
+      this.metadata = response.getMetadata();
+    }
+
+    public int getStatus() {
+      return status;
+    }
+
+    public void setStatus(int status) {
+      this.status = status;
+    }
+
+    public Map<String, Collection<String>> getHeaders() {
+      return headers;
+    }
+
+    public void setHeaders(Map<String, Collection<String>> headers) {
+      this.headers = headers;
+    }
+
+    public Object getContent() {
+      return content;
+    }
+
+    public void setContent(Object content) {
+      this.content = content;
+    }
+
+    public String getToken() {
+      return token;
+    }
+
+    public void setToken(String token) {
+      this.token = token;
+    }
+
+    public Map<String, String> getMetadata() {
+      // TODO - Review this once migration of JS occurs. Currently MakeRequestHandler suppresses
+      // this on output but that choice may not be the best one for compatibility.
+      // Suppress metadata on output if it's empty
+      if (metadata != null && metadata.isEmpty()) {
+        return null;
+      }
+      return metadata;
+    }
+
+    public void setMetadata(Map<String, String> metadata) {
+      this.metadata = metadata;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsServlet.java
new file mode 100644
index 0000000..7861909
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsServlet.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.js.JsException;
+import org.apache.shindig.gadgets.js.JsRequest;
+import org.apache.shindig.gadgets.js.JsRequestBuilder;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.js.JsServingPipeline;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Simple servlet serving up JavaScript files by their registered aliases.
+ * Used by type=URL gadgets in loading JavaScript resources.
+ */
+public class JsServlet extends InjectedServlet {
+  private static final long serialVersionUID = 6255917470412008175L;
+
+  private JsServingPipeline jsServingPipeline;
+  private CachingSetter cachingSetter;
+
+  private JsRequestBuilder jsRequestBuilder;
+
+  @VisibleForTesting
+  static class CachingSetter {
+    public void setCachingHeaders(HttpServletResponse resp, int ttl, boolean noProxy) {
+      if (ttl < 0) {
+        HttpUtil.setCachingHeaders(resp, noProxy);
+      } else if (ttl == 0) {
+        HttpUtil.setNoCache(resp);
+      } else {
+        HttpUtil.setCachingHeaders(resp, ttl, noProxy);
+      }
+    }
+  }
+
+  @Inject
+  public void setJsRequestBuilder(JsRequestBuilder jsRequestBuilder) {
+    checkInitialized();
+    this.jsRequestBuilder = jsRequestBuilder;
+  }
+
+  @Inject
+  public void setCachingSetter(CachingSetter cachingSetter) {
+    this.cachingSetter = cachingSetter;
+  }
+
+  @Inject
+  public void setJsServingPipeline(JsServingPipeline jsServingPipeline) {
+    this.jsServingPipeline = jsServingPipeline;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+      throws IOException {
+
+    JsRequest jsRequest;
+    try {
+      jsRequest = jsRequestBuilder.build(req);
+    } catch (GadgetException e) {
+      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      return;
+    }
+
+    JsResponse jsResponse;
+    try {
+      jsResponse = jsServingPipeline.execute(jsRequest);
+    } catch (JsException e) {
+      resp.sendError(e.getStatusCode(), e.getMessage());
+      return;
+    }
+
+    emitJsResponse(jsResponse, req, resp);
+  }
+
+  protected void emitJsResponse(JsResponse jsResponse, HttpServletRequest req,
+      HttpServletResponse resp) throws IOException {
+    if (jsResponse.getStatusCode() == HttpServletResponse.SC_NOT_MODIFIED) {
+      resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+      cachingSetter.setCachingHeaders(
+          resp, jsResponse.getCacheTtlSecs(), !jsResponse.isProxyCacheable());
+      return;
+    }
+    if (jsResponse.getStatusCode() == HttpServletResponse.SC_OK && jsResponse.toJsString().length() == 0) {
+      resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+
+    cachingSetter.setCachingHeaders(
+        resp, jsResponse.getCacheTtlSecs(), !jsResponse.isProxyCacheable());
+
+    resp.setStatus(jsResponse.getStatusCode());
+    resp.setContentType("text/javascript; charset=utf-8");
+    byte[] response = CharsetUtil.getUtf8Bytes(jsResponse.toJsString());
+    resp.setContentLength(response.length);
+    resp.getOutputStream().write(response);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsonRpcGadgetContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsonRpcGadgetContext.java
new file mode 100644
index 0000000..0a87995
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsonRpcGadgetContext.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.UserPrefs;
+
+import com.google.common.collect.Maps;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Extracts context from JSON input.
+ */
+public class JsonRpcGadgetContext extends GadgetContext {
+  private final JSONObject context;
+  private final JSONObject gadget;
+
+  private final String container;
+  private final Boolean debug;
+  private final Boolean ignoreCache;
+  private final Locale locale;
+  private final Long moduleId;
+  private final Uri url;
+  private final UserPrefs userPrefs;
+  private final String view;
+
+  /**
+   * @param context Request global parameters.
+   * @param gadget Values for the gadget being rendered.
+   * @throws JSONException If parameters can't be extracted or aren't correctly formed.
+   */
+  public JsonRpcGadgetContext(JSONObject context, JSONObject gadget) throws JSONException {
+    this.context = context;
+    this.gadget = gadget;
+
+    url = getUrl(gadget);
+    moduleId = getModuleId(gadget);
+    userPrefs = getUserPrefs(gadget);
+    locale = getLocale(context);
+    view = context.optString("view");
+    ignoreCache = context.optBoolean("ignoreCache");
+    container = context.optString("container");
+    debug = context.optBoolean("debug");
+  }
+
+  @Override
+  public String getParameter(String name) {
+    return gadget.has(name) ? gadget.optString(name) : context.optString(name, null);
+  }
+
+  @Override
+  public String getContainer() {
+    return container == null ? super.getContainer() : container;
+  }
+
+  @Override
+  public boolean getDebug() {
+    return debug == null ? super.getDebug() : debug;
+  }
+  @Override
+  public boolean getIgnoreCache() {
+    return ignoreCache == null ? super.getIgnoreCache() : ignoreCache;
+  }
+
+  @Override
+  public Locale getLocale() {
+    return locale == null ? super.getLocale() : locale;
+  }
+  @Override
+  public long getModuleId() {
+    return moduleId == null ? super.getModuleId() : moduleId;
+  }
+
+  @Override
+  public RenderingContext getRenderingContext() {
+    return RenderingContext.METADATA;
+  }
+
+  @Override
+  public Uri getUrl() {
+    return url == null ? super.getUrl() : url;
+  }
+
+  @Override
+  public UserPrefs getUserPrefs() {
+    return userPrefs == null ? super.getUserPrefs() : userPrefs;
+  }
+  @Override
+  public String getView() {
+    return view == null ? super.getView() : view;
+  }
+
+  /**
+   * @param obj
+   * @return The locale, if appropriate parameters are set, or null.
+   */
+  private static Locale getLocale(JSONObject obj) {
+    String language = obj.optString("language");
+    String country = obj.optString("country");
+    if (language == null || country == null) {
+      return null;
+    }
+    return new Locale(language, country);
+  }
+
+  /**
+   * @param json
+   * @return module id from the request, or null if not present
+   * @throws JSONException
+   */
+  private static Long getModuleId(JSONObject json) throws JSONException {
+    if (json.has("moduleId")) {
+      return Long.valueOf(json.getLong("moduleId"));
+    }
+    return null;
+  }
+
+  /**
+   *
+   * @param json
+   * @return URL from the request, or null if not present
+   * @throws JSONException
+   */
+  private static Uri getUrl(JSONObject json) throws JSONException {
+    try {
+      String url = json.getString("url");
+      return Uri.parse(url);
+    } catch (IllegalArgumentException e) {
+      return null;
+    }
+  }
+
+  /**
+   * @param json
+   * @return UserPrefs, if any are set for this request.
+   * @throws JSONException
+   */
+  @SuppressWarnings("unchecked")
+  private static UserPrefs getUserPrefs(JSONObject json) throws JSONException {
+    JSONObject prefs = json.optJSONObject("prefs");
+    if (prefs == null) {
+      return null;
+    }
+    Map<String, String> p = Maps.newHashMap();
+    Iterator i = prefs.keys();
+    while (i.hasNext()) {
+      String key = (String)i.next();
+      p.put(key, prefs.getString(key));
+    }
+    return new UserPrefs(p);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsonRpcHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsonRpcHandler.java
new file mode 100644
index 0000000..7596f42
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/JsonRpcHandler.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LinkSpec;
+import org.apache.shindig.gadgets.spec.ModulePrefs;
+import org.apache.shindig.gadgets.spec.UserPref;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.uri.IframeUriManager;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Processes JSON-RPC requests by retrieving all necessary meta data in parallel and coalescing into
+ * a single output JSON construct.
+ */
+public class JsonRpcHandler {
+  protected final ExecutorService executor;
+  protected final Processor processor;
+  protected final IframeUriManager iframeUriManager;
+
+  @Inject
+  public JsonRpcHandler(ExecutorService executor, Processor processor, IframeUriManager iframeUriManager) {
+    this.executor = executor;
+    this.processor = processor;
+    this.iframeUriManager = iframeUriManager;
+  }
+
+  /**
+   * Processes a JSON request.
+   *
+   * @param request Original JSON request
+   * @return The JSON response.
+   */
+  public JSONObject process(JSONObject request) throws RpcException, JSONException {
+    List<GadgetContext> gadgets;
+
+    JSONObject requestContext = request.getJSONObject("context");
+    JSONArray requestedGadgets = request.getJSONArray("gadgets");
+
+    // Process all JSON first so that we don't wind up with hanging threads if
+    // a JSONException is thrown.
+    gadgets = Lists.newArrayListWithCapacity(requestedGadgets.length());
+
+    for (int i = 0, j = requestedGadgets.length(); i < j; ++i) {
+      GadgetContext context = new JsonRpcGadgetContext(
+          requestContext, requestedGadgets.getJSONObject(i));
+      gadgets.add(context);
+    }
+
+    // Dispatch a separate thread for each gadget that we wish to render.
+    // We could probably just submit these directly to the ExecutorService, but if it's an async
+    // service instead of a threaded one we would just block.
+    CompletionService<JSONObject> processor =  new ExecutorCompletionService<JSONObject>(executor);
+
+    for (GadgetContext context : gadgets) {
+      processor.submit(createNewJob(context));
+    }
+
+    JSONObject response = new JSONObject();
+
+    int numJobs = gadgets.size();
+    while (numJobs > 0) {
+      try {
+        JSONObject gadget = processor.take().get();
+        response.append("gadgets", gadget);
+      } catch (InterruptedException e) {
+        throw new RpcException("Processing interrupted", e);
+      } catch (ExecutionException ee) {
+        if (!(ee.getCause() instanceof RpcException)) {
+          throw new RpcException("Processing interrupted", ee);
+        }
+        RpcException e = (RpcException)ee.getCause();
+        // Just one gadget failed; mark it as such.
+        try {
+          GadgetContext context = e.getContext();
+          JSONObject errorObj = new JSONObject();
+          errorObj.put("url", context.getUrl())
+                  .put("moduleId", context.getModuleId());
+          errorObj.append("errors", e.getCause().getLocalizedMessage());
+          response.append("gadgets", errorObj);
+        } catch (JSONException je) {
+          throw new RpcException("Unable to write JSON", je);
+        }
+      } catch (JSONException e) {
+        throw new RpcException("Unable to write JSON", e);
+      } finally {
+        numJobs--;
+      }
+    }
+    return response;
+  }
+
+  protected Job createNewJob(GadgetContext context) {
+    return new Job(context);
+  }
+
+  protected class Job implements Callable<JSONObject> {
+    protected final GadgetContext context;
+
+    public Job(GadgetContext context) {
+      this.context = context;
+    }
+
+    public JSONObject call() throws RpcException {
+      try {
+        Gadget gadget = processor.process(context);
+        GadgetSpec spec = gadget.getSpec();
+        return getGadgetJson(gadget,spec);
+      } catch (Exception e) {
+        throw new RpcException(context, e);
+      }
+    }
+
+    protected JSONObject getGadgetJson(Gadget gadget, GadgetSpec spec)
+        throws JSONException {
+        JSONObject gadgetJson = new JSONObject();
+
+        ModulePrefs prefs = spec.getModulePrefs();
+
+        // TODO: modularize response fields based on requested items.
+        JSONObject views = new JSONObject();
+        for (View view : spec.getViews().values()) {
+          JSONObject jv = new JSONObject()
+               // .put("content", view.getContent())
+               .put("type", view.getType().toString())
+               .put("quirks", view.getQuirks())
+               .put("preferredHeight", view.getPreferredHeight())
+               .put("preferredWidth", view.getPreferredWidth());
+          Map<String, String> vattrs = view.getAttributes();
+          if (!vattrs.isEmpty()){
+            JSONObject ja = new JSONObject(vattrs);
+            jv.put("attributes", ja);
+          }
+          views.put(view.getName(), jv);
+        }
+
+        // Features.
+        Set<String> feats = prefs.getFeatures().keySet();
+        String[] features = feats.toArray(new String[feats.size()]);
+
+        // Feature details
+        // The following renders an object containing feature details, of the form
+        //   { <featureName>*: { "required": <boolean>, "parameters": { <paramName>*: <string> } } }
+        JSONObject featureDetailList = new JSONObject();
+        for (Feature featureSpec : prefs.getFeatures().values()) {
+          JSONObject featureDetail = new JSONObject();
+          featureDetail.put("required", featureSpec.getRequired());
+          JSONObject featureParameters = new JSONObject();
+          featureDetail.put("parameters", featureParameters);
+          Multimap<String, String> featureParams = featureSpec.getParams();
+          for (String paramName : featureParams.keySet()) {
+            featureParameters.put(paramName, featureParams.get(paramName));
+          }
+          featureDetailList.put(featureSpec.getName(), featureDetail);
+        }
+
+        // Links
+        JSONObject links = new JSONObject();
+        for (LinkSpec link : prefs.getLinks().values()) {
+          links.put(link.getRel(), link.getHref());
+        }
+
+        JSONObject userPrefs = new JSONObject();
+
+        // User pref specs
+        for (UserPref pref : spec.getUserPrefs().values()) {
+          JSONObject up = new JSONObject()
+              .put("displayName", pref.getDisplayName())
+              .put("type", pref.getDataType().toString().toLowerCase())
+              .put("default", pref.getDefaultValue())
+              .put("enumValues", pref.getEnumValues())
+              .put("orderedEnumValues", getOrderedEnums(pref));
+          userPrefs.put(pref.getName(), up);
+        }
+
+        // TODO: This should probably just copy all data from
+        // ModulePrefs.getAttributes(), but names have to be converted to
+        // camel case.
+        gadgetJson.put("iframeUrl", iframeUriManager.makeRenderingUri(gadget).toString())
+                  .put("url",context.getUrl().toString())
+                  .put("moduleId", context.getModuleId())
+                  .put("title", prefs.getTitle())
+                  .put("titleUrl", prefs.getTitleUrl().toString())
+                  .put("views", views)
+                  .put("features", features)
+                  .put("featureDetails", featureDetailList)
+                  .put("userPrefs", userPrefs)
+                  .put("links", links)
+
+                  // extended meta data
+                  .put("directoryTitle", prefs.getDirectoryTitle())
+                  .put("thumbnail", prefs.getThumbnail().toString())
+                  .put("screenshot", prefs.getScreenshot().toString())
+                  .put("author", prefs.getAuthor())
+                  .put("authorEmail", prefs.getAuthorEmail())
+                  .put("authorAffiliation", prefs.getAuthorAffiliation())
+                  .put("authorLocation", prefs.getAuthorLocation())
+                  .put("authorPhoto", prefs.getAuthorPhoto())
+                  .put("authorAboutme", prefs.getAuthorAboutme())
+                  .put("authorQuote", prefs.getAuthorQuote())
+                  .put("authorLink", prefs.getAuthorLink())
+                  .put("categories", prefs.getCategories())
+                  .put("screenshot", prefs.getScreenshot().toString())
+                  .put("height", prefs.getHeight())
+                  .put("width", prefs.getWidth())
+                  .put("showStats", prefs.getShowStats())
+                  .put("showInDirectory", prefs.getShowInDirectory())
+                  .put("singleton", prefs.getSingleton())
+                  .put("scaling", prefs.getScaling())
+                  .put("scrolling", prefs.getScrolling());
+        return gadgetJson;
+    }
+
+    private List<JSONObject> getOrderedEnums(UserPref pref) throws JSONException {
+      List<UserPref.EnumValuePair> orderedEnums = pref.getOrderedEnumValues();
+      List<JSONObject> jsonEnums = Lists.newArrayListWithCapacity(orderedEnums.size());
+      for (UserPref.EnumValuePair evp : orderedEnums) {
+        JSONObject curEnum = new JSONObject();
+        curEnum.put("value", evp.getValue());
+        curEnum.put("displayValue", evp.getDisplayValue());
+        jsonEnums.add(curEnum);
+      }
+      return jsonEnums;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java
new file mode 100644
index 0000000..057a414
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestHandler.java
@@ -0,0 +1,503 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.FeedProcessor;
+import org.apache.shindig.gadgets.FetchResponseUtils;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetException.Code;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+/**
+ * Handles gadgets.io.makeRequest requests.
+ *
+ * Unlike ProxyHandler, this may perform operations such as OAuth or signed fetch.
+ */
+@Singleton
+public class MakeRequestHandler implements ContainerConfig.ConfigObserver {
+  // Relaxed visibility for ease of integration. Try to avoid relying on these.
+  public static final String ALIAS_PARAM = "alias";
+  public static final String POST_DATA_PARAM = "postData";
+  public static final String METHOD_PARAM = "httpMethod";
+  public static final String HEADERS_PARAM = "headers";
+  public static final String CONTENT_TYPE_PARAM = "contentType";
+  public static final String NUM_ENTRIES_PARAM = "numEntries";
+  public static final String DEFAULT_NUM_ENTRIES = "3";
+  public static final String GET_SUMMARIES_PARAM = "getSummaries";
+  public static final String GET_FULL_HEADERS_PARAM = "getFullHeaders";
+  public static final String AUTHZ_PARAM = "authz";
+  public static final String MAX_POST_SIZE_KEY = "gadgets.jsonProxyUrl.maxPostSize";
+  public static final String MULTI_PART_FORM_POST = "MPFP";
+  public static final String MULTI_PART_FORM_POST_IFRAME = "iframe";
+  public static final String GADGETS_FEATURES = "gadgets.features";
+  public static final String CORE_IO = "core.io";
+  public static final String UNPARSEABLE_CRUFT = "unparseableCruft";
+  public static final int MAX_POST_SIZE_DEFAULT = 5 * 1024 * 1024; // 5 MiB
+  public static final String IFRAME_RESPONSE_PREFIX = "<html><head></head><body><textarea></textarea><script type='text/javascript'>document.getElementsByTagName('TEXTAREA')[0].value='";
+  public static final String IFRAME_RESPONSE_SUFFIX = "';</script></body></html>";
+
+  private final Map<String, String> unparseableCruftMsgs;
+  private final RequestPipeline requestPipeline;
+  private final ResponseRewriterRegistry contentRewriterRegistry;
+  private final Provider<FeedProcessor> feedProcessorProvider;
+  private final GadgetAdminStore gadgetAdminStore;
+  private final Processor processor;
+  private final LockedDomainService lockedDomainService;
+  private final Map<String, Integer> maxPostSizes;
+
+  @Inject
+  public MakeRequestHandler(
+          ContainerConfig config,
+          RequestPipeline requestPipeline,
+          @RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT) ResponseRewriterRegistry contentRewriterRegistry,
+          Provider<FeedProcessor> feedProcessorProvider, GadgetAdminStore gadgetAdminStore,
+          Processor processor, LockedDomainService lockedDomainService) {
+
+    this.requestPipeline = requestPipeline;
+    this.contentRewriterRegistry = contentRewriterRegistry;
+    this.feedProcessorProvider = feedProcessorProvider;
+    this.gadgetAdminStore = gadgetAdminStore;
+    this.processor = processor;
+    this.lockedDomainService = lockedDomainService;
+    this.maxPostSizes = Maps.newConcurrentMap();
+    this.unparseableCruftMsgs = Maps.newConcurrentMap();
+    config.addConfigObserver(this, true);
+  }
+
+  /**
+   * Executes a request, returning the response as JSON to be handled by makeRequest.
+   */
+  public void fetch(HttpServletRequest request, HttpServletResponse response)
+          throws GadgetException, IOException {
+
+    HttpRequest rcr = buildHttpRequest(request);
+    String container = rcr.getContainer();
+    final Uri gadgetUri = rcr.getGadget();
+    if (gadgetUri == null) {
+      throw new GadgetException(GadgetException.Code.MISSING_PARAMETER,
+              "Unable to find gadget in request", HttpResponse.SC_FORBIDDEN);
+    }
+
+    Gadget gadget;
+    GadgetContext context = new HttpGadgetContext(request) {
+      @Override
+      public Uri getUrl() {
+        return gadgetUri;
+      }
+      @Override
+      public boolean getIgnoreCache() {
+        return getParameter("bypassSpecCache").equals("1");
+      }
+    };
+    try {
+      gadget = processor.process(context);
+    } catch (ProcessingException e) {
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+              "Error processing gadget", e, HttpResponse.SC_BAD_REQUEST);
+    }
+
+    // Validate gadget is correct for the host.
+    // Ensures that the gadget has not hand crafted this request to represent itself as
+    // another gadget in a locked domain environment.
+    if (!lockedDomainService.isGadgetValidForHost(context.getHost(), gadget, container)) {
+      throw new GadgetException(GadgetException.Code.GADGET_HOST_MISMATCH,
+              "The gadget is incorrect for this request", HttpResponse.SC_FORBIDDEN);
+    }
+
+    if (!gadgetAdminStore.isWhitelisted(container, gadgetUri.toString())) {
+      throw new GadgetException(GadgetException.Code.NON_WHITELISTED_GADGET,
+              "The requested content is unavailable", HttpResponse.SC_FORBIDDEN);
+    }
+
+    // Serialize the response
+    HttpResponse results = requestPipeline.execute(rcr);
+
+    // Rewrite the response
+    if (contentRewriterRegistry != null) {
+      try {
+        results = contentRewriterRegistry.rewriteHttpResponse(rcr, results, gadget);
+      } catch (RewritingException e) {
+        throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e,
+                e.getHttpStatusCode());
+      }
+    }
+
+    // Serialize the response
+    String output = convertResponseToJson(rcr.getSecurityToken(), request, results);
+
+    // Find and set the refresh interval
+    setResponseHeaders(request, response, results);
+    response.setStatus(HttpServletResponse.SC_OK);
+    response.setCharacterEncoding("UTF-8");
+
+    PrintWriter out = response.getWriter();
+    if ("1".equals(getParameter(request, MULTI_PART_FORM_POST_IFRAME, null))) {
+      response.setContentType("text/html");
+      out.write(IFRAME_RESPONSE_PREFIX);
+      out.write(StringEscapeUtils.escapeEcmaScript(this.unparseableCruftMsgs.get(container)));
+      out.write(StringEscapeUtils.escapeEcmaScript(output));
+      out.write(IFRAME_RESPONSE_SUFFIX);
+    } else {
+      response.setContentType("application/json");
+      out.write(this.unparseableCruftMsgs.get(container) + output);
+    }
+  }
+
+  /**
+   * Generate a remote content request based on the parameters sent from the client.
+   *
+   * @throws GadgetException
+   */
+  protected HttpRequest buildHttpRequest(HttpServletRequest request) throws GadgetException {
+    String urlStr = getParameter(request, Param.URL.getKey(), null);
+    if (urlStr == null) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, Param.URL.getKey()
+              + " parameter is missing.", HttpResponse.SC_BAD_REQUEST);
+    }
+
+    Uri url;
+    try {
+      url = ServletUtil.validateUrl(Uri.parse(urlStr));
+    } catch (IllegalArgumentException e) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, "Invalid "
+              + Param.URL.getKey() + " parameter", HttpResponse.SC_BAD_REQUEST);
+    }
+
+    final SecurityToken token = AuthInfoUtil.getSecurityTokenFromRequest(request);
+    String container = null;
+    Uri gadgetUri = null;
+    if ("1".equals(getParameter(request, MULTI_PART_FORM_POST, null))) {
+      // This endpoint is being used by the proxied-form-post feature.
+      // Require a token.
+      if (token == null) {
+        throw new GadgetException(GadgetException.Code.INVALID_SECURITY_TOKEN);
+      }
+    }
+
+    // If we have a token, we should use it.
+    if (token != null && !token.isAnonymous()) {
+      container = token.getContainer();
+      String appurl = token.getAppUrl();
+      if (appurl != null) {
+        gadgetUri = Uri.parse(appurl);
+      }
+    } else {
+      container = getContainer(request);
+      String gadgetUrl = getParameter(request, Param.GADGET.getKey(), null);
+      if (gadgetUrl != null) {
+        gadgetUri = Uri.parse(gadgetUrl);
+      }
+    }
+
+    HttpRequest req = new HttpRequest(url).setMethod(getParameter(request, METHOD_PARAM, "GET"))
+            .setContainer(container).setGadget(gadgetUri);
+
+    if ("POST".equals(req.getMethod()) || "PUT".equals(req.getMethod())) {
+      setPostData(container, request, req);
+    }
+
+    String headerData = getParameter(request, HEADERS_PARAM, "");
+    if (headerData.length() > 0) {
+      String[] headerList = StringUtils.split(headerData, '&');
+      for (String header : headerList) {
+        String[] parts = StringUtils.splitPreserveAllTokens(header, '=');
+        if (parts.length != 2) {
+          throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+                  "Malformed header param specified:" + header, HttpResponse.SC_BAD_REQUEST);
+        }
+        String headerName = Utf8UrlCoder.decode(parts[0]);
+        if (!HttpRequestHandler.BAD_HEADERS.contains(headerName.toUpperCase())) {
+          req.addHeader(headerName, Utf8UrlCoder.decode(parts[1]));
+        }
+      }
+    }
+
+    // Set the default content type for post requests when a content type is not specified
+    if ("POST".equals(req.getMethod()) && req.getHeader("Content-Type") == null) {
+      req.addHeader("Content-Type", "application/x-www-form-urlencoded");
+    } else if ("1".equals(getParameter(request, MULTI_PART_FORM_POST, null))) {
+      // We need the entire header from the original request because it comes with a boundary value
+      // we need to reuse.
+      req.setHeader("Content-Type", request.getHeader("Content-Type"));
+    }
+
+    req.setIgnoreCache("1".equals(getParameter(request, Param.NO_CACHE.getKey(), null)));
+
+
+
+    // If the proxy request specifies a refresh param then we want to force the min TTL for
+    // the retrieved entry in the cache regardless of the headers on the content when it
+    // is fetched from the original source.
+    String refresh = getParameter(request, Param.REFRESH.getKey(), null);
+    if (refresh != null) {
+      try {
+        req.setCacheTtl(Integer.parseInt(refresh));
+      } catch (NumberFormatException ignore) {}
+    }
+    // Allow the rewriter to use an externally forced mime type. This is needed
+    // allows proper rewriting of <script src="x"/> where x is returned with
+    // a content type like text/html which unfortunately happens all too often
+    req.setRewriteMimeType(getParameter(request, Param.REWRITE_MIME_TYPE.getKey(), null));
+
+    // Figure out whether authentication is required
+    AuthType auth = AuthType.parse(getParameter(request, AUTHZ_PARAM, null));
+    req.setAuthType(auth);
+    if (auth != AuthType.NONE) {
+      req.setSecurityToken(extractAndValidateToken(request));
+      if (auth == AuthType.OAUTH2) {
+        req.setOAuth2Arguments(new OAuth2Arguments(request));
+      } else {
+        req.setOAuthArguments(new OAuthArguments(auth, request));
+      }
+    } else {
+      // if not authenticated, set the token that we received
+      req.setSecurityToken(token);
+    }
+
+    if (req.getHeader("User-Agent") == null) {
+      final String userAgent = request.getHeader("User-Agent");
+      if (userAgent != null) {
+        req.setHeader("User-Agent", userAgent);
+      }
+    }
+
+    ServletUtil.setXForwardedForHeader(request, req);
+    return req;
+  }
+
+  /**
+   * Set http request post data according to servlet request. It uses header encoding if available,
+   * and defaulted to utf8 Override the function if different behavior is needed.
+   */
+  protected void setPostData(String container, HttpServletRequest request, HttpRequest req)
+          throws GadgetException {
+    if (maxPostSizes.get(container) < request.getContentLength()) {
+      throw new GadgetException(GadgetException.Code.POST_TOO_LARGE, "Posted data too large.",
+          HttpResponse.SC_REQUEST_ENTITY_TOO_LARGE);
+    }
+
+    String encoding = request.getCharacterEncoding();
+    if (encoding == null) {
+      encoding = "UTF-8";
+    }
+    try {
+      String contentType = request.getHeader("Content-Type");
+      if (contentType != null && contentType.startsWith("multipart/form-data")) {
+        // TODO: This will read the entire posted response in server memory.
+        // Is there a way to stream this even with OAUTH flows?
+        req.setPostBody(request.getInputStream());
+      } else {
+        req.setPostBody(getParameter(request, POST_DATA_PARAM, "").getBytes(encoding.toUpperCase()));
+      }
+    } catch (UnsupportedEncodingException e) {
+      // We might consider enumerating at least a small list of encodings
+      // that we must always honor. For now, we return SC_BAD_REQUEST since
+      // the encoding parameter could theoretically be anything.
+      throw new GadgetException(Code.HTML_PARSE_ERROR, e, HttpResponse.SC_BAD_REQUEST);
+    } catch (IOException e) {
+      // Something went wrong reading the request data.
+      // TODO: perhaps also support a max post size and enforce it by throwing and catching
+      // exceptions here.
+      throw new GadgetException(Code.INTERNAL_SERVER_ERROR, e, HttpResponse.SC_BAD_REQUEST);
+    }
+  }
+
+  /**
+   * Format a response as JSON, including additional JSON inserted by chained content fetchers.
+   */
+  protected String convertResponseToJson(SecurityToken authToken, HttpServletRequest request,
+          HttpResponse results) throws GadgetException {
+    boolean getFullHeaders = Boolean.parseBoolean(getParameter(request, GET_FULL_HEADERS_PARAM,
+            "false"));
+    String originalUrl = getParameter(request, Param.URL.getKey(), null);
+    String body = results.getResponseAsString();
+    if (body.length() > 0) {
+      if ("FEED".equals(getParameter(request, CONTENT_TYPE_PARAM, null))) {
+        body = processFeed(originalUrl, request, body);
+      }
+    }
+    Map<String, Object> resp = FetchResponseUtils.getResponseAsJson(results, null, body,
+            getFullHeaders);
+
+    if (authToken != null) {
+      String updatedAuthToken = authToken.getUpdatedToken();
+      if (updatedAuthToken != null) {
+        resp.put("st", updatedAuthToken);
+      }
+    }
+
+    // Use raw param as key as URL may have to be decoded
+    return JsonSerializer.serialize(Collections.singletonMap(originalUrl, resp));
+  }
+
+  protected RequestPipeline getRequestPipeline() {
+    return requestPipeline;
+  }
+
+  /**
+   * @param request
+   * @return A valid token for the given input.
+   */
+  private SecurityToken extractAndValidateToken(HttpServletRequest request) throws GadgetException {
+    SecurityToken token = AuthInfoUtil.getSecurityTokenFromRequest(request);
+    if (token == null) {
+      // TODO: Determine appropriate external error code for this.
+      throw new GadgetException(GadgetException.Code.INVALID_SECURITY_TOKEN);
+    }
+    return token;
+  }
+
+  /**
+   * Processes a feed (RSS or Atom) using FeedProcessor.
+   */
+  private String processFeed(String url, HttpServletRequest req, String xml) throws GadgetException {
+    boolean getSummaries = Boolean.parseBoolean(getParameter(req, GET_SUMMARIES_PARAM, "false"));
+    int numEntries;
+    try {
+      numEntries = Integer.valueOf(getParameter(req, NUM_ENTRIES_PARAM, DEFAULT_NUM_ENTRIES));
+    } catch (NumberFormatException e) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+              "numEntries paramater is not a number", HttpResponse.SC_BAD_REQUEST);
+    }
+    return feedProcessorProvider.get().process(url, xml, getSummaries, numEntries).toString();
+  }
+
+  /**
+   * Extracts the container name from the request.
+   */
+  @SuppressWarnings("deprecation")
+  protected static String getContainer(HttpServletRequest request) {
+    String container = getParameter(request, Param.CONTAINER.getKey(), null);
+    if (container == null) {
+      container = getParameter(request, Param.SYND.getKey(), null);
+    }
+    return container != null ? container : ContainerConfig.DEFAULT_CONTAINER;
+  }
+
+  /**
+   * getParameter helper method, returning default value if param not present.
+   */
+  protected static String getParameter(HttpServletRequest request, String key, String defaultValue) {
+    String ret = request.getParameter(key);
+    return ret != null ? ret : defaultValue;
+  }
+
+  /**
+   * Sets cache control headers for the response.
+   */
+  @SuppressWarnings("boxing")
+  protected void setResponseHeaders(HttpServletRequest request,
+          HttpServletResponse response, HttpResponse results) throws GadgetException {
+    int refreshInterval = 0;
+    if (results.isStrictNoCache()
+            || "1".equals(getParameter(request, Param.NO_CACHE.getKey(), null))) {
+      refreshInterval = 0;
+    } else if (getParameter(request, Param.REFRESH.getKey(), null) != null) {
+      try {
+        refreshInterval = Integer.valueOf(getParameter(request, Param.REFRESH.getKey(), null));
+      } catch (NumberFormatException nfe) {
+        throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+                "refresh parameter is not a number", HttpResponse.SC_BAD_REQUEST);
+      }
+    } else {
+      refreshInterval = Math.max(60 * 60, (int) (results.getCacheTtl() / 1000L));
+    }
+    HttpUtil.setCachingHeaders(response, refreshInterval, false);
+
+    /*
+     * The proxied-form-post feature uses this endpoint to post a form
+     * element (in order to support file upload).
+     *
+     * For cross-browser support (IE) it requires that we use a hidden iframe
+     * to post the request. Setting Content-Disposition breaks that solution.
+     * In this particular case, we will always have a security token, so we
+     * shouldn't need to be as cautious here.
+     */
+    if (!"1".equals(getParameter(request, MULTI_PART_FORM_POST, null))) {
+      // Always set Content-Disposition header as XSS prevention mechanism.
+      response.setHeader("Content-Disposition", "attachment;filename=p.txt");
+    }
+
+    if (response.getContentType() == null) {
+      response.setContentType("application/octet-stream");
+    }
+  }
+
+  public void containersChanged(ContainerConfig config, Collection<String> changed,
+      Collection<String> removed) {
+    for (String container : changed) {
+      Integer maxPostSize = config.getInt(container, MAX_POST_SIZE_KEY);
+      if (maxPostSize == 0) {
+        maxPostSize = MAX_POST_SIZE_DEFAULT;
+      }
+      maxPostSizes.put(container, maxPostSize);
+      Map<String, Map<String, String>> features = config.getMap(container, GADGETS_FEATURES);
+      if (features != null) {
+        Map<String, String> coreIO = (Map<String, String>) features.get(CORE_IO);
+        if (coreIO != null) {
+          unparseableCruftMsgs.put(container, coreIO.get(UNPARSEABLE_CRUFT));
+        }
+      }
+    }
+    for (String container : removed) {
+      maxPostSizes.remove(container);
+      unparseableCruftMsgs.remove(container);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java
new file mode 100644
index 0000000..73e758b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/MakeRequestServlet.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.gadgets.GadgetException;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.inject.Inject;
+
+/**
+ * Handles calls to gadgets.io.makeRequest.
+ *
+ * GET and POST are supported so as to facilitate improved browser caching.
+ *
+ * Currently this just delegates to MakeRequestHandler, which deals with both
+ * makeRequest and open proxy calls.
+ */
+public class MakeRequestServlet extends InjectedServlet {
+
+  private static final long serialVersionUID = -8298705081500283786L;
+  private static final String classname = MakeRequestServlet.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  private transient MakeRequestHandler makeRequestHandler;
+
+  @Inject
+  public void setMakeRequestHandler(MakeRequestHandler makeRequestHandler) {
+    checkInitialized();
+    this.makeRequestHandler = makeRequestHandler;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+    try {
+      makeRequestHandler.fetch(request, response);
+    } catch (GadgetException e) {
+      if (LOG.isLoggable(Level.FINEST)) {
+        LOG.logp(Level.FINEST, classname, "doGet", MessageKeys.HTTP_ERROR_FETCHING, e);
+      }
+      int responseCode = HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+      if (e.getCode() != GadgetException.Code.INTERNAL_SERVER_ERROR) {
+        responseCode = HttpServletResponse.SC_BAD_REQUEST;
+      }
+      response.sendError(responseCode, e.getMessage() != null ? e.getMessage() : "");
+    }
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request,  HttpServletResponse response)
+      throws IOException {
+    doGet(request, response);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCache.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCache.java
new file mode 100644
index 0000000..9cfcf52
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCache.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.caja.plugin.Job;
+import com.google.caja.plugin.stages.JobCache;
+import com.google.caja.parser.ParseTreeNode;
+import com.google.caja.util.ContentType;
+import com.google.common.collect.ImmutableList;
+
+import java.util.List;
+
+import org.apache.shindig.common.cache.Cache;
+
+/**
+ * A per-module cache of intermediate cajoling results.
+ */
+final class ModuleCache extends JobCache {
+  private final Cache<ModuleCacheKey, ImmutableList<Job>> backingCache;
+
+  ModuleCache(Cache<ModuleCacheKey, ImmutableList<Job>> backingCache) {
+    this.backingCache = backingCache;
+  }
+
+  public ModuleCacheKey forJob(ContentType type, ParseTreeNode node) {
+    return new ModuleCacheKey(type, node);
+  }
+
+  public List<? extends Job> fetch(Key k) {
+    if (!(k instanceof ModuleCacheKey)) { return null; }
+    ImmutableList<Job> cachedJobs = backingCache.getElement((ModuleCacheKey) k);
+    if (cachedJobs == null) { return null; }
+    if (cachedJobs.isEmpty()) { return cachedJobs; }
+    return cloneJobs(cachedJobs);
+  }
+
+  public void store(Key k, List<? extends Job> derivatives) {
+    if (!(k instanceof ModuleCacheKey)) {
+      throw new IllegalArgumentException(k.getClass().getName());
+    }
+    ModuleCacheKey key = (ModuleCacheKey) k;
+    backingCache.addElement(key, cloneJobs(derivatives));
+  }
+
+  private static ImmutableList<Job> cloneJobs(Iterable<? extends Job> jobs) {
+    ImmutableList.Builder<Job> clones = ImmutableList.builder();
+    for (Job job : jobs) {
+      clones.add(job.clone());
+    }
+    return clones.build();
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCacheKey.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCacheKey.java
new file mode 100644
index 0000000..1781ba0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCacheKey.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.caja.parser.ParseTreeNode;
+import com.google.caja.plugin.stages.JobCache;
+import com.google.caja.util.ContentType;
+import org.w3c.dom.Node;
+import org.w3c.dom.NamedNodeMap;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A cryptographically strong hash of an abstract syntax tree.
+ */
+final class ModuleCacheKey implements JobCache.Key {
+  private final byte[] hashBytes;
+  private final int first32Bits;
+
+  ModuleCacheKey(ContentType type, ParseTreeNode node) {
+    Hasher hasher = new Hasher(type);
+    hasher.hash(node);
+    this.hashBytes = hasher.getHashBytes();
+    this.first32Bits = (hashBytes[0] & 0xff)
+        | ((hashBytes[1] & 0xff) << 8)
+        | ((hashBytes[2] & 0xff) << 16)
+        | ((hashBytes[3] & 0xff) << 24);
+  }
+
+  public ModuleCacheKeys asSingleton() {
+    return new ModuleCacheKeys(this);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return o instanceof ModuleCacheKey && Arrays.equals(hashBytes, ((ModuleCacheKey) o).hashBytes);
+  }
+
+  @Override
+  public int hashCode() {
+    return first32Bits;
+  }
+
+
+  /** A helper that walks a tree to feed tree details to a hash fn. */
+  private static final class Hasher {
+    final MessageDigest md;
+    /** Buffer that captures output to allow md to amortize hashing. */
+    final byte[] buffer = new byte[1024];
+    /** Index of last byte in buffer that needs to be updated to md. */
+    int posInBuffer;
+
+    Hasher(ContentType t) {
+      try {
+        md = MessageDigest.getInstance("MD5");
+      } catch (NoSuchAlgorithmException ex) {
+        // We can't recover if a basic algorithm like MD5 is not supported.
+        throw (AssertionError) (new AssertionError().initCause(ex));
+      }
+      md.update((byte) t.ordinal());
+    }
+
+    /** Returns the hash of anything passed to {@link #hash(ParseTreeNode)}. */
+    byte[] getHashBytes() {
+      flushBuffer();
+      return md.digest();
+    }
+
+    /** Hashes the given parse tree. */
+    void hash(ParseTreeNode node) {
+      hash(System.identityHashCode(node.getClass()));
+
+      Object value = node.getValue();
+      if (value != null) {
+        if (value instanceof String) {
+          hash((String) value);
+        } else if (value instanceof Node) {
+          hash((Node) value);
+        } else {
+          hash(value.hashCode());
+        }
+      }
+
+      List<? extends ParseTreeNode> children = node.children();
+      hash((short) children.size());
+
+      for (ParseTreeNode child : children) {
+        hash(child);
+      }
+    }
+
+    private void hash(Node node) {
+      hash(node.getNodeType());
+      switch (node.getNodeType()) {
+        case Node.ATTRIBUTE_NODE:
+        case Node.ELEMENT_NODE:
+          hash(node.getNodeName());
+          break;
+        case Node.TEXT_NODE:
+        case Node.CDATA_SECTION_NODE:
+          hash(node.getNodeValue());
+          break;
+      }
+
+      hash((short) node.getChildNodes().getLength());
+
+      if (node.getNodeType() == Node.ELEMENT_NODE) {
+        NamedNodeMap attrs = node.getAttributes();
+        int nAttrs = attrs.getLength();
+        hash((short) nAttrs);
+        for (int i = 0; i < nAttrs; ++i) {
+          hash(attrs.item(i));
+        }
+      }
+
+      for (Node child = node.getFirstChild(); child != null;
+           child = child.getNextSibling()) {
+        hash(child);
+      }
+    }
+
+    private void hash(int n) {
+      requireSpaceInBuffer(4);
+      buffer[++posInBuffer] = (byte) ((n >> 24) & 0xff);
+      buffer[++posInBuffer] = (byte) ((n >> 16) & 0xff);
+      buffer[++posInBuffer] = (byte) ((n >> 8) & 0xff);
+      buffer[++posInBuffer] = (byte) (n & 0xff);
+    }
+
+    private void hash(short n) {
+      requireSpaceInBuffer(2);
+      buffer[++posInBuffer] = (byte) ((n >> 8) & 0xff);
+      buffer[++posInBuffer] = (byte) (n & 0xff);
+    }
+
+    private void hash(String text) {
+      int n = text.length();
+      for (int i = 0; i < n; ++i) {
+        char ch = text.charAt(i);
+        if (ch < 0x0080) {
+          requireSpaceInBuffer(1);
+          buffer[++posInBuffer] = (byte) ch;
+        } else if (ch < 0x080) {
+          requireSpaceInBuffer(2);
+          buffer[++posInBuffer] = (byte) (((ch >> 6) & 0x1f) | 0xc0);
+          buffer[++posInBuffer] = (byte) ((ch & 0x3f) | 0x80);
+        } else {
+          requireSpaceInBuffer(3);
+          buffer[++posInBuffer] = (byte) (((ch >> 12) & 0x0f) | 0xe0);
+          buffer[++posInBuffer] = (byte) (((ch >> 6) & 0x3f) | 0x80);
+          buffer[++posInBuffer] = (byte) ((ch & 0x3f) | 0x80);
+        }
+      }
+    }
+
+    /** Flushes the buffer if there is not enough space. */
+    private void requireSpaceInBuffer(int space) {
+      if (posInBuffer + space >= buffer.length) {
+        flushBuffer();
+      }
+    }
+
+    /** Writes the buffer content to the message digest. */
+    private void flushBuffer() {
+      md.update(buffer, 0, posInBuffer + 1);
+      posInBuffer = -1;  // Reset the buffer.
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCacheKeys.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCacheKeys.java
new file mode 100644
index 0000000..4a3184e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleCacheKeys.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.caja.plugin.stages.JobCache;
+
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * A bundle of {@link ModuleCacheKey}s.
+ */
+final class ModuleCacheKeys implements JobCache.Keys {
+
+  final ImmutableList<ModuleCacheKey> keys;
+
+  ModuleCacheKeys(ModuleCacheKey key) {
+    this.keys = ImmutableList.of(key);
+  }
+
+  private ModuleCacheKeys(Iterable<? extends ModuleCacheKey> keys) {
+    this.keys = ImmutableList.copyOf(keys);
+  }
+
+  public ModuleCacheKeys union(JobCache.Keys other) {
+    if (!other.iterator().hasNext()) { return this; }
+    ModuleCacheKeys that = (ModuleCacheKeys) other;
+    Set<ModuleCacheKey> allKeys = Sets.newLinkedHashSet();
+    allKeys.addAll(this.keys);
+    allKeys.addAll(that.keys);
+    if (allKeys.size() == this.keys.size()) { return this; }
+    if (allKeys.size() == that.keys.size()) { return that; }
+    return new ModuleCacheKeys(allKeys);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return o instanceof ModuleCacheKeys && keys.equals(((ModuleCacheKeys) o).keys);
+  }
+
+  @Override
+  public int hashCode() {
+    return keys.hashCode();
+  }
+
+  public Iterator<JobCache.Key> iterator() {
+    return ImmutableList.<JobCache.Key>copyOf(keys).iterator();
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleIdManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleIdManager.java
new file mode 100644
index 0000000..754da65
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleIdManager.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.servlet.GadgetsHandlerApi.AuthContext;
+
+import com.google.inject.ImplementedBy;
+
+@ImplementedBy (ModuleIdManagerImpl.class)
+public interface ModuleIdManager {
+  /**
+   * Checks to make sure that the proposed moduleId for this gadget is valid.
+   * This data is not 100% trustworthy becaue we can't extract it from a
+   * token, so we validate it here, usually against the AuthContext viewerId,
+   * gadgetUrl, moduleId combination.
+   *
+   * If the moduleId is invalid the implementation may return:
+   *   null (in which case a null security token will be returned to the container)
+   *   0 (Default value for non-persisted gadgets)
+   *   A newly generated moduleId
+   *
+   * If the supplied moduleId is valid, this function is expected to return the
+   * value of the moduleId param.
+   *
+   * @param gadgetUri The location of the gadget xml to validate the token for
+   * @param containerAuthContext The Auth context.  Basically, the container security token.
+   * @param moduleId The moduleId sent by the container page.
+   * @return moduleId.
+   */
+  public Long validate(Uri gadgetUri, AuthContext containerAuthContext, Long moduleId);
+
+  /**
+   * Generate and persist a new moduleId for the given gadgetUri and container auth context.
+   *
+   * @param gadgetUri The location of the gadget xml to generate the token for
+   * @param containerAuthContext The Auth context.  Basically, the container security token.
+   * @return moduleId.
+   */
+  public Long generate(Uri gadgetUri, AuthContext containerAuthContext);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleIdManagerImpl.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleIdManagerImpl.java
new file mode 100644
index 0000000..15839d6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ModuleIdManagerImpl.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.servlet.GadgetsHandlerApi.AuthContext;
+
+/**
+ * Override this class to provide meaningful moduleId validation and generation for gadgets.
+ */
+public class ModuleIdManagerImpl implements ModuleIdManager {
+  public Long validate(Uri gadgetUri, AuthContext containerAuthContext, Long moduleId) {
+    return 0L;
+  }
+
+  public Long generate(Uri gadgetUri, AuthContext containerAuthContext) {
+    return 0L;
+  }
+}
+
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/OAuth2CallbackServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/OAuth2CallbackServlet.java
new file mode 100644
index 0000000..1786d9c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/OAuth2CallbackServlet.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2CallbackState;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2FetcherConfig;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Module;
+import org.apache.shindig.gadgets.oauth2.OAuth2Store;
+import org.apache.shindig.gadgets.oauth2.handler.AuthorizationEndpointResponseHandler;
+import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class OAuth2CallbackServlet extends InjectedServlet {
+  private static final long serialVersionUID = -8829844832872635091L;
+
+  private static final String LOG_CLASS = OAuth2CallbackServlet.class.getName();
+  private static final Logger LOGGER = Logger.getLogger(OAuth2CallbackServlet.LOG_CLASS);
+
+  private transient List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers;
+  private transient OAuth2Store store;
+  private transient Provider<OAuth2Message> oauth2MessageProvider;
+  private transient BlobCrypter stateCrypter;
+  private transient boolean sendTraceToClient = false;
+
+  // This bit of magic passes the entire callback URL into the opening gadget
+  // for later use.
+  // gadgets.io.makeRequest (or osapi.oauth) will then pick up the callback URL
+  // to complete the
+  // oauth dance.
+  private static final String RESP_BODY = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+          + "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+          + "<html>\n"
+          + "<head>\n"
+          + "<title>Close this window</title>\n"
+          + "</head>\n"
+          + "<body>\n"
+          + "<script type='text/javascript'>\n"
+          + "try {\n"
+          + "  window.opener.gadgets.io.oauthReceivedCallbackUrl_ = document.location.href;\n"
+          + "} catch (e) {\n"
+          + "}\n"
+          + "window.close();\n"
+          + "</script>\n"
+          + "Close this window.\n" + "</body>\n" + "</html>\n";
+
+  private static final String RESP_ERROR_BODY = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" "
+          + "\"http://www.w3.org/TR/html4/loose.dtd\">\n"
+          + "<html>\n"
+          + "<head>\n"
+          + "<title>OAuth2 Error</title>\n"
+          + "</head>\n"
+          + "<body>\n"
+          + "<p>error = %s</p>"
+          + "<p>error description = %s</p>"
+          + "<p>error uri = %s</p>"
+          + "Close this window.\n"
+          + "</body>\n" + "</html>\n";
+
+  @Override
+  protected void doGet(final HttpServletRequest request, final HttpServletResponse resp)
+          throws IOException {
+
+    OAuth2Accessor accessor = null;
+    try {
+      final OAuth2Message msg = this.oauth2MessageProvider.get();
+      msg.parseRequest(request);
+      final OAuth2Error error = msg.getError();
+      final String encRequestStateKey = msg.getState();
+      if (encRequestStateKey == null) {
+        if (error != null) {
+          OAuth2CallbackServlet.sendError(error, "encRequestStateKey is null", msg.getErrorUri(),
+                  msg.getErrorDescription(), null, resp, null, this.sendTraceToClient);
+        } else {
+          OAuth2CallbackServlet.sendError(OAuth2Error.CALLBACK_PROBLEM,
+                  "OAuth2CallbackServlet requestStateKey is null.", "", "", null, resp, null,
+                  this.sendTraceToClient);
+        }
+        return;
+      }
+
+      final OAuth2CallbackState state = new OAuth2CallbackState(this.stateCrypter,
+              encRequestStateKey);
+
+      accessor = this.store.getOAuth2Accessor(state);
+
+      if (error != null) {
+        OAuth2CallbackServlet.sendError(error, "error parsing request", msg.getErrorDescription(),
+                msg.getErrorUri(), accessor, resp, null, this.sendTraceToClient);
+        return;
+      }
+
+      if (accessor == null || !accessor.isValid() || accessor.isErrorResponse()) {
+        String message;
+        if (accessor != null) {
+          message = accessor.isValid() ? "OAuth2CallbackServlet accessor isErrorResponse "
+                  : "OAuth2CallbackServlet accessor is invalid ";
+          message = message + accessor;
+        } else {
+          message = "OAuth2CallbackServlet accessor is null";
+        }
+
+        OAuth2CallbackServlet.sendError(OAuth2Error.CALLBACK_PROBLEM, message,
+                accessor.getErrorContextMessage(), accessor.getErrorUri(), accessor, resp,
+                accessor.getErrorException(), this.sendTraceToClient);
+
+        return;
+      }
+
+      if (!accessor.isRedirecting()) {
+        // Somehow our accessor got lost. We should not proceed.
+        OAuth2CallbackServlet.sendError(OAuth2Error.CALLBACK_PROBLEM,
+                "OAuth2CallbackServlet accessor is not valid, isn't redirecting.", "", "",
+                accessor, resp, null, this.sendTraceToClient);
+        return;
+      }
+
+      boolean foundHandler = false;
+      for (final AuthorizationEndpointResponseHandler authorizationEndpointResponseHandler : this.authorizationEndpointResponseHandlers) {
+        if (authorizationEndpointResponseHandler.handlesRequest(accessor, request)) {
+          final OAuth2HandlerError handlerError = authorizationEndpointResponseHandler
+                  .handleRequest(accessor, request);
+          if (handlerError != null) {
+            OAuth2CallbackServlet.sendError(handlerError.getError(),
+                    handlerError.getContextMessage(), handlerError.getDescription(),
+                    handlerError.getUri(), accessor, resp, handlerError.getCause(),
+                    this.sendTraceToClient);
+            return;
+          }
+          foundHandler = true;
+          break;
+        }
+      }
+
+      if (!foundHandler) {
+        OAuth2CallbackServlet.sendError(OAuth2Error.NO_RESPONSE_HANDLER,
+                "OAuth2Callback servlet couldn't find a AuthorizationEndpointResponseHandler", "",
+                "", accessor, resp, null, this.sendTraceToClient);
+        return;
+      }
+
+      HttpUtil.setNoCache(resp);
+      resp.setContentType("text/html; charset=UTF-8");
+      resp.getWriter().write(OAuth2CallbackServlet.RESP_BODY);
+    } catch (final Exception e) {
+      OAuth2CallbackServlet.sendError(OAuth2Error.CALLBACK_PROBLEM,
+              "Exception occurred processing redirect.", "", "", accessor, resp, e,
+              this.sendTraceToClient);
+      if (IOException.class.isInstance(e)) {
+        throw (IOException) e;
+      }
+    } finally {
+      if (accessor != null) {
+        if (!accessor.isErrorResponse()) {
+          accessor.invalidate();
+          this.store.removeOAuth2Accessor(accessor);
+        } else {
+          this.store.storeOAuth2Accessor(accessor);
+        }
+      }
+    }
+  }
+
+  private static void sendError(final OAuth2Error error, final String contextMessage,
+          final String description, final String uri, final OAuth2Accessor accessor,
+          final HttpServletResponse resp, final Throwable t, final boolean sendTraceToClient)
+          throws IOException {
+
+    OAuth2CallbackServlet.LOGGER.warning(OAuth2CallbackServlet.LOG_CLASS + " , callback error "
+            + error + " -  " + contextMessage + " , " + description + " - " + uri);
+    if (t != null) {
+      if (OAuth2CallbackServlet.LOGGER.isLoggable(Level.FINE)) {
+        OAuth2CallbackServlet.LOGGER.log(Level.FINE, " callback exception ", t);
+      }
+    }
+
+    HttpUtil.setNoCache(resp);
+    resp.setContentType("text/html; charset=UTF-8");
+
+    if (accessor != null) {
+      accessor.setErrorResponse(t, error, contextMessage + " , " + description, uri);
+    } else {
+      // We don't have an accessor to report the error back to the client in the
+      // normal manner.
+      // Anything is better than nothing, hack something together....
+      final String errorResponse;
+      if (sendTraceToClient) {
+        errorResponse = String.format(OAuth2CallbackServlet.RESP_ERROR_BODY, error.getErrorCode(),
+                error.getErrorDescription(description), uri);
+      } else {
+        errorResponse = String.format(OAuth2CallbackServlet.RESP_ERROR_BODY, error.getErrorCode(),
+                "", "");
+      }
+      resp.getWriter().write(errorResponse);
+      return;
+    }
+
+    resp.getWriter().write(OAuth2CallbackServlet.RESP_BODY);
+  }
+
+  @Inject
+  public void setAuthorizationResponseHandlers(
+          final List<AuthorizationEndpointResponseHandler> authorizationEndpointResponseHandlers) {
+    this.authorizationEndpointResponseHandlers = authorizationEndpointResponseHandlers;
+  }
+
+  @Inject
+  public void setOAuth2Store(@Named(OAuth2Module.SEND_TRACE_TO_CLIENT)
+  final boolean sendTraceToClient) {
+    this.sendTraceToClient = sendTraceToClient;
+  }
+
+  @Inject
+  public void setOAuth2Store(final OAuth2Store store) {
+    this.store = store;
+  }
+
+  @Inject
+  public void setOauth2MessageProvider(final Provider<OAuth2Message> oauth2MessageProvider) {
+    this.oauth2MessageProvider = oauth2MessageProvider;
+  }
+
+  @Inject
+  public void setStateCrypter(@Named(OAuth2FetcherConfig.OAUTH2_STATE_CRYPTER)
+  final BlobCrypter stateCrypter) {
+    this.stateCrypter = stateCrypter;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/OAuthCallbackServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/OAuthCallbackServlet.java
new file mode 100644
index 0000000..abcc3bb
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/OAuthCallbackServlet.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.oauth.OAuthCallbackState;
+import org.apache.shindig.gadgets.oauth.OAuthFetcherConfig;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Servlet to act as our OAuth callback URL.  When gadget authors register a consumer key with an
+ * OAuth service provider, they can provide a URL pointing to this servlet as their callback URL.
+ *
+ * Protocol flow:
+ * - gadget discovers it needs approval to access data at OAuth SP.
+ * - gadget opens popup window to approval URL, passing URL to this servlet as the oauth_callback
+ *   parameter on the approval URL.
+ * - user grants approval at service provider
+ * - service provider redirects to this servlet
+ * - this servlet closes the window
+ * - gadget discovers the window has closed and automatically fetches the user's data.
+ */
+public class OAuthCallbackServlet extends InjectedServlet {
+
+  private static final long serialVersionUID = 7126255229334669172L;
+
+  public static final String CALLBACK_STATE_PARAM = "cs";
+  public static final String REAL_DOMAIN_PARAM = "d";
+  private static final int ONE_HOUR_IN_SECONDS = 3600;
+
+  // This bit of magic passes the entire callback URL into the opening gadget for later use.
+  // gadgets.io.makeRequest (or osapi.oauth) will then pick up the callback URL to complete the
+  // oauth dance.
+  private static final String RESP_BODY =
+    "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" " +
+    "\"http://www.w3.org/TR/html4/loose.dtd\">\n" +
+    "<html>\n" +
+    "<head>\n" +
+    "<title>Close this window</title>\n" +
+    "</head>\n" +
+    "<body>\n" +
+    "<script type='text/javascript'>\n" +
+    "try {\n" +
+    "  window.opener.gadgets.io.oauthReceivedCallbackUrl_ = document.location.href;\n" +
+    "} catch (e) {\n" +
+    "}\n" +
+    "window.close();\n" +
+    "</script>\n" +
+    "Close this window.\n" +
+    "</body>\n" +
+    "</html>\n";
+
+  private transient BlobCrypter stateCrypter;
+
+  @Inject
+  public void setStateCrypter(@Named(OAuthFetcherConfig.OAUTH_STATE_CRYPTER) BlobCrypter stateCrypter) {
+    checkInitialized();
+    this.stateCrypter = stateCrypter;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+    OAuthCallbackState callbackState = new OAuthCallbackState(stateCrypter,
+        req.getParameter(CALLBACK_STATE_PARAM));
+    if (callbackState.getRealCallbackUrl() != null) {
+      // Copy the query parameters from this URL over to the real URL.
+      UriBuilder realUri = UriBuilder.parse(callbackState.getRealCallbackUrl());
+      Map<String, List<String>> params = UriBuilder.splitParameters(req.getQueryString());
+      for (Map.Entry<String, List<String>> entry : params.entrySet()) {
+        realUri.putQueryParameter(entry.getKey(), entry.getValue());
+      }
+      realUri.removeQueryParameter(CALLBACK_STATE_PARAM);
+      HttpUtil.setCachingHeaders(resp, ONE_HOUR_IN_SECONDS, true);
+      resp.sendRedirect(realUri.toString());
+      return;
+    }
+    HttpUtil.setCachingHeaders(resp, ONE_HOUR_IN_SECONDS, true);
+    resp.setContentType("text/html; charset=UTF-8");
+    resp.getWriter().write(RESP_BODY);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
new file mode 100644
index 0000000..30359b6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyHandler.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.Nullable;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.UriUtils;
+import org.apache.shindig.gadgets.uri.UriUtils.DisallowedHeaders;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+/**
+ * Handles open proxy requests.
+ */
+@Singleton
+public class ProxyHandler {
+  private final RequestPipeline requestPipeline;
+  private final ResponseRewriterRegistry contentRewriterRegistry;
+  protected final boolean remapInternalServerError;
+  private final GadgetAdminStore gadgetAdminStore;
+  private final Integer longLivedRefreshSec;
+  private static final String POST = "POST";
+
+  @Inject
+  public ProxyHandler(RequestPipeline requestPipeline,
+      @RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT) ResponseRewriterRegistry contentRewriterRegistry,
+      @Named("shindig.proxy.remapInternalServerError") Boolean remapInternalServerError,
+      GadgetAdminStore gadgetAdminStore,
+      @Named("org.apache.shindig.gadgets.servlet.longLivedRefreshSec") int longLivedRefreshSec) {
+    this.requestPipeline = requestPipeline;
+    this.contentRewriterRegistry = contentRewriterRegistry;
+    this.remapInternalServerError = remapInternalServerError;
+    this.gadgetAdminStore = gadgetAdminStore;
+    this.longLivedRefreshSec = longLivedRefreshSec;
+  }
+
+  /**
+   * Generate a remote content request based on the parameters sent from the client.
+   * @param uriCtx
+   * @param tgt
+   * @param postBody
+   */
+  private HttpRequest buildHttpRequest(ProxyUriManager.ProxyUri uriCtx, Uri tgt, @Nullable String postBody)
+      throws GadgetException, IOException {
+    ServletUtil.validateUrl(tgt);
+    HttpRequest req = uriCtx.makeHttpRequest(tgt);
+    req.setRewriteMimeType(uriCtx.getRewriteMimeType());
+    if (postBody != null) {
+      req.setMethod(POST);
+      // convert String into InputStream
+      req.setPostBody(new ByteArrayInputStream(postBody.getBytes()));
+    }
+    if (req.getHeader("User-Agent") == null) {
+      final String userAgent = uriCtx.getUserAgent();
+      if (userAgent != null) {
+        req.setHeader("User-Agent", userAgent);
+      }
+    }
+    return req;
+  }
+
+  public HttpResponse fetch(ProxyUriManager.ProxyUri proxyUri) throws IOException, GadgetException {
+    return fetch(proxyUri, null);
+  }
+
+  public HttpResponse fetch(ProxyUriManager.ProxyUri proxyUri, @Nullable String postBody)
+      throws IOException, GadgetException {
+    HttpRequest rcr = buildHttpRequest(proxyUri, proxyUri.getResource(), postBody);
+    if (rcr == null) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+        "No url parameter in request", HttpResponse.SC_BAD_REQUEST);
+    }
+
+    if (rcr.getGadget() != null &&
+            !gadgetAdminStore.isWhitelisted(rcr.getContainer(), rcr.getGadget().toString())) {
+      throw new GadgetException(GadgetException.Code.NON_WHITELISTED_GADGET,
+        "The requested content is unavailable", HttpResponse.SC_FORBIDDEN);
+    }
+
+    HttpResponse results = requestPipeline.execute(rcr);
+
+    if (results.isError()) {
+      // Error: try the fallback. Particularly useful for proxied images.
+      Uri fallbackUri = proxyUri.getFallbackUri();
+      if (fallbackUri != null) {
+        HttpRequest fallbackRcr = buildHttpRequest(proxyUri, fallbackUri, null);
+        results = requestPipeline.execute(fallbackRcr);
+      }
+    }
+
+    if (contentRewriterRegistry != null) {
+      try {
+        results = contentRewriterRegistry.rewriteHttpResponse(rcr, results, null);
+      } catch (RewritingException e) {
+        // Throw exception if the RETURN_ORIGINAL_CONTENT_ON_ERROR param is not
+        // set to "true" or the error is irrecoverable from.
+        if (!proxyUri.shouldReturnOrigOnErr() || !isRecoverable(results)) {
+          throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e,
+                  e.getHttpStatusCode());
+        }
+      }
+    }
+
+    HttpResponseBuilder response = new HttpResponseBuilder(results);
+    response.clearAllHeaders();
+
+    try {
+      ServletUtil.setCachingHeaders(response, proxyUri.translateStatusRefresh(longLivedRefreshSec,
+        (int) (results.getCacheTtl() / 1000)), false);
+    } catch (GadgetException gex) {
+      return ServletUtil.errorResponse(gex);
+    }
+
+    UriUtils.copyResponseHeadersAndStatusCode(results, response, remapInternalServerError, true,
+      DisallowedHeaders.CACHING_DIRECTIVES, // Proxy sets its own caching headers.
+      DisallowedHeaders.CLIENT_STATE_DIRECTIVES, // Overridden or irrelevant to proxy.
+      DisallowedHeaders.OUTPUT_TRANSFER_DIRECTIVES);
+
+    // Set Content-Type and Content-Disposition. Do this after copy results headers,
+    // in order to prevent those from overwriting the correct values.
+    setResponseContentHeaders(response, results);
+
+    UriUtils.maybeRewriteContentType(rcr, response);
+
+    // TODO: replace this with streaming APIs when ready
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    IOUtils.copy(results.getResponse(), baos);
+    response.setResponse(baos.toByteArray());
+    return response.create();
+  }
+
+  protected void setResponseContentHeaders(HttpResponseBuilder response, HttpResponse results) {
+    // We're skipping the content disposition header for flash due to an issue with Flash player 10
+    // This does make some sites a higher value phishing target, but this can be mitigated by
+    // additional referer checks.
+    if (!isFlash(response.getHeader("Content-Type"), results.getHeader("Content-Type"))) {
+      String contentDispositionValue = results.getHeader("Content-Disposition");
+      if (StringUtils.isBlank(contentDispositionValue)
+              || contentDispositionValue.indexOf("attachment;") == -1
+              || contentDispositionValue.indexOf("filename") == -1) {
+        response.setHeader("Content-Disposition", "attachment;filename=p.txt");
+      } else {
+        response.setHeader("Content-Disposition", contentDispositionValue);
+      }
+    }
+    if (results.getHeader("Content-Type") == null) {
+      response.setHeader("Content-Type", "application/octet-stream");
+    }
+  }
+
+  private static final String FLASH_CONTENT_TYPE = "application/x-shockwave-flash";
+
+  /**
+   * Test for presence of flash
+   *
+   * @param responseContentType
+   *          the Content-Type header from the HttpResponseBuilder
+   * @param resultsContentType
+   *          the Content-Type header from the HttpResponse
+   * @return true if either content type matches that of Flash
+   */
+  private boolean isFlash(String responseContentType, String resultsContentType) {
+    return StringUtils.startsWithIgnoreCase(responseContentType, FLASH_CONTENT_TYPE)
+            || StringUtils.startsWithIgnoreCase(resultsContentType, FLASH_CONTENT_TYPE);
+  }
+
+  /**
+   * Returns true in case the error encountered while rewriting the content is recoverable. The
+   * rationale behind it is that errors should be thrown only in case of serious grave errors
+   * (defined to be un recoverable). It should always be preferred to handle errors and return the
+   * original content at least.
+   *
+   * @param results
+   *          The result of rewriting.
+   * @return True if the error is recoverable, false otherwise.
+   */
+  public boolean isRecoverable(HttpResponse results) {
+    return !(Strings.isNullOrEmpty(results.getResponseAsString()) && results.getHeaders() == null);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyServlet.java
new file mode 100644
index 0000000..ba37ca0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyServlet.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Handles open proxy requests (used in rewriting and for URLs returned by gadgets.io.getProxyUrl).
+ */
+public class ProxyServlet extends InjectedServlet {
+  private static final long serialVersionUID = 9085050443492307723L;
+
+  // class name for logging purpose
+  private static final String classname = ProxyServlet.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  private transient ProxyUriManager proxyUriManager;
+  private transient LockedDomainService lockedDomainService;
+  private transient ProxyHandler proxyHandler;
+
+  @Inject
+  public void setProxyHandler(ProxyHandler proxyHandler) {
+    checkInitialized();
+    this.proxyHandler = proxyHandler;
+  }
+
+  @Inject
+  public void setProxyUriManager(ProxyUriManager proxyUriManager) {
+    checkInitialized();
+    this.proxyUriManager = proxyUriManager;
+  }
+
+  @Inject
+  public void setLockedDomainService(LockedDomainService lockedDomainService) {
+    checkInitialized();
+    this.lockedDomainService = lockedDomainService;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse servletResponse)
+      throws IOException {
+    processRequest(request, servletResponse);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse servletResponse)
+      throws IOException {
+    processRequest(request, servletResponse);
+  }
+
+  private void processRequest(HttpServletRequest request, HttpServletResponse servletResponse)
+      throws IOException {
+    if (request.getHeader("If-Modified-Since") != null) {
+      servletResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+      return;
+    }
+
+    Uri reqUri = new UriBuilder(request).toUri();
+
+    HttpResponse response;
+    try {
+      // Parse request uri:
+      ProxyUriManager.ProxyUri proxyUri = proxyUriManager.process(reqUri);
+      SecurityToken st = AuthInfoUtil.getSecurityTokenFromRequest(request);
+      proxyUri.setSecurityToken(st);
+      proxyUri.setUserAgent(request.getHeader("User-Agent"));
+      // get gadget from security token
+      if(proxyUri.getGadget() == null) {
+        if(st != null && !st.isAnonymous()) {
+          proxyUri.setGadget(st.getAppUrl());
+        }
+      }
+      AuthType authType = proxyUri.getAuthType();
+      if(AuthType.OAUTH.equals(authType)) {
+        proxyUri.setOAuthArguments(new OAuthArguments(AuthType.OAUTH, request));
+      } else if(AuthType.OAUTH2.equals(authType)) {
+        proxyUri.setOAuth2Arguments(new OAuth2Arguments(request));
+      }
+
+      // TODO: Consider removing due to redundant logic.
+      String host = request.getHeader("Host");
+      if (!lockedDomainService.isSafeForOpenProxy(host)) {
+        // Force embedded images and the like to their own domain to avoid XSS
+        // in gadget domains.
+        Uri resourceUri = proxyUri.getResource();
+        String msg = "Embed request for url " + (resourceUri != null ? resourceUri.toString() : "n/a")
+            + " made to wrong domain " + host;
+        if (LOG.isLoggable(Level.INFO)) {
+          LOG.logp(Level.INFO, classname, "processRequest", MessageKeys.EMBEDED_IMG_WRONG_DOMAIN,
+            new Object[] { resourceUri != null ? resourceUri.toString() : "n/a", host });
+        }
+        throw new GadgetException(GadgetException.Code.INVALID_PARAMETER, msg,
+          HttpResponse.SC_BAD_REQUEST);
+      }
+      if ("POST".equalsIgnoreCase(request.getMethod())) {
+        StringBuffer buffer = getPOSTContent(request);
+        response = proxyHandler.fetch(proxyUri, buffer.toString());
+      } else {
+        response = proxyHandler.fetch(proxyUri);
+      }
+    } catch (GadgetException e) {
+      response = ServletUtil.errorResponse(new GadgetException(e.getCode(), e.getMessage(),
+          HttpServletResponse.SC_BAD_REQUEST));
+    }
+
+    ServletUtil.copyToServletResponseAndOverrideCacheHeaders(response, servletResponse);
+  }
+
+  private StringBuffer getPOSTContent(HttpServletRequest request) throws IOException {
+    // Convert POST content from request to a string
+    StringBuffer buffer = new StringBuffer();
+    BufferedReader reader = null;
+    try {
+      reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
+      int letter = 0;
+      while ((letter = reader.read()) != -1) {
+        buffer.append((char) letter);
+      }
+      reader.close();
+    } catch (IOException e) {
+      LOG.logp(Level.WARNING, classname, "getPOSTContent", "Caught exception while reading POST body:"
+          + e.getMessage());
+    } finally {
+      IOUtils.closeQuietly(reader);
+    }
+    return buffer;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcException.java
new file mode 100644
index 0000000..350a6e1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcException.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.shindig.gadgets.GadgetContext;
+
+/**
+ * Contains RPC-specific exceptions.
+ */
+public class RpcException extends Exception {
+  private final GadgetContext context;
+
+  public GadgetContext getContext() {
+    return context;
+  }
+
+  public RpcException(String message) {
+    super(message);
+    context = null;
+  }
+
+  public RpcException(String message, Throwable cause) {
+    super(message, cause);
+    context = null;
+  }
+
+  public RpcException(GadgetContext context, Throwable cause) {
+    super(cause);
+    this.context = context;
+  }
+
+  public RpcException(GadgetContext context, String message) {
+    super(message);
+    this.context = context;
+  }
+
+  public RpcException(GadgetContext context, String message, Throwable cause) {
+    super(message, cause);
+    this.context = context;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java
new file mode 100644
index 0000000..17e39df
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcServlet.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.base.Preconditions;
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Handles RPC metadata requests.
+ */
+public class RpcServlet extends InjectedServlet {
+
+  private static final long serialVersionUID = 1382573217773582182L;
+
+  static final String GET_REQUEST_REQ_PARAM = "req";
+  static final String GET_REQUEST_CALLBACK_PARAM = "callback";
+
+  private static final Logger LOG = Logger.getLogger("org.apache.shindig.gadgets.servlet.RpcServlet");
+
+  private transient JsonRpcHandler jsonHandler;
+  private Boolean isJSONPAllowed;
+
+  @Inject
+  public void setJsonRpcHandler(JsonRpcHandler jsonHandler) {
+    checkInitialized();
+    this.jsonHandler = jsonHandler;
+  }
+
+  @Inject
+  public void setJSONPAllowed(
+      @Named("shindig.allowJSONP") Boolean isJSONPAllowed) {
+    this.isJSONPAllowed = isJSONPAllowed;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+    String reqValue;
+    String callbackValue;
+
+    try {
+      if (this.isJSONPAllowed) {
+        HttpUtil.isJSONP(request);
+        callbackValue = validateParameterValue(request, GET_REQUEST_CALLBACK_PARAM);
+      } else {
+        callbackValue = validateParameterValueNull(request, GET_REQUEST_CALLBACK_PARAM);
+      }
+      reqValue = validateParameterValue(request, GET_REQUEST_REQ_PARAM);
+    } catch (IllegalArgumentException e) {
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      LOG.log(Level.INFO, e.getMessage(), e);
+      return;
+    }
+
+    Result result = process(request, response, reqValue);
+    if (result.isSuccess()) {
+      if (callbackValue != null) {
+        response.getWriter().write(callbackValue + '(' + result.getOutput() + ')');
+      } else {
+        response.getWriter().write(result.getOutput());
+      }
+    } else {
+      response.getWriter().write(result.getOutput());
+    }
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse response)
+      throws IOException {
+    try{
+      InputStreamReader is = new InputStreamReader(request.getInputStream(),
+          getRequestCharacterEncoding(request));
+      String body = IOUtils.toString(is);
+      Result result = process(request, response, body);
+      response.getWriter().write(result.getOutput());
+    } catch (UnsupportedEncodingException e) {
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      LOG.log(Level.INFO, e.getMessage(), e);
+      response.getWriter().write("Unsupported input character set");
+    }
+  }
+
+  private String validateParameterValue(HttpServletRequest request, String parameter)
+      throws IllegalArgumentException {
+    String result = request.getParameter(parameter);
+    Preconditions.checkArgument(result != null, "No parameter '%s' specified", parameter);
+    return result;
+  }
+
+  private String validateParameterValueNull(HttpServletRequest request, String parameter)
+      throws IllegalArgumentException {
+    String result = request.getParameter(parameter);
+    Preconditions.checkArgument(result == null, "Wrong parameter '%s' found", parameter);
+    return result;
+  }
+
+  private Result process(HttpServletRequest request, HttpServletResponse response, String body) {
+    try {
+      JSONObject req = new JSONObject(body);
+      JSONObject resp = jsonHandler.process(req);
+      response.setStatus(HttpServletResponse.SC_OK);
+      response.setContentType("application/json; charset=utf-8");
+      response.setHeader("Content-Disposition", "attachment;filename=rpc.txt");
+      return new Result(resp.toString(), true);
+    } catch (JSONException e) {
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      return new Result("Malformed JSON request.", false);
+    } catch (RpcException e) {
+      response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      LOG.log(Level.INFO, e.getMessage(), e);
+      return new Result(e.getMessage(), false);
+    }
+  }
+
+  private String getRequestCharacterEncoding(HttpServletRequest request) {
+    String encoding = request.getCharacterEncoding();
+    if (encoding == null) {
+      encoding = "UTF-8";
+    }
+    return encoding;
+  }
+
+  private static class Result {
+    private final String output;
+    private final boolean success;
+
+    public Result(String output, boolean success) {
+      this.output = output;
+      this.success = success;
+    }
+
+    public String getOutput() {
+      return output;
+    }
+
+    public boolean isSuccess() {
+      return success;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcSwfServlet.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcSwfServlet.java
new file mode 100644
index 0000000..bd89d86
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/RpcSwfServlet.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.gadgets.uri.UriCommon;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Trivial servlet that does precisely one thing: serves the SWF needed for gadgets.rpc's
+ * Flash-based transport.
+ */
+public class RpcSwfServlet extends HttpServlet {
+  private static final String SWF_RESOURCE_NAME = "files/xpc.swf";
+  private static final int ONE_YEAR_IN_SEC = 365 * 24 * 60 * 60;
+  private static final int DEFAULT_SWF_TTL = 24 * 60 * 60;
+
+  private final byte[] swfBytes;
+  private final String hash;
+  private int defaultSwfTtl = DEFAULT_SWF_TTL;
+
+  public RpcSwfServlet() {
+    this(SWF_RESOURCE_NAME);
+  }
+
+  public RpcSwfServlet(String swfResource) {
+    try {
+      InputStream is = ResourceLoader.openResource(swfResource);
+      if (is == null) {
+        throw new RuntimeException("Failed to locate Flash SWF");
+      }
+      this.swfBytes = IOUtils.toByteArray(is);
+      this.hash = HashUtil.checksum(swfBytes);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Inject(optional = true)
+  public void setDefaultRpcSwfTtl(@Named("shindig.rpc.swf.defaultTtl") Integer defaultTtl) {
+    defaultSwfTtl = defaultTtl;
+  }
+
+  public String getSwfHash() {
+    return hash;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+    resp.setStatus(HttpServletResponse.SC_OK);
+
+    // Similar versioning method to other APIs, implemented more compactly.
+    String v = req.getParameter(UriCommon.Param.VERSION.getKey());
+    if (v != null && v.equals(hash)) {
+      HttpUtil.setCachingHeaders(resp, ONE_YEAR_IN_SEC, true);
+    } else {
+      HttpUtil.setCachingHeaders(resp, defaultSwfTtl, true);
+    }
+
+    resp.setHeader("Content-Type", "application/x-shockwave-flash");
+
+    resp.getOutputStream().write(swfBytes);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ServletUtil.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ServletUtil.java
new file mode 100644
index 0000000..e87d935
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ServletUtil.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.base.Strings;
+import org.apache.commons.codec.binary.Base64InputStream;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Enumeration;
+import java.util.Map;
+
+/**
+ * Utility routines for dealing with servlets.
+ *
+ * @since 2.0.0
+ */
+public final class ServletUtil {
+  public static final String REMOTE_ADDR_KEY = "RemoteAddress";
+  public static final String DATA_URI_KEY = "dataUri";
+
+  private ServletUtil() {}
+
+  /**
+   * Returns an HttpRequest object encapsulating the servlet request.
+   * NOTE: Request parameters are not explicitly taken care of, instead we copy
+   * the InputStream and query parameters separately.
+   *
+   * @param servletReq The http servlet request.
+   * @return An HttpRequest object with all the information provided by the
+   *   servlet request.
+   * @throws IOException In case of errors.
+   */
+  public static HttpRequest fromHttpServletRequest(HttpServletRequest servletReq) throws IOException {
+    HttpRequest req = new HttpRequest(new UriBuilder(servletReq).toUri());
+
+    Enumeration<?> headerNames = servletReq.getHeaderNames();
+    while (headerNames.hasMoreElements()) {
+      Object obj = headerNames.nextElement();
+      if (obj instanceof String) {
+        String headerName = (String) obj;
+
+        Enumeration<?> headerValues = servletReq.getHeaders(headerName);
+        while (headerValues.hasMoreElements()) {
+          obj = headerValues.nextElement();
+          if (obj instanceof String) {
+            req.addHeader(headerName, (String) obj);
+          }
+        }
+      }
+    }
+
+    req.setMethod(servletReq.getMethod());
+    if ("POST".equalsIgnoreCase(req.getMethod())) {
+      req.setPostBody(servletReq.getInputStream());
+    }
+    req.setParam(REMOTE_ADDR_KEY, servletReq.getRemoteAddr());
+    return req;
+  }
+
+  public static void setCachingHeaders(HttpResponseBuilder response, int ttl, boolean noProxy) {
+    // Initial cache control headers are in this response, we should now sanitize them or set them if they are missing.
+    String cacheControl = response.getHeader("Cache-Control");
+    String pragma = response.getHeader("Pragma");
+    for (Pair<String, String> header : HttpUtil.getCachingHeadersToSet(ttl, cacheControl, pragma, noProxy)) {
+      response.setHeader(header.one, header.two);
+    }
+  }
+
+  public static void copyToServletResponseAndOverrideCacheHeaders(
+      HttpResponse response, HttpServletResponse servletResponse)
+      throws IOException {
+    copyHeadersAndStatusToServletResponse(response, servletResponse);
+    HttpUtil.setCachingHeaders(servletResponse, (int)(response.getCacheTtl() / 1000L));
+    copyContentToServletResponse(response, servletResponse);
+  }
+
+  public static void copyToServletResponse(
+      HttpResponse response, HttpServletResponse servletResponse) throws IOException {
+    copyHeadersAndStatusToServletResponse(response, servletResponse);
+    copyContentToServletResponse(response, servletResponse);
+  }
+
+  public static void copyContentToServletResponse(
+      HttpResponse response, HttpServletResponse servletResponse) throws IOException {
+    servletResponse.setContentLength(response.getContentLength());
+    IOUtils.copy(response.getResponse(), servletResponse.getOutputStream());
+
+  }
+  public static void copyHeadersAndStatusToServletResponse(
+      HttpResponse response, HttpServletResponse servletResponse) {
+    servletResponse.setStatus(response.getHttpStatusCode());
+    for (Map.Entry<String, String> header : response.getHeaders().entries()) {
+      servletResponse.addHeader(header.getKey(), header.getValue());
+    }
+  }
+
+  /**
+   * Validates and normalizes the given url, ensuring that it is non-null, has
+   * scheme http or https, and has a path value of some kind.
+   *
+   * @return A URI representing a validated form of the url.
+   * @throws GadgetException If the url is not valid.
+   */
+  public static Uri validateUrl(Uri urlToValidate) throws GadgetException {
+    if (urlToValidate == null) {
+      throw new GadgetException(GadgetException.Code.MISSING_PARAMETER, "Missing url param",
+          HttpResponse.SC_BAD_REQUEST);
+    }
+    UriBuilder url = new UriBuilder(urlToValidate);
+    if (!"http".equals(url.getScheme()) && !"https".equals(url.getScheme())) {
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "Invalid request url scheme in url: " + Utf8UrlCoder.encode(urlToValidate.toString()) +
+          "; only \"http\" and \"https\" supported.", HttpResponse.SC_BAD_REQUEST);
+    }
+    if (url.getPath() == null || url.getPath().length() == 0) {
+      url.setPath("/");
+    }
+    return url.toUri();
+  }
+
+  /**
+   * Sets standard forwarding headers on the proxied request.
+   * @param inboundRequest
+   * @param req
+   * @throws GadgetException
+   */
+  public static void setXForwardedForHeader(HttpRequest inboundRequest, HttpRequest req)
+      throws GadgetException {
+    String forwardedFor = getXForwardedForHeader(inboundRequest.getHeader("X-Forwarded-For"),
+        inboundRequest.getParam(ServletUtil.REMOTE_ADDR_KEY));
+    if (forwardedFor != null) {
+      req.setHeader("X-Forwarded-For", forwardedFor);
+    }
+  }
+
+  public static void setXForwardedForHeader(HttpServletRequest inboundRequest, HttpRequest req) {
+    String forwardedFor = getXForwardedForHeader(inboundRequest.getHeader("X-Forwarded-For"),
+        inboundRequest.getRemoteAddr());
+    if (forwardedFor != null) {
+      req.setHeader("X-Forwarded-For", forwardedFor);
+    }
+  }
+
+  private static String getXForwardedForHeader(String origValue, String remoteAddr) {
+    if (!Strings.isNullOrEmpty(remoteAddr)) {
+      if (Strings.isNullOrEmpty(origValue)) {
+        origValue = remoteAddr;
+      } else {
+        origValue = remoteAddr + ", " + origValue;
+      }
+    }
+    return origValue;
+  }
+
+  /**
+   * @return An HttpResponse object wrapping the given GadgetException.
+   */
+  public static HttpResponse errorResponse(GadgetException e) {
+    return new HttpResponseBuilder().setHttpStatusCode(e.getHttpStatusCode())
+        .setHeader("Content-Type", "text/plain")
+        .setResponseString(e.getMessage() != null ? e.getMessage() : "").create();
+  }
+
+  /**
+   * Converts the given {@code HttpResponse} into JSON form, with at least
+   * one field, dataUri, containing a Data URI that can be inlined into an HTML page.
+   * Any metadata on the given {@code HttpResponse} is also added as fields.
+   *
+   * @param response Input HttpResponse to convert to JSON.
+   * @return JSON-containing HttpResponse.
+   * @throws IOException If there are problems reading from {@code response}.
+   */
+  public static HttpResponse convertToJsonResponse(HttpResponse response) throws IOException {
+    // Pull out charset, if present. If not, this operation simply returns contentType.
+    String contentType = response.getHeader("Content-Type");
+    if (contentType == null) {
+      contentType = "";
+    } else if (contentType.contains(";")) {
+      contentType = StringUtils.split(contentType, ';')[0].trim();
+    }
+    // First and most importantly, emit dataUri.
+    // Do so in streaming fashion, to avoid needless buffering.
+    ByteArrayOutputStream os = new ByteArrayOutputStream();
+    PrintWriter pw = new PrintWriter(os);
+    pw.write("{\n  ");
+    pw.write(DATA_URI_KEY);
+    pw.write(":'data:");
+    pw.write(contentType);
+    pw.write(";base64;charset=");
+    pw.write(response.getEncoding());
+    pw.write(",");
+    pw.flush();
+
+    // Stream out the base64-encoded data.
+    // Ctor args indicate to encode w/o line breaks.
+    Base64InputStream b64input = new Base64InputStream(response.getResponse(), true, 0, null);
+    byte[] buf = new byte[1024];
+
+    try {
+      int read;
+      while (( read = b64input.read(buf, 0, 1024)) > 0) {
+        os.write(buf, 0, read);
+      }
+    } finally {
+      IOUtils.closeQuietly(b64input);
+    }
+
+    // Complete the JSON object.
+    pw.write("',\n  ");
+    boolean first = true;
+    for (Map.Entry<String, String> metaEntry : response.getMetadata().entrySet()) {
+      if (DATA_URI_KEY.equals(metaEntry.getKey())) continue;
+      if (!first) {
+        pw.write(",\n  ");
+      }
+      first = false;
+      pw.write("'");
+      pw.write(StringEscapeUtils.escapeEcmaScript(metaEntry.getKey()).replace("'", "\'"));
+      pw.write("':'");
+      pw.write(StringEscapeUtils.escapeEcmaScript(metaEntry.getValue()).replace("'", "\'"));
+      pw.write("'");
+    }
+    pw.write("\n}");
+    pw.flush();
+
+    return new HttpResponseBuilder()
+        .setHeader("Content-Type", "application/json")
+        .setResponseNoCopy(os.toByteArray())
+        .create();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java
new file mode 100644
index 0000000..31fe8b3
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ApplicationManifest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.uri.Uri;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents an opensocial application manifest.
+ */
+public class ApplicationManifest {
+  public static final String NAMESPACE = "http://ns.opensocial.org/2008/application";
+
+  private final Map<String, String> versions;
+  private final Map<String, Uri> gadgets;
+  private final Uri uri;
+
+  public ApplicationManifest(Uri uri, Element xml) throws SpecParserException {
+    ImmutableMap.Builder<String, String> versions = ImmutableMap.builder();
+    ImmutableMap.Builder<String, Uri> gadgets = ImmutableMap.builder();
+
+    NodeList nodes = xml.getElementsByTagName("gadget");
+    for (int i = 0, j = nodes.getLength(); i < j; ++i) {
+      Element gadget = (Element) nodes.item(i);
+      String version = getVersionString(gadget);
+      Uri spec = getSpecUri(uri, gadget);
+      gadgets.put(version, spec);
+      for (String label : getLabels(gadget)) {
+        versions.put(label, version);
+      }
+    }
+
+    this.uri = uri;
+    this.versions = versions.build();
+    this.gadgets = gadgets.build();
+  }
+
+  private static Uri getSpecUri(Uri baseUri, Element gadget) throws SpecParserException {
+    NodeList specs = gadget.getElementsByTagName("spec");
+
+    if (specs.getLength() > 1) {
+      throw new SpecParserException("Only one spec per gadget block may be specified.");
+    } else if (specs.getLength() == 0) {
+      throw new SpecParserException("No spec specified.");
+    }
+
+    try {
+      String relative = specs.item(0).getTextContent();
+      Uri specUri = baseUri.resolve(Uri.parse(relative));
+      if (specUri.equals(baseUri)) {
+        throw new SpecParserException("Manifest is self-referencing.");
+      }
+      return specUri;
+    } catch (IllegalArgumentException e) {
+      throw new SpecParserException("Invalid spec URI.");
+    }
+  }
+
+  private static String getVersionString(Element gadget) throws SpecParserException {
+    NodeList versions = gadget.getElementsByTagName("version");
+
+    if (versions.getLength() > 1) {
+      throw new SpecParserException("Only one version per gadget block may be specified.");
+    } else if (versions.getLength() == 0) {
+      throw new SpecParserException("No version specified.");
+    }
+
+    return versions.item(0).getTextContent();
+  }
+
+  private static List<String> getLabels(Element gadget) {
+    NodeList labels = gadget.getElementsByTagName("label");
+    List<String> list = new ArrayList<String>(labels.getLength());
+
+    for (int i = 0, j = labels.getLength(); i < j; ++i) {
+      list.add(labels.item(i).getTextContent());
+    }
+
+    return list;
+  }
+
+  /**
+   * @return The URI of this manifest.
+   */
+  public Uri getUri() {
+    return uri;
+  }
+
+  /**
+   * @return The gadget specified for the version string, or null if the version doesn't exist.
+   */
+  public Uri getGadget(String version) {
+    return gadgets.get(version);
+  }
+
+  /**
+   * @return The version of the gadget for the given label, or null if the label is unsupported.
+   */
+  public String getVersion(String label) {
+    return versions.get(label);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/BaseOAuthService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/BaseOAuthService.java
new file mode 100644
index 0000000..ca332dc
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/BaseOAuthService.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+
+import org.w3c.dom.Element;
+
+import java.util.Map;
+
+/**
+ * Information about an OAuth service that a gadget wants to use.
+ * This class defines the information common to both OAuth1.0 based service and OAuth2.0 based service.
+ *
+ * Instances are immutable.
+ */
+public abstract class BaseOAuthService {
+
+  /**
+   * Constructor for testing only.
+   */
+  BaseOAuthService() { }
+
+  public BaseOAuthService(Element serviceElement, Uri base) throws SpecParserException{
+
+  }
+
+
+
+  /**
+   * Represents /OAuth/Service@name
+   */
+  abstract public String getName() ;
+
+  /**
+   * Method to use for requests to an OAuth request token or access token URL.
+   */
+  public enum Method {
+    GET, POST;
+
+    private static final Map<String, Method> METHODS =
+            ImmutableMap.of(GET.toString(), GET, POST.toString(), POST, "", GET);
+
+    public static Method parse(String value) throws SpecParserException {
+      value = value.trim();
+      Method result = METHODS.get(value);
+      if (result == null) {
+        throw new SpecParserException("Unknown OAuth method: " + value);
+      }
+      return result;
+    }
+  }
+
+  /**
+   * Location for OAuth parameters in requests to an OAuth request token,
+   * access token, or resource URL.
+   */
+  public enum Location {
+    HEADER("auth-header"),
+    URL("uri-query"),
+    BODY("post-body");
+
+    private static final Map<String, Location> LOCATIONS;
+
+    static {
+      LOCATIONS = Maps.newHashMap();
+      for (Location l : Location.values()) {
+        LOCATIONS.put(l.locationString, l);
+      }
+      // Default value
+      LOCATIONS.put("", Location.HEADER);
+    }
+
+    private String locationString;
+    private Location(String locationString) {
+      this.locationString = locationString;
+    }
+
+    @Override
+    public String toString() {
+      return locationString;
+    }
+
+    public static Location parse(String value) throws SpecParserException {
+      value = value.trim();
+      Location result = LOCATIONS.get(value);
+      if (result == null) {
+        throw new SpecParserException("Unknown OAuth param_location: " + value);
+      }
+      return result;
+    }
+  }
+
+  private static final String URL_ATTR = "url";
+  private static final String PARAM_LOCATION_ATTR = "param_location";
+  private static final String METHOD_ATTR = "method";
+
+  /**
+   * Description of an OAuth request token or access token URL.
+   */
+  public static class EndPoint {
+    public final Uri url;
+    public Uri getUrl() {
+		return url;
+	}
+
+	public Method getMethod() {
+		return method;
+	}
+
+	public Location getLocation() {
+		return location;
+	}
+
+	public final Method method;
+    public final Location location;
+
+    public EndPoint(Uri url, Method method, Location location) {
+      this.url = url;
+      this.method = method;
+      this.location = location;
+    }
+
+    public String toString(String element) {
+      return '<' + element + " url='" + url.toString() + "' " +
+              "method='" + method + "' param_location='" + location + "'/>";
+    }
+  }
+
+  Uri parseAuthorizationUrl(Element child, Uri base) throws SpecParserException {
+    Uri url = XmlUtil.getHttpUriAttribute(child, URL_ATTR, base);
+    if (url == null) {
+      throw new SpecParserException("OAuth/Service/Authorization @url is not valid: " +
+          child.getAttribute(URL_ATTR));
+    }
+    return base.resolve(url);
+  }
+
+
+  EndPoint parseEndPoint(String where, Element child, Uri base) throws SpecParserException {
+    Uri url = XmlUtil.getHttpUriAttribute(child, URL_ATTR, base);
+    if (url == null) {
+      throw new SpecParserException("Not an HTTP url: " + child.getAttribute(URL_ATTR));
+    }
+
+    Location location = Location.parse(child.getAttribute(PARAM_LOCATION_ATTR));
+    Method method = Method.parse(child.getAttribute(METHOD_ATTR));
+    return new EndPoint(base.resolve(url), method, location);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ExternalServices.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ExternalServices.java
new file mode 100644
index 0000000..d1d4e5c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ExternalServices.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Map;
+
+/**
+ * Represents the ExternalServices tag in the gadget spec.
+ *
+ * It includes the child ServiceTag and its text element.
+ *
+ * @since 2.5.0
+ */
+public class ExternalServices {
+  // The name to be used in the "alias" request parameters.
+  private static final String ATTR_ALIAS = "alias";
+
+  private Map<String, ServiceTag> serviceTags;
+
+  public ExternalServices(Element element) {
+    Map<String, ServiceTag> serviceTagsBuilder = Maps.newLinkedHashMap();
+    parseServiceTags(element, serviceTagsBuilder);
+
+    serviceTags = ImmutableMap.copyOf(serviceTagsBuilder);
+  }
+
+  public Map<String, ServiceTag> getServiceTags() {
+    return serviceTags;
+  }
+
+  private void parseServiceTags(Element element, Map<String, ServiceTag> serviceTagsBuilder) {
+    NodeList children = element.getChildNodes();
+    for (int i = 0, j = children.getLength(); i < j; ++i) {
+      Node child = children.item(i);
+      String tagName = child.getNodeName();
+      if (!(child instanceof Element)) continue;
+
+      // only process ServiceTag child tags
+      if(ServiceTag.SERVICE_TAG.equals(tagName)) {
+        String alias = XmlUtil.getAttribute(child, ATTR_ALIAS, "");
+        String tag = child.getTextContent();
+        tag = (tag != null) ? tag.trim() : "";
+        ServiceTag serviceTag = new ServiceTag(alias, tag);
+        serviceTagsBuilder.put(alias, serviceTag);
+      }
+    }
+  }
+
+  /**
+   * Represent the ServiceTag tag in the gadget spec.
+   *
+   * @since 2.5.0
+   */
+  public static class ServiceTag {
+    public static final String SERVICE_TAG = "ServiceTag";
+
+    private final String alias;
+    private final String tag;
+
+    public ServiceTag(String alias, String tag) {
+      this.alias = alias;
+      this.tag = tag;
+    }
+
+    public String getAlias() {
+      return alias;
+    }
+
+    public String getTag() {
+      return tag;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java
new file mode 100644
index 0000000..d5a4298
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Feature.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+
+/**
+ * Represents a Require or Optional tag.
+ * No substitutions on any fields.
+ */
+public class Feature {
+  public static final Feature CORE_FEATURE = new Feature("core");
+  public static final Feature SECURITY_TOKEN_FEATURE = new Feature("security-token");
+
+  // Instantiable only by CORE_FEATURE.
+  private Feature(String name) {
+    this.params = ImmutableMultimap.of();
+    this.required = true;
+    this.name = name;
+    this.views = ImmutableSet.of();
+  }
+
+  /**
+   * Require@feature
+   * Optional@feature
+   */
+  private final String name;
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Require.Param
+   * Optional.Param
+   *
+   * Flattened into a map where Param@name is the key and Param content is
+   * the value.
+   */
+  private final Multimap<String, String> params;
+  public Multimap<String, String> getParams() {
+    return params;
+  }
+
+  /**
+   * Returns the first value for any feature parameter, or null
+   * if the parameter does not exist.
+   */
+  public String getParam(String key) {
+    Collection<String> values = params.get(key);
+    if (values == null || values.isEmpty()) {
+      return null;
+    }
+
+    return values.iterator().next();
+  }
+
+  /**
+   * Returns the values for the key, or an empty collection.
+   */
+  public Collection<String> getParamCollection(String key) {
+    return params.get(key);
+  }
+
+  /**
+   * Whether this is a Require or an Optional feature.
+   */
+  private final boolean required;
+  public boolean getRequired() {
+    return required;
+  }
+
+  /**
+   * Require@views
+   * Optional@views
+   *
+   * Views associated with this feature
+   */
+  private final Set<String> views;
+  public Set<String> getViews() {
+    return views;
+  }
+
+  /**
+   * Produces an xml representation of the feature.
+   */
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append(required ? "<Require" : "<Optional")
+       .append(" feature=\"")
+       .append(name);
+    if (!views.isEmpty()) {
+      buf.append("\" views=\"").append(StringUtils.join(views, ','));
+    }
+    buf.append("\">");
+    for (Map.Entry<String, Collection<String>> entry : params.asMap().entrySet()) {
+      buf.append("\n<Param name=\"")
+         .append(entry.getKey())
+         .append("\">")
+         .append(entry.getValue())
+         .append("</Param>");
+    }
+    buf.append(required ? "</Require>" : "</Optional>");
+    return buf.toString();
+  }
+
+  /**
+   * Creates a new Feature from an xml node.
+   *
+   * @param feature The feature to create
+   * @throws SpecParserException When the Require or Optional tag is not valid
+   */
+  public Feature(Element feature) throws SpecParserException {
+    this.required = feature.getNodeName().equals("Require");
+    String name = XmlUtil.getAttribute(feature, "feature");
+    if (name == null) {
+      throw new SpecParserException(
+          (required ? "Require" : "Optional") +"@feature is required.");
+    }
+    this.name = name;
+    NodeList children = feature.getElementsByTagName("Param");
+    if (children.getLength() > 0) {
+      ImmutableMultimap.Builder<String, String> params = ImmutableMultimap.builder();
+
+      for (int i = 0, j = children.getLength(); i < j; ++i) {
+        Element param = (Element)children.item(i);
+        String paramName = XmlUtil.getAttribute(param, "name");
+        if (paramName == null) {
+          throw new SpecParserException("Param@name is required");
+        }
+        params.put(paramName, param.getTextContent());
+      }
+      this.params = params.build();
+    } else {
+      this.params = ImmutableMultimap.of();
+    }
+    // Record all the associated views
+    String viewNames = XmlUtil.getAttribute(feature, "views", "").trim();
+    this.views = ImmutableSet.copyOf(Splitter.on(',').omitEmptyStrings().trimResults().split(viewNames));
+  }
+
+
+  /**
+   * @param name feature name
+   * @param params feature parameters
+   * @param required true if feature is required, false otherwise
+   * @param views views declared in the feature.
+   */
+  public Feature(String name, Multimap<String, String> params,
+      boolean required, Set<String> views) {
+    this.name = name;
+    this.params = params;
+    this.required = required;
+    this.views = views;
+  }
+
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java
new file mode 100644
index 0000000..4849410
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/GadgetSpec.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.common.util.OpenSocialVersion;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.spec.View.ContentType;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.MapMaker;
+import com.google.common.collect.Maps;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Represents a gadget specification root element (Module).
+ *
+ * @see <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v08/gadget-spec">gadgets spec</a>
+ */
+public class GadgetSpec {
+  public static final String DEFAULT_VIEW = "default";
+  public static final Locale DEFAULT_LOCALE = new Locale("all", "ALL");
+
+  private static final String ATTR_SPECIFICATION_VERSION = "specificationVersion";
+  public static final String DOCTYPE_QUIRKSMODE = "quirksmode";
+
+  /**
+   * Creates a new Module from the given xml input.
+   *
+   * @param url The original url of the gadget.
+   * @param doc The pre-parsed xml document.
+   * @param original Unparsed input XML. Used to generate checksums.
+   *
+   * @throws SpecParserException If xml can not be processed as a valid gadget spec.
+   */
+  public GadgetSpec(Uri url, Element doc, String original) throws SpecParserException {
+    this.url = url;
+
+    // This might not be good enough; should we take message bundle changes into account?
+    this.checksum = HashUtil.checksum(original.getBytes());
+
+    NodeList children = doc.getChildNodes();
+    //Save specification version of this Gadget
+    setAttribute(ATTR_SPECIFICATION_VERSION,doc.getAttribute(ATTR_SPECIFICATION_VERSION));
+
+    ModulePrefs modulePrefs = null;
+    // Lets try keep order of user prefs and views
+    Map<String,UserPref> prefsBuilder = Maps.newLinkedHashMap();
+    Map<String, List<Element>> views = Maps.newLinkedHashMap();
+    for (int i = 0, j = children.getLength(); i < j; ++i) {
+      Node child = children.item(i);
+      if (!(child instanceof Element)) {
+        continue;
+      }
+      Element element = (Element)child;
+      String name = element.getTagName();
+      if ("ModulePrefs".equals(name)) {
+        if (modulePrefs == null) {
+          modulePrefs = new ModulePrefs(element, url);
+        } else {
+          throw new SpecParserException("Only 1 ModulePrefs is allowed.");
+        }
+      }
+      if ("UserPref".equals(name)) {
+        UserPref pref = new UserPref(element);
+        if (prefsBuilder.containsKey(pref.getName())) {
+          throw new SpecParserException("Duplicate value for user pref " + pref.getName());
+        }
+        prefsBuilder.put(pref.getName(), pref);
+      }
+      if ("Content".equals(name)) {
+        String viewNames = XmlUtil.getAttribute(element, "view", "default");
+        for (String view : Splitter.on(',').trimResults().split(viewNames)) {
+          List<Element> viewElements = views.get(view);
+          if (viewElements == null) {
+            viewElements = Lists.newLinkedList();
+            views.put(view, viewElements);
+          }
+          viewElements.add(element);
+        }
+      }
+      if("ExternalServices".equals(name)) {
+        // There could be only one ExternalServices tag
+        if(externalServices != null) {
+          throw new SpecParserException("Only 1 ExternalServices is allowed.");
+        }
+        externalServices = new ExternalServices(element);
+      }
+    }
+
+    if (modulePrefs == null) {
+      throw new SpecParserException("At least 1 ModulePrefs is required.");
+    } else {
+      this.modulePrefs = modulePrefs;
+    }
+
+    if (views.isEmpty()) {
+      throw new SpecParserException("At least 1 Content is required.");
+    } else {
+      Map<String, View> tmpViews = Maps.newHashMap();
+      for (Map.Entry<String, List<Element>> view : views.entrySet()) {
+        View v = new View(view.getKey(), view.getValue(), url);
+        tmpViews.put(v.getName(), v);
+      }
+      this.views = ImmutableMap.copyOf(tmpViews);
+    }
+    this.userPrefs = ImmutableMap.copyOf(prefsBuilder);
+  }
+
+  /**
+   * Use for testing.
+   */
+  @VisibleForTesting
+  public GadgetSpec(Uri url, String xml) throws SpecParserException {
+    this(url, XmlUtil.parseSilent(xml), xml);
+  }
+
+  /**
+   * Constructs a GadgetSpec for substitute calls.
+   * @param spec
+   */
+  protected GadgetSpec(GadgetSpec spec) {
+    url = spec.url;
+    checksum = spec.checksum;
+    attributes.putAll(spec.attributes);
+  }
+
+  /**
+   * Returns this Gadget's specification version.  Defaults to 1.0 if attribute not set.
+   * @return Version value as String
+   */
+  public OpenSocialVersion getSpecificationVersion(){
+    // 1.0 is default if unspecified as defined in Section 7 of OS 1.1 Core Gadget specification
+    String value = (String)attributes.get(ATTR_SPECIFICATION_VERSION);
+    if (value == null) {
+      return new OpenSocialVersion("1.0");
+    } else {
+      return new OpenSocialVersion(value);
+    }
+  }
+
+  /**
+   * The url for this gadget spec.
+   */
+  private final Uri url;
+  public Uri getUrl() {
+    return url;
+  }
+
+  /**
+   * A checksum of the gadget's content.
+   */
+  private final String checksum;
+  public String getChecksum() {
+    return checksum;
+  }
+
+  /**
+   * ModulePrefs
+   */
+  protected ModulePrefs modulePrefs;
+  public ModulePrefs getModulePrefs() {
+    return modulePrefs;
+  }
+
+
+  /**
+   * UserPref
+   */
+  protected Map<String,UserPref> userPrefs;
+  public Map<String,UserPref> getUserPrefs() {
+    return userPrefs;
+  }
+
+  /**
+   * Content
+   * Mapping is view -> Content section.
+   */
+  protected Map<String, View> views;
+  public Map<String, View> getViews() {
+    return views;
+  }
+
+  /**
+   * ExternalServices
+   */
+  protected ExternalServices externalServices;
+  public ExternalServices getExternalServices() {
+    return externalServices;
+  }
+
+  /**
+   * Retrieves a single view by name.
+   *
+   * @param name The name of the view you want to see
+   * @return The view object, if it exists, or null.
+   */
+  public View getView(String name) {
+    return views.get(name);
+  }
+
+  /**
+   * A map of attributes associated with the instance of the spec
+   * Used by handler classes to use specs to carry context.
+   * Not defined by the specification
+   */
+  private final Map<String, Object> attributes = new MapMaker().makeMap();
+  public Object getAttribute(String key) {
+    return attributes.get(key);
+  }
+
+  /**
+   * Sets an attribute on the gadget spec. This should only be done during a constructing phase, as
+   * a GadgetSpec should be effectively immutable after it is constructed.
+   *
+   * @param key The attribute name.
+   * @param o The value of the attribute.
+   */
+  public void setAttribute(String key, Object o) {
+    attributes.put(key, o);
+  }
+
+  /**
+   * Performs substitutions on the spec. See individual elements for
+   * details on what gets substituted.
+   *
+   * @param substituter
+   * @return The substituted spec.
+   */
+  public GadgetSpec substitute(Substitutions substituter) {
+    GadgetSpec spec = new GadgetSpec(this);
+    spec.modulePrefs = modulePrefs.substitute(substituter);
+
+    if (userPrefs.isEmpty()) {
+      spec.userPrefs = ImmutableMap.of();
+    } else {
+      ImmutableMap.Builder<String,UserPref> prefs = ImmutableMap.builder();
+      for (UserPref pref : this.userPrefs.values()) {
+        prefs.put(pref.getName(), pref.substitute(substituter));
+      }
+      spec.userPrefs = prefs.build();
+    }
+
+    ImmutableMap.Builder<String, View> viewMap = ImmutableMap.builder();
+    for (View view : views.values()) {
+      viewMap.put(view.getName(), view.substitute(substituter));
+    }
+    spec.views = viewMap.build();
+
+    return spec;
+  }
+
+  /**
+   * Returns a copy of the spec with all type=url views removed.
+   */
+  public GadgetSpec removeUrlViews() {
+    GadgetSpec spec = new GadgetSpec(this);
+    spec.modulePrefs = modulePrefs;
+    spec.userPrefs = userPrefs;
+    ImmutableMap.Builder<String, View> viewMap = ImmutableMap.builder();
+    for (View view : views.values()) {
+      if (view.getType() != ContentType.URL) {
+        viewMap.put(view.getName(), view);
+      }
+    }
+    spec.views = viewMap.build();
+    return spec;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<Module>\n")
+       .append(modulePrefs).append('\n');
+    for (UserPref pref : userPrefs.values()) {
+      buf.append(pref).append('\n');
+    }
+    for (Map.Entry<String, View> view : views.entrySet()) {
+      buf.append(view.getValue()).append('\n');
+    }
+    buf.append("</Module>");
+    return buf.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Icon.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Icon.java
new file mode 100644
index 0000000..c543c29
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Icon.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import org.w3c.dom.Element;
+
+/**
+ * Represents a ModuleSpec.Icon tag.
+ *
+ * TODO: Support substitution
+ */
+public class Icon {
+  /**
+   * Icon@mode
+   * Probably better labeled "encoding"; currently only base64 is supported.
+   * If mode is not set, content must be a url. Otherwise, content is
+   * a mode-encoded image with a mime type equal to type.
+   */
+  private final String mode;
+  public String getMode() {
+    return mode;
+  }
+
+  /**
+   * Icon@type
+   * Mime type of the icon
+   */
+  private final String type;
+  public String getType() {
+    return type;
+  }
+
+  /**
+   * Icon#CDATA
+   *
+   * Message Bundles
+   */
+  private String content;
+  public String getContent() {
+    return content;
+  }
+
+  /**
+   * Substitutes the icon fields according to the spec.
+   *
+   * @param substituter
+   * @return The substituted icon
+   */
+  public Icon substitute(Substitutions substituter) {
+    Icon icon = new Icon(this);
+    icon.content = substituter.substituteString(content);
+    return icon;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder(32 + type.length() + content.length());
+    buf.append("<Icon type='").append(type).append('\'');
+    if (mode != null) {
+      buf.append(" mode='").append(mode).append('\'');
+    }
+    buf.append('>')
+       .append(content)
+       .append("</Icon>");
+    return buf.toString();
+  }
+
+  /**
+   * Currently does not validate icon data.
+   * @param element
+   */
+  public Icon(Element element) throws SpecParserException {
+    mode = XmlUtil.getAttribute(element, "mode");
+    if (mode != null && !mode.equals("base64")) {
+      throw new SpecParserException(
+          "The only valid value for Icon@mode is \"base64\"");
+    }
+    type = XmlUtil.getAttribute(element, "type", "");
+    content = element.getTextContent();
+  }
+
+  /**
+   * Creates an icon for substitute()
+   *
+   * @param icon
+   */
+  private Icon(Icon icon) {
+    mode = icon.mode;
+    type = icon.type;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LinkSpec.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LinkSpec.java
new file mode 100644
index 0000000..5df6be0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LinkSpec.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import org.w3c.dom.Element;
+
+/**
+ * Represents /ModulePrefs/Link elements.
+ */
+public class LinkSpec {
+  private final Uri base;
+
+  public LinkSpec(Element element, Uri base) throws SpecParserException {
+    this.base = base;
+    rel = XmlUtil.getAttribute(element, "rel");
+    if (rel == null) {
+      throw new SpecParserException("Link/@rel is required!");
+    }
+    href = XmlUtil.getUriAttribute(element, "href");
+    if (href == null) {
+      throw new SpecParserException("Link/@href is required!");
+    }
+    method = getMethodAttribute(element);
+  }
+
+  private LinkSpec(LinkSpec rhs, Substitutions substitutions) {
+    rel = substitutions.substituteString(rhs.rel);
+    base = rhs.base;
+    href = base.resolve(substitutions.substituteUri(rhs.href));
+    method = rhs.method;
+  }
+
+  /**
+   * Link/@rel
+   */
+  private final String rel;
+  public String getRel() {
+    return rel;
+  }
+
+  /**
+   * Link/@href
+   */
+  private final Uri href;
+  public Uri getHref() {
+    return href;
+  }
+
+  /**
+   * Link/@method
+   */
+  private final String method;
+  public String getMethod() {
+    return method;
+  }
+
+  /**
+   * Performs variable substitution on all visible elements.
+   */
+  public LinkSpec substitute(Substitutions substitutions) {
+    return new LinkSpec(this, substitutions);
+  }
+
+  @Override
+  public String toString() {
+    String methodAttribute = (method != null) ? "method='" + method + "' " : "";
+    return "<Link rel='" + rel + "' href='" + href.toString() + "' " + methodAttribute + "/>";
+  }
+
+  private String getMethodAttribute(Element element) {
+    String method = XmlUtil.getAttribute(element, "method");
+    return ("GET".equals(method) || "POST".equals(method)) ? method : "GET";
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LocaleSpec.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LocaleSpec.java
new file mode 100644
index 0000000..86abf53
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/LocaleSpec.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.w3c.dom.Element;
+
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents a Locale tag.
+ * Generally compatible with java.util.Locale, but with some extra
+ * localization data from the spec.
+ * Named "LocaleSpec" so as to not conflict with java.util.Locale
+ *
+ * No localization.
+ * No user pref substitution.
+ */
+public class LocaleSpec {
+  private final Locale locale;
+  private final String languageDirection;
+  private final Uri messages;
+  private final MessageBundle messageBundle;
+
+  /**
+   * @param specUrl The url that the spec is loaded from. messages is assumed
+   *     to be relative to this path.
+   * @throws SpecParserException If language_direction is not valid
+   */
+  public LocaleSpec(Element element, Uri specUrl) throws SpecParserException {
+    String language = XmlUtil.getAttribute(element, "lang", "all").toLowerCase();
+    String country = XmlUtil.getAttribute(element, "country", "ALL").toUpperCase();
+    this.locale = new Locale(language, country);
+
+    languageDirection = XmlUtil.getAttribute(element, "language_direction", "ltr");
+    if (!("ltr".equals(languageDirection) || "rtl".equals(languageDirection))) {
+      throw new SpecParserException("Locale/@language_direction must be ltr or rtl");
+    }
+    // Record all the associated views
+    String viewNames = XmlUtil.getAttribute(element, "views", "").trim();
+
+    this.views = ImmutableSet.copyOf(Splitter.on(',').omitEmptyStrings().trimResults().split(viewNames));
+
+    String messagesString = XmlUtil.getAttribute(element, "messages");
+    if (messagesString == null) {
+      this.messages = Uri.parse("");
+    } else {
+      try {
+        this.messages = specUrl.resolve(Uri.parse(messagesString));
+      } catch (IllegalArgumentException e) {
+        throw new SpecParserException("Locale@messages url is invalid.");
+      }
+    }
+    messageBundle = new MessageBundle(element);
+  }
+
+  public Locale getLocale() {
+    return locale;
+  }
+
+  /**
+   * Locale@lang
+   */
+  public String getLanguage() {
+    return locale.getLanguage();
+  }
+
+  /**
+   * Locale@country
+   */
+  public String getCountry() {
+    return locale.getCountry();
+  }
+
+  /**
+   * Locale@language_direction
+   */
+  public String getLanguageDirection() {
+    return languageDirection;
+  }
+
+  /**
+   * Locale@messages
+   */
+  public Uri getMessages() {
+    return messages;
+  }
+
+  /**
+   * Locale/msg
+   */
+  public MessageBundle getMessageBundle() {
+    return messageBundle;
+  }
+
+  /**
+   * Locale@views
+   *
+   * Views associated with this Locale
+   */
+  private final Set<String> views;
+  public Set<String> getViews() {
+    return views;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<Locale").append(" lang='").append(getLanguage()).append('\'')
+        .append(" country='").append(getCountry()).append('\'')
+        .append(" language_direction='").append(languageDirection).append('\'');
+    if (!views.isEmpty()) {
+      buf.append(" views=\'").append(StringUtils.join(views, ',')).append('\'');
+    }
+    buf.append(" messages='").append(messages).append("'>\n");
+    for (Map.Entry<String, String> entry : messageBundle.getMessages().entrySet()) {
+      buf.append("<msg name='").append(entry.getKey()).append("'>").append(entry.getValue()).append("</msg>\n");
+    }
+    buf.append("</Locale>");
+    return buf.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/MessageBundle.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/MessageBundle.java
new file mode 100644
index 0000000..22958f5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/MessageBundle.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Map;
+
+/**
+ * Represents a messagebundle structure.
+ */
+public class MessageBundle {
+  public static final MessageBundle EMPTY = new MessageBundle();
+
+  private static final DefaultHtmlSerializer HTML_SERIALIZER = new DefaultHtmlSerializer();
+  private final ImmutableMap<String, String> messages;
+  private final String languageDirection;
+
+  /* lazily created cache of the json-encoded form of the bundle */
+  private String jsonString;
+
+   /**
+   * Constructs a message bundle from input xml (fetched from an external file).
+   *
+   * @param locale The LocaleSpec element that this bundle was constructed from.
+   * @param xml The content of the remote file.
+   * @throws SpecParserException if parsing fails.
+   */
+  public MessageBundle(LocaleSpec locale, String xml) throws SpecParserException {
+    Element doc;
+    try {
+      doc = XmlUtil.parse(xml);
+    } catch (XmlException e) {
+      throw new SpecParserException("Malformed XML in file " + locale.getMessages()
+          + ": " + e.getMessage());
+    }
+    messages = parseMessages(doc);
+    languageDirection = locale.getLanguageDirection();
+  }
+
+   /**
+   * Constructs a message bundle from a prebuilt map.
+   *
+   * @param locale The LocaleSpec element that this bundle was constructed from.
+   * @param map The content of the message map.
+   */
+  public MessageBundle(LocaleSpec locale, Map<String, String> map) {
+     messages = ImmutableMap.copyOf(map);
+     languageDirection = locale.getLanguageDirection();
+   }
+
+  /**
+   * Constructs a message bundle from a /ModulePrefs/Locale with nested messages.
+   * @param element XML Dom element to parse
+   * @throws SpecParserException when badly formed xml is provided
+   */
+  public MessageBundle(Element element) throws SpecParserException {
+    messages = parseMessages(element);
+    languageDirection = XmlUtil.getAttribute(element, "language_direction", "ltr");
+  }
+
+  /**
+   * Create a MessageBundle by merging multiple bundles together.
+   *
+   * @param bundles the bundles to merge, in order
+   */
+  public MessageBundle(MessageBundle... bundles) {
+    Map<String, String> merged = Maps.newHashMap();
+    String dir = null;
+    for (MessageBundle bundle : bundles) {
+      merged.putAll(bundle.messages);
+      dir = bundle == EMPTY ? dir : bundle.languageDirection;
+    }
+    messages = ImmutableMap.copyOf(merged);
+    languageDirection = dir != null ? dir : "ltr";
+  }
+
+  private MessageBundle() {
+    this.messages = ImmutableMap.of();
+    jsonString = "{}";
+    languageDirection = "ltr";
+  }
+
+  /**
+   * @return The language direction associated with this message bundle, derived from the LocaleSpec
+   * element that the bundle was constructed from.
+   */
+  public String getLanguageDirection() {
+    return languageDirection;
+  }
+
+  /**
+   * @return A read-only view of the message bundle.
+   */
+  public Map<String, String> getMessages() {
+    return messages;
+  }
+
+  /**
+   * Return the message bundle contents as a JSON encoded string.
+   *
+   * @return json representation of the message bundler
+   */
+  public String toJSONString() {
+    if (jsonString == null) {
+      jsonString = JsonSerializer.serialize(messages);
+    }
+    return jsonString;
+  }
+
+  /**
+   * Extracts messages from an element.
+   * @param element Xml dom containing mesage bundle nodes
+   * @return Immutable map of message keys to values
+   * @throws SpecParserException when invalid xml is parsed
+   */
+  private ImmutableMap<String, String> parseMessages(Element element)
+      throws SpecParserException {
+    NodeList nodes = element.getElementsByTagName("msg");
+
+    Map<String, String> messages = Maps.newHashMapWithExpectedSize(nodes.getLength());
+
+    for (int i = 0, j = nodes.getLength(); i < j; ++i) {
+      Element msg = (Element)nodes.item(i);
+      String name = XmlUtil.getAttribute(msg, "name");
+      if (name == null) {
+        throw new SpecParserException(
+            "All message bundle entries must have a name attribute.");
+      }
+      StringWriter sw = new StringWriter();
+      NodeList msgChildren = msg.getChildNodes();
+      for (int child = 0; child < msgChildren.getLength(); ++child) {
+        try {
+          if (msgChildren.item(child).getNodeType() == Node.CDATA_SECTION_NODE) {
+            // Workaround to treat CDATA as text.
+            sw.append(msgChildren.item(child).getTextContent());
+          } else {
+            HTML_SERIALIZER.serialize(msgChildren.item(child), sw);
+          }
+        } catch (IOException e) {
+          throw new SpecParserException("Unexpected error getting value of msg node",
+                                        new XmlException(e));
+        }
+      }
+      messages.put(name, sw.toString());
+    }
+
+    return ImmutableMap.copyOf(messages);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<messagebundle>\n");
+    for (Map.Entry<String, String> entry : messages.entrySet()) {
+      buf.append("<msg name=\"").append(entry.getKey()).append("\">")
+         .append(entry.getValue())
+         .append("</msg>\n");
+    }
+    buf.append("</messagebundle>");
+    return buf.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java
new file mode 100644
index 0000000..f9ba86f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/ModulePrefs.java
@@ -0,0 +1,943 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import org.apache.commons.lang3.mutable.MutableBoolean;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.variables.Substitutions;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+
+
+
+/**
+ * Represents the ModulePrefs element of a gadget spec.
+ *
+ * This encapsulates most gadget meta data, including everything except for
+ * Content and UserPref nodes.
+ */
+public class ModulePrefs {
+
+
+  private static final String ATTR_TITLE = "title";
+  private static final String ATTR_TITLE_URL = "title_url";
+  private static final String ATTR_DESCRIPTION = "description";
+  private static final String ATTR_AUTHOR = "author";
+  private static final String ATTR_AUTHOR_EMAIL = "author_email";
+  private static final String ATTR_SCREENSHOT = "screenshot";
+  private static final String ATTR_THUMBNAIL = "thumbnail";
+  private static final String ATTR_DIRECTORY_TITLE = "directory_title";
+  private static final String ATTR_AUTHOR_AFFILIATION = "author_affiliation";
+  private static final String ATTR_AUTHOR_LOCATION = "author_location";
+  private static final String ATTR_AUTHOR_PHOTO = "author_photo";
+  private static final String ATTR_AUTHOR_ABOUTME = "author_aboutme";
+  private static final String ATTR_AUTHOR_QUOTE = "author_quote";
+  private static final String ATTR_AUTHOR_LINK = "author_link";
+  private static final String ATTR_SHOW_STATS = "show_stats";
+  private static final String ATTR_SHOW_IN_DIRECTORY = "show_in_directory";
+  private static final String ATTR_SINGLETON = "singleton";
+  private static final String ATTR_SCALING = "scaling";
+  private static final String ATTR_SCROLLING = "scrolling";
+  private static final String ATTR_DOCTYPE = "doctype";
+  private static final String ATTR_WIDTH = "width";
+  private static final String ATTR_HEIGHT = "height";
+  private static final String ATTR_CATEGORY = "category";
+  private static final String ATTR_CATEGORY2 = "category2";
+  private static final Uri EMPTY_URI = Uri.parse("");
+  private static final String UP_SUBST_PREFIX = "__UP_";
+
+  // Used to identify Locales that are globally scoped
+  private static final String GLOBAL_LOCALE = "";
+
+  private final Map<String, String> attributes;
+  private final Uri base;
+  private final boolean needsUserPrefSubstitution;
+
+  public ModulePrefs(Element element, Uri base) throws SpecParserException {
+    this.base = base;
+    attributes = Maps.newHashMap();
+    NamedNodeMap attributeNodes = element.getAttributes();
+    for (int i = 0; i < attributeNodes.getLength(); i++) {
+      Node node = attributeNodes.item(i);
+      attributes.put(node.getNodeName(), node.getNodeValue());
+    }
+
+    categories = ImmutableList.of(getAttribute(ATTR_CATEGORY, ""), getAttribute(ATTR_CATEGORY2, ""));
+
+    // Eventually use a list of classes
+    MutableBoolean oauthMarker = new MutableBoolean(false);
+
+    Set<ElementVisitor> visitors = ImmutableSet.of(
+        new FeatureVisitor(oauthMarker),
+        new PreloadVisitor(),
+        new OAuthVisitor(oauthMarker),
+        new OAuth2Visitor(oauthMarker),
+        new IconVisitor(),
+        new LocaleVisitor(),
+        new LinkVisitor(),
+        new ExtraElementsVisitor() // keep this last since it accepts any tag
+    );
+
+    walk(element, visitors);
+
+    // Tell the visitors to apply their knowledge
+    for (ElementVisitor ev : visitors) {
+      ev.apply(this);
+    }
+
+    needsUserPrefSubstitution = prefsNeedsUserPrefSubstitution(this);
+  }
+
+  /**
+   * Produces a new, substituted ModulePrefs
+   * @param prefs An existing ModulePrefs instance
+   * @param substituter The substituter to apply
+   */
+  private ModulePrefs(ModulePrefs prefs, Substitutions substituter) {
+    base = prefs.base;
+    categories = prefs.getCategories();
+    features = prefs.getFeatures();
+    globalFeatures = prefs.globalFeatures;
+    allFeatures = prefs.getAllFeatures();
+    allLocales = prefs.allLocales;
+    locales = prefs.locales;
+    oauth = prefs.oauth;
+    oauth2 = prefs.oauth2;
+
+    List<Preload> preloads = Lists.newArrayList();
+    for (Preload preload : prefs.preloads) {
+      preloads.add(preload.substitute(substituter));
+    }
+    this.preloads = ImmutableList.copyOf(preloads);
+
+    List<Icon> icons = Lists.newArrayList();
+    for (Icon icon : prefs.icons) {
+      icons.add(icon.substitute(substituter));
+    }
+    this.icons = ImmutableList.copyOf(icons);
+
+    ImmutableMap.Builder<String, LinkSpec> links = ImmutableMap.builder();
+    for (LinkSpec link : prefs.links.values()) {
+      LinkSpec sub = link.substitute(substituter);
+      links.put(sub.getRel(), sub);
+    }
+    this.links = links.build();
+
+    ImmutableMap.Builder<String, String> attributes = ImmutableMap.builder();
+    for (Map.Entry<String, String> attr : prefs.attributes.entrySet()) {
+      String substituted = substituter.substituteString(attr.getValue());
+      attributes.put(attr.getKey(), substituted);
+    }
+
+    ImmutableMap.Builder<String, Feature> featureBuilder= ImmutableMap.builder();
+    for (Map.Entry<String, Feature> feature : features.entrySet()) {
+      ImmutableMultimap.Builder<String, String> params = ImmutableMultimap.builder();
+      for (Map.Entry<String, String> param: feature.getValue().getParams().entries()){
+        String substituted=substituter.substituteString(param.getValue());
+        params.put(param.getKey(), substituted);
+      }
+      Feature oldFeature=feature.getValue();
+      Feature newFeature=new Feature(oldFeature.getName(), params.build(), oldFeature.getRequired(), oldFeature.getViews());
+      featureBuilder.put(feature.getKey(), newFeature);
+    }
+    this.features=featureBuilder.build();
+
+
+    this.extraElements = ImmutableMultimap.copyOf(prefs.extraElements);
+    this.attributes = attributes.build();
+    this.needsUserPrefSubstitution = prefs.needsUserPrefSubstitution;
+  }
+
+  // Canonical spec items first.
+
+  /**
+   * ModulePrefs@title
+   *
+   * User Pref + Message Bundle + Bidi
+   */
+  public String getTitle() {
+    String title = getAttribute(ATTR_TITLE);
+    return title == null ? "" : title;
+  }
+
+  /**
+   * ModulePrefs@title_url
+   *
+   * User Pref + Message Bundle + Bidi
+   */
+  public Uri getTitleUrl() {
+    return getUriAttribute(ATTR_TITLE_URL);
+  }
+
+  /**
+   * ModulePrefs@description
+   *
+   * Message Bundles
+   */
+  public String getDescription() {
+    return getAttribute(ATTR_DESCRIPTION);
+  }
+
+  /**
+   * ModulePrefs@author
+   *
+   * Message Bundles
+   */
+  public String getAuthor() {
+    return getAttribute(ATTR_AUTHOR);
+  }
+
+  /**
+   * ModulePrefs@author_email
+   *
+   * Message Bundles
+   */
+  public String getAuthorEmail() {
+    return getAttribute(ATTR_AUTHOR_EMAIL);
+  }
+
+  /**
+   * ModulePrefs@screenshot
+   *
+   * Message Bundles
+   */
+  public Uri getScreenshot() {
+    return getUriAttribute(ATTR_SCREENSHOT);
+  }
+
+  /**
+   * ModulePrefs@thumbnail
+   *
+   * Message Bundles
+   */
+  public Uri getThumbnail() {
+    return getUriAttribute(ATTR_THUMBNAIL);
+  }
+
+  // Extended data (typically used by directories)
+
+  /**
+   * ModulePrefs@directory_title
+   *
+   * Message Bundles
+   */
+  public String getDirectoryTitle() {
+    return getAttribute(ATTR_DIRECTORY_TITLE);
+  }
+
+  /**
+   * ModulePrefs@author_affiliation
+   *
+   * Message Bundles
+   */
+  public String getAuthorAffiliation() {
+    return getAttribute(ATTR_AUTHOR_AFFILIATION);
+  }
+
+  /**
+   * ModulePrefs@author_location
+   *
+   * Message Bundles
+   */
+  public String getAuthorLocation() {
+    return getAttribute(ATTR_AUTHOR_LOCATION);
+  }
+
+  /**
+   * ModulePrefs@author_photo
+   *
+   * Message Bundles
+   */
+  public Uri getAuthorPhoto() {
+    return getUriAttribute(ATTR_AUTHOR_PHOTO);
+  }
+
+  /**
+   * ModulePrefs@author_aboutme
+   *
+   * Message Bundles
+   */
+  public String getAuthorAboutme() {
+    return getAttribute(ATTR_AUTHOR_ABOUTME);
+  }
+
+  /**
+   * ModulePrefs@author_quote
+   *
+   * Message Bundles
+   */
+  public String getAuthorQuote() {
+    return getAttribute(ATTR_AUTHOR_QUOTE);
+  }
+
+  /**
+   * ModulePrefs@author_link
+   *
+   * Message Bundles
+   */
+  public Uri getAuthorLink() {
+    return getUriAttribute(ATTR_AUTHOR_LINK);
+  }
+
+  /**
+   * ModulePrefs@show_stats
+   */
+  public boolean getShowStats() {
+    return getBoolAttribute(ATTR_SHOW_STATS);
+  }
+
+  /**
+   * ModulePrefs@show_in_directory
+   */
+  public boolean getShowInDirectory() {
+    return getBoolAttribute(ATTR_SHOW_IN_DIRECTORY);
+  }
+
+  /**
+   * ModulePrefs@singleton
+   */
+  public boolean getSingleton() {
+    return getBoolAttribute(ATTR_SINGLETON);
+  }
+
+  /**
+   * ModulePrefs@scaling
+   */
+  public boolean getScaling() {
+    return getBoolAttribute(ATTR_SCALING);
+  }
+
+  /**
+   * ModulePrefs@scrolling
+   */
+  public boolean getScrolling() {
+    return getBoolAttribute(ATTR_SCROLLING);
+  }
+
+  /**
+   * ModuleSpec@width
+   */
+  public int getWidth() {
+    return getIntAttribute(ATTR_WIDTH);
+  }
+
+  /**
+   * ModuleSpec@height
+   */
+  public int getHeight() {
+    return getIntAttribute(ATTR_HEIGHT);
+  }
+
+  /**
+   * Returns this Gadget's doctype mode.  If null, we will use default mode.
+   *
+   * @return Value of doctype attribute
+   */
+  public String getDoctype(){
+    return getAttribute(ATTR_DOCTYPE);
+  }
+
+  /**
+   * @param name the attribute name
+   * @return the value of an ModulePrefs attribute by name, or null if the
+   *     attribute doesn't exist
+   */
+  public String getAttribute(String name) {
+    return attributes.get(name);
+  }
+
+  /**
+   * @param name the attribute name
+   * @param defaultValue the default Value
+   * @return the value of an ModulePrefs attribute by name, or the default
+   *     value if the attribute doesn't exist
+   */
+  public String getAttribute(String name, String defaultValue) {
+    String value = getAttribute(name);
+    if (value == null) {
+      return defaultValue;
+    } else {
+      return value;
+    }
+  }
+
+  /**
+   * @param name the attribute name
+   * @return the attribute by name converted to an URI, or the empty URI if the
+   *    attribute couldn't be converted
+   */
+  public Uri getUriAttribute(String name) {
+    String uriAttribute = getAttribute(name);
+    if (uriAttribute != null) {
+      try {
+        Uri uri = Uri.parse(uriAttribute);
+        return base.resolve(uri);
+      } catch (IllegalArgumentException e) {
+        return EMPTY_URI;
+      }
+    }
+    return EMPTY_URI;
+  }
+
+  /**
+   * @param name the attribute name
+   * @return the attribute by name converted to a boolean (false if the
+   *     attribute doesn't exist)
+   */
+  public boolean getBoolAttribute(String name) {
+    String value = getAttribute(name);
+    return Boolean.parseBoolean(value);
+  }
+
+  /**
+   * @param name the attribute name
+   * @return the attribute by name converted to an integer, or 0 if the
+   *     attribute doesn't exist or is not a valid number.
+   */
+  public int getIntAttribute(String name) {
+    String value = getAttribute(name);
+    if (value == null) {
+      return 0;
+    } else {
+      try {
+        return Integer.parseInt(value);
+      } catch (NumberFormatException e) {
+        return 0;
+      }
+    }
+  }
+
+
+  private final List<String> categories;
+  private List<Feature> allFeatures;
+  private Map<String, Feature> features;
+  private Map<String, Feature> globalFeatures;
+  private List<Preload> preloads;
+  private List<Icon> icons;
+  private Map<String, Map<Locale, LocaleSpec>>  locales;
+  private Map<Locale, LocaleSpec> allLocales;
+  private Map<String, LinkSpec> links;
+  private OAuthSpec oauth;
+  private Multimap<String,Node> extraElements;
+  private OAuth2Spec oauth2;
+
+
+  /**
+   * @return Returns a list of flattened attributes for:
+   * ModuleSpec@category
+   * ModuleSpec@category2
+   */
+  public List<String> getCategories() {
+    return categories;
+  }
+
+  /**
+   * All features are included in ModulePrefs.
+   * View level features have view qualifiers appended.
+   * @return a map of ModulePrefs/Require and ModulePrefs/Optional elements to Feature
+   */
+  public Map<String, Feature> getFeatures() {
+    return features;
+  }
+
+
+  /**
+   * All features elements defined in ModulePrefs
+   * @return a list of all Features included in ModulePrefs
+   */
+  public List<Feature> getAllFeatures() {
+    return allFeatures;
+  }
+
+  /**
+   * Returns Map of features to load for the given View
+   * @return a map of ModuleSpec/Require and ModuleSpec/Optional elements to Feature
+   */
+  public Map<String, Feature> getViewFeatures(String view) {
+    Map<String, Feature> map = Maps.newHashMap();
+    // Global features are in all views..
+    map.putAll(globalFeatures);
+    // By adding view level features last so they can override global feature configurations
+    for (Feature feature : features.values()) {
+      if (feature.getViews().contains(view)) {
+        map.put(feature.getName(), feature);
+      }
+    }
+    return map;
+  }
+
+  /**
+   * @return a list of Preloads from the ModuleSpec/Preload element
+   */
+  public List<Preload> getPreloads() {
+    return preloads;
+  }
+
+  /**
+   * @return a list of Icons from the ModuleSpec/Icon element
+   */
+  public List<Icon> getIcons() {
+    return icons;
+  }
+
+  /**
+   * @return a Map of Locales to LocalSpec from the ModuleSpec/Locale element
+   */
+  public Map<Locale, LocaleSpec> getLocales() {
+    return allLocales;
+  }
+
+  /**
+   * @return a map of Link names to LinkSpec from the ModuleSpec/Link element
+   */
+  public Map<String, LinkSpec> getLinks() {
+    return links;
+  }
+
+  /**
+   * @return an OAuthSpec built from the ModuleSpec/OAuthSpec element
+   */
+  public OAuthSpec getOAuthSpec() {
+    return oauth;
+  }
+
+  /**
+   * @return an OAuth2Spec built from the ModuleSpec/OAuthSpec element
+   */
+  public OAuth2Spec getOAuth2Spec() {
+    return oauth2;
+  }
+
+  /**
+   * @return a Multimap of tagnames to child elements of the ModuleSpec element
+   */
+  public Multimap<String,Node> getExtraElements() {
+    return extraElements;
+  }
+
+  /**
+   * Note: not part of the spec.
+   *
+   * @return true when UserPref-substitutable fields in this prefs require __UP_ substitution.
+   */
+  public boolean needsUserPrefSubstitution() {
+    return needsUserPrefSubstitution;
+  }
+
+  /**
+   * Gets the global locale spec for the given locale, if any exists.
+   *
+   * @return The locale spec, if there is a matching one, or null.
+   */
+  public LocaleSpec getGlobalLocale(Locale locale) {
+    return getLocale(locale, GLOBAL_LOCALE);
+  }
+
+  /**
+   * Gets the locale spec for the given locale and view, if any exists.
+   *
+   * @return The locale spec, if there is a matching one, or null.
+   */
+  public LocaleSpec getLocale(Locale locale, String view) {
+    if (view == null) {
+      view = GLOBAL_LOCALE;
+    }
+    Map<Locale, LocaleSpec> viewLocales = locales.get(view);
+    LocaleSpec locSpec = null;
+    if (viewLocales != null) {
+      locSpec = viewLocales.get(locale); // Check view specific locale...
+    }
+    if (locSpec == null && !view.equals(GLOBAL_LOCALE)) { // If not there, check Global map
+      locSpec = getGlobalLocale(locale);
+    }
+    return locSpec;
+  }
+
+  /**
+   * Produces a new ModulePrefs by substituting hangman variables from
+   * substituter. See comments on individual fields to see what actually
+   * has substitutions performed.
+   *
+   * @param substituter the substituter to execute
+   * @return a substituted ModulePrefs
+   */
+  public ModulePrefs substitute(Substitutions substituter) {
+    return new ModulePrefs(this, substituter);
+  }
+
+
+  /**
+   * Walks child nodes of the given node.
+   * @param element root node to be applied
+   * @param visitors Set of visitors to apply to children of element.
+   * @throws SpecParserException when encountering bad input
+   */
+  private static void walk(Element element, Set<ElementVisitor> visitors)
+      throws SpecParserException {
+    NodeList children = element.getChildNodes();
+    for (int i = 0, j = children.getLength(); i < j; ++i) {
+      Node child = children.item(i);
+      String tagName = child.getNodeName();
+
+      if (!(child instanceof Element)) continue;
+
+      // Try our visitors in order until we find a match
+      for (ElementVisitor ev : visitors) {
+        if (ev.visit(tagName, (Element)child))
+          break;
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<ModulePrefs");
+
+    for (Map.Entry<String, String> attr : attributes.entrySet()) {
+      buf.append(' ').append(attr.getKey()).append("=\"")
+         .append(attr.getValue()).append('\"');
+    }
+    buf.append(">\n");
+
+    Joiner j = Joiner.on("\n");
+
+    j.appendTo(buf, preloads);
+    j.appendTo(buf, features.values());
+    j.appendTo(buf, icons);
+    j.appendTo(buf, locales.values());
+    j.appendTo(buf, links.values());
+
+    if (oauth != null) {
+      buf.append(oauth).append('\n');
+    }
+
+    if (extraElements != null) {
+      for (Node node : extraElements.values()) {
+        Source source = new DOMSource(node);
+        StringWriter sw = new StringWriter();
+        Result result = new StreamResult(sw);
+        try {
+          Transformer xformer = TransformerFactory.newInstance().newTransformer();
+          xformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+          xformer.transform(source, result);
+        } catch (TransformerConfigurationException e) {
+          // ignore
+        } catch (TransformerException e) {
+          // ignore
+        }
+        buf.append(sw.toString());
+      }
+    }
+    buf.append("</ModulePrefs>");
+    return buf.toString();
+  }
+
+  /**
+   * @param prefs ModulePrefs object
+   * @return true if any UserPref-substitutable fields in the given
+   * {@code prefs} require such substitution.
+   */
+  static boolean prefsNeedsUserPrefSubstitution(ModulePrefs prefs) {
+    for (Preload preload : prefs.preloads) {
+      if (preload.getHref().toString().contains(UP_SUBST_PREFIX)) {
+        return true;
+      }
+    }
+    return prefs.getTitle().contains(UP_SUBST_PREFIX) ||
+           prefs.getTitleUrl().toString().contains(UP_SUBST_PREFIX);
+  }
+
+  /**
+   * Interface used for parsing specific chunks of the gadget spec
+   */
+  interface ElementVisitor {
+    /**
+     * Called on each node that matches
+     *
+     * @param tag the name of the tag being parsed
+     * @param element the element to parse
+     * @return true if we handled the tag, false if not
+     * @throws SpecParserException when parsing issues are present
+     */
+    boolean visit(String tag, Element element) throws SpecParserException;
+
+    /**
+     * Called when all elements have been processed.  Any data that is set on the ModulePrefs instance should be
+     * Immutable
+     *
+     * @param moduleprefs The moduleprefs object to mutate
+     */
+    void apply(ModulePrefs moduleprefs);
+  }
+
+  /**
+   * Processes ModulePrefs/Preload into a list.
+   */
+  private class PreloadVisitor implements ElementVisitor {
+    private final List<Preload> preloaded = Lists.newLinkedList();
+
+    public boolean visit(String tag,Element element) throws SpecParserException {
+      if (!"Preload".equals(tag)) return false;
+
+      Preload preload = new Preload(element, base);
+      preloaded.add(preload);
+      return true;
+    }
+
+    public void apply(ModulePrefs moduleprefs) {
+      moduleprefs.preloads = ImmutableList.copyOf(preloaded);
+    }
+  }
+
+  /**
+   * Process ModulePrefs/OAuth
+   */
+  private final class OAuthVisitor implements ElementVisitor {
+    private OAuthSpec oauthSpec = null;
+    private final MutableBoolean oauthMarker;
+
+    private OAuthVisitor(MutableBoolean oauthMarker) {
+      this.oauthMarker = oauthMarker;
+    }
+
+    public boolean visit(String tag, Element element) throws SpecParserException {
+      if (!"OAuth".equals(tag)) return false;
+
+      if (oauthSpec != null) {
+        throw new SpecParserException("ModulePrefs/OAuth may only occur once.");
+      }
+      oauthSpec = new OAuthSpec(element, base);
+      oauthMarker.setValue(true);
+      return true;
+    }
+
+    public void apply(ModulePrefs moduleprefs) {
+      moduleprefs.oauth = oauthSpec;
+    }
+
+  }
+
+  /**
+   * Process ModulePrefs/OAuth2
+   */
+  private final class OAuth2Visitor implements ElementVisitor {
+    private OAuth2Spec oauth2Spec = null;
+    private final MutableBoolean oauth2Marker;
+
+    private OAuth2Visitor(MutableBoolean oauth2Marker) {
+      this.oauth2Marker = oauth2Marker;
+    }
+
+    public boolean visit(String tag, Element element) throws SpecParserException {
+      if (!"OAuth2".equals(tag)) return false;
+
+      if (oauth2Spec != null) {
+        throw new SpecParserException("ModulePrefs/OAuth2 may only occur once.");
+      }
+      oauth2Spec = new OAuth2Spec(element, base);
+      oauth2Marker.setValue(true);
+      return true;
+    }
+
+    public void apply(ModulePrefs moduleprefs) {
+      moduleprefs.oauth2 = oauth2Spec;
+    }
+
+  }
+
+  /**
+   * Processes ModulePrefs/Require and ModulePrefs/Optional
+   */
+  private static final class FeatureVisitor implements ElementVisitor {
+    private final Map<String, Feature> features = Maps.newHashMap();
+    private final Map<String, Feature> globalFeatures = Maps.newHashMap();
+    private final MutableBoolean oauthMarker;
+    private boolean coreIncluded = false;
+
+    private static final Set<String> TAGS = ImmutableSet.of("Require", "Optional");
+
+    private FeatureVisitor(MutableBoolean oauthMarker) {
+      this.oauthMarker = oauthMarker;
+    }
+
+    public boolean visit(String tag, Element element)
+        throws SpecParserException {
+      if (!TAGS.contains(tag))
+        return false;
+
+      Feature feature = new Feature(element);
+      if (feature.getViews().isEmpty()) {
+        coreIncluded = coreIncluded || feature.getName().startsWith("core");
+        features.put(feature.getName(), feature);
+        globalFeatures.put(feature.getName(), feature);
+      } else {
+        // We are going to include Core feature globally, so skip it if it was
+        // included for any Views
+        if (!feature.getName().startsWith("core")) {
+          // Key view level features by qualifying with the view ID
+          for (String view : feature.getViews()) {
+            StringBuilder buff = new StringBuilder(feature.getName());
+            buff.append('.');
+            buff.append(view);
+            features.put(buff.toString(), feature);
+          }
+        }
+      }
+      return true;
+    }
+
+    public void apply(ModulePrefs moduleprefs) {
+      if (!coreIncluded) {
+        // No library was explicitly included from core - add it as an implicit dependency.
+        features.put(Feature.CORE_FEATURE.getName(), Feature.CORE_FEATURE);
+        globalFeatures.put(Feature.CORE_FEATURE.getName(), Feature.CORE_FEATURE);
+      }
+      if (oauthMarker.booleanValue()) {
+        // <OAuth>/<OAuth2> tag found: security token needed.
+        features.put(Feature.SECURITY_TOKEN_FEATURE.getName(), Feature.SECURITY_TOKEN_FEATURE);
+        globalFeatures.put(Feature.SECURITY_TOKEN_FEATURE.getName(), Feature.SECURITY_TOKEN_FEATURE);
+      }
+      moduleprefs.features = ImmutableMap.copyOf(features);
+      moduleprefs.globalFeatures = ImmutableMap.copyOf(globalFeatures);
+      moduleprefs.allFeatures = ImmutableList.copyOf(features.values());
+    }
+  }
+
+  /**
+   * Processes ModulePrefs/Icon
+   */
+  private static class IconVisitor implements ElementVisitor {
+    private final List<Icon> icons = Lists.newLinkedList();
+
+    public boolean visit(String tag, Element element) throws SpecParserException {
+      if (!"Icon".equals(tag)) return false;
+
+      icons.add(new Icon(element));
+      return true;
+    }
+    public void apply(ModulePrefs moduleprefs) {
+      moduleprefs.icons = ImmutableList.copyOf(icons);
+    }
+  }
+
+  /**
+   * Process ModulePrefs/Locale
+   */
+  private class LocaleVisitor implements ElementVisitor {
+
+    private Map<String, Map<Locale, LocaleSpec>> locales = Maps.newHashMap();
+
+    public boolean visit(String tag, Element element)
+        throws SpecParserException {
+      if (!"Locale".equals(tag))
+        return false;
+      LocaleSpec locale = new LocaleSpec(element, base);
+      if (locale.getViews().isEmpty()) {
+        storeLocaleSpec(GLOBAL_LOCALE, locale);
+      } else {
+        // We've got a view level Locale, need to store the mapping of Views to
+        // the appropriate LocaleSpecs
+        for (String view : locale.getViews()) {
+          storeLocaleSpec(view,locale);
+        }
+      }
+      return true;
+    }
+
+    public void apply(ModulePrefs moduleprefs) {
+      Map<Locale, LocaleSpec> allLocales = Maps.newHashMap();
+      moduleprefs.locales = locales;
+      for(Map<Locale, LocaleSpec> map : locales.values()){
+        allLocales.putAll(map);
+      }
+      moduleprefs.allLocales = ImmutableMap.copyOf(allLocales);
+    }
+
+    private void storeLocaleSpec(String view, LocaleSpec locale){
+      Map<Locale, LocaleSpec> viewLocaleSpecs;
+      if (locales.get(view) == null) {
+        viewLocaleSpecs = Maps.newHashMap();
+        locales.put(view, viewLocaleSpecs);
+      } else {
+        viewLocaleSpecs = locales.get(view);
+      }
+      viewLocaleSpecs.put(new Locale(locale.getLanguage(), locale.getCountry()), locale);
+    }
+
+  }
+
+  /**
+   * Process ModulePrefs/Link
+   */
+  private class LinkVisitor implements ElementVisitor {
+    private final Map<String, LinkSpec> linkMap = Maps.newHashMap();
+
+    public boolean visit(String tag, Element element) throws SpecParserException {
+      if (!"Link".equals(tag)) return false;
+      LinkSpec link = new LinkSpec(element, base);
+      linkMap.put(link.getRel(), link);
+      return true;
+    }
+
+    public void apply(ModulePrefs moduleprefs) {
+      moduleprefs.links = ImmutableMap.copyOf(linkMap);
+    }
+  }
+
+  private static class ExtraElementsVisitor implements ElementVisitor {
+    private Multimap<String,Node> elements = ArrayListMultimap.create();
+
+    public boolean visit(String tag, Element element) throws SpecParserException {
+      elements.put(tag, element.cloneNode(true));
+      return true;
+    }
+    public void apply(ModulePrefs moduleprefs) {
+      moduleprefs.extraElements = ImmutableMultimap.copyOf(elements);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuth2Service.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuth2Service.java
new file mode 100644
index 0000000..f972c87
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuth2Service.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.uri.Uri;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Information about an OAuth2 service that a gadget wants to use.
+ *
+ * Instances are immutable.
+ */
+public class OAuth2Service extends BaseOAuthService {
+  private EndPoint authorizationUrl;
+  private EndPoint tokenUrl;
+  private String name;
+  private String scope;
+
+  /**
+   * Constructor for testing only.
+   */
+  OAuth2Service() { }
+
+  public OAuth2Service(Element serviceElement, Uri base) throws SpecParserException {
+    name = serviceElement.getAttribute("name");
+    scope = serviceElement.getAttribute("scope");
+    NodeList children = serviceElement.getChildNodes();
+    for (int i=0; i < children.getLength(); ++i) {
+      Node child = children.item(i);
+      if (child.getNodeType() != Node.ELEMENT_NODE) {
+        continue;
+      }
+      String childName = child.getNodeName();
+      if ("Authorization".equals(childName)) {
+        if (authorizationUrl != null) {
+          throw new SpecParserException("Multiple OAuth2/Service/Authorization elements");
+        }
+        authorizationUrl = parseEndPoint("OAuth2/Service/Authorization", (Element)child, base);
+      } else if ("Token".equals(childName)) {
+        if (tokenUrl != null) {
+          throw new SpecParserException("Multiple OAuth2/Service/Token elements");
+        }
+        tokenUrl = parseEndPoint("OAuth2/Service/Token", (Element)child, base);
+      }
+    }
+  }
+
+  /**
+   * Represents /OAuth2/Service/Authorization elements.
+   */
+  public EndPoint getAuthorizationUrl() {
+    return authorizationUrl;
+  }
+
+  /**
+   * Represents /OAuth2/Service/Token elements.
+   */
+  public EndPoint getTokenUrl() {
+    return tokenUrl;
+  }
+
+
+  /**
+   * Represents /OAuth2/Service@scope
+   */
+  public String getScope() {
+    return scope;
+  }
+
+  /**
+   * Represents /OAuth/Service@name
+   */
+  public String getName() {
+    return name;
+  }
+
+
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuth2Spec.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuth2Spec.java
new file mode 100644
index 0000000..7eb710f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuth2Spec.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Wraps an &lt;OAuth2&gt; element from the gadget spec.
+ *
+ * Instances are immutable.
+ */
+public class OAuth2Spec {
+
+  /** Keys are service names, values are service descriptors */
+  private final Map<String, OAuth2Service> serviceMap;
+
+  /**
+   * @param element the OAuth2 spec element
+   * @param base  The uri that the spec is loaded from. messages is assumed
+   *     to be relative to this path.
+   * @throws SpecParserException
+   */
+  public OAuth2Spec(Element element, Uri base) throws SpecParserException {
+    serviceMap = Maps.newHashMap();
+    NodeList services = element.getElementsByTagName("Service");
+    for (int i=0; i < services.getLength(); ++i) {
+      Node node = services.item(i);
+      if (node.getNodeType() == Node.ELEMENT_NODE) {
+        parseService((Element)node, base);
+      }
+    }
+  }
+
+  private void parseService(Element serviceElement, Uri base) throws SpecParserException {
+    OAuth2Service service = new OAuth2Service(serviceElement, base);
+    serviceMap.put(service.getName(), service);
+  }
+
+  public Map<String, OAuth2Service> getServices() {
+    return Collections.unmodifiableMap(serviceMap);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("<OAuth2>");
+    for (Map.Entry<String, OAuth2Service> entry : serviceMap.entrySet()) {
+      sb.append("<Service name='");
+      sb.append(entry.getKey());
+      sb.append("'>");
+      OAuth2Service service = entry.getValue();
+      if (service.getAuthorizationUrl() != null) {
+        sb.append(service.getAuthorizationUrl().toString("Authorization"));
+      }
+      if (service.getTokenUrl() != null) {
+        sb.append(service.getTokenUrl().toString("Token"));
+      }
+      sb.append("</Service>");
+    }
+    sb.append("</OAuth2>");
+    return sb.toString();
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuthService.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuthService.java
new file mode 100644
index 0000000..e99cbf9
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuthService.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.uri.Uri;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Information about an OAuth service that a gadget wants to use.
+ *
+ * Instances are immutable.
+ */
+public class OAuthService extends BaseOAuthService {
+  private EndPoint requestUrl;
+  private EndPoint accessUrl;
+  private Uri authorizationUrl;
+  private String name;
+
+  /**
+   * Constructor for testing only.
+   */
+  OAuthService() {}
+
+  public OAuthService(Element serviceElement, Uri base) throws SpecParserException {
+    name = serviceElement.getAttribute("name");
+    NodeList children = serviceElement.getChildNodes();
+    for (int i = 0; i < children.getLength(); ++i) {
+      Node child = children.item(i);
+      if (child.getNodeType() != Node.ELEMENT_NODE) {
+        continue;
+      }
+      String childName = child.getNodeName();
+      if ("Request".equals(childName)) {
+        if (requestUrl != null) {
+          throw new SpecParserException("Multiple OAuth/Service/Request elements");
+        }
+        requestUrl = parseEndPoint("OAuth/Service/Request", (Element) child, base);
+      } else if ("Authorization".equals(childName)) {
+        if (authorizationUrl != null) {
+          throw new SpecParserException("Multiple OAuth/Service/Authorization elements");
+        }
+        authorizationUrl = parseAuthorizationUrl((Element) child, base);
+      } else if ("Access".equals(childName)) {
+        if (accessUrl != null) {
+          throw new SpecParserException("Multiple OAuth/Service/Access elements");
+        }
+        accessUrl = parseEndPoint("OAuth/Service/Access", (Element) child, base);
+      }
+    }
+    if (requestUrl == null) {
+      throw new SpecParserException("/OAuth/Service/Request is required");
+    }
+    if (accessUrl == null) {
+      throw new SpecParserException("/OAuth/Service/Access is required");
+    }
+    if (authorizationUrl == null) {
+      throw new SpecParserException("/OAuth/Service/Authorization is required");
+    }
+    if (requestUrl.location != accessUrl.location) {
+      throw new SpecParserException("Access@location must be identical to Request@location");
+    }
+    if (requestUrl.method != accessUrl.method) {
+      throw new SpecParserException("Access@method must be identical to Request@method");
+    }
+    if (requestUrl.location == Location.BODY && requestUrl.method == Method.GET) {
+      throw new SpecParserException("Incompatible parameter location, cannot"
+              + "use post-body with GET requests");
+    }
+  }
+
+  /**
+   * Represents /OAuth/Service/Request elements.
+   */
+  public EndPoint getRequestUrl() {
+    return requestUrl;
+  }
+
+  /**
+   * Represents /OAuth/Service/Access elements.
+   */
+  public EndPoint getAccessUrl() {
+    return accessUrl;
+  }
+
+  /**
+   * Represents /OAuth/Service/Authorization elements.
+   */
+  public Uri getAuthorizationUrl() {
+    return authorizationUrl;
+  }
+
+  /**
+   * Represents /OAuth/Service@name
+   */
+  public String getName() {
+    return name;
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuthSpec.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuthSpec.java
new file mode 100644
index 0000000..48ce55a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/OAuthSpec.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Wraps an &lt;OAuth&gt; element from the gadget spec.
+ *
+ * Instances are immutable.
+ */
+public class OAuthSpec {
+
+  /** Keys are service names, values are service descriptors */
+  private final Map<String, OAuthService> serviceMap;
+
+  public OAuthSpec(Element element, Uri base) throws SpecParserException {
+    serviceMap = Maps.newHashMap();
+    NodeList services = element.getElementsByTagName("Service");
+    for (int i=0; i < services.getLength(); ++i) {
+      Node node = services.item(i);
+      if (node.getNodeType() == Node.ELEMENT_NODE) {
+        parseService((Element)node, base);
+      }
+    }
+  }
+
+  private void parseService(Element serviceElement, Uri base) throws SpecParserException {
+    OAuthService service = new OAuthService(serviceElement, base);
+    serviceMap.put(service.getName(), service);
+  }
+
+  public Map<String, OAuthService> getServices() {
+    return Collections.unmodifiableMap(serviceMap);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append("<OAuth>");
+    for (Map.Entry<String, OAuthService> entry : serviceMap.entrySet()) {
+      sb.append("<Service name='");
+      sb.append(entry.getKey());
+      sb.append("'>");
+      OAuthService service = entry.getValue();
+      sb.append(service.getRequestUrl().toString("Request"));
+      sb.append(service.getAccessUrl().toString("Access"));
+      sb.append("<Authorization url='").append(service.getAuthorizationUrl().toString()).append("'/>");
+      sb.append("</Service>");
+    }
+    sb.append("</OAuth>");
+    return sb.toString();
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java
new file mode 100644
index 0000000..4c34a69
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/PipelinedData.java
@@ -0,0 +1,658 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.PropertyNotFoundException;
+import javax.el.ValueExpression;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * Parsing code for &lt;os:*&gt; elements.
+ */
+public class PipelinedData {
+  private boolean needsViewer;
+  private boolean needsOwner;
+  private Map<String, BatchItemData> allPreloads;
+
+  public static final String OPENSOCIAL_NAMESPACE = "http://ns.opensocial.org/2008/markup";
+  public static final String EXTENSION_NAMESPACE = "http://ns.opensocial.org/2009/extensions";
+
+  public PipelinedData(Element element, Uri base) throws SpecParserException {
+    Map<String, BatchItemData> allPreloads = Maps.newHashMap();
+
+    // TODO: extract this loop into XmlUtils.getChildrenWithNamespace
+    for (Node node = element.getFirstChild(); node != null; node = node.getNextSibling()) {
+      if (!(node instanceof Element)) {
+        continue;
+      }
+
+      Element child = (Element) node;
+
+      if (EXTENSION_NAMESPACE.equals(child.getNamespaceURI())) {
+        if ("Variable".equals(child.getLocalName())) {
+          allPreloads.put(child.getAttribute("key"), createVariableRequest(child));
+        }
+
+      } else if (OPENSOCIAL_NAMESPACE.equals(child.getNamespaceURI())) {
+        String elementName = child.getLocalName();
+
+        String key = child.getAttribute("key");
+        if (key == null) {
+          throw new SpecParserException("Missing key attribute on os:" + elementName);
+        }
+
+        try {
+          if ("PeopleRequest".equals(elementName)) {
+            allPreloads.put(key, createPeopleRequest(child));
+          } else if ("ViewerRequest".equals(elementName)) {
+            allPreloads.put(key, createViewerRequest(child));
+          } else if ("OwnerRequest".equals(elementName)) {
+            allPreloads.put(key, createOwnerRequest(child));
+          } else if ("PersonAppDataRequest".equals(elementName)) {
+            // TODO: delete when 0.9 app data retrieval is supported
+            allPreloads.put(key, createPersonAppDataRequest(child));
+          } else if ("ActivitiesRequest".equals(elementName)) {
+            allPreloads.put(key, createActivityRequest(child));
+          } else if ("ActivityStreamsRequest".equals(elementName)) {
+            allPreloads.put(key, createActivityStreamRequest(child));
+          } else if ("DataRequest".equals(elementName)) {
+            allPreloads.put(key, createDataRequest(child));
+          } else if ("HttpRequest".equals(elementName)) {
+            allPreloads.put(key, createHttpRequest(child, base));
+          } else {
+            // TODO: This is wrong - the spec should parse, but should preload
+            // notImplemented
+            throw new SpecParserException("Unknown element <os:" + elementName + '>');
+          }
+        } catch (ELException ele) {
+          throw new SpecParserException(new XmlException(ele));
+        }
+      }
+    }
+
+    this.allPreloads = Collections.unmodifiableMap(allPreloads);
+  }
+
+  private BatchItemData createVariableRequest(Element child) {
+    return new VariableData(child.getAttribute("value"));
+  }
+
+  private PipelinedData(PipelinedData socialData, Substitutions substituter) {
+    Map<String, BatchItemData> allPreloads = Maps.newHashMap();
+    for (Map.Entry<String, BatchItemData> preload : socialData.allPreloads.entrySet()) {
+      allPreloads.put(preload.getKey(), preload.getValue().substitute(substituter));
+    }
+
+    this.allPreloads = Collections.unmodifiableMap(allPreloads);
+  }
+
+  /**
+   * Allows the creation of a view from an existing view so that localization
+   * can be performed.
+   */
+  public PipelinedData substitute(Substitutions substituter) {
+    return new PipelinedData(this, substituter);
+  }
+
+  public interface Batch {
+    Map<String, BatchItem> getPreloads();
+    Batch getNextBatch(ELResolver rootObjects);
+  }
+
+  /** Temporary type until BatchItem is made fully polymorphic */
+  public enum BatchType {
+    SOCIAL,
+    HTTP,
+    VARIABLE
+  }
+
+  /** Item within a batch */
+  public interface BatchItem {
+    BatchType getType();
+    Object getData();
+  }
+
+  /** Shared data used to generate BatchItems */
+  interface BatchItemData {
+    BatchItem evaluate(Expressions expressions, ELContext elContext);
+    BatchItemData substitute(Substitutions substituter);
+  }
+
+  /**
+   * Gets the first batch of preload requests.  Preloads that require root
+   * objects not yet available will not be executed in this batch, but may
+   * become available in subsequent batches.
+   *
+   * @param rootObjects an ELResolver that can evaluate currently available
+   *     root objects.
+   * @see org.apache.shindig.gadgets.GadgetELResolver
+   * @return a batch, or null if no batch could be created
+   */
+  public Batch getBatch(Expressions expressions, ELResolver rootObjects) {
+    return getBatch(expressions, rootObjects, allPreloads);
+  }
+
+  /**
+   * Create a Batch of preload requests
+   * @param expressions expressions instance for parsing expressions
+   * @param rootObjects an ELResolver that can evaluate currently available
+   *     root objects.
+   * @param currentPreloads the remaining social/http preloads
+   */
+  private Batch getBatch(Expressions expressions, ELResolver rootObjects,
+      Map<String, BatchItemData> currentPreloads) {
+    ELContext elContext = expressions.newELContext(rootObjects);
+
+    Map<String, BatchItem> evaluatedPreloads = Maps.newHashMap();
+    Map<String, BatchItemData> pendingPreloads = null;
+
+    if (currentPreloads != null) {
+      for (Map.Entry<String, BatchItemData> preload : currentPreloads.entrySet()) {
+        try {
+          BatchItem value = preload.getValue().evaluate(expressions, elContext);
+          evaluatedPreloads.put(preload.getKey(), value);
+        } catch (PropertyNotFoundException pe) {
+          // Property-not-found: presume that this is because a top-level
+          // variable isn't available yet, which means that this needs to be
+          // postponed to the next batch.
+          if (pendingPreloads == null) {
+            pendingPreloads = Maps.newHashMap();
+          }
+
+          pendingPreloads.put(preload.getKey(), preload.getValue());
+        } catch (ELException e) {
+          // TODO: Handle!?!
+          throw new RuntimeException(e);
+        }
+      }
+    }
+
+    // Nothing evaluated or pending;  return null for the batch.  Note that
+    // there may be multiple PipelinedData objects (e.g., from multiple
+    // <script type="text/os-data"> elements), so even if all evaluations
+    // fail here, evaluations might succeed elsewhere and free up pending preloads
+    if (evaluatedPreloads.isEmpty() && pendingPreloads == null) {
+      return null;
+    }
+
+    return new BatchImpl(expressions, evaluatedPreloads, pendingPreloads);
+  }
+
+  /** Batch implementation */
+  class BatchImpl implements Batch {
+
+    private final Expressions expressions;
+    private final Map<String, BatchItem> evaluatedPreloads;
+    private final Map<String, BatchItemData> pendingPreloads;
+
+    public BatchImpl(Expressions expressions, Map<String, BatchItem> evaluatedPreloads,
+        Map<String, BatchItemData> pendingPreloads) {
+          this.expressions = expressions;
+          this.evaluatedPreloads = evaluatedPreloads;
+          this.pendingPreloads = pendingPreloads;
+    }
+
+    public Batch getNextBatch(ELResolver rootObjects) {
+      return getBatch(expressions, rootObjects, pendingPreloads);
+    }
+
+    public Map<String, BatchItem> getPreloads() {
+      return evaluatedPreloads;
+    }
+  }
+
+  public boolean needsViewer() {
+    return needsViewer;
+  }
+
+  public boolean needsOwner() {
+    return needsOwner;
+  }
+
+  /** Handle the os:PeopleRequest element */
+  private SocialData createPeopleRequest(Element child) throws ELException {
+    SocialData expression = new SocialData(child.getAttribute("key"), "people.get");
+
+    copyAttribute("groupId", child, expression, String.class);
+    copyAttribute("userId", child, expression, JSONArray.class);
+    updateUserArrayState("userId", child);
+    copyAttribute("personId", child, expression, JSONArray.class);
+    updateUserArrayState("personId", child);
+
+    copyAttribute("startIndex", child, expression, Integer.class);
+    copyAttribute("count", child, expression, Integer.class);
+    copyAttribute("sortBy", child, expression, String.class);
+    copyAttribute("sortOrder", child, expression, String.class);
+    copyAttribute("filterBy", child, expression, String.class);
+    copyAttribute("filterOperation", child, expression, String.class);
+    copyAttribute("filterValue", child, expression, String.class);
+    copyAttribute("fields", child, expression, JSONArray.class);
+
+    return expression;
+  }
+
+  /** Handle the os:ViewerRequest element */
+  private SocialData createViewerRequest(Element child) throws ELException {
+    return createPersonRequest(child, "@viewer");
+  }
+
+  /** Handle the os:OwnerRequest element */
+  private SocialData createOwnerRequest(Element child) throws ELException {
+    return createPersonRequest(child, "@owner");
+  }
+
+  private SocialData createPersonRequest(Element child, String userId) throws ELException {
+    SocialData expression = new SocialData(child.getAttribute("key"), "people.get");
+
+    expression.addProperty("userId", userId, JSONArray.class);
+    updateUserState(userId);
+    copyAttribute("fields", child, expression, JSONArray.class);
+
+    return expression;
+  }
+
+  /** Handle the os:PersonAppDataRequest element */
+  private SocialData createPersonAppDataRequest(Element child) throws ELException {
+    SocialData expression = new SocialData(child.getAttribute("key"), "appdata.get");
+
+    copyAttribute("groupId", child, expression, String.class);
+    copyAttribute("userId", child, expression, JSONArray.class);
+    updateUserArrayState("userId", child);
+    copyAttribute("appId", child, expression, String.class);
+    copyAttribute("fields", child, expression, JSONArray.class);
+
+    return expression;
+  }
+
+  /** Handle the os:ActivitiesRequest element */
+  private SocialData createActivityRequest(Element child) throws ELException {
+    SocialData expression = new SocialData(child.getAttribute("key"), "activities.get");
+
+    copyAttribute("groupId", child, expression, String.class);
+    copyAttribute("userId", child, expression, JSONArray.class);
+    updateUserArrayState("userId", child);
+    copyAttribute("appId", child, expression, String.class);
+    // TODO: SHINDIG-711 should be activityIds?
+    copyAttribute("activityId", child, expression, JSONArray.class);
+    copyAttribute("fields", child, expression, JSONArray.class);
+    copyAttribute("startIndex", child, expression, Integer.class);
+    copyAttribute("count", child, expression, Integer.class);
+
+    // TODO: add activity paging support
+
+    return expression;
+  }
+
+  /** Handle the os:ActivityStreamsRequest element */
+  private SocialData createActivityStreamRequest(Element child) throws ELException {
+    SocialData expression = new SocialData(child.getAttribute("key"), "activitystreams.get");
+
+    copyAttribute("groupId", child, expression, String.class);
+    copyAttribute("userId", child, expression, JSONArray.class);
+    updateUserArrayState("userId", child);
+    copyAttribute("appId", child, expression, String.class);
+    copyAttribute("activityEntryId", child, expression, JSONArray.class);
+    copyAttribute("fields", child, expression, JSONArray.class);
+    copyAttribute("startIndex", child, expression, Integer.class);
+    copyAttribute("count", child, expression, Integer.class);
+
+    // TODO: add activity paging support
+    return expression;
+  }
+
+  /** Handle the os:DataRequest element */
+  private SocialData createDataRequest(Element child) throws ELException, SpecParserException {
+    String method = child.getAttribute("method");
+    if (method == null) {
+      throw new SpecParserException("Missing @method attribute on os:DataRequest");
+    }
+
+    // TODO: should we support anything that doesn't end in .get?
+    // i.e, should this be a whitelist not a blacklist?
+    if (method.endsWith(".update")
+        || method.endsWith(".create")
+        || method.endsWith(".delete")) {
+      throw new SpecParserException("Unsupported @method attribute \"" + method + "\" on os:DataRequest");
+    }
+
+    SocialData expression = new SocialData(child.getAttribute("key"), method);
+    NamedNodeMap nodeMap = child.getAttributes();
+    for (int i = 0; i < nodeMap.getLength(); i++) {
+      Node attrNode = nodeMap.item(i);
+      // Skip namespaced attributes
+      if (attrNode.getNamespaceURI() != null) {
+        continue;
+      }
+
+      // Use getNodeName() instead of getLocalName().  NekoHTML has an incorrect
+      // implementation of node name that returns null.
+      String name = attrNode.getNodeName();
+      // Skip the built-in names
+      if ("method".equals(name) || "key".equals(name)) {
+        continue;
+      }
+
+      String value = attrNode.getNodeValue();
+      expression.addProperty(name, value, Object.class);
+    }
+
+    return expression;
+  }
+
+  /** Handle an os:HttpRequest element */
+  private HttpData createHttpRequest(Element child, Uri base) throws ELException {
+    HttpData data = new HttpData(child, base);
+    // Update needsOwner and needsViewer
+    if (data.authz != AuthType.NONE) {
+      if (data.signOwner) {
+        needsOwner = true;
+      }
+
+      if (data.signViewer) {
+        needsViewer = true;
+      }
+    }
+
+    return data;
+  }
+
+  private void copyAttribute(String name, Element element, SocialData expression, Class<?> type)
+    throws ELException {
+    if (element.hasAttribute(name)) {
+      expression.addProperty(name, element.getAttribute(name), type);
+    }
+  }
+
+  /** Look for @viewer, @owner within a userId attribute */
+  private void updateUserArrayState(String name, Element element) {
+    if (element.hasAttribute(name)) {
+      // TODO: check after Expression evaluation?
+      StringTokenizer tokens = new StringTokenizer(element.getAttribute(name), ",");
+      while (tokens.hasMoreTokens()) {
+        updateUserState(tokens.nextToken());
+      }
+    }
+  }
+
+  /** Updates whether this batch of SocialData needs owner or viewer data */
+  private void updateUserState(String userId) {
+    if ("@owner".equals(userId)) {
+      needsOwner = true;
+    } else if ("@viewer".equals(userId) || "@me".equals(userId)) {
+      needsViewer = true;
+    }
+  }
+
+  /**
+   * A single pipelined HTTP makerequest.
+   */
+  private static class HttpData implements BatchItemData {
+    private final AuthType authz;
+    private final Uri base;
+    private final String href;
+    private final boolean signOwner;
+    private final boolean signViewer;
+    private final Map<String, String> attributes;
+
+    private static final Set<String> KNOWN_ATTRIBUTES =
+          ImmutableSet.of("authz", "href", "sign_owner", "sign_viewer");
+
+    /**
+     * Create an HttpData off an <os:makeRequest> element.
+     */
+    public HttpData(Element element, Uri base) throws ELException {
+      this.base = base;
+
+      this.authz = element.hasAttribute("authz") ?
+          AuthType.parse(element.getAttribute("authz")) : AuthType.NONE;
+
+      // TODO: Spec question;  should EL values be URL escaped?
+      this.href = element.getAttribute("href");
+
+      // TODO: Spec question;  should sign_* default to true?
+      this.signOwner = booleanValue(element, "sign_owner", true);
+      this.signViewer = booleanValue(element, "sign_viewer", true);
+
+      // TODO: many of these attributes should not be EL enabled
+      ImmutableMap.Builder<String, String> attributes = ImmutableMap.builder();
+      for (int i = 0; i < element.getAttributes().getLength(); i++) {
+        Node attr = element.getAttributes().item(i);
+        if (!KNOWN_ATTRIBUTES.contains(attr.getNodeName())) {
+          attributes.put(attr.getNodeName(), attr.getNodeValue());
+        }
+      }
+
+      this.attributes = attributes.build();
+    }
+
+    private HttpData(HttpData data, Substitutions substituter) {
+      this.base = data.base;
+      this.authz = data.authz;
+      this.href = substituter.substituteString(data.href);
+      this.signOwner = data.signOwner;
+      this.signViewer = data.signViewer;
+      this.attributes = data.attributes;
+    }
+
+    /** Run substitutions over an HttpData */
+    public HttpData substitute(Substitutions substituter) {
+      return new HttpData(this, substituter);
+    }
+
+    /**
+     * Evaluate expressions and return a RequestAuthenticationInfo.
+     * @throws ELException if expression evaluation fails.
+     */
+    public BatchItem evaluate(Expressions expressions, ELContext context)
+        throws ELException {
+      String hrefString = String.valueOf(expressions.parse(href, String.class)
+          .getValue(context));
+      final Uri evaluatedHref;
+
+      try {
+        evaluatedHref = base.resolve(Uri.parse(hrefString));
+      } catch (IllegalArgumentException e) {
+        throw new ELException("bad Uri '" + hrefString + "' - " + e.getMessage(), e);
+      }
+
+      final Map<String, String> evaluatedAttributes = Maps.newHashMap();
+      for (Map.Entry<String, String> attr : attributes.entrySet()) {
+        ValueExpression expression = expressions.parse(attr.getValue(), String.class);
+        evaluatedAttributes.put(attr.getKey(),
+            String.valueOf(expression.getValue(context)));
+      }
+
+      final RequestAuthenticationInfo info = new RequestAuthenticationInfo() {
+        public Map<String, String> getAttributes() {
+          return evaluatedAttributes;
+        }
+
+        public AuthType getAuthType() {
+          return authz;
+        }
+
+        public Uri getHref() {
+          return evaluatedHref;
+        }
+
+        public boolean isSignOwner() {
+          return signOwner;
+        }
+
+        public boolean isSignViewer() {
+          return signViewer;
+        }
+      };
+
+      return new BatchItem() {
+        public Object getData() {
+          return info;
+        }
+
+        public BatchType getType() {
+          return BatchType.HTTP;
+        }
+      };
+    }
+
+    /** Parse a boolean expression off an XML attribute. */
+    private boolean booleanValue(Element element, String attrName,
+        boolean defaultValue) {
+      if (!element.hasAttribute(attrName)) {
+        return defaultValue;
+      }
+
+      return "true".equalsIgnoreCase(element.getAttribute(attrName));
+    }
+  }
+
+  /**
+   * A single social data request.
+   */
+  private static class SocialData implements BatchItemData {
+    private final List<Property> properties = Lists.newArrayList();
+    private final String id;
+    private final String method;
+
+    public SocialData(String id, String method) {
+      this.id = id;
+      this.method = method;
+    }
+
+    public void addProperty(String name, String value, Class<?> type) throws ELException {
+      properties.add(new Property(name, value, type));
+    }
+
+    /** Create the JSON request form for the social data */
+    private JSONObject toJson(Expressions expressions, ELContext elContext) throws ELException {
+      JSONObject object = new JSONObject();
+      try {
+        object.put("method", method);
+        object.put("id", id);
+
+        JSONObject params = new JSONObject();
+        for (Property property : properties) {
+          property.set(expressions, elContext, params);
+        }
+        object.put("params", params);
+      } catch (JSONException je) {
+        throw new ELException(je);
+      }
+
+      return object;
+    }
+
+    /** Single property for an expression */
+    private static class Property {
+      private final String name;
+      private final String value;
+      private final Class<?> type;
+
+      public Property(String name, String value, Class<?> type) {
+        this.name = name;
+        this.value = value;
+        this.type = type;
+      }
+
+      public void set(Expressions expressions, ELContext elContext, JSONObject object)
+          throws ELException {
+        ValueExpression expression = expressions.parse(value, type);
+        Object value = expression.getValue(elContext);
+        try {
+          if (value != null) {
+            object.put(name, value);
+          }
+        } catch (JSONException e) {
+          throw new ELException("Error parsing property \"" + name + '\"', e);
+        }
+      }
+    }
+
+    public BatchItem evaluate(Expressions expressions, ELContext elContext) throws ELException {
+      final JSONObject jsonResult = toJson(expressions, elContext);
+      return new BatchItem() {
+        public Object getData() {
+          return jsonResult;
+        }
+
+        public BatchType getType() {
+          return BatchType.SOCIAL;
+        }
+      };
+    }
+
+    public BatchItemData substitute(Substitutions substituter) {
+      // TODO: support hangman substution on social data?
+      return this;
+    }
+  }
+
+  private static class VariableData implements BatchItemData {
+    private final String value;
+
+    public VariableData(String value) {
+      this.value = value;
+    }
+
+    public BatchItem evaluate(Expressions expressions, ELContext elContext) throws ELException {
+      ValueExpression expression = expressions.parse(value, Object.class);
+      final Object result = expression.getValue(elContext);
+      return new BatchItem() {
+        public Object getData() {
+          return result;
+        }
+
+        public BatchType getType() {
+          return BatchType.VARIABLE;
+        }
+
+      };
+    }
+
+    public BatchItemData substitute(Substitutions substituter) {
+      return this;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Preload.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Preload.java
new file mode 100644
index 0000000..fc792f3
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/Preload.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.base.Splitter;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents an addressable piece of content that can be preloaded by the server
+ * to satisfy makeRequest calls
+ */
+public class Preload implements RequestAuthenticationInfo {
+  private static final Set<String> KNOWN_ATTRIBUTES
+      = ImmutableSet.of("views", "href", "authz", "sign_owner", "sign_viewer");
+
+  private final Uri base;
+
+  /**
+   * Creates a new Preload from an xml node.
+   *
+   * @param preload The Preload to create
+   * @throws SpecParserException When the href is not specified
+   */
+  public Preload(Element preload, Uri base) throws SpecParserException {
+    this.base = base;
+    href = XmlUtil.getUriAttribute(preload, "href");
+    if (href == null) {
+      throw new SpecParserException("Preload/@href is missing or invalid.");
+    }
+
+    // Record all the associated views
+    String viewNames = XmlUtil.getAttribute(preload, "views", "").trim();
+    if (viewNames.length() == 0) {
+      this.views = ImmutableSet.of();
+    } else {
+      this.views = ImmutableSet.copyOf(Splitter.on(',').trimResults().omitEmptyStrings().split(viewNames));
+    }
+
+    auth = AuthType.parse(XmlUtil.getAttribute(preload, "authz"));
+    signOwner = XmlUtil.getBoolAttribute(preload, "sign_owner", true);
+    signViewer = XmlUtil.getBoolAttribute(preload, "sign_viewer", true);
+    Map<String, String> attributes = Maps.newHashMap();
+    NamedNodeMap attrs = preload.getAttributes();
+    for (int i = 0; i < attrs.getLength(); ++i) {
+      Node attr = attrs.item(i);
+      if (!KNOWN_ATTRIBUTES.contains(attr.getNodeName())) {
+        attributes.put(attr.getNodeName(), attr.getNodeValue());
+      }
+    }
+    this.attributes = Collections.unmodifiableMap(attributes);
+  }
+
+  private Preload(Preload preload, Substitutions substituter) {
+    base = preload.base;
+    views = preload.views;
+    auth = preload.auth;
+    signOwner = preload.signOwner;
+    signViewer = preload.signViewer;
+    href = base.resolve(substituter.substituteUri(preload.href));
+    Map<String, String> attributes = Maps.newHashMap();
+    for (Map.Entry<String, String> entry : preload.attributes.entrySet()) {
+      attributes.put(entry.getKey(), substituter.substituteString(entry.getValue()));
+    }
+    this.attributes = Collections.unmodifiableMap(attributes);
+  }
+
+  /**
+   * Preload@href
+   */
+  private final Uri href;
+  public Uri getHref() {
+    return href;
+  }
+
+  /**
+   * Preload@auth
+   */
+  private final AuthType auth;
+  public AuthType getAuthType() {
+    return auth;
+  }
+
+  /**
+   * Preload/@sign_owner
+   */
+  private final boolean signOwner;
+  public boolean isSignOwner() {
+    return signOwner;
+  }
+
+  /**
+   * Preload/@sign_viewer
+   */
+  private final boolean signViewer;
+  public boolean isSignViewer() {
+    return signViewer;
+  }
+
+  /**
+   * All attributes from the preload tag
+   */
+  private final Map<String, String> attributes;
+  public Map<String, String> getAttributes() {
+    return attributes;
+  }
+
+  /**
+   * Prelaod@views
+   */
+  private final Set<String> views;
+  public Set<String> getViews() {
+    return views;
+  }
+
+  public Preload substitute(Substitutions substituter) {
+    return new Preload(this, substituter);
+  }
+
+  /**
+   * Produces an xml representation of the Preload.
+   */
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<Preload href='").append(href).append('\'')
+       .append(" authz='").append(auth.toString().toLowerCase()).append('\'')
+       .append(" views='");
+    Joiner.on(',').appendTo(buf, views);
+    buf.append('\'');
+
+    for (Map.Entry<String, String> entry : attributes.entrySet()) {
+      buf.append(' ').append(entry.getKey()).append("='").append(entry.getValue())
+         .append('\'');
+    }
+    buf.append("/>");
+    return buf.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/RequestAuthenticationInfo.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/RequestAuthenticationInfo.java
new file mode 100644
index 0000000..a9b40e6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/RequestAuthenticationInfo.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+
+import java.util.Map;
+
+/**
+ * Exposes authentication information to be extracted for making authenticated requests.
+ */
+public interface RequestAuthenticationInfo {
+  /**
+   * @return The type of authentication to use.
+   */
+  AuthType getAuthType();
+
+  /**
+   * @return The destination URI for making authenticated requests to.
+   */
+  Uri getHref();
+
+  /**
+   * @return True if owner signing is needed.
+   */
+  boolean isSignOwner();
+
+
+  /**
+   * @return True if viewer signing is needed.
+   */
+  boolean isSignViewer();
+
+  /**
+   * @return A map of all relevant auth-related attributes.
+   */
+  Map<String, String> getAttributes();
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/SpecParserException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/SpecParserException.java
new file mode 100644
index 0000000..92c452c
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/SpecParserException.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.gadgets.GadgetException;
+
+/**
+ * Exceptions for Gadget Spec parsing.
+ */
+public class SpecParserException extends GadgetException {
+  /**
+   * @param message
+   */
+  public SpecParserException(String message) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, message);
+  }
+
+  public SpecParserException(XmlException e) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, e);
+  }
+
+  public SpecParserException(String message, XmlException e) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, message, e);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/UserPref.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/UserPref.java
new file mode 100644
index 0000000..149beb8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/UserPref.java
@@ -0,0 +1,259 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.NodeList;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a UserPref tag.
+ */
+public class UserPref {
+  /**
+   * UserPref@name
+   * Message bundles
+   */
+  private final String name;
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * UserPref@display_name
+   * Message bundles
+   */
+  private String displayName;
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  /**
+   * UserPref@default_value
+   * Message bundles
+   */
+  private String defaultValue;
+  public String getDefaultValue() {
+    return defaultValue;
+  }
+
+  /**
+   * UserPref@required
+   */
+  private final boolean required;
+  public boolean getRequired() {
+    return required;
+  }
+
+  /**
+   * UserPref@datatype
+   */
+  private final DataType dataType;
+  public DataType getDataType() {
+    return dataType;
+  }
+
+  /**
+   * UserPref.EnumValue
+   * Collapsed so that EnumValue@value is the key and EnumValue@display_value
+   * is the value. If display_value is not present, value will be used.
+   * Message bundles are substituted into display_value, but not value.
+   */
+  private Map<String, String> enumValues;
+  public Map<String, String> getEnumValues() {
+    return enumValues;
+  }
+
+  /**
+   * UserPref.EnumValue (ordered)
+   * Useful for rendering ordered lists of user prefs with enum type.
+   */
+  private List<EnumValuePair> orderedEnumValues;
+  public List<EnumValuePair> getOrderedEnumValues() {
+    return orderedEnumValues;
+  }
+
+  /**
+   * Performs substitutions on the pref. See field comments for details on what
+   * is substituted.
+   *
+   * @param substituter
+   * @return The substituted pref.
+   */
+  public UserPref substitute(Substitutions substituter) {
+    UserPref pref = new UserPref(this);
+    pref.displayName = substituter.substituteString(displayName);
+    pref.defaultValue = substituter.substituteString(defaultValue);
+    if (enumValues.isEmpty()) {
+      pref.enumValues = Collections.emptyMap();
+    } else {
+      Map<String, String> values = Maps.newHashMapWithExpectedSize(enumValues.size());
+      for (Map.Entry<String, String> entry : enumValues.entrySet()) {
+        values.put(entry.getKey(), substituter.substituteString(entry.getValue()));
+      }
+      pref.enumValues = ImmutableMap.copyOf(values);
+    }
+    if (orderedEnumValues.isEmpty()) {
+      pref.orderedEnumValues = Collections.emptyList();
+    } else {
+      List<EnumValuePair> orderedValues = Lists.newLinkedList();
+      for (EnumValuePair evp : orderedEnumValues) {
+        orderedValues.add(new EnumValuePair(evp.getValue(),
+            substituter.substituteString(evp.getDisplayValue())));
+      }
+      pref.orderedEnumValues = Collections.unmodifiableList(orderedValues);
+    }
+    return pref;
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<UserPref name=\"")
+       .append(name)
+       .append("\" display_name=\"")
+       .append(displayName)
+       .append("\" default_value=\"")
+       .append(defaultValue)
+       .append("\" required=\"")
+       .append(required)
+       .append("\" datatype=\"")
+       .append(dataType.toString().toLowerCase())
+       .append('\"');
+    if (enumValues.isEmpty()) {
+      buf.append("/>");
+    } else {
+      buf.append(">\n");
+      for (Map.Entry<String, String> entry : enumValues.entrySet()) {
+        buf.append("<EnumValue value=\"")
+           .append(entry.getKey())
+           .append("\" display_value=\"")
+           .append(entry.getValue())
+           .append("\"/>\n");
+      }
+      buf.append("</UserPref>");
+    }
+    return buf.toString();
+  }
+
+  /**
+   * @param element
+   * @throws SpecParserException
+   */
+  public UserPref(Element element) throws SpecParserException {
+    String name = XmlUtil.getAttribute(element, "name");
+    if (name == null) {
+      throw new SpecParserException("UserPref@name is required.");
+    }
+    this.name = name;
+
+    displayName = XmlUtil.getAttribute(element, "display_name", name);
+    defaultValue = XmlUtil.getAttribute(element, "default_value", "");
+    required = XmlUtil.getBoolAttribute(element, "required");
+
+    String dataType = XmlUtil.getAttribute(element, "datatype", "string");
+    this.dataType = DataType.parse(dataType);
+
+    NodeList children = element.getElementsByTagName("EnumValue");
+    if (children.getLength() > 0) {
+      Map<String, String> enumValues = Maps.newHashMap();
+      List<EnumValuePair> orderedEnumValues = Lists.newLinkedList();
+      for (int i = 0, j = children.getLength(); i < j; ++i) {
+        Element child = (Element)children.item(i);
+        String value = XmlUtil.getAttribute(child, "value");
+        if (value == null) {
+          throw new SpecParserException("EnumValue@value is required.");
+        }
+        String displayValue
+            = XmlUtil.getAttribute(child, "display_value", value);
+        enumValues.put(value, displayValue);
+        orderedEnumValues.add(new EnumValuePair(value, displayValue));
+      }
+      this.enumValues = Collections.unmodifiableMap(enumValues);
+      this.orderedEnumValues = Collections.unmodifiableList(orderedEnumValues);
+    } else {
+      this.enumValues = Collections.emptyMap();
+      this.orderedEnumValues = Collections.emptyList();
+    }
+  }
+
+  /**
+   * Produces a UserPref suitable for substitute()
+   * @param userPref
+   */
+  private UserPref(UserPref userPref) {
+    name = userPref.name;
+    dataType = userPref.dataType;
+    required = userPref.required;
+  }
+
+  /**
+   * Possible values for UserPref@datatype
+   */
+  public static enum DataType {
+    STRING, HIDDEN, BOOL, ENUM, LIST, NUMBER;
+
+    /**
+     * Parses a data type from the input string.
+     *
+     * @param value
+     * @return The data type of the given value.
+     */
+    public static DataType parse(String value) {
+      for (DataType type : DataType.values()) {
+        if (type.toString().compareToIgnoreCase(value) == 0) {
+          return type;
+        }
+      }
+      return STRING;
+    }
+  }
+
+  /**
+   * Simple data structure representing a value/displayValue pair
+   * for UserPref enums. Value is EnumValue@value, and DisplayValue is EnumValue@displayValue.
+   */
+  public static final class EnumValuePair {
+    private final String value;
+    private final String displayValue;
+
+    private EnumValuePair(String value, String displayValue) {
+      this.value = value;
+      this.displayValue = displayValue;
+    }
+
+    public String getValue() {
+      return value;
+    }
+
+    public String getDisplayValue() {
+      return displayValue;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/View.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/View.java
new file mode 100644
index 0000000..8ad1eb0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/spec/View.java
@@ -0,0 +1,371 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.variables.Substitutions;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents a Content section, but normalized into an individual
+ * view value after views are split on commas.
+ */
+public class View implements RequestAuthenticationInfo {
+  private static final Set<String> KNOWN_ATTRIBUTES = ImmutableSet.of(
+      "type", "view", "href", "preferred_height", "preferred_width", "authz", "quirks",
+      "sign_owner", "sign_viewer"
+  );
+
+  private final Uri base;
+
+  /**
+   * @param name The name of this view.
+   * @param elements List of all views, in order, that make up this view.
+   *     An ordered list is required per the spec, since values must
+   *     overwrite one another.
+   * @param base The base url to resolve href against.
+   * @throws SpecParserException
+   */
+  public View(String name, List<Element> elements, Uri base) throws SpecParserException {
+    this.name = name;
+    this.base = base;
+
+    boolean quirks = true;
+    Uri href = null;
+    String contentType = null;
+    ContentType type = null;
+    int preferredHeight = 0;
+    int preferredWidth = 0;
+    String auth = null;
+    boolean signOwner = true;
+    boolean signViewer = true;
+    Map<String, String> attributes = Maps.newHashMap();
+    StringBuilder content = new StringBuilder();
+
+    boolean needOwner = false;
+    boolean needViewer = false;
+
+    PipelinedData pipelinedData = null;
+
+    for (Element element : elements) {
+      contentType = XmlUtil.getAttribute(element, "type");
+      if (contentType != null) {
+        ContentType newType = ContentType.parse(contentType);
+        if (type != null && newType != type) {
+          throw new SpecParserException("You may not mix content types in the same view.");
+        } else {
+          type = newType;
+        }
+      }
+      href = XmlUtil.getUriAttribute(element, "href", href);
+      quirks = XmlUtil.getBoolAttribute(element, "quirks", quirks);
+      preferredHeight = XmlUtil.getIntAttribute(element, "preferred_height");
+      preferredWidth = XmlUtil.getIntAttribute(element, "preferred_width");
+      auth = XmlUtil.getAttribute(element, "authz", auth);
+      signOwner = XmlUtil.getBoolAttribute(element, "sign_owner", signOwner);
+      signViewer = XmlUtil.getBoolAttribute(element, "sign_viewer", signViewer);
+      content.append(element.getTextContent());
+      NamedNodeMap attrs = element.getAttributes();
+      for (int i = 0; i < attrs.getLength(); ++i) {
+        Node attr = attrs.item(i);
+        if (!KNOWN_ATTRIBUTES.contains(attr.getNodeName())) {
+          attributes.put(attr.getNodeName(), attr.getNodeValue());
+        }
+      }
+
+      // For proxied rendering, parse all SocialData inside the View element
+      if (href != null && (type != ContentType.URL)) {
+        pipelinedData = new PipelinedData(element, base);
+        needOwner = needOwner || pipelinedData.needsOwner();
+        needViewer = needViewer || pipelinedData.needsViewer();
+      }
+    }
+    this.content = content.toString();
+    this.needsUserPrefSubstitution = this.content.contains("__UP_");
+    this.quirks = quirks;
+    this.href = href;
+    this.rawType = Objects.firstNonNull(contentType, "html");
+    this.type = Objects.firstNonNull(type, ContentType.HTML);
+    this.preferredHeight = preferredHeight;
+    this.preferredWidth = preferredWidth;
+    this.attributes = Collections.unmodifiableMap(attributes);
+    this.pipelinedData = pipelinedData;
+
+    this.authType = AuthType.parse(auth);
+    this.signOwner = signOwner;
+    this.signViewer = signViewer;
+    if (type == ContentType.URL && this.href == null) {
+      throw new SpecParserException("Content@href must be set when Content@type is \"url\".");
+    }
+
+    // Verify that there is no use of viewer and/or owner data when the request
+    // is not signed by viewer and/or owner
+
+    // TODO: this does not catch use of <Preload> with sign-by-owner
+    // for proxied rendering that is not sign-by-owner
+    if (needOwner && (!this.signOwner || this.authType == AuthType.NONE)) {
+      throw new SpecParserException("Must sign by owner to request owner.");
+    }
+
+    if (needViewer && (!this.signViewer || this.authType == AuthType.NONE)) {
+      throw new SpecParserException("Must sign by viewer to request viewer.");
+    }
+  }
+
+  /**
+   * Allows the creation of a view from an existing view so that localization
+   * can be performed.
+   */
+  private View(View view, Substitutions substituter) {
+    needsUserPrefSubstitution = view.needsUserPrefSubstitution;
+    name = view.name;
+    rawType = view.rawType;
+    type = view.type;
+    quirks = view.quirks;
+    preferredHeight = view.preferredHeight;
+    preferredWidth = view.preferredWidth;
+    authType = view.authType;
+    signOwner = view.signOwner;
+    signViewer = view.signViewer;
+
+    content = substituter.substituteString(view.content);
+    base = view.base;
+    href = base.resolve(substituter.substituteUri(view.href));
+
+    // Facilitates type=url support of dual-schema endpoints.
+    if (view.getType() == ContentType.URL && view.href.getScheme() == null) {
+      href = new UriBuilder(href).setScheme(null).toUri();
+    }
+
+    Map<String, String> attributes = Maps.newHashMap();
+    for (Map.Entry<String, String> entry : view.attributes.entrySet()) {
+      attributes.put(entry.getKey(), substituter.substituteString(entry.getValue()));
+    }
+    this.attributes = Collections.unmodifiableMap(attributes);
+    pipelinedData = view.pipelinedData == null ? null : view.pipelinedData.substitute(substituter);
+  }
+
+  /**
+   * Content@view
+   */
+  private final String name;
+  public String getName() {
+    return name;
+  }
+
+  /**
+   * Content@type
+   */
+  private final ContentType type;
+  public ContentType getType() {
+    return type;
+  }
+
+  /**
+   * Content@type - the raw, possibly non-standard string
+   */
+  private final String rawType;
+  public String getRawType() {
+    return rawType;
+  }
+
+  /**
+   * Content@href
+   *
+   * All substitutions
+   */
+  private Uri href;
+  public Uri getHref() {
+    return href;
+  }
+
+  /**
+   * Content@quirks
+   */
+  private final boolean quirks;
+  public boolean getQuirks() {
+    return quirks;
+  }
+
+  /**
+   * Content@preferred_height
+   */
+  private final int preferredHeight;
+  public int getPreferredHeight() {
+    return preferredHeight;
+  }
+
+  /**
+   * Content@preferred_width
+   */
+  private final int preferredWidth;
+  public int getPreferredWidth() {
+    return preferredWidth;
+  }
+
+  /**
+   * Content#CDATA
+   *
+   * All substitutions
+   */
+  private String content;
+  public String getContent() {
+    return content;
+  }
+
+  /**
+   * Set content for a type=html, href=URL style gadget.
+   * This is the last bastion of GadgetSpec mutability,
+   * and should only be used for the described case.
+   * Call nulls out href in order to indicate content was
+   * successfully retrieved.
+   * @param content New gadget content retrieved from href.
+   */
+  public void setHrefContent(String content) {
+    this.content = content;
+    this.href = null;
+  }
+
+  /**
+   * Whether or not the content section has any __UP_ hangman variables,
+   * or is type=url.
+   */
+  private final boolean needsUserPrefSubstitution;
+  public boolean needsUserPrefSubstitution() {
+    return needsUserPrefSubstitution || type == ContentType.URL;
+  }
+
+  /**
+   * Content/@authz
+   */
+  private final AuthType authType;
+  public AuthType getAuthType() {
+    return authType;
+  }
+
+  /**
+   * Content/@sign_owner
+   */
+  private final boolean signOwner;
+  public boolean isSignOwner() {
+    return signOwner;
+  }
+
+  /**
+   * Content/@sign_viewer
+   */
+  private final boolean signViewer;
+  public boolean isSignViewer() {
+    return signViewer;
+  }
+
+  /**
+   * All attributes.
+   */
+  private final Map<String, String> attributes;
+  public Map<String, String> getAttributes() {
+    return attributes;
+  }
+
+  private final PipelinedData pipelinedData;
+
+  /**
+   * All os: preloads.
+   */
+  public PipelinedData getPipelinedData() {
+    return pipelinedData;
+  }
+
+  /**
+   * Creates a new view by performing hangman substitution. See field comments
+   * for details on what gets substituted.
+   *
+   * @param substituter
+   * @return The substituted view.
+   */
+  public View substitute(Substitutions substituter) {
+    return new View(this, substituter);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder buf = new StringBuilder();
+    buf.append("<Content")
+       .append(" type='").append(rawType).append('\'')
+       .append(" href='").append(href).append('\'')
+       .append(" view='").append(name).append('\'')
+       .append(" quirks='").append(quirks).append('\'')
+       .append(" preferred_height='").append(preferredHeight).append('\'')
+       .append(" preferred_width='").append(preferredWidth).append('\'')
+       .append(" authz=").append(authType.toString().toLowerCase()).append('\'');
+    for (Map.Entry<String, String> entry : attributes.entrySet()) {
+      buf.append(entry.getKey()).append("='").append(entry.getValue()).append('\'');
+    }
+    buf.append("'>")
+       .append(content)
+       .append("</Content>");
+    return buf.toString();
+  }
+
+  /**
+   * Possible values for Content/@type
+   */
+  public enum ContentType {
+    HTML("html"), URL("url"), HTML_SANITIZED("x-html-sanitized");
+
+    private String viewName;
+
+    private ContentType(String viewName) {
+      this.viewName = viewName;
+    }
+
+    /**
+     * @param viewName
+     * @return The parsed value (defaults to html)
+     */
+    public static ContentType parse(String viewName) {
+      viewName = viewName.toLowerCase().trim();
+      for (ContentType enumVal : ContentType.values()) {
+        if (enumVal.viewName.equals(viewName)) {
+          return enumVal;
+        }
+      }
+      return HTML;
+    }
+
+    @Override
+    public String toString() {
+      return viewName;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/ContainerTagLibraryFactory.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/ContainerTagLibraryFactory.java
new file mode 100644
index 0000000..7a9664b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/ContainerTagLibraryFactory.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.GadgetException;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * Serves up a per-container tag library to the TemplateRewriter.
+ */
+@Singleton
+public class ContainerTagLibraryFactory {
+  private static final Logger LOG = Logger.getLogger(
+      ContainerTagLibraryFactory.class.getName());
+
+  private final ContainerConfig config;
+  private final LoadingCache<String, TemplateLibrary> osmlLibraryCache = CacheBuilder
+      .newBuilder()
+      .build(new CacheLoader<String, TemplateLibrary>() {
+          public TemplateLibrary load(String resourceName) {
+            return loadTrustedLibrary(resourceName);
+          }
+        });
+
+  @Inject
+  public ContainerTagLibraryFactory(ContainerConfig config) {
+    this.config = config;
+  }
+
+  /**
+   * Return a per-container tag registry.
+   */
+  public TemplateLibrary getLibrary(String container) {
+    return getOsmlLibrary(container);
+  }
+
+  private TemplateLibrary getOsmlLibrary(String container) {
+    String library = config.getString(container,
+        "${Cur['gadgets.features'].osml.library}");
+    if (Strings.isNullOrEmpty(library)) {
+      return NullTemplateLibrary.INSTANCE;
+    }
+
+    return osmlLibraryCache.getUnchecked(library);
+  }
+
+  static private TemplateLibrary loadTrustedLibrary(String resource) {
+    try {
+      String content = ResourceLoader.getContent(resource);
+      return new XmlTemplateLibrary(Uri.parse("#OSML"), XmlUtil.parse(content),
+          content, true);
+    } catch (IOException ioe) {
+      LOG.log(Level.WARNING, null, ioe);
+    } catch (XmlException xe) {
+      LOG.log(Level.WARNING, null, xe);
+    } catch (GadgetException tpe) {
+      LOG.log(Level.WARNING, null, tpe);
+    }
+
+    return NullTemplateLibrary.INSTANCE;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java
new file mode 100644
index 0000000..a0038fa
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessor.java
@@ -0,0 +1,503 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.GadgetELResolver;
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.apache.shindig.gadgets.templates.tags.RepeatTagHandler;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.ValueExpression;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+/**
+ * Implements a DOM-based OS templates compiler.
+ * Supports:
+ *   - ${...} style expressions in content and attributes
+ *   - @if attribute
+ *   - @repeat attribute
+ * TODO:
+ *   - Handle built-in/custom tags
+ */
+public class DefaultTemplateProcessor implements TemplateProcessor {
+
+  //class name for logging purpose
+  private static final String classname = DefaultTemplateProcessor.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+
+  public static final String PROPERTY_INDEX = "Index";
+  public static final String PROPERTY_COUNT = "Count";
+
+  public static final String ATTRIBUTE_IF = "if";
+  public static final String ATTRIBUTE_INDEX = "index";
+  public static final String ATTRIBUTE_REPEAT = "repeat";
+  public static final String ATTRIBUTE_VAR = "var";
+  public static final String ATTRIBUTE_CUR = "cur";
+
+  /**
+   * Set of attributes in HTML 4 that are boolean, and may only be set
+   * to that value, and should be omitted to indicate "false".
+   */
+  private static final Set<String> HTML4_BOOLEAN_ATTRIBUTES =
+    ImmutableSet.of("checked", "compact", "declare", "defer", "disabled", "ismap",
+        "multiple", "nohref", "noresize", "noshade", "nowrap", "readonly", "selected");
+
+  private static final Set<String> ONCREATE_ATTRIBUTES =
+    ImmutableSet.of("oncreate", "x-oncreate");
+
+  private final Expressions expressions;
+  // Reused buffer for creating template output
+  private final StringBuilder outputBuffer;
+
+  private TagRegistry registry;
+  private TemplateContext templateContext;
+  private ELContext elContext;
+
+  private int uniqueIdCounter = 0;
+
+  @Inject
+  public DefaultTemplateProcessor(Expressions expressions) {
+    this.expressions = expressions;
+    outputBuffer = new StringBuilder();
+  }
+
+  /**
+   * Process an entire template.
+   *
+   * @param template the DOM template, typically a script element
+   * @param templateContext a template context providing top-level
+   *     variables
+   * @param globals ELResolver providing global variables other
+   *     than those in the templateContext
+   * @return a document fragment with the resolved content
+   */
+  public DocumentFragment processTemplate(Element template,
+      TemplateContext templateContext, ELResolver globals, TagRegistry registry) {
+
+    this.registry = registry;
+    this.templateContext = templateContext;
+    this.elContext = expressions.newELContext(globals,
+        new GadgetELResolver(templateContext.getGadget().getContext()),
+        new TemplateELResolver(templateContext),
+        new ElementELResolver());
+
+    DocumentFragment result = template.getOwnerDocument().createDocumentFragment();
+    processChildNodes(result, template);
+    return result;
+  }
+
+  /** Process the children of an element or document. */
+  public void processChildNodes(Node result, Node source) {
+    NodeList nodes = source.getChildNodes();
+    for (int i = 0; i < nodes.getLength(); i++) {
+      processNode(result, nodes.item(i));
+    }
+  }
+
+  public TemplateContext getTemplateContext() {
+    return templateContext;
+  }
+
+  /**
+   * Process a node.
+   *
+   * @param result the target node where results should be inserted
+   * @param source the source node of the template being processed
+   */
+  private void processNode(Node result, Node source) {
+    switch (source.getNodeType()) {
+      case Node.TEXT_NODE:
+        processText(result, source.getTextContent());
+        break;
+      case Node.ELEMENT_NODE:
+        processElement(result, (Element) source);
+        break;
+      case Node.DOCUMENT_NODE:
+        processChildNodes(result, source);
+        break;
+    }
+  }
+
+  /**
+   * Process text content by including non-expression content verbatim and
+   * escaping expression content.
+
+   * @param result the target node where results should be inserted
+   * @param textContent the text content being processed
+   */
+  private void processText(Node result, String textContent) {
+    Document ownerDocument = result.getOwnerDocument();
+
+    int start = 0;
+    int current = 0;
+    while (current < textContent.length()) {
+      current = textContent.indexOf("${", current);
+      // No expressions, we're done
+      if (current < 0) {
+        break;
+      }
+
+      // An escaped expression "\${"
+      if (current > 0 && textContent.charAt(current - 1) == '\\') {
+        // Drop the \ by outputting everything before it, and moving past
+        // the ${
+        if (current - 1 > start) {
+          String staticText = textContent.substring(start, current - 1);
+          result.appendChild(ownerDocument.createTextNode(staticText));
+        }
+        //EL syntax is supported in gadget rendering(https://reviews.apache.org/r/8184),
+        //so keep the \ into expression result, to make sure "\${" will not be evaluated in gadget rendering as expected
+        start = current -1 ;
+        current = current + 2;
+        continue;
+      }
+
+      // Not a real expression, we're done
+      int expressionEnd = textContent.indexOf('}', current + 2);
+      if (expressionEnd < 0) {
+        break;
+      }
+
+      // Append the existing static text, if any
+      if (current > start) {
+        result.appendChild(ownerDocument.createTextNode(textContent.substring(start, current)));
+      }
+
+      // Isolate the expression, parse and evaluate
+      String expression = textContent.substring(current, expressionEnd + 1);
+      String value = evaluate(expression, String.class, "");
+
+      if (!"".equals(value)) {
+        // And now escape
+        outputBuffer.setLength(0);
+        try {
+          HtmlSerialization.printEscapedText(value, outputBuffer);
+        } catch (IOException e) {
+          // Can't happen writing to StringBuilder
+          throw new RuntimeException(e);
+        }
+
+        result.appendChild(ownerDocument.createTextNode(outputBuffer.toString()));
+      }
+
+      // And continue with the next expression
+      current = start = expressionEnd + 1;
+    }
+
+    // Add any static text left over
+    if (start < textContent.length()) {
+      result.appendChild(ownerDocument.createTextNode(textContent.substring(start)));
+    }
+  }
+
+  /**
+   * Process repeater state, if needed, on an element.
+   */
+  private void processElement(final Node result, final Element element) {
+    Attr repeat = element.getAttributeNode(ATTRIBUTE_REPEAT);
+    if (repeat != null) {
+      Iterable<?> dataList = evaluate(repeat.getValue(), Iterable.class, null);
+      processRepeat(result, element, dataList, new Runnable() {
+        public void run() {
+          processElementInner(result, element);
+        }
+      });
+    } else {
+      processElementInner(result, element);
+    }
+  }
+
+  /**
+   * @param result
+   * @param element
+   * @param dataList
+   */
+  public void processRepeat(Node result, Element element, Iterable<?> dataList,
+      Runnable onEachLoop) {
+    if (dataList == null) {
+      return;
+    }
+
+    // Compute list size
+    int size = Iterables.size(dataList);
+
+    if (size > 0) {
+      // Save the initial EL state
+      Map<String, ? extends Object> oldContext = templateContext.getContext();
+      Object oldCur = templateContext.getCur();
+      ValueExpression oldVarExpression = null;
+
+      // Set the new Context variable.  Copy the old context to preserve
+      // any existing "index" variable
+      Map<String, Object> loopData = Maps.newHashMap(oldContext);
+      loopData.put(PROPERTY_COUNT, size);
+      templateContext.setContext(loopData);
+
+      // TODO: This means that any loop with @var doesn't make the loop
+      // variable available in the default expression context.
+      // Update the specification to make this explicit.
+      Attr varAttr = element.getAttributeNode(ATTRIBUTE_VAR);
+      if (varAttr == null) {
+        oldCur = templateContext.getCur();
+      } else {
+        oldVarExpression = elContext.getVariableMapper().resolveVariable(varAttr.getValue());
+      }
+
+      Attr indexVarAttr = element.getAttributeNode(ATTRIBUTE_INDEX);
+      String indexVar = indexVarAttr == null ? PROPERTY_INDEX : indexVarAttr.getValue();
+
+      int index = 0;
+      for (Object data : dataList) {
+        loopData.put(indexVar, index++);
+
+        // Set up context for rendering inner node
+        templateContext.setCur(data);
+        if (varAttr != null) {
+          ValueExpression varExpression = expressions.constant(data, Object.class);
+          elContext.getVariableMapper().setVariable(varAttr.getValue(), varExpression);
+        }
+
+        onEachLoop.run();
+
+      }
+
+      // Restore EL state
+      if (varAttr == null) {
+        templateContext.setCur(oldCur);
+      } else {
+        elContext.getVariableMapper().setVariable(varAttr.getValue(), oldVarExpression);
+      }
+
+      templateContext.setContext(oldContext);
+    }
+  }
+
+  /**
+   * Process conditionals and non-repeat attributes on an element
+   */
+  private void processElementInner(Node result, Element element) {
+    TagHandler handler = registry.getHandlerFor(element);
+
+    // An ugly special-case:  <os:Repeat> will re-evaluate the "if" attribute
+    // (as it should) for each loop of the repeat.  Don't evaluate it here.
+    if (!(handler instanceof RepeatTagHandler)) {
+      Attr ifAttribute = element.getAttributeNode(ATTRIBUTE_IF);
+      if (ifAttribute != null) {
+        if (!evaluate(ifAttribute.getValue(), Boolean.class, false)) {
+          return;
+        }
+      }
+    }
+
+    // TODO: the spec is silent on order of evaluation of "cur" relative
+    // to "if" and "repeat"
+    Attr curAttribute = element.getAttributeNode(ATTRIBUTE_CUR);
+    Object oldCur = templateContext.getCur();
+    if (curAttribute != null) {
+      templateContext.setCur(evaluate(curAttribute.getValue(), Object.class, null));
+    }
+
+    if (handler != null) {
+      handler.process(result, element, this);
+    } else {
+      // Be careful cloning nodes! If a target node belongs to a different document than the
+      // template node then use importNode rather than cloneNode as that avoids side-effects
+      // in UserDataHandlers where the cloned template node would belong to its original
+      // document before being adopted by the target document.
+      Element resultNode;
+      if (element.getOwnerDocument() != result.getOwnerDocument()) {
+        resultNode = (Element)result.getOwnerDocument().importNode(element, false);
+      } else {
+        resultNode = (Element)element.cloneNode(false);
+      }
+
+      clearSpecialAttributes(resultNode);
+      Node additionalNode = processAttributes(resultNode);
+
+      processChildNodes(resultNode, element);
+      result.appendChild(resultNode);
+
+      if (additionalNode != null) {
+        result.appendChild(additionalNode);
+      }
+    }
+
+    if (curAttribute != null) {
+      templateContext.setCur(oldCur);
+    }
+  }
+
+  private void clearSpecialAttributes(Element element) {
+    element.removeAttribute(ATTRIBUTE_IF);
+    element.removeAttribute(ATTRIBUTE_REPEAT);
+    element.removeAttribute(ATTRIBUTE_INDEX);
+    element.removeAttribute(ATTRIBUTE_VAR);
+    element.removeAttribute(ATTRIBUTE_CUR);
+  }
+
+  /**
+   * Process expressions on attributes.
+   * @param element The Element to process attributes on
+   * @return Node to attach after this Element, or null
+   */
+  private Node processAttributes(Element element) {
+    NamedNodeMap attributes = element.getAttributes();
+    Node additionalNode = null;
+
+    // Mutations to perform after iterating (if needed)
+    List<Attr> attrsToRemove = null;
+    String newId = null;
+
+    for (int i = 0; i < attributes.getLength(); i++) {
+      boolean removeThisAttribute = false;
+
+      Attr attribute = (Attr) attributes.item(i);
+      // Boolean attributes: evaluate as a boolean.  If true, set the value to the
+      // name of the attribute, e.g. selected="selected".  If false, remove the attribute
+      // altogether.  The check here has some limitations for efficiency:  it assumes the
+      // attribute is lowercase, and doesn't bother to check whether the boolean attribute
+      // actually exists on the referred element (but HTML has no attrs that are sometimes
+      // boolean and sometimes not)
+      if (element.getNamespaceURI() == null &&
+          HTML4_BOOLEAN_ATTRIBUTES.contains(attribute.getName())) {
+        if (Boolean.TRUE.equals(evaluate(attribute.getValue(), Boolean.class, Boolean.FALSE))) {
+          attribute.setNodeValue(attribute.getName());
+        } else {
+          removeThisAttribute = true;
+        }
+      } else if (ONCREATE_ATTRIBUTES.contains(attribute.getName())) {
+        String id = element.getAttribute("id");
+        if (id.length() == 0) {
+          newId = id = getUniqueId();
+        }
+
+        additionalNode = buildOnCreateScript(
+            evaluate(attribute.getValue(), String.class, null), id, element.getOwnerDocument());
+        removeThisAttribute = true;
+      } else {
+        attribute.setNodeValue(evaluate(attribute.getValue(), String.class, null));
+      }
+
+      // Because NamedNodeMaps are live, removing them interferes with iteration.
+      // Remove the attributes in a later pass
+      if (removeThisAttribute) {
+        if (attrsToRemove == null) {
+          attrsToRemove = Lists.newArrayListWithCapacity(attributes.getLength());
+        }
+
+        attrsToRemove.add(attribute);
+      }
+    }
+
+    // Now that iteration is complete, perform mutations
+    if (attrsToRemove != null) {
+      for (Attr attr : attrsToRemove) {
+        element.removeAttributeNode(attr);
+      }
+    }
+
+    if (newId != null) {
+      element.setAttribute("id", newId);
+    }
+
+    return additionalNode;
+  }
+
+  /**
+   * Inserts an inline script element that executes a snippet of Javascript
+   * code after the element is emitted.
+   * <p>
+   * The approach used involves using Javascript to find the previous sibling
+   * node and apply the code to it - this avoids decorating nodes with IDs, an
+   * approach that could potentially clash with existing element IDs that could
+   * be non-unique.
+   * <p>
+   * The resulting script element is subject to sanitization.
+   * <p>
+   * @param code Javascript code to execute
+   * @param id Element ID which should be used
+   * @param document document for creating elements
+   *
+   * TODO: Move boilerplate code for finding the right node out to a function
+   * to reduce code size.
+   */
+  private Node buildOnCreateScript(String code, String id, Document document) {
+    Element script = document.createElement("script");
+    script.setAttribute("type", "text/javascript");
+    StringBuilder builder = new StringBuilder();
+    builder.append("(function(){");
+    builder.append(code);
+    builder.append("}).apply(document.getElementById('");
+    builder.append(id);
+    builder.append("'));");
+    script.setTextContent(builder.toString());
+    return script;
+  }
+
+  /**
+   *  Evaluates an expression within the scope of this processor's context.
+   *  @param expression The String expression
+   *  @param type Expected result type
+   *  @param defaultValue Default value to return in case of error
+   */
+  public <T> T evaluate(String expression, Class<T> type, T defaultValue) {
+    try {
+      ValueExpression expr = expressions.parse(expression, type);
+      // Workaround for inability of Jasper-EL resolvers to access VariableMapper
+      elContext.putContext(TemplateContext.class, elContext);
+      Object result = expr.getValue(elContext);
+      return type.cast(result);
+    } catch (ELException e) {
+      if (LOG.isLoggable(Level.WARNING)) {
+        LOG.logp(Level.WARNING, classname, "evaluate", MessageKeys.EL_FAILURE,
+        		  new Object[] {getTemplateContext().getGadget().getContext().getUrl(), e.getMessage()});
+      }
+      return defaultValue;
+    }
+  }
+
+  private String getUniqueId() {
+    return "ostid" + (uniqueIdCounter++);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/ElementELResolver.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/ElementELResolver.java
new file mode 100644
index 0000000..01beb2b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/ElementELResolver.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.el.ELContext;
+import javax.el.ELResolver;
+
+import com.google.common.collect.Lists;
+
+/**
+ * ELResolver that processes DOM elements.
+ */
+public class ElementELResolver extends ELResolver {
+  /**
+   * A wrapper for a DOM Element that overrides toString().
+   * TODO: remove with JUEL 2.1.1.
+   */
+  public static class ElementWrapper {
+    public final Element element;
+
+    public ElementWrapper(Element element) {
+      this.element = element;
+    }
+
+    @Override
+    public String toString() {
+      return element.getTextContent();
+    }
+  }
+
+  @Override
+  public Class<?> getCommonPropertyType(ELContext context, Object base) {
+    if (base instanceof ElementWrapper) {
+      return String.class;
+    }
+    return null;
+  }
+
+  @Override
+  public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+      Object base) {
+    return null;
+  }
+
+  @Override
+  public Class<?> getType(ELContext context, Object base, Object property) {
+    Object value = getValue(context, base, property);
+    return value == null ? null : value.getClass();
+  }
+
+  @Override
+  public Object getValue(ELContext context, Object base, Object property) {
+    if (!(base instanceof ElementWrapper)) {
+      return null;
+    }
+
+    context.setPropertyResolved(true);
+    Element element = ((ElementWrapper) base).element;
+    String propertyString = property.toString();
+
+    // See if there is an Object property.
+    Object data = element.getUserData(propertyString);
+    if (data != null) {
+      return data;
+    }
+
+    // Next, check for an attribute.
+    Attr attribute = element.getAttributeNode(propertyString);
+    if (attribute != null) {
+      return attribute.getValue();
+    }
+
+    // Finally, look for child nodes with matching local names.
+    List<ElementWrapper> childElements = null;
+    for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) {
+      if (!(child instanceof Element)) {
+        continue;
+      }
+
+      Element childElement = (Element) child;
+      if (!propertyString.equals(childElement.getLocalName())) {
+        continue;
+      }
+
+      if (childElements == null) {
+        childElements = Lists.newArrayListWithCapacity(2);
+      }
+
+      childElements.add(new ElementWrapper(childElement));
+    }
+
+    if (childElements == null) {
+      return null;
+    } else if (childElements.size() == 1) {
+      return childElements.get(0);
+    } else {
+      return childElements;
+    }
+  }
+
+  @Override
+  public boolean isReadOnly(ELContext context, Object base, Object property) {
+    if (base instanceof ElementWrapper) {
+      context.setPropertyResolved(true);
+    }
+    return true;
+  }
+
+  @Override
+  public void setValue(ELContext context, Object base, Object property,
+      Object value) {
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/MessageELResolver.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/MessageELResolver.java
new file mode 100644
index 0000000..91c2608
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/MessageELResolver.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+import javax.el.ELResolver;
+import javax.el.PropertyNotWritableException;
+
+import com.google.common.collect.Lists;
+
+/**
+ * ELResolver for the Msg property in templates.
+ */
+public class MessageELResolver extends ELResolver {
+  public static final String PROPERTY_MSG = "Msg";
+  private final MessageBundle bundle;
+  private final Expressions expressions;
+
+  public MessageELResolver(Expressions expressions, MessageBundle bundle) {
+    this.expressions = expressions;
+    this.bundle = bundle;
+  }
+
+  @Override
+  public Class<?> getCommonPropertyType(ELContext context, Object base) {
+    if (base == null) {
+      return String.class;
+    }
+
+    return null;
+  }
+
+  @Override
+  public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+      Object base) {
+    return null;
+  }
+
+  @Override
+  public Class<?> getType(ELContext context, Object base, Object property) {
+    // TODO: implement
+    return null;
+  }
+
+  @Override
+  public Object getValue(ELContext context, Object base, Object property) {
+    if ((base == null) && PROPERTY_MSG.equals(property)) {
+      context.setPropertyResolved(true);
+      return bundle;
+    } else if (base instanceof MessageBundle) {
+      String text = bundle.getMessages().get(property.toString());
+      if (text == null) {
+        context.setPropertyResolved(true);
+        return null;
+      }
+
+      List<Object> properties = null;
+      try {
+        properties = pushCurrentProperty(context, property);
+        context.setPropertyResolved(false);
+        return expressions.parse(text, Object.class).getValue(context);
+      } finally {
+        popProperty(properties);
+        context.setPropertyResolved(true);
+      }
+    }
+
+    return null;
+  }
+
+  /**
+   * Track the set of message bundle properties being evaluated.  We allow
+   * recursion, but don't want to allow infinite self-recursion (though the
+   * stack overflows quickly).
+   */
+  private List<Object> pushCurrentProperty(ELContext context, Object property) {
+    @SuppressWarnings("unchecked")
+    List<Object> propertyList = (List<Object>) context.getContext(MessageELResolver.class);
+    if (propertyList == null) {
+      propertyList = Lists.newArrayList();
+      context.putContext(MessageELResolver.class, propertyList);
+    } else {
+      if (propertyList.contains(property)) {
+        throw new ELException("Recursive invocation of message bundle properties");
+      }
+    }
+
+    propertyList.add(property);
+    return propertyList;
+  }
+
+  private void popProperty(List<Object> properties) {
+    if (properties != null) {
+      properties.remove(properties.size() - 1);
+    }
+  }
+
+  @Override
+  public boolean isReadOnly(ELContext context, Object base, Object property) {
+    if ((base == null) && PROPERTY_MSG.equals(property)) {
+      context.setPropertyResolved(true);
+      return true;
+    }
+
+    return false;
+  }
+
+  @Override
+  public void setValue(ELContext context, Object base, Object property, Object value) {
+    if ((base == null) && PROPERTY_MSG.equals(property)) {
+      throw new PropertyNotWritableException();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/NullTemplateLibrary.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/NullTemplateLibrary.java
new file mode 100644
index 0000000..60eae74
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/NullTemplateLibrary.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Null object implementation of TemplateLibrary.
+ */
+public final class NullTemplateLibrary implements TemplateLibrary {
+  public static final TemplateLibrary INSTANCE = new NullTemplateLibrary();
+
+  private final TagRegistry registry = new DefaultTagRegistry(ImmutableSet.<TagHandler>of());
+
+  private NullTemplateLibrary() {
+  }
+
+  public Uri getLibraryUri() {
+    return null;
+  }
+
+  public TagRegistry getTagRegistry() {
+    return registry;
+  }
+
+  public boolean isSafe() {
+    return false;
+  }
+
+  public String serialize() {
+    return null;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TagRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TagRegistry.java
new file mode 100644
index 0000000..0e43855
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TagRegistry.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.w3c.dom.Element;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * A registry of custom tag handlers, keyed by a combination of namespace URL
+ * and tag name.
+ */
+@ImplementedBy(DefaultTagRegistry.class)
+public interface TagRegistry {
+
+  public TagHandler getHandlerFor(Element element);
+
+  public TagHandler getHandlerFor(NSName name);
+
+  /**
+   * A namespace-name pair used as Hash key for handler lookups.
+   */
+  public static class NSName {
+    private final String namespaceUri;
+    private final String localName;
+    private final int hash;
+
+    public NSName(String namespaceUri, String localName) {
+      this.namespaceUri = namespaceUri;
+      this.localName = localName;
+      hash = (namespaceUri.hashCode() * 37) ^ localName.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return namespaceUri + ':' + localName;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) { return true; }
+      if (!(obj instanceof NSName)) { return false; }
+      NSName nsn = (NSName) obj;
+      return namespaceUri.equals(nsn.namespaceUri) && localName.equals(nsn.localName);
+    }
+
+    @Override
+    public int hashCode() {
+      return hash;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java
new file mode 100644
index 0000000..a235d26
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateContext.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.w3c.dom.Node;
+
+import java.util.Collection;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+/**
+ * Context for processing a single template.
+ */
+public class TemplateContext {
+  private final Map<String, ? extends Object> top;
+  private final Gadget gadget;
+
+  private Object cur = null;
+  // TODO: support unique Id
+  private Map<String, ? extends Object> context = ImmutableMap.of();
+  private Map<String, Object> myMap = null;
+  private Node templateRoot;
+  private Map<Object, TemplateResource> resources = Maps.newLinkedHashMap();
+
+  public TemplateContext(Gadget gadget, Map<String, ? extends Object> top) {
+    this.gadget = gadget;
+    this.top = top;
+    this.cur = top;
+  }
+
+  public Map<String, ? extends Object> getTop() {
+    return top;
+  }
+
+  public Object getCur() {
+    return cur;
+  }
+
+  public Object setCur(Object data) {
+    Object oldCur = cur;
+    cur = data;
+    return oldCur;
+  }
+
+  public Map<String, ? extends Object> getContext() {
+    return context;
+  }
+
+  public Map<String, ? extends Object> setContext(Map<String, ? extends Object> newContext) {
+    Map<String, ? extends Object> oldContext = context;
+    context = newContext;
+    return oldContext;
+  }
+
+  public Map<String, Object> setMy(Map<String, Object> myMap) {
+    Map<String, Object> oldMy = this.myMap;
+    this.myMap = myMap;
+    return oldMy;
+  }
+
+  public Map<String, Object> getMy() {
+    return myMap;
+  }
+
+  public Gadget getGadget() {
+    return gadget;
+  }
+
+  public Node setTemplateRoot(Node root) {
+    Node oldRoot = this.templateRoot;
+    this.templateRoot = root;
+    return oldRoot;
+  }
+
+  public Node getTemplateRoot() {
+    return this.templateRoot;
+  }
+
+  public void addResource(Object key, TemplateResource resource) {
+    if (!resources.containsKey(key)) {
+      resources.put(key, resource);
+    }
+  }
+
+  public Collection<TemplateResource> getResources() {
+    return resources.values();
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java
new file mode 100644
index 0000000..aa5c2f4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateELResolver.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import java.beans.FeatureDescriptor;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.el.ELContext;
+import javax.el.ELResolver;
+import javax.el.PropertyNotWritableException;
+import javax.el.ValueExpression;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * ELResolver used to process OpenSocial templates.  Provides three variables:
+ * <ul>
+ * <li>"Top": Global values </li>
+ * <li>"Cur": Current template variable</li>
+ * <li>"Context": Miscellaneous contextual information</li>
+ */
+public class TemplateELResolver extends ELResolver {
+  public static final String PROPERTY_TOP = "Top";
+  public static final String PROPERTY_CONTEXT = "Context";
+  public static final String PROPERTY_CUR = "Cur";
+  public static final String PROPERTY_MY = "My";
+
+  private static final Set<String> TOP_LEVEL_PROPERTIES =
+    ImmutableSet.of(PROPERTY_TOP, PROPERTY_CONTEXT, PROPERTY_CUR, PROPERTY_MY);
+
+  private final TemplateContext templateContext;
+
+  public TemplateELResolver(TemplateContext templateContext) {
+    this.templateContext = templateContext;
+  }
+
+  @Override
+  public Class<?> getCommonPropertyType(ELContext context, Object base) {
+    if (base == null) {
+      return String.class;
+    }
+
+    return null;
+  }
+
+  @Override
+  public Iterator<FeatureDescriptor> getFeatureDescriptors(ELContext context,
+      Object base) {
+    return null;
+  }
+
+  @Override
+  public Class<?> getType(ELContext context, Object base, Object property) {
+    // TODO: implement
+    return null;
+  }
+
+  @Override
+  public Object getValue(ELContext context, Object base, Object property) {
+    if (base == null) {
+      if (TOP_LEVEL_PROPERTIES.contains(property)) {
+        context.setPropertyResolved(true);
+        if (PROPERTY_TOP.equals(property)) {
+          return templateContext.getTop();
+        } else if (PROPERTY_CONTEXT.equals(property)) {
+          return templateContext.getContext();
+        } else if (PROPERTY_MY.equals(property)) {
+          return templateContext.getMy();
+        } else {
+          return templateContext.getCur();
+        }
+      }
+
+      // Check variables.
+      if (property instanceof String) {
+        // Workaround for inability of Jasper-EL resolvers to access VariableMapper
+        ELContext elContext = (ELContext)context.getContext(TemplateContext.class);
+        ValueExpression valueExp = elContext.getVariableMapper().resolveVariable((String) property);
+        if (valueExp != null) {
+          context.setPropertyResolved(true);
+          return valueExp.getValue(context);
+        }
+      }
+
+      // Check ${Cur} next.
+      Object cur = templateContext.getCur();
+      // Resolve through "cur" as if it were a value - if "isPropertyResolved()"
+      // is true, it was handled
+      if (cur != null) {
+        Object value = context.getELResolver().getValue(context, cur, property);
+        if (context.isPropertyResolved()) {
+          if (value != null) {
+            return value;
+          } else {
+            context.setPropertyResolved(false);
+          }
+        }
+      }
+
+      // Check ${My} next.
+      Map<String, ? extends Object> scope = templateContext.getMy();
+      if (scope != null && scope.containsKey(property)) {
+        context.setPropertyResolved(true);
+        return scope.get(property);
+      }
+
+      // Look at ${Top} context last.
+      scope = templateContext.getTop();
+      if (scope != null && scope.containsKey(property)) {
+        context.setPropertyResolved(true);
+        return scope.get(property);
+      }
+    }
+
+    return null;
+  }
+
+  @Override
+  public boolean isReadOnly(ELContext context, Object base, Object property) {
+    if (base == null && TOP_LEVEL_PROPERTIES.contains(property)) {
+      context.setPropertyResolved(true);
+      return true;
+    }
+
+    return false;
+  }
+
+  @Override
+  public void setValue(ELContext context, Object base, Object property, Object value) {
+    if (base == null && TOP_LEVEL_PROPERTIES.contains(property)) {
+      throw new PropertyNotWritableException();
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateLibrary.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateLibrary.java
new file mode 100644
index 0000000..0e36842
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateLibrary.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.common.uri.Uri;
+
+/**
+ * A Template Library is a collection of tag handlers, and any necessary
+ * assets (CSS and Javascript).
+ */
+public interface TemplateLibrary {
+
+  /**
+   * @return a registry of tags in this library.
+   */
+  TagRegistry getTagRegistry();
+
+  Uri getLibraryUri();
+
+  boolean isSafe();
+
+  String serialize();
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateLibraryFactory.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateLibraryFactory.java
new file mode 100644
index 0000000..3c6f734
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateLibraryFactory.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.w3c.dom.Element;
+
+import com.google.inject.Inject;
+
+/**
+ * Factory for template libraries.
+ */
+public class TemplateLibraryFactory {
+  private static final String PARSED_XML_CACHE = "parsedXml";
+
+  private final RequestPipeline pipeline;
+  private final Cache<String, Element> parsedXmlCache;
+
+  @Inject
+  public TemplateLibraryFactory(RequestPipeline pipeline, CacheProvider cacheProvider) {
+    this.pipeline = pipeline;
+    // Support null cacheProvider only for testing
+    if (cacheProvider == null) {
+      this.parsedXmlCache = null;
+    } else {
+      this.parsedXmlCache = cacheProvider.createCache(PARSED_XML_CACHE);
+    }
+  }
+
+  public TemplateLibrary loadTemplateLibrary(GadgetContext context, Uri uri) throws GadgetException {
+    HttpRequest request = new HttpRequest(uri).setSecurityToken( new AnonymousSecurityToken( "", 0L, context.getUrl().toString()));
+    // 5 minute TTL.
+    request.setCacheTtl(300);
+    HttpResponse response = pipeline.execute(request);
+    if (response.getHttpStatusCode() != HttpResponse.SC_OK) {
+      int retcode = response.getHttpStatusCode();
+      if (retcode == HttpResponse.SC_INTERNAL_SERVER_ERROR) {
+        // Convert external "internal error" to gateway error:
+        retcode = HttpResponse.SC_BAD_GATEWAY;
+      }
+      throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
+          "Unable to retrieve template library xml. HTTP error " +
+          response.getHttpStatusCode(), retcode);
+    }
+
+    String content = response.getResponseAsString();
+    try {
+      String key = null;
+      Element element = null;
+      if (!context.getIgnoreCache()) {
+        key = HashUtil.checksum(CharsetUtil.getUtf8Bytes(content));
+        element = parsedXmlCache.getElement(key);
+      }
+
+      if (element == null) {
+        element = XmlUtil.parse(content);
+        if (key != null) {
+          parsedXmlCache.addElement(key, element);
+        }
+      }
+
+      return new XmlTemplateLibrary(uri, element, content);
+    } catch (XmlException e) {
+      throw new GadgetException(GadgetException.Code.MALFORMED_XML_DOCUMENT, e,
+          HttpResponse.SC_BAD_REQUEST);
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateModule.java
new file mode 100644
index 0000000..c955ae4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateModule.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+
+import org.apache.shindig.gadgets.templates.tags.FlashTagHandler;
+import org.apache.shindig.gadgets.templates.tags.HtmlTagHandler;
+import org.apache.shindig.gadgets.templates.tags.IfTagHandler;
+import org.apache.shindig.gadgets.templates.tags.RenderTagHandler;
+import org.apache.shindig.gadgets.templates.tags.RepeatTagHandler;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.apache.shindig.gadgets.templates.tags.VarTagHandler;
+import org.apache.shindig.gadgets.templates.tags.VariableTagHandler;
+
+/**
+ * Guice Module to provide Template-specific classes
+ */
+public class TemplateModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(TemplateProcessor.class).to(DefaultTemplateProcessor.class);
+    bindTagHandlers();
+  }
+
+  /* No need to subclass.
+     You can add the same construct in your own modules to register your own tag handler.. */
+  protected void bindTagHandlers() {
+    Multibinder<TagHandler> tagBinder = Multibinder.newSetBinder(binder(), TagHandler.class);
+    tagBinder.addBinding().to(HtmlTagHandler.class);
+    tagBinder.addBinding().to(IfTagHandler.class);
+    tagBinder.addBinding().to(RenderTagHandler.class);
+    tagBinder.addBinding().to(RepeatTagHandler.class);
+    tagBinder.addBinding().to(FlashTagHandler.class);
+    tagBinder.addBinding().to(VariableTagHandler.class);
+    tagBinder.addBinding().to(VarTagHandler.class);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateParserException.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateParserException.java
new file mode 100644
index 0000000..49c96f7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateParserException.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.gadgets.GadgetException;
+
+/**
+ * Exceptions for Gadget Template parsing.
+ */
+public class TemplateParserException extends GadgetException {
+  /**
+   * @param message
+   */
+  public TemplateParserException(String message) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, message);
+  }
+
+  public TemplateParserException(XmlException e) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, e);
+  }
+
+  public TemplateParserException(String message, XmlException e) {
+    super(GadgetException.Code.MALFORMED_XML_DOCUMENT, message, e);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java
new file mode 100644
index 0000000..0528b0a
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateProcessor.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.apache.shindig.common.Nullable;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import javax.el.ELResolver;
+
+/**
+ * A Template Processor can process templates and evaluate expressions.
+ */
+public interface TemplateProcessor {
+
+  /**
+   * Process an entire template.
+   *
+   * @param template the DOM template, typically a script element
+   * @param templateContext a template context providing top-level
+   *     variables
+   * @param globals ELResolver providing global variables other
+   *     than those in the templateContext
+   * @return a document fragment with the resolved content
+   */
+  DocumentFragment processTemplate(Element template,
+      TemplateContext templateContext, ELResolver globals, TagRegistry registry);
+
+
+  /**
+   * @return the current template context.
+   */
+  TemplateContext getTemplateContext();
+
+  /**
+   * Process the children of an element or document.
+   * @param result the node to which results should be appended
+   * @param source the node whose children should be processed
+   */
+  void processChildNodes(Node result, Node source);
+
+  void processRepeat(Node result, Element element, Iterable<?> dataList,
+      Runnable onEachLoop);
+
+    /**
+   *  Evaluates an expression within the scope of this processor's context.
+   *  @param expression The String expression
+   *  @param type Expected result type
+   *  @param defaultValue Default value to return
+   */
+  <T> T evaluate(String expression, Class<T> type, @Nullable T defaultValue);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateResource.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateResource.java
new file mode 100644
index 0000000..1064089
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/TemplateResource.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+/**
+ * Encapsulation of a single resource imported by a library for template
+ * execution.
+ */
+public final class TemplateResource {
+  private final String content;
+  private final Type type;
+  private final boolean isSafe;
+
+  public enum Type { JAVASCRIPT, STYLE }
+
+    /**
+   * Create a Javascript resource.
+   * @param javascript the script content
+   * @param library the library that is the source of the script
+   */
+  public static TemplateResource newJavascriptResource(String javascript, TemplateLibrary library) {
+    return new TemplateResource(javascript, Type.JAVASCRIPT, library.isSafe());
+  }
+
+  /**
+   * Create a CSS resource.
+   * @param style the CSS content
+   * @param library the library that is the source of the content
+   */
+  public static TemplateResource newStyleResource(String style, TemplateLibrary library) {
+    return new TemplateResource(style, Type.STYLE, library.isSafe());
+  }
+
+  private TemplateResource(String content, Type type, boolean isSafe) {
+    this.content = content;
+    this.type = type;
+    this.isSafe = isSafe;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public boolean isSafe() {
+    return isSafe;
+  }
+
+  @Override
+  public String toString() {
+    return "<" + type + '>' + content + "</" + type + '>';
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/XmlTemplateLibrary.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/XmlTemplateLibrary.java
new file mode 100644
index 0000000..3b4d795
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/XmlTemplateLibrary.java
@@ -0,0 +1,308 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSet.Builder;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.apache.shindig.gadgets.templates.tags.TemplateBasedTagHandler;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Set;
+
+/**
+ * An Object representing a Library of Template-based custom OSML tags.
+ */
+public class XmlTemplateLibrary implements TemplateLibrary {
+
+  public static final String TAG_ATTRIBUTE = "tag";
+  public static final String NAMESPACE_TAG = "Namespace";
+  public static final String TEMPLATE_TAG = "Template";
+  public static final String STYLE_TAG = "Style";
+  public static final String JAVASCRIPT_TAG = "JavaScript";
+  public static final String TEMPLATEDEF_TAG = "TemplateDef";
+
+  private final Uri libraryUri;
+  private final String source;
+  private final boolean safe;
+  private final TagRegistry registry;
+  private String nsPrefix;
+  private String nsUri;
+  private String style;
+  private String javaScript;
+  private final Set<TemplateResource> libraryResources;
+
+  /**
+   * @param uri URI of the template library
+   * @param root Element representing the Templates tag of this library
+   */
+  public XmlTemplateLibrary(Uri uri, Element root, String source)
+      throws GadgetException {
+    this(uri, root, source, false);
+  }
+
+  /**
+   * @param uri URI of the template library
+   * @param root Element representing the Templates tag of this library
+   * @param safe Is this library exempt from being sanitized?
+   */
+  public XmlTemplateLibrary(Uri uri, Element root, String source, boolean safe)
+      throws GadgetException {
+    this.libraryUri = uri;
+    this.source = source;
+    this.safe = safe;
+    this.registry = new DefaultTagRegistry(parseLibraryDocument(root));
+    ImmutableSet.Builder<TemplateResource> resources = ImmutableSet.builder();
+    if (style != null) {
+      resources.add(TemplateResource.newStyleResource(style, this));
+    }
+    if (javaScript != null) {
+      resources.add(TemplateResource.newJavascriptResource(javaScript, this));
+    }
+
+    this.libraryResources = resources.build();
+  }
+
+  /**
+   * @return a registry of tags in this library.
+   */
+  public TagRegistry getTagRegistry() {
+    return registry;
+  }
+
+  /**
+   * @return the URI from which the library was loaded.  (This is not the
+   * namespace of tags in the library.)
+   */
+  public Uri getLibraryUri() {
+    return libraryUri;
+  }
+
+  /**
+   * @return this library is safe and its content doesn't need to be sanitized.
+   */
+  public boolean isSafe() {
+    return safe;
+  }
+
+  /**
+   * @return This library as XML source.
+   */
+  public String serialize() {
+    return source;
+  }
+
+  /**
+   * Creates a tag handler wrapping an element.  By default, creates
+   * a {@link TemplateBasedTagHandler}.  Override this to create custom
+   * tag handlers.
+   */
+  protected TagHandler createTagHandler(Element template, String namespaceUri,
+      String localName) {
+    return new TemplateBasedTagHandler(template, namespaceUri, localName);
+  }
+
+  private Set<TagHandler> parseLibraryDocument(Element root) throws GadgetException {
+    ImmutableSet.Builder<TagHandler> handlers = ImmutableSet.builder();
+
+    NodeList nodes = root.getChildNodes();
+    for (int i = 0; i < nodes.getLength(); i++) {
+      Node node = nodes.item(i);
+      if (!(node instanceof Element)) {
+        continue;
+      }
+
+      Element element = (Element) node;
+      if (NAMESPACE_TAG.equals(element.getLocalName())) {
+        processNamespace(element);
+      } else if (STYLE_TAG.equals(element.getLocalName())) {
+        processStyle(element);
+      } else if (JAVASCRIPT_TAG.equals(element.getLocalName())) {
+        processJavaScript(element);
+      } else if (TEMPLATE_TAG.equals(element.getLocalName())) {
+        processTemplate(handlers, element);
+      } else if (TEMPLATEDEF_TAG.equals(element.getLocalName())) {
+        processTemplateDef(handlers, element);
+      }
+    }
+
+    return handlers.build();
+  }
+
+  private void processTemplateDef(Builder<TagHandler> handlers, Element defElement)
+      throws TemplateParserException {
+    Attr tagAttribute = defElement.getAttributeNode(TAG_ATTRIBUTE);
+    if (tagAttribute == null) {
+      throw new TemplateParserException("Missing tag attribute on TemplateDef");
+    }
+
+    ImmutableSet.Builder<TemplateResource> resources = ImmutableSet.builder();
+
+    Element scriptElement = (Element) DomUtil.getFirstNamedChildNode(defElement, JAVASCRIPT_TAG);
+    if (scriptElement != null) {
+      resources.add(TemplateResource.newJavascriptResource(scriptElement.getTextContent(), this));
+    }
+
+    Element styleElement = (Element) DomUtil.getFirstNamedChildNode(defElement, STYLE_TAG);
+    if (styleElement != null) {
+      resources.add(TemplateResource.newStyleResource(styleElement.getTextContent(), this));
+    }
+
+    Element templateElement = (Element) DomUtil.getFirstNamedChildNode(defElement, TEMPLATE_TAG);
+    TagHandler handler = createHandler(tagAttribute.getNodeValue(), templateElement,
+        resources.build());
+    if (handler != null) {
+      handlers.add(handler);
+    }
+  }
+
+  private void processTemplate(Builder<TagHandler> handlers, Element templateElement)
+      throws TemplateParserException {
+    Attr tagAttribute = templateElement.getAttributeNode(TAG_ATTRIBUTE);
+    if (tagAttribute == null) {
+      throw new TemplateParserException("Missing tag attribute on Template");
+    }
+
+    TagHandler handler = createHandler(tagAttribute.getNodeValue(), templateElement,
+        ImmutableSet.<TemplateResource>of());
+    if (handler != null) {
+      handlers.add(handler);
+    }
+  }
+
+  private void processStyle(Element element) {
+    if (style == null) {
+      style = element.getTextContent();
+    } else {
+      style = style + '\n' + element.getTextContent();
+    }
+  }
+
+  private void processJavaScript(Element element) {
+    if (javaScript == null) {
+      javaScript = element.getTextContent();
+    } else {
+      javaScript = javaScript + '\n' + element.getTextContent();
+    }
+  }
+
+  private void processNamespace(Element namespaceNode) throws TemplateParserException {
+    if ((nsPrefix != null) || (nsUri != null)) {
+      throw new TemplateParserException("Duplicate Namespace elements");
+    }
+
+    nsPrefix = namespaceNode.getAttribute("prefix");
+    if ("".equals(nsPrefix)) {
+      throw new TemplateParserException("Missing prefix attribute on Namespace");
+    }
+
+    nsUri = namespaceNode.getAttribute("url");
+    if ("".equals(nsUri)) {
+      throw new TemplateParserException("Missing url attribute on Namespace");
+    }
+  }
+
+  private TagHandler createHandler(String tagName, Element template,
+      Set<TemplateResource> resources)
+      throws TemplateParserException {
+    String [] nameParts = StringUtils.splitPreserveAllTokens(tagName, ':');
+    // At this time, we only support namespaced tags
+    if (nameParts.length != 2) {
+      return null;
+    }
+    String namespaceUri = template.lookupNamespaceURI(nameParts[0]);
+    if (!nsPrefix.equals(nameParts[0]) || !nsUri.equals(namespaceUri)) {
+      throw new TemplateParserException(
+          "Can't create tags in undeclared namespace: " + nameParts[0]);
+    }
+
+    if (isSafe()) {
+      bypassTemplateSanitization(template);
+    }
+
+    return new LibraryTagHandler(
+        createTagHandler(template, namespaceUri, nameParts[1]),
+        resources);
+  }
+
+  /**
+   * For "safe" libraries, bypass sanitization.  Sanitization should
+   * be bypassed on each element in the tree, but not on the whole
+   * tree (false, not true, in the call to bypassSanitization() below),
+   * since os:Render elements will insert unsafe content.
+   */
+  private void bypassTemplateSanitization(Element template) {
+    SanitizingGadgetRewriter.bypassSanitization(template, false);
+    NodeList children = template.getChildNodes();
+    for (int i = 0; i < children.getLength(); i++) {
+      Node node = children.item(i);
+      if (node instanceof Element) {
+        bypassTemplateSanitization((Element) node);
+      }
+    }
+  }
+
+  /**
+   * TagHandler delegate reponsible for adding necessary tag resources
+   * as each tag gets processed.
+   */
+  private class LibraryTagHandler implements TagHandler {
+    private final TagHandler tagHandler;
+    private final Set<TemplateResource> tagResources;
+
+    public LibraryTagHandler(TagHandler tagHandler, Set<TemplateResource> resources) {
+      this.tagHandler = tagHandler;
+      tagResources = resources;
+    }
+
+    public String getNamespaceUri() {
+      return tagHandler.getNamespaceUri();
+    }
+
+    public String getTagName() {
+      return tagHandler.getTagName();
+    }
+
+    public void process(Node result, Element tag, TemplateProcessor processor) {
+      // Add all template resources and library resources.  Use the resource
+      // instance as its own key, since we're careful to create the resource
+      // objects once.  NOTE: this assumes that TemplateResource uses instance
+      // equality, not value equality.
+      for (TemplateResource resource : tagResources) {
+        processor.getTemplateContext().addResource(resource, resource);
+      }
+
+      for (TemplateResource resource : libraryResources) {
+        processor.getTemplateContext().addResource(resource, resource);
+      }
+
+      tagHandler.process(result, tag, processor);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/AbstractTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/AbstractTagHandler.java
new file mode 100644
index 0000000..e6b60f7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/AbstractTagHandler.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+
+/**
+ * Abstract implementation of TagHandler, provides convenience methods
+ * for resolving values in context.
+ */
+public abstract class AbstractTagHandler implements TagHandler {
+
+  private final String tagName;
+  private final String namespaceUri;
+
+  /**
+   * Create the tag handler instance.
+   * @param namespaceUri the namespace of element this tag parses.
+   * @param tagName the local name of the element this tag parses.
+   */
+  public AbstractTagHandler(String namespaceUri, String tagName) {
+    this.tagName = tagName;
+    this.namespaceUri = namespaceUri;
+  }
+
+  public String getTagName() {
+    return tagName;
+  }
+
+  public String getNamespaceUri() {
+    return namespaceUri;
+  }
+
+  /**
+   * Returns the value of a tag attribute, evaluating any contained EL
+   * expressions if necessary.
+   *
+   * @param <T> the type of the value
+   * @param tag the element
+   * @param name the attribute name
+   * @param processor the template processor
+   * @param type the type of the value
+   * @return the value of the attribute, or null if the attribute is not
+   *     present.
+   */
+  protected final <T> T getValueFromTag(Element tag, String name,
+      TemplateProcessor processor, Class<T> type) {
+    if (tag.hasAttribute(name)) {
+      return processor.evaluate(tag.getAttribute(name), type, null);
+    } else {
+      return null;
+    }
+  }
+
+  protected final DocumentFragment processChildren(Element tag,
+      TemplateProcessor processor) {
+    DocumentFragment fragment = tag.getOwnerDocument().createDocumentFragment();
+    processor.processChildNodes(fragment, tag);
+    return fragment;
+  }
+
+  /**
+   * Create a text node with proper escaping.
+   */
+  protected final void appendTextNode(Node parent, String text) {
+    if (text == null || "".equals(text)) {
+      return;
+    }
+
+    try {
+      StringBuilder sb = new StringBuilder(text.length());
+      HtmlSerialization.printEscapedText(text, sb);
+      parent.appendChild(parent.getOwnerDocument().createTextNode(sb.toString()));
+    } catch (IOException ioe) {
+      throw new RuntimeException(ioe);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/AbstractTagRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/AbstractTagRegistry.java
new file mode 100644
index 0000000..c14dbbc
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/AbstractTagRegistry.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.w3c.dom.Element;
+
+/**
+ * Base class for handling tags
+ */
+public abstract class AbstractTagRegistry implements TagRegistry {
+
+  public final TagHandler getHandlerFor(Element element) {
+    if (element.getNamespaceURI() == null) {
+      return null;
+    }
+    return getHandlerFor(new NSName(element.getNamespaceURI(), element.getLocalName()));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/CompositeTagRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/CompositeTagRegistry.java
new file mode 100644
index 0000000..67361b8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/CompositeTagRegistry.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TagRegistry;
+
+import java.util.Collection;
+
+/**
+ * Tag registry that supports multiple tags.
+ */
+public class CompositeTagRegistry extends AbstractTagRegistry {
+  private final Collection<? extends TagRegistry> registries;
+
+  public CompositeTagRegistry(Collection<? extends TagRegistry> registries) {
+    this.registries = registries;
+  }
+
+  public TagHandler getHandlerFor(NSName name) {
+    TagHandler handler;
+    for (TagRegistry registry : registries) {
+      handler = registry.getHandlerFor(name);
+      if (handler != null) {
+        return handler;
+      }
+    }
+    return null;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/DefaultTagRegistry.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/DefaultTagRegistry.java
new file mode 100644
index 0000000..0d11237
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/DefaultTagRegistry.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+/**
+ * A registry of custom tag handlers, keyed by a combination of namespace URL
+ * and tag name.
+ */
+public class DefaultTagRegistry extends AbstractTagRegistry {
+
+  private final Map<NSName, TagHandler> handlers = Maps.newHashMap();
+
+  @Inject
+  public DefaultTagRegistry(Set<TagHandler> handlers) {
+    for (TagHandler handler : handlers) {
+      this.handlers.put(new NSName(handler.getNamespaceUri(), handler.getTagName()), handler);
+    }
+  }
+
+  public TagHandler getHandlerFor(NSName name) {
+    return handlers.get(name);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandler.java
new file mode 100644
index 0000000..365b56b
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandler.java
@@ -0,0 +1,418 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.json.JSONObject;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.w3c.dom.Document;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.io.IOException;
+
+/**
+ * Implement the os:Flash tag
+ */
+public class FlashTagHandler extends AbstractTagHandler {
+
+  static final String SWFOBJECT = "swfobject";
+  static final String TAG_NAME = "Flash";
+
+  private final BeanJsonConverter beanConverter;
+  private final FeatureRegistry featureRegistry;
+  private final String flashMinVersion;
+
+  /**
+   * Used to generate id's for generated tags and functions
+   */
+  final AtomicLong idGenerator = new AtomicLong();
+  private static final String ALT_CONTENT_PREFIX = "os_xFlash_alt_";
+
+  @Inject
+  public FlashTagHandler(BeanJsonConverter beanConverter, FeatureRegistry featureRegistry,
+      @Named("shindig.template-rewrite.extension-tag-namespace") String namespace,
+      @Named("shindig.flash.min-version") String flashMinVersion) {
+    super(namespace, TAG_NAME);
+    this.beanConverter = beanConverter;
+    this.featureRegistry = featureRegistry;
+    this.flashMinVersion = flashMinVersion;
+  }
+
+  public void process(Node result, Element tag, TemplateProcessor processor) {
+    SwfObjectConfig config;
+    try {
+      config = getSwfConfig(tag, processor);
+    } catch (RuntimeException re) {
+      // Record the processing error into the output
+      Element err = result.getOwnerDocument().createElement("span");
+      err.setTextContent("Failed to process os:Flash tag: " +
+          StringEscapeUtils.escapeHtml4(re.getMessage()));
+      result.appendChild(err);
+      return;
+    }
+
+    // Bind the security token to the flashvars if its available
+    String st = processor.getTemplateContext().getGadget()
+        .getContext().getParameter("st");
+    if (!Strings.isNullOrEmpty(st)) {
+      String stVar = "st=" + Utf8UrlCoder.encode(st);
+      if (Strings.isNullOrEmpty(config.flashvars)) {
+        config.flashvars = stVar;
+      } else {
+        config.flashvars += '&' + stVar;
+      }
+    }
+
+    // Restrict the content if sanitization is enabled
+    if (processor.getTemplateContext().getGadget().sanitizeOutput()) {
+      config.allowscriptaccess = SwfObjectConfig.ScriptAccess.never;
+      config.swliveconnect = false;
+      config.allownetworking = SwfObjectConfig.NetworkAccess.internal;
+      // TODO - Implement container control over autoplay on views
+    }
+
+    // Create a div wrapper around the provided alternate content
+    Element altHolder = result.getOwnerDocument().createElement("div");
+    String altContentId = ALT_CONTENT_PREFIX + idGenerator.incrementAndGet();
+    altHolder.setAttribute("id", altContentId);
+    result.appendChild(altHolder);
+
+    // Add the alternate content to the holder
+    NodeList alternateContent = tag.getChildNodes();
+    if (alternateContent.getLength() > 0) {
+      processor.processChildNodes(altHolder, tag);
+    }
+
+    // Create the call to swfobject
+    String swfObjectCall = buildSwfObjectCall(config, altContentId);
+    Element script = result.getOwnerDocument().createElement("script");
+    script.setAttribute("type", "text/javascript");
+    result.appendChild(script);
+
+    if (config.play == SwfObjectConfig.Play.immediate) {
+      // Call swfobject immediately
+      script.setTextContent(swfObjectCall);
+    } else {
+      // Add onclick handler to trigger call to swfobject
+      script.setTextContent("function " + altContentId + "(){ " + swfObjectCall + " }");
+      altHolder.setAttribute("onclick", altContentId + "()");
+    }
+
+    // Bypass sanitization for the holder tag and the call to swfobject
+    SanitizingGadgetRewriter.bypassSanitization(altHolder, false);
+    SanitizingGadgetRewriter.bypassSanitization(script, false);
+    ensureSwfobject(result.getOwnerDocument(), processor);
+  }
+
+  /**
+   * Generate the correctly parameterized Javascript call to swfobject
+   */
+  String buildSwfObjectCall(SwfObjectConfig config, String altContentId) {
+    try {
+      StringBuilder builder = new StringBuilder();
+      builder.append("swfobject.embedSWF(");
+      JsonSerializer.appendString(builder, config.swf.toString());
+      builder.append(",\"");
+      builder.append(altContentId);
+      builder.append("\",");
+      JsonSerializer.appendString(builder, config.width);
+      builder.append(',');
+      JsonSerializer.appendString(builder, config.height);
+      builder.append(",\"").append(flashMinVersion).append("\",");
+      builder.append("null,null,");
+      JsonSerializer.appendMap(builder, config.getParams());
+      builder.append(',');
+      JsonSerializer.appendMap(builder, config.getAttributes());
+      builder.append(");");
+      return builder.toString();
+    } catch (IOException ioe) {
+      // Should not happen
+      throw new RuntimeException(ioe);
+    }
+
+  }
+
+  /**
+   * Read the swfconfig from the tag
+   */
+  SwfObjectConfig getSwfConfig(Element tag, TemplateProcessor processor) {
+    Map<String, String> params = getAllAttributesLowerCase(tag, processor);
+    return (SwfObjectConfig) beanConverter.convertToObject(new JSONObject(params),
+        SwfObjectConfig.class);
+  }
+
+  Map<String, String> getAllAttributesLowerCase(Element tag, TemplateProcessor processor) {
+    Map<String, String> result = Maps.newHashMap();
+    for (int i = 0; i < tag.getAttributes().getLength(); i++) {
+      Node attr = tag.getAttributes().item(i);
+      String attrName = attr.getNodeName().toLowerCase();
+      result.put(attrName, processor.evaluate(attr.getNodeValue(), String.class, null));
+    }
+    return result;
+  }
+
+  /**
+   * Ensure that the swfobject JS is inlined
+   */
+  void ensureSwfobject(Document doc, TemplateProcessor processor) {
+    // TODO: This should probably be a function of the rewriter.
+    Element head = (Element) DomUtil.getFirstNamedChildNode(doc.getDocumentElement(), "head");
+    NodeList childNodes = head.getChildNodes();
+    for (int i = 0; i < childNodes.getLength(); i++) {
+      Node node = childNodes.item(i);
+      if (node.getUserData(SWFOBJECT) != null) {
+        return;
+      }
+    }
+    Element swfobject = doc.createElement("script");
+    swfobject.setAttribute("type", "text/javascript");
+    List<FeatureResource> resources =
+        featureRegistry.getFeatureResources(processor.getTemplateContext().getGadget().getContext(),
+          ImmutableSet.of(SWFOBJECT), null).getResources();
+    for (FeatureResource resource : resources) {
+      // Emits all content for feature SWFOBJECT, which has no downstream dependencies.
+      swfobject.setTextContent(resource.getContent());
+    }
+    swfobject.setUserData(SWFOBJECT, SWFOBJECT, null);
+    head.appendChild(swfobject);
+    SanitizingGadgetRewriter.bypassSanitization(swfobject, false);
+  }
+
+  /**
+   * Definition of the flash tag and mapping to swfobject structures
+   */
+  public static class SwfObjectConfig {
+    String id;
+    Uri swf;
+    String width = "100px";
+    String height = "100px";
+    String name;
+    String clazz;
+    Boolean menu;
+
+    public static enum Play { immediate, onclick }
+    Play play = Play.immediate;
+
+    public static enum Scale { showall, noborder, exactfit, noscale }
+    Scale scale;
+
+    public static enum WMode { window, opaque, transparent, direct, gpu}
+    WMode wmode;
+
+    Boolean devicefont;
+    Boolean swliveconnect;
+
+    public static enum ScriptAccess { always, samedomain, never }
+    ScriptAccess allowscriptaccess;
+
+    Boolean loop;
+
+    public static enum Quality { best, high, medium, autohigh, autolow, low }
+    Quality quality;
+
+    public static enum Align { middle, left, right, top, bottom }
+    Align align;
+
+    public static enum SAlign { tl, tr, bl, br, l, t, r, b}
+    SAlign salign;
+
+    String bgcolor;
+
+    Boolean seamlesstabbing;
+
+    Boolean allowfullscreen;
+
+    public static enum NetworkAccess { all, internal, none }
+    NetworkAccess allownetworking;
+
+    String flashvars;
+
+    public void setId(String id) {
+      this.id = id;
+    }
+
+    public void setSwf(Uri swf) {
+      this.swf = swf;
+    }
+
+    public void setWidth(String width) {
+      this.width = width;
+    }
+
+    public void setHeight(String height) {
+      this.height = height;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    public void setClass(String clazz) {
+      this.clazz = clazz;
+    }
+
+    public void setPlay(Play play) {
+      this.play = play;
+    }
+
+    public void setMenu(Boolean menu) {
+      this.menu = menu;
+    }
+
+    public void setScale(Scale scale) {
+      this.scale = scale;
+    }
+
+    public void setWmode(WMode wmode) {
+      this.wmode = wmode;
+    }
+
+    public void setDevicefont(Boolean devicefont) {
+      this.devicefont = devicefont;
+    }
+
+    public void setSwliveconnect(Boolean swliveconnect) {
+      this.swliveconnect = swliveconnect;
+    }
+
+    public void setAllowscriptaccess(ScriptAccess allowscriptaccess) {
+      this.allowscriptaccess = allowscriptaccess;
+    }
+
+    public void setLoop(Boolean loop) {
+      this.loop = loop;
+    }
+
+    public void setQuality(Quality quality) {
+      this.quality = quality;
+    }
+
+    public void setAlign(Align align) {
+      this.align = align;
+    }
+
+    public void setSalign(SAlign salign) {
+      this.salign = salign;
+    }
+
+    public void setBgcolor(String bgcolor) {
+      this.bgcolor = bgcolor;
+    }
+
+    public void setSeamlesstabbing(Boolean seamlesstabbing) {
+      this.seamlesstabbing = seamlesstabbing;
+    }
+
+    public void setAllowfullscreen(Boolean allowfullscreen) {
+      this.allowfullscreen = allowfullscreen;
+    }
+
+    public void setAllownetworking(NetworkAccess allownetworking) {
+      this.allownetworking = allownetworking;
+    }
+
+    public void setFlashvars(String flashvars) {
+      this.flashvars = flashvars;
+    }
+
+    public Map<String, Object> getParams() {
+
+      Map<String, Object> swfobjectParams = Maps.newLinkedHashMap();
+      if (loop != null) {
+        swfobjectParams.put("loop", loop);
+      }
+      if (menu != null) {
+        swfobjectParams.put("menu", menu);
+      }
+      if (quality != null) {
+        swfobjectParams.put("quality", quality);
+      }
+      if (scale != null) {
+        swfobjectParams.put("scale", scale);
+      }
+      if (salign != null) {
+        swfobjectParams.put("salign", salign);
+      }
+      if (wmode != null) {
+        swfobjectParams.put("wmode", wmode);
+      }
+      if (bgcolor != null) {
+        swfobjectParams.put("bgcolor", bgcolor);
+      }
+      if (swliveconnect != null) {
+        swfobjectParams.put("swliveconnect", swliveconnect);
+      }
+      if (flashvars != null) {
+        swfobjectParams.put("flashvars", flashvars);
+      }
+      if (devicefont != null) {
+        swfobjectParams.put("devicefont", devicefont);
+      }
+      if (allowscriptaccess != null) {
+        swfobjectParams.put("allowscriptaccess", allowscriptaccess);
+      }
+      if (seamlesstabbing != null) {
+        swfobjectParams.put("seamlesstabbing", seamlesstabbing);
+      }
+      if (allowfullscreen != null) {
+        swfobjectParams.put("allowfullscreen", allowfullscreen);
+      }
+      if (allownetworking != null) {
+        swfobjectParams.put("allownetworking", allownetworking);
+      }
+      return swfobjectParams;
+    }
+
+    public Map<String, Object> getAttributes() {
+      Map<String, Object> swfObjectAttrs = Maps.newLinkedHashMap();
+      if (id != null) {
+        swfObjectAttrs.put("id", id);
+      }
+      if (name != null) {
+        swfObjectAttrs.put("name", name);
+      }
+      if (clazz != null) {
+        swfObjectAttrs.put("styleclass", clazz);
+      }
+      if (align != null) {
+        swfObjectAttrs.put("align", align.toString());
+      }
+      return swfObjectAttrs;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/HtmlTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/HtmlTagHandler.java
new file mode 100644
index 0000000..b81f076
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/HtmlTagHandler.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.HtmlSerialization;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.IOException;
+
+import com.google.inject.Inject;
+
+/**
+ * A TagHandler for the &lt;os:Html code="..."/&gt; tag.
+ * The value of the @code attribute will be treated as HTML markup.
+ */
+public class HtmlTagHandler extends AbstractTagHandler {
+
+  static final String TAG_NAME = "Html";
+  static final String ATTR_CODE = "code";
+
+  private final GadgetHtmlParser parser;
+
+  @Inject
+  public HtmlTagHandler(GadgetHtmlParser parser) {
+    super(TagHandler.OPENSOCIAL_NAMESPACE, TAG_NAME);
+    this.parser = parser;
+  }
+
+  public void process(Node result, Element tag, TemplateProcessor processor) {
+    String code = getValueFromTag(tag, ATTR_CODE, processor, String.class);
+    if ((code == null) || "".equals(code)) {
+      return;
+    }
+
+    try {
+      parser.parseFragment(code, result);
+    } catch (GadgetException ge) {
+      try {
+        StringBuilder sb = new StringBuilder("Error: ");
+        HtmlSerialization.printEscapedText(ge.getMessage(), sb);
+        Node comment = result.getOwnerDocument().createComment(sb.toString());
+        result.appendChild(comment);
+      } catch (IOException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/IfTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/IfTagHandler.java
new file mode 100644
index 0000000..8dc6d66
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/IfTagHandler.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.inject.Inject;
+
+/**
+ * Handles the os:If tag for osml.
+ */
+public class IfTagHandler extends AbstractTagHandler {
+
+  static final String TAG_IF = "If";
+  static final String CONDITION_ATTR = "condition";
+
+  @Inject
+  public IfTagHandler() {
+    super(TagHandler.OPENSOCIAL_NAMESPACE, TAG_IF);
+  }
+
+  public void process(Node result, Element tag, TemplateProcessor processor) {
+    Boolean condition = getValueFromTag(tag, CONDITION_ATTR, processor, Boolean.class);
+    if (condition == null || !condition.booleanValue()) {
+      return;
+    }
+
+    // Condition succeeded, process all child nodes
+    processor.processChildNodes(result, tag);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/RenderTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/RenderTagHandler.java
new file mode 100644
index 0000000..560c8cf
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/RenderTagHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.gadgets.templates.ElementELResolver.ElementWrapper;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+import java.util.Map;
+
+import com.google.inject.Inject;
+
+/**
+ * Tag Handler for <os:Render/> tag.
+ */
+public class RenderTagHandler extends AbstractTagHandler {
+
+  public static final String DEFAULT_NAME = "Render";
+
+  public static final String ATTR_CONTENT = "content";
+
+  @Inject
+  public RenderTagHandler() {
+    super(TagHandler.OPENSOCIAL_NAMESPACE, DEFAULT_NAME);
+  }
+
+  public void process(Node result, Element tag, TemplateProcessor processor) {
+    Map<String, Object> myMap = processor.getTemplateContext().getMy();
+    if (myMap == null) {
+      return;
+    }
+
+    String content = tag.getAttribute(ATTR_CONTENT);
+    // No @content specified - move it all.
+    if ("".equals(content)) {
+      Node root = processor.getTemplateContext().getTemplateRoot();
+      if (root != null) {
+        for (Node child = root.getFirstChild(); child != null; child = child.getNextSibling()) {
+          result.appendChild(child.cloneNode(true));
+        }
+      }
+    } else {
+      Object value = myMap.get(content);
+      // TODO: for non-Elements, output errors
+      if (value instanceof ElementWrapper) {
+        copyChildren((ElementWrapper) value, result);
+      } else if (value instanceof List<?>) {
+        List<?> children = (List<?>) value;
+        for (Object probablyAnElement : children) {
+          if (probablyAnElement instanceof ElementWrapper) {
+            copyChildren((ElementWrapper) probablyAnElement, result);
+          }
+        }
+      }
+    }
+  }
+
+  private void copyChildren(ElementWrapper fromWrapper, Node to) {
+    Element from = fromWrapper.element;
+    for (Node child = from.getFirstChild(); child != null; child = child.getNextSibling()) {
+      to.appendChild(child.cloneNode(true));
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/RepeatTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/RepeatTagHandler.java
new file mode 100644
index 0000000..b8ee6e0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/RepeatTagHandler.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.inject.Inject;
+
+/**
+ * Implementation of the <os:Repeat> tag.
+ */
+public class RepeatTagHandler extends AbstractTagHandler {
+
+  static final String TAG_REPEAT = "Repeat";
+  static final String EXPRESSION_ATTR = "expression";
+  static final String IF_ATTR = "if";
+
+  @Inject
+  public RepeatTagHandler() {
+    super(TagHandler.OPENSOCIAL_NAMESPACE, TAG_REPEAT);
+  }
+
+  public void process(final Node result, final Element tag, final TemplateProcessor processor) {
+    Iterable<?> repeat = getValueFromTag(tag, EXPRESSION_ATTR, processor, Iterable.class);
+    if (repeat != null) {
+      final Attr ifAttribute = tag.getAttributeNode(IF_ATTR);
+
+      // On each iteration, process child nodes, after checking the value of the "if" attribute
+      processor.processRepeat(result, tag, repeat, new Runnable() {
+        public void run() {
+          if (ifAttribute != null) {
+            if (!processor.evaluate(ifAttribute.getValue(), Boolean.class, false)) {
+              return;
+            }
+          }
+
+          processor.processChildNodes(result, tag);
+        }
+      });
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/TagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/TagHandler.java
new file mode 100644
index 0000000..5ecc951
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/TagHandler.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * A Handler for custom tags in template markup.
+ */
+public interface TagHandler {
+
+  /**
+   * Namespace used by tags in the default Opensocial namespace.
+   */
+  public static final String OPENSOCIAL_NAMESPACE = "http://ns.opensocial.org/2008/markup";
+
+  /**
+   * @return the local name of the element this tag parses.
+   */
+  String getTagName();
+
+  /**
+   * @return the namespace of the element this tag parses.
+   */
+  String getNamespaceUri();
+
+  /**
+   * Processes the custom tag.
+   * @param result A Node to append output to.
+   * @param tag The Element reference to the tag, useful for inspecting
+   *     attributes and children
+   * @param processor A TemplateProcessor, used to evaluate expressions and render
+   *     sub-templates if needed.
+   */
+  void process(Node result, Element tag, TemplateProcessor processor);
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/TemplateBasedTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/TemplateBasedTagHandler.java
new file mode 100644
index 0000000..71da80f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/TemplateBasedTagHandler.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.gadgets.templates.ElementELResolver.ElementWrapper;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * TagHandler implemented by an declarative XML definition.
+ */
+public class TemplateBasedTagHandler extends AbstractTagHandler {
+
+  private final Element templateDefinition;
+
+  public TemplateBasedTagHandler(Element templateDefinition, String namespaceUri, String tagName) {
+    super(namespaceUri, tagName);
+    this.templateDefinition = templateDefinition;
+  }
+
+  public void process(Node result, Element tagInstance, TemplateProcessor processor) {
+    // Process the children of the tag
+    DocumentFragment processedContent = processChildren(tagInstance, processor);
+
+    // Save the old values of "My", "Cur", and the template root element,
+    // and update each
+    Map<String, Object> oldMy = processor.getTemplateContext().setMy(
+        computeMy(tagInstance, processedContent, processor));
+    Object oldCur = processor.getTemplateContext().setCur(null);
+    Node oldTemplateRoot = processor.getTemplateContext().setTemplateRoot(processedContent);
+
+    processTemplate(result, tagInstance, processor);
+
+    // And restore the template context
+    processor.getTemplateContext().setMy(oldMy);
+    processor.getTemplateContext().setCur(oldCur);
+    processor.getTemplateContext().setTemplateRoot(oldTemplateRoot);
+  }
+
+  /** Process the template content in the new EL state */
+  protected void processTemplate(Node result, Element tagInstance, TemplateProcessor processor) {
+    processor.processChildNodes(result, templateDefinition);
+  }
+
+  /**
+   * Compute the value of ${My} for this tag execution.
+   */
+  protected Map<String, Object> computeMy(Element tagInstance, Node processedContent,
+      TemplateProcessor processor) {
+    Map<String, Object> myMap = Maps.newHashMap();
+
+    NodeList children = processedContent.getChildNodes();
+
+    for (int i = 0;  i < children.getLength(); i++) {
+      Node child = children.item(i);
+      if (child instanceof Element) {
+        Element el = (Element) child;
+        String name = el.getLocalName();
+        // TODO: why???  There should always be a local name.
+        if (name == null) {
+          name = el.getNodeName();
+        }
+
+        ElementWrapper wrapper = new ElementWrapper(el);
+        Object previous = myMap.get(name);
+        if (previous == null) {
+          myMap.put(name, wrapper);
+        } else if (previous instanceof ElementWrapper) {
+          List<ElementWrapper> bucket = Lists.newArrayListWithCapacity(children.getLength());
+          bucket.add((ElementWrapper) previous);
+          bucket.add(wrapper);
+          myMap.put(name, bucket);
+         } else {
+           // Must be a List<ElementWrapper>
+           @SuppressWarnings("unchecked")
+           List<ElementWrapper> bucket = (List<ElementWrapper>) previous;
+           bucket.add(wrapper);
+        }
+      }
+    }
+
+    NamedNodeMap atts = tagInstance.getAttributes();
+    for (int i = 0; i < atts.getLength(); i++) {
+      String name = atts.item(i).getNodeName();
+      // Overwrite any pre-existing values, as attributes take
+      // precedence over elements.  This is wasteful if there are attributes
+      // and elements with the same name, but that should be very rare
+      myMap.put(name, getValueFromTag(tagInstance, name, processor, Object.class));
+    }
+
+    return myMap;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/VarTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/VarTagHandler.java
new file mode 100644
index 0000000..a106e00
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/VarTagHandler.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+/**
+ * Implement the os:Var tag
+ */
+public class VarTagHandler extends AbstractTagHandler {
+
+  private static final String TAG_VAR = "Var";
+  private static final String TAG_KEY = "key";
+  private static final String TAG_VALUE = "value";
+
+  @Inject
+  public VarTagHandler() {
+    super(TagHandler.OPENSOCIAL_NAMESPACE, TAG_VAR);
+  }
+
+  public void process(Node result, Element tag, TemplateProcessor processor) {
+    // Get the key.  Don't support EL (to match pipelining)
+    String key = tag.getAttribute(TAG_KEY);
+    if ("".equals(key)) {
+      return;
+    }
+
+    // Get the value (with EL)
+    Object value = getValueFromTag(tag, TAG_VALUE, processor, Object.class);
+
+    if (processor.getTemplateContext().getMy() == null) {
+      processor.getTemplateContext().setMy(Maps.<String, Object> newHashMap());
+    }
+    processor.getTemplateContext().getMy().put(key, value);
+  }
+
+}
+
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/VariableTagHandler.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/VariableTagHandler.java
new file mode 100644
index 0000000..9087fb7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/templates/tags/VariableTagHandler.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Implement the osx:Variable tag
+ */
+public class VariableTagHandler extends AbstractTagHandler {
+
+  static final String TAG_NAME = "Variable";
+
+  @Inject
+  public VariableTagHandler(@Named("shindig.template-rewrite.extension-tag-namespace") String namespace) {
+    super(namespace, TAG_NAME);
+  }
+
+  public void process(Node result, Element tag, TemplateProcessor processor) {
+    // Get the key.  Don't support EL (to match pipelining)
+    String key = tag.getAttribute("key");
+    if ("".equals(key)) {
+      return;
+    }
+
+    // Get the value (with EL)
+    Object value = getValueFromTag(tag, "value", processor, Object.class);
+
+    if (processor.getTemplateContext().getMy() == null) {
+      processor.getTemplateContext().setMy(Maps.<String, Object> newHashMap());
+    }
+    processor.getTemplateContext().getMy().put(key, value);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/AccelUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/AccelUriManager.java
new file mode 100644
index 0000000..a8351de
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/AccelUriManager.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.inject.ImplementedBy;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+
+/**
+ * UriManager for Accel servlet.
+ *
+ * @since 2.0.0
+ */
+@ImplementedBy(DefaultAccelUriManager.class)
+public interface AccelUriManager {
+  public static final String PROXY_HOST_PARAM = DefaultProxyUriManager.PROXY_HOST_PARAM;
+  public static final String PROXY_PATH_PARAM = DefaultProxyUriManager.PROXY_PATH_PARAM;
+  public static final String CONTAINER = "accel";
+
+  /**
+   * Parses and normalizes the given request uri to be proxied through accel.
+   *
+   * @param httpRequest The http request.
+   * @return Normalized uri which is proxied through accel.
+   * @throws GadgetException In case of errors.
+   */
+  public Uri parseAndNormalize(HttpRequest httpRequest) throws GadgetException;
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/AllJsIframeVersioner.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/AllJsIframeVersioner.java
new file mode 100644
index 0000000..778273f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/AllJsIframeVersioner.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.IframeUriManager.Versioner;
+
+import com.google.inject.Inject;
+
+/**
+ * Simple, but naive, implementation of an IFRAME version generator that
+ * returns the same version value for all renders: the hash of all JS in the
+ * feature system. This serves as an implicit version of the whole build.
+ *
+ * While often a reasonable heuristic, use of this versioner completely
+ * ignores code changes. For instance, a rewriter may be deployed, yet
+ * if no JS changed, it would never run since a generated/versioned URL
+ * would cache the previously-generated render.
+ *
+ * More sophisticated Versioner implementations may take these sorts of
+ * scenarios into consideration, and even go further, retrieving the
+ * referenced gadget from the GadgetSpecFactory. Such an implementation's
+ * performance is highly installation-specific, however, so is left as
+ * an exercise to integrators to achieve effectively.
+ */
+public class AllJsIframeVersioner implements Versioner {
+  private final String allJsChecksum;
+
+  @Inject
+  public AllJsIframeVersioner(FeatureRegistry registry) {
+    String charset = Charset.defaultCharset().name();
+    MessageDigest digest = HashUtil.getMessageDigest();
+    digest.reset();
+    for (FeatureResource resource : registry.getAllFeatures().getResources()) {
+      // Emulate StringBuilder append of content
+      update(digest, resource.getContent(), charset);
+      update(digest, resource.getDebugContent(), charset);
+    }
+    allJsChecksum = HashUtil.bytesToHex(digest.digest());
+  }
+
+  private void update(MessageDigest digest, String content, String charset) {
+    try {
+      digest.update((content == null ? "null" : content).getBytes(charset));
+    } catch (UnsupportedEncodingException e) {
+      digest.update((content == null ? "null" : content).getBytes());
+    }
+  }
+
+  public String version(Uri gadgetUri, String container) {
+    return allJsChecksum;
+  }
+
+  public UriStatus validate(Uri gadgetUri, String container, String value) {
+    if (value == null || value.length() == 0) {
+      return UriStatus.VALID_UNVERSIONED;
+    }
+
+    if (value.equals(allJsChecksum)) {
+      return UriStatus.VALID_VERSIONED;
+    }
+
+    return UriStatus.INVALID_VERSION;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ConcatUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ConcatUriManager.java
new file mode 100644
index 0000000..9d804ef
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ConcatUriManager.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Generates concat servlet specific uris.
+ *
+ * @since 2.0.0
+ */
+public interface ConcatUriManager {
+  public enum Type {
+    JS("text/javascript", "src", "js", "script"), // JavaScript
+    CSS("text/css", "href", "css", "link");     // CSS/styling
+
+    private final String mimeType;
+    private final String srcAttrib;
+    private final String type;
+    private final String tagName;
+
+    private Type(String mimeType, String srcAttrib, String type, String tagName) {
+      this.mimeType = mimeType;
+      this.srcAttrib = srcAttrib;
+      this.type = type;
+      this.tagName = tagName;
+    }
+
+    public String getMimeType() {
+      return mimeType;
+    }
+
+    public String getSrcAttrib() {
+      return srcAttrib;
+    }
+
+    public String getType() {
+      return type;
+    }
+
+    public String getTagName() {
+      return tagName;
+    }
+
+    public static Type fromType(String type) {
+      for (Type val : Type.values()) {
+        if (val.getType().equalsIgnoreCase(type)) {
+          return val;
+        }
+      }
+      return null;
+    }
+
+    public static Type fromMime(String mime) {
+      for (Type val : Type.values()) {
+        if (val.getMimeType().equals(mime)) {
+          return val;
+        }
+      }
+      return null;
+    }
+  }
+
+  /**
+   * Generate Uris that concatenate all given resources together.
+   * @param batches List of batches to concatenate
+   * @param isAdjacent True if Uris are adjacent in the source DOM
+   * @return List of proxied-concatenated Uris (or null if unable to generate)
+   *     in index-correlated order, one per input.
+   */
+  List<ConcatData> make(List<ConcatUri> batches, boolean isAdjacent);
+
+  /**
+   * Represents a single concatenated Uri. This must include a Uri for
+   * loading the given resource(s), and may optionally include a
+   * Map from Uri to String of Snippets, each of which provides a
+   * piece of JavaScript, assumed to be executed after the resource Uri
+   * is loaded, which causes the given Uri's content to be loaded. In
+   * practice, this supports split-JS, where multiple chunks of
+   * (non-contiguous) JS are included as Strings (once) and evaluated
+   * in their correct original position.
+   */
+  public static class ConcatData {
+    private final List<Uri> uris;
+    private final Map<Uri, String> snippets;
+
+    public ConcatData(List<Uri> uris, Map<Uri, String> snippets) {
+      this.uris = Collections.unmodifiableList(uris);
+      this.snippets = snippets;
+    }
+
+    public List<Uri> getUris() {
+      return uris;
+    }
+
+    public String getSnippet(Uri orig) {
+      return snippets == null || !snippets.containsKey(orig) ?
+          null : snippets.get(orig);
+    }
+  }
+
+  public static class ConcatUri extends ProxyUriBase {
+    private final List<Uri> batch;
+    private final Type type;
+    private final String splitParam;
+
+    public ConcatUri(Gadget gadget, List<Uri> batch, Type type) {
+      super(gadget);
+      this.batch = batch;
+      this.type = type;
+      this.splitParam = null;
+    }
+
+    public ConcatUri(
+        UriStatus status, List<Uri> uris, String splitParam, Type type, Uri origUri) {
+      super(status, origUri);
+      this.batch = uris;
+      this.splitParam = splitParam;
+      this.type = type;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof ConcatUri)) {
+        return false;
+      }
+      ConcatUri objUri = (ConcatUri) obj;
+      return (super.equals(obj)
+          && Objects.equal(this.batch, objUri.batch)
+          && Objects.equal(this.splitParam, objUri.splitParam)
+          && Objects.equal(this.type, objUri.type));
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(super.hashCode(), batch, splitParam, type);
+    }
+
+    public List<Uri> getBatch() {
+      return batch;
+    }
+
+    public Type getType() {
+      return type;
+    }
+
+    public String getSplitParam() {
+      return splitParam;
+    }
+
+    public static List<ConcatUri> fromList(Gadget gadget, List<List<Uri>> batches, Type type) {
+      List<ConcatUri> ctx = Lists.newArrayListWithCapacity(batches.size());
+      for (List<Uri> batch : batches) {
+        ctx.add(new ConcatUri(gadget, batch, type));
+      }
+      return ctx;
+    }
+  }
+
+  /**
+   * Parses a given Uri indicating whether it's a concat Uri and if so,
+   * whether it's valid.
+   * @param uri Uri to validate for concat-ness
+   * @return Uri validation status
+   */
+  ConcatUri process(Uri uri);
+
+  public interface Versioner {
+    /**
+     * Generates a version for each of the provided resources.
+     * @param resourceUris List of resource "batches" to version.
+     * @param container Container making the request
+     * @param resourceTags Index-correlated list of html tags, one per list of resouceUris as only
+     * similar tags can be concat. Each entry in resourceTags corresponds to html tag of resources
+     * uris. Any older implementations can just ignore.
+     * @return Index-correlated list of version strings, one per input.
+     */
+    List<String> version(List<List<Uri>> resourceUris, String container,
+                         List<String> resourceTags);
+
+    /**
+     * Validate the version of the resource list.
+     * @param resourceUris Uris of a proxied resource
+     * @param container Container requesting the resource
+     * @param value Version value to validate.
+     * @return Status of the version.
+     */
+    UriStatus validate(List<Uri> resourceUris, String container, String value);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultAccelUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultAccelUriManager.java
new file mode 100644
index 0000000..eee59f3
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultAccelUriManager.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+
+import java.util.Collection;
+
+/**
+ * Default UriManager for Accel servlet.
+ * TODO: Add support for multiple accel hosts.
+ *
+ * @since 2.0.0
+ */
+public class DefaultAccelUriManager implements AccelUriManager, ContainerConfig.ConfigObserver {
+  String accelHost;
+  String accelPath;
+
+  ProxyUriManager proxyUriManager;
+
+  @Inject
+  public DefaultAccelUriManager(ContainerConfig config,
+                                ProxyUriManager proxyUriManager) {
+    this.proxyUriManager = proxyUriManager;
+    config.addConfigObserver(this, true);
+  }
+
+  public void containersChanged(
+      ContainerConfig config, Collection<String> changed, Collection<String> removed) {
+    accelHost = config.getString(AccelUriManager.CONTAINER, PROXY_HOST_PARAM);
+    accelPath = config.getString(AccelUriManager.CONTAINER, PROXY_PATH_PARAM);
+  }
+
+  public Uri parseAndNormalize(HttpRequest httpRequest) throws GadgetException {
+    // Make a gadget object with the accel container.
+    Gadget gadget = DomWalker.makeGadget(httpRequest);
+    gadget.setContext(new GadgetContext(gadget.getContext()) {
+      @Override
+      public String getContainer() {
+        return AccelUriManager.CONTAINER;
+      }
+    });
+
+    // Normalize the request url to proxy uri form.
+    ProxyUriManager.ProxyUri proxied = looksLikeAccelUri(httpRequest.getUri()) ?
+        proxyUriManager.process(httpRequest.getUri()) : new ProxyUriManager.ProxyUri(
+        gadget, httpRequest.getUri());
+    return proxyUriManager.make(ImmutableList.of(proxied), 0).get(0);
+  }
+
+  /**
+   * Is the given uri looks like a valid accel uri. If not, it should
+   * definitely be normalized.
+   * @param requestUri The uri to check.
+   * @return True in case the given uri was possibly generated by accel, false
+   *   otherwise.
+   */
+  protected boolean looksLikeAccelUri(Uri requestUri) {
+    return accelHost.equals(requestUri.getAuthority()) &&
+           accelPath.equals(requestUri.getPath()) &&
+           !Strings.isNullOrEmpty(requestUri.getQueryParameter(
+                   UriCommon.Param.URL.getKey()));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultConcatUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultConcatUriManager.java
new file mode 100644
index 0000000..d9534a6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultConcatUriManager.java
@@ -0,0 +1,296 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+
+// Temporary replacement of javax.annotation.Nullable
+import org.apache.shindig.common.Nullable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Default implementation of a ConcatUriManager
+ *
+ * @since 2.0.0
+ */
+public class DefaultConcatUriManager implements ConcatUriManager {
+  public static final String CONCAT_HOST_PARAM = "gadgets.uri.concat.host";
+  public static final String CONCAT_PATH_PARAM = "gadgets.uri.concat.path";
+  public static final String CONCAT_JS_SPLIT_PARAM = "gadgets.uri.concat.js.splitToken";
+  public static final String CONCAT_JS_EVAL_TPL = "eval(%s['%s']);";
+
+  private static final ConcatUri BAD_URI =
+      new ConcatUri(UriStatus.BAD_URI, null, null, null, null);
+  private static final Integer START_INDEX = 1;
+
+  private final ContainerConfig config;
+  private final Versioner versioner;
+  private boolean strictParsing;
+  private Authority authority;
+  private static int DEFAULT_URL_MAX_LENGTH = 2048;
+  private int urlMaxLength = DEFAULT_URL_MAX_LENGTH;
+  private static final float URL_LENGTH_BUFFER_MARGIN = .8f;
+
+  @Inject
+  public DefaultConcatUriManager(ContainerConfig config, @Nullable Versioner versioner) {
+    this.config = config;
+    this.versioner = versioner;
+  }
+
+  @Inject(optional = true)
+  public void setUseStrictParsing(
+      @Named("shindig.uri.concat.use-strict-parsing") boolean useStrict) {
+    this.strictParsing = useStrict;
+  }
+
+  @Inject(optional = true)
+  public void setUrlMaxLength(
+      @Named("org.apache.shindig.gadgets.uri.urlMaxLength") int urlMaxLength) {
+    this.urlMaxLength = urlMaxLength;
+  }
+
+  @Inject(optional = true)
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  public int getUrlMaxLength() {
+    return this.urlMaxLength;
+  }
+
+  public List<ConcatData> make(List<ConcatUri> resourceUris,
+      boolean isAdjacent) {
+    List<ConcatData> concatUris = Lists.newArrayListWithCapacity(resourceUris.size());
+
+    if (resourceUris.isEmpty()) {
+      return concatUris;
+    }
+
+    ConcatUri exemplar = resourceUris.get(0);
+    String container = exemplar.getContainer();
+
+    for (ConcatUri ctx : resourceUris) {
+      concatUris.add(makeConcatUri(ctx, isAdjacent, container));
+    }
+    return concatUris;
+  }
+
+  private ConcatData makeConcatUri(ConcatUri ctx, boolean isAdjacent, String container) {
+    // TODO: Consider per-bundle isAdjacent plus first-bundle direct evaluation
+
+    if (!isAdjacent && ctx.getType() != Type.JS) {
+      // Split-concat is only supported for JS at the moment.
+      // This situation should never occur due to ConcatLinkRewriter's implementation.
+      throw new UnsupportedOperationException("Split concatenation only supported for JS");
+    }
+
+    String concatHost = getReqVal(ctx.getContainer(), CONCAT_HOST_PARAM);
+    String concatPath = getReqVal(ctx.getContainer(), CONCAT_PATH_PARAM);
+
+    List<Uri> resourceUris = ctx.getBatch();
+    Map<Uri, String> snippets = Maps.newHashMapWithExpectedSize(resourceUris.size());
+
+    String splitParam = config.getString(ctx.getContainer(), CONCAT_JS_SPLIT_PARAM);
+
+    boolean doSplit = false;
+    if (!isAdjacent && splitParam != null && !"false".equalsIgnoreCase(splitParam)) {
+      doSplit = true;
+    }
+
+    UriBuilder uriBuilder = makeUriBuilder(ctx, concatHost, concatPath);
+
+    // Allowed Max Url length is .80 times of actual max length. So, Split will
+    // happen whenever Concat url length crosses this value. Here, buffer also assumes
+    // version length.
+    int injectedMaxUrlLength = (int) (this.getUrlMaxLength() * URL_LENGTH_BUFFER_MARGIN);
+
+    // batchUris holds uris for the current batch of uris being concatenated.
+    List<Uri> batchUris = Lists.newArrayList();
+
+    // uris holds the concatenated uris formed from batches which satisfy the
+    // GET URL limit constraint.
+    List<Uri> uris = Lists.newArrayList();
+
+    Integer i = START_INDEX;
+    for (Uri resource : resourceUris) {
+      uriBuilder.addQueryParameter(i.toString(), resource.toString());
+      if (uriBuilder.toString().length() > injectedMaxUrlLength) {
+        uriBuilder.removeQueryParameter(i.toString());
+
+        addVersionAndSplitParam(uriBuilder, splitParam, doSplit, batchUris, container,
+            ctx.getType());
+        uris.add(uriBuilder.toUri());
+
+        uriBuilder = makeUriBuilder(ctx, concatHost, concatPath);
+        batchUris = Lists.newArrayList();
+        i = START_INDEX;
+        uriBuilder.addQueryParameter(i.toString(), resource.toString());
+      }
+      i++;
+      batchUris.add(resource);
+    }
+
+    if (batchUris != null && uriBuilder != ctx.makeQueryParams(null, null)) {
+      addVersionAndSplitParam(uriBuilder, splitParam, doSplit, batchUris, container, ctx.getType());
+      uris.add(uriBuilder.toUri());
+    }
+
+    if (doSplit) {
+      snippets = createSnippets(uris);
+    }
+   return new ConcatData(uris, snippets);
+  }
+
+  private void addVersionAndSplitParam(UriBuilder uriBuilder, String splitParam, boolean doSplit,
+                                       List<Uri> batchUris, String container, Type type) {
+    // HashCode is used to differentiate splitParam paramter across ConcatUris
+    // within single page/url. This value is appended to the splitParam value which
+    // is recieved from config container.
+    int hashCode = uriBuilder.hashCode();
+    if (doSplit) {
+      uriBuilder.addQueryParameter(Param.JSON.getKey(),
+          (splitParam + String.valueOf(Math.abs(hashCode))));
+    }
+
+    if (versioner != null) {
+      List<List<Uri>> batches = Lists.newArrayList();
+      List<String> resourceTags = Lists.newArrayList();
+
+      batches.add(batchUris);
+      resourceTags.add(type.getTagName().toLowerCase());
+
+      List<String> versions = versioner.version(batches, container, resourceTags);
+
+      if (versions != null && versions.size() == 1) {
+        String version = versions.get(0);
+        if (version != null) {
+          uriBuilder.addQueryParameter(Param.VERSION.getKey(), version);
+        }
+      }
+    }
+  }
+
+  private Map<Uri, String> createSnippets(List<Uri> uris) {
+    Map<Uri, String> snippets = Maps.newHashMap();
+    for (Uri uri : uris) {
+      Integer i = START_INDEX;
+      String splitParam = uri.getQueryParameter(Param.JSON.getKey());
+      String resourceUri;
+      while ((resourceUri = uri.getQueryParameter(i.toString())) != null) {
+        Uri resource = Uri.parse(resourceUri);
+        snippets.put(resource, getJsSnippet(splitParam, resource));
+        i++;
+      }
+    }
+    return snippets;
+  }
+
+  private UriBuilder makeUriBuilder(ConcatUri ctx, String authority, String path) {
+    UriBuilder uriBuilder = ctx.makeQueryParams(null, null);
+    uriBuilder.setAuthority(authority);
+    uriBuilder.setPath(path);
+    uriBuilder.addQueryParameter(Param.TYPE.getKey(), ctx.getType().getType());
+    return uriBuilder;
+  }
+
+  static String getJsSnippet(String splitParam, Uri resource) {
+    return String.format(CONCAT_JS_EVAL_TPL, splitParam,
+        StringEscapeUtils.escapeEcmaScript(resource.toString()));
+  }
+
+  private String getReqVal(String container, String key) {
+    String val = config.getString(container, key);
+    if (val == null) {
+      throw new RuntimeException(
+          "Missing required config '" + key + "' for container: " + container);
+    }
+    if (authority != null) {
+      val = val.replace("%authority%", authority.getAuthority());
+    }
+
+    return val;
+  }
+
+  public ConcatUri process(Uri uri) {
+    String container = uri.getQueryParameter(Param.CONTAINER.getKey());
+    if (strictParsing && container == null) {
+      return BAD_URI;
+    }
+
+    if (strictParsing) {
+      String concatHost = getReqVal(container, CONCAT_HOST_PARAM);
+      String concatPath = getReqVal(container, CONCAT_PATH_PARAM);
+      if (!uri.getAuthority().equalsIgnoreCase(concatHost) ||
+          !uri.getPath().equals(concatPath)) {
+        return BAD_URI;
+      }
+    }
+
+    // At this point the Uri is at least concat.
+    UriStatus status = UriStatus.VALID_UNVERSIONED;
+    List<Uri> uris = Lists.newLinkedList();
+    Type type = Type.fromType(uri.getQueryParameter(Param.TYPE.getKey()));
+    if (type == null) {
+      // try "legacy" method
+      type = Type.fromMime(uri.getQueryParameter("rewriteMime"));
+      if (type == null) {
+        return BAD_URI;
+      }
+    }
+    String splitParam = type == Type.JS ? uri.getQueryParameter(Param.JSON.getKey()) : null;
+
+    Integer i = START_INDEX;
+    String uriStr;
+    while ((uriStr = uri.getQueryParameter(i.toString())) != null) {
+      try {
+        Uri concatUri = Uri.parse(uriStr);
+        if (concatUri.getScheme() == null) {
+          // For non schema url, use the request schema:
+          concatUri = new UriBuilder(concatUri).setScheme(uri.getScheme()).toUri();
+        }
+        uris.add(concatUri);
+      } catch (IllegalArgumentException e) {
+        // Malformed inbound Uri. Don't process.
+        return BAD_URI;
+      }
+      i++;
+    }
+
+    if (versioner != null) {
+      String version = uri.getQueryParameter(Param.VERSION.getKey());
+      if (version != null) {
+        status = versioner.validate(uris, container, version);
+      }
+    }
+    return new ConcatUri(status, uris, splitParam, type, uri);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultIframeUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultIframeUriManager.java
new file mode 100644
index 0000000..137e52d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultIframeUriManager.java
@@ -0,0 +1,392 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenException;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.UserPrefs;
+import org.apache.shindig.gadgets.spec.UserPref;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import com.google.caja.util.Maps;
+import com.google.inject.ImplementedBy;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+/**
+ * Default implementation of an IframeUriManager which references the /ifr endpoint.
+ */
+public class DefaultIframeUriManager implements IframeUriManager {
+  // By default, fills in values that could otherwise be templated for client population.
+  private static final boolean DEFAULT_USE_TEMPLATES = false;
+  static final String IFRAME_BASE_PATH_KEY = "gadgets.uri.iframe.basePath";
+  static final String LOCKED_DOMAIN_REQUIRED_KEY = "gadgets.uri.iframe.lockedDomainRequired";
+  public static final String LOCKED_DOMAIN_SUFFIX_KEY = "gadgets.uri.iframe.lockedDomainSuffix";
+  public static final String UNLOCKED_DOMAIN_KEY = "gadgets.uri.iframe.unlockedDomain";
+  public static final String SECURITY_TOKEN_ALWAYS_KEY = "gadgets.uri.iframe.alwaysAppendSecurityToken";
+  public static final String LOCKED_DOMAIN_FEATURE_NAME = "locked-domain";
+  public static final String SECURITY_TOKEN_FEATURE_NAME = "security-token";
+  private TemplatingSignal tplSignal = null;
+  private Versioner versioner = null;
+  private Authority authority;
+
+  private final ContainerConfig config;
+  private final LockedDomainService ldService;
+  private final SecurityTokenCodec securityTokenCodec;
+
+  @Inject
+  public DefaultIframeUriManager(ContainerConfig config,
+                                 LockedDomainService ldService,
+                                 SecurityTokenCodec securityTokenCodec) {
+    this.config = config;
+    this.ldService = ldService;
+    this.securityTokenCodec = securityTokenCodec;
+  }
+
+  @Inject(optional = true)
+  public void setVersioner(Versioner versioner) {
+    this.versioner = versioner;
+  }
+
+  @Inject(optional = true)
+  public void setTemplatingSignal(TemplatingSignal tplSignal) {
+    this.tplSignal = tplSignal;
+  }
+
+  @Inject(optional = true)
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  public Uri makeRenderingUri(Gadget gadget) {
+    View view = gadget.getCurrentView();
+    return buildUri(view, gadget);
+  }
+
+  // The overridable entrance method to build URI for the gadget iframe URI.
+  // Implementors should not override this method if not necessary but instead override the builder
+  // methods for different parts of the URI.
+  protected Uri buildUri(View view, Gadget gadget) {
+    UriBuilder uri;
+    GadgetContext context = gadget.getContext();
+    String container = context.getContainer();
+
+    // Create UriBuilder based on different types of View
+    if (View.ContentType.URL.equals(view.getType())) {
+      uri = this.processUriForUrlTypeView(view, gadget);
+    } else {
+      uri = this.processUriForHtmlTypeView(view, gadget);
+    }
+
+    boolean useTpl = tplSignal != null ? tplSignal.useTemplates() : DEFAULT_USE_TEMPLATES;
+
+    // Add required/default parameters for the gadget iframe uri
+    this.addDefaultUriParameters(uri, gadget, view, useTpl);
+
+    // Add all UserPrefs
+    this.addAllUserPrefs(uri, gadget, view, useTpl);
+
+    // Add the version to provide caching if needed
+    if (versioner != null) {
+      // Added on the query string, obviously not templated.
+      addParam(uri, Param.VERSION.getKey(),
+          versioner.version(gadget.getSpec().getUrl(), container), false, false);
+    }
+
+    // Handle addition of security token to the URI param
+    if (wantsSecurityToken(gadget)) {
+      boolean securityTokenOnQuery = isTokenNeededForRendering(gadget);
+
+      String securityToken = generateSecurityToken(gadget);
+      addParam(uri, Param.SECURITY_TOKEN.getKey(), securityToken, securityToken == null,
+          !securityTokenOnQuery);
+    }
+
+    // Overridable method to allow additional parameters
+    addExtras(uri, gadget);
+
+    return uri.toUri();
+  }
+
+  // Overrideable method to add extra logic for URL type gadget view
+  protected UriBuilder processUriForUrlTypeView(View view, Gadget gadget) {
+    // A. type=url. Initializes all except standard parameters.
+    UriBuilder uri = new UriBuilder(view.getHref());
+
+    String container = gadget.getContext().getContainer();
+    addExtrasForTypeUrl(uri, gadget, container);
+    return uri;
+  }
+
+  // Overrideable method to add extra logic for HTML type gadget view
+  protected UriBuilder processUriForHtmlTypeView(View view, Gadget gadget) {
+    // B. Others, aka. type=html and html_sanitized.
+    UriBuilder uri = new UriBuilder();
+
+    GadgetContext context = gadget.getContext();
+    String container = context.getContainer();
+
+    // 1. Set base path.
+    uri.setPath(getReqVal(container, IFRAME_BASE_PATH_KEY));
+
+    // 2. Set host/authority.
+    String ldDomain;
+    try {
+      ldDomain = ldService.getLockedDomainForGadget(gadget, container);
+    } catch (GadgetException e) {
+      throw new RuntimeException(e);
+    }
+    String host = "//" +
+        (ldDomain == null ? getReqVal(container, UNLOCKED_DOMAIN_KEY) : ldDomain);
+
+    Uri gadgetUri = Uri.parse(host);
+    if (gadgetUri.getAuthority() == null
+        && gadgetUri.getScheme() == null
+        && gadgetUri.getPath().equals(host)) {
+      // This is for backwards compatibility with unlocked domains like
+      // "unlockeddomain.com"
+      gadgetUri = Uri.parse("//" + host);
+    }
+
+    // 3. Set the scheme.
+    if (StringUtils.isBlank(gadgetUri.getScheme())) {
+      uri.setScheme(getScheme(gadget, container));
+    } else {
+      uri.setScheme(gadgetUri.getScheme());
+    }
+
+    // 4. Set the authority.
+    uri.setAuthority(gadgetUri.getAuthority());
+
+    // 5. Add the URL.
+    uri.addQueryParameter(Param.URL.getKey(), context.getUrl().toString());
+
+    return uri;
+  }
+
+  // Overrideable method to add extra logic default gadget URI parameters
+  protected void addDefaultUriParameters(UriBuilder uri, Gadget gadget, View view,
+      boolean useTpl) {
+    GadgetContext context = gadget.getContext();
+    String container = context.getContainer();
+
+    // Add container, whose input derived other components of the URI.
+    uri.addQueryParameter(Param.CONTAINER.getKey(), container);
+
+    // Add remaining non-url standard parameters, in templated or filled form.
+    addParam(uri, Param.VIEW.getKey(), view.getName(), useTpl, false);
+    addParam(uri, Param.LANG.getKey(), context.getLocale().getLanguage(), useTpl, false);
+    addParam(uri, Param.COUNTRY.getKey(), context.getLocale().getCountry(), useTpl, false);
+    addParam(uri, Param.DEBUG.getKey(), context.getDebug() ? "1" : "0", useTpl, false);
+    addParam(uri, Param.NO_CACHE.getKey(), context.getIgnoreCache() ? "1" : "0", useTpl, false);
+    addParam(uri, Param.SANITIZE.getKey(), context.getSanitize() ? "1" : "0", useTpl, false);
+    if (context.getCajoled()) {
+      addParam(uri, Param.CAJOLE.getKey(), "1", useTpl, false);
+    }
+  }
+
+  // Overrideable method to add extra logic to append user preferences. The default implementation
+  // will simply read from the gadget spec for inline user prefs.
+  protected void addAllUserPrefs(UriBuilder uri, Gadget gadget, View view, boolean useTpl) {
+    GadgetContext context = gadget.getContext();
+
+    UserPrefs prefs = context.getUserPrefs();
+    for (UserPref up : gadget.getSpec().getUserPrefs().values()) {
+      String name = up.getName();
+      String data = prefs.getPref(name);
+      if (data == null) {
+        data = up.getDefaultValue();
+      }
+
+      boolean upInFragment = !view.needsUserPrefSubstitution();
+      addParam(uri, UriCommon.USER_PREF_PREFIX + up.getName(), data, useTpl, upInFragment);
+    }
+  }
+
+  // *** Start overrideable methods to handle generation of security token for the gadget URI ***
+
+  protected String generateSecurityToken(Gadget gadget) {
+    // Find a security token in the context
+    try {
+      SecurityToken token = gadget.getContext().getToken();
+
+      if (securityTokenCodec != null && token != null) {
+        return securityTokenCodec.encodeToken(token);
+      }
+    } catch (SecurityTokenException e) {
+      // ignore -- no security token
+    }
+    return null;
+  }
+
+  protected boolean wantsSecurityToken(Gadget gadget) {
+    return gadget.getAllFeatures().contains(SECURITY_TOKEN_FEATURE_NAME) ||
+           config.getBool(gadget.getContext().getContainer(), SECURITY_TOKEN_ALWAYS_KEY);
+  }
+
+  // This method should be overridden to provide better caching characteristics
+  // for rendering Uris. In particular, it should return true only when the gadget
+  // uses server-side processing of such things as OpenSocial templates, Data pipelining,
+  // and Preloads. The default implementation is naive, returning true for all URIs,
+  // as this is the conservative result that will always functionally work.
+  protected boolean isTokenNeededForRendering(Gadget gadget) {
+    return true;
+  }
+
+  // *** End overrideable methods to handle generation of security token for the gadget URI ***
+
+  public UriStatus validateRenderingUri(Uri inUri) {
+    UriBuilder uri = new UriBuilder(inUri);
+
+    String gadgetStr = uri.getQueryParameter(Param.URL.getKey());
+    Uri gadgetUri;
+    try {
+      gadgetUri = Uri.parse(gadgetStr);
+    } catch (Exception e) {
+      // RuntimeException eg. InvalidArgumentException
+      return UriStatus.BAD_URI;
+    }
+
+    String container = uri.getQueryParameter(Param.CONTAINER.getKey());
+    if (container == null) {
+      container = ContainerConfig.DEFAULT_CONTAINER;
+    }
+
+    String version = uri.getQueryParameter(Param.VERSION.getKey());
+    if (versioner == null || version == null) {
+      return UriStatus.VALID_UNVERSIONED;
+    }
+
+    return versioner.validate(gadgetUri, container, version);
+  }
+
+  public static String tplKey(String key) {
+    return '%' + key + '%';
+  }
+
+  protected String getScheme(Gadget gadget, String container) {
+    // Scheme-relative by default. Override for specific use cases.
+    return null;
+  }
+
+  protected void addExtrasForTypeUrl(UriBuilder uri, Gadget gadget, String container) {
+    Set<String> features = gadget.getViewFeatures().keySet();
+    String jsHost = getReqVal(container, DefaultJsUriManager.JS_HOST_PARAM);
+    String jsPathBase = getReqVal(container, DefaultJsUriManager.JS_PATH_PARAM);
+
+    UriBuilder jsuri = null;
+    if (features.size() > 0) {
+      // We somewhat cheat in that jsHost may contain protocol/scheme as well.
+      jsuri = new UriBuilder(Uri.parse(jsHost));
+
+      // Add JS info to path and set it in URI.
+      StringBuilder builder = new StringBuilder(jsPathBase);
+      if (!jsPathBase.endsWith("/")) {
+        builder.append('/');
+      }
+      builder.append(DefaultJsUriManager.addJsLibs(features));
+      builder.append(DefaultJsUriManager.JS_SUFFIX);
+      jsuri.setPath(builder.toString());
+    }
+    addParam(uri, Param.LIBS.getKey(), jsuri == null ? "" : jsuri.toString(), false, false);
+  }
+
+  protected void addExtras(UriBuilder uri, Gadget gadget) {
+    // Add whatever custom flags are desired here.
+  }
+
+  private void addParam(UriBuilder uri, String key, String data, boolean templated,
+      boolean fragment) {
+    String value;
+    if (templated) {
+      value = tplKey(key);
+    } else {
+      value = data;
+    }
+
+    if (!fragment) {
+      uri.addQueryParameter(key, value);
+    } else {
+      uri.addFragmentParameter(key, value);
+    }
+  }
+
+  private String getReqVal(String container, String key) {
+    String val = config.getString(container, key);
+
+    if (val == null) {
+      throw new RuntimeException("Missing required container config param, key: "
+          + key + ", container: " + container);
+    }
+    if (authority != null) {
+      val = val.replace("%authority%", authority.getAuthority());
+    }
+
+    return val;
+  }
+
+  @ImplementedBy(DefaultTemplatingSignal.class)
+  public static interface TemplatingSignal {
+    boolean useTemplates();
+  }
+
+  public static final class DefaultTemplatingSignal implements TemplatingSignal {
+    private boolean useTemplates = true;
+
+    @Inject(optional = true)
+    public void setUseTemplates(
+        @Named("shindig.urlgen.use-templates-default") Boolean useTemplates) {
+      this.useTemplates = useTemplates;
+    }
+
+    public boolean useTemplates() {
+      return useTemplates;
+    }
+  }
+
+  /**
+   * Returns a list of all URIs for rendering this gadget.  The map is
+   * keyed by the view name.
+   * @param gadget The gadget to generate the URIs for.
+   * @return A map of URIs indexed by the view name.
+   */
+  public Map<String, Uri> makeAllRenderingUris(Gadget gadget) {
+    Map<String, Uri> uris = Maps.newHashMap();
+    Map<String, View> views = gadget.getSpec().getViews();
+    for(String key : views.keySet()) {
+      View view = views.get(key);
+      uris.put(key, buildUri(view, gadget));
+    }
+    return uris;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultJsUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultJsUriManager.java
new file mode 100644
index 0000000..748b94f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultJsUriManager.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetException.Code;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import org.apache.shindig.common.servlet.Authority;
+
+import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Generates and validates URLs serviced by a gadget JavaScript service (JsServlet).
+ */
+public class DefaultJsUriManager implements JsUriManager {
+
+  static final String JS_HOST_PARAM = "gadgets.uri.js.host";
+  static final String JS_PATH_PARAM = "gadgets.uri.js.path";
+  static final JsUri INVALID_URI = new JsUri(UriStatus.BAD_URI);
+  protected static final String JS_SUFFIX = ".js";
+  protected static final String JS_DELIMITER = ":";
+
+  private static final Logger LOG = Logger.getLogger(DefaultJsUriManager.class.getName());
+
+  private final ContainerConfig config;
+  private final Versioner versioner;
+  private Authority authority;
+
+  @Inject
+  public DefaultJsUriManager(ContainerConfig config, Versioner versioner) {
+    this.config = config;
+    this.versioner = versioner;
+  }
+
+  @Inject(optional = true)
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  public Uri makeExternJsUri(JsUri ctx) {
+    String container = ctx.getContainer();
+    String jsHost = getReqConfig(container, JS_HOST_PARAM);
+    String jsPathBase = getReqConfig(container, JS_PATH_PARAM);
+
+    // We somewhat cheat in that jsHost may contain protocol/scheme as well.
+    UriBuilder uri = new UriBuilder(Uri.parse(jsHost));
+
+    // Add JS info to path and set it in URI.
+    StringBuilder jsPath = new StringBuilder(jsPathBase);
+    if (!jsPathBase.endsWith("/")) {
+      jsPath.append('/');
+    }
+    jsPath.append(addJsLibs(ctx.getLibs()));
+
+    // Add the list of already-loaded libs
+    if (!ctx.getLoadedLibs().isEmpty()) {
+      jsPath.append('!').append(addJsLibs(ctx.getLoadedLibs()));
+    }
+
+    jsPath.append(JS_SUFFIX);
+    uri.setPath(jsPath.toString());
+
+    // Standard container param, as JS may be container-specific.
+    uri.addQueryParameter(Param.CONTAINER.getKey(), container);
+
+    // Pass through nocache param for dev purposes.
+    uri.addQueryParameter(Param.NO_CACHE.getKey(),
+        ctx.isNoCache() ? "1" : "0");
+
+    // Pass through debug param for debugging use.
+    uri.addQueryParameter(Param.DEBUG.getKey(),
+        ctx.isDebug() ? "1" : "0");
+
+    uri.addQueryParameter(Param.CONTAINER_MODE.getKey(),
+        ctx.getContext().getParamValue());
+
+    // Pass through gadget Uri
+    if (addGadgetUri()) {
+      uri.addQueryParameter(Param.URL.getKey(), ctx.getGadget());
+    }
+
+    if (ctx.getOnload() != null) {
+      uri.addQueryParameter(Param.ONLOAD.getKey(), ctx.getOnload());
+    }
+
+    if (ctx.isJsload()) {
+      uri.addQueryParameter(Param.JSLOAD.getKey(), "1");
+    }
+
+    if (ctx.isNohint()) {
+      uri.addQueryParameter(Param.NO_HINT.getKey(), "1");
+    }
+
+    JsCompileMode mode = ctx.getCompileMode();
+    if (mode != null && mode != JsCompileMode.getDefault()) {
+      uri.addQueryParameter(Param.COMPILE_MODE.getKey(), mode.getParamValue());
+    }
+
+    if (ctx.cajoleContent()) {
+      uri.addQueryParameter(Param.CAJOLE.getKey(), "1");
+    }
+
+    if (ctx.getRepository() != null) {
+      uri.addQueryParameter(Param.REPOSITORY_ID.getKey(), ctx.getRepository());
+    }
+
+    // Finally, version it, but only if !nocache.
+    if (versioner != null && !ctx.isNoCache()) {
+      String version = versioner.version(ctx);
+      if (version != null && version.length() > 0) {
+        uri.addQueryParameter(Param.VERSION.getKey(), version);
+      }
+    }
+    if (ctx.getExtensionParams() != null) {
+      uri.addQueryParameters(ctx.getExtensionParams());
+    }
+
+    return uri.toUri();
+  }
+
+  /**
+   * Essentially pulls apart a Uri created by makeExternJsUri, validating its
+   * contents, especially the version key.
+   */
+  public JsUri processExternJsUri(Uri uri) throws GadgetException {
+    // Validate basic Uri structure and params
+    String container = uri.getQueryParameter(Param.CONTAINER.getKey());
+    if (container == null) {
+      container = ContainerConfig.DEFAULT_CONTAINER;
+    }
+
+    String host = uri.getAuthority();
+    if (host == null) {
+      issueUriFormatError("Unexpected: Js Uri has no host");
+      return INVALID_URI;
+    }
+
+    // Pull out the collection of features referenced by the Uri.
+    String path = uri.getPath();
+    if (path == null) {
+      issueUriFormatError("Unexpected: Js Uri has no path");
+      return INVALID_URI;
+    }
+    // Decode the path here because it is not automatically decoded when the Uri object is created.
+    path = Utf8UrlCoder.decode(path);
+
+    int lastSlash = path.lastIndexOf('/');
+    if (lastSlash != -1) {
+      path = path.substring(lastSlash + 1);
+    }
+
+    // Convenience suffix: pull off .js if present; leave alone otherwise.
+    if (path.endsWith(JS_SUFFIX)) {
+      path = path.substring(0, path.length() - JS_SUFFIX.length());
+    }
+
+    while (path.startsWith("/")) {
+      path = path.substring(1);
+    }
+
+    String[] splits = StringUtils.split(path, '!');
+    Collection<String> libs = getJsLibs(splits.length >= 1 ? splits[0] : "");
+
+    String haveString = (splits.length >= 2 ? splits[1] : "");
+    String haveQueryParam = uri.getQueryParameter(Param.LOADED.getKey());
+    if (haveQueryParam == null) {
+      haveQueryParam = "";
+    } else {
+      LOG.log(Level.WARNING, "Using deprecated query param ?loaded=c:d in URL. " +
+          "Replace by specifying it in path as /gadgets/js/a:b!c:d.js");
+    }
+    haveString = haveString + JS_DELIMITER + haveQueryParam;
+    Collection<String> have = getJsLibs(haveString);
+
+    UriStatus status = UriStatus.VALID_UNVERSIONED;
+    String version = uri.getQueryParameter(Param.VERSION.getKey());
+    JsUri jsUri = new JsUri(status, uri, libs, have);
+    if (version != null && versioner != null) {
+      status = versioner.validate(jsUri, version);
+      if (status != UriStatus.VALID_UNVERSIONED) {
+        jsUri = new JsUri(status, jsUri);
+      }
+    }
+
+    return jsUri;
+  }
+
+  static String addJsLibs(Collection<String> extern) {
+    return Joiner.on(JS_DELIMITER).join(extern);
+  }
+
+  static Collection<String> getJsLibs(String path) {
+    return ImmutableList.copyOf(Splitter.on(JS_DELIMITER)
+            .trimResults()
+            .omitEmptyStrings().split(path));
+  }
+
+  private String getReqConfig(String container, String key) {
+    String ret = config.getString(container, key);
+    if (ret == null) {
+      ret = config.getString(ContainerConfig.DEFAULT_CONTAINER, key);
+      if (ret == null) {
+        throw new RuntimeException("Container '" + container +
+            "' missing config for required param: " + key);
+      }
+    }
+    if (authority != null) {
+      ret = ret.replace("%authority%", authority.getAuthority());
+    }
+    return ret;
+  }
+
+  // May be overridden to report errors in an alternate way to the user.
+  protected void issueUriFormatError(String err) throws GadgetException {
+    throw new GadgetException(Code.INVALID_PARAMETER, err, HttpResponse.SC_BAD_REQUEST);
+  }
+
+  // Overridable in the event that a Versioner implementation is injected
+  // that uses the gadget itself to perform intelligent optimization and versioning.
+  // This isn't the cleanest logic, so should be cleaned up when better concrete
+  // examples of this behavior exist.
+  protected boolean addGadgetUri() {
+    return false;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultJsVersioner.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultJsVersioner.java
new file mode 100644
index 0000000..165d9e2
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultJsVersioner.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.JsUriManager.Versioner;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Straightforward versioner for collections of requested features to extern.
+ * This implementation covers non-dynamic JS use cases pretty well, so it's set
+ * as the default implementation for the system.
+ */
+public class DefaultJsVersioner implements Versioner {
+  private final FeatureRegistry registry;
+  private final Map<List<FeatureResource>, String> versionCache;
+
+  @Inject
+  public DefaultJsVersioner(FeatureRegistry registry) {
+    this.registry = registry;
+    this.versionCache = Maps.newHashMap();
+  }
+
+  public String version(final JsUri jsUri) {
+    GadgetContext ctx = new GadgetContext() {
+      @Override
+      public String getContainer() {
+        return jsUri.getContainer();
+      }
+
+      @Override
+      public RenderingContext getRenderingContext() {
+        return jsUri.getContext();
+      }
+    };
+
+    // Registry itself will cache these requests.
+    List<FeatureResource> resources =
+        registry.getFeatureResources(ctx, jsUri.getLibs(), null).getResources();
+    if (versionCache.containsKey(resources)) {
+      return versionCache.get(resources);
+    }
+
+    StringBuilder jsBuf = new StringBuilder();
+    for (FeatureResource resource : resources) {
+      jsBuf.append(resource.getContent()).append(resource.getDebugContent());
+    }
+
+    String checksum = HashUtil.checksum(jsBuf.toString().getBytes());
+    versionCache.put(resources, checksum);
+    return checksum;
+  }
+
+  public UriStatus validate(JsUri jsUri, String version) {
+    if (version == null || version.length() == 0) {
+      return UriStatus.VALID_UNVERSIONED;
+    }
+
+    // Punt up to version(), utilizing its cache.
+    String expectedVersion = version(jsUri);
+    if (version.equals(expectedVersion)) {
+      return UriStatus.VALID_VERSIONED;
+    }
+
+    return UriStatus.INVALID_VERSION;
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultOAuthUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultOAuthUriManager.java
new file mode 100644
index 0000000..8790bf8
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultOAuthUriManager.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+
+import com.google.inject.Inject;
+
+/**
+ * Straightforward implemenation of an OAuth callback Uri generator.
+ */
+public class DefaultOAuthUriManager implements OAuthUriManager {
+  static final String OAUTH_GADGET_CALLBACK_URI_PARAM =
+      "gadgets.uri.oauth.callbackTemplate";
+
+  private final ContainerConfig config;
+
+  @Inject
+  public DefaultOAuthUriManager(ContainerConfig config) {
+    this.config = config;
+  }
+
+  public Uri makeOAuthCallbackUri(String container, String host) {
+    String callback = config.getString(container, OAUTH_GADGET_CALLBACK_URI_PARAM);
+    if (callback == null) {
+      return null;
+    }
+    return Uri.parse(callback.replace("%host%", host));
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultProxyUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultProxyUriManager.java
new file mode 100644
index 0000000..d4984fb
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/DefaultProxyUriManager.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.Utf8UrlCoder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import org.apache.shindig.common.servlet.Authority;
+
+// Temporary replacement of javax.annotation.Nullable
+import org.apache.shindig.common.Nullable;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Generates URIs for use by the Shindig proxy service.
+ *
+ * URIs are generated on the host specified in ContainerConfig at key
+ * "gadgets.uri.proxy.host".
+ *
+ * The remainder of the URL may reference either chained or query-style
+ * proxy syntax. The former is used when "gadgets.uri.proxy.path" has token
+ * "%chained_params%" in it.
+ *
+ * Chained: Returned URI contains query params in its path, with the proxied
+ * resource's URI appended verbatim to the end. This enables proxied SWFs
+ * to perform proxied, relative-URI resource loads. Example:
+ * http://www.example.com/gadgets/proxy/refresh=1&.../http://www.foo.com/img.gif
+ *
+ * Query param: All params are provided on the query string. Example:
+ * http://www.example.com/gadgets/proxy?refresh=1&url=http://www.foo.com/img.gif&...
+ *
+ * This implementation supports batched versioning as well. The old-style "fp"
+ * (fingerprint) parameter is not supported any longer; its functionality is assumed
+ * to be subsumed into the version param.
+ *
+ * @since 2.0.0
+ */
+public class DefaultProxyUriManager implements ProxyUriManager {
+  public static final String PROXY_HOST_PARAM = "gadgets.uri.proxy.host";
+  public static final String PROXY_PATH_PARAM = "gadgets.uri.proxy.path";
+  static final String CHAINED_PARAMS_TOKEN = "%chained_params%";
+
+  private final ContainerConfig config;
+  private final Versioner versioner;
+  private boolean strictParsing = false;
+  private Authority authority;
+
+  @Inject
+  public DefaultProxyUriManager(ContainerConfig config,
+                                @Nullable Versioner versioner) {
+    this.config = config;
+    this.versioner = versioner;
+  }
+
+  @Inject(optional = true)
+  public void setUseStrictParsing(@Named("shindig.uri.proxy.use-strict-parsing") boolean useStrict) {
+    this.strictParsing = useStrict;
+  }
+
+  @Inject(optional = true)
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  public List<Uri> make(List<ProxyUri> resources, Integer forcedRefresh) {
+    if (resources.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    List<Uri> resourceUris = Lists.newArrayListWithCapacity(resources.size());
+    List<String> resourceTags = Lists.newArrayListWithCapacity(resources.size());
+
+    for (ProxyUri puc : resources) {
+      resourceUris.add(puc.getResource());
+      resourceTags.add(puc.getHtmlTagContext());
+    }
+
+    Map<Uri, String> versions;
+    if (versioner == null) {
+      versions = Collections.emptyMap();
+    } else {
+      versions = Maps.newHashMapWithExpectedSize(resources.size());
+      List<String> versionList = versioner.version(resourceUris, resources.get(0).getContainer(),
+          resourceTags);
+      if (versionList != null && versionList.size() == resources.size()) {
+        // This should always be the case.
+        // Should we error if not, or just WARNING?
+        Iterator<String> versionIt = versionList.iterator();
+        for (ProxyUri puc : resources) {
+          versions.put(puc.getResource(), versionIt.next());
+        }
+      }
+    }
+
+    List<Uri> result = Lists.newArrayListWithCapacity(resources.size());
+    for (ProxyUri puc : resources) {
+      result.add(makeProxiedUri(puc, forcedRefresh, versions.get(puc.getResource())));
+    }
+
+    return result;
+  }
+
+  private Uri makeProxiedUri(ProxyUri puc, Integer forcedRefresh, String version) {
+    UriBuilder queryBuilder = puc.makeQueryParams(forcedRefresh, version);
+
+    String container = puc.getContainer();
+    UriBuilder uri = new UriBuilder();
+    uri.setAuthority(getReqConfig(container, PROXY_HOST_PARAM));
+
+    // Chained vs. query-style syntax is determined by the presence of CHAINED_PARAMS_TOKEN
+    String path = getReqConfig(container, PROXY_PATH_PARAM);
+    if (path.contains(CHAINED_PARAMS_TOKEN)) {
+      // Chained proxy syntax. Stuff query params into the path and append URI verbatim at the end
+      path = path.replace(CHAINED_PARAMS_TOKEN, queryBuilder.getQuery());
+      uri.setPath(path);
+      String uriStr = uri.toString();
+      String curUri = uriStr + (!uriStr.endsWith("/") ? "/" : "") + puc.getResource().toString();
+      return Uri.parse(curUri);
+    }
+
+    // Query-style syntax. Use path as normal and append query params at the end.
+    queryBuilder.addQueryParameter(Param.URL.getKey(), puc.getResource().toString());
+    uri.setQuery(queryBuilder.getQuery());
+    uri.setPath(path);
+
+    return uri.toUri();
+  }
+
+  @SuppressWarnings("deprecation")
+  public ProxyUri process(Uri uriIn) throws GadgetException {
+    // First determine if the URI is chained-syntax or query-style.
+    String container = uriIn.getQueryParameter(Param.CONTAINER.getKey());
+    if (container == null) {
+      container = uriIn.getQueryParameter(Param.SYND.getKey());
+    }
+    String uriStr = null;
+    Uri queryUri = null;
+    if (container != null &&
+        config.getString(container, PROXY_PATH_PARAM) != null &&
+        config.getString(container, PROXY_PATH_PARAM).equalsIgnoreCase(uriIn.getPath())) {
+      // Query-style. Has container param and path matches.
+      uriStr = uriIn.getQueryParameter(Param.URL.getKey());
+      queryUri = uriIn;
+    } else {
+      // Check for chained query string in the path.
+      String containerStr = Param.CONTAINER.getKey() + '=';
+      String path = uriIn.getPath();
+      // It is possible to get decoded url ('=' converted to %3d)
+      // for example from CssResponseRewriter, so we should support it
+      boolean doDecode = (!path.contains(containerStr));
+      if (doDecode) {
+        path = Utf8UrlCoder.decode(path);
+      }
+      int start = path.indexOf(containerStr);
+      if (start > 0) {
+        start += containerStr.length();
+        int end = path.indexOf('&', start);
+        if (end < start) {
+          end = path.indexOf('/', start);
+        }
+        if (end > start) {
+          // Looks like chained proxy syntax. Pull out params.
+          container = path.substring(start,end);
+        }
+        if (container != null) {
+          String proxyPath = config.getString(container, PROXY_PATH_PARAM);
+          if (proxyPath != null) {
+            String[] chainedChunks = StringUtils.splitByWholeSeparatorPreserveAllTokens(
+                proxyPath, CHAINED_PARAMS_TOKEN);
+
+            // Parse out the URI of the actual resource. This URI is found as the
+            // substring of the "full" URI, after the chained proxy prefix. We
+            // first search for the pre- and post-fixes of the original /pre/%chained_params%/post
+            // ContainerConfig value, and take the URI as everything beyond that point.
+            String startToken = chainedChunks[0];
+            String endToken = "/";
+            if (chainedChunks.length == 2 && chainedChunks[1].length() > 0) {
+              endToken = chainedChunks[1];
+            }
+            if (!endToken.endsWith("/")) {
+              // add suffix '/' that was added by the creator
+              endToken = endToken + '/';
+            }
+
+            // Pull URI out of original inUri's full representation.
+            String fullProxyUri = uriIn.toString();
+            int startIx = fullProxyUri.indexOf(startToken) + startToken.length();
+            int endIx = fullProxyUri.indexOf(endToken, startIx);
+            if (startIx > 0 && endIx > 0) {
+              String chainedQuery = fullProxyUri.substring(startIx, endIx);
+              if (doDecode) {
+                chainedQuery = Utf8UrlCoder.decode(chainedQuery);
+              }
+              queryUri = new UriBuilder().setQuery(chainedQuery).toUri();
+              uriStr = fullProxyUri.substring(endIx + endToken.length());
+            }
+          }
+        }
+      }
+    }
+
+    if (!strictParsing && container != null && Strings.isNullOrEmpty(uriStr)) {
+      // Query-style despite the container being configured for chained style.
+      uriStr = uriIn.getQueryParameter(Param.URL.getKey());
+      queryUri = uriIn;
+    }
+
+    // Parameter validation.
+    if (Strings.isNullOrEmpty(uriStr) || Strings.isNullOrEmpty(container)) {
+      throw new GadgetException(GadgetException.Code.MISSING_PARAMETER,
+          "Missing required parameter(s):" +
+          (Strings.isNullOrEmpty(uriStr) ? ' ' + Param.URL.getKey() : "") +
+          (Strings.isNullOrEmpty(container) ? ' ' + Param.CONTAINER.getKey() : ""),
+          HttpResponse.SC_BAD_REQUEST);
+    }
+
+    String queryHost = config.getString(container, PROXY_HOST_PARAM);
+    if (strictParsing) {
+      if (queryHost == null || !queryHost.equalsIgnoreCase(uriIn.getAuthority())) {
+        throw new GadgetException(GadgetException.Code.INVALID_PATH, "Invalid proxy host",
+            HttpResponse.SC_BAD_REQUEST);
+      }
+    }
+
+
+    Uri uri;
+    try {
+      uri = Uri.parse(uriStr);
+      if (uri.getScheme() == null) {
+        // For non schema url, use the proxy schema:
+        uri = new UriBuilder(uri).setScheme(uriIn.getScheme()).toUri();
+      }
+    } catch (Exception e) {
+      // NullPointerException or InvalidArgumentException.
+      throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+          "Invalid " + Param.URL.getKey() + ": " + uriStr, HttpResponse.SC_BAD_REQUEST);
+    }
+
+    // URI is valid.
+    UriStatus status = UriStatus.VALID_UNVERSIONED;
+
+    String version = queryUri.getQueryParameter(Param.VERSION.getKey());
+    if (versioner != null && version != null) {
+      status = versioner.validate(uri, container, version);
+    }
+
+    ProxyUri proxied = new ProxyUri(status, uri, queryUri);
+    proxied.setHtmlTagContext(uriIn.getQueryParameter(Param.HTML_TAG_CONTEXT.getKey()));
+    return proxied;
+  }
+
+  private String getReqConfig(String container, String key) {
+    String val = config.getString(container, key);
+    if (val == null) {
+      throw new RuntimeException("Missing required container config key: " + key + " for " +
+          "container: " + container);
+    }
+    if (authority != null) {
+      val = val.replace("%authority%", authority.getAuthority());
+    } else{
+      //require this for test purpose, %host% needs to be replaced with default value eg. StyleTagProxyEmbeddedUrlsVisitorTest
+      if (val.contains("%authority%")) {
+        val = val.replace("%authority%", "localhost:8080");
+      }
+    }
+    return val;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/HashShaLockedDomainPrefixGenerator.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/HashShaLockedDomainPrefixGenerator.java
new file mode 100644
index 0000000..cd9eaac
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/HashShaLockedDomainPrefixGenerator.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.shindig.common.util.Base32;
+import org.apache.shindig.common.uri.Uri;
+
+/**
+ * A simple implementation of locked domain that hashes the gadgeturi as the prefix.
+ */
+public class HashShaLockedDomainPrefixGenerator implements LockedDomainPrefixGenerator {
+  public String getLockedDomainPrefix(Uri gadgetUri) {
+    return getLockedDomainPrefix(gadgetUri.toString().toLowerCase());
+  }
+
+  public String getLockedDomainPrefix(String token) {
+    byte[] sha1 = DigestUtils.sha(token);
+    return new String(Base32.encodeBase32(sha1)); // a hash
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/IframeUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/IframeUriManager.java
new file mode 100644
index 0000000..adf1b29
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/IframeUriManager.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import java.util.Map;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+
+/**
+ * Interface defining methods needed to generate iframe URL for the /ifr servlet.
+ */
+public interface IframeUriManager {
+  /**
+   * Generates iframe urls for meta data service.
+   * Use this rather than generating your own urls by hand.
+   *
+   * @return The generated iframe url.
+   */
+  Uri makeRenderingUri(Gadget gadget);
+
+  /**
+   * Generates iframe uris for all views in the gadget.
+   * @param gadget The gadget to generate the URI for.
+   *
+   * @return A map of views to iframe uris.
+   */
+  Map<String, Uri> makeAllRenderingUris (Gadget gadget);
+
+  /**
+   * Validates the provided rendering Uri. May include
+   * locked-domain, version param, and/or other checks.
+   *
+   * @Return Validation status of the Uri.
+   */
+  UriStatus validateRenderingUri(Uri uri);
+
+  public interface Versioner {
+    /**
+     * @param gadgetUri Gadget whose content to version.
+     * @param container Container in which gadget is being rendered.
+     * @return Version string for the pair.
+     */
+    String version(Uri gadgetUri, String container);
+
+    /**
+     * @param gadgetUri Gadget whose version to validate.
+     * @param container Container in which gadget is being rendered.
+     * @param value Previously returned version string for the pair.
+     * @return UriStatus indicating version (mis)match.
+     */
+    UriStatus validate(Uri gadgetUri, String container, String value);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/JsUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/JsUriManager.java
new file mode 100644
index 0000000..f82c304
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/JsUriManager.java
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+/**
+ * Interface defining methods used to generate Uris for the /js servlet.
+ */
+public interface JsUriManager {
+  /**
+   * @param ctx The js parameters.
+   * @return The uri for the externed javascript that includes all listed extern libraries.
+   */
+  Uri makeExternJsUri(JsUri ctx);
+
+  /**
+   * Processes the inbound URL, for use by serving code in determining which JS to serve
+   * and with what caching properties.
+   *
+   * @param uri Generated extern JS Uri
+   * @return Processed status of the provided Uri.
+   */
+  JsUri processExternJsUri(Uri uri) throws GadgetException;
+
+  public static class JsUri extends ProxyUriBase {
+    private final static Collection<String> EMPTY_COLL = Collections.emptyList();
+    private final Collection<String> libs;
+    private final Collection<String> loadedLibs;
+    private final String onload;
+    private final RenderingContext context;
+    private final Uri origUri;
+    private JsCompileMode compileMode;
+    private boolean jsload;
+    private boolean nohint;
+    private String repository;
+
+    public JsUri(UriStatus status, Uri origUri, Collection<String> libs, Collection<String> have) {
+      super(status, origUri);
+      if (origUri != null) {
+        String contextParam = origUri.getQueryParameter(Param.CONTAINER_MODE.getKey());
+        this.context = RenderingContext.valueOfParam(contextParam);
+        String compileParam = origUri.getQueryParameter(Param.COMPILE_MODE.getKey());
+        this.compileMode = JsCompileMode.valueOfParam(compileParam);
+        this.jsload = "1".equals(origUri.getQueryParameter(Param.JSLOAD.getKey()));
+        this.onload = origUri.getQueryParameter(Param.ONLOAD.getKey());
+        this.nohint = "1".equals(origUri.getQueryParameter(Param.NO_HINT.getKey()));
+        this.repository = origUri.getQueryParameter(Param.REPOSITORY_ID.getKey());
+      } else {
+        this.context = RenderingContext.getDefault();
+        this.compileMode = JsCompileMode.getDefault();
+        this.jsload = false;
+        this.onload = null;
+        this.nohint = false;
+        this.repository = null;
+      }
+      this.libs = nonNullLibs(libs);
+      this.loadedLibs = nonNullLibs(have);
+      this.origUri = origUri;
+    }
+
+    public JsUri(UriStatus status) {
+      this(status, null, EMPTY_COLL, EMPTY_COLL);
+    }
+
+    public JsUri(UriStatus status, Collection<String> libs, RenderingContext context,
+        String onload, boolean jsload, boolean nohint, String repository) {
+      super(status, null);
+      this.compileMode = JsCompileMode.getDefault();
+      this.onload = onload;
+      this.jsload = jsload;
+      this.nohint = nohint;
+      this.context = context;
+      this.libs = nonNullLibs(libs);
+      this.loadedLibs = EMPTY_COLL;
+      this.origUri = null;
+      this.repository = repository;
+    }
+
+    public JsUri(Gadget gadget, Collection<String> libs) {
+      super(gadget);
+      this.compileMode = JsCompileMode.getDefault();
+      this.onload = null;
+      this.jsload = false;
+      this.nohint = false;
+      this.context = RenderingContext.getDefault();
+      this.libs = nonNullLibs(libs);
+      this.loadedLibs = EMPTY_COLL;
+      this.origUri = null;
+      this.setCajoleContent(gadget.requiresCaja());
+    }
+
+    public JsUri(Integer refresh, boolean debug, boolean noCache, String container, String gadget,
+        Collection<String> libs, Collection<String> loadedLibs, String onload, boolean jsload,
+        boolean nohint, RenderingContext context, Uri origUri, String repository) {
+      super(null, refresh, debug, noCache, container, gadget);
+      this.compileMode = JsCompileMode.getDefault();
+      this.onload = onload;
+      this.jsload = jsload;
+      this.nohint = nohint;
+      this.context = context;
+      this.libs = nonNullLibs(libs);
+      this.loadedLibs = nonNullLibs(loadedLibs);
+      this.origUri = origUri;
+      this.repository = repository;
+    }
+
+    public JsUri(JsUri origJsUri) {
+      this(origJsUri.getStatus(), origJsUri);
+    }
+
+    public JsUri(UriStatus status, JsUri origJsUri) {
+      super(status, origJsUri.getRefresh(),
+          origJsUri.isDebug(),
+          origJsUri.isNoCache(),
+          origJsUri.getContainer(),
+          origJsUri.getGadget());
+      this.setCajoleContent(origJsUri.cajoleContent());
+      this.libs = origJsUri.getLibs();
+      this.loadedLibs = origJsUri.getLoadedLibs();
+      this.onload = origJsUri.getOnload();
+      this.jsload = origJsUri.isJsload();
+      this.nohint = origJsUri.isNohint();
+      this.compileMode = origJsUri.getCompileMode();
+      this.context = origJsUri.getContext();
+      this.origUri = origJsUri.getOrigUri();
+      this.repository = origJsUri.getRepository();
+      this.extensionParams = origJsUri.getExtensionParams();
+    }
+
+    public Collection<String> getLibs() {
+      return libs;
+    }
+
+    public Collection<String> getLoadedLibs() {
+      return loadedLibs;
+    }
+
+    private Collection<String> nonNullLibs(Collection<String> in) {
+      return in != null ? Collections.unmodifiableList(Lists.newArrayList(in)) : EMPTY_COLL;
+    }
+
+    public RenderingContext getContext() {
+      return context;
+    }
+
+    public JsCompileMode getCompileMode() {
+      return compileMode;
+    }
+
+    public void setCompileMode(JsCompileMode mode) {
+      this.compileMode = mode;
+    }
+
+    public String getOnload() {
+      return onload;
+    }
+
+    public boolean isJsload() {
+      return jsload;
+    }
+
+    public void setJsload(boolean jsload) {
+      this.jsload = jsload;
+    }
+
+    public boolean isNohint() {
+      return nohint;
+    }
+
+    public void setNohint(boolean nohint) {
+      this.nohint = nohint;
+    }
+
+    public Uri getOrigUri() {
+      return origUri;
+    }
+
+    public void setRepository(String repository) {
+      this.repository = repository;
+    }
+
+    public String getRepository() {
+      return repository;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof JsUri)) {
+        return false;
+      }
+      JsUri objUri = (JsUri) obj;
+      return (super.equals(obj)
+          && Objects.equal(this.libs, objUri.libs)
+          && Objects.equal(this.loadedLibs, objUri.loadedLibs)
+          && Objects.equal(this.onload, objUri.onload)
+          && Objects.equal(this.jsload, objUri.jsload)
+          && Objects.equal(this.nohint, objUri.nohint)
+          && Objects.equal(this.compileMode, objUri.compileMode)
+          && Objects.equal(this.context, objUri.context)
+          && Objects.equal(this.origUri, objUri.origUri)
+          && Objects.equal(this.repository, objUri.repository));
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(this.libs, this.loadedLibs, this.onload, this.jsload,
+                              this.nohint, this.context, this.origUri,
+                              this.compileMode, this.repository);
+    }
+  }
+
+  public interface Versioner {
+    /**
+     * @param jsUri js request to create version for
+     * @return Version string for the Uri.
+     */
+    String version(JsUri jsUri);
+
+    /**
+     * @param jsUri js request to validate
+     * @param version Version string generated by the Versioner.
+     * @return Validation status of the version.
+     */
+    UriStatus validate(JsUri jsUri, String version);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/LockedDomainPrefixGenerator.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/LockedDomainPrefixGenerator.java
new file mode 100644
index 0000000..026b6c0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/LockedDomainPrefixGenerator.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import org.apache.shindig.common.uri.Uri;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ *  Interface that defines how to lookup a prefix from a gadget uri.
+ */
+@ImplementedBy(HashShaLockedDomainPrefixGenerator.class)
+public interface LockedDomainPrefixGenerator {
+  String getLockedDomainPrefix(Uri gadgetUri);
+  String getLockedDomainPrefix(String token);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/OAuthUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/OAuthUriManager.java
new file mode 100644
index 0000000..a8bf816
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/OAuthUriManager.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import org.apache.shindig.common.uri.Uri;
+
+/**
+ * Methods used by the oauth proxy, currently only used for an OAuth callback.
+ */
+public interface OAuthUriManager {
+  /**
+   * @return the OAuth Callback Uri on the provided host.
+   */
+  Uri makeOAuthCallbackUri(String container, String host);
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ProxyUriBase.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ProxyUriBase.java
new file mode 100644
index 0000000..64e05ef
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ProxyUriBase.java
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.base.Objects;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import java.util.Map;
+
+/**
+ * Represents state/config information for the proxy.
+ *
+ * @since 2.0.0
+ */
+public class ProxyUriBase {
+
+  @Inject(optional=true)
+  @Named("org.apache.shindig.gadgets.uri.setAuthorityAsGadgetParam")
+  static private boolean setAuthorityAsGadgetParam = false;
+
+  private UriStatus status = null;
+  private Integer refresh = null;
+  private boolean debug = false;
+  private boolean noCache = false;
+  private String container = ContainerConfig.DEFAULT_CONTAINER;
+  private String gadget = null;
+  private String rewriteMimeType = null;
+  private boolean sanitizeContent = false;
+  private boolean cajoleContent = false;
+  /** Extension parameters to support implementation specific flags */
+  protected Map<String, String> extensionParams = null;
+
+  protected ProxyUriBase(Gadget gadget) {
+    this(null,  // Meaningless in "context" mode. translateStatusRefresh invalid here.
+         getIntegerValue(gadget.getContext().getParameter(Param.REFRESH.getKey())),
+         gadget.getContext().getDebug(),
+         gadget.getContext().getIgnoreCache(),
+         gadget.getContext().getContainer(),
+         setAuthorityAsGadgetParam ? gadget.getSpec().getUrl().getAuthority() :
+             gadget.getSpec().getUrl().toString());
+  }
+
+  protected ProxyUriBase(UriStatus status, Uri origUri) {
+    this.status = status;
+    setFromUri(origUri);
+  }
+
+  protected ProxyUriBase(UriStatus status, Integer refresh, boolean debug, boolean noCache,
+      String container, String gadget) {
+    this.status = status;
+    this.refresh = refresh;
+    this.debug = debug;
+    this.noCache = noCache;
+    this.container = container;
+    this.gadget = gadget;
+  }
+
+  /**
+   * Parse uri query paramaters.
+   * Note this function is called by a constructor,
+   * and can be override to handle derived class parsing
+   */
+  @SuppressWarnings("deprecation") // we still need to support SYND while parsing
+  public void setFromUri(Uri uri) {
+    if (uri != null) {
+      refresh = getIntegerValue(uri.getQueryParameter(Param.REFRESH.getKey()));
+      debug = getTruthyValue(uri.getQueryParameter(Param.DEBUG.getKey()));
+      noCache = getBooleanValue(uri.getQueryParameter(Param.NO_CACHE.getKey()));
+      String newContainer = uri.getQueryParameter(Param.CONTAINER.getKey());
+      if (newContainer == null) {
+        // Support "synd" for legacy purposes.
+        newContainer = uri.getQueryParameter(Param.SYND.getKey());
+      }
+      // Preserve "default" container.
+      if (newContainer != null) {
+        container = newContainer;
+      }
+      gadget = uri.getQueryParameter(Param.GADGET.getKey());
+      rewriteMimeType = uri.getQueryParameter(Param.REWRITE_MIME_TYPE.getKey());
+      sanitizeContent = getBooleanValue(uri.getQueryParameter(Param.SANITIZE.getKey()));
+      cajoleContent = getBooleanValue(uri.getQueryParameter(Param.CAJOLE.getKey()));
+    }
+  }
+
+  protected static boolean getTruthyValue(String str) {
+    return str != null && !"0".equals(str) && str.length() > 0;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj == this) {
+      return true;
+    }
+    if (!(obj instanceof ProxyUriBase)) {
+      return false;
+    }
+    ProxyUriBase objUri = (ProxyUriBase) obj;
+    return (Objects.equal(this.status, objUri.status)
+        && (noCache || Objects.equal(this.refresh, objUri.refresh))
+        && Objects.equal(this.container, objUri.container)
+        && Objects.equal(this.gadget, objUri.gadget)
+        && Objects.equal(this.rewriteMimeType, objUri.rewriteMimeType)
+        && this.noCache == objUri.noCache
+        && this.debug == objUri.debug
+        && this.sanitizeContent == objUri.sanitizeContent
+        && this.cajoleContent == objUri.cajoleContent
+        && Objects.equal(this.extensionParams, objUri.extensionParams));
+
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(status, refresh, container, gadget, rewriteMimeType,
+            noCache, debug, sanitizeContent, cajoleContent);
+  }
+
+  public ProxyUriBase setRewriteMimeType(String type) {
+    this.rewriteMimeType = type;
+    return this;
+  }
+
+  public ProxyUriBase setSanitizeContent(boolean sanitize) {
+    this.sanitizeContent = sanitize;
+    return this;
+  }
+
+  public ProxyUriBase setCajoleContent(boolean cajole) {
+    this.cajoleContent = cajole;
+    return this;
+  }
+
+  public ProxyUriBase setGadget(String gadget) {
+    this.gadget = gadget;
+    return this;
+  }
+  public UriStatus getStatus() {
+    return status;
+  }
+
+  public Integer getRefresh() {
+    return noCache ? Integer.valueOf(0) : refresh;
+  }
+
+  public boolean isDebug() {
+    return debug;
+  }
+
+  public boolean isNoCache() {
+    return noCache;
+  }
+
+  public String getContainer() {
+    return container;
+  }
+
+  public String getGadget() {
+    return gadget;
+  }
+
+  public String getRewriteMimeType() {
+    return rewriteMimeType;
+  }
+
+  public boolean sanitizeContent() {
+    return sanitizeContent;
+  }
+
+  public boolean cajoleContent() {
+    return cajoleContent;
+  }
+
+  public void setExtensionParam(String key, String val) {
+    if (extensionParams == null) {
+      extensionParams = Maps.newHashMap();
+    }
+    extensionParams.put(key, val);
+  }
+
+  public String getExtensionParam(String key) {
+    if (extensionParams == null) {
+      return null;
+    }
+    return extensionParams.get(key);
+  }
+
+  public ProxyUriBase setExtensionParams(Map<String, String> extensionParams) {
+    this.extensionParams = extensionParams;
+    return this;
+  }
+
+  public Map<String, String> getExtensionParams() {
+    return extensionParams;
+  }
+
+  public HttpRequest makeHttpRequest(Uri targetUri) throws GadgetException {
+    HttpRequest req = new HttpRequest(targetUri)
+        .setIgnoreCache(isNoCache())
+        .setContainer(getContainer());
+    if (!Strings.isNullOrEmpty(getGadget())) {
+      try {
+        req.setGadget(Uri.parse(getGadget()));
+      } catch (IllegalArgumentException e) {
+        throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+            "Invalid " + Param.GADGET.getKey() + " param: " + getGadget(),
+            HttpResponse.SC_BAD_REQUEST);
+      }
+    }
+    if (getRefresh() != null && getRefresh() >= 0) {
+      req.setCacheTtl(getRefresh());
+    }
+
+    // Allow the rewriter to use an externally forced MIME type. This is needed
+    // allows proper rewriting of <script src="x"/> where x is returned with
+    // a content type like text/html which unfortunately happens all too often
+    if (rewriteMimeType != null) {
+      req.setRewriteMimeType(getRewriteMimeType());
+    }
+    req.setSanitizationRequested(sanitizeContent());
+    req.setCajaRequested(cajoleContent());
+
+    return req;
+  }
+
+  /**
+   * Construct the query parameters for proxy url
+   * @param forcedRefresh optional overwrite the refresh time
+   * @param version optional version
+   * @return Url with only query parameters set
+   */
+  public UriBuilder makeQueryParams(Integer forcedRefresh, String version) {
+    UriBuilder queryBuilder = new UriBuilder();
+
+    // Add all params common to both chained and query syntax.
+    String container = getContainer();
+    queryBuilder.addQueryParameter(Param.CONTAINER.getKey(), container);
+    if (getGadget() != null) {
+      queryBuilder.addQueryParameter(Param.GADGET.getKey(), getGadget());
+    }
+    queryBuilder.addQueryParameter(Param.DEBUG.getKey(), isDebug() ? "1" : "0");
+    queryBuilder.addQueryParameter(Param.NO_CACHE.getKey(), isNoCache() ? "1" : "0");
+    if (!isNoCache()) {
+      if (forcedRefresh != null && forcedRefresh >= 0) {
+        queryBuilder.addQueryParameter(Param.REFRESH.getKey(), forcedRefresh.toString());
+      } else if (getRefresh() != null) {
+        queryBuilder.addQueryParameter(Param.REFRESH.getKey(), getRefresh().toString());
+      }
+    }
+
+    if (version != null) {
+      queryBuilder.addQueryParameter(Param.VERSION.getKey(), version);
+    }
+    if (rewriteMimeType != null) {
+      queryBuilder.addQueryParameter(Param.REWRITE_MIME_TYPE.getKey(), rewriteMimeType);
+    }
+    if (sanitizeContent) {
+      queryBuilder.addQueryParameter(Param.SANITIZE.getKey(), "1");
+    }
+    if (cajoleContent) {
+      queryBuilder.addQueryParameter(Param.CAJOLE.getKey(), "1");
+    }
+    if (extensionParams != null) {
+      queryBuilder.addQueryParameters(extensionParams);
+    }
+    return queryBuilder;
+  }
+
+  /**
+   * Calculate cache time for a resource url. Provide long period for versioned resource,
+   * use original value for unversioned resource, and no cache for invalid versions.
+   * Invalid version can happen in multiple instances environemnt where the url can be
+   * created on one server and the actually retreival is done on another that have an older version
+   * of resource in cache. In that case no caching will cause the user browser to try again next
+   * time and hopefully getting the newer version.
+   * @param longVal long expiry for versioned resource
+   * @param originalResourceTtl original resource ttl, to be served fro unversion resource
+   * @return the calculated expiry to the Uri according to status
+   * @throws GadgetException
+   */
+  public Integer translateStatusRefresh(int longVal, int originalResourceTtl)
+      throws GadgetException {
+    Integer retRefresh = 0;
+    switch (getStatus()) {
+    case VALID_VERSIONED:
+      retRefresh = longVal;
+      break;
+    case VALID_UNVERSIONED:
+      retRefresh = originalResourceTtl;
+      break;
+    case INVALID_VERSION:
+      retRefresh = 0;
+      break;
+    case BAD_URI:
+      throw new GadgetException(GadgetException.Code.INVALID_PATH,
+          "Invalid path", HttpResponse.SC_BAD_REQUEST);
+    default:
+      // Should never happen.
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR,
+          "Unknown status: " + getStatus());
+    }
+    Integer setVal = getRefresh();
+    if (setVal != null) {
+      // Override always wins.
+      if (setVal != -1) {
+        retRefresh = setVal;
+      }
+    }
+    return retRefresh;
+  }
+
+  protected static boolean getBooleanValue(String str) {
+    return str != null && "1".equals(str);
+  }
+
+  protected static Integer getIntegerValue(String str) {
+    try {
+      return NumberUtils.createInteger(str);
+    } catch (NumberFormatException e) {
+      // -1 is sentinel for invalid value.
+      return -1;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ProxyUriManager.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ProxyUriManager.java
new file mode 100644
index 0000000..24d38e7
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/ProxyUriManager.java
@@ -0,0 +1,332 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import java.util.List;
+
+/**
+ * Generates Uris used by the /proxy servlet
+ * @since 2.0.0
+ */
+public interface ProxyUriManager {
+  /**
+   * Generate a Uri that proxies the given resource Uri.
+   *
+   * @param resource Resource Uri to proxy
+   * @param forcedRefresh Forced expires value to use for resource
+   * @return Uri of proxied resource
+   */
+  List<Uri> make(List<ProxyUri> resource, Integer forcedRefresh);
+
+  public static class ProxyUri extends ProxyUriBase {
+    private final Uri resource;
+    private String fallbackUrl;
+    private Integer resizeHeight;
+    private Integer resizeWidth;
+    private Integer resizeQuality;
+    private boolean resizeNoExpand;
+    private SecurityToken securityToken;
+    private AuthType authType;
+    private OAuth2Arguments oauth2Arguments;
+    private OAuthArguments oauthArguments;
+
+    // If "true" then the original content should be returned to the user
+    // instead of internal server errors.
+    @VisibleForTesting
+    String returnOriginalContentOnError;
+
+    // The html tag that requested this ProxyUri.
+    private String htmlTagContext;
+
+    // The User-Agent from the request
+    private String userAgent;
+
+    public ProxyUri(Gadget gadget, Uri resource) {
+      super(gadget);
+      this.resource = resource;
+      if (AccelUriManager.CONTAINER.equals(gadget.getContext().getContainer())) {
+        setReturnOriginalContentOnError(true);
+      }
+      if(authType == null) {
+        authType = AuthType.NONE;
+      }
+    }
+
+    public ProxyUri(Integer refresh, boolean debug, boolean noCache,
+        String container, String gadget, Uri resource) {
+      super(UriStatus.VALID_UNVERSIONED, refresh, debug, noCache, container, gadget);
+      this.resource = resource;
+      if (AccelUriManager.CONTAINER.equals(container)) {
+        setReturnOriginalContentOnError(true);
+      }
+      if(authType == null) {
+        authType = AuthType.NONE;
+      }
+    }
+
+    public ProxyUri(UriStatus status, Uri resource, Uri base) {
+      super(status, base);
+      this.resource = resource;
+    }
+
+    @VisibleForTesting
+    public void setReturnOriginalContentOnError(boolean returnOriginalContentOnError) {
+      this.returnOriginalContentOnError = returnOriginalContentOnError ? "1" : null;
+    }
+
+    public void setUserAgent(String ua) {
+      this.userAgent = ua;
+    }
+    public String getUserAgent() {
+      return userAgent;
+    }
+    public void setHtmlTagContext(String htmlTagContext) {
+      this.htmlTagContext = htmlTagContext;
+    }
+    public String getHtmlTagContext() {
+      return htmlTagContext;
+    }
+    public SecurityToken getSecurityToken() {
+      return securityToken;
+    }
+    public AuthType getAuthType() {
+      return authType;
+    }
+    public OAuthArguments getOAuthArguments() {
+      return oauthArguments;
+    }
+    public OAuth2Arguments getOAuth2Arguments() {
+      return oauth2Arguments;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof ProxyUri)) {
+        return false;
+      }
+      ProxyUri objUri = (ProxyUri) obj;
+      return (super.equals(obj)
+          && Objects.equal(this.resource, objUri.resource)
+          && Objects.equal(this.fallbackUrl, objUri.fallbackUrl)
+          && Objects.equal(this.resizeHeight, objUri.resizeHeight)
+          && Objects.equal(this.resizeWidth, objUri.resizeWidth)
+          && Objects.equal(this.resizeQuality, objUri.resizeQuality)
+          && Objects.equal(this.resizeNoExpand, objUri.resizeNoExpand)
+          && Objects.equal(this.returnOriginalContentOnError, objUri.returnOriginalContentOnError)
+          && Objects.equal(this.htmlTagContext, objUri.htmlTagContext)
+          && Objects.equal(this.securityToken, objUri.securityToken)
+          && Objects.equal(this.authType, objUri.authType))
+          && Objects.equal(this.oauthArguments, objUri.oauthArguments)
+          && Objects.equal(this.oauth2Arguments, objUri.oauth2Arguments);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(super.hashCode(), resource, fallbackUrl, resizeHeight,
+              resizeWidth, resizeQuality, resizeNoExpand, returnOriginalContentOnError,
+              htmlTagContext, securityToken, authType, oauthArguments, oauth2Arguments);
+    }
+
+    /* (non-Javadoc)
+     * @see org.apache.shindig.gadgets.uri.ProxyUriBase#setFromUri(org.apache.shindig.common.uri.Uri)
+     */
+    @Override
+    public void setFromUri(Uri uri) {
+      super.setFromUri(uri);
+      if (uri != null) {
+        fallbackUrl = uri.getQueryParameter(Param.FALLBACK_URL_PARAM.getKey());
+        resizeHeight = getIntegerValue(uri.getQueryParameter(Param.RESIZE_HEIGHT.getKey()));
+        resizeWidth = getIntegerValue(uri.getQueryParameter(Param.RESIZE_WIDTH.getKey()));
+        resizeQuality = getIntegerValue(uri.getQueryParameter(Param.RESIZE_QUALITY.getKey()));
+        resizeNoExpand = getBooleanValue(uri.getQueryParameter(Param.NO_EXPAND.getKey()));
+        returnOriginalContentOnError = uri.getQueryParameter(
+            Param.RETURN_ORIGINAL_CONTENT_ON_ERROR.getKey());
+        htmlTagContext = uri.getQueryParameter(Param.HTML_TAG_CONTEXT.getKey());
+        authType = AuthType.parse(uri.getQueryParameter(Param.AUTHZ.getKey()));
+      }
+    }
+
+    public ProxyUri setResize(Integer w, Integer h, Integer q, boolean noExpand) {
+      this.resizeHeight = h;
+      this.resizeWidth = w;
+      this.resizeQuality = q;
+      this.resizeNoExpand = noExpand;
+      return this;
+    }
+
+    public ProxyUri setFallbackUrl(String fallbackUrl) {
+      this.fallbackUrl = fallbackUrl;
+      return this;
+    }
+
+    public ProxyUri setSecurityToken(SecurityToken securityToken) {
+      this.securityToken = securityToken;
+      return this;
+    }
+
+    public ProxyUri setAuthType(AuthType authType) {
+      this.authType = authType;
+      return this;
+    }
+
+    public ProxyUri setOAuthArguments(OAuthArguments oauthArgments) {
+      this.oauthArguments = oauthArgments;
+      return this;
+    }
+
+    public ProxyUri setOAuth2Arguments(OAuth2Arguments oauth2Arguments) {
+      this.oauth2Arguments = oauth2Arguments;
+      return this;
+    }
+
+    public Uri getResource() {
+      return resource;
+    }
+
+    public Uri getFallbackUri() throws GadgetException {
+      if (fallbackUrl == null) {
+        return null;
+      }
+      try {
+        // Doing delay parsing.
+        return Uri.parse(fallbackUrl);
+      } catch (IllegalArgumentException e) {
+        throw new GadgetException(GadgetException.Code.INVALID_PARAMETER,
+            Param.FALLBACK_URL_PARAM.getKey() + " param is invalid: "
+            + e, HttpResponse.SC_BAD_REQUEST);
+      }
+    }
+
+    public boolean shouldReturnOrigOnErr() {
+      return "1".equals(this.returnOriginalContentOnError) ||
+             "true".equalsIgnoreCase(this.returnOriginalContentOnError);
+    }
+
+    @Override
+    public UriBuilder makeQueryParams(Integer forcedRefresh, String version) {
+      UriBuilder builder = super.makeQueryParams(forcedRefresh, version);
+      if (resizeHeight != null) {
+        builder.addQueryParameter(Param.RESIZE_HEIGHT.getKey(), resizeHeight.toString());
+      }
+      if (resizeWidth != null) {
+        builder.addQueryParameter(Param.RESIZE_WIDTH.getKey(), resizeWidth.toString());
+      }
+      if (resizeQuality != null) {
+        builder.addQueryParameter(Param.RESIZE_QUALITY.getKey(), resizeQuality.toString());
+      }
+      if (resizeNoExpand) {
+        builder.addQueryParameter(Param.NO_EXPAND.getKey(), "1");
+      }
+      if (fallbackUrl != null) {
+        builder.addQueryParameter(Param.FALLBACK_URL_PARAM.getKey(), fallbackUrl);
+      }
+
+      if (returnOriginalContentOnError != null) {
+        builder.addQueryParameter(Param.RETURN_ORIGINAL_CONTENT_ON_ERROR.getKey(),
+                                  returnOriginalContentOnError);
+      }
+      if (htmlTagContext != null) {
+        builder.addQueryParameter(Param.HTML_TAG_CONTEXT.getKey(), htmlTagContext);
+      }
+      return builder;
+    }
+
+    @Override
+    public HttpRequest makeHttpRequest(Uri targetUri)
+        throws GadgetException {
+      HttpRequest req = super.makeHttpRequest(targetUri);
+      // Set image params:
+      req.setParam(Param.RESIZE_HEIGHT.getKey(), resizeHeight);
+      req.setParam(Param.RESIZE_WIDTH.getKey(), resizeWidth);
+      req.setParam(Param.RESIZE_QUALITY.getKey(), resizeQuality);
+      req.setParam(Param.NO_EXPAND.getKey(), resizeNoExpand ? "1" : "0");
+
+      req.setParam(Param.RETURN_ORIGINAL_CONTENT_ON_ERROR.getKey(),
+                   returnOriginalContentOnError);
+      req.setParam(Param.HTML_TAG_CONTEXT.getKey(), htmlTagContext);
+      req.setSecurityToken(securityToken);
+      req.setAuthType(authType);
+      if(AuthType.OAUTH.equals(authType)) {
+        req.setOAuthArguments(oauthArguments);
+      } else if(AuthType.OAUTH2.equals(authType)) {
+        req.setOAuth2Arguments(oauth2Arguments);
+      }
+      return req;
+    }
+
+    // Creates new ProxyUri's for the given list of resource uri's. Note that
+    // the proxy uri's will have default values for internal parameters.
+    public static List<ProxyUri> fromList(Gadget gadget, List<Uri> uris) {
+      List<ProxyUri> res = Lists.newArrayListWithCapacity(uris.size());
+      for (Uri uri : uris) {
+        res.add(new ProxyUri(gadget, uri));
+      }
+      return res;
+    }
+  }
+
+  /**
+   * Parse and validate the proxied Uri.
+   *
+   * @param uri A Uri presumed to be a proxied Uri generated
+   *     by this class or in a compatible way
+   * @return Status of the Uri passed in
+   */
+  ProxyUri process(Uri uri) throws GadgetException;
+
+  public interface Versioner {
+    /**
+     * Generates a version for each of the provided resources.
+     * @param resources Resources to version.
+     * @param container Container making the request
+     * @param resourceTags Index-correlated list of html tags, one per resouceUris. Any older
+     * implementations can just ignore.
+     * @return Index-correlated list of version strings
+     */
+    List<String> version(List<Uri> resources, String container, List<String> resourceTags);
+
+    /**
+     * Validate the version of the resource.
+     * @param resource Uri of a proxied resource
+     * @param container Container requesting the resource
+     * @param value Version value to validate.
+     * @return Status of the version.
+     */
+    UriStatus validate(Uri resource, String container, String value);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriCommon.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriCommon.java
new file mode 100644
index 0000000..f61ae85
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriCommon.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+/**
+ * Common class used for all Uri params.  Makes it very easy to find classes that
+ * use an affected parameter and to insure against duplicates.
+ */
+public interface UriCommon {
+  public static final String USER_PREF_PREFIX = "up_";
+  public enum Param {
+    URL("url"),
+    GADGET("gadget"),
+    CONTAINER("container"),
+    VIEW("view"),
+    LANG("lang"),
+    COUNTRY("country"),
+    DEBUG("debug"),
+    NO_CACHE("nocache"),
+    VERSION("v"),
+    SECURITY_TOKEN("st"),
+    OAUTH2_TOKEN("oauth_token"),
+    MODULE_ID("mid"),
+    REFRESH("refresh"),
+    LIBS("libs"),
+    JSON("json"),
+    TYPE("type"),
+    REWRITE_MIME_TYPE("rewriteMime"),
+    SANITIZE("sanitize"),
+    CAJOLE("caja"),
+
+    // JS request params
+    CONTAINER_MODE("c"),
+    COMPILE_MODE("compile"),
+    JSLOAD("jsload"),
+    ONLOAD("onload"),
+    LOADED("loaded"),
+    NO_HINT("nohint"),
+    REPOSITORY_ID("r"),
+
+    // Proxy resize params:
+    RESIZE_HEIGHT("resize_h"),
+    RESIZE_WIDTH("resize_w"),
+    RESIZE_QUALITY("resize_q"),
+    NO_EXPAND("no_expand"),
+    FALLBACK_URL_PARAM("fallback_url"),
+
+    // proxy authz params:
+    OAUTH_SERVICE_NAME("OAUTH_SERVICE_NAME"),
+    AUTHZ("authz"),
+
+    RETURN_ORIGINAL_CONTENT_ON_ERROR("rooe"),
+    // The html tag which requested this proxy uri. For example, "script" when
+    // "<script src='blah.js'></script>" is being proxied.
+    HTML_TAG_CONTEXT("html_tag_context"),
+
+    // This is a legacy param, superseded by container.
+    @Deprecated
+    SYND("synd");
+
+    private final String key;
+    private Param(String key) {
+      this.key = key;
+    }
+
+    public String getKey() {
+      return key;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriModule.java
new file mode 100644
index 0000000..4af4185
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriModule.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.util.Providers;
+
+/**
+ * Provides default configuration and bindings for Uri classes.
+ *
+ * @since 2.0.0
+ */
+public class UriModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(IframeUriManager.class).to(DefaultIframeUriManager.class);
+    bind(IframeUriManager.Versioner.class).to(AllJsIframeVersioner.class);
+
+    bind(JsUriManager.class).to(DefaultJsUriManager.class);
+    bind(JsUriManager.Versioner.class).to(DefaultJsVersioner.class);
+
+    bind(OAuthUriManager.class).to(DefaultOAuthUriManager.class);
+
+    bind(ProxyUriManager.class).to(DefaultProxyUriManager.class);
+    bind(ProxyUriManager.Versioner.class)
+        .toProvider(Providers.<ProxyUriManager.Versioner>of(null));
+
+    bind(ConcatUriManager.class).to(DefaultConcatUriManager.class);
+    bind(ConcatUriManager.Versioner.class)
+        .toProvider(Providers.<ConcatUriManager.Versioner>of(null));
+  }
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriStatus.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriStatus.java
new file mode 100644
index 0000000..181dc63
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriStatus.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+/**
+ * Contains all states of a Uri - versioned/unversioned, etc.
+ */
+public enum UriStatus {
+  VALID_VERSIONED,
+  VALID_UNVERSIONED,
+  INVALID_VERSION,
+  BAD_URI
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java
new file mode 100644
index 0000000..d99d6f1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/uri/UriUtils.java
@@ -0,0 +1,288 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utility functions related to URI and Http servlet response management.
+ *
+ * @since 2.0.0
+ */
+public final class UriUtils {
+  public static final String CHARSET = "charset";
+  //class name for logging purpose
+  private static final String classname = UriUtils.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname, MessageKeys.MESSAGES);
+
+  private UriUtils() {}
+
+  /**
+   * Enum of disallowed response headers that should not be passed on as is to
+   * the user. The webserver serving out the response should be responsible
+   * for filling these.
+   */
+  public enum DisallowedHeaders {
+    // Directives controlled by the serving infrastructure.
+    OUTPUT_TRANSFER_DIRECTIVES(ImmutableSet.of(
+        "content-length", "transfer-encoding", "content-encoding", "server",
+        "accept-ranges")),
+
+    CACHING_DIRECTIVES(ImmutableSet.of("vary", "expires", "date", "pragma",
+                                       "cache-control", "etag", "last-modified")),
+
+    CLIENT_STATE_DIRECTIVES(ImmutableSet.of("set-cookie", "set-cookie2", "www-authenticate")),
+
+    AUTHENTICATION_DIRECTIVES(ImmutableSet.of("www-authenticate")),
+
+    // Headers that the fetcher itself would like to fill. For example,
+    // httpclient library crashes if Content-Length header is set in the
+    // request being fetched.
+    POST_INCOMPATIBLE_DIRECTIVES(ImmutableSet.of("content-length")),
+
+    HOST_HEADER(ImmutableSet.of("host"));
+
+    // Miscellaneous headers we should take care of, but are left for now.
+    // "set-cookie", "content-length", "content-encoding", "etag",
+    // "last-modified" ,"accept-ranges", "vary", "expires", "date",
+    // "pragma", "cache-control", "transfer-encoding", "www-authenticate"
+
+    private Set<String> disallowedHeaders;
+    DisallowedHeaders(Set<String> disallowedHeaders) {
+      this.disallowedHeaders = disallowedHeaders;
+    }
+
+    public Set<String> getDisallowedHeaders() {
+      return disallowedHeaders;
+    }
+  }
+
+  /**
+   * Returns true if the header name is valid.
+   * NOTE: RFC 822 section 3.1.2 describes the structure of header fields.
+   * According to the RFC, a header name (or field-name) must be composed of printable ASCII
+   * characters (i.e., characters that have values between 33. and 126. decimal, except colon).
+   * @param name The header name.
+   * @return True if the header name is valid, false otherwise.
+   */
+  public static boolean isValidHeaderName(String name) {
+    char[] dst = new char[name.length()];
+    name.getChars(0, name.length(), dst, 0);
+
+    for (char c : dst) {
+      if (c < 33 || c > 126) {
+        return false;
+      }
+      if (c == ':') {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Returns true if the header value is valid.
+   * NOTE: RFC 822 section 3.1.2 describes the structure of header fields.
+   * According to the RFC, a header value (or field-body) may be composed of any ASCII characters,
+   * except CR or LF.
+   * @param val The header value.
+   * @return True if the header value is valid, false otherwise.
+   */
+  public static boolean isValidHeaderValue(String val) {
+    char[] dst = new char[val.length()];
+    val.getChars(0, val.length(), dst, 0);
+
+    for (char c : dst) {
+      if (c == 13 || c == 10) {
+        // CR and LF.
+        return false;
+      }
+      if (c > 127) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Copies the http response headers and status code to the final servlet
+   *   response.
+   * @param data The http response when fetching the requested accel uri.
+   * @param resp The servlet response to return back to client.
+   * @param remapInternalServerError If true, then SC_INTERNAL_SERVER_ERROR is
+   *   remapped to SC_BAD_GATEWAY.
+   * @param setHeaders If true, then setHeader method of HttpServletResponse is
+   *   called, otherwise addHeader is called for every header.
+   * @param disallowedResponseHeaders Disallowed response headers to omit from the response
+   *   returned to the user.
+   * @throws IOException In case the http response was not successful.
+   */
+  public static void copyResponseHeadersAndStatusCode(
+      HttpResponse data, HttpResponseBuilder resp,
+      boolean remapInternalServerError,
+      boolean setHeaders,
+      DisallowedHeaders... disallowedResponseHeaders)
+      throws IOException {
+    // Pass original return code:
+    resp.setHttpStatusCode(data.getHttpStatusCode());
+
+    Set<String> allDisallowedHeaders = new HashSet<String>();
+    for (DisallowedHeaders h : disallowedResponseHeaders) {
+      allDisallowedHeaders.addAll(h.getDisallowedHeaders());
+    }
+
+    for (Map.Entry<String, String> entry : data.getHeaders().entries()) {
+      if (isValidHeaderName(entry.getKey()) && isValidHeaderValue(entry.getValue()) &&
+          !allDisallowedHeaders.contains(entry.getKey().toLowerCase())) {
+        try {
+          if (setHeaders) {
+            resp.setHeader(entry.getKey(), entry.getValue());
+          } else {
+            resp.addHeader(entry.getKey(), entry.getValue());
+          }
+        } catch (IllegalArgumentException e) {
+          // Skip illegal header
+          if (LOG.isLoggable(Level.WARNING)) {
+            LOG.logp(Level.WARNING, classname, "copyResponseHeadersAndStatusCode", MessageKeys.SKIP_ILLEGAL_HEADER, new Object[] {entry.getKey(),entry.getValue()});
+          }
+        }
+      }
+    }
+
+    if (remapInternalServerError) {
+      // External "internal error" should be mapped to gateway error.
+      if (data.getHttpStatusCode() == HttpResponse.SC_INTERNAL_SERVER_ERROR) {
+        resp.setHttpStatusCode(HttpResponse.SC_BAD_GATEWAY);
+      }
+    }
+  }
+
+  /**
+   * Copies headers from HttpServletRequest object to HttpRequest object.
+   * @param origRequest Servlet request to copy headers from.
+   * @param req The HttpRequest object to copy headers to.
+   * @param disallowedRequestHeaders Disallowed request headers to omit from
+   *   the servlet request
+   */
+  public static void copyRequestHeaders(HttpRequest origRequest,
+                                        HttpRequest req,
+                                        DisallowedHeaders... disallowedRequestHeaders) {
+    Set<String> allDisallowedHeaders = new HashSet<String>();
+    for (DisallowedHeaders h : disallowedRequestHeaders) {
+      allDisallowedHeaders.addAll(h.getDisallowedHeaders());
+    }
+
+    for (Map.Entry<String, List<String>> inHeader : origRequest.getHeaders().entrySet()) {
+      String header = inHeader.getKey();
+      List<String> headerValues = inHeader.getValue();
+
+      if (headerValues != null && !headerValues.isEmpty() &&
+          isValidHeaderName(header) &&
+          !allDisallowedHeaders.contains(header.toLowerCase())) {
+        // Remove existing values of this header.
+        req.removeHeader(header);
+        for (String headerVal : headerValues) {
+          if (isValidHeaderValue(headerVal)) {
+            req.addHeader(header, headerVal);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Copies the post data from HttpServletRequest object to HttpRequest object.
+   * @param origRequest Request to copy post data from.
+   * @param req The HttpRequest object to copy post data to.
+   * @throws GadgetException In case of errors.
+   */
+  public static void copyRequestData(HttpRequest origRequest,
+                                     HttpRequest req) throws GadgetException {
+    req.setMethod(origRequest.getMethod());
+    try {
+      if (origRequest.getMethod().equalsIgnoreCase("post")) {
+        req.setPostBody(origRequest.getPostBody());
+      }
+    } catch (IOException e) {
+      throw new GadgetException(GadgetException.Code.INTERNAL_SERVER_ERROR, e);
+    }
+  }
+
+  /**
+   * Rewrite the content type of the final http response if the request has the
+   * rewrite-mime-type param.
+   * @param req The http request.
+   * @param response The final http response to be returned to user.
+   */
+  public static void maybeRewriteContentType(HttpRequest req, HttpResponseBuilder response) {
+    String responseType = response.getHeader("Content-Type");
+    String requiredType = req.getRewriteMimeType();
+    if (!Strings.isNullOrEmpty(requiredType)) {
+      // Use a 'Vary' style check on the response
+      if (requiredType.endsWith("/*") && !Strings.isNullOrEmpty(responseType)) {
+        String requiredTypePrefix = requiredType.substring(0, requiredType.length() - 1);
+        if (!responseType.toLowerCase().startsWith(requiredTypePrefix.toLowerCase())) {
+          // TODO: We are currently setting the content type to something like x/* (e.g. text/*)
+          // which is not a valid content type. Need to fix this.
+          response.setHeader("Content-Type", requiredType);
+        }
+      } else {
+        response.setHeader("Content-Type", requiredType);
+      }
+    }
+  }
+
+  /**
+   * Parses the value of content-type header and returns the content type header
+   * without the 'charset' attribute.
+   * @param content The content type header value.
+   * @return Content type header value without charset.
+   */
+  public static String getContentTypeWithoutCharset(String content) {
+    String contentTypeWithoutCharset = content;
+    String[] parts = StringUtils.split(content, ';');
+    if (parts.length >= 2) {
+      StringBuilder contentTypeWithoutCharsetBuilder = new StringBuilder(parts.length);
+      contentTypeWithoutCharsetBuilder.append(parts[0]);
+
+      for (int i = 1; i < parts.length; i++) {
+        String parameterAndValue = parts[i].trim().toLowerCase();
+        String[] splits = StringUtils.split(parameterAndValue, '=');
+        if (splits.length > 0 && !splits[0].trim().equals(CHARSET)) {
+          contentTypeWithoutCharsetBuilder.append(';').append(parts[i]);
+        }
+      }
+      contentTypeWithoutCharset = contentTypeWithoutCharsetBuilder.toString();
+    }
+
+    return contentTypeWithoutCharset;
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/BidiSubstituter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/BidiSubstituter.java
new file mode 100644
index 0000000..453cff6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/BidiSubstituter.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.MessageBundleFactory;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+/**
+ * Provides static hangman substitutions for bidirectional language support.
+ * Useful for generating internationalized layouts using CSS.
+ */
+public class BidiSubstituter implements Substituter {
+  public static final String START_EDGE = "START_EDGE";
+  public static final String END_EDGE = "END_EDGE";
+  public static final String DIR = "DIR";
+  public static final String REVERSE_DIR = "REVERSE_DIR";
+
+  public static final String RIGHT = "right";
+  public static final String LEFT = "left";
+  public static final String RTL = "rtl";
+  public static final String LTR = "ltr";
+
+  private final MessageBundleFactory messageBundleFactory;
+
+  @Inject
+  public BidiSubstituter(MessageBundleFactory messageBundleFactory) {
+    this.messageBundleFactory = messageBundleFactory;
+  }
+
+  public void addSubstitutions(Substitutions substituter, GadgetContext context, GadgetSpec spec)
+      throws GadgetException {
+    MessageBundle bundle =
+        messageBundleFactory.getBundle(spec, context.getLocale(), context.getIgnoreCache(),
+                    context.getContainer(), context.getView());
+    String dir = bundle.getLanguageDirection();
+
+    boolean rtl = RTL.equals(dir);
+    substituter.addSubstitution(Substitutions.Type.BIDI, START_EDGE, rtl ? RIGHT : LEFT);
+    substituter.addSubstitution(Substitutions.Type.BIDI, END_EDGE, rtl ? LEFT : RIGHT);
+    substituter.addSubstitution(Substitutions.Type.BIDI, DIR, rtl ? RTL : LTR);
+    substituter.addSubstitution(Substitutions.Type.BIDI, REVERSE_DIR, rtl ? LTR : RTL);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/MessageSubstituter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/MessageSubstituter.java
new file mode 100644
index 0000000..14aebe5
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/MessageSubstituter.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.MessageBundleFactory;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+/**
+ * Provides static hangman substitutions for message bundles.
+ *
+ * @since 2.0.0
+ */
+public class MessageSubstituter implements Substituter {
+  private final MessageBundleFactory messageBundleFactory;
+
+  @Inject
+  public MessageSubstituter(MessageBundleFactory messageBundleFactory) {
+    this.messageBundleFactory = messageBundleFactory;
+  }
+
+  public void addSubstitutions(Substitutions substituter, GadgetContext context, GadgetSpec spec)
+          throws GadgetException {
+    MessageBundle bundle = messageBundleFactory.getBundle(spec, context.getLocale(),
+        context.getIgnoreCache(), context.getContainer(), context.getView());
+
+    substituter.addSubstitutions(Substitutions.Type.MESSAGE, bundle.getMessages());
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/ModuleSubstituter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/ModuleSubstituter.java
new file mode 100644
index 0000000..f719411
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/ModuleSubstituter.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+/**
+ * Provides hangman substitution variables related to the Module (i.e. __MODULE_ID__)
+ *
+ * @since 2.0.0
+ */
+public class ModuleSubstituter implements Substituter {
+  public void addSubstitutions(Substitutions substituter, GadgetContext context, GadgetSpec spec)
+        throws GadgetException {
+    substituter.addSubstitution(Substitutions.Type.MODULE, "ID", Long.toString(context.getModuleId()));
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/Substituter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/Substituter.java
new file mode 100644
index 0000000..67d306d
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/Substituter.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import org.apache.shindig.gadgets.GadgetException;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+/**
+ * Substituter that provides variables to {@link VariableSubstituter}.
+ *
+ * @since 2.0.0
+ */
+public interface Substituter {
+
+  /**
+   * Add the substitutions from this Substituter to the {@link Substitutions}.
+   *
+   * @param substituter container for the new substitutions, containing any existing substitutions
+   * @param context the context in which this gadget is being rendered
+   * @param spec the gadget specification being substituted
+   * @throws GadgetException when there has been a general error adding substitutions
+   */
+  void addSubstitutions(Substitutions substituter, GadgetContext context, GadgetSpec spec) throws GadgetException;
+
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/SubstituterModule.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/SubstituterModule.java
new file mode 100644
index 0000000..b1643b4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/SubstituterModule.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+
+/**
+ * Guice bindings for the variables package.
+ *
+ * @since 2.0.0
+ */
+public class SubstituterModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(new TypeLiteral<List<Substituter>>(){})
+        .annotatedWith(Names.named("shindig.substituters.gadget"))
+        .toProvider(SubstitutersProvider.class);
+  }
+
+  public static class SubstitutersProvider implements Provider<List<Substituter>> {
+    private final List<Substituter> substituters;
+
+    @Inject
+    public SubstitutersProvider(MessageSubstituter messageSubstituter,
+        UserPrefSubstituter prefSubstituter,
+        ModuleSubstituter moduleSubstituter,
+        BidiSubstituter bidiSubstituter) {
+      substituters = Lists.newArrayList();
+      substituters.add(messageSubstituter);
+      substituters.add(prefSubstituter);
+      substituters.add(moduleSubstituter);
+      substituters.add(bidiSubstituter);
+    }
+
+    public List<Substituter> get() {
+      return substituters;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/Substitutions.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/Substitutions.java
new file mode 100644
index 0000000..13d37b1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/Substitutions.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import org.apache.shindig.common.uri.Uri;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+/**
+ * Performs string substitutions for message bundles, user prefs, and bidi
+ * variables.
+ */
+public class Substitutions {
+  /**
+   * Defines all of the valid types of message substitutions.
+   *
+   * NOTE: Order is critical here, since substitutions are only recursive on nodes with lower order
+   * this is to prevent infinite recursion in substitution logic.
+   */
+  public enum Type {
+    /**
+     * Localization strings.
+     */
+    MESSAGE("MSG"),
+
+    /**
+     * User preferences.
+     */
+    USER_PREF("UP"),
+
+    /**
+     * MODULE_ variables (i.e. MODULE_ID)
+     */
+    MODULE("MODULE"),
+
+    /**
+     * Bi-directional text transformations.
+     */
+    BIDI("BIDI");
+
+    private final String prefix;
+
+    /**
+     * Creates a Type with the specified prefix.
+     *
+     * @param prefix
+     *        The placeholder prefix for substituted strings.
+     */
+    Type(String prefix) {
+      this.prefix = "__" + prefix + '_';
+    }
+  }
+
+  private final Map<String, String> substitutions;
+
+  public Substitutions() {
+    substitutions = Maps.newHashMap();
+  }
+
+
+  /**
+   * Adds a new substitution for the given type.
+   *
+   * @param type
+   * @param key
+   * @param value
+   */
+  public void addSubstitution(Type type, String key, String value) {
+    substitutions.put(type.prefix + key, substituteString(value));
+  }
+
+  /**
+   * @return The value stored for the given type and key, or null.
+   */
+  public String getSubstitution(Type type, String key) {
+    return substitutions.get(type.prefix + key);
+  }
+
+  /**
+   * Adds many substitutions of the same type at once.
+   *
+   * @param type
+   * @param entries
+   */
+  public void addSubstitutions(Type type, Map<String, String> entries) {
+    for (Map.Entry<String, String> entry : entries.entrySet()) {
+      addSubstitution(type, entry.getKey(), entry.getValue());
+    }
+  }
+
+  private void performSubstitutions(String input, StringBuilder output, boolean isNested) {
+    int lastPosition = 0, i;
+    while ((i = input.indexOf("__", lastPosition)) != -1) {
+      int next = input.indexOf("__", i + 2);
+      if (next == -1) {
+        // No matches, we're done.
+        break;
+      }
+
+      output.append(input.substring(lastPosition, i));
+
+      String pattern = input.substring(i, next);
+
+      boolean isMessage = pattern.startsWith(Type.MESSAGE.prefix);
+      String replacement;
+
+      if (isMessage && isNested) {
+        replacement = pattern + "__";
+      } else {
+        replacement = substitutions.get(pattern);
+      }
+
+      if (replacement == null) {
+        // Just append the first underbar of the __ prefix. The substitution
+        // selection algorithm will move on to the next underbar, which itself
+        // might be a __ prefix suitable for substitution, ensuring proper
+        // accommodation of cases such as ___MODULE_ID__.
+        output.append('_');
+        lastPosition = i + 1;
+      } else {
+        lastPosition = next + 2;
+        if (isMessage && !isNested) {
+          // Messages can be recursive
+          performSubstitutions(replacement, output, true);
+        } else {
+          output.append(replacement);
+        }
+      }
+    }
+
+    output.append(input.substring(lastPosition));
+  }
+
+  /**
+   * Performs string substitution only for the specified type. If no
+   * substitution for {@code input} was provided or {@code input} is null,
+   * the output is left untouched.
+   * @param input The base string, with substitution markers.
+   * @return The substituted string.
+   */
+  public String substituteString(String input) {
+    if (input.contains("__")) {
+      StringBuilder output = new StringBuilder(input.length() * 120 / 100);
+      performSubstitutions(input, output, false);
+      return output.toString();
+    }
+    return input;
+  }
+
+  /**
+   * Substitutes a uri
+   * @param uri
+   * @return The substituted uri, or a dummy value if the result is invalid.
+   */
+  public Uri substituteUri(Uri uri) {
+    if (uri == null) {
+      return null;
+    }
+    try {
+      return Uri.parse(substituteString(uri.toString()));
+    } catch (IllegalArgumentException e) {
+      return Uri.parse("");
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/UserPrefSubstituter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/UserPrefSubstituter.java
new file mode 100644
index 0000000..317c0d1
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/UserPrefSubstituter.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.UserPrefs;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.UserPref;
+
+/**
+ * Substitutes user prefs into the spec.
+ */
+public class UserPrefSubstituter implements Substituter {
+
+  public void addSubstitutions(Substitutions substituter, GadgetContext context, GadgetSpec spec) {
+    UserPrefs values = context.getUserPrefs();
+
+    for (UserPref pref : spec.getUserPrefs().values()) {
+      String name = pref.getName();
+      String value = values.getPref(name);
+      if (value == null) {
+        value = pref.getDefaultValue();
+        if (value == null) {
+          value = "";
+        }
+      }
+      substituter.addSubstitution(Substitutions.Type.USER_PREF, name, StringEscapeUtils
+            .escapeHtml4(value));
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/VariableSubstituter.java b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/VariableSubstituter.java
new file mode 100644
index 0000000..ac8787f
--- /dev/null
+++ b/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/variables/VariableSubstituter.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+/**
+ * Performs variable substitution on a gadget spec.
+ */
+public class VariableSubstituter {
+  private final List<Substituter> substituters;
+
+  @Inject
+  public VariableSubstituter(@Named("shindig.substituters.gadget") List<Substituter> substituters) {
+    this.substituters = ImmutableList.copyOf(substituters);
+  }
+
+  /**
+   * Substitutes all hangman variables into the gadget spec.
+   *
+   * @return A new GadgetSpec, with all fields substituted as needed.
+   */
+  public GadgetSpec substitute(GadgetContext context, GadgetSpec spec) throws GadgetException {
+    Substitutions substitutions = new Substitutions();
+
+    for (Substituter substituter : substituters) {
+        substituter.addSubstitutions(substitutions, context, spec);
+    }
+
+    return spec.substitute(substitutions);
+  }
+}
diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource.properties
new file mode 100644
index 0000000..ab41327
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource.properties
@@ -0,0 +1,81 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.  
+message_header= {0} encountered an error :  
+authorization_code_problem=The authorization code is being exchanged for the access token : {0}
+authorization_code_problem.explanation=An error occurred when exchanging the authorization code for the access_token.
+authorize_problem=The authorization process for {0} is initiating.
+authorize_problem.explanation=An error occurred when initiating authorization process.
+callback_problem=The redirect response is being processed : {0}
+callback_problem.explanation=An error occurred when processing the redirect response from the service provider.
+client_credentials_problem=The access_token is being retrieved for the client with the following client credential : {0}
+client_credentials_problem.explanation=An error occurred when retrieving access token in the client_credentials flow.
+fetch_init_problem=The OAuth2Request is initializing: {0}
+fetch_init_problem.explanation=A low-level error occurred when initializing the OAuth2Request fetch.
+fetch_problem=executing OAuth2Request.fetch() : {0}
+fetch_problem.explanation=An error occurred when issuing the OAuth2Request fetch.
+gadget_spec_problem=The following gadget specification is processing : {0}
+gadget_spec_problem.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.
+get_oauth2_accessor_problem=getting an OAuth2Accessor for the OAuth2Request : {0}
+get_oauth2_accessor_problem.explanation=An error occured. Ask your administrator to create an OAuth2.0 Client binding for this gadget and service.
+lookup_spec_problem=The gadget specification is being retrieved : {0}
+lookup_spec_problem.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.
+missing_fetch_params=The following required fetch parameters are missing : {0} 
+missing_fetch_params.explanation=The fetch() method was called with bad parameters.
+missing_server_response=An error occurred during OAuth2.0 service provider response : {0}
+missing_server_response.explanation=The server created a valid OAuth2Request but was either unable to issue the request or get a valid response from the service provider.
+no_response_handler=A response handler did not process the following response : {0}
+no_response_handler.explanation=Response handler do not exists for processing the authorization and token endpoints. AuthorizationEndpointResponseHandler or TokenEndpointResponseHandler.
+no_gadget_spec=The gadget specification for {0} can not be found.
+no_gadget_spec.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.
+refresh_token_problem=The refresh token is being exchanged for the access_token : {0}
+refresh_token_problem.explanation=An error occurred when exchanging the refresh token for the access token.
+secret_encryption_problem=The token secret is bing encrypted for persistence : {0}
+secret_encryption_problem.explanation=An error occurred when storing OAuth2.0 secrets with the provided encryption module.
+access_denied=Access was denied.
+access_denied.explanation=The user denied or canceled the authorization with the service provider.
+invalid_client=The client is invalid : {0}
+invalid_client.explanation= The client cannot be authenticated,possibly because hte client is not known., the client authentication was not included, or the authentication method is not suppported.
+invalid_grant=The authorization grant is invalid : {0}
+invalid_grant.explanation=The authorization grant is invalid,expired,revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The authorization grant can include the authorization code, the credentials of the resource owner, or the credentials of the client.
+invalid_request=The request is invalid : {0}
+invalid_request.explanation=The request is invalid because it is missing a required parameter, includes an unsupported parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.
+invalid_scope=The scope is invalid : {0}
+invalid_scope.explanation=The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.
+server_error=A server error occurred: {0}
+server_error.explanation=The authorization server encountered an unexpected condition which prevented it from fulfilling the request.
+server_rejected_request=The server rejected the request : statuscode = {0}
+server_rejected_request.explanation=The server created an OAuth2Request, but the service provider rejected it.
+temporarily_unavailable=The service is temporarily unavailable : {0}
+temporarily_unavailable.explanation=The authorization server is unable to handle the request because it is temporarily overloaded or is in maintenance mode.
+token_response_problem=The response from token endpoint is being processed : {0}
+token_response_problem.explanation=The token endpoint sent a response that the server could not process.
+unauthorized_client=The client is not authorized : {0}
+unauthorized_client.explanation=The client is not authorized to request an access token using this method.
+unsupported_grant_type=The grant type if not supported : {0}
+unsupported_grant_type.explanation=The authorization server does not support obtaining an access token using this method.
+unsupported_response_type=The response type is unsupported: {0}
+unsupported_response_type.explanation=The authorization server does not support obtaining an authorization code using this method.
+mac_token_problem=The mac token is being requested : {0}
+mac_token_problem.explanation=The mac token on the request could not be added to the resource server.
+bearer_token_problem=The bearer token is being requested : {0}
+bearer_token_problem.explanation=The bearer token on the request could not be added to the resource server.
+authentication_problem=The client authentication is being added : {0}
+authentication_problem.explanation=The authentication headers could not be added to the request.
+unknown_problem=An unknown error occurred : {0}
+unknown_problem.explanation=Ask your system administrator to investigate the problem.
+code_grant_problem=The access token being obtained from the authorization code : {0}
+code_grant_problem.explanation=An error occurred during the authorization code flow.
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ar.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ar.properties
new file mode 100644
index 0000000..285be72
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ar.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= \u0642\u0627\u0645 {0} \u0628\u0627\u0643\u062a\u0634\u0627\u0641 \u062e\u0637\u0623 :

+authorization_code_problem=\u062c\u0627\u0631\u064a \u062a\u0628\u0627\u062f\u0644 \u0643\u0648\u062f \u0627\u0644\u0635\u0644\u0627\u062d\u064a\u0627\u062a \u0644\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u062a\u0648\u0635\u0644 : {0}

+authorization_code_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u062a\u0628\u0627\u062f\u0644 \u0643\u0648\u062f \u0627\u0644\u062a\u0648\u062b\u064a\u0642 \u0627\u0644\u0649 \u200f\u200eaccess_token\u200e\u200f.

+authorize_problem=\u062a\u0645 \u0628\u062f\u0621 \u0639\u0645\u0644\u064a\u0629 \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u0627\u0644\u0649 {0}.

+authorize_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u0628\u062f\u0621 \u0639\u0645\u0644\u064a\u0629 \u0627\u0644\u062a\u0631\u062e\u064a\u0635.

+callback_problem=\u062c\u0627\u0631\u064a \u062a\u0634\u063a\u064a\u0644 \u0627\u0639\u0627\u062f\u0629 \u062a\u0648\u062c\u064a\u0647 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 : {0}

+callback_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u062a\u0634\u063a\u064a\u0644 \u0627\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0648\u062c\u064a\u0647 \u0644\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0645\u0646 \u062c\u0647\u0629 \u0627\u062a\u0627\u062d\u0629 \u0627\u0644\u062e\u062f\u0645\u0629.

+client_credentials_problem=\u062c\u0627\u0631\u064a \u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u200f\u200eaccess_token\u200e\u200f \u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0627\u0639\u062a\u0645\u0627\u062f \u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 \u0627\u0644\u062a\u0627\u0644\u064a : {0}

+client_credentials_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u062a\u0648\u0635\u0644 \u0641\u064a \u0645\u0633\u0627\u0631 \u200f\u200eclient_credentials\u200e\u200f.

+fetch_init_problem=\u062c\u0627\u0631\u064a \u0627\u0644\u0627\u0639\u062f\u0627\u062f \u0644\u0628\u062f\u0621 OAuth2Request: {0}

+fetch_init_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0645\u0646\u062e\u0641\u0636 \u0627\u0644\u0645\u0633\u062a\u0648\u0649 \u0639\u0646\u062f \u0627\u0644\u0627\u0639\u062f\u0627\u062f \u0644\u0628\u062f\u0621 OAuth2Request.

+fetch_problem=\u062a\u0646\u0641\u064a\u0630 OAuth2Request.fetch() : {0}

+fetch_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u0627\u0635\u062f\u0627\u0631 \u0627\u062d\u0636\u0627\u0631 OAuth2Request.

+gadget_spec_problem=\u062c\u0627\u0631\u064a \u062a\u0634\u063a\u064a\u0644 \u0645\u062d\u062f\u062f\u0627\u062a \u0627\u0644\u0623\u062f\u0627\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629 : {0}

+gadget_spec_problem.explanation=\u0644\u0645 \u064a\u062a\u0645 \u0627\u064a\u062c\u0627\u062f \u0645\u0648\u0627\u0635\u0641\u0627\u062a \u0627\u0644\u0623\u062f\u0627\u0629.  \u0623\u0637\u0644\u0628 \u0645\u0646 \u0645\u0648\u062c\u0647 \u0627\u0644\u0646\u0638\u0627\u0645 \u0623\u0646 \u064a\u0642\u0648\u0645 \u0628\u062a\u0648\u0635\u064a\u0641 \u0627\u0644\u0623\u062f\u0627\u0629 \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643 \u0644\u0644\u062a\u0639\u0627\u0645\u0644 \u0645\u0639 OAuth2.0.

+get_oauth2_accessor_problem=\u0627\u062d\u0636\u0627\u0631 OAuth2Accessor \u0627\u0644\u0649 OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623. \u0623\u0637\u0644\u0628 \u0645\u0646 \u0645\u0648\u062c\u0647 \u0627\u0644\u0646\u0638\u0627\u0645 \u0623\u0646 \u064a\u0642\u0648\u0645 \u0628\u062a\u0643\u0648\u064a\u0646 \u0631\u0627\u0628\u0637 OAuth2.0 Client \u0644\u0647\u0630\u0647 \u0627\u0644\u0623\u062f\u0627\u0629 \u0648\u0627\u0644\u062e\u062f\u0645\u0629.

+lookup_spec_problem=\u062c\u0627\u0631\u064a \u0627\u0633\u062a\u0631\u062c\u0627\u0639 \u0645\u0648\u0627\u0635\u0641\u0627\u062a \u0627\u0644\u0623\u062f\u0627\u0629 : {0}

+lookup_spec_problem.explanation=\u0644\u0645 \u064a\u062a\u0645 \u0627\u064a\u062c\u0627\u062f \u0645\u0648\u0627\u0635\u0641\u0627\u062a \u0627\u0644\u0623\u062f\u0627\u0629.  \u0623\u0637\u0644\u0628 \u0645\u0646 \u0645\u0648\u062c\u0647 \u0627\u0644\u0646\u0638\u0627\u0645 \u0623\u0646 \u064a\u0642\u0648\u0645 \u0628\u062a\u0648\u0635\u064a\u0641 \u0627\u0644\u0623\u062f\u0627\u0629 \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643 \u0644\u0644\u062a\u0639\u0627\u0645\u0644 \u0645\u0639 OAuth2.0.

+missing_fetch_params=\u0645\u0639\u0627\u0645\u0644\u0627\u062a \u0627\u0644\u0627\u062d\u0636\u0627\u0631 \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629 \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f\u0629 : {0}

+missing_fetch_params.explanation=\u062a\u0645 \u0627\u0633\u062a\u062f\u0639\u0627\u0621 \u0627\u0644\u0637\u0631\u064a\u0642\u0629 fetch() \u0628\u0645\u0639\u0627\u0645\u0644\u0627\u062a \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629.

+missing_server_response=\u062d\u062f\u062b \u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u062c\u0647\u0629 \u062a\u0642\u062f\u064a\u0645 \u0627\u0644\u062e\u062f\u0645\u0629 \u200f\u200eOAuth2.0\u200e\u200f : {0}

+missing_server_response.explanation=\u0642\u0627\u0645\u062a \u0648\u062d\u062f\u0629 \u0627\u0644\u062e\u062f\u0645\u0629 \u0628\u062a\u0643\u0648\u064a\u0646 OAuth2Request \u0635\u062d\u064a\u062d \u0648\u0644\u0643\u0646\u0647\u0627 \u0643\u0627\u0646\u062a \u063a\u064a\u0631 \u0645\u062a\u0627\u062d\u0629 \u0644\u0627\u0635\u062f\u0627\u0631 \u0627\u0644\u0637\u0644\u0628 \u0623\u0648 \u0642\u0645 \u0628\u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0635\u062d\u064a\u062d\u0629 \u0645\u0646 \u062c\u0647\u0629 \u0627\u062a\u0627\u062d\u0629 \u0627\u0644\u062e\u062f\u0645\u0629.

+no_response_handler=\u0644\u0645 \u064a\u0642\u0645 \u0628\u0631\u0646\u0627\u0645\u062c \u0645\u0639\u0627\u0644\u062c\u0629 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0628\u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0627\u0644\u062a\u0627\u0644\u064a\u0629 : {0}

+no_response_handler.explanation=\u0628\u0631\u0646\u0627\u0645\u062d \u0627\u0644\u0645\u0639\u0627\u0644\u062c\u0629 \u0644\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u063a\u064a\u0631 \u0645\u0648\u062c\u0648\u062f \u0644\u064a\u0642\u0648\u0645 \u0628\u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u0648\u0627\u0644\u0646\u0642\u0627\u0637 \u0627\u0644\u0637\u0631\u0641\u064a\u0629 \u0644\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632. AuthorizationEndpointResponseHandler \u0623\u0648 TokenEndpointResponseHandler.

+no_gadget_spec=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u064a\u062c\u0627\u062f \u0645\u0648\u0627\u0635\u0641\u0627\u062a \u0627\u0644\u0623\u062f\u0627\u0629 \u0627\u0644\u0649 {0}.

+no_gadget_spec.explanation=\u0644\u0645 \u064a\u062a\u0645 \u0627\u064a\u062c\u0627\u062f \u0645\u0648\u0627\u0635\u0641\u0627\u062a \u0627\u0644\u0623\u062f\u0627\u0629.  \u0623\u0637\u0644\u0628 \u0645\u0646 \u0645\u0648\u062c\u0647 \u0627\u0644\u0646\u0638\u0627\u0645 \u0623\u0646 \u064a\u0642\u0648\u0645 \u0628\u062a\u0648\u0635\u064a\u0641 \u0627\u0644\u0623\u062f\u0627\u0629 \u0627\u0644\u062e\u0627\u0635\u0629 \u0628\u0643 \u0644\u0644\u062a\u0639\u0627\u0645\u0644 \u0645\u0639 OAuth2.0.

+refresh_token_problem=\u062c\u0627\u0631\u064a \u062a\u0628\u0627\u062f\u0644 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u062a\u062c\u062f\u064a\u062f \u0627\u0644\u0649 \u200f\u200eaccess_token\u200e\u200f : {0}

+refresh_token_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u062a\u0628\u0627\u062f\u0644 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u062a\u062c\u062f\u064a\u062f \u0644\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u062a\u0648\u0635\u0644.

+secret_encryption_problem=\u062c\u0627\u0631\u064a \u062a\u0634\u0641\u064a\u0631 \u0633\u0631 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u0627\u0633\u062a\u0645\u0631\u0627\u0631 : {0}

+secret_encryption_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0639\u0646\u062f \u062a\u062e\u0632\u064a\u0646 \u0623\u0633\u0631\u0627\u0631 OAuth2.0 \u0645\u0639 \u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0634\u0641\u064a\u0631 \u0627\u0644\u0645\u0642\u062f\u0645\u0629.

+access_denied=\u062a\u0645 \u0631\u0641\u0636 \u0627\u0644\u0627\u062a\u0635\u0627\u0644.

+access_denied.explanation=\u0642\u0627\u0645 \u0627\u0644\u0645\u0633\u062a\u062e\u062f\u0645 \u0628\u0631\u0641\u0636 \u0623\u0648 \u0627\u0644\u063a\u0627\u0621 \u0627\u0644\u0635\u0644\u0627\u062d\u064a\u0627\u062a \u0645\u0639 \u062c\u0647\u0629 \u062a\u0642\u062f\u064a\u0645 \u0627\u0644\u062e\u062f\u0645\u0629.

+invalid_client=\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d : {0}

+invalid_client.explanation= \u0644\u0627 \u064a\u0645\u0643\u0646 \u062a\u0648\u062b\u064a\u0642 \u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629\u060c \u0642\u062f \u064a\u0643\u0648\u0646 \u0630\u0644\u0643 \u0644\u0623\u0646 \u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641. \u0648\u0644\u0645 \u064a\u062a\u0645 \u062a\u0636\u0645\u064a\u0646 \u062a\u0648\u062b\u064a\u0642 \u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629\u060c \u0623\u0648 \u0623\u0646 \u0637\u0631\u064a\u0642\u0629 \u0627\u0644\u062a\u0648\u062b\u064a\u0642 \u063a\u064a\u0631 \u0645\u062f\u0639\u0645\u0629.

+invalid_grant=\u0645\u0646\u062d \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u064a\u0639\u062f \u063a\u064a\u0631 \u0635\u062d\u064a\u062d : {0}

+invalid_grant.explanation=\u0645\u0646\u062d \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u060c \u0623\u0648 \u0645\u0646\u062a\u0647\u064a \u0623\u0648 \u0645\u0631\u0641\u0648\u0636\u060c \u0623\u0648 \u0644\u0627 \u064a\u0637\u0627\u0628\u0642  URI \u0644\u0627\u0639\u0627\u062f\u0629 \u0627\u0644\u062a\u0648\u062c\u064a\u0647 \u0627\u0644\u0630\u064a \u064a\u062a\u0645 \u0627\u0633\u062a\u062e\u062f\u0627\u0645\u0647 \u0641\u064a \u0637\u0644\u0628 \u0627\u0644\u062a\u0631\u062e\u064a\u0635\u060c \u0623\u0648 \u062a\u0645 \u0627\u0635\u062f\u0627\u0631\u0647 \u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0622\u062e\u0631. \u0645\u0646\u062d \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u064a\u0645\u0643\u0646 \u0623\u0646 \u064a\u062a\u0636\u0645\u0646 \u0643\u0648\u062f \u0627\u0644\u062a\u0631\u062e\u064a\u0635\u060c \u0623\u0648 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0627\u0639\u062a\u0645\u0627\u062f \u0644\u0645\u0627\u0644\u0643 \u0627\u0644\u0645\u0635\u062f\u0631\u060c \u0623\u0648 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0627\u0639\u062a\u0645\u0627\u062f \u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629.

+invalid_request=\u0627\u0644\u0637\u0644\u0628 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d : {0}

+invalid_request.explanation=\u0627\u0644\u0637\u0644\u0628 \u064a\u0639\u062f \u063a\u064a\u0631 \u0635\u062d\u064a\u062d \u0644\u0623\u0646\u0647 \u064a\u0641\u062a\u0642\u062f \u0623\u062d\u062f \u0627\u0644\u0645\u0639\u0627\u0645\u0644\u0627\u062a \u0627\u0644\u0645\u0637\u0644\u0648\u0628\u0629\u060c \u0623\u0648 \u064a\u062a\u0636\u0645\u0646 \u0642\u064a\u0645\u0629 \u0645\u0639\u0627\u0645\u0644 \u063a\u064a\u0631 \u0645\u062f\u0639\u0645\u0629\u060c \u0623\u0648 \u064a\u0642\u0648\u0645 \u0628\u062a\u0643\u0631\u0627\u0631 \u0623\u062d\u062f \u0627\u0644\u0645\u0639\u0627\u0645\u0644\u0627\u062a\u060c \u0623\u0648 \u064a\u062a\u0636\u0645\u0646 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0639\u062a\u0645\u0627\u062f \u0645\u062a\u0639\u062f\u062f\u0629\u060c \u0623\u0648 \u064a\u0642\u0648\u0645 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0623\u0643\u062b\u0631 \u0645\u0646 \u0648\u0627\u062d\u062f\u0629 \u0645\u0646 \u0622\u0644\u064a\u062a \u0627\u0644\u062a\u0648\u062b\u064a\u0642\u0629 \u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629\u060c \u0623\u0648 \u0628\u0647 \u062a\u0644\u0641 \u0622\u062e\u0631.

+invalid_scope=\u0627\u0644\u0646\u0637\u0627\u0642 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d : {0}

+invalid_scope.explanation=\u0627\u0644\u0646\u0637\u0627\u0642 \u0627\u0644\u0645\u0637\u0644\u0648\u0628 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d \u0623\u0648 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641 \u0623\u0648 \u0628\u0647 \u062a\u0644\u0641\u060c \u0623\u0648 \u064a\u062a\u0639\u062f\u0649 \u0627\u0644\u0646\u0637\u0627\u0642 \u0627\u0644\u0630\u064a \u064a\u062a\u064a\u062d\u0647 \u0645\u0627\u0644\u0643 \u0627\u0644\u0645\u0635\u062f\u0631.

+server_error=\u062d\u062f\u062b \u062e\u0637\u0623 \u0628\u0648\u062d\u062f\u0629 \u0627\u0644\u062e\u062f\u0645\u0629: {0}

+server_error.explanation=\u0627\u0643\u062a\u0634\u0641\u062a \u0648\u062d\u062f\u0629 \u062e\u062f\u0645\u0629 \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u062d\u0627\u0644\u0629 \u063a\u064a\u0631 \u0635\u062d\u064a\u062d\u0629 \u062a\u0645\u0646\u0639 \u0627\u0644\u0648\u0641\u0627\u0621 \u0628\u0627\u0644\u0637\u0644\u0628.

+server_rejected_request=\u0642\u0627\u0645\u062a \u0648\u062d\u062f\u0629 \u0627\u0644\u062e\u062f\u0645\u0629 \u0628\u0631\u0641\u0636 \u0627\u0644\u0637\u0644\u0628 : \u0643\u0648\u062f \u0627\u0644\u062d\u0627\u0644\u0629 = {0}

+server_rejected_request.explanation=\u0642\u0627\u0645\u062a \u0648\u062d\u062f\u0629 \u0627\u0644\u062e\u062f\u0645\u0629 \u0628\u062a\u0643\u0648\u064a\u0646 OAuth2Request\u060c \u0648\u0644\u0643\u0646 \u0642\u0627\u0645\u062a \u062c\u0647\u0629 \u062a\u0642\u062f\u064a\u0645 \u0627\u0644\u062e\u062f\u0645\u0629 \u0628\u0631\u0641\u0636\u0647.

+temporarily_unavailable=\u0648\u062d\u062f\u0629 \u0627\u0644\u062e\u062f\u0645\u0629 \u063a\u064a\u0631 \u0645\u062a\u0627\u062d\u0629 \u0628\u0635\u0641\u0629 \u0645\u0624\u0642\u062a\u0629 : {0}

+temporarily_unavailable.explanation=\u0648\u062d\u062f\u0629 \u062e\u062f\u0645\u0629 \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u063a\u064a\u0631 \u0645\u062a\u0627\u062d\u0629 \u0644\u0645\u0639\u0627\u0644\u062c\u0629 \u0627\u0644\u0637\u0644\u0628\u060c \u0628\u0633\u0628\u0628 \u0632\u064a\u0627\u062f\u0629 \u0627\u0644\u062a\u062d\u0645\u064a\u0644 \u0628\u0635\u0641\u0629 \u0645\u0624\u0642\u062a\u0629 \u0623\u0648 \u062c\u0627\u0631\u064a \u0627\u062c\u0631\u0627\u0621 \u0635\u064a\u0627\u0646\u0629 \u0644\u0647\u0627.

+token_response_problem=\u062c\u0627\u0631\u064a \u062a\u0634\u063a\u064a\u0644 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0645\u0646 \u0627\u0644\u0646\u0642\u0637\u0629 \u0627\u0644\u0637\u0631\u0641\u064a\u0629 \u0644\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 : {0}

+token_response_problem.explanation=\u0642\u0627\u0645\u062a \u0627\u0644\u0646\u0642\u0637\u0629 \u0627\u0644\u0637\u0631\u0641\u064a\u0629 \u0644\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0628\u0627\u0631\u0633\u0627\u0644 \u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u0628\u0623\u0646 \u0648\u062d\u062f\u0629 \u0627\u0644\u062e\u062f\u0645\u0629 \u0644\u0627 \u064a\u0645\u0643\u0646\u0647\u0627 \u0627\u0644\u062a\u0634\u063a\u064a\u0644.

+unauthorized_client=\u0644\u0627 \u062a\u062a\u0648\u0627\u0641\u0631 \u0635\u0644\u0627\u062d\u064a\u0627\u062a \u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 : {0}

+unauthorized_client.explanation=\u063a\u064a\u0631 \u0645\u0635\u0631\u062d \u0644\u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 \u0628\u0637\u0644\u0628 \u0627\u0644\u062a\u0648\u0635\u0644 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0647\u0630\u0647 \u0627\u0644\u0637\u0631\u064a\u0642\u0629.

+unsupported_grant_type=\u0646\u0648\u0639 \u0627\u0644\u0645\u0646\u062d \u063a\u064a\u0631 \u0645\u062f\u0639\u0645 : {0}

+unsupported_grant_type.explanation=\u0648\u062d\u062f\u0629 \u062e\u062f\u0645\u0629 \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u0644\u0627 \u062a\u062f\u0639\u0645 \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u062a\u0648\u0635\u0644 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0647\u0630\u0647 \u0627\u0644\u0637\u0631\u064a\u0642\u0629.

+unsupported_response_type=\u0646\u0648\u0639 \u0627\u0644\u0627\u0633\u062a\u062c\u0627\u0628\u0629 \u063a\u064a\u0631 \u0645\u062f\u0639\u0645: {0}

+unsupported_response_type.explanation=\u0648\u062d\u062f\u0629 \u062e\u062f\u0645\u0629 \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u0644\u0627 \u062a\u062f\u0639\u0645 \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0643\u0648\u062f \u0627\u0644\u062a\u0631\u062e\u064a\u0635 \u0628\u0627\u0633\u062a\u062e\u062f\u0627\u0645 \u0647\u0630\u0647 \u0627\u0644\u0637\u0631\u064a\u0642\u0629.

+mac_token_problem=\u062c\u0627\u0631\u064a \u0637\u0644\u0628 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 mac : {0}

+mac_token_problem.explanation=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0636\u0627\u0641\u0629 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 mac \u0627\u0644\u0649 \u0648\u062d\u062f\u0629 \u062e\u062f\u0645\u0629 \u0627\u0644\u0645\u0635\u062f\u0631.

+bearer_token_problem=\u062c\u0627\u0631\u064a \u0637\u0644\u0628 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 bearer : {0}

+bearer_token_problem.explanation=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0636\u0627\u0641\u0629 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 bearer \u0627\u0644\u0649 \u0648\u062d\u062f\u0629 \u062e\u062f\u0645\u0629 \u0627\u0644\u0645\u0635\u062f\u0631.

+authentication_problem=\u062c\u0627\u0631\u064a \u0627\u0636\u0627\u0641\u0629 \u062a\u0648\u062b\u064a\u0642 \u0628\u0631\u0646\u0627\u0645\u062c \u0627\u0644\u0648\u062d\u062f\u0629 \u0627\u0644\u062a\u0627\u0628\u0639\u0629 : {0}

+authentication_problem.explanation=\u0644\u0627 \u064a\u0645\u0643\u0646 \u0627\u0636\u0627\u0641\u0629 \u0639\u0646\u0627\u0648\u064a\u0646 \u0627\u0644\u062a\u0648\u062b\u064a\u0642 \u0627\u0644\u0649 \u0627\u0644\u0637\u0644\u0628.

+unknown_problem=\u062d\u062f\u062b \u062e\u0637\u0623 \u063a\u064a\u0631 \u0645\u0639\u0631\u0648\u0641 : {0}

+unknown_problem.explanation=\u0623\u0637\u0644\u0628 \u0645\u0646 \u0645\u0648\u062c\u0647 \u0627\u0644\u0646\u0638\u0627\u0645 \u0627\u0644\u062a\u0639\u0631\u0641 \u0639\u0644\u0649 \u0627\u0644\u0645\u0634\u0643\u0644\u0629.

+code_grant_problem=\u062c\u0627\u0631\u064a \u0627\u0644\u062d\u0635\u0648\u0644 \u0639\u0644\u0649 \u0627\u0644\u0631\u0645\u0632 \u0627\u0644\u0645\u0645\u064a\u0632 \u0644\u0644\u062a\u0648\u0635\u0644 \u0645\u0646 \u0643\u0648\u062f \u0627\u0644\u062a\u0631\u062e\u064a\u0635 : {0}

+code_grant_problem.explanation=\u062d\u062f\u062b \u062e\u0637\u0623 \u0623\u062b\u0646\u0627\u0621 \u0645\u0633\u0627\u0631 \u0643\u0648\u062f \u0627\u0644\u062a\u0631\u062e\u064a\u0635.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ca.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ca.properties
new file mode 100644
index 0000000..acdb440
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ca.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} ha trobat un error :

+authorization_code_problem=El codi d''autoritzaci\u00f3 s''intercanviar\u00e0 pel testimoni d''acc\u00e9s : {0}

+authorization_code_problem.explanation=S'ha produ\u00eft un error en intercanviar el codi d'autoritzaci\u00f3 d'access_token.

+authorize_problem=S''est\u00e0 iniciant el proc\u00e9s d''autoritzaci\u00f3 de {0}.

+authorize_problem.explanation=S'ha produ\u00eft un error en iniciar el proc\u00e9s d'autoritzaci\u00f3.

+callback_problem=S''est\u00e0 processant la resposta redirigida : {0}

+callback_problem.explanation=S'ha produ\u00eft un error en processar la resposta redirigida des del prove\u00efdor de serveis.

+client_credentials_problem=L''access_token s''est\u00e0 recuperant per al client amb la seg\u00fcent credencial de client : {0}

+client_credentials_problem.explanation=S'ha produ\u00eft un error en recuperar el testimoni d'acc\u00e9s al flux de credencials de client.

+fetch_init_problem=L''OAuth2Request s''est\u00e0 inicialitzant: {0}

+fetch_init_problem.explanation=S'ha produ\u00eft un error de nivell baix en inicialitzar l'obtenci\u00f3 d'OAuth2Request.

+fetch_problem=s''est\u00e0 executant OAuth2Request.fetch() : {0}

+fetch_problem.explanation=S'ha produ\u00eft un error en emetre l'obtenci\u00f3 d'OAuth2Request.

+gadget_spec_problem=S''est\u00e0 processant la seg\u00fcent especificaci\u00f3 de gadget : {0}

+gadget_spec_problem.explanation=No s'ha trobat cap especificaci\u00f3 de gadget.  Demaneu a l'administrador que configuri el gadget per treballar amb OAuth2.0.

+get_oauth2_accessor_problem=s''est\u00e0 obtenint un OAuth2Accessor per a l''OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=S'ha produ\u00eft un error. Demaneu a l'administrador que cre\u00ef una vinculaci\u00f3 de client OAuth2.0 per aquest gadget i servei.

+lookup_spec_problem=S''est\u00e0 recuperant l''especificaci\u00f3 del gadget : {0}

+lookup_spec_problem.explanation=No s'ha trobat cap especificaci\u00f3 de gadget.  Demaneu a l'administrador que configuri el gadget per treballar amb OAuth2.0.

+missing_fetch_params=Falten els seg\u00fcents par\u00e0metres d''obtenci\u00f3 necessaris : {0}

+missing_fetch_params.explanation=S'ha cridat el m\u00e8tode fetch() amb par\u00e0metres incorrectes.

+missing_server_response=S''ha produ\u00eft un error durant la resposta del prove\u00efdor de servei OAuth2.0 : {0}

+missing_server_response.explanation=El servidor ha creat un OAuth2Request v\u00e0lid, per\u00f2 no ha pogut emetre la sol\u00b7licitud o no ha pogut obtenir una resposta v\u00e0lida del prove\u00efdor de servei.

+no_response_handler=Un gestor de respostes no ha processat la resposta seg\u00fcent : {0}

+no_response_handler.explanation=El gestor de respostes no existeix per processar els punts finals d'autoritzaci\u00f3 i de testimoni.AuthorizationEndpointResponseHandler o TokenEndpointResponseHandler.

+no_gadget_spec=No es troba l''especificaci\u00f3 de gadget de {0}.

+no_gadget_spec.explanation=No s'ha trobat cap especificaci\u00f3 de gadget.  Demaneu a l'administrador que configuri el gadget per treballar amb OAuth2.0.

+refresh_token_problem=El codi d''autoritzaci\u00f3 s''intercanviar\u00e0 pel testimoni d''acc\u00e9s : {0}

+refresh_token_problem.explanation=S'ha produ\u00eft un error en intercanviar el codi d'actualitzaci\u00f3 pel testimoni d'access.

+secret_encryption_problem=El testimoni secret s''est\u00e0 xifrant per persist\u00e8ncia: {0}

+secret_encryption_problem.explanation=S'ha produ\u00eft un error en emmagatzemar secrets OAuth2.0 amb el m\u00f2dul de xifrat proporcionat.

+access_denied=S'ha denegat l'acc\u00e9s.

+access_denied.explanation=L'usuari denegat o cancel\u00b7lat l'autoritzaci\u00f3 amb el prove\u00efdor de serveis.

+invalid_client=El client no \u00e9s v\u00e0lid : {0}

+invalid_client.explanation= El client no es pot autenticar, possiblement perqu\u00e8 \u00e9s desconegut, No s'ha incl\u00f2s l'autenticaci\u00f3 del client o el m\u00e8tode no se suporta.

+invalid_grant=La concessi\u00f3 d''autoritzaci\u00f3 no \u00e9s v\u00e0lida : {0}

+invalid_grant.explanation=La concessi\u00f3 d'autoritzaci\u00f3 no \u00e9s v\u00e0lida, ha caducat, ha estat revocada, no coincideix amb l'URI de redirecci\u00f3 de la sol\u00b7licitud d'autoritzaci\u00f3 o ha estat emesa per un altre client. La concessi\u00f3 d'autoritzaci\u00f3 pot incloure el codi d'autoritzaci\u00f3, les credencials del propietari del recurs o les credencials del client.

+invalid_request=La sol\u00b7licitud no \u00e9s v\u00e0lida : {0}

+invalid_request.explanation=La sol\u00b7licitud no \u00e9s v\u00e0lida perqu\u00e8 falta un par\u00e0metre necessari, inclou un valor de par\u00e0metre no adm\u00e8s, inclou diverses credencials, utilitza m\u00e9s d'un mecanisme per autenticar el client o t\u00e9 un format incorrecte.

+invalid_scope=L''\u00e0mbit no \u00e9s v\u00e0lid : {0}

+invalid_scope.explanation=L'\u00e0mbit sol\u00b7licitat no \u00e9s v\u00e0lid, \u00e9s desconegut, t\u00e9 un format incorrecte o supera l'\u00e0mbit atorgat pel propietari de recurs.

+server_error=S''ha produ\u00eft un error de servidor: {0}

+server_error.explanation=El servidor d'autoritzaci\u00f3 ha trobat una condici\u00f3 inesperada que ha evitat que es completi la sol\u00b7licitud.

+server_rejected_request=El servidor ha rebutjat la sol\u00b7licitud : statuscode = {0}

+server_rejected_request.explanation=El servidor ha creat un OAuth2Request, per\u00f2 el prove\u00efdor de servei l'ha rebutjat.

+temporarily_unavailable=El servei no est\u00e0 disponible en aquests moments : {0}

+temporarily_unavailable.explanation=El servidor d'autoritzaci\u00f3 no pot gestionar la sol\u00b7licitud perqu\u00e8 est\u00e0 temporalment sobrecarregat o en mode de manteniment.

+token_response_problem=La resposta del punt final de testimoni s''est\u00e0 processant: {0}

+token_response_problem.explanation=El punt final de testimoni ha enviat una resposta que el servidor no ha pogut processar.

+unauthorized_client=El client no est\u00e0 autoritzat : {0}

+unauthorized_client.explanation=El client no est\u00e0 autoritzat per sol\u00b7licitar un acc\u00e9s al testimoni utilitzant aquest m\u00e8tode.

+unsupported_grant_type=El tipus de concessi\u00f3 si no s''admet : {0}

+unsupported_grant_type.explanation=El servidor d'autoritzaci\u00f3 no admet l'obtenci\u00f3 del testimoni d'acc\u00e9s utilitzant aquest m\u00e8tode.

+unsupported_response_type=El tipus de resposta no s''admet: {0}

+unsupported_response_type.explanation=El servidor d'autoritzaci\u00f3 no admet l'obtenci\u00f3 d'un codi d'autoritzaci\u00f3 utilitzant aquest m\u00e8tode.

+mac_token_problem=S''est\u00e0 sol\u00b7licitant el testimoni mac : {0}

+mac_token_problem.explanation=El testimoni mac a la sol\u00b7licitud no s'ha pogut afegir al servidor de recursos.

+bearer_token_problem=S''est\u00e0 sol\u00b7licitant el testimoni portador : {0}

+bearer_token_problem.explanation=El testimoni portador a la sol\u00b7licitud no s'ha pogut afegir al servidor de recursos.

+authentication_problem=S''est\u00e0 afegint l''autenticaci\u00f3 de client : {0}

+authentication_problem.explanation=Les cap\u00e7aleres d'autenticaci\u00f3 no s'han pogut afegir a la sol\u00b7licitud.

+unknown_problem=S''ha produ\u00eft un error desconegut : {0}

+unknown_problem.explanation=Demaneu a l'administrador que investigui el problema.

+code_grant_problem=S''est\u00e0 obtenint el testimoni d''acc\u00e9s del codi d''autoritzaci\u00f3 : {0}

+code_grant_problem.explanation=S'ha produ\u00eft un error durant el flux de codi d'autoritzaci\u00f3.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_cs.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_cs.properties
new file mode 100644
index 0000000..80388e3
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_cs.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= V {0} se vyskytla chyba:

+authorization_code_problem=Autoriza\u010dn\u00ed k\u00f3d bude vym\u011bn\u011bn za p\u0159\u00edstupov\u00fd token: {0}

+authorization_code_problem.explanation=P\u0159i v\u00fdm\u011bn\u011b autoriza\u010dn\u00edho k\u00f3du za p\u0159\u00edstupov\u00fd token do\u0161lo k chyb\u011b.

+authorize_problem=Za\u010d\u00edn\u00e1 proces inicializace {0}.

+authorize_problem.explanation=P\u0159i spou\u0161t\u011bn\u00ed procesu autorizace se vyskytla chyba.

+callback_problem=Doch\u00e1z\u00ed ke zpracov\u00e1n\u00ed p\u0159esm\u011brovan\u00e9 odpov\u011bdi : {0}

+callback_problem.explanation=P\u0159i zpracov\u00e1n\u00ed p\u0159esm\u011brovan\u00e9 odpov\u011bdi od poskytovatele slu\u017eeb do\u0161lo k chyb\u011b.

+client_credentials_problem=P\u0159\u00edstupov\u00fd token se na\u010d\u00edt\u00e1 pro klienta s n\u00e1sleduj\u00edc\u00edmi pov\u011b\u0159en\u00edmi: {0}

+client_credentials_problem.explanation=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed p\u0159\u00edstupov\u00e9ho prvku pro pov\u011b\u0159en\u00ed klienta se vyskytla chyba.

+fetch_init_problem=Prob\u00edh\u00e1 spou\u0161t\u011bn\u00ed OAuth2Request: {0}

+fetch_init_problem.explanation=P\u0159i spou\u0161t\u011bn\u00ed OAuth2Request do\u0161lo k chyb\u011b n\u00edzk\u00e9 \u00farovn\u011b.

+fetch_problem=p\u0159i prov\u00e1d\u011bn\u00ed funkce OAuth2Request.fetch() : {0}

+fetch_problem.explanation=P\u0159i prov\u00e1d\u011bn\u00ed na\u010dten\u00ed OAuth2Request do\u0161lo k chyb\u011b.

+gadget_spec_problem=Prob\u00edh\u00e1 zpracov\u00e1n\u00ed n\u00e1sleduj\u00edc\u00ed specifikace modulu gadget: {0}

+gadget_spec_problem.explanation=Specifikace modulu gadget nebyla nalezena. Po\u017e\u00e1dejte sv\u00e9ho administr\u00e1tora o zprovozn\u011bn\u00ed modulu gadget se slu\u017ebou OAuth2.0.

+get_oauth2_accessor_problem=z\u00edsk\u00e1v\u00e1n\u00ed OAuth2Accessor pro OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=Do\u0161lo k chyb\u011b. Po\u017e\u00e1dejte administr\u00e1tora, aby vytvo\u0159il vazbu klienta OAuth2.0 pro tento modul gadget a slu\u017ebu.

+lookup_spec_problem=Z\u00edsk\u00e1v\u00e1n\u00ed specifikace modulu gadget: {0}

+lookup_spec_problem.explanation=Specifikace modulu gadget nebyla nalezena. Po\u017e\u00e1dejte sv\u00e9ho administr\u00e1tora o zprovozn\u011bn\u00ed modulu gadget se slu\u017ebou OAuth2.0.

+missing_fetch_params=Chyb\u011bj\u00ed n\u00e1sleduj\u00edc\u00ed vy\u017eadovan\u00e9 parametry : {0}

+missing_fetch_params.explanation=Metoda fetch() byla vol\u00e1na s chybn\u00fdmi parametry.

+missing_server_response=B\u011bhem odpov\u011bdi poskytovatele slu\u017eby OAuth2.0 do\u0161lo k chyb\u011b: {0}

+missing_server_response.explanation=Server vytvo\u0159il platn\u00fd po\u017eadavek OAuth2Request, ale nebylo mo\u017en\u00e9 odeslat po\u017eadavek nebo z\u00edskat platnou odpov\u011b\u010f od poskytovatele slu\u017eby.

+no_response_handler=Obslu\u017en\u00e1 rutina odpov\u011bdi nezpracovala n\u00e1sleduj\u00edc\u00ed odpov\u011b\u010f: {0}

+no_response_handler.explanation=Obslu\u017en\u00e1 rutina odpov\u011bdi pro zpracov\u00e1n\u00ed koncov\u00fdch bod\u016f autorizace a tokenu neexistuje. AuthorizationEndpointResponseHandler nebo TokenEndpointResponseHandler.

+no_gadget_spec=Specifikace modulu gadget pro {0} nebyla nalezena.

+no_gadget_spec.explanation=Specifikace modulu gadget nebyla nalezena. Po\u017e\u00e1dejte sv\u00e9ho administr\u00e1tora o zprovozn\u011bn\u00ed modulu gadget se slu\u017ebou OAuth2.0.

+refresh_token_problem=Token obnoven\u00ed bude vym\u011bn\u011bn za p\u0159\u00edstupov\u00fd token: {0}

+refresh_token_problem.explanation=P\u0159i v\u00fdm\u011bn\u011b tokenu obnoven\u00ed za p\u0159\u00edstupov\u00fd token se vyskytla chyba.

+secret_encryption_problem=Token je pro \u00fa\u010dely perzistence \u0161ifrov\u00e1n: {0}

+secret_encryption_problem.explanation=P\u0159i ukl\u00e1d\u00e1n\u00ed tajn\u00fdch kl\u00ed\u010d\u016f slu\u017eby OAuth2.0 se zadan\u00fdm \u0161ifrovac\u00ed modulem do\u0161lo k chyb\u011b.

+access_denied=P\u0159\u00edstup byl odep\u0159en.

+access_denied.explanation=U\u017eivatel odep\u0159el nebo zru\u0161il autorizaci s poskytovatelem slu\u017eeb.

+invalid_client=Klient nen\u00ed platn\u00fd: {0}

+invalid_client.explanation= Klienta nelze ov\u011b\u0159it pravd\u011bpodobn\u011b z d\u016fvodu, \u017ee se jedn\u00e1 o nezn\u00e1m\u00e9ho klienta, ov\u011b\u0159en\u00ed klienta nebylo provedeno nebo metody ov\u011b\u0159ov\u00e1n\u00ed nejsou podporov\u00e1ny.

+invalid_grant=Ud\u011blen\u00ed autorizace nen\u00ed platn\u00e9: {0}

+invalid_grant.explanation=Ud\u011blen\u00ed autorizace nen\u00ed platn\u00e9, vypr\u0161elo, bylo zru\u0161eno, neodpov\u00edd\u00e1 URI pro p\u0159esm\u011brov\u00e1n\u00ed pou\u017eit\u00e9 pro po\u017eadavek autorizace nebo bylo vyd\u00e1no pro jin\u00e9ho klienta. Ud\u011blen\u00ed autorizace m\u016f\u017ee obsahovat k\u00f3d autorizace, pov\u011b\u0159en\u00ed vlastn\u00edka prost\u0159edku nebo pov\u011b\u0159en\u00ed klienta.

+invalid_request=Po\u017eadavek nen\u00ed platn\u00fd : {0}

+invalid_request.explanation=Po\u017eadavek nen\u00ed platn\u00fd, proto\u017ee v n\u011bm chyb\u00ed vy\u017eadovan\u00fd parametr, obsahuje nepodporovanou hodnotu parametru, obsahuje duplicitn\u00ed parametry, obsahuje v\u00edce pov\u011b\u0159en\u00ed, ov\u011b\u0159uje klienta v\u00edce metodami nebo je jinak po\u0161kozen.

+invalid_scope=Rozsah nen\u00ed platn\u00fd: {0}

+invalid_scope.explanation=Po\u017eadovan\u00fd rozsah je neplatn\u00fd, nezn\u00e1m\u00fd, po\u0161kozen\u00fd nebo p\u0159ekra\u010duje rozsah poskytnut\u00fd vlastn\u00edkem prost\u0159edku.

+server_error=: Do\u0161lo k chyb\u011b serveru: {0}

+server_error.explanation=Na autoriza\u010dn\u00edm serveru se vyskytla neo\u010dek\u00e1van\u00e1 podm\u00ednka zabra\u0148uj\u00edc\u00ed dokon\u010den\u00ed po\u017eadavku.

+server_rejected_request=Server po\u017eadavek odm\u00edtnul: stavov\u00fd k\u00f3d = {0}

+server_rejected_request.explanation=Server vytvo\u0159il po\u017eadavek OAuth2Request, ale poskytovatel slu\u017eby jej odm\u00edtl.

+temporarily_unavailable=Slu\u017eba je do\u010dasn\u011b nedostupn\u00e1: {0}

+temporarily_unavailable.explanation=Autoriza\u010dn\u00ed server nem\u016f\u017ee po\u017eadavek zpracovat, proto\u017ee je do\u010dasn\u011b p\u0159et\u00ed\u017een nebo je v re\u017eimu \u00fadr\u017eby.

+token_response_problem=Zpracov\u00e1v\u00e1 se odpov\u011b\u010f z koncov\u00e9ho bodu tokenu: {0}

+token_response_problem.explanation=Koncov\u00fd bod tokenu v odpov\u011bdi zaslal informaci, \u017ee server nem\u016f\u017ee po\u017eadavek zpracovat.

+unauthorized_client=Klient nen\u00ed autorizov\u00e1n: {0}

+unauthorized_client.explanation=Klient nem\u016f\u017ee \u017e\u00e1dat o p\u0159\u00edstupov\u00fd token pomoc\u00ed t\u00e9to metody.

+unsupported_grant_type=Typ ud\u011blen\u00ed, pokud nebude podporov\u00e1na: {0}

+unsupported_grant_type.explanation=Autoriza\u010dn\u00ed server nepodporuje z\u00edsk\u00e1v\u00e1n\u00ed p\u0159\u00edstupov\u00e9ho tokenu touto metodou.

+unsupported_response_type=Typ odpov\u011bdi nen\u00ed podporov\u00e1n: {0}

+unsupported_response_type.explanation=Autoriza\u010dn\u00ed server nepodporuje z\u00edsk\u00e1v\u00e1n\u00ed autoriza\u010dn\u00edho k\u00f3du pomoc\u00ed t\u00e9to metody.

+mac_token_problem=Prob\u00edh\u00e1 \u017e\u00e1dost o mac token: {0}

+mac_token_problem.explanation=Mac token v po\u017eadavku nelze p\u0159idat do serveru s prost\u0159edky.

+bearer_token_problem=Je vy\u017eadov\u00e1n token dr\u017eitele: {0}

+bearer_token_problem.explanation=Token dr\u017eitele v po\u017eadavku nelze p\u0159idat na server s prost\u0159edky.

+authentication_problem=P\u0159id\u00e1v\u00e1 se ov\u011b\u0159en\u00ed klienta: {0}

+authentication_problem.explanation=Z\u00e1hlav\u00ed ov\u011b\u0159en\u00ed se nepoda\u0159ilo p\u0159idat do po\u017eadavku.

+unknown_problem=Vyskytla se nezn\u00e1m\u00e1 chyba: {0}

+unknown_problem.explanation=Po\u017e\u00e1dejte administr\u00e1tora syst\u00e9mu, aby chybu prozkoumal.

+code_grant_problem=P\u0159\u00edstupov\u00fd token z\u00edskan\u00fd z autoriza\u010dn\u00edho k\u00f3du: {0}

+code_grant_problem.explanation=P\u0159i z\u00edsk\u00e1v\u00e1n\u00ed autoriza\u010dn\u00edho k\u00f3du do\u0161lo k chyb\u011b.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_da.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_da.properties
new file mode 100644
index 0000000..3c51d70
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_da.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= Der er opst\u00e5et en fejl i {0}:

+authorization_code_problem=Autorisationskoden udveksles med adgangstokenet: {0}

+authorization_code_problem.explanation=Der er opst\u00e5et en fejl under udveksling af autorisationskoden med adgangstokenet.

+authorize_problem=Autorisationsprocessen for {0} initieres.

+authorize_problem.explanation=Der er opst\u00e5et en fejl under initiering af autorisationsprocessen.

+callback_problem=Omdirigeringssvaret behandles: {0}

+callback_problem.explanation=Der er opst\u00e5et en fejl under behandling af omdirigeringssvaret fra serviceudbyderen.

+client_credentials_problem=Adgangstokenet hentes for klienten med f\u00f8lgende klientlegitimationsoplysninger: {0}

+client_credentials_problem.explanation=Der er opst\u00e5et en fejl under hentning af adgangstoken i client_credentials-str\u00f8mmen.

+fetch_init_problem=OAuth2Request initialiseres: {0}

+fetch_init_problem.explanation=Der er opst\u00e5et en fejl p\u00e5 lavt niveau under initialisering af OAuth2Request-hentningen.

+fetch_problem=udf\u00f8relse af OAuth2Request.fetch(): {0}

+fetch_problem.explanation=Der er opst\u00e5et en fejl under afsendelse af anmodningen om OAuth2Request-hentning.

+gadget_spec_problem=F\u00f8lgende gadgetspecifikation behandles: {0}

+gadget_spec_problem.explanation=Der er ikke fundet en gadgetspecifikation. Bed administratoren om at konfigurere din gadget til at fungere med OAuth2.0.

+get_oauth2_accessor_problem=hentning af en OAuth2Accessor til OAuth2Request: {0}

+get_oauth2_accessor_problem.explanation=Der er opst\u00e5et en fejl. Bed administratoren om at oprette en OAuth2.0-klientbinding for denne gadget og service.

+lookup_spec_problem=Gadgetspecifikationen hentes: {0}

+lookup_spec_problem.explanation=Der er ikke fundet en gadgetspecifikation. Bed administratoren om at konfigurere din gadget til at fungere med OAuth2.0.

+missing_fetch_params=F\u00f8lgende p\u00e5kr\u00e6vede fetch-parametre mangler: {0}

+missing_fetch_params.explanation=Metoden fetch() er kaldt med forkerte parametre.

+missing_server_response=Der er opst\u00e5et en fejl under svar fra OAuth2.0-serviceudbyder: {0}

+missing_server_response.explanation=Serveren har oprettet en gyldig OAuth2Request, men kan enten ikke afsende anmodningen eller ikke f\u00e5 et gyldigt svar fra serviceudbyderen.

+no_response_handler=En svarbehandler har ikke behandlet f\u00f8lgende svar: {0}

+no_response_handler.explanation=Der findes ikke en svarbehandler til behandling af autorisations- og tokenslutpunkter. AuthorizationEndpointResponseHandler eller TokenEndpointResponseHandler.

+no_gadget_spec=Gadgetspecifikationen for {0} findes ikke.

+no_gadget_spec.explanation=Der er ikke fundet en gadgetspecifikation. Bed administratoren om at konfigurere din gadget til at fungere med OAuth2.0.

+refresh_token_problem=Opfriskningstokenet udveksles med adgangstokenet: {0}

+refresh_token_problem.explanation=Der er opst\u00e5et en fejl under udveksling af opfriskningstokenet med adgangstokenet.

+secret_encryption_problem=Tokenhemmeligheden krypteres med henblik p\u00e5 persistens: {0}

+secret_encryption_problem.explanation=Der er opst\u00e5et en fejl under lagring af OAuth2.0-hemmeligheder med det leverede krypteringsmodul.

+access_denied=Adgang er n\u00e6gtet.

+access_denied.explanation=Brugeren har n\u00e6gtet eller annulleret autorisationen med serviceudbyderen.

+invalid_client=Klienten er ugyldig: {0}

+invalid_client.explanation= Klienten kan ikke valideres. Det skyldes sandsynligvis, at klienten er ukendt, at klientvalidering ikke er inkluderet, eller at valideringsmetoden ikke underst\u00f8ttes.

+invalid_grant=Autorisationstildelingen er ugyldig: {0}

+invalid_grant.explanation=Autorisationstildelingen er ugyldig, udl\u00f8bet eller tilbagekaldt, eller ogs\u00e5 svarer den ikke til den omdirigerings-URI, der bruges i autorisationsanmodningen, eller den er afsendt til en anden klient. Autorisationstildelingen kan indeholde autorisationskoden, ressourceejerens legitimationsoplysninger eller klientens legitimationsoplysninger.

+invalid_request=Anmodningen er ugyldig: {0}

+invalid_request.explanation=Anmodningen er ugyldig, fordi den mangler en p\u00e5kr\u00e6vet parameter, indeholder en parameter, der ikke underst\u00f8ttes, gentager en parameter, indeholder flere legitimationsoplysninger, bruger mere end \u00e9n mekanisme til validering af klienten eller p\u00e5 anden vis er formateret forkert.

+invalid_scope=Omfanget er ugyldigt: {0}

+invalid_scope.explanation=Det \u00f8nskede omfang er ugyldigt, ukendt, forkert formateret eller overskrider det omfang, som er tildelt af ressourcejeren.

+server_error=Der er opst\u00e5et en serverfejl: {0}

+server_error.explanation=Der er opst\u00e5et en uventet betingelse i autorisationsserveren, som har forhindret, at anmodningen kan fuldf\u00f8res.

+server_rejected_request=Serveren har afvist anmodningen: statuscode = {0}

+server_rejected_request.explanation=Serveren har oprettet en OAuth2Request, men serviceudbyderen har afvist den.

+temporarily_unavailable=Servicen er midlertidigt utilg\u00e6ngelig: {0}

+temporarily_unavailable.explanation=Autorisationsserveren kan ikke h\u00e5ndtere anmodningen, fordi den er midlertidigt overbelastet eller er i vedligeholdelsestilstand.

+token_response_problem=Svaret for tokenslutpunktet behandles: {0}

+token_response_problem.explanation=Tokenslutpunktet har sendt et svar, som serveren ikke kan behandle.

+unauthorized_client=Klienten er ikke autoriseret: {0}

+unauthorized_client.explanation=Klienten er ikke autoriseret til at anmode om et adgangstoken vha. denne metode.

+unsupported_grant_type=Tildelingstypen underst\u00f8ttes ikke: {0}

+unsupported_grant_type.explanation=Autorisationsserveren underst\u00f8tter ikke hentning af et adgangstoken vha. denne metode.

+unsupported_response_type=Svartypen underst\u00f8ttes ikke: {0}

+unsupported_response_type.explanation=Autorisationsserveren underst\u00f8tter ikke hentning af en autorisationskode vha. denne metode.

+mac_token_problem=Der anmodes om mac-tokenet: {0}

+mac_token_problem.explanation=Mac-tokenet i anmodningen kan ikke tilf\u00f8jes til ressourceserveren.

+bearer_token_problem=Der anmodes om bearer-tokenet: {0}

+bearer_token_problem.explanation=Bearer-tokenet i anmodningen kan ikke tilf\u00f8jes til ressourceserveren.

+authentication_problem=Klientvalideringen tilf\u00f8jes: {0}

+authentication_problem.explanation=Valideringsoverskrifter kan ikke tilf\u00f8jes til anmodningen.

+unknown_problem=Der er opst\u00e5et en ukendt fejl: {0}

+unknown_problem.explanation=Bed systemadministratoren om at unders\u00f8ge problemet.

+code_grant_problem=Det kodeord, der hentes fra autorisationskoden: {0}

+code_grant_problem.explanation=Der er opst\u00e5et en fejl under autorisationskodestr\u00f8mmen.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_de.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_de.properties
new file mode 100644
index 0000000..a82ed03
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_de.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} hat einen Fehler gemeldet:

+authorization_code_problem=Der Berechtigungscode wird durch das Zugriffstoken ersetzt: {0}

+authorization_code_problem.explanation=Beim Ersetzen des Berechtigungscodes durch das Zugriffstoken ist ein Fehler aufgetreten.

+authorize_problem=Der Berechtigungsprozess f\u00fcr {0} wird initiiert.

+authorize_problem.explanation=Beim Initiieren des Berechtigungsprozesses ist ein Fehler aufgetreten.

+callback_problem=Die Umleitungsantwort wird verarbeitet: {0}

+callback_problem.explanation=Beim Verarbeiten der Umleitungsantwort vom Service-Provider ist ein Fehler aufgetreten.

+client_credentials_problem=Das Zugriffstoken wird f\u00fcr den Client mit dem folgenden Clientberechtigungsnachweis abgerufen: {0}

+client_credentials_problem.explanation=Beim Abrufen des Zugriffstokens im Datenfluss des Clientberechtigungsnachweises ist ein Fehler aufgetreten.

+fetch_init_problem=OAuth2Request wird initialisiert: {0}

+fetch_init_problem.explanation=Beim Initialisieren des OAuth2Request-Abrufs ist ein Fehler der unteren Ebene aufgetreten.

+fetch_problem=OAuth2Request.fetch() wird ausgef\u00fchrt: {0}

+fetch_problem.explanation=Beim Ausgeben des OAuth2Request-Abrufs ist ein Fehler aufgetreten.

+gadget_spec_problem=Die folgende Gadgetspezifikation wird verarbeitet: {0}

+gadget_spec_problem.explanation=Es konnte keine Gadgetspezifikation gefunden werden. Bitten Sie den Administrator, das Gadget f\u00fcr die Verwendung mit OAuth2.0 zu konfigurieren.

+get_oauth2_accessor_problem=OAuth2Accessor wird f\u00fcr OAuth2Request abgerufen: {0}

+get_oauth2_accessor_problem.explanation=Es ist ein Fehler aufgetreten. Bitten Sie den Administrator, eine OAuth2.0-Clientbindung f\u00fcr dieses Gadget und diesen Service zu erstellen.

+lookup_spec_problem=Die Gadgetspezifikation wird abgerufen: {0}

+lookup_spec_problem.explanation=Es konnte keine Gadgetspezifikation gefunden werden. Bitten Sie den Administrator, das Gadget f\u00fcr die Verwendung mit OAuth2.0 zu konfigurieren.

+missing_fetch_params=Die folgenden erforderlichen Abrufparameter fehlen: {0}

+missing_fetch_params.explanation=Die fetch()-Methode wurde mit falschen Parametern aufgerufen.

+missing_server_response=W\u00e4hrend der OAuth2.0-Antwort des Service-Providers ist ein Fehler aufgetreten: {0}

+missing_server_response.explanation=Der Server hat eine g\u00fcltige OAuth2Request erstellt, konnte die Anfrage jedoch nicht ausgeben oder konnte keine g\u00fcltige Antwort vom Service-Provider erhalten.

+no_response_handler=Eine Antwortbehandlungsroutine konnte die folgende Antwort nicht verarbeiten: {0}

+no_response_handler.explanation=Es ist keine Antwortbehandlungsroutine f\u00fcr die Verarbeitung der Berechtigungs- und Tokenendpunkte vorhanden. AuthorizationEndpointResponseHandler oder TokenEndpointResponseHandler.

+no_gadget_spec=Die Gadgetspezifikation f\u00fcr {0} konnte nicht gefunden werden.

+no_gadget_spec.explanation=Es konnte keine Gadgetspezifikation gefunden werden. Bitten Sie den Administrator, das Gadget f\u00fcr die Verwendung mit OAuth2.0 zu konfigurieren.

+refresh_token_problem=Das Aktualisierungstoken wird durch das Zugriffstoken ersetzt: {0}

+refresh_token_problem.explanation=Beim Ersetzen des Aktualisierungstokens durch das Zugriffstoken ist ein Fehler aufgetreten.

+secret_encryption_problem=Der geheime Schl\u00fcssel des Tokens wird aus Persistenzgr\u00fcnden verschl\u00fcsselt: {0}

+secret_encryption_problem.explanation=Beim Speichern geheimer OAuth2.0-Schl\u00fcssel mit dem angegebenen Verschl\u00fcsselungsmodul ist ein Fehler aufgetreten.

+access_denied=Zugriff verweigert.

+access_denied.explanation=Der Benutzer hat die Berechtigung f\u00fcr den Service-Provider abgelehnt oder abgebrochen.

+invalid_client=Der Client ist ung\u00fcltig: {0}

+invalid_client.explanation= Der Client kann nicht authentifiziert werden, vermutlich weil er unbekannt ist, die Clientauthentifizierung nicht eingeschlossen war oder die Authentifizierungsmethode nicht unterst\u00fctzt wird.

+invalid_grant=Der Berechtigungsgrant ist ung\u00fcltig: {0}

+invalid_grant.explanation=Der Berechtigungsgrant ist ung\u00fcltig oder abgelaufen, wurde widerrufen, stimmt nicht mit dem Umleitungs-URI in der Berechtigungsanforderung \u00fcberein oder wurde f\u00fcr einen anderen Client ausgegeben. Der Berechtigungsgrant kann den Berechtigungscode, den Berechtigungsnachweis des Ressourcenverantwortlichen oder den Berechtigungsnachweis des Clients enthalten.

+invalid_request=Die Anforderung ist ung\u00fcltig: {0}

+invalid_request.explanation=Die Anforderung ist ung\u00fcltig, weil ein Parameter fehlt, ein ung\u00fcltiger Parameterwert enthalten ist, ein Parameter wiederholt wird, mehrere Berechtigungsnachweise enthalten sind, mehrere Mechanismen f\u00fcr die Authentifizierung des Clients verwendet werden oder andere Fehler vorhanden sind.

+invalid_scope=Der Umfang ist ung\u00fcltig: {0}

+invalid_scope.explanation=Der angeforderte Umfang ist ung\u00fcltig, unbekannt, fehlerhaft oder \u00fcberschreitet den vom Ressourcenverantwortlichen genehmigten Umfang.

+server_error=Ein Serverfehler ist aufgetreten: {0}

+server_error.explanation=Auf dem Autorisierungsserver ist eine nicht erwartete Bedingung aufgetreten. Die Anforderung konnte nicht ausgef\u00fchrt werden.

+server_rejected_request=Der Server hat die folgende Anforderung zur\u00fcckgewiesen: Statuscode = {0}

+server_rejected_request.explanation=Der Server hat eine OAuth2Request erstellt, aber der Service-Provider hat sie zur\u00fcckgewiesen.

+temporarily_unavailable=Der Service ist derzeit nicht verf\u00fcgbar: {0}

+temporarily_unavailable.explanation=Der Autorisierungsserver kann die Anforderung nicht verarbeiten, weil er derzeit \u00fcberlastet ist oder sich im Wartungsmodus befindet.

+token_response_problem=Die Antwort des Tokenendpunkts wird verarbeitet: {0}

+token_response_problem.explanation=Der Tokenendpunkt hat eine Antwort gesendet, die der Server nicht verarbeiten konnte.

+unauthorized_client=Der Client ist nicht autorisiert: {0}

+unauthorized_client.explanation=Der Client ist nicht autorisiert, \u00fcber diese Methode ein Zugriffstoken anzufordern.

+unsupported_grant_type=Der Granttyp wird nicht unterst\u00fctzt: {0}

+unsupported_grant_type.explanation=Das Anfordern eines Zugriffstokens \u00fcber diese Methode wird vom Autorisierungsserver nicht unterst\u00fctzt.

+unsupported_response_type=Der Antworttyp wird nicht unterst\u00fctzt: {0}

+unsupported_response_type.explanation=Das Anfordern eines Berechtigungscodes \u00fcber diese Methode wird vom Autorisierungsserver nicht unterst\u00fctzt.

+mac_token_problem=Das MAC-Token wird angefordert: {0}

+mac_token_problem.explanation=Das MAC-Token in der Anforderung konnte dem Ressourcenserver nicht hinzugef\u00fcgt werden.

+bearer_token_problem=Das Tr\u00e4gertoken wird angefordert: {0}

+bearer_token_problem.explanation=Das Tr\u00e4gertoken in der Anforderung konnte dem Ressourcenserver nicht hinzugef\u00fcgt werden.

+authentication_problem=Die Clientauthentifzierung wird hinzugef\u00fcgt: {0}

+authentication_problem.explanation=Die Authentifizierungsheader konnten der Anforderung nicht hinzugef\u00fcgt werden.

+unknown_problem=Ein unerwarteter Fehler ist aufgetreten: {0}

+unknown_problem.explanation=Bitten Sie Ihren Systemadministrator, das Problem zu untersuchen.

+code_grant_problem=Das vom Berechtigungscode bezogene Zugriffstoken: {0}

+code_grant_problem.explanation=W\u00e4hrend des Verarbeitungsablaufs f\u00fcr den Berechtigungscode ist ein Fehler aufgetreten.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_el.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_el.properties
new file mode 100644
index 0000000..4d6d38e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_el.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= \u03a4\u03bf {0} \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1:

+authorization_code_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2: {0}

+authorization_code_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03bf\u03c5 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03bc\u03b5 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.

+authorize_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03ad\u03bd\u03b1\u03c1\u03be\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf {0}.

+authorize_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03ad\u03bd\u03b1\u03c1\u03be\u03b7 \u03c4\u03b7\u03c2 \u03b4\u03b9\u03b1\u03b4\u03b9\u03ba\u03b1\u03c3\u03af\u03b1\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.

+callback_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c4\u03b7\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b1\u03bd\u03b1\u03ba\u03b1\u03c4\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2: {0}

+callback_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c4\u03b7\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b1\u03bd\u03b1\u03ba\u03b1\u03c4\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03bf\u03c7\u03ad\u03b1 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd.

+client_credentials_problem=\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b1\u03bd\u03b1\u03ba\u03c4\u03ac\u03c4\u03b1\u03b9 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 \u03bc\u03b5 \u03c4\u03bf \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03bf \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2: {0}

+client_credentials_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c3\u03c4\u03b7 \u03c1\u03bf\u03ae client_credentials.

+fetch_init_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ce\u03bd \u03c4\u03b9\u03bc\u03ce\u03bd \u03c3\u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 OAuth2Request: {0}

+fetch_init_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c7\u03b1\u03bc\u03b7\u03bb\u03bf\u03cd \u03b5\u03c0\u03b9\u03c0\u03ad\u03b4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03b4\u03bf\u03c3\u03b7 \u03b1\u03c1\u03c7\u03b9\u03ba\u03ce\u03bd \u03c4\u03b9\u03bc\u03ce\u03bd \u03c3\u03c4\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03af\u03b1 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 OAuth2Request.

+fetch_problem=\u03b5\u03ba\u03c4\u03ad\u03bb\u03b5\u03c3\u03b7 OAuth2Request.fetch() : {0}

+fetch_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03c5\u03c0\u03bf\u03b2\u03bf\u03bb\u03ae \u03c4\u03b7\u03c2 \u03b5\u03bd\u03c4\u03bf\u03bb\u03ae\u03c2 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7\u03c2 \u03c4\u03b7\u03c2 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 OAuth2Request.

+gadget_spec_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c4\u03bf\u03c5 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03bf\u03c5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2: {0}

+gadget_spec_problem.explanation=\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2.  \u0396\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf  OAuth2.0.

+get_oauth2_accessor_problem=\u0391\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 OAuth2Accessor \u03b3\u03b9\u03b1 \u03c4\u03bf OAuth2Request: {0}

+get_oauth2_accessor_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1. \u0396\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae \u03bd\u03b1 \u03b4\u03b7\u03bc\u03b9\u03bf\u03c5\u03c1\u03b3\u03ae\u03c3\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03c3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7 OAuth2.0 Client \u03b3\u03b9\u03b1 \u03c4\u03b7 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03ba\u03b1\u03b9 \u03c4\u03b7\u03bd \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1.

+lookup_spec_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2: {0}

+lookup_spec_problem.explanation=\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2.  \u0396\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf  OAuth2.0.

+missing_fetch_params=\u0394\u03b5\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03bf\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03bf\u03b9 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b5\u03c2 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b5\u03c2 \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf\u03b9 \u03b1\u03bd\u03ac\u03ba\u03c4\u03b7\u03c3\u03b7\u03c2: {0}

+missing_fetch_params.explanation=\u0397 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 fetch() \u03ba\u03bb\u03ae\u03b8\u03b7\u03ba\u03b5 \u03bc\u03b5 \u03bc\u03b7 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b5\u03c2 \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5\u03c2.

+missing_server_response=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03c0\u03b1\u03c1\u03bf\u03c7\u03ad\u03b1 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd OAuth2.0: {0}

+missing_server_response.explanation=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b4\u03b7\u03bc\u03b9\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 OAuth2Request \u03b1\u03bb\u03bb\u03ac \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03c5\u03c0\u03bf\u03b2\u03ac\u03bb\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03ae \u03b4\u03b5\u03bd \u03ad\u03bb\u03b1\u03b2\u03b5 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03bf\u03c7\u03ad\u03b1 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd.

+no_response_handler=\u0394\u03b5\u03bd \u03ad\u03b3\u03b9\u03bd\u03b5 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c4\u03b7\u03c2 \u03b1\u03ba\u03cc\u03bb\u03bf\u03c5\u03b8\u03b7\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03bc\u03b9\u03b1 \u03b4\u03b9\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd: {0}

+no_response_handler.explanation=\u0394\u03b5\u03bd \u03c5\u03c0\u03ac\u03c1\u03c7\u03bf\u03c5\u03bd \u03b4\u03b9\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b5\u03c2 \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03b1\u03c0\u03bf\u03ba\u03c1\u03af\u03c3\u03b5\u03c9\u03bd \u03b3\u03b9\u03b1 \u03c4\u03b7\u03bd \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c4\u03c9\u03bd \u03c4\u03b5\u03bb\u03b9\u03ba\u03ce\u03bd \u03c3\u03b7\u03bc\u03b5\u03af\u03c9\u03bd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03ba\u03b1\u03b9 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd. AuthorizationEndpointResponseHandler \u03ae TokenEndpointResponseHandler.

+no_gadget_spec=\u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03cd\u03c1\u03b5\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03bf\u03cd \u03c4\u03b7\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2 {0}.

+no_gadget_spec.explanation=\u0394\u03b5\u03bd \u03b5\u03bd\u03c4\u03bf\u03c0\u03af\u03c3\u03c4\u03b7\u03ba\u03b5 \u03bf\u03c1\u03b9\u03c3\u03bc\u03cc\u03c2 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae\u03c2. \u0396\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae \u03bd\u03b1 \u03c1\u03c5\u03b8\u03bc\u03af\u03c3\u03b5\u03b9 \u03c4\u03b7 \u03bc\u03b9\u03ba\u03c1\u03bf\u03b5\u03c6\u03b1\u03c1\u03bc\u03bf\u03b3\u03ae \u03ce\u03c3\u03c4\u03b5 \u03bd\u03b1 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf  OAuth2.0.

+refresh_token_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03bd\u03ad\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2: {0}

+refresh_token_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03bd\u03c4\u03b1\u03bb\u03bb\u03b1\u03b3\u03ae \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03b1\u03bd\u03b1\u03bd\u03ad\u03c9\u03c3\u03b7\u03c2 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2.

+secret_encryption_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7 \u03c4\u03bf\u03c5 \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03bf\u03cd \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03b3\u03b9\u03b1 \u03bc\u03cc\u03bd\u03b9\u03bc\u03b7 \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7: {0}

+secret_encryption_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7\u03bd \u03b1\u03c0\u03bf\u03b8\u03ae\u03ba\u03b5\u03c5\u03c3\u03b7 \u03c4\u03c9\u03bd \u03bc\u03c5\u03c3\u03c4\u03b9\u03ba\u03ce\u03bd OAuth2.0 \u03bc\u03b5 \u03c4\u03b7\u03bd \u03ba\u03b1\u03b8\u03bf\u03c1\u03b9\u03c3\u03bc\u03ad\u03bd\u03b7 \u03bb\u03b5\u03b9\u03c4\u03bf\u03c5\u03c1\u03b3\u03b9\u03ba\u03ae \u03bc\u03bf\u03bd\u03ac\u03b4\u03b1 \u03ba\u03c1\u03c5\u03c0\u03c4\u03bf\u03b3\u03c1\u03ac\u03c6\u03b7\u03c3\u03b7\u03c2.

+access_denied=\u0397 \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03b1\u03c0\u03b1\u03b3\u03bf\u03c1\u03b5\u03cd\u03c4\u03b7\u03ba\u03b5.

+access_denied.explanation=\u039f \u03c7\u03c1\u03ae\u03c3\u03c4\u03b7\u03c2 \u03b1\u03c0\u03b1\u03b3\u03cc\u03c1\u03b5\u03c5\u03c3\u03b5 \u03ae \u03b1\u03ba\u03cd\u03c1\u03c9\u03c3\u03b7 \u03c4\u03b7\u03bd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf\u03bd \u03c0\u03b1\u03c1\u03bf\u03c7\u03ad\u03b1 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd.

+invalid_client=\u039f \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03bf\u03c2: {0}

+invalid_client.explanation= \u0394\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03b5\u03be\u03b1\u03ba\u03c1\u03af\u03b2\u03c9\u03c3\u03b7 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03c4\u03bf\u03c5 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7. \u03a0\u03b9\u03b8\u03b1\u03bd\u03ad\u03c2 \u03b1\u03b9\u03c4\u03af\u03b5\u03c2 \u03b5\u03af\u03bd\u03b1\u03b9 \u03bf \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7\u03c2 \u03bd\u03b1 \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf\u03c2, \u03bd\u03b1 \u03bc\u03b7\u03bd \u03ad\u03c7\u03bf\u03c5\u03bd \u03c3\u03c5\u03bc\u03c0\u03b5\u03c1\u03b9\u03bb\u03b7\u03c6\u03b8\u03b5\u03af \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03ac\u03c2 \u03c4\u03bf\u03c5 \u03ae \u03bd\u03b1 \u03bc\u03b7\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf\u03c2 \u03b5\u03be\u03b1\u03ba\u03c1\u03af\u03b2\u03c9\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd.

+invalid_grant=\u0397 \u03b5\u03ba\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7: {0}

+invalid_grant.explanation=\u0397 \u03b5\u03ba\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7, \u03ad\u03c7\u03b5\u03b9 \u03bb\u03ae\u03be\u03b5\u03b9, \u03ad\u03c7\u03b5\u03b9 \u03b1\u03bd\u03b1\u03ba\u03bb\u03b7\u03b8\u03b5\u03af, \u03b4\u03b5\u03bd \u03c3\u03c5\u03bc\u03c6\u03c9\u03bd\u03b5\u03af \u03bc\u03b5 \u03c4\u03bf URI \u03b1\u03bd\u03b1\u03ba\u03b1\u03c4\u03b5\u03cd\u03b8\u03c5\u03bd\u03c3\u03b7\u03c2 \u03c0\u03bf\u03c5 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af\u03c4\u03b1\u03b9 \u03c3\u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03ae \u03ad\u03c7\u03b5\u03b9 \u03c0\u03b1\u03c1\u03b1\u03c3\u03c7\u03b5\u03b8\u03b5\u03af \u03b3\u03b9\u03b1 \u03ba\u03ac\u03c0\u03bf\u03b9\u03bf\u03bd \u03ac\u03bb\u03bb\u03bf \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7. \u0397 \u03b5\u03ba\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c0\u03b5\u03c1\u03b9\u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03b9 \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2, \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03ba\u03b1\u03c4\u03cc\u03c7\u03bf\u03c5 \u03c4\u03bf\u03c5 \u03c0\u03cc\u03c1\u03bf\u03c5 \u03ae \u03c4\u03b1 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2 \u03c4\u03bf\u03c5 \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7.

+invalid_request=\u0397 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7: {0}

+invalid_request.explanation=\u0397 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7 \u03b3\u03b9\u03b1\u03c4\u03af \u03b4\u03b5\u03bd \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03b1\u03b9\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf, \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03bc\u03b9\u03b1 \u03bc\u03b7 \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03b9\u03b6\u03cc\u03bc\u03b5\u03bd\u03b7 \u03c4\u03b9\u03bc\u03ae \u03c0\u03b1\u03c1\u03b1\u03bc\u03ad\u03c4\u03c1\u03bf\u03c5, \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03b4\u03cd\u03bf \u03c6\u03bf\u03c1\u03ad\u03c2 \u03bc\u03b9\u03b1 \u03c0\u03b1\u03c1\u03ac\u03bc\u03b5\u03c4\u03c1\u03bf, \u03c0\u03b5\u03c1\u03b9\u03ad\u03c7\u03b5\u03b9 \u03c0\u03bf\u03bb\u03bb\u03b1\u03c0\u03bb\u03ac \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03b1 \u03c4\u03b1\u03c5\u03c4\u03cc\u03c4\u03b7\u03c4\u03b1\u03c2, \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03b5\u03af \u03c0\u03b5\u03c1\u03b9\u03c3\u03c3\u03cc\u03c4\u03b5\u03c1\u03bf\u03c5\u03c2 \u03b1\u03c0\u03cc \u03ad\u03bd\u03b1\u03bd \u03bc\u03b7\u03c7\u03b1\u03bd\u03b9\u03c3\u03bc\u03bf\u03cd\u03c2 \u03b5\u03be\u03b1\u03ba\u03c1\u03af\u03b2\u03c9\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03ae \u03ad\u03c7\u03b5\u03b9 \u03ba\u03ac\u03c0\u03bf\u03b9\u03bf \u03ac\u03bb\u03bb\u03bf \u03c3\u03c5\u03bd\u03c4\u03b1\u03ba\u03c4\u03b9\u03ba\u03cc \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1.

+invalid_scope=\u0397 \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7: {0}

+invalid_scope.explanation=\u0397 \u03b6\u03b7\u03c4\u03bf\u03cd\u03bc\u03b5\u03bd\u03b7 \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03ad\u03b3\u03ba\u03c5\u03c1\u03b7, \u03b5\u03af\u03bd\u03b1\u03b9 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03b7, \u03ad\u03c7\u03b5\u03b9 \u03b5\u03c3\u03c6\u03b1\u03bb\u03bc\u03ad\u03bd\u03b7 \u03bc\u03bf\u03c1\u03c6\u03ae \u03ae \u03c5\u03c0\u03b5\u03c1\u03b2\u03b1\u03af\u03bd\u03b5\u03b9 \u03c4\u03b7\u03bd \u03b5\u03bc\u03b2\u03ad\u03bb\u03b5\u03b9\u03b1 \u03c0\u03bf\u03c5 \u03ad\u03c7\u03b5\u03b9 \u03b5\u03ba\u03c7\u03c9\u03c1\u03b7\u03b8\u03b5\u03af \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03ba\u03ac\u03c4\u03bf\u03c7\u03bf \u03c4\u03bf\u03c5 \u03c0\u03cc\u03c1\u03bf\u03c5.

+server_error=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03c3\u03c4\u03bf\u03bd \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae: {0}

+server_error.explanation=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b5\u03bd\u03c4\u03cc\u03c0\u03b9\u03c3\u03b5 \u03bc\u03b9\u03b1 \u03bc\u03b7 \u03b1\u03bd\u03b1\u03bc\u03b5\u03bd\u03cc\u03bc\u03b5\u03bd\u03b7 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c0\u03bf\u03c5 \u03b4\u03b5\u03bd \u03b5\u03c0\u03ad\u03c4\u03c1\u03b5\u03c8\u03b5 \u03c4\u03b7 \u03b4\u03b9\u03b5\u03ba\u03c0\u03b5\u03c1\u03b1\u03af\u03c9\u03c3\u03b7 \u03c4\u03b7\u03c2 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2.

+server_rejected_request=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b1\u03c0\u03ad\u03c1\u03c1\u03b9\u03c8\u03b5 \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 : statuscode = {0}

+server_rejected_request.explanation=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b4\u03b7\u03bc\u03b9\u03bf\u03cd\u03c1\u03b3\u03b7\u03c3\u03b5 \u03bc\u03b9\u03b1 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 OAuth2Request, \u03b1\u03bb\u03bb\u03ac \u03bf \u03c0\u03b1\u03c1\u03bf\u03c7\u03ad\u03b1\u03c2 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03b9\u03ce\u03bd \u03c4\u03b7\u03bd \u03b1\u03c0\u03ad\u03c1\u03c1\u03b9\u03c8\u03b5.

+temporarily_unavailable=\u0397 \u03c5\u03c0\u03b7\u03c1\u03b5\u03c3\u03af\u03b1 \u03b4\u03b5\u03bd \u03b5\u03af\u03bd\u03b1\u03b9 \u03b4\u03b9\u03b1\u03b8\u03ad\u03c3\u03b9\u03bc\u03b7 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03c3\u03c4\u03b9\u03b3\u03bc\u03ae: {0}

+temporarily_unavailable.explanation=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03bf\u03c1\u03b5\u03af \u03bd\u03b1 \u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03b5\u03af \u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03bb\u03cc\u03b3\u03c9 \u03c5\u03c0\u03b5\u03c1\u03b2\u03bf\u03bb\u03b9\u03ba\u03bf\u03cd \u03c6\u03cc\u03c1\u03c4\u03bf\u03c5 \u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1\u03c2 \u03ae \u03b3\u03b9\u03b1\u03c4\u03af \u03b2\u03c1\u03af\u03c3\u03ba\u03b5\u03c4\u03b1\u03b9 \u03c3\u03b5 \u03ba\u03b1\u03c4\u03ac\u03c3\u03c4\u03b1\u03c3\u03b7 \u03c3\u03c5\u03bd\u03c4\u03ae\u03c1\u03b7\u03c3\u03b7\u03c2.

+token_response_problem=\u0393\u03af\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03af\u03b1 \u03c4\u03b7\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b1\u03c0\u03cc \u03c4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd: {0}

+token_response_problem.explanation=\u03a4\u03bf \u03c4\u03b5\u03bb\u03b9\u03ba\u03cc \u03c3\u03b7\u03bc\u03b5\u03af\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03ad\u03c3\u03c4\u03b5\u03b9\u03bb\u03b5 \u03bc\u03b9\u03b1 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7 \u03c4\u03b7\u03bd \u03bf\u03c0\u03bf\u03af\u03b1 \u03bf \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b4\u03b5\u03bd \u03bc\u03c0\u03cc\u03c1\u03b5\u03c3\u03b5 \u03bd\u03b1 \u03b5\u03c0\u03b5\u03be\u03b5\u03c1\u03b3\u03b1\u03c3\u03c4\u03b5\u03af.

+unauthorized_client=\u039f \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7: {0}

+unauthorized_client.explanation=\u039f \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7\u03c2 \u03b4\u03b5\u03bd \u03ad\u03c7\u03b5\u03b9 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7 \u03bd\u03b1 \u03b6\u03b7\u03c4\u03ae\u03c3\u03b5\u03b9 \u03ad\u03bd\u03b1 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf.

+unsupported_grant_type=\u03a4\u03bf \u03b5\u03af\u03b4\u03bf\u03c2 \u03b5\u03ba\u03c7\u03ce\u03c1\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9: {0}

+unsupported_grant_type.explanation=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7 \u03bb\u03ae\u03c8\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf.

+unsupported_response_type=\u03a4\u03bf \u03b5\u03af\u03b4\u03bf\u03c2 \u03b1\u03c0\u03cc\u03ba\u03c1\u03b9\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03c4\u03b1\u03b9: {0}

+unsupported_response_type.explanation=\u039f \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae\u03c2 \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03b4\u03b5\u03bd \u03c5\u03c0\u03bf\u03c3\u03c4\u03b7\u03c1\u03af\u03b6\u03b5\u03b9 \u03c4\u03b7 \u03bb\u03ae\u03c8\u03b7 \u03b5\u03bd\u03cc\u03c2 \u03ba\u03c9\u03b4\u03b9\u03ba\u03bf\u03cd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2 \u03c7\u03c1\u03b7\u03c3\u03b9\u03bc\u03bf\u03c0\u03bf\u03b9\u03ce\u03bd\u03c4\u03b1\u03c2 \u03b1\u03c5\u03c4\u03ae \u03c4\u03b7 \u03bc\u03ad\u03b8\u03bf\u03b4\u03bf.

+mac_token_problem=\u03a5\u03c0\u03bf\u03b2\u03ac\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc mac: {0}

+mac_token_problem.explanation=\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd mac \u03c4\u03b7\u03c2 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03bd \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae \u03c0\u03cc\u03c1\u03c9\u03bd.

+bearer_token_problem=\u03a5\u03c0\u03bf\u03b2\u03ac\u03bb\u03bb\u03b5\u03c4\u03b1\u03b9 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7 \u03b3\u03b9\u03b1 \u03c4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc bearer: {0}

+bearer_token_problem.explanation=\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c4\u03bf\u03c5 \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03bf\u03cd bearer \u03c4\u03b7\u03c2 \u03b1\u03af\u03c4\u03b7\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03bd \u03b5\u03be\u03c5\u03c0\u03b7\u03c1\u03b5\u03c4\u03b7\u03c4\u03ae \u03c0\u03cc\u03c1\u03c9\u03bd.

+authentication_problem=\u03a0\u03c1\u03bf\u03c3\u03c4\u03af\u03b8\u03b5\u03c4\u03b1\u03b9 \u03b7 \u03b5\u03be\u03b1\u03ba\u03c1\u03af\u03b2\u03c9\u03c3\u03b7 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03c0\u03b5\u03bb\u03ac\u03c4\u03b7: {0}

+authentication_problem.explanation=\u0394\u03b5\u03bd \u03ae\u03c4\u03b1\u03bd \u03b4\u03c5\u03bd\u03b1\u03c4\u03ae \u03b7 \u03c0\u03c1\u03bf\u03c3\u03b8\u03ae\u03ba\u03b7 \u03c4\u03c9\u03bd \u03ba\u03b5\u03c6\u03b1\u03bb\u03af\u03b4\u03c9\u03bd \u03b5\u03be\u03b1\u03ba\u03c1\u03af\u03b2\u03c9\u03c3\u03b7\u03c2 \u03c3\u03c4\u03bf\u03b9\u03c7\u03b5\u03af\u03c9\u03bd \u03c3\u03c4\u03b7\u03bd \u03b1\u03af\u03c4\u03b7\u03c3\u03b7.

+unknown_problem=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ac\u03b3\u03bd\u03c9\u03c3\u03c4\u03bf \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1: {0}

+unknown_problem.explanation=\u0396\u03b7\u03c4\u03ae\u03c3\u03c4\u03b5 \u03b1\u03c0\u03cc \u03c4\u03bf \u03b4\u03b9\u03b1\u03c7\u03b5\u03b9\u03c1\u03b9\u03c3\u03c4\u03ae \u03bd\u03b1 \u03b4\u03b9\u03b5\u03c1\u03b5\u03c5\u03bd\u03ae\u03c3\u03b5\u03b9 \u03c4\u03bf \u03c0\u03c1\u03cc\u03b2\u03bb\u03b7\u03bc\u03b1.

+code_grant_problem=\u03a4\u03bf \u03b4\u03b9\u03b1\u03ba\u03c1\u03b9\u03c4\u03b9\u03ba\u03cc \u03c0\u03c1\u03cc\u03c3\u03b2\u03b1\u03c3\u03b7\u03c2 \u03bb\u03b1\u03bc\u03b2\u03ac\u03bd\u03b5\u03c4\u03b1\u03b9 \u03b1\u03c0\u03cc \u03c4\u03bf\u03bd \u03ba\u03c9\u03b4\u03b9\u03ba\u03cc \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2: {0}

+code_grant_problem.explanation=\u03a0\u03b1\u03c1\u03bf\u03c5\u03c3\u03b9\u03ac\u03c3\u03c4\u03b7\u03ba\u03b5 \u03ad\u03bd\u03b1 \u03c3\u03c6\u03ac\u03bb\u03bc\u03b1 \u03ba\u03b1\u03c4\u03ac \u03c4\u03b7 \u03c1\u03bf\u03ae \u03ba\u03c9\u03b4\u03b9\u03ba\u03ce\u03bd \u03b5\u03be\u03bf\u03c5\u03c3\u03b9\u03bf\u03b4\u03cc\u03c4\u03b7\u03c3\u03b7\u03c2.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_en_US.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_en_US.properties
new file mode 100644
index 0000000..ab41327
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_en_US.properties
@@ -0,0 +1,81 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.  
+message_header= {0} encountered an error :  
+authorization_code_problem=The authorization code is being exchanged for the access token : {0}
+authorization_code_problem.explanation=An error occurred when exchanging the authorization code for the access_token.
+authorize_problem=The authorization process for {0} is initiating.
+authorize_problem.explanation=An error occurred when initiating authorization process.
+callback_problem=The redirect response is being processed : {0}
+callback_problem.explanation=An error occurred when processing the redirect response from the service provider.
+client_credentials_problem=The access_token is being retrieved for the client with the following client credential : {0}
+client_credentials_problem.explanation=An error occurred when retrieving access token in the client_credentials flow.
+fetch_init_problem=The OAuth2Request is initializing: {0}
+fetch_init_problem.explanation=A low-level error occurred when initializing the OAuth2Request fetch.
+fetch_problem=executing OAuth2Request.fetch() : {0}
+fetch_problem.explanation=An error occurred when issuing the OAuth2Request fetch.
+gadget_spec_problem=The following gadget specification is processing : {0}
+gadget_spec_problem.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.
+get_oauth2_accessor_problem=getting an OAuth2Accessor for the OAuth2Request : {0}
+get_oauth2_accessor_problem.explanation=An error occured. Ask your administrator to create an OAuth2.0 Client binding for this gadget and service.
+lookup_spec_problem=The gadget specification is being retrieved : {0}
+lookup_spec_problem.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.
+missing_fetch_params=The following required fetch parameters are missing : {0} 
+missing_fetch_params.explanation=The fetch() method was called with bad parameters.
+missing_server_response=An error occurred during OAuth2.0 service provider response : {0}
+missing_server_response.explanation=The server created a valid OAuth2Request but was either unable to issue the request or get a valid response from the service provider.
+no_response_handler=A response handler did not process the following response : {0}
+no_response_handler.explanation=Response handler do not exists for processing the authorization and token endpoints. AuthorizationEndpointResponseHandler or TokenEndpointResponseHandler.
+no_gadget_spec=The gadget specification for {0} can not be found.
+no_gadget_spec.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.
+refresh_token_problem=The refresh token is being exchanged for the access_token : {0}
+refresh_token_problem.explanation=An error occurred when exchanging the refresh token for the access token.
+secret_encryption_problem=The token secret is bing encrypted for persistence : {0}
+secret_encryption_problem.explanation=An error occurred when storing OAuth2.0 secrets with the provided encryption module.
+access_denied=Access was denied.
+access_denied.explanation=The user denied or canceled the authorization with the service provider.
+invalid_client=The client is invalid : {0}
+invalid_client.explanation= The client cannot be authenticated,possibly because hte client is not known., the client authentication was not included, or the authentication method is not suppported.
+invalid_grant=The authorization grant is invalid : {0}
+invalid_grant.explanation=The authorization grant is invalid,expired,revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The authorization grant can include the authorization code, the credentials of the resource owner, or the credentials of the client.
+invalid_request=The request is invalid : {0}
+invalid_request.explanation=The request is invalid because it is missing a required parameter, includes an unsupported parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.
+invalid_scope=The scope is invalid : {0}
+invalid_scope.explanation=The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.
+server_error=A server error occurred: {0}
+server_error.explanation=The authorization server encountered an unexpected condition which prevented it from fulfilling the request.
+server_rejected_request=The server rejected the request : statuscode = {0}
+server_rejected_request.explanation=The server created an OAuth2Request, but the service provider rejected it.
+temporarily_unavailable=The service is temporarily unavailable : {0}
+temporarily_unavailable.explanation=The authorization server is unable to handle the request because it is temporarily overloaded or is in maintenance mode.
+token_response_problem=The response from token endpoint is being processed : {0}
+token_response_problem.explanation=The token endpoint sent a response that the server could not process.
+unauthorized_client=The client is not authorized : {0}
+unauthorized_client.explanation=The client is not authorized to request an access token using this method.
+unsupported_grant_type=The grant type if not supported : {0}
+unsupported_grant_type.explanation=The authorization server does not support obtaining an access token using this method.
+unsupported_response_type=The response type is unsupported: {0}
+unsupported_response_type.explanation=The authorization server does not support obtaining an authorization code using this method.
+mac_token_problem=The mac token is being requested : {0}
+mac_token_problem.explanation=The mac token on the request could not be added to the resource server.
+bearer_token_problem=The bearer token is being requested : {0}
+bearer_token_problem.explanation=The bearer token on the request could not be added to the resource server.
+authentication_problem=The client authentication is being added : {0}
+authentication_problem.explanation=The authentication headers could not be added to the request.
+unknown_problem=An unknown error occurred : {0}
+unknown_problem.explanation=Ask your system administrator to investigate the problem.
+code_grant_problem=The access token being obtained from the authorization code : {0}
+code_grant_problem.explanation=An error occurred during the authorization code flow.
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_es.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_es.properties
new file mode 100644
index 0000000..100dba6
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_es.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} ha detectado un error:

+authorization_code_problem=El c\u00f3digo de autorizaci\u00f3n se est\u00e1 intercambiando por la se\u00f1al de acceso: {0}

+authorization_code_problem.explanation=Se ha producido un error al intercambiar el c\u00f3digo de autorizaci\u00f3n para access_token (se\u00f1al de acceso).

+authorize_problem=El proceso de autorizaci\u00f3n para {0} se est\u00e1 iniciando.

+authorize_problem.explanation=Se ha producido un error al iniciar el proceso de autorizaci\u00f3n.

+callback_problem=La respuesta de redirecci\u00f3n se est\u00e1 procesando: {0}

+callback_problem.explanation=Se ha producido un error al procesar la respuesta de redirecci\u00f3n desde el proveedor de servicios.

+client_credentials_problem=access_token (se\u00f1al de acceso) se est\u00e1 recuperando para el cliente con la credencial de cliente siguiente: {0}

+client_credentials_problem.explanation=Se ha producido un error al recuperar la se\u00f1al de acceso en el flujo client_credentials (credenciales de cliente).

+fetch_init_problem=OAuth2Request se est\u00e1 inicializando: {0}

+fetch_init_problem.explanation=Se ha producido un error de bajo nivel al inicializar la captaci\u00f3n de OAuth2Request.

+fetch_problem=ejecutando OAuth2Request.fetch() : {0}

+fetch_problem.explanation=Se ha producido un error al emitir la captaci\u00f3n de OAuth2Request.

+gadget_spec_problem=La especificaci\u00f3n de gadget siguiente se est\u00e1 procesando : {0}

+gadget_spec_problem.explanation=No se ha encontrado una especificaci\u00f3n de gadget. Solicite al administrador que configure su gadget para que funcione con OAuth2.0.

+get_oauth2_accessor_problem=obteniendo un OAuth2Accessor para la solicitud OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=Se ha producido un error. Solicite al administrador que cree un enlace de cliente de OAuth2.0 para este gadget y servicio.

+lookup_spec_problem=La especificaci\u00f3n de gadget se est\u00e1 recuperando: {0}

+lookup_spec_problem.explanation=No se ha encontrado una especificaci\u00f3n de gadget. Solicite al administrador que configure su gadget para que funcione con OAuth2.0.

+missing_fetch_params=Faltan los par\u00e1metros de captaci\u00f3n necesarios siguientes: {0}

+missing_fetch_params.explanation=El m\u00e9todo fetch() se ha llamado con par\u00e1metros err\u00f3neos.

+missing_server_response=Se ha producido un error durante la respuesta del proveedor de servicios de OAuth2.0: {0}

+missing_server_response.explanation=El servidor creado una solicitud OAuth2Request v\u00e1lida, pero no ha podido emitir la solicitud u obtener una respuesta v\u00e1lida desde el proveedor de servicios.

+no_response_handler=Un manejador de respuestas no ha procesado la respuesta siguiente: {0}

+no_response_handler.explanation=El manejador de respuestas no existe para procesar la autorizaci\u00f3n y los puntos finales de se\u00f1al. AuthorizationEndpointResponseHandler o TokenEndpointResponseHandler.

+no_gadget_spec=La especificaci\u00f3n de gadget para {0} no se ha podido encontrar.

+no_gadget_spec.explanation=No se ha encontrado una especificaci\u00f3n de gadget. Solicite al administrador que configure su gadget para que funcione con OAuth2.0.

+refresh_token_problem=La se\u00f1al de renovaci\u00f3n se est\u00e1 intercambiando por access_token (se\u00f1al_acceso): {0}

+refresh_token_problem.explanation=Se ha producido un error al intercambiar la se\u00f1al de renovaci\u00f3n para la se\u00f1al de acceso.

+secret_encryption_problem=El secreto de se\u00f1al se est\u00e1 cifrando para persistencia: {0}

+secret_encryption_problem.explanation=Se ha producido un error al almacenar secretos OAuth2.0 con el m\u00f3dulo de cifrado proporcionado.

+access_denied=Acceso denegado.

+access_denied.explanation=El usuario ha denegado o cancelado la autorizaci\u00f3n con el proveedor de servicios.

+invalid_client=El cliente no es v\u00e1lido: {0}

+invalid_client.explanation= El cliente no se puede autenticar, probablemente debido a que el cliente es desconocido, la autenticaci\u00f3n de cliente no se ha incluido o el m\u00e9todo de autenticaci\u00f3n no tienen soporte.

+invalid_grant=El otorgamiento de autorizaci\u00f3n no es v\u00e1lido: {0}

+invalid_grant.explanation=El otorgamiento de autorizaci\u00f3n no es v\u00e1lido, ha caducado, ha sido revocado, no coincide con el URI de redirecci\u00f3n en la solicitud de autorizaci\u00f3n o ha sido emitido por otro cliente. El otorgamiento de autorizaci\u00f3n puede incluir un c\u00f3digo de autorizaci\u00f3n, las credenciales del propietario del recurso o las credenciales del cliente.

+invalid_request=La solicitud no es v\u00e1lida: {0}

+invalid_request.explanation=La solicitud no es v\u00e1lida porque le falta un par\u00e1metro necesario, incluye un valor de par\u00e1metro no soportado, repite un par\u00e1metro, incluye varias credenciales, utiliza m\u00e1s de un mecanismo para autenticar el cliente o tiene alg\u00fan otro tipo de problema con el formato.

+invalid_scope=El \u00e1mbito no es v\u00e1lido: {0}

+invalid_scope.explanation=El \u00e1mbito solicitado no es v\u00e1lido, es desconocido, no est\u00e1 bien formado o sobrepasa el \u00e1mbito otorgado por el propietario del recurso.

+server_error=Se ha producido un error de servidor: {0}

+server_error.explanation=El servidor de autorizaci\u00f3n ha detectado una condici\u00f3n no esperada que ha impedido que se complete la solicitud.

+server_rejected_request=El servidor ha rechazado la solicitud: c\u00f3digo de estado = {0}

+server_rejected_request.explanation=El servidor ha creado una OAuth2Request, pero el proveedor de servicios la ha rechazado.

+temporarily_unavailable=El servicio no est\u00e1 disponible temporalmente: {0}

+temporarily_unavailable.explanation=El servidor de autorizaci\u00f3n no ha podido gestionar la solicitud porque est\u00e1 temporalmente sobrecargado o est\u00e1 en modalidad de mantenimiento.

+token_response_problem=La respuesta desde el punto final de se\u00f1al se est\u00e1 procesando: {0}

+token_response_problem.explanation=El punto final de se\u00f1al ha enviado una respuesta que el servidor no ha podido procesar.

+unauthorized_client=El cliente no est\u00e1 autorizado: {0}

+unauthorized_client.explanation=El cliente no est\u00e1 autorizado para solicitar una se\u00f1al de acceso usando este m\u00e9todo.

+unsupported_grant_type=El tipo grant (otorgar) no tiene soporte: {0}

+unsupported_grant_type.explanation=El servidor de autorizaci\u00f3n no tiene soporte para la obtenci\u00f3n de una se\u00f1al de acceso mediante este m\u00e9todo.

+unsupported_response_type=El tipo de respuesta no tiene soporte: {0}

+unsupported_response_type.explanation=El servidor de autorizaci\u00f3n no tiene soporte para la obtenci\u00f3n de un c\u00f3digo de autorizaci\u00f3n mediante este m\u00e9todo.

+mac_token_problem=La se\u00f1al mac se est\u00e1 solicitando: {0}

+mac_token_problem.explanation=La se\u00f1al mac en la solicitud no se ha podido a\u00f1adir al servidor de recursos.

+bearer_token_problem=La se\u00f1al bearer se est\u00e1 solicitando: {0}

+bearer_token_problem.explanation=La se\u00f1al bearer en la solicitud no se ha podido a\u00f1adir al servidor de recursos.

+authentication_problem=La autenticaci\u00f3n de cliente se est\u00e1 a\u00f1adiendo: {0}

+authentication_problem.explanation=Las cabeceras de autenticaci\u00f3n no se han podido a\u00f1adir a la solicitud.

+unknown_problem=Se ha producido un error desconocido: {0}

+unknown_problem.explanation=Solicite al administrador del sistema que investigue el problema.

+code_grant_problem=La se\u00f1al de acceso que se obtiene del c\u00f3digo de autorizaci\u00f3n: {0}

+code_grant_problem.explanation=Se ha producido un error durante el flujo del c\u00f3digo de autorizaci\u00f3n.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_fi.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_fi.properties
new file mode 100644
index 0000000..83a9961
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_fi.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= Kohteessa {0} on ilmennyt virhe:

+authorization_code_problem=J\u00e4rjestelm\u00e4 vaihtaa valtuutuskoodin k\u00e4ytt\u00f6sanakkeeseen: {0}

+authorization_code_problem.explanation=On ilmennyt virhe vaihdettaessa valtuutuskoodia k\u00e4ytt\u00f6sanakkeeseen.

+authorize_problem=Kohteen {0} valtuutus alkaa.

+authorize_problem.explanation=Valtuutuksen aloituksessa on ilmennyt virhe.

+callback_problem=Edelleenohjauksen vastauksen k\u00e4sittely on meneill\u00e4\u00e4n: {0}

+callback_problem.explanation=Palveluntarjoajan l\u00e4hett\u00e4m\u00e4n edelleenohjauksen vastauksen k\u00e4sittelyss\u00e4 on ilmennyt virhe.

+client_credentials_problem=J\u00e4rjestelm\u00e4 noutaa seuraavia ty\u00f6asemaohjelman valtuustietoja vastaavan ty\u00f6asemaohjelman k\u00e4ytt\u00f6sanakkeen: {0}

+client_credentials_problem.explanation=On ilmennyt virhe noudettaessa k\u00e4ytt\u00f6sanaketta ty\u00f6asemaohjelman valtuustietojen tietovirrasta.

+fetch_init_problem=OAuth2Request-pyynn\u00f6n alustus on meneill\u00e4\u00e4n: {0}

+fetch_init_problem.explanation=OAuth2Request-noudon alustuksessa on ilmennyt alatason virhe.

+fetch_problem=Komennon OAuth2Request.fetch() toteutus on meneill\u00e4\u00e4n: {0}

+fetch_problem.explanation=OAuth2Request-noudon toteutuksessa on ilmennyt virhe.

+gadget_spec_problem=Seuraavan gadget-m\u00e4\u00e4rityksen k\u00e4sittely on meneill\u00e4\u00e4n: {0}

+gadget_spec_problem.explanation=Gadget-m\u00e4\u00e4rityst\u00e4 ei l\u00f6ytynyt.  Pyyd\u00e4 p\u00e4\u00e4k\u00e4ytt\u00e4j\u00e4\u00e4 m\u00e4\u00e4ritt\u00e4m\u00e4\u00e4n gadget-komponentin kokoonpano niin, ett\u00e4 se toimii OAuth2.0-version kanssa.

+get_oauth2_accessor_problem=OAuth2Request-pyynn\u00f6n OAuth2Accessor-rajapinnan nouto on meneill\u00e4\u00e4n: {0}

+get_oauth2_accessor_problem.explanation=On ilmennyt virhe. Pyyd\u00e4 p\u00e4\u00e4k\u00e4ytt\u00e4j\u00e4\u00e4 luomaan OAuth2.0-ty\u00f6asemaohjelmasidonta t\u00e4lle gadget-komponentille ja palvelulle.

+lookup_spec_problem=Gadget-m\u00e4\u00e4rityksen nouto on meneill\u00e4\u00e4n: {0}

+lookup_spec_problem.explanation=Gadget-m\u00e4\u00e4rityst\u00e4 ei l\u00f6ytynyt.  Pyyd\u00e4 p\u00e4\u00e4k\u00e4ytt\u00e4j\u00e4\u00e4 m\u00e4\u00e4ritt\u00e4m\u00e4\u00e4n gadget-komponentin kokoonpano niin, ett\u00e4 se toimii OAuth2.0-version kanssa.

+missing_fetch_params=Seuraavat pakolliset noutoparametrit puuttuvat: {0}

+missing_fetch_params.explanation=Fetch()-metodin kutsuun on m\u00e4\u00e4ritetty virheellisi\u00e4 parametreja.

+missing_server_response=OAuth2.0-palveluntarjoajan vastauksessa on ilmennyt virhe: {0}

+missing_server_response.explanation=Palvelin on luonut kelvollisen OAuth2Request-pyynn\u00f6n, mutta palvelin ei voinut toteuttaa sit\u00e4 tai palvelin ei saanut kelvollista vastausta palveluntarjoajalta.

+no_response_handler=Vastausten k\u00e4sittelytoiminto ei k\u00e4sitellyt seuraavaa vastausta: {0}

+no_response_handler.explanation=Valtuutuksen ja sanakkeiden p\u00e4\u00e4tepisteit\u00e4 k\u00e4sittelev\u00e4\u00e4 vastausten k\u00e4sittelytoimintoa ei ole. Tarvittava k\u00e4sittelytoiminto on AuthorizationEndpointResponseHandler tai TokenEndpointResponseHandler.

+no_gadget_spec=Kohteen {0} gadget-m\u00e4\u00e4rityst\u00e4 ei l\u00f6ydy.

+no_gadget_spec.explanation=Gadget-m\u00e4\u00e4rityst\u00e4 ei l\u00f6ytynyt.  Pyyd\u00e4 p\u00e4\u00e4k\u00e4ytt\u00e4j\u00e4\u00e4 m\u00e4\u00e4ritt\u00e4m\u00e4\u00e4n gadget-komponentin kokoonpano niin, ett\u00e4 se toimii OAuth2.0-version kanssa.

+refresh_token_problem=J\u00e4rjestelm\u00e4 vaihtaa p\u00e4ivityssanakkeen k\u00e4ytt\u00f6sanakkeeseen: {0}

+refresh_token_problem.explanation=On ilmennyt virhe vaihdettaessa p\u00e4ivityssanaketta k\u00e4ytt\u00f6sanakkeeseen.

+secret_encryption_problem=Sanakkeen salaisuuden salaus pysyvyysm\u00e4\u00e4rityst\u00e4 varten on meneill\u00e4\u00e4n: {0}

+secret_encryption_problem.explanation=OAuth2.0-salaisuuksien tallennuksessa annetun salausmoduulin avulla on ilmennyt virhe.

+access_denied=K\u00e4ytt\u00f6 on estetty.

+access_denied.explanation=Palveluntarjoaja on est\u00e4nyt tai peruuttanut k\u00e4ytt\u00e4j\u00e4n valtuutuksen.

+invalid_client=Ty\u00f6asemaohjelma ei kelpaa: {0}

+invalid_client.explanation= Ty\u00f6asemaohjelman todennus ei onnistu mahdollisesti siksi, ett\u00e4 ty\u00f6asemaohjelma ei ole tunnettu, ty\u00f6asemaohjelman todennusta ei sis\u00e4llytetty ty\u00f6asemaohjelmaan tai todennusmenetelm\u00e4 ei ole tuettu.

+invalid_grant=K\u00e4ytt\u00f6oikeuksia ei ole my\u00f6nnetty oikein: {0}

+invalid_grant.explanation=K\u00e4ytt\u00f6oikeuksia ei ole my\u00f6nnetty oikein, my\u00f6nnetyt k\u00e4ytt\u00f6oikeudet ovat vanhentuneet tai ne on ev\u00e4tty, k\u00e4ytt\u00f6oikeudet eiv\u00e4t vastaa valtuutuspyynn\u00f6ss\u00e4 k\u00e4ytetty\u00e4 edelleenohjauksen URI-osoitetta tai k\u00e4ytt\u00f6oikeudet on my\u00f6nnetty johonkin toiseen ty\u00f6asemaohjelmaan. K\u00e4ytt\u00f6oikeuksien my\u00f6nt\u00e4misen yhteydess\u00e4 voidaan toimittaa valtuutuskoodi, resurssin omistajan valtuustiedot tai ty\u00f6asemaohjelman valtuustiedot.

+invalid_request=Pyynt\u00f6 on virheellinen: {0}

+invalid_request.explanation=Pyynt\u00f6 on virheellinen, koska pyynn\u00f6st\u00e4 puuttuu pakollinen parametri, pyynt\u00f6 sis\u00e4lt\u00e4\u00e4 muita kuin tuettuja parametrien arvoja, pyynn\u00f6ss\u00e4 toistuu parametreja, pyynt\u00f6 sis\u00e4lt\u00e4\u00e4 useita valtuustietoja, pyynt\u00f6 k\u00e4ytt\u00e4\u00e4 useita menetelmi\u00e4 todennukseen ty\u00f6asemaohjelmassa tai pyynt\u00f6 on muutoin virheellisesti muotoiltu.

+invalid_scope=Alue on virheellinen: {0}

+invalid_scope.explanation=Pyydetty alue on virheellinen, tuntematon tai virheellisesti muotoiltu, tai se on suurempi kuin resurssin omistajan my\u00f6nt\u00e4m\u00e4 alue.

+server_error=On ilmennyt palvelinvirhe: {0}

+server_error.explanation=Valtuutuspalvelimessa on ilmennyt odottamaton tila, joka esti pyynn\u00f6n toteutuksen.

+server_rejected_request=Palvelin on hyl\u00e4nnyt pyynn\u00f6n: tilakoodi = {0}

+server_rejected_request.explanation=Palvelin on luonut OAuth2Request-pyynn\u00f6n, mutta palveluntarjoaja hylk\u00e4si sen.

+temporarily_unavailable=Palvelu on tilap\u00e4isesti poissa k\u00e4yt\u00f6st\u00e4: {0}

+temporarily_unavailable.explanation=Valtuutuspalvelin ei voi k\u00e4sitell\u00e4 pyynt\u00f6\u00e4, koska palvelin on tilap\u00e4isesti ylikuormittunut tai palvelin on yll\u00e4pitotilassa.

+token_response_problem=Sanakkeen p\u00e4\u00e4tepisteen vastauksen k\u00e4sittely on meneill\u00e4\u00e4n: {0}

+token_response_problem.explanation=Sanakkeen p\u00e4\u00e4tepiste on l\u00e4hett\u00e4nyt vastauksen, jota palvelin ei voinut k\u00e4sitell\u00e4.

+unauthorized_client=Ty\u00f6asemaohjelmalla ei ole valtuuksia: {0}

+unauthorized_client.explanation=Ty\u00f6asemaohjelman valtuudet eiv\u00e4t riit\u00e4 pyyt\u00e4m\u00e4\u00e4n k\u00e4ytt\u00f6sanaketta t\u00e4m\u00e4n metodin avulla.

+unsupported_grant_type=K\u00e4ytt\u00f6oikeuksien my\u00f6nt\u00e4mislaji, jos se ei ole tuettu: {0}

+unsupported_grant_type.explanation=K\u00e4ytt\u00f6sanakkeen nouto t\u00e4m\u00e4n metodin avulla ei ole tuettua valtuutuspalvelimessa.

+unsupported_response_type=Vastauslaji ei ole tuettu: {0}

+unsupported_response_type.explanation=Valtuutuskoodin nouto t\u00e4m\u00e4n metodin avulla ei ole tuettua valtuutuspalvelimessa.

+mac_token_problem=Mac-sanakkeen pyynt\u00f6 on meneill\u00e4\u00e4n: {0}

+mac_token_problem.explanation=Pyynn\u00f6n sis\u00e4lt\u00e4m\u00e4n mac-sanakkeen lis\u00e4ys resurssipalvelimeen ei onnistunut.

+bearer_token_problem=Haltijakohtaisen sanakkeen pyynt\u00f6 on meneill\u00e4\u00e4n: {0}

+bearer_token_problem.explanation=Pyynn\u00f6n sis\u00e4lt\u00e4m\u00e4n haltijakohtaisen sanakkeen lis\u00e4ys resurssipalvelimeen ei onnistunut.

+authentication_problem=Ty\u00f6asemaohjelman todennuksen lis\u00e4ys on meneill\u00e4\u00e4n: {0}

+authentication_problem.explanation=Todennussaatteiden lis\u00e4ys pyynt\u00f6\u00f6n ei onnistunut.

+unknown_problem=On ilmennyt tuntematon virhe: {0}

+unknown_problem.explanation=Pyyd\u00e4 j\u00e4rjestelm\u00e4n p\u00e4\u00e4k\u00e4ytt\u00e4j\u00e4\u00e4 tutkimaan ongelmaa.

+code_grant_problem=K\u00e4ytt\u00f6sanakkeen nouto valtuutuskoodista on meneill\u00e4\u00e4n: {0}

+code_grant_problem.explanation=Valtuutuskoodivirrassa on ilmennyt virhe.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_fr.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_fr.properties
new file mode 100644
index 0000000..5a3d8ed
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_fr.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} a rencontr\u00e9 une erreur :

+authorization_code_problem=Le code d''autorisation est \u00e9chang\u00e9 pour le jeton d''acc\u00e8s : {0}

+authorization_code_problem.explanation=Une erreur s'est produite lors de l'\u00e9change du code d'autorisation pour access_token.

+authorize_problem=Le processus d''autorisation pour {0} est en cours d''initialisation.

+authorize_problem.explanation=Une erreur s'est produite lors du d\u00e9marrage du processus d'autorisation.

+callback_problem=La r\u00e9ponse de redirection est en cours de traitement : {0}

+callback_problem.explanation=Une erreur s'est produite lors du traitement de la r\u00e9ponse de redirection \u00e0 partir du fournisseur de services.

+client_credentials_problem=access_token est en cours d''extraction pour le client avec les donn\u00e9es d''identification du client suivantes : {0}

+client_credentials_problem.explanation=Une erreur s'est produite lors de l'extraction du jeton d'acc\u00e8s dans le flux client_credentials.

+fetch_init_problem=OAuth2Request est en d''initialisation: {0}

+fetch_init_problem.explanation=Une erreur de bas niveau s'est produite lors de l'initialisation de l'extraction de OAuth2Request.

+fetch_problem=ex\u00e9cution de OAuth2Request.fetch() : {0}

+fetch_problem.explanation=Une erreur s'est produite lors du lancement de l'extraction de OAuth2Request.

+gadget_spec_problem=La sp\u00e9cification de gadget suivante est en cours de traitement : {0}

+gadget_spec_problem.explanation=Aucune sp\u00e9cification de gadget n'a \u00e9t\u00e9 trouv\u00e9e.  Demandez \u00e0 votre administrateur de configurer votre gadget pour l'utiliser avec OAuth2.0.

+get_oauth2_accessor_problem=obtention d''OAuth2Accessor pour OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=Une erreur s'est produite. Demandez \u00e0 votre administrateur de cr\u00e9er un client OAuth2.0 de liaison pour cet gadget et ce service.

+lookup_spec_problem=La sp\u00e9cification du gadget est en cours d''extraction : {0}

+lookup_spec_problem.explanation=Aucune sp\u00e9cification de gadget n'a \u00e9t\u00e9 trouv\u00e9e.  Demandez \u00e0 votre administrateur de configurer votre gadget pour l'utiliser avec OAuth2.0.

+missing_fetch_params=Les param\u00e8tres d''extraction requis suivants sont manquants : {0}

+missing_fetch_params.explanation=La m\u00e9thode fetch() a \u00e9t\u00e9 appel\u00e9e avec des param\u00e8tres incorrects.

+missing_server_response=Une erreur s''est produite pendant la r\u00e9ponse du fournisseur de service OAuth2.0 : {0}

+missing_server_response.explanation=Le serveur a cr\u00e9\u00e9 un OAuth2Request valide, mais n'a pas pu \u00e9mettre la requ\u00eate ni obtenir une r\u00e9ponse valide \u00e0 partir du fournisseur de services.

+no_response_handler=Un gestionnaire de r\u00e9ponses n''a pas trait\u00e9 la r\u00e9ponse suivante : {0}

+no_response_handler.explanation=Le gestionnaire de r\u00e9ponses n'existe pas pour le traitement des noeuds finaux d'autorisation et de jeton AuthorizationEndpointResponseHandler ou TokenEndpointResponseHandler.

+no_gadget_spec=La sp\u00e9cification du gadget pour {0} est introuvable.

+no_gadget_spec.explanation=Aucune sp\u00e9cification de gadget n'a \u00e9t\u00e9 trouv\u00e9e.  Demandez \u00e0 votre administrateur de configurer votre gadget pour l'utiliser avec OAuth2.0.

+refresh_token_problem=Le jeton d''autorisation est \u00e9chang\u00e9 contre le jeton d''acc\u00e8s : {0}

+refresh_token_problem.explanation=Une erreur s'est produite lors de l'\u00e9change du token d'actualisation pour le jeton d'acc\u00e8s.

+secret_encryption_problem=Le secret du jeton est chiffr\u00e9 en code bing pour assurer la persistance : {0}

+secret_encryption_problem.explanation=Une erreur s'est produite lors du stockage des secrets OAuth2.0 avec le module de chiffrement fourni.

+access_denied=L'acc\u00e8s a \u00e9t\u00e9 refus\u00e9.

+access_denied.explanation=L'utilisateur a refus\u00e9 ou a annul\u00e9 l'autorisation aupr\u00e8s du fournisseur de services.

+invalid_client=Le client n''est pas valide : {0}

+invalid_client.explanation= Le client ne peut pas \u00eatre authentifi\u00e9, probablement car le client est inconnu, l'authentification du client n'\u00e9tait pas incluse ou la m\u00e9thode d'authentification n'est pas prise en charge.

+invalid_grant=L''octroi d''autorisation n''est pas valide : {0}

+invalid_grant.explanation=L'octroi d'autorisation n'est pas valide,a expir\u00e9, est annul\u00e9, ne correspond pas \u00e0 l'URI de redirection utilis\u00e9 dans la demande d'autorisation, ou a \u00e9t\u00e9 \u00e9mis pour un autre client. L'octroi d'autorisation peut inclure le code d'autorisation, les donn\u00e9es d'identification du propri\u00e9taire de la ressource ou les donn\u00e9es d'identification du client.

+invalid_request=La requ\u00eate n''est pas valide : {0}

+invalid_request.explanation=La requ\u00eate n'est pas valide, car il manque un param\u00e8tre obligatoire, elle comporte une valeur de param\u00e8tre non prise en charge, elle r\u00e9p\u00e8te un param\u00e8tre, elle comprend plusieurs donn\u00e9es d'identification, elle utilise plus d'un m\u00e9canisme d'authentification du client ou elle n'est pas correctement format\u00e9e.

+invalid_scope=La port\u00e9e n''est pas valide : {0}

+invalid_scope.explanation=La port\u00e9e demand\u00e9e n'est pas valide, est inconnue, mal form\u00e9e ou d\u00e9passe la port\u00e9e accord\u00e9e par le propri\u00e9taire de la ressource.

+server_error=Une erreur de serveur s''est produite: {0}

+server_error.explanation=Le serveur d'autorisation a rencontr\u00e9 une condition inattendue qui l'a emp\u00each\u00e9 de traiter la requ\u00eate.

+server_rejected_request=Le serveur a rejet\u00e9 la requ\u00eate : statuscode = {0}

+server_rejected_request.explanation=Le serveur a cr\u00e9\u00e9 une OAuth2Request, mais le fournisseur de service l'a rejet\u00e9e.

+temporarily_unavailable=Le service est temporairement indisponible : {0}

+temporarily_unavailable.explanation=Le serveur d'autorisation est incapable de g\u00e9rer la requ\u00eate car il est temporairement surcharg\u00e9 ou est en mode maintenance.

+token_response_problem=La r\u00e9ponse provenant du noeud final de jeton est en cours de traitement : {0}

+token_response_problem.explanation=Le noeud final de jeton a envoy\u00e9 une r\u00e9ponse que le serveur n'a pas pu traiter.

+unauthorized_client=Le client n''est pas habilit\u00e9 : {0}

+unauthorized_client.explanation=Le client n'est pas autoris\u00e9 \u00e0 demander un jeton d'acc\u00e8s \u00e0 l'aide de cette m\u00e9thode.

+unsupported_grant_type=Type d''octroi si non pris en charge : {0}

+unsupported_grant_type.explanation=Le serveur d'autorisation ne prend pas en charge l'obtention d'un jeton d'acc\u00e8s \u00e0 l'aide de cette m\u00e9thode.

+unsupported_response_type=Le type de r\u00e9ponse n''est pas pris en charge : {0}

+unsupported_response_type.explanation=Le serveur d'autorisation ne prend pas en charge l'obtention d'un code d'autorisation \u00e0 l'aide de cette m\u00e9thode.

+mac_token_problem=Le jeton mac est demand\u00e9 : {0}

+mac_token_problem.explanation=Le jeton mac sur la demande n'a pu \u00eatre ajout\u00e9 \u00e0 la ressource serveur.

+bearer_token_problem=Le jeton bearer est demand\u00e9 : {0}

+bearer_token_problem.explanation=Le jeton bearer sur la demande n'a pu \u00eatre ajout\u00e9 \u00e0 la ressource serveur.

+authentication_problem=L''authentification du client est ajout\u00e9e : {0}

+authentication_problem.explanation=Les en-t\u00eates d'authentification n'ont pas pu \u00eatre ajout\u00e9es \u00e0 la requ\u00eate.

+unknown_problem=Une erreur inconnue s''est produite : {0}

+unknown_problem.explanation=Demandez \u00e0 votre administrateur syst\u00e8me d'analyser le probl\u00e8me.

+code_grant_problem=Jeton d''acc\u00e8s en cours d''obtention \u00e0 partir du code d''autorisation : {0}

+code_grant_problem.explanation=Une erreur s'est produite lors du flux du code d'autorisation.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_hu.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_hu.properties
new file mode 100644
index 0000000..cd38375
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_hu.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} hib\u00e1t \u00e9szlelt:

+authorization_code_problem=A jogosults\u00e1gi k\u00f3d cser\u00e9je hozz\u00e1f\u00e9r\u00e9si jelsorra: {0}

+authorization_code_problem.explanation=Hiba t\u00f6rt\u00e9nt a jogosults\u00e1gi k\u00f3d cser\u00e9jekor access_token-re.

+authorize_problem={0} jogosults\u00e1gi folyamat\u00e1nak inicializ\u00e1l\u00e1sa.

+authorize_problem.explanation=Hiba t\u00f6rt\u00e9nt a jogosults\u00e1gi folyamat inicializ\u00e1l\u00e1sakor.

+callback_problem=Az \u00e1tir\u00e1ny\u00edt\u00e1si v\u00e1lasz feldolgoz\u00e1sa folyamatban van: {0}

+callback_problem.explanation=Hiba t\u00f6rt\u00e9nt a szolg\u00e1ltat\u00f3 \u00e1tir\u00e1ny\u00edt\u00e1si v\u00e1lasz\u00e1nak feldolgoz\u00e1sakor.

+client_credentials_problem=Az access_token beolvas\u00e1sa folyamatban van a k\u00f6vetkez\u0151 \u00fcgyf\u00e9l-hiteles\u00edt\u00e9ssel rendelkez\u0151 klienshez: {0}

+client_credentials_problem.explanation=Hiba t\u00f6rt\u00e9nt a hozz\u00e1f\u00e9r\u00e9si jelsor beolvas\u00e1sakor a client_credentials folyamban.

+fetch_init_problem=Az OAuth2Request inicializ\u00e1l\u00e1sa folyamatban: {0}

+fetch_init_problem.explanation=Alacsony szint\u0171 hiba t\u00f6rt\u00e9nt az OAuth2Request lek\u00e9r\u00e9s inicializ\u00e1l\u00e1sakor.

+fetch_problem=OAuth2Request.fetch() v\u00e9grehajt\u00e1sa: {0}

+fetch_problem.explanation=Hiba t\u00f6rt\u00e9nt az OAuth2Request lek\u00e9r\u00e9s kiad\u00e1sakor.

+gadget_spec_problem=Az al\u00e1bbi vez\u00e9rl\u0151elem-specifik\u00e1ci\u00f3 feldolgoz\u00e1sa folyamatban: {0}

+gadget_spec_problem.explanation=A vez\u00e9rl\u0151elem-specifik\u00e1ci\u00f3 nem tal\u00e1lhat\u00f3.  K\u00e9rje meg az adminisztr\u00e1tort, hogy \u00e1ll\u00edtsa be a vez\u00e9rl\u0151elemet, hogy m\u0171k\u00f6dj\u00f6n az OAuth2.0 protokollal.

+get_oauth2_accessor_problem=OAuth2Accessor beolvas\u00e1sa OAuth2Request k\u00e9r\u00e9shez: {0}

+get_oauth2_accessor_problem.explanation=Hiba t\u00f6rt\u00e9nt. K\u00e9rje meg az adminisztr\u00e1tort, hogy hozzon l\u00e9tre OAuth2.0 kliens-\u00f6sszerendel\u00e9st ehhez a vez\u00e9rl\u0151elemhez \u00e9s szolg\u00e1ltat\u00e1shoz.

+lookup_spec_problem=A vez\u00e9rl\u0151elem specifik\u00e1ci\u00f3j\u00e1nak beolvas\u00e1sa folyamatban: {0}

+lookup_spec_problem.explanation=A vez\u00e9rl\u0151elem-specifik\u00e1ci\u00f3 nem tal\u00e1lhat\u00f3.  K\u00e9rje meg az adminisztr\u00e1tort, hogy \u00e1ll\u00edtsa be a vez\u00e9rl\u0151elemet, hogy m\u0171k\u00f6dj\u00f6n az OAuth2.0 protokollal.

+missing_fetch_params=A k\u00f6vetkez\u0151 k\u00f6telez\u0151 lek\u00e9r\u00e9si param\u00e9terek hi\u00e1nyoznak: {0}

+missing_fetch_params.explanation=A fetch() met\u00f3dust hib\u00e1s param\u00e9terekkel h\u00edvt\u00e1k meg.

+missing_server_response=Hiba t\u00f6rt\u00e9nt az OAuth2.0 szolg\u00e1ltat\u00f3 v\u00e1lasza sor\u00e1n: {0}

+missing_server_response.explanation=A szerver \u00e9rv\u00e9nyes OAuth2Request k\u00e9r\u00e9st hozott l\u00e9tre, de vagy nem tudta kiadni a k\u00e9r\u00e9st, vagy nem kapott \u00e9rv\u00e9nyes v\u00e1laszt a szolg\u00e1ltat\u00f3t\u00f3l.

+no_response_handler=Egy v\u00e1laszkezel\u0151 nem dolgozta fel a k\u00f6vetkez\u0151 v\u00e1laszt: {0}

+no_response_handler.explanation=Nem l\u00e9tezik v\u00e1laszkezel\u0151 a jogosults\u00e1gi \u00e9s jelsor v\u00e9gpontok feldolgoz\u00e1s\u00e1hoz. AuthorizationEndpointResponseHandler vagy TokenEndpointResponseHandler.

+no_gadget_spec=Nem tal\u00e1lhat\u00f3 vez\u00e9rl\u0151elem-specifik\u00e1ci\u00f3 a k\u00f6vetkez\u0151h\u00f6z: {0}.

+no_gadget_spec.explanation=A vez\u00e9rl\u0151elem-specifik\u00e1ci\u00f3 nem tal\u00e1lhat\u00f3.  K\u00e9rje meg az adminisztr\u00e1tort, hogy \u00e1ll\u00edtsa be a vez\u00e9rl\u0151elemet, hogy m\u0171k\u00f6dj\u00f6n az OAuth2.0 protokollal.

+refresh_token_problem=A friss\u00edt\u00e9si jelsor cser\u00e9je access_token-re: {0}

+refresh_token_problem.explanation=Hiba t\u00f6rt\u00e9nt a friss\u00edt\u00e9si jelsor cser\u00e9jekor jogosults\u00e1gi jelsorra.

+secret_encryption_problem=A titkos jelsor titkos\u00edt\u00e1sa folyamatban van t\u00e1rol\u00e1shoz: {0}

+secret_encryption_problem.explanation=Hiba t\u00f6rt\u00e9nt az OAuth2.0 titkok t\u00e1rol\u00e1sakor a megadott titkos\u00edt\u00e1si modullal.

+access_denied=Hozz\u00e1f\u00e9r\u00e9s megtagadva.

+access_denied.explanation=A felhaszn\u00e1l\u00f3 elutas\u00edtotta vagy visszavonta a felhatalmaz\u00e1st a szolg\u00e1ltat\u00f3val.

+invalid_client=A kliens \u00e9rv\u00e9nytelen: {0}

+invalid_client.explanation= A klienst nem lehet hiteles\u00edteni, feltehet\u0151en az\u00e9rt, mert a kliens nem ismert, a kliens hiteles\u00edt\u00e9se nem volt megadva vagy a hiteles\u00edt\u00e9si m\u00f3dszer nem t\u00e1mogatott.

+invalid_grant=A jogosults\u00e1g megad\u00e1sa \u00e9rv\u00e9nytelen: {0}

+invalid_grant.explanation=A jogosults\u00e1g megad\u00e1sa \u00e9rv\u00e9nytelen, lej\u00e1rt, visszavont\u00e1k, nem egyezik a jogosults\u00e1gi k\u00e9r\u00e9sben haszn\u00e1lt \u00e1tir\u00e1ny\u00edt\u00e1si URI-vel vagy m\u00e1sik kliens sz\u00e1m\u00e1ra adt\u00e1k ki. A jogosults\u00e1g megad\u00e1sa tartalmazhatja a jogosults\u00e1gi k\u00f3dot, az er\u0151forr\u00e1s-tulajdonos hiteles\u00edt\u00e9si adatait vagy a kliens hiteles\u00edt\u00e9si adatait.

+invalid_request=A k\u00e9r\u00e9s \u00e9rv\u00e9nytelen: {0}

+invalid_request.explanation=A k\u00e9r\u00e9s \u00e9rv\u00e9nytelen, mert hi\u00e1nyzik egy k\u00f6telez\u0151 param\u00e9ter, nem t\u00e1mogatott param\u00e9ter\u00e9rt\u00e9ket tartalmaz, ism\u00e9tl\u0151dik egy param\u00e9ter, t\u00f6bb hiteles\u00edt\u00e9si adatot tartalmaz, t\u00f6bb mechanizmust haszn\u00e1l a kliens hiteles\u00edt\u00e9s\u00e9re vagy m\u00e1s okb\u00f3l rossz a form\u00e1tuma.

+invalid_scope=A hat\u00f3k\u00f6r \u00e9rv\u00e9nytelen: {0}

+invalid_scope.explanation=A k\u00e9rt hat\u00f3k\u00f6r \u00e9rv\u00e9nytelen, ismeretlen, hib\u00e1s form\u00e1tum\u00fa vagy t\u00fall\u00e9pi az er\u0151forr\u00e1s-tulajdonos \u00e1ltal megadott hat\u00f3k\u00f6rt.

+server_error=Szerverhiba t\u00f6rt\u00e9nt: {0}

+server_error.explanation=A jogosults\u00e1gi szerver nem v\u00e1rt helyzetbe \u00fctk\u00f6z\u00f6tt, ami megakad\u00e1lyozta a k\u00e9r\u00e9s teljes\u00edt\u00e9s\u00e9t.

+server_rejected_request=A szerver elutas\u00edtotta a k\u00e9r\u00e9st: \u00e1llapotk\u00f3d = {0}

+server_rejected_request.explanation=A szerver l\u00e9trehozott egy OAuth2Request k\u00e9r\u00e9st, de a szolg\u00e1ltat\u00f3 elutas\u00edtotta azt.

+temporarily_unavailable=A szolg\u00e1ltat\u00e1s ideiglenesen nem el\u00e9rhet\u0151: {0}

+temporarily_unavailable.explanation=A jogosults\u00e1gi szerver nem tudja kezelni a k\u00e9r\u00e9st, mert az ideiglenesen t\u00falterhelt vagy karbantart\u00e1si m\u00f3dban van.

+token_response_problem=A jelsor v\u00e9gpont v\u00e1lasza feldolgoz\u00e1s alatt van: {0}

+token_response_problem.explanation=A jelsor v\u00e9gpont v\u00e1laszt k\u00fcld\u00f6tt, amit a szerver nem tudott feldolgozni.

+unauthorized_client=A kliensnek nincs jogosults\u00e1ga: {0}

+unauthorized_client.explanation=A kliens nem jogosult a m\u00f3dszer \u00e1ltal haszn\u00e1lt hozz\u00e1f\u00e9r\u00e9si jelsor k\u00e9r\u00e9s\u00e9re.

+unsupported_grant_type=A megad\u00e1s t\u00edpusa nem t\u00e1mogatott: {0}

+unsupported_grant_type.explanation=A jogosults\u00e1gi szerver nem t\u00e1mogatja hozz\u00e1f\u00e9r\u00e9si jelsor megszerz\u00e9s\u00e9t ezzel a m\u00f3dszerrel.

+unsupported_response_type=A v\u00e1lasz t\u00edpusa nem t\u00e1mogatott: {0}

+unsupported_response_type.explanation=A jogosults\u00e1gi szerver nem t\u00e1mogatja jogosults\u00e1gi k\u00f3d megszerz\u00e9s\u00e9t ezzel a m\u00f3dszerrel.

+mac_token_problem=A mac jelsor k\u00e9r\u00e9se: {0}

+mac_token_problem.explanation=A k\u00e9r\u00e9sben l\u00e9v\u0151 mac jelsort nem lehetett hozz\u00e1adni az er\u0151forr\u00e1sszerverhez.

+bearer_token_problem=A bearer jelsor k\u00e9r\u00e9se: {0}

+bearer_token_problem.explanation=A k\u00e9r\u00e9sben l\u00e9v\u0151 bearer jelsort nem lehetett hozz\u00e1adni az er\u0151forr\u00e1sszerverhez.

+authentication_problem=A klienshiteles\u00edt\u00e9s hozz\u00e1ad\u00e1sa folyamatban: {0}

+authentication_problem.explanation=A hiteles\u00edt\u00e9si fejl\u00e9ceket nem lehetett hozz\u00e1adni a k\u00e9r\u00e9shez.

+unknown_problem=Ismeretlen hiba t\u00f6rt\u00e9nt: {0}

+unknown_problem.explanation=K\u00e9rje meg a rendszergazd\u00e1t, hogy vizsg\u00e1lja ki a probl\u00e9m\u00e1t.

+code_grant_problem=A jogosults\u00e1gi k\u00f3db\u00f3l megszerzett hozz\u00e1f\u00e9r\u00e9si jelsor: {0}

+code_grant_problem.explanation=Hiba t\u00f6rt\u00e9nt a jogosults\u00e1gi k\u00f3dfolyam sor\u00e1n.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_it.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_it.properties
new file mode 100644
index 0000000..5136dc4
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_it.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} ha riportato un errore:

+authorization_code_problem=Il codice di autorizzazione sta per essere scambiato per il token di accesso: {0}

+authorization_code_problem.explanation=Si \u00e8 verificato un errore durante lo scambio del codice di autorizzazione per il token di accesso.

+authorize_problem=Il processo di autorizzazione per {0} sta per iniziare.

+authorize_problem.explanation=Si \u00e8 verificato un errore durante l'inizializzazione del processo di autorizzazione.

+callback_problem=La risposta di reindirizzamento sta per essere elaborata: {0}

+callback_problem.explanation=Si \u00e8 verificato un errore durante l'elaborazione della risposta di reindirizzamento dal provider di servizi.

+client_credentials_problem=Il token di accesso sta per essere richiamato per il client con le seguenti credenziali client: {0}

+client_credentials_problem.explanation=Si \u00e8 verificato un errore durante il richiamo del token di accesso nel flusso delle credenziali del client.

+fetch_init_problem=OAuth2Request in fase di inizializzazione: {0}

+fetch_init_problem.explanation=Si \u00e8 verificato un errore di basso livello durante l'inizializzazione del caricamento di OAuth2Request.

+fetch_problem=Esecuzione di OAuth2Request.fetch() in corso: {0}

+fetch_problem.explanation=Si \u00e8 verificato un errore durante l'emissione del caricamento OAuth2Request.

+gadget_spec_problem=La seguente specifica di gadget sta per essere elaborata: {0}

+gadget_spec_problem.explanation=Non \u00e8 stata trovata una specifica di gadget.  Richiedere all'amministratore di configurare il gadget per poter utilizzare OAuth2.0.

+get_oauth2_accessor_problem=Ottenimento di un OAuth2Accessor per OAuth2Request in corso: {0}

+get_oauth2_accessor_problem.explanation=Si \u00e8 verificato un errore. Richiedere all'amministratore di creare un binding al client OAuth2.0 per questo gadget e il servizio.

+lookup_spec_problem=La specifica di gadget sta per essere richiamata: {0}

+lookup_spec_problem.explanation=Non \u00e8 stata trovata una specifica di gadget.  Richiedere all'amministratore di configurare il gadget per poter utilizzare OAuth2.0.

+missing_fetch_params=I seguenti parametri di caricamento richiesti non sono presenti: {0}

+missing_fetch_params.explanation=Il metodo fetch() \u00e8 stato richiamato con parametri non corretti.

+missing_server_response=Si \u00e8 verificato un errore durante la risposta del provider di servizi di OAuth2.0: {0}

+missing_server_response.explanation=Il server ha creato una OAuth2Request valida, ma non \u00e8 riuscito a emettere la richiesta o a ottenere una risposta valida dal provider di servizi.

+no_response_handler=Un gestore di risposte non ha elaborato la seguente risposta: {0}

+no_response_handler.explanation=Il gestore di risposte non esiste per l'elaborazione degli endpoint di autorizzazione e token. AuthorizationEndpointResponseHandler o TokenEndpointResponseHandler.

+no_gadget_spec=Impossibile trovare la specifica di gadget per {0}.

+no_gadget_spec.explanation=Non \u00e8 stata trovata una specifica di gadget.  Richiedere all'amministratore di configurare il gadget per poter utilizzare OAuth2.0.

+refresh_token_problem=Il token di aggiornamento sta per essere scambiato per il token di accesso: {0}

+refresh_token_problem.explanation=Si \u00e8 verificato un errore durante lo scambio del token di aggiornamento per il token di accesso.

+secret_encryption_problem=Il segreto del token sta per essere crittografato per la persistenza: {0}

+secret_encryption_problem.explanation=Si \u00e8 verificato un errore durante la memorizzazione dei segreti di OAuth2.0 con il modulo di crittografia fornito.

+access_denied=L'accesso \u00e8 stato negato.

+access_denied.explanation=L'utente ha negato o annullato l'autorizzazione con il provider di servizi.

+invalid_client=Il client non \u00e8 valido: {0}

+invalid_client.explanation= Il client non pu\u00f2 essere autenticato, probabilmente perch\u00e9 non \u00e8 noto, perch\u00e9 non \u00e8 stata inclusa l'autenticazione client oppure perch\u00e9 il metodo di autenticazione non \u00e8 supportato.

+invalid_grant=La concessione dell''autorizzazione non \u00e8 valida: {0}

+invalid_grant.explanation=La concessione dell'autorizzazione non \u00e8 valida, \u00e8 scaduta, \u00e8 stata revocata, non corrisponde all'URI di reindirizzamento utilizzato nella richiesta di autorizzazione o \u00e8 stata emessa su un altro client. La concessione dell'autorizzazione pu\u00f2 includere il codice di autorizzazione, le credenziali del proprietario della risorsa o le credenziali del client.

+invalid_request=La richiesta non \u00e8 valida: {0}

+invalid_request.explanation=La richiesta non \u00e8 valida perch\u00e9 non contiene un parametro richiesto, contiene il valore di un parametro non supportato, contiene un parametro ripetuto, include pi\u00f9 credenziali, utilizza pi\u00f9 di un meccanismo per l'autenticazione del client o non \u00e8 strutturata correttamente.

+invalid_scope=L''ambito non \u00e8 valido: {0}

+invalid_scope.explanation=L'ambito richiesto non \u00e8 valido, \u00e8 sconosciuto, non \u00e8 strutturato correttamente o supera l'ambito concesso dal proprietario della risorsa.

+server_error=Si \u00e8 verificato un errore server: {0}

+server_error.explanation=Il server di autorizzazione ha rilevato una condizione non prevista che ha impedito l'evasione della richiesta.

+server_rejected_request=Il server ha rifiutato la richiesta: codice stato = {0}

+server_rejected_request.explanation=Il server ha creato una OAuth2Request, ma il provider di servizi l'ha rifiutata.

+temporarily_unavailable=Il servizio \u00e8 temporaneamente non disponibile: {0}

+temporarily_unavailable.explanation=Il server di autorizzazione non \u00e8 in grado di gestire la richiesta in quanto \u00e8 temporaneamente sovraccarico o si trova in modalit\u00e0 di manutenzione.

+token_response_problem=La risposta dall''endpoint del token sta per essere elaborata: {0}

+token_response_problem.explanation=L'endpoint del token ha inviato una risposta che il server non ha elaborato.

+unauthorized_client=Il client non \u00e8 autorizzato: {0}

+unauthorized_client.explanation=Il client non \u00e8 autorizzato per richiedere un token di accesso con questo metodo.

+unsupported_grant_type=Il tipo di concessione non \u00e8 supportato: {0}

+unsupported_grant_type.explanation=Il server di autorizzazione non supporta l'ottenimento di un token di accesso mediante questo metodo.

+unsupported_response_type=Il tipo di risposta non \u00e8 supportato: {0}

+unsupported_response_type.explanation=Il server di autorizzazione non supporta l'ottenimento di un codice di autorizzazione mediante questo metodo.

+mac_token_problem=Il token mac sta per essere richiesto: {0}

+mac_token_problem.explanation=Il token mac sulla richiesta non \u00e8 stato aggiunto al server di risorse.

+bearer_token_problem=Il token bearer sta per essere richiesto: {0}

+bearer_token_problem.explanation=Il token bearer sulla richiesta non \u00e8 stato aggiunto al server di risorse.

+authentication_problem=L''autenticazione client sta per essere aggiunta: {0}

+authentication_problem.explanation=Le intestazioni di autenticazione non sono state aggiunte alla richiesta.

+unknown_problem=Si \u00e8 verificato un errore sconosciuto: {0}

+unknown_problem.explanation=Richiedere all'amministratore di analizzare il problema.

+code_grant_problem=Il token di accesso sta per essere ottenuto dal codice di autorizzazione: {0}

+code_grant_problem.explanation=Si \u00e8 verificato un errore durante il flusso del codice di autorizzazione.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_iw.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_iw.properties
new file mode 100644
index 0000000..4b55461
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_iw.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} encountered an error :

+authorization_code_problem=The authorization code is being exchanged for the access token : {0}

+authorization_code_problem.explanation=An error occurred when exchanging the authorization code for the access_token.

+authorize_problem=The authorization process for {0} is initiating.

+authorize_problem.explanation=An error occurred when initiating authorization process.

+callback_problem=The redirect response is being processed : {0}

+callback_problem.explanation=An error occurred when processing the redirect response from the service provider.

+client_credentials_problem=The access_token is being retrieved for the client with the following client credential : {0}

+client_credentials_problem.explanation=An error occurred when retrieving access token in the client_credentials flow.

+fetch_init_problem=The OAuth2Request is initializing: {0}

+fetch_init_problem.explanation=A low-level error occurred when initializing the OAuth2Request fetch.

+fetch_problem=executing OAuth2Request.fetch() : {0}

+fetch_problem.explanation=An error occurred when issuing the OAuth2Request fetch.

+gadget_spec_problem=The following gadget specification is processing : {0}

+gadget_spec_problem.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.

+get_oauth2_accessor_problem=getting an OAuth2Accessor for the OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=An error occured. Ask your administrator to create an OAuth2.0 Client binding for this gadget and service.

+lookup_spec_problem=The gadget specification is being retrieved : {0}

+lookup_spec_problem.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.

+missing_fetch_params=The following required fetch parameters are missing : {0}

+missing_fetch_params.explanation=The fetch() method was called with bad parameters.

+missing_server_response=An error occurred during OAuth2.0 service provider response : {0}

+missing_server_response.explanation=The server created a valid OAuth2Request but was either unable to issue the request or get a valid response from the service provider.

+no_response_handler=A response handler did not process the following response : {0}

+no_response_handler.explanation=Response handler do not exists for processing the authorization and token endpoints. AuthorizationEndpointResponseHandler or TokenEndpointResponseHandler.

+no_gadget_spec=The gadget specification for {0} can not be found.

+no_gadget_spec.explanation=A gadget specification was not found.  Ask your administrator to configure your gadget to work with OAuth2.0.

+refresh_token_problem=The refresh token is being exchanged for the access_token : {0}

+refresh_token_problem.explanation=An error occurred when exchanging the refresh token for the access token.

+secret_encryption_problem=The token secret is bing encrypted for persistence : {0}

+secret_encryption_problem.explanation=An error occurred when storing OAuth2.0 secrets with the provided encryption module.

+access_denied=Access was denied.

+access_denied.explanation=The user denied or canceled the authorization with the service provider.

+invalid_client=The client is invalid : {0}

+invalid_client.explanation= The client cannot be authenticated,possibly because hte client is not known., the client authentication was not included, or the authentication method is not suppported.

+invalid_grant=The authorization grant is invalid : {0}

+invalid_grant.explanation=The authorization grant is invalid,expired,revoked, does not match the redirection URI used in the authorization request, or was issued to another client. The authorization grant can include the authorization code, the credentials of the resource owner, or the credentials of the client.

+invalid_request=The request is invalid : {0}

+invalid_request.explanation=The request is invalid because it is missing a required parameter, includes an unsupported parameter value, repeats a parameter, includes multiple credentials, utilizes more than one mechanism for authenticating the client, or is otherwise malformed.

+invalid_scope=The scope is invalid : {0}

+invalid_scope.explanation=The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.

+server_error=A server error occurred: {0}

+server_error.explanation=The authorization server encountered an unexpected condition which prevented it from fulfilling the request.

+server_rejected_request=The server rejected the request : statuscode = {0}

+server_rejected_request.explanation=The server created an OAuth2Request, but the service provider rejected it.

+temporarily_unavailable=The service is temporarily unavailable : {0}

+temporarily_unavailable.explanation=The authorization server is unable to handle the request because it is temporarily overloaded or is in maintenance mode.

+token_response_problem=The response from token endpoint is being processed : {0}

+token_response_problem.explanation=The token endpoint sent a response that the server could not process.

+unauthorized_client=The client is not authorized : {0}

+unauthorized_client.explanation=The client is not authorized to request an access token using this method.

+unsupported_grant_type=The grant type if not supported : {0}

+unsupported_grant_type.explanation=The authorization server does not support obtaining an access token using this method.

+unsupported_response_type=The response type is unsupported: {0}

+unsupported_response_type.explanation=The authorization server does not support obtaining an authorization code using this method.

+mac_token_problem=The mac token is being requested : {0}

+mac_token_problem.explanation=The mac token on the request could not be added to the resource server.

+bearer_token_problem=The bearer token is being requested : {0}

+bearer_token_problem.explanation=The bearer token on the request could not be added to the resource server.

+authentication_problem=The client authentication is being added : {0}

+authentication_problem.explanation=The authentication headers could not be added to the request.

+unknown_problem=An unknown error occurred : {0}

+unknown_problem.explanation=Ask your system administrator to investigate the problem.

+code_grant_problem=The access token being obtained from the authorization code : {0}

+code_grant_problem.explanation=An error occurred during the authorization code flow.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ja.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ja.properties
new file mode 100644
index 0000000..48c9fd0
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ja.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} \u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f :

+authorization_code_problem=\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u7528\u306b\u8a31\u53ef\u30b3\u30fc\u30c9\u3092\u4ea4\u63db\u3057\u3066\u3044\u307e\u3059 : {0}

+authorization_code_problem.explanation=\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u7528\u306b\u8a31\u53ef\u30b3\u30fc\u30c9\u3092\u4ea4\u63db\u3059\u308b\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+authorize_problem={0} \u306e\u8a31\u53ef\u30d7\u30ed\u30bb\u30b9\u3092\u958b\u59cb\u3057\u3066\u3044\u307e\u3059\u3002

+authorize_problem.explanation=\u8a31\u53ef\u30d7\u30ed\u30bb\u30b9\u306e\u958b\u59cb\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+callback_problem=\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u5fdc\u7b54\u3092\u51e6\u7406\u3057\u3066\u3044\u307e\u3059 : {0}

+callback_problem.explanation=\u30b5\u30fc\u30d3\u30b9\u30fb\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc\u304b\u3089\u306e\u30ea\u30c0\u30a4\u30ec\u30af\u30c8\u5fdc\u7b54\u306e\u51e6\u7406\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+client_credentials_problem=\u4ee5\u4e0b\u306e\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8cc7\u683c\u60c5\u5831\u3092\u6301\u3064\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306e\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u53d6\u5f97\u3057\u3066\u3044\u307e\u3059 : {0}

+client_credentials_problem.explanation=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8cc7\u683c\u60c5\u5831\u30d5\u30ed\u30fc\u3067\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u53d6\u5f97\u3059\u308b\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+fetch_init_problem=OAuth2Request \u3092\u521d\u671f\u5316\u3057\u3066\u3044\u307e\u3059: {0}

+fetch_init_problem.explanation=OAuth2Request \u306e\u53d6\u308a\u51fa\u3057\u3092\u521d\u671f\u5316\u3059\u308b\u969b\u306b\u4f4e\u30ec\u30d9\u30eb\u30fb\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+fetch_problem=OAuth2Request.fetch() \u3092\u5b9f\u884c\u3057\u3066\u3044\u307e\u3059 : {0}

+fetch_problem.explanation=OAuth2Request \u306e\u53d6\u308a\u51fa\u3057\u3092\u767a\u884c\u3059\u308b\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+gadget_spec_problem=\u4ee5\u4e0b\u306e\u30ac\u30b8\u30a7\u30c3\u30c8\u4ed5\u69d8\u3092\u51e6\u7406\u3057\u3066\u3044\u307e\u3059 : {0}

+gadget_spec_problem.explanation=\u30ac\u30b8\u30a7\u30c3\u30c8\u4ed5\u69d8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002  OAuth2.0 \u3068\u9023\u52d5\u3059\u308b\u3088\u3046\u306b\u30ac\u30b8\u30a7\u30c3\u30c8\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u3092\u7ba1\u7406\u8005\u306b\u4f9d\u983c\u3057\u3066\u304f\u3060\u3055\u3044\u3002

+get_oauth2_accessor_problem=OAuth2Request \u306e OAuth2Accessor \u3092\u53d6\u5f97\u3057\u3066\u3044\u307e\u3059 : {0}

+get_oauth2_accessor_problem.explanation=\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002 \u3053\u306e\u30ac\u30b8\u30a7\u30c3\u30c8\u3068\u30b5\u30fc\u30d3\u30b9\u306b\u5bfe\u3057\u3066 OAuth2.0 \u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u30fb\u30d0\u30a4\u30f3\u30c7\u30a3\u30f3\u30b0\u3092\u4f5c\u6210\u3059\u308b\u3088\u3046\u306b\u7ba1\u7406\u8005\u306b\u4f9d\u983c\u3057\u3066\u304f\u3060\u3055\u3044\u3002

+lookup_spec_problem=\u30ac\u30b8\u30a7\u30c3\u30c8\u4ed5\u69d8\u3092\u53d6\u5f97\u3057\u3066\u3044\u307e\u3059 : {0}

+lookup_spec_problem.explanation=\u30ac\u30b8\u30a7\u30c3\u30c8\u4ed5\u69d8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002  OAuth2.0 \u3068\u9023\u52d5\u3059\u308b\u3088\u3046\u306b\u30ac\u30b8\u30a7\u30c3\u30c8\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u3092\u7ba1\u7406\u8005\u306b\u4f9d\u983c\u3057\u3066\u304f\u3060\u3055\u3044\u3002

+missing_fetch_params=\u4ee5\u4e0b\u306e\u5fc5\u9808\u306e\u53d6\u308a\u51fa\u3057\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093 : {0}

+missing_fetch_params.explanation=\u8aa4\u3063\u305f\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u304c\u6307\u5b9a\u3055\u308c\u305f fetch() \u30e1\u30bd\u30c3\u30c9\u304c\u547c\u3073\u51fa\u3055\u308c\u307e\u3057\u305f\u3002

+missing_server_response=OAuth2.0 \u30b5\u30fc\u30d3\u30b9\u30fb\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc\u306e\u5fdc\u7b54\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f : {0}

+missing_server_response.explanation=\u30b5\u30fc\u30d0\u30fc\u306f\u3001\u6709\u52b9\u306a OAuth2Request \u3092\u4f5c\u6210\u3057\u307e\u3057\u305f\u304c\u3001\u3053\u306e\u8981\u6c42\u3092\u767a\u884c\u3067\u304d\u306a\u304b\u3063\u305f\u304b\u3001\u30b5\u30fc\u30d3\u30b9\u30fb\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc\u304b\u3089\u6709\u52b9\u306a\u5fdc\u7b54\u3092\u53d6\u5f97\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+no_response_handler=\u5fdc\u7b54\u30cf\u30f3\u30c9\u30e9\u30fc\u306f\u3001\u4ee5\u4e0b\u306e\u5fdc\u7b54\u3092\u51e6\u7406\u3057\u307e\u305b\u3093\u3067\u3057\u305f : {0}

+no_response_handler.explanation=\u8a31\u53ef\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u3068\u30c8\u30fc\u30af\u30f3\u30fb\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u3092\u51e6\u7406\u3059\u308b\u305f\u3081\u306e\u5fdc\u7b54\u30cf\u30f3\u30c9\u30e9\u30fc\u304c\u5b58\u5728\u3057\u307e\u305b\u3093\u3002 AuthorizationEndpointResponseHandler \u307e\u305f\u306f TokenEndpointResponseHandler\u3002

+no_gadget_spec={0} \u306e\u30ac\u30b8\u30a7\u30c3\u30c8\u4ed5\u69d8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3002

+no_gadget_spec.explanation=\u30ac\u30b8\u30a7\u30c3\u30c8\u4ed5\u69d8\u304c\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002  OAuth2.0 \u3068\u9023\u52d5\u3059\u308b\u3088\u3046\u306b\u30ac\u30b8\u30a7\u30c3\u30c8\u3092\u69cb\u6210\u3059\u308b\u3053\u3068\u3092\u7ba1\u7406\u8005\u306b\u4f9d\u983c\u3057\u3066\u304f\u3060\u3055\u3044\u3002

+refresh_token_problem=\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u7528\u306b\u30ea\u30d5\u30ec\u30c3\u30b7\u30e5\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u4ea4\u63db\u3057\u3066\u3044\u307e\u3059 : {0}

+refresh_token_problem.explanation=\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u7528\u306b\u30ea\u30d5\u30ec\u30c3\u30b7\u30e5\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u4ea4\u63db\u3059\u308b\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+secret_encryption_problem=\u30c8\u30fc\u30af\u30f3\u79d8\u5bc6\u60c5\u5831\u304c\u6c38\u7d9a\u5316\u7528\u306b\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u307e\u3059 : {0}

+secret_encryption_problem.explanation=\u6307\u5b9a\u3055\u308c\u305f\u6697\u53f7\u5316\u30e2\u30b8\u30e5\u30fc\u30eb\u3067 OAuth2.0 \u79d8\u5bc6\u60c5\u5831\u3092\u4fdd\u7ba1\u3059\u308b\u969b\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

+access_denied=\u30a2\u30af\u30bb\u30b9\u304c\u62d2\u5426\u3055\u308c\u307e\u3057\u305f\u3002

+access_denied.explanation=\u30e6\u30fc\u30b6\u30fc\u304c\u3001\u30b5\u30fc\u30d3\u30b9\u30fb\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc\u306b\u3088\u308b\u8a31\u53ef\u3092\u62d2\u5426\u3057\u305f\u304b\u3001\u53d6\u308a\u6d88\u3057\u307e\u3057\u305f\u3002

+invalid_client=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u304c\u7121\u52b9\u3067\u3059: {0}

+invalid_client.explanation= \u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8a8d\u8a3c\u3067\u304d\u307e\u305b\u3093\u3002\u539f\u56e0\u3068\u3057\u3066\u3001\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u304c\u4e0d\u660e\u3067\u3042\u308b\u304b\u3001\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a8d\u8a3c\u304c\u542b\u307e\u308c\u3066\u3044\u306a\u3044\u304b\u3001\u8a8d\u8a3c\u65b9\u5f0f\u304c\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u3053\u3068\u304c\u8003\u3048\u3089\u308c\u307e\u3059\u3002

+invalid_grant=\u6a29\u9650\u4ed8\u4e0e\u304c\u7121\u52b9\u3067\u3059 : {0}

+invalid_grant.explanation=\u6a29\u9650\u4ed8\u4e0e\u304c\u3001\u7121\u52b9\u3067\u3042\u308b\u304b\u3001\u6709\u52b9\u671f\u9650\u304c\u5207\u308c\u3066\u3044\u308b\u304b\u3001\u53d6\u308a\u6d88\u3055\u308c\u305f\u304b\u3001\u6a29\u9650\u8981\u6c42\u3067\u4f7f\u7528\u3055\u308c\u308b\u30ea\u30c0\u30a4\u30ec\u30af\u30c8 URI \u306b\u4e00\u81f4\u3057\u306a\u3044\u304b\u3001\u5225\u306e\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u5bfe\u3057\u3066\u767a\u884c\u3055\u308c\u307e\u3057\u305f\u3002 \u6a29\u9650\u4ed8\u4e0e\u306b\u306f\u3001\u8a31\u53ef\u30b3\u30fc\u30c9\u3001\u30ea\u30bd\u30fc\u30b9\u6240\u6709\u8005\u306e\u8cc7\u683c\u60c5\u5831\u3001\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306e\u8cc7\u683c\u60c5\u5831\u3092\u542b\u3081\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002

+invalid_request=\u8981\u6c42\u304c\u7121\u52b9\u3067\u3059: {0}

+invalid_request.explanation=\u8981\u6c42\u304c\u7121\u52b9\u3067\u3059\u3002\u5fc5\u9808\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u306a\u3044\u3001\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u306a\u3044\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u5024\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3001\u30d1\u30e9\u30e1\u30fc\u30bf\u30fc\u304c\u7e70\u308a\u8fd4\u3057\u3066\u6307\u5b9a\u3055\u308c\u3066\u3044\u308b\u3001\u8907\u6570\u306e\u8cc7\u683c\u60c5\u5831\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u3001\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u3092\u8a8d\u8a3c\u3059\u308b\u30e1\u30ab\u30cb\u30ba\u30e0\u304c\u8907\u6570\u4f7f\u7528\u3055\u308c\u3066\u3044\u308b\u3001\u305d\u306e\u4ed6\u306e\u5f62\u5f0f\u306e\u8aa4\u308a\u304c\u3042\u308b\u305f\u3081\u3067\u3059\u3002

+invalid_scope=\u7bc4\u56f2\u304c\u7121\u52b9\u3067\u3059 : {0}

+invalid_scope.explanation=\u8981\u6c42\u3055\u308c\u305f\u7bc4\u56f2\u304c\u7121\u52b9\u3067\u3042\u308b\u304b\u3001\u4e0d\u660e\u3067\u3042\u308b\u304b\u3001\u5f62\u5f0f\u304c\u8aa4\u3063\u3066\u3044\u308b\u304b\u3001\u30ea\u30bd\u30fc\u30b9\u6240\u6709\u8005\u306b\u3088\u3063\u3066\u8a31\u53ef\u3055\u308c\u3066\u3044\u308b\u7bc4\u56f2\u3092\u8d85\u3048\u3066\u3044\u307e\u3059\u3002

+server_error=\u30b5\u30fc\u30d0\u30fc\u30fb\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {0}

+server_error.explanation=\u8a31\u53ef\u30b5\u30fc\u30d0\u30fc\u3067\u4e88\u671f\u3057\u306a\u3044\u6761\u4ef6\u304c\u767a\u751f\u3057\u305f\u305f\u3081\u3001\u8981\u6c42\u3092\u5b9f\u884c\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+server_rejected_request=\u30b5\u30fc\u30d0\u30fc\u306f\u8981\u6c42\u3092\u62d2\u5426\u3057\u307e\u3057\u305f : statuscode = {0}

+server_rejected_request.explanation=\u30b5\u30fc\u30d0\u30fc\u306f OAuth2Request \u3092\u4f5c\u6210\u3057\u307e\u3057\u305f\u304c\u3001\u30b5\u30fc\u30d3\u30b9\u30fb\u30d7\u30ed\u30d0\u30a4\u30c0\u30fc\u304c\u3053\u308c\u3092\u62d2\u5426\u3057\u307e\u3057\u305f\u3002

+temporarily_unavailable=\u30b5\u30fc\u30d3\u30b9\u306f\u4e00\u6642\u7684\u306b\u5229\u7528\u3067\u304d\u307e\u305b\u3093: {0}

+temporarily_unavailable.explanation=\u8a31\u53ef\u30b5\u30fc\u30d0\u30fc\u304c\u4e00\u6642\u7684\u306b\u904e\u8ca0\u8377\u306b\u306a\u3063\u3066\u3044\u308b\u304b\u3001\u4fdd\u5b88\u30e2\u30fc\u30c9\u306b\u306a\u3063\u3066\u3044\u308b\u305f\u3081\u3001\u8981\u6c42\u3092\u51e6\u7406\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u305b\u3093\u3002

+token_response_problem=\u30c8\u30fc\u30af\u30f3\u30fb\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u304b\u3089\u306e\u5fdc\u7b54\u3092\u51e6\u7406\u3057\u3066\u3044\u307e\u3059 : {0}

+token_response_problem.explanation=\u30c8\u30fc\u30af\u30f3\u30fb\u30a8\u30f3\u30c9\u30dd\u30a4\u30f3\u30c8\u304c\u5fdc\u7b54\u3092\u9001\u4fe1\u3057\u307e\u3057\u305f\u304c\u3001\u30b5\u30fc\u30d0\u30fc\u306f\u3053\u306e\u5fdc\u7b54\u3092\u51e6\u7406\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+unauthorized_client=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u6a29\u9650\u304c\u3042\u308a\u307e\u305b\u3093 : {0}

+unauthorized_client.explanation=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u306b\u306f\u3001\u3053\u306e\u65b9\u5f0f\u3092\u4f7f\u7528\u3057\u3066\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u8981\u6c42\u3059\u308b\u6a29\u9650\u304c\u3042\u308a\u307e\u305b\u3093\u3002

+unsupported_grant_type=\u3053\u306e\u6a29\u9650\u4ed8\u4e0e\u30bf\u30a4\u30d7\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093 : {0}

+unsupported_grant_type.explanation=\u8a31\u53ef\u30b5\u30fc\u30d0\u30fc\u306f\u3001\u3053\u306e\u65b9\u5f0f\u3092\u4f7f\u7528\u3057\u305f\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u306e\u53d6\u5f97\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093\u3002

+unsupported_response_type=\u3053\u306e\u5fdc\u7b54\u30bf\u30a4\u30d7\u306f\u30b5\u30dd\u30fc\u30c8\u3055\u308c\u3066\u3044\u307e\u305b\u3093: {0}

+unsupported_response_type.explanation=\u8a31\u53ef\u30b5\u30fc\u30d0\u30fc\u306f\u3001\u3053\u306e\u65b9\u5f0f\u3092\u4f7f\u7528\u3057\u305f\u8a31\u53ef\u30b3\u30fc\u30c9\u306e\u53d6\u5f97\u3092\u30b5\u30dd\u30fc\u30c8\u3057\u3066\u3044\u307e\u305b\u3093\u3002

+mac_token_problem=mac \u30c8\u30fc\u30af\u30f3\u3092\u8981\u6c42\u3057\u3066\u3044\u307e\u3059 : {0}

+mac_token_problem.explanation=\u8981\u6c42\u3055\u308c\u305f mac \u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30bd\u30fc\u30b9\u30fb\u30b5\u30fc\u30d0\u30fc\u306b\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+bearer_token_problem=\u30d9\u30a2\u30e9\u30fc\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u8981\u6c42\u3057\u3066\u3044\u307e\u3059 : {0}

+bearer_token_problem.explanation=\u8981\u6c42\u3055\u308c\u305f\u30d9\u30a2\u30e9\u30fc\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u30ea\u30bd\u30fc\u30b9\u30fb\u30b5\u30fc\u30d0\u30fc\u306b\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+authentication_problem=\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u8a8d\u8a3c\u3092\u8ffd\u52a0\u3057\u3066\u3044\u307e\u3059 : {0}

+authentication_problem.explanation=\u8a8d\u8a3c\u30d8\u30c3\u30c0\u30fc\u3092\u8981\u6c42\u306b\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002

+unknown_problem=\u4e0d\u660e\u306a\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f: {0}

+unknown_problem.explanation=\u554f\u984c\u306e\u8abf\u67fb\u3092\u30b7\u30b9\u30c6\u30e0\u7ba1\u7406\u8005\u306b\u4f9d\u983c\u3057\u3066\u304f\u3060\u3055\u3044\u3002

+code_grant_problem=\u8a31\u53ef\u30b3\u30fc\u30c9\u304b\u3089\u30a2\u30af\u30bb\u30b9\u30fb\u30c8\u30fc\u30af\u30f3\u3092\u53d6\u5f97\u3057\u3066\u3044\u307e\u3059 : {0}

+code_grant_problem.explanation=\u8a31\u53ef\u30b3\u30fc\u30c9\u30fb\u30d5\u30ed\u30fc\u3067\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f\u3002

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_kk.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_kk.properties
new file mode 100644
index 0000000..5aba631
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_kk.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} \u049b\u0430\u0442\u0435\u0433\u0435 \u0436\u043e\u043b \u0431\u0435\u0440\u0434\u0456:

+authorization_code_problem=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u043a\u043e\u0434\u044b \u049b\u0430\u0442\u044b\u043d\u0430\u0441\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b \u04af\u0448\u0456\u043d \u0430\u0443\u044b\u0441\u0442\u044b\u0440\u044b\u043b\u0443\u0434\u0430: {0}

+authorization_code_problem.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u043a\u043e\u0434\u044b\u043d \u049b\u0430\u0442\u044b\u043d\u0430\u0441\u0443_\u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b \u04af\u0448\u0456\u043d \u0430\u0443\u044b\u0441\u0442\u044b\u0440\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+authorize_problem={0} \u04af\u0448\u0456\u043d \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u04af\u0434\u0435\u0440\u0456\u0441\u0456 \u0431\u0430\u0441\u0442\u0430\u043b\u0443\u0434\u0430.

+authorize_problem.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u04af\u0434\u0435\u0440\u0456\u0441\u0456\u043d \u0431\u0430\u0441\u0442\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+callback_problem=\u049a\u0430\u0439\u0442\u0430 \u0431\u0430\u0493\u044b\u0442\u0442\u0430\u0443 \u0436\u0430\u0443\u0430\u0431\u044b \u04e9\u04a3\u0434\u0435\u043b\u0443\u0434\u0435: {0}

+callback_problem.explanation=\u049a\u044b\u0437\u043c\u0435\u0442 \u0436\u0430\u0431\u0434\u044b\u049b\u0442\u0430\u0443\u0448\u044b\u0441\u044b\u043d\u0430\u043d \u049b\u0430\u0439\u0442\u0430 \u0431\u0430\u0493\u044b\u0442\u0442\u0430\u0443 \u0436\u0430\u0443\u0430\u0431\u044b\u043d \u04e9\u04a3\u0434\u0435\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+client_credentials_problem=\u049a\u0430\u0442\u044b\u043d\u0430\u0441\u0443_\u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b \u043a\u043b\u0438\u0435\u043d\u0442 \u04af\u0448\u0456\u043d \u043a\u0435\u043b\u0435\u0441\u0456 \u043a\u043b\u0438\u0435\u043d\u0442\u0442\u0456\u04a3 \u0442\u0456\u0440\u043a\u0435\u043b\u0433\u0456 \u0434\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0456\u043c\u0435\u043d \u0448\u044b\u0493\u0430\u0440\u044b\u043b\u0443\u0434\u0430: {0}

+client_credentials_problem.explanation=\u049a\u0430\u0442\u044b\u043d\u0430\u0441\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d \u043a\u043b\u0438\u0435\u043d\u0442\u0442\u0456\u04a3_\u0442\u0456\u0440\u043a\u0435\u043b\u0433\u0456 \u0434\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0456\u043d\u0456\u04a3 \u0430\u0493\u044b\u043d\u044b\u043d\u0434\u0430 \u0448\u044b\u0493\u0430\u0440\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+fetch_init_problem=OAuth2Request \u0431\u0430\u043f\u0442\u0430\u043d\u0434\u044b\u0440\u044b\u043b\u0443\u0434\u0430: {0}

+fetch_init_problem.explanation=OAuth2Request \u0430\u043b\u044b\u043d\u0443\u044b\u043d \u0431\u0430\u043f\u0442\u0430\u043d\u0434\u044b\u0440\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u0442\u04e9\u043c\u0435\u043d \u0434\u0435\u04a3\u0433\u0435\u0439\u043b\u0456 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+fetch_problem=OAuth2Request.fetch() \u043e\u0440\u044b\u043d\u0434\u0430\u0443: {0}

+fetch_problem.explanation=OAuth2Request \u0430\u043b\u044b\u043d\u0443\u044b\u043d \u0448\u044b\u0493\u0430\u0440\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+gadget_spec_problem=\u041a\u0435\u043b\u0435\u0441\u0456 \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u0441\u0438\u043f\u0430\u0442\u0442\u0430\u043c\u0430\u0441\u044b \u04e9\u04a3\u0434\u0435\u0443\u0434\u0435: {0}

+gadget_spec_problem.explanation=\u0428\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u0441\u0438\u043f\u0430\u0442\u0442\u0430\u043c\u0430\u0441\u044b \u0442\u0430\u0431\u044b\u043b\u043c\u0430\u0434\u044b. \u04d8\u043a\u0456\u043c\u0448\u0456\u0434\u0435\u043d \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u04a3\u044b\u0437\u0434\u044b OAuth2.0 \u0431\u0456\u0440\u0433\u0435 \u0436\u04b1\u043c\u044b\u0441 \u0456\u0441\u0442\u0435\u0443 \u04af\u0448\u0456\u043d \u0442\u0435\u04a3\u0448\u0435\u0443\u0434\u0456 \u0441\u04b1\u0440\u0430\u04a3\u044b\u0437.

+get_oauth2_accessor_problem=OAuth2Request \u04af\u0448\u0456\u043d OAuth2Accessor \u0430\u043b\u0443: {0}

+get_oauth2_accessor_problem.explanation=\u049a\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b. \u04d8\u043a\u0456\u043c\u0448\u0456\u0434\u0435\u043d \u043e\u0441\u044b \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u043c\u0435\u043d \u049b\u044b\u0437\u043c\u0435\u0442 \u04af\u0448\u0456\u043d OAuth2.0 \u043a\u043b\u0438\u0435\u043d\u0442\u0456\u043d\u0456\u04a3 \u0431\u0456\u0440\u0456\u043a\u0442\u0456\u0440\u0443\u0456\u043d \u0436\u0430\u0441\u0430\u0443\u0434\u044b \u0441\u04b1\u0440\u0430\u04a3\u044b\u0437.

+lookup_spec_problem=\u0428\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u043d\u044b\u04a3 \u0441\u0438\u043f\u0430\u0442\u0442\u0430\u043c\u0430\u0441\u044b \u0448\u044b\u0493\u0430\u0440\u044b\u043b\u0443\u0434\u0430: {0}

+lookup_spec_problem.explanation=\u0428\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u0441\u0438\u043f\u0430\u0442\u0442\u0430\u043c\u0430\u0441\u044b \u0442\u0430\u0431\u044b\u043b\u043c\u0430\u0434\u044b. \u04d8\u043a\u0456\u043c\u0448\u0456\u0434\u0435\u043d \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u04a3\u044b\u0437\u0434\u044b OAuth2.0 \u0431\u0456\u0440\u0433\u0435 \u0436\u04b1\u043c\u044b\u0441 \u0456\u0441\u0442\u0435\u0443 \u04af\u0448\u0456\u043d \u0442\u0435\u04a3\u0448\u0435\u0443\u0434\u0456 \u0441\u04b1\u0440\u0430\u04a3\u044b\u0437.

+missing_fetch_params=\u041a\u0435\u043b\u0435\u0441\u0456 \u049b\u0430\u0436\u0435\u0442\u0442\u0456 \u0430\u043b\u0443 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043b\u0435\u0440\u0456 \u0436\u043e\u049b: {0}

+missing_fetch_params.explanation=\u0410\u043b\u0443() \u04d9\u0434\u0456\u0441\u0456 \u043d\u0430\u0448\u0430\u0440 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043b\u0435\u0440\u043c\u0435\u043d \u0448\u0430\u049b\u044b\u0440\u044b\u043b\u0434\u044b.

+missing_server_response=OAuth2.0 \u049b\u044b\u0437\u043c\u0435\u0442 \u0436\u0430\u0431\u0434\u044b\u049b\u0442\u0430\u0443\u0448\u044b\u0441\u044b\u043d\u044b\u04a3 \u0436\u0430\u0443\u0430\u0431\u044b \u0431\u0430\u0440\u044b\u0441\u044b\u043d\u0434\u0430 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {0}

+missing_server_response.explanation=\u0421\u0435\u0440\u0432\u0435\u0440 \u0436\u0430\u0440\u0430\u043c\u0434\u044b OAuth2Request \u0436\u0430\u0441\u0430\u0434\u044b, \u0431\u0456\u0440\u0430\u049b \u0441\u04b1\u0440\u0430\u0443\u0434\u044b \u0448\u044b\u0493\u0430\u0440\u0443\u0493\u0430 \u043d\u0435\u043c\u0435\u0441\u0435 \u049b\u044b\u0437\u043c\u0435\u0442 \u0436\u0430\u0431\u0434\u044b\u049b\u0442\u0430\u0443\u0448\u044b\u0441\u044b\u043d\u0430\u043d \u0436\u0430\u0440\u0430\u043c\u0434\u044b \u0436\u0430\u0443\u0430\u043f \u0430\u043b\u0443\u0493\u0430 \u043c\u04af\u043c\u043a\u0456\u043d \u0431\u043e\u043b\u043c\u0430\u0434\u044b.

+no_response_handler=\u0416\u0430\u0443\u0430\u043f \u04e9\u04a3\u0434\u0435\u0433\u0456\u0448\u0456 \u043a\u0435\u043b\u0435\u0441\u0456 \u0436\u0430\u0443\u0430\u043f\u0442\u044b \u04e9\u04a3\u0434\u0435\u0433\u0435\u043d \u0436\u043e\u049b: {0}

+no_response_handler.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443\u0434\u044b \u0436\u04d9\u043d\u0435 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u0442\u044b\u04a3 \u0441\u043e\u04a3\u0493\u044b \u043d\u04af\u043a\u0442\u0435\u043b\u0435\u0440\u0456\u043d \u04e9\u04a3\u0434\u0435\u0443 \u04af\u0448\u0456\u043d \u0436\u0430\u0443\u0430\u043f \u04e9\u04a3\u0434\u0435\u0433\u0456\u0448\u0456 \u0436\u043e\u049b. AuthorizationEndpointResponseHandler \u043d\u0435\u043c\u0435\u0441\u0435 TokenEndpointResponseHandler.

+no_gadget_spec={0} \u04af\u0448\u0456\u043d \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u0441\u0438\u043f\u0430\u0442\u0442\u0430\u043c\u0430\u0441\u044b\u043d \u0442\u0430\u0431\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441.

+no_gadget_spec.explanation=\u0428\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430 \u0441\u0438\u043f\u0430\u0442\u0442\u0430\u043c\u0430\u0441\u044b \u0442\u0430\u0431\u044b\u043b\u043c\u0430\u0434\u044b. \u04d8\u043a\u0456\u043c\u0448\u0456\u0434\u0435\u043d \u0448\u0430\u0493\u044b\u043d \u0431\u0430\u0493\u0434\u0430\u0440\u043b\u0430\u043c\u0430\u04a3\u044b\u0437\u0434\u044b OAuth2.0 \u0431\u0456\u0440\u0433\u0435 \u0436\u04b1\u043c\u044b\u0441 \u0456\u0441\u0442\u0435\u0443 \u04af\u0448\u0456\u043d \u0442\u0435\u04a3\u0448\u0435\u0443\u0434\u0456 \u0441\u04b1\u0440\u0430\u04a3\u044b\u0437.

+refresh_token_problem=\u0416\u0430\u04a3\u0430\u0440\u0442\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b \u049b\u0430\u0442\u044b\u043d\u0430\u0441\u0443_\u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b \u04af\u0448\u0456\u043d \u0430\u0443\u044b\u0441\u0442\u044b\u0440\u044b\u043b\u0443\u0434\u0430: {0}

+refresh_token_problem.explanation=\u0416\u0430\u04a3\u0430\u0440\u0442\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d \u049b\u0430\u0442\u044b\u043d\u0430\u0441\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d\u0430 \u0430\u0443\u044b\u0441\u0442\u044b\u0440\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+secret_encryption_problem=\u0422\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448 \u049b\u04b1\u043f\u0438\u044f\u0441\u044b\u043d\u044b\u04a3 \u0442\u04b1\u0440\u0430\u049b\u0442\u044b\u043b\u044b\u049b\u0442\u044b \u049b\u0430\u043c\u0442\u0430\u043c\u0430\u0441\u044b\u0437 \u0435\u0442\u0443 \u04af\u0448\u0456\u043d \u0448\u0438\u0444\u0440\u043b\u0430\u043d\u0443\u0434\u0430: {0}

+secret_encryption_problem.explanation=OAuth2.0 \u049b\u04b1\u043f\u0438\u044f\u043b\u0430\u0440\u044b\u043d \u049b\u0430\u043c\u0442\u0430\u043c\u0430\u0441\u044b\u0437 \u0435\u0442\u0456\u043b\u0433\u0435\u043d \u0448\u0438\u0444\u0440\u043b\u0430\u0443 \u043c\u043e\u0434\u0443\u043b\u0456\u043c\u0435\u043d \u0441\u0430\u049b\u0442\u0430\u0443 \u043a\u0435\u0437\u0456\u043d\u0434\u0435 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+access_denied=\u049a\u0430\u0442\u044b\u043d\u0430\u0441\u0442\u0430\u043d \u0431\u0430\u0441 \u0442\u0430\u0440\u0442\u044b\u043b\u0434\u044b.

+access_denied.explanation=\u041f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u0443\u0448\u044b \u049b\u044b\u0437\u043c\u0435\u0442 \u0436\u0430\u0431\u0434\u044b\u049b\u0442\u0430\u0443\u0448\u044b\u0441\u044b\u043c\u0435\u043d \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443\u0434\u0430\u043d \u0431\u0430\u0441 \u0442\u0430\u0440\u0442\u0442\u044b \u043d\u0435\u043c\u0435\u0441\u0435 \u043e\u043d\u044b \u0431\u043e\u043b\u0434\u044b\u0440\u043c\u0430\u0434\u044b.

+invalid_client=\u041a\u043b\u0438\u0435\u043d\u0442 \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437: {0}

+invalid_client.explanation= \u041a\u043b\u0438\u0435\u043d\u0442\u0442\u0456 \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0435\u043c\u0435\u0441, \u0441\u0435\u0431\u0435\u0431\u0456 \u043e\u043d\u044b\u04a3 \u0431\u0435\u043b\u0433\u0456\u0441\u0456\u0437 \u0431\u043e\u043b\u0443\u044b, \u043a\u043b\u0438\u0435\u043d\u0442 \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443\u044b \u049b\u043e\u0441\u044b\u043b\u043c\u0430\u0493\u0430\u043d\u044b \u043d\u0435\u043c\u0435\u0441\u0435 \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u04d9\u0434\u0456\u0441\u0456\u043d\u0435 \u049b\u043e\u043b\u0434\u0430\u0443 \u043a\u04e9\u0440\u0441\u0435\u0442\u0456\u043b\u043c\u0435\u0433\u0435\u043d\u0456 \u0431\u043e\u043b\u0443\u044b \u043c\u04af\u043c\u043a\u0456\u043d.

+invalid_grant=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0440\u04b1\u049b\u0441\u0430\u0442\u044b \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437: {0}

+invalid_grant.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0440\u04b1\u049b\u0441\u0430\u0442\u044b \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437, \u043e\u043d\u044b\u04a3 \u043c\u0435\u0440\u0437\u0456\u043c\u0456 \u04e9\u0442\u043a\u0435\u043d, \u0431\u043e\u043b\u0434\u044b\u0440\u044b\u043b\u043c\u0430\u0493\u0430\u043d, \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0441\u04b1\u0440\u0430\u0443\u044b\u043d\u0434\u0430 \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043b\u0493\u0430\u043d \u049b\u0430\u0439\u0442\u0430 \u0431\u0430\u0493\u044b\u0442\u0442\u0430\u0443 URI \u043c\u0435\u043a\u0435\u043d\u0436\u0430\u0439\u044b\u043d\u0430 \u0441\u04d9\u0439\u043a\u0435\u0441 \u043a\u0435\u043b\u043c\u0435\u0439\u0434\u0456 \u043d\u0435\u043c\u0435\u0441\u0435 \u0431\u0430\u0441\u049b\u0430 \u043a\u043b\u0438\u0435\u043d\u0442\u043a\u0435 \u0431\u0435\u0440\u0456\u043b\u0434\u0456. \u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0440\u04b1\u049b\u0441\u0430\u0442\u044b \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u043a\u043e\u0434\u044b\u043d, \u0440\u0435\u0441\u0443\u0440\u0441 \u0438\u0435\u0441\u0456\u043d\u0456\u04a3 \u0442\u0456\u0440\u043a\u0435\u043b\u0433\u0456 \u0434\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0456\u043d \u043d\u0435\u043c\u0435\u0441\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0442\u0456\u04a3 \u0442\u0456\u0440\u043a\u0435\u043b\u0433\u0456 \u0434\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0456\u043d \u049b\u0430\u043c\u0442\u0443\u044b \u043c\u04af\u043c\u043a\u0456\u043d.

+invalid_request=\u0421\u04b1\u0440\u0430\u0443 \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437: {0}

+invalid_request.explanation=\u0421\u04b1\u0440\u0430\u0443 \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437, \u0441\u0435\u0431\u0435\u0431\u0456 \u043e\u043d\u044b\u04a3 \u049b\u0430\u0436\u0435\u0442\u0442\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456 \u0436\u043e\u049b, \u049b\u043e\u043b\u0434\u0430\u0443 \u043a\u04e9\u0440\u0441\u0435\u0442\u0456\u043b\u043c\u0435\u0439\u0442\u0456\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 \u043c\u04d9\u043d\u0456\u043d \u049b\u0430\u043c\u0442\u0438\u0434\u044b, \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0434\u0456 \u049b\u0430\u0439\u0442\u0430\u043b\u0430\u0439\u0434\u044b, \u0431\u0456\u0440\u043d\u0435\u0448\u0435 \u0442\u0456\u0440\u043a\u0435\u043b\u0433\u0456 \u0434\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0456\u043d \u049b\u0430\u043c\u0442\u0438\u0434\u044b, \u043a\u043b\u0438\u0435\u043d\u0442\u0442\u0456 \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u04af\u0448\u0456\u043d \u0431\u0456\u0440 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u043d\u0435\u043d \u0430\u0440\u0442\u044b\u049b \u049b\u043e\u043b\u0434\u0430\u043d\u0430\u0434\u044b \u043d\u0435\u043c\u0435\u0441\u0435 \u0431\u0430\u0441\u049b\u0430 \u0442\u04af\u0440\u0434\u0435 \u049b\u0430\u0442\u0435 \u0436\u0430\u0441\u0430\u043b\u0493\u0430\u043d.

+invalid_scope=\u041a\u04e9\u043b\u0435\u043c \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437: {0}

+invalid_scope.explanation=\u0421\u04b1\u0440\u0430\u043b\u0493\u0430\u043d \u043a\u04e9\u043b\u0435\u043c \u0436\u0430\u0440\u0430\u043c\u0441\u044b\u0437, \u0431\u0435\u043b\u0433\u0456\u0441\u0456\u0437, \u049b\u0430\u0442\u0435 \u0436\u0430\u0441\u0430\u043b\u0493\u0430\u043d \u043d\u0435\u043c\u0435\u0441\u0435 \u0440\u0435\u0441\u0443\u0440\u0441 \u0438\u0435\u0441\u0456 \u0440\u04b1\u049b\u0441\u0430\u0442 \u0435\u0442\u043a\u0435\u043d \u043a\u04e9\u043b\u0435\u043c\u043d\u0435\u043d \u0430\u0441\u0430\u0434\u044b.

+server_error=\u0421\u0435\u0440\u0432\u0435\u0440 \u049b\u0430\u0442\u0435\u0441\u0456 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {0}

+server_error.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u043d\u0434\u0435 \u0441\u04b1\u0440\u0430\u0443\u0434\u044b \u043e\u0440\u044b\u043d\u0434\u0430\u0443\u0493\u0430 \u0436\u043e\u043b \u0431\u0435\u0440\u043c\u0435\u0439\u0442\u0456\u043d \u043a\u04af\u0442\u043f\u0435\u0433\u0435\u043d \u0436\u0430\u0493\u0434\u0430\u0439 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

+server_rejected_request=\u0421\u0435\u0440\u0432\u0435\u0440 \u0441\u04b1\u0440\u0430\u0443\u0434\u0430\u043d \u0431\u0430\u0441 \u0442\u0430\u0440\u0442\u0442\u044b: \u043a\u04af\u0439 \u043a\u043e\u0434\u044b = {0}

+server_rejected_request.explanation=\u0421\u0435\u0440\u0432\u0435\u0440 OAuth2Request \u0436\u0430\u0441\u0430\u0434\u044b, \u0431\u0456\u0440\u0430\u049b \u049b\u044b\u0437\u043c\u0435\u0442 \u0436\u0430\u0431\u0434\u044b\u049b\u0442\u0430\u0443\u0448\u044b\u0441\u044b \u043e\u0434\u0430\u043d \u0431\u0430\u0441 \u0442\u0430\u0440\u0442\u0442\u044b.

+temporarily_unavailable=\u049a\u044b\u0437\u043c\u0435\u0442 \u0443\u0430\u049b\u044b\u0442\u0448\u0430 \u049b\u043e\u043b\u0436\u0435\u0442\u0456\u043c\u0441\u0456\u0437: {0}

+temporarily_unavailable.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u0443\u0430\u049b\u044b\u0442\u0448\u0430 \u0448\u0430\u043c\u0430\u0434\u0430\u043d \u0442\u044b\u0441 \u0436\u04af\u043a\u0442\u0435\u043b\u0433\u0435\u043d\u0456\u043d\u0435 \u043d\u0435\u043c\u0435\u0441\u0435 \u049b\u044b\u0437\u043c\u0435\u0442 \u043a\u04e9\u0440\u0441\u0435\u0442\u0443 \u0440\u0435\u0436\u0438\u043c\u0456\u043d\u0434\u0435 \u0431\u043e\u043b\u0443\u044b\u043d\u0430 \u0431\u0430\u0439\u043b\u0430\u043d\u044b\u0441\u0442\u044b \u0441\u04b1\u0440\u0430\u0443\u0434\u044b \u04e9\u04a3\u0434\u0435\u0439 \u0430\u043b\u043c\u0430\u0439\u0434\u044b.

+token_response_problem=\u0422\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448 \u0441\u043e\u04a3\u0493\u044b \u043d\u04af\u043a\u0442\u0435\u0441\u0456\u043d\u0456\u04a3 \u0436\u0430\u0443\u0430\u0431\u044b \u04e9\u04a3\u0434\u0435\u043b\u0443\u0434\u0435: {0}

+token_response_problem.explanation=\u0422\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u0442\u044b\u04a3 \u0441\u043e\u04a3\u0493\u044b \u043d\u04af\u043a\u0442\u0435\u0441\u0456 \u0441\u0435\u0440\u0432\u0435\u0440 \u04e9\u04a3\u0434\u0435\u0439 \u0430\u043b\u043c\u0430\u0439\u0442\u044b\u043d \u0441\u04b1\u0440\u0430\u0443\u0434\u044b \u0436\u0456\u0431\u0435\u0440\u0434\u0456.

+unauthorized_client=\u041a\u043b\u0438\u0435\u043d\u0442 \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u044b\u043b\u043c\u0430\u0493\u0430\u043d: {0}

+unauthorized_client.explanation=\u041a\u043b\u0438\u0435\u043d\u0442 \u043e\u0441\u044b \u04d9\u0434\u0456\u0441\u0442\u0456 \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043f \u049b\u0430\u0442\u044b\u043d\u0430\u0441\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d\u0430 \u0441\u04b1\u0440\u0430\u0443 \u0441\u0430\u043b\u0443\u0493\u0430 \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u044b\u043b\u043c\u0430\u0493\u0430\u043d.

+unsupported_grant_type=\u0420\u04b1\u049b\u0441\u0430\u0442 \u0442\u04af\u0440\u0456\u043d\u0435 \u049b\u043e\u043b\u0434\u0430\u0443 \u043a\u04e9\u0440\u0441\u0435\u0442\u0456\u043b\u043c\u0435\u0439\u0434\u0456: {0}

+unsupported_grant_type.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u043e\u0441\u044b \u04d9\u0434\u0456\u0441\u0442\u0456 \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043f \u049b\u0430\u0442\u044b\u043d\u0430\u0441\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d \u0430\u043b\u0443\u0493\u0430 \u049b\u043e\u043b\u0434\u0430\u0443 \u043a\u04e9\u0440\u0441\u0435\u0442\u043f\u0435\u0439\u0434\u0456.

+unsupported_response_type=\u0416\u0430\u0443\u0430\u043f \u0442\u04af\u0440\u0456\u043d\u0435 \u049b\u043e\u043b\u0434\u0430\u0443 \u043a\u04e9\u0440\u0441\u0435\u0442\u0456\u043b\u043c\u0435\u0439\u0434\u0456: {0}

+unsupported_response_type.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u043e\u0441\u044b \u04d9\u0434\u0456\u0441\u0442\u0456 \u043f\u0430\u0439\u0434\u0430\u043b\u0430\u043d\u044b\u043f \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u043a\u043e\u0434\u044b\u043d \u0430\u043b\u0443\u0493\u0430 \u049b\u043e\u043b\u0434\u0430\u0443 \u043a\u04e9\u0440\u0441\u0435\u0442\u043f\u0435\u0439\u0434\u0456.

+mac_token_problem=Mac \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d\u0430 \u0441\u04b1\u0440\u0430\u0443 \u0441\u0430\u043b\u044b\u043d\u0443\u0434\u0430: {0}

+mac_token_problem.explanation=\u0421\u04b1\u0440\u0430\u043b\u0493\u0430\u043d Mac \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d \u0440\u0435\u0441\u0443\u0440\u0441\u0442\u0430\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u043d\u0435 \u049b\u043e\u0441\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0431\u043e\u043b\u043c\u0430\u0434\u044b.

+bearer_token_problem=\u0414\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0434\u0456 \u0442\u0430\u0441\u0443\u0448\u044b\u043d\u044b\u04a3 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d\u0430 \u0441\u04b1\u0440\u0430\u0443 \u0441\u0430\u043b\u044b\u043d\u0443\u0434\u0430: {0}

+bearer_token_problem.explanation=\u0421\u04b1\u0440\u0430\u043b\u0493\u0430\u043d \u0434\u0435\u0440\u0435\u043a\u0442\u0435\u0440\u0434\u0456 \u0442\u0430\u0441\u0443\u0448\u044b\u043d\u044b\u04a3 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b\u043d \u0440\u0435\u0441\u0443\u0440\u0441\u0442\u0430\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u043d\u0435 \u049b\u043e\u0441\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0431\u043e\u043b\u043c\u0430\u0434\u044b.

+authentication_problem=\u041a\u043b\u0438\u0435\u043d\u0442 \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443\u044b \u049b\u043e\u0441\u044b\u043b\u0443\u0434\u0430: {0}

+authentication_problem.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u0442\u0430\u049b\u044b\u0440\u044b\u043f\u0442\u0430\u0440\u044b\u043d \u0441\u04b1\u0440\u0430\u0443\u0493\u0430 \u049b\u043e\u0441\u0443 \u043c\u04af\u043c\u043a\u0456\u043d \u0431\u043e\u043b\u043c\u0430\u0434\u044b.

+unknown_problem=\u0411\u0435\u043b\u0433\u0456\u0441\u0456\u0437 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b: {0}

+unknown_problem.explanation=\u0416\u04af\u0439\u0435 \u04d9\u043a\u0456\u043c\u0448\u0456\u0441\u0456\u043d\u0435\u043d \u043c\u04d9\u0441\u0435\u043b\u0435\u043d\u0456 \u0437\u0435\u0440\u0442\u0442\u0435\u0443\u0434\u0456 \u0441\u04b1\u0440\u0430\u04a3\u044b\u0437.

+code_grant_problem=\u049a\u0430\u0442\u044b\u043d\u0430\u0441\u0443 \u0442\u0430\u04a3\u0431\u0430\u043b\u0430\u0443\u044b\u0448\u044b \u043a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u043a\u043e\u0434\u044b\u043d\u0430\u043d \u0430\u043b\u044b\u043d\u0443\u0434\u0430: {0}

+code_grant_problem.explanation=\u041a\u0443\u04d9\u043b\u0430\u043d\u0434\u044b\u0440\u0443 \u043a\u043e\u0434\u044b\u043d\u044b\u04a3 \u0430\u0493\u044b\u043d\u044b \u0431\u0430\u0440\u044b\u0441\u044b\u043d\u0434\u0430 \u049b\u0430\u0442\u0435 \u043e\u0440\u044b\u043d \u0430\u043b\u0434\u044b.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ko.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ko.properties
new file mode 100644
index 0000000..0b74a55
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ko.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0}\uc5d0\uc11c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+authorization_code_problem=\uc561\uc138\uc2a4 \ud1a0\ud070\uc5d0 \ub300\ud55c \uad8c\ud55c \ucf54\ub4dc\ub97c \uad50\ud658 \uc911\uc785\ub2c8\ub2e4. {0}

+authorization_code_problem.explanation=access_token\uc5d0 \ub300\ud55c \uad8c\ud55c \ucf54\ub4dc\ub97c \uad50\ud658\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+authorize_problem={0}\uc5d0 \ub300\ud55c \uad8c\ud55c \ud504\ub85c\uc138\uc2a4\ub97c \ucd08\uae30\ud654\ud558\ub294 \uc911\uc785\ub2c8\ub2e4.

+authorize_problem.explanation=\uad8c\ud55c \ud504\ub85c\uc138\uc2a4\ub97c \ucd08\uae30\ud654\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+callback_problem=\uc7ac\uc9c0\uc815 \uc751\ub2f5\uc744 \ucc98\ub9ac \uc911\uc785\ub2c8\ub2e4. {0}

+callback_problem.explanation=\uc11c\ube44\uc2a4 \uc81c\uacf5\uc790\uc758 \uc7ac\uc9c0\uc815 \uc751\ub2f5\uc744 \ucc98\ub9ac\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+client_credentials_problem=\ub2e4\uc74c \ud074\ub77c\uc774\uc5b8\ud2b8 \uc2e0\uc784 \uc815\ubcf4\uac00 \uc788\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0 \ub300\ud55c access_token\uc744 \uac80\uc0c9 \uc911\uc785\ub2c8\ub2e4. {0}

+client_credentials_problem.explanation=client_credentials \ud50c\ub85c\uc6b0\uc5d0\uc11c \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uac80\uc0c9 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+fetch_init_problem=OAuth2Request\ub97c \ucd08\uae30\ud654 \uc911\uc785\ub2c8\ub2e4. {0}

+fetch_init_problem.explanation=OAuth2Request \uac00\uc838\uc624\uae30\ub97c \ucd08\uae30\ud654\ud558\ub294 \uc911\uc5d0 \ud558\uc704 \ub808\ubca8 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+fetch_problem=OAuth2Request.fetch() \uc2e4\ud589: {0}

+fetch_problem.explanation=OAuth2Request \uac00\uc838\uc624\uae30 \ubc1c\ud589 \uc2dc \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+gadget_spec_problem=\ub2e4\uc74c \uac00\uc82f \uc2a4\ud399\uc744 \ucc98\ub9ac \uc911\uc785\ub2c8\ub2e4. {0}

+gadget_spec_problem.explanation=\uac00\uc82f \uc2a4\ud399\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad00\ub9ac\uc790\uc5d0\uac8c \uac00\uc82f\uc774 OAuth2.0\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc791\uc5c5\ud560 \uc218 \uc788\uac8c \uad6c\uc131\ud558\ub3c4\ub85d \uc694\uccad\ud558\uc2ed\uc2dc\uc624.

+get_oauth2_accessor_problem=OAuth2Request\uc5d0 \ub300\ud574 OAuth2Accessor\ub97c \uac00\uc838\uc624\ub294 \uc911: {0}

+get_oauth2_accessor_problem.explanation=\uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. \uad00\ub9ac\uc790\uc5d0\uac8c \uc774 \uac00\uc82f \ubc0f \uc11c\ube44\uc2a4\uc5d0 \ub300\ud574 OAuth2.0 \ud074\ub77c\uc774\uc5b8\ud2b8 \ubc14\uc778\ub529\uc744 \uc791\uc131\ud558\ub3c4\ub85d \uc694\uccad\ud558\uc2ed\uc2dc\uc624.

+lookup_spec_problem=\uac00\uc82f \uc2a4\ud399\uc744 \uac80\uc0c9 \uc911\uc785\ub2c8\ub2e4. {0}

+lookup_spec_problem.explanation=\uac00\uc82f \uc2a4\ud399\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad00\ub9ac\uc790\uc5d0\uac8c \uac00\uc82f\uc774 OAuth2.0\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc791\uc5c5\ud560 \uc218 \uc788\uac8c \uad6c\uc131\ud558\ub3c4\ub85d \uc694\uccad\ud558\uc2ed\uc2dc\uc624.

+missing_fetch_params=\ub2e4\uc74c \ud544\uc218 \uac00\uc838\uc624\uae30 \ub9e4\uac1c\ubcc0\uc218\uac00 \ub204\ub77d\ub418\uc5c8\uc2b5\ub2c8\ub2e4. {0}.

+missing_fetch_params.explanation=fetch() \uba54\uc18c\ub4dc\uac00 \uc798\ubabb\ub41c \ub9e4\uac1c\ubcc0\uc218\ub85c \ud638\ucd9c\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+missing_server_response=OAuth2.0 \uc11c\ube44\uc2a4 \uc81c\uacf5\uc790 \uc751\ub2f5 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {0}

+missing_server_response.explanation=\uc11c\ubc84\uac00 \uc720\ud6a8\ud55c OAuth2Request\ub97c \uc791\uc131\ud588\uc9c0\ub9cc \uc694\uccad\uc744 \ubc1c\ud589\ud558\uac70\ub098 \uc11c\ube44\uc2a4 \uc81c\uacf5\uc790\uc5d0\uac8c\uc11c \uc720\ud6a8\ud55c \uc751\ub2f5\uc744 \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+no_response_handler=\uc751\ub2f5 \ud578\ub4e4\ub7ec\uac00 \ub2e4\uc74c \uc751\ub2f5\uc744 \ucc98\ub9ac\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. {0}

+no_response_handler.explanation=\uad8c\ud55c \ubc0f \ud1a0\ud070 \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub97c \ucc98\ub9ac\ud558\uae30 \uc704\ud55c \uc751\ub2f5 \ud578\ub4e4\ub7ec\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. AuthorizationEndpointResponseHandler \ub610\ub294 TokenEndpointResponseHandler\uc785\ub2c8\ub2e4.

+no_gadget_spec={0}\uc5d0 \uac00\uc82f \uc2a4\ud399\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+no_gadget_spec.explanation=\uac00\uc82f \uc2a4\ud399\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uad00\ub9ac\uc790\uc5d0\uac8c \uac00\uc82f\uc774 OAuth2.0\uc744 \uc0ac\uc6a9\ud558\uc5ec \uc791\uc5c5\ud560 \uc218 \uc788\uac8c \uad6c\uc131\ud558\ub3c4\ub85d \uc694\uccad\ud558\uc2ed\uc2dc\uc624.

+refresh_token_problem=access_token\uc5d0 \ub300\ud574 \uc0c8\ub85c \uace0\uce68 \ud1a0\ud070\uc744 \uad50\ud658 \uc911\uc785\ub2c8\ub2e4. {0}

+refresh_token_problem.explanation=\uc561\uc138\uc2a4 \ud1a0\ud070\uc5d0 \ub300\ud55c \uc0c8\ub85c \uace0\uce68 \ud1a0\ud070\uc744 \uad50\ud658\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+secret_encryption_problem=\uc9c0\uc18d\uc131\uc744 \uc704\ud574 \ud1a0\ud070 \ubcf4\uc548\uc744 \uc554\ud638\ud654 \uc911\uc785\ub2c8\ub2e4. {0}

+secret_encryption_problem.explanation=\uc81c\uacf5\ub41c \uc554\ud638\ud654 \ubaa8\ub4c8\ub85c OAuth2.0 \ubcf4\uc548\uc744 \uc800\uc7a5\ud558\ub294 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

+access_denied=\uc561\uc138\uc2a4\uac00 \uac70\ubd80\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+access_denied.explanation=\uc0ac\uc6a9\uc790\uac00 \uc11c\ube44\uc2a4 \uc81c\uacf5\uc790\uc758 \uad8c\ud55c\uc744 \uac70\ubd80\ud558\uac70\ub098 \ucde8\uc18c\ud588\uc2b5\ub2c8\ub2e4.

+invalid_client=\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. {0}

+invalid_client.explanation= \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc54c \uc218 \uc5c6\uace0 \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d\uc774 \ud3ec\ud568\ub418\uc5b4 \uc788\uc9c0 \uc54a\uac70\ub098 \uc778\uc99d \uba54\uc18c\ub4dc\uac00 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uae30 \ub54c\ubb38\uc5d0 \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc778\uc99d\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+invalid_grant=\uad8c\ud55c \ubd80\uc5ec\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. {0}

+invalid_grant.explanation=\uad8c\ud55c \ubd80\uc5ec\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uac70\ub098, \ub9cc\uae30, \ucde8\uc18c\ub418\uac70\ub098 \uad8c\ud55c \uc694\uccad\uc5d0 \uc0ac\uc6a9\ub41c \ub9ac\ub514\ub809\uc158 URI\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uac70\ub098, \ub2e4\ub978 \ud074\ub77c\uc774\uc5b8\ud2b8\ub85c \ubc1c\ud589\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uad8c\ud55c \ubd80\uc5ec\uc5d0 \uad8c\ud55c \ucf54\ub4dc, \uc790\uc6d0 \uc18c\uc720\uc790\uc758 \uc2e0\uc784 \uc815\ubcf4 \ub610\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8\uc758 \uc2e0\uc784 \uc815\ubcf4\uac00 \ud3ec\ud568\ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4.

+invalid_request=\uc694\uccad\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. {0}

+invalid_request.explanation=\ud544\uc218 \ub9e4\uac1c\ubcc0\uc218\uac00 \ub204\ub77d\ub418\uac70\ub098, \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc740 \ub9e4\uac1c\ubcc0\uc218 \uac12\uc774 \ud3ec\ud568\ub418\uac70\ub098, \ub9e4\uac1c\ubcc0\uc218\uac00 \ubc18\ubcf5\ub418\uac70\ub098 \uc5ec\ub7ec \uc2e0\uc784 \uc815\ubcf4\uac00 \ud3ec\ud568\ub418\uac70\ub098, \ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d\uc744 \uc704\ud55c \ub458 \uc774\uc0c1\uc758 \uba54\ucee4\ub2c8\uc998\uc744 \uc0ac\uc6a9\ud558\uac70\ub098, \ud615\uc2dd\uc774 \uc798\ubabb\ub418\uc5c8\uae30 \ub54c\ubb38\uc5d0 \uc694\uccad\uc774 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.

+invalid_scope=\ubc94\uc704\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. {0}

+invalid_scope.explanation=\uc694\uccad\ub41c \ubc94\uc704\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uac70\ub098, \uc54c \uc218 \uc5c6\uac70\ub098, \ud615\uc2dd\uc774 \uc798\ubabb\ub418\uc5c8\uac70\ub098, \uc790\uc6d0 \uc18c\uc720\uc790\uac00 \ubd80\uc5ec\ud55c \ubc94\uc704\ub97c \ucd08\uacfc\ud569\ub2c8\ub2e4.

+server_error=\uc11c\ubc84 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {0}

+server_error.explanation=\uad8c\ud55c \uc11c\ubc84\uc5d0\uc11c \uc694\uccad\uc744 \uc774\ud589\ud560 \uc218 \uc5c6\uac8c \ud558\ub294 \uc608\uae30\uce58 \uc54a\uc740 \uc870\uac74\uc774 \ubc1c\uacac\ub418\uc5c8\uc2b5\ub2c8\ub2e4.

+server_rejected_request=\uc11c\ubc84\ub294 \uc694\uccad\uc744 \uac70\ubd80\ud588\uc2b5\ub2c8\ub2e4. statuscode = {0}

+server_rejected_request.explanation=\uc11c\ubc84\uac00 OAuth2Request\ub97c \uc791\uc131\ud588\uc9c0\ub9cc \uc11c\ube44\uc2a4 \uc81c\uacf5\uc790\uac00 \uc774\ub97c \uac70\ubd80\ud588\uc2b5\ub2c8\ub2e4.

+temporarily_unavailable=\uc11c\ube44\uc2a4\uac00 \uc77c\uc2dc\uc801\uc73c\ub85c \uc0ac\uc6a9 \ubd88\uac00\ub2a5\ud569\ub2c8\ub2e4. {0}

+temporarily_unavailable.explanation=\uad8c\ud55c \ubd80\uc5ec \uc11c\ubc84\uac00 \uc77c\uc2dc\uc801\uc73c\ub85c \uacfc\ubd80\ud558\ub418\uac70\ub098 \uc720\uc9c0\ubcf4\uc218 \ubaa8\ub4dc\uc5d0 \uc788\uae30 \ub54c\ubb38\uc5d0 \uad8c\ud55c \ubd80\uc5ec \uc11c\ubc84\uac00 \uc694\uccad\uc744 \ucc98\ub9ac\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+token_response_problem=\ud1a0\ud070 \uc5d4\ub4dc\ud3ec\uc778\ud2b8\uc5d0\uc11c\uc758 \uc751\ub2f5\uc744 \ucc98\ub9ac \uc911\uc785\ub2c8\ub2e4. {0}

+token_response_problem.explanation=\ud1a0\ud070 \uc5d4\ub4dc\ud3ec\uc778\ud2b8\ub294 \uc11c\ubc84\uac00 \ucc98\ub9ac\ud560 \uc218 \uc5c6\ub294 \uc751\ub2f5\uc744 \ubcf4\ub0c8\uc2b5\ub2c8\ub2e4.

+unauthorized_client=\ud074\ub77c\uc774\uc5b8\ud2b8\uc5d0\uac8c \uad8c\ud55c\uc774 \ubd80\uc5ec\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. {0}

+unauthorized_client.explanation=\ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc774 \uba54\uc18c\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uc694\uccad\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.

+unsupported_grant_type=\uc9c0\uc6d0\ub418\uc9c0 \uc54a\ub294 \uacbd\uc6b0 \ud5c8\uc6a9 \uc720\ud615: {0}

+unsupported_grant_type.explanation=\uad8c\ud55c \ubd80\uc5ec \uc11c\ubc84\ub294 \uc774 \uba54\uc18c\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uac00\uc838\uc624\ub294 \uac83\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.

+unsupported_response_type=\uc751\ub2f5 \uc720\ud615\uc774 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc74c: {0}

+unsupported_response_type.explanation=\uad8c\ud55c \ubd80\uc5ec \uc11c\ubc84\ub294 \uc774 \uba54\uc18c\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc5ec \uad8c\ud55c \ucf54\ub4dc\ub97c \uac00\uc838\uc624\ub294 \uac83\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.

+mac_token_problem=Mac \ud1a0\ud070\uc774 \uc694\uccad\ub428: {0}

+mac_token_problem.explanation=\uc694\uccad \uc2dc mac \ud1a0\ud070\uc744 \uc790\uc6d0 \uc11c\ubc84\uc5d0 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+bearer_token_problem=bearer \ud1a0\ud070\uc774 \uc694\uccad\ub428: {0}

+bearer_token_problem.explanation=\uc694\uccad \uc2dc bearer \ud1a0\ud070\uc744 \uc790\uc6d0 \uc11c\ubc84\uc5d0 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+authentication_problem=\ud074\ub77c\uc774\uc5b8\ud2b8 \uc778\uc99d\uc774 \ucd94\uac00\ub428: {0}

+authentication_problem.explanation=\uc778\uc99d \ud5e4\ub354\ub97c \uc694\uccad\uc5d0 \ucd94\uac00\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.

+unknown_problem=\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4. {0}

+unknown_problem.explanation=\uc2dc\uc2a4\ud15c \uad00\ub9ac\uc790\uc5d0\uac8c \uc774 \ubb38\uc81c\ub97c \uc870\uc0ac\ud558\ub3c4\ub85d \uc694\uccad\ud558\uc2ed\uc2dc\uc624.

+code_grant_problem=\uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \uad8c\ud55c \ucf54\ub4dc\uc5d0\uc11c \uac00\uc838\uc624\ub294 \uc911: {0}

+code_grant_problem.explanation=\uad8c\ud55c \ucf54\ub4dc \ud50c\ub85c\uc6b0 \uc911\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_nl.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_nl.properties
new file mode 100644
index 0000000..3c83802
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_nl.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} heeft een fout aangetroffen:

+authorization_code_problem=De machtigingscode wordt uitgewisseld voor toegangstoken: {0}

+authorization_code_problem.explanation=Een fout is opgetreden tijdens het uitwisselen van de machtigingscode voor access_token.

+authorize_problem=Het machtigingsproces voor {0} wordt gestart.

+authorize_problem.explanation=Er is een fout opgetreden bij het starten van het machtigingsproces.

+callback_problem=De respons voor doorverwijzing wordt verwerkt: {0}

+callback_problem.explanation=Er is een fout opgetreden tijdens het verwerken van de respons voor doorverwijzing van de serviceprovider.

+client_credentials_problem=De access_token wordt opgehaald voor de client met het volgende legitimatiegegeven: {0}

+client_credentials_problem.explanation=Een fout is opgetreden tijdens het ophalen van het toegangstoken in de stroom client_credentials.

+fetch_init_problem=OAuth2Request wordt ge\u00efnitialiseerd: {0}

+fetch_init_problem.explanation=Een low-level fout is opgetreden bij het initialiseren van de OAuth2Request-fetchaanvraag.

+fetch_problem=bezig met uitvoeren van OAuth2Request.fetch() : {0}

+fetch_problem.explanation=Een fout is opgetreden bij het opgeven van de OAuth2Request-fetchaanvraag.

+gadget_spec_problem=De volgende gadgetspecificatie wordt verwerkt: {0}

+gadget_spec_problem.explanation=Een gadgetspecificatie is niet gevonden. Vraag de beheerder om uw gadget te configureren om te werken met OAuth2.0.

+get_oauth2_accessor_problem=bezig met ophalen van een OAuth2Accessor voor de OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=Een fout is opgetreden. Vraag de beheerder om de OAuth2.0 Client-binding te maken voor deze gadget en service.

+lookup_spec_problem=De gadgetspecificatie wordt opgehaald: {0}

+lookup_spec_problem.explanation=Een gadgetspecificatie is niet gevonden. Vraag de beheerder om uw gadget te configureren om te werken met OAuth2.0.

+missing_fetch_params=De volgende verplichte fetchparameters ontbreken: {0}

+missing_fetch_params.explanation=De methode fetch() is aangeroepen met onjuiste parameters.

+missing_server_response=Een fout is opgetreden in de respons van de OAuth2.0-serviceprovider: {0}

+missing_server_response.explanation=De server heeft een geldige OAuth2Request gemaakt maar was niet in staat om de aanvraag te verzenden of om een geldige respons van de serviceprovider te verkrijgen.

+no_response_handler=Een responshandler heeft niet de volgende respons verwerkt: {0}

+no_response_handler.explanation=Responshandler ontbreekt voor het verwerken van de machtigings- en tokeneindpunten. AuthorizationEndpointResponseHandler of TokenEndpointResponseHandler.

+no_gadget_spec=De gadgetspecificatie voor {0} is niet gevonden.

+no_gadget_spec.explanation=Een gadgetspecificatie is niet gevonden. Vraag de beheerder om uw gadget te configureren om te werken met OAuth2.0.

+refresh_token_problem=Het vernieuwingstoken wordt uitgewisseld voor access_token: {0}

+refresh_token_problem.explanation=Een fout is opgetreden tijdens het uitwisselen van het vernieuwingstoken voor het toegangstoken.

+secret_encryption_problem=Het tokengeheim wordt versleuteld voor persistentie: {0}

+secret_encryption_problem.explanation=Een fout is opgetreden bij het opslaan van OAuth2.0-geheimen met de verstrekte versleutelingsmodule.

+access_denied=Toegang is geweigerd.

+access_denied.explanation=De gebruiker heeft de autorisatie voor de serviceprovider geweigerd of geannuleerd.

+invalid_client=De client is ongeldig: {0}

+invalid_client.explanation= De client kan niet worden geverifieerd, mogelijk omdat the client niet bekend is, de clientverificatie is weggelaten of omdat de verificatiemethode niet wordt ondersteund.

+invalid_grant=De verleende machtiging is ongeldig: {0}

+invalid_grant.explanation=De verleende machtiging is ongeldig, vervallen of ingetrokken, of komt niet overeen met de omleidings-URI in de machtigingsaanvraag of is opgegeven voor een andere client. De verleende machtiging kan de machtigingscode of  de legitimatiegegevens van de resource-eigenaar of client bevatten.

+invalid_request=De aanvraag is ongeldig : {0}

+invalid_request.explanation=De aanvraag is ongeldig omdat: een vereiste parameter ontbreekt, een niet-ondersteunde parameterwaarde is opgegeven, een parameter wordt herhaald, meerdere legitimatiegegevens zijn opgegeven, meerdere mechanismen voor verificatie van de client worden gebruikt of omdat de aanvraag op een andere manier onjuist is geformuleerd.

+invalid_scope=Het bereik is niet geldig: {0}

+invalid_scope.explanation=Het aangevraagd bereik is ongeldig, onbekend, onjuist geformuleerd of overschrijdt het bereik dat is verleend door de resource-eigenaar.

+server_error=Er is een serverfout opgetreden: {0}

+server_error.explanation=De machtigingsserver is op een onverwachte situatie gestuit, waardoor de opdracht niet kon worden uitgevoerd.

+server_rejected_request=De server heeft de aanvraag afgewezen: statuscode = {0}

+server_rejected_request.explanation=De server heeft een OAuth2Request gemaakt, maar de serviceprovider heeft deze afgewezen.

+temporarily_unavailable=De service is tijdelijk niet beschikbaar : {0}

+temporarily_unavailable.explanation=De machtigingsserver kan de aanvraag niet verwerken omdat de server tijdelijk overbelast is of omdat deze in de onderhoudswerkstand staat.

+token_response_problem=De respons van het tokeneindpunt wordt verwerkt: {0}

+token_response_problem.explanation=Het tokeneindpunt heeft een respons verzonden die de server niet kan verwerken.

+unauthorized_client=De client is niet gemachtigd : {0}

+unauthorized_client.explanation=De client is niet gemachtigd om met deze methode een toegangstoken aan te vragen.

+unsupported_grant_type=Het machtigingstype indien niet ondersteund: {0}

+unsupported_grant_type.explanation=De machtigingsserver biedt geen ondersteuning voor het verkrijgen van een toegangstoken met deze methode.

+unsupported_response_type=Het responstype wordt niet ondersteund: {0}

+unsupported_response_type.explanation=De machtigingsserver biedt geen ondersteuning voor het verkrijgen van een machtigingscode met deze methode.

+mac_token_problem=Het MAC-token wordt aangevraagd: {0}

+mac_token_problem.explanation=Het MAC-token in de aanvraag kan niet worden toegevoegd aan de resourceserver.

+bearer_token_problem=Het dragertoken wordt aangevraagd: {0}

+bearer_token_problem.explanation=Het dragertoken in de aanvraag kan niet worden toegevoegd aan de resourceserver.

+authentication_problem=De clientverificatie wordt toegevoegd: {0}

+authentication_problem.explanation=De verificatieheaders kunnen niet worden toegevoegd aan de aanvraag.

+unknown_problem=Er is een onvoorziene fout opgetreden: {0}

+unknown_problem.explanation=Vraag de systeembeheerder om het probleem te onderzoeken.

+code_grant_problem=Het toegangstoken dat wordt verkregen van de machtigingscode: {0}

+code_grant_problem.explanation=Een fout is opgetreden in de machtigingscodestroom.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_no.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_no.properties
new file mode 100644
index 0000000..1b9e0aa
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_no.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} oppdaget en feil:

+authorization_code_problem=Autorisasjonskoden blir byttet ut for tilgangstokenet: {0}

+authorization_code_problem.explanation=Det oppstod en feil ved utbytting av autorisasjonskoden for tilgangstokenet.

+authorize_problem=Autorisasjonsprosessen for {0} blir initialisert.

+authorize_problem.explanation=Det oppstod en feil ved initialisering av autorisasjonsprosess.

+callback_problem=Omdirigeringssvaret blir behandlet: {0}

+callback_problem.explanation=Det oppstod en feil ved behandling av omdirigeringssvaret fra tjenesteleverand\u00f8ren.

+client_credentials_problem=Tilgangstokenet blir hentet for klienten med f\u00f8lgende klientlegitimasjon: {0}

+client_credentials_problem.explanation=Det oppstod en feil ved henting av tilgangstoken i dataflyten for klientlegitimasjon (client_credentials).

+fetch_init_problem=OAuth2Request blir initialisert: {0}

+fetch_init_problem.explanation=Det oppstod en lavniv\u00e5feil ved initialisering av OAuth2Request-hentingen.

+fetch_problem=utf\u00f8rer OAuth2Request.fetch() : {0}

+fetch_problem.explanation=Det oppstod en feil ved utf\u00f8ring av OAuth2Request-hentingen.

+gadget_spec_problem=F\u00f8lgende gadgetspesifikasjon blir behandlet: {0}

+gadget_spec_problem.explanation=Det ble ikke funnet noen gadgetspesifikasjon. Be administratoren om \u00e5 konfigurere gadgeten slik at den fungerer med OAuth2.0.

+get_oauth2_accessor_problem=henting av en OAuth2Accessor for OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=Det oppstod en feil. Be administratoren om \u00e5 opprette en OAuth2.0 Client-binding for denne gadgeten og tjenesten.

+lookup_spec_problem=Gadgetspesifikasjonen blir hentet: {0}

+lookup_spec_problem.explanation=Det ble ikke funnet noen gadgetspesifikasjon. Be administratoren om \u00e5 konfigurere gadgeten slik at den fungerer med OAuth2.0.

+missing_fetch_params=F\u00f8lgende obligatoriske fetch-parametere mangler: {0}

+missing_fetch_params.explanation=fetch()-metoden ble kalt med ugyldige parametere.

+missing_server_response=Det oppstod en feil under svaret fra OAuth2.0-tjenesteleverand\u00f8ren: {0}

+missing_server_response.explanation=Serveren opprettet en gyldig OAuth2Request, men kunne ikke sende foresp\u00f8rselen eller f\u00e5 et gyldig svar fra tjenesteleverand\u00f8ren.

+no_response_handler=En svarbehandler behandlet ikke f\u00f8lgende svar: {0}

+no_response_handler.explanation=Det finnes ingen svarbehandler for behandling av autorisasjons- og tokensluttpunkter. AuthorizationEndpointResponseHandler eller TokenEndpointResponseHandler.

+no_gadget_spec=Gadgetspesifikasjon for {0} ble ikke funnet.

+no_gadget_spec.explanation=Det ble ikke funnet noen gadgetspesifikasjon. Be administratoren om \u00e5 konfigurere gadgeten slik at den fungerer med OAuth2.0.

+refresh_token_problem=Oppdateringstokenet blir byttet ut for tilgangstokenet: {0}

+refresh_token_problem.explanation=Det oppstod en feil ved utbytting av oppdateringstokenet for tilgangstokenet.

+secret_encryption_problem=Tokenhemmeligheten blir kryptert for persistens: {0}

+secret_encryption_problem.explanation=Det oppstod en feil ved lagring av OAuth2.0-hemmeligheter med den oppgitte krypteringsmodulen.

+access_denied=Tilgang ble nektet.

+access_denied.explanation=Brukeren ble nektet eller avbr\u00f8t autorisasjonen hos tjenesteleverand\u00f8ren.

+invalid_client=Klienten er ugyldig: {0}

+invalid_client.explanation= Klienten kan ikke autentiseres, kanskje fordi klienten ikke er kjent. Klientautentiseringen ble ikke inkludert, eller autentiseringsmetoden st\u00f8ttes ikke.

+invalid_grant=Autorisasjonstildelingen er ugyldig: {0}

+invalid_grant.explanation=Autorisasjonstildelingen er ugyldig, utl\u00f8pt, inndratt, samsvarer ikke med omdirigerings-URIen som er brukt i autorisasjonsforesp\u00f8rselen, eller ble gitt til en annen klient. Autorisasjonstildelingen kan inkludere autorisasjonskoden, legitimasjonen for ressurseieren eller legitimasjonen for klienten.

+invalid_request=Foresp\u00f8rselen er ugyldig: {0}

+invalid_request.explanation=Foresp\u00f8rselen er ugyldig fordi den mangler en obligatorisk parameter, inneholder en parameterverdi som ikke st\u00f8ttes, gjentar en parameter, inkluderer flere legitimasjoner, bruker mer enn en mekanisme for \u00e5 autentisere klienten, eller har feil format av en annen grunn.

+invalid_scope=Omfanget er ugyldig: {0}

+invalid_scope.explanation=Det forespurte omfanget er ugyldig, ukjent, har feil format eller overskrider omfanget som er tildelt av ressurseieren.

+server_error=Det oppstod en serverfeil: {0}

+server_error.explanation=Autorisasjonsserveren oppdaget en uventet tilstand som hindret den i \u00e5 etterkomme foresp\u00f8rselen.

+server_rejected_request=Serveren avviste foresp\u00f8rselen: statuskode = {0}

+server_rejected_request.explanation=Serveren opprettet en OAuth2Request, men tjenesteleverand\u00f8ren avviste den.

+temporarily_unavailable=Tjenesten er ikke tilgjengelig n\u00e5: {0}

+temporarily_unavailable.explanation=Autorisasjonsserveren kan ikke behandle foresp\u00f8rselen fordi den er midlertidig overbelastet eller er i vedlikeholdsmodus.

+token_response_problem=Svaret fra tokensluttpunktet blir behandlet: {0}

+token_response_problem.explanation=Tokensluttpunktet sendte et svar som serveren ikke kunne behandle.

+unauthorized_client=Klienten er ikke autorisert: {0}

+unauthorized_client.explanation=Klienten er ikke autorisert for \u00e5 be om et tilgangstoken ved hjelp av denne metoden.

+unsupported_grant_type=Tildelingstypen hvis ikke st\u00f8ttet: {0}

+unsupported_grant_type.explanation=Autorisasjonsserveren st\u00f8tter ikke henting av et tilgangstoken ved hjelp av denne metoden.

+unsupported_response_type=Svartypen st\u00f8ttes ikke: {0}

+unsupported_response_type.explanation=Autorisasjonsserveren st\u00f8tter ikke henting av en autorisasjonskode ved hjelp av denne metoden.

+mac_token_problem=Det blir bedt om mac-tokenet: {0}

+mac_token_problem.explanation=mac-tokenet p\u00e5 foresp\u00f8rselen kunne ikke legges til p\u00e5 ressursserveren.

+bearer_token_problem=Det blir bedt om bearer-tokenet: {0}

+bearer_token_problem.explanation=bearer-tokenet p\u00e5 foresp\u00f8rselen kunne ikke legges til p\u00e5 ressursserveren.

+authentication_problem=Klientautentiseringen blir lagt til: {0}

+authentication_problem.explanation=Autentiseringstopptekstene kunne ikke legges til i foresp\u00f8rselen.

+unknown_problem=Det oppstod en ukjent feil: {0}

+unknown_problem.explanation=Be systemadministratoren om \u00e5 unders\u00f8ke problemet.

+code_grant_problem=Tilgangstokenet blir hentet fra autorisasjonskoden: {0}

+code_grant_problem.explanation=Det oppstod en feil i autorisasjonskodeflyten.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pl.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pl.properties
new file mode 100644
index 0000000..677d098
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pl.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= Element {0} napotka\u0142 b\u0142\u0105d:

+authorization_code_problem=Kod autoryzacji jest wymieniany na znacznik dost\u0119pu: {0}

+authorization_code_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas wymiany kodu autoryzacji na znacznik dost\u0119pu.

+authorize_problem=Inicjowanie procesu autoryzacji dla elementu {0}.

+authorize_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas inicjowania procesu autoryzacji.

+callback_problem=Przetwarzanie odpowiedzi przekierowania: {0}

+callback_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas przetwarzania odpowiedzi przekierowania od dostawcy us\u0142ug.

+client_credentials_problem=Pobieranie znacznika dost\u0119pu dla klienta o nast\u0119puj\u0105cych referencjach: {0}

+client_credentials_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas pobierania znacznika dost\u0119pu w przep\u0142ywie client_credentials.

+fetch_init_problem=Inicjowanie \u017c\u0105dania OAuth2Request: {0}

+fetch_init_problem.explanation=Wyst\u0105pi\u0142 niskopoziomowy b\u0142\u0105d podczas inicjowania pobierania \u017c\u0105dania OAuth2Request.

+fetch_problem=Wykonywanie metody OAuth2Request.fetch(): {0}

+fetch_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas wywo\u0142ywania pobierania \u017c\u0105dania OAuth2Request.

+gadget_spec_problem=Przetwarzanie nast\u0119puj\u0105cej specyfikacji gad\u017cetu: {0}

+gadget_spec_problem.explanation=Nie znaleziono specyfikacji gad\u017cetu. Popro\u015b administratora o skonfigurowanie gad\u017cetu do pracy z protoko\u0142em OAuth2.0.

+get_oauth2_accessor_problem=Pobieranie obiektu korzystaj\u0105cego OAuth2Accessor dla \u017c\u0105dania OAuth2Request: {0}

+get_oauth2_accessor_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d. Popro\u015b administratora o utworzenie powi\u0105zania klienta OAuth2.0 dla tego gad\u017cetu i us\u0142ugi.

+lookup_spec_problem=Pobieranie specyfikacji gad\u017cetu: {0}

+lookup_spec_problem.explanation=Nie znaleziono specyfikacji gad\u017cetu. Popro\u015b administratora o skonfigurowanie gad\u017cetu do pracy z protoko\u0142em OAuth2.0.

+missing_fetch_params=Brak nast\u0119puj\u0105cych wymaganych parametr\u00f3w pobierania: {0}.

+missing_fetch_params.explanation=Metoda fetch() zosta\u0142a wywo\u0142ana z niepoprawnymi parametrami.

+missing_server_response=Wyst\u0105pi\u0142 b\u0142\u0105d podczas uzyskiwania odpowiedzi dostawcy us\u0142ug OAuth2.0: {0}

+missing_server_response.explanation=Serwer utworzy\u0142 poprawne \u017c\u0105danie OAuth2Request, ale nie m\u00f3g\u0142 wys\u0142a\u0107 \u017c\u0105dania lub pobra\u0107 poprawnej odpowiedzi od dostawcy us\u0142ug.

+no_response_handler=Procedura obs\u0142ugi odpowiedzi nie przetworzy\u0142a nast\u0119puj\u0105cej odpowiedzi: {0}

+no_response_handler.explanation=Nie istnieje procedura obs\u0142ugi odpowiedzi na potrzeby przetwarzania punkt\u00f3w ko\u0144cowych znacznika i autoryzacji. Wymagana jest procedura AuthorizationEndpointResponseHandler lub TokenEndpointResponseHandler.

+no_gadget_spec=Nie znaleziono specyfikacji gad\u017cetu {0}.

+no_gadget_spec.explanation=Nie znaleziono specyfikacji gad\u017cetu. Popro\u015b administratora o skonfigurowanie gad\u017cetu do pracy z protoko\u0142em OAuth2.0.

+refresh_token_problem=Wymiana znacznika od\u015bwie\u017cania na znacznik dost\u0119pu: {0}

+refresh_token_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas wymiany znacznika od\u015bwie\u017cania na znacznik dost\u0119pu.

+secret_encryption_problem=Szyfrowanie klucza tajnego znacznika na potrzeby utrwalania: {0}

+secret_encryption_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas zapisywania kluczy tajnych OAuth2.0 przy u\u017cyciu podanego modu\u0142u szyfrowania.

+access_denied=Odmowa dost\u0119pu.

+access_denied.explanation=Odmowa autoryzacji u\u017cytkownika w dostawcy us\u0142ug lub autoryzacja zosta\u0142a anulowana.

+invalid_client=Klient jest niepoprawny: {0}

+invalid_client.explanation= Klient nie mo\u017ce zosta\u0107 uwierzytelniony, prawdopodobnie poniewa\u017c klient nie jest znany, uwierzytelnianie klienta nie zosta\u0142o w\u0142\u0105czone lub metoda uwierzytelniania nie jest obs\u0142ugiwana.

+invalid_grant=Nadanie autoryzacji jest niepoprawne: {0}

+invalid_grant.explanation=Nadanie autoryzacji jest niepoprawne, utraci\u0142o wa\u017cno\u015b\u0107, zosta\u0142o odwo\u0142ane, nie jest zgodne z identyfikatorem URI przekierowania u\u017cywanym w \u017c\u0105daniu autoryzacji lub zosta\u0142o wydane dla innego klienta. Nadanie autoryzacji mo\u017ce zawiera\u0107 kod autoryzacji, referencje w\u0142a\u015bciciela zasobu lub referencje klienta.

+invalid_request=\u017b\u0105danie jest niepoprawne: {0}

+invalid_request.explanation=\u017b\u0105danie jest niepoprawne, poniewa\u017c brakuje w nim wymaganego parametru, zawiera nieobs\u0142ugiwan\u0105 warto\u015b\u0107 parametru, zawiera duplikat parametru, obejmuje wiele referencji, wykorzystuje wi\u0119cej ni\u017c jeden mechanizm uwierzytelniania klienta lub jest w inny spos\u00f3b zniekszta\u0142cone.

+invalid_scope=Zasi\u0119g jest niepoprawny: {0}

+invalid_scope.explanation=\u017b\u0105dany zasi\u0119g jest niepoprawny, nieznany lub zniekszta\u0142cony albo przekracza zasi\u0119g nadany przez w\u0142a\u015bciciela zasobu.

+server_error=Wyst\u0105pi\u0142 b\u0142\u0105d serwera: {0}

+server_error.explanation=Serwer autoryzacji napotka\u0142 nieoczekiwany warunek, co uniemo\u017cliwi\u0142o spe\u0142nienie \u017c\u0105dania.

+server_rejected_request=Serwer odrzuci\u0142 \u017c\u0105danie. Kod statusu: {0}

+server_rejected_request.explanation=Serwer utworzy\u0142 \u017c\u0105danie OAuth2Request, ale zosta\u0142o ono odrzucone przez dostawc\u0119 us\u0142ug.

+temporarily_unavailable=Us\u0142uga jest tymczasowo niedost\u0119pna: {0}

+temporarily_unavailable.explanation=Serwer autoryzacji nie mo\u017ce obs\u0142u\u017cy\u0107 \u017c\u0105dania, poniewa\u017c jest on tymczasowo przeci\u0105\u017cony lub znajduje si\u0119 w trybie konserwacji.

+token_response_problem=Przetwarzanie odpowiedzi punktu ko\u0144cowego znacznika: {0}

+token_response_problem.explanation=Punkt ko\u0144cowy znacznika wys\u0142a\u0142 odpowied\u017a, kt\u00f3ra nie mog\u0142a zosta\u0107 przetworzona przez serwer.

+unauthorized_client=Klient nie ma uprawnie\u0144: {0}

+unauthorized_client.explanation=Klient nie ma uprawnie\u0144 do \u017c\u0105dania znacznika dost\u0119pu przy u\u017cyciu tej metody.

+unsupported_grant_type=Typ nadania nie jest obs\u0142ugiwany: {0}

+unsupported_grant_type.explanation=Serwer autoryzacji nie obs\u0142uguje uzyskiwania znacznika dost\u0119pu za pomoc\u0105 tej metody.

+unsupported_response_type=Typ odpowiedzi nie jest obs\u0142ugiwany: {0}

+unsupported_response_type.explanation=Serwer autoryzacji nie obs\u0142uguje uzyskiwania kodu autoryzacji za pomoc\u0105 tej metody.

+mac_token_problem=Za\u017c\u0105dano znacznika kodu MAC (Message Authentication Code): {0}

+mac_token_problem.explanation=Znacznika kodu MAC w \u017c\u0105daniu nie mo\u017cna doda\u0107 do serwera zasob\u00f3w.

+bearer_token_problem=Za\u017c\u0105dano znacznika multimedi\u00f3w: {0}

+bearer_token_problem.explanation=Znacznika multimedi\u00f3w w \u017c\u0105daniu nie mo\u017cna doda\u0107 do serwera zasob\u00f3w.

+authentication_problem=Dodawanie uwierzytelniania klienta: {0}

+authentication_problem.explanation=Do \u017c\u0105dania nie mo\u017cna doda\u0107 nag\u0142\u00f3wk\u00f3w uwierzytelniania.

+unknown_problem=Wyst\u0105pi\u0142 nieznany b\u0142\u0105d: {0}

+unknown_problem.explanation=Popro\u015b administratora systemu o przeanalizowanie problemu.

+code_grant_problem=Znacznik dost\u0119pu uzyskiwany z kodu autoryzacji: {0}

+code_grant_problem.explanation=Wyst\u0105pi\u0142 b\u0142\u0105d podczas przep\u0142ywu kodu autoryzacji.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pt.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pt.properties
new file mode 100644
index 0000000..c0d6705
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pt.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} detectou um erro:

+authorization_code_problem=O c\u00f3digo de autoriza\u00e7\u00e3o est\u00e1 a ser permutado para o s\u00edmbolo de acesso: {0}

+authorization_code_problem.explanation=Ocorreu um erro ao permutar o c\u00f3digo de autoriza\u00e7\u00e3o para access_token.

+authorize_problem=O processo de autoriza\u00e7\u00e3o para {0} est\u00e1 a iniciar.

+authorize_problem.explanation=Ocorreu um erro ao iniciar o processo de autoriza\u00e7\u00e3o.

+callback_problem=A resposta de redireccionamento est\u00e1 a ser processada: {0}

+callback_problem.explanation=Ocorreu um erro ao processar a resposta de redireccionamento a partir do fornecedor de servi\u00e7os.

+client_credentials_problem=O access_token est\u00e1 a ser obtido a partir do cliente com as seguintes credenciais de cliente: {0}

+client_credentials_problem.explanation=Ocorreu um erro ao obter o s\u00edmbolo de acesso no fluxo client_credentials.

+fetch_init_problem=O OAuth2Request est\u00e1 a inicializar: {0}

+fetch_init_problem.explanation=Ocorreu um erro de n\u00edvel inferior ao inicializar a obten\u00e7\u00e3o de OAuth2Request.

+fetch_problem=a executar OAuth2Request.fetch() : {0}

+fetch_problem.explanation=Ocorreu um erro ao emitir a obten\u00e7\u00e3o de OAuth2Request.

+gadget_spec_problem=A seguinte especifica\u00e7\u00e3o de gadget est\u00e1 a ser processada: {0}

+gadget_spec_problem.explanation=N\u00e3o foi localizada uma especifica\u00e7\u00e3o do gadget. Solicite ao administrador que configure o gadget para funcionar com o OAuth2.0.

+get_oauth2_accessor_problem=a obter um OAuth2Accessor para o OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=Ocorreu um erro. Solicite ao administrador que crie uma associa\u00e7\u00e3o de cliente do OAuth2.0 para este gadget e servi\u00e7o.

+lookup_spec_problem=A especifica\u00e7\u00e3o do gadget est\u00e1 a ser obtida: {0}

+lookup_spec_problem.explanation=N\u00e3o foi localizada uma especifica\u00e7\u00e3o do gadget. Solicite ao administrador que configure o gadget para funcionar com o OAuth2.0.

+missing_fetch_params=Os seguintes par\u00e2metros de obten\u00e7\u00e3o requeridos est\u00e3o em falta: {0}

+missing_fetch_params.explanation=O m\u00e9todo fetch() foi invocado com par\u00e2metros incorrectos.

+missing_server_response=Ocorreu um erro durante a resposta do fornecedor de servi\u00e7os do OAuth2.0: {0}

+missing_server_response.explanation=O servidor criou um OAuth2Request v\u00e1lido, mas n\u00e3o conseguiu emitir o pedido ou obter uma resposta v\u00e1lida a partir do fornecedor de servi\u00e7os.

+no_response_handler=Uma rotina de tratamento de respostas n\u00e3o processou a seguinte resposta: {0}

+no_response_handler.explanation=A rotina de tratamento de respostas n\u00e3o existe para processar a autoriza\u00e7\u00e3o e os pontos finais de s\u00edmbolos. AuthorizationEndpointResponseHandler ou TokenEndpointResponseHandler.

+no_gadget_spec=N\u00e3o \u00e9 poss\u00edvel localizar a especifica\u00e7\u00e3o do gadget para {0}.

+no_gadget_spec.explanation=N\u00e3o foi localizada uma especifica\u00e7\u00e3o do gadget. Solicite ao administrador que configure o gadget para funcionar com o OAuth2.0.

+refresh_token_problem=O s\u00edmbolo de actualiza\u00e7\u00e3o est\u00e1 a ser permutado para access_token: {0}

+refresh_token_problem.explanation=Ocorreu um erro ao permutar o s\u00edmbolo de actualiza\u00e7\u00e3o para o s\u00edmbolo de acesso.

+secret_encryption_problem=O c\u00f3digo do s\u00edmbolo est\u00e1 a ser encriptado para persist\u00eancia: {0}

+secret_encryption_problem.explanation=Ocorreu um erro ao armazenar os c\u00f3digos do OAuth2.0 com o m\u00f3dulo de encripta\u00e7\u00e3o fornecido.

+access_denied=O acesso foi recusado.

+access_denied.explanation=O utilizador recusou ou cancelou a autoriza\u00e7\u00e3o com o fornecedor de servi\u00e7os.

+invalid_client=O cliente n\u00e3o \u00e9 v\u00e1lido: {0}

+invalid_client.explanation= N\u00e3o \u00e9 poss\u00edvel autenticar o cliente, possivelmente devido ao facto de o cliente ser desconhecido. A autentica\u00e7\u00e3o do cliente n\u00e3o foi inclu\u00edda ou o m\u00e9todo de autentica\u00e7\u00e3o n\u00e3o \u00e9 suportado.

+invalid_grant=A concess\u00e3o de autoriza\u00e7\u00e3o n\u00e3o \u00e9 v\u00e1lida: {0}

+invalid_grant.explanation=A concess\u00e3o de autoriza\u00e7\u00e3o n\u00e3o \u00e9 v\u00e1lida, expirou, foi revogada, n\u00e3o corresponde ao URI de redireccionamento utilizado no pedido de autoriza\u00e7\u00e3o ou foi emitida por outro cliente. A concess\u00e3o de autoriza\u00e7\u00e3o pode incluir o c\u00f3digo de autoriza\u00e7\u00e3o, as credenciais do propriet\u00e1rio do recurso ou as credenciais do cliente.

+invalid_request=O pedido n\u00e3o \u00e9 v\u00e1lido: {0}

+invalid_request.explanation=O pedido n\u00e3o \u00e9 v\u00e1lido, uma vez que n\u00e3o cont\u00e9m um par\u00e2metro requerido, inclui um valor de par\u00e2metro n\u00e3o suportado, repete um par\u00e2metro, inclui v\u00e1rias credenciais, utiliza mais do que um mecanismo para autentica\u00e7\u00e3o do cliente ou est\u00e1 incorrecto.

+invalid_scope=O \u00e2mbito n\u00e3o \u00e9 v\u00e1lido: {0}

+invalid_scope.explanation=O \u00e2mbito solicitado n\u00e3o \u00e9 v\u00e1lido, \u00e9 desconhecido, est\u00e1 incorrecto ou excede o \u00e2mbito concedido pelo propriet\u00e1rio do recurso.

+server_error=Ocorreu um erro de servidor: {0}

+server_error.explanation=O servidor de autoriza\u00e7\u00f5es detectou uma condi\u00e7\u00e3o inesperada que impediu o mesmo de concluir o pedido.

+server_rejected_request=O servidor rejeitou o pedido: c\u00f3digo de estado = {0}

+server_rejected_request.explanation=O servidor criou um OAuth2Request, mas o fornecedor de servi\u00e7os rejeitou o mesmo.

+temporarily_unavailable=O servi\u00e7o est\u00e1 temporariamente indispon\u00edvel: {0}

+temporarily_unavailable.explanation=N\u00e3o \u00e9 poss\u00edvel ao servidor de autoriza\u00e7\u00f5es processar o pedido, uma vez que este est\u00e1 temporariamente sobrecarregado ou em modo de manuten\u00e7\u00e3o.

+token_response_problem=A resposta do ponto final do s\u00edmbolo est\u00e1 a ser processada: {0}

+token_response_problem.explanation=O ponto final do s\u00edmbolo enviou uma resposta que o servidor n\u00e3o conseguiu processar.

+unauthorized_client=O cliente n\u00e3o est\u00e1 autorizado: {0}

+unauthorized_client.explanation=O cliente n\u00e3o est\u00e1 autorizado a solicitar um s\u00edmbolo de acesso utilizando este m\u00e9todo.

+unsupported_grant_type=O tipo de concess\u00e3o n\u00e3o \u00e9 suportado: {0}

+unsupported_grant_type.explanation=O servidor de autoriza\u00e7\u00f5es n\u00e3o suporta a obten\u00e7\u00e3o de um s\u00edmbolo de acesso utilizando este m\u00e9todo.

+unsupported_response_type=O tipo de resposta n\u00e3o \u00e9 suportado: {0}

+unsupported_response_type.explanation=O servidor de autoriza\u00e7\u00f5es n\u00e3o suporta a obten\u00e7\u00e3o de um c\u00f3digo de autoriza\u00e7\u00e3o utilizando este m\u00e9todo.

+mac_token_problem=O s\u00edmbolo mac est\u00e1 a ser solicitado: {0}

+mac_token_problem.explanation=N\u00e3o foi poss\u00edvel adicionar o s\u00edmbolo mac no pedido ao servidor de recursos.

+bearer_token_problem=O s\u00edmbolo bearer est\u00e1 a ser solicitado: {0}

+bearer_token_problem.explanation=N\u00e3o foi poss\u00edvel adicionar o s\u00edmbolo bearer no pedido ao servidor de recursos.

+authentication_problem=A autentica\u00e7\u00e3o do cliente est\u00e1 a ser adicionada: {0}

+authentication_problem.explanation=N\u00e3o foi poss\u00edvel adicionar os cabe\u00e7alhos de autentica\u00e7\u00e3o ao pedido.

+unknown_problem=Ocorreu um erro desconhecido: {0}

+unknown_problem.explanation=Solicite ao administrador do sistema que investigue o problema.

+code_grant_problem=O s\u00edmbolo de acesso est\u00e1 a ser obtido a partir do c\u00f3digo de autoriza\u00e7\u00e3o: {0}

+code_grant_problem.explanation=Ocorreu um erro durante o fluxo de c\u00f3digos de autoriza\u00e7\u00e3o.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pt_BR.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pt_BR.properties
new file mode 100644
index 0000000..c85f992
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_pt_BR.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} encontrou um erro :

+authorization_code_problem=O c\u00f3digo de autoriza\u00e7\u00e3o est\u00e1 sendo trocado para o token de acesso: {0}

+authorization_code_problem.explanation=Ocorreu um erro ao trocar o c\u00f3digo de autoriza\u00e7\u00e3o para o token_de_acesso.

+authorize_problem=O processo de autoriza\u00e7\u00e3o para {0} est\u00e1 sendo iniciado.

+authorize_problem.explanation=Ocorreu um erro ao iniciar o processo de autoriza\u00e7\u00e3o.

+callback_problem=A resposta de redirecionamento est\u00e1 sendo processada: {0}

+callback_problem.explanation=Ocorreu um erro ao processar a resposta de redirecionamento do provedor de servi\u00e7os.

+client_credentials_problem=O token_de_acesso est\u00e1 sendo recuperado para o cliente com a seguinte credencial do cliente: {0}

+client_credentials_problem.explanation=Ocorreu um erro ao recuperar o token de acesso no fluxo de credenciais_do_cliente.

+fetch_init_problem=O OAuth2Request est\u00e1 sendo inicializado: {0}

+fetch_init_problem.explanation=Um erro de n\u00edvel inferior ocorreu ao inicializar a busca OAuth2Request.

+fetch_problem=executando OAuth2Request.fetch() : {0}

+fetch_problem.explanation=Ocorreu um erro ao emitir a busca OAuth2Request.

+gadget_spec_problem=A seguinte especifica\u00e7\u00e3o de dispositivo est\u00e1 em processamento: {0}

+gadget_spec_problem.explanation=Uma especifica\u00e7\u00e3o de dispositivo n\u00e3o foi localizada.  Pe\u00e7a ao administrador para configurar seu dispositivo para funcionar com OAuth2.0.

+get_oauth2_accessor_problem=obtendo um OAuth2Accessor para o OAuth2Request: {0}

+get_oauth2_accessor_problem.explanation=Ocorreu um erro. Pe\u00e7a ao seu administrador para criar uma liga\u00e7\u00e3o do Cliente OAuth2.0 para esse dispositivo e servi\u00e7o.

+lookup_spec_problem=A especifica\u00e7\u00e3o de dispositivo est\u00e1 sendo recuperada: {0}

+lookup_spec_problem.explanation=Uma especifica\u00e7\u00e3o de dispositivo n\u00e3o foi localizada.  Pe\u00e7a ao administrador para configurar seu dispositivo para funcionar com OAuth2.0.

+missing_fetch_params=Os seguintes par\u00e2metros de busca requeridos est\u00e3o ausentes: {0}

+missing_fetch_params.explanation=O m\u00e9todo fetch() foi chamado com par\u00e2metros inv\u00e1lidos.

+missing_server_response=Ocorreu um erro durante a resposta do provedor de servi\u00e7os OAuth2.0: {0}

+missing_server_response.explanation=O servidor criou um OAuth2Request v\u00e1lido, mas n\u00e3o p\u00f4de emitir a solicita\u00e7\u00e3o ou obter uma resposta v\u00e1lida do provedor de servi\u00e7os.

+no_response_handler=Um manipulador de resposta n\u00e3o processou a seguinte resposta: {0}

+no_response_handler.explanation=O manipulador de resposta n\u00e3o existe para processamento dos terminais de autoriza\u00e7\u00e3o e token. AuthorizationEndpointResponseHandler ou TokenEndpointResponseHandler.

+no_gadget_spec=A especifica\u00e7\u00e3o de dispositivo para {0} n\u00e3o pode ser localizada.

+no_gadget_spec.explanation=Uma especifica\u00e7\u00e3o de dispositivo n\u00e3o foi localizada.  Pe\u00e7a ao administrador para configurar seu dispositivo para funcionar com OAuth2.0.

+refresh_token_problem=O token de atualiza\u00e7\u00e3o est\u00e1 sendo trocado para o token_de_acesso: {0}

+refresh_token_problem.explanation=Ocorreu um erro ao trocar o token de atualiza\u00e7\u00e3o para o token de acesso.

+secret_encryption_problem=O segredo do token est\u00e1 sendo criptografado para persist\u00eancia: {0}

+secret_encryption_problem.explanation=Ocorreu um erro ao armazenar segredos OAuth2.0 com o m\u00f3dulo de criptografia fornecido.

+access_denied=O acesso foi negado.

+access_denied.explanation=O usu\u00e1rio negou ou cancelou a autoriza\u00e7\u00e3o com o provedor de servi\u00e7os.

+invalid_client=O cliente \u00e9 inv\u00e1lido: {0}

+invalid_client.explanation= O cliente n\u00e3o pode ser autenticado possivelmente porque n\u00e3o \u00e9 conhecido, a autentica\u00e7\u00e3o do cliente n\u00e3o estava inclu\u00edda ou o m\u00e9todo de autentica\u00e7\u00e3o n\u00e3o \u00e9 suportado.

+invalid_grant=A concess\u00e3o de autoriza\u00e7\u00e3o \u00e9 inv\u00e1lida: {0}

+invalid_grant.explanation=A concess\u00e3o de autoriza\u00e7\u00e3o \u00e9 inv\u00e1lida, expirou, foi revogada, n\u00e3o corresponde ao URI de redirecionamento na solicita\u00e7\u00e3o de autoriza\u00e7\u00e3o ou foi emitida para outro cliente. A concess\u00e3o de autoriza\u00e7\u00e3o pode incluir o c\u00f3digo de autoriza\u00e7\u00e3o, as credenciais do propriet\u00e1rio do recurso ou as credenciais do cliente.

+invalid_request=A solicita\u00e7\u00e3o \u00e9 inv\u00e1lida: {0}

+invalid_request.explanation=A solicita\u00e7\u00e3o \u00e9 inv\u00e1lida porque um par\u00e2metro necess\u00e1rio est\u00e1 ausente, inclui um valor de par\u00e2metro n\u00e3o suportado, repete um par\u00e2metro, inclui diversas credenciais, utiliza mais de um mecanismo para autenticar o cliente ou ent\u00e3o est\u00e1 malformada.

+invalid_scope=O escopo \u00e9 inv\u00e1lido: {0}

+invalid_scope.explanation=O escopo solicitado \u00e9 inv\u00e1lido, desconhecido, malformado ou excede o escopo concedido pelo propriet\u00e1rio do recurso.

+server_error=Ocorreu um erro do servidor: {0}

+server_error.explanation=O servidor de autoriza\u00e7\u00e3o encontrou uma condi\u00e7\u00e3o inesperada que o impediu de cumprir a solicita\u00e7\u00e3o.

+server_rejected_request=O servidor rejeitou a solicita\u00e7\u00e3o: statuscode = {0}

+server_rejected_request.explanation=O servidor criou um OAuth2Request, mas o provedor de servi\u00e7os o rejeitou.

+temporarily_unavailable=O servi\u00e7o est\u00e1 temporariamente indispon\u00edvel: {0}

+temporarily_unavailable.explanation=O servidor de autoriza\u00e7\u00e3o n\u00e3o pode manipular a solicita\u00e7\u00e3o porque est\u00e1 temporariamente sobrecarregado ou no modo de manuten\u00e7\u00e3o.

+token_response_problem=A resposta do terminal de token est\u00e1 sendo processada: {0}

+token_response_problem.explanation=O terminal de token enviou uma resposta que o servidor n\u00e3o p\u00f4de processar.

+unauthorized_client=O cliente n\u00e3o est\u00e1 autorizado: {0}

+unauthorized_client.explanation=O cliente n\u00e3o est\u00e1 autorizado a solicitar um token de acesso usando este m\u00e9todo.

+unsupported_grant_type=O tipo de concess\u00e3o se n\u00e3o suportado: {0}

+unsupported_grant_type.explanation=O servidor de autoriza\u00e7\u00e3o n\u00e3o suporta a obten\u00e7\u00e3o de um token de acesso usando este m\u00e9todo.

+unsupported_response_type=O tipo de resposta n\u00e3o \u00e9 suportado: {0}

+unsupported_response_type.explanation=O servidor de autoriza\u00e7\u00e3o n\u00e3o suporta a obten\u00e7\u00e3o de um c\u00f3digo de autoriza\u00e7\u00e3o usando este m\u00e9todo.

+mac_token_problem=O token mac est\u00e1 sendo solicitado: {0}

+mac_token_problem.explanation=O token mac na solicita\u00e7\u00e3o n\u00e3o p\u00f4de ser inclu\u00eddo no servidor de recursos.

+bearer_token_problem=O token bearer est\u00e1 sendo solicitado: {0}

+bearer_token_problem.explanation=O token bearer na solicita\u00e7\u00e3o n\u00e3o p\u00f4de ser inclu\u00eddo no servidor de recursos.

+authentication_problem=A autentica\u00e7\u00e3o do cliente est\u00e1 sendo inclu\u00edda: {0}

+authentication_problem.explanation=N\u00e3o foi poss\u00edvel incluir os cabe\u00e7alhos de autentica\u00e7\u00e3o na solicita\u00e7\u00e3o.

+unknown_problem=Ocorreu um erro desconhecido: {0}

+unknown_problem.explanation=Pe\u00e7a ao administrador do sistema para investigar o problema.

+code_grant_problem=O token de acesso est\u00e1 sendo obtido do c\u00f3digo de autoriza\u00e7\u00e3o: {0}

+code_grant_problem.explanation=Ocorreu um erro durante o fluxo do c\u00f3digo de autoriza\u00e7\u00e3o.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ru.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ru.properties
new file mode 100644
index 0000000..6b95def
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_ru.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= \u0412 {0} \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430:

+authorization_code_problem=\u0418\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d \u0434\u043b\u044f \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430: {0}

+authorization_code_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u043b\u044f \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430.

+authorize_problem=\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u043b\u044f {0} \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u0443\u0435\u0442\u0441\u044f.

+authorize_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0438\u043d\u0438\u0446\u0438\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0438 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.

+callback_problem=\u041e\u0442\u0432\u0435\u0442 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d: {0}

+callback_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0435 \u043e\u0442\u0432\u0435\u0442\u0430 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043e\u0442 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430 \u0441\u043b\u0443\u0436\u0431\u044b.

+client_credentials_problem=\u0418\u0437\u0432\u043b\u0435\u0447\u0435\u043d access_token \u0434\u043b\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0441\u043e \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u044b\u043c \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043d\u0438\u0435\u043c: {0}

+client_credentials_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0432 \u043f\u043e\u0442\u043e\u043a\u0435 client_credentials.

+fetch_init_problem=OAuth2Request \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0438\u0440\u0443\u0435\u0442\u0441\u044f: {0}

+fetch_init_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0438\u0437\u043a\u043e\u0443\u0440\u043e\u0432\u043d\u0435\u0432\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0438\u043d\u0438\u0446\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u044b\u0431\u043e\u0440\u043a\u0438 OAuth2Request.

+fetch_problem=\u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 OAuth2Request.fetch() : {0}

+fetch_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 \u0432\u044b\u0431\u043e\u0440\u043a\u0438 OAuth2Request.

+gadget_spec_problem=\u041e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430: {0}

+gadget_spec_problem.explanation=\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430.  \u041f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0433\u0430\u0434\u0436\u0435\u0442 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 OAuth2.0.

+get_oauth2_accessor_problem=\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 OAuth2Accessor \u0434\u043b\u044f OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430. \u041f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0441\u0432\u044f\u0437\u044b\u0432\u0430\u043d\u0438\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 OAuth2.0 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0433\u0430\u0434\u0436\u0435\u0442\u0430 \u0438 \u0441\u043b\u0443\u0436\u0431\u044b.

+lookup_spec_problem=\u0421\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430 \u0438\u0437\u0432\u043b\u0435\u0447\u0435\u043d\u0430: {0}

+lookup_spec_problem.explanation=\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430.  \u041f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0433\u0430\u0434\u0436\u0435\u0442 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 OAuth2.0.

+missing_fetch_params=\u041e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0435 \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u0431\u043e\u0440\u043a\u0438: {0}

+missing_fetch_params.explanation=\u041c\u0435\u0442\u043e\u0434 fetch() \u0432\u044b\u0437\u0432\u0430\u043d \u0441 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u043c\u0438 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u043c\u0438.

+missing_server_response=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043e\u0442\u0432\u0435\u0442\u0435 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430 \u0441\u043b\u0443\u0436\u0431\u044b OAuth2.0: {0}

+missing_server_response.explanation=\u0421\u0435\u0440\u0432\u0435\u0440 \u0441\u043e\u0437\u0434\u0430\u043d \u0441 \u0432\u0435\u0440\u043d\u044b\u043c OAuth2Request, \u043d\u043e \u043d\u0435 \u0438\u043c\u0435\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441 \u0438\u043b\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0432\u0435\u0440\u043d\u044b\u0439 \u043e\u0442\u0432\u0435\u0442 \u043e\u0442 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0430 \u0441\u043b\u0443\u0436\u0431\u044b.

+no_response_handler=\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043e\u0442\u0432\u0435\u0442\u043e\u0432 \u043d\u0435 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043b \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0438\u0439 \u043e\u0442\u0432\u0435\u0442: {0}

+no_response_handler.explanation=\u041d\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0447\u0438\u043a \u043e\u0442\u0432\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043a\u043e\u043d\u0435\u0447\u043d\u044b\u0445 \u0442\u043e\u0447\u0435\u043a \u043a\u043b\u044e\u0447\u0430 \u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438. AuthorizationEndpointResponseHandler \u0438\u043b\u0438 TokenEndpointResponseHandler.

+no_gadget_spec=\u0421\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430 \u0434\u043b\u044f {0} \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430.

+no_gadget_spec.explanation=\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u0430 \u0441\u043f\u0435\u0446\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0433\u0430\u0434\u0436\u0435\u0442\u0430.  \u041f\u043e\u043f\u0440\u043e\u0441\u0438\u0442\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0433\u0430\u0434\u0436\u0435\u0442 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 OAuth2.0.

+refresh_token_problem=\u041a\u043b\u044e\u0447 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u0437\u043c\u0435\u043d\u0435\u043d \u0434\u043b\u044f access_token: {0}

+refresh_token_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0434\u043b\u044f \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430.

+secret_encryption_problem=\u041f\u0430\u0440\u043e\u043b\u044c \u043a\u043b\u044e\u0447\u0430 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u0445\u0440\u0430\u043d\u0438\u043b\u0438\u0449\u0430: {0}

+secret_encryption_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u0438 \u043f\u0430\u0440\u043e\u043b\u0435\u0439 OAuth2.0 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043c\u043e\u0434\u0443\u043b\u044f \u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0438\u044f.

+access_denied=\u0414\u043e\u0441\u0442\u0443\u043f \u0437\u0430\u043f\u0440\u0435\u0449\u0435\u043d.

+access_denied.explanation=\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0437\u0430\u043f\u0440\u0435\u0442\u0438\u043b \u0438\u043b\u0438 \u043e\u0442\u043c\u0435\u043d\u0438\u043b \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u0432 \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a\u0435 \u0441\u043b\u0443\u0436\u0431\u044b.

+invalid_client=\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442: {0}

+invalid_client.explanation= \u041a\u043b\u0438\u0435\u043d\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0437\u0430\u0449\u0438\u0449\u0435\u043d. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u0435 \u043f\u0440\u0438\u0447\u0438\u043d\u044b: \u043a\u043b\u0438\u0435\u043d\u0442 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043d, \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043d\u0435 \u0431\u044b\u043b\u0430 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u0438\u043b\u0438 \u043c\u0435\u0442\u043e\u0434 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.

+invalid_grant=\u041d\u0435\u0432\u0435\u0440\u043d\u043e\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0430\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u0430: {0}

+invalid_grant.explanation=\u041f\u0440\u0430\u0432\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435\u0432\u0435\u0440\u043d\u043e, \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u0435\u043d\u043e, \u0430\u043d\u043d\u0443\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u043e, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 URI \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u043c\u043e\u0433\u043e \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u043b\u0438 \u0431\u044b\u043b\u043e \u043e\u0442\u043e\u0441\u043b\u0430\u043d\u043e \u0434\u0440\u0443\u0433\u043e\u043c\u0443 \u043a\u043b\u0438\u0435\u043d\u0442\u0443. \u041f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u0430\u0432\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043c\u043e\u0436\u0435\u0442 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0432 \u0441\u0435\u0431\u044f \u0438\u0441\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438, \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0432\u043b\u0430\u0434\u0435\u043b\u044c\u0446\u0430 \u0440\u0435\u0441\u0443\u0440\u0441\u0430 \u0438\u043b\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u043a\u043b\u0438\u0435\u043d\u0442\u0430.

+invalid_request=\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0437\u0430\u043f\u0440\u043e\u0441: {0}

+invalid_request.explanation=\u0417\u0430\u043f\u0440\u043e\u0441 \u043d\u0435\u0432\u0435\u0440\u0435\u043d, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0442\u0440\u0435\u0431\u0443\u0435\u043c\u044b\u0439 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440, \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u043d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430, \u0434\u0443\u0431\u043b\u0438\u0440\u0443\u0435\u0442 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0445 \u0434\u0430\u043d\u043d\u044b\u0445, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0431\u043e\u043b\u0435\u0435 \u043e\u0434\u043d\u043e\u0433\u043e \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c\u0430 \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0438\u043b\u0438 \u0438\u043c\u0435\u0435\u0442 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442.

+invalid_scope=\u041e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430: {0}

+invalid_scope.explanation=\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u0430\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430, \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u0438\u043c\u0435\u0435\u0442 \u043d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442 \u0438\u043b\u0438 \u043f\u0440\u0435\u0432\u044b\u0448\u0430\u0435\u0442 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u0443\u044e \u0432\u043b\u0430\u0434\u0435\u043b\u044c\u0446\u0435\u043c \u0440\u0435\u0441\u0443\u0440\u0441\u0430.

+server_error=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: {0}

+server_error.explanation=\u041d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u043e\u0436\u0438\u0434\u0430\u043d\u043d\u0430\u044f \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u044f, \u0438\u0437-\u0437\u0430 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u0437\u0430\u043f\u0440\u043e\u0441 \u043d\u0435 \u0431\u044b\u043b \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d.

+server_rejected_request=\u0421\u0435\u0440\u0432\u0435\u0440 \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b \u0437\u0430\u043f\u0440\u043e\u0441: statuscode = {0}

+server_rejected_request.explanation=\u0421\u0435\u0440\u0432\u0435\u0440 \u0441\u043e\u0437\u0434\u0430\u043b OAuth2Request, \u043d\u043e \u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a \u0441\u043b\u0443\u0436\u0431\u044b \u043e\u0442\u043a\u043b\u043e\u043d\u0438\u043b \u0435\u0433\u043e.

+temporarily_unavailable=\u0421\u043b\u0443\u0436\u0431\u0430 \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430: {0}

+temporarily_unavailable.explanation=\u0421\u0435\u0440\u0432\u0435\u0440 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0437\u0430\u043f\u0440\u043e\u0441, \u043f\u043e\u0442\u043e\u043c\u0443 \u0447\u0442\u043e \u043e\u043d \u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u043f\u0435\u0440\u0435\u0433\u0440\u0443\u0436\u0435\u043d \u0438\u043b\u0438 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u043e\u0431\u0441\u043b\u0443\u0436\u0438\u0432\u0430\u043d\u0438\u044f.

+token_response_problem=\u041e\u0442\u0432\u0435\u0442 \u043e\u0442 \u043a\u043e\u043d\u0435\u0447\u043d\u043e\u0439 \u0442\u043e\u0447\u043a\u0438 \u043a\u043b\u044e\u0447\u0430 \u043e\u0431\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442\u0441\u044f: {0}

+token_response_problem.explanation=\u041a\u043e\u043d\u0435\u0447\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430 \u043a\u043b\u044e\u0447\u0430 \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u043e\u0442\u0432\u0435\u0442, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c.

+unauthorized_client=\u0423 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043d\u0435\u0442 \u043f\u0440\u0430\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u0430: {0}

+unauthorized_client.explanation=\u0423 \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u043d\u0435\u0442 \u043f\u0440\u0430\u0432 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430.

+unsupported_grant_type=\u0422\u0438\u043f \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f: {0}

+unsupported_grant_type.explanation=\u0421\u0435\u0440\u0432\u0435\u0440 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043a\u043b\u044e\u0447\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430.

+unsupported_response_type=\u0422\u0438\u043f \u043e\u0442\u0432\u0435\u0442\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f: {0}

+unsupported_response_type.explanation=\u0421\u0435\u0440\u0432\u0435\u0440 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0442\u043e\u0434\u0430.

+mac_token_problem=\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d \u043a\u043b\u044e\u0447 mac: {0}

+mac_token_problem.explanation=\u041a\u043b\u044e\u0447 mac \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432.

+bearer_token_problem=\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d \u043a\u043b\u044e\u0447 \u043e\u0434\u043d\u043e\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043a\u0430\u043d\u0430\u043b\u0430: {0}

+bearer_token_problem.explanation=\u041a\u043b\u044e\u0447 \u043e\u0434\u043d\u043e\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u043a\u0430\u043d\u0430\u043b\u0430 \u0432 \u0437\u0430\u043f\u0440\u043e\u0441\u0435 \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0440\u0435\u0441\u0443\u0440\u0441\u043e\u0432.

+authentication_problem=\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430: {0}

+authentication_problem.explanation=\u0417\u0430\u0433\u043e\u043b\u043e\u0432\u043a\u0438 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043d\u0435 \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u044b \u0432 \u0437\u0430\u043f\u0440\u043e\u0441.

+unknown_problem=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: {0}

+unknown_problem.explanation=\u041e\u0431\u0440\u0430\u0442\u0438\u0442\u0435\u0441\u044c \u043a \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0443 \u0434\u043b\u044f \u0438\u0441\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u043d\u0438\u044f \u043e\u0448\u0438\u0431\u043a\u0438.

+code_grant_problem=\u041a\u043b\u044e\u0447 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u043e\u043b\u0443\u0447\u0435\u043d \u0438\u0437 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438: {0}

+code_grant_problem.explanation=\u041f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0435 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u0434\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_sl.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_sl.properties
new file mode 100644
index 0000000..991f28e
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_sl.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} je naletel na napako:

+authorization_code_problem=Poteka zamenjava pooblastitvene kode za \u017eeton dostopa: {0}

+authorization_code_problem.explanation=Med izmenjavo pooblastitvene kode za \u017eeton dostopa je pri\u0161lo do napake.

+authorize_problem=Inicira se postopek poobla\u0161\u010danja za {0}.

+authorize_problem.explanation=Med iniciranjem postopka poobla\u0161\u010danja je pri\u0161lo do napake.

+callback_problem=Poteka obdelava odgovora na preusmeritev: {0}

+callback_problem.explanation=Med obdelovanjem odgovora na preusmeritev iz ponudnika storitev je pri\u0161lo do napake.

+client_credentials_problem=Poteka pridobivanje \u017eetona dostopa za odjemalca z naslednjimi poverilnicami odjemalca: {0}

+client_credentials_problem.explanation=Med pridobivanjem \u017eetona dostopa v toku poverilnic odjemalca je pri\u0161lo do napake.

+fetch_init_problem=OAuth2Request se inicializira: {0}

+fetch_init_problem.explanation=Med inicializiranjem pridobivanja OAuth2Request je pri\u0161lo do napake nizke ravni.

+fetch_problem=izvajanje OAuth2Request.fetch() : {0}

+fetch_problem.explanation=Med izdajanjem pridobitve OAuth2Request je pri\u0161lo do napake.

+gadget_spec_problem=Poteka obdelava naslednje specifikacije pripomo\u010dka: {0}

+gadget_spec_problem.explanation=Specifikacije pripomo\u010dka ni bilo mogo\u010de najti. Prosite skrbnika, da pripomo\u010dek konfigurira tako, da bo deloval z OAuth2.0.

+get_oauth2_accessor_problem=pridobivanje OAuth2Accessor za OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=Zgodila se je napaka. Skrbnika prosite, naj ustvari vezavo odjemalca OAuth2.0 za ta pripomo\u010dek in storitev.

+lookup_spec_problem=Poteka pridobivanje specifikacije pripomo\u010dka: {0}

+lookup_spec_problem.explanation=Specifikacije pripomo\u010dka ni bilo mogo\u010de najti. Prosite skrbnika, da pripomo\u010dek konfigurira tako, da bo deloval z OAuth2.0.

+missing_fetch_params=Naslednji zahtevani parametri pridobivanja manjkajo: {0}

+missing_fetch_params.explanation=Metoda fetch() je bila poklicana z neveljavnimi parametri.

+missing_server_response=Med odgovorom ponudnika storitev OAuth2.0 je pri\u0161lo do napake: {0}

+missing_server_response.explanation=Stre\u017enik je ustvaril veljaven OAuth2Request, toda ni mogel izdati zahteve ali pridobiti veljavnega odgovora od ponudnika storitev.

+no_response_handler=Nadzornik odgovorov ni obdelal naslednjega odgovora: {0}

+no_response_handler.explanation=Nadzornik odgovorov ne obstaja za obdelavo pooblastitve in zaklju\u010dnih to\u010dk \u017eetona. AuthorizationEndpointResponseHandler ali TokenEndpointResponseHandler.

+no_gadget_spec=Specifikacije pripomo\u010dka za {0} ni mogo\u010de najti.

+no_gadget_spec.explanation=Specifikacije pripomo\u010dka ni bilo mogo\u010de najti. Prosite skrbnika, da pripomo\u010dek konfigurira tako, da bo deloval z OAuth2.0.

+refresh_token_problem=Poteka zamenjava \u017eetona osve\u017eitve za \u017eeton dostopa: {0}

+refresh_token_problem.explanation=Med zamenjavo \u017eetona osve\u017eitve za \u017eeton dostopa je pri\u0161lo do napake.

+secret_encryption_problem=Poteka \u0161ifriranje skrivnosti \u017eetona za trajnost: {0}

+secret_encryption_problem.explanation=Med shranjevanjem skrivnosti OAuth2.0 s ponujenim modulom za \u0161ifriranje je pri\u0161lo do napake.

+access_denied=Dostop je bil zavrnjen.

+access_denied.explanation=Uporabnik je zavrnil ali preklical pooblastilo s ponudnikom storitev.

+invalid_client=Odjemalec ni veljaven: {0}

+invalid_client.explanation= Odjemalca ni mogo\u010de overiti, najverjetneje zato, ker ni znan, overitev odjemalca ni bila vklju\u010dena ali metoda overjanja ni podprta.

+invalid_grant=Odobritev pooblastitve ni veljavna: {0}

+invalid_grant.explanation=Odobritev pooblastitve ni veljavna, je potekla, je bila preklicana ali se ne ujema z URI-jem preusmeritve, uporabljenim v zahtevi za pooblastitev, ali je bila izdana za drugega odjemalca. Odobritev pooblastitve lahko vklju\u010duje pooblastitveno kodo, poverilnice lastnika virov ali poverilnice odjemalca.

+invalid_request=Zahteva ni veljavna: {0}

+invalid_request.explanation=Zahteva ni veljavna, ker ne vsebuje obveznih parametrov, vklju\u010duje nepodprto vrednost parametra, ponovi parameter, vklju\u010duje ve\u010d poverilnic, uporablja ve\u010d kot en mehanizem za overjanje odjemalca ali je kako druga\u010de napa\u010dno oblikovana.

+invalid_scope=Obmo\u010dje ni veljavno: {0}

+invalid_scope.explanation=Zahtevano obmo\u010dje ni veljavno, je neznano, napa\u010dno oblikovano ali presega obmo\u010dje, ki ga je odobril lastnik vira.

+server_error=Zgodila se je napaka stre\u017enika: {0}

+server_error.explanation=Stre\u017enik za poobla\u0161\u010danje je naletel na nepri\u010dakovano stanje, ki mu je prepre\u010dilo, da bi izpolnil zahtevo.

+server_rejected_request=Stre\u017enik je zavrnil zahtevo: statusna koda = {0}

+server_rejected_request.explanation=Stre\u017enik je ustvaril OAuth2Request, toda ponudnik storitev ga je zavrnil.

+temporarily_unavailable=Storitev trenutno ni na voljo: {0}

+temporarily_unavailable.explanation=Stre\u017enik za poobla\u0161\u010danje ne more obravnavati zahteve, ker je za\u010dasno preobremenjen ali je v vzdr\u017eevalnem na\u010dinu.

+token_response_problem=Poteka obdelava odgovora iz zaklju\u010dne to\u010dke \u017eetona: {0}

+token_response_problem.explanation=Zaklju\u010dna to\u010dka \u017eetona je poslala odgovor, ki ga stre\u017enik ni mogel obdelati.

+unauthorized_client=Odjemalec ni poobla\u0161\u010den: {0}

+unauthorized_client.explanation=Odjemalec ni poobla\u0161\u010den za zahtevanje \u017eetona dostopa z uporabo te metode.

+unsupported_grant_type=Vrsta odobritve ni podprta: {0}

+unsupported_grant_type.explanation=Stre\u017enik za poobla\u0161\u010danje ne podpira pridobivanja \u017eetona dostopa z uporabo te metode.

+unsupported_response_type=Vrsta odgovora ni podprta: {0}

+unsupported_response_type.explanation=Stre\u017enik za poobla\u0161\u010danje ne podpira pridobivanja pooblastitvene kode z uporabo te metode.

+mac_token_problem=Zahtevan je \u017eeton mac: {0}

+mac_token_problem.explanation=\u017detona mac v zahtevi ni bilo mogo\u010de dodati stre\u017eniku virov.

+bearer_token_problem=Zahtevan je \u017eeton nosilca: {0}

+bearer_token_problem.explanation=\u017detona nosilca v zahtevi ni bilo mogo\u010de dodati stre\u017eniku virov.

+authentication_problem=Poteka dodajanje overjanja odjemalca: {0}

+authentication_problem.explanation=Glav overjanja ni bilo mogo\u010de dodati v zahtevo.

+unknown_problem=Zgodila se je neznana napaka: {0}

+unknown_problem.explanation=Skrbnika sistema prosite, naj razi\u0161\u010de te\u017eavo.

+code_grant_problem=Poteka pridobivanje \u017eetona dostopa iz pooblastitvene kode: {0}

+code_grant_problem.explanation=Med pridobivanjem toka pooblastitvene kode je pri\u0161lo do napake.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_sv.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_sv.properties
new file mode 100644
index 0000000..4ab6705
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_sv.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= Det uppstod ett fel i {0}:

+authorization_code_problem=Autentiseringskoden byts ut mot \u00e5tkomstelementet {0}

+authorization_code_problem.explanation=Det uppstod ett fel n\u00e4r autentiseringskoden skulle bytas ut mot \u00e5tkomstelementet.

+authorize_problem=Autentiseringsprocessen f\u00f6r {0} initieras.

+authorize_problem.explanation=Det uppstod ett fel n\u00e4r autentiseringsprocessen skulle initieras.

+callback_problem=Omdirigeringssvaret bearbetas: {0}

+callback_problem.explanation=Det uppstod ett fel n\u00e4r omdirigeringssvaret fr\u00e5n tj\u00e4nsteleverant\u00f6ren skulle omdirigeras.

+client_credentials_problem=\u00c5tkomstelementet h\u00e4mtas f\u00f6r klienten med f\u00f6ljande klientanv\u00e4ndarinformation: {0}

+client_credentials_problem.explanation=Det uppstod ett fel n\u00e4r \u00e5tkomstelementet skulle h\u00e4mtas fr\u00e5n klientanv\u00e4ndarinformationsfl\u00f6det.

+fetch_init_problem=OAuth2Request-beg\u00e4ran initieras: {0}

+fetch_init_problem.explanation=Det uppstod ett l\u00e5gniv\u00e5fel n\u00e4r OAuth2Request-h\u00e4mtningen skulle initieras.

+fetch_problem=OAuth2Request.fetch() utf\u00f6rs: {0}

+fetch_problem.explanation=Det uppstod ett fel n\u00e4r OAuth2Request-h\u00e4mtningen skulle utf\u00e4rdas.

+gadget_spec_problem=F\u00f6ljande gadgetprogramsspecifikation bearbetas: {0}

+gadget_spec_problem.explanation=Det gick inte att hitta n\u00e5gon gadgetprogramsspecifikation. Kontakta administrat\u00f6ren och be honom/henne att konfigurera gadgetprogrammet s\u00e5 att det g\u00e5r att anv\u00e4nda OAuth 2.0.

+get_oauth2_accessor_problem=OAuth2Accessor-\u00e5tkomstfunktionen f\u00f6r OAuth2Request-beg\u00e4ran h\u00e4mtas: {0}

+get_oauth2_accessor_problem.explanation=Det uppstod ett fel. Kontakta administrat\u00f6ren och be honom/henne att skapa en OAuth 2.0-klientbindning f\u00f6r det h\u00e4r gadgetprogrammet och den h\u00e4r tj\u00e4nsten.

+lookup_spec_problem=Gadgetprogramsspecifikationen h\u00e4mtas: {0}

+lookup_spec_problem.explanation=Det gick inte att hitta n\u00e5gon gadgetprogramsspecifikation. Kontakta administrat\u00f6ren och be honom/henne att konfigurera gadgetprogrammet s\u00e5 att det g\u00e5r att anv\u00e4nda OAuth 2.0.

+missing_fetch_params=F\u00f6ljande obligatoriska h\u00e4mtningsparametrar saknas: {0}

+missing_fetch_params.explanation=Metoden fetch() anropades med ogiltiga parametrar.

+missing_server_response=Det uppstod ett fel i OAuth 2.0-tj\u00e4nsteleverant\u00f6rssvaret: {0}

+missing_server_response.explanation=En giltig OAuth2Request-beg\u00e4ran skapades p\u00e5 servern men det gick inte att utf\u00e4rda beg\u00e4ran eller ta emot n\u00e5got giltigt svar fr\u00e5n tj\u00e4nsteleverant\u00f6ren.

+no_response_handler=Det gick inte att bearbeta f\u00f6ljande svar i en svarshanterare: {0}

+no_response_handler.explanation=Det finns ingen svarshanterare f\u00f6r bearbetning av autentiserings- och elementslutpunkterna. AuthorizationEndpointResponseHandler eller TokenEndpointResponseHandler.

+no_gadget_spec=Det gick inte att hitta gadgetprogramsspecifikationen f\u00f6r {0}.

+no_gadget_spec.explanation=Det gick inte att hitta n\u00e5gon gadgetprogramsspecifikation. Kontakta administrat\u00f6ren och be honom/henne att konfigurera gadgetprogrammet s\u00e5 att det g\u00e5r att anv\u00e4nda OAuth 2.0.

+refresh_token_problem=Uppdateringselementet byts ut mot \u00e5tkomstelementet {0}

+refresh_token_problem.explanation=Det uppstod ett fel n\u00e4r uppdateringselementet skulle bytas ut mot \u00e5tkomstelementet.

+secret_encryption_problem=Elementhemligheten krypteras f\u00f6r best\u00e4ndighet: {0}

+secret_encryption_problem.explanation=Det uppstod ett fel n\u00e4r OAuth 2.0-hemligheterna skulle lagras med hj\u00e4lp av den angivna krypteringsmodulen.

+access_denied=\u00c5tkomst nekades.

+access_denied.explanation=Anv\u00e4ndaren nekade eller avbr\u00f6t autentiseringen med tj\u00e4nsteleverant\u00f6ren.

+invalid_client=Klienten \u00e4r ogiltig: {0}

+invalid_client.explanation= Det gick inte att autentisera klienten. Det kan bero p\u00e5 att klienten \u00e4r ok\u00e4nd, p\u00e5 att klientautentiseringen inte har tagits med eller p\u00e5 att autentiseringsmetoden inte kan anv\u00e4ndas.

+invalid_grant=Autentiseringstilldelningen \u00e4r ogiltig: {0}

+invalid_grant.explanation=Autentiseringstilldelningen \u00e4r ogiltig, har upph\u00f6rt att g\u00e4lla, har \u00e5terkallats, \u00f6verensst\u00e4mmer inte med den omdirigerings-URI-adress som har anv\u00e4nts i autentiseringsbeg\u00e4ran eller har utf\u00e4rdats till en annan klient. Autentiseringstilldelningen kan inneh\u00e5lla autentiseringskoden, anv\u00e4ndarinformationen f\u00f6r resurs\u00e4garen eller anv\u00e4ndarinformationen f\u00f6r klienten.

+invalid_request=Beg\u00e4ran \u00e4r ogiltig: {0}

+invalid_request.explanation=Beg\u00e4ran \u00e4r ogiltig eftersom den saknar en obligatorisk parameter, inneh\u00e5ller ett parameterv\u00e4rde som inte kan anv\u00e4ndas, inneh\u00e5ller en upprepad parameter, inneh\u00e5ller flera upps\u00e4ttningar med anv\u00e4ndarinformation, inneh\u00e5ller flera metoder f\u00f6r autentisering av klienten eller \u00e4r felformaterad p\u00e5 n\u00e5got annat s\u00e4tt.

+invalid_scope=Omf\u00e5nget \u00e4r ogiltigt: {0}

+invalid_scope.explanation=Det beg\u00e4rda omf\u00e5nget \u00e4r ogiltigt, ok\u00e4nt, felformaterat eller \u00f6verskrider det omf\u00e5ng som \u00e4r angett av resurs\u00e4garen.

+server_error=Det uppstod ett serverfel: {0}

+server_error.explanation=Det uppstod ett ov\u00e4ntat villkor p\u00e5 autentiseringsservern vilket medf\u00f6rde att det inte gick att slutf\u00f6ra beg\u00e4ran.

+server_rejected_request=Beg\u00e4ran avvisades p\u00e5 servern. Statuskod: {0}

+server_rejected_request.explanation=En OAuth2Request-beg\u00e4ran skapades p\u00e5 servern, men beg\u00e4ran avvisades av tj\u00e4nsteleverant\u00f6ren.

+temporarily_unavailable=Tj\u00e4nsten \u00e4r inte tillg\u00e4nglig: {0}

+temporarily_unavailable.explanation=Det gick inte att hantera beg\u00e4ran p\u00e5 autentiseringsservern eftersom servern \u00e4r \u00f6verbelastad eller i underh\u00e5llsl\u00e4ge.

+token_response_problem=Svaret fr\u00e5n elementslutpunkten bearbetas: {0}

+token_response_problem.explanation=Ett svar s\u00e4ndes fr\u00e5n elementslutpunkten som det inte gick att bearbeta p\u00e5 servern.

+unauthorized_client=Klienten \u00e4r inte autentiserad: {0}

+unauthorized_client.explanation=Klienten \u00e4r inte autentiserad f\u00f6r att beg\u00e4ra ett \u00e5tkomstelement med hj\u00e4lp av den angivna metoden.

+unsupported_grant_type=Det g\u00e5r inte att anv\u00e4nda tilldelningstypen {0}

+unsupported_grant_type.explanation=Det finns inga funktioner p\u00e5 autentiseringsservern f\u00f6r att tillhandah\u00e5lla ett \u00e5tkomstelement med hj\u00e4lp av den angivna metoden.

+unsupported_response_type=Det g\u00e5r inte att anv\u00e4nda svarstypen {0}

+unsupported_response_type.explanation=Det finns inga funktioner p\u00e5 autentiseringsservern f\u00f6r att tillhandah\u00e5lla en autentiseringskod med hj\u00e4lp av den angivna metoden.

+mac_token_problem=mac-element som beg\u00e4rs: {0}

+mac_token_problem.explanation=Det gick inte att l\u00e4gga till mac-elementet i beg\u00e4ran p\u00e5 resursservern.

+bearer_token_problem=bearer-element som beg\u00e4rs: {0}

+bearer_token_problem.explanation=Det gick inte att l\u00e4gga till bearer-elementet i beg\u00e4ran p\u00e5 resursservern.

+authentication_problem=Klientautentisering som l\u00e4ggs till: {0}

+authentication_problem.explanation=Det gick inte att l\u00e4gga till autentiseringshuvudena till beg\u00e4ran.

+unknown_problem=Det uppstod ett ok\u00e4nt fel: {0}

+unknown_problem.explanation=Kontakta administrat\u00f6ren och be honom/henne att unders\u00f6ka problemet.

+code_grant_problem=\u00c5tkomstelement som h\u00e4mtas fr\u00e5n autentiseringskoden: {0}

+code_grant_problem.explanation=Det uppstod ett fel i autentiseringskodfl\u00f6det.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_th.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_th.properties
new file mode 100644
index 0000000..96b48ad
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_th.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} \u0e1e\u0e1a\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14 :

+authorization_code_problem=\u0e42\u0e04\u0e49\u0e14\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e16\u0e39\u0e01\u0e41\u0e25\u0e01\u0e40\u0e1b\u0e25\u0e35\u0e48\u0e22\u0e19\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19: {0}

+authorization_code_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e41\u0e25\u0e01\u0e40\u0e1b\u0e25\u0e35\u0e48\u0e22\u0e19\u0e42\u0e04\u0e49\u0e14\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a access_token

+authorize_problem=\u0e01\u0e23\u0e30\u0e1a\u0e27\u0e19\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {0} \u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e34\u0e48\u0e21\u0e15\u0e49\u0e19

+authorize_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e13\u0e30\u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e34\u0e48\u0e21\u0e15\u0e49\u0e19\u0e01\u0e23\u0e30\u0e1a\u0e27\u0e19\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19

+callback_problem=\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e17\u0e35\u0e48\u0e40\u0e1b\u0e25\u0e35\u0e48\u0e22\u0e19\u0e17\u0e34\u0e28\u0e17\u0e32\u0e07\u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25 : {0}

+callback_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e13\u0e30\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e17\u0e35\u0e48\u0e40\u0e1b\u0e25\u0e35\u0e48\u0e22\u0e19\u0e17\u0e34\u0e28\u0e17\u0e32\u0e07\u0e08\u0e32\u0e01\u0e1c\u0e39\u0e49\u0e43\u0e2b\u0e49\u0e1a\u0e23\u0e34\u0e01\u0e32\u0e23

+client_credentials_problem=\u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e35\u0e22\u0e01 access_token \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e14\u0e49\u0e27\u0e22\u0e2b\u0e19\u0e31\u0e07\u0e2a\u0e37\u0e2d\u0e23\u0e31\u0e1a\u0e23\u0e2d\u0e07\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49 : {0}

+client_credentials_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e13\u0e30\u0e14\u0e36\u0e07\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e43\u0e19\u0e42\u0e1f\u0e25\u0e27\u0e4c client_credentials

+fetch_init_problem=OAuth2Request \u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e15\u0e23\u0e35\u0e22\u0e21\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e40\u0e1a\u0e37\u0e49\u0e2d\u0e07\u0e15\u0e49\u0e19: {0}

+fetch_init_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e23\u0e30\u0e14\u0e31\u0e1a\u0e15\u0e48\u0e33\u0e02\u0e13\u0e30\u0e40\u0e15\u0e23\u0e35\u0e22\u0e21\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e40\u0e1a\u0e37\u0e49\u0e2d\u0e07\u0e15\u0e49\u0e19\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e14\u0e36\u0e07 OAuth2Request

+fetch_problem=\u0e01\u0e33\u0e25\u0e31\u0e07\u0e40\u0e23\u0e35\u0e22\u0e01\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19 OAuth2Request.fetch() : {0}

+fetch_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e13\u0e30\u0e14\u0e36\u0e07\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25 OAuth2Request

+gadget_spec_problem=\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e08\u0e33\u0e40\u0e1e\u0e32\u0e30\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e01\u0e33\u0e25\u0e31\u0e07\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25 : {0}

+gadget_spec_problem.explanation=\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e08\u0e33\u0e40\u0e1e\u0e32\u0e30\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15  \u0e42\u0e1b\u0e23\u0e14\u0e41\u0e08\u0e49\u0e07\u0e1c\u0e39\u0e49\u0e14\u0e39\u0e41\u0e25\u0e23\u0e30\u0e1a\u0e1a\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e01\u0e33\u0e2b\u0e19\u0e14\u0e04\u0e2d\u0e19\u0e1f\u0e34\u0e01\u0e43\u0e2b\u0e49\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e43\u0e2b\u0e49\u0e17\u0e33\u0e07\u0e32\u0e19\u0e01\u0e31\u0e1a OAuth2.0

+get_oauth2_accessor_problem=\u0e01\u0e33\u0e25\u0e31\u0e07\u0e02\u0e2d\u0e23\u0e31\u0e1a OAuth2Accessor \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a OAuth2Request : {0}

+get_oauth2_accessor_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e36\u0e49\u0e19 \u0e42\u0e1b\u0e23\u0e14\u0e41\u0e08\u0e49\u0e07\u0e1c\u0e39\u0e49\u0e14\u0e39\u0e41\u0e25\u0e23\u0e30\u0e1a\u0e1a\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e2a\u0e23\u0e49\u0e32\u0e07\u0e01\u0e32\u0e23\u0e42\u0e22\u0e07\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c OAuth2.0 \u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e41\u0e25\u0e30\u0e40\u0e0b\u0e2d\u0e23\u0e4c\u0e27\u0e34\u0e2a\u0e19\u0e35\u0e49

+lookup_spec_problem=\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e08\u0e33\u0e40\u0e1e\u0e32\u0e30\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e14\u0e36\u0e07\u0e2d\u0e2d\u0e01\u0e21\u0e32 : {0}

+lookup_spec_problem.explanation=\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e08\u0e33\u0e40\u0e1e\u0e32\u0e30\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15  \u0e42\u0e1b\u0e23\u0e14\u0e41\u0e08\u0e49\u0e07\u0e1c\u0e39\u0e49\u0e14\u0e39\u0e41\u0e25\u0e23\u0e30\u0e1a\u0e1a\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e01\u0e33\u0e2b\u0e19\u0e14\u0e04\u0e2d\u0e19\u0e1f\u0e34\u0e01\u0e43\u0e2b\u0e49\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e43\u0e2b\u0e49\u0e17\u0e33\u0e07\u0e32\u0e19\u0e01\u0e31\u0e1a OAuth2.0

+missing_fetch_params=\u0e1e\u0e32\u0e23\u0e32\u0e21\u0e34\u0e40\u0e15\u0e2d\u0e23\u0e4c\u0e01\u0e32\u0e23\u0e14\u0e36\u0e07\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e17\u0e35\u0e48\u0e08\u0e33\u0e40\u0e1b\u0e47\u0e19\u0e15\u0e49\u0e2d\u0e07\u0e21\u0e35\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49\u0e2b\u0e32\u0e22\u0e44\u0e1b : {0}

+missing_fetch_params.explanation=\u0e40\u0e21\u0e18\u0e2d\u0e14 fetch() \u0e16\u0e39\u0e01\u0e40\u0e23\u0e35\u0e22\u0e01\u0e14\u0e49\u0e27\u0e22\u0e1e\u0e32\u0e23\u0e32\u0e21\u0e34\u0e40\u0e15\u0e2d\u0e23\u0e4c\u0e17\u0e35\u0e48\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49

+missing_server_response=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e36\u0e49\u0e19\u0e43\u0e19\u0e23\u0e30\u0e2b\u0e27\u0e48\u0e32\u0e07\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e02\u0e2d\u0e07\u0e1c\u0e39\u0e49\u0e43\u0e2b\u0e49\u0e1a\u0e23\u0e34\u0e01\u0e32\u0e23 OAuth2.0 : {0}

+missing_server_response.explanation=\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e17\u0e35\u0e48\u0e2a\u0e23\u0e49\u0e32\u0e07 OAuth2Request \u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07\u0e41\u0e15\u0e48\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e2d\u0e2d\u0e01\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e2b\u0e23\u0e37\u0e2d\u0e02\u0e2d\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e1c\u0e39\u0e49\u0e43\u0e2b\u0e49\u0e1a\u0e23\u0e34\u0e01\u0e32\u0e23

+no_response_handler=\u0e15\u0e31\u0e27\u0e08\u0e31\u0e14\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e15\u0e48\u0e2d\u0e44\u0e1b\u0e19\u0e35\u0e49 : {0}

+no_response_handler.explanation=\u0e15\u0e31\u0e27\u0e08\u0e31\u0e14\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e44\u0e21\u0e48\u0e21\u0e35\u0e2d\u0e22\u0e39\u0e48\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e41\u0e25\u0e30\u0e08\u0e38\u0e14\u0e1b\u0e25\u0e32\u0e22\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19 AuthorizationEndpointResponseHandler \u0e2b\u0e23\u0e37\u0e2d TokenEndpointResponseHandler

+no_gadget_spec=\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e04\u0e49\u0e19\u0e2b\u0e32\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e08\u0e33\u0e40\u0e1e\u0e32\u0e30\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a {0}

+no_gadget_spec.explanation=\u0e44\u0e21\u0e48\u0e1e\u0e1a\u0e02\u0e49\u0e2d\u0e21\u0e39\u0e25\u0e08\u0e33\u0e40\u0e1e\u0e32\u0e30\u0e40\u0e01\u0e35\u0e48\u0e22\u0e27\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15  \u0e42\u0e1b\u0e23\u0e14\u0e41\u0e08\u0e49\u0e07\u0e1c\u0e39\u0e49\u0e14\u0e39\u0e41\u0e25\u0e23\u0e30\u0e1a\u0e1a\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e01\u0e33\u0e2b\u0e19\u0e14\u0e04\u0e2d\u0e19\u0e1f\u0e34\u0e01\u0e43\u0e2b\u0e49\u0e01\u0e31\u0e1a\u0e41\u0e01\u0e14\u0e40\u0e08\u0e47\u0e15\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e43\u0e2b\u0e49\u0e17\u0e33\u0e07\u0e32\u0e19\u0e01\u0e31\u0e1a OAuth2.0

+refresh_token_problem=\u0e01\u0e32\u0e23\u0e23\u0e35\u0e40\u0e1f\u0e23\u0e0a\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e16\u0e39\u0e01\u0e41\u0e25\u0e01\u0e40\u0e1b\u0e25\u0e35\u0e48\u0e22\u0e19\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a access_token : {0}

+refresh_token_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e13\u0e30\u0e41\u0e25\u0e01\u0e40\u0e1b\u0e25\u0e35\u0e48\u0e22\u0e19\u0e01\u0e32\u0e23\u0e23\u0e35\u0e40\u0e1f\u0e23\u0e0a\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19

+secret_encryption_problem=\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e17\u0e35\u0e48\u0e2a\u0e33\u0e04\u0e31\u0e0d\u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e40\u0e02\u0e49\u0e32\u0e23\u0e2b\u0e31\u0e2a\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e43\u0e2b\u0e49\u0e22\u0e31\u0e07\u0e04\u0e07\u0e2d\u0e22\u0e39\u0e48 : {0}

+secret_encryption_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e40\u0e21\u0e37\u0e48\u0e2d\u0e40\u0e01\u0e47\u0e1a OAuth2.0 \u0e17\u0e35\u0e48\u0e21\u0e35\u0e04\u0e27\u0e32\u0e21\u0e2a\u0e31\u0e21\u0e1e\u0e31\u0e19\u0e18\u0e4c\u0e01\u0e31\u0e1a\u0e42\u0e21\u0e14\u0e39\u0e25\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e23\u0e2b\u0e31\u0e2a\u0e17\u0e35\u0e48\u0e08\u0e31\u0e14\u0e40\u0e15\u0e23\u0e35\u0e22\u0e21\u0e44\u0e27\u0e49

+access_denied=\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e16\u0e39\u0e01\u0e1b\u0e0f\u0e34\u0e40\u0e2a\u0e18

+access_denied.explanation=\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49\u0e1b\u0e0f\u0e34\u0e40\u0e2a\u0e18\u0e2b\u0e23\u0e37\u0e2d\u0e22\u0e01\u0e40\u0e25\u0e34\u0e01\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e01\u0e31\u0e1a\u0e1c\u0e39\u0e49\u0e43\u0e2b\u0e49\u0e1a\u0e23\u0e34\u0e01\u0e32\u0e23

+invalid_client=\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 : {0}

+invalid_client.explanation= \u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e44\u0e14\u0e49 \u0e0b\u0e36\u0e48\u0e07\u0e2d\u0e32\u0e08\u0e40\u0e1b\u0e47\u0e19\u0e40\u0e1e\u0e23\u0e32\u0e30\u0e44\u0e21\u0e48\u0e23\u0e39\u0e49\u0e08\u0e31\u0e01\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c \u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e2a\u0e2d\u0e14\u0e41\u0e17\u0e23\u0e01\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e44\u0e27\u0e49 \u0e2b\u0e23\u0e37\u0e2d\u0e44\u0e21\u0e48\u0e2a\u0e19\u0e31\u0e1a\u0e2a\u0e19\u0e38\u0e19\u0e40\u0e21\u0e18\u0e2d\u0e14\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19

+invalid_grant=\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 : {0}

+invalid_grant.explanation=\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 \u0e2b\u0e21\u0e14\u0e2d\u0e32\u0e22\u0e38 \u0e40\u0e23\u0e35\u0e22\u0e01\u0e04\u0e37\u0e19 \u0e44\u0e21\u0e48\u0e15\u0e23\u0e07\u0e01\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e1b\u0e25\u0e35\u0e48\u0e22\u0e19\u0e17\u0e34\u0e28\u0e17\u0e32\u0e07 URl \u0e17\u0e35\u0e48\u0e43\u0e0a\u0e49\u0e43\u0e19\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c \u0e2b\u0e23\u0e37\u0e2d\u0e43\u0e0a\u0e49\u0e01\u0e31\u0e1a\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e2d\u0e37\u0e48\u0e19 \u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e2a\u0e2d\u0e14\u0e41\u0e17\u0e23\u0e01\u0e42\u0e04\u0e49\u0e14\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c \u0e2b\u0e19\u0e31\u0e07\u0e2a\u0e37\u0e2d\u0e23\u0e31\u0e1a\u0e23\u0e2d\u0e07\u0e02\u0e2d\u0e07\u0e40\u0e08\u0e49\u0e32\u0e02\u0e2d\u0e07\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a \u0e2b\u0e23\u0e37\u0e2d\u0e2b\u0e19\u0e31\u0e07\u0e2a\u0e37\u0e2d\u0e23\u0e31\u0e1a\u0e23\u0e2d\u0e07\u0e02\u0e2d\u0e07\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c

+invalid_request=\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 : {0}

+invalid_request.explanation=\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e44\u0e21\u0e48\u0e21\u0e35\u0e1e\u0e32\u0e23\u0e32\u0e21\u0e34\u0e40\u0e15\u0e2d\u0e23\u0e4c\u0e17\u0e35\u0e48\u0e15\u0e49\u0e2d\u0e07\u0e01\u0e32\u0e23 \u0e2a\u0e2d\u0e14\u0e41\u0e17\u0e23\u0e01\u0e04\u0e48\u0e32\u0e1e\u0e32\u0e23\u0e32\u0e21\u0e34\u0e40\u0e15\u0e2d\u0e23\u0e4c\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e2a\u0e19\u0e31\u0e1a\u0e2a\u0e19\u0e38\u0e19 \u0e17\u0e33\u0e0b\u0e49\u0e33\u0e1e\u0e32\u0e23\u0e32\u0e21\u0e34\u0e40\u0e15\u0e2d\u0e23\u0e4c \u0e2a\u0e2d\u0e14\u0e41\u0e17\u0e23\u0e01\u0e2b\u0e19\u0e31\u0e07\u0e2a\u0e37\u0e2d\u0e23\u0e31\u0e1a\u0e23\u0e2d\u0e07\u0e08\u0e33\u0e19\u0e27\u0e19\u0e21\u0e32\u0e01 \u0e43\u0e0a\u0e49\u0e1b\u0e23\u0e30\u0e42\u0e22\u0e0a\u0e19\u0e4c\u0e08\u0e32\u0e01\u0e01\u0e25\u0e44\u0e01\u0e15\u0e31\u0e49\u0e07\u0e41\u0e15\u0e48\u0e2b\u0e19\u0e36\u0e48\u0e07\u0e41\u0e1a\u0e1a\u0e02\u0e36\u0e49\u0e19\u0e44\u0e1b\u0e2a\u0e33\u0e2b\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e02\u0e2d\u0e07\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c \u0e2b\u0e23\u0e37\u0e2d\u0e1c\u0e34\u0e14\u0e23\u0e39\u0e1b\u0e41\u0e1a\u0e1a

+invalid_scope=\u0e02\u0e2d\u0e1a\u0e40\u0e02\u0e15\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 : {0}

+invalid_scope.explanation=\u0e02\u0e2d\u0e1a\u0e40\u0e02\u0e15\u0e17\u0e35\u0e48\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e21\u0e48\u0e16\u0e39\u0e01\u0e15\u0e49\u0e2d\u0e07 \u0e44\u0e21\u0e48\u0e23\u0e39\u0e49\u0e08\u0e31\u0e01 \u0e1c\u0e34\u0e14\u0e23\u0e39\u0e1b\u0e41\u0e1a\u0e1a \u0e2b\u0e23\u0e37\u0e2d\u0e40\u0e01\u0e34\u0e19\u0e02\u0e2d\u0e1a\u0e40\u0e02\u0e15\u0e17\u0e35\u0e48\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e44\u0e27\u0e49\u0e42\u0e14\u0e22\u0e40\u0e08\u0e49\u0e32\u0e02\u0e2d\u0e07\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a

+server_error=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e01\u0e31\u0e1a\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c: {0}

+server_error.explanation=\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e1e\u0e1a\u0e40\u0e07\u0e37\u0e48\u0e2d\u0e19\u0e44\u0e02\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e04\u0e32\u0e14\u0e04\u0e34\u0e14\u0e44\u0e27\u0e49\u0e0b\u0e36\u0e48\u0e07\u0e16\u0e39\u0e01\u0e1b\u0e01\u0e1b\u0e49\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e01\u0e32\u0e23\u0e17\u0e33\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e43\u0e2b\u0e49\u0e40\u0e2a\u0e23\u0e47\u0e08\u0e2a\u0e34\u0e49\u0e19

+server_rejected_request=\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e16\u0e39\u0e01\u0e1b\u0e0f\u0e34\u0e40\u0e2a\u0e18\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d : statuscode = {0}

+server_rejected_request.explanation=\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e2a\u0e23\u0e49\u0e32\u0e07 OAuth2Request \u0e41\u0e15\u0e48\u0e1c\u0e39\u0e49\u0e43\u0e2b\u0e49\u0e1a\u0e23\u0e34\u0e01\u0e32\u0e23\u0e1b\u0e0f\u0e34\u0e40\u0e2a\u0e18

+temporarily_unavailable=\u0e40\u0e0b\u0e2d\u0e23\u0e4c\u0e27\u0e34\u0e2a\u0e44\u0e21\u0e48\u0e1e\u0e23\u0e49\u0e2d\u0e21\u0e43\u0e0a\u0e49\u0e07\u0e32\u0e19\u0e0a\u0e31\u0e48\u0e27\u0e04\u0e23\u0e32\u0e27 : {0}

+temporarily_unavailable.explanation=\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e08\u0e31\u0e14\u0e01\u0e32\u0e23\u0e01\u0e31\u0e1a\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e14\u0e49\u0e40\u0e19\u0e37\u0e48\u0e2d\u0e07\u0e08\u0e32\u0e01\u0e42\u0e2d\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e42\u0e2b\u0e25\u0e14\u0e2b\u0e23\u0e37\u0e2d\u0e2d\u0e22\u0e39\u0e48\u0e43\u0e19\u0e42\u0e2b\u0e21\u0e14\u0e01\u0e32\u0e23\u0e1a\u0e33\u0e23\u0e38\u0e07\u0e23\u0e31\u0e01\u0e29\u0e32

+token_response_problem=\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e08\u0e32\u0e01\u0e08\u0e38\u0e14\u0e1b\u0e25\u0e32\u0e22\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25 : {0}

+token_response_problem.explanation=\u0e08\u0e38\u0e14\u0e1b\u0e25\u0e32\u0e22\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e2a\u0e48\u0e07\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e17\u0e35\u0e48\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e1b\u0e23\u0e30\u0e21\u0e27\u0e25\u0e1c\u0e25\u0e44\u0e14\u0e49

+unauthorized_client=\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e16\u0e39\u0e01\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c : {0}

+unauthorized_client.explanation=\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e16\u0e39\u0e01\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e42\u0e14\u0e22\u0e43\u0e0a\u0e49\u0e40\u0e21\u0e18\u0e2d\u0e14\u0e19\u0e35\u0e49

+unsupported_grant_type=\u0e1b\u0e23\u0e30\u0e40\u0e20\u0e17\u0e02\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e2b\u0e32\u0e01\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e2a\u0e19\u0e31\u0e1a\u0e2a\u0e19\u0e38\u0e19 : {0}

+unsupported_grant_type.explanation=\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e44\u0e21\u0e48\u0e2a\u0e19\u0e31\u0e1a\u0e2a\u0e19\u0e38\u0e19\u0e01\u0e32\u0e23\u0e02\u0e2d\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e42\u0e14\u0e22\u0e43\u0e0a\u0e49\u0e40\u0e21\u0e18\u0e2d\u0e14\u0e19\u0e35\u0e49

+unsupported_response_type=\u0e1b\u0e23\u0e30\u0e40\u0e20\u0e17\u0e01\u0e32\u0e23\u0e15\u0e2d\u0e1a\u0e01\u0e25\u0e31\u0e1a\u0e44\u0e21\u0e48\u0e44\u0e14\u0e49\u0e23\u0e31\u0e1a\u0e01\u0e32\u0e23\u0e2a\u0e19\u0e31\u0e1a\u0e2a\u0e19\u0e38\u0e19: {0}

+unsupported_response_type.explanation=\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e44\u0e21\u0e48\u0e2a\u0e19\u0e31\u0e1a\u0e2a\u0e19\u0e38\u0e19\u0e01\u0e32\u0e23\u0e02\u0e2d\u0e23\u0e31\u0e1a\u0e42\u0e04\u0e49\u0e14\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c\u0e42\u0e14\u0e22\u0e43\u0e0a\u0e49\u0e40\u0e21\u0e18\u0e2d\u0e14\u0e19\u0e35\u0e49

+mac_token_problem=\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19 mac \u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d : {0}

+mac_token_problem.explanation=\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19 mac \u0e1a\u0e19\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e44\u0e1b\u0e22\u0e31\u0e07\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a

+bearer_token_problem=\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19 bearer \u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d : {0}

+bearer_token_problem.explanation=\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19 bearer \u0e1a\u0e19\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e43\u0e2b\u0e49\u0e01\u0e31\u0e1a\u0e40\u0e0b\u0e34\u0e23\u0e4c\u0e1f\u0e40\u0e27\u0e2d\u0e23\u0e4c\u0e23\u0e35\u0e0b\u0e2d\u0e23\u0e4c\u0e2a

+authentication_problem=\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e02\u0e2d\u0e07\u0e44\u0e04\u0e25\u0e40\u0e2d\u0e47\u0e19\u0e15\u0e4c\u0e01\u0e33\u0e25\u0e31\u0e07\u0e16\u0e39\u0e01\u0e40\u0e1e\u0e34\u0e48\u0e21 : {0}

+authentication_problem.explanation=\u0e2a\u0e48\u0e27\u0e19\u0e2b\u0e31\u0e27\u0e02\u0e2d\u0e07\u0e01\u0e32\u0e23\u0e1e\u0e34\u0e2a\u0e39\u0e08\u0e19\u0e4c\u0e15\u0e31\u0e27\u0e15\u0e19\u0e44\u0e21\u0e48\u0e2a\u0e32\u0e21\u0e32\u0e23\u0e16\u0e40\u0e1e\u0e34\u0e48\u0e21\u0e44\u0e1b\u0e22\u0e31\u0e07\u0e04\u0e33\u0e23\u0e49\u0e2d\u0e07\u0e02\u0e2d\u0e44\u0e14\u0e49

+unknown_problem=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e17\u0e35\u0e48\u0e44\u0e21\u0e48\u0e23\u0e39\u0e49\u0e08\u0e31\u0e01\u0e02\u0e36\u0e49\u0e19 : {0}

+unknown_problem.explanation=\u0e41\u0e08\u0e49\u0e07\u0e43\u0e2b\u0e49\u0e1c\u0e39\u0e49\u0e14\u0e39\u0e41\u0e25\u0e23\u0e30\u0e1a\u0e1a\u0e02\u0e2d\u0e07\u0e04\u0e38\u0e13\u0e17\u0e23\u0e32\u0e1a\u0e40\u0e1e\u0e37\u0e48\u0e2d\u0e15\u0e23\u0e27\u0e08\u0e2a\u0e2d\u0e1a\u0e1b\u0e31\u0e0d\u0e2b\u0e32

+code_grant_problem=\u0e01\u0e32\u0e23\u0e40\u0e02\u0e49\u0e32\u0e16\u0e36\u0e07\u0e42\u0e17\u0e40\u0e04\u0e47\u0e19\u0e17\u0e35\u0e48\u0e16\u0e39\u0e01\u0e02\u0e2d\u0e23\u0e31\u0e1a\u0e08\u0e32\u0e01\u0e42\u0e04\u0e49\u0e14\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c : {0}

+code_grant_problem.explanation=\u0e40\u0e01\u0e34\u0e14\u0e02\u0e49\u0e2d\u0e1c\u0e34\u0e14\u0e1e\u0e25\u0e32\u0e14\u0e02\u0e36\u0e49\u0e19\u0e43\u0e19\u0e23\u0e30\u0e2b\u0e27\u0e48\u0e32\u0e07\u0e42\u0e1f\u0e25\u0e27\u0e4c\u0e42\u0e04\u0e49\u0e14\u0e01\u0e32\u0e23\u0e43\u0e2b\u0e49\u0e2a\u0e34\u0e17\u0e18\u0e34\u0e4c

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_tr.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_tr.properties
new file mode 100644
index 0000000..f47b4cc
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_tr.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} bir hata ile kar\u015f\u0131la\u015ft\u0131 :

+authorization_code_problem=Yetkilendirme kodu, eri\u015fim belirteci i\u00e7in de\u011fi\u015f toku\u015f ediliyor : {0}

+authorization_code_problem.explanation=Eri\u015fim belirtecine ili\u015fkin yetkilendirme kodu de\u011fi\u015f toku\u015f edilirken bir hata olu\u015ftu.

+authorize_problem={0} i\u00e7in yetkilendirme i\u015flemi ba\u015flat\u0131l\u0131yor.

+authorize_problem.explanation=Yetkilendirme i\u015flemi ba\u015flat\u0131l\u0131rken bir hata olu\u015ftu.

+callback_problem=Y\u00f6nlendirme yan\u0131t\u0131 i\u015fleniyor : {0}

+callback_problem.explanation=Hizmet sa\u011flay\u0131c\u0131dan al\u0131nan y\u00f6nlendirme yan\u0131t\u0131 i\u015flenirken bir hata olu\u015ftu.

+client_credentials_problem=Eri\u015fim_belirteci, istemci i\u00e7in \u015fu istemci kimlik bilgisiyle al\u0131n\u0131yor : {0}

+client_credentials_problem.explanation=\u0130stemci_kimlik_bilgileri ak\u0131\u015f\u0131nda eri\u015fim belirteci al\u0131n\u0131rken bir hata olu\u015ftu.

+fetch_init_problem=OAuth2Request ba\u015flat\u0131l\u0131yor: {0}

+fetch_init_problem.explanation=OAuth2Request getirme i\u015flemi ba\u015flat\u0131l\u0131rken d\u00fc\u015f\u00fck d\u00fczeyli bir hata olu\u015ftu.

+fetch_problem=OAuth2Request.fetch() y\u00fcr\u00fct\u00fcl\u00fcyor : {0}

+fetch_problem.explanation=OAuth2Request getirme komutu verilirken bir hata olu\u015ftu.

+gadget_spec_problem=\u015eu ara\u00e7 belirtimi i\u015fleniyor : {0}

+gadget_spec_problem.explanation=Bir ara\u00e7 belirtimi bulunamad\u0131.  Y\u00f6neticinizden, arac\u0131n\u0131z\u0131 OAuth2.0 ile \u00e7al\u0131\u015facak \u015fekilde yap\u0131land\u0131rmas\u0131n\u0131 isteyin.

+get_oauth2_accessor_problem=OAuth2Request i\u00e7in bir OAuth2Accessor al\u0131n\u0131yor : {0}

+get_oauth2_accessor_problem.explanation=Bir hata olu\u015ftu. Y\u00f6neticinizden bu ara\u00e7 ve hizmet i\u00e7in bir OAuth2.0 \u0130stemci ba\u011flamas\u0131 olu\u015fturmas\u0131n\u0131 isteyin.

+lookup_spec_problem=Ara\u00e7 belirtimi al\u0131n\u0131yor : {0}

+lookup_spec_problem.explanation=Bir ara\u00e7 belirtimi bulunamad\u0131.  Y\u00f6neticinizden, arac\u0131n\u0131z\u0131 OAuth2.0 ile \u00e7al\u0131\u015facak \u015fekilde yap\u0131land\u0131rmas\u0131n\u0131 isteyin.

+missing_fetch_params=\u015eu zorunlu getirme parametreleri eksik : {0}

+missing_fetch_params.explanation=fetch() y\u00f6ntemi hatal\u0131 parametrelerle \u00e7a\u011fr\u0131ld\u0131.

+missing_server_response=OAuth2.0 hizmet sa\u011flay\u0131c\u0131 yan\u0131t\u0131 s\u0131ras\u0131nda bir hata olu\u015ftu : {0}

+missing_server_response.explanation=Sunucu ge\u00e7erli bir OAuth2Request olu\u015fturdu, ancak iste\u011fi yay\u0131nlayamad\u0131 ya da hizmet sa\u011flay\u0131c\u0131s\u0131ndan ge\u00e7erli bir yan\u0131t alamad\u0131.

+no_response_handler=Bir yan\u0131t i\u015fleyici \u015fu yan\u0131t\u0131 i\u015flemedi : {0}

+no_response_handler.explanation=Yetkilendirme ve belirte\u00e7 u\u00e7 noktalar\u0131n\u0131 i\u015flemek i\u00e7in bir yan\u0131t i\u015fleyici mevcut de\u011fil. AuthorizationEndpointResponseHandler ya da TokenEndpointResponseHandler.

+no_gadget_spec={0} i\u00e7in ara\u00e7 belirtimi bulunam\u0131yor.

+no_gadget_spec.explanation=Bir ara\u00e7 belirtimi bulunamad\u0131.  Y\u00f6neticinizden, arac\u0131n\u0131z\u0131 OAuth2.0 ile \u00e7al\u0131\u015facak \u015fekilde yap\u0131land\u0131rmas\u0131n\u0131 isteyin.

+refresh_token_problem=Yenileme belirteci, eri\u015fim belirteciyle de\u011fi\u015f toku\u015f ediliyor : {0}

+refresh_token_problem.explanation=Yenileme belirteci eri\u015fim belirteciyle de\u011fi\u015f toku\u015f edilirken bir hata olu\u015ftu.

+secret_encryption_problem=Belirte\u00e7 parolas\u0131 s\u00fcreklilik i\u00e7in \u015fifreleniyor : {0}

+secret_encryption_problem.explanation=OAuth2.0 parolalar\u0131 sa\u011flanan \u015fifreleme mod\u00fcl\u00fcyle saklan\u0131rken bir hata olu\u015ftu.

+access_denied=Eri\u015fim reddedildi.

+access_denied.explanation=Kullan\u0131c\u0131, hizmet sa\u011flay\u0131c\u0131s\u0131yla yetkilendirmeyi reddetti ya da iptal etti.

+invalid_client=\u0130stemci ge\u00e7ersiz : {0}

+invalid_client.explanation= \u0130stemci do\u011frulanam\u0131yor. Bu durum, istemcinin bilinmemesinden, istemci kimlik do\u011frulamas\u0131n\u0131n i\u00e7erilmemesinden ya da kimlik do\u011frulama y\u00f6nteminin desteklenmemesinden kaynaklan\u0131yor olabilir.

+invalid_grant=Yetkilendirme izni ge\u00e7ersiz : {0}

+invalid_grant.explanation=Yetkilendirme izni ge\u00e7ersiz, s\u00fcresi dolmu\u015f, iptal edilmi\u015f, yetkilendirme iste\u011finde kullan\u0131lan y\u00f6nlendirme URI'si ile uyu\u015fmuyor ya da ba\u015fka bir istemciye verildi. Verilen yetkilendirme, yetkilendirme kodunu, kaynak sahibinin kimlik bilgilerini ya da istemcinin kimlik bilgilerini i\u00e7erebilir.

+invalid_request=\u0130stek ge\u00e7ersiz : {0}

+invalid_request.explanation=\u0130stek ge\u00e7ersiz. Bu durum, zorunlu bir parametrenin eksik olmas\u0131ndan, iste\u011fin desteklenmeyen bir parametre de\u011feri i\u00e7ermesinden, bir parametrenin tekrar edilmesinden, birden \u00e7ok kimlik bilgisi bulunmas\u0131ndan, istemci kimlik do\u011frulamas\u0131 i\u00e7in birden fazla mekanizma kullan\u0131lmas\u0131ndan ya da iste\u011fin bozuk olmas\u0131ndan kaynaklan\u0131yor olabilir.

+invalid_scope=Kapsam ge\u00e7ersiz : {0}

+invalid_scope.explanation=\u0130stenen kapsam ge\u00e7ersiz, bilinmiyor, bozuk ya da kaynak sahibi taraf\u0131ndan verilen kapsam\u0131 a\u015f\u0131yor.

+server_error=Bir sunucu hatas\u0131 olu\u015ftu: {0}

+server_error.explanation=Yetkilendirme sunucusu, iste\u011fi yerine getirmesini engelleyen beklenmedik bir ko\u015fulla kar\u015f\u0131la\u015ft\u0131.

+server_rejected_request=Sunucu iste\u011fi reddetti : statuscode = {0}

+server_rejected_request.explanation=Sunucu bir OAuth2Request olu\u015fturdu, ancak hizmet sa\u011flay\u0131c\u0131s\u0131 bunu reddetti.

+temporarily_unavailable=Hizmet ge\u00e7ici olarak kullan\u0131lam\u0131yor : {0}

+temporarily_unavailable.explanation=Yetkilendirme sunucusu, ge\u00e7ici olarak a\u015f\u0131r\u0131 y\u00fcklendi\u011fi ya da bak\u0131m modunda oldu\u011fu i\u00e7in iste\u011fi i\u015fleyemiyor.

+token_response_problem=Belirte\u00e7 u\u00e7 noktas\u0131n\u0131n yan\u0131t\u0131 i\u015fleniyor : {0}

+token_response_problem.explanation=Belirte\u00e7 u\u00e7 noktas\u0131, sunucunun i\u015fleyemedi\u011fi bir yan\u0131t g\u00f6nderdi.

+unauthorized_client=\u0130stemci yetkili de\u011fil : {0}

+unauthorized_client.explanation=\u0130stemci, bu y\u00f6ntemi kullanarak bir eri\u015fim belirteci iste\u011finde bulunmak i\u00e7in yetkili de\u011fil.

+unsupported_grant_type=\u0130zin t\u00fcr\u00fc desteklenmiyor : {0}

+unsupported_grant_type.explanation=Yetkilendirme sunucusu, bu y\u00f6ntem kullan\u0131larak bir eri\u015fim belirteci edinilmesini desteklemiyor.

+unsupported_response_type=Yan\u0131t t\u00fcr\u00fc desteklenmiyor: {0}

+unsupported_response_type.explanation=Yetkilendirme sunucusu, bu y\u00f6ntem kullan\u0131larak bir yetkilendirme kodu edinilmesini desteklemiyor.

+mac_token_problem=Mac belirteci isteniyor : {0}

+mac_token_problem.explanation=\u0130stekteki mac belirteci, kaynak sunucuya eklenemedi.

+bearer_token_problem=Ta\u015f\u0131y\u0131c\u0131 belirteci isteniyor : {0}

+bearer_token_problem.explanation=\u0130stekteki ta\u015f\u0131y\u0131c\u0131 belirteci, kaynak sunucuya eklenemedi.

+authentication_problem=\u0130stemci kimlik do\u011frulamas\u0131 ekleniyor : {0}

+authentication_problem.explanation=Kimlik do\u011frulama \u00fcstbilgileri iste\u011fe eklenemedi.

+unknown_problem=Bilinmeyen bir hata olu\u015ftu : {0}

+unknown_problem.explanation=Sistem y\u00f6neticinizden sorunu ara\u015ft\u0131rmas\u0131n\u0131 isteyin.

+code_grant_problem=Eri\u015fim belirteci yetkilendirme kodundan al\u0131n\u0131yor : {0}

+code_grant_problem.explanation=Yetkilendirme kodu ak\u0131\u015f\u0131 s\u0131ras\u0131nda bir hata olu\u015ftu.

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_zh.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_zh.properties
new file mode 100644
index 0000000..a285990
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_zh.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} \u9047\u5230\u9519\u8bef\uff1a

+authorization_code_problem=\u6b63\u5728\u4ea4\u6362\u8bbf\u95ee\u4ee4\u724c\u7684\u6388\u6743\u7801\uff1a{0}

+authorization_code_problem.explanation=\u4ea4\u6362 access_token \u7684\u6388\u6743\u7801\u65f6\u53d1\u751f\u9519\u8bef\u3002

+authorize_problem=\u6b63\u5728\u542f\u52a8 {0} \u7684\u6388\u6743\u8fc7\u7a0b\u3002

+authorize_problem.explanation=\u521d\u59cb\u5316\u6388\u6743\u8fc7\u7a0b\u65f6\u53d1\u751f\u9519\u8bef\u3002

+callback_problem=\u6b63\u5728\u5904\u7406\u91cd\u5b9a\u5411\u54cd\u5e94\uff1a{0}

+callback_problem.explanation=\u5904\u7406\u6765\u81ea\u670d\u52a1\u63d0\u4f9b\u7a0b\u5e8f\u7684\u91cd\u5b9a\u5411\u54cd\u5e94\u65f6\u53d1\u751f\u9519\u8bef\u3002

+client_credentials_problem=\u6b63\u5728\u4f7f\u7528\u4ee5\u4e0b\u5ba2\u6237\u673a\u51ed\u8bc1\u6765\u68c0\u7d22\u5ba2\u6237\u673a\u7684 access_token\uff1a{0}

+client_credentials_problem.explanation=\u5728 client_credentials \u6d41\u4e2d\u68c0\u7d22\u8bbf\u95ee\u4ee4\u724c\u65f6\u53d1\u751f\u9519\u8bef\u3002

+fetch_init_problem=\u6b63\u5728\u521d\u59cb\u5316 OAuth2Request\uff1a{0}

+fetch_init_problem.explanation=\u521d\u59cb\u5316 OAuth2Request \u63d0\u53d6\u65f6\u53d1\u751f\u4f4e\u7ea7\u9519\u8bef\u3002

+fetch_problem=\u6b63\u5728\u6267\u884c OAuth2Request.fetch()\uff1a{0}

+fetch_problem.explanation=\u53d1\u51fa OAuth2Request \u63d0\u53d6\u65f6\u53d1\u751f\u9519\u8bef\u3002

+gadget_spec_problem=\u6b63\u5728\u5904\u7406\u4ee5\u4e0b\u5c0f\u914d\u4ef6\u89c4\u8303\uff1a{0}

+gadget_spec_problem.explanation=\u627e\u4e0d\u5230\u5c0f\u914d\u4ef6\u89c4\u8303\u3002  \u8bf7\u8981\u6c42\u7ba1\u7406\u5458\u5c06\u5c0f\u914d\u4ef6\u914d\u7f6e\u4e3a\u914d\u5408 OAuth2.0 \u5de5\u4f5c\u3002

+get_oauth2_accessor_problem=\u6b63\u5728\u83b7\u53d6 OAuth2Request \u7684 OAuth2Accessor\uff1a{0}

+get_oauth2_accessor_problem.explanation=\u53d1\u751f\u9519\u8bef\u3002 \u8bf7\u8981\u6c42\u7ba1\u7406\u5458\u4e3a\u6b64\u5c0f\u914d\u4ef6\u548c\u670d\u52a1\u521b\u5efa OAuth2.0 \u5ba2\u6237\u673a\u7ed1\u5b9a\u3002

+lookup_spec_problem=\u6b63\u5728\u68c0\u7d22\u5c0f\u914d\u4ef6\u89c4\u8303\uff1a{0}

+lookup_spec_problem.explanation=\u627e\u4e0d\u5230\u5c0f\u914d\u4ef6\u89c4\u8303\u3002  \u8bf7\u8981\u6c42\u7ba1\u7406\u5458\u5c06\u5c0f\u914d\u4ef6\u914d\u7f6e\u4e3a\u914d\u5408 OAuth2.0 \u5de5\u4f5c\u3002

+missing_fetch_params=\u7f3a\u5c11\u4e0b\u5217\u5fc5\u9700\u7684\u63d0\u53d6\u53c2\u6570\uff1a{0}

+missing_fetch_params.explanation=\u8c03\u7528 fetch() \u65b9\u6cd5\u65f6\u6307\u5b9a\u7684\u53c2\u6570\u4e0d\u6b63\u786e\u3002

+missing_server_response=OAuth2.0 \u670d\u52a1\u63d0\u4f9b\u7a0b\u5e8f\u54cd\u5e94\u671f\u95f4\u53d1\u751f\u9519\u8bef\uff1a{0}

+missing_server_response.explanation=\u670d\u52a1\u5668\u521b\u5efa\u4e86\u6709\u6548\u7684 OAuth2Request\uff0c\u4f46\u65e0\u6cd5\u53d1\u51fa\u8bf7\u6c42\u6216\u8005\u65e0\u6cd5\u83b7\u53d6\u6765\u81ea\u670d\u52a1\u63d0\u4f9b\u7a0b\u5e8f\u7684\u6709\u6548\u54cd\u5e94\u3002

+no_response_handler=\u54cd\u5e94\u5904\u7406\u7a0b\u5e8f\u672a\u5904\u7406\u4ee5\u4e0b\u54cd\u5e94\uff1a{0}

+no_response_handler.explanation=\u54cd\u5e94\u5904\u7406\u7a0b\u5e8f\u4e0d\u5b58\u5728\uff0c\u65e0\u6cd5\u5904\u7406\u6388\u6743\u548c\u4ee4\u724c\u7aef\u70b9\u3002 AuthorizationEndpointResponseHandler \u6216 TokenEndpointResponseHandler\u3002

+no_gadget_spec=\u627e\u4e0d\u5230 {0} \u7684\u5c0f\u914d\u4ef6\u89c4\u8303\u3002

+no_gadget_spec.explanation=\u627e\u4e0d\u5230\u5c0f\u914d\u4ef6\u89c4\u8303\u3002  \u8bf7\u8981\u6c42\u7ba1\u7406\u5458\u5c06\u5c0f\u914d\u4ef6\u914d\u7f6e\u4e3a\u914d\u5408 OAuth2.0 \u5de5\u4f5c\u3002

+refresh_token_problem=\u6b63\u5728\u4ea4\u6362 access_token \u7684\u5237\u65b0\u4ee4\u724c\uff1a{0}

+refresh_token_problem.explanation=\u4ea4\u6362\u8bbf\u95ee\u4ee4\u724c\u7684\u5237\u65b0\u4ee4\u724c\u65f6\u53d1\u751f\u9519\u8bef\u3002

+secret_encryption_problem=\u6b63\u5728\u5bf9\u4ee4\u724c\u5bc6\u7801\u63d0\u793a\u8fdb\u884c\u52a0\u5bc6\u4ee5\u8fdb\u884c\u4fdd\u5b58\uff1a{0}

+secret_encryption_problem.explanation=\u4f7f\u7528\u63d0\u4f9b\u7684\u52a0\u5bc6\u6a21\u5757\u5b58\u50a8 OAuth2.0 \u5bc6\u7801\u63d0\u793a\u65f6\u53d1\u751f\u9519\u8bef\u3002

+access_denied=\u8bbf\u95ee\u88ab\u62d2\u7edd\u3002

+access_denied.explanation=\u7528\u6237\u62d2\u7edd\u6216\u53d6\u6d88\u4e86\u670d\u52a1\u63d0\u4f9b\u7a0b\u5e8f\u7684\u6388\u6743\u3002

+invalid_client=\u5ba2\u6237\u673a\u65e0\u6548\uff1a{0}

+invalid_client.explanation= \u65e0\u6cd5\u8ba4\u8bc1\u5ba2\u6237\u673a\uff0c\u8fd9\u53ef\u80fd\u662f\u56e0\u4e3a\u5ba2\u6237\u673a\u672a\u77e5\u3001\u672a\u5305\u62ec\u5ba2\u6237\u673a\u8ba4\u8bc1\u4fe1\u606f\u6216\u8005\u8ba4\u8bc1\u65b9\u6cd5\u4e0d\u53d7\u652f\u6301\u3002

+invalid_grant=\u6388\u4e88\u7684\u6743\u9650\u65e0\u6548\uff1a{0}

+invalid_grant.explanation=\u6388\u4e88\u7684\u6743\u9650\u65e0\u6548\u3001\u5df2\u5230\u671f\u3001\u5df2\u88ab\u64a4\u9500\u3001\u4e0e\u6388\u6743\u8bf7\u6c42\u4e2d\u4f7f\u7528\u7684\u91cd\u5b9a\u5411 URI \u4e0d\u5339\u914d\u6216\u8005\u662f\u5411\u53e6\u4e00\u5ba2\u6237\u673a\u53d1\u653e\u3002 \u6388\u4e88\u7684\u6743\u9650\u53ef\u4ee5\u5305\u62ec\u6388\u6743\u7801\u3001\u8d44\u6e90\u6240\u6709\u8005\u7684\u51ed\u8bc1\u6216\u5ba2\u6237\u673a\u7684\u51ed\u8bc1\u3002

+invalid_request=\u8bf7\u6c42\u65e0\u6548\uff1a{0}

+invalid_request.explanation=\u8bf7\u6c42\u65e0\u6548\uff0c\u8fd9\u662f\u56e0\u4e3a\u5b83\u7f3a\u5c11\u5fc5\u9700\u53c2\u6570\u3001\u5305\u62ec\u4e86\u4e0d\u53d7\u652f\u6301\u7684\u53c2\u6570\u503c\u3001\u91cd\u590d\u4e86\u67d0\u4e2a\u53c2\u6570\u3001\u5305\u62ec\u4e86\u591a\u4e2a\u51ed\u8bc1\u3001\u4f7f\u7528\u4e86\u591a\u79cd\u673a\u5236\u6765\u8ba4\u8bc1\u5ba2\u6237\u673a\u6216\u8005\u5728\u5176\u4ed6\u65b9\u9762\u683c\u5f0f\u4e0d\u6b63\u786e\u3002

+invalid_scope=\u4f5c\u7528\u57df\u65e0\u6548\uff1a{0}

+invalid_scope.explanation=\u8bf7\u6c42\u7684\u4f5c\u7528\u57df\u65e0\u6548\u3001\u672a\u77e5\u3001\u683c\u5f0f\u4e0d\u6b63\u786e\u6216\u8d85\u51fa\u8d44\u6e90\u6240\u6709\u8005\u6388\u4e88\u7684\u4f5c\u7528\u57df\u3002

+server_error=\u53d1\u751f\u670d\u52a1\u5668\u9519\u8bef\uff1a{0}

+server_error.explanation=\u6388\u6743\u670d\u52a1\u5668\u9047\u5230\u610f\u5916\u60c5\u51b5\uff0c\u5bfc\u81f4\u5b83\u65e0\u6cd5\u6ee1\u8db3\u8bf7\u6c42\u3002

+server_rejected_request=\u670d\u52a1\u5668\u62d2\u7edd\u4e86\u8bf7\u6c42\uff1a\u72b6\u6001\u7801\u4e3a {0}

+server_rejected_request.explanation=\u670d\u52a1\u5668\u521b\u5efa\u4e86 OAuth2Request\uff0c\u4f46\u670d\u52a1\u63d0\u4f9b\u7a0b\u5e8f\u5c06\u5176\u62d2\u7edd\u3002

+temporarily_unavailable=\u670d\u52a1\u6682\u65f6\u4e0d\u53ef\u7528\uff1a{0}

+temporarily_unavailable.explanation=\u6388\u6743\u670d\u52a1\u5668\u6682\u65f6\u8fc7\u8f7d\u6216\u5904\u4e8e\u7ef4\u62a4\u65b9\u5f0f\uff0c\u56e0\u6b64\u65e0\u6cd5\u5904\u7406\u8bf7\u6c42\u3002

+token_response_problem=\u6b63\u5728\u5904\u7406\u6765\u81ea\u4ee4\u724c\u7aef\u70b9\u7684\u54cd\u5e94\uff1a{0}

+token_response_problem.explanation=\u4ee4\u724c\u7aef\u70b9\u53d1\u9001\u4e86\u670d\u52a1\u5668\u65e0\u6cd5\u5904\u7406\u7684\u54cd\u5e94\u3002

+unauthorized_client=\u5ba2\u6237\u673a\u672a\u7ecf\u6388\u6743\uff1a{0}

+unauthorized_client.explanation=\u5ba2\u6237\u673a\u65e0\u6743\u4f7f\u7528\u6b64\u65b9\u6cd5\u6765\u8bf7\u6c42\u83b7\u53d6\u8bbf\u95ee\u4ee4\u724c\u3002

+unsupported_grant_type=\u6388\u6743\u7c7b\u578b\u4e0d\u53d7\u652f\u6301\uff1a{0}

+unsupported_grant_type.explanation=\u6388\u6743\u670d\u52a1\u5668\u4e0d\u652f\u6301\u4f7f\u7528\u6b64\u65b9\u6cd5\u6765\u83b7\u53d6\u8bbf\u95ee\u4ee4\u724c\u3002

+unsupported_response_type=\u54cd\u5e94\u7c7b\u578b\u4e0d\u53d7\u652f\u6301\uff1a{0}

+unsupported_response_type.explanation=\u6388\u6743\u670d\u52a1\u5668\u4e0d\u652f\u6301\u4f7f\u7528\u6b64\u65b9\u6cd5\u6765\u83b7\u53d6\u6388\u6743\u7801\u3002

+mac_token_problem=\u6b63\u5728\u8bf7\u6c42\u83b7\u53d6 MAC \u4ee4\u724c\uff1a{0}

+mac_token_problem.explanation=\u65e0\u6cd5\u5c06\u8be5\u8bf7\u6c42\u4e2d\u7684 MAC \u4ee4\u724c\u6dfb\u52a0\u5230\u8d44\u6e90\u670d\u52a1\u5668\u3002

+bearer_token_problem=\u6b63\u5728\u8bf7\u6c42\u83b7\u53d6\u627f\u8f7d\u8005\u4ee4\u724c\uff1a{0}

+bearer_token_problem.explanation=\u65e0\u6cd5\u5c06\u8be5\u8bf7\u6c42\u4e2d\u7684\u627f\u8f7d\u8005\u4ee4\u724c\u6dfb\u52a0\u5230\u8d44\u6e90\u670d\u52a1\u5668\u3002

+authentication_problem=\u6b63\u5728\u6dfb\u52a0\u5ba2\u6237\u673a\u8ba4\u8bc1\u4fe1\u606f\uff1a{0}

+authentication_problem.explanation=\u65e0\u6cd5\u5c06\u8ba4\u8bc1\u5934\u6dfb\u52a0\u5230\u8bf7\u6c42\u4e2d\u3002

+unknown_problem=\u53d1\u751f\u672a\u77e5\u7684\u9519\u8bef\uff1a{0}

+unknown_problem.explanation=\u8bf7\u8981\u6c42\u7cfb\u7edf\u7ba1\u7406\u5458\u8c03\u67e5\u95ee\u9898\u3002

+code_grant_problem=\u6b63\u5728\u4ece\u6388\u6743\u7801\u4e2d\u83b7\u53d6\u8bbf\u95ee\u4ee4\u724c\uff1a{0}

+code_grant_problem.explanation=\u6388\u6743\u7801\u6d41\u671f\u95f4\u53d1\u751f\u53d1\u751f\u9519\u8bef\u3002

diff --git a/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_zh_TW.properties b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_zh_TW.properties
new file mode 100644
index 0000000..08de111
--- /dev/null
+++ b/trunk/java/gadgets/src/main/resources/org/apache/shindig/gadgets/oauth2/resource_zh_TW.properties
@@ -0,0 +1,85 @@
+# Licensed to the Apache Software Foundation (ASF) under one

+# or more contributor license agreements.  See the NOTICE file

+# distributed with this work for additional information

+# regarding copyright ownership.  The ASF licenses this file

+# to you 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.

+#

+# NLS_ENCODING=UNICODE

+## G11N SA UI

+# NLS_MESSAGEFORMAT_VAR

+message_header= {0} \u9047\u5230\u932f\u8aa4\uff1a

+authorization_code_problem=\u6b63\u5728\u4ea4\u63db\u5b58\u53d6\u8a18\u865f\u7684\u6388\u6b0a\u78bc\uff1a{0}

+authorization_code_problem.explanation=\u4ea4\u63db access_token \u7684\u6388\u6b0a\u78bc\u6642\u767c\u751f\u932f\u8aa4\u3002

+authorize_problem=\u6b63\u5728\u8d77\u59cb {0} \u7684\u6388\u6b0a\u8655\u7406\u7a0b\u5e8f\u3002

+authorize_problem.explanation=\u8d77\u59cb\u6388\u6b0a\u8655\u7406\u7a0b\u5e8f\u6642\u767c\u751f\u932f\u8aa4\u3002

+callback_problem=\u6b63\u5728\u8655\u7406\u91cd\u65b0\u5c0e\u5411\u56de\u61c9\uff1a{0}

+callback_problem.explanation=\u8655\u7406\u4f86\u81ea\u670d\u52d9\u63d0\u4f9b\u8005\u7684\u91cd\u65b0\u5c0e\u5411\u56de\u61c9\u6642\u767c\u751f\u932f\u8aa4\u3002

+client_credentials_problem=\u6b63\u5728\u900f\u904e\u4e0b\u5217\u7528\u6236\u7aef\u8a8d\u8b49\u64f7\u53d6\u7528\u6236\u7aef\u7684 access_token\uff1a{0}

+client_credentials_problem.explanation=\u5728 client_credentials \u6d41\u7a0b\u4e2d\u64f7\u53d6\u5b58\u53d6\u8a18\u865f\u6642\u767c\u751f\u932f\u8aa4\u3002

+fetch_init_problem=\u6b63\u5728\u8d77\u59cb\u8a2d\u5b9a OAuth2Request\uff1a{0}

+fetch_init_problem.explanation=\u8d77\u59cb\u8a2d\u5b9a OAuth2Request \u63d0\u53d6\u6642\u767c\u751f\u4f4e\u968e\u932f\u8aa4\u3002

+fetch_problem=\u6b63\u5728\u57f7\u884c OAuth2Request.fetch()\uff1a{0}

+fetch_problem.explanation=\u767c\u51fa OAuth2Request \u63d0\u53d6\u6642\u767c\u751f\u932f\u8aa4\u3002

+gadget_spec_problem=\u6b63\u5728\u8655\u7406\u4e0b\u5217\u5c0f\u5de5\u5177\u898f\u683c\uff1a{0}

+gadget_spec_problem.explanation=\u627e\u4e0d\u5230\u5c0f\u5de5\u5177\u898f\u683c\u3002  \u8acb\u8981\u6c42\u60a8\u7684\u7ba1\u7406\u8005\u5c07\u5c0f\u5de5\u5177\u914d\u7f6e\u70ba\u4f7f\u7528 OAuth2.0\u3002

+get_oauth2_accessor_problem=\u6b63\u5728\u53d6\u5f97 OAuth2Request \u7684 OAuth2Accessor\uff1a{0}

+get_oauth2_accessor_problem.explanation=\u767c\u751f\u932f\u8aa4\u3002 \u8acb\u8981\u6c42\u60a8\u7684\u7ba1\u7406\u8005\u91dd\u5c0d\u6b64\u5c0f\u5de5\u5177\u8207\u670d\u52d9\u5efa\u7acb\u300cOAuth2.0 \u7528\u6236\u7aef\u300d\u9023\u7d50\u3002

+lookup_spec_problem=\u6b63\u5728\u64f7\u53d6\u5c0f\u5de5\u5177\u898f\u683c\uff1a{0}

+lookup_spec_problem.explanation=\u627e\u4e0d\u5230\u5c0f\u5de5\u5177\u898f\u683c\u3002  \u8acb\u8981\u6c42\u60a8\u7684\u7ba1\u7406\u8005\u5c07\u5c0f\u5de5\u5177\u914d\u7f6e\u70ba\u4f7f\u7528 OAuth2.0\u3002

+missing_fetch_params=\u907a\u6f0f\u4e0b\u5217\u5fc5\u8981\u7684\u63d0\u53d6\u53c3\u6578\uff1a{0}

+missing_fetch_params.explanation=\u5df2\u4f7f\u7528\u4e0d\u6b63\u78ba\u7684\u53c3\u6578\u547c\u53eb fetch() \u65b9\u6cd5\u3002

+missing_server_response=OAuth2.0 \u670d\u52d9\u63d0\u4f9b\u8005\u56de\u61c9\u671f\u9593\u767c\u751f\u932f\u8aa4\uff1a{0}

+missing_server_response.explanation=\u4f3a\u670d\u5668\u5df2\u5efa\u7acb\u6709\u6548\u7684 OAuth2Request\uff0c\u4f46\u7121\u6cd5\u767c\u51fa\u8981\u6c42\u6216\u5f9e\u670d\u52d9\u63d0\u4f9b\u8005\u53d6\u5f97\u6709\u6548\u7684\u56de\u61c9\u3002

+no_response_handler=\u56de\u61c9\u8655\u7406\u7a0b\u5f0f\u672a\u8655\u7406\u4e0b\u5217\u56de\u61c9\uff1a{0}

+no_response_handler.explanation=\u4e0d\u5b58\u5728\u7528\u65bc\u8655\u7406\u6388\u6b0a\u53ca\u8a18\u865f\u7aef\u9ede\u7684\u56de\u61c9\u8655\u7406\u7a0b\u5f0f\u3002 AuthorizationEndpointResponseHandler \u6216 TokenEndpointResponseHandler\u3002

+no_gadget_spec=\u627e\u4e0d\u5230 {0} \u7684\u5c0f\u5de5\u5177\u898f\u683c\u3002

+no_gadget_spec.explanation=\u627e\u4e0d\u5230\u5c0f\u5de5\u5177\u898f\u683c\u3002  \u8acb\u8981\u6c42\u60a8\u7684\u7ba1\u7406\u8005\u5c07\u5c0f\u5de5\u5177\u914d\u7f6e\u70ba\u4f7f\u7528 OAuth2.0\u3002

+refresh_token_problem=\u6b63\u5728\u4ea4\u63db access_token \u7684\u91cd\u65b0\u6574\u7406\u8a18\u865f\uff1a{0}

+refresh_token_problem.explanation=\u4ea4\u63db\u5b58\u53d6\u8a18\u865f\u7684\u91cd\u65b0\u6574\u7406\u8a18\u865f\u6642\u767c\u751f\u932f\u8aa4\u3002

+secret_encryption_problem=\u6b63\u5728\u52a0\u5bc6\u8a18\u865f\u79d8\u5bc6\u4ee5\u5be6\u73fe\u6301\u7e8c\u6027\uff1a{0}

+secret_encryption_problem.explanation=\u5c07 OAuth2.0 \u79d8\u5bc6\u8207\u6240\u63d0\u4f9b\u7684\u52a0\u5bc6\u6a21\u7d44\u4e00\u8d77\u5132\u5b58\u6642\u767c\u751f\u932f\u8aa4\u3002

+access_denied=\u62d2\u7d55\u5b58\u53d6\u3002

+access_denied.explanation=\u4f7f\u7528\u8005\u5df2\u62d2\u7d55\u6216\u53d6\u6d88\u670d\u52d9\u63d0\u4f9b\u8005\u7684\u6388\u6b0a\u3002

+invalid_client=\u7528\u6236\u7aef\u7121\u6548\uff1a{0}

+invalid_client.explanation= \u7121\u6cd5\u9451\u5225\u7528\u6236\u7aef\uff0c\u53ef\u80fd\u7684\u539f\u56e0\u662f\u7528\u6236\u7aef\u4e0d\u660e\uff0c\u672a\u4f75\u5165\u7528\u6236\u7aef\u9451\u5225\uff0c\u6216\u8005\u9451\u5225\u65b9\u6cd5\u4e0d\u53d7\u652f\u63f4\u3002

+invalid_grant=\u6388\u6b0a\u6388\u8207\u7121\u6548\uff1a{0}

+invalid_grant.explanation=\u6388\u6b0a\u6388\u8207\u7121\u6548\u3001\u5df2\u904e\u671f\u3001\u5df2\u64a4\u92b7\u3001\u4e0d\u7b26\u5408\u6388\u6b0a\u8981\u6c42\u4e2d\u6240\u4f7f\u7528\u7684\u91cd\u65b0\u5c0e\u5411 URI\uff0c\u6216\u8005\u5df2\u7c3d\u767c\u7d66\u5176\u4ed6\u7528\u6236\u7aef\u3002 \u6388\u6b0a\u6388\u8207\u53ef\u80fd\u5305\u62ec\u6388\u6b0a\u78bc\u3001\u8cc7\u6e90\u64c1\u6709\u8005\u7684\u8a8d\u8b49\u6216\u7528\u6236\u7aef\u7684\u8a8d\u8b49\u3002

+invalid_request=\u8981\u6c42\u7121\u6548\uff1a{0}

+invalid_request.explanation=\u56e0\u70ba\u8981\u6c42\u907a\u6f0f\u5fc5\u8981\u7684\u53c3\u6578\uff0c\u5305\u62ec\u4e0d\u53d7\u652f\u63f4\u7684\u53c3\u6578\u503c\uff0c\u91cd\u8907\u53c3\u6578\uff0c\u5305\u62ec\u591a\u500b\u8a8d\u8b49\uff0c\u5229\u7528\u591a\u500b\u6a5f\u5236\u9451\u5225\u7528\u6236\u7aef\uff0c\u6216\u8005\u662f\u5f62\u614b\u7570\u5e38\uff0c\u6240\u4ee5\u8a72\u8981\u6c42\u7121\u6548\u3002

+invalid_scope=\u7bc4\u570d\u7121\u6548\uff1a{0}

+invalid_scope.explanation=\u6240\u8981\u6c42\u7684\u7bc4\u570d\u7121\u6548\u3001\u4e0d\u660e\u3001\u5f62\u614b\u7570\u5e38\uff0c\u6216\u8005\u8d85\u51fa\u8cc7\u6e90\u64c1\u6709\u8005\u6240\u6388\u8207\u7684\u7bc4\u570d\u3002

+server_error=\u767c\u751f\u4f3a\u670d\u5668\u932f\u8aa4\uff1a{0}

+server_error.explanation=\u6388\u6b0a\u4f3a\u670d\u5668\u9047\u5230\u975e\u9810\u671f\u7684\u72c0\u6cc1\uff0c\u5c0e\u81f4\u5176\u7121\u6cd5\u57f7\u884c\u8981\u6c42\u3002

+server_rejected_request=\u4f3a\u670d\u5668\u5df2\u62d2\u7d55\u8981\u6c42\uff1astatuscode = {0}

+server_rejected_request.explanation=\u4f3a\u670d\u5668\u5df2\u5efa\u7acb OAuth2Request\uff0c\u4f46\u670d\u52d9\u63d0\u4f9b\u8005\u5df2\u5c07\u5176\u62d2\u7d55\u3002

+temporarily_unavailable=\u670d\u52d9\u66ab\u6642\u7121\u6cd5\u4f7f\u7528\uff1a{0}

+temporarily_unavailable.explanation=\u56e0\u70ba\u6388\u6b0a\u4f3a\u670d\u5668\u66ab\u6642\u8d85\u8f09\u6216\u8655\u65bc\u7dad\u8b77\u6a21\u5f0f\uff0c\u6240\u4ee5\u5b83\u7121\u6cd5\u8655\u7406\u8981\u6c42\u3002

+token_response_problem=\u6b63\u5728\u8655\u7406\u4f86\u81ea\u8a18\u865f\u7aef\u9ede\u7684\u56de\u61c9\uff1a{0}

+token_response_problem.explanation=\u8a18\u865f\u7aef\u9ede\u5df2\u50b3\u9001\u4f3a\u670d\u5668\u7121\u6cd5\u8655\u7406\u7684\u56de\u61c9\u3002

+unauthorized_client=\u7528\u6236\u7aef\u672a\u7372\u6388\u6b0a\uff1a{0}

+unauthorized_client.explanation=\u7528\u6236\u7aef\u672a\u7372\u6388\u6b0a\uff0c\u7121\u6cd5\u4f7f\u7528\u6b64\u65b9\u6cd5\u4f86\u8981\u6c42\u5b58\u53d6\u8a18\u865f\u3002

+unsupported_grant_type=\u6388\u6b0a\u985e\u578b\u4e0d\u53d7\u652f\u63f4\uff1a{0}

+unsupported_grant_type.explanation=\u6388\u6b0a\u4f3a\u670d\u5668\u4e0d\u652f\u63f4\u4f7f\u7528\u6b64\u65b9\u6cd5\u53d6\u5f97\u5b58\u53d6\u8a18\u865f\u3002

+unsupported_response_type=\u56de\u61c9\u985e\u578b\u4e0d\u53d7\u652f\u63f4\uff1a{0}

+unsupported_response_type.explanation=\u6388\u6b0a\u4f3a\u670d\u5668\u4e0d\u652f\u63f4\u4f7f\u7528\u6b64\u65b9\u6cd5\u53d6\u5f97\u6388\u6b0a\u78bc\u3002

+mac_token_problem=\u6b63\u5728\u8981\u6c42 Mac \u8a18\u865f\uff1a{0}

+mac_token_problem.explanation=\u8981\u6c42\u4e0a\u7684 Mac \u8a18\u865f\u7121\u6cd5\u65b0\u589e\u81f3\u8cc7\u6e90\u4f3a\u670d\u5668\u3002

+bearer_token_problem=\u6b63\u5728\u8981\u6c42\u8f09\u9001\u8a18\u865f\uff1a{0}

+bearer_token_problem.explanation=\u8981\u6c42\u4e0a\u7684\u8f09\u9001\u8a18\u865f\u7121\u6cd5\u65b0\u589e\u81f3\u8cc7\u6e90\u4f3a\u670d\u5668\u3002

+authentication_problem=\u6b63\u5728\u65b0\u589e\u7528\u6236\u7aef\u9451\u5225\uff1a{0}

+authentication_problem.explanation=\u9451\u5225\u6a19\u982d\u7121\u6cd5\u65b0\u589e\u81f3\u8981\u6c42\u3002

+unknown_problem=\u767c\u751f\u4e0d\u660e\u932f\u8aa4\uff1a{0}

+unknown_problem.explanation=\u8acb\u8981\u6c42\u60a8\u7684\u7cfb\u7d71\u7ba1\u7406\u8005\u8abf\u67e5\u554f\u984c\u3002

+code_grant_problem=\u6b63\u5728\u5f9e\u6388\u6b0a\u78bc\u53d6\u5f97\u7684\u5b58\u53d6\u8a18\u865f\uff1a{0}

+code_grant_problem.explanation=\u6388\u6b0a\u78bc\u6d41\u7a0b\u671f\u9593\u767c\u751f\u932f\u8aa4\u3002

diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/AuthTypeTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/AuthTypeTest.java
new file mode 100644
index 0000000..1d31c72
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/AuthTypeTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class AuthTypeTest {
+
+  @Test
+  public void testAuth() {
+    assertEquals(AuthType.OAUTH, AuthType.parse("oauth"));
+    assertEquals(AuthType.OAUTH, AuthType.parse("   oauth   "));
+    assertEquals(AuthType.OAUTH2, AuthType.parse("oauth2"));
+    assertEquals(AuthType.OAUTH2, AuthType.parse("   oauth2   "));
+    assertEquals(AuthType.SIGNED, AuthType.parse("SiGnEd"));
+    assertEquals(AuthType.NONE, AuthType.parse("NONE"));
+    assertEquals(AuthType.NONE, AuthType.parse(""));
+    assertEquals(AuthType.NONE, AuthType.parse(null));
+    assertEquals(AuthType.NONE, AuthType.parse("foo"));
+    assertEquals("none", AuthType.NONE.toString());
+    assertEquals("oauth", AuthType.OAUTH.toString());
+    assertEquals("oauth2", AuthType.OAUTH2.toString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java
new file mode 100644
index 0000000..ae181e6
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultGadgetSpecFactoryTest.java
@@ -0,0 +1,374 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.cache.LruCacheProvider;
+import org.apache.shindig.common.cache.SoftExpiringCache;
+import org.apache.shindig.common.testing.ImmediateExecutorService;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+
+/**
+ * Tests for DefaultGadgetSpecFactory
+ */
+public class DefaultGadgetSpecFactoryTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private static final String LOCAL_CONTENT = "Hello, local content!";
+  private static final String ALT_LOCAL_CONTENT = "Hello, local content!";
+  private static final String RAWXML_CONTENT = "Hello, rawxml content!";
+  private static final String LOCAL_SPEC_XML
+      = "<Module>" +
+        "  <ModulePrefs title='GadgetSpecFactoryTest'/>" +
+        "  <Content type='html'>" + LOCAL_CONTENT + "</Content>" +
+        "</Module>";
+  private static final String ALT_LOCAL_SPEC_XML
+      = "<Module>" +
+        "  <ModulePrefs title='GadgetSpecFactoryTest'/>" +
+        "  <Content type='html'>" + ALT_LOCAL_CONTENT + "</Content>" +
+        "</Module>";
+  private static final String RAWXML_SPEC_XML
+      = "<Module>" +
+        "  <ModulePrefs title='GadgetSpecFactoryTest'/>" +
+        "  <Content type='html'>" + RAWXML_CONTENT + "</Content>" +
+        "</Module>";
+
+  private static final GadgetContext RAWXML_GADGET_CONTEXT = new GadgetContext() {
+    @Override
+    public boolean getIgnoreCache() {
+      // This should be ignored by calling code.
+      return false;
+    }
+
+    @Override
+    public Uri getUrl() {
+      return SPEC_URL;
+    }
+
+    @Override
+    public String getParameter(String param) {
+      if (param.equals(DefaultGadgetSpecFactory.RAW_GADGETSPEC_XML_PARAM_NAME)) {
+        return RAWXML_SPEC_XML;
+      }
+      return null;
+    }
+  };
+
+  private static final int MAX_AGE = 10000;
+
+  private final CountingExecutor executor = new CountingExecutor();
+
+  private final RequestPipeline pipeline = EasyMock.createNiceMock(RequestPipeline.class);
+
+  private final CacheProvider cacheProvider = new LruCacheProvider(5);
+
+  private final DefaultGadgetSpecFactory specFactory
+      = new DefaultGadgetSpecFactory(executor, pipeline, cacheProvider, MAX_AGE);
+
+  private static HttpRequest createIgnoreCacheRequest() {
+    return new HttpRequest(SPEC_URL)
+        .setIgnoreCache(true)
+        .setGadget(SPEC_URL)
+        .setContainer(ContainerConfig.DEFAULT_CONTAINER);
+  }
+
+  private static HttpRequest createCacheableRequest() {
+    return new HttpRequest(SPEC_URL)
+        .setGadget(SPEC_URL)
+        .setContainer(ContainerConfig.DEFAULT_CONTAINER);
+  }
+
+  private static GadgetContext createContext(final Uri uri, final boolean ignoreCache) {
+    return new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        return uri;
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return ignoreCache;
+      }
+    };
+  }
+
+  @Test
+  public void specFetched() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    HttpResponse response = new HttpResponse(LOCAL_SPEC_XML);
+    expect(pipeline.execute(request)).andReturn(response);
+    replay(pipeline);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+
+    assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test
+  public void specFetchedWithBom() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    HttpResponse response = new HttpResponse("&#xFEFF;" + LOCAL_SPEC_XML);
+    expect(pipeline.execute(request)).andReturn(response);
+    replay(pipeline);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+
+    assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test(expected = GadgetException.class)
+  public void specFetchedEmptyContent() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    HttpResponse response = new HttpResponse("");
+    expect(pipeline.execute(request)).andReturn(response);
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+  }
+
+  @Test(expected = GadgetException.class)
+  public void malformedGadgetSpecIsCachedAndThrows2() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    expect(pipeline.execute(request)).andReturn(new HttpResponse("")).once();
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+  }
+
+  @Test
+  public void specFetchedWithBomChar() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    HttpResponse response = new HttpResponse('\uFEFF' + LOCAL_SPEC_XML);
+    expect(pipeline.execute(request)).andReturn(response);
+    replay(pipeline);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+
+    assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  // TODO: Move these tests into AbstractSpecFactoryTest
+  @Test
+  public void specRefetchedAsync() throws Exception {
+    HttpRequest request = createCacheableRequest();
+    HttpResponse response = new HttpResponse(ALT_LOCAL_SPEC_XML);
+    expect(pipeline.execute(request)).andReturn(response);
+    replay(pipeline);
+
+    specFactory.cache.addElement(
+        SPEC_URL.toString(), new GadgetSpec(SPEC_URL, LOCAL_SPEC_XML), -1);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    assertEquals(LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+
+    spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    assertEquals(ALT_LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+
+    assertEquals(1, executor.runnableCount);
+  }
+
+  @Test
+  public void specFetchedFromParam() throws Exception {
+    // Set up request as if it's a regular spec request, and ensure that
+    // the return value comes from rawxml, not the pipeline.
+    HttpRequest request = createIgnoreCacheRequest();
+    HttpResponse response = new HttpResponse(LOCAL_SPEC_XML);
+    expect(pipeline.execute(request)).andReturn(response);
+    replay(pipeline);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(RAWXML_GADGET_CONTEXT);
+
+    assertEquals(RAWXML_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+    assertEquals(DefaultGadgetSpecFactory.RAW_GADGET_URI, spec.getUrl());
+  }
+
+  @Test
+  public void staleSpecIsRefetched() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    HttpRequest retriedRequest = createCacheableRequest();
+
+    HttpResponse expiredResponse = new HttpResponseBuilder()
+        .addHeader("Pragma", "no-cache")
+        .setResponse(LOCAL_SPEC_XML.getBytes("UTF-8"))
+        .create();
+    HttpResponse updatedResponse = new HttpResponse(ALT_LOCAL_SPEC_XML);
+    expect(pipeline.execute(request)).andReturn(expiredResponse).once();
+    expect(pipeline.execute(retriedRequest)).andReturn(updatedResponse).once();
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+
+    SoftExpiringCache.CachedObject<Object> inCache = specFactory.cache.getElement(SPEC_URL.toString());
+    specFactory.cache.addElement(SPEC_URL.toString(), inCache.obj, -1);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    assertEquals(ALT_LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test
+  public void staleSpecReturnedFromCacheOnError() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    HttpRequest retriedRequest = createCacheableRequest();
+
+    HttpResponse expiredResponse = new HttpResponseBuilder()
+        .setResponse(LOCAL_SPEC_XML.getBytes("UTF-8"))
+        .addHeader("Pragma", "no-cache")
+        .create();
+    expect(pipeline.execute(request)).andReturn(expiredResponse);
+    expect(pipeline.execute(retriedRequest)).andReturn(HttpResponse.notFound()).once();
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+
+    SoftExpiringCache.CachedObject<Object> inCache = specFactory.cache.getElement(SPEC_URL.toString());
+    specFactory.cache.addElement(SPEC_URL.toString(), inCache.obj, -1);
+
+    GadgetSpec spec = specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    assertEquals(ALT_LOCAL_CONTENT, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test
+  public void ttlPropagatesToPipeline() throws Exception {
+    CapturingPipeline capturingPipeline = new CapturingPipeline();
+
+    GadgetSpecFactory forcedCacheFactory = new DefaultGadgetSpecFactory(
+        new ImmediateExecutorService(), capturingPipeline, cacheProvider, 10000);
+
+    forcedCacheFactory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    assertEquals(10, capturingPipeline.request.getCacheTtl());
+  }
+
+  @Test
+  public void specRequestMarkedWithAnonymousToken() throws Exception {
+    CapturingPipeline capturingPipeline = new CapturingPipeline();
+
+    GadgetSpecFactory factory = new DefaultGadgetSpecFactory(
+        new CountingExecutor(), capturingPipeline, cacheProvider, 10000);
+
+    factory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    SecurityToken st = capturingPipeline.request.getSecurityToken();
+    assertNotNull(st);
+    assertTrue( st.isAnonymous() );
+    assertEquals( SPEC_URL.toString(), st.getAppUrl() );
+  }
+
+
+  @Test(expected = GadgetException.class)
+  public void badFetchThrows() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    expect(pipeline.execute(request)).andReturn(HttpResponse.error());
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+  }
+
+  public void badFetchThrowsExceptionOverridingCache() throws Exception {
+    HttpRequest firstRequest = createCacheableRequest();
+    expect(pipeline.execute(firstRequest)).andReturn(new HttpResponse(LOCAL_SPEC_XML)).times(2);
+    HttpRequest secondRequest = createIgnoreCacheRequest();
+    expect(pipeline.execute(secondRequest)).andReturn(HttpResponse.error()).once();
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+
+    try {
+      specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+    } catch (GadgetException e) {
+      // Expected condition.
+    }
+
+    // Now make sure the cache wasn't populated w/ the error.
+    specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+  }
+
+  @Test(expected = GadgetException.class)
+  public void malformedGadgetSpecThrows() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    expect(pipeline.execute(request)).andReturn(new HttpResponse("malformed junk"));
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+  }
+
+  @Test(expected = GadgetException.class)
+  public void malformedGadgetSpecIsCachedAndThrows() throws Exception {
+    HttpRequest request = createCacheableRequest();
+    expect(pipeline.execute(request)).andReturn(new HttpResponse("malformed junk")).once();
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+  }
+
+  @Test(expected = GadgetException.class)
+  public void throwingPipelineRethrows() throws Exception {
+    HttpRequest request = createIgnoreCacheRequest();
+    expect(pipeline.execute(request)).andThrow(
+        new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT));
+    replay(pipeline);
+
+    specFactory.getGadgetSpec(createContext(SPEC_URL, true));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void negativeCachingEnforced() throws Exception {
+    specFactory.cache.addElement(SPEC_URL.toString(), new SpecParserException("broken"), 1000);
+    specFactory.getGadgetSpec(createContext(SPEC_URL, false));
+  }
+
+  private static class CountingExecutor extends ImmediateExecutorService {
+    int runnableCount = 0;
+
+    @Override
+    public void execute(Runnable r) {
+      runnableCount++;
+      r.run();
+    }
+  }
+
+  private static class CapturingPipeline implements RequestPipeline {
+    HttpRequest request;
+
+    public HttpResponse execute(HttpRequest request) {
+      this.request = request;
+      return new HttpResponse(LOCAL_SPEC_XML);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java
new file mode 100644
index 0000000..1123078
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/DefaultMessageBundleFactoryTest.java
@@ -0,0 +1,382 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.verify;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.cache.LruCacheProvider;
+import org.apache.shindig.common.testing.ImmediateExecutorService;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Tests for DefaultMessageBundleFactory
+ */
+public class DefaultMessageBundleFactoryTest {
+  private static final Uri BUNDLE_URI = Uri.parse("http://example.org/messagex.xml");
+  private static final Uri LANG_BUNDLE_URI = Uri.parse("http://example.org/messagex.xml");
+  private static final Uri COUNTRY_BUNDLE_URI = Uri.parse("http://example.org/messagex.xml");
+  private static final Uri ALL_BUNDLE_URI = Uri.parse("http://example.org/messagex.xml");
+  private static final Uri SPEC_URI = Uri.parse("http://example.org/gadget.xml");
+
+  private static final String MSG_0_NAME = "messageZero";
+  private static final String MSG_1_NAME = "message1";
+  private static final String MSG_2_NAME = "message 2";
+  private static final String MSG_3_NAME = "message 3";
+  private static final String MSG_0_VALUE = "Message 0 VALUE";
+  private static final String MSG_0_LANG_VALUE = "Message 0 language VALUE";
+  private static final String MSG_0_COUNTRY_VALUE = "Message 0 country VALUE";
+  private static final String MSG_0_VIEW_VALUE = "Message 0 view VALUE";
+  private static final String MSG_0_ALL_VALUE = "Message 0 a VALUE";
+  private static final String MSG_1_VALUE = "msg one val";
+  private static final String MSG_2_VALUE = "message two val.";
+  private static final String MSG_2_VIEW_VALUE = "message two view val.";
+  private static final String MSG_3_VALUE = "message three value";
+
+  private static final Locale COUNTRY_LOCALE = new Locale("all", "US");
+  private static final Locale LANG_LOCALE = new Locale("en", "ALL");
+  private static final Locale LOCALE = new Locale("en", "US");
+
+  private static final String BASIC_BUNDLE
+      = "<messagebundle>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_VALUE + "</msg>" +
+        "  <msg name='" + MSG_1_NAME + "'>" + MSG_1_VALUE + "</msg>" +
+        "</messagebundle>";
+
+  private static final String LANG_BUNDLE
+      = "<messagebundle>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_LANG_VALUE + "</msg>" +
+        "  <msg name='lang'>true</msg>" +
+        "</messagebundle>";
+
+  private static final String COUNTRY_BUNDLE
+      = "<messagebundle>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_COUNTRY_VALUE + "</msg>" +
+        "  <msg name='country'>true</msg>" +
+        "</messagebundle>";
+
+  private static final String ALL_ALL_BUNDLE
+      = "<messagebundle>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_ALL_VALUE + "</msg>" +
+        "  <msg name='all'>true</msg>" +
+        "</messagebundle>";
+
+  private static final String BASIC_SPEC
+      = "<Module>" +
+        "<ModulePrefs title='foo'>" +
+        " <Locale>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_ALL_VALUE + "</msg>" +
+        " </Locale>" +
+        " <Locale country='" + LOCALE.getCountry() + "'>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_COUNTRY_VALUE + "</msg>" +
+        "  <msg name='" + MSG_3_NAME + "'>" + MSG_3_VALUE + "</msg>" +
+        " </Locale>" +
+        " <Locale country='" + LOCALE.getCountry() + "' views='view1,view2'>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_VIEW_VALUE + "</msg>" +
+        "  <msg name='" + MSG_3_NAME + "'>" + MSG_3_VALUE + "</msg>" +
+        " </Locale>" +
+        " <Locale views='view1'>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_ALL_VALUE + "</msg>" +
+        "  <msg name='" + MSG_2_NAME + "'>" + MSG_2_VIEW_VALUE + "</msg>" +
+        " </Locale>" +
+        " <Locale lang='" + LOCALE.getLanguage() + "'>" +
+        "  <msg name='" + MSG_0_NAME + "'>" + MSG_0_LANG_VALUE + "</msg>" +
+        "  <msg name='" + MSG_1_NAME + "'>" + MSG_1_VALUE + "</msg>" +
+        "  <msg name='" + MSG_2_NAME + "'>" + MSG_2_VALUE + "</msg>" +
+        " </Locale>" +
+        " <Locale lang='" + LOCALE.getLanguage() + "' country='" + LOCALE.getCountry() + "' " +
+        "  messages='" + BUNDLE_URI + "'/>" +
+        "</ModulePrefs>" +
+        "<Content type='html'/>" +
+        "</Module>";
+
+  private static final String ALL_EXTERNAL_SPEC
+      = "<Module>" +
+        "<ModulePrefs title='foo'>" +
+        " <Locale messages='" + BUNDLE_URI + "'/>" +
+        " <Locale country='" + LOCALE.getCountry() + '\'' +
+        "  messages='" + COUNTRY_BUNDLE_URI + "'/>" +
+        " <Locale lang='" + LOCALE.getLanguage() + "' messages='" + LANG_BUNDLE_URI + "'/>" +
+        " <Locale lang='" + LOCALE.getLanguage() + "' country='" + LOCALE.getCountry() + "' " +
+        "  messages='" + ALL_BUNDLE_URI + "'/>" +
+        "</ModulePrefs>" +
+        "<Content type='html'/>" +
+        "</Module>";
+
+  private static final int MAX_AGE = 10000;
+
+  private final RequestPipeline pipeline = EasyMock.createNiceMock(RequestPipeline.class);
+  private final CacheProvider cacheProvider = new LruCacheProvider(10);
+  private final Cache<String, MessageBundle> cache
+      = cacheProvider.createCache(DefaultMessageBundleFactory.CACHE_NAME);
+  private final DefaultMessageBundleFactory bundleFactory
+      = new DefaultMessageBundleFactory(new ImmediateExecutorService(), pipeline, cacheProvider, MAX_AGE);
+  private final GadgetSpec gadgetSpec;
+  private final GadgetSpec externalSpec;
+
+  public DefaultMessageBundleFactoryTest() {
+    try {
+      gadgetSpec = new GadgetSpec(SPEC_URI, BASIC_SPEC);
+      externalSpec = new GadgetSpec(SPEC_URI, ALL_EXTERNAL_SPEC);
+    } catch (GadgetException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Test
+  public void getExactBundle() throws Exception {
+    HttpResponse response = new HttpResponse(BASIC_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(response);
+    replay(pipeline);
+
+    MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, LOCALE, true, ContainerConfig.DEFAULT_CONTAINER, null);
+
+    assertEquals(MSG_0_VALUE, bundle.getMessages().get(MSG_0_NAME));
+    assertEquals(MSG_1_VALUE, bundle.getMessages().get(MSG_1_NAME));
+    assertEquals(MSG_2_VALUE, bundle.getMessages().get(MSG_2_NAME));
+    assertEquals(MSG_3_VALUE, bundle.getMessages().get(MSG_3_NAME));
+  }
+
+  @Test
+  public void getLangBundle() throws Exception {
+    MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, LANG_LOCALE, true, ContainerConfig.DEFAULT_CONTAINER, null);
+
+    assertEquals(MSG_0_LANG_VALUE, bundle.getMessages().get(MSG_0_NAME));
+    assertEquals(MSG_1_VALUE, bundle.getMessages().get(MSG_1_NAME));
+    assertEquals(MSG_2_VALUE, bundle.getMessages().get(MSG_2_NAME));
+    assertNull(bundle.getMessages().get(MSG_3_NAME));
+  }
+
+  @Test
+  public void getCountryBundle() throws Exception {
+    MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, COUNTRY_LOCALE, true, ContainerConfig.DEFAULT_CONTAINER, null);
+
+    assertEquals(MSG_0_COUNTRY_VALUE, bundle.getMessages().get(MSG_0_NAME));
+    assertNull(bundle.getMessages().get(MSG_1_NAME));
+    assertNull(bundle.getMessages().get(MSG_2_NAME));
+    assertEquals(MSG_3_VALUE, bundle.getMessages().get(MSG_3_NAME));
+  }
+
+  @Test
+  public void getViewCountryBundle() throws Exception {
+    MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, COUNTRY_LOCALE, true, ContainerConfig.DEFAULT_CONTAINER, "view1");
+
+    assertEquals(MSG_0_VIEW_VALUE, bundle.getMessages().get(MSG_0_NAME));
+    assertNull(bundle.getMessages().get(MSG_1_NAME));
+    assertEquals(MSG_2_VIEW_VALUE, bundle.getMessages().get(MSG_2_NAME));
+    assertEquals(MSG_3_VALUE, bundle.getMessages().get(MSG_3_NAME));
+  }
+
+
+  @Test
+  public void getViewAllAllBundle() throws Exception {
+    MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, new Locale("all", "ALL"), true, ContainerConfig.DEFAULT_CONTAINER, "view1");
+
+    assertEquals(MSG_0_ALL_VALUE, bundle.getMessages().get(MSG_0_NAME));
+    assertNull(bundle.getMessages().get(MSG_1_NAME));
+    assertEquals(MSG_2_VIEW_VALUE, bundle.getMessages().get(MSG_2_NAME));
+    assertNull(bundle.getMessages().get(MSG_3_NAME));
+  }
+
+  @Test
+  public void getAllAllBundle() throws Exception {
+    MessageBundle bundle = bundleFactory.getBundle(gadgetSpec, new Locale("all", "ALL"), true, ContainerConfig.DEFAULT_CONTAINER, null);
+    assertEquals(MSG_0_ALL_VALUE, bundle.getMessages().get(MSG_0_NAME));
+    assertNull(bundle.getMessages().get(MSG_1_NAME));
+    assertNull(bundle.getMessages().get(MSG_2_NAME));
+    assertNull(bundle.getMessages().get(MSG_3_NAME));
+  }
+
+  @Test
+  public void getExactBundleAllExternal() throws Exception {
+    HttpResponse response = new HttpResponse(BASIC_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(response);
+    HttpResponse langResponse = new HttpResponse(LANG_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(langResponse);
+    HttpResponse countryResponse = new HttpResponse(COUNTRY_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(countryResponse);
+    HttpResponse allAllResponse = new HttpResponse(ALL_ALL_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(allAllResponse);
+
+    replay(pipeline);
+    MessageBundle bundle = bundleFactory.getBundle(externalSpec, LOCALE, true, ContainerConfig.DEFAULT_CONTAINER, null);
+    verify(pipeline);
+
+    assertEquals("true", bundle.getMessages().get("lang"));
+    assertEquals("true", bundle.getMessages().get("country"));
+    assertEquals("true", bundle.getMessages().get("all"));
+    assertEquals(MSG_0_VALUE, bundle.getMessages().get(MSG_0_NAME));
+  }
+
+  @Test
+  public void getLangBundleAllExternal() throws Exception {
+    HttpResponse langResponse = new HttpResponse(LANG_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(langResponse);
+    HttpResponse allAllResponse = new HttpResponse(ALL_ALL_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(allAllResponse);
+
+    replay(pipeline);
+    MessageBundle bundle = bundleFactory.getBundle(externalSpec, LANG_LOCALE, true, ContainerConfig.DEFAULT_CONTAINER, null);
+    verify(pipeline);
+
+    assertEquals("true", bundle.getMessages().get("lang"));
+    assertEquals("true", bundle.getMessages().get("all"));
+    assertEquals(MSG_0_LANG_VALUE, bundle.getMessages().get(MSG_0_NAME));
+  }
+
+  @Test
+  public void getCountryBundleAllExternal() throws Exception {
+    HttpResponse countryResponse = new HttpResponse(COUNTRY_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(countryResponse);
+    HttpResponse allAllResponse = new HttpResponse(ALL_ALL_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(allAllResponse);
+
+    replay(pipeline);
+    MessageBundle bundle = bundleFactory.getBundle(externalSpec, COUNTRY_LOCALE, true, ContainerConfig.DEFAULT_CONTAINER, null);
+    verify(pipeline);
+
+    assertEquals("true", bundle.getMessages().get("country"));
+    assertEquals("true", bundle.getMessages().get("all"));
+    assertEquals(MSG_0_COUNTRY_VALUE, bundle.getMessages().get(MSG_0_NAME));
+  }
+
+  @Test
+  public void getAllAllExternal() throws Exception {
+    HttpResponse allAllResponse = new HttpResponse(ALL_ALL_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(allAllResponse);
+
+    replay(pipeline);
+    MessageBundle bundle = bundleFactory.getBundle(externalSpec, new Locale("all", "ALL"), true, ContainerConfig.DEFAULT_CONTAINER, null);
+    verify(pipeline);
+
+    assertEquals("true", bundle.getMessages().get("all"));
+    assertEquals(MSG_0_ALL_VALUE, bundle.getMessages().get(MSG_0_NAME));
+  }
+
+  @Test
+  public void getBundleFromCache() throws Exception {
+    HttpResponse response = new HttpResponse(BASIC_BUNDLE);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(response).once();
+    replay(pipeline);
+
+    MessageBundle bundle0 = bundleFactory.getBundle(gadgetSpec, LOCALE, false, ContainerConfig.DEFAULT_CONTAINER, null);
+    MessageBundle bundle1 = bundleFactory.getBundle(gadgetSpec, LOCALE, false, ContainerConfig.DEFAULT_CONTAINER, null);
+
+    verify(pipeline);
+
+    assertEquals(bundle0.getMessages().get(MSG_0_NAME), bundle1.getMessages().get(MSG_0_NAME));
+  }
+
+  @Test
+  public void ignoreCacheDoesNotStore() throws Exception {
+    bundleFactory.getBundle(gadgetSpec, new Locale("all", "ALL"), true, ContainerConfig.DEFAULT_CONTAINER, null);
+    assertEquals(0, cache.getSize());
+  }
+
+  @Test
+  public void badResponseServedFromCache() throws Exception {
+    HttpResponse expiredResponse = new HttpResponseBuilder()
+        .setResponse(BASIC_BUNDLE.getBytes("UTF-8"))
+        .addHeader("Pragma", "no-cache")
+        .create();
+    HttpResponse badResponse = HttpResponse.error();
+
+    expect(pipeline.execute(isA(HttpRequest.class)))
+        .andReturn(expiredResponse).once();
+    expect(pipeline.execute(isA(HttpRequest.class)))
+        .andReturn(badResponse).once();
+    replay(pipeline);
+
+    final AtomicLong time = new AtomicLong();
+
+    bundleFactory.cache.setTimeSource(new TimeSource() {
+      @Override
+      public long currentTimeMillis() {
+        return time.get();
+      }
+    });
+
+    time.set(System.currentTimeMillis());
+
+    MessageBundle bundle0 = bundleFactory.getBundle(gadgetSpec, LOCALE, false, ContainerConfig.DEFAULT_CONTAINER, null);
+
+    time.set(time.get() + MAX_AGE + 1);
+
+    MessageBundle bundle1 = bundleFactory.getBundle(gadgetSpec, LOCALE, false, ContainerConfig.DEFAULT_CONTAINER, null);
+
+    verify(pipeline);
+
+    assertEquals(bundle0.getMessages().get(MSG_0_NAME), bundle1.getMessages().get(MSG_0_NAME));
+  }
+
+  @Test(expected=GadgetException.class)
+  public void badResponsePropagatesException() throws Exception {
+    HttpResponse badResponse = HttpResponse.error();
+
+    expect(pipeline.execute(isA(HttpRequest.class)))
+        .andReturn(badResponse).once();
+    replay(pipeline);
+
+    bundleFactory.getBundle(gadgetSpec, LOCALE, false, ContainerConfig.DEFAULT_CONTAINER, null);
+  }
+
+  @Test
+  public void ttlPropagatesToFetcher() throws Exception {
+    CapturingFetcher capturingFetcher = new CapturingFetcher();
+
+    MessageBundleFactory factory = new DefaultMessageBundleFactory(
+        new ImmediateExecutorService(), capturingFetcher, cacheProvider, MAX_AGE);
+
+    factory.getBundle(gadgetSpec, LOCALE, false, ContainerConfig.DEFAULT_CONTAINER, null);
+
+    assertEquals(MAX_AGE / 1000, capturingFetcher.request.getCacheTtl());
+  }
+
+  private static class CapturingFetcher implements RequestPipeline {
+    HttpRequest request;
+
+    protected CapturingFetcher() {
+    }
+
+    public HttpResponse execute(HttpRequest request) {
+      this.request = request;
+      return new HttpResponse(BASIC_BUNDLE);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FakeGadgetSpecFactory.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FakeGadgetSpecFactory.java
new file mode 100644
index 0000000..78701df
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FakeGadgetSpecFactory.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.oauth.GadgetTokenStoreTest;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+/**
+ * Fakes out a gadget spec factory
+ */
+public class FakeGadgetSpecFactory implements GadgetSpecFactory {
+  public static final String SERVICE_NAME = "testservice";
+  public static final String SERVICE_NAME_NO_KEY = "nokey";
+
+  public GadgetSpec getGadgetSpec(GadgetContext context) throws GadgetException {
+    Uri uri = context.getUrl();
+    String gadget = uri.toString();
+    String baseSpec = GadgetTokenStoreTest.GADGET_SPEC;
+
+    if (gadget.contains("nokey")) {
+      // For testing key lookup failures
+      String nokeySpec = baseSpec.replace(SERVICE_NAME, SERVICE_NAME_NO_KEY);
+      return new GadgetSpec(uri, nokeySpec);
+    } else if (gadget.contains("header")) {
+      // For testing oauth data in header
+      String headerSpec = baseSpec.replace("uri-query", "auth-header");
+      return new GadgetSpec(uri, headerSpec);
+    } else if (gadget.contains("body")) {
+      // For testing oauth data in body
+      String bodySpec = baseSpec.replace("uri-query", "post-body");
+      bodySpec = bodySpec.replace("'GET'", "'POST'");
+      return new GadgetSpec(uri, bodySpec);
+    } else if (gadget.contains("badoauthurl")) {
+      String spec = baseSpec.replace("/request?param=foo", "/echo?mary_had_a_little_lamb");
+      spec = spec.replace("/access", "/echo?add_oauth_token=with_fleece_as_white_as_snow");
+      spec = spec.replace("uri-query", "auth-header");
+      return new GadgetSpec(uri, spec);
+    } else if (gadget.contains("approvalparams")) {
+      String spec = baseSpec.replace("/authorize", "/authorize?oauth_callback=foo");
+      return new GadgetSpec(uri, spec);
+    } else {
+      return new GadgetSpec(uri, baseSpec);
+    }
+  }
+
+  public Uri getGadgetUri(GadgetContext context) throws GadgetException {
+    return context.getUrl();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FeedProcessorImplTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FeedProcessorImplTest.java
new file mode 100644
index 0000000..cb2af6f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FeedProcessorImplTest.java
@@ -0,0 +1,354 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Test;
+
+/**
+ * Tests for FeedProcessorImpl
+ */
+public class FeedProcessorImplTest {
+
+  private final static String FEED_TITLE = "Example Feed";
+  private final static String FEED_AUTHOR = "John Doe";
+  private final static String FEED_AUTHOR_EMAIL = "john.doe@example.com";
+  private final static String FEED_IMAGE_URL = "http://example.org/example.gif";
+  private final static String FEED_IMAGE_TITLE = "Example Feed Image";
+  private final static String FEED_IMAGE_DESCRIPTION = "Example Feed Image Description";
+  private final static String FEED_IMAGE_LINK = "http://example.org/";
+  private final static String FEED_ENTRY_TITLE = "Atom-Powered Robots Run Amok";
+  private final static String FEED_ENTRY_LINK = "http://example.org/2003/12/13/entry03";
+  private final static String FEED_ENTRY_SUMMARY = "Some text.";
+  private final static String URL_RSS = "http://www.example.com/rss.xml";
+  private final static long TIMESTAMP = 1212790800000L;
+  private final static String DATE_RSS = "Fri, 06 Jun 2008 22:20:00 GMT";
+  private final static String DATA_RSS =
+      "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+      "<rss version=\"2.0\"><channel>" +
+      "<title>" + FEED_TITLE + "</title>" +
+      "<link>http://example.org/</link>" +
+      "<description>Example RSS Feed</description>" +
+      "<pubDate>Sun, 19 May 2002 15:21:36 GMT</pubDate>" +
+      "<image>" +
+      "<url>" + FEED_IMAGE_URL + "</url>" +
+      "<title>" + FEED_IMAGE_TITLE + "</title>" +
+      "<description>" + FEED_IMAGE_DESCRIPTION + "</description>" +
+      "<link>" + FEED_IMAGE_LINK + "</link>" +
+      "</image>" +
+      "<item>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link>" + FEED_ENTRY_LINK + "</link>" +
+      "<guid>" + FEED_ENTRY_LINK + "#item1" + "</guid>" +
+      "<pubDate>" + DATE_RSS + "</pubDate>" +
+      "<description>" + FEED_ENTRY_SUMMARY + "</description>" +
+      "<author>" + FEED_AUTHOR_EMAIL + "</author>" +
+      "</item>" +
+      "<item>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link>" + FEED_ENTRY_LINK + "</link>" +
+      "<guid>" + FEED_ENTRY_LINK + "#item1" + "</guid>" +
+      "<description>" + FEED_ENTRY_SUMMARY + "</description>" +
+      "</item>" +
+      "</channel></rss>";
+  private final static String MEDIA_CONTENT_URL1 = "http://example.com/img1.jpg";
+  private final static String MEDIA_CONTENT_URL2 = "http://example.com/img2.jpg";
+  private final static String MEDIA_CONTENT_URL3 = "http://example.com/img3.jpg";
+  private final static String MEDIA_CONTENT_TYPE = "image/jpeg";
+  private final static int MEDIA_CONTENT_WIDTH = 800;
+  private final static int MEDIA_CONTENT_HEIGHT = 600;
+  private final static String MEDIA_THUMB_URL = "http://exmaple.com/thumb.jpg";
+  private final static int MEDIA_THUMB_WIDTH = 75;
+  private final static int MEDIA_THUMB_HEIGHT = 50;
+  private final static String DATA_RSS_WITH_MEDIARSS =
+      "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+      "<rss version=\"2.0\" xmlns:media=\"http://search.yahoo.com/mrss/\"><channel>" +
+      "<title>" + FEED_TITLE + "</title>" +
+      "<link>http://example.org/</link>" +
+      "<description>Example RSS Feed</description>" +
+      "<pubDate>Sun, 19 May 2002 15:21:36 GMT</pubDate>" +
+      "<item>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link>" + FEED_ENTRY_LINK + "</link>" +
+      "<guid>" + FEED_ENTRY_LINK + "#item1" + "</guid>" +
+      "<pubDate>" + DATE_RSS + "</pubDate>" +
+      "<description>" + FEED_ENTRY_SUMMARY + "</description>" +
+      "<author>" + FEED_AUTHOR_EMAIL + "</author>" +
+      "<media:content url=\"" + MEDIA_CONTENT_URL1 + "\" type=\"" + MEDIA_CONTENT_TYPE + "\" isDefault=\"false\" expression=\"sample\" width=\"" + MEDIA_CONTENT_WIDTH + "\" height=\"" + MEDIA_CONTENT_HEIGHT + "\" />" +
+      "<media:content url=\"" + MEDIA_CONTENT_URL2 + "\" type=\"" + MEDIA_CONTENT_TYPE + "\" isDefault=\"false\" expression=\"sample\" width=\"" + MEDIA_CONTENT_WIDTH + "\" height=\"" + MEDIA_CONTENT_HEIGHT + "\" />" +
+      "<media:content url=\"" + MEDIA_CONTENT_URL3 + "\" type=\"" + MEDIA_CONTENT_TYPE + "\" isDefault=\"false\" expression=\"sample\" width=\"" + MEDIA_CONTENT_WIDTH + "\" height=\"" + MEDIA_CONTENT_HEIGHT + "\" />" +
+      "<media:thumbnail url=\"" + MEDIA_THUMB_URL + "\" width=\"" + MEDIA_THUMB_WIDTH + "\" height=\"" + MEDIA_THUMB_HEIGHT + "\" />" +
+      "</item>" +
+      "<item>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link>" + FEED_ENTRY_LINK + "</link>" +
+      "<guid>" + FEED_ENTRY_LINK + "#item1" + "</guid>" +
+      "<description>" + FEED_ENTRY_SUMMARY + "</description>" +
+      "<media:thumbnail url=\"" + MEDIA_THUMB_URL + "\" />" +
+      "</item>" +
+      "</channel></rss>";
+
+  private final static String URL_ATOM = "http://www.example.com/feed.atom";
+  private final static String DATE_ATOM = "2008-06-06T22:20:00Z";
+  private final static String DATA_ATOM =
+      "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+      "<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
+      "<title>" + FEED_TITLE + "</title>" +
+      "<link href=\"http://example.org/\"/>" +
+      "<updated>2003-12-13T18:30:02Z</updated>" +
+      "<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>" +
+      "<author><name>" + FEED_AUTHOR + "</name></author>" +
+      "<entry>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link href=\"" + FEED_ENTRY_LINK + "\"/>" +
+      "<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>" +
+      "<updated>" + DATE_ATOM + "</updated>" +
+      "<summary>" + FEED_ENTRY_SUMMARY + "</summary>" +
+      "</entry>" +
+      "<entry>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link href=\"" + FEED_ENTRY_LINK + "\"/>" +
+      "<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da3443fa6a</id>" +
+      "<summary>" + FEED_ENTRY_SUMMARY + "</summary>" +
+      "</entry>" +
+      "</feed>";
+  private final static String DATA_ATOM_WITH_MEDIARSS =
+      "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+      "<feed xmlns=\"http://www.w3.org/2005/Atom\" xmlns:media=\"http://search.yahoo.com/mrss/\">" +
+      "<title>" + FEED_TITLE + "</title>" +
+      "<link href=\"http://example.org/\"/>" +
+      "<updated>2003-12-13T18:30:02Z</updated>" +
+      "<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>" +
+      "<author><name>" + FEED_AUTHOR + "</name></author>" +
+      "<entry>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link href=\"" + FEED_ENTRY_LINK + "\"/>" +
+      "<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>" +
+      "<updated>" + DATE_ATOM + "</updated>" +
+      "<summary>" + FEED_ENTRY_SUMMARY + "</summary>" +
+      "<media:content url=\"" + MEDIA_CONTENT_URL1 + "\" type=\"" + MEDIA_CONTENT_TYPE + "\" isDefault=\"false\" expression=\"sample\" width=\"" + MEDIA_CONTENT_WIDTH + "\" height=\"" + MEDIA_CONTENT_HEIGHT + "\" />" +
+      "<media:content url=\"" + MEDIA_CONTENT_URL2 + "\" type=\"" + MEDIA_CONTENT_TYPE + "\" isDefault=\"false\" expression=\"sample\" width=\"" + MEDIA_CONTENT_WIDTH + "\" height=\"" + MEDIA_CONTENT_HEIGHT + "\" />" +
+      "<media:content url=\"" + MEDIA_CONTENT_URL3 + "\" type=\"" + MEDIA_CONTENT_TYPE + "\" isDefault=\"false\" expression=\"sample\" width=\"" + MEDIA_CONTENT_WIDTH + "\" height=\"" + MEDIA_CONTENT_HEIGHT + "\" />" +
+      "<media:thumbnail url=\"" + MEDIA_THUMB_URL + "\" width=\"" + MEDIA_THUMB_WIDTH + "\" height=\"" + MEDIA_THUMB_HEIGHT + "\" />" +
+      "</entry>" +
+      "<entry>" +
+      "<title>" + FEED_ENTRY_TITLE + "</title>" +
+      "<link href=\"" + FEED_ENTRY_LINK + "\"/>" +
+      "<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da3443fa6a</id>" +
+      "<summary>" + FEED_ENTRY_SUMMARY + "</summary>" +
+      "<media:thumbnail url=\"" + MEDIA_THUMB_URL + "\" />" +
+      "</entry>" +
+      "</feed>";
+  private final static String BAD_XML = "broken xml !!!! & ><";
+  private final static String INVALID_XML = "<data><foo/></data>";
+
+  private final FeedProcessor processor;
+
+  public FeedProcessorImplTest() {
+    processor = new FeedProcessorImpl();
+  }
+
+  @Test
+  public void parseRss() throws Exception {
+    JSONObject feed = processor.process(URL_RSS, DATA_RSS, true, 1);
+
+    assertEquals(URL_RSS, feed.getString("URL"));
+    assertEquals(FEED_TITLE, feed.getString("Title"));
+    assertEquals(FEED_AUTHOR_EMAIL, feed.getString("Author"));
+
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    JSONObject entry = entryArray.getJSONObject(0);
+
+    assertEquals(1, entryArray.length());
+    assertEquals(FEED_ENTRY_TITLE, entry.getString("Title"));
+    assertEquals(FEED_ENTRY_LINK, entry.getString("Link"));
+    assertEquals(FEED_ENTRY_SUMMARY, entry.getString("Summary"));
+  }
+
+  @Test
+  public void parseRssMultiple() throws Exception {
+    JSONObject feed = processor.process(URL_RSS, DATA_RSS, true, 2);
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    assertEquals(2, entryArray.length());
+  }
+
+  @Test
+  public void parseRssDate() throws Exception {
+    JSONObject feed = processor.process(URL_RSS, DATA_RSS, true, 2);
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    assertEquals(TIMESTAMP, entryArray.getJSONObject(0).getLong("Date"));
+    assertEquals(0, entryArray.getJSONObject(1).getLong("Date"));
+  }
+
+  @Test
+  public void parseRssNoSummaries() throws Exception {
+    JSONObject feed = processor.process(URL_RSS, DATA_RSS, false, 1);
+    feed.getJSONArray("Entry");
+    JSONObject entry = feed.getJSONArray("Entry").getJSONObject(0);
+    assertNull("Summary should not be returned when getSummaries is false",
+        entry.optString("Summary", null));
+  }
+
+  @Test
+  public void parseMediaRss() throws Exception {
+    JSONObject feed = processor.process(URL_RSS, DATA_RSS_WITH_MEDIARSS, true, 1);
+
+    assertEquals(URL_RSS, feed.getString("URL"));
+    assertEquals(FEED_TITLE, feed.getString("Title"));
+    assertEquals(FEED_AUTHOR_EMAIL, feed.getString("Author"));
+
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    JSONObject entry = entryArray.getJSONObject(0);
+
+    assertEquals(1, entryArray.length());
+    assertEquals(FEED_ENTRY_TITLE, entry.getString("Title"));
+    assertEquals(FEED_ENTRY_LINK, entry.getString("Link"));
+    assertEquals(FEED_ENTRY_SUMMARY, entry.getString("Summary"));
+
+    // Three lots of content, each with a width/height and type
+    JSONObject media = entry.getJSONObject("Media");
+    JSONArray contents = media.getJSONArray("Contents");
+
+    assertEquals(3, contents.length());
+
+    JSONObject contents1 = contents.getJSONObject(0);
+    assertEquals(MEDIA_CONTENT_URL1, contents1.getString("URL"));
+    assertEquals(MEDIA_CONTENT_TYPE, contents1.getString("Type"));
+    assertEquals(MEDIA_CONTENT_WIDTH, contents1.getInt("Width"));
+    assertEquals(MEDIA_CONTENT_HEIGHT, contents1.getInt("Height"));
+
+    JSONObject contents2 = contents.getJSONObject(1);
+    assertEquals(MEDIA_CONTENT_URL2, contents2.getString("URL"));
+    assertEquals(MEDIA_CONTENT_TYPE, contents2.getString("Type"));
+    assertEquals(MEDIA_CONTENT_WIDTH, contents2.getInt("Width"));
+    assertEquals(MEDIA_CONTENT_HEIGHT, contents2.getInt("Height"));
+
+    JSONObject contents3 = contents.getJSONObject(2);
+    assertEquals(MEDIA_CONTENT_URL3, contents3.getString("URL"));
+    assertEquals(MEDIA_CONTENT_TYPE, contents3.getString("Type"));
+    assertEquals(MEDIA_CONTENT_WIDTH, contents3.getInt("Width"));
+    assertEquals(MEDIA_CONTENT_HEIGHT, contents3.getInt("Height"));
+
+    JSONObject thumbnail = media.getJSONObject("Thumbnail");
+    assertEquals(MEDIA_THUMB_URL, thumbnail.getString("URL"));
+    assertEquals(MEDIA_THUMB_WIDTH, thumbnail.getInt("Width"));
+    assertEquals(MEDIA_THUMB_HEIGHT, thumbnail.getInt("Height"));
+  }
+
+  @Test
+  public void parseAtom() throws Exception {
+    JSONObject feed = processor.process(URL_ATOM, DATA_ATOM, true, 1);
+
+    assertEquals(URL_ATOM, feed.getString("URL"));
+    assertEquals(FEED_TITLE, feed.getString("Title"));
+    assertEquals(FEED_AUTHOR, feed.getString("Author"));
+
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    JSONObject entry = entryArray.getJSONObject(0);
+
+    assertEquals(1, entryArray.length());
+    assertEquals(FEED_ENTRY_TITLE, entry.getString("Title"));
+    assertEquals(FEED_ENTRY_LINK, entry.getString("Link"));
+    assertEquals(FEED_ENTRY_SUMMARY, entry.getString("Summary"));
+  }
+
+  @Test
+  public void parseAtomMultiple() throws Exception {
+    JSONObject feed = processor.process(URL_ATOM, DATA_ATOM, true, 2);
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    assertEquals(2, entryArray.length());
+  }
+
+  @Test
+  public void parseAtomDate() throws Exception {
+    JSONObject feed = processor.process(URL_ATOM, DATA_ATOM, true, 2);
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    assertEquals(TIMESTAMP, entryArray.getJSONObject(0).getLong("Date"));
+    assertEquals(0, entryArray.getJSONObject(1).getLong("Date"));
+  }
+
+  @Test
+  public void parseAtomNoSummaries() throws Exception {
+    JSONObject feed = processor.process(URL_ATOM, DATA_ATOM, false, 1);
+    feed.getJSONArray("Entry");
+    JSONObject entry = feed.getJSONArray("Entry").getJSONObject(0);
+    assertNull("Summary should not be returned when getSummaries is false",
+        entry.optString("Summary", null));
+  }
+
+  @Test
+  public void parseMediaAtom() throws Exception {
+    JSONObject feed = processor.process(URL_ATOM, DATA_ATOM_WITH_MEDIARSS, true, 1);
+
+    assertEquals(URL_ATOM, feed.getString("URL"));
+    assertEquals(FEED_TITLE, feed.getString("Title"));
+    assertEquals(FEED_AUTHOR, feed.getString("Author"));
+
+    JSONArray entryArray = feed.getJSONArray("Entry");
+    JSONObject entry = entryArray.getJSONObject(0);
+
+    assertEquals(1, entryArray.length());
+    assertEquals(FEED_ENTRY_TITLE, entry.getString("Title"));
+    assertEquals(FEED_ENTRY_LINK, entry.getString("Link"));
+    assertEquals(FEED_ENTRY_SUMMARY, entry.getString("Summary"));
+
+    // Three lots of content, each with a width/height and type
+    JSONObject media = entry.getJSONObject("Media");
+    JSONArray contents = media.getJSONArray("Contents");
+
+    assertEquals(3, contents.length());
+
+    JSONObject contents1 = contents.getJSONObject(0);
+    assertEquals(MEDIA_CONTENT_URL1, contents1.getString("URL"));
+    assertEquals(MEDIA_CONTENT_TYPE, contents1.getString("Type"));
+    assertEquals(MEDIA_CONTENT_WIDTH, contents1.getInt("Width"));
+    assertEquals(MEDIA_CONTENT_HEIGHT, contents1.getInt("Height"));
+
+    JSONObject contents2 = contents.getJSONObject(1);
+    assertEquals(MEDIA_CONTENT_URL2, contents2.getString("URL"));
+    assertEquals(MEDIA_CONTENT_TYPE, contents2.getString("Type"));
+    assertEquals(MEDIA_CONTENT_WIDTH, contents2.getInt("Width"));
+    assertEquals(MEDIA_CONTENT_HEIGHT, contents2.getInt("Height"));
+
+    JSONObject contents3 = contents.getJSONObject(2);
+    assertEquals(MEDIA_CONTENT_URL3, contents3.getString("URL"));
+    assertEquals(MEDIA_CONTENT_TYPE, contents3.getString("Type"));
+    assertEquals(MEDIA_CONTENT_WIDTH, contents3.getInt("Width"));
+    assertEquals(MEDIA_CONTENT_HEIGHT, contents3.getInt("Height"));
+
+    JSONObject thumbnail = media.getJSONObject("Thumbnail");
+    assertEquals(MEDIA_THUMB_URL, thumbnail.getString("URL"));
+    assertEquals(MEDIA_THUMB_WIDTH, thumbnail.getInt("Width"));
+    assertEquals(MEDIA_THUMB_HEIGHT, thumbnail.getInt("Height"));
+  }
+
+  @Test(expected = GadgetException.class)
+  public void parseBadXml() throws GadgetException {
+    processor.process(URL_RSS, BAD_XML, false, 1);
+  }
+
+  @Test(expected = GadgetException.class)
+  public void parseInvalidXml() throws GadgetException {
+    processor.process(URL_RSS, INVALID_XML, false, 1);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FetchResponseUtilsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FetchResponseUtilsTest.java
new file mode 100644
index 0000000..5e0beff
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/FetchResponseUtilsTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+/**
+ * Test of FetchResponseUtils
+ */
+public class FetchResponseUtilsTest {
+
+  @Test
+  public void testSimpleResponse() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setHttpStatusCode(999)
+        .create();
+    Map<String, Object> obj = FetchResponseUtils.getResponseAsJson(response, "key", "body", false);
+
+    JsonAssert.assertObjectEquals("{'rc':999,'id':'key',body:'body'}", obj);
+  }
+
+  @Test
+  public void testMetadata() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setHttpStatusCode(999)
+        .setMetadata("metaname", "metavalue")
+        .setMetadata("more meta", "more value")
+        .create();
+    Map<String, Object> obj = FetchResponseUtils.getResponseAsJson(response, null, "body", false);
+
+    JsonAssert.assertObjectEquals(
+        "{rc:999,body:'body',metaname:'metavalue','more meta':'more value'}", obj);
+  }
+
+  @Test
+  public void testHeaders() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setHttpStatusCode(999)
+        .setHeader("Set-Cookie", "cookie")
+        .setHeader("location", "here")
+        .create();
+    Map<String, Object> obj = FetchResponseUtils.getResponseAsJson(response, "key", "body", false);
+    JsonAssert.assertObjectEquals(
+        "{rc:999,id:'key',body:'body',headers:{set-cookie:['cookie'],location:['here']}}", obj);
+  }
+
+  @Test
+  public void testMultiValuedHeaders() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setHttpStatusCode(999)
+        .addHeader("Set-Cookie", "cookie")
+        .addHeader("Set-Cookie", "cookie2")
+        .addHeader("Set-Cookie", "cookie3")
+        .create();
+    Map<String, Object> obj = FetchResponseUtils.getResponseAsJson(response, "key", "body", false);
+    JsonAssert.assertObjectEquals(
+        "{rc:999,id:'key',body:'body',headers:{set-cookie:['cookie','cookie2','cookie3']}}", obj);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetELResolverTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetELResolverTest.java
new file mode 100644
index 0000000..6b889d6
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetELResolverTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.expressions.Expressions;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.el.ELContext;
+import javax.el.ELResolver;
+import javax.el.ValueExpression;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Test of GadgetELResolver.
+ */
+public class GadgetELResolverTest {
+  private UserPrefs userPrefs;
+  private String viewParams;
+  private ELResolver resolver;
+  private Expressions expressions;
+  private ELContext context;
+
+  @Before
+  public void setUp() throws Exception {
+    GadgetContext gadgetContext = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if ("view-params".equals(name)) {
+          return viewParams;
+        }
+
+        return null;
+      }
+
+      @Override
+      public UserPrefs getUserPrefs() {
+        return userPrefs;
+      }
+    };
+
+    resolver = new GadgetELResolver(gadgetContext);
+    expressions = Expressions.forTesting();
+
+    context = expressions.newELContext(resolver);
+  }
+
+  @Test
+  public void getPrefs() {
+    userPrefs = new UserPrefs(ImmutableMap.of("foo", "bar"));
+    ValueExpression expression = expressions.parse("${UserPrefs.foo}", String.class);
+
+    assertEquals("bar", expression.getValue(context));
+
+    expression = expressions.parse("${UserPrefs.wrongKey}", String.class);
+    assertEquals("", expression.getValue(context));
+  }
+
+  @Test
+  public void getPrefsEmpty() {
+    userPrefs = UserPrefs.EMPTY;
+    ValueExpression expression = expressions.parse("${UserPrefs.foo}", String.class);
+    assertEquals("", expression.getValue(context));
+  }
+
+  @Test
+  public void testViewParams() {
+    viewParams = "{foo: 'bar'}";
+
+    ValueExpression expression = expressions.parse("${ViewParams.foo}", String.class);
+    assertEquals("bar", expression.getValue(context));
+
+    expression = expressions.parse("${ViewParams.wrongKey}", String.class);
+    assertEquals("", expression.getValue(context));
+  }
+
+  @Test
+  public void testViewParamsEmpty() {
+    ValueExpression expression = expressions.parse("${ViewParams.foo}", String.class);
+    assertEquals("", expression.getValue(context));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java
new file mode 100644
index 0000000..27a9106
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/GadgetTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LocaleSpec;
+import org.apache.shindig.gadgets.spec.View;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Tests for Gadget
+ */
+public class GadgetTest extends EasyMockTestCase {
+  private final static String SPEC_URL = "http://example.org/gadget.xml";
+  private final static String SPEC_XML
+      = "<Module>" +
+        "<ModulePrefs title='title'>" +
+        "  <Preload href='http://example.org/foo'/>" +
+        "  <Locale>" +
+        "    <msg name='name'>VALUE</msg>" +
+        "  </Locale>" +
+        "</ModulePrefs>" +
+        "<Content type='html'>DEFAULT VIEW</Content>" +
+        "<Content view='one' type='html'>VIEW ONE</Content>" +
+        "<Content view='two' type='html'>VIEW TWO</Content>" +
+        "</Module>";
+
+  private final DummyContext context = new DummyContext();
+
+  @Test
+  public void getLocale() throws Exception {
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(new GadgetSpec(Uri.parse(SPEC_URL), SPEC_XML));
+
+    LocaleSpec localeSpec = gadget.getLocale();
+    assertEquals("VALUE", localeSpec.getMessageBundle().getMessages().get("name"));
+  }
+
+  @Test
+  public void testGetFeatures() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"hello\">" +
+                 "<Require feature=\"required1\"/>" +
+                 "</ModulePrefs>" +
+                 "<Content type=\"html\"/>" +
+                 "</Module>";
+    FeatureRegistry registry = mock(FeatureRegistry.class, true);
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setGadgetFeatureRegistry(registry)
+        .setSpec(new GadgetSpec(Uri.parse(SPEC_URL), xml));
+    Collection<String> needed = Lists.newArrayList(gadget.getSpec().getModulePrefs().getFeatures().keySet());
+    List<String> returned = Lists.newArrayList(needed);
+    // Call should only happen once, and be cached from there on out.
+    expect(registry.getFeatures(eq(needed))).andReturn(returned).anyTimes();
+    replay();
+    List<String> requiredFeatures1 = gadget.getAllFeatures();
+    assertEquals(returned, requiredFeatures1);
+    List<String> requiredFeatures2 = gadget.getAllFeatures();
+    assertSame(returned, requiredFeatures2);
+    verify();
+  }
+
+  @Test
+  public void testGetView1Features() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"hello\">" +
+                 "<Require feature=\"required1\"/>" +
+                 "<Require feature=\"requiredview1\" views=\"default\"/>" +
+                 "<Require feature=\"requiredview2\" views=\"view2\"/>" +
+                 "</ModulePrefs>" +
+                 "<Content views=\"view1, default\" type=\"html\"/>" +
+                 "<Content views=\"view2\" type=\"html\"/>" +
+                 "</Module>";
+    FeatureRegistry registry = mock(FeatureRegistry.class, true);
+    Gadget gadget = new Gadget()
+    		.setContext(context)
+        .setGadgetFeatureRegistry(registry)
+        .setSpec(new GadgetSpec(Uri.parse(SPEC_URL), xml));
+    Collection<String> needed = Lists.newArrayList(gadget.getSpec().getModulePrefs().getViewFeatures(GadgetSpec.DEFAULT_VIEW).keySet());
+    List<String> returned = Lists.newArrayList(needed);
+    // Call should only happen once, and be cached from there on out.
+    expect(registry.getFeatures(eq(needed))).andReturn(returned).anyTimes();
+    replay();
+    List<String> requiredFeatures = Lists.newArrayList(gadget.getViewFeatures().keySet());
+    assertEquals(returned, requiredFeatures);
+    assertTrue(requiredFeatures.contains("requiredview1"));
+    assertTrue(requiredFeatures.contains("core"));
+    assertTrue(!requiredFeatures.contains("requiredview2"));
+
+    verify();
+  }
+
+  @Test
+  public void testGetView2Features() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"hello\">" +
+                 "<Require feature=\"required\"/>" +
+                 "<Require feature=\"requiredview1\" views=\"default\"/>" +
+                 "<Require feature=\"requiredview2\" views=\"view2\"/>" +
+                 "</ModulePrefs>" +
+                 "<Content views=\"view1, default\" type=\"html\"/>" +
+                 "<Content views=\"view2\" type=\"html\"/>" +
+                 "</Module>";
+    FeatureRegistry registry = mock(FeatureRegistry.class, true);
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setGadgetFeatureRegistry(registry)
+        .setSpec(new GadgetSpec(Uri.parse(SPEC_URL), xml));
+    List<Element> viewEles = Lists.newArrayList();
+    gadget.setCurrentView(new View("view2", viewEles, null));
+    Collection<String> needed = Lists.newArrayList(gadget.getSpec().getModulePrefs().getViewFeatures("view2").keySet());
+    List<String> returned = Lists.newArrayList(needed);
+    // Call should only happen once, and be cached from there on out.
+    expect(registry.getFeatures(eq(needed))).andReturn(returned).anyTimes();
+    replay();
+    List<String> requiredFeatures = Lists.newArrayList(gadget.getViewFeatures().keySet());
+    assertEquals(returned, requiredFeatures);
+    assertEquals(3, requiredFeatures.size());
+    assertTrue(!requiredFeatures.contains("requiredview1"));
+    assertTrue(requiredFeatures.contains("required"));
+    assertTrue(requiredFeatures.contains("core"));
+    assertTrue(requiredFeatures.contains("requiredview2"));
+
+    verify();
+  }
+
+
+  private static class DummyContext extends GadgetContext {
+    public String view = super.getView();
+    public String container = super.getContainer();
+
+    protected DummyContext() {
+    }
+
+    @Override
+    public String getView() {
+      return view;
+    }
+
+    @Override
+    public String getContainer() {
+      return container;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java
new file mode 100644
index 0000000..004cfb7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/HashLockedDomainServiceTest.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.apache.shindig.gadgets.HashLockedDomainService.LOCKED_DOMAIN_REQUIRED_KEY;
+import static org.apache.shindig.gadgets.HashLockedDomainService.LOCKED_DOMAIN_SUFFIX_KEY;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.uri.HashShaLockedDomainPrefixGenerator;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+
+public class HashLockedDomainServiceTest extends EasyMockTestCase {
+
+  private HashLockedDomainService lockedDomainService;
+  private HashShaLockedDomainPrefixGenerator ldgen = new HashShaLockedDomainPrefixGenerator();
+  private Gadget wantsLocked = null;
+  private Gadget notLocked = null;
+  private Gadget wantsSecurityToken = null;
+  private Gadget wantsBoth = null;
+  private ContainerConfig requiredConfig;
+  private ContainerConfig enabledConfig;
+
+  @SuppressWarnings("unchecked")
+  private Gadget makeGadget(boolean wantsLocked, boolean wantsSecurityToken, String url) {
+
+    List<String> gadgetFeatures = Lists.newArrayList();
+    String requires = "";
+    if (wantsLocked || wantsSecurityToken) {
+      gadgetFeatures.add("locked-domain");
+      if (wantsLocked) {
+        requires += "  <Require feature='locked-domain'/>";
+      }
+      if (wantsSecurityToken) {
+        requires += "  <Require feature='security-token'/>";
+        gadgetFeatures.add("security-token");
+      }
+    }
+
+    String gadgetXml = "<Module><ModulePrefs title=''>" + requires + "</ModulePrefs><Content/></Module>";
+
+    GadgetSpec spec = null;
+    try {
+      spec = new GadgetSpec(Uri.parse(url), gadgetXml);
+    } catch (GadgetException e) {
+      return null;
+    }
+
+    FeatureRegistry registry = mock(FeatureRegistry.class);
+    expect(registry.getFeatures(isA(Collection.class))).andReturn(gadgetFeatures).anyTimes();
+
+    return new Gadget().setSpec(spec).setContext(new GadgetContext()).setGadgetFeatureRegistry(registry);
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    requiredConfig = new BasicContainerConfig();
+    requiredConfig.newTransaction().addContainer(
+        makeContainer(ContainerConfig.DEFAULT_CONTAINER, LOCKED_DOMAIN_SUFFIX_KEY,
+            "-a.example.com:8080", LOCKED_DOMAIN_REQUIRED_KEY, true)).commit();
+
+    enabledConfig = new BasicContainerConfig();
+    enabledConfig.newTransaction().addContainer(
+        makeContainer(ContainerConfig.DEFAULT_CONTAINER, LOCKED_DOMAIN_SUFFIX_KEY,
+            "-a.example.com:8080")).commit();
+
+    wantsLocked = makeGadget(true, false, "http://somehost.com/somegadget.xml");
+    notLocked = makeGadget(false, false, "not-locked");
+    wantsSecurityToken = makeGadget(false, true, "http://somehost.com/securitytoken.xml");
+    wantsBoth =
+        makeGadget(true, true, "http://somehost.com/tokenandlocked.xml");
+  }
+
+  @Test
+  public void testDisabledGlobally() {
+    replay();
+
+    lockedDomainService = new HashLockedDomainService(requiredConfig, false, ldgen);
+    assertTrue(lockedDomainService.isSafeForOpenProxy("anywhere.com"));
+    assertTrue(lockedDomainService.isSafeForOpenProxy("embed.com"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", wantsLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", notLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", wantsSecurityToken, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", wantsBoth, "default"));
+
+    lockedDomainService = new HashLockedDomainService(enabledConfig, false, ldgen);
+    assertTrue(lockedDomainService.isSafeForOpenProxy("anywhere.com"));
+    assertTrue(lockedDomainService.isSafeForOpenProxy("embed.com"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", wantsLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", notLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", wantsSecurityToken, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("embed.com", wantsBoth, "default"));
+  }
+
+  @Test
+  public void testEnabledForGadget() throws GadgetException {
+    replay();
+
+    lockedDomainService = new HashLockedDomainService(enabledConfig, true, ldgen);
+    assertFalse(lockedDomainService.isSafeForOpenProxy("images-a.example.com:8080"));
+    assertFalse(lockedDomainService.isSafeForOpenProxy("-a.example.com:8080"));
+    assertTrue(lockedDomainService.isSafeForOpenProxy("embed.com"));
+    assertFalse(lockedDomainService.isGadgetValidForHost("www.example.com", wantsLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "default"));
+    assertFalse(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsSecurityToken, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "h2nlf2a2dqou2lul3n50jb4v7e8t34kc-a.example.com:8080", wantsBoth, "default"));
+
+    String target = lockedDomainService.getLockedDomainForGadget(wantsLocked, "default");
+    assertEquals("8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", target);
+
+    target = lockedDomainService.getLockedDomainForGadget(wantsBoth, "default");
+    assertEquals("h2nlf2a2dqou2lul3n50jb4v7e8t34kc-a.example.com:8080", target);
+
+    lockedDomainService.setLockSecurityTokens(true);
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "lrrq12l8s5flpqcjoj1h1872lp9p93nk-a.example.com:8080", wantsSecurityToken, "default"));
+    target = lockedDomainService.getLockedDomainForGadget(wantsSecurityToken, "default");
+    assertEquals("lrrq12l8s5flpqcjoj1h1872lp9p93nk-a.example.com:8080", target);
+
+    // Direct includes work as before.
+    target = lockedDomainService.getLockedDomainForGadget(wantsLocked, "default");
+    assertEquals("8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", target);
+
+    target = lockedDomainService.getLockedDomainForGadget(wantsBoth, "default");
+    assertEquals("h2nlf2a2dqou2lul3n50jb4v7e8t34kc-a.example.com:8080", target);
+  }
+
+  @Test
+  public void testNotEnabledForGadget() throws GadgetException {
+    replay();
+
+    lockedDomainService = new HashLockedDomainService(enabledConfig, true, ldgen);
+
+    assertFalse(lockedDomainService.isSafeForOpenProxy("images-a.example.com:8080"));
+    assertFalse(lockedDomainService.isSafeForOpenProxy("-a.example.com:8080"));
+    assertTrue(lockedDomainService.isSafeForOpenProxy("embed.com"));
+
+    assertTrue(lockedDomainService.isGadgetValidForHost("www.example.com", notLocked, "default"));
+    assertFalse(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", notLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "auvn86n7q0l4ju2tq5cq8akotcjlda66-a.example.com:8080", notLocked, "default"));
+    assertNull(lockedDomainService.getLockedDomainForGadget(notLocked, "default"));
+  }
+
+  @Test
+  public void testRequiredForContainer() throws GadgetException {
+    replay();
+
+    lockedDomainService = new HashLockedDomainService(requiredConfig, true, ldgen);
+
+    assertFalse(lockedDomainService.isSafeForOpenProxy("images-a.example.com:8080"));
+    assertFalse(lockedDomainService.isSafeForOpenProxy("-a.example.com:8080"));
+    assertTrue(lockedDomainService.isSafeForOpenProxy("embed.com"));
+
+    assertFalse(lockedDomainService.isGadgetValidForHost("www.example.com", wantsLocked, "default"));
+    assertFalse(lockedDomainService.isGadgetValidForHost("www.example.com", notLocked, "default"));
+
+    String target = lockedDomainService.getLockedDomainForGadget(wantsLocked, "default");
+    assertEquals("8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", target);
+    target = lockedDomainService.getLockedDomainForGadget(notLocked, "default");
+    assertEquals("auvn86n7q0l4ju2tq5cq8akotcjlda66-a.example.com:8080", target);
+
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "default"));
+    assertFalse(lockedDomainService.isGadgetValidForHost(
+        "auvn86n7q0l4ju2tq5cq8akotcjlda66-a.example.com:8080", wantsLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "auvn86n7q0l4ju2tq5cq8akotcjlda66-a.example.com:8080", notLocked, "default"));
+    assertFalse(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", notLocked, "default"));
+
+  }
+
+  @Test
+  public void testMissingConfig() throws Exception {
+    ContainerConfig containerMissingConfig = new BasicContainerConfig();
+    containerMissingConfig.newTransaction().addContainer(makeContainer(ContainerConfig.DEFAULT_CONTAINER)).commit();
+
+    lockedDomainService = new HashLockedDomainService(containerMissingConfig, true, ldgen);
+    assertFalse(lockedDomainService.isGadgetValidForHost("www.example.com", wantsLocked, "default"));
+    assertTrue(lockedDomainService.isGadgetValidForHost("www.example.com", notLocked, "default"));
+  }
+
+  @Test
+  public void testMultiContainer() throws Exception {
+    ContainerConfig inheritsConfig = new BasicContainerConfig();
+    inheritsConfig
+        .newTransaction()
+        .addContainer(
+            makeContainer(ContainerConfig.DEFAULT_CONTAINER, LOCKED_DOMAIN_SUFFIX_KEY,
+                "-a.example.com:8080", LOCKED_DOMAIN_REQUIRED_KEY, true))
+        .addContainer(makeContainer("other"))
+        .commit();
+
+    lockedDomainService = new HashLockedDomainService(inheritsConfig, true, ldgen);
+    assertFalse(lockedDomainService.isGadgetValidForHost("www.example.com", wantsLocked, "other"));
+    assertFalse(lockedDomainService.isGadgetValidForHost("www.example.com", notLocked, "other"));
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "other"));
+  }
+
+  @Test
+  public void testConfigurationChanged() throws Exception {
+    ContainerConfig config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(makeContainer(ContainerConfig.DEFAULT_CONTAINER))
+        .addContainer(
+            makeContainer("container", LOCKED_DOMAIN_REQUIRED_KEY, true, LOCKED_DOMAIN_SUFFIX_KEY,
+                "-a.example.com:8080"))
+        .commit();
+
+    lockedDomainService = new HashLockedDomainService(config, true, ldgen);
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "container"));
+    assertFalse(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "other"));
+
+    config.newTransaction().addContainer(makeContainer(
+        "other", LOCKED_DOMAIN_REQUIRED_KEY, true, LOCKED_DOMAIN_SUFFIX_KEY, "-a.example.com:8080"))
+        .commit();
+    lockedDomainService.getConfigObserver().containersChanged(
+        config, ImmutableSet.of("other"), ImmutableSet.<String>of());
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "container"));
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "other"));
+
+    config.newTransaction().removeContainer("container").commit();
+    assertFalse(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "container"));
+    assertTrue(lockedDomainService.isGadgetValidForHost(
+        "8uhr00296d2o3sfhqilj387krjmgjv3v-a.example.com:8080", wantsLocked, "other"));
+  }
+
+  private Map<String, Object> makeContainer(String name, Object... props) {
+    ImmutableMap.Builder<String, Object> builder =
+        ImmutableMap.<String, Object>builder().put(ContainerConfig.CONTAINER_KEY, name);
+    for (int i = 0; i < props.length; i += 2) {
+      builder.put((String) props[i], props[i + 1]);
+    }
+    return builder.build();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/JsCompileModeTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/JsCompileModeTest.java
new file mode 100644
index 0000000..e90fc7b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/JsCompileModeTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+public class JsCompileModeTest {
+  @Test
+  public void testValueOfParam() {
+    assertEquals(JsCompileMode.COMPILE_CONCAT, JsCompileMode.valueOfParam(null));
+    assertEquals(JsCompileMode.COMPILE_CONCAT, JsCompileMode.valueOfParam("0"));
+    assertEquals(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL, JsCompileMode.valueOfParam("1"));
+    assertEquals(JsCompileMode.CONCAT_COMPILE_EXPORT_EXPLICIT, JsCompileMode.valueOfParam("2"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/BasicGadgetAdminStoreTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/BasicGadgetAdminStoreTest.java
new file mode 100644
index 0000000..48bd0b8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/BasicGadgetAdminStoreTest.java
@@ -0,0 +1,506 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import static org.easymock.EasyMock.eq;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.admin.FeatureAdminData.Type;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.ModulePrefs;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+/**
+ * @version $Id: $
+ */
+public class BasicGadgetAdminStoreTest extends EasyMockTestCase {
+
+  private static final String SAMPLE_STORE = "{" + "\"default\" : {" + "\"gadgets\" : {"
+          + "\"http://www.google.com:80/ig/modules/horoscope.xml\" : {"
+          + "\"features\" : {"
+          + "\"names\" : [\"views\", \"tabs\", \"setprefs\", \"dynamic-height\", \"settitle\"],"
+          + "\"type\" : \"whitelist\"" + "}},"
+          + "\"http://www.labpixies.com/campaigns/todo/todo.xml\" : {"
+          + "\"features\" : {"
+          + "\"names\" : [\"setprefs\", \"dynamic-height\", \"views\"],"
+          + "\"type\" : \"blacklist\"" + "}},"
+          + "\"https://foo.com/*\" : {"
+          + "\"features\" : {"
+          + "\"names\" : []" + "}},"
+          + "\"http://*\" : {"
+          + "\"features\" : {"
+          + "\"names\" : [],"
+          + "\"type\" : \"whitelist\""
+          + "},"
+          + "\"rpc\" : {"
+          + "\"additionalServiceIds\" : [\"rpc1\", \"rpc2\"]"
+          +"}}}"
+          + "}}";
+
+  private static final String DEFAULT = "default";
+  private static final String HOROSCOPE = "http://www.google.com/ig/modules/horoscope.xml";
+  private static final String HOROSCOPE_WITH_PORT = "http://www.google.com:80/ig/modules/horoscope.xml";
+  private static final String TODO = "http://www.labpixies.com/campaigns/todo/todo.xml";
+  private static final String TEST_GADGET = "http://www.example.com/gadget.xml";
+  private static final String FOO_GADGET = "https://foo.com/*";
+  private static final String HTTP_GADGET = "http://*";
+  private Set<String> HOROSCOPE_FEATURES = Sets.newHashSet("views", "tabs", "setprefs",
+          "dynamic-height", "settitle", "core");
+  private Set<String> TODO_FEATURES = Sets.newHashSet("views", "setprefs", "dynamic-height");
+  private Set<String> FOO_FEATURES = Sets.newHashSet("core");
+  private Set<String> HTTP_FEATURES = Sets.newHashSet("core");
+
+  private final FeatureRegistry mockRegistry = mock(FeatureRegistry.class);
+  private final Gadget mockGadget = mock(Gadget.class);
+  private final GadgetContext mockContext = mock(GadgetContext.class);
+  private final GadgetSpec mockSpec = mock(GadgetSpec.class);
+  private final ModulePrefs mockPrefs = mock(ModulePrefs.class);
+  private final ContainerConfig enabledConfig = new FakeContainerConfig(true, true);
+  private final ContainerConfig disabledConfig = new FakeContainerConfig(false, false);
+
+  private BasicGadgetAdminStore enabledStore;
+  private BasicGadgetAdminStore disabledStore;
+  private GadgetAdminData horoscopeAdminData;
+  private GadgetAdminData todoAdminData;
+  private GadgetAdminData fooAdminData;
+  private GadgetAdminData httpAdminData;
+  private ContainerAdminData defaultAdminData;
+  private FeatureRegistryProvider featureRegistryProvider;
+  private RpcAdminData rpcAdminData;
+
+  @Before
+  public void setUp() throws Exception {
+    featureRegistryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) throws GadgetException {
+        return mockRegistry;
+      }
+    };
+
+    rpcAdminData = new RpcAdminData(Sets.newHashSet("rpc1", "rpc2"));
+
+    enabledStore = new BasicGadgetAdminStore(featureRegistryProvider, enabledConfig,
+        new ServerAdminData());
+    enabledStore.init(SAMPLE_STORE);
+
+    disabledStore = new BasicGadgetAdminStore(featureRegistryProvider, disabledConfig,
+        new ServerAdminData());
+
+    horoscopeAdminData = new GadgetAdminData(new FeatureAdminData(HOROSCOPE_FEATURES,
+            Type.WHITELIST), new RpcAdminData());
+    todoAdminData = new GadgetAdminData(new FeatureAdminData(TODO_FEATURES,
+            Type.BLACKLIST), new RpcAdminData());
+    fooAdminData = new GadgetAdminData(new FeatureAdminData(FOO_FEATURES,
+            Type.WHITELIST), new RpcAdminData());
+    httpAdminData = new GadgetAdminData(new FeatureAdminData(HTTP_FEATURES,
+            Type.WHITELIST), rpcAdminData);
+
+    defaultAdminData = new ContainerAdminData();
+    defaultAdminData.addGadgetAdminData(TODO, todoAdminData);
+    defaultAdminData.addGadgetAdminData(HOROSCOPE_WITH_PORT, horoscopeAdminData);
+    defaultAdminData.addGadgetAdminData(FOO_GADGET, fooAdminData);
+    defaultAdminData.addGadgetAdminData(HTTP_GADGET, httpAdminData);
+
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    enabledStore = null;
+    horoscopeAdminData = null;
+    todoAdminData = null;
+    defaultAdminData = null;
+    rpcAdminData = null;
+  }
+
+  private void mockGadget(List<Feature> allFeatures) {
+    mockGadget(allFeatures, DEFAULT, TEST_GADGET);
+  }
+
+  private void mockGadget(List<Feature> allFeatures, String container) {
+    mockGadget(allFeatures, container, TEST_GADGET);
+  }
+
+  private void mockGadget(List<Feature> allFeatures, String container, String gadgetUrl) {
+    mockGadgetContext(container);
+    mockGadgetSpec(allFeatures, gadgetUrl);
+    EasyMock.expect(mockGadget.getContext()).andReturn(mockContext).anyTimes();
+    EasyMock.expect(mockGadget.getSpec()).andReturn(mockSpec).anyTimes();
+  }
+
+  private void mockGadgetContext(String container) {
+    EasyMock.expect(mockContext.getContainer()).andReturn(container).anyTimes();
+  }
+
+  private void mockGadgetSpec(List<Feature> allFeatures, String gadgetUrl) {
+    mockModulePrefs(allFeatures);
+    EasyMock.expect(mockSpec.getUrl()).andReturn(Uri.parse(gadgetUrl)).anyTimes();
+    EasyMock.expect(mockSpec.getModulePrefs()).andReturn(mockPrefs).anyTimes();
+  }
+
+  private void mockModulePrefs(List<Feature> features) {
+    EasyMock.expect(mockPrefs.getAllFeatures()).andReturn(features).anyTimes();
+  }
+
+  private Feature createMockFeature(String name, boolean required) {
+    Feature feature = mock(Feature.class);
+    EasyMock.expect(feature.getName()).andReturn(name).anyTimes();
+    EasyMock.expect(feature.getRequired()).andReturn(required).anyTimes();
+    return feature;
+  }
+
+  private void mockRegistryForFeatureAdmin(Set<String> allowed, List<String> getFeaturesAllowed,
+          List<String> allGadgetFeatures, List<String> gadgetRequiredFeatureNames) {
+    EasyMock.expect(mockRegistry.getFeatures(eq(Sets.newHashSet(allowed))))
+            .andReturn(Lists.newArrayList(getFeaturesAllowed)).anyTimes();
+    EasyMock.expect(mockRegistry.getFeatures(eq(Lists.newArrayList("core"))))
+            .andReturn(Lists.newArrayList(allGadgetFeatures)).anyTimes();
+    EasyMock.expect(mockRegistry.getFeatures(eq(gadgetRequiredFeatureNames)))
+            .andReturn(allGadgetFeatures).anyTimes();
+  }
+
+  @Test
+  public void testGetGadgetAdminData() {
+    assertEquals(horoscopeAdminData, enabledStore.getGadgetAdminData(DEFAULT, HOROSCOPE));
+    assertEquals(todoAdminData, enabledStore.getGadgetAdminData(DEFAULT, TODO));
+    assertEquals(fooAdminData, enabledStore.getGadgetAdminData(DEFAULT, "https://foo.com/bar/gadget.xml"));
+    assertEquals(fooAdminData, enabledStore.getGadgetAdminData(DEFAULT, "https://foo.com:443/bar/gadget.xml"));
+    assertNull(enabledStore.getGadgetAdminData("my_container", HOROSCOPE));
+    assertEquals(httpAdminData, enabledStore.getGadgetAdminData(DEFAULT, "http://example.com/gadget2.xml"));
+  }
+
+  @Test
+  public void testSetGadgetAdminData() {
+    assertEquals(horoscopeAdminData, enabledStore.getGadgetAdminData(DEFAULT, HOROSCOPE));
+
+    horoscopeAdminData.getFeatureAdminData().addFeature("foo_feature");
+    enabledStore.setGadgetAdminData(DEFAULT, HOROSCOPE, horoscopeAdminData);
+    assertTrue(enabledStore.getGadgetAdminData(DEFAULT, HOROSCOPE).getFeatureAdminData()
+            .getFeatures().contains("foo_feature"));
+
+    assertEquals(httpAdminData, enabledStore.getGadgetAdminData(DEFAULT, "http://example.com/gadget2.xml"));
+    enabledStore.setGadgetAdminData(DEFAULT, "http://example.com/gadget2.xml", todoAdminData);
+    assertEquals(todoAdminData,
+            enabledStore.getGadgetAdminData(DEFAULT, "http://example.com/gadget2.xml"));
+
+    enabledStore.setGadgetAdminData(DEFAULT, "http://example.com/gadget1.xml", null);
+    assertNotNull(enabledStore.getGadgetAdminData(DEFAULT, "http://example.com/gadget1.xml"));
+
+    enabledStore.setGadgetAdminData(DEFAULT, null, horoscopeAdminData);
+    assertNull(enabledStore.getGadgetAdminData(DEFAULT, null));
+  }
+
+  @Test
+  public void testGetContainerAdminData() {
+    assertEquals(defaultAdminData, enabledStore.getContainerAdminData(DEFAULT));
+    assertNull(enabledStore.getContainerAdminData("my_constianer"));
+  }
+
+  @Test
+  public void testSetContainerAdminData() {
+    assertEquals(defaultAdminData, enabledStore.getContainerAdminData(DEFAULT));
+
+    defaultAdminData.removeGadgetAdminData(TODO);
+    enabledStore.setContainerAdminData(DEFAULT, defaultAdminData);
+    assertEquals(defaultAdminData, enabledStore.getContainerAdminData(DEFAULT));
+
+    assertNull(enabledStore.getContainerAdminData("my_container"));
+    enabledStore.setContainerAdminData("my_container", defaultAdminData);
+    assertEquals(defaultAdminData, enabledStore.getContainerAdminData("my_container"));
+
+    enabledStore.setContainerAdminData(null, defaultAdminData);
+    assertNull(enabledStore.getContainerAdminData(null));
+
+    enabledStore.setContainerAdminData("my_container_2", null);
+    assertNotNull(enabledStore.getContainerAdminData("my_container_2"));
+  }
+
+  @Test
+  public void testGetServerAdminData() {
+    ServerAdminData test = new ServerAdminData();
+    test.addContainerAdminData(DEFAULT, defaultAdminData);
+    assertEquals(test, enabledStore.getServerAdminData());
+  }
+
+  @Test
+  public void testBlacklistAll() throws Exception {
+    Set<String> features = Sets.newHashSet();
+    List<String> featuresAndDeps = Lists.newArrayList();
+    List<String> allGadgetFeatures = Lists.newArrayList("dep1", "dep2", "foo1", "foo2", "foo3");
+    FeatureAdminData data = new FeatureAdminData(features, Type.WHITELIST);
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo1", "foo2", "foo3");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(2), true));
+    enabledStore.getContainerAdminData(DEFAULT).addGadgetAdminData(TEST_GADGET,
+            new GadgetAdminData(data, null));
+    mockRegistryForFeatureAdmin(features, featuresAndDeps,
+            allGadgetFeatures, gadgetRequiredFeatureNames);
+    mockGadget(allFeatures);
+    replay();
+    assertFalse(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testWhitelistAll() throws Exception {
+    Set<String> features = Sets.newHashSet();
+    List<String> featuresAndDeps = Lists.newArrayList();
+    List<String> allGadgetFeatures = Lists.newArrayList("dep1", "dep2", "foo1", "foo2", "foo3");
+    FeatureAdminData data = new FeatureAdminData(features, Type.BLACKLIST);
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo1", "foo2", "foo3");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(2), true));
+    enabledStore.getContainerAdminData(DEFAULT).addGadgetAdminData(TEST_GADGET,
+            new GadgetAdminData(data, null));
+    mockRegistryForFeatureAdmin(features, featuresAndDeps,
+            allGadgetFeatures, gadgetRequiredFeatureNames);
+    mockGadget(allFeatures);
+    replay();
+    assertTrue(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testAllowedGadgetWhitelist() throws Exception {
+    Set<String> features = Sets.newHashSet("foo4", "foo3");
+    List<String> featuresAndDeps = Lists.newArrayList("foo4", "dep1", "dep2", "foo3");
+    List<String> allGadgetFeatures = Lists.newArrayList("dep1", "dep2", "foo3", "foo4");
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo3", "foo4");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true));
+    FeatureAdminData data = new FeatureAdminData(features,Type.WHITELIST);
+    enabledStore.getContainerAdminData(DEFAULT).addGadgetAdminData(TEST_GADGET,
+            new GadgetAdminData(data, new RpcAdminData()));
+    mockRegistryForFeatureAdmin(features, featuresAndDeps,
+            allGadgetFeatures, gadgetRequiredFeatureNames);
+    mockGadget(allFeatures);
+    replay();
+    assertTrue(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testDeniedGadgetWhitelist() throws Exception {
+    Set<String> features = Sets.newHashSet("foo4", "foo3");
+    List<String> featuresAndDeps = Lists.newArrayList("foo4", "dep1", "dep2", "foo3");
+    List<String> allGadgetFeatures = Lists.newArrayList("dep1", "dep2", "foo3", "foo4", "foo5");
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo3", "foo4", "foo5");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(2), true));
+    FeatureAdminData data = new FeatureAdminData(features,Type.WHITELIST);
+    enabledStore.getContainerAdminData(DEFAULT).addGadgetAdminData(TEST_GADGET,
+            new GadgetAdminData(data, new RpcAdminData()));
+    mockRegistryForFeatureAdmin(features, featuresAndDeps,
+            allGadgetFeatures, gadgetRequiredFeatureNames);
+    mockGadget(allFeatures);
+    replay();
+    assertFalse(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testAllowedGadgetBlacklist() throws Exception {
+    Set<String> features = Sets.newHashSet("foo5", "foo6");
+    List<String> featuresAndDeps = Lists.newArrayList("foo5", "dep1", "dep2", "foo6");
+    List<String> allGadgetFeatures = Lists.newArrayList("dep1", "dep2", "foo3", "foo4");
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo3", "foo4");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true));
+    FeatureAdminData data = new FeatureAdminData(features,Type.BLACKLIST);
+    enabledStore.getContainerAdminData(DEFAULT).addGadgetAdminData(TEST_GADGET,
+            new GadgetAdminData(data, null));
+    mockRegistryForFeatureAdmin(features, featuresAndDeps,
+            allGadgetFeatures, gadgetRequiredFeatureNames);
+    mockGadget(allFeatures);
+    replay();
+    assertTrue(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testDeniedGadgetBlacklist() throws Exception {
+    Set<String> features = Sets.newHashSet("foo4", "foo3");
+    List<String> featuresAndDeps = Lists.newArrayList("foo5", "dep1", "dep2", "foo6");
+    List<String> allGadgetFeatures = Lists.newArrayList("dep1", "dep2", "foo3", "foo4");
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo3", "foo4");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true));
+    FeatureAdminData data = new FeatureAdminData(features,Type.BLACKLIST);
+    enabledStore.getContainerAdminData(DEFAULT).addGadgetAdminData(TEST_GADGET,
+            new GadgetAdminData(data, null));
+    mockRegistryForFeatureAdmin(features, featuresAndDeps,
+            allGadgetFeatures, gadgetRequiredFeatureNames);
+    mockGadget(allFeatures);
+    replay();
+    assertFalse(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testDeniedOptionalFeature() throws Exception {
+    Set<String> features = Sets.newHashSet("foo4", "foo3");
+    List<String> featuresAndDeps = Lists.newArrayList("foo4", "dep1", "dep2", "foo3");
+    List<String> allGadgetFeatures = Lists.newArrayList("dep1", "dep2", "foo3", "foo4");
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo3", "foo4");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true),
+            createMockFeature("foo5", false));
+    FeatureAdminData data = new FeatureAdminData(features,Type.WHITELIST);
+    enabledStore.getContainerAdminData(DEFAULT).addGadgetAdminData(TEST_GADGET,
+            new GadgetAdminData(data, new RpcAdminData()));
+    mockRegistryForFeatureAdmin(features, featuresAndDeps,
+            allGadgetFeatures, gadgetRequiredFeatureNames);
+    mockGadget(allFeatures);
+    replay();
+    assertTrue(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testFeatureAdminNullGadgetData() throws Exception {
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo3", "foo4");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true));
+    mockGadget(allFeatures, DEFAULT, "https://example.com/dontexist.xml");
+    replay();
+    assertFalse(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testFeatureAdminNullContainerData() throws Exception {
+    List<String> gadgetRequiredFeatureNames = Lists.newArrayList("foo3", "foo4");
+    List<Feature> allFeatures = Lists.newArrayList(
+            createMockFeature(gadgetRequiredFeatureNames.get(0), true),
+            createMockFeature(gadgetRequiredFeatureNames.get(1), true));
+    mockGadget(allFeatures, "foocontainer");
+    replay();
+    assertFalse(enabledStore.checkFeatureAdminInfo(mockGadget));
+    assertTrue(disabledStore.checkFeatureAdminInfo(mockGadget));
+    verify();
+  }
+
+  @Test
+  public void testIsWhiteListed() throws Exception {
+    assertTrue(enabledStore.isWhitelisted(DEFAULT, HOROSCOPE));
+    assertTrue(enabledStore.isWhitelisted(DEFAULT, TEST_GADGET));
+    assertFalse(enabledStore.isWhitelisted(DEFAULT, "https://example.com/gadget.xml"));
+    assertFalse(enabledStore.isWhitelisted("myContainer", HOROSCOPE));
+    assertTrue(enabledStore.isWhitelisted(DEFAULT, "http://foo.com/gadget.xml"));
+    assertTrue(enabledStore.isWhitelisted(DEFAULT, "http://example.com/gadget.xml"));
+    assertTrue(disabledStore.isWhitelisted(DEFAULT, HOROSCOPE));
+    assertTrue(disabledStore.isWhitelisted(DEFAULT, TEST_GADGET));
+    assertTrue(disabledStore.isWhitelisted("myContainer", HOROSCOPE));
+  }
+
+  @Test
+  public void testIsAllowedFeature() throws Exception {
+    mockGadget(ImmutableList.<Feature> of(), DEFAULT, TODO);
+    Feature denied = createMockFeature("setprefs", true);
+    Feature allowed = createMockFeature("foo", true);
+    replay();
+    assertFalse(enabledStore.isAllowedFeature(denied, mockGadget));
+    assertTrue(enabledStore.isAllowedFeature(allowed, mockGadget));
+    assertTrue(disabledStore.isAllowedFeature(denied, mockGadget));
+    assertTrue(disabledStore.isAllowedFeature(allowed, mockGadget));
+  }
+
+  @Test
+  public void testGetAdditionalRpcServiceIds() throws Exception {
+    mockGadget(ImmutableList.<Feature>of(), DEFAULT, "http://example.com/gadget.xml");
+    replay();
+    assertEquals(Sets.newHashSet("rpc1", "rpc2"),
+            enabledStore.getAdditionalRpcServiceIds(mockGadget));
+    assertEquals(Sets.newHashSet(),
+            disabledStore.getAdditionalRpcServiceIds(mockGadget));
+
+    reset();
+    mockGadget(ImmutableList.<Feature>of(), DEFAULT, "https://example.com/gadget.xml");
+    replay();
+    assertEquals(Sets.newHashSet(),
+            enabledStore.getAdditionalRpcServiceIds(mockGadget));
+    assertEquals(Sets.newHashSet(),
+            disabledStore.getAdditionalRpcServiceIds(mockGadget));
+
+    reset();
+    mockGadget(ImmutableList.<Feature>of(), DEFAULT, HOROSCOPE);
+    replay();
+    assertEquals(Sets.newHashSet(),
+            enabledStore.getAdditionalRpcServiceIds(mockGadget));
+    assertEquals(Sets.newHashSet(),
+            disabledStore.getAdditionalRpcServiceIds(mockGadget));
+  }
+
+  private static class FakeContainerConfig extends BasicContainerConfig {
+    protected final Map<String, Object> data;
+
+    public FakeContainerConfig(boolean enableFeatureAdministration, boolean enableGadgetWhitelist) {
+      data = ImmutableMap
+              .<String, Object> builder()
+              .put("gadgets.admin.enableFeatureAdministration",
+                      new Boolean(enableFeatureAdministration).toString())
+              .put("gadgets.admin.enableGadgetWhitelist", new Boolean(enableGadgetWhitelist))
+              .build();
+    }
+
+    @Override
+    public Object getProperty(String container, String name) {
+      return data.get(name);
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/ContainerAdminDataTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/ContainerAdminDataTest.java
new file mode 100644
index 0000000..16a9da5
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/ContainerAdminDataTest.java
@@ -0,0 +1,219 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.shindig.gadgets.admin.FeatureAdminData.Type;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.caja.util.Sets;
+import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
+
+/**
+ * @since 2.5.0
+ */
+public class ContainerAdminDataTest {
+
+  private static final String VIEWS = "views";
+  private static final String SETPREFS = "setprefs";
+  private static final String TABS = "tabs";
+  private static final String EE = "embedded-experiences";
+  private static final String SELECTION = "selection";
+  private static final String GADGET_URL_1 = "http://sample.com/gadget1.xml";
+  private static final String GADGET_URL_1_WITH_PORT = "http://sample.com:80/gadget1.xml";
+  private static final String GADGET_URL_2 = "http://sample.com/gadget2.xml";
+  private static final String GADGET_URL_3 = "http://example.com/*";
+  private static final String GADGET_URL_4 = "https://sample.com/gadget1.xml";
+  private static final String GADGET_URL_4_WITH_PORT = "https://sample.com:443/gadget1.xml";
+
+  private Set<String> whitelist;
+  private Set<String> blacklist;
+  private FeatureAdminData whitelistFeatures;
+  private FeatureAdminData blacklistFeatures;
+  private GadgetAdminData whitelistData;
+  private GadgetAdminData blacklistData;
+  private Map<String, GadgetAdminData> gadgetMap;
+  private ContainerAdminData validData;
+  private ContainerAdminData emptyData;
+  private ContainerAdminData nullData;
+  private ContainerAdminData defaultData;
+  private RpcAdminData rpcAdminData;
+
+  @Before
+  public void setUp() throws Exception {
+    whitelist = Sets.newHashSet(VIEWS, SETPREFS, TABS);
+    blacklist = Sets.newHashSet(EE, SELECTION);
+    whitelistFeatures = new FeatureAdminData(whitelist, Type.WHITELIST);
+    blacklistFeatures = new FeatureAdminData(blacklist, Type.BLACKLIST);
+    rpcAdminData = new RpcAdminData(Sets.newHashSet("rpc1", "rpc2"));
+
+    whitelistData = new GadgetAdminData(whitelistFeatures, rpcAdminData);
+    blacklistData = new GadgetAdminData(blacklistFeatures, new RpcAdminData());
+
+    gadgetMap = Maps.newHashMap();
+    gadgetMap.put(GADGET_URL_1, whitelistData);
+    gadgetMap.put(GADGET_URL_2, blacklistData);
+    gadgetMap.put(GADGET_URL_3, new GadgetAdminData());
+    gadgetMap.put("http://*", blacklistData);
+    gadgetMap.put(GADGET_URL_4_WITH_PORT, whitelistData);
+
+    validData = new ContainerAdminData(gadgetMap);
+    emptyData = new ContainerAdminData(new HashMap<String, GadgetAdminData>());
+    nullData = new ContainerAdminData(null);
+    defaultData = new ContainerAdminData();
+
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    whitelist = null;
+    blacklist = null;
+    whitelistFeatures = null;
+    blacklistFeatures = null;
+    whitelistData = null;
+    blacklistData = null;
+    gadgetMap = null;
+    validData = null;
+    emptyData = null;
+    nullData = null;
+    defaultData = null;
+    rpcAdminData = null;
+  }
+
+  @Test
+  public void testGetGadgetAdminData() {
+    assertEquals(whitelistData, validData.getGadgetAdminData(GADGET_URL_1));
+    assertEquals(whitelistData, validData.getGadgetAdminData(GADGET_URL_1_WITH_PORT));
+    assertEquals(blacklistData, validData.getGadgetAdminData(GADGET_URL_2));
+    assertEquals(new GadgetAdminData(),
+            validData.getGadgetAdminData("http://example.com/gadgets/gadget.xml"));
+    assertEquals(new GadgetAdminData(),
+            validData.getGadgetAdminData("http://example.com/gadget.xml"));
+    assertEquals(blacklistData, validData.getGadgetAdminData("http://foo.com/gadget.xml"));
+    assertEquals(blacklistData, validData.getGadgetAdminData("http://foo.com:80/gadget.xml"));
+    assertNull(validData.getGadgetAdminData("https://foo.com:80/gadget.xml"));
+    assertEquals(whitelistData, validData.getGadgetAdminData(GADGET_URL_4));
+    assertEquals(whitelistData, validData.getGadgetAdminData(GADGET_URL_4_WITH_PORT));
+    assertNull(emptyData.getGadgetAdminData(GADGET_URL_1));
+    assertNull(nullData.getGadgetAdminData(GADGET_URL_1));
+    assertNull(defaultData.getGadgetAdminData(GADGET_URL_1));
+  }
+
+  @Test
+  public void testGetGadgetAdminMap() {
+    assertEquals(gadgetMap, validData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), emptyData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), nullData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), defaultData.getGadgetAdminMap());
+  }
+
+  @Test
+  public void testEquals() {
+    assertTrue(validData.equals(new ContainerAdminData(gadgetMap)));
+    assertTrue(emptyData.equals(new ContainerAdminData(new HashMap<String, GadgetAdminData>())));
+    assertTrue(defaultData.equals(new ContainerAdminData(new HashMap<String, GadgetAdminData>())));
+    assertTrue(nullData.equals(new ContainerAdminData(null)));
+    assertTrue(emptyData.equals(defaultData));
+    assertFalse(validData.equals(null));
+    assertFalse(validData.equals(new Object()));
+    assertFalse(validData.equals(gadgetMap));
+    assertFalse(validData.equals(emptyData));
+    assertFalse(validData.equals(nullData));
+  }
+
+  @Test
+  public void testAddAndRemove() {
+    defaultData.addGadgetAdminData(GADGET_URL_1, whitelistData);
+    assertEquals(whitelistData, defaultData.getGadgetAdminData(GADGET_URL_1));
+    GadgetAdminData test = defaultData.removeGadgetAdminData(GADGET_URL_1);
+    assertNull(defaultData.getGadgetAdminData(GADGET_URL_1));
+    assertEquals(whitelistData, test);
+
+    defaultData.addGadgetAdminData(null, whitelistData);
+    assertNull(defaultData.getGadgetAdminData(null));
+
+    test = defaultData.removeGadgetAdminData(null);
+    assertNull(defaultData.getGadgetAdminData(null));
+    assertNull(test);
+
+    defaultData.addGadgetAdminData(GADGET_URL_1, null);
+    assertNotNull(defaultData.getGadgetAdminData(GADGET_URL_1));
+
+    validData.addGadgetAdminData(GADGET_URL_2, null);
+    assertNotNull(validData.getGadgetAdminData(GADGET_URL_2));
+  }
+
+  @Test
+  public void testClearGadgetAdminData() {
+    assertEquals(gadgetMap, validData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), nullData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), emptyData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), defaultData.getGadgetAdminMap());
+
+    validData.clearGadgetAdminData();
+    nullData.clearGadgetAdminData();
+    emptyData.clearGadgetAdminData();
+    defaultData.clearGadgetAdminData();
+
+    assertEquals(new HashMap<String, GadgetAdminData>(), validData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), nullData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), emptyData.getGadgetAdminMap());
+    assertEquals(new HashMap<String, GadgetAdminData>(), defaultData.getGadgetAdminMap());
+  }
+
+  @Test
+  public void testHasGadgetAdminData() {
+    assertTrue(validData.hasGadgetAdminData(GADGET_URL_1));
+    assertTrue(validData.hasGadgetAdminData(GADGET_URL_2));
+    assertTrue(validData.hasGadgetAdminData(GADGET_URL_1_WITH_PORT));
+    assertTrue(validData.hasGadgetAdminData("http://example.com/gadget3.xml"));
+    assertTrue(validData.hasGadgetAdminData("http://example.com:80/gadget3.xml"));
+    assertFalse(validData.hasGadgetAdminData("https://example.com/gadget3.xml"));
+    assertTrue(validData.hasGadgetAdminData(GADGET_URL_4));
+    assertTrue(validData.hasGadgetAdminData(GADGET_URL_4_WITH_PORT));
+    assertTrue(validData.hasGadgetAdminData("http://foo.com/gadget.xml"));
+    assertTrue(validData.hasGadgetAdminData("http://foo.com:80/gadget.xml"));
+    assertFalse(validData.hasGadgetAdminData("https://foo.com/gadget.xml"));
+    assertFalse(nullData.hasGadgetAdminData(GADGET_URL_1));
+    assertFalse(emptyData.hasGadgetAdminData(GADGET_URL_2));
+    assertFalse(defaultData.hasGadgetAdminData(GADGET_URL_2));
+  }
+
+  @Test
+  public void testHashCode() {
+    assertEquals(Objects.hashCode(this.gadgetMap), validData.hashCode());
+    assertEquals(Objects.hashCode(Maps.newHashMap()), nullData.hashCode());
+    assertEquals(Objects.hashCode(Maps.newHashMap()), emptyData.hashCode());
+    assertEquals(Objects.hashCode(Maps.newHashMap()), defaultData.hashCode());
+    assertEquals(nullData.hashCode(), emptyData.hashCode());
+    assertFalse(validData.hashCode() == defaultData.hashCode());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/FeatureAdminDataTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/FeatureAdminDataTest.java
new file mode 100644
index 0000000..7092a15
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/FeatureAdminDataTest.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.shindig.gadgets.admin.FeatureAdminData.Type;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.caja.util.Sets;
+import com.google.common.base.Objects;
+
+/**
+ * Tests for feature admin data.
+ *
+ * @version $Id: $
+ */
+public class FeatureAdminDataTest {
+
+  private static final String VIEWS = "views";
+  private static final String SETPREFS = "setprefs";
+  private static final String TABS = "tabs";
+  private static final String EE = "embedded-experiences";
+  private static final String SELECTION = "selection";
+  private Set<String> blacklist;
+  private Set<String> whitelist;
+  private FeatureAdminData whitelistData;
+  private FeatureAdminData blacklistData;
+  private FeatureAdminData nullData;
+  private FeatureAdminData defaultData;
+
+  @Before
+  public void setUp() throws Exception {
+    whitelist = Sets.newHashSet();
+    whitelist.add(VIEWS);
+    whitelist.add(SETPREFS);
+    whitelist.add(TABS);
+
+    blacklist = Sets.newHashSet();
+    blacklist.add(EE);
+    blacklist.add(SELECTION);
+
+    whitelistData = new FeatureAdminData(whitelist, Type.WHITELIST);
+    blacklistData = new FeatureAdminData(blacklist, Type.BLACKLIST);
+    nullData = new FeatureAdminData(null, null);
+    defaultData = new FeatureAdminData();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    whitelist = null;
+    blacklist = null;
+    whitelistData = null;
+    blacklistData = null;
+    nullData = null;
+    defaultData = null;
+  }
+
+  private void validateDefaultFeatures() {
+    assertEquals(whitelist, whitelistData.getFeatures());
+    assertEquals(blacklist, blacklistData.getFeatures());
+    assertEquals(Sets.newHashSet(), nullData.getFeatures());
+    assertEquals(Sets.newHashSet(), defaultData.getFeatures());
+  }
+
+  @Test
+  public void testGetFeatures() {
+    validateDefaultFeatures();
+  }
+
+  @Test
+  public void testAddFeatures() {
+    validateDefaultFeatures();
+
+    Set<String> toAdd = Sets.newHashSet("foo", "bar", null);
+    whitelistData.addFeatures(toAdd);
+    blacklistData.addFeatures(toAdd);
+    nullData.addFeatures(toAdd);
+    defaultData.addFeatures(toAdd);
+
+    Set<String> actuallyAdded = Sets.newHashSet("foo", "bar");
+    whitelist.addAll(actuallyAdded);
+    blacklist.addAll(actuallyAdded);
+    assertEquals(whitelist, whitelistData.getFeatures());
+    assertEquals(blacklist, blacklistData.getFeatures());
+    assertEquals(actuallyAdded, nullData.getFeatures());
+    assertEquals(actuallyAdded, defaultData.getFeatures());
+  }
+
+  @Test
+  public void testAddFeature() {
+    validateDefaultFeatures();
+
+    whitelistData.addFeature("foo");
+    blacklistData.addFeature("foo");
+    nullData.addFeature("foo");
+    defaultData.addFeature("foo");
+    defaultData.addFeature(null);
+
+    whitelist.add("foo");
+    blacklist.add("foo");
+    assertEquals(whitelist, whitelistData.getFeatures());
+    assertEquals(blacklist, blacklistData.getFeatures());
+    assertEquals(Sets.newHashSet("foo"), nullData.getFeatures());
+    assertEquals(Sets.newHashSet("foo"), defaultData.getFeatures());
+  }
+
+  @Test
+  public void testClearFeatures() {
+    validateDefaultFeatures();
+
+    whitelistData.clearFeatures();
+    blacklistData.clearFeatures();
+    nullData.clearFeatures();
+    defaultData.clearFeatures();
+
+    assertEquals(Sets.newHashSet(), whitelistData.getFeatures());
+    assertEquals(Sets.newHashSet(), blacklistData.getFeatures());
+    assertEquals(Sets.newHashSet(), nullData.getFeatures());
+    assertEquals(Sets.newHashSet(), defaultData.getFeatures());
+  }
+
+  @Test
+  public void testRemoveFeatures() {
+    validateDefaultFeatures();
+
+    Set<String> toRemoveWhitelist = Sets.newHashSet(TABS, VIEWS);
+    Set<String> toRemoveBlacklist = Sets.newHashSet(EE);
+    whitelistData.removeFeatures(toRemoveWhitelist);
+    blacklistData.removeFeatures(toRemoveBlacklist);
+    nullData.removeFeatures(toRemoveWhitelist);
+    defaultData.removeFeatures(toRemoveWhitelist);
+
+    assertEquals(Sets.newHashSet(SETPREFS), whitelistData.getFeatures());
+    assertEquals(Sets.newHashSet(SELECTION), blacklistData.getFeatures());
+    assertEquals(Sets.newHashSet(), nullData.getFeatures());
+    assertEquals(Sets.newHashSet(), defaultData.getFeatures());
+  }
+
+  @Test
+  public void testRemoveFeature() {
+    validateDefaultFeatures();
+
+    whitelistData.removeFeature(TABS);
+    blacklistData.removeFeature(SELECTION);
+    nullData.removeFeature(TABS);
+    defaultData.removeFeature(TABS);
+
+    assertEquals(Sets.newHashSet(SETPREFS, VIEWS), whitelistData.getFeatures());
+    assertEquals(Sets.newHashSet(EE), blacklistData.getFeatures());
+    assertEquals(Sets.newHashSet(), nullData.getFeatures());
+    assertEquals(Sets.newHashSet(), defaultData.getFeatures());
+  }
+
+  @Test
+  public void testGetPriority() {
+    assertEquals(Type.WHITELIST, whitelistData.getType());
+    assertEquals(Type.BLACKLIST, blacklistData.getType());
+    assertEquals(Type.WHITELIST, nullData.getType());
+    assertEquals(Type.WHITELIST, defaultData.getType());
+  }
+
+  @Test
+  public void testSetPriority() {
+    whitelistData.setType(Type.BLACKLIST);
+    blacklistData.setType(Type.WHITELIST);
+    nullData.setType(Type.BLACKLIST);
+    defaultData.setType(Type.BLACKLIST);
+
+    assertEquals(Type.BLACKLIST, whitelistData.getType());
+    assertEquals(Type.BLACKLIST, nullData.getType());
+    assertEquals(Type.BLACKLIST, defaultData.getType());
+    assertEquals(Type.WHITELIST, blacklistData.getType());
+
+    nullData.setType(null);
+    assertEquals(Type.WHITELIST, nullData.getType());
+  }
+
+  @Test
+  public void testEquals() {
+    assertTrue(whitelistData.equals(new FeatureAdminData(whitelist, Type.WHITELIST)));
+    assertFalse(whitelistData.equals(new FeatureAdminData(whitelist,Type.BLACKLIST)));
+    assertFalse(whitelistData.equals(new FeatureAdminData(new HashSet<String>(),
+            Type.WHITELIST)));
+    assertFalse(whitelistData.equals(new FeatureAdminData(Sets.newHashSet(EE), Type.WHITELIST)));
+    assertFalse(whitelistData.equals(null));
+    assertTrue(blacklistData.equals(new FeatureAdminData(blacklist, Type.BLACKLIST)));
+    assertTrue(nullData.equals(defaultData));
+    assertFalse(nullData.equals(whitelistData));
+  }
+
+  @Test
+  public void testHashCode() {
+    assertEquals(Objects.hashCode(this.whitelist, Type.WHITELIST), whitelistData.hashCode());
+    assertEquals(Objects.hashCode(this.blacklist, Type.BLACKLIST), blacklistData.hashCode());
+    assertEquals(Objects.hashCode(Sets.newHashSet(), Type.WHITELIST),
+            nullData.hashCode());
+    assertEquals(Objects.hashCode(Sets.newHashSet(), Type.WHITELIST),
+            defaultData.hashCode());
+    assertFalse(Objects.hashCode(this.blacklist, Type.WHITELIST) == whitelistData
+            .hashCode());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/GadgetAdminDataTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/GadgetAdminDataTest.java
new file mode 100644
index 0000000..8b0c810
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/GadgetAdminDataTest.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Set;
+
+import org.apache.shindig.gadgets.admin.FeatureAdminData.Type;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.caja.util.Sets;
+import com.google.common.base.Objects;
+
+/**
+ * @since 2.5.0
+ */
+public class GadgetAdminDataTest {
+
+  private static final String VIEWS = "views";
+  private static final String SETPREFS = "setprefs";
+  private static final String TABS = "tabs";
+  private static final String EE = "embedded-experiences";
+  private static final String SELECTION = "selection";
+  private static final String RPC1 = "rcp1";
+  private static final String RPC2 = "rpc2";
+  private Set<String> whitelist;
+  private Set<String> blacklist;
+  private Set<String> rpcServiceIds;
+  private FeatureAdminData whitelistFeatures;
+  private FeatureAdminData blacklistFeatures;
+  private RpcAdminData rpcAdminData;
+  private GadgetAdminData whitelistInfo;
+  private GadgetAdminData blacklistInfo;
+  private GadgetAdminData nullInfo;
+  private GadgetAdminData defaultInfo;
+
+  @Before
+  public void setUp() throws Exception {
+    whitelist = Sets.newHashSet(VIEWS, SETPREFS, TABS);
+    blacklist = Sets.newHashSet(EE, SELECTION);
+    rpcServiceIds = Sets.newHashSet(RPC1, RPC2);
+    whitelistFeatures = new FeatureAdminData(whitelist, Type.WHITELIST);
+    blacklistFeatures = new FeatureAdminData(blacklist, Type.BLACKLIST);
+    rpcAdminData = new RpcAdminData(rpcServiceIds);
+    whitelistInfo = new GadgetAdminData(whitelistFeatures, rpcAdminData);
+    blacklistInfo = new GadgetAdminData(blacklistFeatures, new RpcAdminData());
+    nullInfo = new GadgetAdminData(null, null);
+    defaultInfo = new GadgetAdminData();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    whitelist = null;
+    whitelistInfo = null;
+    blacklistInfo = null;
+    rpcServiceIds = null;
+    whitelistFeatures = null;
+    blacklistFeatures = null;
+    rpcAdminData = null;
+    nullInfo = null;
+    defaultInfo = null;
+  }
+
+  @Test
+  public void testGetFeatureAdminData() {
+    assertEquals(whitelistFeatures, whitelistInfo.getFeatureAdminData());
+    assertEquals(blacklistFeatures, blacklistInfo.getFeatureAdminData());
+    assertEquals(new FeatureAdminData(), nullInfo.getFeatureAdminData());
+    assertEquals(new FeatureAdminData(), defaultInfo.getFeatureAdminData());
+  }
+
+  @Test
+  public void testSetFeatureAdminData() {
+    assertEquals(whitelistFeatures, whitelistInfo.getFeatureAdminData());
+    whitelistInfo.setFeatureAdminData(null);
+    assertEquals(new FeatureAdminData(), whitelistInfo.getFeatureAdminData());
+
+    assertEquals(blacklistFeatures, blacklistInfo.getFeatureAdminData());
+    blacklistInfo.setFeatureAdminData(whitelistFeatures);
+    assertEquals(whitelistFeatures, blacklistInfo.getFeatureAdminData());
+
+    assertEquals(new FeatureAdminData(), nullInfo.getFeatureAdminData());
+    nullInfo.setFeatureAdminData(whitelistFeatures);
+    assertEquals(whitelistFeatures, nullInfo.getFeatureAdminData());
+
+    assertEquals(new FeatureAdminData(), defaultInfo.getFeatureAdminData());
+    defaultInfo.setFeatureAdminData(whitelistFeatures);
+    assertEquals(whitelistFeatures, defaultInfo.getFeatureAdminData());
+  }
+
+  @Test
+  public void testGetRpcAdminData() {
+    assertEquals(rpcAdminData, whitelistInfo.getRpcAdminData());
+    assertEquals(new RpcAdminData(), blacklistInfo.getRpcAdminData());
+    assertEquals(new RpcAdminData(), nullInfo.getRpcAdminData());
+    assertEquals(new RpcAdminData(), defaultInfo.getRpcAdminData());
+  }
+
+  @Test
+  public void testSetRpcAdminData() {
+    assertEquals(rpcAdminData, whitelistInfo.getRpcAdminData());
+    whitelistInfo.setRpcAdminData(null);
+    assertEquals(new RpcAdminData(), whitelistInfo.getRpcAdminData());
+
+    assertEquals(new RpcAdminData(), blacklistInfo.getRpcAdminData());
+    blacklistInfo.setRpcAdminData(rpcAdminData);
+    assertEquals(rpcAdminData, blacklistInfo.getRpcAdminData());
+
+    assertEquals(new RpcAdminData(), nullInfo.getRpcAdminData());
+    nullInfo.setRpcAdminData(rpcAdminData);
+    assertEquals(rpcAdminData, nullInfo.getRpcAdminData());
+
+    assertEquals(new RpcAdminData(), defaultInfo.getRpcAdminData());
+    defaultInfo.setRpcAdminData(rpcAdminData);
+    assertEquals(rpcAdminData, defaultInfo.getRpcAdminData());
+  }
+
+  @Test
+  public void testEquals() {
+    assertTrue(whitelistInfo.equals(new GadgetAdminData(whitelistFeatures,
+            rpcAdminData)));
+    assertTrue(nullInfo.equals(new GadgetAdminData(null, null)));
+    assertTrue(defaultInfo.equals(new GadgetAdminData()));
+    assertTrue(nullInfo.equals(defaultInfo));
+    assertFalse(whitelistInfo.equals(null));
+    assertFalse(whitelistInfo.equals(new Object()));
+    assertFalse(whitelistInfo.equals(blacklistInfo));
+    assertFalse(whitelistInfo.equals(defaultInfo));
+    assertFalse(whitelistInfo.equals(nullInfo));
+  }
+
+  @Test
+  public void testHashCode() {
+    assertEquals(Objects.hashCode(whitelistFeatures, rpcAdminData),
+            whitelistInfo.hashCode());
+    assertEquals(Objects.hashCode(blacklistFeatures, new RpcAdminData()),
+            blacklistInfo.hashCode());
+    assertEquals(Objects.hashCode(new FeatureAdminData(), new RpcAdminData()),
+            nullInfo.hashCode());
+    assertEquals(Objects.hashCode(new FeatureAdminData(), new RpcAdminData()),
+            defaultInfo.hashCode());
+    assertEquals(nullInfo.hashCode(), defaultInfo.hashCode());
+    assertFalse(blacklistInfo.hashCode() == whitelistInfo.hashCode());
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/RpcAdminDataTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/RpcAdminDataTest.java
new file mode 100644
index 0000000..046d91d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/RpcAdminDataTest.java
@@ -0,0 +1,150 @@
+/*

+ * Licensed to the Apache Software Foundation (ASF) under one

+ * or more contributor license agreements.  See the NOTICE file

+ * distributed with this work for additional information

+ * regarding copyright ownership.  The ASF licenses this file

+ * to you 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.

+ */

+package org.apache.shindig.gadgets.admin;

+

+import static org.junit.Assert.assertEquals;

+import static org.junit.Assert.assertFalse;

+import static org.junit.Assert.assertTrue;

+

+import java.util.Set;

+

+import org.junit.After;

+import org.junit.Before;

+import org.junit.Test;

+

+import com.google.caja.util.Sets;

+import com.google.common.base.Objects;

+

+/**

+ * Unit tests for RpcAdminData.

+ *

+ * @since 2.5.0

+ */

+public class RpcAdminDataTest {

+

+  private Set<String> populatedIds;

+  private RpcAdminData empty;

+  private RpcAdminData populated;

+  private RpcAdminData nullData;

+

+  @Before

+  public void setUp() throws Exception {

+    populatedIds = Sets.newHashSet("rpc1", "rpc2");

+    empty = new RpcAdminData();

+    populated = new RpcAdminData(populatedIds);

+    nullData = new RpcAdminData(null);

+  }

+

+  @After

+  public void tearDown() throws Exception {

+    empty = null;

+    populated = null;

+    populatedIds = null;

+    nullData = null;

+  }

+

+  @Test

+  public void testHashCode() {

+    assertEquals(Objects.hashCode(populatedIds), populated.hashCode());

+    assertEquals(Objects.hashCode(Sets.newHashSet()), empty.hashCode());

+    assertEquals(Objects.hashCode(Sets.newHashSet()), nullData.hashCode());

+    assertEquals(empty.hashCode(), nullData.hashCode());

+    assertFalse(populated.hashCode() == empty.hashCode());

+  }

+

+  @Test

+  public void testGetAdditionalRpcServiceIds() {

+    assertEquals(populatedIds, populated.getAdditionalRpcServiceIds());

+    assertEquals(Sets.newHashSet(), empty.getAdditionalRpcServiceIds());

+    assertEquals(Sets.newHashSet(), nullData.getAdditionalRpcServiceIds());

+    assertEquals(empty.getAdditionalRpcServiceIds(), nullData.getAdditionalRpcServiceIds());

+    assertFalse(populated.getAdditionalRpcServiceIds().equals(empty.getAdditionalRpcServiceIds()));

+  }

+

+  @Test

+  public void testSetAdditionalRpcServiceIds() {

+    assertEquals(populatedIds, populated.getAdditionalRpcServiceIds());

+    Set<String> emptySet = Sets.newHashSet();

+    populated.setAdditionalRpcServiceIds(emptySet);

+    assertEquals(Sets.newHashSet(), populated.getAdditionalRpcServiceIds());

+

+    assertEquals(Sets.newHashSet(), empty.getAdditionalRpcServiceIds());

+    empty.setAdditionalRpcServiceIds(populatedIds);

+    assertEquals(populatedIds, empty.getAdditionalRpcServiceIds());

+

+    assertEquals(Sets.newHashSet(), nullData.getAdditionalRpcServiceIds());

+    nullData.setAdditionalRpcServiceIds(populatedIds);

+    assertEquals(populatedIds, nullData.getAdditionalRpcServiceIds());

+  }

+

+  @Test

+  public void testAddAdditionalRpcServiceId() {

+    assertEquals(populatedIds, populated.getAdditionalRpcServiceIds());

+    Set<String> newIds = Sets.newHashSet(populatedIds);

+    populated.addAdditionalRpcServiceId("rpc3");

+    populated.addAdditionalRpcServiceId(null);

+    newIds.add("rpc3");

+    assertEquals(newIds, populated.getAdditionalRpcServiceIds());

+

+    Set<String> emptyRpcIds = Sets.newHashSet();

+    assertEquals(emptyRpcIds, empty.getAdditionalRpcServiceIds());

+    empty.addAdditionalRpcServiceId("rpc4");

+    empty.addAdditionalRpcServiceId(null);

+    emptyRpcIds.add("rpc4");

+    assertEquals(emptyRpcIds, empty.getAdditionalRpcServiceIds());

+

+    emptyRpcIds = Sets.newHashSet();

+    assertEquals(emptyRpcIds, nullData.getAdditionalRpcServiceIds());

+    nullData.addAdditionalRpcServiceId("rpc4");

+    nullData.addAdditionalRpcServiceId(null);

+    emptyRpcIds.add("rpc4");

+    assertEquals(emptyRpcIds, nullData.getAdditionalRpcServiceIds());

+  }

+

+  @Test

+  public void testRemoveAdditionalRpcServiceId() {

+    assertEquals(populatedIds, populated.getAdditionalRpcServiceIds());

+    populated.removeAdditionalRpcServiceId("rpc1");

+    populated.removeAdditionalRpcServiceId(null);

+    Set<String> newIds = Sets.newHashSet("rpc2");

+    assertEquals(newIds, populated.getAdditionalRpcServiceIds());

+

+    Set<String> emptyRpcIds = Sets.newHashSet();

+    assertEquals(emptyRpcIds, empty.getAdditionalRpcServiceIds());

+    empty.removeAdditionalRpcServiceId("rpc1");

+    empty.removeAdditionalRpcServiceId("");

+    assertEquals(emptyRpcIds, empty.getAdditionalRpcServiceIds());

+

+    emptyRpcIds = Sets.newHashSet();

+    assertEquals(emptyRpcIds, nullData.getAdditionalRpcServiceIds());

+    nullData.removeAdditionalRpcServiceId("rpc1");

+    nullData.removeAdditionalRpcServiceId("");

+    assertEquals(emptyRpcIds, nullData.getAdditionalRpcServiceIds());

+  }

+

+  @Test

+  public void testEqualsObject() {

+    assertTrue(new RpcAdminData(populatedIds).equals(populated));

+    assertTrue(new RpcAdminData().equals(empty));

+    assertTrue(new RpcAdminData().equals(nullData));

+    assertTrue(nullData.equals(empty));

+    assertFalse(populated.equals(empty));

+  }

+

+}

diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/ServerAdminDataTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/ServerAdminDataTest.java
new file mode 100644
index 0000000..edd23ac
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/admin/ServerAdminDataTest.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.admin;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.shindig.gadgets.admin.FeatureAdminData.Type;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.caja.util.Sets;
+import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
+
+/**
+ * @since 2.5.0
+ */
+public class ServerAdminDataTest {
+
+  private static final String VIEWS = "views";
+  private static final String SETPREFS = "setprefs";
+  private static final String TABS = "tabs";
+  private static final String EE = "embedded-experiences";
+  private static final String SELECTION = "selection";
+  private static final String GADGET_URL_1 = "http://sample.com/gadget1.xml";
+  private static final String GADGET_URL_2 = "http://sample.com/gadget2.xml";
+  private static final String DEFAULT = "default";
+  private static final String MY_CONTAINER = "my_container";
+
+  private Set<String> whitelist;
+  private Set<String> blacklist;
+  private FeatureAdminData whitelistFeatures;
+  private FeatureAdminData blacklistFeatures;
+  private GadgetAdminData whitelistInfo;
+  private GadgetAdminData blacklistInfo;
+  private RpcAdminData rpcAdminData;
+  private Map<String, GadgetAdminData> defaultMap;
+  private Map<String, GadgetAdminData> myMap;
+  private Map<String, ContainerAdminData> containerMap;
+  private ContainerAdminData defaultContainerData;
+  private ContainerAdminData myContainerData;
+  private ServerAdminData validData;
+  private ServerAdminData emptyData;
+  private ServerAdminData defaultData;
+  private ServerAdminData nullData;
+
+  @Before
+  public void setUp() throws Exception {
+    whitelist = Sets.newHashSet(VIEWS, SETPREFS, TABS);
+    blacklist = Sets.newHashSet(EE, SELECTION);
+    whitelistFeatures = new FeatureAdminData(whitelist, Type.WHITELIST);
+    blacklistFeatures = new FeatureAdminData(blacklist, Type.BLACKLIST);
+    rpcAdminData = new RpcAdminData(Sets.newHashSet("rpc1", "rpc2"));
+
+    whitelistInfo = new GadgetAdminData(whitelistFeatures, rpcAdminData);
+    blacklistInfo = new GadgetAdminData(blacklistFeatures, new RpcAdminData());
+
+    defaultMap = Maps.newHashMap();
+    defaultMap.put(GADGET_URL_1, whitelistInfo);
+    defaultMap.put(GADGET_URL_2, blacklistInfo);
+
+    myMap = Maps.newHashMap();
+    myMap.put(GADGET_URL_2, whitelistInfo);
+    myMap.put(GADGET_URL_1, new GadgetAdminData());
+
+    defaultContainerData = new ContainerAdminData(defaultMap);
+    myContainerData = new ContainerAdminData(myMap);
+
+    containerMap = Maps.newHashMap();
+    containerMap.put(DEFAULT, defaultContainerData);
+    containerMap.put(MY_CONTAINER, myContainerData);
+
+    validData = new ServerAdminData(containerMap);
+    emptyData = new ServerAdminData(new HashMap<String, ContainerAdminData>());
+    defaultData = new ServerAdminData();
+    nullData = new ServerAdminData(null);
+
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    whitelist = null;
+    blacklist = null;
+    whitelistFeatures = null;
+    blacklistFeatures = null;
+    whitelistInfo = null;
+    blacklistInfo = null;
+    defaultMap = null;
+    myMap = null;
+    containerMap = null;
+    defaultContainerData = null;
+    myContainerData = null;
+    validData = null;
+    emptyData = null;
+    defaultData = null;
+    nullData = null;
+  }
+
+  @Test
+  public void testGetContainerAdminData() {
+    assertEquals(myContainerData, validData.getContainerAdminData(MY_CONTAINER));
+    assertEquals(defaultContainerData, validData.getContainerAdminData(DEFAULT));
+    assertNull(emptyData.getContainerAdminData(MY_CONTAINER));
+    assertNull(nullData.getContainerAdminData(DEFAULT));
+    assertNull(defaultData.getContainerAdminData(MY_CONTAINER));
+  }
+
+  @Test
+  public void testRemoveContainerAdminData() {
+    validData.removeContainerAdminData(DEFAULT);
+    emptyData.removeContainerAdminData(MY_CONTAINER);
+    nullData.removeContainerAdminData(DEFAULT);
+    defaultData.removeContainerAdminData(MY_CONTAINER);
+    Map<String, ContainerAdminData> newMap = Maps.newHashMap();
+    newMap.put(MY_CONTAINER, myContainerData);
+    assertEquals(newMap, validData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), emptyData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), nullData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), defaultData.getContainerAdminDataMap());
+  }
+
+  @Test
+  public void testAddContainerAdminData() {
+    emptyData.addContainerAdminData(MY_CONTAINER, myContainerData);
+    nullData.addContainerAdminData(DEFAULT, defaultContainerData);
+    defaultData.addContainerAdminData(MY_CONTAINER, myContainerData);
+    defaultData.addContainerAdminData(DEFAULT, defaultContainerData);
+    defaultData.addContainerAdminData(null, myContainerData);
+
+    assertEquals(myContainerData, emptyData.getContainerAdminData(MY_CONTAINER));
+    assertEquals(defaultContainerData, nullData.getContainerAdminData(DEFAULT));
+    assertEquals(defaultContainerData, defaultData.getContainerAdminData(DEFAULT));
+    assertEquals(myContainerData, defaultData.getContainerAdminData(MY_CONTAINER));
+    assertNull(defaultData.getContainerAdminData(null));
+  }
+
+  @Test
+  public void testGetContainerAdminDataMap() {
+    assertEquals(containerMap, validData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), emptyData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), nullData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), defaultData.getContainerAdminDataMap());
+  }
+
+  @Test
+  public void testClearContainerAdminData() {
+    validData.clearContainerAdminData();
+    emptyData.clearContainerAdminData();
+    nullData.clearContainerAdminData();
+    defaultData.clearContainerAdminData();
+
+    assertEquals(new HashMap<String, ContainerAdminData>(), validData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), emptyData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), nullData.getContainerAdminDataMap());
+    assertEquals(new HashMap<String, ContainerAdminData>(), defaultData.getContainerAdminDataMap());
+  }
+
+  @Test
+  public void testEqualsObject() {
+    Map<String, ContainerAdminData> testMap = Maps.newHashMap();
+    testMap.put(DEFAULT, defaultContainerData);
+    testMap.put(MY_CONTAINER, myContainerData);
+    assertTrue(validData.equals(new ServerAdminData(testMap)));
+    assertFalse(validData.equals(nullData));
+    assertTrue(nullData.equals(defaultData));
+
+    testMap = Maps.newHashMap();
+    testMap.put(MY_CONTAINER, myContainerData);
+    assertFalse(validData.equals(testMap));
+  }
+
+  @Test
+  public void testHasContainerAdminData() {
+    assertTrue(validData.hasContainerAdminData(MY_CONTAINER));
+    assertTrue(validData.hasContainerAdminData(DEFAULT));
+    assertFalse(validData.hasContainerAdminData("foo"));
+    assertFalse(nullData.hasContainerAdminData(MY_CONTAINER));
+    assertFalse(defaultData.hasContainerAdminData(DEFAULT));
+    assertFalse(emptyData.hasContainerAdminData(MY_CONTAINER));
+  }
+
+  @Test
+  public void testHashCode() {
+    assertEquals(Objects.hashCode(this.containerMap), validData.hashCode());
+    assertEquals(Objects.hashCode(Maps.newHashMap()), nullData.hashCode());
+    assertEquals(Objects.hashCode(Maps.newHashMap()), emptyData.hashCode());
+    assertEquals(Objects.hashCode(Maps.newHashMap()), defaultData.hashCode());
+    assertEquals(nullData.hashCode(), emptyData.hashCode());
+    assertFalse(validData.hashCode() == defaultData.hashCode());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/config/DefaultConfigProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/config/DefaultConfigProcessorTest.java
new file mode 100644
index 0000000..0fcb3ef
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/config/DefaultConfigProcessorTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.config;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.notNull;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.easymock.EasyMock;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+public class DefaultConfigProcessorTest {
+  private static final String CONFIG_FEATURE = "config-feature";
+  private static final List<String> CONFIG_FEATURES = Lists.newArrayList(CONFIG_FEATURE);
+  private static final Map<String, Object> CONFIG_FEATURE_MAP = ImmutableMap.<String, Object>of("key1", "val1", "key2", "val2");
+  private static final String NOCONFIG_FEATURE = "noconfig-feature";
+  private static final String CONTAINER = "container";
+  private static final String HOST = "host";
+  private static final Gadget GADGET = new Gadget();
+
+  private ContainerConfig config;
+
+  @Before
+  public void setUp() {
+    config = createMock(ContainerConfig.class);
+    expect(config.getMap(CONTAINER, DefaultConfigProcessor.GADGETS_FEATURES_KEY)).andReturn(CONFIG_FEATURE_MAP);
+    replay(config);
+  }
+
+  @Test
+  public void testGlobalConfig() {
+    ConfigContributor contrib = mockContrib(HOST);
+    List<ConfigContributor> globalContrib = Lists.newArrayList(contrib);
+    ConfigContributor noContrib = mockContrib((String)null);
+    Map<String, ConfigContributor> featureContrib = ImmutableMap.of(NOCONFIG_FEATURE, noContrib);
+    DefaultConfigProcessor processor = new DefaultConfigProcessor(featureContrib, config);
+    processor.setGlobalContributors(globalContrib);
+    processor.getConfig(CONTAINER, CONFIG_FEATURES, HOST, null);
+    verify(config, contrib, noContrib);
+  }
+
+  @Test
+  public void testFeatureConfigHost() {
+    ConfigContributor contrib = mockContrib(HOST);
+    List<ConfigContributor> globalContrib = Lists.newArrayList();
+    ConfigContributor noContrib = mockContrib((String)null);
+    Map<String, ConfigContributor> featureContrib = ImmutableMap.of(CONFIG_FEATURE, contrib,
+        NOCONFIG_FEATURE, noContrib);
+    DefaultConfigProcessor processor = new DefaultConfigProcessor(featureContrib, config);
+    processor.setGlobalContributors(globalContrib);
+    processor.getConfig(CONTAINER, CONFIG_FEATURES, HOST, null);
+    verify(config, contrib, noContrib);
+  }
+
+  @Test
+  public void testFeatureConfigGadget() {
+    ConfigContributor contrib = mockContrib(GADGET);
+    List<ConfigContributor> globalContrib = Lists.newArrayList();
+    ConfigContributor noContrib = mockContrib((Gadget)null);
+    Map<String, ConfigContributor> featureContrib = ImmutableMap.of(CONFIG_FEATURE, contrib,
+        NOCONFIG_FEATURE, noContrib);
+    DefaultConfigProcessor processor = new DefaultConfigProcessor(featureContrib, config);
+    processor.setGlobalContributors(globalContrib);
+    processor.getConfig(CONTAINER, CONFIG_FEATURES, null, GADGET);
+    verify(config, contrib, noContrib);
+  }
+
+  @SuppressWarnings("unchecked")
+  private ConfigContributor mockContrib(String host) {
+    ConfigContributor contrib = EasyMock.createMock(ConfigContributor.class);
+    createMock(ConfigContributor.class);
+    if (host != null) {
+      contrib.contribute((Map<String, Object>) notNull(), eq(CONTAINER), eq(host));
+      expectLastCall();
+    }
+    replay(contrib);
+    return contrib;
+  }
+
+  @SuppressWarnings("unchecked")
+  private ConfigContributor mockContrib(Gadget gadget) {
+    ConfigContributor contrib = EasyMock.createMock(ConfigContributor.class);
+    createMock(ConfigContributor.class);
+    if (gadget != null) {
+      contrib.contribute((Map<String, Object>) notNull(), eq(gadget));
+      expectLastCall();
+    }
+    replay(contrib);
+    return contrib;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/encoding/EncodingDetectorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/encoding/EncodingDetectorTest.java
new file mode 100644
index 0000000..cc30915
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/encoding/EncodingDetectorTest.java
@@ -0,0 +1,129 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.encoding;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.nio.charset.Charset;
+
+public class EncodingDetectorTest {
+
+  private EncodingDetector.FallbackEncodingDetector newMockFallbackEncoding(byte[] input,
+      String charset) {
+    EncodingDetector.FallbackEncodingDetector detector =
+      EasyMock.createNiceMock(EncodingDetector.FallbackEncodingDetector.class);
+    expect(detector.detectEncoding(input)).andReturn(Charset.forName(charset)).once();
+    replay(detector);
+    return detector;
+  }
+
+  @Test
+  public void asciiAssumesUtf8() throws Exception {
+    byte[] data = "Hello, world".getBytes("US-ASCII");
+    assertEquals("UTF-8", EncodingDetector.detectEncoding(data, true, null).name());
+  }
+
+  @Test
+  public void detectedUtf8WithByteOrderMark() {
+    byte[] data = {
+        (byte)0xEF, (byte)0xBB, (byte)0xBF, 'h', 'e', 'l', 'l', 'o'
+    };
+
+    assertEquals("UTF-8", EncodingDetector.detectEncoding(data, true, null).name());
+  }
+
+  @Test
+  public void assumeLatin1OnInvalidUtf8() throws Exception {
+    byte[] data = "\u4F60\u597D".getBytes("BIG5");
+
+    assertEquals("ISO-8859-1", EncodingDetector.detectEncoding(data, true, null).name());
+  }
+
+  @Test
+  public void badStreamEnd() throws Exception {
+    byte[] data = { 'd', 'u',  (byte)0xC0 };
+
+    assertEquals("ISO-8859-1", EncodingDetector.detectEncoding(data, true, null).name());
+  }
+
+  @Test
+  public void testFallbackDetectorIsUsed() throws Exception {
+    byte[] data = ("\u6211\u662F\u4E00\u4E2A\u4E0D\u5584\u4E8E\u8BB2\u8BDD\u7684\u4EBA\uFF0C" +
+                   "\u552F\u5176\u4E0D\u5584\u4E8E\u8BB2\u8BDD\uFF0C\u6709\u601D\u60F3\u8868" +
+                   "\u8FBE\u4E0D\u51FA\uFF0C\u6709\u611F\u60C5\u65E0\u6CD5\u503E\u5410")
+                   .getBytes("GB18030");
+
+    EncodingDetector.FallbackEncodingDetector detector =
+      newMockFallbackEncoding(data, "GB18030");
+
+    assertEquals("GB18030", EncodingDetector.detectEncoding(data, false, detector).name());
+    verify(detector);
+  }
+
+  // Test the fallback detector:
+  @Test
+  public void doNotAssumeLatin1OnInvalidUtf8() throws Exception {
+    byte[] data = ("\u6211\u662F\u4E00\u4E2A\u4E0D\u5584\u4E8E\u8BB2\u8BDD\u7684\u4EBA\uFF0C" +
+                   "\u552F\u5176\u4E0D\u5584\u4E8E\u8BB2\u8BDD\uFF0C\u6709\u601D\u60F3\u8868" +
+                   "\u8FBE\u4E0D\u51FA\uFF0C\u6709\u611F\u60C5\u65E0\u6CD5\u503E\u5410")
+                   .getBytes("GB18030");
+
+    EncodingDetector.FallbackEncodingDetector detector =
+        new EncodingDetector.FallbackEncodingDetector();
+
+    assertEquals("GB18030", EncodingDetector.detectEncoding(data, false, detector).name());
+  }
+
+  @Test
+  public void longUtf8StringIsUtf8() throws Exception {
+    byte[] data = ("\u6211\u662F\u4E00\u4E2A\u4E0D\u5584\u4E8E\u8BB2\u8BDD\u7684\u4EBA\uFF0C" +
+                   "\u552F\u5176\u4E0D\u5584\u4E8E\u8BB2\u8BDD\uFF0C\u6709\u601D\u60F3\u8868" +
+                   "\u8FBE\u4E0D\u51FA\uFF0C\u6709\u611F\u60C5\u65E0\u6CD5\u503E\u5410")
+                   .getBytes("UTF-8");
+
+    EncodingDetector.FallbackEncodingDetector detector =
+        new EncodingDetector.FallbackEncodingDetector();
+
+    assertEquals("UTF-8", detector.detectEncoding(data).name());
+  }
+
+  @Test
+  public void shortUtf8StringIsUtf8() throws Exception {
+    byte[] data = "Games, HQ, Mang\u00E1, Anime e tudo que um bom nerd ama".getBytes("UTF-8");
+
+    EncodingDetector.FallbackEncodingDetector detector =
+        new EncodingDetector.FallbackEncodingDetector();
+
+    assertEquals("UTF-8", detector.detectEncoding(data).name());
+  }
+
+  @Test(expected=NullPointerException.class)
+  public void nullCustomDetector() throws Exception {
+    byte[] data = "\u4F60\u597D".getBytes("BIG5");
+
+    // expect a NPE
+    assertEquals("ISO-8859-1", EncodingDetector.detectEncoding(data, false, null).name());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureParserTest.java
new file mode 100644
index 0000000..bccdeca
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureParserTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+
+import org.junit.Test;
+
+public class FeatureParserTest {
+  @Test
+  public void parseCompleteFeatureFile() throws Exception {
+    Uri parent = Uri.parse("scheme://host.com/root/path");
+    String featureXml =
+      "<feature>" +
+      "  <name>the_feature</name>" +
+      "  <dependency>myDep1</dependency>" +
+      "  <dependency>mySecondDep</dependency>" +
+      "  <gadget>" +
+      "    <ignored>This tag is ignored</ignored>" +
+      "    <api>" +
+      "      <!-- This is a comment about the API -->" +
+      "      <uses type=\"js\">foo.symbol</uses>" +
+      "      <exports type=\"rpc\">rpc_service</exports>" +
+      "      More extraneous crap." +
+      "      <exports type=\"js\">bar.symbol</exports>" +
+      "    </api>" +
+      "    <script src=\"http://www.apache.org/file.js\"/>" +
+      "    <api>" +
+      "      <!-- A way of adding APIs together per-bundle but not tied to a resource -->" +
+      "      <uses type=\"rpc\">uses_service</uses>" +
+      "      <uses type=\"js\">last.symbol</uses>" +
+      "    </api>" +
+      "    <script src=\"relative/resource.js\" gadget_attrib=\"gadget_value\"/>" +
+      "  </gadget>" +
+      "  <gadget container=\"container1\">" +
+      "    <!-- No child values, testing outlier case -->" +
+      "  </gadget>" +
+      "  <container randomAttrib=\"randomValue\" secondAttrib=\"secondValue\">" +
+      "    <script src=\"/authority/relative.js\" r2_attr=\"r2_val\" r3_attr=\"r3_val\"></script>" +
+      "    <script>Inlined content</script>" +
+      "  </container>" +
+      "  <other_type>" +
+      "    <api> <!-- No actual API decls here --> </api>" +
+      "    <script src=\"http://www.apache.org/two.js\"/>" +
+      "    <script src=\"//extern/unchanged.dat\" inline=\"false\"/>" +
+      "  </other_type>" +
+      "</feature>";
+    FeatureParser.ParsedFeature parsed = new FeatureParser().parse(parent, featureXml);
+
+    // Top-level validation.
+    assertEquals("the_feature", parsed.getName());
+    assertEquals(2, parsed.getDeps().size());
+    assertEquals("myDep1", parsed.getDeps().get(0));
+    assertEquals("mySecondDep", parsed.getDeps().get(1));
+    assertEquals(4, parsed.getBundles().size());
+
+    // First gadget bundle.
+    FeatureParser.ParsedFeature.Bundle bundle1 = parsed.getBundles().get(0);
+    assertEquals("gadget", bundle1.getType());
+    assertEquals(0, bundle1.getAttribs().size());
+    assertEquals(2, bundle1.getResources().size());
+    assertNull(bundle1.getResources().get(0).getContent());
+    assertEquals(Uri.parse("http://www.apache.org/file.js"),
+        bundle1.getResources().get(0).getSource());
+    assertEquals(0, bundle1.getResources().get(0).getAttribs().size());
+    assertNull(bundle1.getResources().get(1).getContent());
+    assertEquals(Uri.parse("scheme://host.com/root/relative/resource.js"),
+        bundle1.getResources().get(1).getSource());
+    assertEquals(1, bundle1.getResources().get(1).getAttribs().size());
+    assertEquals("gadget_value", bundle1.getResources().get(1).getAttribs().get("gadget_attrib"));
+    assertEquals(5, bundle1.getApis().size());
+    assertEquals(ApiDirective.Type.JS, bundle1.getApis().get(0).getType());
+    assertTrue(bundle1.getApis().get(0).isUses());
+    assertEquals("foo.symbol", bundle1.getApis().get(0).getValue());
+    assertEquals(ApiDirective.Type.RPC, bundle1.getApis().get(1).getType());
+    assertFalse(bundle1.getApis().get(1).isUses());
+    assertEquals("rpc_service", bundle1.getApis().get(1).getValue());
+    assertEquals(ApiDirective.Type.JS, bundle1.getApis().get(2).getType());
+    assertFalse(bundle1.getApis().get(2).isUses());
+    assertEquals("bar.symbol", bundle1.getApis().get(2).getValue());
+    assertEquals(ApiDirective.Type.RPC, bundle1.getApis().get(3).getType());
+    assertTrue(bundle1.getApis().get(3).isUses());
+    assertEquals("uses_service", bundle1.getApis().get(3).getValue());
+    assertEquals(ApiDirective.Type.JS, bundle1.getApis().get(4).getType());
+    assertTrue(bundle1.getApis().get(4).isUses());
+    assertEquals("last.symbol", bundle1.getApis().get(4).getValue());
+
+    // Second gadget bundle.
+    FeatureParser.ParsedFeature.Bundle bundle2 = parsed.getBundles().get(1);
+    assertEquals("gadget", bundle2.getType());
+    assertEquals(1, bundle2.getAttribs().size());
+    assertEquals("container1", bundle2.getAttribs().get("container"));
+    assertEquals(0, bundle2.getResources().size());
+    assertEquals(0, bundle2.getApis().size());
+
+    // Container bundle.
+    FeatureParser.ParsedFeature.Bundle bundle3 = parsed.getBundles().get(2);
+    assertEquals("container", bundle3.getType());
+    assertEquals(2, bundle3.getAttribs().size());
+    assertEquals("randomValue", bundle3.getAttribs().get("randomAttrib"));
+    assertEquals("secondValue", bundle3.getAttribs().get("secondAttrib"));
+    assertEquals(2, bundle3.getResources().size());
+    assertNull(bundle3.getResources().get(0).getContent());
+    assertEquals(Uri.parse("scheme://host.com/authority/relative.js"),
+        bundle3.getResources().get(0).getSource());
+    assertEquals(2, bundle3.getResources().get(0).getAttribs().size());
+    assertEquals("r2_val", bundle3.getResources().get(0).getAttribs().get("r2_attr"));
+    assertEquals("r3_val", bundle3.getResources().get(0).getAttribs().get("r3_attr"));
+    assertNull(bundle3.getResources().get(1).getSource());
+    assertEquals("Inlined content", bundle3.getResources().get(1).getContent());
+    assertEquals(0, bundle3.getResources().get(1).getAttribs().size());
+    assertEquals(0, bundle3.getApis().size());
+
+    // Other_type bundle.
+    FeatureParser.ParsedFeature.Bundle bundle4 = parsed.getBundles().get(3);
+    assertEquals("other_type", bundle4.getType());
+    assertEquals(0, bundle4.getAttribs().size());
+    assertEquals(2, bundle4.getResources().size());
+    assertNull(bundle4.getResources().get(0).getContent());
+    assertEquals(Uri.parse("http://www.apache.org/two.js"),
+        bundle4.getResources().get(0).getSource());
+    assertNull(bundle4.getResources().get(1).getContent());
+    assertEquals(Uri.parse("//extern/unchanged.dat"),
+        bundle4.getResources().get(1).getSource());
+    assertEquals(0, bundle4.getResources().get(0).getAttribs().size());
+    assertEquals(0, bundle4.getApis().size());
+  }
+
+  @Test(expected=GadgetException.class)
+  public void parseInvalidXml() throws GadgetException {
+    // Should failed to parse invalid XML");
+    new FeatureParser().parse(Uri.parse(""), "This is not valid XML.");
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureRegistryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureRegistryTest.java
new file mode 100644
index 0000000..d956d1a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureRegistryTest.java
@@ -0,0 +1,924 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.junit.Test;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+public class FeatureRegistryTest {
+  private static final String NODEP_TPL =
+      getFeatureTpl("nodep", new String[] {});
+  private static final String TOP_TPL =
+      getFeatureTpl("top", new String[] { "mid_a", "mid_b" });
+  private static final String MID_A_TPL =
+      getFeatureTpl("mid_a", new String[] { "bottom" });
+  private static final String MID_B_TPL =
+      getFeatureTpl("mid_b", new String[] { "bottom" });
+  private static final String BOTTOM_TPL =
+      getFeatureTpl("bottom", new String[] {});
+  private static final String LOOP_A_TPL =
+      getFeatureTpl("loop_a", new String[] { "loop_b" });
+  private static final String LOOP_B_TPL =
+      getFeatureTpl("loop_b", new String[] { "loop_c" });
+  private static final String LOOP_C_TPL =
+      getFeatureTpl("loop_c", new String[] { "loop_a" });
+  private static final String BAD_DEP_TPL =
+      getFeatureTpl("bad_dep", new String[] { "no-exists" });
+
+  private static String RESOURCE_BASE_PATH = "/resource/base/path";
+  private static int resourceIdx = 0;
+  private TestFeatureRegistry registry;
+
+  @Test
+  public void registerFromFileFeatureXmlFileScheme() throws Exception {
+    checkRegisterFromFileFeatureXml(true);
+  }
+
+  @Test
+  public void registerFromFileFeatureXmlNoScheme() throws Exception {
+    checkRegisterFromFileFeatureXml(false);
+  }
+
+  private void checkRegisterFromFileFeatureXml(boolean withScheme) throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+    String content = "content-" + (withScheme ? "withScheme" : "noScheme");
+    Uri resUri = makeFile(content);
+    Uri featureFile = makeFile(xml(NODEP_TPL, "gadget",
+        withScheme ? resUri.toString() : resUri.getPath(), null));
+    registry = builder.build(withScheme ? featureFile.toString() : featureFile.getPath());
+
+    // Verify single resource works all the way through.
+    List<FeatureResource> resources = registry.getAllFeatures().getResources();
+    assertEquals(1, resources.size());
+    assertEquals(content, resources.get(0).getContent());
+  }
+
+  @Test
+  public void registerFromFileInNestedDirectoryFeatureXmlFile() throws Exception {
+    // Get the directory from dummyUri and create a subdir.
+    File tmpFile = File.createTempFile("dummy", ".dat");
+    tmpFile.deleteOnExit();
+    File parentDir = tmpFile.getParentFile();
+    String childDirName = String.valueOf(Math.random());
+    File childDir = new File(parentDir, childDirName);
+    childDir.mkdirs();
+    childDir.deleteOnExit();
+    File featureDir = new File(childDir, "thefeature");
+    featureDir.mkdirs();
+    featureDir.deleteOnExit();
+    File resFile = File.createTempFile("content", ".js", featureDir);
+    resFile.deleteOnExit();
+    String content = "content-foo";
+    BufferedWriter out = new BufferedWriter(new FileWriter(resFile));
+    out.write(content);
+    out.close();
+    File featureFile = File.createTempFile("feature", ".xml", featureDir);
+    featureFile.deleteOnExit();
+    out = new BufferedWriter(new FileWriter(featureFile));
+    out.write(xml(NODEP_TPL, "gadget", resFile.toURI().toString(), null));
+    out.close();
+    registry = TestFeatureRegistry.newBuilder().build(childDir.toURI().toString());
+
+    // Verify single resource works all the way through.
+    List<FeatureResource> resources = registry.getAllFeatures().getResources();
+    assertEquals(1, resources.size());
+    assertEquals(content, resources.get(0).getContent());
+  }
+
+  @Test
+  public void registerFromResourceFeatureXml() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+    String content = "resource-content()";
+    Uri contentUri = builder.expectResource(content);
+    Uri featureUri = builder.expectResource(xml(NODEP_TPL, "gadget", contentUri.getPath(), null));
+    builder.addFeatureFile(featureUri.toString());
+    registry = builder.build();
+
+    // Verify single resource works all the way through.
+    List<FeatureResource> resources = registry.getAllFeatures().getResources();
+    assertEquals(1, resources.size());
+    assertEquals(content, resources.get(0).getContent());
+  }
+
+  @Test
+  public void registerFromResourceFeatureXmlRelativeContent() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+    String content = "resource-content-relative()";
+    Uri contentUri = builder.expectResource(content);
+    String relativePath = contentUri.getPath().substring(contentUri.getPath().lastIndexOf('/') + 1);
+    Uri featureUri = builder.expectResource(xml(NODEP_TPL, "gadget", relativePath, null));
+    registry = builder.build(featureUri.toString());
+
+    // Verify single resource works all the way through.
+    List<FeatureResource> resources = registry.getAllFeatures().getResources();
+    assertEquals(1, resources.size());
+    assertEquals(content, resources.get(0).getContent());
+  }
+
+  @Test
+  public void registerFromResourceIndex() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    // One with extern resource loaded content...
+    String content1 = "content1()";
+    Uri content1Uri = builder.expectResource(content1);
+    Uri feature1Uri = builder.expectResource(xml(MID_A_TPL, "gadget", content1Uri.getPath(), null));
+
+    // One feature with inline content (that it depends on)...
+    String content2 = "inline()";
+    Uri feature2Uri = builder.expectResource(xml(BOTTOM_TPL, "gadget", null, content2));
+
+    // .txt file to join the two
+    Uri txtFile = builder.expectResource(feature1Uri.toString() + '\n' + feature2Uri.toString(), ".txt");
+
+    // Load resources from the text file and do basic validation they're good.
+    registry = builder.build(txtFile.toString());
+
+    // Contents should be ordered based on the way they went in.
+    List<FeatureResource> resources = registry.getAllFeatures().getResources();
+    assertEquals(2, resources.size());
+    assertEquals(content2, resources.get(0).getContent());
+    assertEquals(content1, resources.get(1).getContent());
+  }
+
+  @Test
+  public void registerOverrideFeature() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    // Feature 1
+    String content1 = "content1()";
+    Uri content1Uri = builder.expectResource(content1);
+    Uri feature1Uri = builder.expectResource(xml(BOTTOM_TPL, "gadget", content1Uri.getPath(), null));
+
+    String content2 = "content_two()";
+    Uri content2Uri = builder.expectResource(content2);
+    Uri feature2Uri = builder.expectResource(xml(BOTTOM_TPL, "gadget", content2Uri.getPath(), null));
+
+    registry = builder.build(feature1Uri.toString());
+    List<FeatureResource> resources1 = registry.getAllFeatures().getResources();
+    assertEquals(1, resources1.size());
+    assertEquals(content1, resources1.get(0).getContent());
+
+    // Register it again, different def.
+    registry = builder.build(feature2Uri.toString());
+    List<FeatureResource> resources2 = registry.getAllFeatures().getResources();
+    assertEquals(1, resources2.size());
+    assertEquals(content2, resources2.get(0).getContent());
+
+    // Check cached resources too.
+    List<FeatureResource> resourcesAgain = registry.getAllFeatures().getResources();
+    assertSame(resources2, resourcesAgain);
+  }
+
+  @Test
+  public void cacheAccountsForUnsupportedState() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    String content1 = "content1()";
+    Uri content1Uri = builder.expectResource(content1);
+    Map<String, String> attribs = Maps.newHashMap();
+    String theContainer = "the-container";
+    attribs.put("container", theContainer);
+    Uri feature1Uri = builder.expectResource(xml(BOTTOM_TPL, "gadget", content1Uri.getPath(), null, attribs));
+
+    // Register it.
+    registry = builder.build(feature1Uri.toString());
+
+    // Retrieve content for matching context.
+    List<String> needed = Lists.newArrayList("bottom");
+    List<String> unsupported = Lists.newArrayList();
+    List<FeatureResource> resources = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer), needed, unsupported).getResources();
+
+    // Retrieve again w/ no unsupported list.
+    List<FeatureResource> resourcesUnsup = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer), needed, null).getResources();
+
+    assertNotSame(resources, resourcesUnsup);
+    assertEquals(resources, resourcesUnsup);
+    assertEquals(1, resources.size());
+    assertEquals(content1, resources.get(0).getContent());
+
+    // Now make sure the cache DOES work when needed.
+    List<FeatureResource> resources2 = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer), needed, unsupported).getResources();
+    assertSame(resources, resources2);
+
+    List<FeatureResource> resourcesUnsup2 = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer), needed, null).getResources();
+    assertSame(resourcesUnsup, resourcesUnsup2);
+
+    // Lastly, ensure that ignoreCache is properly accounted.
+    List<FeatureResource> resourcesIgnoreCache = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer, true), needed, unsupported).getResources();
+    assertNotSame(resources, resourcesIgnoreCache);
+    assertEquals(1, resourcesIgnoreCache.size());
+    assertEquals(content1, resourcesIgnoreCache.get(0).getContent());
+  }
+
+  @Test
+  public void cacheAccountsForContext() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    String content1 = "content1()";
+    Uri content1Uri = builder.expectResource(content1);
+    Map<String, String> attribs = Maps.newHashMap();
+    String theContainer = "the-container";
+    attribs.put("container", theContainer);
+    Uri feature1Uri = builder.expectResource(xml(BOTTOM_TPL, "gadget", content1Uri.getPath(), null, attribs));
+
+    // Register it.
+    registry = builder.build(feature1Uri.toString());
+
+    // Retrieve content for matching context.
+    List<String> needed = Lists.newArrayList("bottom");
+    List<String> unsupported = Lists.newArrayList();
+    List<FeatureResource> resources = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer), needed, unsupported).getResources();
+
+    // Retrieve again w/ mismatch container.
+    List<FeatureResource> resourcesNoMatch = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, "foo"), needed, unsupported).getResources();
+
+    // Retrieve again w/ mismatched context.
+    List<FeatureResource> ctxMismatch = registry.getFeatureResources(
+        getCtx(RenderingContext.CONTAINER, theContainer), needed, unsupported).getResources();
+
+    assertNotSame(resources, resourcesNoMatch);
+    assertNotSame(resources, ctxMismatch);
+
+    assertEquals(1, resources.size());
+    assertEquals(content1, resources.get(0).getContent());
+
+    assertEquals(0, resourcesNoMatch.size());
+    assertEquals(0, ctxMismatch.size());
+
+    // Make sure caches work with appropriate matching.
+    List<FeatureResource> resources2 = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer), needed, unsupported).getResources();
+    assertSame(resources, resources2);
+
+    List<FeatureResource> resourcesNoMatch2 = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, "foo"), needed, unsupported).getResources();
+    assertSame(resourcesNoMatch, resourcesNoMatch2);
+
+    List<FeatureResource> ctxMismatch2 = registry.getFeatureResources(
+        getCtx(RenderingContext.CONTAINER, theContainer), needed, unsupported).getResources();
+    assertSame(ctxMismatch, ctxMismatch2);
+
+    // Check ignoreCache
+    List<FeatureResource> resourcesIC = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer, true), needed, unsupported).getResources();
+    assertNotSame(resources, resourcesIC);
+  }
+
+  @Test
+  public void missingIndexResultsInException() throws Exception {
+    try {
+      registry = TestFeatureRegistry.newBuilder().build(makeResourceUri(".txt").toString());
+      fail("Should have thrown an exception for missing .txt file");
+    } catch (GadgetException e) {
+      // Expected. Verify code.
+      assertEquals(GadgetException.Code.INVALID_PATH, e.getCode());
+    }
+  }
+
+  @Test
+  public void missingFileResultsInException() throws Exception {
+    try {
+      registry = TestFeatureRegistry.newBuilder().build(new UriBuilder().setScheme("file")
+          .setPath("/is/not/there.foo.xml").toUri().toString());
+      fail("Should have thrown missing .xml file exception");
+    } catch (GadgetException e) {
+      // Expected. Verify code.
+      assertEquals(GadgetException.Code.INVALID_CONFIG, e.getCode());
+    }
+  }
+
+  @Test
+  public void selectExactFeatureResourcesGadget() throws Exception {
+    checkExactFeatureResources("gadget", RenderingContext.GADGET);
+  }
+
+  @Test
+  public void selectExactFeatureResourcesContainer() throws Exception {
+    checkExactFeatureResources("container", RenderingContext.CONTAINER);
+  }
+
+  private void checkExactFeatureResources(String type, RenderingContext rctx) throws Exception {
+    setupFullRegistry(type, null);
+    GadgetContext ctx = getCtx(rctx, null);
+    List<String> needed = Lists.newArrayList("nodep", "bottom");
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+    assertEquals(0, unsupported.size());
+    assertEquals(2, resources.size());
+    assertEquals("nodep", resources.get(0).getContent());
+    assertEquals("bottom", resources.get(1).getContent());
+  }
+
+  @Test
+  public void selectNoContentValidFeatureResourcesGadget() throws Exception {
+    checkNoContentValidFeatureResources("gadget", RenderingContext.CONTAINER);
+  }
+
+  @Test
+  public void selectNoContentValidFeatureResourcesContainer() throws Exception {
+    checkNoContentValidFeatureResources("container", RenderingContext.GADGET);
+  }
+
+  private void checkNoContentValidFeatureResources(
+      String type, RenderingContext rctx) throws Exception {
+    setupFullRegistry(type, null);
+    GadgetContext ctx = getCtx(rctx, null);
+    List<String> needed = Lists.newArrayList("nodep", "bottom");
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+    assertEquals(0, resources.size());
+  }
+
+  @Test
+  public void testTransitiveFeatureResourcesGadget() throws Exception {
+    checkTransitiveFeatureResources("gadget", RenderingContext.GADGET);
+  }
+
+  @Test
+  public void testTransitiveFeatureResourcesContainer() throws Exception {
+    checkTransitiveFeatureResources("container", RenderingContext.CONTAINER);
+  }
+
+  private void checkTransitiveFeatureResources(String type, RenderingContext rctx)
+      throws Exception {
+    setupFullRegistry(type, null);
+    GadgetContext ctx = getCtx(rctx, null);
+    List<String> needed = Lists.newArrayList("top", "nodep");
+    List<String> unsupported = Lists.newLinkedList();
+
+    // Should come back in insertable order (from bottom of the graph up),
+    // querying in feature.xml dependency order.
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+    assertEquals(5, resources.size());
+    assertEquals("bottom", resources.get(0).getContent());
+    assertEquals("mid_a", resources.get(1).getContent());
+    assertEquals("mid_b", resources.get(2).getContent());
+    assertEquals("top", resources.get(3).getContent());
+    assertEquals("nodep", resources.get(4).getContent());
+  }
+
+  @Test
+  public void unsupportedFeaturesPopulated() throws Exception {
+    // Test only for gadget case; above tests are sufficient to ensure
+    // that type and RenderingContext filter results properly.
+    setupFullRegistry("gadget", null);
+    GadgetContext ctx = getCtx(RenderingContext.GADGET, null);
+    List<String> needed = Lists.newArrayList("nodep", "does-not-exist");
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+    assertEquals(1, resources.size());
+    assertEquals("nodep", resources.get(0).getContent());
+    assertEquals(1, unsupported.size());
+    assertEquals("does-not-exist", unsupported.get(0));
+  }
+
+  @Test
+  public void filterFeaturesByContainerMatch() throws Exception {
+    // Again test only for gadget case; above tests cover type <-> RenderingContext
+    setupFullRegistry("gadget", "one, two , three");
+    GadgetContext ctx = getCtx(RenderingContext.GADGET, "two");
+    List<String> needed = Lists.newArrayList("nodep", "bottom");
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+    assertEquals(2, resources.size());
+    assertEquals("nodep", resources.get(0).getContent());
+    assertEquals("bottom", resources.get(1).getContent());
+    assertEquals(0, unsupported.size());
+  }
+
+  @Test
+  public void filterFeaturesByContainerNoMatch() throws Exception {
+    // Again test only for gadget case; above tests cover type <-> RenderingContext
+    setupFullRegistry("gadget", "one, two, three");
+    GadgetContext ctx = getCtx(RenderingContext.GADGET, "four");
+    List<String> needed = Lists.newArrayList("nodep", "bottom");
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+    assertEquals(0, resources.size());  // no resource matches but all feature keys valid
+    assertEquals(0, unsupported.size());
+  }
+
+  @Test
+  public void getFeatureResourcesNoTransitiveSingle() throws Exception {
+    setupFullRegistry("gadget", null);
+    GadgetContext ctx = getCtx(RenderingContext.GADGET, null);
+    List<String> needed = Lists.newArrayList("top", "bottom");
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported, false).getResources();
+    // Should return in order requested.
+    assertEquals(2, resources.size());
+    assertEquals("top", resources.get(0).getContent());
+    assertEquals("bottom", resources.get(1).getContent());
+    assertEquals(0, unsupported.size());
+  }
+
+  @Test
+  public void getAllFeatures() throws Exception {
+    setupFullRegistry("gadget", null);
+    List<FeatureResource> resources = registry.getAllFeatures().getResources();
+
+    // No guaranteed order (top/mid/bottom bundle may be before nodep)
+    // Just check that there are 5 resources around and let the above tests
+    // handle transitivity checks.
+    assertEquals(5, resources.size());
+  }
+
+  @Test
+  public void getFeaturesStringsNoTransitive() throws Exception {
+    setupFullRegistry("gadget", null);
+    List<String> needed = Lists.newArrayList("nodep", "bottom");
+    List<String> featureNames = registry.getFeatures(needed);
+    assertEquals(2, featureNames.size());
+    assertEquals("nodep", featureNames.get(0));
+    assertEquals("bottom", featureNames.get(1));
+  }
+
+  @Test
+  public void getFeaturesStringsTransitive() throws Exception {
+    setupFullRegistry("gadget", null);
+    List<String> needed = Lists.newArrayList("top", "nodep");
+    List<String> featureNames = registry.getFeatures(needed);
+    assertEquals(5, featureNames.size());
+    assertEquals("bottom", featureNames.get(0));
+    assertEquals("mid_a", featureNames.get(1));
+    assertEquals("mid_b", featureNames.get(2));
+    assertEquals("top", featureNames.get(3));
+    assertEquals("nodep", featureNames.get(4));
+  }
+
+  @Test
+  public void loopIsDetectedAndCrashes() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    // Set up a registry with features loop_a,b,c. C points back to A, which should
+    // cause an exception to be thrown by the register method.
+    String type = "gadget";
+    Uri loopAUri = builder.expectResource(xml(LOOP_A_TPL, type, null, "loop_a"));
+    Uri loopBUri = builder.expectResource(xml(LOOP_B_TPL, type, null, "loop_b"));
+    Uri loopCUri = builder.expectResource(xml(LOOP_C_TPL, type, null, "loop_c"));
+    Uri txtFile = builder.expectResource(loopAUri.toString() + '\n' + loopBUri.toString() + '\n' +
+        loopCUri.toString(), ".txt");
+    try {
+      registry = builder.build(txtFile.toString());
+      fail("Should have thrown a loop-detected exception");
+    } catch (GadgetException e) {
+      assertEquals(GadgetException.Code.INVALID_CONFIG, e.getCode());
+    }
+  }
+
+  @Test
+  public void unavailableFeatureCrashes() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+    Uri featUri = builder.expectResource(xml(BAD_DEP_TPL, "gadget", null, "content"));
+    try {
+      registry = builder.build(featUri.toString());
+    } catch (GadgetException e) {
+      assertEquals(GadgetException.Code.INVALID_CONFIG, e.getCode());
+    }
+  }
+
+  @Test
+  public void returnOnlyContainerFilteredJs() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+    String feature = "thefeature";
+    String container =  "foo";
+    String containerContent = "content1();";
+    String defaultContent = "content2();";
+    Uri featureUri =
+        builder.expectResource(
+          getContainerAndDefaultTpl(feature, container, containerContent, defaultContent));
+    registry = builder.build(featureUri.toString());
+    List<String> needed = Lists.newArrayList(feature);
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources =
+        registry.getFeatureResources(
+          getCtx(RenderingContext.GADGET, container), needed, unsupported).getResources();
+    assertEquals(1, resources.size());
+    assertEquals(containerContent, resources.get(0).getContent());
+  }
+
+  @Test
+  public void returnDefaultMatchJs() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+    String feature = "thefeature";
+    String container =  "foo";
+    String containerContent = "content1();";
+    String defaultContent = "content2();";
+    Uri featureUri =
+        builder.expectResource(
+          getContainerAndDefaultTpl(feature, container, containerContent, defaultContent));
+    registry = builder.build(featureUri.toString());
+    List<String> needed = Lists.newArrayList(feature);
+    List<String> unsupported = Lists.newLinkedList();
+    List<FeatureResource> resources =
+        registry.getFeatureResources(
+          getCtx(RenderingContext.GADGET, "othercontainer"), needed, unsupported).getResources();
+    assertEquals(1, resources.size());
+    assertEquals(defaultContent, resources.get(0).getContent());
+  }
+
+  private String getContainerAndDefaultTpl(String name, String container, String c1, String c2) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("<feature><name>").append(name).append("</name>");
+    sb.append("<gadget container=\"").append(container).append("\">");
+    sb.append("<script>").append(c1).append("</script></gadget>");
+    sb.append("<gadget>");
+    sb.append("<script>").append(c2).append("</script></gadget>");
+    sb.append("</feature>");
+    return sb.toString();
+  }
+
+  @Test
+  public void resourceGetsMergedAttribs() throws Exception {
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    String content1 = "content1()";
+    Uri content1Uri = builder.expectResource(content1);
+    Map<String, String> attribs = Maps.newHashMap();
+    String theContainer = "the-container";
+    attribs.put("container", theContainer);
+    attribs.put("one", "bundle-one");
+    attribs.put("two", "bundle-two");
+
+    Map<String, String> resourceAttribs = Maps.newHashMap();
+    attribs.put("two", "attrib-two");
+    attribs.put("three", "attrib-three");
+    Uri feature1Uri = builder.expectResource(xml(BOTTOM_TPL, "gadget", content1Uri.getPath(),
+        null, attribs, resourceAttribs));
+
+    // Register it.
+    registry = builder.build(feature1Uri.toString());
+
+    // Retrieve the resource for matching context.
+    List<String> needed = Lists.newArrayList("bottom");
+    List<String> unsupported = Lists.newArrayList();
+    List<FeatureResource> resources = registry.getFeatureResources(
+        getCtx(RenderingContext.GADGET, theContainer), needed, unsupported).getResources();
+
+    // Sanity test.
+    assertEquals(1, resources.size());
+
+    // Check the attribs passed into the resource. This is a little funky, but it works.
+    Map<String, String> lastAttribs = registry.getLastAttribs();
+    assertNotNull(lastAttribs);
+    assertEquals(4, lastAttribs.size());
+    assertEquals(theContainer, lastAttribs.get("container"));
+    assertEquals("bundle-one", lastAttribs.get("one"));
+    assertEquals("attrib-two", lastAttribs.get("two"));
+    assertEquals("attrib-three", lastAttribs.get("three"));
+  }
+
+  @Test
+  public void testGetGadgetFromMixedRegistry() throws Exception {
+    setupMixedFullRegistry();
+
+    GadgetContext ctx = getCtx(RenderingContext.GADGET, null);
+    List<String> needed = Lists.newArrayList("top");
+    List<String> unsupported = Lists.newLinkedList();
+
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+
+    assertEquals(4, resources.size());
+    assertEquals("bottom();all();", resources.get(0).getContent());
+    assertEquals("mid_b();gadget();", resources.get(1).getContent());
+    assertEquals("mid_a();all();", resources.get(2).getContent());
+    assertEquals("top();gadget();", resources.get(3).getContent());
+  }
+
+  @Test
+  public void testGetContainerFromMixedRegistry() throws Exception {
+    setupMixedFullRegistry();
+
+    GadgetContext ctx = getCtx(RenderingContext.CONTAINER, null);
+    List<String> needed = Lists.newArrayList("top");
+    List<String> unsupported = Lists.newLinkedList();
+
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+
+    assertEquals(4, resources.size());
+    assertEquals("bottom();all();", resources.get(0).getContent());
+    assertEquals("mid_b();container();", resources.get(1).getContent());
+    assertEquals("mid_a();container();", resources.get(2).getContent());
+    assertEquals("top();all();", resources.get(3).getContent());
+  }
+
+  @Test
+  public void testGetConfiguredGadgetFromMixedRegistry() throws Exception {
+    setupMixedFullRegistry();
+
+    GadgetContext ctx = getCtx(RenderingContext.CONFIGURED_GADGET, null);
+    List<String> needed = Lists.newArrayList("top");
+    List<String> unsupported = Lists.newLinkedList();
+
+    List<FeatureResource> resources = registry.getFeatureResources(ctx, needed, unsupported).getResources();
+
+    assertEquals(4, resources.size());
+    assertEquals("bottom();all();", resources.get(0).getContent());
+    assertEquals("mid_b();gadget();", resources.get(1).getContent());
+    assertEquals("mid_a();all();", resources.get(2).getContent());
+    assertEquals("top();gadget();", resources.get(3).getContent());
+  }
+
+  @Test(expected = GadgetException.class)
+  public void testCheckDependencyLoopWithLoopDependencyError() throws Exception {
+        TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+        Uri featureA, featureB, featureC, featureD, txtFile;
+
+        // featureA, featureB, featureC and featureD have dependency loop
+        // problem.
+        featureA = builder.expectResource("<feature>" + "<name>featureA</name>"
+                + "<dependency>featureB</dependency>" + "<gadget>"
+                + "  <script>top();gadget();</script>" + "</gadget>" + "<all>"
+                + "  <script>top();all();</script>" + "</all>" + "</feature>");
+        featureB = builder.expectResource("<feature>" + "<name>featureB</name>"
+                + "<dependency>featureC</dependency>" + "<container>"
+                + "  <script>mid_a();container();</script>" + "</container>"
+                + "<all>" + "  <script>mid_a();all();</script>" + "</all>"
+                + "</feature>");
+        featureC = builder.expectResource("<feature>" + "<name>featureC</name>"
+                + "<dependency>featureD</dependency>" + "<container>"
+                + "  <script>mid_b();container();</script>" + "</container>"
+                + "<gadget>" + "  <script>mid_b();gadget();</script>"
+                + "</gadget>" + "</feature>");
+        featureD = builder.expectResource("<feature>" + "<name>featureD</name>"
+                + "<dependency>featureA</dependency>" + "<all>"
+                + "  <script>bottom();all();</script>" + "</all>"
+                + "</feature>");
+        txtFile = builder.expectResource(
+                featureA.toString() + '\n' + featureB.toString() + '\n'
+                        + featureC.toString() + '\n' + featureD.toString(),
+                ".txt");
+
+        registry = builder.build(txtFile.toString());
+  }
+
+  @Test(expected = GadgetException.class)
+  public void testCheckDependencyLoopWithPartialLoopDependencyError() throws Exception {
+        TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+        Uri featureA, featureB, featureC, featureD, txtFile;
+
+        // featureC and featureD have dependency loop problem.
+        featureA = builder.expectResource("<feature>" + "<name>featureA</name>"
+                + "<dependency>featureB</dependency>" + "<gadget>"
+                + "  <script>top();gadget();</script>" + "</gadget>" + "<all>"
+                + "  <script>top();all();</script>" + "</all>" + "</feature>");
+        featureB = builder.expectResource("<feature>" + "<name>featureB</name>"
+                + "<dependency>featureC</dependency>" + "<container>"
+                + "  <script>mid_a();container();</script>" + "</container>"
+                + "<all>" + "  <script>mid_a();all();</script>" + "</all>"
+                + "</feature>");
+        featureC = builder.expectResource("<feature>" + "<name>featureC</name>"
+                + "<dependency>featureD</dependency>" + "<container>"
+                + "  <script>mid_b();container();</script>" + "</container>"
+                + "<gadget>" + "  <script>mid_b();gadget();</script>"
+                + "</gadget>" + "</feature>");
+        featureD = builder.expectResource("<feature>" + "<name>featureD</name>"
+                + "<dependency>featureC</dependency>" + "<all>"
+                + "  <script>bottom();all();</script>" + "</all>"
+                + "</feature>");
+        txtFile = builder.expectResource(
+                featureA.toString() + '\n' + featureB.toString() + '\n'
+                        + featureC.toString() + '\n' + featureD.toString(),
+                ".txt");
+
+        registry = builder.build(txtFile.toString());
+  }
+
+  @Test
+  public void testCheckDependencyLoopWithNormalDependency() {
+        TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+        Uri featureA, featureB, featureC, featureD, txtFile;
+
+        // There is no loop problem.
+        featureA = builder.expectResource("<feature>" + "<name>featureA</name>"
+                + "<dependency>featureB</dependency>" + "<gadget>"
+                + "  <script>top();gadget();</script>" + "</gadget>" + "<all>"
+                + "  <script>top();all();</script>" + "</all>" + "</feature>");
+        featureB = builder.expectResource("<feature>" + "<name>featureB</name>"
+                + "<dependency>featureC</dependency>" + "<container>"
+                + "  <script>mid_a();container();</script>" + "</container>"
+                + "<all>" + "  <script>mid_a();all();</script>" + "</all>"
+                + "</feature>");
+        featureC = builder.expectResource("<feature>" + "<name>featureC</name>"
+                + "<dependency>featureD</dependency>" + "<container>"
+                + "  <script>mid_b();container();</script>" + "</container>"
+                + "<gadget>" + "  <script>mid_b();gadget();</script>"
+                + "</gadget>" + "</feature>");
+        featureD = builder.expectResource("<feature>" + "<name>featureD</name>"
+                + "<all>" + "  <script>bottom();all();</script>" + "</all>"
+                + "</feature>");
+        txtFile = builder.expectResource(
+                featureA.toString() + '\n' + featureB.toString() + '\n'
+                        + featureC.toString() + '\n' + featureD.toString(),
+                ".txt");
+
+        try {
+            registry = builder.build(txtFile.toString());
+        } catch (GadgetException e) {
+            fail("Shouldn't throw a GadgetException.");
+        }
+  }
+
+  private GadgetContext getCtx(final RenderingContext rctx, final String container) {
+    return getCtx(rctx, container, false);
+  }
+
+  private GadgetContext getCtx(final RenderingContext rctx, final String container,
+      final boolean ignoreCache) {
+    return new GadgetContext() {
+      @Override
+      public RenderingContext getRenderingContext() {
+        return rctx;
+      }
+
+      @Override
+      public String getContainer() {
+        return container != null ? container : ContainerConfig.DEFAULT_CONTAINER;
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return ignoreCache;
+      }
+    };
+  }
+
+  private void setupMixedFullRegistry() throws Exception {
+    // Sets up a "full" gadget feature registry with several features registered in linear
+    // dependency order: top -> mid_a -> mid_b -> bottom
+    // The content registered for each is equal to the feature's name, for simplicity.
+    // Also, all content is loaded as inline, also for simplicity.
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    Uri topUri = builder.expectResource(
+      "<feature>" +
+        "<name>top</name>" +
+        "<dependency>mid_a</dependency>" +
+        "<dependency>mid_b</dependency>" +
+        "<gadget>" +
+        "  <script>top();gadget();</script>" +
+        "</gadget>" +
+        "<all>" +
+        "  <script>top();all();</script>" +
+        "</all>" +
+      "</feature>"
+    );
+    Uri midAUri = builder.expectResource(
+      "<feature>" +
+        "<name>mid_a</name>" +
+        "<dependency>mid_b</dependency>" +
+        "<container>" +
+        "  <script>mid_a();container();</script>" +
+        "</container>" +
+        "<all>" +
+        "  <script>mid_a();all();</script>" +
+        "</all>" +
+      "</feature>"
+    );
+    Uri midBUri = builder.expectResource(
+      "<feature>" +
+        "<name>mid_b</name>" +
+        "<dependency>bottom</dependency>" +
+        "<container>" +
+        "  <script>mid_b();container();</script>" +
+        "</container>" +
+        "<gadget>" +
+        "  <script>mid_b();gadget();</script>" +
+        "</gadget>" +
+      "</feature>"
+    );
+    Uri bottomUri = builder.expectResource(
+        "<feature>" +
+          "<name>bottom</name>" +
+          "<all>" +
+          "  <script>bottom();all();</script>" +
+          "</all>" +
+        "</feature>"
+      );
+    Uri txtFile = builder.expectResource(topUri.toString() + '\n' +
+        midAUri.toString() + '\n' + midBUri.toString() + '\n' + bottomUri.toString(), ".txt");
+
+    registry = builder.build(txtFile.toString());
+  }
+
+  private void setupFullRegistry(String type, String containers) throws Exception {
+    // Sets up a "full" gadget feature registry with several features registered:
+    // nodep - has no deps on anything else
+    // top - depends on mid_a and mid_b
+    // mid_a and mid_b - both depend on bottom
+    // bottom - depends on nothing else
+    // The content registered for each is equal to the feature's name, for simplicity.
+    // Also, all content is loaded as inline, also for simplicity.
+    TestFeatureRegistry.Builder builder = TestFeatureRegistry.newBuilder();
+
+    Map<String, String> attribs = Maps.newHashMap();
+    if (containers != null) {
+      attribs.put("container", containers);
+    }
+
+    Uri nodepUri = builder.expectResource(xml(NODEP_TPL, type, null, "nodep", attribs));
+    Uri topUri = builder.expectResource(xml(TOP_TPL, type, null, "top", attribs));
+    Uri midAUri = builder.expectResource(xml(MID_A_TPL, type, null, "mid_a", attribs));
+    Uri midBUri = builder.expectResource(xml(MID_B_TPL, type, null, "mid_b", attribs));
+    Uri bottomUri = builder.expectResource(xml(BOTTOM_TPL, type, null, "bottom", attribs));
+    Uri txtFile = builder.expectResource(nodepUri.toString() + '\n' + topUri.toString() + '\n' +
+        midAUri.toString() + '\n' + midBUri.toString() + '\n' + bottomUri.toString(), ".txt");
+    registry = builder.build(txtFile.toString());
+  }
+
+  private static String getFeatureTpl(String name, String[] deps) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("<feature><name>").append(name).append("</name>");
+    for (String dep : deps) {
+      sb.append("<dependency>").append(dep).append("</dependency>");
+    }
+    sb.append("<%type% %type_attribs%><script %uri% %res_attribs%>%content%</script></%type%>");
+    sb.append("</feature>");
+    return sb.toString();
+  }
+
+  private static String xml(String tpl, String type, String uri, String content) {
+    return xml(tpl, type, uri, content, Maps.<String, String>newHashMap());
+  }
+
+  private static String xml(String tpl, String type, String uri, String content,
+      Map<String, String> attribs) {
+    return xml(tpl, type, uri, content, attribs, Maps.<String, String>newHashMap());
+  }
+
+  private static String xml(String tpl, String type, String uri, String content,
+      Map<String, String> attribs, Map<String, String> resourceAttribs) {
+    StringBuilder sb = new StringBuilder();
+    for (Map.Entry<String, String> entry : attribs.entrySet()) {
+      sb.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\" ");
+    }
+    StringBuilder sbRes = new StringBuilder();
+    for (Map.Entry<String, String> entry : resourceAttribs.entrySet()) {
+      sbRes.append(entry.getKey()).append("=\"").append(entry.getValue()).append("\" ");
+    }
+    return tpl.replace("%type%", type)
+        .replace("%uri%", uri != null ? "src=\"" + uri + '\"' : "")
+        .replace("%content%", content != null ? content : "")
+        .replace("%type_attribs%", sb.toString())
+        .replace("%res_attribs%", sbRes.toString());
+  }
+
+  private static Uri makeFile(String content) throws Exception {
+    // .xml suffix used even for js -- should be OK per FeatureResourceLoader tests
+    // which simply indicate not to attempt .opt.js loading in this case.
+    File file = File.createTempFile("feat", ".xml");
+    file.deleteOnExit();
+    BufferedWriter out = new BufferedWriter(new FileWriter(file));
+    out.write(content);
+    out.close();
+    return Uri.fromJavaUri(file.toURI());
+  }
+
+  private static Uri makeResourceUri(String suffix) {
+    return Uri.parse("res://" + RESOURCE_BASE_PATH + "/file" + (++resourceIdx) + suffix);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java
new file mode 100644
index 0000000..07f9827
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/features/FeatureResourceLoaderTest.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.features;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.util.Map;
+
+public class FeatureResourceLoaderTest {
+  private final static String FILE_JS = "gadgets.test.pattern = function(){};";
+  private final static String UPDATED_FILE_JS = "different.impl.completely = function(){};";
+  private final static String UNCOMPRESSED_FILE_JS
+      = "/** Some comments* /\n" +
+        "gadgets.test.pattern = function() {" +
+        "};";
+  private final static String UPDATED_UNCOMPRESSED_FILE_JS
+  = "/** Different comments* /\n" +
+    "different.impl.completely = function() {" +
+    "};";
+  private final static String URL_JS = "while(true){alert('hello');}";
+
+  private TestFeatureResourceLoader loader;
+  private FakeTimeSource timeSource;
+  private HttpFetcher fetcher;
+
+
+  private static class TestFeatureResourceLoader extends FeatureResourceLoader {
+    public TestFeatureResourceLoader(
+        HttpFetcher fetcher, TimeSource timeSource, FeatureFileSystem fileSystem) {
+      super(fetcher, timeSource, fileSystem);
+    }
+
+    private final Map<String, Boolean> forceFileChanged = Maps.newHashMap();
+
+    @Override
+    protected boolean fileHasChanged(org.apache.shindig.gadgets.features.FeatureFile file, long lastModified) {
+      // TODO: Update test to use a mocked file and file system instead of real files
+      Boolean changeOverride = forceFileChanged.get(file.getAbsolutePath());
+      return file.lastModified() > lastModified ? true :
+          changeOverride != null && changeOverride;
+    }
+  }
+
+  @Before
+  public void setUp() {
+    fetcher = createMock(HttpFetcher.class);
+    timeSource = new FakeTimeSource();
+    timeSource.setCurrentTimeMillis(0);
+    loader = new TestFeatureResourceLoader(fetcher, timeSource, new DefaultFeatureFileSystem());
+  }
+
+  @Test
+  public void loadFileOptOnlyAvailable() throws Exception {
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    FeatureResource resource = loader.load(optUri.one, null);
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+
+  @Test
+  public void loadFileDebugOnlyAvailable() throws Exception {
+    Pair<Uri, File> dbgUri = makeFile(".js", UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getContent());
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+
+  @Test
+  public void loadFileBothModesAvailable() throws Exception {
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    File dbgFile = new File(optUri.one.getPath().replace(".opt.js", ".js"));
+    dbgFile.createNewFile();
+    Pair<Uri, File> dbgUri = makeFile(dbgFile, UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+
+  @Test(expected=IllegalArgumentException.class)
+  public void loadFileNothingAvailable() throws Exception {
+    Uri nilUri = new UriBuilder().setScheme("file").setPath("/does/not/exist.js").toUri();
+    loader.load(nilUri, null);
+    fail("Should have failed indicating could not find: " + nilUri.toString());
+  }
+
+  @Test
+  public void loadFileNoOptPathCalculable() throws Exception {
+    // File doesn't end in .js, so it's loaded for both opt and debug.
+    Pair<Uri, File> dbgUri = makeFile(".notjssuffix", UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getContent());
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+
+  @Test
+  public void loadFileUpdateIgnoredIfUpdatesDisabled() throws Exception {
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    FeatureResource resource = loader.load(optUri.one, null);
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+    setFileContent(optUri.two, UPDATED_FILE_JS);
+
+    // Advance the time. Update checks disabled by default.
+    timeSource.incrementSeconds(10);
+
+    // Same asserts.
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+
+  @Test
+  public void loadFileUpdateBehavior() throws Exception {
+    loader.setSupportFileUpdates(5000);  // set in millis
+    Pair<Uri, File> optUri = makeFile(".opt.js", FILE_JS);
+    File dbgFile = new File(optUri.one.getPath().replace(".opt.js", ".js"));
+    dbgFile.createNewFile();
+    Pair<Uri, File> dbgUri = makeFile(dbgFile, UNCOMPRESSED_FILE_JS);
+    FeatureResource resource = loader.load(dbgUri.one, null);
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+
+    // Update file contents.
+    setFileContent(optUri.two, UPDATED_FILE_JS);
+    loader.forceFileChanged.put(optUri.two.getAbsolutePath(), true);
+    setFileContent(dbgUri.two, UPDATED_UNCOMPRESSED_FILE_JS);
+    loader.forceFileChanged.put(dbgUri.two.getAbsolutePath(), true);
+
+    // Advance the time, but not by 5 seconds.
+    timeSource.incrementSeconds(4);
+
+    // Same asserts.
+    assertEquals(FILE_JS, resource.getContent());
+    assertEquals(UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+
+    // Advance the time, now beyond 5 seconds.
+    timeSource.incrementSeconds(4);
+
+    // New content should be reflected.
+    assertEquals(UPDATED_FILE_JS, resource.getContent());
+    assertEquals(UPDATED_UNCOMPRESSED_FILE_JS, resource.getDebugContent());
+    assertFalse(resource.isExternal());
+    assertTrue(resource.isProxyCacheable());
+  }
+
+  @Test
+  public void loadUriInline() throws Exception {
+    Uri uri = Uri.parse("http://apache.org/resource.js");
+    Map<String, String> attribs = Maps.newHashMap();
+    attribs.put("inline", "true");
+    mockFetcher(uri, URL_JS);
+    FeatureResource resource = loader.load(uri, attribs);
+    assertEquals(URL_JS, resource.getContent());
+    assertEquals(URL_JS, resource.getDebugContent());
+    assertTrue(resource.isProxyCacheable());
+    assertFalse(resource.isExternal());
+  }
+
+  @Test
+  public void loadUriInlineFetcherFailure() throws Exception {
+    Uri uri = Uri.parse("http://apache.org/resource.js");
+    Map<String, String> attribs = Maps.newHashMap();
+    attribs.put("inline", "true");
+    expect(fetcher.fetch(eq(new HttpRequest(uri))))
+        .andThrow(new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT));
+    replay(fetcher);
+    FeatureResource resource = loader.load(uri, attribs);
+    assertNull(resource.getContent());
+    assertNull(resource.getDebugContent());
+    assertFalse(resource.isProxyCacheable());
+    assertFalse(resource.isExternal());
+  }
+
+  @Test
+  public void loadUriExtern() throws Exception {
+    String theUrl = "http://apache.org/resource.js";
+    Uri uri = Uri.parse(theUrl);
+    Map<String, String> attribs = Maps.newHashMap();
+    mockFetcher(uri, URL_JS);
+    FeatureResource resource = loader.load(uri, attribs);
+    assertEquals(theUrl, resource.getContent());
+    assertEquals(theUrl, resource.getDebugContent());
+    assertTrue(resource.isProxyCacheable());
+    assertTrue(resource.isExternal());
+  }
+
+  @Test
+  public void loadRequestMarkedInternal() throws Exception {
+    String theUrl = "http://apache.org/resource.js";
+    Uri uri = Uri.parse(theUrl);
+    Map<String, String> attribs = Maps.newHashMap();
+    attribs.put( "inline", "true" );
+    CapturingHttpFetcher fetcher = new CapturingHttpFetcher();
+    FeatureResourceLoader frLoader = new TestFeatureResourceLoader(fetcher, timeSource, new DefaultFeatureFileSystem());
+    FeatureResource resource = frLoader.load(uri, attribs);
+    assertEquals(URL_JS, resource.getContent());
+    assertNotNull( fetcher.request );
+    assertTrue( fetcher.request.isInternalRequest() );
+  }
+
+  private Pair<Uri, File> makeFile(String suffix, String content) throws Exception {
+    File tmpFile = File.createTempFile("restmp", suffix);
+    return makeFile(tmpFile, content);
+  }
+
+  private Pair<Uri, File> makeFile(File file, String content) throws Exception {
+    file.deleteOnExit();
+    setFileContent(file, content);
+    return Pair.of(new UriBuilder().setScheme("file").setPath(file.getPath()).toUri(), file);
+  }
+
+  private void setFileContent(File file, String content) throws Exception {
+    BufferedWriter out = new BufferedWriter(new FileWriter(file));
+    out.write(content);
+    out.close();
+  }
+
+  private void mockFetcher(Uri toFetch, String content) throws Exception {
+    HttpRequest req = new HttpRequest(toFetch);
+    HttpResponse resp =
+        new HttpResponseBuilder().setHttpStatusCode(HttpResponse.SC_OK)
+                                 .setResponseString(content).create();
+    expect(fetcher.fetch(eq(req))).andReturn(resp);
+    replay(fetcher);
+  }
+
+  static class CapturingHttpFetcher implements HttpFetcher
+  {
+    public HttpRequest request;
+
+    public CapturingHttpFetcher() {
+    }
+
+    public HttpResponse fetch(HttpRequest request) throws GadgetException {
+      this.request = request;
+      return new HttpResponseBuilder().setHttpStatusCode( HttpResponse.SC_OK )
+                                      .setResponseString( URL_JS ).create();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/AbstractHttpCacheTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/AbstractHttpCacheTest.java
new file mode 100644
index 0000000..43e8200
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/AbstractHttpCacheTest.java
@@ -0,0 +1,549 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.auth.BasicSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class AbstractHttpCacheTest {
+  protected static final Uri DEFAULT_URI = Uri.parse("http://example.org/file.txt");
+  protected static final Uri IMAGE_URI = Uri.parse("http://example.org/image.png");
+  private static final Uri APP_URI = Uri.parse("http://example.org/gadget.xml");
+  private static final String MODULE_ID = "100";
+  private static final String SERVICE_NAME = "service";
+  private static final String TOKEN_NAME = "token";
+  private static final String CONTAINER_NAME = "container";
+
+  private final TestHttpCache cache = new TestHttpCache();
+  // Cache designed to return 86400ms for refetchStrictNoCacheAfterMs.
+  private TestHttpCache extendedStrictNoCacheTtlCache;
+
+  @Before
+  public void setUp() {
+    extendedStrictNoCacheTtlCache = new TestHttpCache();
+    extendedStrictNoCacheTtlCache.setRefetchStrictNoCacheAfterMs(86400L);
+  }
+
+  @Test
+  public void createKeySimple() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+    CacheKeyBuilder key = new CacheKeyBuilder()
+        .setLegacyParam(0, DEFAULT_URI).setLegacyParam(1, AuthType.NONE);
+
+    assertEquals(key.build(), cache.createKey(request));
+  }
+
+  private HttpRequest getMockImageRequest(String height, String width, String quality,
+      boolean noExpand, String mimeType, String ua) {
+    HttpRequest request = EasyMock.createMock(HttpRequest.class);
+    expect(request.getUri()).andReturn(IMAGE_URI).anyTimes();
+    expect(request.getAuthType()).andReturn(AuthType.NONE).anyTimes();
+    expect(request.getSecurityToken()).andReturn(null).anyTimes();
+    expect(request.getParam(Param.RESIZE_HEIGHT.getKey())).andReturn(height).anyTimes();
+    expect(request.getParam(Param.RESIZE_WIDTH.getKey())).andReturn(width).anyTimes();
+    expect(request.getParam(Param.RESIZE_QUALITY.getKey())).andReturn(quality).anyTimes();
+    expect(request.getParam(Param.NO_EXPAND.getKey())).andReturn(noExpand ? "1" : null).anyTimes();
+    expect(request.getRewriteMimeType()).andReturn(mimeType).anyTimes();
+    expect(request.getHeader("User-Agent")).andReturn(ua).anyTimes();
+    replay(request);
+    return request;
+  }
+
+  @Test
+  public void createKeySimpleImageRequest() throws Exception {
+    // Mock the Request with Image Resize (Quality) params, without rewrite mimeType.
+    HttpRequest request = getMockImageRequest("100", "80", "70", false, null, "Mozilla");
+    CacheKeyBuilder key = new CacheKeyBuilder()
+        .setLegacyParam(0, IMAGE_URI)
+        .setLegacyParam(1, AuthType.NONE)
+        .setParam("rh", "100")
+        .setParam("rw", "80")
+        .setParam("rq", "70")
+        .setParam("ua", "Mozilla");
+
+    assertEquals(key.build(), cache.createKey(request));
+  }
+
+  @Test
+  public void createKeyImageRequestRewrite() throws Exception {
+    // Mock the Request with Image Resize (Quality) params and specified rewrite mimeType.
+    HttpRequest request = getMockImageRequest("100", "80", "70", true, "image/jpg", "Mozilla");
+    CacheKeyBuilder key = new CacheKeyBuilder()
+        .setLegacyParam(0, IMAGE_URI)
+        .setLegacyParam(1, AuthType.NONE)
+        .setParam("rh", "100")
+        .setParam("rw", "80")
+        .setParam("rq", "70")
+        .setParam("ne", "1")
+        .setParam("rm", "image/jpg")
+        .setParam("ua", "Mozilla");
+
+    assertEquals(key.build(), cache.createKey(request));
+  }
+
+  @Test
+  public void createKeySignedOwner() throws Exception {
+    // Using a mock instead of a fake object makes the test less brittle if the interface should
+    // change.
+    RequestAuthenticationInfo authInfo = newMockAuthInfo(
+        true /* isSignOwner */,
+        false /* isSignViewer */,
+        ImmutableMap.of("OAUTH_SERVICE_NAME", SERVICE_NAME, "OAUTH_TOKEN_NAME", TOKEN_NAME));
+    replay(authInfo);
+
+    String ownerId = "owner eye dee";
+    SecurityToken securityToken = new BasicSecurityToken(ownerId, "", "", "",
+        APP_URI.toString(), MODULE_ID, CONTAINER_NAME, null, null);
+
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setAuthType(AuthType.SIGNED)
+        .setOAuthArguments(new OAuthArguments(authInfo))
+        .setSecurityToken(securityToken);
+
+    CacheKeyBuilder key = new CacheKeyBuilder()
+        .setLegacyParam(0, DEFAULT_URI)
+        .setLegacyParam(1, AuthType.SIGNED)
+        .setLegacyParam(2, ownerId)
+        .setLegacyParam(3, "")
+        .setLegacyParam(5, APP_URI)
+        .setLegacyParam(6, MODULE_ID)
+        .setLegacyParam(7, SERVICE_NAME)
+        .setLegacyParam(8, TOKEN_NAME);
+
+    assertEquals(key.build(), cache.createKey(request));
+  }
+
+  private RequestAuthenticationInfo newMockAuthInfo(boolean isSignOwner, boolean isSignViewer,
+      Map<String, String> attributesMap) {
+    RequestAuthenticationInfo authInfo = EasyMock.createNiceMock(RequestAuthenticationInfo.class);
+    expect(authInfo.getAttributes()).andReturn(attributesMap).anyTimes();
+    expect(authInfo.getAuthType()).andReturn(AuthType.SIGNED).anyTimes();
+    expect(authInfo.getHref()).andReturn(DEFAULT_URI).anyTimes();
+    expect(authInfo.isSignOwner()).andReturn(isSignOwner).anyTimes();
+    expect(authInfo.isSignViewer()).andReturn(isSignOwner).anyTimes();
+    return authInfo;
+  }
+
+  @Test
+  public void createKeySignedViewer() throws Exception {
+    RequestAuthenticationInfo authInfo = newMockAuthInfo(
+        false /* isSignOwner */,
+        true /* isSignViewer */,
+        ImmutableMap.of("OAUTH_SERVICE_NAME", SERVICE_NAME, "OAUTH_TOKEN_NAME", TOKEN_NAME));
+    replay(authInfo);
+
+    String viewerId = "viewer eye dee";
+    SecurityToken securityToken = new BasicSecurityToken(
+        "", viewerId, "", "", APP_URI.toString(), MODULE_ID, CONTAINER_NAME, null, null);
+
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setAuthType(AuthType.SIGNED)
+        .setOAuthArguments(new OAuthArguments(authInfo))
+        .setSecurityToken(securityToken);
+
+    CacheKeyBuilder key = new CacheKeyBuilder()
+        .setLegacyParam(0, DEFAULT_URI)
+        .setLegacyParam(1, AuthType.SIGNED)
+        .setLegacyParam(3, null) // The Viewer ID is in this case defaults to null
+        .setLegacyParam(5, APP_URI)
+        .setLegacyParam(6, MODULE_ID)
+        .setLegacyParam(7, SERVICE_NAME)
+        .setLegacyParam(8, TOKEN_NAME);
+
+    assertEquals(key.build(), cache.createKey(request));
+  }
+
+  @Test
+  public void createKeyWithTokenOwner() throws Exception {
+    RequestAuthenticationInfo authInfo = newMockAuthInfo(
+        true /* isSignOwner */,
+        true /* isSignViewer */,
+        ImmutableMap.of("OAUTH_SERVICE_NAME", SERVICE_NAME, "OAUTH_TOKEN_NAME", TOKEN_NAME,
+            "OAUTH_USE_TOKEN", "always"));
+    replay(authInfo);
+
+    String userId = "user id";
+    SecurityToken securityToken = new BasicSecurityToken(
+        userId, userId, "", "", APP_URI.toString(), MODULE_ID, CONTAINER_NAME, null, null);
+
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setAuthType(AuthType.SIGNED)
+        .setOAuthArguments(new OAuthArguments(authInfo))
+        .setSecurityToken(securityToken);
+
+    CacheKeyBuilder key = new CacheKeyBuilder()
+        .setLegacyParam(0, DEFAULT_URI)
+        .setLegacyParam(1, AuthType.SIGNED)
+        .setLegacyParam(2, userId)
+        .setLegacyParam(3, userId)
+        .setLegacyParam(4, userId)
+        .setLegacyParam(5, APP_URI)
+        .setLegacyParam(6, MODULE_ID)
+        .setLegacyParam(7, SERVICE_NAME)
+        .setLegacyParam(8, TOKEN_NAME);
+
+    assertEquals(key.build(), cache.createKey(request));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void createKeyWithoutSecurityToken() throws Exception {
+    RequestAuthenticationInfo authInfo = newMockAuthInfo(
+        true /* isSignOwner */,
+        false /* isSignViewer */,
+        ImmutableMap.<String, String>of());
+    replay(authInfo);
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setAuthType(AuthType.SIGNED)
+        .setOAuthArguments(new OAuthArguments(authInfo));
+    cache.createKey(request);
+  }
+
+
+  @Test
+  public void getResponse() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponse("result");
+    cache.map.put(key, response);
+
+    assertEquals(response, cache.getResponse(request));
+
+    extendedStrictNoCacheTtlCache.map.put(key, response);
+    assertEquals(response, extendedStrictNoCacheTtlCache.getResponse(request));
+  }
+
+  @Test
+  public void getResponseUsingPost() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setMethod("POST");
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponse("result");
+    cache.map.put(key, response);
+
+    assertNull("Did not return null when method was POST", cache.getResponse(request));
+
+    extendedStrictNoCacheTtlCache.map.put(key, response);
+    assertNull("Did not return null when method was POST",
+               extendedStrictNoCacheTtlCache.getResponse(request));
+  }
+
+  @Test
+  public void getResponseUsingMethodOverride() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setMethod("POST")
+        .addHeader("X-Method-Override", "GET");
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponse("result");
+    cache.map.put(key, response);
+
+    assertEquals(response, cache.getResponse(request));
+
+    extendedStrictNoCacheTtlCache.map.put(key, response);
+    assertEquals(response, extendedStrictNoCacheTtlCache.getResponse(request));
+  }
+
+  @Test
+  public void getResponseIgnoreCache() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponse("result");
+    cache.map.put(key, response);
+
+    request.setIgnoreCache(true);
+
+    assertNull("Did not return null when ignoreCache was true", cache.getResponse(request));
+
+    extendedStrictNoCacheTtlCache.map.put(key, response);
+    assertNull("Did not return null when ignoreCache was true",
+               extendedStrictNoCacheTtlCache.getResponse(request));
+  }
+
+  @Test
+  public void getResponseNotCacheable() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponseBuilder().setStrictNoCache().create();
+    cache.map.put(key, response);
+
+    assertNull("Did not return null when response was uncacheable", cache.getResponse(request));
+
+    extendedStrictNoCacheTtlCache.map.put(key, response);
+    assertEquals(response, extendedStrictNoCacheTtlCache.getResponse(request));
+  }
+
+  @Test
+  public void addResponse() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    HttpResponse response = new HttpResponse("normal");
+    String key = cache.createKey(request);
+
+    assertNotNull("response should have been cached", cache.addResponse(request, response));
+    assertEquals(response, cache.map.get(key));
+
+    assertNotNull("response should have been cached",
+               extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals(response, extendedStrictNoCacheTtlCache.map.get(key));
+  }
+
+  @Test
+  public void addResponseIgnoreCache() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setIgnoreCache(true);
+    HttpResponse response = new HttpResponse("does not matter");
+
+    assertNull("response should not have been cached", cache.addResponse(request, response));
+    assertEquals(0, cache.map.size());
+
+    assertNull("response should not have been cached",
+                extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals(0, extendedStrictNoCacheTtlCache.map.size());
+  }
+
+  @Test
+  public void addResponseNotCacheable() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    HttpResponse response = new HttpResponseBuilder().setStrictNoCache().create();
+    String key = cache.createKey(request);
+
+    assertNull(cache.addResponse(request, response));
+    assertEquals(0, cache.map.size());
+
+    assertNotNull("response should have been cached",
+               extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals(
+        extendedStrictNoCacheTtlCache.buildStrictNoCacheHttpResponse(response).create(),
+        extendedStrictNoCacheTtlCache.map.get(key));
+  }
+
+  @Test
+  public void addResponseIfModifiedSince() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    HttpResponse response = new HttpResponseBuilder().setHttpStatusCode(HttpResponse.SC_NOT_MODIFIED).create();
+
+    assertNull(cache.addResponse(request, response));
+    assertEquals(0, cache.map.size());
+
+    assertNull(extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals(0, extendedStrictNoCacheTtlCache.map.size());
+  }
+
+  @Test
+  public void addResponseUsingPost() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setMethod("POST");
+    HttpResponse response = new HttpResponse("does not matter");
+
+    assertNull(cache.addResponse(request, response));
+    assertEquals(0, cache.map.size());
+
+    assertNull(extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals(0, extendedStrictNoCacheTtlCache.map.size());
+  }
+
+  @Test
+  public void addResponseUsingMethodOverride() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setMethod("POST")
+        .addHeader("X-Method-Override", "GET");
+    HttpResponse response = new HttpResponse("normal");
+    String key = cache.createKey(request);
+
+    assertNotNull(cache.addResponse(request, response));
+    assertEquals(response, cache.map.get(key));
+
+    assertNotNull(extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals(response, extendedStrictNoCacheTtlCache.map.get(key));
+  }
+
+  @Test
+  public void addResponseWithForcedTtl() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setCacheTtl(10);
+
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponse("result");
+
+    assertNotNull(cache.addResponse(request, response));
+
+    assertEquals("public,max-age=10", cache.map.get(key).getHeader("Cache-Control"));
+
+    assertNotNull(extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals("public,max-age=10",
+                 extendedStrictNoCacheTtlCache.map.get(key).getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void addResponseWithForcedTtlAndStrictNoCache() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setCacheTtl(10);
+
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("result")
+        .setStrictNoCache()
+        .create();
+
+    assertNotNull(cache.addResponse(request, response));
+
+    assertEquals("public,max-age=10", cache.map.get(key).getHeader("Cache-Control"));
+
+    assertNotNull(extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals("public,max-age=10",
+                 extendedStrictNoCacheTtlCache.map.get(key).getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void addResponseWithForcedTtlAndErrorResponse() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setCacheTtl(10);
+
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("result")
+        .setHttpStatusCode(500)
+        .create();
+
+    assertNotNull(cache.addResponse(request, response));
+
+    assertNull(cache.map.get(key).getHeader("Cache-Control"));
+
+    assertNotNull(extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertNull(extendedStrictNoCacheTtlCache.map.get(key).getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void addResponseWithNoCachingHeaders() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponse("no headers");
+
+    assertNotNull(cache.addResponse(request, response));
+
+    assertEquals("no headers", cache.map.get(key).getResponseAsString());
+
+    assertNotNull(extendedStrictNoCacheTtlCache.addResponse(request, response));
+    assertEquals("no headers", extendedStrictNoCacheTtlCache.map.get(key).getResponseAsString());
+  }
+
+  @Test
+  public void buildStrictNoCacheHttpResponse() {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("result")
+        .addHeader("Cache-Control", "private, max-age=1000")
+        .addHeader("X-Method-Override", "GET")
+        .create();
+    assertTrue(response.isStrictNoCache());
+    HttpResponse builtResponse = extendedStrictNoCacheTtlCache
+        .buildStrictNoCacheHttpResponse(response).create();
+
+    assertTrue(builtResponse.isStrictNoCache());
+    assertEquals("", builtResponse.getResponseAsString());
+    assertEquals("private, max-age=1000", builtResponse.getHeader("Cache-Control"));
+    assertEquals(86400, builtResponse.getRefetchStrictNoCacheAfterMs());
+    assertFalse(builtResponse.getHeaders().containsKey("Pragma"));
+    assertNull(builtResponse.getHeader("X-Method-Override"));
+  }
+
+  @Test
+  public void buildStrictNoCacheHttpResponseWithPragmaHeader() {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("result")
+        .addHeader("Pragma", "no-cache")
+        .create();
+    assertTrue(response.isStrictNoCache());
+    HttpResponse builtResponse = cache
+        .buildStrictNoCacheHttpResponse(response).create();
+
+    assertTrue(builtResponse.isStrictNoCache());
+    assertEquals("", builtResponse.getResponseAsString());
+    assertNull(builtResponse.getHeader("Cache-Control"));
+    assertEquals("no-cache", builtResponse.getHeader("Pragma"));
+  }
+
+  @Test
+  public void removeResponse() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponse("result");
+    cache.map.put(key, response);
+
+    assertEquals(response, cache.removeResponse(request));
+    assertEquals(0, cache.map.size());
+  }
+
+  @Test
+  public void removeResponseIsStaled() {
+    long expiration = System.currentTimeMillis() + 1000L;
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    String key = cache.createKey(request);
+    HttpResponse response = new HttpResponseBuilder()
+        .setExpirationTime(expiration)
+        .create();
+    cache.map.put(key, response);
+
+    // The cache itself still hold and return staled value,
+    // caller responsible to decide what to do about it
+    assertEquals(response, cache.removeResponse(request));
+    assertEquals(0, cache.map.size());
+  }
+
+  private static class TestHttpCache extends AbstractHttpCache {
+    protected final Map<String, HttpResponse> map;
+
+    public TestHttpCache() {
+      map = Maps.newHashMap();
+    }
+
+    @Override
+    public void addResponseImpl(String key, HttpResponse response) {
+      map.put(key, response);
+    }
+
+    @Override
+    public HttpResponse getResponseImpl(String key) {
+      return map.get(key);
+    }
+
+    @Override
+    public void removeResponseImpl(String key) {
+      map.remove(key);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/AbstractHttpFetcherTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/AbstractHttpFetcherTest.java
new file mode 100644
index 0000000..b9b80ad
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/AbstractHttpFetcherTest.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import junitx.framework.ArrayAssert;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.GadgetException;
+import org.junit.AfterClass;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Holds test cases that all HttpFetcher implementations should pass.  This
+ * starts up an HTTP server and runs tests against it.
+ */
+public abstract class AbstractHttpFetcherTest {
+  private static final int ECHO_PORT = 9003;
+  protected static final Uri BASE_URL = Uri.parse("http://localhost:9003/");
+  private static EchoServer server;
+  protected HttpFetcher fetcher = null;
+
+  @BeforeClass
+  public static void setUpOnce() throws Exception {
+    server = new EchoServer();
+    server.start(ECHO_PORT);
+  }
+
+  @AfterClass
+  public static void tearDownOnce() throws Exception {
+    if (server != null) {
+      server.stop();
+    }
+  }
+
+  @Test public void testHttpBadHost() throws Exception {
+    Uri uri = Uri.parse("http://a:b:c/");
+    HttpRequest request = new HttpRequest(uri);
+    try {
+      fetcher.fetch(request);
+      fail("Expected GadgetException");
+    } catch (GadgetException e) {
+      assertEquals(400, e.getHttpStatusCode());
+      assertTrue(e.getMessage().contains("Bad host name in request"));
+    }
+  }
+
+  @Test public void testHttpBadPort() throws Exception {
+    Uri uri = Uri.parse("http://a:b/");
+    HttpRequest request = new HttpRequest(uri);
+    try {
+      fetcher.fetch(request);
+      fail("Expected GadgetException");
+    } catch (GadgetException e) {
+      assertEquals(400, e.getHttpStatusCode());
+      assertTrue(e.getMessage().contains("Bad port number in request"));
+    }
+  }
+
+  @Test public void testHttpBadUrl() throws Exception {
+    Uri uri = Uri.parse("host/data");
+    HttpRequest request = new HttpRequest(uri);
+    try {
+      fetcher.fetch(request);
+      fail("Expected GadgetException");
+    } catch (GadgetException e) {
+      assertEquals(400, e.getHttpStatusCode());
+      assertTrue(e.getMessage().contains("Missing domain name for request"));
+    }
+  }
+
+  @Test public void testHttpNoSchema() throws Exception {
+    Uri uri = Uri.parse("//host/data");
+    HttpRequest request = new HttpRequest(uri);
+    try {
+      fetcher.fetch(request);
+      fail("Expected GadgetException");
+    } catch (GadgetException e) {
+      assertEquals(400, e.getHttpStatusCode());
+      assertTrue(e.getMessage().contains("Missing schema for request"));
+    }
+  }
+
+  @Test public void testHttpUnderscore() throws Exception {
+    Uri uri = Uri.parse("http://0.test_host.com/data");
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(504, response.getHttpStatusCode()); //timeout
+  }
+
+  @Test public void testHttpFetch() throws Exception {
+    String content = "Hello, world!";
+    Uri uri = new UriBuilder(BASE_URL).addQueryParameter("body", content).toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(200, response.getHttpStatusCode());
+    assertEquals(content, response.getResponseAsString());
+  }
+
+  @Test public void testHttp404() throws Exception {
+    String content = "Hello, world!";
+    Uri uri = new UriBuilder(BASE_URL)
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "404")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(404, response.getHttpStatusCode());
+    assertEquals(content, response.getResponseAsString());
+  }
+
+  @Test public void testHttp403() throws Exception {
+    String content = "Hello, world!";
+    Uri uri = new UriBuilder(BASE_URL)
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "403")
+        .addQueryParameter("header", "WWW-Authenticate=some auth data")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(content, response.getResponseAsString());
+    assertEquals("some auth data", response.getHeader("WWW-Authenticate"));
+  }
+
+  @Test public void testHttp403NoBody() throws Exception {
+    String content = "";
+    Uri uri = new UriBuilder(BASE_URL)
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "403")
+        .addQueryParameter("header", "WWW-Authenticate=some auth data")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(content, response.getResponseAsString());
+    assertEquals("some auth data", response.getHeader("WWW-Authenticate"));
+  }
+
+  @Test public void testHttp401NoBody() throws Exception {
+    String content = "";
+    Uri uri = new UriBuilder(BASE_URL)
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "401")
+        .addQueryParameter("header", "WWW-Authenticate=some auth data")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(401, response.getHttpStatusCode());
+    assertEquals(content, response.getResponseAsString());
+    assertEquals("some auth data", response.getHeader("WWW-Authenticate"));
+  }
+
+  @Test public void testDelete() throws Exception {
+    HttpRequest request = new HttpRequest(BASE_URL).setMethod("DELETE");
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals("DELETE", response.getHeader("x-method"));
+  }
+
+  @Test public void testPost_noBody() throws Exception {
+    HttpRequest request = new HttpRequest(BASE_URL).setMethod("POST");
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals("POST", response.getHeader("x-method"));
+    assertEquals("", response.getResponseAsString());
+  }
+
+  @Test public void testPost_withBody() throws Exception {
+    byte[] body = new byte[5000];
+    for (int i=0; i < body.length; ++i) {
+      body[i] = (byte)(i % 255);
+    }
+    HttpRequest request = new HttpRequest(BASE_URL)
+        .setMethod("POST")
+        .setPostBody(body)
+        .addHeader("content-type", "application/octet-stream");
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals("POST", response.getHeader("x-method"));
+    ArrayAssert.assertEquals(body, response.getResponseAsBytes());
+  }
+
+  @Test public void testPut_noBody() throws Exception {
+    HttpRequest request = new HttpRequest(BASE_URL).setMethod("PUT");
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals("PUT", response.getHeader("x-method"));
+    assertEquals("", response.getResponseAsString());
+  }
+
+  @Test public void testPut_withBody() throws Exception {
+    byte[] body = new byte[5000];
+    for (int i=0; i < body.length; ++i) {
+      body[i] = (byte)i;
+    }
+    HttpRequest request = new HttpRequest(BASE_URL)
+        .setMethod("PUT")
+        .setPostBody(body)
+        .addHeader("content-type", "application/octet-stream");
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals("PUT", response.getHeader("x-method"));
+    ArrayAssert.assertEquals(body, response.getResponseAsBytes());
+  }
+
+  @Test public void testHugeBody() throws Exception {
+    byte[] body = new byte[1024*1024]; // 1 MB
+    for (int i=0; i < body.length; ++i) {
+      body[i] = (byte)i;
+    }
+    HttpRequest request = new HttpRequest(BASE_URL)
+        .setMethod("POST")
+        .setPostBody(body)
+        .addHeader("content-type", "application/octet-stream");
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals("POST", response.getHeader("x-method"));
+    ArrayAssert.assertEquals(body, response.getResponseAsBytes());
+  }
+
+  @Test public void testFollowRedirects() throws Exception {
+    String content = "";
+    Uri uri = new UriBuilder(BASE_URL)
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "302")
+        .addQueryParameter("header", "Location=" + BASE_URL.toString() + "?body=redirected")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(200, response.getHttpStatusCode());
+    assertEquals("redirected", response.getResponseAsString());
+  }
+
+  @Test public void testFollowRelativeRedirects() throws Exception {
+    String content = "";
+    Uri uri = new UriBuilder(BASE_URL)
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "302")
+        .addQueryParameter("header", "Location=/?body=redirected")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(200, response.getHttpStatusCode());
+    assertEquals("redirected", response.getResponseAsString());
+  }
+
+  @Test public void testNoFollowRedirects() throws Exception {
+    String content = "";
+    Uri uri = new UriBuilder(BASE_URL)
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "302")
+        .addQueryParameter("header", "Location=" + BASE_URL.toString() + "?body=redirected")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri)
+        .setFollowRedirects(false);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(302, response.getHttpStatusCode());
+    assertEquals(content, response.getResponseAsString());
+    assertEquals(BASE_URL.toString() + "?body=redirected", response.getHeader("Location"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/BasicHttpFetcherTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/BasicHttpFetcherTest.java
new file mode 100644
index 0000000..73fc6f7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/BasicHttpFetcherTest.java
@@ -0,0 +1,197 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.http.HttpEntity;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.easymock.EasyMock;
+import org.junit.AfterClass;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class BasicHttpFetcherTest {
+  private static final int ECHO_PORT = 9003;
+  protected static final Uri BASE_URL = Uri.parse("http://localhost:9003/");
+  private static EchoServer server;
+
+  protected BasicHttpFetcher fetcher = null;
+  protected HttpEntity mockEntity;
+  protected InputStream mockInputStream;
+
+  @BeforeClass
+  public static void setUpOnce() throws Exception {
+    server = new EchoServer();
+    server.start(ECHO_PORT);
+  }
+
+  @AfterClass
+  public static void tearDownOnce() throws Exception {
+    if (server != null) {
+      server.stop();
+    }
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    fetcher = new BasicHttpFetcher(BASE_URL.getAuthority());
+
+    mockInputStream = EasyMock.createMock(InputStream.class);
+    EasyMock.expect(mockInputStream.available()).andReturn(0);
+    mockInputStream.close();
+
+    mockEntity = EasyMock.createMock(HttpEntity.class);
+    EasyMock.expect(mockEntity.getContent()).andReturn(mockInputStream);
+    EasyMock.expect(mockEntity.getContentLength()).andReturn(16384L).anyTimes();
+  }
+
+  @Test
+  public void testWithProxy() throws Exception {
+    String content = "Hello, Gagan!";
+    Uri uri = new UriBuilder(Uri.parse("http://www.google.com/search"))
+        .addQueryParameter("body", content)
+        .addQueryParameter("status", "201")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(201, response.getHttpStatusCode());
+    assertEquals(content, response.getResponseAsString());
+  }
+
+  @Test
+  public void testToByteArraySafeThrowsException1() throws Exception {
+    EasyMock.reset(mockInputStream);
+    mockInputStream.close();
+
+    String exceptionMessage = "IO Exception and Any Random Cause";
+    IOException e = new IOException(exceptionMessage);
+    EasyMock.expect(mockInputStream.read(EasyMock.isA(byte[].class))).andThrow(e).anyTimes();
+
+    EasyMock.replay(mockEntity, mockInputStream);
+    boolean exceptionCaught = false;
+
+    try {
+      fetcher.toByteArraySafe(mockEntity);
+    } catch (IOException ioe) {
+      assertEquals(exceptionMessage, ioe.getMessage());
+      exceptionCaught = true;
+    }
+    assertTrue(exceptionCaught);
+    EasyMock.verify(mockEntity, mockInputStream);
+  }
+
+  @Test
+  public void testToByteArraySafeThrowsException2() throws Exception {
+    String exceptionMessage = "EOF Exception and Any Random Cause";
+    EOFException e = new EOFException(exceptionMessage);
+    EasyMock.expect(mockInputStream.read(EasyMock.isA(byte[].class))).andThrow(e).anyTimes();
+
+    EasyMock.replay(mockEntity, mockInputStream);
+    boolean exceptionCaught = false;
+
+    try {
+      fetcher.toByteArraySafe(mockEntity);
+    } catch (EOFException eofe) {
+      assertEquals(exceptionMessage, eofe.getMessage());
+      exceptionCaught = true;
+    }
+    assertTrue(exceptionCaught);
+    EasyMock.verify(mockEntity, mockInputStream);
+  }
+
+  @Test
+  public void testToByteArraySafeThrowsException3() throws Exception {
+    EasyMock.reset(mockInputStream);
+    mockInputStream.close();
+
+    // Return non-zero for 'InputStream.available()'. This should violate the other condition.
+    EasyMock.expect(mockInputStream.available()).andReturn(1);
+    String exceptionMessage = "Unexpected end of ZLIB input stream";
+    EOFException e = new EOFException(exceptionMessage);
+    EasyMock.expect(mockInputStream.read(EasyMock.isA(byte[].class))).andThrow(e).anyTimes();
+
+    EasyMock.replay(mockEntity, mockInputStream);
+    boolean exceptionCaught = false;
+
+    try {
+      fetcher.toByteArraySafe(mockEntity);
+    } catch (EOFException eofe) {
+      assertEquals(exceptionMessage, eofe.getMessage());
+      exceptionCaught = true;
+    }
+    EasyMock.verify(mockEntity, mockInputStream);
+    assertTrue(exceptionCaught);
+  }
+
+  @Test
+  public void testToByteArraySafeHandleException() throws Exception {
+    String exceptionMessage = "Unexpected end of ZLIB input stream";
+    EOFException e = new EOFException(exceptionMessage);
+    EasyMock.expect(mockInputStream.read(EasyMock.isA(byte[].class))).andThrow(e).anyTimes();
+
+    EasyMock.replay(mockEntity, mockInputStream);
+
+    try {
+      fetcher.toByteArraySafe(mockEntity);
+    } catch (EOFException eofe) {
+      fail("Exception Should have been caught");
+    }
+    EasyMock.verify(mockEntity, mockInputStream);
+  }
+
+  @Test
+  public void testToByteArraySafeHandlesExceptionWithNoMessage() throws Exception {
+    EOFException e = new EOFException();
+    EasyMock.expect(mockInputStream.read(EasyMock.isA(byte[].class))).andThrow(e).anyTimes();
+
+    EasyMock.replay(mockEntity, mockInputStream);
+
+    try {
+      fetcher.toByteArraySafe(mockEntity);
+    } catch (EOFException eofe) {
+      fail("Exception Should have been caught");
+    }
+    EasyMock.verify(mockEntity, mockInputStream);
+  }
+
+  /*
+   * https://issues.apache.org/jira/browse/SHINDIG-1425
+   */
+  @Test
+  public void testHeadWithMaxObjectSizeBytes() throws Exception {
+	fetcher.setMaxObjectSizeBytes(1024 * 1024);
+    Uri uri = new UriBuilder(Uri.parse("http://www.google.com/search"))
+        .addQueryParameter("body", "")
+        .addQueryParameter("status", "200")
+        .toUri();
+    HttpRequest request = new HttpRequest(uri);
+    request.setMethod("HEAD");
+    HttpResponse response = fetcher.fetch(request);
+    assertEquals(200, response.getHttpStatusCode());
+    assertEquals("", response.getResponseAsString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/CacheKeyBuilderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/CacheKeyBuilderTest.java
new file mode 100644
index 0000000..54e3027
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/CacheKeyBuilderTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for the {@link CacheKeyBuilder}.
+ *
+ * <p>These tests are critical in that when the implementation of the CacheKeyBuilder changes,
+ * the tests seen below <em>must</em> still pass, unmodified.
+ *
+ * <p>This ensures smooth rolling restart of the high-load servers with persistent cache, for which
+ * changing the caching scheme across runs would generate lots of traffic due to artificial
+ * cache misses.
+ */
+public class CacheKeyBuilderTest extends Assert {
+
+  private CacheKeyBuilder builder;
+
+  @Before
+  public void setUp() {
+    builder = new CacheKeyBuilder()
+        .setLegacyParam(0, Uri.parse("http://example.com"))
+        .setLegacyParam(1, AuthType.SIGNED);
+  }
+
+  @Test
+  public void testBuilder() {
+    assertEquals("http://example.com:signed:0:0:0:0:0:0:0", builder.build());
+  }
+
+  @Test
+  public void testOwner() {
+    builder.setLegacyParam(2, "owner");
+    assertEquals("http://example.com:signed:owner:0:0:0:0:0:0", builder.build());
+  }
+
+  @Test
+  public void testViewer() {
+    builder.setLegacyParam(3, "viewer");
+    assertEquals("http://example.com:signed:0:viewer:0:0:0:0:0", builder.build());
+  }
+
+  @Test
+  public void testTokenOwner() {
+    builder.setLegacyParam(4, "token");
+    assertEquals("http://example.com:signed:0:0:token:0:0:0:0", builder.build());
+  }
+
+  @Test
+  public void testAppUrl() {
+    builder.setLegacyParam(5, "appurl");
+    assertEquals("http://example.com:signed:0:0:0:appurl:0:0:0", builder.build());
+  }
+
+  @Test
+  public void testInstanceId() {
+    builder.setLegacyParam(6, "id");
+    assertEquals("http://example.com:signed:0:0:0:0:id:0:0", builder.build());
+  }
+
+  @Test
+  public void testServiceName() {
+    builder.setLegacyParam(7, "srv");
+    assertEquals("http://example.com:signed:0:0:0:0:0:srv:0", builder.build());
+  }
+
+  @Test
+  public void testTokenName() {
+    builder.setLegacyParam(8, "token");
+    assertEquals("http://example.com:signed:0:0:0:0:0:0:token", builder.build());
+  }
+
+  // The additional parameters, proxy image dimensions
+  @Test
+  public void testParam() {
+    builder.setParam("rh", 1);
+    assertEquals("http://example.com:signed:0:0:0:0:0:0:0:rh=1", builder.build());
+  }
+
+  @Test
+  public void testResizeParams() {
+    builder.setParam("rh", 1);
+    builder.setParam("rq", 2);
+    builder.setParam("rw", 3);
+    assertEquals("http://example.com:signed:0:0:0:0:0:0:0:rh=1:rq=2:rw=3", builder.build());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultHttpCacheTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultHttpCacheTest.java
new file mode 100644
index 0000000..a1c2617
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultHttpCacheTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.cache.LruCacheProvider;
+import org.apache.shindig.common.uri.Uri;
+
+import org.junit.Test;
+
+/**
+ * Tests for basic content cache
+ */
+public class DefaultHttpCacheTest {
+  private static final Uri DEFAULT_URI = Uri.parse("http://example.org/file.txt");
+  private final CacheProvider cacheProvider = new LruCacheProvider(10);
+  private final Cache<String, HttpResponse> cache
+      = cacheProvider.createCache(DefaultHttpCache.CACHE_NAME);
+  private final DefaultHttpCache httpCache = new DefaultHttpCache(cacheProvider);
+
+  @Test
+  public void getResponse() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    HttpResponse response = new HttpResponse("response");
+
+    String key = httpCache.createKey(request);
+
+    cache.addElement(key, response);
+
+    assertEquals(response, httpCache.getResponse(request));
+  }
+
+  @Test
+  public void addResponse() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    HttpResponse response = new HttpResponse("response");
+
+    httpCache.addResponse(request, response);
+
+    String key = httpCache.createKey(request);
+
+    assertEquals(response, cache.getElement(key));
+  }
+
+  @Test
+  public void removeResponse() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    HttpResponse response = new HttpResponse("response");
+
+    String key = httpCache.createKey(request);
+
+    cache.addElement(key, response);
+
+    assertEquals(response, httpCache.removeResponse(request));
+
+    assertEquals(0, cache.getSize());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultInvalidationServiceTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultInvalidationServiceTest.java
new file mode 100644
index 0000000..92913da
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultInvalidationServiceTest.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.shindig.common.cache.LruCacheProvider;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.rewrite.DefaultResponseRewriterRegistry;
+import org.easymock.IMocksControl;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+
+public class DefaultInvalidationServiceTest extends Assert {
+
+  private static final Uri URI = Uri.parse("http://www.example.org/spec.xml");
+  private static final HttpResponse CACHEABLE = new HttpResponseBuilder()
+      .setResponseString("ORIGINALCONTENT")
+      .setHeader("Cache-Control", "max-age=1000").create();
+
+  IMocksControl control;
+  HttpCache cache;
+  DefaultInvalidationService service;
+  LruCacheProvider cacheProvider;
+  FakeGadgetToken appxToken;
+  FakeGadgetToken appyToken;
+
+  DefaultRequestPipelineTest.FakeHttpFetcher fetcher;
+  DefaultRequestPipelineTest.FakeOAuthRequestProvider oauth;
+  DefaultRequestPipelineTest.FakeOAuth2RequestProvider oauth2;
+  DefaultRequestPipeline requestPipeline;
+  HttpRequest signedRequest;
+
+
+  @Before
+  public void setUp() {
+    cacheProvider = new LruCacheProvider(100);
+    cache = new DefaultHttpCache(cacheProvider);
+    service = new DefaultInvalidationService(cache, cacheProvider, new AtomicLong());
+    appxToken = new FakeGadgetToken();
+    appxToken.setAppId("AppX");
+    appxToken.setOwnerId("OwnerX");
+    appxToken.setViewerId("ViewerX");
+    appyToken = new FakeGadgetToken();
+    appyToken.setAppId("AppY");
+    appyToken.setOwnerId("OwnerY");
+    appyToken.setViewerId("ViewerY");
+
+    signedRequest = new HttpRequest(URI);
+    signedRequest.setAuthType(AuthType.SIGNED);
+    signedRequest.setSecurityToken(appxToken);
+    signedRequest.setOAuthArguments(new OAuthArguments());
+    signedRequest.getOAuthArguments().setUseToken(OAuthArguments.UseToken.NEVER);
+    signedRequest.getOAuthArguments().setSignOwner(true);
+    signedRequest.getOAuthArguments().setSignViewer(true);
+
+    fetcher = new DefaultRequestPipelineTest.FakeHttpFetcher();
+    oauth = new DefaultRequestPipelineTest.FakeOAuthRequestProvider();
+    requestPipeline = new DefaultRequestPipeline(fetcher, cache, oauth, oauth2,
+        new DefaultResponseRewriterRegistry(null, null), service,
+        new HttpResponseMetadataHelper());
+  }
+
+  @Test
+  public void testInvalidateUrl() throws Exception {
+    cache.addResponse(new HttpRequest(URI), CACHEABLE);
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+    service.invalidateApplicationResources(
+        ImmutableSet.of(URI),
+        appxToken);
+    assertEquals(0, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+  }
+
+  @Test
+  public void testInvalidateUsers() throws Exception {
+    service.invalidateUserResources(ImmutableSet.of("example.org:1", "example.org:2"),
+        appxToken);
+    service.invalidateUserResources(ImmutableSet.of("example.org:1", "example.org:2"),
+        appyToken);
+    assertEquals(4, cacheProvider.createCache(DefaultInvalidationService.CACHE_NAME).getSize());
+    assertNotNull(cacheProvider.createCache(DefaultInvalidationService.CACHE_NAME)
+        .getElement("INV_TOK:AppX:1"));
+    assertNotNull(cacheProvider.createCache(DefaultInvalidationService.CACHE_NAME)
+        .getElement("INV_TOK:AppX:2"));
+    assertNotNull(cacheProvider.createCache(DefaultInvalidationService.CACHE_NAME)
+        .getElement("INV_TOK:AppY:1"));
+    assertNotNull(cacheProvider.createCache(DefaultInvalidationService.CACHE_NAME)
+        .getElement("INV_TOK:AppY:2"));
+  }
+
+  @Test
+  public void testFetchWithInvalidationEnabled() throws Exception {
+    cache.addResponse(new HttpRequest(URI), CACHEABLE);
+    assertEquals(CACHEABLE, requestPipeline.execute(new HttpRequest(URI)));
+  }
+
+  @Test
+  public void testFetchInvalidatedContent() throws Exception {
+    // Prime the cache
+    cache.addResponse(new HttpRequest(URI), CACHEABLE);
+
+    // Invalidate the entry
+    service.invalidateApplicationResources(
+        ImmutableSet.of(URI),
+        appxToken);
+
+    fetcher.response = new HttpResponseBuilder(CACHEABLE).setResponseString("NEWCONTENT1").
+        create();
+    assertEquals(requestPipeline.execute(new HttpRequest(URI)), fetcher.response);
+  }
+
+  @Test
+  public void testFetchContentWithMarker() throws Exception {
+    oauth.httpResponse = CACHEABLE;
+
+    // First entry added to cache is unmarked
+    HttpResponse httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals(CACHEABLE, httpResponse);
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+
+    // Invalidate content for OwnerX. Next entry will have owner mark
+    service.invalidateUserResources(ImmutableSet.of("OwnerX"), appxToken);
+
+    oauth.httpResponse = new HttpResponseBuilder(CACHEABLE).setResponseString("NEWCONTENT1").
+        create();
+    httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals("NEWCONTENT1", httpResponse.getResponseAsString());
+    assertEquals("o=1;", httpResponse.getHeader(DefaultInvalidationService.INVALIDATION_HEADER));
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+
+    // Invalidate content for ViewerX. Next entry will have both owner and viewer mark
+    service.invalidateUserResources(ImmutableSet.of("ViewerX"), appxToken);
+    oauth.httpResponse = new HttpResponseBuilder(CACHEABLE).setResponseString("NEWCONTENT2").
+        create();
+    httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals("NEWCONTENT2", httpResponse.getResponseAsString());
+    assertEquals("o=1;v=2;",
+        httpResponse.getHeader(DefaultInvalidationService.INVALIDATION_HEADER));
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+  }
+
+  @Test
+  public void testFetchContentSignedOwner() throws Exception {
+    oauth.httpResponse = CACHEABLE;
+    signedRequest.getOAuthArguments().setSignViewer(false);
+    HttpResponse httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals(CACHEABLE, httpResponse);
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+
+    // Invalidate by owner only
+    service.invalidateUserResources(ImmutableSet.of("OwnerX"), appxToken);
+
+    oauth.httpResponse = new HttpResponseBuilder(CACHEABLE).setResponseString("NEWCONTENT1").
+        create();
+    httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals("NEWCONTENT1", httpResponse.getResponseAsString());
+    assertEquals("o=1;", httpResponse.getHeader(DefaultInvalidationService.INVALIDATION_HEADER));
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+
+    // Invalidating viewer has no effect
+    service.invalidateUserResources(ImmutableSet.of("ViewerX"), appxToken);
+    oauth.httpResponse = new HttpResponseBuilder(CACHEABLE).setResponseString("NEWCONTENT2").
+        create();
+    httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals("NEWCONTENT1", httpResponse.getResponseAsString());
+    assertEquals("o=1;", httpResponse.getHeader(DefaultInvalidationService.INVALIDATION_HEADER));
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+  }
+
+  @Test
+  public void testFetchContentSignedViewer() throws Exception {
+    oauth.httpResponse = CACHEABLE;
+    signedRequest.getOAuthArguments().setSignOwner(false);
+    HttpResponse httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals(CACHEABLE, httpResponse);
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+
+    // Invalidate by owner has no effect
+    service.invalidateUserResources(ImmutableSet.of("OwnerX"), appxToken);
+    oauth.httpResponse = new HttpResponseBuilder(CACHEABLE).setResponseString("NEWCONTENT1").
+        create();
+    httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals(CACHEABLE, httpResponse);
+
+    // Invalidate the viewer
+    service.invalidateUserResources(ImmutableSet.of("ViewerX"), appxToken);
+    oauth.httpResponse = new HttpResponseBuilder(CACHEABLE).setResponseString("NEWCONTENT2").
+        create();
+    httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals("NEWCONTENT2", httpResponse.getResponseAsString());
+    assertEquals("v=2;", httpResponse.getHeader(DefaultInvalidationService.INVALIDATION_HEADER));
+    assertEquals(1, cacheProvider.createCache(DefaultHttpCache.CACHE_NAME).getSize());
+  }
+
+  @Test
+  public void testServeInvalidatedContentWithFetcherError() throws Exception {
+    oauth.httpResponse = CACHEABLE;
+    HttpResponse httpResponse = requestPipeline.execute(signedRequest);
+
+    // Invalidate by owner
+    service.invalidateUserResources(ImmutableSet.of("OwnerX"), appxToken);
+
+    // Next request returns error
+    oauth.httpResponse = HttpResponse.error();
+    httpResponse = requestPipeline.execute(signedRequest);
+    assertEquals(CACHEABLE, httpResponse);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java
new file mode 100644
index 0000000..40fbafe
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/DefaultRequestPipelineTest.java
@@ -0,0 +1,493 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import com.google.common.collect.Maps;
+import com.google.common.net.HttpHeaders;
+import com.google.inject.Provider;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.oauth.OAuthRequest;
+import org.apache.shindig.gadgets.oauth2.BasicOAuth2Request;
+import org.apache.shindig.gadgets.oauth2.OAuth2Request;
+import org.apache.shindig.gadgets.rewrite.DefaultResponseRewriterRegistry;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class DefaultRequestPipelineTest {
+  private static final Uri DEFAULT_URI = Uri.parse("http://example.org/gadget.xml");
+  private static final String RFC1123_EPOCH = DateUtil.formatRfc1123Date(0);
+
+  private final FakeHttpFetcher fetcher = new FakeHttpFetcher();
+  private final FakeHttpCache cache = new FakeHttpCache();
+  private final FakeOAuthRequestProvider oauth = new FakeOAuthRequestProvider();
+  private final FakeOAuth2RequestProvider oauth2 = new FakeOAuth2RequestProvider();
+
+  private final HttpResponseMetadataHelper helper = new HttpResponseMetadataHelper() {
+    @Override
+    public String getHash(HttpResponse resp) {
+      return resp.getResponseAsString();
+    }
+  };
+  private final RequestPipeline pipeline = new DefaultRequestPipeline(fetcher, cache, oauth,
+          oauth2, new DefaultResponseRewriterRegistry(null, null), new NoOpInvalidationService(),
+          helper);
+
+  @Before
+  public void setUp() {
+    HttpResponseTest.setHttpTimeSource();
+  }
+
+  @Test
+  public void authTypeNoneNotCached() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    fetcher.response = new HttpResponse("response");
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(request, fetcher.request);
+    assertEquals(fetcher.response, response);
+    assertEquals(response, cache.data.get(DEFAULT_URI));
+    assertEquals(1, cache.readCount);
+    assertEquals(1, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+    assertEquals(1, response.getMetadata().size());
+    assertEquals("response", response.getMetadata().get(HttpResponseMetadataHelper.DATA_HASH));
+  }
+
+  @Test
+  public void verifyHashCode() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    int time = roundToSeconds(HttpResponseTest.timeSource.currentTimeMillis()) - 10;
+    String date = DateUtil.formatRfc1123Date(1000L * time);
+    HttpResponseBuilder builder = new HttpResponseBuilder().setCacheTtl(100)
+            .addHeader("Date", date);
+    builder.setContent("response");
+
+    fetcher.response = builder.create();
+
+    RequestPipeline pipeline = new DefaultRequestPipeline(fetcher, cache, oauth, oauth2,
+            new DefaultResponseRewriterRegistry(null, null), new NoOpInvalidationService(),
+            new HttpResponseMetadataHelper());
+    HttpResponse response = pipeline.execute(request);
+    assertEquals(1, response.getMetadata().size());
+    assertEquals("q7u8tbpmidtu1gtqhjv0kb0rvo",
+            response.getMetadata().get(HttpResponseMetadataHelper.DATA_HASH));
+    assertEquals(date, response.getHeader("Date"));
+    assertEquals(roundToSeconds(90000 - 1), roundToSeconds(response.getCacheTtl() - 1));
+  }
+
+  @Test
+  public void verifyFixedDate() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    int time = roundToSeconds(HttpResponseTest.timeSource.currentTimeMillis());
+    String date = DateUtil.formatRfc1123Date(1000L * time - 1000
+            - DefaultRequestPipeline.DEFAULT_DRIFT_LIMIT_MS);
+    HttpResponseBuilder builder = new HttpResponseBuilder().setCacheTtl(100)
+            .addHeader("Date", date);
+    builder.setContent("response");
+
+    fetcher.response = builder.create();
+
+    RequestPipeline pipeline = new DefaultRequestPipeline(fetcher, cache, oauth, oauth2,
+            new DefaultResponseRewriterRegistry(null, null), new NoOpInvalidationService(),
+            new HttpResponseMetadataHelper());
+    HttpResponse response = pipeline.execute(request);
+    // Verify time is current time instead of expired
+    assertEquals(DateUtil.formatRfc1123Date(1000L * time), response.getHeader("Date"));
+    assertEquals(roundToSeconds(100000 - 1), roundToSeconds(response.getCacheTtl() - 1));
+  }
+
+  @Test
+  public void authTypeNoneWasCached() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponse("cached");
+    cache.data.put(DEFAULT_URI, cached);
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(cached, response);
+    assertEquals(1, cache.readCount);
+    assertEquals(0, cache.writeCount);
+    assertEquals(0, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneWasCachedButStale() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder().setStrictNoCache().create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    HttpResponse fetched = new HttpResponse("fetched");
+    fetcher.response = fetched;
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(fetched, response);
+    assertEquals(request, fetcher.request);
+    assertEquals(fetched, cache.data.get(DEFAULT_URI));
+    assertEquals(1, cache.readCount);
+    assertEquals(1, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+    assertEquals(1, response.getMetadata().size());
+    assertEquals("fetched", response.getMetadata().get(HttpResponseMetadataHelper.DATA_HASH));
+
+  }
+
+  @Test
+  public void authTypeNoneStaleCachedServed() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder().setCacheTtl(-1).create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    fetcher.response = HttpResponse.error();
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(cached, response); // cached item is served instead of 500
+    assertEquals(request, fetcher.request);
+    assertEquals(1, cache.readCount);
+    assertEquals(0, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneWasCachedErrorStale() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder().setCacheTtl(-1).setHttpStatusCode(401).create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    HttpResponse fetched = HttpResponse.error();
+    fetcher.response = fetched;
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(fetched, response); // 500 served because cached is an error (401)
+    assertEquals(request, fetcher.request);
+    assertEquals(fetched, cache.data.get(DEFAULT_URI));
+    assertEquals(1, cache.readCount);
+    assertEquals(1, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneIgnoreCache() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE).setIgnoreCache(
+            true);
+
+    HttpResponse fetched = new HttpResponse("fetched");
+    fetcher.response = fetched;
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(fetched, response);
+    assertEquals(request, fetcher.request);
+    assertEquals(0, cache.readCount);
+    assertEquals(0, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneStaleConditionalGet() throws Exception {
+    // Cached response that is stale.  Test that a conditional GET is used.
+    // Verify that the cached response is updated and returned.
+    // Verify that the 304 doesn't get cached.
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setHeader(HttpHeaders.LAST_MODIFIED, RFC1123_EPOCH)
+                                .setHeader(HttpHeaders.ETAG, "ETAG")
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    String expiresDate = DateUtil.formatRfc1123Date(System.currentTimeMillis() + 3600 * 1000);
+    String maxAge = "max-age=3600";
+    HttpResponse notModified = new HttpResponseBuilder()
+                                    .setHttpStatusCode(HttpResponse.SC_NOT_MODIFIED)
+                                    .setHeader(HttpHeaders.EXPIRES, expiresDate)
+                                    .setHeader(HttpHeaders.CACHE_CONTROL, maxAge)
+                                    .create();
+    fetcher.response = notModified;
+    HttpResponse response = pipeline.execute(request);
+
+    HttpResponse expectedResponse = new HttpResponseBuilder(cached)
+                                      .setHeader(HttpHeaders.EXPIRES, expiresDate)
+                                      .setHeader(HttpHeaders.CACHE_CONTROL, maxAge)
+                                      .create();
+
+    assertEquals(RFC1123_EPOCH, fetcher.request.getHeader(HttpHeaders.IF_MODIFIED_SINCE));
+    assertEquals("ETAG", fetcher.request.getHeader(HttpHeaders.IF_NONE_MATCH));
+    assertEquals(expectedResponse, response);
+    assertEquals(expectedResponse, cache.data.get(DEFAULT_URI));
+    assertEquals(1, cache.readCount);
+    assertEquals(1, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneStaleConditionalGetNoExpiresNoMaxAge() throws Exception {
+    // Cached response that is stale and a conditional GET is used. Response has no Expires
+    // header and no Cache-Control header with max-age. Remove the cached entry from the cache and
+    // return it.
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setHeader(HttpHeaders.LAST_MODIFIED, RFC1123_EPOCH)
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    HttpResponse notModified = new HttpResponseBuilder()
+                                    .setHttpStatusCode(HttpResponse.SC_NOT_MODIFIED)
+                                    .create();
+    fetcher.response = notModified;
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(RFC1123_EPOCH, fetcher.request.getHeader(HttpHeaders.IF_MODIFIED_SINCE));
+    assertEquals(cached, response);
+    assertEquals(null, cache.data.get(DEFAULT_URI));
+    assertEquals(1, cache.readCount);
+    assertEquals(0, cache.writeCount);
+    assertEquals(1, fetcher.fetchCount);
+  }
+
+  @Test
+  public void authTypeNoneStaleConditionalGetNoLastModified() throws Exception {
+    // Cached response is stale and has no Last-Modified header on it. Test that an
+    // If-Modified-Since header is not issued.
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    fetcher.response = HttpResponse.error(); // Really don't care what this is.
+    pipeline.execute(request);
+
+    assertEquals(null, fetcher.request.getHeader(HttpHeaders.IF_MODIFIED_SINCE));
+  }
+
+  @Test
+  public void authTypeNoneStaleConditionalGetNoEtag() throws Exception {
+    // Cached response is stale and has no Etag header on it. Test that an
+    // If-None-Match header is not issued.
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.NONE);
+
+    HttpResponse cached = new HttpResponseBuilder()
+                                .setCacheTtl(-1)
+                                .create();
+    cache.data.put(DEFAULT_URI, cached);
+
+    fetcher.response = HttpResponse.error(); // Really don't care what this is.
+    pipeline.execute(request);
+
+    assertEquals(null, fetcher.request.getHeader(HttpHeaders.IF_NONE_MATCH));
+  }
+
+  @Test
+  public void authTypeOAuthNotCached() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.OAUTH);
+
+    oauth.httpResponse = new HttpResponse("oauth result");
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(oauth.httpResponse, response);
+    assertEquals(request, oauth.httpRequest);
+    assertEquals(response, cache.data.get(DEFAULT_URI));
+    assertEquals(1, oauth.fetchCount);
+    assertEquals(0, fetcher.fetchCount);
+    assertEquals(1, cache.readCount);
+    assertEquals(1, cache.writeCount);
+  }
+
+  @Test
+  public void authTypeOAuthWasCached() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setAuthType(AuthType.OAUTH);
+
+    HttpResponse cached = new HttpResponse("cached");
+    cache.data.put(DEFAULT_URI, cached);
+
+    HttpResponse response = pipeline.execute(request);
+
+    assertEquals(cached, response);
+    assertEquals(0, oauth.fetchCount);
+    assertEquals(0, fetcher.fetchCount);
+    assertEquals(1, cache.readCount);
+    assertEquals(0, cache.writeCount);
+  }
+
+  private static int roundToSeconds(long ts) {
+    return (int) (ts / 1000);
+  }
+
+  @Test
+  public void testFixedDateOk() throws Exception {
+    int time = roundToSeconds(HttpResponseTest.timeSource.currentTimeMillis());
+    HttpResponse response = new HttpResponseBuilder()
+            .addHeader(
+                    "Date",
+                    DateUtil.formatRfc1123Date(1000L * time + 1000
+                            - DefaultRequestPipeline.DEFAULT_DRIFT_LIMIT_MS)).setCacheTtl(100)
+            .create();
+
+    HttpResponse newResponse = DefaultRequestPipeline.maybeFixDriftTime(response);
+    assertSame(response, newResponse);
+  }
+
+  @Test
+  public void testFixedDateOld() throws Exception {
+    int time = roundToSeconds(HttpResponseTest.timeSource.currentTimeMillis());
+    HttpResponse response = new HttpResponseBuilder()
+            .addHeader(
+                    "Date",
+                    DateUtil.formatRfc1123Date(1000L * time - 1000
+                            - DefaultRequestPipeline.DEFAULT_DRIFT_LIMIT_MS)).setCacheTtl(100)
+            .create();
+
+    response = DefaultRequestPipeline.maybeFixDriftTime(response);
+    // Verify that the old time is ignored:
+    assertEquals(time + 100, roundToSeconds(response.getCacheExpiration()));
+    assertEquals(DateUtil.formatRfc1123Date(HttpResponseTest.timeSource.currentTimeMillis()),
+            response.getHeader("Date"));
+  }
+
+  @Test
+  public void testFixedDateNew() throws Exception {
+    int time = roundToSeconds(HttpResponseTest.timeSource.currentTimeMillis());
+    HttpResponse response = new HttpResponseBuilder()
+            .addHeader(
+                    "Date",
+                    DateUtil.formatRfc1123Date(1000L * time + 1000
+                            + DefaultRequestPipeline.DEFAULT_DRIFT_LIMIT_MS)).setCacheTtl(100)
+            .create();
+
+    response = DefaultRequestPipeline.maybeFixDriftTime(response);
+    // Verify that the old time is ignored:
+    assertEquals(time + 100, roundToSeconds(response.getCacheExpiration()));
+    assertEquals(DateUtil.formatRfc1123Date(HttpResponseTest.timeSource.currentTimeMillis()),
+            response.getHeader("Date"));
+  }
+
+  public static class FakeHttpFetcher implements HttpFetcher {
+    protected HttpRequest request;
+    protected HttpResponse response;
+    protected int fetchCount = 0;
+
+    protected FakeHttpFetcher() {}
+
+    public HttpResponse fetch(HttpRequest request) throws GadgetException {
+      fetchCount++;
+      this.request = request;
+      if (response == null) {
+        throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT);
+      }
+      return response;
+    }
+  }
+
+  public static class FakeHttpCache implements HttpCache {
+    protected final Map<Uri, HttpResponse> data = Maps.newHashMap();
+    protected int writeCount = 0;
+    protected int readCount = 0;
+
+    protected FakeHttpCache() {}
+
+    public HttpResponse addResponse(HttpRequest request, HttpResponse response) {
+      writeCount++;
+      data.put(request.getUri(), response);
+      return response;
+    }
+
+    public HttpResponse getResponse(HttpRequest request) {
+      readCount++;
+      return data.get(request.getUri());
+    }
+
+    public HttpResponse removeResponse(HttpRequest key) {
+      return data.remove(key.getUri());
+    }
+
+    public String createKey(HttpRequest request) {
+      return request.getUri().getQuery();
+    }
+  }
+
+  public static class FakeOAuthRequestProvider implements Provider<OAuthRequest> {
+    protected int fetchCount = 0;
+    protected HttpRequest httpRequest;
+    protected HttpResponse httpResponse;
+
+    protected FakeOAuthRequestProvider() {}
+
+    private final OAuthRequest oauthRequest = new OAuthRequest(null, null) {
+      @Override
+      public HttpResponse fetch(HttpRequest request) {
+        fetchCount++;
+        httpRequest = request;
+        return httpResponse;
+      }
+    };
+
+    public OAuthRequest get() {
+      return oauthRequest;
+    }
+
+  }
+
+  public static class FakeOAuth2RequestProvider implements Provider<OAuth2Request> {
+    protected int fetchCount = 0;
+    protected HttpRequest httpRequest;
+    protected HttpResponse httpResponse;
+
+    protected FakeOAuth2RequestProvider() {}
+
+    private final OAuth2Request oauth2Request = new BasicOAuth2Request(null, null, null, null,
+            null, null, null, false, null) {
+      @Override
+      public HttpResponse fetch(HttpRequest request) {
+        fetchCount++;
+        httpRequest = request;
+        return httpResponse;
+      }
+    };
+
+    public OAuth2Request get() {
+      return oauth2Request;
+    }
+
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/EchoServer.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/EchoServer.java
new file mode 100644
index 0000000..bcf26a7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/EchoServer.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.commons.lang3.StringUtils;
+import org.mortbay.jetty.servlet.ServletHolder;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A server that echoes back whatever you send to it.
+ */
+public class EchoServer extends FakeHttpServer {
+
+  public static final String STATUS_PARAM = "status";
+  public static final String BODY_PARAM = "body";
+  public static final String HEADER_PARAM = "header";
+
+  @Override
+  protected void addServlets() throws Exception {
+    ServletHolder servletHolder = new ServletHolder(new EchoServlet());
+    context.addServlet(servletHolder, "/*");
+  }
+
+  private static class EchoServlet extends HttpServlet {
+
+    protected EchoServlet() {
+    }
+
+    @Override
+    protected void service(HttpServletRequest req, HttpServletResponse resp)
+        throws ServletException, IOException {
+      handleEcho(req, resp);
+    }
+
+    private void handleEcho(HttpServletRequest req, HttpServletResponse resp)
+        throws IOException {
+      int code = HttpServletResponse.SC_OK;
+      if (req.getParameter(STATUS_PARAM) != null) {
+        code = Integer.parseInt(req.getParameter(STATUS_PARAM));
+      }
+      resp.setStatus(code);
+
+      String[] headers = req.getParameterValues(HEADER_PARAM);
+      if (headers != null) {
+        for (String header : headers) {
+          String[] nameAndValue = StringUtils.splitPreserveAllTokens(header, "=", 2);
+          resp.setHeader(nameAndValue[0], nameAndValue[1]);
+        }
+      }
+
+      resp.setHeader("X-Method", req.getMethod());
+
+      String body = req.getParameter(BODY_PARAM);
+      if (body != null) {
+        resp.getWriter().print(body);
+      } else {
+        resp.setHeader("Content-Type", "application/octet-stream");
+
+        // Read the input stream into memory
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        InputStream is = req.getInputStream();
+        byte[] buf = new byte[1024];
+        int len;
+        while ((len = is.read(buf)) > 0) {
+          baos.write(buf, 0, len);
+        }
+
+        // Echo the bytes back to the output stream
+        OutputStream os = resp.getOutputStream();
+        ByteArrayInputStream bais = new ByteArrayInputStream(
+            baos.toByteArray());
+        while ((len = bais.read(buf)) > 0) {
+          os.write(buf, 0, len);
+        }
+      }
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/FakeHttpServer.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/FakeHttpServer.java
new file mode 100644
index 0000000..4b67843
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/FakeHttpServer.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.servlet.Context;
+
+/**
+ * A simple HTTP server to test against.
+ */
+public abstract class FakeHttpServer {
+  protected Server server = null;
+  protected Context context = null;
+
+  public void start(int port) throws Exception {
+    server = new Server(port);
+    context = new Context(server, "/", Context.SESSIONS);
+    addServlets();
+    server.start();
+  }
+
+  /** Override to add your servlets */
+  protected abstract void addServlets() throws Exception;
+
+  public void stop() throws Exception {
+    server.stop();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpRequestTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpRequestTest.java
new file mode 100644
index 0000000..615a9c0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpRequestTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class HttpRequestTest {
+  private static final String POST_BODY = "Hello, world!";
+  private static final String CONTENT_TYPE = "text/plain";
+  private static final String TEST_HEADER_KEY = "X-Test-Header";
+  private static final String TEST_HEADER_VALUE = "Hello!";
+  private static final String TEST_HEADER_VALUE2 = "Goodbye.";
+  private static final Uri DEFAULT_URI = Uri.parse("http://example.org/");
+
+  @Test
+  public void dosPreventionHeaderAdded() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    assertNotNull("DoS prevention header not present in request.",
+        request.getHeader(HttpRequest.DOS_PREVENTION_HEADER));
+  }
+
+  @Test
+  public void postBodyCopied() throws Exception {
+    HttpRequest request  = new HttpRequest(DEFAULT_URI).setPostBody(POST_BODY.getBytes());
+    assertEquals(POST_BODY.length(), request.getPostBodyLength());
+    assertEquals(POST_BODY, IOUtils.toString(request.getPostBody(), "UTF-8"));
+    assertEquals(POST_BODY, request.getPostBodyAsString());
+  }
+
+  @Test
+  public void contentTypeExtraction() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .addHeader("Content-Type", CONTENT_TYPE);
+    assertEquals(CONTENT_TYPE, request.getContentType());
+  }
+
+  @Test
+  public void getHeader() throws Exception {
+    Map<String, List<String>> headers = Maps.newHashMap();
+    headers.put(TEST_HEADER_KEY, Arrays.asList(TEST_HEADER_VALUE));
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .addHeader(TEST_HEADER_KEY, TEST_HEADER_VALUE);
+    assertEquals(TEST_HEADER_VALUE, request.getHeader(TEST_HEADER_KEY));
+  }
+
+  @Test
+  public void getHeaders() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .addHeader(TEST_HEADER_KEY, TEST_HEADER_VALUE)
+        .addHeader(TEST_HEADER_KEY, TEST_HEADER_VALUE2);
+
+    Collection<String> expected = Arrays.asList(TEST_HEADER_VALUE, TEST_HEADER_VALUE2);
+    assertTrue(request.getHeaders(TEST_HEADER_KEY).containsAll(expected));
+  }
+
+  @Test
+  public void ignoreCacheAddsPragmaHeader() throws Exception {
+    HttpRequest request = new HttpRequest(DEFAULT_URI).setIgnoreCache(true);
+
+    assertTrue("Pragma: no-cache not added when ignoreCache == true",
+        request.getHeaders("Pragma").contains("no-cache"));
+  }
+
+  @Test
+  public void testDefaultIsFollowRedirects() {
+    HttpRequest request = new HttpRequest(DEFAULT_URI);
+    assertTrue(request.getFollowRedirects());
+  }
+
+  @Test
+  public void copyCtorCopiesAllFields() {
+    OAuthArguments oauthArguments = new OAuthArguments();
+    oauthArguments.setSignOwner(false);
+    oauthArguments.setSignViewer(true);
+    HttpRequest request = new HttpRequest(DEFAULT_URI)
+        .setCacheTtl(100)
+        .addHeader(TEST_HEADER_KEY, TEST_HEADER_VALUE)
+        .setContainer("container")
+        .setGadget(DEFAULT_URI)
+        .setMethod("POST")
+        .setPostBody(POST_BODY.getBytes())
+        .setRewriteMimeType("text/fake")
+        .setSecurityToken(new AnonymousSecurityToken())
+        .setOAuthArguments(oauthArguments)
+        .setAuthType(AuthType.OAUTH)
+        .setFollowRedirects(false)
+        .setInternalRequest(true);
+
+    HttpRequest request2 = new HttpRequest(request).setUri(Uri.parse("http://example.org/foo"));
+
+    assertEquals(request.getCacheTtl(), request2.getCacheTtl());
+    assertEquals(request.getHeaders(), request2.getHeaders());
+    assertEquals(request.getContainer(), request2.getContainer());
+    assertEquals(request.getGadget(), request2.getGadget());
+    assertEquals(request.getMethod(), request2.getMethod());
+    assertEquals(request.getPostBodyAsString(), request2.getPostBodyAsString());
+    assertEquals(request.getRewriteMimeType(), request2.getRewriteMimeType());
+    assertEquals(request.getSecurityToken(), request2.getSecurityToken());
+    assertEquals(request.getOAuthArguments().getSignOwner(),
+        request2.getOAuthArguments().getSignOwner());
+    assertEquals(request.getOAuthArguments().getSignViewer(),
+        request2.getOAuthArguments().getSignViewer());
+    assertEquals(AuthType.OAUTH, request.getAuthType());
+    assertFalse(request.getFollowRedirects());
+    assertTrue(request.isInternalRequest());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseBuilderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseBuilderTest.java
new file mode 100644
index 0000000..a46acb8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseBuilderTest.java
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Tests for HttpResponseBuilder.
+ *
+ * This test case compliments HttpResponseTest; not all tests are duplicated here.
+ */
+public class HttpResponseBuilderTest {
+
+  @Test
+  public void copyConstructor() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .setHttpStatusCode(HttpResponse.SC_NOT_FOUND)
+        .setMetadata("foo", "bar")
+        .addHeader("Foo-bar", "baz");
+
+    HttpResponseBuilder builder2 = new HttpResponseBuilder(builder);
+    assertEquals(builder.create(), builder2.create());
+  }
+
+  @Test
+  public void addHeader() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Foo-bar", "baz");
+
+    assertEquals("baz", builder.getHeaders().get("Foo-bar").iterator().next());
+  }
+
+  @Test
+  public void addHeadersMap() {
+    Map<String, String> headers = ImmutableMap.of("foo", "bar", "blah", "blah");
+
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeaders(headers);
+
+    assertEquals(Arrays.asList("bar"), Lists.newArrayList(builder.getHeaders().get("foo")));
+    assertEquals(Arrays.asList("blah"), Lists.newArrayList(builder.getHeaders().get("blah")));
+  }
+
+  @Test
+  public void addAllHeaders() {
+    Map<String, List<String>> headers = Maps.newHashMap();
+
+    List<String> foo = Lists.newArrayList("bar", "blah");
+    List<String> bar = Lists.newArrayList("baz");
+    headers.put("foo", foo);
+    headers.put("bar", bar);
+
+
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addAllHeaders(headers);
+
+    assertTrue(builder.getHeaders().get("foo").containsAll(foo));
+    assertTrue(builder.getHeaders().get("bar").containsAll(bar));
+  }
+
+  @Test
+  public void setExpirationTime() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Pragma", "no-cache")
+        .addHeader("Cache-Control", "public,max-age=100")
+        .setExpirationTime(100);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertTrue("No Expires header added.", headers.containsKey("Expires"));
+    assertFalse("Pragma header not removed", headers.containsKey("Pragma"));
+    assertFalse("Cache-Control header not removed", headers.containsKey("Cache-Control"));
+  }
+
+  @Test
+  public void setCacheControlMaxAge() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "public,max-age=100")
+        .setCacheControlMaxAge(12345);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertEquals("public,max-age=12345", headers.get("Cache-Control").iterator().next());
+  }
+
+  @Test
+  public void setCacheControlMaxAgeWithSpacesInCacheControlHeader() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "public, max-age=123, no-transform ")
+        .setCacheControlMaxAge(12345);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertEquals("public,no-transform,max-age=12345",
+                 headers.get("Cache-Control").iterator().next());
+  }
+
+  @Test
+  public void setCacheControlMaxAgeWithBadMaxAgeFormat() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "public, max-age=12=ab")
+        .setCacheControlMaxAge(12345);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertEquals("public,max-age=12=ab,max-age=12345",
+                 headers.get("Cache-Control").iterator().next());
+  }
+
+  @Test
+  public void setCacheControlMaxAgeWithNoInitialMaxAge() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "private")
+        .setCacheControlMaxAge(10000);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertEquals("private,max-age=10000", headers.get("Cache-Control").iterator().next());
+  }
+
+  @Test
+  public void setCacheControlMaxAgeWithNoCacheControlHeader() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .setCacheControlMaxAge(86400);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertEquals("max-age=86400", headers.get("Cache-Control").iterator().next());
+  }
+
+  @Test
+  public void setCacheTtl() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Pragma", "no-cache")
+        .addHeader("Expires", "some time stamp normally goes here")
+        .addHeader("Cache-Control", "no-cache")
+        .setCacheTtl(100);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertFalse("Expires header not removed.", headers.containsKey("Expires"));
+    assertFalse("Pragma header not removed", headers.containsKey("Pragma"));
+    assertEquals("public,max-age=100", headers.get("Cache-Control").iterator().next());
+  }
+
+  @Test
+  public void setStrictNoCache() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "public,max-age=100")
+        .addHeader("Expires", "some time stamp normally goes here")
+        .setStrictNoCache();
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertFalse("Expires header not removed.", headers.containsKey("Expires"));
+    assertEquals("no-cache", headers.get("Cache-Control").iterator().next());
+    assertEquals("no-cache", headers.get("Pragma").iterator().next());
+  }
+
+  @Test
+  public void setEncoding() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/html; charset=Big5")
+        .setEncoding(Charsets.UTF_8);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertEquals("text/html; charset=UTF-8", headers.get("Content-Type").iterator().next());
+  }
+
+  @Test
+  public void setEncodingEmpty() {
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/html")
+        .setEncoding(Charsets.UTF_8);
+
+    Multimap<String, String> headers = builder.getHeaders();
+    assertEquals("text/html; charset=UTF-8", headers.get("Content-Type").iterator().next());
+  }
+
+  @Test
+  public void setResponseString() {
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponseString("foo")
+        .create();
+    assertEquals("foo", resp.getResponseAsString());
+  }
+
+  @Test
+  public void setResponseStringWithContentType() {
+    HttpResponse resp = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/html")
+        .setResponseString("foo")
+        .create();
+    Multimap<String, String> headers = resp.getHeaders();
+    assertEquals("text/html; charset=UTF-8", headers.get("Content-Type").iterator().next());
+    assertEquals("foo", resp.getResponseAsString());
+  }
+
+  @Test
+  public void setResponse() {
+    byte[] someData = "some data".getBytes();
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(someData)
+        .create();
+
+    assertNotSame(someData, resp.getResponseAsBytes());
+    assertArrayEquals(someData, resp.getResponseAsBytes());
+  }
+
+  @Test
+  public void setResponseNoCopy() {
+    byte[] someData = "some data".getBytes();
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponseNoCopy(someData)
+        .create();
+
+    assertSame(someData, resp.getResponseAsBytes());
+  }
+
+  @Test
+  public void headerOrdering() {
+    ImmutableList<String> soupList = ImmutableList.of("Tomato", "Potato", "Lentil", "Onion");
+    HttpResponseBuilder b = new HttpResponseBuilder();
+    for (String soup : soupList) {
+      b.addHeader("Soup", soup);
+    }
+    HttpResponse resp = b.create();
+
+    // Insure that headers are stored in the order they are added
+    assertEquals(Joiner.on(",").join(resp.getHeaders("Soup")), Joiner.on(",").join(soupList));
+  }
+
+  @Test
+  public void noModsReturnsSameResponse() {
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+    builder.setHttpStatusCode(HttpResponse.SC_BAD_GATEWAY);
+    builder.setResponseString("foo");
+    HttpResponse response = builder.create();
+    assertSame(response, builder.create());
+  }
+
+  @Test
+  public void noModsReturnsSameResponseBuilderCtor() {
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+    builder.setHttpStatusCode(HttpResponse.SC_OK);
+    HttpResponseBuilder nextBuilder = new HttpResponseBuilder(builder);
+    assertSame(builder.create(), nextBuilder.create());
+  }
+
+  @Test
+  public void noModsReturnsSameResponseBaseCtor() {
+    HttpResponse response = new HttpResponse("foo");
+    HttpResponseBuilder builder = new HttpResponseBuilder(response);
+    assertSame(response, builder.create());
+    builder.setHttpStatusCode(HttpResponse.SC_BAD_GATEWAY);
+    HttpResponse newResponse = builder.create();
+    assertNotSame(response, newResponse);
+    assertSame(newResponse, builder.create());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseMetadataHelperTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseMetadataHelperTest.java
new file mode 100644
index 0000000..ba7d12a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseMetadataHelperTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+
+public class HttpResponseMetadataHelperTest {
+
+  @Test
+  public void testUpdateMetadata() {
+    HttpResponse local = new HttpResponseBuilder()
+        .setResponseString("data1")
+        .create();
+
+    HttpResponse compiled = HttpResponseMetadataHelper.updateMetadata(local,
+        ImmutableMap.<String, String>of("K", "V"));
+    assertEquals(1, compiled.getMetadata().size());
+    assertEquals("V", compiled.getMetadata().get("K"));
+
+    HttpResponse compiled2 = HttpResponseMetadataHelper.updateMetadata(compiled,
+        ImmutableMap.<String, String>of("K2", "V2"));
+    assertEquals(2, compiled2.getMetadata().size());
+    assertEquals("V2", compiled2.getMetadata().get("K2"));
+
+    HttpResponse compiled3 = HttpResponseMetadataHelper.updateMetadata(compiled2,
+        ImmutableMap.<String, String>of("K", "V3"));
+    assertEquals(2, compiled3.getMetadata().size());
+    assertEquals("V3", compiled3.getMetadata().get("K"));
+  }
+
+  @Test
+  public void testHashCodeSimple() {
+    HttpResponse local = new HttpResponseBuilder()
+        .setResponseString("data1")
+        .create();
+    verifyHash(local, 1, "h7cg7f1lrrf74jul5h8k6vvlvk");
+  }
+
+  @Test
+  public void testHashCodeExtraMeta() {
+    HttpResponse local = new HttpResponseBuilder()
+        .setResponseString("data1")
+        .setMetadata(ImmutableMap.<String, String>of("K","V"))
+        .setHeader("X-data", "no data")
+        .create();
+    verifyHash(local, 2, "h7cg7f1lrrf74jul5h8k6vvlvk");
+  }
+
+  @Test
+  public void testHashCodeError() {
+    verifyHash(HttpResponse.error(), 1, "qgeopmcf02p09qc016cepu22fo");
+  }
+
+  @Test
+  public void testHashCodeEmpty() {
+    HttpResponse local = new HttpResponseBuilder()
+        .setHttpStatusCode(200)
+        .create();
+    verifyHash(local, 1, "qgeopmcf02p09qc016cepu22fo");
+  }
+
+  private void verifyHash(HttpResponse resp, int metadataSize, String hash) {
+    HttpResponseMetadataHelper metdataHelper = new HttpResponseMetadataHelper();
+    HttpResponse compiled = HttpResponseMetadataHelper.updateHash(resp, metdataHelper);
+    assertEquals(metadataSize, compiled.getMetadata().size());
+    assertEquals(hash, compiled.getMetadata().get(HttpResponseMetadataHelper.DATA_HASH));
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseTest.java
new file mode 100644
index 0000000..d854d80
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/HttpResponseTest.java
@@ -0,0 +1,631 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.Arrays;
+
+public class HttpResponseTest extends Assert {
+  private static final byte[] UTF8_DATA = {
+    (byte)0xEF, (byte)0xBB, (byte)0xBF, 'h', 'e', 'l', 'l', 'o'
+  };
+  private static final String UTF8_STRING = "hello";
+
+  // A large string is needed for accurate charset detection.
+  private static final byte[] LATIN1_DATA = {
+    'G', 'a', 'm', 'e', 's', ',', ' ', 'H', 'Q', ',', ' ', 'M', 'a', 'n', 'g', (byte)0xE1, ',', ' ',
+    'A', 'n', 'i', 'm', 'e', ' ', 'e', ' ', 't', 'u', 'd', 'o', ' ', 'q', 'u', 'e', ' ', 'u', 'm',
+    ' ', 'b', 'o', 'm', ' ', 'n', 'e', 'r', 'd', ' ', 'a', 'm', 'a'
+  };
+  private static final String LATIN1_STRING
+      = "Games, HQ, Mang\u00E1, Anime e tudo que um bom nerd ama";
+
+  private static final byte[] BIG5_DATA = {
+    (byte)0xa7, (byte)0x41, (byte)0xa6, (byte)0x6e
+  };
+
+  private static final String BIG5_STRING = "\u4F60\u597D";
+
+  private static int roundToSeconds(long ts) {
+    return (int)(ts / 1000);
+  }
+
+  public static FakeTimeSource timeSource = new FakeTimeSource(System.currentTimeMillis());
+  public static void setHttpTimeSource() {
+    HttpResponse.setTimeSource(timeSource);
+  }
+  @Before
+  public void setUp() {
+    setHttpTimeSource();
+  }
+
+  @Test
+  public void testEncodingDetectionUtf8WithBom() throws Exception {
+     HttpResponse response = new HttpResponseBuilder()
+         .addHeader("Content-Type", "text/plain; charset=UTF-8")
+         .setResponse(UTF8_DATA)
+         .create();
+    assertEquals(UTF8_STRING, response.getResponseAsString());
+    assertEquals("UTF-8", response.getEncoding());
+  }
+
+  @Test
+  public void testEncodingDetectionUtf8WithBomCaseInsensitiveKey() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/plain; Charset=utf-8")
+        // Legitimate data, should be ignored in favor of explicit charset.
+        .setResponse(LATIN1_DATA)
+        .create();
+    assertEquals("UTF-8", response.getEncoding());
+  }
+
+  @Test
+  public void testEncodingDetectionLatin1() throws Exception {
+    // Input is a basic latin-1 string with 1 non-UTF8 compatible char.
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/plain; charset=iso-8859-1")
+        .setResponse(LATIN1_DATA)
+        .create();
+    assertEquals(LATIN1_STRING, response.getResponseAsString());
+  }
+
+  @Test
+  public void testEncodingDetectionLatin1withIncorrectCharset() throws Exception {
+    // Input is a basic latin-1 string with 1 non-UTF8 compatible char.
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/plain; charset=iso-88859-1")
+        .setResponse(LATIN1_DATA)
+        .create();
+    assertEquals(LATIN1_STRING, response.getResponseAsString());
+    assertEquals("ISO-8859-1", response.getEncoding());
+  }
+
+  @Test
+  public void testEncodingDetectionUtf8WithBomAndIncorrectCharset() throws Exception {
+     HttpResponse response = new HttpResponseBuilder()
+         .addHeader("Content-Type", "text/plain; charset=UTTFF-88")
+         .setResponse(UTF8_DATA)
+         .create();
+    assertEquals(UTF8_STRING, response.getResponseAsString());
+    assertEquals("UTF-8", response.getEncoding());
+  }
+
+  @Test
+  public void testEncodingDetectionUtf8WithBomAndInvalidCharset() throws Exception {
+     HttpResponse response = new HttpResponseBuilder()
+         // Use a charset that will generate an IllegalCharsetNameException
+         .addHeader("Content-Type", "text/plain; charset=.UTF-8")
+         .setResponse(UTF8_DATA)
+         .create();
+    assertEquals(UTF8_STRING, response.getResponseAsString());
+    assertEquals("UTF-8", response.getEncoding());
+  }
+
+  @Test
+  public void testEncodingDetectionBig5() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/plain; charset=BIG5")
+        .setResponse(BIG5_DATA)
+        .create();
+    assertEquals(BIG5_STRING, response.getResponseAsString());
+    assertEquals("text/plain; charset=BIG5", response.getHeader("Content-Type"));
+
+    HttpResponseBuilder subResponseBuilder = new HttpResponseBuilder(response);
+    subResponseBuilder.setContent(response.getResponseAsString());
+    HttpResponse subResponse = subResponseBuilder.create();
+    // Same string.
+    assertEquals("text/plain; charset=UTF-8", subResponse.getHeader("Content-Type"));
+    assertEquals(BIG5_STRING, subResponse.getResponseAsString());
+    // New encoding.
+  }
+
+  @Test
+  public void testEncodingDetectionBig5WithQuotes() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/plain; charset=\"BIG5\"")
+        .setResponse(BIG5_DATA)
+        .create();
+    assertEquals(BIG5_STRING, response.getResponseAsString());
+  }
+
+  @Test
+  public void testEncodingDetectionUtf8WithBomNoCharsetSpecified() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/plain")
+        .setResponse(UTF8_DATA)
+        .create();
+    assertEquals("UTF-8", response.getEncoding().toUpperCase());
+    assertEquals(UTF8_STRING, response.getResponseAsString());
+  }
+
+  @Test
+  public void testEncodingDetectionLatin1NoCharsetSpecified() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/plain;")
+        .setResponse(LATIN1_DATA)
+        .create();
+    assertEquals("ISO-8859-1", response.getEncoding().toUpperCase());
+    assertEquals(LATIN1_STRING, response.getResponseAsString());
+  }
+
+  @Test
+  public void testEncodingDetectionWithEmptyContentType() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "")
+        .setResponseString("something")
+        .create();
+    assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding());
+  }
+
+  @Test
+  public void testEncodingDetectionUtf8WithBomNoContentHeader() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponse(UTF8_DATA)
+        .create();
+    assertEquals("UTF-8", response.getEncoding().toUpperCase());
+    assertEquals(UTF8_STRING, response.getResponseAsString());
+  }
+
+  @Test
+  public void testEncodingDetectionLatin1NoContentHeader() throws Exception {
+     HttpResponse response = new HttpResponseBuilder()
+        .setResponse(LATIN1_DATA)
+        .create();
+    assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding());
+  }
+
+  @Test
+  public void testGetEncodingForImageContentType() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponse(LATIN1_DATA)
+        .addHeader("Content-Type", "image/png; charset=iso-8859-1")
+        .create();
+    assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding().toUpperCase());
+  }
+
+  @Test
+  public void testGetEncodingForFlashContentType() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponse(LATIN1_DATA)
+        .addHeader("Content-Type", "application/x-shockwave-flash; charset=iso-8859-1")
+        .create();
+    assertEquals(HttpResponse.DEFAULT_ENCODING.name(), response.getEncoding().toUpperCase());
+  }
+
+  @Test
+  public void testPreserveBinaryData() throws Exception {
+    byte[] data = {
+        (byte)0x00, (byte)0xDE, (byte)0xEA, (byte)0xDB, (byte)0xEE, (byte)0xF0
+    };
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Content-Type", "application/octet-stream")
+        .setResponse(data)
+        .create();
+
+    byte[] out = IOUtils.toByteArray(response.getResponse());
+    assertEquals(data.length, response.getContentLength());
+    assertTrue(Arrays.equals(data, out));
+
+    out = IOUtils.toByteArray(response.getResponse());
+    assertTrue(Arrays.equals(data, out));
+  }
+
+  @Test
+  public void testStrictCacheControlNoCache() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "no-cache")
+        .create();
+    assertTrue(response.isStrictNoCache());
+    assertEquals(-1, response.getCacheExpiration());
+    assertEquals(-1, response.getCacheTtl());
+  }
+
+  @Test
+  public void testStrictPragmaNoCache() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Pragma", "no-cache")
+        .create();
+    assertTrue(response.isStrictNoCache());
+    assertEquals(-1, response.getCacheExpiration());
+    assertEquals(-1, response.getCacheTtl());
+  }
+
+  @Test
+  public void testStrictPragmaJunk() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Pragma", "junk")
+        .create();
+    assertFalse(response.isStrictNoCache());
+    int expected = roundToSeconds(timeSource.currentTimeMillis() + response.getDefaultTtl());
+    int expires = roundToSeconds(response.getCacheExpiration());
+    assertEquals(expected, expires);
+    assertTrue(response.getCacheTtl() <= response.getDefaultTtl() && response.getCacheTtl() > 0);
+  }
+
+  @Test
+  public void testCachingHeadersIgnoredOnError() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "no-cache")
+        .setHttpStatusCode(404)
+        .create();
+    assertFalse(response.isStrictNoCache());
+
+    response = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "no-cache")
+        .setHttpStatusCode(403)
+        .create();
+    assertTrue(response.isStrictNoCache());
+
+    response = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "no-cache")
+        .setHttpStatusCode(401)
+        .create();
+    assertTrue(response.isStrictNoCache());
+  }
+
+  /**
+   * Verifies that the cache TTL is within acceptable ranges.
+   * This always rounds down due to timing, so actual verification will be against maxAge - 1.
+   */
+  private static void assertTtlOk(int maxAge, HttpResponse response) {
+    assertEquals(maxAge - 1, roundToSeconds(response.getCacheTtl() - 1));
+  }
+
+  @Test
+  public void testExpires() throws Exception {
+    int maxAge = 10;
+    int time = roundToSeconds(timeSource.currentTimeMillis()) + maxAge;
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Expires", DateUtil.formatRfc1123Date(1000L * time))
+        .create();
+    assertEquals(time, roundToSeconds(response.getCacheExpiration()));
+    // Second rounding makes this n-1.
+    assertTtlOk(maxAge, response);
+  }
+
+  @Test
+  public void testExpiresZeroValue() throws Exception {
+    HttpResponse response = new HttpResponseBuilder().addHeader("Expires", "0").create();
+    assertEquals(0, roundToSeconds(response.getCacheExpiration()));
+  }
+
+  @Test
+  public void testExpiresUnknownValue() throws Exception {
+    HttpResponse response = new HttpResponseBuilder().addHeader("Expires", "howdy").create();
+    assertEquals(0, roundToSeconds(response.getCacheExpiration()));
+  }
+
+  @Test
+  public void testMaxAgeNoDate() throws Exception {
+    int maxAge = 10;
+    // Guess time.
+    int expected = roundToSeconds(timeSource.currentTimeMillis()) + maxAge;
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Cache-Control", "public, max-age=" + maxAge)
+        .create();
+    int expiration = roundToSeconds(response.getCacheExpiration());
+
+    assertEquals(expected, expiration);
+    assertTtlOk(maxAge, response);
+  }
+
+  @Test
+  public void testMaxAgeInvalidDate() throws Exception {
+    int maxAge = 10;
+    // Guess time.
+    int expected = roundToSeconds(timeSource.currentTimeMillis()) + maxAge;
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Date", "Wed, 09 Jul 2008 19:18:33 EDT")
+        .addHeader("Cache-Control", "public, max-age=" + maxAge)
+        .create();
+    int expiration = roundToSeconds(response.getCacheExpiration());
+
+    assertEquals(expected, expiration);
+    assertTtlOk(maxAge, response);
+  }
+
+  @Test
+  public void testMaxAgeWithDate() throws Exception {
+    int maxAge = 10;
+    int now = roundToSeconds(timeSource.currentTimeMillis());
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Date", DateUtil.formatRfc1123Date(1000L * now))
+        .addHeader("Cache-Control", "public, max-age=" + maxAge)
+        .create();
+
+    assertEquals(now + maxAge, roundToSeconds(response.getCacheExpiration()));
+    assertTtlOk(maxAge, response);
+  }
+
+  @Test
+  public void testFixedDate() throws Exception {
+    int time = roundToSeconds(timeSource.currentTimeMillis());
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Date", DateUtil.formatRfc1123Date(1000L * time))
+        .create();
+    assertEquals(time + roundToSeconds(response.getDefaultTtl()),
+        roundToSeconds(response.getCacheExpiration()));
+    assertEquals(DateUtil.formatRfc1123Date(timeSource.currentTimeMillis()),
+        response.getHeader("Date"));
+    assertTtlOk(roundToSeconds(response.getDefaultTtl()), response);
+  }
+
+  @Test
+  public void testNegativeCaching() {
+    assertTrue("Bad HTTP responses must be cacheable!",
+        HttpResponse.error().getCacheExpiration() > timeSource.currentTimeMillis());
+    assertTrue("Bad HTTP responses must be cacheable!",
+        HttpResponse.notFound().getCacheExpiration() > timeSource.currentTimeMillis());
+    assertTrue("Bad HTTP responses must be cacheable!",
+        HttpResponse.timeout().getCacheExpiration() > timeSource.currentTimeMillis());
+    long ttl = HttpResponse.error().getCacheTtl();
+    assertTrue(ttl <= HttpResponse.DEFAULT_TTL && ttl > 0);
+  }
+
+  private static void assertDoesNotAllowNegativeCaching(int status)  {
+    HttpResponse response = new HttpResponseBuilder()
+        .setHttpStatusCode(status)
+        .setResponse(UTF8_DATA)
+        .setStrictNoCache()
+        .create();
+    assertEquals(-1, response.getCacheTtl());
+  }
+
+  private static void assertAllowsNegativeCaching(int status) {
+    HttpResponse response = new HttpResponseBuilder()
+        .setHttpStatusCode(status)
+        .setResponse(UTF8_DATA)
+        .setStrictNoCache()
+        .create();
+    long ttl = response.getCacheTtl();
+    assertTrue(ttl <= response.getDefaultTtl() && ttl > 0);
+  }
+
+  @Test
+  public void testStrictNoCacheAndNegativeCaching() {
+    assertDoesNotAllowNegativeCaching(HttpResponse.SC_UNAUTHORIZED);
+    assertDoesNotAllowNegativeCaching(HttpResponse.SC_FORBIDDEN);
+    assertDoesNotAllowNegativeCaching(HttpResponse.SC_OK);
+    assertAllowsNegativeCaching(HttpResponse.SC_NOT_FOUND);
+    assertAllowsNegativeCaching(HttpResponse.SC_INTERNAL_SERVER_ERROR);
+    assertAllowsNegativeCaching(HttpResponse.SC_GATEWAY_TIMEOUT);
+  }
+
+  @Test
+  public void testRetryAfter() {
+    HttpResponse response;
+    for (int rc : Arrays.asList(HttpResponse.SC_INTERNAL_SERVER_ERROR, HttpResponse.SC_GATEWAY_TIMEOUT, HttpResponse.SC_BAD_REQUEST)) {
+      response = new HttpResponseBuilder()
+          .setHttpStatusCode(rc)
+          .setHeader("Retry-After","60")
+          .create();
+      long ttl = response.getCacheTtl();
+      assertTrue(ttl <= 60 * 1000L && ttl > 0);
+    }
+  }
+
+  @Test
+  public void testSetNoCache() {
+    int time = roundToSeconds(timeSource.currentTimeMillis());
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Expires", DateUtil.formatRfc1123Date(1000L * time))
+        .setStrictNoCache()
+        .create();
+    assertNull(response.getHeader("Expires"));
+    assertEquals("no-cache", response.getHeader("Pragma"));
+    assertEquals("no-cache", response.getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void testNullHeaderNamesStripped() {
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader(null, "dummy")
+        .create();
+    for (String key : response.getHeaders().keySet()) {
+      assertNotNull("Null header not removed.", key);
+    }
+  }
+
+  @Test
+  public void testIsError() {
+    // These aren't all valid status codes, but they're reserved in these blocks. Changes
+    // would be required to the HTTP standard anyway before this test would be invalid.
+    for (int i = 100; i < 400; i += 100) {
+      for (int j = 0; j < 10; ++j) {
+        HttpResponse response = new HttpResponseBuilder().setHttpStatusCode(i).create();
+        assertFalse("Status below 400 considered to be an error", response.isError());
+      }
+    }
+
+    for (int i = 400; i < 600; i += 100) {
+      for (int j = 0; j < 10; ++j) {
+        HttpResponse response = new HttpResponseBuilder().setHttpStatusCode(i).create();
+        assertTrue("Status above 400 considered to be an error", response.isError());
+      }
+    }
+  }
+
+  @Test
+  public void testSerialization() throws Exception {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    ObjectOutputStream out = new ObjectOutputStream(baos);
+
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Foo", "bar")
+        .addHeader("Foo", "baz")
+        .addHeader("Blah", "blah")
+        .setHttpStatusCode(204)
+        .setResponseString("This is the response string")
+        .create();
+
+    out.writeObject(response);
+
+    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+    ObjectInputStream in = new ObjectInputStream(bais);
+
+    HttpResponse deserialized = (HttpResponse)in.readObject();
+
+    assertEquals(response, deserialized);
+  }
+
+  @Test
+  public void testSerializationWithTransientFields() throws Exception {
+    ByteArrayOutputStream baos = new ByteArrayOutputStream();
+    ObjectOutputStream out = new ObjectOutputStream(baos);
+
+    long now = timeSource.currentTimeMillis();
+
+    HttpResponse response = new HttpResponseBuilder()
+        .addHeader("Foo", "bar")
+        .addHeader("Foo", "baz")
+        .addHeader("Blah", "blah")
+        .addHeader("Date", DateUtil.formatRfc1123Date(now))
+        .setHttpStatusCode(204)
+        .setResponseString("This is the response string")
+        .setMetadata("foo", "bar")
+        .create();
+
+    out.writeObject(response);
+
+    ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+    ObjectInputStream in = new ObjectInputStream(bais);
+
+    HttpResponse deserialized = (HttpResponse)in.readObject();
+
+    HttpResponse expectedResponse = new HttpResponseBuilder()
+        .addHeader("Foo", "bar")
+        .addHeader("Foo", "baz")
+        .addHeader("Blah", "blah")
+        .addHeader("Date", DateUtil.formatRfc1123Date(now))
+        .setHttpStatusCode(204)
+        .setResponseString("This is the response string")
+        .create();
+
+    assertEquals(expectedResponse, deserialized);
+  }
+
+  @Test
+  public void testCacheExpirationForStrictNoCacheResponse() throws Exception {
+    assertEquals(-1,
+        new HttpResponseBuilder()
+            .setStrictNoCache()
+            .setRefetchStrictNoCacheAfterMs(10000)
+            .create()
+            .getCacheExpiration());
+  }
+
+  @Test
+  public void testCacheExpirationForStrictNoCacheResponsePrivateLowMaxAge() throws Exception {
+    assertEquals(-1,
+        new HttpResponseBuilder()
+            .addHeader("Cache-Control", "private, max-age=5000")
+            .setRefetchStrictNoCacheAfterMs(10000)
+            .create()
+            .getCacheExpiration());
+  }
+
+  @Test
+  public void testCacheExpirationForStrictNoCacheResponsePrivateHighMaxAge() throws Exception {
+    assertEquals(-1,
+        new HttpResponseBuilder()
+            .addHeader("Cache-Control", "private, max-age=20000")
+            .setRefetchStrictNoCacheAfterMs(10000)
+            .create()
+            .getCacheExpiration());
+  }
+
+  @Test
+  public void testShouldRefetchForStrictNoCacheResponseCurrentTime() throws Exception {
+    assertEquals(false,
+        new HttpResponseBuilder()
+            .setStrictNoCache()
+            .setRefetchStrictNoCacheAfterMs(10000)
+            .create()
+            .shouldRefetch());
+  }
+
+  @Test
+  public void testShouldRefetchForStrictNoCacheResponseCurrentTimePrivateLowMaxAge() throws Exception {
+    assertEquals(false,
+        new HttpResponseBuilder()
+            .addHeader("Cache-Control", "private, max-age=5000")
+            .setRefetchStrictNoCacheAfterMs(10000)
+            .create()
+            .shouldRefetch());
+  }
+
+  @Test
+  public void testShouldRefetchForStrictNoCacheResponseCurrentTimePrivateHighMaxAge() throws Exception {
+    assertEquals(false,
+        new HttpResponseBuilder()
+            .addHeader("Cache-Control", "private, max-age=20000")
+            .setRefetchStrictNoCacheAfterMs(10000)
+            .create()
+            .shouldRefetch());
+  }
+
+  @Test
+  public void testShouldRefetchForStrictNoCacheResponsePastShouldRefetch() throws Exception {
+    assertEquals(true, new HttpResponseBuilder().setStrictNoCache()
+        .setHeader("Date",
+            DateUtil.formatRfc1123Date(HttpUtil.getTimeSource().currentTimeMillis() - 20000))
+        .setRefetchStrictNoCacheAfterMs(10000)
+        .create()
+        .shouldRefetch());
+  }
+
+  @Test
+  public void testShouldRefetchForStrictNoCacheResponsePastShouldNotRefetch() throws Exception {
+    assertEquals(false, new HttpResponseBuilder().setStrictNoCache()
+        .setHeader("Date",
+            DateUtil.formatRfc1123Date(HttpUtil.getTimeSource().currentTimeMillis() - 5000))
+        .setRefetchStrictNoCacheAfterMs(10000)
+        .create()
+        .shouldRefetch());
+  }
+
+  @Test
+  public void testCacheExpirationForStrictNoCacheResponseWithoutOverride() throws Exception {
+    assertEquals(-1, new HttpResponseBuilder().setStrictNoCache().create().getCacheExpiration());
+  }
+
+  @Test
+  public void testCacheExpirationForNegativeCacheExemptNoCacheControl() throws Exception {
+    // Response should return a 401 or 403 (which are negative cache exempt) that don't have cache
+    // control headers. They should still be cached for the negative ttl.
+    HttpResponse response = new HttpResponseBuilder()
+                                  .setHttpStatusCode(HttpResponse.SC_FORBIDDEN)
+                                  .create();
+    assertTrue(
+            "Response is cached for the negative TTL",
+            response.getCacheExpiration() <= timeSource.currentTimeMillis()
+                    + response.getNegativeTtl());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/InvalidationHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/InvalidationHandlerTest.java
new file mode 100644
index 0000000..3f98b1d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/InvalidationHandlerTest.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import org.apache.shindig.auth.AuthenticationMode;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expectLastCall;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Basic test of invalidation handler
+ */
+public class InvalidationHandlerTest extends EasyMockTestCase {
+
+  private BeanJsonConverter converter;
+
+  private InvalidationService invalidationService;
+
+  private InvalidationHandler handler;
+
+  private FakeGadgetToken token;
+
+  private Map<String, String[]> params;
+
+  protected HandlerRegistry registry;
+  protected ContainerConfig containerConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    token.setAppId("appId");
+    token.setViewerId("userX");
+
+    converter = mock(BeanJsonConverter.class);
+    invalidationService = mock(InvalidationService.class);
+
+    handler = new InvalidationHandler(invalidationService);
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(Sets.<Object>newHashSet(handler));
+
+    params = Maps.newHashMap();
+  }
+
+  @Test
+  public void testHandleSimpleGetInvalidateViewer() throws Exception {
+    String path = "/cache/invalidate";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    invalidationService.invalidateUserResources(
+        eq(ImmutableSet.of("userX")),
+        eq(token));
+    expectLastCall();
+
+    replay();
+    operation.execute(params, null, token, converter).get();
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testAllowConsumerAuthInvalidateAppResource() throws Exception {
+    String path = "/cache/invalidate";
+    RestHandler operation = registry.getRestHandler(path, "POST");
+    params.put(InvalidationHandler.KEYS_PARAM, new String[]{"http://www.example.org/gadget.xml"});
+    token.setAuthenticationMode(AuthenticationMode.OAUTH_CONSUMER_REQUEST.name());
+    invalidationService.invalidateApplicationResources(
+        eq(ImmutableSet.of(Uri.parse("http://www.example.org/gadget.xml"))),
+        eq(token));
+    expectLastCall();
+
+    replay();
+    operation.execute(params, null, token, converter).get();
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testFailTokenAuthInvalidateAppResource() throws Exception {
+    String path = "/cache/invalidate";
+    RestHandler operation = registry.getRestHandler(path, "POST");
+    params.put(InvalidationHandler.KEYS_PARAM, new String[]{"http://www.example.org/gadget.xml"});
+
+    try {
+      operation.execute(params, null, token, converter).get();
+      fail("Expected error");
+    } catch (ExecutionException ee) {
+      assertTrue(ee.getCause() instanceof ProtocolException);
+    }
+  }
+
+  @Test
+  public void testFailInvalidateNoApp() throws Exception {
+    String path = "/cache/invalidate";
+    RestHandler operation = registry.getRestHandler(path, "POST");
+    params.put(InvalidationHandler.KEYS_PARAM, new String[]{"http://www.example.org/gadget.xml"});
+
+    try {
+      token.setAppId("");
+      token.setAppUrl("");
+      operation.execute(params, null, token, converter).get();
+      fail("Expected error");
+    } catch (ExecutionException ee) {
+      assertTrue(ee.getCause() instanceof ProtocolException);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/MultipleResourceHttpFetcherTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/MultipleResourceHttpFetcherTest.java
new file mode 100644
index 0000000..3cd3f93
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/http/MultipleResourceHttpFetcherTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.http;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.eq;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.Pair;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.http.MultipleResourceHttpFetcher.RequestContext;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.*;
+import java.util.*;
+import java.io.*;
+
+/**
+ * Tests for {@code MultipleResourceHttpFetcher}.
+ */
+public class MultipleResourceHttpFetcherTest extends EasyMockTestCase {
+  private RequestPipeline requestPipeline;
+  private transient ExecutorService executor = Executors.newSingleThreadExecutor();
+  private MultipleResourceHttpFetcher fetcher;
+
+  private static final Uri IMG_URI =
+      UriBuilder.parse("org/apache/shindig/gadgets/rewrite/image/small.jpg").toUri();
+  private static final Uri CSS_URI =
+      UriBuilder.parse("org/apache/shindig/gadgets/rewrite/image/large.css").toUri();
+
+  private RequestContext reqCxt1;
+  private RequestContext reqCxt2;
+  private RequestContext reqCxt3;
+
+  @Before
+  public void setUp() throws Exception {
+    requestPipeline = mock(RequestPipeline.class);
+    fetcher = new MultipleResourceHttpFetcher(requestPipeline, executor);
+
+    reqCxt1 = createRequestContext(IMG_URI, "jpeg image", "image/jpeg");
+    reqCxt2 = createRequestContext(CSS_URI, "css files", "text/css");
+    reqCxt3 = createRequestContext(IMG_URI, "jpeg image", "image/jpeg");
+  }
+
+  @Test
+  public void testFetchAll() throws Exception {
+    List<HttpRequest> requests = createRequestArray();
+
+    expect(requestPipeline.execute(eq(reqCxt1.getHttpReq()))).andReturn(reqCxt1.getHttpResp());
+    expect(requestPipeline.execute(eq(reqCxt2.getHttpReq()))).andReturn(reqCxt2.getHttpResp());
+    expect(requestPipeline.execute(eq(reqCxt3.getHttpReq()))).andReturn(reqCxt3.getHttpResp());
+
+    replay();
+    List<Pair<Uri, FutureTask<RequestContext>>> futureTasks = fetcher.fetchAll(requests);
+    assertEquals(3, futureTasks.size());
+    assertEquals(IMG_URI, futureTasks.get(0).one);
+    assertEquals(reqCxt1, futureTasks.get(0).two.get());
+    assertEquals(CSS_URI, futureTasks.get(1).one);
+    assertEquals(reqCxt2, futureTasks.get(1).two.get());
+    assertEquals(IMG_URI, futureTasks.get(2).one);
+    assertEquals(reqCxt3, futureTasks.get(2).two.get());
+    verify();
+  }
+
+  @Test
+  public void testFetchUnique() throws Exception {
+    List<HttpRequest> requests = createRequestArray();
+
+    expect(requestPipeline.execute(eq(reqCxt1.getHttpReq()))).andReturn(reqCxt1.getHttpResp());
+    expect(requestPipeline.execute(eq(reqCxt2.getHttpReq()))).andReturn(reqCxt2.getHttpResp());
+
+    replay();
+    Map<Uri, FutureTask<RequestContext>> futureTasks = fetcher.fetchUnique(requests);
+    assertEquals(2, futureTasks.size());
+    assertTrue(futureTasks.containsKey(IMG_URI));
+    assertEquals(reqCxt1, futureTasks.get(IMG_URI).get());
+    assertTrue(futureTasks.containsKey(CSS_URI));
+    assertEquals(reqCxt2, futureTasks.get(CSS_URI).get());
+    verify();
+  }
+
+  private RequestContext createRequestContext(Uri uri, String content, String mimeType)
+      throws IOException {
+    HttpRequest request = new HttpRequest(uri);
+
+    HttpResponse response =  new HttpResponseBuilder().addHeader("Content-Type", mimeType)
+            .setResponse(content.getBytes()).create();
+
+    return new RequestContext(request, response, null);
+  }
+
+  private List<HttpRequest> createRequestArray() {
+    return ImmutableList.of(reqCxt1.getHttpReq(), reqCxt2.getHttpReq(), reqCxt3.getHttpReq());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddJslInfoVariableProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddJslInfoVariableProcessorTest.java
new file mode 100644
index 0000000..5bbac41
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddJslInfoVariableProcessorTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+
+/**
+ * Tests for {@link AddJslInfoVariableProcessor}.
+ */
+public class AddJslInfoVariableProcessorTest {
+
+  private static final String URI = "http://localhost";
+  private static final List<String> LIBS = ImmutableList.of("fo'o", "bar", "baz");
+  private static final String LIBS_JS = "'bar','baz','fo\\'o'";
+
+  private IMocksControl control;
+  private JsRequest request;
+  private JsUri jsUri;
+  private JsResponseBuilder response;
+  private AddJslInfoVariableProcessor processor;
+  private final FeatureRegistryProvider fregProvider = EasyMock.createMock(FeatureRegistryProvider.class);
+  private final FeatureRegistry freg = EasyMock.createMock(FeatureRegistry.class);
+
+  @Before
+  public void setUp() throws GadgetException {
+    control = EasyMock.createControl();
+    request = control.createMock(JsRequest.class);
+    response = new JsResponseBuilder();
+    processor = new AddJslInfoVariableProcessor(fregProvider);
+
+    EasyMock.reset(fregProvider, freg);
+    EasyMock.expect(fregProvider.get(EasyMock.anyObject(String.class))).andReturn(freg).anyTimes();
+
+    Capture<List<String>> features = new Capture<List<String>>();
+    EasyMock.expect(freg.getFeatures(EasyMock.capture(features))).andAnswer(new IAnswer<List<String>>() {
+      public List<String> answer() throws Throwable {
+        return LIBS;
+      }
+    });
+
+    EasyMock.replay(fregProvider, freg);
+  }
+
+  @Test
+  public void skipsWhenNohintIsTrue() throws Exception {
+    setJsUri(URI + "?nohint=1");
+    control.replay();
+    processor.process(request, response);
+    assertEquals("", response.build().toJsString());
+    control.verify();
+  }
+
+  @Test
+  public void featureInfo() throws Exception {
+    setJsUri(URI);
+    control.replay();
+    processor.process(request, response);
+    String expected = String.format(AddJslInfoVariableProcessor.BASE_HINT_TEMPLATE +
+        AddJslInfoVariableProcessor.FEATURES_HINT_TEMPLATE, LIBS_JS);
+    assertEquals(expected, response.build().toJsString());
+    control.verify();
+  }
+
+  private void setJsUri(String uri) {
+    jsUri = new JsUri(UriStatus.VALID_UNVERSIONED, Uri.parse(uri), LIBS, null);
+    EasyMock.expect(request.getJsUri()).andReturn(jsUri);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddJslLoadedVariableProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddJslLoadedVariableProcessorTest.java
new file mode 100644
index 0000000..fe23f7d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddJslLoadedVariableProcessorTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import java.util.Set;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Sets;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Tests for {@link AddJslLoadedVariableProcessor}.
+ */
+public class AddJslLoadedVariableProcessorTest {
+  private static final String REQ_1_LIB = "foo";
+  private static final String REQ_2_LIB = "gig";
+  private static final String LOAD_LIB = "bar";
+  private static final String URI = "http://localhost";
+  private static final List<String> REQ_LIBS = ImmutableList.of(
+      REQ_1_LIB, REQ_2_LIB, REQ_1_LIB, REQ_2_LIB, LOAD_LIB);
+  private static final List<String> LOAD_LIBS = ImmutableList.of(
+      LOAD_LIB, LOAD_LIB);
+
+  private IMocksControl control;
+  private JsRequest request;
+  private JsUri jsUri;
+  private JsResponseBuilder response;
+  private AddJslLoadedVariableProcessor processor;
+  private final FeatureRegistryProvider fregProvider = EasyMock.createMock(FeatureRegistryProvider.class);
+  private final FeatureRegistry freg = EasyMock.createMock(FeatureRegistry.class);
+
+  @Before
+  public void setUp() throws GadgetException {
+    control = EasyMock.createControl();
+    request = control.createMock(JsRequest.class);
+    response = new JsResponseBuilder();
+    processor = new AddJslLoadedVariableProcessor(fregProvider);
+
+    EasyMock.reset(fregProvider, freg);
+    EasyMock.expect(fregProvider.get(EasyMock.anyObject(String.class))).andReturn(freg).anyTimes();
+
+    Set<String> required = Sets.newHashSet(REQ_LIBS);
+    EasyMock.expect(freg.getAllFeatureNames()).andReturn(required).anyTimes();
+
+    EasyMock.replay(fregProvider, freg);
+  }
+
+  @Test
+  public void testSucceeds() throws Exception {
+    setUpJsUri(URI + "/" + REQ_1_LIB + ".js");
+    control.replay();
+    processor.process(request, response);
+    assertEquals(String.format(AddJslLoadedVariableProcessor.TEMPLATE,
+        "['foo','gig']"), response.build().toJsString());
+    control.verify();
+  }
+
+  @Test
+  public void testSkips() throws Exception {
+    setUpJsUri(URI + "?nohint=1");
+    control.replay();
+    processor.process(request, response);
+    assertEquals("", response.build().toJsString());
+    control.verify();
+  }
+
+  private void setUpJsUri(String uri) {
+    jsUri = new JsUri(UriStatus.VALID_UNVERSIONED, Uri.parse(uri), REQ_LIBS, LOAD_LIBS);
+    EasyMock.expect(request.getJsUri()).andReturn(jsUri);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddOnloadFunctionProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddOnloadFunctionProcessorTest.java
new file mode 100644
index 0000000..c18e4b0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AddOnloadFunctionProcessorTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.*;
+
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Tests for {@link AddOnloadFunctionProcessor}.
+ */
+public class AddOnloadFunctionProcessorTest {
+
+  private static final String ONLOAD_FUNCTION = "onloadFunc";
+
+  private IMocksControl control;
+  private JsUri jsUri;
+  private JsRequest request;
+  private JsResponseBuilder response;
+  private AddOnloadFunctionProcessor processor;
+
+  @Before
+  public void setUp() {
+    control = EasyMock.createControl();
+    jsUri = control.createMock(JsUri.class);
+    request = control.createMock(JsRequest.class);
+    response = new JsResponseBuilder();
+    processor = new AddOnloadFunctionProcessor();
+
+    EasyMock.expect(request.getJsUri()).andReturn(jsUri);
+  }
+
+  @Test
+  public void testSkipsWhenNoOnloadAndWithHintSpecified() throws Exception {
+    EasyMock.expect(jsUri.getOnload()).andReturn(null);
+    EasyMock.expect(jsUri.isNohint()).andReturn(false);
+    response = control.createMock(JsResponseBuilder.class);
+    control.replay();
+    assertTrue(processor.process(request, response));
+    control.verify();
+  }
+
+  @Test
+  public void testFailsWithInvalidFunctionName() throws Exception {
+    EasyMock.expect(jsUri.getOnload()).andReturn("!!%%!!%%");
+    control.replay();
+    try {
+      processor.process(request, response);
+      fail("A JsException should have been thrown by the processor.");
+    } catch (JsException e) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getStatusCode());
+      assertEquals(AddOnloadFunctionProcessor.ONLOAD_FUNCTION_NAME_ERROR, e.getMessage());
+    }
+    control.verify();
+  }
+
+  @Test
+  public void testGeneratesCallbackCode() throws Exception {
+    EasyMock.expect(jsUri.getOnload()).andReturn(ONLOAD_FUNCTION);
+    control.replay();
+    assertTrue(processor.process(request, response));
+    assertEquals(HttpServletResponse.SC_OK, response.getStatusCode());
+    String expectedBody = String.format(AddOnloadFunctionProcessor.ONLOAD_JS_TPL, ONLOAD_FUNCTION);
+    assertEquals(expectedBody, response.build().toJsString());
+    control.verify();
+  }
+
+  @Test
+  public void testWithoutHint() throws Exception {
+    EasyMock.expect(jsUri.getOnload()).andReturn(null);
+    EasyMock.expect(jsUri.isNohint()).andReturn(true);
+    control.replay();
+    assertTrue(processor.process(request, response));
+    assertEquals(AddOnloadFunctionProcessor.JSL_CALLBACK_JS, response.build().toJsString());
+    control.verify();
+  }
+
+  @Test
+  public void testWithHint() throws Exception {
+    EasyMock.expect(jsUri.getOnload()).andReturn(null);
+    EasyMock.expect(jsUri.isNohint()).andReturn(false);
+    control.replay();
+    assertTrue(processor.process(request, response));
+    assertEquals("", response.build().toJsString());
+    control.verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AnonFuncWrappingProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AnonFuncWrappingProcessorTest.java
new file mode 100644
index 0000000..3213184
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/AnonFuncWrappingProcessorTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.easymock.EasyMock.createControl;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.IMocksControl;
+
+import org.junit.Test;
+
+public class AnonFuncWrappingProcessorTest {
+  @Test
+  public void wrapCodeAllRunTime() throws Exception {
+    checkWrapCode(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL, true);
+  }
+
+  @Test
+  public void wrapCodeExplicitRunTime() throws Exception {
+    checkWrapCode(JsCompileMode.CONCAT_COMPILE_EXPORT_EXPLICIT, true);
+  }
+
+  @Test
+  public void wrapCodeBuildTimeDoesNothing() throws Exception {
+    checkWrapCode(JsCompileMode.COMPILE_CONCAT, false);
+  }
+
+  private void checkWrapCode(JsCompileMode mode, boolean wraps) throws Exception {
+    IMocksControl control = createControl();
+    JsRequest request = control.createMock(JsRequest.class);
+    JsUri jsUri = control.createMock(JsUri.class);
+    expect(jsUri.getCompileMode()).andReturn(mode);
+    expect(request.getJsUri()).andReturn(jsUri);
+    JsResponseBuilder builder = new JsResponseBuilder().appendJs("JS_CODE", "source");
+    AnonFuncWrappingProcessor processor = new AnonFuncWrappingProcessor();
+    control.replay();
+    assertTrue(processor.process(request, builder));
+    control.verify();
+    if (wraps) {
+      assertEquals("(function(){JS_CODE})();", builder.build().toJsString());
+    } else {
+      assertEquals("JS_CODE", builder.build().toJsString());
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/CajaJsSubtractingProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/CajaJsSubtractingProcessorTest.java
new file mode 100644
index 0000000..e9e6603
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/CajaJsSubtractingProcessorTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.apache.shindig.gadgets.js.CajaJsSubtractingProcessor.ATTRIB_VALUE;
+import static org.easymock.EasyMock.createControl;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriCommon;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+
+public class CajaJsSubtractingProcessorTest {
+
+  private static final List<String> ERRORS = ImmutableList.<String>of();
+
+  private static final String SOURCE = "source";
+  private static final String NORMAL_CONTENT_JS = "normal";
+  private static final String CAJA_CONTENT_JS = "cajoled";
+
+  private IMocksControl control;
+  private List<JsContent> contents = Lists.newArrayList();
+  private JsResponse response;
+  private JsResponseBuilder builder;
+
+  private CajaJsSubtractingProcessor processor;
+
+  @Before
+  public void setUp() {
+    control = createControl();
+
+    contents = Lists.newArrayList();
+    contents.add(JsContent.fromFeature(NORMAL_CONTENT_JS, SOURCE, null, null));
+    contents.add(JsContent.fromFeature(NORMAL_CONTENT_JS, SOURCE, null,
+        mockFeatureResource(null)));
+    contents.add(JsContent.fromFeature(NORMAL_CONTENT_JS, SOURCE, null,
+        mockFeatureResource(ImmutableMap.of(UriCommon.Param.CAJOLE.getKey(), "blah"))));
+    contents.add(JsContent.fromFeature(CAJA_CONTENT_JS, SOURCE, null,
+        mockFeatureResource(ImmutableMap.of(UriCommon.Param.CAJOLE.getKey(), ATTRIB_VALUE))));
+
+    response = new JsResponse(contents, -1, -1, false, ERRORS, null);
+    builder = new JsResponseBuilder(response);
+
+    processor = new CajaJsSubtractingProcessor();
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void noCajoleRequest() throws Exception {
+    JsUri uri = mockJsUri(false);
+    JsRequest request = mockJsRequest(uri);
+
+    control.replay();
+    boolean actualReturn = processor.process(request, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals(Strings.repeat(NORMAL_CONTENT_JS, 3), actualResponse.toJsString());
+  }
+
+  @Test
+  public void cajoleRequest() throws Exception {
+    JsUri uri = mockJsUri(true);
+    JsRequest request = mockJsRequest(uri);
+
+    control.replay();
+    boolean actualReturn = processor.process(request, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals(Strings.repeat(NORMAL_CONTENT_JS, 3) + CAJA_CONTENT_JS,
+        actualResponse.toJsString());
+  }
+
+  private FeatureResource mockFeatureResource(Map<String, String> map) {
+    FeatureResource result = control.createMock(FeatureResource.class);
+    expect(result.getAttribs()).andReturn(map).anyTimes();
+    return result;
+  }
+
+  private JsUri mockJsUri(boolean cajole) {
+    JsUri uri = control.createMock(JsUri.class);
+    expect(uri.cajoleContent()).andReturn(cajole).anyTimes();
+    return uri;
+  }
+
+  private JsRequest mockJsRequest(JsUri uri) {
+    JsRequest request = control.createMock(JsRequest.class);
+    expect(request.getJsUri()).andReturn(uri).anyTimes();
+    return request;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/CompilationProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/CompilationProcessorTest.java
new file mode 100644
index 0000000..f74430b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/CompilationProcessorTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.easymock.EasyMock.createControl;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.same;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.rewrite.js.JsCompiler;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+public class CompilationProcessorTest {
+  private IMocksControl control;
+  private JsCompiler compiler;
+  private CompilationProcessor processor;
+
+  @Before
+  public void setUp() throws Exception {
+    control = createControl();
+    compiler = control.createMock(JsCompiler.class);
+    processor = new CompilationProcessor(compiler);
+  }
+
+  @Test
+  public void compilerIsRun() throws Exception {
+    JsUri jsUri = control.createMock(JsUri.class);
+    JsResponseBuilder builder =
+        new JsResponseBuilder().setCacheTtlSecs(1234).setStatusCode(200)
+          .appendJs("content1:", "source1")
+          .appendJs("content2", "source2")
+          .appendJs(JsContent.fromFeature("content3:", "source3", mockBundle("extern3"), null))
+          .appendJs(JsContent.fromFeature("content4:", "source4", mockBundle("extern4"), null));
+    JsResponse outputResponse = new JsResponseBuilder().appendJs("content3", "s3").build();
+    JsRequest request = control.createMock(JsRequest.class);
+    expect(request.getJsUri()).andReturn(jsUri);
+    expect(compiler.compile(same(jsUri), eq(builder.build().getAllJsContent()),
+        isA(String.class))).andReturn(outputResponse);
+
+    control.replay();
+    boolean status = processor.process(request, builder);
+    control.verify();
+
+    assertTrue(status);
+    JsResponse compResult = builder.build();
+    assertEquals(200, compResult.getStatusCode());
+    assertEquals(1234, compResult.getCacheTtlSecs());
+    assertEquals("content3", compResult.toJsString());
+    Iterator<JsContent> outIterator = compResult.getAllJsContent().iterator();
+    JsContent firstOut = outIterator.next();
+    assertEquals("content3", firstOut.get());
+    assertEquals("s3", firstOut.getSource());
+    assertFalse(outIterator.hasNext());
+  }
+
+  @Test
+  public void compilerTtlIsUsed() throws Exception {
+    JsUri jsUri = control.createMock(JsUri.class);
+    JsResponseBuilder builder =
+        new JsResponseBuilder().setCacheTtlSecs(1234).setStatusCode(200)
+          .appendJs("content1:", "source1");
+    JsResponse outputResponse =
+        new JsResponseBuilder().setCacheTtlSecs(789)
+            .appendJs("content3", "s3").build();
+    JsRequest request = control.createMock(JsRequest.class);
+    expect(request.getJsUri()).andReturn(jsUri);
+    expect(compiler.compile(same(jsUri), eq(builder.build().getAllJsContent()),
+        isA(String.class))).andReturn(outputResponse);
+
+    control.replay();
+    boolean status = processor.process(request, builder);
+    control.verify();
+
+    JsResponse compResult = builder.build();
+    assertEquals(200, compResult.getStatusCode());
+    assertEquals(789, compResult.getCacheTtlSecs());
+  }
+
+  private FeatureBundle mockBundle(String... externs) {
+    FeatureBundle result = createMock(FeatureBundle.class);
+    expect(result.getApis(ApiDirective.Type.JS, true)).andReturn(
+        Lists.newArrayList(externs)).anyTimes();
+    replay(result);
+    return result;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/ConfigInjectionProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/ConfigInjectionProcessorTest.java
new file mode 100644
index 0000000..701906c
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/ConfigInjectionProcessorTest.java
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createControl;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.config.ConfigContributor;
+import org.apache.shindig.gadgets.config.DefaultConfigProcessor;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.Capture;
+import org.easymock.IAnswer;
+import org.easymock.IMocksControl;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+public class ConfigInjectionProcessorTest {
+  private static final List<String> EMPTY_LIST = ImmutableList.of();
+  private static final String HOST = "myHost";
+  private static final String BASE_CODE = "code\n";
+  private static final String CONTAINER = "container";
+  private IMocksControl control;
+  private JsUri jsUri;
+  private JsRequest request;
+  private FeatureRegistry registry;
+  private ContainerConfig containerConfig;
+  private Map<String, ConfigContributor> configContributors;
+  private ConfigInjectionProcessor processor;
+
+  @Before
+  public void setUp() throws Exception {
+    control = createControl();
+    jsUri = control.createMock(JsUri.class);
+    request = control.createMock(JsRequest.class);
+    registry = control.createMock(FeatureRegistry.class);
+    containerConfig = control.createMock(ContainerConfig.class);
+    configContributors = Maps.newHashMap();
+    FeatureRegistryProvider registryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) {
+        return registry;
+      }
+    };
+
+    processor = new ConfigInjectionProcessor(registryProvider,
+        new DefaultConfigProcessor(configContributors, containerConfig));
+  }
+
+  @Test
+  public void gadgetGetsNothing() throws Exception {
+    JsResponseBuilder builder = prepareRequestReturnBuilder(RenderingContext.GADGET);
+    control.replay();
+    assertTrue(processor.process(request, builder));
+    control.verify();
+    assertEquals(BASE_CODE, builder.build().toJsString());
+  }
+
+  @Test
+  public void containerNoFeaturesDoesNothing() throws Exception {
+    checkNoFeaturesDoesNothing(RenderingContext.CONTAINER);
+  }
+
+  @Test
+  public void configuredNoFeaturesDoesNothing() throws Exception {
+    checkNoFeaturesDoesNothing(RenderingContext.CONFIGURED_GADGET);
+  }
+
+  private void checkNoFeaturesDoesNothing(RenderingContext ctx) throws Exception {
+    JsResponseBuilder builder = prepareRequestReturnBuilder(ctx);
+    expect(containerConfig.getMap(CONTAINER, ConfigInjectionProcessor.GADGETS_FEATURES_KEY))
+        .andReturn(null);
+    List<String> libs = ImmutableList.of();
+    expect(jsUri.getLibs()).andReturn(libs);
+    expect(jsUri.getLoadedLibs()).andReturn(EMPTY_LIST);
+    expect(registry.getFeatures(libs)).andReturn(libs);
+    expect(request.getHost()).andReturn("host");
+    control.replay();
+    assertTrue(processor.process(request, builder));
+    control.verify();
+    assertEquals(BASE_CODE, builder.build().toJsString());
+  }
+
+  @Test
+  public void containerNoMatchingFeaturesDoesNothing() throws Exception {
+    checkNoMatchingFeaturesDoesNothing(RenderingContext.CONTAINER);
+  }
+
+  @Test
+  public void configuredNoMatchingFeaturesDoesNothing() throws Exception {
+    checkNoMatchingFeaturesDoesNothing(RenderingContext.CONFIGURED_GADGET);
+  }
+
+  private void checkNoMatchingFeaturesDoesNothing(RenderingContext ctx) throws Exception {
+    JsResponseBuilder builder = prepareRequestReturnBuilder(ctx);
+    Map<String, Object> baseConfig = Maps.newHashMap();
+    baseConfig.put("feature1", "config1");
+    Map<String, String> f2MapConfig = Maps.newHashMap();
+    f2MapConfig.put("key1", "val1");
+    f2MapConfig.put("key2", "val2");
+    baseConfig.put("feature2", f2MapConfig);
+    expect(containerConfig.getMap(CONTAINER, ConfigInjectionProcessor.GADGETS_FEATURES_KEY))
+        .andReturn(baseConfig);
+    List<String> libs = ImmutableList.of("lib1", "lib2");
+    expect(jsUri.getLibs()).andReturn(libs);
+    expect(jsUri.getLoadedLibs()).andReturn(EMPTY_LIST);
+    expect(registry.getFeatures(libs)).andReturn(libs);
+    expect(request.getHost()).andReturn("host");
+    control.replay();
+    assertTrue(processor.process(request, builder));
+    control.verify();
+    assertEquals(BASE_CODE, builder.build().toJsString());
+  }
+
+  @Test
+  public void containerNoContributorsGetsBase() throws Exception {
+    checkNoContributorsGetsBase(RenderingContext.CONTAINER);
+  }
+
+  @Test
+  public void configuredNoContributorsGetsBase() throws Exception {
+    checkNoContributorsGetsBase(RenderingContext.CONFIGURED_GADGET);
+  }
+
+  private void checkNoContributorsGetsBase(RenderingContext ctx) throws Exception {
+    checkInjectConfig(ctx, false);
+  }
+
+  @Test
+  public void containerModeInjectConfig() throws Exception {
+    checkInjectConfig(RenderingContext.CONTAINER);
+  }
+
+  @Test
+  public void configuredModeInjectConfig() throws Exception {
+    checkInjectConfig(RenderingContext.CONFIGURED_GADGET);
+  }
+
+  private void checkInjectConfig(RenderingContext ctx) throws Exception {
+    checkInjectConfig(ctx, true);
+  }
+
+  private void checkInjectConfig(RenderingContext ctx, boolean extraContrib) throws Exception {
+    JsResponseBuilder builder = prepareRequestReturnBuilder(ctx);
+    Map<String, Object> baseConfig = Maps.newHashMap();
+    baseConfig.put("feature1", "config1");
+    Map<String, String> f2MapConfig = Maps.newHashMap();
+    f2MapConfig.put("key1", "val1");
+    f2MapConfig.put("key2", "val2");
+    baseConfig.put("feature2", f2MapConfig);
+    baseConfig.put("feature3", "contributorListens");
+    baseConfig.put("feature4", "unused");
+    expect(containerConfig.getMap(CONTAINER, ConfigInjectionProcessor.GADGETS_FEATURES_KEY))
+        .andReturn(baseConfig);
+    expect(request.getHost()).andReturn(HOST).anyTimes();
+    ImmutableList.Builder<String> libsBuilder =
+        ImmutableList.<String>builder().add(ConfigInjectionProcessor.CONFIG_FEATURE,
+          "feature1", "feature2");
+    if (extraContrib) {
+      libsBuilder.add("feature3");
+      ConfigContributor cc = control.createMock(ConfigContributor.class);
+      Capture<Map<String, Object>> captureConfig = new Capture<Map<String, Object>>();
+      cc.contribute(capture(captureConfig), eq(CONTAINER), eq(HOST));
+      expectLastCall().andAnswer(new IAnswer<Void>() {
+        @SuppressWarnings("unchecked")
+        public Void answer() throws Throwable {
+          Map<String, Object> config = (Map<String, Object>)getCurrentArguments()[0];
+          String f3Value = (String)config.get("feature3");
+          config.put("feature3", f3Value + ":MODIFIED");
+          return null;
+        }
+      });
+      configContributors.put("feature3", cc);
+    }
+    List<String> libs = libsBuilder.build();
+    expect(jsUri.getLibs()).andReturn(libs);
+    expect(jsUri.getLoadedLibs()).andReturn(EMPTY_LIST);
+    expect(registry.getFeatures(libs)).andReturn(libs);
+
+    control.replay();
+    assertTrue(processor.process(request, builder));
+    control.verify();
+    String jsCode = builder.build().toJsString();
+    String baseMatch = "window['___jsl'] = window['___jsl'] || {};" +
+        "(window['___jsl']['ci'] = (window['___jsl']['ci'] || [])).push(";
+    assertTrue(jsCode.startsWith(baseMatch));
+    String endMatch = ");\n";
+    assertTrue(jsCode.endsWith(endMatch));
+    String injectedConfig = jsCode.substring(baseMatch.length(),
+        jsCode.length() - endMatch.length());
+
+    // Convert to JSON object to bypass ordering issues.
+    // This is bulky but works. There's probably a better way.
+    JSONObject configObj = new JSONObject(injectedConfig);
+    JSONObject expected = new JSONObject();
+    expected.put("feature1", "config1");
+    JSONObject subConfig = new JSONObject();
+    subConfig.put("key1", "val1");
+    subConfig.put("key2", "val2");
+    expected.put("feature2", subConfig);
+    if (extraContrib) {
+      expected.put("feature3", "contributorListens:MODIFIED");
+    }
+    assertEquals(expected.length(), configObj.length());
+    assertEquals(expected.get("feature1").toString(), configObj.get("feature1").toString());
+    assertEquals(expected.get("feature2").toString(), configObj.get("feature2").toString());
+    if (extraContrib) {
+      assertEquals(expected.get("feature3").toString(), configObj.get("feature3").toString());
+    }
+  }
+
+  private JsResponseBuilder prepareRequestReturnBuilder(RenderingContext ctx) {
+    expect(jsUri.getContext()).andReturn(ctx);
+    expect(jsUri.getContainer()).andReturn(CONTAINER);
+    expect(jsUri.isDebug()).andReturn(false);
+    expect(jsUri.getRepository()).andReturn(null);
+    expect(request.getJsUri()).andReturn(jsUri);
+    return new JsResponseBuilder().appendJs(BASE_CODE, "source");
+  }
+
+  @Test
+  public void newGlobalConfigAdded() throws Exception {
+    List<String> requested = ImmutableList.of("reqfeature1", "reqfeature2", "already1");
+    List<String> alreadyHas = ImmutableList.of("already1");
+    expect(jsUri.getLibs()).andReturn(requested);
+    expect(jsUri.getLoadedLibs()).andReturn(alreadyHas);
+    Map<String, Object> config =
+        ImmutableMap.<String, Object>of("reqfeature1", "reqval1", "already1", "alval1");
+    expect(containerConfig.getMap(CONTAINER, ConfigInjectionProcessor.GADGETS_FEATURES_KEY))
+        .andReturn(config);
+    expect(request.getHost()).andReturn(HOST).anyTimes();
+    expect(registry.getFeatures(requested)).andReturn(requested);
+
+    JsResponseBuilder builder = prepareRequestReturnBuilder(RenderingContext.CONFIGURED_GADGET);
+
+    ConfigContributor cc = control.createMock(ConfigContributor.class);
+    Capture<Map<String, Object>> captureConfig = new Capture<Map<String, Object>>();
+    cc.contribute(capture(captureConfig), eq(CONTAINER), eq(HOST));
+    expectLastCall().andAnswer(new IAnswer<Void>() {
+      @SuppressWarnings("unchecked")
+      public Void answer() throws Throwable {
+        Map<String, Object> config = (Map<String, Object>)getCurrentArguments()[0];
+        String f3Value = (String)config.get("reqfeature1");
+        config.put("reqfeature1", f3Value + ":MODIFIED");
+        return null;
+      }
+    });
+    configContributors.put("reqfeature1", cc);
+
+    control.replay();
+    assertTrue(processor.process(request, builder));
+    control.verify();
+
+    String jsCode = builder.build().toJsString();
+    String startCode = BASE_CODE + "window['___cfg']=";
+    assertTrue(jsCode.startsWith(startCode));
+    String json = jsCode.substring(startCode.length(), jsCode.length() - ";\n".length());
+    JSONObject configObj = new JSONObject(json);
+    assertEquals(1, configObj.names().length());
+    assertEquals("reqval1:MODIFIED", configObj.getString("reqfeature1"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DefaultJsProcessorRegistryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DefaultJsProcessorRegistryTest.java
new file mode 100644
index 0000000..6ecb9a3
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DefaultJsProcessorRegistryTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for {@link DefaultJsProcessorRegistry}.
+ */
+public class DefaultJsProcessorRegistryTest {
+
+  private static final String JS_CODE = "some JS code";
+
+  private IMocksControl control;
+  private JsRequest request;
+  private JsResponseBuilder response;
+  private JsProcessor processor0;
+  private JsProcessor processor1;
+  private JsProcessor processor2;
+  private JsProcessor processor3;
+  private DefaultJsProcessorRegistry registry;
+
+  @Before
+  public void setUp() {
+    control = EasyMock.createControl();
+    request = control.createMock(JsRequest.class);
+    response = new JsResponseBuilder();
+    processor0 = control.createMock(JsProcessor.class);
+    processor1 = control.createMock(JsProcessor.class);
+    processor2 = control.createMock(JsProcessor.class);
+    processor3 = control.createMock(JsProcessor.class);
+    registry = new DefaultJsProcessorRegistry(
+        ImmutableList.of(processor0),
+        ImmutableList.of(processor1, processor2),
+        ImmutableList.of(processor3));
+  }
+
+  @Test
+  public void testProcessorModifiesResponse() throws Exception {
+    JsProcessor preprocessor = new JsProcessor() {
+      public boolean process(JsRequest request, JsResponseBuilder builder) {
+        return true;
+      }
+    };
+    JsProcessor processor = new JsProcessor() {
+      public boolean process(JsRequest request, JsResponseBuilder builder) {
+        builder.clearJs().appendJs(JS_CODE, "js");
+        return true;
+      }
+    };
+    registry = new DefaultJsProcessorRegistry(
+        ImmutableList.of(preprocessor),
+        ImmutableList.of(processor),
+        ImmutableList.<JsProcessor>of());
+    control.replay();
+    registry.process(request, response);
+    assertEquals(JS_CODE, response.build().toJsString());
+    control.verify();
+  }
+
+  @Test
+  public void testTwoProcessorsAreRunOneAfterAnother() throws Exception {
+    EasyMock.expect(processor0.process(request, response)).andReturn(true);
+    EasyMock.expect(processor1.process(request, response)).andReturn(true);
+    EasyMock.expect(processor2.process(request, response)).andReturn(true);
+    EasyMock.expect(processor3.process(request, response)).andReturn(true);
+    control.replay();
+    registry.process(request, response);
+    control.verify();
+  }
+
+  @Test
+  public void testProcessorStopsProcessingWhenItReturnsFalse() throws Exception {
+    EasyMock.expect(processor0.process(request, response)).andReturn(true);
+    EasyMock.expect(processor1.process(request, response)).andReturn(false);
+    EasyMock.expect(processor3.process(request, response)).andReturn(true);
+    control.replay();
+    registry.process(request, response);
+    control.verify();
+  }
+
+  @Test
+  public void testProcessorStopsProcessingWhenPreProcessorsReturnsFalse() throws Exception {
+    EasyMock.expect(processor0.process(request, response)).andReturn(false);
+    control.replay();
+    registry.process(request, response);
+    control.verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DefaultJsServingPipelineTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DefaultJsServingPipelineTest.java
new file mode 100644
index 0000000..be61d4b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DefaultJsServingPipelineTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Test;
+
+
+/**
+ * Tests for {@link DefaultJsServingPipeline}.
+ */
+public class DefaultJsServingPipelineTest {
+
+  @Test
+  public void testProcessorsAreCalledForRequest() throws Exception {
+    IMocksControl control = EasyMock.createControl();
+    JsRequest request = control.createMock(JsRequest.class);
+    JsProcessorRegistry registry = control.createMock(JsProcessorRegistry.class);
+    DefaultJsServingPipeline pipeline = new DefaultJsServingPipeline(registry);
+    registry.process(EasyMock.eq(request), EasyMock.isA(JsResponseBuilder.class));
+    control.replay();
+
+    pipeline.execute(request);
+
+    control.verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DeferJsProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DeferJsProcessorTest.java
new file mode 100644
index 0000000..96330c0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/DeferJsProcessorTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class DeferJsProcessorTest {
+  private final String DEFER_JS_DEB = "function deferJs() {};";
+
+  private final List<String> EXPORTS_1 = ImmutableList.of(
+      "gadgets",
+      "gadgets.rpc.call",
+      "gadgets.rpc.register",
+      "shindig",
+      "shindig.random");
+
+  private final List<String> EXPORTS_2 = ImmutableList.of(
+      "foo",
+      "foo.prototype.bar");
+
+  private final String EXPORT_STRING_1_DEFER =
+    "deferJs('gadgets');" +
+    "deferJs('shindig');" +
+    "deferJs('gadgets.rpc',['call','register']);" +
+    "deferJs('shindig',['random']);";
+
+  private final List<String> LIBS_WITH_DEFER = Lists.newArrayList("lib1");
+  private final List<String> LIBS_WITHOUT_DEFER = Lists.newArrayList("lib2");
+  private final List<String> LOADED = Lists.newArrayList();
+
+  private DeferJsProcessor processor;
+  private FeatureRegistry featureRegistry;
+
+  @Before
+  public void setUp() throws Exception {
+    GadgetContext ctx = new GadgetContext();
+    Provider<GadgetContext> contextProviderMock = Providers.of(ctx);
+    FeatureResource resource = mockResource(DEFER_JS_DEB);
+    FeatureRegistry.FeatureBundle bundle = mockExportJsBundle(resource);
+    LookupResult lookupMock = mockLookupResult(bundle);
+    final FeatureRegistry featureRegistryMock = mockRegistry(lookupMock);
+    featureRegistry = featureRegistryMock;
+    FeatureRegistryProvider registryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) {
+        return featureRegistryMock;
+      }
+    };
+    processor = new DeferJsProcessor(registryProvider, contextProviderMock);
+  }
+
+  @Test
+  public void processWithOneNonEmptyFeatureDeferred() throws Exception {
+    JsUri jsUri = mockJsUri(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL, true, LIBS_WITH_DEFER);
+    JsRequest jsRequest = new JsRequest(jsUri, null, false, featureRegistry);
+    JsResponseBuilder jsBuilder = new JsResponseBuilder();
+    boolean actualReturnCode = processor.process(jsRequest, jsBuilder);
+    assertTrue(actualReturnCode);
+    assertEquals(
+        DEFER_JS_DEB + EXPORT_STRING_1_DEFER,
+        jsBuilder.build().toJsString());
+  }
+
+  @Test
+  public void processWithOneNonEmptyFeatureDeferredNotSupported() throws Exception {
+    JsUri jsUri = mockJsUri(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL, true, LIBS_WITHOUT_DEFER);
+    JsRequest jsRequest = new JsRequest(jsUri, null, false, featureRegistry);
+    JsResponseBuilder jsBuilder = new JsResponseBuilder();
+    boolean actualReturnCode = processor.process(jsRequest, jsBuilder);
+    assertTrue(actualReturnCode);
+    assertEquals(
+        "",
+        jsBuilder.build().toJsString());
+  }
+
+  @SuppressWarnings("unchecked")
+  private FeatureRegistry mockRegistry(LookupResult lookupMock) {
+    FeatureRegistry result = createMock(FeatureRegistry.class);
+    expect(result.getFeatureResources(
+        isA(GadgetContext.class), isA(List.class), EasyMock.isNull(List.class))).
+        andReturn(lookupMock).anyTimes();
+    expect(result.getFeatureResources(
+        isA(GadgetContext.class), eq(LIBS_WITH_DEFER), EasyMock.isNull(List.class), eq(false))).
+        andReturn(mockLookupResult(mockBundle(EXPORTS_1, true))).anyTimes();
+    expect(result.getFeatureResources(
+        isA(GadgetContext.class), eq(LIBS_WITHOUT_DEFER), EasyMock.isNull(List.class), eq(false))).
+        andReturn(mockLookupResult(mockBundle(EXPORTS_2, false))).anyTimes();
+    expect(result.getFeatures(LIBS_WITHOUT_DEFER)).andReturn(LIBS_WITHOUT_DEFER).anyTimes();
+    expect(result.getFeatures(LIBS_WITH_DEFER)).andReturn(LIBS_WITH_DEFER).anyTimes();
+    expect(result.getFeatures(LOADED)).andReturn(LOADED).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private JsUri mockJsUri(JsCompileMode mode, boolean isJsload, List<String> libs) {
+    JsUri result = createMock(JsUri.class);
+    expect(result.getCompileMode()).andStubReturn(mode);
+    expect(result.getRepository()).andStubReturn(null);
+    expect(result.isJsload()).andReturn(isJsload).anyTimes();
+    expect(result.getLibs()).andReturn(libs).anyTimes();
+    expect(result.getLoadedLibs()).andReturn(LOADED).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private LookupResult mockLookupResult(FeatureRegistry.FeatureBundle featureBundle) {
+    LookupResult result = createMock(LookupResult.class);
+    expect(result.getBundles()).andReturn(ImmutableList.of(featureBundle)).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureResource mockResource(String debContent) {
+    FeatureResource result = createMock(FeatureResource.class);
+    expect(result.getDebugContent()).andReturn(debContent).anyTimes();
+    expect(result.getName()).andReturn("js").anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureBundle mockBundle(List<String> exports, boolean isDefer) {
+    List<ApiDirective> apis = Lists.newArrayList();
+    for (String e : exports) apis.add(mockApiDirective(true, e));
+    FeatureBundle result = createMock(FeatureBundle.class);
+    expect(result.getApis(ApiDirective.Type.JS, true)).andReturn(exports).anyTimes();
+    expect(result.isSupportDefer()).andReturn(isDefer).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureBundle mockExportJsBundle(FeatureResource featureResourceMock) {
+    FeatureRegistry.FeatureBundle featureBundle = createMock(FeatureBundle.class);
+    expect(featureBundle.getResources()).andReturn(
+        ImmutableList.of(featureResourceMock)).anyTimes();
+    replay(featureBundle);
+    return featureBundle;
+  }
+
+  private ApiDirective mockApiDirective(boolean isExports, String value) {
+    ApiDirective result = createMock(ApiDirective.class);
+    expect(result.getType()).andReturn(ApiDirective.Type.JS).anyTimes();
+    expect(result.getValue()).andReturn(value).anyTimes();
+    expect(result.isExports()).andReturn(isExports).anyTimes();
+    replay(result);
+    return result;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/ExportJsProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/ExportJsProcessorTest.java
new file mode 100644
index 0000000..9d05316
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/ExportJsProcessorTest.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class ExportJsProcessorTest {
+  private final String EXPORT_JS_DEB = "function exportJs() { };";
+  private final String TEXT_CONTENT_1 = "text1;";
+  private final String TEXT_CONTENT_2 = "text2;";
+  private final String FEATURE_CONTENT_1 = "feature1;";
+  private final String FEATURE_CONTENT_2 = "feature2;";
+  private final String FEATURE_CONTENT_3 = "feature3;";
+
+  private final List<String> EXPORTS_1 = ImmutableList.of(
+      "gadgets",
+      "gadgets.rpc.call",
+      "gadgets.rpc.register",
+      "shindig",
+      "shindig.random");
+
+  private final List<String> EXPORTS_2 = ImmutableList.of(
+      "foo",
+      "foo.prototype.bar");
+
+  private final List<String> EXPORTS_3 = ImmutableList.<String>of();
+
+  private final String EXPORT_STRING_1 =
+      "exportJs('gadgets',[gadgets]);" +
+      "exportJs('shindig',[shindig]);" +
+      "exportJs('gadgets.rpc',[gadgets,gadgets.rpc],{call:'call',register:'register'});" +
+      "exportJs('shindig',[shindig],{random:'random'});";
+
+  private final String EXPORT_STRING_2 =
+      "exportJs('foo',[foo]);" +
+      "exportJs('foo.prototype',[foo,foo.prototype],{bar:'bar'});";
+
+  private final String EXPORT_STRING_3 = "";
+
+  private JsContent textJsContent1;
+  private JsContent textJsContent2;
+  private JsContent featureJsContent1;
+  private JsContent featureJsContent2;
+  private JsContent featureJsContent3;
+  private ExportJsProcessor processor;
+
+  @Before
+  public void setUp() throws Exception {
+    GadgetContext ctx = new GadgetContext();
+    Provider<GadgetContext> contextProviderMock = Providers.of(ctx);
+    FeatureResource resource = mockResource(EXPORT_JS_DEB);
+    FeatureRegistry.FeatureBundle bundle = mockExportJsBundle(resource);
+    LookupResult lookupMock = mockLookupResult(bundle);
+    final FeatureRegistry featureRegistryMock = mockRegistry(lookupMock);
+    FeatureRegistryProvider registryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) {
+        return featureRegistryMock;
+      }
+    };
+
+    textJsContent1 = JsContent.fromText(TEXT_CONTENT_1, "source1");
+    textJsContent2 = JsContent.fromText(TEXT_CONTENT_2, "source2");
+    featureJsContent1 = JsContent.fromFeature(FEATURE_CONTENT_1, "source3", mockBundle(EXPORTS_1), null);
+    featureJsContent2 = JsContent.fromFeature(FEATURE_CONTENT_2, "source4", mockBundle(EXPORTS_2), null);
+    featureJsContent3 = JsContent.fromFeature(FEATURE_CONTENT_3, "source5", mockBundle(EXPORTS_3), null);
+    processor = new ExportJsProcessor(registryProvider, contextProviderMock);
+  }
+
+  @SuppressWarnings("unchecked")
+  private FeatureRegistry mockRegistry(LookupResult lookupMock) {
+    FeatureRegistry result = createMock(FeatureRegistry.class);
+    expect(result.getFeatureResources(
+        isA(GadgetContext.class), isA(List.class), EasyMock.isNull(List.class))).
+        andReturn(lookupMock).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private JsUri mockJsUri(JsCompileMode mode) {
+    return mockJsUri(mode, false);
+  }
+
+  private JsUri mockJsUri(JsCompileMode mode, boolean isJsload) {
+    JsUri result = createMock(JsUri.class);
+    expect(result.getCompileMode()).andStubReturn(mode);
+    expect(result.getRepository()).andStubReturn(null);
+    expect(result.isJsload()).andReturn(isJsload).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private LookupResult mockLookupResult(FeatureRegistry.FeatureBundle featureBundle) {
+    LookupResult result = createMock(LookupResult.class);
+    expect(result.getBundles()).andReturn(ImmutableList.of(featureBundle)).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureResource mockResource(String debContent) {
+    FeatureResource result = createMock(FeatureResource.class);
+    expect(result.getDebugContent()).andReturn(debContent).anyTimes();
+    expect(result.getName()).andReturn("js").anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureBundle mockBundle(List<String> exports) {
+    List<ApiDirective> apis = Lists.newArrayList();
+    for (String e : exports) apis.add(mockApiDirective(true, e));
+    FeatureBundle result = createMock(FeatureBundle.class);
+    expect(result.getApis(ApiDirective.Type.JS, true)).andReturn(exports).anyTimes();
+    expect(result.isSupportDefer()).andReturn(false).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureBundle mockExportJsBundle(FeatureResource featureResourceMock) {
+    FeatureRegistry.FeatureBundle featureBundle = createMock(FeatureBundle.class);
+    expect(featureBundle.getResources()).andReturn(
+        ImmutableList.of(featureResourceMock)).anyTimes();
+    replay(featureBundle);
+    return featureBundle;
+  }
+
+  private ApiDirective mockApiDirective(boolean isExports, String value) {
+    ApiDirective result = createMock(ApiDirective.class);
+    expect(result.getType()).andReturn(ApiDirective.Type.JS).anyTimes();
+    expect(result.getValue()).andReturn(value).anyTimes();
+    expect(result.isExports()).andReturn(isExports).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  @Test
+  public void processEmpty() throws Exception {
+    JsUri jsUri = mockJsUri(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL);
+    JsRequest jsRequest = new JsRequest(jsUri, null, false, null);
+    JsResponseBuilder jsBuilder = new JsResponseBuilder();
+    boolean actualReturnCode = processor.process(jsRequest, jsBuilder);
+    assertTrue(actualReturnCode);
+    assertEquals("", jsBuilder.build().toJsString());
+  }
+
+  @Test
+  public void processWithOneText() throws Exception {
+    JsUri jsUri = mockJsUri(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL);
+    JsRequest jsRequest = new JsRequest(jsUri, null, false, null);
+    JsResponseBuilder jsBuilder = new JsResponseBuilder();
+    jsBuilder.appendJs(textJsContent1);
+    boolean actualReturnCode = processor.process(jsRequest, jsBuilder);
+    assertTrue(actualReturnCode);
+    assertEquals(
+        TEXT_CONTENT_1,
+        jsBuilder.build().toJsString());
+  }
+
+  @Test
+  public void processWithOneNonEmptyFeature() throws Exception {
+    JsUri jsUri = mockJsUri(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL);
+    JsRequest jsRequest = new JsRequest(jsUri, null, false, null);
+    JsResponseBuilder jsBuilder = new JsResponseBuilder();
+    jsBuilder.appendJs(featureJsContent1);
+    boolean actualReturnCode = processor.process(jsRequest, jsBuilder);
+    assertTrue(actualReturnCode);
+    assertEquals(
+        EXPORT_JS_DEB + FEATURE_CONTENT_1 + EXPORT_STRING_1,
+        jsBuilder.build().toJsString());
+  }
+
+  @Test
+  public void processWithOneEmptyFeature() throws Exception {
+    JsUri jsUri = mockJsUri(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL);
+    JsRequest jsRequest = new JsRequest(jsUri, null, false, null);
+    JsResponseBuilder jsBuilder = new JsResponseBuilder();
+    jsBuilder.appendJs(featureJsContent3);
+    boolean actualReturnCode = processor.process(jsRequest, jsBuilder);
+    assertTrue(actualReturnCode);
+    assertEquals(
+        FEATURE_CONTENT_3 + EXPORT_STRING_3,
+        jsBuilder.build().toJsString());
+  }
+
+  @Test
+  public void processWithFeaturesAndTexts() throws Exception {
+    JsUri jsUri = mockJsUri(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL);
+    JsRequest jsRequest = new JsRequest(jsUri, null, false, null);
+    JsResponseBuilder jsBuilder = new JsResponseBuilder();
+    jsBuilder.appendJs(textJsContent1);
+    jsBuilder.appendJs(featureJsContent1);
+    jsBuilder.appendJs(featureJsContent2);
+    jsBuilder.appendJs(textJsContent2);
+    jsBuilder.appendJs(featureJsContent3);
+    boolean actualReturnCode = processor.process(jsRequest, jsBuilder);
+    assertTrue(actualReturnCode);
+    assertEquals(EXPORT_JS_DEB + TEXT_CONTENT_1 +
+        FEATURE_CONTENT_1 + EXPORT_STRING_1 +
+        FEATURE_CONTENT_2 + EXPORT_STRING_2 +
+        TEXT_CONTENT_2 +
+        FEATURE_CONTENT_3 + EXPORT_STRING_3,
+        jsBuilder.build().toJsString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/GetJsContentProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/GetJsContentProcessorTest.java
new file mode 100644
index 0000000..07d2a59
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/GetJsContentProcessorTest.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.easymock.EasyMock.createControl;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.isNull;
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.features.DefaultFeatureRegistryProvider;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistry.LookupResult;
+import org.apache.shindig.gadgets.rewrite.js.JsCompiler;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.apache.shindig.gadgets.features.ApiDirective;
+
+/**
+ * Tests for {@link GetJsContentProcessor}.
+ */
+public class GetJsContentProcessorTest {
+  private static final String JS_CODE1 = "js1";
+  private static final String JS_CODE2 = "js2";
+
+  private static final List<String> EMPTY_STRING_LIST = ImmutableList.<String>of();
+  private static final List<FeatureBundle> EMPTY_BUNDLE_LIST = ImmutableList.<FeatureBundle>of();
+
+  private IMocksControl control;
+  private FeatureRegistry registry;
+  private JsCompiler compiler;
+  private JsUri jsUri;
+  private JsRequest request;
+  private JsResponseBuilder response;
+  private GetJsContentProcessor processor;
+
+  @Before
+  public void setUp() {
+    control = createControl();
+    registry = control.createMock(FeatureRegistry.class);
+    compiler = control.createMock(JsCompiler.class);
+    jsUri = control.createMock(JsUri.class);
+    expect(jsUri.getRepository()).andStubReturn(null);
+    request = control.createMock(JsRequest.class);
+    response = new JsResponseBuilder();
+    processor = new GetJsContentProcessor(new DefaultFeatureRegistryProvider(registry), compiler);
+    processor.setVersionedMaxAge(GetJsContentProcessor.DEFAULT_VERSIONED_MAXAGE);
+    processor.setUnversionedMaxAge(GetJsContentProcessor.DEFAULT_UNVERSIONED_MAXAGE);
+    processor.setInvalidMaxAge(GetJsContentProcessor.DEFAULT_INVALID_MAXAGE);
+  }
+
+  @Test
+  public void testPopulatesResponseForUnversionedRequest() throws Exception {
+    setupForVersionAndProxy(true, UriStatus.VALID_UNVERSIONED);
+    control.replay();
+    processor.process(request, response);
+    checkResponse(true, GetJsContentProcessor.DEFAULT_UNVERSIONED_MAXAGE, JS_CODE1 + JS_CODE2, "");
+    control.verify();
+  }
+
+  @Test
+  public void testPopulatesResponseForVersionedRequest() throws Exception {
+    setupForVersionAndProxy(true, UriStatus.VALID_VERSIONED);
+    control.replay();
+    processor.process(request, response);
+    checkResponse(true, GetJsContentProcessor.DEFAULT_VERSIONED_MAXAGE, JS_CODE1 + JS_CODE2, "");
+    control.verify();
+  }
+
+  @Test
+  public void testPopulatesResponseForInvalidVersion() throws Exception {
+    setupForVersionAndProxy(true, UriStatus.INVALID_VERSION);
+    control.replay();
+    processor.process(request, response);
+    checkResponse(true, GetJsContentProcessor.DEFAULT_INVALID_MAXAGE, JS_CODE1 + JS_CODE2, "");
+    control.verify();
+  }
+
+  @Test
+  public void testPopulatesResponseForNoProxyCacheable() throws Exception {
+    setupForVersionAndProxy(false, UriStatus.VALID_UNVERSIONED);
+    control.replay();
+    processor.process(request, response);
+    checkResponse(false, GetJsContentProcessor.DEFAULT_UNVERSIONED_MAXAGE, JS_CODE1 + JS_CODE2, "");
+    control.verify();
+  }
+
+  @Test
+  public void testPopulateWithLoadedFeatures() throws Exception {
+    List<String> reqLibs = ImmutableList.of("feature1", "feature2");
+    List<String> loadLibs = ImmutableList.of("feature2");
+
+    FeatureResource resource1 = mockResource(true);
+    FeatureBundle bundle1 = mockBundle("feature1", null, null, Lists.newArrayList(resource1));
+    FeatureBundle bundle2 = mockBundle("feature2", "export2", "extern2", null);
+
+    setupJsUriAndRegistry(UriStatus.VALID_UNVERSIONED,
+        reqLibs, ImmutableList.of(bundle1, bundle2),
+        loadLibs, ImmutableList.of(bundle2));
+
+    expect(compiler.getJsContent(jsUri, bundle1))
+      .andReturn(ImmutableList.<JsContent>of(
+          JsContent.fromFeature(JS_CODE1, "source1", null, null)));
+
+    control.replay();
+    processor.process(request, response);
+    checkResponse(true, GetJsContentProcessor.DEFAULT_UNVERSIONED_MAXAGE, JS_CODE1, "export2", "extern2");
+    control.verify();
+  }
+
+  private void setupForVersionAndProxy(boolean proxyCacheable, UriStatus uriStatus) {
+    List<String> reqLibs = ImmutableList.of("feature");
+    List<String> loadLibs = EMPTY_STRING_LIST;
+
+    FeatureResource resource1 = mockResource(proxyCacheable);
+    FeatureResource resource2 = mockResource(proxyCacheable);
+    FeatureBundle bundle = mockBundle("feature", null, null,
+        Lists.newArrayList(resource1, resource2));
+
+    setupJsUriAndRegistry(uriStatus,
+        reqLibs, Lists.newArrayList(bundle),
+        loadLibs, EMPTY_BUNDLE_LIST);
+
+    expect(compiler.getJsContent(jsUri, bundle))
+        .andReturn(ImmutableList.<JsContent>of(
+            JsContent.fromFeature(JS_CODE1, "source1", bundle, resource1),
+            JsContent.fromFeature(JS_CODE2, "source2", bundle, resource2)));
+  }
+
+  @SuppressWarnings("unchecked")
+  private void setupJsUriAndRegistry(UriStatus uriStatus,
+      List<String> reqLibs, List<FeatureBundle> reqLookupBundles,
+      List<String> loadLibs, List<FeatureBundle> loadLookupBundles) {
+
+    expect(jsUri.getStatus()).andReturn(uriStatus);
+    expect(jsUri.getContainer()).andReturn("container");
+    expect(jsUri.getContext()).andReturn(RenderingContext.CONFIGURED_GADGET);
+    expect(jsUri.isDebug()).andReturn(false);
+    expect(jsUri.getLibs()).andReturn(reqLibs);
+    expect(jsUri.getLoadedLibs()).andReturn(loadLibs);
+
+    expect(request.getJsUri()).andReturn(jsUri);
+
+    LookupResult reqLookup = mockLookupResult(reqLookupBundles);
+    LookupResult loadLookup = mockLookupResult(loadLookupBundles);
+
+    expect(registry.getFeatureResources(isA(JsGadgetContext.class), eq(reqLibs),
+        isNull(List.class))).andReturn(reqLookup);
+    expect(registry.getFeatureResources(isA(JsGadgetContext.class), eq(loadLibs),
+        isNull(List.class))).andReturn(loadLookup);
+  }
+
+  private LookupResult mockLookupResult(List<FeatureBundle> bundles) {
+    LookupResult result = control.createMock(LookupResult.class);
+    expect(result.getBundles()).andReturn(bundles);
+    return result;
+  }
+
+  private FeatureResource mockResource(boolean proxyCacheable) {
+    FeatureResource result = control.createMock(FeatureResource.class);
+    expect(result.isProxyCacheable()).andReturn(proxyCacheable).anyTimes();
+    return result;
+  }
+
+  private FeatureBundle mockBundle(String name, String export, String extern, List<FeatureResource> resources) {
+    FeatureBundle result = control.createMock(FeatureBundle.class);
+    expect(result.getName()).andReturn(name).anyTimes();
+    if (export != null) {
+      expect(result.getApis(ApiDirective.Type.JS, true)).andReturn(ImmutableList.of(export));
+    }
+    if (extern != null) {
+      expect(result.getApis(ApiDirective.Type.JS, false)).andReturn(ImmutableList.of(extern));
+    }
+    if (resources != null) {
+      expect(result.getResources()).andReturn(resources);
+    }
+    return result;
+  }
+
+  private void checkResponse(boolean proxyCacheable, int expectedTtl,
+      String jsString, String... externs) {
+    assertEquals(proxyCacheable, response.isProxyCacheable());
+    assertEquals(expectedTtl, response.getCacheTtlSecs());
+    assertEquals(jsString, response.build().toJsString());
+    for (String extern : externs) {
+      assertTrue(response.build().getExterns().contains(extern));
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/IfModifiedSinceProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/IfModifiedSinceProcessorTest.java
new file mode 100644
index 0000000..b72cccd
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/IfModifiedSinceProcessorTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.*;
+
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Tests for {@link IfModifiedSinceProcessor}.
+ */
+public class IfModifiedSinceProcessorTest {
+
+  private IMocksControl control;
+  private JsUri jsUri;
+  private JsRequest request;
+  private JsResponseBuilder response;
+  private IfModifiedSinceProcessor processor;
+
+  @Before
+  public void setUp() {
+    control = EasyMock.createControl();
+    jsUri = control.createMock(JsUri.class);
+    request = control.createMock(JsRequest.class);
+    response = new JsResponseBuilder();
+    processor = new IfModifiedSinceProcessor();
+  }
+
+  @Test
+  public void testDoesNothingAndContinuesProcessingWhenHeaderIsAbsent() throws Exception {
+    EasyMock.expect(request.isInCache()).andReturn(false);
+    control.replay();
+    assertTrue(processor.process(request, response));
+    control.verify();
+  }
+
+  @Test
+  public void testDoesNothingAndContinuesProcessingWhenNotVersioned() throws Exception {
+    EasyMock.expect(request.isInCache()).andReturn(true);
+    EasyMock.expect(request.getJsUri()).andReturn(jsUri);
+    EasyMock.expect(jsUri.getStatus()).andReturn(UriStatus.VALID_UNVERSIONED);
+    control.replay();
+    assertTrue(processor.process(request, response));
+    control.verify();
+  }
+
+  @Test
+  public void testReturnsNotModifiedAndStopsProcessingWithHeaderAndVersion() throws Exception {
+    EasyMock.expect(request.isInCache()).andReturn(true);
+    EasyMock.expect(request.getJsUri()).andReturn(jsUri);
+    EasyMock.expect(jsUri.getStatus()).andReturn(UriStatus.VALID_VERSIONED);
+    control.replay();
+    assertFalse(processor.process(request, response));
+    assertEquals(HttpServletResponse.SC_NOT_MODIFIED, response.getStatusCode());
+    control.verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsLoadProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsLoadProcessorTest.java
new file mode 100644
index 0000000..54bbbbb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsLoadProcessorTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.*;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Tests for {@link JsLoadProcessor}.
+ */
+public class JsLoadProcessorTest {
+
+  private static final String ONLOAD_FUNCTION = "onloadFunc";
+
+  private IMocksControl control;
+  private JsRequest request;
+  private JsUriManager jsUriManager;
+  private JsUri jsUri;
+  private Uri uri;
+  private JsResponseBuilder response;
+  private JsLoadProcessor processor;
+
+  @Before
+  public void setUp() {
+    control = EasyMock.createControl();
+    request = control.createMock(JsRequest.class);
+    jsUriManager = control.createMock(JsUriManager.class);
+    jsUri = control.createMock(JsUri.class);
+    uri = Uri.parse("http://example.org/foo.xml");
+    response = new JsResponseBuilder();
+    processor = new JsLoadProcessor(jsUriManager, 1234, true);
+
+    EasyMock.expect(request.getJsUri()).andReturn(jsUri);
+  }
+
+  @Test
+  public void testSkipsWhenNoJsLoad() throws Exception {
+    EasyMock.expect(jsUri.isJsload()).andReturn(false);
+    response = control.createMock(JsResponseBuilder.class);
+    control.replay();
+    assertTrue(processor.process(request, response));
+    control.verify();
+  }
+
+  @Test
+  public void testFailsWhenNoOnloadIsSpecified() throws Exception {
+    EasyMock.expect(jsUri.isJsload()).andReturn(true);
+    EasyMock.expect(jsUri.getOnload()).andReturn(null);
+    control.replay();
+    try {
+      processor.process(request, response);
+      fail("A JsException should have been thrown by the processor.");
+    } catch (JsException e) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getStatusCode());
+      assertEquals(JsLoadProcessor.JSLOAD_ONLOAD_ERROR, e.getMessage());
+    }
+    control.verify();
+  }
+
+  @Test
+  public void testGeneratesLoaderCodeWithNoCache() throws Exception {
+    setExpectations(true, null);
+    control.replay();
+    checkGeneratedCode(0);
+    control.verify();
+  }
+
+  @Test
+  public void testGeneratesLoaderCodeWithDefaultTtl() throws Exception {
+    setExpectations(false, null);
+    control.replay();
+    checkGeneratedCode(1234);
+    control.verify();
+  }
+
+  @Test
+  public void testGeneratesLoaderCodeWithRefresh() throws Exception {
+    setExpectations(false, 300);
+    control.replay();
+    checkGeneratedCode(300);
+    control.verify();
+  }
+
+  private void setExpectations(boolean noCache, Integer refresh) {
+    EasyMock.expect(jsUri.isJsload()).andReturn(true);
+    EasyMock.expect(jsUri.getOnload()).andReturn(ONLOAD_FUNCTION);
+    jsUri.setJsload(false);
+    jsUri.setNohint(true);
+    EasyMock.expect(jsUriManager.makeExternJsUri(jsUri)).andReturn(uri);
+    EasyMock.expect(jsUri.isNoCache()).andReturn(noCache);
+    if (!noCache) {
+      EasyMock.expect(jsUri.getRefresh()).andReturn(refresh);
+    }
+  }
+
+  private void checkGeneratedCode(int expectedTtl) throws JsException {
+    assertFalse(processor.process(request, response));
+    assertEquals(HttpServletResponse.SC_OK, response.getStatusCode());
+    assertEquals(expectedTtl, response.getCacheTtlSecs());
+    String expectedBody = String.format(JsLoadProcessor.JSLOAD_JS_TPL,
+        uri.toString() + "?jsload=0");
+    assertEquals(expectedBody, response.build().toJsString());
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsRequestBuilderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsRequestBuilderTest.java
new file mode 100644
index 0000000..1ec519b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsRequestBuilderTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetException.Code;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.caja.util.Lists;
+
+import javax.servlet.http.HttpServletRequest;
+
+
+/**
+ * Tests for {@link JsRequestBuilder}.
+ */
+public class JsRequestBuilderTest {
+  private static final String HOST_HEADER_KEY = "Host";
+  private static final String IMS_HEADER_KEY = "If-Modified-Since";
+  private static final String HOST = "localhost";
+
+  private IMocksControl control;
+  private JsUriManager jsUriManager;
+  private JsUri jsUri;
+  private HttpServletRequest request;
+  private JsRequestBuilder builder;
+  private FeatureRegistry registry;
+
+  @Before
+  public void setUp() {
+    control = EasyMock.createControl();
+    jsUriManager = control.createMock(JsUriManager.class);
+    jsUri = control.createMock(JsUri.class);
+    request = control.createMock(HttpServletRequest.class);
+    registry = control.createMock(FeatureRegistry.class);
+    builder = new JsRequestBuilder(jsUriManager, registry);
+
+    EasyMock.expect(request.getScheme()).andReturn("http");
+    EasyMock.expect(request.getServerPort()).andReturn(80);
+    EasyMock.expect(request.getServerName()).andReturn("HOST");
+    EasyMock.expect(request.getRequestURI()).andReturn("/foo");
+    EasyMock.expect(request.getQueryString()).andReturn("");
+  }
+
+  @Test
+  public void testCreateRequestNotInCache() throws Exception {
+    EasyMock.expect(jsUriManager.processExternJsUri(EasyMock.isA(Uri.class))).andReturn(jsUri);
+    EasyMock.expect(request.getHeader(HOST_HEADER_KEY)).andReturn(HOST);
+    EasyMock.expect(request.getHeader(IMS_HEADER_KEY)).andReturn(null);
+    control.replay();
+    JsRequest jsRequest = builder.build(request);
+    control.verify();
+    assertSame(jsUri, jsRequest.getJsUri());
+    assertEquals(HOST, jsRequest.getHost());
+    assertFalse(jsRequest.isInCache());
+  }
+
+  @Test
+  public void testCreateRequestInCache() throws Exception {
+    EasyMock.expect(jsUriManager.processExternJsUri(EasyMock.isA(Uri.class))).andReturn(jsUri);
+    EasyMock.expect(request.getHeader(HOST_HEADER_KEY)).andReturn(HOST);
+    EasyMock.expect(request.getHeader(IMS_HEADER_KEY)).andReturn("today");
+    control.replay();
+    JsRequest jsRequest = builder.build(request);
+    control.verify();
+    assertSame(jsUri, jsRequest.getJsUri());
+    assertEquals(HOST, jsRequest.getHost());
+    assertTrue(jsRequest.isInCache());
+  }
+
+  @Test
+  public void testCreateRequestThrowsExceptionOnParseError() throws Exception {
+    EasyMock.expect(jsUriManager.processExternJsUri(EasyMock.isA(Uri.class))).andThrow(
+        new GadgetException(Code.INVALID_PARAMETER));
+    control.replay();
+    try {
+      builder.build(request);
+      fail("Should have thrown a GadgetException.");
+    } catch (GadgetException e) {
+      // pass
+    }
+    control.verify();
+  }
+
+  @Test
+  public void testCreateRequestComputesDeps() throws Exception {
+    List<String> requested = Lists.newArrayList("req1", "req2");
+    List<String> loaded = Lists.newArrayList("load1", "load2");
+    List<String> fullClosure =
+        Lists.newArrayList("dep-s1", "dep1", "dep2", "dep-s2", "load1", "load2", "req1", "req2");
+    List<String> loadedClosure =
+        Lists.newArrayList("dep-s1", "dep-s2", "load1", "load2");
+    EasyMock.expect(registry.getFeatures(requested)).andReturn(fullClosure);
+    EasyMock.expect(registry.getFeatures(loaded)).andReturn(loadedClosure);
+    EasyMock.expect(jsUri.getLibs()).andReturn(requested);
+    EasyMock.expect(jsUri.getLoadedLibs()).andReturn(loaded);
+    EasyMock.expect(jsUriManager.processExternJsUri(EasyMock.isA(Uri.class))).andReturn(jsUri);
+    EasyMock.expect(request.getHeader(IMS_HEADER_KEY)).andReturn(null);
+    EasyMock.expect(request.getHeader(HOST_HEADER_KEY)).andReturn(HOST);
+    control.replay();
+    JsRequest jsRequest = builder.build(request);
+    assertSame(jsUri, jsRequest.getJsUri());
+    assertEquals(HOST, jsRequest.getHost());
+
+    List<String> allMatch =
+        Lists.newArrayList("dep-s1", "dep1", "dep2", "dep-s2", "load1", "load2", "req1", "req2");
+    assertEquals(allMatch, jsRequest.getAllFeatures());
+
+    List<String> loadedMatch =
+        Lists.newArrayList("dep-s1", "dep-s2", "load1", "load2");
+    assertEquals(loadedMatch, jsRequest.getLoadedFeatures());
+
+    List<String> newMatch =
+        Lists.newArrayList("dep1", "dep2", "req1", "req2");
+    assertEquals(newMatch, jsRequest.getNewFeatures());
+
+    // Verify calls at the end, since they're made lazily in the context of .getFeatures() calls.
+    control.verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsResponseBuilderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsResponseBuilderTest.java
new file mode 100644
index 0000000..18a1c84
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/JsResponseBuilderTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.junit.Assert.*;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Iterator;
+
+public class JsResponseBuilderTest {
+
+  private JsResponseBuilder builder;
+
+  @Before
+  public void setUp() {
+    builder = new JsResponseBuilder();
+  }
+
+  @Test
+  public void testExterns() throws Exception {
+    builder.appendExtern("b");
+    builder.appendExtern("b");
+    builder.appendExtern("c.d");
+    builder.appendExtern("c.d");
+    builder.appendExtern("e.prototype.f");
+    builder.appendExtern("e.prototype.f");
+    builder.appendRawExtern("var a");
+    String eee = builder.build().getExterns();
+    assertEquals(
+        "var a;\n" +
+        "var b = {};\n" +
+        "var c = {};\nc.d = {};\n" +
+        "var e = {};\ne.prototype.f = {};\n",
+        builder.build().getExterns());
+  }
+
+  @Test
+  public void skipsEmptyContent() throws Exception {
+    builder.appendJs("number 1", "num1");
+    builder.appendJs("", "num2");
+    builder.appendJs("number 3", "num3");
+    builder.prependJs("number 4", "num4");
+    builder.prependJs("", "num5");
+    Iterator<JsContent> allJsContent = builder.build().getAllJsContent().iterator();
+    assertEquals("num4", allJsContent.next().getSource());
+    assertEquals("num1", allJsContent.next().getSource());
+    assertEquals("num3", allJsContent.next().getSource());
+    assertFalse(allJsContent.hasNext());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/SeparatorCommentingProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/SeparatorCommentingProcessorTest.java
new file mode 100644
index 0000000..fe45875
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/js/SeparatorCommentingProcessorTest.java
@@ -0,0 +1,177 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.js;
+
+import static org.easymock.EasyMock.createControl;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class SeparatorCommentingProcessorTest {
+  private static final List<String> ERRORS = ImmutableList.<String>of();
+
+  private IMocksControl control;
+  private SeparatorCommentingProcessor processor;
+  private JsResponse response;
+
+  @Before
+  public void setUp() {
+    control = createControl();
+    processor = new SeparatorCommentingProcessor();
+  }
+
+  @Test
+  public void testNoFeature() throws Exception {
+    JsResponseBuilder builder = newBuilder();
+
+    control.replay();
+    boolean actualReturn = processor.process(null, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals("", actualResponse.toJsString());
+  }
+
+  @Test
+  public void testOneFeature() throws Exception {
+    JsContent js = JsContent.fromFeature("content", "source", mockBundle("bundle"), null);
+    JsResponseBuilder builder = newBuilder(js);
+
+    control.replay();
+    boolean actualReturn = processor.process(null, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals(
+        "\n/* [start] feature=bundle */\n" +
+        "content" +
+        "\n/* [end] feature=bundle */\n",
+        actualResponse.toJsString());
+  }
+
+  @Test
+  public void testOneText() throws Exception {
+    JsContent text1 = JsContent.fromText("text1", "source");
+    JsResponseBuilder builder = newBuilder(text1);
+
+    control.replay();
+    boolean actualReturn = processor.process(null, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals("text1", actualResponse.toJsString());
+  }
+
+  @Test
+  public void testMultipleFeaturesWithoutInBetweenTexts() throws Exception {
+    JsContent js1 = JsContent.fromFeature("content1", "source1", mockBundle("bundle1"), null);
+    JsContent js2 = JsContent.fromFeature("content2", "source2", mockBundle("bundle2"), null);
+    JsResponseBuilder builder = newBuilder(js1, js2);
+
+    control.replay();
+    boolean actualReturn = processor.process(null, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals(
+        "\n/* [start] feature=bundle1 */\n" +
+        "content1" +
+        "\n/* [end] feature=bundle1 */\n" +
+        "\n/* [start] feature=bundle2 */\n" +
+        "content2" +
+        "\n/* [end] feature=bundle2 */\n",
+        actualResponse.toJsString());
+  }
+
+  @Test
+  public void testNeighboringSameFeatures() throws Exception {
+    FeatureBundle bundle = mockBundle("bundle");
+    JsContent js1 = JsContent.fromFeature("content1", "source1", bundle, null);
+    JsContent js2 = JsContent.fromFeature("content2", "source2", bundle, null);
+    JsResponseBuilder builder = newBuilder(js1, js2);
+
+    control.replay();
+    boolean actualReturn = processor.process(null, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals(
+        "\n/* [start] feature=bundle */\n" +
+        "content1" +
+        "content2" +
+        "\n/* [end] feature=bundle */\n",
+        actualResponse.toJsString());
+  }
+
+  @Test
+  public void testMultipleFeaturesWithInBetweenTexts() throws Exception {
+    JsContent text1 = JsContent.fromText("text1", "source1");
+    JsContent text2 = JsContent.fromText("text2", "source2");
+    JsContent text3 = JsContent.fromText("text3", "source3");
+    JsContent js1 = JsContent.fromFeature("content1", "source4", mockBundle("bundle1"), null);
+    JsContent js2 = JsContent.fromFeature("content2", "source5", mockBundle("bundle2"), null);
+    JsResponseBuilder builder = newBuilder(text1, js1, text2, js2, text3);
+
+    control.replay();
+    boolean actualReturn = processor.process(null, builder);
+    JsResponse actualResponse = builder.build();
+
+    control.verify();
+    assertTrue(actualReturn);
+    assertEquals(
+        "text1" +
+        "\n/* [start] feature=bundle1 */\n" +
+        "content1" +
+        "\n/* [end] feature=bundle1 */\n" +
+        "text2" +
+        "\n/* [start] feature=bundle2 */\n" +
+        "content2" +
+        "\n/* [end] feature=bundle2 */\n" +
+        "text3",
+        actualResponse.toJsString());
+  }
+
+  private JsResponseBuilder newBuilder(JsContent... contents) {
+    response = new JsResponse(Lists.newArrayList(contents),
+        -1, -1, false, ERRORS, null);
+    return new JsResponseBuilder(response);
+  }
+
+  private FeatureBundle mockBundle(String name) {
+    FeatureBundle result = control.createMock(FeatureBundle.class);
+    expect(result.getName()).andReturn(name).anyTimes();
+    return result;
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTest.java
new file mode 100644
index 0000000..e94033d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTest.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthServiceProvider;
+import net.oauth.signature.RSA_SHA1;
+
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret.KeyType;
+import org.apache.shindig.gadgets.oauth.OAuthStore.ConsumerInfo;
+import org.apache.shindig.gadgets.oauth.OAuthStore.TokenInfo;
+import org.junit.Before;
+import org.junit.Test;
+
+public class BasicOAuthStoreTest {
+
+  private static final String SAMPLE_FILE =
+      '{' +
+    "'http://localhost:8080/gadgets/oauth.xml' : {" +
+    "'' : {" +
+    "'consumer_key' : 'gadgetConsumer'," +
+    "'consumer_secret' : 'gadgetSecret'," +
+    "'key_type' : 'HMAC_SYMMETRIC'" +
+          '}' +
+    "}," +
+    "'http://rsagadget/test.xml' : {" +
+    "'' : {" +
+    "'consumer_key' : 'rsaconsumer'," +
+    "'consumer_secret' : 'rsaprivate'," +
+    "'callback_url' : 'callback'," +
+    "'key_type' : 'RSA_PRIVATE'" +
+          '}' +
+          '}' +
+
+          '}';
+
+  private BasicOAuthStore store;
+
+  @Before
+  public void setUp() throws Exception {
+    store = new BasicOAuthStore();
+    store.initFromConfigString(SAMPLE_FILE);
+    store.setDefaultCallbackUrl("default callback");
+  }
+
+  @Test
+  public void testInit() throws Exception {
+    FakeGadgetToken t = new FakeGadgetToken();
+    t.setAppUrl("http://localhost:8080/gadgets/oauth.xml");
+    OAuthServiceProvider provider = new OAuthServiceProvider("req", "authorize", "access");
+    ConsumerInfo consumerInfo = store.getConsumerKeyAndSecret(t, "", provider);
+    OAuthConsumer consumer = consumerInfo.getConsumer();
+    assertEquals("gadgetConsumer", consumer.consumerKey);
+    assertEquals("gadgetSecret", consumer.consumerSecret);
+    assertEquals("HMAC-SHA1", consumer.getProperty("oauth_signature_method"));
+    assertEquals(provider, consumer.serviceProvider);
+    assertNull(consumerInfo.getKeyName());
+    assertEquals("default callback", consumerInfo.getCallbackUrl());
+
+    t.setAppUrl("http://rsagadget/test.xml");
+    consumerInfo = store.getConsumerKeyAndSecret(t, "", provider);
+    consumer = consumerInfo.getConsumer();
+    assertEquals("rsaconsumer", consumer.consumerKey);
+    assertNull(consumer.consumerSecret);
+    assertEquals("RSA-SHA1", consumer.getProperty("oauth_signature_method"));
+    assertEquals(provider, consumer.serviceProvider);
+    assertEquals("rsaprivate", consumer.getProperty(RSA_SHA1.PRIVATE_KEY));
+    assertNull(consumerInfo.getKeyName());
+    assertEquals("callback", consumerInfo.getCallbackUrl());
+  }
+
+  @Test
+  public void testGetAndSetAndRemoveToken() {
+    FakeGadgetToken t = new FakeGadgetToken();
+    ConsumerInfo consumer = new ConsumerInfo(null, null, null);
+    t.setAppUrl("http://localhost:8080/gadgets/oauth.xml");
+    t.setViewerId("viewer-one");
+    assertNull(store.getTokenInfo(t, consumer, "", ""));
+
+    TokenInfo info = new TokenInfo("token", "secret", null, 0);
+    store.setTokenInfo(t, consumer, "service", "token", info);
+
+    info = store.getTokenInfo(t, consumer, "service", "token");
+    assertEquals("token", info.getAccessToken());
+    assertEquals("secret", info.getTokenSecret());
+
+    FakeGadgetToken t2 = new FakeGadgetToken();
+    t2.setAppUrl("http://localhost:8080/gadgets/oauth.xml");
+    t2.setViewerId("viewer-two");
+    assertNull(store.getTokenInfo(t2, consumer, "service", "token"));
+
+    store.removeToken(t, consumer, "service", "token");
+    assertNull(store.getTokenInfo(t, consumer, "service", "token"));
+  }
+
+  @Test
+  public void testDefaultKey() throws Exception {
+    FakeGadgetToken t = new FakeGadgetToken();
+    t.setAppUrl("http://localhost:8080/not-in-store.xml");
+    OAuthServiceProvider provider = new OAuthServiceProvider("req", "authorize", "access");
+
+    try {
+      store.getConsumerKeyAndSecret(t, "", provider);
+      fail();
+    } catch (GadgetException e) {
+      // good
+    }
+
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret(
+        "somekey", "default", KeyType.RSA_PRIVATE, "keyname", null);
+    store.setDefaultKey(cks);
+
+    ConsumerInfo consumer = store.getConsumerKeyAndSecret(t, "", provider);
+    assertEquals("somekey", consumer.getConsumer().consumerKey);
+    assertNull(consumer.getConsumer().consumerSecret);
+    assertEquals("RSA-SHA1", consumer.getConsumer().getProperty("oauth_signature_method"));
+    assertEquals("default", consumer.getConsumer().getProperty(RSA_SHA1.PRIVATE_KEY));
+    assertEquals(provider, consumer.getConsumer().serviceProvider);
+    assertEquals("keyname", consumer.getKeyName());
+    assertEquals("default callback", consumer.getCallbackUrl());
+
+    cks = new BasicOAuthStoreConsumerKeyAndSecret(
+        "somekey", "default", KeyType.RSA_PRIVATE, "keyname", "callback");
+    store.setDefaultKey(cks);
+    consumer = store.getConsumerKeyAndSecret(t, "", provider);
+    assertEquals("callback", consumer.getCallbackUrl());
+  }
+
+  @Test
+  public void testNullCallback() throws Exception {
+    store = new BasicOAuthStore();
+    store.initFromConfigString(SAMPLE_FILE);
+
+    FakeGadgetToken t = new FakeGadgetToken();
+    t.setAppUrl("http://localhost:8080/gadgets/oauth.xml");
+    OAuthServiceProvider provider = new OAuthServiceProvider("req", "authorize", "access");
+    ConsumerInfo consumerInfo = store.getConsumerKeyAndSecret(t, "", provider);
+    OAuthConsumer consumer = consumerInfo.getConsumer();
+    assertEquals("gadgetConsumer", consumer.consumerKey);
+    assertNull(consumerInfo.getKeyName());
+    assertNull(consumerInfo.getCallbackUrl());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTokenIndexTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTokenIndexTest.java
new file mode 100644
index 0000000..810ebf4
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/BasicOAuthStoreTokenIndexTest.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import junitx.extensions.EqualsHashCodeTestCase;
+import static junitx.framework.Assert.assertNotEquals;
+
+public class BasicOAuthStoreTokenIndexTest extends EqualsHashCodeTestCase {
+  public BasicOAuthStoreTokenIndexTest() { super("TestHashCodeEquals");}
+
+  protected Object createInstance() throws Exception {
+    BasicOAuthStoreTokenIndex eq =  new BasicOAuthStoreTokenIndex();
+    eq.setGadgetUri("http://www.example.com/foo");
+    eq.setModuleId(100000000);
+    eq.setServiceName("test");
+    eq.setUserId("abc");
+    return eq;
+  }
+
+  protected Object createNotEqualInstance() throws Exception {
+    return new BasicOAuthStoreTokenIndex();
+  }
+
+
+  public void testHashCode() {
+    BasicOAuthStoreTokenIndex eq1 = new BasicOAuthStoreTokenIndex();
+    BasicOAuthStoreTokenIndex eq2 = new BasicOAuthStoreTokenIndex();
+
+    // just be sure that our new hashcode method works
+    eq1.setModuleId(100);
+    eq2.setModuleId(200);
+    assertNotEquals(eq1.hashCode(), eq2.hashCode());
+
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/GadgetOAuthCallbackGeneratorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/GadgetOAuthCallbackGeneratorTest.java
new file mode 100644
index 0000000..556d6ca
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/GadgetOAuthCallbackGeneratorTest.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.reportMatcher;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.apache.shindig.auth.BasicSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.uri.OAuthUriManager;
+import org.easymock.IArgumentMatcher;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+public class GadgetOAuthCallbackGeneratorTest {
+
+  private static final String MAKE_REQUEST_URL = "http://renderinghost/gadgets/makeRequest";
+  private static final Uri DEST_URL = Uri.parse("http://www.example.com/destination");
+
+  private IMocksControl control;
+  private Processor processor;
+  private LockedDomainService lockedDomainService;
+  private OAuthUriManager oauthUriManager;
+  private BlobCrypter stateCrypter;
+  private SecurityToken securityToken;
+  private Gadget gadget;
+  private OAuthFetcherConfig fetcherConfig;
+  private OAuthResponseParams responseParams;
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createNiceControl();
+    processor = control.createMock(Processor.class);
+    lockedDomainService = control.createMock(LockedDomainService.class);
+    oauthUriManager = control.createMock(OAuthUriManager.class);
+    stateCrypter = new BasicBlobCrypter("1111111111111111111".getBytes());
+    securityToken = new BasicSecurityToken("viewer", "viewer", "app", "container.com",
+        "gadget", "0", "default", MAKE_REQUEST_URL, null);
+    gadget = control.createMock(Gadget.class);
+    fetcherConfig = new OAuthFetcherConfig(null, null, null, null, false);
+    responseParams = new OAuthResponseParams(null, null, null);
+  }
+
+  private GadgetOAuthCallbackGenerator getGenerator() {
+    return new GadgetOAuthCallbackGenerator(processor, lockedDomainService, oauthUriManager,
+        stateCrypter);
+  }
+
+  @Test
+  public void testWrongDomain() throws Exception {
+    HttpRequest request = new HttpRequest(DEST_URL);
+    request.setSecurityToken(securityToken);
+    request.setOAuthArguments(new OAuthArguments());
+    expect(processor.process(eqContext(securityToken, request.getOAuthArguments())))
+        .andReturn(gadget);
+    expect(lockedDomainService.isGadgetValidForHost("renderinghost", gadget, "default"))
+        .andReturn(false);
+
+    control.replay();
+
+    try {
+      getGenerator().generateCallback(fetcherConfig, "base", request, responseParams);
+      fail("Should have thrown");
+    } catch (OAuthRequestException e) {
+      assertEquals(OAuthError.UNKNOWN_PROBLEM.name(), e.getError());
+    }
+
+    control.verify();
+  }
+
+  @Test
+  public void testBadGadget() throws Exception {
+    HttpRequest request = new HttpRequest(DEST_URL);
+    request.setSecurityToken(securityToken);
+    request.setOAuthArguments(new OAuthArguments());
+    expect(processor.process(eqContext(securityToken, request.getOAuthArguments())))
+        .andThrow(new ProcessingException("doh", HttpServletResponse.SC_BAD_REQUEST));
+
+    control.replay();
+
+    try {
+      getGenerator().generateCallback(fetcherConfig, "base", request, responseParams);
+      fail("Should have thrown");
+    } catch (OAuthRequestException e) {
+      assertEquals(OAuthError.UNKNOWN_PROBLEM.name(), e.getError());
+    }
+
+    control.verify();
+  }
+
+  @Test
+  public void testGenerateUrl_schemeRelative() throws Exception {
+    HttpRequest request = new HttpRequest(DEST_URL);
+    request.setSecurityToken(securityToken);
+    request.setOAuthArguments(new OAuthArguments());
+    expect(processor.process(eqContext(securityToken, request.getOAuthArguments())))
+        .andReturn(gadget);
+    expect(lockedDomainService.isGadgetValidForHost("renderinghost", gadget, "default"))
+        .andReturn(true);
+    expect(oauthUriManager.makeOAuthCallbackUri("default", "renderinghost"))
+        .andReturn(Uri.parse("//renderinghost/final/callback"));
+
+    control.replay();
+
+    String callback = getGenerator().generateCallback(fetcherConfig, "http://base/basecallback",
+        request, responseParams);
+    Uri callbackUri = Uri.parse(callback);
+    assertEquals("http", callbackUri.getScheme());
+    assertEquals("base", callbackUri.getAuthority());
+    assertEquals("/basecallback", callbackUri.getPath());
+    OAuthCallbackState state = new OAuthCallbackState(stateCrypter,
+        callbackUri.getQueryParameter("cs"));
+    assertEquals("http://renderinghost/final/callback", state.getRealCallbackUrl());
+
+    control.verify();
+  }
+
+  @Test
+  public void testGenerateUrl_absolute() throws Exception {
+    HttpRequest request = new HttpRequest(DEST_URL);
+    request.setSecurityToken(securityToken);
+    request.setOAuthArguments(new OAuthArguments());
+    expect(processor.process(eqContext(securityToken, request.getOAuthArguments())))
+        .andReturn(gadget);
+    expect(lockedDomainService.isGadgetValidForHost("renderinghost", gadget, "default"))
+        .andReturn(true);
+    expect(oauthUriManager.makeOAuthCallbackUri("default", "renderinghost"))
+        .andReturn(Uri.parse("https://renderinghost/final/callback"));
+
+    control.replay();
+
+    String callback = getGenerator().generateCallback(fetcherConfig, "http://base/basecallback",
+        request, responseParams);
+    Uri callbackUri = Uri.parse(callback);
+    assertEquals("http", callbackUri.getScheme());
+    assertEquals("base", callbackUri.getAuthority());
+    assertEquals("/basecallback", callbackUri.getPath());
+    OAuthCallbackState state = new OAuthCallbackState(stateCrypter,
+        callbackUri.getQueryParameter("cs"));
+    assertEquals("https://renderinghost/final/callback", state.getRealCallbackUrl());
+
+    control.verify();
+  }
+
+  @Test
+  public void testGenerateUrl_otherQueryParams() throws Exception {
+    HttpRequest request = new HttpRequest(DEST_URL);
+    request.setSecurityToken(securityToken);
+    request.setOAuthArguments(new OAuthArguments());
+    expect(processor.process(eqContext(securityToken, request.getOAuthArguments())))
+        .andReturn(gadget);
+    expect(lockedDomainService.isGadgetValidForHost("renderinghost", gadget, "default"))
+        .andReturn(true);
+    expect(oauthUriManager.makeOAuthCallbackUri("default", "renderinghost"))
+        .andReturn(Uri.parse("https://renderinghost/final/callback"));
+
+    control.replay();
+
+    String callback = getGenerator().generateCallback(fetcherConfig,
+        "http://base/basecallback?foo=bar%20baz", request, responseParams);
+    Uri callbackUri = Uri.parse(callback);
+    assertEquals("http", callbackUri.getScheme());
+    assertEquals("base", callbackUri.getAuthority());
+    assertEquals("/basecallback", callbackUri.getPath());
+    assertEquals("bar baz", callbackUri.getQueryParameter("foo"));
+    OAuthCallbackState state = new OAuthCallbackState(stateCrypter,
+        callbackUri.getQueryParameter("cs"));
+    assertEquals("https://renderinghost/final/callback", state.getRealCallbackUrl());
+
+    control.verify();
+  }
+
+  @Test
+  public void testGenerateUrl_noGadgetDomainCallback() throws Exception {
+    HttpRequest request = new HttpRequest(DEST_URL);
+    request.setSecurityToken(securityToken);
+    request.setOAuthArguments(new OAuthArguments());
+    expect(processor.process(eqContext(securityToken, request.getOAuthArguments())))
+        .andReturn(gadget);
+    expect(lockedDomainService.isGadgetValidForHost("renderinghost", gadget, "default"))
+        .andReturn(true);
+    expect(oauthUriManager.makeOAuthCallbackUri("default", "renderinghost"))
+        .andReturn(null);
+
+    control.replay();
+
+    assertNull(getGenerator().generateCallback(fetcherConfig,
+        "http://base/basecallback?foo=bar%20baz", request, responseParams));
+
+    control.verify();
+  }
+
+  private GadgetContext eqContext(SecurityToken securityToken, OAuthArguments arguments) {
+    reportMatcher(new GadgetContextMatcher(securityToken, arguments));
+    return null;
+  }
+
+  private static class GadgetContextMatcher implements IArgumentMatcher {
+    private final SecurityToken securityToken;
+    private final OAuthArguments arguments;
+
+    public GadgetContextMatcher(SecurityToken securityToken, OAuthArguments arguments) {
+      this.securityToken = securityToken;
+      this.arguments = arguments;
+    }
+
+    public boolean matches(Object argument) {
+      if (!(argument instanceof OAuthGadgetContext)) {
+        return false;
+      }
+      OAuthGadgetContext context = (OAuthGadgetContext) argument;
+      return (securityToken == context.getToken() &&
+          arguments.getBypassSpecCache() == context.getIgnoreCache());
+  }
+
+    public void appendTo(StringBuffer buffer) {
+      buffer.append("GadgetContextMatcher(").append(securityToken).append(", ").append(arguments).append(')');
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/GadgetTokenStoreTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/GadgetTokenStoreTest.java
new file mode 100644
index 0000000..1b56d60
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/GadgetTokenStoreTest.java
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.gadgets.FakeGadgetSpecFactory;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.HttpMethod;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret.KeyType;
+import org.apache.shindig.gadgets.oauth.OAuthArguments.UseToken;
+import org.apache.shindig.gadgets.oauth.OAuthStore.TokenInfo;
+import org.apache.shindig.gadgets.oauth.testing.FakeOAuthServiceProvider;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.Assert;
+
+public class GadgetTokenStoreTest {
+
+  private static final String GADGET_URL = "http://www.example.com/gadget.xml";
+
+  public static final String DEFAULT_CALLBACK = "http://www.example.com/oauthcallback";
+
+  public static final String GADGET_SPEC =
+      "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
+      "  <Module>\n" +
+      "    <ModulePrefs title=\"hello world example\">\n" +
+      "   \n" +
+      "    <OAuth>\n" +
+      "      <Service name='testservice'>" +
+      "        <Access " +
+      "          url='" + FakeOAuthServiceProvider.ACCESS_TOKEN_URL + '\'' +
+      "          param_location='uri-query' " +
+      "          method='GET'" +
+      "        />" +
+      "        <Request " +
+      "          url='" + FakeOAuthServiceProvider.REQUEST_TOKEN_URL + '\'' +
+      "          param_location='uri-query' " +
+      "          method='GET'" +
+      "        />" +
+      "        <Authorization " +
+      "          url='" + FakeOAuthServiceProvider.APPROVAL_URL + '\'' +
+      "        />" +
+      "      </Service>" +
+      "    </OAuth>\n" +
+      "    </ModulePrefs>\n" +
+      "    <Content type=\"html\">\n" +
+      "       <![CDATA[\n" +
+      "         Hello, world!\n" +
+      "       ]]>\n" +
+      "       \n" +
+      "    </Content>\n" +
+      "  </Module>";
+
+  private BasicOAuthStore backingStore;
+  private GadgetOAuthTokenStore store;
+  private FakeGadgetToken socialToken;
+  private FakeGadgetToken privateToken;
+  private BlobCrypter stateCrypter;
+  private OAuthClientState clientState;
+  private OAuthResponseParams responseParams;
+  private OAuthFetcherConfig fetcherConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    backingStore = new BasicOAuthStore();
+    backingStore.setDefaultKey(new BasicOAuthStoreConsumerKeyAndSecret("key", "secret",
+        KeyType.RSA_PRIVATE, "keyname", null));
+    backingStore.setDefaultCallbackUrl(DEFAULT_CALLBACK);
+    store = new GadgetOAuthTokenStore(backingStore, new FakeGadgetSpecFactory());
+
+    socialToken = new FakeGadgetToken();
+    socialToken.setOwnerId("owner");
+    socialToken.setViewerId("viewer");
+    socialToken.setAppUrl(GADGET_URL);
+
+    privateToken = new FakeGadgetToken();
+    privateToken.setOwnerId("owner");
+    privateToken.setViewerId("owner");
+    privateToken.setAppUrl(GADGET_URL);
+
+    stateCrypter = new BasicBlobCrypter("abcdefghijklmnop".getBytes());
+    clientState = new OAuthClientState(stateCrypter);
+    responseParams = new OAuthResponseParams(socialToken, null, stateCrypter);
+    fetcherConfig = new OAuthFetcherConfig(stateCrypter, store, new FakeTimeSource(), null, false);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_signedFetch() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setUseToken(UseToken.NEVER);
+    AccessorInfo info = store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    assertEquals("keyname", info.getConsumer().getKeyName());
+    assertEquals("key", info.getConsumer().getConsumer().consumerKey);
+    assertNull(info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_useToken_noOAuthInSpec() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setUseToken(UseToken.IF_AVAILABLE);
+    try {
+      store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+      fail();
+    } catch (OAuthRequestException e) {
+      assertEquals("BAD_OAUTH_CONFIGURATION", e.getError());
+    }
+  }
+
+  @Test
+  public void testGetOAuthAccessor_signedFetch_hmacKey() throws Exception {
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(GADGET_URL);
+    index.setServiceName("hmac");
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret("hmac",
+        "hmacsecret", KeyType.HMAC_SYMMETRIC, null, null);
+    backingStore.setConsumerKeyAndSecret(index, cks);
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setUseToken(UseToken.NEVER);
+    arguments.setServiceName("hmac");
+    AccessorInfo info = store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    Assert.assertNull(info.getConsumer().getKeyName());
+    assertEquals("hmac", info.getConsumer().getConsumer().consumerKey);
+    assertEquals("hmacsecret", info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_signedFetch_badServiceName() throws Exception {
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(GADGET_URL);
+    index.setServiceName("otherservice");
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret("hmac",
+        "hmacsecret", KeyType.HMAC_SYMMETRIC, null, null);
+    backingStore.setConsumerKeyAndSecret(index, cks);
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setUseToken(UseToken.NEVER);
+    arguments.setServiceName("hmac");
+    AccessorInfo info = store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+    assertEquals("keyname", info.getConsumer().getKeyName());
+    assertEquals("key", info.getConsumer().getConsumer().consumerKey);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_signedFetch_defaultHmac() throws Exception {
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(GADGET_URL);
+    index.setServiceName("");
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret("hmac",
+        "hmacsecret", KeyType.HMAC_SYMMETRIC, null, null);
+    backingStore.setConsumerKeyAndSecret(index, cks);
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setUseToken(UseToken.NEVER);
+    AccessorInfo info = store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    Assert.assertNull(info.getConsumer().getKeyName());
+    assertEquals("hmac", info.getConsumer().getConsumer().consumerKey);
+    assertEquals("hmacsecret", info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_socialOAuth_socialPage() throws Exception {
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(GADGET_URL);
+    index.setServiceName("testservice");
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret("hmac",
+        "hmacsecret", KeyType.HMAC_SYMMETRIC, null, null);
+    backingStore.setConsumerKeyAndSecret(index, cks);
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.IF_AVAILABLE);
+    AccessorInfo info = store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    Assert.assertNull(info.getConsumer().getKeyName());
+    assertEquals("hmac", info.getConsumer().getConsumer().consumerKey);
+    assertEquals("hmacsecret", info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_socialOAuth_privatePage() throws Exception {
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(GADGET_URL);
+    index.setServiceName("testservice");
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret("hmac",
+        "hmacsecret", KeyType.HMAC_SYMMETRIC, null, null);
+    backingStore.setConsumerKeyAndSecret(index, cks);
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.IF_AVAILABLE);
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    Assert.assertNull(info.getConsumer().getKeyName());
+    assertEquals("hmac", info.getConsumer().getConsumer().consumerKey);
+    assertEquals("hmacsecret", info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_socialOAuth_withToken() throws Exception {
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(GADGET_URL);
+    index.setServiceName("testservice");
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret("hmac",
+        "hmacsecret", KeyType.HMAC_SYMMETRIC, null, null);
+    backingStore.setConsumerKeyAndSecret(index, cks);
+
+    backingStore.setTokenInfo(privateToken, null, "testservice", "",
+        new TokenInfo("token", "secret", null, 0));
+
+    // Owner views their own page
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.IF_AVAILABLE);
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    Assert.assertNull(info.getConsumer().getKeyName());
+    assertEquals("hmac", info.getConsumer().getConsumer().consumerKey);
+    assertEquals("hmacsecret", info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertEquals("token", info.getAccessor().accessToken);
+    assertEquals("secret", info.getAccessor().tokenSecret);
+
+    // Friend views page
+    info = store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    Assert.assertNull(info.getConsumer().getKeyName());
+    assertEquals("hmac", info.getConsumer().getConsumer().consumerKey);
+    assertEquals("hmacsecret", info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_fullOAuth_socialPage() throws Exception {
+    BasicOAuthStoreConsumerIndex index = new BasicOAuthStoreConsumerIndex();
+    index.setGadgetUri(GADGET_URL);
+    index.setServiceName("testservice");
+    BasicOAuthStoreConsumerKeyAndSecret cks = new BasicOAuthStoreConsumerKeyAndSecret("hmac",
+        "hmacsecret", KeyType.HMAC_SYMMETRIC, null, null);
+    backingStore.setConsumerKeyAndSecret(index, cks);
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    AccessorInfo info = store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+    assertEquals(OAuthParamLocation.URI_QUERY, info.getParamLocation());
+    Assert.assertNull(info.getConsumer().getKeyName());
+    assertEquals("hmac", info.getConsumer().getConsumer().consumerKey);
+    assertEquals("hmacsecret", info.getConsumer().getConsumer().consumerSecret);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testGetOAuthAccessor_serviceNotFound() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("no such service");
+    arguments.setUseToken(UseToken.ALWAYS);
+    try {
+      store.getOAuthAccessor(socialToken, arguments, clientState, responseParams, fetcherConfig);
+      fail();
+    } catch (OAuthRequestException e) {
+      assertEquals("BAD_OAUTH_CONFIGURATION", e.getError());
+    }
+  }
+
+  @Test
+  public void testGetOAuthAccessor_oauthParamsInBody() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    privateToken.setAppUrl("http://www.example.com/body.xml");
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertEquals(
+        FakeOAuthServiceProvider.REQUEST_TOKEN_URL,
+        info.getConsumer().getConsumer().serviceProvider.requestTokenURL);
+    assertEquals(
+        FakeOAuthServiceProvider.APPROVAL_URL,
+        info.getConsumer().getConsumer().serviceProvider.userAuthorizationURL);
+    assertEquals(
+        FakeOAuthServiceProvider.ACCESS_TOKEN_URL,
+        info.getConsumer().getConsumer().serviceProvider.accessTokenURL);
+    assertEquals(HttpMethod.POST, info.getHttpMethod());
+    assertEquals(OAuthParamLocation.POST_BODY, info.getParamLocation());
+  }
+
+  @Test
+  public void testGetOAuthAccessor_oauthParamsInHeader() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    privateToken.setAppUrl("http://www.example.com/header.xml");
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertEquals(
+        FakeOAuthServiceProvider.REQUEST_TOKEN_URL,
+        info.getConsumer().getConsumer().serviceProvider.requestTokenURL);
+    assertEquals(
+        FakeOAuthServiceProvider.APPROVAL_URL,
+        info.getConsumer().getConsumer().serviceProvider.userAuthorizationURL);
+    assertEquals(
+        FakeOAuthServiceProvider.ACCESS_TOKEN_URL,
+        info.getConsumer().getConsumer().serviceProvider.accessTokenURL);
+    assertEquals(HttpMethod.GET, info.getHttpMethod());
+    assertEquals(OAuthParamLocation.AUTH_HEADER, info.getParamLocation());
+  }
+
+  @Test
+  public void testAccessTokenFromServerDatabase() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    store.storeTokenKeyAndSecret(privateToken, null, arguments,
+        new TokenInfo("access", "secret", "sessionhandle", 12345L), responseParams);
+
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertNull(info.getAccessor().requestToken);
+    assertEquals("access", info.getAccessor().accessToken);
+    assertEquals("secret", info.getAccessor().tokenSecret);
+    assertEquals("sessionhandle", info.getSessionHandle());
+    assertEquals(12345L, info.getTokenExpireMillis());
+  }
+
+  @Test
+  public void testAccessTokenFromClient() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    store.storeTokenKeyAndSecret(privateToken, null, arguments,
+        new TokenInfo("access", "secret", null, 0), responseParams);
+
+    clientState.setAccessToken("clienttoken");
+    clientState.setAccessTokenSecret("clienttokensecret");
+    clientState.setSessionHandle("clienthandle");
+    clientState.setTokenExpireMillis(56789L);
+
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertNull(info.getAccessor().requestToken);
+    assertEquals("clienttoken", info.getAccessor().accessToken);
+    assertEquals("clienttokensecret", info.getAccessor().tokenSecret);
+    assertEquals("clienthandle", info.getSessionHandle());
+    assertEquals(56789L, info.getTokenExpireMillis());
+  }
+
+  @Test
+  public void testRequestTokenFromClientState() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    store.storeTokenKeyAndSecret(privateToken, null, arguments,
+        new TokenInfo("access", "secret", null, 0), responseParams);
+
+    clientState.setRequestToken("request");
+    clientState.setRequestTokenSecret("requestsecret");
+
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertEquals("request", info.getAccessor().requestToken);
+    assertEquals("requestsecret", info.getAccessor().tokenSecret);
+    assertNull(info.getAccessor().accessToken);
+  }
+
+  @Test
+  public void testRequestTokenFromClient_preferTokenInStorage() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    arguments.setRequestToken("preapproved");
+    arguments.setRequestTokenSecret("preapprovedsecret");
+    store.storeTokenKeyAndSecret(privateToken, null, arguments,
+        new TokenInfo("access", "secret", null, 0), responseParams);
+
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertNull(info.getAccessor().requestToken);
+    assertEquals("access", info.getAccessor().accessToken);
+    assertEquals("secret", info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testRequestTokenFromClient_noTokenInStorage() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    arguments.setRequestToken("preapproved");
+    arguments.setRequestTokenSecret("preapprovedsecret");
+
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertNull(info.getAccessor().accessToken);
+    assertEquals("preapproved", info.getAccessor().requestToken);
+    assertEquals("preapprovedsecret", info.getAccessor().tokenSecret);
+  }
+
+  @Test
+  public void testRemoveToken() throws Exception {
+    OAuthArguments arguments = new OAuthArguments();
+    arguments.setServiceName("testservice");
+    arguments.setUseToken(UseToken.ALWAYS);
+    store.storeTokenKeyAndSecret(privateToken, null, arguments,
+        new TokenInfo("access", "secret", null, 0), responseParams);
+
+    AccessorInfo info = store.getOAuthAccessor(privateToken, arguments, clientState,
+        responseParams, fetcherConfig);
+    assertNull(info.getAccessor().requestToken);
+    assertEquals("access", info.getAccessor().accessToken);
+    assertEquals("secret", info.getAccessor().tokenSecret);
+
+    store.removeToken(privateToken, null, arguments, responseParams);
+
+    info = store.getOAuthAccessor(privateToken, arguments, clientState, responseParams, fetcherConfig);
+    assertNull(info.getAccessor().requestToken);
+    assertNull(info.getAccessor().accessToken);
+    assertNull(info.getAccessor().tokenSecret);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthArgumentsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthArgumentsTest.java
new file mode 100644
index 0000000..c8a5907
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthArgumentsTest.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.oauth.OAuthArguments.UseToken;
+import org.apache.shindig.gadgets.spec.Preload;
+
+import org.junit.Test;
+import org.junit.Assert;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Tests parameter parsing
+ */
+public class OAuthArgumentsTest {
+
+  @Test
+  public void testInitFromPreload() throws Exception {
+    String xml = "<Preload href='http://www.example.com' " +
+        "oauth_service_name='service' " +
+        "OAUTH_TOKEN_NAME='token' " +
+        "OAUTH_REQuest_token='requesttoken' " +
+        "oauth_request_token_secret='tokensecret' " +
+        "OAUTH_USE_TOKEN='never' " +
+        "random='stuff'" +
+        "/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), Uri.parse(""));
+    OAuthArguments params = new OAuthArguments(preload);
+    assertEquals("service", params.getServiceName());
+    assertEquals("token", params.getTokenName());
+    assertEquals("requesttoken", params.getRequestToken());
+    assertEquals("tokensecret", params.getRequestTokenSecret());
+    assertEquals(UseToken.NEVER, params.getUseToken());
+    assertNull(params.getOrigClientState());
+    assertFalse(params.getBypassSpecCache());
+    assertEquals("stuff", params.getRequestOption("random"));
+  }
+
+  private FakeHttpServletRequest makeDummyRequest() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.setParameter("OAUTH_USE_TOKEN", true, "never");
+    req.setParameter("OAUTH_SERVICE_NAME", true, "service");
+    req.setParameter("OAUTH_TOKEN_NAME", true, "token");
+    req.setParameter("OAUTH_REQUEST_TOKEN", true, "reqtoken");
+    req.setParameter("OAUTH_REQUEST_TOKEN_SECRET", true, "secret");
+    req.setParameter("oauthState", true, "state");
+    req.setParameter("bypassSpecCache", true, "1");
+    req.setParameter("signOwner", true, "false");
+    req.setParameter("signViewer", true, "false");
+    req.setParameter("random", true, "stuff");
+    return req;
+  }
+
+  @Test
+  public void testInitFromRequest() throws Exception {
+    HttpServletRequest req = makeDummyRequest();
+
+    OAuthArguments args = new OAuthArguments(AuthType.SIGNED, req);
+    assertEquals(UseToken.NEVER, args.getUseToken());
+    assertEquals("service", args.getServiceName());
+    assertEquals("token", args.getTokenName());
+    assertEquals("reqtoken", args.getRequestToken());
+    assertEquals("secret", args.getRequestTokenSecret());
+    assertEquals("state", args.getOrigClientState());
+    Assert.assertTrue(args.getBypassSpecCache());
+    Assert.assertFalse(args.getSignOwner());
+    Assert.assertFalse(args.getSignViewer());
+    assertEquals("stuff", args.getRequestOption("random"));
+    assertEquals("stuff", args.getRequestOption("rAnDoM"));
+  }
+
+  @Test
+  public void testInitFromRequest_defaults() throws Exception {
+    HttpServletRequest req = new FakeHttpServletRequest();
+    OAuthArguments args = new OAuthArguments(AuthType.SIGNED, req);
+    assertEquals(UseToken.NEVER, args.getUseToken());
+    assertEquals("", args.getServiceName());
+    assertEquals("", args.getTokenName());
+    Assert.assertNull(args.getRequestToken());
+    Assert.assertNull(args.getRequestTokenSecret());
+    Assert.assertNull(args.getOrigClientState());
+    Assert.assertFalse(args.getBypassSpecCache());
+    Assert.assertTrue(args.getSignOwner());
+    Assert.assertTrue(args.getSignViewer());
+    assertNull(args.getRequestOption("random"));
+  }
+
+  @Test
+  public void testInitFromRequest_oauthDefaults() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    OAuthArguments args = new OAuthArguments(AuthType.OAUTH, req);
+    assertEquals(UseToken.ALWAYS, args.getUseToken());
+  }
+
+  @Test
+  public void testNoArgConstructorDefaults() throws Exception {
+    OAuthArguments args = new OAuthArguments();
+    assertEquals(UseToken.ALWAYS, args.getUseToken());
+    assertEquals("", args.getServiceName());
+    assertEquals("", args.getTokenName());
+    Assert.assertNull(args.getRequestToken());
+    Assert.assertNull(args.getRequestTokenSecret());
+    Assert.assertNull(args.getOrigClientState());
+    Assert.assertFalse(args.getBypassSpecCache());
+    Assert.assertFalse(args.getSignOwner());
+    Assert.assertFalse(args.getSignViewer());
+  }
+
+  @Test
+  public void testGetAndSet() throws Exception {
+    OAuthArguments args = new OAuthArguments();
+    args.setBypassSpecCache(true);
+    Assert.assertTrue(args.getBypassSpecCache());
+
+    args.setOrigClientState("thestate");
+    assertEquals("thestate", args.getOrigClientState());
+
+    args.setRequestToken("rt");
+    assertEquals("rt", args.getRequestToken());
+
+    args.setRequestTokenSecret("rts");
+    assertEquals("rts", args.getRequestTokenSecret());
+
+    args.setServiceName("s");
+    assertEquals("s", args.getServiceName());
+
+    args.setSignOwner(true);
+    Assert.assertTrue(args.getSignOwner());
+
+    args.setSignViewer(true);
+    Assert.assertTrue(args.getSignViewer());
+
+    args.setUseToken(UseToken.IF_AVAILABLE);
+    assertEquals(UseToken.IF_AVAILABLE, args.getUseToken());
+
+    args.setRequestOption("foo", "bar");
+    assertEquals("bar", args.getRequestOption("foo"));
+    args.removeRequestOption("foo");
+    assertNull(args.getRequestOption("foo"));
+  }
+
+  @Test
+  public void testCopyConstructor() throws Exception {
+    HttpServletRequest req = makeDummyRequest();
+    OAuthArguments args = new OAuthArguments(AuthType.OAUTH, req);
+    args = new OAuthArguments(args);
+    assertEquals(UseToken.NEVER, args.getUseToken());
+    assertEquals("service", args.getServiceName());
+    assertEquals("token", args.getTokenName());
+    assertEquals("reqtoken", args.getRequestToken());
+    assertEquals("secret", args.getRequestTokenSecret());
+    assertEquals("state", args.getOrigClientState());
+    Assert.assertTrue(args.getBypassSpecCache());
+    Assert.assertFalse(args.getSignOwner());
+    Assert.assertFalse(args.getSignViewer());
+  }
+
+  @Test
+  public void testCopyConstructor_options() throws Exception {
+    HttpServletRequest req = makeDummyRequest();
+    OAuthArguments args = new OAuthArguments(AuthType.OAUTH, req);
+    args = new OAuthArguments(args);
+
+    args.setRequestOption("foo", "bar");
+    args.setRequestOption("quux", "baz");
+    assertEquals("bar", args.getRequestOption("foo"));
+    assertEquals("baz", args.getRequestOption("quux"));
+  }
+
+  @Test
+  public void testParseUseToken() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.setParameter("OAUTH_USE_TOKEN", "ALWAYS");
+    OAuthArguments args = new OAuthArguments(AuthType.SIGNED, req);
+    assertEquals(UseToken.ALWAYS, args.getUseToken());
+
+    req.setParameter("OAUTH_USE_TOKEN", "if_available");
+    args = new OAuthArguments(AuthType.SIGNED, req);
+    assertEquals(UseToken.IF_AVAILABLE, args.getUseToken());
+
+    req.setParameter("OAUTH_USE_TOKEN", "never");
+    args = new OAuthArguments(AuthType.SIGNED, req);
+    assertEquals(UseToken.NEVER, args.getUseToken());
+
+    req.setParameter("OAUTH_USE_TOKEN", "");
+    args = new OAuthArguments(AuthType.SIGNED, req);
+    assertEquals(UseToken.NEVER, args.getUseToken());
+
+    req.setParameter("OAUTH_USE_TOKEN", "");
+    args = new OAuthArguments(AuthType.OAUTH, req);
+    assertEquals(UseToken.ALWAYS, args.getUseToken());
+
+    try {
+      req.setParameter("OAUTH_USE_TOKEN", "stuff");
+      new OAuthArguments(AuthType.OAUTH, req);
+      fail("Should have thrown");
+    } catch (GadgetException e) {
+      // good.
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthClientStateTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthClientStateTest.java
new file mode 100644
index 0000000..51dc9b8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthClientStateTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OAuthClientStateTest {
+
+  private FakeTimeSource timeSource;
+  private BasicBlobCrypter crypter;
+
+  @Before
+  public void setUp() throws Exception {
+    crypter = new BasicBlobCrypter("abcdefghijklmnop".getBytes());
+    timeSource = new FakeTimeSource();
+    crypter.timeSource = timeSource;
+  }
+
+  private void assertEmpty(OAuthClientState state) {
+    assertTrue(state.isEmpty());
+    assertNull(state.getRequestToken());
+    assertNull(state.getRequestTokenSecret());
+    assertNull(state.getAccessToken());
+    assertNull(state.getAccessTokenSecret());
+    assertNull(state.getOwner());
+  }
+
+  @Test
+  public void testEncryptEmpty() throws Exception {
+    OAuthClientState state = new OAuthClientState(crypter);
+    assertEmpty(state);
+    String encrypted = state.getEncryptedState();
+    state = new OAuthClientState(crypter, encrypted);
+    assertEmpty(state);
+  }
+
+  @Test
+  public void testValuesSet() throws Exception {
+    OAuthClientState state = new OAuthClientState(crypter);
+    state.setAccessToken("atoken");
+    state.setAccessTokenSecret("atokensecret");
+    state.setOwner("owner");
+    state.setRequestToken("reqtoken");
+    state.setRequestTokenSecret("reqtokensecret");
+    String encrypted = state.getEncryptedState();
+    state = new OAuthClientState(crypter, encrypted);
+    assertEquals("atoken", state.getAccessToken());
+    assertEquals("atokensecret", state.getAccessTokenSecret());
+    assertEquals("owner", state.getOwner());
+    assertEquals("reqtoken", state.getRequestToken());
+    assertEquals("reqtokensecret", state.getRequestTokenSecret());
+  }
+
+  @Test
+  public void testNullConstructorArg() throws Exception {
+    OAuthClientState state = new OAuthClientState(crypter, null);
+    assertEmpty(state);
+  }
+
+  @Test
+  public void testExpired() throws Exception {
+    OAuthClientState state = new OAuthClientState(crypter);
+    timeSource.incrementSeconds(-1 * (3600 + 180 + 1)); // expiry time + skew.
+    state.setTimeSource(timeSource);
+    state.setRequestToken("reqtoken");
+    String encrypted = state.getEncryptedState();
+    state = new OAuthClientState(crypter, encrypted);
+    assertNull(state.getRequestToken());
+  }
+
+  @Test
+  public void testNullValue() throws Exception {
+    OAuthClientState state = new OAuthClientState(crypter);
+    state.setRequestToken("reqtoken");
+    state.setRequestToken(null);
+    state.setOwner("owner");
+    String encrypted = state.getEncryptedState();
+    state = new OAuthClientState(crypter, encrypted);
+    assertNull(state.getRequestToken());
+    assertEquals("owner", state.getOwner());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherConfigTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherConfigTest.java
new file mode 100644
index 0000000..ceee267
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthFetcherConfigTest.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.gadgets.http.HttpCache;
+
+import org.junit.Test;
+
+/**
+ * Simple test for a simple class.
+ */
+public class OAuthFetcherConfigTest extends EasyMockTestCase {
+
+  @Test
+  public void testOAuthFetcherConfig() {
+    BlobCrypter crypter = mock(BlobCrypter.class);
+    mock(HttpCache.class);
+    GadgetOAuthTokenStore tokenStore = mock(GadgetOAuthTokenStore.class);
+    OAuthCallbackGenerator callbackGenerator = mock(OAuthCallbackGenerator.class);
+    OAuthFetcherConfig config = new OAuthFetcherConfig(crypter, tokenStore, new TimeSource(),
+        callbackGenerator, false);
+    assertEquals(crypter, config.getStateCrypter());
+    assertEquals(tokenStore, config.getTokenStore());
+    assertEquals(callbackGenerator, config.getOAuthCallbackGenerator());
+    assertFalse(config.isViewerAccessTokensEnabled());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java
new file mode 100644
index 0000000..19b0217
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthRequestTest.java
@@ -0,0 +1,2072 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.common.collect.Lists;
+
+import net.oauth.OAuth;
+import net.oauth.OAuth.Parameter;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.ArrayUtils;
+import org.apache.shindig.auth.BasicSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.gadgets.FakeGadgetSpecFactory;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+import org.apache.shindig.gadgets.oauth.BasicOAuthStoreConsumerKeyAndSecret.KeyType;
+import org.apache.shindig.gadgets.oauth.OAuthArguments.UseToken;
+import org.apache.shindig.gadgets.oauth.testing.FakeOAuthServiceProvider;
+import org.apache.shindig.gadgets.oauth.testing.MakeRequestClient;
+import org.apache.shindig.gadgets.oauth.testing.FakeOAuthServiceProvider.TokenPair;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+
+/**
+ * Tests for signing requests.
+ */
+public class OAuthRequestTest {
+
+  private OAuthFetcherConfig fetcherConfig;
+  private FakeOAuthServiceProvider serviceProvider;
+  private OAuthCallbackGenerator callbackGenerator;
+  private BasicOAuthStore base;
+  private Logger logger;
+  protected final List<LogRecord> logRecords = Lists.newArrayList();
+  private final FakeTimeSource clock = new FakeTimeSource();
+
+  public static final String GADGET_URL = "http://www.example.com/gadget.xml";
+  public static final String GADGET_URL_NO_KEY = "http://www.example.com/nokey.xml";
+  public static final String GADGET_URL_HEADER = "http://www.example.com/header.xml";
+  public static final String GADGET_URL_BODY = "http://www.example.com/body.xml";
+  public static final String GADGET_URL_BAD_OAUTH_URL = "http://www.example.com/badoauthurl.xml";
+  public static final String GADGET_URL_APPROVAL_PARAMS =
+      "http://www.example.com/approvalparams.xml";
+  public static final String GADGET_MAKE_REQUEST_URL =
+      "http://127.0.0.1/gadgets/makeRequest?params=foo";
+
+  @Before
+  public void setUp() throws Exception {
+    base = new BasicOAuthStore();
+    base.setDefaultCallbackUrl(GadgetTokenStoreTest.DEFAULT_CALLBACK);
+    serviceProvider = new FakeOAuthServiceProvider(clock);
+    callbackGenerator = createNullCallbackGenerator();
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base),
+        clock,
+        callbackGenerator,
+        false);
+
+    logger = Logger.getLogger(OAuthResponseParams.class.getName());
+    logger.addHandler(new Handler() {
+      @Override
+      public void close() throws SecurityException {
+      }
+
+      @Override
+      public void flush() {
+      }
+
+      @Override
+      public void publish(LogRecord arg0) {
+        logRecords.add(arg0);
+      }
+    });
+    logger.setLevel(Level.FINE);
+  }
+
+  private OAuthCallbackGenerator createNullCallbackGenerator() {
+    return new OAuthCallbackGenerator() {
+      public String generateCallback(OAuthFetcherConfig fetcherConfig, String baseCallback,
+          HttpRequest request, OAuthResponseParams responseParams) {
+        return null;
+      }
+    };
+  }
+
+  private OAuthCallbackGenerator createRealCallbackGenerator() {
+    return new OAuthCallbackGenerator() {
+      public String generateCallback(OAuthFetcherConfig fetcherConfig, String baseCallback,
+          HttpRequest request, OAuthResponseParams responseParams) {
+        SecurityToken st = request.getSecurityToken();
+        Uri activeUrl = Uri.parse(st.getActiveUrl());
+        assertEquals(GADGET_MAKE_REQUEST_URL, activeUrl.toString());
+        assertEquals(GadgetTokenStoreTest.DEFAULT_CALLBACK, baseCallback);
+        return new UriBuilder()
+            .setScheme("http")
+            .setAuthority(activeUrl.getAuthority())
+            .setPath("/realcallback")
+            .toString();
+      }
+    };
+  }
+
+  /**
+   * Builds a nicely populated fake token store.
+   */
+  public GadgetOAuthTokenStore getOAuthStore(BasicOAuthStore base) {
+    return getOAuthStore(base, new FakeGadgetSpecFactory());
+  }
+
+  private GadgetOAuthTokenStore getOAuthStore(BasicOAuthStore base,
+      GadgetSpecFactory specFactory) {
+    if (base == null) {
+      base = new BasicOAuthStore();
+      base.setDefaultCallbackUrl(GadgetTokenStoreTest.DEFAULT_CALLBACK);
+    }
+    addValidConsumer(base);
+    addInvalidConsumer(base);
+    addAuthHeaderConsumer(base);
+    addBodyConsumer(base);
+    addBadOAuthUrlConsumer(base);
+    addApprovalParamsConsumer(base);
+    addDefaultKey(base);
+    return new GadgetOAuthTokenStore(base, specFactory);
+  }
+
+  private static void addValidConsumer(BasicOAuthStore base) {
+    addConsumer(
+        base,
+        GADGET_URL,
+        FakeGadgetSpecFactory.SERVICE_NAME,
+        FakeOAuthServiceProvider.CONSUMER_KEY,
+        FakeOAuthServiceProvider.CONSUMER_SECRET);
+  }
+
+  private static void addInvalidConsumer(BasicOAuthStore base) {
+    addConsumer(
+        base,
+        GADGET_URL_NO_KEY,
+        FakeGadgetSpecFactory.SERVICE_NAME_NO_KEY,
+        "garbage_key", "garbage_secret");
+  }
+
+  private static void addAuthHeaderConsumer(BasicOAuthStore base) {
+    addConsumer(
+        base,
+        GADGET_URL_HEADER,
+        FakeGadgetSpecFactory.SERVICE_NAME,
+        FakeOAuthServiceProvider.CONSUMER_KEY,
+        FakeOAuthServiceProvider.CONSUMER_SECRET);
+  }
+
+  private static void addBodyConsumer(BasicOAuthStore base) {
+    addConsumer(
+        base,
+        GADGET_URL_BODY,
+        FakeGadgetSpecFactory.SERVICE_NAME,
+        FakeOAuthServiceProvider.CONSUMER_KEY,
+        FakeOAuthServiceProvider.CONSUMER_SECRET);
+  }
+
+  private static void addBadOAuthUrlConsumer(BasicOAuthStore base) {
+    addConsumer(
+        base,
+        GADGET_URL_BAD_OAUTH_URL,
+        FakeGadgetSpecFactory.SERVICE_NAME,
+        FakeOAuthServiceProvider.CONSUMER_KEY,
+        FakeOAuthServiceProvider.CONSUMER_SECRET);
+  }
+
+  private static void addApprovalParamsConsumer(BasicOAuthStore base) {
+    addConsumer(
+        base,
+        GADGET_URL_APPROVAL_PARAMS,
+        FakeGadgetSpecFactory.SERVICE_NAME,
+        FakeOAuthServiceProvider.CONSUMER_KEY,
+        FakeOAuthServiceProvider.CONSUMER_SECRET);
+  }
+
+  private static void addConsumer(
+      BasicOAuthStore base,
+      String gadgetUrl,
+      String serviceName,
+      String consumerKey,
+      String consumerSecret) {
+    BasicOAuthStoreConsumerIndex providerKey = new BasicOAuthStoreConsumerIndex();
+    providerKey.setGadgetUri(gadgetUrl);
+    providerKey.setServiceName(serviceName);
+
+    BasicOAuthStoreConsumerKeyAndSecret kas = new BasicOAuthStoreConsumerKeyAndSecret(
+        consumerKey, consumerSecret, KeyType.HMAC_SYMMETRIC, null, null);
+
+    base.setConsumerKeyAndSecret(providerKey, kas);
+  }
+
+  private static void addDefaultKey(BasicOAuthStore base) {
+    BasicOAuthStoreConsumerKeyAndSecret defaultKey = new BasicOAuthStoreConsumerKeyAndSecret(
+        "signedfetch", FakeOAuthServiceProvider.PRIVATE_KEY_TEXT, KeyType.RSA_PRIVATE, "foo", null);
+    base.setDefaultKey(defaultKey);
+  }
+
+
+  /**
+   * Builds gadget token for testing a service with parameters in the query.
+   */
+  public static SecurityToken getNormalSecurityToken(String owner, String viewer) throws Exception {
+    return getSecurityToken(owner, viewer, GADGET_URL);
+  }
+
+  /**
+   * Builds gadget token for testing services without a key.
+   */
+  public static SecurityToken getNokeySecurityToken(String owner, String viewer) throws Exception {
+    return getSecurityToken(owner, viewer, GADGET_URL_NO_KEY);
+  }
+
+  /**
+   * Builds gadget token for testing a service that wants parameters in a header.
+   */
+  public static SecurityToken getHeaderSecurityToken(String owner, String viewer) throws Exception {
+    return getSecurityToken(owner, viewer, GADGET_URL_HEADER);
+  }
+
+  /**
+   * Builds gadget token for testing a service that wants parameters in the request body.
+   */
+  public static SecurityToken getBodySecurityToken(String owner, String viewer) throws Exception {
+    return getSecurityToken(owner, viewer, GADGET_URL_BODY);
+  }
+
+  public static SecurityToken getSecurityToken(String owner, String viewer, String gadget)
+      throws Exception {
+    return new BasicSecurityToken(owner, viewer, "app", "container.com", gadget, "0", "default",
+        GADGET_MAKE_REQUEST_URL, null);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+  }
+
+  /** Client that does OAuth and sends opensocial_* params */
+  private MakeRequestClient makeNonSocialClient(String owner, String viewer, String gadget)
+      throws Exception {
+    SecurityToken securityToken = getSecurityToken(owner, viewer, gadget);
+    serviceProvider.setExpectedRequestSecurityToken( securityToken );
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        FakeGadgetSpecFactory.SERVICE_NAME);
+    client.getBaseArgs().setSignOwner(true);
+    client.getBaseArgs().setSignViewer(true);
+    return client;
+  }
+
+  /** Client that does OAuth and does not send opensocial_* params */
+  private MakeRequestClient makeStrictNonSocialClient(String owner, String viewer, String gadget)
+      throws Exception {
+    SecurityToken securityToken = getSecurityToken(owner, viewer, gadget);
+    serviceProvider.setExpectedRequestSecurityToken( securityToken );
+    return new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        FakeGadgetSpecFactory.SERVICE_NAME);
+  }
+
+  private MakeRequestClient makeSocialOAuthClient(String owner, String viewer, String gadget)
+      throws Exception {
+    SecurityToken securityToken = getSecurityToken(owner, viewer, gadget);
+    serviceProvider.setExpectedRequestSecurityToken( securityToken );
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        FakeGadgetSpecFactory.SERVICE_NAME);
+    client.getBaseArgs().setUseToken(UseToken.IF_AVAILABLE);
+    return client;
+  }
+
+  private MakeRequestClient makeSignedFetchClient(String owner, String viewer, String gadget)
+      throws Exception {
+    SecurityToken securityToken = getSecurityToken(owner, viewer, gadget);
+    serviceProvider.setExpectedRequestSecurityToken( securityToken );
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        null);
+    client.setBaseArgs(client.makeSignedFetchArguments());
+    return client;
+  }
+
+  @Test
+  public void testOAuthFlow() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    checkEmptyLog();
+  }
+
+  @Test
+  public void testOAuthFlow_withCallbackVerifier() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base),
+        clock,
+        createRealCallbackGenerator(),
+        false);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    checkEmptyLog();
+  }
+
+  @Test
+  public void testOAuthFlow_badCallbackVerifier() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base),
+        clock,
+        createRealCallbackGenerator(),
+        false);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+
+    client.approveToken("user_data=hello-oauth");
+    client.setReceivedCallbackUrl("nonsense");
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertNotNull(response.getMetadata().get("oauthErrorText"));
+
+    client.approveToken("user_data=try-again");
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is try-again", response.getResponseAsString());
+  }
+
+  @Test
+  public void testOAuthFlow_tokenReused() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    // Check out what happens if the client-side oauth state vanishes.
+    MakeRequestClient client2 = makeNonSocialClient("owner", "owner", GADGET_URL);
+    response = client2.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+  }
+
+  @Test
+  public void testOAuthFlow_unauthUser() throws Exception {
+    MakeRequestClient client = makeNonSocialClient(null, null, GADGET_URL);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(-1, response.getCacheTtl());
+    assertEquals(OAuthError.UNAUTHENTICATED.name(), response.getMetadata().get("oauthError"));
+  }
+
+  @Test
+  public void testOAuthFlow_noViewer() throws Exception {
+    for (boolean secureOwner : Arrays.asList(true, false)) {
+      // Test both with/without secure owner pages
+      fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base),
+        clock, callbackGenerator,
+        secureOwner);
+
+      MakeRequestClient client = makeNonSocialClient("owner", null, GADGET_URL);
+
+      HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+      assertEquals("", response.getResponseAsString());
+      assertEquals(403, response.getHttpStatusCode());
+      assertEquals(-1, response.getCacheTtl());
+      assertEquals(OAuthError.UNAUTHENTICATED.name(), response.getMetadata().get("oauthError"));
+    }
+  }
+
+  @Test
+  public void testOAuthFlow_noSpec() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, callbackGenerator,
+        false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    checkEmptyLog();
+  }
+
+  private void setNoSpecOptions(MakeRequestClient client) {
+    client.getBaseArgs().setRequestOption(OAuthArguments.PROGRAMMATIC_CONFIG_PARAM, "true");
+    client.getBaseArgs().setRequestOption(OAuthArguments.PARAM_LOCATION_PARAM, "uri-query");
+    client.getBaseArgs().setRequestOption(OAuthArguments.REQUEST_METHOD_PARAM, "GET");
+    client.getBaseArgs().setRequestOption(OAuthArguments.REQUEST_TOKEN_URL_PARAM,
+        FakeOAuthServiceProvider.REQUEST_TOKEN_URL);
+    client.getBaseArgs().setRequestOption(OAuthArguments.ACCESS_TOKEN_URL_PARAM,
+        FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    client.getBaseArgs().setRequestOption(OAuthArguments.AUTHORIZATION_URL_PARAM,
+        FakeOAuthServiceProvider.APPROVAL_URL);
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecNoRequestTokenUrl() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, null, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().removeRequestOption(OAuthArguments.REQUEST_TOKEN_URL_PARAM);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(OAuthError.BAD_OAUTH_TOKEN_URL.name(),
+        response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    assertNotNull(errorText);
+    checkStringContains("should report no request token url", errorText,
+        "No request token URL specified");
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecNoAccessTokenUrl() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, callbackGenerator, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().removeRequestOption(OAuthArguments.ACCESS_TOKEN_URL_PARAM);
+
+    // Get the request token
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+
+    // try to swap for access token
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+
+    assertEquals("", response.getResponseAsString());
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(OAuthError.BAD_OAUTH_TOKEN_URL.name(),
+        response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    assertNotNull(errorText);
+    checkStringContains("should report no access token url", errorText,
+        "No access token URL specified");
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecNoApprovalUrl() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, callbackGenerator, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().removeRequestOption(OAuthArguments.AUTHORIZATION_URL_PARAM);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+
+    assertEquals("", response.getResponseAsString());
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(OAuthError.BAD_OAUTH_TOKEN_URL.name(),
+        response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    assertNotNull(errorText);
+    checkStringContains("should report no authorization url", errorText,
+        "No authorization URL specified");
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecAuthHeader() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.AUTH_HEADER);
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, callbackGenerator, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().setRequestOption(OAuthArguments.PARAM_LOCATION_PARAM, "auth-header");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    checkEmptyLog();
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecPostBody() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.POST_BODY);
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, callbackGenerator, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().setRequestOption(OAuthArguments.REQUEST_METHOD_PARAM, "POST");
+    client.getBaseArgs().setRequestOption(OAuthArguments.PARAM_LOCATION_PARAM, "post-body");
+
+    HttpResponse response = client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "");
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    checkEmptyLog();
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecPostBodyAndHeader() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.POST_BODY);
+    serviceProvider.addParamLocation(OAuthParamLocation.AUTH_HEADER);
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, callbackGenerator, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().setRequestOption(OAuthArguments.REQUEST_METHOD_PARAM, "POST");
+    client.getBaseArgs().setRequestOption(OAuthArguments.PARAM_LOCATION_PARAM, "post-body");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    checkEmptyLog();
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecInvalidUrl() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, null, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().setRequestOption(OAuthArguments.REQUEST_TOKEN_URL_PARAM, "foo");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(OAuthError.INVALID_URL.name(),
+        response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    assertNotNull(errorText);
+    checkStringContains("should report invalid url", errorText, "Invalid URL: foo");
+  }
+
+  @Test
+  public void testOAuthFlow_noSpecBlankUrl() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base, null),
+        clock, null, false);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    setNoSpecOptions(client);
+    client.getBaseArgs().setRequestOption(OAuthArguments.REQUEST_TOKEN_URL_PARAM, "");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(OAuthError.INVALID_URL.name(),
+        response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    assertNotNull(errorText);
+    checkStringContains("should report invalid url", errorText, "Invalid URL: ");
+  }
+
+  @Test
+  public void testAccessTokenNotUsedForSocialPage() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    MakeRequestClient friend = makeNonSocialClient("owner", "friend", GADGET_URL);
+    response = friend.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals(OAuthError.NOT_OWNER.name(), response.getMetadata().get("oauthError"));
+  }
+
+  @Test
+  public void testAccessTokenOkForSecureOwnerPage() throws Exception {
+    fetcherConfig = new OAuthFetcherConfig(
+        new BasicBlobCrypter("abcdefghijklmnop".getBytes()),
+        getOAuthStore(base),
+        clock,
+        callbackGenerator,
+        true);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    MakeRequestClient friend = makeNonSocialClient("owner", "friend", GADGET_URL);
+    response = friend.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertEquals(200, response.getHttpStatusCode());
+  }
+
+  @Test
+  public void testParamsInHeader() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.AUTH_HEADER);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL_HEADER);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    String aznHeader = response.getHeader(FakeOAuthServiceProvider.AUTHZ_ECHO_HEADER);
+    assertNotNull(aznHeader);
+    Assert.assertNotSame("azn header: " + aznHeader, aznHeader.indexOf("OAuth"), -1);
+  }
+
+  @Test
+  public void testParamsInBody() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.POST_BODY);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL_BODY);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    String echoedBody = response.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER);
+    assertNotNull(echoedBody);
+    Assert.assertNotSame("body: " + echoedBody, echoedBody.indexOf("oauth_consumer_key="), -1);
+  }
+
+  @Test
+  public void testParamsInBody_withExtraParams() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.POST_BODY);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL_BODY);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "foo=bar&foo=baz");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    String echoedBody = response.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER);
+    assertNotNull(echoedBody);
+    Assert.assertNotSame("body: " + echoedBody, echoedBody.indexOf("oauth_consumer_key="), -1);
+    Assert.assertNotSame("body: " + echoedBody, echoedBody.indexOf("foo=bar&foo=baz"), -1);
+  }
+
+  @Test
+  public void testParamsInBody_forGetRequest() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.POST_BODY);
+    serviceProvider.addParamLocation(OAuthParamLocation.AUTH_HEADER);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL_BODY);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    String aznHeader = response.getHeader(FakeOAuthServiceProvider.AUTHZ_ECHO_HEADER);
+    assertNotNull(aznHeader);
+    Assert.assertNotSame("azn header: " + aznHeader, aznHeader.indexOf("OAuth"), -1);
+  }
+
+  @Test
+  public void testParamsInBody_forGetRequestStrictSp() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.POST_BODY);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL_BODY);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    assertEquals(HttpResponse.SC_FORBIDDEN, response.getHttpStatusCode());
+    assertEquals("parameter_absent", response.getMetadata().get("oauthError"));
+    assertNull(response.getMetadata().get("oauthApprovalUrl"));
+  }
+
+  @Test
+  public void testRevokedAccessToken() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    serviceProvider.revokeAllAccessTokens();
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=2");
+    assertEquals("", response.getResponseAsString());
+    assertNotNull(response.getMetadata().get("oauthApprovalUrl"));
+    assertNull("Should not return oauthError for revoked token",
+        response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    assertNotNull(errorText);
+    checkStringContains("should return original request", errorText, "GET /data?cachebust=2\n");
+    checkStringContains("should return signed request", errorText, "GET /data?cachebust=2&");
+    checkStringContains("should remove secret", errorText, "oauth_token_secret=REMOVED");
+    checkStringContains("should return response", errorText, "HTTP/1.1 401");
+    checkStringContains("should return response", errorText, "oauth_problem=\"token_revoked\"");
+
+    client.approveToken("user_data=reapproved");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
+    assertEquals("User data is reapproved", response.getResponseAsString());
+  }
+
+  @Test
+  public void testError401() throws Exception {
+    serviceProvider.setVagueErrors(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    serviceProvider.revokeAllAccessTokens();
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=2");
+    checkLogContains("GET /data?cachebust=2");
+    checkLogContains("HTTP/1.1 401");
+    assertEquals("", response.getResponseAsString());
+    assertNotNull(response.getMetadata().get("oauthApprovalUrl"));
+
+    client.approveToken("user_data=reapproved");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
+    assertEquals("User data is reapproved", response.getResponseAsString());
+  }
+
+  @Test
+  public void testUnknownConsumerKey() throws Exception {
+    SecurityToken securityToken = getSecurityToken("owner", "owner", GADGET_URL_NO_KEY);
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        FakeGadgetSpecFactory.SERVICE_NAME_NO_KEY);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+
+    Map<String, String> metadata = response.getMetadata();
+    assertNotNull(metadata);
+    assertEquals("consumer_key_unknown", metadata.get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("oauthErrorText mismatch", errorText,
+        "Service provider rejected request");
+    checkStringContains("oauthErrorText mismatch", errorText,
+        "oauth_problem_advice=\"invalid%20consumer%3A%20garbage_key\"");
+    checkStringContains("should return original request", errorText, "GET /data\n");
+    checkStringContains("should return request token request", errorText,
+        "GET /request?param=foo&");
+  }
+
+  @Test
+  public void testBrokenRequestTokenResponse() throws Exception {
+    SecurityToken securityToken = getSecurityToken("owner", "owner", GADGET_URL_BAD_OAUTH_URL);
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        FakeGadgetSpecFactory.SERVICE_NAME);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals("", response.getResponseAsString());
+    Map<String, String> metadata = response.getMetadata();
+    assertNotNull(metadata);
+    assertEquals("MISSING_OAUTH_PARAMETER", metadata.get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("oauthErrorText mismatch", errorText,
+        "No oauth_token returned from service provider");
+    checkStringContains("oauthErrorText mismatch", errorText,
+        "GET /echo?mary_had_a_little_lamb");
+  }
+
+  @Test
+  public void testBrokenAccessTokenResponse() throws Exception {
+    SecurityToken securityToken = getSecurityToken("owner", "owner", GADGET_URL_BAD_OAUTH_URL);
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        FakeGadgetSpecFactory.SERVICE_NAME);
+    // This lets us skip the access token step
+    client.getBaseArgs().setRequestToken("reqtoken");
+    client.getBaseArgs().setRequestTokenSecret("reqtokensecret");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals("", response.getResponseAsString());
+    Map<String, String> metadata = response.getMetadata();
+    assertNotNull(metadata);
+    assertEquals("MISSING_OAUTH_PARAMETER", metadata.get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("oauthErrorText mismatch", errorText,
+        "No oauth_token_secret returned from service provider");
+    checkStringContains("oauthErrorText mismatch", errorText,
+        "with_fleece_as_white_as_snow");
+  }
+
+  @Test
+  public void testExtraApprovalParams() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL_APPROVAL_PARAMS);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    String approvalUrl = response.getMetadata().get("oauthApprovalUrl");
+    Assert.assertSame(approvalUrl, 0, approvalUrl.indexOf(
+        "http://www.example.com/authorize?oauth_callback=foo&oauth_token="));
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    checkEmptyLog();
+  }
+
+  @Test
+  public void testError403() throws Exception {
+    serviceProvider.setVagueErrors(true);
+    SecurityToken securityToken = getSecurityToken("owner", "owner", GADGET_URL_NO_KEY);
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        FakeGadgetSpecFactory.SERVICE_NAME_NO_KEY);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    Map<String, String> metadata = response.getMetadata();
+    assertNotNull(metadata);
+    assertEquals("MISSING_OAUTH_PARAMETER", metadata.get("oauthError"));
+    checkStringContains("oauthErrorText mismatch", metadata.get("oauthErrorText"),
+        "some vague error");
+    checkStringContains("oauthErrorText mismatch", metadata.get("oauthErrorText"),
+        "HTTP/1.1 403");
+    checkLogContains("HTTP/1.1 403");
+    checkLogContains("GET /request");
+    checkLogContains("some vague error");
+  }
+
+  @Test
+  public void testError404() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    response = client.sendGet(FakeOAuthServiceProvider.NOT_FOUND_URL);
+    assertEquals("not found", response.getResponseAsString());
+    assertEquals(404, response.getHttpStatusCode());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+  }
+
+  @Test
+  public void testError400() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    response = client.sendGet(FakeOAuthServiceProvider.ERROR_400);
+    assertEquals("bad request", response.getResponseAsString());
+    assertEquals(400, response.getHttpStatusCode());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+  }
+
+
+  @Test
+  public void testConsumerThrottled() throws Exception {
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(0, serviceProvider.getAccessTokenCount());
+    assertEquals(0, serviceProvider.getResourceAccessCount());
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(0, serviceProvider.getAccessTokenCount());
+    assertEquals(0, serviceProvider.getResourceAccessCount());
+
+    client.approveToken("user_data=hello-oauth");
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+
+    serviceProvider.setConsumersThrottled(true);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=2");
+    assertEquals("", response.getResponseAsString());
+    Map<String, String> metadata = response.getMetadata();
+    assertNotNull(metadata);
+    assertEquals("consumer_key_refused", metadata.get("oauthError"));
+    checkStringContains("oauthErrorText mismatch", metadata.get("oauthErrorText"),
+        "Service provider rejected request");
+    checkStringContains("oauthErrorText missing request entry", metadata.get("oauthErrorText"),
+        "GET /data?cachebust=2\n");
+    checkStringContains("oauthErrorText missing request entry", metadata.get("oauthErrorText"),
+        "GET /data?cachebust=2&oauth_body_hash=2jm");
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(3, serviceProvider.getResourceAccessCount());
+
+    serviceProvider.setConsumersThrottled(false);
+    client.clearState();
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(4, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testConsumerThrottled_vagueErrors() throws Exception {
+    serviceProvider.setVagueErrors(true);
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(0, serviceProvider.getAccessTokenCount());
+    assertEquals(0, serviceProvider.getResourceAccessCount());
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(0, serviceProvider.getAccessTokenCount());
+    assertEquals(0, serviceProvider.getResourceAccessCount());
+
+    client.approveToken("user_data=hello-oauth");
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+
+    serviceProvider.setConsumersThrottled(true);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=2");
+    assertEquals(403, response.getHttpStatusCode());
+    assertEquals("some vague error", response.getResponseAsString());
+    Map<String, String> metadata = response.getMetadata();
+    assertNotNull(metadata);
+    assertNull(metadata.get("oauthError"));
+    checkStringContains("oauthErrorText missing request entry", metadata.get("oauthErrorText"),
+        "GET /data?cachebust=2\n");
+    checkStringContains("oauthErrorText missing request entry", metadata.get("oauthErrorText"),
+        "GET /data?cachebust=2&oauth_body_hash=2jm");
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(3, serviceProvider.getResourceAccessCount());
+
+    serviceProvider.setConsumersThrottled(false);
+
+    client.clearState(); // remove any cached oauth tokens
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=3");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(4, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testSocialOAuth_tokenRevoked() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+
+    client.approveToken("user_data=hello-oauth");
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    serviceProvider.revokeAllAccessTokens();
+
+    assertEquals(0, base.getAccessTokenRemoveCount());
+    client = makeSocialOAuthClient("owner", "owner", GADGET_URL);
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals("", response.getResponseAsString());
+    assertEquals(1, base.getAccessTokenRemoveCount());
+  }
+
+  @Test
+  public void testWrongServiceName() throws Exception {
+    SecurityToken securityToken = getSecurityToken("owner", "owner", GADGET_URL);
+    MakeRequestClient client = new MakeRequestClient(securityToken, fetcherConfig, serviceProvider,
+        "nosuchservice");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    Map<String, String> metadata = response.getMetadata();
+    assertNull(metadata.get("oauthApprovalUrl"));
+    assertEquals("BAD_OAUTH_CONFIGURATION", metadata.get("oauthError"));
+    String errorText = metadata.get("oauthErrorText");
+    assertTrue(errorText, errorText.startsWith(
+        "Failed to retrieve OAuth URLs, spec for gadget does " +
+        "not contain OAuth service nosuchservice.  Known services: testservice"));
+  }
+
+  @Test
+  public void testPreapprovedToken() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    TokenPair reqToken = serviceProvider.getPreapprovedToken("preapproved");
+    client.getBaseArgs().setRequestToken(reqToken.token);
+    client.getBaseArgs().setRequestTokenSecret(reqToken.secret);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is preapproved", response.getResponseAsString());
+
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is preapproved", response.getResponseAsString());
+
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=2");
+    assertEquals("User data is preapproved", response.getResponseAsString());
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(3, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testPreapprovedToken_invalid() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    client.getBaseArgs().setRequestToken("garbage");
+    client.getBaseArgs().setRequestTokenSecret("garbage");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+
+    assertEquals("", response.getResponseAsString());
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(0, serviceProvider.getResourceAccessCount());
+
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testPreapprovedToken_notUsedIfAccessTokenExists() throws Exception {
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    TokenPair reqToken = serviceProvider.getPreapprovedToken("preapproved");
+    client.getBaseArgs().setRequestToken(reqToken.token);
+    client.getBaseArgs().setRequestTokenSecret(reqToken.secret);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is preapproved", response.getResponseAsString());
+
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    MakeRequestClient client2 = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    response = client2.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cachebust=1");
+    assertEquals("User data is preapproved", response.getResponseAsString());
+
+    assertEquals(0, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testSignedFetchParametersSet() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, "opensocial_viewer_id", "v"));
+    assertTrue(contains(queryParams, "opensocial_app_id", "app"));
+    assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "signedfetch"));
+    assertTrue(contains(queryParams, "xoauth_signature_publickey", "foo"));
+    assertTrue(contains(queryParams, "xoauth_public_key", "foo"));
+    assertFalse(contains(queryParams, "opensocial_proxied_content", "1"));
+  }
+
+  @Test
+  public void testSignedFetch_authHeader() throws Exception {
+    serviceProvider.setParamLocation(OAuthParamLocation.AUTH_HEADER);
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    client.getBaseArgs().setRequestOption(OAuthArguments.PROGRAMMATIC_CONFIG_PARAM, "true");
+    client.getBaseArgs().setRequestOption(OAuthArguments.PARAM_LOCATION_PARAM, "auth-header");
+
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    String auth = resp.getHeader(FakeOAuthServiceProvider.AUTHZ_ECHO_HEADER);
+    assertNotNull("Should have echoed authz header", auth);
+    checkStringContains("should have opensocial params in header", auth,
+        "opensocial_owner_id=\"o\"");
+  }
+
+  @Test
+  public void testSignedFetchParametersSetProxiedContent() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    client.getBaseArgs().setProxiedContentRequest(true);
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, "opensocial_viewer_id", "v"));
+    assertTrue(contains(queryParams, "opensocial_app_id", "app"));
+    assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "signedfetch"));
+    assertTrue(contains(queryParams, "xoauth_signature_publickey", "foo"));
+    assertTrue(contains(queryParams, "xoauth_public_key", "foo"));
+    assertTrue(contains(queryParams, "opensocial_proxied_content", "1"));
+  }
+
+  @Test
+  public void testPostBinaryData() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendRawPost(FakeOAuthServiceProvider.RESOURCE_URL, null, raw);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "signedfetch"));
+    String echoed = resp.getHeader(FakeOAuthServiceProvider.RAW_BODY_ECHO_HEADER);
+    byte[] echoedBytes = Base64.decodeBase64(CharsetUtil.getUtf8Bytes(echoed));
+    assertTrue(Arrays.equals(raw, echoedBytes));
+  }
+
+  @Test
+  public void testPostWeirdContentType() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendRawPost(FakeOAuthServiceProvider.RESOURCE_URL,
+        "funky-content", raw);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "signedfetch"));
+    String echoed = resp.getHeader(FakeOAuthServiceProvider.RAW_BODY_ECHO_HEADER);
+    byte[] echoedBytes = Base64.decodeBase64(CharsetUtil.getUtf8Bytes(echoed));
+    assertTrue(Arrays.equals(raw, echoedBytes));
+  }
+
+  @Test
+  public void testGetWithFormEncodedBody() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGetWithBody(FakeOAuthServiceProvider.RESOURCE_URL,
+        OAuth.FORM_ENCODED, "war=peace&yes=no".getBytes());
+    assertEquals("war=peace&yes=no", resp.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER));
+  }
+
+  @Test
+  public void testGetWithRawBody() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGetWithBody(FakeOAuthServiceProvider.RESOURCE_URL,
+        "application/json", "war=peace&yes=no".getBytes());
+    assertEquals("war=peace&yes=no", resp.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER));
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    checkContains(queryParams, "oauth_body_hash", "MfhwxPN6ns5CwQAZN9OcJXu3Jv4=");
+  }
+
+  @Test
+  public void testGetTamperedRawContent() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody("yo momma".getBytes());
+        return serviceProvider.fetch(request);
+      }
+    });
+    try {
+      client.sendGetWithBody(FakeOAuthServiceProvider.RESOURCE_URL,
+          "funky-content", raw);
+      fail("Should have thrown with oauth_body_hash mismatch");
+    } catch (RuntimeException e) {
+      // good
+    }
+  }
+
+  @Test(expected=RuntimeException.class)
+  public void testGetTamperedFormContent() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody("foo=quux".getBytes());
+        return serviceProvider.fetch(request);
+      }
+    });
+    client.sendGetWithBody(FakeOAuthServiceProvider.RESOURCE_URL,
+        OAuth.FORM_ENCODED, "foo=bar".getBytes());
+    fail("Should have thrown with oauth signature mismatch");
+  }
+
+  @Test(expected=RuntimeException.class)
+  public void testGetTamperedRemoveRawContent() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody(ArrayUtils.EMPTY_BYTE_ARRAY);
+        request.setHeader("Content-Type", "application/x-www-form-urlencoded");
+        return serviceProvider.fetch(request);
+      }
+    });
+    client.sendGetWithBody(FakeOAuthServiceProvider.RESOURCE_URL,
+        "funky-content", raw);
+    fail("Should have thrown with body hash in form encoded request");
+  }
+
+  @Test(expected=RuntimeException.class)
+  public void testPostTamperedRawContent() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody("yo momma".getBytes());
+        return serviceProvider.fetch(request);
+      }
+    });
+    client.sendRawPost(FakeOAuthServiceProvider.RESOURCE_URL,
+       "funky-content", raw);
+    fail("Should have thrown with oauth_body_hash mismatch");
+  }
+
+  @Test(expected=RuntimeException.class)
+  public void testPostTamperedFormContent() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody("foo=quux".getBytes());
+        return serviceProvider.fetch(request);
+      }
+    });
+    client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "foo=bar");
+    fail("Should have thrown with oauth signature mismatch");
+  }
+
+  @Test(expected=RuntimeException.class)
+  public void testPostTamperedRemoveRawContent() throws Exception {
+    byte[] raw = { 0, 1, 2, 3, 4, 5 };
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    // Tamper with the body before it hits the service provider
+    client.setNextFetcher(new HttpFetcher() {
+      public HttpResponse fetch(HttpRequest request) throws GadgetException {
+        request.setPostBody(ArrayUtils.EMPTY_BYTE_ARRAY);
+        request.setHeader("Content-Type", "application/x-www-form-urlencoded");
+        return serviceProvider.fetch(request);
+      }
+    });
+    client.sendRawPost(FakeOAuthServiceProvider.RESOURCE_URL,
+        "funky-content", raw);
+    fail("Should have thrown with body hash in form encoded request");
+  }
+
+  @Test
+  public void testSignedFetch_error401() throws Exception {
+    assertEquals(0, base.getAccessTokenRemoveCount());
+    serviceProvider.setConsumerUnauthorized(true);
+    serviceProvider.setVagueErrors(true);
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertNull(response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("Should return sent request", errorText, "GET /data");
+    checkStringContains("Should return response", errorText, "HTTP/1.1 401");
+    checkStringContains("Should return response", errorText, "some vague error");
+    assertEquals(0, base.getAccessTokenRemoveCount());
+  }
+
+  @Test
+  public void testSignedFetch_error403() throws Exception {
+    assertEquals(0, base.getAccessTokenRemoveCount());
+    serviceProvider.setConsumersThrottled(true);
+    serviceProvider.setVagueErrors(true);
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertNull(response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("Should return sent request", errorText, "GET /data");
+    checkStringContains("Should return response", errorText, "HTTP/1.1 403");
+    checkStringContains("Should return response", errorText, "some vague error");
+    assertEquals(0, base.getAccessTokenRemoveCount());
+  }
+
+  @Test
+  public void testSignedFetch_unnamedConsumerKey() throws Exception {
+    BasicOAuthStoreConsumerKeyAndSecret defaultKey = new BasicOAuthStoreConsumerKeyAndSecret(
+        null, FakeOAuthServiceProvider.PRIVATE_KEY_TEXT, KeyType.RSA_PRIVATE, "foo", null);
+    base.setDefaultKey(defaultKey);
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, "opensocial_viewer_id", "v"));
+    assertTrue(contains(queryParams, "opensocial_app_id", "app"));
+    assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "container.com"));
+    assertTrue(contains(queryParams, "xoauth_signature_publickey", "foo"));
+    assertTrue(contains(queryParams, "xoauth_public_key", "foo"));
+  }
+
+  @Test
+  public void testSignedFetch_extraQueryParameters() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?foo=bar&foo=baz");
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, "opensocial_viewer_id", "v"));
+    assertTrue(contains(queryParams, "opensocial_app_id", "app"));
+    assertTrue(contains(queryParams, OAuth.OAUTH_CONSUMER_KEY, "signedfetch"));
+    assertTrue(contains(queryParams, "xoauth_signature_publickey", "foo"));
+    assertTrue(contains(queryParams, "xoauth_public_key", "foo"));
+  }
+
+  @Test
+  public void testNoSignViewer() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    client.getBaseArgs().setSignViewer(false);
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertFalse(contains(queryParams, "opensocial_viewer_id", "v"));
+  }
+
+  @Test
+  public void testNoSignOwner() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    client.getBaseArgs().setSignOwner(false);
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertFalse(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, "opensocial_viewer_id", "v"));
+  }
+
+  @Test
+  public void testTrickyParametersInQuery() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    String tricky = "%6fpensocial_owner_id=gotcha";
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + '?' + tricky);
+    assertEquals(OAuthError.INVALID_PARAMETER.name(),
+        resp.getMetadata().get(OAuthResponseParams.ERROR_CODE));
+    checkStringContains("Wrong error text", resp.getMetadata().get("oauthErrorText"),
+        "Invalid parameter name opensocial_owner_id, applications may not override " +
+        "oauth, xoauth, or opensocial parameters");
+  }
+
+  @Test
+  public void testTrickyParametersInBody() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    String tricky = "%6fpensocial_owner_id=gotcha";
+    HttpResponse resp = client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, tricky);
+    assertEquals(OAuthError.INVALID_PARAMETER.name(),
+        resp.getMetadata().get(OAuthResponseParams.ERROR_CODE));
+    checkStringContains("Wrong error text", resp.getMetadata().get("oauthErrorText"),
+        "Invalid parameter name opensocial_owner_id, applications may not override " +
+        "oauth, xoauth, or opensocial parameters");
+  }
+
+  @Test
+  public void testGetNoQuery() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertTrue(contains(queryParams, "opensocial_viewer_id", "v"));
+  }
+
+  @Test
+  public void testGetWithQuery() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?a=b");
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "a", "b"));
+  }
+
+  @Test
+  public void testGetWithQueryMultiParam() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?a=b&a=c");
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "a", "b"));
+    assertTrue(contains(queryParams, "a", "c"));
+  }
+
+  @Test
+  public void testValidParameterCharacters() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    String weird = "~!@$*()-_[]:,./";
+    HttpResponse resp = client.sendGet(
+        FakeOAuthServiceProvider.RESOURCE_URL + '?' + weird + "=foo");
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, weird, "foo"));
+  }
+
+
+  @Test
+  public void testPostNoQueryNoData() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, null);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "opensocial_owner_id", "o"));
+    assertEquals("", resp.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER));
+  }
+
+  @Test
+  public void testPostWithQueryNoData() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendFormPost(
+        FakeOAuthServiceProvider.RESOURCE_URL + "?name=value", null);
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "name", "value"));
+    assertEquals("", resp.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER));
+  }
+
+  @Test
+  public void testPostNoQueryWithData() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendFormPost(
+        FakeOAuthServiceProvider.RESOURCE_URL, "name=value");
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertFalse(contains(queryParams, "name", "value"));
+    assertEquals("name=value", resp.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER));
+  }
+
+  @Test
+  public void testPostWithQueryWithData() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendFormPost(
+        FakeOAuthServiceProvider.RESOURCE_URL + "?queryName=queryValue", "name=value");
+    List<Parameter> queryParams = OAuth.decodeForm(resp.getResponseAsString());
+    assertTrue(contains(queryParams, "queryName", "queryValue"));
+    assertEquals("name=value", resp.getHeader(FakeOAuthServiceProvider.BODY_ECHO_HEADER));
+  }
+
+  @Test
+  public void testStripOpenSocialParamsFromQuery() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp =
+        client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL + "?opensocial_foo=bar", null);
+    assertEquals(OAuthError.INVALID_PARAMETER.name(),
+        resp.getMetadata().get(OAuthResponseParams.ERROR_CODE));
+    checkStringContains("Wrong error text", resp.getMetadata().get("oauthErrorText"),
+        "Invalid parameter name opensocial_foo");
+  }
+
+  @Test
+  public void testStripOAuthParamsFromQuery() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp =
+        client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL + "?oauth_foo=bar", "name=value");
+    assertEquals(OAuthError.INVALID_PARAMETER.name(),
+        resp.getMetadata().get(OAuthResponseParams.ERROR_CODE));
+    checkStringContains("Wrong error text", resp.getMetadata().get("oauthErrorText"),
+        "Invalid parameter name oauth_foo");
+  }
+
+  @Test
+  public void testStripOpenSocialParamsFromBody() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp =
+        client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "opensocial_foo=bar");
+    assertEquals(OAuthError.INVALID_PARAMETER.name(),
+        resp.getMetadata().get(OAuthResponseParams.ERROR_CODE));
+    checkStringContains("Wrong error text", resp.getMetadata().get("oauthErrorText"),
+        "Invalid parameter name opensocial_foo");
+  }
+
+  @Test
+  public void testStripOAuthParamsFromBody() throws Exception {
+    MakeRequestClient client = makeSignedFetchClient("o", "v", "http://www.example.com/app");
+    HttpResponse resp = client.sendFormPost(FakeOAuthServiceProvider.RESOURCE_URL, "oauth_foo=bar");
+    assertEquals(OAuthError.INVALID_PARAMETER.name(),
+        resp.getMetadata().get(OAuthResponseParams.ERROR_CODE));
+    checkStringContains("Wrong error text", resp.getMetadata().get("oauthErrorText"),
+        "Invalid parameter name oauth_foo");
+  }
+
+  // Test we can refresh an expired access token.
+  @Test
+  public void testAccessTokenExpires_onClient() throws Exception {
+    serviceProvider.setSessionExtension(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    clock.incrementSeconds(FakeOAuthServiceProvider.TOKEN_EXPIRATION_SECONDS + 1);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=3");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(3, serviceProvider.getResourceAccessCount());
+
+    clock.incrementSeconds(FakeOAuthServiceProvider.TOKEN_EXPIRATION_SECONDS + 1);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=4");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(3, serviceProvider.getAccessTokenCount());
+    assertEquals(4, serviceProvider.getResourceAccessCount());
+
+    checkEmptyLog();
+  }
+
+  // Tests the case where the server doesn't tell us when the token will expire.  This requires
+  // an extra round trip to discover that the token has expired.
+  @Test
+  public void testAccessTokenExpires_onClientNoPredictedExpiration() throws Exception {
+    serviceProvider.setSessionExtension(true);
+    serviceProvider.setReportExpirationTimes(false);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    clock.incrementSeconds(FakeOAuthServiceProvider.TOKEN_EXPIRATION_SECONDS + 1);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(3, serviceProvider.getResourceAccessCount());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=3");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(4, serviceProvider.getResourceAccessCount());
+
+    clock.incrementSeconds(FakeOAuthServiceProvider.TOKEN_EXPIRATION_SECONDS + 1);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=4");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(3, serviceProvider.getAccessTokenCount());
+    assertEquals(6, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testAccessTokenExpires_onServer() throws Exception {
+    serviceProvider.setSessionExtension(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    // clears oauthState
+    client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    clock.incrementSeconds(FakeOAuthServiceProvider.TOKEN_EXPIRATION_SECONDS + 1);
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+  }
+
+  @Test
+  public void testAccessTokenExpired_andRevoked() throws Exception {
+    serviceProvider.setSessionExtension(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    clock.incrementSeconds(FakeOAuthServiceProvider.TOKEN_EXPIRATION_SECONDS + 1);
+    serviceProvider.revokeAllAccessTokens();
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals("", response.getResponseAsString());
+    assertEquals(2, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    client.approveToken("user_data=renewed");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals(2, serviceProvider.getRequestTokenCount());
+    assertEquals(3, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+    assertEquals("User data is renewed", response.getResponseAsString());
+    checkLogContains("oauth_token_secret=REMOVED");
+  }
+
+  @Test
+  public void testBadSessionHandle() throws Exception {
+    serviceProvider.setSessionExtension(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    assertEquals(1, serviceProvider.getRequestTokenCount());
+    assertEquals(1, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    clock.incrementSeconds(FakeOAuthServiceProvider.TOKEN_EXPIRATION_SECONDS + 1);
+    serviceProvider.changeAllSessionHandles();
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals("", response.getResponseAsString());
+    assertEquals(2, serviceProvider.getRequestTokenCount());
+    assertEquals(2, serviceProvider.getAccessTokenCount());
+    assertEquals(1, serviceProvider.getResourceAccessCount());
+
+    client.approveToken("user_data=renewed");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL + "?cb=1");
+    assertEquals(2, serviceProvider.getRequestTokenCount());
+    assertEquals(3, serviceProvider.getAccessTokenCount());
+    assertEquals(2, serviceProvider.getResourceAccessCount());
+    assertEquals("User data is renewed", response.getResponseAsString());
+    checkLogContains("oauth_session_handle=REMOVED");
+  }
+
+  @Test
+  public void testExtraParamsRejected() throws Exception {
+    serviceProvider.setRejectExtraParams(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("parameter_rejected", response.getMetadata().get("oauthError"));
+  }
+
+  @Test
+  public void testExtraParamsSuppressed() throws Exception {
+    serviceProvider.setRejectExtraParams(true);
+    MakeRequestClient client = makeStrictNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+  }
+
+  @Test
+  public void testCanRetrieveAccessTokenData() throws Exception {
+    serviceProvider.setReturnAccessTokenData(true);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    assertEquals("application/json; charset=UTF-8", response.getHeader("Content-Type"));
+    JSONObject json = new JSONObject(response.getResponseAsString());
+    assertEquals("userid value", json.get("userid"));
+    assertEquals("xoauth_stuff value", json.get("xoauth_stuff"));
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+  }
+
+  @Test
+  public void testAccessTokenData_noOAuthParams() throws Exception {
+    serviceProvider.setReturnAccessTokenData(true);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    JSONObject json = new JSONObject(response.getResponseAsString());
+    assertEquals("userid value", json.get("userid"));
+    assertEquals("xoauth_stuff value", json.get("xoauth_stuff"));
+    assertEquals(2, json.length());
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+  }
+
+  @Test(expected=RuntimeException.class)
+  public void testAccessTokenData_noDirectRequest() throws Exception {
+    serviceProvider.setReturnAccessTokenData(true);
+
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+
+    client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    fail("Service provider should have rejected bogus request to access token URL");
+  }
+
+  @Test
+  public void testNextFetchReturnsNull() throws Exception {
+    serviceProvider.setReturnNull(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    assertEquals("MISSING_SERVER_RESPONSE", response.getMetadata().get("oauthError"));
+    assertEquals("", response.getResponseAsString());
+    String oauthErrorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("should say no response", oauthErrorText, "No response from server");
+    checkStringContains("should show request", oauthErrorText,
+        "GET /request?param=foo&opensocial_owner_id=owner");
+    checkStringContains("should log empty response", oauthErrorText, "Received response 1:\n\n");
+    checkLogContains("No response from server");
+    checkLogContains("GET /request?param=foo&opensocial_owner_id=owner");
+    checkLogContains("OAuth error [MISSING_SERVER_RESPONSE, No response from server] for " +
+        "application http://www.example.com/gadget.xml");
+  }
+
+  @Test
+  public void testNextFetchThrowsGadgetException() throws Exception {
+    serviceProvider.setThrow(
+        new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, "mildly wrong"));
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+    assertEquals("MISSING_SERVER_RESPONSE", response.getMetadata().get("oauthError"));
+    assertEquals("", response.getResponseAsString());
+    String oauthErrorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("should say no response", oauthErrorText, "No response from server");
+    checkStringContains("should show request", oauthErrorText,
+        "GET /request?param=foo&opensocial_owner_id=owner");
+    checkStringContains("should log empty response", oauthErrorText, "Received response 1:\n\n");
+    checkLogContains("No response from server");
+    checkLogContains("GET /request?param=foo&opensocial_owner_id=owner");
+    checkLogContains("OAuth error [MISSING_SERVER_RESPONSE, No response from server] for " +
+        "application http://www.example.com/gadget.xml");
+    checkLogContains("GadgetException");
+    checkLogContains("mildly wrong");
+  }
+
+  @Test
+  public void testNextFetchThrowsRuntimeException() throws Exception {
+    serviceProvider.setThrow(new RuntimeException("very, very wrong"));
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    try {
+      client.sendGet(FakeOAuthServiceProvider.ACCESS_TOKEN_URL);
+      fail("Should have thrown");
+    } catch (RuntimeException e) {
+      // good
+    }
+    //checkLogContains("OAuth fetch unexpected fatal erro");
+    checkLogContains("GET /request?param=foo&opensocial_owner_id=owner");
+    checkLogContains("OAuth error [very, very wrong] for " +
+        "application http://www.example.com/gadget.xml");
+    checkLogContains("RuntimeException");
+    checkLogContains("very, very wrong");
+  }
+
+  @Test
+  public void testTrustedParams() throws Exception {
+    serviceProvider.setCheckTrustedParams(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    client.setTrustedParam("oauth_magic", "foo");
+    client.setTrustedParam("opensocial_magic", "bar");
+    client.setTrustedParam("xoauth_magic", "quux");
+
+    client.setTrustedParam("opensocial_owner_id", "overridden_opensocial_owner_id");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    assertEquals(12, serviceProvider.getTrustedParamCount());
+  }
+
+  /**
+   * Test different behaviors of trusted parameters.
+   * 1) pass two parameters with same name, the latter will win.
+   * 2) parameter name starting with 'oauth' 'oauth' or 'opensocial'.
+   * 3) trusted parameter can override existing parameter.
+   */
+  @Test
+  public void testTrustedParamsMisc() throws Exception {
+    serviceProvider.setCheckTrustedParams(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    client.setTrustedParam("oauth_magic", "foo");
+    client.setTrustedParam("opensocial_magic", "bar");
+
+    client.setTrustedParam("xoauth_magic", "quux_overridden");
+    client.setTrustedParam("xoauth_magic", "quux");
+
+    client.setTrustedParam("opensocial_owner_id", "overridden_opensocial_owner_id");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    assertEquals(12, serviceProvider.getTrustedParamCount());
+  }
+
+  /**
+   * Test trusted parameters will always be sent when signOwner and signViewer
+   * are false.
+   */
+  @Test
+  public void testAlwaysAppendTrustedParams() throws Exception {
+    serviceProvider.setCheckTrustedParams(true);
+    MakeRequestClient client = makeStrictNonSocialClient("owner", "owner", GADGET_URL);
+    client.setTrustedParam("oauth_magic", "foo");
+    client.setTrustedParam("opensocial_magic", "bar");
+    client.setTrustedParam("xoauth_magic", "quux");
+
+    client.setTrustedParam("opensocial_owner_id", "overridden_opensocial_owner_id");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("", response.getResponseAsString());
+    client.approveToken("user_data=hello-oauth");
+
+    response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals("User data is hello-oauth", response.getResponseAsString());
+    assertEquals(12, serviceProvider.getTrustedParamCount());
+  }
+
+  /**
+   * Test invalid trusted parameters which are not prefixed with 'oauth' 'xoauth' or 'opensocial'.
+   */
+  @Test
+  public void testTrustedParamsInvalidParameter() throws Exception {
+    serviceProvider.setCheckTrustedParams(true);
+    MakeRequestClient client = makeNonSocialClient("owner", "owner", GADGET_URL);
+    client.setTrustedParam("oauth_magic", "foo");
+    client.setTrustedParam("opensocial_magic", "bar");
+    client.setTrustedParam("xoauth_magic", "quux");
+    client.setTrustedParam("opensocial_owner_id", "overridden_opensocial_owner_id");
+    client.setTrustedParam("invalid_trusted_parameter", "invalid");
+
+    HttpResponse response = client.sendGet(FakeOAuthServiceProvider.RESOURCE_URL);
+    assertEquals(HttpResponse.SC_FORBIDDEN, response.getHttpStatusCode());
+  }
+
+
+
+  // Checks whether the given parameter list contains the specified
+  // key/value pair
+  private boolean contains(List<Parameter> params, String key, String value) {
+    for (Parameter p : params) {
+      if (p.getKey().equals(key) && p.getValue().equals(value)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private void checkContains(List<Parameter> params, String key, String value) {
+    for (Parameter p : params) {
+      if (p.getKey().equals(key)) {
+        assertEquals(value, p.getValue());
+        return;
+      }
+    }
+    fail("List did not contain " + key + '=' + value + "; instead was " + params);
+  }
+
+  private String getLogText() {
+    StringBuilder logText = new StringBuilder();
+    for (LogRecord record : logRecords) {
+      logText.append(record.getMessage());
+      if (record.getThrown() != null) {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        record.getThrown().printStackTrace(pw);
+        pw.flush();
+        logText.append(sw.toString());
+      }
+    }
+    return logText.toString();
+  }
+
+  private void checkLogContains(String text) {
+    if ((logger.getLevel()!=null)&&(logger.getLevel().equals(Level.OFF))) {
+        return;
+    }
+    String logText = getLogText();
+    if (!logText.contains(text)) {
+      fail("Should have logged '" + text + "', instead got " + logText);
+    }
+  }
+
+  private void checkEmptyLog() {
+    assertEquals("", getLogText());
+  }
+
+  private void checkStringContains(String message, String text, String expected) {
+    if (!text.contains(expected)) {
+      fail(message + ", expected [" + expected + "], got + [" + text + ']');
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthResponseParamsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthResponseParamsTest.java
new file mode 100644
index 0000000..93d7dde
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/OAuthResponseParamsTest.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.easymock.EasyMock;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class OAuthResponseParamsTest {
+
+  private static final String APP = "http://app/example.xml";
+
+  private HttpRequest origRequest;
+  private SecurityToken token;
+  private BlobCrypter crypter;
+  private OAuthResponseParams params;
+
+  @Before
+  public void setUp() {
+    crypter = new BasicBlobCrypter("abcdefafadfaxxxx".getBytes());
+    token = EasyMock.createMock(SecurityToken.class);
+    origRequest = new HttpRequest(Uri.parse("http://originalrequest/"));
+    EasyMock.expect(token.getAppUrl()).andStubReturn(APP);
+    EasyMock.replay(token);
+    params = new OAuthResponseParams(token, origRequest, crypter);
+  }
+
+  @Test
+  public void testSetAndGet() {
+    params.getNewClientState().setAccessToken("access");
+    params.setAznUrl("aznurl");
+    assertFalse(params.sendTraceToClient());
+    params.setSendTraceToClient(true);
+    assertTrue(params.sendTraceToClient());
+    assertEquals("access", params.getNewClientState().getAccessToken());
+    assertEquals("aznurl", params.getAznUrl());
+  }
+
+  @Test
+  public void testAddParams() {
+    params.getNewClientState().setAccessToken("access");
+    params.setAznUrl("aznurl");
+    OAuthRequestException e = new OAuthRequestException(OAuthError.BAD_OAUTH_CONFIGURATION, "whoa there cowboy");
+
+    HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    params.addToResponse(responseBuilder, e);
+    HttpResponse response = responseBuilder.create();
+    assertEquals("BAD_OAUTH_CONFIGURATION", response.getMetadata().get("oauthError"));
+    String errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("error text returned", errorText, "whoa there cowboy");
+    assertEquals("aznurl", response.getMetadata().get("oauthApprovalUrl"));
+    assertNotNull(response.getMetadata().get("oauthState"));
+    assertTrue(response.getMetadata().get("oauthState").length() > 10);
+  }
+
+  @Test
+  public void testSendTraceToClient() {
+    OAuthRequestException e = new OAuthRequestException(OAuthError.BAD_OAUTH_CONFIGURATION, "whoa there cowboy");
+    params.addRequestTrace(null, null);
+    params.addRequestTrace(null, null);
+
+    HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    params.addToResponse(responseBuilder, e);
+    HttpResponse response = responseBuilder.create();
+
+    String errorText = response.getMetadata().get("oauthErrorText");
+    assertEquals("whoa there cowboy", errorText);
+
+    params.setSendTraceToClient(true);
+    params.addToResponse(responseBuilder, e);
+    response = responseBuilder.create();
+    errorText = response.getMetadata().get("oauthErrorText");
+    checkStringContains("includes error text", errorText, "whoa there cowboy");
+    checkStringContains("Request 1 logged", errorText, "Sent request 1:\n\n");
+    checkStringContains("Request 2 logged", errorText, "Sent request 2:\n\n");
+  }
+
+  @Test
+  public void testAddEmptyParams() {
+    HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    params.addToResponse(responseBuilder, null);
+    HttpResponse response = responseBuilder.create();
+    assertTrue(response.getMetadata().isEmpty());
+  }
+
+  @Test
+  public void testSawErrorResponse() {
+    HttpRequest req = new HttpRequest(Uri.parse("http://www"));
+    HttpResponse ok = new HttpResponseBuilder().setHttpStatusCode(200).create();
+    HttpResponse redir = new HttpResponseBuilder().setHttpStatusCode(302).create();
+    HttpResponse notFound = new HttpResponseBuilder().setHttpStatusCode(404).create();
+    HttpResponse doh = new HttpResponseBuilder().setHttpStatusCode(502).create();
+
+    OAuthResponseParams params = new OAuthResponseParams(token, origRequest, crypter);
+    assertFalse(params.sawErrorResponse());
+    params.addRequestTrace(req, ok);
+    assertFalse(params.sawErrorResponse());
+    params.addRequestTrace(req, redir);
+    assertFalse(params.sawErrorResponse());
+    params.addRequestTrace(req, null);
+    assertTrue(params.sawErrorResponse());
+
+    params = new OAuthResponseParams(token, origRequest, crypter);
+    params.addRequestTrace(req, notFound);
+    assertTrue(params.sawErrorResponse());
+
+    params = new OAuthResponseParams(token, origRequest, crypter);
+    params.addRequestTrace(req, doh);
+    assertTrue(params.sawErrorResponse());
+    params.addRequestTrace(req, ok);
+    assertTrue(params.sawErrorResponse());
+  }
+
+  @Test
+  public void testException() {
+    HttpRequest req = new HttpRequest(Uri.parse("http://www"));
+    HttpResponse ok = new HttpResponseBuilder().setHttpStatusCode(200).create();
+    params.addRequestTrace(req, ok);
+    OAuthRequestException e = new OAuthRequestException("error", "errorText");
+    checkStringContains(e.toString(), "[error,errorText]");
+    params.addRequestTrace(null, null);
+    Throwable cause = new RuntimeException();
+    e = new OAuthRequestException(OAuthError.UNAUTHENTICATED, "errorText", cause);
+    checkStringContains(e.toString(), "[UNAUTHENTICATED,Unauthenticated OAuth fetch]");
+    assertEquals(cause, e.getCause());
+  }
+
+  @Test
+  public void testNullSafe() {
+    params.addRequestTrace(null, null);
+    new OAuthRequestException("error", "errorText");
+    params.logDetailedWarning("org.apache.shindig.gadgets.oauth.OAuthResponseParamsTest","testNullSafe","wow");
+    params.logDetailedWarning("org.apache.shindig.gadgets.oauth.OAuthResponseParamsTest","testNullSafe","new runtime", new RuntimeException());
+  }
+
+  @Test
+  public void testStripSensitiveFromResponse() {
+    verifyStrip("oauth_token=dbce9de6d6da692b99b39cdcde60fd83&oauth_token_secret=60c1aabe0f6db96" +
+        "f2719956168c08d9d");
+
+    String out = verifyStrip("oauth_token=dbce9de6d6da692b99b39cdcde60fd83&oauth_token_secret" +
+              "=60c1aabe0f6db96f2719956168c08d9d&oauth_session_handle=ABCDEFGH");
+    checkStringContains(out, "oauth_token=dbce");
+    checkStringContains(out, "HTTP/1.1 200");
+
+    out = verifyStrip("oauth_token_secret=x");
+    checkStringContains(out, "oauth_token_secret=REMOVED");
+
+    out = verifyStrip("foo&oauth_token_secret=!@#$%$^&(()&");
+    checkStringContains(out, "foo&oauth_token_secret=REMOVED&");
+  }
+
+  private String verifyStrip(String body) {
+    HttpResponseBuilder resp = new HttpResponseBuilder()
+        .setHttpStatusCode(200)
+        .setHeader("Date", "Date: Fri, 09 Jan 2009 00:35:08 GMT")
+        .setResponseString(body);
+    String out = OAuthResponseParams.filterSecrets(resp.create().toString());
+    if (out.contains("oauth_token_secret")) {
+      checkStringContains("should remove secret", out, "oauth_token_secret=REMOVED");
+    }
+    if (out.contains("oauth_session_handle")) {
+      checkStringContains("should remove handle", out, "oauth_session_handle=REMOVED");
+    }
+    return out;
+  }
+
+  @Test
+  public void testStripSecretsFromRequestHeader() {
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.example.com/foo"));
+    req.setHeader("Authorization", "OAuth opensocial_owner_id=\"owner\", opensocial_viewer_id=" +
+        "\"owner\", opensocial_app_id=\"app\", opensocial_app_url=\"http%3A%2F%2Fwww.examp" +
+        "le.com%2Fheader.xml\", oauth_version=\"1.0\", oauth_timestamp=\"1231461306\", oau" +
+        "th_consumer_key=\"consumer\", oauth_signature_method=\"HMAC-SHA1\", oauth_nonce" +
+        "=\"1231461308333563000\", oauth_session_handle=\"w0zAI1yN5ZRvmBX5kcVdra5%2BbZE%" +
+        "3D\"");
+    String filtered = OAuthResponseParams.filterSecrets(req.toString());
+    checkStringContains(filtered, "oauth_session_handle=REMOVED");
+  }
+
+  @Test
+  public void testStripSecretsFromRequestUrl() {
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.example.com/access?param=foo&openso" +
+        "cial_owner_id=owner&opensocial_viewer_id=owner&opensocial_app_id=app&" +
+        "oauth_session_handle" +
+        "=http%3A%2F%2Fwww.example.com%2Fgadget.xml&oauth_version=1.0&oauth_timestamp=12" +
+        "31461132&oauth_consumer_key=consumer&oauth_signature_method=HMAC-SHA1&oauth_nonce=1" +
+        "231461160262578000&oauth_signature=HuFQ%2BRYTrRzcgsi3al6ld9Msvoo%3D"));
+    String filtered = OAuthResponseParams.filterSecrets(req.toString());
+    checkStringContains(filtered, "oauth_session_handle=REMOVED");
+  }
+
+  private void checkStringContains(String text, String expected) {
+    if (!text.contains(expected)) {
+      fail("expected [" + expected + "], got + [" + text + ']');
+    }
+  }
+
+  private void checkStringContains(String message, String text, String expected) {
+    if (!text.contains(expected)) {
+      fail(message + ", expected [" + expected + "], got + [" + text + ']');
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
new file mode 100644
index 0000000..60bb577
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/testing/FakeOAuthServiceProvider.java
@@ -0,0 +1,877 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth.testing;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+import net.oauth.OAuth;
+import net.oauth.OAuth.Parameter;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthException;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuthServiceProvider;
+import net.oauth.OAuthValidator;
+import net.oauth.SimpleOAuthValidator;
+import net.oauth.signature.RSA_SHA1;
+
+import org.apache.shindig.auth.OAuthConstants;
+import org.apache.shindig.auth.OAuthUtil;
+import org.apache.shindig.auth.OAuthUtil.SignatureType;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.cache.LruCache;
+import org.apache.shindig.common.cache.SoftExpiringCache;
+import org.apache.shindig.common.cache.SoftExpiringCache.CachedObject;
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.common.util.TimeSource;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.oauth.AccessorInfo.OAuthParamLocation;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+public class FakeOAuthServiceProvider implements HttpFetcher {
+
+  public static final String BODY_ECHO_HEADER = "X-Echoed-Body";
+
+  public static final String RAW_BODY_ECHO_HEADER = "X-Echoed-Raw-Body";
+
+  public static final String AUTHZ_ECHO_HEADER = "X-Echoed-Authz";
+
+  public final static String SP_HOST = "http://www.example.com";
+
+  public final static String REQUEST_TOKEN_URL = SP_HOST + "/request?param=foo";
+  public final static String ACCESS_TOKEN_URL = SP_HOST + "/access";
+  public final static String APPROVAL_URL = SP_HOST + "/authorize";
+  public final static String RESOURCE_URL = SP_HOST + "/data";
+  public final static String NOT_FOUND_URL = SP_HOST + "/404";
+  public final static String ERROR_400 = SP_HOST + "/400";
+  public final static String ECHO_URL = SP_HOST + "/echo";
+
+  public final static String CONSUMER_KEY = "consumer";
+  public final static String CONSUMER_SECRET = "secret";
+
+  public final static int TOKEN_EXPIRATION_SECONDS = 60;
+
+  public static final String PRIVATE_KEY_TEXT =
+    "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALRiMLAh9iimur8V" +
+    "A7qVvdqxevEuUkW4K+2KdMXmnQbG9Aa7k7eBjK1S+0LYmVjPKlJGNXHDGuy5Fw/d" +
+    "7rjVJ0BLB+ubPK8iA/Tw3hLQgXMRRGRXXCn8ikfuQfjUS1uZSatdLB81mydBETlJ" +
+    "hI6GH4twrbDJCR2Bwy/XWXgqgGRzAgMBAAECgYBYWVtleUzavkbrPjy0T5FMou8H" +
+    "X9u2AC2ry8vD/l7cqedtwMPp9k7TubgNFo+NGvKsl2ynyprOZR1xjQ7WgrgVB+mm" +
+    "uScOM/5HVceFuGRDhYTCObE+y1kxRloNYXnx3ei1zbeYLPCHdhxRYW7T0qcynNmw" +
+    "rn05/KO2RLjgQNalsQJBANeA3Q4Nugqy4QBUCEC09SqylT2K9FrrItqL2QKc9v0Z" +
+    "zO2uwllCbg0dwpVuYPYXYvikNHHg+aCWF+VXsb9rpPsCQQDWR9TT4ORdzoj+Nccn" +
+    "qkMsDmzt0EfNaAOwHOmVJ2RVBspPcxt5iN4HI7HNeG6U5YsFBb+/GZbgfBT3kpNG" +
+    "WPTpAkBI+gFhjfJvRw38n3g/+UeAkwMI2TJQS4n8+hid0uus3/zOjDySH3XHCUno" +
+    "cn1xOJAyZODBo47E+67R4jV1/gzbAkEAklJaspRPXP877NssM5nAZMU0/O/NGCZ+" +
+    "3jPgDUno6WbJn5cqm8MqWhW1xGkImgRk+fkDBquiq4gPiT898jusgQJAd5Zrr6Q8" +
+    "AO/0isr/3aa6O6NLQxISLKcPDk2NOccAfS/xOtfOz4sJYM3+Bs4Io9+dZGSDCA54" +
+    "Lw03eHTNQghS0A==";
+
+  public static final String CERTIFICATE_TEXT =
+    "-----BEGIN CERTIFICATE-----\n" +
+    "MIIBpjCCAQ+gAwIBAgIBATANBgkqhkiG9w0BAQUFADAZMRcwFQYDVQQDDA5UZXN0\n" +
+    "IFByaW5jaXBhbDAeFw03MDAxMDEwODAwMDBaFw0zODEyMzEwODAwMDBaMBkxFzAV\n" +
+    "BgNVBAMMDlRlc3QgUHJpbmNpcGFsMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB\n" +
+    "gQC0YjCwIfYoprq/FQO6lb3asXrxLlJFuCvtinTF5p0GxvQGu5O3gYytUvtC2JlY\n" +
+    "zypSRjVxwxrsuRcP3e641SdASwfrmzyvIgP08N4S0IFzEURkV1wp/IpH7kH41Etb\n" +
+    "mUmrXSwfNZsnQRE5SYSOhh+LcK2wyQkdgcMv11l4KoBkcwIDAQABMA0GCSqGSIb3\n" +
+    "DQEBBQUAA4GBAGZLPEuJ5SiJ2ryq+CmEGOXfvlTtEL2nuGtr9PewxkgnOjZpUy+d\n" +
+    "4TvuXJbNQc8f4AMWL/tO9w0Fk80rWKp9ea8/df4qMq5qlFWlx6yOLQxumNOmECKb\n" +
+    "WpkUQDIDJEoFUzKMVuJf4KO/FJ345+BNLGgbJ6WujreoM1X/gYfdnJ/J\n" +
+    "-----END CERTIFICATE-----";
+
+  enum State {
+    PENDING,
+    APPROVED_UNCLAIMED,
+    APPROVED,
+    REVOKED,
+  }
+
+  private class TokenState {
+    String tokenSecret;
+    State state;
+    String userData;
+    String sessionHandle;
+    long issued;
+    String callbackUrl;
+    String verifier;
+
+    public TokenState(String tokenSecret, String callbackUrl) {
+      this.tokenSecret = tokenSecret;
+      this.state = State.PENDING;
+      this.userData = null;
+      this.callbackUrl = callbackUrl;
+    }
+
+    public void approveToken() {
+      // Waiting for the consumer to claim the token
+      state = State.APPROVED_UNCLAIMED;
+      issued = clock.currentTimeMillis();
+      if (callbackUrl != null) {
+        verifier = Crypto.getRandomString(8);
+      }
+    }
+
+    public void claimToken() {
+      // consumer taking the token
+      state = State.APPROVED;
+      sessionHandle = Crypto.getRandomString(8);
+    }
+
+    public void renewToken() {
+      issued = clock.currentTimeMillis();
+    }
+
+    public void revokeToken() {
+      state = State.REVOKED;
+    }
+
+    public State getState() {
+      return state;
+    }
+
+    public String getSecret() {
+      return tokenSecret;
+    }
+
+    public void setUserData(String userData) {
+      this.userData = userData;
+    }
+
+    public String getUserData() {
+      return userData;
+    }
+  }
+
+  /**
+   * Table of OAuth access tokens
+   */
+  private final HashMap<String, TokenState> tokenState;
+  private final OAuthConsumer signedFetchConsumer;
+  private final OAuthConsumer oauthConsumer;
+  private final TimeSource clock;
+  private final SoftExpiringCache<String, OAuthMessage> nonceCache;
+
+  private boolean unauthorized = false;
+  private boolean throttled = false;
+  private boolean vagueErrors = false;
+  private boolean reportExpirationTimes = true;
+  private boolean sessionExtension = false;
+  private boolean rejectExtraParams = false;
+  private boolean returnAccessTokenData = false;
+
+  private int requestTokenCount = 0;
+
+  private int accessTokenCount = 0;
+
+  private int resourceAccessCount = 0;
+
+  private Set<OAuthParamLocation> validParamLocations;
+
+  private boolean returnNull;
+
+  private GadgetException gadgetException;
+
+  private RuntimeException runtimeException;
+
+  private boolean checkTrustedParams;
+
+  private int trustedParamCount;
+
+  private SecurityToken expectedRequestSecurityToken;
+
+  public FakeOAuthServiceProvider(TimeSource clock) {
+    this.clock = clock;
+    OAuthServiceProvider provider = new OAuthServiceProvider(
+        REQUEST_TOKEN_URL, APPROVAL_URL, ACCESS_TOKEN_URL);
+
+    signedFetchConsumer = new OAuthConsumer(null, null, null, null);
+    signedFetchConsumer.setProperty(RSA_SHA1.X509_CERTIFICATE, CERTIFICATE_TEXT);
+
+    oauthConsumer = new OAuthConsumer(null, CONSUMER_KEY, CONSUMER_SECRET, provider);
+
+    tokenState = Maps.newHashMap();
+    validParamLocations = Sets.newHashSet();
+    validParamLocations.add(OAuthParamLocation.URI_QUERY);
+    nonceCache =
+        new SoftExpiringCache<String, OAuthMessage>(new LruCache<String, OAuthMessage>(10000));
+    nonceCache.setTimeSource(clock);
+  }
+
+  public void setVagueErrors(boolean vagueErrors) {
+    this.vagueErrors = vagueErrors;
+  }
+
+  public void setSessionExtension(boolean sessionExtension) {
+    this.sessionExtension = sessionExtension;
+  }
+
+  public void setReportExpirationTimes(boolean reportExpirationTimes) {
+    this.reportExpirationTimes = reportExpirationTimes;
+  }
+
+  public void setRejectExtraParams(boolean rejectExtraParams) {
+    this.rejectExtraParams = rejectExtraParams;
+  }
+
+  public void setReturnAccessTokenData(boolean returnAccessTokenData) {
+    this.returnAccessTokenData = returnAccessTokenData;
+  }
+
+  public void addParamLocation(OAuthParamLocation paramLocation) {
+    validParamLocations.add(paramLocation);
+  }
+
+  public void removeParamLocation(OAuthParamLocation paramLocation) {
+    validParamLocations.remove(paramLocation);
+  }
+
+  public void setParamLocation(OAuthParamLocation paramLocation) {
+    validParamLocations.clear();
+    validParamLocations.add(paramLocation);
+  }
+
+  public HttpResponse fetch(HttpRequest request) throws GadgetException {
+    if (returnNull) {
+      return null;
+    }
+    if (gadgetException != null) {
+      throw gadgetException;
+    }
+    if (runtimeException != null) {
+      throw runtimeException;
+    }
+    if (request.getFollowRedirects()) {
+      throw new RuntimeException("Not supposed to follow OAuth redirects");
+    }
+    String url = request.getUri().toString();
+    try {
+      if (url.startsWith(REQUEST_TOKEN_URL)) {
+        checkSecurityToken(request);
+        ++requestTokenCount;
+        return handleRequestTokenUrl(request);
+      } else if (url.startsWith(ACCESS_TOKEN_URL)) {
+        checkSecurityToken(request);
+        ++accessTokenCount;
+        return handleAccessTokenUrl(request);
+      } else if (url.startsWith(RESOURCE_URL)){
+        ++resourceAccessCount;
+        return handleResourceUrl(request);
+      } else if (url.startsWith(NOT_FOUND_URL)) {
+        return handleNotFoundUrl(request);
+      } else if (url.startsWith(ERROR_400)) {
+        return handleError400Url(request);
+      } else if (url.startsWith(ECHO_URL)) {
+        return handleEchoUrl(request);
+      }
+    } catch (Exception e) {
+      throw new RuntimeException("Problem with request for URL " + url, e);
+    }
+    throw new RuntimeException("Unexpected request for " + url);
+  }
+
+  private void checkSecurityToken(HttpRequest request) {
+    if (request.getSecurityToken() == null) {
+      throw new RuntimeException("Security token should not be null" );
+    }
+    if (!request.getSecurityToken().isAnonymous()) {
+      throw new RuntimeException("Expected an anonymous security token" );
+    }
+    if (expectedRequestSecurityToken != null) {
+      if (!expectedRequestSecurityToken.getAppUrl().equals( request.getSecurityToken().getAppUrl() )) {
+        throw new RuntimeException("Security token AppUrl mismatch" );
+      }
+    }
+  }
+
+  private HttpResponse handleRequestTokenUrl(HttpRequest request)
+      throws Exception {
+    MessageInfo info = parseMessage(request);
+    String requestConsumer = info.message.getParameter(OAuth.OAUTH_CONSUMER_KEY);
+    OAuthConsumer consumer;
+    if (CONSUMER_KEY.equals(requestConsumer)) {
+      consumer = oauthConsumer;
+    } else {
+      return makeOAuthProblemReport(
+          OAuth.Problems.CONSUMER_KEY_UNKNOWN, "invalid consumer: " + requestConsumer,
+          HttpResponse.SC_FORBIDDEN);
+    }
+    if (throttled) {
+      return makeOAuthProblemReport(
+          OAuth.Problems.CONSUMER_KEY_REFUSED, "exceeded quota exhausted",
+          HttpResponse.SC_FORBIDDEN);
+    }
+    if (unauthorized) {
+      return makeOAuthProblemReport(
+          OAuth.Problems.PERMISSION_DENIED, "user refused access",
+          HttpResponse.SC_BAD_REQUEST);
+    }
+    if (rejectExtraParams) {
+      String extra = hasExtraParams(info.message);
+      if (extra != null) {
+        return makeOAuthProblemReport(OAuth.Problems.PARAMETER_REJECTED, extra,
+            HttpResponse.SC_BAD_REQUEST);
+      }
+    }
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    validateMessage(accessor, info, true);
+    String requestToken = Crypto.getRandomString(16);
+    String requestTokenSecret = Crypto.getRandomString(16);
+    String callbackUrl = info.message.getParameter(OAuth.OAUTH_CALLBACK);
+    tokenState.put(
+        requestToken, new TokenState(requestTokenSecret, callbackUrl));
+    List<Parameter> responseParams = OAuth.newList(
+        "oauth_token", requestToken,
+        "oauth_token_secret", requestTokenSecret);
+    if (callbackUrl != null) {
+      responseParams.add(new Parameter(OAuth.OAUTH_CALLBACK_CONFIRMED, "true"));
+    }
+    return new HttpResponse(OAuth.formEncode(responseParams));
+  }
+
+  private String hasExtraParams(OAuthMessage message) {
+    for (Entry<String, String> param : OAuthUtil.getParameters(message)) {
+      // Our request token URL allows "param" as a query param, and also oauth params of course.
+      if (!param.getKey().startsWith("oauth") && !param.getKey().equals("param")) {
+        return param.getKey();
+      }
+    }
+    return null;
+  }
+
+  private HttpResponse makeOAuthProblemReport(String code, String text, int rc) throws IOException {
+    if (vagueErrors) {
+      return new HttpResponseBuilder()
+          .setHttpStatusCode(rc)
+          .setResponseString("some vague error")
+          .create();
+    }
+    OAuthMessage msg = new OAuthMessage(null, null, null);
+    msg.addParameter("oauth_problem", code);
+    msg.addParameter("oauth_problem_advice", text);
+    return new HttpResponseBuilder()
+        .setHttpStatusCode(rc)
+        .addHeader("WWW-Authenticate", msg.getAuthorizationHeader("realm"))
+        .create();
+  }
+
+  // Loosely based off net.oauth.OAuthServlet, and even more loosely related
+  // to the OAuth specification
+  private MessageInfo parseMessage(HttpRequest request) {
+    MessageInfo info = new MessageInfo();
+    info.request = request;
+    String method = request.getMethod();
+    ParsedUrl parsed = new ParsedUrl(request.getUri().toString());
+
+    List<OAuth.Parameter> params = Lists.newArrayList();
+    params.addAll(parsed.getParsedQuery());
+
+    if (!validParamLocations.contains(OAuthParamLocation.URI_QUERY)) {
+      // Make sure nothing OAuth related ended up in the query string
+      for (OAuth.Parameter p : params) {
+        if (p.getKey().contains("oauth_")) {
+          throw new RuntimeException("Found unexpected query param " + p.getKey());
+        }
+      }
+    }
+
+    // Parse authorization header
+    if (validParamLocations.contains(OAuthParamLocation.AUTH_HEADER)) {
+      String aznHeader = request.getHeader("Authorization");
+      if (aznHeader != null) {
+        info.aznHeader = aznHeader;
+        for (OAuth.Parameter p : OAuthMessage.decodeAuthorization(aznHeader)) {
+          if (!p.getKey().equalsIgnoreCase("realm")) {
+            params.add(p);
+          }
+        }
+      }
+    }
+
+    // Parse body
+    info.body = request.getPostBodyAsString();
+    try {
+      info.rawBody = IOUtils.toByteArray(request.getPostBody());
+    } catch (IOException e) {
+      throw new RuntimeException("Can't read post body bytes", e);
+    }
+    if (OAuth.isFormEncoded(request.getHeader("Content-Type"))) {
+      params.addAll(OAuth.decodeForm(request.getPostBodyAsString()));
+      // If we're not configured to pass oauth parameters in the post body, double check
+      // that they didn't end up there.
+      if (!validParamLocations.contains(OAuthParamLocation.POST_BODY)) {
+        if (info.body.contains("oauth_")) {
+          throw new RuntimeException("Found unexpected post body data" + info.body);
+        }
+      }
+    }
+
+    // Return the lot
+    info.message = new OAuthMessage(method, parsed.getLocation(), params);
+
+    // Check for trusted parameters
+    if (checkTrustedParams) {
+      if (!"foo".equals(OAuthUtil.getParameter(info.message, "oauth_magic"))) {
+        throw new RuntimeException("no oauth_trusted=foo parameter");
+      }
+      if (!"bar".equals(OAuthUtil.getParameter(info.message, "opensocial_magic"))) {
+        throw new RuntimeException("no opensocial_trusted=foo parameter");
+      }
+      if (!"quux".equals(OAuthUtil.getParameter(info.message, "xoauth_magic"))) {
+        throw new RuntimeException("no xoauth_magic=quux parameter");
+      }
+      if (!"overridden_opensocial_owner_id".equals(
+          OAuthUtil.getParameter(info.message, "opensocial_owner_id"))) {
+        throw new RuntimeException("opensocial_owner_id should be overridden");
+      }
+      trustedParamCount += 4;
+    }
+
+    return info;
+  }
+
+  /**
+   * Bundles information about a received OAuthMessage.
+   */
+  private static class MessageInfo {
+    public OAuthMessage message;
+    public String aznHeader;
+    public String body;
+    public byte[] rawBody;
+    public HttpRequest request;
+  }
+
+  /**
+   * Utility class for parsing OAuth URLs.
+   */
+  private static class ParsedUrl {
+    String location = null;
+    String query = null;
+    List<OAuth.Parameter> decodedQuery = null;
+
+    public ParsedUrl(String url) {
+      int queryIndex = url.indexOf('?');
+      if (queryIndex != -1) {
+        query = url.substring(queryIndex+1, url.length());
+        location = url.substring(0, queryIndex);
+      } else {
+        location = url;
+      }
+    }
+
+    public String getLocation() {
+      return location;
+    }
+
+    public String getRawQuery() {
+      return query;
+    }
+
+    public List<OAuth.Parameter> getParsedQuery() {
+      if (decodedQuery == null) {
+        if (query != null) {
+          decodedQuery = OAuth.decodeForm(query);
+        } else {
+          decodedQuery = Lists.newArrayList();
+        }
+      }
+      return decodedQuery;
+    }
+
+    public String getQueryParam(String name) {
+      for (OAuth.Parameter p : getParsedQuery()) {
+        if (p.getKey().equals(name)) {
+          return p.getValue();
+        }
+      }
+      return null;
+    }
+  }
+
+  /**
+   * Used to fake a browser visit to approve a token.
+   *
+   * @return a redirect URL, which may or may not include an oauth verifier
+   */
+  public String browserVisit(String url) throws Exception {
+    ParsedUrl parsed = new ParsedUrl(url);
+    String requestToken = parsed.getQueryParam("oauth_token");
+    TokenState state = tokenState.get(requestToken);
+    state.approveToken();
+    // Not part of the OAuth spec, just a handy thing for testing.
+    state.setUserData(parsed.getQueryParam("user_data"));
+    if (state.callbackUrl != null) {
+      UriBuilder callback = UriBuilder.parse(state.callbackUrl);
+      callback.addQueryParameter(OAuth.OAUTH_VERIFIER, state.verifier);
+      return callback.toString();
+    }
+    return null;
+  }
+
+  public static class TokenPair {
+    public final String token;
+    public final String secret;
+
+    public TokenPair(String token, String secret) {
+      this.token = token;
+      this.secret = secret;
+    }
+  }
+
+  /**
+   * Generate a preapproved request token for the specified user data.
+   *
+   * @param userData
+   * @return the request token and secret
+   */
+  public TokenPair getPreapprovedToken(String userData) {
+    String requestToken = Crypto.getRandomString(16);
+    String requestTokenSecret = Crypto.getRandomString(16);
+    TokenState state = new TokenState(requestTokenSecret, null);
+    state.approveToken();
+    state.setUserData(userData);
+    tokenState.put(requestToken, state);
+    return new TokenPair(requestToken, requestTokenSecret);
+  }
+
+  /**
+   * Used to revoke all access tokens issued by this service provider.
+   *
+   * @throws Exception
+   */
+  public void revokeAllAccessTokens() throws Exception {
+    for (TokenState state : tokenState.values()) {
+      state.revokeToken();
+    }
+  }
+
+  /**
+   * Changes session handles to prevent renewal from working.
+   */
+  public void changeAllSessionHandles() throws Exception {
+    for (TokenState state : tokenState.values()) {
+      state.sessionHandle = null;
+    }
+  }
+
+  private HttpResponse handleAccessTokenUrl(HttpRequest request)
+      throws Exception {
+    MessageInfo info = parseMessage(request);
+    String requestToken = info.message.getParameter("oauth_token");
+    TokenState state = tokenState.get(requestToken);
+    if (throttled) {
+      return makeOAuthProblemReport(OAuth.Problems.CONSUMER_KEY_REFUSED,
+          "exceeded quota", HttpResponse.SC_FORBIDDEN);
+    } else if (unauthorized) {
+      return makeOAuthProblemReport(OAuth.Problems.PERMISSION_DENIED,
+          "user refused access", HttpResponse.SC_UNAUTHORIZED);
+    } else if (state == null) {
+      return makeOAuthProblemReport(OAuth.Problems.TOKEN_REJECTED,
+          "Unknown request token", HttpResponse.SC_UNAUTHORIZED);
+    }
+    if (rejectExtraParams) {
+      String extra = hasExtraParams(info.message);
+      if (extra != null) {
+        return makeOAuthProblemReport(OAuth.Problems.PARAMETER_REJECTED,
+            extra, HttpResponse.SC_BAD_REQUEST);
+      }
+    }
+
+    OAuthAccessor accessor = new OAuthAccessor(oauthConsumer);
+    accessor.requestToken = requestToken;
+    accessor.tokenSecret = state.tokenSecret;
+    validateMessage(accessor, info, true);
+
+    if (state.getState() == State.APPROVED_UNCLAIMED) {
+      String sentVerifier = info.message.getParameter("oauth_verifier");
+      if (state.verifier != null && !state.verifier.equals(sentVerifier)) {
+        return makeOAuthProblemReport(OAuthConstants.PROBLEM_BAD_VERIFIER, "wrong oauth verifier",
+            HttpResponse.SC_UNAUTHORIZED);
+      }
+      state.claimToken();
+    } else if (state.getState() == State.APPROVED) {
+      // Verify can refresh
+      String sentHandle = info.message.getParameter("oauth_session_handle");
+      if (sentHandle == null) {
+        return makeOAuthProblemReport(OAuth.Problems.PARAMETER_ABSENT,
+            "no oauth_session_handle", HttpResponse.SC_BAD_REQUEST);
+      }
+      if (!sentHandle.equals(state.sessionHandle)) {
+        return makeOAuthProblemReport(OAuthConstants.PROBLEM_TOKEN_INVALID, "token not valid",
+            HttpResponse.SC_UNAUTHORIZED);
+      }
+      state.renewToken();
+    } else if (state.getState() == State.REVOKED){
+      return makeOAuthProblemReport(OAuth.Problems.TOKEN_REVOKED,
+          "Revoked access token can't be renewed", HttpResponse.SC_UNAUTHORIZED);
+    } else {
+      throw new Exception("Token in weird state " + state.getState());
+    }
+
+    String accessToken = Crypto.getRandomString(16);
+    String accessTokenSecret = Crypto.getRandomString(16);
+    state.tokenSecret = accessTokenSecret;
+    tokenState.put(accessToken, state);
+    tokenState.remove(requestToken);
+    List<OAuth.Parameter> params = OAuth.newList(
+        "oauth_token", accessToken,
+        "oauth_token_secret", accessTokenSecret);
+    if (sessionExtension) {
+      params.add(new OAuth.Parameter("oauth_session_handle", state.sessionHandle));
+      if (reportExpirationTimes) {
+        params.add(new OAuth.Parameter("oauth_expires_in", "" + TOKEN_EXPIRATION_SECONDS));
+      }
+    }
+    if (returnAccessTokenData) {
+      params.add(new OAuth.Parameter("userid", "userid value"));
+      params.add(new OAuth.Parameter("xoauth_stuff", "xoauth_stuff value"));
+      params.add(new OAuth.Parameter("oauth_stuff", "oauth_stuff value"));
+    }
+    return new HttpResponse(OAuth.formEncode(params));
+  }
+
+  private HttpResponse handleResourceUrl(HttpRequest request)
+      throws Exception {
+    MessageInfo info = parseMessage(request);
+    String consumerId = info.message.getParameter("oauth_consumer_key");
+    OAuthConsumer consumer;
+    if (CONSUMER_KEY.equals(consumerId)) {
+      consumer = oauthConsumer;
+    } else if ("signedfetch".equals(consumerId)) {
+      consumer = signedFetchConsumer;
+    } else if ("container.com".equals(consumerId)) {
+      consumer = signedFetchConsumer;
+    } else {
+      return makeOAuthProblemReport(OAuth.Problems.PARAMETER_ABSENT,
+          "oauth_consumer_key not found", HttpResponse.SC_BAD_REQUEST);
+    }
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    String responseBody = null;
+    if (throttled) {
+      return makeOAuthProblemReport(
+          OAuth.Problems.CONSUMER_KEY_REFUSED, "exceeded quota", HttpResponse.SC_FORBIDDEN);
+    }
+    if (unauthorized) {
+      return makeOAuthProblemReport(
+          OAuth.Problems.PERMISSION_DENIED, "user refused access",
+          HttpResponse.SC_UNAUTHORIZED);
+    }
+    if (consumer == oauthConsumer) {
+      // for OAuth, check the access token.  We skip this for signed fetch
+      String accessToken = info.message.getParameter("oauth_token");
+      TokenState state = tokenState.get(accessToken);
+      if (state == null) {
+        return makeOAuthProblemReport(
+            OAuth.Problems.TOKEN_REJECTED, "Access token unknown",
+            HttpResponse.SC_UNAUTHORIZED);
+      }
+      // Check the signature
+      accessor.accessToken = accessToken;
+      accessor.tokenSecret = state.getSecret();
+      validateMessage(accessor, info, false);
+
+      if (state.getState() != State.APPROVED) {
+        return makeOAuthProblemReport(
+            OAuth.Problems.TOKEN_REVOKED, "User revoked permissions",
+            HttpResponse.SC_UNAUTHORIZED);
+      }
+      if (sessionExtension) {
+        long expiration = state.issued + TOKEN_EXPIRATION_SECONDS * 1000;
+        if (expiration < clock.currentTimeMillis()) {
+          return makeOAuthProblemReport(OAuthConstants.PROBLEM_ACCESS_TOKEN_EXPIRED,
+              "token needs to be refreshed", HttpResponse.SC_UNAUTHORIZED);
+        }
+      }
+      responseBody = "User data is " + state.getUserData();
+    } else {
+      // Check the signature
+      validateMessage(accessor, info, false);
+
+      // For signed fetch, just echo back the query parameters in the body
+      responseBody = request.getUri().getQuery();
+    }
+
+
+    // Send back a response
+    HttpResponseBuilder resp = new HttpResponseBuilder()
+        .setHttpStatusCode(HttpResponse.SC_OK)
+        .setResponseString(responseBody);
+    if (info.aznHeader != null) {
+      resp.setHeader(AUTHZ_ECHO_HEADER, info.aznHeader);
+    }
+    if (info.body != null) {
+      resp.setHeader(BODY_ECHO_HEADER, info.body);
+    }
+    if (info.rawBody != null) {
+      resp.setHeader(RAW_BODY_ECHO_HEADER, new String(Base64.encodeBase64(info.rawBody)));
+    }
+    return resp.create();
+  }
+
+  private void validateMessage(OAuthAccessor accessor, MessageInfo info, boolean tokenEndpoint)
+      throws OAuthException, IOException, URISyntaxException {
+    OAuthValidator validator = new FakeTimeOAuthValidator();
+    validator.validateMessage(info.message,accessor);
+
+    String bodyHash = info.message.getParameter("oauth_body_hash");
+    if (tokenEndpoint && bodyHash != null) {
+      throw new RuntimeException("Can't have body hash on token endpoints");
+    }
+    SignatureType sigType = OAuthUtil.getSignatureType(tokenEndpoint,
+        info.request.getHeader("Content-Type"));
+    switch (sigType) {
+      case URL_ONLY:
+        break;
+      case URL_AND_FORM_PARAMS:
+        if (bodyHash != null) {
+          throw new RuntimeException("Can't have body hash in form-encoded request");
+        }
+        break;
+      case URL_AND_BODY_HASH:
+        if (bodyHash == null) {
+          throw new RuntimeException("Requiring oauth_body_hash parameter");
+        }
+        byte[] received = Base64.decodeBase64(CharsetUtil.getUtf8Bytes(bodyHash));
+        byte[] expected = DigestUtils.sha(info.rawBody);
+        if (!Arrays.equals(received, expected)) {
+          throw new RuntimeException("oauth_body_hash mismatch");
+        }
+    }
+
+    // Most OAuth service providers are much laxer than this about checking nonces (rapidly
+    // changing server-side state scales badly), but we are very strict in test cases.
+    String nonceKey = info.message.getConsumerKey() + ','
+        + info.message.getParameter("oauth_nonce");
+
+    CachedObject<OAuthMessage> previousMessage = nonceCache.getElement(nonceKey);
+    if (previousMessage != null) {
+      throw new RuntimeException("Reused nonce, old message = " + previousMessage.obj
+          + ", new message " + info.message);
+    }
+    nonceCache.addElement(nonceKey, info.message, TimeUnit.SECONDS.toMillis(10 * 60));
+  }
+
+  private HttpResponse handleNotFoundUrl(HttpRequest request) throws Exception {
+    return new HttpResponseBuilder()
+        .setHttpStatusCode(HttpResponse.SC_NOT_FOUND)
+        .setResponseString("not found")
+        .create();
+  }
+
+  private HttpResponse handleError400Url(HttpRequest request) throws Exception {
+    return new HttpResponseBuilder()
+        .setHttpStatusCode(HttpResponse.SC_BAD_REQUEST)
+        .setResponseString("bad request")
+        .create();
+  }
+
+  private HttpResponse handleEchoUrl(HttpRequest request) throws Exception {
+    String query = request.getUri().getQuery();
+    if (query.contains("add_oauth_token")) {
+      query = query + "&oauth_token=abc";
+    }
+    return new HttpResponseBuilder()
+        .setHttpStatusCode(HttpResponse.SC_OK)
+        .setResponseString(query)
+        .create();
+  }
+
+  public void setConsumersThrottled(boolean throttled) {
+    this.throttled = throttled;
+  }
+
+  public void setConsumerUnauthorized(boolean unauthorized) {
+    this.unauthorized = unauthorized;
+  }
+
+  public void setReturnNull(boolean returnNull) {
+    this.returnNull = returnNull;
+  }
+
+  /**
+   * @return number of hits to request token URL.
+   */
+  public int getRequestTokenCount() {
+    return requestTokenCount;
+  }
+
+  /**
+   * @return number of hits to access token URL.
+   */
+  public int getAccessTokenCount() {
+    return accessTokenCount;
+  }
+
+  /**
+   * @return number of hits to resource access URL.
+   */
+  public int getResourceAccessCount() {
+    return resourceAccessCount;
+  }
+
+  /**
+   * Validate oauth messages using a fake time source.
+   */
+  private class FakeTimeOAuthValidator extends SimpleOAuthValidator {
+    @Override
+    protected long currentTimeMsec() {
+      return clock.currentTimeMillis();
+    }
+  }
+
+  public void setThrow(GadgetException gadgetException) {
+    this.gadgetException = gadgetException;
+  }
+
+  public void setThrow(RuntimeException runtimeException) {
+    this.runtimeException = runtimeException;
+  }
+
+  public void setCheckTrustedParams(boolean checkTrustedParams) {
+    this.checkTrustedParams = checkTrustedParams;
+  }
+
+  public int getTrustedParamCount() {
+    return trustedParamCount;
+  }
+
+  public void setExpectedRequestSecurityToken( SecurityToken requestSecurityToken ) {
+    this.expectedRequestSecurityToken = requestSecurityToken;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java
new file mode 100644
index 0000000..ba21857
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth/testing/MakeRequestClient.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth.testing;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth.OAuthFetcherConfig;
+import org.apache.shindig.gadgets.oauth.OAuthRequest;
+import org.apache.shindig.gadgets.oauth.OAuthArguments.UseToken;
+
+import net.oauth.OAuth.Parameter;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Test utility to emulate the requests sent via gadgets.io.makeRequest.  The simulation starts
+ * at what arrives at OAuthRequest.  Code above OAuthRequest (MakeRequestHandler, preloads) are not
+ * exercised here.
+ */
+public class MakeRequestClient {
+
+  private final SecurityToken securityToken;
+  private final OAuthFetcherConfig fetcherConfig;
+  private final FakeOAuthServiceProvider serviceProvider;
+  private final String serviceName;
+  private OAuthArguments baseArgs;
+  private String oauthState;
+  private String approvalUrl;
+  private String receivedCallbackUrl;
+  private boolean ignoreCache;
+  private Map<String, String> trustedParams = Maps.newHashMap();
+  private HttpFetcher nextFetcher;
+
+  /**
+   * Create a make request client with the given security token, sending requests through an
+   * OAuth fetcher to an OAuth service provider.
+   *
+   * @param securityToken identity of the user.
+   * @param fetcherConfig configuration for the OAuthRequest
+   * @param serviceProvider service provider being targeted.
+   * @param serviceName nickname for the service being accessed.
+   */
+  public MakeRequestClient(SecurityToken securityToken, OAuthFetcherConfig fetcherConfig,
+      FakeOAuthServiceProvider serviceProvider, String serviceName) {
+    this.securityToken = securityToken;
+    this.fetcherConfig = fetcherConfig;
+    this.serviceProvider = serviceProvider;
+    this.serviceName = serviceName;
+    this.baseArgs = makeNonSocialOAuthArguments();
+    this.ignoreCache = false;
+  }
+
+  /**
+   * Set the arguments to the OAuth fetch.
+   */
+  public void setBaseArgs(OAuthArguments baseArgs) {
+    this.baseArgs = baseArgs;
+  }
+
+  public OAuthArguments getBaseArgs() {
+    return baseArgs;
+  }
+
+  public void setIgnoreCache(boolean ignoreCache) {
+    this.ignoreCache = ignoreCache;
+  }
+
+  public void setNextFetcher(HttpFetcher nextFetcher) {
+    this.nextFetcher = nextFetcher;
+  }
+
+  public void setTrustedParam(String name, String value) {
+    trustedParams.put(name, value);
+  }
+
+  private OAuthRequest createRequest() {
+    HttpFetcher dest = serviceProvider;
+    if (nextFetcher != null) {
+      dest = nextFetcher;
+    }
+    if (trustedParams != null) {
+      List<Parameter> trusted = Lists.newArrayList();
+      for (Entry<String, String> e : trustedParams.entrySet()) {
+        trusted.add(new Parameter(e.getKey(), e.getValue()));
+      }
+      return new OAuthRequest(fetcherConfig, dest, trusted);
+    }
+    return new OAuthRequest(fetcherConfig, dest);
+  }
+
+  /**
+   * Send an OAuth GET request to the given URL.
+   */
+  public HttpResponse sendGet(String target) throws Exception {
+    HttpRequest request = new HttpRequest(Uri.parse(target));
+    request.setOAuthArguments(recallState());
+    OAuthRequest dest = createRequest();
+    request.setIgnoreCache(ignoreCache);
+    request.setSecurityToken(securityToken);
+    HttpResponse response = dest.fetch(request);
+    saveState(response);
+    return response;
+  }
+
+  // Yes, this is really allowed by the HTTP spec and supported by real servers.
+  public HttpResponse sendGetWithBody(String target, String type, byte[] body) {
+    HttpRequest request = new HttpRequest(Uri.parse(target));
+    request.setOAuthArguments(recallState());
+    OAuthRequest dest = createRequest();
+    if (type != null) {
+      request.setHeader("Content-Type", type);
+    }
+    request.setPostBody(body);
+    request.setSecurityToken(securityToken);
+    HttpResponse response = dest.fetch(request);
+    saveState(response);
+    return response;
+  }
+
+  /**
+   * Send an OAuth POST request to the given URL.
+   */
+  public HttpResponse sendFormPost(String target, String body) throws Exception {
+    HttpRequest request = new HttpRequest(Uri.parse(target));
+    request.setOAuthArguments(recallState());
+    OAuthRequest dest = createRequest();
+    request.setMethod("POST");
+    request.setPostBody(CharsetUtil.getUtf8Bytes(body));
+    request.setHeader("content-type", "application/x-www-form-urlencoded");
+    request.setSecurityToken(securityToken);
+    HttpResponse response = dest.fetch(request);
+    saveState(response);
+    return response;
+  }
+
+  /**
+   * Send an OAuth POST with binary data in the binary.
+   */
+  public HttpResponse sendRawPost(String target, String type, byte[] body) throws Exception {
+    HttpRequest request = new HttpRequest(Uri.parse(target));
+    request.setOAuthArguments(recallState());
+    OAuthRequest dest = createRequest();
+    request.setMethod("POST");
+    if (type != null) {
+      request.setHeader("Content-Type", type);
+    }
+    request.setPostBody(body);
+    request.setSecurityToken(securityToken);
+    HttpResponse response = dest.fetch(request);
+    saveState(response);
+    return response;
+  }
+
+  /**
+   * Create arguments simulating authz=OAUTH.
+   */
+  public OAuthArguments makeNonSocialOAuthArguments() {
+    OAuthArguments params = new OAuthArguments();
+    params.setUseToken(UseToken.ALWAYS);
+    params.setServiceName(serviceName);
+    params.setSignOwner(false);
+    params.setSignViewer(false);
+    return params;
+  }
+
+  /**
+   * Create arguments simulating authz=SIGNED.
+   */
+  public OAuthArguments makeSignedFetchArguments() {
+    OAuthArguments params = new OAuthArguments();
+    params.setUseToken(UseToken.NEVER);
+    params.setSignOwner(true);
+    params.setSignViewer(true);
+    return params;
+  }
+
+  /**
+   * Track state (see gadgets.io.makeRequest handling of the oauthState and received callback
+   * parameters.
+   */
+  private OAuthArguments recallState() {
+    OAuthArguments params = new OAuthArguments(baseArgs);
+    params.setOrigClientState(oauthState);
+    params.setReceivedCallbackUrl(receivedCallbackUrl);
+    receivedCallbackUrl = null;
+    return params;
+  }
+
+  /**
+   * Track state (see gadgets.io.makeRequest handling of the oauthState parameter).
+   */
+  private void saveState(HttpResponse response) {
+    approvalUrl = null;
+    if (response.getMetadata() != null) {
+      if (response.getMetadata().containsKey("oauthState")) {
+        oauthState = response.getMetadata().get("oauthState");
+      }
+      approvalUrl = response.getMetadata().get("oauthApprovalUrl");
+    }
+  }
+
+  /**
+   * Simulate the user visiting the service provider and approved access to their data.
+   */
+  public void approveToken(String params) throws Exception {
+    // This will throw if approvalUrl looks wrong.
+    receivedCallbackUrl = serviceProvider.browserVisit(approvalUrl + '&' + params);
+  }
+
+  public void setReceivedCallbackUrl(String receivedCallbackUrl) {
+    this.receivedCallbackUrl = receivedCallbackUrl;
+  }
+
+  public void clearState() {
+    this.oauthState = null;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2StoreTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2StoreTest.java
new file mode 100644
index 0000000..783a1ee
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/BasicOAuth2StoreTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import static org.easymock.EasyMock.createMockBuilder;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Cache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Persister;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.InMemoryCache;
+
+import org.junit.Test;
+
+/**
+ * @author <a href="mailto:dev@shindig.apache.org">Shindig Dev</a>
+ * @version $Id: $
+ */
+public class BasicOAuth2StoreTest {
+
+  @Test
+  public void testSetTokenForSharedClient() throws Exception {
+    final OAuth2Cache cache = new InMemoryCache();
+    final OAuth2Persister persister = MockUtils.getDummyPersister();
+    final OAuth2Encrypter encrypter = MockUtils.getDummyEncrypter();
+    final BlobCrypter stateCrypter = MockUtils.getDummyStateCrypter();
+
+    OAuth2Token token = MockUtils.getAccessToken();
+    OAuth2Client client = MockUtils.getClient_Code_Confidential();
+    client.setSharedToken( true );
+
+    BasicOAuth2Store mockStore = createMockBuilder( BasicOAuth2Store.class )
+            .withConstructor( OAuth2Cache.class, OAuth2Persister.class, OAuth2Encrypter.class, String.class, Authority.class, String.class, BlobCrypter.class )
+            .withArgs( cache, persister, encrypter, MockUtils.REDIRECT_URI, (Authority)null, (String)null, stateCrypter )
+            .addMockedMethod( "getClient" )
+            .addMockedMethod( "getToken" )
+            .createMock();
+
+    expect( mockStore.getClient( eq(MockUtils.GADGET_URI1), eq(MockUtils.SERVICE_NAME) ) ).andReturn( client );
+    expect( mockStore.getToken( eq(token.getGadgetUri()), eq(token.getServiceName()), eq(token.getUser()), eq(token.getScope()), eq(token.getType() ) )).andReturn( token );
+
+    replay( mockStore );
+
+    mockStore.setToken( token );
+
+    verify( mockStore );
+  }
+}
+
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/GadgetOAuth2TokenStoreTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/GadgetOAuth2TokenStoreTest.java
new file mode 100644
index 0000000..75f587c
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/GadgetOAuth2TokenStoreTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GadgetOAuth2TokenStoreTest extends MockUtils {
+  private static GadgetOAuth2TokenStore gts;
+  private static SecurityToken securityToken;
+  private static Uri gadgetUri = Uri.parse(MockUtils.GADGET_URI1);
+  private static OAuth2Arguments arguments;
+
+  @Before
+  public void setUp() throws Exception {
+    GadgetOAuth2TokenStoreTest.securityToken = MockUtils.getDummySecurityToken(MockUtils.USER,
+        MockUtils.USER, MockUtils.GADGET_URI1);
+    GadgetOAuth2TokenStoreTest.arguments = MockUtils.getDummyArguments();
+
+    final OAuth2Store store = MockUtils.getDummyStore();
+    final GadgetSpecFactory specFactory = MockUtils.getDummySpecFactory();
+
+    GadgetOAuth2TokenStoreTest.gts = new GadgetOAuth2TokenStore(store, specFactory);
+  }
+
+  @Test
+  public void testGadgetOAuth2TokenStore_1() throws Exception {
+    Assert.assertNotNull(GadgetOAuth2TokenStoreTest.gts);
+  }
+
+  @Test
+  public void testGetOAuth2Accessor_1() throws Exception {
+    final OAuth2Accessor result = GadgetOAuth2TokenStoreTest.gts.getOAuth2Accessor(null,
+        GadgetOAuth2TokenStoreTest.arguments, GadgetOAuth2TokenStoreTest.gadgetUri);
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result.isErrorResponse());
+    Assert.assertEquals(OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM, result.getError());
+    Assert.assertTrue(result.getErrorContextMessage().startsWith(
+        "OAuth2Accessor missing a param"));
+  }
+
+  @Test
+  public void testGetOAuth2Accessor_2() throws Exception {
+    final OAuth2Accessor result = GadgetOAuth2TokenStoreTest.gts.getOAuth2Accessor(
+        GadgetOAuth2TokenStoreTest.securityToken, GadgetOAuth2TokenStoreTest.arguments, null);
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result.isErrorResponse());
+    Assert.assertEquals(OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM, result.getError());
+    Assert.assertTrue(result.getErrorContextMessage().startsWith(
+        "OAuth2Accessor missing a param"));
+  }
+
+  @Test
+  public void testGetOAuth2Accessor_3() throws Exception {
+    final OAuth2Accessor result = GadgetOAuth2TokenStoreTest.gts.getOAuth2Accessor(
+        GadgetOAuth2TokenStoreTest.securityToken, GadgetOAuth2TokenStoreTest.arguments,
+        Uri.parse("bad"));
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result.isErrorResponse());
+    Assert.assertEquals(OAuth2Error.NO_GADGET_SPEC, result.getError());
+    Assert.assertTrue(result.getErrorContextMessage().startsWith("gadgetUri ="));
+  }
+
+  @Test
+  public void testGetOAuth2Accessor_4() throws Exception {
+    final OAuth2Accessor result = GadgetOAuth2TokenStoreTest.gts.getOAuth2Accessor(
+        GadgetOAuth2TokenStoreTest.securityToken, GadgetOAuth2TokenStoreTest.arguments,
+        Uri.parse(MockUtils.GADGET_URI1));
+
+    Assert.assertNotNull(result);
+    Assert.assertFalse(result.isErrorResponse());
+    Assert.assertEquals(null, result.getAccessToken());
+    Assert.assertEquals(MockUtils.AUTHORIZE_URL, result.getAuthorizationUrl());
+    Assert.assertEquals(OAuth2Message.BASIC_AUTH_TYPE, result.getClientAuthenticationType());
+    Assert.assertEquals(MockUtils.CLIENT_ID1, result.getClientId());
+    Assert.assertEquals(MockUtils.GADGET_URI1, result.getGadgetUri());
+    Assert.assertEquals(OAuth2Message.AUTHORIZATION, result.getGrantType());
+    Assert.assertEquals(MockUtils.REDIRECT_URI, result.getRedirectUri());
+    Assert.assertEquals(null, result.getRefreshToken());
+    Assert.assertEquals(MockUtils.SCOPE, result.getScope());
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result.getServiceName());
+    Assert.assertEquals(MockUtils.TOKEN_URL, result.getTokenUrl());
+    Assert.assertEquals(OAuth2Accessor.Type.CONFIDENTIAL, result.getType());
+    Assert.assertEquals(MockUtils.USER, result.getUser());
+    Assert.assertTrue(result.isValid());
+    Assert.assertFalse(result.isAllowModuleOverrides());
+    Assert.assertFalse(result.isErrorResponse());
+    Assert.assertFalse(result.isRedirecting());
+    Assert.assertFalse(result.isUrlParameter());
+    Assert.assertTrue(result.isAuthorizationHeader());
+
+  }
+
+  @Test
+  public void testGetOAuth2Store_1() throws Exception {
+    final OAuth2Store result = GadgetOAuth2TokenStoreTest.gts.getOAuth2Store();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(MockUtils.getDummyStore(), result);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/MockUtils.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/MockUtils.java
new file mode 100644
index 0000000..f86acb0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/MockUtils.java
@@ -0,0 +1,539 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Provider;
+
+import org.apache.shindig.auth.AbstractSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.common.crypto.BlobExpiredException;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.oauth2.handler.BasicAuthenticationHandler;
+import org.apache.shindig.gadgets.oauth2.handler.ClientAuthenticationHandler;
+import org.apache.shindig.gadgets.oauth2.handler.StandardAuthenticationHandler;
+import org.apache.shindig.gadgets.oauth2.handler.TokenAuthorizationResponseHandler;
+import org.apache.shindig.gadgets.oauth2.handler.TokenEndpointResponseHandler;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Cache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2EncryptionException;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Persister;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2TokenPersistence;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.InMemoryCache;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.JSONOAuth2Persister;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+import org.apache.commons.io.IOUtils;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+public class MockUtils {
+  protected static final String ACCESS_SECRET = "accessSecret";
+  protected static final String AUTHORIZE_URL = "http://www.example.com/authorize";
+  protected static final String CLIENT_ID1 = "clientId1";
+  protected static final String CLIENT_ID2 = "clientId2";
+  protected static final String CLIENT_SECRET1 = "clientSecret1";
+  protected static final String CLIENT_SECRET2 = "clientSecret2";
+  protected static final Map<String, String> EMPTY_MAP = Collections.emptyMap();
+  protected static final OAuth2Encrypter encrypter = new DummyEncrypter();
+  protected static final BlobCrypter stateCrypter = new DummyStateCrypter();
+  protected static final String GADGET_URI1 = "http://www.example.com/1";
+  protected static final String GADGET_URI2 = "http://www.example.com/2";
+  protected static final String MAC_SECRET = "mac_secret";
+  protected static final String REDIRECT_URI = "https://www.example.com/gadgets/oauth2callback";
+  protected static final String REFRESH_SECRET = "refreshSecret";
+  protected static final Integer REFRESH_TOKEN_INDEX = new Integer(81037012);
+  protected static final String SCOPE = "testScope";
+  protected static final String SERVICE_NAME = "serviceName";
+  protected static final String STATE = "1234";
+  protected static final String TOKEN_URL = "http://www.example.com/token";
+  protected static final String USER = "testUser";
+
+  protected static OAuth2Store dummyStore = null;
+
+  static class DummyAuthority implements Authority {
+    public String getAuthority() {
+      return "authority";
+    }
+
+    public String getOrigin() {
+      return "origin";
+    }
+
+    public String getScheme() {
+      return "scheme";
+    }
+  }
+
+  static class DummyStateCrypter extends BasicBlobCrypter {
+    public DummyStateCrypter() {
+      super("xxxxxxxxxxxxxxxx");
+    }
+  }
+
+  static class DummyEncrypter implements OAuth2Encrypter {
+
+    public byte[] decrypt(final byte[] encryptedSecret) throws OAuth2EncryptionException {
+      final byte[] bytesOut = new byte[encryptedSecret.length];
+      for (int i = 0; i < encryptedSecret.length; i++) {
+        bytesOut[i] = (byte) (encryptedSecret[i] - 1);
+      }
+      return bytesOut;
+
+    }
+
+    public byte[] encrypt(final byte[] plainSecret) throws OAuth2EncryptionException {
+      final byte[] bytesOut = new byte[plainSecret.length];
+      for (int i = 0; i < plainSecret.length; i++) {
+        bytesOut[i] = (byte) (plainSecret[i] + 1);
+      }
+      return bytesOut;
+    }
+  }
+
+  static class DummyHostProvider implements Provider<Authority> {
+    private static final Authority authority = new DummyAuthority();
+
+    public Authority get() {
+      return DummyHostProvider.authority;
+    }
+  }
+
+  public static class DummyHttpFetcher implements HttpFetcher {
+    public HttpRequest request;
+
+    public HttpResponse fetch(final HttpRequest request) throws GadgetException {
+      this.request = request;
+      final HttpResponseBuilder builder = new HttpResponseBuilder();
+      builder.setStrictNoCache();
+      builder.setHttpStatusCode(HttpResponse.SC_OK);
+      builder.setHeader("Content-Type", "application/json");
+      builder.setContent("{\"access_token\":\"xxx\",\"token_type\":\"Bearer\",\"expires_in\":\"1\",\"refresh_token\":\"yyy\",\"example_parameter\":\"example_value\"}");
+      return builder.create();
+    }
+  }
+
+  static class DummyMessageProvider implements Provider<OAuth2Message> {
+    public OAuth2Message get() {
+      return new BasicOAuth2Message();
+    }
+  }
+
+  static class DummySecurityToken extends AbstractSecurityToken {
+
+    public DummySecurityToken(final String ownerId, final String viewerId, final String appUrl) {
+      this.setOwnerId(ownerId);
+      this.setViewerId(viewerId);
+      this.setAppUrl(appUrl);
+    }
+
+    @Override
+    public String getAppId() {
+      return "";
+    }
+
+    @Override
+    public String getDomain() {
+      return "";
+    }
+
+    @Override
+    public String getContainer() {
+      return "";
+    }
+
+    @Override
+    public long getModuleId() {
+      return 0;
+    }
+
+    @Override
+    public Long getExpiresAt() {
+      return 0L;
+    }
+
+    public boolean isExpired(final int maxAge) {
+      return false;
+    }
+
+    public AbstractSecurityToken enforceNotExpired(final int maxAge) throws BlobExpiredException {
+      return this;
+    }
+
+    public String getUpdatedToken() {
+      return "";
+    }
+
+    public String getAuthenticationMode() {
+      return "";
+    }
+
+    @Override
+    public String getTrustedJson() {
+      return "";
+    }
+
+    public boolean isAnonymous() {
+      return false;
+    }
+
+    @Override
+    public String getActiveUrl() {
+      return this.getAppUrl();
+    }
+
+    @Override
+    protected EnumSet<Keys> getMapKeys() {
+      return EnumSet.noneOf(Keys.class);
+    }
+  }
+
+  static class DummyGadgetSpecFactory implements GadgetSpecFactory {
+    private static final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?><Module><ModulePrefs title=\"\"><OAuth2><Service name=\"serviceName\" scope=\"testScope\"></Service></OAuth2></ModulePrefs><Content type=\"html\"></Content></Module>";
+
+    public GadgetSpec getGadgetSpec(final GadgetContext context) throws GadgetException {
+      final Uri contextUri = context.getUrl();
+      if (contextUri != null && contextUri.toString().equals(MockUtils.GADGET_URI1)) {
+        return new GadgetSpec(context.getUrl(), DummyGadgetSpecFactory.xml);
+      }
+
+      throw new GadgetException(GadgetException.Code.OAUTH_STORAGE_ERROR);
+    }
+
+    public Uri getGadgetUri(GadgetContext context) throws GadgetException {
+        return context.getUrl();
+    }
+  }
+
+  private static void setTokenCommons(final OAuth2TokenPersistence token) throws Exception {
+    token.setExpiresAt(1L);
+    token.setGadgetUri(MockUtils.GADGET_URI1);
+    token.setIssuedAt(0L);
+    token.setMacAlgorithm("");
+    token.setMacExt("");
+    token.setMacSecret(new byte[] {});
+    token.setProperties(MockUtils.EMPTY_MAP);
+    token.setScope(MockUtils.SCOPE);
+    token.setSecret(MockUtils.ACCESS_SECRET.getBytes("UTF-8"));
+    token.setServiceName(MockUtils.SERVICE_NAME);
+    token.setTokenType(OAuth2Message.BEARER_TOKEN_TYPE);
+    token.setUser(MockUtils.USER);
+  }
+
+  protected static OAuth2TokenPersistence getAccessToken() throws Exception {
+    final OAuth2TokenPersistence accessToken = new OAuth2TokenPersistence(
+            MockUtils.getDummyEncrypter());
+    MockUtils.setTokenCommons(accessToken);
+    accessToken.setType(OAuth2Token.Type.ACCESS);
+    return accessToken;
+  }
+
+  protected static OAuth2TokenPersistence getBadMacToken() throws Exception {
+    final OAuth2TokenPersistence accessToken = new OAuth2TokenPersistence(
+            MockUtils.getDummyEncrypter());
+    MockUtils.setTokenCommons(accessToken);
+    accessToken.setMacAlgorithm(OAuth2Message.HMAC_SHA_256);
+    accessToken.setMacExt("1 2 3");
+    accessToken.setMacSecret(MockUtils.MAC_SECRET.getBytes("UTF-8"));
+    accessToken.setTokenType(OAuth2Message.MAC_TOKEN_TYPE);
+    accessToken.setType(OAuth2Token.Type.ACCESS);
+    return accessToken;
+  }
+
+  private static void setClientCommons(final OAuth2Client client) throws Exception {
+    client.setAuthorizationUrl(MockUtils.AUTHORIZE_URL);
+    client.setGrantType(OAuth2Message.AUTHORIZATION_CODE);
+    client.setRedirectUri(MockUtils.REDIRECT_URI);
+    client.setServiceName(MockUtils.SERVICE_NAME);
+    client.setTokenUrl(MockUtils.TOKEN_URL);
+  }
+
+  protected static OAuth2Client getClient_Code_Confidential() throws Exception {
+    final OAuth2Client client = new OAuth2Client(MockUtils.getDummyEncrypter());
+    MockUtils.setClientCommons(client);
+    client.setClientAuthenticationType(OAuth2Message.BASIC_AUTH_TYPE);
+    client.setClientId(MockUtils.CLIENT_ID1);
+    client.setClientSecret(MockUtils.CLIENT_SECRET1.getBytes("UTF-8"));
+    client.setGadgetUri(MockUtils.GADGET_URI1);
+    client.setType(OAuth2Accessor.Type.CONFIDENTIAL);
+    client.setAllowModuleOverride(true);
+    client.setAuthorizationHeader(true);
+    client.setUrlParameter(false);
+
+    return client;
+  }
+
+  protected static OAuth2Client getClient_Code_Public() throws Exception {
+    final OAuth2Client client = new OAuth2Client(MockUtils.getDummyEncrypter());
+    MockUtils.setClientCommons(client);
+    client.setClientAuthenticationType(OAuth2Message.STANDARD_AUTH_TYPE);
+    client.setClientId(MockUtils.CLIENT_ID2);
+    client.setClientSecret(MockUtils.CLIENT_SECRET2.getBytes("UTF-8"));
+    client.setGadgetUri(MockUtils.GADGET_URI2);
+    client.setType(OAuth2Accessor.Type.PUBLIC);
+    client.setAllowModuleOverride(false);
+    client.setAuthorizationHeader(false);
+    client.setUrlParameter(true);
+
+    return client;
+  }
+
+  protected static OAuth2Arguments getDummyArguments() throws Exception {
+    final Map<String, String> map = Maps.newHashMap();
+    map.put("OAUTH_SCOPE", MockUtils.SCOPE);
+    map.put("OAUTH_SERVICE_NAME", MockUtils.SERVICE_NAME);
+    return new OAuth2Arguments(AuthType.OAUTH2, map);
+  }
+
+  protected static List<ClientAuthenticationHandler> getDummyClientAuthHandlers() throws Exception {
+    final List<ClientAuthenticationHandler> ret = new ArrayList<ClientAuthenticationHandler>(2);
+    ret.add(new BasicAuthenticationHandler());
+    ret.add(new StandardAuthenticationHandler());
+    return ret;
+  }
+
+  protected static BlobCrypter getDummyStateCrypter() {
+    return MockUtils.stateCrypter;
+  }
+
+  protected static OAuth2Encrypter getDummyEncrypter() {
+    return MockUtils.encrypter;
+  }
+
+  protected static HttpFetcher getDummyFetcher() throws Exception {
+    return new DummyHttpFetcher();
+  }
+
+  protected static Authority getDummyAuthority() {
+    return new DummyAuthority();
+  }
+
+  protected static Provider<OAuth2Message> getDummyMessageProvider() {
+    return new DummyMessageProvider();
+  }
+
+  protected static JSONOAuth2Persister getDummyPersister() throws Exception {
+    final JSONObject configFile = new JSONObject(MockUtils.getJSONString());
+    return new JSONOAuth2Persister(MockUtils.getDummyEncrypter(), MockUtils.getDummyAuthority(),
+            MockUtils.REDIRECT_URI, "xxx", configFile);
+  }
+
+  protected static GadgetSpecFactory getDummySpecFactory() {
+    return new DummyGadgetSpecFactory();
+  }
+
+  protected static SecurityToken getDummySecurityToken(final String ownerId, final String viewerId,
+          final String appUrl) {
+    return new DummySecurityToken(ownerId, viewerId, appUrl);
+  }
+
+  protected static OAuth2Store getDummyStore() throws Exception {
+    if (MockUtils.dummyStore == null) {
+      final OAuth2Cache cache = new InMemoryCache();
+      final OAuth2Persister persister = MockUtils.getDummyPersister();
+      final OAuth2Encrypter encrypter = MockUtils.getDummyEncrypter();
+      final BlobCrypter stateCrypter = MockUtils.getDummyStateCrypter();
+      MockUtils.dummyStore = MockUtils.getDummyStore(cache, persister, encrypter,
+              MockUtils.REDIRECT_URI, null, null, stateCrypter);
+    }
+
+    MockUtils.dummyStore.clearCache();
+    MockUtils.dummyStore.init();
+
+    return MockUtils.dummyStore;
+  }
+
+  protected static OAuth2Store getDummyStore(final OAuth2Cache cache,
+          final OAuth2Persister persister, final OAuth2Encrypter encrypter,
+          final String globalRedirectUri, final Authority authority, final String contextRoot,
+          final BlobCrypter stateCrypter) {
+    final OAuth2Store store = new BasicOAuth2Store(cache, persister, encrypter, globalRedirectUri,
+            authority, contextRoot, stateCrypter);
+
+    return store;
+  }
+
+  protected static List<TokenEndpointResponseHandler> getDummyTokenEndpointResponseHandlers()
+          throws Exception {
+    final List<TokenEndpointResponseHandler> ret = new ArrayList<TokenEndpointResponseHandler>(1);
+    ret.add(new TokenAuthorizationResponseHandler(MockUtils.getDummyMessageProvider(), MockUtils
+            .getDummyStore()));
+    return ret;
+  }
+
+  protected static String getJSONString() throws IOException {
+    return MockUtils.loadFile("org/apache/shindig/gadgets/oauth2/oauth2_test.json");
+  }
+
+  protected static OAuth2TokenPersistence getMacToken() throws Exception {
+    final OAuth2TokenPersistence accessToken = new OAuth2TokenPersistence(
+            MockUtils.getDummyEncrypter());
+
+    MockUtils.setTokenCommons(accessToken);
+    accessToken.setMacAlgorithm(OAuth2Message.HMAC_SHA_1);
+    accessToken.setMacExt("1 2 3");
+    accessToken.setMacSecret(MockUtils.MAC_SECRET.getBytes("UTF-8"));
+    accessToken.setTokenType(OAuth2Message.MAC_TOKEN_TYPE);
+    accessToken.setType(OAuth2Token.Type.ACCESS);
+
+    return accessToken;
+  }
+
+  private static BasicOAuth2Accessor getOAuth2AccessorCommon() throws Exception {
+    final OAuth2Cache cache = new InMemoryCache();
+    final OAuth2Persister persister = MockUtils.getDummyPersister();
+    final OAuth2Encrypter encrypter = MockUtils.getDummyEncrypter();
+    final OAuth2Store store = MockUtils.getDummyStore(cache, persister, encrypter,
+            MockUtils.REDIRECT_URI, null, null, MockUtils.stateCrypter);
+    final BasicOAuth2Accessor accessor = new BasicOAuth2Accessor(MockUtils.GADGET_URI1,
+            MockUtils.SERVICE_NAME, MockUtils.USER, MockUtils.SCOPE, true, store,
+            MockUtils.REDIRECT_URI, null, null);
+
+    accessor.setAccessToken(MockUtils.getAccessToken());
+    accessor.setAuthorizationUrl(MockUtils.AUTHORIZE_URL);
+    accessor.setClientAuthenticationType(OAuth2Message.BASIC_AUTH_TYPE);
+    accessor.setClientId(MockUtils.CLIENT_ID1);
+    accessor.setClientSecret(MockUtils.CLIENT_SECRET1.getBytes("UTF-8"));
+    accessor.setErrorUri(null);
+    accessor.setGrantType(OAuth2Message.AUTHORIZATION);
+    accessor.setRedirectUri(MockUtils.REDIRECT_URI);
+    accessor.setRefreshToken(MockUtils.getRefreshToken());
+    accessor.setTokenUrl(MockUtils.TOKEN_URL);
+    accessor.setType(OAuth2Accessor.Type.CONFIDENTIAL);
+    accessor.setAuthorizationHeader(Boolean.TRUE);
+    accessor.setRedirecting(Boolean.FALSE);
+    accessor.setUrlParameter(Boolean.FALSE);
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_Code() throws Exception {
+    final BasicOAuth2Accessor accessor = MockUtils.getOAuth2AccessorCommon();
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_Error() {
+    final OAuth2Accessor accessor = new BasicOAuth2Accessor(null,
+            OAuth2Error.GET_OAUTH2_ACCESSOR_PROBLEM, "test contextMessage", null);
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_MacToken() throws Exception {
+    final BasicOAuth2Accessor accessor = MockUtils.getOAuth2AccessorCommon();
+
+    accessor.setAccessToken(MockUtils.getMacToken());
+    accessor.setRefreshToken(null);
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_BadMacToken() throws Exception {
+    final BasicOAuth2Accessor accessor = MockUtils.getOAuth2AccessorCommon();
+
+    accessor.setAccessToken(MockUtils.getBadMacToken());
+    accessor.setRefreshToken(null);
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_StandardAuth() throws Exception {
+    final BasicOAuth2Accessor accessor = MockUtils.getOAuth2AccessorCommon();
+
+    accessor.setClientAuthenticationType(OAuth2Message.STANDARD_AUTH_TYPE);
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_ClientCredentials() throws Exception {
+    final BasicOAuth2Accessor accessor = MockUtils.getOAuth2AccessorCommon();
+
+    accessor.setGrantType(OAuth2Message.CLIENT_CREDENTIALS);
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_Redirecting() throws Exception {
+    final BasicOAuth2Accessor accessor = MockUtils.getOAuth2AccessorCommon();
+
+    accessor.setRedirecting(Boolean.TRUE);
+
+    return accessor;
+  }
+
+  protected static OAuth2Accessor getOAuth2Accessor_ClientCredentialsRedirecting() throws Exception {
+    final OAuth2Cache cache = new InMemoryCache();
+    final OAuth2Persister persister = MockUtils.getDummyPersister();
+    final OAuth2Encrypter encrypter = MockUtils.getDummyEncrypter();
+    final OAuth2Store store = MockUtils.getDummyStore(cache, persister, encrypter,
+            MockUtils.REDIRECT_URI, null, null, MockUtils.stateCrypter);
+    final BasicOAuth2Accessor accessor = new BasicOAuth2Accessor(MockUtils.GADGET_URI1,
+            MockUtils.SERVICE_NAME, MockUtils.USER, MockUtils.SCOPE, true, store,
+            MockUtils.REDIRECT_URI, null, null);
+
+    accessor.setGrantType(OAuth2Message.CLIENT_CREDENTIALS);
+    accessor.setRedirecting(Boolean.TRUE);
+
+    return accessor;
+  }
+
+  protected static OAuth2TokenPersistence getRefreshToken() throws Exception {
+    final OAuth2TokenPersistence refreshToken = new OAuth2TokenPersistence(
+            MockUtils.getDummyEncrypter());
+    refreshToken.setExpiresAt(1L);
+    refreshToken.setGadgetUri(MockUtils.GADGET_URI1);
+    refreshToken.setIssuedAt(0L);
+    refreshToken.setMacAlgorithm("");
+    refreshToken.setMacExt("");
+    refreshToken.setMacSecret(new byte[] {});
+    refreshToken.setProperties(MockUtils.EMPTY_MAP);
+    refreshToken.setScope(MockUtils.SCOPE);
+    refreshToken.setSecret(MockUtils.ACCESS_SECRET.getBytes("UTF-8"));
+    refreshToken.setServiceName(MockUtils.SERVICE_NAME);
+    refreshToken.setTokenType(OAuth2Message.MAC_TOKEN_TYPE);
+    refreshToken.setType(OAuth2Token.Type.REFRESH);
+    refreshToken.setUser(MockUtils.USER);
+    return refreshToken;
+  }
+
+  protected static String loadFile(final String path) throws IOException {
+    final InputStream is = MockUtils.class.getClassLoader().getResourceAsStream(path);
+    return IOUtils.toString(is, "UTF-8");
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ArgumentsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ArgumentsTest.java
new file mode 100644
index 0000000..68d6fee
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ArgumentsTest.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Maps;
+
+public class OAuth2ArgumentsTest extends MockUtils {
+  private static HttpServletRequest requestMock;
+  private static Map<String, String> attrs;
+
+  @Before
+  public void setUp() throws Exception {
+    OAuth2ArgumentsTest.attrs = Maps.newHashMap();
+    OAuth2ArgumentsTest.attrs.put("OAUTH_SCOPE", MockUtils.SCOPE);
+    OAuth2ArgumentsTest.attrs.put("OAUTH_SERVICE_NAME", MockUtils.SERVICE_NAME);
+    OAuth2ArgumentsTest.attrs.put("bypassSpecCache", "1");
+    OAuth2ArgumentsTest.attrs.put("extraParam", "extraValue");
+    OAuth2ArgumentsTest.requestMock = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(OAuth2ArgumentsTest.requestMock.getParameterNames()).andReturn(
+        Collections.enumeration(OAuth2ArgumentsTest.attrs.keySet()));
+    EasyMock.expect(OAuth2ArgumentsTest.requestMock.getParameterMap()).andReturn(
+        OAuth2ArgumentsTest.attrs);
+    for (final Entry<String, String> entry : OAuth2ArgumentsTest.attrs.entrySet()) {
+      EasyMock.expect(OAuth2ArgumentsTest.requestMock.getParameter(entry.getKey())).andReturn(
+          entry.getValue());
+    }
+    EasyMock.replay(OAuth2ArgumentsTest.requestMock);
+  }
+
+  @Test
+  public void testOAuth2Arguments_1() throws Exception {
+    final OAuth2Arguments result = new OAuth2Arguments(OAuth2ArgumentsTest.requestMock);
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result.getBypassSpecCache());
+    Assert.assertEquals(MockUtils.SCOPE, result.getScope());
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result.getServiceName());
+  }
+
+  @Test
+  public void testOAuth2Arguments_2() throws Exception {
+    final Map<String, String> attrs1 = Maps.newHashMap();
+    attrs1.put("OAUTH_SCOPE", "xxx");
+    attrs1.put("OAUTH_SERVICE_NAME", "yyy");
+    attrs1.put("bypassSpecCache", "0");
+    final HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getParameterNames())
+        .andReturn(Collections.enumeration(attrs1.keySet()));
+    EasyMock.expect(request.getParameterMap()).andReturn(attrs1);
+    for (final Entry<String, String> entry : attrs1.entrySet()) {
+      EasyMock.expect(request.getParameter(entry.getKey())).andReturn(entry.getValue());
+    }
+    EasyMock.replay(request);
+
+    final OAuth2Arguments orig = new OAuth2Arguments(request);
+
+    final OAuth2Arguments result = new OAuth2Arguments(orig);
+
+    Assert.assertNotNull(result);
+    Assert.assertFalse(result.getBypassSpecCache());
+    Assert.assertEquals("xxx", result.getScope());
+    Assert.assertEquals("yyy", result.getServiceName());
+  }
+
+  @Test
+  public void testOAuth2Arguments_3() throws Exception {
+    final RequestAuthenticationInfo info = EasyMock.createNiceMock(RequestAuthenticationInfo.class);
+    EasyMock.expect(info.getAuthType()).andReturn(AuthType.OAUTH2);
+    EasyMock.expect(info.getAttributes()).andReturn(OAuth2ArgumentsTest.attrs);
+    EasyMock.replay(info);
+
+    final OAuth2Arguments result = new OAuth2Arguments(info);
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result.getBypassSpecCache());
+    Assert.assertEquals(MockUtils.SCOPE, result.getScope());
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result.getServiceName());
+  }
+
+  @Test
+  public void testOAuth2Arguments_4() throws Exception {
+    final OAuth2Arguments result = new OAuth2Arguments(AuthType.OAUTH2, OAuth2ArgumentsTest.attrs);
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result.getBypassSpecCache());
+    Assert.assertEquals(MockUtils.SCOPE, result.getScope());
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result.getServiceName());
+  }
+
+  @Test
+  public void testEquals_1() throws Exception {
+    final OAuth2Arguments fixture = new OAuth2Arguments(AuthType.OAUTH2, OAuth2ArgumentsTest.attrs);
+
+    final Object obj = new OAuth2Arguments(OAuth2ArgumentsTest.requestMock);
+
+    final boolean result = fixture.equals(obj);
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testEquals_2() throws Exception {
+    final Object obj = new Object();
+
+    final boolean result = OAuth2ArgumentsTest.requestMock.equals(obj);
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testEquals_3() throws Exception {
+    final boolean result = OAuth2ArgumentsTest.requestMock.equals(null);
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testEquals_4() throws Exception {
+    final Map<String, String> attrs1 = Maps.newHashMap();
+    attrs1.put("OAUTH_SCOPE", "xxx");
+    attrs1.put("OAUTH_SERVICE_NAME", "yyy");
+    attrs1.put("bypassSpecCache", "0");
+    final HttpServletRequest request = EasyMock.createNiceMock(HttpServletRequest.class);
+    EasyMock.expect(request.getParameterNames())
+        .andReturn(Collections.enumeration(attrs1.keySet()));
+    EasyMock.expect(request.getParameterMap()).andReturn(attrs1);
+    for (final Entry<String, String> entry : attrs1.entrySet()) {
+      EasyMock.expect(request.getParameter(entry.getKey())).andReturn(entry.getValue());
+    }
+    EasyMock.replay(request);
+
+    final OAuth2Arguments obj = new OAuth2Arguments(request);
+
+    final boolean result = OAuth2ArgumentsTest.requestMock.equals(obj);
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testGetBypassSpecCache_1() throws Exception {
+    final OAuth2Arguments fixture = new OAuth2Arguments(OAuth2ArgumentsTest.requestMock);
+
+    final boolean result = fixture.getBypassSpecCache();
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testGetScope_1() throws Exception {
+    final OAuth2Arguments fixture = new OAuth2Arguments(OAuth2ArgumentsTest.requestMock);
+
+    final String result = fixture.getScope();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(MockUtils.SCOPE, result);
+  }
+
+  @Test
+  public void testGetServiceName_1() throws Exception {
+    final OAuth2Arguments fixture = new OAuth2Arguments(OAuth2ArgumentsTest.requestMock);
+
+    final String result = fixture.getServiceName();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result);
+  }
+
+  @Test
+  public void testHashCode_1() throws Exception {
+    final OAuth2Arguments fixture = new OAuth2Arguments(OAuth2ArgumentsTest.requestMock);
+
+    final int result = fixture.hashCode();
+
+    Assert.assertEquals(-1928533070, result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ErrorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ErrorTest.java
new file mode 100644
index 0000000..ff3899a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ErrorTest.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2ErrorTest {
+  @Test
+  public void testGetErrorCode_1() throws Exception {
+    final OAuth2Error fixture = OAuth2Error.AUTHENTICATION_PROBLEM;
+
+    final String result = fixture.getErrorCode();
+
+    Assert.assertEquals("authentication_problem", result);
+  }
+
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2FetcherConfigTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2FetcherConfigTest.java
new file mode 100644
index 0000000..c453df0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2FetcherConfigTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2FetcherConfigTest {
+  @Test
+  public void testOAuth2FetcherConfig_1() throws Exception {
+    final GadgetOAuth2TokenStore tokenStore = new GadgetOAuth2TokenStore(
+        EasyMock.createNiceMock(OAuth2Store.class),
+        EasyMock.createNiceMock(GadgetSpecFactory.class));
+    final boolean viewerAccessTokensEnabled = true;
+
+    final OAuth2FetcherConfig result = new OAuth2FetcherConfig(tokenStore,
+        viewerAccessTokensEnabled);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(true, result.isViewerAccessTokensEnabled());
+  }
+
+  @Test
+  public void testGetOAuth2Store_1() throws Exception {
+    final OAuth2FetcherConfig fixture = new OAuth2FetcherConfig(new GadgetOAuth2TokenStore(
+        EasyMock.createNiceMock(OAuth2Store.class),
+        EasyMock.createNiceMock(GadgetSpecFactory.class)), true);
+
+    final OAuth2Store result = fixture.getOAuth2Store();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(false, result.clearCache());
+  }
+
+  @Test
+  public void testGetTokenStore_1() throws Exception {
+    final OAuth2FetcherConfig fixture = new OAuth2FetcherConfig(new GadgetOAuth2TokenStore(
+        EasyMock.createNiceMock(OAuth2Store.class),
+        EasyMock.createNiceMock(GadgetSpecFactory.class)), true);
+
+    final GadgetOAuth2TokenStore result = fixture.getTokenStore();
+
+    Assert.assertNotNull(result);
+  }
+
+  @Test
+  public void testIsViewerAccessTokensEnabled_1() throws Exception {
+    final OAuth2FetcherConfig fixture = new OAuth2FetcherConfig(new GadgetOAuth2TokenStore(
+        EasyMock.createNiceMock(OAuth2Store.class),
+        EasyMock.createNiceMock(GadgetSpecFactory.class)), true);
+
+    final boolean result = fixture.isViewerAccessTokensEnabled();
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testIsViewerAccessTokensEnabled_2() throws Exception {
+    final OAuth2FetcherConfig fixture = new OAuth2FetcherConfig(new GadgetOAuth2TokenStore(
+        EasyMock.createNiceMock(OAuth2Store.class),
+        EasyMock.createNiceMock(GadgetSpecFactory.class)), false);
+
+    final boolean result = fixture.isViewerAccessTokensEnabled();
+
+    Assert.assertEquals(false, result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2GadgetContextTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2GadgetContextTest.java
new file mode 100644
index 0000000..fe1953b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2GadgetContextTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OAuth2GadgetContextTest extends MockUtils {
+  private static SecurityToken securityToken;
+  private static OAuth2Arguments arguments;
+  private static Uri gadgetUri;
+
+  @Before
+  public void setUp() throws Exception {
+    OAuth2GadgetContextTest.securityToken = MockUtils.getDummySecurityToken(MockUtils.USER,
+        MockUtils.USER, MockUtils.GADGET_URI1);
+    OAuth2GadgetContextTest.arguments = MockUtils.getDummyArguments();
+    OAuth2GadgetContextTest.gadgetUri = Uri.parse(MockUtils.GADGET_URI1);
+  }
+
+  @Test
+  public void testOAuth2GadgetContext_1() throws Exception {
+    final OAuth2GadgetContext result = new OAuth2GadgetContext(
+        OAuth2GadgetContextTest.securityToken, OAuth2GadgetContextTest.arguments,
+        OAuth2GadgetContextTest.gadgetUri);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(false, result.getCajoled());
+    Assert.assertEquals("", result.getContainer());
+    Assert.assertEquals(false, result.getDebug());
+    Assert.assertEquals(null, result.getHost());
+    Assert.assertEquals(false, result.getIgnoreCache());
+    Assert.assertNotNull(result.getLocale());
+    Assert.assertEquals(0, result.getModuleId());
+    Assert.assertNotNull(result.getRenderingContext());
+    Assert.assertEquals(null, result.getRepository());
+    Assert.assertEquals(false, result.getSanitize());
+    Assert.assertEquals(MockUtils.SCOPE, result.getScope());
+    Assert.assertEquals(OAuth2GadgetContextTest.securityToken, result.getToken());
+    Assert.assertEquals(MockUtils.GADGET_URI1, result.getUrl().toString());
+    Assert.assertEquals(null, result.getUserAgent());
+    Assert.assertEquals(null, result.getUserIp());
+    Assert.assertNotNull(result.getUserPrefs());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2MessageModuleTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2MessageModuleTest.java
new file mode 100644
index 0000000..a87eb99
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2MessageModuleTest.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+
+public class OAuth2MessageModuleTest {
+  @Test
+  public void testConfigure_1() throws Exception {
+    final OAuth2MessageModule fixture = new OAuth2MessageModule();
+
+    Assert.assertNotNull(fixture);
+    Assert.assertTrue(AbstractModule.class.isInstance(fixture));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ModuleTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ModuleTest.java
new file mode 100644
index 0000000..4aea10c
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ModuleTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+
+public class OAuth2ModuleTest {
+  @Test
+  public void testOAuth2Module_1() throws Exception {
+    final OAuth2Module result = new OAuth2Module();
+    Assert.assertNotNull(result);
+    Assert.assertTrue(AbstractModule.class.isInstance(result));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestExceptionTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestExceptionTest.java
new file mode 100644
index 0000000..a5bda62
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2RequestExceptionTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2RequestExceptionTest {
+  @Test
+  public void testOAuth2RequestException_1() throws Exception {
+    final OAuth2Error error = OAuth2Error.AUTHENTICATION_PROBLEM;
+    final String errorText = "";
+    final Throwable cause = new Throwable();
+
+    final OAuth2RequestException result = new OAuth2RequestException(error, errorText, cause);
+
+    Assert.assertNotNull(result);
+  }
+
+  @Test
+  public void testGetError_1() throws Exception {
+    final OAuth2RequestException fixture = new OAuth2RequestException(
+        OAuth2Error.AUTHENTICATION_PROBLEM, "", new Throwable());
+
+    final OAuth2Error result = fixture.getError();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("authentication_problem", result.getErrorCode());
+    Assert.assertEquals("AUTHENTICATION_PROBLEM", result.name());
+    Assert.assertEquals(2, result.ordinal());
+    Assert.assertEquals("AUTHENTICATION_PROBLEM", result.toString());
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ResponseParamsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ResponseParamsTest.java
new file mode 100644
index 0000000..5c94eb1
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/OAuth2ResponseParamsTest.java
@@ -0,0 +1,305 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2;
+
+import java.net.URI;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2ResponseParamsTest {
+  @Test
+  public void testOAuth2ResponseParams_1() throws Exception {
+
+    final OAuth2ResponseParams result = new OAuth2ResponseParams();
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getAuthorizationUrl());
+  }
+
+  @Test
+  public void testAddDebug_1() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final String message = "";
+
+    fixture.addDebug(message);
+  }
+
+  @Test
+  public void testAddRequestTrace_1() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder().setStrictNoCache();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+    final HttpResponse response = responseBuilder.create();
+
+    fixture.addRequestTrace(request, response);
+  }
+
+  @Test
+  public void testAddToResponse_1() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = null;
+    final String errorUri = null;
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_2() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = "";
+    final String errorUri = "";
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_3() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = null;
+    final String errorUri = "";
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_4() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = "";
+    final String errorUri = null;
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_5() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = "";
+    final String errorUri = "";
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_6() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = "";
+    final String errorUri = "";
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_7() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = null;
+    final String errorUri = null;
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_8() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = "";
+    final String errorUri = "";
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_9() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = null;
+    final String errorUri = null;
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_10() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = "";
+    final String errorUri = "";
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_11() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = null;
+    final String errorUri = null;
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_12() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = "";
+    final String errorDescription = null;
+    final String errorUri = null;
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_13() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = null;
+    final String errorUri = "";
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_14() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = "";
+    final String errorUri = null;
+    final String errorExplanation = null;
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_15() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = null;
+    final String errorUri = null;
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testAddToResponse_16() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug((String) null);
+    fixture.setAuthorizationUrl("");
+    final HttpResponseBuilder responseBuilder = new HttpResponseBuilder();
+    final String errorCode = null;
+    final String errorDescription = "";
+    final String errorUri = "";
+    final String errorExplanation = "";
+
+    fixture.addToResponse(responseBuilder, errorCode, errorDescription, errorUri, errorExplanation);
+  }
+
+  @Test
+  public void testGetAuthorizationUrl_1() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+
+    final String result = fixture.getAuthorizationUrl();
+
+    Assert.assertEquals("", result);
+  }
+
+  @Test
+  public void testSetAuthorizationUrl_1() throws Exception {
+    final OAuth2ResponseParams fixture = new OAuth2ResponseParams();
+    fixture.addDebug("");
+    fixture.setAuthorizationUrl("");
+    final String authorizationUrl = "";
+
+    fixture.setAuthorizationUrl(authorizationUrl);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/BasicAuthenticationHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/BasicAuthenticationHandlerTest.java
new file mode 100644
index 0000000..c128fc5
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/BasicAuthenticationHandlerTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.net.URI;
+
+public class BasicAuthenticationHandlerTest extends MockUtils {
+  @Test
+  public void testBasicAuthenticationHandler1() throws Exception {
+    final BasicAuthenticationHandler result = new BasicAuthenticationHandler();
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(ClientAuthenticationHandler.class.isInstance(result));
+    Assert.assertEquals(OAuth2Message.BASIC_AUTH_TYPE, result.geClientAuthenticationType());
+  }
+
+  @Test
+  public void testAddOAuth2Authentication1() throws Exception {
+    final BasicAuthenticationHandler fixture = new BasicAuthenticationHandler();
+    final HttpRequest request = null;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals("request is null", result.getContextMessage());
+    Assert.assertEquals(
+            "org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError : AUTHENTICATION_PROBLEM : request is null :  : :null",
+            result.toString());
+  }
+
+  @Test
+  public void testAddOAuth2Authentication2() throws Exception {
+    final BasicAuthenticationHandler fixture = new BasicAuthenticationHandler();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+    final OAuth2Accessor accessor = null;
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals("accessor is invalid null", result.getContextMessage());
+    Assert.assertEquals(
+            "org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError : AUTHENTICATION_PROBLEM : accessor is invalid null :  : :null",
+            result.toString());
+  }
+
+  @Test
+  public void testAddOAuth2Authentication3() throws Exception {
+    final BasicAuthenticationHandler fixture = new BasicAuthenticationHandler();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.AUTHENTICATION_PROBLEM, result.getError());
+    Assert.assertTrue(result.getContextMessage().startsWith("accessor is invalid"));
+  }
+
+  @Test
+  public void testAddOAuth2Authentication4() throws Exception {
+    final BasicAuthenticationHandler fixture = new BasicAuthenticationHandler();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNull(result);
+
+    final String authHeader = request.getHeader("Authorization");
+
+    Assert.assertNotNull(authHeader);
+
+    Assert.assertEquals("Basic: Y2xpZW50SWQxOmNsaWVudFNlY3JldDE=", authHeader);
+  }
+
+  @Test
+  public void testGeClientAuthenticationType1() throws Exception {
+    final BasicAuthenticationHandler fixture = new BasicAuthenticationHandler();
+
+    final String result = fixture.geClientAuthenticationType();
+    Assert.assertEquals(OAuth2Message.BASIC_AUTH_TYPE, result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/BearerTokenHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/BearerTokenHandlerTest.java
new file mode 100644
index 0000000..d4084c7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/BearerTokenHandlerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import java.net.URI;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BearerTokenHandlerTest extends MockUtils {
+  @Test
+  public void testBearerTokenHandler_1() throws Exception {
+
+    final BearerTokenHandler result = new BearerTokenHandler();
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(ResourceRequestHandler.class.isInstance(result));
+    Assert.assertEquals(OAuth2Message.BEARER_TOKEN_TYPE, result.getTokenType());
+  }
+
+  @Test
+  public void testAddOAuth2Params_1() throws Exception {
+    final BearerTokenHandler fixture = new BearerTokenHandler();
+    final OAuth2Accessor accessor = null;
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertNotNull(result.getError());
+    Assert.assertEquals(OAuth2Error.BEARER_TOKEN_PROBLEM, result.getError());
+    Assert.assertNotNull(result.getContextMessage());
+    Assert.assertTrue(result.getContextMessage().startsWith(""));
+  }
+
+  @Test
+  public void testAddOAuth2Params_2() throws Exception {
+    final BearerTokenHandler fixture = new BearerTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertNotNull(result.getError());
+    Assert.assertEquals(OAuth2Error.BEARER_TOKEN_PROBLEM, result.getError());
+    Assert.assertNotNull(result.getContextMessage());
+    Assert.assertTrue(result.getContextMessage().startsWith("accessor is invalid"));
+  }
+
+  @Test
+  public void testAddOAuth2Params_5() throws Exception {
+    final BearerTokenHandler fixture = new BearerTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpRequest request = new HttpRequest((Uri) null);
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertNotNull(result.getError());
+    Assert.assertEquals(OAuth2Error.BEARER_TOKEN_PROBLEM, result.getError());
+    Assert.assertNotNull(result.getContextMessage());
+    Assert.assertTrue(result.getContextMessage().startsWith("unAuthorizedRequestUri"));
+  }
+
+  @Test
+  public void testAddOAuth2Params_6() throws Exception {
+    final BearerTokenHandler fixture = new BearerTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpRequest request = new HttpRequest(Uri.parse(MockUtils.GADGET_URI1));
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNull(result);
+    final String authHeader = request.getHeader("Authorization");
+    Assert.assertNotNull(authHeader);
+    Assert.assertEquals("Bearer accessSecret", authHeader);
+  }
+
+  @Test
+  public void testGetTokenType_1() throws Exception {
+    final BearerTokenHandler fixture = new BearerTokenHandler();
+
+    final String result = fixture.getTokenType();
+
+    Assert.assertEquals("Bearer", result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/ClientCredentialsGrantTypeHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/ClientCredentialsGrantTypeHandlerTest.java
new file mode 100644
index 0000000..5c27b49
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/ClientCredentialsGrantTypeHandlerTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2RequestException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ClientCredentialsGrantTypeHandlerTest extends MockUtils {
+  private static ClientCredentialsGrantTypeHandler ccgth;
+
+  @Before
+  public void setUp() throws Exception {
+    ClientCredentialsGrantTypeHandlerTest.ccgth = new ClientCredentialsGrantTypeHandler(
+        MockUtils.getDummyClientAuthHandlers());
+  }
+
+  @Test
+  public void testClientCredentialsGrantTypeHandler_1() throws Exception {
+    final ClientCredentialsGrantTypeHandler result = ClientCredentialsGrantTypeHandlerTest.ccgth;
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("client_credentials", result.getGrantType());
+    Assert.assertTrue(GrantRequestHandler.class.isInstance(result));
+    Assert.assertEquals(false, result.isAuthorizationEndpointResponse());
+    Assert.assertEquals(false, result.isRedirectRequired());
+    Assert.assertEquals(true, result.isTokenEndpointResponse());
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetAuthorizationRequest_1() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = null;
+
+    final String completeAuthorizationUrl = "xxx";
+
+    fixture.getAuthorizationRequest(accessor, completeAuthorizationUrl);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetAuthorizationRequest_2() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+
+    final String completeAuthorizationUrl = null;
+
+    fixture.getAuthorizationRequest(accessor, completeAuthorizationUrl);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetAuthorizationRequest_3() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+
+    final String completeAuthorizationUrl = "xxx";
+
+    fixture.getAuthorizationRequest(accessor, completeAuthorizationUrl);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetAuthorizationRequest_4() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final String completeAuthorizationUrl = "xxx";
+
+    fixture.getAuthorizationRequest(accessor, completeAuthorizationUrl);
+  }
+
+  @Test
+  public void testGetAuthorizationRequest_5() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_ClientCredentials();
+    final String completeAuthorizationUrl = "xxx";
+
+    final HttpRequest result = fixture.getAuthorizationRequest(accessor, completeAuthorizationUrl);
+
+    Assert.assertNotNull(result);
+    final String postBody = result.getPostBodyAsString();
+    Assert.assertNotNull(postBody);
+    Assert.assertEquals(
+        "client_id=clientId1&client_secret=clientSecret1&grant_type=client_credentials", postBody);
+    Assert.assertNotNull( result.getSecurityToken() );
+    Assert.assertTrue( result.getSecurityToken().isAnonymous() );
+    Assert.assertEquals( accessor.getGadgetUri(), result.getSecurityToken().getAppUrl() );
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetCompleteUrl_1() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = null;
+    fixture.getCompleteUrl(accessor);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetCompleteUrl_2() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+    fixture.getCompleteUrl(accessor);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetCompleteUrl_3() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+
+    fixture.getCompleteUrl(accessor);
+  }
+
+  @Test
+  public void testGetCompleteUrl_4() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_ClientCredentials();
+
+    final String result = fixture.getCompleteUrl(accessor);
+
+    Assert.assertNotNull(result);
+    Assert
+        .assertEquals(
+            "http://www.example.com/token?client_id=clientId1&client_secret=clientSecret1&grant_type=client_credentials&scope=testScope",
+            result);
+  }
+
+  @Test
+  public void testGetGrantType_1() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+
+    final String result = fixture.getGrantType();
+
+    Assert.assertEquals(OAuth2Message.CLIENT_CREDENTIALS, result);
+  }
+
+  @Test
+  public void testIsAuthorizationEndpointResponse_1() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+
+    final boolean result = fixture.isAuthorizationEndpointResponse();
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testIsRedirectRequired_1() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+
+    final boolean result = fixture.isRedirectRequired();
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testIsTokenEndpointResponse_1() throws Exception {
+    final ClientCredentialsGrantTypeHandler fixture = ClientCredentialsGrantTypeHandlerTest.ccgth;
+
+    final boolean result = fixture.isTokenEndpointResponse();
+
+    Assert.assertEquals(true, result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/CodeAuthorizationResponseHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/CodeAuthorizationResponseHandlerTest.java
new file mode 100644
index 0000000..c717694
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/CodeAuthorizationResponseHandlerTest.java
@@ -0,0 +1,504 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.inject.Provider;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Store;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+
+public class CodeAuthorizationResponseHandlerTest extends MockUtils {
+
+  private static CodeAuthorizationResponseHandler carh;
+  private static OAuth2Store store;
+
+  @Before
+  public void setUp() throws Exception {
+    CodeAuthorizationResponseHandlerTest.store = MockUtils.getDummyStore();
+    CodeAuthorizationResponseHandlerTest.carh = new CodeAuthorizationResponseHandler(
+        MockUtils.getDummyMessageProvider(), MockUtils.getDummyClientAuthHandlers(),
+        MockUtils.getDummyTokenEndpointResponseHandlers(), MockUtils.getDummyFetcher());
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  public void testCodeAuthorizationResponseHandler_1() throws Exception {
+    final Provider<OAuth2Message> oauth2MessageProvider = EasyMock.createMock(Provider.class);
+    final List<ClientAuthenticationHandler> clientAuthenticationHandlers = EasyMock
+        .createMock(List.class);
+    final List<TokenEndpointResponseHandler> tokenEndpointResponseHandlers = EasyMock
+        .createMock(List.class);
+    final HttpFetcher fetcher = EasyMock.createMock(HttpFetcher.class);
+
+    EasyMock.replay(oauth2MessageProvider);
+    EasyMock.replay(clientAuthenticationHandlers);
+    EasyMock.replay(tokenEndpointResponseHandlers);
+    EasyMock.replay(fetcher);
+
+    final CodeAuthorizationResponseHandler result = new CodeAuthorizationResponseHandler(
+        oauth2MessageProvider, clientAuthenticationHandlers, tokenEndpointResponseHandlers, fetcher);
+
+    EasyMock.verify(oauth2MessageProvider);
+    EasyMock.verify(clientAuthenticationHandlers);
+    EasyMock.verify(tokenEndpointResponseHandlers);
+    EasyMock.verify(fetcher);
+    Assert.assertNotNull(result);
+    Assert.assertTrue(AuthorizationEndpointResponseHandler.class.isInstance(result));
+  }
+
+  @Test
+  public void testHandleRequest_1() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Redirecting();
+    final HttpServletRequest request = null;
+
+    final OAuth2HandlerError result = fixture.handleRequest(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(OAuth2Error.AUTHORIZATION_CODE_PROBLEM, result.getError());
+    Assert.assertEquals("request is null", result.getContextMessage());
+  }
+
+  @Test
+  public void testHandleRequest_2() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = null;
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final OAuth2HandlerError result = fixture.handleRequest(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(OAuth2Error.AUTHORIZATION_CODE_PROBLEM, result.getError());
+    Assert.assertEquals("accessor is null", result.getContextMessage());
+  }
+
+  @Test
+  public void testHandleRequest_3() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final OAuth2HandlerError result = fixture.handleRequest(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(OAuth2Error.AUTHORIZATION_CODE_PROBLEM, result.getError());
+    Assert.assertEquals("accessor is invalid", result.getContextMessage());
+  }
+
+  @Test
+  public void testHandleRequest_4() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_ClientCredentialsRedirecting();
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final OAuth2HandlerError result = fixture.handleRequest(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(OAuth2Error.AUTHORIZATION_CODE_PROBLEM, result.getError());
+    Assert.assertEquals("grant_type is not code", result.getContextMessage());
+  }
+
+  @Test
+  public void testHandleRequest_5() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Redirecting();
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final OAuth2HandlerError result = fixture.handleRequest(accessor, request);
+
+    Assert.assertNull(result);
+
+    final OAuth2Token accessToken = CodeAuthorizationResponseHandlerTest.store.getToken(
+        accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+        accessor.getScope(), OAuth2Token.Type.ACCESS);
+    Assert.assertNotNull(accessToken);
+    Assert.assertEquals("xxx", new String(accessToken.getSecret(), "UTF-8"));
+    Assert.assertEquals(OAuth2Message.BEARER_TOKEN_TYPE, accessToken.getTokenType());
+    Assert.assertTrue(accessToken.getExpiresAt() > 1000);
+
+    final OAuth2Token refreshToken = CodeAuthorizationResponseHandlerTest.store.getToken(
+        accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+        accessor.getScope(), OAuth2Token.Type.REFRESH);
+    Assert.assertNotNull(refreshToken);
+    Assert.assertEquals("yyy", new String(refreshToken.getSecret(), "UTF-8"));
+  }
+
+  @Test
+  public void testHandleRequest_verifyAnonymousTokenOnRequest() throws Exception {
+    MockUtils.DummyHttpFetcher fetcher = (MockUtils.DummyHttpFetcher)MockUtils.getDummyFetcher();
+    CodeAuthorizationResponseHandler fixture = new CodeAuthorizationResponseHandler(
+        MockUtils.getDummyMessageProvider(), MockUtils.getDummyClientAuthHandlers(),
+        MockUtils.getDummyTokenEndpointResponseHandlers(), fetcher);
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Redirecting();
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final OAuth2HandlerError result = fixture.handleRequest(accessor, request);
+
+    Assert.assertNull(result);
+
+    final OAuth2Token accessToken = CodeAuthorizationResponseHandlerTest.store.getToken(
+        accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+        accessor.getScope(), OAuth2Token.Type.ACCESS);
+    Assert.assertNotNull(accessToken);
+    Assert.assertEquals("xxx", new String(accessToken.getSecret(), "UTF-8"));
+    Assert.assertEquals(OAuth2Message.BEARER_TOKEN_TYPE, accessToken.getTokenType());
+    Assert.assertTrue(accessToken.getExpiresAt() > 1000);
+
+    final OAuth2Token refreshToken = CodeAuthorizationResponseHandlerTest.store.getToken(
+        accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+        accessor.getScope(), OAuth2Token.Type.REFRESH);
+    Assert.assertNotNull(refreshToken);
+    Assert.assertEquals("yyy", new String(refreshToken.getSecret(), "UTF-8"));
+
+    Assert.assertNotNull( fetcher.request );
+
+    SecurityToken st = fetcher.request.getSecurityToken();
+    Assert.assertNotNull( st );
+    Assert.assertTrue( st.isAnonymous() );
+    Assert.assertEquals( accessor.getGadgetUri(), st.getAppUrl() );
+  }
+
+
+  @Test
+  public void testHandleResponse_1() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_ClientCredentials();
+    final HttpResponse response = new HttpResponse();
+    final OAuth2HandlerError result = fixture.handleResponse(accessor, response);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals("doesn't handle responses", result.getContextMessage());
+  }
+
+  @Test
+  public void testHandlesRequest_1() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpServletRequest request = null;
+
+    final boolean result = fixture.handlesRequest(accessor, request);
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testHandlesRequest_2() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Redirecting();
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final boolean result = fixture.handlesRequest(accessor, request);
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testHandlesRequest_3() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = null;
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final boolean result = fixture.handlesRequest(accessor, request);
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testHandlesRequest_4() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpServletRequest request = new DummyHttpServletRequest();
+
+    final boolean result = fixture.handlesRequest(accessor, request);
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testHandlesResponse_1() throws Exception {
+    final CodeAuthorizationResponseHandler fixture = CodeAuthorizationResponseHandlerTest.carh;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponse response = new HttpResponse();
+
+    final boolean result = fixture.handlesResponse(accessor, response);
+
+    Assert.assertEquals(false, result);
+  }
+
+  static class DummyHttpServletRequest implements HttpServletRequest {
+    private final Map<String, String> parameters;
+
+    DummyHttpServletRequest() {
+      this.parameters = new HashMap<String, String>(1);
+      this.parameters.put(OAuth2Message.AUTHORIZATION, "1234");
+    }
+
+    public Object getAttribute(final String arg0) {
+      return null;
+    }
+
+    @SuppressWarnings("rawtypes")
+    public Enumeration getAttributeNames() {
+      return null;
+    }
+
+    public String getCharacterEncoding() {
+      return null;
+    }
+
+    public int getContentLength() {
+      return 0;
+    }
+
+    public String getContentType() {
+      return null;
+    }
+
+    public ServletInputStream getInputStream() throws IOException {
+      return null;
+    }
+
+    public String getLocalAddr() {
+      return null;
+    }
+
+    public String getLocalName() {
+      return null;
+    }
+
+    public int getLocalPort() {
+      return 0;
+    }
+
+    public Locale getLocale() {
+      return null;
+    }
+
+    @SuppressWarnings("rawtypes")
+    public Enumeration getLocales() {
+      return null;
+    }
+
+    public String getParameter(final String arg0) {
+      return this.parameters.get(arg0);
+    }
+
+    @SuppressWarnings("rawtypes")
+    public Map getParameterMap() {
+      return this.parameters;
+    }
+
+    @SuppressWarnings("rawtypes")
+    public Enumeration getParameterNames() {
+      return Collections.enumeration(this.parameters.keySet());
+    }
+
+    public String[] getParameterValues(final String arg0) {
+      return null;
+    }
+
+    public String getProtocol() {
+      return null;
+    }
+
+    public BufferedReader getReader() throws IOException {
+      return null;
+    }
+
+    public String getRealPath(final String arg0) {
+      return null;
+    }
+
+    public String getRemoteAddr() {
+      return null;
+    }
+
+    public String getRemoteHost() {
+      return null;
+    }
+
+    public int getRemotePort() {
+      return 0;
+    }
+
+    public RequestDispatcher getRequestDispatcher(final String arg0) {
+      return null;
+    }
+
+    public String getScheme() {
+      return null;
+    }
+
+    public String getServerName() {
+      return null;
+    }
+
+    public int getServerPort() {
+      return 0;
+    }
+
+    public boolean isSecure() {
+      return false;
+    }
+
+    public void removeAttribute(final String arg0) {
+      // does nothing
+    }
+
+    public void setAttribute(final String arg0, final Object arg1) {
+      // does nothing
+    }
+
+    public void setCharacterEncoding(final String arg0) throws UnsupportedEncodingException {
+      // does nothing
+    }
+
+    public String getAuthType() {
+      return null;
+    }
+
+    public String getContextPath() {
+      return null;
+    }
+
+    public Cookie[] getCookies() {
+      return null;
+    }
+
+    public long getDateHeader(final String arg0) {
+      return 0;
+    }
+
+    public String getHeader(final String arg0) {
+      return null;
+    }
+
+    @SuppressWarnings("rawtypes")
+    public Enumeration getHeaderNames() {
+      return null;
+    }
+
+    @SuppressWarnings("rawtypes")
+    public Enumeration getHeaders(final String arg0) {
+      return null;
+    }
+
+    public int getIntHeader(final String arg0) {
+      return 0;
+    }
+
+    public String getMethod() {
+      return null;
+    }
+
+    public String getPathInfo() {
+      return null;
+    }
+
+    public String getPathTranslated() {
+      return null;
+    }
+
+    public String getQueryString() {
+      return null;
+    }
+
+    public String getRemoteUser() {
+      return null;
+    }
+
+    public String getRequestURI() {
+      return null;
+    }
+
+    public StringBuffer getRequestURL() {
+      return null;
+    }
+
+    public String getRequestedSessionId() {
+      return null;
+    }
+
+    public String getServletPath() {
+      return null;
+    }
+
+    public HttpSession getSession() {
+      return null;
+    }
+
+    public HttpSession getSession(final boolean arg0) {
+      return null;
+    }
+
+    public Principal getUserPrincipal() {
+      return null;
+    }
+
+    public boolean isRequestedSessionIdFromCookie() {
+      return false;
+    }
+
+    public boolean isRequestedSessionIdFromURL() {
+      return false;
+    }
+
+    public boolean isRequestedSessionIdFromUrl() {
+      return false;
+    }
+
+    public boolean isRequestedSessionIdValid() {
+      return false;
+    }
+
+    public boolean isUserInRole(final String arg0) {
+      return false;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/CodeGrantTypeHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/CodeGrantTypeHandlerTest.java
new file mode 100644
index 0000000..2ccc11b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/CodeGrantTypeHandlerTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2RequestException;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class CodeGrantTypeHandlerTest extends MockUtils {
+
+  private static CodeGrantTypeHandler cgth;
+
+  @Before
+  public void setUp() throws Exception {
+    CodeGrantTypeHandlerTest.cgth = new CodeGrantTypeHandler();
+  }
+
+  @Test
+  public void testCodeGrantTypeHandler_1() throws Exception {
+    final CodeGrantTypeHandler result = new CodeGrantTypeHandler();
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(GrantRequestHandler.class.isInstance(result));
+    Assert.assertEquals("code", result.getGrantType());
+    Assert.assertEquals("authorization_code", CodeGrantTypeHandler.getResponseType());
+    Assert.assertEquals(true, result.isAuthorizationEndpointResponse());
+    Assert.assertEquals(true, result.isRedirectRequired());
+    Assert.assertEquals(false, result.isTokenEndpointResponse());
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetAuthorizationRequest_1() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final String completeAuthorizationUrl = "xxx";
+
+    fixture.getAuthorizationRequest(accessor, completeAuthorizationUrl);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetCompleteUrl_1() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+    final OAuth2Accessor accessor = null;
+    fixture.getCompleteUrl(accessor);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetCompleteUrl_2() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+    fixture.getCompleteUrl(accessor);
+  }
+
+  @Test(expected = OAuth2RequestException.class)
+  public void testGetCompleteUrl_3() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_ClientCredentialsRedirecting();
+    fixture.getCompleteUrl(accessor);
+  }
+
+  @Test
+  public void testGetCompleteUrl_4() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final String result = fixture.getCompleteUrl(accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result
+            .startsWith("http://www.example.com/authorize?client_id=clientId1&redirect_uri=https%3A%2F%2Fwww.example.com%2Fgadgets%2Foauth2callback&response_type=code&scope=testScope&state="));
+  }
+
+  @Test
+  public void testGetCompleteUrl_5() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final Map<String, String> additionalParams = Maps.newHashMap();
+    additionalParams.put("param1", "value1");
+    accessor.setAdditionalRequestParams(additionalParams);
+    final String result = fixture.getCompleteUrl(accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(result.contains("&param1=value1"));
+  }
+
+  @Test
+  public void testGetGrantType_1() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+
+    final String result = fixture.getGrantType();
+
+    Assert.assertEquals("code", result);
+  }
+
+  @Test
+  public void testGetResponseType_1() throws Exception {
+    final String result = CodeGrantTypeHandler.getResponseType();
+
+    Assert.assertEquals("authorization_code", result);
+  }
+
+  @Test
+  public void testIsAuthorizationEndpointResponse_1() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+
+    final boolean result = fixture.isAuthorizationEndpointResponse();
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testIsRedirectRequired_1() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+
+    final boolean result = fixture.isRedirectRequired();
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testIsTokenEndpointResponse_1() throws Exception {
+    final CodeGrantTypeHandler fixture = CodeGrantTypeHandlerTest.cgth;
+
+    final boolean result = fixture.isTokenEndpointResponse();
+
+    Assert.assertEquals(false, result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/MacTokenHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/MacTokenHandlerTest.java
new file mode 100644
index 0000000..9f8c641
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/MacTokenHandlerTest.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import java.net.URI;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MacTokenHandlerTest extends MockUtils {
+  @Test
+  public void testMacTokenHandler_1() throws Exception {
+
+    final MacTokenHandler result = new MacTokenHandler();
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(ResourceRequestHandler.class.isInstance(result));
+    Assert.assertEquals(OAuth2Message.MAC_TOKEN_TYPE, result.getTokenType());
+  }
+
+  @Test
+  public void testAddOAuth2Params_1() throws Exception {
+    final MacTokenHandler fixture = new MacTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.MAC_TOKEN_PROBLEM, result.getError());
+    Assert.assertEquals("token type mismatch expected mac but got Bearer",
+        result.getContextMessage());
+  }
+
+  @Test
+  public void testAddOAuth2Params_2() throws Exception {
+    final MacTokenHandler fixture = new MacTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_MacToken();
+    final HttpRequest request = null;
+
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.MAC_TOKEN_PROBLEM, result.getError());
+    Assert.assertEquals("request is null", result.getContextMessage());
+  }
+
+  @Test
+  public void testAddOAuth2Params_3() throws Exception {
+    final MacTokenHandler fixture = new MacTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.MAC_TOKEN_PROBLEM, result.getError());
+    Assert.assertTrue(result.getContextMessage().startsWith("accessor is invalid"));
+  }
+
+  @Test
+  public void testAddOAuth2Params_4() throws Exception {
+    final MacTokenHandler fixture = new MacTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_BadMacToken();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("a")));
+    request.setMethod("");
+
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.MAC_TOKEN_PROBLEM, result.getError());
+    Assert.assertEquals("unsupported algorithm hmac-sha-256", result.getContextMessage());
+  }
+
+  @Test
+  public void testAddOAuth2Params_5() throws Exception {
+    final MacTokenHandler fixture = new MacTokenHandler();
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_MacToken();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI(
+        "http://www.example.com:9080/xxx")));
+    request.setMethod("");
+
+    final OAuth2HandlerError result = fixture.addOAuth2Params(accessor, request);
+
+    Assert.assertNull(result);
+    final String authHeader = request.getHeader("Authorization");
+    Assert.assertNotNull(authHeader);
+    Assert.assertTrue(authHeader.startsWith("MAC id = \"accessSecret\",nonce="));
+  }
+
+  @Test
+  public void testGetTokenType_1() throws Exception {
+    final MacTokenHandler fixture = new MacTokenHandler();
+
+    final String result = fixture.getTokenType();
+
+    Assert.assertEquals(OAuth2Message.MAC_TOKEN_TYPE, result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerErrorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerErrorTest.java
new file mode 100644
index 0000000..c83d6c5
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerErrorTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2HandlerErrorTest {
+  @Test
+  public void testOAuth2HandlerError1() throws Exception {
+    final OAuth2Error error = OAuth2Error.AUTHENTICATION_PROBLEM;
+    final String contextMessage = "";
+    final Exception cause = new Exception();
+
+    final OAuth2HandlerError result = new OAuth2HandlerError(error, contextMessage, cause);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("", result.getContextMessage());
+    Assert.assertEquals(
+            "org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError : AUTHENTICATION_PROBLEM :  :  : :java.lang.Exception",
+            result.toString());
+  }
+
+  @Test
+  public void testGetCause1() throws Exception {
+    final OAuth2HandlerError fixture = new OAuth2HandlerError(OAuth2Error.AUTHENTICATION_PROBLEM,
+            "", new Exception());
+
+    final Exception result = fixture.getCause();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getMessage());
+    Assert.assertEquals(null, result.getLocalizedMessage());
+    Assert.assertEquals("java.lang.Exception", result.toString());
+    Assert.assertEquals(null, result.getCause());
+  }
+
+  @Test
+  public void testGetContextMessage1() throws Exception {
+    final OAuth2HandlerError fixture = new OAuth2HandlerError(OAuth2Error.AUTHENTICATION_PROBLEM,
+            "", new Exception());
+
+    final String result = fixture.getContextMessage();
+
+    Assert.assertEquals("", result);
+  }
+
+  @Test
+  public void testGetError1() throws Exception {
+    final OAuth2HandlerError fixture = new OAuth2HandlerError(OAuth2Error.AUTHENTICATION_PROBLEM,
+            "", new Exception());
+
+    final OAuth2Error result = fixture.getError();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("authentication_problem", result.getErrorCode());
+    Assert.assertEquals("AUTHENTICATION_PROBLEM", result.name());
+    Assert.assertEquals(2, result.ordinal());
+    Assert.assertEquals("AUTHENTICATION_PROBLEM", result.toString());
+  }
+
+  @Test
+  public void testToString1() throws Exception {
+    final OAuth2HandlerError fixture = new OAuth2HandlerError(OAuth2Error.AUTHENTICATION_PROBLEM,
+            "", new Exception());
+
+    final String result = fixture.toString();
+
+    Assert.assertEquals(
+            "org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerError : AUTHENTICATION_PROBLEM :  :  : :java.lang.Exception",
+            result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerModuleTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerModuleTest.java
new file mode 100644
index 0000000..167e942
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/OAuth2HandlerModuleTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import java.util.List;
+
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.oauth2.OAuth2Store;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Provider;
+
+public class OAuth2HandlerModuleTest {
+  @Test
+  public void testConfigure_1() throws Exception {
+    final OAuth2HandlerModule fixture = new OAuth2HandlerModule();
+
+    Assert.assertTrue(AbstractModule.class.isInstance(fixture));
+  }
+
+  @Test
+  @SuppressWarnings({ "unchecked", "unused" })
+  public void testProvideAuthorizationEndpointResponseHandlers_1() throws Exception {
+    final OAuth2HandlerModule fixture = new OAuth2HandlerModule();
+    final CodeAuthorizationResponseHandler codeAuthorizationResponseHandler = new CodeAuthorizationResponseHandler(
+        EasyMock.createNiceMock(Provider.class), EasyMock.createNiceMock(List.class),
+        EasyMock.createNiceMock(List.class), EasyMock.createNiceMock(HttpFetcher.class));
+    final TokenAuthorizationResponseHandler tokenAuthorizationResponseHandler = new TokenAuthorizationResponseHandler(
+        EasyMock.createNiceMock(Provider.class), EasyMock.createNiceMock(OAuth2Store.class));
+
+    final List<AuthorizationEndpointResponseHandler> result = OAuth2HandlerModule
+        .provideAuthorizationEndpointResponseHandlers(codeAuthorizationResponseHandler);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(1, result.size());
+  }
+
+  @SuppressWarnings("unused")
+  @Test
+  public void testProvideClientAuthenticationHandlers_1() throws Exception {
+    final OAuth2HandlerModule fixture = new OAuth2HandlerModule();
+    final BasicAuthenticationHandler basicAuthenticationHandler = new BasicAuthenticationHandler();
+    final StandardAuthenticationHandler standardAuthenticationHandler = new StandardAuthenticationHandler();
+
+    final List<ClientAuthenticationHandler> result = OAuth2HandlerModule
+        .provideClientAuthenticationHandlers(basicAuthenticationHandler,
+            standardAuthenticationHandler);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(2, result.size());
+  }
+
+  @Test
+  @SuppressWarnings({ "unchecked", "unused" })
+  public void testProvideGrantRequestHandlers_1() throws Exception {
+    final OAuth2HandlerModule fixture = new OAuth2HandlerModule();
+    final ClientCredentialsGrantTypeHandler clientCredentialsGrantTypeHandler = new ClientCredentialsGrantTypeHandler(
+        EasyMock.createNiceMock(List.class));
+    final CodeGrantTypeHandler codeGrantTypeHandler = new CodeGrantTypeHandler();
+
+    final List<GrantRequestHandler> result = OAuth2HandlerModule.provideGrantRequestHandlers(
+        clientCredentialsGrantTypeHandler, codeGrantTypeHandler);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(2, result.size());
+  }
+
+  @Test
+  @SuppressWarnings({ "unchecked", "unused" })
+  public void testProvideTokenEndpointResponseHandlers_1() throws Exception {
+    final OAuth2HandlerModule fixture = new OAuth2HandlerModule();
+    final TokenAuthorizationResponseHandler tokenAuthorizationResponseHandler = new TokenAuthorizationResponseHandler(
+        EasyMock.createNiceMock(Provider.class), EasyMock.createNiceMock(OAuth2Store.class));
+
+    final List<TokenEndpointResponseHandler> result = OAuth2HandlerModule
+        .provideTokenEndpointResponseHandlers(tokenAuthorizationResponseHandler);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(1, result.size());
+  }
+
+  @SuppressWarnings("unused")
+  @Test
+  public void testProvideTokenHandlers_1() throws Exception {
+    final OAuth2HandlerModule fixture = new OAuth2HandlerModule();
+    final BearerTokenHandler bearerTokenHandler = new BearerTokenHandler();
+    final MacTokenHandler macTokenHandler = new MacTokenHandler();
+
+    final List<ResourceRequestHandler> result = OAuth2HandlerModule.provideTokenHandlers(
+        bearerTokenHandler, macTokenHandler);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(2, result.size());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/StandardAuthenticationHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/StandardAuthenticationHandlerTest.java
new file mode 100644
index 0000000..0cc23c8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/StandardAuthenticationHandlerTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import java.net.URI;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class StandardAuthenticationHandlerTest extends MockUtils {
+  @Test
+  public void testStandardAuthenticationHandler_1() throws Exception {
+    final StandardAuthenticationHandler result = new StandardAuthenticationHandler();
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(ClientAuthenticationHandler.class.isInstance(result));
+    Assert.assertEquals(OAuth2Message.STANDARD_AUTH_TYPE, result.geClientAuthenticationType());
+
+  }
+
+  @Test
+  public void testAddOAuth2Authentication_1() throws Exception {
+    final StandardAuthenticationHandler fixture = new StandardAuthenticationHandler();
+    final HttpRequest request = null;
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_StandardAuth();
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.AUTHENTICATION_PROBLEM, result.getError());
+    Assert.assertEquals("request is null", result.getContextMessage());
+  }
+
+  @Test
+  public void testAddOAuth2Authentication_2() throws Exception {
+    final StandardAuthenticationHandler fixture = new StandardAuthenticationHandler();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+    final OAuth2Accessor accessor = null;
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(OAuth2Error.AUTHENTICATION_PROBLEM, result.getError());
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals("accessor is null", result.getContextMessage());
+  }
+
+  @Test
+  public void testAddOAuth2Authentication_3() throws Exception {
+    final StandardAuthenticationHandler fixture = new StandardAuthenticationHandler();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(OAuth2Error.AUTHENTICATION_PROBLEM, result.getError());
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals("accessor is invalid", result.getContextMessage());
+  }
+
+  @Test
+  public void testAddOAuth2Authentication_4() throws Exception {
+    final StandardAuthenticationHandler fixture = new StandardAuthenticationHandler();
+    final HttpRequest request = new HttpRequest(Uri.fromJavaUri(new URI("")));
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_StandardAuth();
+
+    final OAuth2HandlerError result = fixture.addOAuth2Authentication(request, accessor);
+
+    Assert.assertNull(result);
+    final String header1 = request.getHeader(OAuth2Message.CLIENT_ID);
+    Assert.assertNotNull(header1);
+    Assert.assertEquals(MockUtils.CLIENT_ID1, header1);
+
+    final String header2 = request.getHeader(OAuth2Message.CLIENT_SECRET);
+    Assert.assertNotNull(header2);
+    Assert.assertEquals(MockUtils.CLIENT_SECRET1, header2);
+
+    final String requestUri = request.getUri().toString();
+    Assert.assertNotNull(requestUri);
+    Assert.assertEquals("", requestUri);
+
+    final String param1 = request.getParam(OAuth2Message.CLIENT_ID);
+    Assert.assertNotNull(param1);
+    Assert.assertEquals(MockUtils.CLIENT_ID1, param1);
+
+    final String param2 = request.getHeader(OAuth2Message.CLIENT_SECRET);
+    Assert.assertNotNull(param2);
+    Assert.assertEquals(MockUtils.CLIENT_SECRET1, param2);
+  }
+
+  @Test
+  public void testGeClientAuthenticationType_1() throws Exception {
+    final StandardAuthenticationHandler fixture = new StandardAuthenticationHandler();
+
+    final String result = fixture.geClientAuthenticationType();
+
+    Assert.assertEquals("STANDARD", result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/TokenAuthorizationResponseHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/TokenAuthorizationResponseHandlerTest.java
new file mode 100644
index 0000000..d12e421
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/handler/TokenAuthorizationResponseHandlerTest.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.handler;
+
+import com.google.inject.Provider;
+
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Store;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TokenAuthorizationResponseHandlerTest extends MockUtils {
+  private static TokenAuthorizationResponseHandler tarh;
+  private static OAuth2Store store;
+
+  @Before
+  public void setUp() throws Exception {
+    final Provider<OAuth2Message> oauth2MessageProvider = MockUtils.getDummyMessageProvider();
+    TokenAuthorizationResponseHandlerTest.store = MockUtils.getDummyStore();
+
+    TokenAuthorizationResponseHandlerTest.tarh = new TokenAuthorizationResponseHandler(
+            oauth2MessageProvider, TokenAuthorizationResponseHandlerTest.store);
+  }
+
+  @Test
+  public void testTokenAuthorizationResponseHandler_1() throws Exception {
+    Assert.assertNotNull(TokenAuthorizationResponseHandlerTest.tarh);
+    Assert.assertTrue(TokenEndpointResponseHandler.class
+            .isInstance(TokenAuthorizationResponseHandlerTest.tarh));
+  }
+
+  @Test
+  public void testHandlesResponse_1() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+    final HttpResponse response = new HttpResponse();
+
+    final boolean result = TokenAuthorizationResponseHandlerTest.tarh.handlesResponse(accessor,
+            response);
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testHandlesResponse_2() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponse response = null;
+
+    final boolean result = TokenAuthorizationResponseHandlerTest.tarh.handlesResponse(accessor,
+            response);
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testHandlesResponse_3() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponse response = new HttpResponse();
+
+    final boolean result = TokenAuthorizationResponseHandlerTest.tarh.handlesResponse(accessor,
+            response);
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testHandleResponse_1() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Error();
+    final HttpResponse response = new HttpResponse();
+
+    final OAuth2HandlerError result = TokenAuthorizationResponseHandlerTest.tarh.handleResponse(
+            accessor, response);
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.TOKEN_RESPONSE_PROBLEM, result.getError());
+    Assert.assertTrue(result.getContextMessage().startsWith("accessor is invalid"));
+  }
+
+  @Test
+  public void testHandleResponse_2() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponse response = null;
+
+    final OAuth2HandlerError result = TokenAuthorizationResponseHandlerTest.tarh.handleResponse(
+            accessor, response);
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.TOKEN_RESPONSE_PROBLEM, result.getError());
+    Assert.assertEquals("response is null", result.getContextMessage());
+  }
+
+  @Test
+  public void testHandleResponse_3() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponseBuilder builder = new HttpResponseBuilder().setStrictNoCache();
+    builder.setHttpStatusCode(HttpResponse.SC_FORBIDDEN);
+    final HttpResponse response = builder.create();
+
+    final OAuth2HandlerError result = TokenAuthorizationResponseHandlerTest.tarh.handleResponse(
+            accessor, response);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.TOKEN_RESPONSE_PROBLEM, result.getError());
+    Assert.assertTrue(result.getContextMessage().startsWith("can't handle error response"));
+  }
+
+  @Test
+  public void testHandleResponse_4() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponseBuilder builder = new HttpResponseBuilder().setStrictNoCache();
+    builder.setHttpStatusCode(HttpResponse.SC_OK);
+    builder.setHeader("Content-Type", "text/plain");
+    builder.setContent("access_token=xxx&token_type=Bearer&expires=1&refresh_token=yyy&example_parameter=example_value");
+    final HttpResponse response = builder.create();
+
+    final OAuth2HandlerError result = TokenAuthorizationResponseHandlerTest.tarh.handleResponse(
+            accessor, response);
+
+    Assert.assertNull(result);
+
+    final OAuth2Token accessToken = TokenAuthorizationResponseHandlerTest.store.getToken(
+            accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+            accessor.getScope(), OAuth2Token.Type.ACCESS);
+    Assert.assertNotNull(accessToken);
+    Assert.assertEquals("xxx", new String(accessToken.getSecret(), "UTF-8"));
+    Assert.assertEquals(OAuth2Message.BEARER_TOKEN_TYPE, accessToken.getTokenType());
+    Assert.assertTrue(accessToken.getExpiresAt() > 1000);
+
+    final OAuth2Token refreshToken = TokenAuthorizationResponseHandlerTest.store.getToken(
+            accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+            accessor.getScope(), OAuth2Token.Type.REFRESH);
+    Assert.assertNotNull(refreshToken);
+    Assert.assertEquals("yyy", new String(refreshToken.getSecret(), "UTF-8"));
+  }
+
+  @Test
+  public void testHandleResponse_5() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponseBuilder builder = new HttpResponseBuilder().setStrictNoCache();
+    builder.setHttpStatusCode(HttpResponse.SC_OK);
+    builder.setHeader("Content-Type", "application/json");
+    builder.setContent("{\"access_token\":\"xxx\",\"token_type\":\"Bearer\",\"expires_in\":\"1\",\"refresh_token\":\"yyy\",\"example_parameter\":\"example_value\"}");
+    final HttpResponse response = builder.create();
+
+    final OAuth2HandlerError result = TokenAuthorizationResponseHandlerTest.tarh.handleResponse(
+            accessor, response);
+
+    Assert.assertNull(result);
+
+    final OAuth2Token accessToken = TokenAuthorizationResponseHandlerTest.store.getToken(
+            accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+            accessor.getScope(), OAuth2Token.Type.ACCESS);
+    Assert.assertNotNull(accessToken);
+    Assert.assertEquals("xxx", new String(accessToken.getSecret(), "UTF-8"));
+    Assert.assertEquals(OAuth2Message.BEARER_TOKEN_TYPE, accessToken.getTokenType());
+    Assert.assertTrue(accessToken.getExpiresAt() > 1000);
+
+    final OAuth2Token refreshToken = TokenAuthorizationResponseHandlerTest.store.getToken(
+            accessor.getGadgetUri(), accessor.getServiceName(), accessor.getUser(),
+            accessor.getScope(), OAuth2Token.Type.REFRESH);
+    Assert.assertNotNull(refreshToken);
+    Assert.assertEquals("yyy", new String(refreshToken.getSecret(), "UTF-8"));
+  }
+
+  @Test
+  public void testHandleResponse_6() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+    final HttpResponseBuilder builder = new HttpResponseBuilder().setStrictNoCache();
+    builder.setHttpStatusCode(HttpResponse.SC_OK);
+    builder.setHeader("Content-Type", "BAD");
+    final HttpResponse response = builder.create();
+
+    final OAuth2HandlerError result = TokenAuthorizationResponseHandlerTest.tarh.handleResponse(
+            accessor, response);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getCause());
+    Assert.assertEquals(OAuth2Error.TOKEN_RESPONSE_PROBLEM, result.getError());
+    Assert.assertTrue(result.getContextMessage().startsWith("Unhandled Content-Type"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/logger/FilteredLoggerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/logger/FilteredLoggerTest.java
new file mode 100644
index 0000000..f984721
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/logger/FilteredLoggerTest.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.logger;
+
+import java.util.ResourceBundle;
+import java.util.logging.Level;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Error;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FilteredLoggerTest {
+  @Test
+  public void testFilteredLogger_1() throws Exception {
+    final String className = "";
+
+    final FilteredLogger result = new FilteredLogger(className);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(false, result.isLoggable());
+  }
+
+  @Test
+  public void testEntering_1() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String sourceClass = "";
+    final String sourceMethod = "";
+
+    fixture.entering(sourceClass, sourceMethod);
+  }
+
+  @Test
+  public void testEntering_2() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String sourceClass = "";
+    final String sourceMethod = "";
+    final Object param = new Object();
+
+    fixture.entering(sourceClass, sourceMethod, param);
+  }
+
+  @Test
+  public void testEntering_3() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String sourceClass = "";
+    final String sourceMethod = "";
+    final Object[] params = new Object[] {};
+
+    fixture.entering(sourceClass, sourceMethod, params);
+  }
+
+  @Test
+  public void testExiting_1() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String sourceClass = "";
+    final String sourceMethod = "";
+
+    fixture.exiting(sourceClass, sourceMethod);
+  }
+
+  @Test
+  public void testExiting_2() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String sourceClass = "";
+    final String sourceMethod = "";
+    final Object result = new Object();
+
+    fixture.exiting(sourceClass, sourceMethod, result);
+  }
+
+  @Test
+  public void testFilterSecrets_1() throws Exception {
+    final String in = "a";
+
+    final String result = FilteredLogger.filterSecrets(in);
+
+    Assert.assertEquals("a", result);
+  }
+
+  @Test
+  public void testFilterSecrets_2() throws Exception {
+    final String in = null;
+
+    final String result = FilteredLogger.filterSecrets(in);
+
+    Assert.assertEquals("", result);
+  }
+
+  @Test
+  public void testFilterSecrets_3() throws Exception {
+    final String in = "";
+
+    final String result = FilteredLogger.filterSecrets(in);
+
+    Assert.assertEquals("", result);
+  }
+
+  @Test
+  public void testFilterSecrets_4() throws Exception {
+    final String in = "?access_token=XXX";
+
+    final String result = FilteredLogger.filterSecrets(in);
+
+    Assert.assertEquals("?access_token=REMOVED", result);
+  }
+
+  @Test
+  public void testFilterSecrets_5() throws Exception {
+    final String in = "Authorization: XXX";
+
+    final String result = FilteredLogger.filterSecrets(in);
+
+    Assert.assertEquals("Authorization:REMOVED", result);
+  }
+
+  @Test
+  public void testGetFilteredLogger_1() throws Exception {
+    final String className = "";
+
+    final FilteredLogger result = FilteredLogger.getFilteredLogger(className);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(false, result.isLoggable());
+  }
+
+  @Test
+  public void testGetResourceBundle_1() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+
+    final ResourceBundle result = fixture.getResourceBundle();
+
+    Assert.assertNotNull(result);
+  }
+
+  @Test
+  public void testIsLoggable_1() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+
+    final boolean result = fixture.isLoggable();
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testIsLoggable_2() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+
+    final boolean result = fixture.isLoggable();
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testIsLoggable_3() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final Level logLevel = Level.FINE;
+
+    final boolean result = fixture.isLoggable(logLevel);
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testLog_1() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String msg = "";
+    final Object param = new Object();
+
+    fixture.log(msg, param);
+  }
+
+  @Test
+  public void testLog_2() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String msg = "";
+    final Throwable thrown = new Throwable();
+
+    fixture.log(msg, thrown);
+
+  }
+
+  @Test
+  public void testLog_3() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final String msg = "";
+    final Object[] params = new Object[] {};
+
+    fixture.log(msg, params);
+  }
+
+  @Test
+  public void testLog_4() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final Level logLevel = Level.FINE;
+    final String msg = "";
+    final Object param = new Object();
+
+    fixture.log(logLevel, msg, param);
+  }
+
+  @Test
+  public void testLog_5() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final Level logLevel = Level.FINE;
+    final String msg = "";
+    final Throwable thrown = new Throwable();
+
+    fixture.log(logLevel, msg, thrown);
+  }
+
+  @Test
+  public void testLog_6() throws Exception {
+    final FilteredLogger fixture = FilteredLogger.getFilteredLogger("");
+    final Level logLevel = Level.FINE;
+    final String msg = "";
+    final Object[] params = new Object[] {};
+
+    fixture.log(logLevel, msg, params);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2CacheExceptionTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2CacheExceptionTest.java
new file mode 100644
index 0000000..d2532dd
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2CacheExceptionTest.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2CacheExceptionTest {
+  @Test
+  public void testOAuth2CacheException_1() throws Exception {
+    final Exception cause = new Exception();
+
+    final OAuth2CacheException result = new OAuth2CacheException(cause);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("java.lang.Exception", result.getMessage());
+    Assert.assertEquals("java.lang.Exception", result.getLocalizedMessage());
+    Assert.assertEquals(
+        "org.apache.shindig.gadgets.oauth2.persistence.OAuth2CacheException: java.lang.Exception",
+        result.toString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2ClientTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2ClientTest.java
new file mode 100644
index 0000000..4c1181f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2ClientTest.java
@@ -0,0 +1,665 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OAuth2ClientTest extends MockUtils {
+  private static OAuth2Client client1;
+  private static OAuth2Client client2;
+
+  @Before
+  public void setUp() throws Exception {
+    OAuth2ClientTest.client1 = MockUtils.getClient_Code_Confidential();
+    OAuth2ClientTest.client2 = MockUtils.getClient_Code_Public();
+  }
+
+  @Test
+  public void testOAuth2Client_1() throws Exception {
+    final OAuth2Client result = new OAuth2Client(MockUtils.getDummyEncrypter());
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getAuthorizationUrl());
+    Assert.assertEquals(null, result.getClientAuthenticationType());
+    Assert.assertEquals(null, result.getClientId());
+    Assert.assertEquals(null, result.getClientSecret());
+    Assert.assertEquals(null, result.getEncryptedSecret());
+    Assert.assertEquals(null, result.getGadgetUri());
+    Assert.assertEquals("NONE", result.getGrantType());
+    Assert.assertEquals(null, result.getRedirectUri());
+    Assert.assertEquals(null, result.getServiceName());
+    Assert.assertEquals(null, result.getTokenUrl());
+    Assert.assertEquals(false, result.isAllowModuleOverride());
+    Assert.assertEquals(false, result.isAuthorizationHeader());
+    Assert.assertEquals(false, result.isUrlParameter());
+    Assert
+    .assertEquals(
+        "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2ClientImpl: serviceName = null , redirectUri = null , gadgetUri = null , clientId = null , grantType = NONE , type = UNKNOWN , grantType = NONE , tokenUrl = null , authorizationUrl = null , this.clientAuthenticationType = null , this.sharedToken = false, this.allowedDomains = []",
+        result.toString());
+  }
+
+  @Test
+  public void testEquals_1() throws Exception {
+
+    final OAuth2Client obj = new OAuth2Client(MockUtils.getDummyEncrypter());
+    obj.setAuthorizationUrl(MockUtils.AUTHORIZE_URL);
+    obj.setClientAuthenticationType(OAuth2Message.BASIC_AUTH_TYPE);
+    obj.setServiceName(MockUtils.SERVICE_NAME);
+    obj.setRedirectUri(MockUtils.REDIRECT_URI);
+    obj.setGrantType(OAuth2Message.AUTHORIZATION);
+    obj.setAllowModuleOverride(true);
+    obj.setAuthorizationHeader(true);
+    obj.setTokenUrl(MockUtils.TOKEN_URL);
+    obj.setGadgetUri(MockUtils.GADGET_URI1);
+    obj.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    obj.setUrlParameter(false);
+    obj.setClientSecret(MockUtils.CLIENT_SECRET1.getBytes("UTF-8"));
+    obj.setClientId(MockUtils.CLIENT_ID1);
+
+    final boolean result = OAuth2ClientTest.client1.equals(obj);
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testEquals_2() throws Exception {
+    final Object obj = new Object();
+
+    boolean result = OAuth2ClientTest.client1.equals(obj);
+
+    Assert.assertFalse(result);
+
+    result = OAuth2ClientTest.client1.equals(OAuth2ClientTest.client2);
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testEquals_3() throws Exception {
+    final boolean result = OAuth2ClientTest.client1.equals(null);
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testGetAuthorizationUrl_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getAuthorizationUrl();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(MockUtils.AUTHORIZE_URL, result);
+  }
+
+  @Test
+  public void testGetClientAuthenticationType_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getClientAuthenticationType();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(OAuth2Message.BASIC_AUTH_TYPE, result);
+  }
+
+  @Test
+  public void testGetClientId_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getClientId();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.CLIENT_ID1, result);
+  }
+
+  @Test
+  public void testGetClientSecret_1() throws Exception {
+    final byte[] result = OAuth2ClientTest.client1.getClientSecret();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.CLIENT_SECRET1, new String(result, "UTF-8"));
+  }
+
+  @Test
+  public void testGetEncryptedSecret_1() throws Exception {
+    final byte[] result = OAuth2ClientTest.client1.getEncryptedSecret();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals("dmjfouTfdsfu2", new String(result, "UTF-8"));
+  }
+
+  @Test
+  public void testGetEncrypter_1() throws Exception {
+    final OAuth2Encrypter result = OAuth2ClientTest.client1.getEncrypter();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.getDummyEncrypter(), result);
+  }
+
+  @Test
+  public void testGetGadgetUri_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getGadgetUri();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.GADGET_URI1, result);
+  }
+
+  @Test
+  public void testGetGrantType_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getGrantType();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(OAuth2Message.AUTHORIZATION_CODE, result);
+  }
+
+  @Test
+  public void testGetRedirectUri_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getRedirectUri();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.REDIRECT_URI, result);
+  }
+
+  @Test
+  public void testGetServiceName_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getServiceName();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result);
+  }
+
+  @Test
+  public void testGetTokenUrl_1() throws Exception {
+    final String result = OAuth2ClientTest.client1.getTokenUrl();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.TOKEN_URL, result);
+  }
+
+  @Test
+  public void testGetType_1() throws Exception {
+    final org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type result = OAuth2ClientTest.client1
+        .getType();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(OAuth2Accessor.Type.CONFIDENTIAL, result);
+  }
+
+  @Test
+  public void testHashCode_1() throws Exception {
+    final int result = OAuth2ClientTest.client1.hashCode();
+
+    Assert.assertEquals(-1410040560, result);
+  }
+
+  @Test
+  public void testHashCode_2() throws Exception {
+    final int result = OAuth2ClientTest.client2.hashCode();
+
+    Assert.assertEquals(-1410040559, result);
+  }
+
+  @Test
+  public void testHashCode_3() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri((String) null);
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+
+    final int result = fixture.hashCode();
+
+    Assert.assertEquals(0, result);
+  }
+
+  @Test
+  public void testIsAllowModuleOverride_1() throws Exception {
+    final boolean result = OAuth2ClientTest.client1.isAllowModuleOverride();
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testIsAllowModuleOverride_2() throws Exception {
+    final boolean result = OAuth2ClientTest.client2.isAllowModuleOverride();
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testIsAuthorizationHeader_1() throws Exception {
+    final boolean result = OAuth2ClientTest.client1.isAuthorizationHeader();
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testIsAuthorizationHeader_2() throws Exception {
+    final boolean result = OAuth2ClientTest.client2.isAuthorizationHeader();
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testIsUrlParameter_1() throws Exception {
+    final boolean result = OAuth2ClientTest.client1.isUrlParameter();
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testIsUrlParameter_2() throws Exception {
+    final boolean result = OAuth2ClientTest.client2.isUrlParameter();
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testSetAllowModuleOverride_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final boolean alllowModuleOverride = true;
+
+    fixture.setAllowModuleOverride(alllowModuleOverride);
+  }
+
+  @Test
+  public void testSetAuthorizationHeader_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final boolean authorizationHeader = true;
+
+    fixture.setAuthorizationHeader(authorizationHeader);
+  }
+
+  @Test
+  public void testSetAuthorizationUrl_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String authorizationUrl = "";
+
+    fixture.setAuthorizationUrl(authorizationUrl);
+  }
+
+  @Test
+  public void testSetClientAuthenticationType_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String clientAuthenticationType = "";
+
+    fixture.setClientAuthenticationType(clientAuthenticationType);
+  }
+
+  @Test
+  public void testSetClientId_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String clientId = "";
+
+    fixture.setClientId(clientId);
+  }
+
+  @Test
+  public void testSetClientSecret_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final byte[] secret = new byte[] {};
+
+    fixture.setClientSecret(secret);
+  }
+
+  @Test
+  public void testSetClientSecret_2() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final byte[] secret = new byte[] {};
+
+    fixture.setClientSecret(secret);
+  }
+
+  @Test
+  public void testSetEncryptedSecret_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final byte[] encryptedSecret = new byte[] {};
+
+    fixture.setEncryptedSecret(encryptedSecret);
+  }
+
+  @Test
+  public void testSetEncryptedSecret_2() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final byte[] encryptedSecret = new byte[] {};
+
+    fixture.setEncryptedSecret(encryptedSecret);
+  }
+
+  @Test
+  public void testSetGadgetUri_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String gadgetUri = "";
+
+    fixture.setGadgetUri(gadgetUri);
+  }
+
+  @Test
+  public void testSetGrantType_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String grantType = "";
+
+    fixture.setGrantType(grantType);
+  }
+
+  @Test
+  public void testSetRedirectUri_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String redirectUri = "";
+
+    fixture.setRedirectUri(redirectUri);
+  }
+
+  @Test
+  public void testSetServiceName_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String serviceName = "";
+
+    fixture.setServiceName(serviceName);
+  }
+
+  @Test
+  public void testSetTokenUrl_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final String tokenUrl = "";
+
+    fixture.setTokenUrl(tokenUrl);
+  }
+
+  @Test
+  public void testSetType_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type type = org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL;
+
+    fixture.setType(type);
+  }
+
+  @Test
+  public void testSetUrlParameter_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+    final boolean urlParameter = true;
+
+    fixture.setUrlParameter(urlParameter);
+  }
+
+  @Test
+  public void testToString_1() throws Exception {
+    final OAuth2Client fixture = new OAuth2Client(MockUtils.getDummyEncrypter());
+    fixture.setAuthorizationUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setServiceName("");
+    fixture.setRedirectUri("");
+    fixture.setGrantType("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setAllowModuleOverride(true);
+    fixture.setAuthorizationHeader(true);
+    fixture.setTokenUrl("");
+    fixture.setGadgetUri("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Accessor.Type.CONFIDENTIAL);
+    fixture.setUrlParameter(true);
+    fixture.setClientSecret(new byte[] {});
+    fixture.setClientId("");
+
+    final String result = fixture.toString();
+
+    Assert.assertNotNull(result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2EncryptionExceptionTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2EncryptionExceptionTest.java
new file mode 100644
index 0000000..ad6155f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2EncryptionExceptionTest.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2EncryptionExceptionTest {
+  @Test
+  public void testOAuth2EncryptionException_1() throws Exception {
+    final Exception cause = new Exception();
+
+    final OAuth2EncryptionException result = new OAuth2EncryptionException(cause);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(500, result.getHttpStatusCode());
+    Assert.assertEquals("java.lang.Exception", result.getMessage());
+    Assert.assertEquals("java.lang.Exception", result.getLocalizedMessage());
+    Assert
+        .assertEquals(
+            "org.apache.shindig.gadgets.oauth2.persistence.OAuth2EncryptionException: java.lang.Exception",
+            result.toString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2PersistenceExceptionTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2PersistenceExceptionTest.java
new file mode 100644
index 0000000..abc134e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2PersistenceExceptionTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2PersistenceExceptionTest {
+  @Test
+  public void testOAuth2PersistenceException_1() throws Exception {
+    final Exception cause = new Exception();
+
+    final OAuth2PersistenceException result = new OAuth2PersistenceException(cause);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("java.lang.Exception", result.getMessage());
+    Assert.assertEquals("java.lang.Exception", result.getLocalizedMessage());
+    Assert
+        .assertEquals(
+            "org.apache.shindig.gadgets.oauth2.persistence.OAuth2PersistenceException: java.lang.Exception",
+            result.toString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2TokenPersistenceTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2TokenPersistenceTest.java
new file mode 100644
index 0000000..9fc68c9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/OAuth2TokenPersistenceTest.java
@@ -0,0 +1,696 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class OAuth2TokenPersistenceTest extends MockUtils {
+  private static OAuth2TokenPersistence accessToken;
+  private static OAuth2TokenPersistence refreshToken;
+
+  @Before
+  public void setUp() throws Exception {
+    OAuth2TokenPersistenceTest.accessToken = MockUtils.getAccessToken();
+    OAuth2TokenPersistenceTest.refreshToken = MockUtils.getRefreshToken();
+  }
+
+  @Test
+  public void testOAuth2TokenPersistence_1() throws Exception {
+    final OAuth2TokenPersistence result = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(null, result.getEncryptedMacSecret());
+    Assert.assertEquals(null, result.getEncryptedSecret());
+    Assert.assertEquals(0L, result.getExpiresAt());
+    Assert.assertEquals(null, result.getGadgetUri());
+    Assert.assertEquals(0L, result.getIssuedAt());
+    Assert.assertEquals(null, result.getMacAlgorithm());
+    Assert.assertEquals(null, result.getMacExt());
+    Assert.assertEquals(null, result.getMacSecret());
+    Assert.assertEquals(null, result.getScope());
+    Assert.assertEquals(null, result.getSecret());
+    Assert.assertEquals(null, result.getServiceName());
+    Assert.assertEquals("Bearer", result.getTokenType());
+    Assert.assertEquals(null, result.getType());
+    Assert.assertEquals(null, result.getUser());
+    Assert
+        .assertEquals(
+            "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2TokenImpl: serviceName = null , user = null , gadgetUri = null , scope = null , tokenType = Bearer , issuedAt = 0 , expiresAt = 0 , type = null",
+            result.toString());
+  }
+
+  @Test
+  public void testEquals_1() throws Exception {
+    final OAuth2TokenPersistence obj = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    obj.setServiceName(MockUtils.SERVICE_NAME);
+    obj.setUser(MockUtils.USER);
+    obj.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    obj.setIssuedAt(0L);
+    obj.setEncryptedMacSecret(new byte[] {});
+    obj.setMacAlgorithm("");
+    obj.setScope(MockUtils.SCOPE);
+    obj.setExpiresAt(1L);
+    obj.setMacSecret(new byte[] {});
+    obj.setSecret(new byte[] {});
+    obj.setGadgetUri(MockUtils.GADGET_URI1);
+    obj.setMacExt("");
+    obj.setTokenType(OAuth2Message.BEARER_TOKEN_TYPE);
+
+    final boolean result = OAuth2TokenPersistenceTest.accessToken.equals(obj);
+
+    Assert.assertTrue(result);
+  }
+
+  @Test
+  public void testEquals_2() throws Exception {
+    final boolean result = OAuth2TokenPersistenceTest.accessToken
+        .equals(OAuth2TokenPersistenceTest.refreshToken);
+
+    Assert.assertFalse(result);
+  }
+
+  @Test
+  public void testEquals_3() throws Exception {
+    Assert.assertFalse(OAuth2TokenPersistenceTest.accessToken.equals(new Object()));
+  }
+
+  @Test
+  public void testEquals_4() throws Exception {
+    Assert.assertFalse(OAuth2TokenPersistenceTest.accessToken.equals(null));
+  }
+
+  @Test
+  public void testGetEncryptedMacSecret_1() throws Exception {
+    final byte[] result = OAuth2TokenPersistenceTest.accessToken.getEncryptedMacSecret();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("", new String(result, "UTF-8"));
+  }
+
+  @Test
+  public void testGetEncryptedSecret_1() throws Exception {
+    final byte[] result = OAuth2TokenPersistenceTest.accessToken.getEncryptedSecret();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("bddfttTfdsfu", new String(result, "UTF-8"));
+  }
+
+  @Test
+  public void testGetExpiresAt_1() throws Exception {
+    final long result = OAuth2TokenPersistenceTest.accessToken.getExpiresAt();
+
+    Assert.assertEquals(1L, result);
+  }
+
+  @Test
+  public void testGetGadgetUri_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.getGadgetUri();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.GADGET_URI1, result);
+  }
+
+  @Test
+  public void testGetIssuedAt_1() throws Exception {
+    final long result = OAuth2TokenPersistenceTest.accessToken.getIssuedAt();
+
+    Assert.assertEquals(0L, result);
+  }
+
+  @Test
+  public void testGetMacAlgorithm_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.getMacAlgorithm();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals("", result);
+  }
+
+  @Test
+  public void testGetMacExt_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.getMacExt();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals("", result);
+  }
+
+  @Test
+  public void testGetMacSecret_1() throws Exception {
+    final byte[] result = OAuth2TokenPersistenceTest.accessToken.getMacSecret();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals("", new String(result, "UTF-8"));
+  }
+
+  @Test
+  public void testGetProperties_1() throws Exception {
+    final Map<String, String> result = OAuth2TokenPersistenceTest.accessToken.getProperties();
+
+    Assert.assertNotNull(result);
+  }
+
+  @Test
+  public void testGetScope_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.getScope();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.SCOPE, result);
+  }
+
+  @Test
+  public void testGetSecret_1() throws Exception {
+    final byte[] result = OAuth2TokenPersistenceTest.accessToken.getSecret();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.ACCESS_SECRET, new String(result, "UTF-8"));
+  }
+
+  @Test
+  public void testGetServiceName_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.getServiceName();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result);
+  }
+
+  @Test
+  public void testGetTokenType_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.getTokenType();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(OAuth2Message.BEARER_TOKEN_TYPE, result);
+  }
+
+  @Test
+  public void testGetType_1() throws Exception {
+    final org.apache.shindig.gadgets.oauth2.OAuth2Token.Type result = OAuth2TokenPersistenceTest.accessToken
+        .getType();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS, result);
+  }
+
+  @Test
+  public void testGetType_2() throws Exception {
+    final org.apache.shindig.gadgets.oauth2.OAuth2Token.Type result = OAuth2TokenPersistenceTest.refreshToken
+        .getType();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.REFRESH, result);
+  }
+
+  @Test
+  public void testGetUser_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.getUser();
+
+    Assert.assertNotNull(result);
+
+    Assert.assertEquals(MockUtils.USER, result);
+  }
+
+  @Test
+  public void testHashCode_1() throws Exception {
+    final int result = OAuth2TokenPersistenceTest.accessToken.hashCode();
+
+    Assert.assertEquals(-1087355025, result);
+  }
+
+  @Test
+  public void testHashCode_2() throws Exception {
+    final int result = OAuth2TokenPersistenceTest.refreshToken.hashCode();
+
+    Assert.assertEquals(-1380171248, result);
+  }
+
+  @Test
+  public void testHashCode_3() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri((String) null);
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+
+    final int result = fixture.hashCode();
+
+    Assert.assertEquals(0, result);
+  }
+
+  @Test
+  public void testSetEncryptedMacSecret_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final byte[] encryptedSecret = new byte[] {};
+
+    fixture.setEncryptedMacSecret(encryptedSecret);
+  }
+
+  @Test
+  public void testSetEncryptedMaxSecret_2() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final byte[] encryptedSecret = new byte[] {};
+
+    fixture.setEncryptedMacSecret(encryptedSecret);
+  }
+
+  @Test
+  public void testSetEncryptedSecret_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final byte[] encryptedSecret = new byte[] {};
+
+    fixture.setEncryptedSecret(encryptedSecret);
+  }
+
+  @Test
+  public void testSetEncryptedSecret_2() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final byte[] encryptedSecret = new byte[] {};
+
+    fixture.setEncryptedSecret(encryptedSecret);
+  }
+
+  @Test
+  public void testSetExpiresAt_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final long expiresAt = 1L;
+
+    fixture.setExpiresAt(expiresAt);
+  }
+
+  @Test
+  public void testSetGadgetUri_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final String gadgetUri = "";
+
+    fixture.setGadgetUri(gadgetUri);
+  }
+
+  @Test
+  public void testSetIssuedAt_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final long issuedAt = 1L;
+
+    fixture.setIssuedAt(issuedAt);
+  }
+
+  @Test
+  public void testSetMacAlgorithm_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final String algorithm = "";
+
+    fixture.setMacAlgorithm(algorithm);
+  }
+
+  @Test
+  public void testSetMacExt_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final String macExt = "";
+
+    fixture.setMacExt(macExt);
+  }
+
+  @Test
+  public void testSetMacSecret_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final byte[] secret = new byte[] {};
+
+    fixture.setMacSecret(secret);
+  }
+
+  @Test
+  public void testSetMacSecret_2() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final byte[] secret = new byte[] {};
+
+    fixture.setMacSecret(secret);
+  }
+
+  @Test
+  public void testSetScope_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final String scope = "";
+
+    fixture.setScope(scope);
+  }
+
+  @Test
+  public void testSetSecret_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final byte[] secret = new byte[] {};
+
+    fixture.setSecret(secret);
+  }
+
+  @Test
+  public void testSetSecret_2() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence();
+    fixture.setServiceName("");
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+
+    byte[] secret = "abcdef".getBytes();
+    fixture.setEncryptedSecret( secret );
+    assertArrayEquals(secret, fixture.getSecret());
+    fixture.setEncryptedMacSecret(secret);
+    assertArrayEquals(secret, fixture.getMacSecret());
+
+    byte[] secret2 = "zyxwvu".getBytes();
+    fixture.setSecret( secret2 );
+    assertArrayEquals( secret2, fixture.getEncryptedSecret() );
+    fixture.setMacSecret( secret2 );
+    assertArrayEquals( secret2, fixture.getEncryptedMacSecret() );
+
+  }
+
+  @Test
+  public void testSetServiceName_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final String serviceName = "";
+
+    fixture.setServiceName(serviceName);
+  }
+
+  @Test
+  public void testSetTokenType_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final String tokenType = "";
+
+    fixture.setTokenType(tokenType);
+  }
+
+  @Test
+  public void testSetType_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final org.apache.shindig.gadgets.oauth2.OAuth2Token.Type type = org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS;
+
+    fixture.setType(type);
+  }
+
+  @Test
+  public void testSetUser_1() throws Exception {
+    final OAuth2TokenPersistence fixture = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    fixture.setServiceName("");
+    fixture.setEncryptedSecret(new byte[] {});
+    fixture.setUser("");
+    fixture.setType(org.apache.shindig.gadgets.oauth2.OAuth2Token.Type.ACCESS);
+    fixture.setIssuedAt(1L);
+    fixture.setEncryptedMacSecret(new byte[] {});
+    fixture.setMacAlgorithm("");
+    fixture.setScope("");
+    fixture.setExpiresAt(1L);
+    fixture.setMacSecret(new byte[] {});
+    fixture.setSecret(new byte[] {});
+    fixture.setGadgetUri("");
+    fixture.setMacExt("");
+    fixture.setTokenType("");
+    final String user = "";
+
+    fixture.setUser(user);
+
+  }
+
+  @Test
+  public void testToString_1() throws Exception {
+    final String result = OAuth2TokenPersistenceTest.accessToken.toString();
+
+    Assert.assertNotNull(result);
+
+    Assert
+        .assertEquals(
+            "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2TokenImpl: serviceName = serviceName , user = testUser , gadgetUri = http://www.example.com/1 , scope = testScope , tokenType = Bearer , issuedAt = 0 , expiresAt = 1 , type = ACCESS",
+            result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/InMemoryCacheTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/InMemoryCacheTest.java
new file mode 100644
index 0000000..c99aa9e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/InMemoryCacheTest.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.apache.shindig.gadgets.oauth2.BasicOAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2CallbackState;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.OAuth2Store;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token;
+import org.apache.shindig.gadgets.oauth2.OAuth2Token.Type;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Cache;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2TokenPersistence;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.HashSet;
+
+public class InMemoryCacheTest extends MockUtils {
+
+  private InMemoryCache cache;
+
+  @Before
+  public void setUp() throws Exception {
+    this.cache = new InMemoryCache();
+    Assert.assertNotNull(this.cache);
+    Assert.assertTrue(OAuth2Cache.class.isInstance(this.cache));
+
+    this.cache.storeClient(MockUtils.getClient_Code_Confidential());
+    this.cache.storeClient(MockUtils.getClient_Code_Public());
+
+    this.cache.storeToken(MockUtils.getAccessToken());
+    this.cache.storeToken(MockUtils.getRefreshToken());
+
+    this.cache.storeOAuth2Accessor(MockUtils.getOAuth2Accessor_Code());
+    this.cache.storeOAuth2Accessor(MockUtils.getOAuth2Accessor_Error());
+  }
+
+  @Test
+  public void testClearClients_1() throws Exception {
+    Assert.assertNotNull(this.cache.getClient(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME));
+
+    this.cache.clearClients();
+
+    Assert.assertNull(this.cache.getClient(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME));
+  }
+
+  @Test
+  public void testClearTokens_1() throws Exception {
+    Assert.assertNotNull(this.cache.getToken(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME,
+            MockUtils.USER, MockUtils.SCOPE, Type.ACCESS));
+
+    this.cache.clearTokens();
+
+    Assert.assertNull(this.cache.getToken(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME,
+            MockUtils.USER, MockUtils.SCOPE, Type.ACCESS));
+  }
+
+  @Test
+  public void testGetClient_1() throws Exception {
+    final OAuth2Client result = this.cache.getClient(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(MockUtils.CLIENT_ID1, result.getClientId());
+  }
+
+  @Test
+  public void testGetOAuth2Accessor_1() throws Exception {
+    final OAuth2Accessor accessor = MockUtils.getOAuth2Accessor_Code();
+
+    final OAuth2CallbackState state = new OAuth2CallbackState(MockUtils.getDummyStateCrypter());
+    state.setGadgetUri(accessor.getGadgetUri());
+    state.setServiceName(accessor.getServiceName());
+    state.setUser(accessor.getUser());
+    state.setScope(accessor.getScope());
+
+    final OAuth2Accessor result = this.cache.getOAuth2Accessor(state);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(MockUtils.CLIENT_ID1, result.getClientId());
+  }
+
+  @Test
+  public void testGetOAuth2Accessor_3() throws Exception {
+    final OAuth2CallbackState state = new OAuth2CallbackState(MockUtils.getDummyStateCrypter());
+    state.setGadgetUri("BAD");
+    state.setServiceName("BAD");
+    state.setUser("BAD");
+    state.setScope("BAD");
+    final OAuth2Accessor result = this.cache.getOAuth2Accessor(state);
+
+    Assert.assertNull(result);
+  }
+
+  @Test
+  public void testGetToken_1() throws Exception {
+    final OAuth2Token result = this.cache.getToken(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME,
+            MockUtils.USER, MockUtils.SCOPE, Type.ACCESS);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(MockUtils.ACCESS_SECRET, new String(result.getSecret(), "UTF-8"));
+  }
+
+  @Test
+  public void testRemoveClient_1() throws Exception {
+
+    OAuth2Client result = this.cache.getClient(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME);
+
+    Assert.assertNotNull(result);
+
+    result = this.cache.removeClient(result);
+
+    Assert.assertNotNull(result);
+
+    result = this.cache.removeClient(result);
+
+    Assert.assertNull(result);
+  }
+
+  @Test
+  public void testRemoveToken_1() throws Exception {
+
+    OAuth2Token result = this.cache.getToken(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME,
+            MockUtils.USER, MockUtils.SCOPE, Type.ACCESS);
+
+    Assert.assertNotNull(result);
+
+    result = this.cache.removeToken(result);
+
+    Assert.assertNotNull(result);
+
+    result = this.cache.removeToken(result);
+
+    Assert.assertNull(result);
+
+  }
+
+  @Test
+  public void testStoreClient_1() throws Exception {
+
+    OAuth2Client client = new OAuth2Client(MockUtils.getDummyEncrypter());
+    client.setGadgetUri("xxx");
+    client.setServiceName("yyy");
+
+    this.cache.storeClient(client);
+
+    client = this.cache.getClient(client.getGadgetUri(), client.getServiceName());
+
+    Assert.assertNotNull(client);
+    Assert.assertEquals("xxx", client.getGadgetUri());
+    Assert.assertEquals("yyy", client.getServiceName());
+  }
+
+  @Test
+  public void testStoreClients_1() throws Exception {
+
+    this.cache.clearClients();
+
+    final Collection<OAuth2Client> clients = new HashSet<OAuth2Client>();
+    clients.add(MockUtils.getClient_Code_Confidential());
+    clients.add(MockUtils.getClient_Code_Public());
+
+    this.cache.storeClients(clients);
+
+    Assert.assertNotNull(this.cache.getClient(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME));
+    Assert.assertNotNull(this.cache.getClient(MockUtils.GADGET_URI2, MockUtils.SERVICE_NAME));
+  }
+
+  @Test
+  public void testStoreOAuth2Accessor_1() throws Exception {
+    final OAuth2Store store = MockUtils.getDummyStore(this.cache, null, null, null, null, null,
+            null);
+    OAuth2Accessor accessor = new BasicOAuth2Accessor("XXX", "YYY", "ZZZ", "", false, store, "AAA",
+            null, null);
+
+    this.cache.storeOAuth2Accessor(accessor);
+
+    final OAuth2CallbackState state = new OAuth2CallbackState(MockUtils.getDummyStateCrypter());
+    state.setGadgetUri(accessor.getGadgetUri());
+    state.setServiceName(accessor.getServiceName());
+    state.setUser(accessor.getUser());
+    state.setScope(accessor.getScope());
+    accessor = this.cache.getOAuth2Accessor(state);
+
+    Assert.assertNotNull(accessor);
+    Assert.assertEquals("XXX", accessor.getGadgetUri());
+    Assert.assertEquals("YYY", accessor.getServiceName());
+    Assert.assertEquals("ZZZ", accessor.getUser());
+    Assert.assertEquals("", accessor.getScope());
+    Assert.assertEquals(false, accessor.isAllowModuleOverrides());
+    Assert.assertEquals("AAA", accessor.getRedirectUri());
+  }
+
+  @Test
+  public void testStoreToken_1() throws Exception {
+    OAuth2Token token = new OAuth2TokenPersistence(MockUtils.getDummyEncrypter());
+    token.setGadgetUri("xxx");
+    token.setServiceName("yyy");
+    token.setExpiresAt(2);
+    token.setIssuedAt(1);
+    token.setMacAlgorithm(OAuth2Message.HMAC_SHA_1);
+    token.setMacSecret("shh, it's a secret".getBytes("UTF-8"));
+    token.setScope("mac_scope");
+    token.setSecret("i'll never tell".getBytes("UTF-8"));
+    token.setTokenType(OAuth2Message.MAC_TOKEN_TYPE);
+    token.setType(OAuth2Token.Type.ACCESS);
+    token.setUser("zzz");
+
+    this.cache.storeToken(token);
+
+    token = this.cache.getToken(token.getGadgetUri(), token.getServiceName(), token.getUser(),
+            token.getScope(), token.getType());
+
+    Assert.assertNotNull(token);
+    Assert.assertEquals("xxx", token.getGadgetUri());
+    Assert.assertEquals("yyy", token.getServiceName());
+
+    Assert.assertEquals(2, token.getExpiresAt());
+    Assert.assertEquals(1, token.getIssuedAt());
+    Assert.assertEquals(OAuth2Message.HMAC_SHA_1, token.getMacAlgorithm());
+    Assert.assertEquals("shh, it's a secret", new String(token.getMacSecret(), "UTF-8"));
+    Assert.assertEquals("mac_scope", token.getScope());
+    Assert.assertEquals("i'll never tell", new String(token.getSecret(), "UTF-8"));
+    Assert.assertEquals(OAuth2Message.MAC_TOKEN_TYPE, token.getTokenType());
+    Assert.assertEquals(OAuth2Token.Type.ACCESS, token.getType());
+    Assert.assertEquals("zzz", token.getUser());
+  }
+
+  @Test
+  public void testStoreTokens_1() throws Exception {
+    this.cache.clearTokens();
+
+    final Collection<OAuth2Token> tokens = new HashSet<OAuth2Token>(2);
+
+    final OAuth2Token accessToken = MockUtils.getAccessToken();
+    final OAuth2Token refreshToken = MockUtils.getRefreshToken();
+
+    tokens.add(accessToken);
+    tokens.add(refreshToken);
+
+    this.cache.storeTokens(tokens);
+
+    Assert.assertNotNull(this.cache.getToken(accessToken.getGadgetUri(),
+            accessToken.getServiceName(), accessToken.getUser(), accessToken.getScope(),
+            accessToken.getType()));
+    Assert.assertNotNull(this.cache.getToken(refreshToken.getGadgetUri(),
+            refreshToken.getServiceName(), refreshToken.getUser(), refreshToken.getScope(),
+            refreshToken.getType()));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/JSONOAuth2PersisterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/JSONOAuth2PersisterTest.java
new file mode 100644
index 0000000..da54b49
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/JSONOAuth2PersisterTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.apache.shindig.gadgets.oauth2.OAuth2Accessor;
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Client;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Set;
+
+public class JSONOAuth2PersisterTest extends MockUtils {
+  private JSONOAuth2Persister persister;
+
+  @Before
+  public void setUp() throws Exception {
+    this.persister = MockUtils.getDummyPersister();
+  }
+
+  @Test
+  public void testLoadClients_1() throws Exception {
+
+    final Set<OAuth2Client> result = this.persister.loadClients();
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals(2, result.size());
+
+    for (final OAuth2Client client : result) {
+      final String gadgetUri = client.getGadgetUri();
+      Assert.assertNotNull(gadgetUri);
+      final boolean goodClient = gadgetUri.equals(MockUtils.GADGET_URI1)
+              || gadgetUri.equals(MockUtils.GADGET_URI2);
+      Assert.assertTrue(goodClient);
+      if (gadgetUri.equals(MockUtils.GADGET_URI1)) {
+        Assert.assertEquals(MockUtils.AUTHORIZE_URL, client.getAuthorizationUrl());
+        Assert.assertEquals(OAuth2Message.BASIC_AUTH_TYPE, client.getClientAuthenticationType());
+        Assert.assertEquals(MockUtils.CLIENT_ID1, client.getClientId());
+        Assert.assertEquals(MockUtils.CLIENT_SECRET1, new String(client.getClientSecret(), "UTF-8"));
+        Assert.assertEquals(MockUtils.getDummyEncrypter(), client.getEncrypter());
+        Assert.assertEquals(OAuth2Message.AUTHORIZATION, client.getGrantType());
+        Assert.assertEquals(MockUtils.REDIRECT_URI, client.getRedirectUri());
+        Assert.assertEquals(MockUtils.SERVICE_NAME, client.getServiceName());
+        Assert.assertEquals(MockUtils.TOKEN_URL, client.getTokenUrl());
+        Assert.assertEquals(OAuth2Accessor.Type.CONFIDENTIAL, client.getType());
+        Assert.assertEquals(true, client.isAllowModuleOverride());
+        Assert.assertEquals(true, client.isAuthorizationHeader());
+        Assert.assertEquals(false, client.isUrlParameter());
+        Assert.assertArrayEquals(new String[] { "example.com", "ibm.com" },
+                client.getAllowedDomains());
+      } else if (gadgetUri.equals(MockUtils.GADGET_URI2)) {
+        Assert.assertEquals(MockUtils.AUTHORIZE_URL, client.getAuthorizationUrl());
+        Assert.assertEquals(OAuth2Message.STANDARD_AUTH_TYPE, client.getClientAuthenticationType());
+        Assert.assertEquals(MockUtils.CLIENT_ID2, client.getClientId());
+        Assert.assertEquals(MockUtils.CLIENT_SECRET2, new String(client.getClientSecret(), "UTF-8"));
+        Assert.assertEquals(MockUtils.getDummyEncrypter(), client.getEncrypter());
+        Assert.assertEquals(OAuth2Message.CLIENT_CREDENTIALS, client.getGrantType());
+        Assert.assertEquals(MockUtils.REDIRECT_URI, client.getRedirectUri());
+        Assert.assertEquals(MockUtils.SERVICE_NAME, client.getServiceName());
+        Assert.assertEquals(MockUtils.TOKEN_URL, client.getTokenUrl());
+        Assert.assertEquals(OAuth2Accessor.Type.PUBLIC, client.getType());
+        Assert.assertEquals(false, client.isAllowModuleOverride());
+        Assert.assertEquals(false, client.isAuthorizationHeader());
+        Assert.assertEquals(true, client.isUrlParameter());
+        Assert.assertArrayEquals(new String[0], client.getAllowedDomains());
+      } else {
+        throw new RuntimeException("Bad client found " + gadgetUri);
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/NoOpEncrypterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/NoOpEncrypterTest.java
new file mode 100644
index 0000000..6037915
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/NoOpEncrypterTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.apache.shindig.gadgets.oauth2.persistence.OAuth2Encrypter;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class NoOpEncrypterTest {
+  @Test
+  public void testNoOpEncrypter_1() throws Exception {
+
+    final NoOpEncrypter result = new NoOpEncrypter();
+
+    Assert.assertNotNull(result);
+    Assert.assertTrue(OAuth2Encrypter.class.isInstance(result));
+  }
+
+  @Test
+  public void testDecrypt_1() throws Exception {
+    final NoOpEncrypter fixture = new NoOpEncrypter();
+    final String encryptedSecret = "secretin";
+
+    final byte[] bytes = fixture.decrypt(encryptedSecret.getBytes("UTF-8"));
+
+    final String result = new String(bytes, "UTF-8");
+
+    Assert.assertEquals("secretin", result);
+  }
+
+  @Test
+  public void testDecrypt_2() throws Exception {
+    final NoOpEncrypter fixture = new NoOpEncrypter();
+
+    final byte[] result = fixture.decrypt(null);
+
+    Assert.assertEquals(null, result);
+  }
+
+  @Test
+  public void testDecrypt_3() throws Exception {
+    final NoOpEncrypter fixture = new NoOpEncrypter();
+    final String encryptedSecret = "";
+
+    final byte[] bytes = fixture.decrypt(encryptedSecret.getBytes("UTF-8"));
+
+    final String result = new String(bytes, "UTF-8");
+
+    Assert.assertEquals("", result);
+  }
+
+  @Test
+  public void testEncrypt_1() throws Exception {
+    final NoOpEncrypter fixture = new NoOpEncrypter();
+    final String plainSecret = "secretin";
+
+    final byte[] bytes = fixture.encrypt(plainSecret.getBytes("UTF-8"));
+
+    final String result = new String(bytes, "UTF-8");
+
+    Assert.assertEquals("secretin", result);
+  }
+
+  @Test
+  public void testEncrypt_2() throws Exception {
+    final NoOpEncrypter fixture = new NoOpEncrypter();
+
+    final byte[] result = fixture.encrypt(null);
+
+    Assert.assertEquals(null, result);
+  }
+
+  @Test
+  public void testEncrypt_3() throws Exception {
+    final NoOpEncrypter fixture = new NoOpEncrypter();
+    final String plainSecret = "";
+
+    final byte[] bytes = fixture.encrypt(plainSecret.getBytes("UTF-8"));
+
+    final String result = new String(bytes, "UTF-8");
+
+    Assert.assertEquals("", result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2GadgetBindingTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2GadgetBindingTest.java
new file mode 100644
index 0000000..c3433c8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2GadgetBindingTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.apache.shindig.gadgets.oauth2.MockUtils;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class OAuth2GadgetBindingTest extends MockUtils {
+  private static final OAuth2GadgetBinding FIXTURE = new OAuth2GadgetBinding(MockUtils.GADGET_URI1,
+      MockUtils.SERVICE_NAME, "xxx", true);
+
+  @Test
+  public void testOAuth2GadgetBinding_1() throws Exception {
+    final OAuth2GadgetBinding result = new OAuth2GadgetBinding("xxx", "yyy", "zzz", false);
+
+    Assert.assertNotNull(result);
+    Assert.assertEquals("zzz", result.getClientName());
+    Assert.assertEquals("yyy", result.getGadgetServiceName());
+    Assert.assertEquals("xxx", result.getGadgetUri());
+    Assert.assertEquals(false, result.isAllowOverride());
+  }
+
+  @Test
+  public void testEquals_1() throws Exception {
+    final Object obj = new OAuth2GadgetBinding(MockUtils.GADGET_URI1, MockUtils.SERVICE_NAME,
+        "xxx", true);
+
+    final boolean result = OAuth2GadgetBindingTest.FIXTURE.equals(obj);
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testEquals_2() throws Exception {
+    final Object obj = new Object();
+
+    final boolean result = OAuth2GadgetBindingTest.FIXTURE.equals(obj);
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testEquals_3() throws Exception {
+    final boolean result = OAuth2GadgetBindingTest.FIXTURE.equals(null);
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testGetClientName_1() throws Exception {
+    final String result = OAuth2GadgetBindingTest.FIXTURE.getClientName();
+
+    Assert.assertEquals("xxx", result);
+  }
+
+  @Test
+  public void testGetGadgetServiceName_1() throws Exception {
+    final String result = OAuth2GadgetBindingTest.FIXTURE.getGadgetServiceName();
+
+    Assert.assertEquals(MockUtils.SERVICE_NAME, result);
+  }
+
+  @Test
+  public void testGetGadgetUri_1() throws Exception {
+    final String result = OAuth2GadgetBindingTest.FIXTURE.getGadgetUri();
+
+    Assert.assertEquals(MockUtils.GADGET_URI1, result);
+  }
+
+  @Test
+  public void testHashCode_1() throws Exception {
+    final int result = OAuth2GadgetBindingTest.FIXTURE.hashCode();
+
+    Assert.assertEquals(-1901114596, result);
+  }
+
+  @Test
+  public void testIsAllowOverride_1() throws Exception {
+    final boolean result = OAuth2GadgetBindingTest.FIXTURE.isAllowOverride();
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testIsAllowOverride_2() throws Exception {
+    final boolean result = OAuth2GadgetBindingTest.FIXTURE.isAllowOverride();
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testToString_1() throws Exception {
+    final String result = OAuth2GadgetBindingTest.FIXTURE.toString();
+
+    Assert.assertEquals(
+        "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2GadgetBinding: gadgetUri = "
+            + MockUtils.GADGET_URI1 + " , gadgetServiceName = " + MockUtils.SERVICE_NAME
+            + " , allowOverride = true", result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2PersistenceModuleTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2PersistenceModuleTest.java
new file mode 100644
index 0000000..223d5f3
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2PersistenceModuleTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.inject.AbstractModule;
+
+public class OAuth2PersistenceModuleTest {
+  @Test
+  public void testConfigure_1() throws Exception {
+    final OAuth2PersistenceModule fixture = new OAuth2PersistenceModule();
+    Assert.assertNotNull(fixture);
+    Assert.assertTrue(AbstractModule.class.isInstance(fixture));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2ProviderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2ProviderTest.java
new file mode 100644
index 0000000..a678d68
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/oauth2/persistence/sample/OAuth2ProviderTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.oauth2.persistence.sample;
+
+import org.apache.shindig.gadgets.oauth2.OAuth2Message;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OAuth2ProviderTest {
+  private OAuth2Provider PROVIDER;
+
+  @Before
+  public void setUp() {
+    this.PROVIDER = new OAuth2Provider();
+    this.PROVIDER.setAuthorizationHeader(true);
+    this.PROVIDER.setAuthorizationUrl("xxx");
+    this.PROVIDER.setClientAuthenticationType(OAuth2Message.BASIC_AUTH_TYPE);
+    this.PROVIDER.setName("yyy");
+    this.PROVIDER.setTokenUrl("zzz");
+    this.PROVIDER.setUrlParameter(false);
+
+  }
+
+  @Test
+  public void testOAuth2Provider_1() throws Exception {
+    final OAuth2Provider result = new OAuth2Provider();
+    Assert.assertNotNull(result);
+  }
+
+  @Test
+  public void testEquals_1() throws Exception {
+    final OAuth2Provider obj = new OAuth2Provider();
+    obj.setTokenUrl("zzz");
+    obj.setClientAuthenticationType(OAuth2Message.BASIC_AUTH_TYPE);
+    obj.setAuthorizationUrl("xxx");
+    obj.setAuthorizationHeader(true);
+    obj.setName("yyy");
+    obj.setUrlParameter(false);
+
+    final boolean result = this.PROVIDER.equals(obj);
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testEquals_2() throws Exception {
+
+    final Object obj = new Object();
+
+    final boolean result = this.PROVIDER.equals(obj);
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testGetAuthorizationUrl_1() throws Exception {
+    final String result = this.PROVIDER.getAuthorizationUrl();
+
+    Assert.assertEquals("xxx", result);
+  }
+
+  @Test
+  public void testGetClientAuthenticationType_1() throws Exception {
+    final String result = this.PROVIDER.getClientAuthenticationType();
+
+    Assert.assertEquals(OAuth2Message.BASIC_AUTH_TYPE, result);
+  }
+
+  @Test
+  public void testGetName_1() throws Exception {
+    final String result = this.PROVIDER.getName();
+
+    Assert.assertEquals("yyy", result);
+  }
+
+  @Test
+  public void testGetTokenUrl_1() throws Exception {
+    final String result = this.PROVIDER.getTokenUrl();
+
+    Assert.assertEquals("zzz", result);
+  }
+
+  @Test
+  public void testHashCode_1() throws Exception {
+    final int result = this.PROVIDER.hashCode();
+
+    Assert.assertEquals(120153, result);
+  }
+
+  @Test
+  public void testHashCode_2() throws Exception {
+    final OAuth2Provider fixture = new OAuth2Provider();
+    fixture.setTokenUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setAuthorizationUrl("");
+    fixture.setAuthorizationHeader(true);
+    fixture.setName((String) null);
+    fixture.setUrlParameter(true);
+
+    final int result = fixture.hashCode();
+
+    Assert.assertEquals(0, result);
+  }
+
+  @Test
+  public void testIsAuthorizationHeader_1() throws Exception {
+    final boolean result = this.PROVIDER.isAuthorizationHeader();
+
+    Assert.assertEquals(true, result);
+  }
+
+  @Test
+  public void testIsAuthorizationHeader_2() throws Exception {
+    final OAuth2Provider fixture = new OAuth2Provider();
+    fixture.setTokenUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setAuthorizationUrl("");
+    fixture.setAuthorizationHeader(false);
+    fixture.setName("");
+    fixture.setUrlParameter(true);
+
+    final boolean result = fixture.isAuthorizationHeader();
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testIsUrlParameter_1() throws Exception {
+    final boolean result = this.PROVIDER.isUrlParameter();
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testIsUrlParameter_2() throws Exception {
+    final OAuth2Provider fixture = new OAuth2Provider();
+    fixture.setTokenUrl("");
+    fixture.setClientAuthenticationType("");
+    fixture.setAuthorizationUrl("");
+    fixture.setAuthorizationHeader(true);
+    fixture.setName("");
+    fixture.setUrlParameter(false);
+
+    final boolean result = fixture.isUrlParameter();
+
+    Assert.assertEquals(false, result);
+  }
+
+  @Test
+  public void testToString_1() throws Exception {
+    final String result = this.PROVIDER.toString();
+
+    Assert
+        .assertEquals(
+            "org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2Provider: name = yyy , authorizationUrl = xxx , tokenUrl = zzz , clientAuthenticationType = Basic",
+            result);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractParserAndSerializerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractParserAndSerializerTest.java
new file mode 100644
index 0000000..40424a0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractParserAndSerializerTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Base test fixture for HTML parsing and serialization.
+ */
+public abstract class AbstractParserAndSerializerTest extends AbstractParsingTestBase {
+  protected GadgetHtmlParser parser;
+
+  protected abstract GadgetHtmlParser makeParser();
+
+  @Before
+  public void setUp() throws Exception {
+    parser = makeParser();
+  }
+
+  @Test
+  public void docWithDoctype() throws Exception {
+    // Note that doctype is properly retained
+    String content = loadFile("org/apache/shindig/gadgets/parse/test.html");
+    String expected = loadFile("org/apache/shindig/gadgets/parse/test-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void docNoDoctype() throws Exception {
+    // Note that no doctype is properly created when none specified
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-fulldocnodoctype.html");
+    String expected =
+        loadFile("org/apache/shindig/gadgets/parse/test-fulldocnodoctype-expected.html");
+    assertNull(parser.parseDom(content).getDoctype());
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void docStartsWithHeader() throws Exception {
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-startswithcomment.html");
+    String expected =
+        loadFile("org/apache/shindig/gadgets/parse/test-startswithcomment-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void notADocument() throws Exception {
+    // Note that no doctype is injected for fragments
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-fragment.html");
+    String expected = loadFile("org/apache/shindig/gadgets/parse/test-fragment-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void notADocument2() throws Exception {
+    // Note that no doctype is injected for fragments
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-fragment2.html");
+    String expected = loadFile("org/apache/shindig/gadgets/parse/test-fragment2-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void noBody() throws Exception {
+    // Note that no doctype is injected for fragments
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-headnobody.html");
+    String expected = loadFile("org/apache/shindig/gadgets/parse/test-headnobody-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void ampersand() throws Exception {
+    // Note that no doctype is injected for fragments
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-with-ampersands.html");
+    String expected =
+        loadFile("org/apache/shindig/gadgets/parse/test-with-ampersands-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void textBeforeScript() throws Exception {
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-text-before-script.html");
+    String expected =
+        loadFile("org/apache/shindig/gadgets/parse/test-text-before-script-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractParsingTestBase.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractParsingTestBase.java
new file mode 100644
index 0000000..3f605b9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractParsingTestBase.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedList;
+
+import name.fraser.neil.plaintext.diff_match_patch;
+import name.fraser.neil.plaintext.diff_match_patch.Diff;
+import name.fraser.neil.plaintext.diff_match_patch.Operation;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.w3c.dom.Document;
+
+/**
+ * Simple base class providing test helpers for parsing/serializing tests.
+ */
+public abstract class AbstractParsingTestBase {
+  /** The vm line separator */
+  private static final String EOL = System.getProperty("line.separator");
+
+  protected String loadFile(String path) throws IOException {
+    InputStream is = this.getClass().getClassLoader().getResourceAsStream(path);
+    // ENABLE THIS if you have troubles in your IDE loading resources.
+    /*if (is == null) {
+      is = new FileInputStream(new File("/path/to/your/files/" + path));
+    }*/
+    return IOUtils.toString(is);
+  }
+
+  protected void parseAndCompareBalanced(String content, String expected, GadgetHtmlParser parser)
+      throws Exception {
+    Document document = parser.parseDom(content);
+    expected = expected.replace(EOL, "\n");
+    String serialized = HtmlSerialization.serialize(document);
+    assertHtmlEquals(expected, serialized);
+  }
+
+  private void assertHtmlEquals(String expected, String serialized) {
+    // Compute the diff of expected vs. serialized, and disregard constructs that we don't
+    // care about, such as whitespace deltas and differently-computed escape sequences.
+    diff_match_patch dmp = new diff_match_patch();
+    LinkedList<Diff> diffs = dmp.diff_main(expected, serialized);
+    while (!diffs.isEmpty()) {
+      Diff cur = diffs.removeFirst();
+      switch (cur.operation) {
+      case DELETE:
+        if (StringUtils.isBlank(cur.text) || "amp;".equalsIgnoreCase(cur.text)) {
+          continue;
+        }
+        if (diffs.isEmpty()) {
+          // End of the set: assert known failure.
+          assertEquals(expected, serialized);
+        }
+        Diff next = diffs.removeFirst();
+        if (next.operation != Operation.INSERT) {
+          // Next operation isn't a paired insert: assert known failure.
+          assertEquals(expected, serialized);
+        }
+        if (!equivalentEntities(cur.text, next.text) &&
+            !cur.text.equalsIgnoreCase(next.text)) {
+          // Delete/insert pair: fail unless each's text is equivalent
+          // either in terms of case or entity equivalence.
+          assertEquals(expected, serialized);
+        }
+        break;
+      case INSERT:
+        // Assert known failure unless insert is whitespace/blank.
+        if (StringUtils.isBlank(cur.text) || "amp;".equalsIgnoreCase(cur.text)) {
+          continue;
+        }
+        assertEquals(expected, serialized);
+        break;
+      default:
+        // EQUALS: move on.
+        break;
+      }
+    }
+  }
+
+  private boolean equivalentEntities(String prev, String cur) {
+    if (!prev.endsWith(";") && !cur.endsWith(";")) {
+      return false;
+    }
+    String prevEnt = StringEscapeUtils.unescapeHtml4(prev);
+    String curEnt = StringEscapeUtils.unescapeHtml4(cur);
+    return prevEnt.equals(curEnt);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractSocialMarkupHtmlParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractSocialMarkupHtmlParserTest.java
new file mode 100644
index 0000000..ce8944c
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/AbstractSocialMarkupHtmlParserTest.java
@@ -0,0 +1,160 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Strings;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Test for the social markup parser.
+ */
+public abstract class AbstractSocialMarkupHtmlParserTest extends AbstractParsingTestBase {
+  private GadgetHtmlParser parser;
+  private Document document;
+
+  protected abstract GadgetHtmlParser makeParser();
+
+  @Before
+  public void setUp() throws Exception {
+    parser = makeParser();
+
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-socialmarkup.html");
+    document = parser.parseDom(content);
+  }
+
+  @Test
+  public void testSocialData() {
+    // Verify elements are preserved in social data
+    List<Element> scripts = SocialDataTags.getTags(document, SocialDataTags.OSML_DATA_TAG);
+    assertEquals(1, scripts.size());
+
+    NodeList viewerRequests = scripts.get(0).getElementsByTagNameNS(
+        PipelinedData.OPENSOCIAL_NAMESPACE, "ViewerRequest");
+    assertEquals(1, viewerRequests.getLength());
+    Element viewerRequest = (Element) viewerRequests.item(0);
+    assertEquals("viewer", viewerRequest.getAttribute("key"));
+    assertEmpty(viewerRequest);
+  }
+
+  @Test
+  public void testSocialTemplate() {
+    // Verify elements and text content are preserved in social templates
+    List<Element> scripts = SocialDataTags.getTags(document, SocialDataTags.OSML_TEMPLATE_TAG);
+    assertEquals(1, scripts.size());
+
+    assertEquals("template-id", scripts.get(0).getAttribute("id"));
+    assertEquals("template-name", scripts.get(0).getAttribute("name"));
+    assertEquals("template-tag", scripts.get(0).getAttribute("tag"));
+
+    NodeList boldElements = scripts.get(0).getElementsByTagName("b");
+    assertEquals(1, boldElements.getLength());
+    Element boldElement = (Element) boldElements.item(0);
+    assertEquals("Some ${viewer} content", boldElement.getTextContent());
+
+    NodeList osHtmlElements = scripts.get(0).getElementsByTagNameNS(
+        "http://ns.opensocial.org/2008/markup", "Html");
+    assertEquals(1, osHtmlElements.getLength());
+  }
+
+  @Test
+  public void testSocialTemplateSerialization() {
+    String content = HtmlSerialization.serialize(document);
+    assertTrue("Empty elements not preserved as XML inside template",
+        content.contains("<img/>"));
+  }
+
+  @Test
+  public void testJavascript() {
+    // Verify text content is unmodified in javascript blocks
+    List<Element> scripts = SocialDataTags.getTags(document, "script");
+
+    // Remove any OpenSocial-specific nodes.
+    Iterator<Element> scriptIt = scripts.iterator();
+    while (scriptIt.hasNext()) {
+      if (SocialDataTags.isOpenSocialScript(scriptIt.next())) {
+        scriptIt.remove();
+      }
+    }
+
+    assertEquals(1, scripts.size());
+
+    NodeList boldElements = scripts.get(0).getElementsByTagName("b");
+    assertEquals(0, boldElements.getLength());
+
+    String scriptContent = scripts.get(0).getTextContent().trim();
+    assertEquals("<b>Some ${viewer} content</b>", scriptContent);
+  }
+
+  @Test
+  public void testPlainContent() {
+    // Verify text content is preserved in non-script content
+    NodeList spanElements = document.getElementsByTagName("span");
+    assertEquals(1, spanElements.getLength());
+    assertEquals("Some content", spanElements.item(0).getTextContent());
+  }
+
+  @Test
+  public void testCommentOrdering() {
+    NodeList divElements = document.getElementsByTagName("div");
+    assertEquals(1, divElements.getLength());
+    NodeList children = divElements.item(0).getChildNodes();
+    assertEquals(3, children.getLength());
+
+    // Should be comment/text/comment, not comment/comment/text
+    assertEquals(Node.COMMENT_NODE, children.item(0).getNodeType());
+    assertEquals(Node.TEXT_NODE, children.item(1).getNodeType());
+    assertEquals(Node.COMMENT_NODE, children.item(2).getNodeType());
+  }
+
+  @Test
+  public void testInvalid() throws Exception {
+    String content =
+        "<html><div id=\"div_super\" class=\"div_super\" valign:\"middle\"></div></html>";
+    Document doc = parser.parseDom(content);
+
+    // Returns a bare Document with error text in it.
+    Node body = doc.getElementsByTagName("body").item(0);
+
+    assertTrue(body.getTextContent().contains("INVALID_CHARACTER_ERR"));
+    assertTrue(body.getTextContent().contains(
+        "Around ...<div id=\"div_super\" class=\"div_super\"..."));
+    // Verify Serialization:
+    assertTrue(HtmlSerialization.serialize(doc).contains("INVALID_CHARACTER_ERR"));
+  }
+
+  private void assertEmpty(Node n) {
+    if (n.getChildNodes().getLength() != 0) {
+      assertTrue(Strings.isNullOrEmpty(n.getTextContent()) ||
+          StringUtils.isWhitespace(n.getTextContent()));
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/CompactHtmlSerializerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/CompactHtmlSerializerTest.java
new file mode 100644
index 0000000..e62611a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/CompactHtmlSerializerTest.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.inject.Provider;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+/**
+ * Test cases for CompactHtmlSerializer.
+ */
+public abstract class CompactHtmlSerializerTest extends AbstractParsingTestBase {
+
+  protected abstract GadgetHtmlParser makeParser();
+
+  private GadgetHtmlParser full = makeParser();
+
+  @Before
+  public void setUp() throws Exception {
+    full.setSerializerProvider(new Provider<HtmlSerializer>() {
+      public HtmlSerializer get() {
+        return new CompactHtmlSerializer();
+      }
+    });
+  }
+
+  @Test
+  public void whitespaceNotCollapsedInSpecialTags() throws Exception {
+    String content = loadFile(
+        "org/apache/shindig/gadgets/parse/test-with-specialtags.html");
+    String expected = loadFile(
+        "org/apache/shindig/gadgets/parse/test-with-specialtags-expected.html");
+    parseAndCompareBalanced(content, expected, full);
+  }
+
+  @Test
+  public void ieConditionalCommentNotRemoved() throws Exception {
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-with-iecond-comments.html");
+    String expected = loadFile(
+        "org/apache/shindig/gadgets/parse/test-with-iecond-comments-expected.html");
+    parseAndCompareBalanced(content, expected, full);
+  }
+
+  @Test
+  public void specialTagsAreRecognized() {
+    assertSpecialTag("textArea");
+    assertSpecialTag("scrIpt");
+    assertSpecialTag("Style");
+    assertSpecialTag("pRe");
+  }
+
+  private static void assertSpecialTag(String tagName) {
+    assertTrue(tagName + "should be special tag",
+        CompactHtmlSerializer.isSpecialTag(tagName));
+    assertTrue(tagName.toUpperCase() + " should be special tag",
+        CompactHtmlSerializer.isSpecialTag(tagName.toUpperCase()));
+    assertTrue(tagName.toLowerCase() + "should be special tag",
+        CompactHtmlSerializer.isSpecialTag(tagName.toLowerCase()));
+  }
+
+  public void testCollapseHtmlWhitespace() throws IOException {
+    assertCollapsed("abc", "abc");
+    assertCollapsed("abc ", "abc");
+    assertCollapsed(" abc", "abc");
+    assertCollapsed("  abc", "abc");
+    assertCollapsed("abc \r", "abc");
+    assertCollapsed("a\t bc", "a bc");
+    assertCollapsed("a  b\n\r  c", "a b c");
+    assertCollapsed(" \ra \tb  \n c  ", "a b c");
+    assertCollapsed(" \n\t\r ", "");
+  }
+
+  private static void assertCollapsed(String input, String expected) throws IOException {
+    Appendable output = new StringWriter();
+    CompactHtmlSerializer.collapseWhitespace(input, output);
+    assertEquals(expected, output.toString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/DefaultHtmlSerializerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/DefaultHtmlSerializerTest.java
new file mode 100644
index 0000000..626a708
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/DefaultHtmlSerializerTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import static org.junit.Assert.assertEquals;
+
+public class DefaultHtmlSerializerTest {
+
+  @Test
+  public void testComplicatedSerialize() throws Exception {
+    String txt = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\""
+            + " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"
+            + "<html xml:lang=\"en\" xmlns=\"http://www.w3.org/1999/xhtml\">"
+            + "<head><title>Apache Shindig!</title></head>"
+            + "<body class=\"composite\">\n"
+            + "    <div id=\"bodyColumn\">hello\n"
+            + "      <div id=\"contentBox\"></div> \n"
+            + "      <div class=\"clear\"><hr></div> \n"
+            + "    </div>\n"
+            +   "</body></html>";
+    NekoSimplifiedHtmlParser parser = new NekoSimplifiedHtmlParser(
+        new ParseModule.DOMImplementationProvider().get());
+
+    Document doc = parser.parseDom(txt);
+    DefaultHtmlSerializer serializer = new DefaultHtmlSerializer();
+    assertEquals("Serialized full document", txt, serializer.serialize(doc));
+  }
+
+  @Test
+  public void testComments() throws Exception {
+
+    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+    doc.appendChild(doc.createElement("ABC"));
+    doc.appendChild(doc.createComment("XYZ"));
+
+    DefaultHtmlSerializer serializer = new DefaultHtmlSerializer();
+    assertEquals("Comment is preserved",
+        "<ABC></ABC><!--XYZ-->", serializer.serialize(doc));
+  }
+
+  @Test
+  public void testEntities() throws Exception {
+    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+
+    Element element = doc.createElement("abc");
+    element.setAttribute("a", "\\x3e\">");
+    doc.appendChild(element);
+
+    DefaultHtmlSerializer serializer = new DefaultHtmlSerializer();
+    assertEquals("Entities escaped",
+        "<abc a=\"\\x3e&#34;&gt;\"></abc>", serializer.serialize(doc));
+  }
+
+  @Test
+  public void testHrefEntities() throws Exception {
+    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+
+    Element element = doc.createElement("a");
+    element.setAttribute("href", "http://apache.org/?a=0&query=2+3");
+    doc.appendChild(element);
+
+    DefaultHtmlSerializer serializer = new DefaultHtmlSerializer();
+    assertEquals("href entities escaped",
+        "<a href=\"http://apache.org/?a=0&amp;query=2+3\"></a>",
+        serializer.serialize(doc));
+  }
+
+  @Test
+  public void testDataTemplateTags() throws Exception {
+    Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+
+    Element element = doc.createElement("osdata");
+    element.setAttribute("xmlns:foo", "#foo");
+    doc.appendChild(element);
+
+    DefaultHtmlSerializer serializer = new DefaultHtmlSerializer();
+    assertEquals("OSData normalized",
+        "<script type=\"text/os-data\" xmlns:foo=\"#foo\"></script>",
+        serializer.serialize(doc));
+  }
+}
+
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/HtmlParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/HtmlParserTest.java
new file mode 100644
index 0000000..fdbd783
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/HtmlParserTest.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.rewrite.XPathWrapper;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+/**
+ * Note these tests are of marginal use. Consider removing. More useful tests would exercise
+ * the capability of the parser to handle strange HTML.
+ */
+public class HtmlParserTest extends Assert {
+
+  private final GadgetHtmlParser nekoParser = new NekoSimplifiedHtmlParser(
+      new ParseModule.DOMImplementationProvider().get());
+
+  @Test
+  public void testParseSimpleString() throws Exception {
+    parseSimpleString(nekoParser);
+  }
+
+  private void parseSimpleString(GadgetHtmlParser htmlParser) throws Exception {
+    Document doc = htmlParser.parseDom("content");
+    XPathWrapper wrapper = new XPathWrapper(doc);
+    assertEquals("content", wrapper.getValue("/html/body"));
+  }
+
+  @Test
+  public void testParseTagWithStringContents() throws Exception {
+    parseTagWithStringContents(nekoParser);
+  }
+
+  void parseTagWithStringContents(GadgetHtmlParser htmlParser) throws Exception {
+    Document doc = htmlParser.parseDom("<span>content</span>");
+    XPathWrapper wrapper = new XPathWrapper(doc);
+    assertEquals("content", wrapper.getValue("/html/body/span"));
+  }
+
+  @Test
+  public void testParseTagWithAttributes() throws Exception {
+    parseTagWithAttributes(nekoParser);
+  }
+
+  void parseTagWithAttributes(GadgetHtmlParser htmlParser) throws Exception {
+    Document doc = htmlParser.parseDom("<div id=\"foo\">content</div>");
+    XPathWrapper wrapper = new XPathWrapper(doc);
+    assertEquals("content", wrapper.getValue("/html/body/div"));
+    assertEquals("foo", wrapper.getValue("/html/body/div/@id"));
+  }
+
+  @Test
+  public void testParseNestedContentWithNoCloseForBrAndHr() throws Exception {
+    parseNestedContentWithNoCloseForBrAndHr(nekoParser);
+  }
+
+  void parseNestedContentWithNoCloseForBrAndHr(GadgetHtmlParser htmlParser) throws Exception {
+    Document doc = htmlParser.parseDom("<div>x and y<br> and <hr>z</div>");
+    XPathWrapper wrapper = new XPathWrapper(doc);
+    assertEquals("x and y and z", wrapper.getValue("/html/body/div"));
+    assertEquals(1, wrapper.getNodeList("/html/body/div/br").getLength());
+    assertEquals(1, wrapper.getNodeList("/html/body/div/hr").getLength());
+  }
+
+  // TODO: figure out to what extent it makes sense to test "invalid"
+  // HTML, semi-structured HTML, and comment parsing
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/HtmlSerializationTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/HtmlSerializationTest.java
new file mode 100644
index 0000000..ff363d4
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/HtmlSerializationTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import com.google.common.collect.ImmutableList;
+
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.w3c.dom.Document;
+
+import static org.junit.Assert.assertEquals;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.util.List;
+
+public class HtmlSerializationTest {
+  Document doc;
+  List<GadgetHtmlParser> parsers;
+
+  @Before
+  public void setUp() throws Exception {
+    doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
+    GadgetHtmlParser neko = new NekoSimplifiedHtmlParser(
+            new ParseModule.DOMImplementationProvider().get());
+
+    GadgetHtmlParser caja = new CajaHtmlParser(
+            new ParseModule.DOMImplementationProvider().get());
+
+    parsers = ImmutableList.of(neko, caja);
+  }
+
+  @Test
+  @Ignore("Caja parses OS script tags but does not serialize them to their original form")
+  public void testSerialize() throws Exception {
+    String markup = "<!DOCTYPE html>\n"
+        + "<html><head><title>Apache Shindig!</title></head>"
+        + "<body>"
+        + "<script type=\"text/os-data\" xmlns:os=\"http://ns.opensocial.org/2008/markup\">"
+        + "  <os:PeopleRequest groupId=\"@friends\" key=\"friends\" userId=\"@viewer\"></os:PeopleRequest>\n"
+        + "</script>"
+        + "<script require=\"friends\" type=\"text/os-template\">\n"
+        + "  <ul><li repeat=\"${friends}\">\n"
+        + "    <span id=\"id${Context.Index}\">${Cur.name.givenName}</span>\n"
+        + "  </li></ul>"
+        + "</script>"
+        + "</body></html>";
+
+    for(GadgetHtmlParser parser : parsers) {
+      Document doc = parser.parseDom(markup);
+      String result = HtmlSerialization.serialize(doc);
+      assertEquals(markup, result);
+    }
+  }
+
+  @Test
+  public void testSerializeHtml() throws Exception {
+    String markup = "<!DOCTYPE html>\n"
+        + "<html><head><title>Apache Shindig!</title></head>"
+        + "<body>"
+        + "<div xmlns:osx=\"http://ns.opensocial.org/2008/extensions\">"
+        + "<osx:NavigateToApp>\n"
+        + "<img border=\"0\" src=\"foo.gif\">\n"
+        + "</osx:NavigateToApp>\n"
+        + "</div>"
+        + "</body></html>";
+
+    for(GadgetHtmlParser parser : parsers) {
+      Document doc = parser.parseDom(markup);
+      String result = HtmlSerialization.serialize(doc);
+      assertEquals(markup, result);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/ParseTreeSerializerBenchmark.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/ParseTreeSerializerBenchmark.java
new file mode 100644
index 0000000..c19ccdd
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/ParseTreeSerializerBenchmark.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+
+import org.w3c.dom.DOMImplementation;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.StringWriter;
+
+/**
+ * Benchmarks for HTML parsing and serialization
+ */
+public class ParseTreeSerializerBenchmark {
+  private int numRuns;
+  private String content;
+
+  private GadgetHtmlParser nekoSimpleParser = new NekoSimplifiedHtmlParser(
+      DOCUMENT_PROVIDER);
+
+  private GadgetHtmlParser cajaParser = new CajaHtmlParser(
+      DOCUMENT_PROVIDER);
+
+
+  private boolean warmup;
+
+  private static final DOMImplementation DOCUMENT_PROVIDER =
+      new ParseModule.DOMImplementationProvider().get();
+
+  private ParseTreeSerializerBenchmark(String file, int numRuns) throws Exception {
+    File inputFile = new File(file);
+    if (!inputFile.exists() || !inputFile.canRead()) {
+      System.err.println("Input file: " + file + " not found or can't be read.");
+      System.exit(1);
+    }
+    content = new String(IOUtils.toByteArray(new FileInputStream(file)));
+
+    this.numRuns = 10;
+    warmup = true;
+    runCaja();
+    runNekoSimple();
+
+    //Sleep to let JIT kick in
+    Thread.sleep(10000L);
+    this.numRuns = numRuns;
+    warmup = false;
+    runCaja();
+    runNekoSimple();
+  }
+
+  private void runNekoSimple() throws Exception {
+    output("NekoSimple-----------------");
+    timeParseDom(nekoSimpleParser);
+    timeParseDomSerialize(nekoSimpleParser);
+  }
+
+
+  private void runCaja() throws Exception {
+    output("Caja-----------------");
+    timeParseDom(cajaParser);
+    timeParseDomSerialize(cajaParser);
+  }
+
+  private void output(String string) {
+    if (!warmup) {
+      System.out.println(string);
+    }
+  }
+
+  private void timeParseDom(GadgetHtmlParser parser) throws GadgetException {
+    long parseStart = System.currentTimeMillis();
+    for (int i = 0; i < numRuns; ++i) {
+      parser.parseDom(content);
+    }
+    long parseMillis = System.currentTimeMillis() - parseStart;
+
+    output("Parsing W3C DOM [" + parseMillis + " ms total: " +
+          ((double)parseMillis)/numRuns + "ms/run]");
+  }
+
+  private void timeParseDomSerialize(GadgetHtmlParser parser) throws GadgetException {
+    org.w3c.dom.Document document = parser.parseDom(content);
+    try {
+      long parseStart = System.currentTimeMillis();
+      for (int i = 0; i < numRuns; ++i) {
+        HtmlSerialization.serialize(document);
+      }
+      long parseMillis = System.currentTimeMillis() - parseStart;
+
+      output("Serializing [" + parseMillis + " ms total: " +
+            ((double) parseMillis) / numRuns + "ms/run]");
+    } catch (Exception e) {
+      throw new GadgetException(GadgetException.Code.HTML_PARSE_ERROR, e);
+    }
+
+    try {
+      // Create an "identity" transformer - copies input to output
+      Transformer t = TransformerFactory.newInstance().newTransformer();
+      t.setOutputProperty(OutputKeys.METHOD, "html");
+
+      long parseStart = System.currentTimeMillis();
+      for (int i = 0; i < numRuns; ++i) {
+        StringWriter sw = new StringWriter((content.length() * 11) / 10);
+        t.transform(new DOMSource(document), new StreamResult(sw));
+        sw.toString();
+      }
+      long parseMillis = System.currentTimeMillis() - parseStart;
+
+      output("Serializing DOM Transformer [" + parseMillis + " ms total: " +
+            ((double) parseMillis) / numRuns + "ms/run]");
+
+    } catch (Exception e) {
+      throw new GadgetException(GadgetException.Code.HTML_PARSE_ERROR, e);
+    }
+  }
+
+  public static void main(String[] args) {
+    // Test can be run as standalone program to test out serialization and parsing
+    // performance numbers, using Caja as a parser.
+    if (args.length != 2) {
+      System.err.println("Args: <input-file> <num-runs>");
+      System.exit(1);
+    }
+
+    String fileArg = args[0];
+    String runsArg = args[1];
+    int numRuns = -1;
+    try {
+      numRuns = Integer.parseInt(runsArg);
+    } catch (Exception e) {
+      System.err.println("Invalid num-runs argument: " + runsArg + ", reason: " + e);
+    }
+    try {
+      new ParseTreeSerializerBenchmark(fileArg, numRuns);
+    } catch (Exception e) {
+      e.printStackTrace();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCompactHtmlSerializerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCompactHtmlSerializerTest.java
new file mode 100644
index 0000000..8f864f6
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCompactHtmlSerializerTest.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.gadgets.parse.CompactHtmlSerializerTest;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+
+public class CajaCompactHtmlSerializerTest extends CompactHtmlSerializerTest {
+
+  @Override
+  protected GadgetHtmlParser makeParser() {
+    return new CajaHtmlParser(new ParseModule.DOMImplementationProvider().get());
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssLexerParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssLexerParserTest.java
new file mode 100644
index 0000000..72bab0c
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssLexerParserTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.common.cache.LruCacheProvider;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Basic test of CSS lexer
+ */
+public class CajaCssLexerParserTest extends Assert {
+
+  private CajaCssLexerParser cajaCssParser;
+
+  private static final String CSS = "@import url('www.example.org/someother.css');\n" +
+      ".xyz { background-image : url(http://www.example.org/someimage.gif); }\n" +
+      "A { color : #7f7f7f }\n";
+
+  @Before
+  public void setUp() throws Exception {
+    cajaCssParser = new CajaCssLexerParser();
+  }
+
+  @Test
+  public void testBasicCssParse() throws Exception {
+    String css = ".xyz { font : bold; } A { color : #7f7f7f }";
+    List<Object> styleSheet = cajaCssParser.parse(css);
+    assertEquals(cajaCssParser.serialize(styleSheet), css);
+  }
+
+  @Test
+  public void testClone() throws Exception {
+    // Set the cache so we force cloning
+    cajaCssParser.setCacheProvider(new LruCacheProvider(100));
+
+    // Compare the raw parsed structure to a cloned one
+    List<Object> styleSheet = cajaCssParser.parseImpl(CSS);
+    List<Object> styleSheet2 = cajaCssParser.parse(CSS);
+    assertEquals(cajaCssParser.serialize(styleSheet), cajaCssParser.serialize(styleSheet2));
+  }
+
+  @Test
+  public void testCache() throws Exception {
+    cajaCssParser.setCacheProvider(new LruCacheProvider(100));
+    // Ensure that we return cloned instances and not the original out of the cache. Cloned
+    // instances intentionally do not compare equal but should produce the same output
+    List<Object> styleSheet = cajaCssParser.parse(CSS);
+    List<Object> styleSheet2 = cajaCssParser.parse(CSS);
+    assertFalse(styleSheet.equals(styleSheet2));
+    assertEquals(cajaCssParser.serialize(styleSheet), cajaCssParser.serialize(styleSheet2));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssParserTest.java
new file mode 100644
index 0000000..c26d719
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssParserTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import com.google.caja.parser.css.CssTree;
+
+import org.apache.shindig.common.cache.LruCacheProvider;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Basic CSS parse tests
+ */
+public class CajaCssParserTest extends Assert {
+
+  private CajaCssParser cajaCssParser;
+
+  @Before
+  public void setUp() throws Exception {
+    cajaCssParser = new CajaCssParser();
+    cajaCssParser.setCacheProvider(new LruCacheProvider(10));
+  }
+
+  @Test
+  public void testBasicCssParse() throws Exception {
+    String css = ".xyz { font : bold; } A { color : #7f7f7f }";
+    CssTree.StyleSheet styleSheet = cajaCssParser.parseDom(css);
+    List<CssTree.SimpleSelector> selectorList = CajaCssUtils.descendants(styleSheet,
+        CssTree.SimpleSelector.class);
+    assertEquals(2, selectorList.size());
+    assertSame(CssTree.SimpleSelector.class, selectorList.get(0).getClass());
+  }
+
+  /**
+   * These tests test Caja's parsing of "funky" CSS which are not legal
+   * but accepted by commonly used browsers
+   */
+  @Test
+  public void testCajaParseColonInRValue() throws Exception {
+    String original = " A {\n"
+        + " -moz-opacity: 0.80;\n"
+        + " filter: alpha(opacity=40);\n"
+        + " filter: progid:DXImageTransform.Microsoft.Alpha(opacity=80);\n"
+        + '}';
+    CssTree.StyleSheet styleSheet = cajaCssParser.parseDom(original);
+    List<CssTree.SimpleSelector> selectorList = CajaCssUtils.descendants(
+        styleSheet, CssTree.SimpleSelector.class);
+    assertEquals(1, selectorList.size());
+    assertSame(CssTree.SimpleSelector.class, selectorList.get(0).getClass());
+  }
+
+  @Test
+  public void testCajaParseNoLValue() throws Exception {
+    String original = "body, input, td {\n"
+        + "  Arial, sans-serif;\n"
+        + '}';
+    cajaCssParser.parseDom(original);
+    CssTree.StyleSheet styleSheet = cajaCssParser.parseDom(original);
+    List<CssTree.SimpleSelector> selectorList = CajaCssUtils.descendants(
+        styleSheet, CssTree.SimpleSelector.class);
+    assertEquals(3, selectorList.size());
+    assertSame(CssTree.SimpleSelector.class, selectorList.get(0).getClass());
+  }
+
+  @Test
+  public void testCajaParseNoScheme() throws Exception {
+    String original = "span { background-image:url('//www.example.org/image.gif'); }";
+    cajaCssParser.parseDom(original);
+    CssTree.StyleSheet styleSheet = cajaCssParser.parseDom(original);
+    List<CssTree.SimpleSelector> selectorList = CajaCssUtils.descendants(
+        styleSheet, CssTree.SimpleSelector.class);
+
+    // TODO: Remove with next caja update
+    // This will break once Caja cloning works again
+    assertEquals(1, selectorList.size());
+    // assertEquals(3, selectorList.size());
+    assertSame(CssTree.SimpleSelector.class, selectorList.get(0).getClass());
+  }
+
+  @Test
+  public void testCajaParseCommentInContent() throws Exception {
+    String original = "body { font : bold; } \n//A comment\n A { font : bold; }";
+    cajaCssParser.parseDom(original);
+    CssTree.StyleSheet styleSheet = cajaCssParser.parseDom(original);
+    List<CssTree.SimpleSelector> selectorList = CajaCssUtils.descendants(
+        styleSheet, CssTree.SimpleSelector.class);
+    assertEquals(2, selectorList.size());
+    assertSame(CssTree.SimpleSelector.class, selectorList.get(0).getClass());
+  }
+
+  @Test
+  public void testCajaParseDotInIdent() throws Exception {
+    String original = "li{list-style:none;.padding-bottom:4px;}";
+    cajaCssParser.parseDom(original);
+    CssTree.StyleSheet styleSheet = cajaCssParser.parseDom(original);
+    List<CssTree.SimpleSelector> selectorList = CajaCssUtils.descendants(
+        styleSheet, CssTree.SimpleSelector.class);
+    assertEquals(1, selectorList.size());
+    assertSame(CssTree.SimpleSelector.class, selectorList.get(0).getClass());
+  }
+
+  @Test
+  public void testCajaParseDotInFunction() throws Exception {
+    String original = ".iepngfix {behavior: expression(IEPNGFIX.fix(this)); }";
+    cajaCssParser.parseDom(original);
+    CssTree.StyleSheet styleSheet = cajaCssParser.parseDom(original);
+    List<CssTree.SimpleSelector> selectorList = CajaCssUtils.descendants(
+        styleSheet, CssTree.SimpleSelector.class);
+    assertEquals(1, selectorList.size());
+    assertSame(CssTree.SimpleSelector.class, selectorList.get(0).getClass());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java
new file mode 100644
index 0000000..c346474
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaCssSanitizerTest.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import com.google.caja.parser.css.CssTree;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.render.SanitizingProxyUriManager;
+import org.apache.shindig.gadgets.uri.DefaultProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for CajaCssSanitizer.
+ */
+public class CajaCssSanitizerTest extends EasyMockTestCase {
+  private CajaCssParser parser;
+  private CajaCssSanitizer sanitizer;
+  private final Uri DUMMY = Uri.parse("http://www.example.org/base");
+  private SanitizingProxyUriManager importRewriter;
+  private SanitizingProxyUriManager imageRewriter;
+  private GadgetContext gadgetContext;
+  public static final String MOCK_CONTAINER = "mockContainer";
+  private static final ImmutableMap<String, Object> DEFAULT_CONTAINER_CONFIG = ImmutableMap
+      .<String, Object>builder()
+      .put(ContainerConfig.CONTAINER_KEY, ImmutableList.of("default"))
+      .put(DefaultProxyUriManager.PROXY_HOST_PARAM, "www.test.com")
+      .put(DefaultProxyUriManager.PROXY_PATH_PARAM, "/dir/proxy")
+      .build();
+  private static final ImmutableMap<String, Object> MOCK_CONTAINER_CONFIG = ImmutableMap
+      .<String, Object>builder()
+      .put(ContainerConfig.CONTAINER_KEY, ImmutableList.of(MOCK_CONTAINER))
+      .put(DefaultProxyUriManager.PROXY_HOST_PARAM, "www.mock.com")
+      .build();
+
+  @Before
+  public void setUp() throws Exception {
+    parser = new CajaCssParser();
+    sanitizer = new CajaCssSanitizer(parser);
+
+    ContainerConfig config = new BasicContainerConfig();
+    config.newTransaction().addContainer(DEFAULT_CONTAINER_CONFIG).addContainer(MOCK_CONTAINER_CONFIG).commit();
+    ProxyUriManager proxyUriManager = new DefaultProxyUriManager(config, null);
+
+    importRewriter = new SanitizingProxyUriManager(proxyUriManager, "text/css");
+    imageRewriter = new SanitizingProxyUriManager(proxyUriManager, "image/*");
+    gadgetContext = new GadgetContext() {
+      @Override
+      public String getContainer() {
+        return MOCK_CONTAINER;
+      }
+    };
+  }
+
+  @Test
+  public void testPreserveSafe() throws Exception {
+    String css = ".xyz { font: bold;} A { color: #7f7f7f}";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertStyleEquals(css, styleSheet);
+  }
+
+  @Test
+  public void testSanitizeFunctionCall() throws Exception {
+    String css = ".xyz { font : iamevil(bold); }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz {}", styleSheet);
+  }
+
+  @Test
+  public void testSanitizeBadField() throws Exception {
+    String css = ".xyz { iamevil: 1; }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz {}", styleSheet);
+  }
+
+  @Test
+  public void testSanitizeCleanToParent() throws Exception {
+    String css = ".q_action:hover, #questionsDIV li:nth-child(even) .q_action:hover, .stream li:nth-child(even) .q_action:hover {" +
+    		" background: #d0ebfe; text-decoration: none; }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertStyleEquals(css, styleSheet);
+  }
+
+  @Test
+   public void testSanitizeUnsafeProperties() throws Exception {
+    String css = ".xyz { behavior: url('xyz.htc'); -moz-binding:url(\"http://ha.ckers.org/xssmoz.xml#xss\") }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz {}", styleSheet);
+  }
+
+  @Test
+  public void testSanitizeScriptUrls() throws Exception {
+    String css = ".xyz { background: url('javascript:doevill'); background : url(vbscript:moreevil); }";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz {}", styleSheet);
+  }
+
+  @Test
+  public void testProxyUrls() throws Exception {
+    String css = ".xyz { background: url('http://www.example.org/img.gif');}";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertStyleEquals(".xyz { " +
+        "background: url('//www.mock.com/dir/proxy?container=mockContainer&gadget=http%3A%2F%2Fwww.example.org%2Fbase" +
+        "&debug=0&nocache=0&rewriteMime=image%2F%2a&sanitize=1&" +
+        "url=http%3A%2F%2Fwww.example.org%2Fimg.gif');}", styleSheet);
+  }
+
+  @Test
+  public void testUrlEscapingMockContainer() throws Exception {
+    String css = ".xyz { background: url('http://www.example.org/img.gif');}";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertEquals(".xyz{" +
+        "background:url('//www.mock.com/dir/proxy?container=mockContainer&gadget=http%3A%2F%2Fwww.example.org%2Fbase" +
+        "&debug=0&nocache=0&rewriteMime=image%2F%2a&sanitize=1" +
+        "&url=http%3A%2F%2Fwww.example.org%2Fimg.gif');}",
+        parser.serialize(styleSheet).replaceAll("\\s", ""));
+  }
+
+  @Test
+  public void testUrlEscapingDefaultContainer() throws Exception {
+    String css = ".xyz { background: url('http://www.example.org/img.gif');}";
+    CssTree.StyleSheet styleSheet = parser.parseDom(css);
+    GadgetContext gadgetContext = new GadgetContext() {
+      @Override
+      public String getContainer() {
+        return ContainerConfig.DEFAULT_CONTAINER;
+      }
+    };
+
+    sanitizer.sanitize(styleSheet, DUMMY, gadgetContext, importRewriter, imageRewriter);
+    assertEquals(".xyz{" +
+        "background:url('//www.test.com/dir/proxy?container=default&gadget=http%3A%2F%2Fwww.example.org%2Fbase" +
+        "&debug=0&nocache=0&rewriteMime=image%2F%2a&sanitize=1" +
+        "&url=http%3A%2F%2Fwww.example.org%2Fimg.gif');}",
+        parser.serialize(styleSheet).replaceAll("\\s", ""));
+  }
+
+  public void assertStyleEquals(String expected, CssTree.StyleSheet styleSheet) throws Exception {
+    assertEquals(parser.serialize(parser.parseDom(expected)), parser.serialize(styleSheet));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaParserAndSerializerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaParserAndSerializerTest.java
new file mode 100644
index 0000000..629d15b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaParserAndSerializerTest.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.gadgets.parse.AbstractParserAndSerializerTest;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.junit.Test;
+
+public class CajaParserAndSerializerTest extends AbstractParserAndSerializerTest {
+
+  @Override
+  protected GadgetHtmlParser makeParser() {
+    return new CajaHtmlParser(new ParseModule.DOMImplementationProvider().get());
+  }
+
+  @Override
+  @Test
+  public void docStartsWithHeader() throws Exception {
+    // TODO: fix Caja to handle this case!
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaSocialMarkupHtmlParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaSocialMarkupHtmlParserTest.java
new file mode 100644
index 0000000..7cda08b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/CajaSocialMarkupHtmlParserTest.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.gadgets.parse.AbstractSocialMarkupHtmlParserTest;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class CajaSocialMarkupHtmlParserTest extends AbstractSocialMarkupHtmlParserTest {
+
+  @Override
+  protected GadgetHtmlParser makeParser() {
+    return new CajaHtmlParser(new ParseModule.DOMImplementationProvider().get());
+  }
+
+  @Test
+  @Override
+  @Ignore("Look into treating DOMException in Caja parser as Error not Warning")
+  public void testInvalid() throws Exception { super.testInvalid(); }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlParserTest.java
new file mode 100644
index 0000000..2568f6d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlParserTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.apache.shindig.gadgets.GadgetException;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.bootstrap.DOMImplementationRegistry;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for VanillaCajaHtmlParser.
+ */
+public class VanillaCajaHtmlParserTest {
+  private VanillaCajaHtmlParser parser;
+  private VanillaCajaHtmlSerializer serializer;
+
+  @Before
+  public void setUp() throws Exception {
+    DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
+    // Require the traversal API
+    DOMImplementation domImpl = registry.getDOMImplementation("XML 1.0 Traversal 2.0");
+    parser = new VanillaCajaHtmlParser(domImpl, true);
+    serializer = new VanillaCajaHtmlSerializer();
+  }
+
+  @Ignore
+  @Test(expected = GadgetException.class)
+  public void testEmptyDocument() throws Exception {
+    boolean exceptionCaught = false;
+    parser.parseDom("");
+  }
+
+  // Bad behavior by Caja DomParser. Bug to be raised with Caja team.
+  // Caja should not parse such javascript as html. Ideally it should throw an
+  // exception indicating non html content.
+  // TODO: Update test case when the issue is fixed.
+  @Test
+  public void testNonHtml() throws Exception {
+    String html = "var hello=\"world\";";
+    String expected = "<html><head></head><body>var hello=&#34;world&#34;;"
+                      + "</body></html>";
+    assertEquals(expected, serializer.serialize(parser.parseDom(html)));
+  }
+
+  @Test
+  public void testNoHead() throws Exception {
+    String html = "<html><body><a href=\"hello\"></a></body></html>";
+    String expected = "<html><head></head><body><a href=\"hello\"></a>"
+                      + "</body></html>";
+    assertEquals(expected, serializer.serialize(parser.parseDom(html)));
+  }
+
+  @Test
+  public void testParseAndSerialize() throws Exception {
+    String html = "<html><head><script src=\"1.js\"></script></head>"
+                  + "<body><a href=\"hello\"></a></body></html>";
+    String expected = "<html><head><script src=\"1.js\"></script></head>"
+                      + "<body><a href=\"hello\"></a>"
+                      + "</body></html>";
+    assertEquals(expected, serializer.serialize(parser.parseDom(html)));
+  }
+
+  @Test
+  public void testUnbalanced() throws Exception {
+    String html = "<html><head><script src=\"1.js\"></script></head>"
+                  + "<body><p><embed></p></embed></body></html>";
+    String expected = "<html><head><script src=\"1.js\"></script></head>"
+                      + "<body><p><embed /></p>"
+                      + "</body></html>";
+    assertEquals(expected, serializer.serialize(parser.parseDom(html)));
+  }
+
+  // Weird case of normalization. Chrome and Firefox do not seem to execute the
+  // script since there is no closing </script> tag. Hence Caja is consistent
+  // with modern browsers.
+  @Test
+  public void testBadTagBalancing() throws Exception {
+    String html = "<html><head><script src=\"1.js\"></head>"
+                  + "<body></body></html>";
+    String expected = "<html><head><script src=\"1.js\">"
+                      + "</head><body></body></html>"
+                      + "</script></head><body></body></html>";
+    assertEquals(expected, serializer.serialize(parser.parseDom(html)));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlSerializerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlSerializerTest.java
new file mode 100644
index 0000000..496e0d0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/caja/VanillaCajaHtmlSerializerTest.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.caja;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.bootstrap.DOMImplementationRegistry;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for VanillaCajaHtmlSerializer.
+ */
+public class VanillaCajaHtmlSerializerTest {
+  private VanillaCajaHtmlParser parser;
+  private VanillaCajaHtmlSerializer serializer;
+
+  @Before
+  public void setUp() throws Exception {
+    DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance();
+    // Require the traversal API
+    DOMImplementation domImpl = registry.getDOMImplementation("XML 1.0 Traversal 2.0");
+    parser = new VanillaCajaHtmlParser(domImpl, true);
+    serializer = new VanillaCajaHtmlSerializer();
+  }
+
+  @Test
+  public void testParseAndSerializeNonASCIINotEscaped() throws Exception {
+    String html = "<html><head><script src=\"1.js\"></script></head>"
+                  + "<body><a href=\"hello\">\\u200E\\u200F\\u2010\\u0410</a>"
+                  + "</body></html>";
+    assertEquals(html, serializer.serialize(parser.parseDom(html)));
+  }
+
+  @Test
+  public void testParseAndSerializeCommentsNotRemoved() throws Exception {
+    String html = "<html><head><script src=\"1.js\"></script></head>"
+                  + "<body><div>before <!-- Test Data --> after \n"
+                  + "<!-- [if IE ]>"
+                  + "<link href=\"iecss.css\" rel=\"stylesheet\" type=\"text/css\">"
+                  + "<![endif]-->"
+                  + "</div></body></html>";
+    // If we run the serializer with wantsComments set to false, all comments are removed from the
+    // serialized html and the output is:
+    // "<html><head><script src=\"1.js\"></script></head>"
+    // + "<body><div>"
+    // + "</div></body></html>"
+    assertEquals(html, serializer.serialize(parser.parseDom(html)));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/NekoCompactHtmlSerializerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/NekoCompactHtmlSerializerTest.java
new file mode 100644
index 0000000..fa76d2d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/NekoCompactHtmlSerializerTest.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.nekohtml;
+
+import org.apache.shindig.gadgets.parse.CompactHtmlSerializerTest;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+
+/**
+ * Compact HTML serializer test using the Neko parser implementation.
+ */
+public class NekoCompactHtmlSerializerTest extends CompactHtmlSerializerTest {
+
+  @Override
+  protected GadgetHtmlParser makeParser() {
+    return new NekoSimplifiedHtmlParser(
+        new ParseModule.DOMImplementationProvider().get());
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/NekoParserAndSerializeTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/NekoParserAndSerializeTest.java
new file mode 100644
index 0000000..5a4fe25
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/NekoParserAndSerializeTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.nekohtml;
+
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.gadgets.parse.AbstractParserAndSerializerTest;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.junit.Test;
+
+/**
+ * Test behavior of neko based parser and serializers
+ */
+public class NekoParserAndSerializeTest extends AbstractParserAndSerializerTest {
+  @Override
+  protected GadgetHtmlParser makeParser() {
+    return new NekoSimplifiedHtmlParser(
+        new ParseModule.DOMImplementationProvider().get());
+  }
+
+  // Neko-specific tests.
+  @Test
+  public void scriptPushedToBody() throws Exception {
+    String content = loadFile("org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript.html");
+    String expected =
+        loadFile("org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  // Neko overridden tests (due to Neko quirks)
+  @Override
+  @Test
+  public void notADocument() throws Exception {
+    // Note that no doctype is injected for fragments
+    String content = loadFile("org/apache/shindig/gadgets/parse/nekohtml/test-fragment.html");
+    String expected = loadFile("org/apache/shindig/gadgets/parse/nekohtml/test-fragment-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Override
+  @Test
+  public void noBody() throws Exception {
+    // Note that no doctype is injected for fragments
+    String content = loadFile("org/apache/shindig/gadgets/parse/nekohtml/test-headnobody.html");
+    String expected = loadFile("org/apache/shindig/gadgets/parse/nekohtml/test-headnobody-expected.html");
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  // Overridden because of comment vs. script ordering. Neko stuffs script into head, but
+  // postprocessing moves it back down into body, *above* the comment element. This is
+  // semantically meaningless (to HTML), so we create a new test to accommodate it.
+  @Override
+  @Test
+  public void docNoDoctype() throws Exception {
+    // Note that no doctype is properly created when none specified
+    String content = loadFile("org/apache/shindig/gadgets/parse/test-fulldocnodoctype.html");
+    String expected =
+        loadFile("org/apache/shindig/gadgets/parse/nekohtml/test-fulldocnodoctype-expected.html");
+    assertNull(parser.parseDom(content).getDoctype());
+    parseAndCompareBalanced(content, expected, parser);
+  }
+
+  @Test
+  public void textBeforeScript() throws Exception {
+    // Doesn't work in "native" form due to Neko's internals. Upon finding first text, then a
+    // <script> node, Neko discards the text. To fix this, we would have to either dive into
+    // Neko's internals, which could change underneath us, or do some overly complicated and
+    // costly dual-parsing pass, to detect which "early" elements have been discarded from
+    // the document by Neko. These use cases are marginal at best, and Caja's parser does not
+    // exhibit this behavior, so we don't do so.
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParserTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParserTest.java
new file mode 100644
index 0000000..486eea7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/parse/nekohtml/SocialMarkupHtmlParserTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.parse.nekohtml;
+
+import org.apache.shindig.gadgets.parse.AbstractSocialMarkupHtmlParserTest;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+
+/**
+ * Test for the social markup parser.
+ */
+public class SocialMarkupHtmlParserTest extends AbstractSocialMarkupHtmlParserTest {
+  @Override
+  protected GadgetHtmlParser makeParser() {
+    return new NekoSimplifiedHtmlParser(new ParseModule.DOMImplementationProvider().get());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderServiceTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderServiceTest.java
new file mode 100644
index 0000000..3d71ec2
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/ConcurrentPreloaderServiceTest.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import org.apache.shindig.common.testing.ImmediateExecutorService;
+import org.apache.shindig.gadgets.Gadget;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+/**
+ * Tests for FuturePreloaderService.
+ */
+public class ConcurrentPreloaderServiceTest {
+  private static final String PRELOAD_STRING_KEY = "key a";
+  private static final String PRELOAD_NUMERIC_KEY = "key b";
+  private static final String PRELOAD_MAP_KEY = "key c";
+  private static final String PRELOAD_STRING_VALUE = "Some random string";
+  private static final Integer PRELOAD_NUMERIC_VALUE = 5;
+  private static final Map<String, String> PRELOAD_MAP_VALUE
+      = ImmutableMap.of("foo", "bar", "baz", "blah");
+
+  private final TestPreloader preloader = new TestPreloader();
+
+  @Test
+  public void preloadSingleService() throws Exception {
+    preloader.tasks.add(new TestPreloadCallable(
+        new DataPreload(PRELOAD_STRING_KEY, PRELOAD_STRING_VALUE)));
+
+    PreloaderService service = new ConcurrentPreloaderService(new ImmediateExecutorService(),
+        preloader);
+
+    Collection<PreloadedData> preloads = service.preload((Gadget) null);
+
+    Collection<Object> preloaded = getAll(preloads);
+    assertEquals(ImmutableMap.of(PRELOAD_STRING_KEY, PRELOAD_STRING_VALUE),
+        preloaded.iterator().next());
+  }
+
+  /** Load all the data out of a preloads object */
+  private Collection<Object> getAll(Collection<PreloadedData> preloads) throws PreloadException {
+    List<Object> list = Lists.newArrayList();
+    for (PreloadedData preloadCallable : preloads) {
+      list.addAll(preloadCallable.toJson());
+    }
+
+    return list;
+  }
+
+  @Test
+  public void preloadMultipleServices() throws PreloadException {
+    preloader.tasks.add(new TestPreloadCallable(
+        new DataPreload(PRELOAD_STRING_KEY, PRELOAD_STRING_VALUE)));
+
+    preloader.tasks.add(new TestPreloadCallable(
+        new DataPreload(PRELOAD_NUMERIC_KEY, PRELOAD_NUMERIC_VALUE)));
+
+    preloader.tasks.add(new TestPreloadCallable(
+        new DataPreload(PRELOAD_MAP_KEY, PRELOAD_MAP_VALUE)));
+
+    PreloaderService service = new ConcurrentPreloaderService(new ImmediateExecutorService(),
+        preloader);
+
+    Collection<PreloadedData> preloads =
+      service.preload((Gadget) null);
+
+    Collection<Object> preloaded = getAll(preloads);
+    assertEquals(ImmutableList.<Object>of(
+        ImmutableMap.of(PRELOAD_STRING_KEY, PRELOAD_STRING_VALUE),
+        ImmutableMap.of(PRELOAD_NUMERIC_KEY, PRELOAD_NUMERIC_VALUE),
+        ImmutableMap.of(PRELOAD_MAP_KEY, PRELOAD_MAP_VALUE)), preloaded);
+  }
+
+  @Test
+  public void multiplePreloadsFiresJustOneInCurrentThread() throws Exception {
+    TestPreloadCallable first =
+        new TestPreloadCallable(new DataPreload(PRELOAD_STRING_KEY, PRELOAD_STRING_VALUE));
+    TestPreloadCallable second =
+        new TestPreloadCallable(new DataPreload(PRELOAD_NUMERIC_KEY, PRELOAD_MAP_VALUE));
+    TestPreloadCallable third =
+        new TestPreloadCallable(new DataPreload(PRELOAD_MAP_KEY, PRELOAD_NUMERIC_VALUE));
+
+    preloader.tasks.add(first);
+    preloader.tasks.add(second);
+    preloader.tasks.add(third);
+
+    PreloaderService service = new ConcurrentPreloaderService(Executors.newFixedThreadPool(5),
+        preloader);
+
+    service.preload((Gadget) null);
+
+    TestPreloadCallable ranInSameThread = null;
+    for (TestPreloadCallable preloadCallable: Lists.newArrayList(first, second, third)) {
+      if (preloadCallable.executedThread == Thread.currentThread()) {
+        if (ranInSameThread != null) {
+          fail("More than one request ran in the current thread.");
+        }
+
+        ranInSameThread = preloadCallable;
+      }
+    }
+
+    assertNotNull("No preloads executed in the current thread. ", ranInSameThread);
+  }
+
+  @Test
+  public void singlePreloadExecutesInCurrentThread() throws Exception {
+    TestPreloadCallable callable =
+        new TestPreloadCallable(new DataPreload(PRELOAD_STRING_KEY, PRELOAD_STRING_VALUE));
+    preloader.tasks.add(callable);
+
+    PreloaderService service = new ConcurrentPreloaderService(Executors.newCachedThreadPool(),
+        preloader);
+
+    service.preload((Gadget) null);
+
+    assertSame("Single request not run in current thread",
+        Thread.currentThread(), callable.executedThread);
+  }
+
+  private static class TestPreloader implements Preloader {
+    protected final Collection<Callable<PreloadedData>> tasks = Lists.newArrayList();
+
+    protected TestPreloader() {
+    }
+
+    public Collection<Callable<PreloadedData>> createPreloadTasks(
+        Gadget gadget) {
+      return tasks;
+    }
+  }
+
+  private static class TestPreloadCallable implements Callable<PreloadedData> {
+    private final PreloadedData preload;
+    public Thread executedThread;
+
+    public TestPreloadCallable(PreloadedData preload) {
+      this.preload = preload;
+    }
+
+    public PreloadedData call() throws Exception {
+      executedThread = Thread.currentThread();
+      if (preload == null) {
+        throw new PreloadException("No preload for this test.");
+      }
+
+      return preload;
+    }
+  }
+
+  private static class DataPreload implements PreloadedData {
+    private final String key;
+    private final Object data;
+
+    public DataPreload(String key, Object data) {
+      this.key = key;
+      this.data = data;
+    }
+
+    public Collection<Object> toJson() {
+      return ImmutableList.of((Object) ImmutableMap.of(key, data));
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/ConcurrentPreloadsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/ConcurrentPreloadsTest.java
new file mode 100644
index 0000000..9e72490
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/ConcurrentPreloadsTest.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Tests for ConcurrentPreloads.
+ */
+public class ConcurrentPreloadsTest {
+
+  @Test
+  public void getData() throws Exception {
+    ConcurrentPreloads preloads = new ConcurrentPreloads();
+    preloads.add(TestFuture.returnsNormal("foo"));
+    preloads.add(TestFuture.returnsNormal("bar"));
+
+    assertEquals(2, preloads.size());
+    Iterator<PreloadedData> iterator = preloads.iterator();
+    assertEquals(TestFuture.expectedResult("foo"), iterator.next().toJson());
+    assertEquals(TestFuture.expectedResult("bar"), iterator.next().toJson());
+    assertFalse(iterator.hasNext());
+  }
+
+  @Test
+  public void getDataWithRuntimeException() throws Exception{
+    ConcurrentPreloads preloads = new ConcurrentPreloads();
+    preloads.add(TestFuture.throwsExecution());
+    preloads.add(TestFuture.returnsNormal("foo"));
+
+    assertEquals(2, preloads.size());
+    Iterator<PreloadedData> iterator = preloads.iterator();
+
+    // First item should throw an exception, a PreloadException around
+    // a RuntimeException
+    PreloadedData withError = iterator.next();
+    try {
+      withError.toJson();
+      fail();
+    } catch (PreloadException pe) {
+      assertSame(pe.getCause().getClass(), RuntimeException.class);
+    }
+
+    // And iteration should continue
+    assertEquals(TestFuture.expectedResult("foo"), iterator.next().toJson());
+  }
+
+  @Test
+  public void getDataWithPreloadException() throws Exception{
+    ConcurrentPreloads preloads = new ConcurrentPreloads();
+    preloads.add(TestFuture.throwsExecutionWrapped());
+    preloads.add(TestFuture.returnsNormal("foo"));
+
+    assertEquals(2, preloads.size());
+    Iterator<PreloadedData> iterator = preloads.iterator();
+
+    // First item should throw an exception, a straight PreloadException
+    PreloadedData withError = iterator.next();
+    try {
+      withError.toJson();
+      fail();
+    } catch (PreloadException pe) {
+      assertNull(pe.getCause());
+    }
+
+    // And iteration should continue
+    assertEquals(TestFuture.expectedResult("foo"), iterator.next().toJson());
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void getDataThrowsInterruped() throws Exception{
+    ConcurrentPreloads preloads = new ConcurrentPreloads();
+    preloads.add(TestFuture.throwsInterrupted());
+    preloads.add(TestFuture.returnsNormal("foo"));
+
+    assertEquals(2, preloads.size());
+    Iterator<PreloadedData> iterator = preloads.iterator();
+    // InterruptedException should immediately terminate
+    iterator.next();
+  }
+
+  private static class TestFuture implements Future<PreloadedData> {
+    private boolean throwsInterrupted;
+    private boolean throwsExecution;
+    private boolean throwsExecutionWrapped;
+    protected final String key;
+
+    private TestFuture(String key) {
+      this.key = key;
+    }
+
+    public static TestFuture returnsNormal(String key) {
+      return new TestFuture(key);
+    }
+
+    public static TestFuture throwsInterrupted() {
+      TestFuture future = new TestFuture(null);
+      future.throwsInterrupted = true;
+      return future;
+    }
+
+    public static TestFuture throwsExecution() {
+      TestFuture future = new TestFuture(null);
+      future.throwsExecution = true;
+      return future;
+    }
+
+    public static TestFuture throwsExecutionWrapped() {
+      TestFuture future = new TestFuture(null);
+      future.throwsExecutionWrapped = true;
+      return future;
+    }
+
+    public static Collection<Object> expectedResult(String key) {
+      return ImmutableList.of((Object) ImmutableMap.of(key, "Preloaded"));
+    }
+
+    public PreloadedData get() throws InterruptedException, ExecutionException {
+      if (throwsInterrupted) {
+        throw new InterruptedException("Interrupted!");
+      }
+
+      if (throwsExecution) {
+        throw new ExecutionException(new RuntimeException("Fail"));
+      }
+
+      if (throwsExecutionWrapped) {
+        throw new ExecutionException(new PreloadException("Preload failed."));
+      }
+
+      return new PreloadedData() {
+        public Collection<Object> toJson() {
+          return expectedResult(key);
+        }
+      };
+    }
+
+    public boolean cancel(boolean mayInterruptIfRunning) {
+      return false;
+    }
+
+    public PreloadedData get(long timeout, TimeUnit unit) throws InterruptedException,
+        ExecutionException  {
+      return get();
+    }
+
+    public boolean isCancelled() {
+      return false;
+    }
+
+    public boolean isDone() {
+      return true;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/HttpPreloaderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/HttpPreloaderTest.java
new file mode 100644
index 0000000..e6f7f1f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/HttpPreloaderTest.java
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+/**
+ * Tests for HttpPreloader.
+ */
+public class HttpPreloaderTest extends PreloaderTestFixture {
+  private static final String PRELOAD_HREF = "http://www.example.org/file";
+  private static final String PRELOAD_HREF2 = "http://www.example.org/file-two";
+  private static final String PRELOAD_CONTENT = "Preloaded data";
+  protected static final Map<String, String> PRELOAD_METADATA = ImmutableMap.of("foo", "bar");
+  protected final RecordingHttpFetcher plainFetcher = new RecordingHttpFetcher();
+  protected final RecordingHttpFetcher oauthFetcher = new RecordingHttpFetcher();
+
+  private final RequestPipeline requestPipeline = new RequestPipeline() {
+    public HttpResponse execute(HttpRequest request) {
+      if (request.getAuthType() == AuthType.NONE) {
+        return plainFetcher.fetch(request);
+      }
+      return oauthFetcher.fetch(request);
+    }
+  };
+
+  private void checkRequest(HttpRequest request) {
+    assertEquals(context.getContainer(), request.getContainer());
+    assertEquals(GADGET_URL.toString(), request.getGadget().toString());
+    assertEquals(context.getToken().getAppId(), request.getSecurityToken().getAppId());
+  }
+
+  private static void checkResults(Object results, String url) throws Exception {
+    Map<String, Object> expected = Maps.newHashMap();
+    expected.put("body", PRELOAD_CONTENT);
+    expected.put("rc", HttpResponse.SC_OK);
+    expected.put("id", url);
+    expected.put("headers", Collections.singletonMap("set-cookie", Arrays.asList("yo=momma")));
+    expected.putAll(PRELOAD_METADATA);
+
+    JsonAssert.assertObjectEquals(expected, results);
+  }
+
+  private static void checkResults(Object results) throws Exception {
+    checkResults(results, PRELOAD_HREF);
+  }
+
+  @Test
+  public void normalPreloads() throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        " <Preload href='" + PRELOAD_HREF + "'/>" +
+        "</ModulePrefs><Content/></Module>";
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+    Preloader preloader = new HttpPreloader(requestPipeline);
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW));
+    Collection<Callable<PreloadedData>> preloaded =
+        preloader.createPreloadTasks(gadget);
+
+    assertEquals(1, preloaded.size());
+    PreloadedData data = preloaded.iterator().next().call();
+
+    checkRequest(plainFetcher.requests.get(0));
+    assertFalse("request should not ignore cache", plainFetcher.requests.get(0).getIgnoreCache());
+    checkResults(data.toJson().iterator().next());
+  }
+
+  @Test
+  public void ignoreCachePreloads() throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        " <Preload href='" + PRELOAD_HREF + "' authz='signed' sign_viewer='false'/>" +
+        "</ModulePrefs><Content/></Module>";
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+    Preloader preloader = new HttpPreloader(requestPipeline);
+
+    ignoreCache = true;
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW));
+    Collection<Callable<PreloadedData>> preloaded =
+        preloader.createPreloadTasks(gadget);
+
+    assertEquals(1, preloaded.size());
+    preloaded.iterator().next().call();
+
+    HttpRequest request = oauthFetcher.requests.get(0);
+    assertTrue("request should ignore cache", request.getIgnoreCache());
+    checkRequest(request);
+  }
+
+
+  @Test
+  public void signedPreloads() throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        " <Preload href='" + PRELOAD_HREF + "' authz='signed' sign_viewer='false'/>" +
+        "</ModulePrefs><Content/></Module>";
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+    Preloader preloader = new HttpPreloader(requestPipeline);
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW));
+    Collection<Callable<PreloadedData>> preloaded =
+        preloader.createPreloadTasks(gadget);
+
+    assertEquals(1, preloaded.size());
+    PreloadedData data = preloaded.iterator().next().call();
+
+    HttpRequest request = oauthFetcher.requests.get(0);
+    checkRequest(request);
+    assertTrue(request.getOAuthArguments().getSignOwner());
+    assertFalse(request.getOAuthArguments().getSignViewer());
+    checkResults(data.toJson().iterator().next());
+  }
+
+  @Test
+  public void oauthPreloads() throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        // This is kind of a bogus test since oauth params aren't set.
+        " <Preload href='" + PRELOAD_HREF + "' authz='oauth'/>" +
+        "</ModulePrefs><Content/></Module>";
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+    Preloader preloader = new HttpPreloader(requestPipeline);
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW));
+    Collection<Callable<PreloadedData>> preloaded = preloader.createPreloadTasks(
+        gadget);
+
+    assertEquals(1, preloaded.size());
+    PreloadedData data = preloaded.iterator().next().call();
+
+    HttpRequest request = oauthFetcher.requests.get(0);
+    checkRequest(request);
+    checkResults(data.toJson().iterator().next());
+  }
+
+  @Test
+  public void multiplePreloads() throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        " <Preload href='" + PRELOAD_HREF + "'/>" +
+        " <Preload href='" + PRELOAD_HREF2 + "'/>" +
+        "</ModulePrefs><Content/></Module>";
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+    Preloader preloader = new HttpPreloader(requestPipeline);
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW));
+    Collection<Callable<PreloadedData>> preloaded = preloader.createPreloadTasks(
+        gadget);
+
+    assertEquals(2, preloaded.size());
+    List<Object> list = getAll(preloaded);
+    assertEquals(2, list.size());
+
+    checkRequest(plainFetcher.requests.get(0));
+    checkResults(list.get(0));
+
+    checkRequest(plainFetcher.requests.get(1));
+    checkResults(list.get(1), PRELOAD_HREF2);
+  }
+
+  private List<Object> getAll(Collection<Callable<PreloadedData>> preloaded) throws Exception {
+    List<Object> list = Lists.newArrayList();
+    for (Callable<PreloadedData> preloadCallable : preloaded) {
+      list.addAll(preloadCallable.call().toJson());
+    }
+
+    return list;
+  }
+
+  @Test
+  public void onlyPreloadForCorrectView() throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        " <Preload href='" + PRELOAD_HREF + "' views='foo,bar,baz'/>" +
+        " <Preload href='" + PRELOAD_HREF2 + "' views='bar'/>" +
+        "</ModulePrefs><Content/></Module>";
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+    Preloader preloader = new HttpPreloader(requestPipeline);
+
+    view = "foo";
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW));
+    Collection<Callable<PreloadedData>> preloaded
+        = preloader.createPreloadTasks(gadget);
+
+    List<Object> list = getAll(preloaded);
+    assertEquals(1, list.size());
+    checkRequest(plainFetcher.requests.get(0));
+    checkResults(list.get(0));
+  }
+
+  private static class RecordingHttpFetcher implements HttpFetcher {
+    protected final List<HttpRequest> requests = Lists.newArrayList();
+
+    protected RecordingHttpFetcher() {
+    }
+
+    public HttpResponse fetch(HttpRequest request) {
+      requests.add(request);
+      return new HttpResponseBuilder()
+          .setMetadata(PRELOAD_METADATA)
+          .setResponseString(PRELOAD_CONTENT)
+          .addHeader("Set-Cookie", "yo=momma")
+          .create();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelineExecutorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelineExecutorTest.java
new file mode 100644
index 0000000..8af26f7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelineExecutorTest.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import static org.easymock.EasyMock.and;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.reportMatcher;
+import static org.easymock.EasyMock.same;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.RequestAuthenticationInfo;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.easymock.Capture;
+import org.easymock.IArgumentMatcher;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Collection;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+public class PipelineExecutorTest {
+
+  private IMocksControl control;
+  private PipelinedDataPreloader preloader;
+  private PreloaderService preloaderService;
+  private GadgetContext context;
+  private PipelineExecutor executor;
+
+  private static final Uri GADGET_URI = Uri.parse("http://example.org/gadget.php");
+
+  private static final String CONTENT =
+    "<Content xmlns:os=\"http://ns.opensocial.org/2008/markup\">"
+      + "  <os:PeopleRequest key=\"me\" userId=\"canonical\"/>"
+      + "  <os:HttpRequest key=\"json\" href=\"test.json\"/>"
+      + "</Content>";
+
+  // Two requests, one depends on the other
+  private static final String TWO_BATCH_CONTENT =
+    "<Content xmlns:os=\"http://ns.opensocial.org/2008/markup\">"
+    + "  <os:PeopleRequest key=\"me\" userId=\"${json.user}\"/>"
+    + "  <os:HttpRequest key=\"json\" href=\"${ViewParams.file}\"/>"
+    + "</Content>";
+
+  // One request, but it requires data that isn\"t present
+  private static final String BLOCKED_FIRST_BATCH_CONTENT =
+    "<Content xmlns:os=\"http://ns.opensocial.org/2008/markup\">"
+    + "  <os:PeopleRequest key=\"me\" userId=\"${json.user}\"/>"
+    + "</Content>";
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createStrictControl();
+    preloader = control.createMock(PipelinedDataPreloader.class);
+    preloaderService = new ConcurrentPreloaderService(Executors.newSingleThreadExecutor(), null);
+    executor = new PipelineExecutor(preloader, preloaderService, Expressions.forTesting());
+
+    context = new GadgetContext(){};
+  }
+
+  private PipelinedData getPipelinedData(String pipelineXml) throws SpecParserException {
+    Element element = XmlUtil.parseSilent(pipelineXml);
+    return new PipelinedData(element, GADGET_URI);
+  }
+
+  @Test
+  public void execute() throws Exception {
+    PipelinedData pipeline = getPipelinedData(CONTENT);
+
+    Capture<PipelinedData.Batch> batchCapture =
+      new Capture<PipelinedData.Batch>();
+
+    JSONObject expectedData = new JSONObject("{result: {foo: 'bar'}}");
+
+    // Dummy return results (the "real" return would have two values)
+    Callable<PreloadedData> callable = createPreloadTask("key", expectedData.toString());
+
+    // One batch with 1 each HTTP and Social preload
+    expect(preloader.createPreloadTasks(same(context),
+            and(eqBatch(1, 1), capture(batchCapture))))
+            .andReturn(ImmutableList.of(callable));
+
+    control.replay();
+
+    PipelineExecutor.Results results = executor.execute(context,
+        ImmutableList.of(pipeline));
+
+    // Verify the data set is injected, and the os-data was deleted
+    assertTrue(batchCapture.getValue().getPreloads().containsKey("me"));
+    assertTrue(batchCapture.getValue().getPreloads().containsKey("json"));
+
+    JsonAssert.assertJsonEquals("[{id: 'key', result: {foo: 'bar'}}]",
+        JsonSerializer.serialize(results.results));
+    JsonAssert.assertJsonEquals("{foo: 'bar'}",
+        JsonSerializer.serialize(results.keyedResults.get("key")));
+    assertTrue(results.remainingPipelines.isEmpty());
+
+    control.verify();
+  }
+
+  @Test
+  public void executeWithTwoBatches() throws Exception {
+    PipelinedData pipeline = getPipelinedData(TWO_BATCH_CONTENT);
+
+    context = new GadgetContext() {
+      @Override
+      public String getParameter(String property) {
+        // Provide the filename to be requested in the first batch
+        if ("view-params".equals(property)) {
+          return "{'file': 'test.json'}";
+        }
+        return null;
+      }
+    };
+
+    // First batch, the HTTP fetch
+    Capture<PipelinedData.Batch> firstBatch =
+      new Capture<PipelinedData.Batch>();
+    Callable<PreloadedData> firstTask = createPreloadTask("json",
+        "{result: {user: 'canonical'}}");
+
+    // Second batch, the user fetch
+    Capture<PipelinedData.Batch> secondBatch =
+      new Capture<PipelinedData.Batch>();
+    Callable<PreloadedData> secondTask = createPreloadTask("me",
+        "{result: {'id':'canonical'}}");
+
+    // First, a batch with an HTTP request
+    expect(
+        preloader.createPreloadTasks(same(context),
+            and(eqBatch(0, 1), capture(firstBatch))))
+            .andReturn(ImmutableList.of(firstTask));
+    // Second, a batch with a social request
+    expect(
+        preloader.createPreloadTasks(same(context),
+            and(eqBatch(1, 0), capture(secondBatch))))
+            .andReturn(ImmutableList.of(secondTask));
+
+    control.replay();
+
+    PipelineExecutor.Results results = executor.execute(context,
+        ImmutableList.of(pipeline));
+
+    JsonAssert.assertJsonEquals("[{id: 'json', result: {user: 'canonical'}}," +
+        "{id: 'me', result: {id: 'canonical'}}]",
+        JsonSerializer.serialize(results.results));
+    assertEquals(ImmutableSet.of("json", "me"), results.keyedResults.keySet());
+    assertTrue(results.remainingPipelines.isEmpty());
+
+    control.verify();
+
+    // Verify the data set is injected, and the os-data was deleted
+
+    // Check the evaluated HTTP request
+    RequestAuthenticationInfo request = (RequestAuthenticationInfo)
+        firstBatch.getValue().getPreloads().get("json").getData();
+    assertEquals("http://example.org/test.json", request.getHref().toString());
+
+    // Check the evaluated person request
+    JSONObject personRequest = (JSONObject) secondBatch.getValue().getPreloads().get("me").getData();
+    assertEquals("canonical", personRequest.getJSONObject("params").getJSONArray("userId").get(0));
+  }
+
+  @Test
+  public void executeWithBlockedBatch() throws Exception {
+    PipelinedData pipeline = getPipelinedData(BLOCKED_FIRST_BATCH_CONTENT);
+
+    // Expect a batch with no content
+    expect(
+        preloader.createPreloadTasks(same(context), eqBatch(0, 0)))
+            .andReturn(ImmutableList.<Callable<PreloadedData>>of());
+
+    control.replay();
+
+    PipelineExecutor.Results results = executor.execute(context,
+        ImmutableList.of(pipeline));
+    assertEquals(0, results.results.size());
+    assertTrue(results.keyedResults.isEmpty());
+    assertEquals(1, results.remainingPipelines.size());
+    assertSame(pipeline, results.remainingPipelines.iterator().next());
+
+    control.verify();
+  }
+
+  @Test
+  public void executeError() throws Exception {
+    PipelinedData pipeline = getPipelinedData(CONTENT);
+
+    Capture<PipelinedData.Batch> batchCapture =
+      new Capture<PipelinedData.Batch>();
+
+    JSONObject expectedData = new JSONObject("{error: {message: 'NO!', code: 500}}");
+
+    // Dummy return results (the "real" return would have two values)
+    Callable<PreloadedData> callable = createPreloadTask("key", expectedData.toString());
+
+    // One batch with 1 each HTTP and Social preload
+    expect(preloader.createPreloadTasks(same(context),
+            and(eqBatch(1, 1), capture(batchCapture))))
+            .andReturn(ImmutableList.of(callable));
+
+    control.replay();
+
+    PipelineExecutor.Results results = executor.execute(context,
+        ImmutableList.of(pipeline));
+
+    // Verify the data set is injected, and the os-data was deleted
+    assertTrue(batchCapture.getValue().getPreloads().containsKey("me"));
+    assertTrue(batchCapture.getValue().getPreloads().containsKey("json"));
+
+    JsonAssert.assertJsonEquals("[{id: 'key', error: {message: 'NO!', code: 500}}]",
+        JsonSerializer.serialize(results.results));
+    JsonAssert.assertJsonEquals("{message: 'NO!', code: 500}",
+        JsonSerializer.serialize(results.keyedResults.get("key")));
+    assertTrue(results.remainingPipelines.isEmpty());
+
+    control.verify();
+  }
+
+  @Test
+  public void executePreloadException() throws Exception {
+    PipelinedData pipeline = getPipelinedData(CONTENT);
+    final PreloadedData willThrow = control.createMock(PreloadedData.class);
+
+    Callable<PreloadedData> callable = new Callable<PreloadedData>() {
+      public PreloadedData call() throws Exception {
+        return willThrow;
+      }
+    };
+
+    // One batch
+    expect(preloader.createPreloadTasks(same(context),
+        isA(PipelinedData.Batch.class))).andReturn(ImmutableList.of(callable));
+    // And PreloadedData that throws an exception
+    expect(willThrow.toJson()).andThrow(new PreloadException("Failed"));
+
+
+    control.replay();
+
+    PipelineExecutor.Results results = executor.execute(context,
+        ImmutableList.of(pipeline));
+
+    // The exception is fully handled, and leads to empty results
+    assertEquals(0, results.results.size());
+    assertTrue(results.keyedResults.isEmpty());
+    assertTrue(results.remainingPipelines.isEmpty());
+
+    control.verify();
+  }
+
+  /** Match a batch with the specified count of social and HTTP data items */
+  private PipelinedData.Batch eqBatch(int socialCount, int httpCount) {
+    reportMatcher(new BatchMatcher(socialCount, httpCount));
+    return null;
+  }
+
+  private static class BatchMatcher implements IArgumentMatcher {
+    private final int socialCount;
+    private final int httpCount;
+
+    public BatchMatcher(int socialCount, int httpCount) {
+      this.socialCount = socialCount;
+      this.httpCount = httpCount;
+    }
+
+    public void appendTo(StringBuffer buffer) {
+      buffer.append("eqBuffer[social=").append(socialCount).append(",http=").append(httpCount).append(']');
+    }
+
+    public boolean matches(Object obj) {
+      if (!(obj instanceof PipelinedData.Batch)) {
+        return false;
+      }
+
+      PipelinedData.Batch batch = (PipelinedData.Batch) obj;
+      int actualSocialCount = 0;
+      int actualHttpCount = 0;
+      for (PipelinedData.BatchItem item : batch.getPreloads().values()) {
+        if (item.getType() == PipelinedData.BatchType.HTTP) {
+          actualHttpCount++;
+        } else if (item.getType() == PipelinedData.BatchType.SOCIAL) {
+          actualSocialCount++;
+        }
+      }
+
+      return socialCount == actualSocialCount && httpCount == actualHttpCount;
+    }
+
+  }
+  /** Create a mock Callable for a single preload task */
+  private Callable<PreloadedData> createPreloadTask(final String key, String jsonResult)
+      throws JSONException {
+    final JSONObject value = new JSONObject(jsonResult);
+    value.put("id", key);
+    final PreloadedData preloadResult = new PreloadedData() {
+      public Collection<Object> toJson() throws PreloadException {
+        return ImmutableList.<Object>of(value);
+      }
+    };
+
+    Callable<PreloadedData> callable = new Callable<PreloadedData>() {
+      public PreloadedData call() throws Exception {
+        return preloadResult;
+      }
+    };
+    return callable;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java
new file mode 100644
index 0000000..6d29bc8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PipelinedDataPreloaderTest.java
@@ -0,0 +1,511 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.JsonUtil;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetELResolver;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.PipelinedData.Batch;
+import org.easymock.EasyMock;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+/**
+ * Test for PipelinedDataPreloader.
+ */
+public class PipelinedDataPreloaderTest extends PreloaderTestFixture {
+  private ContainerConfig containerConfig;
+  private final Expressions expressions = Expressions.forTesting();
+
+  private static final String XML = "<Module xmlns:os=\"" + PipelinedData.OPENSOCIAL_NAMESPACE
+      + "\">" + "<ModulePrefs title=\"Title\"/>"
+      + "<Content href=\"http://example.org/proxied.php\" view=\"profile\">"
+      + "  <os:PeopleRequest key=\"p\" userIds=\"you\"/>"
+      + "  <os:PersonAppDataRequest key=\"a\" userId=\"she\"/>" + "</Content></Module>";
+
+  private static final String HTTP_REQUEST_URL =  "http://example.org/preload.html";
+  private static final String PARAMS = "a=b&c=d";
+  private static final String XML_PARAMS = "a=b&amp;c=d";
+
+  private static final String XML_WITH_HTTP_REQUEST = "<Module xmlns:os=\""
+      + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+      + "<ModulePrefs title=\"Title\"/>"
+      + "<Content href=\"http://example.org/proxied.php\" view=\"profile\">"
+      + "  <os:HttpRequest key=\"p\" href=\"" + HTTP_REQUEST_URL + "\" "
+      + "refreshInterval=\"60\" method=\"POST\"/>" + "</Content></Module>";
+
+  private static final String XML_WITH_VARIABLE = "<Module " +
+      "xmlns:os=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" " +
+        "xmlns:osx=\"" + PipelinedData.EXTENSION_NAMESPACE + "\">"
+    + "<ModulePrefs title=\"Title\"/>"
+    + "<Content href=\"http://example.org/proxied.php\" view=\"profile\">"
+    + "  <osx:Variable key=\"p\" value=\"${1+1}\"/>" + "</Content></Module>";
+
+  private static final String XML_WITH_HTTP_REQUEST_FOR_TEXT = "<Module xmlns:os=\""
+    + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+    + "<ModulePrefs title=\"Title\"/>"
+    + "<Content href=\"http://example.org/proxied.php\" view=\"profile\">"
+    + "  <os:HttpRequest key=\"p\" format=\"text\" href=\"" + HTTP_REQUEST_URL + "\" "
+    + "refreshInterval=\"60\" method=\"POST\"/>" + "</Content></Module>";
+
+  private static final String XML_WITH_HTTP_REQUEST_AND_PARAMS = "<Module xmlns:os=\""
+    + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+    + "<ModulePrefs title=\"Title\"/>"
+    + "<Content href=\"http://example.org/proxied.php\" view=\"profile\">"
+    + "  <os:HttpRequest key=\"p\" href=\"" + HTTP_REQUEST_URL + "\" "
+    + "                  method=\"POST\" params=\"" + XML_PARAMS + "\"/>"
+    + "</Content></Module>";
+
+  private static final String XML_WITH_HTTP_REQUEST_AND_GET_PARAMS = "<Module xmlns:os=\""
+    + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+    + "<ModulePrefs title=\"Title\"/>"
+    + "<Content href=\"http://example.org/proxied.php\" view=\"profile\">"
+    + "  <os:HttpRequest key=\"p\" href=\"" + HTTP_REQUEST_URL + "\" "
+    + "                  method=\"GET\" params=\"" + XML_PARAMS + "\"/>"
+    + "</Content></Module>";
+
+  private static final String XML_IN_DEFAULT_CONTAINER = "<Module xmlns:os=\""
+    + PipelinedData.OPENSOCIAL_NAMESPACE + "\">" + "<ModulePrefs title=\"Title\"/>"
+    + "<Content href=\"http://example.org/proxied.php\">"
+    + "  <os:PeopleRequest key=\"p\" userIds=\"you\"/>"
+    + "  <os:PersonAppDataRequest key=\"a\" userId=\"she\"/>" + "</Content></Module>";
+
+  @Before
+  public void createContainerConfig() {
+    containerConfig = EasyMock.createMock(ContainerConfig.class);
+    EasyMock.expect(containerConfig.getString(CONTAINER, "gadgets.osDataUri")).andStubReturn(
+        "http://%host%/social/rpc");
+    EasyMock.replay(containerConfig);
+  }
+
+  @Test
+  public void testSocialPreload() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML);
+
+    String socialResult = "[{id:'p', result:1}, {id:'a', result:2}]";
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(socialResult);
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+
+    view = "profile";
+    contextParams.put("st", "token");
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    assertEquals(1, tasks.size());
+    // Nothing fetched yet
+    assertEquals(0, pipeline.requests.size());
+
+    Collection<Object> result = tasks.iterator().next().call().toJson();
+    assertEquals(2, result.size());
+
+    JSONObject resultWithKeyP = new JSONObject("{id: 'p', result: 1}");
+    JSONObject resultWithKeyA = new JSONObject("{id: 'a', result: 2}");
+    Map<String, String> resultsById = getResultsById(result);
+    JsonAssert.assertJsonEquals(resultWithKeyA.toString(), resultsById.get("a"));
+    JsonAssert.assertJsonEquals(resultWithKeyP.toString(), resultsById.get("p"));
+
+    // Should have only fetched one request
+    assertEquals(1, pipeline.requests.size());
+    HttpRequest request = pipeline.requests.get(0);
+
+    assertEquals("http://" + context.getHost() + "/social/rpc?st=token", request.getUri()
+        .toString());
+    assertEquals("POST", request.getMethod());
+    assertTrue(request.getContentType().startsWith("application/json"));
+  }
+
+  @Test
+  public void testSocialPreloadWithBatchError() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML);
+
+    String socialResult = "{code: 401, message: 'unauthorized'}";
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(socialResult);
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+
+    view = "profile";
+    contextParams.put("st", "token");
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    assertEquals(1, tasks.size());
+    // Nothing fetched yet
+    assertEquals(0, pipeline.requests.size());
+
+    Collection<Object> result = tasks.iterator().next().call().toJson();
+    assertEquals(2, result.size());
+
+    JSONObject resultWithKeyP = new JSONObject("{id: 'p', error: {code: 401, message: 'unauthorized'}}");
+    JSONObject resultWithKeyA = new JSONObject("{id: 'a', error: {code: 401, message: 'unauthorized'}}");
+    Map<String, String> resultsById = getResultsById(result);
+    JsonAssert.assertJsonEquals(resultWithKeyA.toString(), resultsById.get("a"));
+    JsonAssert.assertJsonEquals(resultWithKeyP.toString(), resultsById.get("p"));
+  }
+
+  @Test
+  public void testSocialPreloadWithHttpError() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML);
+
+    HttpResponse httpError = new HttpResponseBuilder()
+        .setHttpStatusCode(HttpResponse.SC_INTERNAL_SERVER_ERROR)
+        .create();
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(httpError);
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+
+    view = "profile";
+    contextParams.put("st", "token");
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+
+    Collection<Object> result = tasks.iterator().next().call().toJson();
+    assertEquals(2, result.size());
+
+    JSONObject resultWithKeyP = new JSONObject("{id: 'p', error: {code: 500}}");
+    JSONObject resultWithKeyA = new JSONObject("{id: 'a', error: {code: 500}}");
+    Map<String, String> resultsById = getResultsById(result);
+    JsonAssert.assertJsonEquals(resultWithKeyA.toString(), resultsById.get("a"));
+    JsonAssert.assertJsonEquals(resultWithKeyP.toString(), resultsById.get("p"));
+  }
+
+  @Test
+  /**
+   * Verify that social preloads where the request doesn't contain a token
+   * serve up 403s for the preloaded data, instead of failing the whole request.
+   */
+  public void testSocialPreloadWithoutToken() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML);
+
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline("");
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+    view = "profile";
+    // But don't set the security token
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    PreloadedData data = tasks.iterator().next().call();
+    JSONObject resultWithKeyA = new JSONObject(
+        "{error:{code:403,data:{content:\"Security token missing\"}},id:\"a\"}");
+    JSONObject resultWithKeyP = new JSONObject(
+        "{error:{code:403,data:{content:\"Security token missing\"}},id:\"p\"}");
+    Collection<Object> result = data.toJson();
+    assertEquals(2, result.size());
+    Map<String, String> resultsById = getResultsById(result);
+    JsonAssert.assertJsonEquals(resultWithKeyA.toString(), resultsById.get("a"));
+    JsonAssert.assertJsonEquals(resultWithKeyP.toString(), resultsById.get("p"));
+  }
+
+  private Map<String, String> getResultsById(Collection<Object> result) {
+    Map<String, String> resultsById = Maps.newHashMap();
+    for (Object o : result) {
+      resultsById.put((String) JsonUtil.getProperty(o, "id"),
+          JsonSerializer.serialize(o));
+    }
+
+    return resultsById;
+  }
+
+  private Batch getBatch(Gadget gadget) {
+    return gadget.getCurrentView().getPipelinedData().getBatch(expressions,
+        new GadgetELResolver(gadget.getContext()));
+  }
+
+  @Test
+  public void testHttpPreloadOfJsonObject() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("{foo: 'bar'}")
+        .create();
+    String expectedResult = "{result: {status: 200, content: {foo: 'bar'}}, id: 'p'}";
+
+    verifyHttpPreload(response, expectedResult);
+  }
+
+  @Test
+  public void testHttpPreloadOfJsonArrayWithHeaders() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("[1, 2]")
+        .addHeader("content-type", "application/json")
+        .addHeader("set-cookie", "cookiecookie")
+        .addHeader("not-ok", "shouldn'tbehere")
+        .create();
+
+    String expectedResult = "{result: {status: 200, headers:" +
+        "{'content-type': ['application/json; charset=UTF-8'], 'set-cookie': ['cookiecookie']}," +
+        "content: [1, 2]}, id: 'p'}";
+
+    verifyHttpPreload(response, expectedResult);
+  }
+
+  @Test
+  public void testHttpPreloadOfJsonWithErrorCode() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("not found")
+        .addHeader("content-type", "text/html")
+        .setHttpStatusCode(HttpResponse.SC_NOT_FOUND)
+        .create();
+
+    String expectedResult = "{error: {code: 404, data:" +
+        "{headers: {'content-type': ['text/html; charset=UTF-8']}," +
+            "content: 'not found'}}, id: 'p'}";
+
+    verifyHttpPreload(response, expectedResult);
+  }
+
+  @Test
+  public void testHttpPreloadWithBadJson() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("notjson")
+        .addHeader("content-type", "text/html")
+        .create();
+
+    JSONObject result = new JSONObject(executeHttpPreload(response, XML_WITH_HTTP_REQUEST));
+    assertFalse(result.has("result"));
+
+    JSONObject error = result.getJSONObject("error");
+    assertEquals(HttpResponse.SC_NOT_ACCEPTABLE, error.getInt("code"));
+  }
+
+  @Test
+  public void testHttpPreloadOfText() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("{foo: 'bar'}")
+        .addHeader("content-type", "application/json")
+        .create();
+    // Even though the response was actually JSON, @format=text, so the content
+    // will be a block of text
+    String expectedResult = "{result: {status: 200, headers:" +
+            "{'content-type': ['application/json; charset=UTF-8']}," +
+            "content: '{foo: \\'bar\\'}'}, id: 'p'}";
+
+    String resultString = executeHttpPreload(response, XML_WITH_HTTP_REQUEST_FOR_TEXT);
+    JsonAssert.assertJsonEquals(expectedResult, resultString);
+  }
+
+  private void verifyHttpPreload(HttpResponse response, String expectedJson) throws Exception {
+    String resultString = executeHttpPreload(response, XML_WITH_HTTP_REQUEST);
+    JsonAssert.assertJsonEquals(expectedJson, resultString);
+  }
+
+  /**
+   * Run an HTTP Preload test, returning the String result.
+   */
+  private String executeHttpPreload(HttpResponse response, String xml) throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, xml);
+
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(response);
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+    view = "profile";
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    assertEquals(1, tasks.size());
+    // Nothing fetched yet
+    assertEquals(0, pipeline.requests.size());
+
+    Collection<Object> result = tasks.iterator().next().call().toJson();
+    assertEquals(1, result.size());
+
+    // Should have only fetched one request
+    assertEquals(1, pipeline.requests.size());
+    HttpRequest request = pipeline.requests.get(0);
+
+    assertEquals(HTTP_REQUEST_URL, request.getUri().toString());
+    assertEquals("POST", request.getMethod());
+    assertEquals(60, request.getCacheTtl());
+
+    return result.iterator().next().toString();
+  }
+
+  @Test
+  public void testHttpPreloadWithPostParams() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML_WITH_HTTP_REQUEST_AND_PARAMS);
+
+    String httpResult = "{foo: 'bar'}";
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(httpResult);
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+    view = "profile";
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    tasks.iterator().next().call();
+
+    // Should have only fetched one request
+    assertEquals(1, pipeline.requests.size());
+    HttpRequest request = pipeline.requests.get(0);
+
+    assertEquals(HTTP_REQUEST_URL, request.getUri().toString());
+    assertEquals("POST", request.getMethod());
+    assertEquals(PARAMS, request.getPostBodyAsString());
+  }
+
+  @Test
+  public void testHttpPreloadWithGetParams() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML_WITH_HTTP_REQUEST_AND_GET_PARAMS);
+
+    String httpResult = "{foo: 'bar'}";
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(httpResult);
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+    view = "profile";
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    tasks.iterator().next().call();
+
+    // Should have only fetched one request
+    assertEquals(1, pipeline.requests.size());
+    HttpRequest request = pipeline.requests.get(0);
+
+    assertEquals(HTTP_REQUEST_URL + '?' + PARAMS, request.getUri().toString());
+    assertEquals("GET", request.getMethod());
+  }
+
+  /**
+   * Verify that social preloads pay attention to view resolution by
+   * using gadget.getCurrentView().
+   */
+  @Test
+  public void testSocialPreloadViewResolution() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML_IN_DEFAULT_CONTAINER);
+
+    String socialResult = "[{id:'p', result:1}, {id:'a', result:2}]";
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline(socialResult);
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+
+    view = "profile";
+    contextParams.put("st", "token");
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        // Assume view resolution has behaved correctly
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW));
+
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    assertEquals(1, tasks.size());
+  }
+
+  @Test
+  public void testVariablePreload() throws Exception {
+    GadgetSpec spec = new GadgetSpec(GADGET_URL, XML_WITH_VARIABLE);
+
+    RecordingRequestPipeline pipeline = new RecordingRequestPipeline("");
+    PipelinedDataPreloader preloader = new PipelinedDataPreloader(pipeline, containerConfig);
+
+    view = "profile";
+    contextParams.put("st", "token");
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec)
+        .setCurrentView(spec.getView("profile"));
+
+    PipelinedData.Batch batch = getBatch(gadget);
+    Collection<Callable<PreloadedData>> tasks = preloader.createPreloadTasks(
+        context, batch);
+    assertEquals(1, tasks.size());
+    // Nothing fetched yet
+    assertEquals(0, pipeline.requests.size());
+
+    Collection<Object> result = tasks.iterator().next().call().toJson();
+    assertEquals(1, result.size());
+
+    JsonAssert.assertObjectEquals("{id: 'p', result: 2}", result.iterator().next());
+  }
+
+  private static class RecordingRequestPipeline implements RequestPipeline {
+    public final List<HttpRequest> requests = Lists.newArrayList();
+    private final HttpResponse response;
+
+    public RecordingRequestPipeline(String content) {
+      this(new HttpResponseBuilder().setResponseString(content).create());
+    }
+
+    public RecordingRequestPipeline(HttpResponse response) {
+      this.response = response;
+    }
+
+    public HttpResponse execute(HttpRequest request) {
+      requests.add(request);
+      return response;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PreloaderTestFixture.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PreloaderTestFixture.java
new file mode 100644
index 0000000..459887b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/preload/PreloaderTestFixture.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.preload;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+
+/**
+ * Base code for the preloader tests.
+ */
+public class PreloaderTestFixture {
+  protected static final Uri GADGET_URL = Uri.parse("http://example.org/gadget.xml");
+  protected static final String CONTAINER = "some-container";
+  protected static final String HOST = "example.org";
+  protected String view = "default";
+  protected boolean ignoreCache = false;
+  public Map<String, String> contextParams = Maps.newHashMap();
+
+  public final GadgetContext context = new GadgetContext() {
+    @Override
+    public SecurityToken getToken() {
+      return new FakeGadgetToken();
+    }
+
+    @Override
+    public String getView() {
+      return view;
+    }
+
+    @Override
+    public String getContainer() {
+      return CONTAINER;
+    }
+
+    @Override
+    public Uri getUrl() {
+      return GADGET_URL;
+    }
+
+    @Override
+    public String getHost() {
+      return HOST;
+    }
+
+    @Override
+    public String getParameter(String name) {
+      return contextParams.get(name);
+    }
+
+    @Override
+    public boolean getIgnoreCache() {
+      return ignoreCache;
+    }
+  };
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/process/ProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/process/ProcessorTest.java
new file mode 100644
index 0000000..6de595a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/process/ProcessorTest.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.process;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.variables.Substituter;
+import org.apache.shindig.gadgets.variables.VariableSubstituter;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Lists;
+
+public class ProcessorTest extends EasyMockTestCase {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private static final Uri TYPE_URL_HREF = Uri.parse("http://example.org/gadget.php");
+  private static final String BASIC_HTML_CONTENT = "Hello, World!";
+  protected static final String GADGET =
+      "<Module>" +
+      " <ModulePrefs title='foo'/>" +
+      " <Content view='html' type='html'>" + BASIC_HTML_CONTENT + "</Content>" +
+      " <Content view='url' type='url' href='" + TYPE_URL_HREF + "'/>" +
+      " <Content view='alias' type='html'>" + BASIC_HTML_CONTENT + "</Content>" +
+      "</Module>";
+
+  private final FakeGadgetSpecFactory gadgetSpecFactory = new FakeGadgetSpecFactory();
+  private final FakeVariableSubstituter substituter = new FakeVariableSubstituter();
+  private final GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
+
+  private ContainerConfig containerConfig;
+  private Processor processor;
+
+  @Before
+  public void setUp() throws Exception {
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+         "'gadgets.features':{views:" +
+           "{aliased: {aliases: ['some-alias', 'alias']}}" +
+         "}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    FeatureRegistryProvider registryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) {
+        return null;
+      }
+    };
+
+    processor = new Processor(gadgetSpecFactory, substituter, containerConfig, gadgetAdminStore,
+        registryProvider);
+  }
+
+  private GadgetContext makeContext(final String view, final Uri specUrl, final boolean sanitize) {
+    return new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        if (specUrl == null) {
+          return null;
+        }
+        return specUrl;
+      }
+
+      @Override
+      public String getView() {
+        return view;
+      }
+
+      @Override
+      public boolean getSanitize() {
+        return sanitize;
+      }
+    };
+  }
+
+  private GadgetContext makeContext(final String view) {
+    return makeContext(view, SPEC_URL, false);
+  }
+
+  @Test
+  public void normalProcessing() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+    replay();
+    Gadget gadget = processor.process(makeContext("html"));
+    assertEquals(BASIC_HTML_CONTENT, gadget.getCurrentView().getContent());
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void handlesGadgetExceptionGracefully() throws Exception {
+    gadgetSpecFactory.exception = new GadgetException(GadgetException.Code.INVALID_PATH);
+    processor.process(makeContext("url"));
+  }
+
+  @Test
+  public void doViewAliasing() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+    replay();
+    Gadget gadget = processor.process(makeContext("aliased"));
+    assertEquals(BASIC_HTML_CONTENT, gadget.getCurrentView().getContent());
+  }
+
+  @Test
+  public void noSupportedViewHasNoCurrentView() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+    replay();
+    Gadget gadget = processor.process(makeContext("not-real-view"));
+    assertNull(gadget.getCurrentView());
+  }
+
+  @Test
+  public void substitutionsPerformedTypeHtml() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+    replay();
+    processor.process(makeContext("html"));
+    assertTrue("Substitutions not performed", substituter.wasSubstituted);
+  }
+
+  @Test
+  public void substitutionsPerformedTypeUrl() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+    replay();
+    processor.process(makeContext("url"));
+    assertTrue("Substitutions not performed", substituter.wasSubstituted);
+  }
+
+  @Test
+  public void whitelistChecked() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+    replay();
+    processor.process(makeContext("url"));
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void nonWhitelistedGadgetThrows() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(false);
+    replay();
+    processor.process(makeContext("html"));
+  }
+
+  @Test
+  public void nullUrlThrows() throws Exception {
+    try {
+      processor.process(makeContext("html", null, false));
+      fail("expected ProcessingException");
+    } catch (ProcessingException e) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getHttpStatusCode());
+    }
+  }
+
+  @Test
+  public void nonHttpOrHttpsThrows() throws Exception {
+    try {
+      processor.process(makeContext("html", Uri.parse("file://foo"), false));
+      fail("expected ProcessingException");
+    } catch (ProcessingException e) {
+      assertEquals(HttpServletResponse.SC_FORBIDDEN, e.getHttpStatusCode());
+    }
+  }
+
+  @Test
+  public void typeUrlViewsAreSkippedForSanitizedGadget() throws Exception {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class)))
+    .andReturn(true).anyTimes();
+    replay();
+    Gadget gadget = processor.process(makeContext("url", SPEC_URL, true));
+    assertNull(gadget.getCurrentView());
+    gadget = processor.process(makeContext("html", SPEC_URL, true));
+    assertEquals(BASIC_HTML_CONTENT, gadget.getCurrentView().getContent());
+  }
+
+  private static class FakeGadgetSpecFactory implements GadgetSpecFactory {
+    protected GadgetException exception;
+
+    protected FakeGadgetSpecFactory() {
+    }
+
+    public GadgetSpec getGadgetSpec(GadgetContext context) throws GadgetException {
+      if (exception != null) {
+        throw exception;
+      }
+      return new GadgetSpec(context.getUrl(), GADGET);
+    }
+
+    public Uri getGadgetUri(GadgetContext context) throws GadgetException {
+        return context.getUrl();
+    }
+  }
+
+  private static class FakeVariableSubstituter extends VariableSubstituter {
+    protected boolean wasSubstituted;
+
+    protected FakeVariableSubstituter() {
+      super(Lists.<Substituter>newArrayList());
+    }
+
+    @Override
+    public GadgetSpec substitute(GadgetContext context, GadgetSpec spec) {
+      wasSubstituted = true;
+      return spec;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/CajaResponseRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/CajaResponseRewriterTest.java
new file mode 100644
index 0000000..8784d91
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/CajaResponseRewriterTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.RewriterTestBase;
+import org.junit.Test;
+
+public class CajaResponseRewriterTest extends RewriterTestBase {
+  private static final Uri CONTENT_URI = Uri.parse("http://www.example.org/content");
+
+  private String rewrite(HttpRequest request, HttpResponse response) throws Exception {
+    return rewrite(request, response, null);
+  }
+
+  private String rewrite(HttpRequest request, HttpResponse response, Gadget gadget) throws Exception {
+    request.setSanitizationRequested(true);
+    ResponseRewriter rewriter = createRewriter();
+
+    HttpResponseBuilder hrb = new HttpResponseBuilder(parser, response);
+    rewriter.rewrite(request, hrb, gadget);
+    return hrb.getContent();
+  }
+
+  private ResponseRewriter createRewriter() {
+    return new CajaResponseRewriter(new RequestPipeline() {
+      public HttpResponse execute(HttpRequest request) {
+        return null;
+      }
+    });
+  }
+
+  @Test
+  public void testJs() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/javascript");
+    req.setCajaRequested(true);
+    HttpResponse response = new HttpResponseBuilder().setResponseString("var a;").create();
+    String sanitized = "___.di(IMPORTS___,'a');";
+
+    assertTrue(rewrite(req, response).contains(sanitized));
+    assertTrue(rewrite(req, response, gadget).contains(sanitized));
+  }
+
+  @Test
+  public void testJsWithoutCaja() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/javascript");
+    req.setCajaRequested(false);
+    HttpResponse response = new HttpResponseBuilder().setResponseString("var a;").create();
+    String sanitized = "var a;";
+
+    assertTrue(rewrite(req, response).contains(sanitized));
+    assertTrue(rewrite(req, response, gadget).contains(sanitized));
+  }
+
+  @Test
+  public void testNonJs() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/html");
+    req.setCajaRequested(true);
+    HttpResponse response = new HttpResponseBuilder().setResponseString("<html></html>").create();
+
+    assertEquals("", rewrite(req, response));
+    assertEquals("", rewrite(req, response, gadget));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/DefaultRpcServiceLookupTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/DefaultRpcServiceLookupTest.java
new file mode 100644
index 0000000..3c006ef
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/DefaultRpcServiceLookupTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+
+import org.apache.shindig.gadgets.http.BasicHttpFetcher;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DefaultRpcServiceLookupTest extends Assert {
+
+  private DefaultRpcServiceLookup svcLookup;
+  private String socialEndpoint;
+  private String host;
+
+  @Before
+  public void setUp() throws Exception {
+    svcLookup = new DefaultRpcServiceLookup(new DefaultServiceFetcher(null, new BasicHttpFetcher(null)),
+                                            60l);
+    socialEndpoint = "http://localhost:8080/social/rpc";
+    host = "localhost:8080";
+  }
+
+  @Test
+  public void testGetServicesForContainer_Empty() throws Exception {
+    String container = "ig";
+    Multimap<String, String> services = svcLookup.getServicesFor(container, host);
+    assertEquals(0, services.size());
+  }
+
+  @Test
+  public void testGetServicesForContainer_Null() throws Exception {
+    String container = null;
+    Multimap<String, String> services = svcLookup.getServicesFor(container, host);
+    assertEquals(0, services.size());
+  }
+
+  @Test
+  public void testGetServicesForContainer_OneContainerOneService() throws Exception {
+    ImmutableSet<String> expectedServiceMethods = ImmutableSet.of("system.listMethods");
+    LinkedHashMultimap<String, String> expectedServices = LinkedHashMultimap.create();
+    expectedServices.putAll(socialEndpoint, expectedServiceMethods);
+    String container = "ig";
+    svcLookup.setServicesFor(container, expectedServices);
+
+    Multimap<String, String> actualServices = svcLookup.getServicesFor(container, host);
+    assertEquals(1, actualServices.size());
+    assertTrue(actualServices.containsKey(socialEndpoint));
+    Set<String> actualServiceMethods = (Set<String>) actualServices.get(socialEndpoint);
+    assertEquals(expectedServiceMethods, actualServiceMethods);
+  }
+
+  @Test
+  public void testGetServicesForContainer_OneContainerTwoServices() throws Exception {
+    Set<String> expectedServiceMethods = Sets.newHashSet("system.listMethods", "people.get",
+            "people.update", "people.create", "people.delete");
+
+    LinkedHashMultimap<String, String> expectedServices = LinkedHashMultimap.create();
+    expectedServices.putAll(socialEndpoint, expectedServiceMethods);
+
+    String container = "ig";
+    svcLookup.setServicesFor(container, expectedServices);
+
+    assertServiceHasCorrectConfig(socialEndpoint, expectedServiceMethods, container, 1);
+  }
+
+  @Test
+  public void testGetServiceForContainer_TwoContainersOneEndpoint() throws Exception {
+    String socialEndpoint2 = "http://localhost:8080/api/rpc";
+    Set<String> expectedServiceMethods = Sets.newHashSet("system.listMethods", "people.get",
+            "people.update", "people.create", "people.delete");
+    Set<String> expectedServiceMethods2 = Sets.newHashSet("cache.invalidate");
+
+    LinkedHashMultimap<String, String> expectedServices = LinkedHashMultimap.create();
+    expectedServices.putAll(socialEndpoint, expectedServiceMethods);
+
+    LinkedHashMultimap<String, String> expectedServices2 = LinkedHashMultimap.create();
+    expectedServices2.putAll(socialEndpoint2, expectedServiceMethods2);
+
+    String container = "ig";
+    String container2 = "gm";
+    svcLookup.setServicesFor(container, expectedServices);
+    svcLookup.setServicesFor(container2, expectedServices2);
+
+    assertServiceHasCorrectConfig(socialEndpoint, expectedServiceMethods, container, 1);
+    assertServiceHasCorrectConfig(socialEndpoint2, expectedServiceMethods2, container2, 1);
+  }
+
+  private void assertServiceHasCorrectConfig(String socialEndpoint,
+          Set<String> expectedServiceMethods, String container, int expectedServiceCount) {
+    Multimap<String, String> actualServices = svcLookup.getServicesFor(container, host);
+    assertEquals(expectedServiceCount, actualServices.keySet().size());
+    assertTrue(actualServices.containsKey(socialEndpoint));
+    Set<String> actualServiceMethods = (Set<String>) actualServices.get(socialEndpoint);
+    assertEquals(expectedServiceMethods, actualServiceMethods);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/DefaultServiceFetcherTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/DefaultServiceFetcherTest.java
new file mode 100644
index 0000000..cd3d0f8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/DefaultServiceFetcherTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+
+import org.apache.shindig.auth.BasicSecurityTokenCodec;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.Functions;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+
+import org.easymock.EasyMock;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Test fetching of osapi services from container config and endpoints.
+ */
+public class DefaultServiceFetcherTest extends EasyMockTestCase {
+  protected DefaultServiceFetcher fetcher;
+  protected HttpFetcher mockFetcher;
+  protected Multimap<String, String> configuredServices;
+  protected static final String endPoint1 = "http://%host%/api/rpc";
+  protected static final String endPoint2 = "http://%host%/social/api/rpc";
+
+
+  @Before
+  public void setUp() throws Exception {
+    JSONObject config = createConfig();
+
+    JsonContainerConfig containerConfig =
+        new JsonContainerConfig(config, Expressions.forTesting(new Functions()));
+    mockFetcher = mock(HttpFetcher.class);
+    fetcher = new DefaultServiceFetcher(containerConfig, mockFetcher);
+  }
+
+  private JSONObject createConfig() throws JSONException {
+    JSONObject config = new JSONObject();
+    JSONObject container = new JSONObject();
+    JSONObject services = new JSONObject();
+    JSONObject features = new JSONObject();
+
+    configuredServices = ImmutableMultimap.<String, String>builder()
+      .putAll("http://localhost/api/rpc", "system.listMethods", "service.get")
+      .putAll("gadgets.rpc", "messages.send", "ui.resize").build();
+
+    for (String key : configuredServices.keySet()) {
+      services.put(key, configuredServices.get(key));
+    }
+    features.put(DefaultServiceFetcher.OSAPI_SERVICES, services);
+
+    JSONObject endpoints = new JSONObject();
+
+    endpoints.put(DefaultServiceFetcher.OSAPI_BASE_ENDPOINTS,
+        new JSONArray(ImmutableList.of(endPoint1, endPoint2)));
+    features.put(DefaultServiceFetcher.OSAPI_FEATURE_CONFIG, endpoints);
+    container.put(ContainerConfig.CONTAINER_KEY, new JSONArray("['default']"));
+    container.put(DefaultServiceFetcher.GADGETS_FEATURES_CONFIG, features);
+
+    config.put("default", container);
+    return config;
+  }
+
+  @Test
+  public void testReadConfigNoEndpoints() throws Exception {
+    JSONObject config = createConfig();
+    config.getJSONObject("default").
+        getJSONObject(DefaultServiceFetcher.GADGETS_FEATURES_CONFIG)
+        .remove(DefaultServiceFetcher.OSAPI_FEATURE_CONFIG);
+    JsonContainerConfig containerConfig =
+        new JsonContainerConfig(config,
+            Expressions.forTesting(new Functions()));
+    fetcher = new DefaultServiceFetcher(containerConfig, mockFetcher);
+
+    EasyMock.expect(mockFetcher.fetch(EasyMock.isA(HttpRequest.class))).andReturn(
+        new HttpResponse("")).anyTimes();
+    replay();
+    Multimap<String, String> services = fetcher.getServicesForContainer("default", "dontcare");
+    verify();
+    assertEquals(configuredServices, services);
+  }
+
+  @Test
+  public void testReadConfigEndpointsDown() throws Exception {
+    EasyMock.expect(mockFetcher.fetch(EasyMock.isA(HttpRequest.class))).andReturn(
+        new HttpResponse("")).anyTimes();
+    replay();
+    fetcher.setSecurityTokenCodec( new BasicSecurityTokenCodec() );
+    Multimap<String, String> services = fetcher.getServicesForContainer("default", "dontcare");
+    verify();
+    assertEquals(configuredServices, services);
+  }
+
+  @Test
+  public void testReadConfigWithValidEndpoints() throws Exception {
+    List<String> endPoint1Services = ImmutableList.of("do.something", "delete.someting");
+    JSONObject service1 = new JSONObject();
+    service1.put("result", endPoint1Services);
+
+    List<String> endPoint2Services = ImmutableList.of("weather.get");
+    JSONObject service2 = new JSONObject();
+    service2.put("result", endPoint2Services);
+
+    EasyMock.expect(mockFetcher.fetch(EasyMock.isA(HttpRequest.class))).andReturn(
+        new HttpResponse(service1.toString()));
+    EasyMock.expect(mockFetcher.fetch(EasyMock.isA(HttpRequest.class))).andReturn(
+        new HttpResponse(service2.toString()));
+
+    replay();
+    fetcher.setSecurityTokenCodec( new BasicSecurityTokenCodec() );
+    Multimap<String, String> services = fetcher.getServicesForContainer("default", "dontcare");
+    verify();
+    Multimap<String, String> mergedServices = LinkedHashMultimap.create(configuredServices);
+    mergedServices.putAll(endPoint1, endPoint1Services);
+    mergedServices.putAll(endPoint2, endPoint2Services);
+    assertEquals(mergedServices, LinkedHashMultimap.create(services));
+  }
+
+  @Test
+  public void testReadConfigBadContainer() throws Exception {
+    Multimap<String, String> multimap = fetcher.getServicesForContainer("badcontainer", "dontcare");
+    assertEquals(0, multimap.size());
+  }
+
+  @Test
+  public void testReadConfigRequestMarkedInternal() throws Exception {
+    JSONObject config = createConfig();
+    config.getJSONObject("default").
+        getJSONObject(DefaultServiceFetcher.GADGETS_FEATURES_CONFIG)
+        .getJSONObject(DefaultServiceFetcher.OSAPI_FEATURE_CONFIG)
+        .put(DefaultServiceFetcher.OSAPI_BASE_ENDPOINTS, new JSONArray(ImmutableList.of(endPoint1)));
+
+    JsonContainerConfig containerConfig =
+        new JsonContainerConfig(config,
+            Expressions.forTesting(new Functions()));
+    CapturingHttpFetcher httpFetcher = new CapturingHttpFetcher();
+    fetcher = new DefaultServiceFetcher(containerConfig, httpFetcher);
+    fetcher.setSecurityTokenCodec( new BasicSecurityTokenCodec() );
+    Multimap<String, String> services = fetcher.getServicesForContainer("default", "dontcare");
+    assertEquals(configuredServices, services);
+    assertNotNull( httpFetcher.request );
+    assertTrue( httpFetcher.request.isInternalRequest() );
+  }
+
+  static class CapturingHttpFetcher implements HttpFetcher {
+
+    public HttpRequest request;
+
+    public CapturingHttpFetcher() {
+    }
+
+    public HttpResponse fetch(HttpRequest request) throws GadgetException {
+      this.request = request;
+      return new HttpResponseBuilder().setHttpStatusCode( HttpResponse.SC_OK )
+                                      .setResponseString( "{\"result\":[]}" ).create();
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/FakeMessageBundleFactory.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/FakeMessageBundleFactory.java
new file mode 100644
index 0000000..e8c239a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/FakeMessageBundleFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import org.apache.shindig.gadgets.MessageBundleFactory;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LocaleSpec;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+import java.util.Locale;
+
+/**
+ * Simple message bundle factory -- only honors inline bundles.
+ */
+public class FakeMessageBundleFactory implements MessageBundleFactory {
+  public MessageBundle getBundle(GadgetSpec spec, Locale locale, boolean ignoreCache, String container, String view) {
+    LocaleSpec localeSpec = spec.getModulePrefs().getLocale(locale, view);
+    if (localeSpec == null) {
+      return MessageBundle.EMPTY;
+    }
+    return spec.getModulePrefs().getLocale(locale, view).getMessageBundle();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java
new file mode 100644
index 0000000..f7523a2
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/HtmlRendererTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.preload.PreloaderService;
+import org.apache.shindig.gadgets.rewrite.CaptureRewriter;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.View;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.concurrent.Callable;
+
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Tests for HtmlRenderer
+ */
+public class HtmlRendererTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private static final String BASIC_HTML_CONTENT = "Hello, World!";
+  private static final String PROXIED_HTML_CONTENT = "Hello, Universe!";
+  private static final Uri PROXIED_HTML_HREF = Uri.parse("http://example.org/proxied.php");
+  private static final GadgetContext CONTEXT = new GadgetContext() {
+    @Override
+    public SecurityToken getToken() {
+      return new AnonymousSecurityToken();
+    }
+  };
+
+  private final FakePreloaderService preloaderService = new FakePreloaderService();
+  private final FakeProxyRenderer proxyRenderer = new FakeProxyRenderer();
+  private final CaptureRewriter captureRewriter = new CaptureRewriter();
+  private HtmlRenderer renderer;
+
+  private Gadget makeGadget(String content) throws GadgetException {
+    GadgetSpec spec = new GadgetSpec(SPEC_URL,
+        "<Module><ModulePrefs title=''/><Content><![CDATA[" + content + "]]></Content></Module>");
+
+    return new Gadget()
+        .setSpec(spec)
+        .setContext(CONTEXT)
+        .setCurrentView(spec.getView("default"));
+  }
+
+  private Gadget makeHrefGadget(String authz) throws Exception {
+    Gadget gadget = makeGadget("");
+    String doc = "<Content href='" + PROXIED_HTML_HREF + "' authz='" + authz + "'/>";
+    View view = new View("proxied", Arrays.asList(XmlUtil.parse(doc)), SPEC_URL);
+    gadget.setCurrentView(view);
+    return gadget;
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    renderer = new HtmlRenderer(preloaderService, proxyRenderer,
+        new GadgetRewritersProvider(ImmutableList.of((GadgetRewriter) captureRewriter)),
+        null);
+
+  }
+
+  @Test
+  public void renderPlainTypeHtml() throws Exception {
+    String content = renderer.render(makeGadget(BASIC_HTML_CONTENT));
+    assertEquals(BASIC_HTML_CONTENT, content);
+  }
+
+  @Test
+  public void renderProxied() throws Exception {
+    String content = renderer.render(makeHrefGadget("none"));
+    assertEquals(PROXIED_HTML_CONTENT, content);
+  }
+
+  @Test
+  public void doPreloading() throws Exception {
+    renderer.render(makeGadget(BASIC_HTML_CONTENT));
+    assertTrue("Preloading not performed.", preloaderService.wasPreloaded);
+  }
+
+  @Test
+  public void doRewriting() throws Exception {
+    renderer.render(makeGadget(BASIC_HTML_CONTENT));
+    assertTrue("Rewriting not performed.", captureRewriter.viewWasRewritten());
+  }
+
+  private static class FakeProxyRenderer extends ProxyRenderer {
+    public FakeProxyRenderer() {
+      super(null, null, null);
+    }
+
+    @Override
+    public String render(Gadget gadget) throws RenderingException, GadgetException {
+      return PROXIED_HTML_CONTENT;
+    }
+  }
+
+  private static class FakePreloaderService implements PreloaderService {
+    protected boolean wasPreloaded;
+    protected Collection<PreloadedData> preloads;
+
+    protected FakePreloaderService() {
+    }
+
+    public Collection<PreloadedData> preload(Gadget gadget) {
+      wasPreloaded = true;
+      return preloads;
+    }
+
+    public Collection<PreloadedData> preload(Collection<Callable<PreloadedData>> tasks) {
+      wasPreloaded = true;
+      return preloads;
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriterTest.java
new file mode 100644
index 0000000..2e08631
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/OpenSocialI18NGadgetRewriterTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+public class OpenSocialI18NGadgetRewriterTest {
+  private OpenSocialI18NGadgetRewriter i18nRewriter;
+  private Locale localeAtRendering;
+
+  @Before
+  public void setUp() throws Exception {
+    i18nRewriter = new FakeOpenSocialI18NGadgetRewriter();
+  }
+
+  @Test
+  public void localeNameForEnglish() throws Exception {
+    localeAtRendering = new Locale("en");
+    assertEquals("en",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForEnglishUS() throws Exception {
+    localeAtRendering = new Locale("en", "US");
+    assertEquals("en_US",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForChinese() throws Exception {
+    localeAtRendering = new Locale("zh");
+    assertEquals("zh",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForChineseCN() throws Exception {
+    localeAtRendering = new Locale("zh", "CN");
+    assertEquals("zh_CN",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForChineseAll() throws Exception {
+    localeAtRendering = new Locale("zh", "All");
+    assertEquals("zh",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForAllCN() throws Exception {
+    localeAtRendering = new Locale("All", "CN");
+    assertEquals("en",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForDefault() throws Exception {
+    localeAtRendering = new Locale("All", "All");
+    assertEquals("en",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForInvalidCountry() throws Exception {
+    localeAtRendering = new Locale("zh", "foo");
+    assertEquals("zh",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForInvalidLanguage() throws Exception {
+    localeAtRendering = new Locale("foo", "CN");
+    assertEquals("en",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  @Test
+  public void localeNameForInvalidLanguageAndCountry() throws Exception {
+    localeAtRendering = new Locale("foo", "foo");
+    assertEquals("en",
+                 i18nRewriter.getLocaleNameForLoadingI18NConstants(localeAtRendering));
+  }
+
+  private static class FakeOpenSocialI18NGadgetRewriter extends OpenSocialI18NGadgetRewriter {
+    private Map<String, String> resources = new HashMap<String,String>();
+    public FakeOpenSocialI18NGadgetRewriter() {
+      resources.put("features/i18n/data/DateTimeConstants__en.js", "content for en");
+      resources.put("features/i18n/data/DateTimeConstants__en_US.js", "content for en_US");
+      resources.put("features/i18n/data/DateTimeConstants__zh.js", "content for zh");
+      resources.put("features/i18n/data/DateTimeConstants__zh_CN.js", "content for zh_CN");
+    }
+
+    @Override
+    protected String attemptToLoadDateConstants(String localeName) throws IOException {
+      String resource = "features/i18n/data/DateTimeConstants__" + localeName + ".js";
+      if (resources.containsKey(resource)) {
+        return resources.get(resource);
+      } else {
+        throw new IOException("Resource Unavailable.");
+      }
+    }
+  }
+}
+
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/ProxyRendererTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/ProxyRendererTest.java
new file mode 100644
index 0000000..f20a57f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/ProxyRendererTest.java
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.JsonSerializer;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.AbstractHttpCache;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.preload.PipelineExecutor;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.View;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+
+/**
+ * Tests for ProxyRenderer.
+ */
+public class ProxyRendererTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private static final String PROXIED_HTML_CONTENT = "Hello, Universe!";
+  private static final Uri PROXIED_HTML_HREF = Uri.parse("http://example.org/proxied.php");
+  private static final Uri EXPECTED_PROXIED_HTML_HREF
+      = Uri.parse("http://example.org/proxied.php?lang=all&country=ALL");
+  private static final String USER_AGENT = "TestUserAgent/1.0";
+  private static final String USER_AGENT_SET = "TestUserAgent/1.0 Shindig";
+  private static final GadgetContext CONTEXT = new GadgetContext() {
+    @Override
+    public SecurityToken getToken() {
+      return new AnonymousSecurityToken();
+    }
+
+    @Override
+    public String getUserAgent() {
+      return USER_AGENT;
+    }
+  };
+
+  private final FakeHttpCache cache = new FakeHttpCache();
+  private final FakeRequestPipeline pipeline = new FakeRequestPipeline();
+  private final FakePipelineExecutor pipelineExecutor = new FakePipelineExecutor();
+  private final ProxyRenderer proxyRenderer = new ProxyRenderer(pipeline,
+      cache, pipelineExecutor);
+
+  private Gadget makeGadget(String content) throws GadgetException {
+    GadgetSpec spec = new GadgetSpec(SPEC_URL,
+        "<Module><ModulePrefs title=''/><Content><![CDATA[" + content + "]]></Content></Module>");
+
+    return new Gadget()
+        .setSpec(spec)
+        .setContext(CONTEXT)
+        .setCurrentView(spec.getView("default"));
+  }
+
+  private Gadget makeHrefGadget(String authz) throws Exception {
+    Gadget gadget = makeGadget("");
+    String doc = "<Content href='" + PROXIED_HTML_HREF + "' authz='" + authz + "'/>";
+    View view = new View("proxied", Arrays.asList(XmlUtil.parse(doc)), SPEC_URL);
+    gadget.setCurrentView(view);
+    return gadget;
+  }
+
+  @Test
+  public void renderProxied() throws Exception {
+    HttpRequest request = new HttpRequest(EXPECTED_PROXIED_HTML_HREF);
+    request.setHeader("User-Agent", USER_AGENT_SET);
+    HttpResponse response = new HttpResponse(PROXIED_HTML_CONTENT);
+    pipeline.plainResponses.put(EXPECTED_PROXIED_HTML_HREF, response);
+
+    String content = proxyRenderer.render(makeHrefGadget("none"));
+    assertEquals(PROXIED_HTML_CONTENT, content);
+    assertEquals(response, cache.getResponse(request));
+  }
+
+  @Test
+  public void renderProxiedRelative() throws Exception {
+    Uri base = EXPECTED_PROXIED_HTML_HREF;
+    final Uri relative = Uri.parse("/some/path?foo=bar");
+    Uri resolved = new UriBuilder(base.resolve(relative))
+      .addQueryParameter("lang", GadgetSpec.DEFAULT_LOCALE.getLanguage())
+      .addQueryParameter("country", GadgetSpec.DEFAULT_LOCALE.getCountry())
+      .toUri();
+
+    HttpRequest request = new HttpRequest(resolved);
+    request.setHeader("User-Agent", USER_AGENT_SET);
+    HttpResponse response = new HttpResponse(PROXIED_HTML_CONTENT);
+
+    pipeline.plainResponses.put(resolved, response);
+
+    Gadget gadget = makeHrefGadget("none");
+    gadget.setContext(new GadgetContext(gadget.getContext()) {
+      @Override
+      public String getParameter(String name) {
+        return name.equals(HtmlRenderer.PATH_PARAM) ? relative.toString() : null;
+      }
+    });
+
+    String content = proxyRenderer.render(gadget);
+    assertEquals(PROXIED_HTML_CONTENT, content);
+    assertEquals(response, cache.getResponse(request));
+  }
+
+  @Test
+  public void renderProxiedRelativeBadPath() throws Exception {
+    HttpRequest request = new HttpRequest(EXPECTED_PROXIED_HTML_HREF);
+    request.setHeader("User-Agent", USER_AGENT_SET);
+    HttpResponse response = new HttpResponse(PROXIED_HTML_CONTENT);
+    pipeline.plainResponses.put(EXPECTED_PROXIED_HTML_HREF, response);
+
+    Gadget gadget = makeHrefGadget("none");
+    gadget.setContext(new GadgetContext(gadget.getContext()) {
+      @Override
+      public String getParameter(String name) {
+        return name.equals(HtmlRenderer.PATH_PARAM) ? "$(^)$" : null;
+      }
+    });
+
+    String content = proxyRenderer.render(gadget);
+
+    assertEquals(PROXIED_HTML_CONTENT, content);
+    assertEquals(response, cache.getResponse(request));
+  }
+
+  @Test
+  public void renderProxiedFromCache() throws Exception {
+    HttpRequest request = new HttpRequest(EXPECTED_PROXIED_HTML_HREF);
+    request.setHeader("User-Agent", USER_AGENT_SET);
+    HttpResponse response = new HttpResponse(PROXIED_HTML_CONTENT);
+    cache.addResponse(request, response);
+    String content = proxyRenderer.render(makeHrefGadget("none"));
+    assertEquals(PROXIED_HTML_CONTENT, content);
+  }
+
+  @Test
+  public void renderProxiedSigned() throws Exception {
+    pipeline.signedResponses.put(EXPECTED_PROXIED_HTML_HREF, new HttpResponse(PROXIED_HTML_CONTENT));
+    String content = proxyRenderer.render(makeHrefGadget("signed"));
+    assertEquals(PROXIED_HTML_CONTENT, content);
+  }
+
+  @Test
+  public void renderProxiedOAuth() throws Exception {
+    // TODO: We need to disambiguate between oauth and signed.
+    pipeline.oauthResponses.put(EXPECTED_PROXIED_HTML_HREF, new HttpResponse(PROXIED_HTML_CONTENT));
+    String content = proxyRenderer.render(makeHrefGadget("oauth"));
+    assertEquals(PROXIED_HTML_CONTENT, content);
+  }
+
+  @Test
+  public void renderProxiedCustomLocale() throws Exception {
+    UriBuilder uri = new UriBuilder(PROXIED_HTML_HREF);
+    uri.putQueryParameter("lang", "foo");
+    uri.putQueryParameter("country", "BAR");
+
+    Gadget gadget = makeHrefGadget("none");
+    gadget.setContext(new GadgetContext() {
+      @Override
+      public Locale getLocale() {
+        return new Locale("foo", "BAR");
+      }
+
+      @Override
+      public SecurityToken getToken() {
+        return new AnonymousSecurityToken();
+      }
+    });
+
+    pipeline.plainResponses.put(uri.toUri(), new HttpResponse(PROXIED_HTML_CONTENT));
+    String content = proxyRenderer.render(gadget);
+    assertEquals(PROXIED_HTML_CONTENT, content);
+  }
+
+  @Test
+  public void renderProxiedWithPreload() throws Exception {
+    List<JSONObject> prefetchedJson = ImmutableList.of(new JSONObject("{id: 'foo', data: 'bar'}"));
+
+    pipelineExecutor.results = new PipelineExecutor.Results(null, prefetchedJson, null);
+
+    pipeline.plainResponses.put(EXPECTED_PROXIED_HTML_HREF, new HttpResponse(PROXIED_HTML_CONTENT));
+
+    String content = proxyRenderer.render(makeHrefGadget("none"));
+    assertEquals(PROXIED_HTML_CONTENT, content);
+
+    HttpRequest lastHttpRequest = pipeline.getLastHttpRequest();
+    assertEquals("POST", lastHttpRequest.getMethod());
+    assertEquals("application/json;charset=utf-8", lastHttpRequest.getHeader("Content-Type"));
+    String postBody = lastHttpRequest.getPostBodyAsString();
+
+    JsonAssert.assertJsonEquals(JsonSerializer.serialize(prefetchedJson), postBody);
+    assertTrue(pipelineExecutor.wasPreloaded);
+  }
+
+  @Test
+  public void appendUserAgent() throws Exception {
+    String expectedUA = USER_AGENT + " Shindig";
+    HttpResponse response = new HttpResponse(PROXIED_HTML_CONTENT);
+    pipeline.plainResponses.put(EXPECTED_PROXIED_HTML_HREF, response);
+
+    proxyRenderer.render(makeHrefGadget("none"));
+    String actualUA = pipeline.lastHttpRequest.getHeader("User-Agent");
+    assertEquals(expectedUA, actualUA);
+  }
+
+  private static class FakeHttpCache extends AbstractHttpCache {
+    private final Map<String, HttpResponse> map = Maps.newHashMap();
+
+    protected FakeHttpCache() {
+    }
+
+    @Override
+    protected void addResponseImpl(String key, HttpResponse response) {
+      map.put(key, response);
+    }
+
+    @Override
+    protected HttpResponse getResponseImpl(String key) {
+      return map.get(key);
+    }
+
+    @Override
+    protected void removeResponseImpl(String key) {
+      map.remove(key);
+    }
+  }
+
+  private static class FakeRequestPipeline implements RequestPipeline {
+    protected final Map<Uri, HttpResponse> plainResponses = Maps.newHashMap();
+    protected final Map<Uri, HttpResponse> signedResponses = Maps.newHashMap();
+    protected final Map<Uri, HttpResponse> oauthResponses = Maps.newHashMap();
+    private HttpRequest lastHttpRequest;
+
+    protected FakeRequestPipeline() {
+    }
+
+    public HttpResponse execute(HttpRequest request) throws GadgetException {
+      lastHttpRequest = request;
+
+      if (request.getGadget() == null) {
+        throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
+            "No gadget associated with rendering request.");
+      }
+
+      if (request.getContainer() == null) {
+        throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
+            "No container associated with rendering request.");
+      }
+
+      if (request.getSecurityToken() == null) {
+        throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
+            "No security token associated with rendering request.");
+      }
+
+      if (request.getOAuthArguments() == null) {
+        throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
+            "No oauth arguments associated with rendering request.");
+      }
+
+      assertTrue(request.getOAuthArguments().isProxiedContentRequest());
+
+      HttpResponse response;
+      switch (request.getAuthType()) {
+        case NONE:
+          response = plainResponses.get(request.getUri());
+          break;
+        case SIGNED:
+          response = signedResponses.get(request.getUri());
+          break;
+        case OAUTH:
+          response = oauthResponses.get(request.getUri());
+          break;
+        default:
+          response = null;
+          break;
+      }
+      if (response == null) {
+        throw new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT,
+            "Unknown file: " + request.getUri());
+      }
+      return response;
+    }
+
+    public HttpRequest getLastHttpRequest() {
+      return lastHttpRequest;
+    }
+  }
+
+  private static class FakePipelineExecutor extends PipelineExecutor {
+    protected boolean wasPreloaded;
+    protected Results results;
+
+    public FakePipelineExecutor() {
+      super(null, null, null);
+    }
+
+    @Override
+    public Results execute(GadgetContext context, Collection<PipelinedData> pipelines) {
+      wasPreloaded = true;
+      return results;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/RendererTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/RendererTest.java
new file mode 100644
index 0000000..2644ab0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/RendererTest.java
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.View;
+
+import com.google.common.collect.Maps;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Tests for Renderer.
+ */
+public class RendererTest {
+  protected static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private static final Uri TYPE_URL_HREF = Uri.parse("http://example.org/gadget.php");
+  private static final String BASIC_HTML_CONTENT = "Hello, World!";
+  protected static final String GADGET =
+      "<Module>" +
+      " <ModulePrefs title='foo'/>" +
+      " <Content view='html' type='html'>" + BASIC_HTML_CONTENT + "</Content>" +
+      " <Content view='url' type='url' href='" + TYPE_URL_HREF + "'/>" +
+      "</Module>";
+  protected static final String GADGET_CAJA =
+    "<Module>" +
+    " <ModulePrefs title='foo'>" +
+    "   <Require feature='caja'/>" +
+    " </ModulePrefs>" +
+    " <Content view='html' type='html'>" + BASIC_HTML_CONTENT + "</Content>" +
+    " <Content view='url' type='url' href='" + TYPE_URL_HREF + "'/>" +
+    "</Module>";
+
+  private final FakeHtmlRenderer htmlRenderer = new FakeHtmlRenderer();
+  private final FakeProcessor processor = new FakeProcessor();
+  private final FakeLockedDomainService lockedDomainService =  new FakeLockedDomainService();
+  private FakeContainerConfig containerConfig;
+  private Renderer renderer;
+
+  @Before
+  public void setUp() throws Exception {
+    containerConfig = new FakeContainerConfig();
+    renderer = new Renderer(processor, htmlRenderer, containerConfig, lockedDomainService);
+  }
+
+  private GadgetContext makeContext(final String view) {
+    return makeContext(view, null, null);
+  }
+
+  private GadgetContext makeContext(final String view, final String sanitize, final String caja) {
+    return new GadgetContext() {
+      @Override
+      public String getView() {
+        return view;
+      }
+
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("parent")) {
+          return "http://example.org/foo";
+        } else if (name.equals("sanitize")) {
+          return sanitize;
+        } else if (name.equals("caja")) {
+          return caja;
+        }
+        return null;
+      }
+    };
+  }
+
+  @Test
+  public void renderTypeHtml() {
+    RenderingResults results = renderer.render(makeContext("html"));
+    assertEquals(RenderingResults.Status.OK, results.getStatus());
+    assertEquals(BASIC_HTML_CONTENT, results.getContent());
+  }
+
+  @Test
+  public void renderTypeUrl() {
+    RenderingResults results = renderer.render(makeContext("url"));
+    assertEquals(RenderingResults.Status.MUST_REDIRECT, results.getStatus());
+    assertEquals(TYPE_URL_HREF, results.getRedirect());
+  }
+
+  @Test
+  public void renderTypeUrlRequiresCajaIncompatible() {
+    processor.setGadgetData(GADGET_CAJA);
+    RenderingResults results = renderer.render(makeContext("url"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, results.getHttpStatusCode());
+  }
+
+  @Test
+  public void renderTypeUrlCajaParamIncompatible() {
+    RenderingResults results = renderer.render(makeContext("url", null, "1"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, results.getHttpStatusCode());
+  }
+
+  @Test
+  public void renderTypeUrlSanitizedIncompatible() {
+    RenderingResults results = renderer.render(makeContext("url", "1", null));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, results.getHttpStatusCode());
+  }
+
+  @Test
+  public void handlesProcessingExceptionGracefully() {
+    processor.exception = new ProcessingException("foo", HttpServletResponse.SC_FORBIDDEN);
+    RenderingResults results = renderer.render(makeContext("html"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertEquals("foo", results.getErrorMessage());
+    assertEquals(HttpServletResponse.SC_FORBIDDEN, results.getHttpStatusCode());
+  }
+
+  @Test
+  public void handlesRenderingExceptionGracefully() {
+    htmlRenderer.exception = new RenderingException("four-oh-four", HttpServletResponse.SC_NOT_FOUND);
+    RenderingResults results = renderer.render(makeContext("html"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertEquals("four-oh-four", results.getErrorMessage());
+    assertEquals(HttpServletResponse.SC_NOT_FOUND, results.getHttpStatusCode());
+  }
+
+  @Test
+  public void handlesRuntimeWrappedGadgetExceptionGracefully() {
+    htmlRenderer.runtimeException = new RuntimeException(
+        new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, "oh no!"));
+    RenderingResults results = renderer.render(makeContext("html"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertEquals("oh no!", results.getErrorMessage());
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void otherRuntimeExceptionsThrow() {
+    htmlRenderer.runtimeException = new RuntimeException("Help!");
+    renderer.render(makeContext("html"));
+  }
+
+  @Test
+  public void validateParent() throws Exception {
+    containerConfig.data.put("gadgets.parent",
+        Arrays.asList("http:\\/\\/example\\.org\\/[a-z]+", "localhost"));
+
+    RenderingResults results = renderer.render(makeContext("html"));
+    assertEquals(RenderingResults.Status.OK, results.getStatus());
+  }
+
+  @Test
+  public void validateBadParent() throws Exception {
+    containerConfig.data.put("gadgets.parent",
+        Arrays.asList("http:\\/\\/example\\.com\\/[a-z]+", "localhost"));
+    RenderingResults results = renderer.render(makeContext("html"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertNotNull("No error message provided for bad parent.", results.getErrorMessage());
+  }
+
+  @Test
+  public void handlesNoCurrentViewGracefully() throws Exception {
+    RenderingResults results = renderer.render(makeContext("bad-view-name"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+    assertNotNull("No error message for missing current view", results.getErrorMessage());
+  }
+
+  @Test
+  public void verifyLockedDomain() throws Exception {
+    renderer.render(makeContext("html"));
+    assertTrue("Locked domain not verified", lockedDomainService.wasChecked);
+  }
+
+  @Test
+  public void wrongDomainFails() throws Exception {
+    lockedDomainService.canRender = false;
+    RenderingResults results = renderer.render(makeContext("html"));
+    assertEquals(RenderingResults.Status.ERROR, results.getStatus());
+  }
+
+  private static class FakeContainerConfig extends BasicContainerConfig {
+    protected final Map<String, Object> data = Maps.newHashMap();
+
+    @Override
+    public Object getProperty(String container, String name) {
+      return data.get(name);
+    }
+  }
+
+  private static class FakeHtmlRenderer extends HtmlRenderer {
+    protected RenderingException exception;
+    protected RuntimeException runtimeException;
+
+    public FakeHtmlRenderer() {
+      super(null, null, null, null);
+    }
+
+    @Override
+    public String render(Gadget gadget) throws RenderingException {
+      if (exception != null) {
+        throw exception;
+      }
+      if (runtimeException != null) {
+        throw runtimeException;
+      }
+      return gadget.getCurrentView().getContent();
+    }
+  }
+
+  private static class FakeProcessor extends Processor {
+    protected ProcessingException exception;
+    private String gadgetData;
+
+    public FakeProcessor() {
+      super(null, null, null, null, null);
+      this.gadgetData = GADGET;
+    }
+
+    public void setGadgetData(String gadgetData) {
+      this.gadgetData = gadgetData;
+    }
+
+    @Override
+    public Gadget process(GadgetContext context) throws ProcessingException {
+      if (exception != null) {
+        throw exception;
+      }
+      try {
+        GadgetSpec spec = new GadgetSpec(SPEC_URL, gadgetData);
+        View view = spec.getView(context.getView());
+        return new Gadget()
+            .setContext(context)
+            .setSpec(spec)
+            .setCurrentView(view);
+      } catch (GadgetException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  private static class FakeLockedDomainService implements LockedDomainService {
+    protected boolean wasChecked = false;
+    protected boolean canRender = true;
+
+    protected FakeLockedDomainService() {
+    }
+
+    public boolean isGadgetValidForHost(String host, Gadget gadget, String container) {
+      wasChecked = true;
+      return canRender;
+    }
+
+    public String getLockedDomainForGadget(Gadget gadget, String container) {
+      return null;
+    }
+
+    public boolean isSafeForOpenProxy(String host) {
+      return false;
+    }
+
+    public boolean isEnabled() {
+      return false;
+    }
+
+    public boolean isHostUsingLockedDomain(String host) {
+      return false;
+    }
+
+    public boolean isRefererCheckEnabled() {
+        return false;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriterTest.java
new file mode 100644
index 0000000..a872240
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/RenderingGadgetRewriterTest.java
@@ -0,0 +1,1356 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.apache.shindig.gadgets.render.RenderingGadgetRewriter.DEFAULT_CSS;
+import static org.apache.shindig.gadgets.render.RenderingGadgetRewriter.FEATURES_KEY;
+import static org.apache.shindig.gadgets.render.RenderingGadgetRewriter.INSERT_BASE_ELEMENT_KEY;
+import static org.apache.shindig.gadgets.render.RenderingGadgetRewriter.IS_GADGET_BEACON;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.same;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.config.ConfigContributor;
+import org.apache.shindig.gadgets.config.CoreUtilConfigContributor;
+import org.apache.shindig.gadgets.config.DefaultConfigProcessor;
+import org.apache.shindig.gadgets.config.XhrwrapperConfigContributor;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.js.JsException;
+import org.apache.shindig.gadgets.js.JsRequest;
+import org.apache.shindig.gadgets.js.JsResponseBuilder;
+import org.apache.shindig.gadgets.js.JsServingPipeline;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.preload.PreloadException;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+/**
+ * Tests for RenderingContentRewriter.
+ */
+public class RenderingGadgetRewriterTest extends EasyMockTestCase{
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private static final String BODY_CONTENT = "Some body content";
+  static final Pattern DOCUMENT_SPLIT_PATTERN = Pattern.compile(
+      "(.*)<head>(.*?)<\\/head>(?:.*)<body(.*?)>(.*?)<\\/body>(?:.*)", Pattern.DOTALL |
+      Pattern.CASE_INSENSITIVE);
+  private static final String CUSTOM_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">";
+  private static final String CUSTOM_DOCTYPE_QNAME = "html";
+  private static final String CUSTOM_DOCTYPE_PUBID = "-//W3C//DTD HTML 4.01 Transitional//EN";
+  private static final String CUSTOM_DOCTYPE_SYSID = "http://www.w3.org/TR/html4/loose.dtd";
+
+  static final int BEFORE_HEAD_GROUP = 1;
+  static final int HEAD_GROUP = 2;
+  static final int BODY_ATTRIBUTES_GROUP = 3;
+  static final int BODY_GROUP = 4;
+
+  private final FakeMessageBundleFactory messageBundleFactory = new FakeMessageBundleFactory();
+  private final FakeContainerConfig config = new FakeContainerConfig();
+  private final JsUriManager jsUriManager = new FakeJsUriManager();
+  private final MapGadgetContext context = new MapGadgetContext();
+  private final GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
+
+  private FeatureRegistry featureRegistry;
+  private JsServingPipeline jsServingPipeline;
+  private RenderingGadgetRewriter rewriter;
+  private GadgetHtmlParser parser;
+  private Expressions expressions;
+
+  @Before
+  public void setUp() throws Exception {
+    expressions = Expressions.forTesting();
+    featureRegistry = createMock(FeatureRegistry.class);
+    FeatureRegistryProvider featureRegistryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) {
+        return featureRegistry;
+      }
+    };
+    jsServingPipeline = createMock(JsServingPipeline.class);
+    Map<String, ConfigContributor> configContributors = ImmutableMap.of(
+        "core.util", new CoreUtilConfigContributor(featureRegistry,
+                gadgetAdminStore),
+        "shindig.xhrwrapper", new XhrwrapperConfigContributor()
+    );
+    rewriter
+        = new RenderingGadgetRewriter(messageBundleFactory, expressions, config, featureRegistryProvider,
+            jsServingPipeline, jsUriManager,
+            new DefaultConfigProcessor(configContributors, config), gadgetAdminStore);
+    Injector injector = Guice.createInjector(new ParseModule(), new PropertiesModule());
+    parser = injector.getInstance(GadgetHtmlParser.class);
+  }
+
+  private Gadget makeGadgetWithSpec(String gadgetXml) throws GadgetException {
+    GadgetSpec spec = new GadgetSpec(SPEC_URL, gadgetXml);
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setPreloads(ImmutableList.<PreloadedData>of())
+        .setSpec(spec)
+        .setCurrentView(spec.getView(GadgetSpec.DEFAULT_VIEW))
+        .setGadgetFeatureRegistry(featureRegistry);
+
+    // Convenience: by default expect no features requested, by gadget or extern.
+    // expectFeatureCalls(...) resets featureRegistry if called again.
+    expectFeatureCalls(gadget,
+        ImmutableList.<FeatureResource>of(),
+        ImmutableSet.<String>of(),
+        ImmutableList.<FeatureResource>of());
+
+    //Convenience: by default expect that the gadget is allowed to render
+    reset(gadgetAdminStore);
+    expect(gadgetAdminStore.checkFeatureAdminInfo(isA(Gadget.class))).andReturn(true);
+    expect(gadgetAdminStore.isAllowedFeature(isA(Feature.class), isA(Gadget.class)))
+    .andReturn(true).anyTimes();
+    replay(gadgetAdminStore);
+    return gadget;
+  }
+
+  private Gadget makeDefaultGadget() throws GadgetException {
+    String defaultXml = "<Module><ModulePrefs title=''/><Content type='html'/></Module>";
+    return makeGadgetWithSpec(defaultXml);
+  }
+
+  private Gadget makeDefaultOpenSocial2Gadget(boolean useQuirks) throws GadgetException {
+    String defaultXml = "<Module specificationVersion='2' ><ModulePrefs " + (useQuirks ? "doctype='quirksmode'" : "") +" title=''/><Content type='html'/></Module>";
+    return makeGadgetWithSpec(defaultXml);
+  }
+
+  private String rewrite(Gadget gadget, String content) throws Exception {
+    MutableContent mc = new MutableContent(parser, content);
+    rewriter.rewrite(gadget, mc);
+    return mc.getContent();
+  }
+
+  @Test
+  public void defaultOutput() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    assertTrue("Missing opening html tag", matcher.group(BEFORE_HEAD_GROUP).
+        toLowerCase().contains("<html"));
+    assertTrue("Default CSS missing.", matcher.group(HEAD_GROUP).contains(DEFAULT_CSS));
+    // Not very accurate -- could have just been user prefs.
+    assertTrue("Default javascript not included.",
+        matcher.group(HEAD_GROUP).contains("<script>"));
+    assertTrue("Original document not preserved.",
+        matcher.group(BODY_GROUP).contains(BODY_CONTENT));
+    assertTrue("gadgets.util.runOnLoadHandlers not invoked.",
+        matcher.group(BODY_GROUP).contains("gadgets.util.runOnLoadHandlers();"));
+  }
+
+  @Test
+  public void overrideDefaultDoctype() throws Exception{
+    Gadget gadget = makeDefaultOpenSocial2Gadget(false);
+    String body = "hello, world.";
+    String doc = new StringBuilder()
+        .append("<html><head>")
+        .append("</head><body>")
+        .append(body)
+        .append("</body></html>")
+        .toString();
+
+    rewriter.setDefaultDoctypeQName(CUSTOM_DOCTYPE_QNAME);
+    rewriter.setDefaultDoctypePubId(CUSTOM_DOCTYPE_PUBID);
+    rewriter.setDefaultDoctypeSysId(CUSTOM_DOCTYPE_SYSID);
+    String rewritten = rewrite(gadget, doc);
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    assertTrue("DOCTYPE not preserved", matcher.group(BEFORE_HEAD_GROUP).contains(CUSTOM_DOCTYPE));
+
+  }
+
+  @Test
+  public void quirksmodeInOS2() throws Exception{
+    Gadget gadget = makeDefaultOpenSocial2Gadget(true);
+    String body = "hello, world.";
+    String doc = new StringBuilder()
+        .append("<html><head>")
+        .append("</head><body>")
+        .append(body)
+        .append("</body></html>")
+        .toString();
+
+    String rewritten = rewrite(gadget, doc);
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    assertTrue("Should not include doctype, this will default to quirksmode (old Shindig behavior)", !matcher.group(BEFORE_HEAD_GROUP).contains("<!DOCTYPE"));
+
+    gadget = makeDefaultOpenSocial2Gadget(true);
+    String docType = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">";
+    doc = new StringBuilder()
+        .append(docType)
+        .append("<html><head>")
+        .append("</head><body>")
+        .append(body)
+        .append("</body></html>")
+        .toString();
+    rewritten = rewrite(gadget, doc);
+
+    matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    assertTrue("Should include doctype, when in quirksmode we should use pre OS2.0 Shindig behavior.", matcher.group(BEFORE_HEAD_GROUP).contains(docType));
+
+
+  }
+
+  @Test
+  public void completeDocument() throws Exception {
+    String docType = "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">";
+    String head = "<script src=\"foo.js\"></script><style type=\"text/css\">body{color:red;}</style>";
+    String bodyAttr = " onload=\"foo();\"";
+    String body = "hello, world.";
+    String doc = new StringBuilder()
+        .append(docType)
+        .append("<html><head>")
+        .append(head)
+        .append("</head><body").append(bodyAttr).append('>')
+        .append(body)
+        .append("</body></html>")
+        .toString();
+
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return "foo";
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeDefaultGadget()
+        .setContext(context);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.<FeatureResource>of(),
+        ImmutableSet.of("foo"),
+        ImmutableList.of(inline("blah", "n/a")));
+
+    String rewritten = rewrite(gadget, doc);
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    assertTrue("DOCTYPE not preserved", matcher.group(BEFORE_HEAD_GROUP).contains(docType));
+    assertTrue("Missing opening html tag", matcher.group(BEFORE_HEAD_GROUP).contains("<html"));
+    // TODO: reinstate test when non-tag-reordering parser is used.
+    // assertTrue("Custom head content is missing.", matcher.group(HEAD_GROUP).contains(head));
+    assertTrue("IsGadget beacon not included.",
+        matcher.group(HEAD_GROUP).contains("<script>" + IS_GADGET_BEACON + "</script>"));
+    assertTrue("Forced javascript not included.",
+        matcher.group(HEAD_GROUP).contains("<script src=\"/js/foo?jsload=0\">"));
+    assertFalse("Default styling was injected when a doctype was specified.",
+        matcher.group(HEAD_GROUP).contains(DEFAULT_CSS));
+    assertTrue("Custom body attributes missing.",
+        matcher.group(BODY_ATTRIBUTES_GROUP).contains(bodyAttr));
+    assertTrue("Original document not preserved.",
+        matcher.group(BODY_GROUP).contains(body));
+    assertTrue("gadgets.util.runOnLoadHandlers not invoked.",
+        matcher.group(BODY_GROUP).contains("gadgets.util.runOnLoadHandlers();"));
+
+    // Skipping other tests; code path should be the same for the rest.
+  }
+
+  @Test
+  public void completeDocumentOpenSocial2() throws Exception {
+    String head = "<script src=\"foo.js\"></script><style type=\"text/css\">body{color:red;}</style>";
+    String bodyAttr = " onload=\"foo();\"";
+    String body = "hello, world.";
+    String doc = new StringBuilder()
+        .append("<html><head>")
+        .append(head)
+        .append("</head><body").append(bodyAttr).append('>')
+        .append(body)
+        .append("</body></html>")
+        .toString();
+
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return "foo";
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeDefaultOpenSocial2Gadget(false)
+        .setContext(context);
+    expectFeatureCalls(gadget,
+        ImmutableList.<FeatureResource>of(),
+        ImmutableSet.of("foo"),
+        ImmutableList.of(inline("blah", "n/a")));
+
+    String rewritten = rewrite(gadget, doc);
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    assertTrue("Doctype should have been rewritten to HTML5", matcher.group(BEFORE_HEAD_GROUP).contains("<!DOCTYPE html>"));
+
+    // Skipping other tests; code path should be the same for the rest.
+  }
+
+  @Test
+  public void bidiSettings() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Locale language_direction='rtl'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    String rewritten = rewrite(gadget, "");
+
+    assertTrue("Bi-directional locale settings not preserved.",
+        rewritten.contains("<body dir=\"rtl\">"));
+  }
+
+  private Set<String> getInjectedScript(String content) {
+    Pattern featurePattern
+        = Pattern.compile("(?:.*)<script src=\"\\/js\\/(.*?)\\?jsload=0\"><\\/script>(?:.*)", Pattern.DOTALL);
+    Matcher matcher = featurePattern.matcher(content);
+
+    assertTrue("Forced scripts not injected.", matcher.matches());
+
+    return Sets.newHashSet(matcher.group(1).split(":"));
+  }
+
+  @Test
+  public void forcedFeaturesInjectedExternal() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    final Set<String> libs = ImmutableSortedSet.of("foo", "bar", "baz");
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return Joiner.on(':').join(libs);
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml).setContext(context);
+
+    FeatureResource fooResource = inline("foo-content", "foo-debug");
+    expectFeatureCalls(gadget,
+        ImmutableList.of(fooResource),
+        libs,
+        ImmutableList.of(fooResource, inline("bar-c", "bar-d"), inline("baz-c", "baz-d")));
+
+    String rewritten = rewrite(gadget, "");
+
+    Set<String> actual = getInjectedScript(rewritten);
+    Set<String> expected = ImmutableSortedSet.of("foo", "bar", "baz");
+    assertEquals(expected, actual);
+  }
+
+  @Test
+  public void inlinedFeaturesWhenNothingForced() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo_content();", "foo_content_debug();")),
+        ImmutableSet.<String>of(),
+        ImmutableList.<FeatureResource>of());
+
+    String rewritten = rewrite(gadget, "");
+
+    assertTrue("Requested scripts not inlined.", rewritten.contains("foo_content();"));
+  }
+
+  @Test
+  public void featuresNotInjectedWhenRemoved() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+    gadget.removeFeature("foo");
+
+    expectFeatureCalls(gadget,
+        ImmutableList.<FeatureResource>of(),
+        ImmutableSet.<String>of(),
+        ImmutableList.<FeatureResource>of());
+
+    String rewritten = rewrite(gadget, "");
+
+    assertFalse("Removed script still inlined.", rewritten.contains("foo_content();"));
+  }
+
+  @Test
+  public void featuresInjectedWhenAdded() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+    gadget.addFeature("foo");
+    // add non existing feature,
+    gadget.addFeature("do-not-exists");
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo_content();", "foo_content_dbg();")),
+        ImmutableSet.<String>of(),
+        ImmutableList.<FeatureResource>of());
+
+    String rewritten = rewrite(gadget, "");
+
+    assertTrue("Added script not inlined.", rewritten.contains("foo_content();"));
+  }
+
+  @Test
+  public void mixedExternalAndInline() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    final Set<String> libs = ImmutableSet.of("bar", "baz");
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return Joiner.on(':').join(libs);
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml).setContext(context);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo_content();", "foo_content_debug();")),
+        libs,
+        ImmutableList.of(inline("bar-c", "bar-d"), inline("baz-c", "baz-d")));
+
+    String rewritten = rewrite(gadget, "");
+
+    Set<String> actual = getInjectedScript(rewritten);
+    Set<String> expected = ImmutableSortedSet.of("bar", "baz");
+    assertEquals(expected, actual);
+    assertTrue("Requested scripts not inlined.", rewritten.contains("foo_content();"));
+  }
+
+  @Test
+  public void featuresInjectedBeforeExistingScript() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+
+    String rewritten = rewrite(gadget,
+        "<html><head><script src=\"foo.js\"></script></head><body>hello</body></html>");
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+
+    String headContent = matcher.group(HEAD_GROUP);
+
+    // Locate user script.
+    int userPosition = headContent.indexOf("<script src=\"foo.js\"></script>");
+
+    // Anything else here, we added.
+    int ourPosition = headContent.indexOf("<script>");
+
+    // TODO: restore when moved to a non-tag-shifting HTML parser (userPosition == -1 in body)
+    // assertTrue("Injected script must come before user script.", ourPosition < userPosition);
+  }
+
+  @Test
+  public void featuresDeclaredBeforeUsed() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo_content();", "foo_content_debug();")),
+        ImmutableSet.<String>of(),
+        ImmutableList.<FeatureResource>of());
+
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+
+    String headContent = matcher.group(HEAD_GROUP);
+
+    // Locate user script.
+    int declaredPosition = headContent.indexOf("foo_content();");
+    assertTrue(declaredPosition >= 0);
+
+    // Anything else here, we added.
+    int usedPosition = headContent.indexOf("gadgets.Prefs.setMessages_");
+    assertTrue(usedPosition >= 0);
+
+    assertTrue("Inline JS needs to exist before it is used.", declaredPosition < usedPosition);
+  }
+
+  @Test
+  public void urlFeaturesForcedExternal() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "  <Require feature='bar'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return "baz";
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml).setContext(context);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo_content();", "foo_content_debug();"),
+                         extern("http://example.org/external.js", "dbg")),
+        ImmutableSet.of("baz"),
+        ImmutableList.of(inline("does-not-matter", "dbg")));
+
+    String rewritten = rewrite(gadget, "");
+
+    Set<String> actual = getInjectedScript(rewritten);
+    Set<String> expected = ImmutableSortedSet.of("baz");
+    assertEquals(expected, actual);
+    assertTrue("Requested scripts not inlined.", rewritten.contains("foo_content();"));
+    assertTrue("Forced external file not forced.",
+        rewritten.contains("<script src=\"http://example.org/external.js\">"));
+  }
+
+  @Test(expected = RewritingException.class)
+  public void exceptionWhenFeatureNotAllowed() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+    reset(gadgetAdminStore);
+    expect(gadgetAdminStore.checkFeatureAdminInfo(isA(Gadget.class))).andReturn(false);
+    replay(gadgetAdminStore);
+    rewrite(gadget, BODY_CONTENT);
+  }
+
+  @Test
+  public void  gadgetAdminDefaultContent() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    final Set<String> libs = ImmutableSortedSet.of("foo", "bar", "baz");
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return Joiner.on(':').join(libs);
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml).setContext(context);
+    FeatureResource fooResource = inline("foo-content", "foo-debug");
+    expectFeatureCalls(gadget,
+        ImmutableList.of(fooResource),
+        libs,
+        ImmutableList.of(fooResource, inline("bar-c", "bar-d"), inline("baz-c", "baz-d")));
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(rewritten);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    assertTrue("Missing opening html tag", matcher.group(BEFORE_HEAD_GROUP).
+        toLowerCase().contains("<html"));
+    assertTrue("Default CSS missing.", matcher.group(HEAD_GROUP).contains(DEFAULT_CSS));
+    // Not very accurate -- could have just been user prefs.
+    assertTrue("Default javascript not included.",
+        matcher.group(HEAD_GROUP).contains("<script>"));
+    assertTrue("Original document not preserved.",
+        matcher.group(BODY_GROUP).contains(BODY_CONTENT));
+    assertTrue("gadgets.util.runOnLoadHandlers not invoked.",
+        matcher.group(BODY_GROUP).contains("gadgets.util.runOnLoadHandlers();"));
+  }
+
+  @Test
+  public void optionalDeniedFeature() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='core.util'/>" +
+      "  <Require feature='foo'/>" +
+      "  <Optional feature='hello'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    final Set<String> libs = ImmutableSortedSet.of("core.util", "foo", "bar", "baz");
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return Joiner.on(':').join(libs);
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml).setContext(context);
+    reset(gadgetAdminStore);
+    Feature denied = mock(Feature.class);
+    expect(denied.getName()).andReturn("hello");
+    expect(gadgetAdminStore.checkFeatureAdminInfo(isA(Gadget.class))).andReturn(true);
+    expect(gadgetAdminStore.isAllowedFeature(eq(denied), isA(Gadget.class))).andReturn(false);
+    replay();
+
+    FeatureResource fooResource = inline("foo-content", "foo-debug");
+    expectFeatureCalls(gadget,
+        ImmutableList.of(fooResource),
+        libs,
+        ImmutableList.of(fooResource, inline("bar-c", "bar-d"), inline("baz-c", "baz-d")));
+
+    String rewritten = rewrite(gadget, "");
+    JSONObject json = getConfigJson(rewritten);
+
+    Set<String> actual = getInjectedScript(rewritten);
+    Set<String> expected = ImmutableSortedSet.of("core.util", "foo", "bar", "baz");
+    assertFalse(actual.contains("hello"));
+    assertEquals(expected, actual);
+    assertFalse(json.getJSONObject("core.util").has("hello"));
+  }
+
+  private JSONObject getConfigJson(String content) throws JSONException {
+    Pattern prefsPattern
+        = Pattern.compile("(?:.*)gadgets\\.config\\.init\\((.*)\\);(?:.*)", Pattern.DOTALL);
+    Matcher matcher = prefsPattern.matcher(content);
+    assertTrue("gadgets.config.init not invoked.", matcher.matches());
+    return new JSONObject(matcher.group(1));
+  }
+
+  @Test
+  public void featureConfigurationInjected() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo", "dbg")),
+        ImmutableSet.<String>of(),
+        ImmutableList.<FeatureResource>of());
+
+    config.data.put(FEATURES_KEY, ImmutableMap.of("foo", "blah"));
+
+    String rewritten = rewrite(gadget, "");
+
+    JSONObject json = getConfigJson(rewritten);
+    assertEquals("blah", json.get("foo"));
+  }
+
+  @Test
+  public void featureConfigurationForced() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return "bar";
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml).setContext(context);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo", "foo-dbg")),
+        ImmutableSet.of("bar"),
+        ImmutableList.of(inline("bar", "bar-dbg")));
+
+    config.data.put(FEATURES_KEY, ImmutableMap.of(
+        "foo", "blah",
+        "bar", "baz"
+    ));
+
+    String rewritten = rewrite(gadget, "");
+
+    JSONObject json = getConfigJson(rewritten);
+    assertEquals("blah", json.get("foo"));
+    assertEquals("baz", json.get("bar"));
+  }
+
+  @Test
+  public void gadgetsUtilConfigInjected() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='core.util'/>" +
+      "  <Require feature='foo'>" +
+      "    <Param name='bar'>baz</Param>" +
+      "  </Require>" +
+      "  <Require feature='foo2'>" +
+      "    <Param name='bar'>baz</Param>" +
+      "    <Param name='bar'>bop</Param>" +
+      "  </Require>" +
+      "  <Require feature='unsupported'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    expectFeatureCalls(gadget,
+        ImmutableList.of(inline("foo", "foo-dbg"), inline("foo2", "foo2-dbg")),
+        ImmutableSet.<String>of(),
+        ImmutableList.<FeatureResource>of());
+
+    config.data.put(FEATURES_KEY, ImmutableMap.of("foo", "blah"));
+
+    String rewritten = rewrite(gadget, "");
+
+    JSONObject json = getConfigJson(rewritten);
+    assertEquals("blah", json.get("foo"));
+
+    JSONObject util = json.getJSONObject("core.util");
+    JSONObject foo = util.getJSONObject("foo");
+    assertEquals("baz", foo.get("bar"));
+    JSONObject foo2 = util.getJSONObject("foo2");
+    JsonAssert.assertObjectEquals(ImmutableList.of("baz", "bop"),
+        foo2.get("bar"));
+
+    assertTrue(!util.has("unsupported"));
+  }
+
+  // TODO: Test for auth token stuff.
+
+  @Test
+  public void userPrefsInitializationInjected() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Locale>" +
+      "    <msg name='one'>foo</msg>" +
+      "    <msg name='two'>bar</msg>" +
+      "  </Locale>" +
+      "</ModulePrefs>" +
+      "<UserPref name='pref_one' default_value='default_one'/>" +
+      "<UserPref name='pref_two'/>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    String rewritten = rewrite(gadget, "");
+
+    Pattern prefsPattern
+        = Pattern.compile("(?:.*)gadgets\\.Prefs\\.setMessages_\\((.*)\\);(?:.*)", Pattern.DOTALL);
+    Matcher matcher = prefsPattern.matcher(rewritten);
+    assertTrue("gadgets.Prefs.setMessages_ not invoked.", matcher.matches());
+    JSONObject json = new JSONObject(matcher.group(1));
+    assertEquals("foo", json.get("one"));
+    assertEquals("bar", json.get("two"));
+
+    Pattern defaultsPattern = Pattern.compile(
+        "(?:.*)gadgets\\.Prefs\\.setDefaultPrefs_\\((.*)\\);(?:.*)", Pattern.DOTALL);
+    Matcher defaultsMatcher = defaultsPattern.matcher(rewritten);
+    assertTrue("gadgets.Prefs.setDefaultPrefs_ not invoked.", defaultsMatcher.matches());
+    JSONObject defaultsJson = new JSONObject(defaultsMatcher.group(1));
+    assertEquals(2, defaultsJson.length());
+    assertEquals("default_one", defaultsJson.get("pref_one"));
+    assertEquals("", defaultsJson.get("pref_two"));
+  }
+
+  @Test
+  public void xhrWrapperConfigurationInjected() throws Exception {
+    checkXhrWrapperConfigurationInjection(
+        "No shindig.xhrwrapper configuration present in rewritten HTML.", null, null, null);
+
+    checkXhrWrapperConfigurationInjection(
+        "No shindig.xhrwrapper.authorization=signed configuration present in rewritten HTML.",
+        "signed", null, null);
+
+    checkXhrWrapperConfigurationInjection(
+        "No shindig.xhrwrapper.oauthService configuration present in rewritten HTML.",
+        "oauth", "serviceName", null);
+
+    checkXhrWrapperConfigurationInjection(
+        "No shindig.xhrwrapper.oauthTokenName configuration present in rewritten HTML.",
+        "oauth", "serviceName", "tokenName");
+  }
+
+  private void checkXhrWrapperConfigurationInjection(String message, String auth, String oauthService, String oauthToken)
+      throws Exception {
+    String oAuthBlock = "";
+    String authzAttr = "";
+    if (auth != null) {
+      authzAttr = " authz='" + auth + '\'';
+      if ("oauth".equals(auth)) {
+        if (oauthService != null) {
+          oAuthBlock =
+              "<OAuth><Service name='" + oauthService + "'>" +
+              "<Access url='http://foo' method='GET' />" +
+              "<Request url='http://bar' method='GET' />" +
+              "<Authorization url='http://baz' />" +
+              "</Service></OAuth>";
+          authzAttr += " oauth_service_name='" + oauthService + '\'';
+        }
+        if (oauthToken != null) {
+          authzAttr += " oauth_token_name='" + oauthToken + '\'';
+        }
+      }
+    }
+
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='shindig.xhrwrapper' />" +
+      oAuthBlock +
+      "</ModulePrefs>" +
+      "<Content type='html' href='http://foo.com/bar/baz.html'" + authzAttr + " />" +
+      "</Module>";
+
+    String expected = '{' +
+        (oauthService == null ? "" : "\"oauthService\":\"serviceName\",") +
+        "\"contentUrl\":\"http://foo.com/bar/baz.html\"" +
+        (auth == null ? "" : ",\"authorization\":\"" + auth + '\"') +
+        (oauthToken == null ? "" : ",\"oauthTokenName\":\"tokenName\"") +
+        '}';
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+    gadget.setCurrentView(gadget.getSpec().getView("default"));
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+
+    assertXhrConfigContains(message, expected, rewritten);
+  }
+
+  private void assertXhrConfigContains(String message, String expected, String content) throws Exception {
+    // TODO: make this test a little more robust. This check ensures that ordering is not taken
+    // into account during config comparison.
+    String prefix = "gadgets.config.init(";
+    int configIdx = content.indexOf(prefix);
+    assertTrue("gadgets.config.init not found in rewritten content", configIdx != -1);
+    int endIdx = content.indexOf(')', configIdx + prefix.length());
+    assertTrue("unexpected error, gadgets.config.init not closed", endIdx != -1);
+    String configJson = content.substring(configIdx + prefix.length(), endIdx);
+    JSONObject config = new JSONObject(configJson);
+    JSONObject xhrConfig = config.getJSONObject("shindig.xhrwrapper");
+    JSONObject expectedJson = new JSONObject(expected);
+    JsonAssert.assertJsonObjectEquals(xhrConfig, expectedJson);
+  }
+
+  @Test
+  public void xhrWrapperConfigurationNotInjectedIfUnnecessary() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title='' />" +
+      "<Content type='html' href='http://foo.com/bar/baz.html' />" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+    gadget.setCurrentView(gadget.getSpec().getView("default"));
+
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+
+    boolean containsConfig = rewritten.contains("\"shindig.xhrwrapper\"");
+    assertFalse("shindig.xhrwrapper configuration present in rewritten HTML.", containsConfig);
+  }
+
+  @Test(expected = RewritingException.class)
+  public void unsupportedFeatureThrows() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    reset(featureRegistry);
+    FeatureRegistry.LookupResult lr = createMock(FeatureRegistry.LookupResult.class);
+    expect(lr.getResources()).andReturn(ImmutableList.<FeatureResource>of());
+    replay(lr);
+    expect(featureRegistry.getFeatureResources(same(gadget.getContext()),
+        eq(ImmutableSet.<String>of()), eq(Lists.<String>newLinkedList())))
+        .andReturn(lr);
+    final FeatureRegistry.LookupResult lr2 = createMock(FeatureRegistry.LookupResult.class);
+    expect(lr2.getResources()).andReturn(ImmutableList.<FeatureResource>of());
+    replay(lr2);
+    assertTrue(gadget.getDirectFeatureDeps().contains("core"));
+    assertTrue(gadget.getDirectFeatureDeps().contains("foo"));
+    assertEquals(gadget.getDirectFeatureDeps().size(),2);
+    expect(featureRegistry.getFeatureResources(same(gadget.getContext()),
+        eq(Lists.newLinkedList(gadget.getDirectFeatureDeps())), eq(Lists.<String>newLinkedList())))
+        .andAnswer(new IAnswer<FeatureRegistry.LookupResult>() {
+          @SuppressWarnings("unchecked")
+          public FeatureRegistry.LookupResult answer() throws Throwable {
+            List<String> unsupported = (List<String>)getCurrentArguments()[2];
+            unsupported.add("foo");
+            return lr2;
+          }
+        });
+    replay(featureRegistry);
+
+    rewrite(gadget, "");
+  }
+
+  @Test(expected = RewritingException.class)
+  public void unsupportedViewFeatureThrows() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Require feature='foo' views='default'/>" +
+      "</ModulePrefs>" +
+      "<Content view='default' type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    reset(featureRegistry);
+    FeatureRegistry.LookupResult lr = createMock(FeatureRegistry.LookupResult.class);
+    expect(lr.getResources()).andReturn(ImmutableList.<FeatureResource>of());
+    replay(lr);
+    expect(featureRegistry.getFeatureResources(same(gadget.getContext()),
+        eq(ImmutableSet.<String>of()), eq(Lists.<String>newLinkedList())))
+        .andReturn(lr);
+    final FeatureRegistry.LookupResult lr2 = createMock(FeatureRegistry.LookupResult.class);
+    expect(lr2.getResources()).andReturn(ImmutableList.<FeatureResource>of());
+    replay(lr2);
+    assertTrue(gadget.getDirectFeatureDeps().contains("core"));
+    assertTrue(gadget.getDirectFeatureDeps().contains("foo"));
+    assertEquals(gadget.getDirectFeatureDeps().size(),2);
+    Lists.newLinkedList();
+    expect(featureRegistry.getFeatureResources(same(gadget.getContext()),
+        eq(Lists.newLinkedList(gadget.getDirectFeatureDeps())), eq(Lists.<String>newLinkedList())))
+        .andAnswer(new IAnswer<FeatureRegistry.LookupResult>() {
+          @SuppressWarnings("unchecked")
+          public FeatureRegistry.LookupResult answer() throws Throwable {
+            List<String> unsupported = (List<String>)getCurrentArguments()[2];
+            unsupported.add("foo");
+            return lr2;
+          }
+        });
+    replay(featureRegistry);
+
+    rewrite(gadget, "");
+  }
+
+  @Test
+  public void unsupportedExternFeatureDoesNotThrow() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        if (name.equals("libs")) {
+          return "bar";
+        }
+        return null;
+      }
+    };
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml).setContext(context);
+
+    reset(featureRegistry);
+    final FeatureRegistry.LookupResult lr = createMock(FeatureRegistry.LookupResult.class);
+    expect(lr.getResources()).andReturn(ImmutableList.<FeatureResource>of()).anyTimes();
+    replay(lr);
+    expect(featureRegistry.getFeatureResources(same(gadget.getContext()),
+        eq(ImmutableSet.<String>of("bar")), eq(Lists.<String>newArrayList())))
+        .andAnswer(new IAnswer<FeatureRegistry.LookupResult>() {
+          @SuppressWarnings("unchecked")
+          public FeatureRegistry.LookupResult answer() throws Throwable {
+            List<String> unsupported = (List<String>)getCurrentArguments()[2];
+            unsupported.add("bar");
+            return lr;
+          }
+        });
+    expect(featureRegistry.getFeatureResources(same(gadget.getContext()),
+        eq(ImmutableList.<String>of("core")), eq(Lists.<String>newArrayList())))
+        .andReturn(lr);
+    expect(featureRegistry.getFeatures(eq(ImmutableSet.of("core", "bar"))))
+        .andReturn(ImmutableList.of("core"));
+    expect(featureRegistry.getFeatures(eq(ImmutableList.of("core"))))
+        .andReturn(ImmutableList.of("core"));
+    replay(featureRegistry);
+
+    rewrite(gadget, "");
+  }
+
+  @Test
+  public void unsupportedOptionalFeatureDoesNotThrow() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Optional feature='foo'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    rewrite(gadget, "");
+    // rewrite will throw if the optional unsupported feature doesn't work.
+  }
+
+  @Test
+  public void multipleUnsupportedOptionalFeaturesDoNotThrow() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Optional feature='foo'/>" +
+      "  <Optional feature='bar'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    rewrite(gadget, "");
+    // rewrite will throw if the optional unsupported feature doesn't work.
+  }
+
+  @Test
+  public void unsupportedViewFeaturesDoNotThrow() throws Exception {
+    String gadgetXml =
+      "<Module><ModulePrefs title=''>" +
+      "  <Optional feature='foo'/>" +
+      "  <Optional feature='bar'/>" +
+      "  <Require feature='bar2' views='view1'/>" +
+      "  <Optional feature='bar3' views='view1'/>" +
+      "</ModulePrefs>" +
+      "<Content type='html'/>" +
+      "</Module>";
+
+    Gadget gadget = makeGadgetWithSpec(gadgetXml);
+
+    rewrite(gadget, "");
+    // rewrite will throw if the optional unsupported feature doesn't work.
+  }
+
+  private JSONArray getPreloadedJson(String content) throws JSONException {
+    Pattern preloadPattern
+        = Pattern.compile("(?:.*)gadgets\\.io\\.preloaded_=\\[(.*?)\\];(?:.*)", Pattern.DOTALL);
+    Matcher matcher = preloadPattern.matcher(content);
+    assertTrue("gadgets.io.preloaded not set.", matcher.matches());
+    return new JSONArray('[' + matcher.group(1) + ']');
+  }
+
+  @Test
+  public void preloadsInjected() throws Exception {
+    final Collection<Object> someData = ImmutableList.of("string", (Object) 99, 4343434.345345d);
+
+    // Other types are supported (anything valid for org.json.JSONObject), but equality comparisons
+    // are more complicated because JSON doesn't implement interfaces like Collection or Map, or
+    // implementing equals.
+    PreloadedData preloadedData = new PreloadedData() {
+      public Collection<Object> toJson() {
+        return someData;
+      }
+    };
+    Gadget gadget = makeDefaultGadget().setPreloads(ImmutableList.of(preloadedData));
+
+    String rewritten = rewrite(gadget, "");
+
+    JSONArray json = getPreloadedJson(rewritten);
+    int i = 0;
+    for (Object entry : someData) {
+      assertEquals(entry, json.get(i++));
+    }
+  }
+
+  @Test
+  public void failedPreloadHandledGracefully() throws Exception {
+    PreloadedData preloadedData = new PreloadedData() {
+      public Collection<Object> toJson() throws PreloadException {
+        throw new PreloadException("test");
+      }
+    };
+
+    Gadget gadget = makeDefaultGadget().setPreloads(ImmutableList.of(preloadedData));
+    String rewritten = rewrite(gadget, "");
+
+    JSONArray json = getPreloadedJson(rewritten);
+
+    assertEquals(0, json.length());
+  }
+
+  private String getBaseElement(String content) {
+    Matcher matcher = DOCUMENT_SPLIT_PATTERN.matcher(content);
+    assertTrue("Output is not valid HTML.", matcher.matches());
+    Pattern baseElementPattern
+        = Pattern.compile("^<base href=\"(.*?)\">(?:.*)", Pattern.DOTALL);
+    Matcher baseElementMatcher = baseElementPattern.matcher(matcher.group(HEAD_GROUP));
+    assertTrue("Base element does not exist at the beginning of the head element.",
+        baseElementMatcher.matches());
+    return baseElementMatcher.group(1);
+  }
+
+  @Test
+  public void baseElementInsertedWhenContentIsInline() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+
+    config.data.put(INSERT_BASE_ELEMENT_KEY, true);
+
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+    String base = getBaseElement(rewritten);
+
+    assertEquals(SPEC_URL.toString(), base);
+  }
+
+  @Test
+  public void baseElementInsertedWhenContentIsProxied() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+
+    String viewUrl = "http://example.org/view.html";
+    String xml = "<Content href='" + viewUrl + "'/>";
+    View fakeView = new View("foo", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+    gadget.setCurrentView(fakeView);
+
+    config.data.put(INSERT_BASE_ELEMENT_KEY, true);
+
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+    String base = getBaseElement(rewritten);
+
+    assertEquals(viewUrl, base);
+  }
+
+  @Test
+  public void baseElementNotInsertedWhenConfigDoesNotAllowIt() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+
+    config.data.put(INSERT_BASE_ELEMENT_KEY, false);
+
+    String rewritten = rewrite(gadget, BODY_CONTENT);
+    assertFalse("Base element injected incorrectly.", rewritten.contains("<base"));
+  }
+
+  @Test
+  public void doesNotRewriteWhenSanitizeEquals1() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+
+    context.params.put("sanitize", "1");
+
+    assertEquals(BODY_CONTENT, rewrite(gadget, BODY_CONTENT));
+  }
+
+  @Test
+  public void doesRewriteWhenSanitizeEquals0() throws Exception {
+    Gadget gadget = makeDefaultGadget();
+
+    context.params.put("sanitize", "0");
+
+    assertFalse("Didn't rewrite when sanitize was '0'.",
+        BODY_CONTENT.equals(rewrite(gadget, BODY_CONTENT)));
+  }
+
+  private List<String> getAllRequiredFeatures(Gadget gadget) {
+    List<String> names = Lists.newArrayList();
+    List<Feature> features = gadget.getSpec().getModulePrefs().getAllFeatures();
+    for(Feature feature : features) {
+      if(feature.getRequired()) {
+        names.add(feature.getName());
+      }
+    }
+    return names;
+  }
+
+  private void expectFeatureCalls(Gadget gadget,
+                                  List<FeatureResource> gadgetResources,
+                                  Set<String> externLibs,
+                                  List<FeatureResource> externResources) {
+    reset(featureRegistry);
+    GadgetContext gadgetContext = gadget.getContext();
+    List<String> gadgetFeatures = Lists.newLinkedList(gadget.getDirectFeatureDeps());
+    List<String> allFeatures = Lists.newLinkedList(gadgetFeatures);
+    List<String> allFeaturesAndLibs = Lists.newLinkedList(gadgetFeatures);
+    allFeaturesAndLibs.addAll(externLibs);
+    List<String> allRequiredFeatures = Lists.newLinkedList(getAllRequiredFeatures(gadget));
+    List<String> allRequiredFeatuesAndLibs = Lists.newLinkedList(allRequiredFeatures);
+    allRequiredFeatuesAndLibs.addAll(externLibs);
+    List<String> emptyList = Lists.newLinkedList();
+    final FeatureRegistry.LookupResult externLr = createMock(FeatureRegistry.LookupResult.class);
+    expect(externLr.getResources()).andReturn(externResources);
+    replay(externLr);
+    final FeatureRegistry.LookupResult gadgetLr = createMock(FeatureRegistry.LookupResult.class);
+    expect(gadgetLr.getResources()).andReturn(gadgetResources);
+    replay(gadgetLr);
+    expect(featureRegistry.getFeatureResources(same(gadgetContext), eq(externLibs), eq(emptyList)))
+        .andReturn(externLr);
+    expect(featureRegistry.getFeatureResources(same(gadgetContext), eq(gadgetFeatures),
+        eq(emptyList))).andReturn(gadgetLr);
+    expect(featureRegistry.getFeatures(eq(allFeatures)))
+        .andReturn(allFeatures).anyTimes();
+    expect(featureRegistry.getFeatures(eq(Sets.newHashSet(allFeaturesAndLibs))))
+        .andReturn(allFeaturesAndLibs);
+    expect(featureRegistry.getFeatures(eq(ImmutableSet.of("*"))))
+    .andReturn(ImmutableList.<String>of()).anyTimes();
+    expect(featureRegistry.getFeatures(eq(ImmutableSet.of("hello"))))
+    .andReturn(ImmutableList.<String>of("hello")).anyTimes();
+    if(!allRequiredFeatures.equals(allFeatures)) {
+      expect(featureRegistry.getFeatures(eq(allRequiredFeatures)))
+      .andReturn(allRequiredFeatuesAndLibs).anyTimes();
+      expect(featureRegistry.getFeatures(eq(Sets.newHashSet(allRequiredFeatuesAndLibs))))
+      .andReturn(allRequiredFeatuesAndLibs).anyTimes();
+    }
+    // Add CoreUtilConfigContributor behavior
+    expect(featureRegistry.getAllFeatureNames()).
+        andReturn(ImmutableSet.of("foo", "foo2", "core.util")).anyTimes();
+    replay(featureRegistry);
+
+    JsResponseBuilder builder = new JsResponseBuilder();
+    for (FeatureResource r :  gadgetResources) {
+      if (r.isExternal()) {
+        builder.appendJs("<script src=\"" + r.getContent() + "\">", r.getName());
+      } else {
+        builder.appendJs(r.getContent(), r.getName());
+      }
+    }
+    reset(jsServingPipeline);
+    try {
+      expect(jsServingPipeline.execute(EasyMock.<JsRequest>anyObject())).andReturn(builder.build());
+    } catch (JsException e) {
+      throw new RuntimeException("Should not fail here");
+    }
+    replay(jsServingPipeline);
+  }
+
+  private FeatureResource inline(String content, String debugContent) {
+    return new FeatureResource.Simple(content, debugContent, "js");
+  }
+
+  private FeatureResource extern(String content, String debugContent) {
+    return new FeatureResource.Simple(content, debugContent, "js") {
+      @Override
+      public boolean isExternal() {
+        return true;
+      }
+    };
+  }
+
+  public static class MapGadgetContext extends GadgetContext {
+    protected final Map<String, String> params = Maps.newHashMap();
+
+    @Override
+    public String getParameter(String name) {
+      return params.get(name);
+    }
+  }
+
+  private static class FakeContainerConfig extends BasicContainerConfig {
+    protected final Map<String, Object> data = Maps.newHashMap();
+
+    @Override
+    public Object getProperty(String container, String name) {
+      return data.get(name);
+    }
+  }
+
+  private static class FakeJsUriManager implements JsUriManager {
+    public Uri makeExternJsUri(JsUri ctx) {
+      return Uri.parse("/js/" + Joiner.on(':').join(ctx.getLibs()));
+    }
+
+    public JsUri processExternJsUri(Uri uri) {
+      throw new UnsupportedOperationException();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java
new file mode 100644
index 0000000..aaf1076
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingGadgetRewriterTest.java
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import com.google.inject.util.Providers;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.caja.CajaCssParser;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.apache.shindig.gadgets.rewrite.RewriterTestBase;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.GadgetRewriter;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.uri.PassthruManager;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SanitizingGadgetRewriterTest extends RewriterTestBase {
+  private static final Set<String> DEFAULT_TAGS = ImmutableSet.of("html", "head", "body");
+  private static final Pattern BODY_REGEX = Pattern.compile(".*<body>(.+)</body>.*");
+
+  private final GadgetContext sanitaryGadgetContext = new GadgetContext() {
+    @Override
+    public String getParameter(String name) {
+      return Param.SANITIZE.getKey().equals(name) ? "1" : null;
+    }
+
+    @Override
+    public String getContainer() {
+      return MOCK_CONTAINER;
+    }
+  };
+
+  private final GadgetContext unsanitaryGadgetContext = new GadgetContext();
+  private final GadgetContext unsanitaryGadgetContextNoCacheAndDebug = new GadgetContext(){
+    @Override
+    public boolean getIgnoreCache() {
+      return true;
+    }
+    @Override
+    public boolean getDebug() {
+      return true;
+    }
+  };
+  private Gadget gadget;
+  private Gadget gadgetNoCacheAndDebug;
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    gadget = new Gadget().setContext(unsanitaryGadgetContext);
+    gadget.setSpec(new GadgetSpec(Uri.parse("http://www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='x-html-sanitized'/></Module>"));
+    gadget.setCurrentView(gadget.getSpec().getViews().values().iterator().next());
+
+    gadgetNoCacheAndDebug = new Gadget().setContext(unsanitaryGadgetContextNoCacheAndDebug);
+    gadgetNoCacheAndDebug.setSpec(new GadgetSpec(Uri.parse("http://www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='x-html-sanitized'/></Module>"));
+    gadgetNoCacheAndDebug.setCurrentView(gadgetNoCacheAndDebug.getSpec().getViews().values().iterator().next());
+  }
+
+  @Override
+  protected Class<? extends GadgetHtmlParser> getParserClass() {
+    return CajaHtmlParser.class;
+  }
+
+  private String rewrite(Gadget gadget, String content, Set<String> tags, Set<String> attributes)
+      throws Exception {
+    GadgetRewriter rewriter = createRewriter(tags, attributes);
+
+    MutableContent mc = new MutableContent(parser, content);
+    rewriter.rewrite(gadget, mc);
+
+    Matcher matcher = BODY_REGEX.matcher(mc.getContent());
+    if (matcher.matches()) {
+      return matcher.group(1);
+    }
+    return mc.getContent();
+  }
+
+  private static Set<String> set(String... items) {
+    return Sets.newHashSet(items);
+  }
+
+  private GadgetRewriter createRewriter(Set<String> tags, Set<String> attributes) {
+    Set<String> newTags = new HashSet<String>(tags);
+    newTags.addAll(DEFAULT_TAGS);
+    ContentRewriterFeature.Factory rewriterFeatureFactory =
+        new ContentRewriterFeature.Factory(null,
+          Providers.of(new ContentRewriterFeature.DefaultConfig(
+            ".*", "", "HTTP", "embed,img,script,link,style", false, false, false)));
+    return new SanitizingGadgetRewriter(Providers.of(newTags), Providers.of(attributes), rewriterFeatureFactory,
+        new CajaCssSanitizer(new CajaCssParser()), new PassthruManager("host.com", "/proxy"));
+  }
+
+  @Test
+  public void enforceTagWhiteList() throws Exception {
+    String markup =
+        "<p><style type=\"text/css\">A { font : bold }</style>text <b>bold text</b></p>" +
+        "<b>Bold text</b><i>Italic text<b>Bold text</b></i>";
+
+    String sanitized = "<p>text <b>bold text</b></p><b>Bold text</b>";
+
+    assertEquals(sanitized, rewrite(gadget, markup, set("p", "b"), set()));
+  }
+
+  @Test
+  public void enforceStyleSanitized() throws Exception {
+    String markup =
+        "<p><style type=\"text/css\">A { font : bold; behavior : bad }</style>text <b>bold text</b></p>" +
+        "<b>Bold text</b><i>Italic text<b>Bold text</b></i>";
+
+    String sanitized = "<html><head></head><body><p><style>A {\n  font: bold\n}</style>text " +
+        "<b>bold text</b></p><b>Bold text</b></body></html>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("p", "b", "style"), set()));
+  }
+
+  @Test
+  public void enforceStyleLinkRewritten() throws Exception {
+    String markup =
+        "<link rel=\"stylesheet\" "
+            + "href=\"http://www.test.com/dir/proxy?"
+            + "url=http%3A%2F%2Fwww.evil.com%2Fx.css&gadget=www.example.org%2Fgadget.xml&"
+            + "fp=45508&rewriteMime=text/css\"/>";
+    String sanitized =
+        "<html><head><link href=\"http://host.com/proxy?url=http%3A%2F%2Fwww.test.com%2Fdir%2F" +
+        "proxy%3Furl%3Dhttp%253A%252F%252Fwww.evil.com%252Fx.css%26gadget%3Dwww.example.org%252F" +
+        "gadget.xml%26fp%3D45508%26rewriteMime%3Dtext%2Fcss&amp;sanitize=1&amp;rewriteMime=text%2Fcss\" " +
+        "rel=\"stylesheet\"></head><body></body></html>";
+    String rewritten = rewrite(gadget, markup, set("link"), set("rel", "href"));
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceStyleLinkRewrittenNoCacheAndDebug() throws Exception {
+    String markup =
+        "<link rel=\"stylesheet\" "
+            + "href=\"http://www.test.com/dir/proxy?"
+            + "url=http%3A%2F%2Fwww.evil.com%2Fx.css&gadget=www.example.org%2Fgadget.xml&"
+            + "fp=45508&rewriteMime=text/css\"/>";
+    String sanitized =
+        "<html><head><link href=\"http://host.com/proxy?url=http%3A%2F%2Fwww.test.com%2F"
+            + "dir%2Fproxy%3Furl%3Dhttp%253A%252F%252Fwww.evil.com%252Fx.css%26gadget%3D"
+            + "www.example.org%252Fgadget.xml%26fp%3D45508%26rewriteMime%3Dtext%2Fcss&amp;"
+            + "sanitize=1&amp;rewriteMime=text%2Fcss\" rel=\"stylesheet\">"
+            + "</head><body></body></html>";
+    String rewritten = rewrite(gadgetNoCacheAndDebug, markup, set("link"), set("rel", "href"));
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceNonStyleLinkStripped() throws Exception {
+    String markup =
+        "<link rel=\"script\" "
+            + "href=\"www.exmaple.org/evil.js\"/>";
+    String rewritten = rewrite(gadget, markup, set("link"), set("rel", "href", "type"));
+    assertEquals("<html><head></head><body></body></html>", rewritten);
+  }
+
+  @Test
+  public void enforceNonStyleLinkStrippedNoCacheAndDebug() throws Exception {
+    String markup =
+        "<link rel=\"script\" "
+            + "href=\"www.exmaple.org/evil.js\"/>";
+    String rewritten = rewrite(gadgetNoCacheAndDebug, markup, set("link"), set("rel", "href", "type"));
+    assertEquals("<html><head></head><body></body></html>", rewritten);
+  }
+
+  @Test
+  public void enforceCssImportLinkRewritten() throws Exception {
+    String markup =
+        "<style type=\"text/css\">@import url('www.evil.com/x.js');</style>";
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized =
+        "<html><head><style>"
+      + "@import url('http://host.com/proxy?url=http%3A%2F%2Fwww.example.org%2Fwww.evil.com%2Fx.js&"
+      + "sanitize=1&rewriteMime=text%2Fcss');"
+      + "</style></head><body></body></html>";
+    String rewritten = rewrite(gadget, markup, set("style"), set());
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceCssImportLinkRewrittenNoCacheAndDebug() throws Exception {
+    String markup =
+        "<style type=\"text/css\">@import url('www.evil.com/x.js');</style>";
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized =
+        "<html><head><style>"
+      + "@import url('http://host.com/proxy?url=http%3A%2F%2Fwww.example.org%2Fwww.evil.com%2Fx.js&sanitize=1"
+      + "&rewriteMime=text%2Fcss');</style></head><body></body></html>";
+    String rewritten = rewrite(gadgetNoCacheAndDebug, markup, set("style"), set());
+    assertEquals(sanitized, rewritten);
+  }
+
+  @Test
+  public void enforceCssImportBadLinkStripped() throws Exception {
+    String markup =
+        "<style type=\"text/css\">@import url('javascript:doevil()'); A { font : bold }</style>";
+    String sanitized = "<html><head><style>A {\n"
+        + "  font: bold\n"
+        + "}</style></head><body></body></html>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("style"), set()));
+  }
+
+  @Test
+  public void enforceAttributeWhiteList() throws Exception {
+    String markup = "<p foo=\"bar\" bar=\"baz\">Paragraph</p>";
+    String sanitized = "<p bar=\"baz\">Paragraph</p>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("p"), set("bar")));
+  }
+
+  @Test
+  public void enforceImageSrcProxied() throws Exception {
+    String markup = "<img src='http://www.evil.com/x.js'>Evil happens</img>";
+    String sanitized = "<img src=\"http://host.com/proxy?url=http%3A%2F%2F" +
+        "www.evil.com%2Fx.js&amp;sanitize=1&amp;rewriteMime=image%2F*\">Evil happens";
+    assertEquals(sanitized, rewrite(gadget, markup, set("img"), set("src")));
+  }
+
+  @Test
+  public void enforceImageSrcProxiedNoCacheAndDebug() throws Exception {
+    String markup = "<img src='http://www.evil.com/x.js'>Evil happens</img>";
+    String sanitized = "<img src=\"http://host.com/proxy?url=http%3A%2F%2Fwww.evil.com" +
+        "%2Fx.js&amp;sanitize=1&amp;rewriteMime=image%2F*\">Evil happens";
+    assertEquals(sanitized, rewrite(gadgetNoCacheAndDebug, markup, set("img"), set("src")));
+  }
+
+  @Test
+  public void enforceBadImageUrlStripped() throws Exception {
+    String markup = "<img src='java\\ script:evil()'>Evil happens</img>";
+    String sanitized = "<img>Evil happens";
+    assertEquals(sanitized, rewrite(gadget, markup, set("img"), set("src")));
+  }
+
+  @Test
+  public void enforceTargetTopRestricted() throws Exception {
+    String markup = "<a href=\"http://www.example.com\" target=\"_top\">x</a>";
+    String sanitized = "<a href=\"http://www.example.com\">x</a>";
+    assertEquals(sanitized, rewrite(gadget, markup, set("a"), set("href", "target")));
+  }
+
+  @Test
+  public void enforceTargetSelfAllowed() throws Exception {
+    String markup = "<a href=\"http://www.example.com\" target=\"_self\">x</a>";
+    assertEquals(markup, rewrite(gadget, markup, set("a"), set("href", "target")));
+  }
+
+  @Test
+  public void enforceTargetBlankAllowed() throws Exception {
+    String markup = "<a href=\"http://www.example.com\" target=\"_BlAnK\">x</a>";
+    assertEquals(markup, rewrite(gadget, markup, set("a"), set("href", "target")));
+  }
+
+  @Test
+  public void sanitizationBypassAllowed() throws Exception {
+    String markup = "<p foo=\"bar\"><b>Parag</b><!--raph--></p>";
+    // Create a rewriter that would strip everything
+    GadgetRewriter rewriter = createRewriter(set(), set());
+
+    MutableContent mc = new MutableContent(parser, markup);
+    Document document = mc.getDocument();
+    // Force the content to get re-serialized
+    MutableContent.notifyEdit(document);
+    String fullMarkup = mc.getContent();
+
+    Element paragraphTag = (Element) document.getElementsByTagName("p").item(0);
+    // Mark the paragraph tag element as trusted
+    SanitizingGadgetRewriter.bypassSanitization(paragraphTag, true);
+    rewriter.rewrite(gadget, mc);
+
+    // The document should be unchanged
+    assertEquals(fullMarkup, mc.getContent());
+  }
+
+  @Test
+  public void sanitizationBypassOnlySelf() throws Exception {
+    String markup = "<p foo=\"bar\"><b>Parag</b><!--raph--></p>";
+    // Create a rewriter that would strip everything
+    GadgetRewriter rewriter = createRewriter(set(), set());
+
+    MutableContent mc = new MutableContent(parser, markup);
+    Document document = mc.getDocument();
+
+    Element paragraphTag = (Element) document.getElementsByTagName("p").item(0);
+    // Mark the paragraph tag element as trusted
+    SanitizingGadgetRewriter.bypassSanitization(paragraphTag, false);
+    rewriter.rewrite(gadget, mc);
+
+    // The document should be unchanged
+    String content = mc.getContent();
+    Matcher matcher = BODY_REGEX.matcher(content);
+    matcher.matches();
+    assertEquals("<p foo=\"bar\"></p>", matcher.group(1));
+  }
+
+  @Test
+  public void sanitizationBypassPreservedAcrossClone() throws Exception {
+    String markup = "<p foo=\"bar\"><b>Parag</b><!--raph--></p>";
+    // Create a rewriter that would strip everything
+    GadgetRewriter rewriter = createRewriter(set(), set());
+
+    MutableContent mc = new MutableContent(parser, markup);
+    Document document = mc.getDocument();
+
+    Element paragraphTag = (Element) document.getElementsByTagName("p").item(0);
+    // Mark the paragraph tag element as trusted
+    SanitizingGadgetRewriter.bypassSanitization(paragraphTag, false);
+
+    // Now, clone the paragraph tag and replace the paragraph tag
+    Element cloned = (Element) paragraphTag.cloneNode(true);
+    paragraphTag.getParentNode().replaceChild(cloned, paragraphTag);
+
+    rewriter.rewrite(gadget, mc);
+
+    // The document should be unchanged
+    String content = mc.getContent();
+    Matcher matcher = BODY_REGEX.matcher(content);
+    matcher.matches();
+    assertEquals("<p foo=\"bar\"></p>", matcher.group(1));
+  }
+
+  @Test
+  public void allCommentsStripped() throws Exception {
+    String markup = "<b>Hello, world</b><!--<b>evil</b>-->";
+    assertEquals("<b>Hello, world</b>", rewrite(gadget, markup, set("b"), set()));
+  }
+
+  @Test
+  public void doesNothingWhenNotSanitized() throws Exception {
+    String markup = "<script src=\"http://evil.org/evil\"></script> <b>hello</b>";
+    Gadget gadget = new Gadget().setContext(unsanitaryGadgetContext);
+    gadget.setSpec(new GadgetSpec(Uri.parse("www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='html'/></Module>"));
+    gadget.setCurrentView(gadget.getSpec().getViews().values().iterator().next());
+    assertEquals(markup, rewrite(gadget, markup, set("b"), set()));
+  }
+
+  @Test
+  public void forceSanitizeUnsanitaryGadget() throws Exception {
+    String markup =
+        "<p><style type=\"text/css\">A { font : bold; behavior : bad }</style>text <b>bold text</b></p>" +
+        "<b>Bold text</b><i>Italic text<b>Bold text</b></i>";
+
+    String sanitized = "<html><head></head><body><p><style>A {\n  font: bold\n}</style>text " +
+        "<b>bold text</b></p><b>Bold text</b></body></html>";
+
+    Gadget gadget = new Gadget().setContext(sanitaryGadgetContext);
+    gadget.setSpec(new GadgetSpec(Uri.parse("http://www.example.org/gadget.xml"),
+        "<Module><ModulePrefs title=''/><Content type='html'/></Module>"));
+    gadget.setCurrentView(gadget.getSpec().getViews().values().iterator().next());
+    assertEquals(sanitized, rewrite(gadget, markup, set("p", "b", "style"), set()));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java
new file mode 100644
index 0000000..d738737
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingProxyUriManagerTest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager.ProxyUri;
+import org.easymock.Capture;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Lists;
+
+import java.util.List;
+
+public class SanitizingProxyUriManagerTest {
+  private ProxyUriManager uriManager;
+  private Uri uri;
+  private ProxyUri proxyUri;
+
+  @Before
+  public void setUp() throws Exception {
+    uriManager = createMock(ProxyUriManager.class);
+    uri = new UriBuilder().setScheme("http").setAuthority("host.com").setPath("/path").toUri();
+    proxyUri = createMock(ProxyUri.class);
+  }
+
+  @Test
+  public void processPassesThrough() throws Exception {
+    Capture<Uri> uriCapture = new Capture<Uri>();
+    expect(uriManager.process(capture(uriCapture))).andReturn(proxyUri).once();
+    replay(uriManager);
+
+    SanitizingProxyUriManager rewriter = makeRewriter(null);
+    ProxyUri returned = rewriter.process(uri);
+
+    verify(uriManager);
+    assertSame(uri, uriCapture.getValue());
+    assertSame(returned, proxyUri);
+  }
+
+  @Test
+  public void makeSingleNoMime() throws Exception {
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    Capture<Integer> intCapture = new Capture<Integer>();
+    List<ProxyUri> input = Lists.newArrayList(proxyUri);
+    List<Uri> output = Lists.newArrayList(uri);
+    Integer refresh = new Integer(0);
+    expect(uriManager.make(capture(uriCapture), capture(intCapture)))
+        .andReturn(output).once();
+    replay(uriManager);
+    expect(proxyUri.setSanitizeContent(true)).andReturn(proxyUri).once();
+    replay(proxyUri);
+
+    SanitizingProxyUriManager rewriter = makeRewriter(null);
+    List<Uri> returned = rewriter.make(input, refresh);
+
+    verify(uriManager);
+    assertSame(uriCapture.getValue(), input);
+    assertSame(intCapture.getValue(), refresh);
+    assertEquals(1, returned.size());
+    verify(proxyUri);
+  }
+
+  @Test
+  public void makeSingleExpectedMime() throws Exception {
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    Capture<Integer> intCapture = new Capture<Integer>();
+    List<ProxyUri> input = Lists.newArrayList(proxyUri);
+    List<Uri> output = Lists.newArrayList(uri);
+    Integer refresh = new Integer(0);
+    String mime = "my/mime";
+    expect(uriManager.make(capture(uriCapture), capture(intCapture)))
+        .andReturn(output).once();
+    replay(uriManager);
+    expect(proxyUri.setSanitizeContent(true)).andReturn(proxyUri).once();
+    expect(proxyUri.setRewriteMimeType(mime)).andReturn(proxyUri).once();
+    replay(proxyUri);
+
+    SanitizingProxyUriManager rewriter = makeRewriter(mime);
+    List<Uri> returned = rewriter.make(input, refresh);
+
+    verify(uriManager);
+    assertSame(uriCapture.getValue(), input);
+    assertSame(intCapture.getValue(), refresh);
+    assertEquals(1, returned.size());
+    verify(proxyUri);
+  }
+
+  @Test
+  public void makeList() throws Exception {
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    Capture<Integer> intCapture = new Capture<Integer>();
+    ProxyUri proxyUri2 = createMock(ProxyUri.class);
+    List<ProxyUri> input = Lists.newArrayList(proxyUri, proxyUri2);
+    Uri uri2 = new UriBuilder().toUri();
+    List<Uri> output = Lists.newArrayList(uri, uri2);
+    Integer refresh = new Integer(0);
+    String mime = "my/mime";
+    expect(uriManager.make(capture(uriCapture), capture(intCapture)))
+        .andReturn(output).once();
+    replay(uriManager);
+    expect(proxyUri.setSanitizeContent(true)).andReturn(proxyUri).once();
+    expect(proxyUri.setRewriteMimeType(mime)).andReturn(proxyUri).once();
+    expect(proxyUri2.setSanitizeContent(true)).andReturn(proxyUri2).once();
+    expect(proxyUri2.setRewriteMimeType(mime)).andReturn(proxyUri2).once();
+    replay(proxyUri, proxyUri2);
+
+    SanitizingProxyUriManager rewriter = makeRewriter(mime);
+    List<Uri> returned = rewriter.make(input, refresh);
+
+    verify(uriManager);
+    assertSame(uriCapture.getValue(), input);
+    assertSame(intCapture.getValue(), refresh);
+    assertEquals(2, returned.size());
+    verify(proxyUri, proxyUri2);
+  }
+
+  private SanitizingProxyUriManager makeRewriter(String mime) {
+    return new SanitizingProxyUriManager(uriManager, mime);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingResponseRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingResponseRewriterTest.java
new file mode 100644
index 0000000..d2890c2
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/render/SanitizingResponseRewriterTest.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.render;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.caja.CajaCssParser;
+import org.apache.shindig.gadgets.parse.caja.CajaCssSanitizer;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.RewriterTestBase;
+import org.apache.shindig.gadgets.uri.PassthruManager;
+import org.junit.Test;
+
+import com.google.inject.util.Providers;
+
+public class SanitizingResponseRewriterTest extends RewriterTestBase {
+  private static final Uri CONTENT_URI = Uri.parse("http://www.example.org/content");
+  private static final String PROXY_HOST = "proxy.com";
+  private static final String PROXY_PATH = "/gadgets/proxy";
+  private static final String PROXY_BASE = PROXY_HOST + PROXY_PATH;
+
+  private String rewrite(HttpRequest request, HttpResponse response) throws Exception {
+    return rewrite(request, response, null);
+  }
+
+  private String rewrite(HttpRequest request, HttpResponse response, Gadget gadget) throws Exception {
+    request.setSanitizationRequested(true);
+    ResponseRewriter rewriter = createRewriter(Collections.<String>emptySet(),
+        Collections.<String>emptySet());
+
+    HttpResponseBuilder hrb = new HttpResponseBuilder(parser, response);
+    rewriter.rewrite(request, hrb, gadget);
+    if (hrb.getNumChanges() == 0) {
+      return null;
+    }
+    return hrb.getContent();
+  }
+
+  private ResponseRewriter createRewriter(Set<String> tags, Set<String> attributes) {
+    ContentRewriterFeature.Factory rewriterFeatureFactory =
+        new ContentRewriterFeature.Factory(null,
+          Providers.of(new ContentRewriterFeature.DefaultConfig(
+            ".*", "", "HTTP", "embed,img,script,link,style", false, false, false)));
+    return new SanitizingResponseRewriter(rewriterFeatureFactory,
+        new CajaCssSanitizer(new CajaCssParser()), new PassthruManager(PROXY_HOST, PROXY_PATH));
+  }
+
+  @Test
+  public void enforceInvalidProxedCssRejected() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/css");
+    HttpResponse response = new HttpResponseBuilder().setResponseString("doEvil()").create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+    assertEquals(sanitized, rewrite(req, response, gadget));
+  }
+
+  @Test
+  public void enforceValidProxedCssAccepted() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/css");
+    HttpResponse response = new HttpResponseBuilder().setResponseString(
+        "@import url('http://www.evil.com/more.css'); A { font : BOLD }").create();
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized =
+      // Resultant URL is just the "sanitized" version of same, since we're using
+      // PassthruUriManager for testing purposes.
+      "@import url('http://" + PROXY_BASE + "?url="
+        + "http%3A%2F%2Fwww.evil.com%2Fmore.css&sanitize=1&rewriteMime=text%2Fcss');\n"
+        + "A {\n"
+        + "  font: BOLD\n"
+        + '}';
+    String rewritten = rewrite(req, response);
+    String rewrittenGadget = rewrite(req, response, gadget);
+    assertEquals(sanitized, rewritten);
+    assertEquals(sanitized, rewrittenGadget);
+  }
+
+  @Test
+  public void enforceValidProxedCssAcceptedNoCache() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/css");
+    req.setIgnoreCache(true);
+    HttpResponse response = new HttpResponseBuilder().setResponseString(
+        "@import url('http://www.evil.com/more.css'); A { font : BOLD }").create();
+    // The caja css sanitizer does *not* remove the initial colon in urls
+    // since this does not work in IE
+    String sanitized =
+      "@import url('http://" + PROXY_BASE + "?url="
+        + "http%3A%2F%2Fwww.evil.com%2Fmore.css&sanitize=1&rewriteMime=text%2Fcss');\n"
+        + "A {\n"
+        + "  font: BOLD\n"
+        + '}';
+    String rewritten = rewrite(req, response);
+    String rewrittenGadget = rewrite(req, response, gadget);
+    assertEquals(sanitized, rewritten);
+    assertEquals(sanitized, rewrittenGadget);
+  }
+
+  @Test
+  public void enforceInvalidProxedImageRejected() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("image/*");
+    HttpResponse response = new HttpResponseBuilder().setResponse("NOTIMAGE".getBytes()).create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+    assertEquals(sanitized, rewrite(req, response, gadget));
+  }
+
+  @Test
+  public void validProxiedImageAccepted() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("image/*");
+    HttpResponse response = new HttpResponseBuilder().setResponse(
+        IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream(
+            "org/apache/shindig/gadgets/rewrite/image/inefficient.png"))).create();
+    assertNull(rewrite(req, response));
+    assertNull(rewrite(req, response, gadget));
+  }
+
+  @Test
+  public void enforceUnknownMimeTypeRejected() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    req.setRewriteMimeType("text/foo");
+    HttpResponse response = new HttpResponseBuilder().setResponseString("doEvil()").create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+    assertEquals(sanitized, rewrite(req, response, gadget));
+  }
+
+  @Test
+  public void enforceMissingMimeTypeRejected() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    HttpRequest req = new HttpRequest(CONTENT_URI);
+    // A request without a mime type, but requesting sanitization, should be rejected
+    req.setRewriteMimeType(null);
+    HttpResponse response = new HttpResponseBuilder().setResponseString("doEvil()").create();
+    String sanitized = "";
+    assertEquals(sanitized, rewrite(req, response));
+    assertEquals(sanitized, rewrite(req, response, gadget));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceVisitorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceVisitorTest.java
new file mode 100644
index 0000000..54dad2d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/AbsolutePathReferenceVisitorTest.java
@@ -0,0 +1,249 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor.VisitStatus;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Test;
+import org.w3c.dom.Comment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AbsolutePathReferenceVisitorTest extends DomWalkerTestBase {
+  private static final Uri ABSOLUTE_URI = Uri.parse("http://host.com/path");
+  private static final String JS_URI_STR = "javascript:foo();";
+  private static final Uri RELATIVE_URI = Uri.parse("/host/relative");
+  private static final Uri RELATIVE_RESOLVED_URI = GADGET_URI.resolve(RELATIVE_URI);
+  private static final Uri PATH_RELATIVE_URI = Uri.parse("path/relative");
+  private static final Uri PATH_RELATIVE_RESOLVED_URI = GADGET_URI.resolve(PATH_RELATIVE_URI);
+  private static final String INVALID_URI_STRING = "!^|BAD URI|^!";
+
+  AbsolutePathReferenceVisitor visitorForAllTags() {
+    return new AbsolutePathReferenceVisitor(
+        AbsolutePathReferenceVisitor.Tags.RESOURCES,
+        AbsolutePathReferenceVisitor.Tags.HYPERLINKS);
+  }
+
+  AbsolutePathReferenceVisitor visitorForHyperlinks() {
+    return new AbsolutePathReferenceVisitor(
+        AbsolutePathReferenceVisitor.Tags.HYPERLINKS);
+  }
+
+  AbsolutePathReferenceVisitor visitorForResources() {
+    return new AbsolutePathReferenceVisitor(
+        AbsolutePathReferenceVisitor.Tags.RESOURCES);
+  }
+
+  @Test
+  public void bypassComment() throws Exception {
+    Comment comment = doc.createComment("howdy pardner");
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(comment));
+  }
+
+  @Test
+  public void bypassText() throws Exception {
+    Text text = doc.createTextNode("back scratchah! get ya back scratcha he'yah!");
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(text));
+  }
+
+  @Test
+  public void bypassNonSupportedTag() throws Exception {
+    Element div = elem("div", "src", RELATIVE_URI.toString(), "href", RELATIVE_URI.toString());
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(div));
+  }
+
+  @Test
+  public void bypassObjectTag() throws Exception {
+    Element objectElement = elem("object", "src", RELATIVE_URI.toString());
+    assertEquals("Element with object tag should be bypassed",
+                 VisitStatus.BYPASS, getVisitStatus(objectElement));
+  }
+
+  @Test
+  public void bypassTagWithoutAttrib() throws Exception {
+    Element a = elem("a");
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(a));
+  }
+
+  @Test
+  public void absolutifyTagA() throws Exception {
+    checkAbsolutifyStates("a");
+  }
+
+  @Test
+  public void absolutifyTagImg() throws Exception {
+    checkAbsolutifyStates("img");
+  }
+
+  @Test
+  public void absolutifyTagInput() throws Exception {
+    checkAbsolutifyStates("input");
+  }
+
+  @Test
+  public void absolutifyTagBody() throws Exception {
+    checkAbsolutifyStates("body");
+  }
+
+  @Test
+  public void absolutifyTagLink() throws Exception {
+    Element cssLink = elem("link", "href", RELATIVE_URI.toString(),
+                           "rel", "stylesheet", "type", "text/css");
+    assertEquals("CSS link tag should not be bypassed",
+                 VisitStatus.MODIFY, getVisitStatus(cssLink));
+  }
+
+  @Test
+  public void bypassTagLinkWithNoRel() throws Exception {
+    Element cssLink = elem("link", "href", RELATIVE_URI.toString(), "type", "text/css");
+    assertEquals("CSS link tag should be bypassed",
+                 VisitStatus.BYPASS, getVisitStatus(cssLink));
+  }
+
+  @Test
+  public void bypassTagLinkWithNoType() throws Exception {
+    Element cssLink = elem("link", "href", RELATIVE_URI.toString(), "rel", "stylesheet");
+    assertEquals("CSS link tag should be bypassed",
+                 VisitStatus.BYPASS, getVisitStatus(cssLink));
+  }
+
+  @Test
+  public void bypassTagLinkAlternate() throws Exception {
+    Element cssLink = elem("link", "href", RELATIVE_URI.toString(),
+                           "rel", "alternate", "hreflang", "el");
+    assertEquals("CSS link tag should be bypassed",
+                 VisitStatus.BYPASS, getVisitStatus(cssLink));
+  }
+
+  @Test
+  public void absolutifyTagScript() throws Exception {
+    checkAbsolutifyStates("script");
+  }
+
+  @Test
+  public void revisitDoesNothing() throws Exception {
+    assertFalse(visitorForAllTags().revisit(gadget(), null));
+  }
+
+  @Test
+  public void resolveRelativeToBaseTagIfPresent() throws Exception {
+    Element baseTag = elem("base", "href", "http://www.example.org");
+    Element img = elem("img", "src", RELATIVE_URI.toString());
+    Element html = htmlDoc(null, baseTag, img);
+
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(baseTag));
+    assertEquals(VisitStatus.MODIFY, getVisitStatus(img));
+    assertEquals("http://www.example.org" + RELATIVE_URI.toString(),
+                 img.getAttribute("src"));
+  }
+
+  @Test
+  public void getBaseHrefReturnsNullIfBaseTagWithoutHrefAttribute()
+      throws Exception {
+    Element baseTag = elem("base");
+    Element img = elem("img", "src", RELATIVE_URI.toString());
+    Element html = htmlDoc(null, baseTag, img);
+
+    AbsolutePathReferenceVisitor visitor = visitorForAllTags();
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(baseTag));
+    assertEquals(VisitStatus.MODIFY, getVisitStatus(img));
+    assertEquals(RELATIVE_RESOLVED_URI.toString(), img.getAttribute("src"));
+  }
+
+  @Test
+  public void testGetBaseUri() throws Exception {
+    Element baseTag1 = elem("base", "href", "http://www.example1.org");
+    Element baseTag2 = elem("base", "href", "http://www.example2.org");
+
+    Element img = elem("img", "src", RELATIVE_URI.toString());
+    Element a = elem("a", "href", RELATIVE_URI.toString());
+
+    Node[] headNodes = { baseTag1 };
+    Element html = htmlDoc(headNodes, baseTag2, img, a);
+
+    AbsolutePathReferenceVisitor visitor = visitorForAllTags();
+    assertEquals("http://www.example1.org",
+                 visitor.getBaseHref(html.getOwnerDocument()));
+    assertEquals("http://www.example1.org",
+                 visitor.getBaseUri(html.getOwnerDocument()).toString());
+  }
+
+  private void checkAbsolutifyStates(String tagName) throws Exception {
+    String lcTag = tagName.toLowerCase();
+    String ucTag = tagName.toUpperCase();
+    Map<String, String> resourceTags = new HashMap<String, String>();
+    resourceTags.putAll(AbsolutePathReferenceVisitor.Tags
+        .RESOURCES.getResourceTags());
+    resourceTags.putAll(AbsolutePathReferenceVisitor.Tags
+        .HYPERLINKS.getResourceTags());
+    String validAttr = resourceTags.get(lcTag);
+    String invalidAttr = validAttr + "whoknows";
+
+    // lowercase, correct attrib, relative-possible URL
+    Element lcValidRelative = elem(lcTag, validAttr, RELATIVE_URI.toString());
+    assertEquals(VisitStatus.MODIFY, getVisitStatus(lcValidRelative));
+    assertEquals(RELATIVE_RESOLVED_URI.toString(), lcValidRelative.getAttribute(validAttr));
+
+    Element lcValidPathRelative = elem(lcTag, validAttr, PATH_RELATIVE_URI.toString());
+    assertEquals(VisitStatus.MODIFY, getVisitStatus(lcValidPathRelative));
+    assertEquals(PATH_RELATIVE_RESOLVED_URI.toString(),
+        lcValidPathRelative.getAttribute(validAttr));
+
+    // uppercase, same
+    Element ucValidRelative = elem(ucTag, validAttr, RELATIVE_URI.toString());
+    assertEquals(VisitStatus.MODIFY, getVisitStatus(ucValidRelative));
+    assertEquals(RELATIVE_RESOLVED_URI.toString(), ucValidRelative.getAttribute(validAttr));
+
+    Element ucValidPathRelative = elem(ucTag, validAttr, PATH_RELATIVE_URI.toString());
+    assertEquals(VisitStatus.MODIFY, getVisitStatus(ucValidPathRelative));
+    assertEquals(PATH_RELATIVE_RESOLVED_URI.toString(),
+        ucValidPathRelative.getAttribute(validAttr));
+
+    // lowercase, correct attrib, invalid URL
+    Element lcValidInvalid = elem(lcTag, validAttr, INVALID_URI_STRING);
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(lcValidRelative));
+    assertEquals(INVALID_URI_STRING, lcValidInvalid.getAttribute(validAttr));
+
+    // lowercase, correct attrib, absolute URL
+    Element lcValidAbsolute = elem(lcTag, validAttr, ABSOLUTE_URI.toString());
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(lcValidAbsolute));
+    assertEquals(ABSOLUTE_URI.toString(), lcValidAbsolute.getAttribute(validAttr));
+
+    // lowercase, invalid attrib, relative-possible URL
+    Element lcInvalidRelative = elem(lcTag, invalidAttr, RELATIVE_URI.toString());
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(lcInvalidRelative));
+    assertEquals(RELATIVE_URI.toString(), lcInvalidRelative.getAttribute(invalidAttr));
+
+    // lowercase, valid attrib, absolute (JS) URL
+    Element lcValidJs = elem(lcTag, validAttr, JS_URI_STR);
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(lcValidJs));
+    assertEquals(JS_URI_STR, lcValidJs.getAttribute(validAttr));
+  }
+
+  private VisitStatus getVisitStatus(Node node) throws Exception {
+    return visitorForAllTags().visit(gadget(), node);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java
new file mode 100644
index 0000000..de7dd26
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseRewriterTestCase.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Joiner;
+import com.google.inject.util.Providers;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.util.Modules;
+
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+
+import java.util.Set;
+
+/**
+ * Base class for testing content rewriting functionality
+ */
+public abstract class BaseRewriterTestCase {
+  public static final Uri SPEC_URL = Uri.parse("http://www.example.org/dir/g.xml");
+  public static final String DEFAULT_PROXY_BASE = "http://www.test.com/dir/proxy?url=";
+  public static final String DEFAULT_CONCAT_BASE = "http://www.test.com/dir/concat?";
+
+  public static final String MOCK_CONTAINER = "mock";
+  public static final String MOCK_PROXY_BASE =
+    replaceDefaultWithMockServer(DEFAULT_PROXY_BASE);
+  public static final String MOCK_CONCAT_BASE =
+    replaceDefaultWithMockServer(DEFAULT_CONCAT_BASE);
+  protected final String TAGS = "embed,img,script,link,style";
+
+  protected Set<String> tags;
+  protected ContentRewriterFeature.Config defaultRewriterFeature;
+  protected ContentRewriterFeature.Factory rewriterFeatureFactory;
+  protected GadgetHtmlParser parser;
+  protected Injector injector;
+  protected HttpResponseBuilder fakeResponse;
+  protected IMocksControl control;
+
+  @Before
+  public void setUp() throws Exception {
+    rewriterFeatureFactory = new ContentRewriterFeature.Factory(null,
+        Providers.of(new ContentRewriterFeature.DefaultConfig(".*", "", "86400", "embed,img,script,link,style", false, false, false)));
+    defaultRewriterFeature = rewriterFeatureFactory.getDefault();
+    tags = defaultRewriterFeature.getIncludedTags();
+    injector = Guice.createInjector(getParseModule(), new PropertiesModule(), new TestModule());
+    parser = injector.getInstance(GadgetHtmlParser.class);
+    fakeResponse = new HttpResponseBuilder().setHeader("Content-Type", "unknown")
+        .setResponse(new byte[]{ (byte)0xFE, (byte)0xFF});
+    control = EasyMock.createControl();
+  }
+
+  private Module getParseModule() {
+    return Modules.override(new ParseModule()).with(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(GadgetHtmlParser.class).to(getParserClass());
+      }
+    });
+  }
+
+  protected Class<? extends GadgetHtmlParser> getParserClass() {
+    return NekoSimplifiedHtmlParser.class;
+  }
+
+  public static GadgetSpec createSpecWithRewrite(String include, String exclude, String expires,
+      Set<String> tags) throws GadgetException {
+    StringBuilder xml = new StringBuilder();
+    xml.append("<Module>");
+    xml.append("<ModulePrefs title=\"title\">");
+    xml.append("<Optional feature=\"content-rewrite\">\n");
+    if(expires != null)
+      xml.append("      <Param name=\"expires\">" + expires + "</Param>\n");
+    if(include != null)
+      xml.append("      <Param name=\"include-urls\">" + include + "</Param>\n");
+    if(exclude != null)
+      xml.append("      <Param name=\"exclude-urls\">" + exclude + "</Param>\n");
+    if(tags != null)
+      xml.append("      <Param name=\"include-tags\">" + Joiner.on(',').join(tags) + "</Param>\n");
+    xml.append("</Optional>");
+    xml.append("</ModulePrefs>");
+    xml.append("<Content type=\"html\">Hello!</Content>");
+    xml.append("</Module>");
+    return new GadgetSpec(SPEC_URL, xml.toString());
+  }
+
+  public static GadgetSpec createSpecWithRewriteOS9(String[] includes, String[] excludes, String expires,
+      Set<String> tags) throws GadgetException {
+    StringBuilder xml = new StringBuilder();
+    xml.append("<Module>");
+    xml.append("<ModulePrefs title=\"title\">");
+    xml.append("<Optional feature=\"content-rewrite\">\n");
+    if(expires != null)
+      xml.append("      <Param name=\"expires\">" + expires + "</Param>\n");
+    if(includes != null)
+      for (String include : includes) {
+        xml.append("      <Param name=\"include-url\">" + include + "</Param>\n");
+      }
+    if(excludes != null)
+      for (String exclude : excludes) {
+        xml.append("      <Param name=\"exclude-url\">" + exclude + "</Param>\n");
+      }
+    if(tags != null)
+      xml.append("      <Param name=\"include-tags\">" + Joiner.on(',').join(tags) + "</Param>\n");
+    xml.append("</Optional>");
+    xml.append("</ModulePrefs>");
+    xml.append("<Content type=\"html\">Hello!</Content>");
+    xml.append("</Module>");
+    return new GadgetSpec(SPEC_URL, xml.toString());
+  }
+
+  public static GadgetSpec createSpecWithoutRewrite() throws GadgetException {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"title\">" +
+                 "</ModulePrefs>" +
+                 "<Content type=\"html\">Hello!</Content>" +
+                 "</Module>";
+    return new GadgetSpec(SPEC_URL, xml);
+  }
+
+  public static String replaceDefaultWithMockServer(String originalText) {
+    return originalText.replace("test.com", "mock.com");
+  }
+
+  protected ContentRewriterFeature.Factory mockContentRewriterFeatureFactory(
+      ContentRewriterFeature.Config feature) {
+    return new FakeRewriterFeatureFactory(feature);
+  }
+
+  String rewriteHelper(GadgetRewriter rewriter, String s)
+      throws Exception {
+    MutableContent mc = rewriteContent(rewriter, s, null);
+    String rewrittenContent = mc.getContent();
+
+    // Strip around the HTML tags for convenience
+    int htmlTagIndex = rewrittenContent.indexOf("<HTML>");
+    if (htmlTagIndex != -1) {
+      return rewrittenContent.substring(htmlTagIndex + 6,
+          rewrittenContent.lastIndexOf("</HTML>"));
+    }
+    return rewrittenContent;
+  }
+
+  protected MutableContent rewriteContent(GadgetRewriter rewriter, String s,
+      final String container) throws Exception {
+    return rewriteContent(rewriter, s, container, false, false);
+  }
+
+  protected MutableContent rewriteContent(GadgetRewriter rewriter, String s,
+      final String container, final boolean debug, final boolean ignoreCache)
+      throws Exception {
+    MutableContent mc = new MutableContent(parser, s);
+
+    GadgetSpec spec = new GadgetSpec(SPEC_URL,
+        "<Module><ModulePrefs title=''/><Content><![CDATA[" + s + "]]></Content></Module>");
+
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        return SPEC_URL;
+      }
+
+      @Override
+      public String getContainer() {
+        return container;
+      }
+
+      @Override
+      public boolean getDebug() {
+        return debug;
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return ignoreCache;
+      }
+    };
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec);
+    rewriter.rewrite(gadget, mc);
+    return mc;
+  }
+
+  private static class FakeRewriterFeatureFactory extends ContentRewriterFeature.Factory {
+    private final ContentRewriterFeature.Config feature;
+
+    public FakeRewriterFeatureFactory(ContentRewriterFeature.Config feature) {
+      super(null, Providers.of(new ContentRewriterFeature.DefaultConfig(".*", "", "HTTP", "", false, false, false)));
+      this.feature = feature;
+    }
+
+    @Override
+    public ContentRewriterFeature.Config get(GadgetSpec spec) {
+      return feature;
+    }
+
+    @Override
+    public ContentRewriterFeature.Config get(HttpRequest request) {
+      return feature;
+    }
+  }
+
+  private static class TestModule extends AbstractModule {
+
+    @Override
+    protected void configure() {
+      bind(RequestPipeline.class).toInstance(new RequestPipeline() {
+        public HttpResponse execute(HttpRequest request) { return null; }
+      });
+
+      bind(GadgetSpecFactory.class).toInstance(new GadgetSpecFactory() {
+        public GadgetSpec getGadgetSpec(GadgetContext context) {
+          return null;
+        }
+        public Uri getGadgetUri(GadgetContext context) {
+          return null;
+        }
+      });
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseTagRemoverRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseTagRemoverRewriterTest.java
new file mode 100644
index 0000000..79a8a97
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/BaseTagRemoverRewriterTest.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test case for BaseTagRemoverRewriter.
+ */
+public class BaseTagRemoverRewriterTest extends RewriterTestBase {
+  BaseTagRemoverRewriter rewriter;
+
+  CajaHtmlParser parser;
+  ParseModule.DOMImplementationProvider domImpl;
+
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    rewriter = new BaseTagRemoverRewriter();
+    domImpl = new ParseModule.DOMImplementationProvider();
+    parser = new CajaHtmlParser(domImpl.get());
+  }
+
+  public void testRemoveBaseTag(Gadget gadget) throws Exception {
+    String content = "<html><head><base href='http://www.ppq.com/'>"
+                     + "</head><body>"
+                     + "<img src='/img1.png'>"
+                     + "</body></html>";
+    String expected = "<html><head>"
+                     + "</head><body>"
+                     + "<img src=\"/img1.png\">"
+                     + "</body></html>";
+
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.google.com/"));
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(200)
+        .setHeader("Content-Type", "text/html")
+        .setResponse(content.getBytes())
+        .create();
+    HttpResponseBuilder builder = new HttpResponseBuilder(parser, resp);
+
+    rewriter.rewrite(req, builder, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+                 StringUtils.deleteWhitespace(builder.getContent()));
+  }
+
+  @Test
+  public void testRemoveBaseTagGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testRemoveBaseTag(gadget);
+  }
+
+  @Test
+  public void testRemoveBaseTagNoGadget() throws Exception {
+    testRemoveBaseTag(null);
+  }
+
+  public void testNoBaseTag(Gadget gadget) throws Exception {
+    String content = "<html><head>"
+                     + "</head><body>"
+                     + "<img src='/img1.png'>"
+                     + "</body></html>";
+    String expected = "<html><head>"
+                     + "</head><body>"
+                     + "<img src=\"/img1.png\">"
+                     + "</body></html>";
+
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.google.com/"));
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(200)
+        .setHeader("Content-Type", "text/html")
+        .setResponse(content.getBytes())
+        .create();
+    HttpResponseBuilder builder = new HttpResponseBuilder(parser, resp);
+
+    rewriter.rewrite(req, builder, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+                 StringUtils.deleteWhitespace(builder.getContent()));
+  }
+
+  @Test
+  public void testNoBaseTagGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testNoBaseTag(gadget);
+  }
+
+  @Test
+  public void testNoBaseTagNoGadget() throws Exception {
+    testNoBaseTag(null);
+  }
+
+  public void testContentTypeString(Gadget gadget) throws Exception {
+    String content = "Hello world. My name is gagan<html><head>"
+                     + "<base href='http://hello.com/'></head><body>"
+                     + "<img src='/img1.png'>"
+                     + "</body></html>";
+    String expected = "Hello world. My name is gagan<html><head>"
+                     + "<base href='http://hello.com/'></head><body>"
+                     + "<img src='/img1.png'>"
+                     + "</body></html>";
+
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.google.com/"));
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(200)
+        .setHeader("Content-Type", "text/plain")
+        .setResponse(content.getBytes())
+        .create();
+    HttpResponseBuilder builder = new HttpResponseBuilder(parser, resp);
+
+    rewriter.rewrite(req, builder, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+                 StringUtils.deleteWhitespace(builder.getContent()));
+  }
+
+  @Test
+  public void testContentTypeStringGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testContentTypeString(gadget);
+  }
+
+  @Test
+  public void testContentTypeStringNoGadget() throws Exception {
+    testContentTypeString(null);
+  }
+
+  public void testContentTypeXml(Gadget gadget) throws Exception {
+    String content = "Hello world. My name is gagan<html><head>"
+                     + "<base href='http://hello.com/'></head><body>"
+                     + "<img src='/img1.png'>"
+                     + "</body></html>";
+    String expected = "Hello world. My name is gagan<html><head>"
+                     + "<base href='http://hello.com/'></head><body>"
+                     + "<img src='/img1.png'>"
+                     + "</body></html>";
+
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.google.com/"));
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(200)
+        .setHeader("Content-Type", "text/xml")
+        .setResponse(content.getBytes())
+        .create();
+    HttpResponseBuilder builder = new HttpResponseBuilder(parser, resp);
+
+    rewriter.rewrite(req, builder, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+                 StringUtils.deleteWhitespace(builder.getContent()));
+  }
+
+  @Test
+  public void testContentTypeXmlGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testContentTypeString(gadget);
+  }
+
+  @Test
+  public void testContentTypeXmlNoGadget() throws Exception {
+    testContentTypeString(null);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/CacheEnforcementVisitorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/CacheEnforcementVisitorTest.java
new file mode 100644
index 0000000..5fd0072
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/CacheEnforcementVisitorTest.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.AbstractHttpCache;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.parse.ParseModule.DOMImplementationProvider;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for CacheEnforcementVisitor.
+ */
+public class CacheEnforcementVisitorTest extends DomWalkerTestBase {
+  private ExecutorService executor;
+  private TestHttpCache cache;
+  protected Document doc;
+  private static final Map<String, String> ALL_RESOURCES =
+      CacheEnforcementVisitor.Tags.ALL_RESOURCES.getResourceTags();
+  private static final String IMG_URL = "http://www.example.org/1.gif";
+  private static final String CONTAINER = "test_container";
+  private static final String GADGET = "http://www.test.com";
+
+  @Before
+  public void setUp() {
+    executor = MoreExecutors.sameThreadExecutor();
+    DOMImplementationProvider domImpl = new DOMImplementationProvider();
+    doc = domImpl.get().createDocument(null, null, null);
+    cache = new TestHttpCache();
+    cache.setRefetchStrictNoCacheAfterMs(86400L);
+  }
+
+  @Test
+  public void testCreateNewHttpRequest() throws Exception {
+    Gadget gadget = createMock(Gadget.class);
+    Uri uri = Uri.parse(GADGET);
+    GadgetSpec gadgetSpec = createMock(GadgetSpec.class);
+    expect(gadgetSpec.getUrl()).andReturn(uri);
+    expect(gadget.getSpec()).andReturn(gadgetSpec);
+
+    GadgetContext context = createMock(GadgetContext.class);
+    expect(context.getContainer()).andReturn(CONTAINER);
+    expect(gadget.getContext()).andReturn(context);
+
+    replay(gadgetSpec);
+    replay(context);
+    replay(gadget);
+
+    CacheEnforcementVisitor visitor = new CacheEnforcementVisitor(
+        null, null, null, null, CacheEnforcementVisitor.Tags.ALL_RESOURCES);
+    HttpRequest newRequest = visitor.createNewHttpRequest(gadget, IMG_URL);
+    assertEquals(CONTAINER, newRequest.getContainer());
+    assertEquals("1", newRequest.getParam(CacheEnforcementVisitor.CACHE_ENFORCEMENT_FETCH_PARAM));
+    assertEquals(uri, newRequest.getGadget());
+  }
+
+  @Test
+  public void testStaleImgWithNegativeTtlReservedAndFetchTriggered() throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .setCacheTtl(-1).create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, true);
+  }
+
+  @Test
+  public void testStaleImgWithZeroMaxAgeReservedAndFetchNotTriggered() throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .addHeader("Cache-Control", "max-age=0").create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, false);
+  }
+
+  @Test
+  public void testImgWithErrorResponseReservedAndFetchNotTriggered() throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .setHttpStatusCode(404).create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, false);
+  }
+
+  @Test
+  public void testImgBypassedAndFetchNotTriggered() throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test").create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, true, false);
+  }
+
+  @Test
+  public void testEmbedImgBypassedAndFetchNotTriggered() throws Exception {
+    // This test checks that non img nodes are always bypassed and fetches are not triggered for
+    // them, since they aren't in the tags specified in CacheEnforcementVisitor.
+    checkVisitBypassedAndFetchTriggered("embed", IMG_URL, true, false);
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test").create());
+    checkVisitBypassedAndFetchTriggered("embed", IMG_URL, true, false);
+  }
+
+  @Test
+  public void testImgNotInCacheReservedAndFetchTriggered() throws Exception {
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, true);
+  }
+
+  @Test
+  public void testImgWithCacheControlPrivateReservedAndFetchNotTriggered() throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .addHeader("Cache-Control", "private").create());
+    // Ensure that the strict no-cache resource is cached.
+    assertTrue(cache.getResponse(new HttpRequest(Uri.parse(IMG_URL))) != null);
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, false);
+  }
+
+  @Test
+  public void testImgWithCacheControlNoCacheReservedAndFetchNotTriggered() throws Exception {
+
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .addHeader("Cache-Control", "no-cache").create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, false);
+  }
+
+  @Test
+  public void testImgWithCacheControlNoStoreReservedAndFetchNotTriggered() throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .addHeader("Cache-Control", "no-store").create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, false);
+  }
+
+  @Test
+  public void testImgWithPragmaNoCacheReservedAndFetchNotTriggered() throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .addHeader("Pragma", "no-cache").create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, false);
+  }
+
+  @Test
+  public void testImgWithSetCookieButNotStrictNoCacheReservedAndFetchNotTriggered()
+      throws Exception {
+    cache.addResponse(new HttpRequest(Uri.parse(IMG_URL)),
+                      new HttpResponseBuilder().setResponseString("test")
+                          .addHeader("Cache-Control", "public,max-age=86400")
+                          .addHeader("Set-Cookie", "name=val").create());
+    checkVisitBypassedAndFetchTriggered("img", IMG_URL, false, false);
+  }
+
+  /**
+   * Checks whether a node with the specified tag and url is bypassed by the
+   * CacheAwareResourceMutateVisitor, and also whether a fetch is triggered for
+   * the resource.
+   *
+   * @param tag The name of the tag for the node.
+   * @param url The url of the node.
+   * @param expectBypass Boolean to check if the node will be bypassed by the
+   *     visitor.
+   * @param expectFetch Boolean to check if a fetch will be triggered for the
+   * resource.
+   * @throws Exception
+   */
+  private void checkVisitBypassedAndFetchTriggered(String tag, String url, boolean expectBypass,
+                                                   boolean expectFetch) throws Exception {
+    // Try to get the attribute name for the specified tag, or otherwise use src.
+    String attrName = ALL_RESOURCES.get(tag.toLowerCase());
+    attrName = attrName != null ? attrName : "src";
+
+    // Create a node with the specified tag name and attribute.
+    Element node = doc.createElement(tag);
+    Attr attr = doc.createAttribute(attrName);
+    attr.setValue(url);
+    node.setAttributeNode(attr);
+
+    // Mock the RequestPipeline.
+    RequestPipeline requestPipeline = createStrictMock(RequestPipeline.class);
+    if (expectFetch) {
+      expect(requestPipeline.execute(new HttpRequest(Uri.parse(url))))
+          .andReturn(new HttpResponseBuilder().setResponseString("test").create()).once();
+    }
+    replay(requestPipeline);
+
+    ContentRewriterFeature.Config config = createMock(ContentRewriterFeature.Config.class);
+    expect(config.shouldRewriteURL(IMG_URL)).andReturn(true).anyTimes();
+    expect(config.shouldRewriteTag("img")).andReturn(true).anyTimes();
+    replay(config);
+
+    CacheEnforcementVisitor visitor = new CacheEnforcementVisitor(
+        config, executor, cache, requestPipeline,
+        ProxyingVisitor.Tags.SCRIPT, ProxyingVisitor.Tags.STYLESHEET,
+        ProxyingVisitor.Tags.EMBEDDED_IMAGES);
+
+    DomWalker.Visitor.VisitStatus status = visitor.visit(null, node);
+
+    executor.shutdown();
+    executor.awaitTermination(5, TimeUnit.SECONDS);
+    verify(requestPipeline);
+    verify(config);
+
+    assertEquals(expectBypass, status == DomWalker.Visitor.VisitStatus.BYPASS);
+  }
+
+  private static class TestHttpCache extends AbstractHttpCache {
+    protected final Map<String, HttpResponse> map;
+
+    public TestHttpCache() {
+      map = Maps.newHashMap();
+    }
+
+    public void addResponseImpl(String key, HttpResponse response) {
+      map.put(key, response);
+    }
+
+    public HttpResponse getResponseImpl(String key) {
+      return map.get(key);
+    }
+
+    public void removeResponseImpl(String key) {
+      map.remove(key);
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ConcatVisitorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ConcatVisitorTest.java
new file mode 100644
index 0000000..d23ef74
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ConcatVisitorTest.java
@@ -0,0 +1,922 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
+import org.apache.shindig.gadgets.parse.HtmlSerializer;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlSerializer;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor.VisitStatus;
+import org.apache.shindig.gadgets.uri.ConcatUriManager;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+import java.util.Map;
+
+public class ConcatVisitorTest extends DomWalkerTestBase {
+  private static final String JS1_URL_STR = "http://one.com/foo.js?test=1&ui=2";
+  private Node js1;
+
+  private static final String JS2_URL_STR = "http://two.com/foo.js";
+  private Node js2;
+
+  private static final String JS3_URL_STR = "http://three.com/foo.js";
+  private Node js3;
+
+  private static final String JS4_URL_STR = "http://four.com/foo.js";
+  private Node js4;
+
+  private static final String JS5_URL_STR = "http://~^|BAD |^/foo.js";
+  private Node js5;
+
+  private static final String JS6_URL_STR = "http://six.com/foo.js";
+  private Node js6;
+
+  private static final String CSS1_URL_STR = "http://one.com/foo.js";
+  private Node css1;
+
+  private static final String CSS2_URL_STR = "http://two.com/foo.js";
+  private Node css2;
+
+  private static final String CSS3_URL_STR = "http://three.com/foo.js";
+  private Node css3;
+
+  private static final String CSS4_URL_STR = "http://four.com/foo.js";
+  private Node css4;
+
+  private static final String CSS5_URL_STR = "http://five.com/foo.js";
+  private Node css5;
+
+  private static final String CSS6_URL_STR = "http://six.com/foo.js";
+  private Node css6;
+
+  private static final String CSS7_URL_STR = "http://seven.com/foo.js";
+  private Node css7;
+
+  private static final String CSS8_URL_STR = "http://eight.com/foo.js";
+  private Node css8;
+
+  private static final String CSS9_URL_STR = "http://nine.com/foo.js";
+  private Node css9;
+
+  private static final String CSS10_URL_STR = "http://ten.com/foo.js";
+  private Node css10;
+
+  private static final String CSS11_URL_STR = "http://eleven.com/foo.js";
+  private Node css11;
+
+  private static final String CSS12_URL_STR = "http://twelve.com/foo.js";
+  private Node css12;
+
+  private static final Uri CONCAT_BASE_URI = Uri.parse("http://test.com/proxy");
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    js1 = elem("script", "src", JS1_URL_STR);
+    js2 = elem("script", "src", JS2_URL_STR);
+    js3 = elem("script", "src", JS3_URL_STR);
+    js4 = elem("script", "src", JS4_URL_STR);
+    js5 = elem("script", "src", JS5_URL_STR);
+    js6 = elem("script", "src", JS6_URL_STR);
+    css1 = elem("link", "rel", "Stylesheet", "type", "Text/css", "href", CSS1_URL_STR);
+    css2 = elem("link", "rel", "stylesheet", "type", "text/css", "href", CSS2_URL_STR);
+    css3 = elem("link", "rel", "stylesheet", "type", "text/css", "href", CSS3_URL_STR);
+    css4 = elem("link", "rel", "stylesheet", "type", "text/css", "href", CSS4_URL_STR);
+    css5 = elem("link", "rel", "stylesheet", "type", "text/css", "media", "print", "href", CSS5_URL_STR);
+    css6 = elem("link", "rel", "stylesheet", "type", "text/css", "media", "print", "href", CSS6_URL_STR);
+    css7 = elem("link", "rel", "stylesheet", "type", "text/css", "media", "screen", "href", CSS7_URL_STR);
+    css8 = elem("link", "rel", "stylesheet", "type", "text/css", "media", "screen", "href", CSS8_URL_STR);
+    css9 = elem("link", "rel", "stylesheet", "type", "text/css", "href", CSS9_URL_STR);
+    css10 = elem("link", "rel", "stylesheet", "type", "text/css", "media", "all", "href", CSS10_URL_STR);
+    css11 = elem("link", "rel", "stylesheet", "type", "text/css", "media", "all", "href", CSS11_URL_STR);
+    css12 = elem("link", "rel", "stylesheet", "type", "text/css", "media", "all", "href", CSS12_URL_STR);
+  }
+
+  @Test
+  public void dontVisitSingleJs() throws Exception {
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(js1, null, false, false));
+  }
+
+  @Test
+  public void dontVisitSingleCss() throws Exception {
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(css1, null, false));
+  }
+
+  @Test
+  public void visitSingleJsWhenSingleResourceEnabled() throws Exception {
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js1, null, false, true));
+  }
+
+  @Test
+  public void visitSingleCssWhenSingleResourceEnabled() throws Exception {
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(css1, null, true));
+  }
+
+  @Test
+  public void dontVisitJsWithoutSrc() throws Exception {
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(elem("script"), null, false, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(elem("script"), null, false, true));
+  }
+
+  @Test
+  public void dontVisitUnknown() throws Exception {
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(elem("div"), null, true, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(elem("div"), null, true, true));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(elem("div"), null, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(elem("div"), null, true));
+  }
+
+  @Test
+  public void dontVisitContigJsMiddleNotRewritable() throws Exception {
+    ContentRewriterFeature.Config config = config(".*two.*", false, false);
+    seqNodes(js1, js2, js3);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, js1));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, js2));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, js3));
+  }
+
+  @Test
+  public void visitJsButNotMiddleWhenNotRewritable() throws Exception {
+    ContentRewriterFeature.Config config = config(".*two.*", false, true);
+    seqNodes(js1, js2, js3);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(config, js1));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, js2));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(config, js3));
+  }
+
+  @Test
+  public void dontVisitContigCssMiddleNotRewritable() throws Exception {
+    ContentRewriterFeature.Config config = config(".*two.*", true, false);
+    seqNodes(css1, css2, css3);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(config, css1));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(config, css2));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(config, css3));
+  }
+
+  @Test
+  public void visitCssButNotMiddleWhenNotRewritable() throws Exception {
+    ContentRewriterFeature.Config config = config(".*two.*", true, true);
+    seqNodes(css1, css2, css3);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(config, css1));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(config, css2));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(config, css3));
+  }
+
+  @Test
+  public void dontVisitSeparatedJsNotSplit() throws Exception {
+    ContentRewriterFeature.Config config = config(null, false, false);
+    Node sep1 = elem("div");
+    Node sep2 = elem("span");
+    seqNodes(js1, sep1, js2, sep2, js3);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, js1));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, sep1));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, js2));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, sep2));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusJs(config, js3));
+  }
+
+  @Test
+  public void visitValidCss() throws Exception {
+    Node textNode = doc.createTextNode("");
+    Node node = elem("link", "type", "text/css", "rel", "stylesheet", "href", CSS1_URL_STR);
+    seqNodes(node, textNode, css1);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void visitCssSeperatedByTextNode() throws Exception {
+    Node textNode = doc.createTextNode("Data\n");
+    Node node = elem("link", "type", "text/css", "rel", "stylesheet", "href", CSS1_URL_STR);
+    seqNodes(node, textNode, css1);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void visitCssSeperatedByNormalComment() throws Exception {
+    Node commentNode = doc.createComment("This is a comment");
+    Node node = elem("link", "type", "text/css", "rel", "stylesheet", "href", CSS1_URL_STR);
+    seqNodes(node, commentNode, css1);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void dontVisitCssSeperatedByConditionalComment() throws Exception {
+    Node commentNode = doc.createComment("[if IE]");
+    Node node = elem("link", "type", "text/css", "rel", "stylesheet", "href", CSS1_URL_STR);
+    seqNodes(node, commentNode, css1);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, false));
+  }
+
+  @Test
+  public void visitCssSeperatedByConditionalCommentWhenSingleResourceConcatEnabled()
+      throws Exception {
+    Node commentNode = doc.createComment("[if IE]");
+    Node node = elem("link", "type", "text/css", "rel", "stylesheet", "href", CSS1_URL_STR);
+    seqNodes(node, commentNode, css1);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(node, null, true));
+  }
+
+
+  @Test
+  public void dontVisitCssWithoutRelAttrib() throws Exception {
+    Node node = elem("link", "type", "text/css", "href", CSS1_URL_STR);
+    seqNodes(node, css1);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void dontVisitCssWithoutTypeAttribAsCss() throws Exception {
+    Node node = elem("link", "rel", "stylesheet", "href", CSS1_URL_STR);
+    seqNodes(node, css1);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void dontVisitTypeCssWrongRelAttributes() throws Exception {
+    Node node = elem("link", "rel", "alternate", "type", "text/css", "href", CSS1_URL_STR);
+    seqNodes(node, css1);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void dontVisitTypeCssWrongTypeAttributes() throws Exception {
+    Node node = elem("link", "rel", "stylesheet", "type", "text/javascript", "href", CSS1_URL_STR);
+    seqNodes(node, css1);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void dontVisitCssWithoutAttribs() throws Exception {
+    Node node = elem("link", "href", CSS1_URL_STR);
+    seqNodes(node, css1);
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, false));
+    assertEquals(VisitStatus.BYPASS, getVisitStatusCss(node, null, true));
+  }
+
+  @Test
+  public void visitContigJs() throws Exception {
+    seqNodes(js1, js2, js3);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js1, null, false, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js2, null, false, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js3, null, false, false));
+  }
+
+  @Test
+  public void visitContigCss() throws Exception {
+    seqNodes(css1, css2, css3);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(css1, null, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(css2, null, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusCss(css3, null, false));
+  }
+
+  @Test
+  public void visitSplitJsSingle() throws Exception {
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js1, null, true, false));
+  }
+
+  @Test
+  public void visitSplitJsSeparated() throws Exception {
+    seqNodes(js1, elem("span"), js2, elem("div"), js3);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js1, null, true, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js2, null, true, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js3, null, true, false));
+  }
+
+  @Test
+  public void visitSplitJsContiguous() throws Exception {
+    seqNodes(js1, js2, js3);
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js1, null, true, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js2, null, true, false));
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatusJs(js3, null, true, false));
+  }
+
+  @Test
+  public void concatSingleJs() throws Exception {
+    List<Node> nodes = seqNodes(js1);
+    Node parent = js1.getParentNode();
+
+    // Sanity check.
+    assertEquals(1, parent.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Js rewriter = new ConcatVisitor.Js(config(null, false, true), mgr);
+    assertTrue(rewriter.revisit(gadget(), nodes));
+
+    // There should be one JS node child which is rewritten.
+    assertEquals(1, parent.getChildNodes().getLength());
+    Element concatNode = (Element)parent.getChildNodes().item(0);
+    Uri concatUri = Uri.parse(concatNode.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri.getPath());
+    assertEquals(JS1_URL_STR, concatUri.getQueryParameter("1"));
+    assertNull(concatUri.getQueryParameter("2"));
+  }
+
+  @Test
+  public void concatSingleBatchJs() throws Exception {
+    List<Node> nodes = seqNodes(js1, js2, js3);
+    Node parent = js1.getParentNode();
+
+    // Sanity check.
+    assertEquals(3, parent.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Js rewriter = new ConcatVisitor.Js(config(null, false, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), nodes));
+
+    // Should be left with a single JS node child to parent.
+    assertEquals(1, parent.getChildNodes().getLength());
+    Element concatNode = (Element)parent.getChildNodes().item(0);
+    Uri concatUri = Uri.parse(concatNode.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri.getPath());
+    assertEquals(JS1_URL_STR, concatUri.getQueryParameter("1"));
+    assertEquals(JS2_URL_STR, concatUri.getQueryParameter("2"));
+    assertEquals(JS3_URL_STR, concatUri.getQueryParameter("3"));
+  }
+
+  @Test
+  public void concatSingleCss() throws Exception {
+    List<Node> nodes = seqNodes(css1);
+    Node parent = css1.getParentNode();
+
+    // Sanity check.
+    assertEquals(1, parent.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Css rewriter = new ConcatVisitor.Css(config(null, false, true), mgr);
+    assertTrue(rewriter.revisit(gadget(), nodes));
+
+    // There should be one CSS node child which is rewritten.
+    assertEquals(1, parent.getChildNodes().getLength());
+    Element concatNode = (Element)parent.getChildNodes().item(0);
+    Uri concatUri = Uri.parse(concatNode.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri.getPath());
+    assertEquals(CSS1_URL_STR, concatUri.getQueryParameter("1"));
+    assertNull(concatUri.getQueryParameter("2"));
+  }
+
+  @Test
+  public void concatSingleBatchCss() throws Exception {
+    List<Node> nodes = seqNodes(css1, css2, css3);
+    Node parent = css1.getParentNode();
+
+    // Sanity check.
+    assertEquals(3, parent.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Css rewriter = new ConcatVisitor.Css(config(null, false, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), nodes));
+
+    // Should be left with a single CSS node child to parent.
+    assertEquals(1, parent.getChildNodes().getLength());
+    Element concatNode = (Element)parent.getChildNodes().item(0);
+    Uri concatUri = Uri.parse(concatNode.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri.getPath());
+    assertEquals(CSS1_URL_STR, concatUri.getQueryParameter("1"));
+    assertEquals(CSS2_URL_STR, concatUri.getQueryParameter("2"));
+    assertEquals(CSS3_URL_STR, concatUri.getQueryParameter("3"));
+  }
+
+  protected Element elemWithNameSpace(String namespace, String tag, String... attrStrs) {
+    Element elem = doc.createElementNS(namespace, tag);
+    for (int i = 0; attrStrs != null && i < attrStrs.length; i += 2) {
+      Attr attr = doc.createAttribute(attrStrs[i]);
+      attr.setValue(attrStrs[i+1]);
+      elem.setAttributeNode(attr);
+    }
+    return elem;
+  }
+
+  @Test
+  public void concatSingleBatchCssWithNamespace() throws Exception {
+
+    String namespace = "http://www.w3.org/1999/xhtml";
+    css1 = elemWithNameSpace(namespace, "link", "rel", "Stylesheet", "type", "Text/css",
+        "href", CSS1_URL_STR);
+    css2 = elemWithNameSpace(namespace, "link", "rel", "stylesheet", "type", "text/css",
+        "href", CSS2_URL_STR);
+    css3 = elemWithNameSpace(namespace, "link", "rel", "stylesheet", "type", "text/css",
+        "href", CSS3_URL_STR);
+
+    List<Node> nodes = seqNodes(css1, css2, css3);
+    Node parent = css1.getParentNode();
+
+    // Sanity check.
+    assertEquals(3, parent.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Css rewriter = new ConcatVisitor.Css(config(null, false, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), nodes));
+
+    // Should be left with a single JS node child to parent.
+    assertEquals(1, parent.getChildNodes().getLength());
+    Element concatNode = (Element)parent.getChildNodes().item(0);
+
+    Uri concatUri = Uri.parse(concatNode.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri.getPath());
+    assertEquals(CSS1_URL_STR, concatUri.getQueryParameter("1"));
+    assertEquals(CSS2_URL_STR, concatUri.getQueryParameter("2"));
+    assertEquals(CSS3_URL_STR, concatUri.getQueryParameter("3"));
+
+    // Verify serializer escape '&' once:
+    assertFalse(concatUri.toString().contains("&amp;"));
+    doc.appendChild(concatNode);
+    HtmlSerializer serializer = new DefaultHtmlSerializer();
+    String html = serializer.serialize(doc);
+    assertTrue(html.contains(concatUri.toString().replace("&", "&amp;")));
+    serializer = new CajaHtmlSerializer();
+    html = serializer.serialize(doc);
+    assertTrue(html.contains(concatUri.toString().replace("&", "&amp;")));
+  }
+
+  @Test
+  public void concatMultiBatchJs() throws Exception {
+    List<Node> fullListJs = Lists.newArrayList();
+    fullListJs.addAll(seqNodes(js1, js2));
+    Node parent1 = js1.getParentNode();
+    assertEquals(2, parent1.getChildNodes().getLength());
+
+    fullListJs.addAll(seqNodes(js3, js4));
+    Node parent2 = js3.getParentNode();
+    assertEquals(2, js3.getParentNode().getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Js rewriter = new ConcatVisitor.Js(config(null, false, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), fullListJs));
+
+    // Should have been independently concatenated.
+    assertEquals(1, parent1.getChildNodes().getLength());
+    Element cn1 = (Element)parent1.getChildNodes().item(0);
+    Uri concatUri1 = Uri.parse(cn1.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri1.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri1.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri1.getPath());
+    assertEquals(JS1_URL_STR, concatUri1.getQueryParameter("1"));
+    assertEquals(JS2_URL_STR, concatUri1.getQueryParameter("2"));
+    assertNull(concatUri1.getQueryParameter("3"));
+
+    assertEquals(1, parent2.getChildNodes().getLength());
+    Element cn2 = (Element)parent2.getChildNodes().item(0);
+    Uri concatUri2 = Uri.parse(cn2.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri2.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri2.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri2.getPath());
+    assertEquals(JS3_URL_STR, concatUri2.getQueryParameter("1"));
+    assertEquals(JS4_URL_STR, concatUri2.getQueryParameter("2"));
+    assertNull(concatUri2.getQueryParameter("3"));
+  }
+
+  @Test
+  public void concatMultiBatchJsWithSingleResource() throws Exception {
+    List<Node> fullListJs = Lists.newArrayList();
+    fullListJs.addAll(seqNodes(js1, js2));
+    Node parent1 = js1.getParentNode();
+    assertEquals(2, parent1.getChildNodes().getLength());
+
+    fullListJs.addAll(seqNodes(js3));
+    Node parent2 = js3.getParentNode();
+    assertEquals(1, js3.getParentNode().getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Js rewriter = new ConcatVisitor.Js(config(null, false, true), mgr);
+    assertTrue(rewriter.revisit(gadget(), fullListJs));
+
+    // Should have been independently concatenated.
+    assertEquals(1, parent1.getChildNodes().getLength());
+    Element cn1 = (Element)parent1.getChildNodes().item(0);
+    Uri concatUri1 = Uri.parse(cn1.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri1.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri1.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri1.getPath());
+    assertEquals(JS1_URL_STR, concatUri1.getQueryParameter("1"));
+    assertEquals(JS2_URL_STR, concatUri1.getQueryParameter("2"));
+    assertNull(concatUri1.getQueryParameter("3"));
+
+    assertEquals(1, parent2.getChildNodes().getLength());
+    Element cn2 = (Element)parent2.getChildNodes().item(0);
+    Uri concatUri2 = Uri.parse(cn2.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri2.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri2.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri2.getPath());
+    assertEquals(JS3_URL_STR, concatUri2.getQueryParameter("1"));
+    assertNull(concatUri2.getQueryParameter("2"));
+  }
+
+  @Test
+  public void concatMultiBatchCss() throws Exception {
+    List<Node> fullListCss = Lists.newArrayList();
+    fullListCss.addAll(seqNodes(css1, css2));
+    Node parent1 = css1.getParentNode();
+    assertEquals(2, parent1.getChildNodes().getLength());
+
+    fullListCss.addAll(seqNodes(css3, css4, css5, css7, css6, css8, css9));
+    Node parent2 = css3.getParentNode();
+    assertEquals(7, css3.getParentNode().getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Css rewriter = new ConcatVisitor.Css(config(null, false, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), fullListCss));
+
+    // Should have been independently concatenated.
+    assertEquals(1, parent1.getChildNodes().getLength());
+    Element cn1 = (Element)parent1.getChildNodes().item(0);
+    Uri concatUri1 = Uri.parse(cn1.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri1.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri1.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri1.getPath());
+    assertEquals(CSS1_URL_STR, concatUri1.getQueryParameter("1"));
+    assertEquals(CSS2_URL_STR, concatUri1.getQueryParameter("2"));
+    assertNull(concatUri1.getQueryParameter("3"));
+
+    assertEquals(2, parent2.getChildNodes().getLength());
+    Element cn2 = (Element)parent2.getChildNodes().item(0);
+    Uri concatUri2 = Uri.parse(cn2.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri2.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri2.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri2.getPath());
+    assertEquals(CSS3_URL_STR, concatUri2.getQueryParameter("1"));
+    assertEquals(CSS4_URL_STR, concatUri2.getQueryParameter("2"));
+    assertEquals(CSS7_URL_STR, concatUri2.getQueryParameter("3"));
+    assertEquals(CSS8_URL_STR, concatUri2.getQueryParameter("4"));
+    assertEquals(CSS9_URL_STR, concatUri2.getQueryParameter("5"));
+    assertNull(concatUri2.getQueryParameter("6"));
+    assertEquals("", cn2.getAttribute("media"));
+
+    Element cn3 = (Element)parent2.getChildNodes().item(1);
+    Uri concatUri3 = Uri.parse(cn3.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri3.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri3.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri3.getPath());
+    assertEquals(CSS5_URL_STR, concatUri3.getQueryParameter("1"));
+    assertEquals(CSS6_URL_STR, concatUri3.getQueryParameter("2"));
+    assertNull(concatUri3.getQueryParameter("3"));
+    assertEquals("print", cn3.getAttribute("media"));
+  }
+
+  @Test
+  public void concatMultiBatchCssWithAllMediaTypeAndTitle() throws Exception {
+  List<Node> fullListCss = Lists.newArrayList();
+    // modify few node to have the title attriblue.
+    ((Element) css2).setAttribute("title", "one");
+    ((Element) css3).setAttribute("title", "two");
+    ((Element) css4).setAttribute("title", "two");
+    ((Element) css10).setAttribute("title", "two");
+    fullListCss.addAll(seqNodes(css1, css2, css3, css4, css10, css11, css12, css7, css8, css9));
+    Node parent1 = css1.getParentNode();
+    assertEquals(10, parent1.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Css rewriter = new ConcatVisitor.Css(config(null, false, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), fullListCss));
+
+    // Should have been split across 'all' media type and then batches should be independently
+    // concatenated.
+    Element cn1 = (Element)parent1.getChildNodes().item(0);
+    Uri concatUri1 = Uri.parse(cn1.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri1.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri1.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri1.getPath());
+    assertEquals(CSS1_URL_STR, concatUri1.getQueryParameter("1"));
+    assertNull(concatUri1.getQueryParameter("2"));
+    assertEquals("", cn1.getAttribute("media"));
+
+    Element cn2 = (Element)parent1.getChildNodes().item(1);
+    Uri concatUri2 = Uri.parse(cn2.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri2.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri2.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri2.getPath());
+    assertEquals(CSS2_URL_STR, concatUri2.getQueryParameter("1"));
+    assertNull(concatUri2.getQueryParameter("2"));
+    assertEquals("", cn2.getAttribute("media"));
+    assertEquals("one", cn2.getAttribute("title"));
+
+    Element cn3 = (Element)parent1.getChildNodes().item(2);
+    Uri concatUri3 = Uri.parse(cn3.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri3.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri3.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri3.getPath());
+    assertEquals(CSS3_URL_STR, concatUri3.getQueryParameter("1"));
+    assertEquals(CSS4_URL_STR, concatUri3.getQueryParameter("2"));
+    assertNull(concatUri3.getQueryParameter("3"));
+    assertEquals("", cn3.getAttribute("media"));
+    assertEquals("two", cn3.getAttribute("title"));
+
+    Element cn4 = (Element)parent1.getChildNodes().item(3);
+    Uri concatUri4 = Uri.parse(cn4.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri4.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri4.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri4.getPath());
+    assertEquals(CSS10_URL_STR, concatUri4.getQueryParameter("1"));
+    assertNull(concatUri4.getQueryParameter("2"));
+    assertEquals("all", cn4.getAttribute("media"));
+    assertEquals("two", cn4.getAttribute("title"));
+
+    Element cn5 = (Element)parent1.getChildNodes().item(4);
+    Uri concatUri5 = Uri.parse(cn5.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri5.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri5.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri5.getPath());
+    assertEquals(CSS11_URL_STR, concatUri5.getQueryParameter("1"));
+    assertEquals(CSS12_URL_STR, concatUri5.getQueryParameter("2"));
+    assertNull(concatUri5.getQueryParameter("3"));
+    assertEquals("all", cn5.getAttribute("media"));
+    assertEquals("", cn5.getAttribute("title"));
+
+    Element cn6 = (Element)parent1.getChildNodes().item(5);
+    Uri concatUri6 = Uri.parse(cn6.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri6.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri6.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri6.getPath());
+    assertEquals(CSS7_URL_STR, concatUri6.getQueryParameter("1"));
+    assertEquals(CSS8_URL_STR, concatUri6.getQueryParameter("2"));
+    assertEquals(CSS9_URL_STR, concatUri6.getQueryParameter("3"));
+    assertNull(concatUri6.getQueryParameter("4"));
+    assertEquals("screen", cn6.getAttribute("media"));
+    assertEquals("", cn6.getAttribute("title"));
+  }
+
+  @Test
+  public void concatMultiBatchCssWithSingleResource() throws Exception {
+    List<Node> fullListCss = Lists.newArrayList();
+    fullListCss.addAll(seqNodes(css1, css2));
+    Node parent1 = css1.getParentNode();
+    assertEquals(2, parent1.getChildNodes().getLength());
+
+    fullListCss.addAll(seqNodes(css3));
+    Node parent2 = css3.getParentNode();
+    assertEquals(1, css3.getParentNode().getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Css rewriter = new ConcatVisitor.Css(config(null, false, true), mgr);
+    assertTrue(rewriter.revisit(gadget(), fullListCss));
+
+    // Should have been independently concatenated.
+    assertEquals(1, parent1.getChildNodes().getLength());
+    Element cn1 = (Element)parent1.getChildNodes().item(0);
+    Uri concatUri1 = Uri.parse(cn1.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri1.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri1.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri1.getPath());
+    assertEquals(CSS1_URL_STR, concatUri1.getQueryParameter("1"));
+    assertEquals(CSS2_URL_STR, concatUri1.getQueryParameter("2"));
+    assertNull(concatUri1.getQueryParameter("3"));
+
+    assertEquals(1, parent2.getChildNodes().getLength());
+    Element cn2 = (Element)parent2.getChildNodes().item(0);
+    Uri concatUri2 = Uri.parse(cn2.getAttribute("href"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri2.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri2.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri2.getPath());
+    assertEquals(CSS3_URL_STR, concatUri2.getQueryParameter("1"));
+    assertNull(concatUri2.getQueryParameter("2"));
+  }
+
+  @Test
+  public void concatMultiBatchJsBadBatch() throws Exception {
+    List<Node> fullListJs = Lists.newArrayList();
+    fullListJs.addAll(seqNodes(js1, js2));
+    Node parent1 = js1.getParentNode();
+    assertEquals(2, parent1.getChildNodes().getLength());
+
+    fullListJs.addAll(seqNodes(js5, js6));
+    Node parent3 = js5.getParentNode();
+    assertEquals(2, parent3.getChildNodes().getLength());
+
+    fullListJs.addAll(seqNodes(js3, js4));
+    Node parent2 = js3.getParentNode();
+    assertEquals(2, js3.getParentNode().getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Js rewriter = new ConcatVisitor.Js(config(null, false, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), fullListJs));
+
+    // Should have been independently concatenated. Batches #1 and #2 are OK. Middle skipped.
+    assertEquals(1, parent1.getChildNodes().getLength());
+    Element cn1 = (Element)parent1.getChildNodes().item(0);
+    Uri concatUri1 = Uri.parse(cn1.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri1.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri1.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri1.getPath());
+    assertEquals(JS1_URL_STR, concatUri1.getQueryParameter("1"));
+    assertEquals(JS2_URL_STR, concatUri1.getQueryParameter("2"));
+
+    assertEquals(2, parent3.getChildNodes().getLength());
+    assertSame(js5, parent3.getChildNodes().item(0));
+    assertSame(js6, parent3.getChildNodes().item(1));
+
+    assertEquals(1, parent2.getChildNodes().getLength());
+    Element cn2 = (Element)parent2.getChildNodes().item(0);
+    Uri concatUri2 = Uri.parse(cn2.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri2.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri2.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri2.getPath());
+    assertEquals(JS3_URL_STR, concatUri2.getQueryParameter("1"));
+    assertEquals(JS4_URL_STR, concatUri2.getQueryParameter("2"));
+  }
+
+  @Test
+  public void concatSplitJsSingleBatch() throws Exception {
+    List<Node> nodes = seqNodes(js1, js2);
+    Node parent = js1.getParentNode();
+    assertEquals(2, parent.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Js rewriter = new ConcatVisitor.Js(config(null, true, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), nodes));
+
+    // Same number of nodes. Now the second JS node is a new script node eval'ing JS.
+    // For test purposes the code is just the Uri.
+    assertEquals(3, parent.getChildNodes().getLength());
+    Element jsConcat = (Element)parent.getChildNodes().item(0);
+    assertEquals("script", jsConcat.getTagName());
+    Uri concatUri = Uri.parse(jsConcat.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri.getPath());
+    assertEquals(JS1_URL_STR, concatUri.getQueryParameter("1"));
+    assertEquals(JS2_URL_STR, concatUri.getQueryParameter("2"));
+    assertNull(concatUri.getQueryParameter("3"));
+    assertEquals("1", concatUri.getQueryParameter("SPLIT"));
+
+    // Split-eval nodes 1 and 2
+    Element splitEval1 = (Element)parent.getChildNodes().item(1);
+    assertEquals("script", splitEval1.getTagName());
+    assertNull(splitEval1.getAttributeNode("src"));
+    assertEquals(JS1_URL_STR, splitEval1.getTextContent());
+
+    Element splitEval2 = (Element)parent.getChildNodes().item(2);
+    assertEquals("script", splitEval2.getTagName());
+    assertNull(splitEval2.getAttributeNode("src"));
+    assertEquals(JS2_URL_STR, splitEval2.getTextContent());
+  }
+
+  @Test
+  public void concatSplitJsSplitNodes() throws Exception {
+    Node parent = doc.createElement("container");
+    parent.appendChild(doc.createElement("div"));
+    parent.appendChild(js1);
+    parent.appendChild(doc.createTextNode("text"));
+    parent.appendChild(doc.createComment("comment"));
+    parent.appendChild(js2);
+    parent.appendChild(doc.createElement("span"));
+    List<Node> nodes = ImmutableList.of(js1, js2);
+    assertEquals(6, parent.getChildNodes().getLength());
+
+    SimpleConcatUriManager mgr = simpleMgr();
+    ConcatVisitor.Js rewriter = new ConcatVisitor.Js(config(null, true, false), mgr);
+    assertTrue(rewriter.revisit(gadget(), nodes));
+
+    // Same number of nodes. Now the second JS node is a new script node eval'ing JS.
+    // For test purposes the code is just the Uri.
+    assertEquals(7, parent.getChildNodes().getLength());
+    Element jsConcat = (Element)parent.getChildNodes().item(1);
+    assertEquals("script", jsConcat.getTagName());
+    Uri concatUri = Uri.parse(jsConcat.getAttribute("src"));
+    assertEquals(CONCAT_BASE_URI.getScheme(), concatUri.getScheme());
+    assertEquals(CONCAT_BASE_URI.getAuthority(), concatUri.getAuthority());
+    assertEquals(CONCAT_BASE_URI.getPath(), concatUri.getPath());
+    assertEquals(JS1_URL_STR, concatUri.getQueryParameter("1"));
+    assertEquals(JS2_URL_STR, concatUri.getQueryParameter("2"));
+    assertNull(concatUri.getQueryParameter("3"));
+    assertEquals("1", concatUri.getQueryParameter("SPLIT"));
+
+    // Split-eval nodes 1 and 2
+    Element splitEval1 = (Element)parent.getChildNodes().item(2);
+    assertEquals("script", splitEval1.getTagName());
+    assertNull(splitEval1.getAttributeNode("src"));
+    assertEquals(JS1_URL_STR, splitEval1.getTextContent());
+
+    Element splitEval2 = (Element)parent.getChildNodes().item(5);
+    assertEquals("script", splitEval2.getTagName());
+    assertNull(splitEval2.getAttributeNode("src"));
+    assertEquals(JS2_URL_STR, splitEval2.getTextContent());
+  }
+
+  private VisitStatus getVisitStatusJs(ContentRewriterFeature.Config config, Node node)
+      throws RewritingException {
+    return new ConcatVisitor.Js(config, null).visit(gadget(), node);
+  }
+
+  private VisitStatus getVisitStatusJs(
+      Node node, String rewriteRegex, boolean splitJs, boolean singleResouce)
+      throws Exception {
+    ContentRewriterFeature.Config config = config(rewriteRegex, splitJs, singleResouce);
+    return getVisitStatusJs(config, node);
+  }
+
+  private VisitStatus getVisitStatusCss(ContentRewriterFeature.Config config, Node node)
+      throws RewritingException {
+    return new ConcatVisitor.Css(config, null).visit(gadget(), node);
+  }
+
+  private VisitStatus getVisitStatusCss(Node node, String rewriteRegex, boolean singleResource)
+      throws Exception {
+    // True, but never used (splitJS support)
+    ContentRewriterFeature.Config config = config(rewriteRegex, true, singleResource);
+    return getVisitStatusCss(config, node);
+  }
+
+  private ContentRewriterFeature.Config config(
+      String exclude, boolean splitJs, boolean singleResourceConcat) {
+    return new ContentRewriterFeature.DefaultConfig(".*", exclude == null ? "" : exclude,
+        "0", "", false, splitJs, singleResourceConcat);
+  }
+
+  private List<Node> seqNodes(Node... nodes) {
+    Node container = doc.createElement("container");
+    List<Node> seq = Lists.newArrayListWithCapacity(nodes.length);
+    for (Node node : nodes) {
+      container.appendChild(node);
+      seq.add(node);
+    }
+    return seq;
+  }
+
+  private SimpleConcatUriManager simpleMgr() {
+    return new SimpleConcatUriManager(CONCAT_BASE_URI);
+  }
+
+  private static class SimpleConcatUriManager implements ConcatUriManager {
+    private final Uri base;
+
+    private SimpleConcatUriManager(Uri base) {
+      this.base = base;
+    }
+
+    public List<ConcatData> make(List<ConcatUri> batches, boolean isAdjacent) {
+      List<ConcatData> results = Lists.newArrayListWithCapacity(batches.size());
+      for (ConcatUri batch : batches) {
+        UriBuilder uriBuilder = new UriBuilder(base);
+        Integer i = 1;
+        for (Uri uri : batch.getBatch()) {
+          uriBuilder.addQueryParameter((i++).toString(), uri.toString());
+        }
+        Map<Uri, String> snippets = Maps.newHashMap();
+        if (!isAdjacent) {
+          for (Uri uri : batch.getBatch()) {
+            snippets.put(uri, uri.toString());
+          }
+          uriBuilder.addQueryParameter("SPLIT", "1");
+        }
+        results.add(new ConcatData(Lists.newArrayList(uriBuilder.toUri()), snippets));
+      }
+      return results;
+    }
+
+    public ConcatUri process(Uri uri) {
+      // Not used in test code.
+      throw new UnsupportedOperationException();
+    }
+
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeatureTestCase.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeatureTestCase.java
new file mode 100644
index 0000000..1f36ddd
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeatureTestCase.java
@@ -0,0 +1,251 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.junit.Test;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+import static org.junit.Assert.*;
+
+/**
+ * Test basic parsing of content-rewriter feature
+ */
+public class ContentRewriterFeatureTestCase extends BaseRewriterTestCase {
+  @Test
+  public void testContainerDefaultIncludeAll() throws Exception {
+    defaultRewriterFeature =
+        new ContentRewriterFeature.Config(createSpecWithoutRewrite(),
+          new ContentRewriterFeature.DefaultConfig(".*", "", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testContainerDefaultIncludeNone() throws Exception {
+    defaultRewriterFeature =
+        new ContentRewriterFeature.Config(createSpecWithoutRewrite(),
+          new ContentRewriterFeature.DefaultConfig("", ".*", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testContainerDefaultExcludeOverridesInclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(createSpecWithoutRewrite(),
+        new ContentRewriterFeature.DefaultConfig(".*", ".*", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOverridesContainerDefaultInclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("", ".*", "0", tags),
+        new ContentRewriterFeature.DefaultConfig(".*", "", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOnlyOverridesContainerDefaultInclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite(null, ".*", null, null),
+        new ContentRewriterFeature.DefaultConfig(".*", "", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOverridesContainerDefaultExclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite(".*", "", "0", tags),
+        new ContentRewriterFeature.DefaultConfig("", ".*", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testExcludeOverridesInclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "test", "0", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testIncludeOnlyMatch() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "0", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://testx.test.com"));
+  }
+
+  @Test
+  public void testTagRewrite() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "0", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("IFRAME"));
+    assertTrue(defaultRewriterFeature.shouldRewriteTag("img"));
+    assertTrue(defaultRewriterFeature.shouldRewriteTag("ScripT"));
+  }
+
+  @Test
+  public void testOverrideTagRewrite() throws Exception {
+    Set<String> newTags = Sets.newHashSet("iframe");
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "0", newTags),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.shouldRewriteTag("IFRAME"));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("img"));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("ScripT"));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("link"));
+  }
+
+  @Test
+  public void testExpiresTimeParse() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "12345", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", TAGS, false, false, false));
+    assertNotNull(defaultRewriterFeature.getExpires());
+    assertNotNull(defaultRewriterFeature.getExpires() == 12345);
+  }
+
+  @Test
+  public void testExpiresHTTPParse() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "htTp ", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "12345", TAGS, false, false, false));
+    assertEquals(ContentRewriterFeature.EXPIRES_HTTP, defaultRewriterFeature.getExpires());
+  }
+
+  @Test
+  public void testExpiresOverwriteTooBig() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "20000", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "12345", TAGS, false, false, false));
+    assertEquals(12345, defaultRewriterFeature.getExpires().intValue());
+  }
+
+  @Test
+  public void testExpiresBadValue() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "X", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "12345", TAGS, false, false, false));
+    assertEquals(12345, defaultRewriterFeature.getExpires().intValue());
+  }
+
+  @Test
+  public void testExpiresOverwrite() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "10", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "12345", TAGS, false, false, false));
+    assertEquals(10, defaultRewriterFeature.getExpires().intValue());
+  }
+
+  @Test
+  public void testExpiresOverwriteDefault() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "10", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "-1", TAGS, false, false, false));
+    assertEquals(10, defaultRewriterFeature.getExpires().intValue());
+  }
+
+  @Test
+  public void testExpiresInvalidParse() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "junk", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "12345", TAGS, false, false, false));
+    assertNotNull(defaultRewriterFeature.getExpires());
+    assertNotNull(defaultRewriterFeature.getExpires() == 12345);
+  }
+
+  @Test
+  public void testSpecEmptyContainerWithExclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite(null, null, null, null),
+        new ContentRewriterFeature.DefaultConfig(".*", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foobar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOnlyOverridesContainerWithExclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite(null, "", null, null),
+        new ContentRewriterFeature.DefaultConfig(".*", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foobar.com"));
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecEmptyDoesNotOverridesContainerDefaultNoInclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite(null, null, null, null),
+        new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.foobar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecIncludeOnlyOverridesContainerDefaultNoInclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite(".*", null, null, null),
+        new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foobar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSplitJsSupported() throws Exception {
+    defaultRewriterFeature =
+        new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, true, false);
+    assertTrue(defaultRewriterFeature.isSplitJsEnabled());
+  }
+
+  @Test
+  public void testSplitJsNotSupported() throws Exception {
+    defaultRewriterFeature =
+      new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, false);
+    assertFalse(defaultRewriterFeature.isSplitJsEnabled());
+  }
+
+  @Test
+  public void testSingleResourceConcatEnabled() throws Exception {
+    defaultRewriterFeature =
+        new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, true);
+    assertTrue(defaultRewriterFeature.isSingleResourceConcatEnabled());
+  }
+
+  @Test
+  public void testSingleResourceConcatNotEnabled() throws Exception {
+    defaultRewriterFeature =
+      new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, false);
+    assertFalse(defaultRewriterFeature.isSingleResourceConcatEnabled());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeatureTestCaseOS9.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeatureTestCaseOS9.java
new file mode 100644
index 0000000..598acbf
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentRewriterFeatureTestCaseOS9.java
@@ -0,0 +1,266 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Sets;
+
+import org.junit.Test;
+
+/**
+ * Test basic parsing of content-rewriter feature using Open Social v0.9 keywords
+ */
+public class ContentRewriterFeatureTestCaseOS9 extends BaseRewriterTestCase {
+
+  @Test
+  public void testSpecExcludeOverridesContainerDefaultInclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "" }, new String[] { "*" }, "0", tags),
+        new ContentRewriterFeature.DefaultConfig(".*", "", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOverridesMultipleContainerDefaultInclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(null, new String[] { "foo", "bar" }, "0", tags),
+        new ContentRewriterFeature.DefaultConfig(".*", "", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.foo.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.bar.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOnlyOverridesContainerDefaultInclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(null, new String[] { "*" }, null, null),
+        new ContentRewriterFeature.DefaultConfig(".*", "", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOverridesContainerDefaultExclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "*" }, new String[] { "" }, "0", tags),
+        new ContentRewriterFeature.DefaultConfig("", ".*", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testExcludeOverridesInclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "test.com" }, new String[] { "test" }, "0", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testIncludeOnlyMatch() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "test.com" }, new String[] { "testx" }, "0", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://testx.test.com"));
+  }
+
+  @Test
+  public void testSpecEmptyContainerWithExclude() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(null, null, null, null),
+        new ContentRewriterFeature.DefaultConfig(".*", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foobar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeOnlyOverridesContainerWithExclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(null, new String[] { "" }, null, null),
+        new ContentRewriterFeature.DefaultConfig(".*", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foobar.com"));
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecEmptyDoesNotOverridesContainerDefaultNoInclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(null, null, null, null),
+        new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, false));
+    assertFalse(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.foobar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecIncludeOnlyOverridesContainerDefaultNoInclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "*" }, null, null, null),
+        new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foobar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecIncludeMultipleOnlyOverridesContainerDefaultNoInclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "foo", "bar" }, null, null, null),
+        new ContentRewriterFeature.DefaultConfig("", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foo.com"));
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.bar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecIncludeMultipleOnlyOverridesContainerDefaultInclude()
+      throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "foo", "bar" }, null, null, null),
+        new ContentRewriterFeature.DefaultConfig("*", "test", "0", TAGS, false, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.foo.com"));
+    assertTrue(defaultRewriterFeature.shouldRewriteURL("http://www.bar.com"));
+    assertFalse(defaultRewriterFeature.shouldRewriteURL("http://www.test.com"));
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideIncludeUrls() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("norewrite", null, null, null),
+        new ContentRewriterFeature.DefaultConfig("^http://www.include.com", "def", "3600", TAGS, true, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature
+        .shouldRewriteURL("http://www.include.com/abc"));
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.include.com/def"));
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.norewrite.com/abc"));
+  }
+
+  @Test
+  public void testSpecExcludeOverrideExcludeUrls() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite(null, "abc", null, null),
+        new ContentRewriterFeature.DefaultConfig("^http://www.include.com", "def", "3600", TAGS, true, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.include.com/abc"));
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.norewrite.com/abc"));
+    assertTrue(defaultRewriterFeature
+        .shouldRewriteURL("http://www.include.com/def"));
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideIncludeUrlOS9() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(new String[] { "abc" }, null, null, null),
+        new ContentRewriterFeature.DefaultConfig("^http://www.include.com", "", "3600", TAGS, true, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature
+        .shouldRewriteURL("http://www.include.com/abc"));
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.norewrite.com/abc"));
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideExcludeUrlOS9() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewriteOS9(null, new String[] { "def" }, null, null),
+        new ContentRewriterFeature.DefaultConfig("^http://www.include.com", "", "3600", TAGS, true, false, false));
+    assertTrue(defaultRewriterFeature.isRewriteEnabled());
+    assertTrue(defaultRewriterFeature
+        .shouldRewriteURL("http://www.include.com/abc"));
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.include.com/def"));
+    assertFalse(defaultRewriterFeature
+        .shouldRewriteURL("http://www.norewrite.com/abc"));
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideDefaultExpires() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "3000", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "", TAGS, true, false, false));
+    assertNotNull(defaultRewriterFeature.getExpires());
+    assertNotNull(defaultRewriterFeature.getExpires() == 3000);
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideExpiresGreater() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "8000", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "3000", TAGS, true, false, false));
+    assertNotNull(defaultRewriterFeature.getExpires());
+    assertNotNull(defaultRewriterFeature.getExpires() == 3000);
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideExpiresLesser() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "2000", tags),
+        new ContentRewriterFeature.DefaultConfig("", "", "3000", TAGS, true, false, false));
+    assertNotNull(defaultRewriterFeature.getExpires());
+    assertNotNull(defaultRewriterFeature.getExpires() == 2000);
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideTagsSubset() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite("test\\.com", "testx", "0", Sets.newHashSet("img")),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", "img,script", true, false, false));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("IFRAME"));
+    assertTrue(defaultRewriterFeature.shouldRewriteTag("img"));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("ScripT"));
+  }
+
+  @Test
+  public void testSpecExcludeDisallowOverrideTagsSuperset() throws Exception {
+    defaultRewriterFeature = new ContentRewriterFeature.Config(
+        createSpecWithRewrite( "test\\.com", "testx", "0", Sets.newHashSet("img", "script", "link")),
+        new ContentRewriterFeature.DefaultConfig("", "", "0", "img,script", true, false, false));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("IFRAME"));
+    assertTrue(defaultRewriterFeature.shouldRewriteTag("img"));
+    assertTrue(defaultRewriterFeature.shouldRewriteTag("ScripT"));
+    assertFalse(defaultRewriterFeature.shouldRewriteTag("link"));
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverRewriterTest.java
new file mode 100644
index 0000000..9df7e99
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContentTypeCharsetRemoverRewriterTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for ContentTypeCharsetRemoverRewriter.
+ */
+public class ContentTypeCharsetRemoverRewriterTest extends DomWalkerTestBase {
+  private CajaHtmlParser htmlParser;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    ParseModule.DOMImplementationProvider domImpl =
+        new ParseModule.DOMImplementationProvider();
+    htmlParser = new CajaHtmlParser(domImpl.get());
+  }
+
+  @Test
+  public void testContentTypeCharsetRemoved() throws Exception {
+    String html = "<html><head>"
+                  + "<META Content=\"hello world\" "
+                  + "Http-equiv=\"Content-Title\">"
+                  + "<META Content=\"text/html ; charset = \'GBK\'\" "
+                  + "Http-equiv=\"Content-TYPE\">"
+                  + "<META Content=\"gzip\" "
+                  + "Http-EQuIv=\"Content-Encoding\">"
+                  + "</head><body><a href=\"hello\">Hello</a>"
+                  + "</body></html>";
+    String expected = "<html><head>"
+                      + "<meta content=\"hello world\" "
+                      + "http-equiv=\"Content-Title\">"
+                      + "<meta content=\"text/html \" "
+                      + "http-equiv=\"Content-TYPE\">"
+                      + "<meta content=\"gzip\" "
+                      + "http-equiv=\"Content-Encoding\">"
+                      + "</head><body><a href=\"hello\">Hello</a>"
+                      + "</body></html>";
+
+    ContentTypeCharsetRemoverRewriter rewriter =
+        new ContentTypeCharsetRemoverRewriter();
+    Gadget gadget = DomWalker.makeGadget(new HttpRequest(
+        Uri.parse("http://1.com/")));
+    MutableContent mc = new MutableContent(htmlParser, html);
+    rewriter.rewrite(gadget, mc);
+
+    assertEquals(expected, mc.getContent());
+  }
+
+  @Test
+  public void testNoMetaNode() throws Exception {
+    String html = "<html><head><title>hello</title>"
+                  + "</head><body><a href=\"hello\">Hello</a>"
+                  + "</body></html>";
+    String expected = "<html><head><title>hello</title>"
+                      + "</head><body><a href=\"hello\">Hello</a>"
+                      + "</body></html>";
+
+    ContentTypeCharsetRemoverRewriter rewriter =
+        new ContentTypeCharsetRemoverRewriter();
+    Gadget gadget = DomWalker.makeGadget(new HttpRequest(
+        Uri.parse("http://1.com/")));
+    MutableContent mc = new MutableContent(htmlParser, html);
+    rewriter.rewrite(gadget, mc);
+
+    assertEquals(expected, mc.getContent());
+  }
+
+  @Test
+  public void testMalformedCharset() throws Exception {
+    String html = "<html><head>"
+                  + "<META Content=\"text/html ; pharset=\'hello\'; hello=world\" "
+                  + "Http-equiv=\"Content-TYPE\">"
+                  + "</head><body><a href=\"hello\">Hello</a>"
+                  + "</body></html>";
+    String expected = "<html><head>"
+                      + "<meta content=\"text/html ; pharset=&#39;hello&#39;; hello=world\" "
+                      + "http-equiv=\"Content-TYPE\">"
+                      + "</head><body><a href=\"hello\">Hello</a>"
+                      + "</body></html>";
+
+    ContentTypeCharsetRemoverRewriter rewriter =
+        new ContentTypeCharsetRemoverRewriter();
+    Gadget gadget = DomWalker.makeGadget(new HttpRequest(
+        Uri.parse("http://1.com/")));
+    MutableContent mc = new MutableContent(htmlParser, html);
+    rewriter.rewrite(gadget, mc);
+
+    assertEquals(expected, mc.getContent());
+
+    html = "<html><head>"
+           + "<META Content=\"text/html ; charsett=\'hello\'; hello=world\" "
+           + "Http-equiv=\"Content-TYPE\">"
+           + "</head><body><a href=\"hello\">Hello</a>"
+           + "</body></html>";
+    expected = "<html><head>"
+               + "<meta content=\"text/html ; charsett=&#39;hello&#39;; hello=world\" "
+               + "http-equiv=\"Content-TYPE\">"
+               + "</head><body><a href=\"hello\">Hello</a>"
+               + "</body></html>";
+
+    mc = new MutableContent(htmlParser, html);
+    rewriter.rewrite(gadget, mc);
+
+    assertEquals(expected, mc.getContent());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContextAwareRegistryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContextAwareRegistryTest.java
new file mode 100644
index 0000000..b4e90bb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ContextAwareRegistryTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+/**
+ * Tests for ContextAwareRegistryTest.
+ */
+public class ContextAwareRegistryTest extends RewriterTestBase {
+  private ContextAwareRegistry contextAwareRegistry;
+  public static final String TEST_CONTAINER = "test";
+  public static final String DUMMY_CONTAINER = "dummy";
+
+  private class TestRewriter implements ResponseRewriter {
+    public final String val;
+    public TestRewriter(String val) {
+      this.val = val;
+    }
+
+    public void rewrite(HttpRequest request, HttpResponseBuilder response, Gadget gadget)
+            throws RewritingException {
+      response.addHeader("helloo", val);
+      response.addHeader("gadget", val);
+    }
+  }
+
+  void addBindingForRewritePath(String container, RewriteFlow rewriteFlow,
+                                Provider<List<ResponseRewriter>> list,
+                                Map<RewritePath, Provider<List<ResponseRewriter>>> map) {
+    RewritePath rewritePath = new RewritePath(container, rewriteFlow);
+    map.put(rewritePath, list);
+  }
+
+  @Test
+  public void testGetResponseRewriters() throws Exception {
+    final Map<RewritePath, Provider<List<ResponseRewriter>>> rewritePathToList = Maps.newHashMap();
+    Provider<Map<RewritePath, Provider<List<ResponseRewriter>>>> mapProvider = Providers.of(
+        rewritePathToList);
+
+    List<ResponseRewriter> list = ImmutableList.<ResponseRewriter>of(
+        new TestRewriter("helo"), new TestRewriter("buffalo"));
+    List<ResponseRewriter> emptyList = ImmutableList.of();
+    List<ResponseRewriter> list2 = ImmutableList.<ResponseRewriter>of(new TestRewriter(null));
+
+    addBindingForRewritePath(ContainerConfig.DEFAULT_CONTAINER, RewriteFlow.ACCELERATE,
+                             Providers.of(list), rewritePathToList);
+    addBindingForRewritePath(ContainerConfig.DEFAULT_CONTAINER, RewriteFlow.DEFAULT,
+                             Providers.of(emptyList), rewritePathToList);
+
+    list = ImmutableList.<ResponseRewriter>of(new TestRewriter("cat"),
+                                               new TestRewriter("dog"));
+    addBindingForRewritePath(TEST_CONTAINER, RewriteFlow.ACCELERATE,
+                             Providers.of(list), rewritePathToList);
+    addBindingForRewritePath(TEST_CONTAINER, RewriteFlow.DEFAULT,
+                             Providers.of(list2), rewritePathToList);
+
+    // Test container present and flow present.
+    contextAwareRegistry = new ContextAwareRegistry(null, RewriteFlow.ACCELERATE, mapProvider);
+    list = contextAwareRegistry.getResponseRewriters(TEST_CONTAINER);
+    assertEquals(2, list.size());
+    assertEquals("cat", ((TestRewriter) list.get(0)).val);
+    assertEquals("dog", ((TestRewriter) list.get(1)).val);
+
+    // Test container present but flow absent.
+    contextAwareRegistry = new ContextAwareRegistry(null, RewriteFlow.DUMMY_FLOW, mapProvider);
+    list = contextAwareRegistry.getResponseRewriters(TEST_CONTAINER);
+    assertEquals(0, list.size());
+
+    // Test container absent, fallback to default container.
+    contextAwareRegistry = new ContextAwareRegistry(null, RewriteFlow.ACCELERATE, mapProvider);
+    list = contextAwareRegistry.getResponseRewriters(DUMMY_CONTAINER);
+    assertEquals(2, list.size());
+    assertEquals("helo", ((TestRewriter) list.get(0)).val);
+    assertEquals("buffalo", ((TestRewriter) list.get(1)).val);
+
+    // Test container absent, fallback to default container which is also absent.
+    rewritePathToList.remove(new RewritePath(ContainerConfig.DEFAULT_CONTAINER,
+                                             RewriteFlow.ACCELERATE));
+    rewritePathToList.remove(new RewritePath(ContainerConfig.DEFAULT_CONTAINER,
+                                             RewriteFlow.DEFAULT));
+    contextAwareRegistry = new ContextAwareRegistry(null, RewriteFlow.ACCELERATE, mapProvider);
+    list = contextAwareRegistry.getResponseRewriters(DUMMY_CONTAINER);
+    assertEquals(0, list.size());
+  }
+
+  @Test
+  public void testRewriteResponse() throws Exception {
+  final Map<RewritePath, Provider<List<ResponseRewriter>>> rewritePathToList = Maps.newHashMap();
+
+    List<ResponseRewriter> list = ImmutableList.<ResponseRewriter>of(
+        new TestRewriter("helo"), new TestRewriter("buffalo"));
+    List<ResponseRewriter> emptyList = ImmutableList.of();
+
+    addBindingForRewritePath(TEST_CONTAINER, RewriteFlow.ACCELERATE,
+                             Providers.of(list), rewritePathToList);
+    addBindingForRewritePath(TEST_CONTAINER, RewriteFlow.DEFAULT,
+                             Providers.of(emptyList), rewritePathToList);
+
+    // Test container present and flow present.
+    contextAwareRegistry = new ContextAwareRegistry(
+        null, RewriteFlow.ACCELERATE, Providers.of(rewritePathToList));
+
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.example.org/"));
+    req.setContainer(TEST_CONTAINER);
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+    HttpResponse resp = contextAwareRegistry.rewriteHttpResponse(
+        req, builder.create(), null);
+
+    List<String> headers = Lists.newArrayList(resp.getHeaders("helloo"));
+    assertEquals(2, headers.size());
+    assertEquals("helo", headers.get(0));
+    assertEquals("buffalo", headers.get(1));
+  }
+
+  @Test
+  public void testRewriteResponseGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    final Map<RewritePath, Provider<List<ResponseRewriter>>> rewritePathToList = Maps.newHashMap();
+
+    List<ResponseRewriter> list = ImmutableList.<ResponseRewriter>of(
+        new TestRewriter("helo"), new TestRewriter("buffalo"));
+    List<ResponseRewriter> emptyList = ImmutableList.of();
+
+    addBindingForRewritePath(TEST_CONTAINER, RewriteFlow.ACCELERATE,
+                             Providers.of(list), rewritePathToList);
+    addBindingForRewritePath(TEST_CONTAINER, RewriteFlow.DEFAULT,
+                             Providers.of(emptyList), rewritePathToList);
+
+    // Test container present and flow present.
+    contextAwareRegistry = new ContextAwareRegistry(
+        null, RewriteFlow.ACCELERATE, Providers.of(rewritePathToList));
+
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.example.org/"));
+    req.setContainer(TEST_CONTAINER);
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+    HttpResponse resp = contextAwareRegistry.rewriteHttpResponse(
+        req, builder.create(), gadget);
+
+    List<String> headers = Lists.newArrayList(resp.getHeaders("helloo"));
+    assertEquals(2, headers.size());
+    assertEquals("helo", headers.get(0));
+    assertEquals("buffalo", headers.get(1));
+
+    headers = Lists.newArrayList(resp.getHeaders("gadget"));
+    assertEquals(2, headers.size());
+    assertEquals("helo", headers.get(0));
+    assertEquals("buffalo", headers.get(1));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/CssResponseRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/CssResponseRewriterTest.java
new file mode 100644
index 0000000..7bad9f1
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/CssResponseRewriterTest.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.caja.CajaCssParser;
+import org.apache.shindig.gadgets.uri.DefaultProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+/**
+ * Tests for CssResponseRewriter.
+ */
+public class CssResponseRewriterTest extends RewriterTestBase {
+  private static final ImmutableMap<String, Object> DEFAULT_CONTAINER_CONFIG = ImmutableMap
+      .<String, Object>builder()
+      .put(ContainerConfig.CONTAINER_KEY, ImmutableList.of("default"))
+      .put(DefaultProxyUriManager.PROXY_HOST_PARAM, "www.test.com")
+      .put(DefaultProxyUriManager.PROXY_PATH_PARAM, "/dir/proxy")
+      .build();
+  private static final ImmutableMap<String, Object> MOCK_CONTAINER_CONFIG = ImmutableMap
+      .<String, Object>builder()
+      .put(ContainerConfig.CONTAINER_KEY, ImmutableList.of(MOCK_CONTAINER))
+      .put(DefaultProxyUriManager.PROXY_HOST_PARAM, "www.mock.com")
+      .build();
+
+  private CssResponseRewriter rewriter;
+  private CssResponseRewriter rewriterNoOverrideExpires;
+  private Uri dummyUri;
+  private GadgetContext gadgetContext;
+  private ProxyUriManager proxyUriManager;
+  private ContentRewriterFeature.Factory factory;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    final ContentRewriterFeature.Config overrideFeatureNoOverrideExpires =
+        rewriterFeatureFactory.get(createSpecWithRewrite(".*", ".*exclude.*", null, tags));
+    ContentRewriterFeature.Factory factoryNoOverrideExpires =
+        new ContentRewriterFeature.Factory(null, null) {
+          @Override
+          public ContentRewriterFeature.Config get(HttpRequest req) {
+            return overrideFeatureNoOverrideExpires;
+          }
+        };
+    ContainerConfig config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(DEFAULT_CONTAINER_CONFIG)
+        .addContainer(MOCK_CONTAINER_CONFIG)
+        .commit();
+    proxyUriManager = new DefaultProxyUriManager(config, null);
+    rewriterNoOverrideExpires = new CssResponseRewriter(new CajaCssParser(),
+        proxyUriManager, factoryNoOverrideExpires);
+    final ContentRewriterFeature.Config overrideFeature =
+        rewriterFeatureFactory.get(createSpecWithRewrite(".*", ".*exclude.*", "3600", tags));
+    factory = new ContentRewriterFeature.Factory(null, null) {
+      @Override
+      public ContentRewriterFeature.Config get(HttpRequest req) {
+        return overrideFeature;
+      }
+    };
+
+    rewriter = new CssResponseRewriter(new CajaCssParser(),
+        proxyUriManager, factory);
+    dummyUri = Uri.parse("http://www.w3c.org");
+    gadgetContext = new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        return dummyUri;
+      }
+    };
+  }
+
+  private void testCssBasic(Gadget gadget) throws Exception {
+    String content = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic.css"));
+    String expected = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic-expected.css"));
+    HttpRequest request = new HttpRequest(Uri.parse("http://www.example.org/path/rewritebasic.css"));
+    request.setMethod("GET");
+    request.setGadget(SPEC_URL);
+
+    HttpResponseBuilder response = new HttpResponseBuilder().setHeader("Content-Type", "text/css")
+        .setResponseString(content);
+
+    rewriter.rewrite(request, response, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(response.getContent()));
+  }
+
+  @Test
+  public void testCssBasicGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testCssBasic(gadget);
+  }
+
+  @Test
+  public void testCssBasicNoGadget() throws Exception {
+    testCssBasic(null);
+  }
+
+  private void testCssBasicNoOverrideExpires(Gadget gadget) throws Exception {
+    String content = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic.css"));
+    String expected = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic-expected.css"));
+    expected = expected.replace("refresh=3600", "refresh=86400");
+    HttpRequest request = new HttpRequest(Uri.parse("http://www.example.org/path/rewritebasic.css"));
+    request.setMethod("GET");
+    request.setGadget(SPEC_URL);
+
+    HttpResponseBuilder response = new HttpResponseBuilder().setHeader("Content-Type", "text/css")
+      .setResponseString(content);
+
+    rewriterNoOverrideExpires.rewrite(request, response, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(response.getContent()));
+  }
+
+  @Test
+  public void testCssBasicNoOverrideExpiresGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testCssBasicNoOverrideExpires(gadget);
+  }
+
+  @Test
+  public void testCssBasicNoOverrideExpiresNoGadget() throws Exception {
+    testCssBasicNoOverrideExpires(null);
+  }
+
+  private void testCssBasicNoCache(Gadget gadget) throws Exception {
+    String content = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic.css"));
+    String expected = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic-expected.css"));
+    expected = expected.replace("nocache=0", "nocache=1");
+    HttpRequest request = new HttpRequest(Uri.parse("http://www.example.org/path/rewritebasic.css"));
+    request.setMethod("GET");
+    request.setGadget(SPEC_URL);
+    request.setIgnoreCache(true);
+
+    HttpResponseBuilder response = new HttpResponseBuilder().setHeader("Content-Type", "text/css")
+      .setResponseString(content);
+
+    rewriter.rewrite(request, response, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(response.getContent()));
+  }
+
+  @Test
+  public void testCssBasicNoCacheGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testCssBasicNoCache(gadget);
+  }
+
+  @Test
+  public void testCssBasicNoCacheNoGadget() throws Exception {
+    testCssBasicNoCache(null);
+  }
+
+  private void testCssWithContainerProxy(Gadget gadget) throws Exception {
+    String content = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic.css"));
+    String expected = IOUtils.toString(this.getClass().getClassLoader().
+        getResourceAsStream("org/apache/shindig/gadgets/rewrite/rewritebasic-expected.css"));
+    expected = replaceDefaultWithMockServer(expected);
+    expected = expected.replace("container=default", "container=" + MOCK_CONTAINER);
+    rewriter = new CssResponseRewriter(new CajaCssParser(),
+        proxyUriManager, factory);
+
+    HttpRequest request = new HttpRequest(Uri.parse("http://www.example.org/path/rewritebasic.css"));
+    request.setMethod("GET");
+    request.setGadget(SPEC_URL);
+    request.setContainer(MOCK_CONTAINER);
+
+    HttpResponseBuilder response = new HttpResponseBuilder().setHeader("Content-Type", "text/css")
+      .setResponseString(content);
+
+    rewriter.rewrite(request, response, gadget);
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(response.getContent()));
+  }
+
+  @Test
+  public void testCssWithContainerProxyGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testCssWithContainerProxy(gadget);
+  }
+
+  @Test
+  public void testCssWithContainerProxyNoGadget() throws Exception {
+    testCssWithContainerProxy(null);
+  }
+
+  private void testNoRewriteUnknownMimeType(Gadget gadget) throws Exception {
+    HttpRequest req = control.createMock(HttpRequest.class);
+    EasyMock.expect(req.getRewriteMimeType()).andReturn("unknown");
+    control.replay();
+    int changesBefore = fakeResponse.getNumChanges();
+
+    rewriter.rewrite(req, fakeResponse, gadget);
+    assertEquals(changesBefore, fakeResponse.getNumChanges());
+    control.verify();
+  }
+
+  @Test
+  public void testNoRewriteUnknownMimeTypeGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    testNoRewriteUnknownMimeType(gadget);
+  }
+
+  @Test
+  public void testNoRewriteUnknownMimeTypeNoGadget() throws Exception {
+    testNoRewriteUnknownMimeType(null);
+  }
+
+  private void validateRewritten(String content, Uri base, String expected, Gadget gadget) throws Exception {
+    HttpResponseBuilder response = new HttpResponseBuilder().setHeader("Content-Type", "text/css");
+    response.setContent(content);
+    HttpRequest request = new HttpRequest(base);
+    if(gadget == null) {
+      rewriter.rewrite(request, response, gadget);
+    } else {
+      rewriter.rewrite(request, response, gadget);
+    }
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(response.getContent()));
+  }
+
+  private void validateRewritten(String content, String expected, Gadget gadget) throws Exception {
+    validateRewritten(content, dummyUri, expected, gadget);
+  }
+
+  public void testUrlDeclarationRewrite(Gadget gadget) throws Exception {
+    String original =
+        "div {list-style-image:url('http://a.b.com/bullet.gif');list-style-position:outside;margin:5px;padding:0}\n" +
+         ".someid {background-image:url(http://a.b.com/bigimg.png);float:right;width:165px;height:23px;margin-top:4px;margin-left:5px}";
+    String rewritten =
+        "div {list-style-image:url('//www.test.com/dir/proxy?container=default"
+            + "&gadget=http%3A%2F%2Fwww.w3c.org&debug=0&nocache=0"
+            + "&url=http%3A%2F%2Fa.b.com%2Fbullet.gif');\n"
+            + "list-style-position:outside;margin:5px;padding:0}\n"
+            + ".someid {background-image:url('//www.test.com/dir/proxy?container=default"
+            + "&gadget=http%3A%2F%2Fwww.w3c.org&debug=0&nocache=0"
+            + "&url=http%3A%2F%2Fa.b.com%2Fbigimg.png');\n"
+            + "float:right;width:165px;height:23px;margin-top:4px;margin-left:5px}";
+    validateRewritten(original, rewritten, gadget);
+  }
+
+  @Test
+  public void testUrlDeclarationRewriteGadget() throws Exception {
+    Gadget gadget = mockGadget();
+    control.replay();
+    testUrlDeclarationRewrite(gadget);
+  }
+
+  @Test
+  public void testUrlDeclarationRewriteNoGadget() throws Exception {
+    testUrlDeclarationRewrite(null);
+  }
+  @Test
+  public void testExtractImports() throws Exception {
+    String original = " @import url(www.example.org/some.css);\n" +
+        "@import url('www.example.org/someother.css');\n" +
+        "@import url(\"www.example.org/another.css\");\n" +
+        " div { color: blue; }\n" +
+        " p { color: black; }\n" +
+        " span { color: red; }";
+    String expected = " div { color: blue; }\n" +
+        " p { color: black; }\n" +
+        " span { color: red; }";
+    StringWriter sw = new StringWriter();
+    List<String> stringList = rewriter
+        .rewrite(new StringReader(original), dummyUri,
+            CssResponseRewriter.uriMaker(proxyUriManager, defaultRewriterFeature), sw,
+            true, gadgetContext);
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(sw.toString()));
+    assertEquals(Lists.newArrayList("www.example.org/some.css",
+        "www.example.org/someother.css", "www.example.org/another.css"), stringList);
+  }
+
+  @Test
+  public void testMalformedImport() throws Exception {
+    String original = " @import \"www.example.org/some.css\";\n" +
+        " span { color: red; }";
+    String expected = " span { color: red; }";
+    StringWriter sw = new StringWriter();
+    List<String> stringList = rewriter
+        .rewrite(new StringReader(original), dummyUri,
+            CssResponseRewriter.uriMaker(proxyUriManager, defaultRewriterFeature), sw,
+            true, gadgetContext);
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(sw.toString()));
+    assertEquals(Lists.newArrayList("www.example.org/some.css"), stringList);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DefaultContentRewriterRegistryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DefaultContentRewriterRegistryTest.java
new file mode 100644
index 0000000..342a89a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DefaultContentRewriterRegistryTest.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import com.google.common.collect.Lists;
+
+public class DefaultContentRewriterRegistryTest extends BaseRewriterTestCase {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/gadget.xml");
+  private List<CaptureRewriter> rewriters;
+  private List<ResponseRewriter> contentRewriters;
+  private ResponseRewriterRegistry registry;
+
+  @Before
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    rewriters = Lists.newArrayList(new CaptureRewriter(), new CaptureRewriter());
+    contentRewriters = Lists.<ResponseRewriter>newArrayList(rewriters);
+    registry = new DefaultResponseRewriterRegistry(contentRewriters, parser);
+  }
+
+  @Test
+  public void testRewriteHttpResponse() throws Exception {
+    String body = "Hello, world";
+    HttpRequest request = new HttpRequest(SPEC_URL);
+    HttpResponse response = new HttpResponse(body);
+
+    HttpResponse rewritten = registry.rewriteHttpResponse(request, response, null);
+
+    assertTrue("First rewriter not invoked.", rewriters.get(0).responseWasRewritten());
+    assertTrue("Second rewriter not invoked.", rewriters.get(1).responseWasRewritten());
+
+    assertEquals(response, rewritten);
+  }
+
+  /**
+   * This test ensures that we dont call HttpResponse.getResponseAsString if no content
+   * rewriter does so either. This is important
+   * from a performance and content consistency standpoint. Because HttpResponse is final
+   * we test that no new response object was created.
+   */
+  @Test
+  public void testNoDecodeHttpResponseForUnRewriteableMimeTypes() throws Exception {
+    List<ResponseRewriter> rewriters = Lists.newArrayList();
+    rewriters.add(new ResponseRewriter() {
+      public void rewrite(HttpRequest request, HttpResponseBuilder response, Gadget gadget)
+              throws RewritingException {
+        // Do nothing.
+      }
+    });
+    registry = new DefaultResponseRewriterRegistry(rewriters, parser);
+
+    HttpRequest req = control.createMock(HttpRequest.class);
+    EasyMock.expect(req.getRewriteMimeType()).andStubReturn("unknown");
+
+    control.replay();
+    HttpResponse rewritten = registry.rewriteHttpResponse(req, fakeResponse.create(), null);
+    // Assert that response is untouched
+    assertSame(rewritten, fakeResponse.create());
+    control.verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java
new file mode 100644
index 0000000..964275d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTest.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.gadgets.Gadget;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Node;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class DomWalkerTest extends DomWalkerTestBase {
+  private Node root;
+  private Node child1;
+  private Node child2;
+  private Node subchild1;
+  private Node text1;
+  private Node text2;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    // Create a base document with structure:
+    // <root>
+    //   <child1>text1</child1>
+    //   <child2>
+    //     <subchild1>text2</subchild1>
+    //   </child2>
+    // </root>
+    // ...which should allow all relevant test cases to be exercised.
+    root = doc.createElement("root");
+    child1 = doc.createElement("child1");
+    text1 = doc.createTextNode("text1");
+    child1.appendChild(text1);
+    root.appendChild(child1);
+    child2 = doc.createElement("child2");
+    subchild1 = doc.createElement("subchild1");
+    text2 = doc.createTextNode("text2");
+    subchild1.appendChild(text2);
+    child2.appendChild(subchild1);
+    root.appendChild(child2);
+    doc.appendChild(root);
+  }
+
+  @Test
+  public void allBypassDoesNothing() throws Exception {
+    Gadget gadget = gadget();
+
+    // Visitor always bypasses nodes, never gets called with revisit(),
+    // but visits every node in the document.
+    DomWalker.Visitor visitor = createMock(DomWalker.Visitor.class);
+    expect(visitor.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor.visit(gadget, text2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    replay(visitor);
+
+    MutableContent mc = getContent(0);
+
+    DomWalker.Rewriter rewriter = getRewriter(visitor);
+    rewriter.rewrite(gadget, mc);
+
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+
+  @Test
+  public void allMutateMutatesEveryTime() throws Exception {
+    Gadget gadget = gadget();
+
+    // Visitor mutates every node it sees immediately and inline.
+    DomWalker.Visitor visitor = createMock(DomWalker.Visitor.class);
+    expect(visitor.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    expect(visitor.visit(gadget, text2))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    replay(visitor);
+
+    MutableContent mc = getContent(6);
+
+    DomWalker.Rewriter rewriter = getRewriter(visitor);
+    rewriter.rewrite(gadget, mc);
+
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+
+  @Test
+  public void allReserveNodeReservesAll() throws Exception {
+    Gadget gadget = gadget();
+
+    // Visitor mutates every node it sees immediately and inline.
+    DomWalker.Visitor visitor = createMock(DomWalker.Visitor.class);
+    expect(visitor.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor.visit(gadget, text2))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+
+    // All nodes are revisited in DFS order.
+    List<Node> allReserved =
+        Lists.newArrayList(root, child1, text1, child2, subchild1, text2);
+    expect(visitor.revisit(gadget, allReserved))
+        .andReturn(true).once();
+    replay(visitor);
+
+    MutableContent mc = getContent(1);  // Mutated each revisit.
+
+    DomWalker.Rewriter rewriter = getRewriter(visitor);
+    rewriter.rewrite(gadget, mc);
+
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+
+  @Test
+  public void reserveRootPrecludesAllElse() throws Exception {
+    Gadget gadget = gadget();
+
+    // Visitor1 reserves root, visitor2 never gets anything.
+    DomWalker.Visitor visitor1 = createMock(DomWalker.Visitor.class);
+    expect(visitor1.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_TREE).once();
+    List<Node> allReserved = Lists.newArrayList(root);
+    expect(visitor1.revisit(gadget, allReserved))
+        .andReturn(true).once();
+    DomWalker.Visitor visitor2 = createMock(DomWalker.Visitor.class);
+    replay(visitor1, visitor2);
+
+    MutableContent mc = getContent(1);  // Mutated once by revisit.
+
+    DomWalker.Rewriter rewriter = getRewriter(visitor1, visitor2);
+    rewriter.rewrite(gadget, mc);
+
+    // Verifying mutations on MutableContent completes the test.
+    verify(mc);
+  }
+
+  @Test
+  public void allMixedModes() throws Exception {
+    Gadget gadget = gadget();
+
+    // Visitor1 reserves single text node 1
+    DomWalker.Visitor visitor1 = createMock(DomWalker.Visitor.class);
+    expect(visitor1.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor1.visit(gadget, child1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_NODE).once();
+    expect(visitor1.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor1.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No visitation of text2 for visitor1 since visitor2 reserves the tree.
+    expect(visitor1.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No modification the second time around.
+    List<Node> reserved1 = Lists.newArrayList(child1);
+    expect(visitor1.revisit(gadget, reserved1))
+        .andReturn(false).once();
+
+    // Visitor2 reserves tree of subchild 1
+    DomWalker.Visitor visitor2 = createMock(DomWalker.Visitor.class);
+    expect(visitor2.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No visitation of v1-reserved child 1
+    expect(visitor2.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor2.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor2.visit(gadget, subchild1))
+        .andReturn(DomWalker.Visitor.VisitStatus.RESERVE_TREE).once();
+    List<Node> reserved2 = Lists.newArrayList(subchild1);
+    expect(visitor2.revisit(gadget, reserved2))
+        .andReturn(true).once();
+
+    // Visitor3 modifies child 2
+    DomWalker.Visitor visitor3 = createMock(DomWalker.Visitor.class);
+    expect(visitor3.visit(gadget, root))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    // No visitation of v1-reserved child 1
+    expect(visitor3.visit(gadget, text1))
+        .andReturn(DomWalker.Visitor.VisitStatus.BYPASS).once();
+    expect(visitor3.visit(gadget, child2))
+        .andReturn(DomWalker.Visitor.VisitStatus.MODIFY).once();
+    // No visitation of tree of subchild 1
+
+    replay(visitor1, visitor2, visitor3);
+
+    MutableContent mc = getContent(2);  // Once v2.revisit(), once v3.visit()
+
+    DomWalker.Rewriter rewriter = getRewriter(visitor1, visitor2, visitor3);
+    rewriter.rewrite(gadget, mc);
+
+    // As before, MutableContent verification is the test.
+    verify(mc);
+  }
+
+  @Test
+  public void rewriteThrowsRewritingExceptionIfGetDocumentIsNull() throws Exception {
+    DomWalker.Visitor visitor1 = createMock(DomWalker.Visitor.class);
+    DomWalker.Rewriter rewriter = getRewriter(visitor1);
+
+    MutableContent mc = createMock(MutableContent.class);
+    expect(mc.getDocument()).andReturn(null);
+    expect(mc.getContent()).andReturn("hello!");
+    replay(mc);
+
+    Gadget gadget = gadget();
+    boolean exceptionCaught = false;
+    try {
+      rewriter.rewrite(gadget, mc);
+    } catch (RewritingException e) {
+      assertEquals(e.getHttpStatusCode(),
+                   HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      exceptionCaught = true;
+    }
+
+    assertTrue(exceptionCaught);
+  }
+
+  private DomWalker.Rewriter getRewriter(DomWalker.Visitor... visitors) {
+    return new DomWalker.Rewriter(Lists.newArrayList(visitors));
+  }
+
+  private MutableContent getContent(int docChangedTimes) {
+    MutableContent mc = createMock(MutableContent.class);
+    expect(mc.getDocument()).andReturn(doc).once();
+    if (docChangedTimes > 0) {
+      mc.documentChanged();
+      expectLastCall().times(docChangedTimes);
+    }
+    replay(mc);
+    return mc;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTestBase.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTestBase.java
new file mode 100644
index 0000000..6f8d0a2
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/DomWalkerTestBase.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.replay;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.junit.Before;
+import org.w3c.dom.Attr;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.name.Names;
+import com.google.inject.util.Modules;
+
+public class DomWalkerTestBase {
+  protected static final Uri GADGET_URI = Uri.parse("http://example.com/gadget.xml");
+  protected static final String CONTAINER = "container";
+
+  protected Document doc;
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(Modules.override(new ParseModule())
+        .with(new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(Integer.class).annotatedWith(
+                Names.named("shindig.cache.lru.default.capacity"))
+                  .toInstance(0);
+          }
+        }));
+    DOMImplementation domImpl = injector.getInstance(DOMImplementation.class);
+    doc = domImpl.createDocument(null, null, null);
+  }
+
+  protected Element elem(String tag, String... attrStrs) {
+    Element elem = doc.createElement(tag);
+    for (int i = 0; attrStrs != null && i < attrStrs.length; i += 2) {
+      Attr attr = doc.createAttribute(attrStrs[i]);
+      attr.setValue(attrStrs[i+1]);
+      elem.setAttributeNode(attr);
+    }
+    return elem;
+  }
+
+  protected Element htmlDoc(Node[] headNodes, Node... bodyNodes) {
+    // Clear document of all nodes.
+    while (doc.hasChildNodes()) {
+      doc.removeChild(doc.getFirstChild());
+    }
+
+    // Recreate document with valid HTML structure.
+    Element html = elem("html");
+    Element head = elem("head");
+    appendAll(head, headNodes);
+    Element body = elem("body");
+    appendAll(body, bodyNodes);
+    html.appendChild(head);
+    html.appendChild(body);
+    doc.appendChild(html);
+
+    return html;
+  }
+
+  private void appendAll(Node parent, Node[] children) {
+    if (children == null || children.length == 0) return;
+
+    for (Node child : children) {
+      parent.appendChild(child);
+    }
+  }
+
+  protected Gadget gadget() {
+    return gadget(false, false);
+  }
+
+  protected Gadget gadget(boolean debug, boolean ignoreCache) {
+    return gadget(debug, ignoreCache, null);
+  }
+
+  protected Gadget gadget(boolean debug, boolean ignoreCache, Uri curviewHref) {
+    GadgetSpec spec = createMock(GadgetSpec.class);
+    expect(spec.getUrl()).andReturn(GADGET_URI).anyTimes();
+    Gadget gadget = createMock(Gadget.class);
+    expect(gadget.getSpec()).andReturn(spec).anyTimes();
+    GadgetContext ctx = createMock(GadgetContext.class);
+    expect(ctx.getParameter(Param.REFRESH.getKey())).andReturn(null).anyTimes();
+    expect(ctx.getDebug()).andReturn(debug).anyTimes();
+    expect(ctx.getIgnoreCache()).andReturn(ignoreCache).anyTimes();
+    expect(ctx.getContainer()).andReturn(CONTAINER).anyTimes();
+    expect(gadget.getContext()).andReturn(ctx).anyTimes();
+    View currentView = createMock(View.class);
+    expect(currentView.getHref()).andReturn(curviewHref).anyTimes();
+    expect(gadget.getCurrentView()).andReturn(currentView).anyTimes();
+    replay(ctx, spec, currentView, gadget);
+    return gadget;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ImageAttributeRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ImageAttributeRewriterTest.java
new file mode 100644
index 0000000..b4b7b19
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ImageAttributeRewriterTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.eq;
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor.VisitStatus;
+import org.apache.shindig.gadgets.rewrite.ImageAttributeRewriter.ImageAttributeVisitor;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.MultipleResourceHttpFetcher.RequestContext;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+import org.w3c.dom.Node;
+import org.easymock.IMocksControl;
+import org.easymock.EasyMock;
+
+import java.util.*;
+import java.util.concurrent.*;
+
+/**
+ * Tests for {@code ImageAttributeRewriter}
+ */
+public class ImageAttributeRewriterTest extends DomWalkerTestBase {
+  private RequestPipeline requestPipeline;
+  private IMocksControl control;
+  private transient ExecutorService executor = Executors.newSingleThreadExecutor();
+  private static final String IMG_JPG_SMALL_URL =
+      "org/apache/shindig/gadgets/rewrite/image/small.jpg";
+  private static final String IMG_JPG_LARGE_URL =
+      "org/apache/shindig/gadgets/rewrite/image/large.jpg";
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    control = EasyMock.createControl();
+    requestPipeline = control.createMock(RequestPipeline.class);
+  }
+
+  @Test
+  public void dontVisitImgTagWithClass() throws Exception {
+    Node img = elem("img", "class", "classname", "src", IMG_JPG_SMALL_URL);
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(img));
+  }
+
+  @Test
+  public void dontVisitImgTagWithId() throws Exception {
+    Node img = elem("img", "id", "idname", "src", IMG_JPG_SMALL_URL);
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(img));
+  }
+
+  @Test
+  public void dontVisitImgTagWithHeight() throws Exception {
+    Node img = elem("img", "height", "30", "src", IMG_JPG_SMALL_URL);
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(img));
+  }
+
+  @Test
+  public void dontVisitImgTagWithWidth() throws Exception {
+    Node img = elem("img", "width", "70", "src", IMG_JPG_SMALL_URL);
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(img));
+  }
+
+  @Test
+  public void dontVisitImgTagWithoutSrc() throws Exception {
+    Node img = elem("img");
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(img));
+  }
+
+  @Test
+  public void visitImgTagWithSrc() throws Exception {
+    Node img = elem("img", "src", IMG_JPG_SMALL_URL, "title", "test image");
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatus(img));
+  }
+
+  @Test
+  public void revisitZeroNodes() throws Exception {
+    assertEquals(false, getRevisitState(new ArrayList<Node>()));
+  }
+
+  @Test
+  public void revisit() throws Exception {
+    Node img1 = elem("img", "src", IMG_JPG_SMALL_URL);
+    Node img2 = elem("img", "src", IMG_JPG_LARGE_URL);
+    List<Node> nodes = ImmutableList.of(img1, img2);
+
+    RequestContext reqCxtImg1 = createRequestContext(IMG_JPG_SMALL_URL, "image/jpeg");
+    RequestContext reqCxtImg2 = createRequestContext(IMG_JPG_LARGE_URL, "image/jpeg");
+
+    expect(requestPipeline.execute(eq(reqCxtImg1.getHttpReq())))
+        .andReturn(reqCxtImg1.getHttpResp());
+    expect(requestPipeline.execute(eq(reqCxtImg2.getHttpReq())))
+        .andReturn(reqCxtImg2.getHttpResp());
+
+    Node html = htmlDoc(new Node[] {}, img1, img2);
+
+    String expectedContent = new StringBuilder()
+        .append(".__shindig__image0 {\n")
+        .append("  height: 16px;\n").append("  width: 16px;\n")
+        .append("}\n")
+        .append(".__shindig__image1 {\n")
+        .append("  height: 125px;\n").append("  width: 108px;\n")
+        .append("}\n").toString();
+
+    control.replay();
+    assertEquals(true, getRevisitState(nodes));
+    Node head = doc.getElementsByTagName("head").item(0);
+    assertEquals(1, head.getChildNodes().getLength());
+    assertEquals("style", head.getFirstChild().getNodeName());
+    assertEquals(expectedContent, head.getFirstChild().getTextContent());
+    control.verify();
+  }
+
+  private VisitStatus getVisitStatus(Node node) throws Exception {
+    return new ImageAttributeVisitor(requestPipeline, executor).visit(gadget(), node);
+  }
+
+  private boolean getRevisitState(List<Node> nodes) throws Exception{
+    return new ImageAttributeVisitor(requestPipeline, executor).revisit(gadget(), nodes);
+  }
+
+  private RequestContext createRequestContext(String resource, String mimeType) throws Exception {
+    HttpRequest request = null;
+    Uri uri = UriBuilder.parse(resource).toUri();
+    request = ImageAttributeVisitor.buildHttpRequest(gadget(), uri);
+
+
+    byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream(resource));
+    HttpResponse response =  new HttpResponseBuilder().addHeader("Content-Type", mimeType)
+            .setResponse(bytes).create();
+
+    return new RequestContext(request, response, null);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ImageResizeRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ImageResizeRewriterTest.java
new file mode 100644
index 0000000..173c62c
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ImageResizeRewriterTest.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.junit.Test;
+import org.junit.Before;
+import static org.junit.Assert.assertEquals;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.DefaultProxyUriManager;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.commons.lang3.StringUtils;
+import org.easymock.EasyMock;
+
+/**
+ * Tests for {@code ImageResizeRewriter}
+ */
+public class ImageResizeRewriterTest {
+  static final String CONTAINER = "test";
+
+  private ImageResizeRewriter rewriter;
+  private CajaHtmlParser parser;
+  private ParseModule.DOMImplementationProvider domImpl;
+  private ContainerConfig config;
+  private ContentRewriterFeature.Config featureConfig;
+  private ContentRewriterFeature.Factory factory;
+
+  @Before
+  public void setUp() {
+    config = EasyMock.createMock(ContainerConfig.class);
+    factory = EasyMock.createMock(ContentRewriterFeature.Factory.class);
+    featureConfig = EasyMock.createMock(ContentRewriterFeature.Config.class);
+
+    ProxyUriManager proxyUriManager = new DefaultProxyUriManager(config, null);
+    rewriter = new ImageResizeRewriter(proxyUriManager, factory);
+    domImpl = new ParseModule.DOMImplementationProvider();
+    parser = new CajaHtmlParser(domImpl.get());
+    EasyMock.expect(factory.get(EasyMock.isA(HttpRequest.class))).andReturn(featureConfig).anyTimes();
+    EasyMock.expect(factory.get(EasyMock.isA(GadgetSpec.class))).andReturn(featureConfig).anyTimes();
+    EasyMock.expect(config.getString(CONTAINER, DefaultProxyUriManager.PROXY_HOST_PARAM))
+        .andReturn("shindig.com").anyTimes();
+    EasyMock.expect(config.getString(CONTAINER, DefaultProxyUriManager.PROXY_PATH_PARAM))
+        .andReturn("/proxy").anyTimes();
+    EasyMock.expect(featureConfig.getExpires()).andReturn(new Integer(0)).anyTimes();
+  }
+
+  @Test
+  public void testImageResizeRewriter() throws Exception {
+
+    String content = "<html><head></head><body>"
+        + "<p> p tag </p>"
+        + "<img src=\"shindig.com/proxy?container=test&url=1.jpg\">"
+        + "<img height=\"50px\" id=\"img\" src=\"shindig.com/proxy?container=test&url=2.jpg\">"
+        + "<img src=\"shindig.com/proxy?container=test&url=3.jpg\" width=\"50px\">"
+        + "<img height=\"50px\" id=\"id\" src=\"shindig.com/proxy?container=test&url=4.jpg\""
+        + " width=\"110px\">"
+        + "<img height=\"5\" src=\"shindig.com/proxy?container=test&url=5.jpg\" width=\"10em\">"
+        + "<img height=\"50\" src=\"shindig.com/proxy?container=test&url=6.jpg\" width=\"110px\">"
+        + "<img src=\"shindig.com/proxy?container=test&url=7.jpg\""
+        + " style=\"height:50px; width:110px\">"
+        + "<img src=\"example.com/8.jpg\" style=\"height:50px; width:110px\">"
+        + "<img height=\"60px\" width=\"120px\" src=\"shindig.com/proxy?container=test&url=9.jpg\""
+        + " style=\"height:50px; width:110px\">"
+        + "<img width=\"120px\" src=\"shindig.com/proxy?container=test&url=10.jpg\""
+        + " style=\"height:50px;\">"
+        + "<img height=\"60px\" src=\"shindig.com/proxy?container=test&url=11.jpg\""
+        + " style=\"width:110px\">"
+        + "<img height=\"60px\" src=\"shindig.com/proxy?container=test&url=12.jpg\""
+        + " style=\"width:110px\" width=\"50px\">"
+        + "</body></html>";
+
+    String expected = "<html><head></head><body>"
+        + "<p> p tag </p>"
+        + "<img src=\"shindig.com/proxy?container=test&amp;url=1.jpg\">"
+        + "<img height=\"50px\" id=\"img\" src=\"shindig.com/proxy?container=test&amp;url=2.jpg\">"
+        + "<img src=\"shindig.com/proxy?container=test&amp;url=3.jpg\" width=\"50px\">"
+        + "<img height=\"50px\" id=\"id\" src=\"shindig.com/proxy?container=test&amp;url=4.jpg\""
+        + " width=\"110px\">"
+        + "<img height=\"5\" src=\"shindig.com/proxy?container=test&amp;url=5.jpg\" width=\"10em\">"
+        + "<img height=\"50\" src=\"" + getProxiedUrl("6.jpg", "50", "110") + "\" width=\"110px\">"
+        + "<img src=\"" + getProxiedUrl("7.jpg", "50", "110") + "\""
+        + " style=\"height:50px; width:110px\">"
+        + "<img src=\"example.com/8.jpg\" style=\"height:50px; width:110px\">"
+        + "<img  height=\"60px\" src=\"" + getProxiedUrl("9.jpg", "50", "110") + "\""
+        + " style=\"height:50px; width:110px\" width=\"120px\">"
+        + "<img src=\"shindig.com/proxy?container=test&amp;url=10.jpg\""
+        + " style=\"height:50px;\" width=\"120px\">"
+        + "<img height=\"60px\" src=\"shindig.com/proxy?container=test&amp;url=11.jpg\""
+        + " style=\"width:110px\">"
+        + "<img height=\"60px\" src=\"" + getProxiedUrl("12.jpg", "60", "110") + "\""
+        + " style=\"width:110px\" width=\"50px\">"
+        + "</body></html>";
+
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.shindig.com/"));
+    req.setGadget(UriBuilder.parse("http://www.shindig.com/").toUri());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(200)
+        .setHeader("Content-Type", "text/html")
+        .setResponse(content.getBytes())
+        .create();
+    HttpResponseBuilder builder = new HttpResponseBuilder(parser, resp);
+
+    EasyMock.replay(config, featureConfig, factory);
+    rewriter.rewrite(req, builder, null);
+    assertEquals(StringUtils.deleteWhitespace(expected),
+                 StringUtils.deleteWhitespace(builder.getContent()));
+    EasyMock.verify(config, featureConfig, factory);
+  }
+
+  private String getProxiedUrl(String resource, String height, String width) {
+    return "//shindig.com/proxy?container=test&amp;debug=0&amp;nocache=0&amp;refresh=0&amp;"
+           + "resize_h=" + height + "&amp;resize_w=" + width + "&amp;no_expand=1&amp;url="
+           + resource;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/MutableContentTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/MutableContentTest.java
new file mode 100644
index 0000000..e3711ec
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/MutableContentTest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.base.Charsets;
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.junit.Assert;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+import java.io.InputStream;
+import java.util.Arrays;
+
+public class MutableContentTest {
+  private MutableContent mhc;
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(new ParseModule(), new PropertiesModule());
+    mhc = new MutableContent(injector.getInstance(GadgetHtmlParser.class), "DEFAULT VIEW");
+  }
+
+  @Test
+  public void getContentAndParseTreeNoSets() throws Exception {
+    String content = mhc.getContent();
+    assertEquals("DEFAULT VIEW", content);
+
+    Document document = mhc.getDocument();
+    assertEquals(2, document.getFirstChild().getChildNodes().getLength());
+    Assert.assertSame(document.getFirstChild().getChildNodes().item(1).getFirstChild().getNodeType(), Node.TEXT_NODE);
+    assertEquals(content, document.getFirstChild().getChildNodes().item(1).getTextContent());
+
+    assertSame(content, mhc.getContent());
+    assertTrue(Arrays.equals(
+        content.getBytes("UTF8"), IOUtils.toByteArray(mhc.getContentBytes())));
+    assertSame(document, mhc.getDocument());
+    assertEquals(0, mhc.getNumChanges());
+  }
+
+  @Test
+  public void modifyContentReflectedInTreeAndBytes() throws Exception {
+    assertEquals(0, mhc.getNumChanges());
+    mhc.setContent("NEW CONTENT");
+    assertEquals(1, mhc.getNumChanges());
+    assertEquals("NEW CONTENT", new String(IOUtils.toByteArray(mhc.getContentBytes()), "UTF8"));
+    Document document = mhc.getDocument();
+    assertEquals(1, document.getChildNodes().getLength());
+    assertEquals("NEW CONTENT", document.getChildNodes().item(0).getTextContent());
+    mhc.documentChanged();
+    assertEquals(2, mhc.getNumChanges());
+  }
+
+  @Test
+  public void modifyContentReflectedInTreeUtf8() throws Exception {
+    String theContent = "N\uFFFDW C\uFFFDNT\uFFFDNT";
+
+    assertEquals(0, mhc.getNumChanges());
+    mhc.setContent(theContent);
+    assertEquals(1, mhc.getNumChanges());
+    assertEquals(theContent, new String(IOUtils.toByteArray(mhc.getContentBytes()), "UTF8"));
+    Document document = mhc.getDocument();
+    assertEquals(1, document.getChildNodes().getLength());
+    assertEquals(theContent, document.getChildNodes().item(0).getTextContent());
+    mhc.documentChanged();
+    assertEquals(2, mhc.getNumChanges());
+  }
+
+  @Test
+  public void modifyBytesReflectedInContentAndTree() throws Exception {
+    assertEquals(0, mhc.getNumChanges());
+    mhc.setContentBytes("NEW CONTENT".getBytes("UTF8"), Charsets.UTF_8);
+    assertEquals(1, mhc.getNumChanges());
+    Document document = mhc.getDocument();
+    assertEquals(1, document.getChildNodes().getLength());
+    assertEquals("NEW CONTENT", document.getChildNodes().item(0).getTextContent());
+    assertEquals("NEW CONTENT", mhc.getContent());
+    assertEquals(1, mhc.getNumChanges());
+    InputStream is = mhc.getContentBytes();
+    assertEquals("NEW CONTENT", new String(IOUtils.toByteArray(is), "UTF8"));
+    assertEquals(1, mhc.getNumChanges());
+  }
+
+  @Test
+  public void modifyTreeReflectedInContent() throws Exception {
+    Document document = mhc.getDocument();
+
+    // First child should be text node per other tests. Modify it.
+    document.getFirstChild().getFirstChild().setTextContent("FOO CONTENT");
+    assertEquals(0, mhc.getNumChanges());
+    MutableContent.notifyEdit(document);
+    assertEquals(1, mhc.getNumChanges());
+    assertTrue(mhc.getContent().contains("FOO CONTENT"));
+
+    // Do it again
+    document.getFirstChild().getFirstChild().setTextContent("BAR CONTENT");
+    MutableContent.notifyEdit(document);
+    assertEquals(2, mhc.getNumChanges());
+    assertTrue(mhc.getContent().contains("BAR CONTENT"));
+    assertTrue(new String(IOUtils.toByteArray(mhc.getContentBytes()), "UTF8").contains("BAR CONTENT"));
+
+    // GadgetHtmlNode hasn't changed because string hasn't changed
+    assertSame(document, mhc.getDocument());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/OsTemplateXmlLoaderRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/OsTemplateXmlLoaderRewriterTest.java
new file mode 100644
index 0000000..5eb5fa9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/OsTemplateXmlLoaderRewriterTest.java
@@ -0,0 +1,340 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor.VisitStatus;
+import org.apache.shindig.gadgets.rewrite.OsTemplateXmlLoaderRewriter.Converter;
+
+import org.json.JSONObject;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+public class OsTemplateXmlLoaderRewriterTest {
+  private GadgetHtmlParser parser;
+  private DOMImplementation domImpl;
+  private Document doc;
+  private Converter converter;
+
+  @Before
+  public void setUp() {
+    Injector injector = Guice.createInjector(new ParseModule(), new PropertiesModule());
+    parser = injector.getInstance(GadgetHtmlParser.class);
+    domImpl = injector.getInstance(DOMImplementation.class);
+    doc = domImpl.createDocument(null, null, null);
+    converter = new Converter(parser, domImpl);
+  }
+
+  @Test
+  public void convertSingleElement() throws Exception {
+    String xml = "<os:elem id=\"id\" foo=\"bar\">String value</os:elem>";
+    assertEquals(
+        new JSONObject("{n:\"template\",a:[],c:[{n:\"os:elem\",a:[{n:\"foo\",v:\"bar\"}," +
+            "{n:\"id\",v:\"id\"}],c:[\"String value\"]}]}").toString(),
+        converter.domToJson(xml));
+  }
+
+  @Test
+  public void convertMixedTreeWithIgnorables() throws Exception {
+    String xml = "<b>Some ${viewer} content</b>  <img/><!-- comment --><os:Html/>";
+    assertEquals(
+        new JSONObject("{n:\"template\",a:[],c:[{n:\"b\",a:[],c:" +
+            "[\"Some ${viewer} content\"]},\"  \",{n:\"img\",a:[],c:[]}," +
+            "{n:\"os:Html\",a:[],c:[]}]}").toString(),
+        converter.domToJson(xml));
+  }
+
+  @Test
+  public void visitNonElement() throws Exception {
+    assertEquals(VisitStatus.BYPASS, visit(doc.createTextNode("text")));
+    assertEquals(VisitStatus.BYPASS, visit(doc.createAttribute("foo")));
+    assertEquals(VisitStatus.BYPASS, visit(doc.createComment("comment")));
+  }
+
+  @Test
+  public void visitDivSansType() throws Exception {
+    assertEquals(VisitStatus.BYPASS, visit(doc.createElement("div")));
+  }
+
+  @Test
+  public void visitDivMismatchingType() throws Exception {
+    Element div = doc.createElement("div");
+    div.setAttribute("id", "id");
+    div.setAttribute("type", "os/template-but-not");
+    assertEquals(VisitStatus.BYPASS, visit(div));
+  }
+
+  @Test
+  public void visitDivMatchingTypeNoId() throws Exception {
+    Element div = doc.createElement("div");
+    div.setAttribute("type", OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME);
+    assertEquals(VisitStatus.BYPASS, visit(div));
+  }
+
+  @Test
+  public void visitDivMatchingTypeBlankIdAndName() throws Exception {
+    Element div = doc.createElement("div");
+    div.setAttribute("id", "");
+    div.setAttribute("name", "");
+    div.setAttribute("type", OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME);
+    assertEquals(VisitStatus.BYPASS, visit(div));
+  }
+
+  @Test
+  public void visitDivMatchingTypeWithId() throws Exception {
+    Element div = createRewritableDiv();
+    assertEquals(VisitStatus.RESERVE_NODE, visit(div));
+  }
+
+  @Test
+  public void visitDivMatchingCaseMixedWithId() throws Exception {
+    Element div = doc.createElement("dIv");
+    div.setAttribute("id", "id");
+    div.setAttribute("type", OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME.toUpperCase());
+    assertEquals(VisitStatus.RESERVE_NODE, visit(div));
+  }
+
+  @Test
+  public void visitDivMatchingTypeWithName() throws Exception {
+    Element div = doc.createElement("div");
+    div.setAttribute("name", "id");
+    div.setAttribute("type", OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME);
+    assertEquals(VisitStatus.RESERVE_NODE, visit(div));
+  }
+
+  @Test
+  public void visitDivMatchingCaseMixedWithName() throws Exception {
+    Element div = doc.createElement("dIv");
+    div.setAttribute("name", "id");
+    div.setAttribute("type", OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME.toUpperCase());
+    assertEquals(VisitStatus.RESERVE_NODE, visit(div));
+  }
+
+  private VisitStatus visit(Node node) throws Exception {
+    return new OsTemplateXmlLoaderRewriter.GadgetHtmlVisitor(null).visit(null, node);
+  }
+
+  @Test
+  public void revisitWithoutOsTemplates() throws Exception {
+    assertFalse(revisit(mockGadget("foo", "bar"), null));
+  }
+
+  @Test(expected = RewritingException.class)
+  public void revisitWithoutValidDocument() throws Exception {
+    revisit(mockGadget(OsTemplateXmlLoaderRewriter.OS_TEMPLATES_FEATURE_NAME, "foo"),
+        null, createRewritableDiv());
+  }
+
+  @Test(expected = RewritingException.class)
+  public void revisitWithoutHeadNode() throws Exception {
+    Node html = doc.createElement("html");
+    html.appendChild(doc.createElement("body"));
+    doc.appendChild(html);
+    revisit(mockGadget(OsTemplateXmlLoaderRewriter.OS_TEMPLATES_FEATURE_NAME, "foo"),
+        null, createRewritableDiv());
+  }
+
+  @Test
+  public void revisitWithIdDivSingle() throws Exception {
+    Element tpl = createRewritableDiv("tpl_id");
+    checkRevisitSingle(tpl, "tpl_id");
+  }
+
+  @Test
+  public void revisitWithNameDivSingle() throws Exception {
+    Element tpl = createRewritableDiv();
+    tpl.removeAttribute("id");
+    tpl.setAttribute("name", "otherid");
+    checkRevisitSingle(tpl, "otherid");
+  }
+
+  @Test
+  public void revisitWithBothLabeledDivSingle() throws Exception {
+    Element tpl = createRewritableDiv();
+    tpl.setAttribute("name", "otherid");
+    checkRevisitSingle(tpl, "otherid");
+  }
+
+  private void checkRevisitSingle(Element tpl, String id) throws Exception {
+    Gadget gadget = mockGadget(OsTemplateXmlLoaderRewriter.OS_TEMPLATES_FEATURE_NAME, "another");
+    String xmlVal = "xml";
+    Converter converter = mockConverter(xmlVal, "{thejson}", 1);
+    tpl.setTextContent(xmlVal);
+    completeDocAsHtml(tpl);
+    assertTrue(revisit(gadget, converter, tpl));
+    verify(gadget);
+    verify(converter);
+    Node head = DomUtil.getFirstNamedChildNode(doc.getDocumentElement(), "head");
+    assertNotNull(head);
+    assertEquals(2, head.getChildNodes().getLength());
+    Node addedScript = head.getChildNodes().item(1);
+    assertEquals(Node.ELEMENT_NODE, addedScript.getNodeType());
+    assertEquals("script", addedScript.getNodeName());
+    assertEquals("gadgets.jsondom.preload_('" + id + "',{thejson});", addedScript.getTextContent());
+  }
+
+  @Test
+  public void revisitMultiples() throws Exception {
+    Element tplId = createRewritableDiv("tpl_id");
+    Element tplName = createRewritableDiv();
+    tplName.removeAttribute("id");
+    tplName.setAttribute("name", "otherid");
+    Gadget gadget = mockGadget(OsTemplateXmlLoaderRewriter.OS_TEMPLATES_FEATURE_NAME, "another");
+    String xmlVal = "thexml";
+    Converter converter = mockConverter(xmlVal, "{thejson}", 2);
+    tplId.setTextContent(xmlVal);
+    tplName.setTextContent(xmlVal);
+    completeDocAsHtml(tplId, tplName);
+    assertTrue(revisit(gadget, converter, tplId, tplName));
+    verify(gadget);
+    verify(converter);
+    Node head = DomUtil.getFirstNamedChildNode(doc.getDocumentElement(), "head");
+    assertNotNull(head);
+    assertEquals(2, head.getChildNodes().getLength());
+    Node addedScript = head.getChildNodes().item(1);
+    assertEquals(Node.ELEMENT_NODE, addedScript.getNodeType());
+    assertEquals("script", addedScript.getNodeName());
+    assertEquals(
+        "gadgets.jsondom.preload_('tpl_id',{thejson});gadgets.jsondom.preload_('otherid',{thejson});",
+        addedScript.getTextContent());
+  }
+
+  private boolean revisit(Gadget gadget, Converter converter, Node... nodes) throws Exception {
+    return new OsTemplateXmlLoaderRewriter.GadgetHtmlVisitor(converter)
+        .revisit(gadget, Arrays.asList(nodes));
+  }
+
+  private Gadget mockGadget(String... features) {
+    Gadget gadget = createMock(Gadget.class);
+    expect(gadget.getAllFeatures()).andReturn(Arrays.asList(features)).once();
+    replay(gadget);
+    return gadget;
+  }
+
+  private Converter mockConverter(String xml, String result, int times) {
+    Converter converter = createMock(Converter.class);
+    expect(converter.domToJson(xml)).andReturn(result).times(times);
+    replay(converter);
+    return converter;
+  }
+
+  private Element createRewritableDiv() {
+    return createRewritableDiv("id");
+  }
+
+  private Element createRewritableDiv(String id) {
+    Element div = doc.createElement("div");
+    div.setAttribute("type", OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME);
+    div.setAttribute("id", id);
+    return div;
+  }
+
+  private void completeDocAsHtml(Node... nodes) {
+    Node html = doc.createElement("html");
+    Node head = doc.createElement("head");
+    Node headScript = doc.createElement("script");
+    head.appendChild(headScript);
+    Node body = doc.createElement("body");
+    for (Node node : nodes) {
+      body.appendChild(node);
+    }
+    html.appendChild(head);
+    html.appendChild(body);
+    while (doc.hasChildNodes()) {
+      doc.removeChild(doc.getFirstChild());
+    }
+    doc.appendChild(html);
+  }
+
+  @Test
+  public void rewriteHttpNoMime() throws Exception {
+    checkRewriteHttp(null, null, false);
+  }
+
+  @Test
+  public void rewriteHttpMismatchedMime() throws Exception {
+    checkRewriteHttp("os/template-not!", null, false);
+  }
+
+  @Test
+  public void rewriteHttpMimeMatchOverride() throws Exception {
+    checkRewriteHttp(OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME, "os/template-not!", true);
+  }
+
+  @Test
+  public void rewriteHttpMimeMatchOriginal() throws Exception {
+    checkRewriteHttp(null, OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME, true);
+  }
+
+  @Test
+  public void rewriteHttpMimeMatchOverrideMismatchOriginal() throws Exception {
+    checkRewriteHttp("foo", OsTemplateXmlLoaderRewriter.OS_TEMPLATE_MIME, false);
+  }
+
+  private void checkRewriteHttp(String reqMime, String origMime, boolean expectRewrite)
+      throws Exception {
+    HttpRequest req = new HttpRequest(Uri.parse("http://dummy.com")).setRewriteMimeType(reqMime);
+    HttpResponse resp = new HttpResponseBuilder().setHeader("Content-Type", origMime).create();
+    String inXml = "thexml";
+    String outJson = "{thejson}";
+    Converter converter = mockConverter(inXml, outJson, 1);
+    MutableContent mc = createMock(MutableContent.class);
+    if (expectRewrite) {
+      expect(mc.getContent()).andReturn(inXml).once();
+      mc.setContent(outJson);
+      expectLastCall().once();
+    }
+    replay(mc);
+    boolean result = new OsTemplateXmlLoaderRewriter(converter).rewrite(req, resp, mc);
+    assertEquals(expectRewrite, result);
+    verify(mc);
+    if (expectRewrite) {
+      verify(converter);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataGadgetRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataGadgetRewriterTest.java
new file mode 100644
index 0000000..779fe4e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/PipelineDataGadgetRewriterTest.java
@@ -0,0 +1,282 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.preload.ConcurrentPreloaderService;
+import org.apache.shindig.gadgets.preload.PipelineExecutor;
+import org.apache.shindig.gadgets.preload.PipelinedDataPreloader;
+import org.apache.shindig.gadgets.preload.PreloadException;
+import org.apache.shindig.gadgets.preload.PreloadedData;
+import org.apache.shindig.gadgets.preload.PreloaderService;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.PipelinedData;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+
+import com.google.common.collect.ImmutableList;
+import org.easymock.Capture;
+import static org.easymock.EasyMock.and;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.reportMatcher;
+import static org.easymock.EasyMock.same;
+import org.easymock.IArgumentMatcher;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.json.JSONException;
+import org.json.JSONObject;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+/**
+ * Test of PipelineDataContentRewriter.
+ */
+public class PipelineDataGadgetRewriterTest {
+
+  private IMocksControl control;
+  private PipelinedDataPreloader preloader;
+  private PreloaderService preloaderService;
+  private PipelineDataGadgetRewriter rewriter;
+  private GadgetSpec gadgetSpec;
+  private Gadget gadget;
+  private MutableContent content;
+  private static final Uri GADGET_URI = Uri.parse("http://example.org/gadget.php");
+
+  private static final String CONTENT =
+    "<script xmlns:os=\"http://ns.opensocial.org/2008/markup\" type=\"text/os-data\">"
+      + "  <os:PeopleRequest key=\"me\" userId=\"canonical\"/>"
+      + "  <os:HttpRequest key=\"json\" href=\"test.json\"/>"
+      + "</script>";
+
+  // One request, but it requires data that isn\"t present
+  private static final String BLOCKED_FIRST_BATCH_CONTENT =
+    "<script xmlns:os=\"http://ns.opensocial.org/2008/markup\" type=\"text/os-data\">"
+    + "  <os:PeopleRequest key=\"me\" userId=\"${json.user}\"/>"
+    + "</script>";
+
+  private static final String XML_WITHOUT_FEATURE = "<Module>" + "<ModulePrefs title=\"Title\">"
+      + "</ModulePrefs>" + "<Content>" + "    <![CDATA[" + CONTENT + "]]></Content></Module>";
+
+  private static final String XML_WITHOUT_PIPELINE = "<Module>" + "<ModulePrefs title=\"Title\">"
+      + "<Require feature=\"opensocial-data\"/>" + "</ModulePrefs>" + "<Content/></Module>";
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createStrictControl();
+    preloader = control.createMock(PipelinedDataPreloader.class);
+    preloaderService = new ConcurrentPreloaderService(Executors.newSingleThreadExecutor(), null);
+    rewriter = new PipelineDataGadgetRewriter(new PipelineExecutor(preloader, preloaderService,
+        Expressions.forTesting()));
+  }
+
+  private void setupGadget(String gadgetXml) throws SpecParserException {
+    gadgetSpec = new GadgetSpec(GADGET_URI, gadgetXml);
+    gadget = new Gadget();
+    gadget.setSpec(gadgetSpec);
+    gadget.setContext(new GadgetContext() {});
+    gadget.setCurrentView(gadgetSpec.getView("default"));
+
+    content = new MutableContent(new NekoSimplifiedHtmlParser(
+        new ParseModule.DOMImplementationProvider().get()), gadget.getCurrentView().getContent());
+  }
+
+  @Test
+  public void rewrite() throws Exception {
+    setupGadget(getGadgetXml(CONTENT));
+
+    Capture<PipelinedData.Batch> batchCapture =
+      new Capture<PipelinedData.Batch>();
+
+    // Dummy return results (the "real" return would have two values)
+    Callable<PreloadedData> callable = createPreloadTask(
+        "key", "{result: {foo: 'bar'}}");
+
+    // One batch with 1 each HTTP and Social preload
+    expect(preloader.createPreloadTasks(same(gadget.getContext()),
+            and(eqBatch(1, 1), capture(batchCapture))))
+            .andReturn(ImmutableList.of(callable));
+
+    control.replay();
+
+    rewriter.rewrite(gadget, content);
+
+    // Verify the data set is injected, and the os-data was deleted
+    assertTrue("Script not inserted", content.getContent().contains("DataContext.putDataSet(\"key\",{\"foo\":\"bar\"})"));
+    assertFalse("os-data wasn't deleted",
+        content.getContent().contains("type=\"text/os-data\""));
+
+    assertTrue(batchCapture.getValue().getPreloads().containsKey("me"));
+    assertTrue(batchCapture.getValue().getPreloads().containsKey("json"));
+
+    assertFalse(gadget.getDirectFeatureDeps().contains("opensocial-data"));
+    assertTrue(gadget.getDirectFeatureDeps().contains("opensocial-data-context"));
+
+    control.verify();
+  }
+  @Test
+  public void rewriteWithBlockedBatch() throws Exception {
+    setupGadget(getGadgetXml(BLOCKED_FIRST_BATCH_CONTENT));
+
+    // Expect a batch with no content
+    expect(
+        preloader.createPreloadTasks(same(gadget.getContext()), eqBatch(0, 0)))
+            .andReturn(ImmutableList.<Callable<PreloadedData>>of());
+
+    control.replay();
+
+    rewriter.rewrite(gadget, content);
+
+    control.verify();
+
+    // Check there is no DataContext inserted
+    assertFalse("DataContext write shouldn't be present", content.getContent().indexOf(
+        "DataContext.putDataSet(") > 0);
+    // And the os-data elements should be present
+    assertTrue("os-data was deleted",
+        content.getContent().indexOf("type=\"text/os-data\"") > 0);
+  }
+
+  /** Match a batch with the specified count of social and HTTP data items */
+  private PipelinedData.Batch eqBatch(int socialCount, int httpCount) {
+    reportMatcher(new BatchMatcher(socialCount, httpCount));
+    return null;
+  }
+
+  private static class BatchMatcher implements IArgumentMatcher {
+    private final int socialCount;
+    private final int httpCount;
+
+    public BatchMatcher(int socialCount, int httpCount) {
+      this.socialCount = socialCount;
+      this.httpCount = httpCount;
+    }
+
+    public void appendTo(StringBuffer buffer) {
+      buffer.append("eqBuffer[social=" + socialCount + ",http=" + httpCount + ']');
+    }
+
+    public boolean matches(Object obj) {
+      if (!(obj instanceof PipelinedData.Batch)) {
+        return false;
+      }
+
+      PipelinedData.Batch batch = (PipelinedData.Batch) obj;
+      int actualSocialCount = 0;
+      int actualHttpCount = 0;
+      for (PipelinedData.BatchItem item : batch.getPreloads().values()) {
+        if (item.getType() == PipelinedData.BatchType.HTTP) {
+          actualHttpCount++;
+        } else if (item.getType() == PipelinedData.BatchType.SOCIAL) {
+          actualSocialCount++;
+        }
+      }
+
+      return socialCount == actualSocialCount && httpCount == actualHttpCount;
+    }
+
+  }
+
+  @Test
+  public void rewriteWithoutPipeline() throws Exception {
+    setupGadget(XML_WITHOUT_PIPELINE);
+    control.replay();
+
+    // If there are no pipeline elements, the rewrite is a no-op
+    rewriter.rewrite(gadget, content);
+
+    control.verify();
+  }
+
+  @Test
+  public void rewriteWithoutFeature() throws Exception {
+    // If the opensocial-data feature is present, the rewrite is a no-op
+    setupGadget(XML_WITHOUT_FEATURE);
+
+    control.replay();
+
+    rewriter.rewrite(gadget, content);
+
+    control.verify();
+  }
+
+  @Test
+  /** Test that os:DataRequest is parsed correctly */
+  public void parseOfDataRequest() throws Exception {
+    final String contentWithDataRequest =
+      "<script xmlns:os=\"http://ns.opensocial.org/2008/markup\" type=\"text/os-data\">"
+        + "  <os:DataRequest key=\"me\" method=\"people.get\" userId=\"canonical\"/>"
+        + "</script>";
+
+    setupGadget(getGadgetXml(contentWithDataRequest));
+    Map<PipelinedData, ? extends Object> pipelines =
+        rewriter.parsePipelinedData(gadget, content.getDocument());
+    assertEquals(1, pipelines.size());
+    PipelinedData pipeline = pipelines.keySet().iterator().next();
+    PipelinedData.Batch batch = pipeline.getBatch(Expressions.forTesting(), new RootELResolver());
+    Map<String, PipelinedData.BatchItem> preloads = batch.getPreloads();
+    assertTrue(preloads.containsKey("me"));
+    assertEquals(PipelinedData.BatchType.SOCIAL, preloads.get("me").getType());
+
+    JsonAssert.assertObjectEquals(
+        "{params: {userId: 'canonical'}, method: 'people.get', id: 'me'}",
+        preloads.get("me").getData());
+  }
+
+  /** Create a mock Callable for a single preload task */
+  private Callable<PreloadedData> createPreloadTask(final String key, String jsonResult)
+      throws JSONException {
+    final JSONObject value = new JSONObject(jsonResult);
+    value.put("id", key);
+    final PreloadedData preloadResult = new PreloadedData() {
+      public Collection<Object> toJson() throws PreloadException {
+        return ImmutableList.<Object>of(value);
+      }
+    };
+
+    Callable<PreloadedData> callable = new Callable<PreloadedData>() {
+      public PreloadedData call() throws Exception {
+        return preloadResult;
+      }
+    };
+    return callable;
+  }
+
+  private static String getGadgetXml(String content) {
+    return "<Module>" + "<ModulePrefs title='Title'>"
+        + "<Require feature='opensocial-data'/>" + "</ModulePrefs>"
+        + "<Content>"
+        + "    <![CDATA[" + content + "]]>"
+        + "</Content></Module>";
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ProxyingContentRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ProxyingContentRewriterTest.java
new file mode 100644
index 0000000..d521f70
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ProxyingContentRewriterTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.junit.Test;
+
+public class ProxyingContentRewriterTest {
+  @Test
+  public void implementIntegrationTests() throws Exception {
+    // TODO: what the method says
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ProxyingVisitorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ProxyingVisitorTest.java
new file mode 100644
index 0000000..1941a24
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ProxyingVisitorTest.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor.VisitStatus;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.easymock.Capture;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+
+/**
+ * Test of proxying rewriter
+ */
+public class ProxyingVisitorTest extends DomWalkerTestBase {
+  private static final String URL_STRING = "http://www.foo.com/";
+  private static final Map<String, String> ALL_RESOURCES = ProxyingVisitor.Tags
+      .ALL_RESOURCES.getResourceTags();
+
+  @Test
+  public void imgVisitReserved() throws Exception {
+    checkVisitReserved("img", true);
+  }
+
+  @Test
+  public void inputVisitReserved() throws Exception {
+    checkVisitReserved("input", true);
+  }
+
+  @Test
+  public void bodyVisitReserved() throws Exception {
+    checkVisitReserved("body", true);
+  }
+
+  @Test
+  public void embedVisitReserved() throws Exception {
+    checkVisitReserved("embed", false);
+  }
+
+  @Test
+  public void csslinkVisitReserved() throws Exception {
+    checkVisitReserved("link", true, "rel", "stylesheet", "type", "text/css");
+  }
+
+  @Test
+  public void linkWithNoRelVisitReserved() throws Exception {
+    checkVisitReserved("link", false, "type", "text/css");
+  }
+
+  @Test
+  public void linkWithNoTypeVisitReserved() throws Exception {
+    checkVisitReserved("link", false, "rel", "stylesheet");
+  }
+
+  @Test
+  public void altlinkVisitReserved() throws Exception {
+    checkVisitReserved("link", false, "rel", "alternate", "hreflang", "el");
+  }
+
+  @Test
+  public void scriptVisitReserved() throws Exception {
+    checkVisitReserved("script", true);
+  }
+
+  @Test
+  public void objectVisitReserved() throws Exception {
+    checkVisitReserved("object", false);
+  }
+
+  @Test
+  public void otherVisitNotReserved() throws Exception {
+    checkVisitReserved("other", false);
+  }
+
+  @Test
+  public void imgWithEmptySrc() throws Exception {
+    Node node = elem("img", "src", "");
+    ContentRewriterFeature.Config config = createMock(ContentRewriterFeature.Config.class);
+    expect(config.shouldRewriteURL("")).andReturn(true).anyTimes();
+    expect(config.shouldRewriteTag("img")).andReturn(true).anyTimes();
+    replay(config);
+
+    ProxyingVisitor rewriter = new ProxyingVisitor(config, null,
+        ProxyingVisitor.Tags.SCRIPT,
+        ProxyingVisitor.Tags.STYLESHEET,
+        ProxyingVisitor.Tags.EMBEDDED_IMAGES);
+    VisitStatus status = rewriter.visit(null, node);
+    verify(config);
+
+    assertEquals("Empty attribute should not be rewritten", VisitStatus.BYPASS, status);
+  }
+
+  private void checkVisitReserved(String tag, boolean result, String ... attrs) throws Exception {
+    tag = tag.toLowerCase();
+    assertEquals(result, getVisitReserved(tag, true, true, attrs));
+    assertEquals(result, getVisitReserved(tag.toUpperCase(), true, true, attrs));
+    assertFalse(getVisitReserved(tag, false, true, attrs));
+    assertFalse(getVisitReserved(tag, true, false, attrs));
+    assertFalse(getVisitReserved(tag, false, false, attrs));
+  }
+
+  private boolean getVisitReserved(String tag, boolean resUrl, boolean resTag, String ... attrs) throws Exception {
+    // Reserved when lower-case and both URL and Tag reserved.
+    String attrName = ALL_RESOURCES.get(tag.toLowerCase());
+    attrName = attrName != null ? attrName : "src";
+
+    ArrayList <String> attrsList = Lists.newArrayList(attrs);
+    attrsList.add(0, attrName);
+    attrsList.add(1, URL_STRING);
+    attrs = attrsList.toArray(attrs);
+    Node node = elem(tag, attrs);
+    ContentRewriterFeature.Config config = createMock(ContentRewriterFeature.Config.class);
+    expect(config.shouldRewriteURL(URL_STRING)).andReturn(resUrl).anyTimes();
+    expect(config.shouldRewriteTag(tag.toLowerCase())).andReturn(resTag).anyTimes();
+    replay(config);
+
+    ProxyingVisitor rewriter = new ProxyingVisitor(config, null,
+        ProxyingVisitor.Tags.SCRIPT,
+        ProxyingVisitor.Tags.STYLESHEET,
+        ProxyingVisitor.Tags.EMBEDDED_IMAGES);
+    VisitStatus status = rewriter.visit(null, node);
+    verify(config);
+
+    return status != VisitStatus.BYPASS;
+  }
+
+  @Test
+  public void revisitModifyValidSkipInvalid() throws Exception {
+    // Batch test: ensures in-order modification.
+    // Includes one mod and one skip.
+    // No need to test invalid nodes since visit() and DomWalker tests preclude this.
+    String scriptSrc = "http://script.com/foo.js";
+    String imgSrc = "http://script.com/foo.jpg";
+    Element e1 = elem("script", "src", scriptSrc);
+    Element e2 = elem("script", "src", "^!,,|BLARGH");
+    Element e3 = elem("IMG", "src", imgSrc);
+    Element e4 = elem("script", "src", " " + scriptSrc + " ");
+    List<Node> nodes = ImmutableList.<Node>of(e1, e2, e3, e4);
+    ProxyUriManager uriManager = createMock(ProxyUriManager.class);
+    Uri rewrittenUri = Uri.parse("http://bar.com/");
+    List<Uri> returned = Lists.newArrayList(rewrittenUri, rewrittenUri, rewrittenUri);
+    ContentRewriterFeature.Config config = createMock(ContentRewriterFeature.Config.class);
+    Integer expires = 3;
+    expect(config.getExpires()).andReturn(expires).once();
+    expect(config);
+    Capture<List<ProxyUriManager.ProxyUri>> cap = new Capture<List<ProxyUriManager.ProxyUri>>();
+    Capture<Integer> intCap = new Capture<Integer>();
+    expect(uriManager.make(capture(cap), capture(intCap))).andReturn(returned).once();
+    replay(config, uriManager);
+    Gadget gadget = gadget();
+
+    ProxyingVisitor rewriter = new ProxyingVisitor(config, uriManager,
+        ProxyingVisitor.Tags.SCRIPT,
+        ProxyingVisitor.Tags.STYLESHEET,
+        ProxyingVisitor.Tags.EMBEDDED_IMAGES);
+    assertTrue(rewriter.revisit(gadget, nodes));
+    verify(config, uriManager);
+
+    assertEquals(3, cap.getValue().size());
+    assertEquals(Uri.parse(scriptSrc), cap.getValue().get(0).getResource());
+    assertEquals(Uri.parse(imgSrc), cap.getValue().get(1).getResource());
+    assertEquals(Uri.parse(scriptSrc), cap.getValue().get(2).getResource());
+    assertSame(expires, intCap.getValue());
+    assertEquals(rewrittenUri.toString(), e1.getAttribute("src"));
+    assertEquals("^!,,|BLARGH", e2.getAttribute("src"));
+    assertEquals(rewrittenUri.toString(), e3.getAttribute("src"));
+    assertEquals(rewrittenUri.toString(), e4.getAttribute("src"));
+
+    // Test that the html tag context has been correctly filled.
+    assertEquals("script", cap.getValue().get(0).getHtmlTagContext());
+    assertEquals("img", cap.getValue().get(1).getHtmlTagContext());
+    assertEquals("script", cap.getValue().get(2).getHtmlTagContext());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriteModuleTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriteModuleTest.java
new file mode 100644
index 0000000..670758a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriteModuleTest.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.DefaultGuiceModule;
+import org.apache.shindig.gadgets.admin.GadgetAdminModule;
+import org.apache.shindig.gadgets.oauth.OAuthModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2Module;
+import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerModule;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2PersistenceModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2MessageModule;
+import org.apache.shindig.gadgets.render.CajaResponseRewriter;
+import org.apache.shindig.gadgets.render.SanitizingResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterList.RewriteFlow;
+import org.apache.shindig.gadgets.rewrite.image.BasicImageRewriter;
+import org.apache.shindig.gadgets.uri.AccelUriManager;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+/**
+ * Tests for RewriteModule. Tests the flows and the associated rewriters.
+ */
+public class RewriteModuleTest {
+  Injector injector;
+
+  public static class TestClass {
+    public final ResponseRewriterRegistry defaultPipelineRegistry;
+    public final ResponseRewriterRegistry requestPipelineRegistry;
+    public final ResponseRewriterRegistry accelPipelineRegistry;
+
+    @Inject
+    public TestClass(@RewriterRegistry(rewriteFlow = RewriteFlow.REQUEST_PIPELINE)
+                     ResponseRewriterRegistry requestPipelineRegistry,
+                     @RewriterRegistry(rewriteFlow = RewriteFlow.DEFAULT)
+                     ResponseRewriterRegistry defaultPipelineRegistry,
+                     @RewriterRegistry(rewriteFlow = RewriteFlow.ACCELERATE)
+                     ResponseRewriterRegistry accelPipelineRegistry) {
+      this.defaultPipelineRegistry = defaultPipelineRegistry;
+      this.requestPipelineRegistry = requestPipelineRegistry;
+      this.accelPipelineRegistry = accelPipelineRegistry;
+    }
+  }
+
+  @Before
+  public void setUp() {
+    injector = Guice.createInjector(
+        new PropertiesModule(),
+        new GadgetAdminModule(),
+        new DefaultGuiceModule(), new OAuthModule(), new OAuth2Module(), new OAuth2PersistenceModule(), new OAuth2MessageModule(), new OAuth2HandlerModule());
+  }
+
+  @Test
+  public void testDefaultRewriters() throws Exception {
+    ContextAwareRegistry defaultPipelineRegistry = (ContextAwareRegistry)
+        injector.getInstance(TestClass.class).defaultPipelineRegistry;
+
+    List<ResponseRewriter> list = defaultPipelineRegistry.getResponseRewriters(
+        ContainerConfig.DEFAULT_CONTAINER);
+    assertEquals(7, list.size());
+    assertTrue(list.get(0) instanceof AbsolutePathReferenceRewriter);
+    assertTrue(list.get(1) instanceof StyleTagExtractorContentRewriter);
+    assertTrue(list.get(2) instanceof StyleAdjacencyContentRewriter);
+    assertTrue(list.get(3) instanceof ProxyingContentRewriter);
+    assertTrue(list.get(4) instanceof CssResponseRewriter);
+    assertTrue(list.get(5) instanceof SanitizingResponseRewriter);
+    assertTrue(list.get(6) instanceof CajaResponseRewriter);
+
+    list = defaultPipelineRegistry.getResponseRewriters(AccelUriManager.CONTAINER);
+    assertEquals(3, list.size());
+    assertTrue(list.get(0) instanceof AbsolutePathReferenceRewriter);
+    assertTrue(list.get(1) instanceof StyleTagProxyEmbeddedUrlsRewriter);
+    assertTrue(list.get(2) instanceof ProxyingContentRewriter);
+  }
+
+  @Test
+  public void testRequestPipelineRewriters() throws Exception {
+    ContextAwareRegistry requestPipelineRegistry = (ContextAwareRegistry)
+        injector.getInstance(TestClass.class).requestPipelineRegistry;
+
+    List<ResponseRewriter> list = requestPipelineRegistry.getResponseRewriters(
+        ContainerConfig.DEFAULT_CONTAINER);
+    assertEquals(1, list.size());
+    assertTrue(list.get(0) instanceof BasicImageRewriter);
+
+    list = requestPipelineRegistry.getResponseRewriters(AccelUriManager.CONTAINER);
+    assertEquals(1, list.size());
+    assertTrue(list.get(0) instanceof BasicImageRewriter);
+  }
+
+  @Test
+  public void testAccelRewriters() throws Exception {
+    ContextAwareRegistry accelPipelineRegistry = (ContextAwareRegistry)
+        injector.getInstance(TestClass.class).accelPipelineRegistry;
+
+    List<ResponseRewriter> list = accelPipelineRegistry.getResponseRewriters(
+        AccelUriManager.CONTAINER);
+    assertEquals(3, list.size());
+    assertTrue(list.get(0) instanceof AbsolutePathReferenceRewriter);
+    assertTrue(list.get(1) instanceof StyleTagProxyEmbeddedUrlsRewriter);
+    assertTrue(list.get(2) instanceof ProxyingContentRewriter);
+
+    list = accelPipelineRegistry.getResponseRewriters(ContainerConfig.DEFAULT_CONTAINER);
+    assertEquals(0, list.size());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriterTestBase.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriterTestBase.java
new file mode 100644
index 0000000..277d7ea
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriterTestBase.java
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetSpecFactory;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.ModulePrefs;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+
+import com.google.common.base.Joiner;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.util.Modules;
+import com.google.inject.util.Providers;
+
+/**
+ * Base class for testing content rewriting functionality
+ */
+public abstract class RewriterTestBase {
+  public static final Uri SPEC_URL = Uri.parse("http://www.example.org/dir/g.xml");
+  public static final String DEFAULT_PROXY_BASE = "http://www.test.com/dir/proxy?url=";
+  public static final String DEFAULT_CONCAT_BASE = "http://www.test.com/dir/concat?";
+  protected final String TAGS = "embed,img,script,link,style";
+
+  public static final String MOCK_CONTAINER = "mock";
+  public static final String MOCK_PROXY_BASE =
+    replaceDefaultWithMockServer(DEFAULT_PROXY_BASE);
+  public static final String MOCK_CONCAT_BASE =
+    replaceDefaultWithMockServer(DEFAULT_CONCAT_BASE);
+
+  protected Set<String> tags;
+  protected ContentRewriterFeature.Config defaultRewriterFeature;
+  protected ContentRewriterFeature.Factory rewriterFeatureFactory;
+  protected GadgetHtmlParser parser;
+  protected Injector injector;
+  protected HttpResponseBuilder fakeResponse;
+  protected IMocksControl control;
+
+  @Before
+  public void setUp() throws Exception {
+    rewriterFeatureFactory = new ContentRewriterFeature.Factory(null,
+        Providers.of(new ContentRewriterFeature.DefaultConfig(".*", "", "86400", TAGS, false, false, false)));
+    defaultRewriterFeature = rewriterFeatureFactory.getDefault();
+    tags = defaultRewriterFeature.getIncludedTags();
+    injector = Guice.createInjector(getParseModule(), new PropertiesModule(), new TestModule());
+    parser = injector.getInstance(GadgetHtmlParser.class);
+    fakeResponse = new HttpResponseBuilder().setHeader("Content-Type", "unknown")
+        .setResponse(new byte[]{ (byte)0xFE, (byte)0xFF});
+    control = EasyMock.createControl();
+  }
+
+  private Module getParseModule() {
+    return Modules.override(new ParseModule()).with(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(GadgetHtmlParser.class).to(getParserClass());
+      }
+    });
+  }
+
+  protected Class<? extends GadgetHtmlParser> getParserClass() {
+    return NekoSimplifiedHtmlParser.class;
+  }
+
+  public Gadget mockGadget() {
+    return mockGadget(new ArrayList<Feature>(), MOCK_CONTAINER, SPEC_URL.toString());
+  }
+
+  public Gadget mockGadget(List<Feature> allFeatures, String container, String gadgetUrl) {
+    Gadget mockGadget = control.createMock(Gadget.class);
+    GadgetContext mockContext = mockGadgetContext(container);
+    GadgetSpec mockSpec = mockGadgetSpec(allFeatures, gadgetUrl);
+    EasyMock.expect(mockGadget.getContext()).andReturn(mockContext).anyTimes();
+    EasyMock.expect(mockGadget.getSpec()).andReturn(mockSpec).anyTimes();
+    return mockGadget;
+  }
+
+  private GadgetContext mockGadgetContext(String container) {
+    GadgetContext mockContext = control.createMock(GadgetContext.class);
+    EasyMock.expect(mockContext.getContainer()).andReturn(container).anyTimes();
+    return mockContext;
+  }
+
+  private GadgetSpec mockGadgetSpec(List<Feature> allFeatures, String gadgetUrl) {
+    GadgetSpec mockSpec = control.createMock(GadgetSpec.class);
+    ModulePrefs mockPrefs = mockModulePrefs(allFeatures);
+    EasyMock.expect(mockSpec.getUrl()).andReturn(Uri.parse(gadgetUrl)).anyTimes();
+    EasyMock.expect(mockSpec.getModulePrefs()).andReturn(mockPrefs).anyTimes();
+    return mockSpec;
+  }
+
+  private ModulePrefs mockModulePrefs(List<Feature> features) {
+    ModulePrefs mockPrefs = control.createMock(ModulePrefs.class);
+    EasyMock.expect(mockPrefs.getAllFeatures()).andReturn(features).anyTimes();
+    return mockPrefs;
+  }
+
+  public static GadgetSpec createSpecWithRewrite(String include, String exclude, String expires,
+      Set<String> tags) throws GadgetException {
+    StringBuilder xml = new StringBuilder();
+    xml.append("<Module>");
+    xml.append("<ModulePrefs title=\"title\">");
+    xml.append("<Optional feature=\"content-rewrite\">\n");
+    if(expires != null)
+      xml.append("      <Param name=\"expires\">" + expires + "</Param>\n");
+    if(include != null)
+      xml.append("      <Param name=\"include-urls\">" + include + "</Param>\n");
+    if(exclude != null)
+      xml.append("      <Param name=\"exclude-urls\">" + exclude + "</Param>\n");
+    if(tags != null)
+      xml.append("      <Param name=\"include-tags\">" + Joiner.on(',').join(tags) + "</Param>\n");
+    xml.append("</Optional>");
+    xml.append("</ModulePrefs>");
+    xml.append("<Content type=\"html\">Hello!</Content>");
+    xml.append("</Module>");
+    return new GadgetSpec(SPEC_URL, xml.toString());
+  }
+
+  public static GadgetSpec createSpecWithRewriteOS9(String[] includes, String[] excludes, String expires,
+      Set<String> tags) throws GadgetException {
+    StringBuilder xml = new StringBuilder();
+    xml.append("<Module>");
+    xml.append("<ModulePrefs title=\"title\">");
+    xml.append("<Optional feature=\"content-rewrite\">\n");
+    if(expires != null)
+      xml.append("      <Param name=\"expires\">" + expires + "</Param>\n");
+    if(includes != null)
+      for (String include : includes) {
+        xml.append("      <Param name=\"include-url\">" + include + "</Param>\n");
+      }
+    if(excludes != null)
+      for (String exclude : excludes) {
+        xml.append("      <Param name=\"exclude-url\">" + exclude + "</Param>\n");
+      }
+    if(tags != null)
+      xml.append("      <Param name=\"include-tags\">" + Joiner.on(',').join(tags) + "</Param>\n");
+    xml.append("</Optional>");
+    xml.append("</ModulePrefs>");
+    xml.append("<Content type=\"html\">Hello!</Content>");
+    xml.append("</Module>");
+    return new GadgetSpec(SPEC_URL, xml.toString());
+  }
+
+  public static GadgetSpec createSpecWithoutRewrite() throws GadgetException {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"title\">" +
+                 "</ModulePrefs>" +
+                 "<Content type=\"html\">Hello!</Content>" +
+                 "</Module>";
+    return new GadgetSpec(SPEC_URL, xml);
+  }
+
+  public static String replaceDefaultWithMockServer(String originalText) {
+    return originalText.replace("test.com", "mock.com");
+  }
+
+  protected String rewriteHelper(GadgetRewriter rewriter, String s)
+      throws Exception {
+    MutableContent mc = rewriteContent(rewriter, s, null);
+    String rewrittenContent = mc.getContent();
+
+    // Strip around the HTML tags for convenience
+    int htmlTagIndex = rewrittenContent.indexOf("<HTML>");
+    if (htmlTagIndex != -1) {
+      return rewrittenContent.substring(htmlTagIndex + 6,
+          rewrittenContent.lastIndexOf("</HTML>"));
+    }
+    return rewrittenContent;
+  }
+
+  protected MutableContent rewriteContent(GadgetRewriter rewriter, String s,
+      final String container) throws Exception {
+    return rewriteContent(rewriter, s, container, false, false);
+  }
+
+  protected MutableContent rewriteContent(GadgetRewriter rewriter, String s,
+      final String container, final boolean debug, final boolean ignoreCache)
+      throws Exception {
+    MutableContent mc = new MutableContent(parser, s);
+
+    GadgetSpec spec = new GadgetSpec(SPEC_URL,
+        "<Module><ModulePrefs title=''/><Content><![CDATA[" + s + "]]></Content></Module>");
+
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        return SPEC_URL;
+      }
+
+      @Override
+      public String getContainer() {
+        return container;
+      }
+
+      @Override
+      public boolean getDebug() {
+        return debug;
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return ignoreCache;
+      }
+    };
+
+    Gadget gadget = new Gadget()
+        .setContext(context)
+        .setSpec(spec);
+    rewriter.rewrite(gadget, mc);
+    return mc;
+  }
+
+  private static class TestModule extends AbstractModule {
+
+    @Override
+    protected void configure() {
+      bind(RequestPipeline.class).toInstance(new RequestPipeline() {
+        public HttpResponse execute(HttpRequest request) { return null; }
+      });
+
+      bind(GadgetSpecFactory.class).toInstance(new GadgetSpecFactory() {
+        public GadgetSpec getGadgetSpec(GadgetContext context) {
+          return null;
+        }
+        public Uri getGadgetUri(GadgetContext context) {
+          return null;
+        }
+      });
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriterUtilsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriterUtilsTest.java
new file mode 100644
index 0000000..f800955
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/RewriterUtilsTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.uri.UriCommon;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests for RewriterUtils.
+ */
+public class RewriterUtilsTest {
+  @Test
+  public void testIsHtmlWithoutHtmlTagContext() throws Exception {
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.example.org/"));
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/html");
+    assertTrue(RewriterUtils.isHtml(req, builder));
+  }
+
+  @Test
+  public void testIsHtmlReturnsFalseIfNonHtmlTagContext() throws Exception {
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.example.org/"));
+    req.setParam(UriCommon.Param.HTML_TAG_CONTEXT.getKey(), "script");
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/html");
+    assertFalse(RewriterUtils.isHtml(req, builder));
+  }
+
+  @Test
+  public void testIsHtmlReturnsTrueIfHtmlAcceptingTagContext() throws Exception {
+    HttpRequest req = new HttpRequest(Uri.parse("http://www.example.org/"));
+    req.setParam(UriCommon.Param.HTML_TAG_CONTEXT.getKey(), "link");
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .addHeader("Content-Type", "text/html");
+    assertTrue(RewriterUtils.isHtml(req, builder));
+
+    req.setParam(UriCommon.Param.HTML_TAG_CONTEXT.getKey(), "iframe");
+    assertTrue(RewriterUtils.isHtml(req, builder));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ScriptConcatContentRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ScriptConcatContentRewriterTest.java
new file mode 100644
index 0000000..01cf853
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/ScriptConcatContentRewriterTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.junit.Test;
+
+public class ScriptConcatContentRewriterTest {
+  @Test
+  public void implementIntegrationTests() throws Exception {
+    // TODO: what the method says
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyContentRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyContentRewriterTest.java
new file mode 100644
index 0000000..841c430
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyContentRewriterTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.junit.Test;
+
+public class StyleAdjacencyContentRewriterTest {
+  @Test
+  public void implementIntegrationTests() throws Exception {
+    // TODO: what the method says
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyVisitorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyVisitorTest.java
new file mode 100644
index 0000000..669cee4
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleAdjacencyVisitorTest.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor.VisitStatus;
+
+import org.w3c.dom.Node;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class StyleAdjacencyVisitorTest extends DomWalkerTestBase {
+  @Test
+  public void visitStyle() throws Exception {
+    Node node = elem("style");
+    assertEquals(VisitStatus.RESERVE_TREE, visit(node));
+  }
+
+  @Test
+  public void visitLinkWithRel() throws Exception {
+    Node node = elem("link", "rel", "stylesheet");
+    assertEquals(VisitStatus.RESERVE_TREE, visit(node));
+  }
+
+  @Test
+  public void visitLinkWithType() throws Exception {
+    Node node = elem("link", "type", "text/css");
+    assertEquals(VisitStatus.RESERVE_TREE, visit(node));
+  }
+
+  @Test
+  public void visitStyleCaseInsensitive() throws Exception {
+    Node node = elem("sTYlE");
+    assertEquals(VisitStatus.RESERVE_TREE, visit(node));
+  }
+
+  @Test
+  public void visitLinkCaseInsensitive() throws Exception {
+    Node node = elem("lINK", "REL", "stYlEsheet");
+    assertEquals(VisitStatus.RESERVE_TREE, visit(node));
+    node = elem("LINk", "tyPe", "text/csS");
+    assertEquals(VisitStatus.RESERVE_TREE, visit(node));
+  }
+
+  @Test
+  public void visitStyleWithAttribs() throws Exception {
+    Node node = elem("style", "foo", "bar");
+    assertEquals(VisitStatus.RESERVE_TREE, visit(node));
+  }
+
+  @Test
+  public void bypassUnknownElement() throws Exception {
+    Node node = elem("div");
+    assertEquals(VisitStatus.BYPASS, visit(node));
+  }
+
+  @Test
+  public void bypassLinkWithoutAttribs() throws Exception {
+    Node node = elem("link");
+    assertEquals(VisitStatus.BYPASS, visit(node));
+  }
+
+  @Test
+  public void bypassLinkWithWrongAttribs() throws Exception {
+    Node node = elem("link", "type", "somecss");
+    assertEquals(VisitStatus.BYPASS, visit(node));
+  }
+
+  @Test
+  public void bypassText() throws Exception {
+    Node node = doc.createTextNode("text");
+    assertEquals(VisitStatus.BYPASS, visit(node));
+  }
+
+  @Test
+  public void bypassComment() throws Exception {
+    Node node = doc.createComment("comment");
+    assertEquals(VisitStatus.BYPASS, visit(node));
+  }
+
+  @Test
+  public void reshuffleSingleNodeInHead() throws Exception {
+    Node style = elem("style");
+    Node html = htmlDoc(new Node[] { elem("script"), doc.createTextNode("foo"),
+        style, doc.createComment("comment") });
+    assertTrue(revisit(style));
+
+    // Document structure sanity tests.
+    assertEquals(2, html.getChildNodes().getLength());
+    Node head = html.getFirstChild();
+    assertEquals("head", head.getNodeName());
+    Node body = html.getLastChild();
+    assertEquals("body", body.getNodeName());
+
+    // Reshuffling validation.
+    assertEquals(4, head.getChildNodes().getLength());
+    assertSame(style, head.getChildNodes().item(0)); // First.
+  }
+
+  @Test
+  public void reshuffleSingleNodeFromBody() throws Exception {
+    Node style = elem("style");
+    Node html = htmlDoc(new Node[] { elem("foo") }, elem("script"), doc.createTextNode("foo"),
+        style, doc.createComment("comment"));
+    assertTrue(revisit(style));
+
+    // Document structure sanity tests.
+    assertEquals(2, html.getChildNodes().getLength());
+    Node head = html.getFirstChild();
+    assertEquals("head", head.getNodeName());
+    Node body = html.getLastChild();
+    assertEquals("body", body.getNodeName());
+
+    // Reshuffling validation.
+    assertEquals(2, head.getChildNodes().getLength());
+    assertSame(style, head.getChildNodes().item(0)); // First.
+    assertEquals(3, body.getChildNodes().getLength());
+  }
+
+  @Test
+  public void reshuffleMultipleStyleNodesWithNoChildernInHead() throws Exception {
+    Node style1 = elem("style");
+    Node style2 = elem("style");
+    Node style3 = elem("style");
+
+    // Some in head, some in body.
+    Node html = htmlDoc(new Node[] {}, elem("script"), style1, elem("foo"),
+        doc.createTextNode("text1"), style2, doc.createComment("comment"), elem("div"),
+        style3);
+    assertTrue(revisit(style1, style2, style3));
+
+    // Document structure sanity tests.
+    assertEquals(2, html.getChildNodes().getLength());
+    Node head = html.getFirstChild();
+    assertEquals("head", head.getNodeName());
+    Node body = html.getLastChild();
+    assertEquals("body", body.getNodeName());
+
+    // Reshuffling validation.
+    assertEquals(3, head.getChildNodes().getLength());
+    assertSame(style1, head.getChildNodes().item(0));
+    assertSame(style2, head.getChildNodes().item(1));
+    assertSame(style3, head.getChildNodes().item(2));
+    assertEquals(5, body.getChildNodes().getLength());
+  }
+
+  @Test
+  public void reshuffleMultipleStyleNodes() throws Exception {
+    Node style1 = elem("style");
+    Node style2 = elem("style");
+    Node style3 = elem("style");
+
+    // Some in head, some in body.
+    Node html = htmlDoc(new Node[] { elem("script"), style1, elem("foo") },
+        doc.createTextNode("text1"), style2, doc.createComment("comment"), elem("div"),
+        style3);
+    assertTrue(revisit(style1, style2, style3));
+
+    // Document structure sanity tests.
+    assertEquals(2, html.getChildNodes().getLength());
+    Node head = html.getFirstChild();
+    assertEquals("head", head.getNodeName());
+    Node body = html.getLastChild();
+    assertEquals("body", body.getNodeName());
+
+    // Reshuffling validation.
+    assertEquals(5, head.getChildNodes().getLength());
+    assertSame(style1, head.getChildNodes().item(0));
+    assertSame(style2, head.getChildNodes().item(1));
+    assertSame(style3, head.getChildNodes().item(2));
+    assertEquals(3, body.getChildNodes().getLength());
+  }
+
+  @Test
+  public void reshuffleMultipleLinkNodes() throws Exception {
+    Node link1 = elem("link", "rel", "stylesheet");
+    Node link2 = elem("link", "rel", "stylesheet");
+    Node link3 = elem("link", "rel", "stylesheet");
+
+    // Some in head, some in body.
+    Node html = htmlDoc(new Node[] { link1, elem("script"), elem("foo") },
+        doc.createTextNode("text1"), link2, doc.createComment("comment"), elem("div"),
+        link3);
+    assertTrue(revisit(link1, link2, link3));
+
+    // Document structure sanity tests.
+    assertEquals(2, html.getChildNodes().getLength());
+    Node head = html.getFirstChild();
+    assertEquals("head", head.getNodeName());
+    Node body = html.getLastChild();
+    assertEquals("body", body.getNodeName());
+
+    // Reshuffling validation.
+    assertEquals(5, head.getChildNodes().getLength());
+    assertSame(link1, head.getChildNodes().item(0));
+    assertSame(link2, head.getChildNodes().item(1));
+    assertSame(link3, head.getChildNodes().item(2));
+    assertEquals(3, body.getChildNodes().getLength());
+  }
+
+  @Test
+  public void reshuffleMultiMatchedNodes() throws Exception {
+    Node style1 = elem("style");
+    Node style2 = elem("style");
+    Node link1 = elem("link", "rel", "stylesheet");
+    Node link2 = elem("link", "type", "text/css");
+
+    // Some in head, some in body, one embedded.
+    Node div = elem("div");
+    div.appendChild(style2);
+    Node html = htmlDoc(new Node[] { elem("base"), elem("script"), elem("script"), style1,
+        doc.createComment("comment"), link1 },
+        elem("div"), div, link2, doc.createTextNode("text"));
+    assertTrue(revisit(style1, link1, style2, link2));
+
+    // Document structure sanity tests.
+    assertEquals(2, html.getChildNodes().getLength());
+    Node head = html.getFirstChild();
+    assertEquals("head", head.getNodeName());
+    Node body = html.getLastChild();
+    assertEquals("body", body.getNodeName());
+
+    // Reshuffling validation.
+    assertEquals(8, head.getChildNodes().getLength());
+    assertSame(style1, head.getChildNodes().item(0));
+    assertSame(link1, head.getChildNodes().item(1));
+    assertSame(style2, head.getChildNodes().item(2));
+    assertSame(link2, head.getChildNodes().item(3));
+    assertEquals(0, div.getChildNodes().getLength());
+    assertEquals(3, body.getChildNodes().getLength());
+  }
+
+  @Test
+  public void avoidReshufflingInHeadlessDocument() throws Exception {
+    Node style = elem("style");
+    Node html = elem("html");
+    Node body = elem("body");
+    body.appendChild(style);
+    html.appendChild(body);
+    doc.appendChild(html);
+
+    assertFalse(revisit(style));
+
+    // Document structure sanity tests.
+    assertEquals(1, html.getChildNodes().getLength());
+    assertSame(body, html.getFirstChild());
+  }
+
+  @Test
+  public void singleStyleNodeInHead() throws Exception {
+    Node style = elem("style", "type", "text/css");
+    Node head = elem("head");
+    head.appendChild(style);
+
+    Node html = elem("html");
+    html.appendChild(head);
+    html.appendChild(elem("body"));
+    doc.appendChild(html);
+
+    assertTrue(revisit(style));
+
+    // Document structure sanity tests.
+    assertEquals(2, html.getChildNodes().getLength());
+    assertSame(head, html.getFirstChild());
+  }
+
+
+
+  private VisitStatus visit(Node node) throws Exception {
+    return new StyleAdjacencyVisitor().visit(gadget(), node);
+  }
+
+  private boolean revisit(Node... nodes) throws Exception {
+    return new StyleAdjacencyVisitor().revisit(gadget(), ImmutableList.<Node>copyOf(nodes));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleConcatContentRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleConcatContentRewriterTest.java
new file mode 100644
index 0000000..47a79b5
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleConcatContentRewriterTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.junit.Test;
+
+public class StyleConcatContentRewriterTest {
+  @Test
+  public void implementIntegrationTests() throws Exception {
+    // TODO: what the method says
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorContentRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorContentRewriterTest.java
new file mode 100644
index 0000000..27c3e73
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorContentRewriterTest.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.junit.Test;
+
+public class StyleTagExtractorContentRewriterTest {
+  @Test
+  public void implementIntegrationTests() throws Exception {
+    // TODO: what the method says
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorVisitorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorVisitorTest.java
new file mode 100644
index 0000000..0a5eb1f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagExtractorVisitorTest.java
@@ -0,0 +1,256 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.rewrite.CssResponseRewriter.UriMaker;
+import org.apache.shindig.gadgets.rewrite.DomWalker.Visitor.VisitStatus;
+import org.apache.shindig.gadgets.uri.PassthruManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.w3c.dom.Comment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.Text;
+
+import java.util.List;
+
+public class StyleTagExtractorVisitorTest extends DomWalkerTestBase {
+  private ProxyUriManager proxyUriManager;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    proxyUriManager = new PassthruManager();
+  }
+
+  @Test
+  public void visitBypassesComment() throws Exception {
+    Comment comment = doc.createComment("comment");
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(comment));
+  }
+
+  @Test
+  public void visitBypassesText() throws Exception {
+    Text text = doc.createTextNode("text");
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(text));
+  }
+
+  @Test
+  public void visitBypassesNonStyle() throws Exception {
+    Node node = elem("div");
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(node));
+  }
+
+  @Test
+  public void visitBypassesStyleWhenRewriterOff() throws Exception {
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(config(false, true, true), elem("style")));
+  }
+
+  @Test
+  public void visitBypassesStyleWhenStyleTagNotIncluded() throws Exception {
+    assertEquals(VisitStatus.BYPASS, getVisitStatus(config(true, false, true), elem("style")));
+  }
+
+  @Test
+  public void visitReservesStyleNode() throws Exception {
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatus(elem("style")));
+  }
+
+  @Test
+  public void visitReservesCasedStyleNode() throws Exception {
+    assertEquals(VisitStatus.RESERVE_NODE, getVisitStatus(elem("sTyLE")));
+  }
+
+  @Test
+  public void revisitNothingExtracted() throws Exception {
+    Gadget gadget = gadget();
+    CssResponseRewriter cssRewriter = createMock(CssResponseRewriter.class);
+    replay(cssRewriter);
+
+    // Tag name isn't inspected since visit() filters this.
+    List<Node> nodes = ImmutableList.of();
+    Node head = addNodesToHtml(nodes);
+
+    assertFalse(getRevisitStatus(gadget, true, cssRewriter, nodes));
+    verify(cssRewriter);
+    assertEquals(0, head.getChildNodes().getLength());
+  }
+
+  @Test
+  public void revisitExtractSpecRelative() throws Exception {
+    Uri base = GADGET_URI;
+    Gadget gadget = gadget();
+    CssResponseRewriter cssRewriter = createMock(CssResponseRewriter.class);
+    Element elem1 = elem("elem1");
+    Element elem2 = elem("elem2");
+    String urlStr1 = "http://foo.com/1.css";
+    List<String> extractedUrls1 = ImmutableList.of(urlStr1);
+    String urlStr2 = "http://bar.com/1.css";
+    List<String> extractedUrls2 = ImmutableList.of(urlStr2);
+    expect(cssRewriter.rewrite(eq(elem1), eq(base), isA(UriMaker.class), eq(true), eq(gadget.getContext())))
+        .andReturn(extractedUrls1).once();
+    expect(cssRewriter.rewrite(eq(elem2), eq(base), isA(UriMaker.class), eq(true), eq(gadget.getContext())))
+        .andReturn(extractedUrls2).once();
+    replay(cssRewriter);
+
+    // Tag name isn't inspected since visit() filters this.
+    List<Node> nodes = ImmutableList.<Node>of(elem1, elem2);
+    Node head = addNodesToHtml(nodes);
+
+    assertTrue(getRevisitStatus(gadget, true, cssRewriter, nodes));
+    verify(cssRewriter);
+    assertEquals(2, head.getChildNodes().getLength());
+    Element child1 = (Element)head.getChildNodes().item(0);
+    assertEquals("link", child1.getTagName());
+    assertEquals("stylesheet", child1.getAttribute("rel"));
+    assertEquals("text/css", child1.getAttribute("type"));
+    // PassthruManager doesn't modify the inbound URI.
+    assertEquals(urlStr1, child1.getAttribute("href"));
+    Element child2 = (Element)head.getChildNodes().item(1);
+    assertEquals("link", child2.getTagName());
+    assertEquals("stylesheet", child2.getAttribute("rel"));
+    assertEquals("text/css", child2.getAttribute("type"));
+    // PassthruManager doesn't modify the inbound URI.
+    assertEquals(urlStr2, child2.getAttribute("href"));
+  }
+
+  @Test
+  public void revisitExtractViewHrefRelative() throws Exception {
+    Uri base = Uri.parse("http://view.com/viewbase.xml");
+    Gadget gadget = gadget(true, true, base);
+    CssResponseRewriter cssRewriter = createMock(CssResponseRewriter.class);
+    Element elem1 = elem("elem1");
+    Element elem2 = elem("elem2");
+    String urlStr1 = "http://foo.com/1.css";
+    List<String> extractedUrls1 = ImmutableList.of(urlStr1);
+    String urlStr2 = "http://bar.com/1.css";
+    List<String> extractedUrls2 = ImmutableList.of(urlStr2);
+    expect(cssRewriter.rewrite(eq(elem1), eq(base), isA(UriMaker.class), eq(true), eq(gadget.getContext())))
+        .andReturn(extractedUrls1).once();
+    expect(cssRewriter.rewrite(eq(elem2), eq(base), isA(UriMaker.class), eq(true), eq(gadget.getContext())))
+        .andReturn(extractedUrls2).once();
+    replay(cssRewriter);
+
+    // Tag name isn't inspected since visit() filters this.
+    List<Node> nodes = ImmutableList.<Node>of(elem1, elem2);
+    Node head = addNodesToHtml(nodes);
+
+    assertTrue(getRevisitStatus(gadget, true, cssRewriter, nodes));
+    verify(cssRewriter);
+    assertEquals(2, head.getChildNodes().getLength());
+    Element child1 = (Element)head.getChildNodes().item(0);
+    assertEquals("link", child1.getTagName());
+    assertEquals("stylesheet", child1.getAttribute("rel"));
+    assertEquals("text/css", child1.getAttribute("type"));
+    // PassthruManager doesn't modify the inbound URI.
+    assertEquals(urlStr1, child1.getAttribute("href"));
+    Element child2 = (Element)head.getChildNodes().item(1);
+    assertEquals("link", child2.getTagName());
+    assertEquals("stylesheet", child2.getAttribute("rel"));
+    assertEquals("text/css", child2.getAttribute("type"));
+    // PassthruManager doesn't modify the inbound URI.
+    assertEquals(urlStr2, child2.getAttribute("href"));
+  }
+
+  @Test
+  public void revisitExtractSpecRelativeDisabled() throws Exception {
+    Uri base = GADGET_URI;
+    Gadget gadget = gadget();
+    CssResponseRewriter cssRewriter = createMock(CssResponseRewriter.class);
+    Element elem1 = elem("elem1");
+    Element elem2 = elem("elem2");
+    List<String> extractedUrls1 = ImmutableList.of();
+    List<String> extractedUrls2 = ImmutableList.of();
+    expect(cssRewriter.rewrite(eq(elem1), eq(base), isA(UriMaker.class), eq(true), eq(gadget.getContext())))
+        .andReturn(extractedUrls1).once();
+    expect(cssRewriter.rewrite(eq(elem2), eq(base), isA(UriMaker.class), eq(true), eq(gadget.getContext())))
+        .andReturn(extractedUrls2).once();
+    replay(cssRewriter);
+
+    // Tag name isn't inspected since visit() filters this.
+    List<Node> nodes = ImmutableList.<Node>of(elem1, elem2);
+    Node head = addNodesToHtml(nodes);
+
+    assertFalse(getRevisitStatus(gadget, false, cssRewriter, nodes));
+    verify(cssRewriter);
+    assertEquals(0, head.getChildNodes().getLength());
+  }
+
+  private VisitStatus getVisitStatus(Node node) throws Exception {
+    return getVisitStatus(config(true, true, true), node);
+  }
+
+  private VisitStatus getVisitStatus(ContentRewriterFeature.Config config, Node node)
+      throws Exception {
+    // Pass null for all unused (viz. visitor()) APIs to underscore their lack of use.
+    return new StyleTagExtractorVisitor(config, null, null).visit(null, node);
+  }
+
+  private boolean getRevisitStatus(
+      Gadget gadget, boolean shouldRewriteUrl, CssResponseRewriter cssRewriter, List<Node> nodes)
+      throws Exception {
+    return new StyleTagExtractorVisitor(
+        config(true, true, shouldRewriteUrl), cssRewriter, proxyUriManager)
+        .revisit(gadget, nodes);
+  }
+
+  private ContentRewriterFeature.Config config(
+      boolean enabled, boolean styleInc, boolean rewriteUrl) {
+    ContentRewriterFeature.Config config = createMock(ContentRewriterFeature.Config.class);
+    expect(config.isRewriteEnabled()).andReturn(enabled).anyTimes();
+    expect(config.getIncludedTags())
+        .andReturn(ImmutableSet.of(styleInc ? "style" : "foo")).anyTimes();
+    expect(config.shouldRewriteURL(isA(String.class))).andReturn(rewriteUrl).anyTimes();
+    replay(config);
+    return config;
+  }
+
+  private Node addNodesToHtml(List<Node> nodes) throws Exception {
+    Node html = elem("html");
+    Node head = elem("head");
+    Node body = elem("body");
+    html.appendChild(head);
+    html.appendChild(body);
+    for (Node node : nodes) {
+      body.appendChild(node);
+    }
+    html.getOwnerDocument().appendChild(html);
+    return head;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsVisitorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsVisitorTest.java
new file mode 100644
index 0000000..28eb68f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/StyleTagProxyEmbeddedUrlsVisitorTest.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.DefaultGuiceModule;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.admin.GadgetAdminModule;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.oauth.OAuthModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2MessageModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2Module;
+import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerModule;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2PersistenceModule;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlSerializer;
+import org.apache.shindig.gadgets.rewrite.ContentRewriterFeature.Config;
+import org.apache.shindig.gadgets.uri.DefaultProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+/**
+ * Tests for StyleTagProxyEmbeddedUrlsVisitor.
+ */
+public class StyleTagProxyEmbeddedUrlsVisitorTest extends DomWalkerTestBase {
+  protected static final String MOCK_CONTAINER = "mock";
+  private static final ImmutableMap<String, Object> MOCK_CONTAINER_CONFIG = ImmutableMap
+      .<String, Object>builder()
+      .put(ContainerConfig.CONTAINER_KEY, ImmutableList.of("mock"))
+      .put(DefaultProxyUriManager.PROXY_HOST_PARAM, "www.mock.com")
+      .build();
+
+  private Injector injector;
+  private CajaHtmlParser htmlParser;
+  private CajaHtmlSerializer serializer;
+  private ProxyUriManager proxyUriManager;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    injector = Guice.createInjector(
+        new PropertiesModule(), new GadgetAdminModule(), new DefaultGuiceModule(),
+        new OAuthModule(), new OAuth2Module(), new OAuth2PersistenceModule(), new OAuth2MessageModule(), new OAuth2HandlerModule());
+    ParseModule.DOMImplementationProvider domImpl =
+        new ParseModule.DOMImplementationProvider();
+    htmlParser = new CajaHtmlParser(domImpl.get());
+    serializer = new CajaHtmlSerializer();
+    ContainerConfig config = injector.getInstance(ContainerConfig.class);
+    config.newTransaction().addContainer(MOCK_CONTAINER_CONFIG).commit();
+    proxyUriManager = new DefaultProxyUriManager(config, null);
+  }
+
+  private static final String ORIGINAL = "<html><head>"
+      + "<style>"
+      + "@import url(/1.css);"
+      + "P {color:blue;}"
+      + "P {color:red;}"
+      + "A {background: url(/2.jpg);}"
+      + "</style>"
+      + "</head><body><a href=\"hello\">Hello</a>"
+      + "</body></html>";
+
+  private static final String NOREWRITE = "<html><head>"
+      + "<style>"
+      + "@import url('http://1.com/1.css');"
+      + "P {color:blue;}"
+      + "P {color:red;}"
+      + "A {background: url('http://1.com/2.jpg');}"
+      + "</style>"
+      + "</head><body><a href=\"hello\">Hello</a>"
+      + "</body></html>";
+
+  private static final String EXPECTED = "<html><head>"
+      + "<style>"
+      + "@import url('//localhost:8080/gadgets/proxy?container=default&"
+      + "gadget=http%3A%2F%2F1.com%2F&debug=0&nocache=0"
+      + "&url=http%3A%2F%2F1.com%2F1.css');\n"
+      + "P {color:blue;}"
+      + "P {color:red;}"
+      + "A {background: url('//localhost:8080/gadgets/proxy?container=default"
+      + "&gadget=http%3A%2F%2F1.com%2F&debug=0&nocache=0"
+      + "&url=http%3A%2F%2F1.com%2F2.jpg');}"
+      + "</style></head>"
+      + "<body><a href=\"hello\">Hello</a>\n"
+      + "</body></html>";
+
+  @Test
+  public void testImportsAndBackgroundUrlsInStyleTagDefaultContainer() throws Exception {
+    // TODO: IMPORTANT!  This test needs to not rely on the packaged shindig config, but rather
+    //       mock the config with expected values, so that tests do not fail when people set
+    //       alternative defaults.
+    Config config = injector.getInstance(ContentRewriterFeature.DefaultConfig.class);
+    EasyMock.replay();
+    if (config.isRewriteEnabled())
+      testImportsAndBackgroundUrlsInStyleTag(ORIGINAL, EXPECTED, ContainerConfig.DEFAULT_CONTAINER, config);
+    else
+      testImportsAndBackgroundUrlsInStyleTag(ORIGINAL, NOREWRITE, ContainerConfig.DEFAULT_CONTAINER, config);
+  }
+
+  @Test
+  public void testImportsAndBackgroundUrlsInStyleTagMockContainer() throws Exception {
+    // TODO: IMPORTANT!  This test needs to not rely on the packaged shindig config, but rather
+    //       mock the config with expected values, so that tests do not fail when people set
+    //       alternative defaults.
+    Config config = injector.getInstance(ContentRewriterFeature.DefaultConfig.class);
+    EasyMock.replay();
+
+    if (config.isRewriteEnabled()) {
+      testImportsAndBackgroundUrlsInStyleTag(ORIGINAL, EXPECTED.replace(
+          "localhost:8080/gadgets/proxy?container=default", "www.mock.com/gadgets/proxy?container=mock"),
+          MOCK_CONTAINER, config);
+    } else {
+      testImportsAndBackgroundUrlsInStyleTag(ORIGINAL, NOREWRITE, ContainerConfig.DEFAULT_CONTAINER, config);
+    }
+
+  }
+
+  private void testImportsAndBackgroundUrlsInStyleTag(String html, String expected, String container, Config config)
+      throws Exception {
+    // TODO: IMPORTANT!  This test needs to not rely on the packaged shindig config, but rather
+    //       mock the config with expected values, so that tests do not fail when people set
+    //       alternative defaults.
+    Document doc = htmlParser.parseDom(html);
+
+    StyleTagProxyEmbeddedUrlsVisitor visitor = new StyleTagProxyEmbeddedUrlsVisitor(
+        config, proxyUriManager,
+        injector.getInstance(CssResponseRewriter.class));
+
+    Gadget gadget = DomWalker.makeGadget(new HttpRequest(Uri.parse("http://1.com/")).setContainer(
+        container));
+
+    NodeList list = doc.getElementsByTagName("style");
+    visitor.revisit(gadget, ImmutableList.of(list.item(0)));
+    EasyMock.verify();
+
+    assertEquals(StringUtils.deleteWhitespace(expected),
+        StringUtils.deleteWhitespace(serializer.serialize(doc)));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java
new file mode 100644
index 0000000..7130ff3
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/TemplateRewriterTest.java
@@ -0,0 +1,396 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.render.FakeMessageBundleFactory;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.apache.shindig.gadgets.templates.ContainerTagLibraryFactory;
+import org.apache.shindig.gadgets.templates.DefaultTemplateProcessor;
+import org.apache.shindig.gadgets.templates.TemplateLibrary;
+import org.apache.shindig.gadgets.templates.TemplateLibraryFactory;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.gadgets.templates.XmlTemplateLibrary;
+import org.apache.shindig.gadgets.templates.tags.AbstractTagHandler;
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.inject.Provider;
+import org.json.JSONException;
+import org.json.JSONObject;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tests for TemplateRewriter
+ */
+public class TemplateRewriterTest {
+
+  private GadgetSpec gadgetSpec;
+  private Gadget gadget;
+  private MutableContent content;
+  private TemplateRewriter rewriter;
+  private final Map<String, Object> data = Maps.newHashMap();
+
+  private static final Uri GADGET_URI = Uri.parse("http://example.org/gadget.php");
+
+  private static final String CONTENT_PLAIN =
+    "<script type='text/os-template'>Hello, ${user.name}</script>";
+
+  private static final String CONTENT_WITH_MESSAGE =
+    "<script type='text/os-template'>Hello, ${Msg.name}</script>";
+
+  private static final String CONTENT_REQUIRE =
+    "<script type='text/os-template' require='user'>Hello, ${user.name}</script>";
+
+  private static final String CONTENT_REQUIRE_MISSING =
+    "<script type='text/os-template' require='foo'>Hello, ${user.name}</script>";
+
+  private static final String CONTENT_WITH_TAG =
+    "<script type='text/os-template' xmlns:foo='#foo' tag='foo:Bar'>Hello, ${user.name}</script>";
+
+  private static final String CONTENT_WITH_AUTO_UPDATE =
+    "<script type='text/os-template' autoUpdate='true'>Hello, ${user.name}</script>";
+
+  private static final String TEMPLATE_LIBRARY =
+    "<Templates xmlns:my='#my'>" +
+    "  <Namespace prefix='my' url='#my'/>" +
+    "  <JavaScript>script</JavaScript>" +
+    "  <Style>style</Style>" +
+    "  <Template tag='my:Tag1'>external1</Template>" +
+    "  <Template tag='my:Tag2'>external2</Template>" +
+    "  <Template tag='my:Tag3'>external3</Template>" +
+    "  <Template tag='my:Tag4'>external4</Template>" +
+    "</Templates>";
+
+  private static final String TEMPLATE_LIBRARY_URI = "http://example.org/library.xml";
+  private static final String CONTENT_WITH_TAG_FROM_LIBRARY =
+    "<script type='text/os-template' xmlns:my='#my'><my:Tag4/></script>";
+
+  private static final String CONTENT_TESTING_PRECEDENCE_RULES =
+    "<script type='text/os-template' xmlns:my='#my' tag='my:Tag1'>inline1</script>" +
+    "<script type='text/os-template' xmlns:my='#my' tag='my:Tag2'>inline2</script>" +
+    "<script type='text/os-template' xmlns:my='#my' tag='my:Tag3'>inline3</script>" +
+    "<script type='text/os-template' xmlns:my='#my'><my:Tag1/><my:Tag2/><my:Tag3/><my:Tag4/></script>";
+
+  @Before
+  public void setUp() {
+    Set<TagHandler> handlers = ImmutableSet.of(testTagHandler("Tag1", "default1"));
+    rewriter = new TemplateRewriter(
+        new Provider<TemplateProcessor>() {
+          public TemplateProcessor get() {
+            return new DefaultTemplateProcessor(Expressions.forTesting());
+          }
+        },
+        new FakeMessageBundleFactory(),
+        Expressions.forTesting(),
+        new DefaultTagRegistry(handlers),
+        new FakeTemplateLibraryFactory(),
+        new ContainerTagLibraryFactory(new FakeContainerConfig()));
+  }
+
+ private static TagHandler testTagHandler(String name, final String content) {
+   return new AbstractTagHandler("#my", name) {
+    public void process(Node result, Element tag, TemplateProcessor processor) {
+      result.appendChild(result.getOwnerDocument().createTextNode(content));
+    }
+   };
+ }
+
+  @Test
+  public void simpleTemplate() throws Exception {
+    // Render a simple template
+    testExpectingTransform(getGadgetXml(CONTENT_PLAIN), "simple");
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void noTemplateFeature() throws Exception {
+    // Without opensocial-templates feature, shouldn't render
+    testExpectingNoTransform(getGadgetXml(CONTENT_PLAIN, false), "no feature");
+  }
+
+  @Test
+  public void requiredDataPresent() throws Exception {
+    // Required data is present - render
+    testExpectingTransform(getGadgetXml(CONTENT_REQUIRE), "required data");
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void requiredDataMissing() throws Exception {
+    // Required data is missing - don't render
+    testExpectingNoTransform(getGadgetXml(CONTENT_REQUIRE_MISSING), "missing data");
+    testFeatureNotRemoved();
+  }
+
+  @Test
+  public void tagAttributePresent() throws Exception {
+    // Don't render templates with a @tag
+    testExpectingNoTransform(getGadgetXml(CONTENT_WITH_TAG), "with @tag");
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void templateUsingMessage() throws Exception {
+    // Render a simple template
+    testExpectingTransform(getGadgetXml(CONTENT_WITH_MESSAGE), "simple");
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void autoUpdateTemplate() throws Exception {
+    setupGadget(getGadgetXml(CONTENT_WITH_AUTO_UPDATE));
+    rewriter.rewrite(gadget, content);
+    // The template should get transformed, but not removed
+    assertTrue("Template wasn't transformed",
+        content.getContent().indexOf("Hello, John") > 0);
+    assertTrue("Template tag was removed",
+        content.getContent().contains("text/os-template"));
+    assertTrue("ID span was not created",
+        content.getContent().contains("<span id=\"_T_template_auto0\">"));
+    testFeatureNotRemoved();
+  }
+
+  @Test
+  public void templateWithLibrary() throws Exception {
+    setupGadget(getGadgetXmlWithLibrary(CONTENT_WITH_TAG_FROM_LIBRARY));
+    rewriter.rewrite(gadget, content);
+    assertTrue("Script not inserted", content.getContent().indexOf(
+        "<script type=\"text/javascript\">script</script>") > 0);
+    assertTrue("Style not inserted", content.getContent().indexOf(
+        "<style type=\"text/css\">style</style>") > 0);
+    assertTrue("Tag not executed", content.getContent().indexOf(
+        "external4") > 0);
+
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void osmlWithLibrary() throws Exception {
+    setupGadget(getGadgetXmlWithLibrary(CONTENT_WITH_TAG_FROM_LIBRARY, "osml"));
+    rewriter.rewrite(gadget, content);
+    assertTrue("Custom tags were evaluated", content.getContent().equals(
+        "<html><head></head><body><my:Tag4></my:Tag4></body></html>"));
+
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void tagPrecedenceRules() throws Exception {
+    // Tag definitions include:
+    // Default handlers: tag1 default1
+    // OSML: tag1 osml1 tag2 osml2
+    // inline tags: tag1 inline1 tag2 inline2 tag3 inline3
+    // External tags: tag1 external1 tag2 external2 tag3 external3 tag4 external4
+
+    data.put("${Cur['gadgets.features'].osml.library}",
+        "org/apache/shindig/gadgets/rewrite/OSML_test.xml");
+
+    setupGadget(getGadgetXmlWithLibrary(CONTENT_TESTING_PRECEDENCE_RULES));
+    rewriter.rewrite(gadget, content);
+    assertTrue("Precedence rules violated",
+        content.getContent().indexOf("default1osml2inline3external4") > 0);
+
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void tagPrecedenceRulesWithOSMLFeature() throws Exception {
+    // A strict subset of os templating is enabled when the osml feature is required
+    // Tag definitions include:
+    // Default handlers: tag1 default1
+    // OSML: tag1 osml1 tag2 osml2
+
+    data.put("${Cur['gadgets.features'].osml.library}",
+        "org/apache/shindig/gadgets/rewrite/OSML_test.xml");
+
+    setupGadget(getGadgetXmlWithLibrary(CONTENT_TESTING_PRECEDENCE_RULES, "osml"));
+    rewriter.rewrite(gadget, content);
+    assertTrue("Precedence rules violated", content.getContent().indexOf(
+        "default1osml2<my:Tag3></my:Tag3><my:Tag4></my:Tag4>") > 0);
+
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void tagPrecedenceRulesWithoutOSML() throws Exception {
+    // Tag definitions include:
+    // Default handlers: tag1 default1
+    // OSML: tag1 osml1 tag2 osml2
+    // inline tags: tag1 inline1 tag2 inline2 tag3 inline3
+    // External tags: tag1 external1 tag2 external2 tag3 external3 tag4 external4
+
+    // Explicitly don't support OSML
+    data.put("${Cur['gadgets.features'].osml.library}", "");
+
+    setupGadget(getGadgetXmlWithLibrary(CONTENT_TESTING_PRECEDENCE_RULES));
+    rewriter.rewrite(gadget, content);
+    assertTrue("Precedence rules violated",
+        content.getContent().indexOf("default1inline2inline3external4") > 0);
+
+    testFeatureRemoved();
+  }
+
+  @Test
+  public void testClientOverride() throws Exception {
+    // Should normally remove feature
+    testExpectingTransform(getGadgetXml(CONTENT_PLAIN, true, "true"), "keep client");
+    testFeatureNotRemoved();
+
+    // Should normally keep feature
+    testExpectingNoTransform(getGadgetXml(CONTENT_WITH_TAG, true, "false"), "remove client");
+    testFeatureRemoved();
+  }
+
+  private void testFeatureRemoved() {
+    assertFalse("Feature wasn't removed",
+        gadget.getDirectFeatureDeps().contains("opensocial-templates"));
+  }
+
+  private void testFeatureNotRemoved() {
+    assertTrue("Feature was removed",
+        gadget.getDirectFeatureDeps().contains("opensocial-templates"));
+  }
+
+  private void testExpectingTransform(String code, String condition) throws Exception {
+    setupGadget(code);
+    rewriter.rewrite(gadget, content);
+    assertTrue("Template wasn't transformed (" + condition + ')',
+        content.getContent().indexOf("Hello, John") > 0);
+    assertTrue("Template tag wasn't removed (" + condition + ')',
+        !content.getContent().contains("text/os-template"));
+  }
+
+  private void testExpectingNoTransform(String code, String condition) throws Exception {
+    setupGadget(code);
+    rewriter.rewrite(gadget, content);
+    assertTrue("Template was transformed (" + condition + ')',
+        content.getContent().indexOf("${user.name}") > 0);
+    assertTrue("Template tag was removed (" + condition + ')',
+        content.getContent().indexOf("text/os-template") > 0);
+  }
+
+  private void setupGadget(String gadgetXml) throws SpecParserException, JSONException {
+    gadgetSpec = new GadgetSpec(GADGET_URI, gadgetXml);
+    gadget = new Gadget();
+    gadget.setSpec(gadgetSpec);
+    gadget.setContext(new GadgetContext() {
+
+      @Override
+      public Uri getUrl() {
+        return GADGET_URI;
+      }
+    });
+    gadget.setCurrentView(gadgetSpec.getView("default"));
+
+    content = new MutableContent(new NekoSimplifiedHtmlParser(
+        new ParseModule.DOMImplementationProvider().get()), gadget.getCurrentView().getContent());
+    putPipelinedData("user", new JSONObject("{ name: 'John'}"));
+  }
+
+  private void putPipelinedData(String key, JSONObject data) {
+    content.addPipelinedData(key, data);
+  }
+
+  private static String getGadgetXml(String content) {
+    return getGadgetXml(content, true);
+  }
+
+  private static String getGadgetXml(String content, boolean requireFeature) {
+    return getGadgetXml(content, requireFeature, null);
+  }
+
+  private static String getGadgetXml(String content, boolean requireFeature,
+      String clientParam) {
+    String feature = requireFeature ?
+        "<Require feature='opensocial-templates'" +
+        (clientParam != null ?
+            ("><Param name='client'>" + clientParam + "</Param></Require>")
+            : "/>")
+        : "";
+    return "<Module>" + "<ModulePrefs title='Title'>"
+        + feature
+        + "  <Locale>"
+        + "    <msg name='name'>John</msg>"
+        + "  </Locale>"
+        + "</ModulePrefs>"
+        + "<Content>"
+        + "    <![CDATA[" + content + "]]>"
+        + "</Content></Module>";
+  }
+
+  private static String getGadgetXmlWithLibrary(String content) {
+    return getGadgetXmlWithLibrary(content, "opensocial-templates");
+  }
+
+  private static String getGadgetXmlWithLibrary(String content, String feature) {
+    return "<Module>" + "<ModulePrefs title='Title'>"
+        + "  <Require feature='" + feature + "'>"
+        + "    <Param name='" + TemplateRewriter.REQUIRE_LIBRARY_PARAM + "'>"
+        + TEMPLATE_LIBRARY_URI
+        + "    </Param>"
+        + "  </Require>"
+        + "</ModulePrefs>"
+        + "<Content>"
+        + "    <![CDATA[" + content + "]]>"
+        + "</Content></Module>";
+  }
+
+  private static class FakeTemplateLibraryFactory extends TemplateLibraryFactory {
+    public FakeTemplateLibraryFactory() {
+      super(null, null);
+    }
+
+    @Override
+    public TemplateLibrary loadTemplateLibrary(GadgetContext context, Uri uri)
+        throws GadgetException {
+      assertEquals(TEMPLATE_LIBRARY_URI, uri.toString());
+      return new XmlTemplateLibrary(uri, XmlUtil.parseSilent(TEMPLATE_LIBRARY),
+          TEMPLATE_LIBRARY);
+    }
+  }
+
+  private class FakeContainerConfig extends BasicContainerConfig {
+    @Override
+    public Object getProperty(String container, String name) {
+      return data.get(name);
+    }
+
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/XPathWrapper.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/XPathWrapper.java
new file mode 100644
index 0000000..dd79245
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/XPathWrapper.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+
+/**
+ * Utility functions to test documents via XPath
+ */
+public class XPathWrapper {
+  private static final XPathFactory FACTORY = XPathFactory.newInstance();
+  private final Document doc;
+
+  public XPathWrapper(Document doc) {
+    this.doc = doc;
+  }
+
+  public String getValue(String pathExpr) throws Exception {
+    XPath xPath = FACTORY.newXPath();
+    return xPath.evaluate(pathExpr, doc);
+  }
+
+  public Node getNode(String pathExpr) throws Exception {
+    XPath xPath = FACTORY.newXPath();
+    return (Node)xPath.evaluate(pathExpr, doc, XPathConstants.NODE);
+  }
+
+  public NodeList getNodeList(String pathExpr) throws Exception {
+    XPath xPath = FACTORY.newXPath();
+    return (NodeList)xPath.evaluate(pathExpr, doc, XPathConstants.NODESET);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/BMPOptimizerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/BMPOptimizerTest.java
new file mode 100644
index 0000000..f7b51e8
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/BMPOptimizerTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Test BMPOptimizer
+ */
+public class BMPOptimizerTest extends BaseOptimizerTest {
+
+  static final Logger log = Logger.getLogger(BMPOptimizerTest.class.getName());
+
+  @Test
+  public void testSimpleImage() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/simple.bmp", "image/bmp");
+    HttpResponse rewritten = rewrite(resp);
+    assertTrue(rewritten.getContentLength() < resp.getContentLength());
+    assertEquals("image/png", rewritten.getHeader("Content-Type"));
+  }
+
+  @Test
+  @Ignore("Kills some VMs")
+  public void testEvilImages() throws Exception {
+    try {
+      HttpResponse resp =
+          createResponse("org/apache/shindig/gadgets/rewrite/image/evil.bmp", "image/bmp");
+      rewrite(resp);
+      fail("Evil image should not be readable");
+    } catch (RuntimeException re) {
+      log.log(Level.INFO, "Good failure while reading evil image", re);
+    }
+  }
+
+  @Test
+  public void testEvilImage2() throws Exception {
+    try {
+      HttpResponse resp =
+          createResponse("org/apache/shindig/gadgets/rewrite/image/evil2.bmp", "image/bmp");
+      rewrite(resp);
+      fail("Evil image should not be readable");
+    } catch (RuntimeException re) {
+      log.log(Level.INFO, "Good failure while reading evil image", re);
+    }
+  }
+
+  protected HttpResponse rewrite(HttpResponse original)
+       throws IOException, ImageReadException {
+    HttpResponseBuilder builder = new HttpResponseBuilder(original);
+    new BMPOptimizer(new OptimizerConfig(), builder).rewrite(
+         Sanselan.getBufferedImage(original.getResponse()));
+    return builder.create();
+   }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizerTest.java
new file mode 100644
index 0000000..663e0fa
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/BaseOptimizerTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.junit.Assert;
+
+import java.io.IOException;
+
+/**
+ * Test BasicOptimizer
+ */
+public abstract class BaseOptimizerTest extends Assert {
+
+  protected HttpResponse createResponse(String resource, String mimeType) throws IOException {
+    byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream(resource));
+    return new HttpResponseBuilder().addHeader("Content-Type", mimeType)
+            .setResponse(bytes).create();
+  }
+
+  protected HttpResponseBuilder createResponseBuilder(String resource, String mimeType)
+      throws IOException {
+    byte[] bytes = IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream(resource));
+    return new HttpResponseBuilder().addHeader("Content-Type", mimeType)
+        .setResponse(bytes);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/GIFOptimizerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/GIFOptimizerTest.java
new file mode 100644
index 0000000..036a152
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/GIFOptimizerTest.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Tests for GIFOptimizer
+ */
+public class GIFOptimizerTest extends BaseOptimizerTest {
+
+  @Test
+  public void testEfficientGIF() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/unanimated.gif", "image/gif");
+    HttpResponse httpResponse = rewrite(resp);
+    assertSame(resp, httpResponse);
+  }
+
+  /**
+   * This is a GIF image with an palette that contains transparent entries but
+   * that has not pixels that map to them so it is Opaque.
+   * @throws Exception
+   */
+  @Test
+  public void testBadPaletteGIFToPNG() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/large.gif", "image/gif");
+    HttpResponse httpResponse = rewrite(resp);
+    assertTrue(httpResponse.getContentLength() <= resp.getContentLength());
+    assertEquals("image/png", httpResponse.getHeader("Content-Type"));
+  }
+
+  /**
+   * This is a GIF image with has a direct color model instead of an indexed one and has
+   * tranparency
+   * @throws Exception
+   */
+  @Test
+  public void testDirectColorModelGif() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/directcolor.gif", "image/gif");
+    HttpResponse httpResponse = rewrite(resp);
+    assertTrue(httpResponse.getContentLength() <= resp.getContentLength());
+    assertEquals("image/gif", httpResponse.getHeader("Content-Type"));
+  }
+
+  protected HttpResponse rewrite(HttpResponse original)
+      throws IOException, ImageReadException {
+    HttpResponseBuilder builder = new HttpResponseBuilder(original);
+    new GIFOptimizer(new OptimizerConfig(), builder).rewrite(
+         Sanselan.getBufferedImage(original.getResponse()));
+    return builder.create();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/ImageRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/ImageRewriterTest.java
new file mode 100644
index 0000000..4562e4e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/ImageRewriterTest.java
@@ -0,0 +1,306 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import static
+    org.apache.shindig.gadgets.rewrite.image.BasicImageRewriter.CONTENT_TYPE_AND_EXTENSION_MISMATCH;
+import static
+    org.apache.shindig.gadgets.rewrite.image.BasicImageRewriter.CONTENT_TYPE_AND_MIME_MISMATCH;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.createControl;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.easymock.IMocksControl;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.imageio.ImageIO;
+
+/**
+ * Tests for the {@link ImageRewriter} class.
+ */
+public class ImageRewriterTest extends Assert {
+
+  /** The image used for the scaling tests, a head-shot of a white dog, 500pix x 500pix */
+  private static final String SCALE_IMAGE = "org/apache/shindig/gadgets/rewrite/image/dog.gif";
+
+  /** A 60 x 30 image whose size in bytes expands when resized to 120 x 60 */
+  private static final String EXPAND_IMAGE = "org/apache/shindig/gadgets/rewrite/image/expand.gif";
+
+  /** A 600 x 400 image whose size used to cause trouble with rounding when resizing to 171 x 171 */
+  private static final String RATIO_IMAGE = "org/apache/shindig/gadgets/rewrite/image/ratio.gif";
+
+  /**
+   * This image has a huge memory footprint that the rewriter should refuse to resize, but not
+   * refuse to render.  The response containing this image should not be rewritten.
+   */
+  private static final String HUGE_IMAGE = "org/apache/shindig/gadgets/rewrite/image/huge.gif";
+
+  private static final String CONTENT_TYPE_BOGUS = "notimage/anything";
+  private static final String CONTENT_TYPE_JPG = "image/jpeg";
+  private static final String CONTENT_TYPE_GIF = "image/gif";
+  private static final String CONTENT_TYPE_PNG = "image/png";
+  private static final String CONTENT_TYPE_HEADER = "Content-Type";
+
+  private static final Uri IMAGE_URL = Uri.parse("http://www.example.com/image.gif");
+
+  private ResponseRewriter rewriter;
+  private IMocksControl mockControl;
+
+  @Before
+  public void setUp() throws Exception {
+    rewriter = new BasicImageRewriter(new OptimizerConfig());
+    mockControl = createControl();
+  }
+
+  /** Makes a new {@link HttpResponse} with an image content. */
+  private HttpResponseBuilder getImageResponse(String contentType, byte[] imageBytes) {
+    return new HttpResponseBuilder()
+        .setHeader(CONTENT_TYPE_HEADER, contentType)
+        .setHttpStatusCode(HttpResponse.SC_OK)
+        .setResponse(imageBytes);
+  }
+
+  /** Extracts an image by its resource name and converts it into a byte array. */
+  private byte[] getImageBytes(String imageResourceName) throws IOException {
+    ClassLoader classLoader = getClass().getClassLoader();
+    byte[] imageBytes = IOUtils.toByteArray(classLoader.getResourceAsStream(imageResourceName));
+    assertNotNull(imageBytes);
+    return imageBytes;
+  }
+
+  private BufferedImage getResizedHttpResponseContent(String sourceContentType,
+      String targetContentType, String imageName, Integer width, Integer height, Integer quality)
+      throws Exception {
+    return getResizedHttpResponseContent(
+        sourceContentType, targetContentType, imageName, width, height, quality, false);
+  }
+
+  private BufferedImage getResizedHttpResponseContent(String sourceContentType,
+      String targetContentType, String imageName, Integer width, Integer height, Integer quality,
+      boolean noExpand)
+      throws Exception {
+    HttpResponseBuilder response = getImageResponse(sourceContentType, getImageBytes(imageName));
+    HttpRequest request = getMockRequest(width, height, quality, noExpand);
+
+    mockControl.replay();
+    rewriter.rewrite(request, response, null);
+    mockControl.verify();
+
+    assertEquals(targetContentType, response.getHeader(CONTENT_TYPE_HEADER));
+    return ImageIO.read(response.getContentBytes());
+  }
+
+  private HttpRequest getMockRequest(Integer width, Integer height, Integer quality, boolean noExpand) {
+    HttpRequest request = mockControl.createMock(HttpRequest.class);
+    expect(request.getUri()).andReturn(IMAGE_URL).anyTimes();
+    expect(request.getParamAsInteger(Param.RESIZE_QUALITY.getKey())).andReturn(quality).anyTimes();
+    expect(request.getParamAsInteger(Param.RESIZE_WIDTH.getKey())).andReturn(width).anyTimes();
+    expect(request.getParamAsInteger(Param.RESIZE_HEIGHT.getKey())).andReturn(height).anyTimes();
+    expect(request.getParam(Param.NO_EXPAND.getKey())).andReturn(noExpand ? "1" : null).anyTimes();
+    return request;
+  }
+
+  @Test
+  public void testRewriteValidImageWithValidMimeAndExtn() throws Exception {
+    byte[] bytes = getImageBytes("org/apache/shindig/gadgets/rewrite/image/inefficient.png");
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_PNG, bytes);
+    int originalContentLength = response.getContentLength();
+
+    rewriter.rewrite(new HttpRequest(Uri.parse("some.png")), response, null);
+    assertEquals(HttpResponse.SC_OK, response.getHttpStatusCode());
+    assertTrue(response.getContentLength() < originalContentLength);
+  }
+
+  @Test
+  public void testRewriteValidImageWithInvalidMimeAndFileExtn() throws Exception {
+    byte[] bytes = getImageBytes("org/apache/shindig/gadgets/rewrite/image/inefficient.png");
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_BOGUS, bytes);
+    int originalContentLength = response.getContentLength();
+
+    rewriter.rewrite(new HttpRequest(Uri.parse("some.junk")), response, null);
+    assertEquals(HttpResponse.SC_OK, response.getHttpStatusCode());
+    assertEquals(response.getContentLength(), originalContentLength);
+  }
+
+  @Test
+  public void testRewriteInvalidImageContentWithValidMime() throws Exception {
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_PNG, "This is not a PNG".getBytes());
+    rewriter.rewrite(new HttpRequest(Uri.parse("some.junk")), response, null);
+
+    assertEquals(HttpResponse.SC_UNSUPPORTED_MEDIA_TYPE, response.getHttpStatusCode());
+    assertEquals(CONTENT_TYPE_AND_MIME_MISMATCH, response.create().getResponseAsString());
+  }
+
+  @Test
+  public void testRewriteInvalidImageContentWithValidFileExtn() throws Exception {
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_BOGUS, "This is not an image".getBytes());
+    rewriter.rewrite(new HttpRequest(Uri.parse("some.png")), response, null);
+
+    assertEquals(HttpResponse.SC_UNSUPPORTED_MEDIA_TYPE, response.getHttpStatusCode());
+    assertEquals(CONTENT_TYPE_AND_EXTENSION_MISMATCH,
+        response.create().getResponseAsString());
+  }
+
+  @Test
+  public void testNoRewriteAnimatedGIF() throws Exception {
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_GIF,
+        getImageBytes("org/apache/shindig/gadgets/rewrite/image/animated.gif"));
+    int changesBefore = response.getNumChanges();
+    rewriter.rewrite(new HttpRequest(Uri.parse("animated.gif")), response, null);
+    assertEquals(changesBefore, response.getNumChanges());
+  }
+
+  @Test
+  public void testRewriteUnAnimatedGIF() throws Exception {
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_GIF,
+        getImageBytes("org/apache/shindig/gadgets/rewrite/image/large.gif"));
+    rewriter.rewrite(new HttpRequest(Uri.parse("large.gif")), response, null);
+    assertEquals(CONTENT_TYPE_PNG, response.getHeader(CONTENT_TYPE_HEADER));
+  }
+
+  // Resizing image tests
+  //
+  // Checks at least the basic image parameters.  It is rather nontrivial to check for the actual
+  // image content, as the ImageIO implementations vary across JVMs, so we have to skip it.
+
+  @Test
+  public void testResize_width() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(CONTENT_TYPE_GIF, CONTENT_TYPE_JPG,
+        SCALE_IMAGE, 100 /* width */, null /* height */, null /* quality */);
+    assertEquals(100, image.getWidth());
+    assertEquals(100, image.getHeight());
+  }
+
+  @Test
+  public void testResize_height() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, SCALE_IMAGE,  null, 100, null);
+    assertEquals(100, image.getWidth());
+    assertEquals(100, image.getHeight());
+  }
+
+  @Test
+  public void testResize_both() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, SCALE_IMAGE, 100, 100, null);
+    assertEquals(100, image.getWidth());
+    assertEquals(100, image.getHeight());
+  }
+
+  @Test
+  public void testResize_all() throws Exception {
+    // The quality hint apparently has no effect on the result here
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, SCALE_IMAGE, 100, 100, 10);
+    assertEquals(100, image.getWidth());
+    assertEquals(100, image.getHeight());
+  }
+
+  @Test
+  public void testResize_wideImage() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, SCALE_IMAGE, 100, 50, null);
+    assertEquals(100, image.getWidth());
+    assertEquals(50, image.getHeight());
+  }
+
+  @Test
+  public void testResize_tallImage() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, SCALE_IMAGE,  50, 100, null);
+    assertEquals(50, image.getWidth());
+    assertEquals(100, image.getHeight());
+  }
+
+  @Test
+  public void testResize_skipResizeHugeOutputImage() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, SCALE_IMAGE, 10000, 10000, null);
+    assertEquals(500, image.getWidth());
+    assertEquals(500, image.getHeight());
+  }
+
+  @Test
+  public void testResize_brokenParameter() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_GIF, SCALE_IMAGE, -1, null, null);
+    assertEquals(500, image.getWidth());
+    assertEquals(500, image.getHeight());
+  }
+
+  @Test
+  public void testResize_expandImage() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_JPG, EXPAND_IMAGE, 120, 60, null);
+    assertEquals(120, image.getWidth());
+    assertEquals(60, image.getHeight());
+  }
+
+  @Test
+  public void testResize_noExpandImage() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_PNG /* still optimized */,
+        EXPAND_IMAGE, 120, 60, null, true /* no expand */);
+    assertEquals(60, image.getWidth());
+    assertEquals(30, image.getHeight());
+  }
+
+  @Test
+  public void testResize_refuseHugeInputImages() throws Exception {
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_GIF, getImageBytes(HUGE_IMAGE));
+    HttpRequest request = getMockRequest(120, 60, null, false);
+    mockControl.replay();
+    rewriter.rewrite(request, response, null);
+    mockControl.verify();
+    assertEquals(HttpResponse.SC_FORBIDDEN, response.getHttpStatusCode());
+  }
+
+  @Test
+  public void testResize_acceptServeHugeImages() throws Exception {
+    byte[] imageBytes = getImageBytes(HUGE_IMAGE);
+    HttpResponseBuilder response = getImageResponse(CONTENT_TYPE_GIF, imageBytes);
+    HttpRequest request = getMockRequest(null, null, null, false);
+    mockControl.replay();
+    rewriter.rewrite(request, response, null);
+    mockControl.verify();
+    assertEquals(HttpResponse.SC_OK, response.getHttpStatusCode());
+    assertTrue(Arrays.equals(imageBytes, IOUtils.toByteArray(response.getContentBytes())));
+  }
+
+  @Test
+  public void testResize_avoidFloatingPointRoundingProblems() throws Exception {
+    BufferedImage image = getResizedHttpResponseContent(
+        CONTENT_TYPE_GIF, CONTENT_TYPE_PNG, RATIO_IMAGE, 171, 171, null, true);
+    assertEquals(171, image.getWidth());
+    assertEquals(114, image.getHeight());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java
new file mode 100644
index 0000000..bc608df
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JPEGOptimizerTest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Test JPEG rewiting.
+ */
+public class JPEGOptimizerTest extends BaseOptimizerTest {
+  @Test
+  public void testSmallJPEGToPNG() throws Exception {
+    // Should be significantly smaller
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/small.jpg", "image/jpeg");
+    HttpResponse rewritten = rewrite(resp);
+    assertEquals("image/png", rewritten.getHeader("Content-Type"));
+    assertTrue(rewritten.getContentLength() * 100 / resp.getContentLength() < 70);
+  }
+
+  @Test
+  public void testSmallJPEGIsNotConvertedToPNGWithJpegConversionDisabled() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/small.jpg", "image/jpeg");
+    HttpResponse rewritten = rewrite(resp, getDefaultConfigWithJpegConversionDisabled());
+    assertEquals("image/jpeg", rewritten.getHeader("Content-Type"));
+    assertTrue(rewritten.getContentLength() <= resp.getContentLength());
+  }
+
+  @Test
+  public void testRetainSubsmaplingDisabled() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/testImage444.jpg", "image/jpeg");
+    HttpResponse rewritten = rewrite(resp, getConfigWithRetainSampling(false, 0.70f));
+    JpegImageUtils.JpegImageParams params =
+        JpegImageUtils.getJpegImageData(rewritten.getResponse(), "");
+    assertTrue(rewritten.getContentLength() < resp.getContentLength());
+    assertEquals(JpegImageUtils.SamplingModes.YUV420, params.getSamplingMode());
+  }
+
+  @Test
+  public void testRetainSubsmaplingEnabled() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/testImage444.jpg", "image/jpeg");
+    HttpResponse rewritten = rewrite(resp, getConfigWithRetainSampling(true, 0.70f));
+    JpegImageUtils.JpegImageParams params =
+        JpegImageUtils.getJpegImageData(rewritten.getResponse(), "");
+    assertTrue(rewritten.getContentLength() < resp.getContentLength());
+    assertEquals(JpegImageUtils.SamplingModes.YUV444, params.getSamplingMode());
+  }
+
+  @Test
+  public void testLargeJPEG() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/large.jpg", "image/jpeg");
+    HttpResponse rewritten = rewrite(resp);
+    assertEquals("image/jpeg", resp.getHeader("Content-Type"));
+    assertTrue(rewritten.getContentLength() <= resp.getContentLength());
+  }
+
+  @Test
+  public void testLargeJPEGWithEtagAndCacheHeaders() throws Exception {
+    HttpResponseBuilder responseBuilder =
+        createResponseBuilder("org/apache/shindig/gadgets/rewrite/image/large.jpg", "image/jpeg");
+    responseBuilder.addHeader("ETag", "wereertret");
+    responseBuilder.addHeader("Cache-Control", "public, max-age=86400");
+    HttpResponse resp = responseBuilder.create();
+    HttpResponse rewritten = rewrite(resp, getConfigWithRetainSampling(false, 0.70f));
+    assertEquals("image/jpeg", resp.getHeader("Content-Type"));
+    assertEquals("public, max-age=86400", resp.getHeader("Cache-Control"));
+    assertNull(rewritten.getHeader("ETag"));
+    assertTrue(rewritten.getContentLength() < resp.getContentLength());
+  }
+
+  @Test(expected=Throwable.class)
+  public void testBadImage() throws Exception {
+    // Not a JPEG
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/bad.jpg", "image/jpeg");
+    rewrite(resp);
+  }
+
+  @Test(expected=Throwable.class)
+  public void xtestBadICC1() throws Exception {
+    // ICC section too long
+    HttpResponse resp = createResponse("org/apache/shindig/gadgets/rewrite/image/badicc.jpg", "image/jpeg");
+    rewrite(resp);
+  }
+
+  @Test(expected=Throwable.class)
+  public void testBadICC2() throws Exception {
+    // ICC section too long
+    HttpResponse resp = createResponse("org/apache/shindig/gadgets/rewrite/image/badicc2.jpg", "image/jpeg");
+    rewrite(resp);
+  }
+
+  @Test(expected=Throwable.class)
+  public void testBadICC3() throws Exception {
+    // ICC length lies
+    HttpResponse resp = createResponse("org/apache/shindig/gadgets/rewrite/image/badicc3.jpg", "image/jpeg");
+    rewrite(resp);
+  }
+
+  @Test
+  public void testBadICC4() throws Exception {
+    // ICC count too large
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/badicc4.jpg", "image/jpeg");
+    try {
+      rewrite(resp);
+      fail("Should have failed with OutOfMemory exception");
+    } catch (OutOfMemoryError oome) {
+      // Currently we expect an OutOfMemory error. Working on this with Sanselan
+    } catch (NullPointerException npe) {
+      // For IBM JVM, NPE is thrown, bug: SANSELAN-23
+    }
+  }
+
+  @Test
+  public void testBadICC5() throws Exception {
+    // ICC length too large. Should be readable by most VMs
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/2ndjpgbad.jpg", "image/jpeg");
+    rewrite(resp);
+  }
+
+  HttpResponse rewrite(HttpResponse original) throws Exception {
+    return rewrite(original, new OptimizerConfig());
+  }
+
+  HttpResponse rewrite(HttpResponse original, OptimizerConfig config)
+      throws IOException, ImageReadException {
+    HttpResponseBuilder builder = new HttpResponseBuilder(original);
+    new JPEGOptimizer(config, builder,
+                      JpegImageUtils.getJpegImageData(builder.getContentBytes(), ""))
+        .rewrite(JPEGOptimizer.readJpeg(original.getResponse()));
+    return builder.create();
+  }
+
+  OptimizerConfig getDefaultConfigWithJpegConversionDisabled() {
+    OptimizerConfig defaultConfig = new OptimizerConfig();
+    return new OptimizerConfig(defaultConfig.getMaxInMemoryBytes(),
+    defaultConfig.getMaxPaletteSize(), false,
+    defaultConfig.getJpegCompression(), defaultConfig.getMinThresholdBytes(),
+    defaultConfig.getJpegHuffmanOptimization(), defaultConfig.getJpegRetainSubsampling());
+  }
+
+  OptimizerConfig getConfigWithRetainSampling(boolean enabled, float quality) {
+    OptimizerConfig defaultConfig = new OptimizerConfig();
+    return new OptimizerConfig(defaultConfig.getMaxInMemoryBytes(),
+       defaultConfig.getMaxPaletteSize(), defaultConfig.isJpegConversionAllowed(),
+       quality, defaultConfig.getMinThresholdBytes(), defaultConfig.getJpegHuffmanOptimization(),
+       enabled);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java
new file mode 100644
index 0000000..e416230
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/JpegImageUtilsTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.Assert;
+
+import java.io.*;
+
+/**
+ * Tests for {@code JpegImageUtils}
+ */
+public class JpegImageUtilsTest extends Assert {
+  @Before
+  public void setUp() throws Exception {
+  }
+
+  @Test
+  public void testGetJpegImageData_huffmanOptimized() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImage420.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(true, imageParams.isHuffmanOptimized());
+  }
+
+  @Test
+  public void testGetJpegImageData_420Sampling() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImage420.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(JpegImageUtils.SamplingModes.YUV420, imageParams.getSamplingMode());
+  }
+
+  @Test
+  public void testGetJpegImageData_444Sampling() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImage444.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(JpegImageUtils.SamplingModes.YUV444, imageParams.getSamplingMode());
+    assertEquals(0.90F, imageParams.getChromaQualityFactor(), 0.01F);
+    assertEquals(0.90F, imageParams.getLumaQualityFactor(), 0.01F);
+    assertEquals(0.90F, imageParams.getApproxQualityFactor(), 0.01F);
+  }
+
+  @Test
+  public void testGetJpegImageData_notHuffmanOptimized() throws Exception {
+    String resource = "org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg";
+    InputStream is = getClass().getClassLoader().getResourceAsStream(resource);
+    JpegImageUtils.JpegImageParams imageParams = JpegImageUtils.getJpegImageData(is, resource);
+    assertEquals(false, imageParams.isHuffmanOptimized());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java
new file mode 100644
index 0000000..4cb063d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/OptimizerConfigTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * Tests for {@code OptimizerConfig}
+ */
+public class OptimizerConfigTest {
+
+  @Test
+  public void testForHighJpegCompression() {
+    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 1.00f, 200, false, false);
+    assertEquals(0.9f, config.getJpegCompression(), 0.0001);
+  }
+
+  @Test
+  public void testForLowJpegCompression() {
+    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 0.10f, 200, false, false);
+    assertEquals(0.5f, config.getJpegCompression() , 0.0001);
+  }
+
+  @Test
+  public void testForAcceptableJpegCompression() {
+    OptimizerConfig config = new OptimizerConfig(1024 * 1024, 256, true, 0.85f, 200, false, false);
+    assertEquals(0.85f, config.getJpegCompression(), 0.0001);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/PNGOptimizerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/PNGOptimizerTest.java
new file mode 100644
index 0000000..325774e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/image/PNGOptimizerTest.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.image;
+
+import org.apache.sanselan.ImageReadException;
+import org.apache.sanselan.Sanselan;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.junit.Test;
+
+import java.io.IOException;
+
+/**
+ * Test PNG handling
+ */
+public class PNGOptimizerTest extends BaseOptimizerTest {
+  @Test
+  public void testRewriteInefficientPNG() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/inefficient.png", "image/png");
+    HttpResponse httpResponse = rewrite(resp);
+    assertTrue(httpResponse.getContentLength() <= resp.getContentLength());
+    assertEquals("image/png", httpResponse.getHeader("Content-Type"));
+  }
+
+  // Strip the alpha component from an image that was stored in RGBA form but
+  // which is entirely opaque
+  @Test
+  public void testStripAlpha() throws Exception {
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/rgbawithnoalpha.png", "image/png");
+    HttpResponse httpResponse = rewrite(resp);
+    assertTrue(httpResponse.getContentLength() <= resp.getContentLength());
+    assertEquals("image/png", httpResponse.getHeader("Content-Type"));
+  }
+
+  @Test(expected=IOException.class)
+  public void testEvil() throws Exception {
+    // Metadata length is too long causes OutOfMemory
+    HttpResponse resp =
+        createResponse("org/apache/shindig/gadgets/rewrite/image/evil.png", "image/png");
+    rewrite(resp);
+    fail("Should have failed to read image");
+  }
+
+  HttpResponse rewrite(HttpResponse original)
+      throws IOException, ImageReadException {
+    HttpResponseBuilder builder = new HttpResponseBuilder(original);
+    new PNGOptimizer(new OptimizerConfig(), builder).rewrite(
+         Sanselan.getBufferedImage(original.getResponse()));
+    return builder.create();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/ClosureJsCompilerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/ClosureJsCompilerTest.java
new file mode 100644
index 0000000..e26484a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/ClosureJsCompilerTest.java
@@ -0,0 +1,327 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.js;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.createMockBuilder;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.common.cache.Cache;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.cache.NullCache;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.js.JsContent;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.javascript.jscomp.Compiler;
+import com.google.javascript.jscomp.CompilerOptions;
+import com.google.javascript.jscomp.DiagnosticType;
+import com.google.javascript.jscomp.JSError;
+import com.google.javascript.jscomp.JSSourceFile;
+import com.google.javascript.jscomp.Result;
+import com.google.javascript.jscomp.SourceMap;
+import com.google.javascript.jscomp.SourceMap.Format;
+
+public class ClosureJsCompilerTest {
+  private static final String ACTUAL_COMPILER_OUTPUT = "window.abc={};";
+  private static final String EXPORT_COMPILER_STRING = "window['abc'] = {};";
+  private static final Iterable<JsContent> EXPORT_COMPILER_CONTENTS =
+      newJsContents(EXPORT_COMPILER_STRING);
+  private static final List<String> EXPORTS = ImmutableList.of("foo", "bar");
+  private static final String EXTERN = "extern";
+  private static final String ERROR_NAME = "error";
+  private static final JSError JS_ERROR = JSError.make("js", 12, 34,
+      DiagnosticType.error(ERROR_NAME, "errDesc"));
+  private static final Map<String, String> COMPILER_IO = ImmutableMap.<String, String>builder()
+      .put("  ", "")
+      .put(EXPORT_COMPILER_STRING, ACTUAL_COMPILER_OUTPUT)
+      .put("var foo = function(x) {}", "var foo=function(x){};")
+      .put("var foo = function(x) { bar(x, x) }; \n var bar = function(x, y) { bar(x) };",
+          "var foo=function(x){bar(x,x)};var bar=function(x,y){bar(x)};")
+      .put("", "")
+      .build();
+
+  private Compiler realCompMock;
+  private CompilerOptions realOptionsMock;
+  private Result realResultMock;
+  private DefaultJsCompiler compilerMock;
+  private JsResponse exportResponseMock;
+  private JsUri jsUriMock;
+  private CacheProvider cacheMock;
+  private ClosureJsCompiler compiler;
+  private ExecutorService executorServiceMock;
+
+  @Before
+  public void setUp() throws Exception {
+    cacheMock = new MockProvider();
+    exportResponseMock = mockJsResponse(EXPORT_COMPILER_STRING);
+    compilerMock = mockDefaultJsCompiler(exportResponseMock, EXPORT_COMPILER_CONTENTS);
+    executorServiceMock = EasyMock.createMock(ExecutorService.class);
+    EasyMock.makeThreadSafe(executorServiceMock, true);
+  }
+
+  @Ignore("This class was not being run and when I ran it this test did not pass.  Not familiar enough to enable it.")
+  @Test
+  public void testGetJsContentWithGoogSymbolExports() throws Exception {
+    realOptionsMock = mockRealJsCompilerOptions(true); // with
+    compiler = newClosureJsCompiler(null, realOptionsMock, compilerMock, cacheMock);
+    FeatureBundle bundle = mockBundle(EXPORTS);
+    Iterable<JsContent> actual = compiler.getJsContent(mockJsUri(false), bundle);
+    assertEquals(EXPORT_COMPILER_STRING +
+        "goog.exportSymbol('bar', bar);\n" +
+        "goog.exportSymbol('foo', foo);\n",
+        getContent(actual));
+  }
+
+  @Ignore("This class was not being run and when I ran it this test did not pass.  Not familiar enough to enable it.")
+  @Test
+  public void testGetJsContentWithoutGoogSymbolExports() throws Exception {
+    realOptionsMock = mockRealJsCompilerOptions(false); // without
+    compiler = newClosureJsCompiler(null, realOptionsMock, compilerMock, cacheMock);
+    FeatureBundle bundle = mockBundle(EXPORTS);
+    Iterable<JsContent> actual = compiler.getJsContent(mockJsUri(false), bundle);
+    assertEquals(EXPORT_COMPILER_STRING, getContent(actual));
+  }
+
+  @Test
+  public void testCompileSuccessOpt() throws Exception {
+    jsUriMock = mockJsUri(false); // opt
+    realResultMock = mockRealJsResult();
+    realCompMock = mockRealJsCompiler(null, realResultMock, ACTUAL_COMPILER_OUTPUT);
+    realOptionsMock = mockRealJsCompilerOptions(false);
+    compiler = newClosureJsCompiler(realCompMock, realOptionsMock, compilerMock, cacheMock);
+    JsResponse actual = compiler.compile(jsUriMock, EXPORT_COMPILER_CONTENTS, EXTERN);
+    assertEquals(ACTUAL_COMPILER_OUTPUT, actual.toJsString());
+    assertTrue(actual.getErrors().isEmpty());
+  }
+
+  @Ignore("This class was not being run and when I ran it this test did not pass.  Not familiar enough to enable it.")
+  @SuppressWarnings("unchecked")
+  @Test
+  public void testCompileSuccessOptWithProfiling() throws Exception {
+    jsUriMock = mockJsUri(false); // opt
+
+    realOptionsMock = new CompilerOptions();
+    realOptionsMock.enableExternExports(false);
+    realOptionsMock.sourceMapOutputPath = "test.out";
+    realOptionsMock.sourceMapFormat = Format.V2;
+    realOptionsMock.sourceMapDetailLevel = SourceMap.DetailLevel.ALL;
+    realOptionsMock.ideMode = false;
+    realOptionsMock.convertToDottedProperties = true;
+
+    for (Map.Entry<String, String> compilerTest : COMPILER_IO.entrySet()) {
+      List<JsContent> content = newJsContents(compilerTest.getKey());
+      exportResponseMock = mockJsResponse(compilerTest.getKey());
+      compilerMock = mockDefaultJsCompiler(exportResponseMock, content);
+      compiler = newProfilingClosureJsCompiler(realOptionsMock, compilerMock, cacheMock);
+
+      JsResponse actual = compiler.compile(jsUriMock, content, EXTERN);
+      assertEquals(compilerTest.getValue(), actual.toJsString());
+      assertTrue(actual.getErrors().isEmpty());
+    }
+  }
+
+  @Test
+  public void testCompileSuccessDeb() throws Exception {
+    jsUriMock = mockJsUri(true); // debug
+    realResultMock = mockRealJsResult();
+    realCompMock = mockRealJsCompiler(null, realResultMock, ACTUAL_COMPILER_OUTPUT);
+    realOptionsMock = mockRealJsCompilerOptions(false);
+    compiler = newClosureJsCompiler(realCompMock, realOptionsMock, compilerMock, cacheMock);
+    JsResponse actual = compiler.compile(jsUriMock, EXPORT_COMPILER_CONTENTS, EXTERN);
+    assertEquals(EXPORT_COMPILER_STRING, actual.toJsString());
+    assertTrue(actual.getErrors().isEmpty());
+  }
+
+  @Ignore("This class was not being run and when I ran it this test did not pass.  Not familiar enough to enable it.")
+  @Test
+  public void testCompileErrorOpt() throws Exception {
+    jsUriMock = mockJsUri(false); // opt
+    realCompMock = mockRealJsCompiler(JS_ERROR, realResultMock, ACTUAL_COMPILER_OUTPUT);
+    realOptionsMock = mockRealJsCompilerOptions(true); // force compiler to run
+    compiler = newClosureJsCompiler(realCompMock, realOptionsMock, compilerMock, cacheMock);
+    JsResponse actual = compiler.compile(jsUriMock, EXPORT_COMPILER_CONTENTS, EXTERN);
+    assertTrue(actual.getErrors().get(0).contains(ERROR_NAME));
+    assertEquals(1, actual.getErrors().size());
+  }
+
+  @Ignore("This class was not being run and when I ran it this test did not pass.  Not familiar enough to enable it.")
+  @Test
+  public void testCompileErrorDeb() throws Exception {
+    jsUriMock = mockJsUri(true); // debug
+    realCompMock = mockRealJsCompiler(JS_ERROR, realResultMock, ACTUAL_COMPILER_OUTPUT);
+    realOptionsMock = mockRealJsCompilerOptions(true); // force compiler to run
+    realResultMock = mockRealJsResult();
+    compiler = newClosureJsCompiler(realCompMock, realOptionsMock, compilerMock, cacheMock);
+    JsResponse actual = compiler.compile(jsUriMock, EXPORT_COMPILER_CONTENTS, EXTERN);
+    assertTrue(actual.getErrors().get(0).contains(ERROR_NAME));
+    assertEquals(1, actual.getErrors().size());
+  }
+
+  private ClosureJsCompiler newClosureJsCompiler(final Compiler realComp,
+      CompilerOptions realOptions, DefaultJsCompiler defaultComp, CacheProvider cache) throws InterruptedException, ExecutionException {
+    Future<CompileResult> mockFuture = EasyMock.createMock(Future.class);
+    expect(mockFuture.get()).andReturn(new CompileResult(realComp, realResultMock)).anyTimes();
+    replay(mockFuture);
+    expect(executorServiceMock.submit(isA(Callable.class))).andReturn(mockFuture);
+    replay(executorServiceMock);
+    ClosureJsCompiler compiler = createMockBuilder(ClosureJsCompiler.class)
+        .addMockedMethods("getCompilerOptions")
+        .withConstructor(defaultComp, cache, "simple", executorServiceMock)
+        .createMock();
+    expect(compiler.getCompilerOptions(isA(JsUri.class))).andReturn(realOptionsMock).anyTimes();
+    
+    replay(compiler);
+    return compiler;
+  }
+
+  private ClosureJsCompiler newProfilingClosureJsCompiler(CompilerOptions realOptions,
+      DefaultJsCompiler defaultComp, CacheProvider cache) {
+    ClosureJsCompiler compiler =
+        createMockBuilder(ClosureJsCompiler.class)
+            .addMockedMethods("getCompilerOptions")
+            .withConstructor(defaultComp, cache, "simple").createMock();
+    expect(compiler.getCompilerOptions(isA(JsUri.class))).andReturn(realOptions).anyTimes();
+    replay(compiler);
+    return compiler;
+  }
+
+  private JsResponse mockJsResponse(String content) {
+    JsResponse result = createMock(JsResponse.class);
+    expect(result.toJsString()).andReturn(content).anyTimes();
+    expect(result.getAllJsContent()).andReturn(newJsContents(content)).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  @SuppressWarnings("unchecked")
+  private DefaultJsCompiler mockDefaultJsCompiler(JsResponse res, Iterable<JsContent> content) {
+    DefaultJsCompiler result = createMock(DefaultJsCompiler.class);
+    expect(result.getJsContent(isA(JsUri.class), isA(FeatureBundle.class)))
+        .andReturn(content).anyTimes();
+    expect(result.compile(isA(JsUri.class), isA(Iterable.class), isA(String.class)))
+        .andReturn(res).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private Result mockRealJsResult() {
+    Result result = createMock(Result.class);
+    replay(result);
+    return result;
+  }
+
+  private Compiler mockRealJsCompiler(JSError error, Result res, String toSource) {
+    Compiler result = createMock(Compiler.class);
+    expect(result.compile(EasyMock.<List<JSSourceFile>>anyObject(),
+        EasyMock.<List<JSSourceFile>>anyObject(),
+        isA(CompilerOptions.class))).andReturn(res);
+    if (error != null) {
+      expect(result.hasErrors()).andReturn(true);
+      expect(result.getErrors()).andReturn(new JSError[] { error });
+    } else {
+      expect(result.hasErrors()).andReturn(false);
+    }
+    expect(result.getResult()).andReturn(res);
+    expect(result.toSource()).andReturn(toSource);
+    replay(result);
+    return result;
+  }
+
+  private CompilerOptions mockRealJsCompilerOptions(boolean enableExternExports) {
+    CompilerOptions result = createMock(CompilerOptions.class);
+    expect(result.isExternExportsEnabled()).andReturn(enableExternExports).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private JsUri mockJsUri(boolean debug) {
+    JsUri result = createMock(JsUri.class);
+    expect(result.isDebug()).andReturn(debug).anyTimes();
+    expect(result.getCompileMode()).andReturn(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL).anyTimes();
+    expect(result.getStatus()).andReturn(UriStatus.VALID_UNVERSIONED).anyTimes();
+    expect(result.getContainer()).andReturn("container").anyTimes();
+    expect(result.getContext()).andReturn(RenderingContext.CONFIGURED_GADGET).anyTimes();
+    expect(result.getRefresh()).andReturn(1000).anyTimes();
+    expect(result.isNoCache()).andReturn(false).anyTimes();
+    expect(result.getGadget()).andReturn("http://foo.com/g.xml").anyTimes();
+    expect(result.getLibs()).andReturn(ImmutableList.<String>of()).anyTimes();
+    expect(result.getLoadedLibs()).andReturn(ImmutableList.<String>of()).anyTimes();
+    expect(result.getOnload()).andReturn("foo").anyTimes();
+    expect(result.isJsload()).andReturn(true).anyTimes();
+    expect(result.isNohint()).andReturn(true).anyTimes();
+    expect(result.getOrigUri()).andReturn(null).anyTimes();
+    expect(result.getRepository()).andReturn(null).anyTimes();
+    expect(result.getExtensionParams()).andReturn(null).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureBundle mockBundle(List<String> exports) {
+    FeatureBundle result = createMock(FeatureBundle.class);
+    expect(result.getApis(ApiDirective.Type.JS, true)).andReturn(exports).anyTimes();
+    expect(result.getName()).andReturn(null).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private class MockProvider implements CacheProvider {
+    public <K, V> Cache<K, V> createCache(String name) {
+      return new NullCache<K, V>();
+    }
+  }
+
+  private String getContent(Iterable<JsContent> jsContent) {
+    StringBuilder sb = new StringBuilder();
+    for (JsContent js : jsContent) {
+      sb.append(js.get());
+    }
+    return sb.toString();
+  }
+
+  private static List<JsContent> newJsContents(String jsCode) {
+    List<JsContent> result = Lists.newArrayList();
+    result.add(JsContent.fromText(jsCode, "testSource"));
+    return result;
+  }
+  
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/DefaultJsCompilerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/DefaultJsCompilerTest.java
new file mode 100644
index 0000000..ca2d94a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/rewrite/js/DefaultJsCompilerTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.rewrite.js;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.js.JsContent;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+
+public class DefaultJsCompilerTest {
+  private final String COMPILE_CONTENT = "alert('compile');";
+  private final String RESOURCE_CONTENT_DEB = "alert('deb');";
+  private final String RESOURCE_CONTENT_OPT = "alert('opt');";
+  private final String RESOURCE_URL_DEB = "deb.js";
+  private final String RESOURCE_URL_OPT = "opt.js";
+
+  private DefaultJsCompiler compiler;
+
+  @Before
+  public void setUp() throws Exception {
+    compiler = new DefaultJsCompiler();
+  }
+
+  @Test
+  public void testGetJsContentWithDeb() throws Exception {
+    JsUri jsUri = mockJsUri(true); // debug
+    FeatureResource extRes = mockResource(true, RESOURCE_URL_DEB, RESOURCE_URL_OPT);
+    FeatureResource intRes = mockResource(false, RESOURCE_CONTENT_DEB, RESOURCE_CONTENT_OPT);
+    FeatureBundle bundle = mockBundle(Lists.newArrayList(extRes, intRes));
+    Iterable<JsContent> actual = compiler.getJsContent(jsUri, bundle);
+    assertEquals(
+        "document.write('<script src=\"" + RESOURCE_URL_DEB + "\"></script>');\n" +
+        RESOURCE_CONTENT_DEB + ";\n",
+        getContent(actual));
+  }
+
+  @Test
+  public void testGetJsContentWithOpt() throws Exception {
+    JsUri jsUri = mockJsUri(false); // optimized
+    FeatureResource extRes = mockResource(true, RESOURCE_URL_DEB, RESOURCE_URL_OPT);
+    FeatureResource intRes = mockResource(false, RESOURCE_CONTENT_DEB, RESOURCE_CONTENT_OPT);
+    FeatureBundle bundle = mockBundle(Lists.newArrayList(extRes, intRes));
+    Iterable<JsContent> actual = compiler.getJsContent(jsUri, bundle);
+    assertEquals(
+        "document.write('<script src=\"" + RESOURCE_URL_OPT + "\"></script>');\n" +
+        RESOURCE_CONTENT_OPT + ";\n",
+        getContent(actual));
+  }
+
+  @Test
+  public void testCompile() throws Exception {
+    JsResponse actual = compiler.compile(null,
+        ImmutableList.of(JsContent.fromText(COMPILE_CONTENT, "js")), null);
+    assertEquals(COMPILE_CONTENT, actual.toJsString());
+    assertEquals(0, actual.getErrors().size());
+  }
+
+  private JsUri mockJsUri(boolean debug) {
+    JsUri result = createMock(JsUri.class);
+    expect(result.isDebug()).andReturn(debug).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureBundle mockBundle(List<FeatureResource> resources) {
+    FeatureBundle result = createMock(FeatureBundle.class);
+    expect(result.getResources()).andReturn(resources).anyTimes();
+    expect(result.getName()).andReturn("feature").anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureResource mockResource(boolean external, String debContent, String optContent) {
+    FeatureResource result = createMock(FeatureResource.class);
+    expect(result.getDebugContent()).andReturn(debContent).anyTimes();
+    expect(result.getContent()).andReturn(optContent).anyTimes();
+    expect(result.isExternal()).andReturn(external).anyTimes();
+    expect(result.getName()).andReturn("source").anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private String getContent(Iterable<JsContent> jsContent) {
+    StringBuilder sb = new StringBuilder();
+    for (JsContent js : jsContent) {
+      sb.append(js.get());
+    }
+    return sb.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/CajaContentRewriterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/CajaContentRewriterTest.java
new file mode 100644
index 0000000..6fadf29
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/CajaContentRewriterTest.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.caja.plugin.PluginCompiler;
+import com.google.caja.plugin.PluginMeta;
+import com.google.caja.reporting.BuildInfo;
+import com.google.caja.reporting.MessageQueue;
+import com.google.common.collect.ImmutableList;
+import org.apache.shindig.common.cache.CacheProvider;
+import org.apache.shindig.common.cache.LruCacheProvider;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.caja.CajaHtmlParser;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.RewriterTestBase;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.easymock.EasyMock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+
+import java.util.List;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.matchers.JUnitMatchers.containsString;
+
+public class CajaContentRewriterTest extends RewriterTestBase {
+  private List<GadgetHtmlParser> parsers;
+  private CajaContentRewriter rewriter;
+  private ProxyUriManager proxyUriManager;
+
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    DOMImplementation dom = new ParseModule.DOMImplementationProvider().get();
+    GadgetHtmlParser neko =  new NekoSimplifiedHtmlParser(dom);
+    GadgetHtmlParser caja =  new CajaHtmlParser(dom);
+
+    // FIXME: Caja has trouble with the NekoSimplifiedHtmlParser
+    // Disabling neko for now
+    parsers = ImmutableList.of(/*neko, */caja);
+
+    CacheProvider lru = new LruCacheProvider(3);
+    RequestPipeline pipeline = EasyMock.createNiceMock(RequestPipeline.class);
+    DefaultHtmlSerializer defaultSerializer = new DefaultHtmlSerializer();
+    proxyUriManager = EasyMock.createNiceMock(ProxyUriManager.class);
+    rewriter = new CajaContentRewriter(lru, pipeline, defaultSerializer, proxyUriManager) {
+      @Override
+      protected PluginCompiler makePluginCompiler(PluginMeta m, MessageQueue q) {
+        BuildInfo bi = EasyMock.createNiceMock(BuildInfo.class);
+        expect(bi.getBuildInfo()).andReturn("bi").anyTimes();
+        expect(bi.getBuildTimestamp()).andReturn("0").anyTimes();
+        expect(bi.getBuildVersion()).andReturn("0").anyTimes();
+        expect(bi.getCurrentTime()).andReturn(0L).anyTimes();
+        replay(bi);
+        return new PluginCompiler(bi, m, q);
+      }
+    };
+  }
+
+  @Test
+  public void testErrorDuringRewrite() throws Exception {
+    String markup = "<script>window['x']={}; with(x) {};</script>";
+    String expected = "<html><head></head><body><ul class=\"gadgets-messages\">";
+
+    List<String> messages = ImmutableList.of(
+            "&#34;with&#34; blocks are not allowed");
+    testMarkup(markup, expected, messages);
+  }
+
+  @Test
+  public void testCssExpression() throws Exception {
+    String markup = "<div style='top:expression(alert(0), 0)'>test</div>";
+    String expected =
+        "<div>test</div>";
+
+    List<String> messages = ImmutableList.of(
+            "css property top has bad value: ==&gt;expression(alert(0), 0)");
+    testMarkup(markup, expected, messages);
+  }
+
+  @Test
+  public void testRewrite() throws Exception {
+    String markup = "<script>window['a']=0;</script>";
+    String expected =
+        "caja___.start";
+
+    List<String> messages = ImmutableList.of();
+    testMarkup(markup, expected, messages);
+  }
+
+  @Test
+  public void testUrlRewrite() throws Exception {
+    String uri = "http://www.example.com/";
+    String unproxied = uri;
+    String proxied = "http://shindig.com/gadgets/proxy?url=" + uri;
+
+    expect(proxyUriManager.make(EasyMock.anyObject(List.class), EasyMock.isNull(Integer.class)))
+        .andReturn(ImmutableList.<Uri>of(Uri.parse(proxied))).anyTimes();
+    replay(proxyUriManager);
+
+    // Uris that transistion the page
+    assertUrlRewritten("a", "href", uri, unproxied);
+    assertUrlRewritten("area", "href", uri, unproxied);
+
+    // Uris that load media
+    assertUrlRewritten("img", "src", uri, proxied);
+
+    // Uris that have no effect on the document
+    assertUrlRewritten("blockquote", "cite", uri, unproxied);
+    assertUrlRewritten("q", "cite", uri, unproxied);
+    assertUrlRewritten("del", "cite", uri, unproxied);
+    assertUrlRewritten("ins", "cite", uri, unproxied);
+  }
+
+
+  // Fails due to non-existent mail classes referenced in caja
+  // @Test
+  public void testIncludedURLRequestMarkedInternal() throws Exception {
+    CacheProvider lru = new LruCacheProvider(3);
+    DefaultHtmlSerializer defaultSerializer = new DefaultHtmlSerializer();
+    CapturingPipeline pipeline = new CapturingPipeline();
+    rewriter = new CajaContentRewriter(lru, pipeline, defaultSerializer, proxyUriManager) {
+      @Override
+      protected PluginCompiler makePluginCompiler(PluginMeta m, MessageQueue q) {
+        BuildInfo bi = EasyMock.createNiceMock(BuildInfo.class);
+        expect(bi.getBuildInfo()).andReturn("bi").anyTimes();
+        expect(bi.getBuildTimestamp()).andReturn("0").anyTimes();
+        expect(bi.getBuildVersion()).andReturn("0").anyTimes();
+        expect(bi.getCurrentTime()).andReturn(0L).anyTimes();
+        replay(bi);
+        return new PluginCompiler(bi, m, q);
+      }
+    };
+
+    // we don't really care what the result looks like, we just want to check the issued request
+    String markup = "<script type=\"text/javascript\" src=\"http://www.example.com/scripts/scriptFile.js\"></script>";
+    String expected = "";
+    testMarkup( markup, expected );
+
+    assertNotNull( pipeline.request );
+    assertTrue( pipeline.request.isInternalRequest() );
+  }
+
+  private void testMarkup(String markup, String expected) throws GadgetException{
+    testMarkup(markup, expected, null);
+  }
+
+  private void assertUrlRewritten(String tagName, String attr, String orig, String rewritten)
+      throws Exception {
+    String markUp = "<" + tagName + " " + attr + "=\"" + orig + "\">";
+    String expected = attr + "=\"" + rewritten + "\"";
+    testMarkup(markUp, expected);
+  }
+
+  private void testMarkup(String markup, String expected, List<String> msgs) throws GadgetException{
+    Gadget gadget = makeGadget();
+    for (GadgetHtmlParser parser : parsers) {
+      MutableContent mc = new MutableContent(parser, markup);
+      rewriter.rewrite(gadget, mc);
+
+      String actual = mc.getContent();
+
+      if (msgs != null) {
+        for (String msg : msgs) {
+          System.out.println("Msg:" + msg);
+          assertThat(actual, containsString(msg));
+        }
+      }
+    }
+  }
+
+  private Gadget makeGadget() throws GadgetException {
+    Gadget gadget = EasyMock.createNiceMock(Gadget.class);
+    GadgetContext context = EasyMock.createNiceMock(GadgetContext.class);
+
+    expect(context.getUrl()).andReturn(Uri.parse("http://example.com/gadget.xml")).anyTimes();
+    expect(context.getContainer()).andReturn("cajaContainer").anyTimes();
+    expect(context.getDebug()).andReturn(false).anyTimes();
+
+    expect(gadget.getContext()).andReturn(context).anyTimes();
+    expect(gadget.getAllFeatures()).andReturn(ImmutableList.of("caja")).anyTimes();
+    expect(gadget.requiresCaja()).andReturn(true).anyTimes();
+
+    replay(context, gadget);
+    return gadget;
+  }
+
+  private static class CapturingPipeline implements RequestPipeline {
+    HttpRequest request;
+
+    public HttpResponse execute(HttpRequest request) {
+      this.request = request;
+      return new HttpResponse("");
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ConcatProxyServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ConcatProxyServletTest.java
new file mode 100644
index 0000000..edb960a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ConcatProxyServletTest.java
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.expect;
+
+import java.util.List;
+
+import org.apache.shindig.common.servlet.HttpServletResponseRecorder;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.uri.ConcatUriManager;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class ConcatProxyServletTest extends ServletTestFixture {
+  private static final String REQUEST_DOMAIN = "example.org";
+
+  private static final Uri URL1 = Uri.parse("http://example.org/1.js");
+  private static final Uri URL2 = Uri.parse("http://example.org/2.js");
+  private static final Uri URL3 = Uri.parse("http://example.org/3.js");
+
+  private static final String SCRT1 = "var v1 = 1;";
+  private static final String SCRT2 = "var v2 = { \"a-b\": 1 , c: \"hello!,\" };";
+  private static final String SCRT3 = "var v3 = \"world\";";
+
+  private static final String SCRT1_ESCAPED = "var v1 = 1;";
+  private static final String SCRT2_ESCAPED =
+      "var v2 = { \\\"a-b\\\": 1 , c: \\\"hello!,\\\" };";
+  private static final String SCRT3_ESCAPED = "var v3 = \\\"world\\\";";
+
+  private final ConcatProxyServlet servlet = new ConcatProxyServlet();
+  private TestConcatUriManager uriManager;
+
+  private final ExecutorService sequentialExecutor = Executors.newSingleThreadExecutor();
+  private final ExecutorService threadedExecutor = Executors.newCachedThreadPool();
+
+  @Before
+  public void setUp() throws Exception {
+    servlet.setRequestPipeline(pipeline);
+    uriManager = new TestConcatUriManager();
+    servlet.setConcatUriManager(uriManager);
+
+    expect(request.getHeader("Host")).andReturn(REQUEST_DOMAIN).anyTimes();
+    expect(lockedDomainService.isSafeForOpenProxy(REQUEST_DOMAIN))
+        .andReturn(true).anyTimes();
+
+    expectGetAndReturnData(URL1, SCRT1);
+    expectGetAndReturnData(URL2, SCRT2);
+    expectGetAndReturnData(URL3, SCRT3);
+  }
+
+  private void expectGetAndReturnData(Uri url, String data) throws Exception {
+    HttpRequest req = new HttpRequest(url);
+    HttpResponse resp = new HttpResponseBuilder().setResponse(data.getBytes()).create();
+    expect(pipeline.execute(req)).andReturn(resp).anyTimes();
+  }
+
+  /**
+   * Simulate the added comments by concat
+   * @param data - concatenated data
+   * @param url - data source url
+   * @return data with added comments
+   */
+  private String addComment(String data, String url) {
+    String res = "/* ---- Start " + url + " ---- */\r\n"
+        + data + "/* ---- End " + url + " ---- */\r\n";
+    return res;
+  }
+
+  private String addErrComment(String url, int code) {
+    return "/* ---- Error " + code + " (" + url + ") ---- */\r\n";
+  }
+
+  private String addConcatErrComment(GadgetException.Code code , String url) {
+    return "/* ---- Error " + code.toString() + " concat(" + url + ") null ---- */\r\n";
+  }
+  /**
+   * Simulate the asJSON result of one script
+   * @param url - the script url
+   * @param data - the script escaped content
+   * @return simulated hash mapping
+   */
+  private String addVar(String url, String data) {
+    return '\"' + url + "\":\"" + data +"\",\r\n";
+  }
+
+  private String addLastVar(String url, String data) {
+    return '\"' + url + "\":\"" + data +"\"";
+  }
+
+  /**
+   * Run a concat test by fetching resources as configured by given Executor
+   * @param result - expected concat results
+   * @param uris - list of uris to concat
+   * @throws Exception
+   */
+  private void runConcat(ExecutorService exec, String result, String tok, Uri... uris)
+      throws Exception {
+    expectRequestWithUris(Lists.newArrayList(uris), tok);
+
+    // Run the servlet
+    servlet.setExecutor(exec);
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(result, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void testSimpleConcat() throws Exception {
+    String results = addComment(SCRT1, URL1.toString()) + addComment(SCRT2,URL2.toString());
+    runConcat(sequentialExecutor, results, null, URL1, URL2);
+  }
+
+  @Test
+  public void testSimpleConcatThreaded() throws Exception {
+    String results = addComment(SCRT1, URL1.toString()) + addComment(SCRT2,URL2.toString());
+    runConcat(threadedExecutor, results, null, URL1, URL2);
+  }
+
+  @Test
+  public void testThreeConcat() throws Exception {
+    String results = addComment(SCRT1, URL1.toString()) + addComment(SCRT2,URL2.toString())
+        + addComment(SCRT3, URL3.toString());
+    runConcat(sequentialExecutor, results, null, URL1, URL2, URL3);
+  }
+
+  @Test
+  public void testThreeConcatThreaded() throws Exception {
+    String results = addComment(SCRT1, URL1.toString()) + addComment(SCRT2,URL2.toString())
+        + addComment(SCRT3, URL3.toString());
+    runConcat(threadedExecutor, results, null, URL1, URL2, URL3);
+  }
+
+  @Test
+  public void testConcatBadException() throws Exception {
+    final Uri URL4 = Uri.parse("http://example.org/4.js");
+
+    HttpRequest req = new HttpRequest(URL4);
+    expect(pipeline.execute(req)).andThrow(
+        new GadgetException(GadgetException.Code.HTML_PARSE_ERROR)).anyTimes();
+
+    expectRequestWithUris(Lists.newArrayList(URL1, URL4));
+
+    // Run the servlet
+    servlet.doGet(request, recorder);
+    verify();
+
+    String results = addComment(SCRT1, URL1.toString())
+        + addConcatErrComment(GadgetException.Code.HTML_PARSE_ERROR, URL4.toString());
+    assertEquals(results, recorder.getResponseAsString());
+
+    assertEquals(400, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void testConcat404() throws Exception {
+    String url = "http://nobodyhome.com/";
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    HttpResponse resp = new HttpResponseBuilder().setHttpStatusCode(404).create();
+    expect(pipeline.execute(req)).andReturn(resp).anyTimes();
+
+    expectRequestWithUris(Lists.newArrayList(URL1, Uri.parse(url)));
+
+    servlet.doGet(request, recorder);
+    verify();
+
+    String results = addComment(SCRT1, URL1.toString()) + addErrComment(url,404);
+    assertEquals(results, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void testAsJsonConcat() throws Exception {
+    String results = "_js={\r\n"
+        + addVar(URL1.toString(), SCRT1_ESCAPED)
+        + addLastVar(URL2.toString(), SCRT2_ESCAPED)
+        + "};\r\n";
+    runConcat(sequentialExecutor, results, "_js", URL1, URL2);
+  }
+
+  @Test
+  public void testThreeAsJsonConcat() throws Exception {
+    String results = "_js={\r\n"
+        + addVar(URL1.toString(), SCRT1_ESCAPED)
+        + addVar(URL2.toString(), SCRT2_ESCAPED)
+        + addLastVar(URL3.toString(), SCRT3_ESCAPED)
+        + "};\r\n";
+    runConcat(sequentialExecutor, results, "_js", URL1, URL2, URL3);
+  }
+
+  @Test
+  public void testBadJsonVarConcat() throws Exception {
+    expectRequestWithUris(Lists.<Uri>newArrayList(), "bad code;");
+    servlet.doGet(request, recorder);
+    verify();
+    String results = "/* ---- Error 400, Bad json variable name bad code; ---- */\r\n";
+    assertEquals(results, recorder.getResponseAsString());
+    assertEquals(400, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void testAsJsonConcat404() throws Exception {
+    final Uri URL4 = Uri.parse("http://example.org/4.js");
+
+    HttpRequest req = new HttpRequest(URL4);
+    HttpResponse resp = new HttpResponseBuilder().setHttpStatusCode(404).create();
+    expect(pipeline.execute(req)).andReturn(resp).anyTimes();
+
+    String results = "_js={\r\n"
+        + addLastVar(URL1.toString(), SCRT1_ESCAPED)
+        + "/* ---- Error 404 (http://example.org/4.js) ---- */\r\n"
+        + "};\r\n";
+    runConcat(sequentialExecutor, results, "_js", URL1, URL4);
+  }
+
+  @Test
+  public void testAsJsonConcatException() throws Exception {
+    final Uri URL4 = Uri.parse("http://example.org/4.js");
+
+    HttpRequest req = new HttpRequest(URL4);
+    expect(pipeline.execute(req)).andThrow(
+        new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT)).anyTimes();
+
+    expectRequestWithUris(Lists.newArrayList(URL1, URL4), "_js");
+    servlet.doGet(request, recorder);
+    verify();
+    String results = "_js={\r\n"
+      + addLastVar(URL1.toString(), SCRT1_ESCAPED)
+      + addConcatErrComment(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, URL4.toString()) + "};\r\n";
+    assertEquals(results, recorder.getResponseAsString());
+    assertEquals(400, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void testAsJsonConcatBadException() throws Exception {
+    final Uri URL4 = Uri.parse("http://example.org/4.js");
+
+    HttpRequest req = new HttpRequest(URL4);
+    expect(pipeline.execute(req)).andThrow(
+        new GadgetException(GadgetException.Code.HTML_PARSE_ERROR)).anyTimes();
+
+    String results = "_js={\r\n"
+        + addLastVar(URL1.toString(), SCRT1_ESCAPED)
+        + addConcatErrComment(GadgetException.Code.HTML_PARSE_ERROR, URL4.toString()) + "};\r\n";
+
+    expectRequestWithUris(Lists.newArrayList(URL1, URL4), "_js");
+
+    // Run the servlet
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(results, recorder.getResponseAsString());
+    assertEquals(400, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void testMinimumCacheTtl() throws Exception {
+    final Uri URL4 = Uri.parse("http://example.org/4.js");
+    final Uri URL5 = Uri.parse("http://example.org/5.js");
+    final Uri URL6 = Uri.parse("http://example.org/6.js");
+
+    final Integer cacheTtl4 = Integer.MAX_VALUE;
+    final Integer cacheTtl5 = 100000;
+    final Integer cacheTtl6 = Integer.MAX_VALUE;
+
+    expectGetAndSetCacheTtl(URL4, cacheTtl4);
+    expectGetAndSetCacheTtl(URL5, cacheTtl5);
+    expectGetAndSetCacheTtl(URL6, cacheTtl6);
+
+    expectRequestWithUris(Lists.newArrayList(URL4, URL5, URL6));
+
+    // Run the servlet
+    servlet.doGet(request, recorder);
+    verify();
+    int cacheValue = getCacheControlMaxAge(recorder);
+    assertEquals(cacheTtl5, cacheValue, 10);
+  }
+
+  @Test
+  public void testDefaultCacheTtlCacheHeaderMissing() throws Exception {
+    final Uri URL4 = Uri.parse("http://example.org/4.js");
+    final Uri URL5 = Uri.parse("http://example.org/5.js");
+
+    expectGetAndReturnData(URL4, "");
+    expectGetAndReturnData(URL5, "");
+    expectRequestWithUris(Lists.newArrayList(URL4, URL5));
+
+    servlet.doGet(request, recorder);
+    verify();
+    int cacheValue = getCacheControlMaxAge(recorder);
+    // HttpResponse.defaultTtl is in msec, division by 1000 is required to convert into sec.
+    assertEquals((int) (HttpResponse.defaultTtl / 1000), cacheValue, 10);
+  }
+
+  private void expectGetAndSetCacheTtl(Uri url, Integer cacheTtl) throws Exception {
+    HttpRequest req = new HttpRequest(url);
+    HttpResponse resp = new HttpResponseBuilder().setCacheTtl(cacheTtl).create();
+    expect(pipeline.execute(req)).andReturn(resp);
+  }
+
+  /**
+   * Returns cache control max age from HttpServletResponseRecorder
+   */
+  private int getCacheControlMaxAge(HttpServletResponseRecorder recorder) {
+    String cacheControl = recorder.getHeader("Cache-Control");
+    assertTrue(cacheControl != null);
+    String cacheValue = cacheControl.substring(cacheControl.indexOf('=') + 1);
+    return Integer.decode(cacheValue);
+  }
+
+  private void expectRequestWithUris(List<Uri> uris) {
+    expectRequestWithUris(uris, null);
+  }
+
+  private void expectRequestWithUris(List<Uri> uris, String tok) {
+    expect(request.getScheme()).andReturn("http").anyTimes();
+    expect(request.getServerPort()).andReturn(80).anyTimes();
+    expect(request.getServerName()).andReturn("example.com").anyTimes();
+    expect(request.getRequestURI()).andReturn("/path").anyTimes();
+    expect(request.getQueryString()).andReturn("").anyTimes();
+    replay();
+
+    Uri uri = new UriBuilder(request).toUri();
+    uriManager.expect(uri, uris, tok);
+  }
+
+  private static class TestConcatUriManager implements ConcatUriManager {
+    private final Map<Uri, ConcatUri> uriMap;
+
+    private TestConcatUriManager() {
+      this.uriMap = Maps.newHashMap();
+    }
+
+    public List<ConcatData> make(List<ConcatUri> resourceUris, boolean isAdjacent) {
+      // Not used by ConcatProxyServlet
+      throw new UnsupportedOperationException();
+    }
+
+    public ConcatUri process(Uri uri) {
+      return uriMap.get(uri);
+    }
+
+    private void expect(Uri orig, UriStatus status, Type type, List<Uri> uris, String json) {
+      uriMap.put(orig, new ConcatUri(status, uris, json, type, null));
+    }
+
+    private void expect(Uri orig, List<Uri> uris, String tok) {
+      expect(orig, UriStatus.VALID_UNVERSIONED, Type.JS, uris, tok);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java
new file mode 100644
index 0000000..8c96e31
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.junit.Assert.*;
+
+import com.google.common.collect.Lists;
+
+import org.apache.http.util.ByteArrayBuffer;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests for {@link ETagFilter}.
+ */
+public class ETagFilterTest {
+
+  // Using Spanish string and UTF-8 encoding to check for encoding errors.
+  private static final String ENCODING = "UTF-8";
+  private static final byte[] RESPONSE_BODY_BYTES =
+      new byte[] {-62, -95, 72, 111, 108, 97, 44, 32, 110, 105, -61, -79, 111, 33};
+  private static final int RESPONSE_BODY_LENGTH = RESPONSE_BODY_BYTES.length;
+  private static final String GOOD_ETAG = "dae018f624d09423e7c4d7209fbea597";
+
+  private IMocksControl control;
+  private HttpServletRequest request;
+  private HttpServletResponse response;
+  private MockFilterChain chain;
+  private MockServletOutputStream stream;
+  private ETagFilter filter;
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createControl();
+    request = control.createMock(HttpServletRequest.class);
+    response = control.createMock(HttpServletResponse.class);
+    chain = new MockFilterChain();
+    stream = new MockServletOutputStream();
+    filter = new ETagFilter();
+
+    EasyMock.expect(response.isCommitted()).andReturn(false).anyTimes();
+    EasyMock.expect(response.getOutputStream()).andReturn(stream).anyTimes();
+    EasyMock.expect(response.getCharacterEncoding()).andReturn(ENCODING).anyTimes();
+    EasyMock.expect(request.getHeader(ETaggingHttpResponse.REQUEST_HEADER)).andReturn(null);
+    response.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, '"' + GOOD_ETAG + '"');
+    response.setContentLength(RESPONSE_BODY_LENGTH);
+  }
+
+  @Test
+  public void testTagContent() throws Exception {
+    chain.write(RESPONSE_BODY_BYTES);
+    control.replay();
+
+    filter.doFilter(request, response, chain);
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+    control.verify();
+  }
+
+  @Test
+  public void testTagContentOnException() throws Exception {
+    chain.write(RESPONSE_BODY_BYTES);
+    chain.throwException();
+    control.replay();
+
+    try {
+      filter.doFilter(request, response, chain);
+      fail("Should have thrown an IOException");
+    } catch (IOException e) {
+      // pass
+    }
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+    control.verify();
+  }
+
+  private class MockServletOutputStream extends ServletOutputStream {
+    private ByteArrayBuffer buffer = new ByteArrayBuffer(1024);
+
+    @Override
+    public void write(int b) {
+      buffer.append(b);
+    }
+
+    public byte[] getBuffer() {
+      return buffer.toByteArray();
+    }
+  }
+
+  private class MockFilterChain implements FilterChain {
+    private List<Object> commands = Lists.newArrayList();
+
+    public void write(byte[] s) {
+      commands.add(s);
+    }
+
+    public void throwException() {
+      commands.add(new IOException());
+    }
+
+    public void doFilter(ServletRequest request, ServletResponse response) throws IOException {
+      for (Object cmd : commands) {
+        if (cmd instanceof byte[]) {
+          response.getOutputStream().write((byte[]) cmd);
+        } else if (cmd instanceof IOException) {
+          throw (IOException) cmd;
+        }
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java
new file mode 100644
index 0000000..640af48
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.junit.Assert.*;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+
+import org.apache.http.util.ByteArrayBuffer;
+import org.apache.shindig.gadgets.servlet.ETaggingHttpResponse.BufferServletOutputStream;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests for {@link ETaggingHttpResponse}.
+ */
+public class ETaggingHttpResponseTest {
+
+  // Using Spanish string and UTF-8 encoding to check for encoding errors.
+  private static final String ENCODING = "UTF-8";
+  private static final String RESPONSE_BODY = "¡Hola, niño!";
+  private static final byte[] RESPONSE_BODY_BYTES =
+      new byte[] {-62, -95, 72, 111, 108, 97, 44, 32, 110, 105, -61, -79, 111, 33};
+  private static final String SECOND_RESPONSE_BODY = "你好";
+  private static final byte[] AFTER_SECOND_RESPONSE_BODY_BYTES =
+      new byte[] {-62, -95, 72, 111, 108, 97, 44, 32, 110, 105, -61, -79, 111, 33,
+          -28, -67, -96, -27, -91, -67};
+  private static final int RESPONSE_BODY_LENGTH = RESPONSE_BODY_BYTES.length;
+  private static final String GOOD_ETAG = "dae018f624d09423e7c4d7209fbea597";
+  private static final String SECOND_ETAG = "b6e56fb0129c3530f23dbb795daa3200";
+  private static final String BAD_ETAG = "some bogus etag";
+  private static final String EMPTY_CONTENT_ETAG = "d41d8cd98f00b204e9800998ecf8427e";
+
+  private static final Function<String, String> ETAG_QUOTER = new Function<String, String>() {
+    public String apply(String input) {
+      return '"' + input + '"';
+    }
+  };
+
+  private IMocksControl control;
+  private HttpServletRequest request;
+  private HttpServletResponse origResponse;
+  private MockServletOutputStream stream;
+  private ETaggingHttpResponse response;
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createControl();
+    request = control.createMock(HttpServletRequest.class);
+    origResponse = control.createMock(HttpServletResponse.class);
+    stream = new MockServletOutputStream();
+    response = new ETaggingHttpResponse(request, origResponse);
+
+    EasyMock.expect(origResponse.isCommitted()).andReturn(false).anyTimes();
+    EasyMock.expect(origResponse.getOutputStream()).andReturn(stream).anyTimes();
+    EasyMock.expect(origResponse.getCharacterEncoding()).andReturn(ENCODING).anyTimes();
+    origResponse.flushBuffer();
+    EasyMock.expectLastCall().anyTimes();
+  }
+
+  @Test
+  public void testTagContentWithPrint() throws Exception {
+    expectRequestETag();
+    expectFullResponse();
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testNotModifiedWithPrint() throws Exception {
+    expectRequestETag(GOOD_ETAG);
+    expectNotModifiedResponse(GOOD_ETAG);
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseBodyIsEmpty();
+    control.verify();
+  }
+
+  @Test
+  public void testNotModifiedWithManyETagsInRequest() throws Exception {
+    expectRequestETag(SECOND_ETAG, GOOD_ETAG, BAD_ETAG);
+    expectNotModifiedResponse(GOOD_ETAG);
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseBodyIsEmpty();
+    control.verify();
+  }
+
+  @Test
+  public void testNonMatchingETagWithPrint() throws Exception {
+    expectRequestETag(BAD_ETAG);
+    expectFullResponse();
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testNonMatchingETagWithManyETagsInRequest() throws Exception {
+    expectRequestETag(BAD_ETAG, SECOND_ETAG, EMPTY_CONTENT_ETAG);
+    expectFullResponse();
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testTagContentWithWrite() throws Exception {
+    expectRequestETag();
+    expectFullResponse();
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY_BYTES);
+    response.flushBuffer();
+
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testNotModifiedWithWrite() throws Exception {
+    expectRequestETag(GOOD_ETAG);
+    expectNotModifiedResponse(GOOD_ETAG);
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY_BYTES);
+    response.flushBuffer();
+
+    assertResponseBodyIsEmpty();
+    control.verify();
+  }
+
+  @Test
+  public void testNonMatchingETagWithWrite() throws Exception {
+    expectRequestETag(BAD_ETAG);
+    expectFullResponse();
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY_BYTES);
+    response.flushBuffer();
+
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testTagEmptyContent() throws Exception {
+    expectRequestETag();
+    origResponse.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, '"' + EMPTY_CONTENT_ETAG + '"');
+    origResponse.setContentLength(0);
+    control.replay();
+
+    response.getOutputStream();
+    response.flushBuffer();
+
+    assertEquals(0, stream.getBuffer().length);
+    control.verify();
+  }
+
+  @Test
+  public void testStreamingMode() throws Exception {
+    expectRequestETag();
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    assertEquals(0, stream.getBuffer().length);
+
+    response.startStreaming();
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+
+    response.getOutputStream().write(SECOND_RESPONSE_BODY.getBytes("UTF-8"));
+    assertArrayEquals(AFTER_SECOND_RESPONSE_BODY_BYTES, stream.getBuffer());
+  }
+
+  @Test
+  public void testCanCalculateHashSeveralTimes() throws Exception {
+    expectRequestETag(GOOD_ETAG);
+    expectNotModifiedResponse(GOOD_ETAG);
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY.getBytes("UTF-8"));
+    String hash = ((BufferServletOutputStream) response.getOutputStream()).getContentHash();
+    assertEquals(GOOD_ETAG, hash);
+    hash = ((BufferServletOutputStream) response.getOutputStream()).getContentHash();
+    assertEquals(GOOD_ETAG, hash);
+
+    response.flushBuffer();
+    assertResponseBodyIsEmpty();
+    control.verify();
+  }
+
+  @Test
+  public void testHashVariesAsDataIsAdded() throws Exception {
+    expectRequestETag(SECOND_ETAG);
+    expectNotModifiedResponse(SECOND_ETAG);
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY.getBytes("UTF-8"));
+    String hash = ((BufferServletOutputStream) response.getOutputStream()).getContentHash();
+    assertEquals(GOOD_ETAG, hash);
+    response.getOutputStream().write(SECOND_RESPONSE_BODY.getBytes("UTF-8"));
+    hash = ((BufferServletOutputStream) response.getOutputStream()).getContentHash();
+    assertEquals(SECOND_ETAG, hash);
+
+    response.flushBuffer();
+    assertResponseBodyIsEmpty();
+    control.verify();
+  }
+
+  private void expectRequestETag(String... eTag) {
+    String eTags = null;
+    if (eTag.length > 0) {
+      eTags = Joiner.on(',').join(Lists.transform(Arrays.asList(eTag), ETAG_QUOTER));
+    }
+    EasyMock.expect(request.getHeader(ETaggingHttpResponse.REQUEST_HEADER)).andReturn(eTags);
+  }
+
+  private void expectFullResponse() {
+    origResponse.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, '"' + GOOD_ETAG + '"');
+    origResponse.setContentLength(RESPONSE_BODY_LENGTH);
+  }
+
+  private void expectNotModifiedResponse(String eTag) {
+    origResponse.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, '"' + eTag + '"');
+    origResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+    origResponse.setContentLength(0);
+  }
+
+  private void assertResponseHasBody() {
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+  }
+
+  private void assertResponseBodyIsEmpty() {
+    assertEquals(0, stream.getBuffer().length);
+  }
+
+  private class MockServletOutputStream extends ServletOutputStream {
+    private ByteArrayBuffer buffer = new ByteArrayBuffer(1024);
+
+    @Override
+    public void write(int b) {
+      buffer.append(b);
+    }
+
+    public byte[] getBuffer() {
+      return buffer.toByteArray();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/FakeIframeUriManager.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/FakeIframeUriManager.java
new file mode 100644
index 0000000..f7d2685
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/FakeIframeUriManager.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.util.Map;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.uri.IframeUriManager;
+import org.apache.shindig.gadgets.uri.UriStatus;
+
+import com.google.caja.util.Maps;
+
+public class FakeIframeUriManager implements IframeUriManager {
+  protected boolean throwRandomFault = false;
+  public static final Uri DEFAULT_IFRAME_URI = Uri.parse("http://example.org/gadgets/foo-does-not-matter");
+  protected Uri iframeUrl = DEFAULT_IFRAME_URI;
+  public static Map<String, Uri> IFRAME_URIS = Maps.newHashMap();
+  static {
+    IFRAME_URIS.put("default", DEFAULT_IFRAME_URI);
+  }
+
+  FakeIframeUriManager() { }
+
+  public Uri makeRenderingUri(Gadget gadget) {
+    if (throwRandomFault) {
+      throw new RuntimeException("BROKEN");
+    }
+    return iframeUrl;
+  }
+
+  public UriStatus validateRenderingUri(Uri uri) {
+    throw new UnsupportedOperationException();
+  }
+
+  public Map<String, Uri> makeAllRenderingUris(Gadget gadget) {
+    return IFRAME_URIS;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/FakeProcessor.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/FakeProcessor.java
new file mode 100644
index 0000000..8c1fcfc
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/FakeProcessor.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.View;
+
+import java.util.List;
+import java.util.Map;
+
+public class FakeProcessor extends Processor {
+  protected final Map<Uri, ProcessingException> exceptions = Maps.newHashMap();
+  protected final Map<Uri, String> gadgets = Maps.newHashMap();
+  public static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+  public static final Uri SPEC_URL2 = Uri.parse("http://example.org/g2.xml");
+  public static final Uri SPEC_URL3 = Uri.parse("http://example.org/g3.xml");
+  public static final Uri SPEC_URL4 = Uri.parse("http://example.org/g4.xml");
+  public static final String SPEC_TITLE = "JSON-TEST";
+  public static final String SPEC_TITLE2 = "JSON-TEST2";
+  public static final int PREFERRED_HEIGHT = 100;
+  public static final int PREFERRED_WIDTH = 242;
+  public static final String FEATURE1="core";
+  public static final String FEATURE2="example-feature";
+  public static final String FEATURE3="example-feature2";
+  public static final String PARAM_NAME="param-name";
+  public static final String PARAM_NAME2="param-name2";
+  public static final String PARAM_VALUE="param-value";
+  public static final String PARAM_VALUE2="param-value2";
+  public static final String PARAM_VALUE3="param-value3";
+  public static final String LINK_REL = "rel";
+  public static final String LINK_HREF = "http://example.org/foo";
+  public static final String SPEC_XML =
+      "<Module>" +
+      "<ModulePrefs title=\"" + SPEC_TITLE + "\">" +
+      "  <Link rel='" + LINK_REL + "' href='" + LINK_HREF + "'/>" +
+      "</ModulePrefs>" +
+      "<UserPref name=\"up_one\">" +
+      "  <EnumValue value=\"val1\" display_value=\"disp1\"/>" +
+      "  <EnumValue value=\"abc\" display_value=\"disp2\"/>" +
+      "  <EnumValue value=\"z_xabc\" display_value=\"disp3\"/>" +
+      "  <EnumValue value=\"foo\" display_value=\"disp4\"/>" +
+      "</UserPref>" +
+      "<Content type=\"html\"" +
+      " preferred_height = \"" + PREFERRED_HEIGHT + '\"' +
+      " preferred_width = \"" + PREFERRED_WIDTH + '\"' +
+      ">Hello, world</Content>" +
+      "</Module>";
+
+  public static final String SPEC_XML2 =
+          "<Module>" +
+          "<ModulePrefs title=\"" + SPEC_TITLE2 + "\"/>" +
+          "<Content type=\"html\">Hello, world</Content>" +
+          "</Module>";
+
+  public static final String SPEC_XML3 =
+      "<Module>" +
+      "<ModulePrefs title=\"" + SPEC_TITLE2 + "\"/>" +
+      "<Content name=\"canvas\">Hello, world</Content>" +
+      "</Module>";
+
+  public static final String SPEC_XML4 =
+      "<Module>" +
+      "<ModulePrefs title=\"" + SPEC_TITLE + "\">" +
+      "  <Link rel='" + LINK_REL + "' href='" + LINK_HREF + "'/>" +
+      "<Optional feature=\""+FEATURE2+"\">"+
+      "<Param name=\""+PARAM_NAME+"\">"+PARAM_VALUE+"</Param>"+
+      "<Param name=\""+PARAM_NAME+"\">"+PARAM_VALUE2+"</Param>"+
+      "</Optional>"+
+      "<Require feature=\""+FEATURE3+"\">"+
+      "<Param name=\""+PARAM_NAME2+"\">"+PARAM_VALUE3+"</Param>"+
+      "</Require>"+
+      "</ModulePrefs>" +
+      "<UserPref name=\"up_one\">" +
+      "  <EnumValue value=\"val1\" display_value=\"disp1\"/>" +
+      "  <EnumValue value=\"abc\" display_value=\"disp2\"/>" +
+      "  <EnumValue value=\"z_xabc\" display_value=\"disp3\"/>" +
+      "  <EnumValue value=\"foo\" display_value=\"disp4\"/>" +
+      "</UserPref>" +
+      "<Content type=\"html\"" +
+      " preferred_height = \"" + PREFERRED_HEIGHT + '\"' +
+      " preferred_width = \"" + PREFERRED_WIDTH + '\"' +
+      ">Hello, world</Content>" +
+      "</Module>";
+
+  private final FeatureRegistry featureRegistry;
+
+  public static final List<String> FEATURE_NAMES=ImmutableList.of(FEATURE1, FEATURE2, FEATURE3);
+
+  public FakeProcessor() {
+    this(null);
+  }
+
+  public FakeProcessor(FeatureRegistry featureRegistry) {
+    super(null, null, null, null, null);
+    this.gadgets.put(FakeProcessor.SPEC_URL, FakeProcessor.SPEC_XML);
+    this.gadgets.put(FakeProcessor.SPEC_URL2, FakeProcessor.SPEC_XML2);
+    this.gadgets.put(FakeProcessor.SPEC_URL3, FakeProcessor.SPEC_XML3);
+    this.gadgets.put(FakeProcessor.SPEC_URL4, FakeProcessor.SPEC_XML4);
+    this.featureRegistry = featureRegistry;
+  }
+
+  @Override
+  public Gadget process(GadgetContext context) throws ProcessingException {
+
+    ProcessingException exception = exceptions.get(context.getUrl());
+    if (exception != null) {
+      throw exception;
+    }
+
+    try {
+      GadgetSpec spec = new GadgetSpec(context.getUrl(), gadgets.get(context.getUrl()));
+      View view = spec.getView(context.getView());
+      return new Gadget()
+          .setContext(context)
+          .setSpec(spec)
+          .setCurrentView(view)
+          .setGadgetFeatureRegistry(featureRegistry);
+    } catch (GadgetException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetRenderingServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetRenderingServletTest.java
new file mode 100644
index 0000000..82c0f9b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetRenderingServletTest.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.common.servlet.HttpServletResponseRecorder;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.render.Renderer;
+import org.apache.shindig.gadgets.render.RenderingResults;
+import org.apache.shindig.gadgets.uri.IframeUriManager;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import org.easymock.IMocksControl;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class GadgetRenderingServletTest {
+  private static final String NON_ASCII_STRING
+      = "Games, HQ, Mang\u00E1, Anime e tudo que um bom nerd ama";
+
+  private final IMocksControl control = EasyMock.createNiceControl();
+  private final HttpServletRequest request = makeRequestMock(this);
+  private final HttpServletResponse response = control.createMock(HttpServletResponse.class);
+  private final Renderer renderer = control.createMock(Renderer.class);
+  public final HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(response);
+  private final GadgetRenderingServlet servlet = new GadgetRenderingServlet();
+  private final IframeUriManager iframeUriManager = control.createMock(IframeUriManager.class);
+
+  @Before
+  public void setUpUrlGenerator() {
+    expect(iframeUriManager.validateRenderingUri(isA(Uri.class))).andReturn(UriStatus.VALID_UNVERSIONED);
+    expect(request.getRequestURL()).andReturn(new StringBuffer("http://foo.com"));
+    expect(request.getQueryString()).andReturn("?q=a");
+    servlet.setIframeUriManager(iframeUriManager);
+  }
+
+  @Test
+  public void dosHeaderRejected() throws Exception {
+    expect(request.getHeader(HttpRequest.DOS_PREVENTION_HEADER)).andReturn("foo");
+    control.replay();
+    servlet.doGet(request, recorder);
+
+    assertEquals(HttpServletResponse.SC_FORBIDDEN, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void renderWithTtl() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.ok("working"));
+    expect(request.getParameter(Param.REFRESH.getKey())).andReturn("120");
+    control.replay();
+    servlet.doGet(request, recorder);
+    assertEquals("private,max-age=120", recorder.getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void renderWithBadTtl() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.ok("working"));
+    expect(request.getParameter(Param.REFRESH.getKey())).andReturn("");
+    control.replay();
+    servlet.doGet(request, recorder);
+    assertEquals("private,max-age=300", recorder.getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void normalResponse() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.ok("working"));
+    control.replay();
+
+    servlet.doGet(request, recorder);
+
+    assertEquals(HttpServletResponse.SC_OK, recorder.getHttpStatusCode());
+    assertEquals("private,max-age=" + GadgetRenderingServlet.DEFAULT_CACHE_TTL,
+        recorder.getHeader("Cache-Control"));
+    assertEquals("working", recorder.getResponseAsString());
+  }
+
+  @Test
+  public void errorsPassedThrough() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.error("busted", HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
+    control.replay();
+
+    servlet.doGet(request, recorder);
+
+    assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, recorder.getHttpStatusCode());
+    assertNull("Cache-Control header passed where it should not be.",
+        recorder.getHeader("Cache-Control"));
+    assertEquals("busted", recorder.getResponseAsString());
+
+  }
+
+  @Test
+  public void errorsAreEscaped() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.error("busted<script>alert(document.domain)</script>",
+                HttpServletResponse.SC_INTERNAL_SERVER_ERROR));
+    control.replay();
+
+    servlet.doGet(request, recorder);
+
+    assertEquals("busted&lt;script&gt;alert(document.domain)&lt;/script&gt;",
+        recorder.getResponseAsString());
+
+    assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void outputEncodingIsUtf8() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.ok(NON_ASCII_STRING));
+    control.replay();
+
+    servlet.doGet(request, recorder);
+
+
+    assertEquals("UTF-8", recorder.getCharacterEncoding());
+    assertEquals("text/html", recorder.getContentType());
+    assertEquals(NON_ASCII_STRING, recorder.getResponseAsString());
+  }
+
+  @Test
+  public void refreshParameter_specified() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(request.getParameter("refresh")).andReturn("1000");
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.ok("working"));
+    control.replay();
+    servlet.doGet(request, recorder);
+    assertEquals("private,max-age=1000", recorder.getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void refreshParameter_default() throws Exception {
+    servlet.setRenderer(renderer);
+    expect(renderer.render(isA(GadgetContext.class)))
+        .andReturn(RenderingResults.ok("working"));
+    control.replay();
+    servlet.doGet(request, recorder);
+    assertEquals("private,max-age=300", recorder.getHeader("Cache-Control"));
+  }
+
+  private static HttpServletRequest makeRequestMock(GadgetRenderingServletTest testcase) {
+    HttpServletRequest req = testcase.control.createMock(HttpServletRequest.class);
+    expect(req.getScheme()).andReturn("http").anyTimes();
+    expect(req.getServerPort()).andReturn(80).anyTimes();
+    expect(req.getServerName()).andReturn("example.com").anyTimes();
+    expect(req.getRequestURI()).andReturn("/path").anyTimes();
+    return req;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java
new file mode 100644
index 0000000..0edbe3d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerServiceTest.java
@@ -0,0 +1,770 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenException;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.GadgetException.Code;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.js.JsException;
+import org.apache.shindig.gadgets.js.JsRequest;
+import org.apache.shindig.gadgets.js.JsRequestBuilder;
+import org.apache.shindig.gadgets.js.JsResponseBuilder;
+import org.apache.shindig.gadgets.js.JsServingPipeline;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.servlet.GadgetsHandlerApi.Feature;
+import org.apache.shindig.gadgets.uri.DefaultIframeUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager.ProxyUri;
+import org.apache.shindig.protocol.conversion.BeanDelegator;
+import org.apache.shindig.protocol.conversion.BeanFilter;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+
+public class GadgetsHandlerServiceTest extends EasyMockTestCase {
+
+  private static final String TOKEN = "<token data>";
+  private static final String OWNER = "<owner>";
+  private static final String VIEWER = "<viewer>";
+  private static final String CONTAINER = "container";
+  private static final Long CURRENT_TIME_MS = 123L;
+  private static final Long SPEC_REFRESH_INTERVAL_MS = 456L;
+  private static final Long METADATA_EXPIRY_TIME_MS = CURRENT_TIME_MS + SPEC_REFRESH_INTERVAL_MS;
+  private static final Long TOKEN_EXPIRY_TIME_MS = CURRENT_TIME_MS + 789L;
+  private static final Uri RESOURCE = Uri.parse("http://example.com/data");
+  private static final String FALLBACK = "http://example.com/data2";
+  private static final String RPC_SERVICE_1 = "rcp_service_1";
+  private static final String RPC_SERVICE_2 = "rpc_service_2";
+  private static final String RPC_SERVICE_3 = "rpc_service_3";
+
+  private final BeanDelegator delegator = new BeanDelegator(GadgetsHandlerService.API_CLASSES,
+          GadgetsHandlerService.ENUM_CONVERSION_MAP);
+
+  private final FakeTimeSource timeSource = new FakeTimeSource(CURRENT_TIME_MS);
+  private final FeatureRegistry mockRegistry = mock(FeatureRegistry.class);
+  private final FakeProcessor processor = new FakeProcessor(mockRegistry);
+  private final FakeIframeUriManager urlGenerator = new FakeIframeUriManager();
+  private final ProxyUriManager proxyUriManager = mock(ProxyUriManager.class);
+  private final JsUriManager jsUriManager = mock(JsUriManager.class);
+  private final ProxyHandler proxyHandler = mock(ProxyHandler.class);
+  private final CajaContentRewriter cajaContentRewriter = mock(CajaContentRewriter.class);
+  private final JsServingPipeline jsPipeline = mock(JsServingPipeline.class);
+  private final JsRequestBuilder jsRequestBuilder = new JsRequestBuilder(jsUriManager, null);
+  private final GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
+
+  private ContainerConfig config;
+  private FakeSecurityTokenCodec tokenCodec;
+  private GadgetsHandlerService gadgetHandler;
+  private GadgetsHandlerService gadgetHandlerWithAdmin;
+  private FeatureRegistryProvider featureRegistryProvider;
+
+  @Before
+  public void setUp() {
+    tokenCodec = new FakeSecurityTokenCodec();
+    featureRegistryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) throws GadgetException {
+        return mockRegistry;
+      }
+    };
+    config = createMock(ContainerConfig.class);
+    gadgetHandler = new GadgetsHandlerService(timeSource, processor, urlGenerator, tokenCodec,
+            proxyUriManager, jsUriManager, proxyHandler, jsPipeline, jsRequestBuilder,
+            SPEC_REFRESH_INTERVAL_MS, new BeanFilter(), cajaContentRewriter, gadgetAdminStore,
+            featureRegistryProvider, new ModuleIdManagerImpl(),config);
+    gadgetHandlerWithAdmin = new GadgetsHandlerService(timeSource, processor, urlGenerator,
+            tokenCodec, proxyUriManager, jsUriManager, proxyHandler, jsPipeline, jsRequestBuilder,
+            SPEC_REFRESH_INTERVAL_MS, new BeanFilter(), cajaContentRewriter, gadgetAdminStore,
+            featureRegistryProvider, new ModuleIdManagerImpl(),config);
+  }
+
+  // Next test verify that the API data classes are configured correctly.
+  // The mapping is done using reflection in runtime, so this test verify mapping is complete
+  // this test will prevent from not intended change to the API.
+  // DO NOT REMOVE TEST
+  @Test
+  public void testHandlerDataDelegation() throws Exception {
+    delegator.validate();
+  }
+
+  private void setupMockGadgetAdminStore(boolean isAllowed) {
+    EasyMock.expect(gadgetAdminStore.checkFeatureAdminInfo(EasyMock.isA(Gadget.class)))
+    .andReturn(isAllowed).anyTimes();
+    EasyMock.expect(gadgetAdminStore.getAdditionalRpcServiceIds(EasyMock.isA(Gadget.class)))
+    .andReturn(Sets.newHashSet(RPC_SERVICE_3));
+  }
+
+  @SuppressWarnings("unchecked")
+  private void setupMockRegistry(List<String> features) {
+    EasyMock.expect(mockRegistry.getFeatures(EasyMock.isA(Collection.class)))
+            .andReturn(Lists.newArrayList(features)).anyTimes();
+    FeatureBundle featureBundle = createMockFeatureBundle();
+    FeatureRegistry.LookupResult lr = createMockLookupResult(ImmutableList.of(featureBundle));
+    EasyMock.expect(
+            mockRegistry.getFeatureResources(isA(GadgetContext.class),
+                    eq(Lists.newArrayList(features)), EasyMock.<List<String>> isNull()))
+            .andReturn(lr).anyTimes();
+    replay();
+  }
+
+  private FeatureBundle createMockFeatureBundle() {
+    FeatureBundle result = createMock(FeatureBundle.class);
+    expect(result.getApis(ApiDirective.Type.RPC, false)).andReturn(
+            Lists.newArrayList(RPC_SERVICE_1, RPC_SERVICE_2)).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureRegistry.LookupResult createMockLookupResult(List<FeatureBundle> featureBundles) {
+    FeatureRegistry.LookupResult result = createMock(FeatureRegistry.LookupResult.class);
+    EasyMock.expect(result.getBundles()).andReturn(featureBundles).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  @Test
+  public void testGetMetadata() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL,
+            CONTAINER, "default", createAuthContext(null, null), ImmutableList.of("*"));
+    setupMockGadgetAdminStore(true);
+    setupMockRegistry(ImmutableList.<String> of("auth-refresh"));
+    GadgetsHandlerApi.MetadataResponse response = gadgetHandler.getMetadata(request);
+    assertEquals(FakeIframeUriManager.IFRAME_URIS, response.getIframeUrls());
+    assertTrue(response.getNeedsTokenRefresh());
+    assertEquals(1, response.getViews().size());
+    assertEquals(FakeProcessor.SPEC_TITLE, response.getModulePrefs().getTitle());
+    assertEquals(FakeProcessor.LINK_HREF,
+            response.getModulePrefs().getLinks().get(FakeProcessor.LINK_REL).getHref().toString());
+    assertEquals(FakeProcessor.LINK_REL,
+            response.getModulePrefs().getLinks().get(FakeProcessor.LINK_REL).getRel());
+    assertEquals(1, response.getUserPrefs().size());
+    assertEquals("up_one", response.getUserPrefs().get("up_one").getDisplayName());
+    assertEquals(4, response.getUserPrefs().get("up_one").getOrderedEnumValues().size());
+    assertEquals(CURRENT_TIME_MS, response.getResponseTimeMs());
+    assertEquals(METADATA_EXPIRY_TIME_MS, response.getExpireTimeMs());
+    assertEquals(Sets.newHashSet(RPC_SERVICE_1, RPC_SERVICE_2, RPC_SERVICE_3), response.getRpcServiceIds());
+    verify();
+  }
+
+  @Test
+  public void testGetMetadataWithalwaysAppendST() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL,
+            CONTAINER, "default", createAuthContext(null, null), ImmutableList.of("*"));
+    setupMockGadgetAdminStore(true);
+    setupMockRegistry(ImmutableList.<String> of(""));
+    expect(config.getBool(CONTAINER, DefaultIframeUriManager.SECURITY_TOKEN_ALWAYS_KEY)).andReturn(
+            true).once();
+    replay(config);
+    GadgetsHandlerApi.MetadataResponse response = gadgetHandler.getMetadata(request);
+    assertTrue(response.getNeedsTokenRefresh());
+    verify();
+  }
+
+  @Test
+  public void testFeatureAdminAllowedGadget() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL4,
+            CONTAINER, "default", createAuthContext(null, null), ImmutableList.of("*"));
+    setupMockGadgetAdminStore(true);
+    setupMockRegistry(Lists.newArrayList("example-feature", "example-feature2"));
+
+    GadgetsHandlerApi.MetadataResponse response = gadgetHandlerWithAdmin.getMetadata(request);
+    assertEquals(FakeIframeUriManager.IFRAME_URIS, response.getIframeUrls());
+    assertEquals(1, response.getViews().size());
+    assertEquals(FakeProcessor.SPEC_TITLE, response.getModulePrefs().getTitle());
+    assertEquals(FakeProcessor.LINK_HREF,
+            response.getModulePrefs().getLinks().get(FakeProcessor.LINK_REL).getHref().toString());
+    assertEquals(FakeProcessor.LINK_REL,
+            response.getModulePrefs().getLinks().get(FakeProcessor.LINK_REL).getRel());
+    assertEquals(1, response.getUserPrefs().size());
+    assertEquals("up_one", response.getUserPrefs().get("up_one").getDisplayName());
+    assertEquals(4, response.getUserPrefs().get("up_one").getOrderedEnumValues().size());
+    assertEquals(CURRENT_TIME_MS, response.getResponseTimeMs());
+    assertEquals(METADATA_EXPIRY_TIME_MS, response.getExpireTimeMs());
+    assertEquals(Sets.newHashSet(RPC_SERVICE_1, RPC_SERVICE_2, RPC_SERVICE_3), response.getRpcServiceIds());
+    verify();
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testFeatureAdminDeniedGadget() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL4,
+            CONTAINER, "default", createAuthContext(null, null), ImmutableList.of("*"));
+    setupMockGadgetAdminStore(false);
+    setupMockRegistry(Lists.newArrayList("example-feature", "example-feature2"));
+    gadgetHandlerWithAdmin.getMetadata(request);
+  }
+
+  @Test
+  public void testGetMetadataOnlyView() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL,
+            CONTAINER, null, createAuthContext(null, null), ImmutableList.of("views.*"));
+    setupMockGadgetAdminStore(false);
+    setupMockRegistry(new ArrayList<String>());
+    GadgetsHandlerApi.MetadataResponse response = gadgetHandler.getMetadata(request);
+    assertNull(response.getIframeUrls());
+    assertNull(response.getUserPrefs());
+    assertNull(response.getModulePrefs());
+    assertNull(response.getUrl());
+    assertEquals(1, response.getViews().size());
+    verify();
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetMetadataNoView() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL3,
+            CONTAINER, "invalid_view", createAuthContext(null, null), ImmutableList.of("*"));
+    replay();
+    gadgetHandler.getMetadata(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetMetadataNoContainer() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL, null,
+            null, createAuthContext(null, null), ImmutableList.of("*"));
+    replay();
+    gadgetHandler.getMetadata(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetMetadataNoUrl() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(null, CONTAINER, null,
+            createAuthContext(null, null), ImmutableList.of("*"));
+    replay();
+    gadgetHandler.getMetadata(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetMetadataNoFields() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL,
+            CONTAINER, null, createAuthContext(null, null), null);
+    replay();
+    gadgetHandler.getMetadata(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetMetadataBadGadget() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(Uri.parse("unknown"),
+            CONTAINER, null, createAuthContext(null, null), null);
+    replay();
+    gadgetHandler.getMetadata(request);
+  }
+
+  @Test
+  public void testGetMetadataNoToken() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL,
+            CONTAINER, "default", null, ImmutableList.of("*"));
+    setupMockGadgetAdminStore(true);
+    setupMockRegistry(Lists.newArrayList("auth-refresh"));
+    GadgetsHandlerApi.MetadataResponse response = gadgetHandler.getMetadata(request);
+    assertEquals(FakeIframeUriManager.IFRAME_URIS, response.getIframeUrls());
+    verify();
+  }
+
+  @Test
+  public void testGetMetadataWithParams() throws Exception {
+    GadgetsHandlerApi.MetadataRequest request = createMetadataRequest(FakeProcessor.SPEC_URL4,
+            CONTAINER, "default", createAuthContext(null, null), ImmutableList.of("*"));
+    setupMockGadgetAdminStore(true);
+    setupMockRegistry(Lists.newArrayList("auth-refresh"));
+    GadgetsHandlerApi.MetadataResponse response = gadgetHandler.getMetadata(request);
+
+    Map<String, Feature> features = response.getModulePrefs().getFeatures();
+    // make sure that the feature set contains all the features, and no extra features
+    // Note that the core feature is automatically included.
+    assertTrue(features.containsKey(FakeProcessor.FEATURE1)
+            && features.containsKey(FakeProcessor.FEATURE2)
+            && features.containsKey(FakeProcessor.FEATURE3) && features.size() == 3);
+    Multimap<String, String> params1 = features.get(FakeProcessor.FEATURE2).getParams();
+    assertEquals(ImmutableList.of(FakeProcessor.PARAM_VALUE, FakeProcessor.PARAM_VALUE2),
+            params1.get(FakeProcessor.PARAM_NAME));
+    Multimap<String, String> params2 = features.get(FakeProcessor.FEATURE3).getParams();
+    assertEquals(ImmutableList.of(FakeProcessor.PARAM_VALUE3),
+            params2.get(FakeProcessor.PARAM_NAME2));
+
+    verify();
+  }
+
+  @Test
+  public void testGetToken() throws Exception {
+    GadgetsHandlerApi.TokenRequest request = createTokenRequest(FakeProcessor.SPEC_URL, CONTAINER,
+            createAuthContext(OWNER, VIEWER), ImmutableList.of("*"));
+    replay();
+    tokenCodec.encodedToken = TOKEN;
+    GadgetsHandlerApi.TokenResponse response = gadgetHandler.getToken(request);
+    assertEquals(TOKEN, response.getToken());
+    assertEquals(CURRENT_TIME_MS, response.getResponseTimeMs());
+    assertEquals(TOKEN_EXPIRY_TIME_MS, response.getExpireTimeMs());
+    assertEquals(OWNER, tokenCodec.authContext.getOwnerId());
+    assertEquals(VIEWER, tokenCodec.authContext.getViewerId());
+    assertEquals(CONTAINER, tokenCodec.authContext.getContainer());
+    verify();
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetTokenNoContainer() throws Exception {
+    GadgetsHandlerApi.TokenRequest request = createTokenRequest(FakeProcessor.SPEC_URL, null,
+            createAuthContext(OWNER, VIEWER), ImmutableList.of("*"));
+    replay();
+    gadgetHandler.getToken(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetTokenNoUrl() throws Exception {
+    GadgetsHandlerApi.TokenRequest request = createTokenRequest(null, CONTAINER,
+            createAuthContext(OWNER, VIEWER), ImmutableList.of("*"));
+    replay();
+    gadgetHandler.getToken(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetTokenNoFields() throws Exception {
+    GadgetsHandlerApi.TokenRequest request = createTokenRequest(FakeProcessor.SPEC_URL, CONTAINER,
+            createAuthContext(OWNER, VIEWER), null);
+    replay();
+    gadgetHandler.getToken(request);
+  }
+
+  @Test(expected = SecurityTokenException.class)
+  public void testGetTokenException() throws Exception {
+    GadgetsHandlerApi.TokenRequest request = createTokenRequest(FakeProcessor.SPEC_URL, CONTAINER,
+            createAuthContext(OWNER, VIEWER), ImmutableList.of("*"));
+    replay();
+    tokenCodec.exc = new SecurityTokenException("bad data");
+    gadgetHandler.getToken(request);
+  }
+
+  @Test
+  public void testGetTokenNoToken() throws Exception {
+    GadgetsHandlerApi.TokenRequest request = createTokenRequest(FakeProcessor.SPEC_URL, CONTAINER,
+            null, ImmutableList.of("*"));
+    replay();
+    tokenCodec.encodedToken = TOKEN;
+    GadgetsHandlerApi.TokenResponse response = gadgetHandler.getToken(request);
+    assertEquals(TOKEN, response.getToken());
+    assertNull(CONTAINER, tokenCodec.authContext);
+    verify();
+  }
+
+  @Test
+  public void testCreateJsResponse() throws Exception {
+    Uri jsUri = Uri.parse("http://www.shindig.com/js");
+    String content = "content";
+    GadgetsHandlerApi.JsResponse jsResponse = gadgetHandler.createJsResponse(null, jsUri, content,
+            ImmutableSet.of("*"), null);
+    BeanDelegator.validateDelegator(jsResponse);
+  }
+
+  @Test
+  public void testGetJsUri() throws Exception {
+    List<String> fields = ImmutableList.of("jsurl");
+    List<String> features = ImmutableList.of("rpc");
+    Uri resUri = Uri.parse("server.com/gadgets/js/rpc");
+    GadgetsHandlerApi.JsRequest request = createJsRequest(null, CONTAINER, fields, features, null);
+    Capture<JsUri> uriCapture = new Capture<JsUri>();
+    expect(jsUriManager.makeExternJsUri(capture(uriCapture))).andReturn(resUri);
+    replay();
+
+    GadgetsHandlerApi.JsResponse response = gadgetHandler.getJs(request);
+    JsUri expectedUri = new JsUri(null, false, false, CONTAINER, null, features, null, null, false,
+            false, RenderingContext.GADGET, null, null);
+    assertEquals(expectedUri, uriCapture.getValue());
+    assertEquals(resUri, response.getJsUrl());
+    assertNull(response.getJsContent());
+    assertEquals(timeSource.currentTimeMillis() + HttpUtil.getDefaultTtl() * 1000, response
+            .getExpireTimeMs().longValue());
+    verify();
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testJsNoContainer() throws Exception {
+    List<String> fields = ImmutableList.of("*");
+    GadgetsHandlerApi.JsRequest request = createJsRequest(null, null, fields,
+            ImmutableList.of("rpc"), null);
+    gadgetHandler.getJs(request);
+  }
+
+  @Test
+  public void testGetJsData() throws Exception {
+    List<String> fields = ImmutableList.of("jscontent");
+    List<String> features = ImmutableList.of("rpc");
+    Uri resUri = Uri.parse("http://server.com/gadgets/js/rpc");
+    Capture<JsUri> uriCapture = new Capture<JsUri>();
+    String jsContent = "var a;";
+    String onload = "do this";
+    String repository = "v01";
+    expect(jsUriManager.makeExternJsUri(capture(uriCapture))).andReturn(resUri);
+    expect(jsPipeline.execute(EasyMock.isA(JsRequest.class))).andReturn(
+            new JsResponseBuilder().appendJs(jsContent, "js").setProxyCacheable(true).build());
+    GadgetsHandlerApi.JsRequest request = createJsRequest(FakeProcessor.SPEC_URL.toString(),
+            CONTAINER, fields, features, repository);
+    expect(request.getOnload()).andStubReturn(onload);
+    expect(request.getContext()).andStubReturn(GadgetsHandlerApi.RenderingContext.CONTAINER);
+    replay();
+
+    GadgetsHandlerApi.JsResponse response = gadgetHandler.getJs(request);
+    JsUri expectedUri = new JsUri(null, false, false, CONTAINER, FakeProcessor.SPEC_URL.toString(),
+            features, null, onload, false, false, RenderingContext.CONTAINER, null, repository);
+    assertEquals(expectedUri, uriCapture.getValue());
+    assertNull(response.getJsUrl());
+    assertEquals(jsContent, response.getJsContent());
+    assertEquals(timeSource.currentTimeMillis() + HttpUtil.getDefaultTtl() * 1000, response
+            .getExpireTimeMs().longValue());
+    verify();
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetJsDataWithException() throws Exception {
+    List<String> fields = ImmutableList.of("jscontent");
+    List<String> features = ImmutableList.of("unknown");
+    Uri resUri = Uri.parse("http://server.com/gadgets/js/foo");
+    Capture<JsUri> uriCapture = new Capture<JsUri>();
+    expect(jsUriManager.makeExternJsUri(capture(uriCapture))).andReturn(resUri);
+    expect(jsPipeline.execute(EasyMock.isA(JsRequest.class))).andThrow(
+            new JsException(404, "error"));
+    GadgetsHandlerApi.JsRequest request = createJsRequest(FakeProcessor.SPEC_URL.toString(),
+            CONTAINER, fields, features, null);
+    expect(request.getOnload()).andStubReturn("do this");
+    expect(request.getContext()).andStubReturn(GadgetsHandlerApi.RenderingContext.CONTAINER);
+    replay();
+
+    gadgetHandler.getJs(request);
+  }
+
+  @Test
+  public void testCreateProxyUri() throws Exception {
+    GadgetsHandlerApi.ImageParams image = mock(GadgetsHandlerApi.ImageParams.class);
+    expect(image.getDoNotExpand()).andStubReturn(true);
+    expect(image.getHeight()).andStubReturn(120);
+    expect(image.getWidth()).andStubReturn(210);
+    expect(image.getQuality()).andStubReturn(77);
+
+    GadgetsHandlerApi.ProxyRequest request = mock(GadgetsHandlerApi.ProxyRequest.class);
+    expect(request.getContainer()).andStubReturn(CONTAINER);
+    expect(request.getUrl()).andStubReturn(RESOURCE);
+    expect(request.getRefresh()).andStubReturn(new Integer(333));
+    expect(request.getDebug()).andStubReturn(true);
+    expect(request.getFallbackUrl()).andStubReturn(FALLBACK);
+    expect(request.getGadget()).andStubReturn(FakeProcessor.SPEC_URL.toString());
+    expect(request.getIgnoreCache()).andStubReturn(true);
+    expect(request.getImageParams()).andStubReturn(image);
+    expect(request.getRewriteMimeType()).andStubReturn("image/png");
+    expect(request.getSanitize()).andStubReturn(true);
+    replay();
+    ProxyUri pUri = gadgetHandler.createProxyUri(request);
+
+    ProxyUri expectedUri = new ProxyUri(333, true, true, CONTAINER,
+            FakeProcessor.SPEC_URL.toString(), RESOURCE);
+    expectedUri.setRewriteMimeType("image/png").setSanitizeContent(true);
+    expectedUri.setResize(210, 120, 77, true).setFallbackUrl(FALLBACK);
+    assertEquals(pUri, expectedUri);
+    verify();
+  }
+
+  @Test
+  public void testValidateProxyResponse() throws Exception {
+    GadgetsHandlerApi.ProxyResponse response = gadgetHandler.createProxyResponse(RESOURCE, null,
+            ImmutableSet.<String> of("*"), 1000001L);
+
+    BeanDelegator.validateDelegator(response);
+    assertEquals(RESOURCE, response.getProxyUrl());
+    assertNull(response.getProxyContent());
+  }
+
+  @Test
+  public void testCreateProxyResponse() throws Exception {
+    HttpResponseBuilder httpResponse = new HttpResponseBuilder();
+    httpResponse.setContent("Content");
+    httpResponse.addHeader("header", "hval");
+    httpResponse.setEncoding(Charset.forName("UTF8"));
+    httpResponse.setHttpStatusCode(404);
+
+    GadgetsHandlerApi.ProxyResponse response = gadgetHandler.createProxyResponse(RESOURCE,
+            httpResponse.create(), ImmutableSet.<String> of("*"), 1000001L);
+    BeanDelegator.validateDelegator(response);
+    assertEquals("Content",
+            new String(Base64.decodeBase64(response.getProxyContent().getContentBase64())));
+    assertEquals(404, response.getProxyContent().getCode());
+    assertEquals(2, response.getProxyContent().getHeaders().size());
+    assertEquals("Date", response.getProxyContent().getHeaders().get(0).getName());
+    assertEquals("header", response.getProxyContent().getHeaders().get(1).getName());
+    assertEquals("hval", response.getProxyContent().getHeaders().get(1).getValue());
+    assertEquals(1000001L, response.getExpireTimeMs().longValue());
+  }
+
+  @Test
+  public void testFilterProxyResponse() throws Exception {
+    HttpResponse httpResponse = new HttpResponse("data");
+    GadgetsHandlerApi.ProxyResponse response = gadgetHandler.createProxyResponse(RESOURCE,
+            httpResponse, ImmutableSet.<String> of("proxyurl"), 1000001L);
+    assertNull(response.getProxyContent());
+    assertEquals(RESOURCE, response.getProxyUrl());
+  }
+
+  @Test
+  public void testGetProxySimple() throws Exception {
+    List<String> fields = ImmutableList.of("proxyurl");
+    Uri resUri = Uri.parse("server.com/gadgets/proxy?url=" + RESOURCE);
+    GadgetsHandlerApi.ProxyRequest request = createProxyRequest(RESOURCE, CONTAINER, fields);
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    expect(proxyUriManager.make(capture(uriCapture), EasyMock.anyInt())).andReturn(
+            ImmutableList.of(resUri));
+    replay();
+    GadgetsHandlerApi.ProxyResponse response = gadgetHandler.getProxy(request);
+    assertEquals(1, uriCapture.getValue().size());
+    ProxyUri pUri = uriCapture.getValue().get(0);
+    assertEquals(CONTAINER, pUri.getContainer());
+    assertEquals(resUri, response.getProxyUrl());
+    assertNull(response.getProxyContent());
+    assertEquals(timeSource.currentTimeMillis() + HttpUtil.getDefaultTtl() * 1000, response
+            .getExpireTimeMs().longValue());
+    verify();
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetProxyNoContainer() throws Exception {
+    List<String> fields = ImmutableList.of("*");
+    GadgetsHandlerApi.ProxyRequest request = createProxyRequest(RESOURCE, null, fields);
+    gadgetHandler.getProxy(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetProxyNoResource() throws Exception {
+    List<String> fields = ImmutableList.of("*");
+    GadgetsHandlerApi.ProxyRequest request = createProxyRequest(null, CONTAINER, fields);
+    gadgetHandler.getProxy(request);
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetProxyNoFields() throws Exception {
+    GadgetsHandlerApi.ProxyRequest request = createProxyRequest(RESOURCE, CONTAINER, null);
+    gadgetHandler.getProxy(request);
+  }
+
+  @Test
+  public void testGetProxyData() throws Exception {
+    List<String> fields = ImmutableList.of("proxycontent.*");
+    Uri resUri = Uri.parse("server.com/gadgets/proxy?url=" + RESOURCE);
+    GadgetsHandlerApi.ProxyRequest request = createProxyRequest(RESOURCE, CONTAINER, fields);
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    expect(proxyUriManager.make(capture(uriCapture), EasyMock.anyInt())).andReturn(
+            ImmutableList.of(resUri));
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+    builder.setExpirationTime(20000).setContent("response");
+    HttpResponse httpResponse = builder.create();
+    expect(proxyHandler.fetch(EasyMock.isA(ProxyUri.class))).andReturn(httpResponse);
+    replay();
+    GadgetsHandlerApi.ProxyResponse response = gadgetHandler.getProxy(request);
+    assertEquals(1, uriCapture.getValue().size());
+    ProxyUri pUri = uriCapture.getValue().get(0);
+    assertEquals(CONTAINER, pUri.getContainer());
+    assertNull(response.getProxyUrl());
+    assertEquals("response",
+            new String(Base64.decodeBase64(response.getProxyContent().getContentBase64())));
+    assertEquals(20000L, response.getExpireTimeMs().longValue());
+    verify();
+  }
+
+  @Test
+  public void testGetProxyEmptyData() throws Exception {
+    List<String> fields = ImmutableList.of("proxycontent.*");
+    Uri resUri = Uri.parse("server.com/gadgets/proxy?url=" + RESOURCE);
+    GadgetsHandlerApi.ProxyRequest request = createProxyRequest(RESOURCE, CONTAINER, fields);
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    expect(proxyUriManager.make(capture(uriCapture), EasyMock.anyInt())).andReturn(
+            ImmutableList.of(resUri));
+    HttpResponse httpResponse = new HttpResponseBuilder().setHttpStatusCode(504).create();
+    expect(proxyHandler.fetch(EasyMock.isA(ProxyUri.class))).andReturn(httpResponse);
+    replay();
+    GadgetsHandlerApi.ProxyResponse response = gadgetHandler.getProxy(request);
+    assertEquals(1, uriCapture.getValue().size());
+    ProxyUri pUri = uriCapture.getValue().get(0);
+    assertEquals(CONTAINER, pUri.getContainer());
+    assertNull(response.getProxyUrl());
+    assertEquals("", response.getProxyContent().getContentBase64());
+    assertEquals(504, response.getProxyContent().getCode());
+    verify();
+  }
+
+  @Test(expected = ProcessingException.class)
+  public void testGetProxyDataFail() throws Exception {
+    List<String> fields = ImmutableList.of("proxycontent.*");
+    Uri resUri = Uri.parse("server.com/gadgets/proxy?url=" + RESOURCE);
+    GadgetsHandlerApi.ProxyRequest request = createProxyRequest(RESOURCE, CONTAINER, fields);
+    Capture<List<ProxyUri>> uriCapture = new Capture<List<ProxyUri>>();
+    expect(proxyUriManager.make(capture(uriCapture), EasyMock.anyInt())).andReturn(
+            ImmutableList.of(resUri));
+    new HttpResponse("response");
+    expect(proxyHandler.fetch(EasyMock.isA(ProxyUri.class))).andThrow(
+            new GadgetException(Code.FAILED_TO_RETRIEVE_CONTENT));
+    replay();
+    gadgetHandler.getProxy(request);
+  }
+
+  @Test
+  public void testCreateCajaResponse() throws Exception {
+    String goldenEntries[][] = { { "name1", "LINT", "msg1" }, { "name2", "LINT", "msg2" } };
+    List<GadgetsHandlerApi.Message> goldenMessages = Lists.newArrayList();
+
+    for (String[] goldenEntry : goldenEntries) {
+      GadgetsHandlerApi.Message m = mock(GadgetsHandlerApi.Message.class);
+      expect(m.getName()).andReturn(goldenEntry[0]);
+      expect(m.getLevel()).andReturn(GadgetsHandlerApi.MessageLevel.valueOf(goldenEntry[1]));
+      expect(m.getMessage()).andReturn(goldenEntry[2]);
+      goldenMessages.add(m);
+    }
+    replay();
+
+    Uri jsUri = Uri.parse("http://www.shindig.com/js");
+    GadgetsHandlerApi.CajaResponse jsResponse = gadgetHandler.createCajaResponse(jsUri, "html",
+            "js", goldenMessages, ImmutableSet.of("*"), null);
+    BeanDelegator.validateDelegator(jsResponse);
+
+    assertEquals("html", jsResponse.getHtml());
+    assertEquals("js", jsResponse.getJs());
+    List<GadgetsHandlerApi.Message> response = jsResponse.getMessages();
+    assertEquals(goldenMessages.size(), response.size());
+    for (int i = 0; i < response.size(); i++) {
+      assertEquals(goldenEntries[i][0], response.get(i).getName());
+      assertEquals(goldenEntries[i][1], response.get(i).getLevel().name());
+      assertEquals(goldenEntries[i][2], response.get(i).getMessage());
+    }
+  }
+
+  private GadgetsHandlerApi.AuthContext createAuthContext(String ownerId, String viewerId) {
+    GadgetsHandlerApi.AuthContext authContext = mock(GadgetsHandlerApi.AuthContext.class);
+    if (ownerId != null) {
+      EasyMock.expect(authContext.getOwnerId()).andReturn(ownerId).once();
+    }
+    if (viewerId != null) {
+      EasyMock.expect(authContext.getViewerId()).andReturn(viewerId).once();
+    }
+    EasyMock.expect(authContext.getExpiresAt()).andReturn(TOKEN_EXPIRY_TIME_MS).anyTimes();
+    return authContext;
+  }
+
+  private GadgetsHandlerApi.MetadataRequest createMetadataRequest(Uri url, String container,
+          String view, GadgetsHandlerApi.AuthContext authContext, List<String> fields) {
+    GadgetsHandlerApi.MetadataRequest request = mock(GadgetsHandlerApi.MetadataRequest.class);
+    EasyMock.expect(request.getFields()).andReturn(fields).anyTimes();
+    EasyMock.expect(request.getView()).andReturn(view).once();
+    EasyMock.expect(request.getUrl()).andReturn(url).anyTimes();
+    EasyMock.expect(request.getContainer()).andReturn(container).anyTimes();
+    EasyMock.expect(request.getAuthContext()).andReturn(authContext).once();
+
+    return request;
+  }
+
+  private GadgetsHandlerApi.TokenRequest createTokenRequest(Uri url, String container,
+          GadgetsHandlerApi.AuthContext authContext, List<String> fields) {
+    GadgetsHandlerApi.TokenRequest request = mock(GadgetsHandlerApi.TokenRequest.class);
+    EasyMock.expect(request.getFields()).andReturn(fields).anyTimes();
+    EasyMock.expect(request.getUrl()).andReturn(url).anyTimes();
+    EasyMock.expect(request.getContainer()).andReturn(container).anyTimes();
+    EasyMock.expect(request.getAuthContext()).andReturn(authContext).once();
+    return request;
+  }
+
+  private GadgetsHandlerApi.JsRequest createJsRequest(String gadget, String container,
+          List<String> fields, List<String> features, String repository) {
+    GadgetsHandlerApi.JsRequest request = mock(GadgetsHandlerApi.JsRequest.class);
+    EasyMock.expect(request.getFields()).andStubReturn(fields);
+    EasyMock.expect(request.getContainer()).andStubReturn(container);
+    EasyMock.expect(request.getGadget()).andStubReturn(gadget);
+    EasyMock.expect(request.getFeatures()).andStubReturn(features);
+    EasyMock.expect(request.getRepository()).andStubReturn(repository);
+    return request;
+  }
+
+  private GadgetsHandlerApi.ProxyRequest createProxyRequest(Uri url, String container,
+          List<String> fields) {
+    GadgetsHandlerApi.ProxyRequest request = mock(GadgetsHandlerApi.ProxyRequest.class);
+    EasyMock.expect(request.getFields()).andStubReturn(fields);
+    EasyMock.expect(request.getContainer()).andStubReturn(container);
+    EasyMock.expect(request.getUrl()).andStubReturn(url);
+    return request;
+  }
+
+  private class FakeSecurityTokenCodec implements SecurityTokenCodec {
+    public SecurityToken authContext = null;
+    public SecurityTokenException exc = null;
+    public String encodedToken = null;
+
+    public String encodeToken(SecurityToken authContext) throws SecurityTokenException {
+      this.authContext = authContext;
+      if (exc != null) {
+        throw exc;
+      }
+      return encodedToken;
+    }
+
+    public SecurityToken createToken(Map<String, String> tokenParameters)
+            throws SecurityTokenException {
+      if (exc != null) {
+        throw exc;
+      }
+      return authContext;
+    }
+
+    public int getTokenTimeToLive() {
+      return 0;  // Not used.
+    }
+
+    public int getTokenTimeToLive(String container) {
+      return 0;  // Not used.
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java
new file mode 100644
index 0000000..77ebd25
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/GadgetsHandlerTest.java
@@ -0,0 +1,840 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenException;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.testing.ImmediateExecutorService;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.features.ApiDirective;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistry.FeatureBundle;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.js.JsException;
+import org.apache.shindig.gadgets.js.JsRequest;
+import org.apache.shindig.gadgets.js.JsRequestBuilder;
+import org.apache.shindig.gadgets.js.JsResponseBuilder;
+import org.apache.shindig.gadgets.js.JsServingPipeline;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.servlet.CajaContentRewriter.CajoledResult;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager.ProxyUri;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.RpcHandler;
+import org.apache.shindig.protocol.conversion.BeanFilter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.lexer.ParseException;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.reporting.SimpleMessageQueue;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class GadgetsHandlerTest extends EasyMockTestCase {
+  private static final String GADGET1_URL = FakeProcessor.SPEC_URL.toString();
+  private static final String GADGET2_URL = FakeProcessor.SPEC_URL2.toString();
+  private static final Uri HTML_URL = Uri.parse("http://www.example.com/a.html");
+  private static final Uri JS_URL = Uri.parse("http://www.example.com/a.js");
+  private static final String CONTAINER = "container";
+  private static final String TOKEN = "_nekot_";
+  private static final Long SPEC_REFRESH_INTERVAL = 123L;
+  private static final String RPC_SERVICE_1 = "rcp_service_1";
+  private static final String RPC_SERVICE_2 = "rpc_service_2";
+  private static final String RPC_SERVICE_3 = "rpc_service_3";
+
+  private final FakeTimeSource timeSource = new FakeTimeSource();
+  private final FeatureRegistry mockRegistry = mock(FeatureRegistry.class);
+  private final FakeProcessor processor = new FakeProcessor(mockRegistry);
+  private final FakeIframeUriManager urlGenerator = new FakeIframeUriManager();
+  private final Map<String, FormDataItem> emptyFormItems = Collections.emptyMap();
+  private final ProxyUriManager proxyUriManager = mock(ProxyUriManager.class);
+  private final JsUriManager jsUriManager = mock(JsUriManager.class);
+  private final ProxyHandler proxyHandler = mock(ProxyHandler.class);
+  private final CajaContentRewriter cajaContentRewriter = mock(CajaContentRewriter.class);
+  private final JsServingPipeline jsPipeline = mock(JsServingPipeline.class);
+  private final JsRequestBuilder jsRequestBuilder = new JsRequestBuilder(jsUriManager, null);
+  private final GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
+  private final ContainerConfig config = mock(ContainerConfig.class);
+
+  private Injector injector;
+  private BeanJsonConverter converter;
+  private HandlerRegistry registry;
+  private FakeGadgetToken authContext;
+  private FeatureRegistryProvider featureRegistryProvider;
+
+  @Before
+  public void setUp() throws Exception {
+    injector = Guice.createInjector();
+    converter = new BeanJsonConverter(injector);
+    authContext = new FakeGadgetToken();
+    featureRegistryProvider = new FeatureRegistryProvider() {
+      public FeatureRegistry get(String repository) throws GadgetException {
+        return mockRegistry;
+      }
+    };
+    authContext.setAppUrl("http://www.example.com/gadget.xml");
+  }
+
+  private void registerGadgetsHandler(SecurityTokenCodec codec) {
+    BeanFilter beanFilter = new BeanFilter();
+
+    GadgetsHandlerService service = new GadgetsHandlerService(timeSource, processor, urlGenerator,
+            codec, proxyUriManager, jsUriManager, proxyHandler, jsPipeline, jsRequestBuilder,
+            SPEC_REFRESH_INTERVAL, beanFilter, cajaContentRewriter, gadgetAdminStore,
+            featureRegistryProvider, new ModuleIdManagerImpl(),config);
+    GadgetsHandler handler = new GadgetsHandler(new ImmediateExecutorService(), service, beanFilter);
+    registry = new DefaultHandlerRegistry(injector, converter,
+            new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object> of(handler));
+  }
+
+  private JSONObject makeMetadataRequest(String lang, String country, String[] fields,
+          String... uris) throws JSONException {
+    JSONObject req = new JSONObject()
+            .put("method", "gadgets.metadata")
+            .put("id", "req1")
+            .put("params",
+                    new JSONObject().put("ids", ImmutableList.copyOf(uris)).put("container",
+                            CONTAINER));
+    if (lang != null)
+      req.put("language", lang);
+    if (country != null)
+      req.put("country", country);
+    if (fields != null)
+      req.getJSONObject("params").put("fields", new JSONArray(fields));
+    return req;
+  }
+
+  private JSONObject makeMetadataNoContainerRequest(String... uris) throws JSONException {
+    JSONObject req = new JSONObject().put("method", "gadgets.metadata").put("id", "req1")
+            .put("params", new JSONObject().put("ids", ImmutableList.copyOf(uris)));
+    return req;
+  }
+
+  private JSONObject makeTokenRequest(String... uris) throws JSONException {
+    JSONObject req = new JSONObject()
+            .put("method", "gadgets.token")
+            .put("id", "req1")
+            .put("params",
+                    new JSONObject().put("ids", ImmutableList.copyOf(uris)).put("container",
+                            CONTAINER));
+    return req;
+  }
+
+  @SuppressWarnings("unchecked")
+  private void setupMockRegistry(List<String> features) {
+    EasyMock.expect(mockRegistry.getFeatures(EasyMock.isA(List.class)))
+            .andReturn(Lists.newArrayList(features)).anyTimes();
+    FeatureBundle featureBundle = createMockFeatureBundle();
+    FeatureRegistry.LookupResult lr = createMockLookupResult(ImmutableList.of(featureBundle));
+    EasyMock.expect(
+            mockRegistry.getFeatureResources(isA(GadgetContext.class),
+                    eq(Lists.newArrayList(features)), EasyMock.<List<String>> isNull()))
+            .andReturn(lr).anyTimes();
+    replay();
+  }
+
+  private void setupGadgetAdminStore() {
+    EasyMock.expect(gadgetAdminStore.checkFeatureAdminInfo(isA(Gadget.class)))
+    .andReturn(true).anyTimes();
+    EasyMock.expect(gadgetAdminStore.getAdditionalRpcServiceIds(isA(Gadget.class)))
+    .andReturn((Sets.newHashSet(RPC_SERVICE_3))).anyTimes();
+  }
+
+  private FeatureBundle createMockFeatureBundle() {
+    FeatureBundle result = createMock(FeatureBundle.class);
+    expect(result.getApis(ApiDirective.Type.RPC, false)).andReturn(
+            Lists.newArrayList(RPC_SERVICE_1, RPC_SERVICE_2)).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private FeatureRegistry.LookupResult createMockLookupResult(List<FeatureBundle> featureBundles) {
+    FeatureRegistry.LookupResult result = createMock(FeatureRegistry.LookupResult.class);
+    EasyMock.expect(result.getBundles()).andReturn(featureBundles).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  @Test
+  public void testMetadataEmptyRequest() throws Exception {
+    registerGadgetsHandler(null);
+    JSONObject request = makeMetadataRequest(null, null, null);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object empty = operation.execute(emptyFormItems, authContext, converter).get();
+    JsonAssert.assertJsonEquals("{}", converter.convertToString(empty));
+  }
+
+  @Test
+  public void testMetadataNoContainerRequest() throws Exception {
+    registerGadgetsHandler(null);
+    JSONObject request = makeMetadataNoContainerRequest(GADGET1_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    try {
+      operation.execute(emptyFormItems, authContext, converter).get();
+      fail("Missing container");
+    } catch (Exception e) {
+      assertTrue(e.getMessage().contains("Missing container"));
+    }
+  }
+
+  private JSONObject makeCajaRequest(String mime, String... uris) throws JSONException {
+    JSONObject params = new JSONObject().put("container", CONTAINER).put("ids",
+            ImmutableList.copyOf(uris));
+
+    if (null != mime) {
+      params.put("mime_type", mime);
+    }
+
+    return new JSONObject().put("id", "req1").put("method", "gadgets.cajole").put("params", params);
+  }
+
+  @Test
+  public void testCajaEmptyRequest() throws Exception {
+    registerGadgetsHandler(null);
+    JSONObject request = makeCajaRequest(null);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object empty = operation.execute(emptyFormItems, authContext, converter).get();
+    JsonAssert.assertJsonEquals("{}", converter.convertToString(empty));
+  }
+
+  private CajoledResult cajole(Uri uri, String mime, String content) throws ParseException {
+    CajaContentRewriter rw = new CajaContentRewriter(null, null, null, proxyUriManager);
+    InputSource is = new InputSource(uri.toJavaUri());
+    MessageQueue mq = new SimpleMessageQueue();
+    CharProducer cp = CharProducer.Factory.create(new StringReader(content), is);
+    return rw.rewrite(uri, CONTAINER, CajaContentRewriter.parse(is, cp, mime, mq), false, false);
+  }
+
+  @Test
+  public void testCajaJsRequest() throws Exception {
+    registerGadgetsHandler(null);
+    Capture<Uri> uriCapture = new Capture<Uri>();
+    Capture<String> containerCapture = new Capture<String>();
+    Capture<String> mimeCapture = new Capture<String>();
+
+    String goldenMime = "text/javascript";
+
+    CajoledResult golden = cajole(JS_URL, goldenMime, "alert('hi');");
+
+    EasyMock.expect(
+            cajaContentRewriter.rewrite(EasyMock.capture(uriCapture),
+                    EasyMock.capture(containerCapture), EasyMock.capture(mimeCapture),
+                    EasyMock.eq(true), EasyMock.anyBoolean())).andReturn(golden).anyTimes();
+    replay();
+
+    JSONObject request = makeCajaRequest(goldenMime, JS_URL.toString());
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object result = operation.execute(emptyFormItems, authContext, converter).get();
+
+    assertEquals(CONTAINER, containerCapture.getValue());
+    assertEquals(JS_URL, uriCapture.getValue());
+    assertTrue(mimeCapture.getValue().contains("javascript"));
+
+    JSONObject response = new JSONObject(converter.convertToString(result));
+    assertTrue(response.has(JS_URL.toString()));
+    JSONObject cajaResponse = response.getJSONObject(JS_URL.toString());
+
+    // Pure js url - no html produced
+    assertFalse(cajaResponse.has("html"));
+    assertTrue(cajaResponse.has("js"));
+    assertTrue(cajaResponse.has("messages"));
+    assertTrue(cajaResponse.getString("js").contains("alert"));
+    assertTrue(cajaResponse.getJSONArray("messages").length() > 0);
+  }
+
+  @Test
+  public void testCajaHtmlRequest() throws Exception {
+    registerGadgetsHandler(null);
+    Capture<Uri> uriCapture = new Capture<Uri>();
+    Capture<String> containerCapture = new Capture<String>();
+    Capture<String> mimeCapture = new Capture<String>();
+
+    String goldenMime = "text/html";
+
+    CajoledResult golden = cajole(HTML_URL, goldenMime,
+            "<b>hello</b>world<script>evilFunc1()</script><div onclick='evilFunc2'></div>");
+
+    EasyMock.expect(
+            cajaContentRewriter.rewrite(EasyMock.capture(uriCapture),
+                    EasyMock.capture(containerCapture), EasyMock.capture(mimeCapture),
+                    EasyMock.eq(true), EasyMock.anyBoolean())).andReturn(golden).anyTimes();
+    replay();
+
+    JSONObject request = makeCajaRequest(goldenMime, HTML_URL.toString());
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object result = operation.execute(emptyFormItems, authContext, converter).get();
+
+    assertEquals(CONTAINER, containerCapture.getValue());
+    assertEquals(HTML_URL, uriCapture.getValue());
+    assertTrue(mimeCapture.getValue().contains("html"));
+
+    JSONObject response = new JSONObject(converter.convertToString(result));
+    assertTrue(response.has(HTML_URL.toString()));
+
+    JSONObject cajaResponse = response.getJSONObject(HTML_URL.toString());
+    assertTrue(cajaResponse.has("html"));
+    assertTrue(cajaResponse.has("js"));
+    assertTrue(cajaResponse.has("messages"));
+
+    // HTML is sanitized
+    assertTrue(cajaResponse.getString("html").contains("<b>hello</b>world"));
+    assertFalse(cajaResponse.getString("html").contains("evilFunc"));
+    assertTrue(cajaResponse.getString("js").contains("evilFunc"));
+    assertTrue(cajaResponse.getJSONArray("messages").length() > 0);
+  }
+
+  @Test
+  public void testCajaES53Request() throws Exception {
+    registerGadgetsHandler(null);
+    JSONObject request = makeCajaRequest(null);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object empty = operation.execute(emptyFormItems, authContext, converter).get();
+    JsonAssert.assertJsonEquals("{}", converter.convertToString(empty));
+  }
+
+  @Test
+  public void testGetRenderingType() throws Exception {
+    assertEquals(GadgetsHandlerApi.RenderingType.DEFAULT, GadgetsHandler.getRenderingType(null));
+    assertEquals(GadgetsHandlerApi.RenderingType.SANITIZED,
+            GadgetsHandler.getRenderingType("sanitized"));
+    assertEquals(GadgetsHandlerApi.RenderingType.INLINE_CAJOLED,
+            GadgetsHandler.getRenderingType("inline_cajoled"));
+    try {
+      GadgetsHandler.getRenderingType("unknown");
+      fail("Should have failed");
+    } catch (ProcessingException e) {
+      // As expected
+    }
+  }
+
+  @Test
+  public void testTokenEmptyRequest() throws Exception {
+    registerGadgetsHandler(null);
+    JSONObject request = makeTokenRequest();
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object empty = operation.execute(emptyFormItems, authContext, converter).get();
+    JsonAssert.assertJsonEquals("{}", converter.convertToString(empty));
+  }
+
+  @Test
+  public void testMetadataInvalidUrl() throws Exception {
+    registerGadgetsHandler(null);
+    String badUrl = "[moo]";
+    JSONObject request = makeMetadataRequest(null, null, null, badUrl);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+    JSONObject gadget = response.getJSONObject(badUrl);
+    assertEquals("Bad url - " + badUrl, gadget.getJSONObject("error").getString("message"));
+    assertEquals(400, gadget.getJSONObject("error").getInt("code"));
+  }
+
+  @Test
+  public void testTokenInvalidUrl() throws Exception {
+    registerGadgetsHandler(null);
+    String badUrl = "[moo]";
+    JSONObject request = makeTokenRequest(badUrl);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+    JSONObject gadget = response.getJSONObject(badUrl);
+    assertEquals("Bad url - " + badUrl, gadget.getJSONObject("error").getString("message"));
+    assertEquals(400, gadget.getJSONObject("error").getInt("code"));
+  }
+
+  @Test
+  public void testMetadataOneGadget() throws Exception {
+    registerGadgetsHandler(null);
+    setupGadgetAdminStore();
+    setupMockRegistry(Lists.newArrayList("core"));
+    JSONObject request = makeMetadataRequest(null, null, null, GADGET1_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget = response.getJSONObject(GADGET1_URL);
+    JSONObject iframeUrls = gadget.getJSONObject("iframeUrls");
+    assertEquals(FakeIframeUriManager.DEFAULT_IFRAME_URI.toString(),
+            iframeUrls.getString("default"));
+    assertEquals(FakeProcessor.SPEC_TITLE, gadget.getJSONObject("modulePrefs").getString("title"));
+    assertFalse(gadget.has("error"));
+    assertFalse(gadget.has("url")); // filtered out
+    JSONObject view = gadget.getJSONObject("views").getJSONObject(GadgetSpec.DEFAULT_VIEW);
+    assertEquals(FakeProcessor.PREFERRED_HEIGHT, view.getInt("preferredHeight"));
+    assertEquals(FakeProcessor.PREFERRED_WIDTH, view.getInt("preferredWidth"));
+    assertEquals(FakeProcessor.LINK_HREF, gadget.getJSONObject("modulePrefs")
+            .getJSONObject("links").getJSONObject(FakeProcessor.LINK_REL).getString("href"));
+
+    JSONObject userPrefs = gadget.getJSONObject("userPrefs");
+    assertNotNull(userPrefs);
+
+    JSONObject userPrefData = userPrefs.getJSONObject("up_one");
+    assertNotNull(userPrefData);
+
+    JSONArray orderedEnums = userPrefData.getJSONArray("orderedEnumValues");
+    assertNotNull(orderedEnums);
+    assertEquals(4, orderedEnums.length());
+    assertEquals("disp1", orderedEnums.getJSONObject(0).getString("displayValue"));
+    assertEquals("val1", orderedEnums.getJSONObject(0).getString("value"));
+    assertEquals("disp2", orderedEnums.getJSONObject(1).getString("displayValue"));
+    assertEquals("abc", orderedEnums.getJSONObject(1).getString("value"));
+    assertEquals("disp3", orderedEnums.getJSONObject(2).getString("displayValue"));
+    assertEquals("z_xabc", orderedEnums.getJSONObject(2).getString("value"));
+    assertEquals("disp4", orderedEnums.getJSONObject(3).getString("displayValue"));
+    assertEquals("foo", orderedEnums.getJSONObject(3).getString("value"));
+
+    verify();
+  }
+
+  @Test
+  public void testMetadataOneGadgetRequestTokenTTLParam() throws Exception {
+    SecurityTokenCodec codec = createMock(SecurityTokenCodec.class);
+    expect(codec.getTokenTimeToLive(CONTAINER)).andReturn(42).anyTimes();
+    replay(codec);
+
+    registerGadgetsHandler(codec);
+    setupGadgetAdminStore();
+    setupMockRegistry(Lists.newArrayList("core"));
+    JSONObject request = makeMetadataRequest(null, null, new String[]{"tokenTTL", "iframeurl"}, GADGET1_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    assertEquals(42, response.getJSONObject(FakeProcessor.SPEC_URL.toString()).getInt("tokenTTL"));
+  }
+
+  @Test
+  public void testMetadataOneGadgetNoRequestTokenTTLParam() throws Exception {
+    registerGadgetsHandler(null);
+    setupGadgetAdminStore();
+    setupMockRegistry(Lists.newArrayList("core"));
+    JSONObject request = makeMetadataRequest(null, null, null, GADGET1_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    assertFalse(response.getJSONObject(FakeProcessor.SPEC_URL.toString()).has("tokenTTL"));
+  }
+
+  @Test
+  public void testAllowedRpcSecurityIds() throws Exception {
+    registerGadgetsHandler(null);
+    setupGadgetAdminStore();
+    setupMockRegistry(Lists.newArrayList("core"));
+    JSONObject request = makeMetadataRequest(null, null, new String[] { "rpcServiceIds" },
+            GADGET1_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget = response.getJSONObject(GADGET1_URL);
+
+    JSONArray rpcServiceIds = gadget.getJSONArray("rpcServiceIds");
+    assertEquals(3, rpcServiceIds.length());
+    boolean result = rpcServiceIds.get(0).equals(RPC_SERVICE_2) || rpcServiceIds.get(0).equals(RPC_SERVICE_1) || rpcServiceIds.get(0).equals(RPC_SERVICE_3);
+    result &= rpcServiceIds.get(1).equals(RPC_SERVICE_2) || rpcServiceIds.get(1).equals(RPC_SERVICE_1) || rpcServiceIds.get(1).equals(RPC_SERVICE_3);
+    result &= rpcServiceIds.get(2).equals(RPC_SERVICE_2) || rpcServiceIds.get(2).equals(RPC_SERVICE_1) || rpcServiceIds.get(2).equals(RPC_SERVICE_3);
+    assertTrue(result);
+
+    verify();
+  }
+
+  @Test
+  public void testTokenOneGadget() throws Exception {
+    SecurityTokenCodec codec = EasyMock.createMock(SecurityTokenCodec.class);
+    Capture<SecurityToken> authContextCapture = new Capture<SecurityToken>();
+    EasyMock.expect(codec.encodeToken(EasyMock.capture(authContextCapture))).andReturn(TOKEN)
+            .anyTimes();
+    replay(codec);
+
+    registerGadgetsHandler(codec);
+    JSONObject request = makeTokenRequest(GADGET1_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget = response.getJSONObject(GADGET1_URL);
+    assertEquals(TOKEN, gadget.getString("token"));
+    assertFalse(gadget.has("error"));
+    assertFalse(gadget.has("url")); // filtered out
+    // next checks verify all fiels that canbe used for token generation are passed in
+    assertEquals("container", authContextCapture.getValue().getContainer());
+    assertEquals(GADGET1_URL, authContextCapture.getValue().getAppId());
+    assertEquals(GADGET1_URL, authContextCapture.getValue().getAppUrl());
+    assertSame(authContext.getOwnerId(), authContextCapture.getValue().getOwnerId());
+    assertSame(authContext.getViewerId(), authContextCapture.getValue().getViewerId());
+  }
+
+  @Test
+  public void testMetadataOneGadgetFailure() throws Exception {
+    registerGadgetsHandler(null);
+    setupGadgetAdminStore();
+    replay();
+
+    JSONObject request = makeMetadataRequest(null, null, null, GADGET1_URL);
+    urlGenerator.throwRandomFault = true;
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget = response.getJSONObject(GADGET1_URL);
+    assertEquals(GadgetsHandler.FAILURE_METADATA, gadget.getJSONObject("error")
+            .getString("message"));
+    assertEquals(500, gadget.getJSONObject("error").getInt("code"));
+  }
+
+  @Test
+  public void testTokenOneGadgetFailure() throws Exception {
+    SecurityTokenCodec codec = EasyMock.createMock(SecurityTokenCodec.class);
+    EasyMock.expect(codec.encodeToken(EasyMock.isA(SecurityToken.class))).andThrow(
+            new SecurityTokenException("blah"));
+    replay(codec);
+
+    registerGadgetsHandler(codec);
+    JSONObject request = makeTokenRequest(GADGET1_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget = response.getJSONObject(GADGET1_URL);
+    assertFalse(gadget.has("token"));
+    assertEquals(GadgetsHandler.FAILURE_TOKEN, gadget.getJSONObject("error").getString("message"));
+    assertEquals(500, gadget.getJSONObject("error").getInt("code"));
+  }
+
+  @Test
+  public void testMetadataMultipleGadgets() throws Exception {
+    registerGadgetsHandler(null);
+    setupGadgetAdminStore();
+    setupMockRegistry(Lists.newArrayList("core"));
+    JSONObject request = makeMetadataRequest("en", "US", null, GADGET1_URL, GADGET2_URL);
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject modulePrefs1 = response.getJSONObject(GADGET1_URL).getJSONObject("modulePrefs");
+    assertEquals(FakeProcessor.SPEC_TITLE, modulePrefs1.getString("title"));
+
+    JSONObject modulePrefs2 = response.getJSONObject(GADGET2_URL).getJSONObject("modulePrefs");
+    assertEquals(FakeProcessor.SPEC_TITLE2, modulePrefs2.getString("title"));
+    verify();
+  }
+
+  @Test
+  public void testTokenMultipleGadgetsWithSuccessAndFailure() throws Exception {
+    SecurityTokenCodec codec = EasyMock.createMock(SecurityTokenCodec.class);
+    EasyMock.expect(codec.encodeToken(EasyMock.isA(SecurityToken.class))).andReturn(TOKEN);
+    EasyMock.expect(codec.encodeToken(EasyMock.isA(SecurityToken.class))).andThrow(
+            new SecurityTokenException("blah"));
+    replay(codec);
+
+    registerGadgetsHandler(codec);
+    JSONObject request = makeTokenRequest(GADGET1_URL, GADGET2_URL);
+
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget1 = response.getJSONObject(GADGET1_URL);
+    assertEquals(TOKEN, gadget1.getString("token"));
+    assertFalse(gadget1.has("error"));
+
+    JSONObject gadget2 = response.getJSONObject(GADGET2_URL);
+    assertFalse(gadget2.has("token"));
+    assertEquals(GadgetsHandler.FAILURE_TOKEN, gadget2.getJSONObject("error").getString("message"));
+    assertEquals(500, gadget2.getJSONObject("error").getInt("code"));
+  }
+
+  @Test
+  public void testMetadataMultipleGadgetsWithFailure() throws Exception {
+    registerGadgetsHandler(null);
+    setupGadgetAdminStore();
+    setupMockRegistry(Lists.newArrayList("core"));
+    JSONObject request = makeMetadataRequest("en", "US", null, GADGET1_URL, GADGET2_URL);
+    processor.exceptions.put(FakeProcessor.SPEC_URL2, new ProcessingException("broken",
+            HttpServletResponse.SC_BAD_REQUEST));
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject modulePrefs1 = response.getJSONObject(GADGET1_URL).getJSONObject("modulePrefs");
+    assertEquals(FakeProcessor.SPEC_TITLE, modulePrefs1.getString("title"));
+
+    JSONObject gadget2 = response.getJSONObject(GADGET2_URL);
+    assertNotNull("got gadget2", gadget2);
+    assertEquals("broken", // Processing exception message is used
+            gadget2.getJSONObject("error").getString("message"));
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, gadget2.getJSONObject("error").getInt("code"));
+    verify();
+  }
+
+  private JSONObject makeSimpleProxyRequest(String fields, String... uris) throws JSONException {
+    JSONObject params = new JSONObject().put("ids", ImmutableList.copyOf(uris)).put("container",
+            CONTAINER);
+    if (fields != null) {
+      params.put("fields", fields);
+    }
+    JSONObject req = new JSONObject().put("method", "gadgets.proxy").put("id", "req1")
+            .put("params", params);
+    return req;
+  }
+
+  @Test
+  public void testSimpleProxy() throws Exception {
+    registerGadgetsHandler(null);
+    String resUri = "http://example.com/data";
+    String proxyUri = "http://shindig.com/gadgets/proxy?url=" + resUri;
+    JSONObject request = makeSimpleProxyRequest(null, resUri);
+    Capture<List<ProxyUri>> captureProxyUri = new Capture<List<ProxyUri>>();
+    EasyMock.expect(
+            proxyUriManager.make(EasyMock.capture(captureProxyUri), EasyMock.isNull(Integer.class)))
+            .andReturn(ImmutableList.<Uri> of(Uri.parse(proxyUri)));
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget1 = response.getJSONObject(resUri);
+    assertEquals(proxyUri, gadget1.getString("proxyUrl"));
+    ProxyUri pUri = captureProxyUri.getValue().get(0);
+    ProxyUri expectedUri = new ProxyUri(null, false, false, CONTAINER, null, Uri.parse(resUri));
+    assertTrue(expectedUri.equals(pUri));
+    assertFalse(gadget1.has("error"));
+    verify();
+  }
+
+  private JSONObject makeSimpleJsRequest(String fields, List<String> features) throws JSONException {
+    JSONObject params = new JSONObject().put("gadget", GADGET1_URL).put("container", CONTAINER)
+            .put("features", features);
+    if (fields != null) {
+      params.put("fields", fields);
+    }
+    JSONObject req = new JSONObject().put("method", "gadgets.js").put("id", "req1")
+            .put("params", params);
+    return req;
+  }
+
+  @Test
+  public void testJsSimple() throws Exception {
+    registerGadgetsHandler(null);
+    List<String> features = ImmutableList.of("rpc", "io");
+    Uri jsUri = Uri.parse("http://shindig.com/gadgets/js/rpc:io");
+    JSONObject request = makeSimpleJsRequest(null, features);
+    Capture<JsUri> captureUri = new Capture<JsUri>();
+    EasyMock.expect(jsUriManager.makeExternJsUri(EasyMock.capture(captureUri))).andReturn(jsUri);
+    replay();
+
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject results = new JSONObject(converter.convertToString(responseObj));
+    assertEquals(jsUri.toString(), results.getString("jsUrl"));
+    JsUri expectedUri = new JsUri(null, false, false, CONTAINER, GADGET1_URL, features, null, null,
+            false, false, RenderingContext.GADGET, null, null);
+    assertEquals(expectedUri, captureUri.getValue());
+    assertFalse(results.has("error"));
+    assertFalse(results.has("jsContent"));
+    verify();
+  }
+
+  private JSONObject makeComplexJsRequest(List<String> features, List<String> loadedFeatures,
+          String onload, String repository) throws JSONException {
+    JSONObject params = new JSONObject().put("gadget", GADGET1_URL).put("container", CONTAINER)
+            .put("features", features).put("loadedFeatures", loadedFeatures).put("fields", "*")
+            .put("refresh", "123").put("debug", "1").put("nocache", "1").put("onload", onload)
+            .put("c", "1");
+    if (repository != null) {
+      params.put("r", repository);
+    }
+    JSONObject request = new JSONObject().put("method", "gadgets.js").put("id", "req1")
+            .put("params", params);
+    return request;
+  }
+
+  @Test
+  public void testJsData() throws Exception {
+    registerGadgetsHandler(null);
+    List<String> features = ImmutableList.of("rpc", "io");
+    List<String> loadedFeatures = ImmutableList.of("rpc");
+    Uri jsUri = Uri.parse("http://shindig.com/gadgets/js/rpc:io");
+    String onload = "do \"this\";";
+    String repository = "v01";
+
+    JSONObject request = makeComplexJsRequest(features, loadedFeatures, onload, repository);
+
+    Capture<JsUri> captureUri = new Capture<JsUri>();
+    EasyMock.expect(jsUriManager.makeExternJsUri(EasyMock.capture(captureUri))).andReturn(jsUri);
+    String jsContent = "var b=\"123\";";
+    EasyMock.expect(jsPipeline.execute(EasyMock.isA(JsRequest.class))).andReturn(
+            new JsResponseBuilder().appendJs(jsContent, "js").build());
+    replay();
+
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject results = new JSONObject(converter.convertToString(responseObj));
+    assertEquals(jsUri.toString(), results.getString("jsUrl"));
+    JsUri expectedUri = new JsUri(123, true, true, CONTAINER, GADGET1_URL, features,
+            loadedFeatures, onload, false, false, RenderingContext.CONTAINER, null, repository);
+    assertEquals(expectedUri, captureUri.getValue());
+    assertFalse(results.has("error"));
+    assertEquals(jsContent, results.getString("jsContent"));
+    verify();
+  }
+
+  @Test
+  public void testJsFailure() throws Exception {
+    registerGadgetsHandler(null);
+    List<String> features = ImmutableList.of("rpc2");
+    List<String> loadedFeatures = ImmutableList.of();
+    Uri jsUri = Uri.parse("http://shindig.com/gadgets/js/rpc:io");
+    String onload = "do \"this\";";
+
+    JSONObject request = makeComplexJsRequest(features, loadedFeatures, onload, null);
+
+    Capture<JsUri> captureUri = new Capture<JsUri>();
+    EasyMock.expect(jsUriManager.makeExternJsUri(EasyMock.capture(captureUri))).andReturn(jsUri);
+    EasyMock.expect(jsPipeline.execute(EasyMock.isA(JsRequest.class))).andThrow(
+            new JsException(404, "not found"));
+    replay();
+
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject results = new JSONObject(converter.convertToString(responseObj));
+    assertFalse(results.has("jsUrl"));
+    assertEquals(HttpResponse.SC_NOT_FOUND, results.getJSONObject("error").getInt("code"));
+    assertTrue(results.getJSONObject("error").getString("message").contains("not found"));
+    verify();
+  }
+
+  @Test
+  public void testSimpleProxyData() throws Exception {
+    registerGadgetsHandler(null);
+    String resUri = "http://example.com/data";
+    String proxyUri = "http://shindig.com/gadgets/proxy?url=" + resUri;
+    JSONObject request = makeSimpleProxyRequest("*", resUri);
+    Capture<List<ProxyUri>> captureProxyUri = new Capture<List<ProxyUri>>();
+    EasyMock.expect(
+            proxyUriManager.make(EasyMock.capture(captureProxyUri), EasyMock.isNull(Integer.class)))
+            .andReturn(ImmutableList.<Uri> of(Uri.parse(proxyUri)));
+    String responseData = "response data";
+    HttpResponse httpResponse = new HttpResponse(responseData);
+    EasyMock.expect(proxyHandler.fetch(EasyMock.isA(ProxyUri.class))).andReturn(httpResponse);
+    replay();
+
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget1 = response.getJSONObject(resUri);
+    assertEquals(proxyUri, gadget1.getString("proxyUrl"));
+    ProxyUri pUri = captureProxyUri.getValue().get(0);
+    ProxyUri expectedUri = new ProxyUri(null, false, false, CONTAINER, null, Uri.parse(resUri));
+    assertTrue(expectedUri.equals(pUri));
+    assertEquals(
+            responseData,
+            new String(Base64.decodeBase64(((JSONObject) gadget1.get("proxyContent")).getString(
+                    "contentBase64").getBytes())));
+    assertFalse(gadget1.has("error"));
+    verify();
+  }
+
+  private JSONObject makeComplexProxyRequest(String... uris) throws JSONException {
+    JSONObject req = new JSONObject()
+            .put("method", "gadgets.proxy")
+            .put("id", "req1")
+            .put("params",
+                    new JSONObject().put("ids", ImmutableList.copyOf(uris))
+                            .put("container", CONTAINER).put("nocache", "1").put("debug", "1")
+                            .put("sanitize", "true").put("gadget", GADGET1_URL)
+                            .put("refresh", "333").put("rewriteMime", "text/xml")
+                            .put("fallback_url", uris[0]).put("no_expand", "true")
+                            .put("resize_h", "444").put("resize_w", "555").put("resize_q", "88"));
+    return req;
+  }
+
+  @Test
+  public void testComplexProxy() throws Exception {
+    registerGadgetsHandler(null);
+    String resUri = "http://example.com/data";
+    String proxyUri = "http://shindig.com/gadgets/proxy?url=" + resUri;
+    JSONObject request = makeComplexProxyRequest(resUri);
+    Capture<List<ProxyUri>> captureProxyUri = new Capture<List<ProxyUri>>();
+    EasyMock.expect(
+            proxyUriManager.make(EasyMock.capture(captureProxyUri), EasyMock.isNull(Integer.class)))
+            .andReturn(ImmutableList.<Uri> of(Uri.parse(proxyUri)));
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+    Object responseObj = operation.execute(emptyFormItems, authContext, converter).get();
+    JSONObject response = new JSONObject(converter.convertToString(responseObj));
+
+    JSONObject gadget1 = response.getJSONObject(resUri);
+    assertEquals(proxyUri, gadget1.getString("proxyUrl"));
+    ProxyUri pUri = captureProxyUri.getValue().get(0);
+    ProxyUri expectedUri = new ProxyUri(333, true, true, CONTAINER, GADGET1_URL, Uri.parse(resUri));
+    expectedUri.setRewriteMimeType("text/xml").setSanitizeContent(true);
+    expectedUri.setFallbackUrl(resUri).setResize(555, 444, 88, true);
+    assertTrue(expectedUri.equals(pUri));
+    assertFalse(gadget1.has("error"));
+    verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HtmlAccelServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HtmlAccelServletTest.java
new file mode 100644
index 0000000..c9ba5e9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HtmlAccelServletTest.java
@@ -0,0 +1,440 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.rewrite.CaptureRewriter;
+import org.apache.shindig.gadgets.rewrite.DefaultResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.AccelUriManager;
+import org.apache.shindig.gadgets.uri.DefaultAccelUriManager;
+import org.apache.shindig.gadgets.uri.DefaultProxyUriManager;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.ServletInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Vector;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+
+public class HtmlAccelServletTest extends ServletTestFixture {
+
+  private final ContainerConfig config = new FakeContainerConfig();
+
+  private final AccelUriManager accelUriManager = new DefaultAccelUriManager(
+      config, new DefaultProxyUriManager(config, null));
+
+  private static class FakeContainerConfig extends BasicContainerConfig {
+    protected final Map<String, Object> data = ImmutableMap.<String, Object>builder()
+        .put(AccelUriManager.PROXY_HOST_PARAM, "apache.org")
+        .put(AccelUriManager.PROXY_PATH_PARAM, "/gadgets/accel")
+        .build();
+
+    @Override
+    public Object getProperty(String container, String name) {
+      return data.get(name);
+    }
+  }
+
+  private static class FakeCaptureRewriter extends CaptureRewriter {
+    String contentToRewrite;
+
+    public void setContentToRewrite(String s) {
+      contentToRewrite = s;
+    }
+    @Override
+    public void rewrite(HttpRequest request, HttpResponseBuilder original, Gadget gadget) {
+      super.rewrite(request, original, gadget);
+      if (!Strings.isNullOrEmpty(contentToRewrite)) {
+        original.setResponse(contentToRewrite.getBytes());
+      }
+    }
+  }
+
+  private static final String REWRITE_CONTENT = "working rewrite";
+  private static final String SERVLET = "/gadgets/accel";
+  private HtmlAccelServlet servlet;
+
+  @Before
+  public void setUp() throws Exception {
+
+    rewriter = new FakeCaptureRewriter();
+    rewriterRegistry = new DefaultResponseRewriterRegistry(
+        Arrays.<ResponseRewriter>asList(rewriter), null);
+    servlet = new HtmlAccelServlet();
+    servlet.setHandler(new AccelHandler(pipeline, rewriterRegistry,
+                                        accelUriManager, true));
+  }
+
+  private void expectRequest(String extraPath, String url) {
+    expect(request.getMethod()).andReturn("GET").anyTimes();
+    expect(request.getServletPath()).andReturn(SERVLET).anyTimes();
+    expect(request.getScheme()).andReturn("http").anyTimes();
+    expect(request.getServerName()).andReturn("apache.org").anyTimes();
+    expect(request.getServerPort()).andReturn(-1).anyTimes();
+    expect(request.getRequestURI()).andReturn(SERVLET + extraPath).anyTimes();
+    expect(request.getRequestURL())
+        .andReturn(new StringBuffer("apache.org" + SERVLET + extraPath))
+        .anyTimes();
+    String queryParams = (url == null ? "" : "url=" + url + "&container=accel"
+                                             + "&gadget=test");
+    expect(request.getQueryString()).andReturn(queryParams).anyTimes();
+    Vector<String> headerNames = new Vector<String>();
+    expect(request.getHeaderNames()).andReturn(headerNames.elements());
+  }
+
+  private void expectPostRequest(String extraPath, String url,
+                                 final String data)
+      throws IOException {
+    expect(request.getMethod()).andReturn("POST").anyTimes();
+    expect(request.getServletPath()).andReturn(SERVLET).anyTimes();
+    expect(request.getScheme()).andReturn("http").anyTimes();
+    expect(request.getServerName()).andReturn("apache.org").anyTimes();
+    expect(request.getServerPort()).andReturn(-1).anyTimes();
+    expect(request.getRequestURI()).andReturn(SERVLET + extraPath).anyTimes();
+    expect(request.getRequestURL())
+        .andReturn(new StringBuffer("apache.org" + SERVLET + extraPath))
+        .anyTimes();
+    String queryParams = (url == null ? "" : "url=" + url + "&container=accel"
+                                             + "&gadget=test");
+    expect(request.getQueryString()).andReturn(queryParams).anyTimes();
+
+    ServletInputStream inputStream = mock(ServletInputStream.class);
+    expect(request.getInputStream()).andReturn(inputStream);
+    expect(inputStream.read((byte[]) EasyMock.anyObject()))
+        .andAnswer(new IAnswer<Integer>() {
+          public Integer answer() throws Throwable {
+            byte[] byteArray = (byte[]) EasyMock.getCurrentArguments()[0];
+            System.arraycopy(data.getBytes(), 0, byteArray, 0, data.length());
+            return data.length();
+          }
+        });
+    expect(inputStream.read((byte[]) EasyMock.anyObject()))
+        .andReturn(-1);
+    Vector<String> headerNames = new Vector<String>();
+    expect(request.getHeaderNames()).andReturn(headerNames.elements());
+  }
+
+  @Test
+  public void testHtmlAccelNoData() throws Exception {
+    Uri url = Uri.parse("http://example.org/data.html");
+
+    HttpRequest req = new HttpRequest(url);
+    req.addHeader("Host", url.getAuthority());
+    expect(pipeline.execute(req)).andReturn(null).once();
+    expectRequest("", url.toString());
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(AccelHandler.ERROR_FETCHING_DATA,
+                 recorder.getResponseAsString());
+    assertEquals(404, recorder.getHttpStatusCode());
+  }
+
+  @Test
+  public void testHtmlAccelRewriteSimple() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>Hello World</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(REWRITE_CONTENT);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHttpStatusCode(200)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(REWRITE_CONTENT, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testHtmlAccelRewriteDoesNotFollowRedirects() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>Moved to new page</body></html>";
+    String redirectLocation = "http://example-redirected.org/data.html";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(data);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHeader("Location", redirectLocation)
+        .setHttpStatusCode(302)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(data, recorder.getResponseAsString());
+    assertEquals(redirectLocation, recorder.getHeader("Location"));
+    assertEquals(302, recorder.getHttpStatusCode());
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testHtmlAccelReturnsOriginal404MessageAndCode() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>This is error page</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(REWRITE_CONTENT);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHttpStatusCode(404)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(data, recorder.getResponseAsString());
+    assertEquals(404, recorder.getHttpStatusCode());
+    assertFalse(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testHtmlAccelRewriteInternalError() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>This is error page</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(data);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHttpStatusCode(500)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(data, recorder.getResponseAsString());
+    assertEquals(502, recorder.getHttpStatusCode());
+    assertFalse(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testHtmlAccelHandlesPost() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>This is error page</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(data);
+    Capture<HttpRequest> req = new Capture<HttpRequest>();
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .create();
+    expect(pipeline.execute(capture(req))).andReturn(resp).once();
+    expectPostRequest("", url, "hello=world");
+    replay();
+
+    servlet.doPost(request, recorder);
+    verify();
+    assertEquals(data, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertTrue(rewriter.responseWasRewritten());
+    assertEquals("POST", req.getValue().getMethod());
+    assertEquals("hello=world", req.getValue().getPostBodyAsString());
+  }
+
+  @Test
+  public void testHtmlAccelReturnsSameStatusCodeAndMessageWhenError() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>This is error page</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(data);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHttpStatusCode(5001)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(data, recorder.getResponseAsString());
+    assertEquals(5001, recorder.getHttpStatusCode());
+    assertFalse(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testSetCookieHeadersPassed() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>Hello World</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(REWRITE_CONTENT);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHeader("Set-Cookie", "name=value")
+        .setHeader("Set-Cookie2", "name2=value2")
+        .setHttpStatusCode(200)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertEquals(recorder.getHeader("Set-Cookie"), "name=value");
+    assertEquals(recorder.getHeader("Set-Cookie2"), "name2=value2");
+    assertEquals(REWRITE_CONTENT, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testCacheControlExpiresAndDateHeadersPassed() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>Hello World</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(REWRITE_CONTENT);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+
+    Map<String, String> headersMap = Maps.newHashMap();
+    headersMap.put("Set-Cookie", "name=value");
+    headersMap.put("Set-Cookie2", "name2=value2");
+    headersMap.put("Date", "Mon, 01 Jan 1970 00:00:00 GMT");
+    headersMap.put("Cache-Control", "private,max-age=10,no-transform,proxy-revalidate");
+    headersMap.put("Pragma", "no-cache");
+    headersMap.put("Expires", "123");
+
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .addHeaders(headersMap)
+        .setHeader("Content-Type", "text/html")
+        .setHttpStatusCode(200)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    for (Map.Entry<String, String> header : headersMap.entrySet()) {
+      assertEquals(recorder.getHeader(header.getKey()), header.getValue());
+    }
+    assertEquals(REWRITE_CONTENT, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testNoCacheControlHeaderSetIfAbsent() throws Exception {
+    String url = "http://example.org/data.html";
+    String data = "<html><body>Hello World</body></html>";
+
+    ((FakeCaptureRewriter) rewriter).setContentToRewrite(REWRITE_CONTENT);
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHttpStatusCode(200)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+    verify();
+    assertNull(recorder.getHeader("Cache-Control"));
+    assertEquals(REWRITE_CONTENT, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testReturnOriginalResponseIfRewritingFails() throws Exception {
+    ResponseRewriter throwingRewriter = new ResponseRewriter() {
+      public void rewrite(HttpRequest request, HttpResponseBuilder response, Gadget gadget)
+              throws RewritingException {
+        response.setContent(REWRITE_CONTENT);
+        throw new RewritingException("", 404);
+      }
+    };
+    rewriterRegistry = new DefaultResponseRewriterRegistry(
+        Arrays.<ResponseRewriter>asList(throwingRewriter), null);
+    servlet = new HtmlAccelServlet();
+    servlet.setHandler(new AccelHandler(pipeline, rewriterRegistry,
+                                        accelUriManager, true));
+    String url = "http://example.org/data.html";
+    String data = "<html><body>Hello World</body></html>";
+
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    req.addHeader("Host", Uri.parse(url).getAuthority());
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponse(data.getBytes())
+        .setHeader("Content-Type", "text/html")
+        .setHttpStatusCode(200)
+        .create();
+    expect(pipeline.execute(req)).andReturn(resp).once();
+    expectRequest("", url);
+    replay();
+
+    servlet.doGet(request, recorder);
+
+    verify();
+    assertEquals(data, recorder.getResponseAsString());
+    assertEquals(200, recorder.getHttpStatusCode());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpGadgetContextTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpGadgetContextTest.java
new file mode 100644
index 0000000..14a67f7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpGadgetContextTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.expect;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.junit.Test;
+
+import java.util.Locale;
+
+public class HttpGadgetContextTest extends ServletTestFixture {
+  @Test
+  public void testIgnoreCacheParam() {
+    expect(request.getParameter("nocache")).andReturn(Integer.toString(Integer.MAX_VALUE));
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertTrue(context.getIgnoreCache());
+  }
+
+  @Test
+  public void testLocale() {
+    expect(request.getParameter("lang")).andReturn(Locale.CHINA.getLanguage());
+    expect(request.getParameter("country")).andReturn(Locale.CHINA.getCountry());
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertEquals(Locale.CHINA, context.getLocale());
+  }
+
+  @Test
+  public void testDebug() {
+    expect(request.getParameter("debug")).andReturn("1");
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertTrue(context.getDebug());
+  }
+
+  @Test
+  public void testGetParameter() {
+    expect(request.getParameter("foo")).andReturn("bar");
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertEquals("bar", context.getParameter("foo"));
+  }
+
+  @Test
+  public void testGetHost() {
+    expect(request.getHeader("Host")).andReturn("foo.org");
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertEquals("foo.org", context.getHost());
+  }
+
+  @Test
+  public void testGetUserIp() {
+    expect(request.getRemoteAddr()).andReturn("2.3.4.5");
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertEquals("2.3.4.5", context.getUserIp());
+  }
+
+  @Test
+  public void testGetSecurityToken() throws Exception {
+    SecurityToken expected = new AnonymousSecurityToken();
+    expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId())).andReturn(expected);
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertEquals(expected, context.getToken());
+  }
+
+  @Test
+  public void testGetUserAgent() throws Exception {
+    expect(request.getHeader("User-Agent")).andReturn("Mozilla/4.0");
+    replay();
+    GadgetContext context = new HttpGadgetContext(request);
+    assertEquals("Mozilla/4.0", context.getUserAgent());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpRequestHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpRequestHandlerTest.java
new file mode 100644
index 0000000..30de434
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/HttpRequestHandlerTest.java
@@ -0,0 +1,732 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.reportMatcher;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.FeedProcessor;
+import org.apache.shindig.gadgets.FeedProcessorImpl;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.rewrite.CaptureRewriter;
+import org.apache.shindig.gadgets.rewrite.DefaultResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.ModulePrefs;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RpcHandler;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.easymock.IArgumentMatcher;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+
+/**
+ * Has coverage for all tests in MakeRequestHandlerTest and should be maintained in sync  until
+ * MakeRequestHandler is eliminated.
+ */
+public class HttpRequestHandlerTest extends EasyMockTestCase {
+
+  private BeanJsonConverter converter;
+
+  private FakeGadgetToken token;
+
+  private final RequestPipeline pipeline = mock(RequestPipeline.class);
+  private final CaptureRewriter rewriter = new CaptureRewriter();
+  private final ResponseRewriterRegistry rewriterRegistry
+      = new DefaultResponseRewriterRegistry(Arrays.<ResponseRewriter>asList(rewriter), null);
+  private final Processor mockProcessor = mock(Processor.class);
+  private final GadgetContext mockContext = mock(GadgetContext.class);
+  private final GadgetSpec mockSpec = mock(GadgetSpec.class);
+  private final ModulePrefs mockPrefs = mock(ModulePrefs.class);
+  private final Gadget mockGadget = mock(Gadget.class);
+
+  private HandlerRegistry registry;
+
+  private HttpResponseBuilder builder;
+
+  private final Map<String,FormDataItem> emptyFormItems = Collections.emptyMap();
+
+  private final Provider<FeedProcessor> feedProcessorProvider = new Provider<FeedProcessor>() {
+        public FeedProcessor get() {
+            return new FeedProcessorImpl();
+        }
+  };
+
+  private void mockGadget(List<Feature> allFeatures, String container, String gadgetUrl) {
+    mockGadgetContext(container);
+    mockGadgetSpec(allFeatures, gadgetUrl);
+    EasyMock.expect(mockGadget.getContext()).andReturn(mockContext).anyTimes();
+    EasyMock.expect(mockGadget.getSpec()).andReturn(mockSpec).anyTimes();
+  }
+
+  private void mockGadgetContext(String container) {
+    EasyMock.expect(mockContext.getContainer()).andReturn(container).anyTimes();
+  }
+
+  private void mockGadgetSpec(List<Feature> allFeatures, String gadgetUrl) {
+    mockModulePrefs(allFeatures);
+    EasyMock.expect(mockSpec.getUrl()).andReturn(Uri.parse(gadgetUrl)).anyTimes();
+    EasyMock.expect(mockSpec.getModulePrefs()).andReturn(mockPrefs).anyTimes();
+  }
+
+  private void mockModulePrefs(List<Feature> features) {
+    EasyMock.expect(mockPrefs.getAllFeatures()).andReturn(features).anyTimes();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    token.setAppUrl("http://www.example.com/gadget.xml");
+
+    Injector injector = Guice.createInjector();
+    converter = new BeanJsonConverter(injector);
+
+    HttpRequestHandler handler = new HttpRequestHandler(pipeline, rewriterRegistry, feedProcessorProvider,
+            mockProcessor);
+    registry = new DefaultHandlerRegistry(injector, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+    builder = new HttpResponseBuilder().setResponseString("CONTENT");
+  }
+
+  @Test
+  public void testGetWithValidGadget() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+            + "href:'http://www.example.org/somecontent'"
+            + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    mockGadget(new ArrayList<Feature>(), "default","http://www.example.com/gadget.xml");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    expect(mockProcessor.process(EasyMock.isA(GadgetContext.class))).andReturn(mockGadget);
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+            (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT' }}",
+            converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testGetWithValidGadgeWithProcessorExceptiont() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+            + "href:'http://www.example.org/somecontent'"
+            + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    expect(mockProcessor.process(EasyMock.isA(GadgetContext.class))).andThrow(
+            new ProcessingException("error", HttpServletResponse.SC_BAD_REQUEST)).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+            (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT' }}",
+            converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testSimpleGet() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT' }}",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testFailGetWithBodyGet() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "body:'POSTBODY'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    RpcHandler operation = registry.getRpcHandler(request);
+    try {
+      operation.execute(emptyFormItems, token, converter).get();
+      fail("Body should not be allowed in GET request");
+    } catch (ExecutionException ee) {
+      assertTrue(ee.getCause() instanceof ProtocolException);
+    }
+  }
+
+  @Test
+  public void testSimplePost() throws Exception {
+    JSONObject request = new JSONObject("{method:http.post, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "body:'POSTBODY'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("POST");
+    httpRequest.setPostBody("POSTBODY".getBytes());
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT' }}",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testPostWithHeaders() throws Exception {
+    JSONObject request = new JSONObject("{method:http.post, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "body:'POSTBODY',"
+        + "headers:{goodheader:[good], host : [iamstripped], 'Content-Length':['1000']}"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("POST");
+    httpRequest.setPostBody("POSTBODY".getBytes());
+    httpRequest.setHeader("goodheader", "good");
+    httpRequest.setHeader("Content-Length", "1000");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT' }}",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testFetchContentTypeFeed() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "format : FEED"
+        + "}}");
+
+    String entryTitle = "Feed title";
+    String entryLink = "http://example.org/entry/0/1";
+    String entrySummary = "This is the summary";
+    String rss = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                 "<rss version=\"2.0\"><channel>" +
+                 "<title>dummy</title>" +
+                 "<link>http://example.org/</link>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "</channel></rss>";
+    builder.setResponseString(rss);
+
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JSONObject feed = (JSONObject) httpApiResponse.getContent();
+      JSONObject entry = feed.getJSONArray("Entry").getJSONObject(0);
+
+    assertEquals(entryTitle, entry.getString("Title"));
+    assertEquals(entryLink, entry.getString("Link"));
+    assertNull("getSummaries has the wrong default value (should be false).",
+        entry.optString("Summary", null));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testFetchFeedWithParameters() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "format : FEED,"
+        + "summarize : true,"
+        + "entryCount : 2"
+        + "}}");
+
+    String entryTitle = "Feed title";
+    String entryLink = "http://example.org/entry/0/1";
+    String entrySummary = "This is the summary";
+    String rss = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                 "<rss version=\"2.0\"><channel>" +
+                 "<title>dummy</title>" +
+                 "<link>http://example.org/</link>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "</channel></rss>";
+
+    builder.setResponseString(rss);
+
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JSONObject feed = (JSONObject) httpApiResponse.getContent();
+    JSONArray feeds = feed.getJSONArray("Entry");
+
+    assertEquals("numEntries not parsed correctly.", 2, feeds.length());
+
+    JSONObject entry = feeds.getJSONObject(1);
+    assertEquals(entryTitle, entry.getString("Title"));
+    assertEquals(entryLink, entry.getString("Link"));
+    assertTrue("getSummaries not parsed correctly.", entry.has("Summary"));
+    assertEquals(entrySummary, entry.getString("Summary"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testJsonObjectGet() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent', format:'json'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    builder.setResponseString("{key:1}");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : {key: 1}}}",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testJsonArrayGet() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent', format:'json'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    builder.setResponseString("[{key:1},{key:2}]");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : [{key:1},{key:2}]}}",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testSignedGetRequest() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "authz : 'signed' }"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    httpRequest.setAuthType(AuthType.SIGNED);
+    httpRequest.setOAuthArguments(
+        new OAuthArguments(AuthType.SIGNED, ImmutableMap.<String, String>of()));
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT' }}",
+        converter.convertToString(httpApiResponse));
+
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testSignedPostAndUpdateSecurityToken() throws Exception {
+    token.setUpdatedToken("updated");
+    JSONObject request = new JSONObject("{method:http.post, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "body:'POSTBODY',"
+        + "authz: 'signed' }"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("POST");
+    httpRequest.setAuthType(AuthType.SIGNED);
+    httpRequest.setOAuthArguments(
+        new OAuthArguments(AuthType.SIGNED, ImmutableMap.<String, String>of()));
+    httpRequest.setPostBody("POSTBODY".getBytes());
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT', token : updated }}",
+        converter.convertToString(httpApiResponse));
+
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testOAuthRequest() throws Exception {
+    JSONObject request = new JSONObject("{method:http.post, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "body:'POSTBODY',"
+        + "authz: 'oauth' }"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("POST");
+    httpRequest.setAuthType(AuthType.OAUTH);
+    httpRequest.setOAuthArguments(
+        new OAuthArguments(AuthType.OAUTH, ImmutableMap.<String, String>of()));
+    httpRequest.setPostBody("POSTBODY".getBytes());
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    operation.execute(emptyFormItems, token, converter).get();
+    verify();
+  }
+
+  @Test
+  public void testOAuthRequestWithParameters() throws Exception {
+    JSONObject request = new JSONObject("{method:http.post, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "body:'POSTBODY',"
+        + "sign_owner:'false',"
+        + "sign_viewer:'true',"
+        + "oauth_service_name:'oauthService',"
+        + "authz: 'oauth' }"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("POST");
+    httpRequest.setAuthType(AuthType.OAUTH);
+    OAuthArguments oauthArgs =
+        new OAuthArguments(AuthType.OAUTH, ImmutableMap.<String, String>of());
+    oauthArgs.setSignOwner(false);
+    oauthArgs.setServiceName("oauthService");
+    httpRequest.setOAuthArguments(oauthArgs);
+    httpRequest.setPostBody("POSTBODY".getBytes());
+
+    Capture<HttpRequest> requestCapture = new Capture<HttpRequest>();
+    expect(pipeline.execute(capture(requestCapture))).andReturn(builder.create());
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    assertEquals(httpRequest.getOAuthArguments(),
+        requestCapture.getValue().getOAuthArguments());
+  }
+
+  @Test
+  public void testInvalidSigningTypeTreatedAsNone() throws Exception {
+    JSONObject request = new JSONObject("{method:http.post, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "body:'POSTBODY',"
+        + "authz : 'rubbish' }"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("POST");
+    httpRequest.setAuthType(AuthType.NONE);
+    httpRequest.setPostBody("POSTBODY".getBytes());
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    operation.execute(emptyFormItems, token, converter).get();
+    verify();
+  }
+
+  @Test
+  public void testSignedGetRequestNoSecurityToken() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "authz : 'signed'}"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    httpRequest.setAuthType(AuthType.SIGNED);
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    try {
+      operation.execute(emptyFormItems, null, converter).get();
+      fail("Cannot execute a request without a security token");
+    } catch (ExecutionException ee) {
+      assertTrue(ee.getCause() instanceof ProtocolException);
+    }
+    verify();
+  }
+
+  @Test
+  public void testBadHttpResponseIsPropagated() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    httpRequest.setAuthType(AuthType.NONE);
+    builder.setHttpStatusCode(HttpResponse.SC_INTERNAL_SERVER_ERROR);
+    builder.setResponseString("I AM AN ERROR MESSAGE");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 500, content : 'I AM AN ERROR MESSAGE' }}",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testMetadataCopied() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    builder.setMetadata("foo", "CONTENT");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT', metadata : { foo : 'CONTENT' }}",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testSetCookiesReturned() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    builder.addHeader("Set-Cookie", "foo=bar; Secure");
+    builder.addHeader("Set-Cookie", "name=value");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals(
+        "{ headers : { 'set-cookie' : ['foo=bar; Secure','name=value'] },"
+            + " status : 200, content : 'CONTENT' }",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testLocationReturned() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent',"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    builder.addHeader("Location", "here");
+    expect(pipeline.execute(eqRequest(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : { 'location' : ['here'] },"
+            + " status : 200, content : 'CONTENT' }",
+        converter.convertToString(httpApiResponse));
+  }
+
+  @Test
+  public void testSimpleGetVerifySecurityTokenPresent() throws Exception {
+    JSONObject request = new JSONObject("{method:http.get, id:req1, params : {"
+        + "href:'http://www.example.org/somecontent'"
+        + "}}");
+    HttpRequest httpRequest = new HttpRequest(Uri.parse("http://www.example.org/somecontent"));
+    httpRequest.setMethod("GET");
+    httpRequest.setSecurityToken( token );
+
+    // check to make sure that the security token is being passed through to the pipeline, and not
+    // stripped because this is not an auth request
+
+    expect(pipeline.execute(eqRequest2(httpRequest))).andReturn(builder.create()).anyTimes();
+
+    replay();
+    RpcHandler operation = registry.getRpcHandler(request);
+
+    HttpRequestHandler.HttpApiResponse httpApiResponse =
+        (HttpRequestHandler.HttpApiResponse)operation.execute(emptyFormItems, token, converter).get();
+    verify();
+
+    JsonAssert.assertJsonEquals("{ headers : {}, status : 200, content : 'CONTENT' }}",
+        converter.convertToString(httpApiResponse));
+  }
+
+
+  private static HttpRequest eqRequest(HttpRequest request) {
+    reportMatcher(new RequestMatcher(request));
+    return null;
+  }
+
+  private static HttpRequest eqRequest2(HttpRequest request) {
+    reportMatcher(new RequestMatcherWithToken(request));
+    return null;
+  }
+
+  private static class RequestMatcher implements IArgumentMatcher {
+
+    protected final HttpRequest req;
+
+    public RequestMatcher(HttpRequest request) {
+      this.req = request;
+    }
+
+    public void appendTo(StringBuffer buffer) {
+      buffer.append("eqRequest[]");
+    }
+
+    public boolean matches(Object obj) {
+      HttpRequest match = (HttpRequest)obj;
+      return (match.getMethod().equals(req.getMethod()) &&
+          match.getUri().equals(req.getUri()) &&
+          match.getAuthType().equals(req.getAuthType()) &&
+          match.getPostBodyAsString().equals(req.getPostBodyAsString()) &&
+          Objects.equal(match.getOAuthArguments(), req.getOAuthArguments()) &&
+          match.getHeaders().equals(req.getHeaders()));
+    }
+  }
+
+  private static class RequestMatcherWithToken extends RequestMatcher {
+
+    public RequestMatcherWithToken(HttpRequest request) {
+      super(request);
+    }
+
+    public boolean matches(Object obj) {
+      HttpRequest match = (HttpRequest)obj;
+      return super.matches(obj) &&
+          match.getSecurityToken() != null &&
+          match.getSecurityToken().equals( req.getSecurityToken() );
+    }
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsServletTest.java
new file mode 100644
index 0000000..cc8d908
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsServletTest.java
@@ -0,0 +1,264 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.getCurrentArguments;
+import static org.easymock.EasyMock.isA;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.js.AddOnloadFunctionProcessor;
+import org.apache.shindig.gadgets.js.DefaultJsProcessorRegistry;
+import org.apache.shindig.gadgets.js.DefaultJsServingPipeline;
+import org.apache.shindig.gadgets.js.GetJsContentProcessor;
+import org.apache.shindig.gadgets.js.IfModifiedSinceProcessor;
+import org.apache.shindig.gadgets.js.JsException;
+import org.apache.shindig.gadgets.js.JsLoadProcessor;
+import org.apache.shindig.gadgets.js.JsProcessor;
+import org.apache.shindig.gadgets.js.JsProcessorRegistry;
+import org.apache.shindig.gadgets.js.JsRequest;
+import org.apache.shindig.gadgets.js.JsRequestBuilder;
+import org.apache.shindig.gadgets.js.JsResponse;
+import org.apache.shindig.gadgets.js.JsResponseBuilder;
+import org.apache.shindig.gadgets.js.JsServingPipeline;
+import org.apache.shindig.gadgets.uri.JsUriManager;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.UriStatus;
+import org.easymock.IAnswer;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+public class JsServletTest extends ServletTestFixture {
+  private static final String EXAMPLE_JS_CODE = "some javascript code";
+  private static final String CONTAINER_PARAM = "im_a_container";
+  private static final String ONLOAD_PARAM = "onload_me";
+  private static final int REFRESH_INTERVAL_SEC = 200;
+  private static final int TIMEOUT_SEC = 1000;
+
+  private final JsServlet servlet = new JsServlet();
+  private JsServlet.CachingSetter httpUtilMock;
+  private GetJsContentProcessor getJsProcessorMock;
+  private JsUriManager jsUriManagerMock;
+  private JsLoadProcessor jsLoadProcessor;
+  private DefaultJsServingPipeline jsServingPipeline;
+
+  @Before
+  public void setUp() throws Exception {
+    httpUtilMock = mock(JsServlet.CachingSetter.class);
+    servlet.setCachingSetter(httpUtilMock);
+
+    jsUriManagerMock = mock(JsUriManager.class);
+    servlet.setJsRequestBuilder(new JsRequestBuilder(jsUriManagerMock, null));
+
+    getJsProcessorMock = mock(GetJsContentProcessor.class);
+  }
+
+  private void setUp(int jsloadTtlSecs) {
+    jsLoadProcessor = new JsLoadProcessor(jsUriManagerMock, jsloadTtlSecs, true);
+    JsProcessorRegistry jsProcessorRegistry =
+        new DefaultJsProcessorRegistry(
+            ImmutableList.<JsProcessor>of(new IfModifiedSinceProcessor()),
+            ImmutableList.<JsProcessor>of(jsLoadProcessor, getJsProcessorMock, new AddOnloadFunctionProcessor()),
+            ImmutableList.<JsProcessor>of());
+
+    jsServingPipeline = new DefaultJsServingPipeline(jsProcessorRegistry);
+    servlet.setJsServingPipeline(jsServingPipeline);
+
+    // TODO: Abstract this out into a helper function associated with Uri class.
+    expect(request.getScheme()).andReturn("http");
+    expect(request.getServerPort()).andReturn(8080);
+    expect(request.getServerName()).andReturn("localhost");
+    expect(request.getRequestURI()).andReturn("/gadgets/js");
+    expect(request.getQueryString()).andReturn(null);
+  }
+
+  private JsUri mockJsUri(String container, RenderingContext context, boolean debug,
+      boolean jsload, boolean nocache, String onload, int refresh, UriStatus status,
+      String... libs) {
+    JsUri result = mock(JsUri.class);
+    expect(result.getContainer()).andReturn(container).anyTimes();
+    expect(result.getContext()).andReturn(context).anyTimes();
+    expect(result.getOnload()).andReturn(onload).anyTimes();
+    expect(result.getRefresh()).andReturn(refresh).anyTimes();
+    expect(result.isDebug()).andReturn(debug).anyTimes();
+    expect(result.isNoCache()).andReturn(nocache).anyTimes();
+    expect(result.isJsload()).andReturn(jsload).anyTimes();
+    expect(result.getStatus()).andReturn(status).anyTimes();
+    if (libs != null) {
+      expect(result.getLibs()).andReturn(Lists.newArrayList(libs)).anyTimes();
+    }
+    return result;
+  }
+
+  @Test
+  public void testJsServletGivesErrorWhenUriManagerThrowsException() throws Exception {
+    setUp(0);
+    expect(jsUriManagerMock.processExternJsUri(isA(Uri.class))).andThrow(new GadgetException(null));
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    verify();
+  }
+
+  @Test
+  public void testWithIfModifiedSinceHeaderPresentAndVersionReturnsNotModified() throws Exception {
+    setUp(0);
+    JsUri jsUri = mockJsUri(CONTAINER_PARAM, RenderingContext.CONTAINER, false, false, false,
+        null, REFRESH_INTERVAL_SEC, UriStatus.VALID_VERSIONED);
+    expect(jsUriManagerMock.processExternJsUri(isA(Uri.class))).andReturn(jsUri);
+    expect(request.getHeader("If-Modified-Since")).andReturn("12345");
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_NOT_MODIFIED, recorder.getHttpStatusCode());
+    verify();
+  }
+
+  @Test
+  public void testWithIfModifiedSinceHeaderPresentButNoVersionActsNormal() throws Exception {
+    setUp(0);
+    JsUri jsUri = mockJsUri(CONTAINER_PARAM, RenderingContext.CONTAINER, false, false, false,
+        null, REFRESH_INTERVAL_SEC, UriStatus.VALID_UNVERSIONED);
+    expect(jsUriManagerMock.processExternJsUri(isA(Uri.class))).andReturn(jsUri);
+    expect(request.getHeader("If-Modified-Since")).andReturn("12345");
+    final JsResponse response = new JsResponseBuilder().appendJs(EXAMPLE_JS_CODE, "js").build();
+    expect(request.getHeader("Host")).andReturn("localhost");
+    expect(getJsProcessorMock.process(isA(JsRequest.class), isA(JsResponseBuilder.class))).andAnswer(
+        new IAnswer<Boolean>() {
+          public Boolean answer() throws Throwable {
+            JsResponseBuilder builder = (JsResponseBuilder)getCurrentArguments()[1];
+            builder.appendAllJs(response.getAllJsContent());
+            return true;
+          }
+        });
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_OK, recorder.getHttpStatusCode());
+    assertEquals(EXAMPLE_JS_CODE, recorder.getResponseAsString());
+    verify();
+  }
+
+  @Test
+  public void testDoJsloadNormal() throws Exception {
+    setUp(0);
+    String url = "http://localhost/gadgets/js/feature.js?v=abc&nocache=0&onload=" + ONLOAD_PARAM;
+    JsUri jsUri = mockJsUri(CONTAINER_PARAM, RenderingContext.CONTAINER, true, true, false,
+        ONLOAD_PARAM, REFRESH_INTERVAL_SEC, UriStatus.VALID_VERSIONED);
+
+    expect(jsUriManagerMock.processExternJsUri(isA(Uri.class))).andReturn(jsUri);
+    expect(jsUriManagerMock.makeExternJsUri(isA(JsUri.class)))
+        .andReturn(Uri.parse(url));
+    httpUtilMock.setCachingHeaders(recorder, REFRESH_INTERVAL_SEC, false);
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_OK, recorder.getHttpStatusCode());
+    assertEquals(String.format(JsLoadProcessor.JSLOAD_JS_TPL, url + "&jsload=0"),
+        recorder.getResponseAsString());
+    verify();
+  }
+
+  @Test
+  public void testDoJsloadWithJsLoadTimeout() throws Exception {
+    setUp(TIMEOUT_SEC); // Enable JS load timeout.
+
+    String url = "http://localhost/gadgets/js/feature.js?v=abc&nocache=0&onload=" + ONLOAD_PARAM;
+    JsUri jsUri = mockJsUri(CONTAINER_PARAM, RenderingContext.CONTAINER, true, true,
+        false, ONLOAD_PARAM, -1, UriStatus.VALID_VERSIONED); // Disable refresh interval.
+
+    expect(jsUriManagerMock.processExternJsUri(isA(Uri.class))).andReturn(jsUri);
+    expect(jsUriManagerMock.makeExternJsUri(isA(JsUri.class)))
+        .andReturn(Uri.parse(url));
+    httpUtilMock.setCachingHeaders(recorder, TIMEOUT_SEC, false);
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_OK, recorder.getHttpStatusCode());
+    assertEquals(String.format(JsLoadProcessor.JSLOAD_JS_TPL, url + "&jsload=0"),
+        recorder.getResponseAsString());
+    verify();
+  }
+
+  @Test
+  public void testDoJsloadNoOnload() throws Exception {
+    setUp(0);
+    JsUri jsUri = mockJsUri(CONTAINER_PARAM, RenderingContext.CONTAINER, true, true, false,
+        null, // lacks &onload=
+        REFRESH_INTERVAL_SEC, UriStatus.VALID_VERSIONED);
+    expect(jsUriManagerMock.processExternJsUri(isA(Uri.class))).andReturn(jsUri);
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    assertEquals(JsLoadProcessor.JSLOAD_ONLOAD_ERROR, recorder.getResponseAsString());
+    verify();
+  }
+
+  @Test
+  public void testDoJsloadNoCache() throws Exception {
+    setUp(0);
+    String url = "http://localhost/gadgets/js/feature.js?v=abc&nocache=1&onload=" + ONLOAD_PARAM;
+    JsUri jsUri = mockJsUri(CONTAINER_PARAM, RenderingContext.CONTAINER, true, true,
+        true, // Set to no cache.
+        ONLOAD_PARAM, REFRESH_INTERVAL_SEC, UriStatus.VALID_VERSIONED);
+
+    expect(jsUriManagerMock.processExternJsUri(isA(Uri.class))).andReturn(jsUri);
+    expect(jsUriManagerMock.makeExternJsUri(isA(JsUri.class)))
+        .andReturn(Uri.parse(url));
+    httpUtilMock.setCachingHeaders(recorder, 0, false); // TTL of 0 is set.
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_OK, recorder.getHttpStatusCode());
+    assertEquals(String.format(JsLoadProcessor.JSLOAD_JS_TPL, url + "&jsload=0"),
+        recorder.getResponseAsString());
+    verify();
+  }
+
+  @Test
+  public void testJsServletGivesErrorWhenJsResponseHasError() throws Exception {
+    setUp(0);
+    JsProcessor errorProcessor = new JsProcessor(){
+      public boolean process(JsRequest jsRequest, JsResponseBuilder builder) throws JsException {
+        builder.setStatusCode(HttpServletResponse.SC_NOT_FOUND);
+        builder.addError("Something bad happened");
+        return false;
+      }};
+    JsProcessorRegistry jsProcessorRegistry = new DefaultJsProcessorRegistry(ImmutableList.<JsProcessor> of(),
+            ImmutableList.<JsProcessor> of(errorProcessor), ImmutableList.<JsProcessor> of());
+
+    JsServingPipeline pipeline = new DefaultJsServingPipeline(jsProcessorRegistry);
+    servlet.setJsServingPipeline(pipeline);
+    replay();
+
+    servlet.doGet(request, recorder);
+    assertEquals(HttpServletResponse.SC_NOT_FOUND, recorder.getHttpStatusCode());
+    assertEquals("Something bad happened", recorder.getResponseAsString());
+    verify();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsonRpcGadgetContextTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsonRpcGadgetContextTest.java
new file mode 100644
index 0000000..b78adbb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsonRpcGadgetContextTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.gadgets.GadgetContext;
+
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Locale;
+import java.util.Map;
+
+public class JsonRpcGadgetContextTest extends Assert {
+  final static String SPEC_URL = "http://example.org/gadget.xml";
+  final static int SPEC_ID = 1234;
+  final static String[] PREF_KEYS = {"hello", "foo"};
+  final static String[] PREF_VALUES = {"world", "bar"};
+  final static Map<String, String> prefs = Maps.newHashMap();
+  static {
+    for (int i = 0, j = PREF_KEYS.length; i < j; ++i) {
+      prefs.put(PREF_KEYS[i], PREF_VALUES[i]);
+    }
+  }
+
+  @Test
+  public void testCorrectExtraction() throws Exception {
+    JSONObject gadget = new JSONObject()
+        .put("url", SPEC_URL)
+        .put("moduleId", SPEC_ID)
+        .put("prefs", prefs)
+        .put("gadget-field", "gadget-value");
+
+    JSONObject context = new JSONObject()
+        .put("language", Locale.US.getLanguage())
+        .put("country", Locale.US.getCountry().toUpperCase())
+        .put("context-field", "context-value");
+
+    GadgetContext jsonContext = new JsonRpcGadgetContext(context, gadget);
+    assertEquals(SPEC_URL, jsonContext.getUrl().toString());
+    assertEquals(SPEC_ID, jsonContext.getModuleId());
+    assertEquals(Locale.US.getLanguage(),
+                 jsonContext.getLocale().getLanguage());
+    assertEquals(Locale.US.getCountry(), jsonContext.getLocale().getCountry());
+
+    for (String key : PREF_KEYS) {
+      String value = jsonContext.getUserPrefs().getPref(key);
+      assertEquals(prefs.get(key), value);
+    }
+
+    assertEquals("gadget-value", jsonContext.getParameter("gadget-field"));
+    assertEquals("context-value", jsonContext.getParameter("context-field"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsonRpcHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsonRpcHandlerTest.java
new file mode 100644
index 0000000..aba47a9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/JsonRpcHandlerTest.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.testing.ImmediateExecutorService;
+import org.apache.shindig.gadgets.process.ProcessingException;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+public class JsonRpcHandlerTest {
+  private final FakeProcessor processor = new FakeProcessor();
+  private final FakeIframeUriManager urlGenerator = new FakeIframeUriManager();
+  private final JsonRpcHandler jsonRpcHandler
+      = new JsonRpcHandler(new ImmediateExecutorService(), processor, urlGenerator);
+
+  private JSONObject createContext(String lang, String country)
+      throws JSONException {
+    return new JSONObject().put("language", lang).put("country", country);
+  }
+
+  private JSONObject createGadget(String url, int moduleId, Map<String, String> prefs)
+      throws JSONException {
+    return new JSONObject()
+        .put("url", url)
+        .put("moduleId", moduleId)
+        .put("prefs", prefs == null ? Collections.emptySet() : prefs);
+  }
+
+  @Test
+  public void testSimpleRequest() throws Exception {
+    JSONArray gadgets = new JSONArray()
+        .put(createGadget(FakeProcessor.SPEC_URL.toString(), 0, null));
+    JSONObject input = new JSONObject()
+        .put("context", createContext("en", "US"))
+        .put("gadgets", gadgets);
+
+    urlGenerator.iframeUrl = FakeProcessor.SPEC_URL;
+
+    JSONObject response = jsonRpcHandler.process(input);
+
+    JSONArray outGadgets = response.getJSONArray("gadgets");
+    JSONObject gadget = outGadgets.getJSONObject(0);
+    assertEquals(FakeProcessor.SPEC_URL.toString(), gadget.getString("iframeUrl"));
+    assertEquals(FakeProcessor.SPEC_TITLE, gadget.getString("title"));
+    assertEquals(0, gadget.getInt("moduleId"));
+
+    JSONObject view = gadget.getJSONObject("views").getJSONObject(GadgetSpec.DEFAULT_VIEW);
+    assertEquals(FakeProcessor.PREFERRED_HEIGHT, view.getInt("preferredHeight"));
+    assertEquals(FakeProcessor.PREFERRED_WIDTH, view.getInt("preferredWidth"));
+    assertEquals(FakeProcessor.LINK_HREF, gadget.getJSONObject("links").getString(FakeProcessor.LINK_REL));
+
+    JSONObject userPrefs = gadget.getJSONObject("userPrefs");
+    assertNotNull(userPrefs);
+
+    JSONObject userPrefData = userPrefs.getJSONObject("up_one");
+    assertNotNull(userPrefData);
+
+    JSONObject upEnums = userPrefData.getJSONObject("enumValues");
+    assertNotNull(upEnums);
+    assertEquals("disp1", upEnums.get("val1"));
+    assertEquals("disp2", upEnums.get("abc"));
+    assertEquals("disp3", upEnums.get("z_xabc"));
+    assertEquals("disp4", upEnums.get("foo"));
+
+    JSONArray orderedEnums = userPrefData.getJSONArray("orderedEnumValues");
+    assertNotNull(orderedEnums);
+    assertEquals(4, orderedEnums.length());
+    assertEquals("val1", orderedEnums.getJSONObject(0).getString("value"));
+    assertEquals("abc", orderedEnums.getJSONObject(1).getString("value"));
+    assertEquals("z_xabc", orderedEnums.getJSONObject(2).getString("value"));
+    assertEquals("foo", orderedEnums.getJSONObject(3).getString("value"));
+  }
+
+  @Test
+  public void testUnexpectedError() throws Exception {
+    JSONArray gadgets = new JSONArray()
+        .put(createGadget(FakeProcessor.SPEC_URL.toString(), 0, null));
+    JSONObject input = new JSONObject()
+        .put("context", createContext("en", "US"))
+        .put("gadgets", gadgets);
+
+    urlGenerator.throwRandomFault = true;
+    JSONObject resp = jsonRpcHandler.process(input);
+    String actual = resp.getJSONArray("gadgets").getJSONObject(0).getJSONArray("errors").getString(0);
+    assertEquals("BROKEN", actual);
+  }
+
+  // TODO: Verify that user pref specs are returned correctly.
+
+  @Test
+  public void testMultipleGadgets() throws Exception {
+    JSONArray gadgets = new JSONArray()
+        .put(createGadget(FakeProcessor.SPEC_URL.toString(), 0, null))
+        .put(createGadget(FakeProcessor.SPEC_URL2.toString(), 1, null));
+    JSONObject input = new JSONObject()
+        .put("context", createContext("en", "US"))
+        .put("gadgets", gadgets);
+
+    JSONObject response = jsonRpcHandler.process(input);
+
+    JSONArray outGadgets = response.getJSONArray("gadgets");
+
+    boolean first = false;
+    boolean second = false;
+    for (int i = 0, j = outGadgets.length(); i < j; ++i) {
+      JSONObject gadget = outGadgets.getJSONObject(i);
+      if (gadget.getString("url").equals(FakeProcessor.SPEC_URL.toString())) {
+        assertEquals(FakeProcessor.SPEC_TITLE, gadget.getString("title"));
+        assertEquals(0, gadget.getInt("moduleId"));
+        first = true;
+      } else {
+        assertEquals(FakeProcessor.SPEC_TITLE2, gadget.getString("title"));
+        assertEquals(1, gadget.getInt("moduleId"));
+        second = true;
+      }
+    }
+
+    assertTrue("First gadget not returned!", first);
+    assertTrue("Second gadget not returned!", second);
+  }
+
+  @Test
+  public void testMultipleGadgetsWithAnError() throws Exception {
+    JSONArray gadgets = new JSONArray()
+        .put(createGadget(FakeProcessor.SPEC_URL.toString(), 0, null))
+        .put(createGadget(FakeProcessor.SPEC_URL2.toString(), 1, null));
+    JSONObject input = new JSONObject()
+        .put("context", createContext("en", "US"))
+        .put("gadgets", gadgets);
+
+    processor.exceptions.put(FakeProcessor.SPEC_URL2,
+        new ProcessingException("broken", HttpServletResponse.SC_BAD_REQUEST));
+
+    JSONObject response = jsonRpcHandler.process(input);
+
+    JSONArray outGadgets = response.getJSONArray("gadgets");
+
+    boolean first = false;
+    boolean second = false;
+    for (int i = 0, j = outGadgets.length(); i < j; ++i) {
+      JSONObject gadget = outGadgets.getJSONObject(i);
+      if (gadget.getString("url").equals(FakeProcessor.SPEC_URL.toString())) {
+        assertEquals(FakeProcessor.SPEC_TITLE, gadget.getString("title"));
+        assertEquals(0, gadget.getInt("moduleId"));
+        first = true;
+      } else {
+        JSONArray errors = gadget.getJSONArray("errors");
+        assertEquals(1, errors.length());
+        assertEquals("broken", errors.optString(0));
+        second = true;
+      }
+    }
+
+    assertTrue("First gadget not returned!", first);
+    assertTrue("Second gadget not returned!", second);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java
new file mode 100644
index 0000000..54eaaf3
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestHandlerTest.java
@@ -0,0 +1,845 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static junitx.framework.StringAssert.assertStartsWith;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.servlet.HttpUtilTest;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.HashLockedDomainService;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.uri.HashShaLockedDomainPrefixGenerator;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.easymock.Capture;
+import org.easymock.IAnswer;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+
+/**
+ * Tests for MakeRequestHandler.
+ */
+public class MakeRequestHandlerTest extends ServletTestFixture {
+  private static final Uri REQUEST_URL = Uri.parse("http://example.org/file");
+  private static final String REQUEST_BODY = "I+am+the+request+body!foo=baz%20la";
+  private static final String RESPONSE_BODY = "makeRequest response body";
+  private static final FakeGadgetToken DUMMY_TOKEN = new FakeGadgetToken();
+
+  private final GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
+  private ContainerConfig containerConfig;
+  private LockedDomainService ldService;
+  private MakeRequestHandler handler;
+  private Gadget gadget = mock(Gadget.class);
+  private Capture<GadgetContext> context = new Capture<GadgetContext>();
+
+  private void expectGetAndReturnBody(String response) throws Exception {
+    expectGetAndReturnBody(AuthType.NONE, response);
+  }
+
+  private void expectGetAndReturnBody(AuthType authType, String response) throws Exception {
+    HttpRequest request = new HttpRequest(REQUEST_URL).setAuthType(authType);
+    expect(pipeline.execute(request)).andReturn(new HttpResponse(response));
+  }
+
+  private void expectPostAndReturnBody(String postData, String response) throws Exception {
+    expectPostAndReturnBody(AuthType.NONE, postData, response);
+  }
+
+  private void expectPostAndReturnBody(AuthType authType, String postData, String response)
+      throws Exception {
+    HttpRequest req = new HttpRequest(REQUEST_URL).setMethod("POST")
+        .setPostBody(postData.getBytes("UTF-8"))
+        .setAuthType(authType)
+        .addHeader("Content-Type", "application/x-www-form-urlencoded");
+    expect(pipeline.execute(req)).andReturn(new HttpResponse(response));
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("POST");
+    expect(request.getParameter(MakeRequestHandler.POST_DATA_PARAM))
+        .andReturn(postData);
+  }
+
+  private void expectPutAndReturnBody(String putData, String response) throws Exception {
+    expectPutAndReturnBody(AuthType.NONE, putData, response);
+  }
+
+  private void expectPutAndReturnBody(AuthType authType, String putData, String response)
+          throws Exception {
+    HttpRequest req = new HttpRequest(REQUEST_URL).setMethod("PUT")
+        .setPostBody(putData.getBytes("UTF-8"))
+        .setAuthType(authType);
+    expect(pipeline.execute(req)).andReturn(new HttpResponse(response));
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("PUT");
+    expect(request.getParameter(MakeRequestHandler.POST_DATA_PARAM))
+        .andReturn(putData);
+  }
+
+  private void expectDeleteAndReturnBody(String response) throws Exception {
+    expectDeleteAndReturnBody(AuthType.NONE, response);
+  }
+
+  private void expectDeleteAndReturnBody(AuthType authType, String response) throws Exception {
+    HttpRequest req = new HttpRequest(REQUEST_URL).setMethod("DELETE").setAuthType(authType);
+    expect(pipeline.execute(req)).andReturn(new HttpResponse(response));
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("DELETE");
+  }
+
+  private void expectHead() throws Exception {
+    expectHead(AuthType.NONE);
+  }
+
+  private void expectHead(AuthType authType) throws Exception {
+    HttpRequest req = new HttpRequest(REQUEST_URL).setMethod("HEAD").setAuthType(authType);
+    expect(pipeline.execute(req)).andReturn(new HttpResponse(""));
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("HEAD");
+  }
+
+  private void expectPatchAndReturnBody(String response) throws Exception {
+    expectPatchAndReturnBody(AuthType.NONE, response);
+  }
+
+  private void expectPatchAndReturnBody(AuthType authType, String response) throws Exception {
+    HttpRequest req = new HttpRequest(REQUEST_URL).setMethod("PATCH").setAuthType(authType);
+    expect(pipeline.execute(req)).andReturn(new HttpResponse(response));
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("PATCH");
+  }
+
+  private JSONObject extractJsonFromResponse() throws JSONException {
+    return extractJsonFromResponse(recorder.getResponseAsString());
+  }
+
+  private JSONObject extractJsonFromResponse(String response) throws JSONException {
+    String defaultCruftMsg = "throw 1; < don't be evil' >";
+    assertStartsWith(defaultCruftMsg, response);
+    response = response.substring(defaultCruftMsg.length());
+    return new JSONObject(response).getJSONObject(REQUEST_URL.toString());
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    expect(request.getMethod()).andReturn("POST").anyTimes();
+    expect(request.getParameter(Param.URL.getKey()))
+        .andReturn(REQUEST_URL.toString()).anyTimes();
+
+
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+        "'gadgets.features':{views:" +
+        "{aliased: {aliases: ['some-alias', 'alias']}}" +
+        ",'core.io':" +
+        "{unparseableCruft :\"throw 1; < don't be evil' >\"}}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    ldService = new HashLockedDomainService(containerConfig, false, new HashShaLockedDomainPrefixGenerator());
+    handler = new MakeRequestHandler(containerConfig, pipeline, rewriterRegistry, feedProcessorProvider, gadgetAdminStore, processor, ldService);
+
+    DUMMY_TOKEN.setAppUrl("http://some/gadget.xml");
+    DUMMY_TOKEN.setContainer(ContainerConfig.DEFAULT_CONTAINER);
+    expect(request.getParameter(Param.GADGET.getKey())).andReturn("http://some/gadget.xml").anyTimes();
+    expect(processor.process(capture(context))).andReturn(gadget).anyTimes();
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+  }
+
+  @Test
+  public void testGetRequest() throws Exception {
+    expectGetAndReturnBody(RESPONSE_BODY);
+    replay();
+
+    handler.fetch(request, recorder);
+
+    JSONObject results = extractJsonFromResponse();
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testGetRequestWithUncommonStatusCode() throws Exception {
+    HttpRequest req = new HttpRequest(REQUEST_URL);
+    HttpResponse response = new HttpResponseBuilder()
+        .setHttpStatusCode(HttpResponse.SC_CREATED)
+        .setResponseString(RESPONSE_BODY)
+        .create();
+    expect(pipeline.execute(req)).andReturn(response);
+    replay();
+
+    handler.fetch(request, recorder);
+
+    JSONObject results = extractJsonFromResponse();
+    assertEquals(HttpResponse.SC_CREATED, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testGetRequestWithRefresh() throws Exception {
+    expect(request.getParameter(Param.REFRESH.getKey())).andReturn("120").anyTimes();
+
+    Capture<HttpRequest> requestCapture = new Capture<HttpRequest>();
+    expect(pipeline.execute(capture(requestCapture))).andReturn(new HttpResponse(RESPONSE_BODY));
+
+    replay();
+
+    handler.fetch(request, recorder);
+
+    HttpRequest httpRequest = requestCapture.getValue();
+    assertEquals("public,max-age=120", recorder.getHeader("Cache-Control"));
+    assertEquals(120, httpRequest.getCacheTtl());
+  }
+
+  @Test
+  public void testGetRequestWithBadTtl() throws Exception {
+    expect(request.getParameter(Param.REFRESH.getKey())).andReturn("foo").anyTimes();
+
+    Capture<HttpRequest> requestCapture = new Capture<HttpRequest>();
+    expect(pipeline.execute(capture(requestCapture))).andReturn(new HttpResponse(RESPONSE_BODY));
+
+    replay();
+
+    try {
+      handler.fetch(request, recorder);
+    } catch (GadgetException e) {
+      // Expected - catch now occurs at the MakeRequestServlet level.
+    }
+
+    HttpRequest httpRequest = requestCapture.getValue();
+    assertEquals(null, recorder.getHeader("Cache-Control"));
+    assertEquals(-1, httpRequest.getCacheTtl());
+  }
+
+  @Test
+  public void GetRequestWithNonWhitelistedGadget() throws Exception {
+    reset(gadgetAdminStore);
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(false);
+    replay();
+    boolean exceptionThrown = false;
+    try {
+      handler.fetch(request, recorder);
+    } catch (GadgetException e) {
+      exceptionThrown = true;
+      assertEquals(403, e.getHttpStatusCode());
+      assertEquals(GadgetException.Code.NON_WHITELISTED_GADGET, e.getCode());
+    }
+    assertTrue(exceptionThrown);
+    verify();
+  }
+
+  @Test
+  public void testExplicitHeaders() throws Exception {
+    String headerString = "X-Foo=bar&X-Bar=baz%20foo";
+
+    HttpRequest expected = new HttpRequest(REQUEST_URL)
+        .addHeader("X-Foo", "bar")
+        .addHeader("X-Bar", "baz foo");
+    expect(pipeline.execute(expected)).andReturn(new HttpResponse(RESPONSE_BODY));
+    expect(request.getParameter(MakeRequestHandler.HEADERS_PARAM)).andReturn(headerString);
+    replay();
+
+    handler.fetch(request, recorder);
+    verify();
+
+    JSONObject results = extractJsonFromResponse();
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testPostRequest() throws Exception {
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("POST");
+    expectPostAndReturnBody(REQUEST_BODY, RESPONSE_BODY);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testPutRequest() throws Exception {
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("PUT");
+    expectPutAndReturnBody(REQUEST_BODY, RESPONSE_BODY);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testDeleteRequest() throws Exception {
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("DELETE");
+    expectDeleteAndReturnBody("");
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testHeadRequest() throws Exception {
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("HEAD");
+    expectHead();
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testPatchRequest() throws Exception {
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM)).andReturn("PATCH");
+    expectPatchAndReturnBody("");
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testFetchAtom1Feed() throws Exception {
+    String txt = "<?xml version='1.0' encoding='utf-8'?>" +
+      "<feed xmlns=\"http://www.w3.org/2005/Atom\">" +
+        "<id>fooId</id>" +
+        "<title type=\"text\">feed</title>" +
+        "<updated>2011-01-07T14:26:19.879Z</updated>" +
+        "<author>" +
+          "<name>author@example.org</name>" +
+        "</author>" +
+        "<entry>" +
+          "<updated>2011-01-07T14:26:19.879Z</updated>" +
+          "<author />" +
+          "<title type=\"text\">howdy</title>" +
+          "<content type=\"application/xml\">" +
+            "<entity xmlns=\"\"><Data>hello world</Data></entity>" +
+            "</content>" +
+          "<id>entity1ID</id>" +
+          "<link href=\"http://example.org/edit/entity1ID\"/>" +
+        "</entry>" +
+      "</feed>";
+    expectGetAndReturnBody(txt);
+    expect(request.getParameter(MakeRequestHandler.CONTENT_TYPE_PARAM)).andReturn("FEED");
+    expect(request.getParameter(MakeRequestHandler.GET_SUMMARIES_PARAM)).andReturn("true");
+    replay();
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+    JSONObject feed = new JSONObject(results.getString("body"));
+    assertEquals("feed", feed.getString("Title"));
+    assertEquals("author@example.org", feed.getString("Author"));
+    assertEquals("http://example.org/file", feed.getString("URL"));
+
+    JSONObject entry = feed.getJSONArray("Entry").getJSONObject(0);
+    assertEquals("howdy", entry.getString("Title"));
+    assertEquals("http://example.org/edit/entity1ID", entry.getString("Link"));
+    assertEquals("<entity><Data>hello world</Data></entity>",
+        entry.getString("Summary"));
+  }
+
+  @Test
+  public void testFetchContentTypeFeed() throws Exception {
+    String entryTitle = "Feed title";
+    String entryLink = "http://example.org/entry/0/1";
+    String entrySummary = "This is the summary";
+    String rss = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                 "<rss version=\"2.0\"><channel>" +
+                 "<title>dummy</title>" +
+                 "<link>http://example.org/</link>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "</channel></rss>";
+
+    expectGetAndReturnBody(rss);
+    expect(request.getParameter(MakeRequestHandler.CONTENT_TYPE_PARAM)).andReturn("FEED");
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    JSONObject feed = new JSONObject(results.getString("body"));
+    JSONObject entry = feed.getJSONArray("Entry").getJSONObject(0);
+
+    assertEquals(entryTitle, entry.getString("Title"));
+    assertEquals(entryLink, entry.getString("Link"));
+    assertNull("getSummaries has the wrong default value (should be false).",
+        entry.optString("Summary", null));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testFetchFeedWithParameters() throws Exception {
+    String entryTitle = "Feed title";
+    String entryLink = "http://example.org/entry/0/1";
+    String entrySummary = "This is the summary";
+    String rss = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
+                 "<rss version=\"2.0\"><channel>" +
+                 "<title>dummy</title>" +
+                 "<link>http://example.org/</link>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "<item>" +
+                 "<title>" + entryTitle + "</title>" +
+                 "<link>" + entryLink + "</link>" +
+                 "<description>" + entrySummary + "</description>" +
+                 "</item>" +
+                 "</channel></rss>";
+
+    expectGetAndReturnBody(rss);
+    expect(request.getParameter(MakeRequestHandler.GET_SUMMARIES_PARAM)).andReturn("true");
+    expect(request.getParameter(MakeRequestHandler.NUM_ENTRIES_PARAM)).andReturn("2");
+    expect(request.getParameter(MakeRequestHandler.CONTENT_TYPE_PARAM)).andReturn("FEED");
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    JSONObject feed = new JSONObject(results.getString("body"));
+    JSONArray feeds = feed.getJSONArray("Entry");
+
+    assertEquals("numEntries not parsed correctly.", 2, feeds.length());
+
+    JSONObject entry = feeds.getJSONObject(1);
+    assertEquals(entryTitle, entry.getString("Title"));
+    assertEquals(entryLink, entry.getString("Link"));
+    assertTrue("getSummaries not parsed correctly.", entry.has("Summary"));
+    assertEquals(entrySummary, entry.getString("Summary"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testMultiPartFormPostWithSpecialChars() throws Exception {
+    String body = "\u003c!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\"\u003e"
+      + "<html><body>&quot;Hello, world!&quot;</body></html>";
+    expectGetAndReturnBody(body);
+
+    expect(request.getParameter(MakeRequestHandler.CONTENT_TYPE_PARAM)).andReturn("TEXT");
+    expect(request.getParameter(MakeRequestHandler.MULTI_PART_FORM_POST_IFRAME)).andReturn("1");
+    replay();
+
+    handler.fetch(request, recorder);
+    String response = recorder.getResponseAsString();
+    response = StringUtils.removeStart(response, MakeRequestHandler.IFRAME_RESPONSE_PREFIX);
+    response = StringUtils.removeEnd(response, MakeRequestHandler.IFRAME_RESPONSE_SUFFIX);
+    response = StringEscapeUtils.unescapeEcmaScript(response);
+    JSONObject result = extractJsonFromResponse(response);
+    assertEquals(
+      "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"
+        + "<html><body>&quot;Hello, world!&quot;</body></html>",
+      result.get("body")
+    );
+  }
+
+  @Test
+  public void testFetchEmptyDocument() throws Exception {
+    expectGetAndReturnBody("");
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals("", results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  private void expectParameters(HttpServletRequest request, String... params) {
+    final List<String> v = Lists.newArrayList(params);
+
+    expect(request.getParameterNames()).andStubAnswer(new IAnswer<Enumeration<String>>() {
+      public Enumeration<String> answer() throws Throwable {
+        return Collections.enumeration(v);
+      }
+    });
+  }
+
+  @Test
+  public void testSignedGetRequest() throws Exception {
+    expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId()))
+        .andReturn(DUMMY_TOKEN).atLeastOnce();
+    expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM))
+        .andReturn(AuthType.SIGNED.toString()).atLeastOnce();
+    HttpRequest expected = new HttpRequest(REQUEST_URL)
+        .setAuthType(AuthType.SIGNED);
+    expect(pipeline.execute(expected))
+        .andReturn(new HttpResponse(RESPONSE_BODY));
+    expectParameters(request, MakeRequestHandler.AUTHZ_PARAM);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testSignedPostRequest() throws Exception {
+    // Doesn't actually sign since it returns the standard fetcher.
+    // Signing tests are in SigningFetcherTest
+    expectPostAndReturnBody(AuthType.SIGNED, REQUEST_BODY, RESPONSE_BODY);
+    expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId()))
+        .andReturn(DUMMY_TOKEN).atLeastOnce();
+    expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM))
+        .andReturn(AuthType.SIGNED.toString()).atLeastOnce();
+    expectParameters(request, MakeRequestHandler.METHOD_PARAM, MakeRequestHandler.POST_DATA_PARAM,
+        MakeRequestHandler.AUTHZ_PARAM);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertFalse("A security token was returned when it was not requested.",
+        results.has("st"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testChangeSecurityToken() throws Exception {
+    // Doesn't actually sign since it returns the standard fetcher.
+    // Signing tests are in SigningFetcherTest
+    expectGetAndReturnBody(AuthType.SIGNED, RESPONSE_BODY);
+    FakeGadgetToken authToken = new FakeGadgetToken()
+      .setUpdatedToken("updated")
+      .setAppUrl(DUMMY_TOKEN.getAppUrl())
+      .setContainer(DUMMY_TOKEN.getContainer());
+    expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId()))
+        .andReturn(authToken).atLeastOnce();
+    expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM))
+        .andReturn(AuthType.SIGNED.toString()).atLeastOnce();
+    expectParameters(request, MakeRequestHandler.AUTHZ_PARAM);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertEquals("updated", results.getString("st"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testDoOAuthRequest() throws Exception {
+    // Doesn't actually do oauth dance since it returns the standard fetcher.
+    // OAuth tests are in OAuthRequestTest
+    expectGetAndReturnBody(AuthType.OAUTH, RESPONSE_BODY);
+    FakeGadgetToken authToken = new FakeGadgetToken()
+      .setUpdatedToken("updated")
+      .setAppUrl(DUMMY_TOKEN.getAppUrl())
+      .setContainer(DUMMY_TOKEN.getContainer());
+    expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId()))
+        .andReturn(authToken).atLeastOnce();
+    expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM))
+        .andReturn(AuthType.OAUTH.toString()).atLeastOnce();
+    // This isn't terribly accurate, but is close enough for this test.
+    expect(request.getParameterMap()).andStubReturn(Collections.emptyMap());
+    expectParameters(request);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testInvalidSigningTypeTreatedAsNone() throws Exception {
+    expectGetAndReturnBody(RESPONSE_BODY);
+    expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM)).andReturn("garbage");
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testBadHttpResponseIsPropagated() throws Exception {
+    HttpRequest internalRequest = new HttpRequest(REQUEST_URL);
+    expect(pipeline.execute(internalRequest)).andReturn(HttpResponse.error());
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(HttpResponse.SC_INTERNAL_SERVER_ERROR, results.getInt("rc"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test(expected=GadgetException.class)
+  public void testBadSecurityTokenThrows() throws Exception {
+    expect(request.getAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId()))
+        .andReturn(null).atLeastOnce();
+    expect(request.getParameter(MakeRequestHandler.AUTHZ_PARAM))
+        .andReturn(AuthType.SIGNED.toString()).atLeastOnce();
+    replay();
+
+    handler.fetch(request, recorder);
+  }
+
+  @Test
+  public void testMetadataCopied() throws Exception {
+    HttpRequest internalRequest = new HttpRequest(REQUEST_URL);
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponse("foo".getBytes("UTF-8"))
+        .setMetadata("foo", RESPONSE_BODY)
+        .create();
+
+    expect(pipeline.execute(internalRequest)).andReturn(response);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+
+    assertEquals(RESPONSE_BODY, results.getString("foo"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testSetCookiesReturned() throws Exception {
+    HttpRequest internalRequest = new HttpRequest(REQUEST_URL);
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponse("foo".getBytes("UTF-8"))
+        .addHeader("Set-Cookie", "foo=bar; Secure")
+        .addHeader("Set-Cookie", "name=value")
+        .create();
+
+    expect(pipeline.execute(internalRequest)).andReturn(response);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+    JSONObject headers = results.getJSONObject("headers");
+    assertNotNull(headers);
+    JSONArray cookies = headers.getJSONArray("set-cookie");
+    assertNotNull(cookies);
+    assertEquals(2, cookies.length());
+    assertEquals("foo=bar; Secure", cookies.get(0));
+    assertEquals("name=value", cookies.get(1));
+  }
+
+  @Test
+  public void testLocationReturned() throws Exception {
+    HttpRequest internalRequest = new HttpRequest(REQUEST_URL);
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponse("foo".getBytes("UTF-8"))
+        .addHeader("Location", "somewhere else")
+        .create();
+
+    expect(pipeline.execute(internalRequest)).andReturn(response);
+    replay();
+
+    handler.fetch(request, recorder);
+    JSONObject results = extractJsonFromResponse();
+    JSONObject headers = results.getJSONObject("headers");
+    assertNotNull(headers);
+    JSONArray locations = headers.getJSONArray("location");
+    assertNotNull(locations);
+    assertEquals(1, locations.length());
+    assertEquals("somewhere else", locations.get(0));
+  }
+
+  @Test
+  public void testSetResponseHeaders() throws Exception {
+    HttpResponse results = new HttpResponseBuilder().create();
+    replay();
+
+    handler.setResponseHeaders(request, recorder, results);
+
+    // Just verify that they were set. Specific values are configurable.
+    assertNotNull("Expires header not set", recorder.getHeader("Expires"));
+    assertNotNull("Cache-Control header not set", recorder.getHeader("Cache-Control"));
+    assertEquals("attachment;filename=p.txt", recorder.getHeader("Content-Disposition"));
+  }
+
+  @Test
+  public void testSetContentTypeHeader() throws Exception {
+    HttpResponse results = new HttpResponseBuilder()
+        .create();
+    replay();
+    handler.setResponseHeaders(request, recorder, results);
+
+    assertEquals("application/octet-stream", recorder.getHeader("Content-Type"));
+  }
+
+  @Test
+  public void testSetResponseHeadersNoCache() throws Exception {
+    Map<String, List<String>> headers = new TreeMap<String,List<String>>(String.CASE_INSENSITIVE_ORDER);
+    headers.put("Pragma", Arrays.asList("no-cache"));
+    HttpResponse results = new HttpResponseBuilder()
+        .addHeader("Pragma", "no-cache")
+        .create();
+    replay();
+
+    handler.setResponseHeaders(request, recorder, results);
+
+    // Just verify that they were set. Specific values are configurable.
+    assertNotNull("Expires header not set", recorder.getHeader("Expires"));
+    assertEquals("no-cache", recorder.getHeader("Pragma"));
+    assertEquals("no-cache", recorder.getHeader("Cache-Control"));
+    assertEquals("attachment;filename=p.txt", recorder.getHeader("Content-Disposition"));
+  }
+
+  @Test
+  public void testSetResponseHeadersForceParam() throws Exception {
+    HttpResponse results = new HttpResponseBuilder().create();
+    expect(request.getParameter(Param.REFRESH.getKey())).andReturn("30").anyTimes();
+    replay();
+
+    // not sure why but the following line seems to help this test past deterministically
+    System.out.println("request started at " + HttpUtilTest.testStartTime);
+    handler.setResponseHeaders(request, recorder, results);
+    HttpUtilTest.checkCacheControlHeaders(HttpUtilTest.testStartTime, recorder, 30, false);
+    assertEquals("attachment;filename=p.txt", recorder.getHeader("Content-Disposition"));
+  }
+
+  @Test
+  public void testSetResponseHeadersForceParamInvalid() throws Exception {
+    HttpResponse results = new HttpResponseBuilder().create();
+    expect(request.getParameter(Param.REFRESH.getKey())).andReturn("foo").anyTimes();
+    replay();
+
+    try {
+      handler.setResponseHeaders(request, recorder, results);
+    } catch (GadgetException e) {
+      assertEquals(GadgetException.Code.INVALID_PARAMETER, e.getCode());
+    }
+  }
+
+  @Test
+  public void testGetParameter() {
+    expect(request.getParameter("foo")).andReturn("bar");
+    replay();
+
+    assertEquals("bar", MakeRequestHandler.getParameter(request, "foo", "not foo"));
+  }
+
+  @Test
+  public void testGetParameterWithNullValue() {
+    expect(request.getParameter("foo")).andReturn(null);
+    replay();
+
+    assertEquals("not foo", MakeRequestHandler.getParameter(request, "foo", "not foo"));
+  }
+
+  @Test
+  public void testGetContainerWithContainer() {
+    expect(request.getParameter(Param.CONTAINER.getKey())).andReturn("bar");
+    replay();
+
+    assertEquals("bar", MakeRequestHandler.getContainer(request));
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testGetContainerWithSynd() {
+    expect(request.getParameter(Param.CONTAINER.getKey())).andReturn(null);
+    expect(request.getParameter(Param.SYND.getKey())).andReturn("syndtainer");
+    replay();
+
+    assertEquals("syndtainer", MakeRequestHandler.getContainer(request));
+  }
+
+  @SuppressWarnings("deprecation")
+  @Test
+  public void testGetContainerNoParam() {
+    expect(request.getParameter(Param.CONTAINER.getKey())).andReturn(null);
+    expect(request.getParameter(Param.SYND.getKey())).andReturn(null);
+    replay();
+
+    assertEquals(ContainerConfig.DEFAULT_CONTAINER, MakeRequestHandler.getContainer(request));
+  }
+
+  @Test
+  public void testUserAgent() throws Exception {
+    HttpRequest expected = new HttpRequest(REQUEST_URL).addHeader("User-Agent", "ua");
+    expect(pipeline.execute(expected)).andReturn(new HttpResponse(RESPONSE_BODY));
+    expect(request.getHeader("User-Agent")).andReturn("ua");
+    replay();
+
+    handler.fetch(request, recorder);
+    verify();
+
+    JSONObject results = extractJsonFromResponse();
+    assertEquals(HttpResponse.SC_OK, results.getInt("rc"));
+    assertEquals(RESPONSE_BODY, results.get("body"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java
new file mode 100644
index 0000000..030c01e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/MakeRequestServletTest.java
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static junitx.framework.StringAssert.assertContains;
+import static junitx.framework.StringAssert.assertStartsWith;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+
+import java.util.Collections;
+import java.util.Enumeration;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.HashLockedDomainService;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.uri.LockedDomainPrefixGenerator;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for MakeRequestServlet.
+ *
+ * Tests are trivial; real tests are in MakeRequestHandlerTest.
+ */
+public class MakeRequestServletTest extends ServletTestFixture {
+  private static final Uri REQUEST_URL = Uri.parse("http://example.org/file");
+  private static final Uri REQUEST_GADGET = Uri.parse("http://example.org/file/gadget.xml");
+  private static final String RESPONSE_BODY = "Hello, world!";
+  private static final String ERROR_MESSAGE = "Broken!";
+  private static final Enumeration<String> EMPTY_ENUM
+      = Collections.enumeration(Collections.<String>emptyList());
+
+  private final GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
+  private final MakeRequestServlet servlet = new MakeRequestServlet();
+  private ContainerConfig containerConfig;
+  private Processor processor;
+  private LockedDomainService ldService;
+  private MakeRequestHandler handler;
+
+  private final HttpRequest internalRequest = new HttpRequest(REQUEST_URL);
+  private final HttpResponse internalResponse = new HttpResponse(RESPONSE_BODY);
+
+  @Before
+  public void setUp() throws Exception {
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+        "'gadgets.features':{views:" +
+        "{aliased: {aliases: ['some-alias', 'alias']}}" +
+        ",'core.io':" +
+        "{unparseableCruft :\"throw 1; < don't be evil' >\"}}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    Gadget gadget = mock(Gadget.class);
+    processor = mock(Processor.class);
+    Capture<GadgetContext> context = new Capture<GadgetContext>();
+    expect(processor.process(EasyMock.capture(context))).andReturn(gadget).anyTimes();
+    ldService = new HashLockedDomainService(containerConfig, false, mock(LockedDomainPrefixGenerator.class));
+    handler = new MakeRequestHandler(containerConfig, pipeline, rewriterRegistry, feedProcessorProvider, gadgetAdminStore, processor, ldService);
+
+    servlet.setMakeRequestHandler(handler);
+    expect(request.getHeaderNames()).andReturn(EMPTY_ENUM).anyTimes();
+    expect(request.getParameter(MakeRequestHandler.METHOD_PARAM))
+        .andReturn("GET").anyTimes();
+    expect(request.getParameter(Param.URL.getKey()))
+        .andReturn(REQUEST_URL.toString()).anyTimes();
+    expect(request.getParameter(Param.GADGET.getKey()))
+        .andReturn(REQUEST_GADGET.toString()).anyTimes();
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class))).andReturn(true);
+  }
+
+  private void setupGet() {
+    expect(request.getMethod()).andReturn("GET").anyTimes();
+  }
+
+  private void setupPost() {
+    expect(request.getMethod()).andReturn("POST").anyTimes();
+  }
+
+  private void assertResponseOk(int expectedStatus, String expectedBody) throws JSONException {
+    if (recorder.getHttpStatusCode() == HttpServletResponse.SC_OK) {
+      String body = recorder.getResponseAsString();
+      String defaultCruftMsg = "throw 1; < don't be evil' >";
+      assertStartsWith(defaultCruftMsg, body);
+      body = body.substring(defaultCruftMsg.length());
+      JSONObject object = new JSONObject(body);
+      object = object.getJSONObject(REQUEST_URL.toString());
+      assertEquals(expectedStatus, object.getInt("rc"));
+      assertEquals(expectedBody, object.getString("body"));
+    } else {
+      fail("Invalid response for request.");
+    }
+  }
+
+  public void testDoGetNormal() throws Exception {
+    setupGet();
+    expect(pipeline.execute(internalRequest)).andReturn(internalResponse);
+    replay();
+
+    servlet.doGet(request, recorder);
+
+    assertResponseOk(HttpResponse.SC_OK, RESPONSE_BODY);
+  }
+
+  @Test
+  public void testDoGetHttpError() throws Exception {
+    setupGet();
+    expect(pipeline.execute(internalRequest)).andReturn(HttpResponse.notFound());
+    replay();
+
+    servlet.doGet(request, recorder);
+
+    assertResponseOk(HttpResponse.SC_NOT_FOUND, "");
+  }
+
+  @Test
+  public void testDoGetException() throws Exception {
+    setupGet();
+    expect(pipeline.execute(internalRequest)).andThrow(
+        new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, ERROR_MESSAGE));
+    replay();
+
+    servlet.doGet(request, recorder);
+
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    assertContains(ERROR_MESSAGE, recorder.getResponseAsString());
+  }
+
+  @Test
+  public void testDoPostNormal() throws Exception {
+    setupPost();
+    expect(pipeline.execute(internalRequest)).andReturn(internalResponse);
+    replay();
+
+    servlet.doPost(request, recorder);
+
+    assertResponseOk(HttpResponse.SC_OK, RESPONSE_BODY);
+  }
+
+  @Test
+  public void testDoPostHttpError() throws Exception {
+    setupPost();
+    expect(pipeline.execute(internalRequest)).andReturn(HttpResponse.notFound());
+    replay();
+
+    servlet.doGet(request, recorder);
+
+    assertResponseOk(HttpResponse.SC_NOT_FOUND, "");
+  }
+
+  @Test
+  public void testDoPostException() throws Exception {
+    setupPost();
+    expect(pipeline.execute(internalRequest)).andThrow(
+        new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, ERROR_MESSAGE));
+    replay();
+
+    servlet.doPost(request, recorder);
+
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    assertContains(ERROR_MESSAGE, recorder.getResponseAsString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ModuleCacheTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ModuleCacheTest.java
new file mode 100644
index 0000000..e89eae4
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ModuleCacheTest.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import com.google.caja.lexer.CharProducer;
+import com.google.caja.lexer.InputSource;
+import com.google.caja.parser.ParseTreeNode;
+import com.google.caja.reporting.EchoingMessageQueue;
+import com.google.caja.reporting.MessageContext;
+import com.google.caja.reporting.MessageQueue;
+import com.google.caja.util.ContentType;
+
+import java.io.PrintWriter;
+import java.net.URI;
+
+import junit.framework.TestCase;
+
+/**
+ * Tests equality of ModuleCacheKey{,s} instances.
+ */
+public class ModuleCacheTest extends TestCase {
+
+  public final void testKeyEqualToKeyFromSameNode() throws Exception {
+    assertKeyEquality(true, key("42"), key("42"));
+    assertKeyEquality(true, key("<br>"), key("<br>"));
+  }
+
+  public final void testKeysDifferBasedOnContentType() throws Exception {
+    assertKeysDiffer(key("foo", true), key("foo", false));
+  }
+
+  public final void testKeysDifferBasedOnNodeType() throws Exception {
+    assertKeysDiffer(key("foo"), key("'foo'"));
+    assertKeysDiffer(key("<div>"), key("div", true));
+  }
+
+  public final void testKeysDifferBasedOnNodeValue() throws Exception {
+    assertKeysDiffer(key("'foo'"), key("'bar'"));
+    assertKeysDiffer(key("break foo"), key("break"));
+    assertKeysDiffer(key("break"), key("break foo"));
+  }
+
+  public final void testKeysDifferBasedOnChildren() throws Exception {
+    assertKeysDiffer(key("return"), key("return 42"));
+  }
+
+  public final void testKeysDifferBasedOnElementName() throws Exception {
+    assertKeysDiffer(key("<div>"), key("<span>"));
+  }
+
+  public final void testKeysDifferBasedOnAttributeName() throws Exception {
+    assertKeysDiffer(key("<input type=text>"), key("<input name=text>"));
+  }
+
+  public final void testKeysDifferBasedOnAttributeValue() throws Exception {
+    assertKeysDiffer(key("<input type=text>"), key("<input type=checkbox>"));
+  }
+
+  public final void testKeysDifferBasedOnText() throws Exception {
+    assertKeysDiffer(key("<div>foo</div>"), key("<div>bar</div>"));
+  }
+
+  public final void testKeysDifferBasedOnCdataSection() throws Exception {
+    assertKeysDiffer(key("<?xml version=\"1.0\"?><div><![CDATA[foo]]></div>"),
+                     key("<?xml version=\"1.0\"?><div><![CDATA[bar]]></div>"));
+  }
+
+  private static void assertKeyEquality(
+      boolean equal, ModuleCacheKey k, ModuleCacheKey j) {
+    assertEquality(equal, k, j);
+    assertEquality(equal, k.asSingleton(), j.asSingleton());
+  }
+
+  private static void assertEquality(boolean equal, Object a, Object b) {
+    assertEquals(equal, a.equals(b));
+    if (equal) {
+      assertEquals(a.hashCode(), b.hashCode());
+    }
+  }
+
+  private static void assertKeysDiffer(ModuleCacheKey k, ModuleCacheKey j) {
+    assertKeyEquality(false, k, j);
+  }
+
+  private ModuleCacheKey key(String codeSnippet) throws Exception {
+    boolean isHtml = codeSnippet.trim().startsWith("<");
+    return key(codeSnippet, isHtml);
+  }
+
+  private ModuleCacheKey key(String codeSnippet, boolean isHtml) throws Exception {
+    MessageQueue mq = new EchoingMessageQueue(
+        new PrintWriter(System.err, true), new MessageContext());
+    InputSource is = new InputSource(new URI("test:///" + getName()));
+    ParseTreeNode node = CajaContentRewriter.parse(
+        is, CharProducer.Factory.fromString(codeSnippet, is),
+        isHtml ? "text/html" : "text/javascript",
+        mq);
+    return new ModuleCacheKey(isHtml ? ContentType.HTML : ContentType.JS, node);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/OAuthCallbackServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/OAuthCallbackServletTest.java
new file mode 100644
index 0000000..e488fc9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/OAuthCallbackServletTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.expect;
+
+import org.apache.shindig.common.crypto.BasicBlobCrypter;
+import org.apache.shindig.common.crypto.BlobCrypter;
+import org.apache.shindig.gadgets.oauth.OAuthCallbackState;
+import org.junit.Test;
+import org.junit.Assert;
+
+/**
+ * Tests for OAuth callback servlet.
+ */
+public class OAuthCallbackServletTest extends ServletTestFixture {
+
+  @Test
+  public void testServlet() throws Exception {
+    OAuthCallbackServlet servlet = new OAuthCallbackServlet();
+    replay();
+    servlet.doGet(this.request, this.recorder);
+    verify();
+    assertEquals("text/html; charset=UTF-8", this.recorder.getContentType());
+    String body = this.recorder.getResponseAsString();
+    Assert.assertNotSame("body is " + body, body.indexOf("window.close()"), -1);
+  }
+
+  @Test
+  public void testServletWithCallback() throws Exception {
+    BlobCrypter crypter = new BasicBlobCrypter("00000000000000000000".getBytes());
+    OAuthCallbackState state = new OAuthCallbackState(crypter);
+    OAuthCallbackServlet servlet = new OAuthCallbackServlet();
+    servlet.setStateCrypter(crypter);
+    state.setRealCallbackUrl("http://www.example.com/callback");
+    expect(request.getParameter("cs")).andReturn(state.getEncryptedState());
+    expect(request.getQueryString()).andReturn("cs=foo&bar=baz");
+    replay();
+    servlet.doGet(this.request, this.recorder);
+    verify();
+    assertEquals(302, this.recorder.getHttpStatusCode());
+    assertEquals("http://www.example.com/callback?bar=baz", this.recorder.getHeader("Location"));
+    String cacheControl = this.recorder.getHeader("Cache-Control");
+    assertEquals("private,max-age=3600", cacheControl);
+  }
+
+  @Test
+  public void testServletWithCallback_noQueryParams() throws Exception {
+    BlobCrypter crypter = new BasicBlobCrypter("00000000000000000000".getBytes());
+    OAuthCallbackState state = new OAuthCallbackState(crypter);
+    OAuthCallbackServlet servlet = new OAuthCallbackServlet();
+    servlet.setStateCrypter(crypter);
+    state.setRealCallbackUrl("http://www.example.com/callback");
+    expect(request.getParameter("cs")).andReturn(state.getEncryptedState());
+    expect(request.getQueryString()).andReturn("cs=foo");
+    replay();
+    servlet.doGet(this.request, this.recorder);
+    verify();
+    assertEquals(302, this.recorder.getHttpStatusCode());
+    assertEquals("http://www.example.com/callback", this.recorder.getHeader("Location"));
+    String cacheControl = this.recorder.getHeader("Cache-Control");
+    assertEquals("private,max-age=3600", cacheControl);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java
new file mode 100644
index 0000000..d36c64e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyHandlerTest.java
@@ -0,0 +1,589 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.admin.GadgetAdminStore;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.rewrite.CaptureRewriter;
+import org.apache.shindig.gadgets.rewrite.DefaultResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.DomWalker;
+import org.apache.shindig.gadgets.rewrite.MutableContent;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.RewritingException;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ProxyHandlerTest extends EasyMockTestCase {
+  private final static String GADGET = "http://some/gadget.xml";
+  private final static String URL_ONE = "http://www.example.org/test.html";
+  private final static String DATA_ONE = "hello world";
+  private final static Integer LONG_LIVED_REFRESH = (365 * 24 * 60 * 60);  // 1 year
+
+
+  public final RequestPipeline pipeline = mock(RequestPipeline.class);
+  private GadgetAdminStore gadgetAdminStore = mock(GadgetAdminStore.class);
+  public CaptureRewriter rewriter = new CaptureRewriter();
+  public ResponseRewriterRegistry rewriterRegistry
+      = new DefaultResponseRewriterRegistry(Arrays.<ResponseRewriter>asList(rewriter), null);
+  private ProxyUriManager.ProxyUri request;
+
+  private final ProxyHandler proxyHandler
+      = new ProxyHandler(pipeline, rewriterRegistry, true, gadgetAdminStore, LONG_LIVED_REFRESH);
+
+  private void expectGetAndReturnData(String url, byte[] data) throws Exception {
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    HttpResponse resp = new HttpResponseBuilder().setResponse(data).create();
+    expect(pipeline.execute(req)).andReturn(resp);
+  }
+
+  private void expectGetAndReturnHeaders(String url, Map<String, List<String>> headers)
+      throws Exception {
+    HttpRequest req = new HttpRequest(Uri.parse(url));
+    HttpResponse resp = new HttpResponseBuilder().addAllHeaders(headers).create();
+    expect(pipeline.execute(req)).andReturn(resp);
+  }
+
+  private void setupGadgetAdminMock(boolean isWhitelisted) {
+    expect(gadgetAdminStore.isWhitelisted(isA(String.class), isA(String.class)))
+    .andReturn(isWhitelisted);
+  }
+
+  private void setupProxyRequestMock(String host, String url,
+      boolean noCache, int refresh, String rewriteMime, String fallbackUrl) throws Exception {
+    request = new ProxyUriManager.ProxyUri(
+        refresh, false, noCache, ContainerConfig.DEFAULT_CONTAINER, GADGET, Uri.parse(url));
+    request.setFallbackUrl(fallbackUrl);
+    request.setRewriteMimeType(rewriteMime);
+  }
+
+  private void setupNoArgsProxyRequestMock(String host, String url) throws Exception {
+    request = new ProxyUriManager.ProxyUri(
+        -1, false, false, ContainerConfig.DEFAULT_CONTAINER, GADGET,
+        url != null ? Uri.parse(url) : null);
+  }
+
+  private ResponseRewriter getResponseRewriterThatThrowsExceptions(
+      final StringBuilder stringBuilder) {
+    return new DomWalker.Rewriter() {
+      @Override
+      public void rewrite(Gadget gadget, MutableContent content)
+          throws RewritingException {
+        stringBuilder.append("exceptionThrown");
+        throw new RewritingException("sad", 404);
+      }
+
+      @Override
+      public void rewrite(HttpRequest request, HttpResponseBuilder builder, Gadget gadget)
+          throws RewritingException {
+        stringBuilder.append("exceptionThrown");
+        throw new RewritingException("sad", 404);
+      }
+    };
+  }
+
+  @Test
+  public void testNonWhitelistedGadget() throws Exception {
+    String url = "http://example.org/mypage.html";
+    String domain = "example.org";
+    setupProxyRequestMock(domain, url, true, -1, null, null);
+    setupGadgetAdminMock(false);
+    replay();
+    boolean exceptionCaught = false;
+    try {
+      proxyHandler.fetch(request);
+    } catch (GadgetException e) {
+      exceptionCaught = true;
+      assertEquals(GadgetException.Code.NON_WHITELISTED_GADGET, e.getCode());
+      assertEquals(HttpResponse.SC_FORBIDDEN, e.getHttpStatusCode());
+    }
+    assertTrue(exceptionCaught);
+    verify();
+  }
+
+  @Test
+  public void testInvalidHeaderDropped() throws Exception {
+    String url = "http://example.org/mypage.html";
+    String domain = "example.org";
+
+    setupProxyRequestMock(domain, url, true, -1, null, null);
+    setupGadgetAdminMock(true);
+
+    HttpRequest req = new HttpRequest(Uri.parse(url))
+        .setIgnoreCache(true);
+    String contentType = "text/html; charset=UTF-8";
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponseString("Hello")
+        .addHeader("Content-Type", contentType)
+        .addHeader("Content-Length", "200")  // Disallowed header.
+        .addHeader(":", "someDummyValue") // Invalid header name.
+        .create();
+
+    expect(pipeline.execute(req)).andReturn(resp);
+
+    replay();
+
+    HttpResponse recorder = proxyHandler.fetch(request);
+
+    verify();
+    assertNull(recorder.getHeader(":"));
+    assertNull(recorder.getHeader("Content-Length"));
+    assertEquals(recorder.getHeader("Content-Type"), contentType);
+  }
+
+  @Test
+  public void testLockedDomainEmbed() throws Exception {
+    setupNoArgsProxyRequestMock("www.example.com", URL_ONE);
+    expectGetAndReturnData(URL_ONE, DATA_ONE.getBytes());
+    setupGadgetAdminMock(true);
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    assertEquals(DATA_ONE, response.getResponseAsString());
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test(expected=GadgetException.class)
+  public void testNoUrl() throws Exception {
+    setupNoArgsProxyRequestMock("www.example.com", null);
+    replay();
+
+    proxyHandler.fetch(request);
+    fail("Proxy should raise exception if there is no url");
+  }
+
+  @Test
+  public void testHttpRequestFillsParentAndContainer() throws Exception {
+    setupNoArgsProxyRequestMock("www.example.com", URL_ONE);
+    setupGadgetAdminMock(true);
+    //HttpRequest req = new HttpRequest(Uri.parse(URL_ONE));
+    HttpResponse resp = new HttpResponseBuilder().setResponse(DATA_ONE.getBytes()).create();
+
+    Capture<HttpRequest> httpRequest = new Capture<HttpRequest>();
+    expect(pipeline.execute(capture(httpRequest))).andReturn(resp);
+
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    // Check that the HttpRequest passed in has all the relevant fields sets
+    assertEquals("default", httpRequest.getValue().getContainer());
+    assertEquals(Uri.parse(URL_ONE), httpRequest.getValue().getUri());
+
+    assertEquals(DATA_ONE, response.getResponseAsString());
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testHeadersPreserved() throws Exception {
+    // Some headers may be blacklisted. These are OK.
+    String url = "http://example.org/file.evil";
+    String domain = "example.org";
+    String contentType = "text/evil; charset=UTF-8";
+    String magicGarbage = "fadfdfdfd";
+    Map<String, List<String>> headers = Maps.newHashMap();
+    headers.put("Content-Type", Arrays.asList(contentType));
+    headers.put("X-Magic-Garbage", Arrays.asList(magicGarbage));
+
+    setupNoArgsProxyRequestMock(domain, url);
+    setupGadgetAdminMock(true);
+    expectGetAndReturnHeaders(url, headers);
+
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    assertEquals(contentType, response.getHeader("Content-Type"));
+    assertEquals(magicGarbage, response.getHeader("X-Magic-Garbage"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testOctetSetOnNullContentType() throws Exception {
+    String url = "http://example.org/file.evil";
+    String domain = "example.org";
+
+    setupNoArgsProxyRequestMock(domain, url);
+    setupGadgetAdminMock(true);
+    expectGetAndReturnHeaders(url, Maps.<String, List<String>>newHashMap());
+
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    assertEquals("application/octet-stream", response.getHeader("Content-Type"));
+    assertNotNull(response.getHeader("Content-Disposition"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testNoContentDispositionForFlash() throws Exception {
+    setupGadgetAdminMock(true);
+    assertNoContentDispositionForFlash("application/x-shockwave-flash");
+  }
+
+  @Test
+  public void testNoContentDispositionForFlashUtf8() throws Exception {
+    setupGadgetAdminMock(true);
+    assertNoContentDispositionForFlash("application/x-shockwave-flash;charset=utf-8");
+  }
+
+  private void assertNoContentDispositionForFlash(String contentType) throws Exception {
+    // Some headers may be blacklisted. These are OK.
+    String url = "http://example.org/file.evil";
+    String domain = "example.org";
+    Map<String, List<String>> headers =
+        ImmutableMap.of("Content-Type", Arrays.asList(contentType));
+
+    setupNoArgsProxyRequestMock(domain, url);
+    expectGetAndReturnHeaders(url, headers);
+
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    assertEquals(contentType, response.getHeader("Content-Type"));
+    assertNull(response.getHeader("Content-Disposition"));
+    assertTrue(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testGetFallback() throws Exception {
+    String url = "http://example.org/file.evil";
+    String domain = "example.org";
+    String fallback_url = "http://fallback.com/fallback.png";
+
+    setupProxyRequestMock(domain, url, true, -1, null, fallback_url);
+    setupGadgetAdminMock(true);
+
+    HttpRequest req = new HttpRequest(Uri.parse(url)).setIgnoreCache(true);
+    HttpResponse resp = HttpResponse.error();
+    HttpResponse fallback_resp = new HttpResponse("Fallback");
+    expect(pipeline.execute(req)).andReturn(resp);
+    expect(pipeline.execute(isA(HttpRequest.class))).andReturn(fallback_resp);
+
+    replay();
+    proxyHandler.fetch(request);
+    verify();
+  }
+
+  @Test
+  public void testNoCache() throws Exception {
+    String url = "http://example.org/file.evil";
+    String domain = "example.org";
+
+    setupProxyRequestMock(domain, url, true, -1, null, null);
+    setupGadgetAdminMock(true);
+
+    HttpRequest req = new HttpRequest(Uri.parse(url)).setIgnoreCache(true);
+    HttpResponse resp = new HttpResponse("Hello");
+    expect(pipeline.execute(req)).andReturn(resp);
+
+    replay();
+    proxyHandler.fetch(request);
+    verify();
+  }
+
+  // ProxyHandler throws INTERNAL_SERVER_ERRORS without isRecoverable() check.
+  @Test
+  public void testRecoverableRewritingException() throws Exception {
+    String url = "http://example.org/mypage.html";
+    String domain = "example.org";
+
+    setupProxyRequestMock(domain, url, true, -1, null, null);
+    setupGadgetAdminMock(true);
+
+    String contentType = "text/html; charset=UTF-8";
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponseString("Hello")
+        .addHeader("Content-Type", contentType)
+        .create();
+
+    expect(pipeline.execute((HttpRequest) EasyMock.anyObject())).andReturn(resp);
+
+    replay();
+
+    final StringBuilder stringBuilder = new StringBuilder("");
+    ResponseRewriter rewriter = getResponseRewriterThatThrowsExceptions(stringBuilder);
+
+    ResponseRewriterRegistry rewriterRegistry =
+        new DefaultResponseRewriterRegistry(
+            Arrays.<ResponseRewriter>asList(rewriter), null);
+    ProxyHandler proxyHandler = new ProxyHandler(pipeline, rewriterRegistry, true, gadgetAdminStore,
+        LONG_LIVED_REFRESH);
+
+    request.setReturnOriginalContentOnError(true);
+    HttpResponse recorder = proxyHandler.fetch(request);
+
+    verify();
+
+    // Ensure that original content is returned.
+    assertEquals(recorder.getHeader("Content-Type"), contentType);
+    assertEquals("Hello", recorder.getResponseAsString());
+    assertEquals("exceptionThrown", stringBuilder.toString());
+  }
+
+  @Test
+  public void testThrowExceptionIfReturnOriginalContentOnErrorNotSet()
+      throws Exception {
+    String url = "http://example.org/mypage.html";
+    String domain = "example.org";
+
+    setupProxyRequestMock(domain, url, true, -1, null, null);
+    setupGadgetAdminMock(true);
+
+    String contentType = "text/html; charset=UTF-8";
+    HttpResponse resp = new HttpResponseBuilder()
+        .setResponseString("Hello")
+        .addHeader("Content-Type", contentType)
+        .create();
+
+    expect(pipeline.execute((HttpRequest) EasyMock.anyObject())).andReturn(resp);
+
+    replay();
+
+    final StringBuilder stringBuilder = new StringBuilder("");
+    ResponseRewriter rewriter = getResponseRewriterThatThrowsExceptions(stringBuilder);
+
+    ResponseRewriterRegistry rewriterRegistry =
+        new DefaultResponseRewriterRegistry(
+            Arrays.<ResponseRewriter>asList(rewriter), null);
+    ProxyHandler proxyHandler = new ProxyHandler(pipeline, rewriterRegistry, true, gadgetAdminStore,
+        LONG_LIVED_REFRESH);
+
+    boolean exceptionCaught = false;
+    try {
+      proxyHandler.fetch(request);
+    } catch (GadgetException e) {
+      exceptionCaught = true;
+      assertEquals(404, e.getHttpStatusCode());
+    }
+    assertTrue(exceptionCaught);
+    assertEquals("exceptionThrown", stringBuilder.toString());
+  }
+
+  /**
+   * Override HttpRequest equals to check for cache control fields
+   */
+  static class HttpRequestCache extends HttpRequest {
+    public HttpRequestCache(Uri uri) {
+      super(uri);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj == this) {
+        return true;
+      }
+      if (!(obj instanceof HttpRequest)) {
+        return false;
+      }
+      HttpRequest req = (HttpRequest)obj;
+      return super.equals(obj) && req.getCacheTtl() == getCacheTtl() &&
+              req.getIgnoreCache() == getIgnoreCache();
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(super.hashCode(), getCacheTtl(), getIgnoreCache());
+    }
+  }
+
+  @Test
+  public void testWithCache() throws Exception {
+    String url = "http://example.org/file.evil";
+    String domain = "example.org";
+    HttpResponse.setTimeSource(new FakeTimeSource());
+
+    setupProxyRequestMock(domain, url, false, 120, null, null);
+    setupGadgetAdminMock(true);
+
+    HttpRequest req = new HttpRequestCache(Uri.parse(url)).setCacheTtl(120).setIgnoreCache(false);
+    HttpResponseBuilder resp = new HttpResponseBuilder().setCacheTtl(1234);
+    resp.setContent("Hello");
+    expect(pipeline.execute(req)).andReturn(resp.create());
+
+    replay();
+    HttpResponse proxyResp = proxyHandler.fetch(request);
+    assertEquals(120, proxyResp.getCacheTtl() / 1000);
+    verify();
+  }
+
+  @Test
+  public void testWithBadTtl() throws Exception {
+    String url = "http://example.org/file.evil";
+    String domain = "example.org";
+    HttpResponse.setTimeSource(new FakeTimeSource());
+
+    setupProxyRequestMock(domain, url, false, -1, null, null);
+    setupGadgetAdminMock(true);
+
+    HttpRequest req = new HttpRequestCache(Uri.parse(url)).setCacheTtl(-1).setIgnoreCache(false);
+    HttpResponseBuilder resp = new HttpResponseBuilder().setCacheTtl(1234);
+    resp.setContent("Hello");
+    expect(pipeline.execute(req)).andReturn(resp.create());
+
+    replay();
+    HttpResponse proxyResp = proxyHandler.fetch(request);
+    assertEquals(1234, proxyResp.getCacheTtl() / 1000);
+    verify();
+  }
+
+  @Test
+  public void testWithOAuth2() throws Exception {
+    String url = "http://example.org/oauth2";
+    String domain = "example.org";
+    setupProxyRequestMock(domain, url, false, -1, null, null);
+    setupGadgetAdminMock(true);
+    Map<String, String> options = new HashMap<String, String>();
+    options.put("OAUTH_SERVICE_NAME", "example");
+    options.put("OAUTH_SCOPE", "scope1 scope2");
+    request.setAuthType(AuthType.OAUTH2);
+    request.setOAuth2Arguments(new OAuth2Arguments(AuthType.OAUTH2, options));
+
+    options = new HashMap<String, String>();
+    options.put("OAUTH_SERVICE_NAME", "example");
+    options.put("OAUTH_SCOPE", "scope1 scope2");
+    HttpRequest req = new HttpRequest(Uri.parse(url))
+        .setAuthType(AuthType.OAUTH2)
+        .setGadget(Uri.parse(""))
+        .setContainer("default")
+        .setOAuth2Arguments(new OAuth2Arguments(AuthType.OAUTH2, options));
+
+    HttpResponse resp = new HttpResponseBuilder()
+    .setResponseString("Hello")
+    .create();
+    expect(pipeline.execute(req)).andReturn(resp);
+
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    assertEquals("Hello", response.getResponseAsString());
+  }
+
+  @Test
+  public void testWithOAuth() throws Exception {
+    String url = "http://example.org/oauth2";
+    String domain = "example.org";
+    setupProxyRequestMock(domain, url, false, -1, null, null);
+    setupGadgetAdminMock(true);
+    Map<String, String> options = new HashMap<String, String>();
+    options.put("OAUTH_SERVICE_NAME", "example");
+    request.setAuthType(AuthType.OAUTH);
+    request.setOAuthArguments(new OAuthArguments(AuthType.OAUTH, options));
+
+    options = new HashMap<String, String>();
+    options.put("OAUTH_SERVICE_NAME", "example");
+    HttpRequest req = new HttpRequest(Uri.parse(url))
+        .setAuthType(AuthType.OAUTH)
+        .setGadget(Uri.parse(""))
+        .setContainer("default")
+        .setOAuthArguments(new OAuthArguments(AuthType.OAUTH, options));
+
+    HttpResponse resp = new HttpResponseBuilder()
+    .setResponseString("Hello")
+    .create();
+    expect(pipeline.execute(req)).andReturn(resp);
+
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    assertEquals("Hello", response.getResponseAsString());
+  }
+
+  private void expectMime(String expectedMime, String contentMime, String outputMime)
+      throws Exception {
+    String url = "http://example.org/file.img?" + Param.REWRITE_MIME_TYPE.getKey() +
+        '=' + expectedMime;
+    String domain = "example.org";
+
+    setupProxyRequestMock(domain, url, false, -1, expectedMime, null);
+    setupGadgetAdminMock(true);
+
+    HttpRequest req = new HttpRequest(Uri.parse(url))
+        .setRewriteMimeType(expectedMime);
+
+    HttpResponse resp = new HttpResponseBuilder()
+      .setResponseString("Hello")
+      .addHeader("Content-Type", contentMime)
+      .create();
+
+    expect(pipeline.execute(req)).andReturn(resp);
+
+    replay();
+    HttpResponse response = proxyHandler.fetch(request);
+    verify();
+
+    assertEquals(outputMime, response.getHeader("Content-Type"));
+    reset();
+  }
+
+  @Test
+  public void testMimeMatchPass() throws Exception {
+    expectMime("text/css", "text/css", "text/css; charset=UTF-8");
+  }
+
+  @Test
+  public void testMimeMatchPassWithAdditionalAttributes() throws Exception {
+    expectMime("text/css", "text/css", "text/css; charset=UTF-8");
+  }
+
+  @Test
+  public void testMimeMatchOverrideNonMatch() throws Exception {
+    expectMime("text/css", "image/png", "text/css; charset=UTF-8");
+  }
+
+  @Test
+  public void testMimeMatchVarySupport() throws Exception {
+    // We use CaptureRewrite which always rewrite - always set encoding
+    expectMime("image/*", "image/gif", "image/gif");
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java
new file mode 100644
index 0000000..cfbb931
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ProxyServletTest.java
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static junitx.framework.StringAssert.assertContains;
+import static org.easymock.EasyMock.expect;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.oauth.OAuthArguments;
+import org.apache.shindig.gadgets.oauth2.OAuth2Arguments;
+import org.apache.shindig.gadgets.uri.ProxyUriManager;
+import org.apache.shindig.gadgets.uri.ProxyUriManager.ProxyUri;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests for ProxyServlet.
+ *
+ * Tests are trivial; real tests are in ProxyHandlerTest.
+ */
+public class ProxyServletTest extends ServletTestFixture {
+  private static final Uri REQUEST_URL = Uri.parse("http://example.org/file");
+  private static final String BASIC_SYNTAX_URL = "http://opensocial.org/proxy?foo=bar&url="
+          + REQUEST_URL;
+  private static final String RESPONSE_BODY = "Hello, world!";
+  private static final String ERROR_MESSAGE = "Broken!";
+  private static final String POST_CONTENT = "my post stuff";
+  private static final String POST_METHOD = "POST";
+
+  private ServletInputStream postContentStream = new ServletInputStream() {
+    InputStream is = new ByteArrayInputStream(POST_CONTENT.getBytes());
+    @Override
+    public int read() throws IOException {
+      return is.read();
+    }
+
+    @Override
+    public void close() throws IOException {
+      is.close();
+    }
+
+  };
+
+  private final ProxyUriManager proxyUriManager = mock(ProxyUriManager.class);
+  private final LockedDomainService lockedDomainService = mock(LockedDomainService.class);
+  private final ProxyHandler proxyHandler = mock(ProxyHandler.class);
+  private final ProxyServlet servlet = new ProxyServlet();
+  private final ProxyUriManager.ProxyUri proxyUri = mock(ProxyUriManager.ProxyUri.class);
+
+  @Before
+  public void setUp() throws Exception {
+    servlet.setProxyHandler(proxyHandler);
+    servlet.setProxyUriManager(proxyUriManager);
+    servlet.setLockedDomainService(lockedDomainService);
+  }
+
+  private void setupRequest(String str) throws Exception {
+    setupRequest(str, true);
+  }
+
+  private void setupRequest(String str, boolean ldSafe) throws Exception {
+    Uri uri = Uri.parse(str);
+
+    expect(request.getScheme()).andReturn(uri.getScheme());
+    expect(request.getServerName()).andReturn(uri.getAuthority());
+    expect(request.getServerPort()).andReturn(80);
+    expect(request.getRequestURI()).andReturn(uri.getPath());
+    expect(request.getQueryString()).andReturn(uri.getQuery());
+    expect(request.getHeader("Host")).andReturn(uri.getAuthority());
+    expect(proxyUriManager.process(uri)).andReturn(proxyUri);
+    expect(lockedDomainService.isSafeForOpenProxy(uri.getAuthority())).andReturn(ldSafe);
+  }
+
+  private void assertResponseOk(int expectedStatus, String expectedBody) {
+    assertEquals(expectedStatus, recorder.getHttpStatusCode());
+    assertEquals(expectedBody, recorder.getResponseAsString());
+  }
+
+  @Test
+  public void testIfModifiedSinceAlwaysReturnsEarly() throws Exception {
+    expect(request.getHeader("If-Modified-Since")).andReturn("Yes, this is an invalid header");
+
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+
+    assertEquals(HttpServletResponse.SC_NOT_MODIFIED, recorder.getHttpStatusCode());
+    assertFalse(rewriter.responseWasRewritten());
+  }
+
+  @Test
+  public void testDoGetNormal() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL);
+    expect(proxyHandler.fetch(proxyUri)).andReturn(new HttpResponse(RESPONSE_BODY));
+
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+
+    assertResponseOk(HttpResponse.SC_OK, RESPONSE_BODY);
+  }
+
+  @Test
+  public void testDoGetHttpError() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL);
+    expect(proxyHandler.fetch(proxyUri)).andReturn(HttpResponse.notFound());
+
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+
+    assertResponseOk(HttpResponse.SC_NOT_FOUND, "");
+  }
+
+  @Test
+  public void testDoGetException() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL);
+    expect(proxyHandler.fetch(proxyUri)).andThrow(
+            new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, ERROR_MESSAGE));
+
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    assertContains(ERROR_MESSAGE, recorder.getResponseAsString());
+  }
+
+  @Test
+  public void testDoGetNormalWithLockedDomainUnsafe() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL, false);
+
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    assertContains("wrong domain", recorder.getResponseAsString());
+  }
+
+  @Test
+  public void testDoPostNormal() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL);
+    expect(request.getInputStream()).andReturn(postContentStream);
+    expect(request.getMethod()).andReturn(POST_METHOD);
+    expect(proxyHandler.fetch(proxyUri, POST_CONTENT)).andReturn(new HttpResponse(RESPONSE_BODY));
+
+    replay();
+    servlet.doPost(request, recorder);
+    verify();
+
+    assertResponseOk(HttpResponse.SC_OK, RESPONSE_BODY);
+  }
+
+  @Test
+  public void testDoPostHttpError() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL);
+    expect(proxyHandler.fetch(proxyUri, POST_CONTENT)).andReturn(HttpResponse.notFound());
+    expect(request.getMethod()).andReturn(POST_METHOD);
+    expect(request.getInputStream()).andReturn(postContentStream);
+
+    replay();
+    servlet.doPost(request, recorder);
+    verify();
+
+    assertResponseOk(HttpResponse.SC_NOT_FOUND, "");
+  }
+
+  @Test
+  public void testDoPostException() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL);
+    expect(request.getInputStream()).andReturn(postContentStream);
+    expect(request.getMethod()).andReturn(POST_METHOD);
+    expect(proxyHandler.fetch(proxyUri, POST_CONTENT)).andThrow(
+            new GadgetException(GadgetException.Code.FAILED_TO_RETRIEVE_CONTENT, ERROR_MESSAGE));
+
+    replay();
+    servlet.doPost(request, recorder);
+    verify();
+
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    assertContains(ERROR_MESSAGE, recorder.getResponseAsString());
+  }
+
+  @Test
+  public void testDoPostNormalWithLockedDomainUnsafe() throws Exception {
+    setupRequest(BASIC_SYNTAX_URL, false);
+
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+
+    assertEquals(HttpServletResponse.SC_BAD_REQUEST, recorder.getHttpStatusCode());
+    assertContains("wrong domain", recorder.getResponseAsString());
+  }
+
+  @Test
+  public void testDoGetWithOAuth2() throws Exception {
+    Map<String, String> options = new HashMap<String, String>();
+    options.put("OAUTH_SERVICE_NAME", "example");
+    ProxyUriManager.ProxyUri proxyUri = new ProxyUri(-1, false, true, "default", "http://example.org/gadget.xml", REQUEST_URL);
+    proxyUri.setAuthType(AuthType.OAUTH2);
+
+    Uri uri = Uri.parse(BASIC_SYNTAX_URL + "&authz=oauth2&OAUTH_SERVICE_NAME=example&container=default&gadget=http://example.org/gadget.xml");
+    expect(proxyUriManager.process(uri)).andReturn(proxyUri);
+    expect(request.getScheme()).andReturn(uri.getScheme());
+    expect(request.getServerName()).andReturn(uri.getAuthority());
+    expect(request.getServerPort()).andReturn(80);
+    expect(request.getRequestURI()).andReturn(uri.getPath());
+    expect(request.getQueryString()).andReturn(uri.getQuery());
+    expect(request.getHeader("Host")).andReturn(uri.getAuthority());
+    expect(request.getParameter("OAUTH_SERVICE_NAME")).andReturn("example");
+    expect(request.getParameterNames()).andReturn(Collections.enumeration(options.keySet()));
+    expect(lockedDomainService.isSafeForOpenProxy(uri.getAuthority())).andReturn(true);
+
+    ProxyUriManager.ProxyUri pUri = new ProxyUri(-1, false, true, "default", "http://example.org/gadget.xml", REQUEST_URL);
+    pUri.setAuthType(AuthType.OAUTH2);
+    pUri.setOAuth2Arguments(new OAuth2Arguments(AuthType.OAUTH2, options));
+
+    expect(proxyHandler.fetch(pUri)).andReturn(new HttpResponse(RESPONSE_BODY));
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+    assertResponseOk(HttpResponse.SC_OK, RESPONSE_BODY);
+  }
+
+  @Test
+  public void testDoGetWithOAuth() throws Exception {
+    Map<String, String> options = new HashMap<String, String>();
+    options.put("OAUTH_SERVICE_NAME", "example");
+    ProxyUriManager.ProxyUri proxyUri = new ProxyUri(-1, false, true, "default", "http://example.org/gadget.xml", REQUEST_URL);
+    proxyUri.setAuthType(AuthType.OAUTH);
+
+    Uri uri = Uri.parse(BASIC_SYNTAX_URL + "&authz=oauth&OAUTH_SERVICE_NAME=example&container=default&gadget=http://example.org/gadget.xml");
+    expect(proxyUriManager.process(uri)).andReturn(proxyUri);
+    expect(request.getScheme()).andReturn(uri.getScheme());
+    expect(request.getServerName()).andReturn(uri.getAuthority());
+    expect(request.getServerPort()).andReturn(80);
+    expect(request.getRequestURI()).andReturn(uri.getPath());
+    expect(request.getQueryString()).andReturn(uri.getQuery());
+    expect(request.getHeader("Host")).andReturn(uri.getAuthority());
+    expect(request.getParameter("OAUTH_SERVICE_NAME")).andReturn("example");
+    expect(request.getParameterNames()).andReturn(Collections.enumeration(options.keySet()));
+    expect(lockedDomainService.isSafeForOpenProxy(uri.getAuthority())).andReturn(true);
+
+    ProxyUriManager.ProxyUri pUri = new ProxyUri(-1, false, true, "default", "http://example.org/gadget.xml", REQUEST_URL);
+    pUri.setAuthType(AuthType.OAUTH);
+    pUri.setOAuthArguments(new OAuthArguments(AuthType.OAUTH, options));
+
+    expect(proxyHandler.fetch(pUri)).andReturn(new HttpResponse(RESPONSE_BODY));
+    replay();
+    servlet.doGet(request, recorder);
+    verify();
+    assertResponseOk(HttpResponse.SC_OK, RESPONSE_BODY);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/RpcServletTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/RpcServletTest.java
new file mode 100644
index 0000000..f6cc451
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/RpcServletTest.java
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Tests for RpcServlet.
+ */
+public class RpcServletTest extends Assert {
+  private RpcServlet servlet;
+  private JsonRpcHandler handler;
+
+  @Before
+  public void setUp() throws Exception {
+    servlet = new RpcServlet();
+    handler = createMock(JsonRpcHandler.class);
+    servlet.setJsonRpcHandler(handler);
+    servlet.setJSONPAllowed(true);
+  }
+
+  @Test
+  public void testDoGetNormal() throws Exception {
+    HttpServletRequest request = createGetRequest("{\"gadgets\":[]}",
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._");
+    HttpServletResponse response = createHttpResponse("Content-Disposition",
+        "attachment;filename=rpc.txt", "application/json; charset=utf-8",
+        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz._({\"GADGETS\":[]})",
+        HttpServletResponse.SC_OK);
+    JSONObject handlerResponse = new JSONObject("{\"GADGETS\":[]}");
+    expect(handler.process(isA(JSONObject.class))).andReturn(handlerResponse);
+    replay(handler);
+    servlet.doGet(request, response);
+    verify(response);
+  }
+
+  @Test
+  public void testDisallowJSONP() throws Exception {
+    servlet.setJSONPAllowed(false);
+    HttpServletRequest request = createGetRequest("{\"gadgets\":[]}",null);
+    HttpServletResponse response = createHttpResponse("Content-Disposition",
+        "attachment;filename=rpc.txt", "application/json; charset=utf-8",
+        "{\"GADGETS\":[]}", HttpServletResponse.SC_OK);
+    JSONObject handlerResponse = new JSONObject("{\"GADGETS\":[]}");
+    expect(handler.process(isA(JSONObject.class))).andReturn(handlerResponse);
+    replay(handler);
+    servlet.doGet(request, response);
+    verify(response);
+    servlet.setJSONPAllowed(true);
+  }
+
+  @Test
+  public void testDoGetWithHandlerRpcException() throws Exception {
+    HttpServletRequest request = createGetRequest("{\"gadgets\":[]}", "function");
+    HttpServletResponse response = createHttpResponse("rpcExceptionMessage",
+        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+    expect(handler.process(isA(JSONObject.class))).andThrow(
+        new RpcException("rpcExceptionMessage"));
+    replay(handler);
+    servlet.doGet(request, response);
+    verify(response);
+  }
+
+  @Test
+  public void testDoGetWithHandlerJsonException() throws Exception {
+    HttpServletRequest request = createGetRequest("{\"gadgets\":[]}", "function");
+    HttpServletResponse response = createHttpResponse("Malformed JSON request.",
+        HttpServletResponse.SC_BAD_REQUEST);
+    expect(handler.process(isA(JSONObject.class))).andThrow(new JSONException("json"));
+    replay(handler);
+    servlet.doGet(request, response);
+    verify(response);
+  }
+
+  @Test
+  public void testDoGetWithMissingReqParam() throws Exception {
+    HttpServletRequest request = createGetRequest(null, "function");
+    HttpServletResponse response = createHttpResponse(null, HttpServletResponse.SC_BAD_REQUEST);
+    servlet.doGet(request, response);
+    verify(response);
+  }
+
+  @Test
+  public void testDoGetWithMissingCallbackParam() throws Exception {
+    HttpServletRequest request = createGetRequest("{\"gadgets\":[]}", null);
+    HttpServletResponse response = createHttpResponse(null, HttpServletResponse.SC_BAD_REQUEST);
+    servlet.doGet(request, response);
+    verify(response);
+  }
+
+  @Test
+  public void testDoGetWithBadCallbackParamValue() throws Exception {
+    HttpServletRequest request = createGetRequest("{\"gadgets\":[]}", "/'!=");
+    HttpServletResponse response = createHttpResponse(null, HttpServletResponse.SC_BAD_REQUEST);
+    servlet.doGet(request, response);
+    verify(response);
+  }
+
+  private HttpServletRequest createGetRequest(String reqParamValue, String callbackParamValue) {
+    HttpServletRequest result = createMock(HttpServletRequest.class);
+    expect(result.getMethod()).andReturn("GET").anyTimes();
+    expect(result.getCharacterEncoding()).andReturn("UTF-8").anyTimes();
+    expect(result.getParameter(RpcServlet.GET_REQUEST_REQ_PARAM))
+        .andReturn(reqParamValue).anyTimes();
+    expect(result.getParameter(RpcServlet.GET_REQUEST_CALLBACK_PARAM))
+        .andReturn(callbackParamValue).anyTimes();
+    replay(result);
+    return result;
+  }
+
+  private HttpServletResponse createHttpResponse(String response, int httpStatusCode)
+    throws IOException {
+    return createHttpResponse(null, null, null, response, httpStatusCode);
+  }
+
+  private HttpServletResponse createHttpResponse(String header1, String header2,
+      String contentType, String response, int httpStatusCode) throws IOException {
+    HttpServletResponse result = createMock(HttpServletResponse.class);
+    PrintWriter writer = createMock(PrintWriter.class);
+    if (response != null) {
+      expect(result.getWriter()).andReturn(writer);
+      writer.write(response);
+    }
+    if (header1 != null && header2 != null) {
+      result.setHeader(header1, header2);
+    }
+    if (contentType != null) {
+      result.setContentType(contentType);
+    }
+    result.setStatus(httpStatusCode);
+    replay(result, writer);
+    return result;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ServletTestFixture.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ServletTestFixture.java
new file mode 100644
index 0000000..ba3b329
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ServletTestFixture.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.servlet.HttpServletResponseRecorder;
+import org.apache.shindig.gadgets.FeedProcessor;
+import org.apache.shindig.gadgets.FeedProcessorImpl;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+import org.apache.shindig.gadgets.process.Processor;
+import org.apache.shindig.gadgets.rewrite.CaptureRewriter;
+import org.apache.shindig.gadgets.rewrite.DefaultResponseRewriterRegistry;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriter;
+import org.apache.shindig.gadgets.rewrite.ResponseRewriterRegistry;
+
+import com.google.inject.Provider;
+
+/**
+ * Contains everything needed for making servlet requests, plus a bunch of stuff that shouldn't be
+ * here.
+ *
+ * TODO: Get rid of 'stuff that shouldn't be here'.
+ */
+public abstract class ServletTestFixture extends EasyMockTestCase {
+  public final RequestPipeline pipeline = mock(RequestPipeline.class);
+  public CaptureRewriter rewriter = new CaptureRewriter();
+  public ResponseRewriterRegistry rewriterRegistry
+      = new DefaultResponseRewriterRegistry(Arrays.<ResponseRewriter>asList(rewriter), null);
+  public final HttpServletRequest request = mock(HttpServletRequest.class);
+  public final HttpServletResponse response = mock(HttpServletResponse.class);
+  public final HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(response);
+  public final LockedDomainService lockedDomainService = mock(LockedDomainService.class);
+  public final Processor processor = mock(Processor.class);
+  public final Provider<FeedProcessor> feedProcessorProvider = new Provider<FeedProcessor>() {
+    public FeedProcessor get() {
+      return new FeedProcessorImpl();
+    }
+  };
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ServletUtilTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ServletUtilTest.java
new file mode 100644
index 0000000..6dcd469
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ServletUtilTest.java
@@ -0,0 +1,308 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.shindig.common.servlet.HttpServletResponseRecorder;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.DateUtil;
+import org.apache.shindig.common.util.FakeTimeSource;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.easymock.EasyMock;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.Vector;
+
+import static junitx.framework.ComparableAssert.assertGreater;
+import static junitx.framework.ComparableAssert.assertLesser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class ServletUtilTest {
+  @Test
+  public void testValidateUrlNoPath() throws Exception {
+    Uri url = ServletUtil.validateUrl(Uri.parse("http://www.example.com"));
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getAuthority());
+    assertEquals("/", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void testValidateUrlHttps() throws Exception {
+    Uri url = ServletUtil.validateUrl(Uri.parse("https://www.example.com"));
+    assertEquals("https", url.getScheme());
+    assertEquals("www.example.com", url.getAuthority());
+    assertEquals("/", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void testValidateUrlWithPath() throws Exception {
+    Uri url = ServletUtil.validateUrl(Uri.parse("http://www.example.com/foo"));
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getAuthority());
+    assertEquals("/foo", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void testValidateUrlWithPort() throws Exception {
+    Uri url = ServletUtil.validateUrl(Uri.parse("http://www.example.com:8080/foo"));
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com:8080", url.getAuthority());
+    assertEquals("/foo", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void testValidateUrlWithEncodedPath() throws Exception {
+    Uri url = ServletUtil.validateUrl(Uri.parse("http://www.example.com/foo%20bar"));
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getAuthority());
+    assertEquals("/foo%20bar", url.getPath());
+    assertNull(url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void testValidateUrlWithEncodedQuery() throws Exception {
+    Uri url = ServletUtil.validateUrl(Uri.parse("http://www.example.com/foo?q=with%20space"));
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getAuthority());
+    assertEquals("/foo", url.getPath());
+    assertEquals("q=with%20space", url.getQuery());
+    assertEquals("with space", url.getQueryParameter("q"));
+    assertNull(url.getFragment());
+  }
+
+  @Test
+  public void testValidateUrlWithNoPathAndEncodedQuery() throws Exception {
+    Uri url = ServletUtil.validateUrl(Uri.parse("http://www.example.com?q=with%20space"));
+    assertEquals("http", url.getScheme());
+    assertEquals("www.example.com", url.getAuthority());
+    assertEquals("/", url.getPath());
+    assertEquals("q=with%20space", url.getQuery());
+    assertNull(url.getFragment());
+  }
+
+  @Test(expected = GadgetException.class)
+  public void testValidateUrlNullInput() throws Exception {
+    ServletUtil.validateUrl(null);
+  }
+
+  @Test
+  public void testOutputDataUri() throws Exception {
+    checkOutputDataUri("text/foo", "text/foo", "UTF-8");
+  }
+
+  @Test
+  public void testOutputDataUriWithCharset() throws Exception {
+    checkOutputDataUri("text/bar; charset=ISO-8859-1", "text/bar", "ISO-8859-1");
+  }
+
+  @Test
+  public void testOutputDataUriWithEmptyCharset() throws Exception {
+    checkOutputDataUri("text/bar; charset=", "text/bar", "UTF-8");
+  }
+
+  private void checkOutputDataUri(String contentType, String expectedType,
+      String expectedEncoding) throws Exception {
+    String theData = "this is the data";
+    String mk1 = "meta1", mv1 = "val1";
+    String mk2 = "'\"}key", mv2 = "val{\"'";
+    HttpResponse response = new HttpResponseBuilder()
+      .setResponseString(theData)
+      .addHeader("Content-Type", contentType)
+      .setMetadata(mk1, mv1)
+      .setMetadata(mk2, mv2)
+      .setMetadata(ServletUtil.DATA_URI_KEY, "foo")  // Should be ignored.
+      .create();
+
+    HttpResponse jsonified = ServletUtil.convertToJsonResponse(response);
+
+    assertEquals("application/json; charset=UTF-8", jsonified.getHeader("Content-Type"));
+
+    String emitted = jsonified.getResponseAsString();
+    JSONObject js = new JSONObject(emitted);
+    assertEquals(mv1, js.getString(mk1));
+    assertEquals(mv2, js.getString(mk2));
+    String content64 = getBase64(theData);
+    assertEquals("data:" + expectedType + ";base64;charset=" + expectedEncoding + "," + content64,
+        js.getString(ServletUtil.DATA_URI_KEY));
+  }
+
+  private String getBase64(String input) throws Exception {
+    return new String(Base64.encodeBase64(input.getBytes("UTF8")), "UTF8");
+  }
+
+  @Test
+  public void testFromHttpServletRequest() throws Exception {
+    HttpServletRequest original = EasyMock.createMock(HttpServletRequest.class);
+    EasyMock.expect(original.getScheme()).andReturn("https");
+    EasyMock.expect(original.getServerName()).andReturn("www.example.org");
+    EasyMock.expect(original.getServerPort()).andReturn(444);
+    EasyMock.expect(original.getRequestURI()).andReturn("/path/foo");
+    EasyMock.expect(original.getQueryString()).andReturn("one=two&three=four");
+    Vector<String> headerNames = new Vector<String>();
+    headerNames.add("Header1");
+    headerNames.add("Header2");
+    EasyMock.expect(original.getHeaderNames()).andReturn(headerNames.elements());
+    EasyMock.expect(original.getHeaders("Header1"))
+        .andReturn(makeEnumeration("HVal1", "HVal3"));
+    EasyMock.expect(original.getHeaders("Header2"))
+        .andReturn(makeEnumeration("HVal2", "HVal4"));
+    EasyMock.expect(original.getMethod()).andReturn("post");
+    final ByteArrayInputStream bais = new ByteArrayInputStream("post body".getBytes());
+    ServletInputStream sis = new ServletInputStream() {
+      @Override
+      public int read() throws IOException {
+        return bais.read();
+      }
+    };
+    EasyMock.expect(original.getInputStream()).andReturn(sis);
+    EasyMock.expect(original.getRemoteAddr()).andReturn("1.2.3.4");
+
+    EasyMock.replay(original);
+    HttpRequest request = ServletUtil.fromHttpServletRequest(original);
+    EasyMock.verify(original);
+
+    assertEquals(Uri.parse("https://www.example.org:444/path/foo?one=two&three=four"),
+        request.getUri());
+    assertEquals(3, request.getHeaders().size());
+    assertEquals("HVal1", request.getHeaders("Header1").get(0));
+    assertEquals("HVal3", request.getHeaders("Header1").get(1));
+    assertEquals("HVal2", request.getHeaders("Header2").get(0));
+    assertEquals("HVal4", request.getHeaders("Header2").get(1));
+    assertEquals("post", request.getMethod());
+    assertEquals("post body", request.getPostBodyAsString());
+    assertEquals("1.2.3.4", request.getParam(ServletUtil.REMOTE_ADDR_KEY));
+  }
+
+  @Test
+  public void testCopyToServletResponseAndOverrideCacheHeadersForPublic() throws Exception {
+    FakeTimeSource fakeTime = new FakeTimeSource();
+    HttpUtil.setTimeSource(fakeTime);
+
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("response string").setHttpStatusCode(200).addHeader("h1", "v1")
+        .addHeader("h2", "v2").setCacheTtl(1000).create();
+
+    HttpServletResponse servletResponse = EasyMock.createMock(HttpServletResponse.class);
+    HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(servletResponse);
+
+    ServletUtil.copyToServletResponseAndOverrideCacheHeaders(response, recorder);
+
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertEquals("response string", recorder.getResponseAsString());
+    assertEquals("v1", recorder.getHeader("h1"));
+    assertEquals("v2", recorder.getHeader("h2"));
+    assertEquals("public,max-age=1000", recorder.getHeader("Cache-Control"));
+  }
+
+  @Test
+  public void testCopyToServletResponse() throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("response string").setHttpStatusCode(200).addHeader("h1", "v1")
+        .addHeader("h2", "v2").addHeader("Cache-Control", "private,no-store,max-age=10")
+        .addHeader("Expires", "123").create();
+
+    HttpServletResponse servletResponse = EasyMock.createMock(HttpServletResponse.class);
+    HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(servletResponse);
+
+    ServletUtil.copyToServletResponse(response, recorder);
+
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertEquals("response string", recorder.getResponseAsString());
+    assertEquals("v1", recorder.getHeader("h1"));
+    assertEquals("v2", recorder.getHeader("h2"));
+    assertEquals("private,no-store,max-age=10", recorder.getHeader("Cache-Control"));
+    assertEquals("123", recorder.getHeader("Expires"));
+  }
+
+  @Test
+  public void testCopyToServletResponseAndOverrideCacheHeadersForPrivate()
+      throws Exception {
+    FakeTimeSource fakeTime = new FakeTimeSource();
+    HttpUtil.setTimeSource(fakeTime);
+
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("response string").setHttpStatusCode(200).addHeader("h1", "v1")
+        .addHeader("h2", "v2").addHeader("Cache-Control", "private,no-store,max-age=10")
+        .addHeader("Expires", "123").create();
+
+    HttpServletResponse servletResponse = EasyMock.createMock(HttpServletResponse.class);
+    HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(servletResponse);
+    long testStartTime = fakeTime.currentTimeMillis();
+    ServletUtil.copyToServletResponseAndOverrideCacheHeaders(response, recorder);
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertEquals("response string", recorder.getResponseAsString());
+    assertEquals("v1", recorder.getHeader("h1"));
+    assertEquals("v2", recorder.getHeader("h2"));
+    assertEquals("no-cache", recorder.getHeader("Cache-Control"));
+    long expires = DateUtil.parseRfc1123Date(recorder.getHeader("Expires")).getTime();
+    assertGreater(testStartTime - 2000L, expires);
+    assertLesser(testStartTime + 2000L, expires);
+  }
+
+  @Test
+  public void testCopyToServletResponseAndOverrideCacheHeadersForStrictNoCache()
+      throws Exception {
+    HttpResponse response = new HttpResponseBuilder()
+        .setResponseString("response string").setHttpStatusCode(200).addHeader("h1", "v1")
+        .addHeader("h2", "v2").setStrictNoCache().create();
+
+    HttpServletResponse servletResponse = EasyMock.createMock(HttpServletResponse.class);
+    HttpServletResponseRecorder recorder = new HttpServletResponseRecorder(servletResponse);
+
+    FakeTimeSource fakeTime = new FakeTimeSource();
+    HttpUtil.setTimeSource(fakeTime);
+    ServletUtil.copyToServletResponseAndOverrideCacheHeaders(response, recorder);
+
+    assertEquals(200, recorder.getHttpStatusCode());
+    assertEquals("response string", recorder.getResponseAsString());
+    assertEquals("v1", recorder.getHeader("h1"));
+    assertEquals("v2", recorder.getHeader("h2"));
+    assertEquals("no-cache", recorder.getHeader("Pragma"));
+    assertEquals("no-cache", recorder.getHeader("Cache-Control"));
+  }
+
+  Enumeration<String> makeEnumeration(String... args) {
+    Vector<String> vector = new Vector<String>();
+    vector.addAll(Arrays.asList(args));
+    return vector.elements();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java
new file mode 100644
index 0000000..7f9ba9d
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ApplicationManifestTest.java
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+
+import org.junit.Test;
+
+public class ApplicationManifestTest {
+  private static final Uri BASE_URI = Uri.parse("http://example.org/manifest.xml");
+
+  @Test
+  public void resolveRelativeUri() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec>app.xml</spec>" +
+        "</gadget></app>";
+
+    ApplicationManifest manifest = new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+
+    assertEquals(BASE_URI.resolve(Uri.parse("app.xml")), manifest.getGadget("1.0"));
+    assertEquals(BASE_URI, manifest.getUri());
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void malformedUriThrows() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec><![CDATA[$%&$%*$%&$%(]]></spec>" +
+        "</gadget></app>";
+
+    new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+  }
+
+  @Test
+  public void getVersion() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>development</label>" +
+        "  <version>2.0</version>" +
+        "  <spec>whatever</spec>" +
+        "</gadget>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec>whatever</spec>" +
+        "</gadget></app>";
+
+    ApplicationManifest manifest = new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+
+    assertEquals("1.0", manifest.getVersion("production"));
+    assertEquals("2.0", manifest.getVersion("development"));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void missingVersion() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <spec>whatever</spec>" +
+        "</gadget></app>";
+
+    new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void tooManyVersions() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <version>2.0</version>" +
+        "  <spec>whatever</spec>" +
+        "</gadget></app>";
+
+    new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+  }
+
+  @Test
+  public void getGadget() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>development</label>" +
+        "  <version>2.0</version>" +
+        "  <spec>app2.xml</spec>" +
+        "</gadget>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec>app.xml</spec>" +
+        "</gadget></app>";
+
+    ApplicationManifest manifest = new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+
+    assertEquals(BASE_URI.resolve(Uri.parse("app.xml")), manifest.getGadget("1.0"));
+    assertEquals(BASE_URI.resolve(Uri.parse("app2.xml")), manifest.getGadget("2.0"));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void missingSpec() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "</gadget></app>";
+
+    new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void tooManySpecs() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec>whoever</spec>" +
+        "  <spec>whatever</spec>" +
+        "</gadget></app>";
+
+    new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void selfReferencingManifest() throws Exception {
+    String xml =
+        "<app xmlns='" + ApplicationManifest.NAMESPACE + "'>" +
+        "<gadget>" +
+        "  <label>production</label>" +
+        "  <version>1.0</version>" +
+        "  <spec>whoever</spec>" +
+        "  <spec>" + BASE_URI + "</spec>" +
+        "</gadget></app>";
+
+    new ApplicationManifest(BASE_URI, XmlUtil.parse(xml));
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/FeatureTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/FeatureTest.java
new file mode 100644
index 0000000..ec6aceb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/FeatureTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import java.util.Set;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+
+public class FeatureTest extends Assert {
+  @Test
+  public void testRequire() throws Exception {
+    String xml = "<Require feature=\"foo\"/>";
+    Feature feature = new Feature(XmlUtil.parse(xml));
+    assertEquals("foo", feature.getName());
+    assertTrue(feature.getRequired());
+  }
+
+  @Test
+  public void testOptional() throws Exception {
+    String xml = "<Optional feature=\"foo\"/>";
+    Feature feature = new Feature(XmlUtil.parse(xml));
+    assertEquals("foo", feature.getName());
+    assertFalse(feature.getRequired());
+  }
+
+  @Test
+  public void testParams() throws Exception {
+    String key = "bar";
+    String value = "Hello, World!";
+    String xml = "<Require feature=\"foo\">" +
+                 "  <Param name=\"" + key + "\">" + value + "</Param>" +
+                 "</Require>";
+    Feature feature = new Feature(XmlUtil.parse(xml));
+    Multimap<String, String> params = feature.getParams();
+    assertEquals(1, params.size());
+    assertEquals(ImmutableList.of(value), params.get(key));
+  }
+
+  @Test
+  public void testMultiParams() throws Exception {
+    String key = "bar";
+    String key2 = "bar2";
+    String value = "Hello, World!";
+    String value2 = "Goodbye, World!";
+    // Verify that multiple parameters are supported, and are returned in-order
+    String xml = "<Require feature=\"foo\">" +
+                 "  <Param name=\"" + key + "\">" + value + "</Param>" +
+                 "  <Param name=\"" + key + "\">" + value2 + "</Param>" +
+                 "  <Param name=\"" + key2 + "\">" + value2 + "</Param>" +
+                 "  <Param name=\"" + key2 + "\">" + value + "</Param>" +
+                 "</Require>";
+    Feature feature = new Feature(XmlUtil.parse(xml));
+    Multimap<String, String> params = feature.getParams();
+    assertEquals(2, params.keySet().size());
+    assertEquals(ImmutableList.of(value, value2), params.get(key));
+    assertEquals(value, feature.getParam(key));
+    assertEquals(ImmutableList.of(value2, value), params.get(key2));
+    assertEquals(value2, feature.getParam(key2));
+    assertTrue(params.get("foobar").isEmpty());
+    assertNull(feature.getParam("foobar"));
+  }
+
+
+  @Test
+  public void testViews() throws Exception {
+    String xml = "<Require feature=\"foo\" views=\"view1\">" +
+                 "</Require>";
+    Feature feature = new Feature(XmlUtil.parse(xml));
+    Set<String> views = feature.getViews();
+    assertTrue(views.size() == 1);
+    assertTrue(views.contains("view1"));
+
+    xml = "<Require feature=\"foo\" views=\"view1, view2\">" +
+    "</Require>";
+		feature = new Feature(XmlUtil.parse(xml));
+		views = feature.getViews();
+		assertTrue(views.size() == 2);
+		assertTrue(views.contains("view1"));
+		assertTrue(views.contains("view2"));
+
+    xml = "<Require feature=\"foo\">" +
+    "</Require>";
+		feature = new Feature(XmlUtil.parse(xml));
+		views = feature.getViews();
+		assertTrue(views != null);
+		assertTrue(views.size() == 0);
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testDoesNotLikeUnnamedFeatures() throws Exception {
+    String xml = "<Require/>";
+    new Feature(XmlUtil.parse(xml));
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testEnforceParamNames() throws Exception {
+    String xml = "<Require feature=\"foo\"><Param>Test</Param></Require>";
+    new Feature(XmlUtil.parse(xml));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java
new file mode 100644
index 0000000..4af022e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/GadgetSpecTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+import org.apache.shindig.gadgets.variables.Substitutions.Type;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+public class GadgetSpecTest extends Assert {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+
+  @Test
+  public void testBasic() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"title\"/>" +
+                 "<UserPref name=\"foo\" datatype=\"string\"/>" +
+                 "<Content type=\"html\">Hello!</Content>" +
+                 "</Module>";
+    GadgetSpec spec = new GadgetSpec(SPEC_URL, xml);
+    assertEquals("title", spec.getModulePrefs().getTitle());
+    assertEquals(UserPref.DataType.STRING,
+        spec.getUserPrefs().get("foo").getDataType());
+    assertEquals("Hello!", spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test
+  public void testUserPrefsOrder() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"title\"/>" +
+                 "<UserPref name=\"a\" datatype=\"string\"/>" +
+                 "<UserPref name=\"z\" datatype=\"string\"/>" +
+                 "<UserPref name=\"b\" datatype=\"string\"/>" +
+                 "<UserPref name=\"y\" datatype=\"string\"/>" +
+                 "<Content type=\"html\">Hello!</Content>" +
+                 "</Module>";
+    GadgetSpec spec = new GadgetSpec(SPEC_URL, xml);
+    assertEquals("title", spec.getModulePrefs().getTitle());
+    Collection<UserPref> prefs = spec.getUserPrefs().values();
+    Iterator<UserPref> iter = prefs.iterator();
+    assertEquals("a", iter.next().getName());
+    assertEquals("z", iter.next().getName());
+    assertEquals("b", iter.next().getName());
+    assertEquals("y", iter.next().getName());
+    assertEquals("Hello!", spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+  }
+
+  @Test
+  public void testAlternativeConstructor() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"title\"/>" +
+                 "<UserPref name=\"foo\" datatype=\"string\"/>" +
+                 "<Content type=\"html\">Hello!</Content>" +
+                 "</Module>";
+    GadgetSpec spec = new GadgetSpec(SPEC_URL, XmlUtil.parse(xml), xml);
+    assertEquals("title", spec.getModulePrefs().getTitle());
+    assertEquals(UserPref.DataType.STRING,
+        spec.getUserPrefs().get("foo").getDataType());
+    assertEquals("Hello!", spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+
+    assertEquals(HashUtil.checksum(xml.getBytes()), spec.getChecksum());
+  }
+
+  @Test
+  public void testMultipleContentSections() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"title\"/>" +
+                 "<Content type=\"html\" view=\"hello\">hello </Content>" +
+                 "<Content type=\"html\" view=\"world\">world</Content>" +
+                 "<Content type=\"html\" view=\"hello, test\">test</Content>" +
+                 "</Module>";
+    GadgetSpec spec = new GadgetSpec(SPEC_URL, xml);
+    assertEquals("hello test", spec.getView("hello").getContent());
+    assertEquals("world", spec.getView("world").getContent());
+    assertEquals("test", spec.getView("test").getContent());
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testMissingModulePrefs() throws Exception {
+    String xml = "<Module>" +
+                 "<Content type=\"html\"/>" +
+                 "</Module>";
+    new GadgetSpec(SPEC_URL, xml);
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testEnforceOneModulePrefs() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"hello\"/>" +
+                 "<ModulePrefs title=\"world\"/>" +
+                 "<Content type=\"html\"/>" +
+                 "</Module>";
+    new GadgetSpec(SPEC_URL, xml);
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testEnforceUserPrefNoDuplicate() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"hello\"/>" +
+                 "<UserPref name=\"foo\" datatype=\"string\"/>" +
+                 "<UserPref name=\"foo\" datatype=\"int\"/>" +
+                 "<Content type=\"html\"/>" +
+                 "</Module>";
+    new GadgetSpec(SPEC_URL, xml);
+  }
+
+  @Test
+  public void testSubstitutions() throws Exception {
+    Substitutions substituter = new Substitutions();
+    String title = "Hello, World!";
+    String content = "Goodbye, world :(";
+    String xml = "<Module>" +
+                 "<ModulePrefs title=\"__UP_title__\"/>" +
+                 "<Content type=\"html\">__MSG_content__</Content>" +
+                 "</Module>";
+    substituter.addSubstitution(Type.USER_PREF, "title", title);
+    substituter.addSubstitution(Type.MESSAGE, "content", content);
+
+    GadgetSpec baseSpec = new GadgetSpec(SPEC_URL, xml);
+    baseSpec.setAttribute("foo", 100);
+    baseSpec.setAttribute("bar", "baz");
+
+    GadgetSpec spec = baseSpec.substitute(substituter);
+    assertEquals(title, spec.getModulePrefs().getTitle());
+    assertEquals(content, spec.getView(GadgetSpec.DEFAULT_VIEW).getContent());
+    assertEquals(100, spec.getAttribute("foo"));
+    assertEquals("baz", spec.getAttribute("bar"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/IconTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/IconTest.java
new file mode 100644
index 0000000..bac185e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/IconTest.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class IconTest extends Assert {
+  @Test
+  public void testBasicIcon() throws Exception {
+    String xml = "<Icon type=\"foo\" mode=\"base64\">helloWorld</Icon>";
+    Icon icon = new Icon(XmlUtil.parse(xml));
+    assertEquals("foo", icon.getType());
+    assertEquals("base64", icon.getMode());
+    assertEquals("helloWorld", icon.getContent());
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testInvalidMode() throws Exception {
+    String xml = "<Icon type=\"foo\" mode=\"broken\"/>";
+    new Icon(XmlUtil.parse(xml));
+  }
+
+  @Test
+  public void testSubstitutions() throws Exception {
+    String xml = "<Icon>http://__MSG_domain__/icon.png</Icon>";
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Substitutions.Type.MESSAGE, "domain",
+        "example.org");
+    Icon icon = new Icon(XmlUtil.parse(xml)).substitute(substituter);
+    assertEquals("http://example.org/icon.png", icon.getContent());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/LinkSpecTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/LinkSpecTest.java
new file mode 100644
index 0000000..481b279
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/LinkSpecTest.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import org.junit.Test;
+
+/**
+ * Tests for Link.
+ */
+public class LinkSpecTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+  private static final String REL_VALUE = "foo";
+  private static final Uri HREF_VALUE = Uri.parse("http://example.org/foo");
+  private static final String DEFAULT_METHOD_VALUE = "GET";
+  private static final String METHOD_VALUE = "POST";
+
+  @Test
+  public void parseBasicLink() throws Exception {
+    String xml = "<Link rel='" + REL_VALUE + "' href='" + HREF_VALUE + "'/>";
+
+    LinkSpec link = new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+
+    assertEquals(REL_VALUE, link.getRel());
+    assertEquals(HREF_VALUE, link.getHref());
+  }
+
+  @Test
+  public void parseRelativeLink() throws Exception {
+    String xml = "<Link rel='" + REL_VALUE + "' href='/foo'/>";
+
+    LinkSpec link = new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+
+    link = link.substitute(new Substitutions());
+
+    assertEquals(REL_VALUE, link.getRel());
+    assertEquals(HREF_VALUE.resolve(Uri.parse("/foo")), link.getHref());
+  }
+
+  @Test
+  public void parseMethodAttribute() throws Exception {
+    String xml = "<Link rel='" + REL_VALUE + "' href='" + HREF_VALUE + "'/>";
+    LinkSpec link = new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(DEFAULT_METHOD_VALUE, link.getMethod());
+  }
+
+  @Test
+  public void parseAltMethodAttribute() throws Exception {
+    String xml = "<Link rel='" + REL_VALUE + "' href='" + HREF_VALUE + "' method='POST'/>";
+    LinkSpec link = new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(METHOD_VALUE, link.getMethod());
+  }
+
+  @Test
+  public void substitutionsPerformed() throws Exception {
+    String rel = "foo.bar";
+    String href = "jp-DE.xml";
+    Uri expectedHref = Uri.parse("http://example.org/jp-DE.xml");
+    String xml = "<Link rel='__MSG_rel__' href='http://example.org/__MSG_href__'/>";
+
+    LinkSpec link = new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+    Substitutions substitutions = new Substitutions();
+    substitutions.addSubstitution(Substitutions.Type.MESSAGE, "rel", rel);
+    substitutions.addSubstitution(Substitutions.Type.MESSAGE, "href", href);
+    LinkSpec substituted = link.substitute(substitutions);
+
+    assertEquals(rel, substituted.getRel());
+    assertEquals(expectedHref, substituted.getHref());
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void parseNoRel() throws Exception {
+    String xml = "<Link href='foo'/>";
+    new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void parseNoHref() throws Exception {
+    String xml = "<Link rel='bar'/>";
+    new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void parseBogusHref() throws Exception {
+    String xml = "<Link rel='foo' href='$%^$#%$#$%'/>";
+    new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test
+  public void toStringIsSane() throws Exception {
+    String xml = "<Link rel='" + REL_VALUE + "' href='" + HREF_VALUE + "'/>";
+
+    LinkSpec link = new LinkSpec(XmlUtil.parse(xml), SPEC_URL);
+    LinkSpec link2 = new LinkSpec(XmlUtil.parse(link.toString()), SPEC_URL);
+
+    assertEquals(link.getRel(), link2.getRel());
+    assertEquals(link.getHref(), link2.getHref());
+    assertEquals(REL_VALUE, link2.getRel());
+    assertEquals(HREF_VALUE, link2.getHref());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/LocaleSpecTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/LocaleSpecTest.java
new file mode 100644
index 0000000..2b72ab2
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/LocaleSpecTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.junit.Test;
+
+public class LocaleSpecTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/foo.xml");
+
+  @Test
+  public void normalLocale() throws Exception {
+    String xml = "<Locale" +
+                 " lang=\"en\"" +
+                 " country=\"US\"" +
+                 " language_direction=\"rtl\"" +
+                 " messages=\"http://example.org/msgs.xml\"/>";
+
+    LocaleSpec locale = new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("en", locale.getLanguage());
+    assertEquals("US", locale.getCountry());
+    assertEquals("rtl", locale.getLanguageDirection());
+    assertEquals("http://example.org/msgs.xml", locale.getMessages().toString());
+    assertEquals(0, locale.getViews().size());
+  }
+
+  @Test
+  public void viewLocale() throws Exception {
+    String xml = "<Locale" +
+                 " lang=\"en\"" +
+                 " country=\"US\"" +
+                 " language_direction=\"rtl\"" +
+                 " messages=\"http://example.org/msgs.xml\"" +
+                 " views=\"view1\"/>";
+
+    LocaleSpec locale = new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("en", locale.getLanguage());
+    assertEquals("US", locale.getCountry());
+    assertEquals("rtl", locale.getLanguageDirection());
+    assertEquals("http://example.org/msgs.xml", locale.getMessages().toString());
+    assertEquals(1, locale.getViews().size());
+    Object[] views = locale.getViews().toArray();
+    assertEquals("view1",views[0].toString());
+  }
+
+  @Test
+  public void relativeLocale() throws Exception {
+    String xml = "<Locale messages=\"/test/msgs.xml\"/>";
+    LocaleSpec locale = new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("http://example.org/test/msgs.xml", locale.getMessages().toString());
+  }
+
+  @Test
+  public void defaultLanguageAndCountry() throws Exception {
+    String xml = "<Locale/>";
+    LocaleSpec locale = new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("all", locale.getLanguage());
+    assertEquals("ALL", locale.getCountry());
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void invalidLanguageDirection() throws Exception {
+    String xml = "<Locale language_direction=\"invalid\"/>";
+    new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void invalidMessagesUrl() throws Exception {
+    String xml = "<Locale messages=\"fobad@$%!fdf\"/>";
+    new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test
+  public void nestedMessages() throws Exception {
+    String msgName = "message name";
+    String msgValue = "message value";
+    String xml = "<Locale>" +
+                 "<msg name=\"" + msgName + "\">" + msgValue + "</msg>" +
+                 "</Locale>";
+    LocaleSpec locale = new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(msgValue, locale.getMessageBundle().getMessages().get(msgName));
+  }
+
+  @Test
+  public void toStringIsSane() throws Exception {
+    String xml = "<Locale lang='en' country='US' language_direction='rtl'" +
+                 " messages='foo' views='view1, view2'>" +
+                 "  <msg name='hello'>World</msg>" +
+                 "  <msg name='foo'>Bar</msg>" +
+                 "</Locale>";
+    LocaleSpec loc = new LocaleSpec(XmlUtil.parse(xml), SPEC_URL);
+    LocaleSpec loc2 = new LocaleSpec(XmlUtil.parse(loc.toString()), SPEC_URL);
+    assertEquals(loc.getLanguage(), loc2.getLanguage());
+    assertEquals(loc.getCountry(), loc2.getCountry());
+    assertEquals(loc.getLanguageDirection(), loc2.getLanguageDirection());
+    assertEquals(loc.getMessages(), loc2.getMessages());
+    assertEquals(loc.getMessageBundle().getMessages(),
+                 loc2.getMessageBundle().getMessages());
+    assertEquals(loc.getViews().toString(),loc2.getViews().toString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/MessageBundleTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/MessageBundleTest.java
new file mode 100644
index 0000000..5041ef3
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/MessageBundleTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+
+import java.util.Map;
+
+public class MessageBundleTest {
+  private static final Uri BUNDLE_URL = Uri.parse("http://example.org/m.xml");
+  private static final String LOCALE
+      = "<Locale lang='en' country='US' messages='" + BUNDLE_URL + "'/>";
+  private static final String PARENT_LOCALE
+      = "<Locale lang='en' country='ALL' language_direction='rtl'>" +
+        " <msg name='one'>VALUE</msg>" +
+        " <msg name='foo'>adfdfdf</msg>" +
+        "</Locale>";
+  private static final Map<String, String> MESSAGES = Maps.newHashMap();
+  private static final String XML;
+  static {
+    MESSAGES.put("hello", "world");
+    MESSAGES.put("foo", "bar");
+    StringBuilder buf = new StringBuilder();
+    buf.append("<messagebundle>");
+    for (Map.Entry<String, String> entry : MESSAGES.entrySet()) {
+      buf.append("<msg name='").append(entry.getKey()).append("'>")
+         .append(entry.getValue())
+         .append("</msg>");
+    }
+    buf.append("</messagebundle>");
+    XML = buf.toString();
+  }
+
+  private LocaleSpec locale;
+
+  @Before
+  public void setUp() throws Exception {
+    locale = new LocaleSpec(XmlUtil.parse(LOCALE), Uri.parse("http://example.org/gadget"));
+  }
+
+  @Test
+  public void normalMessageBundleParsesOk() throws Exception {
+    MessageBundle bundle = new MessageBundle(locale, XML);
+    assertEquals(MESSAGES, bundle.getMessages());
+  }
+
+  @Test
+  public void duplicateKeyIgnored() throws Exception {
+    String duplicateKeyXml =
+      "<messagebundle>" +
+      "  <msg name='key'>value</msg>" +
+      "  <msg name='key'>value</msg>" +
+      "</messagebundle>";
+    MessageBundle bundle = new MessageBundle(locale, duplicateKeyXml);
+    assertEquals(ImmutableMap.of("key", "value"), bundle.getMessages());
+  }
+
+  @Test
+  public void containsCdataSection() throws Exception {
+    String cdataXml =
+       "<messagebundle>" +
+       "  <msg name='key'><![CDATA[<span id='foo'>data</span>]]></msg>" +
+       "</messagebundle>";
+    MessageBundle bundle = new MessageBundle(locale, cdataXml);
+    assertEquals(ImmutableMap.of("key", "<span id='foo'>data</span>"), bundle.getMessages());
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void missingNameThrows() throws SpecParserException {
+    String xml = "<messagebundle><msg>foo</msg></messagebundle>";
+    new MessageBundle(locale, xml);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void malformedXmlThrows() throws SpecParserException {
+    String xml = "</messagebundle>";
+    new MessageBundle(locale, xml);
+  }
+
+  @Test
+  public void extractFromElement() throws Exception {
+    Element element = XmlUtil.parse(XML);
+    MessageBundle bundle = new MessageBundle(element);
+    assertEquals(MESSAGES, bundle.getMessages());
+  }
+
+  @Test
+  public void extractFromElementWithLanguageDir() throws Exception {
+    Element element = XmlUtil.parse(PARENT_LOCALE);
+    MessageBundle bundle = new MessageBundle(element);
+    assertEquals("rtl", bundle.getLanguageDirection());
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void extractFromElementsWithNoName() throws Exception {
+    String xml = "<messagebundle><msg>foo</msg></messagebundle>";
+    Element element = XmlUtil.parse(xml);
+    new MessageBundle(element);
+  }
+
+  @Test
+  public void extractNestedTagsVerbatim() throws Exception {
+    String xml = "<messagebundle><msg name='key'>This is <x>nested</x> content</msg>" +
+                 "</messagebundle>";
+    Element element = XmlUtil.parse(xml);
+    MessageBundle bundle = new MessageBundle(element);
+    assertEquals("This is <x>nested</x> content", bundle.getMessages().get("key"));
+  }
+
+  @Test
+  public void merge() throws Exception {
+    MessageBundle parent = new MessageBundle(XmlUtil.parse(PARENT_LOCALE));
+    MessageBundle child = new MessageBundle(XmlUtil.parse(XML));
+    MessageBundle bundle = new MessageBundle(parent, child);
+    assertEquals("ltr", bundle.getLanguageDirection());
+    assertEquals("VALUE", bundle.getMessages().get("one"));
+    assertEquals("bar", bundle.getMessages().get("foo"));
+  }
+
+  @Test
+  public void toStringIsSane() throws Exception {
+    MessageBundle b0 = new MessageBundle(locale, XML);
+    MessageBundle b1 = new MessageBundle(locale, b0.toString());
+    assertEquals(b0.getMessages(), b1.getMessages());
+  }
+
+  private static void assertJsonEquals(JSONObject left, JSONObject right) throws JSONException {
+    assertEquals(left.length(), right.length());
+    for (String key : JSONObject.getNames(left)) {
+      assertEquals(left.get(key), right.get(key));
+    }
+  }
+
+  @Test
+  public void toJSONStringMatchesValues() throws Exception {
+    MessageBundle simple = new MessageBundle(XmlUtil.parse(PARENT_LOCALE));
+
+    JSONObject fromString = new JSONObject(simple.toJSONString());
+    JSONObject fromMap = new JSONObject(simple.getMessages());
+    assertJsonEquals(fromString, fromMap);
+  }
+
+  @Test
+  public void toJSONStringMatchesValuesLocaleCtor() throws Exception {
+    MessageBundle bundle = new MessageBundle(locale, XML);
+
+    JSONObject fromString = new JSONObject(bundle.toJSONString());
+    JSONObject fromMap = new JSONObject(bundle.getMessages());
+    assertJsonEquals(fromString, fromMap);
+  }
+
+  @Test
+  public void toJSONStringMatchesValuesWithChild() throws Exception {
+    MessageBundle parent = new MessageBundle(XmlUtil.parse(PARENT_LOCALE));
+    MessageBundle child = new MessageBundle(XmlUtil.parse(XML));
+    MessageBundle bundle = new MessageBundle(parent, child);
+
+    JSONObject fromString = new JSONObject(bundle.toJSONString());
+    JSONObject fromMap = new JSONObject(bundle.getMessages());
+    assertJsonEquals(fromString, fromMap);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ModulePrefsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ModulePrefsTest.java
new file mode 100644
index 0000000..cc810bf
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ModulePrefsTest.java
@@ -0,0 +1,396 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+import org.junit.Test;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.Multimap;
+
+public class ModulePrefsTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+  private static final String FULL_XML
+      = "<ModulePrefs" +
+        " title='title'" +
+        " title_url='title_url'" +
+        " description='description'" +
+        " author='author'" +
+        " author_email='author_email'" +
+        " screenshot='screenshot'" +
+        " thumbnail='thumbnail'" +
+        " directory_title='directory_title'" +
+        " width='1'" +
+        " height='2'" +
+        " scrolling='true'" +
+        " category='category'" +
+        " category2='category2'" +
+        " author_affiliation='author_affiliation'" +
+        " author_location='author_location'" +
+        " author_photo='author_photo'" +
+        " author_aboutme='author_aboutme'" +
+        " author_quote='author_quote'" +
+        " author_link='author_link'" +
+        " show_stats='true'" +
+        " show_in_directory='true'" +
+        " singleton='true'>" +
+        "  <Require feature='require'/>" +
+        "  <Optional feature='optional'/>" +
+        "  <Preload href='http://example.org' authz='signed'/>" +
+        "	 <Require feature='requiredview1' views='default, view1'/>" +
+        "	 <Require feature='requiredview2' views='view2'/>" +
+        "	 <Require feature='require' views='view2'>" +
+        "	 		<Param name='param_name'>param_value</Param>" +
+        "  </Require>" +
+        "  <Icon/>" +
+        "  <Locale/>" +
+        "  <Link rel='link' href='http://example.org/link'/>" +
+        "  <OAuth>" +
+        "    <Service name='serviceOne'>" +
+        "      <Request url='http://www.example.com/request'" +
+        "          method='GET' param_location='auth-header' />" +
+        "      <Authorization url='http://www.example.com/authorize'/>" +
+        "      <Access url='http://www.example.com/access' method='GET'" +
+        "          param_location='auth-header' />" +
+        "    </Service>" +
+        "  </OAuth>" +
+        "  <NavigationItem title=\"moo\"><AppParameter key=\"test\" value=\"1\"/></NavigationItem>" +
+        "</ModulePrefs>";
+
+  private void doAsserts(ModulePrefs prefs) {
+    assertEquals("title", prefs.getTitle());
+    assertEquals(SPEC_URL.resolve(Uri.parse("title_url")), prefs.getTitleUrl());
+    assertEquals("description", prefs.getDescription());
+    assertEquals("author", prefs.getAuthor());
+    assertEquals("author_email", prefs.getAuthorEmail());
+    assertEquals(SPEC_URL.resolve(Uri.parse("screenshot")), prefs.getScreenshot());
+    assertEquals(SPEC_URL.resolve(Uri.parse("thumbnail")), prefs.getThumbnail());
+    assertEquals("directory_title", prefs.getDirectoryTitle());
+    assertEquals(1, prefs.getWidth());
+    assertEquals(2, prefs.getHeight());
+    assertTrue(prefs.getScrolling());
+    assertFalse(prefs.getScaling());
+    assertEquals("category", prefs.getCategories().get(0));
+    assertEquals("category2", prefs.getCategories().get(1));
+    assertEquals("author_affiliation", prefs.getAuthorAffiliation());
+    assertEquals("author_location", prefs.getAuthorLocation());
+    assertEquals(SPEC_URL.resolve(Uri.parse("author_photo")), prefs.getAuthorPhoto());
+    assertEquals(SPEC_URL.resolve(Uri.parse("author_link")), prefs.getAuthorLink());
+    assertEquals("author_aboutme", prefs.getAuthorAboutme());
+    assertEquals("author_quote", prefs.getAuthorQuote());
+    assertTrue(prefs.getShowStats());
+    assertTrue(prefs.getShowInDirectory());
+    assertTrue(prefs.getSingleton());
+
+    assertTrue(prefs.getFeatures().get("require").getRequired());
+    assertFalse(prefs.getFeatures().get("optional").getRequired());
+
+    assertEquals("http://example.org",
+        prefs.getPreloads().get(0).getHref().toString());
+
+    assertEquals(1, prefs.getIcons().size());
+
+    assertEquals(1, prefs.getLocales().size());
+
+    assertEquals(Uri.parse("http://example.org/link"), prefs.getLinks().get("link").getHref());
+
+    OAuthService oauth = prefs.getOAuthSpec().getServices().get("serviceOne");
+    assertEquals(Uri.parse("http://www.example.com/request"), oauth.getRequestUrl().url);
+    assertEquals(OAuthService.Method.GET, oauth.getRequestUrl().method);
+    assertEquals(OAuthService.Method.GET, oauth.getAccessUrl().method);
+    assertEquals(OAuthService.Location.HEADER, oauth.getAccessUrl().location);
+    assertEquals(Uri.parse("http://www.example.com/authorize"), oauth.getAuthorizationUrl());
+
+    Multimap<String, Node> extra = prefs.getExtraElements();
+
+    assertTrue(extra.containsKey("NavigationItem"));
+    assertEquals(1, extra.get("NavigationItem").iterator().next().getChildNodes().getLength());
+  }
+
+  @Test
+  public void substitutionsCopyConstructor() throws Exception{
+    ModulePrefs basePrefs = new ModulePrefs(XmlUtil.parse(FULL_XML), SPEC_URL);
+    Substitutions substituter = new Substitutions();
+    String gadgetXml = "<Module>" +
+    FULL_XML +
+    "<Content type=\"html\"></Content>" +
+    "</Module>";
+    GadgetSpec baseSpec = new GadgetSpec(SPEC_URL, gadgetXml);
+    GadgetSpec spec = baseSpec.substitute(substituter);
+    ModulePrefs subsPrefs = spec.getModulePrefs();
+    assertEquals(basePrefs.toString(), subsPrefs.toString());
+    doAsserts(subsPrefs);
+  }
+
+  @Test
+  public void basicElementsParseOk() throws Exception {
+    doAsserts(new ModulePrefs(XmlUtil.parse(FULL_XML), SPEC_URL));
+  }
+
+  @Test
+  public void getAttribute() throws Exception {
+    String xml = "<ModulePrefs title='title' some_attribute='attribute' " +
+        "empty_attribute=''/>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("title", prefs.getAttribute("title"));
+    assertEquals("attribute", prefs.getAttribute("some_attribute"));
+    assertEquals("", prefs.getAttribute("empty_attribute"));
+    assertNull(prefs.getAttribute("gobbledygook"));
+  }
+
+  @Test
+  public void getGlobalLocale() throws Exception {
+    String xml = "<ModulePrefs title='locales'>" +
+                 "  <Locale lang='en' country='UK' messages='en.xml'/>" +
+                 "  <Locale lang='foo' language_direction='rtl'/>" +
+                 "</ModulePrefs>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    LocaleSpec spec = prefs.getGlobalLocale(new Locale("en", "UK"));
+    assertEquals("http://example.org/en.xml", spec.getMessages().toString());
+
+    spec = prefs.getGlobalLocale(new Locale("foo", "ALL"));
+    assertEquals("rtl", spec.getLanguageDirection());
+
+    spec = prefs.getGlobalLocale(new Locale("en", "notexist"));
+    assertNull(spec);
+  }
+
+  @Test
+  public void getViewLocale() throws Exception {
+    String xml = "<ModulePrefs title='locales'>" +
+                 "  <Locale lang='en' country='UK' messages='en.xml' views=\"view1\"/>" +
+                 "  <Locale lang='en' country='US' messages='en_US.xml' views=\"view2\"/>" +
+                 "  <Locale lang='foo' language_direction='rtl'/>" +
+                 "</ModulePrefs>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    LocaleSpec spec = prefs.getGlobalLocale(new Locale("en", "UK"));
+    assertNull(spec);
+    spec = prefs.getLocale(new Locale("en", "UK"),"view1");
+    assertEquals("http://example.org/en.xml", spec.getMessages().toString());
+    spec = prefs.getLocale(new Locale("en", "UK"),"view2");
+    assertNull(spec);
+    spec = prefs.getLocale(new Locale("en", "US"),"view2");
+    assertEquals("http://example.org/en_US.xml", spec.getMessages().toString());
+    spec = prefs.getLocale(new Locale("foo", "ALL"),"view2");
+    assertEquals("rtl", spec.getLanguageDirection());
+  }
+
+  @Test
+  public void getLinks() throws Exception {
+    String link1Rel = "foo";
+    String link2Rel = "bar";
+    Uri link1Href = Uri.parse("http://example.org/foo");
+    Uri link2Href = Uri.parse("/bar");
+    String xml = "<ModulePrefs title='links'>" +
+                 "  <Link rel='" + link1Rel + "' href='" + link1Href + "'/>" +
+                 "  <Link rel='" + link2Rel + "' href='" + link2Href + "'/>" +
+                 "</ModulePrefs>";
+
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL)
+        .substitute(new Substitutions());
+
+    assertEquals(link1Href, prefs.getLinks().get(link1Rel).getHref());
+    assertEquals(SPEC_URL.resolve(link2Href), prefs.getLinks().get(link2Rel).getHref());
+  }
+
+  @Test
+  public void getViewFeatures() throws Exception {
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(FULL_XML), SPEC_URL);
+    Map<String, Feature> features = prefs.getViewFeatures("default");
+    assertEquals(5, features.size());
+    assertTrue(features.containsKey("requiredview1"));
+    assertTrue(features.containsKey("require"));
+    assertTrue(features.containsKey("optional"));
+    assertTrue(features.containsKey("core"));
+    assertTrue(features.containsKey("security-token"));
+    assertTrue(!features.containsKey("requiredview2"));
+
+    features = prefs.getViewFeatures("view2");
+    assertEquals(5, features.size());
+    assertTrue(features.containsKey("requiredview2"));
+    assertTrue(features.containsKey("require"));
+    assertTrue(features.containsKey("optional"));
+    assertTrue(features.containsKey("core"));
+    assertTrue(features.containsKey("security-token"));
+    assertTrue(!features.containsKey("requiredview1"));
+  }
+
+  @Test
+  public void getViewFeatureParams() throws Exception {
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(FULL_XML), SPEC_URL);
+    Map<String, Feature> features = prefs.getViewFeatures("view2");
+    String paramValue = features.get("require").getParam("param_name");
+    assertNotNull(paramValue);
+    assertEquals("param_value", paramValue);
+  }
+
+  @Test
+  public void doSubstitution() throws Exception {
+    String xml = "<ModulePrefs title='__MSG_title__'>" +
+                 "  <Icon>__MSG_icon__</Icon>" +
+                 "  <Link rel='__MSG_rel__' href='__MSG_link_href__'/>" +
+                 "  <Preload href='__MSG_pre_href__'/>" +
+                 "    <Require feature=\"testFeature\">" +
+                 "           <Param name=\"test_param\">__MSG_test_param__</Param>" +
+                 "  </Require>" +
+                 "</ModulePrefs>";
+    String title = "blah";
+    String icon = "http://example.org/icon.gif";
+    String rel = "foo-bar";
+    String linkHref = "http://example.org/link.html";
+    String preHref = "http://example.org/preload.html";
+    String testParam = "bar-foo";
+
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    Substitutions subst = new Substitutions();
+    subst.addSubstitution(Substitutions.Type.MESSAGE, "title", title);
+    subst.addSubstitution(Substitutions.Type.MESSAGE, "icon", icon);
+    subst.addSubstitution(Substitutions.Type.MESSAGE, "rel", rel);
+    subst.addSubstitution(Substitutions.Type.MESSAGE, "link_href", linkHref);
+    subst.addSubstitution(Substitutions.Type.MESSAGE, "pre_href", preHref);
+    subst.addSubstitution(Substitutions.Type.MESSAGE, "test_param", testParam);
+    prefs = prefs.substitute(subst);
+
+    assertEquals(title, prefs.getTitle());
+    assertEquals(icon, prefs.getIcons().get(0).getContent());
+    assertEquals(rel, prefs.getLinks().get(rel).getRel());
+    assertEquals(linkHref, prefs.getLinks().get(rel).getHref().toString());
+    assertEquals(preHref, prefs.getPreloads().get(0).getHref().toString());
+    assertEquals(testParam, prefs.getFeatures().get("testFeature").getParam("test_param"));
+  }
+
+  @Test
+  public void malformedIntAttributeTreatedAsZero() throws Exception {
+    String xml = "<ModulePrefs title='' height='100px' width='foobar' arbitrary='0xff'/>";
+
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+
+    assertEquals(0, prefs.getHeight());
+    assertEquals(0, prefs.getWidth());
+    assertEquals(0, prefs.getIntAttribute("arbitrary"));
+  }
+
+  @Test
+  public void missingTitleOkay() throws Exception {
+    String xml = "<ModulePrefs/>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    assertNotNull("Empty ModulePrefs Parses", prefs);
+    assertEquals("Title is empty string", "", prefs.getTitle());
+  }
+
+  @Test
+  public void coreInjectedAutomatically() throws Exception {
+    String xml = "<ModulePrefs title=''><Require feature='foo'/></ModulePrefs>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(2, prefs.getFeatures().size());
+    assertTrue(prefs.getFeatures().containsKey("foo"));
+    assertTrue(prefs.getFeatures().containsKey("core"));
+  }
+
+  @Test
+  public void coreNotInjectedOnSplitCoreInclusion() throws Exception {
+    String xml = "<ModulePrefs title=''><Require feature='core.config'/></ModulePrefs>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(1, prefs.getFeatures().size());
+    assertTrue(prefs.getFeatures().containsKey("core.config"));
+  }
+
+  @Test
+  public void securityTokenInjectedOnOAuthTag() throws Exception {
+    // Make sure it is injected when it should be
+    String xml =
+        "<ModulePrefs title=''>" +
+        "  <OAuth>" +
+        "    <Service name='serviceOne'>" +
+        "      <Request url='http://www.example.com/request'" +
+        "          method='GET' param_location='auth-header' />" +
+        "      <Authorization url='http://www.example.com/authorize'/>" +
+        "      <Access url='http://www.example.com/access' method='GET'" +
+        "          param_location='auth-header' />" +
+        "    </Service>" +
+        "  </OAuth>" +
+        "</ModulePrefs>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    Map<String, Feature> features = prefs.getFeatures();
+    assertEquals(2, features.size());
+    assertTrue(features.containsKey("core"));
+    assertTrue(features.containsKey("security-token"));
+
+    Map<String, Feature> viewFeatures = prefs.getViewFeatures("default");
+    assertEquals(2, viewFeatures.size());
+    assertTrue(viewFeatures.containsKey("core"));
+    assertTrue(viewFeatures.containsKey("security-token"));
+  }
+
+  @Test
+  public void securityTokenNotInjectedByDefault() throws Exception {
+    // Make sure the token isn't injected when it shouldn't be, i.e., not required/optional and no
+    // OAuth
+    String xml =
+      "<ModulePrefs title=''>" +
+      "</ModulePrefs>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    Map<String, Feature> features = prefs.getFeatures();
+    assertEquals(1, features.size());
+    assertTrue(features.containsKey("core"));
+
+    Map<String, Feature> viewFeatures = prefs.getViewFeatures("default");
+    assertEquals(1, viewFeatures.size());
+    assertTrue(viewFeatures.containsKey("core"));
+  }
+
+  @Test
+  public void toStringIsSane() throws Exception {
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(FULL_XML), SPEC_URL);
+    doAsserts(new ModulePrefs(XmlUtil.parse(prefs.toString()), SPEC_URL));
+  }
+
+  @Test
+  public void needsUserPrefSubstInTitle() throws Exception {
+    String xml = "<ModulePrefs title='Title __UP_foo__'/>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    assertTrue(prefs.needsUserPrefSubstitution());
+  }
+
+  @Test
+  public void needsUserPrefSubstInTitleUrl() throws Exception {
+    String xml = "<ModulePrefs title='foo' title_url='http://__UP_url__'/>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    assertTrue(prefs.needsUserPrefSubstitution());
+  }
+
+  @Test
+  public void needsUserPrefSubstInPreload() throws Exception {
+    String xml = "<ModulePrefs title='foo'>" +
+        "  <Preload href='__UP_foo__' authz='signed'/></ModulePrefs>";
+    ModulePrefs prefs = new ModulePrefs(XmlUtil.parse(xml), SPEC_URL);
+    assertTrue(prefs.needsUserPrefSubstitution());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/OAuthServiceTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/OAuthServiceTest.java
new file mode 100644
index 0000000..c84ad2e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/OAuthServiceTest.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class OAuthServiceTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+  private OAuthService service;
+
+  @Before
+  public void setUp() {
+    service = new OAuthService();
+  }
+
+  @Test
+  public void testParseAuthorizeUrl() throws Exception {
+    String xml = "<Authorization url='http://azn.example.com'/>";
+    Uri url = service.parseAuthorizationUrl(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("http://azn.example.com", url.toString());
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testParseAuthorizeUrl_nourl() throws Exception {
+    String xml = "<Authorization/>";
+    service.parseAuthorizationUrl(XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test
+  public void testParseAuthorizeUrl_extraAttr() throws Exception {
+    String xml = "<Authorization url='http://www.example.com' foo='bar'/>";
+    Uri url = service.parseAuthorizationUrl(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("http://www.example.com", url.toString());
+  }
+
+  @Test
+  public void testParseAuthorizeUrl_notHttp() throws Exception {
+    OAuthService service = new OAuthService();
+    String xml = "<Authorization url='ftp://www.example.com'/>";
+    try {
+      service.parseAuthorizationUrl(XmlUtil.parse(xml), SPEC_URL);
+      fail("Should have rejected malformed Authorization element");
+    } catch (SpecParserException e) {
+      assertEquals("OAuth/Service/Authorization @url is not valid: ftp://www.example.com", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testParseEndPoint() throws Exception {
+    String xml = "<Request url='http://www.example.com'/>";
+    OAuthService.EndPoint ep = service.parseEndPoint("Request", XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("http://www.example.com", ep.url.toString());
+    assertEquals(OAuthService.Location.HEADER, ep.location);
+    assertEquals(OAuthService.Method.GET, ep.method);
+  }
+
+  @Test
+  public void testParseEndPoint_badlocation() throws Exception {
+    try {
+      String xml = "<Request url='http://www.example.com' method='GET' param_location='body'/>";
+      service.parseEndPoint("Request", XmlUtil.parse(xml), SPEC_URL);
+      fail("Should have thrown");
+    } catch (SpecParserException e) {
+      assertEquals("Unknown OAuth param_location: body", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testParseEndPoint_nodefaults() throws Exception {
+    String xml = "<Request url='http://www.example.com' method='GET' param_location='post-body'/>";
+    OAuthService.EndPoint ep = service.parseEndPoint("Request", XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("http://www.example.com", ep.url.toString());
+    assertEquals(OAuthService.Location.BODY, ep.location);
+    assertEquals(OAuthService.Method.GET, ep.method);
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testParseEndPoint_nourl() throws Exception {
+    String xml = "<Request method='GET' param_location='post-body'/>";
+    service.parseEndPoint("Request", XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testParseEndPoint_badurl() throws Exception {
+    String xml = "<Request url='ftp://www.example.com' />";
+    service.parseEndPoint("Request", XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test
+  public void testParseService() throws Exception {
+    String xml = "" +
+        "<Service name='thename'>" +
+        "   <Request url='http://request.example.com/foo'/>" +
+        "   <Access url='http://access.example.com/bar'/>" +
+        "   <Authorization url='http://azn.example.com/quux'/>" +
+        "</Service>";
+    OAuthService s = new OAuthService(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("thename", s.getName());
+    assertEquals(OAuthService.Location.HEADER, s.getAccessUrl().location);
+    assertEquals("http://azn.example.com/quux", s.getAuthorizationUrl().toString());
+  }
+
+  @Test
+  public void testParseService_noname() throws Exception {
+    String xml = "" +
+        "<Service>" +
+        "   <Request url='http://request.example.com/foo'/>" +
+        "   <Access url='http://access.example.com/bar'/>" +
+        "   <Authorization url='http://azn.example.com/quux'/>" +
+        "</Service>";
+    OAuthService s = new OAuthService(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("", s.getName());
+    assertEquals(OAuthService.Location.HEADER, s.getAccessUrl().location);
+    assertEquals("http://azn.example.com/quux", s.getAuthorizationUrl().toString());
+  }
+
+  @Test
+  public void testParseService_nodata() throws Exception {
+    String xml = "<Service/>";
+    try {
+      new OAuthService(XmlUtil.parse(xml), SPEC_URL);
+    } catch (SpecParserException e) {
+      assertEquals("/OAuth/Service/Request is required", e.getMessage());
+    }
+  }
+
+  @Test
+  public void testParseService_reqonly() throws Exception {
+    String xml = "<Service>" +
+        "<Request url='http://www.example.com/request'/>" +
+        "</Service>";
+    try {
+      new OAuthService(XmlUtil.parse(xml), SPEC_URL);
+    } catch (SpecParserException e) {
+      assertEquals("/OAuth/Service/Access is required", e.getMessage());
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/OAuthSpecTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/OAuthSpecTest.java
new file mode 100644
index 0000000..22db36c
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/OAuthSpecTest.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+
+import org.junit.Test;
+
+/**
+ * Tests for OAuthSpec
+ */
+public class OAuthSpecTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+
+  @Test
+  public void testOAuthSpec() throws Exception {
+    String xml = "<OAuth><Service>" +
+      "<Request url='http://www.example.com/request'/>" +
+      "<Access url='http://www.example.com/access'/>" +
+      "<Authorization url='http://www.example.com/authorize'/>" +
+      "</Service></OAuth>";
+    OAuthSpec oauth = new OAuthSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(1, oauth.getServices().size());
+  }
+
+  @Test
+  public void testOAuthRelativeUrl() throws Exception {
+    String xml = "<OAuth><Service>" +
+      "<Request url='/request'/>" +
+      "<Access url='/access'/>" +
+      "<Authorization url='/authorize'/>" +
+      "</Service></OAuth>";
+    OAuthSpec oauth = new OAuthSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(1, oauth.getServices().size());
+    OAuthService service = oauth.getServices().get("");
+    assertEquals(service.getRequestUrl().url.toString(), "http://example.org/request");
+    assertEquals(service.getAuthorizationUrl().toString(), "http://example.org/authorize");
+    assertEquals(service.getAccessUrl().url.toString(), "http://example.org/access");
+  }
+
+  @Test
+  public void testOAuthSpec_noservice() throws Exception {
+    String xml = "<OAuth/>";
+    OAuthSpec oauth = new OAuthSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals(0, oauth.getServices().size());
+  }
+
+  @Test
+  public void testOAuthSpec_threeservice() throws Exception {
+    String xml = "<OAuth>" +
+        "<Service name='one'>" +
+        " <Request url='http://req.example.com' param_location='uri-query' method='POST'/>" +
+        " <Access url='http://acc.example.com' param_location='uri-query' method='POST'/>" +
+        " <Authorization url='http://azn.example.com'/>" +
+        "</Service>" +
+        "<Service name='two'>" +
+        " <Request url='http://two.example.com/req'/>" +
+        " <Access url='http://two.example.com'/>" +
+        " <Authorization url='http://two.example.com/authorize'/>" +
+        "</Service>" +
+        "<Service name='three'>" +
+        " <Request url='http://three.example.com' param_location='uri-query' method='POST'/>" +
+        " <Access url='http://three.example.com/acc' param_location='uri-query' method='POST'/>" +
+        " <Authorization url='http://three.example.com/authorize'/>" +
+        "</Service>" +
+        "</OAuth>";
+    OAuthSpec oauth = new OAuthSpec(XmlUtil.parse(xml), SPEC_URL);
+    assertEquals("http://req.example.com",
+        oauth.getServices().get("one").getRequestUrl().url.toString());
+    assertEquals(OAuthService.Location.URL,
+        oauth.getServices().get("one").getRequestUrl().location);
+    assertEquals("http://two.example.com",
+        oauth.getServices().get("two").getAccessUrl().url.toString());
+    assertEquals(OAuthService.Method.POST,
+        oauth.getServices().get("three").getRequestUrl().method);
+    assertEquals("http://three.example.com/acc",
+        oauth.getServices().get("three").getAccessUrl().url.toJavaUri().toASCIIString());
+    assertEquals("http://three.example.com/authorize",
+        oauth.getServices().get("three").getAuthorizationUrl().toJavaUri().toASCIIString());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java
new file mode 100644
index 0000000..bb4084e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PipelinedDataTest.java
@@ -0,0 +1,418 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.spec.PipelinedData.BatchItem;
+import org.apache.shindig.gadgets.spec.PipelinedData.BatchType;
+
+import java.util.Map;
+
+import javax.el.ELResolver;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class PipelinedDataTest {
+
+  private static final Uri GADGET_URI = Uri.parse("http://example.org/");
+  private ELResolver elResolver;
+  private Map<String, Object> elValues;
+  private Expressions expressions;
+
+  @Before
+  public void setUp() {
+    elValues = Maps.newHashMap();
+    elResolver = new RootELResolver(elValues);
+    expressions = Expressions.forTesting();
+  }
+
+  @Test
+  public void testDataRequest() throws Exception {
+    String xml = "<Content><DataRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " method=\"people.get\""
+        + " groupId=\"${params.groupId}\""
+        + " userId=\"${userIds}\""
+        + " startIndex=\"${startIndex}\""
+        + " fields=\"${fields}\""
+        + "/></Content>";
+
+    elValues.put("startIndex", 10);
+    // Test a param that evaluates to null
+    elValues.put("params", ImmutableMap.of());
+    elValues.put("userIds", Lists.newArrayList("first", "second"));
+    elValues.put("fields", new JSONArray("['name','id']"));
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertFalse(socialData.needsOwner());
+    assertFalse(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'people.get', id: 'key', params:"
+        + "{userId: ['first','second'],"
+        + "startIndex: 10,"
+        + "fields: ['name','id']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+  }
+
+  @Test
+  public void testPeopleRequest() throws Exception {
+    String xml = "<Content><PeopleRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " groupId=\"group\""
+        + " userId=\"first,second\""
+        + " startIndex=\"20\""
+        + " count=\"10\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertFalse(socialData.needsOwner());
+    assertFalse(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'people.get', id: 'key', params:"
+        + "{groupId: 'group',"
+        + "userId: ['first','second'],"
+        + "startIndex: 20,"
+        + "count: 10,"
+        + "fields: ['name','id']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+    assertNull(batch.getNextBatch(elResolver));
+  }
+
+  @Test
+  public void testPeopleRequestWithExpressions() throws Exception {
+    String xml = "<Content><PeopleRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " groupId=\"group\""
+        + " userId=\"first,second\""
+        + " startIndex=\"20\""
+        + " count=\"${count}\""
+        + " fields=\"${fields}\""
+        + "/></Content>";
+
+    elValues.put("count", 10);
+    // TODO: try List, JSONArray
+    elValues.put("fields", "name,id");
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertFalse(socialData.needsOwner());
+    assertFalse(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'people.get', id: 'key', params:"
+        + "{groupId: 'group',"
+        + "userId: ['first','second'],"
+        + "startIndex: 20,"
+        + "count: 10,"
+        + "fields: ['name','id']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+  }
+
+  @Test
+  public void testViewerRequest() throws Exception {
+    String xml = "<Content><ViewerRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertFalse(socialData.needsOwner());
+    assertTrue(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'people.get', id: 'key', params:"
+        + "{userId: ['@viewer'],"
+        + "fields: ['name','id']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+  }
+
+  @Test
+  public void testOwnerRequest() throws Exception {
+    String xml = "<Content><OwnerRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertTrue(socialData.needsOwner());
+    assertFalse(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'people.get', id: 'key', params:"
+        + "{userId: ['@owner'],"
+        + "fields: ['name','id']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+  }
+
+  @Test
+  public void testPersonAppData() throws Exception {
+    String xml = "<Content><PersonAppDataRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " userId=\"@viewer\""
+        + " fields=\"foo,bar\""
+        + "/></Content>";
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertFalse(socialData.needsOwner());
+    assertTrue(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'appdata.get', id: 'key', params:"
+        + "{userId: ['@viewer'],"
+        + "fields: ['foo','bar']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+  }
+
+  @Test
+  public void testActivitiesRequest() throws Exception {
+    String xml = "<Content><ActivitiesRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " userId=\"@owner,@viewer\""
+        + " startIndex=\"10\""
+        + " count=\"20\""
+        + " fields=\"foo,bar\""
+        + "/></Content>";
+
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertTrue(socialData.needsOwner());
+    assertTrue(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'activities.get', id: 'key', params:"
+        + "{userId: ['@owner','@viewer'],"
+        + "startIndex: 10,"
+        + "count: 20,"
+        + "fields: ['foo','bar']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+  }
+
+  @Test
+  public void testActivityStreamsRequest() throws Exception {
+    String xml = "<Content><ActivityStreamsRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " userId=\"@owner,@viewer\""
+        + " startIndex=\"10\""
+        + " count=\"20\""
+        + " fields=\"foo,bar\""
+        + "/></Content>";
+
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertTrue(socialData.needsOwner());
+    assertTrue(socialData.needsViewer());
+
+    JSONObject expected = new JSONObject("{method: 'activitystreams.get', id: 'key', params:"
+        + "{userId: ['@owner','@viewer'],"
+        + "startIndex: 10,"
+        + "count: 20,"
+        + "fields: ['foo','bar']"
+        + "}}");
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    PipelinedData.BatchItem batchItem = batch.getPreloads().get("key");
+    assertEquals(PipelinedData.BatchType.SOCIAL, batchItem.getType());
+    JsonAssert.assertObjectEquals(expected, batchItem.getData());
+  }
+
+  @Test
+  public void testIgnoreNoNamespace() throws Exception {
+    String xml = "<Content><PersonRequest"
+        + " key=\"key\""
+        + " userId=\"@owner\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), null);
+    assertFalse(socialData.needsOwner());
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+    assertNull(batch);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void testErrorOnUnknownOpensocialElement() throws Exception {
+    String xml = "<Content><NotARealElement xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + "/></Content>";
+
+    new PipelinedData(XmlUtil.parse(xml), null);
+  }
+
+  @Test
+  public void testBatching() throws Exception {
+    String xml = "<Content xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+        + "<PeopleRequest key=\"key\" userId=\"${userId}\"/>"
+        + "<HttpRequest key=\"key2\" href=\"${key}\"/>"
+        + "</Content>";
+
+    PipelinedData socialData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+
+    PipelinedData.Batch batch = socialData.getBatch(expressions, elResolver);
+
+    assertTrue(batch.getPreloads().isEmpty());
+
+    // Now have "userId", the next batch should resolve the people request
+    elValues.put("userId", "foo");
+    batch = batch.getNextBatch(elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    assertTrue(batch.getPreloads().containsKey("key"));
+
+    // Now, add "key", the next batch should resolve the HTTP request
+    elValues.put("key", "somedata");
+    batch = batch.getNextBatch(elResolver);
+    assertEquals(1, batch.getPreloads().size());
+    assertTrue(batch.getPreloads().containsKey("key2"));
+
+    // And the final batch should be empty
+    assertNull(batch.getNextBatch(elResolver));
+  }
+
+  @Test
+  public void testVariable() throws Exception {
+    String xml = "<Content xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\">"
+            + "<osx:Variable xmlns:osx=\"" + PipelinedData.EXTENSION_NAMESPACE + "\" "
+            +       "key=\"key\" value=\"${1+1}\"/>"
+        + "</Content>";
+
+    PipelinedData pipelinedData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+
+    PipelinedData.Batch batch = pipelinedData.getBatch(expressions, elResolver);
+    assertFalse(pipelinedData.needsViewer());
+    assertFalse(pipelinedData.needsOwner());
+
+    assertEquals(1, batch.getPreloads().size());
+    BatchItem output = batch.getPreloads().get("key");
+    assertEquals(BatchType.VARIABLE, output.getType());
+    assertEquals(2L, output.getData());
+  }
+
+ @Test
+  public void httpRequestDefaults() throws Exception {
+    String xml = "<Content><HttpRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " href=\"/example.html\""
+        + "/></Content>";
+
+    PipelinedData pipelinedData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+    PipelinedData.Batch batch = pipelinedData.getBatch(expressions, elResolver);
+    assertFalse(pipelinedData.needsViewer());
+    assertFalse(pipelinedData.needsOwner());
+
+    assertEquals(1, batch.getPreloads().size());
+    BatchItem output = batch.getPreloads().get("key");
+    assertEquals(BatchType.HTTP, output.getType());
+    RequestAuthenticationInfo preload = (RequestAuthenticationInfo) output.getData();
+    assertEquals(AuthType.NONE, preload.getAuthType());
+    assertEquals(Uri.parse("http://example.org/example.html"), preload.getHref());
+  }
+
+  @Test
+  public void badHrefTest() throws Exception {
+    // unparseable url escape
+    String xml = "<Content><HttpRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " href=\"/example.html%\""
+        + "/></Content>";
+
+    boolean foundException = false;
+    try {
+      PipelinedData pipelinedData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+      PipelinedData.Batch batch = pipelinedData.getBatch(expressions, elResolver);
+    } catch (RuntimeException e) {
+      foundException = true;
+    }
+    assertTrue("found RuntimeException (for now) see SHINDIG-1090", foundException);
+  }
+
+  @Test
+  public void httpRequestDefaultsSigned() throws Exception {
+    String xml = "<Content><HttpRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " href=\"/example.html\""
+        + " authz=\"signed\""
+        + " sign_owner=\"false\""
+        + "/></Content>";
+
+    PipelinedData pipelinedData = new PipelinedData(XmlUtil.parse(xml), GADGET_URI);
+    PipelinedData.Batch batch = pipelinedData.getBatch(expressions, elResolver);
+    assertTrue(pipelinedData.needsViewer());
+    assertFalse(pipelinedData.needsOwner());
+
+    assertEquals(1, batch.getPreloads().size());
+    BatchItem output = batch.getPreloads().get("key");
+    assertEquals(BatchType.HTTP, output.getType());
+    RequestAuthenticationInfo preload = (RequestAuthenticationInfo) output.getData();
+    assertEquals(AuthType.SIGNED, preload.getAuthType());
+    assertTrue(preload.isSignViewer());
+    assertFalse(preload.isSignOwner());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PreloadTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PreloadTest.java
new file mode 100644
index 0000000..44b8179
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/PreloadTest.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.AuthType;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import org.junit.Test;
+
+import java.util.Set;
+
+/**
+ * Tests for Preload
+ */
+public class PreloadTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+  private final static String HREF = "http://example.org/preload.xml";
+  private final static Set<String> VIEWS = ImmutableSet.of("view0", "view1");
+
+  @Test
+  public void basicPreload() throws Exception {
+    String xml = "<Preload href='" + HREF + "'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+
+    assertEquals(HREF, preload.getHref().toString());
+    assertEquals(AuthType.NONE, preload.getAuthType());
+    assertEquals(0, preload.getAttributes().size());
+    assertTrue("Default value for sign_owner should be true.",
+                preload.isSignOwner());
+    assertTrue("Default value for sign_viewer should be true.",
+               preload.isSignViewer());
+  }
+
+  @Test
+  public void authzSigned() throws Exception {
+    String xml = "<Preload href='" + HREF + "' authz='signed'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+
+    assertEquals(AuthType.SIGNED, preload.getAuthType());
+  }
+
+  @Test
+  public void authzOAuth() throws Exception {
+    String xml = "<Preload href='" + HREF + "' authz='oauth'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+
+    assertEquals(AuthType.OAUTH, preload.getAuthType());
+  }
+
+  @Test
+  public void authzUnknownTreatedAsNone() throws Exception {
+    String xml = "<Preload href='foo' authz='bad-bad-bad value!'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+
+    assertEquals(AuthType.NONE, preload.getAuthType());
+  }
+
+  @Test
+  public void multipleViews() throws Exception {
+    String xml = "<Preload href='" + HREF + '\'' +
+                 " views='" + Joiner.on(',').join(VIEWS) + "'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+
+    assertEquals(VIEWS, preload.getViews());
+  }
+
+  @Test
+  public void substitutionsOk() throws Exception {
+    String xml = "<Preload href='__MSG_preload__'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Substitutions.Type.MESSAGE, "preload", HREF);
+    Preload substituted = preload.substitute(substituter);
+
+    assertEquals(HREF, substituted.getHref().toString());
+  }
+
+  @Test
+  public void relativeSubstitutionsOk() throws Exception {
+    String xml = "<Preload href='__MSG_preload__'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Substitutions.Type.MESSAGE, "preload", "relative");
+    Preload substituted = preload.substitute(substituter);
+
+    assertEquals(SPEC_URL.resolve(Uri.parse("relative")), substituted.getHref());
+  }
+
+  @Test
+  public void arbitraryAttributes() throws Exception {
+    String xml = "<Preload href='" + HREF + "' foo='bar' yo='momma' sub='__MSG_preload__'/>";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Substitutions.Type.MESSAGE, "preload", "stuff");
+    Preload substituted = preload.substitute(substituter);
+    assertEquals("bar", substituted.getAttributes().get("foo"));
+    assertEquals("momma", substituted.getAttributes().get("yo"));
+    assertEquals("stuff", substituted.getAttributes().get("sub"));
+  }
+
+  @Test
+  public void toStringIsSane() throws Exception {
+    String xml = "<Preload" +
+                 " href='" + HREF + '\'' +
+                 " authz='signed'" +
+                 " views='" + Joiner.on(',').join(VIEWS) + '\'' +
+                 " some_attribute='yes' />";
+
+    Preload preload = new Preload(XmlUtil.parse(xml), SPEC_URL);
+    Preload preload2 = new Preload(XmlUtil.parse(preload.toString()), SPEC_URL);
+
+    assertEquals(VIEWS, preload2.getViews());
+    assertEquals(HREF, preload2.getHref().toString());
+    assertEquals(AuthType.SIGNED, preload2.getAuthType());
+    assertEquals("yes", preload2.getAttributes().get("some_attribute"));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void missingHrefThrows() throws Exception {
+    String xml = "<Preload/>";
+    new Preload(XmlUtil.parse(xml), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void malformedHrefThrows() throws Exception {
+    String xml = "<Preload href='@$%@$%$%'/>";
+    new Preload(XmlUtil.parse(xml), SPEC_URL);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/UserPrefTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/UserPrefTest.java
new file mode 100644
index 0000000..58fd65e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/UserPrefTest.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.variables.Substitutions;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class UserPrefTest extends Assert {
+  @Test
+  public void testBasic() throws Exception {
+    String xml = "<UserPref" +
+                 " name=\"name\"" +
+                 " display_name=\"display_name\"" +
+                 " default_value=\"default_value\"" +
+                 " required=\"true\"" +
+                 " datatype=\"hidden\"/>";
+    UserPref userPref = new UserPref(XmlUtil.parse(xml));
+    assertEquals("name", userPref.getName());
+    assertEquals("display_name", userPref.getDisplayName());
+    assertEquals("default_value", userPref.getDefaultValue());
+    assertTrue(userPref.getRequired());
+    assertEquals(UserPref.DataType.HIDDEN, userPref.getDataType());
+  }
+
+  @Test
+  public void testEnum() throws Exception {
+    String xml = "<UserPref name=\"foo\" datatype=\"enum\">" +
+                 " <EnumValue value=\"0\" display_value=\"Zero\"/>" +
+                 " <EnumValue value=\"1\"/>" +
+                 "</UserPref>";
+    UserPref userPref = new UserPref(XmlUtil.parse(xml));
+    assertEquals(2, userPref.getEnumValues().size());
+    assertEquals("Zero", userPref.getEnumValues().get("0"));
+    assertEquals("1", userPref.getEnumValues().get("1"));
+  }
+
+  @Test
+  public void testSubstitutions() throws Exception {
+    String xml = "<UserPref name=\"name\" datatype=\"enum\"" +
+                 " display_name=\"__MSG_display_name__\"" +
+                 " default_value=\"__MSG_default_value__\">" +
+                 " <EnumValue value=\"0\" display_value=\"__MSG_dv__\"/>" +
+                 "</UserPref>";
+    String displayName = "This is the display name";
+    String defaultValue = "This is the default value";
+    String displayValue = "This is the display value";
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Substitutions.Type.MESSAGE,
+        "display_name", displayName);
+    substituter.addSubstitution(Substitutions.Type.MESSAGE,
+        "default_value", defaultValue);
+    substituter.addSubstitution(Substitutions.Type.MESSAGE, "dv", displayValue);
+    UserPref userPref
+        = new UserPref(XmlUtil.parse(xml)).substitute(substituter);
+    assertEquals(displayName, userPref.getDisplayName());
+    assertEquals(defaultValue, userPref.getDefaultValue());
+    assertEquals(displayValue, userPref.getEnumValues().get("0"));
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testMissingName() throws Exception {
+    String xml = "<UserPref datatype=\"string\"/>";
+    new UserPref(XmlUtil.parse(xml));
+  }
+
+  @Test
+  public void testMissingDataType() throws Exception {
+    String xml = "<UserPref name=\"name\"/>";
+    UserPref pref = new UserPref(XmlUtil.parse(xml));
+    assertEquals(UserPref.DataType.STRING, pref.getDataType());
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testMissingEnumValue() throws Exception {
+    String xml = "<UserPref name=\"foo\" datatype=\"enum\">" +
+                 " <EnumValue/>" +
+                 "</UserPref>";
+    new UserPref(XmlUtil.parse(xml));
+  }
+
+  @Test
+  public void testToString() throws Exception {
+    String xml = "<UserPref name=\"name\" display_name=\"__MSG_display_name__\" "
+        + "default_value=\"__MSG_default_value__\" required=\"false\" "
+        + "datatype=\"enum\">"
+        + "<EnumValue value=\"0\" display_value=\"__MSG_dv__\"/>"
+        + "</UserPref>";
+    UserPref userPref = new UserPref(XmlUtil.parse(xml));
+    assertEquals(xml, userPref.toString().replace("\n", ""));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java
new file mode 100644
index 0000000..5e3e644
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/spec/ViewTest.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.spec;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.variables.Substitutions;
+import org.apache.shindig.gadgets.variables.Substitutions.Type;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class ViewTest {
+  private static final Uri SPEC_URL = Uri.parse("http://example.org/g.xml");
+
+  @Test
+  public void testSimpleView() throws Exception {
+    String viewName = "VIEW NAME";
+    String content = "This is the content";
+
+    String xml = "<Content" +
+                 " type=\"html\"" +
+                 " view=\"" + viewName + '\"' +
+                 " quirks=\"false\"><![CDATA[" +
+                    content +
+                 "]]></Content>";
+
+    View view = new View(viewName, Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+
+    assertEquals(viewName, view.getName());
+    Assert.assertFalse(view.getQuirks());
+    assertEquals(View.ContentType.HTML, view.getType());
+    assertEquals("html", view.getRawType());
+    assertEquals(content, view.getContent());
+    assertTrue("Default value for sign_owner should be true.", view.isSignOwner());
+    assertTrue("Default value for sign_viewer should be true.", view.isSignViewer());
+  }
+
+  @Test
+  public void testConcatenation() throws Exception {
+    String body1 = "Hello, ";
+    String body2 = "World!";
+    String content1 = "<Content type=\"html\">" + body1 + "</Content>";
+    String content2 = "<Content type=\"html\">" + body2 + "</Content>";
+    View view = new View("test", Arrays.asList(XmlUtil.parse(content1),
+                                               XmlUtil.parse(content2)), SPEC_URL);
+    assertEquals(body1 + body2, view.getContent());
+  }
+
+  @Test
+  public void testNonStandardContentType() throws Exception {
+    String contentType = "html-inline";
+    String xml = "<Content" +
+                 " type=\"" + contentType + '\"' +
+                 " quirks=\"false\"><![CDATA[blah]]></Content>";
+    View view = new View("default", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+
+    assertEquals(View.ContentType.HTML, view.getType());
+    assertEquals(contentType, view.getRawType());
+  }
+
+  @Test
+  public void testHtmlSanitizedContentType() throws Exception {
+    String contentType = "x-html-sanitized";
+    String xml = "<Content" +
+                 " type=\"" + contentType + '\"' +
+                 " quirks=\"false\"><![CDATA[blah]]></Content>";
+    View view = new View("default", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+
+    assertEquals(View.ContentType.HTML_SANITIZED, view.getType());
+    assertEquals(contentType, view.getRawType());
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void testContentTypeConflict() throws Exception {
+    String content1 = "<Content type=\"html\"/>";
+    String content2 = "<Content type=\"url\" href=\"http://example.org/\"/>";
+    new View("test", Arrays.asList(XmlUtil.parse(content1), XmlUtil.parse(content2)), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void testHrefOnTypeUrl() throws Exception {
+    String xml = "<Content type=\"url\"/>";
+    new View("dummy", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void testHrefMalformed() throws Exception {
+    // Unfortunately, this actually does URI validation rather than URL, so
+    // most anything will pass. urn:isbn:0321146530 is valid here.
+    String xml = "<Content type=\"url\" href=\"fobad@$%!fdf\"/>";
+    new View("dummy", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+  }
+
+  @Test
+  public void testQuirksCascade() throws Exception {
+    String content1 = "<Content type=\"html\" quirks=\"true\"/>";
+    String content2 = "<Content type=\"html\" quirks=\"false\"/>";
+    View view = new View("test", Arrays.asList(XmlUtil.parse(content1),
+                                               XmlUtil.parse(content2)), SPEC_URL);
+    Assert.assertFalse(view.getQuirks());
+  }
+
+  @Test
+  public void testQuirksCascadeReverse() throws Exception {
+    String content1 = "<Content type=\"html\" quirks=\"false\"/>";
+    String content2 = "<Content type=\"html\" quirks=\"true\"/>";
+    View view = new View("test", Arrays.asList(XmlUtil.parse(content1),
+                                               XmlUtil.parse(content2)), SPEC_URL);
+    Assert.assertTrue(view.getQuirks());
+  }
+
+  @Test
+  public void testPreferredHeight() throws Exception {
+    String content1 = "<Content type=\"html\" preferred_height=\"100\"/>";
+    String content2 = "<Content type=\"html\" preferred_height=\"300\"/>";
+    View view = new View("test", Arrays.asList(XmlUtil.parse(content1),
+                                               XmlUtil.parse(content2)), SPEC_URL);
+    assertEquals(300, view.getPreferredHeight());
+  }
+
+  @Test
+  public void testPreferredWidth() throws Exception {
+    String content1 = "<Content type=\"html\" preferred_width=\"300\"/>";
+    String content2 = "<Content type=\"html\" preferred_width=\"172\"/>";
+    View view = new View("test", Arrays.asList(XmlUtil.parse(content1),
+                                               XmlUtil.parse(content2)), SPEC_URL);
+    assertEquals(172, view.getPreferredWidth());
+  }
+
+  @Test
+  public void testContentSubstitution() throws Exception {
+    String xml
+        = "<Content type=\"html\">Hello, __MSG_world__ __MODULE_ID__</Content>";
+
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Type.MESSAGE, "world", "foo __UP_planet____BIDI_START_EDGE__");
+    substituter.addSubstitution(Type.USER_PREF, "planet", "Earth");
+    substituter.addSubstitution(Type.BIDI, "START_EDGE", "right");
+    substituter.addSubstitution(Type.MODULE, "ID", "3");
+
+    View view = new View("test",
+        Arrays.asList(XmlUtil.parse(xml)), SPEC_URL).substitute(substituter);
+    assertEquals("Hello, foo Earthright 3", view.getContent());
+  }
+
+  @Test
+  public void testHrefSubstitution() throws Exception {
+    String href = "http://__MSG_domain__/__MODULE_ID__?dir=__BIDI_DIR__";
+    String xml = "<Content type=\"url\" href=\"" + href + "\"/>";
+
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Type.MESSAGE, "domain", "__UP_subDomain__.example.org");
+    substituter.addSubstitution(Type.USER_PREF, "subDomain", "up");
+    substituter.addSubstitution(Type.BIDI, "DIR", "rtl");
+    substituter.addSubstitution(Type.MODULE, "ID", "123");
+
+    View view = new View("test",
+        Arrays.asList(XmlUtil.parse(xml)), SPEC_URL).substitute(substituter);
+    assertEquals("http://up.example.org/123?dir=rtl",
+                 view.getHref().toString());
+  }
+
+  @Test
+  public void testHrefRelativeSubstitution() throws Exception {
+    String href = "__MSG_foo__";
+    String xml = "<Content type=\"url\" href=\"" + href + "\"/>";
+
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Type.MESSAGE, "foo", "/bar");
+
+    View view = new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+    view = view.substitute(substituter);
+    assertEquals(Uri.parse("//example.org/bar"), view.getHref());
+  }
+
+  @Test
+  public void testHrefWithoutSchemaResolution() throws Exception {
+    String href = "//xyz.com/gadget.xml";
+    String xml = "<Content type=\"url\" href=\"" + href + "\"/>";
+
+    View view = new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+    assertEquals(Uri.parse(href), view.getHref());
+  }
+
+  @Test
+  public void authAttributes() throws Exception {
+    String xml = "<Content type='html' sign_owner='false' sign_viewer='false' foo='bar' " +
+                 "yo='momma' sub='__MSG_view__'/>";
+
+    View view = new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+    Substitutions substituter = new Substitutions();
+    substituter.addSubstitution(Substitutions.Type.MESSAGE, "view", "stuff");
+    View substituted = view.substitute(substituter);
+    assertEquals("bar", substituted.getAttributes().get("foo"));
+    assertEquals("momma", substituted.getAttributes().get("yo"));
+    assertEquals("stuff", substituted.getAttributes().get("sub"));
+    assertFalse("sign_owner parsed incorrectly.", view.isSignOwner());
+    assertFalse("sign_viewer parsed incorrectly.", view.isSignViewer());
+  }
+
+  @Test
+  public void testSocialPreload() throws Exception {
+    String xml = "<Content href=\"http://example.org/proxied.php\" "
+        + "authz=\"SIGNED\">"
+        + "<OwnerRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+    View view = new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+    PipelinedData.Batch batch = view.getPipelinedData().getBatch(
+        Expressions.forTesting(), new RootELResolver());
+
+    assertEquals(1, batch.getPreloads().size());
+    assertTrue(batch.getPreloads().containsKey("key"));
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void testSocialPreloadWithoutAuth() throws Exception {
+    // Not signed, so a parse exception will result
+    String xml = "<Content href=\"http://example.org/proxied.php\" "
+        + "sign_owner=\"true\">"
+        + "<OwnerRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+    new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void testSocialPreloadWithoutSignOwner() throws Exception {
+    // Signed, but not by owner when owner data is fetched
+    String xml = "<Content href=\"http://example.org/proxied.php\" "
+        + "authz=\"SIGNED\" sign_owner=\"false\">"
+        + "<OwnerRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+    new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+  }
+
+  @Test(expected = SpecParserException.class)
+  public void testSocialPreloadWithoutSignViewer() throws Exception {
+    // Signed, but not by viewer when viewer data is fetched
+    String xml = "<Content href=\"http://example.org/proxied.php\" "
+        + "authz=\"SIGNED\" sign_viewer=\"false\">"
+        + "<ViewerRequest xmlns=\"" + PipelinedData.OPENSOCIAL_NAMESPACE + "\" "
+        + " key=\"key\""
+        + " fields=\"name,id\""
+        + "/></Content>";
+    new View("test", Arrays.asList(XmlUtil.parse(xml)), SPEC_URL);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java
new file mode 100644
index 0000000..0f0861f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/DefaultTemplateProcessorTest.java
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.render.SanitizingGadgetRewriter;
+import org.apache.shindig.gadgets.templates.tags.AbstractTagHandler;
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+
+import javax.el.ELResolver;
+
+/**
+ * Unit tests for DefaultTemplateProcessor.
+ * TODO: Refactor to remove boilerplate.
+ * TODO: Add tests for special vars.
+ * TODO: Add test for @var in @repeat loops.
+ */
+public class DefaultTemplateProcessorTest {
+
+  private Expressions expressions;
+
+  private TemplateContext context;
+  private DefaultTemplateProcessor processor;
+  private Map<String, Object> variables;
+  private ELResolver resolver;
+  private TagRegistry registry;
+
+  private NekoSimplifiedHtmlParser parser;
+
+  private static final String TEST_NS = "http://example.com";
+  protected SingletonElementHandler singletonElementHandler;
+
+  @Before
+  public void setUp() throws Exception {
+    expressions = Expressions.forTesting();
+    variables = Maps.newHashMap();
+    singletonElementHandler = new SingletonElementHandler();
+    Set<TagHandler> handlers = ImmutableSet.<TagHandler>of(
+        new TestTagHandler(),
+        singletonElementHandler);
+    registry = new DefaultTagRegistry(handlers);
+
+    processor = new DefaultTemplateProcessor(expressions);
+    resolver = new RootELResolver();
+    parser = new NekoSimplifiedHtmlParser(new ParseModule.DOMImplementationProvider().get());
+    context = new TemplateContext(new Gadget(), variables);
+
+    variables.put("foo", new JSONObject("{ title: 'bar' }"));
+    variables.put("user", new JSONObject("{ id: '101', name: { first: 'John', last: 'Doe' }}"));
+    variables.put("toys", new JSONObject("{ list: [{name: 'Ball'}, {name: 'Car'}]}"));
+    variables.put("countries", new JSONArray("['Ireland','France']"));
+    variables.put("xss", new JSONObject("{ script: '<script>alert();</script>'," +
+        "quote:'\"><script>alert();</script>'}"));
+  }
+
+  @Test
+  public void testTextNode() throws Exception {
+    String output = executeTemplate("${foo.title}");
+    assertEquals("bar", output);
+  }
+
+  @Test
+  public void testTopVariable() throws Exception {
+    String output = executeTemplate("${Top.foo.title}");
+    assertEquals("bar", output);
+  }
+
+  @Test
+  public void testCurVariable() throws Exception {
+    // Cur starts as Top
+    String output = executeTemplate("${Cur.foo.title}");
+    assertEquals("bar", output);
+  }
+
+  @Test
+  public void testMyVariable() throws Exception {
+    // My starts as null
+    String output = executeTemplate("${My.foo.title}");
+    assertEquals("", output);
+  }
+
+  @Test
+  public void testPlainText() throws Exception {
+    // Verify that plain text is not interfered with, or incorrectly escaped
+    String output = executeTemplate("<span>foo&amp;&bar</span>");
+    assertEquals("<span>foo&amp;&bar</span>", output);
+  }
+
+  @Test
+  public void testTextNodeEscaping() throws Exception {
+    String output = executeTemplate("${xss.script}");
+    assertFalse("Escaping not performed: \"" + output + '\"', output.contains("<script>alert("));
+  }
+
+  @Test
+  public void testAppending() throws Exception {
+    String output = executeTemplate("${user.id}${user.name.first}");
+    assertEquals("101John", output);
+
+    output = executeTemplate("foo${user.id}bar${user.name.first}baz");
+    assertEquals("foo101barJohnbaz", output);
+
+    output = executeTemplate("foo${user.nope}bar${user.nor}baz");
+    assertEquals("foobarbaz", output);
+  }
+
+  @Test
+  public void testEscapedExpressions() throws Exception {
+    String output = executeTemplate("\\${escaped}");
+    assertEquals("\\${escaped}", output);
+
+    output = executeTemplate("foo\\${escaped}bar");
+    assertEquals("foo\\${escaped}bar", output);
+  }
+
+  @Test
+  public void testElement() throws Exception {
+    String output = executeTemplate("<span title=\"${user.id}\">${user.name.first} baz</span>");
+    assertEquals("<span title=\"101\">John baz</span>", output);
+  }
+
+  @Test
+  public void testAttributeEscaping() throws Exception {
+    String output = executeTemplate("<span title=\"${xss.quote}\">${user.name.first} baz</span>");
+    assertFalse(output.contains("\"><script>alert("));
+  }
+
+  @Test
+  public void testRepeat() throws Exception {
+    String output = executeTemplate("<span repeat=\"${toys}\">${name}</span>");
+    assertEquals("<span>Ball</span><span>Car</span>", output);
+  }
+
+  @Test
+  public void testRepeatScalar() throws Exception {
+    String output = executeTemplate("<span repeat=\"${countries}\">${Cur}</span>");
+    assertEquals("<span>Ireland</span><span>France</span>", output);
+  }
+
+  @Test
+  public void testCurAttribute() throws Exception {
+    String output = executeTemplate("<span cur=\"${user.name}\">${first}</span>");
+    assertEquals("<span>John</span>", output);
+  }
+
+  @Test
+  public void testConditional() throws Exception {
+    String output = executeTemplate(
+        "<span repeat=\"${toys}\">" +
+          "<span if=\"${name == 'Car'}\">Car</span>" +
+          "<span if=\"${name != 'Car'}\">Not Car</span>" +
+        "</span>");
+    assertEquals("<span><span>Not Car</span></span><span><span>Car</span></span>", output);
+  }
+
+  @Test
+  public void testCustomTag() throws Exception {
+    String output = executeTemplate("<test:Foo text='${foo.title}' data='${user}'/>",
+        "xmlns:test='" + TEST_NS + '\'');
+    assertEquals("<b>BAR</b>", output);
+  }
+
+  @Test
+  public void testBooleanAttributes() throws Exception {
+    String output = executeTemplate("<input class=\"${1 == 2}\" readonly=\"${1 == 2}\"" +
+        "disabled=\"${1 == 1}\">");
+    assertEquals("<input class=\"false\" disabled=\"disabled\">", output);
+  }
+
+  @Test
+  public void testOnCreate() throws Exception {
+    String output = executeTemplate("<span oncreate=\"foo\"></span>");
+    assertEquals("<span id=\"ostid0\"></span><script type=\"text/javascript\">" +
+        "(function(){foo}).apply(document.getElementById('ostid0'));</script>", output);
+
+    output = executeTemplate("<span x-oncreate=\"foo\"></span>");
+    assertEquals("<span id=\"ostid1\"></span><script type=\"text/javascript\">" +
+        "(function(){foo}).apply(document.getElementById('ostid1'));</script>", output);
+
+    output = executeTemplate("<span id=\"bar\" oncreate=\"foo\"></span>");
+    assertEquals("<span id=\"bar\"></span><script type=\"text/javascript\">" +
+        "(function(){foo}).apply(document.getElementById('bar'));</script>", output);
+
+  }
+
+  /**
+   * Ensure that the element cloning handling of processChildren correctly
+   * copies and element to the target element, including making sure that
+   * document references are properly cleaned up and user_data in the original
+   * content does not refer to the target document
+   * @throws Exception
+   */
+  @Test
+  public void testSafeCrossDocumentCloning() throws Exception {
+    String template = "<test:Bar text='${foo.title}' data='${user}'/>";
+    executeTemplate(template, "xmlns:test='" + TEST_NS + '\'');
+    executeTemplate(template, "xmlns:test='" + TEST_NS + '\'');
+
+    // This is a little hacky but is fine for testing purposes. Assumes that DOM implementation
+    // is based on Xerces which will always has a userData hashtable
+    Document doc = singletonElementHandler.elem.getOwnerDocument();
+    Class<?> docClass = doc.getClass();
+    Field userDataField = null;
+    while (userDataField == null) {
+      try {
+        userDataField = docClass.getDeclaredField("userData");
+      } catch (NoSuchFieldException nsfe) {
+        // Ignore. Try the parent
+      }
+      docClass = docClass.getSuperclass();
+    }
+    // Access is typically protected so just bypass
+    userDataField.setAccessible(true);
+    Hashtable<?, ?> userDataMap = (Hashtable<?, ?>) userDataField.get(doc);
+
+    // There should be only one element in the user data map, if there are more then the
+    // cloning process has put them there which can be a nasty source of memory leaks. Consider
+    // the case of this test where the singleton template is a shared and re-used template where
+    // the  template documents userData starts to accumulate cloned nodes for every time that
+    // template is rendered
+    assertEquals(1, userDataMap.size());
+  }
+
+  private String executeTemplate(String markup) throws Exception {
+    return executeTemplate(markup, "");
+  }
+
+  private String executeTemplate(String markup, String extra) throws Exception {
+    Element template = prepareTemplate(markup, extra);
+    DocumentFragment result = processor.processTemplate(template, context, resolver, registry);
+    return serialize(result);
+  }
+
+  private Element prepareTemplate(String markup, String extra) throws GadgetException {
+    String content = "<script type=\"text/os-template\"" + extra + '>' + markup + "</script>";
+    Document document = parser.parseDom(content);
+    return SocialDataTags.getTags(document, SocialDataTags.OSML_TEMPLATE_TAG).get(0);
+  }
+
+  private String serialize(Node node) throws IOException {
+    StringBuilder sb = new StringBuilder();
+    NodeList children = node.getChildNodes();
+    for (int i = 0; i < children.getLength(); i++) {
+      Node child = children.item(i);
+      new DefaultHtmlSerializer().serialize(child, sb);
+    }
+    return sb.toString();
+  }
+
+  /**
+   * A dummy custom tag.
+   * Expects a @text attribute equal to "bar", and a @data attribute that
+   * evaluates to a JSONObject with an id property equal to "101".
+   * If these conditions are met, returns <code>&lt;b&gt;BAR&lt;/b&gt;</code>
+   */
+  private static class TestTagHandler extends AbstractTagHandler {
+
+    public TestTagHandler() {
+      super(TEST_NS, "Foo");
+    }
+
+    public void process(Node result, Element tag, TemplateProcessor processor) {
+      Object data = getValueFromTag(tag, "data", processor, Object.class);
+      assertTrue(data instanceof JSONObject);
+      assertEquals("101", ((JSONObject) data).optString("id"));
+
+      String text = getValueFromTag(tag, "text", processor, String.class);
+      text = text.toUpperCase();
+      Document doc = result.getOwnerDocument();
+      Element b = doc.createElement("b");
+      b.appendChild(doc.createTextNode(text));
+      result.appendChild(b);
+    }
+  }
+
+  /**
+   * A tag to test the correct behavior of user data and element cloning
+   */
+  private static class SingletonElementHandler extends AbstractTagHandler {
+
+    Element elem = XmlUtil.parseSilent("<wrapper><div><a>out</a></div></wrapper>");
+
+    public SingletonElementHandler() {
+      super(TEST_NS, "Bar");
+      SanitizingGadgetRewriter.bypassSanitization((Element)elem.getFirstChild(), true);
+    }
+
+    public void process(Node result, Element tag, TemplateProcessor processor) {
+      processor.processChildNodes(result, elem);
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/FakeTemplateProcessor.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/FakeTemplateProcessor.java
new file mode 100644
index 0000000..a0174db
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/FakeTemplateProcessor.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.el.ELResolver;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Fake implementation of TemplateProcessor for writing TagHandler tests.
+ */
+public class FakeTemplateProcessor implements TemplateProcessor {
+  public Map<String, ? extends Object> expressionResults = Maps.newHashMap();
+  public TemplateContext context;
+
+  public final <T extends Object> T evaluate(String expression, Class<T> type, T defaultValue) {
+    // Some quick-and-dirty mocking:  put a List in the map, and
+    // you get one result per-entry
+    Object result = expressionResults.get(expression);
+    if (result instanceof List<?> && !type.isAssignableFrom(List.class)) {
+      result = ((List<?>) result).remove(0);
+    }
+    return type.cast(result);
+  }
+
+  public TemplateContext getTemplateContext() {
+    return context;
+  }
+
+  public DocumentFragment processTemplate(Element template,
+      TemplateContext templateContext, ELResolver globals, TagRegistry registry) {
+    throw new UnsupportedOperationException();
+  }
+
+  public void processChildNodes(Node result, Node source) {
+    throw new UnsupportedOperationException();
+  }
+
+  public final void processRepeat(Node result, Element element, Iterable<?> dataList, Runnable onEachLoop) {
+    // for (Object data : dataList) produces an unused variable warning
+    Iterator<?> iterator = dataList.iterator();
+    while (iterator.hasNext()) {
+      iterator.next();
+      onEachLoop.run();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/MessageELResolverTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/MessageELResolverTest.java
new file mode 100644
index 0000000..8116e35
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/MessageELResolverTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.el.ELContext;
+import javax.el.ELException;
+
+public class MessageELResolverTest {
+  static private final String MESSAGE_BUNDLE =
+    "<messagebundle>" +
+      "<msg name='hello'>world</msg>" +
+      "<msg name='number'>${1+1}</msg>" +
+      "<msg name='concat'>${Msg.hello} ${Msg.number}</msg>" +
+      "<msg name='multiLevel'>${Msg.concat} ${Msg.concat}</msg>" +
+      // Self-recursive EL, should fail
+      "<msg name='recurse'>${Msg.recurse}</msg>" +
+      // Mutually recursive EL, should fail
+      "<msg name='mutual1'>${Msg.mutual2}</msg>" +
+      "<msg name='mutual2'>${Msg.mutual1}</msg>" +
+    "</messagebundle>";
+  private MessageBundle messageBundle;
+  private Expressions expressions;
+  private ELContext context;
+
+  @Before
+  public void setUp() throws Exception {
+    messageBundle = new MessageBundle(XmlUtil.parse(MESSAGE_BUNDLE));
+    expressions = Expressions.forTesting();
+    context = expressions.newELContext(new MessageELResolver(expressions, messageBundle));
+  }
+
+  @Test
+  public void basicExpression() {
+    assertEquals("world", expressions.parse("${Msg.hello}", String.class).getValue(context));
+  }
+
+  @Test
+  public void nullForMissingProperty() {
+    assertNull(expressions.parse("${Msg.notThere}", Object.class).getValue(context));
+  }
+
+  @Test
+  public void innerEvaluation() {
+    assertEquals(2, expressions.parse("${Msg.number}", Integer.class).getValue(context));
+  }
+
+  @Test
+  public void recursiveEvaluation() {
+    assertEquals("world 2", expressions.parse("${Msg.concat}", String.class).getValue(context));
+  }
+
+  @Test
+  public void multiLevelRecursiveEvaluation() {
+    assertEquals("world 2 world 2", expressions.parse("${Msg.multiLevel}", String.class).getValue(context));
+  }
+
+  @Test(expected = ELException.class)
+  public void failsInsteadOfInfiniteRecursion() {
+    expressions.parse("${Msg.recurse}", String.class).getValue(context);
+  }
+
+  @Test(expected = ELException.class)
+  public void failsInsteadOfMutualInfiniteRecursion() {
+    expressions.parse("${Msg.mutual1}", String.class).getValue(context);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateLibraryFactoryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateLibraryFactoryTest.java
new file mode 100644
index 0000000..2daed0e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/TemplateLibraryFactoryTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.apache.shindig.gadgets.http.RequestPipeline;
+
+import org.junit.Test;
+
+public class TemplateLibraryFactoryTest {
+
+  public static final Uri SPEC_URL = Uri.parse("http://www.example.org/dir/g.xml");
+  public static final Uri TEMPLATE_URL = Uri.parse("http://www.example.org/dir/template.xml");
+  private static final String TEMPLATE_LIBRARY =
+          "<Templates xmlns:my='#my'>" +
+          "  <Namespace prefix='my' url='#my'/>" +
+          "  <JavaScript>script</JavaScript>" +
+          "  <Style>style</Style>" +
+          "  <Template tag='my:Tag1'>external1</Template>" +
+          "  <Template tag='my:Tag2'>external2</Template>" +
+          "  <Template tag='my:Tag3'>external3</Template>" +
+          "  <Template tag='my:Tag4'>external4</Template>" +
+          "</Templates>";
+
+  @Test
+  public void testTemplateRequestAnonymousSecurityToken() throws GadgetException {
+    CapturingPipeline pipeline = new CapturingPipeline();
+    TemplateLibraryFactory factory = new TemplateLibraryFactory( pipeline, null );
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public Uri getUrl() {
+        return SPEC_URL;
+      }
+
+      @Override
+      public String getContainer() {
+        return "default";
+      }
+
+      @Override
+      public boolean getDebug() {
+        return false;
+      }
+
+      @Override
+      public boolean getIgnoreCache() {
+        return true;
+      }
+    };
+
+    factory.loadTemplateLibrary(context, TEMPLATE_URL);
+    assertNotNull( pipeline.request );
+    SecurityToken st = pipeline.request.getSecurityToken();
+    assertNotNull( st );
+    assertTrue( st.isAnonymous() );
+    assertEquals( SPEC_URL.toString(), st.getAppUrl() );
+  }
+
+  private static class CapturingPipeline implements RequestPipeline {
+    HttpRequest request;
+
+    public HttpResponse execute(HttpRequest request) {
+      this.request = request;
+      return new HttpResponseBuilder().setHttpStatusCode( HttpResponse.SC_OK ).setResponseString( TEMPLATE_LIBRARY ).create();
+    }
+  }
+
+}
+
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/XmlTemplateLibraryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/XmlTemplateLibraryTest.java
new file mode 100644
index 0000000..5d1e6af
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/XmlTemplateLibraryTest.java
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.ImmutableMap;
+
+/**
+ * Test for TemplateLibrary parsing.
+ *
+ * TODO: Parse failure tests
+ */
+public class XmlTemplateLibraryTest {
+
+  public static final String LIB_MARKUP =
+    "<Templates xmlns:my='#my'>" +
+    "  <Namespace prefix='my' url='#my'/>" +
+    "  <JavaScript>libscript</JavaScript>" +
+    "  <JavaScript>libscript2</JavaScript>" +
+    "  <Style>libstyle</Style>" +
+    "  <Style>libstyle2</Style>" +
+    "  <Template tag='my:Flat'>Flat tag</Template>" +
+    "  <TemplateDef tag='my:Def'>" +
+    "    <Template>Def tag</Template>" +
+    "    <JavaScript>tagscript</JavaScript>" +
+    "    <Style>tagstyle</Style>" +
+    "  </TemplateDef>" +
+    "</Templates>";
+
+  private static TemplateLibrary lib;
+
+  private static Element doc;
+
+  @BeforeClass
+  public static void createDefaultLibrary() throws Exception {
+    doc = XmlUtil.parse(LIB_MARKUP);
+    lib = new XmlTemplateLibrary(Uri.parse("http://example.com/my"), doc, LIB_MARKUP);
+  }
+
+  @Test
+  public void testTemplateElement() throws Exception {
+    TagRegistry registry = lib.getTagRegistry();
+    assertNotNull(registry.getHandlerFor(new TagRegistry.NSName("#my", "Flat")));
+  }
+
+  @Test
+  public void testTemplateDefElement() throws Exception {
+    TagRegistry registry = lib.getTagRegistry();
+    assertNotNull(registry.getHandlerFor(new TagRegistry.NSName("#my", "Def")));
+  }
+
+  @Test
+  public void testMissingElements() {
+    TagRegistry registry = lib.getTagRegistry();
+    assertNull(registry.getHandlerFor(new TagRegistry.NSName("#my", "Foo")));
+    assertNull(registry.getHandlerFor(new TagRegistry.NSName("my", "Flat")));
+  }
+
+  @Test
+  public void testAddedResources() {
+    final TemplateContext context = new TemplateContext(null, ImmutableMap.<String, Object>of());
+    TemplateProcessor processor = new DefaultTemplateProcessor(Expressions.forTesting()) {
+      @Override
+      public TemplateContext getTemplateContext() {
+        return context;
+      }
+    };
+
+    TagHandler handlerWithResources = lib.getTagRegistry()
+       .getHandlerFor(new TagRegistry.NSName("#my", "Def"));
+    TagHandler handlerWithNoResources = lib.getTagRegistry()
+        .getHandlerFor(new TagRegistry.NSName("#my", "Flat"));
+
+    Node result = doc.getOwnerDocument().createDocumentFragment();
+    Element tag = doc.getOwnerDocument().createElement("test");
+
+    // Script and style elements for the library should get registered
+    // with the first tag for the whole library
+    handlerWithNoResources.process(result, tag, processor);
+    assertEquals("<STYLE>libstyle\nlibstyle2</STYLE>" +
+                 "<JAVASCRIPT>libscript\nlibscript2</JAVASCRIPT>",
+                 serializeResources(context));
+
+    // Now script and style elements for the tag should get registered
+    handlerWithResources.process(result, tag, processor);
+    assertEquals("<STYLE>libstyle\nlibstyle2</STYLE>" +
+        "<JAVASCRIPT>libscript\nlibscript2</JAVASCRIPT>" +
+        "<JAVASCRIPT>tagscript</JAVASCRIPT>" +
+        "<STYLE>tagstyle</STYLE>",
+        serializeResources(context));
+
+    // Nothing new should get registered with one more call
+    handlerWithResources.process(result, tag, processor);
+    assertEquals("<STYLE>libstyle\nlibstyle2</STYLE>" +
+        "<JAVASCRIPT>libscript\nlibscript2</JAVASCRIPT>" +
+        "<JAVASCRIPT>tagscript</JAVASCRIPT>" +
+        "<STYLE>tagstyle</STYLE>",
+        serializeResources(context));
+  }
+
+  private String serializeResources(TemplateContext context) {
+    StringBuilder builder = new StringBuilder();
+    for (TemplateResource resource : context.getResources()) {
+      builder.append(resource);
+    }
+
+    return builder.toString();
+  }
+  @Test
+  public void testSerialize() {
+    assertEquals(LIB_MARKUP, lib.serialize());
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/AbstractTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/AbstractTagHandlerTest.java
new file mode 100644
index 0000000..2dfe0fa
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/AbstractTagHandlerTest.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isNull;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.gadgets.templates.tags.AbstractTagHandler;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+public class AbstractTagHandlerTest {
+  private DOMImplementation documentProvider;
+  private Document document;
+  private AbstractTagHandler handler;
+  private TemplateProcessor templateProcessor;
+
+
+  @Before
+  public void setUp() throws Exception {
+    documentProvider = new ParseModule.DOMImplementationProvider().get();
+    document = documentProvider.createDocument(null, null, null);
+    handler = new AbstractTagHandler(null, null) {
+      public void process(Node result, Element tag, TemplateProcessor processor) {
+      }
+    };
+
+    templateProcessor = createMock(TemplateProcessor.class);
+  }
+
+  @Test
+  public void getValueFromTag() {
+    Element element = document.createElement("test");
+    element.setAttribute("key", "expression");
+
+    expect(templateProcessor.evaluate(eq("expression"), eq(String.class), (String) isNull()))
+        .andReturn("evaluated");
+    replay(templateProcessor);
+
+    assertEquals("evaluated",
+        handler.getValueFromTag(element, "key", templateProcessor, String.class));
+    verify(templateProcessor);
+  }
+
+  @Test
+  public void getValueFromTagNoAttribute() {
+    Element element = document.createElement("test");
+
+    replay(templateProcessor);
+    assertNull(handler.getValueFromTag(element, "notthere", templateProcessor, String.class));
+    verify(templateProcessor);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/CompositeTagRegistryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/CompositeTagRegistryTest.java
new file mode 100644
index 0000000..29c4086
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/CompositeTagRegistryTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.gadgets.templates.tags.AbstractTagHandler;
+import org.apache.shindig.gadgets.templates.tags.CompositeTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+
+public class CompositeTagRegistryTest {
+  public static final String TEST_NAMESPACE = "#test";
+  private TagHandler fooTag;
+  private TagHandler fooTag2;
+  private TagHandler barTag;
+  private CompositeTagRegistry registry;
+
+  @Before
+  public void setUp() {
+    fooTag = createTagHandler("foo");
+    fooTag2 = createTagHandler("foo");
+    barTag = createTagHandler("bar");
+
+    TagRegistry first = new DefaultTagRegistry(ImmutableSet.of(fooTag, barTag));
+    TagRegistry second = new DefaultTagRegistry(ImmutableSet.of(fooTag2));
+
+    registry = new CompositeTagRegistry(ImmutableList.of(first, second));
+  }
+
+  @Test
+  public void firstRegistryWins() {
+    TagRegistry.NSName foo = new TagRegistry.NSName(TEST_NAMESPACE, "foo");
+    assertSame(fooTag, registry.getHandlerFor(foo));
+  }
+
+  @Test
+  public void secondRegistryUsed() {
+    TagRegistry.NSName bar = new TagRegistry.NSName(TEST_NAMESPACE, "bar");
+    assertSame(barTag, registry.getHandlerFor(bar));
+  }
+
+  @Test
+  public void unknownNamesReturnNull() {
+    TagRegistry.NSName baz = new TagRegistry.NSName(TEST_NAMESPACE, "baz");
+    assertNull(registry.getHandlerFor(baz));
+  }
+
+  private TagHandler createTagHandler(String name) {
+    return new AbstractTagHandler(TEST_NAMESPACE, name) {
+      public void process(Node result, Element tag, TemplateProcessor processor) {
+      }
+    };
+
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/DefaultTagRegistryTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/DefaultTagRegistryTest.java
new file mode 100644
index 0000000..1171015
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/DefaultTagRegistryTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.gadgets.templates.tags.AbstractTagHandler;
+import org.apache.shindig.gadgets.templates.tags.DefaultTagRegistry;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.ImmutableSet;
+
+public class DefaultTagRegistryTest {
+  public static final String TEST_NAMESPACE = "#test";
+  public static final String TEST_NAME = "Tag";
+  private TagHandler tag;
+  private DefaultTagRegistry registry;
+
+  @Before
+  public void setUp() {
+    tag = new AbstractTagHandler(TEST_NAMESPACE, TEST_NAME) {
+      public void process(Node result, Element tag, TemplateProcessor processor) {
+      }
+    };
+
+    registry = new DefaultTagRegistry(ImmutableSet.of(tag));
+  }
+
+  @Test
+  public void getHandlerForWithElement() {
+    Element el = XmlUtil.parseSilent("<Tag xmlns='#test'/>");
+    assertSame(tag, registry.getHandlerFor(el));
+  }
+
+  @Test
+  public void getHandlerForUsesNamespace() {
+    Element el = XmlUtil.parseSilent("<Tag xmlns='#nottest'/>");
+    assertNull(registry.getHandlerFor(el));
+  }
+
+  @Test
+  public void getHandlerIsCaseSensitive() {
+    Element el = XmlUtil.parseSilent("<tag xmlns='#test'/>");
+    assertNull(registry.getHandlerFor(el));
+  }
+
+  @Test
+  public void getHandlerForWithNSName() {
+    TagRegistry.NSName nsName = new TagRegistry.NSName(TEST_NAMESPACE, TEST_NAME);
+    assertSame(tag, registry.getHandlerFor(nsName));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandlerTest.java
new file mode 100644
index 0000000..807e22f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/FlashTagHandlerTest.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.isA;
+
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.xml.DomUtil;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.rewrite.XPathWrapper;
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.apache.shindig.gadgets.templates.TemplateContext;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.easymock.EasyMock;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.el.ELResolver;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Basic tests for Flash tag
+ */
+public class FlashTagHandlerTest extends EasyMockTestCase {
+
+  private MyTemplateProcessor processor;
+  private DOMImplementation documentProvider;
+  private FlashTagHandler handler;
+  private FeatureRegistry featureRegistry;
+  private GadgetContext gadgetContext = mock(GadgetContext.class);
+  private Gadget gadget = mock(Gadget.class);
+
+  private NekoSimplifiedHtmlParser parser;
+  protected Document result;
+
+  @Before
+  public void setUp() throws Exception {
+    processor = new MyTemplateProcessor();
+    processor.context = new TemplateContext(gadget, Collections.<String, JSONObject>emptyMap());
+    Injector injector = Guice.createInjector(new ParseModule(), new PropertiesModule());
+    documentProvider = injector.getInstance(DOMImplementation.class);
+    parser = injector.getInstance(NekoSimplifiedHtmlParser.class);
+    featureRegistry = mock(FeatureRegistry.class, true);
+    handler = new FlashTagHandler(new BeanJsonConverter(injector), featureRegistry,
+        "http://example.org/ns", "9.0.115");
+    result = parser.parseDom("");
+
+    EasyMock.expect(gadget.getContext()).andReturn(gadgetContext).anyTimes();
+  }
+
+  private void expectFeatureLookup() throws GadgetException {
+    List<FeatureResource> swfObjectResources = Lists.newArrayList();
+    swfObjectResources.add(new SwfResource());
+    final FeatureRegistry.LookupResult lr = EasyMock.createMock(FeatureRegistry.LookupResult.class);
+    EasyMock.expect(lr.getResources()).andReturn(swfObjectResources);
+    EasyMock.replay(lr);
+    EasyMock.expect(featureRegistry
+        .getFeatureResources(isA(GadgetContext.class), eq(ImmutableSet.of("swfobject")),
+            EasyMock.<List<String>>isNull())).andReturn(lr);
+  }
+
+  private static class SwfResource extends FeatureResource.Default {
+    public String getContent() {
+      return "swfobject()";
+    }
+
+    public String getDebugContent() {
+      return "swfobject";
+    }
+
+    public String getName() {
+      return "swfname";
+    }
+  }
+
+  private void expectSecurityToken() {
+    EasyMock.expect(gadgetContext.getParameter(EasyMock.eq("st"))).andReturn("12345");
+  }
+
+  @Test
+  public void testBasicRender() throws Exception {
+    Document document = parser.parseDom(
+        "<script type='text/os-template'>"
+            + "<osx:Flash swf='http://www.example.org/test.swf'>"
+            + "Click Me"
+          + "</osx:Flash></script>");
+    Element tag = DomUtil.getElementsByTagNameCaseInsensitive(document, ImmutableSet.of("osx:flash"))
+        .get(0);
+
+    expectSecurityToken();
+    EasyMock.expect(gadget.sanitizeOutput()).andReturn(false);
+    expectFeatureLookup();
+    replay();
+    handler.process(result.getDocumentElement().getFirstChild().getNextSibling(), tag, processor);
+    XPathWrapper wrapper = new XPathWrapper(result);
+    assertEquals("swfobject()", wrapper.getValue("/html/head/script[1]"));
+    assertEquals("os_xFlash_alt_1", wrapper.getValue("/html/body/div/@id"));
+    assertEquals("Click Me", wrapper.getValue("/html/body/div"));
+    assertNull(wrapper.getNode("/html/body/div/@onclick"));
+    assertEquals(wrapper.getValue("/html/body/script[1]"),
+        "swfobject.embedSWF(\"http://www.example.org/test.swf\",\"os_xFlash_alt_1\",\"100px\","
+            + "\"100px\",\"9.0.115\",null,null,{\"flashvars\":\"st=12345\"},{});");
+    verify();
+  }
+
+  @Test
+  public void testSanitizedRender() throws Exception {
+    Document document = parser.parseDom(
+        "<script type='text/os-template'>"
+            + "<osx:Flash swf='http://www.example.org/test.swf'>"
+            + "Click Me"
+          + "</osx:Flash></script>");
+    Element tag = DomUtil.getElementsByTagNameCaseInsensitive(document, ImmutableSet.of("osx:flash"))
+        .get(0);
+
+    expectSecurityToken();
+    EasyMock.expect(gadget.sanitizeOutput()).andReturn(true);
+    expectFeatureLookup();
+    replay();
+    handler.process(result.getDocumentElement().getFirstChild().getNextSibling(), tag, processor);
+    XPathWrapper wrapper = new XPathWrapper(result);
+    assertEquals("swfobject()", wrapper.getValue("/html/head/script[1]"));
+    assertEquals("os_xFlash_alt_1", wrapper.getValue("/html/body/div/@id"));
+    assertEquals("Click Me", wrapper.getValue("/html/body/div"));
+    assertNull(wrapper.getNode("/html/body/div/@onclick"));
+    assertEquals(wrapper.getValue("/html/body/script[1]"),
+        "swfobject.embedSWF(\"http://www.example.org/test.swf\",\"os_xFlash_alt_1\",\"100px\","
+            + "\"100px\",\"9.0.115\",null,null,{\"swliveconnect\":false,"
+            + "\"flashvars\":\"st=12345\",\"allowscriptaccess\":\"never\",\"allownetworking\":\"internal\"},{});");
+    verify();
+  }
+
+  @Test
+  public void testSanitizedRenderClickToPlay() throws Exception {
+    Document document = parser.parseDom(
+        "<script type='text/os-template'>"
+            + "<osx:flash swf='http://www.example.org/test.swf' play='onclick'>"
+            + "Click Me"
+          + "</osx:flash></script>");
+    Element tag = DomUtil.getElementsByTagNameCaseInsensitive(document, ImmutableSet.of("osx:flash"))
+        .get(0);
+
+    expectSecurityToken();
+    EasyMock.expect(gadget.sanitizeOutput()).andReturn(true);
+    expectFeatureLookup();
+    replay();
+    handler.process(result.getDocumentElement().getFirstChild().getNextSibling(), tag, processor);
+    XPathWrapper wrapper = new XPathWrapper(result);
+    assertEquals("swfobject()", wrapper.getValue("/html/head/script[1]"));
+    assertEquals("os_xFlash_alt_1", wrapper.getValue("/html/body/div/@id"));
+    assertEquals("Click Me", wrapper.getValue("/html/body/div"));
+    assertEquals("os_xFlash_alt_1()", wrapper.getValue("/html/body/div/@onclick"));
+    assertEquals(wrapper.getValue("/html/body/script[1]"),
+        "function os_xFlash_alt_1(){ swfobject.embedSWF(\"http://www.example.org/test.swf\","
+            + "\"os_xFlash_alt_1\",\"100px\",\"100px\",\"9.0.115\",null,null,"
+            + "{\"swliveconnect\":false,\"flashvars\":\"st=12345\",\"allowscriptaccess\":\"never\",\"allownetworking\":\"internal\"},{}); }");
+    verify();
+  }
+
+  @Test
+  public void testConfigCreation() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("test");
+    tag.setAttribute("id", "myflash");
+    tag.setAttribute("class", "stylish");
+    tag.setAttribute("swf", "http://www.example.org/x.swf");
+    tag.setAttribute("width", "100px");
+    tag.setAttribute("height", "200px");
+    tag.setAttribute("name", "myflashname");
+    tag.setAttribute("play", "onclick");
+    tag.setAttribute("menu", "true");
+    tag.setAttribute("scale", "exactfit");
+    tag.setAttribute("wmode", "transparent");
+    tag.setAttribute("devicefont", "true");
+    tag.setAttribute("swliveconnect", "true");
+    tag.setAttribute("allowscriptaccess", "samedomain");
+    //tag.setAttribute("loop", "true");
+    tag.setAttribute("quality", "autohigh");
+    tag.setAttribute("salign", "tl");
+    tag.setAttribute("bgcolor", "#77ff77");
+    tag.setAttribute("allowfullscreen", "true");
+    tag.setAttribute("allownetworking", "none");
+    tag.setAttribute("flashvars", "a=b&c=d");
+    FlashTagHandler.SwfObjectConfig config = handler.getSwfConfig(tag, processor);
+    assertEquals("myflash", config.id);
+    assertEquals("stylish", config.clazz);
+    assertEquals(config.swf, Uri.parse("http://www.example.org/x.swf"));
+    assertEquals("100px", config.width);
+    assertEquals("200px", config.height);
+    assertEquals("myflashname", config.name);
+    assertEquals(FlashTagHandler.SwfObjectConfig.Play.onclick, config.play);
+    assertEquals(Boolean.TRUE, config.menu);
+    assertEquals(FlashTagHandler.SwfObjectConfig.Scale.exactfit, config.scale);
+    assertEquals(FlashTagHandler.SwfObjectConfig.WMode.transparent, config.wmode);
+    assertEquals(Boolean.TRUE, config.devicefont);
+    assertEquals(Boolean.TRUE, config.swliveconnect);
+    assertEquals(FlashTagHandler.SwfObjectConfig.ScriptAccess.samedomain, config.allowscriptaccess);
+    assertNull(config.loop);
+    assertEquals(FlashTagHandler.SwfObjectConfig.Quality.autohigh, config.quality);
+    assertEquals(FlashTagHandler.SwfObjectConfig.SAlign.tl, config.salign);
+    assertEquals("#77ff77", config.bgcolor);
+    assertEquals(Boolean.TRUE, config.allowfullscreen);
+    assertEquals(FlashTagHandler.SwfObjectConfig.NetworkAccess.none, config.allownetworking);
+    assertEquals("a=b&c=d", config.flashvars);
+  }
+
+  @Test
+  public void testConfigBindingFailure() throws Exception {
+    Document document = parser.parseDom(
+        "<script type='text/os-template'>"
+            + "<osx:flash swf='http://www.example.org/test.swf' play='junk'>"
+            + "Click Me"
+          + "</osx:flash></script>");
+    Element tag = DomUtil.getElementsByTagNameCaseInsensitive(document, ImmutableSet.of("osx:flash"))
+        .get(0);
+    handler.process(result.getDocumentElement().getFirstChild().getNextSibling(), tag, processor);
+    XPathWrapper wrapper = new XPathWrapper(result);
+    assertTrue(wrapper.getValue("/html/body/span").startsWith("Failed to process os:Flash tag"));
+  }
+
+  private static class MyTemplateProcessor implements TemplateProcessor {
+    public TemplateContext context;
+
+    public DocumentFragment processTemplate(Element template, TemplateContext templateContext,
+                                            ELResolver globals, TagRegistry registry) {
+      throw new UnsupportedOperationException();
+    }
+
+    public TemplateContext getTemplateContext() {
+      return context;
+    }
+
+    public void processRepeat(Node result, Element element, Iterable<?> dataList,
+                              Runnable onEachLoop) {
+      // for (Object data : dataList) produces an unused variable warning
+      Iterator<?> iterator = dataList.iterator();
+      while (iterator.hasNext()) {
+        iterator.next();
+        onEachLoop.run();
+      }
+    }
+
+    public <T> T evaluate(String expression, Class<T> type, T defaultValue) {
+      return type.cast(expression);
+    }
+
+    public void processChildNodes(Node result, Node source) {
+      NodeList childNodes = source.getChildNodes();
+      for (int i = 0; i < childNodes.getLength(); i++) {
+        Node child = childNodes.item(0).cloneNode(true);
+        result.getOwnerDocument().adoptNode(child);
+        result.appendChild(child);
+      }
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/HtmlTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/HtmlTagHandlerTest.java
new file mode 100644
index 0000000..28f1e8e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/HtmlTagHandlerTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.templates.FakeTemplateProcessor;
+
+import com.google.common.collect.ImmutableMap;
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+
+/**
+ * Test of the <os:Html> tag.
+ */
+public class HtmlTagHandlerTest {
+  private FakeTemplateProcessor processor;
+  private DOMImplementation documentProvider;
+  private HtmlTagHandler handler;
+
+  @Before
+  public void setUp() throws Exception {
+    processor = new FakeTemplateProcessor();
+    documentProvider = new ParseModule.DOMImplementationProvider().get();
+    handler = new HtmlTagHandler(new NekoSimplifiedHtmlParser(documentProvider));
+  }
+
+  @Test
+  public void testHtmlTag() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("test");
+    tag.setAttribute("code", "${code}");
+    processor.expressionResults = ImmutableMap.of(
+        "${code}", "Hello <b>World</b>!");
+    DocumentFragment fragment = doc.createDocumentFragment();
+    handler.process(fragment, tag, processor);
+    assertEquals(3, fragment.getChildNodes().getLength());
+    assertEquals("b", fragment.getChildNodes().item(1).getNodeName());
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/IfTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/IfTagHandlerTest.java
new file mode 100644
index 0000000..d214aea
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/IfTagHandlerTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.easymock.EasyMock.isNull;
+import static org.easymock.EasyMock.same;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.templates.FakeTemplateProcessor;
+import org.apache.shindig.gadgets.templates.tags.IfTagHandler;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import com.google.common.collect.ImmutableMap;
+
+public class IfTagHandlerTest {
+  private FakeTemplateProcessor processor;
+  private DOMImplementation documentProvider;
+  private TagHandler handler;
+
+  @Before
+  public void setUp() throws Exception {
+    processor = EasyMock.createMock(FakeTemplateProcessor.class);
+    documentProvider = new ParseModule.DOMImplementationProvider().get();
+    handler = new IfTagHandler();
+  }
+
+  @Test
+  public void conditionIsFalse() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("if");
+
+    tag.setAttribute(IfTagHandler.CONDITION_ATTR, "fakeExpression");
+    processor.expressionResults = ImmutableMap.of("fakeExpression", false);
+
+    replay(processor);
+    handler.process(null, tag, processor);
+    verify(processor);
+  }
+
+  @Test
+  public void conditionIsTrue() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("if");
+    tag.setAttribute(IfTagHandler.CONDITION_ATTR, "fakeExpression");
+
+    processor.expressionResults = ImmutableMap.of("fakeExpression", true);
+    processor.processChildNodes((Node) isNull(), same(tag));
+
+    replay(processor);
+    handler.process(null, tag, processor);
+    verify(processor);
+  }
+
+  @Test
+  public void conditionIsMissing() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("if");
+
+    replay(processor);
+    handler.process(null, tag, processor);
+    verify(processor);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/RenderTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/RenderTagHandlerTest.java
new file mode 100644
index 0000000..e298e5a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/RenderTagHandlerTest.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.templates.DefaultTemplateProcessor;
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.apache.shindig.gadgets.templates.TemplateContext;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.json.JSONObject;
+import static org.junit.Assert.assertEquals;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.el.ELResolver;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+public class RenderTagHandlerTest {
+
+  private Expressions expressions;
+
+  private TemplateContext context;
+  private DefaultTemplateProcessor processor;
+  private Map<String, JSONObject> variables;
+  private ELResolver resolver;
+  private TagRegistry registry;
+
+  private NekoSimplifiedHtmlParser parser;
+
+  private static final String TEST_NS = "http://example.com";
+
+  @Before
+  public void setUp() throws Exception {
+    expressions = Expressions.forTesting();
+    variables = Maps.newHashMap();
+    Set<TagHandler> handlers = ImmutableSet.of((TagHandler) new RenderTagHandler());
+    registry = new DefaultTagRegistry(handlers);
+
+    processor = new DefaultTemplateProcessor(expressions);
+    resolver = new RootELResolver();
+    parser = new NekoSimplifiedHtmlParser(new ParseModule.DOMImplementationProvider().get());
+    Gadget gadget = new Gadget();
+    gadget.setContext(new GadgetContext());
+    context = new TemplateContext(gadget, variables);
+
+    addVariable("foo", new JSONObject("{ title: 'bar' }"));
+  }
+
+  @Test
+  public void renderAllChildren() throws Exception {
+    runTest("Bar",
+        "[<os:Render/>]",
+        "<foo:Bar>Hello</foo:Bar>", "[Hello]");
+  }
+
+  @Test
+  public void renderSingleChildren() throws Exception {
+    runTest("Panel",
+        "<os:Render content='header'/> <os:Render content='footer'/>",
+        "<foo:Panel><footer>Second</footer><header>First</header></foo:Panel>",
+        "First Second");
+  }
+
+  private void runTest(String tagName, String tagMarkup, String templateMarkup,
+      String expectedResult) throws GadgetException, IOException {
+    Element templateDef = parseTemplate(templateMarkup);
+    Element tagInstance = parseTemplate(tagMarkup);
+
+    templateDef.getOwnerDocument().adoptNode(tagInstance);
+    TagHandler tagHandler =
+      new TemplateBasedTagHandler(tagInstance, TEST_NS, tagName);
+
+    TagRegistry reg = new CompositeTagRegistry(ImmutableList.of(
+        registry,
+        new DefaultTagRegistry(ImmutableSet.of(tagHandler))));
+
+    DocumentFragment result = processor.processTemplate(templateDef, context, resolver, reg);
+    String output = serialize(result);
+    assertEquals(expectedResult, output);
+  }
+
+  private Element parseTemplate(String markup) throws GadgetException {
+    String content = "<script type=\"text/os-template\" xmlns:foo=\"" + TEST_NS +
+        "\" xmlns:os=\"" + TagHandler.OPENSOCIAL_NAMESPACE + "\">" + markup + "</script>";
+    Document document = parser.parseDom(content);
+    return SocialDataTags.getTags(document, SocialDataTags.OSML_TEMPLATE_TAG).get(0);
+  }
+
+  private String serialize(Node node) throws IOException {
+    StringBuilder sb = new StringBuilder();
+    NodeList children = node.getChildNodes();
+    for (int i = 0; i < children.getLength(); i++) {
+      Node child = children.item(i);
+      new DefaultHtmlSerializer().serialize(child, sb);
+    }
+    return sb.toString();
+  }
+
+  private void addVariable(String key, JSONObject value) {
+    variables.put(key, value);
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/RepeatTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/RepeatTagHandlerTest.java
new file mode 100644
index 0000000..be18d05
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/RepeatTagHandlerTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.templates.FakeTemplateProcessor;
+import org.apache.shindig.gadgets.templates.tags.RepeatTagHandler;
+import org.apache.shindig.gadgets.templates.tags.TagHandler;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.DOMImplementation;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+public class RepeatTagHandlerTest {
+  private FakeTemplateProcessor processor;
+  private DOMImplementation documentProvider;
+  private TagHandler handler;
+
+  @Before
+  public void setUp() throws Exception {
+    processor = EasyMock.createMock(FakeTemplateProcessor.class);
+    documentProvider = new ParseModule.DOMImplementationProvider().get();
+    handler = new RepeatTagHandler();
+  }
+
+  @Test
+  public void repeat() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("repeat");
+    tag.setAttribute(RepeatTagHandler.EXPRESSION_ATTR, "fakeExpression");
+
+    List<String> mockList = ImmutableList.of("a", "b", "c");
+    processor.expressionResults = ImmutableMap.of("fakeExpression", mockList);
+
+    processor.processChildNodes(null, tag);
+    EasyMock.expectLastCall().times(3);
+
+    replay(processor);
+    handler.process(null, tag, processor);
+    verify(processor);
+  }
+
+  @Test
+  public void repeatWithoutExpression() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("repeat");
+
+    replay(processor);
+    handler.process(null, tag, processor);
+    verify(processor);
+  }
+
+  @Test
+  public void repeatWithIf() throws Exception {
+    Document doc = documentProvider.createDocument(null, null, null);
+    // Create a mock tag;  the name doesn't truly matter
+    Element tag = doc.createElement("repeat");
+    tag.setAttribute(RepeatTagHandler.EXPRESSION_ATTR, "fakeExpression");
+    tag.setAttribute(RepeatTagHandler.IF_ATTR, "fakeIf");
+
+    List<String> mockList = ImmutableList.of("a", "b", "c");
+    processor.expressionResults = ImmutableMap.of("fakeExpression", mockList,
+        // Return "false", "true", and "false" for each step
+        "fakeIf", Lists.newArrayList(false, true, false));
+
+    processor.processChildNodes(null, tag);
+    // "if" should evaluate to true only once
+    EasyMock.expectLastCall().times(1);
+
+    replay(processor);
+    handler.process(null, tag, processor);
+    verify(processor);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/TemplateBasedTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/TemplateBasedTagHandlerTest.java
new file mode 100644
index 0000000..6b70f27
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/TemplateBasedTagHandlerTest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import javax.el.ELResolver;
+
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.DefaultGuiceModule;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.admin.GadgetAdminModule;
+import org.apache.shindig.gadgets.oauth.OAuthModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2Module;
+import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerModule;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2PersistenceModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2MessageModule;
+import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
+import org.apache.shindig.gadgets.parse.GadgetHtmlParser;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.apache.shindig.gadgets.templates.TemplateContext;
+import org.apache.shindig.gadgets.templates.TemplateProcessor;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+/**
+ * Tests the behavior of template-based tag handlers.
+ */
+public class TemplateBasedTagHandlerTest {
+
+  private TemplateContext context;
+  private TemplateProcessor processor;
+  private final ELResolver resolver = new RootELResolver();
+  private GadgetHtmlParser parser;
+
+  private static final String TEST_NS = "http://example.com";
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(new GadgetAdminModule(), new DefaultGuiceModule(), new OAuthModule(), new OAuth2Module(), new PropertiesModule(), new OAuth2PersistenceModule(), new OAuth2MessageModule(), new OAuth2HandlerModule());
+    parser = injector.getInstance(GadgetHtmlParser.class);
+    processor = injector.getInstance(TemplateProcessor.class);
+    context = new TemplateContext(new Gadget(), null);
+  }
+
+  @Test
+  public void attributeInMy() throws Exception {
+    // Verify attribute EL retrieval
+    runTest("Bar",
+        "${My.attr}",
+        "<foo:Bar attr='Hello'/>", "Hello");
+  }
+
+  @Test
+  public void elementContentInMy() throws Exception {
+    // Verify element content EL retrieval
+    runTest("Bar",
+        "${My.element}",
+        "<foo:Bar><foo:element>Hello</foo:element></foo:Bar>", "Hello");
+  }
+
+  @Test
+  public void attrTakesPrecedenceInMy() throws Exception {
+    // Verify an attribute takes precedence over an element
+    runTest("Bar",
+        "${My.attr}",
+        "<foo:Bar attr='Hello'><foo:attr>Goodbye</foo:attr></foo:Bar>", "Hello");
+  }
+
+  @Test
+  public void elementAttributeInMy() throws Exception {
+    // Verify an attribute of an element is visible
+    runTest("Bar",
+        "${My.element.text}",
+        "<foo:Bar><foo:element text='Hello'/></foo:Bar>", "Hello");
+  }
+
+  @Test
+  public void descendantElementInMy() throws Exception {
+    // Verify the descendant of an element is visible
+    runTest("Bar",
+        "${My.element.child}",
+        "<foo:Bar><foo:element><foo:child>Hello</foo:child></foo:element></foo:Bar>", "Hello");
+  }
+
+  @Test
+  public void descendantElementNotFoundIfNotFullReference() throws Exception {
+    // Verify the descendant element isn't visible unless directly referenced
+    runTest("Bar",
+        "${My.child}",
+        "<foo:Bar><foo:element><foo:child>Hello</foo:child></foo:element></foo:Bar>", "");
+  }
+
+  @Test
+  @Ignore("The CajaHtmlParser doesn't allow unclosed xml tags.")
+  public void missingElementPropertyIsNull() throws Exception {
+    // Verify the descendant element isn't visible unless directly referenced
+    runTest("Bar",
+        "${My.element.foo == null}",
+        "<foo:Bar><foo:element>Hello/foo:element></foo:Bar>", "true");
+  }
+
+  @Test
+  @Ignore("This currently returns [Hello,Goodbye].  Check the spec, and consider changing the spec.")
+  public void multipleElementContentInMy() throws Exception {
+    // Verify element content EL retrieval is concatenation for multiple elements
+    runTest("Bar",
+        "${My.element}",
+        "<foo:Bar><foo:element>Hello</foo:element><foo:element>Goodbye</foo:element></foo:Bar>", "HelloGoodbye");
+  }
+
+  @Test
+  public void elementListRepeat() throws Exception {
+    // Verify a list of elements can be repeated over
+    runTest("Bar",
+        "<os:Repeat expression='${My.element}'>${text}</os:Repeat>",
+        "<foo:Bar><foo:element text='Hello'/><foo:element text='Goodbye'/></foo:Bar>", "HelloGoodbye");
+  }
+
+  @Test
+  public void singleElementRepeat() throws Exception {
+    // Verify a single element can be "repeated" over
+    runTest("Bar",
+        "<os:Repeat expression='${My.element}'>${text}</os:Repeat>",
+        "<foo:Bar><foo:element text='Hello'/></foo:Bar>", "Hello");
+  }
+
+  private void runTest(String tagName, String tagMarkup, String templateMarkup,
+      String expectedResult) throws GadgetException, IOException {
+    Element templateDef = parseTemplate(templateMarkup);
+    Element tagInstance = parseTemplate(tagMarkup);
+
+    templateDef.getOwnerDocument().adoptNode(tagInstance);
+    TagHandler tagHandler = new TemplateBasedTagHandler(tagInstance, TEST_NS, tagName);
+    TagRegistry reg = new DefaultTagRegistry(
+        ImmutableSet.of(tagHandler, new RepeatTagHandler()));
+
+    DocumentFragment result = processor.processTemplate(templateDef, context, resolver, reg);
+    String output = serialize(result);
+    assertEquals(expectedResult, output);
+  }
+
+  private Element parseTemplate(String markup) throws GadgetException {
+    String content = "<script type=\"text/os-template\" xmlns:foo=\"" + TEST_NS +
+        "\" xmlns:os=\"" + TagHandler.OPENSOCIAL_NAMESPACE + "\">" + markup + "</script>";
+    Document document = parser.parseDom(content);
+    return SocialDataTags.getTags(document, SocialDataTags.OSML_TEMPLATE_TAG).get(0);
+  }
+
+  private String serialize(Node node) throws IOException {
+    StringBuilder sb = new StringBuilder();
+    NodeList children = node.getChildNodes();
+    for (int i = 0; i < children.getLength(); i++) {
+      Node child = children.item(i);
+      new DefaultHtmlSerializer().serialize(child, sb);
+    }
+    return sb.toString();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/VarTagHandlerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/VarTagHandlerTest.java
new file mode 100644
index 0000000..7f6683f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/templates/tags/VarTagHandlerTest.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.templates.tags;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.expressions.RootELResolver;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.parse.DefaultHtmlSerializer;
+import org.apache.shindig.gadgets.parse.ParseModule;
+import org.apache.shindig.gadgets.parse.SocialDataTags;
+import org.apache.shindig.gadgets.parse.nekohtml.NekoSimplifiedHtmlParser;
+import org.apache.shindig.gadgets.templates.DefaultTemplateProcessor;
+import org.apache.shindig.gadgets.templates.TagRegistry;
+import org.apache.shindig.gadgets.templates.TemplateContext;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.w3c.dom.DocumentFragment;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import javax.el.ELResolver;
+
+public class VarTagHandlerTest {
+
+  private Expressions expressions;
+
+  private TemplateContext context;
+  private DefaultTemplateProcessor processor;
+  private Map<String, Object> variables;
+  private ELResolver resolver;
+  private TagRegistry registry;
+
+  private NekoSimplifiedHtmlParser parser;
+  protected Document result;
+
+  @Before
+  public void setUp() throws Exception {
+
+    expressions = Expressions.forTesting();
+    variables = Maps.newHashMap();
+    Set<TagHandler> handlers = ImmutableSet.<TagHandler> of(new VarTagHandler());
+    registry = new DefaultTagRegistry(handlers);
+
+    processor = new DefaultTemplateProcessor(expressions);
+    resolver = new RootELResolver();
+    parser = new NekoSimplifiedHtmlParser(new ParseModule.DOMImplementationProvider().get());
+    context = new TemplateContext(new Gadget(), variables);
+
+  }
+
+  @Test
+  public void testTag() throws Exception {
+    String output = executeTemplate("<os:Var key='myvar' value='3'></os:Var>The value of my var is ${myvar}",
+            "xmlns:os=\"http://ns.opensocial.org/2008/markup\"");
+    assertEquals("The value of my var is 3", output);
+  }
+
+  private String executeTemplate(String markup, String extra) throws Exception {
+    Element template = prepareTemplate(markup, extra);
+    DocumentFragment result = processor.processTemplate(template, context, resolver, registry);
+    return serialize(result);
+  }
+
+  private Element prepareTemplate(String markup, String extra) throws GadgetException {
+    String content = "<script type=\"text/os-template\"" + extra + '>' + markup + "</script>";
+    Document document = parser.parseDom(content);
+    return SocialDataTags.getTags(document, SocialDataTags.OSML_TEMPLATE_TAG).get(0);
+  }
+
+  private String serialize(Node node) throws IOException {
+    StringBuilder sb = new StringBuilder();
+    NodeList children = node.getChildNodes();
+    for (int i = 0; i < children.getLength(); i++) {
+      Node child = children.item(i);
+      new DefaultHtmlSerializer().serialize(child, sb);
+    }
+    return sb.toString();
+  }
+
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/AllJsIframeVersionerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/AllJsIframeVersionerTest.java
new file mode 100644
index 0000000..df8b8b5
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/AllJsIframeVersionerTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.util.HashUtil;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.Lists;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.util.List;
+
+public class AllJsIframeVersionerTest {
+  // Underscores that neither of these values are even read.
+  private static final Uri GADGET_URI = null;
+  private static final String CONTAINER = null;
+
+  private AllJsIframeVersioner versioner;
+  private String featureChecksum;
+
+  @Before
+  public void setUp() {
+    String featureContent = "THE_FEATURE_CONTENT";
+    String debugContent = "FEATURE_DEBUG_CONTENT";
+    String charset = Charset.defaultCharset().name();
+    MessageDigest digest = HashUtil.getMessageDigest();
+    try{
+      digest.update(featureContent.getBytes(charset));
+    } catch (UnsupportedEncodingException e) {
+      digest.update(featureContent.getBytes());
+    }
+    try{
+      digest.update(debugContent.getBytes(charset));
+    } catch (UnsupportedEncodingException e) {
+      digest.update(debugContent.getBytes());
+    }
+
+    featureChecksum = HashUtil.bytesToHex(digest.digest());
+    FeatureRegistry registry = createMock(FeatureRegistry.class);
+    FeatureResource resource = new FeatureResource.Simple(featureContent, debugContent, "js");
+    List<FeatureResource> allResources = Lists.newArrayList(resource);
+    final FeatureRegistry.LookupResult lr = createMock(FeatureRegistry.LookupResult.class);
+    expect(lr.getResources()).andReturn(allResources);
+    replay(lr);
+    expect(registry.getAllFeatures()).andReturn(lr).once();
+    replay(registry);
+    versioner = new AllJsIframeVersioner(registry);
+    verify(registry);
+  }
+
+  @Test
+  public void versionIsAsExpectedAlwaysTheSame() {
+    assertEquals(featureChecksum, versioner.version(GADGET_URI, CONTAINER));
+    assertEquals(featureChecksum, versioner.version(Uri.parse("http://valid.com/"), "foo"));
+  }
+
+  @Test
+  public void validateNull() {
+    assertEquals(UriStatus.VALID_UNVERSIONED, versioner.validate(GADGET_URI, CONTAINER, null));
+  }
+
+  @Test
+  public void validateEmpty() {
+    assertEquals(UriStatus.VALID_UNVERSIONED, versioner.validate(GADGET_URI, CONTAINER, ""));
+  }
+
+  @Test
+  public void validateMismatch() {
+    assertEquals(UriStatus.INVALID_VERSION, versioner.validate(GADGET_URI, CONTAINER,
+        featureChecksum + "-not"));
+  }
+
+  @Test
+  public void validateMatch() {
+    assertEquals(UriStatus.VALID_VERSIONED, versioner.validate(GADGET_URI, CONTAINER,
+        featureChecksum));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultAccelUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultAccelUriManagerTest.java
new file mode 100644
index 0000000..138f3fb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultAccelUriManagerTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for DefaultAccelUriManager.
+ */
+public class DefaultAccelUriManagerTest {
+  DefaultAccelUriManager uriManager;
+  private ContainerConfig config;
+
+  private Map<String, Object> makeConfig(String name, String path) {
+    return ImmutableMap
+        .<String, Object>builder()
+        .put(ContainerConfig.CONTAINER_KEY, name)
+        .put(AccelUriManager.PROXY_HOST_PARAM, "apache.org")
+        .put(AccelUriManager.PROXY_PATH_PARAM, path)
+        .build();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(makeConfig("default", "/gadgets/proxy"))
+        .addContainer(makeConfig("accel", "/gadgets/accel"))
+        .commit();
+    uriManager = new DefaultAccelUriManager(config, new DefaultProxyUriManager(
+        config, null));
+  }
+
+  @Test
+  public void testParseAndNormalizeNonAccelUri() throws Exception {
+    Uri uri = Uri.parse("http://www.example.org/index.html");
+    HttpRequest req = new HttpRequest(uri);
+    assertEquals(Uri.parse("//apache.org/gadgets/accel?container=accel"
+                 + "&gadget=http%3A%2F%2Fwww.example.org%2Findex.html"
+                 + "&debug=0&nocache=0&refresh=0"
+                 + "&rooe=1&url=http%3A%2F%2Fwww.example.org%2Findex.html"),
+                 uriManager.parseAndNormalize(req));
+
+    uri = Uri.parse("http://www.example.org/index.html");
+    req = new HttpRequest(uri);
+    req.setContainer("accel");
+    assertEquals(Uri.parse("//apache.org/gadgets/accel?container=accel"
+                 + "&gadget=http%3A%2F%2Fwww.example.org%2Findex.html"
+                 + "&debug=0&nocache=0&refresh=0"
+                 + "&rooe=1&url=http%3A%2F%2Fwww.example.org%2Findex.html"),
+                 uriManager.parseAndNormalize(req));
+  }
+
+  @Test
+  public void testParseAndNormalizeAccelUri() throws Exception {
+    Uri uri = Uri.parse("http://apache.org/gadgets/accel?container=accel"
+                        + "&gadget=http%3A%2F%2Fwww.1.com%2Fa.html"
+                        + "&url=http%3A%2F%2Fwww.example.org%2Findex.html");
+    HttpRequest req = new HttpRequest(uri);
+    assertEquals(Uri.parse("//apache.org/gadgets/accel?container=accel"
+                 + "&gadget=http%3A%2F%2Fwww.1.com%2Fa.html"
+                 + "&debug=0&nocache=0&refresh=0"
+                 + "&url=http%3A%2F%2Fwww.example.org%2Findex.html"),
+                 uriManager.parseAndNormalize(req));
+  }
+
+  @Test
+  public void testLooksLikeAccelUri() throws Exception {
+    Uri uri = Uri.parse("http://apache.org/gadgets/accel?container=accel"
+                        + "&gadget=http%3A%2F%2Fwww.1.com%2Fa.html"
+                        + "&url=http%3A%2F%2Fwww.example.org%2Findex.html");
+    assertTrue(uriManager.looksLikeAccelUri(uri));
+
+    uri = Uri.parse("http://www.example.org/index.html");
+    assertFalse(uriManager.looksLikeAccelUri(uri));
+  }
+
+  @Test
+  public void testContainersChange() throws Exception {
+    String beforeUrl = "//apache.org/gadgets/accel?container=accel"
+        + "&gadget=http%3A%2F%2Fwww.example.org%2Findex.html"
+        + "&debug=0&nocache=0&refresh=0&rooe=1&url=http%3A%2F%2Fwww.example.org%2Findex.html";
+    String afterUrl = "//apache.org/random/url?container=accel"
+        + "&gadget=http%3A%2F%2Fwww.example.org%2Findex.html"
+        + "&debug=0&nocache=0&refresh=0&rooe=1&url=http%3A%2F%2Fwww.example.org%2Findex.html";
+
+    Uri uri = Uri.parse("http://www.example.org/index.html");
+    HttpRequest req = new HttpRequest(uri);
+    req.setContainer("accel");
+    assertEquals(Uri.parse(beforeUrl), uriManager.parseAndNormalize(req));
+    assertTrue(uriManager.looksLikeAccelUri(Uri.parse(beforeUrl)));
+    assertFalse(uriManager.looksLikeAccelUri(Uri.parse(afterUrl)));
+
+    config.newTransaction().addContainer(makeConfig("accel", "/random/url")).commit();
+
+    uri = Uri.parse("http://www.example.org/index.html");
+    req = new HttpRequest(uri);
+    req.setContainer("accel");
+    assertEquals(Uri.parse(afterUrl), uriManager.parseAndNormalize(req));
+    assertFalse(uriManager.looksLikeAccelUri(Uri.parse(beforeUrl)));
+    assertTrue(uriManager.looksLikeAccelUri(Uri.parse(afterUrl)));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultConcatUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultConcatUriManagerTest.java
new file mode 100644
index 0000000..7162f8e
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultConcatUriManagerTest.java
@@ -0,0 +1,604 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.apache.shindig.gadgets.uri.ConcatUriManager.ConcatUri.fromList;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.commons.lang3.StringEscapeUtils;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.uri.ConcatUriManager.ConcatData;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.HashMap;
+
+public class DefaultConcatUriManagerTest extends UriManagerTestBase {
+  private static final Uri RESOURCE_1 = Uri.parse("http://example.com/one.dat");
+  private static final Uri RESOURCE_2 = Uri.parse("http://gadgets.com/two.dat");
+  private static final Uri RESOURCE_3_NOSCHEMA = Uri.parse("//foobar.com/three.dat");
+  private static final Uri RESOURCE_3_HTTP = Uri.parse("http://foobar.com/three.dat");
+  private static final List<Uri> RESOURCES_ONE =
+      ImmutableList.of(RESOURCE_1, RESOURCE_2, RESOURCE_3_HTTP);
+  private static final List<Uri> RESOURCES_TWO =
+      ImmutableList.of(RESOURCE_3_HTTP, RESOURCE_2, RESOURCE_1);
+
+  @Test
+  public void typeCssBasicParams() throws Exception {
+    checkBasicParams(ConcatUriManager.Type.CSS);
+  }
+
+  @Test
+  public void typeCssAltParams() throws Exception {
+    checkAltParams(ConcatUriManager.Type.CSS);
+  }
+
+  @Test
+  public void typeCssBatch() throws Exception {
+    checkBatchAdjacent(ConcatUriManager.Type.CSS);
+  }
+
+  @Test
+  public void typeCssValidatedGeneratedBatch() throws Exception {
+    checkValidatedBatchAdjacent(ConcatUriManager.Type.CSS);
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void typeCssMissingHostConfig() throws Exception {
+    checkMissingHostConfig(ConcatUriManager.Type.CSS);
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void typeCssMissingPathConfig() throws Exception {
+    checkMissingPathConfig(ConcatUriManager.Type.CSS);
+  }
+
+  @Test(expected = UnsupportedOperationException.class)
+  public void typeCssSplitNotSupported() throws Exception {
+    // Unique to type=CSS, split isn't supported.
+    Gadget gadget = mockGadget(false, false);
+    DefaultConcatUriManager manager = makeManager("host.com", "/foo", "token", null);
+    List<List<Uri>> resourceUris = ImmutableList.of(RESOURCES_ONE);
+    manager.make(fromList(gadget, resourceUris, ConcatUriManager.Type.CSS), false);
+  }
+
+  @Test
+  public void typeJsBasicParams() throws Exception {
+    checkBasicParams(ConcatUriManager.Type.JS);
+  }
+
+  @Test
+  public void typeJsAltParams() throws Exception {
+    checkAltParams(ConcatUriManager.Type.JS);
+  }
+
+  @Test
+  public void typeJsBatchAdjacent() throws Exception {
+    checkBatchAdjacent(ConcatUriManager.Type.JS);
+  }
+
+  @Test
+  public void typeJsBatchSplitBatched() throws Exception {
+    // Unique to type=JS, split is supported.
+    Gadget gadget = mockGadget(false, false);
+    String host = "host.com";
+    String path = "/concat/path";
+    ConcatUriManager.Type type = ConcatUriManager.Type.JS;
+    String splitParam = "token";
+    String[] versions = new String[] { "version1" };
+    ConcatUriManager.Versioner versioner = makeVersioner(null, versions);
+    DefaultConcatUriManager manager = makeManager(host, path, splitParam, versioner);
+    List<List<Uri>> resourceUris =
+        ImmutableList.of(RESOURCES_ONE, RESOURCES_TWO, RESOURCES_ONE);
+    HashMap<String, String> jsonParams = new HashMap<String, String>();
+    List<ConcatData> concatUris =
+        manager.make(fromList(gadget, resourceUris, type), false);
+    assertEquals(3, concatUris.size());
+
+    for (int i = 0; i < 3; ++i) {
+      ConcatData uri = concatUris.get(i);
+      assertEquals(1, uri.getUris().size());
+      String json = uri.getUris().get(0).getQueryParameter(
+          (Param.JSON.toString().toLowerCase()));
+      assertTrue(json.startsWith(splitParam));
+      String currentUri = uri.getUris().get(0).toString();
+      if (jsonParams.keySet().contains(currentUri)) {
+        assertEquals(jsonParams.get(currentUri), json);
+      } else {
+        jsonParams.put(currentUri, json);
+      }
+
+      assertEquals(DefaultConcatUriManager.getJsSnippet(json, RESOURCE_1),
+          uri.getSnippet(RESOURCE_1));
+      assertEquals(DefaultConcatUriManager.getJsSnippet(json, RESOURCE_2),
+          uri.getSnippet(RESOURCE_2));
+      assertNull(uri.getSnippet(RESOURCE_3_NOSCHEMA));
+      assertEquals(DefaultConcatUriManager.getJsSnippet(json, RESOURCE_3_HTTP),
+          uri.getSnippet(RESOURCE_3_HTTP));
+      checkBasicUriParameters(uri.getUris().get(0), host, path, 10, type, "0", "0", versions[0]);
+      List<Uri> resList = (i % 2 == 0) ? RESOURCES_ONE : RESOURCES_TWO;
+      assertEquals(resList.get(0).toString(), uri.getUris().get(0).getQueryParameter("1"));
+      assertEquals(resList.get(1).toString(), uri.getUris().get(0).getQueryParameter("2"));
+      assertEquals(resList.get(2).toString(), uri.getUris().get(0).getQueryParameter("3"));
+    }
+    assertEquals(2, jsonParams.size());
+  }
+
+  @Test
+  public void typeJsValidatedGeneratedBatch() throws Exception {
+    checkValidatedBatchAdjacent(ConcatUriManager.Type.JS);
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void typeJsMissingHostConfig() throws Exception {
+    Gadget gadget = mockGadget(false, false);
+    DefaultConcatUriManager manager = makeManager(null, "/foo", "token", null);
+    List<List<Uri>> resourceUris = ImmutableList.<List<Uri>>of(ImmutableList.of(RESOURCE_1));
+    manager.make(fromList(gadget, resourceUris, ConcatUriManager.Type.JS), false);
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void typeJsMissingPathConfig() throws Exception {
+    Gadget gadget = mockGadget(false, false);
+    DefaultConcatUriManager manager = makeManager("host.com", null, "token", null);
+    List<List<Uri>> resourceUris = ImmutableList.<List<Uri>>of(ImmutableList.of(RESOURCE_1));
+    manager.make(fromList(gadget, resourceUris, ConcatUriManager.Type.JS), false);
+  }
+
+  @Test
+  public void typeJsMissingSplitTokenConfig() throws Exception {
+    Gadget gadget = mockGadget(false, false);
+    DefaultConcatUriManager manager = makeManager("host.com", "/foo", null, null);
+    List<List<Uri>> resourceUris = ImmutableList.<List<Uri>>of(ImmutableList.of(RESOURCE_1));
+    List<ConcatData> concatUris = manager.make(fromList(gadget, resourceUris, ConcatUriManager.Type.JS), false);
+    assertEquals(1, concatUris.size());
+    assertEquals(1, concatUris.get(0).getUris().size());
+    assertNull(concatUris.get(0).getUris().get(0).getQueryParameter(Param.JSON.getKey()));
+  }
+
+  @Test
+  public void jsEvalSnippet() {
+    assertEquals("eval(_js['" + StringEscapeUtils.escapeEcmaScript(RESOURCE_1.toString()) + "']);",
+        DefaultConcatUriManager.getJsSnippet("_js", RESOURCE_1));
+  }
+
+  @Test
+  public void validateNoContainerStrict() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    manager.setUseStrictParsing(true);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(Uri.parse("http://host.com/path?q=f"));
+    assertEquals(UriStatus.BAD_URI, validated.getStatus());
+  }
+
+  @Test
+  public void validateNoContainer() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(Uri.parse("http://host.com/path?q=f"));
+    assertEquals(UriStatus.BAD_URI, validated.getStatus());
+  }
+
+  @Test
+  public void validateHostMismatchStrict() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    manager.setUseStrictParsing(true);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(Uri.parse("http://another.com/path?" +
+            Param.CONTAINER.getKey() + '=' + CONTAINER + "&type=css"));
+    assertEquals(UriStatus.BAD_URI, validated.getStatus());
+  }
+
+  @Test
+  public void validatePathMismatchStrict() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    manager.setUseStrictParsing(true);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(Uri.parse("http://host.com/another?" +
+            Param.CONTAINER.getKey() + '=' + CONTAINER + "&type=css"));
+    assertEquals(UriStatus.BAD_URI, validated.getStatus());
+  }
+
+  @Test
+  public void validateInvalidChildUri() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(
+          Uri.parse("http://host.com/path?" + Param.CONTAINER.getKey() + '=' + CONTAINER +
+            "&1=!!!"));
+    assertEquals(UriStatus.BAD_URI, validated.getStatus());
+  }
+
+  @Test
+  public void validateNullTypeUri() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(
+          Uri.parse("http://host.com/path?" + Param.CONTAINER.getKey() + '=' + CONTAINER +
+            "&1=http://legit.com/1.dat"));
+    assertEquals(UriStatus.BAD_URI, validated.getStatus());
+  }
+
+  @Test
+  public void validateBadTypeUri() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(
+          Uri.parse("http://host.com/path?" + Param.CONTAINER.getKey() + '=' + CONTAINER +
+            "&1=http://legit.com/1.dat&" + Param.TYPE.getKey() + "=NOTATYPE"));
+    assertEquals(UriStatus.BAD_URI, validated.getStatus());
+  }
+
+  @Test
+  public void validateOldStyleTypeUri() {
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, null);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(
+          Uri.parse("http://host.com/path?" + Param.CONTAINER.getKey() + '=' + CONTAINER +
+            "&1=http://legit.com/1.dat&" + Param.TYPE.getKey() + "=NOTATYPE&rewriteMime=text/css"));
+    assertEquals(UriStatus.VALID_UNVERSIONED, validated.getStatus());
+    assertEquals(ConcatUriManager.Type.CSS, validated.getType());
+  }
+
+  @Test
+  public void validateCssUriUnversioned() {
+    checkUnversionedUri(ConcatUriManager.Type.CSS, false);
+  }
+
+  @Test
+  public void validateCssUriVersioned() {
+    checkValidateUri(UriStatus.VALID_VERSIONED, ConcatUriManager.Type.CSS, false);
+  }
+
+  @Test
+  public void validateCssUriBadVersion() {
+    checkValidateUri(UriStatus.INVALID_VERSION, ConcatUriManager.Type.CSS, false);
+  }
+
+  @Test
+  public void validateJsUriUnversioned() {
+    checkUnversionedUri(ConcatUriManager.Type.JS, true);
+  }
+
+  @Test
+  public void validateJsUriVersioned() {
+    checkValidateUri(UriStatus.VALID_VERSIONED, ConcatUriManager.Type.JS, true);
+  }
+
+  @Test
+  public void validateJsUriBadVersion() {
+    checkValidateUri(UriStatus.INVALID_VERSION, ConcatUriManager.Type.JS, true);
+  }
+
+  @Test
+  public void dontConcatMultipleResourceBeyoundUrlLimitSplitBatched() throws Exception {
+    Gadget gadget = mockGadget(true, true);
+    String host = "bar.com";
+    String path = "/other/path";
+    ConcatUriManager.Type type = ConcatUriManager.Type.JS;
+
+    String[] versions = new String[] { "v1" };
+    ConcatUriManager.Versioner versioner = makeVersioner(null, versions);
+    DefaultConcatUriManager manager = makeManager(host, path, "token", versioner);
+
+    Uri urlA = Uri.parse(generateUrl(manager.getUrlMaxLength() / 4));
+    Uri urlB = Uri.parse(generateUrl(manager.getUrlMaxLength() / 4));
+    Uri urlC = Uri.parse(generateUrl(manager.getUrlMaxLength() / 2));
+
+    List<Uri> urlList = ImmutableList.of(urlA, urlB, urlC);
+    List<List<Uri>> resourceUris = ImmutableList.of(urlList);
+    List<ConcatData> concatUris =
+        manager.make(fromList(gadget, resourceUris, type), false);
+
+    assertEquals(2, concatUris.get(0).getUris().size());
+
+    String jsonFirst = concatUris.get(0).getUris().get(0).getQueryParameter(
+        (Param.JSON.toString().toLowerCase()));
+    checkBasicUriParameters(concatUris.get(0).getUris().get(0), host, path, 9, type,
+        "1", "1", versions[0]);
+    assertEquals(urlA.toString(), concatUris.get(0).getUris().get(0).getQueryParameter("1"));
+    assertEquals(DefaultConcatUriManager.getJsSnippet(jsonFirst, urlA),
+        concatUris.get(0).getSnippet(urlA));
+    assertEquals(urlB.toString(), concatUris.get(0).getUris().get(0).getQueryParameter("2"));
+    assertEquals(DefaultConcatUriManager.getJsSnippet(jsonFirst, urlB),
+        concatUris.get(0).getSnippet(urlB));
+    assertNull(concatUris.get(0).getUris().get(0).getQueryParameter("3"));
+
+    String jsonSecond = concatUris.get(0).getUris().get(1).getQueryParameter(
+            (Param.JSON.toString().toLowerCase()));
+    checkBasicUriParameters(concatUris.get(0).getUris().get(1), host, path, 8, type,
+        "1", "1", versions[0]);
+    assertEquals(urlC.toString(), concatUris.get(0).getUris().get(1).getQueryParameter("1"));
+    assertEquals(DefaultConcatUriManager.getJsSnippet(jsonSecond, urlC),
+        concatUris.get(0).getSnippet(urlC));
+    assertNull(concatUris.get(0).getUris().get(1).getQueryParameter("2"));
+
+  }
+
+  @Test
+  public void dontConcatMultipleResourceBeyoundUrlLimitAdjacentBatched() throws Exception {
+    Gadget gadget = mockGadget(true, true);
+    String host = "bar.com";
+    String path = "/other/path";
+    ConcatUriManager.Type type = ConcatUriManager.Type.JS;
+
+    String[] versions = new String[] { "v1" };
+    ConcatUriManager.Versioner versioner = makeVersioner(null, versions);
+    DefaultConcatUriManager manager = makeManager(host, path, null, versioner);
+
+    Uri urlA = Uri.parse(generateUrl(manager.getUrlMaxLength() / 4));
+    Uri urlB = Uri.parse(generateUrl(manager.getUrlMaxLength() / 4));
+    Uri urlC = Uri.parse(generateUrl(manager.getUrlMaxLength() / 2));
+
+    List<Uri> urlList = ImmutableList.of(urlA, urlB, urlC);
+    List<List<Uri>> resourceUris = ImmutableList.of(urlList);
+    List<ConcatData> concatUris =
+        manager.make(fromList(gadget, resourceUris, type), true);
+
+    assertEquals(2, concatUris.get(0).getUris().size());
+
+    checkBasicUriParameters(concatUris.get(0).getUris().get(0), host, path, 8, type,
+        "1", "1", versions[0]);
+    assertEquals(urlA.toString(), concatUris.get(0).getUris().get(0).getQueryParameter("1"));
+    assertNull(concatUris.get(0).getSnippet(urlA));
+    assertEquals(urlB.toString(), concatUris.get(0).getUris().get(0).getQueryParameter("2"));
+    assertNull(concatUris.get(0).getSnippet(urlB));
+    assertNull(concatUris.get(0).getUris().get(0).getQueryParameter("3"));
+
+    checkBasicUriParameters(concatUris.get(0).getUris().get(1), host, path, 7, type,
+        "1", "1", versions[0]);
+    assertEquals(urlC.toString(), concatUris.get(0).getUris().get(1).getQueryParameter("1"));
+    assertNull(concatUris.get(0).getSnippet(urlC));
+    assertNull(concatUris.get(0).getUris().get(1).getQueryParameter("2"));
+  }
+
+  /**
+   * Contains Uri Basic Assert Checks
+   */
+  private void checkBasicUriParameters(Uri uri,
+                                       String host,
+                                       String path,
+                                       int queryParameterSize,
+                                       ConcatUriManager.Type type,
+                                       String debug,
+                                       String noCache) {
+    assertNull(uri.getScheme());
+    assertEquals(host, uri.getAuthority());
+    assertEquals(path, uri.getPath());
+    assertEquals(queryParameterSize, uri.getQueryParameters().size());
+    assertEquals(CONTAINER, uri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(SPEC_URI.toString(), uri.getQueryParameter(Param.GADGET.getKey()));
+    assertEquals(type.getType(), uri.getQueryParameter(Param.TYPE.getKey()));
+    assertEquals(debug, uri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(noCache, uri.getQueryParameter(Param.NO_CACHE.getKey()));
+  }
+
+  /**
+   * Contains Uri Basic Assert Checks
+   */
+  private void checkBasicUriParameters(Uri uri,
+                                       String host,
+                                       String path,
+                                       int queryParameterSize,
+                                       ConcatUriManager.Type type,
+                                       String debug,
+                                       String noCache,
+                                       String version) {
+    checkBasicUriParameters(uri, host, path, queryParameterSize, type, debug, noCache);
+    assertEquals(version, uri.getQueryParameter(Param.VERSION.getKey()));
+  }
+
+  private void checkUnversionedUri(ConcatUriManager.Type type, boolean hasSplit) {
+    // Returns VALID_VERSIONED, but no version is present.
+    ConcatUriManager.Versioner versioner = makeVersioner(UriStatus.VALID_VERSIONED);
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, versioner);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(
+          Uri.parse("http://host.com/path?" + Param.CONTAINER.getKey() + '=' + CONTAINER +
+            "&1=http://legit.com/1.dat&2=http://another.com/2.dat&" + Param.TYPE.getKey() +
+              '=' + type.getType() + '&' + Param.JSON.getKey() +
+            "=split&" + Param.GADGET.getKey() + "=http://www.gadget.com/g.xml&" +
+            Param.REFRESH.getKey() + "=123"));
+    assertEquals(UriStatus.VALID_UNVERSIONED, validated.getStatus());
+    assertEquals(type, validated.getType());
+    assertEquals(CONTAINER, validated.getContainer());
+    assertEquals("http://www.gadget.com/g.xml", validated.getGadget());
+    assertEquals(2, validated.getBatch().size());
+    assertEquals("http://legit.com/1.dat", validated.getBatch().get(0).toString());
+    assertEquals("http://another.com/2.dat", validated.getBatch().get(1).toString());
+    assertEquals(123, validated.getRefresh().intValue());
+    assertEquals(hasSplit ? "split" : null, validated.getSplitParam());
+  }
+
+  private void checkValidateUri(UriStatus status, ConcatUriManager.Type type, boolean hasSplit) {
+    ConcatUriManager.Versioner versioner = makeVersioner(status);
+    DefaultConcatUriManager manager = makeManager("host.com", "/path", null, versioner);
+    ConcatUriManager.ConcatUri validated =
+        manager.process(
+          Uri.parse("http://host.com/path?" + Param.CONTAINER.getKey() + '=' + CONTAINER +
+            "&1=http://legit.com/1.dat&2=http://another.com/2.dat&" + Param.TYPE.getKey() + '='
+            + type.getType() + '&' + Param.VERSION.getKey() + "=something&" + Param.JSON.getKey() +
+            "=split&" + Param.GADGET.getKey() + "=http://www.gadget.com/g.xml&" +
+            Param.REFRESH.getKey() + "=123"));
+    assertEquals(status, validated.getStatus());
+    assertEquals(type, validated.getType());
+    assertEquals(CONTAINER, validated.getContainer());
+    assertEquals("http://www.gadget.com/g.xml", validated.getGadget());
+    assertEquals(2, validated.getBatch().size());
+    assertEquals("http://legit.com/1.dat", validated.getBatch().get(0).toString());
+    assertEquals("http://another.com/2.dat", validated.getBatch().get(1).toString());
+    assertEquals(123, validated.getRefresh().intValue());
+    assertEquals(hasSplit ? "split" : null, validated.getSplitParam());
+  }
+
+  private void checkBasicParams(ConcatUriManager.Type type) throws Exception {
+    Gadget gadget = mockGadget(false, false);
+    String host = "host.com";
+    String path = "/concat/path";
+    DefaultConcatUriManager manager = makeManager(host, path, "token", null);
+    List<List<Uri>> resourceUris = ImmutableList.of(RESOURCES_ONE);
+
+    List<ConcatData> concatUris =
+      manager.make(fromList(gadget, resourceUris, type), true);
+    assertEquals(1, concatUris.size());
+
+    ConcatData uri = concatUris.get(0);
+    assertNull(uri.getSnippet(RESOURCE_1));
+    assertNull(uri.getSnippet(RESOURCE_2));
+    assertNull(uri.getSnippet(RESOURCE_3_NOSCHEMA));
+    assertEquals(1, uri.getUris().size());
+    checkBasicUriParameters(uri.getUris().get(0), host, path, 8, type, "0", "0");
+    assertEquals(RESOURCES_ONE.get(0).toString(), uri.getUris().get(0).getQueryParameter("1"));
+    assertEquals(RESOURCES_ONE.get(1).toString(), uri.getUris().get(0).getQueryParameter("2"));
+    assertEquals(RESOURCES_ONE.get(2).toString(), uri.getUris().get(0).getQueryParameter("3"));
+  }
+
+  private void checkAltParams(ConcatUriManager.Type type) throws Exception {
+    Gadget gadget = mockGadget(true, true);
+    String host = "bar.com";
+    String path = "/other/path";
+    String version = "version";
+    ConcatUriManager.Versioner versioner = makeVersioner(null, version);
+    DefaultConcatUriManager manager = makeManager(host, path, "token", versioner);
+    List<List<Uri>> resourceUris = ImmutableList.of(RESOURCES_ONE);
+
+    List<ConcatData> concatUris =
+      manager.make(fromList(gadget, resourceUris, type), true);
+    assertEquals(1, concatUris.size());
+
+    ConcatData uri = concatUris.get(0);
+    assertNull(uri.getSnippet(RESOURCE_1));
+    assertNull(uri.getSnippet(RESOURCE_2));
+    assertNull(uri.getSnippet(RESOURCE_3_NOSCHEMA));
+    assertEquals(1, uri.getUris().size());
+    checkBasicUriParameters(uri.getUris().get(0), host, path, 9, type, "1", "1", version);
+    assertEquals(RESOURCES_ONE.get(0).toString(), uri.getUris().get(0).getQueryParameter("1"));
+    assertEquals(RESOURCES_ONE.get(1).toString(), uri.getUris().get(0).getQueryParameter("2"));
+    assertEquals(RESOURCES_ONE.get(2).toString(), uri.getUris().get(0).getQueryParameter("3"));
+  }
+
+  private void checkBatchAdjacent(ConcatUriManager.Type type) throws Exception {
+    Gadget gadget = mockGadget(true, true);
+    String host = "bar.com";
+    String path = "/other/path";
+    String[] versions = new String[] { "version1"};
+    ConcatUriManager.Versioner versioner = makeVersioner(null, versions);
+    DefaultConcatUriManager manager = makeManager(host, path, "token", versioner);
+    List<List<Uri>> resourceUris =
+        ImmutableList.of(RESOURCES_ONE, RESOURCES_TWO, RESOURCES_ONE);
+
+    List<ConcatData> concatUris =
+      manager.make(fromList(gadget, resourceUris, type), true);
+    assertEquals(3, concatUris.size());
+
+    for (int i = 0; i < 3; ++i) {
+      ConcatData uri = concatUris.get(i);
+      assertNull(uri.getSnippet(RESOURCE_1));
+      assertNull(uri.getSnippet(RESOURCE_2));
+      assertNull(uri.getSnippet(RESOURCE_3_NOSCHEMA));
+      assertEquals(1, uri.getUris().size());
+      checkBasicUriParameters(uri.getUris().get(0), host, path, 9, type, "1", "1", versions[0]);
+      List<Uri> resList = (i % 2 == 0) ? RESOURCES_ONE : RESOURCES_TWO;
+      assertEquals(resList.get(0).toString(), uri.getUris().get(0).getQueryParameter("1"));
+      assertEquals(resList.get(1).toString(), uri.getUris().get(0).getQueryParameter("2"));
+      assertEquals(resList.get(2).toString(), uri.getUris().get(0).getQueryParameter("3"));
+    }
+  }
+
+  private void checkValidatedBatchAdjacent(ConcatUriManager.Type type) throws Exception {
+    // This is essentially the "integration" test ensuring that a
+    // DefaultConcatUriManager's created Uris can be validated by it in turn.
+    Gadget gadget = mockGadget(true, true);
+    String host = "bar.com";
+    String path = "/other/path";
+    String[] versions = new String[] { "version1"};
+    ConcatUriManager.Versioner versioner = makeVersioner(UriStatus.VALID_VERSIONED, versions);
+    DefaultConcatUriManager manager = makeManager(host, path, "token", versioner);
+    List<List<Uri>> resourceUris =
+        ImmutableList.of(RESOURCES_ONE, RESOURCES_TWO, RESOURCES_ONE);
+
+    List<ConcatData> concatUris =
+        manager.make(fromList(gadget, resourceUris, type), true);
+    assertEquals(3, concatUris.size());
+
+    for (int i = 0; i < 3; ++i) {
+      ConcatUriManager.ConcatUri validated =
+          manager.process(concatUris.get(i).getUris().get(0));
+      assertEquals(UriStatus.VALID_VERSIONED, validated.getStatus());
+      List<Uri> resList = (i % 2 == 0) ? RESOURCES_ONE : RESOURCES_TWO;
+      assertEquals(resList, validated.getBatch());
+      assertEquals(type, validated.getType());
+    }
+  }
+
+  private String generateUrl(int length) {
+    return "http://www.url.com/" + RandomStringUtils.randomAlphanumeric(length - 22) + ".js";
+  }
+
+  private void checkMissingHostConfig(ConcatUriManager.Type type) throws Exception {
+    Gadget gadget = mockGadget(false, false);
+    DefaultConcatUriManager manager = makeManager(null, "/foo", "token", null);
+    List<List<Uri>> resourceUris = ImmutableList.of(RESOURCES_ONE);
+    manager.make(fromList(gadget, resourceUris, type), true);
+  }
+
+  private void checkMissingPathConfig(ConcatUriManager.Type type) throws Exception {
+    Gadget gadget = mockGadget(false, false);
+    DefaultConcatUriManager manager = makeManager("host.com", null, "token", null);
+    List<List<Uri>> resourceUris = ImmutableList.of(RESOURCES_ONE);
+    manager.make(fromList(gadget, resourceUris, type), false);
+  }
+
+  private DefaultConcatUriManager makeManager(String host, String path, String splitToken,
+      ConcatUriManager.Versioner versioner) {
+    ContainerConfig config = createMock(ContainerConfig.class);
+    expect(config.getString(CONTAINER, DefaultConcatUriManager.CONCAT_HOST_PARAM))
+        .andReturn(host).anyTimes();
+    expect(config.getString(CONTAINER, DefaultConcatUriManager.CONCAT_PATH_PARAM))
+        .andReturn(path).anyTimes();
+    expect(config.getString(CONTAINER, DefaultConcatUriManager.CONCAT_JS_SPLIT_PARAM))
+        .andReturn(splitToken).anyTimes();
+    replay(config);
+    return new DefaultConcatUriManager(config, versioner);
+  }
+
+  @SuppressWarnings("unchecked")
+  private ConcatUriManager.Versioner makeVersioner(UriStatus status, String... versions) {
+    ConcatUriManager.Versioner versioner = createMock(ConcatUriManager.Versioner.class);
+    expect(versioner.version(isA(List.class), eq(CONTAINER), isA(List.class)))
+        .andReturn(ImmutableList.copyOf(versions)).anyTimes();
+    expect(versioner.validate(isA(List.class), eq(CONTAINER), isA(String.class)))
+        .andReturn(status).anyTimes();
+    replay(versioner);
+    return versioner;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultIframeUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultIframeUriManagerTest.java
new file mode 100644
index 0000000..dfd1efb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultIframeUriManagerTest.java
@@ -0,0 +1,1118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.IFRAME_BASE_PATH_KEY;
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.LOCKED_DOMAIN_FEATURE_NAME;
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.LOCKED_DOMAIN_REQUIRED_KEY;
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.LOCKED_DOMAIN_SUFFIX_KEY;
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.SECURITY_TOKEN_ALWAYS_KEY;
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.SECURITY_TOKEN_FEATURE_NAME;
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.UNLOCKED_DOMAIN_KEY;
+import static org.apache.shindig.gadgets.uri.DefaultIframeUriManager.tplKey;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.shindig.auth.BasicSecurityTokenCodec;
+import org.apache.shindig.auth.SecurityTokenCodec;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.BasicContainerConfig;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.ContainerConfigException;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.HashLockedDomainService;
+import org.apache.shindig.gadgets.LockedDomainService;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class DefaultIframeUriManagerTest extends UriManagerTestBase {
+  private static final String LD_PREFIX = "locked";
+  private static final String IFRAME_PATH = "/gadgets/ifr";
+  private static final String JS_HOST = "//server.com";
+  private static final String JS_PATH = "/gadgets/js";
+  private static final String LD_SUFFIX = ".lockeddomain.com";
+  private static final String LD_SUFFIX_ALT = ".altld.com";
+  private static final String UNLOCKED_DOMAIN = "unlockeddomain.com";
+  private static final String UNLOCKED_DOMAIN_CONFIG_VALUE = UNLOCKED_DOMAIN;
+  private static final int TYPE_URL_NUM_BASE_PARAMS = 8;
+  private static final int TYPE_HTML_NUM_BASE_PARAMS = 8;
+
+  private static final LockedDomainPrefixGenerator prefixGen = new LockedDomainPrefixGenerator() {
+    public String getLockedDomainPrefix(Uri gadgetUri) {
+      return LD_PREFIX;
+    }
+
+    public String getLockedDomainPrefix(String token) {
+      // TODO Auto-generated method stub
+      return LD_PREFIX;
+    }
+  };
+
+  private static final SecurityTokenCodec tokenCodec = new BasicSecurityTokenCodec();
+
+  @Test
+  public void typeHtmlBasicOptions() throws Exception {
+    String prefKey = "prefKey";
+    String prefVal = "prefVal";
+    Map<String, String> prefs = Maps.newHashMap();
+    prefs.put(prefKey, prefVal);
+    List<String> features = Lists.newArrayList();
+
+    // Make the gadget.
+    Gadget gadget = mockGadget(
+        SPEC_URI.toString(),
+        false,  // not type=url
+        false,  // isDebug
+        false,  // ignoreCache
+        false,  // sanitize
+        false,  // cajoled
+        prefs,  // spec-contained prefs
+        prefs,  // prefs supplied by the requester, same k/v as spec w/ default val for simplicity
+        false,  // no pref substitution needed, ergo prefs in fragment
+        features);
+
+    // Generate a default-option manager
+    TestDefaultIframeUriManager manager = makeManager(
+        false,   // security token beacon not required
+        false);  // locked domain not required
+
+    // Generate URI, turn into UriBuilder for validation
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals("", uri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, uri.getAuthority());
+    assertEquals(IFRAME_PATH, uri.getPath());
+    assertEquals(SPEC_URI.toString(), uri.getQueryParameter(Param.URL.getKey()));
+    assertEquals(CONTAINER, uri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(VIEW, uri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(LANG, uri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(COUNTRY, uri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals("0", uri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals("0", uri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", uri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(prefVal, uri.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(1, uri.getFragmentParameters().size());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+    assertEquals(SPEC_URI.toString(), htmlGadgetUri.getQueryParameter(Param.URL.getKey()));
+    assertEquals(CONTAINER, htmlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(VIEW, htmlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(LANG, htmlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(COUNTRY, htmlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals("0", htmlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals("0", htmlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", htmlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(prefVal, htmlGadgetUri.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(1, htmlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget.xml", urlGadgetUri.getPath());
+    assertNull(urlGadgetUri.getQueryParameter(Param.URL.getKey()));
+    assertEquals(CONTAINER, urlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(ANOTHER_VIEW, urlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(LANG, urlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(COUNTRY, urlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals("0", urlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals("0", urlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", urlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(prefVal, urlGadgetUri.getQueryParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS + 1, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+
+    assertFalse(manager.tokenForRenderingCalled());
+    assertTrue(manager.schemeCalled());
+    assertTrue(manager.addExtrasCalled());
+  }
+
+  @Test
+  public void typeHtmlCajoled() throws Exception {
+    String prefKey = "prefKey";
+    String prefVal = "prefVal";
+    Map<String, String> prefs = Maps.newHashMap();
+    prefs.put(prefKey, prefVal);
+    List<String> features = Lists.newArrayList();
+
+    // Make the gadget.
+    Gadget gadget = mockGadget(
+        SPEC_URI.toString(),
+        false,  // not type=url
+        false,  // isDebug
+        false,  // ignoreCache
+        false,  // sanitize
+        true,  // cajoled
+        prefs,  // spec-contained prefs
+        prefs,  // prefs supplied by the requester, same k/v as spec w/ default val for simplicity
+        false,  // no pref substitution needed, ergo prefs in fragment
+        features);
+
+    // Generate a default-option manager
+    TestDefaultIframeUriManager manager = makeManager(
+        false,   // security token beacon not required
+        false);  // locked domain not required
+
+    // Generate URI, turn into UriBuilder for validation
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals("0", uri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals("1", uri.getQueryParameter(Param.CAJOLE.getKey()));
+    assertEquals(prefVal, uri.getFragmentParameter("up_" + prefKey));
+
+    // Cajoled is and added param
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, uri.getQueryParameters().size());
+    assertEquals(1, uri.getFragmentParameters().size());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("0", htmlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals("1", htmlGadgetUri.getQueryParameter(Param.CAJOLE.getKey()));
+    assertEquals(prefVal, htmlGadgetUri.getFragmentParameter("up_" + prefKey));
+
+    // Cajoled is and added param
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(1, htmlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("0", urlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals("1", urlGadgetUri.getQueryParameter(Param.CAJOLE.getKey()));
+    assertEquals(prefVal, urlGadgetUri.getQueryParameter("up_" + prefKey));
+
+    // Cajoled is and added param
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS + 2, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+
+    assertFalse(manager.tokenForRenderingCalled());
+    assertTrue(manager.schemeCalled());
+    assertTrue(manager.addExtrasCalled());
+  }
+
+  @Test
+  public void typeHtmlBasicOptionsTpl() throws Exception {
+    String prefKey = "prefKey";
+    String prefVal = "prefVal";
+    Map<String, String> prefs = Maps.newHashMap();
+    prefs.put(prefKey, prefVal);
+    List<String> features = Lists.newArrayList();
+
+    // Make the gadget.
+    Gadget gadget = mockGadget(
+        SPEC_URI.toString(),
+        false,  // not type=url
+        false,  // isDebug
+        false,  // ignoreCache
+        false,  // sanitize
+        false,  // cajoled
+        prefs,  // spec-contained prefs
+        prefs,  // prefs supplied by the requester, same k/v as spec w/ default val for simplicity
+        false,  // no pref substitution needed, ergo prefs in fragment
+        features);
+
+    // Create another manager, this time templatized.
+    TestDefaultIframeUriManager managerTpl = makeManager(
+        false,   // security token beacon not required
+        false);  // locked domain not required
+    managerTpl.setTemplatingSignal(tplSignal(true));
+
+    // Templatized results.
+    Uri resultTpl = managerTpl.makeRenderingUri(gadget);
+    assertNotNull(resultTpl);
+
+    UriBuilder uriTpl = new UriBuilder(resultTpl);
+    assertEquals("", uriTpl.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, uriTpl.getAuthority());
+    assertEquals(IFRAME_PATH, uriTpl.getPath());
+    assertEquals(SPEC_URI.toString(), uriTpl.getQueryParameter(Param.URL.getKey()));
+    assertEquals(CONTAINER, uriTpl.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(tplKey(Param.VIEW.getKey()), uriTpl.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(tplKey(Param.LANG.getKey()), uriTpl.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(tplKey(Param.COUNTRY.getKey()), uriTpl.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(tplKey(Param.DEBUG.getKey()), uriTpl.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(tplKey(Param.NO_CACHE.getKey()),
+        uriTpl.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals(tplKey(Param.SANITIZE.getKey()),
+        uriTpl.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(tplKey("up_" + prefKey), uriTpl.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uriTpl.getQueryParameters().size());
+    assertEquals(1, uriTpl.getFragmentParameters().size());
+
+    Map<String, Uri> uris = managerTpl.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+    assertEquals(SPEC_URI.toString(), htmlGadgetUri.getQueryParameter(Param.URL.getKey()));
+    assertEquals(CONTAINER, htmlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(tplKey(Param.VIEW.getKey()), htmlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(tplKey(Param.LANG.getKey()), htmlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(tplKey(Param.COUNTRY.getKey()), htmlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(tplKey(Param.DEBUG.getKey()), htmlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(tplKey(Param.NO_CACHE.getKey()),
+            htmlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals(tplKey(Param.SANITIZE.getKey()),
+            htmlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(tplKey("up_" + prefKey), htmlGadgetUri.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(1, htmlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget.xml", urlGadgetUri.getPath());
+    assertNull(urlGadgetUri.getQueryParameter(Param.URL.getKey()));
+    assertEquals(CONTAINER, urlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(tplKey(Param.VIEW.getKey()), urlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(tplKey(Param.LANG.getKey()), urlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(tplKey(Param.COUNTRY.getKey()), urlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(tplKey(Param.DEBUG.getKey()), urlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(tplKey(Param.NO_CACHE.getKey()),
+            urlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals(tplKey(Param.SANITIZE.getKey()),
+            urlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(tplKey("up_" + prefKey), urlGadgetUri.getQueryParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS + 1, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+
+    assertFalse(managerTpl.tokenForRenderingCalled());
+    assertTrue(managerTpl.schemeCalled());
+    assertTrue(managerTpl.addExtrasCalled());
+  }
+
+  @Test
+  public void typeUrlDefaultOptions() throws Exception {
+    String gadgetSite = "http://example.com/gadget";
+    String prefKey = "prefKey";
+    String prefVal = "prefVal";
+    Map<String, String> prefs = Maps.newHashMap();
+    prefs.put(prefKey, prefVal);
+    List<String> features = Lists.newArrayList("rpc", "setprefs");
+
+    // Make the gadget.
+    Gadget gadget = mockGadget(
+        gadgetSite,
+        true,   // type=url
+        true,   // isDebug
+        true,   // ignoreCache
+        true,   // sanitize
+        false,  // cajoled
+        prefs,  // spec-contained prefs
+        prefs,  // prefs supplied by the requester, same k/v as spec w/ default val for simplicity
+        false,  // no pref substitution needed, ergo prefs in fragment
+        features);
+
+    // Generate a default-option manager
+    TestDefaultIframeUriManager manager = makeManager(
+        false,   // security token beacon not required
+        false);  // locked domain not required
+
+    // Generate URI, turn into UriBuilder for validation
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals("http", uri.getScheme());
+    assertEquals("example.com", uri.getAuthority());
+    assertEquals("/gadget", uri.getPath());
+    assertEquals(CONTAINER, uri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(VIEW, uri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(LANG, uri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(COUNTRY, uri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(JS_HOST + JS_PATH + "/rpc:setprefs" + DefaultJsUriManager.JS_SUFFIX, uri.getQueryParameter(Param.LIBS.getKey()));
+    assertEquals("1", uri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals("1", uri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("1", uri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(prefVal, uri.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(1, uri.getFragmentParameters().size());
+
+    assertFalse(manager.tokenForRenderingCalled());
+    assertFalse(manager.schemeCalled());
+    assertTrue(manager.addExtrasCalled());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget", urlGadgetUri.getPath());
+    assertEquals(CONTAINER, urlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(VIEW, urlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(LANG, urlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(COUNTRY, urlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(JS_HOST + JS_PATH + "/rpc:setprefs" + DefaultJsUriManager.JS_SUFFIX, urlGadgetUri.getQueryParameter(Param.LIBS.getKey()));
+    assertEquals("1", urlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals("1", urlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("1", urlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(prefVal, urlGadgetUri.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(1, urlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+    assertEquals(CONTAINER, htmlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(ANOTHER_VIEW, htmlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(LANG, htmlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(COUNTRY, htmlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertNull(htmlGadgetUri.getQueryParameter(Param.LIBS.getKey()));
+    assertEquals("1", htmlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals("1", htmlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("1", htmlGadgetUri.getQueryParameter(Param.SANITIZE.getKey()));
+    assertEquals(prefVal, htmlGadgetUri.getQueryParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+
+    assertFalse(manager.tokenForRenderingCalled());
+    assertTrue(manager.schemeCalled());
+    assertTrue(manager.addExtrasCalled());
+  }
+
+  @Test
+  public void typeUrlDefaultOptionsTpl() throws Exception {
+    String gadgetSite = "http://example.com/gadget";
+    String prefKey = "prefKey";
+    String prefVal = "prefVal";
+    Map<String, String> prefs = Maps.newHashMap();
+    prefs.put(prefKey, prefVal);
+    List<String> features = Lists.newArrayList();
+
+    // Make the gadget.
+    Gadget gadget = mockGadget(
+        gadgetSite,
+        true,   // type=url
+        true,   // isDebug
+        true,   // ignoreCache
+        true,   // sanitize
+        false,  // cajoled
+        prefs,  // spec-contained prefs
+        prefs,  // prefs supplied by the requester, same k/v as spec w/ default val for simplicity
+        false,  // no pref substitution needed, ergo prefs in fragment
+        features);
+
+    // Generate a default-option manager
+    TestDefaultIframeUriManager managerTpl = makeManager(
+        false,   // security token beacon not required
+        false);  // locked domain not required
+    managerTpl.setTemplatingSignal(tplSignal(true));
+
+    // Generate URI, turn into UriBuilder for validation
+    Uri resultTpl = managerTpl.makeRenderingUri(gadget);
+    assertNotNull(resultTpl);
+
+    UriBuilder uriTpl = new UriBuilder(resultTpl);
+    assertEquals("http", uriTpl.getScheme());
+    assertEquals("example.com", uriTpl.getAuthority());
+    assertEquals("/gadget", uriTpl.getPath());
+    assertEquals(CONTAINER, uriTpl.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals("", uriTpl.getQueryParameter(Param.LIBS.getKey()));
+    assertEquals(tplKey(Param.VIEW.getKey()), uriTpl.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(tplKey(Param.LANG.getKey()), uriTpl.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(tplKey(Param.COUNTRY.getKey()), uriTpl.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(tplKey(Param.DEBUG.getKey()), uriTpl.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(tplKey(Param.NO_CACHE.getKey()),
+        uriTpl.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals(tplKey("up_" + prefKey), uriTpl.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, uriTpl.getQueryParameters().size());
+    assertEquals(1, uriTpl.getFragmentParameters().size());
+
+    assertFalse(managerTpl.tokenForRenderingCalled());
+    assertFalse(managerTpl.schemeCalled());
+    assertTrue(managerTpl.addExtrasCalled());
+
+    Map<String, Uri> uris = managerTpl.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget", urlGadgetUri.getPath());
+    assertEquals(CONTAINER, urlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals("", urlGadgetUri.getQueryParameter(Param.LIBS.getKey()));
+    assertEquals(tplKey(Param.VIEW.getKey()), urlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(tplKey(Param.LANG.getKey()), urlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(tplKey(Param.COUNTRY.getKey()), urlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(tplKey(Param.DEBUG.getKey()), urlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(tplKey(Param.NO_CACHE.getKey()),
+            urlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals(tplKey("up_" + prefKey), urlGadgetUri.getFragmentParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(1, urlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+    assertEquals(CONTAINER, htmlGadgetUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertNull(htmlGadgetUri.getQueryParameter(Param.LIBS.getKey()));
+    assertEquals(tplKey(Param.VIEW.getKey()), htmlGadgetUri.getQueryParameter(Param.VIEW.getKey()));
+    assertEquals(tplKey(Param.LANG.getKey()), htmlGadgetUri.getQueryParameter(Param.LANG.getKey()));
+    assertEquals(tplKey(Param.COUNTRY.getKey()), htmlGadgetUri.getQueryParameter(Param.COUNTRY.getKey()));
+    assertEquals(tplKey(Param.DEBUG.getKey()), htmlGadgetUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(tplKey(Param.NO_CACHE.getKey()),
+            htmlGadgetUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals(tplKey("up_" + prefKey), htmlGadgetUri.getQueryParameter("up_" + prefKey));
+
+    // Only the params that are needed.
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+
+    assertFalse(managerTpl.tokenForRenderingCalled());
+    assertTrue(managerTpl.schemeCalled());
+    assertTrue(managerTpl.addExtrasCalled());
+  }
+
+  @Test
+  public void securityTokenAddedWhenGadgetNeedsItFragment() throws Exception {
+    Gadget gadget = mockGadget(SECURITY_TOKEN_FEATURE_NAME);
+    TestDefaultIframeUriManager manager = makeManager(false, false);
+    manager.setTokenForRendering(false);
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(1, uri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+        uri.getFragmentParameter(Param.SECURITY_TOKEN.getKey()));
+    assertTrue(manager.tokenForRenderingCalled());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(1, htmlGadgetUri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+            htmlGadgetUri.getFragmentParameter(Param.SECURITY_TOKEN.getKey()));
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(1, urlGadgetUri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+            urlGadgetUri.getFragmentParameter(Param.SECURITY_TOKEN.getKey()));
+
+    assertTrue(manager.tokenForRenderingCalled());
+  }
+
+  @Test
+  public void securityTokenAddedWhenGadgetNeedsItQuery() throws Exception {
+    Gadget gadget = mockGadget(SECURITY_TOKEN_FEATURE_NAME);
+    TestDefaultIframeUriManager manager = makeManager(false, false);
+    manager.setTokenForRendering(true);
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, uri.getQueryParameters().size());
+    assertEquals(0, uri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+        uri.getQueryParameter(Param.SECURITY_TOKEN.getKey()));
+    assertTrue(manager.tokenForRenderingCalled());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+            htmlGadgetUri.getQueryParameter(Param.SECURITY_TOKEN.getKey()));
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS + 1, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+            urlGadgetUri.getQueryParameter(Param.SECURITY_TOKEN.getKey()));
+
+    assertTrue(manager.tokenForRenderingCalled());
+  }
+
+  @Test
+  public void securityTokenAddedWhenForced() throws Exception {
+    Gadget gadget = mockGadget("foo", "bar");
+    TestDefaultIframeUriManager manager = makeManager(true, false);  // security token forced
+    manager.setTokenForRendering(false);
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(1, uri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+        uri.getFragmentParameter(Param.SECURITY_TOKEN.getKey()));
+    assertTrue(manager.tokenForRenderingCalled());
+
+    manager.setTokenForRendering(false);
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(1, htmlGadgetUri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+            htmlGadgetUri.getFragmentParameter(Param.SECURITY_TOKEN.getKey()));
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(1, urlGadgetUri.getFragmentParameters().size());
+    assertEquals(tplKey(Param.SECURITY_TOKEN.getKey()),
+            urlGadgetUri.getFragmentParameter(Param.SECURITY_TOKEN.getKey()));
+
+    assertTrue(manager.tokenForRenderingCalled());
+
+  }
+
+  @Test
+  public void ldAddedGadgetRequests() throws Exception {
+    Gadget gadget = mockGadget(LOCKED_DOMAIN_FEATURE_NAME);
+
+    TestDefaultIframeUriManager manager = makeManager(
+        false,   // security token beacon not required
+        false,   // locked domain not (always) required
+        true);
+
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals("", uri.getScheme());
+    assertEquals(LD_PREFIX + LD_SUFFIX, uri.getAuthority());
+    assertEquals(IFRAME_PATH, uri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(0, uri.getFragmentParameters().size());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(LD_PREFIX + LD_SUFFIX, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget.xml", urlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+  }
+
+  @Test
+  public void ldAddedForcedAlways() throws Exception {
+    Gadget gadget = mockGadget();
+
+    TestDefaultIframeUriManager manager = makeManager(
+        false,   // security token beacon not required
+        true);   // locked domain always required
+
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals("", uri.getScheme());
+    assertEquals(LD_PREFIX + LD_SUFFIX, uri.getAuthority());
+    assertEquals(IFRAME_PATH, uri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(0, uri.getFragmentParameters().size());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(LD_PREFIX + LD_SUFFIX, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget.xml", urlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+  }
+
+  @Test
+  public void ldNotAddedIfDisabled() throws Exception {
+    Gadget gadget = mockGadget(LOCKED_DOMAIN_FEATURE_NAME);
+
+    TestDefaultIframeUriManager manager = makeManager(
+        false,   // security token beacon not required
+        false);   // locked domain always required
+
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals("", uri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, uri.getAuthority());
+    assertEquals(IFRAME_PATH, uri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(0, uri.getFragmentParameters().size());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget.xml", urlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+  }
+
+  @Test
+  public void versionAddedWithVersioner() throws Exception {
+    String version = "abcdlkjwef";
+    Gadget gadget = mockGadget();
+    TestDefaultIframeUriManager manager = makeManager(false, false);
+    manager.setVersioner(this.mockVersioner(version, true));
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, uri.getQueryParameters().size());
+    assertEquals(version, uri.getQueryParameter(Param.VERSION.getKey()));
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 1, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(version, htmlGadgetUri.getQueryParameter(Param.VERSION.getKey()));
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS + 1, urlGadgetUri.getQueryParameters().size());
+    assertEquals(version, urlGadgetUri.getQueryParameter(Param.VERSION.getKey()));
+  }
+
+  @Test
+  public void userPrefsAddedQuery() throws Exception {
+    // Scenario exercises all prefs cases: overridden/known key, unknown key, missing key
+    Map<String, String> specPrefs = Maps.newHashMap();
+    specPrefs.put("specKey1", "specDefault1");
+    specPrefs.put("specKey2", "specDefault2");
+    Map<String, String> inPrefs = Maps.newHashMap();
+    inPrefs.put("specKey1", "inVal1");
+    inPrefs.put("otherKey1", "inVal2");
+
+    Gadget gadget = mockGadget(true, specPrefs, inPrefs);
+    TestDefaultIframeUriManager manager = makeManager(false, false);
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+    UriBuilder uri = new UriBuilder(result);
+
+    // otherKey1/inVal2 pair ignored; not known by the gadget
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 2, uri.getQueryParameters().size());
+    assertEquals(0, uri.getFragmentParameters().size());
+    assertEquals("inVal1", uri.getQueryParameter("up_specKey1"));
+    assertEquals("specDefault2", uri.getQueryParameter("up_specKey2"));
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS + 2, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+    assertEquals("inVal1", htmlGadgetUri.getQueryParameter("up_specKey1"));
+    assertEquals("specDefault2", htmlGadgetUri.getQueryParameter("up_specKey2"));
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(2, urlGadgetUri.getFragmentParameters().size());
+    assertEquals("inVal1", urlGadgetUri.getFragmentParameter("up_specKey1"));
+    assertEquals("specDefault2", urlGadgetUri.getFragmentParameter("up_specKey2"));
+  }
+
+  @Test
+  public void userPrefsAddedFragment() throws Exception {
+    // Scenario exercises all prefs cases: overridden/known key, unknown key, missing key
+    Map<String, String> specPrefs = Maps.newHashMap();
+    specPrefs.put("specKey1", "specDefault1");
+    specPrefs.put("specKey2", "specDefault2");
+    Map<String, String> inPrefs = Maps.newHashMap();
+    inPrefs.put("specKey1", "inVal1");
+    inPrefs.put("otherKey1", "inVal2");
+
+    Gadget gadget = mockGadget(false, specPrefs, inPrefs);
+    TestDefaultIframeUriManager manager = makeManager(false, false);
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+    UriBuilder uri = new UriBuilder(result);
+
+    // otherKey1/inVal2 pair ignored; not known by the gadget
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(2, uri.getFragmentParameters().size());
+    assertEquals("inVal1", uri.getFragmentParameter("up_specKey1"));
+    assertEquals("specDefault2", uri.getFragmentParameter("up_specKey2"));
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(2, htmlGadgetUri.getFragmentParameters().size());
+    assertEquals("inVal1", htmlGadgetUri.getFragmentParameter("up_specKey1"));
+    assertEquals("specDefault2", htmlGadgetUri.getFragmentParameter("up_specKey2"));
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS + 2, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+    assertEquals("inVal1", urlGadgetUri.getQueryParameter("up_specKey1"));
+    assertEquals("specDefault2", urlGadgetUri.getQueryParameter("up_specKey2"));
+  }
+
+  @Test
+  public void honorSchemeOverride() throws Exception {
+    String scheme = "file";
+    Gadget gadget = mockGadget();
+    TestDefaultIframeUriManager manager = makeManager(false, false);
+    manager.setScheme(scheme);
+    Uri result = manager.makeRenderingUri(gadget);
+    assertNotNull(result);
+    UriBuilder uri = new UriBuilder(result);
+    assertEquals(scheme, uri.getScheme());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+    assertEquals(scheme, new UriBuilder(uris.get(VIEW)).getScheme());
+    assertFalse(scheme.equalsIgnoreCase(new UriBuilder(uris.get(ANOTHER_VIEW)).getScheme()));
+  }
+
+  @Test
+  public void badUriValidatingUri() throws Exception {
+    Uri uri = new UriBuilder().addQueryParameter(Param.URL.getKey(), "^':   bad:").toUri();
+    TestDefaultIframeUriManager manager = makeManager(false, false);
+    UriStatus status = manager.validateRenderingUri(uri);
+    assertEquals(UriStatus.BAD_URI, status);
+  }
+
+  @Test
+  public void validUnversionedNoVersioner() throws Exception {
+    Uri uri = makeValidationTestUri(LD_PREFIX + LD_SUFFIX, "version");
+    DefaultIframeUriManager manager = makeManager(false, false);
+    assertEquals(UriStatus.VALID_UNVERSIONED, manager.validateRenderingUri(uri));
+  }
+
+  @Test
+  public void validUnversionedNoVersion() throws Exception {
+    Uri uri = makeValidationTestUri(LD_PREFIX + LD_SUFFIX, null);
+    DefaultIframeUriManager manager = makeManager(false, false);
+    manager.setVersioner(this.mockVersioner("version", false));  // Invalid, if present.
+    assertEquals(UriStatus.VALID_UNVERSIONED, manager.validateRenderingUri(uri));
+  }
+
+  @Test
+  public void versionerVersionInvalid() throws Exception {
+    Uri uri = makeValidationTestUri(LD_PREFIX + LD_SUFFIX, "in-version");
+    DefaultIframeUriManager manager = makeManager(false, false);
+    manager.setVersioner(mockVersioner("test-version", false));  // Invalid, if present.
+    assertEquals(UriStatus.INVALID_VERSION, manager.validateRenderingUri(uri));
+  }
+
+  @Test
+  public void versionerVersionMatch() throws Exception {
+    String version = "abcdefg";
+    Uri uri = makeValidationTestUri(LD_PREFIX + LD_SUFFIX, version);
+    DefaultIframeUriManager manager = makeManager(false, false);
+    manager.setVersioner(mockVersioner(version, true));
+    assertEquals(UriStatus.VALID_VERSIONED, manager.validateRenderingUri(uri));
+  }
+
+  @Test
+  public void containerConfigurationChanges() throws Exception {
+    ContainerConfig config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(ImmutableMap
+            .<String, Object>builder()
+            .put(ContainerConfig.CONTAINER_KEY, ContainerConfig.DEFAULT_CONTAINER)
+            .put(LOCKED_DOMAIN_SUFFIX_KEY, LD_SUFFIX)
+            .put(IFRAME_BASE_PATH_KEY, IFRAME_PATH)
+            .put(LOCKED_DOMAIN_REQUIRED_KEY, true)
+            .build())
+        .commit();
+    LockedDomainService ldService = new HashLockedDomainService(config, true, prefixGen);
+    TestDefaultIframeUriManager manager = new TestDefaultIframeUriManager(config, ldService);
+
+    Uri testUri = Uri.parse("http://foobar" + LD_SUFFIX + "/?url=http://example.com");
+
+    config.newTransaction().addContainer(ImmutableMap
+        .<String, Object>builder()
+        .put(ContainerConfig.CONTAINER_KEY, ContainerConfig.DEFAULT_CONTAINER)
+        .put(LOCKED_DOMAIN_SUFFIX_KEY, LD_SUFFIX_ALT)
+        .build()).commit();
+    assertEquals(UriStatus.VALID_UNVERSIONED, manager.validateRenderingUri(testUri));
+  }
+
+  @Test
+  public void schemeLessUnlockedDomain() throws Exception {
+    Gadget gadget = mockGadget();
+    ContainerConfig config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(ImmutableMap.<String, Object>builder()
+            .put(ContainerConfig.CONTAINER_KEY, ContainerConfig.DEFAULT_CONTAINER)
+            .put(LOCKED_DOMAIN_SUFFIX_KEY, LD_SUFFIX)
+            .build())
+                .addContainer(ImmutableMap.<String, Object> builder()
+                    .put(ContainerConfig.CONTAINER_KEY, CONTAINER)
+                    .put(IFRAME_BASE_PATH_KEY, IFRAME_PATH)
+                    .put(UNLOCKED_DOMAIN_KEY, UNLOCKED_DOMAIN)
+                    .put(DefaultJsUriManager.JS_HOST_PARAM, JS_HOST)
+                     .put(DefaultJsUriManager.JS_PATH_PARAM, JS_PATH)
+                    .build())
+                .commit();
+
+    LockedDomainService ldService = new HashLockedDomainService(config, false, prefixGen);
+    TestDefaultIframeUriManager manager = new TestDefaultIframeUriManager(config, ldService);
+
+    Uri renderingUri = manager.makeRenderingUri(gadget);
+    assertNotNull(renderingUri);
+
+    UriBuilder uri = new UriBuilder(renderingUri);
+    assertEquals("", uri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, uri.getAuthority());
+    assertEquals(IFRAME_PATH, uri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, uri.getQueryParameters().size());
+    assertEquals(0, uri.getFragmentParameters().size());
+
+    Map<String, Uri> uris = manager.makeAllRenderingUris(gadget);
+    assertNotNull(uris);
+
+    UriBuilder htmlGadgetUri = new UriBuilder(uris.get(VIEW));
+    assertEquals("", htmlGadgetUri.getScheme());
+    assertEquals(UNLOCKED_DOMAIN, htmlGadgetUri.getAuthority());
+    assertEquals(IFRAME_PATH, htmlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_HTML_NUM_BASE_PARAMS, htmlGadgetUri.getQueryParameters().size());
+    assertEquals(0, htmlGadgetUri.getFragmentParameters().size());
+
+    UriBuilder urlGadgetUri = new UriBuilder(uris.get(ANOTHER_VIEW));
+    assertEquals("http", urlGadgetUri.getScheme());
+    assertEquals("example.com", urlGadgetUri.getAuthority());
+    assertEquals("/gadget.xml", urlGadgetUri.getPath());
+
+    // Basic sanity checks on params
+    assertEquals(TYPE_URL_NUM_BASE_PARAMS, urlGadgetUri.getQueryParameters().size());
+    assertEquals(0, urlGadgetUri.getFragmentParameters().size());
+  }
+
+  private Uri makeValidationTestUri(String domain, String version) {
+    UriBuilder uri = new UriBuilder();
+    uri.setAuthority(domain);
+    uri.setPath(IFRAME_PATH);
+    uri.addQueryParameter(Param.URL.getKey(), SPEC_URI.toString());
+    uri.addQueryParameter(Param.CONTAINER.getKey(), CONTAINER);
+    if (version != null) {
+      uri.addQueryParameter(Param.VERSION.getKey(), version);
+    }
+    return uri.toUri();
+  }
+
+  private TestDefaultIframeUriManager makeManager(boolean alwaysToken, boolean ldRequired)
+      throws ContainerConfigException {
+    return makeManager(alwaysToken, ldRequired, ldRequired ? true: false);
+  }
+  private TestDefaultIframeUriManager makeManager(boolean alwaysToken, boolean ldRequired, boolean enabled)
+      throws ContainerConfigException {
+    String altContainer = CONTAINER + "-alt";
+    ContainerConfig config = new BasicContainerConfig();
+    config
+        .newTransaction()
+        .addContainer(ImmutableMap
+            .<String, Object>builder()
+            .put(ContainerConfig.CONTAINER_KEY, ContainerConfig.DEFAULT_CONTAINER)
+            .put(LOCKED_DOMAIN_SUFFIX_KEY, LD_SUFFIX)
+            .put(DefaultJsUriManager.JS_HOST_PARAM, JS_HOST)
+            .put(DefaultJsUriManager.JS_PATH_PARAM, JS_PATH)
+            .build())
+        .addContainer(ImmutableMap
+            .<String, Object>builder()
+            .put(ContainerConfig.CONTAINER_KEY, CONTAINER)
+            .put(IFRAME_BASE_PATH_KEY, IFRAME_PATH)
+            .put(LOCKED_DOMAIN_SUFFIX_KEY, LD_SUFFIX)
+            .put(UNLOCKED_DOMAIN_KEY, UNLOCKED_DOMAIN_CONFIG_VALUE)
+            .put(SECURITY_TOKEN_ALWAYS_KEY, alwaysToken)
+            .put(LOCKED_DOMAIN_REQUIRED_KEY, ldRequired)
+            .put(DefaultJsUriManager.JS_HOST_PARAM, JS_HOST)
+            .put(DefaultJsUriManager.JS_PATH_PARAM, JS_PATH)
+            .build())
+        .addContainer(ImmutableMap
+            .<String, Object>builder()
+            .put(ContainerConfig.CONTAINER_KEY, altContainer)
+            .put(ContainerConfig.PARENT_KEY, CONTAINER)
+            .put(LOCKED_DOMAIN_SUFFIX_KEY, LD_SUFFIX_ALT)
+            .put(DefaultJsUriManager.JS_HOST_PARAM, JS_HOST)
+            .put(DefaultJsUriManager.JS_PATH_PARAM, JS_PATH)
+            .build())
+        .commit();
+    LockedDomainService ldService = new HashLockedDomainService(config, enabled, prefixGen);
+    return new TestDefaultIframeUriManager(config, ldService);
+  }
+
+  private IframeUriManager.Versioner mockVersioner(String version, boolean valid) {
+    IframeUriManager.Versioner versioner = createMock(IframeUriManager.Versioner.class);
+    expect(versioner.version(isA(Uri.class), isA(String.class))).andReturn(version).anyTimes();
+    expect(versioner.validate(isA(Uri.class), isA(String.class), isA(String.class))).andReturn(
+        valid ? UriStatus.VALID_VERSIONED : UriStatus.INVALID_VERSION).anyTimes();
+    replay(versioner);
+    return versioner;
+  }
+
+  private DefaultIframeUriManager.TemplatingSignal tplSignal(boolean value) {
+    DefaultIframeUriManager.DefaultTemplatingSignal tplSignal =
+        new DefaultIframeUriManager.DefaultTemplatingSignal();
+    tplSignal.setUseTemplates(value);
+    return tplSignal;
+  }
+
+  private static final class TestDefaultIframeUriManager extends DefaultIframeUriManager {
+    private String scheme = "";
+    private boolean schemeCalled = false;
+    private boolean tokenForRendering = true;
+    private boolean tokenForRenderingCalled = false;
+    private boolean addExtrasCalled = false;
+
+    private TestDefaultIframeUriManager(ContainerConfig config, LockedDomainService ldService) {
+      super(config, ldService, tokenCodec);
+    }
+
+    private TestDefaultIframeUriManager setScheme(String scheme) {
+      this.scheme = scheme;
+      return this;
+    }
+
+    private TestDefaultIframeUriManager setTokenForRendering(boolean tokenForRendering) {
+      this.tokenForRendering = tokenForRendering;
+      return this;
+    }
+
+    @Override
+    protected String getScheme(Gadget gadget, String container) {
+      this.schemeCalled = true;
+      return scheme;
+    }
+
+    private boolean schemeCalled() {
+      return schemeCalled;
+    }
+
+    @Override
+    protected boolean isTokenNeededForRendering(Gadget gadget) {
+      this.tokenForRenderingCalled = true;
+      return tokenForRendering;
+    }
+
+    private boolean tokenForRenderingCalled() {
+      return tokenForRenderingCalled;
+    }
+
+    @Override
+    protected void addExtras(UriBuilder uri, Gadget gadget) {
+      this.addExtrasCalled = true;
+    }
+
+    private boolean addExtrasCalled() {
+      return addExtrasCalled;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultJsUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultJsUriManagerTest.java
new file mode 100644
index 0000000..35e6634
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultJsUriManagerTest.java
@@ -0,0 +1,471 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.apache.shindig.gadgets.uri.DefaultJsUriManager.JS_SUFFIX;
+import static org.apache.shindig.gadgets.uri.DefaultJsUriManager.addJsLibs;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.eq;
+import static org.junit.Assert.*;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+import org.apache.shindig.gadgets.uri.JsUriManager.Versioner;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class DefaultJsUriManagerTest {
+  private static final String CONTAINER = "container";
+  private static final String GADGET_URI = "http://example.com/gadget.xml";
+
+  // makeJsUri tests
+  @Test(expected = RuntimeException.class)
+  public void makeMissingHostConfig() {
+    ContainerConfig config = mockConfig(null, "/gadgets/js");
+    DefaultJsUriManager manager = makeManager(config, null);
+    JsUri ctx = mockGadgetContext(false, false, null);
+    manager.makeExternJsUri(ctx);
+  }
+
+  @Test(expected = RuntimeException.class)
+  public void makeMissingPathConfig() {
+    ContainerConfig config = mockConfig("foo", null);
+    DefaultJsUriManager manager = makeManager(config, null);
+    JsUri ctx = mockGadgetContext(false, false, null);
+    manager.makeExternJsUri(ctx);
+  }
+
+  @Test
+  public void makeJsUriNoPathSlashNoVersion() {
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js/");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    List<String> extern = Lists.newArrayList("feature");
+    JsUri ctx = mockGadgetContext(false, false, extern);
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(RenderingContext.GADGET.getParamValue(),
+        jsUri.getQueryParameter(Param.CONTAINER_MODE.getKey()));
+  }
+
+  @Test
+  public void makeJsUriExtensionParams() {
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js/");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    List<String> extern = Lists.newArrayList("feature");
+    JsUri ctx = mockGadgetContext(false, false, extern, null, false,
+        ImmutableMap.of("test", "1"), null, "rep");
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals("1", jsUri.getQueryParameter("test"));
+    assertEquals("rep", jsUri.getQueryParameter(Param.REPOSITORY_ID.getKey()));
+  }
+
+  @Test
+  public void makeJsUriAddPathSlashNoVersion() {
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    List<String> extern = Lists.newArrayList("feature");
+    JsUri ctx = mockGadgetContext(false, false, extern);
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.CONTAINER_MODE.getKey()));
+    assertNull(jsUri.getQueryParameters(Param.REPOSITORY_ID.getKey()));
+  }
+
+  @Test
+  public void makeJsUriAddPathSlashVersioned() {
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js");
+    List<String> extern = Lists.newArrayList("feature");
+    JsUri ctx = mockGadgetContext(false, false, extern);
+    String version = "verstring";
+    Versioner versioner = this.mockVersioner(ctx, version, version);
+    TestDefaultJsUriManager manager = makeManager(config, versioner);
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(version, jsUri.getQueryParameter(Param.VERSION.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(RenderingContext.GADGET.getParamValue(),
+        jsUri.getQueryParameter(Param.CONTAINER_MODE.getKey()));
+  }
+
+  @Test
+  public void makeJsUriWithVersionerNoVersionOnIgnoreCache() {
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js");
+    List<String> extern = Lists.newArrayList("feature");
+    JsUri ctx = mockGadgetContext(true, false, extern);  // no cache
+    String version = "verstring";
+    Versioner versioner = this.mockVersioner(ctx, version, version);
+    TestDefaultJsUriManager manager = makeManager(config, versioner);
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    // No version string appended.
+    assertEquals(null, jsUri.getQueryParameter(Param.VERSION.getKey()));
+    assertEquals("1", jsUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(RenderingContext.GADGET.getParamValue(),
+        jsUri.getQueryParameter(Param.CONTAINER_MODE.getKey()));
+  }
+
+  @Test
+  public void makeJsUriWithContainerContext() {
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js/");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    List<String> extern = Lists.newArrayList("feature", "another");
+    JsUri ctx = mockGadgetContext(false, false, extern, null, true, null,
+        JsCompileMode.CONCAT_COMPILE_EXPORT_ALL, null);
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL.getParamValue(),
+        jsUri.getQueryParameter(Param.COMPILE_MODE.getKey()));
+    assertEquals(RenderingContext.CONTAINER.getParamValue(),
+        jsUri.getQueryParameter(Param.CONTAINER_MODE.getKey()));
+  }
+
+  @Test
+  public void makeJsUriWithLoadedLibraries() throws Exception {
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js/");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    List<String> extern = Lists.newArrayList("feature", "another");
+    List<String> loaded = Lists.newArrayList("another", "onemore");
+    JsUri ctx = mockGadgetContext(false, false, extern, loaded);
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + "!" + addJsLibs(loaded) +
+        JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.DEBUG.getKey()));
+    assertEquals(RenderingContext.GADGET.getParamValue(),
+        jsUri.getQueryParameter(Param.CONTAINER_MODE.getKey()));
+  }
+
+  // processJsUri tests
+  @Test
+  public void processDefaultConfig() throws GadgetException {
+    ContainerConfig config = mockDefaultConfig("foo", "/gadgets/js");
+    DefaultJsUriManager manager = makeManager(config, null);
+    manager.processExternJsUri(Uri.parse("http://example.com?container=" + CONTAINER));
+  }
+
+  @Test
+  public void processPathPrefixMismatch() throws GadgetException {
+    String targetHost = "target-host.org";
+    ContainerConfig config = mockConfig("http://" + targetHost, "/gadgets/js");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    Uri testUri = Uri.parse("http://target-host.org/gadgets/other-js/feature" + JS_SUFFIX + '?' +
+        Param.CONTAINER.getKey() + '=' + CONTAINER);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    List<String> extern = Lists.newArrayList("feature");
+    assertEquals(extern, jsUri.getLibs());
+    List<String> loaded = Lists.newArrayList();
+    assertEquals(loaded, jsUri.getLoadedLibs());
+    assertEquals(CONTAINER, jsUri.getContainer());
+    assertEquals(RenderingContext.GADGET, jsUri.getContext());
+  }
+
+  @Test
+  public void processPathWithEncodedSeparator() throws GadgetException {
+    String targetHost = "target-host.org";
+    ContainerConfig config = mockConfig("http://" + targetHost, "/gadgets/js");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    Uri testUri = Uri.parse("http://target-host.org/gadgets/js/feature%3Aanother?" +
+        Param.CONTAINER.getKey() + '=' + CONTAINER);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    assertEquals(jsUri.getStatus(), UriStatus.VALID_UNVERSIONED);
+    List<String> extern = Lists.newArrayList("feature", "another");
+    assertCollectionEquals(jsUri.getLibs(), extern);
+  }
+
+  @Test
+  public void processPathSuffixNoJs() throws GadgetException {
+    String targetHost = "target-host.org";
+    ContainerConfig config = mockConfig("http://" + targetHost, "/gadgets/js");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    Uri testUri = Uri.parse("http://target-host.org/gadgets/js/feature:another?" +
+        Param.CONTAINER.getKey() + '=' + CONTAINER);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    assertEquals(jsUri.getStatus(), UriStatus.VALID_UNVERSIONED);
+    List<String> extern = Lists.newArrayList("feature", "another");
+    assertCollectionEquals(jsUri.getLibs(), extern);
+  }
+
+  @Test
+  public void processPathWithLoadedJs() throws GadgetException {
+    ContainerConfig config = mockConfig("http://host", "/gadgets/js");
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    Uri testUri = Uri.parse("http://host/gadgets/js/feature:another!load1:load2.js?" +
+        Param.LOADED.getKey() + "=load3:load4&" +
+        Param.CONTAINER.getKey() + '=' + CONTAINER);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    List<String> extern = Lists.newArrayList("feature", "another");
+    assertCollectionEquals(jsUri.getLibs(), extern);
+    assertCollectionEquals(jsUri.getLoadedLibs(), Lists.newArrayList(
+        "load1", "load2", "load3", "load4"));
+  }
+
+  @Test
+  public void processValidUnversionedNoVersioner() throws GadgetException {
+    String targetHost = "target-host.org";
+    ContainerConfig config = mockConfig("http://" + targetHost, "/gadgets/js");
+    List<String> extern = Lists.newArrayList("feature", "another");
+    String version = "verstring";
+    TestDefaultJsUriManager manager = makeManager(config, null);
+    Uri testUri = Uri.parse("http://target-host.org/gadgets/js/" + addJsLibs(extern) +
+        JS_SUFFIX + '?' + Param.CONTAINER.getKey() + '=' + CONTAINER + '&' +
+        Param.VERSION.getKey() + '=' + version);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    assertEquals(jsUri.getStatus(), UriStatus.VALID_UNVERSIONED);
+    assertCollectionEquals(jsUri.getLibs(), extern);
+  }
+
+  @Test
+  public void processValidInvalidVersion() throws GadgetException {
+    String targetHost = "target-host.org";
+    ContainerConfig config = mockConfig("http://" + targetHost, "/gadgets/js");
+    List<String> extern = Lists.newArrayList("feature", "another");
+    String version = "verstring";
+    String badVersion = version + "-a";
+    JsUri ctx = mockGadgetContext(false, false, extern);
+    Versioner versioner = mockVersioner(ctx, version, badVersion);
+    TestDefaultJsUriManager manager = makeManager(config, versioner);
+    Uri testUri = Uri.parse("http://target-host.org/gadgets/js/" + addJsLibs(extern) +
+        JS_SUFFIX + '?' + Param.CONTAINER.getKey() + '=' + CONTAINER + '&' +
+        Param.VERSION.getKey() + '=' + badVersion);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    assertEquals(jsUri.getStatus(), UriStatus.INVALID_VERSION);
+    assertCollectionEquals(jsUri.getLibs(), extern);
+  }
+
+  @Test
+  public void processValidUnversionedNoParam() throws GadgetException {
+    String targetHost = "target-host.org";
+    ContainerConfig config = mockConfig("http://" + targetHost, "/gadgets/js");
+    List<String> extern = Lists.newArrayList("feature", "another");
+    String version = "verstring";
+    JsUri ctx = mockGadgetContext(false, false, extern);
+    Versioner versioner = mockVersioner(ctx, version, version);
+    TestDefaultJsUriManager manager = makeManager(config, versioner);
+    Uri testUri = Uri.parse("http://target-host.org/gadgets/js/" + addJsLibs(extern) +
+        JS_SUFFIX + '?' + Param.CONTAINER.getKey() + '=' + CONTAINER);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    assertEquals(jsUri.getStatus(), UriStatus.VALID_UNVERSIONED);
+    assertCollectionEquals(jsUri.getLibs(), extern);
+  }
+
+  @Test
+  public void processValidVersioned() throws GadgetException {
+    String targetHost = "target-host.org";
+    ContainerConfig config = mockConfig("http://" + targetHost, "/gadgets/js");
+    List<String> extern = Lists.newArrayList("feature", "another");
+    String version = "verstring";
+    JsUri ctx = mockGadgetContext(false, false, extern);
+    Versioner versioner = mockVersioner(ctx, version, version);
+    TestDefaultJsUriManager manager = makeManager(config, versioner);
+    Uri testUri = Uri.parse("http://target-host.org/gadgets/js/" + addJsLibs(extern) +
+        JS_SUFFIX + '?' + Param.CONTAINER.getKey() + '=' + CONTAINER + '&' +
+        Param.VERSION.getKey() + '=' + version);
+    JsUri jsUri = manager.processExternJsUri(testUri);
+    assertFalse(manager.hadError());
+    assertEquals(jsUri.getStatus(), UriStatus.VALID_VERSIONED);
+    assertCollectionEquals(jsUri.getLibs(), extern);
+  }
+
+  // end-to-end integration-ish test: makeX builds a Uri that processX correctly handles
+  @Test
+  public void makeAndProcessSymmetric() throws GadgetException {
+    // Make...
+    ContainerConfig config = mockConfig("http://www.js.org", "/gadgets/js");
+    List<String> extern = Lists.newArrayList("feature1", "feature2", "feature3");
+    JsUri ctx = mockGadgetContext(false, false, extern);
+    String version = "verstring";
+    Versioner versioner = mockVersioner(ctx, version, version);
+    TestDefaultJsUriManager manager = makeManager(config, versioner);
+    Uri jsUri = manager.makeExternJsUri(ctx);
+    assertFalse(manager.hadError());
+    assertEquals("http", jsUri.getScheme());
+    assertEquals("www.js.org", jsUri.getAuthority());
+    assertEquals("/gadgets/js/" + addJsLibs(extern) + JS_SUFFIX, jsUri.getPath());
+    assertEquals(CONTAINER, jsUri.getQueryParameter(Param.CONTAINER.getKey()));
+    assertEquals(version, jsUri.getQueryParameter(Param.VERSION.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.NO_CACHE.getKey()));
+    assertEquals("0", jsUri.getQueryParameter(Param.DEBUG.getKey()));
+
+    // ...and process
+    JsUri processed = manager.processExternJsUri(jsUri);
+    assertEquals(UriStatus.VALID_VERSIONED, processed.getStatus());
+    assertCollectionEquals(extern, processed.getLibs());
+  }
+
+  private void assertCollectionEquals(Collection<String> expected, Collection<String> test) {
+    assertEquals(expected.size(), test.size());
+    List<String> expectedCopy = Lists.newArrayList(expected);
+    List<String> testCopy = Lists.newArrayList(test);
+    assertEquals(expectedCopy, testCopy);
+  }
+
+  private ContainerConfig mockConfig(String jsHost, String jsPath) {
+    ContainerConfig config = createMock(ContainerConfig.class);
+    expect(config.getString(CONTAINER, DefaultJsUriManager.JS_HOST_PARAM))
+        .andReturn(jsHost).anyTimes();
+    expect(config.getString(CONTAINER, DefaultJsUriManager.JS_PATH_PARAM))
+        .andReturn(jsPath).anyTimes();
+    expect(config.getString(ContainerConfig.DEFAULT_CONTAINER, DefaultJsUriManager.JS_HOST_PARAM))
+        .andReturn(null).anyTimes();
+    expect(config.getString(ContainerConfig.DEFAULT_CONTAINER, DefaultJsUriManager.JS_PATH_PARAM))
+        .andReturn(null).anyTimes();
+    replay(config);
+    return config;
+  }
+
+  private ContainerConfig mockDefaultConfig(String jsHost, String jsPath) {
+    ContainerConfig config = createMock(ContainerConfig.class);
+    expect(config.getString(CONTAINER, DefaultJsUriManager.JS_HOST_PARAM))
+        .andReturn(null).anyTimes();
+    expect(config.getString(CONTAINER, DefaultJsUriManager.JS_PATH_PARAM))
+        .andReturn(null).anyTimes();
+    expect(config.getString(ContainerConfig.DEFAULT_CONTAINER, DefaultJsUriManager.JS_HOST_PARAM))
+        .andReturn(jsHost).anyTimes();
+    expect(config.getString(ContainerConfig.DEFAULT_CONTAINER, DefaultJsUriManager.JS_PATH_PARAM))
+        .andReturn(jsPath).anyTimes();
+    replay(config);
+    return config;
+  }
+
+  private Versioner mockVersioner(JsUri jsUri, String genVersion, String testVersion) {
+    JsUriManager.Versioner versioner = createMock(Versioner.class);
+    expect(versioner.version(jsUri)).andStubReturn(genVersion);
+    UriStatus status = (genVersion != null && genVersion.equals(testVersion)) ?
+        UriStatus.VALID_VERSIONED : UriStatus.INVALID_VERSION;
+    expect(versioner.validate(isA(JsUri.class), eq(testVersion))).andStubReturn(status);
+    replay(versioner);
+    return versioner;
+  }
+
+  private JsUri mockGadgetContext(boolean nocache, boolean debug, List<String> extern) {
+    return mockGadgetContext(nocache, debug, extern, ImmutableList.<String>of(), false,
+    null, null, null);
+  }
+
+  private JsUri mockGadgetContext(
+      boolean nocache, boolean debug, List<String> extern, List<String> loaded) {
+    return mockGadgetContext(nocache, debug, extern, loaded, false, null, null, null);
+  }
+
+  private JsUri mockGadgetContext(boolean nocache, boolean debug,
+      List<String> extern, List<String> loaded,
+      boolean isContainer, Map<String, String> params,
+      JsCompileMode compileMode, String repository) {
+    JsUri context = createMock(JsUri.class);
+    expect(context.getContainer()).andStubReturn(CONTAINER);
+    expect(context.isNoCache()).andStubReturn(nocache);
+    expect(context.isDebug()).andStubReturn(debug);
+    expect(context.getGadget()).andStubReturn(GADGET_URI);
+    expect(context.getContext()).andStubReturn(
+        isContainer ? RenderingContext.CONTAINER : RenderingContext.GADGET);
+    expect(context.getLibs()).andStubReturn(extern);
+    expect(context.getLoadedLibs()).andStubReturn(
+        loaded == null ? ImmutableList.<String>of() : loaded);
+    expect(context.getOnload()).andStubReturn(null);
+    expect(context.isJsload()).andStubReturn(false);
+    expect(context.isNohint()).andStubReturn(false);
+    expect(context.getExtensionParams()).andStubReturn(params);
+    expect(context.getOrigUri()).andStubReturn(null);
+    expect(context.getCompileMode()).andStubReturn(compileMode);
+    expect(context.cajoleContent()).andStubReturn(false);
+    expect(context.getRepository()).andStubReturn(repository);
+    replay(context);
+    return context;
+  }
+
+  private TestDefaultJsUriManager makeManager(ContainerConfig config, Versioner versioner) {
+    return new TestDefaultJsUriManager(config, versioner);
+  }
+
+  private static final class TestDefaultJsUriManager extends DefaultJsUriManager {
+    private boolean errorReported = false;
+
+    private TestDefaultJsUriManager(ContainerConfig config, Versioner versioner) {
+      super(config, versioner);
+    }
+
+    @Override
+    protected void issueUriFormatError(String err) {
+      this.errorReported = true;
+    }
+
+    public boolean hadError() {
+      return errorReported;
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultJsVersionerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultJsVersionerTest.java
new file mode 100644
index 0000000..4f9eafc
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultJsVersionerTest.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureResource;
+import org.apache.shindig.gadgets.uri.JsUriManager.JsUri;
+
+import org.easymock.EasyMock;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Tests JS versioner. Ensures that it returns a non-null String which
+ * gets appropriately cached and differs when JS content differs.
+ */
+public class DefaultJsVersionerTest {
+  private DefaultJsVersioner versioner;
+  private FeatureRegistry registry;
+
+  @Before
+  public void setUp() {
+    registry = createMock(FeatureRegistry.class);
+    versioner = new DefaultJsVersioner(registry);
+  }
+
+  @Test
+  public void versionCached() {
+    String feature = "feature1";
+    expectReq(feature, "content");
+    replay(registry);
+    Collection<String> libs = Lists.newArrayList(feature);
+    JsUri jsUri = new JsUri(UriStatus.VALID_UNVERSIONED, null, libs, null);
+    String version = versioner.version(jsUri);
+    assertNotNull(version);
+    String versionAgain = versioner.version(jsUri);
+    assertSame(version, versionAgain);
+    verify(registry);
+  }
+
+  @Test
+  public void versionDifferentForDifferentFeatures() {
+    String feature1 = "feature1";
+    String feature2 = "feature2";
+    expectReq(feature1, "content1");
+    expectReq(feature2, "content2");
+    replay(registry);
+    Collection<String> libs1 = Lists.newArrayList(feature1);
+    JsUri jsUri1 = new JsUri(UriStatus.VALID_UNVERSIONED, null, libs1, null);
+    String version1 = versioner.version(jsUri1);
+    Collection<String> libs2 = Lists.newArrayList(feature2);
+    JsUri jsUri2 = new JsUri(UriStatus.VALID_UNVERSIONED, null, libs2, null);
+    String version2 = versioner.version(jsUri2);
+    assertNotNull(version1);
+    assertNotNull(version2);
+    assertFalse(version1.equals(version2));
+    verify(registry);
+  }
+
+  @Test
+  public void validateMismatch() {
+    String feature = "feature1";
+    expectReq(feature, "content");
+    replay(registry);
+    Collection<String> libs = Lists.newArrayList(feature);
+    JsUri jsUri = new JsUri(UriStatus.VALID_UNVERSIONED, null, libs, null);
+    String version = versioner.version(jsUri);
+    assertNotNull(version);
+    UriStatus status = versioner.validate(jsUri, version + "-nomatch");
+    assertEquals(UriStatus.INVALID_VERSION, status);
+    verify(registry);
+  }
+
+  @Test
+  public void validateNull() {
+    String feature = "feature1";
+    expectReq(feature, "content");
+    replay(registry);
+    Collection<String> libs = Lists.newArrayList(feature);
+    JsUri jsUri = new JsUri(UriStatus.VALID_UNVERSIONED, null, libs, null);
+    String version = versioner.version(jsUri);
+    assertNotNull(version);
+    UriStatus status = versioner.validate(jsUri, null);
+    assertEquals(UriStatus.VALID_UNVERSIONED, status);
+    verify(registry);
+  }
+
+  @Test
+  public void validateEmpty() {
+    String feature = "feature1";
+    expectReq(feature, "content");
+    replay(registry);
+    Collection<String> libs = Lists.newArrayList(feature);
+    JsUri jsUri = new JsUri(UriStatus.VALID_UNVERSIONED, null, libs, null);
+    String version = versioner.version(jsUri);
+    assertNotNull(version);
+    UriStatus status = versioner.validate(jsUri, "");
+    assertEquals(UriStatus.VALID_UNVERSIONED, status);
+    verify(registry);
+  }
+
+  @Test
+  public void createAndValidateVersion() {
+    String feature = "feature1";
+    expectReq(feature, "content");
+    replay(registry);
+    Collection<String> libs = Lists.newArrayList(feature);
+    JsUri jsUri = new JsUri(UriStatus.VALID_UNVERSIONED, null, libs, null);
+    String version = versioner.version(jsUri);
+    assertNotNull(version);
+    UriStatus status = versioner.validate(jsUri, version);
+    assertEquals(UriStatus.VALID_VERSIONED, status);
+    verify(registry);
+  }
+
+  private void expectReq(String feature, String content) {
+    FeatureResource resource = new FeatureResource.Simple(content, "", "js");
+    Collection<String> libs = Lists.newArrayList(feature);
+    List<String> loaded = ImmutableList.of();
+    List<FeatureResource> resources = Lists.newArrayList(resource);
+    final FeatureRegistry.LookupResult lr = createMock(FeatureRegistry.LookupResult.class);
+    expect(lr.getResources()).andReturn(resources).anyTimes();
+    replay(lr);
+    expect(registry.getFeatureResources(isA(GadgetContext.class), eq(libs),
+        EasyMock.<List<String>>isNull())).andReturn(lr).anyTimes();
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultOAuthUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultOAuthUriManagerTest.java
new file mode 100644
index 0000000..c2b3476
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultOAuthUriManagerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.apache.shindig.config.ContainerConfig;
+
+import org.junit.Test;
+
+public class DefaultOAuthUriManagerTest {
+  private static final String CONTAINER = "container";
+  private static final String HOST = "www.host.com";
+
+  @Test
+  public void noConfigValueConfigured() throws Exception {
+    ContainerConfig config = mockConfig(null);
+    DefaultOAuthUriManager manager = makeManager(config);
+    assertNull(manager.makeOAuthCallbackUri(CONTAINER, HOST));
+    verify(config);
+  }
+
+  @Test
+  public void noHostSubstitution() throws Exception {
+    String value = "http://www.apache.org/oauth/callback";
+    ContainerConfig config = mockConfig(value);
+    DefaultOAuthUriManager manager = makeManager(config);
+    assertEquals(value, manager.makeOAuthCallbackUri(CONTAINER, HOST).toString());
+    verify(config);
+  }
+
+  @Test
+  public void oauthUriWithHostSubstitution() throws Exception {
+    String value = "http://%host%/oauth/callback";
+    ContainerConfig config = mockConfig(value);
+    DefaultOAuthUriManager manager = makeManager(config);
+    assertEquals("http://" + HOST + "/oauth/callback",
+        manager.makeOAuthCallbackUri(CONTAINER, HOST).toString());
+    verify(config);
+  }
+
+  private ContainerConfig mockConfig(String tplVal) {
+    ContainerConfig config = createMock(ContainerConfig.class);
+    expect(config.getString(CONTAINER, DefaultOAuthUriManager.OAUTH_GADGET_CALLBACK_URI_PARAM))
+        .andReturn(tplVal).once();
+    replay(config);
+    return config;
+  }
+
+  private DefaultOAuthUriManager makeManager(ContainerConfig config) {
+    return new DefaultOAuthUriManager(config);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultProxyUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultProxyUriManagerTest.java
new file mode 100644
index 0000000..ed5de68
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/DefaultProxyUriManagerTest.java
@@ -0,0 +1,559 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.uri.ProxyUriManager.ProxyUri;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+public class DefaultProxyUriManagerTest extends UriManagerTestBase {
+  private static final Uri RESOURCE_1 = Uri.parse("http://example.com/one.dat?param=value");
+  private static final Uri RESOURCE_2 = Uri.parse("http://gadgets.com/two.dat");
+  private static final Uri RESOURCE_3 = Uri.parse("http://foobar.com/three.dat");
+  private static final Uri RESOURCE_4 = Uri.parse("//foobar.com/three.dat");
+
+  @Test
+  public void basicProxyQueryStyle() throws Exception {
+    checkQueryStyle(false, false, null);
+  }
+
+  @Test
+  public void altParamsProxyQueryStyle() throws Exception {
+    checkQueryStyle(true, true, "version");
+  }
+
+  private void checkQueryStyle(boolean debug, boolean noCache, String version) throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    List<Uri> resources = ImmutableList.of(RESOURCE_1);
+    List<Uri> uris = makeAndGet(host, path, debug, noCache, resources, version);
+    assertEquals(1, uris.size());
+    verifyQueryUri(RESOURCE_1, uris.get(0), debug, noCache, version, host, path);
+  }
+
+  @Test
+  public void testSchemaLessProxy() throws Exception {
+    boolean debug = false;
+    boolean noCache = false;
+    String version = "ver";
+    String host = "host.com";
+    String path = "/proxy/path";
+    List<Uri> resources = ImmutableList.of(RESOURCE_4);
+    List<Uri> uris = makeAndGet(host, path, debug, noCache, resources, version);
+    assertEquals(1, uris.size());
+    verifyQueryUri(new UriBuilder(RESOURCE_4).setScheme("http").toUri(),
+        new UriBuilder(uris.get(0)).setScheme("http").toUri(),
+        debug, noCache, version, host, path);
+  }
+
+  @Test
+  public void refreshVerifyBasic() throws Exception {
+    verifyRefresh(false, false, "version", 20);
+  }
+
+  @Test
+  public void refreshVerifyNoCache() throws Exception {
+    verifyRefresh(false, true, "version", 20);
+  }
+
+  @Test
+  public void refreshVerifyNoRefresh() throws Exception {
+    verifyRefresh(false, false, "version", null);
+  }
+
+  public void verifyRefresh(boolean debug, boolean noCache, String version, Integer refresh)
+      throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    ProxyUriManager.Versioner versioner = makeVersioner(null, version);
+    DefaultProxyUriManager manager = makeManager(host, path, versioner);
+    List<ProxyUri> proxyUris = Lists.newLinkedList();
+    proxyUris.add(new ProxyUri(refresh, debug, noCache, CONTAINER, SPEC_URI.toString(),
+        RESOURCE_1));
+
+    List<Uri> uris = manager.make(proxyUris, null);
+    assertEquals(1, uris.size());
+    verifyQueryUriWithRefresh(RESOURCE_1, uris.get(0), debug, noCache,
+        version, refresh, host, path);
+  }
+
+  @Test
+  public void verifyAddedParamsQuery() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    ProxyUriManager.Versioner versioner = makeVersioner(null, "version1", "version2");
+    DefaultProxyUriManager manager = makeManager(host, path, versioner);
+    List<ProxyUri> proxyUris = Lists.newLinkedList();
+    ProxyUri pUri = new ProxyUri(null, false, true, CONTAINER, SPEC_URI.toString(),
+        RESOURCE_1);
+    pUri.setResize(100, 10, 90, true);
+    proxyUris.add(pUri);
+
+    pUri = new ProxyUri(null, false, true, CONTAINER, SPEC_URI.toString(),
+        RESOURCE_2);
+    pUri.setResize(null, 10, null, false);
+    proxyUris.add(pUri);
+
+    List<Uri> uris = manager.make(proxyUris, null);
+    assertEquals(2, uris.size());
+    verifyQueryUriWithRefresh(RESOURCE_1, uris.get(0), false, true,
+        "version1", null, host, path);
+    // Verify added param:
+    assertEquals("100", uris.get(0).getQueryParameter("resize_w"));
+    assertEquals("10", uris.get(0).getQueryParameter("resize_h"));
+    assertEquals("90", uris.get(0).getQueryParameter("resize_q"));
+    assertEquals("1", uris.get(0).getQueryParameter("no_expand"));
+    assertEquals(null, uris.get(1).getQueryParameter("resize_w"));
+    assertEquals("10", uris.get(1).getQueryParameter("resize_h"));
+    assertEquals(null, uris.get(1).getQueryParameter("resize_q"));
+    assertEquals(null, uris.get(1).getQueryParameter("no_expand"));
+  }
+
+  @Test
+  public void verifyAddedParamsChained() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path";
+    ProxyUriManager.Versioner versioner = makeVersioner(null, "version");
+    DefaultProxyUriManager manager = makeManager(host, path, versioner);
+    List<ProxyUri> proxyUris = Lists.newLinkedList();
+    ProxyUri pUri = new ProxyUri(null, false, true, CONTAINER, SPEC_URI.toString(),
+        RESOURCE_1);
+    pUri.setExtensionParams(ImmutableMap.<String, String>of("test", "1"));
+    pUri.setResize(100, 10, 90, true);
+    proxyUris.add(pUri);
+
+    List<Uri> uris = manager.make(proxyUris, null);
+    assertEquals(1, uris.size());
+    verifyChainedUri(RESOURCE_1, uris.get(0), false, true,
+        null, false, host, path);
+    // Verify added param:
+    assertEquals("/proxy/container=container&gadget=http%3A%2F%2Fexample.com%2Fgadget.xml" +
+        "&debug=0&nocache=1&v=version&test=1&resize_h=10&resize_w=100&resize_q=90&no_expand=1" +
+        "/path/http://example.com/one.dat",
+        uris.get(0).getPath());
+  }
+
+  @Test
+  public void testFallbackUrl() throws Exception {
+    ProxyUri uri = new ProxyUri(null, false, false, "open", "http://example.com/gadget",
+        Uri.parse("http://example.com/resource"));
+    uri.setFallbackUrl("http://example.com/fallback");
+
+    assertEquals("http://example.com/fallback", uri.getFallbackUri().toString());
+  }
+
+  @Test(expected = GadgetException.class)
+  public void testBadFallbackUrl() throws Exception {
+    ProxyUri uri = new ProxyUri(null, false, false, "open", "http://example.com/gadget",
+        Uri.parse("http://example.com/resource"));
+    uri.setFallbackUrl("bad url");
+    uri.getFallbackUri(); // throws exception!
+  }
+
+  @Test
+  public void basicProxyChainedStyle() throws Exception {
+    checkChainedStyle(false, false, null);
+  }
+
+  @Test
+  public void altParamsProxyChainedStyle() throws Exception {
+    checkChainedStyle(true, true, "version");
+  }
+
+  private void checkChainedStyle(boolean debug, boolean noCache, String version) throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path";
+    List<Uri> resources = ImmutableList.of(RESOURCE_1);
+    List<Uri> uris = makeAndGet(host, path, debug, noCache, resources, version);
+    assertEquals(1, uris.size());
+    verifyChainedUri(RESOURCE_1, uris.get(0), debug, noCache, version, false, host, path);
+  }
+
+  @Test
+  public void basicProxyChainedStyleEndOfPath() throws Exception {
+    checkChainedStyleEndOfPath(false, false, null);
+  }
+
+  @Test
+  public void altParamsProxyChainedStyleEndOfPath() throws Exception {
+    checkChainedStyleEndOfPath(true, true, "version");
+  }
+
+  private void checkChainedStyleEndOfPath(boolean debug, boolean noCache, String version) throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN;
+    List<Uri> resources = ImmutableList.of(RESOURCE_1);
+    List<Uri> uris = makeAndGet(host, path, debug, noCache, resources, version);
+    assertEquals(1, uris.size());
+    verifyChainedUri(RESOURCE_1, uris.get(0), debug, noCache, version, true, host, path);
+  }
+
+  @Test
+  public void batchedProxyQueryStyle() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    List<Uri> resources = ImmutableList.of(RESOURCE_1, RESOURCE_2, RESOURCE_3);
+    String[] versions = new String[] { "v1", "v2", "v3" };
+    List<Uri> uris = makeAndGet(host, path, true, true, resources, versions);
+    assertEquals(3, uris.size());
+    for (int i = 0; i < 3; ++i) {
+      verifyQueryUri(resources.get(i), uris.get(i), true, true, versions[i], host, path);
+    }
+  }
+
+  @Test
+  public void batchedProxyChainedStyle() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path";
+    List<Uri> resources = ImmutableList.of(RESOURCE_1, RESOURCE_2, RESOURCE_3);
+    String[] versions = new String[] { "v1", "v2", "v3" };
+    List<Uri> uris = makeAndGet(host, path, true, true, resources, versions);
+    assertEquals(3, uris.size());
+    for (int i = 0; i < 3; ++i) {
+      verifyChainedUri(resources.get(i), uris.get(i), true, true, versions[i], false, host, path);
+    }
+  }
+
+  @Test
+  public void batchedProxyChainedStyleNoVerisons() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path";
+    List<Uri> resources = ImmutableList.of(RESOURCE_1, RESOURCE_2, RESOURCE_3);
+    List<Uri> uris = makeAndGet(host, path, true, true, resources);
+    assertEquals(3, uris.size());
+    for (int i = 0; i < 3; ++i) {
+      verifyChainedUri(resources.get(i), uris.get(i), true, true, null, false, host, path);
+    }
+  }
+
+  @Test
+  public void validateQueryStyleUnversioned() throws Exception {
+    // Validate tests also serve as end-to-end tests: create, unpack.
+    checkValidate("/proxy/path", UriStatus.VALID_UNVERSIONED, null);
+  }
+
+  @Test
+  public void validateChainedStyleUnversioned() throws Exception {
+    checkValidate("/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path",
+        UriStatus.VALID_UNVERSIONED, null);
+  }
+
+  @Test
+  public void validateQueryStyleVersioned() throws Exception {
+    checkValidate("/proxy/path", UriStatus.VALID_VERSIONED, "version");
+  }
+
+  @Test
+  public void validateChainedStyleVersioned() throws Exception {
+    checkValidate("/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path",
+        UriStatus.VALID_VERSIONED, "version");
+  }
+
+  private void checkValidate(String path, UriStatus status, String version) throws Exception {
+    String host = "host.com";
+    // Pass null for status if version is null, since null version shouldn't result
+    // in a check to the versioner.
+    ProxyUriManager.Versioner versioner = makeVersioner(version == null ? null : status, version);
+    DefaultProxyUriManager manager = makeManager(host, path, versioner);
+    Gadget gadget = mockGadget(false, false);
+    List<Uri> resources = ImmutableList.of(RESOURCE_1);
+    List<Uri> uris = manager.make(
+        ProxyUriManager.ProxyUri.fromList(gadget, resources), 123);
+    assertEquals(1, uris.size());
+    ProxyUriManager.ProxyUri proxyUri = manager.process(uris.get(0));
+    assertEquals(RESOURCE_1, proxyUri.getResource());
+    assertEquals(CONTAINER, proxyUri.getContainer());
+    assertEquals(SPEC_URI.toString(), proxyUri.getGadget());
+    assertEquals(123, (int)proxyUri.getRefresh());
+    assertEquals(status, proxyUri.getStatus());
+    assertEquals(false, proxyUri.isDebug());
+    assertEquals(false, proxyUri.isNoCache());
+  }
+
+  @Test
+  public void containerFallsBackToSynd() throws Exception {
+    String host = "host.com";
+    String path = "/path";
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    UriBuilder uriBuilder = new UriBuilder();
+    uriBuilder.setScheme("http").setAuthority(host).setPath(path);
+    uriBuilder.addQueryParameter(Param.URL.getKey(), RESOURCE_1.toString());
+    uriBuilder.addQueryParameter("synd", CONTAINER);
+    uriBuilder.addQueryParameter(Param.GADGET.getKey(), SPEC_URI.toString());
+    uriBuilder.addQueryParameter(Param.REFRESH.getKey(), "321");
+    ProxyUriManager.ProxyUri proxyUri = manager.process(uriBuilder.toUri());
+    assertEquals(RESOURCE_1, proxyUri.getResource());
+    assertEquals(CONTAINER, proxyUri.getContainer());
+    assertEquals(SPEC_URI.toString(), proxyUri.getGadget());
+    assertEquals(321, (int)proxyUri.getRefresh());
+    assertEquals(false, proxyUri.isDebug());
+    assertEquals(false, proxyUri.isNoCache());
+  }
+
+  @Test(expected = GadgetException.class)
+  public void mismatchedHostStrict() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    DefaultProxyUriManager manager = makeManager("foo" + host, path, null);
+    manager.setUseStrictParsing(true);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(path)
+        .addQueryParameter(Param.URL.getKey(), "http://foo.com").toUri();
+    manager.process(testUri);
+  }
+
+  @Test
+  public void mismatchedHostNonStrict() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    DefaultProxyUriManager manager = makeManager("foo" + host, path, null);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(path)
+        .addQueryParameter(Param.URL.getKey(), "http://foo.com")
+        .addQueryParameter(Param.CONTAINER.getKey(), CONTAINER).toUri();
+    manager.process(testUri);
+  }
+
+  @Test(expected = GadgetException.class)
+  public void missingContainerParamQuery() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(path)
+        .addQueryParameter(Param.URL.getKey(), "http://foo.com").toUri();
+    manager.process(testUri);
+  }
+
+  @Test(expected = GadgetException.class)
+  public void missingContainerParamChained() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path";
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(
+        "/proxy/refresh=123/path/http://foo.com").toUri();
+    manager.process(testUri);
+  }
+
+  @Test(expected = GadgetException.class)
+  public void missingUrlQuery() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(path)
+        .addQueryParameter(Param.CONTAINER.getKey(), CONTAINER).toUri();
+    manager.process(testUri);
+  }
+
+  @Test(expected = GadgetException.class)
+  public void missingUrlChained() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN + "/path";
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(
+        "/proxy/container=" +
+        CONTAINER + "/path/").toUri();
+    manager.process(testUri);
+  }
+
+  @Test(expected = GadgetException.class)
+  public void invalidUrlParamQuery() throws Exception {
+    // Only test query style, since chained style should be impossible.
+    String host = "host.com";
+    String path = "/proxy/path";
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(path)
+        .addQueryParameter(Param.CONTAINER.getKey(), CONTAINER)
+        .addQueryParameter(Param.URL.getKey(), "!^!").toUri();
+    manager.process(testUri);
+  }
+
+  @Test
+  public void testHtmlTagContext() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/path";
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    Uri testUri = new UriBuilder().setAuthority(host).setPath(path)
+        .addQueryParameter(Param.CONTAINER.getKey(), CONTAINER)
+        .addQueryParameter(Param.URL.getKey(), "http://www.example.org/")
+        .addQueryParameter(Param.HTML_TAG_CONTEXT.getKey(), "htmlTag")
+        .toUri();
+    ProxyUri proxyUri = manager.process(testUri);
+    assertEquals("htmlTag", proxyUri.getHtmlTagContext());
+
+    Uri targetUri = Uri.parse("http://www.example2.org/");
+    HttpRequest req = proxyUri.makeHttpRequest(targetUri);
+    assertEquals("htmlTag", req.getParam(Param.HTML_TAG_CONTEXT.getKey()));
+
+    UriBuilder builder = proxyUri.makeQueryParams(1, "2");
+    assertEquals("htmlTag", builder.getQueryParameter(Param.HTML_TAG_CONTEXT.getKey()));
+  }
+
+  private List<Uri> makeAndGet(String host, String path, boolean debug, boolean noCache,
+      List<Uri> resources, String... version) {
+    return makeAndGetWithRefresh(host, path, debug, noCache, resources, 123, version);
+  }
+
+  private List<Uri> makeAndGetWithRefresh(String host, String path, boolean debug,
+      boolean noCache, List<Uri> resources, Integer refresh, String... version) {
+    ProxyUriManager.Versioner versioner = makeVersioner(null, version);
+    DefaultProxyUriManager manager = makeManager(host, path, versioner);
+    Gadget gadget = mockGadget(debug, noCache);
+    return manager.make(
+        ProxyUriManager.ProxyUri.fromList(gadget, resources), refresh);
+  }
+
+  private void verifyQueryUri(Uri orig, Uri uri, boolean debug, boolean noCache, String version,
+      String host, String path) throws Exception {
+    verifyQueryUriWithRefresh(orig, uri, debug, noCache, version, 123, host, path);
+  }
+
+  private void verifyQueryUriWithRefresh(Uri orig, Uri uri, boolean debug, boolean noCache,
+      String version, Integer refresh, String host, String path) throws Exception {
+    // Make sure the manager can parse out results.
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    ProxyUri proxyUri = manager.process(uri);
+    assertEquals(orig, proxyUri.getResource());
+    assertEquals(debug, proxyUri.isDebug());
+    assertEquals(noCache, proxyUri.isNoCache());
+    assertEquals(noCache ? Integer.valueOf(0) : refresh, proxyUri.getRefresh());
+    assertEquals(CONTAINER, proxyUri.getContainer());
+    assertEquals(SPEC_URI.toString(), proxyUri.getGadget());
+
+    // "Raw" query param verification.
+    assertEquals(noCache || refresh == null ? null : refresh.toString(),
+        uri.getQueryParameter(Param.REFRESH.getKey()));
+    if (version != null) {
+      assertEquals(version, uri.getQueryParameter(Param.VERSION.getKey()));
+    }
+  }
+
+  private void verifyChainedUri(Uri orig, Uri uri, boolean debug, boolean noCache, String version,
+      boolean endOfPath, String host, String path)
+      throws Exception {
+    // Make sure the manager can parse out results.
+    DefaultProxyUriManager manager = makeManager(host, path, null);
+    ProxyUri proxyUri = manager.process(uri);
+    assertEquals(orig, proxyUri.getResource());
+    assertEquals(debug, proxyUri.isDebug());
+    assertEquals(noCache, proxyUri.isNoCache());
+    assertEquals(noCache ? 0 : 123, (int)proxyUri.getRefresh());
+    assertEquals(CONTAINER, proxyUri.getContainer());
+    assertEquals(SPEC_URI.toString(), proxyUri.getGadget());
+
+    // URI should end with the proxied content.
+    String uriStr = uri.toString();
+    assertTrue(uriStr.endsWith(orig.toString()));
+
+    int proxyEnd = uriStr.indexOf("/proxy/") + "/proxy/".length();
+    String paramsUri = uriStr.substring(
+        proxyEnd,
+        (endOfPath ? uriStr.indexOf('/', proxyEnd) : uriStr.indexOf("/path")));
+    uri = new UriBuilder().setQuery(paramsUri).toUri();
+
+    // "Raw" query param verification.
+    assertEquals(noCache ? null : "123", uri.getQueryParameter(Param.REFRESH.getKey()));
+    if (version != null) {
+      assertEquals(version, uri.getQueryParameter(Param.VERSION.getKey()));
+    }
+  }
+
+  @Test
+  public void testProxyGadgetsChainDecode() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN;
+    DefaultProxyUriManager uriManager = makeManager(host, path, null);
+    Uri uri = Uri.parse("http://host.com/gadgets/proxy/refresh%3d55%26container%3dcontainer/"
+        + "http://www.cnn.com/news?refresh=45");
+    ProxyUri pUri = uriManager.process(uri);
+    assertEquals(new Integer(55), pUri.getRefresh());
+    assertEquals("http://www.cnn.com/news?refresh=45", pUri.getResource().toString());
+    assertEquals(CONTAINER, pUri.getContainer());
+  }
+
+  @Test
+  public void testProxyGadgetsChainDecodeGif() throws Exception {
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN;
+    DefaultProxyUriManager uriManager = makeManager(host, path, null);
+    Uri uri = Uri.parse("http://host.com/gadgets/proxy/container%3dcontainer%26" +
+        "gadget%3dhttp%3A%2F%2Fwww.orkut.com%2Fcss%2Fgen%2Fbase054.css.int%26" +
+        "debug%3d0%26nocache%3d0/http://www.orkut.com/img/castro/i%5freply.gif");
+    ProxyUri pUri = uriManager.process(uri);
+    assertEquals(false, pUri.isDebug());
+    assertEquals("http://www.orkut.com/img/castro/i%5freply.gif", pUri.getResource().toString());
+    assertEquals(CONTAINER, pUri.getContainer());
+    assertEquals("http://www.orkut.com/css/gen/base054.css.int", pUri.getGadget());
+  }
+
+  @Test
+  public void testProxyGadgetsChainGif() throws Exception {
+
+    String host = "host.com";
+    String path = "/proxy/" + DefaultProxyUriManager.CHAINED_PARAMS_TOKEN;
+    DefaultProxyUriManager uriManager = makeManager(host, path, null);
+    Uri uri = Uri.parse("http://host.com/gadgets/proxy/container=container&" +
+        "gadget=http%3A%2F%2Fwww.orkut.com%2Fcss%2Fgen%2Fbase054.css.int&" +
+        "debug=0&nocache=0/http://www.orkut.com/img/castro/i_reply.gif");
+    ProxyUri pUri = uriManager.process(uri);
+    assertEquals(false, pUri.isDebug());
+    assertEquals("http://www.orkut.com/img/castro/i_reply.gif", pUri.getResource().toString());
+    assertEquals(CONTAINER, pUri.getContainer());
+    assertEquals("http://www.orkut.com/css/gen/base054.css.int", pUri.getGadget());
+  }
+
+  private DefaultProxyUriManager makeManager(String host, String path,
+      ProxyUriManager.Versioner versioner) {
+    ContainerConfig config = createMock(ContainerConfig.class);
+    expect(config.getString(CONTAINER, DefaultProxyUriManager.PROXY_HOST_PARAM))
+        .andReturn(host).anyTimes();
+    expect(config.getString(CONTAINER, DefaultProxyUriManager.PROXY_PATH_PARAM))
+        .andReturn(path).anyTimes();
+    replay(config);
+    return new DefaultProxyUriManager(config, versioner);
+  }
+
+  @SuppressWarnings("unchecked")
+  private ProxyUriManager.Versioner makeVersioner(UriStatus status, String... versions) {
+    ProxyUriManager.Versioner versioner = createMock(ProxyUriManager.Versioner.class);
+    if (versions.length > 0) {
+      expect(versioner.version(isA(List.class), eq(CONTAINER), isA(List.class)))
+          .andReturn(Lists.newArrayList(versions)).anyTimes();
+    } else {
+      expect(versioner.version(isA(List.class), eq(CONTAINER), isA(List.class)))
+          .andReturn(null).anyTimes();
+    }
+    expect(versioner.validate(isA(Uri.class), eq(CONTAINER), isA(String.class)))
+        .andReturn(status).anyTimes();
+    replay(versioner);
+    return versioner;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/HashShaLockedDomainPrefixGeneratorTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/HashShaLockedDomainPrefixGeneratorTest.java
new file mode 100644
index 0000000..7d2957b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/HashShaLockedDomainPrefixGeneratorTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+
+import org.junit.Test;
+
+/**
+ * Very simple test case for a very simple class generating
+ * locked-domain prefixes for Uris. The main value of this test is to
+ * increase code coverage numbers, but there's also some value in
+ * checkpointing the class's output, since if it ever changed integrators
+ * would need to be well aware of this fact, since changing l-d requires
+ * a delicate dance in production.
+ */
+public class HashShaLockedDomainPrefixGeneratorTest {
+  private HashShaLockedDomainPrefixGenerator generator = new HashShaLockedDomainPrefixGenerator();
+
+  @Test
+  public void generate() {
+    Uri uri = Uri.parse("http://www.apache.org/gadget.xml");
+    assertEquals("e5bld32ce9pe5ln81rjhe0d0e1vao1ba", generator.getLockedDomainPrefix(uri));
+  }
+
+  @Test(expected = NullPointerException.class)
+  public void isNull() {
+    generator.getLockedDomainPrefix((Uri)null);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/JsUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/JsUriManagerTest.java
new file mode 100644
index 0000000..0ade261
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/JsUriManagerTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.gadgets.JsCompileMode;
+import org.apache.shindig.gadgets.RenderingContext;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+public class JsUriManagerTest extends UriManagerTestBase {
+  private static final UriStatus STATUS = UriStatus.VALID_UNVERSIONED;
+  private static final List<String> LIBS = Lists.newArrayList("feat1", "feat2");
+  private static final List<String> HAVE = Lists.newArrayList("have1", "have2");
+  private static final String CONTAINER_VALUE = "ig";
+  private static final String ONLOAD_VALUE = "ol";
+
+  @Test
+  public void newJsUriWithOriginalUri() throws Exception {
+    Uri uri = newTestUriBuilder(RenderingContext.CONTAINER,
+        JsCompileMode.CONCAT_COMPILE_EXPORT_ALL).toUri();
+    JsUriManager.JsUri jsUri = new JsUriManager.JsUri(STATUS, uri, LIBS, HAVE);
+    assertEquals(RenderingContext.CONTAINER, jsUri.getContext());
+    assertEquals(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL, jsUri.getCompileMode());
+    assertEquals(CONTAINER_VALUE, jsUri.getContainer());
+    assertTrue(jsUri.isJsload());
+    assertTrue(jsUri.isNoCache());
+    assertTrue(jsUri.isNohint());
+    assertEquals(ONLOAD_VALUE, jsUri.getOnload());
+    assertEquals(LIBS, Lists.newArrayList(jsUri.getLibs()));
+    assertEquals(HAVE, Lists.newArrayList(jsUri.getLoadedLibs()));
+    assertEquals(uri, jsUri.getOrigUri());
+  }
+
+  @Test
+  public void newJsUriWithConfiguredGadgetContext() throws Exception {
+    Uri uri = newTestUriBuilder(RenderingContext.CONFIGURED_GADGET,
+        JsCompileMode.CONCAT_COMPILE_EXPORT_ALL).toUri();
+    JsUriManager.JsUri jsUri = new JsUriManager.JsUri(STATUS, uri, LIBS, HAVE);
+    assertEquals(RenderingContext.CONFIGURED_GADGET, jsUri.getContext());
+    assertEquals(JsCompileMode.CONCAT_COMPILE_EXPORT_ALL, jsUri.getCompileMode());
+    assertEquals(CONTAINER_VALUE, jsUri.getContainer());
+    assertTrue(jsUri.isJsload());
+    assertTrue(jsUri.isNoCache());
+    assertTrue(jsUri.isNohint());
+    assertEquals(ONLOAD_VALUE, jsUri.getOnload());
+    assertEquals(LIBS, Lists.newArrayList(jsUri.getLibs()));
+    assertEquals(HAVE, Lists.newArrayList(jsUri.getLoadedLibs()));
+    assertEquals(uri, jsUri.getOrigUri());
+  }
+
+  @Test
+  public void newJsUriWithEmptyOriginalUri() throws Exception {
+    JsUriManager.JsUri jsUri = new JsUriManager.JsUri(STATUS, null,
+        Collections.<String>emptyList(), null); // Null URI.
+    assertEquals(RenderingContext.GADGET, jsUri.getContext());
+    assertEquals(ContainerConfig.DEFAULT_CONTAINER, jsUri.getContainer());
+    assertEquals(JsCompileMode.COMPILE_CONCAT, jsUri.getCompileMode());
+    assertFalse(jsUri.isJsload());
+    assertFalse(jsUri.isNoCache());
+    assertFalse(jsUri.isNohint());
+    assertNull(jsUri.getOnload());
+    assertTrue(jsUri.getLibs().isEmpty());
+    assertTrue(jsUri.getLoadedLibs().isEmpty());
+    assertNull(jsUri.getOrigUri());
+  }
+
+  @Test
+  public void newJsUriCopyOfOtherJsUri() throws Exception {
+    Uri uri = newTestUriBuilder(RenderingContext.CONTAINER,
+        JsCompileMode.CONCAT_COMPILE_EXPORT_ALL).toUri();
+    JsUriManager.JsUri jsUri = new JsUriManager.JsUri(STATUS, uri, LIBS, HAVE);
+    JsUriManager.JsUri jsUriCopy = new JsUriManager.JsUri(jsUri);
+    assertEquals(jsUri, jsUriCopy);
+  }
+
+  private UriBuilder newTestUriBuilder(RenderingContext context,
+      JsCompileMode compileMode) {
+    UriBuilder builder = new UriBuilder();
+    builder.setScheme("http");
+    builder.setAuthority("localhost");
+    builder.setPath("/gadgets/js/feature.js");
+    builder.addQueryParameter(Param.CONTAINER.getKey(), CONTAINER_VALUE);
+    builder.addQueryParameter(Param.CONTAINER_MODE.getKey(), context.getParamValue());
+    builder.addQueryParameter(Param.COMPILE_MODE.getKey(), compileMode.getParamValue());
+    builder.addQueryParameter(Param.JSLOAD.getKey(), "1");
+    builder.addQueryParameter(Param.NO_CACHE.getKey(), "1");
+    builder.addQueryParameter(Param.NO_HINT.getKey(), "1");
+    builder.addQueryParameter(Param.ONLOAD.getKey(), ONLOAD_VALUE);
+    return builder;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/PassthruManager.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/PassthruManager.java
new file mode 100644
index 0000000..040baec
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/PassthruManager.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import com.google.common.collect.Lists;
+
+public class PassthruManager implements ProxyUriManager {
+  private UriStatus expectStatus = UriStatus.VALID_VERSIONED;
+
+  private boolean doProxy = false;
+  private String proxyHost = null;
+  private String proxyPath = null;
+
+  public PassthruManager() {
+    // Regular no-proxy mode.
+  }
+
+  public PassthruManager(String proxyHost, String proxyPath) {
+    this.proxyHost = proxyHost;
+    this.proxyPath = proxyPath;
+    this.doProxy = true;
+  }
+
+  public List<Uri> make(List<ProxyUri> resource, Integer forcedRefresh) {
+    List<Uri> ctx = Lists.newArrayListWithCapacity(resource.size());
+    for (ProxyUri res : resource) {
+      ctx.add(getUri(res));
+    }
+    return ImmutableList.copyOf(ctx);
+  }
+
+  private Uri getUri(ProxyUri src) {
+    if (!doProxy) {
+      return src.getResource();
+    }
+    UriBuilder builder =
+        new UriBuilder().setScheme("http").setAuthority(proxyHost).setPath(proxyPath)
+          .addQueryParameter(Param.URL.getKey(), src.getResource().toString());
+    if (src.sanitizeContent()) {
+      builder.addQueryParameter(Param.SANITIZE.getKey(), "1");
+    }
+    if (src.getRewriteMimeType() != null) {
+      builder.addQueryParameter(Param.REWRITE_MIME_TYPE.getKey(), src.getRewriteMimeType());
+    }
+    return builder.toUri();
+  }
+
+  public ProxyUri process(Uri uri) throws GadgetException {
+    String proxied = uri.getQueryParameter(Param.URL.getKey());
+    ProxyUri proxyUri = new ProxyUri(expectStatus,
+        proxied != null ? Uri.parse(proxied) : null, uri);
+    proxyUri.setHtmlTagContext(uri.getQueryParameter(Param.HTML_TAG_CONTEXT.getKey()));
+    return proxyUri;
+  }
+
+  public void expectStatus(UriStatus status) {
+    this.expectStatus = status;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/ProxyUriBaseTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/ProxyUriBaseTest.java
new file mode 100644
index 0000000..2e88d8a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/ProxyUriBaseTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.junit.Assert.assertEquals;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.name.Names;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+
+import org.junit.Test;
+
+public class ProxyUriBaseTest {
+  final static Uri URI = Uri.parse("http://www.example.org/foo.html");
+
+  private Gadget createGadget(final String container) throws SpecParserException {
+    GadgetSpec spec = new GadgetSpec(URI,
+        "<Module><ModulePrefs author=\"a\" title=\"t\"></ModulePrefs>" +
+        "<Content></Content></Module>");
+    return new Gadget().setContext(new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        return "0";
+      }
+
+      @Override
+      public String getContainer() {
+        return container;
+      }
+    }).setSpec(spec);
+  }
+
+  @Test
+  public void testWithSetAuthorityAsGadgetParam() throws Exception {
+    injectAuthorityAsGadgetParam(true);
+    ProxyUriBase proxyUriBase = new ProxyUriBase(createGadget(null));
+    assertEquals(URI.getAuthority(), proxyUriBase.getGadget());
+
+    injectAuthorityAsGadgetParam(false);
+    proxyUriBase = new ProxyUriBase(createGadget(null));
+    assertEquals(URI.toString(), proxyUriBase.getGadget());
+  }
+
+  private void injectAuthorityAsGadgetParam(final Boolean val) {
+    Guice.createInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(Boolean.class).annotatedWith(
+            Names.named("org.apache.shindig.gadgets.uri.setAuthorityAsGadgetParam"))
+            .toInstance(val);
+        requestStaticInjection(ProxyUriBase.class);
+      }
+    });
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/ProxyUriManagerTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/ProxyUriManagerTest.java
new file mode 100644
index 0000000..be5b238
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/ProxyUriManagerTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Tests for ProxyUriManager.
+ */
+public class ProxyUriManagerTest {
+  final static Uri URI = Uri.parse("http://www.example.org");
+
+  private Gadget getGadget(final String container) throws SpecParserException {
+    GadgetSpec spec = new GadgetSpec(URI,
+        "<Module><ModulePrefs author=\"a\" title=\"t\"></ModulePrefs>" +
+        "<Content></Content></Module>");
+    return new Gadget().setContext(new GadgetContext() {
+      @Override
+      public String getParameter(String name) {
+        return "0";
+      }
+
+      @Override
+      public String getContainer() {
+        return container;
+      }
+    }).setSpec(spec);
+  }
+
+  @Test
+  public void testReturnOrigContentOnErrorNullWhenNullContainer() throws Exception {
+    ProxyUriManager.ProxyUri proxyUri = new ProxyUriManager.ProxyUri(
+        0, true, false, null, "test", URI);
+    assertNull(proxyUri.returnOriginalContentOnError);
+
+    proxyUri = new ProxyUriManager.ProxyUri(getGadget(null), URI);
+    assertNull(proxyUri.returnOriginalContentOnError);
+  }
+
+  @Test
+  public void testReturnOrigContentOnErrorNullWhenNonAccelContainer() throws Exception {
+    ProxyUriManager.ProxyUri proxyUri = new ProxyUriManager.ProxyUri(
+        0, true, false, "dummy", "test", Uri.parse("http://www.example.org"));
+    assertNull(proxyUri.returnOriginalContentOnError);
+
+    proxyUri = new ProxyUriManager.ProxyUri(getGadget("dummy"), URI);
+    assertNull(proxyUri.returnOriginalContentOnError);
+  }
+
+  @Test
+  public void testReturnOrigContentOnErrorTrueWhenAccelContainer() throws Exception {
+    ProxyUriManager.ProxyUri proxyUri = new ProxyUriManager.ProxyUri(
+        0, true, false, "accel", "test", Uri.parse("http://www.example.org"));
+    assertEquals("1", proxyUri.returnOriginalContentOnError);
+
+    proxyUri = new ProxyUriManager.ProxyUri(getGadget("accel"), URI);
+    assertEquals("1", proxyUri.returnOriginalContentOnError);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriManagerTestBase.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriManagerTestBase.java
new file mode 100644
index 0000000..8944643
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriManagerTestBase.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.replay;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.Gadget;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.UserPrefs;
+import org.apache.shindig.gadgets.spec.Feature;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.ModulePrefs;
+import org.apache.shindig.gadgets.spec.UserPref;
+import org.apache.shindig.gadgets.spec.View;
+import org.apache.shindig.gadgets.spec.View.ContentType;
+import org.apache.shindig.gadgets.uri.UriCommon.Param;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+public class UriManagerTestBase {
+  protected static final String CONTAINER = "container";
+  protected static final Uri SPEC_URI = Uri.parse("http://example.com/gadget.xml");
+  protected static final String VIEW = "theview";
+  protected static final String ANOTHER_VIEW = "anotherview";
+  protected static final String LANG = "en";
+  protected static final String COUNTRY = "US";
+
+  // Used for "feature-focused" tests, eg. security token and locked domain
+  protected Gadget mockGadget(String... features) {
+    Map<String, String> prefs = Maps.newHashMap();
+    return mockGadget(SPEC_URI.toString(), false, false, false, false, false, prefs, prefs,
+        false, Lists.newArrayList(features));
+  }
+
+  // Used for prefs-focused tests
+  protected Gadget mockGadget(boolean prefsForRendering, Map<String, String> specPrefs,
+      Map<String, String> inPrefs) {
+    return mockGadget(SPEC_URI.toString(), false, false, false, false, false, specPrefs,
+        inPrefs, prefsForRendering, Lists.<String>newArrayList());
+  }
+
+  // Used for "base" tests.
+  protected Gadget mockGadget(String targetUrl, boolean isTypeUrl, boolean isDebug,
+      boolean ignoreCache, boolean sanitize, boolean cajoled, Map<String, String> specPrefs,
+      Map<String, String> inPrefs, boolean needsPrefSubst, List<String> features) {
+    return mockGadget(targetUrl, isTypeUrl, VIEW, LANG, COUNTRY, isDebug, ignoreCache,
+        sanitize, cajoled, specPrefs, inPrefs, needsPrefSubst, features);
+  }
+
+  // Used for tests that don't care much about prefs or gadget type.
+  protected Gadget mockGadget(boolean isDebug, boolean ignoreCache) {
+    return mockGadget(SPEC_URI.toString(), false, isDebug, ignoreCache,
+        false, false, Maps.<String, String>newHashMap(),
+        Maps.<String, String>newHashMap(), false, Lists.<String>newArrayList());
+  }
+
+  // Actually generates the mock gadget. Used for error (null value) tests.
+  protected Gadget mockGadget(String targetUrl, boolean isTypeUrl, String currentViewStr, String lang,
+      String country, boolean isDebug, boolean ignoreCache, boolean sanitize, boolean cajoled,
+      Map<String, String> specPrefs, Map<String, String> inPrefs, boolean needsPrefSubst, List<String> features) {
+    View currentView = createMock(View.class);
+    View secondView = createMock(View.class);
+    ModulePrefs modulePrefs = createMock(ModulePrefs.class);
+    GadgetSpec spec = createMock(GadgetSpec.class);
+    GadgetContext context = createMock(GadgetContext.class);
+    Gadget gadget = createMock(Gadget.class);
+
+    // Base URL/view.
+    Uri targetUri = Uri.parse(targetUrl);
+    if (isTypeUrl) {
+      expect(currentView.getType()).andReturn(ContentType.URL).anyTimes();
+      expect(currentView.getHref()).andReturn(targetUri).anyTimes();
+      expect(secondView.getType()).andReturn(ContentType.HTML).anyTimes();
+      expect(spec.getUrl()).andReturn(targetUri).anyTimes();
+    } else {
+      expect(currentView.getType()).andReturn(ContentType.HTML).anyTimes();
+      expect(spec.getUrl()).andReturn(targetUri).anyTimes();
+      expect(secondView.getType()).andReturn(ContentType.URL).anyTimes();
+      expect(secondView.getHref()).andReturn(targetUri).anyTimes();
+    }
+    expect(currentView.getName()).andReturn(currentViewStr).anyTimes();
+    expect(secondView.getName()).andReturn(ANOTHER_VIEW).anyTimes();
+
+    // Basic context info
+    Locale locale = new Locale(lang, country);
+    expect(context.getUrl()).andReturn(SPEC_URI).anyTimes();
+    expect(context.getContainer()).andReturn(CONTAINER).anyTimes();
+    expect(context.getLocale()).andReturn(locale).anyTimes();
+    expect(context.getDebug()).andReturn(isDebug).anyTimes();
+    expect(context.getIgnoreCache()).andReturn(ignoreCache).anyTimes();
+    expect(context.getToken()).andReturn(null).anyTimes();
+    expect(context.getSanitize()).andReturn(sanitize).anyTimes();
+    expect(context.getCajoled()).andReturn(cajoled).anyTimes();
+
+    // All Features (doesn't distinguish between transitive and not)
+    expect(gadget.getAllFeatures()).andReturn(features).anyTimes();
+    Map<String, Feature> featureMap = Maps.newLinkedHashMap();
+    for (String feature : features) {
+      featureMap.put(feature, null);
+    }
+    expect(gadget.getViewFeatures()).andReturn(featureMap).anyTimes();
+    expect(modulePrefs.getFeatures()).andReturn(featureMap).anyTimes();
+
+    // User prefs
+    Map<String, UserPref> specPrefMap = Maps.newLinkedHashMap();
+    for (Map.Entry<String, String> specPref : specPrefs.entrySet()) {
+      UserPref up = createMock(UserPref.class);
+      expect(up.getName()).andReturn(specPref.getKey()).anyTimes();
+      expect(up.getDefaultValue()).andReturn(specPref.getValue()).anyTimes();
+      replay(up);
+      specPrefMap.put(up.getName(),up);
+    }
+    expect(spec.getUserPrefs()).andReturn(specPrefMap).anyTimes();
+    UserPrefs ctxPrefs = new UserPrefs(inPrefs);
+    expect(context.getUserPrefs()).andReturn(ctxPrefs).anyTimes();
+    expect(context.getParameter(Param.REFRESH.getKey())).andReturn(null).anyTimes();
+    expect(currentView.needsUserPrefSubstitution()).andReturn(needsPrefSubst).anyTimes();
+    expect(secondView.needsUserPrefSubstitution()).andReturn(!needsPrefSubst).anyTimes();
+
+    Map<String, View> views = Maps.newHashMap();
+    views.put(VIEW, currentView);
+    views.put(ANOTHER_VIEW, secondView);
+
+    // Link all the mocks together
+    expect(spec.getViews()).andReturn(views).anyTimes();
+    expect(spec.getModulePrefs()).andReturn(modulePrefs).anyTimes();
+    expect(gadget.getCurrentView()).andReturn(currentView).anyTimes();
+    expect(gadget.getSpec()).andReturn(spec).anyTimes();
+    expect(gadget.getContext()).andReturn(context).anyTimes();
+
+    // Replay all
+    replay(currentView, secondView, modulePrefs, spec, context, gadget);
+
+    // Return the gadget
+    return gadget;
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java
new file mode 100644
index 0000000..c9c0629
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/uri/UriUtilsTest.java
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.uri;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+import org.apache.shindig.gadgets.http.HttpResponseBuilder;
+import org.junit.Test;
+
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for UriUtils.
+ */
+public class UriUtilsTest {
+  Enumeration<String> makeEnumeration(String... args) {
+    Vector<String> vector = new Vector<String>();
+    if (args != null) {
+      vector.addAll(Arrays.asList(args));
+    }
+    return vector.elements();
+  }
+
+  private void verifyMime(String requestMime, String responseMime, String expectedMime)
+      throws Exception {
+    String url = "http://example.org/foo";
+    HttpRequest req = new HttpRequest(Uri.parse(url))
+        .setRewriteMimeType(requestMime);
+    HttpResponseBuilder builder = new HttpResponseBuilder()
+        .setHeader("Content-Type", responseMime);
+
+    UriUtils.maybeRewriteContentType(req, builder);
+    assertEquals(expectedMime, builder.getHeader("Content-Type"));
+  }
+
+  @Test
+  public void testMimeMatchPass() throws Exception {
+    verifyMime("text/css", "text/css", "text/css");
+  }
+
+  @Test
+  public void testMimeMatchPassWithAdditionalAttributes() throws Exception {
+    verifyMime("text/css", "text/css; charset=UTF-8", "text/css");
+  }
+
+  @Test
+  public void testNonMatchingMime() throws Exception {
+    verifyMime("text/css", "image/png; charset=UTF-8", "text/css");
+  }
+
+  @Test
+  public void testNonMatchingMimeWithSamePrefix() throws Exception {
+    verifyMime("text/html", "text/plain", "text/html");
+  }
+
+  @Test
+  public void testNonMatchingMimeWithWildCard() throws Exception {
+    verifyMime("text/*", "image/png", "text/*");
+  }
+
+  @Test
+  public void testNonMatchingMimeWithDifferentPrefix() throws Exception {
+    verifyMime("text/*", "text123/html", "text/*");
+  }
+
+  @Test
+  public void testMimeMatchVarySupport() throws Exception {
+    verifyMime("image/*", "image/gif", "image/gif");
+  }
+
+  @Test
+  public void testNullRequestMime() throws Exception {
+    verifyMime(null, "image/png; charset=UTF-8", "image/png; charset=UTF-8");
+  }
+
+  @Test
+  public void testEmptyRequestMime() throws Exception {
+    verifyMime("", "image/png; charset=UTF-8", "image/png; charset=UTF-8");
+  }
+
+  @Test
+  public void testNullResponseMime() throws Exception {
+    verifyMime("text/*", null, "text/*");
+  }
+
+  @Test
+  public void testCopyResponseHeadersAndStatusCode_AddHeader() throws Exception {
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(5000)
+        .addHeader("hello", "world1")
+        .addHeader("hello", "world2")
+        .addHeader("hello\u2297", "bad header")
+        .addHeader("Content-length", "10")
+        .addHeader("vary", "1")
+        .create();
+
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+
+    UriUtils.copyResponseHeadersAndStatusCode(resp, builder, false, false,
+        UriUtils.DisallowedHeaders.OUTPUT_TRANSFER_DIRECTIVES,
+        UriUtils.DisallowedHeaders.CACHING_DIRECTIVES);
+
+    HttpResponse response = builder.create();
+
+    // Date is added by HttpResponse.
+    assertEquals(3, response.getHeaders().size());
+    Iterator<String> headers = response.getHeaders("hello").iterator();
+    assertEquals("world1", headers.next());
+    assertEquals("world2", headers.next());
+    assertEquals(5000, response.getHttpStatusCode());
+  }
+
+  @Test
+  public void testCopyResponseHeadersAndStatusCode_SetHeaders() throws Exception {
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(5000)
+        .addHeader("hello", "world1")
+        .addHeader("hello", "world2")
+        .addHeader("hello\u2297", "bad header")
+        .addHeader("Content-length", "10")
+        .addHeader("vary", "1")
+        .create();
+
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+
+    UriUtils.copyResponseHeadersAndStatusCode(resp, builder, false, true,
+        UriUtils.DisallowedHeaders.OUTPUT_TRANSFER_DIRECTIVES,
+        UriUtils.DisallowedHeaders.CACHING_DIRECTIVES);
+
+    HttpResponse response = builder.create();
+    assertEquals(2, response.getHeaders().size());
+    assertEquals("world2", response.getHeader("hello"));
+    assertEquals(5000, response.getHttpStatusCode());
+  }
+
+  @Test
+  public void testCopyResponseHeadersAndStatusCode_RemapTrue() throws Exception {
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(500)
+        .addHeader("hello", "world1")
+        .addHeader("hello", "world2")
+        .addHeader("hello\u2297", "bad header")
+        .addHeader("Content-length", "10")
+        .addHeader("vary", "1")
+        .create();
+
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+
+    UriUtils.copyResponseHeadersAndStatusCode(resp, builder, true, true,
+        UriUtils.DisallowedHeaders.OUTPUT_TRANSFER_DIRECTIVES,
+        UriUtils.DisallowedHeaders.CACHING_DIRECTIVES);
+
+    HttpResponse response = builder.create();
+    assertEquals(2, response.getHeaders().size());
+    assertEquals("world2", response.getHeader("hello"));
+    assertEquals(502, response.getHttpStatusCode());
+  }
+
+  @Test
+  public void testCopyResponseHeadersAndStatusCode_RemapFalse() throws Exception {
+    HttpResponse resp = new HttpResponseBuilder()
+        .setHttpStatusCode(500)
+        .addHeader("hello", "world1")
+        .addHeader("hello", "world2")
+        .addHeader("hello\u2297", "bad header")
+        .addHeader("Content-length", "10")
+        .addHeader("vary", "1")
+        .create();
+
+    HttpResponseBuilder builder = new HttpResponseBuilder();
+
+    UriUtils.copyResponseHeadersAndStatusCode(resp, builder, false, true,
+        UriUtils.DisallowedHeaders.OUTPUT_TRANSFER_DIRECTIVES,
+        UriUtils.DisallowedHeaders.CACHING_DIRECTIVES);
+
+    HttpResponse response = builder.create();
+    assertEquals(2, response.getHeaders().size());
+    assertEquals("world2", response.getHeader("hello"));
+    assertEquals(500, response.getHttpStatusCode());
+  }
+
+  @Test
+  public void testCopyRequestHeaders() throws Exception {
+    HttpRequest origRequest = new HttpRequest(Uri.parse("http://www.example.org/data.html"));
+
+    Map<String, List<String>> addedHeaders =
+        ImmutableMap.<String, List<String>>builder()
+          .put("h1", ImmutableList.of("v1", "v2"))
+          .put("h2", ImmutableList.of("v3", "v4"))
+          .put("hello\u2297", ImmutableList.of("v5", "v6"))
+          .put("unchanged_header", ImmutableList.<String>of())
+          .put("Content-Length", ImmutableList.of("50", "100"))
+          .build();
+
+    origRequest.addAllHeaders(addedHeaders);
+
+    HttpRequest req = new HttpRequest(Uri.parse(
+        "http://www.example.org/data.html"));
+    req.removeHeader(HttpRequest.DOS_PREVENTION_HEADER);
+    req.addHeader("h1", "hello");
+    req.addHeader("Content-Length", "10");
+    req.addHeader("unchanged_header", "original_value");
+
+    UriUtils.copyRequestHeaders(origRequest, req,
+        UriUtils.DisallowedHeaders.POST_INCOMPATIBLE_DIRECTIVES);
+
+    Map<String, List<String>> headers =
+        ImmutableMap.<String, List<String>>builder()
+        .put("h1", ImmutableList.of("v1", "v2"))
+        .put("h2", ImmutableList.of("v3", "v4"))
+        .put("unchanged_header", ImmutableList.of("original_value"))
+        .put("Content-Length", ImmutableList.of("10"))
+        .put(HttpRequest.DOS_PREVENTION_HEADER, ImmutableList.of("on"))
+        .build();
+
+    assertEquals(headers, req.getHeaders());
+  }
+
+  @Test
+  public void testCopyRequestData() throws Exception {
+    HttpRequest origRequest = new HttpRequest(Uri.parse("http://www.example.com"));
+    origRequest.setMethod("Post");
+
+    String data = "hello world";
+    origRequest.setPostBody(data.getBytes());
+
+    HttpRequest req = new HttpRequest(Uri.parse(
+        "http://www.example.org/data.html"));
+
+    UriUtils.copyRequestData(origRequest, req);
+
+    assertEquals(data, req.getPostBodyAsString());
+  }
+
+  @Test
+  public void testGetContentTypeWithoutCharset() {
+    assertEquals("text/html",
+                 UriUtils.getContentTypeWithoutCharset("text/html"));
+    assertEquals("text/html;",
+                 UriUtils.getContentTypeWithoutCharset("text/html;"));
+    assertEquals("text/html",
+                 UriUtils.getContentTypeWithoutCharset("text/html; charset=hello"));
+    assertEquals("text/html; hello=world",
+                 UriUtils.getContentTypeWithoutCharset("text/html; charset=hello; hello=world"));
+    assertEquals("text/html; pharset=hello; hello=world",
+                 UriUtils.getContentTypeWithoutCharset("text/html; pharset=hello; hello=world"));
+    assertEquals("text/html; charsett=utf; hello=world",
+                 UriUtils.getContentTypeWithoutCharset("text/html; charsett=utf; ; hello=world"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/BidiSubstituterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/BidiSubstituterTest.java
new file mode 100644
index 0000000..2e42abb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/BidiSubstituterTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.render.FakeMessageBundleFactory;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.SpecParserException;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class BidiSubstituterTest extends Assert {
+
+  @Test
+  public void testBidiWithRtl() throws Exception {
+    assertRightToLeft(BidiSubstituter.RTL);
+  }
+
+  @Test
+  public void testBidiWithLtr() throws Exception {
+    assertLeftToRight(BidiSubstituter.LTR);
+  }
+
+  @Test(expected=SpecParserException.class)
+  public void testBidiWithEmpty() throws Exception {
+    assertLeftToRight("");
+  }
+
+  private void assertRightToLeft(String direction) throws Exception {
+    assertSubstitutions(direction, BidiSubstituter.RIGHT,
+        BidiSubstituter.LEFT, BidiSubstituter.RTL, BidiSubstituter.LTR);
+  }
+
+  private void assertLeftToRight(String direction) throws Exception {
+    assertSubstitutions(direction, BidiSubstituter.LEFT,
+        BidiSubstituter.RIGHT, BidiSubstituter.LTR, BidiSubstituter.RTL);
+  }
+
+  private void assertSubstitutions(String direction,
+      String startEdge, String endEdge, String dir, String reverseDir) throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        "  <Locale language_direction='" + direction + "' />" +
+        "</ModulePrefs>" +
+        "<Content />" +
+        "</Module>";
+
+    GadgetSpec spec = new GadgetSpec(Uri.parse("#"), xml);
+    GadgetContext context = new GadgetContext();
+
+    BidiSubstituter substituter = new BidiSubstituter(new FakeMessageBundleFactory());
+
+    Substitutions substitutions = new Substitutions();
+    substituter.addSubstitutions(substitutions, context, spec);
+
+    assertEquals(startEdge, substitutions.getSubstitution(
+        Substitutions.Type.BIDI, BidiSubstituter.START_EDGE));
+    assertEquals(endEdge, substitutions.getSubstitution(
+        Substitutions.Type.BIDI, BidiSubstituter.END_EDGE));
+    assertEquals(dir, substitutions.getSubstitution(
+        Substitutions.Type.BIDI, BidiSubstituter.DIR));
+    assertEquals(reverseDir, substitutions.getSubstitution(
+        Substitutions.Type.BIDI, BidiSubstituter.REVERSE_DIR));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/MessageSubstituterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/MessageSubstituterTest.java
new file mode 100644
index 0000000..cd18845
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/MessageSubstituterTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import org.apache.shindig.gadgets.variables.Substitutions.Type;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.render.FakeMessageBundleFactory;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class MessageSubstituterTest extends Assert {
+  private final FakeMessageBundleFactory messageBundleFactory = new FakeMessageBundleFactory();
+  private final MessageSubstituter substituter = new MessageSubstituter(messageBundleFactory);
+
+  private final GadgetContext context = new GadgetContext();
+
+  @Test
+  public void testMessageReplacements() throws Exception {
+    String xml =
+        "<Module>" +
+        " <ModulePrefs title=''>" +
+        "  <Locale>" +
+        "    <msg name='foo'>bar</msg>" +
+        "    <msg name='bar'>baz</msg>" +
+        "  </Locale>" +
+        " </ModulePrefs>" +
+        " <Content />" +
+        "</Module>";
+
+    Substitutions substitutions = new Substitutions();
+    substituter.addSubstitutions(substitutions, context, new GadgetSpec(Uri.parse("#"), xml));
+
+    assertEquals("bar", substitutions.getSubstitution(Type.MESSAGE, "foo"));
+    assertEquals("baz", substitutions.getSubstitution(Type.MESSAGE, "bar"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/ModuleSubstituterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/ModuleSubstituterTest.java
new file mode 100644
index 0000000..a603f04
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/ModuleSubstituterTest.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.variables.Substitutions.Type;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ModuleSubstituterTest extends Assert {
+  private final Substitutions substitutions = new Substitutions();
+  private GadgetSpec spec;
+
+  @Before
+  public void setUp() throws Exception {
+    spec = new GadgetSpec(Uri.parse("#"), "<Module><ModulePrefs title='' /><Content /></Module>");
+  }
+
+  @Test
+  public void testDefault() throws Exception {
+    ModuleSubstituter substituter = new ModuleSubstituter();
+    substituter.addSubstitutions(substitutions, new GadgetContext(), spec);
+
+    assertEquals("0",
+        substitutions.getSubstitution(Type.MODULE, "ID"));
+  }
+
+  @Test
+  public void testSpecific() throws Exception {
+    final long moduleId = 12345678L;
+
+    ModuleSubstituter substituter = new ModuleSubstituter();
+    substituter.addSubstitutions(substitutions, new GadgetContext() {
+        @Override
+        public long getModuleId() {
+            return moduleId;
+        }
+    }, spec);
+
+    assertEquals(Long.toString(moduleId),
+        substitutions.getSubstitution(Type.MODULE, "ID"));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/SubstitutionsTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/SubstitutionsTest.java
new file mode 100644
index 0000000..a2faae1
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/SubstitutionsTest.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import com.google.common.base.Strings;
+import org.apache.shindig.gadgets.variables.Substitutions.Type;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class SubstitutionsTest extends Assert {
+  private Substitutions subst;
+
+  @Before
+  public void setUp() throws Exception {
+    subst = new Substitutions();
+  }
+
+  @Test
+  public void testMessages() throws Exception {
+    String msg = "Hello, __MSG_world__!";
+    subst.addSubstitution(Type.MESSAGE, "world", "planet");
+    assertEquals("Hello, planet!", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testBidi() throws Exception {
+    String msg = "Hello, __BIDI_DIR__-world!";
+    subst.addSubstitution(Type.BIDI, "DIR", "rtl");
+    assertEquals("Hello, rtl-world!", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testUserPref() throws Exception {
+    String msg = "__UP_hello__, world!";
+    subst.addSubstitution(Type.USER_PREF, "hello", "Greetings");
+    assertEquals("Greetings, world!", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testCorrectOrder() throws Exception {
+    String msg = "__UP_hello__, __MSG_world__!";
+    subst.addSubstitution(Type.MESSAGE, "world",
+        "planet __BIDI_DIR__-__UP_planet__");
+    subst.addSubstitution(Type.BIDI, "DIR", "rtl");
+    subst.addSubstitution(Type.USER_PREF, "hello", "Greetings");
+    subst.addSubstitution(Type.USER_PREF, "planet", "Earth");
+    assertEquals("Greetings, planet rtl-Earth!", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testIncorrectOrder() throws Exception {
+    String msg = "__UP_hello__, __MSG_world__";
+    subst.addSubstitution(Type.MESSAGE, "world",
+        "planet __MSG_earth____UP_punc__");
+    subst.addSubstitution(Type.MESSAGE, "earth", "Earth");
+    subst.addSubstitution(Type.USER_PREF, "punc", "???");
+    subst.addSubstitution(Type.USER_PREF, "hello",
+        "Greetings __MSG_foo____UP_bar__");
+    subst.addSubstitution(Type.MESSAGE, "foo", "FOO!!!");
+    subst.addSubstitution(Type.USER_PREF, "bar", "BAR!!!");
+    assertEquals("Greetings __MSG_foo____UP_bar__, planet __MSG_earth__???",
+        subst.substituteString(msg));
+  }
+
+  @Test
+  public void testDanglingUnderScoresAreIgnored() throws Exception {
+    String msg = "__MSG_hello__, var_msg + '__' + 'world __MSG_world__";
+    subst.addSubstitution(Type.MESSAGE, "hello", "Hello");
+    subst.addSubstitution(Type.MESSAGE, "world", "World");
+
+    assertEquals("Hello, var_msg + '__' + 'world World", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testComplexUnderscores() throws Exception {
+    String msg = "__MSG_hello____________ten____________MSG_world______";
+    subst.addSubstitution(Type.MESSAGE, "hello", "Hello");
+    subst.addSubstitution(Type.MESSAGE, "world", "World");
+
+    assertEquals("Hello__________ten__________World____", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testMessageId() throws Exception {
+    String msg = "Hello, __MODULE_ID__!";
+    subst.addSubstitution(Type.MODULE, "ID", "123");
+    assertEquals("Hello, 123!", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testOddNumberOfPrecedingUnderscores() throws Exception {
+    String msg = "<div id='div___MODULE_ID__'/>";
+    subst.addSubstitution(Type.MODULE, "ID", "123");
+    assertEquals("<div id='div_123'/>", subst.substituteString(msg));
+  }
+
+  @Test
+  public void testOddUnderscoresWithInvalidSubstFollowedByValidSubst() throws Exception {
+    String msg = "<div id='div___HI_THERE__MODULE_ID___'/>";
+    subst.addSubstitution(Type.MODULE, "ID", "123");
+    assertEquals("<div id='div___HI_THERE123_'/>", subst.substituteString(msg));
+  }
+
+  @Test
+  @Ignore("off by default, TODO add test logic")
+  public void loadTest() throws Exception {
+    String msg
+        = "Random text and __UP_hello__, amongst other words __MSG_world__ stuff __weeeeee";
+    subst.addSubstitution(Type.MESSAGE, "world",
+        "planet __MSG_earth____UP_punc__");
+    subst.addSubstitution(Type.MESSAGE, "earth", "Earth");
+    subst.addSubstitution(Type.USER_PREF, "punc", "???");
+    subst.addSubstitution(Type.USER_PREF, "hello",
+        "Greetings __MSG_foo____UP_bar__");
+    subst.addSubstitution(Type.MESSAGE, "foo", "FOO!!!");
+    subst.addSubstitution(Type.USER_PREF, "bar", "BAR!!!");
+
+    // Most real-world content contains very few substitutions.
+    msg += Strings.repeat("foo ", 1000);
+
+    String message = Strings.repeat(msg, 1000);
+
+    long now = System.nanoTime();
+    int cnt = 1000;
+    for (int i = 0; i < cnt; ++i) {
+      subst.substituteString(message);
+    }
+    long duration = System.nanoTime() - now;
+    System.out.println("Duration: " + duration + " avg: " + duration / cnt);
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/UserPrefSubstituterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/UserPrefSubstituterTest.java
new file mode 100644
index 0000000..c459176
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/UserPrefSubstituterTest.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.UserPrefs;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.variables.Substitutions.Type;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+public class UserPrefSubstituterTest extends Assert {
+  private final Substitutions substituter = new Substitutions();
+  private final static String DEFAULT_NAME = "default";
+  private final static String DEFAULT_VALUE = "default value";
+  private final static String USER_NAME = "user";
+  private final static String USER_VALUE = "user value";
+  private final static String OVERRIDE_NAME = "override";
+  private final static String OVERRIDE_VALUE = "override value";
+  private final static String UNESCAPED_USER_VALUE = "<hello, & world > \"";
+  private final static String ESCAPED_USER_VALUE
+      = "&lt;hello, &amp; world &gt; &quot;";
+  private static final String DEFAULT_XML
+      = "<Module>" +
+        "<ModulePrefs title=\"Hello, __UP_world__\"/>" +
+        "<UserPref name=\"" + DEFAULT_NAME + "\" datatype=\"string\"" +
+        " default_value=\"" + DEFAULT_VALUE + "\"/>" +
+        "<UserPref name=\"" + USER_NAME + "\" datatype=\"string\"/>" +
+        "<UserPref name=\"" + OVERRIDE_NAME + "\" datatype=\"string\"" +
+        "  default_value=\"FOOOOOOOOOOBAR!\"/>" +
+        "<Content type=\"html\"/>" +
+        "</Module>";
+  private GadgetSpec spec;
+
+  @Before
+  public void setUp() throws Exception {
+    spec = new GadgetSpec(Uri.parse("#"), DEFAULT_XML);
+  }
+
+  @Test
+  public void testSubstitutions() throws Exception {
+    Map<String, String> map = ImmutableMap.of(USER_NAME, USER_VALUE, OVERRIDE_NAME, OVERRIDE_VALUE);
+    final UserPrefs prefs = new UserPrefs(map);
+    GadgetContext context = new GadgetContext() {
+        @Override
+        public UserPrefs getUserPrefs() {
+            return prefs;
+        }
+    };
+
+    new UserPrefSubstituter().addSubstitutions(substituter, context, spec);
+
+    assertEquals(DEFAULT_VALUE,
+        substituter.getSubstitution(Type.USER_PREF, DEFAULT_NAME));
+    assertEquals(USER_VALUE,
+        substituter.getSubstitution(Type.USER_PREF, USER_NAME));
+    assertEquals(OVERRIDE_VALUE,
+        substituter.getSubstitution(Type.USER_PREF, OVERRIDE_NAME));
+  }
+
+  @Test
+  public void testEscaping() throws Exception {
+    Map<String, String> map = ImmutableMap.of(USER_NAME, UNESCAPED_USER_VALUE);
+    final UserPrefs prefs = new UserPrefs(map);
+    GadgetContext context = new GadgetContext() {
+        @Override
+        public UserPrefs getUserPrefs() {
+            return prefs;
+        }
+    };
+
+    new UserPrefSubstituter().addSubstitutions(substituter, context, spec);
+    assertEquals(ESCAPED_USER_VALUE,
+        substituter.getSubstitution(Type.USER_PREF, USER_NAME));
+  }
+}
diff --git a/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/VariableSubstituterTest.java b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/VariableSubstituterTest.java
new file mode 100644
index 0000000..6d2acfb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/variables/VariableSubstituterTest.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.gadgets.variables;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.gadgets.GadgetContext;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.MessageBundleFactory;
+import org.apache.shindig.gadgets.UserPrefs;
+import org.apache.shindig.gadgets.spec.GadgetSpec;
+import org.apache.shindig.gadgets.spec.LocaleSpec;
+import org.apache.shindig.gadgets.spec.MessageBundle;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+
+import java.util.Locale;
+
+public class VariableSubstituterTest {
+  private final FakeMessageBundleFactory messageBundleFactory = new FakeMessageBundleFactory();
+  private final VariableSubstituter substituter = new VariableSubstituter(ImmutableList.<Substituter>of(
+    new MessageSubstituter(messageBundleFactory),
+    new UserPrefSubstituter(),
+    new ModuleSubstituter(),
+    new BidiSubstituter(messageBundleFactory)
+  ));
+
+  private GadgetSpec substitute(String xml) throws Exception {
+    return substituter.substitute(new GadgetContext(), new GadgetSpec(Uri.parse("#"), xml));
+  }
+
+  @Test
+  public void messageBundlesSubstituted() throws Exception {
+    String xml =
+        "<Module><ModulePrefs title=''>" +
+        "  <Locale>" +
+        "    <msg name='foo'>bar</msg>" +
+        "    <msg name='bar'>baz</msg>" +
+        "  </Locale>" +
+        "</ModulePrefs>" +
+        "<Content>__MSG_foo__ - __MSG_bar__</Content>" +
+        "</Module>";
+    GadgetSpec spec = substitute(xml);
+
+    assertEquals("bar - baz", spec.getView("default").getContent());
+  }
+
+  @Test
+  public void bidiSubstituted() throws Exception {
+    String xml = "<Module><ModulePrefs title='__BIDI_END_EDGE__ way'/><Content/></Module>";
+    GadgetSpec spec = substitute(xml);
+
+    assertEquals("right way", spec.getModulePrefs().getTitle());
+  }
+
+  @Test
+  public void moduleIdSubstituted() throws Exception {
+    String xml = "<Module><ModulePrefs title='Module is: __MODULE_ID__'/><Content/></Module>";
+    GadgetSpec spec = substitute(xml);
+
+    assertEquals("Module is: 0", spec.getModulePrefs().getTitle());
+  }
+
+  @Test
+  public void userPrefsSubstituted() throws Exception {
+    String xml = "<Module>" +
+                 "<ModulePrefs title='I heart __UP_foo__'/>" +
+                 "<UserPref name='foo'/>" +
+                 "<Content/>" +
+                 "</Module>";
+    GadgetSpec spec = new GadgetSpec(Uri.parse("#"), xml);
+    GadgetContext context = new GadgetContext() {
+      @Override
+      public UserPrefs getUserPrefs() {
+        return new UserPrefs(ImmutableMap.of("foo", "shindig"));
+      }
+    };
+
+    spec = substituter.substitute(context, spec);
+
+    assertEquals("I heart shindig", spec.getModulePrefs().getTitle());
+  }
+
+  @Test
+  public void nestedMessageBundleInUserPrefSubstituted() throws Exception {
+    String xml =
+      "<Module>" +
+      " <ModulePrefs title='__UP_title__ for __MODULE_ID__'>" +
+      "  <Locale>" +
+      "   <msg name='title'>Gadget title</msg>" +
+      "  </Locale>" +
+      " </ModulePrefs>" +
+      " <UserPref name='title' default_value='__MSG_title__' />" +
+      " <Content />" +
+      "</Module>";
+
+    GadgetSpec spec = substitute(xml);
+
+    assertEquals("Gadget title for 0", spec.getModulePrefs().getTitle());
+  }
+
+  private static class FakeMessageBundleFactory implements MessageBundleFactory {
+
+    public MessageBundle getBundle(GadgetSpec spec, Locale locale, boolean ignoreCache, String container, String view)
+        throws GadgetException {
+      LocaleSpec localeSpec = spec.getModulePrefs().getLocale(locale, view);
+      if (localeSpec == null) {
+        return MessageBundle.EMPTY;
+      }
+      return localeSpec.getMessageBundle();
+    }
+  }
+}
diff --git a/trunk/java/gadgets/src/test/resources/logging.properties b/trunk/java/gadgets/src/test/resources/logging.properties
new file mode 100644
index 0000000..4ef72e7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/logging.properties
@@ -0,0 +1,41 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+handlers=java.util.logging.ConsoleHandler
+java.util.logging.ConsoleHandler.level=ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+.level=INFO
+
+# Shindig commons
+org.apache.shindig.common.cache.LruCacheProvider.level=${java.util.logging.test.level}
+
+# Shindig gadgets
+org.apache.shindig.gadgets.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.DefaultGadgetSpecFactory.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.DefaultMessageBundleFactory.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.GadgetFeatureRegistry.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.HashLockedDomainService.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.JsLibrary.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.JsFeatureLoader.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.oauth.OAuthResponseParams.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.process.Processor.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.render.HtmlRenderer.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.render.Renderer.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.render.RenderingContentRewriter.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.servlet.MakeRequestServlet.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.servlet.RpcServlet.level=${java.util.logging.test.level}
+org.apache.shindig.gadgets.servlet.ProxyHandler.level=${java.util.logging.test.level}
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/oauth2/oauth2_test.json b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/oauth2/oauth2_test.json
new file mode 100644
index 0000000..947f8e7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/oauth2/oauth2_test.json
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+ 
+{
+   "gadgetBindings" : {
+      "http://www.example.com/1" : {
+         "serviceName" : {
+            "clientName"          : "client1",
+            "allowModuleOverride" : "true"
+         }
+      },
+      "http://www.example.com/2" : {
+          "serviceName" : {
+             "clientName"          : "client2",
+             "allowModuleOverride" : "false"
+          }
+      }      
+   },
+   
+   "clients" : {
+      "client1" : {
+         "providerName"  : "provider1",
+         "redirect_uri"  : "https://www.example.com/gadgets/oauth2callback",
+         "type"          : "confidential",
+         "grant_type"    : "code",
+         "client_id"     : "clientId1",
+         "client_secret" : "dmjfouTfdsfu2",
+         "allowedDomains": ["example.com", "ibm.com"]
+      },
+      
+      "client2" : {
+         "providerName"  : "provider2",
+         "redirect_uri"  : "https://www.example.com/gadgets/oauth2callback",
+         "type"          : "public",
+         "grant_type"    : "client_credentials",
+         "client_id"     : "clientId2",
+         "client_secret" : "dmjfouTfdsfu3"
+      }             
+   },
+   
+   "providers" : {
+      "provider1" : {
+        "client_authentication" : "Basic",   
+        "usesAuthorizationHeader" : "true",    
+        "usesUrlParameter" : "false",
+        "endpoints" : {
+            "authorizationUrl"  : "http://www.example.com/authorize",
+            "tokenUrl"          : "http://www.example.com/token"
+        }
+      },
+      
+      "provider2" : {
+        "client_authentication" : "STANDARD",
+        "usesAuthorizationHeader" : "false",
+        "usesUrlParameter" : "true",       
+        "endpoints" : {
+            "authorizationUrl"   : "http://www.example.com/authorize",
+            "tokenUrl"           : "http://www.example.com/token"
+        }
+      }
+   }
+}
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fragment-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fragment-expected.html
new file mode 100644
index 0000000..0b8d4a9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fragment-expected.html
@@ -0,0 +1,2 @@
+<html><head>
+<style type="text/css"> A { font : bold; }</style></head><body><script>document.write("dont add to head or else")</script></body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fragment.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fragment.html
new file mode 100644
index 0000000..c910c7f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fragment.html
@@ -0,0 +1,2 @@
+<script>document.write("dont add to head or else")</script>
+<style type="text/css"> A { font : bold; }</style>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fulldocnodoctype-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fulldocnodoctype-expected.html
new file mode 100644
index 0000000..d7af346
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fulldocnodoctype-expected.html
@@ -0,0 +1,8 @@
+<html>
+  <head><style>CSS</style></head>
+  <body>
+  <script>function foo(){}</script>
+  <!-- This is a full doc with no doctype -->
+  <div id="mydiv">DIV</div>
+  </body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fulldocnodoctype.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fulldocnodoctype.html
new file mode 100644
index 0000000..8b25888
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-fulldocnodoctype.html
@@ -0,0 +1,6 @@
+<html>
+  <head></head>
+  <body>
+  <!-- This is a full doc with no doctype -->
+  </body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-headnobody-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-headnobody-expected.html
new file mode 100644
index 0000000..6955319
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-headnobody-expected.html
@@ -0,0 +1,3 @@
+<html><head>
+    <!-- A head tag but no body tag is not good -->
+<style type="text/css"> A { font : bold; } </style></head><body><script>document.write("dont add to head or else")</script></body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-headnobody.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-headnobody.html
new file mode 100644
index 0000000..dae08b0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-headnobody.html
@@ -0,0 +1,5 @@
+<head>
+    <!-- A head tag but no body tag is not good -->
+</head>
+<script>document.write("dont add to head or else")</script>
+<style type="text/css"> A { font : bold; } </style>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript-expected.html
new file mode 100644
index 0000000..f8d84d0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript-expected.html
@@ -0,0 +1,11 @@
+<html><head>
+<style>Some CSS here</style>
+
+<link rel="linkrel">
+
+</head><body>
+<script>foo1();</script>
+<script>foo2();</script>
+<script>foo3();</script>
+<div id="mydiv">mycontent</div>
+</body></html>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript.html
new file mode 100644
index 0000000..6766e80
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/nekohtml/test-leadingscript.html
@@ -0,0 +1,6 @@
+<script>foo1();</script>
+<style>Some CSS here</style>
+<script>foo2();</script>
+<link rel="linkrel"/>
+<script>foo3();</script>
+<div id="mydiv">mycontent</div>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-expected.html
new file mode 100644
index 0000000..11f897a
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-expected.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html><head id="head">
+  <link href="http://www.example.org/css.css" rel="stylesheet" type="text/css">
+  <title>An example</title>
+</head><body>
+  <!-- Some comment -->
+    <script type="text/javascript">document.write("&&&")</script>
+    <script src="http://www.example.org/1.js" type="text/javascript"></script>
+    <div>
+      <table><TBODY><tr><td>a cell</td></tr></TBODY></table>
+    </div>
+    <p>Lorem ipsum</p>
+    <a href="/test.html" title="">link</a>
+    <form action="/test/submit">
+      <div>
+        <input type="hidden" value="something">
+        <input type="text">
+      </div>
+      <div><-- An unbalanced tag we dont care about -->
+      <p>Some entities &amp;#x27;&quot;</p>
+      <p>Not a real entity &fake;</p>
+    </div></form>
+</body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment-expected.html
new file mode 100644
index 0000000..0b8d4a9
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment-expected.html
@@ -0,0 +1,2 @@
+<html><head>
+<style type="text/css"> A { font : bold; }</style></head><body><script>document.write("dont add to head or else")</script></body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment.html
new file mode 100644
index 0000000..c910c7f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment.html
@@ -0,0 +1,2 @@
+<script>document.write("dont add to head or else")</script>
+<style type="text/css"> A { font : bold; }</style>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment2-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment2-expected.html
new file mode 100644
index 0000000..0f1ee67
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment2-expected.html
@@ -0,0 +1,2 @@
+<html><head><style type="text/css"> A { background-color : #7f7f7f; } </style>
+</head><body><div>A div</div></body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment2.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment2.html
new file mode 100644
index 0000000..a9f09af
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fragment2.html
@@ -0,0 +1,2 @@
+<style type="text/css"> A { background-color : #7f7f7f; } </style>
+<div>A div</div>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fulldocnodoctype-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fulldocnodoctype-expected.html
new file mode 100644
index 0000000..483fdd4
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fulldocnodoctype-expected.html
@@ -0,0 +1,7 @@
+<html>
+  <head><style>CSS</style><script>function foo(){}</script></head>
+  <body>
+  <!-- This is a full doc with no doctype -->
+  <div id="mydiv">DIV</div>
+  </body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fulldocnodoctype.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fulldocnodoctype.html
new file mode 100644
index 0000000..483fdd4
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-fulldocnodoctype.html
@@ -0,0 +1,7 @@
+<html>
+  <head><style>CSS</style><script>function foo(){}</script></head>
+  <body>
+  <!-- This is a full doc with no doctype -->
+  <div id="mydiv">DIV</div>
+  </body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-headnobody-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-headnobody-expected.html
new file mode 100644
index 0000000..5f90cfd
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-headnobody-expected.html
@@ -0,0 +1,5 @@
+<html><head><style type="text/css"> A { font : bold; } </style></head><body>
+    <!-- A head tag but no body tag is not good -->
+<script>document.write("dont add to head or else")</script>
+
+</body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-headnobody.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-headnobody.html
new file mode 100644
index 0000000..dae08b0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-headnobody.html
@@ -0,0 +1,5 @@
+<head>
+    <!-- A head tag but no body tag is not good -->
+</head>
+<script>document.write("dont add to head or else")</script>
+<style type="text/css"> A { font : bold; } </style>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-socialmarkup.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-socialmarkup.html
new file mode 100644
index 0000000..4a7accc
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-socialmarkup.html
@@ -0,0 +1,19 @@
+<link rel="foo"></link>
+
+<script type="text/os-data" xmlns:os="http://ns.opensocial.org/2008/markup">
+  <os:ViewerRequest key="viewer"/>
+</script>
+
+<script id="template-id" name="template-name" type="text/os-template" tag="template-tag" xmlns:os="http://ns.opensocial.org/2008/markup">
+  <b>Some ${viewer} content</b>
+  <img/>
+  <os:Html/>
+</script>
+
+<script type="text/javascript">
+  <b>Some ${viewer} content</b>
+</script>
+
+<span>Some content</span>
+
+<div><!-- foo -->bar<!-- baz --></div>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-startswithcomment-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-startswithcomment-expected.html
new file mode 100644
index 0000000..bb89212
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-startswithcomment-expected.html
@@ -0,0 +1,7 @@
+<html>
+  <head><style>CSS</style></head>
+  <body>
+  <!-- This is a full doc with no doctype -->
+  <div id="mydiv">DIV</div>
+  </body>
+</html>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-startswithcomment.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-startswithcomment.html
new file mode 100644
index 0000000..34249c6
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-startswithcomment.html
@@ -0,0 +1,8 @@
+<!-- This is a comment -->
+<html>
+  <head><style>CSS</style></head>
+  <body>
+  <!-- This is a full doc with no doctype -->
+  <div id="mydiv">DIV</div>
+  </body>
+</html>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-text-before-script-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-text-before-script-expected.html
new file mode 100644
index 0000000..18e1d47
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-text-before-script-expected.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+</head>
+<body>
+This is text.
+<script>alert('And this is script');</script>    
+</body>
+</html>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-text-before-script.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-text-before-script.html
new file mode 100644
index 0000000..7a2acb2
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-text-before-script.html
@@ -0,0 +1,2 @@
+This is text.
+<script>alert('And this is script');</script>    
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-ampersands-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-ampersands-expected.html
new file mode 100644
index 0000000..a18db75
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-ampersands-expected.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html><head id="head">
+  <link href="http://www.example.org/css.css" rel="stylesheet" type="text/css">
+  <title>An example</title>
+</head><body>
+  <!-- Some comment -->
+  <span title="&amp;lt;">content</span>
+</body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-ampersands.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-ampersands.html
new file mode 100644
index 0000000..9d46e02
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-ampersands.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head id="head">
+  <link href="http://www.example.org/css.css" rel="stylesheet" type="text/css">
+  <title>An example</title>
+</head>
+<body>
+  <!-- Some comment -->
+  <span title="&amp;lt;">content</span>
+</body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-iecond-comments-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-iecond-comments-expected.html
new file mode 100644
index 0000000..2b21e83
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-iecond-comments-expected.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html><head id="head"><link href="http://www.example.org/css.css" rel="stylesheet" type="text/css"><title>An example</title></head><body><!--[if IE 5]>
+  <p>Welcome to Internet Explorer 5.</p>
+  <![endif]--><!--[if IE]><p>You are using Internet Explorer.</p><![endif]--><!--[if !IE]><p>You are not using Internet Explorer.</p><![endif]--><!--[if IE 7]><p>Welcome to Internet Explorer 7!</p><![endif]--><!--[if !(IE 7)]><p>You are not using version 7.</p><![endif]--><!--[if gte IE 7]><p>You are using IE 7 or greater.</p><![endif]--><!--[if (IE 5)]><p>You are using IE 5 (any version).</p><![endif]--><!--[if (gte IE 5.5)&(lt IE 7)]><p>You are using IE 5.5 or IE 6.</p><![endif]--><!--[if lt IE 5.5]><p>Please upgrade your version of Internet Explorer.</p><![endif]--><!--[if true]>You are using an <em>uplevel</em> browser.<![endif]--><!--[if false]>You are using a <em>downlevel</em> browser.<![endif]--><!--[if true]><![if IE 7]><p>This nested comment is displayed in IE 7.</p><![endif]><![endif]--></body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-iecond-comments.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-iecond-comments.html
new file mode 100644
index 0000000..6e8b6ab
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-iecond-comments.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head id="head">
+  <link href="http://www.example.org/css.css" rel="stylesheet" type="text/css">
+  <title>An example</title>
+</head>
+<body>
+  <!--[if IE 5]>
+  <p>Welcome to Internet Explorer 5.</p>
+  <![endif]-->
+
+  <!--[if IE]><p>You are using Internet Explorer.</p><![endif]-->
+  <!--[if !IE]><p>You are not using Internet Explorer.</p><![endif]-->
+  
+  <!--[if IE 7]><p>Welcome to Internet Explorer 7!</p><![endif]-->
+  <!--[if !(IE 7)]><p>You are not using version 7.</p><![endif]-->
+  
+  <!--[if gte IE 7]><p>You are using IE 7 or greater.</p><![endif]-->
+  <!--[if (IE 5)]><p>You are using IE 5 (any version).</p><![endif]-->
+  <!--[if (gte IE 5.5)&(lt IE 7)]><p>You are using IE 5.5 or IE 6.</p><![endif]-->
+  <!--[if lt IE 5.5]><p>Please upgrade your version of Internet Explorer.</p><![endif]-->
+  
+  <!--[if true]>You are using an <em>uplevel</em> browser.<![endif]-->
+  <!--[if false]>You are using a <em>downlevel</em> browser.<![endif]-->
+  
+  <!--[if true]><![if IE 7]><p>This nested comment is displayed in IE 7.</p><![endif]><![endif]-->
+
+  <!-- this standard comment should be removed -->
+</body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-specialtags-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-specialtags-expected.html
new file mode 100644
index 0000000..2961981
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-specialtags-expected.html
@@ -0,0 +1,33 @@
+<html><head><title>An example</title><style type="text/css">
+  <!--
+  #mymap #header {
+    background: #FF9700;
+    clear: both;
+    padding: 2px 0 1px;
+    position: relative;
+    width: 640px;
+  }
+
+  -->
+</style></head><body><script type="text/javascript">document.write("&&&")</script><script src="http://www.example.org/1.js" type="text/javascript"></script><script>
+  // scripts with no old comment hack should be preserved.
+  function a1() {
+    var v1 = 0;
+    alert(" this whitespace should be preserved.");
+  }
+</script><div><table><TBODY><tr><td>a cell</td></tr></TBODY></table></div><script type="text/javascript">
+  <!--
+  // script with old comment hack should be preserved.
+  function MM_goToURL() {
+    var i, args = MM_goToURL.arguments;
+    document.MM_returnValue = false;
+    for (i = 0; i < (args.length - 1); i += 2) eval(args[i] + ".location='" + args[i + 1] + "'");
+  }
+  //-->
+</script><p>Lorem ipsum</p><a href="/test.html" title="">link</a><pre>
+ This is a preformatted block of text,
+ and whitespaces should be preserved.
+ </pre><form action="/test/submit"><div><input type="hidden" value="something"><input type="text"><textarea>
+      This is a preformatted block of text,
+      and whitespaces should be preserved too.
+    </textarea></div></form></body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-specialtags.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-specialtags.html
new file mode 100644
index 0000000..c617a86
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test-with-specialtags.html
@@ -0,0 +1,61 @@
+<html>
+<head>
+  <title>An example</title>
+</head>
+<body>
+<style type="text/css">
+  <!--
+  #mymap #header {
+    background: #FF9700;
+    clear: both;
+    padding: 2px 0 1px;
+    position: relative;
+    width: 640px;
+  }
+
+  -->
+</style>
+<script type="text/javascript">document.write("&&&")</script>
+<script src="http://www.example.org/1.js" type="text/javascript"></script>
+<script>
+  // scripts with no old comment hack should be preserved.
+  function a1() {
+    var v1 = 0;
+    alert(" this whitespace should be preserved.");
+  }
+</script>
+<div>
+  <table>
+    <tr>
+      <td>a cell</td>
+    </tr>
+  </table>
+</div>
+<script type="text/javascript">
+  <!--
+  // script with old comment hack should be preserved.
+  function MM_goToURL() {
+    var i, args = MM_goToURL.arguments;
+    document.MM_returnValue = false;
+    for (i = 0; i < (args.length - 1); i += 2) eval(args[i] + ".location='" + args[i + 1] + "'");
+  }
+  //-->
+</script>
+<p>Lorem ipsum</p>
+<a href="/test.html" title="">link</a>
+ <pre>
+ This is a preformatted block of text,
+ and whitespaces should be preserved.
+ </pre>
+<form action="/test/submit">
+  <div>
+    <input type="hidden" value="something">
+    <input type="text"/>
+    <textarea>
+      This is a preformatted block of text,
+      and whitespaces should be preserved too.
+    </textarea>
+  </div>
+</form>
+</body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test.html
new file mode 100644
index 0000000..779a144
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/parse/test.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head id="head">
+  <link href="http://www.example.org/css.css" rel="stylesheet" type="text/css">
+  <title>An example</title>
+</head>
+<body>
+  <!-- Some comment -->
+    <script type="text/javascript">document.write("&&&")</script>
+    <script src="http://www.example.org/1.js" type="text/javascript"></script>
+    <div>
+      <table><tr><td>a cell</td></tr></table>
+    </div>
+    <p>Lorem ipsum</p>
+    <a href="/test.html" title="">link</a>
+    <form action="/test/submit">
+      <div>
+        <input type="hidden" value="something">
+        <input type="text"/>
+      </div>
+      <div><-- An unbalanced tag we dont care about -->
+      <p>Some entities &amp;#x27;&quot;</p>
+      <p>Not a real entity &fake;</p>
+    </form>
+</body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/OSML_test.xml b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/OSML_test.xml
new file mode 100644
index 0000000..fb2e23b
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/OSML_test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<!-- 
+  An "OSML" library used to test precedence rules.  The OSML library
+  may not override built-in tags, but cannot be overridden by
+  user-defined tags.   Used by TemplateRewriterTest.
+-->
+<Templates xmlns:my="#my">
+  <Namespace prefix="my" url="#my"/>
+  <Template tag='my:Tag1'>osml1</Template>
+  <Template tag='my:Tag2'>osml2</Template>
+</Templates>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/2ndjpgbad.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/2ndjpgbad.jpg
new file mode 100644
index 0000000..c284572
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/2ndjpgbad.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/animated.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/animated.gif
new file mode 100644
index 0000000..a6bc823
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/animated.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/bad.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/bad.jpg
new file mode 100644
index 0000000..16b5e91
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/bad.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc.jpg
new file mode 100644
index 0000000..9aa93a0
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc2.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc2.jpg
new file mode 100644
index 0000000..9af6f96
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc2.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc3.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc3.jpg
new file mode 100644
index 0000000..015b4fb
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc3.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc4.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc4.jpg
new file mode 100644
index 0000000..0103ae4
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/badicc4.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/directcolor.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/directcolor.gif
new file mode 100644
index 0000000..2703744
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/directcolor.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/dog.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/dog.gif
new file mode 100644
index 0000000..c265831
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/dog.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil.bmp b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil.bmp
new file mode 100644
index 0000000..c989dec
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil.bmp
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil.png b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil.png
new file mode 100644
index 0000000..fc28922
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil.png
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil2.bmp b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil2.bmp
new file mode 100644
index 0000000..78aeb78
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/evil2.bmp
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/expand.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/expand.gif
new file mode 100644
index 0000000..c0793e7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/expand.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/huge.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/huge.gif
new file mode 100644
index 0000000..2aab499
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/huge.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/inefficient.png b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/inefficient.png
new file mode 100644
index 0000000..1a37040
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/inefficient.png
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/large.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/large.gif
new file mode 100644
index 0000000..e4db2d7
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/large.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/large.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/large.jpg
new file mode 100644
index 0000000..1e0ba9f
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/large.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/ratio.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/ratio.gif
new file mode 100644
index 0000000..692da71
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/ratio.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/rgbawithnoalpha.png b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/rgbawithnoalpha.png
new file mode 100644
index 0000000..f918900
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/rgbawithnoalpha.png
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/simple.bmp b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/simple.bmp
new file mode 100644
index 0000000..c74a843
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/simple.bmp
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/small.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/small.jpg
new file mode 100644
index 0000000..b154012
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/small.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg
new file mode 100644
index 0000000..f9c5be1
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage420.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg
new file mode 100644
index 0000000..a534941
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImage444.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg
new file mode 100644
index 0000000..e6b34ee
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/testImageNotHuffmanOptimized.jpg
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/unanimated.gif b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/unanimated.gif
new file mode 100644
index 0000000..cfbf7ac
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/image/unanimated.gif
Binary files differ
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritebasic-expected.css b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritebasic-expected.css
new file mode 100644
index 0000000..1d97759
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritebasic-expected.css
@@ -0,0 +1,8 @@
+@import url('//www.test.com/dir/proxy?container=default&gadget=http%3A%2F%2Fwww.example.org%2Fpath%2Frewritebasic.css&debug=0&nocache=0&url=http%3A%2F%2Fwww.example.org%2Fother1.css');
+@import url('//www.test.com/dir/proxy?container=default&gadget=http%3A%2F%2Fwww.example.org%2Fpath%2Frewritebasic.css&debug=0&nocache=0&url=http%3A%2F%2Fwww.example.org%2Fpath%2Frelative%2Fother2.css');
+@import url('http://www.example.org/hostrelative/excluded/other1.css');
+
+DiV {
+  font: arial;
+  background-image : url('//www.test.com/dir/proxy?container=default&gadget=http%3A%2F%2Fwww.example.org%2Fpath%2Frewritebasic.css&debug=0&nocache=0&url=http%3A%2F%2Fwww.some.site%2Fimage.gif');
+}
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritebasic.css b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritebasic.css
new file mode 100644
index 0000000..b8b6224
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritebasic.css
@@ -0,0 +1,8 @@
+@import url(http://www.example.org/other1.css);
+@import url("relative/other2.css");
+@import url('/hostrelative/excluded/other1.css');
+
+DiV {
+  font: arial;
+  background-image : url("http://www.some.site/image.gif");
+}
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritelinksbasic.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritelinksbasic.html
new file mode 100644
index 0000000..9aec000
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritelinksbasic.html
@@ -0,0 +1,11 @@
+<html>
+  <head>
+  </head>
+  <body>
+    <!-- Basic URLs with exclusion matching -->
+    <img src="/img.gif">
+    <img src="/excluded/img.gif">
+    <embed src="http://www.example.org/some.swf">Content</embed>
+    <embed src="http://www.example.org/excluded/some.swf"></embed>
+  </body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritescriptbasic.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritescriptbasic.html
new file mode 100644
index 0000000..30cebff
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritescriptbasic.html
@@ -0,0 +1,41 @@
+<html>
+  <head>
+    <script type="text/javascript">headScript1</script>
+  </head>
+  <body>
+    <!-- Test to make sure script tags arent merged across intervening content and
+    contiguous script src=xxx are concatenated. 
+    -->
+    <script type="text/javascript">bodyScript1</script>
+    <script type="text/javascript" src="http://www.example.org/1.js"></script>
+
+
+    <script type="text/javascript" src="/2.js"></script>
+    <script type="text/javascript">bodyScript2</script>
+    <script type="text/javascript" src="http://www.example.org/3.js"></script>
+    <div>untouched</div>
+    <script type="text/javascript"><!-- retain-comment --></script>
+    <script type="text/javascript" src="http://www.example.org/4.js"></script>
+    <script type="text/javascript" src="http://www.example.org/excluded/5.js"></script>
+    <script type="text/javascript" src="http://www.example.org/6.js"></script>
+    
+    <!-- More added to exceed MAX_URL_LENGTH=1500 requirement  -->
+    <script type="text/javascript" src="http://www.example.org/10.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/11.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/12.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/13.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/14.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/15.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/16.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/17.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/18.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/19.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/20.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/21.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/22.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/23.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/23-long.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/24.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+    <script type="text/javascript" src="http://www.example.org/25.js&0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"></script>
+  </body>
+</html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestyle2-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestyle2-expected.html
new file mode 100644
index 0000000..8e2ccb6
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestyle2-expected.html
@@ -0,0 +1,11 @@
+<html><head><style>
+  .testclass {
+    background-image: url('http://www.test.com/dir/proxy?url=http%3A%2F%2Fwww.example.org%2Fimage.jpg&gadget=http%3A%2F%2Fwww.example.org%2Fdir%2Fg.xml&fp=1150739864&refresh=3600');
+  }
+</style></head><body>
+<div class="testclass">Background image Style</div><br>
+<div style="background-image:url(http://www.example.org/image2.jpg);">Background image Inline
+</div><br>
+<script type="text/javascript">
+  somescript();
+</script></body></html>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestyle2.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestyle2.html
new file mode 100644
index 0000000..09b8ffc
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestyle2.html
@@ -0,0 +1,11 @@
+<style>
+  .testclass {
+    background-image: url( http://www.example.org/image.jpg );
+  }
+</style>
+<div class="testclass">Background image Style</div><br/>
+<div style="background-image:url(http://www.example.org/image2.jpg);">Background image Inline
+</div><br/>
+<script type="text/javascript">
+  somescript();
+</script>
\ No newline at end of file
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylebasic.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylebasic.html
new file mode 100644
index 0000000..b244008
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylebasic.html
@@ -0,0 +1,19 @@
+<html>
+  <head>
+    <link rel="stylesheet" type="text/css" href="http://www.example.org/linkedstyle1.css">
+    <link rel="stylesheet" type="text/css" href="http://www.example.org/excluded/linkedstyle2.css">
+    <link rel="stylesheet" type="text/css" href="http://www.example.org/linkedstyle3.css">
+  </head>
+  <body>
+    <style>
+      @import url("/importedstyle1.css");
+      @import url("/excluded/importedstyle2.css");
+      @import url("/importedstyle3.css");
+    </style>
+    <div>somecontent</div>
+    <style>
+      @import url("/importedstyle4.css");
+      div { color : black; }
+    </style>
+  </body>
+</html>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylemedia-expected.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylemedia-expected.html
new file mode 100644
index 0000000..25b43d5
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylemedia-expected.html
@@ -0,0 +1,10 @@
+<html>
+  <head>
+    <link href="http://www.test.com/dir/concat?rewriteMime=text/css&gadget=http%3A%2F%2Fwww.example.org%2Fdir%2Fg.xml&fp=1150739864&container=default&refresh=3600&1=http%3A%2F%2Fwww.example.org%2Flinkedstyle1.css" rel="stylesheet" type="text/css">
+    <link href="http://www.test.com/dir/concat?rewriteMime=text/css&gadget=http%3A%2F%2Fwww.example.org%2Fdir%2Fg.xml&fp=1150739864&container=default&refresh=3600&1=http%3A%2F%2Fwww.example.org%2Flinkedstyle2.css" media="screen" rel="stylesheet" type="text/css">
+    <link href="http://www.test.com/dir/concat?rewriteMime=text/css&gadget=http%3A%2F%2Fwww.example.org%2Fdir%2Fg.xml&fp=1150739864&container=default&refresh=3600&1=http%3A%2F%2Fwww.example.org%2Flinkedstyle3.css" media="print" rel="stylesheet" type="text/css">
+  </head>
+  <body>
+    <div>somecontent</div>
+  </body>
+</html>
diff --git a/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylemedia.html b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylemedia.html
new file mode 100644
index 0000000..28d0907
--- /dev/null
+++ b/trunk/java/gadgets/src/test/resources/org/apache/shindig/gadgets/rewrite/rewritestylemedia.html
@@ -0,0 +1,10 @@
+<html>
+  <head>
+    <link rel="stylesheet" type="text/css" href="http://www.example.org/linkedstyle1.css">
+    <link media="screen" rel="stylesheet" type="text/css" href="http://www.example.org/linkedstyle2.css">
+    <link media="print" rel="stylesheet" type="text/css" href="http://www.example.org/linkedstyle3.css">
+  </head>
+  <body>
+    <div>somecontent</div>
+  </body>
+</html>
diff --git a/trunk/java/sample-container/pom.xml b/trunk/java/sample-container/pom.xml
new file mode 100644
index 0000000..f6e002a
--- /dev/null
+++ b/trunk/java/sample-container/pom.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-sample-container</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Apache Shindig Sample Container</name>
+  <description>Default Shindig Sample Container module</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/sample-container</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/sample-container</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/server</url>
+  </scm>
+
+  <dependencies>
+    <!-- project dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-gadgets</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-social-api</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+
+    <!-- external dependencies -->
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>    
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-multibindings</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shiro</groupId>
+      <artifactId>shiro-web</artifactId>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/container/SampleContainerGuiceModule.java b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/container/SampleContainerGuiceModule.java
new file mode 100644
index 0000000..3011b63
--- /dev/null
+++ b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/container/SampleContainerGuiceModule.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.sample.container;
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Names;
+
+public class SampleContainerGuiceModule extends AbstractModule {
+
+  protected void configure() {
+    // We do this so that jsecurity realms can get access to the jsondbservice singleton
+
+    Multibinder<Object> handlerBinder = Multibinder.newSetBinder(binder(), Object.class, Names.named("org.apache.shindig.handlers"));
+    handlerBinder.addBinding().toInstance(SampleContainerHandler.class);
+  }
+}
diff --git a/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/container/SampleContainerHandler.java b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/container/SampleContainerHandler.java
new file mode 100644
index 0000000..848277a
--- /dev/null
+++ b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/container/SampleContainerHandler.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.sample.container;
+
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.sample.spi.JsonDbOpensocialService;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.http.HttpFetcher;
+import org.apache.shindig.gadgets.http.HttpRequest;
+import org.apache.shindig.gadgets.http.HttpResponse;
+
+
+import java.util.concurrent.Future;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.inject.Inject;
+
+@Service(name = "samplecontainer", path = "/{type}/{doevil}")
+public class SampleContainerHandler {
+
+  private final JsonDbOpensocialService service;
+  private final HttpFetcher fetcher;
+  @Inject
+  public SampleContainerHandler(JsonDbOpensocialService dbService, HttpFetcher fetcher) {
+    this.service = dbService;
+    this.fetcher = fetcher;
+  }
+
+  /**
+   * We don't distinguish between put and post for these urls.
+   */
+  @Operation(httpMethods = "PUT")
+  public Future<?> update(RequestItem request) throws ProtocolException {
+    return create(request);
+  }
+
+  /**
+   * Handles /samplecontainer/setstate and /samplecontainer/setevilness/{doevil}. TODO(doll): These
+   * urls aren't very resty. Consider changing the samplecontainer.html calls post.
+   */
+  @Operation(httpMethods = "POST", bodyParam = "data")
+  public Future<?> create(RequestItem request) throws ProtocolException {
+    String type = request.getParameter("type");
+    if ("setstate".equals(type)) {
+      try {
+        @SuppressWarnings("unchecked")
+        Map<String, String> bodyparams = request.getTypedParameter("data", Map.class);
+        String stateFile = bodyparams.get("fileurl");
+        service.setDb(new JSONObject(fetchStateDocument(stateFile)));
+      } catch (JSONException e) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+            "The json state file was not valid json", e);
+      }
+    } else if ("setevilness".equals(type)) {
+      throw new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+          "evil data has not been implemented yet");
+    }
+
+    return Futures.immediateFuture(null);
+  }
+
+  /**
+   * Handles /samplecontainer/dumpstate
+   */
+  @Operation(httpMethods = "GET")
+  public Future<?> get(RequestItem request) {
+    return Futures.immediateFuture(service.getDb());
+  }
+
+  private String fetchStateDocument(String stateFileLocation) {
+    String errorMessage = "The json state file " + stateFileLocation
+        + " could not be fetched and parsed.";
+
+    try {
+      HttpResponse response = fetcher.fetch(new HttpRequest(Uri.parse(stateFileLocation)));
+      if (response.getHttpStatusCode() != 200) {
+        throw new RuntimeException(errorMessage);
+      }
+      return response.getResponseAsString();
+    } catch (GadgetException e) {
+      throw new RuntimeException(errorMessage, e);
+    }
+  }
+}
diff --git a/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/shiro/SampleShiroRealm.java b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/shiro/SampleShiroRealm.java
new file mode 100644
index 0000000..0c14c45
--- /dev/null
+++ b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/shiro/SampleShiroRealm.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.sample.shiro;
+
+import org.apache.shindig.social.sample.spi.JsonDbOpensocialService;
+import org.apache.shiro.authc.AccountException;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationInfo;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authc.SimpleAuthenticationInfo;
+import org.apache.shiro.authc.UsernamePasswordToken;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.AuthorizationInfo;
+import org.apache.shiro.authz.SimpleAuthorizationInfo;
+import org.apache.shiro.realm.AuthorizingRealm;
+import org.apache.shiro.subject.PrincipalCollection;
+
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Inject;
+
+/**
+ * A Sample Shiro Realm that uses the JSON DB to get passwords
+ *
+ */
+public class SampleShiroRealm extends AuthorizingRealm {
+  // HACK, apache.shiro relies upon no-arg constructors..
+  @Inject
+  private static JsonDbOpensocialService jsonDbService;
+
+  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
+    UsernamePasswordToken upToken = (UsernamePasswordToken) token;
+    String username = upToken.getUsername();
+
+    // Null username is invalid
+    if (username == null) {
+        throw new AccountException("Null usernames are not allowed by this realm.");
+    }
+    String password = jsonDbService.getPassword(username);
+
+    return  new SimpleAuthenticationInfo(username, password, this.getName());
+  }
+
+  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
+    //null usernames are invalid
+    if (principals == null) {
+      throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
+    }
+
+    String username = (String) principals.fromRealm(getName()).iterator().next();
+
+
+    Set<String> roleNames;
+
+    if (username == null) {
+      roleNames = ImmutableSet.of();
+    } else {
+      roleNames = ImmutableSet.of("foo", "goo");
+    }
+
+    return new SimpleAuthorizationInfo(roleNames);
+  }
+
+}
diff --git a/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/shiro/ShiroGuiceModule.java b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/shiro/ShiroGuiceModule.java
new file mode 100644
index 0000000..105a1a2
--- /dev/null
+++ b/trunk/java/sample-container/src/main/java/org/apache/shindig/sample/shiro/ShiroGuiceModule.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.sample.shiro;
+import com.google.inject.AbstractModule;
+
+public class ShiroGuiceModule extends AbstractModule {
+
+  protected void configure() {
+    // We do this so that jsecurity realms can get access to the jsondbservice singleton
+    requestStaticInjection(SampleShiroRealm.class);
+  }
+}
diff --git a/trunk/java/sample-maven-archetype/pom.xml b/trunk/java/sample-maven-archetype/pom.xml
new file mode 100644
index 0000000..04f2964
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>sample-maven-archetype</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>maven-archetype</packaging>
+
+  <name>Apache Shindig Sample Web App Maven Archetype</name>
+  <description>Default server war dependencies</description>
+
+  <build>
+    <!--
+        Replace some property values in the archetype pom upon generation of the archetype, see
+        http://stackoverflow.com/questions/7223031/how-to-embed-archetype-project-version-in-maven-archetype
+    -->
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+        <includes>
+          <include>archetype-resources/pom.xml</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>false</filtering>
+        <excludes>
+          <exclude>archetype-resources/pom.xml</exclude>
+        </excludes>
+      </resource>
+    </resources>
+    <extensions>
+      <extension>
+        <groupId>org.apache.maven.archetype</groupId>
+        <artifactId>archetype-packaging</artifactId>
+        <version>2.2</version>
+      </extension>
+    </extensions>
+
+    <pluginManagement>
+      <plugins>
+        <plugin>
+          <artifactId>maven-archetype-plugin</artifactId>
+          <version>2.2</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-resources-plugin</artifactId>
+          <version>2.5</version>
+          <configuration>
+            <escapeString>\</escapeString>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+  </build>
+
+  <url>http://shindig.apache.org</url>
+</project>
diff --git a/trunk/java/sample-maven-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/trunk/java/sample-maven-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
new file mode 100644
index 0000000..4fc94ca
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<archetype-descriptor xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0 http://maven.apache.org/xsd/archetype-descriptor-1.0.0.xsd" name="shindig-sample"
+    xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <fileSets>
+    <fileSet filtered="true" encoding="UTF-8">
+      <directory>src/main/webapp</directory>
+      <includes>
+        <include>**/*.html</include>
+        <include>**/*.xml</include>
+      </includes>
+    </fileSet>
+    <fileSet filtered="true" encoding="UTF-8">
+      <directory>src/main/resources</directory>
+      <includes>
+        <include>**/*.properties</include>
+      </includes>
+    </fileSet>
+    <fileSet encoding="UTF-8">
+      <directory>src/main/webresources</directory>
+      <includes>
+        <include>**/*.css</include>
+      </includes>
+    </fileSet>
+  </fileSets>
+</archetype-descriptor>
diff --git a/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/pom.xml b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/pom.xml
new file mode 100644
index 0000000..b66bb65
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<!--

+ * Licensed to the Apache Software Foundation (ASF) under one

+ * or more contributor license agreements.  See the NOTICE file

+ * distributed with this work for additional information

+ * regarding copyright ownership.  The ASF licenses this file

+ * to you 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.

+-->

+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

+  <modelVersion>4.0.0</modelVersion>

+  <groupId>\${groupId}</groupId>

+  <artifactId>\${artifactId}</artifactId>

+  <version>\${version}</version>

+  <packaging>war</packaging>

+  <name>Demo Webapp for Apache Shindig</name>

+  <url>http://shindig.apache.org</url>

+  <build>

+    <finalName>shindig-sample</finalName>

+

+    <plugins>

+

+      <plugin>

+	    <groupId>org.mortbay.jetty</groupId>

+	    <artifactId>maven-jetty-plugin</artifactId>

+	    <configuration>

+		  <tempDirectory>${basedir}/target/work</tempDirectory>

+		  <contextPath>/</contextPath>

+	    </configuration>

+      </plugin>

+

+      <plugin>

+        <groupId>org.apache.maven.plugins</groupId>

+        <artifactId>maven-war-plugin</artifactId>

+        <configuration>

+          <webResources>

+            <resource>

+              <directory>${basedir}/src/main/webresources</directory>

+            </resource>

+          </webResources>

+        </configuration>

+      </plugin>

+

+    </plugins>

+  </build>

+

+  <dependencies>

+    <dependency>

+      <groupId>org.apache.shindig</groupId>

+      <artifactId>shindig-features</artifactId>

+      <version>${project.version}</version>

+    </dependency>

+    <dependency>

+      <groupId>org.apache.shindig</groupId>

+      <artifactId>shindig-common</artifactId>

+      <version>${project.version}</version>

+    </dependency>

+    <dependency>

+      <groupId>org.apache.shindig</groupId>

+      <artifactId>shindig-gadgets</artifactId>

+      <version>${project.version}</version>

+    </dependency>

+    <dependency>

+      <groupId>org.apache.shindig</groupId>

+      <artifactId>shindig-social-api</artifactId>

+      <version>${project.version}</version>

+    </dependency>

+    <dependency>

+      <groupId>org.apache.shindig</groupId>

+      <artifactId>shindig-extras</artifactId>

+      <version>${project.version}</version>

+    </dependency>

+  </dependencies>

+

+</project>

diff --git a/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/resources/shindig.properties b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/resources/shindig.properties
new file mode 100644
index 0000000..8e4d6a3
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/resources/shindig.properties
@@ -0,0 +1,214 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+# Location of feature manifests (comma separated)
+shindig.features.default=res://features/features.txt
+
+# Location of container configurations (comma separated)
+shindig.containers.default=res://containers/default/container.js
+
+### Inbound OAuth support
+# The URL base to use for full OAuth support (three-legged)
+shindig.oauth.base-url=/oauth
+shindig.oauth.authorize-action=/WEB-INF/authorize.jsp
+# The range to the past and future of timestamp for OAuth token validation. Default to 5 minutes
+shindig.oauth.validator-max-timestamp-age-ms=300000
+
+### Outbound OAuth support
+shindig.signing.state-key=
+shindig.signing.key-name=
+shindig.signing.key-file=
+shindig.signing.global-callback-url=http://%authority%%contextRoot%/gadgets/oauthcallback
+shindig.signing.enable-signed-callbacks=true
+
+### If a OAuth2Client does not specify a redirect uri it will default here
+shindig.oauth2.global-redirect-uri=http://%authority%%contextRoot%/gadgets/oauth2callback
+### Setting to true will cause the registered OAuth2Persistence plugin to load it's values
+### with what's in config/oauth2.json, no meaning without a second persistence implementation.
+shindig.oauth2.import=false
+### Determines if the import will start by removing everything currently in persistence.
+shindig.oauth2.import.clean=false
+# Set to true if you want to allow the use of 3-party (authorization_code) OAuth 2.0 flow when viewer != owner.
+# This setting is not recommeneded for pages that allow user-controlled javascript, since
+# that javascript could be used to make unauthorized requests on behalf of the viewer of the page
+shindig.oauth2.viewer-access-tokens-enabled=true
+# Set to true to send extended trace messages to the client.  Probably want this to be false for
+# production systems and true for test/development.
+shindig.oauth2.send-trace-to-client=true
+shindig.signing.oauth2.state-key=
+
+# Set to true if you want to allow the use of 3-legged OAuth tokens when viewer != owner.
+# This setting is not recommeneded for pages that allow user-controlled javascript, since
+# that javascript could be used to make unauthorized requests on behalf of the viewer of the page
+shindig.signing.viewer-access-tokens-enabled=false
+
+# If enabled here, configuration values can be found in container configuration files.
+shindig.locked-domain.enabled=false
+
+# TODO: This needs to be moved to container configuration.
+shindig.content-rewrite.only-allow-excludes=false
+shindig.content-rewrite.include-urls=.*
+shindig.content-rewrite.exclude-urls=
+shindig.content-rewrite.include-tags=body,embed,img,input,link,script,style
+shindig.content-rewrite.expires=86400
+shindig.content-rewrite.enable-split-js-concat=true
+shindig.content-rewrite.enable-single-resource-concat=false
+
+#
+# Default set of forced libs to allow for better caching
+#
+# NOTE: setting this causes the EndToEnd test to fail the opensocial-templates test
+shindig.gadget-rewrite.default-forced-libs=core:rpc
+#shindig.gadget-rewrite.default-forced-libs=
+
+#
+# Allow supported JavaScript features required by a gadget to be externalized on demand
+shindig.gadget-rewrite.externalize-feature-libs=true
+
+# Configuration for image rewriter
+shindig.image-rewrite.max-inmem-bytes = 1048576
+shindig.image-rewrite.max-palette-size = 256
+shindig.image-rewrite.allow-jpeg-conversion = true
+shindig.image-rewrite.jpeg-compression = 0.90
+shindig.image-rewrite.min-threshold-bytes = 200
+shindig.image-rewrite.jpeg-retain-subsampling = false
+# Huffman optimization reduces the images size by addition 4-6% without
+# any loss in the quality of the image, but takes extra cpu cycles for
+# computing the optimized huffman tables.
+shindig.image-rewrite.jpeg-huffman-optimization = false
+
+# Configuration for the os:Flash tag
+shindig.flash.min-version = 9.0.115
+
+# Configuration for template rewriter
+shindig.template-rewrite.extension-tag-namespace=http://ns.opensocial.org/2009/extensions
+
+# These values provide default TTLs (in ms) for HTTP responses that don't use caching headers.
+shindig.cache.http.defaultTtl=3600000
+shindig.cache.http.negativeCacheTtl=60000
+
+# Amount of time after which the entry in cache should be considered for a refetch for a
+# non-userfacing internal fetch when the response is strict-no-cache.
+shindig.cache.http.strict-no-cache-resource.refetch-after-ms=-1
+
+# A default refresh interval for XML files, since there is no natural way for developers to
+# specify this value, and most HTTP responses don't include good cache control headers.
+shindig.cache.xml.refreshInterval=300000
+
+# Add entries in the form shindig.cache.lru.<name>.capacity to specify capacities for different
+# caches when using the LruCacheProvider.
+# It is highly recommended that the EhCache implementation be used instead of the LRU cache.
+shindig.cache.lru.default.capacity=1000
+shindig.cache.lru.expressions.capacity=1000
+shindig.cache.lru.gadgetSpecs.capacity=1000
+shindig.cache.lru.messageBundles.capacity=1000
+shindig.cache.lru.httpResponses.capacity=10000
+
+# The location of the EhCache configuration file.
+shindig.cache.ehcache.config=res://org/apache/shindig/common/cache/ehcache/ehcacheConfig.xml
+
+# The location of the filter file for EhCache's SizeOfEngine
+# This gets set as a system property to be consumed by EhCache.
+# Can be a resource on the classpath or a path on the file system.
+shindig.cache.ehcache.sizeof.filter=res://org/apache/shindig/common/cache/ehcache/SizeOfFilter.txt
+
+# true to enable JMX integration.
+shindig.cache.ehcache.jmx.enabled=true
+
+# true to enable JMX stats.
+shindig.cache.ehcache.jmx.stats=true
+
+# true to skip expensive encoding detection.
+# if true, will only attempt to validate utf-8. Assumes all other encodings are ISO-8859-1.
+shindig.http.fast-encoding-detection=true
+
+# Configuration for the HttpFetcher
+# Connection timeout, in milliseconds, for requests.
+shindig.http.client.connection-timeout-ms=5000
+
+# Maximum size, in bytes, of the object we fetched, 0 == no limit
+shindig.http.client.max-object-size-bytes=0
+
+# Strict-mode parsing for proxy and concat URIs ensures that the authority/host and path
+# for the URIs match precisely what is found in the container config for it. This is
+# useful where statistics and traffic routing patterns, typically in large installations,
+# key on hostname (and occasionally path). Enforcing this does come at the cost that
+# mismatches break, which in turn mandates that URI generation always happen in consistent
+# fashion, ie. by the class itself or tightly controlled code.
+shindig.uri.proxy.use-strict-parsing=false
+shindig.uri.concat.use-strict-parsing=false
+
+# Host:port of the proxy to use while fetching urls. Leave blank if proxy is
+# not to be used.
+org.apache.shindig.gadgets.http.basicHttpFetcherProxy=
+
+org.apache.shindig.serviceExpirationDurationMinutes=60
+
+#
+# Older versions of shindig used 'data' in the json-rpc response format
+# The spec calls for using 'result' instead, however to avoid breakage we
+# allow you to set it back to the old way here
+#
+# valid values are
+#  result  - new form
+#  data    - old broken form
+#  both    - return both fields for full compatibility
+#
+shindig.json-rpc.result-field=result
+
+# Remap "Internal server error"s received from the basicHttpFetcherProxy server to
+# "Bad Gateway error"s, so that it is clear to the user that the proxy server is
+# the one that threw the exception.
+shindig.accelerate.remapInternalServerError=true
+shindig.proxy.remapInternalServerError=true
+
+# Add debug data when using VanillaCajaHtmlParser.
+vanillaCajaParser.needsDebugData=true
+
+# Allow non-SSL OAuth 2.0 bearer tokens
+org.apache.shindig.auth.oauth2-require-ssl=false
+
+# Set gadget param in proxied uri as authority if this is true
+org.apache.shindig.gadgets.uri.setAuthorityAsGadgetParam=false
+
+# Maximum Get Url size limit
+org.apache.shindig.gadgets.uri.urlMaxLength=2048
+
+# Default cachettl value for versioned url in seconds. Here default value is 1 year.
+org.apache.shindig.gadgets.servlet.longLivedRefreshSec=31536000
+
+# Closure compiler optimization level.  One of advanced|simple|whitespace_only|none.
+# Defaults to simple.
+shindig.closure.compile.level=simple
+
+# Size of the compiler thread pool
+shindig.closure.compile.threadPoolSize=5
+
+# OAuth 2.0 authorization code, access token, and refresh token expiration times.
+# 5 * 60 * 1000 = 300000 = 5 minutes
+# 5 * 60 * 60 * 1000 = 18000000 = 5 hours
+# 5 * 60 * 60 * 1000 * 24 = 432000000 = 5 days
+shindig.oauth2.authCodeExpiration=300000
+shindig.oauth2.accessTokenExpiration=18000000
+shindig.oauth2.refreshTokenExpiration=432000000
+
+# Allows unauthenticated requests to Shindig
+shindig.allowUnauthenticated=true
+
+# Comma separated tags that need to have its relative path to be resolved as absolute.
+# Possible values are RESOURCES and HYPERLINKS
+shindig.gadgets.rewriter.absolutePath.tags=RESOURCES
diff --git a/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..3d84f1e
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<web-app>
+
+  <display-name>Archetype Created Web Application</display-name>
+
+    <context-param>
+        <param-name>guice-modules</param-name>
+        <param-value>
+            org.apache.shindig.common.PropertiesModule:
+            org.apache.shindig.gadgets.DefaultGuiceModule:
+            org.apache.shindig.gadgets.oauth.OAuthModule:
+            org.apache.shindig.gadgets.oauth2.OAuth2Module:
+            org.apache.shindig.gadgets.oauth2.OAuth2MessageModule:
+            org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerModule:
+            org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2PersistenceModule:
+            org.apache.shindig.gadgets.admin.GadgetAdminModule
+        </param-value>
+    </context-param>
+
+    <!--
+    <context-param>
+      <param-name>system.properties</param-name>
+      <param-value>
+        shindig.host=localhost
+        aKey=/shindig/gadgets/proxy?container=default&amp;url=
+        shindig.port=8380
+      </param-value>
+    </context-param>
+    -->
+
+    <listener>
+        <listener-class>
+            org.apache.shindig.common.servlet.GuiceServletContextListener
+        </listener-class>
+    </listener>
+
+    <!-- Render a Gadget -->
+    <servlet>
+        <servlet-name>xml-to-html</servlet-name>
+        <servlet-class>
+            org.apache.shindig.gadgets.servlet.GadgetRenderingServlet
+        </servlet-class>
+    </servlet>
+
+    <!-- Proxy -->
+    <servlet>
+        <servlet-name>proxy</servlet-name>
+        <servlet-class>
+            org.apache.shindig.gadgets.servlet.ProxyServlet
+        </servlet-class>
+    </servlet>
+
+    <servlet>
+        <servlet-name>concat</servlet-name>
+        <servlet-class>
+            org.apache.shindig.gadgets.servlet.ConcatProxyServlet
+        </servlet-class>
+    </servlet>
+
+    <!-- OAuth callback -->
+    <servlet>
+        <servlet-name>oauthCallback</servlet-name>
+        <servlet-class>
+            org.apache.shindig.gadgets.servlet.OAuthCallbackServlet
+        </servlet-class>
+    </servlet>
+
+    <!-- Metadata RPC -->
+    <servlet>
+        <servlet-name>metadata</servlet-name>
+        <servlet-class>
+            org.apache.shindig.gadgets.servlet.RpcServlet
+        </servlet-class>
+    </servlet>
+
+    <!-- javascript serving -->
+    <servlet>
+        <servlet-name>js</servlet-name>
+        <servlet-class>
+            org.apache.shindig.gadgets.servlet.JsServlet
+        </servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>js</servlet-name>
+        <url-pattern>/gadgets/js/*</url-pattern>
+    </servlet-mapping>
+
+    <servlet-mapping>
+        <servlet-name>proxy</servlet-name>
+        <url-pattern>/gadgets/proxy/*</url-pattern>
+    </servlet-mapping>
+
+    <servlet-mapping>
+        <servlet-name>concat</servlet-name>
+        <url-pattern>/gadgets/concat</url-pattern>
+    </servlet-mapping>
+
+    <servlet-mapping>
+        <servlet-name>oauthCallback</servlet-name>
+        <url-pattern>/gadgets/oauthcallback</url-pattern>
+    </servlet-mapping>
+
+    <servlet-mapping>
+        <servlet-name>xml-to-html</servlet-name>
+        <url-pattern>/gadgets/ifr</url-pattern>
+    </servlet-mapping>
+
+    <servlet-mapping>
+        <servlet-name>metadata</servlet-name>
+        <url-pattern>/gadgets/metadata</url-pattern>
+    </servlet-mapping>
+
+</web-app>
diff --git a/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/index.html b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/index.html
new file mode 100644
index 0000000..6d5abf2
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/index.html
@@ -0,0 +1,53 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
+<head>
+<title>Sample: Simple Container</title>
+<!-- default container look and feel -->
+<link rel="stylesheet" href="css/gadgets.css">
+<script type="text/javascript" src="/gadgets/js/shindig-container:rpc.js?c=1&debug=1&nocache=1"></script>
+<script type="text/javascript">
+var specUrl0 = 'http://localhost:8080/myFirstGadget.xml';
+var specUrl1 = 'http://www.labpixies.com/campaigns/todo/todo.xml';
+
+// This container lays out and renders gadgets itself.
+
+function renderGadgets() {
+  var gadget0 = shindig.container.createGadget({specUrl: specUrl0});
+  var gadget1 = shindig.container.createGadget({specUrl: specUrl1});
+
+  shindig.container.addGadget(gadget0);
+  shindig.container.addGadget(gadget1);
+  shindig.container.layoutManager.setGadgetChromeIds(
+      ['gadget-chrome-x', 'gadget-chrome-y']);
+
+  shindig.container.renderGadget(gadget0);
+  shindig.container.renderGadget(gadget1);
+};
+</script>
+</head>
+<body onLoad="renderGadgets();">
+  <h2>Sample: Simple Container</h2>
+  <div id="gadget-chrome-x" class="gadgets-gadget-chrome"></div>
+  <div id="gadget-chrome-y" class="gadgets-gadget-chrome"></div>
+</body>
+</html>
diff --git a/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/myFirstGadget.xml b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/myFirstGadget.xml
new file mode 100644
index 0000000..3d24062
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webapp/myFirstGadget.xml
@@ -0,0 +1,30 @@
+#set( $symbol_pound = '#' )
+#set( $symbol_dollar = '$' )
+#set( $symbol_escape = '\' )
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="hello world example" /> 
+  <Content type="html">
+     <![CDATA[ 
+       Hello, world!
+     ]]>
+  </Content> 
+</Module>
diff --git a/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webresources/css/gadgets.css b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webresources/css/gadgets.css
new file mode 100644
index 0000000..7159fda
--- /dev/null
+++ b/trunk/java/sample-maven-archetype/src/main/resources/archetype-resources/src/main/webresources/css/gadgets.css
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+ 
+.gadgets-gadget-chrome {
+  float: left;
+  margin: 4px;
+  border: 1px solid #7aa5d6;
+}
+
+.gadgets-gadget {
+  border: none;
+}
+
+.gadgets-gadget-title-bar {
+  padding: 2px 4px;
+  background-color: #e5ecf9;
+}
+
+.gadgets-gadget-title {
+  font-weight: bold;
+  color: #3366cc;
+}
+
+.gadgets-gadget-title-button-bar {
+  font-size: smaller;
+}
+
+.gadgets-gadget-user-prefs-dialog {
+  background-color: #e5ecf9;
+}
+
+.gadgets-gadget-user-prefs-dialog-action-bar {
+  text-align: center;
+  padding-bottom: 4px;
+}
+
+.gadgets-gadget-title-button {
+}
+
+.gadgets-gadget-content {
+  padding: 4px;
+}
+
+.gadgets-log-entry {
+}
+
+/* Used to style messages produced during rewriting by CajaContentRewriter */
+.gadgets-messages {
+	
+}
diff --git a/trunk/java/server-dependencies/pom.xml b/trunk/java/server-dependencies/pom.xml
new file mode 100644
index 0000000..b3bcc42
--- /dev/null
+++ b/trunk/java/server-dependencies/pom.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-server-dependencies</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Apache Shindig Web App Dependencies</name>
+  <description>Default server war dependencies</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/server-dependencies</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/server-dependencies</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/server-dependencies</url>
+  </scm>
+
+  <dependencies>
+    <!-- project dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-gadgets</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-social-api</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-features</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-extras</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-sample-container</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+
+    <!-- external dependencies -->
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>jstl</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-jdk14</artifactId>
+    </dependency>
+    
+  </dependencies>
+</project>
diff --git a/trunk/java/server-resources/pom.xml b/trunk/java/server-resources/pom.xml
new file mode 100644
index 0000000..d8acaf8
--- /dev/null
+++ b/trunk/java/server-resources/pom.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-server-resources</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>war</packaging>
+
+  <name>Apache Shindig Web App Resources</name>
+  <description>Shallow Default server war containing only the configuration and javascript for the gadget rendering and the social api.</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/server-resources</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/server-resources</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/server-resources</url>
+  </scm>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <configuration>
+          <webResources>
+            <resource>
+              <!-- this is relative to the pom.xml directory -->
+              <directory>${basedir}/../../content/</directory>
+              <includes>
+                <include>**/*.*</include>
+              </includes>
+            </resource>
+            <resource>
+              <targetPath>META-INF</targetPath>
+              <directory>target/maven-shared-archive-resources/META-INF</directory>
+              <includes>
+                <include>**/*</include>
+              </includes>
+            </resource>
+          </webResources>
+          <classifier>${shindig.jdk.classifier}</classifier>
+        </configuration>
+      </plugin>
+    </plugins>
+    <resources>
+      <resource>
+        <targetPath>containers/default</targetPath>
+        <directory>${basedir}/../../config</directory>
+        <includes>
+          <include>container.js</include>
+        </includes>
+      </resource>
+      <resource>
+        <targetPath>config</targetPath>
+        <directory>${basedir}/../../config</directory>
+        <includes>
+          <include>oauth.json</include>
+          <include>oauth2.json</include>
+        </includes>
+      </resource>
+    </resources>
+  </build>
+
+</project>
diff --git a/trunk/java/server-resources/src/main/webapp/WEB-INF/authorize.jsp b/trunk/java/server-resources/src/main/webapp/WEB-INF/authorize.jsp
new file mode 100644
index 0000000..d18d96d
--- /dev/null
+++ b/trunk/java/server-resources/src/main/webapp/WEB-INF/authorize.jsp
@@ -0,0 +1,82 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you 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.
+-->
+<%@ page import="org.apache.shiro.SecurityUtils" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
+
+<%@ page import="net.oauth.OAuthConsumer" %>
+<%@ page import="org.apache.shindig.social.opensocial.oauth.OAuthEntry" %>
+<%@ page import="org.apache.shindig.social.opensocial.oauth.OAuthDataStore" %>
+<%@ page import="java.net.URLEncoder" %>
+<%@ page contentType="text/html;charset=UTF-8" language="java" %>
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+   "http://www.w3.org/TR/html4/loose.dtd">
+<%
+  // Gather data passed in to us.
+  OAuthConsumer consumer = (OAuthConsumer)request.getAttribute("CONSUMER");
+  OAuthEntry entry = (OAuthEntry) request.getAttribute("OAUTH_ENTRY");
+  OAuthDataStore dataStore = (OAuthDataStore) request.getAttribute("OAUTH_DATASTORE");
+  String token = (String)request.getAttribute("TOKEN");
+  String callback = (String)request.getAttribute("CALLBACK");
+
+  // Check if the user already authorized
+  // TODO - this is a bit hard since we cannot get at the jsondb here...
+
+  // If user clicked on the Authorize button then we're good.
+  if (request.getParameter("Authorize") != null) {
+    // If the user clicked the Authorize button we authorize the token and redirect back.
+    dataStore.authorizeToken(entry, SecurityUtils.getSubject().getPrincipal().toString());
+
+    // Bounce back to the servlet to handle redirecting to the callback URL
+    request.getRequestDispatcher("/oauth/authorize?oauth_token=" + token + "&oauth_callback=" + callback)
+            .forward(request,response);
+  } else if (request.getParameter("Deny") != null) {
+    dataStore.removeToken(entry);
+  }
+  // Gather some data
+  pageContext.setAttribute("appTitle", consumer.getProperty("title") , PageContext.PAGE_SCOPE);
+  pageContext.setAttribute("appDesc", consumer.getProperty("description"), PageContext.PAGE_SCOPE);
+    
+  pageContext.setAttribute("appIcon", consumer.getProperty("icon"));
+  pageContext.setAttribute("appThumbnail", consumer.getProperty("thumbnail"));
+%>
+<html>
+<head>
+  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+  <title>Your Friendly OAuth Provider</title>
+</head>
+<body>
+Greetings <shiro:principal/>,<br/><br/>
+
+The following application wants to access your account information<br/><br/>
+
+<h3><img src="${appIcon}"/> <b><c:out value="${appTitle}"/></b> is trying to access your information.</h3>
+<img src="${appThumbnail}" align="left" width="120" height="60"/>
+<c:out value="${appDesc}" default=""/>
+<br/>
+
+<form id="authorize_form" name="authZForm" action="authorize" method="POST">
+  <input type="hidden" id="authorize_oauth_token" name="oauth_token" value="<%= token %>"/>
+  <input type="submit" id="authroize_deny" name="Authorize" value="Deny"/>
+  <input type="submit" id="authorize_authorize" name="Authorize" value="Authorize"/>
+</form>
+
+</body>
+</html>
diff --git a/trunk/java/server-resources/src/main/webapp/WEB-INF/web.xml b/trunk/java/server-resources/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..fe47474
--- /dev/null
+++ b/trunk/java/server-resources/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,354 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://java.sun.com/xml/ns/javaee"
+         xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
+         id="Shindig"
+         version="2.5">
+  <display-name>Shindig</display-name>
+  <!-- configuration -->
+  <!-- If you have your own Guice module(s), put them here as a colon-separated list. -->
+  <!-- Note that any extras modules are optional -->
+  <context-param>
+    <param-name>guice-modules</param-name>
+    <param-value>
+      org.apache.shindig.common.PropertiesModule:
+      org.apache.shindig.gadgets.DefaultGuiceModule:
+      org.apache.shindig.social.core.config.SocialApiGuiceModule:
+      org.apache.shindig.social.sample.SampleModule:
+      org.apache.shindig.gadgets.oauth.OAuthModule:
+      org.apache.shindig.gadgets.oauth2.OAuth2Module:
+      org.apache.shindig.gadgets.oauth2.OAuth2MessageModule:
+      org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerModule: 
+      org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2PersistenceModule:
+      org.apache.shindig.common.cache.ehcache.EhCacheModule:
+      org.apache.shindig.sample.shiro.ShiroGuiceModule:
+      org.apache.shindig.sample.container.SampleContainerGuiceModule:
+      org.apache.shindig.extras.ShindigExtrasGuiceModule:
+      org.apache.shindig.gadgets.admin.GadgetAdminModule
+    </param-value>
+  </context-param>
+
+  <!--
+  Syntax: <key>=<value> separated by a newline
+
+  system.properties specifies the environmental variables that will be set to the JVM System Properties at server startup time.
+  Alternatively, you may add these values in your app server (ex: Tomcat) as
+  VM arguments like this: -Dshindig.host="my.production.shindig.server.com".
+
+  Here are a few properties that can be set for Shindig:
+  shindig.host: the server name that Shindig is deployed and running on
+  shindig.port: the port number of shindig.host server
+
+  Make sure you escape all HTML values for the web.xml to be parsed correctly.
+  -->
+   <context-param>
+     <param-name>system.properties</param-name>
+     <param-value>
+     <![CDATA[
+        shindig.host=
+        shindig.port=
+        aKey=/shindig/gadgets/proxy?container=default&url=
+     ]]>
+     </param-value>
+  </context-param>
+
+  <filter>
+    <filter-name>hostFilter</filter-name>
+    <filter-class>org.apache.shindig.common.servlet.HostFilter</filter-class>
+  </filter>
+  <filter-mapping>
+    <filter-name>hostFilter</filter-name>
+    <url-pattern>/gadgets/ifr</url-pattern>
+    <url-pattern>/gadgets/js/*</url-pattern>
+    <url-pattern>/gadgets/proxy/*</url-pattern>
+    <url-pattern>/gadgets/concat</url-pattern>
+    <url-pattern>/gadgets/makeRequest</url-pattern>
+    <url-pattern>/rpc/*</url-pattern>
+    <url-pattern>/rest/*</url-pattern>
+  </filter-mapping>
+
+    <filter>
+        <filter-name>ShiroFilter</filter-name>
+        <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
+        <init-param>
+            <param-name>config</param-name>
+            <param-value>
+            <![CDATA[
+                # The ShiroFilter configuration is very powerful and flexible, while still remaining succinct.
+                # Please read the comprehensive example, with full comments and explanations, in the JavaDoc:
+                #
+                # http://www.jsecurity.org/api/org/jsecurity/web/servlet/JSecurityFilter.html
+                [main]
+                shindigSampleRealm = org.apache.shindig.sample.shiro.SampleShiroRealm
+                securityManager.realm = $shindigSampleRealm
+                authc.loginUrl = /login.jsp
+
+                [urls]
+                # The /login.jsp is not restricted to authenticated users (otherwise no one could log in!), but
+                # the 'authc' filter must still be specified for it so it can process that url's
+                # login submissions. It is 'smart' enough to allow those requests through as specified by the
+                # shiro.loginUrl above.
+                /login.jsp = authc
+
+                /oauth/authorize/** = authc
+                /oauth2/authorize/** = authc
+            ]]>
+            </param-value>
+        </init-param>
+    </filter>
+
+  <filter>
+    <filter-name>authFilter</filter-name>
+    <filter-class>org.apache.shindig.auth.AuthenticationServletFilter</filter-class>
+  </filter>
+
+  <filter>
+    <filter-name>etagFilter</filter-name>
+    <filter-class>org.apache.shindig.gadgets.servlet.ETagFilter</filter-class>
+  </filter>
+
+
+  <filter-mapping>
+      <filter-name>ShiroFilter</filter-name>
+      <url-pattern>/oauth/authorize</url-pattern>
+  </filter-mapping>
+  
+  <filter-mapping>
+      <filter-name>ShiroFilter</filter-name>
+      <url-pattern>/oauth2/authorize</url-pattern>
+  </filter-mapping>
+
+  <filter-mapping>
+      <filter-name>ShiroFilter</filter-name>
+      <url-pattern>*.jsp</url-pattern>
+  </filter-mapping>
+
+  <filter-mapping>
+    <filter-name>authFilter</filter-name>
+    <url-pattern>/social/*</url-pattern>
+    <url-pattern>/gadgets/ifr</url-pattern>
+    <url-pattern>/gadgets/makeRequest</url-pattern>
+    <url-pattern>/gadgets/proxy</url-pattern>
+    <url-pattern>/gadgets/api/rpc/*</url-pattern>
+    <url-pattern>/gadgets/api/rest/*</url-pattern>
+    <url-pattern>/rpc/*</url-pattern>
+    <url-pattern>/rest/*</url-pattern>
+  </filter-mapping>
+
+  <filter-mapping>
+    <filter-name>etagFilter</filter-name>
+    <url-pattern>*</url-pattern>
+  </filter-mapping>
+
+  <listener>
+    <listener-class>org.apache.shindig.common.servlet.GuiceServletContextListener</listener-class>
+  </listener>
+
+  <!-- Render a Gadget -->
+  <servlet>
+    <servlet-name>xml-to-html</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.GadgetRenderingServlet
+    </servlet-class>
+  </servlet>
+
+  <servlet>
+    <servlet-name>accel</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.HtmlAccelServlet
+    </servlet-class>
+  </servlet>
+
+  <!-- Proxy -->
+  <servlet>
+    <servlet-name>proxy</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.ProxyServlet
+    </servlet-class>
+  </servlet>
+
+  <!-- makeRequest -->
+  <servlet>
+    <servlet-name>makeRequest</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.MakeRequestServlet
+    </servlet-class>
+  </servlet>
+
+  <!-- Concat -->
+  <servlet>
+    <servlet-name>concat</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.ConcatProxyServlet
+    </servlet-class>
+  </servlet>
+
+  <!-- OAuth callback -->
+  <servlet>
+    <servlet-name>oauthCallback</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.OAuthCallbackServlet
+    </servlet-class>
+  </servlet>
+
+  <!-- OAuth2 callback -->
+  <servlet>
+    <servlet-name>oauth2callback</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.OAuth2CallbackServlet
+    </servlet-class>
+  </servlet>
+  
+  <!-- Metadata RPC -->
+  <servlet>
+    <servlet-name>metadata</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.RpcServlet
+    </servlet-class>
+  </servlet>
+
+  <!-- javascript serving -->
+  <servlet>
+    <servlet-name>js</servlet-name>
+    <servlet-class>org.apache.shindig.gadgets.servlet.JsServlet</servlet-class>
+  </servlet>
+
+  <servlet>
+    <servlet-name>restapiServlet</servlet-name>
+    <servlet-class>
+      org.apache.shindig.protocol.DataServiceServlet
+    </servlet-class>
+    <init-param>
+      <param-name>handlers</param-name>
+      <param-value>org.apache.shindig.handlers</param-value>
+    </init-param>
+  </servlet>
+
+  <!-- Serve social RPC api -->
+  <servlet>
+    <servlet-name>jsonRpcServlet</servlet-name>
+    <servlet-class>
+      org.apache.shindig.protocol.JsonRpcServlet
+    </servlet-class>
+    <init-param>
+      <param-name>handlers</param-name>
+      <param-value>org.apache.shindig.handlers</param-value>
+    </init-param>
+  </servlet>
+
+  <!-- Serve sample OAuth apis -->
+  <servlet>
+    <servlet-name>sampleOAuth</servlet-name>
+    <servlet-class>
+      org.apache.shindig.social.sample.oauth.SampleOAuthServlet
+    </servlet-class>
+  </servlet>
+  
+  <!-- Serve OAuth 2 APIs -->
+  <servlet>
+    <servlet-name>OAuth2Servlet</servlet-name>
+    <servlet-class>
+      org.apache.shindig.social.core.oauth2.OAuth2Servlet
+    </servlet-class>
+  </servlet>
+
+  <servlet>
+    <servlet-name>rpcSwf</servlet-name>
+    <servlet-class>
+      org.apache.shindig.gadgets.servlet.RpcSwfServlet
+    </servlet-class>
+  </servlet>
+
+  <servlet-mapping>
+    <servlet-name>js</servlet-name>
+    <url-pattern>/gadgets/js/*</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>proxy</servlet-name>
+    <url-pattern>/gadgets/proxy/*</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>makeRequest</servlet-name>
+    <url-pattern>/gadgets/makeRequest</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>jsonRpcServlet</servlet-name>
+    <url-pattern>/rpc/*</url-pattern>
+    <url-pattern>/gadgets/api/rpc/*</url-pattern>
+    <url-pattern>/social/rpc/*</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>restapiServlet</servlet-name>
+    <url-pattern>/rest/*</url-pattern>
+    <url-pattern>/gadgets/api/rest/*</url-pattern>
+    <url-pattern>/social/rest/*</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>concat</servlet-name>
+    <url-pattern>/gadgets/concat</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>oauthCallback</servlet-name>
+    <url-pattern>/gadgets/oauthcallback</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>oauth2callback</servlet-name>
+    <url-pattern>/gadgets/oauth2callback</url-pattern>
+  </servlet-mapping>
+  
+  <servlet-mapping>
+    <servlet-name>xml-to-html</servlet-name>
+    <url-pattern>/gadgets/ifr</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>accel</servlet-name>
+    <url-pattern>/gadgets/accel</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>metadata</servlet-name>
+    <url-pattern>/gadgets/metadata</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>sampleOAuth</servlet-name>
+    <url-pattern>/oauth/*</url-pattern>
+  </servlet-mapping>
+  
+  <servlet-mapping>
+    <servlet-name>OAuth2Servlet</servlet-name>
+    <url-pattern>/oauth2/*</url-pattern>
+  </servlet-mapping>
+
+  <servlet-mapping>
+    <servlet-name>rpcSwf</servlet-name>
+    <url-pattern>/xpc*</url-pattern>
+  </servlet-mapping>
+</web-app>
diff --git a/trunk/java/server-resources/src/main/webapp/login.jsp b/trunk/java/server-resources/src/main/webapp/login.jsp
new file mode 100644
index 0000000..77409e8
--- /dev/null
+++ b/trunk/java/server-resources/src/main/webapp/login.jsp
@@ -0,0 +1,111 @@
+<%--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you 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.
+  --%>
+<%@ page import="org.apache.shiro.SecurityUtils" %>
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
+<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
+
+<html>
+<head>
+</head>
+<body>
+
+<h2>Please Log in</h2>
+
+<shiro:guest>
+    <p>Try one of the accounts defined in canonicaldb.json</p>
+
+
+    <style type="text/css">
+        table.sample {
+            border-width: 1px;
+            border-style: outset;
+            border-color: blue;
+            border-collapse: separate;
+            background-color: rgb( 255, 255, 240 );
+        }
+
+        table.sample th {
+            border-width: 1px;
+            padding: 1px;
+            border-style: none;
+            border-color: blue;
+            background-color: rgb( 255, 255, 240 );
+        }
+
+        table.sample td {
+            border-width: 1px;
+            padding: 1px;
+            border-style: none;
+            border-color: blue;
+            background-color: rgb( 255, 255, 240 );
+        }
+    </style>
+
+
+    <table class="sample">
+        <thead>
+            <tr>
+                <th>Username</th>
+                <th>Password</th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>canonical</td>
+                <td>password</td>
+            </tr>
+            <tr>
+                <td>john.doe</td>
+                <td>password</td>
+            </tr>
+            <tr>
+                <td>jane.doe</td>
+                <td>password</td>
+            </tr>
+        </tbody>
+    </table>
+    <br/><br/>
+</shiro:guest>
+
+
+<c:out value="${shiroLoginFailure}" default=""/><br/>
+
+
+<form id="login_form" action="" method="post">
+    <table align="left" border="0" cellspacing="0" cellpadding="3">
+        <tr>
+            <td>Username:</td>
+            <td><input type="text" id="login_username" name="username" maxlength="30"></td>
+        </tr>
+        <tr>
+            <td>Password:</td>
+            <td><input type="password" id="login_password" name="password" maxlength="30"></td>
+        </tr>
+        <tr>
+            <td colspan="2" align="left"><input type="checkbox" id="login_rememberme" name="rememberMe"><font size="2">Remember Me</font></td>
+        </tr>
+        <tr>
+            <td colspan="2" align="right"><input type="submit" id="login_submit" name="submit" value="Login"></td>
+        </tr>
+    </table>
+</form>
+
+</body>
+</html>
diff --git a/trunk/java/server/README b/trunk/java/server/README
new file mode 100644
index 0000000..cdcbb0c
--- /dev/null
+++ b/trunk/java/server/README
@@ -0,0 +1,10 @@
+Installing and running both servers
+============================================
+
+This is an example server setup containing web.xml files to run
+the gadget rendering and social-api services together or separately.
+
+Please see BUILD-JAVA in the base project directory for information.
+
+
+For more information, see http://shindig.apache.org
diff --git a/trunk/java/server/pom.xml b/trunk/java/server/pom.xml
new file mode 100644
index 0000000..11fa333
--- /dev/null
+++ b/trunk/java/server/pom.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-server</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>war</packaging>
+
+  <name>Apache Shindig Web App</name>
+  <description>Default server war containing both the gadget rendering code and the social api code.</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/server</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/server</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/server</url>
+  </scm>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <configuration>
+          <webResources>
+            <resource>
+              <targetPath>META-INF</targetPath>
+              <directory>target/maven-shared-archive-resources/META-INF</directory>
+              <includes>
+                <include>**/*</include>
+              </includes>
+            </resource>
+          </webResources>
+          <classifier>${shindig.jdk.classifier}</classifier>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.mortbay.jetty</groupId>
+        <artifactId>maven-jetty-plugin</artifactId>
+        <configuration>
+          <reload>manual</reload>
+          <webAppConfig>
+            <contextPath>/</contextPath>
+            <baseResource implementation="org.mortbay.resource.ResourceCollection">
+              <resourcesAsCSV>src/main/webapp,${basedir}/../../content</resourcesAsCSV>
+            </baseResource>
+          </webAppConfig>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <!-- war dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-server-dependencies</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+      <type>pom</type>
+    </dependency>
+    <!-- war resources -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-server-resources</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+      <type>war</type>
+    </dependency>
+
+    <!-- test dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty</groupId>
+      <artifactId>jetty</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>net.sourceforge.htmlunit</groupId>
+      <artifactId>htmlunit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>el-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/java/server/src/main/appended-resources/META-INF/LICENSE b/trunk/java/server/src/main/appended-resources/META-INF/LICENSE
new file mode 100644
index 0000000..190b161
--- /dev/null
+++ b/trunk/java/server/src/main/appended-resources/META-INF/LICENSE
@@ -0,0 +1,1168 @@
+===============================================================================
+
+The Apache Shindig distribution includes a number of subcomponents
+with separate copyright notices and license terms. Your use of the
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+===============================================================================
+OpenSocial Specification 0.8:
+
+Copyright (c) 2008 OpenSocial Foundation (http://www.opensocial.org)
+Released under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+===============================================================================
+Code Mirror:
+ Copyright (c) 2007-2010 Marijn Haverbeke
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any
+ damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any
+ purpose, including commercial applications, and to alter it and
+ redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software. If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+    distribution.
+
+===============================================================================
+swfobject:
+
+The MIT License
+
+Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
+
+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.
+
+
+ Marijn Haverbeke
+ marijnh@gmail.com
+
+===============================================================================
+
+org.json.* components:
+
+Copyright (c) 2002 JSON.org
+
+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 shall be used for Good, not Evil.
+
+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.
+
+===============================================================================
+
+XStream Components:
+
+Copyright (c) 2003-2006, Joe Walnes
+Copyright (c) 2006-2007, XStream Committers
+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 XStream 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 COPYRIGHT HOLDERS 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 COPYRIGHT OWNER OR 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.
+
+
+===============================================================================
+
+XPP3:
+
+Indiana University Extreme! Lab Software License
+
+Version 1.1.1
+
+Copyright (c) 2002 Extreme! Lab, Indiana University. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+2. 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.
+
+3. The end-user documentation included with the redistribution, if any,
+   must include the following acknowledgment:
+
+  "This product includes software developed by the Indiana University
+  Extreme! Lab (http://www.extreme.indiana.edu/)."
+
+Alternately, this acknowledgment may appear in the software itself,
+if and wherever such third-party acknowledgments normally appear.
+
+4. The names "Indiana Univeristy" and "Indiana Univeristy Extreme! Lab"
+must not be used to endorse or promote products derived from this
+software without prior written permission. For written permission,
+please contact http://www.extreme.indiana.edu/.
+
+5. Products derived from this software may not use "Indiana Univeristy"
+name nor may "Indiana Univeristy" appear in their name, without prior
+written permission of the Indiana University.
+
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED 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 AUTHORS, COPYRIGHT HOLDERS OR ITS 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.
+
+===============================================================================
+
+JDOM:
+
+ Copyright (C) 2000-2004 Jason Hunter & Brett McLaughlin.
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions, and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions, and the disclaimer that follows
+    these conditions in the documentation and/or other materials
+    provided with the distribution.
+
+ 3. The name "JDOM" must not be used to endorse or promote products
+    derived from this software without prior written permission.  For
+    written permission, please contact <request_AT_jdom_DOT_org>.
+
+ 4. Products derived from this software may not be called "JDOM", nor
+    may "JDOM" appear in their name, without prior written permission
+    from the JDOM Project Management <request_AT_jdom_DOT_org>.
+
+ In addition, we request (but do not require) that you include in the
+ end-user documentation provided with the redistribution and/or in the
+ software itself an acknowledgement equivalent to the following:
+     "This product includes software developed by the
+      JDOM Project (http://www.jdom.org/)."
+ Alternatively, the acknowledgment may be graphical using the logos
+ available at http://www.jdom.org/images/logos.
+
+ THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED 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 JDOM AUTHORS OR THE PROJECT
+ 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.
+
+ This software consists of voluntary contributions made by many
+ individuals on behalf of the JDOM Project and was originally
+ created by Jason Hunter <jhunter_AT_jdom_DOT_org> and
+ Brett McLaughlin <brett_AT_jdom_DOT_org>.  For more information
+ on the JDOM Project, please see <http://www.jdom.org/>.
+
+===============================================================================
+
+For Args4J component:
+
+Copyright (c) 2003, Kohsuke Kawaguchi
+
+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.
+
+===============================================================================
+
+protocol buffers:
+
+Copyright 2008, Google Inc.
+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 Google Inc. 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 COPYRIGHT HOLDERS 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 COPYRIGHT
+OWNER OR 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.
+
+Code generated by the Protocol Buffer compiler is owned by the owner
+of the input file used when generating it.  This code is not
+standalone and requires a support library to be linked with it.  This
+support library is itself covered by the above license.
+
+===============================================================================
+
+SLF4J:
+
+Copyright (c) 2004-2008 QOS.ch
+ All rights reserved.
+
+ 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.
+
+===============================================================================
+
+HTML Parser:
+
+Common Public License Version 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
+CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+    a) in the case of the initial Contributor, the initial code and
+documentation distributed under this Agreement, and
+
+    b) in the case of each subsequent Contributor:
+
+    i) changes to the Program, and
+
+    ii) additions to the Program;
+
+    where such changes and/or additions to the Program originate from and are
+distributed by that particular Contributor. A Contribution 'originates' from a
+Contributor if it was added to the Program by such Contributor itself or anyone
+acting on such Contributor's behalf. Contributions do not include additions to
+the Program which: (i) are separate modules of software distributed in
+conjunction with the Program under their own license agreement, and (ii) are not
+derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which are
+necessarily infringed by the use or sale of its Contribution alone or when
+combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+2. GRANT OF RIGHTS
+
+    a) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free copyright license to
+reproduce, prepare derivative works of, publicly display, publicly perform,
+distribute and sublicense the Contribution of such Contributor, if any, and such
+derivative works, in source code and object code form.
+
+    b) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed
+Patents to make, use, sell, offer to sell, import and otherwise transfer the
+Contribution of such Contributor, if any, in source code and object code form.
+This patent license shall apply to the combination of the Contribution and the
+Program if, at the time the Contribution is added by the Contributor, such
+addition of the Contribution causes such combination to be covered by the
+Licensed Patents. The patent license shall not apply to any other combinations
+which include the Contribution. No hardware per se is licensed hereunder.
+
+    c) Recipient understands that although each Contributor grants the licenses
+to its Contributions set forth herein, no assurances are provided by any
+Contributor that the Program does not infringe the patent or other intellectual
+property rights of any other entity. Each Contributor disclaims any liability to
+Recipient for claims brought by any other entity based on infringement of
+intellectual property rights or otherwise. As a condition to exercising the
+rights and licenses granted hereunder, each Recipient hereby assumes sole
+responsibility to secure any other intellectual property rights needed, if any.
+For example, if a third party patent license is required to allow Recipient to
+distribute the Program, it is Recipient's responsibility to acquire that license
+before distributing the Program.
+
+    d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright license set
+forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under its
+own license agreement, provided that:
+
+    a) it complies with the terms and conditions of this Agreement; and
+
+    b) its license agreement:
+
+    i) effectively disclaims on behalf of all Contributors all warranties and
+conditions, express and implied, including warranties or conditions of title and
+non-infringement, and implied warranties or conditions of merchantability and
+fitness for a particular purpose;
+
+    ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and consequential
+damages, such as lost profits;
+
+    iii) states that any provisions which differ from this Agreement are offered
+by that Contributor alone and not by any other party; and
+
+    iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable manner on or
+through a medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+    a) it must be made available under this Agreement; and
+
+    b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained within the
+Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if
+any, in a manner that reasonably allows subsequent Recipients to identify the
+originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor who
+includes the Program in a commercial product offering should do so in a manner
+which does not create potential liability for other Contributors. Therefore, if
+a Contributor includes the Program in a commercial product offering, such
+Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
+every other Contributor ("Indemnified Contributor") against any losses, damages
+and costs (collectively "Losses") arising from claims, lawsuits and other legal
+actions brought by a third party against the Indemnified Contributor to the
+extent caused by the acts or omissions of such Commercial Contributor in
+connection with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement. In order
+to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial Contributor to
+control, and cooperate with the Commercial Contributor in, the defense and any
+related settlement negotiations. The Indemnified Contributor may participate in
+any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If that
+Commercial Contributor then makes performance claims, or offers warranties
+related to Product X, those performance claims and warranties are such
+Commercial Contributor's responsibility alone. Under this section, the
+Commercial Contributor would have to defend claims against the other
+Contributors related to those performance claims and warranties, and if a court
+requires any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
+Recipient is solely responsible for determining the appropriateness of using and
+distributing the Program and assumes all risks associated with its exercise of
+rights under this Agreement, including but not limited to the risks and costs of
+program errors, compliance with applicable laws, damage to or loss of data,
+programs or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
+GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable
+law, it shall not affect the validity or enforceability of the remainder of the
+terms of this Agreement, and without further action by the parties hereto, such
+provision shall be reformed to the minimum extent necessary to make such
+provision valid and enforceable.
+
+If Recipient institutes patent litigation against a Contributor with respect to
+a patent applicable to software (including a cross-claim or counterclaim in a
+lawsuit), then any patent licenses granted by that Contributor to such Recipient
+under this Agreement shall terminate as of the date such litigation is filed. In
+addition, if Recipient institutes patent litigation against any entity
+(including a cross-claim or counterclaim in a lawsuit) alleging that the Program
+itself (excluding combinations of the Program with other software or hardware)
+infringes such Recipient's patent(s), then such Recipient's rights granted under
+Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to
+comply with any of the material terms or conditions of this Agreement and does
+not cure such failure in a reasonable period of time after becoming aware of
+such noncompliance. If all Recipient's rights under this Agreement terminate,
+Recipient agrees to cease use and distribution of the Program as soon as
+reasonably practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall continue and
+survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to time.
+No one other than the Agreement Steward has the right to modify this Agreement.
+IBM is the initial Agreement Steward. IBM may assign the responsibility to serve
+as the Agreement Steward to a suitable separate entity. Each new version of the
+Agreement will be given a distinguishing version number. The Program (including
+Contributions) may always be distributed subject to the version of the Agreement
+under which it was received. In addition, after a new version of the Agreement
+is published, Contributor may elect to distribute the Program (including its
+Contributions) under the new version. Except as expressly stated in Sections
+2(a) and 2(b) above, Recipient receives no rights or licenses to the
+intellectual property of any Contributor under this Agreement, whether
+expressly, by implication, estoppel or otherwise. All rights in the Program not
+expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to this
+Agreement will bring a legal action under this Agreement more than one year
+after the cause of action arose. Each party waives its rights to a jury trial in
+any resulting litigation.
+
+===============================================================================
+ICU4J:
+
+The X License
+
+ICU License - ICU 1.8.1 and later
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright (c) 1995-2011 International Business Machines Corporation and others
+
+All rights reserved.
+
+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, and/or sell copies of the
+Software, and to permit persons to whom the Software is furnished to do so,
+provided that the above copyright notice(s) and this permission notice appear
+in all copies of the Software and that both the above copyright notice(s) and
+this permission notice appear in supporting documentation.
+
+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 OF THIRD PARTY RIGHTS. IN
+NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE
+LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY
+DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall not be
+used in advertising or otherwise to promote the sale, use or other dealings in
+this Software without prior written authorization of the copyright holder.
+
+===============================================================================
+
+AspectJ:
+
+Eclipse Public License -v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
+CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and documentation
+distributed under this Agreement, and
+
+b) in the case of each subsequent Contributor:
+
+i) changes to the Program, and
+
+ii) additions to the Program;
+
+where such changes and/or additions to the Program originate from and are
+distributed by that particular Contributor. A Contribution 'originates' from a
+Contributor if it was added to the Program by such Contributor itself or anyone
+acting on such Contributor's behalf. Contributions do not include additions to
+the Program which: (i) are separate modules of software distributed in
+conjunction with the Program under their own license agreement, and (ii) are
+not derivative works of the Program.
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which are
+necessarily infringed by the use or sale of its Contribution alone or when
+combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this
+Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free copyright license to
+reproduce, prepare derivative works of, publicly display, publicly perform,
+distribute and sublicense the Contribution of such Contributor, if any, and
+such derivative works, in source code and object code form.
+
+b) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free patent license under
+Licensed Patents to make, use, sell, offer to sell, import and otherwise
+transfer the Contribution of such Contributor, if any, in source code and
+object code form. This patent license shall apply to the combination of the
+Contribution and the Program if, at the time the Contribution is added by the
+Contributor, such addition of the Contribution causes such combination to be
+covered by the Licensed Patents. The patent license shall not apply to any
+other combinations which include the Contribution. No hardware per se is
+licensed hereunder.
+
+c) Recipient understands that although each Contributor grants the licenses to
+its Contributions set forth herein, no assurances are provided by any
+Contributor that the Program does not infringe the patent or other intellectual
+property rights of any other entity. Each Contributor disclaims any liability
+to Recipient for claims brought by any other entity based on infringement of
+intellectual property rights or otherwise. As a condition to exercising the
+rights and licenses granted hereunder, each Recipient hereby assumes sole
+responsibility to secure any other intellectual property rights needed, if any.
+For example, if a third party patent license is required to allow Recipient to
+distribute the Program, it is Recipient's responsibility to acquire that
+license before distributing the Program.
+
+d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright license
+set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under
+its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+
+b) its license agreement:
+
+i) effectively disclaims on behalf of all Contributors all warranties and
+conditions, express and implied, including warranties or conditions of title
+and non-infringement, and implied warranties or conditions of merchantability
+and fitness for a particular purpose;
+
+ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and consequential
+damages, such as lost profits;
+
+iii) states that any provisions which differ from this Agreement are offered by
+that Contributor alone and not by any other party; and
+
+iv) states that source code for the Program is available from such Contributor,
+and informs licensees how to obtain it in a reasonable manner on or through a
+medium customarily used for software exchange.
+
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained within the
+Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if
+any, in a manner that reasonably allows subsequent Recipients to identify the
+originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor who
+includes the Program in a commercial product offering should do so in a manner
+which does not create potential liability for other Contributors. Therefore, if
+a Contributor includes the Program in a commercial product offering, such
+Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
+every other Contributor ("Indemnified Contributor") against any losses, damages
+and costs (collectively "Losses") arising from claims, lawsuits and other legal
+actions brought by a third party against the Indemnified Contributor to the
+extent caused by the acts or omissions of such Commercial Contributor in
+connection with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement. In order
+to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial Contributor
+to control, and cooperate with the Commercial Contributor in, the defense and
+any related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If that
+Commercial Contributor then makes performance claims, or offers warranties
+related to Product X, those performance claims and warranties are such
+Commercial Contributor's responsibility alone. Under this section, the
+Commercial Contributor would have to defend claims against the other
+Contributors related to those performance claims and warranties, and if a court
+requires any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
+Recipient is solely responsible for determining the appropriateness of using
+and distributing the Program and assumes all risks associated with its exercise
+of rights under this Agreement , including but not limited to the risks and
+costs of program errors, compliance with applicable laws, damage to or loss of
+data, programs or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), 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 OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
+GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable
+law, it shall not affect the validity or enforceability of the remainder of the
+terms of this Agreement, and without further action by the parties hereto, such
+provision shall be reformed to the minimum extent necessary to make such
+provision valid and enforceable.
+
+If Recipient institutes patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Program itself
+(excluding combinations of the Program with other software or hardware)
+infringes such Recipient's patent(s), then such Recipient's rights granted
+under Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to
+comply with any of the material terms or conditions of this Agreement and does
+not cure such failure in a reasonable period of time after becoming aware of
+such noncompliance. If all Recipient's rights under this Agreement terminate,
+Recipient agrees to cease use and distribution of the Program as soon as
+reasonably practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall continue
+and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to time.
+No one other than the Agreement Steward has the right to modify this Agreement.
+The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation
+may assign the responsibility to serve as the Agreement Steward to a suitable
+separate entity. Each new version of the Agreement will be given a
+distinguishing version number. The Program (including Contributions) may always
+be distributed subject to the version of the Agreement under which it was
+received. In addition, after a new version of the Agreement is published,
+Contributor may elect to distribute the Program (including its Contributions)
+under the new version. Except as expressly stated in Sections 2(a) and 2(b)
+above, Recipient receives no rights or licenses to the intellectual property of
+any Contributor under this Agreement, whether expressly, by implication,
+estoppel or otherwise. All rights in the Program not expressly granted under
+this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to this
+Agreement will bring a legal action under this Agreement more than one year
+after the cause of action arose. Each party waives its rights to a jury trial
+in any resulting litigation.
+
+===============================================================================
+jstl:
+
+COMMON DEVELOPMENT AND DISTRIBUTION LICENSE Version 1.0
+
+1. Definitions.
+
+    1.1. "Contributor" means each individual or entity that creates
+         or contributes to the creation of Modifications.
+
+    1.2. "Contributor Version" means the combination of the Original
+         Software, prior Modifications used by a Contributor (if any),
+         and the Modifications made by that particular Contributor.
+
+    1.3. "Covered Software" means (a) the Original Software, or (b)
+         Modifications, or (c) the combination of files containing
+         Original Software with files containing Modifications, in
+         each case including portions thereof.
+
+    1.4. "Executable" means the Covered Software in any form other
+         than Source Code.
+
+    1.5. "Initial Developer" means the individual or entity that first
+         makes Original Software available under this License.
+
+    1.6. "Larger Work" means a work which combines Covered Software or
+         portions thereof with code not governed by the terms of this
+         License.
+
+    1.7. "License" means this document.
+
+    1.8. "Licensable" means having the right to grant, to the maximum
+         extent possible, whether at the time of the initial grant or
+         subsequently acquired, any and all of the rights conveyed
+         herein.
+
+    1.9. "Modifications" means the Source Code and Executable form of
+         any of the following:
+
+        A. Any file that results from an addition to, deletion from or
+           modification of the contents of a file containing Original
+           Software or previous Modifications;
+
+        B. Any new file that contains any part of the Original
+           Software or previous Modifications; or
+
+        C. Any new file that is contributed or otherwise made
+           available under the terms of this License.
+
+    1.10. "Original Software" means the Source Code and Executable
+          form of computer software code that is originally released
+          under this License.
+
+    1.11. "Patent Claims" means any patent claim(s), now owned or
+          hereafter acquired, including without limitation, method,
+          process, and apparatus claims, in any patent Licensable by
+          grantor.
+
+    1.12. "Source Code" means (a) the common form of computer software
+          code in which modifications are made and (b) associated
+          documentation included in or with such code.
+
+    1.13. "You" (or "Your") means an individual or a legal entity
+          exercising rights under, and complying with all of the terms
+          of, this License. For legal entities, "You" includes any
+          entity which controls, is controlled by, or is under common
+          control with You. For purposes of this definition,
+          "control" means (a) the power, direct or indirect, to cause
+          the direction or management of such entity, whether by
+          contract or otherwise, or (b) ownership of more than fifty
+          percent (50%) of the outstanding shares or beneficial
+          ownership of such entity.
+
+2. License Grants.
+
+    2.1. The Initial Developer Grant.
+
+    Conditioned upon Your compliance with Section 3.1 below and
+    subject to third party intellectual property claims, the Initial
+    Developer hereby grants You a world-wide, royalty-free,
+    non-exclusive license:
+
+        (a) under intellectual property rights (other than patent or
+            trademark) Licensable by Initial Developer, to use,
+            reproduce, modify, display, perform, sublicense and
+            distribute the Original Software (or portions thereof),
+            with or without Modifications, and/or as part of a Larger
+            Work; and
+
+        (b) under Patent Claims infringed by the making, using or
+            selling of Original Software, to make, have made, use,
+            practice, sell, and offer for sale, and/or otherwise
+            dispose of the Original Software (or portions thereof).
+
+        (c) The licenses granted in Sections 2.1(a) and (b) are
+            effective on the date Initial Developer first distributes
+            or otherwise makes the Original Software available to a
+            third party under the terms of this License.
+
+        (d) Notwithstanding Section 2.1(b) above, no patent license is
+            granted: (1) for code that You delete from the Original
+            Software, or (2) for infringements caused by: (i) the
+            modification of the Original Software, or (ii) the
+            combination of the Original Software with other software
+            or devices.
+
+    2.2. Contributor Grant.
+
+    Conditioned upon Your compliance with Section 3.1 below and
+    subject to third party intellectual property claims, each
+    Contributor hereby grants You a world-wide, royalty-free,
+    non-exclusive license:
+
+        (a) under intellectual property rights (other than patent or
+            trademark) Licensable by Contributor to use, reproduce,
+            modify, display, perform, sublicense and distribute the
+            Modifications created by such Contributor (or portions
+            thereof), either on an unmodified basis, with other
+            Modifications, as Covered Software and/or as part of a
+            Larger Work; and
+
+        (b) under Patent Claims infringed by the making, using, or
+            selling of Modifications made by that Contributor either
+            alone and/or in combination with its Contributor Version
+            (or portions of such combination), to make, use, sell,
+            offer for sale, have made, and/or otherwise dispose of:
+            (1) Modifications made by that Contributor (or portions
+            thereof); and (2) the combination of Modifications made by
+            that Contributor with its Contributor Version (or portions
+            of such combination).
+
+        (c) The licenses granted in Sections 2.2(a) and 2.2(b) are
+            effective on the date Contributor first distributes or
+            otherwise makes the Modifications available to a third
+            party.
+
+        (d) Notwithstanding Section 2.2(b) above, no patent license is
+            granted: (1) for any code that Contributor has deleted
+            from the Contributor Version; (2) for infringements caused
+            by: (i) third party modifications of Contributor Version,
+            or (ii) the combination of Modifications made by that
+            Contributor with other software (except as part of the
+            Contributor Version) or other devices; or (3) under Patent
+            Claims infringed by Covered Software in the absence of
+            Modifications made by that Contributor.
+
+3. Distribution Obligations.
+
+    3.1. Availability of Source Code.
+
+    Any Covered Software that You distribute or otherwise make
+    available in Executable form must also be made available in Source
+    Code form and that Source Code form must be distributed only under
+    the terms of this License. You must include a copy of this
+    License with every copy of the Source Code form of the Covered
+    Software You distribute or otherwise make available. You must
+    inform recipients of any such Covered Software in Executable form
+    as to how they can obtain such Covered Software in Source Code
+    form in a reasonable manner on or through a medium customarily
+    used for software exchange.
+
+    3.2. Modifications.
+
+    The Modifications that You create or to which You contribute are
+    governed by the terms of this License. You represent that You
+    believe Your Modifications are Your original creation(s) and/or
+    You have sufficient rights to grant the rights conveyed by this
+    License.
+
+    3.3. Required Notices.
+
+    You must include a notice in each of Your Modifications that
+    identifies You as the Contributor of the Modification. You may
+    not remove or alter any copyright, patent or trademark notices
+    contained within the Covered Software, or any notices of licensing
+    or any descriptive text giving attribution to any Contributor or
+    the Initial Developer.
+
+    3.4. Application of Additional Terms.
+
+    You may not offer or impose any terms on any Covered Software in
+    Source Code form that alters or restricts the applicable version
+    of this License or the recipients' rights hereunder. You may
+    choose to offer, and to charge a fee for, warranty, support,
+    indemnity or liability obligations to one or more recipients of
+    Covered Software. However, you may do so only on Your own behalf,
+    and not on behalf of the Initial Developer or any Contributor.
+    You must make it absolutely clear that any such warranty, support,
+    indemnity or liability obligation is offered by You alone, and You
+    hereby agree to indemnify the Initial Developer and every
+    Contributor for any liability incurred by the Initial Developer or
+    such Contributor as a result of warranty, support, indemnity or
+    liability terms You offer.
+
+    3.5. Distribution of Executable Versions.
+
+    You may distribute the Executable form of the Covered Software
+    under the terms of this License or under the terms of a license of
+    Your choice, which may contain terms different from this License,
+    provided that You are in compliance with the terms of this License
+    and that the license for the Executable form does not attempt to
+    limit or alter the recipient's rights in the Source Code form from
+    the rights set forth in this License. If You distribute the
+    Covered Software in Executable form under a different license, You
+    must make it absolutely clear that any terms which differ from
+    this License are offered by You alone, not by the Initial
+    Developer or Contributor. You hereby agree to indemnify the
+    Initial Developer and every Contributor for any liability incurred
+    by the Initial Developer or such Contributor as a result of any
+    such terms You offer.
+
+    3.6. Larger Works.
+
+    You may create a Larger Work by combining Covered Software with
+    other code not governed by the terms of this License and
+    distribute the Larger Work as a single product. In such a case,
+    You must make sure the requirements of this License are fulfilled
+    for the Covered Software.
+
+4. Versions of the License.
+
+    4.1. New Versions.
+
+    Sun Microsystems, Inc. is the initial license steward and may
+    publish revised and/or new versions of this License from time to
+    time. Each version will be given a distinguishing version number.
+    Except as provided in Section 4.3, no one other than the license
+    steward has the right to modify this License.
+
+    4.2. Effect of New Versions.
+
+    You may always continue to use, distribute or otherwise make the
+    Covered Software available under the terms of the version of the
+    License under which You originally received the Covered Software.
+    If the Initial Developer includes a notice in the Original
+    Software prohibiting it from being distributed or otherwise made
+    available under any subsequent version of the License, You must
+    distribute and make the Covered Software available under the terms
+    of the version of the License under which You originally received
+    the Covered Software. Otherwise, You may also choose to use,
+    distribute or otherwise make the Covered Software available under
+    the terms of any subsequent version of the License published by
+    the license steward.
+
+    4.3. Modified Versions.
+
+    When You are an Initial Developer and You want to create a new
+    license for Your Original Software, You may create and use a
+    modified version of this License if You: (a) rename the license
+    and remove any references to the name of the license steward
+    (except to note that the license differs from this License); and
+    (b) otherwise make it clear that the license contains terms which
+    differ from this License.
+
+5. DISCLAIMER OF WARRANTY.
+
+    COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS"
+    BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
+    INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED
+    SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR
+    PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND
+    PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY
+    COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE
+    INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY
+    NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF
+    WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF
+    ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS
+    DISCLAIMER.
+
+6. TERMINATION.
+
+    6.1. This License and the rights granted hereunder will terminate
+    automatically if You fail to comply with terms herein and fail to
+    cure such breach within 30 days of becoming aware of the breach.
+    Provisions which, by their nature, must remain in effect beyond
+    the termination of this License shall survive.
+
+    6.2. If You assert a patent infringement claim (excluding
+    declaratory judgment actions) against Initial Developer or a
+    Contributor (the Initial Developer or Contributor against whom You
+    assert such claim is referred to as "Participant") alleging that
+    the Participant Software (meaning the Contributor Version where
+    the Participant is a Contributor or the Original Software where
+    the Participant is the Initial Developer) directly or indirectly
+    infringes any patent, then any and all rights granted directly or
+    indirectly to You by such Participant, the Initial Developer (if
+    the Initial Developer is not the Participant) and all Contributors
+    under Sections 2.1 and/or 2.2 of this License shall, upon 60 days
+    notice from Participant terminate prospectively and automatically
+    at the expiration of such 60 day notice period, unless if within
+    such 60 day period You withdraw Your claim with respect to the
+    Participant Software against such Participant either unilaterally
+    or pursuant to a written agreement with Participant.
+
+    6.3. In the event of termination under Sections 6.1 or 6.2 above,
+    all end user licenses that have been validly granted by You or any
+    distributor hereunder prior to termination (excluding licenses
+    granted to You by any distributor) shall survive termination.
+
+7. LIMITATION OF LIABILITY.
+
+    UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+    (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE
+    INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF
+    COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE
+    LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR
+    CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT
+    LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK
+    STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER
+    COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN
+    INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF
+    LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL
+    INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT
+    APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO
+    NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR
+    CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT
+    APPLY TO YOU.
+
+8. U.S. GOVERNMENT END USERS.
+
+    The Covered Software is a "commercial item," as that term is
+    defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial
+    computer software" (as that term is defined at 48
+    C.F.R. 252.227-7014(a)(1)) and "commercial computer software
+    documentation" as such terms are used in 48 C.F.R. 12.212
+    (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48
+    C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all
+    U.S. Government End Users acquire Covered Software with only those
+    rights set forth herein. This U.S. Government Rights clause is in
+    lieu of, and supersedes, any other FAR, DFAR, or other clause or
+    provision that addresses Government rights in computer software
+    under this License.
+
+9. MISCELLANEOUS.
+
+    This License represents the complete agreement concerning subject
+    matter hereof. If any provision of this License is held to be
+    unenforceable, such provision shall be reformed only to the extent
+    necessary to make it enforceable. This License shall be governed
+    by the law of the jurisdiction specified in a notice contained
+    within the Original Software (except to the extent applicable law,
+    if any, provides otherwise), excluding such jurisdiction's
+    conflict-of-law provisions. Any litigation relating to this
+    License shall be subject to the jurisdiction of the courts located
+    in the jurisdiction and venue specified in a notice contained
+    within the Original Software, with the losing party responsible
+    for costs, including, without limitation, court costs and
+    reasonable attorneys' fees and expenses. The application of the
+    United Nations Convention on Contracts for the International Sale
+    of Goods is expressly excluded. Any law or regulation which
+    provides that the language of a contract shall be construed
+    against the drafter shall not apply to this License. You agree
+    that You alone are responsible for compliance with the United
+    States export administration regulations (and the export control
+    laws and regulation of any other countries) when You use,
+    distribute or otherwise make available any Covered Software.
+
+10. RESPONSIBILITY FOR CLAIMS.
+
+    As between Initial Developer and the Contributors, each party is
+    responsible for claims and damages arising, directly or
+    indirectly, out of its utilization of rights under this License
+    and You agree to work with Initial Developer and Contributors to
+    distribute such responsibility on an equitable basis. Nothing
+    herein is intended or shall be deemed to constitute any admission
+    of liability.
+
+--------------------------------------------------------------------
+
+NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND
+DISTRIBUTION LICENSE (CDDL)
+
+For Covered Software in this distribution, this License shall
+be governed by the laws of the State of California (excluding
+conflict-of-law provisions).
+
+Any litigation relating to this License shall be subject to the
+jurisdiction of the Federal Courts of the Northern District of
+California and the state courts of the State of California, with
+venue lying in Santa Clara County, California.
\ No newline at end of file
diff --git a/trunk/java/server/src/main/appended-resources/META-INF/NOTICE b/trunk/java/server/src/main/appended-resources/META-INF/NOTICE
new file mode 100644
index 0000000..70ab5f7
--- /dev/null
+++ b/trunk/java/server/src/main/appended-resources/META-INF/NOTICE
@@ -0,0 +1,31 @@
+This product includes software (Gadget Server, Gadget Container)
+originally developed by Google Inc. (http://code.google.com/) and licensed
+to the ASF as initial contribution for Shindig.
+
+This product contains software (sha1 JS impl) developed by Google Inc.
+
+This product includes unmodified, binary redistributions of software (AOP Alliance) developed by
+AOP Alliance (http://aopalliance.sourceforge.net/), which was developed in the Public Domain.
+
+This product includes unmodified, binary redistributions of software (XML PullParser API) developed by
+www.xmlpull.org (http://www.xmlpull.org/), which was developed in the Public Domain.
+
+This product includes software developed by the Indiana University Extreme! Lab (http://www.extreme.indiana.edu/).
+
+This product includes unmodified, binary redistributions of software (HTML Parser) developed by
+the HTML Parser community (http://htmlparser.sourceforge.net), which is licensed under the Common Public License.
+An original copy of the license can be found at http://htmlparser.sourceforge.net/license.html
+
+This product includes software developed by the Joda project (http://www.joda.org/).
+
+This product includes unmodified, binary redistributions of software (ROME Modules) developed by
+Sun Microsystems, Inc. (https://rometools.jira.com/wiki/display/MODULES/Home),
+which is dual licensed under the LGPL or the Apache 2.0 license, and included here using the Apache 2.0 license.
+
+This product includes unmodified, binary redistributions of software (AspectJ) developed by the Eclipse Foundation
+(http://www.eclipse.org/aspectj/), under the (unmodified) EPL 1.0 (Eclipse Public License).
+An original copy of the license agreement can be found at: http://www.eclipse.org/legal/epl-v10.html
+
+This software contains unmodified, binary redistributions of software (JSP Standard Tag Library) developed by
+The Oracle Corporation (http://jstl.java.net/), which is licensed under the CDDL 1.0.
+An original copy of the license can be found at https://glassfish.dev.java.net/public/CDDL+GPL.html
diff --git a/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF b/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..58630c0
--- /dev/null
+++ b/trunk/java/server/src/main/webapp/META-INF/MANIFEST.MF
@@ -0,0 +1,2 @@
+Manifest-Version: 1.0

+

diff --git a/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/AllJsFilter.java b/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/AllJsFilter.java
new file mode 100644
index 0000000..4b256e1
--- /dev/null
+++ b/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/AllJsFilter.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.server.endtoend;
+
+import java.io.IOException;
+import java.util.Set;
+import java.util.HashSet;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.servlet.InjectedFilter;
+import org.apache.shindig.gadgets.GadgetException;
+import org.apache.shindig.gadgets.features.FeatureRegistry;
+import org.apache.shindig.gadgets.features.FeatureRegistryProvider;
+
+import com.google.common.base.Joiner;
+import com.google.inject.Inject;
+
+public class AllJsFilter extends InjectedFilter {
+
+  private String allFeatures;
+
+  @Inject
+  public void setFeatureRegistryProvider(FeatureRegistryProvider provider) {
+    try {
+      FeatureRegistry registry = provider.get(null);
+      Set<String> allFeatureNames = registry.getAllFeatureNames();
+
+      // TODO(felix8a): Temporary hack for caja
+      HashSet<String> someFeatureNames = new HashSet<String>(allFeatureNames);
+      someFeatureNames.remove("es53-guest-frame");
+      someFeatureNames.remove("es53-guest-frame.opt");
+      someFeatureNames.remove("es53-taming-frame");
+      someFeatureNames.remove("es53-taming-frame.opt");
+
+      allFeatures = Joiner.on(':').join(someFeatureNames);
+    } catch (GadgetException e) {
+      e.printStackTrace();
+    }
+  }
+
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+          throws IOException, ServletException {
+    if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) {
+      throw new ServletException("Only HTTP!");
+    }
+
+    HttpServletRequest req = (HttpServletRequest) request;
+    HttpServletResponse resp = (HttpServletResponse) response;
+
+    String requestURI = req.getRequestURI();
+    if (!requestURI.contains("all-features-please.js")) {
+      chain.doFilter(request, response);
+    } else {
+      String newURI = requestURI.replace("all-features-please.js", allFeatures + ".js") + "?" + req.getQueryString();
+      resp.sendRedirect(newURI);
+    }
+  }
+
+  public void destroy() {
+  }
+}
+
diff --git a/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndServer.java b/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndServer.java
new file mode 100644
index 0000000..7c740fc
--- /dev/null
+++ b/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndServer.java
@@ -0,0 +1,232 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.server.endtoend;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.auth.AuthenticationServletFilter;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.common.servlet.GuiceServletContextListener;
+import org.apache.shindig.gadgets.DefaultGuiceModule;
+import org.apache.shindig.gadgets.admin.GadgetAdminModule;
+import org.apache.shindig.gadgets.oauth.OAuthModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2Module;
+import org.apache.shindig.gadgets.oauth2.handler.OAuth2HandlerModule;
+import org.apache.shindig.gadgets.oauth2.persistence.sample.OAuth2PersistenceModule;
+import org.apache.shindig.gadgets.oauth2.OAuth2MessageModule;
+import org.apache.shindig.gadgets.servlet.ConcatProxyServlet;
+import org.apache.shindig.gadgets.servlet.GadgetRenderingServlet;
+import org.apache.shindig.gadgets.servlet.JsServlet;
+import org.apache.shindig.gadgets.servlet.MakeRequestServlet;
+import org.apache.shindig.protocol.DataServiceServlet;
+import org.apache.shindig.protocol.JsonRpcServlet;
+import org.apache.shindig.social.core.config.SocialApiGuiceModule;
+import org.apache.shindig.social.sample.SampleModule;
+import org.mortbay.jetty.Server;
+import org.mortbay.jetty.handler.ResourceHandler;
+import org.mortbay.jetty.servlet.Context;
+import org.mortbay.jetty.servlet.ServletHolder;
+import org.mortbay.resource.Resource;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Map;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Maps;
+
+/**
+ * Suite for running the end-to-end tests. The suite is responsible for starting up and shutting
+ * down the server.
+ */
+public class EndToEndServer {
+  private static final int JETTY_PORT = 9003;
+
+  private static final String GADGET_BASE = "/gadgets/ifr";
+  private static final String GADGET_RPC_BASE = "/gadgets/api/rpc/*";
+  private static final String SOCIAL_REST_BASE = "/social/rest/*";
+  private static final String SOCIAL_RPC_BASE = "/social/rpc/*";
+  private static final String RPC_BASE = "/rpc";
+  private static final String REST_BASE = "/rest";
+  private static final String CONCAT_BASE = "/gadgets/concat";
+  private static final String JS_BASE = "/gadgets/js/*";
+  private static final String MAKE_REQUEST_BASE = "/gadgets/makeRequest";
+  public static final String SERVER_URL = "http://localhost:" + JETTY_PORT;
+  public static final String GADGET_BASEURL = SERVER_URL + GADGET_BASE;
+
+  private final Server server;
+
+  /** Fake error code for data service servlet request */
+  protected int errorCode;
+
+  /** Fake error message for data service servlet request */
+  protected String errorMessage;
+
+  public EndToEndServer() throws Exception {
+    server = createServer(JETTY_PORT);
+  }
+
+  public void start() throws Exception {
+    server.start();
+  }
+
+  public void stop() throws Exception {
+    server.stop();
+  }
+
+  public void clearDataServiceError() {
+    errorCode = 0;
+  }
+
+  public void setDataServiceError(int errorCode, String errorMessage) {
+    this.errorCode = errorCode;
+    this.errorMessage = errorMessage;
+  }
+
+  /**
+   * Starts the server for end-to-end tests.
+   */
+  private Server createServer(int port) throws Exception {
+    System.setProperty("shindig.port", String.valueOf(port));
+    System.setProperty("jetty.port", String.valueOf(port));
+
+    Server newServer = new Server(port);
+
+    // Attach the test resources in /endtoend as static content for the test
+    ResourceHandler resources = new ResourceHandler();
+    URL resource = EndToEndTest.class.getResource("/endtoend");
+    resources.setBaseResource(Resource.newResource(resource));
+    newServer.addHandler(resources);
+
+    Context context = new Context(newServer, "/", Context.SESSIONS);
+    context.addEventListener(new GuiceServletContextListener());
+
+    Map<String, String> initParams = Maps.newHashMap();
+    String modules = Joiner.on(":")
+        .join(SocialApiGuiceModule.class.getName(),
+              SampleModule.class.getName(),
+              GadgetAdminModule.class.getName(),
+              DefaultGuiceModule.class.getName(),
+              PropertiesModule.class.getName(),
+              OAuthModule.class.getName(),
+              OAuth2Module.class.getName(),
+              OAuth2PersistenceModule.class.getName(),
+              OAuth2MessageModule.class.getName(),
+              OAuth2HandlerModule.class.getName()
+             );
+
+    initParams.put(GuiceServletContextListener.MODULES_ATTRIBUTE, modules);
+    context.setInitParams(initParams);
+
+    // Attach the gadget rendering servlet
+    ServletHolder gadgetServletHolder = new ServletHolder(new GadgetRenderingServlet());
+    context.addServlet(gadgetServletHolder, GADGET_BASE);
+
+    // Attach DataServiceServlet, wrapped in a proxy to fake errors
+    ServletHolder restServletHolder = new ServletHolder(new ForceErrorServlet(
+        new DataServiceServlet()));
+    restServletHolder.setInitParameter("handlers", "org.apache.shindig.handlers");
+    context.addServlet(restServletHolder, SOCIAL_REST_BASE);
+    context.addFilter(AuthenticationServletFilter.class, SOCIAL_REST_BASE, 0);
+
+    // Attach JsonRpcServlet, wrapped in a proxy to fake errors
+    ServletHolder rpcServletHolder = new ServletHolder(new ForceErrorServlet(new JsonRpcServlet()));
+    rpcServletHolder.setInitParameter("handlers", "org.apache.shindig.handlers");
+    context.addServlet(rpcServletHolder, SOCIAL_RPC_BASE);
+    context.addFilter(AuthenticationServletFilter.class, SOCIAL_RPC_BASE, 0);
+    context.addServlet(rpcServletHolder, GADGET_RPC_BASE);
+    context.addFilter(AuthenticationServletFilter.class, GADGET_RPC_BASE, 0);
+    context.addServlet(rpcServletHolder, RPC_BASE);
+    context.addFilter(AuthenticationServletFilter.class, RPC_BASE, 0);
+
+    // Attach the ConcatProxyServlet - needed for rewritten JS
+    ServletHolder concatHolder = new ServletHolder(new ConcatProxyServlet());
+    context.addServlet(concatHolder, CONCAT_BASE);
+
+    // Attach the JsServlet - needed for rewritten JS
+    ServletHolder jsHolder = new ServletHolder(new JsServlet());
+    context.addServlet(jsHolder, JS_BASE);
+
+    context.addFilter(AllJsFilter.class, JS_BASE, 0);
+
+    // Attach MakeRequestServlet
+    ServletHolder makeRequestHolder = new ServletHolder(new MakeRequestServlet());
+    context.addServlet(makeRequestHolder, MAKE_REQUEST_BASE);
+
+    // Attach an EchoServlet, used to test proxied rendering
+    ServletHolder echoHolder = new ServletHolder(new EchoServlet());
+    context.addServlet(echoHolder, "/echo");
+
+    return newServer;
+  }
+
+  private class ForceErrorServlet implements Servlet {
+    private final Servlet proxiedServlet;
+
+    public ForceErrorServlet(Servlet proxiedServlet) {
+      this.proxiedServlet = proxiedServlet;
+    }
+
+    public void init(ServletConfig servletConfig) throws ServletException {
+      proxiedServlet.init(servletConfig);
+    }
+
+    public ServletConfig getServletConfig() {
+      return proxiedServlet.getServletConfig();
+    }
+
+    public void service(ServletRequest servletRequest, ServletResponse servletResponse)
+        throws ServletException, IOException {
+      if (errorCode > 0) {
+        ((HttpServletResponse) servletResponse).sendError(errorCode, errorMessage);
+      } else {
+        servletRequest.setCharacterEncoding("UTF-8");
+        proxiedServlet.service(servletRequest, servletResponse);
+      }
+    }
+
+    public String getServletInfo() {
+      return proxiedServlet.getServletInfo();
+    }
+
+    public void destroy() {
+      proxiedServlet.destroy();
+    }
+  }
+
+  static private class EchoServlet extends HttpServlet {
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
+      req.setCharacterEncoding("UTF-8");
+      resp.setContentType(req.getContentType());
+
+      IOUtils.copy(req.getReader(), resp.getWriter());
+    }
+
+  }
+}
diff --git a/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java b/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java
new file mode 100644
index 0000000..2df6eec
--- /dev/null
+++ b/trunk/java/server/src/test/java/org/apache/shindig/server/endtoend/EndToEndTest.java
@@ -0,0 +1,476 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.server.endtoend;
+
+import org.apache.shindig.auth.BasicSecurityToken;
+import org.apache.shindig.auth.BasicSecurityTokenCodec;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.JsonAssert;
+import org.apache.shindig.common.crypto.BlobCrypterException;
+
+import com.gargoylesoftware.htmlunit.CollectingAlertHandler;
+import com.gargoylesoftware.htmlunit.FailingHttpStatusCodeException;
+import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
+import com.gargoylesoftware.htmlunit.Page;
+import com.gargoylesoftware.htmlunit.WebClient;
+import com.gargoylesoftware.htmlunit.html.DomNode;
+import com.gargoylesoftware.htmlunit.html.HtmlPage;
+import com.gargoylesoftware.htmlunit.html.HtmlElement;
+import com.gargoylesoftware.htmlunit.html.HTMLParserListener;
+import com.google.common.collect.Maps;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Ignore;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Base class for end-to-end tests.
+ */
+public class EndToEndTest {
+  private static final String[] EXPECTED_RESOURCES = {
+    "fetchPersonTest.xml",
+    "fetchPeopleTest.xml",
+    "errorTest.xml",
+    "jsonTest.xml",
+    "viewLevelElementsTest.xml",
+    "cajaTest.xml",
+    "failCajaTest.xml",
+    "failCajaUrlTest.xml",
+    "osapi/personTest.xml",
+    "osapi/peopleTest.xml",
+    "osapi/activitiesTest.xml",
+    "osapi/appdataTest.xml",
+    "osapi/batchTest.xml",
+    "testframework.js"
+  };
+
+  static private EndToEndServer server = null;
+
+  private WebClient webClient;
+  private CollectingAlertHandler alertHandler;
+  private SecurityToken token;
+  private String language;
+
+  @Test
+  public void checkResources() throws Exception {
+    for ( String resource : EXPECTED_RESOURCES ) {
+      String url = EndToEndServer.SERVER_URL + '/' + resource;
+      Page p = webClient.getPage(url);
+      assertEquals("Failed to load test resource " + url, 200, p.getWebResponse().getStatusCode());
+    }
+  }
+
+  @Test
+  public void fetchPerson() throws Exception {
+    executeAllPageTests("fetchPersonTest");
+  }
+
+  @Test
+  public void fetchPeople() throws Exception {
+    executeAllPageTests("fetchPeopleTest");
+  }
+
+  @Test
+  public void messageBundles() throws Exception {
+    executeAllPageTests("messageBundle");
+  }
+
+  @Test
+  public void jsonParse() throws Exception {
+    executeAllPageTests("jsonTest");
+  }
+
+  @Test
+  public void viewLevelElements() throws Exception {
+    executeAllPageTests("viewLevelElementsTest");
+  }
+
+  @Test
+  @Ignore("Issues with passing the neko dom to caja") // FIXME
+  public void cajaJsonParse() throws Exception {
+    executeAllPageTests("jsonTest", true /* caja */);
+  }
+
+  @Test
+  @Ignore("Issues with passing the neko dom to caja") // FIXME
+  public void cajaFetchPerson() throws Exception {
+    executeAllPageTests("fetchPersonTest", true /* caja */);
+  }
+
+  @Test
+  @Ignore("Issues with passing the neko dom to caja") // FIXME
+  public void cajaFetchPeople() throws Exception {
+    executeAllPageTests("fetchPeopleTest", true /* caja */);
+  }
+
+  @Test
+  @Ignore("Issues with passing the neko dom to caja") // FIXME
+  public void cajaTestMakeRequest() throws Exception {
+      executeAllPageTests("makeRequestTest", true /* caja */);
+  }
+
+  @Test
+  @Ignore("Issues with passing the neko dom to caja") // FIXME
+  public void caja() throws Exception {
+    executeAllPageTests("cajaTest.xml");
+  }
+
+  @Test
+  public void testMakeRequest() throws Exception {
+    executeAllPageTests("makeRequestTest");
+  }
+
+  @Test
+  public void messageBundlesRtl() throws Exception {
+    // Repeat the messageBundle tests, but with the language set to "ar"
+    language = "ar";
+
+    executeAllPageTests("messageBundle");
+  }
+
+  @Test
+  public void notFoundError() throws Exception {
+    server.setDataServiceError(HttpServletResponse.SC_NOT_FOUND, "Not Found");
+    executePageTest("errorTest", "notFoundError");
+  }
+
+  @Test
+  public void badRequest() throws Exception {
+    server.setDataServiceError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request");
+    executePageTest("errorTest", "badRequestError");
+  }
+
+  @Test
+  public void internalError() throws Exception {
+    server.setDataServiceError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
+    executePageTest("errorTest", "internalError");
+  }
+
+  @Test
+  public void testTemplates() throws Exception {
+    executeAllPageTests("opensocial-templates/ost_test");
+  }
+
+  @Test
+  public void testFailCaja() throws Exception {
+    HtmlPage page = executePageTest("failCajaTest", null);
+    NodeList bodyList = page.getElementsByTagName("body");
+
+    // Result should contain just one body
+    assertEquals(1, bodyList.getLength());
+    DomNode body = (DomNode) bodyList.item(0);
+
+    // Failed output contains only an error block
+    assertEquals(1, body.getChildNodes().getLength());
+    assertEquals("ul", body.getFirstChild().getNodeName());
+  }
+
+  @Test
+  public void testCajaFailUrlGadgets() throws Exception {
+    try {
+      executePageTest("failCajaUrlTest", null, /* caja */ true);
+    } catch (FailingHttpStatusCodeException e) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST, e.getStatusCode());
+    }
+  }
+
+  @Test
+  public void testPipelining() throws Exception {
+    HtmlPage page = executePageTest("pipeliningTest", null);
+    JSONArray array = new JSONArray(page.asText());
+    assertEquals(3, array.length());
+    Map<String, JSONObject> jsonObjects = Maps.newHashMap();
+    for (int i = 0; i < array.length(); i++) {
+      JSONObject jsonObj = array.getJSONObject(i);
+      assertTrue(jsonObj.has("id"));
+
+      jsonObjects.put(jsonObj.getString("id"), jsonObj);
+    }
+
+    JSONObject me = jsonObjects.get("me").getJSONObject("result");
+    assertEquals("Digg", me.getJSONObject("name").getString("familyName"));
+
+    JSONObject json = jsonObjects.get("json").getJSONObject("result");
+    JSONObject expected = new JSONObject("{content: {key: 'value'}, status: 200}");
+    JsonAssert.assertJsonObjectEquals(expected, json);
+
+    JsonAssert.assertObjectEquals("{id: 'var', result: 'value'}", jsonObjects.get("var"));
+  }
+
+  // TODO PML - convert this to use junit 4 Theories to simplify this.
+
+  @Test
+  public void testOsapiPeople() throws Exception {
+    executeAllPageTests("osapi/peopleTest");
+  }
+
+  @Test
+  public void testOsapiPerson() throws Exception {
+    executeAllPageTests("osapi/personTest");
+  }
+
+  @Test
+  public void testOsapiActivities() throws Exception {
+    executeAllPageTests("osapi/activitiesTest");
+  }
+
+  @Test
+  public void testOsapiAppdataFetchId() throws Exception {
+    executePageTest("osapi/appdataTest", "fetchId");
+  }
+
+  @Test
+  public void testOsapiAppdataAppDataWrite() throws Exception {
+    executePageTest("osapi/appdataTest", "appdataWrite");
+  }
+
+  @Test
+  public void testOsapiBatch() throws Exception {
+    executeAllPageTests("osapi/batchTest");
+  }
+
+
+  @Test
+  @Ignore("Issues with passing the neko dom to caja") // FIXME
+  public void testCajaOsapiAppdata() throws Exception {
+    executeAllPageTests("osapi/appdataTest", true /* caja */);
+  }
+
+  @Test
+  @Ignore("Issues with passing the neko dom to caja") // FIXME
+  public void testCajaOsapiBatch() throws Exception {
+    executeAllPageTests("osapi/batchTest", true /* caja */);
+  }
+
+  @Test
+  public void testTemplateRewrite() throws Exception {
+    HtmlPage page = executePageTest("templateRewriter", null);
+
+    // Verify that iteration attributes were processed
+    HtmlElement attrs = page.getElementById("attrs");
+    List<HtmlElement> attrsList = attrs.getElementsByTagName("li");
+    assertEquals(3, attrsList.size());
+
+    Element element = page.getElementById("id0");
+    assertNotNull(element);
+    assertEquals("Jane", element.getTextContent().trim());
+
+    element = page.getElementById("id2");
+    assertNotNull(element);
+    assertEquals("Maija", element.getTextContent().trim());
+
+    // Verify that the repeatTag was processed
+    HtmlElement repeat = page.getElementById("repeatTag");
+    List<HtmlElement> repeatList = repeat.getElementsByTagName("li");
+    assertEquals(1, repeatList.size());
+    assertEquals("George", repeatList.get(0).getTextContent().trim());
+
+    // Verify that the ifTag was processed
+    HtmlElement ifTag = page.getElementById("ifTag");
+    List<HtmlElement> ifList = ifTag.getElementsByTagName("li");
+    assertEquals(3, ifList.size());
+    assertEquals(1, page.getElementsByTagName("b").getLength());
+    assertEquals(1, ifList.get(2).getElementsByTagName("b").size());
+
+    Element jsonPipeline = page.getElementById("json");
+    assertEquals("value", jsonPipeline.getTextContent().trim());
+
+    Element textPipeline = page.getElementById("text");
+    assertEquals("{\"key\": \"value\"}", textPipeline.getTextContent().trim());
+
+    // Test of oncreate
+    Element oncreateSpan = page.getElementById("mutate");
+    assertEquals("mutated", oncreateSpan.getTextContent().trim());
+
+    assertEquals("45", page.getElementById("sum").getTextContent().trim());
+    assertEquals("25", page.getElementById("max").getTextContent().trim());
+  }
+
+  @Test
+  public void testTemplateLibrary() throws Exception {
+    HtmlPage page = executeAllPageTests("templateLibrary");
+    String pageXml = page.asXml();
+    assertTrue(pageXml.replaceAll("[\n\r ]", "").contains("p{color:red}"));
+
+    Node paragraph = page.getElementsByTagName("p").item(0);
+    assertEquals("Hello world", paragraph.getTextContent().trim());
+  }
+
+
+  @Test
+  public void testJavaScriptCompile() throws Exception {
+    // AllJsFilter will redirect to a url with all features being requested
+    webClient.setRedirectEnabled(true);
+
+    String containerJsUrl = EndToEndServer.SERVER_URL + "/gadgets/js/all-features-please.js?container=default&c=1";
+    String gadgetJsUrl = EndToEndServer.SERVER_URL + "/gadgets/js/all-features-please.js?container=default&c=0";
+
+    Page containerJsPage = webClient.getPage(containerJsUrl);
+    assertEquals(containerJsPage.getWebResponse().getStatusCode(), 200);
+
+    Page gadgetJsPage = webClient.getPage(gadgetJsUrl);
+    assertEquals(gadgetJsPage.getWebResponse().getStatusCode(), 200);
+  }
+
+  @BeforeClass
+  public static void setUpOnce() throws Exception {
+    server = new EndToEndServer();
+    server.start();
+  }
+
+  @AfterClass
+  public static void tearDownOnce() throws Exception {
+    server.stop();
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    webClient = new WebClient();
+    // NicelyResynchronizingAjaxController changes XHR calls from asynchronous
+    // to synchronous, saving the test from needing to wait or sleep for XHR
+    // completion.
+    webClient.setAjaxController(new NicelyResynchronizingAjaxController());
+    webClient.waitForBackgroundJavaScript(120000);  // Closure can take a long time...
+    webClient.setHTMLParserListener(HTMLParserListener.LOG_REPORTER);
+    webClient.setTimeout(120000);  // Closure can take a long time...
+
+    alertHandler = new CollectingAlertHandler();
+    webClient.setAlertHandler(alertHandler);
+    token = createToken("canonical", "john.doe");
+    language = null;
+    server.clearDataServiceError();
+  }
+
+//  @After
+//  public void tearDown() {
+//    server.clearDataServiceError();
+//  }
+
+  /**
+   * Verify that the Javascript completed running.  This ensures that
+   * logic errors or exceptions don't get treated as success.
+   */
+  @After
+  public void verifyTestsFinished() {
+    // Verify the format of the alerts - test method names followed by "finished"
+    String testMethod = null;
+
+    //System.out.println("=== All results " + alertHandler.getCollectedAlerts());
+    for (String alert : alertHandler.getCollectedAlerts()) {
+      if (testMethod == null) {
+        assertFalse("Test method omitted - '" + alert + '"', "FINISHED".equals(alert));
+        testMethod = alert;
+      } else {
+        assertEquals("test method " + testMethod + " did not finish", "FINISHED", alert);
+        testMethod = null;
+      }
+    }
+
+    assertNull("test method " + testMethod + " did not finish", testMethod);
+  }
+
+  /**
+   * Executes a page test by loading the HTML page.
+   * @param testName name of the test, which must match a gadget XML file
+   *     name in test/resources/endtoend (minus .xml).
+   * @param testMethod name of the javascript method to execute
+   * @return the parsed HTML page
+   */
+  private HtmlPage executePageTest(String testName, String testMethod)
+      throws IOException {
+    return executePageTest(testName, testMethod, false /* caja */);
+  }
+
+  private HtmlPage executePageTest(String testName, String testMethod, boolean caja)
+      throws IOException {
+    if (!testName.endsWith(".xml")) {
+      testName = testName + ".xml";
+    }
+
+    String gadgetUrl = EndToEndServer.SERVER_URL + '/' + testName;
+    String url = EndToEndServer.GADGET_BASEURL + "?url=" + URLEncoder.encode(gadgetUrl, "UTF-8");
+    BasicSecurityTokenCodec codec = new BasicSecurityTokenCodec();
+    url += "&st=" + URLEncoder.encode(codec.encodeToken(token), "UTF-8");
+    if (testMethod != null) {
+      url += "&testMethod=" + URLEncoder.encode(testMethod, "UTF-8");
+    }
+    if (caja) {
+      url += "&caja=1&libs=caja";
+    }
+
+    url += "&nocache=1";
+    if (language != null) {
+      url += "&lang=" + language;
+    }
+
+    Page page = webClient.getPage(url);
+    // wait for window.setTimeout, window.setInterval or asynchronous XMLHttpRequest
+    webClient.waitForBackgroundJavaScript(10000);
+    if (!(page instanceof HtmlPage)) {
+      fail("Got wrong page type. Was: " + page.getWebResponse().getContentType());
+    }
+    return (HtmlPage) page;
+  }
+
+  /**
+   * Executes all page test in a single XML file.
+   * @param testName name of the test, which must match a gadget XML file
+   *     name in test/resources/endtoend (minus .xml).
+   * @throws IOException
+   */
+  private HtmlPage executeAllPageTests(String testName) throws IOException {
+      return executePageTest(testName, "all", false);
+  }
+
+  /**
+   * Executes all page test in a single XML file injecting a flag to cajole the test first.
+   * @param testName name of the test, which must match a gadget XML file
+   *     name in test/resources/endtoend (minus .xml).
+   * @throws IOException
+   */
+    private HtmlPage executeAllPageTests(String testName, boolean caja) throws IOException {
+        return executePageTest(testName, "all", caja);
+  }
+
+  private BasicSecurityToken createToken(String owner, String viewer)
+      throws BlobCrypterException {
+    return new BasicSecurityToken(owner, viewer, "test", "domain", "appUrl", "1", "default", null, null);
+  }
+}
diff --git a/trunk/java/server/src/test/resources/endtoend/cajaTest.xml b/trunk/java/server/src/test/resources/endtoend/cajaTest.xml
new file mode 100644
index 0000000..64e818d
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/cajaTest.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="caja" />
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        function getAndCheckError(response, key) {
+          assertFalse('Data error', response.hadError());
+          var dataItem = response.get(key);
+          assertFalse('Data item error for ' + key, dataItem.hadError());
+          return dataItem.getData();
+        }
+        var tests = {
+          simpleTest: function() {
+            assertTrue("Basic cajoling failing", true);
+            finished();
+          },          
+          domitaTest: function() {
+            var elDiv = document.createElement('div');
+            elDiv.setAttribute('id', 'elDiv');
+            elDiv.innerHTML = "hello world";
+            document.body.appendChild(elDiv);
+            assertEquals("Basic dom operations failing", 
+              document.getElementById('elDiv').innerHTML, "hello world");
+            finished();
+          }
+        };
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/errorTest.xml b/trunk/java/server/src/test/resources/endtoend/errorTest.xml
new file mode 100644
index 0000000..e21a04a
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/errorTest.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="opensocial-0.9" />
+    <Require feature="views" />
+    <Optional feature="content-rewrite">
+      <Param name="exclude-urls">.*</Param>
+    </Optional>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+
+        var tests = {
+          /** Test 404 */
+          notFoundError: function() {
+            errorTestCase(404, opensocial.ResponseItem.Error.BAD_REQUEST);
+          },
+
+          /** Test 400 */
+          badRequestError: function() {
+            errorTestCase(400, opensocial.ResponseItem.Error.BAD_REQUEST);
+          },
+
+          /** Test 500 */
+          internalError: function() {
+            errorTestCase(500, opensocial.ResponseItem.Error.INTERNAL_ERROR);
+          }
+        };
+        
+
+        /** Test a single error code case */
+        function errorTestCase(httpCode, errorCode) {
+          var req = opensocial.newDataRequest();
+
+          // Request the "canonical" viewer
+          req.add(req.newFetchPersonRequest("canonical"), "canonical");
+
+          function receivedData(response) {
+            assertTrue("Expecting error, got " + gadgets.json.stringify(response), response.hadError());
+            var dataItem = response.get("canonical");
+            assertFalse("Expecting data item", dataItem == undefined);
+            assertTrue("Expecting item error", dataItem.hadError());
+            assertEquals("Mismatched error code", errorCode, dataItem.getErrorCode());
+
+            finished();
+          }
+
+          // Send the request
+          req.send(receivedData);
+        }
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/failCajaTest.xml b/trunk/java/server/src/test/resources/endtoend/failCajaTest.xml
new file mode 100644
index 0000000..9b6c1bb
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/failCajaTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="caja" />
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript">
+			  var x = {};
+        with(x) {};
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/failCajaUrlTest.xml b/trunk/java/server/src/test/resources/endtoend/failCajaUrlTest.xml
new file mode 100644
index 0000000..e38ba3c
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/failCajaUrlTest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+  </ModulePrefs>
+  <Content type="url" href="http://www.example.com" />
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/fetchPeopleTest.xml b/trunk/java/server/src/test/resources/endtoend/fetchPeopleTest.xml
new file mode 100644
index 0000000..00de40f
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/fetchPeopleTest.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="opensocial-0.9"/>
+    <Require feature="views"/>
+    <Optional feature="content-rewrite">
+      <Param name="exclude-urls">.*</Param>
+    </Optional>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+
+        var tests = {
+          /** Test fetching the owner's friends, which should be 'canonical' */
+          fetchOwnerFriends: function() {
+            var req = opensocial.newDataRequest();
+
+            var idSpec = opensocial.newIdSpec({userId : opensocial.IdSpec.PersonId.OWNER,
+                groupId : 'FRIENDS'})
+            req.add(req.newFetchPeopleRequest(idSpec), 'ownerFriends');
+            function receivedData(response) {
+              var ownerFriends = getAndCheckError(response, 'ownerFriends');
+              assertEquals('Wrong friend count', 4, ownerFriends.size());
+
+              var johnDoe = ownerFriends.getById('john.doe');
+              assertEquals('Wrong name for john doe', 'Johnny', johnDoe.getDisplayName());
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+
+          /** Test fetching 'maija.m' friends, of which there are none */
+          fetchEmptyFriendsById: function() {
+            var req = opensocial.newDataRequest();
+
+            var idSpec = opensocial.newIdSpec({userId : 'maija.m', groupId : 'FRIENDS'})
+            req.add(req.newFetchPeopleRequest(idSpec), 'idFriends');
+            function receivedData(response) {
+              var ownerFriends = getAndCheckError(response, 'idFriends');
+              assertEquals('Wrong friend count', 0, ownerFriends.size());
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+
+          /** Test fetching friends of a list of users */
+          fetchPluralUsers: function() {
+            var req = opensocial.newDataRequest();
+
+            var idSpec = opensocial.newIdSpec({userId : ['john.doe', 'jane.doe']})
+            req.add(req.newFetchPeopleRequest(idSpec), 'idUsers');
+            function receivedData(response) {
+              var users = getAndCheckError(response, 'idUsers');
+              assertEquals('Wrong friend count', 2, users.size());
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          }
+        };
+
+
+        function getAndCheckError(response, key) {
+          assertFalse('Data error', response.hadError());
+          var dataItem = response.get(key);
+          assertFalse('Data item error for ' + key, dataItem.hadError());
+          return dataItem.getData();
+        }
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/fetchPersonTest.xml b/trunk/java/server/src/test/resources/endtoend/fetchPersonTest.xml
new file mode 100644
index 0000000..5569e5c
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/fetchPersonTest.xml
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="opensocial-0.9" />
+    <Require feature="views" />
+    <Optional feature="content-rewrite">
+      <Param name="exclude-urls">.*</Param>
+    </Optional>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var tests = {
+          /** Test fetching a specific ID */
+          fetchId: function() {
+            var req = opensocial.newDataRequest();
+
+            // Request the "canonical" viewer
+            req.add(req.newFetchPersonRequest("canonical"), "canonical");
+
+            function receivedData(response) {
+              var user = getAndCheckError(response, "canonical");
+              assertEquals("Names don't match",
+                "Shin Digg", user.getDisplayName());
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+
+          /** Test fetching the owner, which should be "canonical" */
+          fetchOwner: function() {
+            var req = opensocial.newDataRequest();
+
+            // Request the "canonical" viewer
+            req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.OWNER), "owner");
+
+            function receivedData(response) {
+              var user = getAndCheckError(response, "owner");
+              assertEquals("Names don't match",
+                "Shin Digg", user.getDisplayName());
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+
+          /** Test fetching the viewer, which should be "john.doe" */
+          fetchViewer: function() {
+            var req = opensocial.newDataRequest();
+
+            // Request the "canonical" viewer
+            req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER), "viewer");
+
+            function receivedData(response) {
+              var user = getAndCheckError(response, "viewer");
+              assertEquals("Names don't match",
+                "Johnny", user.getDisplayName());
+
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+
+          /** Test fetching the owner and viewer as a batch */
+          fetchOwnerAndViewer: function() {
+            var req = opensocial.newDataRequest();
+
+            // Request the "canonical" viewer
+            req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.OWNER), "owner");
+            req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER), "viewer");
+
+            function receivedData(response) {
+              var user = getAndCheckError(response, "owner");
+              assertEquals("Names don't match",
+                "Shin Digg", user.getDisplayName());
+
+              user = getAndCheckError(response, "viewer");
+              assertEquals("Names don't match",
+                "Johnny", user.getDisplayName());
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+
+          fetchPersonNotFound: function() {
+            var req = opensocial.newDataRequest();
+
+            // Request the "canonical" viewer
+            req.add(req.newFetchPersonRequest("not.a.real.id"), "bad");
+
+            function receivedData(response) {
+              assertTrue("No data error", response.hadError());
+              var dataItem = response.get("bad");
+              assertTrue("No data item error", dataItem.hadError());
+              assertEquals("Not a badRequest", "badRequest", dataItem.getErrorCode());
+              assertEquals("Not null data", null, dataItem.getData());
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+          
+          /** Test fetching app data */
+          fetchAppData: function() {
+            var req = opensocial.newDataRequest();
+
+            // Request the "canonical" viewer
+            req.add(req.newFetchPersonRequest("canonical",
+                {appData: '*'}), "canonical");
+
+            var receivedData = function(response) {
+              var user = getAndCheckError(response, "canonical");
+              assertEquals("Names don't match",
+                "Shin Digg", user.getDisplayName());
+              assertEquals("Wrong app data returned", "2", user.getAppData("count"));
+              assertEquals("Wrong app data returned", "100", user.getAppData("size"));
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+
+          /** Test fetching app data via the array */
+          fetchTwoProperties: function() {
+            var req = opensocial.newDataRequest();
+
+            // Request the "canonical" viewer
+            req.add(req.newFetchPersonRequest("canonical",
+                {appData: ['count', 'notThere']}), "canonical");
+
+            var receivedData = function(response) {
+              var user = getAndCheckError(response, "canonical");
+              assertEquals("Names don't match",
+                "Shin Digg", user.getDisplayName());
+              assertEquals("Wrong app data returned", "2", user.getAppData("count"));
+              assertEquals("Too much app data returned", undefined, user.getAppData("size"));
+              assertEquals("App data that shouldn't exist returned", undefined, user.getAppData("notThere"));
+              finished();
+            }
+
+            // Send the request
+            req.send(receivedData);
+          },
+        };
+
+        function getAndCheckError(response, key) {
+          assertFalse("Data error", response.hadError());
+          var dataItem = response.get(key);
+          assertFalse("Data item error for " + key, dataItem.hadError());
+          return dataItem.getData();
+        }
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/jsonTest.xml b/trunk/java/server/src/test/resources/endtoend/jsonTest.xml
new file mode 100644
index 0000000..b76d942
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/jsonTest.xml
@@ -0,0 +1,174 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="gadgets.json.ext" />
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        function createDom(xmlString) {
+          var xmlDoc;
+          if (window.DOMParser) {
+            var parser = new DOMParser();
+            xmlDoc = parser.parseFromString(xmlString, "text/xml");
+          } else {
+            xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
+            xmlDoc.async = "false";
+           xmlDoc.loadXML(xmlString);
+          }
+          return xmlDoc;
+        };
+        
+        function createJson(xmlString) {
+          var dom = createDom(xmlString);
+          return gadgets.json.xml.convertXmlToJson(dom);
+        };
+
+        var tests = {
+          jsonStringifyTest: function() {
+            var val = {foo: 1, bar: [0, 1, 2], baz: {key: 'value'}};
+            var str = gadgets.json.stringify(val);
+            assertTrue("Serialization missing scalar value", /"foo":1/.test(str));
+            assertTrue("Serialization missing array value", /"bar":\[0,1,2\]/.test(str));
+            assertTrue("Serialization missing literal value", /"baz":\{"key":"value"\}/.test(str));
+            finished();
+          },
+          jsonConvertXmlToJsonTest : function() {
+            var obj = createJson('<e />');
+            assertEquals('Testing e is not null', obj.e, null);
+            
+            obj = createJson('<e>text</e>');
+            assertEquals('Testing e equals text', obj.e, "text");
+            
+            obj = createJson('<e><a>text</a><b>text</b></e>');
+            assertTrue('Testing e is not null', obj.e != null);
+            assertEquals('Testing e.a equals text', obj.e.a, "text");
+            assertEquals('Testing e.a.b equals text', obj.e.b, "text");
+          
+            obj = createJson('<e><a>text</a><a>text</a></e>');
+            assertTrue('Testing e is not null', obj.e != null);
+            assertTrue('Testing e.a is not null', obj.e.a != null);
+            assertTrue('Testing e.a is an instance of Array', obj.e.a instanceof Array);
+            assertEquals('Testing e.a.length equals 2 ', obj.e.a.length, 2);
+          
+            obj = createJson('<e>text<a>text</a></e>');
+            assertTrue('Testing e is not null', obj.e != null);
+            assertEquals('Testing e["#text"] equals text', obj.e["#text"], "text");
+            assertEquals('Testing e.a equals text', obj.e.a, "text"); 
+            
+            obj = createJson('<e><a id="id1"/><a id="id2"/></e>');
+            assertTrue('Testing e is not null', obj.e != null);
+            assertTrue('Testing e.a does not equal null', obj.e.a != null);
+            assertTrue('Testing e.a is an instance of Array', obj.e.a instanceof Array);
+            assertEquals('Testing e.a.length is 2', obj.e.a.length, 2);
+            assertEquals('Testing e.a[0]["@id"] equals id1', obj.e.a[0]["@id"], "id1");
+            assertEquals('Testing e.a[1].["@id"] equals id2', obj.e.a[1]["@id"], "id2"); 
+            
+            obj = createJson(
+              '<ol class="xoxo">' + 
+                '<li>' + 
+                  'Subject 1' + 
+                  '<ol>' + 
+                    '<li>subpoint a</li>' + 
+                    '<li>subpoint b</li>' + 
+                  '</ol>' + 
+                '</li>' + 
+                '<li attr="value">' + 
+                  '<span>Subject 2</span>' + 
+                  '<ol compact="compact">' + 
+                    '<li>subpoint c</li>' + 
+                    '<li>subpoint d</li>' + 
+                  '</ol>' + 
+                '</li>' + 
+                '<li>' + 
+                  '<span>Subject 2</span>' + 
+                  '<ol>' + 
+                    '<li>subpoint c</li>' + 
+                    '<li>subpoint d</li>' + 
+                  '</ol>' + 
+                '</li>' + 
+              '</ol>');
+              assertTrue('Testing ol is not null', obj.ol != null);
+              assertEquals('Testing ol["@class"] is equal to xoxo', obj.ol["@class"], "xoxo");
+              assertTrue('Testing ol.li is not null', obj.ol.li != null);
+              assertTrue('Testing ol.li is an instance of an Array', obj.ol.li instanceof Array);
+              assertEquals('Testing ol.li.length is equal to 3', obj.ol.li.length, 3);
+              assertEquals('Testing ol.li[0]["#text"] is equal to Subject 1', obj.ol.li[0]["#text"], "Subject 1"); 
+              assertTrue('Testing ol.li.[0].ol is not null', obj.ol.li[0].ol != null);
+              assertTrue('Testing ol.li[0].ol.li is not null', obj.ol.li[0].ol.li != null);
+              assertTrue('Testing ol.li[0].ol.li is an instance of Array', obj.ol.li[0].ol.li instanceof Array);
+              assertEquals('Testing ol.li[0].ol.li.length is equal to 2', obj.ol.li[0].ol.li.length, 2);
+              assertEquals('Testing ol.li[0].ol.li.[0] is equal to subpoint a', obj.ol.li[0].ol.li[0], "subpoint a");
+              assertEquals('Testing ol.li[0].ol.li.[1] is equal to subpoint b', obj.ol.li[0].ol.li[1], "subpoint b");
+              assertEquals('Testing ol.li[1]["@attr"] is equal to value', obj.ol.li[1]["@attr"], "value");
+              assertEquals('Testing ol.li[1].span is equal to Subject 2', obj.ol.li[1].span, "Subject 2");
+              assertTrue('Testing ol.li[1].ol is not null', obj.ol.li[1].ol != null);
+              assertEquals('Testing ol.li[1].ol["@compact"]', obj.ol.li[1].ol["@compact"], "compact");
+              assertTrue('Testing ol.li[1].ol.li is not null', obj.ol.li[1].ol.li != null);
+              assertTrue('Testing ol.li[1].ol.li is an instanceof Array', obj.ol.li[1].ol.li instanceof Array);
+              assertEquals('Testing ol.li[1].ol.li[0] equals subpoint c', obj.ol.li[1].ol.li[0], "subpoint c");
+              assertEquals('Testing ol.li[1].ol.li[1] equals subpoint d', obj.ol.li[1].ol.li[1], "subpoint d");
+              assertEquals('Testing old.li[2].span equals Subject 2', obj.ol.li[2].span, "Subject 2");
+              assertTrue('Testing ol.li[2].ol is not null', obj.ol.li[2].ol != null);
+              assertTrue('Testing ol.li[2].ol.li is not null', obj.ol.li[2].ol.li != null);
+              assertTrue('Testing ol.li[2].ol.li is an instance of Array', obj.ol.li[2].ol.li instanceof Array);
+              assertEquals('Testing ol.li[2].ol.li.length is equal to 2', obj.ol.li[2].ol.li.length, 2);
+              assertEquals('Testing ol.li[2].ol.li[0] is equal to subpoint c', obj.ol.li[2].ol.li[0], "subpoint c");
+              assertEquals('Testing ol.li[2].ol.li[1] is equal to subpoint d', obj.ol.li[2].ol.li[1], "subpoint d");
+              
+              obj = createJson('<span class="vevent">' + 
+                '<a class="url" href="http://www.web2con.com/">' + 
+                  '<span class="summary">Web 2.0 Conference</span>' + 
+                  '<abbr class="dtstart" title="2005-10-05">October 5</abbr>' + 
+                  '<abbr class="dtend" title="2005-10-08">7</abbr>' + 
+                  '<span class="location">Argent Hotel, San Francisco, CA</span>' + 
+                '</a>' + 
+              '</span>');
+              assertTrue('Testing span is not null', obj.span != null);
+              assertEquals('Testing span["@class"] equals vevent', obj.span["@class"], "vevent");
+              assertTrue('Testing span.a is not null', obj.span.a != null);
+              assertEquals('Testing span.a["@class"] equals url', obj.span.a["@class"], "url");
+              assertEquals('Testing span.a["@href"] equals http://www.web2con.com/', obj.span.a["@href"], "http://www.web2con.com/");
+              assertTrue('Testing span.a.span is not null', obj.span.a.span != null);
+              assertTrue('Testing span.a.span is an instance of Array', obj.span.a.span instanceof Array);
+              assertEquals('Testing span.a.span.length equals 2', obj.span.a.span.length, 2);
+              assertEquals('Testing span.a.span[0]["@class"] equals summary', obj.span.a.span[0]["@class"], "summary");
+              assertEquals('Testing span.a.span[0]["#text"] equals Web 2.0 Conference', obj.span.a.span[0]["#text"], "Web 2.0 Conference");
+              assertEquals('Testing span.a.span[1]["@class"] equals location', obj.span.a.span[1]["@class"], "location");
+              assertEquals('Testing span.a.span[1]["#text"] equals Argent Hotel, San Francisco, CA', obj.span.a.span[1]["#text"], "Argent Hotel, San Francisco, CA");
+              assertTrue('Testing span.a.abbr is not null', obj.span.a.abbr != null);
+              assertTrue('Testing span.a.abbr is an instance of Array', obj.span.a.abbr instanceof Array);
+              assertEquals('Testing span.a.abbr.length equals 2', obj.span.a.abbr.length, 2);
+              assertEquals('Testing span.a.abbr[0]["@title"] equals 2005-10-05', obj.span.a.abbr[0]["@title"], "2005-10-05");
+              assertEquals('Testing span.a.abbr[0]["@class"] equals dtstart', obj.span.a.abbr[0]["@class"], "dtstart");
+              assertEquals('Testing span.a.abbr[0]["#text"] equals October 5', obj.span.a.abbr[0]["#text"], "October 5");
+              assertEquals('Testing span.a.abbr[1]["@title"] equals 2005-10-08', obj.span.a.abbr[1]["@title"], "2005-10-08");
+              assertEquals('Testing span.a.abbr[1]["@class"] equals dtend', obj.span.a.abbr[1]["@class"], "dtend");
+              assertEquals('Testing span.a.abbr[1]["#text"] equals 7', obj.span.a.abbr[1]["#text"], "7");
+
+            finished();
+          }
+        }
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/makeRequestTest.xml b/trunk/java/server/src/test/resources/endtoend/makeRequestTest.xml
new file mode 100644
index 0000000..e419e8e
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/makeRequestTest.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest"/>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var tests = {
+          /** Test fetching a makeRequest proxied call */
+          fetchMakeRequest: function() {
+            function receivedData(response) {
+              assertEquals('Text property not set', '{"key": "value"}', response.text);
+              assertEquals('Data property not set', '{"key": "value"}', response.data);
+              assertEquals('Response code not set', 200, response.rc);
+              assertEquals('Errors not an empty array', 0, response.errors.length);
+              finished();
+            }
+
+            gadgets.io.makeRequest('http://localhost:9003/test.json', receivedData);
+          },
+
+          /** Test fetching a makeRequest proxied for JSON */
+          fetchMakeRequestJson: function() {
+            function receivedData(response) {
+              assertEquals('Text property not set', '{"key": "value"}', response.text);
+              assertEquals('Data property not set', 'value', response.data.key);
+              assertEquals('Response code not set', 200, response.rc);
+              assertEquals('Errors not an empty array', 0, response.errors.length);
+              finished();
+            }
+
+            gadgets.io.makeRequest('http://localhost:9003/test.json', receivedData,
+              {CONTENT_TYPE: 'JSON'});
+          },
+
+          /** Test fetching a makeRequest proxied call that fails */
+          fetchMakeRequestJson: function() {
+            function receivedData(response) {
+              assertEquals('Response code not set', 404, response.rc);
+              assertEquals('Errors not an empty array', 1, response.errors.length);
+              finished();
+            }
+
+            gadgets.io.makeRequest('http://localhost:9003/doesntexist.txt', receivedData);
+          }
+        };
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/messageBundle.xml b/trunk/java/server/src/test/resources/endtoend/messageBundle.xml
new file mode 100644
index 0000000..506f41a
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/messageBundle.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Locale messages="messages.xml"/>
+    <Locale lang="ar" messages="messages_ar.xml" language_direction="rtl"/>
+    <Require feature="views" />
+    <Optional feature="content-rewrite">
+      <Param name="exclude-urls">.*</Param>
+    </Optional>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <span id="substituteHere">__MSG_TEST__</span>
+      <span id="bidi">__BIDI_DIR__</span>
+      
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        function expectsLtr() {
+            var prefs = new gadgets.Prefs();
+            return 'ar' != prefs.getLang();
+        }
+
+        function getExpectedText() {
+          if (expectsLtr()) {
+            return 'test FTW';
+          } else {
+            return 'WTF tset';
+          }
+        }
+
+        var tests = {
+          /** Test basic message insertion */
+          substituteInContent: function() {
+            var span = document.getElementById('substituteHere');
+            var expectedText = getExpectedText();
+            assertEquals('Text not substituted', expectedText, span.firstChild.data);
+            finished();
+          },
+
+          /** Test message availablity from Prefs */
+          prefsGetMsg: function() {
+            var prefs = new gadgets.Prefs();
+            var expectedText = getExpectedText();
+            assertEquals('getMsg not successful', expectedText, prefs.getMsg('TEST'));
+            finished();
+          },
+
+          /** Test BIDI replacement */
+          substituteBidi: function() {
+            var span = document.getElementById('bidi');
+            var expectedLtr = expectsLtr() ? 'ltr' : 'rtl';
+            assertEquals('BIDI not substituted', expectedLtr, span.firstChild.data);
+            finished();
+          }
+        }
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/messages.xml b/trunk/java/server/src/test/resources/endtoend/messages.xml
new file mode 100644
index 0000000..b6626ec
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/messages.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<messagebundle>
+  <msg name="TEST">test FTW</msg>
+</messagebundle>
\ No newline at end of file
diff --git a/trunk/java/server/src/test/resources/endtoend/messages_ar.xml b/trunk/java/server/src/test/resources/endtoend/messages_ar.xml
new file mode 100644
index 0000000..cd77cc9
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/messages_ar.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<messagebundle>
+  <!-- No, not really Arabic -->
+  <msg name="TEST">WTF tset</msg>
+</messagebundle>
\ No newline at end of file
diff --git a/trunk/java/server/src/test/resources/endtoend/opensocial-templates/compiler_test.js b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/compiler_test.js
new file mode 100644
index 0000000..6e2655a
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/compiler_test.js
@@ -0,0 +1,218 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function testSubstitution() {
+  var values = [
+    [ "hello world", null ],
+    [ "hello $world", null ],
+    [ "hello ${Cur} world", "'hello '+($this)+' world'" ],
+    [ "${Cur} hello", "($this)+' hello'" ],
+    [ "hello ${Cur}", "'hello '+($this)" ],
+    [ "$ ${Cur}", "'$ '+($this)" ],
+    [ "$${Cur}", "'$'+($this)" ],
+    [ "${Cur} ${Context.Index}", "($this)+' '+($_ir($loop, 'Index'))"],
+    [ "a ${Cur} b ${Context.Index} c", "'a '+($this)+' b '+($_ir($loop, 'Index'))+' c'"]
+  ];
+  for (var i = 0; i < values.length; i++) {
+    var compiled = os.parseAttribute_(values[i][0]);
+    assertEquals(values[i][1], compiled);
+  }
+}
+
+/**
+ * Unit test for compiler identifier wrapping.
+ *    "'a'",
+ *    "foo",
+ *    "foo + bar",
+ *    "foo||bar",
+ *    "foo.bar",
+ *    "foo().bar",
+ *    "foo.bar(baz)",
+ *    "foo.bar().baz",
+ *    "foo('a').bar",
+ *    "foo[bar].baz",
+ *    "foo.bar.baz",
+ *    "$my('foo').bar",
+ *    "$cur($context, 'person').ProfileName",
+ *    "foo(bar)[baz]"
+ */
+function testWrapIdentifiers() {
+  assertEquals("$_ir($_ir($context, 'foo'), 'bar')", 
+      os.wrapIdentifiersInExpression("foo.bar"));
+
+  assertEquals("$_ir($_ir($context, 'data'), 'array')()",
+      os.wrapIdentifiersInExpression("data.array()"));
+
+  assertEquals("$_ir($_ir($context, 'data')(), 'array')", 
+      os.wrapIdentifiersInExpression('data().array'));
+
+  // Check that namespaced tags are treated as single identifiers.      
+  assertEquals("$_ir($context, 'os:Item')", 
+      os.wrapIdentifiersInExpression("os:Item"));
+      
+  // Check that a colon surrounded by spaces is not treated as 
+  // part of identifier 
+  assertEquals("$_ir($context, 'foo') ? $_ir($context, 'bar') : " + 
+      "$_ir($context, 'baz')",
+      os.wrapIdentifiersInExpression("foo ? bar : baz"));
+}
+
+function testTransformVariables() {
+  assertEquals("$this.foo", os.transformVariables_('$cur.foo'));
+}
+
+/**
+ * Unit test for JSP operator support.
+ */
+function testOperators() {
+  var data = {A:42, B:101};
+
+  var testData = [
+    { template:"${A lt B}", expected:"true" },
+    { template:"${A gt B}", expected:"false" },
+    { template:"${A eq A}", expected:"true" },
+    { template:"${A neq A}", expected:"false" },
+    { template:"${A lte A}", expected:"true" },
+    { template:"${A lte B}", expected:"true" },
+    { template:"${A gte B}", expected:"false" },
+    { template:"${A gte A}", expected:"true" },
+    { template:"${A eq " + data.A + "}", expected:"true" },
+    { template:"${(A eq A) ? 'PASS' : 'FAIL'}", expected:"PASS" },
+    { template:"${not true}", expected:"false" },
+    { template:"${A eq A and B eq B}", expected:"true" },
+    { template:"${A eq A and false}", expected:"false" },
+    { template:"${false or A eq A}", expected:"true" },
+    { template:"${false or false}", expected:"false" }
+    //TODO: precedence, parenthesis
+  ];
+
+  for (var i = 0; i < testData.length; i++) {
+    var testEntry = testData[i];
+    var template = os.compileTemplateString(testEntry.template);
+    var resultNode = template.render(data);
+    var resultStr = resultNode.firstChild.innerHTML;
+    assertEquals(resultStr, testEntry.expected);
+  }
+}
+
+function testCopyAttributes() {
+  var src = document.createElement('div');
+  var dst = document.createElement('div');
+  src.setAttribute('attr', 'test');
+  src.setAttribute('class', 'foo');
+  os.copyAttributes_(src, dst);
+  assertEquals('test', dst.getAttribute('attr'));
+  // TODO htmlunit >2.1 fails this test, but it's okay in the browser
+  //assertEquals('foo', dst.getAttribute('className'));
+  //assertEquals('foo', dst.className);
+
+}
+
+/**
+ * Tests TBODY injection.
+ */
+function testTbodyInjection() {
+  var src, check, template, output;
+  
+  // One row.
+  src = "<table><tr><td>foo</td></tr></table>";
+  check = "<table><tbody><tr><td>foo</td></tr></tbody></table>";
+  template = os.compileTemplateString(src);
+  output = template.templateRoot_.innerHTML;
+  output = output.toLowerCase();
+  output = output.replace(/\s/g, '');
+  assertEquals(check, output);
+  
+  // Two rows.
+  src = "<table><tr><td>foo</td></tr><tr><td>bar</td></tr></table>";
+  check = "<table><tbody><tr><td>foo</td></tr>" + 
+      "<tr><td>bar</td></tr></tbody></table>";
+  template = os.compileTemplateString(src);
+  output = template.templateRoot_.innerHTML;
+  output = output.toLowerCase();
+  output = output.replace(/\s/g, '');
+  assertEquals(check, output);  
+}
+
+function testEventHandlers() {
+  var src, template, output;
+  
+  window['testEvent'] = function(value) {
+    window['testValue'] = value;
+  };
+  
+  // Static handler
+  src = "<button onclick=\"testEvent(true)\">Foo</button>";
+  template = os.compileTemplateString(src);
+  output = template.render();
+  // Append to document to enable events
+  document.body.appendChild(output);
+  window['testValue'] = false;
+  output.firstChild.click();
+  document.body.removeChild(output);
+  assertEquals(true, window['testValue']);
+  
+  // Dynamic handler
+  src = "<button onclick=\"testEvent('${title}')\">Foo</button>";
+  template = os.compileTemplateString(src);
+  output = template.render({ title: 'foo' });
+  // Append to document to enable events
+  document.body.appendChild(output);
+  window['testValue'] = false;
+  output.firstChild.click();
+  document.body.removeChild(output);
+  assertEquals('foo', window['testValue']); 
+}
+
+function testNestedIndex() {
+  var src, template, output;
+  
+  src = '<table><tr repeat="list" var="row" context="x">' + 
+      '<td repeat="row" context="y">${x.Index},${y.Index}</td></tr></table>';
+  template = os.compileTemplateString(src);
+  output = template.render({ list: [ ['a', 'b'], ['c', 'd'] ] });
+  //                           table  /  tbody  /   tr    /   td
+  assertEquals('1,1', output.lastChild.lastChild.lastChild.lastChild.innerHTML);
+};
+
+function testLoopNullDefaultValue() {
+  var src = '<div repeat="foo">a</div>';
+  var template = os.compileTemplateString(src);
+  var select = template.templateRoot_.firstChild.getAttribute("jsselect");
+  assertEquals("$_ir($context, 'foo', $_ea)", select); 
+}
+
+function testGetFromContext() {
+  // JSON context
+  var context = { foo: 'bar' };
+  assertEquals('bar', os.getFromContext(context, 'foo'));
+  
+  // JsEvalContext  
+  context = os.createContext(context);
+  assertEquals('bar', os.getFromContext(context, 'foo'));
+  
+  // Variable from context
+  context.setVariable('baz', 'bing');
+  assertEquals('bing', os.getFromContext(context, 'baz'));
+  
+  // Non-existent value
+  assertEquals('', os.getFromContext(context, 'title'));
+  
+  // Non-existent value with default
+  assertEquals(null, os.getFromContext(context, 'title', null));
+}
diff --git a/trunk/java/server/src/test/resources/endtoend/opensocial-templates/container_test.js b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/container_test.js
new file mode 100644
index 0000000..da879ba
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/container_test.js
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+
+function testTemplateType() {
+  assertTrue(os.Container.isTemplateType_('text/template'));
+  assertTrue(os.Container.isTemplateType_('text/os-template'));
+  assertTrue(!os.Container.isTemplateType_('os-template'));
+}
+
+function testRegisterTemplates() {
+  os.Container.registerDocumentTemplates();
+  assertNotNull(os.getTemplate('os:Test'));
+  os.Container.processInlineTemplates();
+  var el = document.getElementById('test');
+  assertNotNull(el);
+  assertEquals('tag template', domutil.getVisibleText(el));
+}
diff --git a/trunk/java/server/src/test/resources/endtoend/opensocial-templates/loader_test.js b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/loader_test.js
new file mode 100644
index 0000000..8063391
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/loader_test.js
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Unit test for injecting JavaScript into the global scope with the 
+ * os.Loader.
+ */
+function testInjectJavaScript() {
+  var jsCode = "function testFunction() { return 'foo'; }";
+  os.Loader.injectJavaScript(jsCode);
+  assertTrue(window.testFunction instanceof Function);
+  assertEquals(window.testFunction(), 'foo');
+}
+
+/**
+ * Unit test for injecting CSS through the os.Loader.
+ */
+function testInjectStyle() {
+  var cssCode = '.testCSS { width: 100px; height: 200px; }';
+  os.Loader.injectStyle(cssCode);
+  var rule = getStyleRule('.testCSS');
+  assertNotNull(rule);
+  assertEquals(rule.style.width, '100px');
+  assertEquals(rule.style.height, '200px');
+}
+
+/**
+ * @type {String} Template XML data for testLoadContent.
+ */
+var testContentXML =
+    '<Templates xmlns:test="http://www.google.com/#test">' +
+    '  <Namespace prefix="test" url="http://www.google.com/#test"/>' +
+    '  <Template tag="test:tag">' +
+    '    <div id="tag"></div>' +
+    '  </Template>' +
+    '  <JavaScript>' +
+    '    function testJavaScript() {' +
+    '      return "testJavaScript";' +
+    '    }' +
+    '  </JavaScript>' +
+    '  <Style>' +
+    '    .testStyle {' +
+    '      width: 24px;' +
+    '    }' +
+    '  </Style>' +
+    '  <TemplateDef tag="test:tagDef">' +
+    '    <Template>' +
+    '      <div id="tagDef"></div>' +
+    '    </Template>' +
+    '    <JavaScript>' +
+    '      function testJavaScriptDef() {' +
+    '        return "testJavaScriptDef";' +
+    '      }' +
+    '    </JavaScript>' +
+    '    <Style>' +
+    '      .testStyleDef {' +
+    '        height: 42px;' +
+    '      }' +
+    '    </Style>' +
+    '  </TemplateDef>' +
+    '</Templates>';
+
+/**
+ * System test for os.loadContent functionality. This tests
+ * all functionality except for XHR.
+ */
+function testLoadContent() {
+  os.Loader.loadContent(testContentXML);
+
+  // Verify registered tags.
+  var ns = os.nsmap_['test'];
+  assertNotNull(ns);
+  assertTrue(ns['tag'] instanceof Function);
+  assertTrue(ns['tagDef'] instanceof Function);
+
+  // Verify JavaScript functions.
+  assertTrue(window['testJavaScript'] instanceof Function);
+  assertEquals(window.testJavaScript(), 'testJavaScript');
+  assertTrue(window['testJavaScriptDef'] instanceof Function);
+  assertEquals(window.testJavaScriptDef(), 'testJavaScriptDef');
+
+  // Verify styles.
+  var rule = getStyleRule('.testStyle');
+  assertNotNull(rule);
+  assertEquals(rule.style.width, '24px');
+  var ruleDef = getStyleRule('.testStyleDef');
+  assertNotNull(ruleDef);
+  assertEquals(ruleDef.style.height, '42px');
+}
+
+/**
+ * Utility function for retrieving a style rule by selector text
+ * if its available.
+ * @param {string} name Selector text name.
+ * @return {Object} CSSRule object.
+ */
+function getStyleRule(name) {
+  var sheets = document.styleSheets;
+  for (var i = 0; i < sheets.length; ++i) {
+    var rules = sheets[i].cssRules || sheets[i].rules;
+    if (rules) {
+      for (var j = 0; j < rules.length; ++j) {
+        if (rules[j].selectorText == name
+            //hack for WebKit Quirks mode
+            || rules[j].selectorText == name.toLowerCase()) {
+          return rules[j];
+        }
+      }
+    }
+  }
+  return null;
+}
diff --git a/trunk/java/server/src/test/resources/endtoend/opensocial-templates/os_test.js b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/os_test.js
new file mode 100644
index 0000000..edf6f2c
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/os_test.js
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Unit test for testing the behavior of the OpenSocial identifier resolver.
+ */
+function testResolveOpenSocialIdentifier() {
+  /**
+   * Sample class with a property, property getter, custom getField and a get()
+   * method.
+   */
+  var TestClass = function() {
+    this.foo = 'fooData';
+    this.bar_ = 'barData';
+    this.thumbnailUrl_ = 'thumbnailUrlData';
+    this.responseItem_ = {};
+    this.responseItem_.getData = function() {
+      return 'responseItemData';
+    };
+  };
+  
+  TestClass.prototype.getBar = function() {
+    return this.bar_;
+  };
+  
+  TestClass.prototype.getField = function(field) {
+    if (field == 'THUMBNAIL_URL') {
+      return this.thumbnailUrl_;
+    }
+    return null;
+  };
+  
+  TestClass.prototype.get = function(field) {
+    if (field == 'responseItem') {
+      return this.responseItem_;
+    }
+    return null;
+  };
+  
+  var obj = new TestClass(); 
+  
+  assertEquals('fooData', os.resolveOpenSocialIdentifier(obj, 'foo'));
+  assertEquals('barData', os.resolveOpenSocialIdentifier(obj, 'bar'));
+  assertEquals('thumbnailUrlData',
+      os.resolveOpenSocialIdentifier(obj, 'THUMBNAIL_URL'));
+  assertEquals('responseItemData',
+      os.resolveOpenSocialIdentifier(obj, 'responseItem'));
+}
\ No newline at end of file
diff --git a/trunk/java/server/src/test/resources/endtoend/opensocial-templates/ost_test.xml b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/ost_test.xml
new file mode 100644
index 0000000..09bc6b1
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/ost_test.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="TemplatesEndToEndTest">
+    <Require feature="opensocial-templates">
+      <Param name="disableAutoProcessing">true</Param>
+    </Require>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+    <script type="text/javascript" src="/opensocial-templates/testadapter.js"></script>
+    <!-- the JsUnit tests -->
+    <script type="text/javascript" src="/opensocial-templates/compiler_test.js"></script>
+    <script type="text/javascript" src="/opensocial-templates/container_test.js"></script>
+    <script type="text/javascript" src="/opensocial-templates/loader_test.js"></script>
+    <script type="text/javascript" src="/opensocial-templates/os_test.js"></script>
+    <script type="text/javascript" src="/opensocial-templates/util_test.js"></script>
+
+    <div style="display: none">
+      <div id="domSource">
+        <ul>
+          <li>one</li>
+          <li>two</li>
+        </ul>
+        <b>bold</b>
+      </div>
+      <div id="domTarget">
+      </div>
+    </div>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/opensocial-templates/testadapter.js b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/testadapter.js
new file mode 100644
index 0000000..4acbc4e
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/testadapter.js
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+function findTests() {
+  // The following tests fail due to HtmlUnit limitation.
+  // If a name exists in this object, it is excluded from testing.
+  var excludedTests = {
+    testEventHandlers : 1,
+    testInjectStyle : 1,
+    testLoadContent : 1,
+    testInjectJavaScript : 1,
+    testRegisterTemplates : 1
+  };
+
+  var testSource = typeof RuntimeObject != 'undefined' ?
+                   RuntimeObject('test' + '*') : self;
+  var tests = {};
+  for (i in testSource) {
+    if (i.substring(0, 4) == 'test' && typeof(testSource[i]) == 'function'
+      && ! (i in excludedTests)) {
+      tests[i] = testSource[i];
+    }
+  }
+  return tests;
+}
+
+function assertTrue(value) {
+  if (!value) {
+    throw "assertTrue() failed: ";
+  }
+}
+
+function assertFalse(value) {
+  if (value) {
+    throw "assertFalse() failed: ";
+  }
+}
+
+function assertEquals(a, b) {
+  if (a != b) {
+    throw "assertEquals() failed: " +
+          "\nExpected \"" + a + "\", was \"" + b + "\"";
+  }
+}
+
+function assertNotNull(value) {
+  if (value === null) {
+    throw "assertTrue() failed: ";
+  }
+}
+
+function allTests() {
+  var tests = findTests();
+  for (var testMethod in tests) {
+    alert(testMethod);
+    tests[testMethod]();
+    alert("FINISHED");
+  }
+}
+
+gadgets.util.registerOnLoadHandler(allTests);
diff --git a/trunk/java/server/src/test/resources/endtoend/opensocial-templates/util_test.js b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/util_test.js
new file mode 100644
index 0000000..f4a3391
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/opensocial-templates/util_test.js
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Unit test for various DOM utils.
+ */
+function testDomUtils() {
+  var sourceNode = document.getElementById('domSource');
+  var targetNode = document.getElementById('domTarget');
+  var html = sourceNode.innerHTML;
+  targetNode.innerHTML = '';
+
+  // test appendChildren
+  os.appendChildren(sourceNode, targetNode);
+  assertEquals(html, targetNode.innerHTML);
+
+  // test removeChildren
+  os.removeChildren(targetNode);
+  assertEquals(0, targetNode.childNodes.length);
+}
+
+/**
+ * Unit test for createPropertyGetter
+ */
+function testGetPropertyGetterName() {
+  assertEquals('getFoo', os.getPropertyGetterName('foo'));
+  assertEquals('getFooBar', os.getPropertyGetterName('fooBar'));
+}
+
+/**
+ * Unit test for convertConstantToCamelCase.
+ */
+function testConvertToCamelCase() {
+  assertEquals('foo', os.convertToCamelCase('FOO'));
+  assertEquals('fooBar', os.convertToCamelCase('FOO_BAR'));
+  assertEquals('fooBarBaz', os.convertToCamelCase('FOO_BAR__BAZ'));
+}
diff --git a/trunk/java/server/src/test/resources/endtoend/osapi/activitiesTest.xml b/trunk/java/server/src/test/resources/endtoend/osapi/activitiesTest.xml
new file mode 100644
index 0000000..f02fedb
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/osapi/activitiesTest.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="osapi" />
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var tests = {
+          /** Test fetching a specific ID's activities */
+          fetchId: function() {
+            function receivedData(response) {
+              assertFalse("Should not have error", response.error);
+              assertEquals("Should have 3 activities", 3, response.totalResults);
+              finished();
+            }
+            osapi.activities.get({ userId : 'canonical', groupId : '@self'}).execute(receivedData);
+          },
+          /** Test fetching viewer's activity */
+          fetchViewerActivities: function() {
+            function receivedData(response) {
+              assertFalse("Should not have error", response.error);
+              assertEquals("Should have 1 activity", 1, response.totalResults);
+              assertEquals("Titles don't match", "yellow", response.list[0].title);
+              assertEquals("Body doesn't match", "what a color!", response.list[0].body);
+              finished();
+            }
+
+            osapi.activities.get().execute(receivedData);
+          },
+          /** Test fetching viewer's friends' activities */
+          fetchViewerFriendActivities: function() {
+            function receivedData(response) {
+              assertFalse("Should not have error", response.error);
+              assertEquals("Should have 2 activities", 2, response.totalResults);
+              assertEquals("Titles don't match", "Jane just posted a photo of a monkey", response.list[0].title);
+              assertEquals("Body doesn't match", "and she thinks you look like him", response.list[0].body);
+              assertEquals("Body doesn't match", "Jane says George likes yoda!", response.list[1].title);
+              assertEquals("Body doesn't match", "or is it you?", response.list[1].body);
+              finished();
+            }
+
+            osapi.activities.get({groupId : '@friends'}).execute(receivedData);
+          },
+          /** Test creating an activity */
+          createNewActivity: function() {
+            function newData(response) {
+              assertFalse("Should not have error", response.error);
+              assertEquals("Should have 3 activities", 3, response.totalResults);
+              assertEquals("Titles don't match", "New activity", response.list[2].title);
+              assertEquals("Body doesn't match", "Hey", response.list[2].body);
+              finished();
+            }
+
+            function receivedData(response) {
+              assertFalse("Should not have error", response.error);
+              assertEquals("Should have 0 activities", undefined, response.totalResults);
+              osapi.activities.get({userId:'canonical'}).execute(newData);
+            }
+
+            osapi.activities.create({userId:'canonical', activity : {title : 'New activity', body : 'Hey'}}).execute(receivedData);
+          }
+
+
+        };
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/osapi/appdataTest.xml b/trunk/java/server/src/test/resources/endtoend/osapi/appdataTest.xml
new file mode 100644
index 0000000..9660ccd
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/osapi/appdataTest.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="osapi" />
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var tests = {
+          /** Test fetching a specific ID's appdata */
+          fetchId: function() {
+            function receivedData(response) {
+              assertFalse("Should not have error", response.error);
+              assertEquals("Should have 0 appdata items", undefined, response.length);
+              finished();
+            }
+
+            osapi.appdata.get({ userId : 'canonical', keys : ['gifts']}).execute(receivedData);
+          },
+          /** Test creating appdata, retrieving appdata and deleting appdata */
+          appdataWrite: function() {
+            function assertNoErrorAndEmptyResponse(response) {
+              assertFalse("Should not have error", response.error);
+              assertEquals("Should have 0 appdata items", undefined, response.length);
+            }
+
+            function getEmptyResponse(response) {
+              assertNoErrorAndEmptyResponse(response);
+              finished();
+            }
+ 
+            function deletedResponse(response) {
+              assertNoErrorAndEmptyResponse(response);
+              osapi.appdata.get({userId: '@viewer', keys: ['pet']}).execute(getEmptyResponse);
+            }
+
+            function getPopulatedResponse(data) {
+              assertFalse("Should not have error", data.error);
+              assertEquals("Should have 1 user key", data.length);
+              for (var id in data) if (data.hasOwnProperty(id)) {
+                var mydata = data[id];
+                break;
+              }
+              var pet = mydata["pet"];
+              assertEquals("Pet should match", "crazed monkey", pet);
+              osapi.appdata["delete"]({userId: '@viewer', keys: ['pet']}).execute(deletedResponse);
+            }
+
+            function updateResponse(response) {
+              assertNoErrorAndEmptyResponse(response);
+              osapi.appdata.get({userId: '@viewer', keys: ['pet']}).execute(getPopulatedResponse);
+            }
+
+            osapi.appdata.update({userId: '@viewer', data: {pet: "crazed monkey"}}).execute(
+                updateResponse);
+          }
+        };
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/osapi/batchTest.xml b/trunk/java/server/src/test/resources/endtoend/osapi/batchTest.xml
new file mode 100644
index 0000000..399a946
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/osapi/batchTest.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="OSLite EndToEndTest">
+    <Require feature="osapi"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var assertFriends = function(response) {
+          assertEquals('Wrong friend count', 4, response.totalResults);
+          assertEquals('Should be Johnny', 'Johnny', response.list[0].displayName);
+          assertEquals('Should be Janey', 'Janey', response.list[1].displayName);
+          assertEquals('Should be Georgey', 'Georgey', response.list[2].displayName);
+          assertEquals('Should be Maija', 'Maija', response.list[3].displayName);
+        };
+
+        var tests = {
+          /** Test simple batch, fetching the owner's friends */
+          fetchBatchWithOneOwnerFriendsRequest: function() {
+
+            function receivedData(response) {
+              assertFalse('Data error', response.error);
+              assertFriends(response.self);
+              finished();
+            }
+
+            var batch = osapi.newBatch().
+                add('self', osapi.people.getOwnerFriends({fields : ["id", "displayName"]})).execute(receivedData);
+          },
+
+          /** Test batch with two friends requests */
+          fetchBatchWithTwoPeopleCalls: function() {
+            function receivedData(response) {
+              assertFalse('Data error', response.error);
+              assertTrue('Jane is true - ' + gadgets.json.stringify(response), response.jane);
+              assertTrue('John is true - ' + gadgets.json.stringify(response), response.john);
+//              assertFriends(response.jane.concat(response.john));
+              finished();
+            }
+            var batch = osapi.newBatch()
+               .add('john', osapi.people.get({userId : ['john.doe'], groupId : '@friends', fields : ["id", "displayName"]}))
+               .add('jane', osapi.people.get({userId : ['jane.doe'], groupId : '@friends', fields : ["id", "displayName"]}))
+               .execute(receivedData);
+          },
+
+          /** Test batch with activity and friends requests */
+          fetchBatchWithMixedCalls: function() {
+            function receivedData(response) {
+              assertFalse("Should not have error", response.error);
+              var activityResponse = response.activities;
+              assertEquals("Should have 1 activity", 1, activityResponse.totalResults);
+              assertEquals("Titles don't match", "yellow", activityResponse.list[0].title);
+              assertEquals("Body doesn't match", "what a color!", activityResponse.list[0].body);
+
+              var peopleResponse = response.people;
+              assertFriends(peopleResponse);
+              finished();
+            }
+
+            var batch = osapi.newBatch().add('activities', osapi.activities.get()).
+                add('people', osapi.people.getOwnerFriends({fields : ["id", "displayName"]})).execute(receivedData);
+          }
+        };
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/osapi/errorTest.xml b/trunk/java/server/src/test/resources/endtoend/osapi/errorTest.xml
new file mode 100644
index 0000000..3b21d36
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/osapi/errorTest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="osapi" />
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var tests = {
+          internalErrorTest: function() {
+            function receivedData(response) {
+              assertTrue("Should have error", response.error);
+              assertEquals("Not an internalError", "internalError", response.error.code);
+              finished();
+            }
+
+            osapi.people.getViewer().execute(receivedData);
+          }
+        };
+
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/osapi/peopleTest.xml b/trunk/java/server/src/test/resources/endtoend/osapi/peopleTest.xml
new file mode 100644
index 0000000..30baa2d
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/osapi/peopleTest.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="OSLite EndToEndTest">
+    <Require feature="osapi"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var assertFriends = function(response) {
+          assertEquals('Wrong friend count - ' + gadgets.json.stringify(response), 4, response.totalResults)
+          assertTrue('No response - ' + gadgets.json.stringify(response), response.list);
+          assertEquals('Wrong friend count - ' + gadgets.json.stringify(response), 4, response.list.length);
+          assertTrue('arg 0 is defined', response.list[0]);
+          assertEquals('Should be Johnny', 'Johnny', response.list[0].displayName);
+          assertTrue('arg 1 is defined', response.list[1]);
+          assertEquals('Should be Janey', 'Janey', response.list[1].displayName);
+          assertTrue('arg 2 is defined', response.list[2]);
+          assertEquals('Should be Georgey', 'Georgey', response.list[2].displayName);
+          assertTrue('arg 3 is defined', response.list[3]);
+          assertEquals('Should be Maija', 'Maija', response.list[3].displayName);
+        };
+
+        var tests = {
+          /** Test fetching the owner's friends, which should be 'canonical' */
+          fetchOwnerFriends: function() {
+            function receivedData(response) {
+              assertFalse('Data error', response.error);
+              assertFriends(response);
+
+              finished();
+            }
+
+            var req = osapi.people.getOwnerFriends({fields : ["id", "displayName"]}).execute(receivedData);
+          },
+
+          /** Test fetching 'maija.m' friends, of which there are none */
+          fetchEmptyFriendsById: function() {
+            function receivedData(response) {
+              assertFalse('Data error', response.error);
+              assertEquals('Wrong friend count - '  + response.error, 0, response.totalResults);
+              finished();
+            }
+
+            osapi.people.get({userId : 'maija.m', groupId : '@friends', fields : ["id", "displayName"]}).execute(receivedData);
+          },
+
+          /** Test fetching friends of a list of users */
+          fetchPluralUsers: function() {
+            function receivedData(response) {
+              assertFalse('Data error', response.error);
+              assertFriends(response);
+              finished();
+            }
+
+            osapi.people.get({userId : ["john.doe", "jane.doe"], groupId : '@friends', fields : ["id", "displayName"]}).execute(receivedData);
+          }
+        };
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/osapi/personTest.xml b/trunk/java/server/src/test/resources/endtoend/osapi/personTest.xml
new file mode 100644
index 0000000..70fc60c
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/osapi/personTest.xml
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="osapi" />
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var tests = {
+          /** Test fetching a specific ID */
+          fetchId: function() {
+            function receivedData(response) {
+              var data = gadgets.json.stringify(response);
+              assertFalse("Should not have error - " + data, response.error);
+              assertEquals("Names don't match - " + data, "Shin Digg", response.displayName);
+              finished();
+            }
+            osapi.people.get({ userId : 'canonical', fields : ["id", "displayName"]}).execute(receivedData);
+          },
+
+          /** Test fetching the owner, which should be "canonical" */
+          fetchOwner: function() {
+            function receivedData(response) {
+              var data = gadgets.json.stringify(response);
+              assertFalse("Should not have error - " + data, response.error);
+              assertEquals("Names don't match - " + data, "Shin Digg", response.displayName);
+              finished();
+            }
+            osapi.people.getOwner({ userId : 'canonical', fields : ["id", "displayName"]}).execute(receivedData);
+          },
+
+          /** Test fetching the viewer, which should be "john.doe" */
+          fetchViewer: function() {
+            function receivedData(response) {
+              var data = gadgets.json.stringify(response);
+              assertFalse("Should not have error - " + data, response.error);
+              assertEquals("Names don't match - " + data, "Johnny", response.displayName);
+              finished();
+            }
+            osapi.people.getViewer({ userId : 'canonical', fields : ["id", "displayName"]}).execute(receivedData);
+          },
+
+          /** Test fetching the owner and viewer as a batch */
+          fetchOwnerAndViewer: function() {
+            function receivedData(response) {
+              var data = gadgets.json.stringify(response);
+              assertFalse("Should not have error - " + data, response.error);
+              assertEquals("Names don't match - " + data, "Shin Digg", response.list[0].displayName);
+              assertEquals("Names don't match - " + data, "Johnny", response.list[1].displayName);
+              finished();
+            }
+            osapi.people.get({ userId : ['@owner', '@viewer'], fields : ["id", "displayName"]}).execute(receivedData);
+          },
+
+          fetchPersonNotFound: function() {
+            function receivedData(response) {
+              var data = gadgets.json.stringify(response);
+              assertTrue("Should have error - " + data, response.error);
+              assertEquals("Not a badRequest - " + data, "400", response.error.code);
+              finished();
+            }
+
+            osapi.people.get({ userId : "not.a.real.id", fields : ["id", "displayName"]}).execute(receivedData);
+          },
+
+          /** Test updating a person */
+          updatePersonWhenViewerAllowed: function() {
+            function receivedData(response) {
+              var data = gadgets.json.stringify(response);
+              assertFalse('Data error', response.error);
+              assertEquals("Ids don't match - " + data, "john.doe", response.id);
+              assertEquals("Thumbnails URLs don't match - " + data, "http://url.com", response.thumbnailUrl);
+              finished();
+            }
+
+            // viewer is "john.doe" from security token
+            osapi.people.update({person : {thumbnailUrl: "http://url.com"}}).execute(receivedData);
+          },
+
+          /** Test updating a person */
+          updatePersonWhenViewerNotAllowed: function() {
+            function receivedData(response) {
+              var data = gadgets.json.stringify(response);
+              assertTrue('Should have error - ' + data, response.error);
+              assertEquals("Forbidden " + data, "403", response.error.code);
+              finished();
+            }
+
+            // viewer is "john.doe" from security token
+            osapi.people.update({userId: "canonical", person : {thumbnailUrl: "http://url.com"}}).execute(receivedData);
+          },
+
+        };
+
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/pipeliningTest.xml b/trunk/java/server/src/test/resources/endtoend/pipeliningTest.xml
new file mode 100644
index 0000000..afee84e
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/pipeliningTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module xmlns:os="http://ns.opensocial.org/2008/markup"
+        xmlns:osx="http://ns.opensocial.org/2009/extensions">
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="views" />
+    <Optional feature="content-rewrite">
+      <Param name="exclude-urls">.*</Param>
+    </Optional>
+  </ModulePrefs>
+  <Content type="html" href="http://localhost:9003/echo">
+    <!--  Load the canonical user -->
+    <os:PeopleRequest key="me" userId="canonical"/>
+    <!--  Load a JSON file -->
+    <os:HttpRequest key="json" href="test.json"/>
+    <!--  Process a variable -->
+    <osx:Variable key="var" value="${json.content.key}"/>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/templateLibrary.xml b/trunk/java/server/src/test/resources/endtoend/templateLibrary.xml
new file mode 100644
index 0000000..37d9a12
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/templateLibrary.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="opensocial-templates">
+      <Param name="requireLibrary">testLibrary.xml</Param>
+    </Require>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+        var tests = {
+          /** Test the library script executed */
+          libraryScriptExecuted: function() {
+            assertEquals('Library script didn\'t run', true, templateLibraryExecuted);
+            finished();
+          }
+        };
+      </script>
+      <script type="text/os-template" xmlns:os="http://ns.opensocial.org/2008/markup"
+          xmlns:test="#test">
+        <test:Tag content="Hello world"/>
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml b/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml
new file mode 100644
index 0000000..e3b72fa
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/templateRewriter.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="opensocial-data" />
+    <Require feature="opensocial-templates"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
+        <os:PeopleRequest key="friends" userId="@viewer" groupId="@friends"/>
+        <os:HttpRequest key="json" href="test.json"/>
+        <os:HttpRequest key="text" href="test.json" format="text"/>
+      </script>
+
+      <script type="text/os-template" xmlns:os="http://ns.opensocial.org/2008/markup"
+          xmlns:osx="http://ns.opensocial.org/2009/extensions">
+        <ul id="attrs">
+          <li repeat="${friends}">
+            <span id="id${Context.Index}">${name.givenName}</span>
+          </li>
+        </ul>
+
+        <ul id="repeatTag">
+          <os:Repeat expression="${friends}" if="${Context.Index == 1}">
+            <li>${name.givenName}</li>
+          </os:Repeat>
+        </ul>
+
+        <ul id="ifTag">
+          <li repeat="${friends}">
+            <os:If condition="${Context.Index == 2}">
+              <b>${Cur.name.givenName}</b>
+            </os:If>
+          </li>
+        </ul>
+        
+        <span id="json">${json.content.key}</span>
+        <span id="text">${text.content}</span>
+        <span id="mutate" oncreate="this.innerHTML='mutated'"></span>
+        
+        <osx:Variable key="sum" value="0"/>
+        <osx:Variable key="max" value="0"/>
+        <os:Repeat expression="${osx:parseJson('[10,15,-5,25]')}">
+          <osx:Variable key="max" value="${Cur > max ? Cur : max}"/>
+          <osx:Variable key="sum" value="${sum + Cur}"/>
+        </os:Repeat>
+        <span id="sum">${sum}</span>
+        <span id="max">${max}</span>
+      </script>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/test.json b/trunk/java/server/src/test/resources/endtoend/test.json
new file mode 100644
index 0000000..9082d25
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/test.json
@@ -0,0 +1 @@
+{"key": "value"}
\ No newline at end of file
diff --git a/trunk/java/server/src/test/resources/endtoend/testLibrary.xml b/trunk/java/server/src/test/resources/endtoend/testLibrary.xml
new file mode 100644
index 0000000..9f17d50
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/testLibrary.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Templates xmlns:test="#test">
+  <Namespace prefix="test" url="#test"/>
+  <JavaScript>var templateLibraryExecuted = true;</JavaScript>
+  <Style>p {color: red}</Style>
+  <Template tag="test:Tag">
+    <p>${content}</p>
+  </Template>
+</Templates>
diff --git a/trunk/java/server/src/test/resources/endtoend/testframework.js b/trunk/java/server/src/test/resources/endtoend/testframework.js
new file mode 100644
index 0000000..743f2e6
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/testframework.js
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+var tests;
+
+function assertTrue(msg, value) {
+  if (!value) {
+    throw "assertTrue() failed: " + msg;
+  }
+}
+
+function assertFalse(msg, value) {
+  if (value) {
+    throw "assertFalse() failed: " + msg;
+  }
+}
+
+function assertEquals(msg, a, b) {
+  if (a != b) {
+    throw "assertEquals() failed: " + msg +
+        "\nExpected \"" + a + "\", was \"" + b + "\"";
+  }
+}
+
+/**
+ * Signals the server code that a test successfully finished.  This
+ * method must be called to verify that a test completed successfully,
+ * instead of simply failing to load.
+ */
+
+function finished() {
+  alert("FINISHED");
+  // After finishing run the next test..
+  runOneTest();
+}
+
+/** Executes the test identifed by the testMethod URL parameter */
+function executeTest() {
+  var params = gadgets.util.getUrlParameters();
+  var testMethod = params["testMethod"];
+  if (!testMethod) {
+    throw "No testMethod parameter found.";
+  }
+
+  // "all": run all the tests
+  if ("all" == testMethod) {
+    allTests();
+  } else {
+    // Show an alert for the test method name, identifying what test started.
+    alert(testMethod);
+
+    var testMethodFunction = tests[testMethod];
+    if (!testMethodFunction) {
+      throw "Test method " + testMethod + " not found.";
+    }
+
+    // Execute the test method
+    testMethodFunction();
+  }
+}
+
+var testFunctions = [];
+
+function runOneTest() {
+  var t = testFunctions.pop();
+  if (t) {
+    alert(t);
+    tests[t]();
+  }
+}
+function allTests() {
+  // Collect the test names and iterate through them serially
+  for (var testMethod in tests) {
+    testFunctions.push(testMethod);
+  }
+  runOneTest();
+}
+
+gadgets.util.registerOnLoadHandler(executeTest);
diff --git a/trunk/java/server/src/test/resources/endtoend/viewLevelElementsTest.xml b/trunk/java/server/src/test/resources/endtoend/viewLevelElementsTest.xml
new file mode 100644
index 0000000..b6a5333
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/viewLevelElementsTest.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<Module>
+  <ModulePrefs title="EndToEndTest">
+    <Require feature="views" />
+    <Require feature="opensocial" views="blank"/>
+    <Require feature="osapi">
+      <Param name="paramName">BAD_VALUE</Param>
+    </Require>
+    <Require feature="osapi" views="default">
+      <Param name="paramName">GOOD_VALUE</Param>
+    </Require>
+    <Optional feature="content-rewrite">
+      <Param name="exclude-urls">.*</Param>
+    </Optional>
+    <Locale messages="messages.xml"/>
+    <!-- This bundle should be the one used in substituting messages in the default view -->
+    <Locale messages="viewMessages.xml" views="default"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+    
+      <span id="substituteHere">__MSG_TEST__</span>
+          
+      <script type="text/javascript" src="/testframework.js"></script>
+      <script type="text/javascript">
+
+        var tests = {
+           viewFeaturesTest: function() {
+              var params = gadgets.util.getFeatureParameters('osapi');
+              var expected = "GOOD_VALUE";
+              assertTrue('Should be GOOD_VALUE', expected == params["paramName"]);
+              assertTrue('Missing content-rewrite feature', gadgets.util.hasFeature("content-rewrite"));
+              assertTrue('Should not have opensocial loaded', !gadgets.util.hasFeature("opensocial"));
+              finished();
+           },
+            
+           viewLocalesTest: function(){
+              var span = document.getElementById('substituteHere');
+              var expected = "test view FTW";
+              assertTrue('Wrong text substituted', expected == span.firstChild.data);
+              finished();
+           }
+        }
+      </script>
+    ]]>
+  </Content>
+  <Content type="html" view="blank">
+    <![CDATA[
+      <h1>Blank</h1>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/java/server/src/test/resources/endtoend/viewMessages.xml b/trunk/java/server/src/test/resources/endtoend/viewMessages.xml
new file mode 100644
index 0000000..834766e
--- /dev/null
+++ b/trunk/java/server/src/test/resources/endtoend/viewMessages.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you 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.
+-->
+<messagebundle>
+  <msg name="TEST">test view FTW</msg>
+</messagebundle>
diff --git a/trunk/java/social-api/pom.xml b/trunk/java/social-api/pom.xml
new file mode 100644
index 0000000..b0bfd7e
--- /dev/null
+++ b/trunk/java/social-api/pom.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-social-api</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Apache Shindig Social API</name>
+  <description>Serves OpenSocial Data and the RESTful APIs.</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/social-api</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/social-api</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/social-api</url>
+  </scm>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>conf</directory>
+      </resource>
+      <resource>
+        <directory>${basedir}/../../content/sampledata/</directory>
+        <targetPath>sampledata</targetPath>
+        <includes>
+          <include>**/*.*</include>
+        </includes>
+      </resource>
+    </resources>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>reporting</id>
+      <reporting>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>clirr-maven-plugin</artifactId>
+            <configuration>
+              <comparisonVersion>${shindig.api.previous}</comparisonVersion>
+            </configuration>
+          </plugin>
+        </plugins>
+      </reporting>
+    </profile>
+  </profiles>
+
+  <dependencies>
+    <!-- project dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+
+    <!-- external depenencies -->
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-multibindings</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-codec</groupId>
+      <artifactId>commons-codec</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <artifactId>commons-lang3</artifactId>
+      <groupId>org.apache.commons</groupId>
+    </dependency>
+    <dependency>
+      <groupId>net.oauth.core</groupId>
+      <artifactId>oauth-provider</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>net.oauth.core</groupId>
+      <artifactId>oauth-httpclient4</artifactId>
+    </dependency>
+
+    <!-- may only be needed for JDK < 1.6 -->
+    <dependency>
+      <groupId>org.apache.geronimo.specs</groupId>
+      <artifactId>geronimo-stax-api_1.0_spec</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.thoughtworks.xstream</groupId>
+      <artifactId>xstream</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>xpp3</groupId>
+      <artifactId>xpp3_min</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.httpcomponents</groupId>
+      <artifactId>httpclient</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>xml-apis</groupId>
+      <artifactId>xml-apis</artifactId>
+    </dependency>
+
+    <!-- test -->
+    <dependency>
+      <groupId>rhino</groupId>
+      <artifactId>js</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jetty</groupId>
+      <artifactId>jetty</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>el-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java
new file mode 100644
index 0000000..4a88275
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/SocialApiGuiceModule.java
@@ -0,0 +1,100 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.config;
+
+import java.util.List;
+import java.util.Set;
+
+import net.oauth.OAuthValidator;
+
+import org.apache.shindig.auth.AnonymousAuthenticationHandler;
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.common.servlet.ParameterFetcher;
+import org.apache.shindig.protocol.DataServiceServletFetcher;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.conversion.BeanXStreamConverter;
+import org.apache.shindig.protocol.conversion.xstream.XStreamConfiguration;
+import org.apache.shindig.social.core.oauth.AuthenticationHandlerProvider;
+import org.apache.shindig.social.core.oauth.OAuthValidatorProvider;
+import org.apache.shindig.social.core.util.BeanXStreamAtomConverter;
+import org.apache.shindig.social.core.util.xstream.XStream081Configuration;
+import org.apache.shindig.social.opensocial.service.ActivityHandler;
+import org.apache.shindig.social.opensocial.service.ActivityStreamHandler;
+import org.apache.shindig.social.opensocial.service.AlbumHandler;
+import org.apache.shindig.social.opensocial.service.AppDataHandler;
+import org.apache.shindig.social.opensocial.service.GroupHandler;
+import org.apache.shindig.social.opensocial.service.MediaItemHandler;
+import org.apache.shindig.social.opensocial.service.MessageHandler;
+import org.apache.shindig.social.opensocial.service.PersonHandler;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Names;
+
+/**
+ * Provides social api component injection. Implementor may want to replace this module if they need
+ * to replace some of the internals of the Social API, like for instance the JSON to Bean to JSON
+ * converter Beans, however in general this should not be required, as most default implementations
+ * have been specified with the Guice @ImplementedBy annotation.
+ */
+public class SocialApiGuiceModule extends AbstractModule {
+
+  /** {@inheritDoc} */
+  @Override
+  protected void configure() {
+    bind(ParameterFetcher.class).annotatedWith(Names.named("DataServiceServlet"))
+        .to(DataServiceServletFetcher.class);
+
+    bind(XStreamConfiguration.class).to(XStream081Configuration.class);
+    bind(BeanConverter.class).annotatedWith(Names.named("shindig.bean.converter.xml")).to(
+        BeanXStreamConverter.class);
+    bind(BeanConverter.class).annotatedWith(Names.named("shindig.bean.converter.json")).to(
+        BeanJsonConverter.class);
+    bind(BeanConverter.class).annotatedWith(Names.named("shindig.bean.converter.atom")).to(
+        BeanXStreamAtomConverter.class);
+
+    bind(new TypeLiteral<List<AuthenticationHandler>>(){}).toProvider(
+        AuthenticationHandlerProvider.class);
+
+    Multibinder<Object> handlerBinder = Multibinder.newSetBinder(binder(), Object.class,
+        Names.named("org.apache.shindig.handlers"));
+    for (Class<?> handler : getHandlers()) {
+      handlerBinder.addBinding().toInstance(handler);
+    }
+
+    bind(OAuthValidator.class).toProvider(OAuthValidatorProvider.class).in(Singleton.class);
+  }
+
+  /**
+   * Hook to provide a Set of request handlers.  Subclasses may override
+   * to add or replace additional handlers.
+   *
+   * @return Set<Class<?>> Set of handlers
+   */
+  @SuppressWarnings("unchecked")
+  protected Set<Class<?>> getHandlers() {
+    return ImmutableSet.of(ActivityHandler.class, AppDataHandler.class,
+            PersonHandler.class, MessageHandler.class, AlbumHandler.class,
+            MediaItemHandler.class, ActivityStreamHandler.class, GroupHandler.class);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/package-info.java
new file mode 100644
index 0000000..91e46dd
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/config/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * <h1>Core configuration</h1>
+ * <p>This package contains core configuration classes for Guice to
+ * construct the Social API component. Implementors may want to override
+ * modules in this package</p>
+ */
+package org.apache.shindig.social.core.config;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AccountImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AccountImpl.java
new file mode 100644
index 0000000..ff5982e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AccountImpl.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Account;
+
+/**
+ * Default Implementation of an {@link org.apache.shindig.social.opensocial.model.Account}
+ */
+public class AccountImpl implements Account {
+  private String domain;
+  private String userId;
+  private String username;
+
+  public AccountImpl() { }
+
+  public AccountImpl(String domain, String userId, String username) {
+    this.domain = domain;
+    this.userId = userId;
+    this.username = username;
+  }
+
+  public String getDomain() {
+    return domain;
+  }
+
+  /** {@inheritDoc} */
+  public void setDomain(String domain) {
+    this.domain = domain;
+  }
+
+  public String getUserId() {
+    return userId;
+  }
+
+  /** {@inheritDoc} */
+  public void setUserId(String userId) {
+    this.userId = userId;
+  }
+
+  public String getUsername() {
+    return username;
+  }
+
+  /** {@inheritDoc} */
+  public void setUsername(String username) {
+    this.username = username;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityEntryImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityEntryImpl.java
new file mode 100644
index 0000000..4b5995f
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityEntryImpl.java
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.apache.shindig.protocol.model.ExtendableBeanImpl;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.model.ActivityObject;
+import org.apache.shindig.social.opensocial.model.MediaLink;
+
+/**
+ * A simple bean implementation of an ActivityStream Entry.
+ */
+public class ActivityEntryImpl extends ExtendableBeanImpl implements ActivityEntry {
+
+  private static final long serialVersionUID = 1L;
+  private ActivityObject actor;
+  private String content;
+  private ActivityObject generator;
+  private MediaLink icon;
+  private String id;
+  private ActivityObject object;
+  private String published;
+  private ActivityObject provider;
+  private ActivityObject target;
+  private String title;
+  private String updated;
+  private String url;
+  private String verb;
+  private ExtendableBean openSocial;
+  private ExtendableBean extensions;
+
+  /**
+   * Create a new empty ActivityEntry
+   */
+  public ActivityEntryImpl() { }
+
+  public ActivityObject getActor() {
+    return actor;
+  }
+
+  /** {@inheritDoc} */
+  public void setActor(ActivityObject actor) {
+    this.actor = actor;
+    put("actor", actor);
+  }
+
+  /** {@inheritDoc} */
+  public String getContent() {
+    return content;
+  }
+
+  /** {@inheritDoc} */
+  public void setContent(String content) {
+    this.content = content;
+    put("content", content);
+  }
+
+  /** {@inheritDoc} */
+  public ActivityObject getGenerator() {
+    return generator;
+  }
+
+  /** {@inheritDoc} */
+  public void setGenerator(ActivityObject generator) {
+    this.generator = generator;
+    put("generator", generator);
+  }
+
+  /** {@inheritDoc} */
+  public MediaLink getIcon() {
+    return icon;
+  }
+
+  /** {@inheritDoc} */
+  public void setIcon(MediaLink icon) {
+    this.icon = icon;
+    put("icon", icon);
+  }
+
+  /** {@inheritDoc} */
+  public String getId() {
+    return id;
+  }
+
+  /** {@inheritDoc} */
+  public void setId(String id) {
+    this.id = id;
+    put("id", id);
+  }
+
+  /** {@inheritDoc} */
+  public ActivityObject getObject() {
+    return object;
+  }
+
+  /** {@inheritDoc} */
+  public void setObject(ActivityObject object) {
+    this.object = object;
+    put("object", object);
+  }
+
+  /** {@inheritDoc} */
+  public String getPublished() {
+    return published;
+  }
+
+  /** {@inheritDoc} */
+  public void setPublished(String published) {
+    this.published = published;
+    put("published", published);
+  }
+
+  /** {@inheritDoc} */
+  public ActivityObject getProvider() {
+    return provider;
+  }
+
+  /** {@inheritDoc} */
+  public void setProvider(ActivityObject provider) {
+    this.provider = provider;
+    put("provider", provider);
+  }
+
+  /** {@inheritDoc} */
+  public ActivityObject getTarget() {
+    return target;
+  }
+
+  /** {@inheritDoc} */
+  public void setTarget(ActivityObject target) {
+    this.target = target;
+    put("target", target);
+  }
+
+  /** {@inheritDoc} */
+  public String getTitle() {
+    return title;
+  }
+
+  /** {@inheritDoc} */
+  public void setTitle(String title) {
+    this.title = title;
+    put("title", title);
+  }
+
+  /** {@inheritDoc} */
+  public String getUpdated() {
+    return updated;
+  }
+
+  /** {@inheritDoc} */
+  public void setUpdated(String updated) {
+    this.updated = updated;
+    put("updated", updated);
+  }
+
+  /** {@inheritDoc} */
+  public String getUrl() {
+    return url;
+  }
+
+  /** {@inheritDoc} */
+  public void setUrl(String url) {
+    this.url = url;
+    put("url", url);
+  }
+
+  /** {@inheritDoc} */
+  public String getVerb() {
+    return verb;
+  }
+
+  /** {@inheritDoc} */
+  public void setVerb(String verb) {
+    this.verb = verb;
+    put("verb", verb);
+  }
+
+  /** {@inheritDoc} */
+  public ExtendableBean getOpenSocial() {
+    return openSocial;
+  }
+
+  /** {@inheritDoc} */
+  public void setOpenSocial(ExtendableBean openSocial) {
+    this.openSocial = openSocial;
+    put("openSocial", openSocial);
+  }
+
+  /** {@inheritDoc} */
+  public ExtendableBean getExtensions() {
+    return extensions;
+  }
+
+  /** {@inheritDoc} */
+  public void setExtensions(ExtendableBean extensions) {
+    this.extensions = extensions;
+    put("extensions", extensions);
+  }
+
+  /**
+   * Sorts ActivityEntries in ascending order based on publish date.
+   *
+   * @param that is the ActivityEntry to compare to this ActivityEntry
+   *
+   * @return int represents how the ActivityEntries compare
+   */
+  public int compareTo(ActivityEntry that) {
+    if (this.getPublished() == null && that.getPublished() == null) {
+      return 0;   // both are null, equal
+    } else if (this.getPublished() == null) {
+      return -1;  // this is null, comes before real date
+    } else if (that.getPublished() == null) {
+      return 1;   // that is null, this comes after
+    } else {      // compare publish dates in lexicographical order
+      return this.getPublished().compareTo(that.getPublished());
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityImpl.java
new file mode 100644
index 0000000..ef80397
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityImpl.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Default implementation of an {@link org.apache.shindig.social.opensocial.model.Activity}
+ */
+public class ActivityImpl implements Activity {
+
+  private String appId;
+  private String body;
+  private String bodyId;
+  private String externalId;
+  private String id;
+  private Date updated;
+  private List<MediaItem> mediaItems;
+  private Long postedTime;
+  private Float priority;
+  private String streamFaviconUrl;
+  private String streamSourceUrl;
+  private String streamTitle;
+  private String streamUrl;
+  private Map<String, String> templateParams;
+  private String title;
+  private String titleId;
+  private String url;
+  private String userId;
+
+  public ActivityImpl() {
+  }
+
+  public ActivityImpl(String id, String userId) {
+    this.id = id;
+    this.userId = userId;
+  }
+
+  public String getAppId() {
+    return appId;
+  }
+
+  public void setAppId(String appId) {
+    this.appId = appId;
+  }
+
+  public String getBody() {
+    return body;
+  }
+
+  public void setBody(String body) {
+    this.body = body;
+  }
+
+  public String getBodyId() {
+    return bodyId;
+  }
+
+  public void setBodyId(String bodyId) {
+    this.bodyId = bodyId;
+  }
+
+  public String getExternalId() {
+    return externalId;
+  }
+
+  public void setExternalId(String externalId) {
+    this.externalId = externalId;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public Date getUpdated() {
+    if (updated == null) {
+      return null;
+    }
+    return new Date(updated.getTime());
+  }
+
+  /** {@inheritDoc} */
+  public void setUpdated(Date updated) {
+    if (updated == null) {
+      this.updated = null;
+    } else {
+      this.updated = new Date(updated.getTime());
+    }
+  }
+
+  public List<MediaItem> getMediaItems() {
+    return mediaItems;
+  }
+
+  /** {@inheritDoc} */
+  public void setMediaItems(List<MediaItem> mediaItems) {
+    this.mediaItems = mediaItems;
+  }
+
+  public Long getPostedTime() {
+    return postedTime;
+  }
+
+  /** {@inheritDoc} */
+  public void setPostedTime(Long postedTime) {
+    this.postedTime = postedTime;
+  }
+
+  public Float getPriority() {
+    return priority;
+  }
+
+  /** {@inheritDoc} */
+  public void setPriority(Float priority) {
+    this.priority = priority;
+  }
+
+  public String getStreamFaviconUrl() {
+    return streamFaviconUrl;
+  }
+
+  /** {@inheritDoc} */
+  public void setStreamFaviconUrl(String streamFaviconUrl) {
+    this.streamFaviconUrl = streamFaviconUrl;
+  }
+
+  public String getStreamSourceUrl() {
+    return streamSourceUrl;
+  }
+
+  /** {@inheritDoc} */
+  public void setStreamSourceUrl(String streamSourceUrl) {
+    this.streamSourceUrl = streamSourceUrl;
+  }
+
+  public String getStreamTitle() {
+    return streamTitle;
+  }
+
+  /** {@inheritDoc} */
+  public void setStreamTitle(String streamTitle) {
+    this.streamTitle = streamTitle;
+  }
+
+  public String getStreamUrl() {
+    return streamUrl;
+  }
+
+  /** {@inheritDoc} */
+  public void setStreamUrl(String streamUrl) {
+    this.streamUrl = streamUrl;
+  }
+
+  public Map<String, String> getTemplateParams() {
+    return templateParams;
+  }
+
+  /** {@inheritDoc} */
+  public void setTemplateParams(Map<String, String> templateParams) {
+    this.templateParams = templateParams;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  /** {@inheritDoc} */
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getTitleId() {
+    return titleId;
+  }
+
+  /** {@inheritDoc} */
+  public void setTitleId(String titleId) {
+    this.titleId = titleId;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  /** {@inheritDoc} */
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public String getUserId() {
+    return userId;
+  }
+
+  /** {@inheritDoc} */
+  public void setUserId(String userId) {
+    this.userId = userId;
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityObjectImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityObjectImpl.java
new file mode 100644
index 0000000..c50d504
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ActivityObjectImpl.java
@@ -0,0 +1,207 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import java.util.List;
+
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.apache.shindig.protocol.model.ExtendableBeanImpl;
+import org.apache.shindig.social.opensocial.model.ActivityObject;
+import org.apache.shindig.social.opensocial.model.MediaLink;
+
+/**
+ * <p>ActivityObjectImpl class.</p>
+ */
+public class ActivityObjectImpl extends ExtendableBeanImpl implements ActivityObject {
+
+  private static final long serialVersionUID = 1L;
+  private List<ActivityObject> attachments;
+  private ActivityObject author;
+  private String content;
+  private String displayName;
+  private List<String> downstreamDuplicates;
+  private String id;
+  private MediaLink image;
+  private String objectType;
+  private String published;
+  private String summary;
+  private String updated;
+  private List<String> upstreamDuplicates;
+  private String url;
+  private ExtendableBean openSocial;
+
+  /**
+   * Constructs an empty ActivityObject.
+   */
+  public ActivityObjectImpl() { }
+
+  /** {@inheritDoc} */
+  public List<ActivityObject> getAttachments() {
+    return attachments;
+  }
+
+  /** {@inheritDoc} */
+  public void setAttachments(List<ActivityObject> attachments) {
+    this.attachments = attachments;
+    put("attachments", attachments);
+  }
+
+  /** {@inheritDoc} */
+  public ActivityObject getAuthor() {
+    return author;
+  }
+
+  /** {@inheritDoc} */
+  public void setAuthor(ActivityObject author) {
+    this.author = author;
+    put("author", author);
+  }
+
+  /** {@inheritDoc} */
+  public String getContent() {
+    return content;
+  }
+
+  /** {@inheritDoc} */
+  public void setContent(String content) {
+    this.content = content;
+    put("content", content);
+  }
+
+  /** {@inheritDoc} */
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  /** {@inheritDoc} */
+  public void setDisplayName(String displayName) {
+    this.displayName = displayName;
+    put("displayName", displayName);
+  }
+
+  /** {@inheritDoc} */
+  public List<String> getDownstreamDuplicates() {
+    return downstreamDuplicates;
+  }
+
+  /** {@inheritDoc} */
+  public void setDownstreamDuplicates(List<String> downstreamDuplicates) {
+    this.downstreamDuplicates = downstreamDuplicates;
+    put("downstreamDuplicates", downstreamDuplicates);
+  }
+
+  /** {@inheritDoc} */
+  public String getId() {
+    return id;
+  }
+
+  /** {@inheritDoc} */
+  public void setId(String id) {
+    this.id = id;
+    put("id", id);
+  }
+
+  /** {@inheritDoc} */
+  public MediaLink getImage() {
+    return image;
+  }
+
+  /** {@inheritDoc} */
+  public void setImage(MediaLink image) {
+    this.image = image;
+    put("image", image);
+  }
+
+  /** {@inheritDoc} */
+  public String getObjectType() {
+    return objectType;
+  }
+
+  /** {@inheritDoc} */
+  public void setObjectType(String objectType) {
+    this.objectType = objectType;
+    put("objectType", objectType);
+  }
+
+  /** {@inheritDoc} */
+  public String getPublished() {
+    return published;
+  }
+
+  /** {@inheritDoc} */
+  public void setPublished(String published) {
+    this.published = published;
+    put("published", published);
+  }
+
+  /** {@inheritDoc} */
+  public String getSummary() {
+    return summary;
+  }
+
+  /** {@inheritDoc} */
+  public void setSummary(String summary) {
+    this.summary = summary;
+    put("summary", summary);
+  }
+
+  /** {@inheritDoc} */
+  public String getUpdated() {
+    return updated;
+  }
+
+  /** {@inheritDoc} */
+  public void setUpdated(String updated) {
+    this.updated = updated;
+    put("updated", updated);
+  }
+
+  /** {@inheritDoc} */
+  public List<String> getUpstreamDuplicates() {
+    return upstreamDuplicates;
+  }
+
+  /** {@inheritDoc} */
+  public void setUpstreamDuplicates(List<String> upstreamDuplicates) {
+    this.upstreamDuplicates = upstreamDuplicates;
+    put("upstreamDuplicates", upstreamDuplicates);
+  }
+
+  /** {@inheritDoc} */
+  public String getUrl() {
+    return url;
+  }
+
+  /** {@inheritDoc} */
+  public void setUrl(String url) {
+    this.url = url;
+    put("url", url);
+  }
+
+  /** {@inheritDoc} */
+  public ExtendableBean getOpenSocial() {
+    return openSocial;
+  }
+
+  /** {@inheritDoc} */
+  public void setOpenSocial(ExtendableBean openSocial) {
+    this.openSocial = openSocial;
+    put("openSocial", openSocial);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AddressImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AddressImpl.java
new file mode 100644
index 0000000..7ff459b
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AddressImpl.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Address;
+
+/**
+ * Default representation of an {@link org.apache.shindig.social.opensocial.model.Address}
+ */
+public class AddressImpl implements Address {
+  private String country;
+  private Float latitude;
+  private Float longitude;
+  private String locality;
+  private String postalCode;
+  private String region;
+  private String streetAddress;
+  private String type;
+  private String formatted;
+  private Boolean primary;
+
+  public AddressImpl() { }
+
+  public AddressImpl(String formatted) {
+    this.formatted = formatted;
+  }
+
+  public String getCountry() {
+    return country;
+  }
+
+  /** {@inheritDoc} */
+  public void setCountry(String country) {
+    this.country = country;
+  }
+
+  public Float getLatitude() {
+    return latitude;
+  }
+
+  /** {@inheritDoc} */
+  public void setLatitude(Float latitude) {
+    this.latitude = latitude;
+  }
+
+  public String getLocality() {
+    return locality;
+  }
+
+  /** {@inheritDoc} */
+  public void setLocality(String locality) {
+    this.locality = locality;
+  }
+
+  public Float getLongitude() {
+    return longitude;
+  }
+
+  /** {@inheritDoc} */
+  public void setLongitude(Float longitude) {
+    this.longitude = longitude;
+  }
+
+  public String getPostalCode() {
+    return postalCode;
+  }
+
+  /** {@inheritDoc} */
+  public void setPostalCode(String postalCode) {
+    this.postalCode = postalCode;
+  }
+
+  public String getRegion() {
+    return region;
+  }
+
+  /** {@inheritDoc} */
+  public void setRegion(String region) {
+    this.region = region;
+  }
+
+  public String getStreetAddress() {
+    return streetAddress;
+  }
+
+  /** {@inheritDoc} */
+  public void setStreetAddress(String streetAddress) {
+    this.streetAddress = streetAddress;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  /** {@inheritDoc} */
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public String getFormatted() {
+    return formatted;
+  }
+
+  /** {@inheritDoc} */
+  public void setFormatted(String formatted) {
+    this.formatted = formatted;
+  }
+
+  public Boolean getPrimary() {
+    return primary;
+  }
+
+  /** {@inheritDoc} */
+  public void setPrimary(Boolean primary) {
+    this.primary = primary;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AlbumImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AlbumImpl.java
new file mode 100644
index 0000000..cf00caa
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/AlbumImpl.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.Album;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+
+import java.util.List;
+
+/**
+ * Default Implementation of the {@link org.apache.shindig.social.opensocial.model.Album} object in the model.
+ *
+ * @since 2.0.0
+ */
+public class AlbumImpl implements Album {
+  private String description;
+  private String id;
+  private Address location;
+  private Integer mediaItemCount;
+  private List<String> mediaMimeType;
+  private List<MediaItem.Type> mediaType;
+  private String ownerId;
+  private String thumbnailUrl;
+  private String title;
+
+  public AlbumImpl() {
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  /** {@inheritDoc} */
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  /** {@inheritDoc} */
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public Address getLocation() {
+    return location;
+  }
+
+  /** {@inheritDoc} */
+  public void setLocation(Address location) {
+    this.location = location;
+  }
+
+  public Integer getMediaItemCount() {
+    return mediaItemCount;
+  }
+
+  /** {@inheritDoc} */
+  public void setMediaItemCount(Integer mediaItemCount) {
+    this.mediaItemCount = mediaItemCount;
+  }
+
+  public List<String> getMediaMimeType() {
+    return mediaMimeType;
+  }
+
+  /** {@inheritDoc} */
+  public void setMediaMimeType(List<String> mediaMimeType) {
+    this.mediaMimeType = mediaMimeType;
+  }
+
+  public List<MediaItem.Type> getMediaType() {
+    return mediaType;
+  }
+
+  /** {@inheritDoc} */
+  public void setMediaType(List<MediaItem.Type> mediaType) {
+    this.mediaType = mediaType;
+  }
+
+  public String getOwnerId() {
+    return ownerId;
+  }
+
+  /** {@inheritDoc} */
+  public void setOwnerId(String ownerId) {
+    this.ownerId = ownerId;
+  }
+
+  public String getThumbnailUrl() {
+    return thumbnailUrl;
+  }
+
+  /** {@inheritDoc} */
+  public void setThumbnailUrl(String thumbnailUrl) {
+    this.thumbnailUrl = thumbnailUrl;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  /** {@inheritDoc} */
+  public void setTitle(String title) {
+    this.title = title;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/BodyTypeImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/BodyTypeImpl.java
new file mode 100644
index 0000000..cb23c91
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/BodyTypeImpl.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.BodyType;
+
+/**
+ * see
+ * <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.BodyType">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.BodyType</a>.
+ */
+public class BodyTypeImpl implements BodyType {
+
+  private String build;
+  private String eyeColor;
+  private String hairColor;
+  private Float height;
+  private Float weight;
+
+  public String getBuild() {
+    return build;
+  }
+
+  /** {@inheritDoc} */
+  public void setBuild(String build) {
+    this.build = build;
+  }
+
+  public String getEyeColor() {
+    return eyeColor;
+  }
+
+  /** {@inheritDoc} */
+  public void setEyeColor(String eyeColor) {
+    this.eyeColor = eyeColor;
+  }
+
+  public String getHairColor() {
+    return hairColor;
+  }
+
+  /** {@inheritDoc} */
+  public void setHairColor(String hairColor) {
+    this.hairColor = hairColor;
+  }
+
+  public Float getHeight() {
+    return height;
+  }
+
+  /** {@inheritDoc} */
+  public void setHeight(Float height) {
+    this.height = height;
+  }
+
+  public Float getWeight() {
+    return weight;
+  }
+
+  /** {@inheritDoc} */
+  public void setWeight(Float weight) {
+    this.weight = weight;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/GroupImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/GroupImpl.java
new file mode 100644
index 0000000..6c6961e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/GroupImpl.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import java.util.Map;
+
+import org.apache.shindig.social.opensocial.model.Group;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+
+/**
+ * Default Implementation of the {@link org.apache.shindig.social.opensocial.model.Group} model.
+ *
+ * @since 2.0.0
+ */
+public class GroupImpl implements Group {
+
+  private GroupId id;
+  private String title;
+  private String description;
+
+  /** {@inheritDoc} */
+  public String getTitle() {
+    return title;
+  }
+
+  /** {@inheritDoc} */
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  /** {@inheritDoc} */
+  public String getDescription() {
+    return description;
+  }
+
+  /** {@inheritDoc} */
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  /** {@inheritDoc} */
+  public void setId(Object id) throws IllegalArgumentException {
+    if(id instanceof String) {
+      this.id = new GroupId(id);
+    } else if(id instanceof GroupId) {
+      this.id = (GroupId) id;
+    // Coming from JSON
+    } else if(id instanceof Map) {
+      this.id = new GroupId(((Map) id).get("value"));
+    } else {
+      throw new IllegalArgumentException("The provided GroupId is not valid");
+    }
+  }
+
+  /** {@inheritDoc} */
+  public String getId() {
+    return this.id.toString();
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ListFieldImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ListFieldImpl.java
new file mode 100644
index 0000000..e8fcbab
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/ListFieldImpl.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.ListField;
+
+/**
+ * ListField data structure
+ */
+public class ListFieldImpl implements ListField {
+  private String type;
+  private String value;
+  private Boolean primary;
+
+  public ListFieldImpl() { }
+
+  public ListFieldImpl(String type, String value) {
+    this.type = type;
+    this.value = value;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  /** {@inheritDoc} */
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public String getValue() {
+    return value;
+  }
+
+  /** {@inheritDoc} */
+  public void setValue(String value) {
+    this.value = value;
+  }
+
+  public Boolean getPrimary() {
+    return primary;
+  }
+
+  /** {@inheritDoc} */
+  public void setPrimary(Boolean primary) {
+    this.primary = primary;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MediaItemImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MediaItemImpl.java
new file mode 100644
index 0000000..b14e08c
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MediaItemImpl.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+
+/**
+ * see
+ * <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v09/OpenSocial-Specification.html#opensocial.MediaItem.Field">
+ * opensocial.MediaItem.Field</a>
+ */
+public class MediaItemImpl implements MediaItem {
+  private String albumId;
+  private String created;
+  private String description;
+  private String duration;
+  private String fileSize;
+  private String id;
+  private String language;
+  private String lastUpdated;
+  private Address location;
+  private String mimeType;
+  private String numComments;
+  private String numViews;
+  private String numVotes;
+  private String rating;
+  private String startTime;
+  private String taggedPeople;
+  private String tags;
+  private String thumbnailUrl;
+  private String title;
+  private Type type;
+  private String url;
+
+  public MediaItemImpl() {
+  }
+
+  public MediaItemImpl(String mimeType, Type type, String url) {
+    this.mimeType = mimeType;
+    this.type = type;
+    this.url = url;
+  }
+
+  public String getMimeType() {
+    return mimeType;
+  }
+
+  /** {@inheritDoc} */
+  public void setMimeType(String mimeType) {
+    this.mimeType = mimeType;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public void setType(Type type) {
+    this.type = type;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  /** {@inheritDoc} */
+  public void setUrl(String url) {
+    this.url = url;
+  }
+
+  public String getThumbnailUrl() {
+    return this.thumbnailUrl;
+  }
+
+  /** {@inheritDoc} */
+  public void setThumbnailUrl(String url) {
+    this.thumbnailUrl = url;
+  }
+
+  public String getAlbumId() {
+    return albumId;
+  }
+
+  /** {@inheritDoc} */
+  public void setAlbumId(String albumId) {
+    this.albumId = albumId;
+  }
+
+  public String getCreated() {
+    return created;
+  }
+
+  /** {@inheritDoc} */
+  public void setCreated(String created) {
+    this.created = created;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  /** {@inheritDoc} */
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public String getDuration() {
+    return duration;
+  }
+
+  /** {@inheritDoc} */
+  public void setDuration(String duration) {
+    this.duration = duration;
+  }
+
+  public String getFileSize() {
+    return fileSize;
+  }
+
+  /** {@inheritDoc} */
+  public void setFileSize(String fileSize) {
+    this.fileSize = fileSize;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  /** {@inheritDoc} */
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getLanguage() {
+    return language;
+  }
+
+  /** {@inheritDoc} */
+  public void setLanguage(String language) {
+    this.language = language;
+  }
+
+  public String getLastUpdated() {
+    return lastUpdated;
+  }
+
+  /** {@inheritDoc} */
+  public void setLastUpdated(String lastUpdated) {
+    this.lastUpdated = lastUpdated;
+  }
+
+  public Address getLocation() {
+    return location;
+  }
+
+  /** {@inheritDoc} */
+  public void setLocation(Address location) {
+    this.location = location;
+  }
+
+  public String getNumComments() {
+    return numComments;
+  }
+
+  /** {@inheritDoc} */
+  public void setNumComments(String numComments) {
+    this.numComments = numComments;
+  }
+
+  public String getNumViews() {
+    return numViews;
+  }
+
+  /** {@inheritDoc} */
+  public void setNumViews(String numViews) {
+    this.numViews = numViews;
+  }
+
+  public String getNumVotes() {
+    return numVotes;
+  }
+
+  /** {@inheritDoc} */
+  public void setNumVotes(String numVotes) {
+    this.numVotes = numVotes;
+  }
+
+  public String getRating() {
+    return rating;
+  }
+
+  /** {@inheritDoc} */
+  public void setRating(String rating) {
+    this.rating = rating;
+  }
+
+  public String getStartTime() {
+    return startTime;
+  }
+
+  /** {@inheritDoc} */
+  public void setStartTime(String startTime) {
+    this.startTime = startTime;
+  }
+
+  public String getTaggedPeople() {
+    return taggedPeople;
+  }
+
+  /** {@inheritDoc} */
+  public void setTaggedPeople(String taggedPeople) {
+    this.taggedPeople = taggedPeople;
+  }
+
+  public String getTags() {
+    return tags;
+  }
+
+  /** {@inheritDoc} */
+  public void setTags(String tags) {
+    this.tags = tags;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+  /** {@inheritDoc} */
+  public void setTitle(String title) {
+    this.title = title;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MediaLinkImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MediaLinkImpl.java
new file mode 100644
index 0000000..36b103d
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MediaLinkImpl.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.apache.shindig.protocol.model.ExtendableBeanImpl;
+import org.apache.shindig.social.opensocial.model.MediaLink;
+
+/**
+ * <p>MediaLinkImpl class.</p>
+ *
+ */
+public class MediaLinkImpl extends ExtendableBeanImpl implements MediaLink {
+
+  private static final long serialVersionUID = 1L;
+  private Integer duration;
+  private Integer height;
+  private String url;
+  private Integer width;
+  private ExtendableBean openSocial;
+
+  /**
+   * Create a new MediaLink
+   */
+  public MediaLinkImpl() {
+  }
+
+  /** {@inheritDoc} */
+  public Integer getDuration() {
+    return duration;
+  }
+
+  /** {@inheritDoc} */
+  public void setDuration(Integer duration) {
+    this.duration = duration;
+    put("duration", duration);
+  }
+
+  /** {@inheritDoc} */
+  public Integer getHeight() {
+    return height;
+  }
+
+  /** {@inheritDoc} */
+  public void setHeight(Integer height) {
+    this.height = height;
+    put("height", height);
+  }
+
+  /** {@inheritDoc} */
+  public String getUrl() {
+    return url;
+  }
+
+  /** {@inheritDoc} */
+  public void setUrl(String url) {
+    this.url = url;
+    put("url", url);
+  }
+
+  /** {@inheritDoc} */
+  public Integer getWidth() {
+    return width;
+  }
+
+  /** {@inheritDoc} */
+  public void setWidth(Integer width) {
+    this.width = width;
+    put("width", width);
+  }
+
+  /** {@inheritDoc} */
+  public ExtendableBean getOpenSocial() {
+    return openSocial;
+  }
+
+  /** {@inheritDoc} */
+  public void setOpenSocial(ExtendableBean openSocial) {
+    this.openSocial = openSocial;
+    put("openSocial", openSocial);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MessageCollectionImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MessageCollectionImpl.java
new file mode 100644
index 0000000..246d59a
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MessageCollectionImpl.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.MessageCollection;
+import org.apache.shindig.social.opensocial.model.Url;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Default representation of a MessageCollection
+ */
+public class MessageCollectionImpl implements MessageCollection {
+
+  private String id;
+  private String title;
+  private Integer total;
+  private Integer unread;
+  private Date updated;
+  private List<Url> urls;
+
+  public MessageCollectionImpl() { }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public Integer getTotal() {
+    return total;
+  }
+
+  public void setTotal(Integer total) {
+    this.total = total;
+  }
+
+  public Integer getUnread() {
+    return unread;
+  }
+
+  public void setUnread(Integer unread) {
+    this.unread = unread;
+  }
+
+  public Date getUpdated() {
+    return updated;
+  }
+
+  public void setUpdated(Date updated) {
+    this.updated = updated;
+  }
+
+  public List<Url> getUrls() {
+    return urls;
+  }
+
+  public void setUrls(List<Url> urls) {
+    this.urls = urls;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MessageImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MessageImpl.java
new file mode 100644
index 0000000..f2575bb
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/MessageImpl.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import java.util.List;
+import java.util.Date;
+
+import org.apache.shindig.social.opensocial.model.Message;
+import org.apache.shindig.social.opensocial.model.Url;
+
+/**
+ * Default implementation for a {@link org.apache.shindig.social.opensocial.model.Message}
+ */
+public final class MessageImpl implements Message {
+
+  private String appUrl;
+  private String body;
+  private String bodyId;
+  private List<String> collectionIds;
+  private String id;
+  private String inReplyTo;
+  private List<String> recipients;
+  private List<String> replies;
+  private String senderId;
+  private Status status;
+  private Date timeSent;
+  private String title;
+  private String titleId;
+  private Type type;
+  private Date updated;
+  private List<Url> urls;
+
+
+  public MessageImpl() {
+  }
+
+  public MessageImpl(String initBody, String initTitle, Type initType) {
+    this.body = initBody;
+    this.title = initTitle;
+    this.type = initType;
+  }
+
+  public String getAppUrl() {
+    return appUrl;
+  }
+
+  public void setAppUrl(String appUrl) {
+    this.appUrl = appUrl;
+  }
+
+  public String getBody() {
+    return this.body;
+  }
+
+  public void setBody(String newBody) {
+    this.body = newBody;
+  }
+
+  public String getBodyId() {
+    return bodyId;
+  }
+
+  public void setBodyId(String bodyId) {
+    this.bodyId = bodyId;
+  }
+
+  public List<String> getCollectionIds() {
+    return collectionIds;
+  }
+
+  public void setCollectionIds(List<String> collectionIds) {
+    this.collectionIds = collectionIds;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public String getInReplyTo() {
+    return inReplyTo;
+  }
+
+  public void setInReplyTo(String parentId) {
+    this.inReplyTo = parentId;
+  }
+
+  public List<String> getRecipients() {
+    return this.recipients;
+  }
+
+  public void setRecipients(List<String> recipients) {
+    this.recipients = recipients;
+  }
+
+  public List<String> getReplies() {
+    return replies;
+  }
+
+  public void setReplies(List<String> replies) {
+    this.replies = replies;
+  }
+
+  public String getSenderId() {
+    return senderId;
+  }
+
+  public void setSenderId(String senderId) {
+    this.senderId = senderId;
+  }
+
+  public Status getStatus() {
+    return status;
+  }
+
+  public void setStatus(Status status) {
+    this.status = status;
+  }
+
+  public Date getTimeSent() {
+    return timeSent;
+  }
+
+  public void setTimeSent(Date timeSent) {
+    this.timeSent = timeSent;
+  }
+
+  public String getTitle() {
+    return this.title;
+  }
+
+  public void setTitle(String newTitle) {
+    this.title = newTitle;
+  }
+
+  public String getTitleId() {
+    return titleId;
+  }
+
+  public void setTitleId(String titleId) {
+    this.titleId = titleId;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public void setType(Type newType) {
+    this.type = newType;
+  }
+
+  public Date getUpdated() {
+    return this.updated;
+  }
+
+  public void setUpdated(Date updated) {
+    this.updated = updated;
+  }
+
+  public List<Url> getUrls() {
+    return this.urls;
+  }
+
+  public void setUrls(List<Url> urls) {
+    this.urls = urls;
+  }
+
+  public String sanitizeHTML(String htmlStr) {
+    return htmlStr;
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/NameImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/NameImpl.java
new file mode 100644
index 0000000..b4545b9
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/NameImpl.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Name;
+
+/**
+ * Default implementation of the {@link org.apache.shindig.social.opensocial.model.Name} model.
+ */
+public class NameImpl implements Name {
+
+  private String additionalName;
+  private String familyName;
+  private String givenName;
+  private String honorificPrefix;
+  private String honorificSuffix;
+  private String formatted;
+
+  public NameImpl() {
+  }
+
+  public NameImpl(String formatted) {
+    this.formatted = formatted;
+  }
+
+  public String getFormatted() {
+    return formatted;
+  }
+
+  public void setFormatted(String formatted) {
+    this.formatted = formatted;
+  }
+
+  public String getAdditionalName() {
+    return additionalName;
+  }
+
+  public void setAdditionalName(String additionalName) {
+    this.additionalName = additionalName;
+  }
+
+  public String getFamilyName() {
+    return familyName;
+  }
+
+  public void setFamilyName(String familyName) {
+    this.familyName = familyName;
+  }
+
+  public String getGivenName() {
+    return givenName;
+  }
+
+  public void setGivenName(String givenName) {
+    this.givenName = givenName;
+  }
+
+  public String getHonorificPrefix() {
+    return honorificPrefix;
+  }
+
+  public void setHonorificPrefix(String honorificPrefix) {
+    this.honorificPrefix = honorificPrefix;
+  }
+
+  public String getHonorificSuffix() {
+    return honorificSuffix;
+  }
+
+  public void setHonorificSuffix(String honorificSuffix) {
+    this.honorificSuffix = honorificSuffix;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/OrganizationImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/OrganizationImpl.java
new file mode 100644
index 0000000..41ca5ca
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/OrganizationImpl.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.Organization;
+
+import java.util.Date;
+
+/**
+ * Default implementation of an Organization
+ */
+public class OrganizationImpl implements Organization {
+  private Address address;
+  private String description;
+  private Date endDate;
+  private String field;
+  private String name;
+  private String salary;
+  private Date startDate;
+  private String subField;
+  private String title;
+  private String webpage;
+  private String type;
+  private Boolean primary;
+
+  public Address getAddress() {
+    return address;
+  }
+
+  public void setAddress(Address address) {
+    this.address = address;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public Date getEndDate() {
+    if (endDate == null) {
+      return null;
+    }
+    return new Date(endDate.getTime());
+  }
+
+  public void setEndDate(Date endDate) {
+    if (endDate == null) {
+      this.endDate = null;
+    } else {
+      this.endDate = new Date(endDate.getTime());
+    }
+  }
+
+  public String getField() {
+    return field;
+  }
+
+  public void setField(String field) {
+    this.field = field;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getSalary() {
+    return salary;
+  }
+
+  public void setSalary(String salary) {
+    this.salary = salary;
+  }
+
+  public Date getStartDate() {
+    if (startDate == null) {
+      return null;
+    }
+    return new Date(startDate.getTime());
+  }
+
+  public void setStartDate(Date startDate) {
+    if (startDate == null) {
+      this.startDate = null;
+    } else {
+      this.startDate = new Date(startDate.getTime());
+    }
+  }
+
+  public String getSubField() {
+    return subField;
+  }
+
+  public void setSubField(String subField) {
+    this.subField = subField;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  public String getWebpage() {
+    return webpage;
+  }
+
+  public void setWebpage(String webpage) {
+    this.webpage = webpage;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public void setType(String type) {
+    this.type = type;
+  }
+
+  public Boolean getPrimary() {
+    return primary;
+  }
+
+  public void setPrimary(Boolean primary) {
+    this.primary = primary;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/PersonImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/PersonImpl.java
new file mode 100644
index 0000000..393c97f
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/PersonImpl.java
@@ -0,0 +1,677 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.protocol.model.Enum;
+import org.apache.shindig.social.opensocial.model.Account;
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.BodyType;
+import org.apache.shindig.social.opensocial.model.Drinker;
+import org.apache.shindig.social.opensocial.model.ListField;
+import org.apache.shindig.social.opensocial.model.LookingFor;
+import org.apache.shindig.social.opensocial.model.Name;
+import org.apache.shindig.social.opensocial.model.NetworkPresence;
+import org.apache.shindig.social.opensocial.model.Organization;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.model.Smoker;
+import org.apache.shindig.social.opensocial.model.Url;
+
+import com.google.common.collect.Lists;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Default Implementation of the Person object in the model.
+ */
+public class PersonImpl implements Person {
+  private String aboutMe;
+  private List<Account> accounts;
+  private List<String> activities;
+  private List<Address> addresses;
+  private Integer age;
+  private Map<String, ?> appData;
+  private Date birthday;
+  private BodyType bodyType;
+  private List<String> books;
+  private List<String> cars;
+  private String children;
+  private Address currentLocation;
+  private String displayName;
+  private Enum<Drinker> drinker;
+  private List<ListField> emails;
+  private String ethnicity;
+  private String fashion;
+  private List<String> food;
+  private Gender gender;
+  private String happiestWhen;
+  private Boolean hasApp;
+  private List<String> heroes;
+  private String humor;
+  private String id;
+  private List<ListField> ims;
+  private List<String> interests;
+  private String jobInterests;
+  private List<String> languagesSpoken;
+  private String livingArrangement;
+  private List<Enum<LookingFor>> lookingFor;
+  private List<String> movies;
+  private List<String> music;
+  private Name name;
+  private Enum<NetworkPresence> networkPresence;
+  private String nickname;
+  private List<Organization> organizations;
+  private String pets;
+  private List<ListField> phoneNumbers;
+  private List<ListField> photos;
+  private String politicalViews;
+  private String preferredUsername;
+  private Url profileSong;
+  private Url profileVideo;
+  private List<String> quotes;
+  private String relationshipStatus;
+  private String religion;
+  private String romance;
+  private String scaredOf;
+  private String sexualOrientation;
+  private Enum<Smoker> smoker;
+  private List<String> sports;
+  private String status;
+  private List<String> tags;
+  private Long utcOffset;
+  private List<String> turnOffs;
+  private List<String> turnOns;
+  private List<String> tvShows;
+  private Date updated;
+  private List<Url> urls;
+
+  // Note: Not in the opensocial js person object directly
+  private boolean isOwner = false;
+  private boolean isViewer = false;
+
+  public PersonImpl() {
+  }
+
+  /**
+   * A constructor which contains all of the required fields on a person object
+   * @param id The id of the person
+   * @param displayName The displayName of the person
+   * @param name The person's name broken down into components
+   */
+  public PersonImpl(String id, String displayName, Name name) {
+    this.id = id;
+    this.displayName = displayName;
+    this.name = name;
+  }
+
+  public String getAboutMe() {
+    return aboutMe;
+  }
+
+  public void setAboutMe(String aboutMe) {
+    this.aboutMe = aboutMe;
+  }
+
+  public List<Account> getAccounts() {
+    return accounts;
+  }
+
+  public void setAccounts(List<Account> accounts) {
+    this.accounts = accounts;
+  }
+
+  public List<String> getActivities() {
+    return activities;
+  }
+
+  public void setActivities(List<String> activities) {
+    this.activities = activities;
+  }
+
+  public List<Address> getAddresses() {
+    return addresses;
+  }
+
+  public void setAddresses(List<Address> addresses) {
+    this.addresses = addresses;
+  }
+
+  public Integer getAge() {
+    return age;
+  }
+
+  public void setAge(Integer age) {
+    this.age = age;
+  }
+
+  public Map<String, ?> getAppData() {
+    return this.appData;
+  }
+
+  public void setAppData(Map<String, ?> appData) {
+    this.appData = appData;
+  }
+
+  public BodyType getBodyType() {
+    return bodyType;
+  }
+
+  public void setBodyType(BodyType bodyType) {
+    this.bodyType = bodyType;
+  }
+
+  public List<String> getBooks() {
+    return books;
+  }
+
+  public void setBooks(List<String> books) {
+    this.books = books;
+  }
+
+  public List<String> getCars() {
+    return cars;
+  }
+
+  public void setCars(List<String> cars) {
+    this.cars = cars;
+  }
+
+  public String getChildren() {
+    return children;
+  }
+
+  public void setChildren(String children) {
+    this.children = children;
+  }
+
+  public Address getCurrentLocation() {
+    return currentLocation;
+  }
+
+  public void setCurrentLocation(Address currentLocation) {
+    this.currentLocation = currentLocation;
+  }
+
+  public Date getBirthday() {
+    if (birthday == null) {
+      return null;
+    }
+    return new Date(birthday.getTime());
+  }
+
+  public void setBirthday(Date birthday) {
+    if (birthday == null) {
+      this.birthday = null;
+    } else {
+      this.birthday = new Date(birthday.getTime());
+    }
+  }
+
+  public String getDisplayName() {
+    return displayName;
+  }
+
+  public void setDisplayName(String displayName) {
+    this.displayName = displayName;
+  }
+
+  public org.apache.shindig.protocol.model.Enum<Drinker> getDrinker() {
+    return this.drinker;
+  }
+
+  public void setDrinker(Enum<Drinker> newDrinker) {
+    this.drinker = newDrinker;
+  }
+
+  public List<ListField> getEmails() {
+    return emails;
+  }
+
+  public void setEmails(List<ListField> emails) {
+    this.emails = emails;
+  }
+
+  public String getEthnicity() {
+    return ethnicity;
+  }
+
+  public void setEthnicity(String ethnicity) {
+    this.ethnicity = ethnicity;
+  }
+
+  public String getFashion() {
+    return fashion;
+  }
+
+  public void setFashion(String fashion) {
+    this.fashion = fashion;
+  }
+
+  public List<String> getFood() {
+    return food;
+  }
+
+  public void setFood(List<String> food) {
+    this.food = food;
+  }
+
+  public Gender getGender() {
+    return gender;
+  }
+
+  public void setGender(Gender newGender) {
+    this.gender = newGender;
+  }
+
+  public String getHappiestWhen() {
+    return happiestWhen;
+  }
+
+  public void setHappiestWhen(String happiestWhen) {
+    this.happiestWhen = happiestWhen;
+  }
+
+  public Boolean getHasApp() {
+    return hasApp;
+  }
+
+  public void setHasApp(Boolean hasApp) {
+    this.hasApp = hasApp;
+  }
+
+  public List<String> getHeroes() {
+    return heroes;
+  }
+
+  public void setHeroes(List<String> heroes) {
+    this.heroes = heroes;
+  }
+
+  public String getHumor() {
+    return humor;
+  }
+
+  public void setHumor(String humor) {
+    this.humor = humor;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  public List<ListField> getIms() {
+    return ims;
+  }
+
+  public void setIms(List<ListField> ims) {
+    this.ims = ims;
+  }
+
+  public List<String> getInterests() {
+    return interests;
+  }
+
+  public void setInterests(List<String> interests) {
+    this.interests = interests;
+  }
+
+  public String getJobInterests() {
+    return jobInterests;
+  }
+
+  public void setJobInterests(String jobInterests) {
+    this.jobInterests = jobInterests;
+  }
+
+  public List<String> getLanguagesSpoken() {
+    return languagesSpoken;
+  }
+
+  public void setLanguagesSpoken(List<String> languagesSpoken) {
+    this.languagesSpoken = languagesSpoken;
+  }
+
+  public Date getUpdated() {
+    if (updated == null) {
+      return null;
+    }
+    return new Date(updated.getTime());
+  }
+
+  public void setUpdated(Date updated) {
+    if (updated == null) {
+      this.updated = null;
+    } else {
+      this.updated = new Date(updated.getTime());
+    }
+  }
+
+  public String getLivingArrangement() {
+    return livingArrangement;
+  }
+
+  public void setLivingArrangement(String livingArrangement) {
+    this.livingArrangement = livingArrangement;
+  }
+
+  public List<Enum<LookingFor>> getLookingFor() {
+    return lookingFor;
+  }
+
+  public void setLookingFor(List<Enum<LookingFor>> lookingFor) {
+    this.lookingFor = lookingFor;
+  }
+
+  public List<String> getMovies() {
+    return movies;
+  }
+
+  public void setMovies(List<String> movies) {
+    this.movies = movies;
+  }
+
+  public List<String> getMusic() {
+    return music;
+  }
+
+  public void setMusic(List<String> music) {
+    this.music = music;
+  }
+
+  public Name getName() {
+    return name;
+  }
+
+  public void setName(Name name) {
+    this.name = name;
+  }
+
+  public Enum<NetworkPresence> getNetworkPresence() {
+    return networkPresence;
+  }
+
+  public void setNetworkPresence(Enum<NetworkPresence> networkPresence) {
+    this.networkPresence = networkPresence;
+  }
+
+  public String getNickname() {
+    return nickname;
+  }
+
+  public void setNickname(String nickname) {
+    this.nickname = nickname;
+  }
+
+  public List<Organization> getOrganizations() {
+    return organizations;
+  }
+
+  public void setOrganizations(List<Organization> organizations) {
+    this.organizations = organizations;
+  }
+
+  public String getPets() {
+    return pets;
+  }
+
+  public void setPets(String pets) {
+    this.pets = pets;
+  }
+
+  public List<ListField> getPhoneNumbers() {
+    return phoneNumbers;
+  }
+
+  public void setPhoneNumbers(List<ListField> phoneNumbers) {
+    this.phoneNumbers = phoneNumbers;
+  }
+
+  public List<ListField> getPhotos() {
+    return photos;
+  }
+
+  public void setPhotos(List<ListField> photos) {
+    this.photos = photos;
+  }
+
+  public String getPoliticalViews() {
+    return politicalViews;
+  }
+
+  public void setPoliticalViews(String politicalViews) {
+    this.politicalViews = politicalViews;
+  }
+
+  public String getPreferredUsername() {
+    return preferredUsername;
+  }
+
+  public void setPreferredUsername(String preferredUsername) {
+    this.preferredUsername = preferredUsername;
+  }
+
+  public Url getProfileSong() {
+    return profileSong;
+  }
+
+  public void setProfileSong(Url profileSong) {
+    this.profileSong = profileSong;
+  }
+
+  public Url getProfileVideo() {
+    return profileVideo;
+  }
+
+  public void setProfileVideo(Url profileVideo) {
+    this.profileVideo = profileVideo;
+  }
+
+  public List<String> getQuotes() {
+    return quotes;
+  }
+
+  public void setQuotes(List<String> quotes) {
+    this.quotes = quotes;
+  }
+
+  public String getRelationshipStatus() {
+    return relationshipStatus;
+  }
+
+  public void setRelationshipStatus(String relationshipStatus) {
+    this.relationshipStatus = relationshipStatus;
+  }
+
+  public String getReligion() {
+    return religion;
+  }
+
+  public void setReligion(String religion) {
+    this.religion = religion;
+  }
+
+  public String getRomance() {
+    return romance;
+  }
+
+  public void setRomance(String romance) {
+    this.romance = romance;
+  }
+
+  public String getScaredOf() {
+    return scaredOf;
+  }
+
+  public void setScaredOf(String scaredOf) {
+    this.scaredOf = scaredOf;
+  }
+
+  public String getSexualOrientation() {
+    return sexualOrientation;
+  }
+
+  public void setSexualOrientation(String sexualOrientation) {
+    this.sexualOrientation = sexualOrientation;
+  }
+
+  public Enum<Smoker> getSmoker() {
+    return this.smoker;
+  }
+
+  public void setSmoker(Enum<Smoker> newSmoker) {
+    this.smoker = newSmoker;
+  }
+
+  public List<String> getSports() {
+    return sports;
+  }
+
+  public void setSports(List<String> sports) {
+    this.sports = sports;
+  }
+
+  public String getStatus() {
+    return status;
+  }
+
+  public void setStatus(String status) {
+    this.status = status;
+  }
+
+  public List<String> getTags() {
+    return tags;
+  }
+
+  public void setTags(List<String> tags) {
+    this.tags = tags;
+  }
+
+  public Long getUtcOffset() {
+    return utcOffset;
+  }
+
+  public void setUtcOffset(Long utcOffset) {
+    this.utcOffset = utcOffset;
+  }
+
+  public List<String> getTurnOffs() {
+    return turnOffs;
+  }
+
+  public void setTurnOffs(List<String> turnOffs) {
+    this.turnOffs = turnOffs;
+  }
+
+  public List<String> getTurnOns() {
+    return turnOns;
+  }
+
+  public void setTurnOns(List<String> turnOns) {
+    this.turnOns = turnOns;
+  }
+
+  public List<String> getTvShows() {
+    return tvShows;
+  }
+
+  public void setTvShows(List<String> tvShows) {
+    this.tvShows = tvShows;
+  }
+
+  public List<Url> getUrls() {
+    return urls;
+  }
+
+  public void setUrls(List<Url> urls) {
+    this.urls = urls;
+  }
+
+  public boolean getIsOwner() {
+    return isOwner;
+  }
+
+  public void setIsOwner(boolean isOwner) {
+    this.isOwner = isOwner;
+  }
+
+  public boolean getIsViewer() {
+    return isViewer;
+  }
+
+  public void setIsViewer(boolean isViewer) {
+    this.isViewer = isViewer;
+  }
+
+  // Proxied fields
+
+  public String getProfileUrl() {
+    Url url = getListFieldWithType(PROFILE_URL_TYPE, getUrls());
+    return url == null ? null : url.getValue();
+  }
+
+  public void setProfileUrl(String profileUrl) {
+    Url url = getListFieldWithType(PROFILE_URL_TYPE, getUrls());
+    if (url != null) {
+      url.setValue(profileUrl);
+    } else {
+      if (profileUrl != null)
+        setUrls(addListField(new UrlImpl(profileUrl, null, PROFILE_URL_TYPE), getUrls()));
+    }
+  }
+
+  public String getThumbnailUrl() {
+    ListField photo = getListFieldWithType(THUMBNAIL_PHOTO_TYPE, getPhotos());
+    return photo == null ? null : photo.getValue();
+  }
+
+  public void setThumbnailUrl(String thumbnailUrl) {
+    ListField photo = getListFieldWithType(THUMBNAIL_PHOTO_TYPE, getPhotos());
+    if (photo != null) {
+      photo.setValue(thumbnailUrl);
+    } else {
+      if (thumbnailUrl != null)
+        setPhotos(addListField(new ListFieldImpl(THUMBNAIL_PHOTO_TYPE, thumbnailUrl), getPhotos()));
+    }
+  }
+
+  private <T extends ListField> T getListFieldWithType(String type, List<T> list) {
+    if (list != null) {
+      for (T url : list) {
+        if (type.equalsIgnoreCase(url.getType())) {
+          return url;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  private <T extends ListField> List<T> addListField(T field, List<T> list) {
+    if (list == null) {
+      list = Lists.newArrayList();
+    }
+    list.add(field);
+    return list;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/UrlImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/UrlImpl.java
new file mode 100644
index 0000000..92f8177
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/UrlImpl.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.model;
+
+import org.apache.shindig.social.opensocial.model.Url;
+
+/**
+ * see
+ * <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Url">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Url</a>
+ */
+public class UrlImpl extends ListFieldImpl implements Url {
+  private String linkText;
+
+  public UrlImpl() { }
+
+  public UrlImpl(String value, String linkText, String type) {
+    super(type, value);
+    this.linkText = linkText;
+  }
+
+  public String getLinkText() {
+    return linkText;
+  }
+
+  public void setLinkText(String linkText) {
+    this.linkText = linkText;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/package-info.java
new file mode 100644
index 0000000..fddf21d
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/model/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * <h1>Core Social Model</h1>
+ * <p>Default implementations of the Social Model. Implementors my want
+ * to extend these classes, however the rest of the code should not bind
+ * directly to these classes, but should bind to the interfaces in
+ * o.a.s.social.opensocial.model, if this is observed, then implementors
+ * will also be free to provide their own implementations of the the model
+ * interfaces.</p>
+ */
+
+package org.apache.shindig.social.core.model;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/AuthenticationHandlerProvider.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/AuthenticationHandlerProvider.java
new file mode 100644
index 0000000..d1fd9be
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/AuthenticationHandlerProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import com.google.common.collect.Lists;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.shindig.auth.AnonymousAuthenticationHandler;
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.auth.UrlParameterAuthenticationHandler;
+import org.apache.shindig.social.core.oauth2.OAuth2AuthenticationHandler;
+
+import java.util.List;
+
+/**
+ * Guice provider of an ordered list of Authentication Providers
+ */
+public class AuthenticationHandlerProvider implements Provider<List<AuthenticationHandler>> {
+  protected List<AuthenticationHandler> handlers;
+
+  @Inject
+  public AuthenticationHandlerProvider(OAuth2AuthenticationHandler oauth2Handler, UrlParameterAuthenticationHandler urlParam,
+      OAuthAuthenticationHandler threeLeggedOAuth,
+      AnonymousAuthenticationHandler anonymous) {
+    handlers = Lists.newArrayList(urlParam, threeLeggedOAuth, oauth2Handler, anonymous);
+  }
+
+  public List<AuthenticationHandler> get() {
+    return handlers;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthAuthenticationHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthAuthenticationHandler.java
new file mode 100644
index 0000000..52ce863
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthAuthenticationHandler.java
@@ -0,0 +1,209 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import com.google.common.base.Strings;
+import com.google.inject.Inject;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthException;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuthValidator;
+import net.oauth.OAuthProblemException;
+import net.oauth.server.OAuthServlet;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.auth.OAuthConstants;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.social.opensocial.oauth.OAuthDataStore;
+import org.apache.shindig.social.opensocial.oauth.OAuthEntry;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Handle both 2-legged consumer and full 3-legged OAuth requests.
+ */
+public class OAuthAuthenticationHandler implements AuthenticationHandler {
+
+  public static final String REQUESTOR_ID_PARAM = "xoauth_requestor_id";
+
+  private final OAuthDataStore store;
+  private final OAuthValidator validator;
+
+  @Inject
+  public OAuthAuthenticationHandler(OAuthDataStore store, OAuthValidator validator) {
+    this.store = store;
+    this.validator = validator;
+  }
+
+  public String getName() {
+    return "OAuth";
+  }
+
+  public String getWWWAuthenticateHeader(String realm) {
+    return String.format("OAuth realm=\"%s\"", realm);
+  }
+
+  public SecurityToken getSecurityTokenFromRequest(HttpServletRequest request)
+    throws InvalidAuthenticationException {
+    OAuthMessage message = OAuthServlet.getMessage(request, null);
+    if (Strings.isNullOrEmpty(getParameter(message, OAuth.OAUTH_SIGNATURE))) {
+      // Is not an oauth request
+      return null;
+    }
+    String bodyHash = getParameter(message, OAuthConstants.OAUTH_BODY_HASH);
+    if (!Strings.isNullOrEmpty(bodyHash)) {
+      verifyBodyHash(request, bodyHash);
+    }
+    try {
+      return verifyMessage(message);
+    } catch (OAuthProblemException oauthException) {
+      throw new InvalidAuthenticationException("OAuth Authentication Failure", oauthException);
+    }
+  }
+
+  protected SecurityToken verifyMessage(OAuthMessage message)
+    throws OAuthProblemException {
+    OAuthEntry entry = getOAuthEntry(message);
+    OAuthConsumer authConsumer = getConsumer(message);
+
+    OAuthAccessor accessor = new OAuthAccessor(authConsumer);
+
+    if (entry != null) {
+      accessor.tokenSecret = entry.getTokenSecret();
+      accessor.accessToken = entry.getToken();
+    }
+
+    try {
+      validator.validateMessage(message, accessor);
+    } catch (OAuthProblemException e) {
+      throw e;
+    } catch (OAuthException e) {
+      OAuthProblemException ope = new OAuthProblemException(OAuth.Problems.SIGNATURE_INVALID);
+      ope.setParameter(OAuth.Problems.OAUTH_PROBLEM_ADVICE, e.getMessage());
+      throw ope;
+    } catch (IOException e) {
+      OAuthProblemException ope = new OAuthProblemException(OAuth.Problems.SIGNATURE_INVALID);
+      ope.setParameter(OAuth.Problems.OAUTH_PROBLEM_ADVICE, e.getMessage());
+      throw ope;
+    } catch (URISyntaxException e) {
+      OAuthProblemException ope = new OAuthProblemException(OAuth.Problems.SIGNATURE_INVALID);
+      ope.setParameter(OAuth.Problems.OAUTH_PROBLEM_ADVICE, e.getMessage());
+      throw ope;
+    }
+    return getTokenFromVerifiedRequest(message, entry, authConsumer);
+  }
+
+  protected OAuthEntry getOAuthEntry(OAuthMessage message) throws OAuthProblemException {
+    OAuthEntry entry = null;
+    String token = getParameter(message, OAuth.OAUTH_TOKEN);
+    if (!Strings.isNullOrEmpty(token))  {
+      entry = store.getEntry(token);
+      if (entry == null) {
+        OAuthProblemException e = new OAuthProblemException(OAuth.Problems.TOKEN_REJECTED);
+        e.setParameter(OAuth.Problems.OAUTH_PROBLEM_ADVICE, "cannot find token");
+        throw e;
+      } else if (entry.getType() != OAuthEntry.Type.ACCESS) {
+        OAuthProblemException e = new OAuthProblemException(OAuth.Problems.TOKEN_REJECTED);
+        e.setParameter(OAuth.Problems.OAUTH_PROBLEM_ADVICE, "token is not an access token");
+        throw e;
+      } else if (entry.isExpired()) {
+        throw new OAuthProblemException(OAuth.Problems.TOKEN_EXPIRED);
+      }
+    }
+    return entry;
+  }
+
+  protected OAuthConsumer getConsumer(OAuthMessage message) throws OAuthProblemException {
+    String consumerKey = getParameter(message, OAuth.OAUTH_CONSUMER_KEY);
+    OAuthConsumer authConsumer = store.getConsumer(consumerKey);
+    if (authConsumer == null) {
+      throw new OAuthProblemException(OAuth.Problems.CONSUMER_KEY_UNKNOWN);
+    }
+    return authConsumer;
+  }
+
+  protected SecurityToken getTokenFromVerifiedRequest(OAuthMessage message, OAuthEntry entry,
+                                                      OAuthConsumer authConsumer) throws OAuthProblemException {
+    if (entry != null) {
+      return new OAuthSecurityToken(entry.getUserId(), entry.getCallbackUrl(), entry.getAppId(),
+                                    entry.getDomain(), entry.getContainer(), entry.expiresAt().getTime());
+    } else {
+      String userId = getParameter(message, REQUESTOR_ID_PARAM);
+      return store.getSecurityTokenForConsumerRequest(authConsumer.consumerKey, userId);
+    }
+  }
+
+  public static byte[] readBody(HttpServletRequest request) throws IOException {
+    if (request.getAttribute(AuthenticationHandler.STASHED_BODY) != null) {
+      return (byte[])request.getAttribute(AuthenticationHandler.STASHED_BODY);
+    }
+    byte[] rawBody = IOUtils.toByteArray(request.getInputStream());
+    request.setAttribute(AuthenticationHandler.STASHED_BODY, rawBody);
+    return rawBody;
+  }
+
+  public static String readBodyString(HttpServletRequest request) throws IOException {
+    byte[] rawBody = readBody(request);
+    return IOUtils.toString(new ByteArrayInputStream(rawBody), request.getCharacterEncoding());
+  }
+
+  public static void verifyBodyHash(HttpServletRequest request, String oauthBodyHash)
+    throws InvalidAuthenticationException {
+    // we are doing body hash signing which is not permitted for form-encoded data
+    if (request.getContentType() != null && request.getContentType().contains(OAuth.FORM_ENCODED)) {
+      throw new AuthenticationHandler.InvalidAuthenticationException(
+        "Cannot use oauth_body_hash with a Content-Type of application/x-www-form-urlencoded",
+        null);
+    } else {
+      try {
+        byte[] rawBody = readBody(request);
+        byte[] received = Base64.decodeBase64(CharsetUtil.getUtf8Bytes(oauthBodyHash));
+        byte[] expected = DigestUtils.sha(rawBody);
+        if (!Arrays.equals(received, expected)) {
+          throw new AuthenticationHandler.InvalidAuthenticationException(
+            "oauth_body_hash failed verification", null);
+        }
+      } catch (IOException ioe) {
+        throw new AuthenticationHandler.InvalidAuthenticationException(
+          "Unable to read content body for oauth_body_hash verification", null);
+      }
+    }
+  }
+
+  public static String getParameter(OAuthMessage requestMessage, String key) {
+    try {
+      return StringUtils.trim(requestMessage.getParameter(key));
+    } catch (IOException e) {
+      return null;
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthSecurityToken.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthSecurityToken.java
new file mode 100644
index 0000000..6be8a15
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthSecurityToken.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import java.util.EnumSet;
+
+import org.apache.shindig.auth.AbstractSecurityToken;
+import org.apache.shindig.auth.AuthenticationMode;
+
+/**
+ * A SecurityToken that represents two/three legged OAuth requests
+ */
+public class OAuthSecurityToken extends AbstractSecurityToken {
+  private static final EnumSet<Keys> MAPKEYS = EnumSet.of(
+    Keys.VIEWER, Keys.OWNER, Keys.APP_URL, Keys.APP_ID, Keys.DOMAIN, Keys.CONTAINER, Keys.EXPIRES
+  );
+
+  private final String authMode;
+
+  public OAuthSecurityToken(String userId, String appUrl, String appId, String domain,
+      String container, Long expiresAt) {
+    this(userId, appUrl, appId, domain, container, expiresAt, AuthenticationMode.OAUTH.name());
+  }
+
+  public OAuthSecurityToken(String userId, String appUrl, String appId, String domain,
+      String container, Long expiresAt, String authMode) {
+
+    setViewerId(userId);
+    setOwnerId(userId);
+    setAppUrl(appUrl);
+    setAppId(appId);
+    setDomain(domain);
+    setContainer(container);
+    setExpiresAt(expiresAt);
+    this.authMode = authMode;
+  }
+
+  // We don't support this concept yet. We probably don't need to as opensocial calls don't
+  // currently depend on the app instance id.
+  @Override
+  public long getModuleId() {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getUpdatedToken() {
+    throw new UnsupportedOperationException();
+  }
+
+  public String getAuthenticationMode() {
+    return authMode;
+  }
+
+  @Override
+  public String getTrustedJson() {
+    throw new UnsupportedOperationException();
+  }
+
+  public boolean isAnonymous() {
+    return false;
+  }
+
+  @Override
+  protected EnumSet<Keys> getMapKeys() {
+    return MAPKEYS;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthValidatorProvider.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthValidatorProvider.java
new file mode 100644
index 0000000..196913b
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/OAuthValidatorProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import com.google.inject.name.Named;
+import net.oauth.OAuth;
+import net.oauth.OAuthValidator;
+import net.oauth.SimpleOAuthValidator;
+
+/**
+ * Guice Provider class for OAuthValidator.
+ */
+public class OAuthValidatorProvider implements Provider<OAuthValidator> {
+  private final OAuthValidator validator;
+
+  @Inject
+  public OAuthValidatorProvider(@Named("shindig.oauth.validator-max-timestamp-age-ms")
+                                  long maxTimestampAgeMsec) {
+    validator = new SimpleOAuthValidator(maxTimestampAgeMsec, Double.parseDouble(OAuth.VERSION_1_0));
+  }
+
+  public OAuthValidator get() {
+    return validator;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/package-info.java
new file mode 100644
index 0000000..8c6f2cb
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>The Core Social OAuth package</h1>
+<p>Provides implementations of the OAuth infrastructure for the
+Social component.</p>
+*/
+package org.apache.shindig.social.core.oauth;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2AuthenticationHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2AuthenticationHandler.java
new file mode 100644
index 0000000..97794a4
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2AuthenticationHandler.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+
+import com.google.inject.Inject;
+
+/**
+ * Authentication handler for OAuth 2.0.  Authenticates requests for resources
+ * using one of the OAuth 2.0 flows.
+ */
+public class OAuth2AuthenticationHandler implements AuthenticationHandler {
+
+  //class name for logging purpose
+  private static final String classname = OAuth2AuthenticationHandler.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  private OAuth2Service store;
+
+  public String getName() {
+    return "OAuth2";
+  }
+
+  @Inject
+  public OAuth2AuthenticationHandler(OAuth2Service store) {
+    this.store = store;
+  }
+
+  /**
+   * Only denies authentication when an invalid bearer token is received.
+   * Unauthenticated requests can pass through to other AuthenticationHandlers.
+   */
+  public SecurityToken getSecurityTokenFromRequest(HttpServletRequest request)
+      throws InvalidAuthenticationException {
+
+    OAuth2NormalizedRequest normalizedReq;
+    try {
+      normalizedReq = new OAuth2NormalizedRequest(request);
+    } catch (OAuth2Exception oae) {   // request failed to normalize
+      LOG.logp(Level.WARNING, classname, "getSecurityTokenFromRequest", MessageKeys.INVALID_OAUTH);
+      return null;
+    }
+    try {
+      if (normalizedReq.getAccessToken() != null) {
+        store.validateRequestForResource(normalizedReq, null);
+        return new AnonymousSecurityToken(); // Return your valid security token
+      }
+    } catch (OAuth2Exception oae) {
+      // TODO (Eric): process OAuth2Exception properly
+      throw new InvalidAuthenticationException("Something went wrong: ", oae);
+    }
+    return null;
+  }
+
+  public String getWWWAuthenticateHeader(String realm) {
+    return String.format("Bearer realm=\"%s\"", realm);
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2AuthorizationHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2AuthorizationHandler.java
new file mode 100644
index 0000000..9dda9cc
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2AuthorizationHandler.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.TokenFormat;
+
+/**
+ * Handles requests to the OAuth 2.0 authorization end-point.
+ */
+public class OAuth2AuthorizationHandler {
+
+  private OAuth2Service service;
+
+  public OAuth2AuthorizationHandler(OAuth2Service service) {
+    this.service = service;
+  }
+
+  /**
+   * Handles an OAuth 2.0 authorization request.
+   *
+   * @param request is the original request
+   * @param response is the response of the request
+   * @return OAuth2NormalizedResponse represents the OAuth 2.0 response
+   *
+   * @throws ServletException
+   * @throws IOException
+   */
+  public OAuth2NormalizedResponse handle(HttpServletRequest request,
+      HttpServletResponse response) throws ServletException, IOException {
+    try {
+      // normalize the request
+      OAuth2NormalizedRequest normalizedReq = new OAuth2NormalizedRequest(
+          request);
+
+      // process request according to flow
+      OAuth2NormalizedResponse normalizedResp = new OAuth2NormalizedResponse();
+      if (normalizedReq.getResponseType() != null) {
+        switch (normalizedReq.getEnumeratedResponseType()) {
+        case CODE:
+          // authorization code flow
+          service.validateRequestForAuthCode(normalizedReq);
+          OAuth2Code authCode = service.grantAuthorizationCode(normalizedReq);
+
+          // send response
+          normalizedResp.setCode(authCode.getValue());
+          if (normalizedReq.getState() != null)
+            normalizedResp.setState(normalizedReq.getState());
+          normalizedResp.setHeader(
+              "Location",
+              OAuth2Utils.buildUrl(authCode.getRedirectURI(),
+                  normalizedResp.getResponseParameters(), null));
+          normalizedResp.setStatus(HttpServletResponse.SC_FOUND);
+          normalizedResp.setBodyReturned(false);
+          return normalizedResp;
+        case TOKEN:
+          // implicit flow
+          service.validateRequestForAccessToken(normalizedReq);
+          OAuth2Code accessToken = service.grantAccessToken(normalizedReq);
+
+          // send response
+          normalizedResp.setAccessToken(accessToken.getValue());
+          normalizedResp.setTokenType(TokenFormat.BEARER.toString());
+          normalizedResp.setExpiresIn((accessToken.getExpiration() - System
+              .currentTimeMillis()) + "");
+          if (normalizedReq.getState() != null)
+            normalizedResp.setState(normalizedReq.getState());
+          normalizedResp.setHeader("Location", OAuth2Utils.buildUrl(
+              accessToken.getRedirectURI(), null,
+              normalizedResp.getResponseParameters()));
+          normalizedResp.setStatus(HttpServletResponse.SC_FOUND);
+          normalizedResp.setBodyReturned(false);
+          return normalizedResp;
+        default:
+          OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+          resp.setError(ErrorType.UNSUPPORTED_RESPONSE_TYPE.toString());
+          resp.setErrorDescription("Unsupported response type");
+          resp.setStatus(HttpServletResponse.SC_FOUND);
+          resp.setBodyReturned(false);
+          throw new OAuth2Exception(resp);
+        }
+      } else {
+        OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+        resp.setError(ErrorType.UNSUPPORTED_RESPONSE_TYPE.toString());
+        resp.setErrorDescription("Unsupported response type");
+        resp.setStatus(HttpServletResponse.SC_FOUND);
+        resp.setBodyReturned(false);
+        throw new OAuth2Exception(resp);
+      }
+    } catch (OAuth2Exception oae) {
+      return oae.getNormalizedResponse();
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Client.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Client.java
new file mode 100644
index 0000000..3dfc505
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Client.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+/**
+ * Represents an OAuth 2.0 client.
+ */
+public class OAuth2Client {
+
+  protected String id;
+  protected String secret;
+  protected String redirectURI;
+  protected String title;
+  protected String iconUrl;
+  protected ClientType type;
+  private Flow flow;
+
+  /**
+   * Gets the client's ID.
+   *
+   * @return String represents the client's ID.
+   */
+  public String getId() {
+    return id;
+  }
+
+  /**
+   * Sets the client's ID.
+   *
+   * @param id
+   *          represents the client's ID.
+   */
+  public void setId(String id) {
+    this.id = id;
+  }
+
+  /**
+   * Gets the client's secret.
+   *
+   * @return String represents the client's secret
+   */
+  public String getSecret() {
+    return secret;
+  }
+
+  /**
+   * Sets the client's secret.
+   *
+   * @param secret
+   *          represents the client's secret
+   */
+  public void setSecret(String secret) {
+    this.secret = secret;
+  }
+
+  /**
+   * Gets the client's redirect URI.
+   *
+   * @return String represents the client's redirect URI
+   */
+  public String getRedirectURI() {
+    return redirectURI;
+  }
+
+  /**
+   * Sets the client's redirect URI.
+   *
+   * @param redirectUri
+   *          represents the client's redirect URI
+   */
+  public void setRedirectURI(String redirectUri) {
+    this.redirectURI = redirectUri;
+  }
+
+  /**
+   * Gets the client's title.
+   *
+   * @return String represents the client's title
+   */
+  public String getTitle() {
+    return title;
+  }
+
+  /**
+   * Sets the client's title.
+   *
+   * @param title
+   *          represents the client's title
+   */
+  public void setTitle(String title) {
+    this.title = title;
+  }
+
+  /**
+   * Gets the client's icon URL.
+   *
+   * @return String represents the client's icon URL
+   */
+  public String getIconUrl() {
+    return iconUrl;
+  }
+
+  /**
+   * Sets the client's icon URL.
+   *
+   * @param iconUrl
+   *          represents the client's icon URL
+   */
+  public void setIconUrl(String iconUrl) {
+    this.iconUrl = iconUrl;
+  }
+
+  /**
+   * Gets the client's type.
+   *
+   * @return ClientType represents the client's type
+   */
+  public ClientType getType() {
+    return type;
+  }
+
+  /**
+   * Sets the client's type.
+   *
+   * @param clientType
+   *          represents the client's type
+   */
+  public void setType(ClientType type) {
+    this.type = type;
+  }
+
+  /**
+   * Sets the client's OAuth2 flow (via a String flow identifier)
+   *
+   * @param flow
+   */
+  public void setFlow(String flow) {
+    if (Flow.CLIENT_CREDENTIALS.toString().equals(flow)) {
+      this.flow = Flow.CLIENT_CREDENTIALS;
+    } else if (Flow.AUTHORIZATION_CODE.toString().equals(flow)) {
+      this.flow = Flow.AUTHORIZATION_CODE;
+    } else if (Flow.IMPLICIT.toString().equals(flow)) {
+      this.flow = Flow.IMPLICIT;
+    } else {
+      this.flow = null;
+    }
+  }
+
+  /**
+   * Sets the client's OAuth2 flow
+   *
+   * @param flow
+   */
+  public void setFlowEnum(Flow flow) {
+    this.flow = flow;
+  }
+
+  /**
+   * Gets the client's OAuth2 flow
+   *
+   * @return
+   */
+  public Flow getFlow() {
+    return flow;
+  }
+
+  /**
+   * Enumerated client types in the OAuth 2.0 specification.
+   */
+  public static enum ClientType {
+    PUBLIC("public"), CONFIDENTIAL("confidential");
+
+    private final String name;
+
+    private ClientType(String name) {
+      this.name = name;
+    }
+
+    public String toString() {
+      return name;
+    }
+  }
+
+  public static enum Flow {
+    CLIENT_CREDENTIALS("client_credentials"), AUTHORIZATION_CODE(
+        "authorization_code"), IMPLICIT("implicit");
+
+    private final String name;
+
+    private Flow(String name) {
+      this.name = name;
+    }
+
+    public String toString() {
+      return name;
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Code.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Code.java
new file mode 100644
index 0000000..94b04c7
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Code.java
@@ -0,0 +1,244 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.util.List;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Types.CodeType;
+
+/**
+ * Represents a "code" string in an OAuth 2.0 handshake, including authorization
+ * code, access token, or refresh token. These signatures may all expire. They
+ * may also be associated with a redirect_url and/or another code.
+ */
+public class OAuth2Code implements Comparable<OAuth2Code> {
+
+  private String value;
+  private String redirectURI;
+  private long expiration;
+  private List<String> scope; // TODO (Eric): should be a string, interpret as a list during validation
+  private OAuth2Client client;
+  private OAuth2Code relatedAuthCode;
+  private OAuth2Code relatedRefreshToken;
+  private OAuth2Code relatedAccessToken;
+  private CodeType type;
+
+  public OAuth2Code() {
+
+  }
+
+  /**
+   * Constructs an OAuth2Code.
+   *
+   * @param value is the String key that makes up the code
+   * @param redirectURI is redirect URI associated with this code
+   * @param expiration indicates when this code expires
+   * @param scope indicates the scope of this code
+   */
+  public OAuth2Code(String value, String redirectURI, long expiration,
+      List<String> scope) {
+    this.value = value;
+    this.redirectURI = redirectURI;
+    this.expiration = expiration;
+    this.scope = scope;
+  }
+
+  /**
+   * Constructs an OAuth2Code with a value.
+   *
+   * @param value is the String key that makes up the code
+   */
+  public OAuth2Code(String value) {
+    this.value = value;
+  }
+
+  /**
+   * Returns the value of this code.
+   *
+   * @return String is the key of this code
+   */
+  public String getValue() {
+    return value;
+  }
+
+  /**
+   * Sets the value of this code.
+   *
+   * @param value is the value to set this code to
+   */
+  public void setValue(String value) {
+    this.value = value;
+  }
+
+  /**
+   * Returns the redirect URI associated with this code.
+   *
+   * @return String represents this code's redirect URI
+   */
+  public String getRedirectURI() {
+    return redirectURI;
+  }
+
+  /**
+   * Sets the redirect URI associated with this code.
+   *
+   * @param redirectURI represents the redirect URI of this code
+   */
+  public void setRedirectURI(String redirectURI) {
+    this.redirectURI = redirectURI;
+  }
+
+  /**
+   * Returns when this code expires.
+   *
+   * @return long represents when this code will expire
+   */
+  public long getExpiration() {
+    return expiration;
+  }
+
+  /**
+   * Sets the expiration of this code.
+   *
+   * @param expiration is when this code will expire
+   */
+  public void setExpiration(long expiration) {
+    this.expiration = expiration;
+  }
+
+  /**
+   * Compares this code to another code.
+   *
+   * @return int indicates how the value of this code compares to another
+   */
+  public int compareTo(OAuth2Code target) {
+    return value.compareTo(target.getValue());
+  }
+
+  /**
+   * Returns the scope of this code.
+   *
+   * @return List<String> represents the scope of this code
+   */
+  public List<String> getScope() {
+    return scope;
+  }
+
+  /**
+   * Sets the scope of this code.
+   *
+   * @param scope is this code's authorized scope
+   */
+  public void setScope(List<String> scope) {
+    this.scope = scope;
+  }
+
+  /**
+   * Sets the client associated with this code.
+   *
+   * @param client is the client to associate with this code
+   */
+  public void setClient(OAuth2Client client) {
+    this.client = client;
+  }
+
+  /**
+   * Returns the client associated with this code.
+   *
+   * @return OAuth2Client represents the client associated with this code
+   */
+  public OAuth2Client getClient() {
+    return client;
+  }
+
+  /**
+   * Sets the type of this code; one of
+   *  AUTHORIZATION_CODE,
+   *  ACCESS_TOKEN,
+   *  REFRESH_TOKEN
+   *
+   * @param type is this code's type
+   */
+  public void setType(CodeType type) {
+    this.type = type;
+  }
+
+  /**
+   * Returns the type of this code.
+   *
+   * @return CodeType represents the type of this code
+   */
+  public CodeType getType() {
+    return type;
+  }
+
+  /**
+   * Sets the authorization code that this code is related to, if applicable.
+   *
+   * @param code is the authorization code to associate with this code
+   */
+  public void setRelatedAuthCode(OAuth2Code code) {
+    this.relatedAuthCode = code;
+  }
+
+  /**
+   * Returns the authorization code related to this code.
+   *
+   * @return OAuth2Code is the authorization code related to this code
+   */
+  public OAuth2Code getRelatedAuthCode() {
+    return relatedAuthCode;
+  }
+
+  /**
+   * Sets the related refresh token.
+   *
+   * @param relatedRefreshToken is the refresh token related to this code
+   */
+  public void setRelatedRefreshToken(OAuth2Code relatedRefreshToken) {
+    this.relatedRefreshToken = relatedRefreshToken;
+  }
+
+  /**
+   * Gets the related refresh token.
+   *
+   * @return OAuth2Code is the refresh token related to this code
+   */
+  public OAuth2Code getRelatedRefreshToken() {
+    return relatedRefreshToken;
+  }
+
+  /**
+   * Sets the related access token.
+   *
+   * @param relatedAccessToken is the access token related to this code
+   */
+  public void setRelatedAccessToken(OAuth2Code relatedAccessToken) {
+    this.relatedAccessToken = relatedAccessToken;
+  }
+
+  /**
+   * Gets the related access token.
+   *
+   * @return OAuth2Code is the access token related to this code
+   */
+  public OAuth2Code getRelatedAccessToken() {
+    return relatedAccessToken;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2DataService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2DataService.java
new file mode 100644
index 0000000..43513f5
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2DataService.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+/**
+ * Services to support the management of data for the OAuth 2.0 specification.
+ * Includes management of clients, authorization codes, access tokens, and
+ * refresh tokens.
+ *
+ * TODO (Eric): client registration services
+ */
+public interface OAuth2DataService {
+
+  /**
+   * Retrieves a pre-registered client by ID.
+   *
+   * @param clientId identifies the client to retrieve
+   *
+   * @param OAuth2Client is the retrieved client
+   */
+  public OAuth2Client getClient(String clientId);
+
+  /**
+   * Retrieves an authorization code by its value.
+   *
+   * @param clientId identifies the client who owns the authorization code
+   * @param authCode is the value of the authorization code to get
+   *
+   * @return OAuth2Code is the retrieved authorization code
+   */
+  public OAuth2Code getAuthorizationCode(String clientId, String authCode);
+
+  /**
+   * Registers an authorization code with a client.
+   *
+   * @param clientId identifies the client who owns the authorization code
+   * @param authCode is the authorization code to register with the client
+   */
+  public void registerAuthorizationCode(String clientId, OAuth2Code authCode);
+
+  /**
+   * Unregisters an authorization code with a client.
+   *
+   * @param clientId identifies the client who owns the authorization code
+   * @param authCode is the value of the authorization code to unregister
+   */
+  public void unregisterAuthorizationCode(String clientId, String authCode);
+
+  /**
+   * Retrieves an access token by its value.
+   *
+   * @param accessToken is the value of the accessToken to retrieve
+   *
+   * @return OAuth2Code is the retrieved access token; null if not found
+   */
+  public OAuth2Code getAccessToken(String accessToken);
+
+  /**
+   * Registers an access token with a client.
+   *
+   * @param clientId identifies the client to register the access token with
+   * @param accessToken is the access token to register with the client
+   */
+  public void registerAccessToken(String clientId, OAuth2Code accessToken);
+
+  /**
+   * Unregisters an access token with a client.
+   *
+   * @param clientId identifies the client who owns the access token
+   * @param accessToken is the value of the access token to unregister
+   */
+  public void unregisterAccessToken(String clientId, String accessToken);
+
+  /**
+   * Retrieves a refresh token by its value.
+   *
+   * @param refreshToken is the value of the refresh token to retrieve
+   *
+   * @return OAuth2Code is the retrieved refresh token; null if not found
+   */
+  public OAuth2Code getRefreshToken(String refreshToken);
+
+  /**
+   * Registers a refresh token with a client.
+   *
+   * @param clientId identifies the client who owns the refresh token
+   * @param refreshToken is the refresh token to register with the client
+   */
+  public void registerRefreshToken(String clientId, OAuth2Code refreshToken);
+
+  /**
+   * Unregisters a refresh token with a client.
+   *
+   * @param clientId identifies the client who owns the refresh token
+   * @param refreshToken is the value of the refresh token to unregister
+   */
+  public void unregisterRefreshToken(String clientId, String refreshToken);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2DataServiceImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2DataServiceImpl.java
new file mode 100644
index 0000000..8820151
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2DataServiceImpl.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.social.core.oauth2.OAuth2Client.ClientType;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.CodeType;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+public class OAuth2DataServiceImpl implements OAuth2DataService {
+
+  private JSONObject oauthDB; // the OAuth 2.0 JSON DB
+  private BeanConverter converter; // the JSON<->Bean converter
+  private List<OAuth2Client> clients; // list of clients
+  private Map<String, List<OAuth2Code>> authCodes; // authorization codes per client
+  private Map<String, List<OAuth2Code>> accessTokens; // access tokens per client
+
+  @Inject
+  public OAuth2DataServiceImpl(
+      @Named("shindig.canonical.json.db") String jsonLocation,
+      @Named("shindig.bean.converter.json") BeanConverter converter,
+      @Named("shindig.contextroot") String contextroot) throws Exception {
+    String content = IOUtils.toString(ResourceLoader.openResource(jsonLocation), "UTF-8");
+    content = content.replace("%contextRoot%", contextroot);
+    this.oauthDB = new JSONObject(content).getJSONObject("oauth2");
+    this.converter = converter;
+    this.clients = Lists.newArrayList();
+    this.authCodes = Maps.newHashMap();
+    this.accessTokens = Maps.newHashMap();
+    loadClientsFromCanonical();
+  }
+
+  public OAuth2Client getClient(String clientId) {
+    for (OAuth2Client client : clients) {
+      if (client.getId().equals(clientId)) {
+        return client;
+      }
+    }
+    return null;
+  }
+
+  public OAuth2Code getAuthorizationCode(String clientId, String authCode) {
+    if (authCodes.containsKey(clientId)) {
+      List<OAuth2Code> codes = authCodes.get(clientId);
+      for (OAuth2Code code : codes) {
+        if (code.getValue().equals(authCode)) {
+          return code;
+        }
+      }
+    }
+    return null;
+  }
+
+  public void registerAuthorizationCode(String clientId, OAuth2Code authCode) {
+    if (authCodes.containsKey(clientId)) {
+      authCodes.get(clientId).add(authCode);
+    } else {
+      List<OAuth2Code> list = Lists.newArrayList();
+      list.add(authCode);
+      authCodes.put(clientId, list);
+    }
+  }
+
+  public void unregisterAuthorizationCode(String clientId, String authCode) {
+    if (authCodes.containsKey(clientId)) {
+      List<OAuth2Code> codes = authCodes.get(clientId);
+      for (OAuth2Code code : codes) {
+        if (code.getValue().equals(authCode)) {
+          codes.remove(code);
+          return;
+        }
+      }
+    }
+    throw new RuntimeException("signature not found"); // TODO (Eric): handle error
+  }
+
+  public OAuth2Code getAccessToken(String accessToken) {
+    for (String clientId : accessTokens.keySet()) {
+      List<OAuth2Code> tokens = accessTokens.get(clientId);
+      for (OAuth2Code token : tokens) {
+        if (token.getValue().equals(accessToken)) {
+          return token;
+        }
+      }
+    }
+    return null;
+  }
+
+  public void registerAccessToken(String clientId, OAuth2Code accessToken) {
+    if (accessTokens.containsKey(clientId)) {
+      accessTokens.get(clientId).add(accessToken);
+    } else {
+      List<OAuth2Code> list = Lists.newArrayList();
+      list.add(accessToken);
+      accessTokens.put(clientId, list);
+    }
+  }
+
+  public void unregisterAccessToken(String clientId, String accessToken) {
+    if (accessTokens.containsKey(clientId)) {
+      List<OAuth2Code> tokens = accessTokens.get(clientId);
+      for (OAuth2Code token : tokens) {
+        if (token.getValue().equals(accessToken)) {
+          tokens.remove(token);
+          return;
+        }
+      }
+    }
+    throw new RuntimeException("access token not found"); // TODO (Eric): handle error
+  }
+
+  public OAuth2Code getRefreshToken(String refreshToken) {
+    throw new RuntimeException("not yet implemented");
+  }
+
+  public void registerRefreshToken(String clientId, OAuth2Code refreshToken) {
+    throw new RuntimeException("not yet implemented");
+  }
+
+  public void unregisterRefreshToken(String clientId, String refreshToken) {
+    throw new RuntimeException("not yet implemented");
+  }
+
+  private void loadClientsFromCanonical() {
+    for (String clientId : JSONObject.getNames(oauthDB)) {
+      JSONObject clientJson;
+      try {
+        clientJson = oauthDB.getJSONObject(clientId).getJSONObject("registration");
+        OAuth2Client client = converter.convertToObject(clientJson.toString(), OAuth2Client.class);
+        client.setType(clientJson.getString("type").equals("public") ? ClientType.PUBLIC : ClientType.CONFIDENTIAL);
+        clients.add(client);
+        JSONObject clientJS = oauthDB.getJSONObject(clientId);
+        if (clientJS.has("authorizationCodes")) {
+          JSONObject authCodes = clientJS.getJSONObject("authorizationCodes");
+          for (String authCodeId : JSONObject.getNames(authCodes)) {
+            OAuth2Code code = converter.convertToObject(authCodes
+                .getJSONObject(authCodeId).toString(), OAuth2Code.class);
+            code.setValue(authCodeId);
+            code.setClient(client);
+            registerAuthorizationCode(clientId, code);
+          }
+        }
+        if (clientJS.has("accessTokens")) {
+          JSONObject accessTokens = clientJS.getJSONObject("accessTokens");
+          for (String accessTokenId : JSONObject.getNames(accessTokens)) {
+            OAuth2Code code = converter.convertToObject(accessTokens.getJSONObject(accessTokenId).toString(), OAuth2Code.class);
+            code.setValue(accessTokenId);
+            code.setClient(client);
+            code.setType(CodeType.ACCESS_TOKEN);
+            registerAccessToken(clientId, code);
+          }
+        }
+      } catch (JSONException je) {
+        throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+      }
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Exception.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Exception.java
new file mode 100644
index 0000000..4e475e5
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Exception.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+/**
+ * Represents an exception while dancing with OAuth 2.0.
+ */
+public class OAuth2Exception extends Exception {
+
+  private static final long serialVersionUID = -5892464438773813010L;
+  private OAuth2NormalizedResponse response;
+
+  /**
+   * Constructs an OAuth2Exception.
+   *
+   * @param response is the normalized response that should be used to
+   * formulate a server response.
+   */
+  public OAuth2Exception(OAuth2NormalizedResponse response) {
+    super(response.getErrorDescription());
+    this.response = response;
+  }
+
+  /**
+   * Retrieves the normalized response.
+   *
+   * @return OAuth2NormalizedResponse encapsulates the OAuth error
+   */
+  public OAuth2NormalizedResponse getNormalizedResponse() {
+    return response;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2NormalizedRequest.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2NormalizedRequest.java
new file mode 100644
index 0000000..c679488
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2NormalizedRequest.java
@@ -0,0 +1,275 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.GrantType;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ResponseType;
+
+/**
+ * Normalizes an OAuth 2.0 request by extracting OAuth 2.0 related fields.
+ *
+ * TODO (Eric): implement scope handling.
+ */
+public class OAuth2NormalizedRequest extends HashMap<String, Object> {
+
+  private static final long serialVersionUID = -7849581704967135322L;
+  private HttpServletRequest httpReq = null;
+  private static final Pattern FORM_URL_REGEX = Pattern
+      .compile("application/(x-www-)?form-url(-)?encoded");
+
+  //class name for logging purpose
+  private static final String classname = OAuth2NormalizedRequest.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  @SuppressWarnings("unchecked")
+  public OAuth2NormalizedRequest(HttpServletRequest request) throws OAuth2Exception {
+    super();
+    setHttpServletRequest(request);
+    String contentType = request.getContentType();
+    if (contentType != null) {
+      Matcher match = FORM_URL_REGEX.matcher(contentType);
+      if (match.matches()) {
+        normalizeBody(getBodyAsString(request));
+      }
+    }
+    Enumeration<String> keys = request.getParameterNames();
+    while (keys.hasMoreElements()) {
+      String key = keys.nextElement();
+      put(key, request.getParameter(key));
+    }
+    normalizeClientSecret(request);
+    normalizeAccessToken(request);
+  }
+
+  // --------------------------- NORMALIZED GETTERS ---------------------------
+  public String getClientId() {
+    return getString("client_id");
+  }
+
+  public String getClientSecret() {
+    return getString("client_secret");
+  }
+
+  public String getResponseType() {
+    return getString("response_type");
+  }
+
+  public String getGrantType() {
+    return getString("grant_type");
+  }
+
+  public String getRedirectURI() {
+    return getString("redirect_uri");
+  }
+
+  public String getAccessToken() {
+    return getString("access_token");
+  }
+
+  public String getAuthorizationCode() {
+    return getString("code");
+  }
+
+  public String getState() {
+    return getString("state");
+  }
+
+  public String getScope() {
+    return getString("scope");
+  }
+
+  public ResponseType getEnumeratedResponseType() throws OAuth2Exception {
+    String respType = getResponseType();
+    if (respType == null)
+      return null;
+    if (respType.equals("code")) {
+      return ResponseType.CODE;
+    } else if (respType.equals("token")) {
+      return ResponseType.TOKEN;
+    } else {
+      OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+      resp.setError(ErrorType.UNSUPPORTED_RESPONSE_TYPE.toString());
+      resp.setErrorDescription("Unsupported response type");
+      resp.setStatus(HttpServletResponse.SC_FOUND);
+      resp.setBodyReturned(false);
+      resp.setHeader("Location", OAuth2Utils.buildUrl(getRedirectURI(),
+          resp.getResponseParameters(), null));
+      throw new OAuth2Exception(resp);
+    }
+  }
+
+  public GrantType getEnumeratedGrantType() {
+    String grantType = getGrantType();
+    if (grantType == null)
+      return null;
+    if (grantType.equals("refresh_token")) {
+      return GrantType.REFRESH_TOKEN;
+    } else if (grantType.equals("authorization_code")) {
+      return GrantType.AUTHORIZATION_CODE;
+    } else if (grantType.equals("password")) {
+      return GrantType.PASSWORD;
+    } else if (grantType.equals("client_credentials")) {
+      return GrantType.CLIENT_CREDENTIALS;
+    } else {
+      return GrantType.CUSTOM;
+    }
+  }
+
+  public String getString(String key) {
+    if (!containsKey(key)) return null;
+    return (String) get(key);
+  }
+
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    for (String key : keySet()) {
+      sb.append(key);
+      sb.append(": ");
+      sb.append(get(key));
+      sb.append('\n');
+    }
+    return sb.toString();
+  }
+
+  // -------------------------- PRIVATE HELPERS -------------------------------
+
+  private void normalizeAccessToken(HttpServletRequest req) {
+    String bearerToken = getString("access_token");
+    if (bearerToken == null || bearerToken.equals("")) {
+      String header = req.getHeader("Authorization");
+      if (header != null && header.toLowerCase().startsWith("bearer")) {
+        String[] parts = header.split("[ \\t]+");
+        bearerToken = parts[parts.length - 1];
+      }
+    }
+    put("access_token", bearerToken);
+  }
+
+  private void normalizeClientSecret(HttpServletRequest request)
+      throws OAuth2Exception {
+    String secret = getClientSecret();
+    if (secret == null || secret.equals("")) {
+      String header = request.getHeader("Authorization");
+      if (header != null && header.toLowerCase().startsWith("basic")) {
+        String[] parts = header.split("[ \\t]+");
+        String temp = parts[parts.length - 1];
+        byte[] decodedSecret = Base64.decodeBase64(temp);
+        try {
+          temp = new String(decodedSecret, "UTF-8");
+          parts = temp.split(":");
+          if (parts != null && parts.length == 2) {
+            secret = parts[1];
+            String queryId = getString("client_id");
+            if (queryId != null && !queryId.equals(parts[0])) {
+              OAuth2NormalizedResponse response = new OAuth2NormalizedResponse();
+              response.setError(ErrorType.INVALID_REQUEST.toString());
+              response
+                  .setErrorDescription("Request contains mismatched client ids");
+              response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+              throw new OAuth2Exception(response);
+            }
+            // Lets set the client id from the Basic auth header if not already
+            // set in query,
+            // needed for client_credential flow.
+            if (queryId == null) {
+              put("client_id", parts[0]);
+            }
+          }
+        } catch (UnsupportedEncodingException e) {
+          LOG.logp(Level.WARNING, classname, "normalizeClientSecret", MessageKeys.INVALID_OAUTH, e);
+          return;
+        }
+      }
+    }
+    put("client_secret", secret);
+  }
+
+  private void normalizeBody(String body) throws OAuth2Exception {
+    if (body == null || body.length() == 0)
+      return;
+    List<NameValuePair> params;
+    try {
+      params = URLEncodedUtils.parse(new URI("http://localhost:8080?" + body),
+          "UTF-8");
+      for (NameValuePair param : params) {
+        put(param.getName(), param.getValue());
+      }
+    } catch (URISyntaxException e) {
+      OAuth2NormalizedResponse response = new OAuth2NormalizedResponse();
+      response.setError(ErrorType.INVALID_REQUEST.toString());
+      response.setErrorDescription("The message body's syntax is incorrect");
+      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+      throw new OAuth2Exception(response);
+    }
+  }
+
+  private String getBodyAsString(HttpServletRequest request) {
+    if (request.getContentLength() == 0)
+      return "";
+    InputStream is = null;
+    try {
+      String line;
+      StringBuilder sb = new StringBuilder();
+      is = request.getInputStream();
+      BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+      while ((line = reader.readLine()) != null) {
+        sb.append(line);
+      }
+      is.close();
+      return sb.toString();
+    } catch (IOException ioe) {
+      LOG.logp(Level.WARNING, classname, "getBodyAsString", MessageKeys.INVALID_OAUTH, ioe);
+      return null;
+    } finally {
+      IOUtils.closeQuietly(is);
+    }
+  }
+
+  public void setHttpServletRequest(HttpServletRequest httpReq) {
+    this.httpReq = httpReq;
+  }
+
+  public HttpServletRequest getHttpServletRequest() {
+    return httpReq;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2NormalizedResponse.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2NormalizedResponse.java
new file mode 100644
index 0000000..ca045d9
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2NormalizedResponse.java
@@ -0,0 +1,172 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.util.Map;
+
+import com.google.common.collect.Maps;
+
+/**
+ * Wraps OAuth 2.0 response elements including headers and body parameters.
+ *
+ * TODO (Eric): document this class, including bodyReturned
+ */
+public class OAuth2NormalizedResponse {
+
+  private Map<String, String> headers;
+  private Map<String, String> respParams;
+  private int status;
+  private boolean bodyReturned;
+
+  private static final String ERROR = "error";
+  private static final String ERROR_DESCRIPTION = "error_description";
+  private static final String ERROR_URI = "error_uri";
+  private static final String STATE = "state";
+  private static final String CODE = "code";
+  private static final String ACCESS_TOKEN = "access_token";
+  private static final String TOKEN_TYPE = "token_type";
+  private static final String EXPIRES_IN = "expires_in";
+  private static final String REFRESH_TOKEN = "refresh_token";
+  private static final String SCOPE = "scope";
+
+  public OAuth2NormalizedResponse() {
+    this.headers = Maps.newHashMap();
+    this.respParams = Maps.newHashMap();
+    this.status = -1;
+    this.bodyReturned = false;
+  }
+
+  public void setStatus(int status) {
+    this.status = status;
+  }
+
+  public int getStatus() {
+    return status;
+  }
+
+  public void setBodyReturned(boolean bodyReturned) {
+    this.bodyReturned = bodyReturned;
+  }
+
+  public boolean isBodyReturned() {
+    return bodyReturned;
+  }
+
+  // ------------------------------- HEADER FIELDS ----------------------------
+  public Map<String, String> getHeaders() {
+    return headers;
+  }
+
+  public void setHeaders(Map<String, String> headers) {
+    this.headers = headers;
+  }
+
+  public void setHeader(String key, String value) {
+    headers.put(key, value);
+  }
+
+  // ------------------------------ RESPONSE FIELDS ---------------------------
+  public Map<String, String> getResponseParameters() {
+    return respParams;
+  }
+
+  public void setResponseParameters(Map<String, String> responseParams) {
+    this.respParams = responseParams;
+  }
+
+  public void setError(String error) {
+    respParams.put(ERROR, error);
+  }
+
+  public String getError() {
+    return respParams.get(ERROR);
+  }
+
+  public void setErrorDescription(String errorDescription) {
+    respParams.put(ERROR_DESCRIPTION, errorDescription);
+  }
+
+  public String getErrorDescription() {
+    return respParams.get(ERROR_DESCRIPTION);
+  }
+
+  public void setErrorUri(String errorUri) {
+    respParams.put(ERROR_URI, errorUri);
+  }
+
+  public String getErrorUri() {
+    return respParams.get(ERROR_URI);
+  }
+
+  public void setState(String state) {
+    respParams.put(STATE, state);
+  }
+
+  public String getState() {
+    return respParams.get(STATE);
+  }
+
+  public void setCode(String code) {
+    respParams.put(CODE, code);
+  }
+
+  public String getCode() {
+    return respParams.get(CODE);
+  }
+
+  public void setAccessToken(String accessToken) {
+    respParams.put(ACCESS_TOKEN, accessToken);
+  }
+
+  public String getAccessToken() {
+    return respParams.get(ACCESS_TOKEN);
+  }
+
+  public void setTokenType(String tokenType) {
+    respParams.put(TOKEN_TYPE, tokenType);
+  }
+
+  public String getTokenType() {
+    return respParams.get(TOKEN_TYPE);
+  }
+
+  public void setExpiresIn(String expiresIn) {
+    respParams.put(EXPIRES_IN, expiresIn);
+  }
+
+  public String getExpiresIn() {
+    return respParams.get(EXPIRES_IN);
+  }
+
+  public void setRefreshToken(String refreshToken) {
+    respParams.put(REFRESH_TOKEN, refreshToken);
+  }
+
+  public String getRefreshToken() {
+    return respParams.get(REFRESH_TOKEN);
+  }
+
+  public void setScope(String scope) {
+    respParams.put(SCOPE, scope);
+  }
+
+  public String getScope() {
+    return respParams.get(SCOPE);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Service.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Service.java
new file mode 100644
index 0000000..818c3c7
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Service.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+/**
+ * Services to support the OAuth 2.0 specification flows and enforcement.
+ *
+ * TODO (Eric): include grant methods?
+ */
+public interface OAuth2Service {
+
+  /**
+   * Retrieves the underlying data service.
+   */
+  public OAuth2DataService getDataService();
+
+  // --------------------------- VALIDATION SERVICES --------------------------
+  /**
+   * Validates a client.
+   */
+  public void authenticateClient(OAuth2NormalizedRequest req)
+      throws OAuth2Exception;
+
+  /**
+   * Validates a client's request for an authorization token.
+   */
+  public void validateRequestForAuthCode(OAuth2NormalizedRequest req)
+      throws OAuth2Exception;
+
+  /**
+   * Validates a client's request for an access token.
+   */
+  public void validateRequestForAccessToken(OAuth2NormalizedRequest req)
+      throws OAuth2Exception;
+
+  /**
+   * Validates a client's request to use access a resource.
+   */
+  public void validateRequestForResource(OAuth2NormalizedRequest req,
+      Object resourceRequest) throws OAuth2Exception;
+
+  // ------------------- GENERATION & REGISTRATION OF CODES -------------------
+  /**
+   * Grants an authorization code to the given client by generating and
+   * registering the code.
+   */
+  public OAuth2Code grantAuthorizationCode(OAuth2NormalizedRequest req);
+
+  /**
+   * Grants an access token to the given client by generating and registering
+   * the access token.
+   */
+  public OAuth2Code grantAccessToken(OAuth2NormalizedRequest req);
+
+  /**
+   * Grants a refresh token to the given client by generating and registering
+   * the refresh token.
+   */
+  public OAuth2Code grantRefreshToken(OAuth2NormalizedRequest req);
+
+  // ------------------------ TOKEN GENERATION SERVICES -----------------------
+  /**
+   * Generates an authorization code from a client OAuth 2.0 request.
+   */
+  public OAuth2Code generateAuthorizationCode(OAuth2NormalizedRequest req);
+
+  /**
+   * Generates an access token from a client OAuth 2.0 request.
+   */
+  public OAuth2Code generateAccessToken(OAuth2NormalizedRequest req);
+
+  /**
+   * Generates a refresh token from a client OAuth 2.0 request.
+   */
+  public OAuth2Code generateRefreshToken(OAuth2NormalizedRequest req);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2ServiceImpl.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2ServiceImpl.java
new file mode 100644
index 0000000..fb3aeaf
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2ServiceImpl.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Properties;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.social.core.oauth2.OAuth2Client.ClientType;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.CodeType;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+import org.apache.shindig.social.core.oauth2.validators.AccessTokenRequestValidator;
+import org.apache.shindig.social.core.oauth2.validators.AuthorizationCodeRequestValidator;
+import org.apache.shindig.social.core.oauth2.validators.DefaultResourceRequestValidator;
+import org.apache.shindig.social.core.oauth2.validators.OAuth2ProtectedResourceValidator;
+import org.apache.shindig.social.core.oauth2.validators.OAuth2RequestValidator;
+
+import com.google.inject.CreationException;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.spi.Message;
+
+/**
+ * A simple in-memory implementation of the OAuth 2 services.
+ */
+@Singleton
+public class OAuth2ServiceImpl implements OAuth2Service {
+
+  private OAuth2DataService store; // underlying OAuth data store
+
+  private long authCodeExpires;
+  private long accessTokenExpires;
+
+  // validators
+  private OAuth2RequestValidator accessTokenValidator;
+  private OAuth2RequestValidator authCodeValidator;
+  private OAuth2ProtectedResourceValidator resourceReqValidator;
+
+
+  @Inject
+  public OAuth2ServiceImpl(OAuth2DataService store) {
+    this.store = store;
+
+    // TODO (Eric): properties should be injected, but getting "no implementation bound"
+    Properties props = readPropertyFile("shindig.properties");
+    this.authCodeExpires = Long.valueOf(props.getProperty("shindig.oauth2.authCodeExpiration"));
+    this.accessTokenExpires = Long.valueOf(props.getProperty("shindig.oauth2.accessTokenExpiration"));
+
+    // TODO (Matt): validators should be injected
+    authCodeValidator = new AuthorizationCodeRequestValidator(store);
+    accessTokenValidator = new AccessTokenRequestValidator(store);
+    resourceReqValidator = new DefaultResourceRequestValidator(store);
+  }
+
+  public OAuth2DataService getDataService() {
+    return store;
+  }
+
+  public void authenticateClient(OAuth2NormalizedRequest req)
+      throws OAuth2Exception {
+    OAuth2Client client = store.getClient(req.getClientId());
+    if (client == null) {
+      OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+      resp.setError(ErrorType.INVALID_CLIENT.toString());
+      resp.setErrorDescription("The client ID is invalid or not registered");
+      resp.setBodyReturned(true);
+      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      throw new OAuth2Exception(resp);
+    }
+    String realSecret = client.getSecret();
+    String reqSecret = req.getClientSecret();
+    if (realSecret != null || reqSecret != null
+        || client.getType() == ClientType.CONFIDENTIAL) {
+      if (realSecret == null || reqSecret == null
+          || !realSecret.equals(reqSecret)) {
+        OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+        resp.setError(ErrorType.UNAUTHORIZED_CLIENT.toString());
+        resp.setErrorDescription("The client failed to authorize");
+        resp.setBodyReturned(true);
+        resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        throw new OAuth2Exception(resp);
+      }
+    }
+  }
+
+  public void validateRequestForAuthCode(OAuth2NormalizedRequest req)
+      throws OAuth2Exception {
+    authCodeValidator.validateRequest(req);
+  }
+
+  public void validateRequestForAccessToken(OAuth2NormalizedRequest req)
+      throws OAuth2Exception {
+    accessTokenValidator.validateRequest(req);
+  }
+
+  public void validateRequestForResource(OAuth2NormalizedRequest req,
+      Object resourceRequest) throws OAuth2Exception {
+    resourceReqValidator.validateRequestForResource(req, resourceRequest);
+  }
+
+  public OAuth2Code grantAuthorizationCode(OAuth2NormalizedRequest req) {
+    OAuth2Code authCode = generateAuthorizationCode(req);
+    store.registerAuthorizationCode(req.getClientId(), authCode);
+    return authCode;
+  }
+
+  public OAuth2Code grantAccessToken(OAuth2NormalizedRequest req) {
+    OAuth2Code accessToken = generateAccessToken(req);
+    OAuth2Code authCode = store.getAuthorizationCode(req.getClientId(),
+        req.getAuthorizationCode());
+    if (authCode != null) {
+      authCode.setRelatedAccessToken(accessToken);
+    }
+    store.registerAccessToken(req.getClientId(), accessToken);
+    return accessToken;
+  }
+
+  public OAuth2Code grantRefreshToken(OAuth2NormalizedRequest req) {
+    OAuth2Code refreshToken = generateRefreshToken(req);
+    store.registerRefreshToken(req.getClientId(), refreshToken);
+    return refreshToken;
+  }
+
+  public OAuth2Code generateAuthorizationCode(OAuth2NormalizedRequest req) {
+    OAuth2Code authCode = new OAuth2Code();
+    authCode.setValue(UUID.randomUUID().toString());
+    authCode.setExpiration(System.currentTimeMillis() + authCodeExpires);
+    OAuth2Client client = store.getClient(req.getString("client_id"));
+    authCode.setClient(client);
+    if (req.getRedirectURI() != null) {
+      authCode.setRedirectURI(req.getRedirectURI());
+    } else {
+      authCode.setRedirectURI(client.getRedirectURI());
+    }
+    return authCode;
+  }
+
+  public OAuth2Code generateAccessToken(OAuth2NormalizedRequest req) {
+    // generate token value
+    OAuth2Code accessToken = new OAuth2Code();
+    accessToken.setType(CodeType.ACCESS_TOKEN);
+    accessToken.setValue(UUID.randomUUID().toString());
+    accessToken.setExpiration(System.currentTimeMillis() + accessTokenExpires);
+    if (req.getRedirectURI() != null) {
+      accessToken.setRedirectURI(req.getRedirectURI());
+    } else {
+      accessToken.setRedirectURI(store.getClient(req.getClientId()).getRedirectURI());
+    }
+
+    // associate with existing authorization code, if an auth code exists.
+    if (req.getAuthorizationCode() != null) {
+      OAuth2Code authCode = store.getAuthorizationCode(req.getClientId(), req.getAuthorizationCode());
+      accessToken.setRelatedAuthCode(authCode);
+      accessToken.setClient(authCode.getClient());
+      if (authCode.getScope() != null) {
+        accessToken.setScope(new ArrayList<String>(authCode.getScope()));
+      }
+    }
+
+    return accessToken;
+  }
+
+  // TODO (Eric): Refresh tokens are not yet supported.
+  public OAuth2Code generateRefreshToken(OAuth2NormalizedRequest req) {
+    throw new RuntimeException("not yet implemented");
+  }
+
+  private Properties readPropertyFile(String propertyFile) {
+    Properties properties = new Properties();
+    InputStream is = null;
+    try {
+      is = ResourceLoader.openResource(propertyFile);
+      properties.load(is);
+    } catch (IOException e) {
+      throw new CreationException(Arrays.asList(
+          new Message("Unable to load properties: " + propertyFile)));
+    } finally {
+      IOUtils.closeQuietly( is );
+    }
+    return properties;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Servlet.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Servlet.java
new file mode 100644
index 0000000..a67b6ea
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Servlet.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.json.JSONObject;
+
+import com.google.inject.Inject;
+
+/**
+ * Main servlet to catch OAuth 2.0 requests.
+ */
+public class OAuth2Servlet extends InjectedServlet {
+
+  private static final long serialVersionUID = -4257719224664564922L;
+  private static OAuth2AuthorizationHandler authorizationHandler;
+  private static OAuth2TokenHandler tokenHandler;
+
+  //class name for logging purpose
+  private static final String classname = OAuth2Servlet.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  @Inject
+  public void setOAuth2Service(OAuth2Service oauthService) {
+    authorizationHandler = new OAuth2AuthorizationHandler(oauthService);
+    tokenHandler = new OAuth2TokenHandler(oauthService);
+  }
+
+  @Override
+  public void init(ServletConfig config) throws ServletException {
+    super.init(config);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    HttpUtil.setNoCache(response);
+    String path = request.getPathInfo();
+    if (path.endsWith("authorize")) {
+      sendOAuth2Response(response, authorizationHandler.handle(request, response));
+    } else if (path.endsWith("token")) {
+      sendOAuth2Response(response, tokenHandler.handle(request, response));
+    } else {
+      response.sendError(HttpServletResponse.SC_NOT_FOUND, "Unknown URL");
+    }
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest request, HttpServletResponse response)
+      throws ServletException, IOException {
+    doGet(request, response);
+  }
+
+  /**
+   * Sends an OAuth 2.0 response based on an OAuth2NormalizedResponse object.
+   *
+   * @param servletResp is the servlet's response object
+   * @param normalizedResp maintains the headers and body fields to respond with
+   */
+  private void sendOAuth2Response(HttpServletResponse servletResp,
+      OAuth2NormalizedResponse normalizedResp) {
+    // set status
+    servletResp.setStatus(normalizedResp.getStatus());
+
+    // set body parameters
+    Map<String, String> respParams = normalizedResp.getResponseParameters();
+    if (normalizedResp.isBodyReturned() && respParams != null) {
+      PrintWriter out = null;
+      try {
+        servletResp.setHeader("Content-Type", "application/json");
+        out = servletResp.getWriter();
+        out.println(new JSONObject(respParams).toString());
+        out.flush();
+      } catch (IOException e) {
+        LOG.logp(Level.WARNING, classname, "getBodyAsString", MessageKeys.INVALID_OAUTH, e);
+        throw new RuntimeException(e);
+      } finally {
+        IOUtils.closeQuietly(out);
+      }
+    }
+
+    // set headers
+    Map<String, String> headers = normalizedResp.getHeaders();
+    if (headers != null) {
+      for (String key : headers.keySet()) {
+        servletResp.setHeader(key, headers.get(key));
+      }
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2TokenHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2TokenHandler.java
new file mode 100644
index 0000000..e01915b
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2TokenHandler.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Types.TokenFormat;
+
+/**
+ * Handles operations to the OAuth 2.0 token end point.
+ *
+ * TODO (Eric): generate refreshToken & associate with accessToken
+ */
+public class OAuth2TokenHandler {
+
+  private OAuth2Service service;
+
+  /**
+   * Constructs the token handler with the OAuth2Service.
+   *
+   * @param service is the service that will support this handler
+   */
+  public OAuth2TokenHandler(OAuth2Service service) {
+    this.service = service;
+  }
+
+  /**
+   * Handles an OAuth 2.0 request to the token endpoint.
+   *
+   * @param request is the servlet request object
+   * @param response is the servlet response object
+   * @return OAuth2NormalizedResponse encapsulates the request's response
+   *
+   * @throws ServletException
+   * @throws IOException
+   */
+  public OAuth2NormalizedResponse handle(HttpServletRequest request,
+      HttpServletResponse response) throws ServletException, IOException {
+    try {
+      // normalize the request
+      OAuth2NormalizedRequest normalizedReq = new OAuth2NormalizedRequest(request);
+
+      // grant access token
+      service.authenticateClient(normalizedReq);
+      service.validateRequestForAccessToken(normalizedReq);
+      OAuth2Code accessToken = service.grantAccessToken(normalizedReq);
+
+      // send response
+      OAuth2NormalizedResponse normalizedResp = new OAuth2NormalizedResponse();
+      normalizedResp.setAccessToken(accessToken.getValue());
+      normalizedResp.setTokenType(TokenFormat.BEARER.toString());
+      normalizedResp.setExpiresIn((accessToken.getExpiration() - System.currentTimeMillis() + ""));
+      normalizedResp.setScope(listToString(accessToken.getScope()));
+      normalizedResp.setStatus(HttpServletResponse.SC_OK);
+      normalizedResp.setBodyReturned(true);
+      if (normalizedReq.getState() != null) normalizedResp.setState(normalizedReq.getState());
+      return normalizedResp;
+    } catch (OAuth2Exception oae) {
+      return oae.getNormalizedResponse();
+    }
+  }
+
+  /**
+   * Private utility to comma-delimit a list of Strings
+   */
+  private static String listToString(List<String> list) {
+    if (list == null || list.isEmpty())
+      return "";
+    StringBuilder sb = new StringBuilder();
+    for (String item : list) {
+      sb.append(item);
+      sb.append(',');
+    }
+    sb.deleteCharAt(sb.length());
+    return sb.toString();
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Types.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Types.java
new file mode 100644
index 0000000..7909fb6
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Types.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+/**
+ * A collection of OAuth 2.0's enumerated types.
+ */
+public class OAuth2Types {
+
+  /**
+   * Enumerated error types in the OAuth 2.0 specification.
+   */
+  public static enum ErrorType {
+    INVALID_REQUEST("invalid_request"),
+    INVALID_CLIENT("invalid_client"),
+    INVALID_GRANT("invalid_grant"),
+    UNAUTHORIZED_CLIENT("unauthorized_client"),
+    UNSUPPORTED_GRANT_TYPE("unsupported_grant_type"),
+    INVALID_SCOPE("invalid_scope"), ACCESS_DENIED("access_denied"),
+    UNSUPPORTED_RESPONSE_TYPE("unsupported_response_type"),
+    SERVER_ERROR("server_error"),
+    TEMPORARILY_UNAVAILABLE("temporarily_unavailable");
+
+    private final String name;
+
+    private ErrorType(String name) {
+      this.name = name;
+    }
+
+    public String toString() {
+      return name;
+    }
+  }
+
+  /**
+   * Enumerated grant types in the OAuth 2.0 specification.
+   */
+  public static enum GrantType {
+    REFRESH_TOKEN("refresh_token"),
+    AUTHORIZATION_CODE("authorization_code"),
+    PASSWORD("password"),
+    CLIENT_CREDENTIALS("client_credentials"),
+    CUSTOM("custom");
+
+    private final String name;
+
+    private GrantType(String name) {
+      this.name = name;
+    }
+
+    public String toString() {
+      return name;
+    }
+  }
+
+  /**
+   * Enumerated response types in the OAuth 2.0 specification.
+   */
+  public static enum ResponseType {
+    CODE("code"), TOKEN("token");
+
+    private final String name;
+
+    private ResponseType(String name) {
+      this.name = name;
+    }
+
+    public String toString() {
+      return name;
+    }
+  }
+
+  /**
+   * Enumerated token types in the OAuth 2.0 specification.
+   */
+  public static enum CodeType {
+    AUTHORIZATION_CODE("authorization_code"),
+    ACCESS_TOKEN("access_token"),
+    REFRESH_TOKEN("refresh_token");
+
+    private final String name;
+
+    private CodeType(String name) {
+      this.name = name;
+    }
+
+    public String toString() {
+      return name;
+    }
+  }
+
+  /**
+   * Enumerated token types in the OAuth 2.0 specification.
+   */
+  public static enum TokenFormat {
+    BEARER("bearer"),
+    MAC("mac");
+
+    private final String name;
+
+    private TokenFormat(String name) {
+      this.name = name;
+    }
+
+    public String toString() {
+      return name;
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Utils.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Utils.java
new file mode 100644
index 0000000..c923342
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/OAuth2Utils.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2;
+
+import java.util.Map;
+
+import org.apache.shindig.common.uri.UriBuilder;
+
+/**
+ * Collection of utility classes to support OAuth 2.0 operations.
+ */
+public class OAuth2Utils {
+
+  /**
+   * Converts a Map<String, String> to a URL query string.
+   *
+   * @param params represents the Map of query parameters
+   *
+   * @return String is the URL encoded parameter String
+   */
+  public static String convertQueryString(Map<String, String> params) {
+    if (params == null) return "";
+    UriBuilder builder = new UriBuilder();
+    builder.addQueryParameters(params);
+    return builder.getQuery();
+  }
+
+  /**
+   * Normalizes a URL and parameters. If the URL already contains parameters,
+   * new parameters will be added properly.
+   *
+   * @param URL is the base URL to normalize
+   * @param queryParams query parameters to add to the URL
+   * @param fragmentParams fragment params to add to the URL
+   */
+  public static String buildUrl(String url, Map<String, String> queryParams,
+      Map<String, String> fragmentParams) {
+    UriBuilder builder = new UriBuilder();
+    builder.setPath(url);
+    if (queryParams != null) builder.addQueryParameters(queryParams);
+    if (fragmentParams != null) builder.addFragmentParameters(fragmentParams);
+    return builder.toString();
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AccessTokenRequestValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AccessTokenRequestValidator.java
new file mode 100644
index 0000000..e1ff3f7
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AccessTokenRequestValidator.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Client;
+import org.apache.shindig.social.core.oauth2.OAuth2DataService;
+import org.apache.shindig.social.core.oauth2.OAuth2Exception;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedRequest;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedResponse;
+import org.apache.shindig.social.core.oauth2.OAuth2Client.Flow;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+
+import javax.servlet.http.HttpServletResponse;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.inject.Inject;
+
+public class AccessTokenRequestValidator implements OAuth2RequestValidator {
+
+  private OAuth2DataService store = null;
+  private List<OAuth2GrantValidator> grantValidators; // grant validators
+
+  @Inject
+  public AccessTokenRequestValidator(OAuth2DataService store) {
+    this.grantValidators = new ArrayList<OAuth2GrantValidator>();
+    grantValidators.add(new AuthCodeGrantValidator(store));
+    grantValidators.add(new ClientCredentialsGrantValidator(store));
+    this.store = store;
+  }
+
+  public void validateRequest(OAuth2NormalizedRequest req)
+      throws OAuth2Exception {
+    if (req.getGrantType() != null) {
+      for (OAuth2GrantValidator validator : grantValidators) {
+        if (validator.getGrantType().equals(req.getGrantType())) {
+          validator.validateRequest(req);
+          return; // request validated
+        }
+      }
+      OAuth2NormalizedResponse response = new OAuth2NormalizedResponse();
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      response.setError(ErrorType.UNSUPPORTED_GRANT_TYPE.toString());
+      response.setErrorDescription("Unsupported grant type");
+      response.setBodyReturned(true);
+      throw new OAuth2Exception(response);
+    } else { // implicit flow does not include grant type
+      if (req.getResponseType() == null
+          || !req.getResponseType().equals("token")) {
+        OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+        resp.setError(ErrorType.UNSUPPORTED_RESPONSE_TYPE.toString());
+        resp.setErrorDescription("Unsupported response type");
+        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        throw new OAuth2Exception(resp);
+      }
+      OAuth2Client client = store.getClient(req.getClientId());
+      if (client == null || client.getFlow() != Flow.IMPLICIT) {
+        OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+        resp.setError(ErrorType.INVALID_CLIENT.toString());
+        resp.setErrorDescription(req.getClientId()
+            + " is not a registered implicit client");
+        resp.setBodyReturned(true);
+        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        throw new OAuth2Exception(resp);
+      }
+      if (req.getRedirectURI() == null && client.getRedirectURI() == null) {
+        OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+        resp.setError(ErrorType.INVALID_REQUEST.toString());
+        resp.setErrorDescription("No redirect_uri registered or received in request");
+        resp.setBodyReturned(true);
+        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        throw new OAuth2Exception(resp);
+      }
+      if (req.getRedirectURI() != null
+          && !req.getRedirectURI().equals(client.getRedirectURI())) {
+        OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+        resp.setError(ErrorType.INVALID_REQUEST.toString());
+        resp.setErrorDescription("Redirect URI does not match the one registered for this client");
+        resp.setBodyReturned(true);
+        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        throw new OAuth2Exception(resp);
+      }
+      return; // request validated
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AuthCodeGrantValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AuthCodeGrantValidator.java
new file mode 100644
index 0000000..5137c2b
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AuthCodeGrantValidator.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Client;
+import org.apache.shindig.social.core.oauth2.OAuth2Code;
+import org.apache.shindig.social.core.oauth2.OAuth2DataService;
+import org.apache.shindig.social.core.oauth2.OAuth2Exception;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedRequest;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedResponse;
+import org.apache.shindig.social.core.oauth2.OAuth2Client.Flow;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+
+import com.google.inject.Inject;
+
+public class AuthCodeGrantValidator implements OAuth2GrantValidator {
+
+  private OAuth2DataService service;
+
+  @Inject
+  public AuthCodeGrantValidator(OAuth2DataService service) {
+    this.service = service;
+  }
+
+  public String getGrantType() {
+    return "authorization_code";
+  }
+
+  public void validateRequest(OAuth2NormalizedRequest servletRequest)
+      throws OAuth2Exception {
+    OAuth2Client client = service.getClient(servletRequest.getClientId());
+    if (client == null || client.getFlow() != Flow.AUTHORIZATION_CODE) {
+      OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+      resp.setError(ErrorType.INVALID_CLIENT.toString());
+      resp.setErrorDescription("Invalid client");
+      resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+      throw new OAuth2Exception(resp);
+    }
+    OAuth2Code authCode = service.getAuthorizationCode(
+        servletRequest.getClientId(), servletRequest.getAuthorizationCode());
+    if (authCode == null) {
+      OAuth2NormalizedResponse response = new OAuth2NormalizedResponse();
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      response.setError(ErrorType.INVALID_GRANT.toString());
+      response.setErrorDescription("Bad authorization code");
+      response.setBodyReturned(true);
+      throw new OAuth2Exception(response);
+    }
+    if (servletRequest.getRedirectURI() != null
+        && !servletRequest.getRedirectURI().equals(authCode.getRedirectURI())) {
+      OAuth2NormalizedResponse response = new OAuth2NormalizedResponse();
+      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+      response.setError(ErrorType.INVALID_GRANT.toString());
+      response
+          .setErrorDescription("The redirect URI does not match the one used in the authorization request");
+      response.setBodyReturned(true);
+      throw new OAuth2Exception(response);
+    }
+
+    // ensure authorization code has not already been used
+    if (authCode.getRelatedAccessToken() != null) {
+      service.unregisterAccessToken(client.getId(), authCode
+          .getRelatedAccessToken().getValue());
+      OAuth2NormalizedResponse response = new OAuth2NormalizedResponse();
+      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+      response.setError(ErrorType.INVALID_GRANT.toString());
+      response
+          .setErrorDescription("The authorization code has already been used to generate an access token");
+      response.setBodyReturned(true);
+      throw new OAuth2Exception(response);
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AuthorizationCodeRequestValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AuthorizationCodeRequestValidator.java
new file mode 100644
index 0000000..74e8501
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/AuthorizationCodeRequestValidator.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Client;
+import org.apache.shindig.social.core.oauth2.OAuth2DataService;
+import org.apache.shindig.social.core.oauth2.OAuth2Exception;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedRequest;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedResponse;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.inject.Inject;
+
+public class AuthorizationCodeRequestValidator implements
+    OAuth2RequestValidator {
+
+  private OAuth2DataService store = null;
+
+  @Inject
+  public AuthorizationCodeRequestValidator(OAuth2DataService store) {
+    this.store = store;
+  }
+
+  public void validateRequest(OAuth2NormalizedRequest req)
+      throws OAuth2Exception {
+
+    OAuth2Client client = store.getClient(req.getClientId());
+    if (client == null) {
+      OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+      resp.setError(ErrorType.INVALID_REQUEST.toString());
+      resp.setErrorDescription("The client is invalid or not registered");
+      resp.setBodyReturned(true);
+      resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+      throw new OAuth2Exception(resp);
+    }
+    String storedURI = client.getRedirectURI();
+    if (storedURI == null && req.getRedirectURI() == null) {
+      OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+      resp.setError(ErrorType.INVALID_REQUEST.toString());
+      resp.setErrorDescription("No redirect_uri registered or received in request");
+      resp.setBodyReturned(true);
+      resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+      throw new OAuth2Exception(resp);
+    }
+    if (req.getRedirectURI() != null && storedURI != null) {
+      if (!req.getRedirectURI().equals(storedURI)) {
+        OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+        resp.setError(ErrorType.INVALID_REQUEST.toString());
+        resp.setErrorDescription("Redirect URI does not match the one registered for this client");
+        resp.setBodyReturned(true);
+        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        throw new OAuth2Exception(resp);
+      }
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/ClientCredentialsGrantValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/ClientCredentialsGrantValidator.java
new file mode 100644
index 0000000..e0a29ed
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/ClientCredentialsGrantValidator.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Client;
+import org.apache.shindig.social.core.oauth2.OAuth2DataService;
+import org.apache.shindig.social.core.oauth2.OAuth2Exception;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedRequest;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedResponse;
+import org.apache.shindig.social.core.oauth2.OAuth2Client.ClientType;
+import org.apache.shindig.social.core.oauth2.OAuth2Client.Flow;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+
+import com.google.inject.Inject;
+
+public class ClientCredentialsGrantValidator implements OAuth2GrantValidator {
+
+  private OAuth2DataService service;
+
+  @Inject
+  public ClientCredentialsGrantValidator(OAuth2DataService service) {
+    this.service = service;
+  }
+
+  public void setOAuth2DataService(OAuth2DataService service) {
+    this.service = service;
+  }
+
+  public String getGrantType() {
+    return "client_credentials";
+  }
+
+  public void validateRequest(OAuth2NormalizedRequest req)
+      throws OAuth2Exception {
+    OAuth2Client cl = service.getClient(req.getClientId());
+    if (cl == null || cl.getFlow() != Flow.CLIENT_CREDENTIALS) {
+      throwAccessDenied("Bad client id or password");
+    }
+    if (cl.getType() != ClientType.CONFIDENTIAL) {
+      throwAccessDenied("Client credentials flow does not support public clients");
+    }
+    if (!cl.getSecret().equals(req.getClientSecret())) {
+      throwAccessDenied("Bad client id or password");
+    }
+  }
+
+  private void throwAccessDenied(String msg) throws OAuth2Exception {
+    OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+    resp.setError(ErrorType.ACCESS_DENIED.toString());
+    resp.setErrorDescription(msg);
+    resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+    throw new OAuth2Exception(resp);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/DefaultResourceRequestValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/DefaultResourceRequestValidator.java
new file mode 100644
index 0000000..ceab14e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/DefaultResourceRequestValidator.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Code;
+import org.apache.shindig.social.core.oauth2.OAuth2DataService;
+import org.apache.shindig.social.core.oauth2.OAuth2Exception;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedRequest;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedResponse;
+import org.apache.shindig.social.core.oauth2.OAuth2Types.ErrorType;
+
+import com.google.inject.Inject;
+
+public class DefaultResourceRequestValidator implements
+    OAuth2ProtectedResourceValidator {
+
+  private OAuth2DataService store = null;
+
+  @Inject
+  public DefaultResourceRequestValidator(OAuth2DataService store) {
+    this.store = store;
+  }
+
+  public void validateRequest(OAuth2NormalizedRequest req)
+      throws OAuth2Exception {
+    validateRequestForResource(req, null);
+
+  }
+
+  /**
+   * TODO (Matt): implement scope handling.
+   */
+  public void validateRequestForResource(OAuth2NormalizedRequest req,
+      Object resourceRequest) throws OAuth2Exception {
+
+    OAuth2Code token = store.getAccessToken(req.getAccessToken());
+    if (token == null)
+      throwAccessDenied("Access token is invalid.");
+    if (token.getExpiration() > -1
+        && token.getExpiration() < System.currentTimeMillis()) {
+      throwAccessDenied("Access token has expired.");
+    }
+    if (resourceRequest != null) {
+      // TODO (Matt): validate that requested resource is within scope
+    }
+  }
+
+  // TODO(plindner): change this into a constructor or .create() on OAuth2Exception
+  private void throwAccessDenied(String msg) throws OAuth2Exception {
+    OAuth2NormalizedResponse resp = new OAuth2NormalizedResponse();
+    resp.setError(ErrorType.ACCESS_DENIED.toString());
+    resp.setErrorDescription(msg);
+    resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+    throw new OAuth2Exception(resp);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2GrantValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2GrantValidator.java
new file mode 100644
index 0000000..2e7cfe3
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2GrantValidator.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+/**
+ * Handles the validation of a grant requests for access tokens.
+ */
+public interface OAuth2GrantValidator extends OAuth2RequestValidator{
+
+  /**
+   * Indicates the grant type this handler is registered to handle.
+   */
+  public String getGrantType();
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2ProtectedResourceValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2ProtectedResourceValidator.java
new file mode 100644
index 0000000..6bbcf07
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2ProtectedResourceValidator.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Exception;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedRequest;
+
+/**
+ * Validator interface for a protected resource.
+ */
+public interface OAuth2ProtectedResourceValidator extends OAuth2RequestValidator {
+
+  /**
+   * Validates a request for a protected resource.
+   *
+   * @param req is the normalized OAuth 2.0 request
+   * @param resourceRequest identifies the resource being requested
+   *
+   * @throws OAuth2Exception
+   */
+  public void validateRequestForResource(OAuth2NormalizedRequest req,
+      Object resourceRequest) throws OAuth2Exception;
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2RequestValidator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2RequestValidator.java
new file mode 100644
index 0000000..c760234
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/oauth2/validators/OAuth2RequestValidator.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth2.validators;
+
+import org.apache.shindig.social.core.oauth2.OAuth2Exception;
+import org.apache.shindig.social.core.oauth2.OAuth2NormalizedRequest;
+
+/**
+ * Validator interface for OAuth 2.0 requests.
+ */
+public interface OAuth2RequestValidator {
+
+  /**
+   * Validates an OAuth 2.0 request.
+   *
+   * @param req is the normalized OAuth 2.0 request to validate
+   *
+   * @throws OAuth2Exception if the request failed to validate
+   */
+  public void validateRequest(OAuth2NormalizedRequest req) throws OAuth2Exception;
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/BeanXStreamAtomConverter.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/BeanXStreamAtomConverter.java
new file mode 100644
index 0000000..b5cf40a
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/BeanXStreamAtomConverter.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util;
+
+import com.google.inject.Inject;
+
+import org.apache.shindig.protocol.ContentTypes;
+import org.apache.shindig.protocol.conversion.BeanXStreamConverter;
+import org.apache.shindig.protocol.conversion.xstream.XStreamConfiguration;
+import org.apache.shindig.social.core.util.atom.AtomFeed;
+
+/**
+ * Converts output to atom.
+ * TODO: Move to common once atom binding can be decoupled form social code
+ */
+public class BeanXStreamAtomConverter extends BeanXStreamConverter {
+
+  /**
+   * @param configuration
+   */
+  @Inject
+  public BeanXStreamAtomConverter(XStreamConfiguration configuration) {
+    super(configuration);
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see org.apache.shindig.protocol.conversion.BeanXStreamConverter#getContentType()
+   */
+  @Override
+  public String getContentType() {
+    return ContentTypes.OUTPUT_ATOM_CONTENT_TYPE;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see org.apache.shindig.protocol.conversion.BeanXStreamConverter#convertToString(java.lang.Object)
+   */
+  @Override
+  public String convertToString(Object obj) {
+    writerStack.reset();
+    AtomFeed af = new AtomFeed(obj);
+    XStreamConfiguration.ConverterConfig cc = converterMap.get(XStreamConfiguration.ConverterSet.DEFAULT);
+    cc.mapper.setBaseObject(af); // thread safe method
+
+    return cc.xstream.toXML(af);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAttribute.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAttribute.java
new file mode 100644
index 0000000..d10314d
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAttribute.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+/**
+ * Represents an attribute in atom, ties into attribute converters.
+ */
+public class AtomAttribute {
+
+  private String value;
+
+  /**
+   * @param value
+   */
+  public AtomAttribute(String value) {
+    this.value = value;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see java.lang.Object#toString()
+   */
+  @Override
+  public String toString() {
+    return value;
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAttributeConverter.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAttributeConverter.java
new file mode 100644
index 0000000..4bfb54c
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAttributeConverter.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import com.thoughtworks.xstream.converters.SingleValueConverter;
+import com.google.common.base.Preconditions;
+
+/**
+ * Serializes attributes correctly.
+ */
+public class AtomAttributeConverter implements SingleValueConverter {
+
+  /**
+   * {@inheritDoc}
+   * @see com.thoughtworks.xstream.converters.SingleValueConverter#fromString(java.lang.String)
+   */
+  public Object fromString(String value) {
+    return new AtomAttribute(Preconditions.checkNotNull(value));
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see com.thoughtworks.xstream.converters.SingleValueConverter#toString(java.lang.Object)
+   */
+  public String toString(Object object) {
+    return object.toString();
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class)
+   */
+  @SuppressWarnings("unchecked")
+  public boolean canConvert(Class clazz) {
+    return AtomAttribute.class.equals(clazz);
+  }
+
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAuthor.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAuthor.java
new file mode 100644
index 0000000..6825697
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomAuthor.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+
+/**
+ * Represents and atom:entry/atom:author element.
+ */
+public class AtomAuthor {
+
+  @SuppressWarnings("unused")
+  private String uri;
+  @SuppressWarnings("unused")
+  private String name;
+
+  /**
+   * Default constructor for POSTs to the REST API.
+   */
+  public AtomAuthor() {
+  }
+
+  /**
+   * @param activity
+   */
+  public AtomAuthor(Activity activity) {
+    uri = activity.getUserId();
+  }
+
+  /**
+   * @param activityEntry
+   */
+  public AtomAuthor(ActivityEntry activityEntry) {
+    uri = activityEntry.getActor().getUrl();
+    name = activityEntry.getActor().getDisplayName();
+    if (name == null) {
+      name = activityEntry.getActor().getId();
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomContent.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomContent.java
new file mode 100644
index 0000000..84101a5
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomContent.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.model.Person;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Represents and atom:content element.
+ */
+public class AtomContent {
+
+  @SuppressWarnings("unused")
+  private Person person;
+  @SuppressWarnings("unused")
+  private Activity activity;
+  @SuppressWarnings("unused")
+  private ActivityEntry activityEntry;
+  @SuppressWarnings("unused")
+  private AtomAttribute type = new AtomAttribute("application/xml");
+  @SuppressWarnings("unused")
+  private Object entry;
+  @SuppressWarnings("unused")
+  private Object value;
+
+  /**
+   * @param person
+   */
+  public AtomContent(Person person) {
+    this.person = person;
+  }
+
+  /**
+   * @param activity
+   */
+  public AtomContent(Activity activity) {
+    this.activity = activity;
+  }
+
+  /**
+   * @param activityEntry
+   */
+  public AtomContent(ActivityEntry activityEntry) {
+    this.activityEntry = activityEntry;
+  }
+
+  /**
+   * @param value
+   */
+  public AtomContent(Object value) {
+    if (value instanceof Map<?, ?>) {
+      Map<?, ?> entries = (Map<?, ?>) value;
+      List<AtomKeyValue> keyValues = Lists.newArrayList();
+      for ( Entry<?, ?> e : entries.entrySet() ) {
+        keyValues.add(new AtomKeyValue(e));
+      }
+      entry = keyValues;
+    } else {
+      this.value = value;
+    }
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomEntry.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomEntry.java
new file mode 100644
index 0000000..c6a21ab
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomEntry.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Map.Entry;
+
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.model.Person;
+
+/**
+ * This bean represents a Atom Entry for serialization. It contains, optionally
+ * a person or an activity, from which are extracted the key atom fields.
+ */
+public class AtomEntry {
+
+  @SuppressWarnings("unused")
+  private String id;
+  @SuppressWarnings("unused")
+  private String title;
+  @SuppressWarnings("unused")
+  private AtomSummary summary;
+  @SuppressWarnings("unused")
+  private String icon;
+  @SuppressWarnings("unused")
+  private AtomSource source;
+  @SuppressWarnings("unused")
+  private AtomGenerator generator;
+  @SuppressWarnings("unused")
+  private AtomAuthor author;
+  @SuppressWarnings("unused")
+  private Date updated;
+  @SuppressWarnings("unused")
+  private AtomLink link;
+  @SuppressWarnings("unused")
+  private Object content;
+
+  /**
+   * Default constructor for POSTs to the REST API.
+   */
+  public AtomEntry() {
+  }
+
+  /**
+   * @param o
+   */
+  public AtomEntry(Object o) {
+    Object oCopy = o;
+    if(o instanceof Entry) {
+      // Try to recognize Entry's value
+      o = ((Entry<?, ?>)o).getValue();
+    }
+
+    if (o instanceof Person) {
+      Person person = (Person) o;
+      content = new AtomContent(person);
+      id = "urn:guid:" + person.getId();
+      updated = person.getUpdated();
+    } else if (o instanceof Activity) {
+      Activity activity = (Activity) o;
+      content = new AtomContent(activity);
+      title = activity.getTitle();
+      summary = new AtomSummary(activity.getBody());
+      link = new AtomLink("self", activity.getUrl());
+      icon = activity.getStreamFaviconUrl();
+      source = new AtomSource(activity);
+      generator = new AtomGenerator(activity);
+      author = new AtomAuthor(activity);
+      updated = activity.getUpdated();
+    } else if (o instanceof ActivityEntry) {
+      ActivityEntry activity = (ActivityEntry)o;
+      id = activity.getId();
+      title = activity.getTitle();
+      summary = new AtomSummary(activity.getObject().getSummary());
+      link = new AtomLink("alternate", activity.getObject().getUrl());
+      author = new AtomAuthor(activity);
+      content = new AtomContent(activity);
+      try {
+        updated = new SimpleDateFormat().parse(activity.getPublished());
+      } catch (ParseException e) {
+        // TODO: map published to updated field correctly
+      }
+    } else if (oCopy instanceof Entry) {
+      Entry<?, ?> e = (Entry<?, ?>) oCopy;
+      id = (String) e.getKey();
+      content = new AtomContent(e.getValue());
+    } else {
+      content = o;
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomFeed.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomFeed.java
new file mode 100644
index 0000000..d4b6a1a
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomFeed.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+
+import org.apache.shindig.protocol.DataCollection;
+import org.apache.shindig.protocol.RestfulCollection;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * represents an atom:feed entry
+ */
+public class AtomFeed {
+
+  private Collection<AtomEntry> entry;
+  @SuppressWarnings("unused")
+  private int startIndex;
+  @SuppressWarnings("unused")
+  private int totalResults;
+  @SuppressWarnings("unused")
+  private int itemsPerPage;
+  @SuppressWarnings("unused")
+  private String author;
+  @SuppressWarnings("unused")
+  private String title;
+  @SuppressWarnings("unused")
+  private String updated;
+  @SuppressWarnings("unused")
+  private String id;
+  @SuppressWarnings("unused")
+  private AtomLink link;
+
+  public static final String AUTHOR = "author";
+  public static final String TITLE = "title";
+  public static final String UPDATED = "updated";
+  public static final String ID = "id";
+  public static final String URL = "url";
+
+  /**
+   * @param obj
+   */
+  @SuppressWarnings("unchecked")
+  public AtomFeed(Object obj) {
+    Preconditions.checkNotNull(obj);
+    if (obj instanceof RestfulCollection<?>) {
+      RestfulCollection<?> r = (RestfulCollection<?>) obj;
+      entry = Lists.newArrayList();
+      List<?> entryList = r.getList();
+      for (Object o : entryList) {
+        entry.add(new AtomEntry(o));
+      }
+      startIndex = r.getStartIndex();
+      totalResults = r.getTotalResults();
+      itemsPerPage = r.getItemsPerPage();
+      author = (r.get(AUTHOR)==null) ? "?" : r.get(AUTHOR).toString();
+      title = (r.get(TITLE)==null) ? "?" : r.get(TITLE).toString();
+      id = (r.get(ID)==null) ? "?" : r.get(ID).toString();
+      updated = (r.get(UPDATED)==null) ? "" : r.get(UPDATED).toString();
+
+      if (r.get(URL)!=null) {
+        link = new AtomLink("self", r.get(URL).toString());
+      }
+
+    } else if (obj instanceof Map) {
+      Map<?, ?> m = (Map<?, ?>) obj;
+      entry = Lists.newArrayList();
+      for ( Entry<?, ?> o : m.entrySet()) {
+        entry.add(new AtomEntry(o));
+      }
+      startIndex = 0;
+      totalResults = entry.size();
+      itemsPerPage = entry.size();
+    } else if ( obj instanceof DataCollection ) {
+      DataCollection dc = (DataCollection) obj;
+      entry = Lists.newArrayList();
+      for ( Entry<String, Map<String,String>> o : dc.getEntry().entrySet()) {
+        entry.add(new AtomEntry(o));
+      }
+      startIndex = 0;
+      totalResults = entry.size();
+      itemsPerPage = entry.size();
+    } else {
+      entry = ImmutableList.of(new AtomEntry(obj));
+      startIndex = 0;
+      totalResults = 1;
+      itemsPerPage = 1;
+    }
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomGenerator.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomGenerator.java
new file mode 100644
index 0000000..078b5ee
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomGenerator.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import org.apache.shindig.social.opensocial.model.Activity;
+
+/**
+ * this represents atom:entry/atom:generator configured for the Activity
+ * representation.
+ */
+public class AtomGenerator {
+
+  @SuppressWarnings("unused")
+  private String uri;
+
+  /**
+   * @param activity
+   */
+  public AtomGenerator(Activity activity) {
+    uri = activity.getAppId();
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomKeyValue.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomKeyValue.java
new file mode 100644
index 0000000..b359a18
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomKeyValue.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import java.util.Map.Entry;
+
+/**
+ * represents a key/value pair in a map serialization for atom.
+ */
+public class AtomKeyValue {
+
+  @SuppressWarnings("unused")
+  private Object key;
+  @SuppressWarnings("unused")
+  private Object value;
+
+  /**
+   * @param e
+   */
+  public AtomKeyValue(Entry<?, ?> e) {
+    key = e.getKey();
+    value = e.getValue();
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomLink.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomLink.java
new file mode 100644
index 0000000..08f320d
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomLink.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+/**
+ * represents an atom:link element.
+ */
+public class AtomLink {
+
+  private String href;
+  private String rel;
+  private String type;
+  private String title;
+
+  /**
+   * Construct a new AtomLink
+   * @param rel a value for the rel attribute
+   * @param href a value for the href attribute
+   */
+  public AtomLink(String rel, String href) {
+    this.rel = rel;
+    this.href = href;
+  }
+
+  /**
+   * @return the link href
+   */
+  public String getHref() {
+    return href;
+  }
+
+  /**
+   * @return the rel
+   */
+  public String getRel() {
+    return rel;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomLinkConverter.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomLinkConverter.java
new file mode 100644
index 0000000..eb4c7ba
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomLinkConverter.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.google.common.base.Preconditions;
+
+/**
+ * Serializes links for atom, taking account of attributes.
+ */
+public class AtomLinkConverter implements Converter {
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object,
+   *      com.thoughtworks.xstream.io.HierarchicalStreamWriter,
+   *      com.thoughtworks.xstream.converters.MarshallingContext)
+   */
+  public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {
+    AtomLink link = (AtomLink) object;
+    if (link.getRel() != null) {
+      writer.addAttribute("rel", link.getRel());
+    }
+    if (link.getHref() != null) {
+      writer.setValue(link.getHref());
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader,
+   *      com.thoughtworks.xstream.converters.UnmarshallingContext)
+   */
+  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+    Preconditions.checkNotNull(reader);
+
+    reader.moveDown();
+    AtomLink al = new AtomLink(reader.getAttribute("rel"), reader.getValue());
+    reader.moveUp();
+    return al;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class)
+   */
+  // Base API is inherently unchecked
+  @SuppressWarnings("unchecked")
+  public boolean canConvert(Class clazz) {
+    return AtomLink.class.equals(clazz);
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSource.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSource.java
new file mode 100644
index 0000000..8229c00
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSource.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import org.apache.shindig.social.opensocial.model.Activity;
+import com.google.common.base.Objects;
+
+/**
+ * This represents atom:entry/atom:source for the Activity object.
+ */
+public class AtomSource {
+
+  @SuppressWarnings("unused")
+  private String title;
+  @SuppressWarnings("unused")
+  private AtomLink link;
+
+  /**
+   * @param activity
+   */
+  public AtomSource(Activity activity) {
+    title = activity.getStreamTitle();
+    link = new AtomLink("self", Objects.firstNonNull(activity.getStreamUrl(), "urn:bogus"));
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSummary.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSummary.java
new file mode 100644
index 0000000..0cc0716
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSummary.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+
+/**
+ * Represents an atom:summary element, and exists only so we can add a type
+ */
+public class AtomSummary {
+
+  @SuppressWarnings("unused")
+  private String type = "html";
+  @SuppressWarnings("unused")
+  private String value;
+
+  /**
+   * @param string value
+   */
+  public AtomSummary(String value) {
+    this.value=value;
+  }
+
+  public String getType() {
+    return type;
+  }
+
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSummaryConverter.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSummaryConverter.java
new file mode 100644
index 0000000..3d44d19
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/atom/AtomSummaryConverter.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.atom;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.google.common.base.Preconditions;
+
+/**
+ * Serializes summary for atom, taking account of attributes.
+ */
+public class AtomSummaryConverter implements Converter {
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object,
+   *      com.thoughtworks.xstream.io.HierarchicalStreamWriter,
+   *      com.thoughtworks.xstream.converters.MarshallingContext)
+   */
+  public void marshal(Object object, HierarchicalStreamWriter writer, MarshallingContext context) {
+    AtomSummary link = (AtomSummary) object;
+    if (link.getType() != null) {
+      writer.addAttribute("type", link.getType());
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader,
+   *      com.thoughtworks.xstream.converters.UnmarshallingContext)
+   */
+  public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+    Preconditions.checkNotNull(reader);
+
+    reader.moveDown();
+    AtomSummary al = new AtomSummary(reader.getValue());
+    reader.moveUp();
+    return al;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class)
+   */
+  // Base API is inherently unchecked
+  @SuppressWarnings("unchecked")
+  public boolean canConvert(Class clazz) {
+    return AtomSummary.class.equals(clazz);
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/package-info.java
new file mode 100644
index 0000000..c814e99
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>Core Utilities package</h1>
+<p>Utilities used by core packages.</p>
+*/
+package org.apache.shindig.social.core.util;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/xstream/XStream081Configuration.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/xstream/XStream081Configuration.java
new file mode 100644
index 0000000..067fa24
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/xstream/XStream081Configuration.java
@@ -0,0 +1,415 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.util.xstream;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.ConverterLookup;
+import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
+import com.thoughtworks.xstream.core.DefaultConverterLookup;
+import org.apache.shindig.protocol.DataCollection;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.conversion.xstream.ClassFieldMapping;
+import org.apache.shindig.protocol.conversion.xstream.DataCollectionConverter;
+import org.apache.shindig.protocol.conversion.xstream.ExtendableBeanConverter;
+import org.apache.shindig.protocol.conversion.xstream.GuiceBeanConverter;
+import org.apache.shindig.protocol.conversion.xstream.ImplicitCollectionFieldMapping;
+import org.apache.shindig.protocol.conversion.xstream.InterfaceClassMapper;
+import org.apache.shindig.protocol.conversion.xstream.InterfaceFieldAliasMapping;
+import org.apache.shindig.protocol.conversion.xstream.InterfaceFieldAliasingMapper;
+import org.apache.shindig.protocol.conversion.xstream.MapConverter;
+import org.apache.shindig.protocol.conversion.xstream.NamespaceSet;
+import org.apache.shindig.protocol.conversion.xstream.RestfullCollectionConverter;
+import org.apache.shindig.protocol.conversion.xstream.WriterStack;
+import org.apache.shindig.protocol.conversion.xstream.XStreamConfiguration;
+import org.apache.shindig.protocol.model.EnumImpl;
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.apache.shindig.social.core.util.atom.AtomAttribute;
+import org.apache.shindig.social.core.util.atom.AtomAttributeConverter;
+import org.apache.shindig.social.core.util.atom.AtomContent;
+import org.apache.shindig.social.core.util.atom.AtomEntry;
+import org.apache.shindig.social.core.util.atom.AtomFeed;
+import org.apache.shindig.social.core.util.atom.AtomKeyValue;
+import org.apache.shindig.social.core.util.atom.AtomLinkConverter;
+import org.apache.shindig.social.core.util.atom.AtomSummaryConverter;
+import org.apache.shindig.social.opensocial.model.Account;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.model.ActivityObject;
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.BodyType;
+import org.apache.shindig.social.opensocial.model.ListField;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+import org.apache.shindig.social.opensocial.model.MediaLink;
+import org.apache.shindig.social.opensocial.model.Message;
+import org.apache.shindig.social.opensocial.model.MessageCollection;
+import org.apache.shindig.social.opensocial.model.Name;
+import org.apache.shindig.social.opensocial.model.Organization;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.model.Url;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ForwardingMap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.thoughtworks.xstream.XStream;
+import com.thoughtworks.xstream.converters.extended.ISO8601DateConverter;
+import com.thoughtworks.xstream.converters.extended.ISO8601GregorianCalendarConverter;
+import com.thoughtworks.xstream.converters.extended.ISO8601SqlTimestampConverter;
+import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
+import com.thoughtworks.xstream.io.HierarchicalStreamDriver;
+import com.thoughtworks.xstream.mapper.AttributeMapper;
+import com.thoughtworks.xstream.mapper.Mapper;
+
+/**
+ * Opensocial 0.81 compliant Xstream binding
+ */
+public class XStream081Configuration implements XStreamConfiguration {
+
+  /**
+   * Defines the type of the list container when at the top level where there
+   * are no methods to specify the name of the list.
+   */
+  private static final Map<ConverterSet, List<ClassFieldMapping>> listElementMappingList =
+      DefaultedEnumMap.init(ConverterSet.class,  ConverterSet.DEFAULT);
+
+  /**
+   * Specifies a priority sorted list of Class to Element Name mappings.
+   */
+  private static final Map<ConverterSet, List<ClassFieldMapping>> elementMappingList =
+      DefaultedEnumMap.init(ConverterSet.class, ConverterSet.DEFAULT);
+
+  /**
+   * A list of omits, the potential field is the key, and if the class which the
+   * field is in, is also in the list, the field is supressed.
+   */
+  private static final Map<ConverterSet, ImmutableMultimap<String, Class<?>>> omitMap = ImmutableMap.of(ConverterSet.DEFAULT,
+      ImmutableMultimap.<String,Class<?>>builder()
+          .put("isOwner", Person.class)
+          .put("isViewer", Person.class).build());
+
+  /**
+   * Maps elements names to classes.
+   */
+  private static final Map<ConverterSet, Map<String, Class<?>>> elementClassMap = DefaultedEnumMap.init(ConverterSet.class,  ConverterSet.DEFAULT);
+  private static final Map<ConverterSet, List<ImplicitCollectionFieldMapping>> itemFieldMappings = DefaultedEnumMap.init(ConverterSet.class, ConverterSet.DEFAULT);
+  private static final Map<ConverterSet, List<InterfaceFieldAliasMapping>> fieldAliasMappingList = DefaultedEnumMap.init(ConverterSet.class, ConverterSet.DEFAULT);
+
+  private static final String ATOM_NS = "http://www.w3.org/2005/Atom";
+  private static final String OS_NS = "http://ns.opensocial.org/2008/opensocial";
+  private static final String OSEARCH_NS = "http://a9.com/-/spec/opensearch/1.1";
+
+
+  private static final Map<String, NamespaceSet> NAMESPACES = initNameSpace();
+
+  private static Map<String, NamespaceSet> initNameSpace() {
+    // configure the name space mapping. This does not need to be all the elments in the
+    // namespace, just the point of translation from one namespace to another.
+    // It would have been good to use a standard parser/serializer approach, but
+    // the xstream namespace implementation does not work exactly how we need it to.
+    NamespaceSet atom = new NamespaceSet();
+    atom.addNamespace("xmlns", ATOM_NS);
+    atom.addNamespace("xmlns:osearch", OSEARCH_NS);
+    atom.addPrefixedElement("totalResults", "osearch:totalResults");
+    atom.addPrefixedElement("startIndex", "osearch:startIndex");
+    atom.addPrefixedElement("itemsPerPage", "osearch:itemsPerPage");
+
+    NamespaceSet os = new NamespaceSet();
+    os.addNamespace("xmlns", OS_NS);
+
+    return ImmutableMap.<String, NamespaceSet>builder()
+        .put("feed", atom)
+        .put("person", os)
+        .put("activity", os)
+        .put("activityEntry", os)
+        .put("account", os)
+        .put("address", os)
+        .put("bodyType", os)
+        .put("message", os)
+        .put("mediaItem", os)
+        .put("url", os)
+        .put("response", os)
+        .put("appdata", os)
+        .build();
+  }
+
+  static {
+    elementMappingList.put(ConverterSet.DEFAULT, ImmutableList.of(
+        // this is order specific, so put the more specified interfaces at the top.
+        new ClassFieldMapping("feed", AtomFeed.class),
+        new ClassFieldMapping("content", AtomContent.class),
+        new ClassFieldMapping("entry", AtomEntry.class),
+
+        new ClassFieldMapping("activity", Activity.class),
+        new ClassFieldMapping("activityEntry", ActivityEntry.class),
+        new ClassFieldMapping("object", ActivityObject.class),
+        new ClassFieldMapping("mediaLink", MediaLink.class),
+        new ClassFieldMapping("account", Account.class),
+        new ClassFieldMapping("address", Address.class),
+        new ClassFieldMapping("bodyType", BodyType.class),
+        new ClassFieldMapping("message", Message.class),
+        new ClassFieldMapping("messageCollection", MessageCollection.class),
+        new ClassFieldMapping("mediaItem", MediaItem.class),
+        new ClassFieldMapping("name", Name.class),
+        new ClassFieldMapping("organization", Organization.class),
+        new ClassFieldMapping("person", Person.class),
+        new ClassFieldMapping("url", Url.class),
+        new ClassFieldMapping("openSocial", ExtendableBean.class),
+        // this is an example of a class field mapping with context. If
+        // ListField is mapped inside an element named emails, replace the element
+        // name
+        // that would have been defiend as fqcn ListField with email
+
+        new ClassFieldMapping("ListField", ListField.class),
+
+        // some standard mappings not needed for runtime, but used in test, at the
+        // bottom so as not
+        // to conflict with other mappings.
+
+        new ClassFieldMapping("response", RestfulCollection.class),
+        new ClassFieldMapping("appdata", DataCollection.class),
+        new ClassFieldMapping("list", List.class),
+        new ClassFieldMapping("map", Map.class))
+    );
+
+    // element setup for RestfullCollection Responses
+
+    elementMappingList.put(ConverterSet.COLLECTION, ImmutableList.of(
+        new ClassFieldMapping("feed", AtomFeed.class),
+        new ClassFieldMapping("content", AtomContent.class),
+        new ClassFieldMapping("entry", AtomEntry.class),
+
+        new ClassFieldMapping("activity", Activity.class),
+        new ClassFieldMapping("activityEntry", ActivityEntry.class),
+        new ClassFieldMapping("object", ActivityObject.class),
+        new ClassFieldMapping("mediaLink", MediaLink.class),
+        new ClassFieldMapping("account", Account.class),
+        new ClassFieldMapping("address", Address.class),
+        new ClassFieldMapping("bodyType", BodyType.class),
+        new ClassFieldMapping("message", Message.class),
+        new ClassFieldMapping("messageCollection", MessageCollection.class),
+        new ClassFieldMapping("mediaItem", MediaItem.class),
+        new ClassFieldMapping("name", Name.class),
+        new ClassFieldMapping("organization", Organization.class),
+        new ClassFieldMapping("person", Person.class),
+        new ClassFieldMapping("url", Url.class),
+        new ClassFieldMapping("openSocial", ExtendableBean.class),
+        // this is an example of a class field mapping with context. If
+        // ListField is mapped inside an element named emails, replace the element
+        // name that would have been defiend as fqcn ListField with email
+
+        //     new ClassFieldMapping("emails", "email", ListField.class),
+        //     new ClassFieldMapping("phoneNumbers","phone", ListField.class),
+        new ClassFieldMapping("ListField", ListField.class),
+
+        // some standard mappings not needed for runtime, but used in test, at the
+        // bottom so as not to conflict with other mappings.
+
+        new ClassFieldMapping("response", RestfulCollection.class),
+        new ClassFieldMapping("list", List.class),
+        new ClassFieldMapping("map", Map.class))
+    );
+
+    elementClassMap.put(ConverterSet.DEFAULT, new ImmutableMap.Builder<String, Class<?>>()
+        .put("feed", AtomFeed.class)
+        .put("content", AtomContent.class)
+        .put("entry", AtomEntry.class)
+        .put("email", ListField.class)
+        .put("phone", ListField.class)
+        .put("list", ArrayList.class)
+        .put("map", ConcurrentHashMap.class)
+        .put("appdata", DataCollection.class)
+        .put("activity", Activity.class)
+        .put("activityEntry", ActivityEntry.class)
+        .put("object", ActivityObject.class)
+        .put("openSocial", ExtendableBean.class)
+        .put("mediaLink", MediaLink.class)
+        .put("account", Account.class)
+        .put("address", Address.class)
+        .put("bodyType", BodyType.class)
+        .put("message", Message.class)
+        .put("messageCollection", MessageCollection.class)
+        .put("mediaItem", MediaItem.class)
+        .put("name", Name.class)
+        .put("organization", Organization.class)
+        .put("person", Person.class)
+        .put("url", Url.class)
+        .put("listField", ListField.class).build()
+    );
+
+
+    itemFieldMappings.put(ConverterSet.DEFAULT, ImmutableList.of(
+        new ImplicitCollectionFieldMapping(AtomFeed.class, "entry", AtomEntry.class, "entry"),
+        new ImplicitCollectionFieldMapping(AtomContent.class, "entry", AtomKeyValue.class, "entry"),
+        new ImplicitCollectionFieldMapping(Person.class, "books", String.class, "books"),
+        new ImplicitCollectionFieldMapping(Person.class, "cars", String.class, "cars"),
+        new ImplicitCollectionFieldMapping(Person.class, "heroes", String.class, "heroes"),
+        new ImplicitCollectionFieldMapping(Person.class, "food", String.class, "food"),
+        new ImplicitCollectionFieldMapping(Person.class, "interests", String.class, "interests"),
+        new ImplicitCollectionFieldMapping(Person.class, "languagesSpoken", String.class, "languagesSpoken"),
+        new ImplicitCollectionFieldMapping(Person.class, "movies", String.class, "movies"),
+        new ImplicitCollectionFieldMapping(Person.class, "music", String.class, "music"),
+        new ImplicitCollectionFieldMapping(Person.class, "quotes", String.class, "quotes"),
+        new ImplicitCollectionFieldMapping(Person.class, "sports", String.class, "sports"),
+        new ImplicitCollectionFieldMapping(Person.class, "tags", String.class, "tags"),
+        new ImplicitCollectionFieldMapping(Person.class, "turnOns", String.class, "turnOns"),
+        new ImplicitCollectionFieldMapping(Person.class, "turnOffs", String.class, "turnOffs"),
+        new ImplicitCollectionFieldMapping(Person.class, "tvShows", String.class, "tvShows"),
+
+        new ImplicitCollectionFieldMapping(Person.class, "emails", ListField.class, "emails"),
+        new ImplicitCollectionFieldMapping(Person.class, "phoneNumbers", ListField.class, "phoneNumbers"),
+        new ImplicitCollectionFieldMapping(Person.class, "ims", ListField.class, "ims"),
+        new ImplicitCollectionFieldMapping(Person.class, "photos", ListField.class, "photos"),
+
+        new ImplicitCollectionFieldMapping(Person.class, "activities", Activity.class, "activities"),
+        new ImplicitCollectionFieldMapping(Person.class, "addresses", Address.class, "addresses"),
+        new ImplicitCollectionFieldMapping(Person.class, "organizations", Organization.class, "organizations"),
+        new ImplicitCollectionFieldMapping(Person.class, "urls", Url.class, "urls"),
+        new ImplicitCollectionFieldMapping(Person.class, "lookingFor", EnumImpl.class, "lookingFor"),
+
+        new ImplicitCollectionFieldMapping(Message.class, "recipients", String.class, "recipients"),
+        new ImplicitCollectionFieldMapping(Message.class, "collectionIds", String.class, "collectionsIds"),
+        new ImplicitCollectionFieldMapping(Message.class, "replies", String.class, "replies"),
+
+        new ImplicitCollectionFieldMapping(ActivityObject.class, "downstreamDuplicates", String.class, "downstreamDuplicate"),
+        new ImplicitCollectionFieldMapping(ActivityObject.class, "upstreamDuplicates", String.class, "upstreamDuplicate"),
+
+        new ImplicitCollectionFieldMapping(Activity.class, "mediaItems", MediaItem.class, "mediaItems"))
+    );
+
+    listElementMappingList.put(ConverterSet.DEFAULT, ImmutableList.<ClassFieldMapping>of());
+    fieldAliasMappingList.put(ConverterSet.DEFAULT, ImmutableList.<InterfaceFieldAliasMapping>of());
+  }
+
+  /**
+   * The Guice injector, used for creating new instances of the model.
+   */
+  private Injector injector;
+
+  /**
+   * @param injector the injector to initialize with
+   */
+  @Inject
+  public XStream081Configuration(Injector injector) {
+    this.injector = injector;
+  }
+
+  private static Multimap<String, Class<?>> getOmitMap(ConverterSet c) {
+    return Objects.firstNonNull(omitMap.get(c), omitMap.get(ConverterSet.DEFAULT));
+  }
+
+
+  /**
+   * {@inheritDoc}
+   *
+   * @param writerStack
+   * @see XStreamConfiguration#getConverterConfig(org.apache.shindig.protocol.conversion.xstream.XStreamConfiguration.ConverterSet, com.thoughtworks.xstream.converters.reflection.ReflectionProvider, com.thoughtworks.xstream.mapper.Mapper, com.thoughtworks.xstream.io.HierarchicalStreamDriver, org.apache.shindig.protocol.conversion.xstream.WriterStack)
+   */
+  public ConverterConfig getConverterConfig(ConverterSet c, ReflectionProvider rp,
+                                            Mapper dmapper, HierarchicalStreamDriver driver, WriterStack writerStack) {
+
+    InterfaceFieldAliasingMapper emapper = new InterfaceFieldAliasingMapper(dmapper, writerStack, fieldAliasMappingList.get(c));
+
+    InterfaceClassMapper fmapper = new InterfaceClassMapper(writerStack,
+        emapper,
+        elementMappingList.get(c),
+        listElementMappingList.get(c),
+        itemFieldMappings.get(c),
+        getOmitMap(c),
+        elementClassMap.get(c));
+
+    AttributeMapper amapper = new AttributeMapper(fmapper, new DefaultConverterLookup(), rp);
+
+    XStream xstream = new XStream(rp, driver, getClass().getClassLoader(), amapper);
+
+    xstream.registerConverter(new MapConverter(fmapper));
+    xstream.registerConverter(new RestfullCollectionConverter(fmapper));
+    xstream.registerConverter(new DataCollectionConverter(fmapper));
+    xstream.registerConverter(new AtomLinkConverter());
+    xstream.registerConverter(new AtomSummaryConverter());
+
+    xstream.registerConverter(new ISO8601DateConverter());
+    xstream.registerConverter(new ISO8601GregorianCalendarConverter());
+    xstream.registerConverter(new ISO8601SqlTimestampConverter());
+    xstream.registerConverter(new GuiceBeanConverter(fmapper, injector));
+    xstream.registerConverter(new AtomAttributeConverter());
+    xstream.registerConverter(new ExtendableBeanConverter(), XStream.PRIORITY_VERY_HIGH);
+    xstream.setMode(XStream.NO_REFERENCES);
+
+    amapper.addAttributeFor(AtomAttribute.class);
+
+    // prevent NPE on xstream 1.3.x
+    amapper.setConverterLookup(xstream.getConverterLookup());
+    return new ConverterConfig(fmapper, xstream);
+  }
+
+
+  /**
+   * {@inheritDoc}
+   *
+   * @see org.apache.shindig.protocol.conversion.xstream.XStreamConfiguration#getNameSpaces()
+   */
+  public Map<String, NamespaceSet> getNameSpaces() {
+    return NAMESPACES;
+  }
+
+  /**
+   * Delegate for an EnumMap that returns the value of the defaultkey if the
+   * designated key is not present.
+   * @param <K>
+   * @param <V>
+   */
+
+  private static final class DefaultedEnumMap<K extends Enum<K>,V> extends ForwardingMap<K,V> {
+    private final EnumMap<K,V> backing;
+    final K defaultval;
+
+    public DefaultedEnumMap(Class<K> clz, K defaultkey) {
+      super();
+      this.backing = new EnumMap<K,V>(Preconditions.checkNotNull(clz));
+      this.defaultval = Preconditions.checkNotNull(defaultkey);
+    }
+
+    public static <K extends Enum<K>,V> DefaultedEnumMap<K,V> init(Class<K> clz, K defaultkey) {
+      return new DefaultedEnumMap<K,V>(clz, defaultkey);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public V get(Object o) {
+      K key = (K)o;
+      return Objects.firstNonNull(backing.get(key), backing.get(defaultval));
+    }
+
+    @Override
+    protected Map<K,V> delegate() {
+      return backing;
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/xstream/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/xstream/package-info.java
new file mode 100644
index 0000000..76ddeca
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/core/util/xstream/package-info.java
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>XStream Utilities package</h1>
+<p>The XStream classes enable Bean to XML to Bean conversion using the XStream
+library. These are configured to produce and consume XML that conforms to the
+OpenSocial XSD. Most of the mapping is performed by InterfaceClassMapper which is
+configured by the BeanXStreamConverter class.</p>
+*/
+
+package org.apache.shindig.social.core.util.xstream;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Account.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Account.java
new file mode 100644
index 0000000..5fac95e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Account.java
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.AccountImpl;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * <p>
+ * The Account interface describes the an account held by a person. Describes an account held by a
+ * Person, which MAY be on the Service Provider's service, or MAY be on a different service. The
+ * service provider does not have a requirement to verify that the account actually belongs to the
+ * person that the account is connected to, and consumers are appropriately warned of this in the
+ * Specification.
+ * </p>
+ * <p>
+ * For each account, the domain is the top-most authoritative domain for this
+ * account, e.g. yahoo.com or reader.google.com, and MUST be non-empty. Each account must also
+ * contain a non-empty value for either username or userid, and MAY contain both, in which case the
+ * two values MUST be for the same account. These accounts can be used to determine if a user on one
+ * service is also known to be the same person on a different service, to facilitate connecting to
+ * people the user already knows on different services.
+ * <p>
+ * <p>
+ * Since V0.8.1
+ * </p>
+ */
+@ImplementedBy(AccountImpl.class)
+@Exportablebean
+public interface Account {
+
+
+  /**
+   * The fields that represent the account object in json form.
+   *
+   * <p>
+   * All of the fields that an account can have, all fields are required
+   * </p>
+   *
+   */
+  public static enum Field {
+    /** the json field for domain. */
+    DOMAIN("domain"),
+    /** the json field for userId. */
+    USER_ID("userId"),
+    /** the json field for username. */
+    USERNAME("username");
+
+    /**
+     * The json field that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * create a field base on the a json element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * emit the field as a json element.
+     *
+     * @return the field name
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * The top-most authoritative domain for this account, e.g. "twitter.com". This is the Primary
+   * Sub-Field for this field, for the purposes of sorting and filtering.
+   *
+   * @return the domain
+   */
+  String getDomain();
+
+  /**
+   * The top-most authoritative domain for this account, e.g. "twitter.com". This is the Primary
+   * Sub-Field for this field, for the purposes of sorting and filtering. *
+   *
+   * @param domain the domain
+   */
+  void setDomain(String domain);
+
+  /**
+   * A user ID number, usually chosen automatically, and usually numeric but sometimes alphanumeric,
+   * e.g. "12345" or "1Z425A".
+   *
+   * @return the userId
+   */
+  String getUserId();
+
+  /**
+   * A user ID number, usually chosen automatically, and usually numeric but sometimes alphanumeric,
+   * e.g. "12345" or "1Z425A".
+   *
+   * @param userId the userId
+   */
+  void setUserId(String userId);
+
+  /**
+   * An alphanumeric user name, usually chosen by the user, e.g. "jsmarr".
+   *
+   * @return the username
+   */
+  String getUsername();
+
+  /**
+   * An alphanumeric user name, usually chosen by the user, e.g. "jsmarr".
+   *
+   * @param username the username
+   */
+  void setUsername(String username);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Activity.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Activity.java
new file mode 100644
index 0000000..117eeec
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Activity.java
@@ -0,0 +1,488 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.ActivityImpl;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * see <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Activity.Field">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Activity.Field</a> for all
+ * field meanings. All fields are represented in the js api at this time except for lastUpdated.
+ * This field is currently only in the RESTful spec.
+ *
+ * <p>
+ * Representation of an activity.
+ * <p>
+ * </p>
+ * Activities are rendered with a title and an optional activity body.
+ * <p>
+ * </p>
+ * You may set the title and body directly as strings when calling opensocial.newActivity. However,
+ * it is usually beneficial to create activities using Message Templates for the title and body.
+ * <p>
+ * </p>
+ * Users will have many activities in their activity streams, and containers will not show every
+ * activity that is visible to a user. To help display large numbers of activities, containers will
+ * summarize a list of activities from a given source to a single entry.
+ * <p>
+ * </p>
+ * You can provide Activity Summaries to customize the text shown when multiple activities are
+ * summarized. If no customization is provided, a container may ignore your activities altogether or
+ * provide default text such as "Bob changed his status message + 20 other events like this."
+ * <ul>
+ * <li> Activity Summaries will always summarize around a specific key in a key/value pair. This is
+ * so that the summary can say something concrete (this is clearer in the example below). </li>
+ * <li> Other variables will have synthetic "Count" variables created with the total number of items
+ * summarized. </li>
+ * <li> Message ID of the summary is the message ID of the main template + ":" + the data key </li>
+ * </ul>
+ *
+ * <code><pre>
+ * &lt;messagebundle&gt;
+ *  &lt;msg name=&quot;LISTEN_TO_THIS_SONG:Artist&quot;&gt;
+ *    ${Subject.Count} of your friends have suggested listening to songs
+ *    by ${Artist}!
+ *  &lt;/msg&gt;
+ *  &lt;msg name=&quot;LISTEN_TO_THIS_SONG:Song&quot;&gt;
+ *    ${Subject.Count} of your friends have suggested listening to ${Song}
+ *  !&lt;/msg&gt;
+ *  &lt;msg name=&quot;LISTEN_TO_THIS_SONG:Subject&quot;&gt;
+ *   ${Subject.DisplayName} has recommended ${Song.Count} songs to you.
+ *  &lt;/msg&gt;
+ *  &lt;/messagebundle&gt;
+ * </pre></code>
+ */
+@ImplementedBy(ActivityImpl.class)
+@Exportablebean
+public interface Activity {
+
+  /**
+   * The fields that represent the activity object ion json form.
+   *
+   * <p>
+   * All of the fields that activities can have.
+   * </p>
+   * <p>
+   * It is only OPTIONAL to set one of TITLE_ID or TITLE. In addition, if you are using any
+   * variables in your title or title template, you must set TEMPLATE_PARAMS.
+   * </p>
+   * <p>
+   * Other possible fields to set are: URL, MEDIA_ITEMS, BODY_ID, BODY, EXTERNAL_ID, PRIORITY,
+   * STREAM_TITLE, STREAM_URL, STREAM_SOURCE_URL, and STREAM_FAVICON_URL.
+   * </p>
+   * <p>
+   * Containers are only OPTIONAL to use TITLE_ID or TITLE, they may ignore additional parameters.
+   * </p>
+   *
+   */
+  public static enum Field {
+    /** the json field for appId. */
+    APP_ID("appId"),
+    /** the json field for body. */
+    BODY("body"),
+    /** the json field for bodyId. */
+    BODY_ID("bodyId"),
+    /** the json field for externalId. */
+    EXTERNAL_ID("externalId"),
+    /** the json field for id. */
+    ID("id"),
+    /** the json field for updated. */
+    LAST_UPDATED("updated"), /* Needed to support the RESTful api */
+    /** the json field for mediaItems. */
+    MEDIA_ITEMS("mediaItems"),
+    /** the json field for postedTime. */
+    POSTED_TIME("postedTime"),
+    /** the json field for priority. */
+    PRIORITY("priority"),
+    /** the json field for streamFaviconUrl. */
+    STREAM_FAVICON_URL("streamFaviconUrl"),
+    /** the json field for streamSourceUrl. */
+    STREAM_SOURCE_URL("streamSourceUrl"),
+    /** the json field for streamTitle. */
+    STREAM_TITLE("streamTitle"),
+    /** the json field for streamUrl. */
+    STREAM_URL("streamUrl"),
+    /** the json field for templateParams. */
+    TEMPLATE_PARAMS("templateParams"),
+    /** the json field for title. */
+    TITLE("title"),
+    /** the json field for titleId. */
+    TITLE_ID("titleId"),
+    /** the json field for url. */
+    URL("url"),
+    /** the json field for userId. */
+    USER_ID("userId");
+
+    /**
+     * The json field that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * create a field base on the a json element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * emit the field as a json element.
+     *
+     * @return the field name
+     */
+    @Override
+    public String toString() {
+      return jsonString;
+    }
+  }
+
+  /**
+   * Get a string specifying the application that this activity is associated with. Container
+   * support for this field is REQUIRED.
+   *
+   * @return A string specifying the application that this activity is associated with
+   */
+  String getAppId();
+
+  /**
+   * Set a string specifying the application that this activity is associated with. Container
+   * support for this field is REQUIRED.
+   *
+   * @param appId A string specifying the application that this activity is associated with
+   */
+  void setAppId(String appId);
+
+  /**
+   * Get a string specifying an optional expanded version of an activity. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return a string specifying an optional expanded version of an activity.
+   */
+  String getBody();
+
+  /**
+   * Set a string specifying an optional expanded version of an activity. Container support for this
+   * field is OPTIONAL.
+   *
+   * Bodies may only have the following HTML tags:&lt;b&gt; &lt;i&gt;, &lt;a&gt;, &lt;span&gt;. The
+   * container may ignore this formatting when rendering the activity.
+   *
+   *
+   * @param body a string specifying an optional expanded version of an activity.
+   */
+  void setBody(String body);
+
+  /**
+   * Get a string specifying the body template message ID in the gadget spec. Container support for
+   * this field is OPTIONAL.
+   *
+   * Bodies may only have the following HTML tags: &lt;b&gt; &lt;i&gt;, &lt;a&gt;, &lt;span&gt;. The
+   * container may ignore this formatting when rendering the activity.
+   *
+   * @return a string specifying the body template message ID in the gadget spec.
+   */
+  String getBodyId();
+
+  /**
+   * Set a string specifying the body template message ID in the gadget spec. Container support for
+   * this field is OPTIONAL.
+   *
+   * @param bodyId a string specifying the body template message ID in the gadget spec.
+   */
+  void setBodyId(String bodyId);
+
+  /**
+   * Get an optional string ID generated by the posting application. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return An optional string ID generated by the posting application.
+   */
+  String getExternalId();
+
+  /**
+   * Set an optional string ID generated by the posting application. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param externalId An optional string ID generated by the posting application.
+   */
+  void setExternalId(String externalId);
+
+  /**
+   * Get a string ID that is permanently associated with this activity. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return a string ID that is permanently associated with this activity.
+   */
+  String getId();
+
+  /**
+   * Set a string ID that is permanently associated with this activity. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param id a string ID that is permanently associated with this activity.
+   */
+  void setId(String id);
+
+  /**
+   * Get the last updated date of the Activity, additional to the Opensocial specification for the
+   * REST-API. Container support for this field is OPTIONAL.
+   *
+   * @return the last updated date
+   */
+  Date getUpdated();
+
+  /**
+   * . Set the last updated date of the Activity, additional to the Opensocial specification for the
+   * REST-API. Container support for this field is OPTIONAL.
+   *
+   * @param updated the last updated date
+   */
+  void setUpdated(Date updated);
+
+  /**
+   * Get any photos, videos, or images that should be associated with the activity.
+   *
+   * Container support for this field is OPTIONAL.
+   *
+   * @return A List of {@link MediaItem} containing any photos, videos, or images that should be
+   *         associated with the activity.
+   */
+  List<MediaItem> getMediaItems();
+
+  /**
+   * Set any photos, videos, or images that should be associated with the activity. Container
+   * support for this field is OPTIONAL.
+   *
+   * Higher priority ones are higher in the list.
+   *
+   * @param mediaItems a list of {@link MediaItem} to be associated with the activity
+   */
+  void setMediaItems(List<MediaItem> mediaItems);
+
+  /**
+   * Get the time at which this activity took place in milliseconds since the epoch. Container
+   * support for this field is OPTIONAL.
+   *
+   * Higher priority ones are higher in the list.
+   *
+   * @return The time at which this activity took place in milliseconds since the epoch
+   */
+  Long getPostedTime();
+
+  /**
+   * Set the time at which this activity took place in milliseconds since the epoch Container
+   * support for this field is OPTIONAL.
+   *
+   * This value can not be set by the end user.
+   *
+   * @param postedTime the time at which this activity took place in milliseconds since the epoch
+   */
+  void setPostedTime(Long postedTime);
+
+  /**
+   * Get the priority, a number between 0 and 1 representing the relative priority of this activity
+   * in relation to other activities from the same source. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return a number between 0 and 1 representing the relative priority of this activity in
+   *         relation to other activities from the same source
+   */
+  Float getPriority();
+
+  /**
+   * Set the priority, a number between 0 and 1 representing the relative priority of this activity
+   * in relation to other activities from the same source. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param priority a number between 0 and 1 representing the relative priority of this activity in
+   *                relation to other activities from the same source.
+   */
+  void setPriority(Float priority);
+
+  /**
+   * Get a string specifying the URL for the stream's favicon. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return a string specifying the URL for the stream's favicon.
+   */
+  String getStreamFaviconUrl();
+
+  /**
+   * Set a string specifying the URL for the stream's favicon. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param streamFaviconUrl a string specifying the URL for the stream's favicon.
+   */
+  void setStreamFaviconUrl(String streamFaviconUrl);
+
+  /**
+   * Get a string specifying the stream's source URL. Container support for this field is OPTIONAL.
+   *
+   * @return a string specifying the stream's source URL.
+   */
+  String getStreamSourceUrl();
+
+  /**
+   * Set a string specifying the stream's source URL. Container support for this field is OPTIONAL.
+   *
+   * @param streamSourceUrl a string specifying the stream's source URL.
+   */
+  void setStreamSourceUrl(String streamSourceUrl);
+
+  /**
+   * Get a string specifing the title of the stream. Container support for this field is OPTIONAL.
+   *
+   * @return a string specifing the title of the stream.
+   */
+  String getStreamTitle();
+
+  /**
+   * Set a string specifing the title of the stream. Container support for this field is OPTIONAL.
+   *
+   * @param streamTitle a string specifing the title of the stream.
+   */
+  void setStreamTitle(String streamTitle);
+
+  /**
+   * Get a string specifying the stream's URL. Container support for this field is OPTIONAL.
+   *
+   * @return a string specifying the stream's URL.
+   */
+  String getStreamUrl();
+
+  /**
+   * Set a string specifying the stream's URL. Container support for this field is OPTIONAL.
+   *
+   * @param streamUrl a string specifying the stream's URL.
+   */
+  void setStreamUrl(String streamUrl);
+
+  /**
+   * Get a map of custom key/value pairs associated with this activity. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return a map of custom key/value pairs associated with this activity.
+   */
+  Map<String, String> getTemplateParams();
+
+  /**
+   * Set a map of custom key/value pairs associated with this activity. The data has type
+   * {@link Map<String, Object>}. The object may be either a String or an {@link Person}. When
+   * passing in a person with key PersonKey, can use the following replacement variables in the
+   * template:
+   * <ul>
+   * <li>PersonKey.DisplayName - Display name for the person</li>
+   * <li>PersonKey.ProfileUrl. URL of the person's profile</li>
+   * <li>PersonKey.Id - The ID of the person</li>
+   * <li>PersonKey - Container may replace with DisplayName, but may also optionally link to the
+   * user.</li>
+   * </ul>
+   * Container support for this field is OPTIONAL.
+   *
+   * @param templateParams a map of custom key/value pairs associated with this activity.
+   */
+  void setTemplateParams(Map<String, String> templateParams);
+
+  /**
+   * Get a string specifying the primary text of an activity. Container support for this field is
+   * REQUIRED.
+   *
+   * Titles may only have the following HTML tags: &lt;b&gt; &lt;i&gt;, &lt;a&gt;, &lt;span&gt;. The
+   * container may ignore this formatting when rendering the activity.
+   *
+   * @return astring specifying the primary text of an activity.
+   */
+  String getTitle();
+
+  /**
+   * Set a string specifying the primary text of an activity. Container support for this field is
+   * REQUIRED.
+   *
+   * Titles may only have the following HTML tags: &lt;b&gt; &lt;i&gt;, &lt;a&gt;, &lt;span&gt;. The
+   * container may ignore this formatting when rendering the activity.
+   *
+   * @param title a string specifying the primary text of an activity.
+   */
+  void setTitle(String title);
+
+  /**
+   * Get a string specifying the title template message ID in the gadget spec. Container support for
+   * this field is REQUIRED.
+   *
+   * The title is the primary text of an activity. Titles may only have the following HTML tags:
+   * <&lt;b&gt; &lt;i&gt;, &lt;a&gt;, &lt;span&gt;. The container may ignore this formatting when
+   * rendering the activity.
+   *
+   * @return a string specifying the title template message ID in the gadget spec.
+   */
+  String getTitleId();
+
+  /**
+   * Set a string specifying the title template message ID in the gadget spec. Container support for
+   * this field is REQUIRED.
+   *
+   * The title is the primary text of an activity. Titles may only have the following HTML tags:
+   * <&lt;b&gt; &lt;i&gt;, &lt;a&gt;, &lt;span&gt;. The container may ignore this formatting when
+   * rendering the activity.
+   *
+   * @param titleId a string specifying the title template message ID in the gadget spec.
+   */
+  void setTitleId(String titleId);
+
+  /**
+   * Get a string specifying the URL that represents this activity. Container support for this field
+   * is OPTIONAL.
+   *
+   * @return a string specifying the URL that represents this activity.
+   */
+  String getUrl();
+
+  /**
+   * Set a string specifying the URL that represents this activity. Container support for this field
+   * is OPTIONAL.
+   *
+   * @param url a string specifying the URL that represents this activity.
+   */
+  void setUrl(String url);
+
+  /**
+   * Get a string ID of the user who this activity is for. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return a string ID of the user who this activity is for.
+   */
+  String getUserId();
+
+  /**
+   * Get a string ID of the user who this activity is for. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param userId a string ID of the user who this activity is for.
+   */
+  void setUserId(String userId);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ActivityEntry.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ActivityEntry.java
new file mode 100644
index 0000000..daaeae4
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ActivityEntry.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.apache.shindig.social.core.model.ActivityEntryImpl;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * <p>Represents an 'Activity' within the Activity Streams JSON 1.0
+ * specification.  Refer to http://activitystrea.ms/head/json-activity.html</p>
+ */
+@ImplementedBy(ActivityEntryImpl.class)
+@Exportablebean
+public interface ActivityEntry extends Comparable<ActivityEntry>, ExtendableBean {
+
+  /**
+   * Fields that represent the JSON elements.
+   */
+  public static enum Field {
+    ACTOR("actor"),
+    CONTENT("content"),
+    GENERATOR("generator"),
+    ICON("icon"),
+    ID("id"),
+    OBJECT("object"),
+    PUBLISHED("published"),
+    PROVIDER("provider"),
+    TARGET("target"),
+    TITLE("title"),
+    UPDATED("updated"),
+    URL("url"),
+    VERB("verb"),
+    OPENSOCIAL("openSocial"),
+    EXTENSIONS("extensions");
+
+    // The name of the JSON element
+    private final String jsonString;
+
+    /**
+     * Constructs the field base for the JSON element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * Returns the name of the JSON element.
+     *
+     * @return String the name of the JSON element
+     */
+    public String toString() {
+      return jsonString;
+    }
+  }
+
+  /**
+   * <p>getActor</p>
+   *
+   * @return a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  ActivityObject getActor();
+
+  /**
+   * <p>setActor</p>
+   *
+   * @param actor a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  void setActor(ActivityObject actor);
+
+  /**
+   * <p>getContent</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getContent();
+
+  /**
+   * <p>setContent</p>
+   *
+   * @param content a {@link java.lang.String} object.
+   */
+  void setContent(String content);
+
+  /**
+   * <p>getGenerator</p>
+   *
+   * @return a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  ActivityObject getGenerator();
+
+  /**
+   * <p>setGenerator</p>
+   *
+   * @param generator a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  void setGenerator(ActivityObject generator);
+
+  /**
+   * <p>getIcon</p>
+   *
+   * @return a {@link org.apache.shindig.extras.as.opensocial.model.MediaLink} object.
+   */
+  MediaLink getIcon();
+
+  /**
+   * <p>setIcon</p>
+   *
+   * @param icon a {@link org.apache.shindig.extras.as.opensocial.model.MediaLink} object.
+   */
+  void setIcon(MediaLink icon);
+
+  /**
+   * <p>getId</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getId();
+
+  /**
+   * <p>setId</p>
+   *
+   * @param id a {@link java.lang.String} object.
+   */
+  void setId(String id);
+
+  /**
+   * <p>getObject</p>
+   *
+   * @return a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  ActivityObject getObject();
+
+  /**
+   * <p>setObject</p>
+   *
+   * @param object a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  void setObject(ActivityObject object);
+
+  /**
+   * <p>getPublished</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getPublished();
+
+  /**
+   * <p>setPublished</p>
+   *
+   * @param published a {@link java.lang.String} object.
+   */
+  void setPublished(String published);
+
+  /**
+   * <p>getProvider</p>
+   *
+   * @return a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  ActivityObject getProvider();
+
+  /**
+   * <p>setServiceProvider</p>
+   *
+   * @param provider a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  void setProvider(ActivityObject provider);
+
+  /**
+   * <p>getTarget</p>
+   *
+   * @return a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  ActivityObject getTarget();
+
+  /**
+   * <p>setTarget</p>
+   *
+   * @param target a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object.
+   */
+  void setTarget(ActivityObject target);
+
+  /**
+   * <p>getTitle</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getTitle();
+
+  /**
+   * <p>setTitle</p>
+   *
+   * @param title a {@link java.lang.String} object.
+   */
+  void setTitle(String title);
+
+  /**
+   * <p>getUpdated</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getUpdated();
+
+  /**
+   * <p>setUpdated</p>
+   *
+   * @param updated a {@link java.lang.String} object.
+   */
+  void setUpdated(String updated);
+
+  /**
+   * <p>getUrl</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getUrl();
+
+  /**
+   * <p>setUrl</p>
+   *
+   * @param url a {@link java.lang.String} object.
+   */
+  void setUrl(String url);
+
+  /**
+   * <p>getVerb</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getVerb();
+
+  /**
+   * <p>setVerb</p>
+   *
+   * @param verb a {@link java.lang.String} object.
+   */
+  void setVerb(String verb);
+
+  /**
+   * <p>getOpenSocial</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  ExtendableBean getOpenSocial();
+
+  /**
+   * <p>setOpenSocial</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  void setOpenSocial(ExtendableBean opensocial);
+
+  /**
+   * <p>getExtensions</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  ExtendableBean getExtensions();
+
+  /**
+   * <p>setOpenSocial</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  void setExtensions(ExtendableBean extensions);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ActivityObject.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ActivityObject.java
new file mode 100644
index 0000000..a4925b3
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ActivityObject.java
@@ -0,0 +1,277 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import java.util.List;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.apache.shindig.social.core.model.ActivityObjectImpl;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * A representation of an Activity's object.
+ *
+ * Note that an Activity's object may contain fields from an Activity when
+ * the objectType is of type 'activity'.  As such, ActivityObject becomes
+ * a superset of Activity.  Refer to the Activity Streams spec.
+ */
+@ImplementedBy(ActivityObjectImpl.class)
+@Exportablebean
+public interface ActivityObject extends ExtendableBean {
+
+  /**
+   * Fields that represent the JSON elements.
+   */
+  public static enum Field {
+    // Activity's object fields
+    ATTACHMENTS("attachments"),
+    AUTHOR("author"),
+    CONTENT("content"),
+    DISPLAY_NAME("displayName"),
+    DOWNSTREAM_DUPLICATES("downstreamDuplicates"),
+    ID("id"),
+    IMAGE("image"),
+    OBJECT_TYPE("objectType"),
+    PUBLISHED("published"),
+    SUMMARY("summary"),
+    UPDATED("updated"),
+    UPSTREAM_DUPLICATES("upstreamDuplicates"),
+    URL("url"),
+    OPENSOCIAL("openSocial");
+
+    // The name of the JSON element
+    private final String jsonString;
+
+    /**
+     * Constructs the field base for the JSON element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * Returns the name of the JSON element.
+     *
+     * @return String the name of the JSON element
+     */
+    public String toString() {
+      return jsonString;
+    }
+  }
+
+  /**
+   * <p>getAttachments</p>
+   *
+   * @return a list of {@link org.apache.shindig.social.opensocial.model.ActivityObject} object
+   */
+  List<ActivityObject> getAttachments();
+
+  /**
+   * <p>setAttachments</p>
+   *
+   * @param attachments a list of {@link org.apache.shindig.social.opensocial.model.ActivityObject} objects
+   */
+  void setAttachments(List<ActivityObject> attachments);
+
+  /**
+   * <p>getAuthor</p>
+   *
+   * @return a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object
+   */
+  ActivityObject getAuthor();
+
+  /**
+   * <p>setAuthor</p>
+   *
+   * @param author a {@link org.apache.shindig.social.opensocial.model.ActivityObject} object
+   */
+  void setAuthor(ActivityObject author);
+
+  /**
+   * <p>getContent</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getContent();
+
+  /**
+   * <p>setContent</p>
+   *
+   * @param content a {@link java.lang.String} object.
+   */
+  void setContent(String content);
+
+  /**
+   * <p>getDisplayName</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getDisplayName();
+
+  /**
+   * <p>setDisplayName</p>
+   *
+   * @param displayName a {@link java.lang.String} object
+   */
+  void setDisplayName(String displayName);
+
+  /**
+   * <p>getDownstreamDuplicates</p>
+   *
+   * @return a list of {@link java.lang.String} objects
+   */
+  List<String> getDownstreamDuplicates();
+
+  /**
+   * <p>setDownstreamDuplicates</p>
+   *
+   * @param downstreamDuplicates a list of {@link java.lang.String} objects
+   */
+  void setDownstreamDuplicates(List<String> downstreamDuplicates);
+
+  /**
+   * <p>getId</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getId();
+
+  /**
+   * <p>setId</p>
+   *
+   * @param id a {@link java.lang.String} object.
+   */
+  void setId(String id);
+
+  /**
+   * <p>getImage</p>
+   *
+   * @return a {@link org.apache.shindig.extras.as.opensocial.model.MediaLink} object
+   */
+  MediaLink getImage();
+
+  /**
+   * <p>setImage</p>
+   *
+   * @param image a {@link org.apache.shindig.extras.as.opensocial.model.MediaLink} object
+   */
+  void setImage(MediaLink image);
+
+  /**
+   * <p>getObjectType</p>
+   *
+   * @return a {@link java.lang.String} object
+   */
+  String getObjectType();
+
+  /**
+   * <p>setObjectType</p>
+   *
+   * @param objectType a {@link java.lang.String} object
+   */
+  void setObjectType(String objectType);
+
+  /**
+   * <p>getPublished</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getPublished();
+
+  /**
+   * <p>setPublished</p>
+   *
+   * @param published a {@link java.lang.String} object.
+   */
+  void setPublished(String published);
+
+  /**
+   * <p>getSummary</p>
+   *
+   * @return a {@link java.lang.String} object
+   */
+  String getSummary();
+
+  /**
+   * <p>setSummary</p>
+   *
+   * @param summary a {@link java.lang.String} object
+   */
+  void setSummary(String summary);
+
+  /**
+   * <p>getUpdated</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getUpdated();
+
+  /**
+   * <p>setUpdated</p>
+   *
+   * @param updated a {@link java.lang.String} object.
+   */
+  void setUpdated(String updated);
+
+  /**
+   * <p>getUpstreamDuplicates</p>
+   *
+   * @return a list of {@link java.lang.String} objects
+   */
+  List<String> getUpstreamDuplicates();
+
+  /**
+   * <p>setUpstreamDuplicates</p>
+   *
+   * @param upstreamDuplicates a list of {@link java.lang.String} objects
+   */
+  void setUpstreamDuplicates(List<String> upstreamDuplicates);
+
+  /**
+   * <p>getUrl</p>
+   *
+   * @return a {@link java.lang.String} object.
+   */
+  String getUrl();
+
+  /**
+   * <p>setUrl</p>
+   *
+   * @param url a {@link java.lang.String} object.
+   */
+  void setUrl(String url);
+
+  /**
+   * <p>getOpenSocial</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  ExtendableBean getOpenSocial();
+
+  /**
+   * <p>setOpenSocial</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  void setOpenSocial(ExtendableBean opensocial);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Address.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Address.java
new file mode 100644
index 0000000..d10166b
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Address.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import java.util.EnumSet;
+import java.util.Map;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.AddressImpl;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.Maps;
+import com.google.inject.ImplementedBy;
+
+/**
+ * Base interface for all address objects
+ * see <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Address">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Address</a>.
+ */
+
+@ImplementedBy(AddressImpl.class)
+@Exportablebean
+public interface Address {
+
+  /**
+   * The fields that represent the address object in json form.
+   */
+  public static enum Field {
+    /** the field name for country. */
+    COUNTRY("country"),
+    /** the field name for latitude. */
+    LATITUDE("latitude"),
+    /** the field name for locality. */
+    LOCALITY("locality"),
+    /** the field name for longitude. */
+    LONGITUDE("longitude"),
+    /** the field name for postalCode. */
+    POSTAL_CODE("postalCode"),
+    /** the field name for region. */
+    REGION("region"),
+    /** the feild name for streetAddress this field may be multiple lines. */
+    STREET_ADDRESS("streetAddress"),
+    /** the field name for type. */
+    TYPE("type"),
+    /** the field name for formatted. */
+    FORMATTED("formatted"),
+    /** the field name for primary. */
+    PRIMARY("primary");
+
+    private static final Map<String, Field> LOOKUP = Maps.uniqueIndex(EnumSet.allOf(Field.class),
+        Functions.toStringFunction());
+
+    /**
+     * The json field that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * create a field base on the a json element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * emit the field as a json element.
+     *
+     * @return the field name
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+
+    public static Field getField(String jsonString) {
+      return LOOKUP.get(jsonString);
+    }
+  }
+
+  /**
+   * Get the country.
+   *
+   * @return the country
+   */
+  String getCountry();
+
+  /**
+   * Set the country.
+   *
+   * @param country the country
+   */
+  void setCountry(String country);
+
+  /**
+   * Get the latitude.
+   *
+   * @return latitude
+   */
+  Float getLatitude();
+
+  /**
+   * Set the latitude.
+   *
+   * @param latitude latitude
+   */
+  void setLatitude(Float latitude);
+
+  /**
+   * Get the locality.
+   *
+   * @return the locality
+   */
+  String getLocality();
+
+  /**
+   * Set the locality.
+   *
+   * @param locality the locality
+   */
+  void setLocality(String locality);
+
+  /**
+   * Get the longitude of the address in degrees.
+   *
+   * @return the longitude of the address in degrees
+   */
+  Float getLongitude();
+
+  /**
+   * Set the longitude of the address in degrees.
+   *
+   * @param longitude the longitude of the address in degrees.
+   */
+  void setLongitude(Float longitude);
+
+  /**
+   * Get the Postal code for the address.
+   *
+   * @return the postal code for the address
+   */
+  String getPostalCode();
+
+  /**
+   * Set the postal code for the address.
+   *
+   * @param postalCode the postal code
+   */
+  void setPostalCode(String postalCode);
+
+  /**
+   * Get the region.
+   *
+   * @return the region
+   */
+  String getRegion();
+
+  /**
+   * Set the region.
+   *
+   * @param region the region
+   */
+  void setRegion(String region);
+
+  /**
+   * Get the street address.
+   *
+   * @return the street address
+   */
+  String getStreetAddress();
+
+  /**
+   * Set the street address.
+   *
+   * @param streetAddress the street address
+   */
+  void setStreetAddress(String streetAddress);
+
+  /**
+   * Get the type of label of the address.
+   *
+   * @return the type or label of the address
+   */
+  String getType();
+
+  /**
+   * Get the type of label of the address.
+   *
+   * @param type the type of label of the address.
+   */
+  void setType(String type);
+
+  /**
+   * Get the formatted address.
+   *
+   * @return the formatted address
+   */
+  String getFormatted();
+
+  /**
+   * Set the formatted address.
+   *
+   * @param formatted the formatted address
+   */
+  void setFormatted(String formatted);
+
+  /**
+   * <p>
+   * Get a Boolean value indicating whether this instance of the Plural Field is the primary or
+   * preferred value of for this field, e.g. the preferred mailing address. Service Providers MUST
+   * NOT mark more than one instance of the same Plural Field as primary="true", and MAY choose not
+   * to mark any fields as primary, if this information is not available. Introduced in v0.8.1
+   * </p><p>
+   * The service provider may wish to share the address instance between items and primary related
+   * to the address from which this came, so if the address came from an Organization, primary
+   * relates to the primary address of the organization, and not necessary the primary address of
+   * all addresses.
+   * </p><p>
+   * If the address is not part of a list (eg Person.location ) primary has no meaning.
+   * <p>
+   * @return true if the instance if the primary instance.
+   */
+  Boolean getPrimary();
+
+  /**
+   * @see  org.apache.shindig.social.opensocial.model.Address#getPrimary()
+   * @param primary set the Primary status of this Address.
+   */
+  void setPrimary(Boolean primary);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Album.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Album.java
new file mode 100644
index 0000000..fbc793b
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Album.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.AlbumImpl;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.List;
+
+/**
+ * <p>
+ * The Album API describes the collection of MediaItems of images, movies, and audio.
+ * </p>
+ * <p>
+ * Please see <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v09/OpenSocial-Specification.html#opensocial.Album.Field">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v09/OpenSocial-Specification.html#opensocial.Album.Field</a>
+ * for details about the supported fields.
+ * </p>
+ *
+ * @since 2.0.0
+ */
+@ImplementedBy(AlbumImpl.class)
+@Exportablebean
+public interface Album {
+
+  /**
+   * The fields that represent the Album object in json form.
+   */
+  public static enum Field {
+    DESCRIPTION("description"),
+    ID("id"),
+    LOCATION("location"),
+    MEDIA_ITEM_COUNT("mediaItemCount"),
+    MEDIA_MIME_TYPE("mediaMimeType"),
+    MEDIA_TYPE("mediaType"),
+    OWNER_ID("ownerId"),
+    THUMBNAIL_URL("thumbnailUrl"),
+    TITLE("title");
+
+    /**
+     * The json field that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * create a field base on the a json element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * emit the field as a json element.
+     *
+     * @return the field name
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * Get a string specifying the description of this album.
+   *
+   * @return a string specifying the description of this album.
+   */
+  String getDescription();
+
+  /**
+   * Set the description of this album.
+   *
+   * @param description a string specifying the description of this album.
+   */
+  void setDescription(String description);
+
+  /**
+   * Get a string ID specifying the unique identifier of this album.
+   *
+   * @return a string ID specifying the unique identifier of this album.
+   */
+  String getId();
+
+  /**
+   * Set a string ID specifying a unique identifier of this album.
+   *
+   * @param id a string ID specifying the unique identifier of this album.
+   */
+  void setId(String id);
+
+  /**
+   * Get address location of this album.
+   *
+   * @return an Address specifying the location of this album.
+   */
+  Address getLocation();
+
+  /**
+   * Set the address location of this album.
+   *
+   * @param location an Address specifying the location of this album.
+   */
+  void setLocation(Address location);
+
+  /**
+   * Get the number of items in the album.
+   *
+   * @return an integer specifying the number of items in the album
+   */
+  Integer getMediaItemCount();
+
+  /**
+   * Set the number of items in the album.
+   *
+   * @param mediaItemCount an integer specifying the number of items in the album.
+   */
+  void setMediaItemCount(Integer mediaItemCount);
+
+  /**
+   * Get the identifying mime-types of the items in the album.
+   *
+   * @return a List of strings specifying the mime-types of the items in the album.
+   */
+  List<String> getMediaMimeType();
+
+  /**
+   * Set the identifying mime-types of the items in the album.
+   *
+   * @param mediaMimeType a List of strings specifying the mime-types of the items in the album.
+   */
+  void setMediaMimeType(List<String> mediaMimeType);
+
+  /**
+   * Get the list of media item types for the items in the album.
+   *
+   * @return a List of MediaItem.Type specifying the media item types for items in the album.
+   */
+  List<MediaItem.Type> getMediaType();
+
+  /**
+   * Set the list of media item types for the items in the album.
+   *
+   * @param mediaType List of MediaItem.Type specifying media item types for items in the album.
+   */
+  void setMediaType(List<MediaItem.Type> mediaType);
+
+  /**
+   * Get the ID of the owner of the album.
+   *
+   * @return a string identifying the ID of the owner of the album.
+   */
+  String getOwnerId();
+
+  /**
+   * Set the string ID of the owner of the album.
+   *
+   * @param ownerId a string ID that identify the owner of the album.
+   */
+  void setOwnerId(String ownerId);
+
+  /**
+   * Get the URL to the thumbnail cover image for the album.
+   *
+   * @return a string specifying the URL for thumbnail cover image of the album.
+   */
+  String getThumbnailUrl();
+
+  /**
+   * Set the URL of the thumbnail cover image for the album.
+   *
+   * @param thumbnailUrl a string specifying the URL for thumbnail cover image of the album.
+   */
+  void setThumbnailUrl(String thumbnailUrl);
+
+  /**
+   * Get the title of the album.
+   *
+   * @return a string specifying the tile of the album.
+   */
+  String getTitle();
+
+  /**
+   * Set the title of the album.
+   *
+   * @param title a string specifying the title of the album.
+   */
+  void setTitle(String title);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/BodyType.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/BodyType.java
new file mode 100644
index 0000000..90769fb
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/BodyType.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.BodyTypeImpl;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Base interface for all body type objects. see
+ * <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Person.Field.BODY_TYPE">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Person.Field.BODY_TYPE</a>
+ */
+@ImplementedBy(BodyTypeImpl.class)
+@Exportablebean
+public interface BodyType {
+
+  /**
+   * The fields that represent the person object in serialized form.
+   */
+  public static enum Field {
+    /** the field name for build. */
+    BUILD("build"),
+    /** the field name for build. */
+    EYE_COLOR("eyeColor"),
+    /** the field name for hairColor. */
+    HAIR_COLOR("hairColor"),
+    /** the field name for height. */
+    HEIGHT("height"),
+    /** the field name for weight. */
+    WEIGHT("weight");
+
+    /**
+     * The field name that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * create a field base on the a json element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * emit the field as a json element.
+     *
+     * @return the field name
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * The build of the person's body, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the build of the person's body
+   */
+  String getBuild();
+
+  /**
+   * The build of the person's body, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param build the build of the person's body
+   */
+  void setBuild(String build);
+
+  /**
+   * The eye color of the person, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the eye color of the person
+   */
+  String getEyeColor();
+
+  /**
+   * The eye color of the person, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param eyeColor the eye color of the person
+   */
+  void setEyeColor(String eyeColor);
+
+  /**
+   * The hair color of the person, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the hair color of the person
+   */
+  String getHairColor();
+
+  /**
+   * The hair color of the person, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param hairColor the hair color of the person
+   */
+  void setHairColor(String hairColor);
+
+  /**
+   * The height of the person in meters, specified as a number. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the height of the person in meters
+   */
+  Float getHeight();
+
+  /**
+   * The height of the person in meters, specified as a number. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param height the height of the person in meters
+   */
+  void setHeight(Float height);
+
+  /**
+   * The weight of the person in kilograms, specified as a number. Container support for this field
+   * is OPTIONAL.
+   *
+   * @return the weight of the person in kilograms
+   */
+  Float getWeight();
+
+  /**
+   * The weight of the person in kilograms, specified as a number. Container support for this field
+   * is OPTIONAL.
+   *
+   * @param weight weight of the person in kilograms
+   */
+  void setWeight(Float weight);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Drinker.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Drinker.java
new file mode 100644
index 0000000..7e533d0
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Drinker.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+/**
+ * public java.lang.Enum for opensocial.Enum.Drinker.
+ */
+public enum Drinker implements org.apache.shindig.protocol.model.Enum.EnumKey {
+  /** Heavy drinker. */
+  HEAVILY("HEAVILY", "Heavily"),
+  /** non drinker. */
+  NO("NO", "No"),
+  /** occasional drinker. */
+  OCCASIONALLY("OCCASIONALLY", "Occasionally"),
+  /** has quit drinking. */
+  QUIT("QUIT", "Quit"),
+  /** in the process of quitting. */
+  QUITTING("QUITTING", "Quitting"),
+  /** regular drinker. */
+  REGULARLY("REGULARLY", "Regularly"),
+  /** drinks socially. */
+  SOCIALLY("SOCIALLY", "Socially"),
+  /** yes, a drinker of alchhol. */
+  YES("YES", "Yes");
+
+  /**
+   * the Json representation.
+   */
+  private final String jsonString;
+
+  /**
+   * the value used for display purposes.
+   */
+  private final String displayValue;
+
+  /**
+   * private internal constructor for the enum.
+   * @param jsonString the json representation.
+   * @param displayValue the display value.
+   */
+  private Drinker(String jsonString, String displayValue) {
+    this.jsonString = jsonString;
+    this.displayValue = displayValue;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see java.lang.Enum#toString()
+   */
+  @Override
+  public String toString() {
+    return this.jsonString;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see org.apache.shindig.protocol.model.Enum.EnumKey#getDisplayValue()
+   */
+  public String getDisplayValue() {
+    return displayValue;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/EnumUtil.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/EnumUtil.java
new file mode 100644
index 0000000..42b06cf
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/EnumUtil.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.Set;
+
+/**
+ * Utility class for OpenSocial enums.
+ */
+public final class EnumUtil {
+
+  /**
+   * This is a utility class and can't be constructed.
+   */
+  private EnumUtil() {
+
+  }
+  /**
+   *
+   * @param vals array of enums
+   * @return a set of the names for a list of Enum values defined by toString
+   */
+  // TODO: Because we have a Enum interface in this package we have to explicitly state the java.lang.Enum (bad ?)
+  public static Set<String> getEnumStrings(java.lang.Enum<?>... vals) {
+    ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+    for (java.lang.Enum<?> v : vals) {
+      builder.add(v.toString());
+    }
+    Set<String> result = builder.build();
+
+    Preconditions.checkArgument(result.size() == vals.length, "Enum names are not disjoint set");
+    return result;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Group.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Group.java
new file mode 100644
index 0000000..a833de5
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Group.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.GroupImpl;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.inject.ImplementedBy;
+
+/**
+ * <p>
+ * OpenSocial Groups are owned by people, and are used to tag or categorize people and their relationships.
+ * Each group has a display name, an identifier which is unique within the groups owned by that person, and a URI link.
+ * A group may be a private, invitation-only, public or a personal group used to organize friends.
+ * </p>
+ * <p>
+ * From http://opensocial-resources.googlecode.com/svn/spec/1.0/Social-Data.xml#Group
+ * </p>
+ *
+ * @since 2.0.0
+ */
+@ImplementedBy(GroupImpl.class)
+@Exportablebean
+public interface Group {
+
+  public static enum Field {
+    /**
+     * Unique ID for this group Required.
+     */
+    ID("id"),
+
+    /**
+     * Title of group Required.
+     */
+    TITLE("title"),
+
+    /**
+     * Description of group Optional.
+     */
+    DESCRIPTION("description");
+
+    /**
+     * A Map to convert JSON string to Field representations.
+     */
+    private static final Map<String,Field> LOOKUP = Maps.uniqueIndex(
+        EnumSet.allOf(Field.class), Functions.toStringFunction());
+
+    /**
+     * The set of all fields.
+     */
+    public static final Set<String> ALL_FIELDS = LOOKUP.keySet();
+
+    /**
+     * The set of default fields returned fields.
+     */
+    public static final Set<String> DEFAULT_FIELDS = ImmutableSet.of(
+        ID.toString(),
+        TITLE.toString(),
+        DESCRIPTION.toString());
+
+    /**
+     * The JSON field that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * Create a field base on the a JSON element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * Emit the field as a JSON element.
+     *
+     * @return the field name
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+
+    /**
+     * Converts from a url string (usually passed in the fields= parameter) into the
+     * corresponding field enum.
+     *
+     * @param jsonString The string to translate.
+     * @return The corresponding group field.
+     */
+    public static Group.Field fromUrlString(String jsonString) {
+      return LOOKUP.get(jsonString);
+    }
+  }
+
+  /**
+   * Get ID of this group
+   *
+   * @return groupId for group
+   */
+  String getId();
+
+  /**
+   * Set the default group id
+   *
+   * @param id a valid GroupId representation
+   */
+  void setId(Object id);
+
+  /**
+   * Get title of this group
+   *
+   * @return title of the group
+   */
+  String getTitle();
+
+  /**
+   * Sets the title of this group
+   *
+   * @param title a valid title
+   */
+  void setTitle(String title);
+
+  /**
+   * Get the description of this group
+   *
+   * @return description of group
+   */
+  String getDescription();
+
+  /**
+   * Sets the description of this group
+   *
+   * @param description a valid description
+   */
+  void setDescription(String description);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ListField.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ListField.java
new file mode 100644
index 0000000..16ad800
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/ListField.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.ListFieldImpl;
+
+import com.google.inject.ImplementedBy;
+
+
+/**
+ * <p>
+ * A Interface to represent list fields where there is potential for one of the items to have a
+ * primary or preferred status. Used where the List is a list of string values.
+ * </p>
+ * <p>
+ * Introduced in v0.8.1
+ * </p>
+ */
+@ImplementedBy(ListFieldImpl.class)
+@Exportablebean
+public interface ListField {
+
+  /**
+   * The fields that represent the ListField object in serialized form.
+   */
+  public static enum Field {
+    /** the field name for value. */
+    VALUE("value"),
+    /** the field name for type. */
+    TYPE("type"),
+    /** the field name for primary. */
+    PRIMARY("primary");
+
+    /**
+     * The field name that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * create a field baseD on the name of an element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return the string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * The type of field for this instance, usually used to label the preferred function of the given
+   * contact information. Unless otherwise specified, this string value specifies Canonical Values
+   * of <em>work</em>, <em>home</em>, and <em>other</em>.
+   *
+   * @return the type of the field
+   */
+  String getType();
+
+  /**
+   * Set the type of the field.
+   * @param type the type of the field
+   */
+  void setType(String type);
+
+  /**
+   * Get the primary value of this field, e.g. the actual e-mail address, phone number, or URL. When
+   * specifying a sortBy field in the Query Parameters for a Plural Field, the default meaning is to
+   * sort based on this value sub-field. Each non-empty Plural Field value MUST contain at least the
+   * value sub-field, but all other sub-fields are optional.
+   *
+   * @return the value of the field
+   */
+  String getValue();
+
+  /**
+   * @see ListField#getValue()
+   * @param value the value of the field
+   */
+  void setValue(String value);
+
+  /**
+   * Get Boolean value indicating whether this instance of the Plural Field is the primary or
+   * preferred value of for this field, e.g. the preferred mailing address or primary e-mail
+   * address. Service Providers MUST NOT mark more than one instance of the same Plural Field as
+   * primary="true", and MAY choose not to mark any fields as primary, if this information is not
+   * available. For efficiency, Service Providers SHOULD NOT mark all non-primary fields with
+   * primary="false", but should instead omit this sub-field for all non-primary instances.
+   *
+   * @return true if this is a primary or preferred value
+   */
+  Boolean getPrimary();
+
+  /**
+   * @see ListField#getPrimary()
+   * @param primary set to true if a primary or preferred value
+   */
+  void setPrimary(Boolean primary);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/LookingFor.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/LookingFor.java
new file mode 100644
index 0000000..706d705
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/LookingFor.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+/**
+ * public java.lang.Enum for opensocial.Enum.LookingFor.
+ */
+public enum LookingFor implements org.apache.shindig.protocol.model.Enum.EnumKey {
+  /** Interested in dating. */
+  DATING("DATING", "Dating"),
+  /** Looking for friends. */
+  FRIENDS("FRIENDS", "Friends"),
+  /** Looking for a relationship. */
+  RELATIONSHIP("RELATIONSHIP", "Relationship"),
+  /** Just want to network. */
+  NETWORKING("NETWORKING", "Networking"),
+  /** */
+  ACTIVITY_PARTNERS("ACTIVITY_PARTNERS", "Activity partners"),
+  /** */
+  RANDOM("RANDOM", "Random");
+
+  /**
+   * The Json representation of the value.
+   */
+  private final String jsonString;
+
+  /**
+   * The value used for display purposes.
+   */
+  private final String displayValue;
+
+  /**
+   * Construct a looking for enum.
+   * @param jsonString the json representation of the enum.
+   * @param displayValue the value used for display purposes.
+   */
+  private LookingFor(String jsonString, String displayValue) {
+    this.jsonString = jsonString;
+    this.displayValue = displayValue;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see java.lang.Enum#toString()
+   */
+  @Override
+  public String toString() {
+    return this.jsonString;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see org.apache.shindig.protocol.model.Enum.EnumKey#getDisplayValue()
+   */
+  public String getDisplayValue() {
+    return displayValue;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MediaItem.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MediaItem.java
new file mode 100644
index 0000000..2ed734d
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MediaItem.java
@@ -0,0 +1,367 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.MediaItemImpl;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * A container for the media item.
+ */
+@ImplementedBy(MediaItemImpl.class)
+@Exportablebean
+public interface MediaItem {
+
+  /**
+   * Fields for MediaItem.
+   */
+  public static enum Field {
+    ALBUM_ID("albumId"),
+    CREATED("created"),
+    DESCRIPTION("description"),
+    DURATION("duration"),
+    FILE_SIZE("fileSize"),
+    ID("id"),
+    LANGUAGE("language"),
+    LAST_UPDATED("lastUpdated"),
+    LOCATION("location"),
+    MIME_TYPE("mimeType"),
+    NUM_COMMENTS("numComments"),
+    NUM_VIEWS("numViews"),
+    NUM_VOTES("numVotes"),
+    RATING("rating"),
+    START_TIME("startTime"),
+    TAGGED_PEOPLE("taggedPeople"),
+    TAGS("tags"),
+    THUMBNAIL_URL("thumbnailUrl"),
+    TITLE("title"),
+    TYPE("type"),
+    URL("url");
+
+    /**
+     * The field name that the instance represents.
+     */
+    private final String jsonString;
+
+    /**
+     * create a field base on the an element name.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return a string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * An enumeration of potential media types.
+   */
+  public enum Type {
+    AUDIO("audio"),
+    IMAGE("image"),
+    VIDEO("video");
+
+    /**
+     * The field type.
+     */
+    private final String jsonString;
+
+    /**
+     * Construct a field type based on the name.
+     *
+     * @param jsonString
+     */
+    private Type(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return a string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * Get the mime type for this Media item.
+   * @return the mime type.
+   */
+  String getMimeType();
+
+  /**
+   * Set the mimetype for this Media Item.
+   * @param mimeType the mimeType
+   */
+  void setMimeType(String mimeType);
+
+  /**
+   * Get the Type of this media item, either audio, image or video.
+   * @return the Type of this media item
+   */
+  Type getType();
+
+  /**
+   * Get the Type of this media item, either audio, image or video.
+   * @param type the type of this media item
+   */
+  void setType(Type type);
+
+  /**
+   * Get a URL for the media item.
+   * @return the url of the media item
+   */
+  String getUrl();
+
+  /**
+   * Set a URL for the media item.
+   * @param url the media item URL
+   */
+  void setUrl(String url);
+
+  /**
+   * Get the thumbnail URL for the media item.
+   * @return the thumbnail url of the MediaItem
+   */
+  String getThumbnailUrl();
+
+  /**
+   * Set a thumbnail URL for the media item.
+   * @param url the thumbnail URL of the MediaItem
+   */
+  void setThumbnailUrl(String url);
+
+  /**
+   * Get the album which the media item belongs to.
+   * @return the album id.
+   */
+  String getAlbumId();
+
+  /**
+   * Set the album id which the media item belongs to.
+   * @param albumId the album id
+   */
+  void setAlbumId(String albumId);
+
+  /**
+   * Get the creation time
+   * @return creation time associated with the media item in UTC.
+   */
+  String getCreated();
+
+  /**
+   * Set the creation time
+   * @param created creation time associated with the media item in UTC.
+   */
+  void setCreated(String created);
+
+  /**
+   * Get the description of the media item
+   * @return description
+   */
+  String getDescription();
+
+  /**
+   * Set the description of the media item
+   * @param description
+   */
+  void setDescription(String description);
+
+  /**
+   * Get the playtime length in seconds of the MediaItem
+   * @return playtime
+   */
+  String getDuration();
+
+  /**
+   * Set the playtime length in seconds of the MediaItem
+   * @param duration
+   */
+  void setDuration(String duration);
+
+  /**
+   * Get the MediaItem's file size
+   * @return fileSize
+   */
+  String getFileSize();
+
+  /**
+   * Set the number of bytes for the MediaItem
+   * @param fileSize
+   */
+  void setFileSize(String fileSize);
+
+  /**
+   * Get the MediaItem's id
+   * @return id
+   */
+  String getId();
+
+  /**
+   * Set the MediaItem's id
+   * @param id
+   */
+  void setId(String id);
+
+  /**
+   * Get the language associated with the media item in ISO 639-3 format
+   * @return
+   */
+  String getLanguage();
+
+  /**
+   * Set the language associated with the media item in ISO 639-3 format
+   * @param language
+   */
+  void setLanguage(String language);
+
+  /**
+   * Get the update time associated with the media item
+   * @return lastUpdated
+   */
+  String getLastUpdated();
+
+  /**
+   * Set the update time associated with the media item
+   * @param lastUpdated
+   */
+  void setLastUpdated(String lastUpdated);
+
+  /**
+   * Get the location corresponding to the media item
+   * @return location
+   */
+  Address getLocation();
+
+  /**
+   * Set the location corresponding to the media item
+   * @param location
+   */
+  void setLocation(Address location);
+
+
+  /**
+   * Get the number of comments on the media item
+   * @return numComments
+   */
+  String getNumComments();
+
+  /**
+   * Set the number of comments on the media item
+   * @param numComments
+   */
+  void setNumComments(String numComments);
+
+  /**
+   * Get the number of views for the media item
+   * @return numViews
+   */
+  String getNumViews();
+
+  /**
+   * Set the number of views for the media item
+   * @param numViews
+   */
+  void setNumViews(String numViews);
+
+
+  /**
+   * Get the number of votes received for voting.
+   * @return numVotes
+   */
+  String getNumVotes();
+
+  /**
+   * Set the number of votes received for voting.
+   * @param numVotes
+   */
+  void setNumVotes(String numVotes);
+
+  /**
+   * Get the average rating of the media item on a scale of 0-10
+   * @return rating
+   */
+  String getRating();
+
+  /**
+   * Set the average rating of the media item on a scale of 0-10
+   * @param rating
+   */
+  void setRating(String rating);
+
+  /**
+   * Get the time when the content is available.
+   * @return startTime
+   */
+  String getStartTime();
+
+  /**
+   * Set the the time when the content is available.
+   * @param startTime
+   */
+  void setStartTime(String startTime);
+
+  /**
+   * Get people tagged in the media item.
+   * @return taggedPeople
+   */
+  String getTaggedPeople();
+
+  /**
+   * Set people tagged in the media item.
+   * @param taggedPeople
+   */
+  void setTaggedPeople(String taggedPeople);
+
+
+  /**
+   * Get tags associated with this media item.
+   * @return tags
+   */
+  String getTags();
+
+  /**
+   * Set tags associated with this media item.
+   * @param tags
+   */
+  void setTags(String tags);
+
+  /**
+   * Get the title for this media item
+   * @return title
+   */
+  String getTitle();
+
+  /**
+   * Set the title for this media item
+   * @param title
+   */
+  void setTitle(String title);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MediaLink.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MediaLink.java
new file mode 100644
index 0000000..0ff6360
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MediaLink.java
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.protocol.model.ExtendableBean;
+import org.apache.shindig.social.core.model.MediaLinkImpl;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * <p>MediaLink interface.</p>
+ */
+@ImplementedBy(MediaLinkImpl.class)
+@Exportablebean
+public interface MediaLink extends ExtendableBean {
+
+  /**
+   * Fields that represent the JSON elements.
+   */
+  public static enum Field {
+    DURATION("duration"),
+    HEIGHT("height"),
+    URL("url"),
+    WIDTH("width"),
+    OPENSOCIAL("openSocial");
+
+    // The name of the JSON element
+    private final String jsonString;
+
+    /**
+     * Constructs the field base for the JSON element.
+     *
+     * @param jsonString the name of the element
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * Returns the name of the JSON element.
+     *
+     * @return String the name of the JSON element
+     */
+    public String toString() {
+      return jsonString;
+    }
+  }
+
+  /**
+   * Returns the duration of this media.
+   *
+   * @return Integer is the target's duration
+   */
+  Integer getDuration();
+
+  /**
+   * Sets the duration of this media.
+   *
+   * @param duration is the target's duration
+   */
+  void setDuration(Integer duration);
+
+  /**
+   * Sets the height of this media.
+   *
+   * @return Integer the target's height
+   */
+  Integer getHeight();
+
+  /**
+   * Sets the height of this media.
+   *
+   * @param height is the target's height
+   */
+  void setHeight(Integer height);
+
+  /**
+   * Returns the target URL of this MediaLink.
+   *
+   * @return a target
+   */
+  String getUrl();
+
+  /**
+   * Sets the target URL for this MediaLink.
+   *
+   * @param target a target link
+   */
+  void setUrl(String url);
+
+  /**
+   * <p>Returns the width of this media.</p>
+   *
+   * @return Integer the target's width
+   */
+  Integer getWidth();
+
+  /**
+   * Sets the width of this media.
+   *
+   * @param width is the target's width
+   */
+  void setWidth(Integer width);
+
+  /**
+   * <p>getOpenSocial</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  ExtendableBean getOpenSocial();
+
+  /**
+   * <p>setOpenSocial</p>
+   *
+   * @return a {@link org.apache.shindig.protocol.model.ExtendableBean} object
+   */
+  void setOpenSocial(ExtendableBean opensocial);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Message.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Message.java
new file mode 100644
index 0000000..32b40c8
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Message.java
@@ -0,0 +1,373 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.MessageImpl;
+
+import com.google.inject.ImplementedBy;
+import java.util.List;
+import java.util.Set;
+import java.util.Date;
+
+/**
+ *
+ * Base interface for all message objects.
+ *
+ * see
+ * <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Message">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Message</a>
+ * <br/>
+ * <a href="http://wiki.opensocial.org/index.php?title=Messaging_API_Changes">
+ * http://wiki.opensocial.org/index.php?title=Messaging_API_Changes</a>
+ */
+
+@ImplementedBy(MessageImpl.class)
+@Exportablebean
+public interface Message {
+
+  /**
+   * An enumeration of field names in a message.
+   */
+  public static enum Field {
+    APP_URL("appUrl"),
+    /** the field name for body. */
+    BODY("body"),
+    /** the field name for body id. */
+    BODY_ID("bodyId"),
+    /** the field name for the collection IDs */
+    COLLECTION_IDS("collectionIds"),
+    /** the field name for the unique ID of this message. */
+    ID("id"),
+    /** the field name for the Parent Message Id for this message. */
+    IN_REPLY_TO("inReplyTo"),
+    /** the field name for replies to this message */
+    REPLIES("replies"),
+    /** the field name for recipient list. */
+    RECIPIENTS("recipients"),
+    /** the field name for the ID of the person who sent this message. */
+    SENDER_ID("senderId"),
+    /** the field name for the time this message was sent. */
+    TIME_SENT("timeSent"),
+    /** the field name for title. */
+    TITLE("title"),
+    /** the field name for title id. */
+    TITLE_ID("titleId"),
+    /** the field name for type. */
+    TYPE("type"),
+    /** the field name for status. */
+    STATUS("status"),
+    /** the field name for updated time stamp. */
+    UPDATED("updated"),
+    /** the field name for urls. */
+    URLS("urls");
+
+    /**
+     * the name of the field.
+     */
+    private final String jsonString;
+
+    public static final Set<String> ALL_FIELDS = EnumUtil.getEnumStrings(Field.values());
+
+    /**
+     * Create a field based on a name.
+     * @param jsonString the name of the field
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return a string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * The type of a message.
+   */
+  public enum Type {
+    /** An email. */
+    EMAIL("email"),
+    /** A short private message. */
+    NOTIFICATION("notification"),
+    /** A message to a specific user that can be seen only by that user. */
+    PRIVATE_MESSAGE("privateMessage"),
+    /** A message to a specific user that can be seen by more than that user. */
+    PUBLIC_MESSAGE("publicMessage");
+
+
+    /**
+     * The type of message.
+     */
+    private final String jsonString;
+
+    /**
+     * Create a message type based on a string token.
+     * @param jsonString the type of message
+     */
+    private Type(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return a string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * The Status of a message.
+   */
+  public enum Status {
+    NEW("new"), DELETED("deleted"), FLAGGED("read");
+    /**
+     * The type of message.
+     */
+    private final String jsonString;
+
+    /**
+     * Create a message type based on a string token.
+     * @param jsonString the type of message
+     */
+    private Status(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return a string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+
+  /**
+   * Gets the App URL for a message.
+   *
+   * Used if an App generated the message.
+   *
+   * @return the Application URL
+   */
+  String getAppUrl();
+
+  /**
+   * Set the App URL for a message.
+   * @param url the URL to set.
+   */
+  void setAppUrl(String url);
+
+  /**
+   * Gets the main text of the message.
+   * @return the main text of the message
+   */
+  String getBody();
+
+  /**
+   * Sets the main text of the message.
+   * HTML attributes are allowed and are sanitized by the container
+   * @param newBody the main text of the message
+   */
+  void setBody(String newBody);
+
+  /**
+   * Gets the body id.
+   * Used for message submission
+   * @return the body ID
+   */
+  String getBodyId();
+
+  /**
+   * Sets the body id.
+   * @param bodyId A valid body id defined in the gadget XML.
+   */
+  void setBodyId(String bodyId);
+
+
+  /**
+   * Gets the collection Ids for this message.
+   */
+  List<String> getCollectionIds();
+
+  /**
+   * Sets the collection Ids for this message.
+   */
+  void setCollectionIds(List<String> collectionIds);
+
+  /**
+   * Gets the unique ID of the message
+   * @return the ID of the message
+   */
+  String getId();
+
+  /**
+   * Sets the unique ID of the message.
+   * @param id the ID value to set
+   */
+  void setId(String id);
+
+  /**
+   * Gets the parent message ID.
+   * @return message id
+   */
+  String getInReplyTo();
+  /**
+   * Sets the parent message ID
+   * @param parentId the parentId to set
+   */
+  void setInReplyTo(String parentId);
+
+  /**
+   * Gets the recipient list of the message.
+   * @return the recipients of the message
+   */
+  List<String> getRecipients();
+
+  /**
+   * Gets the list of Replies to this message
+   * @return
+   */
+  List<String> getReplies();
+
+  /**
+   * Gets the Status of the message.
+   * @return the status of the message
+   */
+  Status getStatus();
+
+  /**
+   * Sets the Status of the message.
+   * @param status the status to set
+   */
+  void setStatus(Status status);
+
+  /**
+   * Sets the recipients of the message.
+   * HTML attributes are allowed and are sanitized by the container
+   * @param recipients the recipients text of the message
+   */
+  void setRecipients(List<String> recipients);
+
+  /**
+   * Gets the sender ID value.
+   * @return sender person id
+   */
+  String getSenderId();
+
+  /**
+   * sets the sender ID.
+   * @param senderId the sender id to set
+   */
+  void setSenderId(String senderId);
+
+  /**
+   * Gets the time the message was sent.
+   * @return the message sent time
+   */
+  Date getTimeSent();
+
+  /**
+   * Sets the time the message was sent.
+   * @param timeSent the time the message was sent
+   */
+  void setTimeSent(Date timeSent);
+
+  /**
+   * Gets the title of the message.
+   * @return the title of the message
+   */
+  String getTitle();
+
+  /**
+   * Sets the title of the message.
+   * HTML attributes are allowed and are sanitized by the container.
+   * @param newTitle the title of the message
+   */
+  void setTitle(String newTitle);
+
+  /**
+   * Gets the title ID for this message.
+   * Used for message submission.
+   * @return the title Id
+   */
+  String getTitleId();
+
+  /**
+   * Sets the title ID for this message.
+   * Used for message submission.
+   *
+   * @param titleId the title ID as defined in the gadget XML
+   */
+  void setTitleId(String titleId);
+
+  /**
+   * Gets the type of the message, as specified by opensocial.Message.Type.
+   * @return the type of message (enum Message.Type)
+   */
+  Type getType();
+
+  /**
+   * Sets the type of the message, as specified by opensocial.Message.Type.
+   * @param newType the type of message (enum Message.Type)
+   */
+  void setType(Type newType);
+
+  /**
+   * Gets the updated timestamp for the message.
+   * @return the updated date of the message
+   */
+  Date getUpdated();
+
+  /**
+   * Sets the updated timestamp for the message.
+   */
+  void setUpdated(Date updated);
+
+  /**
+   * Get the URLs related to the message
+   *
+   * @return the URLs related to the person, their webpages, or feeds
+   */
+  List<Url> getUrls();
+
+  /**
+   * Set the URLs related to the message
+   *
+   * @param urls the URLs related to the person, their webpages, or feeds
+   */
+  void setUrls(List<Url> urls);
+
+
+  /**
+   * TODO implement either a standard 'sanitizing' facility or
+   * define an interface that can be set on this class so
+   * others can plug in their own.
+   * @param htmlStr String to be sanitized.
+   * @return the sanitized HTML String
+   */
+  String sanitizeHTML(String htmlStr);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MessageCollection.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MessageCollection.java
new file mode 100644
index 0000000..b1ffc3e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/MessageCollection.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.social.core.model.MessageCollectionImpl;
+
+import com.google.inject.ImplementedBy;
+import java.util.List;
+import java.util.Set;
+import java.util.Date;
+
+/**
+ * Base interface for all message collection objects.
+ *
+ * see
+ * http://code.google.com/apis/opensocial/docs/0.7/reference/opensocial.MessageCollection.html
+ */
+
+@ImplementedBy(MessageCollectionImpl.class)
+public interface MessageCollection {
+
+  public String OUTBOX = "@outbox";
+  public String ALL = "@all";
+
+  /**
+   * An enumeration of field names in a message.
+   */
+  public static enum Field {
+    ID("id"),
+    /** the field name for the title of this message collection. */
+    TITLE("title"),
+    /** the field name for total number of messages. */
+    TOTAL("total"),
+    /** the field name for the total number of unread messages */
+    UNREAD("unread"),
+    /** The field name for the updated time stamp */
+    UPDATED("updated");
+
+    /**
+     * the name of the field.
+     */
+    private final String jsonString;
+
+    public static final Set<String> ALL_FIELDS = EnumUtil.getEnumStrings(Field.values());
+
+    /**
+     * Create a field based on a name.
+     * @param jsonString the name of the field
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return a string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+
+  /**
+   * Gets the unique ID of the message collection.
+   * @return the ID of the message
+   */
+  String getId();
+
+  /**
+   * Sets the unique ID of the message collection.
+   * @param id the ID value to set
+   */
+  void setId(String id);
+
+  /**
+   * Gets the title of the message collection.
+   * @return the title of the message
+   */
+  String getTitle();
+
+  /**
+   * Sets the title of the message message collection.
+   * @param newTitle the title of the message
+   */
+  void setTitle(String newTitle);
+
+  /**
+   * Gets the total number of messages for this collection.
+   * @return the total number of messages
+   */
+  Integer getTotal();
+
+  /**
+   * Sets the total number of messages for this collection
+   *
+   * @param total the total number of messages
+   */
+  void setTotal(Integer total);
+
+  /**
+   * Gets the total number of unread messages.
+   * @return the total number of unread messages
+   */
+  Integer getUnread();
+
+  /**
+   * Sets the total number of unread messages.
+   * @param unread the number of unread messages
+   */
+  void setUnread(Integer unread);
+
+  /**
+   * Returns the last time this message collection was modified.
+   * @return the updated time
+   */
+  Date getUpdated();
+
+  /**
+   * Sets the updated time for this message collection.
+   * @param updated
+   */
+  void setUpdated(Date updated);
+
+  /**
+   * Get the URLs related to the message collection.
+   *
+   * @return the URLs related to the message collection
+   */
+  List<Url> getUrls();
+
+  /**
+   * Set the URLs related to the message collection
+   *
+   * @param urls the URLs related to the message collection
+   */
+  void setUrls(List<Url> urls);
+
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Name.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Name.java
new file mode 100644
index 0000000..fb1df6f
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Name.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.NameImpl;
+
+import com.google.inject.ImplementedBy;
+
+/**
+ * Base interface for all name objects.
+ * see
+ * <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Name">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Name</a>
+ */
+
+@ImplementedBy(NameImpl.class)
+@Exportablebean
+public interface Name {
+
+  /**
+   * An enumeration of fields in the json name object.
+   */
+  public static enum Field {
+    /**
+     * The additional name.
+     */
+    ADDITIONAL_NAME("additionalName"),
+    /**
+     * The family name.
+     */
+    FAMILY_NAME("familyName"),
+    /**
+     * The given name.
+     */
+    GIVEN_NAME("givenName"),
+    /**
+     * The honorific prefix.
+     */
+    HONORIFIC_PREFIX("honorificPrefix"),
+    /**
+     * The honorific suffix.
+     */
+    HONORIFIC_SUFFIX("honorificSuffix"),
+    /**
+     * The formatted name.
+     */
+    FORMATTED("formatted");
+
+    /**
+     * the json key for this field.
+     */
+    private final String jsonString;
+
+    /**
+     * Construct the a field enum.
+     * @param jsonString the json key for the field.
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @see java.lang.Enum#toString()
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * @return the name, formatted.
+   */
+  String getFormatted();
+
+  /**
+   * set the name formatted.
+   * @param formatted the name, formatted.
+   */
+  void setFormatted(String formatted);
+
+  /**
+   * @return get the additional name.
+   */
+  String getAdditionalName();
+
+  /**
+   * @param additionalName set the additional name.
+   */
+  void setAdditionalName(String additionalName);
+
+  /**
+   * @return the family name.
+   */
+  String getFamilyName();
+
+  /**
+   * @param familyName the family name being set.
+   */
+  void setFamilyName(String familyName);
+
+  /**
+   * @return the given name.
+   */
+  String getGivenName();
+
+  /**
+   * @param givenName the given name to be set.
+   */
+  void setGivenName(String givenName);
+
+  /**
+   * @return the honorific prefix.
+   */
+  String getHonorificPrefix();
+
+  /**
+   * @param honorificPrefix the honorific prefix to be set.
+   */
+  void setHonorificPrefix(String honorificPrefix);
+
+  /**
+   * @return the honorific suffix.
+   */
+  String getHonorificSuffix();
+
+  /**
+   * @param honorificSuffix the honorific suffix to set.
+   */
+  void setHonorificSuffix(String honorificSuffix);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/NetworkPresence.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/NetworkPresence.java
new file mode 100644
index 0000000..0d85fa9
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/NetworkPresence.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+/**
+ * public java.lang.Enum for opensocial.Enum.NetworkPresence.
+ */
+public enum NetworkPresence implements org.apache.shindig.protocol.model.Enum.EnumKey {
+  /** Currently Online. */
+  ONLINE("ONLINE", "Online"),
+  /** Currently Offline. */
+  OFFLINE("OFFLINE", "Offline"),
+  /** Currently online but away. */
+  AWAY("AWAY", "Away"),
+  /** In a chat or available to chat. */
+  CHAT("CHAT", "Chat"),
+  /** Online, but don't disturb. */
+  DND("DND", "Do Not Disturb"),
+  /** Gone away for a longer period of time. */
+  XA("XA", "Extended Away");
+
+  /**
+   * The Json representation of the value.
+   */
+  private final String jsonString;
+
+  /**
+   * The value used for display purposes.
+   */
+  private final String displayValue;
+
+  /**
+   * Create a network presence enum.
+   * @param jsonString the json value.
+   * @param displayValue the display value.
+   */
+  private NetworkPresence(String jsonString, String displayValue) {
+    this.jsonString = jsonString;
+    this.displayValue = displayValue;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see java.lang.Enum#toString()
+   */
+  @Override
+  public String toString() {
+    return this.jsonString;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see org.apache.shindig.protocol.model.Enum.EnumKey#getDisplayValue()
+   */
+  public String getDisplayValue() {
+    return displayValue;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Organization.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Organization.java
new file mode 100644
index 0000000..8c5a816
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Organization.java
@@ -0,0 +1,294 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.OrganizationImpl;
+
+import com.google.inject.ImplementedBy;
+
+import java.util.Date;
+
+/**
+ * Describes a current or past organizational affiliation of this contact. Service Providers that
+ * support only a single Company Name and Job Title field should represent them with a single
+ * organization element with name and title properties, respectively.
+ *
+ * see <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Organization">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Organization</a>
+ */
+
+@ImplementedBy(OrganizationImpl.class)
+@Exportablebean
+public interface Organization {
+
+  /**
+   * An Enumeration of field names for Organization.
+   */
+  public static enum Field {
+    /** the name of the address field. */
+    ADDRESS("address"),
+    /** the name of the description field. */
+    DESCRIPTION("description"),
+    /** the name of the endDate field. */
+    END_DATE("endDate"),
+    /** the name of the field field. */
+    FIELD("field"),
+    /** the name of the name field. */
+    NAME("name"),
+    /** the name of the salary field. */
+    SALARY("salary"),
+    /** the name of the startDate field. */
+    START_DATE("startDate"),
+    /** the name of the subField field. */
+    SUB_FIELD("subField"),
+    /** the name of the title field. */
+    TITLE("title"),
+    /** the name of the webpage field. */
+    WEBPAGE("webpage"),
+    /**
+     * the name of the type field, Should have the value of "job" or "school" to be put in the right
+     * js fields.
+     */
+    TYPE("type"),
+    /** the name of the primary field. */
+    PRIMARY("primary");
+
+    /**
+     * the name of this field.
+     */
+    private final String jsonString;
+
+    /**
+     * Construct a field based on the name of the field.
+     *
+     * @param jsonString the name of the field
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * @return a string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * Get the address of the organization, specified as an Address. Container support for this field
+   * is OPTIONAL.
+   *
+   * @return the Address of the organization
+   */
+  Address getAddress();
+
+  /**
+   * Set the address of the organization, specified as an Address. Container support for this field
+   * is OPTIONAL.
+   *
+   * @param address the address of the organization
+   */
+  void setAddress(Address address);
+
+  /**
+   * Get a description or notes about the person's work in the organization, specified as a string.
+   * This could be the courses taken by a student, or a more detailed description about a
+   * Organization role. Container support for this field is OPTIONAL.
+   *
+   * @return a description about the persons work in the organization
+   */
+  String getDescription();
+
+  /**
+   * Set a description or notes about the person's work in the organization, specified as a string.
+   * This could be the courses taken by a student, or a more detailed description about a
+   * Organization role. Container support for this field is OPTIONAL.
+   *
+   * @param description a description about the persons work in the organization
+   */
+  void setDescription(String description);
+
+  /**
+   * Get the date the person stopped at the organization, specified as a Date. A null date indicates
+   * that the person is still involved with the organization. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the date the person stopped at the organization
+   */
+  Date getEndDate();
+
+  /**
+   * Set the date the person stopped at the organization, specified as a Date. A null date indicates
+   * that the person is still involved with the organization. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param endDate the date the person stopped at the organization
+   */
+  void setEndDate(Date endDate);
+
+  /**
+   * Get the field the organization is in, specified as a string. This could be the degree pursued
+   * if the organization is a school. Container support for this field is OPTIONAL.
+   *
+   * @return the field the organization is in
+   */
+  String getField();
+
+  /**
+   * Set the field the organization is in, specified as a string. This could be the degree pursued
+   * if the organization is a school. Container support for this field is OPTIONAL.
+   *
+   * @param field the field the organization is in
+   */
+  void setField(String field);
+
+  /**
+   * Get the name of the organization, specified as a string. For example, could be a school name or
+   * a job company. Container support for this field is OPTIONAL.
+   *
+   * @return the name of the organization
+   */
+  String getName();
+
+  /**
+   * Set the name of the organization, specified as a string. For example, could be a school name or
+   * a job company. Container support for this field is OPTIONAL.
+   *
+   * @param name the name of the organization
+   */
+  void setName(String name);
+
+  /**
+   * Get the salary the person receives from the organization, specified as a string. Container
+   * support for this field is OPTIONAL.
+   *
+   * @return the salary the person receives
+   */
+  String getSalary();
+
+  /**
+   * Set the salary the person receives from the organization, specified as a string. Container
+   * support for this field is OPTIONAL.
+   *
+   * @param salary the salary the person receives
+   */
+  void setSalary(String salary);
+
+  /**
+   * Get the date the person started at the organization, specified as a Date. Container support for
+   * this field is OPTIONAL.
+   *
+   * @return the start date at the organization
+   */
+  Date getStartDate();
+
+  /**
+   * Set the date the person started at the organization, specified as a Date. Container support for
+   * this field is OPTIONAL.
+   *
+   * @param startDate the start date at the organization
+   */
+  void setStartDate(Date startDate);
+
+  /**
+   * Get the subfield the Organization is in, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the subfield the Organization is in
+   */
+  String getSubField();
+
+  /**
+   * Set the subfield the Organization is in, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param subField the subfield the Organization is in
+   */
+  void setSubField(String subField);
+
+  /**
+   * Get the title or role the person has in the organization, specified as a string. This could be
+   * graduate student, or software engineer. Container support for this field is OPTIONAL.
+   *
+   * @return the title or role the person has in the organization
+   */
+  String getTitle();
+
+  /**
+   * Set the title or role the person has in the organization, specified as a string. This could be
+   * graduate student, or software engineer. Container support for this field is OPTIONAL.
+   *
+   * @param title the title or role the person has in the organization
+   */
+  void setTitle(String title);
+
+  /**
+   * Get a webpage related to the organization, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the URL of a webpage related to the organization
+   */
+  String getWebpage();
+
+  /**
+   * Get a webpage related to the organization, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param webpage the URL of a webpage related to the organization
+   */
+  void setWebpage(String webpage);
+
+  /**
+   * Get the type of field for this instance, usually used to label the preferred function of the
+   * given contact information. The type of organization, with Canonical Values <em>job</em> and
+   * <em>school</em>.
+   *
+   * @return the type of the field
+   */
+  String getType();
+
+  /**
+   * Set the type of field for this instance, usually used to label the preferred function of the
+   * given contact information. The type of organization, with Canonical Values <em>job</em> and
+   * <em>school</em>.
+   *
+   * @param type the type of the field
+   */
+  void setType(String type);
+
+  /**
+   * Get Boolean value indicating whether this instance of the Plural Field is the primary or
+   * preferred Organization.
+   *
+   * @return true if this is a primary or preferred value
+   */
+  Boolean getPrimary();
+
+  /**
+   * Set Boolean value indicating whether this instance of the Plural Field is the primary or
+   * preferred Organization.
+   *
+   * @param primary true if this is a primary or preferred value
+   */
+  void setPrimary(Boolean primary);
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Person.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Person.java
new file mode 100644
index 0000000..d3dca5f
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Person.java
@@ -0,0 +1,1221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.protocol.model.Enum;
+import org.apache.shindig.protocol.model.Exportablebean;
+import org.apache.shindig.social.core.model.PersonImpl;
+
+import com.google.common.base.Functions;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.inject.ImplementedBy;
+
+import java.util.Date;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * see <a href="http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Person.Field">
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Person.Field</a>
+ * for all field meanings. All fields are represented in the js api at this time except for lastUpdated.
+ * This field is currently only in the RESTful spec.
+ *
+ */
+@ImplementedBy(PersonImpl.class)
+@Exportablebean
+public interface Person {
+  /**
+   * The type of a profile url when represented as a list field.
+   */
+  String PROFILE_URL_TYPE = "profile";
+
+  /**
+   * The type of thumbnail photo types when represented as list fields.
+   */
+  String THUMBNAIL_PHOTO_TYPE = "thumbnail";
+
+  /**
+   * The display name for the user.
+   * @return the display name
+   */
+  String getDisplayName();
+
+  /**
+   * Set the display name.
+   * @param displayName the new display name.
+   */
+  void setDisplayName(String displayName);
+
+  /**
+   * Enumeration of genders.
+   */
+  public enum Gender {
+    /**
+     * Female.
+     */
+    female,
+    /**
+     * Male.
+     */
+    male
+  }
+
+  /**
+   * The fields that represent the person object in json form.
+   */
+  public static enum Field {
+    /** the json field for aboutMe. */
+    ABOUT_ME("aboutMe"),
+    /** the json field for accounts. */
+    ACCOUNTS("accounts"),
+    /** the json field for activities. */
+    ACTIVITIES("activities"),
+    /** the json field for addresses. */
+    ADDRESSES("addresses"),
+    /** the json field for age. */
+    AGE("age"),
+    /** the json field for appData. */
+    APP_DATA("appData"),
+    /** the json field for bodyType. */
+    BODY_TYPE("bodyType"),
+    /** the json field for books. */
+    BOOKS("books"),
+    /** the json field for cars. */
+    CARS("cars"),
+    /** the json field for children. */
+    CHILDREN("children"),
+    /** the json field for currentLocation. */
+    CURRENT_LOCATION("currentLocation"),
+    /** the json field for birthday. */
+    BIRTHDAY("birthday"),
+    /** the json field for display name. */
+    DISPLAY_NAME("displayName"), /** Needed to support the RESTful api. */
+    /** the json field for drinker. */
+    DRINKER("drinker"),
+    /** the json field for emails. */
+    EMAILS("emails"),
+    /** the json field for ethnicity. */
+    ETHNICITY("ethnicity"),
+    /** the json field for fashion. */
+    FASHION("fashion"),
+    /** the json field for food. */
+    FOOD("food"),
+    /** the json field for gender. */
+    GENDER("gender"),
+    /** the json field for happiestWhen. */
+    HAPPIEST_WHEN("happiestWhen"),
+    /** the json field for hasApp. */
+    HAS_APP("hasApp"),
+    /** the json field for heroes. */
+    HEROES("heroes"),
+    /** the json field for humor. */
+    HUMOR("humor"),
+    /** the json field for id. */
+    ID("id"),
+    /** the json field for IM accounts. */
+    IMS("ims"),
+    /** the json field for interests. */
+    INTERESTS("interests"),
+    /** the json field for jobInterests. */
+    JOB_INTERESTS("jobInterests"),
+    /** the json field for languagesSpoken. */
+    LANGUAGES_SPOKEN("languagesSpoken"),
+    /** the json field for updated. */
+    LAST_UPDATED("updated"), /** Needed to support the RESTful api. */
+    /** the json field for livingArrangement. */
+    LIVING_ARRANGEMENT("livingArrangement"),
+    /** the json field for lookingFor. */
+    LOOKING_FOR("lookingFor"),
+    /** the json field for movies. */
+    MOVIES("movies"),
+    /** the json field for music. */
+    MUSIC("music"),
+    /** the json field for name. */
+    NAME("name"),
+    /** the json field for networkPresence. */
+    NETWORKPRESENCE("networkPresence"),
+    /** the json field for nickname. */
+    NICKNAME("nickname"),
+    /** the json field for organiztions. */
+    ORGANIZATIONS("organizations"),
+    /** the json field for pets. */
+    PETS("pets"),
+    /** the json field for phoneNumbers. */
+    PHONE_NUMBERS("phoneNumbers"),
+    /** the json field for photos. */
+    PHOTOS("photos"),
+    /** the json field for politicalViews. */
+    POLITICAL_VIEWS("politicalViews"),
+    /** the json field for preferredUsername */
+    PREFERRED_USERNAME("preferredUsername"),
+    /** the json field for profileSong. */
+    PROFILE_SONG("profileSong"),
+    /** the json field for profileUrl. */
+    PROFILE_URL("profileUrl"),
+    /** the json field for profileVideo. */
+    PROFILE_VIDEO("profileVideo"),
+    /** the json field for quotes. */
+    QUOTES("quotes"),
+    /** the json field for relationshipStatus. */
+    RELATIONSHIP_STATUS("relationshipStatus"),
+    /** the json field for religion. */
+    RELIGION("religion"),
+    /** the json field for romance. */
+    ROMANCE("romance"),
+    /** the json field for scaredOf. */
+    SCARED_OF("scaredOf"),
+    /** the json field for sexualOrientation. */
+    SEXUAL_ORIENTATION("sexualOrientation"),
+    /** the json field for smoker. */
+    SMOKER("smoker"),
+    /** the json field for sports. */
+    SPORTS("sports"),
+    /** the json field for status. */
+    STATUS("status"),
+    /** the json field for tags. */
+    TAGS("tags"),
+    /** the json field for thumbnailUrl. */
+    THUMBNAIL_URL("thumbnailUrl"),
+    /** the json field for utcOffset. */
+    UTC_OFFSET("utcOffset"),
+    /** the json field for turnOffs. */
+    TURN_OFFS("turnOffs"),
+    /** the json field for turnOns. */
+    TURN_ONS("turnOns"),
+    /** the json field for tvShows. */
+    TV_SHOWS("tvShows"),
+    /** the json field for urls. */
+    URLS("urls");
+
+    /**
+     * a Map to convert json string to Field representations.
+     */
+
+    private static final Map<String,Field> LOOKUP = Maps.uniqueIndex(EnumSet.allOf(Field.class),
+        Functions.toStringFunction());
+
+    /**
+     * The json field that the instance represents.
+     */
+    private final String urlString;
+
+    /**
+     * The set of all fields.
+     */
+    public static final Set<String> ALL_FIELDS = LOOKUP.keySet();
+
+    /**
+     * The set of default fields returned fields.
+     */
+    public static final Set<String> DEFAULT_FIELDS = ImmutableSet.of(
+        ID.toString(),
+        NAME.toString(),
+        THUMBNAIL_URL.toString());
+
+    /**
+     * create a field base on the a json element.
+     *
+     * @param urlString the name of the element
+     */
+    private Field(String urlString) {
+      this.urlString = urlString;
+    }
+
+    /**
+     * emit the field as a json element.
+     *
+     * @return the field name
+     */
+    @Override
+    public String toString() {
+      return this.urlString;
+    }
+
+    public static Field getField(String jsonString) {
+      return LOOKUP.get(jsonString);
+    }
+
+    /**
+     * Converts from a url string (usually passed in the fields= parameter) into the
+     * corresponding field enum.
+     * @param urlString The string to translate.
+     * @return The corresponding person field.
+     */
+    public static Person.Field fromUrlString(String urlString) {
+      return LOOKUP.get(urlString);
+    }
+  }
+
+  /**
+   * Get a general statement about the person, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the value of aboutMe
+   */
+  String getAboutMe();
+
+  /**
+   * Set a general statement about the person, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param aboutMe the value of aboutMe
+   */
+  void setAboutMe(String aboutMe);
+
+  /**
+   * Get the list of online accounts held by this person.
+   * @return a list of Account objects
+   */
+  List<Account> getAccounts();
+
+  /**
+   * Set the list of online accounts held by this person.
+   * @param accounts a list of Account objects
+   */
+  void setAccounts(List<Account> accounts);
+
+  /**
+   * Get the person's favorite activities, specified as an List of strings. Container support for
+   * this field is OPTIONAL.
+   *
+   * @return list of activities.
+   */
+  List<String> getActivities();
+
+  /**
+   * Set the person's favorite activities, specified as an List of strings.
+   *
+   * @param activities a list of activities
+   */
+  void setActivities(List<String> activities);
+
+  /**
+   * Get addresses associated with the person, specified as an List of Address objects. Container
+   * support for this field is OPTIONAL.
+   *
+   * @return a List of address objects
+   */
+  List<Address> getAddresses();
+
+  /**
+   * Set addresses associated with the person, specified as an List of Address objects. Container
+   * support for this field is OPTIONAL.
+   *
+   * @param addresses a list of address objects
+   */
+  void setAddresses(List<Address> addresses);
+
+  /**
+   * Get the person's age, specified as a number. Container support for this field is OPTIONAL.
+   *
+   * @return the persons age
+   */
+  Integer getAge();
+
+  /**
+   * Set the person's age, specified as a number. Container support for this field is OPTIONAL.
+   *
+   * @param age the persons age
+   */
+  void setAge(Integer age);
+
+  /**
+   * Get app data for the person.
+   *
+   * @return the app data, possibly a subset.
+   */
+  Map<String, ?> getAppData();
+
+  /**
+   * Sets app data for the person.
+   *
+   * @param appData the app data, possibly a subset
+   */
+  void setAppData(Map<String, ?> appData);
+
+  /**
+   * Get the person's date of birth, specified as a {@link Date} object. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the person's data of birth
+   */
+  Date getBirthday();
+
+  /**
+   * Set the person's date of birth, specified as a {@link Date} object. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param birthday the person's data of birth
+   */
+  void setBirthday(Date birthday);
+
+  /**
+   * Get the person's body characteristics, specified as an BodyType. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the BodyType
+   */
+  BodyType getBodyType();
+
+  /**
+   * Set the person's body characteristics, specified as an BodyType. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param bodyType the person's BodyType
+   */
+  void setBodyType(BodyType bodyType);
+
+  /**
+   * Get the person's favorite books, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return list of books as strings
+   */
+  List<String> getBooks();
+
+  /**
+   * Set the person's favorite books, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param books a list of the person's books
+   */
+  void setBooks(List<String> books);
+
+  /**
+   * Get the person's favorite cars, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the persons favorite cars
+   */
+  List<String> getCars();
+
+  /**
+   * Set the person's favorite cars, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param cars a list of the persons favorite cars
+   */
+  void setCars(List<String> cars);
+
+  /**
+   * Get a description of the person's children, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the persons children
+   */
+  String getChildren();
+
+  /**
+   * Set a description of the person's children, specified as a string. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param children the persons children
+   */
+  void setChildren(String children);
+
+  /**
+   * Get the person's current location, specified as an {@link Address}. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the persons current location
+   */
+  Address getCurrentLocation();
+
+  /**
+   * Set the person's current location, specified as an {@link Address}. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param currentLocation the persons current location
+   */
+  void setCurrentLocation(Address currentLocation);
+
+  /**
+   * Get the person's drinking status, specified as an {@link Enum} with the enum's key referencing
+   * {@link Drinker}. Container support for this field is OPTIONAL.
+   *
+   * @return the persons drinking status
+   */
+  Enum<Drinker> getDrinker();
+
+  /**
+   * Get the person's drinking status, specified as an {@link Enum} with the enum's key referencing
+   * {@link Drinker}. Container support for this field is OPTIONAL.
+   *
+   * @param newDrinker the persons drinking status
+   */
+  void setDrinker(Enum<Drinker> newDrinker);
+
+  /**
+   * Get the person's Emails associated with the person.
+   * Container support for this field is OPTIONAL.
+   *
+   * @return a list of the person's emails
+   */
+  List<ListField> getEmails();
+
+  /**
+   * Set the person's Emails associated with the person.
+   * Container support for this field is OPTIONAL.
+   *
+   * @param emails a list of the person's emails
+   */
+  void setEmails(List<ListField> emails);
+
+  /**
+   * Get the person's ethnicity, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the person's ethnicity
+   */
+  String getEthnicity();
+
+  /**
+   * Set the person's ethnicity, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param ethnicity the person's ethnicity
+   */
+  void setEthnicity(String ethnicity);
+
+  /**
+   * Get the person's thoughts on fashion, specified as a string. Container support for this field
+   * is OPTIONAL.
+   *
+   * @return the person's thoughts on fashion
+   */
+  String getFashion();
+
+  /**
+   * Set the person's thoughts on fashion, specified as a string. Container support for this field
+   * is OPTIONAL.
+   *
+   * @param fashion the person's thoughts on fashion
+   */
+  void setFashion(String fashion);
+
+  /**
+   * Get the person's favorite food, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the person's favorite food
+   */
+  List<String> getFood();
+
+  /**
+   * Set the person's favorite food, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param food the person's favorite food
+   */
+  void setFood(List<String> food);
+
+  /**
+   * Get a person's gender, specified as an {@link Gender}.
+   *
+   * @return the person's gender
+   */
+  Gender getGender();
+
+  /**
+   * Set a person's gender, specified as an {@link Gender}.
+   *
+   * @param newGender the person's gender
+   */
+  void setGender(Gender newGender);
+
+  /**
+   * Get a description of when the person is happiest, specified as a string. Container support for
+   * this field is OPTIONAL.
+   *
+   * @return a description of when the person is happiest
+   */
+  String getHappiestWhen();
+
+  /**
+   * Set a description of when the person is happiest, specified as a string. Container support for
+   * this field is OPTIONAL.
+   *
+   * @param happiestWhen a description of when the person is happiest
+   */
+  void setHappiestWhen(String happiestWhen);
+
+  /**
+   * Get if the person has used the current app. Container support for this field is OPTIONAL.
+   * Has app needs to take account of the context of the application that is performing the
+   * query on this person object.
+   * @return true the current app has been used
+   */
+  Boolean getHasApp();
+
+  /**
+   * Set if the person has used the current app. Container support for this field is OPTIONAL.
+   *
+   * @param hasApp set true the current app has been used
+   */
+  void setHasApp(Boolean hasApp);
+
+  /**
+   * Get a person's favorite heroes, specified as an Array of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return a list of the person's favorite heroes
+   */
+  List<String> getHeroes();
+
+  /**
+   * Set a person's favorite heroes, specified as an Array of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param heroes a list of the person's favorite heroes
+   */
+  void setHeroes(List<String> heroes);
+
+  /**
+   * Get the person's thoughts on humor, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the person's thoughts on humor
+   */
+  String getHumor();
+
+  /**
+   * Set the person's thoughts on humor, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param humor the person's thoughts on humor
+   */
+  void setHumor(String humor);
+
+  /**
+   * Get A string ID that can be permanently associated with this person. Container support for this
+   * field is REQUIRED.
+   *
+   * @return the permanent ID of the person
+   */
+  String getId();
+
+  /**
+   * Set A string ID that can be permanently associated with this person. Container support for this
+   * field is REQUIRED.
+   *
+   * @param id the permanent ID of the person
+   */
+  void setId(String id);
+
+  /**
+   * Get a list of Instant messaging address for this Person. No official canonicalization rules
+   * exist for all instant messaging addresses, but Service Providers SHOULD remove all whitespace
+   * and convert the address to lowercase, if this is appropriate for the service this IM address is
+   * used for. Instead of the standard Canonical Values for type, this field defines the following
+   * Canonical Values to represent currently popular IM services: aim, gtalk, icq, xmpp, msn, skype,
+   * qq, and yahoo.
+   *
+   * @return A list of IM addresses
+   */
+  List<ListField> getIms();
+
+  /**
+   * Set a list of Instant messaging address for this Person. No official canonicalization rules
+   * exist for all instant messaging addresses, but Service Providers SHOULD remove all whitespace
+   * and convert the address to lowercase, if this is appropriate for the service this IM address is
+   * used for. Instead of the standard Canonical Values for type, this field defines the following
+   * Canonical Values to represent currently popular IM services: aim, gtalk, icq, xmpp, msn, skype,
+   * qq, and yahoo.
+   *
+   * @param ims a list ListFields representing IM addresses.
+   */
+  void setIms(List<ListField> ims);
+
+  /**
+   * Get the person's interests, hobbies or passions, specified as an List of strings. Container
+   * support for this field is OPTIONAL.
+   *
+   * @return the person's interests, hobbies or passions
+   */
+  List<String> getInterests();
+
+  /**
+   * Set the person's interests, hobbies or passions, specified as an List of strings. Container
+   * support for this field is OPTIONAL.
+   *
+   * @param interests the person's interests, hobbies or passions
+   */
+  void setInterests(List<String> interests);
+
+  /**
+   * Get the Person's favorite jobs, or job interests and skills, specified as a string. Container
+   * support for this field is OPTIONAL
+   *
+   * @return the Person's favorite jobs, or job interests and skills
+   */
+  String getJobInterests();
+
+  /**
+   * Set the Person's favorite jobs, or job interests and skills, specified as a string. Container
+   * support for this field is OPTIONAL
+   *
+   * @param jobInterests the Person's favorite jobs, or job interests and skills
+   */
+  void setJobInterests(String jobInterests);
+
+  /**
+   * Get a List of the languages that the person speaks as ISO 639-1 codes, specified as an List of
+   * strings. Container support for this field is OPTIONAL.
+   *
+   * @return a List of the languages that the person speaks
+   */
+  List<String> getLanguagesSpoken();
+
+  /**
+   * Set a List of the languages that the person speaks as ISO 639-1 codes, specified as an List of
+   * strings. Container support for this field is OPTIONAL.
+   *
+   * @param languagesSpoken a List of the languages that the person speaks
+   */
+  void setLanguagesSpoken(List<String> languagesSpoken);
+
+  /**
+   * The time this person was last updated.
+   *
+   * @return the last update time
+   */
+  Date getUpdated();
+
+  /**
+   * Set the time this record was last updated.
+   *
+   * @param updated the last update time
+   */
+  void setUpdated(Date updated);
+
+  /**
+   * Get a description of the person's living arrangement, specified as a string. Container support
+   * for this field is OPTIONAL.
+   *
+   * @return a description of the person's living arrangement
+   */
+  String getLivingArrangement();
+
+  /**
+   * Set a description of the person's living arrangement, specified as a string. Container support
+   * for this field is OPTIONAL.
+   *
+   * @param livingArrangement a description of the person's living arrangement
+   */
+  void setLivingArrangement(String livingArrangement);
+
+  /**
+   * Get a person's statement about who or what they are looking for, or what they are interested in
+   * meeting people for. Specified as an List of {@link org.apache.shindig.protocol.model.Enum} with the enum's key referencing
+   * {@link LookingFor} Container support for this field is OPTIONAL.
+   *
+   * @return person's statement about who or what they are looking for
+   */
+  List<Enum<LookingFor>> getLookingFor();
+
+  /**
+   * Get a person's statement about who or what they are looking for, or what they are interested in
+   * meeting people for. Specified as an List of {@link Enum} with the enum's key referencing
+   * {@link LookingFor} Container support for this field is OPTIONAL.
+   *
+   * @param lookingFor person's statement about who or what they are looking for
+   */
+  void setLookingFor(List<Enum<LookingFor>> lookingFor);
+
+  /**
+   * Get the Person's favorite movies, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @return the Person's favorite movies
+   */
+  List<String> getMovies();
+
+  /**
+   * Set the Person's favorite movies, specified as an List of strings. Container support for this
+   * field is OPTIONAL.
+   *
+   * @param movies the Person's favorite movies
+   */
+  void setMovies(List<String> movies);
+
+  /**
+   * Get the Person's favorite music, specified as an List of strings Container support for this
+   * field is OPTIONAL.
+   *
+   * @return Person's favorite music
+   */
+  List<String> getMusic();
+
+  /**
+   * Set the Person's favorite music, specified as an List of strings Container support for this
+   * field is OPTIONAL.
+   *
+   * @param music Person's favorite music
+   */
+  void setMusic(List<String> music);
+
+  /**
+   * Get the person's name Container support for this field is REQUIRED.
+   *
+   * @return the person's name
+   */
+  Name getName();
+
+  /**
+   * Set the person's name Container support for this field is REQUIRED.
+   *
+   * @param name the person's name
+   */
+  void setName(Name name);
+
+  /**
+   * Get the person's current network status. Specified as an {@link Enum} with the enum's key
+   * referencing {@link NetworkPresence}. Container support for this field is OPTIONAL.
+   *
+   * @return the person's current network status
+   */
+  Enum<NetworkPresence> getNetworkPresence();
+
+  /**
+   * Set the person's current network status. Specified as an {@link org.apache.shindig.protocol.model.Enum} with the enum's key
+   * referencing {@link NetworkPresence}. Container support for this field is OPTIONAL.
+   *
+   * @param networkPresence the person's current network status
+   */
+  void setNetworkPresence(org.apache.shindig.protocol.model.Enum<NetworkPresence> networkPresence);
+
+  /**
+   * Get the person's nickname. Container support for this field is REQUIRED.
+   *
+   * @return the person's nickname.
+   */
+  String getNickname();
+
+  /**
+   * Set the the person's nickname. Container support for this field is REQUIRED.
+   *
+   * @param nickname the person's nickname.
+   */
+  void setNickname(String nickname);
+
+  /**
+   * Get a list of current or past organizational affiliations of this Person.
+   * @return a list of Organization objects
+   */
+  List<Organization> getOrganizations();
+
+  /**
+   * Set a list of current or past organizational affiliations of this Person.
+   * @param organizations a list of Organisation objects
+   */
+  void setOrganizations(List<Organization> organizations);
+
+  /**
+   * Get a description of the person's pets Container support for this field is OPTIONAL.
+   *
+   * @return a description of the person's pets
+   */
+  String getPets();
+
+  /**
+   * Set a description of the person's pets Container support for this field is OPTIONAL.
+   *
+   * @param pets a description of the person's pets
+   */
+  void setPets(String pets);
+
+  /**
+   * Get the Phone numbers associated with the person.
+   *
+   * @return the Phone numbers associated with the person
+   */
+  List<ListField> getPhoneNumbers();
+
+  /**
+   * Set the Phone numbers associated with the person.
+   *
+   * @param phoneNumbers the Phone numbers associated with the person
+   */
+  void setPhoneNumbers(List<ListField> phoneNumbers);
+
+  /**
+   * URL of a photo of this person. The value SHOULD be a canonicalized URL, and MUST point to an
+   * actual image file (e.g. a GIF, JPEG, or PNG image file) rather than to a web page containing an
+   * image. Service Providers MAY return the same image at different sizes, though it is recognized
+   * that no standard for describing images of various sizes currently exists. Note that this field
+   * SHOULD NOT be used to send down arbitrary photos taken by this user, but specifically profile
+   * photos of the contact suitable for display when describing the contact.
+   *
+   * @return a list of Photos
+   */
+  List<ListField> getPhotos();
+
+  /**
+   * Set a list of Photos for the person.
+   * @see Person#getPhotos()
+   *
+   * @param photos a list of photos.
+   */
+  void setPhotos(List<ListField> photos);
+
+  /**
+   * Get the Person's political views, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @return the Person's political views
+   */
+  String getPoliticalViews();
+
+  /**
+   * Set the Person's political views, specified as a string. Container support for this field is
+   * OPTIONAL.
+   *
+   * @param politicalViews the Person's political views
+   */
+  void setPoliticalViews(String politicalViews);
+
+  /**
+   * Get the Person's preferred username, specified as a string. Container support for this field is OPTIONAL
+   *
+   * @return the Person's preferred username
+   */
+  String getPreferredUsername();
+
+  /**
+   * Set the Person's preferred username, specified as a string. Container support for this field is OPTIONAL
+   *
+   * @param preferredString the Person's preferred username
+   */
+  void setPreferredUsername(String preferredString);
+
+  /**
+   * Get the Person's profile song, specified as an {@link Url}. Container support for this field
+   * is OPTIONAL.
+   *
+   * @return the Person's profile song
+   */
+  Url getProfileSong();
+
+  /**
+   * Set the Person's profile song, specified as an {@link Url}. Container support for this field
+   * is OPTIONAL.
+   *
+   * @param profileSong the Person's profile song
+   */
+  void setProfileSong(Url profileSong);
+
+  /**
+   * Get the Person's profile video. Container support for this field is OPTIONAL.
+   *
+   * @return the Person's profile video
+   */
+  Url getProfileVideo();
+
+  /**
+   * Set the Person's profile video. Container support for this field is OPTIONAL.
+   *
+   * @param profileVideo the Person's profile video
+   */
+  void setProfileVideo(Url profileVideo);
+
+  /**
+   * Get the person's favorite quotes Container support for this field is OPTIONAL.
+   *
+   * @return the person's favorite quotes
+   */
+  List<String> getQuotes();
+
+  /**
+   * Set the person's favorite quotes. Container support for this field is OPTIONAL.
+   *
+   * @param quotes the person's favorite quotes
+   */
+  void setQuotes(List<String> quotes);
+
+  /**
+   * Get the person's relationship status. Container support for this field is OPTIONAL.
+   *
+   * @return the person's relationship status
+   */
+  String getRelationshipStatus();
+
+  /**
+   * Set the person's relationship status. Container support for this field is OPTIONAL.
+   *
+   * @param relationshipStatus the person's relationship status
+   */
+  void setRelationshipStatus(String relationshipStatus);
+
+  /**
+   * Get the person's relgion or religious views. Container support for this field is OPTIONAL.
+   *
+   * @return the person's relgion or religious views
+   */
+  String getReligion();
+
+  /**
+   * Set the person's relgion or religious views. Container support for this field is OPTIONAL.
+   *
+   * @param religion the person's relgion or religious views
+   */
+  void setReligion(String religion);
+
+  /**
+   * Get the person's comments about romance. Container support for this field is OPTIONAL.
+   *
+   * @return the person's comments about romance,
+   */
+  String getRomance();
+
+  /**
+   * Set a the person's comments about romance, Container support for this field is OPTIONAL.
+   *
+   * @param romance the person's comments about romance,
+   */
+  void setRomance(String romance);
+
+  /**
+   * Get what the person is scared of Container support for this field is OPTIONAL.
+   *
+   * @return what the person is scared of
+   */
+  String getScaredOf();
+
+  /**
+   * Set what the person is scared of Container support for this field is OPTIONAL.
+   *
+   * @param scaredOf what the person is scared of
+   */
+  void setScaredOf(String scaredOf);
+
+  /**
+   * Get the person's sexual orientation. Container support for this field is OPTIONAL.
+   *
+   * @return the person's sexual orientation
+   */
+  String getSexualOrientation();
+
+  /**
+   * Set the person's sexual orientation Container support for this field is OPTIONAL.
+   *
+   * @param sexualOrientation the person's sexual orientation
+   */
+  void setSexualOrientation(String sexualOrientation);
+
+  /**
+   * Get the person's smoking status. Container support for this field is OPTIONAL.
+   *
+   * @return the person's smoking status
+   */
+  Enum<Smoker> getSmoker();
+
+  /**
+   * Set the person's smoking status. Container support for this field is OPTIONAL.
+   *
+   * @param newSmoker the person's smoking status
+   */
+  void setSmoker(Enum<Smoker> newSmoker);
+
+  /**
+   * Get the person's favorite sports. Container support for this field is OPTIONAL.
+   *
+   * @return the person's favorite sports
+   */
+  List<String> getSports();
+
+  /**
+   * Set the person's favorite sports. Container support for this field is OPTIONAL.
+   *
+   * @param sports the person's favorite sports
+   */
+  void setSports(List<String> sports);
+
+  /**
+   * Get the person's status, headline or shoutout. Container support for this field is OPTIONAL.
+   *
+   * @return the person's status, headline or shoutout
+   */
+  String getStatus();
+
+  /**
+   * Set the person's status, headline or shoutout. Container support for this field is OPTIONAL.
+   *
+   * @param status the person's status, headline or shoutout
+   */
+  void setStatus(String status);
+
+  /**
+   * Get arbitrary tags about the person. Container support for this field is OPTIONAL.
+   *
+   * @return arbitrary tags about the person.
+   */
+  List<String> getTags();
+
+  /**
+   * Set arbitrary tags about the person. Container support for this field is OPTIONAL.
+   *
+   * @param tags arbitrary tags about the person.
+   */
+  void setTags(List<String> tags);
+
+  /**
+   * Get the Person's time zone, specified as the difference in minutes between Greenwich Mean Time
+   * (GMT) and the user's local time. Container support for this field is OPTIONAL.
+   *
+   * @return the Person's time zone
+   */
+  Long getUtcOffset();
+
+  /**
+   * Set the Person's time zone, specified as the difference in minutes between Greenwich Mean Time
+   * (GMT) and the user's local time. Container support for this field is OPTIONAL.
+   *
+   * @param utcOffset the Person's time zone
+   */
+  void setUtcOffset(Long utcOffset);
+
+  /**
+   * Get the person's turn offs. Container support for this field is OPTIONAL.
+   *
+   * @return the person's turn offs
+   */
+  List<String> getTurnOffs();
+
+  /**
+   * Set the person's turn offs. Container support for this field is OPTIONAL.
+   *
+   * @param turnOffs the person's turn offs
+   */
+  void setTurnOffs(List<String> turnOffs);
+
+  /**
+   * Get the person's turn ons. Container support for this field is OPTIONAL.
+   *
+   * @return the person's turn ons
+   */
+  List<String> getTurnOns();
+
+  /**
+   * Set the person's turn ons. Container support for this field is OPTIONAL.
+   *
+   * @param turnOns the person's turn ons
+   */
+  void setTurnOns(List<String> turnOns);
+
+  /**
+   * Get the person's favorite TV shows. Container support for this field is OPTIONAL.
+   *
+   * @return the person's favorite TV shows.
+   */
+  List<String> getTvShows();
+
+  /**
+   * Set the person's favorite TV shows. Container support for this field is OPTIONAL.
+   *
+   * @param tvShows the person's favorite TV shows.
+   */
+  void setTvShows(List<String> tvShows);
+
+  /**
+   * Get the URLs related to the person, their webpages, or feeds Container support for this field
+   * is OPTIONAL.
+   *
+   * @return the URLs related to the person, their webpages, or feeds
+   */
+  List<Url> getUrls();
+
+  /**
+   * Set the URLs related to the person, their webpages, or feeds Container support for this field
+   * is OPTIONAL.
+   *
+   * @param urls the URLs related to the person, their webpages, or feeds
+   */
+  void setUrls(List<Url> urls);
+
+  /**
+   * @return true if this person object represents the owner of the current page.
+   */
+  boolean getIsOwner();
+
+  /**
+   * Set the owner flag.
+   * @param isOwner the isOwnerflag
+   */
+  void setIsOwner(boolean isOwner);
+
+  /**
+   * Returns true if this person object represents the currently logged in user.
+   * @return true if the person accessing this object is a viewer.
+   */
+  boolean getIsViewer();
+
+  /**
+   * Returns true if this person object represents the currently logged in user.
+   * @param isViewer the isViewer Flag
+   */
+  void setIsViewer(boolean isViewer);
+
+
+  // Proxied fields
+
+  /**
+   * Get the person's profile URL. This URL must be fully qualified. Relative URLs will not work in
+   * gadgets. This field MUST be stored in the urls list with a type of "profile".
+   *
+   * Container support for this field is OPTIONAL.
+   *
+   * @return the person's profile URL
+   */
+  String getProfileUrl();
+
+  /**
+   * Set the person's profile URL. This URL must be fully qualified. Relative URLs will not work in
+   * gadgets. This field MUST be stored in the urls list with a type of "profile".
+   *
+   * Container support for this field is OPTIONAL.
+   *
+   * @param profileUrl the person's profile URL
+   */
+  void setProfileUrl(String profileUrl);
+
+  /**
+   * Get the person's photo thumbnail URL, specified as a string. This URL must be fully qualified.
+   * Relative URLs will not work in gadgets.
+   * This field MUST be stored in the photos list with a type of "thumbnail".
+   *
+   * Container support for this field is OPTIONAL.
+   *
+   * @return the person's photo thumbnail URL
+   */
+  String getThumbnailUrl();
+
+  /**
+   * Set the person's photo thumbnail URL, specified as a string. This URL must be fully qualified.
+   * Relative URLs will not work in gadgets.
+   * This field MUST be stored in the photos list with a type of "thumbnail".
+   *
+   * Container support for this field is OPTIONAL.
+   *
+   * @param thumbnailUrl the person's photo thumbnail URL
+   */
+  void setThumbnailUrl(String thumbnailUrl);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Smoker.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Smoker.java
new file mode 100644
index 0000000..79d9eee
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Smoker.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+/**
+ * public java.lang.Enum for opensocial.Enum.Smoker.
+ */
+public enum Smoker implements org.apache.shindig.protocol.model.Enum.EnumKey {
+  /**  A heavy smoker. */
+  HEAVILY("HEAVILY", "Heavily"),
+  /** Non smoker. */
+  NO("NO", "No"),
+  /** Smokes occasionally. */
+  OCCASIONALLY("OCCASIONALLY", "Ocasionally"),
+  /** Has quit smoking. */
+  QUIT("QUIT", "Quit"),
+  /** in the process of quitting smoking. */
+  QUITTING("QUITTING", "Quitting"),
+  /** regular smoker, but not a heavy smoker. */
+  REGULARLY("REGULARLY", "Regularly"),
+  /** smokes socially. */
+  SOCIALLY("SOCIALLY", "Socially"),
+  /** yes, a smoker. */
+  YES("YES", "Yes");
+
+  /**
+   * The Json representation of the value.
+   */
+  private final String jsonString;
+
+  /**
+   * The value used for display purposes.
+   */
+  private final String displayValue;
+
+  /**
+   * Create a Smoker enumeration.
+   * @param jsonString the json representation of the value.
+   * @param displayValue the value used for display purposes.
+   */
+  private Smoker(String jsonString, String displayValue) {
+    this.jsonString = jsonString;
+    this.displayValue = displayValue;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see java.lang.Enum#toString()
+   */
+  @Override
+  public String toString() {
+    return this.jsonString;
+  }
+
+  /**
+   * {@inheritDoc}
+   * @see org.apache.shindig.protocol.model.Enum.EnumKey#getDisplayValue()
+   */
+  public String getDisplayValue() {
+    return displayValue;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Url.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Url.java
new file mode 100644
index 0000000..3c20314
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/Url.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.social.core.model.UrlImpl;
+
+import com.google.inject.ImplementedBy;
+
+
+/**
+ * The base interface of all Url objects.
+ */
+@ImplementedBy(UrlImpl.class)
+public interface Url extends ListField {
+
+  /**
+   * An enumeration of the field names used in Url objects.
+   */
+  public static enum Field {
+    /** the name of the value field. */
+    VALUE("value"),
+    /** the name of the linkText field. */
+    LINK_TEXT("linkText"),
+    /** the name of the type field. */
+    TYPE("type");
+
+    /**
+     * The name of this field.
+     */
+    private final String jsonString;
+
+    /**
+     * Construct a new field based on a name.
+     *
+     * @param jsonString the name of the field
+     */
+    private Field(String jsonString) {
+      this.jsonString = jsonString;
+    }
+
+    /**
+     * The string representation of the enum.
+     */
+    @Override
+    public String toString() {
+      return this.jsonString;
+    }
+  }
+
+  /**
+   * Get the text associated with the link.
+   *
+   * @return the link text
+   */
+  String getLinkText();
+
+  /**
+   * Set the Link text associated with the link.
+   *
+   * @param linkText the link text
+   */
+  void setLinkText(String linkText);
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/package-info.java
new file mode 100644
index 0000000..2ef03f2
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/model/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>The Social Model API package</h1>
+<p>Model interfaces that are used throughout the Social Component to transfer the model. Implementors may implement these interfaces to contain their model objects.
+</p>
+*/
+
+package org.apache.shindig.social.opensocial.model;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/OAuthDataStore.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/OAuthDataStore.java
new file mode 100644
index 0000000..e417f61
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/OAuthDataStore.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.oauth;
+
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthProblemException;
+
+import org.apache.shindig.auth.SecurityToken;
+
+/**
+ * A class that manages the OAuth data for Shindig, including
+ * storing the map of consumer key/secrets, storing request and
+ * access tokens, and providing a way to upgrade tokens to
+ * authorized values.
+ */
+public interface OAuthDataStore {
+  /**
+   * Get the OAuthEntry that corresponds to the oauthToken.
+   *
+   * @param oauthToken a non-null oauthToken
+   * @return a valid OAuthEntry or null if no match
+   */
+  OAuthEntry getEntry(String oauthToken);
+
+
+  /**
+   * Return the proper security token for a 2 legged oauth request that has been validated
+   * for the given consumerKey. App specific checks like making sure the requested user has the
+   * app installed should take place in this method.
+   *
+   * Returning null may allow for other authentication schemes to be used.  Throwing an
+   * OAuthProblemException will allows errors to be returned to the caller.
+   *
+   * @param consumerKey A consumer key
+   * @param userId The userId to validate.
+   * @return A valid Security Token
+   * @throws OAuthProblemException when there are errors
+   */
+  SecurityToken getSecurityTokenForConsumerRequest(String consumerKey, String userId) throws OAuthProblemException;
+
+  /**
+   * Lookup consumers.  Generally this corresponds to an opensocial Application
+   * but could be abstracted in other ways.  If you have multiple containers you
+   * may want to include the container as part of the identifier.
+   *
+   * Your consumer object should have the key and secret, a link to your provider
+   * plus you should consider setting properties that correspond to the metadata
+   * in the opensocial app like icon, description, etc.
+   *
+   * Returning null will inform the client that the consumer key does not exist.  If you
+   * want to control the error response throw an OAuthProblemException
+   *
+   * @param consumerKey A valid, non-null ConsumerKey
+   * @return the consumer object corresponding to the specified key.
+   * @throws OAuthProblemException when the implementing class wants to signal errors
+   */
+  OAuthConsumer getConsumer(String consumerKey) throws OAuthProblemException;
+
+  /**
+   * Generate a valid requestToken for the given consumerKey.
+   *
+   * @param consumerKey A valid consumer key
+   * @param signedCallbackUrl Callback URL sent from consumer, may be null.  If callbackUrl is not
+   *     null then the returned entry should have signedCallbackUrl set to true.
+   * @return An OAuthEntry containing a valid request token.
+   * @throws OAuthProblemException when the implementing class wants to control the error response
+   */
+  OAuthEntry generateRequestToken(String consumerKey, String oauthVersion, String signedCallbackUrl)
+      throws OAuthProblemException;
+
+
+  /**
+   * Called when converting a request token to an access token.  This is called
+   * in the final phase of 3-legged OAuth after the user authorizes the app.
+   *
+   * @param entry The Entry to convert
+   * @return a new entry with type Type.ACCESS
+   * @throws OAuthProblemException when the implementing class wants to control the error response
+   */
+  OAuthEntry convertToAccessToken(OAuthEntry entry) throws OAuthProblemException;
+
+
+  /**
+   * Authorize the request token for the given user id.
+   *
+   * @param entry A valid OAuthEntry
+   * @param userId A user id
+   * @throws OAuthProblemException when the implementing class wants to control the error response
+   */
+  void authorizeToken(OAuthEntry entry, String userId) throws OAuthProblemException;
+
+  /**
+   * Mark a token DISABLED and store it.
+   *
+   * @param entry A valid OAuthEntry
+   */
+  void disableToken(OAuthEntry entry);
+
+  /**
+   * Remove a token
+   *
+   * @param entry A valid OAuthEntry
+   */
+  void removeToken(OAuthEntry entry);
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/OAuthEntry.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/OAuthEntry.java
new file mode 100644
index 0000000..52cfe9e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/OAuthEntry.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.oauth;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * The OAuthEntry class contains state information about OAuth Tokens and
+ * Authorization.
+ */
+public class OAuthEntry implements Serializable {
+  public static final long ONE_YEAR = 365 * 24 * 60 * 60 * 1000L;
+  public static final long FIVE_MINUTES = 5 * 60 * 1000L;
+
+  // Change this when incompatible changes occur..
+  static final long serialVersionUID = 2;
+
+  public static enum Type {
+    REQUEST, ACCESS, DISABLED
+  }
+
+  private String appId;
+  private String callbackUrl;
+  private boolean callbackUrlSigned; // true if consumer supports OAuth 1.0a
+  private String userId;
+  private String token;
+  private String tokenSecret;
+
+  private boolean authorized;
+
+  private String consumerKey;
+
+  private Type type;
+  private Date issueTime;
+
+  private String domain;
+  private String container;
+  private String oauthVersion;
+
+  private String callbackToken;
+  private int callbackTokenAttempts;
+
+  public OAuthEntry() {}
+
+
+  /**
+   * A copy constructor
+   * @param old the OAuthEntry to duplicate
+   */
+  public OAuthEntry(OAuthEntry old) {
+    this.appId = old.appId;
+    this.callbackUrl = old.callbackUrl;
+    this.callbackUrlSigned = old.callbackUrlSigned;
+    this.userId = old.userId;
+    this.token = old.token;
+    this.tokenSecret= old.tokenSecret;
+    this.authorized = old.authorized;
+    this.consumerKey = old.consumerKey;
+    this.type = old.type;
+    this.issueTime = old.issueTime;
+    this.domain = old.domain;
+    this.container = old.container;
+    this.oauthVersion = old.oauthVersion;
+    this.callbackToken = old.callbackToken;
+    this.callbackTokenAttempts = old.callbackTokenAttempts;
+  }
+
+  public boolean isExpired() {
+    Date currentDate = new Date();
+    return currentDate.compareTo(this.expiresAt()) > 0;
+  }
+
+  public Date expiresAt() {
+    long expirationTime = issueTime.getTime();
+    switch (type) {
+    case REQUEST:
+      expirationTime += FIVE_MINUTES;
+      break;
+    case ACCESS:
+      expirationTime += ONE_YEAR;
+      break;
+    }
+
+    return new Date(expirationTime);
+  }
+
+  public String getAppId() {
+    return appId;
+  }
+
+  public String getCallbackUrl() {
+    return callbackUrl;
+  }
+
+  public boolean isCallbackUrlSigned() {
+    return callbackUrlSigned;
+  }
+
+  public String getUserId() {
+    return userId;
+  }
+
+  public String getToken() {
+    return token;
+  }
+
+  public String getTokenSecret() {
+    return tokenSecret;
+  }
+
+  public boolean isAuthorized() {
+    return authorized;
+  }
+
+  public String getConsumerKey() {
+    return consumerKey;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public Date getIssueTime() {
+    return issueTime;
+  }
+
+  public String getDomain() {
+    return domain;
+  }
+
+  public String getContainer() {
+    return container;
+  }
+
+  public String getOauthVersion() {
+    return oauthVersion;
+  }
+
+  public String getCallbackToken() {
+    return callbackToken;
+  }
+
+  public int getCallbackTokenAttempts() {
+    return callbackTokenAttempts;
+  }
+
+  public void setAppId(String appId) {
+    this.appId = appId;
+  }
+
+  public void setCallbackUrl(String callbackUrl) {
+    this.callbackUrl = callbackUrl;
+  }
+
+  public void setCallbackUrlSigned(boolean callbackUrlSigned) {
+    this.callbackUrlSigned = callbackUrlSigned;
+  }
+
+  public void setUserId(String userId) {
+    this.userId = userId;
+  }
+
+  public void setToken(String token) {
+    this.token = token;
+  }
+
+  public void setTokenSecret(String tokenSecret) {
+    this.tokenSecret = tokenSecret;
+  }
+
+  public void setAuthorized(boolean authorized) {
+    this.authorized = authorized;
+  }
+
+  public void setConsumerKey(String consumerKey) {
+    this.consumerKey = consumerKey;
+  }
+
+  public void setType(Type type) {
+    this.type = type;
+  }
+
+  public void setIssueTime(Date issueTime) {
+    this.issueTime = issueTime;
+  }
+
+  public void setDomain(String domain) {
+    this.domain = domain;
+  }
+
+  public void setContainer(String container) {
+    this.container = container;
+  }
+
+  public void setOauthVersion(String oauthVersion) {
+    this.oauthVersion = oauthVersion;
+  }
+
+  public void setCallbackToken(String callbackToken) {
+    this.callbackToken = callbackToken;
+  }
+
+  public void setCallbackTokenAttempts(int callbackTokenAttempts) {
+    this.callbackTokenAttempts = callbackTokenAttempts;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/package-info.java
new file mode 100644
index 0000000..a9a36a7
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/oauth/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>The Social Authentication package</h1>
+<p>OAuth interfaces that are used throughout the Social Component to
+manage OAuth assertions.</p>
+*/
+
+package org.apache.shindig.social.opensocial.oauth;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/ActivityHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/ActivityHandler.java
new file mode 100644
index 0000000..14d131e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/ActivityHandler.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.spi.ActivityService;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+/**
+ * Rest/RPC handler for all activites related requests
+ */
+@Service(name = "activities", path="/{userId}+/{groupId}/{appId}/{activityId}+")
+public class ActivityHandler  {
+
+  private final ActivityService service;
+  private final ContainerConfig config;
+
+  @Inject
+  public ActivityHandler(ActivityService service, ContainerConfig config) {
+    this.service = service;
+    this.config = config;
+  }
+
+  /**
+   * Allowed end-points /activities/{userId}/@self/{actvityId}+
+   *
+   * examples: /activities/john.doe/@self/1
+   */
+  @Operation(httpMethods="DELETE")
+  public Future<?> delete(SocialRequestItem request)
+      throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+    Set<String> activityIds = ImmutableSet.copyOf(request.getListParameter("activityId"));
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+    // Throws exceptions if userIds contains more than one element or zero elements
+    return service.deleteActivities(Iterables.getOnlyElement(userIds), request.getGroup(),
+        request.getAppId(), activityIds, request.getToken());
+  }
+
+  /**
+   * Allowed end-points /activities/{userId}/@self
+   *
+   * examples: /activities/john.doe/@self - postBody is an activity object
+   */
+  @Operation(httpMethods="PUT", bodyParam = "activity")
+  public Future<?> update(SocialRequestItem request) throws ProtocolException {
+    return create(request);
+  }
+
+  /**
+   * Allowed end-points /activities/{userId}/@self
+   *
+   * examples: /activities/john.doe/@self - postBody is an activity object
+   */
+  @Operation(httpMethods="POST", bodyParam = "activity")
+  public Future<?> create(SocialRequestItem request) throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+    List<String> activityIds = request.getListParameter("activityId");
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+    // TODO(lryan) This seems reasonable to allow on PUT but we don't have an update verb.
+    HandlerPreconditions.requireEmpty(activityIds, "Cannot specify activityId in create");
+
+    return service.createActivity(Iterables.getOnlyElement(userIds), request.getGroup(),
+        request.getAppId(), request.getFields(),
+        request.getTypedParameter("activity", Activity.class),
+        request.getToken());
+  }
+
+  /**
+   * Allowed end-points /activities/{userId}/{groupId}/{optionalActvityId}+
+   * /activities/{userId}+/{groupId}
+   *
+   * examples: /activities/john.doe/@self/1 /activities/john.doe/@self
+   * /activities/john.doe,jane.doe/@friends
+   */
+  @Operation(httpMethods="GET")
+  public Future<?> get(SocialRequestItem request)
+      throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+    Set<String> optionalActivityIds = ImmutableSet.copyOf(request.getListParameter("activityId"));
+
+    CollectionOptions options = new CollectionOptions(request);
+
+    // Preconditions
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    if (userIds.size() > 1 && !optionalActivityIds.isEmpty()) {
+      throw new IllegalArgumentException("Cannot fetch same activityIds for multiple userIds");
+    }
+
+    if (!optionalActivityIds.isEmpty()) {
+      if (optionalActivityIds.size() == 1) {
+        return service.getActivity(userIds.iterator().next(), request.getGroup(),
+            request.getAppId(), request.getFields(), optionalActivityIds.iterator().next(),
+            request.getToken());
+      } else {
+        return service.getActivities(userIds.iterator().next(), request.getGroup(),
+            request.getAppId(), request.getFields(), options, optionalActivityIds, request.getToken());
+      }
+    }
+
+    return service.getActivities(userIds, request.getGroup(),
+        request.getAppId(),
+        // TODO: add pagination and sorting support
+        // getSortBy(params), getFilterBy(params), getStartIndex(params), getCount(params),
+        request.getFields(), options, request.getToken());
+  }
+
+  @Operation(httpMethods = "GET", path="/@supportedFields")
+  public List<Object> supportedFields(RequestItem request) {
+    // TODO: Would be nice if name in config matched name of service.
+    String container = Objects.firstNonNull(request.getToken().getContainer(), ContainerConfig.DEFAULT_CONTAINER);
+    return config.getList(container,
+        "${Cur['gadgets.features'].opensocial.supportedFields.activity}");
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/ActivityStreamHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/ActivityStreamHandler.java
new file mode 100644
index 0000000..87f86d4
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/ActivityStreamHandler.java
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.spi.ActivityStreamService;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+/**
+ * <p>ActivityStreamHandler class.</p>
+ */
+@Service(name = "activitystreams", path="/{userId}+/{groupId}/{appId}/{activityId}+")
+public class ActivityStreamHandler {
+
+  private final ActivityStreamService service;
+  private final ContainerConfig config;
+
+  /**
+   * <p>Constructor for ActivityStreamHandler.</p>
+   *
+   * @param service a {@link org.apache.shindig.social.opensocial.spi.ActivityStreamService} object.
+   * @param config a {@link org.apache.shindig.config.ContainerConfig} object.
+   */
+  @Inject
+  public ActivityStreamHandler(ActivityStreamService service, ContainerConfig config) {
+    this.service = service;
+    this.config = config;
+  }
+
+  /**
+   * Allowed end-points /activitystreams/{userId}/@self/{appId}/{activityId}+
+   *
+   * Examples: /activitystreams/john.doe/@self/1/object1,object2
+   *
+   * @param request a {@link org.apache.shindig.social.opensocial.service.SocialRequestItem} object.
+   * @return a {@link java.util.concurrent.Future} object.
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  @Operation(httpMethods="DELETE")
+  public Future<?> delete(SocialRequestItem request)
+      throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+    Set<String> activityIds = ImmutableSet.copyOf(request.getListParameter("activityId"));
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+    HandlerPreconditions.requireNotEmpty(activityIds, "At least one activity ID must be specified");
+
+    return service.deleteActivityEntries(Iterables.getOnlyElement(userIds), request.getGroup(),
+        request.getAppId(), activityIds, request.getToken());
+  }
+
+  /**
+   * Allowed end-points /activitystreams/{userId}/@self/{appId}/{activityId}
+   *
+   * Examples: /activitystreams/john.doe/@self/1/object2 - postBody is an activity object
+   *
+   * @param request a {@link org.apache.shindig.social.opensocial.service.SocialRequestItem} object.
+   * @return a {@link java.util.concurrent.Future} object.
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  @Operation(httpMethods="PUT", bodyParam = "activity")
+  public Future<?> update(SocialRequestItem request) throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+    List<String> activityIds = request.getListParameter("activityId");
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+    HandlerPreconditions.requireSingular(activityIds, "Must specify exactly one activity ID");
+
+    return service.updateActivityEntry(Iterables.getOnlyElement(userIds), request.getGroup(),
+        request.getAppId(), request.getFields(),
+        request.getTypedParameter("activity", ActivityEntry.class),
+        activityIds.iterator().next(),
+        request.getToken());
+  }
+
+  /**
+   * Allowed end-points /activitystreams/{userId}/@self/{appId}
+   *
+   * Examples: /activitystreams/john.doe/@self/{appId} - postBody is an activity object
+   *
+   * @param request a {@link org.apache.shindig.social.opensocial.service.SocialRequestItem} object.
+   * @return a {@link java.util.concurrent.Future} object.
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  @Operation(httpMethods="POST", bodyParam = "activity")
+  public Future<?> create(SocialRequestItem request) throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+    List<String> activityIds = request.getListParameter("activityId");
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+    HandlerPreconditions.requireEmpty(activityIds, "Cannot specify activity ID in create");
+
+    return service.createActivityEntry(Iterables.getOnlyElement(userIds), request.getGroup(),
+        request.getAppId(), request.getFields(),
+        request.getTypedParameter("activity", ActivityEntry.class),
+        request.getToken());
+  }
+
+  /**
+   * Allowed end-points:
+   *   /activitystreams/{userId}/{groupId}/{optionalActvityId}+
+   *   /activitystreams/{userId}+/{groupId}
+   *
+   * Examples:
+   *   /activitystreams/john.doe/@self/1
+   *   /activitystreams/john.doe,jane.doe/@friends
+   *
+   * @param request a {@link org.apache.shindig.social.opensocial.service.SocialRequestItem} object.
+   * @return a {@link java.util.concurrent.Future} object.
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  @Operation(httpMethods="GET")
+  public Future<?> get(SocialRequestItem request)
+      throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+    Set<String> optionalActivityIds = ImmutableSet.copyOf(request.getListParameter("activityId"));
+
+    CollectionOptions options = new CollectionOptions(request);
+
+    // Preconditions
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    if (userIds.size() > 1 && !optionalActivityIds.isEmpty()) {
+      throw new IllegalArgumentException("Cannot fetch activities by ID for multiple users");
+    }
+
+    if (!optionalActivityIds.isEmpty()) {
+      if (optionalActivityIds.size() == 1) {
+        return service.getActivityEntry(userIds.iterator().next(), request.getGroup(),
+            request.getAppId(), request.getFields(), optionalActivityIds.iterator().next(),
+            request.getToken());
+      } else {
+        return service.getActivityEntries(userIds.iterator().next(), request.getGroup(),
+            request.getAppId(), request.getFields(), options, optionalActivityIds, request.getToken());
+      }
+    }
+
+    return service.getActivityEntries(userIds, request.getGroup(),
+        request.getAppId(),
+        // TODO: add pagination and sorting support
+        // getSortBy(params), getFilterBy(params), getStartIndex(params), getCount(params),
+        request.getFields(), options, request.getToken());
+  }
+
+  /**
+   * Return a list of supported fields for the ActivityStreams endpoint
+   *
+   * @param request a {@link org.apache.shindig.protocol.RequestItem} object.
+   * @return a List of supported fields
+   */
+  @Operation(httpMethods = "GET", path="/@supportedFields")
+  public List<Object> supportedFields(RequestItem request) {
+    // TODO: Would be nice if name in config matched name of service.
+    String container = Objects.firstNonNull(request.getToken().getContainer(), ContainerConfig.DEFAULT_CONTAINER);
+    return config.getList(container,
+        "${Cur['gadgets.features'].opensocial.supportedFields.activityEntry}");
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java
new file mode 100644
index 0000000..e81a7e9
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AlbumHandler.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import com.google.common.base.Objects;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.Album;
+import org.apache.shindig.social.opensocial.spi.AlbumService;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+/**
+ * Receives and delegates requests to the OpenSocial Album service.
+ *
+ * @since 2.0.0
+ */
+@Service(name = "albums", path = "/{userId}+/{groupId}/{albumId}+")
+public class AlbumHandler {
+
+  private final AlbumService service;
+  private final ContainerConfig config;
+
+  @Inject
+  public AlbumHandler(AlbumService service, ContainerConfig config) {
+    this.service = service;
+    this.config = config;
+  }
+
+  /*
+    * Handles create operations.
+    *
+    * Allowed end-points: /albums/{userId}/@self
+    *
+    * Examples: /albums/john.doe/@self
+    */
+  @Operation(httpMethods = "POST", bodyParam = "album")
+  public Future<?> create(SocialRequestItem request) throws ProtocolException {
+    // Retrieve userIds and albumIds
+    Set<UserId> userIds = request.getUsers();
+    List<String> albumIds = request.getListParameter("albumId");
+
+    // Preconditions - exactly one userId specified, no albumIds specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+    HandlerPreconditions.requireEmpty(albumIds, "Cannot specify albumId in create");
+
+    return service.createAlbum(Iterables.getOnlyElement(userIds),
+        request.getAppId(),
+        request.getTypedParameter("album", Album.class),
+        request.getToken());
+  }
+
+  /*
+    * Handles retrieve operations.
+    *
+    * Allowed end-points: /albums/{userId}+/{groupId}/{albumId}+
+    *
+    * Examples: /albums/@me/@self /albums/john.doe/@self/1,2
+    * /albums/john.doe,jane.doe/@friends
+    */
+  @Operation(httpMethods = "GET")
+  public Future<?> get(SocialRequestItem request) throws ProtocolException {
+    // Get user, group, and album IDs
+    Set<UserId> userIds = request.getUsers();
+    Set<String> optionalAlbumIds = ImmutableSet.copyOf(request
+        .getListParameter("albumId"));
+
+    // At least one userId must be specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+
+    // If multiple userIds specified, albumIds must not be specified
+    if (userIds.size() > 1 && !optionalAlbumIds.isEmpty()) {
+      throw new IllegalArgumentException("Cannot fetch same albumIds for multiple userIds");
+    }
+
+    // Retrieve albums by ID
+    if (!optionalAlbumIds.isEmpty()) {
+      if (optionalAlbumIds.size() == 1) {
+        return service.getAlbum(Iterables.getOnlyElement(userIds),
+            request.getAppId(), request.getFields(),
+            optionalAlbumIds.iterator().next(), request.getToken());
+      } else {
+        return service.getAlbums(Iterables.getOnlyElement(userIds),
+            request.getAppId(), request.getFields(),
+            new CollectionOptions(request), optionalAlbumIds,
+            request.getToken());
+      }
+    }
+
+    // Retrieve albums by group
+    return service.getAlbums(userIds, request.getGroup(), request
+        .getAppId(), request.getFields(),
+        new CollectionOptions(request), request.getToken());
+  }
+
+  /*
+    * Handles update operations.
+    *
+    * Allowed end-points: /albums/{userId}/@self/{albumId}
+    *
+    * Examples: /albums/john.doe/@self/1
+    */
+  @Operation(httpMethods = "PUT", bodyParam = "album")
+  public Future<?> update(SocialRequestItem request) throws ProtocolException {
+    // Retrieve userIds and albumIds
+    Set<UserId> userIds = request.getUsers();
+    List<String> albumIds = request.getListParameter("albumId");
+
+    // Enforce preconditions - exactly one user and one album specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+    HandlerPreconditions.requireNotEmpty(albumIds, "No albumId specified");
+    HandlerPreconditions.requireSingular(albumIds, "Multiple albumIds not supported");
+
+    return service.updateAlbum(Iterables.getOnlyElement(userIds),
+        request.getAppId(),
+        request.getTypedParameter("album", Album.class),
+        Iterables.getOnlyElement(albumIds), request.getToken());
+  }
+
+  /*
+    * Handles delete operations.
+    *
+    * Allowed end-points: /albums/{userId}/@self/{albumId}
+    *
+    * Examples: /albums/john.doe/@self/1
+    */
+  @Operation(httpMethods = "DELETE")
+  public Future<?> delete(SocialRequestItem request) throws ProtocolException {
+    // Get user and album ID
+    Set<UserId> userIds = request.getUsers();
+    String albumId = request.getParameter("albumId");
+
+    // Enforce preconditions - userIds must contain exactly one element
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    // Service request
+    return service.deleteAlbum(Iterables.getOnlyElement(userIds),
+        request.getAppId(), albumId, request.getToken());
+  }
+
+  /*
+    * Retrieves supported fields for the albums service.
+    */
+  @Operation(httpMethods = "GET", path = "/@supportedFields")
+  public List<Object> supportedFields(RequestItem request) {
+    String container = Objects.firstNonNull(request.getToken().getContainer(),
+        ContainerConfig.DEFAULT_CONTAINER);
+    return config.getList(container,
+        "${Cur['gadgets.features'].opensocial.supportedFields.album}");
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AppDataHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AppDataHandler.java
new file mode 100644
index 0000000..3a052b7
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/AppDataHandler.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.spi.AppDataService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.inject.Inject;
+
+/**
+ * Handles REST/RPC requests for AppData
+ */
+@Service(name = "appdata", path = "/{userId}+/{groupId}/{appId}")
+public class AppDataHandler {
+
+  private final AppDataService service;
+
+  @Inject
+  public AppDataHandler(AppDataService service) {
+    this.service = service;
+  }
+
+  /**
+   * Allowed endpoints /appdata/{userId}/{groupId}/{appId} - fields={field1, field2}
+   *
+   * examples: /appdata/john.doe/@friends/app?fields=count /appdata/john.doe/@self/app
+   *
+   * The post data should be a regular json object. All of the fields vars will be pulled from the
+   * values and set on the person object. If there are no fields vars then all of the data will be
+   * overridden.
+   */
+  @Operation(httpMethods = "DELETE")
+  public Future<?> delete(SocialRequestItem request)
+      throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    return service.deletePersonData(userIds.iterator().next(), request.getGroup(),
+        request.getAppId(), request.getFields(), request.getToken());
+  }
+
+  /**
+   * Allowed endpoints /appdata/{userId}/{groupId}/{appId} - fields={field1, field2}
+   *
+   * examples: /appdata/john.doe/@friends/app?fields=count /appdata/john.doe/@self/app
+   *
+   * The post data should be a regular json object. All of the fields vars will be pulled from the
+   * values and set on the person object. If there are no fields vars then all of the data will be
+   * overridden.
+   */
+  @Operation(httpMethods = "PUT", bodyParam = "data")
+  public Future<?> update(SocialRequestItem request) throws ProtocolException {
+    return create(request);
+  }
+
+  /**
+   * /appdata/{userId}/{groupId}/{appId} - fields={field1, field2}
+   *
+   * examples: /appdata/john.doe/@friends/app?fields=count /appdata/john.doe/@self/app
+   *
+   * The post data should be a regular json object. All of the fields vars will be pulled from the
+   * values and set. If there are no fields vars then all of the data will be overridden.
+   */
+  @Operation(httpMethods = "POST", bodyParam = "data")
+  public Future<?> create(SocialRequestItem request) throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    @SuppressWarnings("unchecked")
+    // As of today, this is the only format supported by the AppData protocol
+    Map<String, Object> values = request.getTypedParameter("data", Map.class);
+    for (String key : values.keySet()) {
+      if (!isValidKey(key)) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+            "One or more of the app data keys are invalid: " + key);
+      }
+    }
+
+    return service.updatePersonData(userIds.iterator().next(), request.getGroup(),
+        request.getAppId(), request.getFields(), values, request.getToken());
+  }
+
+  /**
+   * /appdata/{userId}+/{groupId}/{appId} - fields={field1, field2}
+   *
+   * examples: /appdata/john.doe/@friends/app?fields=count /appdata/john.doe/@self/app
+   */
+  @Operation(httpMethods = "GET")
+  public Future<?> get(SocialRequestItem request) throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+
+    // Preconditions
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+
+    return service.getPersonData(userIds, request.getGroup(),
+        request.getAppId(), request.getFields(), request.getToken());
+  }
+
+  /**
+   * Determines whether the input is a valid key. Valid keys match the regular expression [\w\-\.]+.
+   * The logic is not done using java.util.regex.* as that is 20X slower.
+   *
+   * @param key the key to validate.
+   * @return true if the key is a valid appdata key, false otherwise.
+   */
+  public static boolean isValidKey(String key) {
+    if (key == null || key.length() == 0) {
+      return false;
+    }
+    for (int i = 0; i < key.length(); ++i) {
+      char c = key.charAt(i);
+      if ((c >= 'a' && c <= 'z') ||
+          (c >= 'A' && c <= 'Z') ||
+          (c >= '0' && c <= '9') ||
+          (c == '-') ||
+          (c == '_') ||
+          (c == '.')) {
+        continue;
+      }
+      return false;
+    }
+    return true;
+  }
+
+}
+
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/GroupHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/GroupHandler.java
new file mode 100644
index 0000000..2426e91
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/GroupHandler.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.GroupService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.inject.Inject;
+
+
+/**
+ * RPC/REST handler for groups requests
+ * @since 2.0.0
+ */
+@Service(name = "groups", path = "/{userId}")
+public class GroupHandler {
+
+  private final GroupService service;
+
+  @Inject
+  public GroupHandler(GroupService service) {
+    this.service = service;
+  }
+
+  @Operation(httpMethods = "GET")
+  public Future<?> get(SocialRequestItem request) throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+    CollectionOptions options = new CollectionOptions(request);
+
+    // Preconditions
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Only one userId must be specified");
+
+    return service.getGroups(userIds.iterator().next(), options, request.getFields(), request.getToken());
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java
new file mode 100644
index 0000000..63d9902
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MediaItemHandler.java
@@ -0,0 +1,216 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import com.google.common.base.Objects;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.MediaItemService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.inject.Inject;
+
+/**
+ * Receives and delegates requests to the OpenSocial MediaItems service.
+ *
+ * @since 2.0.0
+ */
+@Service(name = "mediaItems", path = "/{userId}+/{groupId}/{albumId}/{mediaItemId}+")
+public class MediaItemHandler {
+
+
+  private final MediaItemService service;
+  private final ContainerConfig config;
+
+  @Inject
+  public MediaItemHandler(MediaItemService service, ContainerConfig config) {
+    this.service = service;
+    this.config = config;
+  }
+
+  /*
+    * Handles GET operations.
+    *
+    * Allowed end-points: /mediaItems/{userId}+/{groupId}/{albumId}/{mediaItemId}+
+    *
+    * Examples: /mediaItems/john.doe/@self
+    *           /mediaItems/john.doe,jane.doe/@self
+    *           /mediaItems/john.doe/@self/album123
+    *           /mediaItems/john.doe/@self/album123/1,2,3
+    */
+  @Operation(httpMethods = "GET")
+  public Future<?> get(SocialRequestItem request) throws ProtocolException {
+    // Get user, group, album IDs, and MediaItem IDs
+    Set<UserId> userIds = request.getUsers();
+    Set<String> optionalAlbumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+    Set<String> optionalMediaItemIds = ImmutableSet.copyOf(request.getListParameter("mediaItemId"));
+
+    // At least one userId must be specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No user ID specified");
+
+    // Get Album ID; null if not provided
+    String albumId = null;
+    if (optionalAlbumIds.size() == 1) {
+      albumId = Iterables.getOnlyElement(optionalAlbumIds);
+    } else if (optionalAlbumIds.size() > 1) {
+      throw new IllegalArgumentException("Multiple Album IDs not supported");
+    }
+
+    // Cannot retrieve by ID if album ID not provided
+    if (albumId == null && !optionalMediaItemIds.isEmpty()) {
+      throw new IllegalArgumentException("Cannot fetch by MediaItem ID without Album ID");
+    }
+
+    // Cannot retrieve by ID or album if multiple user's given
+    if (userIds.size() > 1) {
+      if (!optionalMediaItemIds.isEmpty()) {
+        throw new IllegalArgumentException("Cannot fetch MediaItem by ID for multiple users");
+      } else if (albumId != null) {
+        throw new IllegalArgumentException("Cannot fetch MediaItem by Album for multiple users");
+      }
+    }
+
+    // Retrieve by ID(s)
+    if (!optionalMediaItemIds.isEmpty()) {
+      if (optionalMediaItemIds.size() == 1) {
+        return service.getMediaItem(Iterables.getOnlyElement(userIds),
+            request.getAppId(), albumId,
+            Iterables.getOnlyElement(optionalMediaItemIds),
+            request.getFields(), request.getToken());
+      } else {
+        return service.getMediaItems(Iterables.getOnlyElement(userIds),
+            request.getAppId(), albumId, optionalMediaItemIds,
+            request.getFields(), new CollectionOptions(request),
+            request.getToken());
+      }
+    }
+
+    // Retrieve by Album
+    if (albumId != null) {
+      return service.getMediaItems(Iterables.getOnlyElement(userIds),
+          request.getAppId(), albumId, request.getFields(),
+          new CollectionOptions(request), request.getToken());
+    }
+
+    // Retrieve by users and groups
+    return service.getMediaItems(userIds, request.getGroup(), request
+        .getAppId(), request.getFields(),
+        new CollectionOptions(request), request.getToken());
+  }
+
+  /*
+    * Handles DELETE operations.
+    *
+    * Allowed end-points: /mediaItem/{userId}/@self/{albumId}/{mediaItemId}
+    *
+    * Examples: /mediaItems/john.doe/@self/1/2
+    */
+  @Operation(httpMethods = "DELETE")
+  public Future<?> delete(SocialRequestItem request) throws ProtocolException {
+    // Get users, Album ID, and MediaItem ID
+    Set<UserId> userIds = request.getUsers();
+    Set<String> albumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+    Set<String> mediaItemIds = ImmutableSet.copyOf(request.getListParameter("mediaItemId"));
+
+    // Exactly one user, Album, and MediaItem must be specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Exactly one user ID must be specified");
+    HandlerPreconditions.requireSingular(albumIds, "Exactly one Album ID must be specified");
+    HandlerPreconditions.requireSingular(mediaItemIds, "Exactly one MediaItem ID must be specified");
+
+    // Service request
+    return service.deleteMediaItem(Iterables.getOnlyElement(userIds),
+        request.getAppId(), Iterables.getOnlyElement(albumIds),
+        Iterables.getOnlyElement(mediaItemIds), request.getToken());
+  }
+
+  /*
+    * Handles POST operations.
+    *
+    * Allowed end-points: /mediaItems/{userId}/@self/{albumId}
+    *
+    * Examples: /mediaItems/john.doe/@self/1
+    */
+  @Operation(httpMethods = "POST", bodyParam = "mediaItem")
+  public Future<?> create(SocialRequestItem request) throws ProtocolException {
+    // Retrieve userIds and albumIds
+    Set<UserId> userIds = request.getUsers();
+    Set<String> albumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+
+    // Exactly one user and Album must be specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Exactly one user ID must be specified");
+    HandlerPreconditions.requireSingular(albumIds, "Exactly one Album ID must be specified");
+
+    // Service request
+    return service.createMediaItem(Iterables.getOnlyElement(userIds),
+        request.getAppId(), Iterables.getOnlyElement(albumIds),
+        request.getTypedParameter("mediaItem", MediaItem.class),
+        request.getToken());
+  }
+
+  /*
+    * Handles PUT operations.
+    *
+    * Allowed end-points: /mediaItems/{userId}/@self/{albumId}/{mediaItemId}
+    *
+    * Examples: /mediaItems/john.doe/@self/1/2
+    */
+  @Operation(httpMethods = "PUT", bodyParam = "mediaItem")
+  public Future<?> update(SocialRequestItem request) throws ProtocolException {
+    // Retrieve userIds, albumIds, and mediaItemIds
+    Set<UserId> userIds = request.getUsers();
+    Set<String> albumIds = ImmutableSet.copyOf(request.getListParameter("albumId"));
+    Set<String> mediaItemIds = ImmutableSet.copyOf(request.getListParameter("mediaItemId"));
+
+    // Exactly one user, Album, and MediaItem must be specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Exactly one user ID must be specified");
+    HandlerPreconditions.requireSingular(albumIds, "Exactly one Album ID must be specified");
+    HandlerPreconditions.requireSingular(mediaItemIds, "Exactly one MediaItem ID must be specified");
+
+    // Service request
+    return service.updateMediaItem(Iterables.getOnlyElement(userIds),
+        request.getAppId(), Iterables.getOnlyElement(albumIds),
+        Iterables.getOnlyElement(mediaItemIds),
+        request.getTypedParameter("mediaItem", MediaItem.class),
+        request.getToken());
+  }
+
+  @Operation(httpMethods = "GET", path = "/@supportedFields")
+  public List<Object> supportedFields(RequestItem request) {
+    // TODO: Would be nice if name in config matched name of service.
+    String container = Objects.firstNonNull(request.getToken().getContainer(),
+        ContainerConfig.DEFAULT_CONTAINER);
+    return config.getList(container,
+        "${Cur['gadgets.features'].opensocial.supportedFields.mediaItem}");
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MessageHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MessageHandler.java
new file mode 100644
index 0000000..826d7e5
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/MessageHandler.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.Message;
+import org.apache.shindig.social.opensocial.model.MessageCollection;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.MessageService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.inject.Inject;
+
+/**
+ * RPC/REST handler for all Messages requests
+ */
+@Service(name = "messages", path="/{userId}+/{msgCollId}/{messageIds}+")
+public class MessageHandler {
+
+  private final MessageService service;
+
+  @Inject
+  public MessageHandler(MessageService service) {
+    this.service = service;
+  }
+
+  @Operation(httpMethods = "DELETE")
+  public Future<?> delete(SocialRequestItem request) throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+    String msgCollId = request.getParameter("msgCollId");
+    List<String> messageIds = request.getListParameter("messageIds");
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    if (msgCollId == null) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+          "A message collection is required");
+    }
+
+    HandlerPreconditions.requireNotEmpty(messageIds, "No message IDs specified");
+
+    UserId user = request.getUsers().iterator().next();
+
+    return service.deleteMessages(user, msgCollId, messageIds, request.getToken());
+  }
+
+
+  @Operation(httpMethods = "GET")
+  public Future<?> get(SocialRequestItem request) throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+    String msgCollId = request.getParameter("msgCollId");
+    List<String> messageIds = request.getListParameter("messageIds");
+
+    CollectionOptions options = new CollectionOptions(request);
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    UserId user = request.getUsers().iterator().next();
+
+    if (msgCollId == null) {
+      // No message collection specified, return list of message collections
+      Set<String> fields = request.getFields(MessageCollection.Field.ALL_FIELDS);
+      return service.getMessageCollections(user, fields, options, request.getToken());
+    }
+    // If messageIds are specified return them, otherwise return entries in the given collection.
+    Set<String> fields = request.getFields(Message.Field.ALL_FIELDS);
+    return service.getMessages(user, msgCollId, fields, messageIds, options, request.getToken());
+  }
+
+  /**
+   * Creates a new message collection or message
+   */
+  @Operation(httpMethods = "POST", bodyParam = "entity")
+  public Future<?> create(SocialRequestItem request) throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+    String msgCollId = request.getParameter("msgCollId");
+    List<String> messageIds = request.getListParameter("messageIds");
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    UserId user = request.getUsers().iterator().next();
+
+
+    if (msgCollId == null) {
+      // Request to create a new message collection
+      MessageCollection msgCollection = request.getTypedParameter("entity", MessageCollection.class);
+
+      return service.createMessageCollection(user, msgCollection, request.getToken());
+    }
+
+    // A message collection has been specified, allow for posting
+
+    HandlerPreconditions.requireEmpty(messageIds,"Message IDs not allowed here, use PUT instead");
+
+    Message message = request.getTypedParameter("entity", Message.class);
+    HandlerPreconditions.requireNotEmpty(message.getRecipients(), "No recipients specified");
+
+    return service.createMessage(userIds.iterator().next(), request.getAppId(), msgCollId, message,
+        request.getToken());
+  }
+
+  /**
+   * Handles modifying a message or a message collection.
+   */
+  @Operation(httpMethods = "PUT", bodyParam = "entity")
+  public Future<?> modify(SocialRequestItem request) throws ProtocolException {
+
+    Set<UserId> userIds = request.getUsers();
+    String msgCollId = request.getParameter("msgCollId");
+    List<String> messageIds = request.getListParameter("messageIds");
+
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    UserId user = request.getUsers().iterator().next();
+
+    if (msgCollId == null) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+          "A message collection is required");
+    }
+
+    if (messageIds.isEmpty()) {
+      // No message IDs specified, this is a PUT to a message collection
+      MessageCollection msgCollection = request.getTypedParameter("entity", MessageCollection.class);
+      if (msgCollection == null) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+            "cannot parse message collection");
+      }
+
+      // TODO, do more validation.
+
+      return service.modifyMessageCollection(user, msgCollection, request.getToken());
+    }
+
+    HandlerPreconditions.requireSingular(messageIds, "Only one messageId at a time");
+
+    Message message = request.getTypedParameter("entity", Message.class);
+    // TODO, do more validation.
+
+    if (message == null || message.getId() == null) {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST,
+        "cannot parse message or missing ID");
+    }
+
+    return service.modifyMessage(user, msgCollId, messageIds.get(0), message, request.getToken());
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/PersonHandler.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/PersonHandler.java
new file mode 100644
index 0000000..18b7a33
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/PersonHandler.java
@@ -0,0 +1,162 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.HandlerPreconditions;
+import org.apache.shindig.protocol.Operation;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.Service;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.PersonService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.Futures;
+
+import com.google.inject.Inject;
+
+/**
+ * RPC/REST handler for all /people requests
+ */
+@Service(name = "people", path = "/{userId}+/{groupId}/{personId}+")
+public class PersonHandler {
+  private final PersonService personService;
+  private final ContainerConfig config;
+
+
+  // Return a future for the first item of a collection
+  private static <T> Future<T> firstItem(Future<RestfulCollection<T>> collection) {
+    Function<RestfulCollection<T>, T> firstItem = new Function<RestfulCollection<T>, T>() {
+      @Override
+      public T apply(RestfulCollection<T> c) {
+        if (c != null && c.getTotalResults() > 0) {
+          return c.getList().get(0);
+        }
+        return null;
+      };
+    };
+    return Futures.lazyTransform(collection, firstItem);
+ }
+
+  @Inject
+  public PersonHandler(PersonService personService, ContainerConfig config) {
+    this.personService = personService;
+    this.config = config;
+  }
+
+  /**
+   * Allowed end-points /people/{userId}+/{groupId} /people/{userId}/{groupId}/{optionalPersonId}+
+   *
+   * examples: /people/john.doe/@all /people/john.doe/@friends /people/john.doe/@self
+   */
+  @Operation(httpMethods = "GET")
+  public Future<?> get(SocialRequestItem request) throws ProtocolException {
+    GroupId groupId = request.getGroup();
+    Set<String> optionalPersonId = ImmutableSet.copyOf(request.getListParameter("personId"));
+    Set<String> fields = request.getFields(Person.Field.DEFAULT_FIELDS);
+    Set<UserId> userIds = request.getUsers();
+
+    // Preconditions
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    if (userIds.size() > 1 && !optionalPersonId.isEmpty()) {
+      throw new IllegalArgumentException("Cannot fetch personIds for multiple userIds");
+    }
+
+    CollectionOptions options = new CollectionOptions(request);
+
+    if (userIds.size() == 1) {
+      if (optionalPersonId.isEmpty()) {
+        if (groupId.getType() == GroupId.Type.self) {
+            // If a filter is set then we have to call getPeople(), otherwise use the simpler getPerson()
+          if (options.getFilter() != null) {
+            Future<RestfulCollection<Person>> people = personService.getPeople(
+                userIds, groupId, options, fields, request.getToken());
+            return firstItem(people);
+          } else {
+            return personService.getPerson(userIds.iterator().next(), fields, request.getToken());
+          }
+        } else {
+          return personService.getPeople(userIds, groupId, options, fields, request.getToken());
+        }
+      } else if (optionalPersonId.size() == 1) {
+        // TODO: Add some crazy concept to handle the userId?
+        Set<UserId> optionalUserIds = ImmutableSet.of(
+            new UserId(UserId.Type.userId, optionalPersonId.iterator().next()));
+
+        Future<RestfulCollection<Person>> people = personService.getPeople(
+            optionalUserIds, new GroupId(GroupId.Type.self, null),
+            options, fields, request.getToken());
+        return firstItem(people);
+      } else {
+        ImmutableSet.Builder<UserId> personIds = ImmutableSet.builder();
+        for (String pid : optionalPersonId) {
+          personIds.add(new UserId(UserId.Type.userId, pid));
+        }
+        // Every other case is a collection response of optional person ids
+        return personService.getPeople(personIds.build(), new GroupId(GroupId.Type.self, null),
+            options, fields, request.getToken());
+      }
+    }
+
+    // Every other case is a collection response.
+    return personService.getPeople(userIds, groupId, options, fields, request.getToken());
+  }
+
+  /**
+   * Allowed end-points /people/{userId}/{groupId}
+   *
+   * examples: /people/john.doe/@all /people/john.doe/@friends /people/john.doe/@self
+   */
+  @Operation(httpMethods = "PUT", bodyParam = "person")
+  public Future<?> update(SocialRequestItem request) throws ProtocolException {
+    Set<UserId> userIds = request.getUsers();
+
+    // Enforce preconditions - exactly one user is specified
+    HandlerPreconditions.requireNotEmpty(userIds, "No userId specified");
+    HandlerPreconditions.requireSingular(userIds, "Multiple userIds not supported");
+
+    UserId userId = userIds.iterator().next();
+
+    // Update person and return it
+    return personService.updatePerson(Iterables.getOnlyElement(userIds),
+        request.getTypedParameter("person", Person.class),
+        request.getToken());
+  }
+
+  @Operation(httpMethods = "GET", path="/@supportedFields")
+  public List<Object> supportedFields(RequestItem request) {
+    // TODO: Would be nice if name in config matched name of service.
+    String container = Objects.firstNonNull(request.getToken().getContainer(), "default");
+    return config.getList(container,
+        "${Cur['gadgets.features'].opensocial.supportedFields.person}");
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/SocialRequestItem.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/SocialRequestItem.java
new file mode 100644
index 0000000..14de390
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/SocialRequestItem.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Preconditions;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.BaseRequestItem;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.multipart.FormDataItem;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.PersonService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+import org.json.JSONObject;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * Subclass with social specific extensions
+ */
+public class SocialRequestItem extends BaseRequestItem {
+
+  private static final String USER_ID = "userId";
+  private static final String GROUP_ID = "groupId";
+
+  public SocialRequestItem(Map<String, String[]> parameters,
+      SecurityToken token, BeanConverter converter, BeanJsonConverter jsonConverter) {
+    super(parameters, token, converter, jsonConverter);
+  }
+
+  public SocialRequestItem(JSONObject parameters, Map<String, FormDataItem> formItems,
+      SecurityToken token, BeanConverter converter, BeanJsonConverter jsonConverter) {
+    super(parameters, formItems, token, converter, jsonConverter);
+  }
+
+  public Set<UserId> getUsers() {
+    List<String> ids = getListParameter(USER_ID);
+    if (ids.isEmpty()) {
+      Preconditions.checkArgument(token.getViewerId() != null, "No userId provided and viewer not available");
+      // Assume @me
+      return ImmutableSet.of(UserId.fromJson("@me"));
+    }
+    ImmutableSet.Builder<UserId> userIds = ImmutableSet.builder();
+    for (String id : ids) {
+      userIds.add(UserId.fromJson(id));
+    }
+    return userIds.build();
+  }
+
+  public GroupId getGroup() {
+    return GroupId.fromJson(getParameter(GROUP_ID, "@self"));
+  }
+
+  public String getSortBy() {
+    String sortBy = super.getSortBy();
+    return sortBy == null ? PersonService.TOP_FRIENDS_SORT : sortBy;
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/package-info.java
new file mode 100644
index 0000000..d754c76
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/service/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>The Social Service API package</h1>
+<p>Service classes and interfaces that bind to the HTTP transport
+and provide routing and handling of the REST requests. Containers may
+extend the DataServiceServlet and other classes.</p>
+*/
+
+package org.apache.shindig.social.opensocial.service;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ActivityService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ActivityService.java
new file mode 100644
index 0000000..30b1c41
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ActivityService.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.Activity;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+/**
+ * The ActivityService interface defines the service provider interface to retrieve activities from
+ * the underlying SNS.
+ */
+public interface ActivityService {
+
+  /**
+   * Returns a list of activities that correspond to the passed in users and group.
+   *
+   * @param userIds The set of ids of the people to fetch activities for.
+   * @param groupId Indicates whether to fetch activities for a group.
+   * @param appId   The app id.
+   * @param fields  The fields to return. Empty set implies all
+   * @param options The sorting/filtering/pagination options
+   * @param token   A valid SecurityToken
+   * @return a response item with the list of activities.
+   */
+  Future<RestfulCollection<Activity>> getActivities(Set<UserId> userIds,
+      GroupId groupId, String appId, Set<String> fields, CollectionOptions options, SecurityToken token)
+      throws ProtocolException;
+
+  /**
+   * Returns a set of activities for the passed in user and group that corresponds to a list of
+   * activityIds.
+   *
+   * @param userId      The set of ids of the people to fetch activities for.
+   * @param groupId     Indicates whether to fetch activities for a group.
+   * @param appId       The app id.
+   * @param fields      The fields to return. Empty set implies all
+   * @param options The sorting/filtering/pagination options
+   * @param activityIds The set of activity ids to fetch.
+   * @param token       A valid SecurityToken
+   * @return a response item with the list of activities.
+   */
+  Future<RestfulCollection<Activity>> getActivities(UserId userId, GroupId groupId,
+      String appId, Set<String> fields, CollectionOptions options, Set<String> activityIds, SecurityToken token)
+      throws ProtocolException;
+
+
+  /**
+   * Returns an activity for the passed in user and group that corresponds to a single
+   * activityId.
+   *
+   * @param userId     The set of ids of the people to fetch activities for.
+   * @param groupId    Indicates whether to fetch activities for a group.
+   * @param appId      The app id.
+   * @param fields     The fields to return. Empty set implies all
+   * @param activityId The activity id to fetch.
+   * @param token      A valid SecurityToken
+   * @return a response item with the list of activities.
+   */
+  Future<Activity> getActivity(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, String activityId, SecurityToken token)
+      throws ProtocolException;
+
+  /**
+   * Deletes the activity for the passed in user and group that corresponds to the activityId.
+   *
+   * @param userId      The user.
+   * @param groupId     The group.
+   * @param appId       The app id.
+   * @param activityIds A list of activity ids to delete.
+   * @param token       A valid SecurityToken.
+   * @return a response item containing any errors
+   */
+  Future<Void> deleteActivities(UserId userId, GroupId groupId, String appId,
+      Set<String> activityIds, SecurityToken token) throws ProtocolException;
+
+  /**
+   * Creates the passed in activity for the passed in user and group. Once createActivity is called,
+   * getActivities will be able to return the Activity.
+   *
+   * @param userId   The id of the person to create the activity for.
+   * @param groupId  The group.
+   * @param appId    The app id.
+   * @param fields   The fields to return.
+   * @param activity The activity to create.
+   * @param token    A valid SecurityToken
+   * @return a response item containing any errors
+   */
+  Future<Void> createActivity(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, Activity activity, SecurityToken token) throws ProtocolException;
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ActivityStreamService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ActivityStreamService.java
new file mode 100644
index 0000000..2cfd7ad
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ActivityStreamService.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+
+/**
+ * The ActivityStreamService interface defines the service provider interface to retrieve activities from
+ * the underlying SNS.
+ */
+public interface ActivityStreamService {
+
+  /**
+   * Returns a list of activities that correspond to the passed in users and group.
+   *
+   * @param userIds The set of ids of the people to fetch activities for.
+   * @param groupId Indicates whether to fetch activities for a group.
+   * @param appId   The app id.
+   * @param fields  The fields to return. Empty set implies all
+   * @param options The sorting/filtering/pagination options
+   * @param token   A valid SecurityToken
+   * @return a response item with the list of activities.
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  Future<RestfulCollection<ActivityEntry>> getActivityEntries(Set<UserId> userIds,
+      GroupId groupId, String appId, Set<String> fields, CollectionOptions options, SecurityToken token)
+      throws ProtocolException;
+
+  /**
+   * Returns a set of activities for the passed in user and group that corresponds to a list of
+   * activityIds.
+   *
+   * @param userId      The set of ids of the people to fetch activities for.
+   * @param groupId     Indicates whether to fetch activities for a group.
+   * @param appId       The app id.
+   * @param fields      The fields to return. Empty set implies all
+   * @param options     The sorting/filtering/pagination options
+   * @param activityIds The set of activity ids to fetch.
+   * @param token       A valid SecurityToken
+   * @return a response item with the list of activities.
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  Future<RestfulCollection<ActivityEntry>> getActivityEntries(UserId userId, GroupId groupId,
+      String appId, Set<String> fields, CollectionOptions options, Set<String> activityIds, SecurityToken token)
+      throws ProtocolException;
+
+
+  /**
+   * Returns an activity for the passed in user and group that corresponds to a single
+   * activityId.
+   *
+   * @param userId     The set of ids of the people to fetch activities for.
+   * @param groupId    Indicates whether to fetch activities for a group.
+   * @param appId      The app id.
+   * @param fields     The fields to return. Empty set implies all
+   * @param activityId The activity id to fetch.
+   * @param token      A valid SecurityToken
+   * @return a response item with the list of activities.
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  Future<ActivityEntry> getActivityEntry(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, String activityId, SecurityToken token)
+      throws ProtocolException;
+
+  /**
+   * Deletes the activity for the passed in user and group that corresponds to the activityId.
+   *
+   * @param userId      The user.
+   * @param groupId     The group.
+   * @param appId       The app id.
+   * @param activityIds A list of activity ids to delete.
+   * @param token       A valid SecurityToken.
+   * @return a response item containing any errors
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  Future<Void> deleteActivityEntries(UserId userId, GroupId groupId, String appId,
+      Set<String> activityIds, SecurityToken token) throws ProtocolException;
+
+  /**
+   * Updates the specified Activity.
+   *
+   * @param userId      The id of the person to update the activity for
+   * @param groupId     The group
+   * @param appId       The app id
+   * @param fields      The fields to return
+   * @param activity    The updated activity
+   * @param activityId  The id of the existing activity to update
+   * @param token       A valid SecurityToken
+   * @return a response item containing any errors
+   * @throws org.apache.shindig.protocol.ProtocolException if any
+   */
+  Future<ActivityEntry> updateActivityEntry(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, ActivityEntry activity, String activityId,
+      SecurityToken token) throws ProtocolException;
+
+  /**
+   * Creates the passed in activity for the passed in user and group. Once createActivity is called,
+   * getActivities will be able to return the Activity.
+   *
+   * @param userId   The id of the person to create the activity for.
+   * @param groupId  The group.
+   * @param appId    The app id.
+   * @param fields   The fields to return.
+   * @param activity The activity to create.
+   * @param token    A valid SecurityToken
+   * @return a response item containing any errors
+   * @throws org.apache.shindig.protocol.ProtocolException if any.
+   */
+  Future<ActivityEntry> createActivityEntry(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, ActivityEntry activity, SecurityToken token) throws ProtocolException;
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java
new file mode 100644
index 0000000..5bb952d
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AlbumService.java
@@ -0,0 +1,154 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import com.google.inject.ImplementedBy;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.Album;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * The AlbumService interface defines the service provider interface for
+ * creating, retrieving, updating, and deleting OpenSocial albums.
+ *
+ * @since 2.0.0
+ */
+@ImplementedBy(AlbumService.NotImplementedAlbumService.class)
+public interface AlbumService {
+
+  /*
+    * Retrieves a single album for the given user with the given album ID.
+    *
+    * @param userId  Identifies the person to retrieve the album from
+    * @param appId    Identifies the application to retrieve the album from
+    * @param fields  Indicates the fields to return.  Empty set implies all
+    * @param albumId  Identifies the album to retrieve
+    * @param token    A valid SecurityToken
+    *
+    * @return a response item with the requested album
+    */
+  Future<Album> getAlbum(UserId userId, String appId, Set<String> fields,
+                         String albumId, SecurityToken token) throws ProtocolException;
+
+  /*
+    * Retrieves albums for the given user with the given album IDs.
+    *
+    * @param userId  Identifies the person to retrieve albums for
+    * @param appId    Identifies the application to retrieve albums from
+    * @param fields  The fields to return; empty set implies all
+    * @param options  The sorting/filtering/pagination options
+    * @param albumIds  The set of album ids to fetch
+    * @param token    A valid SecurityToken
+    *
+    * @return a response item with requested albums
+    */
+  Future<RestfulCollection<Album>> getAlbums(UserId userId, String appId,
+                                             Set<String> fields, CollectionOptions options,
+                                             Set<String> albumIds, SecurityToken token) throws ProtocolException;
+
+  /*
+    * Retrieves albums for the given user and group.
+    *
+    * @param userIds  Identifies the users to retrieve albums from
+    * @param groupId  Identifies the group to retrieve albums from
+    * @param appId    Identifies the application to retrieve albums from
+    * @param fields   The fields to return.  Empty set implies all
+    * @param options  The sorting/filtering/pagination options
+    * @param token    A valid SecurityToken
+    *
+    * @return a response item with the requested albums
+    */
+  Future<RestfulCollection<Album>> getAlbums(Set<UserId> userIds,
+                                             GroupId groupId, String appId, Set<String> fields,
+                                             CollectionOptions options, SecurityToken token)
+      throws ProtocolException;
+
+  /*
+    * Deletes a single album for the given user with the given album ID.
+    *
+    * @param userId   Identifies the user to delete the album from
+    * @param appId    Identifies the application to delete the album from
+    * @param albumId  Identifies the album to delete
+    * @param token    A valid SecurityToken
+    *
+    * @return a response item containing any errors
+    */
+  Future<Void> deleteAlbum(UserId userId, String appId, String albumId,
+                           SecurityToken token) throws ProtocolException;
+
+  /*
+    * Creates an album for the given user.
+    *
+    * @param userId   Identifies the user to create the album for
+    * @param appId    Identifies the application to create the album in
+    * @param album    The album to create
+    * @param token    A valid SecurityToken
+    *
+    * @return a response containing any errors
+    */
+  Future<Void> createAlbum(UserId userId, String appId, Album album,
+                           SecurityToken token) throws ProtocolException;
+
+  /*
+    * Updates an album for the given user.  The album ID specified in the REST
+    * end-point is used, even if the album also defines an ID.
+    *
+    * @param userId   Identifies the user to update the album for
+    * @param appId    Identifies the application to update the album in
+    * @param album    Defines the updated album
+    * @param albumId  Identifies the ID of the album to update
+    * @param token    A valid SecurityToken
+    *
+    * @return a response containing any errors
+    */
+  Future<Void> updateAlbum(UserId userId, String appId, Album album,
+                           String albumId, SecurityToken token) throws ProtocolException;
+
+  public static class NotImplementedAlbumService implements AlbumService {
+    public Future<Album> getAlbum(UserId userId, String appId, Set<String> fields, String albumId, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<RestfulCollection<Album>> getAlbums(UserId userId, String appId, Set<String> fields, CollectionOptions options, Set<String> albumIds, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<RestfulCollection<Album>> getAlbums(Set<UserId> userIds, GroupId groupId, String appId, Set<String> fields, CollectionOptions options, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> deleteAlbum(UserId userId, String appId, String albumId, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> createAlbum(UserId userId, String appId, Album album, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> updateAlbum(UserId userId, String appId, Album album, String albumId, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AppDataService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AppDataService.java
new file mode 100644
index 0000000..5eeec62
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/AppDataService.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.DataCollection;
+import org.apache.shindig.protocol.ProtocolException;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+/**
+ * Data Service SPI interface. This interface represents is used to retrieve information bound to a
+ * person, there are methods to update and delete data.
+ */
+public interface AppDataService {
+
+  /**
+   * Retrives app data for the specified user list and group.
+   *
+   * @param userIds A set of UserIds.
+   * @param groupId The group
+   * @param appId   The app
+   * @param fields  The fields to filter the data by. Empty set implies all
+   * @param token   The security token
+   * @return The data fetched
+   */
+  Future<DataCollection> getPersonData(Set<UserId> userIds, GroupId groupId,
+      String appId, Set<String> fields, SecurityToken token) throws ProtocolException;
+
+  /**
+   * Deletes data for the specified user and group.
+   *
+   * @param userId  The user
+   * @param groupId The group
+   * @param appId   The app
+   * @param fields  The fields to delete. Empty set implies all
+   * @param token   The security token
+   * @return an error if one occurs
+   */
+  Future<Void> deletePersonData(UserId userId, GroupId groupId,
+      String appId, Set<String> fields, SecurityToken token) throws ProtocolException;
+
+  /**
+   * Updates app data for the specified user and group with the new values.
+   *
+   * @param userId  The user
+   * @param groupId The group
+   * @param appId   The app
+   * @param fields  The fields to filter the data by. Empty set implies all
+   * @param values  The values to set
+   * @param token   The security token
+   * @return an error if one occurs
+   */
+  Future<Void> updatePersonData(UserId userId, GroupId groupId,
+      String appId, Set<String> fields, Map<String, Object> values, SecurityToken token)
+      throws ProtocolException;
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/CollectionOptions.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/CollectionOptions.java
new file mode 100644
index 0000000..60bd401
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/CollectionOptions.java
@@ -0,0 +1,224 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.model.FilterOperation;
+import org.apache.shindig.protocol.model.SortOrder;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+
+/**
+ * Data structure representing many of the RPC/REST requests we receive.
+ */
+public class CollectionOptions {
+  private String sortBy;
+  private SortOrder sortOrder;
+  private String filter;
+  private FilterOperation filterOperation;
+  private String filterValue;
+  private int first;
+  private int max;
+  private Date updatedSince;
+
+  private Map<String, String> optionalParameters;
+  private static final String[] predefinedParameters= {
+      RequestItem.COUNT,
+      RequestItem.FIELDS,
+      RequestItem.FILTER_BY,
+      RequestItem.FILTER_OPERATION,
+      RequestItem.FILTER_VALUE,
+      RequestItem.SORT_BY,
+      RequestItem.SORT_ORDER,
+      RequestItem.START_INDEX,
+      "userId",
+      "groupId"
+    };
+  public CollectionOptions() {}
+
+  public CollectionOptions(RequestItem request) {
+    this.sortBy = request.getSortBy();
+    this.sortOrder = request.getSortOrder();
+    this.setFilter(request.getFilterBy());
+    this.setFilterOperation(request.getFilterOperation());
+    this.setFilterValue(request.getFilterValue());
+    this.setFirst(request.getStartIndex());
+    this.setMax(request.getCount());
+    this.setUpdatedSince(request.getUpdatedSince());
+    Set<String> parameterNames = Sets.newHashSet(request.getParameterNames());
+    parameterNames.removeAll(Arrays.asList(predefinedParameters));
+    Map<String, String> optionalParameters = Maps.newHashMap();
+    for (String parameter : parameterNames) {
+      optionalParameters.put(parameter, request.getParameter(parameter));
+    }
+    this.setOptionalParameters(optionalParameters);
+  }
+  /**
+   * This sortBy can be any field of the object being sorted or the special js sort of topFriends.
+   * @return The field to sort by
+   */
+  public String getSortBy() {
+    return sortBy;
+  }
+
+  public void setSortBy(String sortBy) {
+    this.sortBy = sortBy;
+  }
+
+  public SortOrder getSortOrder() {
+    return sortOrder;
+  }
+
+  public void setSortOrder(SortOrder sortOrder) {
+    this.sortOrder = sortOrder;
+  }
+
+  /**
+   * <p>
+   * This filter can be any field of the object being filtered or the special js filters,
+   * hasApp or topFriends.
+   * Other special Filters are
+   * </p>
+   * <dl>
+   * <dt>all</dt>
+   * <dd>Retrieves all friends</dd>
+   * <dt>hasApp</dt>
+   * <dd>Retrieves all friends with any data for this application.</dd>
+   * <dt>'topFriends</dt>
+   * <dd>Retrieves only the user's top friends.</dd>
+   * <dt>isFriendsWith</dt>
+   * <dd>Will filter the people requested by checking if they are friends with
+   * the given <a href="opensocial.IdSpec.html">idSpec</a>. Expects a
+   *    filterOptions parameter to be passed with the following fields defined:
+   *  - idSpec The <a href="opensocial.IdSpec.html">idSpec</a> that each person
+   *        must be friends with.</dd>
+   * </dl>
+   * @return The field to filter by
+   */
+  public String getFilter() {
+    return filter;
+  }
+
+  public void setFilter(String filter) {
+    this.filter = filter;
+  }
+
+  public FilterOperation getFilterOperation() {
+    return filterOperation;
+  }
+
+  public void setFilterOperation(FilterOperation filterOperation) {
+    this.filterOperation = filterOperation;
+  }
+
+  /**
+   * Where a field filter has been specified (ie a non special filter) then this is the value of the
+   * filter. The exception is the isFriendsWith filter where this contains the value of the id who
+   * the all the results need to be friends with.
+   *
+   * @return the filter value
+   */
+  public String getFilterValue() {
+    return filterValue;
+  }
+
+  public void setFilterValue(String filterValue) {
+    this.filterValue = filterValue;
+  }
+
+  /**
+   * When paginating, the index of the first item to fetch.
+   * @return the value of first
+   */
+  public int getFirst() {
+    return first;
+  }
+
+  public void setFirst(int first) {
+    this.first = first;
+  }
+
+
+  /**
+   * The maximum number of items to fetch; defaults to 20. If set to a larger
+   * number, a container may honor the request, or may limit the number to a
+   * container-specified limit of at least 20.
+   * @return the value of max
+   */
+  public int getMax() {
+    return max;
+  }
+
+  public void setMax(int max) {
+    this.max = max;
+  }
+
+  public Date getUpdatedSince() {
+    return updatedSince;
+  }
+
+  public void setUpdatedSince(Date updatedSince) {
+    this.updatedSince = updatedSince;
+  }
+
+
+  // These are overriden so that EasyMock doesn't throw a fit
+  @Override
+  public boolean equals(final Object o) {
+    if (o == this) {
+      return true;
+    }
+
+    if (!(o instanceof CollectionOptions)) {
+      return false;
+    }
+
+    CollectionOptions actual = (CollectionOptions) o;
+    return Objects.equal(this.sortBy, actual.sortBy)
+        && this.sortOrder == actual.sortOrder
+        && Objects.equal(this.filter, actual.filter)
+        && this.filterOperation == actual.filterOperation
+        && Objects.equal(this.filterValue, actual.filterValue)
+        && this.first == actual.first
+        && this.max == actual.max;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.sortBy, this.sortOrder, this.filter,
+        this.filterOperation, this.filterValue, this.first, this.max);
+  }
+
+  public Map<String, String> getOptionalParameter() {
+    return this.optionalParameters;
+  }
+
+  private void setOptionalParameters(Map<String, String> optionalParameters) {
+    this.optionalParameters = optionalParameters;
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/DomainName.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/DomainName.java
new file mode 100644
index 0000000..7d12df5
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/DomainName.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.regex.Pattern;
+
+import com.google.common.base.Objects;
+
+/**
+ * Domain-Name as defined by the OpenSocial 2.0.1 Spec
+ * @see http://opensocial-resources.googlecode.com/svn/spec/2.0.1/Core-Data.xml#Domain-Name
+ */
+public class DomainName {
+
+  private String domainName;
+  private static final Pattern domainNamePattern = Pattern.compile("[\\w.-]*");
+
+  /**
+   * Constructor for DomainName
+   *
+   * @param domainName String to try and create DomainName from
+   * @throws IllegalArgumentException
+   */
+  public DomainName(String domainName) throws IllegalArgumentException {
+    setDomainName(domainName);
+  }
+
+  /**
+   * Validates the domain name meets the spec definition.
+   *
+   * @return boolean Is a valid domain name by spec definition?
+   */
+  private boolean validate(String domainName) {
+	  return domainNamePattern.matcher(domainName).matches();
+  }
+
+  /**
+   * Get the domainName.
+   *
+   * @return domainName String
+   */
+  public String getDomainName() {
+    return this.domainName;
+  }
+
+  /**
+   * Set the domainName after validating its format.
+   *
+   * @param domainName String
+   * @return boolean If succeeded
+   * @throws IllegalArgumentException
+   */
+  public boolean setDomainName(String domainName) throws IllegalArgumentException {
+    if(validate(domainName)) {
+      this.domainName = domainName;
+      return true;
+    } else {
+      throw new IllegalArgumentException("The provided DomainName is not valid");
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof DomainName)) {
+      return false;
+    }
+    DomainName actual = (DomainName) o;
+    return this.getDomainName().equals(actual.getDomainName());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.domainName);
+  }
+
+  @Override
+  public String toString() {
+    return this.domainName;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GlobalId.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GlobalId.java
new file mode 100644
index 0000000..078c93a
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GlobalId.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import com.google.common.base.Objects;
+
+/**
+ * GlobalId as defined by the OpenSocial 2.0.1 Spec
+ * @see http://opensocial-resources.googlecode.com/svn/spec/2.0.1/Core-Data.xml#Global-Id
+ */
+public class GlobalId {
+
+  private DomainName domainName;
+  private LocalId localId;
+
+  /**
+   * Try to construct a GlobalId with a string that contains a valid
+   * DomainName and valid LocalId separated by a colon (:).
+   *
+   * @param globalId String to try and create GlobalId from
+   * @throws IllegalArgumentException when the globalId provided is not valid and
+   *   cannot be parsed into a valid DomainName and/or LocalId
+   */
+  public GlobalId(String globalId) throws IllegalArgumentException {
+    try {
+      String[] gid = globalId.split(":");
+      if(gid.length != 2) {
+        throw new IllegalArgumentException("The provided GlobalId is not valid");
+      }
+      this.domainName = new DomainName(gid[0]);
+      this.localId = new LocalId(gid[1]);
+    } catch(IllegalArgumentException e) {
+      throw new IllegalArgumentException("The provided GlobalId is not valid");
+    }
+  }
+
+  /**
+   * Construct a GlobalId with the provided a valid DomainName and LocalId
+   *
+   * @param domainName DomainName object
+   * @param localId LocalId object
+   */
+  public GlobalId(DomainName domainName, LocalId localId) {
+    this.domainName = domainName;
+    this.localId = localId;
+  }
+
+  /**
+   * Try and construct a GlobalId given a string for a DomainName and a string for a LocalId
+   *
+   * @param domainName String to try and create DomainName from
+   * @param localId String to try and create LocalId from
+   * @throws IllegalArgumentException
+   */
+  public GlobalId(String domainName, String localId) throws IllegalArgumentException {
+    this.domainName = new DomainName(domainName);
+    this.localId = new LocalId(localId);
+  }
+
+  /**
+   * Get the domainName
+   *
+   * @return domainName DomainName
+   */
+  public DomainName getDomainName() {
+    return this.domainName;
+  }
+
+  /**
+   * Set the domainName with a DomainName
+   *
+   * @param domainName DomainName
+   */
+  public void setDomainName(DomainName domainName) {
+    this.domainName = domainName;
+  }
+
+  /**
+   * Set the domainName with a String
+   *
+   * @param domainName String
+   * @throws IllegalArgumentException
+   */
+  public void setDomainName(String domainName) throws IllegalArgumentException {
+    this.domainName = new DomainName(domainName);
+  }
+
+  /**
+   * Get the localId
+   *
+   * @return localId LocalId
+   */
+  public LocalId getLocalId() {
+    return this.localId;
+  }
+
+  /**
+   * Set the localId with a LocalId
+   *
+   * @param localId LocalId
+   */
+  public void setLocalId(LocalId localId) {
+    this.localId = localId;
+  }
+
+  /**
+   * Set the localId with a String
+   *
+   * @param localId String
+   */
+  public void setLocalId(String localId) {
+    this.localId = new LocalId(localId);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof GlobalId)) {
+      return false;
+    }
+    GlobalId actual = (GlobalId) o;
+    return this.getDomainName().equals(actual.getDomainName())
+        && this.getLocalId().equals(actual.getLocalId());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.domainName, this.localId);
+  }
+
+  @Override
+  public String toString() {
+    return domainName + ":" + localId.toString();
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GroupId.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GroupId.java
new file mode 100644
index 0000000..f0d09a8
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GroupId.java
@@ -0,0 +1,205 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import com.google.common.base.Objects;
+
+/**
+ * GroupId as defined by the OpenSocial 2.0.1 Spec
+ * @see "http://opensocial-resources.googlecode.com/svn/spec/2.0.1/Social-Data.xml#Group-Id"
+ */
+public class GroupId {
+
+  public enum Type {
+    objectId  (0),
+    self      (1),
+    friends   (2),
+    all       (3),
+    custom   (4);
+
+    private final int id;
+    Type(int id) { this.id = id; }
+    public int getValue() { return id; }
+  }
+
+  // Can be of type ObjectId or a String that represents a GroupId.Type
+  private Object objectId;
+
+  /**
+   * If the given object is an objectId, create and store
+   * Else we need the string representation to store, including the "@"
+   *
+   * @param objectId Object
+   * @throws IllegalArgumentException
+   */
+  public GroupId(Object objectId) throws IllegalArgumentException {
+    if(objectId == null) {
+      this.objectId = new ObjectId("");
+    // If it is an objectId, store as is
+    } else if(objectId instanceof ObjectId) {
+      this.objectId = (ObjectId) objectId;
+    // Else it must be a string, store as such
+    } else {
+      if(Type.objectId.equals(getType(objectId))) {
+        this.objectId = new ObjectId(objectId.toString());
+      } else {
+        this.objectId = objectId.toString();
+      }
+    }
+  }
+
+  /**
+   * Backwards Compatibility.
+   *
+   * @param type
+   * @param objectId
+   * @throws IllegalArgumentException when the provided objectId is not valid
+   */
+  public GroupId(Type type, String objectId) throws IllegalArgumentException {
+    // If Type is an objectId, convert objectId to ObjectId and store
+    if(type.equals(Type.objectId)) {
+      this.objectId = new ObjectId(objectId);
+    // Else store the string representation of the type
+    } else if(Type.custom.equals(type)){
+      //Custom @ id
+      this.objectId = objectId;
+    } else {
+      this.objectId = typeToString(type);
+    }
+  }
+
+  /**
+   * Convert a type to string
+   *
+   * @param type GroupId.Type to convert
+   * @return JSON string value
+   */
+  private String typeToString(Type type) {
+    if(Type.all.equals(type)) {
+      return "@all";
+    } else if(Type.friends.equals(type)) {
+      return "@friends";
+    } else {
+      return "@self";
+    }
+  }
+
+  /**
+   * Get the type of the stored objectId.
+   *
+   * @return GroupId.Type
+   */
+  public Type getType() {
+    return getType(this.objectId);
+  }
+
+  /**
+   * Get the type of the given objectId.
+   *
+   * @return GroupId.Type
+   */
+  private Type getType(Object objectId) {
+	String type = parseType(objectId);
+    if(type.equals("self")) {
+      return Type.self;
+    } else if(type.equals("friends")) {
+      return Type.friends;
+    } else if(type.equals("all")) {
+      return Type.all;
+    } else if(objectId instanceof String && ((String)objectId).startsWith("@")) {
+    	// Could be a custom @ id, and it certainly is not an object id
+    	// return null we don't know the type
+    	return Type.custom;
+    } else {
+      return Type.objectId;
+    }
+  }
+
+  /**
+   * Parse the type of the provided objectId.
+   *
+   * @param objectId Object to parse
+   * @return type String
+   */
+  private String parseType(Object objectId) {
+    if(objectId instanceof String) {
+      String o = (String) objectId;
+      // Remove the "@"
+      return o.substring(1, o.length());
+    } else {
+      return "";
+    }
+  }
+
+  /**
+   * Set the objectId with a String
+   *
+   * @param objectId String
+   * @throws IllegalArgumentException
+   */
+  public void setObjectId(String objectId) throws IllegalArgumentException {
+    if(getType(objectId).equals(Type.objectId)) {
+      this.objectId = new ObjectId(objectId);
+    } else {
+      this.objectId = objectId;
+    }
+  }
+
+  /**
+   * Get the objectId
+   *
+   * @return objectId Object
+   */
+  public Object getObjectId() {
+    return this.objectId;
+  }
+
+  /**
+   * Backwards compatibility.
+   *
+   * @param jsonId JSON string value of a GroupId
+   * @return GroupId
+   * @throws IllegalArgumentException
+   */
+  public static GroupId fromJson(String jsonId) throws IllegalArgumentException {
+    return new GroupId(jsonId);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof GroupId)) {
+      return false;
+    }
+    GroupId actual = (GroupId) o;
+    return this.objectId.equals(actual.objectId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.objectId);
+  }
+
+  @Override
+  public String toString() {
+    return this.objectId.toString();
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GroupService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GroupService.java
new file mode 100644
index 0000000..15f7b31
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/GroupService.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.Group;
+
+/**
+ * A service for gathering group information for specific users.
+ *
+ * @since 2.0.0
+ */
+public interface GroupService {
+  /**
+   * @param userId  a userId object
+   * @param options search/sort/filtering options
+   * @param fields  Field search/sort
+   * @param token   a valid security token
+   * @return a collection of groups for a specific userId
+   * @throws org.apache.shindig.protocol.ProtocolException
+   */
+  public Future<RestfulCollection<Group>> getGroups(UserId userId, CollectionOptions options,
+    Set<String> fields, SecurityToken token) throws ProtocolException;
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/LocalId.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/LocalId.java
new file mode 100644
index 0000000..79537cd
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/LocalId.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.regex.Pattern;
+
+import com.google.common.base.Objects;
+
+/**
+ * LocalId as defined by the OpenSocial 2.0.1 Spec
+ * @see http://opensocial-resources.googlecode.com/svn/spec/2.0.1/Core-Data.xml#Local-Id
+ */
+public class LocalId {
+
+  private String localId;
+  private static final Pattern localIdPattern = Pattern.compile("[\\w.-]*");
+
+  /**
+   * Constructor for LocalId.
+   *
+   * @param localId String to try and create LocalId from
+   * @throws IllegalArgumentException
+   */
+  public LocalId(String localId) throws IllegalArgumentException {
+    if(localId != null) {
+      setLocalId(localId);
+    } else {
+      setLocalId("");
+    }
+  }
+
+  /**
+   * Validate localId is of the from defined in spec.
+   *
+   * @param localId String
+   * @return boolean If validation passes
+   */
+  private boolean validate(String localId) {
+    return localIdPattern.matcher(localId).matches();
+  }
+
+  /**
+   * Get the stored localId.
+   *
+   * @return localId String
+   */
+  public String getLocalId() {
+    return this.localId;
+  }
+
+  /**
+   * Sets the localId after validating its format
+   *
+   * @param localId String
+   * @return boolean If succeeded
+   * @throws IllegalArgumentException
+   */
+  public boolean setLocalId(String localId) throws IllegalArgumentException {
+    if(validate(localId)) {
+      this.localId = localId;
+      return true;
+    } else {
+      throw new IllegalArgumentException("The provided LocalId is not valid");
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof LocalId)) {
+      return false;
+    }
+    LocalId actual = (LocalId) o;
+    return this.getLocalId().equals(actual.getLocalId());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.localId);
+  }
+
+  @Override
+  public String toString() {
+    return this.localId;
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java
new file mode 100644
index 0000000..b1441ad
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MediaItemService.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * The MediaItemService interface defines the service provider interface for
+ * creating, retrieving, updating, and deleting OpenSocial MediaItems.
+ *
+ * @since 2.0.0
+ */
+public interface MediaItemService {
+
+  /*
+    * Retrieves a MediaItem by ID.
+    *
+    * @param userId    Identifies the owner of the MediaItem to retrieve
+    * @param appId      Identifies the application of the MeiaItem to retrieve
+    * @param albumId    Identifies the album containing the MediaItem
+    * @param mediaItemId  Identifies the MediaItem to retrieve
+    * @param fields    Indicates fields to be returned; empty set implies all
+    * @param token      A valid SecurityToken
+    *
+    * @return a response item with the requested MediaItem
+    */
+  Future<MediaItem> getMediaItem(UserId userId, String appId, String albumId,
+                                 String mediaItemId, Set<String> fields, SecurityToken token)
+      throws ProtocolException;
+
+  /*
+    * Retrieves MediaItems by IDs.
+    *
+    * @param userId    Identifies the owner of the MediaItems
+    * @param appId      Identifies the application of the MediaItems
+    * @param albumId    Identifies the album containing the MediaItems
+    * @param mediaItemIds  Identifies the MediaItems to retrieve
+    * @param fields    Specifies the fields to return; empty set implies all
+    * @param options    Sorting/filtering/pagination options
+    * @param token      A valid SecurityToken
+    *
+    * @return a response item with the requested MediaItems
+    */
+  Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+                                                     String appId, String albumId, Set<String> mediaItemIds,
+                                                     Set<String> fields, CollectionOptions options, SecurityToken token)
+      throws ProtocolException;
+
+  /*
+    * Retrieves MediaItems by Album.
+    *
+    * @param userId  Identifies the owner of the MediaItems
+    * @param appId    Identifies the application of the MediaItems
+    * @param albumId  Identifies the Album containing the MediaItems
+    * @param fields  Specifies the fields to return; empty set implies all
+    * @param options  Sorting/filtering/pagination options
+    * @param token    A valid SecurityToken
+    *
+    * @return a response item with the requested MediaItems
+    */
+  Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+                                                     String appId, String albumId, Set<String> fields,
+                                                     CollectionOptions options, SecurityToken token)
+      throws ProtocolException;
+
+  /*
+    * Retrieves MediaItems by users and groups.
+    *
+    * @param userIds  Identifies the users that this request is relative to
+    * @param groupId  Identifies the users' groups to retrieve MediaItems from
+    * @param appId    Identifies the application to retrieve MediaItems from
+    * @param fields  The fields to return; empty set implies all
+    * @param options  Sorting/filtering/pagination options
+    * @param token    A valid SecurityToken
+    *
+    * @return a response item with the requested MediaItems
+    */
+  Future<RestfulCollection<MediaItem>> getMediaItems(Set<UserId> userIds,
+                                                     GroupId groupId, String appId, Set<String> fields,
+                                                     CollectionOptions options, SecurityToken token)
+      throws ProtocolException;
+
+  /*
+    * Deletes a MediaItem by ID.
+    *
+    * @param userId    Identifies the owner of the MediaItem to delete
+    * @param appId      Identifies the application hosting the MediaItem
+    * @param albumId    Identifies the parent album of the MediaItem
+    * @param mediaItemId  Identifies the MediaItem to delete
+    * @param token      A valid SecurityToken
+    *
+    * @return a response item containing any errors
+    */
+  Future<Void> deleteMediaItem(UserId userId, String appId, String albumId,
+                               String mediaItemId, SecurityToken token) throws ProtocolException;
+
+  /*
+    * Create a MediaItem in the given album for the given user.
+    *
+    * @param userId    Identifies the owner of the MediaItem to create
+    * @param appId      Identifies the application hosting the MediaItem
+    * @param albumId    Identifies the album to contain the MediaItem
+    * @param mediaItem    The MediaItem to create
+    * @param token      A valid SecurityToken
+    *
+    * @return a response containing any errors
+    */
+  Future<Void> createMediaItem(UserId userId, String appId, String albumId,
+                               MediaItem mediaItem, SecurityToken token) throws ProtocolException;
+
+  /*
+    * Updates a MediaItem for the given user.  The MediaItem ID specified in
+    * the REST end-point is used, even if the MediaItem also defines an ID.
+    *
+    * @param userId    Identifies the owner of the MediaItem to update
+    * @param appId      Identifies the application hosting the MediaItem
+    * @param albumId    Identifies the album containing the MediaItem
+    * @param mediaItemId  Identifies the MediaItem to update
+    * @param mediaItem    The updated MediaItem to persist
+    * @param token      A valid SecurityToken
+    *
+    * @return a response containing any errors
+    */
+  Future<Void> updateMediaItem(UserId userId, String appId, String albumId,
+                               String mediaItemId, MediaItem mediaItem, SecurityToken token)
+      throws ProtocolException;
+
+  public class NotImplementedMediaItemService implements MediaItemService {
+    public Future<MediaItem> getMediaItem(UserId userId, String appId, String albumId, String mediaItemId, Set<String> fields, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId, String appId, String albumId, Set<String> mediaItemIds, Set<String> fields, CollectionOptions options, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId, String appId, String albumId, Set<String> fields, CollectionOptions options, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<RestfulCollection<MediaItem>> getMediaItems(Set<UserId> userIds, GroupId groupId, String appId, Set<String> fields, CollectionOptions options, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> deleteMediaItem(UserId userId, String appId, String albumId, String mediaItemId, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> createMediaItem(UserId userId, String appId, String albumId, MediaItem mediaItem, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> updateMediaItem(UserId userId, String appId, String albumId, String mediaItemId, MediaItem mediaItem, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MessageService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MessageService.java
new file mode 100644
index 0000000..de25ede
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/MessageService.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import com.google.inject.ImplementedBy;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.social.opensocial.model.Message;
+import org.apache.shindig.social.opensocial.model.MessageCollection;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+
+import java.util.Set;
+import java.util.List;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * The MessageService interface defines the service provider interface to post messages to
+ * the underlying SNS.
+ */
+@ImplementedBy(MessageService.NotImplementedMessageService.class)
+public interface MessageService {
+
+  /**
+   * Returns a list of message collections corresponding to the given user
+   *
+   * @param userId   The User to fetch for
+   * @param fields   The fields to fetch for the message collections
+   * @param options  Pagination, etal
+   * @param token    Given security token for this request
+   * @return a collection of message collections.
+   * @throws ProtocolException when invalid parameters are given
+   */
+  Future<RestfulCollection<MessageCollection>> getMessageCollections(UserId userId,
+     Set<String> fields, CollectionOptions options, SecurityToken token) throws ProtocolException;
+
+  /**
+   * Creates a new message collection for the given arguments
+   *
+   * @param userId  The userId to create the message collection for
+   * @param msgCollection A message collection that is to be created
+   * @param token  A security token for this request
+   *
+   * @return Data for the message collection that is created
+   * @throws ProtocolException when invalid parameters are given or not implemented
+   */
+  Future<MessageCollection> createMessageCollection(UserId userId, MessageCollection msgCollection, SecurityToken token)  throws ProtocolException;
+
+  /**
+   * Modifies/Updates a message collection for the given arguments
+   *
+   * @param userId  The userId to modify the message collection for
+   * @param msgCollection Data for the message collection to be modified
+   * @param token  A security token for this request
+   *
+   * @throws ProtocolException when invalid parameters are given or not implemented
+   */
+
+  Future<Void> modifyMessageCollection(UserId userId, MessageCollection msgCollection, SecurityToken token) throws ProtocolException;
+
+  /**
+   * Deletes a message collection for the given arguments
+   *
+   * @param userId  The userId to create the message collection for
+   * @param msgCollId Data for the message collection to be modified
+   * @param token  A security token for this request
+   *
+   * @throws ProtocolException when invalid parameters are given, the message collection does not exist or not implemented
+   * @return            Future<Void>
+   */
+
+  Future<Void> deleteMessageCollection(UserId userId, String msgCollId, SecurityToken token) throws ProtocolException;
+
+
+  /**
+   * Returns a list of messages that correspond to the passed in data
+   *
+   * @param userId      The User to fetch for
+   * @param msgCollId   The message Collection ID to fetch from, default @all
+   * @param fields      The fields to fetch for the messages
+   * @param msgIds      An explicit set of message ids to fetch
+   * @param options     Options to control the fetch
+   * @param token       Given security token for this request
+   * @return a collection of messages
+   * @throws ProtocolException when invalid parameters are given
+   */
+  Future<RestfulCollection<Message>> getMessages(UserId userId, String msgCollId,
+      Set<String> fields, List<String> msgIds, CollectionOptions options, SecurityToken token) throws ProtocolException;
+
+
+  /**
+   * Posts a message to the user's specified message collection, to be sent to the set of recipients specified in
+   * the message.
+   *
+   * @param userId      The user posting the message.
+   * @param appId       The app id
+   * @param msgCollId   The message collection Id to post to, default @outbox
+   * @param message     The message to post
+   * @param token       A valid security token @return a response item containing any errors/
+   * @return Void Future
+   * @throws ProtocolException when invalid parameters are given
+   */
+  Future<Void> createMessage(UserId userId, String appId, String msgCollId, Message message,
+                             SecurityToken token) throws ProtocolException;
+
+  /**
+   * Deletes a set of messages for a given user/message collection
+   * @param userId      The User to delete for
+   * @param msgCollId   The Message Collection ID to delete from, default @all
+   * @param ids         List of IDs to delete
+   * @param token       Given Security Token for this request
+   * @return            Future<Void>
+   * @throws ProtocolException
+   */
+  Future<Void> deleteMessages(UserId userId, String msgCollId, List<String> ids,
+      SecurityToken token) throws ProtocolException;
+
+
+  /**
+   * Modifies/Updates a specific message with new data
+   * @param userId      The User to modify for
+   * @param msgCollId   The Message Collection ID to modify from, default @all
+   * @param messageId   The messageId to modify
+   * @param message     The message details to modify
+   * @param token       Given Security Token for this request
+   * @return            Future<Void>
+   * @throws ProtocolException for invalid parameters or missing messages or users
+   */
+
+  Future<Void> modifyMessage(UserId userId, String msgCollId, String messageId, Message message, SecurityToken token)
+      throws ProtocolException;
+
+  public static class NotImplementedMessageService implements MessageService {
+    public Future<RestfulCollection<MessageCollection>> getMessageCollections(UserId userId,
+       Set<String> fields, CollectionOptions options, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<MessageCollection> createMessageCollection(UserId userId, MessageCollection msgCollection, SecurityToken token)  throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> modifyMessageCollection(UserId userId, MessageCollection msgCollection, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> deleteMessageCollection(UserId userId, String msgCollId, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<RestfulCollection<Message>> getMessages(UserId userId, String msgCollId,
+        Set<String> fields, List<String> msgIds, CollectionOptions options, SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> createMessage(UserId userId, String appId, String msgCollId, Message message,
+                               SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> deleteMessages(UserId userId, String msgCollId, List<String> ids,
+        SecurityToken token) throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+
+    public Future<Void> modifyMessage(UserId userId, String msgCollId, String messageId, Message message, SecurityToken token)
+        throws ProtocolException {
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Not Implemented");
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ObjectId.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ObjectId.java
new file mode 100644
index 0000000..c4e02d0
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/ObjectId.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import com.google.common.base.Objects;
+
+/**
+ * ObjectId as defined by the OpenSocial 2.0.1 Spec
+ * @see "http://opensocial-resources.googlecode.com/svn/spec/2.0.1/Core-Data.xml#Object-Id"
+ */
+public class ObjectId {
+
+  private Object objectId;
+
+  /**
+   * This constructor allows for a LocalId to be passed in order
+   * to create an ObjectId.
+   *
+   * @param localId the localId used to create the ObjectId
+   */
+  public ObjectId(LocalId localId) {
+    this.objectId = localId;
+  }
+
+  /**
+   * This constructor allows for a GlobalId to be passed in order
+   * to create an ObjectId.
+   *
+   * @param globalId the globalId used to create the ObjectId
+   */
+  public ObjectId(GlobalId globalId) {
+    this.objectId = globalId;
+  }
+
+  /**
+   * This constructor allows for a String to be passed in order
+   * to create an ObjectId. It will store it as a LocalId and
+   * verify it as such.
+   *
+   * @param id The id of the new LocalId that will be created
+   * @throws IllegalArgumentException when the id provided could not be parsed
+   *   into either a GlobalId or a LocalId
+   */
+  public ObjectId(String id) throws IllegalArgumentException {
+	try {
+	  this.objectId = new GlobalId(id);
+	} catch(IllegalArgumentException e1) {
+      // Not a valid globalId, try localId
+      try {
+        this.objectId = new LocalId(id);
+      } catch(IllegalArgumentException e2) {
+        // Not either so throw exception
+	      throw new IllegalArgumentException("The provided ObjectId is not valid");
+      }
+	}
+  }
+
+  /**
+   * Get the objectId.
+   *
+   * @return objectId Object
+   */
+  public Object getObjectId() {
+    return this.objectId;
+  }
+
+  /**
+   * Set the objectId with an ObjectId
+   *
+   * @param objectId ObjectId
+   */
+  public void setObjectId(ObjectId objectId) {
+	this.objectId = objectId;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+    if (!(o instanceof ObjectId)) {
+      return false;
+    }
+    ObjectId actual = (ObjectId) o;
+    return this.getObjectId().equals(actual.getObjectId());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.objectId);
+  }
+
+  @Override
+  public String toString() {
+    return this.objectId.toString();
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/PersonService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/PersonService.java
new file mode 100644
index 0000000..9581a0a
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/PersonService.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.social.opensocial.model.Person;
+
+import java.util.Set;
+import java.util.concurrent.Future;
+
+/**
+ * Interface that defines how shindig gathers people information.
+ */
+public interface PersonService {
+
+  /**
+   * When used will sort people by the container's definition of top friends. Note that both the
+   * sort order and the filter are required to deliver a topFriends response. The PersonService
+   * implementation should take this into account when delivering a topFriends response.
+   */
+  public static String TOP_FRIENDS_SORT = "topFriends";
+  /**
+   * Retrieves only the user's top friends. The meaning of top and how many top is is defined by the
+   * PersonService implementation.
+   */
+  public static String TOP_FRIENDS_FILTER = "topFriends";
+  /**
+   * Retrieves all friends with any data for this application.
+   * TODO: how is this application defined
+   */
+  public static String HAS_APP_FILTER = "hasApp";
+  /**
+   * Retrieves all friends. (ie no filter)
+   */
+  public static String ALL_FILTER = "all";
+  /**
+   * Will filter the people requested by checking if they are friends with the given idSpec. The
+   * filter value will be set to the userId of the target friend.
+   */
+  public static String IS_WITH_FRIENDS_FILTER = "isFriendsWith";
+
+  /**
+   * Returns a list of people that correspond to the passed in person ids.
+   *
+   * @param userIds A set of users
+   * @param groupId The group
+   * @param collectionOptions How to filter, sort and paginate the collection being fetched
+   * @param fields The profile details to fetch. Empty set implies all
+   * @param token The gadget token @return a list of people.
+   * @return Future that returns a RestfulCollection of Person
+   */
+  Future<RestfulCollection<Person>> getPeople(Set<UserId> userIds, GroupId groupId,
+      CollectionOptions collectionOptions, Set<String> fields, SecurityToken token)
+      throws ProtocolException;
+
+  /**
+   * Returns a person that corresponds to the passed in person id.
+   *
+   * @param id The id of the person to fetch.
+   * @param fields The fields to fetch.
+   * @param token The gadget token
+   * @return a list of people.
+   */
+  Future<Person> getPerson(UserId id, Set<String> fields, SecurityToken token)
+      throws ProtocolException;
+
+  /**
+   * Updates person that corresponds to the passed in person id and updates him
+   *
+   * @param id The id of the person to fetch.
+   * @param request The request object
+   * @param fields The fields to fetch.
+   * @param token The gadget token
+   * @return a list of people.
+   */
+  Future<Person> updatePerson(UserId id, Person person, SecurityToken token)
+      throws ProtocolException;
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/UserId.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/UserId.java
new file mode 100644
index 0000000..ca8028f
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/UserId.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.apache.shindig.auth.SecurityToken;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.base.Objects;
+
+import java.util.Map;
+
+/**
+ * Data structure representing a userid
+ */
+public class UserId {
+  public enum Type {
+    me, viewer, owner, userId;
+
+    /** A map of JSON strings to Type objects */
+    private static final Map<String, Type> jsonTypeMap;
+
+    static {
+      ImmutableMap.Builder<String,Type> builder = ImmutableMap.builder();
+      for (Type type : Type.values()) {
+        builder.put('@' + type.name(), type);
+      }
+      jsonTypeMap = builder.build();
+    }
+    /** Return the Type enum value given a specific jsonType **/
+    public static Type jsonValueOf(String jsonType) {
+       return jsonTypeMap.get(jsonType);
+    }
+  }
+
+  private Type type;
+  private String userId;
+
+  public UserId(Type type, String userId) {
+    this.type = type;
+    this.userId = userId;
+  }
+
+  public Type getType() {
+    return type;
+  }
+
+  public String getUserId() {
+    return userId;
+  }
+
+  public String getUserId(SecurityToken token) {
+    switch(type) {
+      case owner:
+        return token.getOwnerId();
+      case viewer:
+      case me:
+        return token.getViewerId();
+      case userId:
+        return userId;
+      default:
+        throw new IllegalStateException("The type field is not a valid enum: " + type);
+    }
+  }
+
+  public static UserId fromJson(String jsonId) {
+    Type idSpecEnum = Type.jsonValueOf(jsonId);
+    if (idSpecEnum != null) {
+      return new UserId(idSpecEnum, null);
+    }
+
+    return new UserId(Type.userId, jsonId);
+  }
+
+  // These are overriden so that EasyMock doesn't throw a fit
+  @Override
+  public boolean equals(Object o) {
+    if (o == this) {
+      return true;
+    }
+
+    if (!(o instanceof UserId)) {
+      return false;
+    }
+
+    UserId actual = (UserId) o;
+    return this.type == actual.type
+        && Objects.equal(this.userId, actual.userId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(this.userId,  this.type);
+  }
+
+  @Override
+  public String toString() {
+      switch(type) {
+          case owner:
+            return "OWNER";
+          case viewer:
+          case me:
+            return "VIEWER";
+          case userId:
+            return "USER(" + userId + ')';
+          default:
+              return "UNKNOWN";
+        }
+
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/package-info.java
new file mode 100644
index 0000000..c8beb07
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/opensocial/spi/package-info.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>The Social Service Provider Interface package</h1>
+<p>Interface and supporting classes that specify the service
+provider interface between Shindig and the deployed SNS infrastructure.
+Implementors will almost certainly want to implement their own versions
+of these services and override the @ImplementedBy Guice annotations by
+providing their own Guice module.</p>
+*/
+
+package org.apache.shindig.social.opensocial.spi;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/package-info.java
new file mode 100644
index 0000000..b0c70f6
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/package-info.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+<h1>The Social API package</h1>
+<p>Shared API classes and interfaces used by both the back end (spi)
+and front end (service).</p>
+<p>The Social Jar consists of a core set of packages that contain
+the lower level implementation of the component (o.a.s.social.core), and
+a set interface layers (o.a.s.social.opensocial) that represent areas of
+the component that are more outward facing. In addition there is a
+sample package which contains sample implementations of the service and
+spi.</p>
+<h2>Opensocial</h2>
+<p>This package contains classes that are outward facing, there are
+4 main areas.
+<ul>
+  <li><b>model</b> the model interfaces underlying the Opensocial implementation</li>
+  <li><b>oath</b> the OAuth interfaces used inside the Opensocial implementation</li>
+  <li><b>service</b> the HTTP facing service interface</li>
+  <li><b>spi</b> the Service Provider Interfaces</li>
+</ul>
+*/
+
+package org.apache.shindig.social;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java
new file mode 100644
index 0000000..a6a70d0
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/SampleModule.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.sample;
+
+import org.apache.shindig.social.core.oauth2.OAuth2DataService;
+import org.apache.shindig.social.core.oauth2.OAuth2DataServiceImpl;
+import org.apache.shindig.social.core.oauth2.OAuth2Service;
+import org.apache.shindig.social.core.oauth2.OAuth2ServiceImpl;
+import org.apache.shindig.social.opensocial.oauth.OAuthDataStore;
+import org.apache.shindig.social.opensocial.spi.ActivityService;
+import org.apache.shindig.social.opensocial.spi.ActivityStreamService;
+import org.apache.shindig.social.opensocial.spi.AlbumService;
+import org.apache.shindig.social.opensocial.spi.AppDataService;
+import org.apache.shindig.social.opensocial.spi.GroupService;
+import org.apache.shindig.social.opensocial.spi.MediaItemService;
+import org.apache.shindig.social.opensocial.spi.MessageService;
+import org.apache.shindig.social.opensocial.spi.PersonService;
+import org.apache.shindig.social.sample.oauth.SampleOAuthDataStore;
+import org.apache.shindig.social.sample.spi.JsonDbOpensocialService;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+/**
+ * Provides bindings for sample-only implementations of social API
+ * classes.  This class should never be used in production deployments,
+ * but does provide a good overview of the pieces of Shindig that require
+ * custom container implementations.
+ */
+public class SampleModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(String.class).annotatedWith(Names.named("shindig.canonical.json.db"))
+        .toInstance("sampledata/canonicaldb.json");
+    bind(ActivityService.class).to(JsonDbOpensocialService.class);
+    bind(ActivityStreamService.class).to(JsonDbOpensocialService.class);
+    bind(AlbumService.class).to(JsonDbOpensocialService.class);
+    bind(MediaItemService.class).to(JsonDbOpensocialService.class);
+    bind(AppDataService.class).to(JsonDbOpensocialService.class);
+    bind(PersonService.class).to(JsonDbOpensocialService.class);
+    bind(MessageService.class).to(JsonDbOpensocialService.class);
+    bind(GroupService.class).to(JsonDbOpensocialService.class);
+    bind(OAuthDataStore.class).to(SampleOAuthDataStore.class);
+    bind(OAuth2Service.class).to(OAuth2ServiceImpl.class);
+    bind(OAuth2DataService.class).to(OAuth2DataServiceImpl.class);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/SampleOAuthDataStore.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/SampleOAuthDataStore.java
new file mode 100644
index 0000000..961286e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/SampleOAuthDataStore.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.sample.oauth;
+
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.collect.ImmutableList;
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.auth.AuthenticationMode;
+import org.apache.shindig.common.crypto.Crypto;
+import org.apache.shindig.social.core.oauth.OAuthSecurityToken;
+import org.apache.shindig.social.opensocial.oauth.OAuthDataStore;
+import org.apache.shindig.social.opensocial.oauth.OAuthEntry;
+import org.apache.shindig.social.sample.spi.JsonDbOpensocialService;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.Date;
+import java.util.UUID;
+
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthServiceProvider;
+
+/**
+ * Sample implementation for OAuth data store
+ */
+public class SampleOAuthDataStore implements OAuthDataStore {
+  // This needs to be long enough that an attacker can't guess it.  If the attacker can guess this
+  // value before they exceed the maximum number of attempts, they can complete a session fixation
+  // attack against a user.
+  private static final int CALLBACK_TOKEN_LENGTH = 6;
+
+  // We limit the number of trials before disabling the request token.
+  private static final int CALLBACK_TOKEN_ATTEMPTS = 5;
+
+  // used to get samplecontainer data from canonicaldb.json
+  private final JsonDbOpensocialService service;
+  private final OAuthServiceProvider SERVICE_PROVIDER;
+
+  @Inject
+  public SampleOAuthDataStore(JsonDbOpensocialService dbService, @Named("shindig.oauth.base-url") String baseUrl) {
+    this.service = dbService;
+    this.SERVICE_PROVIDER = new OAuthServiceProvider(baseUrl + "/requestToken", baseUrl + "/authorize", baseUrl + "/accessToken");
+  }
+
+  // All valid OAuth tokens
+  private static Cache<String, OAuthEntry> oauthEntries = CacheBuilder.newBuilder()
+      .build();
+
+  // Get the OAuthEntry that corresponds to the oauthToken
+  public OAuthEntry getEntry(String oauthToken) {
+    Preconditions.checkNotNull(oauthToken);
+    return oauthEntries.asMap().get(oauthToken);
+  }
+
+  public OAuthConsumer getConsumer(String consumerKey) {
+    try {
+      JSONObject app = service.getDb().getJSONObject("apps").getJSONObject(Preconditions.checkNotNull(consumerKey));
+      String consumerSecret = app.getString("consumerSecret");
+
+      if (consumerSecret == null)
+        return null;
+
+      // null below is for the callbackUrl, which we don't have in the db
+      OAuthConsumer consumer = new OAuthConsumer(null, consumerKey, consumerSecret, SERVICE_PROVIDER);
+
+      // Set some properties loosely based on the ModulePrefs of a gadget
+      for (String key : ImmutableList.of("title", "summary", "description", "thumbnail", "icon")) {
+        if (app.has(key))
+          consumer.setProperty(key, app.getString(key));
+      }
+
+      return consumer;
+
+    } catch (JSONException e) {
+      return null;
+    }
+  }
+
+  // Generate a valid requestToken for the given consumerKey
+  public OAuthEntry generateRequestToken(String consumerKey, String oauthVersion,
+                                         String signedCallbackUrl) {
+    OAuthEntry entry = new OAuthEntry();
+    entry.setAppId(consumerKey);
+    entry.setConsumerKey(consumerKey);
+    entry.setDomain("samplecontainer.com");
+    entry.setContainer("default");
+
+    entry.setToken(UUID.randomUUID().toString());
+    entry.setTokenSecret(UUID.randomUUID().toString());
+
+    entry.setType(OAuthEntry.Type.REQUEST);
+    entry.setIssueTime(new Date());
+    entry.setOauthVersion(oauthVersion);
+    if (signedCallbackUrl != null) {
+      entry.setCallbackUrlSigned(true);
+      entry.setCallbackUrl(signedCallbackUrl);
+    }
+
+    oauthEntries.put(entry.getToken(), entry);
+    return entry;
+  }
+
+  // Turns the request token into an access token
+  public OAuthEntry convertToAccessToken(OAuthEntry entry) {
+    Preconditions.checkNotNull(entry);
+    Preconditions.checkState(entry.getType() == OAuthEntry.Type.REQUEST, "Token must be a request token");
+
+    OAuthEntry accessEntry = new OAuthEntry(entry);
+
+    accessEntry.setToken(UUID.randomUUID().toString());
+    accessEntry.setTokenSecret(UUID.randomUUID().toString());
+
+    accessEntry.setType(OAuthEntry.Type.ACCESS);
+    accessEntry.setIssueTime(new Date());
+
+    oauthEntries.invalidate(entry.getToken());
+    oauthEntries.put(accessEntry.getToken(), accessEntry);
+
+    return accessEntry;
+  }
+
+  // Authorize the request token for the given user id
+  public void authorizeToken(OAuthEntry entry, String userId) {
+    Preconditions.checkNotNull(entry);
+    entry.setAuthorized(true);
+    entry.setUserId(Preconditions.checkNotNull(userId));
+    if (entry.isCallbackUrlSigned()) {
+      entry.setCallbackToken(Crypto.getRandomDigits(CALLBACK_TOKEN_LENGTH));
+    }
+  }
+
+  public void disableToken(OAuthEntry entry) {
+    Preconditions.checkNotNull(entry);
+    entry.setCallbackTokenAttempts(entry.getCallbackTokenAttempts() + 1);
+    if (!entry.isCallbackUrlSigned() || entry.getCallbackTokenAttempts() >= CALLBACK_TOKEN_ATTEMPTS) {
+      entry.setType(OAuthEntry.Type.DISABLED);
+    }
+
+    oauthEntries.put(entry.getToken(), entry);
+  }
+
+  public void removeToken(OAuthEntry entry) {
+    Preconditions.checkNotNull(entry);
+
+    oauthEntries.invalidate(entry.getToken());
+  }
+
+  // Return the proper security token for a 2 legged oauth request that has been validated
+  // for the given consumerKey. App specific checks like making sure the requested user has the
+  // app installed should take place in this method
+  public SecurityToken getSecurityTokenForConsumerRequest(String consumerKey, String userId) {
+    String domain = "samplecontainer.com";
+    String container = "default";
+
+    return new OAuthSecurityToken(userId, null, consumerKey, domain, container, null,
+                                  AuthenticationMode.OAUTH_CONSUMER_REQUEST.name());
+
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/SampleOAuthServlet.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/SampleOAuthServlet.java
new file mode 100644
index 0000000..5c1b46f
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/SampleOAuthServlet.java
@@ -0,0 +1,324 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.sample.oauth;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.common.servlet.HttpUtil;
+import org.apache.shindig.common.servlet.InjectedServlet;
+import org.apache.shindig.social.opensocial.oauth.OAuthDataStore;
+import org.apache.shindig.social.opensocial.oauth.OAuthEntry;
+
+import com.google.inject.Inject;
+import com.google.inject.name.Named;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthException;
+import net.oauth.OAuthMessage;
+import net.oauth.OAuthProblemException;
+import net.oauth.OAuthValidator;
+import net.oauth.OAuth.Parameter;
+import net.oauth.server.OAuthServlet;
+
+/**
+ * This is a sample class that demonstrates how oauth tokens can be handed out and authorized.
+ * This is most certainly not production code. Your server should have clear ui, require user
+ * login for creating consumer secrets and authorizing request tokens, do better patch dispatching,
+ * and use a non-in memory data store.
+ */
+public class SampleOAuthServlet extends InjectedServlet {
+  private OAuthValidator validator;
+  private OAuthDataStore dataStore;
+  private String oauthAuthorizeAction;
+
+  @Inject
+  public void setValidator(OAuthValidator validator) {
+    this.validator = validator;
+  }
+
+  @Inject
+  public void setDataStore(OAuthDataStore dataStore) {
+    this.dataStore = dataStore;
+  }
+
+  @Inject void setAuthorizeAction(@Named("shindig.oauth.authorize-action") String authorizeAction) {
+    this.oauthAuthorizeAction = authorizeAction;
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest servletRequest,
+                        HttpServletResponse servletResponse) throws ServletException, IOException {
+
+    doGet(servletRequest, servletResponse);
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest servletRequest,
+                       HttpServletResponse servletResponse) throws ServletException, IOException {
+    HttpUtil.setNoCache(servletResponse);
+    String path = servletRequest.getPathInfo();
+
+    try {
+      // dispatch
+      if (path.endsWith("requestToken")) {
+        createRequestToken(servletRequest, servletResponse);
+      } else if (path.endsWith("authorize")) {
+        authorizeRequestToken(servletRequest, servletResponse);
+      } else if (path.endsWith("accessToken")) {
+        createAccessToken(servletRequest, servletResponse);
+      } else {
+        servletResponse.sendError(HttpServletResponse.SC_NOT_FOUND, "unknown Url");
+      }
+    } catch (OAuthException e) {
+      handleException(e, servletRequest, servletResponse, true);
+    } catch (URISyntaxException e) {
+      handleException(e, servletRequest, servletResponse, true);
+    }
+  }
+
+  // Hand out a request token if the consumer key and secret are valid
+  private void createRequestToken(HttpServletRequest servletRequest,
+                                  HttpServletResponse servletResponse) throws IOException, OAuthException, URISyntaxException {
+    OAuthMessage requestMessage = OAuthServlet.getMessage(servletRequest, null);
+
+    String consumerKey = requestMessage.getConsumerKey();
+    if (consumerKey == null) {
+      OAuthProblemException e = new OAuthProblemException(OAuth.Problems.PARAMETER_ABSENT);
+      e.setParameter(OAuth.Problems.OAUTH_PARAMETERS_ABSENT, OAuth.OAUTH_CONSUMER_KEY);
+      throw e;
+    }
+    OAuthConsumer consumer = dataStore.getConsumer(consumerKey);
+
+    if (consumer == null)
+      throw new OAuthProblemException(OAuth.Problems.CONSUMER_KEY_UNKNOWN);
+
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    validator.validateMessage(requestMessage, accessor);
+
+    String callback = requestMessage.getParameter(OAuth.OAUTH_CALLBACK);
+
+    if (callback == null) {
+      // see if the consumer has a callback
+      callback = consumer.callbackURL;
+    }
+    if (callback == null) {
+      callback = "oob";
+    }
+
+    // generate request_token and secret
+    OAuthEntry entry = dataStore.generateRequestToken(consumerKey,
+                                                      requestMessage.getParameter(OAuth.OAUTH_VERSION), callback);
+
+    List<Parameter> responseParams = OAuth.newList(OAuth.OAUTH_TOKEN, entry.getToken(),
+                                                   OAuth.OAUTH_TOKEN_SECRET, entry.getTokenSecret());
+    if (callback != null) {
+      responseParams.add(new Parameter(OAuth.OAUTH_CALLBACK_CONFIRMED, "true"));
+    }
+    sendResponse(servletResponse, responseParams);
+  }
+
+
+  /////////////////////
+  // deal with authorization request
+  private void authorizeRequestToken(HttpServletRequest servletRequest,
+                                     HttpServletResponse servletResponse) throws ServletException, IOException, OAuthException, URISyntaxException {
+
+    OAuthMessage requestMessage = OAuthServlet.getMessage(servletRequest, null);
+
+    if (requestMessage.getToken() == null) {
+      // MALFORMED REQUEST
+      servletResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "Authentication token not found");
+      return;
+    }
+    OAuthEntry entry = dataStore.getEntry(requestMessage.getToken());
+
+    if (entry == null) {
+      servletResponse.sendError(HttpServletResponse.SC_NOT_FOUND, "OAuth Entry not found");
+      return;
+    }
+
+    OAuthConsumer consumer = dataStore.getConsumer(entry.getConsumerKey());
+
+    // Extremely rare case where consumer dissappears
+    if (consumer == null) {
+      servletResponse.sendError(HttpServletResponse.SC_NOT_FOUND, "consumer for entry not found");
+      return;
+    }
+
+    // The token is disabled if you try to convert to an access token prior to authorization
+    if (entry.getType() == OAuthEntry.Type.DISABLED) {
+      servletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "This token is disabled, please reinitate login");
+      return;
+    }
+
+    String callback = entry.getCallbackUrl();
+
+    // Redirect to a UI flow if the token is not authorized
+    if (!entry.isAuthorized()) {
+      // TBD -- need to decode encrypted payload somehow..
+      if (this.oauthAuthorizeAction.startsWith("http")) {
+        // Redirect to authorization page with params
+        // Supply standard set of params
+        // TBD
+      } else {
+        // Use internal forward to a jsp page
+        servletRequest.setAttribute("OAUTH_DATASTORE",  dataStore);
+
+        servletRequest.setAttribute("OAUTH_ENTRY",  entry);
+        servletRequest.setAttribute("CALLBACK", callback);
+
+        servletRequest.setAttribute("TOKEN", entry.getToken());
+        servletRequest.setAttribute("CONSUMER", consumer);
+
+        servletRequest.getRequestDispatcher(oauthAuthorizeAction).forward(servletRequest,servletResponse);
+      }
+      return;
+    }
+
+    // If we're here then the entry has been authorized
+
+    // redirect to callback
+    if (callback == null || "oob".equals(callback)) {
+      // consumer did not specify a callback
+      servletResponse.setContentType("text/plain");
+      PrintWriter out = servletResponse.getWriter();
+      out.write("Token successfully authorized.\n");
+      if (entry.getCallbackToken() != null) {
+        // Usability fail.
+        out.write("Please enter code " + entry.getCallbackToken() + " at the consumer.");
+      }
+    } else {
+      callback = OAuth.addParameters(callback, OAuth.OAUTH_TOKEN, entry.getToken());
+      // Add user_id to the callback
+      callback = OAuth.addParameters(callback, "user_id", entry.getUserId());
+      if (entry.getCallbackToken() != null) {
+        callback = OAuth.addParameters(callback, OAuth.OAUTH_VERIFIER,
+                                       entry.getCallbackToken());
+      }
+
+      servletResponse.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
+      servletResponse.setHeader("Location", callback);
+    }
+  }
+
+  // Hand out an access token if the consumer key and secret are valid and the user authorized
+  // the requestToken
+  private void createAccessToken(HttpServletRequest servletRequest,
+                                 HttpServletResponse servletResponse) throws ServletException, IOException, OAuthException, URISyntaxException {
+    OAuthMessage requestMessage = OAuthServlet.getMessage(servletRequest, null);
+
+    OAuthEntry entry = getValidatedEntry(requestMessage);
+    if (entry == null)
+      throw new OAuthProblemException(OAuth.Problems.TOKEN_REJECTED);
+
+    if (entry.getCallbackToken() != null) {
+      // We're using the fixed protocol
+      String clientCallbackToken = requestMessage.getParameter(OAuth.OAUTH_VERIFIER);
+      if (!entry.getCallbackToken().equals(clientCallbackToken)) {
+        dataStore.disableToken(entry);
+        servletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "This token is not authorized");
+        return;
+      }
+    } else if (!entry.isAuthorized()) {
+      // Old protocol.  Catch consumers trying to convert a token to one that's not authorized
+      dataStore.disableToken(entry);
+      servletResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "This token is not authorized");
+      return;
+    }
+
+    // turn request token into access token
+    OAuthEntry accessEntry = dataStore.convertToAccessToken(entry);
+
+    sendResponse(servletResponse, OAuth.newList(
+                   OAuth.OAUTH_TOKEN, accessEntry.getToken(),
+                   OAuth.OAUTH_TOKEN_SECRET, accessEntry.getTokenSecret(),
+                   "user_id", entry.getUserId()));
+  }
+
+
+  private OAuthEntry getValidatedEntry(OAuthMessage requestMessage)
+    throws IOException, ServletException, OAuthException, URISyntaxException {
+
+    OAuthEntry entry = dataStore.getEntry(requestMessage.getToken());
+    if (entry == null)
+      throw new OAuthProblemException(OAuth.Problems.TOKEN_REJECTED);
+
+    if (entry.getType() != OAuthEntry.Type.REQUEST)
+      throw new OAuthProblemException(OAuth.Problems.TOKEN_USED);
+
+    if (entry.isExpired())
+      throw new OAuthProblemException(OAuth.Problems.TOKEN_EXPIRED);
+
+    // find consumer key, compare with supplied value, if present.
+
+    if  (requestMessage.getConsumerKey() == null) {
+      OAuthProblemException e = new OAuthProblemException(OAuth.Problems.PARAMETER_ABSENT);
+      e.setParameter(OAuth.Problems.OAUTH_PARAMETERS_ABSENT, OAuth.OAUTH_CONSUMER_KEY);
+      throw e;
+    }
+
+    String consumerKey = entry.getConsumerKey();
+    if (!consumerKey.equals(requestMessage.getConsumerKey()))
+      throw new OAuthProblemException(OAuth.Problems.CONSUMER_KEY_REFUSED);
+
+    OAuthConsumer consumer = dataStore.getConsumer(consumerKey);
+    if (consumer == null)
+      throw new OAuthProblemException(OAuth.Problems.CONSUMER_KEY_UNKNOWN);
+
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    accessor.requestToken = entry.getToken();
+    accessor.tokenSecret = entry.getTokenSecret();
+
+    validator.validateMessage(requestMessage, accessor);
+
+    return entry;
+  }
+
+  private void sendResponse(HttpServletResponse servletResponse, List<OAuth.Parameter> parameters)
+    throws IOException {
+    servletResponse.setContentType("text/plain");
+    OutputStream out = servletResponse.getOutputStream();
+    OAuth.formEncode(parameters, out);
+    out.close();
+  }
+
+  private static void handleException(Exception e, HttpServletRequest request,
+                                      HttpServletResponse response, boolean sendBody)
+    throws IOException, ServletException {
+    String realm = (request.isSecure()) ? "https://" : "http://";
+
+    if (request.getHeader("Host") != null) {
+      realm += request.getHeader("Host");
+    } else {
+      realm += request.getLocalName();
+    }
+    OAuthServlet.handleException(response, e, realm, sendBody);
+  }
+
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/package-info.java
new file mode 100644
index 0000000..eaf5744
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/oauth/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * <h1>Sample OAuth implementation package</h1>
+ * <p>A Sample implementation of OAuth for the sample container.</p>
+ */
+package org.apache.shindig.social.sample.oauth;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/service/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/service/package-info.java
new file mode 100644
index 0000000..c3e705e
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/service/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * <h1>Sample Container package</h1>
+ * <p>Support classes for a simple sample container.</p>
+ */
+package org.apache.shindig.social.sample.service;
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java
new file mode 100644
index 0000000..0d3b4c1
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialService.java
@@ -0,0 +1,1493 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.sample.spi;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.servlet.Authority;
+import org.apache.shindig.common.util.ResourceLoader;
+import org.apache.shindig.protocol.DataCollection;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.model.SortOrder;
+import org.apache.shindig.social.core.model.NameImpl;
+import org.apache.shindig.social.core.model.PersonImpl;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.model.Album;
+import org.apache.shindig.social.opensocial.model.Group;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+import org.apache.shindig.social.opensocial.model.Message;
+import org.apache.shindig.social.opensocial.model.MessageCollection;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.spi.ActivityService;
+import org.apache.shindig.social.opensocial.spi.ActivityStreamService;
+import org.apache.shindig.social.opensocial.spi.AlbumService;
+import org.apache.shindig.social.opensocial.spi.AppDataService;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.GroupService;
+import org.apache.shindig.social.opensocial.spi.MediaItemService;
+import org.apache.shindig.social.opensocial.spi.MessageService;
+import org.apache.shindig.social.opensocial.spi.PersonService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+
+/**
+ * Implementation of supported services backed by a JSON DB.
+ */
+@Singleton
+public class JsonDbOpensocialService implements ActivityService, PersonService, AppDataService,
+    MessageService, AlbumService, MediaItemService, ActivityStreamService, GroupService {
+
+  private static final Comparator<Person> NAME_COMPARATOR = new Comparator<Person>() {
+    public int compare(Person person, Person person1) {
+      String name = person.getName().getFormatted();
+      String name1 = person1.getName().getFormatted();
+      return name.compareTo(name1);
+    }
+  };
+
+  /**
+   * The DB
+   */
+  private JSONObject db;
+
+  /**
+   * The JSON<->Bean converter
+   */
+  private BeanConverter converter;
+
+  /**
+   * db["people"] -> Array<Person>
+   */
+  private static final String PEOPLE_TABLE = "people";
+
+  /**
+   * db["groups"] -> Array<Group>
+   */
+  private static final String GROUPS_TABLE = "groups";
+
+  /**
+   * db["groupMembers"] -> Array<Person>
+   */
+  private static final String GROUP_MEMBERS_TABLE = "groupMembers";
+
+  /**
+   * db["activities"] -> Map<Person.Id, Array<Activity>>
+   */
+  private static final String ACTIVITIES_TABLE = "activities";
+
+  /**
+   * db["albums"] -> Map<Person.Id, Array<Album>>
+   */
+  private static final String ALBUMS_TABLE = "albums";
+
+  /**
+   * db["mediaItems"] -> Map<Person.Id, Array<MediaItem>>
+   */
+  private static final String MEDIAITEMS_TABLE = "mediaItems";
+
+  /**
+   * db["data"] -> Map<Person.Id, Map<String, String>>
+   */
+  private static final String DATA_TABLE = "data";
+
+  /**
+   * db["friendLinks"] -> Map<Person.Id, Array<Person.Id>>
+   */
+  private static final String FRIEND_LINK_TABLE = "friendLinks";
+
+  /**
+   * db["messages"] -> Map<Person.Id, Array<Message>>
+   */
+  private static final String MESSAGE_TABLE = "messages";
+
+  /**
+   * db["passwords"] -> Map<Person.Id, String>
+   */
+  private static final String PASSWORDS_TABLE = "passwords";
+
+  /**
+   * db["activityEntries"] -> Map<Person.Id, Array<ActivityEntry>>
+   */
+  private static final String ACTIVITYSTREAMS_TABLE = "activityEntries";
+
+  /**
+   * Anonymous name.
+   */
+  private static final String ANONYMOUS_NAME = "Anonymous";
+
+  private Authority authority;
+
+  /**
+   * Initializes the JsonDbOpensocialService using Guice
+   *
+   * @param jsonLocation location of the json data provided by the shindig.canonical.json.db parameter
+   * @param converter an injected BeanConverter
+   * @throws java.lang.Exception if any
+   */
+  @Inject
+  public JsonDbOpensocialService(@Named("shindig.canonical.json.db")
+  String jsonLocation, @Named("shindig.bean.converter.json")
+  BeanConverter converter,
+  @Named("shindig.contextroot") String contextroot) throws Exception {
+    String content = IOUtils.toString(ResourceLoader.openResource(jsonLocation), "UTF-8");
+    this.db = new JSONObject(content.replace("%contextroot%", contextroot));
+    this.converter = converter;
+  }
+
+  /**
+   * Allows access to the underlying json db.
+   *
+   * @return a reference to the json db
+   */
+  public JSONObject getDb() {
+    return db;
+  }
+
+   /**
+   * override the json database
+   * @param db a {@link org.json.JSONObject}.
+   */
+  public void setDb(JSONObject db) {
+    this.db = db;
+  }
+
+  @Inject(optional = true)
+  public void setAuthority(Authority authority) {
+    this.authority = authority;
+  }
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<Activity>> getActivities(Set<UserId> userIds, GroupId groupId,
+      String appId, Set<String> fields, CollectionOptions options, SecurityToken token)
+      throws ProtocolException {
+    List<Activity> result = Lists.newArrayList();
+    try {
+      Set<String> idSet = getIdSet(userIds, groupId, token);
+      for (String id : idSet) {
+        if (db.getJSONObject(ACTIVITIES_TABLE).has(id)) {
+          JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(id);
+          for (int i = 0; i < activities.length(); i++) {
+            JSONObject activity = activities.getJSONObject(i);
+            if (appId == null || !activity.has(Activity.Field.APP_ID.toString())) {
+              result.add(filterFields(activity, fields, Activity.class));
+            } else if (activity.get(Activity.Field.APP_ID.toString()).equals(appId)) {
+              result.add(filterFields(activity, fields, Activity.class));
+            }
+          }
+        }
+      }
+      return Futures.immediateFuture(new RestfulCollection<Activity>(result));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<Activity>> getActivities(UserId userId, GroupId groupId,
+      String appId, Set<String> fields, CollectionOptions options, Set<String> activityIds,
+      SecurityToken token) throws ProtocolException {
+    List<Activity> result = Lists.newArrayList();
+    try {
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
+        JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
+        for (int i = 0; i < activities.length(); i++) {
+          JSONObject activity = activities.getJSONObject(i);
+          if (activity.get(Activity.Field.USER_ID.toString()).equals(user)
+              && activityIds.contains(activity.getString(Activity.Field.ID.toString()))) {
+            result.add(filterFields(activity, fields, Activity.class));
+          }
+        }
+      }
+      return Futures.immediateFuture(new RestfulCollection<Activity>(result));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Activity> getActivity(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, String activityId, SecurityToken token) throws ProtocolException {
+    try {
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
+        JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
+        for (int i = 0; i < activities.length(); i++) {
+          JSONObject activity = activities.getJSONObject(i);
+          if (activity.get(Activity.Field.USER_ID.toString()).equals(user)
+              && activity.get(Activity.Field.ID.toString()).equals(activityId)) {
+            return Futures.immediateFuture(filterFields(activity, fields, Activity.class));
+          }
+        }
+      }
+
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Activity not found");
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> deleteActivities(UserId userId, GroupId groupId, String appId,
+      Set<String> activityIds, SecurityToken token) throws ProtocolException {
+    try {
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ACTIVITIES_TABLE).has(user)) {
+        JSONArray activities = db.getJSONObject(ACTIVITIES_TABLE).getJSONArray(user);
+        if (activities != null) {
+          JSONArray newList = new JSONArray();
+          for (int i = 0; i < activities.length(); i++) {
+            JSONObject activity = activities.getJSONObject(i);
+            if (!activityIds.contains(activity.getString(Activity.Field.ID.toString()))) {
+              newList.put(activity);
+            }
+          }
+          db.getJSONObject(ACTIVITIES_TABLE).put(user, newList);
+          // TODO. This seems very odd that we return no useful response in this
+          // case
+          // There is no way to represent not-found
+          // if (found) { ??
+          // }
+        }
+      }
+      // What is the appropriate response here??
+      return Futures.immediateFuture(null);
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> createActivity(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, Activity activity, SecurityToken token) throws ProtocolException {
+    // Are fields really needed here?
+    try {
+      JSONObject jsonObject = convertFromActivity(activity, fields);
+      if (!jsonObject.has(Activity.Field.ID.toString())) {
+        jsonObject.put(Activity.Field.ID.toString(), System.currentTimeMillis());
+      }
+      JSONArray jsonArray = db.getJSONObject(ACTIVITIES_TABLE)
+          .getJSONArray(userId.getUserId(token));
+      if (jsonArray == null) {
+        jsonArray = new JSONArray();
+        db.getJSONObject(ACTIVITIES_TABLE).put(userId.getUserId(token), jsonArray);
+      }
+      // TODO (woodser): if used with PUT, duplicate activity would be created?
+      jsonArray.put(jsonObject);
+      return Futures.immediateFuture(null);
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<Person>> getPeople(Set<UserId> userIds, GroupId groupId,
+      CollectionOptions options, Set<String> fields, SecurityToken token) throws ProtocolException {
+    List<Person> result = Lists.newArrayList();
+    try {
+      JSONArray people = db.getJSONArray(PEOPLE_TABLE);
+
+      Set<String> idSet = getIdSet(userIds, groupId, token);
+
+      for (int i = 0; i < people.length(); i++) {
+        JSONObject person = people.getJSONObject(i);
+        if (!idSet.contains(person.get(Person.Field.ID.toString()))) {
+          continue;
+        }
+
+        // Add group support later
+        Person personObj = filterFields(person, fields, Person.class);
+        Map<String, Object> appData = getPersonAppData(
+            person.getString(Person.Field.ID.toString()), fields);
+        personObj.setAppData(appData);
+
+        result.add(personObj);
+      }
+
+      if (GroupId.Type.self == groupId.getType() && result.isEmpty()) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "People '" + idSet + "' not found");
+      }
+
+      // We can pretend that by default the people are in top friends order
+      if (options.getSortBy().equals(Person.Field.NAME.toString())) {
+        Collections.sort(result, NAME_COMPARATOR);
+
+        if (options.getSortOrder() == SortOrder.descending) {
+          Collections.reverse(result);
+        }
+      }
+
+      // TODO: The samplecontainer doesn't really have the concept of HAS_APP so
+      // we can't support any filters yet. We should fix this.
+
+      int totalSize = result.size();
+      int last = options.getFirst() + options.getMax();
+      result = result.subList(options.getFirst(), Math.min(last, totalSize));
+
+      return Futures.immediateFuture(new RestfulCollection<Person>(result, options.getFirst(), totalSize, options.getMax()));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Person> getPerson(UserId id, Set<String> fields, SecurityToken token)
+      throws ProtocolException {
+    if (id != null && AnonymousSecurityToken.ANONYMOUS_ID.equals(id.getUserId())) {
+      Person anonymous = new PersonImpl();
+      anonymous.setId(AnonymousSecurityToken.ANONYMOUS_ID);
+      anonymous.setName(new NameImpl(ANONYMOUS_NAME));
+      anonymous.setNickname(ANONYMOUS_NAME);
+      return Futures.immediateFuture(anonymous);
+    }
+    try {
+      JSONArray people = db.getJSONArray(PEOPLE_TABLE);
+
+      for (int i = 0; i < people.length(); i++) {
+        JSONObject person = people.getJSONObject(i);
+        if (id != null && person.get(Person.Field.ID.toString()).equals(id.getUserId(token))) {
+          Person personObj = filterFields(person, fields, Person.class);
+          Map<String, Object> appData = getPersonAppData(person.getString(Person.Field.ID
+              .toString()), fields);
+          personObj.setAppData(appData);
+
+          return Futures.immediateFuture(personObj);
+        }
+      }
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Person '" + id.getUserId(token) + "' not found");
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Person> updatePerson(UserId id, Person person, SecurityToken token)
+      throws ProtocolException {
+    try {
+      String viewer = token.getViewerId(); // viewer
+      String user = id.getUserId(token); // person to update
+
+      if (!viewerCanUpdatePerson(viewer,user)) {
+        throw new ProtocolException(HttpServletResponse.SC_FORBIDDEN, "User '" + viewer + "' does not have enough privileges to update person '"+user+"'");
+      }
+
+      JSONArray people = db.getJSONArray(PEOPLE_TABLE);
+
+      for (int i = 0; i < people.length(); i++) {
+        JSONObject curPerson = people.getJSONObject(i);
+
+        if (user != null && curPerson.getString(Person.Field.ID.toString()).equals(user)) {
+          // Convert user to JSON and set ID
+          JSONObject jsonPerson = convertToJson(person);
+          // go through all properties to update in the submitted person object
+          // and change them in the current person object
+          for (String key : JSONObject.getNames(jsonPerson)) {
+            curPerson.put(key,jsonPerson.get(key));
+          }
+
+          people.put(i,curPerson);
+          return Futures.immediateFuture(converter.convertToObject(curPerson.toString(), Person.class));
+        }
+      }
+
+      // Error - no album found to update with given ID
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "User ID " + user + " does not exist");
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+
+  }
+
+  /** Check if a viewer is allowed to update the given person record. **/
+  protected boolean viewerCanUpdatePerson(String viewer, String person) {
+    // A person can only update his own personal data (by default)
+    // if you wish to allow other people to update the personal data of the user
+    // you should change the current function
+    return viewer.equals(person) ? true : false;
+  }
+
+  private Map<String, Object> getPersonAppData(String id, Set<String> fields) {
+    try {
+      Map<String, Object> appData = null;
+      JSONObject personData = db.getJSONObject(DATA_TABLE).optJSONObject(id);
+      if (personData != null) {
+        if (fields.contains(Person.Field.APP_DATA.toString())) {
+          appData = Maps.newHashMap();
+          @SuppressWarnings("unchecked")
+          Iterator<String> keys = personData.keys();
+          while (keys.hasNext()) {
+            String key = keys.next();
+            appData.put(key, personData.get(key));
+          }
+        } else {
+          String appDataPrefix = Person.Field.APP_DATA.toString() + '.';
+          for (String field : fields) {
+            if (field.startsWith(appDataPrefix)) {
+              if (appData == null) {
+                appData = Maps.newHashMap();
+              }
+
+              String appDataField = field.substring(appDataPrefix.length());
+              if (personData.has(appDataField)) {
+                appData.put(appDataField, personData.get(appDataField));
+              }
+            }
+          }
+        }
+      }
+
+      return appData;
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<DataCollection> getPersonData(Set<UserId> userIds, GroupId groupId, String appId,
+      Set<String> fields, SecurityToken token) throws ProtocolException {
+    try {
+      Map<String, Map<String, String>> idToData = Maps.newHashMap();
+      Set<String> idSet = getIdSet(userIds, groupId, token);
+      for (String id : idSet) {
+        JSONObject personData;
+        if (!db.getJSONObject(DATA_TABLE).has(id)) {
+          personData = new JSONObject();
+        } else {
+          if (!fields.isEmpty()) {
+            personData = new JSONObject(db.getJSONObject(DATA_TABLE).getJSONObject(id), fields
+                .toArray(new String[fields.size()]));
+          } else {
+            personData = db.getJSONObject(DATA_TABLE).getJSONObject(id);
+          }
+        }
+
+        // TODO: We can use the converter here to do this for us
+
+        // JSONObject keys are always strings
+        @SuppressWarnings("unchecked")
+        Iterator<String> keys = personData.keys();
+        Map<String, String> data = Maps.newHashMap();
+        while (keys.hasNext()) {
+          String key = keys.next();
+          data.put(key, personData.getString(key));
+        }
+        idToData.put(id, data);
+      }
+      return Futures.immediateFuture(new DataCollection(idToData));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> deletePersonData(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, SecurityToken token) throws ProtocolException {
+    try {
+      String user = userId.getUserId(token);
+      if (!db.getJSONObject(DATA_TABLE).has(user)) {
+        return null;
+      }
+      JSONObject newPersonData = new JSONObject();
+      JSONObject oldPersonData = db.getJSONObject(DATA_TABLE).getJSONObject(user);
+
+      // JSONObject keys are always strings
+      @SuppressWarnings("unchecked")
+      Iterator<String> keys = oldPersonData.keys();
+      while (keys.hasNext()) {
+        String key = keys.next();
+        if (!fields.contains(key)) {
+          newPersonData.put(key, oldPersonData.getString(key));
+        }
+      }
+      db.getJSONObject(DATA_TABLE).put(user, newPersonData);
+      return Futures.immediateFuture(null);
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> updatePersonData(UserId userId, GroupId groupId, String appId,
+      Set<String> fields, Map<String, Object> values, SecurityToken token)
+      throws ProtocolException {
+    // TODO: this seems redundant. No need to pass both fields and a map of
+    // field->value
+    // TODO: According to rest, yes there is. If a field is in the param list
+    // but not in the map
+    // that means it is a delete
+
+    try {
+      JSONObject personData = db.getJSONObject(DATA_TABLE).getJSONObject(userId.getUserId(token));
+      if (personData == null) {
+        personData = new JSONObject();
+        db.getJSONObject(DATA_TABLE).put(userId.getUserId(token), personData);
+      }
+
+      for (Map.Entry<String, Object> entry : values.entrySet()) {
+        personData.put(entry.getKey(), entry.getValue());
+      }
+      return Futures.immediateFuture(null);
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<Group>> getGroups(UserId userId,
+		CollectionOptions options, Set<String> fields, SecurityToken token)
+		throws ProtocolException {
+    List<Group> result = Lists.newArrayList();
+    String user = userId.getUserId(token);
+    try {
+      JSONArray groups = db.getJSONObject(GROUPS_TABLE).getJSONArray(user);
+
+      for (int i = 0; i < groups.length(); i++) {
+        JSONObject group = groups.getJSONObject(i);
+
+        Group groupObj = filterFields(group, fields, Group.class);
+        result.add(groupObj);
+      }
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+
+    return Futures.immediateFuture(new RestfulCollection<Group>(result));
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * Post a message for a set of users.
+   */
+  public Future<Void> createMessage(UserId userId, String appId, String msgCollId, Message message,
+      SecurityToken token) throws ProtocolException {
+    for (String recipient : message.getRecipients()) {
+      try {
+        JSONArray outbox = db.getJSONObject(MESSAGE_TABLE).getJSONArray(recipient);
+        if (outbox == null) {
+          outbox = new JSONArray();
+          db.getJSONObject(MESSAGE_TABLE).put(recipient, outbox);
+        }
+
+        outbox.put(message);
+      } catch (JSONException je) {
+        throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+            je);
+      }
+    }
+
+    return Futures.immediateFuture(null);
+  }
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<MessageCollection>> getMessageCollections(UserId userId,
+      Set<String> fields, CollectionOptions options, SecurityToken token) throws ProtocolException {
+    try {
+      List<MessageCollection> result = Lists.newArrayList();
+      JSONObject messageCollections = db.getJSONObject(MESSAGE_TABLE).getJSONObject(
+          userId.getUserId(token));
+      for (String msgCollId : JSONObject.getNames(messageCollections)) {
+        JSONObject msgColl = messageCollections.getJSONObject(msgCollId);
+        msgColl.put("id", msgCollId);
+        JSONArray messages = msgColl.getJSONArray("messages");
+        int numMessages = (messages == null) ? 0 : messages.length();
+        msgColl.put("total", String.valueOf(numMessages));
+        msgColl.put("unread", String.valueOf(numMessages));
+
+        result.add(filterFields(msgColl, fields, MessageCollection.class));
+      }
+      return Futures.immediateFuture(new RestfulCollection<MessageCollection>(result));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> deleteMessages(UserId userId, String msgCollId, List<String> ids,
+      SecurityToken token) throws ProtocolException {
+    throw new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+        "this functionality is not yet available");
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * Gets the messsages in an user's queue.
+   */
+  public Future<RestfulCollection<Message>> getMessages(UserId userId, String msgCollId,
+      Set<String> fields, List<String> msgIds, CollectionOptions options, SecurityToken token)
+      throws ProtocolException {
+    try {
+      List<Message> result = Lists.newArrayList();
+      JSONArray messages = db.getJSONObject(MESSAGE_TABLE).getJSONObject(userId.getUserId(token))
+          .getJSONObject(msgCollId).getJSONArray("messages");
+
+      // TODO: special case @all
+
+      if (messages == null) {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "message collection"
+            + msgCollId + " not found");
+      }
+
+      // TODO: filter and sort outbox.
+      for (int i = 0; i < messages.length(); i++) {
+        JSONObject msg = messages.getJSONObject(i);
+        result.add(filterFields(msg, fields, Message.class));
+      }
+
+      return Futures.immediateFuture(new RestfulCollection<Message>(result));
+
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(),
+          je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<MessageCollection> createMessageCollection(UserId userId,
+      MessageCollection msgCollection, SecurityToken token) throws ProtocolException {
+    throw new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+        "this functionality is not yet available");
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> modifyMessage(UserId userId, String msgCollId, String messageId,
+      Message message, SecurityToken token) throws ProtocolException {
+    throw new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+        "this functionality is not yet available");
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> modifyMessageCollection(UserId userId, MessageCollection msgCollection,
+      SecurityToken token) throws ProtocolException {
+    throw new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+        "this functionality is not yet available");
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> deleteMessageCollection(UserId userId, String msgCollId, SecurityToken token)
+      throws ProtocolException {
+    throw new ProtocolException(HttpServletResponse.SC_NOT_IMPLEMENTED,
+        "this functionality is not yet available");
+  }
+
+  /**
+   * Public methods for use with Authentication Classes
+   *
+   * @param username a {@link java.lang.String} object.
+   * @return a {@link java.lang.String} object.
+   */
+  public String getPassword(String username) {
+    try {
+      return db.getJSONObject(PASSWORDS_TABLE).getString(username);
+    } catch (JSONException e) {
+      return null;
+    }
+  }
+
+  private Set<String> getIdSet(UserId user, GroupId group, SecurityToken token)
+      throws JSONException {
+    String userId = user.getUserId(token);
+
+    if (group == null) {
+      return ImmutableSortedSet.of(userId);
+    }
+
+    Set<String> returnVal = Sets.newLinkedHashSet();
+    switch (group.getType()) {
+    case all:
+    case friends:
+      if (db.getJSONObject(FRIEND_LINK_TABLE).has(userId)) {
+        JSONArray friends = db.getJSONObject(FRIEND_LINK_TABLE).getJSONArray(userId);
+        for (int i = 0; i < friends.length(); i++) {
+          returnVal.add(friends.getString(i));
+        }
+      }
+      break;
+    case objectId:
+      if (db.getJSONObject(GROUP_MEMBERS_TABLE).has(group.toString())) {
+        JSONArray groupMembers = db.getJSONObject(GROUP_MEMBERS_TABLE).getJSONArray(group.toString());
+        for (int i = 0; i < groupMembers.length(); i++) {
+          returnVal.add(groupMembers.getString(i));
+        }
+      }
+      break;
+    case self:
+      returnVal.add(userId);
+      break;
+    }
+    return returnVal;
+  }
+
+  /**
+   * Get the set of user id's for a set of users and a group
+   *
+   * @param users set of UserIds
+   * @param group the group
+   * @param token a token
+   * @return set of Id strings
+   * @throws org.json.JSONException if errors in Json
+   */
+  public Set<String> getIdSet(Set<UserId> users, GroupId group, SecurityToken token)
+      throws JSONException {
+    Set<String> ids = Sets.newLinkedHashSet();
+    for (UserId user : users) {
+      ids.addAll(getIdSet(user, group, token));
+    }
+    return ids;
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<Album> getAlbum(UserId userId, String appId, Set<String> fields,
+                                String albumId, SecurityToken token) throws ProtocolException {
+    try {
+      // First ensure user has a table
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+        // Retrieve user's albums
+        JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+
+        // Search albums for given ID and owner
+        JSONObject album;
+        for (int i = 0; i < userAlbums.length(); i++) {
+          album = userAlbums.getJSONObject(i);
+          if (album.getString(Album.Field.ID.toString()).equals(albumId) &&
+              album.getString(Album.Field.OWNER_ID.toString()).equals(user)) {
+            return Futures.immediateFuture(filterFields(album, fields, Album.class));
+          }
+        }
+      }
+
+      // Album wasn't found
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<Album>> getAlbums(UserId userId, String appId,
+                                                    Set<String> fields, CollectionOptions options, Set<String> albumIds,
+                                                    SecurityToken token) throws ProtocolException {
+    try {
+      // Ensure user has a table
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+        // Get user's albums
+        JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+
+        // Stores target albums
+        List<Album> result = Lists.newArrayList();
+
+        // Search for every albumId
+        boolean found;
+        JSONObject curAlbum;
+        for (String albumId : albumIds) {
+          // Search albums for this albumId
+          found = false;
+          for (int i = 0; i < userAlbums.length(); i++) {
+            curAlbum = userAlbums.getJSONObject(i);
+            if (curAlbum.getString(Album.Field.ID.toString()).equals(albumId) &&
+                curAlbum.getString(Album.Field.OWNER_ID.toString()).equals(user)) {
+              result.add(filterFields(curAlbum, fields, Album.class));
+              found = true;
+              break;
+            }
+          }
+
+          // Error - albumId not found
+          if (!found) {
+            throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+          }
+        }
+
+        // Return found albums
+        return Futures.immediateFuture(new RestfulCollection<Album>(result));
+      }
+
+      // Album table doesn't exist for user
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "User '" + user + "' has no albums");
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<Album>> getAlbums(Set<UserId> userIds,
+                                                    GroupId groupId, String appId, Set<String> fields,
+                                                    CollectionOptions options, SecurityToken token)
+      throws ProtocolException {
+    try {
+      List<Album> result = Lists.newArrayList();
+      Set<String> idSet = getIdSet(userIds, groupId, token);
+
+      // Gather albums for all user IDs
+      for (String id : idSet) {
+        if (db.getJSONObject(ALBUMS_TABLE).has(id)) {
+          JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(id);
+          for (int i = 0; i < userAlbums.length(); i++) {
+            JSONObject album = userAlbums.getJSONObject(i);
+            if (album.getString(Album.Field.OWNER_ID.toString()).equals(id)) {
+              result.add(filterFields(album, fields, Album.class));
+            }
+          }
+        }
+      }
+      return Futures.immediateFuture(new RestfulCollection<Album>(result));
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<Void> deleteAlbum(UserId userId, String appId, String albumId,
+                                  SecurityToken token) throws ProtocolException {
+    try {
+      boolean targetFound = false;      // indicates if target album is found
+      JSONArray newAlbums = new JSONArray();  // list of albums minus target
+      String user = userId.getUserId(token);  // retrieve user id
+
+      // First ensure user has a table
+      if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+        // Get user's albums
+        JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+
+        // Compose new list of albums excluding album to be deleted
+        JSONObject curAlbum;
+        for (int i = 0; i < userAlbums.length(); i++) {
+          curAlbum = userAlbums.getJSONObject(i);
+          if (curAlbum.getString(Album.Field.ID.toString()).equals(albumId)) {
+            targetFound = true;
+          } else {
+            newAlbums.put(curAlbum);
+          }
+        }
+      }
+
+      // Overwrite user's albums with updated list if album found
+      if (targetFound) {
+        db.getJSONObject(ALBUMS_TABLE).put(user, newAlbums);
+        return Futures.immediateFuture(null);
+      } else {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+      }
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: userId and album's ownerId don't have to match - potential problem
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<Void> createAlbum(UserId userId, String appId, Album album,
+                                  SecurityToken token) throws ProtocolException {
+    try {
+      // Get table of user's albums
+      String user = userId.getUserId(token);
+      JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+      if (userAlbums == null) {
+        userAlbums = new JSONArray();
+        db.getJSONObject(ALBUMS_TABLE).put(user, userAlbums);
+      }
+
+      // Convert album to JSON and set ID & owner
+      JSONObject jsonAlbum = convertToJson(album);
+      if (!jsonAlbum.has(Album.Field.ID.toString())) {
+        jsonAlbum.put(Album.Field.ID.toString(), System.currentTimeMillis());
+      }
+      if (!jsonAlbum.has(Album.Field.OWNER_ID.toString())) {
+        jsonAlbum.put(Album.Field.OWNER_ID.toString(), user);
+      }
+
+      // Insert new album into table
+      userAlbums.put(jsonAlbum);
+      return Futures.immediateFuture(null);
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<Void> updateAlbum(UserId userId, String appId, Album album,
+                                  String albumId, SecurityToken token) throws ProtocolException {
+    try {
+      // First ensure user has a table
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ALBUMS_TABLE).has(user)) {
+        // Retrieve user's albums
+        JSONArray userAlbums = db.getJSONObject(ALBUMS_TABLE).getJSONArray(user);
+
+        // Convert album to JSON and set ID
+        JSONObject jsonAlbum = convertToJson(album);
+        jsonAlbum.put(Album.Field.ID.toString(), albumId);
+
+        // Iterate through albums to identify album to update
+        for (int i = 0; i < userAlbums.length(); i++) {
+          JSONObject curAlbum = userAlbums.getJSONObject(i);
+          if (curAlbum.getString(Album.Field.ID.toString()).equals(albumId)) {
+            userAlbums.put(i, jsonAlbum);
+            return Futures.immediateFuture(null);
+          }
+        }
+      }
+
+      // Error - no album found to update with given ID
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<MediaItem> getMediaItem(UserId userId, String appId,
+                                        String albumId, String mediaItemId, Set<String> fields,
+                                        SecurityToken token) throws ProtocolException {
+    try {
+      // First ensure user has a table
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+        // Retrieve user's MediaItems
+        JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+
+        // Search user's MediaItems for given ID and album
+        JSONObject mediaItem;
+        for (int i = 0; i < userMediaItems.length(); i++) {
+          mediaItem = userMediaItems.getJSONObject(i);
+          if (mediaItem.getString(MediaItem.Field.ID.toString()).equals(mediaItemId) &&
+              mediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+            return Futures.immediateFuture(filterFields(mediaItem, fields, MediaItem.class));
+          }
+        }
+      }
+
+      // MediaItem wasn't found
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID '" + mediaItemId + "' does not exist within Album '" + albumId + '\'');
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+                                                            String appId, String albumId, Set<String> mediaItemIds,
+                                                            Set<String> fields, CollectionOptions options, SecurityToken token)
+      throws ProtocolException {
+    try {
+      // Ensure user has a table
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+        // Get user's MediaItems
+        JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+
+        // Stores found MediaItems
+        List<MediaItem> result = Lists.newArrayList();
+
+        // Search for every MediaItem ID target
+        boolean found;
+        JSONObject curMediaItem;
+        for (String mediaItemId : mediaItemIds) {
+          // Search existing MediaItems for this MediaItem ID
+          found = false;
+          for (int i = 0; i < userMediaItems.length(); i++) {
+            curMediaItem = userMediaItems.getJSONObject(i);
+            if (curMediaItem.getString(MediaItem.Field.ID.toString()).equals(albumId) &&
+                curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+              result.add(filterFields(curMediaItem, fields, MediaItem.class));
+              found = true;
+              break;
+            }
+          }
+
+          // Error - MediaItem ID not found
+          if (!found) {
+            throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID " + mediaItemId + " does not exist within Album " + albumId);
+          }
+        }
+
+        // Return found MediaItems
+        return Futures.immediateFuture(new RestfulCollection<MediaItem>(result));
+      }
+
+      // Table doesn't exist for user
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem table not found for user " + user);
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<MediaItem>> getMediaItems(UserId userId,
+                                                            String appId, String albumId, Set<String> fields,
+                                                            CollectionOptions options, SecurityToken token)
+      throws ProtocolException {
+    try {
+      // First ensure user has a table
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+        // Retrieve user's MediaItems
+        JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+
+        // Stores target MediaItems
+        List<MediaItem> result = Lists.newArrayList();
+
+        // Search user's MediaItems for given album
+        JSONObject curMediaItem;
+        for (int i = 0; i < userMediaItems.length(); i++) {
+          curMediaItem = userMediaItems.getJSONObject(i);
+          if (curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+            result.add(filterFields(curMediaItem, fields, MediaItem.class));
+          }
+        }
+
+        // Return found MediaItems
+        return Futures.immediateFuture(new RestfulCollection<MediaItem>(result));
+      }
+
+      // Album wasn't found
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Album ID " + albumId + " does not exist");
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<MediaItem>> getMediaItems(
+      Set<UserId> userIds, GroupId groupId, String appId,
+      Set<String> fields, CollectionOptions options, SecurityToken token)
+      throws ProtocolException {
+    try {
+      List<MediaItem> result = Lists.newArrayList();
+      Set<String> idSet = getIdSet(userIds, groupId, token);
+
+      // Gather MediaItems for all user IDs
+      for (String id : idSet) {
+        if (db.getJSONObject(MEDIAITEMS_TABLE).has(id)) {
+          JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(id);
+          for (int i = 0; i < userMediaItems.length(); i++) {
+            result.add(filterFields(userMediaItems.getJSONObject(i), fields, MediaItem.class));
+          }
+        }
+      }
+      return Futures.immediateFuture(new RestfulCollection<MediaItem>(result));
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<Void> deleteMediaItem(UserId userId, String appId,
+                                      String albumId, String mediaItemId, SecurityToken token)
+      throws ProtocolException {
+    try {
+      boolean targetFound = false;        // indicates if target MediaItem is found
+      JSONArray newMediaItems = new JSONArray();  // list of MediaItems minus target
+      String user = userId.getUserId(token);    // retrieve user id
+
+      // First ensure user has a table
+      if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+        // Get user's MediaItems
+        JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+
+        // Compose new list of MediaItems excluding item to be deleted
+        JSONObject curMediaItem;
+        for (int i = 0; i < userMediaItems.length(); i++) {
+          curMediaItem = userMediaItems.getJSONObject(i);
+          if (curMediaItem.getString(MediaItem.Field.ID.toString()).equals(mediaItemId) &&
+              curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+            targetFound = true;
+          } else {
+            newMediaItems.put(curMediaItem);
+          }
+        }
+      }
+
+      // Overwrite user's MediaItems with updated list if target found
+      if (targetFound) {
+        db.getJSONObject(MEDIAITEMS_TABLE).put(user, newMediaItems);
+        return Futures.immediateFuture(null);
+      } else {
+        throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID " + mediaItemId + " does not exist existin within Album " + albumId);
+      }
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<Void> createMediaItem(UserId userId, String appId,
+                                      String albumId, MediaItem mediaItem, SecurityToken token)
+      throws ProtocolException {
+    try {
+      // Get table of user's MediaItems
+      JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(userId.getUserId(token));
+      if (userMediaItems == null) {
+        userMediaItems = new JSONArray();
+        db.getJSONObject(MEDIAITEMS_TABLE).put(userId.getUserId(token), userMediaItems);
+      }
+
+      // Convert MediaItem to JSON and set ID & Album ID
+      JSONObject jsonMediaItem = convertToJson(mediaItem);
+      jsonMediaItem.put(MediaItem.Field.ALBUM_ID.toString(), albumId);
+      if (!jsonMediaItem.has(MediaItem.Field.ID.toString())) {
+        jsonMediaItem.put(MediaItem.Field.ID.toString(), System.currentTimeMillis());
+      }
+
+      // Insert new MediaItem into table
+      userMediaItems.put(jsonMediaItem);
+      return Futures.immediateFuture(null);
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // TODO: not using appId
+
+  /** {@inheritDoc} */
+  public Future<Void> updateMediaItem(UserId userId, String appId,
+                                      String albumId, String mediaItemId, MediaItem mediaItem,
+                                      SecurityToken token) throws ProtocolException {
+    try {
+      // First ensure user has a table
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(MEDIAITEMS_TABLE).has(user)) {
+        // Retrieve user's MediaItems
+        JSONArray userMediaItems = db.getJSONObject(MEDIAITEMS_TABLE).getJSONArray(user);
+
+        // Convert MediaItem to JSON and set ID & Album ID
+        JSONObject jsonMediaItem = convertToJson(mediaItem);
+        jsonMediaItem.put(MediaItem.Field.ID.toString(), mediaItemId);
+        jsonMediaItem.put(MediaItem.Field.ALBUM_ID.toString(), albumId);
+
+        // Iterate through MediaItems to identify item to update
+        for (int i = 0; i < userMediaItems.length(); i++) {
+          JSONObject curMediaItem = userMediaItems.getJSONObject(i);
+          if (curMediaItem.getString(MediaItem.Field.ID.toString()).equals(mediaItemId) &&
+              curMediaItem.getString(MediaItem.Field.ALBUM_ID.toString()).equals(albumId)) {
+            userMediaItems.put(i, jsonMediaItem);
+            return Futures.immediateFuture(null);
+          }
+        }
+      }
+
+      // Error - no MediaItem found with given ID and Album ID
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "MediaItem ID " + mediaItemId + " does not exist existin within Album " + albumId);
+    } catch (JSONException je) {
+      throw new ProtocolException(
+          HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+          je.getMessage(), je);
+    }
+  }
+
+  // Are fields really needed here?
+  /** {@inheritDoc} */
+  public Future<ActivityEntry> updateActivityEntry(UserId userId, GroupId groupId, String appId,
+        Set<String> fields, ActivityEntry activityEntry, String activityId, SecurityToken token) throws ProtocolException {
+    try {
+      JSONObject jsonEntry = convertFromActivityEntry(activityEntry, fields);
+      if (!jsonEntry.has(ActivityEntry.Field.ID.toString())) {
+        if (activityId != null) {
+          jsonEntry.put(ActivityEntry.Field.ID.toString(), activityId);
+        } else {
+          jsonEntry.put(ActivityEntry.Field.ID.toString(), System.currentTimeMillis());
+        }
+      }
+      activityId = jsonEntry.getString(ActivityEntry.Field.ID.toString());
+
+      JSONArray jsonArray;
+      if (db.getJSONObject(ACTIVITYSTREAMS_TABLE).has(userId.getUserId(token))) {
+        jsonArray = db.getJSONObject(ACTIVITYSTREAMS_TABLE).getJSONArray(userId.getUserId(token));
+      } else {
+        jsonArray = new JSONArray();
+        db.getJSONObject(ACTIVITYSTREAMS_TABLE).put(userId.getUserId(token), jsonArray);
+      }
+
+      // Find & replace activity
+      for (int i = 0; i < jsonArray.length(); i++) {
+        JSONObject entry = jsonArray.getJSONObject(i);
+        if (entry.getString(ActivityEntry.Field.ID.toString()).equals(activityId)) {
+          jsonArray.put(i, jsonEntry);
+          return Futures.immediateFuture(filterFields(jsonEntry, fields, ActivityEntry.class));
+        }
+      }
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Activity not found: " + activityId);
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+  }
+
+  // Are fields really needed here?
+  /** {@inheritDoc} */
+  public Future<ActivityEntry> createActivityEntry(UserId userId, GroupId groupId, String appId,
+        Set<String> fields, ActivityEntry activityEntry, SecurityToken token) throws ProtocolException {
+    try {
+      JSONObject jsonEntry = convertFromActivityEntry(activityEntry, fields);
+      if (!jsonEntry.has(ActivityEntry.Field.ID.toString())) {
+        jsonEntry.put(ActivityEntry.Field.ID.toString(), System.currentTimeMillis());
+      }
+      String activityId = jsonEntry.getString(ActivityEntry.Field.ID.toString());
+
+      JSONArray jsonArray;
+      if (db.getJSONObject(ACTIVITYSTREAMS_TABLE).has(userId.getUserId(token))) {
+        jsonArray = db.getJSONObject(ACTIVITYSTREAMS_TABLE).getJSONArray(userId.getUserId(token));
+      } else {
+        jsonArray = new JSONArray();
+        db.getJSONObject(ACTIVITYSTREAMS_TABLE).put(userId.getUserId(token), jsonArray);
+      }
+
+      // Ensure activity does not already exist
+      for (int i = 0; i < jsonArray.length(); i++) {
+        JSONObject entry = jsonArray.getJSONObject(i);
+        if (entry.getString(ActivityEntry.Field.ID.toString()).equals(activityId)) {
+          throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Activity already exists: " + activityId);
+        }
+      }
+      jsonArray.put(jsonEntry);
+      return Futures.immediateFuture(filterFields(jsonEntry, fields, ActivityEntry.class));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<Void> deleteActivityEntries(UserId userId, GroupId groupId,
+      String appId, Set<String> activityIds, SecurityToken token) throws ProtocolException {
+    try {
+      String user = userId.getUserId(token);
+
+      if (db.getJSONObject(ACTIVITYSTREAMS_TABLE).has(user)) {
+        JSONArray activityEntries = db.getJSONObject(ACTIVITYSTREAMS_TABLE).getJSONArray(user);
+
+        if (activityEntries != null) {
+          JSONArray newList = new JSONArray();
+          for (int i = 0; i < activityEntries.length(); i++) {
+            JSONObject activityEntry = activityEntries.getJSONObject(i);
+            if (!activityIds.contains(activityEntry.getString(ActivityEntry.Field.ID.toString()))) {
+              newList.put(activityEntry);
+            }
+          }
+          db.getJSONObject(ACTIVITYSTREAMS_TABLE).put(user, newList);
+        }
+      }
+      return Futures.immediateFuture(null);
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<ActivityEntry> getActivityEntry(UserId userId, GroupId groupId,
+      String appId, Set<String> fields, String activityId, SecurityToken token)
+      throws ProtocolException {
+    try {
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ACTIVITYSTREAMS_TABLE).has(user)) {
+        JSONArray activityEntries = db.getJSONObject(ACTIVITYSTREAMS_TABLE).getJSONArray(user);
+        for (int i = 0; i < activityEntries.length(); i++) {
+          JSONObject activityEntry = activityEntries.getJSONObject(i);
+          if (activityEntry.getString(ActivityEntry.Field.ID.toString()).equals(activityId)) {
+            return Futures.immediateFuture(filterFields(activityEntry, fields, ActivityEntry.class));
+          }
+        }
+      }
+      throw new ProtocolException(HttpServletResponse.SC_BAD_REQUEST, "Activity not found: " + activityId);
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+  }
+
+
+/** {@inheritDoc} */
+  public Future<RestfulCollection<ActivityEntry>> getActivityEntries(
+      Set<UserId> userIds, GroupId groupId, String appId, Set<String> fields,
+      CollectionOptions options, SecurityToken token)
+      throws ProtocolException {
+      List<ActivityEntry> result = Lists.newArrayList();
+    try {
+      Set<String> idSet = getIdSet(userIds, groupId, token);
+      for (String id : idSet) {
+        if (db.getJSONObject(ACTIVITYSTREAMS_TABLE).has(id)) {
+          JSONArray activityEntries = db.getJSONObject(ACTIVITYSTREAMS_TABLE).getJSONArray(id);
+          for (int i = 0; i < activityEntries.length(); i++) {
+            JSONObject activityEntry = activityEntries.getJSONObject(i);
+            result.add(filterFields(activityEntry, fields, ActivityEntry.class));
+            // TODO: ActivityStreams don't have appIds
+          }
+        }
+      }
+      Collections.sort(result, Collections.reverseOrder());
+      return Futures.immediateFuture(new RestfulCollection<ActivityEntry>(result));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+  }
+
+  /** {@inheritDoc} */
+  public Future<RestfulCollection<ActivityEntry>> getActivityEntries(
+      UserId userId, GroupId groupId, String appId, Set<String> fields,
+      CollectionOptions options, Set<String> activityIds, SecurityToken token)
+      throws ProtocolException {
+    List<ActivityEntry> result = Lists.newArrayList();
+    try {
+      String user = userId.getUserId(token);
+      if (db.getJSONObject(ACTIVITYSTREAMS_TABLE).has(user)) {
+        JSONArray activityEntries = db.getJSONObject(ACTIVITYSTREAMS_TABLE).getJSONArray(user);
+        for(String activityId : activityIds) {
+          boolean found = false;
+          for (int i = 0; i < activityEntries.length(); i++) {
+            JSONObject activityEntry = activityEntries.getJSONObject(i);
+            if (activityEntry.getString(ActivityEntry.Field.ID.toString()).equals(activityId)) {
+              result.add(filterFields(activityEntry, fields, ActivityEntry.class));
+              found = true;
+              break;
+            }
+          }
+          if (!found) {
+            throw new ProtocolException(HttpServletResponse.SC_NOT_FOUND, "Activity not found: " + activityId);
+          }
+        }
+      }
+      Collections.sort(result, Collections.reverseOrder());
+      return Futures.immediateFuture(new RestfulCollection<ActivityEntry>(result));
+    } catch (JSONException je) {
+      throw new ProtocolException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, je.getMessage(), je);
+    }
+  }
+
+  // TODO Why specifically handle Activity instead of generic POJO (below)?
+
+  private JSONObject convertFromActivity(Activity activity, Set<String> fields)
+      throws JSONException {
+    // TODO Not using fields yet
+    return new JSONObject(converter.convertToString(activity));
+  }
+
+  private JSONObject convertFromActivityEntry(ActivityEntry activityEntry, Set<String> fields) throws JSONException {
+    // TODO Not using fields yet
+    return new JSONObject(converter.convertToString(activityEntry));
+  }
+
+  private JSONObject convertToJson(Object object) throws JSONException {
+    // TODO not using fields yet
+    return new JSONObject(converter.convertToString(object));
+  }
+
+  public <T> T filterFields(JSONObject object, Set<String> fields,
+                            Class<T> clz) throws JSONException {
+    if (!fields.isEmpty()) {
+      // Create a copy with just the specified fields
+      object = new JSONObject(object, fields.toArray(new String[fields
+          .size()]));
+    }
+    String objectVal = object.toString();
+    if (authority != null) {
+      objectVal = objectVal.replace("%origin%", authority.getOrigin());
+    } else {
+      //provide default for junit tests
+      objectVal = objectVal.replace("%origin%", "http://localhost:8080");
+    }
+    return converter.convertToObject(objectVal, clz);
+  }
+}
diff --git a/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/package-info.java b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/package-info.java
new file mode 100644
index 0000000..37a1439
--- /dev/null
+++ b/trunk/java/social-api/src/main/java/org/apache/shindig/social/sample/spi/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+/**
+ * <h1>Sample SPI implementation</h1>
+ * <p>A Sample implementation of the SPI, using a JSON DB
+ * implementation. This container is used for end to end testing of the
+ * Social API</p>
+ */
+package org.apache.shindig.social.sample.spi;
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/SocialApiTestsGuiceModule.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/SocialApiTestsGuiceModule.java
new file mode 100644
index 0000000..b40412c
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/SocialApiTestsGuiceModule.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social;
+
+import java.util.Set;
+
+import org.apache.shindig.common.servlet.ParameterFetcher;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.protocol.DataServiceServletFetcher;
+import org.apache.shindig.protocol.conversion.BeanConverter;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.conversion.BeanXStreamConverter;
+import org.apache.shindig.protocol.conversion.xstream.XStreamConfiguration;
+import org.apache.shindig.social.core.oauth2.OAuth2DataService;
+import org.apache.shindig.social.core.oauth2.OAuth2DataServiceImpl;
+import org.apache.shindig.social.core.oauth2.OAuth2Service;
+import org.apache.shindig.social.core.oauth2.OAuth2ServiceImpl;
+import org.apache.shindig.social.core.util.xstream.XStream081Configuration;
+import org.apache.shindig.social.opensocial.service.ActivityHandler;
+import org.apache.shindig.social.opensocial.service.ActivityStreamHandler;
+import org.apache.shindig.social.opensocial.service.AppDataHandler;
+import org.apache.shindig.social.opensocial.service.MessageHandler;
+import org.apache.shindig.social.opensocial.service.PersonHandler;
+import org.apache.shindig.social.opensocial.spi.ActivityService;
+import org.apache.shindig.social.opensocial.spi.ActivityStreamService;
+import org.apache.shindig.social.opensocial.spi.AppDataService;
+import org.apache.shindig.social.opensocial.spi.MessageService;
+import org.apache.shindig.social.opensocial.spi.PersonService;
+import org.apache.shindig.social.sample.spi.JsonDbOpensocialService;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+
+/**
+ * Provides social api component injection for all large tests
+ */
+public class SocialApiTestsGuiceModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(ParameterFetcher.class).annotatedWith(Names.named("DataServiceServlet"))
+        .to(DataServiceServletFetcher.class);
+
+    bind(ActivityService.class).to(JsonDbOpensocialService.class);
+    bind(ActivityStreamService.class).to(JsonDbOpensocialService.class);
+    bind(AppDataService.class).to(JsonDbOpensocialService.class);
+    bind(MessageService.class).to(JsonDbOpensocialService.class);
+    bind(PersonService.class).to(JsonDbOpensocialService.class);
+
+    bind(String.class).annotatedWith(Names.named("shindig.canonical.json.db"))
+        .toInstance("sampledata/canonicaldb.json");
+
+    bind(XStreamConfiguration.class).to(XStream081Configuration.class);
+    bind(BeanConverter.class).annotatedWith(Names.named("shindig.bean.converter.xml")).to(
+        BeanXStreamConverter.class);
+    bind(BeanConverter.class).annotatedWith(Names.named("shindig.bean.converter.json")).to(
+        BeanJsonConverter.class);
+
+    bind(new TypeLiteral<Set<Object>>(){}).annotatedWith(
+        Names.named("org.apache.shindig.handlers"))
+        .toInstance(ImmutableSet.<Object>of(ActivityHandler.class, AppDataHandler.class,
+            PersonHandler.class, MessageHandler.class, ActivityStreamHandler.class));
+
+    bindConstant().annotatedWith(Names.named("shindig.containers.default"))
+        .to("res://containers/default/container.js");
+    bindConstant().annotatedWith(Names.named("shindig.port")).to("8080");
+    bindConstant().annotatedWith(Names.named("shindig.host")).to("localhost");
+    bindConstant().annotatedWith(Names.named("shindig.contextroot")).to("");
+    bind(ContainerConfig.class).to(JsonContainerConfig.class);
+
+    bind(Integer.class).annotatedWith(
+        Names.named("shindig.cache.lru.default.capacity"))
+        .toInstance(10);
+    bind(OAuth2Service.class).to(OAuth2ServiceImpl.class);
+    bind(OAuth2DataService.class).to(OAuth2DataServiceImpl.class);
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/config/SocialApiGuiceModuleTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/config/SocialApiGuiceModuleTest.java
new file mode 100644
index 0000000..2945045
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/config/SocialApiGuiceModuleTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.config;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.social.core.oauth.AuthenticationHandlerProvider;
+import org.apache.shindig.social.core.oauth2.OAuth2Service;
+import org.apache.shindig.social.core.oauth2.OAuth2ServiceImpl;
+import org.apache.shindig.social.opensocial.oauth.OAuthDataStore;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class SocialApiGuiceModuleTest extends Assert {
+  private Injector injector;
+
+  @Before
+  public void setUp() throws Exception {
+    injector = Guice.createInjector(new SocialApiGuiceModule(), new PropertiesModule(),
+        new AbstractModule() {
+          @Override
+          protected void configure() {
+            bind(OAuthDataStore.class).toInstance(EasyMock.createMock(OAuthDataStore.class));
+            bind(OAuth2Service.class).toInstance(EasyMock.createMock(OAuth2ServiceImpl.class));
+          }
+    });
+  }
+
+  /**
+   * Test default auth handler injection
+   */
+  @Test
+  public void testAuthHandler() {
+    injector.getInstance(AuthenticationHandlerProvider.class).get();
+
+    AuthenticationHandlerProvider provider =
+        injector.getInstance(AuthenticationHandlerProvider.class);
+    assertEquals(4, provider.get().size());
+
+    List<AuthenticationHandler> handlers = injector.getInstance(
+        Key.get(new TypeLiteral<List<AuthenticationHandler>>(){}));
+
+    assertEquals(4, handlers.size());
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/AuthenticationProviderHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/AuthenticationProviderHandlerTest.java
new file mode 100644
index 0000000..cb7a477
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/AuthenticationProviderHandlerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.common.PropertiesModule;
+import org.apache.shindig.social.core.config.SocialApiGuiceModule;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.List;
+
+
+public class AuthenticationProviderHandlerTest extends Assert {
+  /**
+   * Test that existing custom handlers won't be broken with the switch
+   * to injecting List<ProviderHandler>.
+   */
+  @Test
+  public void testCustomHandler() {
+    Injector injector = Guice.createInjector(new SocialApiGuiceModule(),
+        new CustomAuthHandlerProviderModule(), new PropertiesModule());
+
+    AuthenticationHandlerProvider provider = injector.getInstance(
+        AuthenticationHandlerProvider.class);
+    assertEquals(0, provider.get().size());
+
+    List<AuthenticationHandler> handlers = injector.getInstance(
+        Key.get(new TypeLiteral<List<AuthenticationHandler>>(){}));
+    assertEquals(0, handlers.size());
+  }
+
+  /**
+   * AuthenticationHandlerProvider with no handlers
+   */
+  public static class ProvidesNoHandlers extends AuthenticationHandlerProvider {
+    public ProvidesNoHandlers() {
+      super(null, null, null, null);
+    }
+
+    @Override
+    public List<AuthenticationHandler> get() {
+      return Collections.emptyList();
+    }
+  }
+
+  /**
+   * Module with a custom AuthenticationHandler
+   */
+  public static class CustomAuthHandlerProviderModule extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(AuthenticationHandlerProvider.class).to(ProvidesNoHandlers.class);
+    }
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/FakeOAuthRequest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/FakeOAuthRequest.java
new file mode 100644
index 0000000..0c72dee
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/FakeOAuthRequest.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthAccessor;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthMessage;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+
+import org.apache.shindig.auth.OAuthConstants;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.common.uri.Uri;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.common.util.CharsetUtil;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This is largely a copy of OAuthCommandLine with some tweaks for FakeHttpServletRequest
+ */
+public class FakeOAuthRequest {
+
+  public static enum BodySigning {
+    NONE,
+    HASH,
+    LEGACY
+  }
+
+  public static enum OAuthParamLocation {
+    AUTH_HEADER,
+    POST_BODY,
+    URI_QUERY
+  }
+
+  public static final String CONSUMER_KEY = "gadget:12345";
+  public static final String CONSUMER_SECRET = "secret";
+  public static final String REQUESTOR = "requestor12345";
+
+  final String method;
+  final String url;
+  final String body;
+  final String contentType;
+
+  public FakeOAuthRequest(String method, String url, String body, String contentType) {
+    this.method = method;
+    this.url = url;
+    this.body = body;
+    this.contentType = contentType;
+  }
+
+  public FakeHttpServletRequest sign(String token, OAuthParamLocation paramLocationEnum,
+      BodySigning bodySigning)
+      throws Exception {
+    return sign(CONSUMER_KEY, CONSUMER_SECRET, REQUESTOR, token,
+        (token == null) ? null :CONSUMER_SECRET,
+        paramLocationEnum, bodySigning);
+  }
+
+  public FakeHttpServletRequest sign(String consumerKey, String consumerSecret, String requestor,
+      String token, String tokenSecret, OAuthParamLocation paramLocationEnum,
+      BodySigning bodySigning)
+      throws Exception {
+    FakeHttpServletRequest request = new FakeHttpServletRequest(url);
+
+    List<OAuth.Parameter> oauthParams = Lists.newArrayList();
+    UriBuilder target = new UriBuilder(Uri.parse(url));
+    String query = target.getQuery();
+    target.setQuery(null);
+    oauthParams.addAll(OAuth.decodeForm(query));
+
+    if (body != null) {
+      if (OAuth.isFormEncoded(contentType)) {
+        oauthParams.addAll(OAuth.decodeForm(body));
+      } else if (bodySigning == BodySigning.LEGACY) {
+        oauthParams.add(new OAuth.Parameter(body, ""));
+      } else if (bodySigning == BodySigning.HASH) {
+        oauthParams.add(
+            new OAuth.Parameter(OAuthConstants.OAUTH_BODY_HASH,
+                new String(Base64.encodeBase64(DigestUtils.sha(body.getBytes())), "UTF-8")));
+      }
+    }
+
+    oauthParams.add(new OAuth.Parameter(OAuth.OAUTH_CONSUMER_KEY, consumerKey));
+    oauthParams.add(new OAuth.Parameter("xoauth_requestor_id", requestor));
+
+    OAuthConsumer consumer = new OAuthConsumer(null,consumerKey,consumerSecret, null);
+    OAuthAccessor accessor = new OAuthAccessor(consumer);
+    if (!Strings.isNullOrEmpty(token)) {
+      accessor.accessToken = token;
+      accessor.tokenSecret = tokenSecret;
+    }
+    OAuthMessage message = accessor.newRequestMessage(method, target.toString(), oauthParams);
+
+    List<Map.Entry<String, String>> entryList = selectOAuthParams(message);
+
+    switch (paramLocationEnum) {
+      case AUTH_HEADER:
+        request.setHeader("Authorization", getAuthorizationHeader(entryList));
+        break;
+      case POST_BODY:
+        if (!OAuth.isFormEncoded(contentType)) {
+          throw new RuntimeException(
+              "OAuth param location can only be post_body if post body is of " +
+                  "type x-www-form-urlencoded");
+        }
+        // All message params should be added if oauth params are added to body
+        for (Map.Entry<String, String> param : message.getParameters()) {
+          request.setParameter(param.getKey(), true, param.getValue());
+        }
+        String oauthData = OAuth.formEncode(message.getParameters());
+        request.setPostData(CharsetUtil.getUtf8Bytes(oauthData));
+        break;
+      case URI_QUERY:
+        request.setQueryString(Uri.parse(OAuth.addParameters(url, entryList)).getQuery());
+        break;
+    }
+
+    if (body != null && paramLocationEnum != OAuthParamLocation.POST_BODY) {
+      request.setContentType(contentType);
+      request.setPostData(body, "UTF-8");
+      if (contentType.contains(OAuth.FORM_ENCODED)) {
+        List<OAuth.Parameter> bodyParams = OAuth.decodeForm(body);
+        for (OAuth.Parameter bodyParam : bodyParams) {
+          request.setParameter(bodyParam.getKey(), bodyParam.getValue());
+        }
+      }
+    }
+    request.setMethod(method);
+
+    return request;
+  }
+
+  private static String getAuthorizationHeader(List<Map.Entry<String, String>> oauthParams) {
+    StringBuilder result = new StringBuilder("OAuth ");
+
+    boolean first = true;
+    for (Map.Entry<String, String> parameter : oauthParams) {
+      if (!first) {
+        result.append(", ");
+      } else {
+        first = false;
+      }
+      result.append(OAuth.percentEncode(parameter.getKey()))
+          .append("=\"")
+          .append(OAuth.percentEncode(parameter.getValue()))
+          .append('"');
+    }
+    return result.toString();
+  }
+
+  private static List<Map.Entry<String, String>> selectOAuthParams(OAuthMessage message)
+      throws IOException {
+    List<Map.Entry<String, String>> result = Lists.newArrayList();
+    for (Map.Entry<String, String> param : message.getParameters()) {
+      if (isContainerInjectedParameter(param.getKey())) {
+        result.add(param);
+      }
+    }
+    return result;
+  }
+
+  private static boolean isContainerInjectedParameter(String key) {
+    key = key.toLowerCase();
+    return key.startsWith("oauth") || key.startsWith("xoauth") || key.startsWith("opensocial");
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/MockServletOutputStream.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/MockServletOutputStream.java
new file mode 100644
index 0000000..e688156
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/MockServletOutputStream.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import org.apache.http.util.ByteArrayBuffer;
+
+import javax.servlet.ServletOutputStream;
+
+/**
+ * Used to capture the raw request response provided by servlet
+ */
+public class MockServletOutputStream extends ServletOutputStream {
+
+  private ByteArrayBuffer buffer = new ByteArrayBuffer(1024);
+
+  @Override
+  public void write(int b) {
+    buffer.append(b);
+  }
+
+  public byte[] getBuffer() {
+    return buffer.toByteArray();
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2AuthCodeFlowTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2AuthCodeFlowTest.java
new file mode 100644
index 0000000..043c992
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2AuthCodeFlowTest.java
@@ -0,0 +1,687 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.shindig.auth.AuthenticationHandler.InvalidAuthenticationException;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.social.core.oauth2.OAuth2AuthenticationHandler;
+import org.apache.shindig.social.core.oauth2.OAuth2Servlet;
+import org.apache.shindig.social.dataservice.integration.AbstractLargeRestfulTests;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+public class OAuth2AuthCodeFlowTest extends AbstractLargeRestfulTests {
+
+  protected static final String SIMPLE_ACCESS_TOKEN = "TEST_TOKEN";
+  protected static final String PUBLIC_CLIENT_ID = "testClient";
+  protected static final String PUBLIC_AUTH_CODE = "testClient_authcode_1";
+  protected static final String CONF_CLIENT_ID = "advancedAuthorizationCodeClient";
+  protected static final String CONF_CLIENT_SECRET = "advancedAuthorizationCodeClient_secret";
+  protected static final String CONF_AUTH_CODE = "advancedClient_authcode_1";
+
+  protected static final String PUBLIC_REDIRECT_URI = "http://localhost:8080/oauthclients/AuthorizationCodeClient";
+  protected static final String REDIRECT_URI = "http://localhost:8080/oauthclients/AuthorizationCodeClient/friends";
+
+  protected OAuth2Servlet servlet = null;
+
+  @Before
+  @Override
+  public void abstractLargeRestfulBefore() throws Exception {
+    super.abstractLargeRestfulBefore();
+    servlet = new OAuth2Servlet();
+    injector.injectMembers(servlet);
+  };
+
+  /**
+   * Test retrieving an access token using a public client
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessToken() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2");
+    req.setContentType("application/x-www-form-urlencoded");
+    req.setPostData(
+        "client_id=" + PUBLIC_CLIENT_ID
+            + "&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(PUBLIC_REDIRECT_URI, "UTF-8") + "&code="
+            + PUBLIC_AUTH_CODE, "UTF-8");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("bearer", tokenResponse.getString("token_type"));
+    assertNotNull(tokenResponse.getString("access_token"));
+    assertTrue(tokenResponse.getLong("expires_in") > 0);
+    verify();
+  }
+
+  /**
+   * Test retrieving an authorization code using a public client
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAuthorizationCode() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2");
+    req.setContentType("application/x-www-form-urlencoded");
+    req.setPostData(
+        "client_id=" + PUBLIC_CLIENT_ID + "&response_type=code&redirect_uri="
+            + URLEncoder.encode(PUBLIC_REDIRECT_URI, "UTF-8"), "UTF-8");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_FOUND));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    assertTrue(response == null || response.equals(""));
+    verify();
+    assertTrue(redirectURI.getValue()
+        .startsWith(PUBLIC_REDIRECT_URI + "?code="));
+    String code = redirectURI.getValue().substring(
+        redirectURI.getValue().indexOf("=") + 1);
+    UUID id = UUID.fromString(code);
+    assertTrue(id != null);
+  }
+
+  /**
+   * Test retrieving an authorization code using a public client that preserves
+   * state
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAuthorizationCodePreserveState() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + PUBLIC_CLIENT_ID
+            + "&response_type=code&state=PRESERVEME&redirect_uri="
+            + URLEncoder.encode(PUBLIC_REDIRECT_URI, "UTF-8"));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_FOUND));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    assertTrue(response == null || response.equals(""));
+    verify();
+    assertTrue(redirectURI.getValue()
+        .startsWith(PUBLIC_REDIRECT_URI));
+    URI uri = new URI(redirectURI.getValue());
+    assertTrue(uri.getQuery().contains("state=PRESERVEME"));
+    assertTrue(uri.getQuery().contains("code="));
+  }
+
+  /**
+   * Test retrieving an authorization code using a confidential client
+   *
+   * Client authentication is not required for confidential clients accessing
+   * the authorization endpoint
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAuthorizationCodeConfidential() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CONF_CLIENT_ID
+            + "&response_type=code&client_secret=" + CONF_CLIENT_SECRET
+            + "redirect_uri=" + URLEncoder.encode(REDIRECT_URI, "UTF-8"));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_FOUND));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    assertTrue(response == null || response.equals(""));
+    verify();
+    assertTrue(redirectURI.getValue().startsWith(REDIRECT_URI + "?code="));
+    String code = redirectURI.getValue().substring(
+        redirectURI.getValue().indexOf("=") + 1);
+    UUID id = UUID.fromString(code);
+    assertTrue(id != null);
+  }
+
+  /**
+   * Test retrieving an authorization code using a confidential client without
+   * setting redirect URI
+   *
+   * The redirect URI is registered with this client, so omitting it should
+   * still generate a response using the registered redirect URI.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAuthorizationCodeNoRedirect() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CONF_CLIENT_ID
+            + "&response_type=code");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    Capture<Integer> respCode = new Capture<Integer>();
+    resp.setStatus(EasyMock.capture(respCode));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    assertTrue(response == null || response.equals(""));
+    verify();
+    assertEquals((Integer) 302, respCode.getValue());
+    assertTrue(redirectURI.getValue().startsWith(REDIRECT_URI + "?code="));
+    String code = redirectURI.getValue().substring(
+        redirectURI.getValue().indexOf("=") + 1);
+    UUID id = UUID.fromString(code);
+    assertTrue(id != null);
+  }
+
+  /**
+   * Test retrieving an authorization code using a confidential client with a
+   * bad redirect URI
+   *
+   * The redirect URI is registered with this client, so passing a redirect that
+   * doesn't match the registered value should generate an error per the OAuth
+   * 2.0 spec.
+   *
+   * See Section 3.1.2.3 under
+   * http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-3.1.2
+   *
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAuthorizationCodeBadRedirect() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CONF_CLIENT_ID
+            + "&response_type=code&redirect_uri="
+            + URLEncoder.encode("http://example.org/redirect/", "UTF-8"));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+    assertEquals("invalid_request", tokenResponse.getString("error"));
+    verify();
+  }
+
+  /**
+   * Test retrieving an auth code and using it to generate an access token
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testConfidentialAuthCodeFlow() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CONF_CLIENT_ID
+            + "&client_secret=" + CONF_CLIENT_SECRET
+            + "&response_type=code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8"));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_FOUND));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    assertTrue(response == null || response.equals(""));
+    verify();
+    assertTrue(redirectURI.getValue().startsWith(REDIRECT_URI + "?code="));
+    String code = redirectURI.getValue().substring(
+        redirectURI.getValue().indexOf("=") + 1);
+    UUID id = UUID.fromString(code);
+    assertTrue(id != null);
+
+    reset();
+
+    req = new FakeHttpServletRequest("http://localhost:8080", "/oauth2",
+        "client_id=" + CONF_CLIENT_ID
+            + "&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code=" + code
+            + "&client_secret=" + CONF_CLIENT_SECRET);
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("bearer", tokenResponse.getString("token_type"));
+    assertNotNull(tokenResponse.getString("access_token"));
+    assertTrue(tokenResponse.getLong("expires_in") > 0);
+    verify();
+
+  }
+
+  /**
+   * Test using URL parameter to pass client secret to authenticate client
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenConfidentialClientParams() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CONF_CLIENT_ID
+            + "&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code="
+            + CONF_AUTH_CODE + "&client_secret=" + CONF_CLIENT_SECRET);
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("bearer", tokenResponse.getString("token_type"));
+    assertNotNull(tokenResponse.getString("access_token"));
+    assertTrue(tokenResponse.getLong("expires_in") > 0);
+    verify();
+  }
+
+  /**
+   * Test using basic authentication scheme for client authentication
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenConfidentialClientBasicAuth() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CONF_CLIENT_ID
+            + "&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code="
+            + CONF_AUTH_CODE);
+    req.setHeader(
+        "Authorization",
+        "Basic "
+            + Base64
+                .encodeBase64String((CONF_CLIENT_ID + ":" + CONF_CLIENT_SECRET)
+                    .getBytes("UTF-8")));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("bearer", tokenResponse.getString("token_type"));
+    assertNotNull(tokenResponse.getString("access_token"));
+    assertTrue(tokenResponse.getLong("expires_in") > 0);
+    verify();
+  }
+
+  /**
+   * Incorrect client ID used in Basic Authorization header
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenConfClientBasicAuthBadID() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CONF_CLIENT_ID
+            + "&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code="
+            + CONF_AUTH_CODE);
+    req.setHeader(
+        "Authorization",
+        "Basic "
+            + Base64.encodeBase64String(("BAD_ID:" + CONF_CLIENT_SECRET)
+                .getBytes("UTF-8")));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    assertTrue(response == null || response.equals(""));
+    verify();
+  }
+
+  /**
+   * Test attempting to get an access token using a bad client secret with a
+   * confidential client.
+   */
+  @Test
+  public void testGetAccessTokenBadConfidentialClientParams() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2");
+    req.setContentType("application/x-www-form-urlencoded");
+    req.setPostData(
+        "client_id=" + CONF_CLIENT_ID
+            + "&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code="
+            + CONF_AUTH_CODE + "&client_secret=BAD_SECRET", "UTF-8");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("unauthorized_client", tokenResponse.getString("error"));
+    verify();
+  }
+
+  /**
+   * Test attempting to get an access token with an unregistered client ID
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenBadClient() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2",
+        "client_id=BAD_CLIENT&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code="
+            + PUBLIC_AUTH_CODE);
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("invalid_client", tokenResponse.getString("error"));
+    verify();
+  }
+
+  /**
+   * Test attempting to get an access token with a bad grant type
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenBadGrantType() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + PUBLIC_CLIENT_ID
+            + "&grant_type=BAD_GRANT&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code="
+            + PUBLIC_AUTH_CODE);
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("unsupported_grant_type", tokenResponse.getString("error"));
+    verify();
+  }
+
+  /**
+   * Test attempting to get an access token with an invalid authorization code
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenBadAuthCode() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + PUBLIC_CLIENT_ID
+            + "&grant_type=authorization_code&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8") + "&code=BAD-CODE-OMG");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("invalid_grant", tokenResponse.getString("error"));
+    verify();
+  }
+
+  /**
+   * Test attempting to re-use an authorization code to get a new access token.
+   */
+  @Test
+  public void testReuseAuthorizationCode() throws Exception {
+    // get authorization code
+    FakeHttpServletRequest req = new FakeHttpServletRequest("http://localhost:8080","/oauth2", "client_id=" + CONF_CLIENT_ID + "&client_secret="+CONF_CLIENT_SECRET+"&response_type=code&redirect_uri="+URLEncoder.encode(REDIRECT_URI,"UTF-8"));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_FOUND));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String response = new String(outputStream.getBuffer(),"UTF-8");
+    assertTrue(response == null || response.equals(""));
+    verify();
+    assertTrue(redirectURI.getValue().startsWith(REDIRECT_URI+"?code="));
+    String code = redirectURI.getValue().substring(redirectURI.getValue().indexOf("=")+1);
+    UUID id = UUID.fromString(code);
+    assertTrue(id != null);
+    System.out.println("Retrieved authorization code: " + code);
+
+    reset();
+
+    // use authorization code to get access token
+    req = new FakeHttpServletRequest("http://localhost:8080","/oauth2", "client_id=" + CONF_CLIENT_ID + "&grant_type=authorization_code&redirect_uri=" + URLEncoder.encode(REDIRECT_URI,"UTF-8") + "&code=" + code + "&client_secret=" + CONF_CLIENT_SECRET);
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    JSONObject tokenResponse = new JSONObject(new String(outputStream.getBuffer(),"UTF-8"));
+    assertEquals("bearer",tokenResponse.getString("token_type"));
+    assertNotNull(tokenResponse.getString("access_token"));
+    assertTrue(tokenResponse.getLong("expires_in") > 0);
+    verify();
+    String accessToken = tokenResponse.getString("access_token");
+    System.out.println("Retrieved access token: " + accessToken);
+
+    reset();
+
+    // ensure access token can get security token for accessing resources
+    OAuth2AuthenticationHandler handler = injector.getInstance(OAuth2AuthenticationHandler.class);
+    req = new FakeHttpServletRequest("http://localhost:8080","/social/rest/activitystreams/john.doe/@self/1/object1", "access_token=" + accessToken);
+    req.setMethod("GET");
+    SecurityToken token = handler.getSecurityTokenFromRequest(req);
+    assertNotNull(token);
+
+    reset();
+
+    // attempt to re-use authorization code to get new access token
+    req = new FakeHttpServletRequest("http://localhost:8080","/oauth2", "client_id=" + CONF_CLIENT_ID + "&grant_type=authorization_code&redirect_uri=" + URLEncoder.encode(REDIRECT_URI,"UTF-8") + "&code=" + code + "&client_secret=" + CONF_CLIENT_SECRET);
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+    outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    tokenResponse = new JSONObject(new String(outputStream.getBuffer(),"UTF-8"));
+    System.out.println("Rejection response: " + tokenResponse.toString());
+    assertEquals("invalid_grant",tokenResponse.getString("error"));
+    verify();
+
+    // use (revoked) access token to get a resource
+    req = new FakeHttpServletRequest("http://localhost:8080","/social/rest/activitystreams/john.doe/@self/1/object1", "access_token=" + accessToken);
+    req.setMethod("GET");
+    try {
+      handler.getSecurityTokenFromRequest(req);
+    } catch (InvalidAuthenticationException ist) {
+      return; // test passed
+    }
+    fail("Should have thrown InvalidAuthenticationException");
+  }
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2AuthenticationHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2AuthenticationHandlerTest.java
new file mode 100644
index 0000000..61c493a
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2AuthenticationHandlerTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import org.apache.shindig.auth.AuthenticationHandler.InvalidAuthenticationException;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.social.SocialApiTestsGuiceModule;
+import org.apache.shindig.social.core.oauth2.OAuth2AuthenticationHandler;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Guice;
+
+public class OAuth2AuthenticationHandlerTest extends EasyMockTestCase {
+
+  private final String ACCESS_TOKEN = "testClient_accesstoken_1";
+  protected OAuth2AuthenticationHandler handler = null;
+
+  @Before
+  public void setUp() {
+    handler = Guice.createInjector(new SocialApiTestsGuiceModule())
+        .getInstance(OAuth2AuthenticationHandler.class);
+  }
+
+  @Test
+  public void testValidAccessTokenViaURL()
+      throws InvalidAuthenticationException {
+    replay();
+    handler.getSecurityTokenFromRequest(new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2", "/some_protected_uri", "access_token="
+            + ACCESS_TOKEN));
+    // Should not throw exception
+  }
+
+  @Test
+  public void testInvalidAccessTokenViaURL() {
+    replay();
+    try {
+      handler.getSecurityTokenFromRequest(new FakeHttpServletRequest(
+          "http://localhost:8080/oauth2", "/some_protected_uri",
+          "access_token=BADTOKEN"));
+    } catch (InvalidAuthenticationException ex) {
+      return;
+    }
+    fail("Handler allowed invalid token without throwing exception");
+    // Should not throw exception
+  }
+
+  @Test
+  public void testValidAccessTokenViaHeader()
+      throws InvalidAuthenticationException {
+    replay();
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2", "/some_protected_uri", "");
+    req.setHeader("Authorization", "Bearer " + ACCESS_TOKEN);
+    handler.getSecurityTokenFromRequest(req);
+    // Should not throw exception
+  }
+
+  @Test
+  public void testInvalidAccessTokenViaHeader() {
+    replay();
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2", "/some_protected_uri", "");
+    req.setHeader("Authorization", "Bearer BADVALUEK");
+    try {
+      handler.getSecurityTokenFromRequest(req);
+    } catch (InvalidAuthenticationException ex) {
+      return;
+    }
+    fail("Handler allowed invalid token without throwing exception");
+    // Should not throw exception
+  }
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2ClientCredentialFlowTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2ClientCredentialFlowTest.java
new file mode 100644
index 0000000..96ee46b
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2ClientCredentialFlowTest.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.social.core.oauth2.OAuth2Servlet;
+import org.apache.shindig.social.dataservice.integration.AbstractLargeRestfulTests;
+import org.easymock.EasyMock;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.PrintWriter;
+
+public class OAuth2ClientCredentialFlowTest extends AbstractLargeRestfulTests {
+
+  protected static final String CLIENT_CRED_CLIENT = "testClientCredentialsClient";
+  protected static final String CLIENT_CRED_SECRET = "clientCredentialsClient_secret";
+
+  protected OAuth2Servlet servlet = null;
+
+  @Before
+  @Override
+  public void abstractLargeRestfulBefore() throws Exception {
+    super.abstractLargeRestfulBefore();
+    servlet = new OAuth2Servlet();
+    injector.injectMembers(servlet);
+  };
+
+  /**
+   * Test using basic authentication scheme for client cred flow
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testClientCredFlowBadHeader() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "grant_type=client_credentials");
+    req.setHeader("Authorization", "Basic *^%#");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_BAD_REQUEST));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    JSONObject respObj = new JSONObject(response);
+    assertTrue(respObj.has("error"));
+    verify();
+  }
+
+  /**
+   * Test using basic authentication scheme for client cred flow
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testClientCredFlowBadPass() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "grant_type=client_credentials");
+    req.setHeader(
+        "Authorization",
+        "Basic "
+            + Base64.encodeBase64String((CLIENT_CRED_CLIENT + ":badsecret")
+                .getBytes("UTF-8")));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_BAD_REQUEST));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    JSONObject respObj = new JSONObject(response);
+    assertTrue(respObj.has("error"));
+    verify();
+  }
+
+  /**
+   * Test using basic authentication scheme for client cred flow
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testClientCredFlowBasicAuth() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "grant_type=client_credentials");
+    req.setHeader(
+        "Authorization",
+        "Basic "
+            + Base64
+                .encodeBase64String((CLIENT_CRED_CLIENT + ":" + CLIENT_CRED_SECRET)
+                    .getBytes("UTF-8")));
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("bearer", tokenResponse.getString("token_type"));
+    assertNotNull(tokenResponse.getString("access_token"));
+    assertTrue(tokenResponse.getLong("expires_in") > 0);
+    verify();
+  }
+
+  /**
+   * Test using URL parameter with client cred flow
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testClientCredFlowParams() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080", "/oauth2", "client_id=" + CLIENT_CRED_CLIENT
+            + "&grant_type=client_credentials&client_secret="
+            + CLIENT_CRED_SECRET);
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/access_token");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(HttpServletResponse.SC_OK);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    JSONObject tokenResponse = new JSONObject(new String(
+        outputStream.getBuffer(), "UTF-8"));
+
+    assertEquals("bearer", tokenResponse.getString("token_type"));
+    assertNotNull(tokenResponse.getString("access_token"));
+    assertTrue(tokenResponse.getLong("expires_in") > 0);
+    verify();
+  }
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2ImplicitFlowTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2ImplicitFlowTest.java
new file mode 100644
index 0000000..800a84b
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuth2ImplicitFlowTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.common.uri.UriBuilder;
+import org.apache.shindig.social.core.oauth2.OAuth2Servlet;
+import org.apache.shindig.social.dataservice.integration.AbstractLargeRestfulTests;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.PrintWriter;
+import java.net.URLEncoder;
+
+public class OAuth2ImplicitFlowTest extends AbstractLargeRestfulTests {
+  protected OAuth2Servlet servlet = null;
+
+  public static final String IMPLICIT_CLIENT_ID = "advancedImplicitClient";
+
+  protected static final String REDIRECT_URI = "http://localhost:8080/oauthclients/ImplicitClientHelper.html";
+
+  @Before
+  @Override
+  public void abstractLargeRestfulBefore() throws Exception {
+    super.abstractLargeRestfulBefore();
+    servlet = new OAuth2Servlet();
+    injector.injectMembers(servlet);
+  };
+
+  /**
+   * Test retrieving an access token using a public client with a redirect uri
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenWithRedirectParamAndState() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2");
+    req.setContentType("application/x-www-form-urlencoded");
+    req.setPostData(
+        "client_id=" + IMPLICIT_CLIENT_ID
+            + "&response_type=token&state=PRESERVEME&redirect_uri="
+            + URLEncoder.encode(REDIRECT_URI, "UTF-8"), "UTF-8");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    resp.setStatus(HttpServletResponse.SC_FOUND);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String fragment = UriBuilder.parse(redirectURI.getValue()).getFragment();
+    assertTrue(redirectURI.getValue().startsWith(REDIRECT_URI));
+    assertTrue(fragment.contains("token_type=bearer"));
+    assertTrue(fragment.contains("access_token="));
+    assertTrue(fragment.contains("expires_in="));
+    assertTrue(fragment.contains("state=PRESERVEME"));
+
+    verify();
+  }
+
+  /**
+   * Test retrieving an access token using a public client with redirect uri
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenNoRedirectParam() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2");
+    req.setContentType("application/x-www-form-urlencoded");
+    req.setPostData("client_id=" + IMPLICIT_CLIENT_ID + "&response_type=token",
+        "UTF-8");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    Capture<String> redirectURI = new Capture<String>();
+    resp.setHeader(EasyMock.eq("Location"), EasyMock.capture(redirectURI));
+    resp.setStatus(HttpServletResponse.SC_FOUND);
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+    String fragment = UriBuilder.parse(redirectURI.getValue()).getFragment();
+    assertTrue(redirectURI.getValue().startsWith(REDIRECT_URI));
+    assertTrue(fragment.contains("token_type=bearer"));
+    assertTrue(fragment.contains("access_token="));
+    assertTrue(fragment.contains("expires_in="));
+    verify();
+  }
+
+  /**
+   * Test attempting to retrieve an access token using a bad redirect URI
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenWithBadRedirect() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2");
+    req.setContentType("application/x-www-form-urlencoded");
+    req.setPostData(
+        "client_id=" + IMPLICIT_CLIENT_ID
+            + "&response_type=token&redirect_uri="
+            + URLEncoder.encode("BAD_REDIRECT", "UTF-8"), "UTF-8");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_FORBIDDEN));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    verify();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    JSONObject respObj = new JSONObject(response);
+    assertTrue(respObj.has("error"));
+  }
+
+  /**
+   * Test attempting to retrieve an access token using a bad client id
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testGetAccessTokenWithBadClientID() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest(
+        "http://localhost:8080/oauth2");
+    req.setContentType("application/x-www-form-urlencoded");
+    req.setPostData("client_id=BAD-ID&response_type=token&redirect_uri="
+        + URLEncoder.encode(REDIRECT_URI, "UTF-8"), "UTF-8");
+    req.setMethod("GET");
+    req.setServletPath("/oauth2");
+    req.setPathInfo("/authorize");
+    HttpServletResponse resp = mock(HttpServletResponse.class);
+    resp.setStatus(EasyMock.eq(HttpServletResponse.SC_FORBIDDEN));
+    MockServletOutputStream outputStream = new MockServletOutputStream();
+    EasyMock.expect(resp.getOutputStream()).andReturn(outputStream).anyTimes();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(resp.getWriter()).andReturn(writer).anyTimes();
+    replay();
+    servlet.service(req, resp);
+    writer.flush();
+
+    verify();
+    String response = new String(outputStream.getBuffer(), "UTF-8");
+    JSONObject respObj = new JSONObject(response);
+    assertTrue(respObj.has("error"));
+  }
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuthAuthenticationHanderTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuthAuthenticationHanderTest.java
new file mode 100644
index 0000000..bb977e9
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/core/oauth/OAuthAuthenticationHanderTest.java
@@ -0,0 +1,425 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.core.oauth;
+
+import net.oauth.OAuth;
+import net.oauth.OAuthConsumer;
+import net.oauth.OAuthServiceProvider;
+import net.oauth.OAuthProblemException;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.AuthenticationHandler;
+import org.apache.shindig.auth.OAuthConstants;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.apache.shindig.social.opensocial.oauth.OAuthDataStore;
+import org.apache.shindig.social.opensocial.oauth.OAuthEntry;
+
+import net.oauth.OAuthValidator;
+import net.oauth.SimpleOAuthValidator;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Date;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Verify behavior of OAuth handler for consumer and 3-legged requests.
+ */
+public class OAuthAuthenticationHanderTest extends EasyMockTestCase {
+
+  OAuthDataStore mockStore = mock(OAuthDataStore.class);
+  OAuthValidator validator = new SimpleOAuthValidator();
+
+  OAuthAuthenticationHandler reqHandler;
+
+  private FakeOAuthRequest formEncodedPost;
+  private FakeOAuthRequest nonFormEncodedPost;
+
+  private static final String TEST_URL = "http://www.example.org/a/b?x=y";
+  private static final String TOKEN = "atoken";
+  private static final String APP_ID = "app:12345";
+  private static final String DOMAIN = "example.org";
+  private static final String CONTAINER = "sandbox";
+
+  @Before
+  public void setUp() throws Exception {
+    reqHandler = new OAuthAuthenticationHandler(mockStore, validator);
+    formEncodedPost = new FakeOAuthRequest("POST", TEST_URL, "a=b&c=d",
+        OAuth.FORM_ENCODED);
+    nonFormEncodedPost = new FakeOAuthRequest("POST", TEST_URL, "BODY",
+        "text/plain");
+  }
+
+  private void expectTokenEntry() {
+    expectTokenEntry(createOAuthEntry());
+  }
+
+  private void expectTokenEntry(OAuthEntry authEntry) {
+    EasyMock.expect(mockStore.getEntry(EasyMock.eq(TOKEN)))
+        .andReturn(authEntry).anyTimes();
+  }
+
+  private OAuthEntry createOAuthEntry() {
+    OAuthEntry authEntry = new OAuthEntry();
+    authEntry.setAppId(APP_ID);
+    authEntry.setAuthorized(true);
+    authEntry.setConsumerKey(FakeOAuthRequest.CONSUMER_KEY);
+    authEntry.setToken(TOKEN);
+    authEntry.setTokenSecret(FakeOAuthRequest.CONSUMER_SECRET);
+    authEntry.setType(OAuthEntry.Type.ACCESS);
+    authEntry.setUserId(FakeOAuthRequest.REQUESTOR);
+    authEntry.setIssueTime(new Date());
+    authEntry.setDomain(DOMAIN);
+    authEntry.setContainer(CONTAINER);
+    return authEntry;
+  }
+
+  private void expectConsumer() {
+    try {
+      EasyMock
+          .expect(
+              mockStore.getConsumer(EasyMock.eq(FakeOAuthRequest.CONSUMER_KEY)))
+          .andReturn(
+              new OAuthConsumer(null, FakeOAuthRequest.CONSUMER_KEY,
+                  FakeOAuthRequest.CONSUMER_SECRET, new OAuthServiceProvider(
+                      null, null, null))).anyTimes();
+    } catch (OAuthProblemException e) {
+      // ignore
+    }
+  }
+
+  private void expectSecurityToken() {
+    try {
+      EasyMock.expect(
+          mockStore.getSecurityTokenForConsumerRequest(
+              EasyMock.eq(FakeOAuthRequest.CONSUMER_KEY),
+              EasyMock.eq(FakeOAuthRequest.REQUESTOR))).andReturn(
+          new AnonymousSecurityToken());
+    } catch (OAuthProblemException e) {
+      // ignore
+    }
+  }
+
+  @Test
+  public void testVerifyOAuthRequest() throws Exception {
+    expectTokenEntry();
+    expectConsumer();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    SecurityToken token = reqHandler.getSecurityTokenFromRequest(request);
+    assertEquals(FakeOAuthRequest.REQUESTOR, token.getViewerId());
+    assertEquals(APP_ID, token.getAppId());
+    assertEquals(DOMAIN, token.getDomain());
+    assertEquals(CONTAINER, token.getContainer());
+    assertNotNull(token);
+    assertTrue(token instanceof OAuthSecurityToken);
+    verify();
+  }
+
+  @Test
+  public void testVerifyGet() throws Exception {
+    expectTokenEntry();
+    expectConsumer();
+    replay();
+    FakeOAuthRequest get = new FakeOAuthRequest("GET", TEST_URL, null, null);
+    FakeHttpServletRequest request = get.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    assertNotNull(reqHandler.getSecurityTokenFromRequest(request));
+  }
+
+  @Test
+  public void testVerifyGetSignatureInHeader() throws Exception {
+    expectTokenEntry();
+    expectConsumer();
+    replay();
+    FakeOAuthRequest get = new FakeOAuthRequest("GET", TEST_URL, null, null);
+    FakeHttpServletRequest request = get.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.AUTH_HEADER,
+        FakeOAuthRequest.BodySigning.NONE);
+    assertNotNull(reqHandler.getSecurityTokenFromRequest(request));
+  }
+
+  @Test
+  public void testVerifyRequestSignatureInBody() throws Exception {
+    expectTokenEntry();
+    expectConsumer();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.POST_BODY,
+        FakeOAuthRequest.BodySigning.NONE);
+    SecurityToken token = reqHandler.getSecurityTokenFromRequest(request);
+    assertNotNull(token);
+    verify();
+  }
+
+  @Test
+  public void testVerifyFailNoTokenEntry() throws Exception {
+    expectTokenEntry(null);
+    expectConsumer();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    try {
+      reqHandler.getSecurityTokenFromRequest(request);
+      fail("Expect failure as no token entry in store");
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      // Pass
+    }
+    verify();
+  }
+
+  @Test
+  public void testVerifyFailTokenSecretMismatch() throws Exception {
+    OAuthEntry authEntry = createOAuthEntry();
+    authEntry.setTokenSecret("badsecret");
+    expectTokenEntry(authEntry);
+    expectConsumer();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    try {
+      reqHandler.getSecurityTokenFromRequest(request);
+      fail("Expect failure as token secrets mismatch");
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      // Pass
+    }
+    verify();
+  }
+
+  @Test
+  public void testVerifyFailTokenIsRequest() throws Exception {
+    OAuthEntry authEntry = createOAuthEntry();
+    authEntry.setType(OAuthEntry.Type.REQUEST);
+    expectTokenEntry(authEntry);
+    expectConsumer();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    try {
+      reqHandler.getSecurityTokenFromRequest(request);
+      fail("Expect failure as token is a request token not an access token");
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      // Pass
+    }
+    verify();
+  }
+
+  @Test
+  public void testVerifyFailTokenIsExpired() throws Exception {
+    OAuthEntry authEntry = createOAuthEntry();
+    authEntry.setIssueTime(new Date(System.currentTimeMillis()
+        - (OAuthEntry.ONE_YEAR + 1)));
+    authEntry.setType(OAuthEntry.Type.REQUEST);
+    expectTokenEntry(authEntry);
+    expectConsumer();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(TOKEN,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    try {
+      reqHandler.getSecurityTokenFromRequest(request);
+      fail("Expect failure as token is expired");
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      // Pass
+    }
+    verify();
+  }
+
+  @Test
+  public void testVerifyConsumerRequest() throws Exception {
+    expectConsumer();
+    expectSecurityToken();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    SecurityToken token = reqHandler.getSecurityTokenFromRequest(request);
+    assertNotNull(token);
+    assertFalse(token instanceof OAuthSecurityToken);
+    verify();
+  }
+
+  @Test
+  public void testVerifyConsumerGet() throws Exception {
+    expectConsumer();
+    expectSecurityToken();
+    replay();
+    FakeOAuthRequest get = new FakeOAuthRequest("GET", TEST_URL, null, null);
+    FakeHttpServletRequest request = get.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    assertNotNull(reqHandler.getSecurityTokenFromRequest(request));
+  }
+
+  @Test
+  public void testVerifyConsumerGetSignatureInHeader() throws Exception {
+    expectConsumer();
+    expectSecurityToken();
+    replay();
+    FakeOAuthRequest get = new FakeOAuthRequest("GET", TEST_URL, null, null);
+    FakeHttpServletRequest request = get.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.AUTH_HEADER,
+        FakeOAuthRequest.BodySigning.NONE);
+    assertNotNull(reqHandler.getSecurityTokenFromRequest(request));
+  }
+
+  @Test
+  public void testVerifyConsumerRequestSignatureInAuthHeader() throws Exception {
+    expectConsumer();
+    expectSecurityToken();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.AUTH_HEADER,
+        FakeOAuthRequest.BodySigning.NONE);
+    reqHandler.getSecurityTokenFromRequest(request);
+    verify();
+  }
+
+  @Test
+  public void testVerifyConsumerRequestSignatureInBody() throws Exception {
+    expectConsumer();
+    expectSecurityToken();
+    replay();
+    HttpServletRequest request = formEncodedPost.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.POST_BODY,
+        FakeOAuthRequest.BodySigning.NONE);
+    reqHandler.getSecurityTokenFromRequest(request);
+    verify();
+  }
+
+  @Test
+  public void testNoSignature() throws Exception {
+    replay();
+    FakeHttpServletRequest request = formEncodedPost.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    // A request without a signature is not an OAuth request
+    request.setParameter(OAuth.OAUTH_SIGNATURE, "");
+    SecurityToken st = reqHandler.getSecurityTokenFromRequest(request);
+    assertNull(st);
+  }
+
+  @Test
+  public void testBodyHashSigning() throws Exception {
+    expectConsumer();
+    expectSecurityToken();
+    replay();
+
+    FakeHttpServletRequest request = nonFormEncodedPost.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.HASH);
+    assertNotNull(reqHandler.getSecurityTokenFromRequest(request));
+  }
+
+  @Test
+  public void testConsumerFailBodyHashSigningWithFormEncoding()
+      throws Exception {
+    replay();
+    FakeOAuthRequest bodyHashPost = new FakeOAuthRequest("POST", TEST_URL,
+        "a=b&c=d&oauth_body_hash=hash", OAuth.FORM_ENCODED);
+    FakeHttpServletRequest request = bodyHashPost.sign(null,
+        FakeOAuthRequest.OAuthParamLocation.URI_QUERY,
+        FakeOAuthRequest.BodySigning.NONE);
+    try {
+      reqHandler.getSecurityTokenFromRequest(request);
+      fail("Cant have body signing with form-encoded post bodies");
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      // Pass
+    }
+  }
+
+  @Test
+  public void testStashBody() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    String body = "BODY";
+    req.setPostData(CharsetUtil.getUtf8Bytes(body));
+    byte[] bytes = OAuthAuthenticationHandler.readBody(req);
+    assertTrue(Arrays.equals(bytes, CharsetUtil.getUtf8Bytes(body)));
+    assertEquals(req.getAttribute(AuthenticationHandler.STASHED_BODY), bytes);
+  }
+
+  @Test
+  public void testBodySigning() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.setContentType("text/plain");
+    String body = "BODY";
+    req.setPostData(CharsetUtil.getUtf8Bytes(body));
+    String hash = new String(Base64.encodeBase64(DigestUtils.sha(CharsetUtil
+        .getUtf8Bytes(body))), "UTF-8");
+    req.setParameter(OAuthConstants.OAUTH_BODY_HASH, hash);
+    OAuthAuthenticationHandler.verifyBodyHash(req, hash);
+  }
+
+  @Test
+  public void testFailBodySigning() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.setContentType("text/plain");
+    String body = "BODY";
+    req.setPostData(CharsetUtil.getUtf8Bytes(body));
+    String hash = new String(Base64.encodeBase64(DigestUtils.sha(CharsetUtil
+        .getUtf8Bytes("NOTBODY"))), "UTF-8");
+    req.setParameter(OAuthConstants.OAUTH_BODY_HASH, hash);
+    try {
+      OAuthAuthenticationHandler.verifyBodyHash(req, hash);
+      fail("Body verification should fail");
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      // Pass
+    }
+  }
+
+  @Test
+  public void testFailBodySigningWithFormEncoded() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.setContentType(OAuth.FORM_ENCODED);
+    String body = "BODY";
+    req.setPostData(CharsetUtil.getUtf8Bytes(body));
+    String hash = new String(Base64.encodeBase64(DigestUtils.sha(CharsetUtil
+        .getUtf8Bytes(body))), "UTF-8");
+    req.setParameter(OAuthConstants.OAUTH_BODY_HASH, hash);
+    try {
+      OAuthAuthenticationHandler.verifyBodyHash(req, hash);
+      fail("Body verification should fail");
+    } catch (AuthenticationHandler.InvalidAuthenticationException iae) {
+      // Pass
+    }
+  }
+
+  @Test
+  public void testBodyHashNoContentType() throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.setPostData(CharsetUtil.getUtf8Bytes(""));
+    String hash = new String(Base64.encodeBase64(DigestUtils.sha(CharsetUtil
+        .getUtf8Bytes(""))), "UTF-8");
+    OAuthAuthenticationHandler.verifyBodyHash(req, hash);
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/AbstractLargeRestfulTests.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/AbstractLargeRestfulTests.java
new file mode 100644
index 0000000..1f99a05
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/AbstractLargeRestfulTests.java
@@ -0,0 +1,299 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.auth.AuthInfoUtil;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.common.testing.FakeHttpServletRequest;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.protocol.DataServiceServlet;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.conversion.BeanXStreamConverter;
+import org.apache.shindig.social.SocialApiTestsGuiceModule;
+import org.apache.shindig.social.core.util.BeanXStreamAtomConverter;
+import org.apache.shindig.social.core.util.xstream.XStream081Configuration;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+import org.custommonkey.xmlunit.NamespaceContext;
+import org.custommonkey.xmlunit.SimpleNamespaceContext;
+import org.custommonkey.xmlunit.XMLUnit;
+import org.custommonkey.xmlunit.XpathEngine;
+import org.easymock.EasyMock;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class AbstractLargeRestfulTests extends EasyMockTestCase {
+  protected static final String XMLSCHEMA = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \n"
+    + " xsi:schemaLocation=\"http://ns.opensocial.org/2008/opensocial classpath:opensocial.xsd\" ";
+  protected static final String XSDRESOURCE = "opensocial.xsd";
+  protected XpathEngine xp;
+  private HttpServletResponse res;
+  protected Injector injector = null;
+
+  private DataServiceServlet servlet;
+
+  private static final FakeGadgetToken FAKE_GADGET_TOKEN = new FakeGadgetToken()
+      .setOwnerId("john.doe").setViewerId("john.doe");
+
+  protected HttpServletResponse getResponse() {
+    return res;
+  }
+
+  protected void setResponse(HttpServletResponse res) {
+    this.res = res;
+  }
+
+  protected DataServiceServlet getServlet() {
+    return servlet;
+  }
+  protected void setServlet(DataServiceServlet servlet) {
+    this.servlet = servlet;
+  }
+
+  @Before
+  public void abstractLargeRestfulBefore() throws Exception {
+    injector = Guice.createInjector(new SocialApiTestsGuiceModule());
+
+    servlet = new DataServiceServlet();
+
+    HandlerRegistry dispatcher = injector.getInstance(HandlerRegistry.class);
+    dispatcher.addHandlers(injector.getInstance(Key.get(new TypeLiteral<Set<Object>>(){},
+        Names.named("org.apache.shindig.handlers"))));
+    servlet.setHandlerRegistry(dispatcher);
+    ContainerConfig containerConfig = EasyMock.createMock(ContainerConfig.class);
+    EasyMock.expect(containerConfig.<String>getList(null, "gadgets.parentOrigins")).andReturn(Collections.<String>singletonList("*")).anyTimes();
+    EasyMock.replay(containerConfig);
+    servlet.setContainerConfig(containerConfig);
+    servlet.setJSONPAllowed(true);
+    servlet.setBeanConverters(new BeanJsonConverter(injector),
+        new BeanXStreamConverter(new XStream081Configuration(injector)),
+        new BeanXStreamAtomConverter(new XStream081Configuration(injector)));
+
+    res = EasyMock.createMock(HttpServletResponse.class);
+    NamespaceContext ns = new SimpleNamespaceContext(ImmutableMap.of("", "http://ns.opensocial.org/2008/opensocial"));
+    XMLUnit.setXpathNamespaceContext(ns);
+    xp = XMLUnit.newXpathEngine();
+  }
+
+  protected String getResponse(String path, String method, String format,
+      String contentType) throws Exception {
+    return getResponse(path, method, Maps.<String, String> newHashMap(), "",
+        format, contentType);
+  }
+
+  protected String getResponse(String path, String method,
+      Map<String, String> extraParams, String format, String contentType)
+      throws Exception {
+    return getResponse(path, method, extraParams, "", format, contentType);
+  }
+
+  protected String getResponse(String path, String method, String postData,
+      String format, String contentType) throws Exception {
+    return getResponse(path, method, Maps.<String,String> newHashMap(),
+        postData, format, contentType);
+  }
+
+  protected String getResponse(String path, String method,
+      Map<String, String> extraParams, String postData, String format,
+      String contentType) throws Exception {
+    FakeHttpServletRequest req = new FakeHttpServletRequest();
+    req.setCharacterEncoding("UTF-8");
+    req.setPathInfo(path);
+    req.setParameter("format",format);
+    req.setParameter("X-HTTP-Method-Override", method);
+    req.setAttribute(AuthInfoUtil.Attribute.SECURITY_TOKEN.getId(), FAKE_GADGET_TOKEN);
+    req.setContentType(contentType);
+    for (Map.Entry<String,String> entry : extraParams.entrySet()) {
+      req.setParameter(entry.getKey(), entry.getValue());
+    }
+
+    if (!("GET").equals(method) && !("HEAD").equals(method)) {
+      if (postData == null) {
+        postData = "";
+      }
+      req.setPostData(postData.getBytes());
+    }
+    req.setMethod(method);
+
+    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+    PrintWriter writer = new PrintWriter(outputStream);
+    EasyMock.expect(res.getWriter()).andReturn(writer);
+    res.setCharacterEncoding("UTF-8");
+    res.setContentType(contentType);
+    res.addHeader("Access-Control-Allow-Origin", "*");
+
+    EasyMock.replay(res);
+    servlet.service(req, res);
+    EasyMock.verify(res);
+    EasyMock.reset(res);
+
+    writer.flush();
+    return outputStream.toString();
+  }
+
+  protected JSONObject getJson(String json) throws Exception {
+    return new JSONObject(json);
+  }
+
+  /**
+   * parse entry.content xml into a Map<> struct
+   *
+   * @param str
+   *          input content string
+   * @return the map<> of <name, value> pairs from the content xml
+   * @throws javax.xml.stream.XMLStreamException
+   *           If the str is not valid xml
+   */
+  protected Map<String, String> parseXmlContent(String str)
+      throws XMLStreamException {
+    ByteArrayInputStream inStr = new ByteArrayInputStream(str.getBytes());
+    XMLInputFactory factory = XMLInputFactory.newInstance();
+    XMLStreamReader parser = factory.createXMLStreamReader(inStr);
+    Map<String, String> columns = Maps.newHashMap();
+
+    while (true) {
+      int event = parser.next();
+      if (event == XMLStreamConstants.END_DOCUMENT) {
+        parser.close();
+        break;
+      } else if (event == XMLStreamConstants.START_ELEMENT) {
+        String name = parser.getLocalName();
+        int eventType = parser.next();
+        if (eventType == XMLStreamConstants.CHARACTERS) {
+          String value = parser.getText();
+          columns.put(name, value);
+        }
+      }
+    }
+    return columns;
+  }
+
+  /**
+   * Converts a node which child nodes into a map keyed on element names
+   * containing the text inside each child node.
+   *
+   * @param n
+   *          the node to convert.
+   * @return a map keyed on element name, containing the contents of each
+   *         element.
+   */
+  protected Map<String, List<String>> childNodesToMap(Node n) {
+    Map<String, List<String>> v = Maps.newHashMap();
+    NodeList result = n.getChildNodes();
+    for (int i = 0; i < result.getLength(); i++) {
+      Node nv = result.item(i);
+      if (nv.getNodeType() == Node.ELEMENT_NODE) {
+        List<String> l = v.get(nv.getLocalName());
+        if (l == null) {
+          l = Lists.newArrayList();
+          v.put(nv.getLocalName(), l);
+        }
+        l.add(nv.getTextContent());
+      }
+    }
+    return v;
+  }
+
+  /**
+   * Converts <entry> <key>k</key> <value> <entry> <key>count</key>
+   * <value>val</value> </entry> <entry> <key>lastUpdate</key>
+   * <value>val</value> </entry> </value> </entry>
+   *
+   * To map.get("k").get("count")
+   *
+   * @param result
+   * @return
+   */
+  protected Map<String, Map<String, List<String>>> childNodesToMapofMap(
+      NodeList result) {
+    Map<String, Map<String, List<String>>> v = Maps.newHashMap();
+    for (int i = 0; i < result.getLength(); i++) {
+      Map<String, List<Node>> keyValue = childNodesToNodeMap(result.item(i));
+
+      assertEquals(2, keyValue.size());
+      assertTrue(keyValue.containsKey("key"));
+      assertTrue(keyValue.containsKey("value"));
+      Node valueNode = keyValue.get("value").get(0);
+      Node key = keyValue.get("key").get(0);
+      NodeList entryList = valueNode.getChildNodes();
+      Map<String, List<String>> pv = Maps.newHashMap();
+      v.put(key.getTextContent(), pv);
+      for (int j = 0; j < entryList.getLength(); j++) {
+        Node n = entryList.item(j);
+        if ("entry".equals(n.getNodeName())) {
+          Map<String, List<String>> ve = childNodesToMap(entryList.item(j));
+          assertTrue(ve.containsKey("key"));
+          List<String> l = pv.get(ve.get("key").get(0));
+          if ( l == null ) {
+            l = Lists.newArrayList();
+            pv.put(ve.get("key").get(0), l);
+          }
+          l.add(ve.get("value").get(0));
+        }
+      }
+    }
+    return v;
+  }
+
+  /**
+   * @param n
+   * @return
+   */
+  protected Map<String, List<Node>> childNodesToNodeMap(Node n) {
+    Map<String, List<Node>> v = Maps.newHashMap();
+    NodeList result = n.getChildNodes();
+    for (int i = 0; i < result.getLength(); i++) {
+      Node nv = result.item(i);
+      if (nv.getNodeType() == Node.ELEMENT_NODE) {
+        List<Node> l = v.get(nv.getLocalName());
+        if (l == null) {
+          l = Lists.newArrayList();
+          v.put(nv.getLocalName(), l);
+        }
+        l.add(nv);
+      }
+    }
+    return v;
+  }
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulAtomActivityEntryTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulAtomActivityEntryTest.java
new file mode 100644
index 0000000..e7e4a9e
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulAtomActivityEntryTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.protocol.ContentTypes;
+import org.junit.Test;
+
+/**
+ * Tests the ATOM serialization of ActivityStreams.
+ */
+public class RestfulAtomActivityEntryTest extends AbstractLargeRestfulTests{
+
+  private static final String FIXTURE_LOC = "src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/";
+
+  @Test
+  public void testGetActivityEntryAtomById() throws Exception {
+    String resp = getResponse("/activitystreams/john.doe/@self/1/activity2", "GET", "atom", ContentTypes.OUTPUT_ATOM_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryAtomId.xml");
+    assertTrue(TestUtils.xmlsEqual(expected, resp));
+  }
+
+  @Test
+  public void testGetActivityEntryAtomByIds() throws Exception {
+    String resp = getResponse("/activitystreams/john.doe/@self/1/activity1,activity2", "GET", "atom", ContentTypes.OUTPUT_ATOM_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryAtomIds.xml");
+    assertTrue(TestUtils.xmlsEqual(expected, resp));
+  }
+
+  @Test
+  public void testCreateActivityEntryAtom() throws Exception {
+    // TODO: Creating activity from ATOM not fully supported
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonActivityEntryTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonActivityEntryTest.java
new file mode 100644
index 0000000..86f1690
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonActivityEntryTest.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.protocol.ContentTypes;
+import org.junit.Test;
+
+/**
+ * Tests the ActivityStreams REST API by comparing test fixtures to actual
+ * server responses.
+ *
+ * TODO: naming consistency with activity & activityEntry
+ * TODO: test server errors with invalid requests e.g. "400 Activity not found: objectXYZ"
+ */
+public class RestfulJsonActivityEntryTest extends AbstractLargeRestfulTests{
+
+  private static final String FIXTURE_LOC = "src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/";
+
+  @Test
+  public void testGetActivityEntryJsonById() throws Exception {
+    String resp = getResponse("/activitystreams/john.doe/@self/1/activity1", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryJsonId.json");
+    assertTrue(TestUtils.jsonsEqual(expected, resp));
+  }
+
+  @Test
+  public void testGetActivityEntryJsonByIds() throws Exception {
+    String resp = getResponse("/activitystreams/john.doe/@self/1/activity1,activity2", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryJsonIds.json");
+    assertTrue(TestUtils.jsonsEqual(expected, resp));
+  }
+
+  @Test
+  public void testGetActivityEntryJsonByGroup() throws Exception {
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryJsonGroup.json");
+
+    // Test @self
+    String resp = getResponse("/activitystreams/john.doe/@self/1", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, resp));
+
+    // Test @friends with multiple people
+    resp = getResponse("/activitystreams/jane.doe,canonical/@friends", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, resp));  // same two activities returned
+  }
+
+  @Test
+  public void testDeleteActivityEntryJson() throws Exception {
+    // First delete activity1, then retrieve & test
+    getResponse("/activitystreams/john.doe/@self/1/activity1", "DELETE", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    String resp = getResponse("/activitystreams/john.doe/@self/1", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryJsonDelete.json");
+    assertTrue(TestUtils.jsonsEqual(expected, resp));
+  }
+
+  @Test
+  public void testUpdateActivityEntryJson() throws Exception {
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryJsonUpdated.json");
+
+    // Update activity
+    String postData = "{id: 'activity2', title : 'Super Updated Activity', actor: {id: 'john.doe'}, object : {id: 'object2'}}";
+    String putResp = getResponse("/activitystreams/john.doe/@self/1/activity2", "PUT", postData, null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, putResp));
+
+    // Retrieve updated activity & test
+    String getResp = getResponse("/activitystreams/john.doe/@self/1/activity2", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, getResp));
+  }
+
+  @Test
+  public void testCreateActivityEntryJson() throws Exception {
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryJsonCreated.json");
+
+    // Create activity
+    String postData = "{id: 'activityCreated', title : 'Super Created Activity', actor: {id: 'john.doe'}, object : {id: 'objectCreated'}}";
+    String postResp = getResponse("/activitystreams/john.doe/@self/1", "POST", postData, null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, postResp));
+
+    // Retrieve created activity & test
+    String getResp = getResponse("/activitystreams/john.doe/@self/1/activityCreated", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, getResp));
+  }
+
+  @Test
+  public void testActivityEntryExtensionJson() throws Exception {
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryJsonExtension.json");
+
+    // Create activity with extensions
+    String postData = "{extension1: 'extension1Value', id: 'activityCreated', title : 'Super Created Activity', actor: {id: 'john.doe', extension2: 'extension2Value'}, object : {extension3: [{ext1: 'ext1Value'}], id: 'objectCreated'}}";
+    String postResp = getResponse("/activitystreams/john.doe/@self/1", "POST", postData, null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, postResp));
+
+    // Retrieve created activity & test
+    String getResp = getResponse("/activitystreams/john.doe/@self/1/activityCreated", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    assertTrue(TestUtils.jsonsEqual(expected, getResp));
+  }
+
+  @Test
+  public void testGetActivityEntrySupportedFields() throws Exception {
+    String resp = getResponse("/activitystreams/@supportedFields", "GET", null, ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityStreamsSupportedFields.json");
+    assertTrue(TestUtils.jsonsEqual(expected, resp));
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonActivityTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonActivityTest.java
new file mode 100644
index 0000000..12efa8d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonActivityTest.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.protocol.ContentTypes;
+import org.apache.shindig.social.core.model.ActivityImpl;
+import org.apache.shindig.social.opensocial.model.Activity;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RestfulJsonActivityTest extends AbstractLargeRestfulTests {
+  Activity johnsActivity;
+
+  @Before
+  public void restfulJsonActivityTestBefore() throws Exception {
+    johnsActivity = new ActivityImpl("1", "john.doe");
+    johnsActivity.setTitle("yellow");
+    johnsActivity.setBody("what a color!");
+  }
+
+  /**
+   * Expected response for an activity in json:
+   * { 'entry' : {
+   *     'id' : '1',
+   *     'userId' : 'john.doe',
+   *     'title' : 'yellow',
+   *     'body' : 'what a color!'
+   *   }
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetActivityJson() throws Exception {
+    String resp = getResponse("/activities/john.doe/@self/@app/1", "GET", null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject result = getJson(resp);
+    assertActivitiesEqual(johnsActivity, result.getJSONObject("entry"));
+  }
+
+  /**
+   * Expected response for a list of activities in json:
+   *
+   * {
+   *  "totalResults" : 1,
+   *  "startIndex" : 0
+   *  "itemsPerPage" : 10 // Note: the js doesn't support paging. Should rest?
+   *  "entry" : [
+   *     {<activity>} // layed out like above
+   *  ]
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetActivitiesJson() throws Exception {
+    String resp = getResponse("/activities/john.doe/@self", "GET", null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject result = getJson(resp);
+
+    assertEquals(1, result.getInt("totalResults"));
+    assertEquals(0, result.getInt("startIndex"));
+    assertActivitiesEqual(johnsActivity, result.getJSONArray("list").getJSONObject(0));
+  }
+
+  /**
+   * Expected response for a list of activities in json:
+   *
+   * {
+   *  "totalResults" : 3,
+   *  "startIndex" : 0
+   *  "itemsPerPage" : 10 // Note: the js doesn't support paging. Should rest?
+   *  "entry" : [
+   *     {<activity>} // layed out like above, except for jane.doe
+   *  ]
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetFriendsActivitiesJson() throws Exception {
+    String resp = getResponse("/activities/john.doe/@friends", "GET", null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject result = getJson(resp);
+
+    assertEquals(2, result.getInt("totalResults"));
+    assertEquals(0, result.getInt("startIndex"));
+  }
+
+  private void assertActivitiesEqual(Activity activity, JSONObject result)
+      throws JSONException {
+    assertEquals(activity.getId(), result.getString("id"));
+    assertEquals(activity.getUserId(), result.getString("userId"));
+    assertEquals(activity.getTitle(), result.getString("title"));
+    assertEquals(activity.getBody(), result.getString("body"));
+  }
+
+  @Test
+  public void testCreateActivity() throws Exception {
+    String postData = "{title : 'hi mom!', body : 'and dad.'}";
+    // Create the activity
+    getResponse("/activities/john.doe/@self", "POST", postData, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    // Verify it can be retrieved
+    String resp = getResponse("/activities/john.doe/@self", "GET", null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject result = getJson(resp);
+
+    assertEquals(2, result.getInt("totalResults"));
+    assertEquals(0, result.getInt("startIndex"));
+
+    JSONArray activities = result.getJSONArray("list");
+    int newActivityIndex = 0;
+    if (activities.getJSONObject(0).has("id")) {
+      newActivityIndex = 1;
+    }
+
+    JSONObject jsonActivity = activities.getJSONObject(newActivityIndex);
+    assertEquals("hi mom!", jsonActivity.getString("title"));
+    assertEquals("and dad.", jsonActivity.getString("body"));
+  }
+
+  // TODO: Add tests for the fields= parameter
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonDataTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonDataTest.java
new file mode 100644
index 0000000..963ea55
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonDataTest.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.protocol.ContentTypes;
+
+import com.google.common.collect.Maps;
+import org.json.JSONObject;
+import org.junit.Test;
+
+import java.util.Map;
+
+
+public class RestfulJsonDataTest extends AbstractLargeRestfulTests {
+
+  /**
+   * Expected response for app data in json:
+   *
+   * {
+   *  "entry" : {
+   *    "jane.doe" : {"count" : "7"},
+   *    "george.doe" : {"count" : "2"},
+   *    "maija.m" : {}, // TODO: Should this entry really be included if she doesn't have any data?
+   *  }
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetFriendsAppDataJson() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "count");
+    String resp = getResponse("/appdata/john.doe/@friends/app", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    JSONObject data = getJson(resp).getJSONObject("entry");
+    assertEquals(3, data.length());
+
+    JSONObject janesEntries = data.getJSONObject("jane.doe");
+    assertEquals(1, janesEntries.length());
+    assertEquals("7", janesEntries.getString("count"));
+
+    JSONObject georgesEntries = data.getJSONObject("george.doe");
+    assertEquals(1, georgesEntries.length());
+    assertEquals("2", georgesEntries.getString("count"));
+  }
+
+  /**
+   * Expected response for app data in json:
+   *
+   * {
+   *  "entry" : {
+   *    "john.doe" : {"count" : "0"},
+   *  }
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetSelfAppDataJson() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", null);
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    JSONObject data = getJson(resp).getJSONObject("entry");
+    assertEquals(1, data.length());
+
+    JSONObject johnsEntries = data.getJSONObject("john.doe");
+    assertEquals(1, johnsEntries.length());
+    assertEquals("0", johnsEntries.getString("count"));
+  }
+
+  /**
+   * Expected response for app data in json:
+   *
+   * {
+   *  "entry" : {
+   *    "john.doe" : {"count" : "0"},
+   *  }
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetSelfAppDataJsonWithKey() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "count");
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    JSONObject data = getJson(resp).getJSONObject("entry");
+    assertEquals(1, data.length());
+
+    JSONObject johnsEntries = data.getJSONObject("john.doe");
+    assertEquals(1, johnsEntries.length());
+    assertEquals("0", johnsEntries.getString("count"));
+  }
+
+  /**
+   * Expected response for app data in json with non-existant key:
+   * TODO: Double check this output with the spec
+   *
+   * {
+   *  "entry" : {
+   *    "john.doe" : {},
+   *  }
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetSelfAppDataJsonWithInvalidKeys() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "peabody");
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    JSONObject data = getJson(resp).getJSONObject("entry");
+    assertEquals(1, data.length());
+
+    JSONObject johnsEntries = data.getJSONObject("john.doe");
+    assertEquals(0, johnsEntries.length());
+  }
+
+  @Test
+  public void testDeleteAppData() throws Exception {
+    assertCount("0");
+
+    // With the wrong field
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "peabody");
+    getResponse("/appdata/john.doe/@self/app", "DELETE", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    assertCount("0");
+
+    extraParams.put("fields", "count");
+    getResponse("/appdata/john.doe/@self/app", "DELETE", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    assertCount(null);
+  }
+
+  @Test
+  public void testUpdateAppData() throws Exception {
+    assertCount("0");
+
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "count");
+    String postData = "{count : 5}";
+    getResponse("/appdata/john.doe/@self/app", "POST", extraParams, postData, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+
+    assertCount("5");
+  }
+
+  private void assertCount(String expectedCount) throws Exception {
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET", null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject data = getJson(resp).getJSONObject("entry");
+    assertEquals(1, data.length());
+
+    JSONObject johnsEntries = data.getJSONObject("john.doe");
+
+    if (expectedCount != null) {
+      assertEquals(1, johnsEntries.length());
+      assertEquals(expectedCount, johnsEntries.getString("count"));
+    } else {
+      assertEquals(0, johnsEntries.length());
+    }
+  }
+
+  // TODO: support for indexBy??
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonPeopleTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonPeopleTest.java
new file mode 100644
index 0000000..ca8d27d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulJsonPeopleTest.java
@@ -0,0 +1,516 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import org.apache.shindig.protocol.ContentTypes;
+import org.apache.shindig.protocol.model.Enum;
+import org.apache.shindig.protocol.model.EnumImpl;
+import org.apache.shindig.social.core.model.AddressImpl;
+import org.apache.shindig.social.core.model.BodyTypeImpl;
+import org.apache.shindig.social.core.model.ListFieldImpl;
+import org.apache.shindig.social.core.model.NameImpl;
+import org.apache.shindig.social.core.model.OrganizationImpl;
+import org.apache.shindig.social.core.model.PersonImpl;
+import org.apache.shindig.social.core.model.UrlImpl;
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.BodyType;
+import org.apache.shindig.social.opensocial.model.Drinker;
+import org.apache.shindig.social.opensocial.model.ListField;
+import org.apache.shindig.social.opensocial.model.LookingFor;
+import org.apache.shindig.social.opensocial.model.Name;
+import org.apache.shindig.social.opensocial.model.NetworkPresence;
+import org.apache.shindig.social.opensocial.model.Organization;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.model.Smoker;
+import org.apache.shindig.social.opensocial.model.Url;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public class RestfulJsonPeopleTest extends AbstractLargeRestfulTests {
+  private Person canonical;
+
+  @Before
+  public void restfulJsonPeopleTestBefore() throws Exception {
+    NameImpl name = new NameImpl("Sir Shin H. Digg Social Butterfly");
+    name.setAdditionalName("H");
+    name.setFamilyName("Digg");
+    name.setGivenName("Shin");
+    name.setHonorificPrefix("Sir");
+    name.setHonorificSuffix("Social Butterfly");
+    canonical = new PersonImpl("canonical", "Shin Digg", name);
+
+    canonical.setAboutMe("I have an example of every piece of data");
+    canonical.setActivities(Lists.newArrayList("Coding Shindig"));
+
+    Address address = new AddressImpl("PoBox 3565, 1 OpenStandards Way, Apache, CA");
+    address.setCountry("US");
+    address.setLatitude(28.3043F);
+    address.setLongitude(143.0859F);
+    address.setLocality("who knows");
+    address.setPostalCode("12345");
+    address.setRegion("Apache, CA");
+    address.setStreetAddress("1 OpenStandards Way");
+    address.setType("home");
+    address.setFormatted("PoBox 3565, 1 OpenStandards Way, Apache, CA");
+    canonical.setAddresses(Lists.newArrayList(address));
+
+    canonical.setAge(33);
+    BodyTypeImpl bodyType = new BodyTypeImpl();
+    bodyType.setBuild("svelte");
+    bodyType.setEyeColor("blue");
+    bodyType.setHairColor("black");
+    bodyType.setHeight(1.84F);
+    bodyType.setWeight(74F);
+    canonical.setBodyType(bodyType);
+
+    canonical.setBooks(Lists.newArrayList("The Cathedral & the Bazaar", "Catch 22"));
+    canonical.setCars(Lists.newArrayList("beetle", "prius"));
+    canonical.setChildren("3");
+    AddressImpl location = new AddressImpl();
+    location.setLatitude(48.858193F);
+    location.setLongitude(2.29419F);
+    canonical.setCurrentLocation(location);
+
+    canonical.setBirthday(new Date());
+    canonical.setDrinker(new EnumImpl<Drinker>(Drinker.SOCIALLY));
+    ListField email = new ListFieldImpl("work", "dev@shindig.apache.org");
+    canonical.setEmails(Lists.newArrayList(email));
+
+    canonical.setEthnicity("developer");
+    canonical.setFashion("t-shirts");
+    canonical.setFood(Lists.newArrayList("sushi", "burgers"));
+    canonical.setGender(Person.Gender.male);
+    canonical.setHappiestWhen("coding");
+    canonical.setHasApp(true);
+    canonical.setHeroes(Lists.newArrayList("Doug Crockford", "Charles Babbage"));
+    canonical.setHumor("none to speak of");
+    canonical.setInterests(Lists.newArrayList("PHP", "Java"));
+    canonical.setJobInterests("will work for beer");
+
+    Organization job1 = new OrganizationImpl();
+    job1.setAddress(new AddressImpl("1 Shindig Drive"));
+    job1.setDescription("lots of coding");
+    job1.setEndDate(new Date());
+    job1.setField("Software Engineering");
+    job1.setName("Apache.com");
+    job1.setSalary("$1000000000");
+    job1.setStartDate(new Date());
+    job1.setSubField("Development");
+    job1.setTitle("Grand PooBah");
+    job1.setWebpage("http://shindig.apache.org/");
+    job1.setType("job");
+
+    Organization job2 = new OrganizationImpl();
+    job2.setAddress(new AddressImpl("1 Skid Row"));
+    job2.setDescription("");
+    job2.setEndDate(new Date());
+    job2.setField("College");
+    job2.setName("School of hard Knocks");
+    job2.setSalary("$100");
+    job2.setStartDate(new Date());
+    job2.setSubField("Lab Tech");
+    job2.setTitle("Gopher");
+    job2.setWebpage("");
+    job2.setType("job");
+
+    canonical.setOrganizations(Lists.newArrayList(job1, job2));
+
+    canonical.setUpdated(new Date());
+    canonical.setLanguagesSpoken(Lists.newArrayList("English", "Dutch", "Esperanto"));
+    canonical.setLivingArrangement("in a house");
+    Enum<LookingFor> lookingForRandom =
+        new EnumImpl<LookingFor>(LookingFor.RANDOM, "Random");
+    Enum<LookingFor> lookingForNetworking =
+        new EnumImpl<LookingFor>(LookingFor.NETWORKING, "Networking");
+    canonical.setLookingFor(Lists.newArrayList(lookingForRandom, lookingForNetworking));
+    canonical.setMovies(Lists.newArrayList("Iron Man", "Nosferatu"));
+    canonical.setMusic(Lists.newArrayList("Chieftains", "Beck"));
+    canonical.setNetworkPresence(new EnumImpl<NetworkPresence>(NetworkPresence.ONLINE));
+    canonical.setNickname("diggy");
+    canonical.setPets("dog,cat");
+    canonical.setPhoneNumbers(Lists.<ListField> newArrayList(new ListFieldImpl("work",
+        "111-111-111"), new ListFieldImpl("mobile", "999-999-999")));
+
+    canonical.setPoliticalViews("open leaning");
+    canonical.setProfileSong(new UrlImpl("http://www.example.org/songs/OnlyTheLonely.mp3",
+        "Feelin' blue", "road"));
+    canonical.setProfileVideo(new UrlImpl("http://www.example.org/videos/Thriller.flv",
+        "Thriller", "video"));
+
+    canonical.setQuotes(Lists.newArrayList("I am therfore I code", "Doh!"));
+    canonical.setRelationshipStatus("married to my job");
+    canonical.setReligion("druidic");
+    canonical.setRomance("twice a year");
+    canonical.setScaredOf("COBOL");
+    canonical.setSexualOrientation("north");
+    canonical.setSmoker(new EnumImpl<Smoker>(Smoker.NO));
+    canonical.setSports(Lists.newArrayList("frisbee", "rugby"));
+    canonical.setStatus("happy");
+    canonical.setTags(Lists.newArrayList("C#", "JSON", "template"));
+    canonical.setThumbnailUrl("/images/nophoto.gif");
+    canonical.setUtcOffset(-8L);
+    canonical.setTurnOffs(Lists.newArrayList("lack of unit tests", "cabbage"));
+    canonical.setTurnOns(Lists.newArrayList("well document code"));
+    canonical.setTvShows(Lists.newArrayList("House", "Battlestar Galactica"));
+
+    canonical.setUrls(Lists.<Url>newArrayList(
+        new UrlImpl("http://www.example.org/?id=1", "my profile", "Profile"),
+        new UrlImpl("/images/nophoto.gif", "my awesome picture", "Thumbnail")));
+  }
+
+  /**
+   * Expected response for john.doe's json:
+   *
+   * { 'entry' : {
+   *     'id' : 'john.doe',
+   *     'name' : {'formatted' : 'John Doe'},
+   *     'phoneNumbers' : [
+   *       { 'number' : '+33H000000000', 'type' : 'home'},
+   *     ],
+   *     'addresses' : [
+   *       {'formatted' : 'My home address'}
+   *     ],
+   *     'emails' : [
+   *       { 'value' : 'john.doe@work.bar', 'type' : 'work'},
+   *     ]
+   *
+   *    ... etc, etc for all fields in the person object
+   *   }
+   * }
+   * TODO: Finish up this test and make refactor so that it is easier to read
+   *
+   * @throws Exception if test encounters an error
+   */
+  @SuppressWarnings("boxing")
+  @Test
+  public void testGetPersonJson() throws Exception {
+    // TODO(doll): Test all of the date fields
+
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", Joiner.on(',').join(Person.Field.ALL_FIELDS));
+
+    // Currently, for Shindig {pid}/@all/{uid} == {uid}/@self
+    String resp = getResponse("/people/canonical/@self", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject result = getJson(resp).getJSONObject("entry");
+
+    assertStringField(result, canonical.getAboutMe(), Person.Field.ABOUT_ME);
+    assertStringListField(result, canonical.getActivities(),
+        Person.Field.ACTIVITIES);
+
+    JSONObject jsonAddress = result.getJSONArray(
+        Person.Field.ADDRESSES.toString()).getJSONObject(0);
+    assertAddressField(canonical.getAddresses().get(0), jsonAddress);
+
+    assertEquals(canonical.getAge().intValue(), result.getInt(
+        Person.Field.AGE.toString()));
+
+    JSONObject jsonBody = result.getJSONObject(
+        Person.Field.BODY_TYPE.toString());
+    BodyType body = canonical.getBodyType();
+    assertStringField(jsonBody, body.getBuild(), BodyType.Field.BUILD);
+    assertStringField(jsonBody, body.getEyeColor(), BodyType.Field.EYE_COLOR);
+    assertStringField(jsonBody, body.getHairColor(), BodyType.Field.HAIR_COLOR);
+    assertFloatField(jsonBody, body.getHeight(), BodyType.Field.HEIGHT);
+    assertFloatField(jsonBody, body.getWeight(), BodyType.Field.WEIGHT);
+
+    assertStringListField(result, canonical.getBooks(), Person.Field.BOOKS);
+    assertStringListField(result, canonical.getCars(), Person.Field.CARS);
+    assertStringField(result, canonical.getChildren(), Person.Field.CHILDREN);
+
+    JSONObject currentLocation = result.getJSONObject(Person.Field.CURRENT_LOCATION.toString());
+    assertFloatField(currentLocation, canonical.getCurrentLocation().getLatitude(),
+        Address.Field.LATITUDE);
+    assertFloatField(currentLocation, canonical.getCurrentLocation().getLongitude(),
+        Address.Field.LONGITUDE);
+
+    assertStringField(result, canonical.getDisplayName(), Person.Field.DISPLAY_NAME);
+
+//    assertLongField(result, canonical.getBirthday().getTime(),
+//        Person.Field.BIRTHDAY);
+//    assertEnumField(result, canonical.getDrinker(), Person.Field.DRINKER);
+
+    JSONArray emailArray = result.getJSONArray(Person.Field.EMAILS.toString());
+    assertEquals(1, emailArray.length());
+
+    for (int i = 0; i < canonical.getEmails().size(); i++) {
+      ListField expectedEmail = canonical.getEmails().get(i);
+      JSONObject actualEmail = emailArray.getJSONObject(i);
+      assertEquals(expectedEmail.getType(),
+          actualEmail.getString(ListField.Field.TYPE.toString()));
+      assertEquals(expectedEmail.getValue(),
+          actualEmail.getString(ListField.Field.VALUE.toString()));
+    }
+
+    assertStringField(result, canonical.getEthnicity(), Person.Field.ETHNICITY);
+    assertStringField(result, canonical.getFashion(), Person.Field.FASHION);
+    assertStringListField(result, canonical.getFood(), Person.Field.FOOD);
+    assertStringField(result, canonical.getGender().toString(), Person.Field.GENDER);
+    assertStringField(result, canonical.getHappiestWhen(),
+        Person.Field.HAPPIEST_WHEN);
+    assertBooleanField(result, canonical.getHasApp(), Person.Field.HAS_APP);
+    assertStringListField(result, canonical.getHeroes(), Person.Field.HEROES);
+    assertStringField(result, canonical.getHumor(), Person.Field.HUMOR);
+    assertStringField(result, canonical.getId(), Person.Field.ID);
+    assertStringListField(result, canonical.getInterests(),
+        Person.Field.INTERESTS);
+    assertStringField(result, canonical.getJobInterests(),
+        Person.Field.JOB_INTERESTS);
+
+    assertOrganizationField(canonical.getOrganizations().get(0),
+        result.getJSONArray(Person.Field.ORGANIZATIONS.toString()).getJSONObject(0));
+
+    assertStringListField(result, canonical.getLanguagesSpoken(),
+        Person.Field.LANGUAGES_SPOKEN);
+//    assertDateField(result, canonical.getUpdated(), Person.Field.LAST_UPDATED);
+    assertStringField(result, canonical.getLivingArrangement(),
+        Person.Field.LIVING_ARRANGEMENT);
+    assertListEnumField(result, canonical.getLookingFor(),
+        Person.Field.LOOKING_FOR);
+    assertStringListField(result, canonical.getMovies(), Person.Field.MOVIES);
+    assertStringListField(result, canonical.getMusic(), Person.Field.MUSIC);
+
+    assertEquals(canonical.getName().getFormatted(),
+        result.getJSONObject(Person.Field.NAME.toString()).getString(
+            Name.Field.FORMATTED.toString()));
+
+    assertEnumField(result, canonical.getNetworkPresence(),
+        Person.Field.NETWORKPRESENCE);
+    assertStringField(result, canonical.getNickname(), Person.Field.NICKNAME);
+    assertStringField(result, canonical.getPets(), Person.Field.PETS);
+
+    JSONArray phoneArray = result.getJSONArray(
+        Person.Field.PHONE_NUMBERS.toString());
+    assertEquals(canonical.getPhoneNumbers().size(), phoneArray.length());
+
+    for (int i = 0; i < canonical.getPhoneNumbers().size(); i++) {
+      ListField expectedPhone = canonical.getPhoneNumbers().get(i);
+      JSONObject actualPhone = phoneArray.getJSONObject(i);
+      assertEquals(expectedPhone.getType(), actualPhone.getString(
+          ListField.Field.TYPE.toString()));
+      assertEquals(expectedPhone.getValue(), actualPhone.getString(
+          ListField.Field.VALUE.toString()));
+    }
+
+    assertStringField(result, canonical.getPoliticalViews(),
+        Person.Field.POLITICAL_VIEWS);
+
+    assertUrlField(canonical.getProfileSong(), result.getJSONObject(
+        Person.Field.PROFILE_SONG.toString()));
+    assertStringField(result, canonical.getProfileUrl(),
+        Person.Field.PROFILE_URL);
+    assertUrlField(canonical.getProfileVideo(), result.getJSONObject(
+        Person.Field.PROFILE_VIDEO.toString()));
+
+    assertStringListField(result, canonical.getQuotes(), Person.Field.QUOTES);
+    assertStringField(result, canonical.getRelationshipStatus(),
+        Person.Field.RELATIONSHIP_STATUS);
+    assertStringField(result, canonical.getReligion(), Person.Field.RELIGION);
+    assertStringField(result, canonical.getRomance(), Person.Field.ROMANCE);
+    assertStringField(result, canonical.getScaredOf(), Person.Field.SCARED_OF);
+
+    assertStringField(result, canonical.getSexualOrientation(), Person.Field.SEXUAL_ORIENTATION);
+    assertEnumField(result, canonical.getSmoker(), Person.Field.SMOKER);
+    assertStringListField(result, canonical.getSports(), Person.Field.SPORTS);
+    assertStringField(result, canonical.getStatus(), Person.Field.STATUS);
+    assertStringListField(result, canonical.getTags(), Person.Field.TAGS);
+    assertStringField(result, canonical.getThumbnailUrl(),
+        Person.Field.THUMBNAIL_URL);
+    // TODO: time zone
+    assertStringListField(result, canonical.getTurnOffs(),
+        Person.Field.TURN_OFFS);
+    assertStringListField(result, canonical.getTurnOns(), Person.Field.TURN_ONS);
+    assertStringListField(result, canonical.getTvShows(), Person.Field.TV_SHOWS);
+  }
+
+  private void assertAddressField(Address expected, JSONObject actual)
+      throws JSONException {
+    assertStringField(actual, expected.getCountry(),
+        Address.Field.COUNTRY);
+    assertFloatField(actual, expected.getLatitude(), Address.Field.LATITUDE);
+    assertStringField(actual, expected.getLocality(), Address.Field.LOCALITY);
+    assertFloatField(actual, expected.getLongitude(), Address.Field.LONGITUDE);
+    assertStringField(actual, expected.getPostalCode(),
+        Address.Field.POSTAL_CODE);
+    assertStringField(actual, expected.getRegion(), Address.Field.REGION);
+    assertStringField(actual, expected.getStreetAddress(),
+        Address.Field.STREET_ADDRESS);
+    assertStringField(actual, expected.getType(), Address.Field.TYPE);
+    assertStringField(actual, expected.getFormatted(),
+        Address.Field.FORMATTED);
+  }
+
+  private void assertUrlField(Url expected, JSONObject actual)
+      throws JSONException {
+    assertStringField(actual, expected.getValue(), Url.Field.VALUE);
+    assertStringField(actual, expected.getLinkText(), Url.Field.LINK_TEXT);
+    assertStringField(actual, expected.getType(), Url.Field.TYPE);
+  }
+
+  private void assertOrganizationField(Organization expected, JSONObject actual)
+      throws JSONException {
+    assertStringField(actual.getJSONObject(Organization.Field.ADDRESS.toString()),
+        expected.getAddress().getFormatted(), Address.Field.FORMATTED);
+    assertStringField(actual, expected.getDescription(),
+        Organization.Field.DESCRIPTION);
+//    assertDateField(actual, expected.getEndDate(), Organization.Field.END_DATE);
+    assertStringField(actual, expected.getField(), Organization.Field.FIELD);
+    assertStringField(actual, expected.getName(), Organization.Field.NAME);
+    assertStringField(actual, expected.getSalary(), Organization.Field.SALARY);
+//    assertDateField(actual, expected.getStartDate(), Organization.Field.START_DATE);
+    assertStringField(actual, expected.getSubField(), Organization.Field.SUB_FIELD);
+    assertStringField(actual, expected.getTitle(), Organization.Field.TITLE);
+    assertStringField(actual, expected.getWebpage(), Organization.Field.WEBPAGE);
+    assertStringField(actual, expected.getType(), Organization.Field.TYPE);
+  }
+
+  private void assertBooleanField(JSONObject result, boolean expected,
+      Object field) throws JSONException {
+    assertEquals(expected, result.getBoolean(field.toString()));
+  }
+
+  private void assertFloatField(JSONObject result, Float expected,
+      Object field) throws JSONException {
+    assertEquals(expected.intValue(), result.getInt(field.toString()));
+  }
+
+  private void assertStringField(JSONObject result, String expected,
+      Object field) throws JSONException {
+    assertEquals(expected, result.getString(field.toString()));
+  }
+
+  private void assertStringListField(JSONObject result, List<String> list,
+      Person.Field field) throws JSONException {
+    JSONArray actual = result.getJSONArray(field.toString());
+    assertEquals(list.get(0), actual.getString(0));
+  }
+
+  @SuppressWarnings("unchecked")
+  private void assertEnumField(JSONObject result, org.apache.shindig.protocol.model.Enum expected,
+      Person.Field field) throws JSONException {
+    JSONObject actual = result.getJSONObject(field.toString());
+    assertEquals(expected.getDisplayValue(), actual.getString("displayValue"));
+    assertEquals(expected.getValue().toString(), actual.getString("value"));
+  }
+
+  private void assertListEnumField(JSONObject result,
+      List<? extends Enum<? extends Enum.EnumKey>> expected,
+      Person.Field field) throws JSONException {
+    JSONArray actual = result.getJSONArray(field.toString());
+    for (int i = 0; i < actual.length(); i++) {
+      assertEquals(expected.get(i).getDisplayValue(),
+          actual.getJSONObject(i).getString("displayValue"));
+      assertEquals(expected.get(i).getValue().toString(),
+          actual.getJSONObject(i).getString("value"));
+    }
+  }
+
+  /**
+   * Expected response for a list of people in json:
+   *
+   * {
+   *  "totalResults" : 3,
+   *  "startIndex" : 0
+   *  "entry" : [
+   *     {<jane doe>}, // layed out like above
+   *     {<george doe>},
+   *     {<maija m>},
+   *  ]
+   * }
+   *
+   * @throws Exception if test encounters an error
+   */
+  @Test
+  public void testGetPeople() throws Exception {
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("sortBy", "name");
+    extraParams.put("sortOrder", null);
+    extraParams.put("filterBy", null);
+    extraParams.put("startIndex", null);
+    extraParams.put("count", "20");
+    extraParams.put("fields", null);
+
+    // Currently, for Shindig @all == @friends
+    String resp = getResponse("/people/john.doe/@friends", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject result = getJson(resp);
+
+    assertEquals(3, result.getInt("totalResults"));
+    assertEquals(0, result.getInt("startIndex"));
+
+    JSONArray people = result.getJSONArray("list");
+
+    // The users should be in alphabetical order
+    assertPerson(people.getJSONObject(0), "george.doe", "George Doe");
+    assertPerson(people.getJSONObject(1), "jane.doe", "Jane Doe");
+  }
+
+  @Test
+  public void testGetPeoplePagination() throws Exception {
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("sortBy", "name");
+    extraParams.put("sortOrder", null);
+    extraParams.put("filterBy", null);
+    extraParams.put("startIndex", "0");
+    extraParams.put("count", "1");
+    extraParams.put("fields", null);
+
+    String resp = getResponse("/people/john.doe/@friends", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    JSONObject result = getJson(resp);
+
+    assertEquals(3, result.getInt("totalResults"));
+    assertEquals(0, result.getInt("startIndex"));
+
+    JSONArray people = result.getJSONArray("list");
+    assertPerson(people.getJSONObject(0), "george.doe", "George Doe");
+
+    // Get the second page
+    extraParams.put("startIndex", "1");
+    resp = getResponse("/people/john.doe/@friends", "GET", extraParams, null,
+        ContentTypes.OUTPUT_JSON_CONTENT_TYPE);
+    result = getJson(resp);
+
+    assertEquals(3, result.getInt("totalResults"));
+    assertEquals(1, result.getInt("startIndex"));
+
+    people = result.getJSONArray("list");
+    assertPerson(people.getJSONObject(0), "jane.doe", "Jane Doe");
+  }
+
+  private void assertPerson(JSONObject person, String expectedId, String expectedName)
+      throws Exception {
+    assertEquals(expectedId, person.getString("id"));
+    assertEquals(expectedName, person.getJSONObject("name").getString("formatted"));
+  }
+
+  // TODO: Add tests for fields parameter
+  // TODO: Add tests for networkDistance
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlActivityEntryTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlActivityEntryTest.java
new file mode 100644
index 0000000..cbeb718
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlActivityEntryTest.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.protocol.ContentTypes;
+import org.junit.Test;
+
+/**
+ * Tests the XML serialization of ActivityStreams.
+ */
+public class RestfulXmlActivityEntryTest extends AbstractLargeRestfulTests{
+
+  private static final String FIXTURE_LOC = "src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/";
+
+  @Test
+  public void testGetActivityEntryXmlById() throws Exception {
+    String resp = getResponse("/activitystreams/john.doe/@self/1/activity2", "GET", "xml", ContentTypes.OUTPUT_XML_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryXmlId.xml");
+    assertTrue(TestUtils.xmlsEqual(expected, resp));
+  }
+
+  @Test
+  public void testGetActivityEntryXmlByIds() throws Exception {
+    String resp = getResponse("/activitystreams/john.doe/@self/1/activity1,activity2", "GET", "xml", ContentTypes.OUTPUT_XML_CONTENT_TYPE);
+    String expected = TestUtils.loadTestFixture(FIXTURE_LOC + "ActivityEntryXmlIds.xml");
+    assertTrue(TestUtils.xmlsEqual(expected, resp));
+  }
+
+  @Test
+  public void testCreateActivityEntryXml() throws Exception {
+    // TODO: Creating activity from XML not fully supported
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlActivityTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlActivityTest.java
new file mode 100644
index 0000000..9b4e578
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlActivityTest.java
@@ -0,0 +1,230 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.social.core.model.ActivityImpl;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.util.XSDValidator;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.custommonkey.xmlunit.XMLUnit;
+
+import java.util.List;
+import java.util.Map;
+
+public class RestfulXmlActivityTest extends AbstractLargeRestfulTests {
+  private Activity johnsActivity;
+
+  @Before
+  public void restfulXmlActivityTestBefore() throws Exception {
+    johnsActivity = new ActivityImpl("1", "john.doe");
+    johnsActivity.setTitle("yellow");
+    johnsActivity.setBody("what a color!");
+  }
+
+  /**
+   * Expected response for an activity in xml:
+   *
+   * <pre>
+   * &lt;response&gt;
+   *    &lt;activity&gt;
+   *       &lt;id&gt;1&lt;/id&gt;
+   *       &lt;userId&gt;john.doe&lt;/userId&gt;
+   *       &lt;title&gt;yellow&lt;/title&gt;
+   *       &lt;body&gt;body&lt;/body&gt;
+   *    &lt;/activity&gt;
+   * &lt;/response&gt;
+   * </pre>
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetActivityJson() throws Exception {
+    String resp = getResponse("/activities/john.doe/@self/@app/1", "GET",
+        "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+
+    NodeList result = xp.getMatchingNodes("/:response/:activity", XMLUnit.buildTestDocument(resp));
+    assertEquals(1, result.getLength());
+    Node n = result.item(0);
+
+    Map<String, List<String>> v = childNodesToMap(n);
+
+    assertEquals(4, v.size());
+    assertActivitiesEqual(johnsActivity, v);
+  }
+
+  /**
+   * Expected response for a list of activities in json:
+   *
+   * <pre>
+   * &lt;response xmlns=&quot;http://ns.opensocial.org/2008/opensocial&quot;
+   *    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
+   *    xsi:schemaLocation=&quot;http://ns.opensocial.org/2008/opensocial file:/Users/ieb/Apache/shindig/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/opensocial.xsd&quot;&gt;
+   *   &lt;activity&gt;
+   *     &lt;itemsPerPage&gt;10&lt;/itemsPerPage&gt;
+   *     &lt;startIndex&gt;0&lt;/startIndex&gt;
+   *     &lt;totalResults&gt;1&lt;/totalResults&gt;
+   *     &lt;entry&gt;
+   *       &lt;appId&gt;&lt;/appId&gt;
+   *       &lt;body&gt;&lt;/body&gt;
+   *       &lt;bodyId&gt;&lt;/bodyId&gt;
+   *       &lt;externalId&gt;&lt;/externalId&gt;
+   *       &lt;id&gt;&lt;/id&gt;
+   *       &lt;mediaItems&gt;
+   *         &lt;mimeType&gt;&lt;/mimeType&gt;
+   *         &lt;type&gt;&lt;/type&gt;
+   *         &lt;url&gt;&lt;/url&gt;
+   *       &lt;/mediaItems&gt;
+   *       &lt;postedTime&gt;&lt;/postedTime&gt;
+   *       &lt;priority&gt;&lt;/priority&gt;
+   *       &lt;streamFaviconUrl&gt;&lt;/streamFaviconUrl&gt;
+   *       &lt;streamSourceUrl&gt;&lt;/streamSourceUrl&gt;
+   *       &lt;streamTitle&gt;&lt;/streamTitle&gt;
+   *       &lt;streamUrl&gt;&lt;/streamUrl&gt;
+   *       &lt;title&gt;&lt;/title&gt;
+   *       &lt;titleId&gt;&lt;/titleId&gt;
+   *       &lt;url&gt;&lt;/url&gt;
+   *       &lt;userId&gt;&lt;/userId&gt;
+   *     &lt;/entry&gt;
+   *   &lt;/activity&gt;
+   * &lt;/response&gt;
+   * </pre>
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetActivitiesXml() throws Exception {
+    String resp = getResponse("/activities/john.doe/@self", "GET", "xml",
+        "application/xml");
+    XSDValidator.validateOpenSocial(resp);
+
+
+    assertEquals("0", xp.evaluate("/:response/:startIndex", XMLUnit.buildTestDocument(resp)));
+    assertEquals("1", xp.evaluate("/:response/:totalResults", XMLUnit.buildTestDocument(resp)));
+    NodeList nl = xp.getMatchingNodes("/:response/:list/:entry/:activity", XMLUnit.buildTestDocument(resp));
+
+    assertEquals(1, nl.getLength());
+
+    assertActivitiesEqual(johnsActivity, childNodesToMap(nl.item(0)));
+  }
+
+  /**
+   * Expected response for a list of activities in json:
+   *
+   *
+   * <pre>
+   * &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
+   * &lt;response xmlns=&quot;http://ns.opensocial.org/2008/opensocial&quot;
+   *    xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
+   *    xsi:schemaLocation=&quot;http://ns.opensocial.org/2008/opensocial file:/Users/ieb/Apache/shindig/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/opensocial.xsd&quot;&gt;
+   *   &lt;activity&gt;
+   *     &lt;itemsPerPage&gt;3&lt;/itemsPerPage&gt;
+   *     &lt;startIndex&gt;0&lt;/startIndex&gt;
+   *     &lt;totalResults&gt;10&lt;/totalResults&gt;
+   *     &lt;entry&gt;
+   *       &lt;appId&gt;&lt;/appId&gt;
+   *       &lt;body&gt;&lt;/body&gt;
+   *       &lt;bodyId&gt;&lt;/bodyId&gt;
+   *       &lt;externalId&gt;&lt;/externalId&gt;
+   *       &lt;id&gt;&lt;/id&gt;
+   *       &lt;mediaItems&gt;
+   *         &lt;mimeType&gt;&lt;/mimeType&gt;
+   *         &lt;type&gt;&lt;/type&gt;
+   *         &lt;url&gt;&lt;/url&gt;
+   *       &lt;/mediaItems&gt;
+   *       &lt;postedTime&gt;&lt;/postedTime&gt;
+   *       &lt;priority&gt;&lt;/priority&gt;
+   *       &lt;streamFaviconUrl&gt;&lt;/streamFaviconUrl&gt;
+   *       &lt;streamSourceUrl&gt;&lt;/streamSourceUrl&gt;
+   *       &lt;streamTitle&gt;&lt;/streamTitle&gt;
+   *       &lt;streamUrl&gt;&lt;/streamUrl&gt;
+   *       &lt;title&gt;&lt;/title&gt;
+   *       &lt;titleId&gt;&lt;/titleId&gt;
+   *       &lt;url&gt;&lt;/url&gt;
+   *       &lt;userId&gt;&lt;/userId&gt;
+   *     &lt;/entry&gt;
+   *   &lt;/activity&gt;
+   * &lt;/response&gt;
+   * </pre>
+   *
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetFriendsActivitiesXml() throws Exception {
+    String resp = getResponse("/activities/john.doe/@friends", "GET", "xml",
+        "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    assertEquals("0", xp.evaluate("/:response/:startIndex", XMLUnit.buildTestDocument(resp)));
+    assertEquals("2", xp.evaluate("/:response/:totalResults", XMLUnit.buildTestDocument(resp)));
+    NodeList nl = xp.getMatchingNodes("/:response/:list/:entry", XMLUnit.buildTestDocument(resp));
+
+    assertEquals(2, nl.getLength());
+  }
+
+  private void assertActivitiesEqual(Activity activity,
+      Map<String, List<String>> result) {
+    assertEquals(activity.getId(), result.get("id").get(0));
+    assertEquals(activity.getUserId(), result.get("userId").get(0));
+    assertEquals(activity.getTitle(), result.get("title").get(0));
+    assertEquals(activity.getBody(), result.get("body").get(0));
+  }
+
+  @Test
+  public void testCreateActivity() throws Exception {
+    String postData = XSDValidator.XMLDEC+"<activity><body>and dad.</body><title>hi mom!</title></activity>";
+    String createResponse = getResponse("/activities/john.doe/@self", "POST",
+        postData, "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(createResponse);
+
+    String resp = getResponse("/activities/john.doe/@self", "GET", "xml",
+        "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+
+    assertEquals("0", xp.evaluate("/:response/:startIndex", XMLUnit.buildTestDocument(resp)));
+    assertEquals("2", xp.evaluate("/:response/:totalResults", XMLUnit.buildTestDocument(resp)));
+    NodeList nl = xp.getMatchingNodes("/:response/:list/:entry/:activity", XMLUnit.buildTestDocument(resp));
+
+    assertEquals(2, nl.getLength());
+
+    Map<String, List<String>> v = childNodesToMap(nl.item(0));
+    if (v.containsKey("id")) {
+      v = childNodesToMap(nl.item(1));
+    }
+
+    assertEquals("hi mom!", v.get("title").get(0));
+    assertEquals("and dad.", v.get("body").get(0));
+  }
+
+  // TODO: Add tests for the fields= parameter
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlDataTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlDataTest.java
new file mode 100644
index 0000000..8fbbe05
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlDataTest.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.social.opensocial.util.XSDValidator;
+import org.custommonkey.xmlunit.XMLUnit;
+import org.junit.Test;
+import org.w3c.dom.NodeList;
+
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+
+public class RestfulXmlDataTest extends AbstractLargeRestfulTests {
+
+  /**
+   * Expected response for app data in json:
+   *
+   * {
+   * "entry" : {
+   * "jane.doe" : {"count" : "7"},
+   * "george.doe" : {"count" : "2"},
+   * "maija.m" : {}, // TODO: Should this entry really be included if she
+   * doesn't have any data? } }
+   *                   s
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetFriendsAppDataJson() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = ImmutableMap.of("fields", "count");
+    String resp = getResponse("/appdata/john.doe/@friends/app", "GET",
+        extraParams, "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+     // /*[local-name()="TestSchema" and namespace-uri()='http://MapTest.TestSchema']/*[local-name()="A"]
+
+    NodeList result = xp.getMatchingNodes("/:appdata/:entry", XMLUnit.buildTestDocument(resp));
+    assertEquals(3, result.getLength());
+
+    Map<String, Map<String, List<String>>> v = childNodesToMapofMap(result);
+
+    assertEquals(3, v.size());
+    assertTrue(v.containsKey("jane.doe"));
+    assertTrue(v.containsKey("george.doe"));
+    assertTrue(v.containsKey("maija.m"));
+
+    assertEquals(1, v.get("jane.doe").size());
+    assertEquals(1, v.get("george.doe").size());
+    assertEquals(0, v.get("maija.m").size());
+
+    assertEquals("7", v.get("jane.doe").get("count").get(0));
+    assertEquals("2", v.get("george.doe").get("count").get(0));
+  }
+
+  /**
+   * Expected response for app data in json:
+   *
+   * { "entry" : {
+   * "john.doe" : {"count" : "0"}, } }
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetSelfAppDataJson() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", null);
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET",
+        extraParams, "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    NodeList result = xp.getMatchingNodes("/:appdata/:entry", XMLUnit.buildTestDocument(resp));
+
+    Map<String, Map<String, List<String>>> v = childNodesToMapofMap(result);
+
+    assertEquals(1, v.size());
+    assertTrue(v.containsKey("john.doe"));
+
+    assertEquals(1, v.get("john.doe").size());
+
+    assertEquals("0", v.get("john.doe").get("count").get(0));
+
+  }
+
+  /**
+   * Expected response for app data in json:
+   *
+   * { "entry" : { "john.doe" : {"count" : "0"}, } }
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetSelfAppDataJsonWithKey() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "count");
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET",
+        extraParams, "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    NodeList result = xp.getMatchingNodes("/:appdata/:entry", XMLUnit.buildTestDocument(resp));
+
+    Map<String, Map<String, List<String>>> v = childNodesToMapofMap(result);
+
+    assertEquals(1, v.size());
+    assertTrue(v.containsKey("john.doe"));
+
+    assertEquals(1, v.get("john.doe").size());
+
+    assertEquals("0", v.get("john.doe").get("count").get(0));
+  }
+
+  /**
+   * Expected response for app data in json with non-existant key: TODO: Double
+   * check this output with the spec
+   *
+   * { "entry" : { "john.doe" : {}, } }
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetSelfAppDataJsonWithInvalidKeys() throws Exception {
+    // app id is mocked out
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "peabody");
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET",
+        extraParams, "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    NodeList result = xp.getMatchingNodes("/:appdata/:entry", XMLUnit.buildTestDocument(resp));
+
+    Map<String, Map<String, List<String>>> v = childNodesToMapofMap(result);
+
+    assertEquals(1, v.size());
+    assertTrue(v.containsKey("john.doe"));
+
+    assertEquals(0, v.get("john.doe").size());
+  }
+
+  @Test
+  public void testDeleteAppData() throws Exception {
+    assertCount("0");
+
+    // With the wrong field
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "peabody");
+    String resp = getResponse("/appdata/john.doe/@self/app", "DELETE", extraParams, "xml",
+        "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    assertCount("0");
+
+    // should be xml ?
+    extraParams.put("fields", "count");
+    getResponse("/appdata/john.doe/@self/app", "DELETE", extraParams, "xml",
+        "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    assertCount(null);
+  }
+
+  @Test
+  public void testUpdateAppData() throws Exception {
+    assertCount("0");
+
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("fields", "count");
+    // should be xml ?
+    String postData = XSDValidator.XMLDEC + "<map><entry><key>count</key><value>5</value></entry></map>";
+    String resp = getResponse("/appdata/john.doe/@self/app", "POST", extraParams, postData,
+        "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+
+    assertCount("5");
+  }
+
+  private void assertCount(String expectedCount) throws Exception {
+    String resp = getResponse("/appdata/john.doe/@self/app", "GET", "xml",
+        "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    NodeList result = xp.getMatchingNodes("/:appdata/:entry", XMLUnit.buildTestDocument(resp));
+
+    Map<String, Map<String, List<String>>> v = childNodesToMapofMap(result);
+
+    assertEquals(1, v.size());
+    assertTrue(v.containsKey("john.doe"));
+
+    if (expectedCount != null) {
+      assertEquals(1, v.get("john.doe").size());
+
+      assertEquals(String.valueOf(expectedCount), v.get("john.doe")
+          .get("count").get(0));
+    } else {
+      assertEquals(0, v.get("john.doe").size());
+
+    }
+  }
+
+  // TODO: support for indexBy??
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlPeopleTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlPeopleTest.java
new file mode 100644
index 0000000..ffc4986
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/RestfulXmlPeopleTest.java
@@ -0,0 +1,598 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import org.apache.shindig.protocol.model.Enum;
+import org.apache.shindig.protocol.model.EnumImpl;
+import org.apache.shindig.social.core.model.AddressImpl;
+import org.apache.shindig.social.core.model.BodyTypeImpl;
+import org.apache.shindig.social.core.model.ListFieldImpl;
+import org.apache.shindig.social.core.model.NameImpl;
+import org.apache.shindig.social.core.model.OrganizationImpl;
+import org.apache.shindig.social.core.model.PersonImpl;
+import org.apache.shindig.social.core.model.UrlImpl;
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.BodyType;
+import org.apache.shindig.social.opensocial.model.Drinker;
+import org.apache.shindig.social.opensocial.model.ListField;
+import org.apache.shindig.social.opensocial.model.LookingFor;
+import org.apache.shindig.social.opensocial.model.Name;
+import org.apache.shindig.social.opensocial.model.NetworkPresence;
+import org.apache.shindig.social.opensocial.model.Organization;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.model.Smoker;
+import org.apache.shindig.social.opensocial.model.Url;
+import org.apache.shindig.social.opensocial.util.XSDValidator;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.custommonkey.xmlunit.XMLUnit;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public class RestfulXmlPeopleTest extends AbstractLargeRestfulTests {
+  private Person canonical;
+
+  @Before
+  public void restfulxXmlPeopleTestBefore() throws Exception {
+
+    NameImpl name = new NameImpl("Sir Shin H. Digg Social Butterfly");
+    name.setAdditionalName("H");
+    name.setFamilyName("Digg");
+    name.setGivenName("Shin");
+    name.setHonorificPrefix("Sir");
+    name.setHonorificSuffix("Social Butterfly");
+    canonical = new PersonImpl("canonical", "Shin Digg", name);
+
+    canonical.setAboutMe("I have an example of every piece of data");
+    canonical.setActivities(Lists.newArrayList("Coding Shindig"));
+
+    Address address = new AddressImpl(
+        "PoBox 3565, 1 OpenStandards Way, Apache, CA");
+    address.setCountry("US");
+    address.setLatitude(28.3043F);
+    address.setLongitude(143.0859F);
+    address.setLocality("who knows");
+    address.setPostalCode("12345");
+    address.setRegion("Apache, CA");
+    address.setStreetAddress("1 OpenStandards Way");
+    address.setType("home");
+    address.setFormatted("PoBox 3565, 1 OpenStandards Way, Apache, CA");
+    canonical.setAddresses(Lists.newArrayList(address));
+
+    canonical.setAge(33);
+    BodyTypeImpl bodyType = new BodyTypeImpl();
+    bodyType.setBuild("svelte");
+    bodyType.setEyeColor("blue");
+    bodyType.setHairColor("black");
+    bodyType.setHeight(1.84F); // meters as per spec
+    bodyType.setWeight(74F); // kg as per spec
+    canonical.setBodyType(bodyType);
+
+    canonical.setBooks(Lists.newArrayList("The Cathedral & the Bazaar",
+        "Catch 22"));
+    canonical.setCars(Lists.newArrayList("beetle", "prius"));
+    canonical.setChildren("3");
+    AddressImpl location = new AddressImpl();
+    location.setLatitude(48.858193F);
+    location.setLongitude(2.29419F);
+    canonical.setCurrentLocation(location);
+
+    canonical.setBirthday(new Date());
+    canonical.setDrinker(new EnumImpl<Drinker>(Drinker.SOCIALLY));
+    ListField email = new ListFieldImpl("work",
+        "dev@shindig.apache.org");
+    canonical.setEmails(Lists.newArrayList(email));
+
+    canonical.setEthnicity("developer");
+    canonical.setFashion("t-shirts");
+    canonical.setFood(Lists.newArrayList("sushi", "burgers"));
+    canonical.setGender(Person.Gender.male);
+    canonical.setHappiestWhen("coding");
+    canonical.setHasApp(true);
+    canonical
+        .setHeroes(Lists.newArrayList("Doug Crockford", "Charles Babbage"));
+    canonical.setHumor("none to speak of");
+    canonical.setInterests(Lists.newArrayList("PHP", "Java"));
+    canonical.setJobInterests("will work for beer");
+
+    Organization job1 = new OrganizationImpl();
+    job1.setAddress(new AddressImpl("1 Shindig Drive"));
+    job1.setDescription("lots of coding");
+    job1.setEndDate(new Date());
+    job1.setField("Software Engineering");
+    job1.setName("Apache.com");
+    job1.setSalary("$1000000000");
+    job1.setStartDate(new Date());
+    job1.setSubField("Development");
+    job1.setTitle("Grand PooBah");
+    job1.setWebpage("http://shindig.apache.org/");
+    job1.setType("job");
+
+    Organization job2 = new OrganizationImpl();
+    job2.setAddress(new AddressImpl("1 Skid Row"));
+    job2.setDescription("");
+    job2.setEndDate(new Date());
+    job2.setField("College");
+    job2.setName("School of hard Knocks");
+    job2.setSalary("$100");
+    job2.setStartDate(new Date());
+    job2.setSubField("Lab Tech");
+    job2.setTitle("Gopher");
+    job2.setWebpage("");
+    job2.setType("job");
+
+    canonical.setOrganizations(Lists.newArrayList(job1, job2));
+
+    canonical.setUpdated(new Date());
+    canonical.setLanguagesSpoken(Lists.newArrayList("English", "Dutch",
+        "Esperanto"));
+    canonical.setLivingArrangement("in a house");
+    org.apache.shindig.protocol.model.Enum<LookingFor> lookingForRandom = new EnumImpl<LookingFor>(
+        LookingFor.RANDOM, "Random");
+    Enum<LookingFor> lookingForNetworking = new EnumImpl<LookingFor>(
+        LookingFor.NETWORKING, "Networking");
+    canonical.setLookingFor(Lists.newArrayList(lookingForRandom,
+        lookingForNetworking));
+    canonical.setMovies(Lists.newArrayList("Iron Man", "Nosferatu"));
+    canonical.setMusic(Lists.newArrayList("Chieftains", "Beck"));
+    canonical.setNetworkPresence(new EnumImpl<NetworkPresence>(
+        NetworkPresence.ONLINE));
+    canonical.setNickname("diggy");
+    canonical.setPets("dog,cat");
+    canonical.setPhoneNumbers(Lists.<ListField> newArrayList(new ListFieldImpl(
+        "work", "111-111-111"), new ListFieldImpl("mobile", "999-999-999")));
+
+    canonical.setPoliticalViews("open leaning");
+    canonical.setProfileSong(new UrlImpl(
+        "http://www.example.org/songs/OnlyTheLonely.mp3", "Feelin' blue",
+        "road"));
+    canonical.setProfileVideo(new UrlImpl(
+        "http://www.example.org/videos/Thriller.flv", "Thriller", "video"));
+
+    canonical.setQuotes(Lists.newArrayList("I am therfore I code", "Doh!"));
+    canonical.setRelationshipStatus("married to my job");
+    canonical.setReligion("druidic");
+    canonical.setRomance("twice a year");
+    canonical.setScaredOf("COBOL");
+    canonical.setSexualOrientation("north");
+    canonical.setSmoker(new EnumImpl<Smoker>(Smoker.NO));
+    canonical.setSports(Lists.newArrayList("frisbee", "rugby"));
+    canonical.setStatus("happy");
+    canonical.setTags(Lists.newArrayList("C#", "JSON", "template"));
+    canonical.setThumbnailUrl("/images/nophoto.gif");
+    canonical.setUtcOffset(-8L);
+    canonical.setTurnOffs(Lists.newArrayList("lack of unit tests", "cabbage"));
+    canonical.setTurnOns(Lists.newArrayList("well document code"));
+    canonical.setTvShows(Lists.newArrayList("House", "Battlestar Galactica"));
+
+    canonical
+        .setUrls(Lists.<Url> newArrayList(new UrlImpl(
+            "http://www.example.org/?id=1", "my profile", "Profile"),
+            new UrlImpl("/images/nophoto.gif",
+                "my awesome picture", "Thumbnail")));
+
+  }
+
+  /**
+   * Expected response for john.doe's json:
+   *
+   * { 'entry' :
+   * { 'id' : 'john.doe',
+   * 'name' : {'formatted' : 'John Doe'},
+   * 'phoneNumbers' : [ { 'number' : '+33H000000000', 'type' : 'home'}, ],
+   * 'addresses' : [ {'formatted' : 'My home address'} ],
+   * 'emails' : [
+   *    { 'value' : 'john.doe@work.bar', 'type' : 'work'}, ]
+   *
+   * ... etc, etc for all fields in the person object } } TODO: Finish up this
+   * test and make refactor so that it is easier to read
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @SuppressWarnings("boxing")
+  @Test
+  public void testGetPersonJson() throws Exception {
+    // TODO(doll): Test all of the date fields
+
+    Map<String, String> extraParams = Maps.newHashMap();
+    StringBuilder allFieldsParam = new StringBuilder();
+    for (String allField : Person.Field.ALL_FIELDS) {
+      allFieldsParam.append(allField).append(',');
+    }
+    extraParams.put("fields", allFieldsParam.toString());
+
+    // Currently, for Shindig {pid}/@all/{uid} == {uid}/@self
+    String resp = getResponse("/people/canonical/@self", "GET", extraParams,
+        "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    NodeList resultNodeList = xp.getMatchingNodes("/:response/:person", XMLUnit.buildTestDocument(resp));
+    assertEquals(1, resultNodeList.getLength());
+
+    Node personNode = resultNodeList.item(0);
+
+    Map<String, List<Node>> childNodeMap = childNodesToNodeMap(personNode);
+    Map<String, List<String>> result = childNodesToMap(personNode);
+
+    assertStringField(result, canonical.getAboutMe(), Person.Field.ABOUT_ME);
+    assertStringListField(result, canonical.getActivities(),
+        Person.Field.ACTIVITIES);
+
+    List<Node> addressNodes = childNodeMap.get(Person.Field.ADDRESSES
+        .toString());
+    assertEquals(addressNodes.size(), canonical.getAddresses().size());
+    for (int i = 0; i < addressNodes.size(); i++) {
+      assertAddressField(canonical.getAddresses().get(i),
+          childNodesToMap(addressNodes.get(i)));
+    }
+
+    assertEquals(canonical.getAge().intValue(), Integer.parseInt(result.get(
+        Person.Field.AGE.toString()).get(0)));
+
+    Map<String, List<String>> bodyMap = childNodesToMap(childNodeMap.get(
+        Person.Field.BODY_TYPE.toString()).get(0));
+    BodyType body = canonical.getBodyType();
+
+    assertStringField(bodyMap, body.getBuild(), BodyType.Field.BUILD);
+    assertStringField(bodyMap, body.getEyeColor(), BodyType.Field.EYE_COLOR);
+    assertStringField(bodyMap, body.getHairColor(), BodyType.Field.HAIR_COLOR);
+    assertFloatField(bodyMap, body.getHeight(), BodyType.Field.HEIGHT);
+    assertFloatField(bodyMap, body.getWeight(), BodyType.Field.WEIGHT);
+
+    assertStringListField(result, canonical.getBooks(), Person.Field.BOOKS);
+    assertStringListField(result, canonical.getCars(), Person.Field.CARS);
+    assertStringField(result, canonical.getChildren(), Person.Field.CHILDREN);
+
+    Map<String, List<String>> currentLocation = childNodesToMap(childNodeMap
+        .get(Person.Field.CURRENT_LOCATION.toString()).get(0));
+    assertFloatField(currentLocation, canonical.getCurrentLocation()
+        .getLatitude(), Address.Field.LATITUDE);
+    assertFloatField(currentLocation, canonical.getCurrentLocation()
+        .getLongitude(), Address.Field.LONGITUDE);
+
+    assertStringField(result, canonical.getDisplayName(),
+        Person.Field.DISPLAY_NAME);
+
+    // assertLongField(result, canonical.getBirthday().getTime(),
+    // Person.Field.BIRTHDAY);
+    // assertEnumField(result, canonical.getDrinker(), Person.Field.DRINKER);
+
+    List<Node> emailArray = childNodeMap.get(Person.Field.EMAILS.toString());
+    assertEquals(1, emailArray.size());
+
+    for (int i = 0; i < canonical.getEmails().size(); i++) {
+      ListField expectedEmail = canonical.getEmails().get(i);
+      Map<String, List<String>> actualEmail = childNodesToMap(emailArray.get(i));
+
+      assertStringField(actualEmail, expectedEmail.getType(),
+          ListField.Field.TYPE);
+      assertStringField(actualEmail, expectedEmail.getValue(),
+          ListField.Field.VALUE);
+    }
+
+    assertStringField(result, canonical.getEthnicity(), Person.Field.ETHNICITY);
+    assertStringField(result, canonical.getFashion(), Person.Field.FASHION);
+    assertStringListField(result, canonical.getFood(), Person.Field.FOOD);
+    assertStringField(result, canonical.getGender().toString(),
+        Person.Field.GENDER);
+    assertStringField(result, canonical.getHappiestWhen(),
+        Person.Field.HAPPIEST_WHEN);
+    assertBooleanField(result, canonical.getHasApp(), Person.Field.HAS_APP);
+    assertStringListField(result, canonical.getHeroes(), Person.Field.HEROES);
+    assertStringField(result, canonical.getHumor(), Person.Field.HUMOR);
+    assertStringField(result, canonical.getId(), Person.Field.ID);
+    assertStringListField(result, canonical.getInterests(),
+        Person.Field.INTERESTS);
+    assertStringField(result, canonical.getJobInterests(),
+        Person.Field.JOB_INTERESTS);
+
+    assertOrganizationField(canonical.getOrganizations().get(0), childNodeMap
+        .get(Person.Field.ORGANIZATIONS.toString()).get(0));
+
+    assertStringListField(result, canonical.getLanguagesSpoken(),
+        Person.Field.LANGUAGES_SPOKEN);
+    // assertDateField(result, canonical.getUpdated(),
+    // Person.Field.LAST_UPDATED);
+    assertStringField(result, canonical.getLivingArrangement(),
+        Person.Field.LIVING_ARRANGEMENT);
+    assertListEnumField(childNodeMap, canonical.getLookingFor(),
+        Person.Field.LOOKING_FOR);
+    assertStringListField(result, canonical.getMovies(), Person.Field.MOVIES);
+    assertStringListField(result, canonical.getMusic(), Person.Field.MUSIC);
+
+    assertEquals(canonical.getName().getFormatted(), childNodesToMap(
+        childNodeMap.get(Person.Field.NAME.toString()).get(0)).get(
+        Name.Field.FORMATTED.toString()).get(0));
+
+    assertEnumField(childNodeMap, canonical.getNetworkPresence(),
+        Person.Field.NETWORKPRESENCE);
+    assertStringField(result, canonical.getNickname(), Person.Field.NICKNAME);
+    assertStringField(result, canonical.getPets(), Person.Field.PETS);
+
+    List<Node> phoneArray = childNodeMap.get(Person.Field.PHONE_NUMBERS
+        .toString());
+    assertEquals(canonical.getPhoneNumbers().size(), phoneArray.size());
+
+    for (int i = 0; i < canonical.getPhoneNumbers().size(); i++) {
+      ListField expectedPhone = canonical.getPhoneNumbers().get(i);
+      Map<String, List<String>> actualPhone = childNodesToMap(phoneArray.get(i));
+      assertEquals(expectedPhone.getType(), actualPhone.get(
+          ListField.Field.TYPE.toString()).get(0));
+      assertEquals(expectedPhone.getValue(), actualPhone.get(
+          ListField.Field.VALUE.toString()).get(0));
+    }
+
+    assertStringField(result, canonical.getPoliticalViews(),
+        Person.Field.POLITICAL_VIEWS);
+
+    assertUrlField(canonical.getProfileSong(), childNodesToMap(childNodeMap
+        .get(Person.Field.PROFILE_SONG.toString()).get(0)));
+    assertStringField(result, canonical.getProfileUrl(),
+        Person.Field.PROFILE_URL);
+    assertUrlField(canonical.getProfileVideo(), childNodesToMap(childNodeMap
+        .get(Person.Field.PROFILE_VIDEO.toString()).get(0)));
+
+    assertStringListField(result, canonical.getQuotes(), Person.Field.QUOTES);
+    assertStringField(result, canonical.getRelationshipStatus(),
+        Person.Field.RELATIONSHIP_STATUS);
+    assertStringField(result, canonical.getReligion(), Person.Field.RELIGION);
+    assertStringField(result, canonical.getRomance(), Person.Field.ROMANCE);
+    assertStringField(result, canonical.getScaredOf(), Person.Field.SCARED_OF);
+
+    assertStringField(result, canonical.getSexualOrientation(),
+        Person.Field.SEXUAL_ORIENTATION);
+    assertEnumField(childNodeMap, canonical.getSmoker(), Person.Field.SMOKER);
+    assertStringListField(result, canonical.getSports(), Person.Field.SPORTS);
+    assertStringField(result, canonical.getStatus(), Person.Field.STATUS);
+    assertStringListField(result, canonical.getTags(), Person.Field.TAGS);
+    assertStringField(result, canonical.getThumbnailUrl(),
+        Person.Field.THUMBNAIL_URL);
+    // TODO: time zone
+    assertStringListField(result, canonical.getTurnOffs(),
+        Person.Field.TURN_OFFS);
+    assertStringListField(result, canonical.getTurnOns(), Person.Field.TURN_ONS);
+    assertStringListField(result, canonical.getTvShows(), Person.Field.TV_SHOWS);
+  }
+
+  private void assertAddressField(Address expected,
+      Map<String, List<String>> actual) {
+    assertStringField(actual, expected.getCountry(), Address.Field.COUNTRY);
+    assertFloatField(actual, expected.getLatitude(), Address.Field.LATITUDE);
+    assertStringField(actual, expected.getLocality(), Address.Field.LOCALITY);
+    assertFloatField(actual, expected.getLongitude(), Address.Field.LONGITUDE);
+    assertStringField(actual, expected.getPostalCode(),
+        Address.Field.POSTAL_CODE);
+    assertStringField(actual, expected.getRegion(), Address.Field.REGION);
+    assertStringField(actual, expected.getStreetAddress(),
+        Address.Field.STREET_ADDRESS);
+    assertStringField(actual, expected.getType(), Address.Field.TYPE);
+    assertStringField(actual, expected.getFormatted(), Address.Field.FORMATTED);
+  }
+
+  private void assertUrlField(Url expected, Map<String, List<String>> actual) {
+    assertStringField(actual, expected.getValue(), Url.Field.VALUE);
+    assertStringField(actual, expected.getLinkText(), Url.Field.LINK_TEXT);
+    assertStringField(actual, expected.getType(), Url.Field.TYPE);
+  }
+
+  private void assertOrganizationField(Organization expected, Node orgNode) {
+    Map<String, List<String>> actual = childNodesToMap(orgNode);
+    Map<String, List<Node>> actualNode = childNodesToNodeMap(orgNode);
+    assertStringField(childNodesToMap(actualNode.get(
+        Organization.Field.ADDRESS.toString()).get(0)), expected.getAddress()
+        .getFormatted(), Address.Field.FORMATTED);
+    assertStringField(actual, expected.getDescription(),
+        Organization.Field.DESCRIPTION);
+    // assertDateField(actual, expected.getEndDate(),
+    // Organization.Field.END_DATE);
+    assertStringField(actual, expected.getField(), Organization.Field.FIELD);
+    assertStringField(actual, expected.getName(), Organization.Field.NAME);
+    assertStringField(actual, expected.getSalary(), Organization.Field.SALARY);
+    // assertDateField(actual, expected.getStartDate(),
+    // Organization.Field.START_DATE);
+    assertStringField(actual, expected.getSubField(),
+        Organization.Field.SUB_FIELD);
+    assertStringField(actual, expected.getTitle(), Organization.Field.TITLE);
+    assertStringField(actual, expected.getWebpage(), Organization.Field.WEBPAGE);
+    assertStringField(actual, expected.getType(), Organization.Field.TYPE);
+  }
+
+  private void assertBooleanField(Map<String, List<String>> result,
+      boolean expected, Object field) {
+    assertEquals(expected, Boolean.parseBoolean(result.get(field.toString())
+        .get(0)));
+  }
+
+  @SuppressWarnings("boxing")
+  private void assertFloatField(Map<String, List<String>> result, Float expected, Object field) {
+    assertEquals(expected.floatValue(), Float.valueOf(result.get(field.toString()).get(0)), 0);
+  }
+
+  private void assertStringField(Map<String, List<String>> result,
+      String expected, Object field) {
+    List<String> v = result.get(field.toString());
+    String t;
+    if ( v == null || v.isEmpty()) {
+      if (expected == null ) {
+        return;
+      }
+      t = "";
+    } else {
+      t = v.get(0);
+    }
+    assertEquals(expected, t);
+  }
+
+  private void assertStringListField(Map<String, List<String>> result,
+      List<String> list, Person.Field field) {
+    assertEquals(list.size(), result.get(field.toString()).size());
+    for (int i = 0; i < list.size(); i++) {
+      assertEquals(list.get(i), result.get(field.toString()).get(i));
+    }
+  }
+
+  private void assertEnumField(Map<String, List<Node>> result, Enum<?> expected,
+      Person.Field field) {
+    Map<String, List<String>> actual = childNodesToMap(result.get(
+        field.toString()).get(0));
+    assertEquals(expected.getDisplayValue(), actual.get("displayValue").get(0));
+    assertEquals(expected.getValue().toString(), actual.get("value").get(0));
+  }
+
+  private void assertListEnumField(Map<String, List<Node>> result,
+      List<? extends Enum<? extends Enum.EnumKey>> expected, Person.Field field) {
+    List<Node> actual = result.get(field.toString());
+    for (int i = 0; i < actual.size(); i++) {
+      Map<String, List<String>> nm = childNodesToMap(actual.get(i));
+      assertEquals(expected.get(i).getDisplayValue(), nm.get("displayValue")
+          .get(0));
+      assertEquals(expected.get(i).getValue().toString(), nm.get("value")
+          .get(0));
+    }
+  }
+
+  /**
+   * Expected response for a list of people in json:
+   *
+   * { "totalResults" : 3,
+   *     "startIndex" : 0
+   *     "entry" : [ {<jane doe>}, // layed out like above
+   * {<george doe>}, {<maija m>}, ] }
+   *
+   * @throws Exception
+   *           if test encounters an error
+   */
+  @Test
+  public void testGetPeople() throws Exception {
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("sortBy", "name");
+    extraParams.put("sortOrder", null);
+    extraParams.put("filterBy", null);
+    extraParams.put("startIndex", null);
+    extraParams.put("count", "20");
+    extraParams.put("fields", null);
+
+    // Currently, for Shindig @all == @friends
+    String resp = getResponse("/people/john.doe/@friends", "GET", extraParams,
+        "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    NodeList resultNodeList = xp.getMatchingNodes("/:response", XMLUnit.buildTestDocument(resp));
+    assertEquals(1, resultNodeList.getLength());
+
+    Map<String, List<String>> result = childNodesToMap(resultNodeList.item(0));
+    Map<String, List<Node>> resultNodes = childNodesToNodeMap(resultNodeList
+        .item(0));
+
+    assertEquals("3", result.get("totalResults").get(0));
+    assertEquals("0", result.get("startIndex").get(0));
+
+    // The users should be in alphabetical order
+    List<Node> listNodes = resultNodes.get("list");
+    Map<String, List<Node>> listNodesChildMap = childNodesToNodeMap(listNodes.get(0));
+    List<Node> entries = listNodesChildMap.get("entry");
+
+    Map<String, List<Node>> entryOne = childNodesToNodeMap(entries.get(0));
+    assertPerson(childNodesToNodeMap(entryOne.get("person").get(0)),
+        "george.doe", "George Doe");
+
+    Map<String, List<Node>> entryTwo = childNodesToNodeMap(entries.get(1));
+    assertPerson(childNodesToNodeMap(entryTwo.get("person").get(0)),
+        "jane.doe", "Jane Doe");
+  }
+
+  @Test
+  public void testGetPeoplePagination() throws Exception {
+    Map<String, String> extraParams = Maps.newHashMap();
+    extraParams.put("sortBy", "name");
+    extraParams.put("sortOrder", null);
+    extraParams.put("filterBy", null);
+    extraParams.put("startIndex", "0");
+    extraParams.put("count", "1");
+    extraParams.put("fields", null);
+
+    String resp = getResponse("/people/john.doe/@friends", "GET", extraParams,
+        "xml", "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    NodeList resultNodeList = xp.getMatchingNodes("/:response", XMLUnit.buildTestDocument(resp));
+    assertEquals(1, resultNodeList.getLength());
+
+    Map<String, List<String>> result = childNodesToMap(resultNodeList.item(0));
+    Map<String, List<Node>> resultNodes = childNodesToNodeMap(resultNodeList
+        .item(0));
+
+    assertEquals("3", result.get("totalResults").get(0));
+    assertEquals("0", result.get("startIndex").get(0));
+
+    List<Node> listNodes = resultNodes.get("list");
+    Map<String, List<Node>> listNodesChildMap = childNodesToNodeMap(listNodes.get(0));
+    List<Node> entries = listNodesChildMap.get("entry");
+
+    Map<String, List<Node>> entryOne = childNodesToNodeMap(entries.get(0));
+
+    assertPerson(childNodesToNodeMap(entryOne.get("person").get(0)),
+        "george.doe", "George Doe");
+
+    // Get the second page
+    extraParams.put("startIndex", "1");
+    resp = getResponse("/people/john.doe/@friends", "GET", extraParams, "xml",
+        "application/xml");
+
+    XSDValidator.validateOpenSocial(resp);
+
+    resultNodeList = xp.getMatchingNodes("/:response", XMLUnit.buildTestDocument(resp));
+    assertEquals(1, resultNodeList.getLength());
+
+    result = childNodesToMap(resultNodeList.item(0));
+    resultNodes = childNodesToNodeMap(resultNodeList.item(0));
+
+    assertEquals("3", result.get("totalResults").get(0));
+    assertEquals("1", result.get("startIndex").get(0));
+
+    listNodes = resultNodes.get("list");
+    listNodesChildMap = childNodesToNodeMap(listNodes.get(0));
+    entries = listNodesChildMap.get("entry");
+
+    Map<String, List<Node>> entryTwo = childNodesToNodeMap(entries.get(0));
+    assertPerson(childNodesToNodeMap(entryTwo.get("person").get(0)),
+        "jane.doe", "Jane Doe");
+  }
+
+  private void assertPerson(Map<String, List<Node>> person, String expectedId,
+      String expectedName) throws Exception {
+    assertEquals(expectedId, person.get("id").get(0).getTextContent());
+    assertEquals(expectedName, childNodesToMap(person.get("name").get(0)).get(
+        "formatted").get(0));
+  }
+
+  // TODO: Add tests for fields parameter
+  // TODO: Add tests for networkDistance
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/TestUtils.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/TestUtils.java
new file mode 100644
index 0000000..74bbaef
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/TestUtils.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.dataservice.integration;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+
+/**
+ * Collection of utilities to assist in testing.
+ */
+public class TestUtils {
+
+  /**
+   * Loads the contents of the test fixture specified at the given path.
+   *
+   * @param path specifies the file to load the contents of
+   * @return String is the file contents
+   * @throws IOException
+   */
+  public static String loadTestFixture(String path) throws IOException {
+    BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(path)));
+    StringBuilder sb = new StringBuilder();
+    String line;
+    while ((line = br.readLine()) != null) {
+      sb.append(line);
+    }
+    return sb.toString();
+  }
+
+  /**
+   * Tests two JSON strings for equality by performing a deep comparison.
+   *
+   * @param json1 represents a JSON object to compare with json2
+   * @param json2 represents a JSON object to compare with json1
+   * @return true if the JSON objects are equal, false otherwise
+   */
+  public static boolean jsonsEqual(String json1, String json2) throws Exception {
+    Object obj1Converted = convertJsonElement(new JSONObject(json1));
+    Object obj2Converted = convertJsonElement(new JSONObject(json2));
+    return obj1Converted.equals(obj2Converted);
+  }
+
+  /**
+   * Tests the DOMs represented by two XML strings for equality by performing
+   * a deep comparison.
+   *
+   * @param xml1 represents the XML DOM to compare with xml2
+   * @param xml2 represents the XML DOM to compare with xml1
+   *
+   * return true if the represented DOMs are equal, false otherwise
+   */
+  public static boolean xmlsEqual(String xml1, String xml2) throws Exception {
+    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+    DocumentBuilder db = dbf.newDocumentBuilder();
+
+    Document doc1 = db.parse(new InputSource(new StringReader(xml1)));
+    Document doc2 = db.parse(new InputSource(new StringReader(xml2)));
+
+    Set<Object> childSet1 = getChildSet(doc1.getDocumentElement(), "");
+    Set<Object> childSet2 = getChildSet(doc2.getDocumentElement(), "");
+
+    return childSet1.equals(childSet2); // comparing sets does all the hard work :)
+  }
+
+  // ---------------------------- PRIVATE HELPERS -----------------------------
+
+  /*
+   * Recursive utility to convert a JSONObject to an Object composed of Sets,
+   * Maps, and the target types (e.g. Integer, String, Double).  Used to do a
+   * deep comparison of two JSON objects.
+   *
+   * @param Object is the JSON element to convert (JSONObject, JSONArray, or target type)
+   *
+   * @return an Object representing the appropriate JSON element
+   */
+  @SuppressWarnings("unchecked")
+  private static Object convertJsonElement(Object elem) throws JSONException {
+    if (elem instanceof JSONObject) {
+      JSONObject obj = (JSONObject) elem;
+      Iterator<String> keys = obj.keys();
+      Map<String, Object> jsonMap = new HashMap<String, Object>();
+      while (keys.hasNext()) {
+        String key = keys.next();
+        jsonMap.put(key, convertJsonElement(obj.get(key)));
+      }
+      return jsonMap;
+    } else if (elem instanceof JSONArray) {
+      JSONArray arr = (JSONArray) elem;
+      Set<Object> jsonSet = new HashSet<Object>();
+      for (int i = 0; i < arr.length(); i++) {
+        jsonSet.add(convertJsonElement(arr.get(i)));
+      }
+      return jsonSet;
+    } else {
+      return elem;
+    }
+  }
+
+  /*
+   * Recursive utility to represent an XML Document as a Set.
+   *
+   * @param node is the root node to map to a Set
+   * @param basePath is the path to the root node
+   *
+   * @return Set<Object> represents the XML Document as a Set
+   */
+  private static Set<Object> getChildSet(Node node, String basePath) {
+    Set<Object> childSet = new HashSet<Object>();
+    if (!node.hasChildNodes() && !node.getTextContent().trim().equals("")) {
+      childSet.add(basePath + ":" + node.getTextContent());
+    } else {
+      NodeList children = node.getChildNodes();
+      for (int i = 0; i < children.getLength(); i++) {
+        childSet.add(getChildSet(children.item(i), basePath + "/" + node.getNodeName()));
+      }
+    }
+    return childSet;
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryAtomId.xml b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryAtomId.xml
new file mode 100644
index 0000000..05a1b58
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryAtomId.xml
@@ -0,0 +1,94 @@
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:osearch="http://a9.com/-/spec/opensearch/1.1">
+  <entry>
+    <id>activity2</id>
+    <title>John posted a new photo album.</title>
+    <summary type="html" />
+    <author>
+      <uri>http://example.org/john</uri>
+      <name>John Doe</name>
+    </author>
+    <link rel="alternate">http://example.org/album/my_fluffy_cat.jpg</link>
+    <content type="application/xml">
+      <activityEntry xmlns="http://ns.opensocial.org/2008/opensocial">
+        <actor>
+          <displayName>John Doe</displayName>
+          <id>john.doe</id>
+          <image>
+            <height>250</height>
+            <url>http://example.org/john/image</url>
+            <width>250</width>
+          </image>
+          <objectType>person</objectType>
+          <url>http://example.org/john</url>
+        </actor>
+        <generator>
+          <url>http://example.org/activities-app</url>
+        </generator>
+        <id>activity2</id>
+        <object>
+          <attachments>
+            <object>
+              <id>attachment1</id>
+              <objectType>attachment</objectType>
+            </object>
+            <object>
+              <id>attachment2</id>
+              <objectType>attachment</objectType>
+            </object>
+          </attachments>
+          <downstreamDuplicate>downstream1</downstreamDuplicate>
+          <downstreamDuplicate>downstream2</downstreamDuplicate>
+          <id>object2</id>
+          <image>
+            <height>250</height>
+            <url>http://example.org/album/my_fluffy_cat_thumb.jpg</url>
+            <width>250</width>
+          </image>
+          <objectType>photo</objectType>
+          <summary>Photo posted</summary>
+          <upstreamDuplicate>upstream1</upstreamDuplicate>
+          <upstreamDuplicate>upstream2</upstreamDuplicate>
+          <url>http://example.org/album/my_fluffy_cat.jpg</url>
+        </object>
+        <provider>
+          <url>http://example.org/activity-stream</url>
+        </provider>
+        <published>2011-03-10T15:04:55Z</published>
+        <target>
+          <displayName>John&apos;s Photo Album</displayName>
+          <id>target2</id>
+          <image>
+            <height>250</height>
+            <url>http://example.org/album/thumbnail.jpg</url>
+            <width>250</width>
+          </image>
+          <objectType>photo-album</objectType>
+          <url>http://example.org/album/</url>
+        </target>
+        <title>John posted a new photo album.</title>
+        <verb>post</verb>
+        <openSocial>
+          <embed>
+            <gadget>http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml</gadget>
+            <context>
+              <albumName>Germany 2009</albumName>
+                <photoUrls>
+                  <java.lang.String>http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg</java.lang.String>
+                </photoUrls>
+            </context>
+          </embed>
+        </openSocial>
+      </activityEntry>
+    </content>
+  </entry>
+  <osearch:startIndex>0</osearch:startIndex>
+  <osearch:totalResults>1</osearch:totalResults>
+  <osearch:itemsPerPage>1</osearch:itemsPerPage>
+
+</feed>
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryAtomIds.xml b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryAtomIds.xml
new file mode 100644
index 0000000..b3c980d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryAtomIds.xml
@@ -0,0 +1,154 @@
+<feed xmlns="http://www.w3.org/2005/Atom" xmlns:osearch="http://a9.com/-/spec/opensearch/1.1">
+  <entry>
+    <id>activity2</id>
+    <title>John posted a new photo album.</title>
+    <summary type="html" />
+    <author>
+      <uri>http://example.org/john</uri>
+      <name>John Doe</name>
+    </author>
+    <link rel="alternate">http://example.org/album/my_fluffy_cat.jpg</link>
+    <content type="application/xml">
+      <activityEntry xmlns="http://ns.opensocial.org/2008/opensocial">
+        <actor>
+          <displayName>John Doe</displayName>
+          <id>john.doe</id>
+          <image>
+            <height>250</height>
+            <url>http://example.org/john/image</url>
+            <width>250</width>
+          </image>
+          <objectType>person</objectType>
+          <url>http://example.org/john</url>
+        </actor>
+        <generator>
+          <url>http://example.org/activities-app</url>
+        </generator>
+        <id>activity2</id>
+        <object>
+          <attachments>
+            <object>
+              <id>attachment1</id>
+              <objectType>attachment</objectType>
+            </object>
+            <object>
+              <id>attachment2</id>
+              <objectType>attachment</objectType>
+            </object>
+          </attachments>
+          <downstreamDuplicate>downstream1</downstreamDuplicate>
+          <downstreamDuplicate>downstream2</downstreamDuplicate>
+          <id>object2</id>
+          <image>
+            <height>250</height>
+            <url>http://example.org/album/my_fluffy_cat_thumb.jpg</url>
+            <width>250</width>
+          </image>
+          <objectType>photo</objectType>
+          <summary>Photo posted</summary>
+          <upstreamDuplicate>upstream1</upstreamDuplicate>
+          <upstreamDuplicate>upstream2</upstreamDuplicate>
+          <url>http://example.org/album/my_fluffy_cat.jpg</url>
+        </object>
+        <openSocial>
+          <embed>
+            <context>
+                <albumName>Germany 2009</albumName>
+                <photoUrls>
+                  <java.lang.String>http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg</java.lang.String>
+                </photoUrls>
+            </context>
+            <gadget>http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml</gadget>
+          </embed>
+        </openSocial>
+        <provider>
+          <url>http://example.org/activity-stream</url>
+        </provider>
+        <published>2011-03-10T15:04:55Z</published>
+        <target>
+          <displayName>John&apos;s Photo Album</displayName>
+          <id>target2</id>
+          <image>
+            <height>250</height>
+            <url>http://example.org/album/thumbnail.jpg</url>
+            <width>250</width>
+          </image>
+          <objectType>photo-album</objectType>
+          <url>http://example.org/album/</url>
+        </target>
+        <title>John posted a new photo album.</title>
+        <verb>post</verb>
+      </activityEntry>
+    </content>
+  </entry>
+  <entry>
+    <id>activity1</id>
+    <title>John shared new photos with you</title>
+    <summary type="html" />
+    <author>
+      <uri>http://example.org/john</uri>
+      <name>John Doe</name>
+    </author>
+    <link rel="alternate">http://example.org/blog/2011/02/entry</link>
+    <content type="application/xml">
+      <activityEntry xmlns="http://ns.opensocial.org/2008/opensocial">
+        <actor>
+          <displayName>John Doe</displayName>
+          <id>john.doe</id>
+          <image>
+            <height>250</height>
+            <url>http://example.org/john/image</url>
+            <width>250</width>
+          </image>
+          <objectType>person</objectType>
+          <url>http://example.org/john</url>
+        </actor>
+        <id>activity1</id>
+        <object>
+          <id>object1</id>
+          <url>http://example.org/blog/2011/02/entry</url>
+        </object>
+         <openSocial>
+          <embed>
+            <context>
+                <albumName>Germany 2009</albumName>
+                <eeGadget>http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml</eeGadget>
+                <photoUrls>
+                  <java.lang.String>http://farm4.static.flickr.com/3495/3925132517_5959dac775.jpg</java.lang.String>
+                  <java.lang.String>http://farm4.static.flickr.com/3629/3394799776_47676abb46.jpg</java.lang.String>
+                  <java.lang.String>http://farm5.static.flickr.com/4009/4413640211_715d924d9b.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2340/3528537244_d2fb037aba.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/36/98407782_9c4c5866d1.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/48/180544479_bb0d0f6559.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7.jpg</java.lang.String>
+                </photoUrls>
+            </context>
+            <gadget>http://localhost:8080/containers/embeddedexperiences/PhotoList.xml</gadget>
+          </embed>
+        </openSocial>
+        <published>2011-02-10T15:04:55Z</published>
+        <target>
+          <displayName>John&apos;s Blog</displayName>
+          <id>target1</id>
+          <objectType>blog</objectType>
+          <url>http://example.org/blog/</url>
+        </target>
+        <title>John shared new photos with you</title>
+        <verb>post</verb>
+      </activityEntry>
+    </content>
+  </entry>
+  <osearch:startIndex>0</osearch:startIndex>
+  <osearch:totalResults>2</osearch:totalResults>
+  <osearch:itemsPerPage>2</osearch:itemsPerPage>
+  <author>?</author>
+  <title>?</title>
+  <updated></updated>
+  <id>?</id>
+</feed>
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonCreated.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonCreated.json
new file mode 100644
index 0000000..b850ea9
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonCreated.json
@@ -0,0 +1,12 @@
+{
+   "entry": {
+   	 "id":"activityCreated",
+     "title":"Super Created Activity",
+     "actor":{
+        "id":"john.doe",
+     },
+     "object":{
+        "id":"objectCreated",
+     }
+   }
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonDelete.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonDelete.json
new file mode 100644
index 0000000..cf9e7db
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonDelete.json
@@ -0,0 +1,133 @@
+{
+  "itemsPerPage": 2,
+  "sorted": true,
+  "startIndex": 0,
+  "totalResults": 2,
+  "filtered": true,
+  "updatedSince": true,
+  "list": [{
+    "id": "activity2",
+    "title": "John posted a new photo album.",
+    "verb": "post",
+    "target": {
+      "id": "target2",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/thumbnail.jpg"
+      },
+      "displayName": "John's Photo Album",
+      "objectType": "photo-album",
+      "url": "http://example.org/album/"
+    },
+    "generator": {
+      "url": "http://example.org/activities-app"
+    },
+    "provider": {
+      "url": "http://example.org/activity-stream"
+    },
+    "object": {
+      "upstreamDuplicates": ["upstream1", "upstream2"],
+      "summary": "Photo posted",
+      "id": "object2",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/my_fluffy_cat_thumb.jpg"
+      },
+      "attachments": [{
+        "id": "attachment1",
+        "objectType": "attachment"
+      }, {
+        "id": "attachment2",
+        "objectType": "attachment"
+      }],
+      "downstreamDuplicates": ["downstream1", "downstream2"],
+      "objectType": "photo",
+      "url": "http://example.org/album/my_fluffy_cat.jpg"
+    },
+    "actor": {
+      "id": "john.doe",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/john/image"
+      },
+      "displayName": "John Doe",
+      "objectType": "person",
+      "url": "http://example.org/john"
+    },
+    "published": "2011-03-10T15:04:55Z",
+    "openSocial": {
+      "embed": {
+        "context": {
+          "photoUrls": ["http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg", "http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg", "http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg", "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg", "http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg", "http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg", "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg"],
+          "albumName": "Germany 2009"
+        },
+        "gadget": "http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml"
+      }
+    }
+  }, {
+    "id": "activity3",
+    "title": "John posted a new photo to his blog",
+    "verb": "post",
+    "target": {
+      "id": "target3",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/thumbnail.jpg"
+      },
+      "displayName": "John's Blog About Life",
+      "objectType": "blog",
+      "url": "http://example.org/blog/"
+    },
+    "generator": {
+      "url": "http://example.org/activities-app"
+    },
+    "provider": {
+      "url": "http://example.org/activity-stream"
+    },
+    "object": {
+      "summary": "Photo about new world",
+      "id": "object3",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/new_world.jpg"
+      },
+      "objectType": "photo",
+      "url": "http://example.org/album/new_world.jpg"
+    },
+    "actor": {
+      "id": "john.doe",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/john/image"
+      },
+      "displayName": "John Doe",
+      "objectType": "person",
+      "url": "http://example.org/john"
+    },
+    "published": "2012-06-02T10:02:55Z",
+    "openSocial": {
+      "embed": {
+        "context": {
+          "photoUrl": "http://example.org/album/new_world.jpg"
+        },
+        "preferredExperience": {
+          "display": {
+            "type": "text",
+            "label": "Checkout new photo in John's blog"
+          },
+          "target": {
+            "type": "gadget",
+            "view": "embedded_canvas"
+          }
+        },
+        "gadget": "http://localhost:8080/containers/embeddedexperiences/BlogViewer.xml"
+      }
+    }
+  }]
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonExtension.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonExtension.json
new file mode 100644
index 0000000..8555ccf
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonExtension.json
@@ -0,0 +1,19 @@
+{
+   "entry": {
+	  id: "activityCreated",
+	  title: "Super Created Activity",
+	  extension1: "extension1Value",
+	  object: {
+	    extension3: [
+	      {
+	        ext1: "ext1Value"
+	      }
+	    ],
+	    id: "objectCreated"
+	  },
+	  actor: {
+	    id: "john.doe",
+	    extension2: "extension2Value"
+	  }
+   }
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonGroup.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonGroup.json
new file mode 100644
index 0000000..624db1a
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonGroup.json
@@ -0,0 +1,169 @@
+{
+  "itemsPerPage": 3,
+  "sorted": true,
+  "startIndex": 0,
+  "totalResults": 3,
+  "filtered": true,
+  "updatedSince": true,
+  "list": [{
+    "id": "activity1",
+    "title": "John shared new photos with you",
+    "verb": "post",
+    "target": {
+      "id": "target1",
+      "displayName": "John's Blog",
+      "objectType": "blog",
+      "url": "http://example.org/blog/"
+    },
+    "object": {
+      "id": "object1",
+      "url": "http://example.org/blog/2011/02/entry"
+    },
+    "actor": {
+      "id": "john.doe",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/john/image"
+      },
+      "displayName": "John Doe",
+      "objectType": "person",
+      "url": "http://example.org/john"
+    },
+    "published": "2011-02-10T15:04:55Z",
+    "openSocial": {
+      "embed": {
+        "context": {
+          "eeGadget": "http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml",
+          "photoUrls": ["http://farm4.static.flickr.com/3495/3925132517_5959dac775.jpg", "http://farm4.static.flickr.com/3629/3394799776_47676abb46.jpg", "http://farm5.static.flickr.com/4009/4413640211_715d924d9b.jpg", "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba.jpg", "http://farm1.static.flickr.com/36/98407782_9c4c5866d1.jpg", "http://farm1.static.flickr.com/48/180544479_bb0d0f6559.jpg", "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7.jpg"],
+          "albumName": "Germany 2009"
+        },
+        "gadget": "http://localhost:8080/containers/embeddedexperiences/PhotoList.xml"
+      }
+    }
+  }, {
+    "id": "activity2",
+    "title": "John posted a new photo album.",
+    "verb": "post",
+    "target": {
+      "id": "target2",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/thumbnail.jpg"
+      },
+      "displayName": "John's Photo Album",
+      "objectType": "photo-album",
+      "url": "http://example.org/album/"
+    },
+    "generator": {
+      "url": "http://example.org/activities-app"
+    },
+    "provider": {
+      "url": "http://example.org/activity-stream"
+    },
+    "object": {
+      "upstreamDuplicates": ["upstream1", "upstream2"],
+      "summary": "Photo posted",
+      "id": "object2",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/my_fluffy_cat_thumb.jpg"
+      },
+      "attachments": [{
+        "id": "attachment1",
+        "objectType": "attachment"
+      }, {
+        "id": "attachment2",
+        "objectType": "attachment"
+      }],
+      "downstreamDuplicates": ["downstream1", "downstream2"],
+      "objectType": "photo",
+      "url": "http://example.org/album/my_fluffy_cat.jpg"
+    },
+    "actor": {
+      "id": "john.doe",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/john/image"
+      },
+      "displayName": "John Doe",
+      "objectType": "person",
+      "url": "http://example.org/john"
+    },
+    "published": "2011-03-10T15:04:55Z",
+    "openSocial": {
+      "embed": {
+        "context": {
+          "photoUrls": ["http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg", "http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg", "http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg", "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg", "http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg", "http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg", "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg"],
+          "albumName": "Germany 2009"
+        },
+        "gadget": "http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml"
+      }
+    }
+  }, {
+    "id": "activity3",
+    "title": "John posted a new photo to his blog",
+    "verb": "post",
+    "target": {
+      "id": "target3",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/thumbnail.jpg"
+      },
+      "displayName": "John's Blog About Life",
+      "objectType": "blog",
+      "url": "http://example.org/blog/"
+    },
+    "generator": {
+      "url": "http://example.org/activities-app"
+    },
+    "provider": {
+      "url": "http://example.org/activity-stream"
+    },
+    "object": {
+      "summary": "Photo about new world",
+      "id": "object3",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/album/new_world.jpg"
+      },
+      "objectType": "photo",
+      "url": "http://example.org/album/new_world.jpg"
+    },
+    "actor": {
+      "id": "john.doe",
+      "image": {
+        "height": 250,
+        "width": 250,
+        "url": "http://example.org/john/image"
+      },
+      "displayName": "John Doe",
+      "objectType": "person",
+      "url": "http://example.org/john"
+    },
+    "published": "2012-06-02T10:02:55Z",
+    "openSocial": {
+      "embed": {
+        "context": {
+          "photoUrl": "http://example.org/album/new_world.jpg"
+        },
+        "preferredExperience": {
+          "display": {
+            "type": "text",
+            "label": "Checkout new photo in John's blog"
+          },
+          "target": {
+            "type": "gadget",
+            "view": "embedded_canvas"
+          }
+        },
+        "gadget": "http://localhost:8080/containers/embeddedexperiences/BlogViewer.xml"
+      }
+    }
+  }]
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonId.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonId.json
new file mode 100644
index 0000000..b9f1d8a
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonId.json
@@ -0,0 +1,49 @@
+{
+   "entry":{
+		"id": "activity1",
+		"title": "John shared new photos with you",
+	    "published": "2011-02-10T15:04:55Z",
+	    "actor": {
+	      "url": "http://example.org/john",
+	      "objectType" : "person",
+	      "id": "john.doe",
+	      "image": {
+	        "url": "http://example.org/john/image",
+	        "width": 250,
+	        "height": 250
+	      },
+	      "displayName": "John Doe"
+	    },
+	    "verb": "post",
+	    "object" : {
+	      "url": "http://example.org/blog/2011/02/entry",
+	      "id": "object1"
+	    },
+	    "target" : {
+	      "url": "http://example.org/blog/",
+	      "objectType": "blog",
+	      "id": "target1",
+	      "displayName": "John's Blog"
+	    },
+          "openSocial":{
+          "embed" : {
+            "gadget" : "http://localhost:8080/containers/embeddedexperiences/PhotoList.xml",
+            "context" : {
+              "albumName": "Germany 2009",
+              "eeGadget" : "http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml",
+              "photoUrls": [
+                "http://farm4.static.flickr.com/3495/3925132517_5959dac775.jpg",
+     	        "http://farm4.static.flickr.com/3629/3394799776_47676abb46.jpg",
+     		    "http://farm5.static.flickr.com/4009/4413640211_715d924d9b.jpg",
+     		    "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba.jpg",
+     		    "http://farm1.static.flickr.com/36/98407782_9c4c5866d1.jpg",
+     		    "http://farm1.static.flickr.com/48/180544479_bb0d0f6559.jpg",
+     		    "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7.jpg"
+     		  ]
+     	    }
+          }
+        }
+
+
+	  }
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonIds.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonIds.json
new file mode 100644
index 0000000..6630ab3
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonIds.json
@@ -0,0 +1,122 @@
+{
+   "itemsPerPage": 2,
+   "sorted": true,
+   "startIndex": 0,
+   "totalResults": 2,
+   "filtered": true,
+   "updatedSince": true,
+   "list":[
+      {
+        "id": "activity1",
+        "title": "John shared new photos with you",
+          "published": "2011-02-10T15:04:55Z",
+          "actor": {
+            "url": "http://example.org/john",
+            "objectType" : "person",
+            "id": "john.doe",
+            "image": {
+              "url": "http://example.org/john/image",
+              "width": 250,
+              "height": 250
+            },
+            "displayName": "John Doe"
+          },
+          "verb": "post",
+          "object" : {
+            "url": "http://example.org/blog/2011/02/entry",
+            "id": "object1"
+          },
+          "target" : {
+            "url": "http://example.org/blog/",
+            "objectType": "blog",
+            "id": "target1",
+            "displayName": "John's Blog"
+          },
+          "openSocial":{
+          "embed" : {
+            "gadget" : "http://localhost:8080/containers/embeddedexperiences/PhotoList.xml",
+            "context" : {
+              "albumName": "Germany 2009",
+              "eeGadget" : "http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml",
+              "photoUrls": [
+                "http://farm4.static.flickr.com/3495/3925132517_5959dac775.jpg",
+     	        "http://farm4.static.flickr.com/3629/3394799776_47676abb46.jpg",
+     		    "http://farm5.static.flickr.com/4009/4413640211_715d924d9b.jpg",
+     		    "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba.jpg",
+     		    "http://farm1.static.flickr.com/36/98407782_9c4c5866d1.jpg",
+     		    "http://farm1.static.flickr.com/48/180544479_bb0d0f6559.jpg",
+     		    "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7.jpg"
+     		  ]
+     	    }
+          }
+        }
+      }, {
+	  	  "id": "activity2",
+        "published": "2011-03-10T15:04:55Z",
+        "generator": {
+          "url": "http://example.org/activities-app"
+        },
+        "provider": {
+          "url": "http://example.org/activity-stream"
+        },
+        "title": "John posted a new photo album.",
+        "actor": {
+          "url": "http://example.org/john",
+          "objectType": "person",
+          "id": "john.doe",
+          "image": {
+            "url": "http://example.org/john/image",
+            "width": 250,
+            "height": 250
+          },
+          "displayName": "John Doe"
+        },
+        "verb": "post",
+        "object" : {
+          "url": "http://example.org/album/my_fluffy_cat.jpg",
+          "objectType": "photo",
+          "id": "object2",
+          "summary": "Photo posted",
+          "image": {
+            "url": "http://example.org/album/my_fluffy_cat_thumb.jpg",
+            "width": 250,
+            "height": 250
+          },
+          "upstreamDuplicates" : ["upstream1", "upstream2"],
+          "downstreamDuplicates" : ["downstream1", "downstream2"],
+          "attachments": [
+          	{"id": "attachment1", "objectType": "attachment"},
+          	{"id": "attachment2", "objectType": "attachment"}
+          ]
+        },
+        "target": {
+          "url": "http://example.org/album/",
+          "objectType": "photo-album",
+          "id": "target2",
+          "displayName": "John's Photo Album",
+          "image": {
+            "url": "http://example.org/album/thumbnail.jpg",
+            "width": 250,
+            "height": 250
+          }
+        },
+        "openSocial":{
+          "embed" : {
+            "gadget" : "http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml",
+            "context" : {
+              "albumName": "Germany 2009",
+              "photoUrls": [
+                "http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg",
+     	        "http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg",
+     		    "http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg",
+     		    "http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg",
+     		    "http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg",
+     		    "http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg",
+     		    "http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg"
+     		  ]
+     	    }
+          }
+        }
+      }
+   ]
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonUpdated.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonUpdated.json
new file mode 100644
index 0000000..e44e05f
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryJsonUpdated.json
@@ -0,0 +1,8 @@
+{
+   "entry": {
+  	 "id": "activity2",
+  	 "title" : "Super Updated Activity",
+  	 "actor": {"id": "john.doe"},
+  	 "object" : {"id": "object2"}
+   }
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryXmlId.xml b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryXmlId.xml
new file mode 100644
index 0000000..4658eee
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryXmlId.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?><response xmlns="http://ns.opensocial.org/2008/opensocial"><activityEntry xmlns="http://ns.opensocial.org/2008/opensocial">
+  <actor>
+    <displayName>John Doe</displayName>
+    <id>john.doe</id>
+    <image>
+      <height>250</height>
+      <url>http://example.org/john/image</url>
+
+      <width>250</width>
+    </image>
+    <objectType>person</objectType>
+    <url>http://example.org/john</url>
+  </actor>
+  <generator>
+    <url>http://example.org/activities-app</url>
+
+  </generator>
+  <id>activity2</id>
+  <object>
+    <attachments>
+      <object>
+        <id>attachment1</id>
+        <objectType>attachment</objectType>
+
+      </object>
+      <object>
+        <id>attachment2</id>
+        <objectType>attachment</objectType>
+      </object>
+    </attachments>
+    <downstreamDuplicate>downstream1</downstreamDuplicate>
+
+    <downstreamDuplicate>downstream2</downstreamDuplicate>
+    <id>object2</id>
+    <summary>Photo posted</summary>
+    <image>
+      <height>250</height>
+      <url>http://example.org/album/my_fluffy_cat_thumb.jpg</url>
+      <width>250</width>
+
+    </image>
+    <objectType>photo</objectType>
+    <upstreamDuplicate>upstream1</upstreamDuplicate>
+    <upstreamDuplicate>upstream2</upstreamDuplicate>
+    <url>http://example.org/album/my_fluffy_cat.jpg</url>
+  </object>
+  <openSocial>
+    <embed>
+      <context>
+           <albumName>Germany 2009</albumName>
+                <photoUrls>
+                  <java.lang.String>http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg</java.lang.String>
+                  <java.lang.String>http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg</java.lang.String>
+                </photoUrls>
+      </context>
+      <gadget>http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml</gadget>
+    </embed>
+  </openSocial>
+  <provider>
+
+    <url>http://example.org/activity-stream</url>
+  </provider>
+  <published>2011-03-10T15:04:55Z</published>
+  <target>
+    <displayName>John&apos;s Photo Album</displayName>
+    <id>target2</id>
+
+    <image>
+      <height>250</height>
+      <url>http://example.org/album/thumbnail.jpg</url>
+      <width>250</width>
+    </image>
+    <objectType>photo-album</objectType>
+    <url>http://example.org/album/</url>
+
+  </target>
+  <title>John posted a new photo album.</title>
+  <verb>post</verb>
+</activityEntry></response>
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryXmlIds.xml b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryXmlIds.xml
new file mode 100644
index 0000000..211018d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityEntryXmlIds.xml
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?><response xmlns="http://ns.opensocial.org/2008/opensocial">
+  <itemsPerPage>2</itemsPerPage>
+  <startIndex>0</startIndex>
+  <totalResults>2</totalResults>
+  <filtered>true</filtered>
+  <sorted>true</sorted>
+  <updatedSince>true</updatedSince>
+  <list>
+      <entry>
+        <activityEntry>
+          <actor>
+            <displayName>John Doe</displayName>
+            <id>john.doe</id>
+            <image>
+              <height>250</height>
+
+              <url>http://example.org/john/image</url>
+              <width>250</width>
+            </image>
+            <objectType>person</objectType>
+            <url>http://example.org/john</url>
+          </actor>
+          <id>activity1</id>
+          <title>John shared new photos with you</title>
+          <object>
+            <id>object1</id>
+            <url>http://example.org/blog/2011/02/entry</url>
+          </object>
+          <published>2011-02-10T15:04:55Z</published>
+          <target>
+            <displayName>John&apos;s Blog</displayName>
+
+            <id>target1</id>
+            <objectType>blog</objectType>
+            <url>http://example.org/blog/</url>
+          </target>
+          <verb>post</verb>
+          <openSocial>
+            <embed>
+              <context>
+                   <albumName>Germany 2009</albumName>
+                   <eeGadget>http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml</eeGadget>
+                   <photoUrls>
+                      <java.lang.String>http://farm4.static.flickr.com/3495/3925132517_5959dac775.jpg</java.lang.String>
+                      <java.lang.String>http://farm4.static.flickr.com/3629/3394799776_47676abb46.jpg</java.lang.String>
+                      <java.lang.String>http://farm5.static.flickr.com/4009/4413640211_715d924d9b.jpg</java.lang.String>
+                      <java.lang.String>http://farm3.static.flickr.com/2340/3528537244_d2fb037aba.jpg</java.lang.String>
+                      <java.lang.String>http://farm1.static.flickr.com/36/98407782_9c4c5866d1.jpg</java.lang.String>
+                      <java.lang.String>http://farm1.static.flickr.com/48/180544479_bb0d0f6559.jpg</java.lang.String>
+                      <java.lang.String>http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7.jpg</java.lang.String>
+                    </photoUrls>
+              </context>
+              <gadget>http://localhost:8080/containers/embeddedexperiences/PhotoList.xml</gadget>
+            </embed>
+          </openSocial>
+        </activityEntry>
+      </entry>
+
+      <entry>
+        <activityEntry>
+          <actor>
+            <displayName>John Doe</displayName>
+            <id>john.doe</id>
+            <image>
+              <height>250</height>
+
+              <url>http://example.org/john/image</url>
+              <width>250</width>
+            </image>
+            <objectType>person</objectType>
+            <url>http://example.org/john</url>
+          </actor>
+          <generator>
+
+            <url>http://example.org/activities-app</url>
+          </generator>
+          <id>activity2</id>
+          <object>
+            <attachments>
+              <object>
+                <id>attachment1</id>
+
+                <objectType>attachment</objectType>
+              </object>
+              <object>
+                <id>attachment2</id>
+                <objectType>attachment</objectType>
+              </object>
+            </attachments>
+
+            <downstreamDuplicate>downstream1</downstreamDuplicate>
+            <downstreamDuplicate>downstream2</downstreamDuplicate>
+            <id>object2</id>
+            <summary>Photo posted</summary>
+            <image>
+              <height>250</height>
+              <url>http://example.org/album/my_fluffy_cat_thumb.jpg</url>
+
+              <width>250</width>
+            </image>
+            <objectType>photo</objectType>
+            <upstreamDuplicate>upstream1</upstreamDuplicate>
+            <upstreamDuplicate>upstream2</upstreamDuplicate>
+            <url>http://example.org/album/my_fluffy_cat.jpg</url>
+
+          </object>
+          <provider>
+            <url>http://example.org/activity-stream</url>
+          </provider>
+          <published>2011-03-10T15:04:55Z</published>
+          <target>
+            <displayName>John&apos;s Photo Album</displayName>
+
+            <id>target2</id>
+            <image>
+              <height>250</height>
+              <url>http://example.org/album/thumbnail.jpg</url>
+              <width>250</width>
+            </image>
+            <objectType>photo-album</objectType>
+
+            <url>http://example.org/album/</url>
+          </target>
+          <title>John posted a new photo album.</title>
+          <verb>post</verb>
+          <openSocial>
+            <embed>
+              <context>
+                   <albumName>Germany 2009</albumName>
+                    <photoUrls>
+                      <java.lang.String>http://farm4.static.flickr.com/3495/3925132517_5959dac775_t.jpg</java.lang.String>
+                      <java.lang.String>http://farm4.static.flickr.com/3629/3394799776_47676abb46_t.jpg</java.lang.String>
+                      <java.lang.String>http://farm5.static.flickr.com/4009/4413640211_715d924d9b_t.jpg</java.lang.String>
+                      <java.lang.String>http://farm3.static.flickr.com/2340/3528537244_d2fb037aba_t.jpg</java.lang.String>
+                      <java.lang.String>http://farm1.static.flickr.com/36/98407782_9c4c5866d1_t.jpg</java.lang.String>
+                      <java.lang.String>http://farm1.static.flickr.com/48/180544479_bb0d0f6559_t.jpg</java.lang.String>
+                      <java.lang.String>http://farm3.static.flickr.com/2668/3858018351_1e7b73c0b7_t.jpg</java.lang.String>
+                    </photoUrls>
+              </context>
+              <gadget>http://localhost:8080/containers/embeddedexperiences/AlbumViewer.xml</gadget>
+            </embed>
+          </openSocial>
+        </activityEntry>
+      </entry>
+  </list>
+</response>
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityStreamsSupportedFields.json b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityStreamsSupportedFields.json
new file mode 100644
index 0000000..a59e109
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/dataservice/integration/fixtures/ActivityStreamsSupportedFields.json
@@ -0,0 +1,19 @@
+{
+   "entry":[
+     "actor",
+     "content",
+     "generator",
+     "icon",
+     "id",
+     "object",
+     "published",
+     "provider",
+     "target",
+     "title",
+     "updated",
+     "url",
+     "verb",
+     "openSocial",
+     "extensions"
+   ]
+}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/model/PersonTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/model/PersonTest.java
new file mode 100644
index 0000000..d27644b
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/model/PersonTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.model;
+
+import org.apache.shindig.social.core.model.PersonImpl;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class PersonTest extends Assert {
+  @Test
+  public void testInvalidFromUrlString() throws Exception {
+    assertNull(Person.Field.fromUrlString("badness"));
+  }
+
+  @Test
+  public void testFromUrlString() throws Exception {
+    assertUrlStringMaps(Person.Field.NAME);
+    assertUrlStringMaps(Person.Field.THUMBNAIL_URL);
+  }
+
+  private void assertUrlStringMaps(Person.Field field) {
+    assertEquals(field, Person.Field.fromUrlString(field.toString()));
+  }
+
+  @Test
+  public void testGetProfileUrl() throws Exception {
+    Person person = new PersonImpl();
+    assertNull(person.getProfileUrl());
+
+    String address = "hi";
+    person.setProfileUrl(address);
+    assertEquals(address, person.getProfileUrl());
+
+    assertEquals(address, person.getUrls().get(0).getValue());
+    assertEquals(Person.PROFILE_URL_TYPE, person.getUrls().get(0).getType());
+    assertNull(person.getUrls().get(0).getLinkText());
+
+    address = "something new";
+    person.setProfileUrl(address);
+    assertEquals(address, person.getProfileUrl());
+
+    assertEquals(1, person.getUrls().size());
+    assertEquals(address, person.getUrls().get(0).getValue());
+  }
+
+  @Test
+  public void testGetThumbnailUrl() throws Exception {
+    Person person = new PersonImpl();
+    assertNull(person.getThumbnailUrl());
+
+    String url = "hi";
+    person.setThumbnailUrl(url);
+    assertEquals(url, person.getThumbnailUrl());
+
+    assertEquals(url, person.getPhotos().get(0).getValue());
+    assertEquals(Person.THUMBNAIL_PHOTO_TYPE, person.getPhotos().get(0).getType());
+
+    url = "something new";
+    person.setThumbnailUrl(url);
+    assertEquals(url, person.getThumbnailUrl());
+
+    assertEquals(1, person.getPhotos().size());
+    assertEquals(url, person.getPhotos().get(0).getValue());
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ActivityHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ActivityHandlerTest.java
new file mode 100644
index 0000000..8cdc41c
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ActivityHandlerTest.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.core.model.ActivityImpl;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.spi.ActivityService;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.isNull;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+public class ActivityHandlerTest extends EasyMockTestCase {
+
+  private BeanJsonConverter converter;
+
+  private ActivityService activityService;
+
+  private ActivityHandler handler;
+
+  private FakeGadgetToken token;
+
+  private static final Set<UserId> JOHN_DOE =
+      ImmutableSet.of(new UserId(UserId.Type.userId, "john.doe"));
+
+  protected HandlerRegistry registry;
+  protected ContainerConfig containerConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    token.setAppId("appId");
+
+    converter = mock(BeanJsonConverter.class);
+    activityService = mock(ActivityService.class);
+
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+         "'gadgets.features':{opensocial:" +
+           "{supportedFields: {activity: ['id', 'title']}}" +
+         "}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    handler = new ActivityHandler(activityService, containerConfig);
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+  }
+
+  private void assertHandleGetForGroup(GroupId.Type group) throws Exception {
+    String path = "/activities/john.doe/@" + group.toString();
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    List<Activity> activityList = ImmutableList.of();
+    RestfulCollection<Activity> data = new RestfulCollection<Activity>(activityList);
+    org.easymock.EasyMock.expect(activityService.getActivities(eq(JOHN_DOE),
+       eq(new GroupId(group, null)), (String)isNull(), eq(ImmutableSet.<String>of()),
+        org.easymock.EasyMock.isA(CollectionOptions.class), eq(token))).
+        andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleGetAll() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.all);
+  }
+
+  @Test
+  public void testHandleGetFriends() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.friends);
+  }
+
+  @Test
+  public void testHandleGetSelf() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.self);
+  }
+
+  @Test
+  public void testHandleGetPlural() throws Exception {
+    String path = "/activities/john.doe,jane.doe/@self/@app";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    List<Activity> activities = ImmutableList.of();
+    RestfulCollection<Activity> data = new RestfulCollection<Activity>(activities);
+    Set<UserId> userIdSet = Sets.newLinkedHashSet(JOHN_DOE);
+    userIdSet.add(new UserId(UserId.Type.userId, "jane.doe"));
+    org.easymock.EasyMock.expect(activityService.getActivities(eq(userIdSet),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"),eq(ImmutableSet.<String>of()),
+        org.easymock.EasyMock.isA((CollectionOptions.class)), eq(token))).andReturn(
+          Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleGetActivityById() throws Exception {
+    String path = "/activities/john.doe/@friends/@app/1";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    Activity activity = new ActivityImpl();
+    org.easymock.EasyMock.expect(activityService.getActivity(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.friends, null)),
+        eq("appId"), eq(ImmutableSet.<String>of()), eq("1"), eq(token))).andReturn(
+        Futures.immediateFuture(activity));
+
+    replay();
+    assertEquals(activity, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+    reset();
+  }
+
+  private Future<?> setupBodyRequest(String method) throws ProtocolException {
+    String jsonActivity = "{title: hi mom!, etc etc}";
+
+    String path = "/activities/john.doe/@self/@app";
+    RestHandler operation = registry.getRestHandler(path, method);
+
+    Activity activity = new ActivityImpl();
+    org.easymock.EasyMock.expect(converter.convertToObject(eq(jsonActivity), eq(Activity.class)))
+        .andReturn(activity);
+
+    org.easymock.EasyMock.expect(activityService.createActivity(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"), eq(ImmutableSet.<String>of()),
+        eq(activity), eq(token))).andReturn(Futures.immediateFuture((Void) null));
+    replay();
+
+    return operation.execute(Maps.<String, String[]>newHashMap(),
+        new StringReader(jsonActivity), token, converter);
+  }
+
+  @Test
+  public void testHandlePost() throws Exception {
+    Future<?> future = setupBodyRequest("POST");
+    assertNull(future.get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandlePut() throws Exception {
+    Future<?> future = setupBodyRequest("PUT");
+    assertNull(future.get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleDelete() throws Exception {
+    String path = "/activities/john.doe/@self/@app/1";
+    RestHandler operation = registry.getRestHandler(path, "DELETE");
+
+
+    org.easymock.EasyMock.expect(activityService.deleteActivities(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"), eq(ImmutableSet.of("1")),
+        eq(token))).andReturn(Futures.immediateFuture((Void) null));
+
+    replay();
+    assertNull(operation.execute(Maps.<String, String[]>newHashMap(), null,
+        token, converter).get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleGetSuportedFields() throws Exception {
+    String path = "/activities/@supportedFields";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    replay();
+    @SuppressWarnings("unchecked")
+    List<Object> received = (List<Object>) operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get();
+    assertEquals(2, received.size());
+    assertEquals("id", received.get(0).toString());
+    assertEquals("title", received.get(1).toString());
+
+    verify();
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ActivityStreamHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ActivityStreamHandlerTest.java
new file mode 100644
index 0000000..406d231
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ActivityStreamHandlerTest.java
@@ -0,0 +1,267 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.isNull;
+
+import java.io.StringReader;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Future;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.core.model.ActivityEntryImpl;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.spi.ActivityStreamService;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.UserId;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+
+/**
+ * Tests the ActivityStreamsHandler.
+ */
+public class ActivityStreamHandlerTest extends EasyMockTestCase {
+
+  private BeanJsonConverter converter;
+
+  private ActivityStreamService service;
+
+  private ActivityStreamHandler handler;
+
+  private FakeGadgetToken token;
+
+  private static final Set<UserId> JOHN_DOE = ImmutableSet.of(new UserId(
+      UserId.Type.userId, "john.doe"));
+
+  protected HandlerRegistry registry;
+  protected ContainerConfig containerConfig;
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    token.setAppId("appId");
+
+    converter = mock(BeanJsonConverter.class);
+    service = mock(ActivityStreamService.class);
+
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+         "'gadgets.features':{opensocial:" +
+           "{supportedFields: {activityEntry: ['id', 'title']}}" +
+         "}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    handler = new ActivityStreamHandler(service, containerConfig);
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+  }
+
+  /* Helper for retrieving groups. */
+  private void assertHandleGetForGroup(GroupId.Type group) throws Exception {
+    String path = "/activitystreams/john.doe/@" + group.toString();
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    List<ActivityEntry> entries = ImmutableList.of();
+    RestfulCollection<ActivityEntry> data = new RestfulCollection<ActivityEntry>(entries);
+    org.easymock.EasyMock.expect(service.getActivityEntries(eq(JOHN_DOE),
+       eq(new GroupId(group, null)), (String)isNull(), eq(ImmutableSet.<String>of()),
+        org.easymock.EasyMock.isA(CollectionOptions.class), eq(token))).
+        andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleGetAll() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.all);
+  }
+
+  @Test
+  public void testHandleGetFriends() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.friends);
+  }
+
+  @Test
+  public void testHandleGetSelf() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.self);
+  }
+
+  @Test
+  public void testHandleGetPlural() throws Exception {
+    String path = "/activitystreams/john.doe,jane.doe/@self/@app";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    List<ActivityEntry> entries = ImmutableList.of();
+    RestfulCollection<ActivityEntry> data = new RestfulCollection<ActivityEntry>(entries);
+    Set<UserId> userIdSet = Sets.newLinkedHashSet(JOHN_DOE);
+    userIdSet.add(new UserId(UserId.Type.userId, "jane.doe"));
+    org.easymock.EasyMock.expect(service.getActivityEntries(eq(userIdSet),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"),eq(ImmutableSet.<String>of()),
+        org.easymock.EasyMock.isA((CollectionOptions.class)), eq(token))).andReturn(
+          Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleGetActivityEntryById() throws Exception {
+    String path = "/activitystreams/john.doe/@friends/@app/myObjectId123";  // TODO: change id=1 in DB for consistency
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    ActivityEntry entry = new ActivityEntryImpl();
+    org.easymock.EasyMock.expect(service.getActivityEntry(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.friends, null)),
+        eq("appId"), eq(ImmutableSet.<String>of()), eq("myObjectId123"), eq(token))).andReturn(
+        Futures.immediateFuture(entry));
+
+    replay();
+    assertEquals(entry, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+    reset();
+  }
+
+  /* Helper for testing PUT and POST */
+  private Future<?> setupBodyRequest(String method) throws ProtocolException {
+    String jsonActivityEntry = "{title: 'hi mom!', object: {id: 'testObject'}}";
+
+    String path = "/activitystreams/john.doe/@self/@app";
+    RestHandler operation = registry.getRestHandler(path, method);
+
+    ActivityEntry entry = new ActivityEntryImpl();
+    org.easymock.EasyMock.expect(converter.convertToObject(eq(jsonActivityEntry), eq(ActivityEntry.class)))
+        .andReturn(entry);
+
+    org.easymock.EasyMock.expect(service.createActivityEntry(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"), eq(ImmutableSet.<String>of()),
+        eq(entry), eq(token))).andReturn(Futures.immediateFuture((ActivityEntry) null));
+    replay();
+
+    return operation.execute(Maps.<String, String[]>newHashMap(),
+        new StringReader(jsonActivityEntry), token, converter);
+  }
+
+  @Test
+  public void testHandlePost() throws Exception {
+    String jsonActivityEntry = "{title: 'hi mom!', object: {id: 'testObject'}}";
+
+    String path = "/activitystreams/john.doe/@self/@app";
+    RestHandler operation = registry.getRestHandler(path, "POST");
+
+    ActivityEntry entry = new ActivityEntryImpl();
+    org.easymock.EasyMock.expect(converter.convertToObject(eq(jsonActivityEntry), eq(ActivityEntry.class)))
+        .andReturn(entry);
+
+    org.easymock.EasyMock.expect(service.createActivityEntry(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"), eq(ImmutableSet.<String>of()),
+        eq(entry), eq(token))).andReturn(Futures.immediateFuture((ActivityEntry) null));
+    replay();
+
+    Future<?> future = operation.execute(Maps.<String, String[]>newHashMap(),
+        new StringReader(jsonActivityEntry), token, converter);
+    assertNull(future.get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandlePut() throws Exception {
+    String jsonActivityEntry = "{title: 'hi mom!', object: {id: 'testObject'}}";
+
+    String path = "/activitystreams/john.doe/@self/@app/testObject";
+    RestHandler operation = registry.getRestHandler(path, "PUT");
+
+    ActivityEntry entry = new ActivityEntryImpl();
+    org.easymock.EasyMock.expect(converter.convertToObject(eq(jsonActivityEntry), eq(ActivityEntry.class)))
+        .andReturn(entry);
+
+    org.easymock.EasyMock.expect(service.updateActivityEntry(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"), eq(ImmutableSet.<String>of()),
+        eq(entry), eq("testObject"), eq(token))).andReturn(Futures.immediateFuture((ActivityEntry) null));
+    replay();
+
+    Future<?> future = operation.execute(Maps.<String, String[]>newHashMap(),
+        new StringReader(jsonActivityEntry), token, converter);
+    assertNull(future.get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleDelete() throws Exception {
+    String path = "/activitystreams/john.doe/@self/@app/myObjectId123";
+    RestHandler operation = registry.getRestHandler(path, "DELETE");
+
+    org.easymock.EasyMock.expect(service.deleteActivityEntries(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)), eq("appId"), eq(ImmutableSet.of("myObjectId123")),
+        eq(token))).andReturn(Futures.immediateFuture((Void) null));
+
+    replay();
+    assertNull(operation.execute(Maps.<String, String[]>newHashMap(), null,
+        token, converter).get());
+    verify();
+    reset();
+  }
+
+  @Test
+  public void testHandleGetSupportedFields() throws Exception {
+    String path = "/activitystreams/@supportedFields";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    replay();
+    @SuppressWarnings("unchecked")
+    List<Object> received = (List<Object>) operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get();
+    assertEquals(2, received.size());
+    assertEquals("id", received.get(0).toString());
+    assertEquals("title", received.get(1).toString());
+
+    verify();
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/AlbumHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/AlbumHandlerTest.java
new file mode 100644
index 0000000..39fbc5f
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/AlbumHandlerTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.opensocial.spi.AlbumService;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class AlbumHandlerTest extends EasyMockTestCase {
+  private AlbumService albumService;
+  private AlbumHandler handler;
+  private ContainerConfig containerConfig;
+  private FakeGadgetToken token;
+  protected HandlerRegistry registry;
+  private BeanJsonConverter converter;
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    converter = mock(BeanJsonConverter.class);
+    albumService = mock(AlbumService.class);
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+         "'gadgets.features':{opensocial:" +
+           "{supportedFields: {album: ['id', 'title', 'location']}}" +
+         "}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    handler = new AlbumHandler(albumService, containerConfig);
+
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+  }
+
+  @Test
+  public void testCreate() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testGet() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testUpdate() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testDelete() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testSupportedFields() throws Exception {
+    String path = "/albums/@supportedFields";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    replay();
+    @SuppressWarnings("unchecked")
+    List<Object> received = (List<Object>) operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get();
+    assertEquals(3, received.size());
+    assertEquals("id", received.get(0).toString());
+    assertEquals("title", received.get(1).toString());
+    assertEquals("location", received.get(2).toString());
+
+    verify();
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/AppDataHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/AppDataHandlerTest.java
new file mode 100644
index 0000000..805611a
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/AppDataHandlerTest.java
@@ -0,0 +1,261 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import static org.easymock.EasyMock.eq;
+
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.protocol.DataCollection;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.opensocial.spi.AppDataService;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.UserId;
+import org.easymock.EasyMock;
+
+import java.io.StringReader;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import org.junit.Before;
+import org.junit.Test;
+
+public class AppDataHandlerTest extends EasyMockTestCase {
+
+  private BeanJsonConverter converter;
+
+  private AppDataService appDataService;
+
+  private FakeGadgetToken token;
+
+  protected HandlerRegistry registry;
+
+
+  private static final Set<UserId> JOHN_DOE = Collections.unmodifiableSet(
+      ImmutableSet.of(new UserId(UserId.Type.userId, "john.doe")));
+
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    converter = mock(BeanJsonConverter.class);
+    appDataService = mock(AppDataService.class);
+    AppDataHandler handler = new AppDataHandler(appDataService);
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+  }
+
+  private void assertHandleGetForGroup(GroupId.Type group) throws Exception {
+    String path = "/appdata/john.doe/@" + group.toString() + "/appId";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    DataCollection data = new DataCollection(null);
+    org.easymock.EasyMock.expect(appDataService.getPersonData(eq(JOHN_DOE),
+        eq(new GroupId(group, null)),
+        eq("appId"), eq(ImmutableSet.<String>of()), eq(token)))
+        .andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetAll() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.all);
+  }
+
+  @Test
+  public void testHandleGetFriends() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.friends);
+  }
+
+  @Test
+  public void testHandleGetSelf() throws Exception {
+    assertHandleGetForGroup(GroupId.Type.self);
+  }
+
+  @Test
+  public void testHandleGetPlural() throws Exception {
+    String path = "/appdata/john.doe,jane.doe/@self/appId";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    DataCollection data = new DataCollection(null);
+    Set<UserId> userIdSet = Sets.newLinkedHashSet(JOHN_DOE);
+    userIdSet.add(new UserId(UserId.Type.userId, "jane.doe"));
+    org.easymock.EasyMock.expect(appDataService.getPersonData(eq(userIdSet),
+        eq(new GroupId(GroupId.Type.self, null)),
+        eq("appId"), eq(ImmutableSet.<String>of()), eq(token)))
+        .andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetWithoutFields() throws Exception {
+    String path = "/appdata/john.doe/@friends/appId";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    Map<String, String[]> params = Maps.newHashMap();
+    params.put("fields", new String[]{"pandas"});
+
+    DataCollection data = new DataCollection(null);
+    org.easymock.EasyMock.expect(appDataService.getPersonData(eq(JOHN_DOE),
+        eq(new GroupId(GroupId.Type.friends, null)),
+        eq("appId"), eq(ImmutableSet.of("pandas")), eq(token)))
+        .andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(params, null, token, converter).get());
+    verify();
+  }
+
+  private Future<?> setupPostData(String method) throws ProtocolException {
+    String path = "/appdata/john.doe/@self/appId";
+    RestHandler operation = registry.getRestHandler(path, method);
+
+    String jsonAppData = "{pandas: 'are fuzzy'}";
+
+    Map<String, String[]> params = Maps.newHashMap();
+    params.put("fields", new String[]{"pandas"});
+
+    HashMap<String, Object> values = Maps.newHashMap();
+    org.easymock.EasyMock.expect(converter.convertToObject(eq(jsonAppData), eq(Map.class)))
+        .andReturn(values);
+
+    org.easymock.EasyMock.expect(appDataService.updatePersonData(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)),
+        eq("appId"), eq(ImmutableSet.of("pandas")), eq(values), eq(token)))
+        .andReturn(Futures.immediateFuture((Void) null));
+    replay();
+    return operation.execute(params, new StringReader(jsonAppData), token, converter);
+  }
+
+  @Test
+  public void testHandlePost() throws Exception {
+    assertNull(setupPostData("POST").get());
+    verify();
+  }
+
+  @Test
+  public void testHandlePut() throws Exception {
+    assertNull(setupPostData("PUT").get());
+    verify();
+  }
+
+  /**
+   * Test that the handler correctly recognizes null keys in the data.
+   * @throws Exception if the test fails
+   */
+  @Test
+  public void testHandleNullPostDataKeys() throws Exception {
+    String path = "/appdata/john.doe/@self/appId";
+    RestHandler operation = registry.getRestHandler(path, "POST");
+    String jsonAppData = "{pandas: 'are fuzzy'}";
+
+    Map<String, String[]> params = Maps.newHashMap();
+    params.put("fields", new String[]{"pandas"});
+
+    HashMap<String, String> values = Maps.newHashMap();
+    // create an invalid set of app data and inject
+    values.put("Aokkey", "an ok key");
+    values.put("", "an empty value");
+    org.easymock.EasyMock.expect(converter.convertToObject(eq(jsonAppData), eq(Map.class)))
+        .andReturn(values);
+
+    replay();
+    try {
+      operation.execute(params, new StringReader(jsonAppData), token, converter).get();
+      fail();
+    } catch (ExecutionException ee) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST,
+          ((ProtocolException) ee.getCause()).getCode());
+      // was expecting an Exception
+    }
+    verify();
+  }
+  /**
+   * Test that the handler correctly recognizes invalid keys in the data.
+   * @throws Exception if the test fails
+   */
+  @Test
+  public void testHandleInvalidPostDataKeys() throws Exception {
+    String path = "/appdata/john.doe/@self/appId";
+    RestHandler operation = registry.getRestHandler(path, "POST");
+    String jsonAppData = "{pandas: 'are fuzzy'}";
+
+    Map<String, String[]> params = Maps.newHashMap();
+    params.put("fields", new String[]{"pandas"});
+
+    HashMap<String, String> values = Maps.newHashMap();
+    // create an invalid set of app data and inject
+    values.put("Aokkey", "an ok key");
+    values.put("a bad key", "a good value");
+    org.easymock.EasyMock.expect(converter.convertToObject(eq(jsonAppData), eq(Map.class)))
+        .andReturn(values);
+
+    replay();
+    try {
+      operation.execute(params, new StringReader(jsonAppData), token, converter).get();
+      fail();
+    } catch (ExecutionException ee) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST,
+          ((ProtocolException) ee.getCause()).getCode());
+    }
+    verify();
+  }
+
+
+  @Test
+  public void testHandleDelete() throws Exception {
+    Map<String, String[]> params = Maps.newHashMap();
+    params.put("fields", new String[]{"pandas"});
+    String path = "/appdata/john.doe/@self/appId";
+    RestHandler operation = registry.getRestHandler(path, "DELETE");
+
+    EasyMock.expect(appDataService.deletePersonData(eq(JOHN_DOE.iterator().next()),
+        eq(new GroupId(GroupId.Type.self, null)),
+        eq("appId"), eq(ImmutableSet.of("pandas")), eq(token)))
+        .andReturn(Futures.immediateFuture((Void) null));
+
+    replay();
+    assertNull(operation.execute(params, null, token, converter).get());
+    verify();
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/MediaItemHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/MediaItemHandlerTest.java
new file mode 100644
index 0000000..4fab47a
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/MediaItemHandlerTest.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.opensocial.spi.MediaItemService;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+public class MediaItemHandlerTest extends EasyMockTestCase {
+  private MediaItemService mediaService;
+  private MediaItemHandler handler;
+  private ContainerConfig containerConfig;
+  private FakeGadgetToken token;
+  protected HandlerRegistry registry;
+  private BeanJsonConverter converter;
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    converter = mock(BeanJsonConverter.class);
+    mediaService = mock(MediaItemService.class);
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+         "'gadgets.features':{opensocial:" +
+           "{supportedFields: {mediaItem: ['id', 'language', 'title']}}" +
+         "}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    handler = new MediaItemHandler(mediaService, containerConfig);
+
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+  }
+
+  @Test
+  public void testCreate() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testGet() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testUpdate() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testDelete() throws Exception {
+    // TODO
+  }
+
+  @Test
+  public void testSupportedFields() throws Exception {
+    String path = "/mediaItems/@supportedFields";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    replay();
+    @SuppressWarnings("unchecked")
+    List<Object> received = (List<Object>) operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get();
+    assertEquals(3, received.size());
+    assertEquals("id", received.get(0).toString());
+    assertEquals("language", received.get(1).toString());
+    assertEquals("title", received.get(2).toString());
+
+    verify();
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/MessageHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/MessageHandlerTest.java
new file mode 100644
index 0000000..704f00c
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/MessageHandlerTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.RequestItem;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.core.model.MessageImpl;
+import org.apache.shindig.social.opensocial.model.Message;
+import org.apache.shindig.social.opensocial.spi.MessageService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+import org.easymock.EasyMock;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.Futures;
+
+public class MessageHandlerTest extends Assert {
+
+  private MessageService messageService;
+  private MessageHandler handler;
+  private BeanJsonConverter converter;
+  private FakeGadgetToken token;
+  private UserId sender;
+  private List<String> recipients;
+  protected HandlerRegistry registry;
+
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    messageService = EasyMock.createMock(MessageService.class);
+    messageService = EasyMock.createMock(MessageService.class);
+    converter = EasyMock.createMock(BeanJsonConverter.class);
+    sender = new UserId(UserId.Type.userId, "message.sender");
+    recipients = ImmutableList.of("second.recipient", "first.recipient");
+
+    handler = new MessageHandler(messageService);
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+  }
+
+  @Test
+  @Ignore
+  public void testPostMessage() throws Exception {
+    MessageImpl message = new MessageImpl("A message body", "A title", Message.Type.PRIVATE_MESSAGE);
+    message.setRecipients(recipients);
+
+    EasyMock.expect(converter.convertToObject(null, Message.class)).andReturn(message);
+    EasyMock.expect(messageService.createMessage(sender, "messageHandlerTest", "@outbox", message,
+        token)).andReturn(Futures.immediateFuture((Void) null));
+
+    EasyMock.replay(messageService, converter);
+
+    RestHandler operation = registry.getRestHandler("/messages/" + sender.getUserId() + "/@outbox", "POST");
+    Map<String,String[]> params = ImmutableMap.of(RequestItem.APP_ID, new String[]{"messageHandlerTest"});
+
+    operation.execute(params,null, token, converter).get();
+    EasyMock.verify(converter, messageService);
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/PersonHandlerTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/PersonHandlerTest.java
new file mode 100644
index 0000000..b8d9b67
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/PersonHandlerTest.java
@@ -0,0 +1,298 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.common.EasyMockTestCase;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.config.ContainerConfig;
+import org.apache.shindig.config.JsonContainerConfig;
+import org.apache.shindig.expressions.Expressions;
+import org.apache.shindig.protocol.DefaultHandlerRegistry;
+import org.apache.shindig.protocol.HandlerExecutionListener;
+import org.apache.shindig.protocol.HandlerRegistry;
+import org.apache.shindig.protocol.RestHandler;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.protocol.model.FilterOperation;
+import org.apache.shindig.protocol.model.SortOrder;
+import org.apache.shindig.social.core.model.PersonImpl;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.PersonService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import org.json.JSONObject;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.StringReader;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class PersonHandlerTest extends EasyMockTestCase {
+  private PersonService personService;
+  private PersonHandler handler;
+  private FakeGadgetToken token;
+  protected HandlerRegistry registry;
+  private BeanJsonConverter converter;
+
+  private static final Set<String> DEFAULT_FIELDS = ImmutableSet.of(Person.Field.ID.toString(),
+      Person.Field.NAME.toString(),
+      Person.Field.THUMBNAIL_URL.toString());
+
+  private static final Set<UserId> JOHN_DOE =
+      ImmutableSet.of(new UserId(UserId.Type.userId, "john.doe"));
+  private static final UserId ANONYMOUS = new UserId(UserId.Type.userId, AnonymousSecurityToken.ANONYMOUS_ID);
+
+  private static CollectionOptions DEFAULT_OPTIONS = new CollectionOptions();
+  protected ContainerConfig containerConfig;
+
+  static {
+    DEFAULT_OPTIONS.setSortBy(PersonService.TOP_FRIENDS_SORT);
+    DEFAULT_OPTIONS.setSortOrder(SortOrder.ascending);
+    DEFAULT_OPTIONS.setFilter(null);
+    DEFAULT_OPTIONS.setFilterOperation(FilterOperation.contains);
+    DEFAULT_OPTIONS.setFilterValue("");
+    DEFAULT_OPTIONS.setFirst(0);
+    DEFAULT_OPTIONS.setMax(20);
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    token = new FakeGadgetToken();
+    converter = mock(BeanJsonConverter.class);
+    personService = mock(PersonService.class);
+    JSONObject config = new JSONObject('{' + ContainerConfig.DEFAULT_CONTAINER + ':' +
+        "{'gadgets.container': ['default']," +
+         "'gadgets.features':{opensocial:" +
+           "{supportedFields: {person: ['id', {name: 'familyName'}]}}" +
+         "}}}");
+
+    containerConfig = new JsonContainerConfig(config, Expressions.forTesting());
+    handler = new PersonHandler(personService, containerConfig);
+    registry = new DefaultHandlerRegistry(null, converter,
+        new HandlerExecutionListener.NoOpHandler());
+    registry.addHandlers(ImmutableSet.<Object>of(handler));
+  }
+
+  @Test
+  public void testHandleGetAllNoParams() throws Exception {
+    String path = "/people/john.doe/@all";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    List<Person> personList = ImmutableList.of();
+    RestfulCollection<Person> data = new RestfulCollection<Person>(personList);
+
+    expect(personService.getPeople(
+        eq(JOHN_DOE),
+        eq(new GroupId(GroupId.Type.all, null)),
+        eq(DEFAULT_OPTIONS),
+        eq(DEFAULT_FIELDS),
+        eq(token)))
+        .andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(), null,
+        token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetFriendsNoParams() throws Exception {
+    String path = "/people/john.doe/@friends";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    List<Person> personList = ImmutableList.of();
+    RestfulCollection<Person> data = new RestfulCollection<Person>(personList);
+    expect(personService.getPeople(
+        eq(JOHN_DOE),
+        eq(new GroupId(GroupId.Type.friends, null)),
+        eq(DEFAULT_OPTIONS),
+        eq(DEFAULT_FIELDS),
+        eq(token)))
+        .andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetFriendsWithParams() throws Exception {
+    String path = "/people/john.doe/@friends";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    CollectionOptions options = new CollectionOptions();
+    options.setSortBy(Person.Field.NAME.toString());
+    options.setSortOrder(SortOrder.descending);
+    options.setFilter(PersonService.TOP_FRIENDS_FILTER);
+    options.setFilterOperation(FilterOperation.present);
+    options.setFilterValue("cassie");
+    options.setFirst(5);
+    options.setMax(10);
+
+    Map<String, String[]> params = Maps.newHashMap();
+    params.put("sortBy", new String[]{options.getSortBy()});
+    params.put("sortOrder", new String[]{options.getSortOrder().toString()});
+    params.put("filterBy", new String[]{options.getFilter()});
+    params.put("filterOp", new String[]{options.getFilterOperation().toString()});
+    params.put("filterValue", new String[]{options.getFilterValue()});
+    params.put("startIndex", new String[]{"5"});
+    params.put("count", new String[]{"10"});
+    params.put("fields", new String[]{"money,fame,fortune"});
+
+
+    List<Person> people = ImmutableList.of();
+    RestfulCollection<Person> data = new RestfulCollection<Person>(people);
+    expect(personService.getPeople(
+        eq(JOHN_DOE),
+        eq(new GroupId(GroupId.Type.friends, null)), eq(options),
+        eq(ImmutableSortedSet.of("money", "fame", "fortune")), eq(token)))
+        .andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(params, null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetFriendById() throws Exception {
+    String path = "/people/john.doe/@friends/jane.doe";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    Person person = new PersonImpl();
+    List<Person> people = Lists.newArrayList(person);
+    RestfulCollection<Person> data = new RestfulCollection<Person>(people);
+    // TODO: We aren't passing john.doe to the service yet.
+    expect(personService.getPeople(
+        eq(ImmutableSet.of(new UserId(UserId.Type.userId, "jane.doe"))),
+        eq(new GroupId(GroupId.Type.self, null)), eq(DEFAULT_OPTIONS),
+        eq(DEFAULT_FIELDS), eq(token)))
+        .andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(person, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetSelf() throws Exception {
+    String path = "/people/john.doe/@self";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    Person data = new PersonImpl();
+    expect(personService.getPerson(eq(JOHN_DOE.iterator().next()),
+        eq(DEFAULT_FIELDS), eq(token))).andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleAnonymousUser() throws Exception {
+    String path = "/people/-1";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    Person data = new PersonImpl();
+    expect(personService.getPerson(eq(ANONYMOUS),
+        eq(DEFAULT_FIELDS), eq(token))).andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetPlural() throws Exception {
+    String path = "/people/john.doe,jane.doe/@self";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    List<Person> people = ImmutableList.of();
+    RestfulCollection<Person> data = new RestfulCollection<Person>(people);
+    Set<UserId> userIdSet = Sets.newLinkedHashSet(JOHN_DOE);
+    userIdSet.add(new UserId(UserId.Type.userId, "jane.doe"));
+    expect(personService.getPeople(eq(userIdSet),
+        eq(new GroupId(GroupId.Type.self, null)),
+        eq(DEFAULT_OPTIONS),
+        eq(DEFAULT_FIELDS),
+        eq(token))).andReturn(Futures.immediateFuture(data));
+
+    replay();
+    assertEquals(data, operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandlePut() throws Exception {
+    String jsonPerson = "{person: {aboutMe: 'A person'}}";
+
+    String path = "/people/john.doe/@self";
+    RestHandler operation = registry.getRestHandler(path, "PUT");
+
+    Person person = new PersonImpl();
+    expect(converter.convertToObject(eq(jsonPerson), eq(Person.class)))
+        .andReturn(person);
+
+    expect(personService.updatePerson(eq(JOHN_DOE.iterator().next()),
+        eq(person),
+        eq(token))).andReturn(Futures.immediateFuture(person));
+
+    replay();
+    assertEquals(person, operation.execute(Maps.<String, String[]>newHashMap(),
+        new StringReader(jsonPerson), token, converter).get());
+    verify();
+  }
+
+  @Test
+  public void testHandleGetSupportedFields() throws Exception {
+    String path = "/people/@supportedFields";
+    RestHandler operation = registry.getRestHandler(path, "GET");
+
+    replay();
+    @SuppressWarnings("unchecked")
+    List<Object> received = (List<Object>) operation.execute(Maps.<String, String[]>newHashMap(),
+        null, token, converter).get();
+    assertEquals(2, received.size());
+    assertEquals("id", received.get(0).toString());
+    @SuppressWarnings("unchecked")
+    Map<String, Object> map = (Map<String, Object>) received.get(1);
+    assertEquals("familyName", map.get("name").toString());
+
+    verify();
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ResponseItemTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ResponseItemTest.java
new file mode 100644
index 0000000..e04606d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/ResponseItemTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import org.apache.shindig.protocol.ResponseItem;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletResponse;
+
+import static junitx.framework.Assert.assertNotEquals;
+
+
+/**
+ * Tests Response Item equality methods.
+ */
+public class ResponseItemTest extends Assert {
+
+  @Test
+  public void testEquals() {
+    ResponseItem responseItem = new ResponseItem(
+        HttpServletResponse.SC_BAD_REQUEST, "message1");
+    ResponseItem responseItemSame = new ResponseItem(
+        HttpServletResponse.SC_BAD_REQUEST, "message1");
+    ResponseItem responseItemDifferent =
+      new ResponseItem(HttpServletResponse.SC_FORBIDDEN, "message2");
+    ResponseItem simpleResponse = new ResponseItem("simple");
+    ResponseItem simpleResponseSame = new ResponseItem("simple");
+    ResponseItem simpleResponseDifferent = new ResponseItem("simpleDiffernt");
+    assertEquals(responseItem.hashCode(), responseItemSame.hashCode());
+    assertEquals(responseItem, responseItem);
+    assertEquals(responseItem, responseItemSame);
+    assertNotEquals(responseItem.hashCode(), responseItemDifferent.hashCode());
+    assertFalse(responseItem.equals(responseItemDifferent));
+    assertNotNull(responseItem);
+    assertNotEquals(responseItem, "A String");
+    assertEquals(simpleResponse.hashCode(), simpleResponseSame.hashCode());
+    assertEquals(simpleResponse, simpleResponse);
+    assertEquals(simpleResponse, simpleResponseSame);
+    assertNotSame(simpleResponse.hashCode(), simpleResponseDifferent.hashCode());
+    assertFalse(simpleResponse.equals(simpleResponseDifferent));
+    assertNotNull(simpleResponse);
+    assertNotEquals(simpleResponse, "A String");
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/SocialRequestItemTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/SocialRequestItemTest.java
new file mode 100644
index 0000000..45227c6
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/service/SocialRequestItemTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.service;
+
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.UserId;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Guice;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test social specific request parameters
+ */
+public class SocialRequestItemTest extends Assert {
+
+  private static final FakeGadgetToken FAKE_TOKEN = new FakeGadgetToken();
+
+  protected SocialRequestItem request;
+  protected BeanJsonConverter converter;
+
+  @Before
+  public void setUp() throws Exception {
+    FAKE_TOKEN.setAppId("12345");
+    FAKE_TOKEN.setOwnerId("someowner");
+    FAKE_TOKEN.setViewerId("someowner");
+    converter = new BeanJsonConverter(Guice.createInjector());
+    request = new SocialRequestItem(
+        Maps.<String, String[]>newHashMap(),
+        FAKE_TOKEN, converter, converter);
+  }
+
+  @Test
+  public void testGetUser() throws Exception {
+    request.setParameter("userId", "@owner");
+    assertEquals(UserId.Type.owner, request.getUsers().iterator().next().getType());
+  }
+
+  @Test
+  public void testGetGroup() throws Exception {
+    request.setParameter("groupId", "@self");
+    assertEquals(GroupId.Type.self, request.getGroup().getType());
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/CollectionOptionsTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/CollectionOptionsTest.java
new file mode 100644
index 0000000..9029d9d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/CollectionOptionsTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.protocol.conversion.BeanJsonConverter;
+import org.apache.shindig.social.opensocial.service.SocialRequestItem;
+
+import com.google.common.collect.Maps;
+import com.google.inject.Guice;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CollectionOptionsTest extends Assert {
+
+	private static final FakeGadgetToken FAKE_TOKEN = new FakeGadgetToken();
+
+	protected SocialRequestItem request;
+	protected BeanJsonConverter converter;
+
+	@Before
+	public void setUp() throws Exception {
+		FAKE_TOKEN.setAppId("12345");
+		FAKE_TOKEN.setOwnerId("someowner");
+		FAKE_TOKEN.setViewerId("someowner");
+		converter = new BeanJsonConverter(Guice.createInjector());
+		Map<String, String[]> optionalParameters = Maps.newHashMap();
+		String[] cvalue1 = {"cvalue1"};
+		String[] cvalue2 = {"cvalue2"};
+		optionalParameters.put("condition1",cvalue1);
+		optionalParameters.put("condition2",cvalue2);
+		request = new SocialRequestItem(
+				optionalParameters,
+				FAKE_TOKEN, converter, converter);
+	}
+
+	@Test
+	public void testOptionalParams() throws Exception {
+		CollectionOptions op = new CollectionOptions(request);
+		Map<String, String> optionalParams = op.getOptionalParameter();
+		Map<String, String> optionalParams1 = Maps.newHashMap();
+		optionalParams1.put("condition1","cvalue1");
+		optionalParams1.put("condition2","cvalue2");
+		assertEquals(optionalParams, optionalParams1);
+	}
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/DomainNameTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/DomainNameTest.java
new file mode 100644
index 0000000..ee2ff8b
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/DomainNameTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DomainNameTest extends Assert {
+
+  @Test
+  public void testDomainName() throws Exception {
+    DomainName d1 = new DomainName("");
+    assertTrue(d1 instanceof DomainName);
+
+    DomainName d2 = new DomainName("localhost");
+    assertTrue(d2 instanceof DomainName);
+
+    DomainName d3 = new DomainName("example.com");
+    assertTrue(d3 instanceof DomainName);
+  }
+
+  @Test(expected=IllegalArgumentException.class)
+  public void testDomainNameException() {
+    new DomainName("example.com/test");
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/GlobalIdTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/GlobalIdTest.java
new file mode 100644
index 0000000..48ca161
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/GlobalIdTest.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GlobalIdTest extends Assert {
+
+  @Test
+  public void testGlobalId() throws Exception {
+    DomainName dn = new DomainName("example.com");
+    LocalId lid = new LocalId("195mg90a39v");
+
+    GlobalId g1 = new GlobalId(dn, lid);
+    assertTrue(g1 instanceof GlobalId);
+
+    GlobalId g2 = new GlobalId("example.com:195mg90a39v");
+    assertTrue(g2 instanceof GlobalId);
+
+    GlobalId g3 = new GlobalId("example.com", "195mg90a39v");
+    assertTrue(g3 instanceof GlobalId);
+  }
+
+  @Test(expected=IllegalArgumentException.class)
+  public void testGlobalIdException() {
+    new GlobalId("example.com/test:195mg90a39v");
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/GroupIdTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/GroupIdTest.java
new file mode 100644
index 0000000..033183d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/GroupIdTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.apache.shindig.social.opensocial.spi.GroupId.Type;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GroupIdTest extends Assert {
+
+  @Test
+  public void testFromJson() {
+    GroupId all = GroupId.fromJson("@all");
+    assertEquals(GroupId.Type.all, all.getType());
+
+    GroupId friends = GroupId.fromJson("@friends");
+    assertEquals(GroupId.Type.friends, friends.getType());
+
+    GroupId self = GroupId.fromJson("@self");
+    assertEquals(GroupId.Type.self, self.getType());
+
+    GroupId group = GroupId.fromJson("superbia");
+    assertEquals(GroupId.Type.objectId, group.getType());
+    assertEquals("superbia", group.getObjectId().toString());
+
+    GroupId unknown = GroupId.fromJson("@foo");
+    assertEquals(Type.custom, unknown.getType());
+    assertEquals("@foo", unknown.getObjectId().toString());
+  }
+
+  @Test
+  public void testGroupId() {
+    DomainName dn1 = new DomainName("example.com");
+    LocalId l1 = new LocalId("195mg90a39v");
+    GlobalId gl1 = new GlobalId(dn1, l1);
+
+    GroupId g1 = new GroupId("example.com:195mg90a39v");
+    GroupId g2 = new GroupId(gl1);
+
+    assertEquals(g1.getType(), g2.getType());
+    assertEquals(g1.getObjectId().toString(), g2.getObjectId().toString());
+
+    GroupId g3 =  new GroupId("@foo");
+    assertEquals(Type.custom, g3.getType());
+    assertEquals("@foo", g3.getObjectId().toString());
+
+    GroupId g4 = new GroupId(Type.objectId, "example.com:195mg90a39v");
+    assertEquals(Type.objectId, g4.getType());
+    assertEquals("example.com:195mg90a39v", g4.getObjectId().toString());
+
+    GroupId g5 = new GroupId(Type.custom, "@foo");
+    assertEquals(Type.custom, g5.getType());
+    assertEquals("@foo", g5.getObjectId().toString());
+
+    GroupId g6 = new GroupId(Type.all, "something");
+    assertEquals(Type.all, g6.getType());
+    assertEquals("@all", g6.getObjectId().toString());
+
+    GroupId g7 = new GroupId(Type.self, null);
+    assertEquals(Type.self, g7.getType());
+    assertEquals("@self", g7.getObjectId().toString());
+
+    GroupId g8 = new GroupId(Type.friends, "bar");
+    assertEquals(Type.friends, g8.getType());
+    assertEquals("@friends", g8.getObjectId().toString());
+
+  }
+
+  @Test(expected=IllegalArgumentException.class)
+  public void testGroupIdException() {
+    new GroupId("195mg90a39v/937194");
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/LocalIdTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/LocalIdTest.java
new file mode 100644
index 0000000..d42fb95
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/LocalIdTest.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class LocalIdTest extends Assert {
+
+  @Test
+  public void testLocalId() throws Exception {
+    LocalId l1 = new LocalId("");
+    assertTrue(l1 instanceof LocalId);
+
+    LocalId l2 = new LocalId("195mg90a39v");
+    assertTrue(l2 instanceof LocalId);
+  }
+
+  @Test(expected=IllegalArgumentException.class)
+  public void testLocalIdException() {
+    new LocalId("195mg90a39v/937194");
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/ObjectIdTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/ObjectIdTest.java
new file mode 100644
index 0000000..6a69ca6
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/ObjectIdTest.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ObjectIdTest extends Assert {
+
+  @Test
+  public void testObjectId() throws Exception {
+    LocalId lid = new LocalId("195mg90a39v");
+    GlobalId gid = new GlobalId("example.com:195mg90a39v");
+
+    ObjectId o1 = new ObjectId(lid);
+    assertTrue(o1 instanceof ObjectId);
+
+    ObjectId o2 = new ObjectId(gid);
+    assertTrue(o2 instanceof ObjectId);
+
+    ObjectId o3 = new ObjectId("195mg90a39v");
+    assertTrue(o3 instanceof ObjectId);
+    assertTrue(o3.getObjectId() instanceof LocalId);
+
+    ObjectId o4 = new ObjectId("example.com:195mg90a39v");
+    assertTrue(o4 instanceof ObjectId);
+    assertTrue(o4.getObjectId() instanceof GlobalId);
+  }
+
+  @Test(expected=IllegalArgumentException.class)
+  public void testObjectIdException() {
+    new ObjectId("195mg90a39v/937194");
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/UserIdTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/UserIdTest.java
new file mode 100644
index 0000000..ae21e9d
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/spi/UserIdTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.spi;
+
+import org.apache.shindig.common.testing.FakeGadgetToken;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class UserIdTest extends Assert {
+  @Test
+  public void testGetUserId() throws Exception {
+    UserId owner = new UserId(UserId.Type.owner, "hello");
+    assertEquals("owner", owner.getUserId(new FakeGadgetToken().setOwnerId("owner")));
+
+    UserId viewer = new UserId(UserId.Type.viewer, "hello");
+    assertEquals("viewer", viewer.getUserId(new FakeGadgetToken().setViewerId("viewer")));
+
+    UserId me = new UserId(UserId.Type.me, "hello");
+    assertEquals("viewer", me.getUserId(new FakeGadgetToken().setViewerId("viewer")));
+
+    UserId user = new UserId(UserId.Type.userId, "hello");
+    assertEquals("hello", user.getUserId(new FakeGadgetToken()));
+  }
+
+  @Test
+  public void testFromJson() throws Exception {
+    UserId owner = UserId.fromJson("@owner");
+    assertEquals(UserId.Type.owner, owner.getType());
+
+    UserId viewer = UserId.fromJson("@viewer");
+    assertEquals(UserId.Type.viewer, viewer.getType());
+
+    UserId me = UserId.fromJson("@me");
+    assertEquals(UserId.Type.me, me.getType());
+
+    UserId user = UserId.fromJson("john.doe");
+    assertEquals(UserId.Type.userId, user.getType());
+    assertEquals("john.doe", user.getUserId());
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/BeanXStreamAtomConverterTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/BeanXStreamAtomConverterTest.java
new file mode 100644
index 0000000..1af7c61
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/BeanXStreamAtomConverterTest.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.util;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.social.SocialApiTestsGuiceModule;
+import org.apache.shindig.social.core.model.ActivityImpl;
+import org.apache.shindig.social.core.model.AddressImpl;
+import org.apache.shindig.social.core.model.ListFieldImpl;
+import org.apache.shindig.social.core.model.MediaItemImpl;
+import org.apache.shindig.social.core.model.NameImpl;
+import org.apache.shindig.social.core.model.PersonImpl;
+import org.apache.shindig.social.core.util.BeanXStreamAtomConverter;
+import org.apache.shindig.social.core.util.xstream.XStream081Configuration;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.ListField;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+import org.apache.shindig.social.opensocial.model.Person;
+
+import org.custommonkey.xmlunit.XMLAssert;
+import org.custommonkey.xmlunit.XMLUnit;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.util.List;
+import java.util.Map;
+
+public class BeanXStreamAtomConverterTest extends Assert {
+  private Person johnDoe;
+  private Activity activity;
+
+  private BeanXStreamAtomConverter beanXmlConverter;
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(new SocialApiTestsGuiceModule());
+
+    johnDoe = new PersonImpl("johnDoeId", "Johnny", new NameImpl("John Doe"));
+    johnDoe.setPhoneNumbers(Lists.<ListField> newArrayList(new ListFieldImpl(
+        "home", "+33H000000000"), new ListFieldImpl("mobile", "+33M000000000"),
+        new ListFieldImpl("work", "+33W000000000")));
+
+    johnDoe.setAddresses(Lists.<Address> newArrayList(new AddressImpl(
+        "My home address")));
+
+    johnDoe.setEmails(Lists.<ListField> newArrayList(new ListFieldImpl("work",
+        "john.doe@work.bar"), new ListFieldImpl("home", "john.doe@home.bar")));
+
+    activity = new ActivityImpl("activityId", johnDoe.getId());
+
+    MediaItemImpl mediaItem = new MediaItemImpl();
+    mediaItem.setMimeType("image/jpg");
+    mediaItem.setType(MediaItem.Type.IMAGE);
+    mediaItem.setUrl("http://foo.bar");
+    mediaItem.setLocation(new AddressImpl("Foo bar address"));
+    mediaItem.setNumViews("10000");
+
+    activity.setMediaItems(Lists.<MediaItem> newArrayList(mediaItem));
+    activity.setUrl("http://foo.com");
+
+    beanXmlConverter = new BeanXStreamAtomConverter(
+        new XStream081Configuration(injector));
+  }
+
+  public static class SimplePerson {
+    private String id;
+    private String name;
+
+    public SimplePerson(String id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+
+    public String getId() {
+      return id;
+    }
+
+    public String getName() {
+      return name;
+    }
+  }
+
+  @Test
+  public void testToXmlOnSimpleClass() throws Exception {
+    // since this doent implement the model, it wont get mapped correctly, hence
+    // we cant validate
+    SimplePerson cassie = new SimplePerson("5", "robot");
+    String xml = beanXmlConverter.convertToString(cassie);
+    Element element = XmlUtil.parse(xml);
+    Node id = element.getElementsByTagName("id").item(0);
+    Node name = element.getElementsByTagName("name").item(0);
+
+    assertEquals("5", id.getTextContent());
+    assertEquals("robot", name.getTextContent());
+  }
+
+  @Test
+  public void testPersonToXml() throws Exception {
+    String xml = beanXmlConverter.convertToString(johnDoe);
+    Element element = XmlUtil.parse(xml);
+    Node id = element.getElementsByTagName("id").item(0);
+    assertEquals("urn:guid:" + johnDoe.getId(), id.getTextContent());
+  }
+
+  @Test
+  public void testActivityToXml() throws Exception {
+    String xml = beanXmlConverter.convertToString(activity);
+
+    Element element = XmlUtil.parse(xml);
+    Node id = element.getElementsByTagName("id").item(0);
+    assertEquals(activity.getId(), id.getTextContent());
+  }
+
+  @Test
+  public void testMapsToXml() throws Exception {
+    // This is the structure our app data currently takes
+    Map<String, Map<String, String>> map = Maps.newTreeMap();
+
+    Map<String, String> item1Map = Maps.newHashMap();
+    item1Map.put("value", "1");
+    map.put("item1", item1Map);
+
+    Map<String, String> item2Map = Maps.newHashMap();
+    item2Map.put("value", "2");
+    map.put("item2", item2Map);
+
+    String xml = beanXmlConverter.convertToString(map);
+
+    XmlUtil.parse(xml);
+
+    String expectedXml = "<feed xmlns=\"http://www.w3.org/2005/Atom\" xmlns:osearch=\"http://a9.com/-/spec/opensearch/1.1\" > "
+        + " <entry><id>item1</id>"
+        + "    <content type=\"application/xml\" ><entry><key>value</key><value>1</value></entry></content>"
+        + " </entry> "
+        + " <entry><id>item2</id>"
+        + "     <content type=\"application/xml\" ><entry><key>value</key><value>2</value></entry></content>"
+        + " </entry> "
+        + " <osearch:startIndex>0</osearch:startIndex> "
+        + " <osearch:totalResults>2</osearch:totalResults> "
+        + " <osearch:itemsPerPage>2</osearch:itemsPerPage></feed> ";
+    XMLUnit.setIgnoreWhitespace(true);
+    XMLAssert.assertXMLEqual(expectedXml, xml);
+  }
+
+  @Test
+  public void testMapToXml() throws Exception {
+    Map<String, String> m = Maps.newLinkedHashMap();
+    m.put("key1", "value1");
+    m.put("key2", "value2");
+    String xml = beanXmlConverter.convertToString(m);
+    XmlUtil.parse(xml);
+    String expectedXml = "<feed xmlns=\"http://www.w3.org/2005/Atom\" "
+        + " xmlns:osearch=\"http://a9.com/-/spec/opensearch/1.1\">"
+        + "  <entry><id>key1</id><content type=\"application/xml\" >"
+        + "    <value>value1</value></content>"
+        + "  </entry>"
+        + "  <entry><id>key2</id>"
+        + "     <content type=\"application/xml\" ><value>value2</value></content>"
+        + "  </entry>"
+        + "  <osearch:startIndex>0</osearch:startIndex>"
+        + "  <osearch:totalResults>2</osearch:totalResults>"
+        + "  <osearch:itemsPerPage>2</osearch:itemsPerPage></feed>";
+    XMLUnit.setIgnoreWhitespace(true);
+    XMLAssert.assertXMLEqual(expectedXml, xml);
+  }
+
+  @Test
+  public void testEmptyList() throws Exception {
+    List<String> empty = Lists.newArrayList();
+    String xml = beanXmlConverter.convertToString(empty);
+    XmlUtil.parse(xml);
+    String expectedXml = "<feed xmlns=\"http://www.w3.org/2005/Atom\" "
+        + "xmlns:osearch=\"http://a9.com/-/spec/opensearch/1.1\" >"
+        + "<entry><content/></entry>"
+        + "<osearch:startIndex>0</osearch:startIndex>"
+        + "<osearch:totalResults>1</osearch:totalResults>"
+        + "<osearch:itemsPerPage>1</osearch:itemsPerPage></feed>";
+    XMLUnit.setIgnoreWhitespace(true);
+    XMLAssert.assertXMLEqual(expectedXml, xml);
+
+    List<List<String>> emptyLists = Lists.newArrayList();
+    List<String> emptyList = Lists.newArrayList();
+    emptyLists.add(emptyList);
+    emptyLists.add(emptyList);
+    emptyLists.add(emptyList);
+    xml = beanXmlConverter.convertToString(emptyLists);
+    XmlUtil.parse(xml);
+    expectedXml = "<feed xmlns=\"http://www.w3.org/2005/Atom\" "
+        + "xmlns:osearch=\"http://a9.com/-/spec/opensearch/1.1\" >"
+        + "<entry><content><list/><list/><list/></content></entry>"
+        + "<osearch:startIndex>0</osearch:startIndex>"
+        + "<osearch:totalResults>1</osearch:totalResults>"
+        + "<osearch:itemsPerPage>1</osearch:itemsPerPage></feed>";
+    XMLUnit.setIgnoreWhitespace(true);
+    XMLAssert.assertXMLEqual(expectedXml, xml);
+  }
+
+  @Test
+  public void testElementNamesInList() throws Exception {
+    List<Activity> activities = Lists.newArrayList();
+    activities.add(activity);
+    activities.add(activity);
+    activities.add(activity);
+    String xml = beanXmlConverter.convertToString(activities);
+    XmlUtil.parse(xml);
+    String expectedXml = "<feed xmlns=\"http://www.w3.org/2005/Atom\" "
+        + "   xmlns:osearch=\"http://a9.com/-/spec/opensearch/1.1\"><entry><content>"
+        + "  <activity xmlns=\"http://ns.opensocial.org/2008/opensocial\">"
+        + "    <id>activityId</id>"
+        + "    <mediaItems>"
+        + "        <mimeType>image/jpg</mimeType>"
+        + "        <type>IMAGE</type>"
+        + "        <url>http://foo.bar</url>"
+        + "        <location>"
+        + "           <formatted>Foo bar address</formatted>"
+        + "        </location>"
+        + "        <numViews>10000</numViews>"
+        + "    </mediaItems>"
+        + "    <url>http://foo.com</url>"
+        + "    <userId>johnDoeId</userId>"
+        + "  </activity>"
+        + "  <activity xmlns=\"http://ns.opensocial.org/2008/opensocial\">"
+        + "    <id>activityId</id>"
+        + "    <mediaItems>"
+        + "        <mimeType>image/jpg</mimeType>"
+        + "        <type>IMAGE</type>"
+        + "        <url>http://foo.bar</url>"
+        + "        <location>"
+        + "           <formatted>Foo bar address</formatted>"
+        + "        </location>"
+        + "        <numViews>10000</numViews>"
+        + "    </mediaItems>"
+        + "    <url>http://foo.com</url>"
+        + "    <userId>johnDoeId</userId>"
+        + "  </activity>"
+        + "  <activity xmlns=\"http://ns.opensocial.org/2008/opensocial\">"
+        + "    <id>activityId</id>"
+        + "    <mediaItems>"
+        + "        <mimeType>image/jpg</mimeType>"
+        + "        <type>IMAGE</type>"
+        + "        <url>http://foo.bar</url>"
+        + "        <location>"
+        + "           <formatted>Foo bar address</formatted>"
+        + "        </location>"
+        + "        <numViews>10000</numViews>"
+        + "    </mediaItems>"
+        + "    <url>http://foo.com</url>"
+        + "    <userId>johnDoeId</userId>"
+        + "  </activity>"
+        + "</content></entry>"
+        + "<osearch:startIndex>0</osearch:startIndex>"
+        + "<osearch:totalResults>1</osearch:totalResults>"
+        + "<osearch:itemsPerPage>1</osearch:itemsPerPage>" + "</feed>";
+    XMLUnit.setIgnoreWhitespace(true);
+    XMLAssert.assertXMLEqual(expectedXml, xml);
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/BeanXStreamConverterTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/BeanXStreamConverterTest.java
new file mode 100644
index 0000000..cf1f8ac
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/BeanXStreamConverterTest.java
@@ -0,0 +1,295 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.util;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shindig.common.xml.XmlException;
+import org.apache.shindig.common.xml.XmlUtil;
+import org.apache.shindig.protocol.conversion.BeanXStreamConverter;
+import org.apache.shindig.social.SocialApiTestsGuiceModule;
+import org.apache.shindig.social.core.model.ActivityImpl;
+import org.apache.shindig.social.core.model.AddressImpl;
+import org.apache.shindig.social.core.model.ListFieldImpl;
+import org.apache.shindig.social.core.model.MediaItemImpl;
+import org.apache.shindig.social.core.model.NameImpl;
+import org.apache.shindig.social.core.model.PersonImpl;
+import org.apache.shindig.social.core.util.xstream.XStream081Configuration;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.Address;
+import org.apache.shindig.social.opensocial.model.ListField;
+import org.apache.shindig.social.opensocial.model.MediaItem;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.custommonkey.xmlunit.XMLAssert;
+import org.custommonkey.xmlunit.XMLUnit;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.Map;
+
+public class BeanXStreamConverterTest extends Assert {
+  private static final String XMLSCHEMA =
+      " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \n"
+      + " xsi:schemaLocation=\"http://ns.opensocial.org/2008/opensocial classpath:opensocial.xsd\" ";
+  private Person johnDoe;
+  private Activity activity;
+
+  private BeanXStreamConverter beanXmlConverter;
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(new SocialApiTestsGuiceModule());
+
+    johnDoe = new PersonImpl("johnDoeId", "Johnny", new NameImpl("John Doe"));
+    johnDoe.setPhoneNumbers(Lists.<ListField> newArrayList(new ListFieldImpl(
+        "home", "+33H000000000"), new ListFieldImpl("mobile", "+33M000000000"),
+        new ListFieldImpl("work", "+33W000000000")));
+
+    johnDoe.setAddresses(Lists.<Address> newArrayList(new AddressImpl(
+        "My home address")));
+
+    johnDoe.setEmails(Lists.<ListField> newArrayList(new ListFieldImpl("work",
+        "john.doe@work.bar"), new ListFieldImpl("home", "john.doe@home.bar")));
+
+    activity = new ActivityImpl("activityId", johnDoe.getId());
+    activity.setUrl("http://foo.com/");
+
+    activity.setMediaItems(Lists.<MediaItem> newArrayList(new MediaItemImpl(
+        "image/jpg", MediaItem.Type.IMAGE, "http://foo.bar")));
+
+
+    beanXmlConverter = new BeanXStreamConverter(new XStream081Configuration(injector));
+  }
+
+
+  public static class SimplePerson {
+    private String id;
+    private String name;
+
+    public SimplePerson(String id, String name) {
+      this.id = id;
+      this.name = name;
+    }
+
+    public String getId() {
+      return id;
+    }
+
+    public String getName() {
+      return name;
+    }
+  }
+
+  @Test
+  public void testToXmlOnSimpleClass() throws Exception {
+    // since this doent implement the model, it wont get mapped correctly, hence
+    // we cant validate
+    SimplePerson cassie = new SimplePerson("5", "robot");
+    String xml = beanXmlConverter.convertToString(cassie);
+    Element element = XmlUtil.parse(xml);
+    Node id = element.getElementsByTagName("id").item(0);
+    Node name = element.getElementsByTagName("name").item(0);
+
+    assertEquals("5", id.getTextContent());
+    assertEquals("robot", name.getTextContent());
+  }
+
+  @Test
+  public void testPersonToXml() throws Exception {
+    String xml = XSDValidator.validateOpenSocial(beanXmlConverter.convertToString(johnDoe));
+    Element element = XmlUtil.parse(xml);
+    Node id = element.getElementsByTagName("id").item(0);
+    assertEquals(johnDoe.getId(), id.getTextContent());
+  }
+
+  @Test
+  public void testActivityToXml() throws Exception {
+    String xml = XSDValidator.validateOpenSocial(beanXmlConverter.convertToString(activity));
+
+    Element element = XmlUtil.parse(xml);
+    Node id = element.getElementsByTagName("id").item(0);
+    assertEquals(activity.getId(), id.getTextContent());
+  }
+
+  @Test
+  public void testMapsToXml() throws Exception {
+    // This is the structure our app data currently takes
+    Map<String, Map<String, String>> map = Maps.newTreeMap();
+
+    Map<String, String> item1Map = Maps.newHashMap();
+    item1Map.put("value", "1");
+    map.put("item1", item1Map);
+
+    Map<String, String> item2Map = Maps.newHashMap();
+    item2Map.put("value", "2");
+    map.put("item2", item2Map);
+
+    String xml = beanXmlConverter.convertToString(map);
+
+    XmlUtil.parse(xml);
+
+    String expectedXml = XSDValidator.XMLDEC + "<response xmlns=\"http://ns.opensocial.org/2008/opensocial\"><map>"
+        + "  <entry><key>item1</key><value><entry><key>value</key><value>1</value></entry></value></entry> "
+        + "  <entry><key>item2</key><value><entry><key>value</key><value>2</value></entry></value></entry> "
+        + "</map></response>";
+    assertEquals(StringUtils.deleteWhitespace(expectedXml), StringUtils
+        .deleteWhitespace(xml));
+  }
+
+  @Test
+  public void testMapToXml() throws XmlException {
+    Map<String, String> m = Maps.newLinkedHashMap();
+    m.put("key1", "value1");
+    m.put("key2", "value2");
+    String xml = beanXmlConverter.convertToString(m);
+    XmlUtil.parse(xml);
+    String expectedXml = XSDValidator.XMLDEC + "<response xmlns=\"http://ns.opensocial.org/2008/opensocial\"><map>"
+        + "  <entry><key>key1</key><value>value1</value></entry> "
+        + "  <entry><key>key2</key><value>value2</value></entry> "
+        + "</map></response>";
+    assertEquals(StringUtils.deleteWhitespace(expectedXml), StringUtils
+        .deleteWhitespace(xml));
+  }
+
+  @Test
+  public void testEmptyList() throws XmlException {
+    List<String> empty = Lists.newArrayList();
+    String xml = beanXmlConverter.convertToString(empty);
+    XmlUtil.parse(xml);
+    String expectedXml = XSDValidator.XMLDEC + "<response xmlns=\"http://ns.opensocial.org/2008/opensocial\"><list/></response>";
+    assertEquals(StringUtils.deleteWhitespace(expectedXml), StringUtils
+        .deleteWhitespace(xml));
+
+    List<List<String>> emptyLists = Lists.newArrayList();
+    List<String> emptyList = Lists.newArrayList();
+    emptyLists.add(emptyList);
+    emptyLists.add(emptyList);
+    emptyLists.add(emptyList);
+    xml = beanXmlConverter.convertToString(emptyLists);
+    XmlUtil.parse(xml);
+    expectedXml = XSDValidator.XMLDEC + "<response xmlns=\"http://ns.opensocial.org/2008/opensocial\"><list.container>" + "  <list/>" + "  <list/>"
+        + "  <list/>" + "</list.container></response>";
+    assertEquals(StringUtils.deleteWhitespace(expectedXml), StringUtils
+        .deleteWhitespace(xml));
+  }
+
+  @Test
+  public void testElementNamesInList() throws Exception {
+
+    List<Activity> activities = Lists.newArrayList();
+    activities.add(activity);
+    activities.add(activity);
+    activities.add(activity);
+    String xml = XSDValidator.validateOpenSocial(beanXmlConverter.convertToString(activities));
+
+    // This test is a bit bogus and relies on some odd voodoo in the bundled opensocial.xsd
+    XmlUtil.parse(xml);
+    String expectedXml = "<response xmlns=\"http://ns.opensocial.org/2008/opensocial\">"
+        + "<list.container>"
+        + "  <activity xmlns=\"http://ns.opensocial.org/2008/opensocial\">"
+        + "    <id>activityId</id>"
+        + "    <mediaItems>"
+        + "        <mimeType>image/jpg</mimeType>"
+        + "        <type>IMAGE</type>"
+        + "        <url>http://foo.bar</url>"
+        + "    </mediaItems>"
+        + "    <url>http://foo.com/</url>"
+        + "    <userId>johnDoeId</userId>"
+        + "  </activity>"
+        + "  <activity xmlns=\"http://ns.opensocial.org/2008/opensocial\">"
+        + "    <id>activityId</id>"
+        + "    <mediaItems>"
+        + "        <mimeType>image/jpg</mimeType>"
+        + "        <type>IMAGE</type>"
+        + "        <url>http://foo.bar</url>"
+        + "    </mediaItems>"
+        + "    <url>http://foo.com/</url>"
+        + "    <userId>johnDoeId</userId>"
+        + "  </activity>"
+        + "  <activity xmlns=\"http://ns.opensocial.org/2008/opensocial\">"
+        + "    <id>activityId</id>"
+        + "    <mediaItems>"
+        + "        <mimeType>image/jpg</mimeType>"
+        + "        <type>IMAGE</type>"
+        + "        <url>http://foo.bar</url>"
+        + "    </mediaItems>"
+        + "    <url>http://foo.com/</url>"
+        + "    <userId>johnDoeId</userId>"
+        + "  </activity>"
+        + "</list.container>"
+        + "</response>";
+    expectedXml = XSDValidator.insertSchema(expectedXml, XMLSCHEMA, true);
+    XMLUnit.setIgnoreWhitespace(true);
+    XMLAssert.assertXMLEqual(expectedXml, xml);
+  }
+
+  @Test
+  public void testPerson1() throws Exception {
+    String xml = loadXML("testxml/person1.xml");
+    beanXmlConverter.convertToObject(xml, Person.class);
+  }
+
+  @Test
+  public void testActivity1() throws Exception {
+    String xml = loadXML("testxml/activity1.xml");
+    beanXmlConverter.convertToObject(xml, Activity.class);
+  }
+
+  @Test
+  public void testAppdata1() throws Exception {
+    String xml = loadXML("testxml/appdata1.xml");
+    beanXmlConverter.convertToObject(xml, Map.class);
+  }
+
+  @Test
+  @Ignore("TODO")
+  public void testGroup1() throws XmlException {
+    // TODO
+  }
+
+  /**
+   * @param resource
+   * @return a string
+   * @throws IOException
+   */
+  private String loadXML(String resource) throws IOException {
+    BufferedReader in = new BufferedReader(new InputStreamReader(this
+        .getClass().getResourceAsStream(resource)));
+    StringBuilder sb = new StringBuilder();
+    for (String line = in.readLine(); line != null; line = in.readLine()) {
+      sb.append(line);
+    }
+    in.close();
+    return sb.toString();
+  }
+
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/XSDValidator.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/XSDValidator.java
new file mode 100644
index 0000000..e51fb40
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/XSDValidator.java
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.util;
+
+import org.apache.shindig.common.logging.i18n.MessageKeys;
+import org.apache.shindig.common.util.CharsetUtil;
+import org.w3c.dom.ls.LSInput;
+import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.SAXException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+
+/**
+ * Validator utility for testing.
+ */
+public class XSDValidator {
+  /**
+   * The schema language being used.
+   */
+  private static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
+
+  /**
+   * The XML declaration
+   */
+  public static final String XMLDEC = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
+
+  //class name for logging purpose
+  private static final String classname = XSDValidator.class.getName();
+  private static final Logger LOG = Logger.getLogger(classname,MessageKeys.MESSAGES);
+
+  /**
+   * Validate a xml string against a supplied schema.
+   *
+   * @param xml
+   *          the xml presented as a string
+   * @param schema
+   *          an input stream containing the xsd
+   * @return a list of errors or a 0 lenght string if none.
+   */
+  public static String validate(String xml, InputStream schema) {
+    return validate(new ByteArrayInputStream(CharsetUtil.getUtf8Bytes(xml)), schema);
+  }
+
+  /**
+   * Validate a xml input stream against a supplied schema.
+   *
+   * @param xml
+   *          a stream containing the xml
+   * @param schema
+   *          a stream containing the schema
+   * @return a list of errors or warnings, a 0 lenght string if none.
+   */
+  public static String validate(InputStream xml, InputStream schema) {
+
+    SAXParserFactory factory = SAXParserFactory.newInstance();
+    factory.setNamespaceAware(true);
+    factory.setValidating(true);
+    final StringBuilder errors = new StringBuilder();
+    try {
+      SchemaFactory schemaFactory = SchemaFactory.newInstance(W3C_XML_SCHEMA);
+      Schema s = schemaFactory.newSchema(new StreamSource(schema));
+
+      Validator validator = s.newValidator();
+      final LSResourceResolver lsr = validator.getResourceResolver();
+      validator.setResourceResolver(new LSResourceResolver() {
+
+        public LSInput resolveResource(String arg0, String arg1, String arg2,
+            String arg3, String arg4) {
+          if (LOG.isLoggable(Level.INFO)) {
+            LOG.logp(Level.INFO, classname, "resolveResource", MessageKeys.RESOLVE_RESOURCE, new Object[] {arg0, arg1, arg2, arg3, arg4});
+          }
+          return lsr.resolveResource(arg0, arg1, arg2, arg3, arg4);
+        }
+
+      });
+
+      validator.validate(new StreamSource(xml));
+    } catch (IOException e) {
+      // ignore
+    } catch (SAXException e) {
+      errors.append(e.getMessage()).append('\n');
+    }
+
+    return errors.toString();
+  }
+
+  /**
+   * Process the response string to strip the container element and insert the
+   * opensocial schema.
+   *
+   * @param xml
+   * @return
+   */
+  public static String insertSchema(String xml, String schemaStatement,
+      boolean removeContainer) {
+    if (xml == null || xml.trim().length() == 0) {
+      return xml;
+    }
+
+    int start = 0;
+    if ( xml.startsWith("<?") ) {
+      start = xml.indexOf('>')+1;
+      int gt = xml.indexOf('>',start);
+      if (gt > 0) {
+        return xml.substring(0, gt) + schemaStatement
+            + xml.substring(gt);
+      }
+    } else {
+      int gt = xml.indexOf('>',start);
+      if (gt > 0) {
+        return XMLDEC + xml.substring(0, gt) + schemaStatement
+            + xml.substring(gt);
+      }
+
+    }
+    return xml;
+  }
+
+  /**
+   * @param xmlFragment
+   * @return a list of errors
+   */
+  public static String validate(String xmlFragment, String schemaStatement,
+      String schemaResource, boolean removeContainer) {
+
+    String xml = XSDValidator.insertSchema(xmlFragment, schemaStatement, removeContainer);
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.fine("Validating " + xml);
+    }
+    String errors = XSDValidator.validate(xml, XSDValidator.class
+        .getResourceAsStream(schemaResource));
+    if (!"".equals(errors)) {
+      if (LOG.isLoggable(Level.SEVERE)) {
+        LOG.logp(Level.SEVERE, classname, "resolveResource", MessageKeys.FAILED_TO_VALIDATE, new Object[] {xml});
+      }
+    }
+    if (!"".equals(errors)) {
+      throw new Error("XML document does not validate \n" + errors + '\n' + xml);
+    }
+    return xml;
+  }
+
+  public static String validateOpenSocial(String xmlFragment) {
+    String XMLSCHEMA = " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" \n"
+    + " xsi:schemaLocation=\"http://ns.opensocial.org/2008/opensocial classpath:opensocial.xsd\" ";
+
+    String xml = XSDValidator.insertSchema(xmlFragment, XMLSCHEMA, true);
+    if (LOG.isLoggable(Level.FINE)) {
+      LOG.fine("Validating " + xml);
+    }
+    String errors = XSDValidator.validate(xml, XSDValidator.class
+        .getResourceAsStream("opensocial.xsd"));
+    if (!"".equals(errors)) {
+      if (LOG.isLoggable(Level.SEVERE)) {
+        LOG.logp(Level.SEVERE, classname, "resolveResource", MessageKeys.FAILED_TO_VALIDATE, new Object[] {xml});
+      }
+    }
+    if (!"".equals(errors)) {
+      throw new Error("XML document does not validate \n" + errors + '\n' + xml);
+    }
+    return xml;
+
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/XSDValidatorTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/XSDValidatorTest.java
new file mode 100644
index 0000000..e3c87e7
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/opensocial/util/XSDValidatorTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.opensocial.util;
+
+import org.apache.shindig.common.xml.XmlException;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Tests the XSDValidator
+ */
+public class XSDValidatorTest extends Assert {
+
+  /**
+   * Check that the XSDValidator is working correctly by testing for pass and fail
+   * @throws XmlException
+   */
+  @Test
+  public void testValidator() throws XmlException {
+    assertEquals("", XSDValidator.validate(this.getClass().getResourceAsStream("testschema1.xml"),this.getClass().getResourceAsStream("opensocial.xsd")));
+    assertNotSame("", XSDValidator.validate(this.getClass().getResourceAsStream("testschema1bad.xml"),this.getClass().getResourceAsStream("opensocial.xsd")));
+  }
+
+  @Test
+  public void testPerson1() throws XmlException {
+    assertEquals("", XSDValidator.validate(this.getClass().getResourceAsStream("testxml/person1.xml"),this.getClass().getResourceAsStream("opensocial.xsd")));
+  }
+
+  @Test
+  public void testActivity1() throws XmlException {
+    assertEquals("", XSDValidator.validate(this.getClass().getResourceAsStream("testxml/activity1.xml"),this.getClass().getResourceAsStream("opensocial.xsd")));
+  }
+
+  @Test
+  public void testAppdata1() throws XmlException {
+    assertEquals("", XSDValidator.validate(this.getClass().getResourceAsStream("testxml/appdata1.xml"),this.getClass().getResourceAsStream("opensocial.xsd")));
+  }
+
+  @Test
+  public void testGroup1() throws XmlException {
+    assertEquals("", XSDValidator.validate(this.getClass().getResourceAsStream("testxml/group1.xml"),this.getClass().getResourceAsStream("opensocial.xsd")));
+  }
+}
diff --git a/trunk/java/social-api/src/test/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialServiceTest.java b/trunk/java/social-api/src/test/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialServiceTest.java
new file mode 100644
index 0000000..0366fdb
--- /dev/null
+++ b/trunk/java/social-api/src/test/java/org/apache/shindig/social/sample/spi/JsonDbOpensocialServiceTest.java
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+package org.apache.shindig.social.sample.spi;
+
+import java.util.Collections;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.shindig.auth.AnonymousSecurityToken;
+import org.apache.shindig.auth.SecurityToken;
+import org.apache.shindig.common.testing.FakeGadgetToken;
+import org.apache.shindig.protocol.DataCollection;
+import org.apache.shindig.protocol.ProtocolException;
+import org.apache.shindig.protocol.RestfulCollection;
+import org.apache.shindig.protocol.model.FilterOperation;
+import org.apache.shindig.protocol.model.SortOrder;
+import org.apache.shindig.social.SocialApiTestsGuiceModule;
+import org.apache.shindig.social.core.model.NameImpl;
+import org.apache.shindig.social.core.model.PersonImpl;
+import org.apache.shindig.social.opensocial.model.Activity;
+import org.apache.shindig.social.opensocial.model.ActivityEntry;
+import org.apache.shindig.social.opensocial.model.Person;
+import org.apache.shindig.social.opensocial.spi.CollectionOptions;
+import org.apache.shindig.social.opensocial.spi.GroupId;
+import org.apache.shindig.social.opensocial.spi.PersonService;
+import org.apache.shindig.social.opensocial.spi.UserId;
+import org.json.JSONArray;
+import org.json.JSONObject;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+/**
+ * Test the JSONOpensocialService
+ */
+public class JsonDbOpensocialServiceTest extends Assert {
+  private JsonDbOpensocialService db;
+
+  private static final UserId CANON_USER = new UserId(UserId.Type.userId, "canonical");
+  private static final UserId JOHN_DOE = new UserId(UserId.Type.userId, "john.doe");
+  private static final UserId JANE_DOE = new UserId(UserId.Type.userId, "jane.doe");
+  private static final UserId ANONYMOUS = new UserId(UserId.Type.userId, AnonymousSecurityToken.ANONYMOUS_ID);
+
+  private static final GroupId SELF_GROUP = new GroupId(GroupId.Type.self, null);
+  private static final String APP_ID = "1";
+  private static final String CANONICAL_USER_ID = "canonical";
+
+  private SecurityToken token = new FakeGadgetToken();
+
+  @Before
+  public void setUp() throws Exception {
+    Injector injector = Guice.createInjector(new SocialApiTestsGuiceModule());
+    db = injector.getInstance(JsonDbOpensocialService.class);
+  }
+
+  @Test
+  public void testGetPersonDefaultFields() throws Exception {
+    Person person = db
+        .getPerson(CANON_USER, Person.Field.DEFAULT_FIELDS, token).get();
+
+    assertNotNull("Canonical user not found", person);
+    assertNotNull("Canonical user has no id", person.getId());
+    assertNotNull("Canonical user has no name", person.getName());
+    assertNotNull("Canonical user has no thumbnail",
+        person.getThumbnailUrl());
+  }
+
+  @Test
+  public void testGetAnonymousUser() throws Exception {
+    Person person = db.getPerson(ANONYMOUS, Person.Field.DEFAULT_FIELDS, token).get();
+    assertEquals("-1", person.getId());
+    assertEquals("Anonymous", person.getName().getFormatted());
+    assertEquals("Anonymous", person.getNickname());
+  }
+
+  @Test
+  public void testGetPersonAllFields() throws Exception {
+    Person person = db
+        .getPerson(CANON_USER, Person.Field.ALL_FIELDS, token).get();
+    assertNotNull("Canonical user not found", person);
+  }
+
+  @Test
+  public void testGetPersonAllAppData() throws Exception {
+    Person person = db
+        .getPerson(CANON_USER, ImmutableSet.of("id", "appData"), token).get();
+
+    assertNotNull("Canonical user not found", person);
+    assertEquals("Canonical user has wrong id", "canonical", person.getId());
+    assertEquals("Canonical user has wrong app data",
+        ImmutableMap.of("count", "2", "size", "100"), person.getAppData());
+  }
+
+  @Test
+  public void testGetPersonOneAppDataField() throws Exception {
+    Person person = db
+        .getPerson(CANON_USER, ImmutableSet.of("id", "appData.size"), token).get();
+
+    assertNotNull("Canonical user not found", person);
+    assertEquals("Canonical user has wrong id", "canonical", person.getId());
+    assertEquals("Canonical user has wrong app data",
+        ImmutableMap.of("size", "100"), person.getAppData());
+  }
+
+  @Test
+  public void testGetPersonMultipleAppDataFields() throws Exception {
+    Person person = db
+        .getPerson(CANON_USER,
+            ImmutableSet.of("id", "appData.size", "appData.count", "appData.bogus"),
+            token).get();
+
+    assertNotNull("Canonical user not found", person);
+    assertEquals("Canonical user has wrong id", "canonical", person.getId());
+    assertEquals("Canonical user has wrong app data",
+        ImmutableMap.of("count", "2", "size", "100"), person.getAppData());
+  }
+
+  @Test
+  public void testGetExpectedFriends() throws Exception {
+    CollectionOptions options = new CollectionOptions();
+    options.setSortBy(PersonService.TOP_FRIENDS_SORT);
+    options.setSortOrder(SortOrder.ascending);
+    options.setFilter(null);
+    options.setFilterOperation(FilterOperation.contains);
+    options.setFilterValue("");
+    options.setFirst(0);
+    options.setMax(20);
+
+    RestfulCollection<Person> responseItem = db.getPeople(
+        ImmutableSet.of(CANON_USER), new GroupId(GroupId.Type.friends, null),
+        options, Collections.<String>emptySet(), token).get();
+    assertNotNull(responseItem);
+    assertEquals(4, responseItem.getTotalResults());
+    // Test a couple of users
+    assertEquals("john.doe", responseItem.getList().get(0).getId());
+    assertEquals("jane.doe", responseItem.getList().get(1).getId());
+  }
+
+  @Test
+  public void testGetExpectedUsersForPlural() throws Exception {
+    CollectionOptions options = new CollectionOptions();
+    options.setSortBy(PersonService.TOP_FRIENDS_SORT);
+    options.setSortOrder(SortOrder.ascending);
+    options.setFilter(null);
+    options.setFilterOperation(FilterOperation.contains);
+    options.setFilterValue("");
+    options.setFirst(0);
+    options.setMax(20);
+
+    RestfulCollection<Person> responseItem = db.getPeople(
+        ImmutableSet.of(JOHN_DOE, JANE_DOE), new GroupId(GroupId.Type.friends, null),
+       options, Collections.<String>emptySet(), token).get();
+    assertNotNull(responseItem);
+    assertEquals(4, responseItem.getTotalResults());
+    // Test a couple of users
+    assertEquals("john.doe", responseItem.getList().get(0).getId());
+    assertEquals("jane.doe", responseItem.getList().get(1).getId());
+  }
+
+  @Test
+  public void testGetExpectedActivities() throws Exception {
+    RestfulCollection<Activity> responseItem = db.getActivities(
+        ImmutableSet.of(CANON_USER), SELF_GROUP, APP_ID, Collections.<String>emptySet(), null,
+        new FakeGadgetToken()).get();
+    assertSame(2, responseItem.getTotalResults());
+  }
+
+  @Test
+  public void testGetExpectedActivitiesForPlural() throws Exception {
+    RestfulCollection<Activity> responseItem = db.getActivities(
+        ImmutableSet.of(CANON_USER, JOHN_DOE), SELF_GROUP, APP_ID, Collections.<String>emptySet(), null,
+        new FakeGadgetToken()).get();
+    assertSame(3, responseItem.getTotalResults());
+  }
+
+  @Test
+  public void testGetExpectedActivity() throws Exception {
+    Activity activity = db.getActivity(
+        CANON_USER, SELF_GROUP, APP_ID,
+        ImmutableSet.of("appId", "body", "mediaItems"), APP_ID, new FakeGadgetToken()).get();
+    assertNotNull(activity);
+    // Check that some fields are fetched and others are not
+    assertNotNull(activity.getBody());
+    assertNull(activity.getBodyId());
+  }
+
+  @Test
+  public void testDeleteExpectedActivity() throws Exception {
+    db.deleteActivities(CANON_USER, SELF_GROUP, APP_ID, ImmutableSet.of(APP_ID),
+        new FakeGadgetToken());
+
+    // Try to fetch the activity
+    try {
+      db.getActivity(
+          CANON_USER, SELF_GROUP, APP_ID,
+          ImmutableSet.of("appId", "body", "mediaItems"), APP_ID, new FakeGadgetToken()).get();
+      fail();
+    } catch (ProtocolException sse) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST, sse.getCode());
+    }
+  }
+
+  @Test
+  public void testGetExpectedAppData() throws Exception {
+    DataCollection responseItem = db.getPersonData(
+        ImmutableSet.of(CANON_USER), SELF_GROUP, APP_ID, Collections.<String>emptySet(),
+        new FakeGadgetToken()).get();
+    assertFalse(responseItem.getEntry().isEmpty());
+    assertFalse(responseItem.getEntry().get(CANONICAL_USER_ID).isEmpty());
+
+    assertSame(2, responseItem.getEntry().get(CANONICAL_USER_ID).size());
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("count"));
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("size"));
+  }
+
+  @Test
+  public void testGetExpectedAppDataForPlural() throws Exception {
+    DataCollection responseItem = db.getPersonData(
+        ImmutableSet.of(CANON_USER, JOHN_DOE), SELF_GROUP, APP_ID, Collections.<String>emptySet(),
+        new FakeGadgetToken()).get();
+    assertFalse(responseItem.getEntry().isEmpty());
+    assertFalse(responseItem.getEntry().get(CANONICAL_USER_ID).isEmpty());
+
+    assertSame(2, responseItem.getEntry().get(CANONICAL_USER_ID).size());
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("count"));
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("size"));
+
+    assertFalse(responseItem.getEntry().get(JOHN_DOE.getUserId()).isEmpty());
+    assertSame(1, responseItem.getEntry().get(JOHN_DOE.getUserId()).size());
+    assertTrue(responseItem.getEntry().get(JOHN_DOE.getUserId()).containsKey("count"));
+  }
+
+  @Test
+  public void testDeleteExpectedAppData() throws Exception {
+    // Delete the data
+    db.deletePersonData(CANON_USER, SELF_GROUP, APP_ID,
+        ImmutableSet.of("count"), new FakeGadgetToken());
+
+    // Fetch the remaining and test
+    DataCollection responseItem = db.getPersonData(
+        ImmutableSet.of(CANON_USER), SELF_GROUP, APP_ID, Collections.<String>emptySet(),
+        new FakeGadgetToken()).get();
+    assertFalse(responseItem.getEntry().isEmpty());
+    assertFalse(responseItem.getEntry().get(CANONICAL_USER_ID).isEmpty());
+
+    assertSame(1, responseItem.getEntry().get(CANONICAL_USER_ID).size());
+    assertFalse(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("count"));
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("size"));
+  }
+
+  @Test
+  public void testUpdateExpectedAppData() throws Exception {
+    // Delete the data
+    db.updatePersonData(CANON_USER, SELF_GROUP, APP_ID,
+        null, ImmutableMap.of("count", (Object)"10", "newvalue", (Object)"20", "isValid", new Boolean(true)), new FakeGadgetToken());
+
+    // Fetch the remaining and test
+    DataCollection responseItem = db.getPersonData(
+        ImmutableSet.of(CANON_USER), SELF_GROUP, APP_ID, Collections.<String>emptySet(),
+        new FakeGadgetToken()).get();
+
+    assertFalse(responseItem.getEntry().isEmpty());
+    assertFalse(responseItem.getEntry().get(CANONICAL_USER_ID).isEmpty());
+
+    assertSame(4, responseItem.getEntry().get(CANONICAL_USER_ID).size());
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("count"));
+    assertEquals("10", responseItem.getEntry().get(CANONICAL_USER_ID).get("count"));
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("newvalue"));
+    assertEquals("20", responseItem.getEntry().get(CANONICAL_USER_ID).get("newvalue"));
+    assertTrue(responseItem.getEntry().get(CANONICAL_USER_ID).containsKey("isValid"));
+    assertEquals(true, Boolean.valueOf(responseItem.getEntry().get(CANONICAL_USER_ID).get("isValid")));
+  }
+
+  @Test
+  public void testGetExpectedActivityEntries() throws Exception {
+    RestfulCollection<ActivityEntry> responseItem = db.getActivityEntries(
+        ImmutableSet.of(JOHN_DOE), SELF_GROUP, APP_ID, Collections.<String>emptySet(), null,
+        new FakeGadgetToken()).get();
+    assertSame(3, responseItem.getTotalResults());
+  }
+
+  @Test
+  public void testGetExpectedActivityEntriesForPlural() throws Exception {
+    RestfulCollection<ActivityEntry> responseItem = db.getActivityEntries(
+        ImmutableSet.of(CANON_USER, JOHN_DOE), SELF_GROUP, APP_ID, Collections.<String>emptySet(), null,
+        new FakeGadgetToken()).get();
+    assertSame(3, responseItem.getTotalResults());
+  }
+
+  @Test
+  public void testGetExpectedActivityEntry() throws Exception {
+    ActivityEntry entry = db.getActivityEntry(JOHN_DOE, SELF_GROUP, APP_ID,
+        ImmutableSet.of("title"), "activity2", new FakeGadgetToken()).get();
+    assertNotNull(entry);
+    // Check that some fields are fetched and others are not
+    assertNotNull(entry.getTitle());
+    assertNull(entry.getPublished());
+  }
+
+  @Test
+  public void testDeleteExpectedActivityEntry() throws Exception {
+    db.deleteActivityEntries(JOHN_DOE, SELF_GROUP, APP_ID, ImmutableSet.of(APP_ID),
+        new FakeGadgetToken());
+
+    // Try to fetch the activity
+    try {
+      db.getActivityEntry(
+          JOHN_DOE, SELF_GROUP, APP_ID,
+          ImmutableSet.of("body"), APP_ID, new FakeGadgetToken()).get();
+      fail();
+    } catch (ProtocolException sse) {
+      assertEquals(HttpServletResponse.SC_BAD_REQUEST, sse.getCode());
+    }
+  }
+
+  @Test
+  public void testViewerCanUpdatePerson() throws Exception {
+    // Create new user
+    JSONArray people = db.getDb().getJSONArray("people");
+    JSONObject jsonPerson = new JSONObject();
+    jsonPerson.put("id", "updatePerson");
+    people.put(people.length(),jsonPerson);
+
+    SecurityToken updateToken = new FakeGadgetToken("appId", "appUrl", "domain", "updatePerson", "trustedJson", "updatePerson", "20");
+
+    // Get user
+    UserId userId = new UserId(UserId.Type.userId, "updatePerson");
+    Person person = db
+        .getPerson(userId, Person.Field.ALL_FIELDS, token).get();
+    assertNotNull("User 'updatePerson' not found", person);
+
+    // update a field in user object
+    person.setThumbnailUrl("http://newthumbnail.url");
+    // Save user to db
+    db.updatePerson(userId, person, updateToken);
+    // Get user again from db and check if the fields were properly updated
+    person = db.getPerson(userId, Person.Field.ALL_FIELDS, token).get();
+    assertNotNull("User 'updatePerson' not found", person);
+
+    assertEquals("http://newthumbnail.url", person.getThumbnailUrl());
+  }
+
+  @Test
+  public void testViewerNotAllowedUpdatePerson() throws Exception {
+    // Create new user
+    JSONArray people = db.getDb().getJSONArray("people");
+    JSONObject jsonPerson = new JSONObject();
+    jsonPerson.put("id", "updatePerson");
+    people.put(people.length(),jsonPerson);
+
+    SecurityToken updateToken = new FakeGadgetToken("appId", "appUrl", "domain", "viewer", "trustedJson", "viewer", "20");
+
+    // Get user
+    UserId userId = new UserId(UserId.Type.userId, "updatePerson");
+    Person person = db
+        .getPerson(userId, Person.Field.ALL_FIELDS, token).get();
+
+    // update a field in user object
+    person.setThumbnailUrl("http://newthumbnail.url");
+    // Save user to db, should throw an exception
+    try {
+      db.updatePerson(userId, person, updateToken);
+      fail();
+    } catch (ProtocolException sse) {
+      assertEquals(HttpServletResponse.SC_FORBIDDEN, sse.getCode());
+    }
+  }
+
+}
diff --git a/trunk/java/social-api/src/test/resources/log4j.properties b/trunk/java/social-api/src/test/resources/log4j.properties
new file mode 100644
index 0000000..4ab4883
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/log4j.properties
@@ -0,0 +1,23 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+log4j.rootCategory=info
+log4j.rootLogger=info, stdout
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern= %p %m  [%d] (%F:%L) %n
diff --git a/trunk/java/social-api/src/test/resources/logging.properties b/trunk/java/social-api/src/test/resources/logging.properties
new file mode 100644
index 0000000..36e28c2
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/logging.properties
@@ -0,0 +1,25 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+handlers=java.util.logging.ConsoleHandler
+java.util.logging.ConsoleHandler.level=ALL
+java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter
+.level=INFO
+
+# Shindig commons
+org.apache.shindig.common.JsonContainerConfig.level=${java.util.logging.test.level}
+org.apache.shindig.common.xml.XmlUtil.level=${java.util.logging.test.level}
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/opensocial.xsd b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/opensocial.xsd
new file mode 100644
index 0000000..b0af09e
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/opensocial.xsd
@@ -0,0 +1,487 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<xs:schema xmlns:tns="http://ns.opensocial.org/2008/opensocial"
+  elementFormDefault="qualified"
+  targetNamespace="http://ns.opensocial.org/2008/opensocial"
+  xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+  <xs:element name="person" type="tns:Person" />
+  <xs:element name="group" type="tns:Group" />
+  <xs:element name="activity" type="tns:Activity" />
+  <xs:element name="appdata" type="tns:Appdata" />
+  
+  <xs:element name="response" type="tns:Response" />
+
+  <xs:complexType name="Response">
+    <xs:choice minOccurs="0" maxOccurs="unbounded" >
+      <xs:element minOccurs="0" name="group" type="tns:Group" />
+      <xs:element minOccurs="0" name="activity" type="tns:Activity" />
+      <xs:element minOccurs="0" name="person" type="tns:Person" />
+
+      <xs:group ref="tns:collection" />
+      <xs:element minOccurs="0" name="invalidationKeys" type="tns:InvalidationKeys" />
+
+      <!--  this is to allow responses to create to validate -->
+      <xs:element minOccurs="0" name="map" type="xs:anyType" />
+      <xs:element minOccurs="0" name="list.container" type="xs:anyType" />
+    </xs:choice>
+  </xs:complexType>
+
+  <!-- update for OpenSocial latest XSD -->
+  <!-- (see http://opensocial-resources.googlecode.com/svn/spec/trunk/Core-API-Server.xml#XML_format_XSD) -->
+  <xs:group name="collection">
+    <xs:sequence>
+      <xs:element minOccurs="0" name="itemsPerPage" type="xs:int"/>
+      <xs:element minOccurs="0" name="startIndex" type="xs:long"/>
+      <xs:element minOccurs="0" name="totalResults" type="xs:long"/>
+      <xs:element minOccurs="0" name="filtered" type="xs:boolean"/>
+      <xs:element minOccurs="0" name="sorted" type="xs:boolean"/>
+      <xs:element minOccurs="0" name="updatedSince" type="xs:boolean"/>
+      <xs:element name="list" type="tns:CollectionList" />
+    </xs:sequence>
+  </xs:group>
+
+  <xs:complexType name="InvalidationKeys">
+    <xs:sequence>
+      <xs:element name="invalidationKey" type="xs:string" maxOccurs="unbounded"/>
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="CollectionList" >
+    <xs:sequence>
+      <xs:element name="entry" type="tns:Entry" minOccurs="0" maxOccurs="unbounded"/>
+    </xs:sequence>
+  </xs:complexType>
+
+  <xs:complexType name="Entry" >
+    <xs:choice>
+      <xs:element minOccurs="0" name="activity" type="tns:Activity" />
+      <xs:element minOccurs="0" name="person" type="tns:Person" />
+      <xs:element minOccurs="0" name="group" type="tns:Group" />
+    </xs:choice>
+  </xs:complexType>
+    
+  <xs:complexType name="Activity">
+    <xs:choice minOccurs="0" maxOccurs="unbounded">
+      <xs:element minOccurs="0" name="appId" type="xs:string" />
+      <xs:element minOccurs="0" name="body" type="xs:string" />
+      <xs:element minOccurs="0" name="bodyId" type="xs:string" />
+      <xs:element minOccurs="0" name="externalId" type="xs:string" />
+      <xs:element minOccurs="0" name="id" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="mediaItems" type="tns:MediaItem" />
+      <xs:element minOccurs="0" name="postedTime" type="xs:long" />
+      <xs:element minOccurs="0" name="priority" type="xs:double" />
+      <xs:element minOccurs="0" name="streamFaviconUrl" type="xs:string" />
+      <xs:element minOccurs="0" name="streamSourceUrl" type="xs:string" />
+      <xs:element minOccurs="0" name="streamTitle" type="xs:string" />
+      <xs:element minOccurs="0" name="streamUrl" type="xs:string" />
+      <xs:element minOccurs="0" name="templateParams" type="tns:ActivityTemplateParams" />
+      <xs:element minOccurs="0" name="title" type="xs:string" />
+      <xs:element minOccurs="0" name="titleId" type="xs:string" />
+      <xs:element minOccurs="0" name="updated" type="xs:dateTime"/>
+      <xs:element minOccurs="0" name="url" type="xs:string" />
+      <xs:element minOccurs="0" name="userId" type="xs:string" />
+    </xs:choice>
+  </xs:complexType>
+  
+  
+  <xs:complexType name="ActivityTemplateParams">
+    <xs:all>
+      <xs:element minOccurs="0" name="PersonKey" type="xs:string" />
+      <xs:element minOccurs="0" name="PersonKey.DisplayName" type="xs:string" />
+      <xs:element minOccurs="0" name="PersonKey.Id" type="xs:string" />
+      <xs:element minOccurs="0" name="PersonKey.ProfileUrl" type="xs:string" />
+      <xs:element minOccurs="0" name="person" type="tns:Person" />
+    </xs:all>
+  </xs:complexType>
+  
+  <xs:complexType name="Person">
+    <xs:choice minOccurs="1" maxOccurs="unbounded">
+      <xs:element minOccurs="0" name="aboutMe" type="xs:string" />
+      <xs:element minOccurs="0" name="accounts" type="tns:Account"/>
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="activities" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="addresses" type="tns:Address" />
+      <xs:element minOccurs="0" name="age" type="xs:string"/>
+      <xs:element minOccurs="0" name="anniversary" type="xs:dateTime" />
+      <xs:element minOccurs="0" name="appData" type="tns:Appdata"/>
+      <xs:element minOccurs="0" name="birthday" type="xs:dateTime" />
+      <xs:element minOccurs="0" name="bodyType" type="tns:BodyType" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="books" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="cars" type="xs:string" />
+      <xs:element minOccurs="0" name="children" type="xs:string" />
+      <xs:element minOccurs="0" name="connected" type="tns:Presence" />
+      <xs:element minOccurs="0" name="currentLocation" type="tns:Address" />
+      <xs:element minOccurs="0" name="displayName" type="xs:string" />
+      <xs:element minOccurs="0" name="drinker" type="tns:Drinker" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="emails" type="tns:PluralPersonField" />
+      <xs:element minOccurs="0" name="ethnicity" type="xs:string" />
+      <xs:element minOccurs="0" name="fashion" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="food" type="xs:string" />
+      <xs:element minOccurs="0" name="gender" type="xs:string" />
+      <xs:element minOccurs="0" name="happiestWhen" type="xs:string" />
+      <xs:element minOccurs="0" name="hasApp" type="xs:boolean" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="heroes" type="xs:string" />
+      <xs:element minOccurs="0" name="humor" type="xs:string" />
+      <xs:element minOccurs="0" name="id" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="ims" type="tns:PluralPersonField"/>
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="interests" type="xs:string" />
+      <xs:element minOccurs="0" name="jobInterests" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="languagesSpoken" type="xs:string" />
+      <xs:element minOccurs="0" name="livingArrangement" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="lookingFor" type="tns:LookingFor" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="movies" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="music" type="xs:string" />
+      <xs:element minOccurs="0" name="name" type="tns:Name" />
+      <xs:element minOccurs="0" name="networkPresence" type="tns:NetworkPresence" />
+      <xs:element minOccurs="0" name="nickname" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="organizations" type="tns:Organization" />
+      <xs:element minOccurs="0" name="pets" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="phoneNumbers" type="tns:PluralPersonField" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="photos" type="tns:PluralPersonField" />
+      <xs:element minOccurs="0" name="politicalViews" type="xs:string" />
+      <xs:element minOccurs="0" name="preferredUsername" type="xs:string" />
+      <xs:element minOccurs="0" name="profileSong" type="tns:Url" />
+      <xs:element minOccurs="0" name="profileUrl" type="xs:string" />
+      <xs:element minOccurs="0" name="profileVideo" type="tns:Url" />
+      <xs:element minOccurs="0" name="published" type="xs:dateTime"/>
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="quotes" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="relationships" type="xs:string" />
+      <xs:element minOccurs="0" name="relationshipStatus" type="xs:string" />
+      <xs:element minOccurs="0" name="religion" type="xs:string" />
+      <xs:element minOccurs="0" name="romance" type="xs:string" />
+      <xs:element minOccurs="0" name="scaredOf" type="xs:string" />
+      <xs:element minOccurs="0" name="sexualOrientation" type="xs:string" />
+      <xs:element minOccurs="0" name="smoker" type="tns:Smoker" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="sports" type="xs:string" />
+      <xs:element minOccurs="0" name="status" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="tags" type="xs:string" />
+      <xs:element minOccurs="0" name="thumbnailUrl" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="turnOffs" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="turnOns" type="xs:string" />
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="tvShows" type="xs:string" />
+      <xs:element minOccurs="0" name="updated" type="xs:dateTime"/>
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="urls" type="tns:Url" />
+      <xs:element minOccurs="0" name="utcOffset" type="xs:int" />
+    </xs:choice>
+  </xs:complexType>
+  
+  <xs:complexType name="Group">
+    <xs:all>
+      <xs:element minOccurs="0" name="id" type="xs:string" />
+      <xs:element minOccurs="0" name="title" type="xs:string" />
+    </xs:all>
+  </xs:complexType>
+  <xs:complexType name="AppdataEntry" mixed="true">
+    <xs:all>
+      <xs:element minOccurs="1" name="key" type="xs:string" />
+      <xs:element minOccurs="1" name="value"  type="xs:anyType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:complexType name="Appdata">
+    <xs:sequence>
+      <xs:element minOccurs="0" maxOccurs="unbounded" name="entry" type="tns:AppdataEntry" />
+    </xs:sequence>
+  </xs:complexType>
+  
+  <xs:complexType name="BodyType">
+    <xs:all>
+      <xs:element minOccurs="0" name="build" type="xs:string" />
+      <xs:element minOccurs="0" name="eyeColor" type="xs:string" />
+      <xs:element minOccurs="0" name="hairColor" type="xs:string" />
+      <xs:element minOccurs="0" name="height" type="xs:double" />
+      <xs:element minOccurs="0" name="weight" type="xs:double" />
+    </xs:all>
+  </xs:complexType>
+  
+  <xs:complexType name="Address">
+    <xs:all>
+      <xs:element minOccurs="0" name="country" type="xs:string" />
+      <xs:element minOccurs="0" name="extendedAddress" type="xs:string" />
+      <xs:element minOccurs="0" name="latitude" type="xs:double" />
+      <xs:element minOccurs="0" name="locality" type="xs:string" />
+      <xs:element minOccurs="0" name="longitude" type="xs:double" />
+      <xs:element minOccurs="0" name="poBox" type="xs:string" />
+      <xs:element minOccurs="0" name="postalCode" type="xs:string" />
+      <xs:element minOccurs="0" name="primary" type="xs:boolean"/>
+      <xs:element minOccurs="0" name="region" type="xs:string" />
+      <xs:element minOccurs="0" name="streetAddress" type="xs:string" />
+      <xs:element minOccurs="0" name="type" type="xs:string" />
+      <xs:element minOccurs="0" name="formatted" type="xs:string" />
+    </xs:all>
+  </xs:complexType>
+  
+  
+  <xs:complexType name="Account">
+    <xs:all>
+      <xs:element minOccurs="0" name="domain" type="xs:string" />
+      <xs:element minOccurs="0" name="primary" type="xs:boolean"/>
+      <xs:element minOccurs="0" name="userid" type="xs:string" />
+      <xs:element minOccurs="0" name="username" type="xs:string" />
+    </xs:all>
+  </xs:complexType> 
+  
+  
+  <xs:complexType name="Organization">
+    <xs:all>
+      <xs:element minOccurs="0" name="address" type="tns:Address" />
+      <xs:element minOccurs="0" name="department" type="xs:string" />
+      <xs:element minOccurs="0" name="description" type="xs:string" />
+      <xs:element minOccurs="0" name="endDate" type="xs:dateTime" />
+      <xs:element minOccurs="0" name="name" type="xs:string" />
+      <xs:element minOccurs="0" name="startDate" type="xs:dateTime" />
+      <xs:element minOccurs="0" name="type" type="xs:string" />
+      <xs:element minOccurs="0" name="title" type="xs:string" />
+      <xs:element minOccurs="0" name="field" type="xs:string" />
+      <xs:element minOccurs="0" name="subField" type="xs:string" />
+      <xs:element minOccurs="0" name="webpage" type="xs:string" />
+      <xs:element minOccurs="0" name="salary" type="xs:string" />
+   
+    </xs:all>
+  </xs:complexType>
+  <xs:complexType name="Name">
+    <xs:all>
+      <xs:element minOccurs="0" name="additionalName" type="xs:string" />
+      <xs:element minOccurs="0" name="familyName" type="xs:string" />
+      <xs:element minOccurs="0" name="givenName" type="xs:string" />
+      <xs:element minOccurs="0" name="honorificPrefix" type="xs:string" />
+      <xs:element minOccurs="0" name="honorificSuffix" type="xs:string" />
+      <xs:element minOccurs="0" name="formatted" type="xs:string" />
+    </xs:all>
+  </xs:complexType>
+  
+  <xs:complexType name="Url">
+    <xs:all>
+      <xs:element minOccurs="0" name="value" type="xs:string" />
+      <xs:element minOccurs="0" name="linkText" type="xs:string" />
+      <xs:element minOccurs="0" name="type" type="xs:string" />
+    </xs:all>
+  </xs:complexType>
+
+  <xs:complexType name="Album">
+    <xs:all>
+      <xs:element minOccurs="0" name="id" type="xs:string" />
+      <xs:element minOccurs="0" name="thumbnailUrl" type="xs:string" />
+      <xs:element minOccurs="0" name="title" type="xs:string" />
+      <xs:element minOccurs="0" name="description" type="xs:string" />
+      <xs:element minOccurs="0" name="location" type="tns:Address" />
+      <xs:element minOccurs="0" name="ownerId" type="xs:string" />
+      <xs:element minOccurs="0" name="mediaType" type="tns:MediaItemType" />
+      <xs:element minOccurs="0" name="mediaMimeType" type="xs:string" />
+      <xs:element minOccurs="0" name="mediaItemCount" type="xs:integer" />
+    </xs:all>
+  </xs:complexType>
+  
+  <xs:complexType name="MediaItem">
+    <xs:all>
+      <xs:element minOccurs="0" name="mimeType" type="xs:string" />
+      <xs:element minOccurs="0" name="type" type="tns:MediaItemType" />
+      <xs:element minOccurs="0" name="url" type="xs:string" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="MediaItemType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="AUDIO" />
+      <xs:enumeration value="IMAGE" />
+      <xs:enumeration value="VIDEO" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  
+  <xs:complexType name="Drinker">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:DrinkerType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="DrinkerType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="HEAVILY" />
+      <xs:enumeration value="NO" />
+      <xs:enumeration value="OCCASIONALLY" />
+      <xs:enumeration value="QUIT" />
+      <xs:enumeration value="QUITTING" />
+      <xs:enumeration value="REGULARLY" />
+      <xs:enumeration value="SOCIALLY" />
+      <xs:enumeration value="YES" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  
+  <xs:complexType name="Presence">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:PresenceType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="PresenceType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="AWAY" />
+      <xs:enumeration value="CHAT" />
+      <xs:enumeration value="DND" />
+      <xs:enumeration value="OFFLINE" />
+      <xs:enumeration value="ONLINE" />
+      <xs:enumeration value="XA" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  
+  <xs:complexType name="Smoker">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:SmokerType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="SmokerType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="HEAVILY" />
+      <xs:enumeration value="NO" />
+      <xs:enumeration value="OCCASIONALLY" />
+      <xs:enumeration value="QUIT" />
+      <xs:enumeration value="QUITTING" />
+      <xs:enumeration value="REGULARLY" />
+      <xs:enumeration value="SOCIALLY" />
+      <xs:enumeration value="YES" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  <xs:complexType name="CreateActivityPriority">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:CreateActivityPriorityType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="CreateActivityPriorityType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="HIGH" />
+      <xs:enumeration value="LOW" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  <xs:complexType name="EscapeType">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:EscapeTypeType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="EscapeTypeType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="HTML_ESCAPE" />
+      <xs:enumeration value="NONE" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  
+  
+  <xs:complexType name="Message">
+    <xs:all>
+      <xs:element minOccurs="0" name="body" type="xs:string" />
+      <xs:element minOccurs="0" name="bodyId" type="xs:string" />
+      <xs:element minOccurs="0" name="title" type="xs:string" />
+      <xs:element minOccurs="0" name="titleId" type="xs:string" />
+    </xs:all>
+  </xs:complexType>
+  <xs:complexType name="MessageType">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:MessageTypeType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="MessageTypeType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="EMAIL" />
+      <xs:enumeration value="NOTIFICATION" />
+      <xs:enumeration value="PRIVATE_MESSAGE" />
+      <xs:enumeration value="PUBLIC_MESSAGE" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  
+  
+  <xs:complexType name="Environment">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:EnvironmentType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="EnvironmentType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="ACTIVITY" />
+      <xs:enumeration value="ADDRESS" />
+      <xs:enumeration value="BODY_TYPE" />
+      <xs:enumeration value="EMAIL" />
+      <xs:enumeration value="FILTER_TYPE" />
+      <xs:enumeration value="MEDIAITEM" />
+      <xs:enumeration value="MESSAGE" />
+      <xs:enumeration value="MESSAGE_TYPE" />
+      <xs:enumeration value="NAME" />
+      <xs:enumeration value="ORGANIZATION" />
+      <xs:enumeration value="PERSON" />
+      <xs:enumeration value="PHONE" />
+      <xs:enumeration value="SORTORDER" />
+      <xs:enumeration value="URL" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  
+  
+  
+  <xs:complexType name="LookingFor">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:LookingForType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="LookingForType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="ACTIVITY_PARTNERS" />
+      <xs:enumeration value="DATING" />
+      <xs:enumeration value="FRIENDS" />
+      <xs:enumeration value="NETWORKING" />
+      <xs:enumeration value="RANDOM" />
+      <xs:enumeration value="RELATIONSHIP" />
+    </xs:restriction>
+  </xs:simpleType>
+  <xs:complexType name="NetworkPresence">
+    <xs:all>
+      <xs:element minOccurs="0" name="displayValue" type="xs:string" />
+      <xs:element minOccurs="0" name="value" type="tns:NetworkPresenceType" />
+    </xs:all>
+  </xs:complexType>
+  <xs:simpleType name="NetworkPresenceType">
+    <xs:restriction base="xs:string">
+      <xs:enumeration value="AWAY" />
+      <xs:enumeration value="CHAT" />
+      <xs:enumeration value="DND" />
+      <xs:enumeration value="OFFLINE" />
+      <xs:enumeration value="ONLINE" />
+      <xs:enumeration value="XA" />
+    </xs:restriction>
+  </xs:simpleType>
+  
+  <xs:complexType name="PluralPersonField">
+    <xs:all>
+      <xs:element minOccurs="0" name="value" type="xs:string" />
+      <xs:element minOccurs="0" name="type" type="xs:string" />
+      <xs:element minOccurs="0" name="primary" type="xs:boolean"/>
+    </xs:all>
+  </xs:complexType>
+  
+  
+</xs:schema>
diff --git a/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testschema1.xml b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testschema1.xml
new file mode 100644
index 0000000..8d69066
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testschema1.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<person xmlns="http://ns.opensocial.org/2008/opensocial"
+   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://ns.opensocial.org/2008/opensocial classpath:opensocial.xsd">
+  <aboutMe></aboutMe>
+  <accounts>
+    <domain>Domain</domain>
+    <primary>true</primary>
+    <userid>useid</userid>
+    <username>username</username>
+  </accounts>
+  <activities>Activity1</activities>
+  <anniversary>2001-10-26T21:32:52</anniversary>
+  <birthday>2001-10-26T21:32:52</birthday>
+  <bodyType></bodyType>
+  <books></books>
+  <cars></cars>
+  <children></children>
+  <connected></connected>
+  <currentLocation></currentLocation>
+  <displayName></displayName>
+  <drinker></drinker>
+  <emails></emails>
+  <ethnicity></ethnicity>
+  <fashion></fashion>
+  <food></food>
+  <gender></gender>
+  <happiestWhen></happiestWhen>
+  <hasApp>false</hasApp>
+  <heroes></heroes>
+  <humor></humor>
+  <id></id>
+  <ims></ims>
+  <interests></interests>
+  <jobInterests></jobInterests>
+  <languagesSpoken></languagesSpoken>
+  <livingArrangement></livingArrangement>
+  <lookingFor></lookingFor>
+  <movies></movies>
+  <music></music>
+  <name></name>
+  <nickname></nickname>
+  <organizations></organizations>
+  <pets></pets>
+  <phoneNumbers></phoneNumbers>
+  <photos></photos>
+  <politicalViews></politicalViews>
+  <preferredUsername></preferredUsername>
+  <profileSong></profileSong>
+  <profileUrl></profileUrl>
+  <profileVideo></profileVideo>
+  <published>2001-10-26T21:32:52</published>
+  <quotes></quotes>
+  <relationshipStatus></relationshipStatus>
+</person>
+  
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testschema1bad.xml b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testschema1bad.xml
new file mode 100644
index 0000000..b26e9f7
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testschema1bad.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<person xmlns="http://ns.opensocial.org/2008/opensocial"
+   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://ns.opensocial.org/2008/opensocial classpath:opensocial.xsd">
+  <aboutMe></aboutMe>
+  <accounts>
+  <!--  ???? accounts is plural but there is only 1 according to the xsd -->
+    <domain>Domain</domain>
+    <primary>true</primary>
+    <userid>useid</userid>
+    <username>username</username>
+  </accounts>
+  <activities>
+    <activity>Activity1</activity>    
+  </activities>
+  <anniversary>INTENTIONAL INVALID DATE</anniversary>
+  <birthday>2001-10-26T21:32:52</birthday>
+  <bodyType></bodyType>
+  <books></books>
+  <cars></cars>
+  <children></children>
+  <connected></connected>
+  <currentLocation></currentLocation>
+  <displayName></displayName>
+  <drinker></drinker>
+  <emails></emails>
+  <ethnicity></ethnicity>
+  <fashion></fashion>
+  <food></food>
+  <gender></gender>
+  <happiestWhen></happiestWhen>
+  <hasApp>false</hasApp>
+  <heroes></heroes>
+  <humor></humor>
+  <id></id>
+  <ims></ims>
+  <interests></interests>
+  <jobInterests></jobInterests>
+  <languagesSpoken></languagesSpoken>
+  <livingArrangement></livingArrangement>
+  <lookingFor></lookingFor>
+  <movies></movies>
+  <music></music>
+  <name></name>
+  <nickname></nickname>
+  <organizations></organizations>
+  <pets></pets>
+  <phoneNumbers></phoneNumbers>
+  <photos></photos>
+  <politicalViews></politicalViews>
+  <preferredUsername></preferredUsername>
+  <profileSong></profileSong>
+  <profileUrl></profileUrl>
+  <profileVideo></profileVideo>
+  <published>2001-10-26T21:32:52</published>
+  <quotes></quotes>
+  <relationshipStatus></relationshipStatus>
+</person>
+  
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/activity1.xml b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/activity1.xml
new file mode 100644
index 0000000..acbf1f7
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/activity1.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<activity xmlns="http://ns.opensocial.org/2008/opensocial">
+  <body>Some details for some activity</body>
+  <bodyId>383777272</bodyId>
+  <id>http://example.org/activities/example.org:87ead8dead6beef/self/af3778</id>
+  <postedTime>12312312312312</postedTime> 
+  <title>&lt;a href=\"foo\"&gt;some activity&lt;/a&gt;</title>
+  <url>http://api.example.org/activity/feeds/.../af3778</url>
+  <userId>example.org:34KJDCSKJN2HHF0DW20394</userId>
+</activity>
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/appdata1.xml b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/appdata1.xml
new file mode 100644
index 0000000..a3b3f45
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/appdata1.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<appdata xmlns="http://ns.opensocial.org/2008/opensocial">
+  <entry>
+    <key>pokes</key>
+    <value>3</value>
+  </entry>
+  <entry>
+    <key>last_poke</key>
+    <value>2008-02-13T18:30:02Z</value>
+  </entry>
+</appdata>
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/group1.xml b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/group1.xml
new file mode 100644
index 0000000..d8bc7d6
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/group1.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<group xmlns="http://ns.opensocial.org/2008/opensocial">
+  <id>example.org:34KJDCSKJN2HHF0DW20394/friends</id>
+  <title>Peeps</title>
+</group>
+
diff --git a/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/person1.xml b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/person1.xml
new file mode 100644
index 0000000..07ae710
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/org/apache/shindig/social/opensocial/util/testxml/person1.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<person xmlns="http://ns.opensocial.org/2008/opensocial">
+  <displayName></displayName>
+  <gender>female</gender>
+  <id></id>
+  <name>
+    <formatted>Jane Doe</formatted>
+  </name>
+</person>
\ No newline at end of file
diff --git a/trunk/java/social-api/src/test/resources/simplelog.properties b/trunk/java/social-api/src/test/resources/simplelog.properties
new file mode 100644
index 0000000..4ab4883
--- /dev/null
+++ b/trunk/java/social-api/src/test/resources/simplelog.properties
@@ -0,0 +1,23 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+log4j.rootCategory=info
+log4j.rootLogger=info, stdout
+
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern= %p %m  [%d] (%F:%L) %n
diff --git a/trunk/java/uber/pom.xml b/trunk/java/uber/pom.xml
new file mode 100644
index 0000000..39efe5f
--- /dev/null
+++ b/trunk/java/uber/pom.xml
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-uber</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>jar</packaging>
+
+  <name>Apache Shindig Uber Jar</name>
+  <description>Uber Jar contains Shindig and dependencies</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/java/uber</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/java/uber</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/java/uber</url>
+  </scm>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>1.4</version>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <artifactSet>
+                <includes>
+                  <include>org.apache.shindig:*</include>
+                  <include>com.google.guava:*</include>
+                </includes>
+              </artifactSet>
+              <relocations>
+                <relocation>
+                  <pattern>com.google.common</pattern>
+                  <shadedPattern>org.apache.shindig.internal.cgc</shadedPattern>
+                </relocation>
+              </relocations>
+            </configuration>
+          </execution>
+        </executions>
+        <configuration>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <!-- project dependencies -->
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-common</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-gadgets</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-social-api</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-features</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.shindig</groupId>
+      <artifactId>shindig-extras</artifactId>
+      <classifier>${shindig.jdk.classifier}</classifier>
+      <version>${project.version}</version>
+    </dependency>
+
+    <!-- external dependencies -->
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-multibindings</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>xml-apis</groupId>
+      <artifactId>xml-apis</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-jdk14</artifactId>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/trunk/java/uber/src/main/appended-resources/META-INF/LICENSE b/trunk/java/uber/src/main/appended-resources/META-INF/LICENSE
new file mode 100644
index 0000000..1d7125f
--- /dev/null
+++ b/trunk/java/uber/src/main/appended-resources/META-INF/LICENSE
@@ -0,0 +1,64 @@
+===============================================================================
+
+The Apache Shindig distribution includes a number of subcomponents
+with separate copyright notices and license terms. Your use of the
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+===============================================================================
+OpenSocial Specification 0.8:
+
+Copyright (c) 2008 OpenSocial Foundation (http://www.opensocial.org)
+Released under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+===============================================================================
+Code Mirror:
+ Copyright (c) 2007-2010 Marijn Haverbeke
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any
+ damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any
+ purpose, including commercial applications, and to alter it and
+ redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software. If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+    distribution.
+
+===============================================================================
+swfobject:
+
+The MIT License
+
+Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
+
+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.
+
+
+ Marijn Haverbeke
+ marijnh@gmail.com
+
diff --git a/trunk/java/uber/src/main/appended-resources/META-INF/NOTICE b/trunk/java/uber/src/main/appended-resources/META-INF/NOTICE
new file mode 100644
index 0000000..76b129f
--- /dev/null
+++ b/trunk/java/uber/src/main/appended-resources/META-INF/NOTICE
@@ -0,0 +1,8 @@
+This product includes software (Gadget Server, Gadget Container)
+originally developed by Google Inc. (http://code.google.com/) and licensed
+to the ASF as initial contribution for Shindig.
+
+This product contains software (sha1 JS impl) developed by Google Inc.
+
+This product includes software (wave) developed by Google, Inc
+Copyright 2010 Google Inc.
diff --git a/trunk/php/.htaccess b/trunk/php/.htaccess
new file mode 100644
index 0000000..a634f2c
--- /dev/null
+++ b/trunk/php/.htaccess
@@ -0,0 +1,28 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+
+<IfModule mod_rewrite.c>
+        RewriteEngine On
+	RewriteCond %{REQUEST_FILENAME} !-f
+	RewriteCond %{REQUEST_FILENAME} !-d
+        # NOTE: If you added a web_prefix to config, add it here too, e.g.:
+        #RewriteRule (.*) /shindig/php/index.php [L]
+        RewriteRule (.*) index.php [L]
+	# for OAuth signatures to work for POSTed data, always_populate_raw_data needs to be turned on
+	php_flag always_populate_raw_post_data On
+        php_flag magic_quotes_gpc Off
+</IfModule>
diff --git a/trunk/php/LICENSE b/trunk/php/LICENSE
new file mode 100644
index 0000000..43811be
--- /dev/null
+++ b/trunk/php/LICENSE
@@ -0,0 +1,417 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
+
+
+===============================================================================
+
+The Apache Shindig distribution includes a number of subcomponents
+with separate copyright notices and license terms. Your use of the
+code for the these subcomponents is subject to the terms and
+conditions of the following licenses.
+
+===============================================================================
+Zend Framework:
+
+Copyright (c) 2006-2007, Zend Technologies USA, Inc.
+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 Zend Technologies USA, Inc. 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 COPYRIGHT HOLDERS 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 COPYRIGHT OWNER OR 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.
+
+===============================================================================
+PHPUnit:
+
+Copyright (c) 2002-2008, Sebastian Bergmann <sb@sebastian-bergmann.de>.
+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 Sebastian Bergmann nor the names of his
+    contributors may be used to endorse or promote products derived
+    from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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
+COPYRIGHT OWNER OR 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.
+
+===============================================================================
+jsmin.php - PHP implementation of Douglas Crockford's JSMin.
+
+This is pretty much a direct port of jsmin.c to PHP with just a few
+PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
+outputs to stdout, this library accepts a string as input and returns another
+string as output.
+
+PHP 5 or higher is required.
+
+Permission is hereby granted to use this version of the library under the
+same terms as jsmin.c, which has the following license:
+
+--
+Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
+
+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 shall be used for Good, not Evil.
+
+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.
+
+===============================================================================
+OpenSocial Specification 0.8:
+
+Copyright (c) 2008 OpenSocial Foundation (http://www.opensocial.org)
+Released under the Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
+===============================================================================
+Code Mirror:
+ Copyright (c) 2007-2010 Marijn Haverbeke
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any
+ damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any
+ purpose, including commercial applications, and to alter it and
+ redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software. If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+    distribution.
+
+===============================================================================
+swfobject:
+
+The MIT License
+
+Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
+
+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.
+
+
+ Marijn Haverbeke
+ marijnh@gmail.com
+
+===============================================================================
+OAuth.php:
+
+The MIT License
+
+Copyright (c) 2007 Andy Smith
+
+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.
+
+===============================================================================
+Symphony ClassLoader:
+
+Copyright (c) 2004-2011 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/trunk/php/NOTICE b/trunk/php/NOTICE
new file mode 100644
index 0000000..5da404e
--- /dev/null
+++ b/trunk/php/NOTICE
@@ -0,0 +1,16 @@
+Apache Shindig
+Copyright 2012 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+-----------------------------------------------------------
+This product includes software (Gadget Server, Gadget Container)
+originally developed by Google Inc. (http://code.google.com/) and licensed
+to the ASF as initial contribution for Shindig.
+
+This product contains software (sha1 JS impl) developed by Google Inc.
+
+This product includes software (Symfony ClassLoader) developed by
+Fabien Potencier (https://github.com/symfony/ClassLoader)
+
diff --git a/trunk/php/README b/trunk/php/README
new file mode 100644
index 0000000..8e605a3
--- /dev/null
+++ b/trunk/php/README
@@ -0,0 +1,154 @@
+                          Apache Shindig PHP
+
+  What is it?
+  -----------
+
+  Shindig is a JavaScript container and implementations of the backend APIs
+  and proxy required for hosting OpenSocial applications.
+
+  This is the PHP implementation of Shindig. If you are looking to the Java 
+  implementation, please visit our website.
+
+  Documentation
+  -------------
+
+  The most up-to-date documentation can be found at http://shindig.apache.org/
+  and at http://shindig.apache.org/developers/php/index.html 
+  for specific PHP documentation.
+
+  Read javascript/README for instructions for using the Shindig Gadget 
+  Container JavaScript to enable your page to render Gadgets.
+
+  Release Notes
+  -------------
+
+  The full list of changes can be found at https://issues.apache.org/jira/browse/SHINDIG.
+
+  System Requirements
+  -------------------
+
+  PHP:
+    5.2.x or above with the json, simplexml, mcrypt and curl extentions 
+    enabled.
+  Web server:
+    Apache with mod_rewrite enabled.
+  Memory:
+    No minimum requirement.
+  Disk:
+    No minimum requirement. 
+  Operating System:
+    No minimum requirement. On Windows, Windows NT and above or Cygwin is 
+    required for the startup scripts. Tested on Windows XP, Fedora Core 
+    and Mac OS X.
+
+  Installing Shindig
+  ------------------
+
+  Unzip the distribution archive, i.e. shindig-${project.version}-php.zip to 
+  the web document root, e.g. /var/www/html. 
+
+  Rename the created shindig-${project.version}-php dir to shindig. 
+
+  a. Create a new virtual host
+
+  Point your Apache to the shindig dir with a virtual host like:
+
+  <VirtualHost your_ip:your_port>
+         ServerName your.host
+         DocumentRoot /var/www/html/shindig
+         ... other normal settings in vhosts...
+    <Directory>
+      AllowOverride All
+    </Directory>
+  </VirtualHost>
+
+  Restart apache, and point your browser to:
+
+  http://<your.host>/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
+  b. Run with an existing host
+
+  If you cannot/don't want to create a virtual host, you can edit the file 
+  php/config/container.php or php/config/local.php (see the comments 
+  php/config/container.php for documentation of the configuration system) 
+  and change the web_prefix setting to '/shindig/php'.
+
+  In this case, you should also change all paths in shindig/config/container.js
+  (see the comments there for documentation of the JSON configuration system).
+
+  Then you can run the gadget by pointing your browser to:
+
+  http://<your.host>/shindig/php/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
+  Going forward
+  -------------
+  
+  Check out the php/config/container.php file, in local.php you only have 
+  to specificy the fields you want to overwrite with other values, for 
+  example on a production system you would probably want to have something
+  like:
+   $shindigConfig = array(
+    'debug' => false,
+    'allow_plaintext_token' => false,
+    'token_cipher_key' => 'MySecretKey',
+    'token_hmac_key' => 'MyOtherSecret',
+    'private_key_phrase' => 'MyCertificatePassword',
+    'person_service' => 'MyPeopleService',
+    'activity_service' => 'MyActivitiesService',
+    'app_data_service' => 'MyAppDataService',
+    'messages_service' => 'MyMessagesService',
+    'oauth_lookup_service' => 'MyOAuthLookupService'
+    'xrds_location' => 'http://www.mycontainer.com/xrds',
+    'check_file_exists' => false
+   );
+ 
+  And then implement your own service and oauth lookup classes.
+
+  Running from an svn checkout
+  ---------
+
+  If you want to run PHP Shindig directly from an svn checkout, please refer to the
+  docs/README.svn file to learn about the configuration differences between the released
+  and svn version. 
+
+  Licensing
+  ---------
+
+  Please see the file called LICENSE.
+
+  Shindig URLS
+  ------------
+
+  Home Page:          http://shindig.apache.org/
+  Downloads:          http://shindig.apache.org/download/index.html
+  Mailing Lists:      http://shindig.apache.org/mail-lists.html
+  Source Code:        http://svn.apache.org/repos/asf/shindig/
+  Issue Tracking:     https://issues.apache.org/jira/browse/SHINDIG
+  Wiki:               http://cwiki.apache.org/confluence/display/SHINDIG/
+
+This distribution includes cryptographic software.  The country in
+which you currently reside may have restrictions on the import,
+possession, use, and/or re-export to another country, of
+encryption software.  BEFORE using any encryption software, please
+check your country's laws, regulations and policies concerning the
+import, possession, or use, and re-export of encryption software, to
+see if this is permitted.  See <http://www.wassenaar.org/> for more
+information.
+
+The U.S. Government Department of Commerce, Bureau of Industry and
+Security (BIS), has classified this software as Export Commodity
+Control Number (ECCN) 5D002.C.1, which includes information security
+software using or performing cryptographic functions with asymmetric
+algorithms.  The form and manner of this Apache Software Foundation
+distribution makes it eligible for export under the License Exception
+ENC Technology Software Unrestricted (TSU) exception (see the BIS
+Export Administration Regulations, Section 740.13) for both object
+code and source code.
+
+The following provides more details on the included cryptographic
+software:
+
+    Apache Shindig PHP interfaces with the mcrypt API
+    <http://mcrypt.sourceforge.net/> to provide encryption
+    of messages using the AES standard.
+
diff --git a/trunk/php/build.xml b/trunk/php/build.xml
new file mode 100644
index 0000000..a67cb90
--- /dev/null
+++ b/trunk/php/build.xml
@@ -0,0 +1,45 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project name="Apache Shindig PHP" default="test">
+    <target name="clean">
+        <delete dir="${basedir}/build"/>
+    </target>
+    <target name="prepare">
+        <mkdir dir="${basedir}/build/logs"/>
+    </target>
+    <target name="phpunit">
+        <exec dir="${basedir}"
+		executable="phpunit"
+		failonerror="true">
+            <arg line="--log-junit build/logs/junit.xml
+			  --coverage-clover build/logs/clover.xml
+			 test/ShindigAllTests.php"/>
+        </exec>
+    </target>
+    <target name="code-coverage">
+        <exec dir="${basedir}"
+		executable="phpunit"
+		failonerror="true">
+            <arg line="--coverage-html ${basedir}/build/logs/coverage_html
+            test/ShindigAllTests.php"/>
+        </exec>
+    </target>
+    <target name="test"
+		 depends="clean,prepare,phpunit,code-coverage"/>
+</project>
\ No newline at end of file
diff --git a/trunk/php/certs/README b/trunk/php/certs/README
new file mode 100644
index 0000000..cea1200
--- /dev/null
+++ b/trunk/php/certs/README
@@ -0,0 +1,18 @@
+To have working oauth & signed requests you need a private and public key.
+
+You can generate these using the 'openssl' command by doing the following:
+
+Goto the certs directory:
+# cd shindig/php/certs
+
+Generate the private key:
+# openssl genrsa -out private.key -des3 1024
+
+Enter a pass phrase, and make sure to put this in 'private_key_phrase' in 
+config/container.php
+
+Generate the public key:
+# openssl req -new -x509 -nodes -sha1 -days 365 -key private.key > public.crt
+
+Enter the pass phrase again, and your host's information
+
diff --git a/trunk/php/config/container.php b/trunk/php/config/container.php
new file mode 100644
index 0000000..c610c55
--- /dev/null
+++ b/trunk/php/config/container.php
@@ -0,0 +1,259 @@
+<?php
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * The default configuration settings
+ *
+ * Put any site specific configuration in a config/local.php file, this way
+ * your configuration won't be lost when upgrading shindig. If your site don't
+ * support any services just use empty string as the service name. i.e.
+ *  'messages_service' => ''
+ *
+ * in local.php you only have to specificy the fields you want to overwrite
+ * with other values, for example on a production system you would probably have:
+ * $shindigConfig = array(
+ * 	'debug' => false,
+ * 	'allow_plaintext_token' => false,
+ * 	'token_cipher_key' => 'MySecretKey',
+ * 	'token_hmac_key' => 'MyOtherSecret',
+ * 	'private_key_phrase' => 'MyCertificatePassword',
+ * 	'person_service' => 'MyPeopleService',
+ * 	'activity_service' => 'MyActivitiesService',
+ * 	'app_data_service' => 'MyAppDataService',
+ * 	'messages_service' => 'MyMessagesService',
+ * 	'oauth_lookup_service' => 'MyOAuthLookupService'
+ * 	'xrds_location' => 'http://www.mycontainer.com/xrds',
+ * 	'check_file_exists' => false
+ * );
+ *
+ */
+return array(
+  // Show debug backtrace's. Disable this on a production site
+  'debug' => true,
+  // do real file_exist checks? Turning this off can be a big performance gain on prod servers but also risky & less verbose errors
+  'check_file_exists' => true,
+
+  // Allow plain text security tokens, this is only here to allow the sample files to work. Disable on a production site
+  'allow_plaintext_token' => true,
+
+  // Is a valid security token required to render a gadget? The token is required for doing signed preloads, but disallowing this
+  // can also help prevent external parties using your rendering server (only for the paranoid :)
+  'render_token_required' => false,
+
+  // Normally we would only rewrite the gadget's html if it has the <Optional feature="content-rewrite"> set, however with this you can
+  // force the content to always be rewritten
+  'rewrite_by_default' => false,
+
+  // Should we sanitize (remove scripts) from certain views? Right now this is useless, but once service sided templating and OSML is done
+  // this could be useful to force (fast) html only gadgets on the profile and/or home view. Set this to false or to an array of view names like: array('profile', 'home')
+  'sanitize_views' => false,
+
+  // Compress the inlined javascript, saves upto 50% of the document size
+  'compress_javascript' => true,
+
+  // Default refresh interval for proxy/makeRequest's if none is specified in the query
+  'default_refresh_interval' => 1209587,
+
+  // The URL Prefix under which shindig lives ie if you have http://myhost.com/shindig/php set web_prefix to /shindig/php
+  'web_prefix' => '',
+  // If you changed the web prefix, add the prefix to these too
+  'default_js_prefix' => '/gadgets/js/',
+  'default_iframe_prefix' => '/gadgets/ifr?',
+
+ 'servlet_map' => array(
+   '/container' => 'apache\shindig\gadgets\servlet\ContentFilesServlet',
+   '/samplecontainer' => 'apache\shindig\gadgets\servlet\ContentFilesServlet',
+   '/gadgets/resources' => 'apache\shindig\gadgets\servlet\ResourcesFilesServlet',
+   '/gadgets/js' => 'apache\shindig\gadgets\servlet\JsServlet',
+   '/gadgets/proxy' => 'apache\shindig\gadgets\servlet\ProxyServlet',
+   '/gadgets/makeRequest' => 'apache\shindig\gadgets\servlet\MakeRequestServlet',
+   '/gadgets/ifr' => 'apache\shindig\gadgets\servlet\GadgetRenderingServlet',
+   '/gadgets/metadata' => 'apache\shindig\gadgets\servlet\MetadataServlet',
+   '/gadgets/oauthcallback' => 'apache\shindig\gadgets\servlet\OAuthCallbackServlet',
+   '/gadgets/api/rpc' => 'apache\shindig\social\servlet\JsonRpcServlet',
+   '/gadgets/api/rest' => 'apache\shindig\social\servlet\DataServiceServlet',
+   '/social/rest' => 'apache\shindig\social\servlet\DataServiceServlet',
+   '/rest' => 'apache\shindig\social\servlet\DataServiceServlet',
+   '/social/rpc' => 'apache\shindig\social\servlet\CompatibilityJsonRpcServlet',
+   '/rpc' => 'apache\shindig\social\servlet\JsonRpcServlet',
+   '/public.crt' => 'apache\shindig\gadgets\servlet\CertServlet',
+   '/public.cer' => 'apache\shindig\gadgets\servlet\CertServlet',
+   '/' => 'apache\shindig\gadgets\servlet\ContentFilesServlet',
+ ),
+
+  // The X-XRDS-Location value for your implementing container, see http://code.google.com/p/partuza/source/browse/trunk/Library/XRDS.php for an example
+  'xrds_location' => '',
+
+  // Allow anonymous (READ) access to the profile information? (aka REST and JSON-RPC interfaces)
+  // setting this to false means you have to be authenticated through OAuth to read the data
+  'allow_anonymous_token' => true,
+
+  // The encryption keys for encrypting the security token, and the expiration of it. Make sure these match the keys used in your container/site
+  'token_cipher_key' => 'INSECURE_DEFAULT_KEY',
+  'token_hmac_key' => 'INSECURE_DEFAULT_KEY',
+  'token_max_age' => 60 * 60,
+
+  // Ability to customize the style thats injected into the gadget document. Don't forget to put the link/etc colors in shindig/config/container.js too!
+  'gadget_css' => 'body,td,div,span,p{font-family:arial,sans-serif;} a {color:#0000cc;}a:visited {color:#551a8b;}a:active {color:#ff0000;}body{margin: 0px;padding: 0px;background-color:white;}',
+
+  // P3P privacy policy to use for the iframe document
+  'P3P' => 'CP="CAO PSA OUR"',
+
+  // The locations of the various required components on disk. If you did a normal svn checkout there's no need to change these
+  'base_path' => realpath(dirname(__FILE__) . '/..') . '/',
+  'features_path' => array(
+    realpath(dirname(__FILE__) . '/../../features/src/main/javascript/features') . '/',
+    realpath(dirname(__FILE__) . '/../../extras/src/main/javascript/features-extras') . '/',
+  ),
+  'container_path' => realpath(dirname(__FILE__) . '/../../config') . '/',
+  'javascript_path' => realpath(dirname(__FILE__) . '/../../content') . '/',
+  'resources_path' => realpath(dirname(__FILE__) . '/../external/resources') . '/',
+
+  // The OAuth SSL certificates to use, and the pass phrase for the private key
+  'private_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/private.key',
+  'public_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/public.crt',
+  'private_key_phrase' => 'partuza',
+
+  // the path to the json db file, used only if your using the JsonDbOpensocialService example/demo service
+  'jsondb_path' => realpath(dirname(__FILE__) . '/../../content/sampledata') . '/canonicaldb.json',
+
+  // Force these libraries to be external (included through <script src="..."> tags), this way they could be cached by the browser
+  // these libraries will be included regardless of the features the gadget requests
+  // example: 'dynamic-height:views' includes the features dynamic-height and views
+  'forcedJsLibs' => '',
+
+  // Force these js libraries to be appended to each gadget regardless if the gadget requested them or not
+  // This can be useful to overwrite existing methods of other javascript packages
+  'forcedAppendedJsLibs' => array(),
+
+  // After checking the internal __autoload function, shindig can also call the 'extension_autoloader' function to load an
+  // unknown custom class, this is particuarly useful for when intergrating shindig into an existing framework that also depends on autoloading
+  'extension_autoloader' => false,
+
+  // Configurable classes. Change these to the class name to use, and make sure the auto-loader can find them
+  'blacklist_class' => 'apache\shindig\gadgets\sample\BasicGadgetBlacklist',
+  'remote_content' => 'apache\shindig\common\sample\BasicRemoteContent',
+  'remote_content_fetcher' => 'apache\shindig\common\sample\BasicRemoteContentFetcher',
+  'security_token_signer' => 'apache\shindig\common\sample\BasicSecurityTokenDecoder',
+  'security_token' => 'apache\shindig\common\sample\BasicSecurityToken',
+  'oauth_lookup_service' => 'apache\shindig\common\sample\BasicOAuthLookupService',
+  // The OAuth Store is used to store the (gadgets/)oauth proxy credentials it obtained on behalf of the user/gadget combo
+  'oauth_store' => 'apache\shindig\gadgets\oauth\BasicOAuthStore',
+  'gadget_oauth_token_store' => 'apache\shindig\gadgets\oauth\BasicGadgetOAuthTokenStore',
+
+  // handler for ApiServlet
+  'service_handler' => array(
+    'people' => 'apache\shindig\social\service\PersonHandler',
+    'activities' => 'apache\shindig\social\service\ActivityHandler',
+    'appdata' => 'apache\shindig\social\service\AppDataHandler',
+    'groups' => 'apache\shindig\social\service\GroupHandler',
+    'messages' => 'apache\shindig\social\service\MessagesHandler',
+    'cache'  => 'apache\shindig\social\service\InvalidateHandler',
+    'system' => 'apache\shindig\social\service\SystemHandler',
+    'albums' => 'apache\shindig\social\service\AlbumHandler',
+    'mediaitems' => 'apache\shindig\social\service\MediaItemHandler',
+    'http' => 'apache\shindig\social\service\HttpHandler',
+  ),
+
+  // class is the name of the concrete input converter class
+  // targetField is the name of the field where the decoded array will be inserted
+  // into the params array or null if you want to overwrite params with the decoded
+  // array or false if you do not want to add the decoded params
+  'service_input_converter' => array(
+    'people' => array('class' => 'apache\shindig\social\converters\InputPeopleConverter', 'targetField' => false),
+    'activities' => array('class' => 'apache\shindig\social\converters\InputActivitiesConverter', 'targetField' => 'activity'),
+    'appdata' => array('class' => 'apache\shindig\social\converters\InputAppDataConverter', 'targetField' => 'data'),
+    'messages' => array('class' => 'apache\shindig\social\converters\InputMessagesConverter', 'targetField' => 'entity'),
+    'cache'  => array('class' => 'apache\shindig\social\converters\InputInvalidateConverter', 'targetField' => null),
+    'albums' => array('class' => 'apache\shindig\social\converters\InputAlbumsConverter', 'targetField' => 'album'),
+    'mediaitems' => array('class' => 'apache\shindig\social\converters\InputMediaItemsConverter', 'targetField' => 'mediaItem'),
+  ),
+
+  // available gadget renderer with the class as key and the needed attributes in the
+  // view's content block to choose this renderer. If constraint's value is a string
+  // the attribute value has to match this string, if it's a boolean the attribute
+  // just has to be available or not available
+  'gadget_renderer' => array(
+    'apache\shindig\gadgets\render\GadgetHtmlRenderer' => array('type' => 'HTML', 'href' => false),
+    'apache\shindig\gadgets\render\GadgetHrefRenderer' => array('type' => 'HTML', 'href' => true),
+    'apache\shindig\gadgets\render\GadgetUrlRenderer'  => array('type' => 'URL'),
+  ),
+
+  'gadget_class' => 'apache\shindig\gadgets\Gadget',
+  'gadget_context_class' => 'apache\shindig\gadgets\GadgetContext',
+  'gadget_factory_class' => 'apache\shindig\gadgets\GadgetFactory',
+  'gadget_spec_parser' => 'apache\shindig\gadgets\GadgetSpecParser',
+  'gadget_spec_class' => 'apache\shindig\gadgets\GadgetSpec',
+  'substitution_class' => 'apache\shindig\gadgets\Substitutions',
+  'proxy_handler' => 'apache\shindig\gadgets\ProxyHandler',
+  'makerequest_handler' => 'apache\shindig\gadgets\MakeRequestHandler',
+  'makerequest_class' => 'apache\shindig\gadgets\MakeRequest',
+  'container_config_class' => 'apache\shindig\gadgets\ContainerConfig',
+
+  // Caching back-end's to use. Shindig ships with CacheStorageFile, CacheStorageApc and CacheStorageMemcache support
+  // The data cache is primarily used for remote content (proxied files, gadget spec, etc)
+  // and the feature_cache is used to cache the parsed features xml structure and javascript
+  // On a production system you probably want to use CacheStorageApc for features, and CacheStorageMemcache for the data cache
+  'data_cache' => 'apache\shindig\common\sample\CacheStorageFile',
+  'feature_cache' => 'apache\shindig\common\sample\CacheStorageFile',
+
+  // RESTful API data service classes to use
+  // See http://code.google.com/p/partuza/source/browse/#svn/trunk/Shindig for a MySql powered example
+  'person_service' => 'apache\shindig\social\sample\JsonDbOpensocialService',
+  'activity_service' => 'apache\shindig\social\sample\JsonDbOpensocialService',
+  'app_data_service' => 'apache\shindig\social\sample\JsonDbOpensocialService',
+  'group_service' => 'apache\shindig\social\sample\JsonDbOpensocialService',
+  'messages_service' => 'apache\shindig\social\sample\JsonDbOpensocialService',
+  'invalidate_service' => 'apache\shindig\social\sample\DefaultInvalidateService',
+  'album_service' => 'apache\shindig\social\sample\JsonDbOpensocialService',
+  'media_item_service' => 'apache\shindig\social\sample\JsonDbOpensocialService',
+
+  // Also scan these directories when looking for <Class>.php files. You can include multiple paths by seperating them with a ,
+  // To enable classes in the extras package you have to add this class path
+  // 'extension_class_paths' => '../extras/src/main/php/extras',
+  'extension_class_paths' => '',
+
+  'userpref_param_prefix' => 'up_',
+  'libs_param_name' => 'libs',
+
+  // If you use CacheStorageMemcache as caching backend, change these to the memcache server settings
+  'cache_host' => 'localhost',
+  'cache_port' => 11211,
+  // When using CacheStorageMemcache, should we use pconnect? There are some reports that apache/mpm + memcache_pconnect can lead to segfaults
+  'cache_memcache_pconnect' => true,
+  'cache_time' => 24 * 60 * 60,
+  // If you use CacheStorageFile as caching backend, this is the directory where it stores the temporary files
+  'cache_root' => sys_get_temp_dir() . '/shindig',
+
+  // connection timeout setting for all curl requests, set this time something low if you want errors reported
+  // quicker to the end user, and high (between 10 and 20) if your on a slow connection
+  'curl_connection_timeout' => '10',
+  'curl_request_timeout' => '10',
+
+  // If your development server is behind a proxy, enter the proxy details here in 'proxy.host.com:port' format.
+  'proxy' => '',
+
+  // If your server is behind a reverse proxy, set the real hostname here so that OAuth signatures match up, for example:
+  // 'http_host' => 'modules.partuza.nl'
+  'http_host' => false,
+
+  // Container id, used for security token
+  'container_id' => 'default'
+);
diff --git a/trunk/php/config/test.php b/trunk/php/config/test.php
new file mode 100644
index 0000000..6343fca
--- /dev/null
+++ b/trunk/php/config/test.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * this configuration overrides some values for unit testing purposes
+ * you should not use these settings in production
+ */
+return array(
+  'compress_javascript' => false,
+  'private_key_file' => realpath(dirname(__FILE__) . '/../test/certs') . '/private.key',
+  'public_key_file' => realpath(dirname(__FILE__) . '/../test/certs') . '/public.crt',
+  'private_key_phrase' => 'partuza',
+);
\ No newline at end of file
diff --git a/trunk/php/docs/README.lighttpd b/trunk/php/docs/README.lighttpd
new file mode 100644
index 0000000..8dd3d22
--- /dev/null
+++ b/trunk/php/docs/README.lighttpd
@@ -0,0 +1,86 @@
+Installing and Running The PHP Shindig Gadget Server with lighttpd
+============================================
+
+Prequisites before building Shindig for PHP
+============================================
+In order to build and run Shindig for PHP, you must have the following:
+
+- A Subversion client installed in order to checkout the code.
+ Instructions for downloading and installing Subversion can be found here:
+ http://subversion.tigris.org/
+- lighttpd with mod_rewrite enabled.
+- PHP 5.2.x(cgi/fastcgi) with the json, simplexml, mcrypt and curl
+ extentions enabled.
+
+Getting the code
+============================================
+Create a directory, e.g. /var/www/html/shindig and checkout the Shindig
+code from its Subversion repository
+
+mkdir /var/www/html/shindig
+cd /var/www/html/shindig
+svn co http://svn.apache.org/repos/asf/shindig/trunk/ .
+
+
+Running Shindig
+============================================
+
+To run the code, you have several options:
+
+a. Create a new virtual host (recommended)
+
+Shindig relies on all requests being redirected to the index.php
+script. In order to do that with lighttpd you need to use the
+"url.rewrite-once" directive in the configuration files.
+
+Point your lighttpd at the code by adding this to your lighttpd.conf:
+
+$HTTP["host"] == "shindig" {
+
+   ... other normal settings for virtual hosts...
+	
+   server.document-root = "$YOURPATHHERE/shindig/php/"
+   server.name = "shindig"
+	
+	url.rewrite-once = (
+		"^[^?]*(\??)(.*)$" => "index.php$1$2"
+	)
+	
+}
+
+(Replace "shindig" and "$YOURPATHHERE" as required)
+
+Restart lighttpd and make sure that you can resolve the new sub-host
+(e.g. by adding it to your local hosts file).
+
+Point your browser at:
+
+http://shindig/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
+(replace shindig with the name of your virtual host)
+
+you should see something like this:
+http://shindig.chabotc.com/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
+b. Run with an existing host
+
+If you cannot/don't want to create a virtual host, check out the Shindig
+code into a subdirectory of your document root and add this to your lighttpd
+configuration file:
+
+url.rewrite-once = (
+	"^shindig/[^?]*(\??)(.*)$" => "shindig/php/index.php$1$2"
+)
+
+Restart lighttpd to apply the changes. You also need to edit the file
+php/config.php and change the web_prefix setting to '/shindig'.
+
+Point your browser at:
+
+http://localhost/shindig/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
+(replace localhost with the name of your host)
+
+you should see something like this:
+http://shindig.chabotc.com/gadgets/ifr?url=http://www.labpixies.com/campaigns/todo/todo.xml
+
diff --git a/trunk/php/docs/README.svn.txt b/trunk/php/docs/README.svn.txt
new file mode 100644
index 0000000..8d85ba2
--- /dev/null
+++ b/trunk/php/docs/README.svn.txt
@@ -0,0 +1,97 @@
+                          Apache Shindig PHP
+
+  Running PHP Shindig from SVN
+  -----------
+
+  This file is intended to be a supplement to the general README file, the release version
+  has a slightly different file path configuration 
+  
+  Differences between the release version and a svn checkout
+  -----------
+  
+  Shindig's svn repository contains both the Java and PHP versions of shindig, and the shared
+  content and features code.
+   
+  To make PHP Shindig work from svn, its default file path configurations in 
+      <shindig>/php/config/container.php
+  are all configured for a file path layout where the features and content code is contained
+  in a directory level above the php folder (ie <shindig>/php/../{features, content}), resulting in a 
+  folder layout like:
+  
+  shindig/             (contains the shared README, NOTICE, LICENSE, etc files)
+  shindig/content      (contains shared javascript code)
+  shindig/features     (contains shared features code)
+  shindig/config       (contains the shared configuration)
+  shindig/java         (contains the java-shindig implementation)
+  shindig/php          (contains the php-shindig implementation)
+  
+  The release script moves these folders to the top level php folder and makes the php folder the top
+  level folder when building it's archives, so in other words the content and features code will be
+  located in <shindig>/{features, content}, resulting in the folowing layout:
+
+  shindig/             (contains the php implementation(!) & the php specific README, NOTICE, LICENSE, etc files)
+  shindig/content      (contains shared javascript code)
+  shindig/features     (contains shared features code)
+  shindig/config       (contains both the shared as wel as php specific configuration)
+  .. etc ..
+  
+  Switching from release to svn, and back
+  -----------
+  
+  There are 2 configurations that need to be updated to switch from release to a svn version:
+  
+  1) Apache's virtual host configuration:
+  
+  The DirectoryRoot for the release version is <shindig>/, while the DirectoryRoot for the svn
+  version is <shindig>/php, ie:
+  
+  RELEASE
+  
+  <VirtualHost your_ip:your_port>
+    ServerName your.host
+    DocumentRoot /var/www/html/shindig
+    ... other normal settings in vhosts...
+  <Directory>
+    AllowOverride All
+  </Directory>
+  </VirtualHost>
+  
+  SVN
+  
+  <VirtualHost your_ip:your_port>
+    ServerName your.host
+    DocumentRoot /var/www/html/shindig/php
+    ... other normal settings in vhosts...
+  <Directory>
+    AllowOverride All
+  </Directory>
+  </VirtualHost>
+   
+   2) PHP Shindig's configuration
+   
+   The file paths of all the shared resources are different between the released and svn versions in the config/container.php config file
+   (notice the extra ../ for the content, features and jsondb path's with the SVN version)
+   
+   RELEASE
+   
+  'base_path' => realpath(dirname(__FILE__) . '/..') . '/',
+  'features_path' => realpath(dirname(__FILE__) . '/../features/src/main/javascript/features') . '/',
+  'container_path' => realpath(dirname(__FILE__) . '/../config') . '/',
+  'javascript_path' => realpath(dirname(__FILE__) . '/../content') . '/',
+  'private_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/private.key',
+  'public_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/public.crt',
+  'private_key_phrase' => 'SOMEKEY',
+  'jsondb_path' => realpath(dirname(__FILE__) . '/../content/sampledata') . '/canonicaldb.json',
+   
+   SVN
+   
+  'base_path' => realpath(dirname(__FILE__) . '/..') . '/',
+  'features_path' => realpath(dirname(__FILE__) . '/../../features/src/main/javascript/features') . '/',
+  'container_path' => realpath(dirname(__FILE__) . '/../../config') . '/',
+  'javascript_path' => realpath(dirname(__FILE__) . '/../../content') . '/',
+  'private_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/private.key',
+  'public_key_file' => realpath(dirname(__FILE__) . '/../certs') . '/public.crt',
+  'private_key_phrase' => 'SOMEKEY',
+  'jsondb_path' => realpath(dirname(__FILE__) . '/../../content/sampledata') . '/canonicaldb.json',
+  
+  
diff --git a/trunk/php/docs/style-conventions.xml b/trunk/php/docs/style-conventions.xml
new file mode 100644
index 0000000..d986636
--- /dev/null
+++ b/trunk/php/docs/style-conventions.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<profiles>
+<profile name="PHP Conventions">
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_arguments_in_allocation_expression_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_arguments_in_allocation_expression_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_arguments_in_allocation_expression_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_arguments_in_method_invocation_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_arguments_in_method_invocation_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_arguments_in_method_invocation_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_assignment_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_assignment_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_assignment_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_binary_expression_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_binary_expression_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_binary_expression_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_compact_if_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_compact_if_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_compact_if_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_conditional_expression_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_conditional_expression_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_conditional_expression_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_expressions_in_array_initializer_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_expressions_in_array_initializer_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_expressions_in_array_initializer_line_wrap_policy" value="1"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_parameters_in_method_declaration_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_parameters_in_method_declaration_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_parameters_in_method_declaration_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_superclass_in_type_declaration_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_superclass_in_type_declaration_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_superclass_in_type_declaration_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_superinterfaces_in_type_declaration_force_split" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_superinterfaces_in_type_declaration_indent_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.alignment_for_superinterfaces_in_type_declaration_line_wrap_policy" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.blank_lines_before_field" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.blank_lines_before_member_type" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.blank_lines_before_method" value="1"/>
+<setting id="com.zend.php.formatter.core.formatter.blank_lines_between_type_declarations" value="1"/>
+<setting id="com.zend.php.formatter.core.formatter.brace_position_for_block" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.brace_position_for_method_declaration" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.brace_position_for_switch" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.brace_position_for_type_declaration" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.continuation_indentation" value="2"/>
+<setting id="com.zend.php.formatter.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
+<setting id="com.zend.php.formatter.core.formatter.format_guardian_clause_on_one_line" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.indent_breaks_compare_to_cases" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.indent_empty_lines" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.indent_statements_compare_to_block" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.indent_statements_compare_to_body" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.indentation.size" value="2"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_before_catch_in_try_statement" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_before_else_in_if_statement" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_before_while_in_do_statement" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_in_empty_block" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_in_empty_method_body" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_new_line_in_empty_type_declaration" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_arrow_in_array_creation" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_arrow_in_field_access" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_arrow_in_foreach" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_arrow_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_assignment_operator" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_binary_operator" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_closing_brace_in_block" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_closing_paren_in_cast" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_colon_in_conditional" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_coloncolon_in_field_access" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_coloncolon_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_array_creation" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_echo" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_for_inits" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_global" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_list" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_multiple_constant_declarations" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_static" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_comma_in_superinterfaces" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_array_creation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_cast" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_catch" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_for" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_foreach" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_if" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_list" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_switch" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_opening_paren_in_while" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_postfix_operator" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_prefix_operator" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_question_in_conditional" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_semicolon_in_for" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_after_unary_operator" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_arrow_in_array_creation" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_arrow_in_field_access" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_arrow_in_foreach" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_arrow_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_assignment_operator" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_binary_operator" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_array_creation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_cast" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_catch" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_for" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_foreach" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_if" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_list" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_switch" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_closing_paren_in_while" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_colon_in_case" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_colon_in_conditional" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_colon_in_default" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_coloncolon_in_field_access" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_coloncolon_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_array_creation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_echo" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_for_inits" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_global" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_list" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_multiple_constant_declarations" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_static" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_comma_in_superinterfaces" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_brace_in_block" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_brace_in_switch" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_array_creation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_catch" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_for" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_foreach" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_if" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_list" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_switch" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_opening_paren_in_while" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_postfix_operator" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_prefix_operator" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_question_in_conditional" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_semicolon" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_semicolon_in_for" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_before_unary_operator" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_between_brackets_in_array_type_reference" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.keep_else_statement_on_same_line" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.keep_elseif_statement_on_same_line" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.keep_imple_if_on_one_line" value="false"/>
+<setting id="com.zend.php.formatter.core.formatter.keep_then_statement_on_same_line" value="true"/>
+<setting id="com.zend.php.formatter.core.formatter.lineSplit" value="120"/>
+<setting id="com.zend.php.formatter.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
+<setting id="com.zend.php.formatter.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
+<setting id="com.zend.php.formatter.core.formatter.put_empty_statement_on_new_line" value="false"/>
+<setting id="indentationChar" value=" "/>
+<setting id="insert_space_after_opening_paren_in_declare" value="false"/>
+<setting id="insert_space_before_closing_paren_in_declare" value="false"/>
+<setting id="insert_space_before_opening_paren_in_declare" value="false"/>
+</profile>
+</profiles>
diff --git a/trunk/php/docs/style-guide.html b/trunk/php/docs/style-guide.html
new file mode 100644
index 0000000..6c014c1
--- /dev/null
+++ b/trunk/php/docs/style-guide.html
@@ -0,0 +1,308 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>PHP Style Guide</title>
+</head>
+
+<body>
+<p>Overview</p>
+<p> * Use PHP 5.2+ whenever feasible.<br />
+  * Follow the C++ formatting rules in spirit.<br />
+  * Provide a header with copyright and author info.<br />
+  * Separate business logic, data, and presentational layers. In other words, keep the Model-View-Controller pattern in mind. It is very handy and can make PHP development a lot easier.<br />
+  * Avoid heavy logic within presentational pages. While some processing and logic is done when it is nestled within a tag soup of HTML, avoid making it complex. One should not be doing more than basic foreach (), if (), and $obj-&gt;get*() within a presentation document source.<br />
+  * Unit test your functions using PHPUnit.<br />
+  * Naming: FunctionNamesLike, $localVariableName, $objectVariable, ClassNamesLike, MethodNamesLike, CONSTANTS_LIKE_THIS. Global names (classes, functions, variables, defines) must be prefixed to prevent naming clashes with PHP itself. This approach includes preventing prefixes that clash with PHP or are likely to. Apart from constants, prevent underscores in your names unless you simulate namespaces and are sure you can switch to real namespaces once PHP has them (and of course for object variables).<br />
+  * Getters/Setters: Required, name them getFoo(), setFoo().<br />
+  * Magic Getters/Setters: Do not use.<br />
+  * Indentation: 2 spaces, no tabs.<br />
+  * Line Wrapping: 120 chars.<br />
+  * Whitespace: Use sparingly. Opening curly brace starts on earliest line possible; initializations are not aligned; multi-line argument lists indented at first arg or spacing+2 if very long.<br />
+  * Parentheses: only where required.<br />
+  * Braces: always.<br />
+  * Strings: Use single quoted strings over double quoted.<br />
+  * Comments: phpdoc (similar to javadoc), and follow the C++ style in spirit. See http://www.phpdoc.org for detailed information.<br />
+  * Filenames: Match classname, generally end in .php.<br />
+  * PHP tags: &lt;?php only. Always have a space (or newline) after an opening tag for multi-line PHP code. Do not use a closing ?&gt; tag at the end of a file.<br />
+  * PHP Tags: Avoid switching between PHP and HTML often.<br />
+  * Comments: Only // and /* */ Two spaces after code if comments on the same line as code. If you intend to write a write a shell script in PHP a hashbang line is ok.<br />
+  * Error Reporting: Set error reporting to E_ALL|E_NOTICE, and enable error logging; use E_STRICT if possible.<br />
+  * Only use new form of super globals (e.g. $_SERVER rather than deprecated $HTTP_SERVER_VARS) .<br />
+  * Avoid using ${var} inside strings and never use {$var} inside strings. Instead prefer to separate the string and use concatenation.</p>
+<p>Style<br />
+  PHP Tags<br />
+  Use only the full PHP tag: &lt;?php (lower case). This is because (1) short tags rely on the PHP configuration, (2) it violates xml specs (&lt;? starts a PI (processing instruction) and must be followed by a name), and (3) it is more readable. For readability, have a space or newline after the opening tag for multi-line PHP code. Do not use the closing ?&gt; php tag at the end of a file. It's optional and will help prevent unwanted output. This applies to all PHP files including those that mix PHP and HTML. You will still need to close PHP blocks if you intersperse them with HTML, but if the file closes with PHP code, then leave off the closing ?&gt; tag.</p>
+<p>File header</p>
+<p>Provide a file header that denotes copyright (in apache projects we chose not to list the author). This should also contain a small description of what the script does.</p>
+<p>&lt;?php<br />
+  /**<br />
+* Licensed to the Apache Software Foundation (ASF) under one<br />
+* or more contributor license agreements. See the NOTICE file<br />
+* distributed with this work for additional information<br />
+* regarding copyright ownership. The ASF licenses this file<br />
+* to you under the Apache License, Version 2.0 (the<br />
+* &quot;License&quot;); you may not use this file except in compliance<br />
+* with the License. You may obtain a copy of the License at<br />
+*<br />
+*     http://www.apache.org/licenses/LICENSE-2.0<br />
+*<br />
+* Unless required by applicable law or agreed to in writing,<br />
+* software distributed under the License is distributed on an<br />
+* &quot;AS IS&quot; BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY<br />
+* KIND, either express or implied. See the License for the<br />
+* specific language governing permissions and limitations under the License.<br />
+*<br />
+  * General script description.<br />
+  */</p>
+<p>PHP and HTML Inlined Together</p>
+<p>Avoid mish-mashing PHP and HTML together. Prefer to generate HTML within PHP (remember, you can single quote, double quote, and Heredoc strings. For large blocks of HTML use Heredoc syntax, as most editors will attempt to syntax highlight the HTML, which is very handy. Consider using when you only need to output a few variables. When using echo to output HTML code, use single quoted HTML attributes in double quoted PHP strings and vice versa. Also avoid ${name} constructs inside echo, prefer to separate the string and use concatenation.</p>
+<p>&lt;?php<br />
+  echo &lt;&lt;&lt;HTML<br />
+  &lt;html&gt;<br />
+  &lt;head&gt;<br />
+  &lt;title&gt;$title&lt;/title&gt;<br />
+  &lt;/head&gt;<br />
+  &lt;/html&gt;<br />
+  HTML;<br />
+</p>
+<p>In a somewhat similar fashion, you can put newlines into PHP strings directly.</p>
+<p>&lt;?php<br />
+  echo &quot;&lt;html&gt;<br />
+  &lt;head&gt;<br />
+  &lt;title&gt;$title&lt;/title&gt;<br />
+  &lt;/head&gt;&quot;;<br />
+  // ...<br />
+</p>
+<p>Additionally, remember you do not need to concatenate all your strings when you echo them out. You can simple echo them out in sequence.</p>
+<p>&lt;?php<br />
+  echo &quot;&lt;html&gt;&quot;,<br />
+  &quot;&lt;head&gt;&quot;,<br />
+  &quot;&lt;title&gt;$title&lt;/title&gt;&quot;;<br />
+  // ...<br />
+</p>
+<p>If you must intersperse, comment the closing brace of conditional logic with what block the brace is ending, and follow one of these two rules consistantly for inlining PHP in HTML:</p>
+<p> 1. Put the PHP tags on their own line at the same indentation level as the parent HTML tag:</p>
+<p> &lt;html&gt;<br />
+  &lt;head&gt;...&lt;/head&gt;<br />
+  &lt;body&gt;<br />
+  &lt;div&gt;<br />
+  &lt;p&gt;<br />
+  &lt;?php<br />
+  if (true) { ... }<br />
+  ?&gt;<br />
+  &lt;/p&gt;<br />
+  &lt;/div&gt;<br />
+  &lt;/body&gt;<br />
+  &lt;/html&gt;</p>
+<p> 2. Put the PHP tags at the indentation level assuming there were no HTML tags:</p>
+<p> &lt;?php<br />
+  // ...code...<br />
+  ?&gt;<br />
+  &lt;html&gt;<br />
+  &lt;head&gt;...&lt;/head&gt;<br />
+  &lt;body&gt;<br />
+  &lt;?php<br />
+  if ($cond) {<br />
+  ?&gt;<br />
+  &lt;div&gt;...content...&lt;/div&gt;<br />
+  &lt;?php<br />
+  if ($cond2) {<br />
+  ?&gt;<br />
+  &lt;p&gt;...content...&lt;/p&gt;<br />
+  &lt;?php<br />
+  } // end if $cond2<br />
+  } // end if $cond<br />
+  ?&gt;<br />
+  &lt;/body&gt;<br />
+  &lt;/html&gt;</p>
+<p>Indentation</p>
+<p>Indent code blocks with 2 spaces. For line continuations, align vertically or with a hanging indent of 4 spaces.</p>
+<p>// Vertically aligned<br />
+  print $foo-&gt;getBar() . ' - baz '<br />
+  . $baz-&gt;getTitle() . &quot;\n&quot;;</p>
+<p>// 4 space hanging indent<br />
+  print $foo-&gt;getBar() . ' - baz ' .<br />
+  $baz-&gt;getTitle() . &quot;\n&quot;;</p>
+<p>Parentheses</p>
+<p>Do not use parentheses when using language constructs such as echo, print, include, require, unset, isset, empty. These are not functions and don't require parentheses around their parameters. Some language constructs behave like functions (for example, have return values) and some do not. Language constructs cannot be called using variable functions.</p>
+<p>echo 'My cat', $fluffysName, ' likes to code.';  // good<br />
+  echo ('My cat', $fluffysName, ' likes to code.');  // bad</p>
+<p>General Variable Naming Rules<br />
+  Globals, Constants, Defines</p>
+<p>Use all capital letters with separating underscores, and enable case sensitivity with define()'d values. Prefix globals with a package-specific name to avoid name conflicts.</p>
+<p> * $APP_GLOBAL_VARIABLE<br />
+  * $APP_CONSTANT<br />
+  * define('APP_DEFINED_CONSTANT', 'value', true)</p>
+<p>Local Variables</p>
+<p>Name them concisely. :) Make names descriptive without being overly long. You can use $i or $c for short loops, but $k and $v are not good variable names ever.</p>
+<p>Examples<br />
+  When iterating over an array, use names that describe what the variables are:</p>
+<p>$pets = array('cat' =&gt; 1, 'dog' =&gt; 3, 'rat' =&gt; 2);</p>
+<p>foreach ($pets as $pet=&gt;$count) {<br />
+  //...<br />
+  }</p>
+<p>A few obvious examples:</p>
+<p>$userName;<br />
+  $ldapGroup;<br />
+  $costCenter;  // good variable names</p>
+<p>$data;<br />
+  $thing;  // too vague</p>
+<p>Don't create new variables by appending an integer to an existing variable name:</p>
+<p>$user1;<br />
+  $user2;  // Not useful names -- what differentiates $user1 from $user2?</p>
+<p>Removing vowels from variable names may shorten them, but don't remove so many that it becomes incomprehensible:</p>
+<p>$grp;  // What is this?  group? gripe? grape? grep?</p>
+<p>Don't use indecipherable abbreviations:</p>
+<p>$fb;  // You might know what this stand for, but does everyone else?<br />
+  $fooBar;  // On the other hand, everyone gets this.</p>
+<p>Classes and Function Related<br />
+  Default Parameters<br />
+  Often the policy is to dissallow default parameters completely, but for PHP, this becomes a bit problematic because there is limited polymorphism, no overloading, and untyped data.</p>
+<p>As such, the only default parameter value allowed is NULL (NULL, not the empty string '' or boolean FALSE). This is to allow primitive overloading.</p>
+<p>Global Functions</p>
+<p>Classes, Class Properties, and Class Methods<br />
+  Class Names</p>
+<p>Class names are ProperCased, meaning, they start with a capital letter with subsequent words capitalized. Acryonyms are treated as normal words. Global names should be prefixed to prevent clashes with PHP itself.</p>
+<p> * AppClassName<br />
+  * AppXmlParser<br />
+  * AppHtmlXmlOmgClass</p>
+<p>Class Properties, Attributes, and the like</p>
+<p>Use camelCasing for class instance variables.</p>
+<p>class AppFoo {<br />
+  private $myPrivateVar;  // PHP 5<br />
+  public $myPublicVar;  // Try to avoid public members.<br />
+  }</p>
+<p>For class static variables, follow the general naming rules. There is little reason to use them, though.</p>
+<p>For class constants, follow the general naming rules.</p>
+<p>Method Names, both Static and Instance</p>
+<p>Use ProperCase? for class methods. Private methods should be documented as private. Getters and setters are required; name them getFoo(), setFoo().</p>
+<p>Files and File Names</p>
+<p> * Use .php extension, with all lower case, use _ for 'spaces'<br />
+  * Only Executable files should have side effects; Configuration files should ONLY initialize script, never modify saved data; Other files should have no side effects from inclusion.<br />
+  * Executable files: These are the files the user is suppose to be requesting. e.g. index.php<br />
+  o They should be all lowercase. Try to keep them sweet, short, and simple; this is the name links point to and the user has to remember.<br />
+  * Class definitions: These are the files that define classes.<br />
+  o A file should only contain a single class definition. Small, closely related classes within the same file are fine.<br />
+  o They should be named after the class<br />
+  o The class file may contain other, small helper classes.<br />
+  o Extension: .php .<br />
+  * Batch includes: This is a file that only serves as a wrapper or include a set of other files, usually common ones that you would always include together.<br />
+  o Name these files logically, eg util.php, domhelpers.php, formatters.php<br />
+  * Configuration files: These files should end in .php, be lowercase, named after their purpose, and have suffix to distinguish their purpose (if needed) and reside in a 'config' or equivilent directory. e.g. config.php, globals.php, setup.php, config-dev.php, config-ajax.php</p>
+<p>Documentation<br />
+  Use PhpDoc syntax, which is almost exactly like JavaDoc syntax. See http://www.phpdoc.org for a detailed tag listing. See examples below for quick reference.</p>
+<p>File Header<br />
+  Start each file with copyright notice comment what's in the file.</p>
+<p>/**<br />
+* Licensed to the Apache Software Foundation (ASF) under one<br />
+* or more contributor license agreements. See the NOTICE file<br />
+* distributed with this work for additional information<br />
+* regarding copyright ownership. The ASF licenses this file<br />
+* to you under the Apache License, Version 2.0 (the<br />
+* &quot;License&quot;); you may not use this file except in compliance<br />
+* with the License. You may obtain a copy of the License at<br />
+*<br />
+*     http://www.apache.org/licenses/LICENSE-2.0<br />
+*<br />
+* Unless required by applicable law or agreed to in writing,<br />
+* software distributed under the License is distributed on an<br />
+* &quot;AS IS&quot; BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY<br />
+* KIND, either express or implied. See the License for the<br />
+* specific language governing permissions and limitations under the License.<br />
+* <br />
+  * This is the file description.<br />
+  */</p>
+<p>Class Header<br />
+  Each class should be documented.</p>
+<p>/**<br />
+  * This documents the class below.<br />
+  * @package SomePackage (if applicable)<br />
+  */<br />
+  class SomeClass {<br />
+  // each class data member should also be documented<br />
+  // see below for variable documentation examples<br />
+  }</p>
+<p>Methods and Functions<br />
+  Methods and functions should also be documented.</p>
+<p>/**<br />
+  * Sample method/function docblock here.<br />
+  * @param string $paramName sample parameter of type string<br />
+  * @param boolean $boolParam sample parameter of type boolean <br />
+  * @return integer sample return value of type integer <br />
+  */<br />
+  function SomeFunction($paramName, $boolParam) {<br />
+  //...<br />
+  return 1;<br />
+  }</p>
+<p>Variable tags<br />
+  Some usefule variable tags:</p>
+<p> * @access [private|protected] (public is assumed when no @access tag is used) Private variables will not be shown in generated documentation.<br />
+  * @var datatype description (dataype mixed is assumed when no @var tag used)</p>
+<p>/**<br />
+  * User id for database lookup<br />
+  * @access private <br />
+  * @var integer user id in ldap <br />
+  */<br />
+  private $userId; </p>
+<p>General docblocks<br />
+  Add docblocks elsewhere as needed:</p>
+<p>/**<br />
+  * Why this include file is important<br />
+  */<br />
+  require_once 'includeFile.php'; </p>
+<p>Whitespace</p>
+<p>Long identifiers or values present problems for aligned initialization lists, so always prefer non-aliged initialization.</p>
+<p>Best Practices<br />
+  Magic Quotes<br />
+  There are 3 rules when it comes to the magic_quotes_* settings of PHP:</p>
+<p> 1. Turn them off.<br />
+  2. Turn them off.<br />
+  3. Make sure they're turned off (they default to on)</p>
+<p>In a nutshell: they modify data without your knowledge, are being removed from PHP 6, are deplored by the entire PHP community, do not provide any security, do not really do what you expect in general and make life as a developer much, much harder. Turn them off.</p>
+<p>Error Reporting<br />
+  Set error reporting to, at least, E_ALL|E_NOTICE. Use E_STRICT if you can, but this may not always be possible. This will generate a lot of messages, but eliminating them will create much more reliable code. While it is easy to ignore unset indices because they are expected to return NULL, this often obscures another bug: Why is it unset to begin with?</p>
+<p>Avoid References</p>
+<p>Avoid using references because they can create quite a headache and it introduces the potential for side-effects from functions. They should only be used when passing around very large amounts of data or when absolutely necessary. Also, remember that 'references' are somewhat of a misnomer in PHP. They are more like unix symlinks than a C pointer. See the PHP manual on references for more information: http://www.php.net/manual/en/language.references.php</p>
+<p>Avoid addslashes() and stripslashes()<br />
+  Do not use these functions to escape or otherwise sanitize data. Many languages do not use, or have more than just, \ as an escape character. Be aware that using these functions requires knowledge of if the data was previously stripped or added, though, making it very difficult to use them within functions that will be called more than one.</p>
+<p>As a general rule do not use either of them.</p>
+<p>When to encode and escape data<br />
+  Data should be kept in as clean a state as long as possible. Only encode data when it is being displayed or when absolutely required.</p>
+<p> * DO Encode and escape:<br />
+  o When doing output. htmlspecialchars($str, ENT_QUOTES) for html data. urlencode() for (surprisingly) urls. JavaScript will require addslashes() and htmlspecialchars().<br />
+  o Use bind parameters for database queries if possible, otherwise use the appropriate escape function (eg, mysql_real_escape_string())<br />
+  o When sending formatted data to preserve original meaning. This is for situations when you are using cURL or making remote requests and run into situation and have a data string such as:</p>
+<p>$name = &quot;me&amp;you&lt;3&quot;; $data = &quot;name=$name&quot;;</p>
+<p> * DO NOT Encode:<br />
+  o Data saved to the datastore (unless special circumstances). The reasoning is similar to why one should avoid addslashes(). When you pull data out of the datastore you must assume it is or isn't encoded, and all subsequent code must operate on that assumption, too. You still need to escape it, though.</p>
+<p>PHP Specific Notes about Control Structures, Functions, Misc<br />
+  Referencial foreach (), foreach ($array as $key =&gt; &amp; $value) {<br />
+  Overview: the $value still exists as a reference when the loop is done, and rebinding the value will rebind it _as a reference_</p>
+<p>This means two things: 1. Do not use it as it does not do what you expect and it's side effects are very hard to understand for other people. Design your code in a different way.</p>
+<p>2. Modifying the $value after the loop is done will affect the last entry of the array, thus, you should unset() the $value to prevent accidentally modifying it.</p>
+<p>foreach ($array as $key =&gt; &amp; $value) {<br />
+  // ...code...<br />
+  } unset ($value); // unset is on the same line as }</p>
+<p>The purpose of putting unset() on the same line is to think of it as a language requirement rather than a coding practice.</p>
+<p>3. Rebinding or copying the $value means it is a reference, and it will be assigned as such in subsequent code. This means, in a sense, those elements of the array cease to be the default copy-by-value. If you do a var_dump() of the two arrays, you'll notice that each element is a reference and they're referring to the same data.</p>
+<p>$a = array(&quot;one&quot;, &quot;two&quot;, &quot;three&quot;);<br />
+  foreach($a as &amp;$value) {<br />
+  $value = &quot;$value ref&quot;;<br />
+  }</p>
+<p>$b = $a;<br />
+  $b[0] = &quot;b one&quot;<br />
+  print $a; // &quot;b one&quot;, &quot;two ref&quot;, &quot;three ref&quot;<br />
+</p>
+<p>Magic PHP Features</p>
+<p>Do not use:</p>
+<p> * object member overloading because it harms readability.</p>
+<p>Do use:</p>
+<p> * __autoload, sparingly. It can greatly simplify dependency issues with sessions and reduces the i/o of require/include, but can make code harder to understand since you don't know what is being included until runtime.<br />
+  * __clone()<br />
+  * __toString to implement default HTML output.</p>
+<p>Avoid: __set_state</p>
+<p>See http://www.php.net/manual/en/language.oop5.magic.php for a complete list of magic methods. Also see http://us3.php.net/manual/en/language.constants.predefined.php for a list of magic constants.</p>
+<p>&nbsp;</p>
+</body>
+</html>
diff --git a/trunk/php/external/OAuth/OAuth.php b/trunk/php/external/OAuth/OAuth.php
new file mode 100644
index 0000000..39464e8
--- /dev/null
+++ b/trunk/php/external/OAuth/OAuth.php
@@ -0,0 +1,879 @@
+<?php
+// vim: foldmethod=marker
+
+/* Generic exception class
+ */
+class OAuthException extends Exception {
+  // pass
+}
+
+class OAuthConsumer {
+  public $key;
+  public $secret;
+
+  function __construct($key, $secret, $callback_url=NULL) {
+    $this->key = $key;
+    $this->secret = $secret;
+    $this->callback_url = $callback_url;
+  }
+
+  function __toString() {
+    return "OAuthConsumer[key=$this->key,secret=$this->secret]";
+  }
+}
+
+class OAuthToken {
+  // access tokens and request tokens
+  public $key;
+  public $secret;
+
+  /**
+   * key = the token
+   * secret = the token secret
+   */
+  function __construct($key, $secret) {
+    $this->key = $key;
+    $this->secret = $secret;
+  }
+
+  /**
+   * generates the basic string serialization of a token that a server
+   * would respond to request_token and access_token calls with
+   */
+  function to_string() {
+    return "oauth_token=" .
+           OAuthUtil::urlencode_rfc3986($this->key) .
+           "&oauth_token_secret=" .
+           OAuthUtil::urlencode_rfc3986($this->secret);
+  }
+
+  function __toString() {
+    return $this->to_string();
+  }
+}
+
+/**
+ * A class for implementing a Signature Method
+ * See section 9 ("Signing Requests") in the spec
+ */
+abstract class OAuthSignatureMethod {
+  /**
+   * Needs to return the name of the Signature Method (ie HMAC-SHA1)
+   * @return string
+   */
+  abstract public function get_name();
+
+  /**
+   * Build up the signature
+   * NOTE: The output of this function MUST NOT be urlencoded.
+   * the encoding is handled in OAuthRequest when the final
+   * request is serialized
+   * @param OAuthRequest $request
+   * @param OAuthConsumer $consumer
+   * @param OAuthToken $token
+   * @return string
+   */
+  abstract public function build_signature($request, $consumer, $token);
+
+  /**
+   * Verifies that a given signature is correct
+   * @param OAuthRequest $request
+   * @param OAuthConsumer $consumer
+   * @param OAuthToken $token
+   * @param string $signature
+   * @return bool
+   */
+  public function check_signature($request, $consumer, $token, $signature) {
+    $built = $this->build_signature($request, $consumer, $token);
+    return $built == $signature;
+  }
+}
+
+/**
+ * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104]
+ * where the Signature Base String is the text and the key is the concatenated values (each first
+ * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&'
+ * character (ASCII code 38) even if empty.
+ *   - Chapter 9.2 ("HMAC-SHA1")
+ */
+class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
+  function get_name() {
+    return "HMAC-SHA1";
+  }
+
+  public function build_signature($request, $consumer, $token) {
+    $base_string = $request->get_signature_base_string();
+    $request->base_string = $base_string;
+
+    $key_parts = array(
+      $consumer->secret,
+      ($token) ? $token->secret : ""
+    );
+
+    $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
+    $key = implode('&', $key_parts);
+
+    return base64_encode(hash_hmac('sha1', $base_string, $key, true));
+  }
+}
+
+/**
+ * The PLAINTEXT method does not provide any security protection and SHOULD only be used
+ * over a secure channel such as HTTPS. It does not use the Signature Base String.
+ *   - Chapter 9.4 ("PLAINTEXT")
+ */
+class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
+  public function get_name() {
+    return "PLAINTEXT";
+  }
+
+  /**
+   * oauth_signature is set to the concatenated encoded values of the Consumer Secret and
+   * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is
+   * empty. The result MUST be encoded again.
+   *   - Chapter 9.4.1 ("Generating Signatures")
+   *
+   * Please note that the second encoding MUST NOT happen in the SignatureMethod, as
+   * OAuthRequest handles this!
+   */
+  public function build_signature($request, $consumer, $token) {
+    $key_parts = array(
+      $consumer->secret,
+      ($token) ? $token->secret : ""
+    );
+
+    $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
+    $key = implode('&', $key_parts);
+    $request->base_string = $key;
+
+    return $key;
+  }
+}
+
+/**
+ * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in
+ * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for
+ * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a
+ * verified way to the Service Provider, in a manner which is beyond the scope of this
+ * specification.
+ *   - Chapter 9.3 ("RSA-SHA1")
+ */
+abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
+  public function get_name() {
+    return "RSA-SHA1";
+  }
+
+  // Up to the SP to implement this lookup of keys. Possible ideas are:
+  // (1) do a lookup in a table of trusted certs keyed off of consumer
+  // (2) fetch via http using a url provided by the requester
+  // (3) some sort of specific discovery code based on request
+  //
+  // Either way should return a string representation of the certificate
+  protected abstract function fetch_public_cert(&$request);
+
+  // Up to the SP to implement this lookup of keys. Possible ideas are:
+  // (1) do a lookup in a table of trusted certs keyed off of consumer
+  //
+  // Either way should return a string representation of the certificate
+  protected abstract function fetch_private_cert(&$request);
+
+  public function build_signature($request, $consumer, $token) {
+    $base_string = $request->get_signature_base_string();
+    $request->base_string = $base_string;
+
+    // Fetch the private key cert based on the request
+    $cert = $this->fetch_private_cert($request);
+
+    // Pull the private key ID from the certificate
+    $privatekeyid = openssl_get_privatekey($cert);
+
+    // Sign using the key
+    $ok = openssl_sign($base_string, $signature, $privatekeyid);
+
+    // Release the key resource
+    openssl_free_key($privatekeyid);
+
+    return base64_encode($signature);
+  }
+
+  public function check_signature($request, $consumer, $token, $signature) {
+    $decoded_sig = base64_decode($signature);
+
+    $base_string = $request->get_signature_base_string();
+
+    // Fetch the public key cert based on the request
+    $cert = $this->fetch_public_cert($request);
+
+    // Pull the public key ID from the certificate
+    $publickeyid = openssl_get_publickey($cert);
+
+    // Check the computed signature against the one passed in the query
+    $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
+
+    // Release the key resource
+    openssl_free_key($publickeyid);
+
+    return $ok == 1;
+  }
+}
+
+class OAuthRequest {
+  protected $parameters;
+  protected $http_method;
+  protected $http_url;
+  // for debug purposes
+  public $base_string;
+  public static $version = '1.0';
+  public static $POST_INPUT = 'php://input';
+
+  function __construct($http_method, $http_url, $parameters=NULL) {
+    $parameters = ($parameters) ? $parameters : array();
+    $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters);
+    $this->parameters = $parameters;
+    $this->http_method = $http_method;
+    $this->http_url = $http_url;
+  }
+
+
+  /**
+   * attempt to build up a request from what was passed to the server
+   */
+  public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
+    $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
+              ? 'http'
+              : 'https';
+    $http_url = ($http_url) ? $http_url : $scheme .
+                              '://' . $_SERVER['HTTP_HOST'] .
+                              ':' .
+                              $_SERVER['SERVER_PORT'] .
+                              $_SERVER['REQUEST_URI'];
+    $http_method = ($http_method) ? $http_method : $_SERVER['REQUEST_METHOD'];
+
+    // We weren't handed any parameters, so let's find the ones relevant to
+    // this request.
+    // If you run XML-RPC or similar you should use this to provide your own
+    // parsed parameter-list
+    if (!$parameters) {
+      // Find request headers
+      $request_headers = OAuthUtil::get_headers();
+
+      // Parse the query-string to find GET parameters
+      $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
+
+      // It's a POST request of the proper content-type, so parse POST
+      // parameters and add those overriding any duplicates from GET
+      if ($http_method == "POST"
+          &&  isset($request_headers['Content-Type'])
+          && strstr($request_headers['Content-Type'],
+                     'application/x-www-form-urlencoded')
+          ) {
+        $post_data = OAuthUtil::parse_parameters(
+          file_get_contents(self::$POST_INPUT)
+        );
+        $parameters = array_merge($parameters, $post_data);
+      }
+
+      // We have a Authorization-header with OAuth data. Parse the header
+      // and add those overriding any duplicates from GET or POST
+      if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') {
+        $header_parameters = OAuthUtil::split_header(
+          $request_headers['Authorization']
+        );
+        $parameters = array_merge($parameters, $header_parameters);
+      }
+
+    }
+
+    return new OAuthRequest($http_method, $http_url, $parameters);
+  }
+
+  /**
+   * pretty much a helper function to set up the request
+   */
+  public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
+    $parameters = ($parameters) ?  $parameters : array();
+    $defaults = array("oauth_version" => OAuthRequest::$version,
+                      "oauth_nonce" => OAuthRequest::generate_nonce(),
+                      "oauth_timestamp" => OAuthRequest::generate_timestamp(),
+                      "oauth_consumer_key" => $consumer->key);
+    if ($token)
+      $defaults['oauth_token'] = $token->key;
+
+    $parameters = array_merge($defaults, $parameters);
+
+    return new OAuthRequest($http_method, $http_url, $parameters);
+  }
+
+  public function set_parameter($name, $value, $allow_duplicates = true) {
+    if ($allow_duplicates && isset($this->parameters[$name])) {
+      // We have already added parameter(s) with this name, so add to the list
+      if (is_scalar($this->parameters[$name])) {
+        // This is the first duplicate, so transform scalar (string)
+        // into an array so we can add the duplicates
+        $this->parameters[$name] = array($this->parameters[$name]);
+      }
+
+      $this->parameters[$name][] = $value;
+    } else {
+      $this->parameters[$name] = $value;
+    }
+  }
+
+  public function get_parameter($name) {
+    return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
+  }
+
+  public function get_parameters() {
+    return $this->parameters;
+  }
+
+  public function unset_parameter($name) {
+    unset($this->parameters[$name]);
+  }
+
+  /**
+   * The request parameters, sorted and concatenated into a normalized string.
+   * @return string
+   */
+  public function get_signable_parameters() {
+    // Grab all parameters
+    $params = $this->parameters;
+
+    // Remove oauth_signature if present
+    // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
+    if (isset($params['oauth_signature'])) {
+      unset($params['oauth_signature']);
+    }
+
+    return OAuthUtil::build_http_query($params);
+  }
+
+  /**
+   * Returns the base string of this request
+   *
+   * The base string defined as the method, the url
+   * and the parameters (normalized), each urlencoded
+   * and the concated with &.
+   */
+  public function get_signature_base_string() {
+    $parts = array(
+      $this->get_normalized_http_method(),
+      $this->get_normalized_http_url(),
+      $this->get_signable_parameters()
+    );
+
+    $parts = OAuthUtil::urlencode_rfc3986($parts);
+
+    return implode('&', $parts);
+  }
+
+  /**
+   * just uppercases the http method
+   */
+  public function get_normalized_http_method() {
+    return strtoupper($this->http_method);
+  }
+
+  /**
+   * parses the url and rebuilds it to be
+   * scheme://host/path
+   */
+  public function get_normalized_http_url() {
+    $parts = parse_url($this->http_url);
+
+    $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http';
+    $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80');
+    $host = (isset($parts['host'])) ? $parts['host'] : '';
+    $path = (isset($parts['path'])) ? $parts['path'] : '';
+
+    if (($scheme == 'https' && $port != '443')
+        || ($scheme == 'http' && $port != '80')) {
+      $host = "$host:$port";
+    }
+    return "$scheme://$host$path";
+  }
+
+  /**
+   * builds a url usable for a GET request
+   */
+  public function to_url() {
+    $post_data = $this->to_postdata();
+    $out = $this->get_normalized_http_url();
+    if ($post_data) {
+      $out .= '?'.$post_data;
+    }
+    return $out;
+  }
+
+  /**
+   * builds the data one would send in a POST request
+   */
+  public function to_postdata() {
+    return OAuthUtil::build_http_query($this->parameters);
+  }
+
+  /**
+   * builds the Authorization: header
+   */
+  public function to_header($realm=null) {
+    $first = true;
+	if($realm) {
+      $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"';
+      $first = false;
+    } else
+      $out = 'Authorization: OAuth';
+
+    $total = array();
+    foreach ($this->parameters as $k => $v) {
+      if (substr($k, 0, 5) != "oauth") continue;
+      if (is_array($v)) {
+        throw new OAuthException('Arrays not supported in headers');
+      }
+      $out .= ($first) ? ' ' : ',';
+      $out .= OAuthUtil::urlencode_rfc3986($k) .
+              '="' .
+              OAuthUtil::urlencode_rfc3986($v) .
+              '"';
+      $first = false;
+    }
+    return $out;
+  }
+
+  public function __toString() {
+    return $this->to_url();
+  }
+
+
+  public function sign_request($signature_method, $consumer, $token) {
+    $this->set_parameter(
+      "oauth_signature_method",
+      $signature_method->get_name(),
+      false
+    );
+    $signature = $this->build_signature($signature_method, $consumer, $token);
+    $this->set_parameter("oauth_signature", $signature, false);
+  }
+
+  public function build_signature($signature_method, $consumer, $token) {
+    $signature = $signature_method->build_signature($this, $consumer, $token);
+    return $signature;
+  }
+
+  /**
+   * util function: current timestamp
+   */
+  private static function generate_timestamp() {
+    return time();
+  }
+
+  /**
+   * util function: current nonce
+   */
+  private static function generate_nonce() {
+    $mt = microtime();
+    $rand = mt_rand();
+
+    return md5($mt . $rand); // md5s look nicer than numbers
+  }
+}
+
+class OAuthServer {
+  protected $timestamp_threshold = 300; // in seconds, five minutes
+  protected $version = '1.0';             // hi blaine
+  protected $signature_methods = array();
+
+  protected $data_store;
+
+  function __construct($data_store) {
+    $this->data_store = $data_store;
+  }
+
+  public function add_signature_method($signature_method) {
+    $this->signature_methods[$signature_method->get_name()] =
+      $signature_method;
+  }
+
+  // high level functions
+
+  /**
+   * process a request_token request
+   * returns the request token on success
+   */
+  public function fetch_request_token(&$request) {
+    $this->get_version($request);
+
+    $consumer = $this->get_consumer($request);
+
+    // no token required for the initial token request
+    $token = NULL;
+
+    $this->check_signature($request, $consumer, $token);
+
+    // Rev A change
+    $callback = $request->get_parameter('oauth_callback');
+    $new_token = $this->data_store->new_request_token($consumer, $callback);
+
+    return $new_token;
+  }
+
+  /**
+   * process an access_token request
+   * returns the access token on success
+   */
+  public function fetch_access_token(&$request) {
+    $this->get_version($request);
+
+    $consumer = $this->get_consumer($request);
+
+    // requires authorized request token
+    $token = $this->get_token($request, $consumer, "request");
+
+    $this->check_signature($request, $consumer, $token);
+
+    // Rev A change
+    $verifier = $request->get_parameter('oauth_verifier');
+    $new_token = $this->data_store->new_access_token($token, $consumer, $verifier);
+
+    return $new_token;
+  }
+
+  /**
+   * verify an api call, checks all the parameters
+   */
+  public function verify_request(&$request) {
+    $this->get_version($request);
+    $consumer = $this->get_consumer($request);
+    $token = $this->get_token($request, $consumer, "access");
+    $this->check_signature($request, $consumer, $token);
+    return array($consumer, $token);
+  }
+
+  // Internals from here
+  /**
+   * version 1
+   */
+  private function get_version(&$request) {
+    $version = $request->get_parameter("oauth_version");
+    if (!$version) {
+      // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present.
+      // Chapter 7.0 ("Accessing Protected Ressources")
+      $version = '1.0';
+    }
+    if ($version !== $this->version) {
+      throw new OAuthException("OAuth version '$version' not supported");
+    }
+    return $version;
+  }
+
+  /**
+   * figure out the signature with some defaults
+   */
+  private function get_signature_method($request) {
+    $signature_method = $request instanceof OAuthRequest
+        ? $request->get_parameter("oauth_signature_method")
+        : NULL;
+
+    if (!$signature_method) {
+      // According to chapter 7 ("Accessing Protected Ressources") the signature-method
+      // parameter is required, and we can't just fallback to PLAINTEXT
+      throw new OAuthException('No signature method parameter. This parameter is required');
+    }
+
+    if (!in_array($signature_method,
+                  array_keys($this->signature_methods))) {
+      throw new OAuthException(
+        "Signature method '$signature_method' not supported " .
+        "try one of the following: " .
+        implode(", ", array_keys($this->signature_methods))
+      );
+    }
+    return $this->signature_methods[$signature_method];
+  }
+
+  /**
+   * try to find the consumer for the provided request's consumer key
+   */
+  private function get_consumer($request) {
+    $consumer_key = $request instanceof OAuthRequest
+        ? $request->get_parameter("oauth_consumer_key")
+        : NULL;
+
+    if (!$consumer_key) {
+      throw new OAuthException("Invalid consumer key");
+    }
+
+    $consumer = $this->data_store->lookup_consumer($consumer_key);
+    if (!$consumer) {
+      throw new OAuthException("Invalid consumer");
+    }
+
+    return $consumer;
+  }
+
+  /**
+   * try to find the token for the provided request's token key
+   */
+  private function get_token($request, $consumer, $token_type="access") {
+    $token_field = $request instanceof OAuthRequest
+         ? $request->get_parameter('oauth_token')
+         : NULL;
+
+    $token = $this->data_store->lookup_token(
+      $consumer, $token_type, $token_field
+    );
+    if (!$token) {
+      throw new OAuthException("Invalid $token_type token: $token_field");
+    }
+    return $token;
+  }
+
+  /**
+   * all-in-one function to check the signature on a request
+   * should guess the signature method appropriately
+   */
+  private function check_signature($request, $consumer, $token) {
+    // this should probably be in a different method
+    $timestamp = $request instanceof OAuthRequest
+        ? $request->get_parameter('oauth_timestamp')
+        : NULL;
+    $nonce = $request instanceof OAuthRequest
+        ? $request->get_parameter('oauth_nonce')
+        : NULL;
+
+    $this->check_timestamp($timestamp);
+    $this->check_nonce($consumer, $token, $nonce, $timestamp);
+
+    $signature_method = $this->get_signature_method($request);
+
+    $signature = $request->get_parameter('oauth_signature');
+    $valid_sig = $signature_method->check_signature(
+      $request,
+      $consumer,
+      $token,
+      $signature
+    );
+
+    if (!$valid_sig) {
+      throw new OAuthException("Invalid signature");
+    }
+  }
+
+  /**
+   * check that the timestamp is new enough
+   */
+  private function check_timestamp($timestamp) {
+    if( ! $timestamp )
+      throw new OAuthException(
+        'Missing timestamp parameter. The parameter is required'
+      );
+
+    // verify that timestamp is recentish
+    $now = time();
+    if (abs($now - $timestamp) > $this->timestamp_threshold) {
+      throw new OAuthException(
+        "Expired timestamp, yours $timestamp, ours $now"
+      );
+    }
+  }
+
+  /**
+   * check that the nonce is not repeated
+   */
+  private function check_nonce($consumer, $token, $nonce, $timestamp) {
+    if( ! $nonce )
+      throw new OAuthException(
+        'Missing nonce parameter. The parameter is required'
+      );
+
+    // verify that the nonce is uniqueish
+    $found = $this->data_store->lookup_nonce(
+      $consumer,
+      $token,
+      $nonce,
+      $timestamp
+    );
+    if ($found) {
+      throw new OAuthException("Nonce already used: $nonce");
+    }
+  }
+
+}
+
+class OAuthDataStore {
+  function lookup_consumer($consumer_key) {
+    // implement me
+  }
+
+  function lookup_token($consumer, $token_type, $token) {
+    // implement me
+  }
+
+  function lookup_nonce($consumer, $token, $nonce, $timestamp) {
+    // implement me
+  }
+
+  function new_request_token($consumer, $callback = null) {
+    // return a new token attached to this consumer
+  }
+
+  function new_access_token($token, $consumer, $verifier = null) {
+    // return a new access token attached to this consumer
+    // for the user associated with this token if the request token
+    // is authorized
+    // should also invalidate the request token
+  }
+
+}
+
+class OAuthUtil {
+  public static function urlencode_rfc3986($input) {
+  if (is_array($input)) {
+    return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
+  } else if (is_scalar($input)) {
+    return str_replace(
+      '+',
+      ' ',
+      str_replace('%7E', '~', rawurlencode($input))
+    );
+  } else {
+    return '';
+  }
+}
+
+
+  // This decode function isn't taking into consideration the above
+  // modifications to the encoding process. However, this method doesn't
+  // seem to be used anywhere so leaving it as is.
+  public static function urldecode_rfc3986($string) {
+    return urldecode($string);
+  }
+
+  // Utility function for turning the Authorization: header into
+  // parameters, has to do some unescaping
+  // Can filter out any non-oauth parameters if needed (default behaviour)
+  // May 28th, 2010 - method updated to tjerk.meesters for a speed improvement.
+  //                  see http://code.google.com/p/oauth/issues/detail?id=163
+  public static function split_header($header, $only_allow_oauth_parameters = true) {
+    $params = array();
+    if (preg_match_all('/('.($only_allow_oauth_parameters ? 'oauth_' : '').'[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches)) {
+      foreach ($matches[1] as $i => $h) {
+        $params[$h] = OAuthUtil::urldecode_rfc3986(empty($matches[3][$i]) ? $matches[4][$i] : $matches[3][$i]);
+      }
+      if (isset($params['realm'])) {
+        unset($params['realm']);
+      }
+    }
+    return $params;
+  }
+
+  // helper to try to sort out headers for people who aren't running apache
+  public static function get_headers() {
+    if (function_exists('apache_request_headers')) {
+      // we need this to get the actual Authorization: header
+      // because apache tends to tell us it doesn't exist
+      $headers = apache_request_headers();
+
+      // sanitize the output of apache_request_headers because
+      // we always want the keys to be Cased-Like-This and arh()
+      // returns the headers in the same case as they are in the
+      // request
+      $out = array();
+      foreach ($headers AS $key => $value) {
+        $key = str_replace(
+            " ",
+            "-",
+            ucwords(strtolower(str_replace("-", " ", $key)))
+          );
+        $out[$key] = $value;
+      }
+    } else {
+      // otherwise we don't have apache and are just going to have to hope
+      // that $_SERVER actually contains what we need
+      $out = array();
+      if( isset($_SERVER['CONTENT_TYPE']) )
+        $out['Content-Type'] = $_SERVER['CONTENT_TYPE'];
+      if( isset($_ENV['CONTENT_TYPE']) )
+        $out['Content-Type'] = $_ENV['CONTENT_TYPE'];
+
+      foreach ($_SERVER as $key => $value) {
+        if (substr($key, 0, 5) == "HTTP_") {
+          // this is chaos, basically it is just there to capitalize the first
+          // letter of every word that is not an initial HTTP and strip HTTP
+          // code from przemek
+          $key = str_replace(
+            " ",
+            "-",
+            ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
+          );
+          $out[$key] = $value;
+        }
+      }
+    }
+    return $out;
+  }
+
+  // This function takes a input like a=b&a=c&d=e and returns the parsed
+  // parameters like this
+  // array('a' => array('b','c'), 'd' => 'e')
+  public static function parse_parameters( $input ) {
+    if (!isset($input) || !$input) return array();
+
+    $pairs = explode('&', $input);
+
+    $parsed_parameters = array();
+    foreach ($pairs as $pair) {
+      $split = explode('=', $pair, 2);
+      $parameter = OAuthUtil::urldecode_rfc3986($split[0]);
+      $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';
+
+      if (isset($parsed_parameters[$parameter])) {
+        // We have already recieved parameter(s) with this name, so add to the list
+        // of parameters with this name
+
+        if (is_scalar($parsed_parameters[$parameter])) {
+          // This is the first duplicate, so transform scalar (string) into an array
+          // so we can add the duplicates
+          $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
+        }
+
+        $parsed_parameters[$parameter][] = $value;
+      } else {
+        $parsed_parameters[$parameter] = $value;
+      }
+    }
+    return $parsed_parameters;
+  }
+
+  public static function build_http_query($params) {
+    if (!$params) return '';
+
+    // Urlencode both keys and values
+    $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
+    $values = OAuthUtil::urlencode_rfc3986(array_values($params));
+    $params = array_combine($keys, $values);
+
+    // Parameters are sorted by name, using lexicographical byte value ordering.
+    // Ref: Spec: 9.1.1 (1)
+    uksort($params, 'strcmp');
+
+    $pairs = array();
+    foreach ($params as $parameter => $value) {
+      if (is_array($value)) {
+        // If two or more parameters share the same name, they are sorted by their value
+        // Ref: Spec: 9.1.1 (1)
+        // June 12th, 2010 - changed to sort because of issue 164 by hidetaka
+        sort($value, SORT_STRING);
+        foreach ($value as $duplicate_value) {
+          $pairs[] = $parameter . '=' . $duplicate_value;
+        }
+      } else {
+        $pairs[] = $parameter . '=' . $value;
+      }
+    }
+    // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
+    // Each name-value pair is separated by an '&' character (ASCII code 38)
+    return implode('&', $pairs);
+  }
+}
+
+?>
\ No newline at end of file
diff --git a/trunk/php/external/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php b/trunk/php/external/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php
new file mode 100644
index 0000000..278f510
--- /dev/null
+++ b/trunk/php/external/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php
@@ -0,0 +1,96 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ClassLoader;
+
+/**
+ * ApcUniversalClassLoader implements a "universal" autoloader cached in APC for PHP 5.3.
+ *
+ * It is able to load classes that use either:
+ *
+ *  * The technical interoperability standards for PHP 5.3 namespaces and
+ *    class names (http://groups.google.com/group/php-standards/web/psr-0-final-proposal);
+ *
+ *  * The PEAR naming convention for classes (http://pear.php.net/).
+ *
+ * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be
+ * looked for in a list of locations to ease the vendoring of a sub-set of
+ * classes for large projects.
+ *
+ * Example usage:
+ *
+ *     require 'vendor/symfony/src/Symfony/Component/ClassLoader/UniversalClassLoader.php';
+ *     require 'vendor/symfony/src/Symfony/Component/ClassLoader/ApcUniversalClassLoader.php';
+ *
+ *     use Symfony\Component\ClassLoader\ApcUniversalClassLoader;
+ *
+ *     $loader = new ApcUniversalClassLoader('apc.prefix.');
+ *
+ *     // register classes with namespaces
+ *     $loader->registerNamespaces(array(
+ *         'Symfony\Component' => __DIR__.'/component',
+ *         'Symfony'           => __DIR__.'/framework',
+ *         'Sensio'            => array(__DIR__.'/src', __DIR__.'/vendor'),
+ *     ));
+ *
+ *     // register a library using the PEAR naming convention
+ *     $loader->registerPrefixes(array(
+ *         'Swift_' => __DIR__.'/Swift',
+ *     ));
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Kris Wallsmith <kris@symfony.com>
+ *
+ * @api
+ */
+class ApcUniversalClassLoader extends UniversalClassLoader
+{
+    private $prefix;
+
+    /**
+     * Constructor.
+     *
+     * @param string $prefix A prefix to create a namespace in APC
+     *
+     * @api
+     */
+    public function __construct($prefix)
+    {
+        if (!extension_loaded('apc')) {
+            throw new \RuntimeException('Unable to use ApcUniversalClassLoader as APC is not enabled.');
+        }
+
+        $this->prefix = $prefix;
+    }
+
+    /**
+     * Finds a file by class name while caching lookups to APC.
+     *
+     * @param string $class A class name to resolve to file
+     */
+    public function findFile($class)
+    {
+        if (false === $file = apc_fetch($this->prefix.$class)) {
+            apc_store($this->prefix.$class, $file = parent::findFile($class));
+        }
+
+        return $file;
+    }
+}
diff --git a/trunk/php/external/Symfony/Component/ClassLoader/ClassCollectionLoader.php b/trunk/php/external/Symfony/Component/ClassLoader/ClassCollectionLoader.php
new file mode 100644
index 0000000..f13aafa
--- /dev/null
+++ b/trunk/php/external/Symfony/Component/ClassLoader/ClassCollectionLoader.php
@@ -0,0 +1,222 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ClassLoader;
+
+/**
+ * ClassCollectionLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ClassCollectionLoader
+{
+    static private $loaded;
+
+    /**
+     * Loads a list of classes and caches them in one big file.
+     *
+     * @param array   $classes    An array of classes to load
+     * @param string  $cacheDir   A cache directory
+     * @param string  $name       The cache name prefix
+     * @param Boolean $autoReload Whether to flush the cache when the cache is stale or not
+     * @param Boolean $adaptive   Whether to remove already declared classes or not
+     * @param string  $extension  File extension of the resulting file
+     *
+     * @throws \InvalidArgumentException When class can't be loaded
+     */
+    static public function load($classes, $cacheDir, $name, $autoReload, $adaptive = false, $extension = '.php')
+    {
+        // each $name can only be loaded once per PHP process
+        if (isset(self::$loaded[$name])) {
+            return;
+        }
+
+        self::$loaded[$name] = true;
+
+        if ($adaptive) {
+            // don't include already declared classes
+            $classes = array_diff($classes, get_declared_classes(), get_declared_interfaces());
+
+            // the cache is different depending on which classes are already declared
+            $name = $name.'-'.substr(md5(implode('|', $classes)), 0, 5);
+        }
+
+        $cache = $cacheDir.'/'.$name.$extension;
+
+        // auto-reload
+        $reload = false;
+        if ($autoReload) {
+            $metadata = $cacheDir.'/'.$name.$extension.'.meta';
+            if (!is_file($metadata) || !is_file($cache)) {
+                $reload = true;
+            } else {
+                $time = filemtime($cache);
+                $meta = unserialize(file_get_contents($metadata));
+
+                if ($meta[1] != $classes) {
+                    $reload = true;
+                } else {
+                    foreach ($meta[0] as $resource) {
+                        if (!is_file($resource) || filemtime($resource) > $time) {
+                            $reload = true;
+
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!$reload && is_file($cache)) {
+            require_once $cache;
+
+            return;
+        }
+
+        $files = array();
+        $content = '';
+        foreach ($classes as $class) {
+            if (!class_exists($class) && !interface_exists($class)) {
+                throw new \InvalidArgumentException(sprintf('Unable to load class "%s"', $class));
+            }
+
+            $r = new \ReflectionClass($class);
+            $files[] = $r->getFileName();
+
+            $c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($r->getFileName()));
+
+            // add namespace declaration for global code
+            if (!$r->inNamespace()) {
+                $c = "\nnamespace\n{\n".self::stripComments($c)."\n}\n";
+            } else {
+                $c = self::fixNamespaceDeclarations('<?php '.$c);
+                $c = preg_replace('/^\s*<\?php/', '', $c);
+            }
+
+            $content .= $c;
+        }
+
+        // cache the core classes
+        if (!is_dir(dirname($cache))) {
+            mkdir(dirname($cache), 0777, true);
+        }
+        self::writeCacheFile($cache, '<?php '.$content);
+
+        if ($autoReload) {
+            // save the resources
+            self::writeCacheFile($metadata, serialize(array($files, $classes)));
+        }
+    }
+
+    /**
+     * Adds brackets around each namespace if it's not already the case.
+     *
+     * @param string $source Namespace string
+     *
+     * @return string Namespaces with brackets
+     */
+    static public function fixNamespaceDeclarations($source)
+    {
+        if (!function_exists('token_get_all')) {
+            return $source;
+        }
+
+        $output = '';
+        $inNamespace = false;
+        $tokens = token_get_all($source);
+
+        for ($i = 0, $max = count($tokens); $i < $max; $i++) {
+            $token = $tokens[$i];
+            if (is_string($token)) {
+                $output .= $token;
+            } elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
+                // strip comments
+                continue;
+            } elseif (T_NAMESPACE === $token[0]) {
+                if ($inNamespace) {
+                    $output .= "}\n";
+                }
+                $output .= $token[1];
+
+                // namespace name and whitespaces
+                while (($t = $tokens[++$i]) && is_array($t) && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) {
+                    $output .= $t[1];
+                }
+                if (is_string($t) && '{' === $t) {
+                    $inNamespace = false;
+                    --$i;
+                } else {
+                    $output .= "\n{";
+                    $inNamespace = true;
+                }
+            } else {
+                $output .= $token[1];
+            }
+        }
+
+        if ($inNamespace) {
+            $output .= "}\n";
+        }
+
+        return $output;
+    }
+
+    /**
+     * Writes a cache file.
+     *
+     * @param string $file Filename
+     * @param string $content Temporary file content
+     *
+     * @throws \RuntimeException when a cache file cannot be written
+     */
+    static private function writeCacheFile($file, $content)
+    {
+        $tmpFile = tempnam(dirname($file), basename($file));
+        if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $file)) {
+            chmod($file, 0644);
+
+            return;
+        }
+
+        throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file));
+    }
+
+    /**
+     * Removes comments from a PHP source string.
+     *
+     * We don't use the PHP php_strip_whitespace() function
+     * as we want the content to be readable and well-formatted.
+     *
+     * @param string $source A PHP string
+     *
+     * @return string The PHP string with the comments removed
+     */
+    static private function stripComments($source)
+    {
+        if (!function_exists('token_get_all')) {
+            return $source;
+        }
+
+        $output = '';
+        foreach (token_get_all($source) as $token) {
+            if (is_string($token)) {
+                $output .= $token;
+            } elseif (!in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
+                $output .= $token[1];
+            }
+        }
+
+        // replace multiple new lines with a single newline
+        $output = preg_replace(array('/\s+$/Sm', '/\n+/S'), "\n", $output);
+
+        return $output;
+    }
+}
diff --git a/trunk/php/external/Symfony/Component/ClassLoader/DebugUniversalClassLoader.php b/trunk/php/external/Symfony/Component/ClassLoader/DebugUniversalClassLoader.php
new file mode 100644
index 0000000..638a46a
--- /dev/null
+++ b/trunk/php/external/Symfony/Component/ClassLoader/DebugUniversalClassLoader.php
@@ -0,0 +1,63 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ClassLoader;
+
+/**
+ * Checks that the class is actually declared in the included file.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class DebugUniversalClassLoader extends UniversalClassLoader
+{
+    /**
+     * Replaces all regular UniversalClassLoader instances by a DebugUniversalClassLoader ones.
+     */
+    static public function enable()
+    {
+        if (!is_array($functions = spl_autoload_functions())) {
+            return;
+        }
+
+        foreach ($functions as $function) {
+            spl_autoload_unregister($function);
+        }
+
+        foreach ($functions as $function) {
+            if (is_array($function) && $function[0] instanceof UniversalClassLoader) {
+                $loader = new static();
+                $loader->registerNamespaceFallbacks($function[0]->getNamespaceFallbacks());
+                $loader->registerPrefixFallbacks($function[0]->getPrefixFallbacks());
+                $loader->registerNamespaces($function[0]->getNamespaces());
+                $loader->registerPrefixes($function[0]->getPrefixes());
+                $loader->useIncludePath($function[0]->getUseIncludePath());
+
+                $function[0] = $loader;
+            }
+
+            spl_autoload_register($function);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            require $file;
+
+            if (!class_exists($class, false) && !interface_exists($class, false)) {
+                throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file));
+            }
+        }
+    }
+}
diff --git a/trunk/php/external/Symfony/Component/ClassLoader/LICENSE b/trunk/php/external/Symfony/Component/ClassLoader/LICENSE
new file mode 100644
index 0000000..89df448
--- /dev/null
+++ b/trunk/php/external/Symfony/Component/ClassLoader/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2004-2011 Fabien Potencier
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/trunk/php/external/Symfony/Component/ClassLoader/MapClassLoader.php b/trunk/php/external/Symfony/Component/ClassLoader/MapClassLoader.php
new file mode 100644
index 0000000..cf17d42
--- /dev/null
+++ b/trunk/php/external/Symfony/Component/ClassLoader/MapClassLoader.php
@@ -0,0 +1,76 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ClassLoader;
+
+/**
+ * A class loader that uses a mapping file to look up paths.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class MapClassLoader
+{
+    private $map = array();
+
+    /**
+     * Constructor.
+     *
+     * @param array $map A map where keys are classes and values the absolute file path
+     */
+    public function __construct(array $map)
+    {
+        $this->map = $map;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param Boolean $prepend Whether to prepend the autoloader or not
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param string $class The name of the class
+     */
+    public function loadClass($class)
+    {
+        if ('\\' === $class[0]) {
+            $class = substr($class, 1);
+        }
+
+        if (isset($this->map[$class])) {
+            require $this->map[$class];
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|null The path, if found
+     */
+    public function findFile($class)
+    {
+        if ('\\' === $class[0]) {
+            $class = substr($class, 1);
+        }
+
+        if (isset($this->map[$class])) {
+            return $this->map[$class];
+        }
+    }
+}
diff --git a/trunk/php/external/Symfony/Component/ClassLoader/UniversalClassLoader.php b/trunk/php/external/Symfony/Component/ClassLoader/UniversalClassLoader.php
new file mode 100644
index 0000000..c8d5d77
--- /dev/null
+++ b/trunk/php/external/Symfony/Component/ClassLoader/UniversalClassLoader.php
@@ -0,0 +1,299 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\ClassLoader;
+
+/**
+ * UniversalClassLoader implements a "universal" autoloader for PHP 5.3.
+ *
+ * It is able to load classes that use either:
+ *
+ *  * The technical interoperability standards for PHP 5.3 namespaces and
+ *    class names (http://groups.google.com/group/php-standards/web/psr-0-final-proposal);
+ *
+ *  * The PEAR naming convention for classes (http://pear.php.net/).
+ *
+ * Classes from a sub-namespace or a sub-hierarchy of PEAR classes can be
+ * looked for in a list of locations to ease the vendoring of a sub-set of
+ * classes for large projects.
+ *
+ * Example usage:
+ *
+ *     $loader = new UniversalClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->registerNamespaces(array(
+ *         'Symfony\Component' => __DIR__.'/component',
+ *         'Symfony'           => __DIR__.'/framework',
+ *         'Sensio'            => array(__DIR__.'/src', __DIR__.'/vendor'),
+ *     ));
+ *
+ *     // register a library using the PEAR naming convention
+ *     $loader->registerPrefixes(array(
+ *         'Swift_' => __DIR__.'/Swift',
+ *     ));
+ *
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->useIncludePath(true);
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ *
+ * @api
+ */
+class UniversalClassLoader
+{
+    private $namespaces = array();
+    private $prefixes = array();
+    private $namespaceFallbacks = array();
+    private $prefixFallbacks = array();
+    private $useIncludePath = false;
+
+    /**
+     * Turns on searching the include for class files. Allows easy loading
+     * of installed PEAR packages
+     *
+     * @param Boolean $useIncludePath
+     */
+    public function useIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return Boolean
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Gets the configured namespaces.
+     *
+     * @return array A hash with namespaces as keys and directories as values
+     */
+    public function getNamespaces()
+    {
+        return $this->namespaces;
+    }
+
+    /**
+     * Gets the configured class prefixes.
+     *
+     * @return array A hash with class prefixes as keys and directories as values
+     */
+    public function getPrefixes()
+    {
+        return $this->prefixes;
+    }
+
+    /**
+     * Gets the directory(ies) to use as a fallback for namespaces.
+     *
+     * @return array An array of directories
+     */
+    public function getNamespaceFallbacks()
+    {
+        return $this->namespaceFallbacks;
+    }
+
+    /**
+     * Gets the directory(ies) to use as a fallback for class prefixes.
+     *
+     * @return array An array of directories
+     */
+    public function getPrefixFallbacks()
+    {
+        return $this->prefixFallbacks;
+    }
+
+    /**
+     * Registers the directory to use as a fallback for namespaces.
+     *
+     * @param array $dirs An array of directories
+     *
+     * @api
+     */
+    public function registerNamespaceFallbacks(array $dirs)
+    {
+        $this->namespaceFallbacks = $dirs;
+    }
+
+    /**
+     * Registers the directory to use as a fallback for class prefixes.
+     *
+     * @param array $dirs An array of directories
+     *
+     * @api
+     */
+    public function registerPrefixFallbacks(array $dirs)
+    {
+        $this->prefixFallbacks = $dirs;
+    }
+
+    /**
+     * Registers an array of namespaces
+     *
+     * @param array $namespaces An array of namespaces (namespaces as keys and locations as values)
+     *
+     * @api
+     */
+    public function registerNamespaces(array $namespaces)
+    {
+        foreach ($namespaces as $namespace => $locations) {
+            $this->namespaces[$namespace] = (array) $locations;
+        }
+    }
+
+    /**
+     * Registers a namespace.
+     *
+     * @param string       $namespace The namespace
+     * @param array|string $paths     The location(s) of the namespace
+     *
+     * @api
+     */
+    public function registerNamespace($namespace, $paths)
+    {
+        $this->namespaces[$namespace] = (array) $paths;
+    }
+
+    /**
+     * Registers an array of classes using the PEAR naming convention.
+     *
+     * @param array $classes An array of classes (prefixes as keys and locations as values)
+     *
+     * @api
+     */
+    public function registerPrefixes(array $classes)
+    {
+        foreach ($classes as $prefix => $locations) {
+            $this->prefixes[$prefix] = (array) $locations;
+        }
+    }
+
+    /**
+     * Registers a set of classes using the PEAR naming convention.
+     *
+     * @param string       $prefix  The classes prefix
+     * @param array|string $paths   The location(s) of the classes
+     *
+     * @api
+     */
+    public function registerPrefix($prefix, $paths)
+    {
+        $this->prefixes[$prefix] = (array) $paths;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param Boolean $prepend Whether to prepend the autoloader or not
+     *
+     * @api
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param string $class The name of the class
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            require $file;
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|null The path, if found
+     */
+    public function findFile($class)
+    {
+        if ('\\' == $class[0]) {
+            $class = substr($class, 1);
+        }
+
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $namespace = substr($class, 0, $pos);
+            $className = substr($class, $pos + 1);
+            $normalizedClass = str_replace('\\', DIRECTORY_SEPARATOR, $namespace).DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $className).'.php';
+            foreach ($this->namespaces as $ns => $dirs) {
+                if (0 !== strpos($namespace, $ns)) {
+                    continue;
+                }
+
+                foreach ($dirs as $dir) {
+                    $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass;
+                    if (is_file($file)) {
+                        return $file;
+                    }
+                }
+            }
+
+            foreach ($this->namespaceFallbacks as $dir) {
+                $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass;
+                if (is_file($file)) {
+                    return $file;
+                }
+            }
+
+        } else {
+            // PEAR-like class name
+            $normalizedClass = str_replace('_', DIRECTORY_SEPARATOR, $class).'.php';
+            foreach ($this->prefixes as $prefix => $dirs) {
+                if (0 !== strpos($class, $prefix)) {
+                    continue;
+                }
+
+                foreach ($dirs as $dir) {
+                    $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass;
+                    if (is_file($file)) {
+                        return $file;
+                    }
+                }
+            }
+
+            foreach ($this->prefixFallbacks as $dir) {
+                $file = $dir.DIRECTORY_SEPARATOR.$normalizedClass;
+                if (is_file($file)) {
+                    return $file;
+                }
+            }
+        }
+
+        if ($this->useIncludePath && $file = stream_resolve_include_path($normalizedClass)) {
+            return $file;
+        }
+    }
+}
diff --git a/trunk/php/external/Zend/Exception.php b/trunk/php/external/Zend/Exception.php
new file mode 100644
index 0000000..d523891
--- /dev/null
+++ b/trunk/php/external/Zend/Exception.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Exception extends Exception {
+}
+
diff --git a/trunk/php/external/Zend/Feed.php b/trunk/php/external/Zend/Feed.php
new file mode 100644
index 0000000..feb4284
--- /dev/null
+++ b/trunk/php/external/Zend/Feed.php
@@ -0,0 +1,373 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Feed.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * Feed utility class
+ *
+ * Base Zend_Feed class, containing constants and the Zend_Http_Client instance
+ * accessor.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed {
+  
+  /**
+   * HTTP client object to use for retrieving feeds
+   *
+   * @var Zend_Http_Client
+   */
+  protected static $_httpClient = null;
+  
+  /**
+   * Override HTTP PUT and DELETE request methods?
+   *
+   * @var boolean
+   */
+  protected static $_httpMethodOverride = false;
+  
+  /**
+   * @var array
+   */
+  protected static $_namespaces = array('opensearch' => 'http://a9.com/-/spec/opensearchrss/1.0/', 
+      'atom' => 'http://www.w3.org/2005/Atom', 
+      'rss' => 'http://blogs.law.harvard.edu/tech/rss');
+
+  /**
+   * Set the HTTP client instance
+   *
+   * Sets the HTTP client object to use for retrieving the feeds.
+   *
+   * @param  Zend_Http_Client $httpClient
+   * @return void
+   */
+  public static function setHttpClient(Zend_Http_Client $httpClient) {
+    self::$_httpClient = $httpClient;
+  }
+
+  /**
+   * Gets the HTTP client object. If none is set, a new Zend_Http_Client will be used.
+   *
+   * @return Zend_Http_Client_Abstract
+   */
+  public static function getHttpClient() {
+    if (! self::$_httpClient instanceof Zend_Http_Client) {
+      /**
+       * @see Zend_Http_Client
+       */
+      require_once 'external/Zend/Http/Client.php';
+      self::$_httpClient = new Zend_Http_Client();
+    }
+    
+    return self::$_httpClient;
+  }
+
+  /**
+   * Toggle using POST instead of PUT and DELETE HTTP methods
+   *
+   * Some feed implementations do not accept PUT and DELETE HTTP
+   * methods, or they can't be used because of proxies or other
+   * measures. This allows turning on using POST where PUT and
+   * DELETE would normally be used; in addition, an
+   * X-Method-Override header will be sent with a value of PUT or
+   * DELETE as appropriate.
+   *
+   * @param  boolean $override Whether to override PUT and DELETE.
+   * @return void
+   */
+  public static function setHttpMethodOverride($override = true) {
+    self::$_httpMethodOverride = $override;
+  }
+
+  /**
+   * Get the HTTP override state
+   *
+   * @return boolean
+   */
+  public static function getHttpMethodOverride() {
+    return self::$_httpMethodOverride;
+  }
+
+  /**
+   * Get the full version of a namespace prefix
+   *
+   * Looks up a prefix (atom:, etc.) in the list of registered
+   * namespaces and returns the full namespace URI if
+   * available. Returns the prefix, unmodified, if it's not
+   * registered.
+   *
+   * @return string
+   */
+  public static function lookupNamespace($prefix) {
+    return isset(self::$_namespaces[$prefix]) ? self::$_namespaces[$prefix] : $prefix;
+  }
+
+  /**
+   * Add a namespace and prefix to the registered list
+   *
+   * Takes a prefix and a full namespace URI and adds them to the
+   * list of registered namespaces for use by
+   * Zend_Feed::lookupNamespace().
+   *
+   * @param  string $prefix The namespace prefix
+   * @param  string $namespaceURI The full namespace URI
+   * @return void
+   */
+  public static function registerNamespace($prefix, $namespaceURI) {
+    self::$_namespaces[$prefix] = $namespaceURI;
+  }
+
+  /**
+   * Imports a feed located at $uri.
+   *
+   * @param  string $uri
+   * @throws Zend_Feed_Exception
+   * @return Zend_Feed_Abstract
+   */
+  public static function import($uri) {
+    $client = self::getHttpClient();
+    $client->setUri($uri);
+    $response = $client->request('GET');
+    if ($response->getStatus() !== 200) {
+      /**
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('Feed failed to load, got response code ' . $response->getStatus());
+    }
+    $feed = $response->getBody();
+    return self::importString($feed);
+  }
+
+  /**
+   * Imports a feed represented by $string.
+   *
+   * @param  string $string
+   * @throws Zend_Feed_Exception
+   * @return Zend_Feed_Abstract
+   */
+  public static function importString($string) {
+    // Load the feed as an XML DOMDocument object
+    @ini_set('track_errors', 1);
+    $doc = @DOMDocument::loadXML($string);
+    @ini_restore('track_errors');
+    
+    if (! $doc) {
+      // prevent the class to generate an undefined variable notice (ZF-2590)
+      if (! isset($php_errormsg)) {
+        if (function_exists('xdebug_is_enabled')) {
+          $php_errormsg = '(error message not available, when XDebug is running)';
+        } else {
+          $php_errormsg = '(error message not available)';
+        }
+      }
+      
+      /**
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception("DOMDocument cannot parse XML: $php_errormsg");
+    }
+    
+    // Try to find the base feed element or a single <entry> of an Atom feed
+    if ($doc->getElementsByTagName('feed')->item(0) || $doc->getElementsByTagName('entry')->item(0)) {
+      /**
+       * @see Zend_Feed_Atom
+       */
+      require_once 'external/Zend/Feed/Atom.php';
+      // return a newly created Zend_Feed_Atom object
+      return new Zend_Feed_Atom(null, $string);
+    }
+    
+    // Try to find the base feed element of an RSS feed
+    if ($doc->getElementsByTagName('channel')->item(0)) {
+      /**
+       * @see Zend_Feed_Rss
+       */
+      require_once 'external/Zend/Feed/Rss.php';
+      // return a newly created Zend_Feed_Rss object
+      return new Zend_Feed_Rss(null, $string);
+    }
+    
+    // $string does not appear to be a valid feed of the supported types
+    /**
+     * @see Zend_Feed_Exception
+     */
+    require_once 'external/Zend/Feed/Exception.php';
+    throw new Zend_Feed_Exception('Invalid or unsupported feed format');
+  }
+
+  /**
+   * Imports a feed from a file located at $filename.
+   *
+   * @param  string $filename
+   * @throws Zend_Feed_Exception
+   * @return Zend_Feed_Abstract
+   */
+  public static function importFile($filename) {
+    @ini_set('track_errors', 1);
+    $feed = @file_get_contents($filename);
+    @ini_restore('track_errors');
+    if ($feed === false) {
+      /**
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception("File could not be loaded: $php_errormsg");
+    }
+    return self::importString($feed);
+  }
+
+  /**
+   * Attempts to find feeds at $uri referenced by <link ... /> tags. Returns an
+   * array of the feeds referenced at $uri.
+   *
+   * @todo Allow findFeeds() to follow one, but only one, code 302.
+   *
+   * @param  string $uri
+   * @throws Zend_Feed_Exception
+   * @return array
+   */
+  public static function findFeeds($uri) {
+    // Get the HTTP response from $uri and save the contents
+    $client = self::getHttpClient();
+    $client->setUri($uri);
+    $response = $client->request();
+    if ($response->getStatus() !== 200) {
+      /**
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception("Failed to access $uri, got response code " . $response->getStatus());
+    }
+    $contents = $response->getBody();
+    
+    // Parse the contents for appropriate <link ... /> tags
+    @ini_set('track_errors', 1);
+    $pattern = '~(<link[^>]+)/?>~i';
+    $result = @preg_match_all($pattern, $contents, $matches);
+    @ini_restore('track_errors');
+    if ($result === false) {
+      /**
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception("Internal error: $php_errormsg");
+    }
+    
+    // Try to fetch a feed for each link tag that appears to refer to a feed
+    $feeds = array();
+    if (isset($matches[1]) && count($matches[1]) > 0) {
+      foreach ($matches[1] as $link) {
+        // force string to be an utf-8 one
+        if (! mb_check_encoding($link, 'UTF-8')) {
+          $link = mb_convert_encoding($link, 'UTF-8');
+        }
+        $xml = @simplexml_load_string(rtrim($link, ' /') . ' />');
+        if ($xml === false) {
+          continue;
+        }
+        $attributes = $xml->attributes();
+        if (! isset($attributes['rel']) || ! @preg_match('~^(?:alternate|service\.feed)~i', $attributes['rel'])) {
+          continue;
+        }
+        if (! isset($attributes['type']) || ! @preg_match('~^application/(?:atom|rss|rdf)\+xml~', $attributes['type'])) {
+          continue;
+        }
+        if (! isset($attributes['href'])) {
+          continue;
+        }
+        try {
+          // checks if we need to canonize the given uri
+          try {
+            $uri = Zend_Uri::factory((string)$attributes['href']);
+          } catch (Zend_Uri_Exception $e) {
+            // canonize the uri
+            $path = (string)$attributes['href'];
+            $query = $fragment = '';
+            if (substr($path, 0, 1) != '/') {
+              // add the current root path to this one
+              $path = rtrim($client->getUri()->getPath(), '/') . '/' . $path;
+            }
+            if (strpos($path, '?') !== false) {
+              list($path, $query) = explode('?', $path, 2);
+            }
+            if (strpos($query, '#') !== false) {
+              list($query, $fragment) = explode('#', $query, 2);
+            }
+            $uri = Zend_Uri::factory($client->getUri(true));
+            $uri->setPath($path);
+            $uri->setQuery($query);
+            $uri->setFragment($fragment);
+          }
+          
+          $feed = self::import($uri);
+        } catch (Exception $e) {
+          continue;
+        }
+        $feeds[] = $feed;
+      }
+    }
+    
+    // Return the fetched feeds
+    return $feeds;
+  }
+
+  /**
+   * Construct a new Zend_Feed_Abstract object from a custom array
+   *
+   * @param  array  $data
+   * @param  string $format (rss|atom) the requested output format
+   * @return Zend_Feed_Abstract
+   */
+  public static function importArray(array $data, $format = 'atom') {
+    $obj = 'Zend_Feed_' . ucfirst(strtolower($format));
+    /**
+     * @see Zend_Loader
+     */
+    require_once 'external/Zend/Loader.php';
+    Zend_Loader::loadClass($obj);
+    Zend_Loader::loadClass('Zend_Feed_Builder');
+    
+    return new $obj(null, null, new Zend_Feed_Builder($data));
+  }
+
+  /**
+   * Construct a new Zend_Feed_Abstract object from a Zend_Feed_Builder_Interface data source
+   *
+   * @param  Zend_Feed_Builder_Interface $builder this object will be used to extract the data of the feed
+   * @param  string                      $format (rss|atom) the requested output format
+   * @return Zend_Feed_Abstract
+   */
+  public static function importBuilder(Zend_Feed_Builder_Interface $builder, $format = 'atom') {
+    $obj = 'Zend_Feed_' . ucfirst(strtolower($format));
+    /**
+     * @see Zend_Loader
+     */
+    require_once 'external/Zend/Loader.php';
+    Zend_Loader::loadClass($obj);
+    
+    return new $obj(null, null, $builder);
+  }
+}
diff --git a/trunk/php/external/Zend/Feed/Abstract.php b/trunk/php/external/Zend/Feed/Abstract.php
new file mode 100644
index 0000000..fdc0cb5
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Abstract.php
@@ -0,0 +1,234 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Abstract.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed_Element
+ */
+require_once 'external/Zend/Feed/Element.php';
+
+/**
+ * The Zend_Feed_Abstract class is an abstract class representing feeds.
+ *
+ * Zend_Feed_Abstract implements two core PHP 5 interfaces: ArrayAccess and
+ * Iterator. In both cases the collection being treated as an array is
+ * considered to be the entry collection, such that iterating over the
+ * feed takes you through each of the feed.s entries.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Feed_Abstract extends Zend_Feed_Element implements Iterator {
+  /**
+   * Current index on the collection of feed entries for the
+   * Iterator implementation.
+   *
+   * @var integer
+   */
+  protected $_entryIndex = 0;
+  
+  /**
+   * Cache of feed entries.
+   *
+   * @var array
+   */
+  protected $_entries;
+
+  /**
+   * Feed constructor
+   *
+   * The Zend_Feed_Abstract constructor takes the URI of a feed or a
+   * feed represented as a string and loads it as XML.
+   *
+   * @param  string $uri The full URI of the feed to load, or NULL if not retrieved via HTTP or as an array.
+   * @param  string $string The feed as a string, or NULL if retrieved via HTTP or as an array.
+   * @param  Zend_Feed_Builder_Interface $builder The feed as a builder instance or NULL if retrieved as a string or via HTTP.
+   * @return void
+   * @throws Zend_Feed_Exception If loading the feed failed.
+   */
+  public function __construct($uri = null, $string = null, Zend_Feed_Builder_Interface $builder = null) {
+    if ($uri !== null) {
+      // Retrieve the feed via HTTP
+      $client = Zend_Feed::getHttpClient();
+      $client->setUri($uri);
+      $response = $client->request('GET');
+      if ($response->getStatus() !== 200) {
+        /** 
+         * @see Zend_Feed_Exception
+         */
+        require_once 'external/Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Feed failed to load, got response code ' . $response->getStatus());
+      }
+      $this->_element = $response->getBody();
+      $this->__wakeup();
+    } elseif ($string !== null) {
+      // Retrieve the feed from $string
+      $this->_element = $string;
+      $this->__wakeup();
+    } else {
+      // Generate the feed from the array
+      $header = $builder->getHeader();
+      $this->_element = new DOMDocument('1.0', $header['charset']);
+      $root = $this->_mapFeedHeaders($header);
+      $this->_mapFeedEntries($root, $builder->getEntries());
+      $this->_element = $root;
+      $this->_buildEntryCache();
+    }
+  }
+
+  /**
+   * Load the feed as an XML DOMDocument object
+   *
+   * @return void
+   * @throws Zend_Feed_Exception
+   */
+  public function __wakeup() {
+    @ini_set('track_errors', 1);
+    $doc = @DOMDocument::loadXML($this->_element);
+    @ini_restore('track_errors');
+    
+    if (! $doc) {
+      // prevent the class to generate an undefined variable notice (ZF-2590)
+      if (! isset($php_errormsg)) {
+        if (function_exists('xdebug_is_enabled')) {
+          $php_errormsg = '(error message not available, when XDebug is running)';
+        } else {
+          $php_errormsg = '(error message not available)';
+        }
+      }
+      
+      /** 
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception("DOMDocument cannot parse XML: $php_errormsg");
+    }
+    
+    $this->_element = $doc;
+  }
+
+  /**
+   * Prepare for serialiation
+   *
+   * @return array
+   */
+  public function __sleep() {
+    $this->_element = $this->saveXML();
+    
+    return array('_element');
+  }
+
+  /**
+   * Cache the individual feed elements so they don't need to be
+   * searched for on every operation.
+   *
+   * @return void
+   */
+  protected function _buildEntryCache() {
+    $this->_entries = array();
+    foreach ($this->_element->childNodes as $child) {
+      if ($child->localName == $this->_entryElementName) {
+        $this->_entries[] = $child;
+      }
+    }
+  }
+
+  /**
+   * Get the number of entries in this feed object.
+   *
+   * @return integer Entry count.
+   */
+  public function count() {
+    return count($this->_entries);
+  }
+
+  /**
+   * Required by the Iterator interface.
+   *
+   * @return void
+   */
+  public function rewind() {
+    $this->_entryIndex = 0;
+  }
+
+  /**
+   * Required by the Iterator interface.
+   *
+   * @return mixed The current row, or null if no rows.
+   */
+  public function current() {
+    return new $this->_entryClassName(null, $this->_entries[$this->_entryIndex]);
+  }
+
+  /**
+   * Required by the Iterator interface.
+   *
+   * @return mixed The current row number (starts at 0), or NULL if no rows
+   */
+  public function key() {
+    return $this->_entryIndex;
+  }
+
+  /**
+   * Required by the Iterator interface.
+   *
+   * @return mixed The next row, or null if no more rows.
+   */
+  public function next() {
+    ++ $this->_entryIndex;
+  }
+
+  /**
+   * Required by the Iterator interface.
+   *
+   * @return boolean Whether the iteration is valid
+   */
+  public function valid() {
+    return 0 <= $this->_entryIndex && $this->_entryIndex < $this->count();
+  }
+
+  /**
+   * Generate the header of the feed when working in write mode
+   *
+   * @param  array $array the data to use
+   * @return DOMElement root node
+   */
+  abstract protected function _mapFeedHeaders($array);
+
+  /**
+   * Generate the entries of the feed when working in write mode
+   *
+   * @param  DOMElement $root the root node to use
+   * @param  array $array the data to use
+   * @return DOMElement root node
+   */
+  abstract protected function _mapFeedEntries(DOMElement $root, $array);
+
+  /**
+   * Send feed to a http client with the correct header
+   *
+   * @throws Zend_Feed_Exception if headers have already been sent
+   * @return void
+   */
+  abstract public function send();
+}
diff --git a/trunk/php/external/Zend/Feed/Atom.php b/trunk/php/external/Zend/Feed/Atom.php
new file mode 100644
index 0000000..fa1c3d3
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Atom.php
@@ -0,0 +1,370 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Atom.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed_Abstract
+ */
+require_once 'external/Zend/Feed/Abstract.php';
+
+/**
+ * @see Zend_Feed_Entry_Atom
+ */
+require_once 'external/Zend/Feed/Entry/Atom.php';
+
+/**
+ * Atom feed class
+ *
+ * The Zend_Feed_Atom class is a concrete subclass of the general
+ * Zend_Feed_Abstract class, tailored for representing an Atom
+ * feed. It shares all of the same methods with its abstract
+ * parent. The distinction is made in the format of data that
+ * Zend_Feed_Atom expects, and as a further pointer for users as to
+ * what kind of feed object they have been passed.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Atom extends Zend_Feed_Abstract {
+  
+  /**
+   * The classname for individual feed elements.
+   *
+   * @var string
+   */
+  protected $_entryClassName = 'Zend_Feed_Entry_Atom';
+  
+  /**
+   * The element name for individual feed elements (Atom <entry>
+   * elements).
+   *
+   * @var string
+   */
+  protected $_entryElementName = 'entry';
+  
+  /**
+   * The default namespace for Atom feeds.
+   *
+   * @var string
+   */
+  protected $_defaultNamespace = 'atom';
+
+  /**
+   * Override Zend_Feed_Abstract to set up the $_element and $_entries aliases.
+   *
+   * @return void
+   * @throws Zend_Feed_Exception
+   */
+  public function __wakeup() {
+    parent::__wakeup();
+    
+    // Find the base feed element and create an alias to it.
+    $element = $this->_element->getElementsByTagName('feed')->item(0);
+    if (! $element) {
+      // Try to find a single <entry> instead.
+      $element = $this->_element->getElementsByTagName($this->_entryElementName)->item(0);
+      if (! $element) {
+        /** 
+         * @see Zend_Feed_Exception
+         */
+        require_once 'external/Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('No root <feed> or <' . $this->_entryElementName . '> element found, cannot parse feed.');
+      }
+      
+      $doc = new DOMDocument($this->_element->version, $this->_element->actualEncoding);
+      $feed = $doc->appendChild($doc->createElement('feed'));
+      $feed->appendChild($doc->importNode($element, true));
+      $element = $feed;
+    }
+    
+    $this->_element = $element;
+    
+    // Find the entries and save a pointer to them for speed and
+    // simplicity.
+    $this->_buildEntryCache();
+  }
+
+  /**
+   * Easy access to <link> tags keyed by "rel" attributes.
+   *
+   * If $elt->link() is called with no arguments, we will attempt to
+   * return the value of the <link> tag(s) like all other
+   * method-syntax attribute access. If an argument is passed to
+   * link(), however, then we will return the "href" value of the
+   * first <link> tag that has a "rel" attribute matching $rel:
+   *
+   * $elt->link(): returns the value of the link tag.
+   * $elt->link('self'): returns the href from the first <link rel="self"> in the entry.
+   *
+   * @param  string $rel The "rel" attribute to look for.
+   * @return mixed
+   */
+  public function link($rel = null) {
+    if ($rel === null) {
+      return parent::__call('link', null);
+    }
+    
+    // index link tags by their "rel" attribute.
+    $links = parent::__get('link');
+    if (! is_array($links)) {
+      if ($links instanceof Zend_Feed_Element) {
+        $links = array($links);
+      } else {
+        return $links;
+      }
+    }
+    
+    foreach ($links as $link) {
+      if (empty($link['rel'])) {
+        continue;
+      }
+      if ($rel == $link['rel']) {
+        return $link['href'];
+      }
+    }
+    
+    return null;
+  }
+
+  /**
+   * Make accessing some individual elements of the feed easier.
+   *
+   * Special accessors 'entry' and 'entries' are provided so that if
+   * you wish to iterate over an Atom feed's entries, you can do so
+   * using foreach ($feed->entries as $entry) or foreach
+   * ($feed->entry as $entry).
+   *
+   * @param  string $var The property to access.
+   * @return mixed
+   */
+  public function __get($var) {
+    switch ($var) {
+      case 'entry':
+      // fall through to the next case
+      case 'entries':
+        return $this;
+      
+      default:
+        return parent::__get($var);
+    }
+  }
+
+  /**
+   * Generate the header of the feed when working in write mode
+   *
+   * @param  array $array the data to use
+   * @return DOMElement root node
+   */
+  protected function _mapFeedHeaders($array) {
+    $feed = $this->_element->createElement('feed');
+    $feed->setAttribute('xmlns', 'http://www.w3.org/2005/Atom');
+    
+    $id = $this->_element->createElement('id', $array->link);
+    $feed->appendChild($id);
+    
+    $title = $this->_element->createElement('title');
+    $title->appendChild($this->_element->createCDATASection($array->title));
+    $feed->appendChild($title);
+    
+    if (isset($array->author)) {
+      $author = $this->_element->createElement('author');
+      $name = $this->_element->createElement('name', $array->author);
+      $author->appendChild($name);
+      if (isset($array->email)) {
+        $email = $this->_element->createElement('email', $array->email);
+        $author->appendChild($email);
+      }
+      $feed->appendChild($author);
+    }
+    
+    $updated = isset($array->lastUpdate) ? $array->lastUpdate : time();
+    $updated = $this->_element->createElement('updated', date(DATE_ATOM, $updated));
+    $feed->appendChild($updated);
+    
+    if (isset($array->published)) {
+      $published = $this->_element->createElement('published', date(DATE_ATOM, $array->published));
+      $feed->appendChild($published);
+    }
+    
+    $link = $this->_element->createElement('link');
+    $link->setAttribute('rel', 'self');
+    $link->setAttribute('href', $array->link);
+    if (isset($array->language)) {
+      $link->setAttribute('hreflang', $array->language);
+    }
+    $feed->appendChild($link);
+    
+    if (isset($array->description)) {
+      $subtitle = $this->_element->createElement('subtitle');
+      $subtitle->appendChild($this->_element->createCDATASection($array->description));
+      $feed->appendChild($subtitle);
+    }
+    
+    if (isset($array->copyright)) {
+      $copyright = $this->_element->createElement('rights', $array->copyright);
+      $feed->appendChild($copyright);
+    }
+    
+    if (isset($array->image)) {
+      $image = $this->_element->createElement('logo', $array->image);
+      $feed->appendChild($image);
+    }
+    
+    $generator = ! empty($array->generator) ? $array->generator : 'Zend_Feed';
+    $generator = $this->_element->createElement('generator', $generator);
+    $feed->appendChild($generator);
+    
+    return $feed;
+  }
+
+  /**
+   * Generate the entries of the feed when working in write mode
+   *
+   * The following nodes are constructed for each feed entry
+   * <entry>
+   *    <id>url to feed entry</id>
+   *    <title>entry title</title>
+   *    <updated>last update</updated>
+   *    <link rel="alternate" href="url to feed entry" />
+   *    <summary>short text</summary>
+   *    <content>long version, can contain html</content>
+   * </entry>
+   *
+   * @param  array      $array the data to use
+   * @param  DOMElement $root  the root node to use
+   * @return void
+   */
+  protected function _mapFeedEntries(DOMElement $root, $array) {
+    foreach ($array as $dataentry) {
+      $entry = $this->_element->createElement('entry');
+      
+      $id = $this->_element->createElement('id', isset($dataentry->guid) ? $dataentry->guid : $dataentry->link);
+      $entry->appendChild($id);
+      
+      $title = $this->_element->createElement('title');
+      $title->appendChild($this->_element->createCDATASection($dataentry->title));
+      $entry->appendChild($title);
+      
+      $updated = isset($dataentry->lastUpdate) ? $dataentry->lastUpdate : time();
+      $updated = $this->_element->createElement('updated', date(DATE_ATOM, $updated));
+      $entry->appendChild($updated);
+      
+      $link = $this->_element->createElement('link');
+      $link->setAttribute('rel', 'alternate');
+      $link->setAttribute('href', $dataentry->link);
+      $entry->appendChild($link);
+      
+      $summary = $this->_element->createElement('summary');
+      $summary->appendChild($this->_element->createCDATASection($dataentry->description));
+      $entry->appendChild($summary);
+      
+      if (isset($dataentry->content)) {
+        $content = $this->_element->createElement('content');
+        $content->setAttribute('type', 'html');
+        $content->appendChild($this->_element->createCDATASection($dataentry->content));
+        $entry->appendChild($content);
+      }
+      
+      if (isset($dataentry->category)) {
+        foreach ($dataentry->category as $category) {
+          $node = $this->_element->createElement('category');
+          $node->setAttribute('term', $category['term']);
+          if (isset($category['scheme'])) {
+            $node->setAttribute('scheme', $category['scheme']);
+          }
+          $entry->appendChild($node);
+        }
+      }
+      
+      if (isset($dataentry->source)) {
+        $source = $this->_element->createElement('source');
+        $title = $this->_element->createElement('title', $dataentry->source['title']);
+        $source->appendChild($title);
+        $link = $this->_element->createElement('link', $dataentry->source['title']);
+        $link->setAttribute('rel', 'alternate');
+        $link->setAttribute('href', $dataentry->source['url']);
+        $source->appendChild($link);
+      }
+      
+      if (isset($dataentry->enclosure)) {
+        foreach ($dataentry->enclosure as $enclosure) {
+          $node = $this->_element->createElement('link');
+          $node->setAttribute('rel', 'enclosure');
+          $node->setAttribute('href', $enclosure['url']);
+          if (isset($enclosure['type'])) {
+            $node->setAttribute('type', $enclosure['type']);
+          }
+          if (isset($enclosure['length'])) {
+            $node->setAttribute('length', $enclosure['length']);
+          }
+          $entry->appendChild($node);
+        }
+      }
+      
+      if (isset($dataentry->comments)) {
+        $comments = $this->_element->createElementNS('http://wellformedweb.org/CommentAPI/', 'wfw:comment', $dataentry->comments);
+        $entry->appendChild($comments);
+      }
+      if (isset($dataentry->commentRss)) {
+        $comments = $this->_element->createElementNS('http://wellformedweb.org/CommentAPI/', 'wfw:commentRss', $dataentry->commentRss);
+        $entry->appendChild($comments);
+      }
+      
+      $root->appendChild($entry);
+    }
+  }
+
+  /**
+   * Override Zend_Feed_Element to allow formated feeds
+   *
+   * @return string
+   */
+  public function saveXml() {
+    // Return a complete document including XML prologue.
+    $doc = new DOMDocument($this->_element->ownerDocument->version, $this->_element->ownerDocument->actualEncoding);
+    $doc->appendChild($doc->importNode($this->_element, true));
+    $doc->formatOutput = true;
+    
+    return $doc->saveXML();
+  }
+
+  /**
+   * Send feed to a http client with the correct header
+   *
+   * @return void
+   * @throws Zend_Feed_Exception if headers have already been sent
+   */
+  public function send() {
+    if (headers_sent()) {
+      /** 
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('Cannot send ATOM because headers have already been sent.');
+    }
+    
+    header('Content-type: application/atom+xml; charset: ' . $this->_element->ownerDocument->actualEncoding);
+    
+    echo $this->saveXML();
+  }
+}
diff --git a/trunk/php/external/Zend/Feed/Builder.php b/trunk/php/external/Zend/Feed/Builder.php
new file mode 100644
index 0000000..2f65f2c
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Builder.php
@@ -0,0 +1,384 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Builder.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed_Builder_Interface
+ */
+require_once 'external/Zend/Feed/Builder/Interface.php';
+
+/**
+ * @see Zend_Feed_Builder_Header
+ */
+require_once 'external/Zend/Feed/Builder/Header.php';
+
+/**
+ * @see Zend_Feed_Builder_Entry
+ */
+require_once 'external/Zend/Feed/Builder/Entry.php';
+
+/**
+ * A simple implementation of Zend_Feed_Builder_Interface.
+ *
+ * Users are encouraged to make their own classes to implement Zend_Feed_Builder_Interface
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Builder implements Zend_Feed_Builder_Interface {
+  /**
+   * The data of the feed
+   *
+   * @var $_data array
+   */
+  private $_data;
+  
+  /**
+   * Header of the feed
+   *
+   * @var $_header Zend_Feed_Builder_Header
+   */
+  private $_header;
+  
+  /**
+   * List of the entries of the feed
+   *
+   * @var $_entries array
+   */
+  private $_entries = array();
+
+  /**
+   * Constructor. The $data array must conform to the following format:
+   * <code>
+   *  array(
+   *  'title'       => 'title of the feed', //required
+   *  'link'        => 'canonical url to the feed', //required
+   *  'lastUpdate'  => 'timestamp of the update date', // optional
+   *  'published'   => 'timestamp of the publication date', //optional
+   *  'charset'     => 'charset', // required
+   *  'description' => 'short description of the feed', //optional
+   *  'author'      => 'author/publisher of the feed', //optional
+   *  'email'       => 'email of the author', //optional
+   *  'webmaster'   => 'email address for person responsible for technical issues' // optional, ignored if atom is used
+   *  'copyright'   => 'copyright notice', //optional
+   *  'image'       => 'url to image', //optional
+   *  'generator'   => 'generator', // optional
+   *  'language'    => 'language the feed is written in', // optional
+   *  'ttl'         => 'how long in minutes a feed can be cached before refreshing', // optional, ignored if atom is used
+   *  'rating'      => 'The PICS rating for the channel.', // optional, ignored if atom is used
+   *  'cloud'       => array(
+   *                    'domain'            => 'domain of the cloud, e.g. rpc.sys.com' // required
+   *                    'port'              => 'port to connect to' // optional, default to 80
+   *                    'path'              => 'path of the cloud, e.g. /RPC2 //required
+   *                    'registerProcedure' => 'procedure to call, e.g. myCloud.rssPleaseNotify' // required
+   *                    'protocol'          => 'protocol to use, e.g. soap or xml-rpc' // required
+   *                    ), a cloud to be notified of updates // optional, ignored if atom is used
+   *  'textInput'   => array(
+   *                    'title'       => 'the label of the Submit button in the text input area' // required,
+   *                    'description' => 'explains the text input area' // required
+   *                    'name'        => 'the name of the text object in the text input area' // required
+   *                    'link'        => 'the URL of the CGI script that processes text input requests' // required
+   *                    ) // a text input box that can be displayed with the feed // optional, ignored if atom is used
+   *  'skipHours'   => array(
+   *                    'hour in 24 format', // e.g 13 (1pm)
+   *                    // up to 24 rows whose value is a number between 0 and 23
+   *                    ) // Hint telling aggregators which hours they can skip // optional, ignored if atom is used
+   *  'skipDays '   => array(
+   *                    'a day to skip', // e.g Monday
+   *                    // up to 7 rows whose value is a Monday, Tuesday, Wednesday, Thursday, Friday, Saturday or Sunday
+   *                    ) // Hint telling aggregators which days they can skip // optional, ignored if atom is used
+   *  'itunes'      => array(
+   *                    'author'       => 'Artist column' // optional, default to the main author value
+   *                    'owner'        => array(
+   *                                        'name' => 'name of the owner' // optional, default to main author value
+   *                                        'email' => 'email of the owner' // optional, default to main email value
+   *                                        ) // Owner of the podcast // optional
+   *                    'image'        => 'album/podcast art' // optional, default to the main image value
+   *                    'subtitle'     => 'short description' // optional, default to the main description value
+   *                    'summary'      => 'longer description' // optional, default to the main description value
+   *                    'block'        => 'Prevent an episode from appearing (yes|no)' // optional
+   *                    'category'     => array(
+   *                                      array('main' => 'main category', // required
+   *                                            'sub'  => 'sub category' // optional
+   *                                        ),
+   *                                        // up to 3 rows
+   *                                        ) // 'Category column and in iTunes Music Store Browse' // required
+   *                    'explicit'     => 'parental advisory graphic (yes|no|clean)' // optional
+   *                    'keywords'     => 'a comma separated list of 12 keywords maximum' // optional
+   *                    'new-feed-url' => 'used to inform iTunes of new feed URL location' // optional
+   *                    ) // Itunes extension data // optional, ignored if atom is used
+   *  'entries'     => array(
+   *                   array(
+   *                    'title'        => 'title of the feed entry', //required
+   *                    'link'         => 'url to a feed entry', //required
+   *                    'description'  => 'short version of a feed entry', // only text, no html, required
+   *                    'guid'         => 'id of the article, if not given link value will used', //optional
+   *                    'content'      => 'long version', // can contain html, optional
+   *                    'lastUpdate'   => 'timestamp of the publication date', // optional
+   *                    'comments'     => 'comments page of the feed entry', // optional
+   *                    'commentRss'   => 'the feed url of the associated comments', // optional
+   *                    'source'       => array(
+   *                                        'title' => 'title of the original source' // required,
+   *                                        'url' => 'url of the original source' // required
+   *                                           ) // original source of the feed entry // optional
+   *                    'category'     => array(
+   *                                      array(
+   *                                        'term' => 'first category label' // required,
+   *                                        'scheme' => 'url that identifies a categorization scheme' // optional
+   *                                            ),
+   *                                      array(
+   *                                         //data for the second category and so on
+   *                                           )
+   *                                        ) // list of the attached categories // optional
+   *                    'enclosure'    => array(
+   *                                      array(
+   *                                        'url' => 'url of the linked enclosure' // required
+   *                                        'type' => 'mime type of the enclosure' // optional
+   *                                        'length' => 'length of the linked content in octets' // optional
+   *                                           ),
+   *                                      array(
+   *                                         //data for the second enclosure and so on
+   *                                           )
+   *                                        ) // list of the enclosures of the feed entry // optional
+   *                   ),
+   *                   array(
+   *                   //data for the second entry and so on
+   *                   )
+   *                 )
+   * );
+   * </code>
+   *
+   * @param  array $data
+   * @return void
+   */
+  public function __construct(array $data) {
+    $this->_data = $data;
+    $this->_createHeader($data);
+    if (isset($data['entries'])) {
+      $this->_createEntries($data['entries']);
+    }
+  }
+
+  /**
+   * Returns an instance of Zend_Feed_Builder_Header
+   * describing the header of the feed
+   *
+   * @return Zend_Feed_Builder_Header
+   */
+  public function getHeader() {
+    return $this->_header;
+  }
+
+  /**
+   * Returns an array of Zend_Feed_Builder_Entry instances
+   * describing the entries of the feed
+   *
+   * @return array of Zend_Feed_Builder_Entry
+   */
+  public function getEntries() {
+    return $this->_entries;
+  }
+
+  /**
+   * Create the Zend_Feed_Builder_Header instance
+   *
+   * @param  array $data
+   * @throws Zend_Feed_Builder_Exception
+   * @return void
+   */
+  private function _createHeader(array $data) {
+    $mandatories = array('title', 'link', 'charset');
+    foreach ($mandatories as $mandatory) {
+      if (! isset($data[$mandatory])) {
+        /**
+         * @see Zend_Feed_Builder_Exception
+         */
+        require_once 'external/Zend/Feed/Builder/Exception.php';
+        throw new Zend_Feed_Builder_Exception("$mandatory key is missing");
+      }
+    }
+    $this->_header = new Zend_Feed_Builder_Header($data['title'], $data['link'], $data['charset']);
+    if (isset($data['lastUpdate'])) {
+      $this->_header->setLastUpdate($data['lastUpdate']);
+    }
+    if (isset($data['published'])) {
+      $this->_header->setPublishedDate($data['published']);
+    }
+    if (isset($data['description'])) {
+      $this->_header->setDescription($data['description']);
+    }
+    if (isset($data['author'])) {
+      $this->_header->setAuthor($data['author']);
+    }
+    if (isset($data['email'])) {
+      $this->_header->setEmail($data['email']);
+    }
+    if (isset($data['webmaster'])) {
+      $this->_header->setWebmaster($data['webmaster']);
+    }
+    if (isset($data['copyright'])) {
+      $this->_header->setCopyright($data['copyright']);
+    }
+    if (isset($data['image'])) {
+      $this->_header->setImage($data['image']);
+    }
+    if (isset($data['generator'])) {
+      $this->_header->setGenerator($data['generator']);
+    }
+    if (isset($data['language'])) {
+      $this->_header->setLanguage($data['language']);
+    }
+    if (isset($data['ttl'])) {
+      $this->_header->setTtl($data['ttl']);
+    }
+    if (isset($data['rating'])) {
+      $this->_header->setRating($data['rating']);
+    }
+    if (isset($data['cloud'])) {
+      $mandatories = array('domain', 'path', 'registerProcedure', 'protocol');
+      foreach ($mandatories as $mandatory) {
+        if (! isset($data['cloud'][$mandatory])) {
+          /**
+           * @see Zend_Feed_Builder_Exception
+           */
+          require_once 'external/Zend/Feed/Builder/Exception.php';
+          throw new Zend_Feed_Builder_Exception("you have to define $mandatory property of your cloud");
+        }
+      }
+      $uri_str = 'http://' . $data['cloud']['domain'] . $data['cloud']['path'];
+      $this->_header->setCloud($uri_str, $data['cloud']['registerProcedure'], $data['cloud']['protocol']);
+    }
+    if (isset($data['textInput'])) {
+      $mandatories = array('title', 'description', 'name', 'link');
+      foreach ($mandatories as $mandatory) {
+        if (! isset($data['textInput'][$mandatory])) {
+          /**
+           * @see Zend_Feed_Builder_Exception
+           */
+          require_once 'external/Zend/Feed/Builder/Exception.php';
+          throw new Zend_Feed_Builder_Exception("you have to define $mandatory property of your textInput");
+        }
+      }
+      $this->_header->setTextInput($data['textInput']['title'], $data['textInput']['description'], $data['textInput']['name'], $data['textInput']['link']);
+    }
+    if (isset($data['skipHours'])) {
+      $this->_header->setSkipHours($data['skipHours']);
+    }
+    if (isset($data['skipDays'])) {
+      $this->_header->setSkipDays($data['skipDays']);
+    }
+    if (isset($data['itunes'])) {
+      $itunes = new Zend_Feed_Builder_Header_Itunes($data['itunes']['category']);
+      if (isset($data['itunes']['author'])) {
+        $itunes->setAuthor($data['itunes']['author']);
+      }
+      if (isset($data['itunes']['owner'])) {
+        $name = isset($data['itunes']['owner']['name']) ? $data['itunes']['owner']['name'] : '';
+        $email = isset($data['itunes']['owner']['email']) ? $data['itunes']['owner']['email'] : '';
+        $itunes->setOwner($name, $email);
+      }
+      if (isset($data['itunes']['image'])) {
+        $itunes->setImage($data['itunes']['image']);
+      }
+      if (isset($data['itunes']['subtitle'])) {
+        $itunes->setSubtitle($data['itunes']['subtitle']);
+      }
+      if (isset($data['itunes']['summary'])) {
+        $itunes->setSummary($data['itunes']['summary']);
+      }
+      if (isset($data['itunes']['block'])) {
+        $itunes->setBlock($data['itunes']['block']);
+      }
+      if (isset($data['itunes']['explicit'])) {
+        $itunes->setExplicit($data['itunes']['explicit']);
+      }
+      if (isset($data['itunes']['keywords'])) {
+        $itunes->setKeywords($data['itunes']['keywords']);
+      }
+      if (isset($data['itunes']['new-feed-url'])) {
+        $itunes->setNewFeedUrl($data['itunes']['new-feed-url']);
+      }
+      
+      $this->_header->setITunes($itunes);
+    }
+  }
+
+  /**
+   * Create the array of article entries
+   *
+   * @param  array $data
+   * @throws Zend_Feed_Builder_Exception
+   * @return void
+   */
+  private function _createEntries(array $data) {
+    foreach ($data as $row) {
+      $mandatories = array('title', 'link', 'description');
+      foreach ($mandatories as $mandatory) {
+        if (! isset($row[$mandatory])) {
+          /**
+           * @see Zend_Feed_Builder_Exception
+           */
+          require_once 'external/Zend/Feed/Builder/Exception.php';
+          throw new Zend_Feed_Builder_Exception("$mandatory key is missing");
+        }
+      }
+      $entry = new Zend_Feed_Builder_Entry($row['title'], $row['link'], $row['description']);
+      if (isset($row['guid'])) {
+        $entry->setId($row['guid']);
+      }
+      if (isset($row['content'])) {
+        $entry->setContent($row['content']);
+      }
+      if (isset($row['lastUpdate'])) {
+        $entry->setLastUpdate($row['lastUpdate']);
+      }
+      if (isset($row['comments'])) {
+        $entry->setCommentsUrl($row['comments']);
+      }
+      if (isset($row['commentRss'])) {
+        $entry->setCommentsRssUrl($row['commentRss']);
+      }
+      if (isset($row['source'])) {
+        $mandatories = array('title', 'url');
+        foreach ($mandatories as $mandatory) {
+          if (! isset($row['source'][$mandatory])) {
+            /**
+             * @see Zend_Feed_Builder_Exception
+             */
+            require_once 'external/Zend/Feed/Builder/Exception.php';
+            throw new Zend_Feed_Builder_Exception("$mandatory key of source property is missing");
+          }
+        }
+        $entry->setSource($row['source']['title'], $row['source']['url']);
+      }
+      if (isset($row['category'])) {
+        $entry->setCategories($row['category']);
+      }
+      if (isset($row['enclosure'])) {
+        $entry->setEnclosures($row['enclosure']);
+      }
+      
+      $this->_entries[] = $entry;
+    }
+  }
+}
diff --git a/trunk/php/external/Zend/Feed/Builder/Entry.php b/trunk/php/external/Zend/Feed/Builder/Entry.php
new file mode 100644
index 0000000..ec8da6d
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Builder/Entry.php
@@ -0,0 +1,266 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Entry.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * An entry of a custom build feed
+ *
+ * Classes implementing the Zend_Feed_Builder_Interface interface
+ * uses this class to describe an entry of a feed
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Builder_Entry extends ArrayObject {
+
+  /**
+   * Create a new builder entry
+   *
+   * @param  string $title
+   * @param  string $link
+   * @param  string $description short version of the entry, no html
+   * @return void
+   */
+  public function __construct($title, $link, $description) {
+    $this->offsetSet('title', $title);
+    $this->offsetSet('link', $link);
+    $this->offsetSet('description', $description);
+    $this->setLastUpdate(time());
+  }
+
+  /**
+   * Read only properties accessor
+   *
+   * @param  string $name property to read
+   * @return mixed
+   */
+  public function __get($name) {
+    if (! $this->offsetExists($name)) {
+      return NULL;
+    }
+    
+    return $this->offsetGet($name);
+  }
+
+  /**
+   * Write properties accessor
+   *
+   * @param  string $name name of the property to set
+   * @param  mixed $value value to set
+   * @return void
+   */
+  public function __set($name, $value) {
+    $this->offsetSet($name, $value);
+  }
+
+  /**
+   * Isset accessor
+   *
+   * @param  string $key
+   * @return boolean
+   */
+  public function __isset($key) {
+    return $this->offsetExists($key);
+  }
+
+  /**
+   * Unset accessor
+   *
+   * @param  string $key
+   * @return void
+   */
+  public function __unset($key) {
+    if ($this->offsetExists($key)) {
+      $this->offsetUnset($key);
+    }
+  }
+
+  /**
+   * Sets the id/guid of the entry
+   *
+   * @param  string $id
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function setId($id) {
+    $this->offsetSet('guid', $id);
+    return $this;
+  }
+
+  /**
+   * Sets the full html content of the entry
+   *
+   * @param  string $content
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function setContent($content) {
+    $this->offsetSet('content', $content);
+    return $this;
+  }
+
+  /**
+   * Timestamp of the update date
+   *
+   * @param  int $lastUpdate
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function setLastUpdate($lastUpdate) {
+    $this->offsetSet('lastUpdate', $lastUpdate);
+    return $this;
+  }
+
+  /**
+   * Sets the url of the commented page associated to the entry
+   *
+   * @param  string $comments
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function setCommentsUrl($comments) {
+    $this->offsetSet('comments', $comments);
+    return $this;
+  }
+
+  /**
+   * Sets the url of the comments feed link
+   *
+   * @param  string $commentRss
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function setCommentsRssUrl($commentRss) {
+    $this->offsetSet('commentRss', $commentRss);
+    return $this;
+  }
+
+  /**
+   * Defines a reference to the original source
+   *
+   * @param  string $title
+   * @param  string $url
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function setSource($title, $url) {
+    $this->offsetSet('source', array('title' => $title, 'url' => $url));
+    return $this;
+  }
+
+  /**
+   * Sets the categories of the entry
+   * Format of the array:
+   * <code>
+   * array(
+   *   array(
+   *         'term' => 'first category label',
+   *         'scheme' => 'url that identifies a categorization scheme' // optional
+   *        ),
+   *   // second category and so one
+   * )
+   * </code>
+   *
+   * @param  array $categories
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function setCategories(array $categories) {
+    foreach ($categories as $category) {
+      $this->addCategory($category);
+    }
+    return $this;
+  }
+
+  /**
+   * Add a category to the entry
+   *
+   * @param  array $category see Zend_Feed_Builder_Entry::setCategories() for format
+   * @return Zend_Feed_Builder_Entry
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function addCategory(array $category) {
+    if (empty($category['term'])) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to define the name of the category");
+    }
+    
+    if (! $this->offsetExists('category')) {
+      $categories = array($category);
+    } else {
+      $categories = $this->offsetGet('category');
+      $categories[] = $category;
+    }
+    $this->offsetSet('category', $categories);
+    return $this;
+  }
+
+  /**
+   * Sets the enclosures of the entry
+   * Format of the array:
+   * <code>
+   * array(
+   *   array(
+   *         'url' => 'url of the linked enclosure',
+   *         'type' => 'mime type of the enclosure' // optional
+   *         'length' => 'length of the linked content in octets' // optional
+   *        ),
+   *   // second enclosure and so one
+   * )
+   * </code>
+   *
+   * @param  array $enclosures
+   * @return Zend_Feed_Builder_Entry
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setEnclosures(array $enclosures) {
+    foreach ($enclosures as $enclosure) {
+      if (empty($enclosure['url'])) {
+        /**
+         * @see Zend_Feed_Builder_Exception
+         */
+        require_once 'external/Zend/Feed/Builder/Exception.php';
+        throw new Zend_Feed_Builder_Exception("you have to supply an url for your enclosure");
+      }
+      $type = isset($enclosure['type']) ? $enclosure['type'] : '';
+      $length = isset($enclosure['length']) ? $enclosure['length'] : '';
+      $this->addEnclosure($enclosure['url'], $type, $length);
+    }
+    return $this;
+  }
+
+  /**
+   * Add an enclosure to the entry
+   *
+   * @param  string $url
+   * @param  string $type
+   * @param  string $length
+   * @return Zend_Feed_Builder_Entry
+   */
+  public function addEnclosure($url, $type = '', $length = '') {
+    if (! $this->offsetExists('enclosure')) {
+      $enclosure = array();
+    } else {
+      $enclosure = $this->offsetGet('enclosure');
+    }
+    $enclosure[] = array('url' => $url, 'type' => $type, 'length' => $length);
+    $this->offsetSet('enclosure', $enclosure);
+    return $this;
+  }
+}
diff --git a/trunk/php/external/Zend/Feed/Builder/Exception.php b/trunk/php/external/Zend/Feed/Builder/Exception.php
new file mode 100644
index 0000000..317cd51
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Builder/Exception.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Exception.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed_Exception
+ */
+require_once 'external/Zend/Feed/Exception.php';
+
+/**
+ * Zend_Feed_Builder exception class
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Builder_Exception extends Zend_Feed_Exception {
+}
diff --git a/trunk/php/external/Zend/Feed/Builder/Header.php b/trunk/php/external/Zend/Feed/Builder/Header.php
new file mode 100644
index 0000000..d8c2f48
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Builder/Header.php
@@ -0,0 +1,391 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Header.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Loader
+ */
+require_once 'external/Zend/Loader.php';
+
+/**
+ * @see Zend_Feed_Builder_Header_Itunes
+ */
+require_once 'external/Zend/Feed/Builder/Header/Itunes.php';
+
+/**
+ * @see Zend_Uri
+ */
+require_once 'external/Zend/Uri.php';
+
+/**
+ * Header of a custom build feed
+ *
+ * Classes implementing the Zend_Feed_Builder_Interface interface
+ * uses this class to describe the header of a feed
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Builder_Header extends ArrayObject {
+
+  /**
+   * Constructor
+   *
+   * @param  string $title title of the feed
+   * @param  string $link canonical url of the feed
+   * @param  string $charset charset of the textual data
+   * @return void
+   */
+  public function __construct($title, $link, $charset = 'utf-8') {
+    $this->offsetSet('title', $title);
+    $this->offsetSet('link', $link);
+    $this->offsetSet('charset', $charset);
+    $this->setLastUpdate(time())->setGenerator('Zend_Feed');
+  }
+
+  /**
+   * Read only properties accessor
+   *
+   * @param  string $name property to read
+   * @return mixed
+   */
+  public function __get($name) {
+    if (! $this->offsetExists($name)) {
+      return NULL;
+    }
+    
+    return $this->offsetGet($name);
+  }
+
+  /**
+   * Write properties accessor
+   *
+   * @param string $name  name of the property to set
+   * @param mixed  $value value to set
+   * @return void
+   */
+  public function __set($name, $value) {
+    $this->offsetSet($name, $value);
+  }
+
+  /**
+   * Isset accessor
+   *
+   * @param  string $key
+   * @return boolean
+   */
+  public function __isset($key) {
+    return $this->offsetExists($key);
+  }
+
+  /**
+   * Unset accessor
+   *
+   * @param  string $key
+   * @return void
+   */
+  public function __unset($key) {
+    if ($this->offsetExists($key)) {
+      $this->offsetUnset($key);
+    }
+  }
+
+  /**
+   * Timestamp of the update date
+   *
+   * @param  int $lastUpdate
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setLastUpdate($lastUpdate) {
+    $this->offsetSet('lastUpdate', $lastUpdate);
+    return $this;
+  }
+
+  /**
+   * Timestamp of the publication date
+   *
+   * @param  int $published
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setPublishedDate($published) {
+    $this->offsetSet('published', $published);
+    return $this;
+  }
+
+  /**
+   * Short description of the feed
+   *
+   * @param  string $description
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setDescription($description) {
+    $this->offsetSet('description', $description);
+    return $this;
+  }
+
+  /**
+   * Sets the author of the feed
+   *
+   * @param  string $author
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setAuthor($author) {
+    $this->offsetSet('author', $author);
+    return $this;
+  }
+
+  /**
+   * Sets the author's email
+   *
+   * @param  string $email
+   * @return Zend_Feed_Builder_Header
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setEmail($email) {
+    Zend_Loader::loadClass('Zend_Validate_EmailAddress');
+    $validate = new Zend_Validate_EmailAddress();
+    if (! $validate->isValid($email)) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to set a valid email address into the email property");
+    }
+    $this->offsetSet('email', $email);
+    return $this;
+  }
+
+  /**
+   * Sets the copyright notice
+   *
+   * @param  string $copyright
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setCopyright($copyright) {
+    $this->offsetSet('copyright', $copyright);
+    return $this;
+  }
+
+  /**
+   * Sets the image of the feed
+   *
+   * @param  string $image
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setImage($image) {
+    $this->offsetSet('image', $image);
+    return $this;
+  }
+
+  /**
+   * Sets the generator of the feed
+   *
+   * @param  string $generator
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setGenerator($generator) {
+    $this->offsetSet('generator', $generator);
+    return $this;
+  }
+
+  /**
+   * Sets the language of the feed
+   *
+   * @param  string $language
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setLanguage($language) {
+    $this->offsetSet('language', $language);
+    return $this;
+  }
+
+  /**
+   * Email address for person responsible for technical issues
+   * Ignored if atom is used
+   *
+   * @param  string $webmaster
+   * @return Zend_Feed_Builder_Header
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setWebmaster($webmaster) {
+    Zend_Loader::loadClass('Zend_Validate_EmailAddress');
+    $validate = new Zend_Validate_EmailAddress();
+    if (! $validate->isValid($webmaster)) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to set a valid email address into the webmaster property");
+    }
+    $this->offsetSet('webmaster', $webmaster);
+    return $this;
+  }
+
+  /**
+   * How long in minutes a feed can be cached before refreshing
+   * Ignored if atom is used
+   *
+   * @param  int $ttl
+   * @return Zend_Feed_Builder_Header
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setTtl($ttl) {
+    Zend_Loader::loadClass('Zend_Validate_Int');
+    $validate = new Zend_Validate_Int();
+    if (! $validate->isValid($ttl)) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to set an integer value to the ttl property");
+    }
+    $this->offsetSet('ttl', $ttl);
+    return $this;
+  }
+
+  /**
+   * PICS rating for the feed
+   * Ignored if atom is used
+   *
+   * @param  string $rating
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setRating($rating) {
+    $this->offsetSet('rating', $rating);
+    return $this;
+  }
+
+  /**
+   * Cloud to be notified of updates of the feed
+   * Ignored if atom is used
+   *
+   * @param  string|Zend_Uri_Http $uri
+   * @param  string               $procedure procedure to call, e.g. myCloud.rssPleaseNotify
+   * @param  string               $protocol  protocol to use, e.g. soap or xml-rpc
+   * @return Zend_Feed_Builder_Header
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setCloud($uri, $procedure, $protocol) {
+    if (is_string($uri) && Zend_Uri_Http::check($uri)) {
+      $uri = Zend_Uri::factory($uri);
+    }
+    if (! $uri instanceof Zend_Uri_Http) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception('Passed parameter is not a valid HTTP URI');
+    }
+    if (! $uri->getPort()) {
+      $uri->setPort(80);
+    }
+    $this->offsetSet('cloud', array('uri' => $uri, 'procedure' => $procedure, 'protocol' => $protocol));
+    return $this;
+  }
+
+  /**
+   * A text input box that can be displayed with the feed
+   * Ignored if atom is used
+   *
+   * @param  string $title       the label of the Submit button in the text input area
+   * @param  string $description explains the text input area
+   * @param  string $name        the name of the text object in the text input area
+   * @param  string $link        the URL of the CGI script that processes text input requests
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setTextInput($title, $description, $name, $link) {
+    $this->offsetSet('textInput', array('title' => $title, 'description' => $description, 
+        'name' => $name, 'link' => $link));
+    return $this;
+  }
+
+  /**
+   * Hint telling aggregators which hours they can skip
+   * Ignored if atom is used
+   *
+   * @param  array $hours list of hours in 24 format
+   * @return Zend_Feed_Builder_Header
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setSkipHours(array $hours) {
+    if (count($hours) > 24) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you can not have more than 24 rows in the skipHours property");
+    }
+    foreach ($hours as $hour) {
+      if ($hour < 0 || $hour > 23) {
+        /**
+         * @see Zend_Feed_Builder_Exception
+         */
+        require_once 'external/Zend/Feed/Builder/Exception.php';
+        throw new Zend_Feed_Builder_Exception("$hour has te be between 0 and 23");
+      }
+    }
+    $this->offsetSet('skipHours', $hours);
+    return $this;
+  }
+
+  /**
+   * Hint telling aggregators which days they can skip
+   * Ignored if atom is used
+   *
+   * @param  array $days list of days to skip, e.g. Monday
+   * @return Zend_Feed_Builder_Header
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setSkipDays(array $days) {
+    if (count($days) > 7) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you can not have more than 7 days in the skipDays property");
+    }
+    $valid = array('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday');
+    foreach ($days as $day) {
+      if (! in_array(strtolower($day), $valid)) {
+        /**
+         * @see Zend_Feed_Builder_Exception
+         */
+        require_once 'external/Zend/Feed/Builder/Exception.php';
+        throw new Zend_Feed_Builder_Exception("$day is not a valid day");
+      }
+    }
+    $this->offsetSet('skipDays', $days);
+    return $this;
+  }
+
+  /**
+   * Sets the iTunes rss extension
+   *
+   * @param  Zend_Feed_Builder_Header_Itunes $itunes
+   * @return Zend_Feed_Builder_Header
+   */
+  public function setITunes(Zend_Feed_Builder_Header_Itunes $itunes) {
+    $this->offsetSet('itunes', $itunes);
+    return $this;
+  }
+}
diff --git a/trunk/php/external/Zend/Feed/Builder/Header/Itunes.php b/trunk/php/external/Zend/Feed/Builder/Header/Itunes.php
new file mode 100644
index 0000000..077ec8d
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Builder/Header/Itunes.php
@@ -0,0 +1,269 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Itunes.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * ITunes rss extension
+ *
+ * Classes used to describe the itunes channel extension
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Builder_Header_Itunes extends ArrayObject {
+
+  /**
+   * Constructor
+   *
+   * @param  array $categories Categories columns and in iTunes Music Store Browse
+   * @return void
+   */
+  public function __construct(array $categories) {
+    $this->setCategories($categories);
+  }
+
+  /**
+   * Sets the categories column and in iTunes Music Store Browse
+   * $categories must conform to the following format:
+   * <code>
+   * array(array('main' => 'main category',
+   *             'sub' => 'sub category' // optionnal
+   *            ),
+   *       // up to 3 rows
+   *      )
+   * </code>
+   *
+   * @param  array $categories
+   * @return Zend_Feed_Builder_Header_Itunes
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setCategories(array $categories) {
+    $nb = count($categories);
+    if (0 === $nb) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to set at least one itunes category");
+    }
+    if ($nb > 3) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to set at most three itunes categories");
+    }
+    foreach ($categories as $i => $category) {
+      if (empty($category['main'])) {
+        /**
+         * @see Zend_Feed_Builder_Exception
+         */
+        require_once 'external/Zend/Feed/Builder/Exception.php';
+        throw new Zend_Feed_Builder_Exception("you have to set the main category (category #$i)");
+      }
+    }
+    $this->offsetSet('category', $categories);
+    return $this;
+  }
+
+  /**
+   * Sets the artist value, default to the feed's author value
+   *
+   * @param  string $author
+   * @return Zend_Feed_Builder_Header_Itunes
+   */
+  public function setAuthor($author) {
+    $this->offsetSet('author', $author);
+    return $this;
+  }
+
+  /**
+   * Sets the owner of the postcast
+   *
+   * @param  string $name  default to the feed's author value
+   * @param  string $email default to the feed's email value
+   * @return Zend_Feed_Builder_Header_Itunes
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setOwner($name = '', $email = '') {
+    if (! empty($email)) {
+      Zend_Loader::loadClass('Zend_Validate_EmailAddress');
+      $validate = new Zend_Validate_EmailAddress();
+      if (! $validate->isValid($email)) {
+        /**
+         * @see Zend_Feed_Builder_Exception
+         */
+        require_once 'external/Zend/Feed/Builder/Exception.php';
+        throw new Zend_Feed_Builder_Exception("you have to set a valid email address into the itunes owner's email property");
+      }
+    }
+    $this->offsetSet('owner', array('name' => $name, 'email' => $email));
+    return $this;
+  }
+
+  /**
+   * Sets the album/podcast art picture
+   * Default to the feed's image value
+   *
+   * @param  string $image
+   * @return Zend_Feed_Builder_Header_Itunes
+   */
+  public function setImage($image) {
+    $this->offsetSet('image', $image);
+    return $this;
+  }
+
+  /**
+   * Sets the short description of the podcast
+   * Default to the feed's description
+   *
+   * @param  string $subtitle
+   * @return Zend_Feed_Builder_Header_Itunes
+   */
+  public function setSubtitle($subtitle) {
+    $this->offsetSet('subtitle', $subtitle);
+    return $this;
+  }
+
+  /**
+   * Sets the longer description of the podcast
+   * Default to the feed's description
+   *
+   * @param  string $summary
+   * @return Zend_Feed_Builder_Header_Itunes
+   */
+  public function setSummary($summary) {
+    $this->offsetSet('summary', $summary);
+    return $this;
+  }
+
+  /**
+   * Prevent a feed from appearing
+   *
+   * @param  string $block can be 'yes' or 'no'
+   * @return Zend_Feed_Builder_Header_Itunes
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setBlock($block) {
+    $block = strtolower($block);
+    if (! in_array($block, array('yes', 'no'))) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to set yes or no to the itunes block property");
+    }
+    $this->offsetSet('block', $block);
+    return $this;
+  }
+
+  /**
+   * Configuration of the parental advisory graphic
+   *
+   * @param  string $explicit can be 'yes', 'no' or 'clean'
+   * @return Zend_Feed_Builder_Header_Itunes
+   * @throws Zend_Feed_Builder_Exception
+   */
+  public function setExplicit($explicit) {
+    $explicit = strtolower($explicit);
+    if (! in_array($explicit, array('yes', 'no', 'clean'))) {
+      /**
+       * @see Zend_Feed_Builder_Exception
+       */
+      require_once 'external/Zend/Feed/Builder/Exception.php';
+      throw new Zend_Feed_Builder_Exception("you have to set yes, no or clean to the itunes explicit property");
+    }
+    $this->offsetSet('explicit', $explicit);
+    return $this;
+  }
+
+  /**
+   * Sets a comma separated list of 12 keywords maximum
+   *
+   * @param  string $keywords
+   * @return Zend_Feed_Builder_Header_Itunes
+   */
+  public function setKeywords($keywords) {
+    $this->offsetSet('keywords', $keywords);
+    return $this;
+  }
+
+  /**
+   * Sets the new feed URL location
+   *
+   * @param  string $url
+   * @return Zend_Feed_Builder_Header_Itunes
+   */
+  public function setNewFeedUrl($url) {
+    $this->offsetSet('new_feed_url', $url);
+    return $this;
+  }
+
+  /**
+   * Read only properties accessor
+   *
+   * @param  string $name property to read
+   * @return mixed
+   */
+  public function __get($name) {
+    if (! $this->offsetExists($name)) {
+      return NULL;
+    }
+    
+    return $this->offsetGet($name);
+  }
+
+  /**
+   * Write properties accessor
+   *
+   * @param  string $name  name of the property to set
+   * @param  mixed  $value value to set
+   * @return void
+   */
+  public function __set($name, $value) {
+    $this->offsetSet($name, $value);
+  }
+
+  /**
+   * Isset accessor
+   *
+   * @param  string $key
+   * @return boolean
+   */
+  public function __isset($key) {
+    return $this->offsetExists($key);
+  }
+
+  /**
+   * Unset accessor
+   *
+   * @param  string $key
+   * @return void
+   */
+  public function __unset($key) {
+    if ($this->offsetExists($key)) {
+      $this->offsetUnset($key);
+    }
+  }
+
+}
diff --git a/trunk/php/external/Zend/Feed/Builder/Interface.php b/trunk/php/external/Zend/Feed/Builder/Interface.php
new file mode 100644
index 0000000..4b4c0ce
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Builder/Interface.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Interface.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * Input feed data interface
+ *
+ * Classes implementing this interface can be passe to Zend_Feed::importBuilder
+ * as an input data source for the Zend_Feed construction
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Feed_Builder_Interface {
+
+  /**
+   * Returns an instance of Zend_Feed_Builder_Header
+   * describing the header of the feed
+   *
+   * @return Zend_Feed_Builder_Header
+   */
+  public function getHeader();
+
+  /**
+   * Returns an array of Zend_Feed_Builder_Entry instances
+   * describing the entries of the feed
+   *
+   * @return array of Zend_Feed_Builder_Entry
+   */
+  public function getEntries();
+}
diff --git a/trunk/php/external/Zend/Feed/Element.php b/trunk/php/external/Zend/Feed/Element.php
new file mode 100644
index 0000000..b00425d
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Element.php
@@ -0,0 +1,370 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Element.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * Wraps a DOMElement allowing for SimpleXML-like access to attributes.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Element implements ArrayAccess {
+  
+  /**
+   * @var DOMElement
+   */
+  protected $_element;
+  
+  /**
+   * @var Zend_Feed_Element
+   */
+  protected $_parentElement;
+  
+  /**
+   * @var boolean
+   */
+  protected $_appended = true;
+
+  /**
+   * Zend_Feed_Element constructor.
+   *
+   * @param  DOMElement $element The DOM element we're encapsulating.
+   * @return void
+   */
+  public function __construct($element = null) {
+    $this->_element = $element;
+  }
+
+  /**
+   * Get a DOM representation of the element
+   *
+   * Returns the underlying DOM object, which can then be
+   * manipulated with full DOM methods.
+   *
+   * @return DOMDocument
+   */
+  public function getDOM() {
+    return $this->_element;
+  }
+
+  /**
+   * Update the object from a DOM element
+   *
+   * Take a DOMElement object, which may be originally from a call
+   * to getDOM() or may be custom created, and use it as the
+   * DOM tree for this Zend_Feed_Element.
+   *
+   * @param  DOMElement $element
+   * @return void
+   */
+  public function setDOM(DOMElement $element) {
+    $this->_element = $this->_element->ownerDocument->importNode($element, true);
+  }
+
+  /**
+   * Set the parent element of this object to another
+   * Zend_Feed_Element.
+   *
+   * @param  Zend_Feed_Element $element
+   * @return void
+   */
+  public function setParent(Zend_Feed_Element $element) {
+    $this->_parentElement = $element;
+    $this->_appended = false;
+  }
+
+  /**
+   * Appends this element to its parent if necessary.
+   *
+   * @return void
+   */
+  protected function ensureAppended() {
+    if (! $this->_appended) {
+      $this->_parentElement->getDOM()->appendChild($this->_element);
+      $this->_appended = true;
+      $this->_parentElement->ensureAppended();
+    }
+  }
+
+  /**
+   * Get an XML string representation of this element
+   *
+   * Returns a string of this element's XML, including the XML
+   * prologue.
+   *
+   * @return string
+   */
+  public function saveXml() {
+    // Return a complete document including XML prologue.
+    $doc = new DOMDocument($this->_element->ownerDocument->version, $this->_element->ownerDocument->actualEncoding);
+    $doc->appendChild($doc->importNode($this->_element, true));
+    return $doc->saveXML();
+  }
+
+  /**
+   * Get the XML for only this element
+   *
+   * Returns a string of this element's XML without prologue.
+   *
+   * @return string
+   */
+  public function saveXmlFragment() {
+    return $this->_element->ownerDocument->saveXML($this->_element);
+  }
+
+  /**
+   * Map variable access onto the underlying entry representation.
+   *
+   * Get-style access returns a Zend_Feed_Element representing the
+   * child element accessed. To get string values, use method syntax
+   * with the __call() overriding.
+   *
+   * @param  string $var The property to access.
+   * @return mixed
+   */
+  public function __get($var) {
+    $nodes = $this->_children($var);
+    $length = count($nodes);
+    
+    if ($length == 1) {
+      return new Zend_Feed_Element($nodes[0]);
+    } elseif ($length > 1) {
+      return array_map(create_function('$e', 'return new Zend_Feed_Element($e);'), $nodes);
+    } else {
+      // When creating anonymous nodes for __set chaining, don't
+      // call appendChild() on them. Instead we pass the current
+      // element to them as an extra reference; the child is
+      // then responsible for appending itself when it is
+      // actually set. This way "if ($foo->bar)" doesn't create
+      // a phantom "bar" element in our tree.
+      if (strpos($var, ':') !== false) {
+        list($ns, $elt) = explode(':', $var, 2);
+        $node = $this->_element->ownerDocument->createElementNS(Zend_Feed::lookupNamespace($ns), $elt);
+      } else {
+        $node = $this->_element->ownerDocument->createElement($var);
+      }
+      $node = new self($node);
+      $node->setParent($this);
+      return $node;
+    }
+  }
+
+  /**
+   * Map variable sets onto the underlying entry representation.
+   *
+   * @param  string $var The property to change.
+   * @param  string $val The property's new value.
+   * @return void
+   * @throws Zend_Feed_Exception
+   */
+  public function __set($var, $val) {
+    $this->ensureAppended();
+    
+    $nodes = $this->_children($var);
+    if (! $nodes) {
+      if (strpos($var, ':') !== false) {
+        list($ns, $elt) = explode(':', $var, 2);
+        $node = $this->_element->ownerDocument->createElementNS(Zend_Feed::lookupNamespace($ns), $var, $val);
+        $this->_element->appendChild($node);
+      } else {
+        $node = $this->_element->ownerDocument->createElement($var, $val);
+        $this->_element->appendChild($node);
+      }
+    } elseif (count($nodes) > 1) {
+      /** 
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('Cannot set the value of multiple tags simultaneously.');
+    } else {
+      $nodes[0]->nodeValue = $val;
+    }
+  }
+
+  /**
+   * Map isset calls onto the underlying entry representation.
+   *
+   * @param  string $var
+   * @return boolean
+   */
+  public function __isset($var) {
+    // Look for access of the form {ns:var}. We don't use
+    // _children() here because we can break out of the loop
+    // immediately once we find something.
+    if (strpos($var, ':') !== false) {
+      list($ns, $elt) = explode(':', $var, 2);
+      foreach ($this->_element->childNodes as $child) {
+        if ($child->localName == $elt && $child->prefix == $ns) {
+          return true;
+        }
+      }
+    } else {
+      foreach ($this->_element->childNodes as $child) {
+        if ($child->localName == $var) {
+          return true;
+        }
+      }
+    }
+  }
+
+  /**
+   * Get the value of an element with method syntax.
+   *
+   * Map method calls to get the string value of the requested
+   * element. If there are multiple elements that match, this will
+   * return an array of those objects.
+   *
+   * @param  string $var    The element to get the string value of.
+   * @param  mixed  $unused This parameter is not used.
+   * @return mixed The node's value, null, or an array of nodes.
+   */
+  public function __call($var, $unused) {
+    $nodes = $this->_children($var);
+    
+    if (! $nodes) {
+      return null;
+    } elseif (count($nodes) > 1) {
+      return $nodes;
+    } else {
+      return $nodes[0]->nodeValue;
+    }
+  }
+
+  /**
+   * Remove all children matching $var.
+   *
+   * @param  string $var
+   * @return void
+   */
+  public function __unset($var) {
+    $nodes = $this->_children($var);
+    foreach ($nodes as $node) {
+      $parent = $node->parentNode;
+      $parent->removeChild($node);
+    }
+  }
+
+  /**
+   * Returns the nodeValue of this element when this object is used
+   * in a string context.
+   *
+   * @return string
+   */
+  public function __toString() {
+    return $this->_element->nodeValue;
+  }
+
+  /**
+   * Finds children with tagnames matching $var
+   *
+   * Similar to SimpleXML's children() method.
+   *
+   * @param  string $var Tagname to match, can be either namespace:tagName or just tagName.
+   * @return array
+   */
+  protected function _children($var) {
+    $found = array();
+    
+    // Look for access of the form {ns:var}.
+    if (strpos($var, ':') !== false) {
+      list($ns, $elt) = explode(':', $var, 2);
+      foreach ($this->_element->childNodes as $child) {
+        if ($child->localName == $elt && $child->prefix == $ns) {
+          $found[] = $child;
+        }
+      }
+    } else {
+      foreach ($this->_element->childNodes as $child) {
+        if ($child->localName == $var) {
+          $found[] = $child;
+        }
+      }
+    }
+    
+    return $found;
+  }
+
+  /**
+   * Required by the ArrayAccess interface.
+   *
+   * @param  string $offset
+   * @return boolean
+   */
+  public function offsetExists($offset) {
+    if (strpos($offset, ':') !== false) {
+      list($ns, $attr) = explode(':', $offset, 2);
+      return $this->_element->hasAttributeNS(Zend_Feed::lookupNamespace($ns), $attr);
+    } else {
+      return $this->_element->hasAttribute($offset);
+    }
+  }
+
+  /**
+   * Required by the ArrayAccess interface.
+   *
+   * @param  string $offset
+   * @return string
+   */
+  public function offsetGet($offset) {
+    if (strpos($offset, ':') !== false) {
+      list($ns, $attr) = explode(':', $offset, 2);
+      return $this->_element->getAttributeNS(Zend_Feed::lookupNamespace($ns), $attr);
+    } else {
+      return $this->_element->getAttribute($offset);
+    }
+  }
+
+  /**
+   * Required by the ArrayAccess interface.
+   *
+   * @param  string $offset
+   * @param  string $value
+   * @return string
+   */
+  public function offsetSet($offset, $value) {
+    $this->ensureAppended();
+    
+    if (strpos($offset, ':') !== false) {
+      list($ns, $attr) = explode(':', $offset, 2);
+      return $this->_element->setAttributeNS(Zend_Feed::lookupNamespace($ns), $attr, $value);
+    } else {
+      return $this->_element->setAttribute($offset, $value);
+    }
+  }
+
+  /**
+   * Required by the ArrayAccess interface.
+   *
+   * @param  string $offset
+   * @return boolean
+   */
+  public function offsetUnset($offset) {
+    if (strpos($offset, ':') !== false) {
+      list($ns, $attr) = explode(':', $offset, 2);
+      return $this->_element->removeAttributeNS(Zend_Feed::lookupNamespace($ns), $attr);
+    } else {
+      return $this->_element->removeAttribute($offset);
+    }
+  }
+
+}
diff --git a/trunk/php/external/Zend/Feed/Entry/Abstract.php b/trunk/php/external/Zend/Feed/Entry/Abstract.php
new file mode 100644
index 0000000..15f406b
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Entry/Abstract.php
@@ -0,0 +1,118 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Abstract.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed
+ */
+require_once 'external/Zend/Feed.php';
+
+/**
+ * @see Zend_Feed_Element
+ */
+require_once 'external/Zend/Feed/Element.php';
+
+/**
+ * Zend_Feed_Entry_Abstract represents a single entry in an Atom or RSS
+ * feed.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Feed_Entry_Abstract extends Zend_Feed_Element {
+  /**
+   * Root XML element for entries. Subclasses must define this to a
+   * non-null value.
+   *
+   * @var string
+   */
+  protected $_rootElement;
+  
+  /**
+   * Root namespace for entries. Subclasses may define this to a
+   * non-null value.
+   *
+   * @var string
+   */
+  protected $_rootNamespace = null;
+
+  /**
+   * Zend_Feed_Entry_Abstract constructor
+   *
+   * The Zend_Feed_Entry_Abstract constructor takes the URI of the feed the entry
+   * is part of, and optionally an XML construct (usually a
+   * SimpleXMLElement, but it can be an XML string or a DOMNode as
+   * well) that contains the contents of the entry.
+   *
+   * @param  string $uri
+   * @param  SimpleXMLElement|DOMNode|string  $element
+   * @return void
+   * @throws Zend_Feed_Exception
+   */
+  public function __construct($uri = null, $element = null) {
+    if (! ($element instanceof DOMElement)) {
+      if ($element) {
+        // Load the feed as an XML DOMDocument object
+        @ini_set('track_errors', 1);
+        $doc = @DOMDocument::loadXML($element);
+        @ini_restore('track_errors');
+        
+        if (! $doc) {
+          // prevent the class to generate an undefined variable notice (ZF-2590)
+          if (! isset($php_errormsg)) {
+            if (function_exists('xdebug_is_enabled')) {
+              $php_errormsg = '(error message not available, when XDebug is running)';
+            } else {
+              $php_errormsg = '(error message not available)';
+            }
+          }
+          
+          /** 
+           * @see Zend_Feed_Exception
+           */
+          require_once 'external/Zend/Feed/Exception.php';
+          throw new Zend_Feed_Exception("DOMDocument cannot parse XML: $php_errormsg");
+        }
+        
+        $element = $doc->getElementsByTagName($this->_rootElement)->item(0);
+        if (! $element) {
+          /** 
+           * @see Zend_Feed_Exception
+           */
+          require_once 'external/Zend/Feed/Exception.php';
+          throw new Zend_Feed_Exception('No root <' . $this->_rootElement . '> element found, cannot parse feed.');
+        }
+      } else {
+        $doc = new DOMDocument('1.0', 'utf-8');
+        if ($this->_rootNamespace !== null) {
+          $element = $doc->createElementNS(Zend_Feed::lookupNamespace($this->_rootNamespace), $this->_rootElement);
+        } else {
+          $element = $doc->createElement($this->_rootElement);
+        }
+      }
+    }
+    
+    parent::__construct($element);
+  }
+
+}
diff --git a/trunk/php/external/Zend/Feed/Entry/Atom.php b/trunk/php/external/Zend/Feed/Entry/Atom.php
new file mode 100644
index 0000000..a54133b
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Entry/Atom.php
@@ -0,0 +1,262 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Atom.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed_Entry_Abstract
+ */
+require_once 'external/Zend/Feed/Entry/Abstract.php';
+
+/**
+ * Concrete class for working with Atom entries.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Entry_Atom extends Zend_Feed_Entry_Abstract {
+  /**
+   * Root XML element for Atom entries.
+   *
+   * @var string
+   */
+  protected $_rootElement = 'entry';
+  
+  /**
+   * Root namespace for Atom entries.
+   *
+   * @var string
+   */
+  protected $_rootNamespace = 'atom';
+
+  /**
+   * Delete an atom entry.
+   *
+   * Delete tries to delete this entry from its feed. If the entry
+   * does not contain a link rel="edit", we throw an error (either
+   * the entry does not yet exist or this is not an editable
+   * feed). If we have a link rel="edit", we do the empty-body
+   * HTTP DELETE to that URI and check for a response of 2xx.
+   * Usually the response would be 204 No Content, but the Atom
+   * Publishing Protocol permits it to be 200 OK.
+   *
+   * @return void
+   * @throws Zend_Feed_Exception
+   */
+  public function delete() {
+    // Look for link rel="edit" in the entry object.
+    $deleteUri = $this->link('edit');
+    if (! $deleteUri) {
+      /** 
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('Cannot delete entry; no link rel="edit" is present.');
+    }
+    
+    // DELETE
+    $client = Zend_Feed::getHttpClient();
+    do {
+      $client->setUri($deleteUri);
+      if (Zend_Feed::getHttpMethodOverride()) {
+        $client->setHeader('X-HTTP-Method-Override', 'DELETE');
+        $response = $client->request('POST');
+      } else {
+        $response = $client->request('DELETE');
+      }
+      $httpStatus = $response->getStatus();
+      switch ((int)$httpStatus / 100) {
+        // Success
+        case 2:
+          return true;
+        // Redirect
+        case 3:
+          $deleteUri = $response->getHeader('Location');
+          continue;
+        // Error
+        default:
+          /** 
+           * @see Zend_Feed_Exception
+           */
+          require_once 'external/Zend/Feed/Exception.php';
+          throw new Zend_Feed_Exception("Expected response code 2xx, got $httpStatus");
+      }
+    } while (true);
+  }
+
+  /**
+   * Save a new or updated Atom entry.
+   *
+   * Save is used to either create new entries or to save changes to
+   * existing ones. If we have a link rel="edit", we are changing
+   * an existing entry. In this case we re-serialize the entry and
+   * PUT it to the edit URI, checking for a 200 OK result.
+   *
+   * For posting new entries, you must specify the $postUri
+   * parameter to save() to tell the object where to post itself.
+   * We use $postUri and POST the serialized entry there, checking
+   * for a 201 Created response. If the insert is successful, we
+   * then parse the response from the POST to get any values that
+   * the server has generated: an id, an updated time, and its new
+   * link rel="edit".
+   *
+   * @param  string $postUri Location to POST for creating new entries.
+   * @return void
+   * @throws Zend_Feed_Exception
+   */
+  public function save($postUri = null) {
+    if ($this->id()) {
+      // If id is set, look for link rel="edit" in the
+      // entry object and PUT.
+      $editUri = $this->link('edit');
+      if (! $editUri) {
+        /** 
+         * @see Zend_Feed_Exception
+         */
+        require_once 'external/Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Cannot edit entry; no link rel="edit" is present.');
+      }
+      
+      $client = Zend_Feed::getHttpClient();
+      $client->setUri($editUri);
+      if (Zend_Feed::getHttpMethodOverride()) {
+        $client->setHeaders(array('X-HTTP-Method-Override: PUT', 
+            'Content-Type: application/atom+xml'));
+        $client->setRawData($this->saveXML());
+        $response = $client->request('POST');
+      } else {
+        $client->setHeaders('Content-Type', 'application/atom+xml');
+        $client->setRawData($this->saveXML());
+        $response = $client->request('PUT');
+      }
+      if ($response->getStatus() !== 200) {
+        /** 
+         * @see Zend_Feed_Exception
+         */
+        require_once 'external/Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Expected response code 200, got ' . $response->getStatus());
+      }
+    } else {
+      if ($postUri === null) {
+        /** 
+         * @see Zend_Feed_Exception
+         */
+        require_once 'external/Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('PostURI must be specified to save new entries.');
+      }
+      $client = Zend_Feed::getHttpClient();
+      $client->setUri($postUri);
+      $client->setRawData($this->saveXML());
+      $response = $client->request('POST');
+      
+      if ($response->getStatus() !== 201) {
+        /** 
+         * @see Zend_Feed_Exception
+         */
+        require_once 'external/Zend/Feed/Exception.php';
+        throw new Zend_Feed_Exception('Expected response code 201, got ' . $response->getStatus());
+      }
+    }
+    
+    // Update internal properties using $client->responseBody;
+    @ini_set('track_errors', 1);
+    $newEntry = @DOMDocument::loadXML($response->getBody());
+    @ini_restore('track_errors');
+    
+    if (! $newEntry) {
+      // prevent the class to generate an undefined variable notice (ZF-2590)
+      if (! isset($php_errormsg)) {
+        if (function_exists('xdebug_is_enabled')) {
+          $php_errormsg = '(error message not available, when XDebug is running)';
+        } else {
+          $php_errormsg = '(error message not available)';
+        }
+      }
+      
+      /** 
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('XML cannot be parsed: ' . $php_errormsg);
+    }
+    
+    $newEntry = $newEntry->getElementsByTagName($this->_rootElement)->item(0);
+    if (! $newEntry) {
+      /** 
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('No root <feed> element found in server response:' . "\n\n" . $client->responseBody);
+    }
+    
+    if ($this->_element->parentNode) {
+      $oldElement = $this->_element;
+      $this->_element = $oldElement->ownerDocument->importNode($newEntry, true);
+      $oldElement->parentNode->replaceChild($this->_element, $oldElement);
+    } else {
+      $this->_element = $newEntry;
+    }
+  }
+
+  /**
+   * Easy access to <link> tags keyed by "rel" attributes.
+   *
+   * If $elt->link() is called with no arguments, we will attempt to
+   * return the value of the <link> tag(s) like all other
+   * method-syntax attribute access. If an argument is passed to
+   * link(), however, then we will return the "href" value of the
+   * first <link> tag that has a "rel" attribute matching $rel:
+   *
+   * $elt->link(): returns the value of the link tag.
+   * $elt->link('self'): returns the href from the first <link rel="self"> in the entry.
+   *
+   * @param  string $rel The "rel" attribute to look for.
+   * @return mixed
+   */
+  public function link($rel = null) {
+    if ($rel === null) {
+      return parent::__call('link', null);
+    }
+    
+    // index link tags by their "rel" attribute.
+    $links = parent::__get('link');
+    if (! is_array($links)) {
+      if ($links instanceof Zend_Feed_Element) {
+        $links = array($links);
+      } else {
+        return $links;
+      }
+    }
+    
+    foreach ($links as $link) {
+      if (empty($link['rel'])) {
+        continue;
+      }
+      if ($rel == $link['rel']) {
+        return $link['href'];
+      }
+    }
+    
+    return null;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Feed/Entry/Rss.php b/trunk/php/external/Zend/Feed/Entry/Rss.php
new file mode 100644
index 0000000..c9be58e
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Entry/Rss.php
@@ -0,0 +1,115 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Rss.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed_Entry_Abstract
+ */
+require_once 'external/Zend/Feed/Entry/Abstract.php';
+
+/**
+ * Concrete class for working with RSS items.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Entry_Rss extends Zend_Feed_Entry_Abstract {
+  /**
+   * Root XML element for RSS items.
+   *
+   * @var string
+   */
+  protected $_rootElement = 'item';
+
+  /**
+   * Overwrites parent::_get method to enable read access
+   * to content:encoded element.
+   *
+   * @param  string $var The property to access.
+   * @return mixed
+   */
+  public function __get($var) {
+    switch ($var) {
+      case 'content':
+        $prefix = $this->_element->lookupPrefix('http://purl.org/rss/1.0/modules/content/');
+        return parent::__get("$prefix:encoded");
+      default:
+        return parent::__get($var);
+    }
+  }
+
+  /**
+   * Overwrites parent::_set method to enable write access
+   * to content:encoded element.
+   *
+   * @param  string $var The property to change.
+   * @param  string $val The property's new value.
+   * @return void
+   */
+  public function __set($var, $value) {
+    switch ($var) {
+      case 'content':
+        parent::__set('content:encoded', $value);
+        break;
+      default:
+        parent::__set($var, $value);
+    }
+  }
+
+  /**
+   * Overwrites parent::_isset method to enable access
+   * to content:encoded element.
+   *
+   * @param  string $var
+   * @return boolean
+   */
+  public function __isset($var) {
+    switch ($var) {
+      case 'content':
+        // don't use other callback to prevent invalid returned value
+        return $this->content() !== null;
+      default:
+        return parent::__isset($var);
+    }
+  }
+
+  /**
+   * Overwrites parent::_call method to enable read access
+   * to content:encoded element.
+   * Please note that method-style write access is not currently supported
+   * by parent method, consequently this method doesn't as well.
+   *
+   * @param  string $var    The element to get the string value of.
+   * @param  mixed  $unused This parameter is not used.
+   * @return mixed The node's value, null, or an array of nodes.
+   */
+  public function __call($var, $unused) {
+    switch ($var) {
+      case 'content':
+        $prefix = $this->_element->lookupPrefix('http://purl.org/rss/1.0/modules/content/');
+        return parent::__call("$prefix:encoded", $unused);
+      default:
+        return parent::__call($var, $unused);
+    }
+  }
+}
diff --git a/trunk/php/external/Zend/Feed/Exception.php b/trunk/php/external/Zend/Feed/Exception.php
new file mode 100644
index 0000000..c15543f
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Exception.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Exception.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Exception
+ */
+require_once 'external/Zend/Exception.php';
+
+/**
+ * Feed exceptions
+ *
+ * Class to represent exceptions that occur during Feed operations.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Exception extends Zend_Exception {
+}
+
diff --git a/trunk/php/external/Zend/Feed/Rss.php b/trunk/php/external/Zend/Feed/Rss.php
new file mode 100644
index 0000000..a7b48cd
--- /dev/null
+++ b/trunk/php/external/Zend/Feed/Rss.php
@@ -0,0 +1,500 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Rss.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Feed_Abstract
+ */
+require_once 'external/Zend/Feed/Abstract.php';
+
+/**
+ * @see Zend_Feed_Entry_Rss
+ */
+require_once 'external/Zend/Feed/Entry/Rss.php';
+
+/**
+ * RSS channel class
+ *
+ * The Zend_Feed_Rss class is a concrete subclass of
+ * Zend_Feed_Abstract meant for representing RSS channels. It does not
+ * add any methods to its parent, just provides a classname to check
+ * against with the instanceof operator, and expects to be handling
+ * RSS-formatted data instead of Atom.
+ *
+ * @category   Zend
+ * @package    Zend_Feed
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Feed_Rss extends Zend_Feed_Abstract {
+  /**
+   * The classname for individual channel elements.
+   *
+   * @var string
+   */
+  protected $_entryClassName = 'Zend_Feed_Entry_Rss';
+
+  /**
+   * The element name for individual channel elements (RSS <item>s).
+   *
+   * @var string
+   */
+  protected $_entryElementName = 'item';
+
+  /**
+   * The default namespace for RSS channels.
+   *
+   * @var string
+   */
+  protected $_defaultNamespace = 'rss';
+
+  /**
+   * Override Zend_Feed_Abstract to set up the $_element and $_entries aliases.
+   *
+   * @return void
+   * @throws Zend_Feed_Exception
+   */
+  public function __wakeup() {
+    parent::__wakeup();
+
+    // Find the base channel element and create an alias to it.
+    $rdf = $this->_element->getElementsByTagNameNS('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'RDF')->item(0);
+    if ($rdf) {
+      $this->_element = $rdf;
+      $channel = $this->_element->getElementsByTagName('channel')->item(0);
+      if ($channel->getElementsByTagName('title')->item(0))
+      $this->_element->appendChild($channel->getElementsByTagName('title')->item(0));
+      if ($channel->getElementsByTagName('link')->item(0))
+      $this->_element->appendChild($channel->getElementsByTagName('link')->item(0));
+      if ($channel->getElementsByTagName('description')->item(0))
+      $this->_element->appendChild($channel->getElementsByTagName('description')->item(0));
+    } else {
+      $this->_element = $this->_element->getElementsByTagName('channel')->item(0);
+    }
+    if (! $this->_element) {
+      /**
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('No root <channel> element found, cannot parse channel.');
+    }
+
+    // Find the entries and save a pointer to them for speed and
+    // simplicity.
+    $this->_buildEntryCache();
+  }
+
+  /**
+   * Make accessing some individual elements of the channel easier.
+   *
+   * Special accessors 'item' and 'items' are provided so that if
+   * you wish to iterate over an RSS channel's items, you can do so
+   * using foreach ($channel->items as $item) or foreach
+   * ($channel->item as $item).
+   *
+   * @param  string $var The property to access.
+   * @return mixed
+   */
+  public function __get($var) {
+    switch ($var) {
+      case 'item':
+        // fall through to the next case
+      case 'items':
+        return $this;
+
+      default:
+        return parent::__get($var);
+    }
+  }
+
+  /**
+   * Generate the header of the feed when working in write mode
+   *
+   * @param  array $array the data to use
+   * @return DOMElement root node
+   */
+  protected function _mapFeedHeaders($array) {
+    $channel = $this->_element->createElement('channel');
+
+    $title = $this->_element->createElement('title');
+    $title->appendChild($this->_element->createCDATASection($array->title));
+    $channel->appendChild($title);
+
+    $link = $this->_element->createElement('link', $array->link);
+    $channel->appendChild($link);
+
+    $desc = isset($array->description) ? $array->description : '';
+    $description = $this->_element->createElement('description');
+    $description->appendChild($this->_element->createCDATASection($desc));
+    $channel->appendChild($description);
+
+    $pubdate = isset($array->lastUpdate) ? $array->lastUpdate : time();
+    $pubdate = $this->_element->createElement('pubDate', gmdate('r', $pubdate));
+    $channel->appendChild($pubdate);
+
+    if (isset($array->published)) {
+      $lastBuildDate = $this->_element->createElement('lastBuildDate', gmdate('r', $array->published));
+    }
+
+    $editor = '';
+    if (! empty($array->email)) {
+      $editor .= $array->email;
+    }
+    if (! empty($array->author)) {
+      $editor .= ' (' . $array->author . ')';
+    }
+    if (! empty($editor)) {
+      $author = $this->_element->createElement('managingEditor', ltrim($editor));
+      $channel->appendChild($author);
+    }
+    if (isset($array->webmaster)) {
+      $channel->appendChild($this->_element->createElement('webMaster', $array->webmaster));
+    }
+
+    if (! empty($array->copyright)) {
+      $copyright = $this->_element->createElement('copyright', $array->copyright);
+      $channel->appendChild($copyright);
+    }
+
+    if (! empty($array->image)) {
+      $image = $this->_element->createElement('image');
+      $url = $this->_element->createElement('url', $array->image);
+      $image->appendChild($url);
+      $imagetitle = $this->_element->createElement('title', $array->title);
+      $image->appendChild($imagetitle);
+      $imagelink = $this->_element->createElement('link', $array->link);
+      $image->appendChild($imagelink);
+
+      $channel->appendChild($image);
+    }
+
+    $generator = ! empty($array->generator) ? $array->generator : 'Zend_Feed';
+    $generator = $this->_element->createElement('generator', $generator);
+    $channel->appendChild($generator);
+
+    if (! empty($array->language)) {
+      $language = $this->_element->createElement('language', $array->language);
+      $channel->appendChild($language);
+    }
+
+    $doc = $this->_element->createElement('docs', 'http://blogs.law.harvard.edu/tech/rss');
+    $channel->appendChild($doc);
+
+    if (isset($array->cloud)) {
+      $cloud = $this->_element->createElement('cloud');
+      $cloud->setAttribute('domain', $array->cloud['uri']->getHost());
+      $cloud->setAttribute('port', $array->cloud['uri']->getPort());
+      $cloud->setAttribute('path', $array->cloud['uri']->getPath());
+      $cloud->setAttribute('registerProcedure', $array->cloud['procedure']);
+      $cloud->setAttribute('protocol', $array->cloud['protocol']);
+      $channel->appendChild($cloud);
+    }
+
+    if (isset($array->rating)) {
+      $rating = $this->_element->createElement('rating', $array->rating);
+      $channel->appendChild($rating);
+    }
+
+    if (isset($array->textInput)) {
+      $textinput = $this->_element->createElement('textInput');
+      $textinput->appendChild($this->_element->createElement('title', $array->textInput['title']));
+      $textinput->appendChild($this->_element->createElement('description', $array->textInput['description']));
+      $textinput->appendChild($this->_element->createElement('name', $array->textInput['name']));
+      $textinput->appendChild($this->_element->createElement('link', $array->textInput['link']));
+      $channel->appendChild($textinput);
+    }
+
+    if (isset($array->skipHours)) {
+      $skipHours = $this->_element->createElement('skipHours');
+      foreach ($array->skipHours as $hour) {
+        $skipHours->appendChild($this->_element->createElement('hour', $hour));
+      }
+      $channel->appendChild($skipHours);
+    }
+
+    if (isset($array->skipDays)) {
+      $skipDays = $this->_element->createElement('skipDays');
+      foreach ($array->skipDays as $day) {
+        $skipDays->appendChild($this->_element->createElement('day', $day));
+      }
+      $channel->appendChild($skipDays);
+    }
+
+    if (isset($array->itunes)) {
+      $this->_buildiTunes($channel, $array);
+    }
+
+    return $channel;
+  }
+
+  /**
+   * Adds the iTunes extensions to a root node
+   *
+   * @param  DOMElement $root
+   * @param  array $array
+   * @return void
+   */
+  private function _buildiTunes(DOMElement $root, $array) {
+    /* author node */
+    $author = '';
+    if (isset($array->itunes->author)) {
+      $author = $array->itunes->author;
+    } elseif (isset($array->author)) {
+      $author = $array->author;
+    }
+    if (! empty($author)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:author', $author);
+      $root->appendChild($node);
+    }
+
+    /* owner node */
+    $author = '';
+    $email = '';
+    if (isset($array->itunes->owner)) {
+      if (isset($array->itunes->owner['name'])) {
+        $author = $array->itunes->owner['name'];
+      }
+      if (isset($array->itunes->owner['email'])) {
+        $email = $array->itunes->owner['email'];
+      }
+    }
+    if (empty($author) && isset($array->author)) {
+      $author = $array->author;
+    }
+    if (empty($email) && isset($array->email)) {
+      $email = $array->email;
+    }
+    if (! empty($author) || ! empty($email)) {
+      $owner = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:owner');
+      if (! empty($author)) {
+        $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:name', $author);
+        $owner->appendChild($node);
+      }
+      if (! empty($email)) {
+        $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:email', $email);
+        $owner->appendChild($node);
+      }
+      $root->appendChild($owner);
+    }
+    $image = '';
+    if (isset($array->itunes->image)) {
+      $image = $array->itunes->image;
+    } elseif (isset($array->image)) {
+      $image = $array->image;
+    }
+    if (! empty($image)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:image');
+      $node->setAttribute('href', $image);
+      $root->appendChild($node);
+    }
+    $subtitle = '';
+    if (isset($array->itunes->subtitle)) {
+      $subtitle = $array->itunes->subtitle;
+    } elseif (isset($array->description)) {
+      $subtitle = $array->description;
+    }
+    if (! empty($subtitle)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:subtitle', $subtitle);
+      $root->appendChild($node);
+    }
+    $summary = '';
+    if (isset($array->itunes->summary)) {
+      $summary = $array->itunes->summary;
+    } elseif (isset($array->description)) {
+      $summary = $array->description;
+    }
+    if (! empty($summary)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:summary', $summary);
+      $root->appendChild($node);
+    }
+    if (isset($array->itunes->block)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:block', $array->itunes->block);
+      $root->appendChild($node);
+    }
+    if (isset($array->itunes->explicit)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:explicit', $array->itunes->explicit);
+      $root->appendChild($node);
+    }
+    if (isset($array->itunes->keywords)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:keywords', $array->itunes->keywords);
+      $root->appendChild($node);
+    }
+    if (isset($array->itunes->new_feed_url)) {
+      $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:new-feed-url', $array->itunes->new_feed_url);
+      $root->appendChild($node);
+    }
+    if (isset($array->itunes->category)) {
+      foreach ($array->itunes->category as $i => $category) {
+        $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:category');
+        $node->setAttribute('text', $category['main']);
+        $root->appendChild($node);
+        $add_end_category = false;
+        if (! empty($category['sub'])) {
+          $add_end_category = true;
+          $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:category');
+          $node->setAttribute('text', $category['sub']);
+          $root->appendChild($node);
+        }
+        if ($i > 0 || $add_end_category) {
+          $node = $this->_element->createElementNS('http://www.itunes.com/DTDs/Podcast-1.0.dtd', 'itunes:category');
+          $root->appendChild($node);
+        }
+      }
+    }
+  }
+
+  /**
+   * Generate the entries of the feed when working in write mode
+   *
+   * The following nodes are constructed for each feed entry
+   * <item>
+   *    <title>entry title</title>
+   *    <link>url to feed entry</link>
+   *    <guid>url to feed entry</guid>
+   *    <description>short text</description>
+   *    <content:encoded>long version, can contain html</content:encoded>
+   * </item>
+   *
+   * @param  DOMElement $root the root node to use
+   * @param  array $array the data to use
+   * @return void
+   */
+  protected function _mapFeedEntries(DOMElement $root, $array) {
+    Zend_Feed::registerNamespace('content', 'http://purl.org/rss/1.0/modules/content/');
+
+    foreach ($array as $dataentry) {
+      $item = $this->_element->createElement('item');
+
+      $title = $this->_element->createElement('title');
+      $title->appendChild($this->_element->createCDATASection($dataentry->title));
+      $item->appendChild($title);
+
+      $link = $this->_element->createElement('link', $dataentry->link);
+      $item->appendChild($link);
+
+      if (isset($dataentry->guid)) {
+        $guid = $this->_element->createElement('guid', $dataentry->guid);
+        $item->appendChild($guid);
+      }
+
+      $description = $this->_element->createElement('description');
+      $description->appendChild($this->_element->createCDATASection($dataentry->description));
+      $item->appendChild($description);
+
+      if (isset($dataentry->content)) {
+        $content = $this->_element->createElement('content:encoded');
+        $content->appendChild($this->_element->createCDATASection($dataentry->content));
+        $item->appendChild($content);
+      }
+
+      $pubdate = isset($dataentry->lastUpdate) ? $dataentry->lastUpdate : time();
+      $pubdate = $this->_element->createElement('pubDate', gmdate('r', $pubdate));
+      $item->appendChild($pubdate);
+
+      if (isset($dataentry->category)) {
+        foreach ($dataentry->category as $category) {
+          $node = $this->_element->createElement('category', $category['term']);
+          if (isset($category['scheme'])) {
+            $node->setAttribute('domain', $category['scheme']);
+          }
+          $item->appendChild($node);
+        }
+      }
+
+      if (isset($dataentry->source)) {
+        $source = $this->_element->createElement('source', $dataentry->source['title']);
+        $source->setAttribute('url', $dataentry->source['url']);
+        $item->appendChild($source);
+      }
+
+      if (isset($dataentry->comments)) {
+        $comments = $this->_element->createElement('comments', $dataentry->comments);
+        $item->appendChild($comments);
+      }
+      if (isset($dataentry->commentRss)) {
+        $comments = $this->_element->createElementNS('http://wellformedweb.org/CommentAPI/', 'wfw:commentRss', $dataentry->commentRss);
+        $item->appendChild($comments);
+      }
+
+      if (isset($dataentry->enclosure)) {
+        foreach ($dataentry->enclosure as $enclosure) {
+          $node = $this->_element->createElement('enclosure');
+          $node->setAttribute('url', $enclosure['url']);
+          if (isset($enclosure['type'])) {
+            $node->setAttribute('type', $enclosure['type']);
+          }
+          if (isset($enclosure['length'])) {
+            $node->setAttribute('length', $enclosure['length']);
+          }
+          $item->appendChild($node);
+        }
+      }
+
+      $root->appendChild($item);
+    }
+  }
+
+  /**
+   * Override Zend_Feed_Element to include <rss> root node
+   *
+   * @return string
+   */
+  public function saveXml() {
+    // Return a complete document including XML prologue.
+    $doc = new DOMDocument($this->_element->ownerDocument->version, $this->_element->ownerDocument->actualEncoding);
+    $root = $doc->createElement('rss');
+
+    // Use rss version 2.0
+    $root->setAttribute('version', '2.0');
+
+    // Content namespace
+    $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:content', 'http://purl.org/rss/1.0/modules/content/');
+    $root->appendChild($doc->importNode($this->_element, true));
+
+    // Append root node
+    $doc->appendChild($root);
+
+    // Format output
+    $doc->formatOutput = true;
+
+    return $doc->saveXML();
+  }
+
+  /**
+   * Send feed to a http client with the correct header
+   *
+   * @return void
+   * @throws Zend_Feed_Exception if headers have already been sent
+   */
+  public function send() {
+    if (headers_sent()) {
+      /**
+       * @see Zend_Feed_Exception
+       */
+      require_once 'external/Zend/Feed/Exception.php';
+      throw new Zend_Feed_Exception('Cannot send RSS because headers have already been sent.');
+    }
+
+    header('Content-type: application/rss+xml; charset: ' . $this->_element->ownerDocument->actualEncoding);
+
+    echo $this->saveXml();
+  }
+
+}
diff --git a/trunk/php/external/Zend/Http/Client.php b/trunk/php/external/Zend/Http/Client.php
new file mode 100644
index 0000000..a8a670a
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Client.php
@@ -0,0 +1,1036 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client
+ * @version    $Id: Client.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Loader.php';
+require_once 'external/Zend/Uri.php';
+require_once 'external/Zend/Http/Client/Adapter/Interface.php';
+require_once 'external/Zend/Http/Response.php';
+
+/**
+ * Zend_Http_Client is an implemetation of an HTTP client in PHP. The client
+ * supports basic features like sending different HTTP requests and handling
+ * redirections, as well as more advanced features like proxy settings, HTTP
+ * authentication and cookie persistance (using a Zend_Http_CookieJar object)
+ *
+ * @todo Implement proxy settings
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client
+ * @throws     Zend_Http_Client_Exception
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Client {
+  /**
+   * HTTP request methods
+   */
+  const GET = 'GET';
+  const POST = 'POST';
+  const PUT = 'PUT';
+  const HEAD = 'HEAD';
+  const DELETE = 'DELETE';
+  const TRACE = 'TRACE';
+  const OPTIONS = 'OPTIONS';
+  const CONNECT = 'CONNECT';
+  
+  /**
+   * Supported HTTP Authentication methods
+   */
+  const AUTH_BASIC = 'basic';
+  //const AUTH_DIGEST = 'digest'; <-- not implemented yet
+  
+
+  /**
+   * HTTP protocol versions
+   */
+  const HTTP_1 = '1.1';
+  const HTTP_0 = '1.0';
+  
+  /**
+   * POST data encoding methods
+   */
+  const ENC_URLENCODED = 'application/x-www-form-urlencoded';
+  const ENC_FORMDATA = 'multipart/form-data';
+  
+  /**
+   * Configuration array, set using the constructor or using ::setConfig()
+   *
+   * @var unknown_type
+   */
+  protected $config = array('maxredirects' => 5, 'strictredirects' => false, 
+      'useragent' => 'Zend_Http_Client', 'timeout' => 10, 
+      'adapter' => 'Zend_Http_Client_Adapter_Socket', 'httpversion' => self::HTTP_1, 
+      'keepalive' => false, 'storeresponse' => true, 'strict' => true);
+  
+  /**
+   * The adapter used to preform the actual connection to the server
+   *
+   * @var Zend_Http_Client_Adapter_Interface
+   */
+  protected $adapter = null;
+  
+  /**
+   * Request URI
+   *
+   * @var Zend_Uri_Http
+   */
+  protected $uri;
+  
+  /**
+   * Associative array of request headers
+   *
+   * @var array
+   */
+  protected $headers = array();
+  
+  /**
+   * HTTP request method
+   *
+   * @var string
+   */
+  protected $method = self::GET;
+  
+  /**
+   * Associative array of GET parameters
+   *
+   * @var array
+   */
+  protected $paramsGet = array();
+  
+  /**
+   * Assiciative array of POST parameters
+   *
+   * @var array
+   */
+  protected $paramsPost = array();
+  
+  /**
+   * Request body content type (for POST requests)
+   *
+   * @var string
+   */
+  protected $enctype = null;
+  
+  /**
+   * The raw post data to send. Could be set by setRawData($data, $enctype).
+   *
+   * @var string
+   */
+  protected $raw_post_data = null;
+  
+  /**
+   * HTTP Authentication settings
+   *
+   * Expected to be an associative array with this structure:
+   * $this->auth = array('user' => 'username', 'password' => 'password', 'type' => 'basic')
+   * Where 'type' should be one of the supported authentication types (see the AUTH_*
+   * constants), for example 'basic' or 'digest'.
+   *
+   * If null, no authentication will be used.
+   *
+   * @var array|null
+   */
+  protected $auth;
+  
+  /**
+   * File upload arrays (used in POST requests)
+   *
+   * An associative array, where each element is of the format:
+   *   'name' => array('filename.txt', 'text/plain', 'This is the actual file contents')
+   *
+   * @var array
+   */
+  protected $files = array();
+  
+  /**
+   * The client's cookie jar
+   *
+   * @var Zend_Http_CookieJar
+   */
+  protected $cookiejar = null;
+  
+  /**
+   * The last HTTP request sent by the client, as string
+   *
+   * @var string
+   */
+  protected $last_request = null;
+  
+  /**
+   * The last HTTP response received by the client
+   *
+   * @var Zend_Http_Response
+   */
+  protected $last_response = null;
+  
+  /**
+   * Redirection counter
+   *
+   * @var int
+   */
+  protected $redirectCounter = 0;
+
+  /**
+   * Contructor method. Will create a new HTTP client. Accepts the target
+   * URL and optionally and array of headers.
+   *
+   * @param Zend_Uri_Http|string $uri
+   * @param array $headers Optional request headers to set
+   */
+  public function __construct($uri = null, $config = null) {
+    if ($uri !== null) $this->setUri($uri);
+    if ($config !== null) $this->setConfig($config);
+  }
+
+  /**
+   * Set the URI for the next request
+   *
+   * @param  Zend_Uri_Http|string $uri
+   * @return Zend_Http_Client
+   * @throws Zend_Http_Client_Exception
+   */
+  public function setUri($uri) {
+    if (is_string($uri)) {
+      $uri = Zend_Uri::factory($uri);
+    }
+    
+    if (! $uri instanceof Zend_Uri_Http) {
+      require_once 'external/Zend/Http/Client/Exception.php';
+      throw new Zend_Http_Client_Exception('Passed parameter is not a valid HTTP URI.');
+    }
+    
+    // We have no ports, set the defaults
+    if (! $uri->getPort()) {
+      $uri->setPort(($uri->getScheme() == 'https' ? 443 : 80));
+    }
+    
+    $this->uri = $uri;
+    
+    return $this;
+  }
+
+  /**
+   * Get the URI for the next request
+   *
+   * @param boolean $as_string If true, will return the URI as a string
+   * @return Zend_Uri_Http|string
+   */
+  public function getUri($as_string = false) {
+    if ($as_string && $this->uri instanceof Zend_Uri_Http) {
+      return $this->uri->__toString();
+    } else {
+      return $this->uri;
+    }
+  }
+
+  /**
+   * Set configuration parameters for this HTTP client
+   *
+   * @param array $config
+   * @return Zend_Http_Client
+   */
+  public function setConfig($config = array()) {
+    if (! is_array($config)) {
+      require_once 'external/Zend/Http/Client/Exception.php';
+      throw new Zend_Http_Client_Exception('Expected array parameter, given ' . gettype($config));
+    }
+    
+    foreach ($config as $k => $v)
+      $this->config[strtolower($k)] = $v;
+    
+    return $this;
+  }
+
+  /**
+   * Set the next request's method
+   *
+   * Validated the passed method and sets it. If we have files set for
+   * POST requests, and the new method is not POST, the files are silently
+   * dropped.
+   *
+   * @param string $method
+   * @return Zend_Http_Client
+   */
+  public function setMethod($method = self::GET) {
+    if (! preg_match('/^[A-Za-z_]+$/', $method)) {
+      require_once 'external/Zend/Http/Client/Exception.php';
+      throw new Zend_Http_Client_Exception("'{$method}' is not a valid HTTP request method.");
+    }
+    
+    if ($method == self::POST && $this->enctype === null) $this->setEncType(self::ENC_URLENCODED);
+    
+    $this->method = $method;
+    
+    return $this;
+  }
+
+  /**
+   * Set one or more request headers
+   *
+   * This function can be used in several ways to set the client's request
+   * headers:
+   * 1. By providing two parameters: $name as the header to set (eg. 'Host')
+   *    and $value as it's value (eg. 'www.example.com').
+   * 2. By providing a single header string as the only parameter
+   *    eg. 'Host: www.example.com'
+   * 3. By providing an array of headers as the first parameter
+   *    eg. array('host' => 'www.example.com', 'x-foo: bar'). In This case
+   *    the function will call itself recursively for each array item.
+   *
+   * @param string|array $name Header name, full header string ('Header: value')
+   *     or an array of headers
+   * @param mixed $value Header value or null
+   * @return Zend_Http_Client
+   */
+  public function setHeaders($name, $value = null) {
+    // If we got an array, go recusive!
+    if (is_array($name)) {
+      foreach ($name as $k => $v) {
+        if (is_string($k)) {
+          $this->setHeaders($k, $v);
+        } else {
+          $this->setHeaders($v, null);
+        }
+      }
+    } else {
+      // Check if $name needs to be split
+      if ($value === null && (strpos($name, ':') > 0)) list($name, $value) = explode(':', $name, 2);
+      
+      // Make sure the name is valid if we are in strict mode
+      if ($this->config['strict'] && (! preg_match('/^[a-zA-Z0-9-]+$/', $name))) {
+        require_once 'external/Zend/Http/Client/Exception.php';
+        throw new Zend_Http_Client_Exception("{$name} is not a valid HTTP header name");
+      }
+      
+      $normalized_name = strtolower($name);
+      
+      // If $value is null or false, unset the header
+      if ($value === null || $value === false) {
+        unset($this->headers[$normalized_name]);
+        
+      // Else, set the header
+      } else {
+        // Header names are storred lowercase internally.
+        if (is_string($value)) $value = trim($value);
+        $this->headers[$normalized_name] = array($name, $value);
+      }
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Get the value of a specific header
+   *
+   * Note that if the header has more than one value, an array
+   * will be returned.
+   *
+   * @param unknown_type $key
+   * @return string|array|null The header value or null if it is not set
+   */
+  public function getHeader($key) {
+    $key = strtolower($key);
+    if (isset($this->headers[$key])) {
+      return $this->headers[$key][1];
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Set a GET parameter for the request. Wrapper around _setParameter
+   *
+   * @param string|array $name
+   * @param string $value
+   * @return Zend_Http_Client
+   */
+  public function setParameterGet($name, $value = null) {
+    if (is_array($name)) {
+      foreach ($name as $k => $v)
+        $this->_setParameter('GET', $k, $v);
+    } else {
+      $this->_setParameter('GET', $name, $value);
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Set a POST parameter for the request. Wrapper around _setParameter
+   *
+   * @param string|array $name
+   * @param string $value
+   * @return Zend_Http_Client
+   */
+  public function setParameterPost($name, $value = null) {
+    if (is_array($name)) {
+      foreach ($name as $k => $v)
+        $this->_setParameter('POST', $k, $v);
+    } else {
+      $this->_setParameter('POST', $name, $value);
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Set a GET or POST parameter - used by SetParameterGet and SetParameterPost
+   *
+   * @param string $type GET or POST
+   * @param string $name
+   * @param string $value
+   */
+  protected function _setParameter($type, $name, $value) {
+    $parray = array();
+    $type = strtolower($type);
+    switch ($type) {
+      case 'get':
+        $parray = &$this->paramsGet;
+        break;
+      case 'post':
+        $parray = &$this->paramsPost;
+        break;
+    }
+    
+    if ($value === null) {
+      if (isset($parray[$name])) unset($parray[$name]);
+    } else {
+      $parray[$name] = $value;
+    }
+  }
+
+  /**
+   * Get the number of redirections done on the last request
+   *
+   * @return int
+   */
+  public function getRedirectionsCount() {
+    return $this->redirectCounter;
+  }
+
+  /**
+   * Set HTTP authentication parameters
+   *
+   * $type should be one of the supported types - see the self::AUTH_*
+   * constants.
+   *
+   * To enable authentication:
+   * <code>
+   * $this->setAuth('shahar', 'secret', Zend_Http_Client::AUTH_BASIC);
+   * </code>
+   *
+   * To disable authentication:
+   * <code>
+   * $this->setAuth(false);
+   * </code>
+   *
+   * @see http://www.faqs.org/rfcs/rfc2617.html
+   * @param string|false $user User name or false disable authentication
+   * @param string $password Password
+   * @param string $type Authentication type
+   * @return Zend_Http_Client
+   */
+  public function setAuth($user, $password = '', $type = self::AUTH_BASIC) {
+    // If we got false or null, disable authentication
+    if ($user === false || $user === null) {
+      $this->auth = null;
+      
+    // Else, set up authentication
+    } else {
+      // Check we got a proper authentication type
+      if (! defined('self::AUTH_' . strtoupper($type))) {
+        require_once 'external/Zend/Http/Client/Exception.php';
+        throw new Zend_Http_Client_Exception("Invalid or not supported authentication type: '$type'");
+      }
+      
+      $this->auth = array('user' => (string)$user, 'password' => (string)$password, 
+          'type' => $type);
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Set the HTTP client's cookie jar.
+   *
+   * A cookie jar is an object that holds and maintains cookies across HTTP requests
+   * and responses.
+   *
+   * @param Zend_Http_CookieJar|boolean $cookiejar Existing cookiejar object, true to create a new one, false to disable
+   * @return Zend_Http_Client
+   */
+  public function setCookieJar($cookiejar = true) {
+    if (! class_exists('Zend_Http_CookieJar')) require_once 'external/Zend/Http/CookieJar.php';
+    
+    if ($cookiejar instanceof Zend_Http_CookieJar) {
+      $this->cookiejar = $cookiejar;
+    } elseif ($cookiejar === true) {
+      $this->cookiejar = new Zend_Http_CookieJar();
+    } elseif (! $cookiejar) {
+      $this->cookiejar = null;
+    } else {
+      require_once 'external/Zend/Http/Client/Exception.php';
+      throw new Zend_Http_Client_Exception('Invalid parameter type passed as CookieJar');
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Return the current cookie jar or null if none.
+   *
+   * @return Zend_Http_CookieJar|null
+   */
+  public function getCookieJar() {
+    return $this->cookiejar;
+  }
+
+  /**
+   * Add a cookie to the request. If the client has no Cookie Jar, the cookies
+   * will be added directly to the headers array as "Cookie" headers.
+   *
+   * @param Zend_Http_Cookie|string $cookie
+   * @param string|null $value If "cookie" is a string, this is the cookie value.
+   * @return Zend_Http_Client
+   */
+  public function setCookie($cookie, $value = null) {
+    if (! class_exists('Zend_Http_Cookie')) require_once 'external/Zend/Http/Cookie.php';
+    
+    if (is_array($cookie)) {
+      foreach ($cookie as $c => $v) {
+        if (is_string($c)) {
+          $this->setCookie($c, $v);
+        } else {
+          $this->setCookie($v);
+        }
+      }
+      
+      return $this;
+    }
+    
+    if ($value !== null) $value = urlencode($value);
+    
+    if (isset($this->cookiejar)) {
+      if ($cookie instanceof Zend_Http_Cookie) {
+        $this->cookiejar->addCookie($cookie);
+      } elseif (is_string($cookie) && $value !== null) {
+        $cookie = Zend_Http_Cookie::fromString("{$cookie}={$value}", $this->uri);
+        $this->cookiejar->addCookie($cookie);
+      }
+    } else {
+      if ($cookie instanceof Zend_Http_Cookie) {
+        $name = $cookie->getName();
+        $value = $cookie->getValue();
+        $cookie = $name;
+      }
+      
+      if (preg_match("/[=,; \t\r\n\013\014]/", $cookie)) {
+        require_once 'external/Zend/Http/Client/Exception.php';
+        throw new Zend_Http_Client_Exception("Cookie name cannot contain these characters: =,; \t\r\n\013\014 ({$cookie})");
+      }
+      
+      $value = addslashes($value);
+      
+      if (! isset($this->headers['cookie'])) $this->headers['cookie'] = array('Cookie', '');
+      $this->headers['cookie'][1] .= $cookie . '=' . $value . '; ';
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Set a file to upload (using a POST request)
+   *
+   * Can be used in two ways:
+   *
+   * 1. $data is null (default): $filename is treated as the name if a local file which
+   *    will be read and sent. Will try to guess the content type using mime_content_type().
+   * 2. $data is set - $filename is sent as the file name, but $data is sent as the file
+   *    contents and no file is read from the file system. In this case, you need to
+   *    manually set the content-type ($ctype) or it will default to
+   *    application/octet-stream.
+   *
+   * @param string $filename Name of file to upload, or name to save as
+   * @param string $formname Name of form element to send as
+   * @param string $data Data to send (if null, $filename is read and sent)
+   * @param string $ctype Content type to use (if $data is set and $ctype is
+   *     null, will be application/octet-stream)
+   * @return Zend_Http_Client
+   */
+  public function setFileUpload($filename, $formname, $data = null, $ctype = null) {
+    if ($data === null) {
+      if (($data = @file_get_contents($filename)) === false) {
+        require_once 'external/Zend/Http/Client/Exception.php';
+        throw new Zend_Http_Client_Exception("Unable to read file '{$filename}' for upload");
+      }
+      
+      if (! $ctype && function_exists('mime_content_type')) $ctype = mime_content_type($filename);
+    }
+    
+    // Force enctype to multipart/form-data
+    $this->setEncType(self::ENC_FORMDATA);
+    
+    if ($ctype === null) $ctype = 'application/octet-stream';
+    $this->files[$formname] = array(basename($filename), $ctype, $data);
+    
+    return $this;
+  }
+
+  /**
+   * Set the encoding type for POST data
+   *
+   * @param string $enctype
+   * @return Zend_Http_Client
+   */
+  public function setEncType($enctype = self::ENC_URLENCODED) {
+    $this->enctype = $enctype;
+    
+    return $this;
+  }
+
+  /**
+   * Set the raw (already encoded) POST data.
+   *
+   * This function is here for two reasons:
+   * 1. For advanced user who would like to set their own data, already encoded
+   * 2. For backwards compatibilty: If someone uses the old post($data) method.
+   *    this method will be used to set the encoded data.
+   *
+   * @param string $data
+   * @param string $enctype
+   * @return Zend_Http_Client
+   */
+  public function setRawData($data, $enctype = null) {
+    $this->raw_post_data = $data;
+    $this->setEncType($enctype);
+    
+    return $this;
+  }
+
+  /**
+   * Clear all GET and POST parameters
+   *
+   * Should be used to reset the request parameters if the client is
+   * used for several concurrent requests.
+   *
+   * @return Zend_Http_Client
+   */
+  public function resetParameters() {
+    // Reset parameter data
+    $this->paramsGet = array();
+    $this->paramsPost = array();
+    $this->files = array();
+    $this->raw_post_data = null;
+    
+    // Clear outdated headers
+    if (isset($this->headers['content-type'])) unset($this->headers['content-type']);
+    if (isset($this->headers['content-length'])) unset($this->headers['content-length']);
+    
+    return $this;
+  }
+
+  /**
+   * Get the last HTTP request as string
+   *
+   * @return string
+   */
+  public function getLastRequest() {
+    return $this->last_request;
+  }
+
+  /**
+   * Get the last HTTP response received by this client
+   *
+   * If $config['storeresponse'] is set to false, or no response was
+   * stored yet, will return null
+   *
+   * @return Zend_Http_Response or null if none
+   */
+  public function getLastResponse() {
+    return $this->last_response;
+  }
+
+  /**
+   * Load the connection adapter
+   *
+   * While this method is not called more than one for a client, it is
+   * seperated from ->request() to preserve logic and readability
+   *
+   * @param Zend_Http_Client_Adapter_Interface|string $adapter
+   */
+  public function setAdapter($adapter) {
+    if (is_string($adapter)) {
+      try {
+        Zend_Loader::loadClass($adapter);
+      } catch (Zend_Exception $e) {
+        require_once 'external/Zend/Http/Client/Exception.php';
+        throw new Zend_Http_Client_Exception("Unable to load adapter '$adapter': {$e->getMessage()}");
+      }
+      
+      $adapter = new $adapter();
+    }
+    
+    if (! $adapter instanceof Zend_Http_Client_Adapter_Interface) {
+      require_once 'external/Zend/Http/Client/Exception.php';
+      throw new Zend_Http_Client_Exception('Passed adapter is not a HTTP connection adapter');
+    }
+    
+    $this->adapter = $adapter;
+    $config = $this->config;
+    unset($config['adapter']);
+    $this->adapter->setConfig($config);
+  }
+
+  /**
+   * Send the HTTP request and return an HTTP response object
+   *
+   * @param string $method
+   * @return Zend_Http_Response
+   */
+  public function request($method = null) {
+    if (! $this->uri instanceof Zend_Uri_Http) {
+      require_once 'external/Zend/Http/Client/Exception.php';
+      throw new Zend_Http_Client_Exception('No valid URI has been passed to the client');
+    }
+    
+    if ($method) $this->setMethod($method);
+    $this->redirectCounter = 0;
+    $response = null;
+    
+    // Make sure the adapter is loaded
+    if ($this->adapter == null) $this->setAdapter($this->config['adapter']);
+    
+    // Send the first request. If redirected, continue.
+    do {
+      // Clone the URI and add the additional GET parameters to it
+      $uri = clone $this->uri;
+      if (! empty($this->paramsGet)) {
+        $query = $uri->getQuery();
+        if (! empty($query)) $query .= '&';
+        $query .= http_build_query($this->paramsGet, null, '&');
+        
+        $uri->setQuery($query);
+      }
+      
+      $body = $this->prepare_body();
+      $headers = $this->prepare_headers();
+      
+      // Open the connection, send the request and read the response
+      $this->adapter->connect($uri->getHost(), $uri->getPort(), ($uri->getScheme() == 'https' ? true : false));
+      
+      $this->last_request = $this->adapter->write($this->method, $uri, $this->config['httpversion'], $headers, $body);
+      
+      $response = $this->adapter->read();
+      if (! $response) {
+        require_once 'external/Zend/Http/Client/Exception.php';
+        throw new Zend_Http_Client_Exception('Unable to read response, or response is empty');
+      }
+      
+      $response = Zend_Http_Response::fromString($response);
+      if ($this->config['storeresponse']) $this->last_response = $response;
+      
+      // Load cookies into cookie jar
+      if (isset($this->cookiejar)) $this->cookiejar->addCookiesFromResponse($response, $uri);
+      
+      // If we got redirected, look for the Location header
+      if ($response->isRedirect() && ($location = $response->getHeader('location'))) {
+        
+        // Check whether we send the exact same request again, or drop the parameters
+        // and send a GET request
+        if ($response->getStatus() == 303 || ((! $this->config['strictredirects']) && ($response->getStatus() == 302 || $response->getStatus() == 301))) {
+          
+          $this->resetParameters();
+          $this->setMethod(self::GET);
+        }
+        
+        // If we got a well formed absolute URI
+        if (Zend_Uri_Http::check($location)) {
+          $this->setHeaders('host', null);
+          $this->setUri($location);
+        
+        } else {
+          
+          // Split into path and query and set the query
+          if (strpos($location, '?') !== false) {
+            list($location, $query) = explode('?', $location, 2);
+          } else {
+            $query = '';
+          }
+          $this->uri->setQuery($query);
+          
+          // Else, if we got just an absolute path, set it
+          if (strpos($location, '/') === 0) {
+            $this->uri->setPath($location);
+            
+          // Else, assume we have a relative path
+          } else {
+            // Get the current path directory, removing any trailing slashes
+            $path = $this->uri->getPath();
+            $path = rtrim(substr($path, 0, strrpos($path, '/')), "/");
+            $this->uri->setPath($path . '/' . $location);
+          }
+        }
+        ++ $this->redirectCounter;
+      
+      } else {
+        // If we didn't get any location, stop redirecting
+        break;
+      }
+    
+    } while ($this->redirectCounter < $this->config['maxredirects']);
+    
+    return $response;
+  }
+
+  /**
+   * Prepare the request headers
+   *
+   * @return array
+   */
+  protected function prepare_headers() {
+    $headers = array();
+    
+    // Set the host header
+    if (! isset($this->headers['host'])) {
+      $host = $this->uri->getHost();
+      
+      // If the port is not default, add it
+      if (! (($this->uri->getScheme() == 'http' && $this->uri->getPort() == 80) || ($this->uri->getScheme() == 'https' && $this->uri->getPort() == 443))) {
+        $host .= ':' . $this->uri->getPort();
+      }
+      
+      $headers[] = "Host: {$host}";
+    }
+    
+    // Set the connection header
+    if (! isset($this->headers['connection'])) {
+      if (! $this->config['keepalive']) $headers[] = "Connection: close";
+    }
+    
+    // Set the Accept-encoding header if not set - depending on whether
+    // zlib is available or not.
+    if (! isset($this->headers['accept-encoding'])) {
+      if (function_exists('gzinflate')) {
+        $headers[] = 'Accept-encoding: gzip, deflate';
+      } else {
+        $headers[] = 'Accept-encoding: identity';
+      }
+    }
+    
+    // Set the content-type header
+    if ($this->method == self::POST && (! isset($this->headers['content-type']) && isset($this->enctype))) {
+      
+      $headers[] = "Content-type: {$this->enctype}";
+    }
+    
+    // Set the user agent header
+    if (! isset($this->headers['user-agent']) && isset($this->config['useragent'])) {
+      $headers[] = "User-agent: {$this->config['useragent']}";
+    }
+    
+    // Set HTTP authentication if needed
+    if (is_array($this->auth)) {
+      $auth = self::encodeAuthHeader($this->auth['user'], $this->auth['password'], $this->auth['type']);
+      $headers[] = "Authorization: {$auth}";
+    }
+    
+    // Load cookies from cookie jar
+    if (isset($this->cookiejar)) {
+      $cookstr = $this->cookiejar->getMatchingCookies($this->uri, true, Zend_Http_CookieJar::COOKIE_STRING_CONCAT);
+      
+      if ($cookstr) $headers[] = "Cookie: {$cookstr}";
+    }
+    
+    // Add all other user defined headers
+    foreach ($this->headers as $header) {
+      list($name, $value) = $header;
+      if (is_array($value)) $value = implode(', ', $value);
+      
+      $headers[] = "$name: $value";
+    }
+    
+    return $headers;
+  }
+
+  /**
+   * Prepare the request body (for POST and PUT requests)
+   *
+   * @return string
+   */
+  protected function prepare_body() {
+    // According to RFC2616, a TRACE request should not have a body.
+    if ($this->method == self::TRACE) {
+      return '';
+    }
+    
+    // If we have raw_post_data set, just use it as the body.
+    if (isset($this->raw_post_data)) {
+      $this->setHeaders('Content-length', strlen($this->raw_post_data));
+      return $this->raw_post_data;
+    }
+    
+    $body = '';
+    
+    // If we have files to upload, force enctype to multipart/form-data
+    if (count($this->files) > 0) $this->setEncType(self::ENC_FORMDATA);
+    
+    // If we have POST parameters or files, encode and add them to the body
+    if (count($this->paramsPost) > 0 || count($this->files) > 0) {
+      switch ($this->enctype) {
+        case self::ENC_FORMDATA:
+          // Encode body as multipart/form-data
+          $boundary = '---ZENDHTTPCLIENT-' . md5(microtime());
+          $this->setHeaders('Content-type', self::ENC_FORMDATA . "; boundary={$boundary}");
+          
+          // Get POST parameters and encode them
+          $params = $this->_getParametersRecursive($this->paramsPost);
+          foreach ($params as $pp) {
+            $body .= self::encodeFormData($boundary, $pp[0], $pp[1]);
+          }
+          
+          // Encode files
+          foreach ($this->files as $name => $file) {
+            $fhead = array('Content-type' => $file[1]);
+            $body .= self::encodeFormData($boundary, $name, $file[2], $file[0], $fhead);
+          }
+          
+          $body .= "--{$boundary}--\r\n";
+          break;
+        
+        case self::ENC_URLENCODED:
+          // Encode body as application/x-www-form-urlencoded
+          $this->setHeaders('Content-type', self::ENC_URLENCODED);
+          $body = http_build_query($this->paramsPost, '', '&');
+          break;
+        
+        default:
+          require_once 'external/Zend/Http/Client/Exception.php';
+          throw new Zend_Http_Client_Exception("Cannot handle content type '{$this->enctype}' automatically." . " Please use Zend_Http_Client::setRawData to send this kind of content.");
+          break;
+      }
+    }
+    
+    if ($body) $this->setHeaders('Content-length', strlen($body));
+    return $body;
+  }
+
+  /**
+   * Helper method that gets a possibly multi-level parameters array (get or
+   * post) and flattens it.
+   *
+   * The method returns an array of (key, value) pairs (because keys are not
+   * necessarily unique. If one of the parameters in as array, it will also
+   * add a [] suffix to the key.
+   *
+   * @param array $parray The parameters array
+   * @param bool $urlencode Whether to urlencode the name and value
+   * @return array
+   */
+  protected function _getParametersRecursive($parray, $urlencode = false) {
+    if (! is_array($parray)) return $parray;
+    $parameters = array();
+    
+    foreach ($parray as $name => $value) {
+      if ($urlencode) $name = urlencode($name);
+      
+      // If $value is an array, iterate over it
+      if (is_array($value)) {
+        $name .= ($urlencode ? '%5B%5D' : '[]');
+        foreach ($value as $subval) {
+          if ($urlencode) $subval = urlencode($subval);
+          $parameters[] = array($name, $subval);
+        }
+      } else {
+        if ($urlencode) $value = urlencode($value);
+        $parameters[] = array($name, $value);
+      }
+    }
+    
+    return $parameters;
+  }
+
+  /**
+   * Encode data to a multipart/form-data part suitable for a POST request.
+   *
+   * @param string $boundary
+   * @param string $name
+   * @param mixed $value
+   * @param string $filename
+   * @param array $headers Associative array of optional headers @example ("Content-transfer-encoding" => "binary")
+   * @return string
+   */
+  public static function encodeFormData($boundary, $name, $value, $filename = null, $headers = array()) {
+    $ret = "--{$boundary}\r\n" . 'Content-disposition: form-data; name="' . $name . '"';
+    
+    if ($filename) $ret .= '; filename="' . $filename . '"';
+    $ret .= "\r\n";
+    
+    foreach ($headers as $hname => $hvalue) {
+      $ret .= "{$hname}: {$hvalue}\r\n";
+    }
+    $ret .= "\r\n";
+    
+    $ret .= "{$value}\r\n";
+    
+    return $ret;
+  }
+
+  /**
+   * Create a HTTP authentication "Authorization:" header according to the
+   * specified user, password and authentication method.
+   *
+   * @see http://www.faqs.org/rfcs/rfc2617.html
+   * @param string $user
+   * @param string $password
+   * @param string $type
+   * @return string
+   */
+  public static function encodeAuthHeader($user, $password, $type = self::AUTH_BASIC) {
+    $authHeader = null;
+    
+    switch ($type) {
+      case self::AUTH_BASIC:
+        // In basic authentication, the user name cannot contain ":"
+        if (strpos($user, ':') !== false) {
+          require_once 'external/Zend/Http/Client/Exception.php';
+          throw new Zend_Http_Client_Exception("The user name cannot contain ':' in 'Basic' HTTP authentication");
+        }
+        
+        $authHeader = 'Basic ' . base64_encode($user . ':' . $password);
+        break;
+      
+      //case self::AUTH_DIGEST:
+      /**
+       * @todo Implement digest authentication
+       */
+      //    break;
+      
+
+      default:
+        require_once 'external/Zend/Http/Client/Exception.php';
+        throw new Zend_Http_Client_Exception("Not a supported HTTP authentication type: '$type'");
+    }
+    
+    return $authHeader;
+  }
+}
diff --git a/trunk/php/external/Zend/Http/Client/Adapter/Exception.php b/trunk/php/external/Zend/Http/Client/Adapter/Exception.php
new file mode 100644
index 0000000..15bbce0
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Client/Adapter/Exception.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter_Exception
+ * @version    $Id: Exception.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Http/Client/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Client_Adapter_Exception extends Zend_Http_Client_Exception {
+}
diff --git a/trunk/php/external/Zend/Http/Client/Adapter/Interface.php b/trunk/php/external/Zend/Http/Client/Adapter/Interface.php
new file mode 100644
index 0000000..3930e64
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Client/Adapter/Interface.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @version    $Id: Interface.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * An interface description for Zend_Http_Client_Adapter classes.
+ *
+ * These classes are used as connectors for Zend_Http_Client, performing the
+ * tasks of connecting, writing, reading and closing connection to the server.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Http_Client_Adapter_Interface {
+
+  /**
+   * Set the configuration array for the adapter
+   *
+   * @param array $config
+   */
+  public function setConfig($config = array());
+
+  /**
+   * Connect to the remote server
+   *
+   * @param string  $host
+   * @param int     $port
+   * @param boolean $secure
+   */
+  public function connect($host, $port = 80, $secure = false);
+
+  /**
+   * Send request to the remote server
+   *
+   * @param string        $method
+   * @param Zend_Uri_Http $url
+   * @param string        $http_ver
+   * @param array         $headers
+   * @param string        $body
+   * @return string Request as text
+   */
+  public function write($method, $url, $http_ver = '1.1', $headers = array(), $body = '');
+
+  /**
+   * Read response from server
+   *
+   * @return string
+   */
+  public function read();
+
+  /**
+   * Close the connection to the server
+   *
+   */
+  public function close();
+}
diff --git a/trunk/php/external/Zend/Http/Client/Adapter/Proxy.php b/trunk/php/external/Zend/Http/Client/Adapter/Proxy.php
new file mode 100644
index 0000000..863bd2b
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Client/Adapter/Proxy.php
@@ -0,0 +1,246 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @version    $Id: Proxy.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Uri/Http.php';
+require_once 'external/Zend/Http/Client.php';
+require_once 'external/Zend/Http/Client/Adapter/Socket.php';
+
+/**
+ * HTTP Proxy-supporting Zend_Http_Client adapter class, based on the default
+ * socket based adapter.
+ *
+ * Should be used if proxy HTTP access is required. If no proxy is set, will
+ * fall back to Zend_Http_Client_Adapter_Socket behavior. Just like the
+ * default Socket adapter, this adapter does not require any special extensions
+ * installed.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Client_Adapter_Proxy extends Zend_Http_Client_Adapter_Socket {
+  /**
+   * Parameters array
+   *
+   * @var array
+   */
+  protected $config = array('ssltransport' => 'ssl', 'proxy_host' => '', 'proxy_port' => 8080, 
+      'proxy_user' => '', 'proxy_pass' => '', 'proxy_auth' => Zend_Http_Client::AUTH_BASIC);
+  
+  /**
+   * Whether HTTPS CONNECT was already negotiated with the proxy or not
+   *
+   * @var boolean
+   */
+  protected $negotiated = false;
+
+  /**
+   * Connect to the remote server
+   *
+   * Will try to connect to the proxy server. If no proxy was set, will
+   * fall back to the target server (behave like regular Socket adapter)
+   *
+   * @param string  $host
+   * @param int     $port
+   * @param boolean $secure
+   * @param int     $timeout
+   */
+  public function connect($host, $port = 80, $secure = false) {
+    // If no proxy is set, fall back to Socket adapter
+    if (! $this->config['proxy_host']) return parent::connect($host, $port, $secure);
+    
+    // Go through a proxy - the connection is actually to the proxy server
+    $host = $this->config['proxy_host'];
+    $port = $this->config['proxy_port'];
+    
+    // If we are connected to the wrong proxy, disconnect first
+    if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) {
+      if (is_resource($this->socket)) $this->close();
+    }
+    
+    // Now, if we are not connected, connect
+    if (! is_resource($this->socket) || ! $this->config['keepalive']) {
+      $this->socket = @fsockopen($host, $port, $errno, $errstr, (int)$this->config['timeout']);
+      if (! $this->socket) {
+        $this->close();
+        require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+        throw new Zend_Http_Client_Adapter_Exception('Unable to Connect to proxy server ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr);
+      }
+      
+      // Set the stream timeout
+      if (! stream_set_timeout($this->socket, (int)$this->config['timeout'])) {
+        require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+        throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout');
+      }
+      
+      // Update connected_to
+      $this->connected_to = array($host, $port);
+    }
+  }
+
+  /**
+   * Send request to the proxy server
+   *
+   * @param string        $method
+   * @param Zend_Uri_Http $uri
+   * @param string        $http_ver
+   * @param array         $headers
+   * @param string        $body
+   * @return string Request as string
+   */
+  public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') {
+    // If no proxy is set, fall back to default Socket adapter
+    if (! $this->config['proxy_host']) return parent::write($method, $uri, $http_ver, $headers, $body);
+    
+    // Make sure we're properly connected
+    if (! $this->socket) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are not connected");
+    }
+    
+    $host = $this->config['proxy_host'];
+    $port = $this->config['proxy_port'];
+    
+    if ($this->connected_to[0] != $host || $this->connected_to[1] != $port) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception("Trying to write but we are connected to the wrong proxy server");
+    }
+    
+    // Add Proxy-Authorization header
+    if ($this->config['proxy_user'] && ! isset($headers['proxy-authorization'])) $headers['proxy-authorization'] = Zend_Http_Client::encodeAuthHeader($this->config['proxy_user'], $this->config['proxy_pass'], $this->config['proxy_auth']);
+    
+    // if we are proxying HTTPS, preform CONNECT handshake with the proxy
+    if ($uri->getScheme() == 'https' && (! $this->negotiated)) {
+      $this->connectHandshake($uri->getHost(), $uri->getPort(), $http_ver, $headers);
+      $this->negotiated = true;
+    }
+    
+    // Save request method for later
+    $this->method = $method;
+    
+    // Build request headers
+    $request = "{$method} {$uri->__toString()} HTTP/{$http_ver}\r\n";
+    
+    // Add all headers to the request string
+    foreach ($headers as $k => $v) {
+      if (is_string($k)) $v = "$k: $v";
+      $request .= "$v\r\n";
+    }
+    
+    // Add the request body
+    $request .= "\r\n" . $body;
+    
+    // Send the request
+    if (! @fwrite($this->socket, $request)) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception("Error writing request to proxy server");
+    }
+    
+    return $request;
+  }
+
+  /**
+   * Preform handshaking with HTTPS proxy using CONNECT method
+   *
+   * @param string  $host
+   * @param integer $port
+   * @param string  $http_ver
+   * @param array   $headers
+   */
+  protected function connectHandshake($host, $port = 443, $http_ver = '1.1', array &$headers = array()) {
+    $request = "CONNECT $host:$port HTTP/$http_ver\r\n" . "Host: " . $this->config['proxy_host'] . "\r\n";
+    
+    // Add the user-agent header
+    if (isset($this->config['useragent'])) {
+      $request .= "User-agent: " . $this->config['useragent'] . "\r\n";
+    }
+    
+    // If the proxy-authorization header is set, send it to proxy but remove
+    // it from headers sent to target host
+    if (isset($headers['proxy-authorization'])) {
+      $request .= "Proxy-authorization: " . $headers['proxy-authorization'] . "\r\n";
+      unset($headers['proxy-authorization']);
+    }
+    
+    $request .= "\r\n";
+    
+    // Send the request
+    if (! @fwrite($this->socket, $request)) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception("Error writing request to proxy server");
+    }
+    
+    // Read response headers only
+    $response = '';
+    $gotStatus = false;
+    while ($line = @fgets($this->socket)) {
+      $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
+      if ($gotStatus) {
+        $response .= $line;
+        if (! chop($line)) break;
+      }
+    }
+    
+    // Check that the response from the proxy is 200
+    if (Zend_Http_Response::extractCode($response) != 200) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception("Unable to connect to HTTPS proxy. Server response: " . $response);
+    }
+    
+    // If all is good, switch socket to secure mode. We have to fall back
+    // through the different modes 
+    $modes = array(STREAM_CRYPTO_METHOD_TLS_CLIENT, 
+        STREAM_CRYPTO_METHOD_SSLv3_CLIENT, STREAM_CRYPTO_METHOD_SSLv23_CLIENT, 
+        STREAM_CRYPTO_METHOD_SSLv2_CLIENT);
+    
+    $success = false;
+    foreach ($modes as $mode) {
+      $success = stream_socket_enable_crypto($this->socket, true, $mode);
+      if ($success) break;
+    }
+    
+    if (! $success) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception("Unable to connect to" . " HTTPS server through proxy: could not negotiate secure connection.");
+    }
+  }
+
+  /**
+   * Close the connection to the server
+   *
+   */
+  public function close() {
+    parent::close();
+    $this->negotiated = false;
+  }
+
+  /**
+   * Destructor: make sure the socket is disconnected
+   *
+   */
+  public function __destruct() {
+    if ($this->socket) $this->close();
+  }
+}
diff --git a/trunk/php/external/Zend/Http/Client/Adapter/Socket.php b/trunk/php/external/Zend/Http/Client/Adapter/Socket.php
new file mode 100644
index 0000000..181a5f7
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Client/Adapter/Socket.php
@@ -0,0 +1,294 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @version    $Id: Socket.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Uri/Http.php';
+require_once 'external/Zend/Http/Client/Adapter/Interface.php';
+
+/**
+ * A sockets based (stream_socket_client) adapter class for Zend_Http_Client. Can be used
+ * on almost every PHP environment, and does not require any special extensions.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface {
+  /**
+   * The socket for server connection
+   *
+   * @var resource|null
+   */
+  protected $socket = null;
+  
+  /**
+   * What host/port are we connected to?
+   *
+   * @var array
+   */
+  protected $connected_to = array(null, null);
+  
+  /**
+   * Parameters array
+   *
+   * @var array
+   */
+  protected $config = array('ssltransport' => 'ssl', 'sslcert' => null, 'sslpassphrase' => null);
+  
+  /**
+   * Request method - will be set by write() and might be used by read()
+   *
+   * @var string
+   */
+  protected $method = null;
+
+  /**
+   * Adapter constructor, currently empty. Config is set using setConfig()
+   *
+   */
+  public function __construct() {}
+
+  /**
+   * Set the configuration array for the adapter
+   *
+   * @param array $config
+   */
+  public function setConfig($config = array()) {
+    if (! is_array($config)) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception('$config expects an array, ' . gettype($config) . ' recieved.');
+    }
+    
+    foreach ($config as $k => $v) {
+      $this->config[strtolower($k)] = $v;
+    }
+  }
+
+  /**
+   * Connect to the remote server
+   *
+   * @param string  $host
+   * @param int     $port
+   * @param boolean $secure
+   * @param int     $timeout
+   */
+  public function connect($host, $port = 80, $secure = false) {
+    // If the URI should be accessed via SSL, prepend the Hostname with ssl://
+    $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
+    
+    // If we are connected to the wrong host, disconnect first
+    if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) {
+      if (is_resource($this->socket)) $this->close();
+    }
+    
+    // Now, if we are not connected, connect
+    if (! is_resource($this->socket) || ! $this->config['keepalive']) {
+      $context = stream_context_create();
+      if ($secure) {
+        if ($this->config['sslcert'] !== null) {
+          if (! stream_context_set_option($context, 'ssl', 'local_cert', $this->config['sslcert'])) {
+            require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+            throw new Zend_Http_Client_Adapter_Exception('Unable to set sslcert option');
+          }
+        }
+        if ($this->config['sslpassphrase'] !== null) {
+          if (! stream_context_set_option($context, 'ssl', 'passphrase', $this->config['sslpassphrase'])) {
+            require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+            throw new Zend_Http_Client_Adapter_Exception('Unable to set sslpassphrase option');
+          }
+        }
+      }
+      
+      $this->socket = @stream_socket_client($host . ':' . $port, $errno, $errstr, (int)$this->config['timeout'], STREAM_CLIENT_CONNECT, $context);
+      if (! $this->socket) {
+        $this->close();
+        require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+        throw new Zend_Http_Client_Adapter_Exception('Unable to Connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr);
+      }
+      
+      // Set the stream timeout
+      if (! stream_set_timeout($this->socket, (int)$this->config['timeout'])) {
+        require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+        throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout');
+      }
+      
+      // Update connected_to
+      $this->connected_to = array($host, $port);
+    }
+  }
+
+  /**
+   * Send request to the remote server
+   *
+   * @param string        $method
+   * @param Zend_Uri_Http $uri
+   * @param string        $http_ver
+   * @param array         $headers
+   * @param string        $body
+   * @return string Request as string
+   */
+  public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') {
+    // Make sure we're properly connected
+    if (! $this->socket) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected');
+    }
+    
+    $host = $uri->getHost();
+    $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
+    if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host');
+    }
+    
+    // Save request method for later
+    $this->method = $method;
+    
+    // Build request headers
+    $path = $uri->getPath();
+    if ($uri->getQuery()) $path .= '?' . $uri->getQuery();
+    $request = "{$method} {$path} HTTP/{$http_ver}\r\n";
+    foreach ($headers as $k => $v) {
+      if (is_string($k)) $v = ucfirst($k) . ": $v";
+      $request .= "$v\r\n";
+    }
+    
+    // Add the request body
+    $request .= "\r\n" . $body;
+    
+    // Send the request
+    if (! @fwrite($this->socket, $request)) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
+    }
+    
+    return $request;
+  }
+
+  /**
+   * Read response from server
+   *
+   * @return string
+   */
+  public function read() {
+    // First, read headers only
+    $response = '';
+    $gotStatus = false;
+    while ($line = @fgets($this->socket)) {
+      $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
+      if ($gotStatus) {
+        $response .= $line;
+        if (! chop($line)) break;
+      }
+    }
+    
+    // Handle 100 and 101 responses internally by restarting the read again
+    if (Zend_Http_Response::extractCode($response) == 100 || Zend_Http_Response::extractCode($response) == 101) return $this->read();
+    
+    // If this was a HEAD request, return after reading the header (no need to read body)
+    if ($this->method == Zend_Http_Client::HEAD) return $response;
+    
+    // Check headers to see what kind of connection / transfer encoding we have
+    $headers = Zend_Http_Response::extractHeaders($response);
+    
+    // if the connection is set to close, just read until socket closes
+    if (isset($headers['connection']) && $headers['connection'] == 'close') {
+      while ($buff = @fread($this->socket, 8192)) {
+        $response .= $buff;
+      }
+      
+      $this->close();
+      
+    // Else, if we got a transfer-encoding header (chunked body)
+    } elseif (isset($headers['transfer-encoding'])) {
+      if ($headers['transfer-encoding'] == 'chunked') {
+        do {
+          $chunk = '';
+          $line = @fgets($this->socket);
+          $chunk .= $line;
+          
+          $hexchunksize = ltrim(chop($line), '0');
+          $hexchunksize = strlen($hexchunksize) ? strtolower($hexchunksize) : 0;
+          
+          $chunksize = hexdec(chop($line));
+          if (dechex($chunksize) != $hexchunksize) {
+            @fclose($this->socket);
+            require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+            throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' . $hexchunksize . '" unable to read chunked body');
+          }
+          
+          $left_to_read = $chunksize;
+          while ($left_to_read > 0) {
+            $line = @fread($this->socket, $left_to_read);
+            $chunk .= $line;
+            $left_to_read -= strlen($line);
+          }
+          
+          $chunk .= @fgets($this->socket);
+          $response .= $chunk;
+        } while ($chunksize > 0);
+      } else {
+        throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' . $headers['transfer-encoding'] . '" transfer encoding');
+      }
+      
+    // Else, if we got the content-length header, read this number of bytes
+    } elseif (isset($headers['content-length'])) {
+      $left_to_read = $headers['content-length'];
+      $chunk = '';
+      while ($left_to_read > 0) {
+        $chunk = @fread($this->socket, $left_to_read);
+        $left_to_read -= strlen($chunk);
+        $response .= $chunk;
+      }
+      
+    // Fallback: just read the response (should not happen)
+    } else {
+      while ($buff = @fread($this->socket, 8192)) {
+        $response .= $buff;
+      }
+      
+      $this->close();
+    }
+    
+    return $response;
+  }
+
+  /**
+   * Close the connection to the server
+   *
+   */
+  public function close() {
+    if (is_resource($this->socket)) @fclose($this->socket);
+    $this->socket = null;
+    $this->connected_to = array(null, null);
+  }
+
+  /**
+   * Destructor: make sure the socket is disconnected
+   *
+   */
+  public function __destruct() {
+    if ($this->socket) $this->close();
+  }
+}
diff --git a/trunk/php/external/Zend/Http/Client/Adapter/Test.php b/trunk/php/external/Zend/Http/Client/Adapter/Test.php
new file mode 100644
index 0000000..bbe5bbb
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Client/Adapter/Test.php
@@ -0,0 +1,182 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @version    $Id: Test.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Uri/Http.php';
+require_once 'external/Zend/Http/Response.php';
+require_once 'external/Zend/Http/Client/Adapter/Interface.php';
+
+/**
+ * A testing-purposes adapter.
+ *
+ * Should be used to test all components that rely on Zend_Http_Client,
+ * without actually performing an HTTP request. You should instantiate this
+ * object manually, and then set it as the client's adapter. Then, you can
+ * set the expected response using the setResponse() method.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Adapter
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Client_Adapter_Test implements Zend_Http_Client_Adapter_Interface {
+  /**
+   * Parameters array
+   *
+   * @var array
+   */
+  protected $config = array();
+  
+  /**
+   * Buffer of responses to be returned by the read() method.  Can be
+   * set using setResponse() and addResponse().
+   *
+   * @var array
+   */
+  protected $responses = array("HTTP/1.1 400 Bad Request\r\n\r\n");
+  
+  /**
+   * Current position in the response buffer
+   *
+   * @var integer
+   */
+  protected $responseIndex = 0;
+
+  /**
+   * Adapter constructor, currently empty. Config is set using setConfig()
+   *
+   */
+  public function __construct() {}
+
+  /**
+   * Set the configuration array for the adapter
+   *
+   * @param array $config
+   */
+  public function setConfig($config = array()) {
+    if (! is_array($config)) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception('$config expects an array, ' . gettype($config) . ' recieved.');
+    }
+    
+    foreach ($config as $k => $v) {
+      $this->config[strtolower($k)] = $v;
+    }
+  }
+
+  /**
+   * Connect to the remote server
+   *
+   * @param string  $host
+   * @param int     $port
+   * @param boolean $secure
+   * @param int     $timeout
+   */
+  public function connect($host, $port = 80, $secure = false) {}
+
+  /**
+   * Send request to the remote server
+   *
+   * @param string        $method
+   * @param Zend_Uri_Http $uri
+   * @param string        $http_ver
+   * @param array         $headers
+   * @param string        $body
+   * @return string Request as string
+   */
+  public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') {
+    $host = $uri->getHost();
+    $host = (strtolower($uri->getScheme()) == 'https' ? 'sslv2://' . $host : $host);
+    
+    // Build request headers
+    $path = $uri->getPath();
+    if ($uri->getQuery()) $path .= '?' . $uri->getQuery();
+    $request = "{$method} {$path} HTTP/{$http_ver}\r\n";
+    foreach ($headers as $k => $v) {
+      if (is_string($k)) $v = ucfirst($k) . ": $v";
+      $request .= "$v\r\n";
+    }
+    
+    // Add the request body
+    $request .= "\r\n" . $body;
+    
+    // Do nothing - just return the request as string
+    
+
+    return $request;
+  }
+
+  /**
+   * Return the response set in $this->setResponse()
+   *
+   * @return string
+   */
+  public function read() {
+    if ($this->responseIndex >= count($this->responses)) {
+      $this->responseIndex = 0;
+    }
+    return $this->responses[$this->responseIndex ++];
+  }
+
+  /**
+   * Close the connection (dummy)
+   *
+   */
+  public function close() {}
+
+  /**
+   * Set the HTTP response(s) to be returned by this adapter
+   *
+   * @param Zend_Http_Response|array|string $response
+   */
+  public function setResponse($response) {
+    if ($response instanceof Zend_Http_Response) {
+      $response = $response->asString();
+    }
+    
+    $this->responses = (array)$response;
+    $this->responseIndex = 0;
+  }
+
+  /**
+   * Add another response to the response buffer.
+   *
+   * @param string $response
+   */
+  public function addResponse($response) {
+    $this->responses[] = $response;
+  }
+
+  /**
+   * Sets the position of the response buffer.  Selects which
+   * response will be returned on the next call to read().
+   *
+   * @param integer $index
+   */
+  public function setResponseIndex($index) {
+    if ($index < 0 || $index >= count($this->responses)) {
+      require_once 'external/Zend/Http/Client/Adapter/Exception.php';
+      throw new Zend_Http_Client_Adapter_Exception('Index out of range of response buffer size');
+    }
+    $this->responseIndex = $index;
+  }
+}
diff --git a/trunk/php/external/Zend/Http/Client/Exception.php b/trunk/php/external/Zend/Http/Client/Exception.php
new file mode 100644
index 0000000..3cfe24b
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Client/Exception.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client_Exception
+ * @version    $Id: Exception.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Http/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Client_Exception extends Zend_Http_Exception {
+}
diff --git a/trunk/php/external/Zend/Http/Cookie.php b/trunk/php/external/Zend/Http/Cookie.php
new file mode 100644
index 0000000..411e2c8
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Cookie.php
@@ -0,0 +1,314 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to version 1.0 of the Zend Framework
+ * license, that is bundled with this package in the file LICENSE.txt,
+ * and is available through the world-wide-web at the following URL:
+ * http://framework.zend.com/license/new-bsd. If you did not
+ * receive a copy of the Zend Framework license and are unable to
+ * obtain it through the world-wide-web, please send a note to
+ * license@zend.com so we can mail you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Cookie
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com/)
+ * @version    $Id: Cookie.php 8064 2008-02-16 10:58:39Z thomas $
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Uri/Http.php';
+
+/**
+ * Zend_Http_Cookie is a class describing an HTTP cookie and all it's parameters.
+ *
+ * Zend_Http_Cookie is a class describing an HTTP cookie and all it's parameters. The
+ * class also enables validating whether the cookie should be sent to the server in
+ * a specified scenario according to the request URI, the expiry time and whether
+ * session cookies should be used or not. Generally speaking cookies should be
+ * contained in a Cookiejar object, or instantiated manually and added to an HTTP
+ * request.
+ *
+ * See http://wp.netscape.com/newsref/std/cookie_spec.html for some specs.
+ *
+ * @category    Zend
+ * @package     Zend_Http
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com/)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Cookie {
+  /**
+   * Cookie name
+   *
+   * @var string
+   */
+  protected $name;
+  
+  /**
+   * Cookie value
+   *
+   * @var string
+   */
+  protected $value;
+  
+  /**
+   * Cookie expiry date
+   *
+   * @var int
+   */
+  protected $expires;
+  
+  /**
+   * Cookie domain
+   *
+   * @var string
+   */
+  protected $domain;
+  
+  /**
+   * Cookie path
+   *
+   * @var string
+   */
+  protected $path;
+  
+  /**
+   * Whether the cookie is secure or not
+   *
+   * @var boolean
+   */
+  protected $secure;
+
+  /**
+   * Cookie object constructor
+   *
+   * @todo Add validation of each one of the parameters (legal domain, etc.)
+   *
+   * @param string $name
+   * @param string $value
+   * @param int $expires
+   * @param string $domain
+   * @param string $path
+   * @param bool $secure
+   */
+  public function __construct($name, $value, $domain, $expires = null, $path = null, $secure = false) {
+    if (preg_match("/[=,; \t\r\n\013\014]/", $name)) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception("Cookie name cannot contain these characters: =,; \\t\\r\\n\\013\\014 ({$name})");
+    }
+    
+    if (! $this->name = (string)$name) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('Cookies must have a name');
+    }
+    
+    if (! $this->domain = (string)$domain) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('Cookies must have a domain');
+    }
+    
+    $this->value = (string)$value;
+    $this->expires = ($expires === null ? null : (int)$expires);
+    $this->path = ($path ? $path : '/');
+    $this->secure = $secure;
+  }
+
+  /**
+   * Get Cookie name
+   *
+   * @return string
+   */
+  public function getName() {
+    return $this->name;
+  }
+
+  /**
+   * Get cookie value
+   *
+   * @return string
+   */
+  public function getValue() {
+    return $this->value;
+  }
+
+  /**
+   * Get cookie domain
+   *
+   * @return string
+   */
+  public function getDomain() {
+    return $this->domain;
+  }
+
+  /**
+   * Get the cookie path
+   *
+   * @return string
+   */
+  public function getPath() {
+    return $this->path;
+  }
+
+  /**
+   * Get the expiry time of the cookie, or null if no expiry time is set
+   *
+   * @return int|null
+   */
+  public function getExpiryTime() {
+    return $this->expires;
+  }
+
+  /**
+   * Check whether the cookie should only be sent over secure connections
+   *
+   * @return boolean
+   */
+  public function isSecure() {
+    return $this->secure;
+  }
+
+  /**
+   * Check whether the cookie has expired
+   *
+   * Always returns false if the cookie is a session cookie (has no expiry time)
+   *
+   * @param int $now Timestamp to consider as "now"
+   * @return boolean
+   */
+  public function isExpired($now = null) {
+    if ($now === null) $now = time();
+    if (is_int($this->expires) && $this->expires < $now) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Check whether the cookie is a session cookie (has no expiry time set)
+   *
+   * @return boolean
+   */
+  public function isSessionCookie() {
+    return ($this->expires === null);
+  }
+
+  /**
+   * Checks whether the cookie should be sent or not in a specific scenario
+   *
+   * @param string|Zend_Uri_Http $uri URI to check against (secure, domain, path)
+   * @param boolean $matchSessionCookies Whether to send session cookies
+   * @param int $now Override the current time when checking for expiry time
+   * @return boolean
+   */
+  public function match($uri, $matchSessionCookies = true, $now = null) {
+    if (is_string($uri)) {
+      $uri = Zend_Uri_Http::factory($uri);
+    }
+    
+    // Make sure we have a valid Zend_Uri_Http object
+    if (! ($uri->valid() && ($uri->getScheme() == 'http' || $uri->getScheme() == 'https'))) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('Passed URI is not a valid HTTP or HTTPS URI');
+    }
+    
+    // Check that the cookie is secure (if required) and not expired
+    if ($this->secure && $uri->getScheme() != 'https') return false;
+    if ($this->isExpired($now)) return false;
+    if ($this->isSessionCookie() && ! $matchSessionCookies) return false;
+    
+    // Validate domain and path
+    // Domain is validated using tail match, while path is validated using head match
+    $domain_preg = preg_quote($this->getDomain(), "/");
+    if (! preg_match("/{$domain_preg}$/", $uri->getHost())) return false;
+    $path_preg = preg_quote($this->getPath(), "/");
+    if (! preg_match("/^{$path_preg}/", $uri->getPath())) return false;
+    
+    // If we didn't die until now, return true.
+    return true;
+  }
+
+  /**
+   * Get the cookie as a string, suitable for sending as a "Cookie" header in an
+   * HTTP request
+   *
+   * @return string
+   */
+  public function __toString() {
+    return $this->name . '=' . urlencode($this->value) . ';';
+  }
+
+  /**
+   * Generate a new Cookie object from a cookie string
+   * (for example the value of the Set-Cookie HTTP header)
+   *
+   * @param string $cookieStr
+   * @param Zend_Uri_Http|string $ref_uri Reference URI for default values (domain, path)
+   * @return Zend_Http_Cookie A new Zend_Http_Cookie object or false on failure.
+   */
+  public static function fromString($cookieStr, $ref_uri = null) {
+    // Set default values
+    if (is_string($ref_uri)) {
+      $ref_uri = Zend_Uri_Http::factory($ref_uri);
+    }
+    
+    $name = '';
+    $value = '';
+    $domain = '';
+    $path = '';
+    $expires = null;
+    $secure = false;
+    $parts = explode(';', $cookieStr);
+    
+    // If first part does not include '=', fail
+    if (strpos($parts[0], '=') === false) return false;
+    
+    // Get the name and value of the cookie
+    list($name, $value) = explode('=', trim(array_shift($parts)), 2);
+    $name = trim($name);
+    $value = urldecode(trim($value));
+    
+    // Set default domain and path
+    if ($ref_uri instanceof Zend_Uri_Http) {
+      $domain = $ref_uri->getHost();
+      $path = $ref_uri->getPath();
+      $path = substr($path, 0, strrpos($path, '/'));
+    }
+    
+    // Set other cookie parameters
+    foreach ($parts as $part) {
+      $part = trim($part);
+      if (strtolower($part) == 'secure') {
+        $secure = true;
+        continue;
+      }
+      
+      $keyValue = explode('=', $part, 2);
+      if (count($keyValue) == 2) {
+        list($k, $v) = $keyValue;
+        switch (strtolower($k)) {
+          case 'expires':
+            $expires = strtotime($v);
+            break;
+          case 'path':
+            $path = $v;
+            break;
+          case 'domain':
+            $domain = $v;
+            break;
+          default:
+            break;
+        }
+      }
+    }
+    
+    if ($name !== '') {
+      return new Zend_Http_Cookie($name, $value, $domain, $expires, $path, $secure);
+    } else {
+      return false;
+    }
+  }
+}
diff --git a/trunk/php/external/Zend/Http/CookieJar.php b/trunk/php/external/Zend/Http/CookieJar.php
new file mode 100644
index 0000000..c8dac96
--- /dev/null
+++ b/trunk/php/external/Zend/Http/CookieJar.php
@@ -0,0 +1,339 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to version 1.0 of the Zend Framework
+ * license, that is bundled with this package in the file LICENSE.txt,
+ * and is available through the world-wide-web at the following URL:
+ * http://framework.zend.com/license/new-bsd. If you did not
+ * receive a copy of the Zend Framework license and are unable to
+ * obtain it through the world-wide-web, please send a note to
+ * license@zend.com so we can mail you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage CookieJar
+ * @version    $Id: CookieJar.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com/)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once "Zend/Uri.php";
+require_once "Zend/Http/Cookie.php";
+require_once "Zend/Http/Response.php";
+
+/**
+ * A Zend_Http_CookieJar object is designed to contain and maintain HTTP cookies, and should
+ * be used along with Zend_Http_Client in order to manage cookies across HTTP requests and
+ * responses.
+ *
+ * The class contains an array of Zend_Http_Cookie objects. Cookies can be added to the jar
+ * automatically from a request or manually. Then, the jar can find and return the cookies
+ * needed for a specific HTTP request.
+ *
+ * A special parameter can be passed to all methods of this class that return cookies: Cookies
+ * can be returned either in their native form (as Zend_Http_Cookie objects) or as strings -
+ * the later is suitable for sending as the value of the "Cookie" header in an HTTP request.
+ * You can also choose, when returning more than one cookie, whether to get an array of strings
+ * (by passing Zend_Http_CookieJar::COOKIE_STRING_ARRAY) or one unified string for all cookies
+ * (by passing Zend_Http_CookieJar::COOKIE_STRING_CONCAT).
+ *
+ * @link       http://wp.netscape.com/newsref/std/cookie_spec.html for some specs.
+ * 
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage CookieJar
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com/)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_CookieJar {
+  /**
+   * Return cookie(s) as a Zend_Http_Cookie object
+   *
+   */
+  const COOKIE_OBJECT = 0;
+  
+  /**
+   * Return cookie(s) as a string (suitable for sending in an HTTP request)
+   *
+   */
+  const COOKIE_STRING_ARRAY = 1;
+  
+  /**
+   * Return all cookies as one long string (suitable for sending in an HTTP request)
+   *
+   */
+  const COOKIE_STRING_CONCAT = 2;
+  
+  /**
+   * Array storing cookies
+   *
+   * Cookies are stored according to domain and path:
+   * $cookies
+   *  + www.mydomain.com
+   *    + /
+   *      - cookie1
+   *      - cookie2
+   *    + /somepath
+   *      - othercookie
+   *  + www.otherdomain.net
+   *    + /
+   *      - alsocookie
+   *
+   * @var array
+   */
+  protected $cookies = array();
+
+  /**
+   * Construct a new CookieJar object
+   *
+   */
+  public function __construct() {}
+
+  /**
+   * Add a cookie to the jar. Cookie should be passed either as a Zend_Http_Cookie object
+   * or as a string - in which case an object is created from the string.
+   *
+   * @param Zend_Http_Cookie|string $cookie
+   * @param Zend_Uri_Http|string    $ref_uri Optional reference URI (for domain, path, secure)
+   */
+  public function addCookie($cookie, $ref_uri = null) {
+    if (is_string($cookie)) {
+      $cookie = Zend_Http_Cookie::fromString($cookie, $ref_uri);
+    }
+    
+    if ($cookie instanceof Zend_Http_Cookie) {
+      $domain = $cookie->getDomain();
+      $path = $cookie->getPath();
+      if (! isset($this->cookies[$domain])) $this->cookies[$domain] = array();
+      if (! isset($this->cookies[$domain][$path])) $this->cookies[$domain][$path] = array();
+      $this->cookies[$domain][$path][$cookie->getName()] = $cookie;
+    } else {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('Supplient argument is not a valid cookie string or object');
+    }
+  }
+
+  /**
+   * Parse an HTTP response, adding all the cookies set in that response
+   * to the cookie jar.
+   *
+   * @param Zend_Http_Response $response
+   * @param Zend_Uri_Http|string $ref_uri Requested URI
+   */
+  public function addCookiesFromResponse($response, $ref_uri) {
+    if (! $response instanceof Zend_Http_Response) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('$response is expected to be a Response object, ' . gettype($response) . ' was passed');
+    }
+    
+    $cookie_hdrs = $response->getHeader('Set-Cookie');
+    
+    if (is_array($cookie_hdrs)) {
+      foreach ($cookie_hdrs as $cookie) {
+        $this->addCookie($cookie, $ref_uri);
+      }
+    } elseif (is_string($cookie_hdrs)) {
+      $this->addCookie($cookie_hdrs, $ref_uri);
+    }
+  }
+
+  /**
+   * Get all cookies in the cookie jar as an array
+   *
+   * @param int $ret_as Whether to return cookies as objects of Zend_Http_Cookie or as strings
+   * @return array|string
+   */
+  public function getAllCookies($ret_as = self::COOKIE_OBJECT) {
+    $cookies = $this->_flattenCookiesArray($this->cookies, $ret_as);
+    return $cookies;
+  }
+
+  /**
+   * Return an array of all cookies matching a specific request according to the request URI,
+   * whether session cookies should be sent or not, and the time to consider as "now" when
+   * checking cookie expiry time.
+   *
+   * @param string|Zend_Uri_Http $uri URI to check against (secure, domain, path)
+   * @param boolean $matchSessionCookies Whether to send session cookies
+   * @param int $ret_as Whether to return cookies as objects of Zend_Http_Cookie or as strings
+   * @param int $now Override the current time when checking for expiry time
+   * @return array|string
+   */
+  public function getMatchingCookies($uri, $matchSessionCookies = true, $ret_as = self::COOKIE_OBJECT, $now = null) {
+    if (is_string($uri)) $uri = Zend_Uri::factory($uri);
+    if (! $uri instanceof Zend_Uri_Http) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception("Invalid URI string or object passed");
+    }
+    
+    // Set path
+    $path = $uri->getPath();
+    $path = substr($path, 0, strrpos($path, '/'));
+    if (! $path) $path = '/';
+    
+    // First, reduce the array of cookies to only those matching domain and path
+    $cookies = $this->_matchDomain($uri->getHost());
+    $cookies = $this->_matchPath($cookies, $path);
+    $cookies = $this->_flattenCookiesArray($cookies, self::COOKIE_OBJECT);
+    
+    // Next, run Cookie->match on all cookies to check secure, time and session mathcing
+    $ret = array();
+    foreach ($cookies as $cookie)
+      if ($cookie->match($uri, $matchSessionCookies, $now)) $ret[] = $cookie;
+      
+    // Now, use self::_flattenCookiesArray again - only to convert to the return format ;)
+    $ret = $this->_flattenCookiesArray($ret, $ret_as);
+    
+    return $ret;
+  }
+
+  /**
+   * Get a specific cookie according to a URI and name
+   *
+   * @param Zend_Uri_Http|string $uri The uri (domain and path) to match
+   * @param string $cookie_name The cookie's name
+   * @param int $ret_as Whether to return cookies as objects of Zend_Http_Cookie or as strings
+   * @return Zend_Http_Cookie|string
+   */
+  public function getCookie($uri, $cookie_name, $ret_as = self::COOKIE_OBJECT) {
+    if (is_string($uri)) {
+      $uri = Zend_Uri::factory($uri);
+    }
+    
+    if (! $uri instanceof Zend_Uri_Http) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('Invalid URI specified');
+    }
+    
+    // Get correct cookie path
+    $path = $uri->getPath();
+    $path = substr($path, 0, strrpos($path, '/'));
+    if (! $path) $path = '/';
+    
+    if (isset($this->cookies[$uri->getHost()][$path][$cookie_name])) {
+      $cookie = $this->cookies[$uri->getHost()][$path][$cookie_name];
+      
+      switch ($ret_as) {
+        case self::COOKIE_OBJECT:
+          return $cookie;
+          break;
+        
+        case self::COOKIE_STRING_ARRAY:
+        case self::COOKIE_STRING_CONCAT:
+          return $cookie->__toString();
+          break;
+        
+        default:
+          require_once 'external/Zend/Http/Exception.php';
+          throw new Zend_Http_Exception("Invalid value passed for \$ret_as: {$ret_as}");
+          break;
+      }
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Helper function to recursivly flatten an array. Shoud be used when exporting the
+   * cookies array (or parts of it)
+   *
+   * @param Zend_Http_Cookie|array $ptr
+   * @param int $ret_as What value to return
+   * @return array|string
+   */
+  protected function _flattenCookiesArray($ptr, $ret_as = self::COOKIE_OBJECT) {
+    if (is_array($ptr)) {
+      $ret = ($ret_as == self::COOKIE_STRING_CONCAT ? '' : array());
+      foreach ($ptr as $item) {
+        if ($ret_as == self::COOKIE_STRING_CONCAT) {
+          $ret .= $this->_flattenCookiesArray($item, $ret_as);
+        } else {
+          $ret = array_merge($ret, $this->_flattenCookiesArray($item, $ret_as));
+        }
+      }
+      return $ret;
+    } elseif ($ptr instanceof Zend_Http_Cookie) {
+      switch ($ret_as) {
+        case self::COOKIE_STRING_ARRAY:
+          return array($ptr->__toString());
+          break;
+        
+        case self::COOKIE_STRING_CONCAT:
+          return $ptr->__toString();
+          break;
+        
+        case self::COOKIE_OBJECT:
+        default:
+          return array($ptr);
+          break;
+      }
+    }
+    
+    return null;
+  }
+
+  /**
+   * Return a subset of the cookies array matching a specific domain
+   *
+   * Returned array is actually an array of pointers to items in the $this->cookies array.
+   *
+   * @param string $domain
+   * @return array
+   */
+  protected function _matchDomain($domain) {
+    $ret = array();
+    
+    foreach (array_keys($this->cookies) as $cdom) {
+      $regex = "/" . preg_quote($cdom, "/") . "$/i";
+      if (preg_match($regex, $domain)) $ret[$cdom] = &$this->cookies[$cdom];
+    }
+    
+    return $ret;
+  }
+
+  /**
+   * Return a subset of a domain-matching cookies that also match a specified path
+   *
+   * Returned array is actually an array of pointers to items in the $passed array.
+   *
+   * @param array $dom_array
+   * @param string $path
+   * @return array
+   */
+  protected function _matchPath($domains, $path) {
+    $ret = array();
+    if (substr($path, - 1) != '/') $path .= '/';
+    
+    foreach ($domains as $dom => $paths_array) {
+      foreach (array_keys($paths_array) as $cpath) {
+        $regex = "|^" . preg_quote($cpath, "|") . "|i";
+        if (preg_match($regex, $path)) {
+          if (! isset($ret[$dom])) $ret[$dom] = array();
+          $ret[$dom][$cpath] = &$paths_array[$cpath];
+        }
+      }
+    }
+    
+    return $ret;
+  }
+
+  /**
+   * Create a new CookieJar object and automatically load into it all the
+   * cookies set in an Http_Response object. If $uri is set, it will be
+   * considered as the requested URI for setting default domain and path
+   * of the cookie.
+   *
+   * @param Zend_Http_Response $response HTTP Response object
+   * @param Zend_Uri_Http|string $uri The requested URI
+   * @return Zend_Http_CookieJar
+   * @todo Add the $uri functionality.
+   */
+  public static function fromResponse(Zend_Http_Response $response, $ref_uri) {
+    $jar = new self();
+    $jar->addCookiesFromResponse($response, $ref_uri);
+    return $jar;
+  }
+}
diff --git a/trunk/php/external/Zend/Http/Exception.php b/trunk/php/external/Zend/Http/Exception.php
new file mode 100644
index 0000000..6b44004
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Exception.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Exception
+ * @version    $Id: Exception.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+require_once 'external/Zend/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Client
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Exception extends Zend_Exception {
+}
diff --git a/trunk/php/external/Zend/Http/Response.php b/trunk/php/external/Zend/Http/Response.php
new file mode 100644
index 0000000..589b941
--- /dev/null
+++ b/trunk/php/external/Zend/Http/Response.php
@@ -0,0 +1,571 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Http
+ * @subpackage Response
+ * @version    $Id: Response.php 8064 2008-02-16 10:58:39Z thomas $
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Zend_Http_Response represents an HTTP 1.0 / 1.1 response message. It
+ * includes easy access to all the response's different elemts, as well as some
+ * convenience methods for parsing and validating HTTP responses.
+ *
+ * @package    Zend_Http
+ * @subpackage Response
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Http_Response {
+  /**
+   * List of all known HTTP response codes - used by responseCodeAsText() to
+   * translate numeric codes to messages.
+   *
+   * @var array
+   */
+  protected static $messages = array(// Informational 1xx
+  100 => 'Continue', 101 => 'Switching Protocols', 
+
+  // Success 2xx
+  200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 
+      204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 
+      
+      // Redirection 3xx
+      300 => 'Multiple Choices', 301 => 'Moved Permanently', 
+      302 => 'Found', // 1.1
+303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 
+      // 306 is deprecated but reserved
+      307 => 'Temporary Redirect', 
+
+      // Client Error 4xx
+      400 => 'Bad Request', 401 => 'Unauthorized', 
+      402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 
+      405 => 'Method Not Allowed', 406 => 'Not Acceptable', 
+      407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 
+      410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 
+      413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 
+      415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 
+      417 => 'Expectation Failed', 
+
+      // Server Error 5xx
+      500 => 'Internal Server Error', 501 => 'Not Implemented', 
+      502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 
+      505 => 'HTTP Version Not Supported', 509 => 'Bandwidth Limit Exceeded');
+  
+  /**
+   * The HTTP version (1.0, 1.1)
+   *
+   * @var string
+   */
+  protected $version;
+  
+  /**
+   * The HTTP response code
+   *
+   * @var int
+   */
+  protected $code;
+  
+  /**
+   * The HTTP response code as string
+   * (e.g. 'Not Found' for 404 or 'Internal Server Error' for 500)
+   *
+   * @var string
+   */
+  protected $message;
+  
+  /**
+   * The HTTP response headers array
+   *
+   * @var array
+   */
+  protected $headers = array();
+  
+  /**
+   * The HTTP response body
+   *
+   * @var string
+   */
+  protected $body;
+
+  /**
+   * HTTP response constructor
+   *
+   * In most cases, you would use Zend_Http_Response::fromString to parse an HTTP
+   * response string and create a new Zend_Http_Response object.
+   *
+   * NOTE: The constructor no longer accepts nulls or empty values for the code and
+   * headers and will throw an exception if the passed values do not form a valid HTTP
+   * responses.
+   *
+   * If no message is passed, the message will be guessed according to the response code.
+   *
+   * @param int $code Response code (200, 404, ...)
+   * @param array $headers Headers array
+   * @param string $body Response body
+   * @param string $version HTTP version
+   * @param string $message Response code as text
+   * @throws Zend_Http_Exception
+   */
+  public function __construct($code, $headers, $body = null, $version = '1.1', $message = null) {
+    // Make sure the response code is valid and set it
+    if (self::responseCodeAsText($code) === null) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception("{$code} is not a valid HTTP response code");
+    }
+    
+    $this->code = $code;
+    
+    // Make sure we got valid headers and set them
+    if (! is_array($headers)) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('No valid headers were passed');
+    }
+    
+    foreach ($headers as $name => $value) {
+      if (is_int($name)) list($name, $value) = explode(": ", $value, 1);
+      
+      $this->headers[ucwords(strtolower($name))] = $value;
+    }
+    
+    // Set the body
+    $this->body = $body;
+    
+    // Set the HTTP version
+    if (! preg_match('|^\d\.\d$|', $version)) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception("Invalid HTTP response version: $version");
+    }
+    
+    $this->version = $version;
+    
+    // If we got the response message, set it. Else, set it according to
+    // the response code
+    if (is_string($message)) {
+      $this->message = $message;
+    } else {
+      $this->message = self::responseCodeAsText($code);
+    }
+  }
+
+  /**
+   * Check whether the response is an error
+   *
+   * @return boolean
+   */
+  public function isError() {
+    $restype = floor($this->code / 100);
+    if ($restype == 4 || $restype == 5) {
+      return true;
+    }
+    
+    return false;
+  }
+
+  /**
+   * Check whether the response in successful
+   *
+   * @return boolean
+   */
+  public function isSuccessful() {
+    $restype = floor($this->code / 100);
+    if ($restype == 2 || $restype == 1) { // Shouldn't 3xx count as success as well ???
+      return true;
+    }
+    
+    return false;
+  }
+
+  /**
+   * Check whether the response is a redirection
+   *
+   * @return boolean
+   */
+  public function isRedirect() {
+    $restype = floor($this->code / 100);
+    if ($restype == 3) {
+      return true;
+    }
+    
+    return false;
+  }
+
+  /**
+   * Get the response body as string
+   *
+   * This method returns the body of the HTTP response (the content), as it
+   * should be in it's readable version - that is, after decoding it (if it
+   * was decoded), deflating it (if it was gzip compressed), etc.
+   *
+   * If you want to get the raw body (as transfered on wire) use
+   * $this->getRawBody() instead.
+   *
+   * @return string
+   */
+  public function getBody() {
+    $body = '';
+    
+    // Decode the body if it was transfer-encoded
+    switch ($this->getHeader('transfer-encoding')) {
+      
+      // Handle chunked body
+      case 'chunked':
+        $body = self::decodeChunkedBody($this->body);
+        break;
+      
+      // No transfer encoding, or unknown encoding extension:
+      // return body as is
+      default:
+        $body = $this->body;
+        break;
+    }
+    
+    // Decode any content-encoding (gzip or deflate) if needed
+    switch (strtolower($this->getHeader('content-encoding'))) {
+      
+      // Handle gzip encoding
+      case 'gzip':
+        $body = self::decodeGzip($body);
+        break;
+      
+      // Handle deflate encoding
+      case 'deflate':
+        $body = self::decodeDeflate($body);
+        break;
+      
+      default:
+        break;
+    }
+    
+    return $body;
+  }
+
+  /**
+   * Get the raw response body (as transfered "on wire") as string
+   *
+   * If the body is encoded (with Transfer-Encoding, not content-encoding -
+   * IE "chunked" body), gzip compressed, etc. it will not be decoded.
+   *
+   * @return string
+   */
+  public function getRawBody() {
+    return $this->body;
+  }
+
+  /**
+   * Get the HTTP version of the response
+   *
+   * @return string
+   */
+  public function getVersion() {
+    return $this->version;
+  }
+
+  /**
+   * Get the HTTP response status code
+   *
+   * @return int
+   */
+  public function getStatus() {
+    return $this->code;
+  }
+
+  /**
+   * Return a message describing the HTTP response code
+   * (Eg. "OK", "Not Found", "Moved Permanently")
+   *
+   * @return string
+   */
+  public function getMessage() {
+    return $this->message;
+  }
+
+  /**
+   * Get the response headers
+   *
+   * @return array
+   */
+  public function getHeaders() {
+    return $this->headers;
+  }
+
+  /**
+   * Get a specific header as string, or null if it is not set
+   *
+   * @param string$header
+   * @return string|array|null
+   */
+  public function getHeader($header) {
+    $header = ucwords(strtolower($header));
+    if (! is_string($header) || ! isset($this->headers[$header])) return null;
+    
+    return $this->headers[$header];
+  }
+
+  /**
+   * Get all headers as string
+   *
+   * @param boolean $status_line Whether to return the first status line (IE "HTTP 200 OK")
+   * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
+   * @return string
+   */
+  public function getHeadersAsString($status_line = true, $br = "\n") {
+    $str = '';
+    
+    if ($status_line) {
+      $str = "HTTP/{$this->version} {$this->code} {$this->message}{$br}";
+    }
+    
+    // Iterate over the headers and stringify them
+    foreach ($this->headers as $name => $value) {
+      if (is_string($value)) $str .= "{$name}: {$value}{$br}";
+      
+      elseif (is_array($value)) {
+        foreach ($value as $subval) {
+          $str .= "{$name}: {$subval}{$br}";
+        }
+      }
+    }
+    
+    return $str;
+  }
+
+  /**
+   * Get the entire response as string
+   *
+   * @param string $br Line breaks (eg. "\n", "\r\n", "<br />")
+   * @return string
+   */
+  public function asString($br = "\n") {
+    return $this->getHeadersAsString(true, $br) . $br . $this->getBody();
+  }
+
+  /**
+   * A convenience function that returns a text representation of
+   * HTTP response codes. Returns 'Unknown' for unknown codes.
+   * Returns array of all codes, if $code is not specified.
+   *
+   * Conforms to HTTP/1.1 as defined in RFC 2616 (except for 'Unknown')
+   * See http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10 for reference
+   *
+   * @param int $code HTTP response code
+   * @param boolean $http11 Use HTTP version 1.1
+   * @return string
+   */
+  public static function responseCodeAsText($code = null, $http11 = true) {
+    $messages = self::$messages;
+    if (! $http11) $messages[302] = 'Moved Temporarily';
+    
+    if ($code === null) {
+      return $messages;
+    } elseif (isset($messages[$code])) {
+      return $messages[$code];
+    } else {
+      return 'Unknown';
+    }
+  }
+
+  /**
+   * Extract the response code from a response string
+   *
+   * @param string $response_str
+   * @return int
+   */
+  public static function extractCode($response_str) {
+    preg_match("|^HTTP/[\d\.x]+ (\d+)|", $response_str, $m);
+    
+    if (isset($m[1])) {
+      return (int)$m[1];
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Extract the HTTP message from a response
+   *
+   * @param string $response_str
+   * @return string
+   */
+  public static function extractMessage($response_str) {
+    preg_match("|^HTTP/[\d\.x]+ \d+ ([^\r\n]+)|", $response_str, $m);
+    
+    if (isset($m[1])) {
+      return $m[1];
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Extract the HTTP version from a response
+   *
+   * @param string $response_str
+   * @return string
+   */
+  public static function extractVersion($response_str) {
+    preg_match("|^HTTP/([\d\.x]+) \d+|", $response_str, $m);
+    
+    if (isset($m[1])) {
+      return $m[1];
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Extract the headers from a response string
+   *
+   * @param string $response_str
+   * @return array
+   */
+  public static function extractHeaders($response_str) {
+    $headers = array();
+    
+    // First, split body and headers
+    $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
+    if (! $parts[0]) return $headers;
+    
+    // Split headers part to lines
+    $lines = explode("\n", $parts[0]);
+    unset($parts);
+    $last_header = null;
+    
+    foreach ($lines as $line) {
+      $line = trim($line, "\r\n");
+      if ($line == "") break;
+      
+      if (preg_match("|^([\w-]+):\s+(.+)|", $line, $m)) {
+        unset($last_header);
+        $h_name = strtolower($m[1]);
+        $h_value = $m[2];
+        
+        if (isset($headers[$h_name])) {
+          if (! is_array($headers[$h_name])) {
+            $headers[$h_name] = array($headers[$h_name]);
+          }
+          
+          $headers[$h_name][] = $h_value;
+        } else {
+          $headers[$h_name] = $h_value;
+        }
+        $last_header = $h_name;
+      } elseif (preg_match("|^\s+(.+)$|", $line, $m) && $last_header !== null) {
+        if (is_array($headers[$last_header])) {
+          end($headers[$last_header]);
+          $last_header_key = key($headers[$last_header]);
+          $headers[$last_header][$last_header_key] .= $m[1];
+        } else {
+          $headers[$last_header] .= $m[1];
+        }
+      }
+    }
+    
+    return $headers;
+  }
+
+  /**
+   * Extract the body from a response string
+   *
+   * @param string $response_str
+   * @return string
+   */
+  public static function extractBody($response_str) {
+    $parts = preg_split('|(?:\r?\n){2}|m', $response_str, 2);
+    if (isset($parts[1])) {
+      return $parts[1];
+    } else {
+      return '';
+    }
+  }
+
+  /**
+   * Decode a "chunked" transfer-encoded body and return the decoded text
+   *
+   * @param string $body
+   * @return string
+   */
+  public static function decodeChunkedBody($body) {
+    $decBody = '';
+    
+    while (trim($body)) {
+      if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) {
+        require_once 'external/Zend/Http/Exception.php';
+        throw new Zend_Http_Exception("Error parsing body - doesn't seem to be a chunked message");
+      }
+      
+      $length = hexdec(trim($m[1]));
+      $cut = strlen($m[0]);
+      
+      $decBody .= substr($body, $cut, $length);
+      $body = substr($body, $cut + $length + 2);
+    }
+    
+    return $decBody;
+  }
+
+  /**
+   * Decode a gzip encoded message (when Content-encoding = gzip)
+   *
+   * Currently requires PHP with zlib support
+   *
+   * @param string $body
+   * @return string
+   */
+  public static function decodeGzip($body) {
+    if (! function_exists('gzinflate')) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('Unable to decode gzipped response ' . 'body: perhaps the zlib extension is not loaded?');
+    }
+    
+    return gzinflate(substr($body, 10));
+  }
+
+  /**
+   * Decode a zlib deflated message (when Content-encoding = deflate)
+   *
+   * Currently requires PHP with zlib support
+   *
+   * @param string $body
+   * @return string
+   */
+  public static function decodeDeflate($body) {
+    if (! function_exists('gzuncompress')) {
+      require_once 'external/Zend/Http/Exception.php';
+      throw new Zend_Http_Exception('Unable to decode deflated response ' . 'body: perhaps the zlib extension is not loaded?');
+    }
+    
+    return gzuncompress($body);
+  }
+
+  /**
+   * Create a new Zend_Http_Response object from a string
+   *
+   * @param string $response_str
+   * @return Zend_Http_Response
+   */
+  public static function fromString($response_str) {
+    $code = self::extractCode($response_str);
+    $headers = self::extractHeaders($response_str);
+    $body = self::extractBody($response_str);
+    $version = self::extractVersion($response_str);
+    $message = self::extractMessage($response_str);
+    
+    return new Zend_Http_Response($code, $headers, $body, $version, $message);
+  }
+}
diff --git a/trunk/php/external/Zend/Loader.php b/trunk/php/external/Zend/Loader.php
new file mode 100644
index 0000000..e9486d2
--- /dev/null
+++ b/trunk/php/external/Zend/Loader.php
@@ -0,0 +1,252 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Loader.php 8906 2008-03-19 19:15:30Z darby $
+ */
+
+/**
+ * Static methods for loading classes and files.
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Loader {
+
+  /**
+   * Loads a class from a PHP file.  The filename must be formatted
+   * as "$class.php".
+   *
+   * If $dirs is a string or an array, it will search the directories
+   * in the order supplied, and attempt to load the first matching file.
+   *
+   * If $dirs is null, it will split the class name at underscores to
+   * generate a path hierarchy (e.g., "Zend_Example_Class" will map
+   * to "Zend/Example/Class.php").
+   *
+   * If the file was not found in the $dirs, or if no $dirs were specified,
+   * it will attempt to load it from PHP's include_path.
+   *
+   * @param string $class      - The full class name of a Zend component.
+   * @param string|array $dirs - OPTIONAL Either a path or an array of paths
+   *                             to search.
+   * @return void
+   * @throws Zend_Exception
+   */
+  public static function loadClass($class, $dirs = null) {
+    if (class_exists($class, false) || interface_exists($class, false)) {
+      return;
+    }
+    
+    if ((null !== $dirs) && ! is_string($dirs) && ! is_array($dirs)) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception('Directory argument must be a string or an array');
+    }
+    
+    // autodiscover the path from the class name
+    $file = str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php';
+    if (! empty($dirs)) {
+      // use the autodiscovered path
+      $dirPath = dirname($file);
+      if (is_string($dirs)) {
+        $dirs = explode(PATH_SEPARATOR, $dirs);
+      }
+      foreach ($dirs as $key => $dir) {
+        if ($dir == '.') {
+          $dirs[$key] = $dirPath;
+        } else {
+          $dir = rtrim($dir, '\\/');
+          $dirs[$key] = $dir . DIRECTORY_SEPARATOR . $dirPath;
+        }
+      }
+      $file = basename($file);
+      self::loadFile($file, $dirs, true);
+    } else {
+      self::_securityCheck($file);
+      include_once 'external/' . $file;
+    }
+    
+    if (! class_exists($class, false) && ! interface_exists($class, false)) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception("File \"$file\" does not exist or class \"$class\" was not found in the file");
+    }
+  }
+
+  /**
+   * Loads a PHP file.  This is a wrapper for PHP's include() function.
+   *
+   * $filename must be the complete filename, including any
+   * extension such as ".php".  Note that a security check is performed that
+   * does not permit extended characters in the filename.  This method is
+   * intended for loading Zend Framework files.
+   *
+   * If $dirs is a string or an array, it will search the directories
+   * in the order supplied, and attempt to load the first matching file.
+   *
+   * If the file was not found in the $dirs, or if no $dirs were specified,
+   * it will attempt to load it from PHP's include_path.
+   *
+   * If $once is TRUE, it will use include_once() instead of include().
+   *
+   * @param  string        $filename
+   * @param  string|array  $dirs - OPTIONAL either a path or array of paths
+   *                       to search.
+   * @param  boolean       $once
+   * @return boolean
+   * @throws Zend_Exception
+   */
+  public static function loadFile($filename, $dirs = null, $once = false) {
+    self::_securityCheck($filename);
+    
+    /**
+     * Search in provided directories, as well as include_path
+     */
+    $incPath = false;
+    if (! empty($dirs) && (is_array($dirs) || is_string($dirs))) {
+      if (is_array($dirs)) {
+        $dirs = implode(PATH_SEPARATOR, $dirs);
+      }
+      $incPath = get_include_path();
+      set_include_path($dirs . PATH_SEPARATOR . $incPath);
+    }
+    
+    /**
+     * Try finding for the plain filename in the include_path.
+     */
+    if ($once) {
+      include_once $filename;
+    } else {
+      include $filename;
+    }
+    
+    /**
+     * If searching in directories, reset include_path
+     */
+    if ($incPath) {
+      set_include_path($incPath);
+    }
+    
+    return true;
+  }
+
+  /**
+   * Returns TRUE if the $filename is readable, or FALSE otherwise.
+   * This function uses the PHP include_path, where PHP's is_readable()
+   * does not.
+   *
+   * @param string   $filename
+   * @return boolean
+   */
+  public static function isReadable($filename) {
+    if (! $fh = @fopen($filename, 'r', true)) {
+      return false;
+    }
+    
+    return true;
+  }
+
+  /**
+   * spl_autoload() suitable implementation for supporting class autoloading.
+   *
+   * Attach to spl_autoload() using the following:
+   * <code>
+   * spl_autoload_register(array('Zend_Loader', 'autoload'));
+   * </code>
+   *
+   * @param string $class
+   * @return string|false Class name on success; false on failure
+   */
+  public static function autoload($class) {
+    try {
+      self::loadClass($class);
+      return $class;
+    } catch (Exception $e) {
+      return false;
+    }
+  }
+
+  /**
+   * Register {@link autoload()} with spl_autoload()
+   *
+   * @param string $class (optional)
+   * @param boolean $enabled (optional)
+   * @return void
+   * @throws Zend_Exception if spl_autoload() is not found
+   * or if the specified class does not have an autoload() method.
+   */
+  public static function registerAutoload($class = 'Zend_Loader', $enabled = true) {
+    if (! function_exists('spl_autoload_register')) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception('spl_autoload does not exist in this PHP installation');
+    }
+    
+    self::loadClass($class);
+    $methods = get_class_methods($class);
+    if (! in_array('autoload', (array)$methods)) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception("The class \"$class\" does not have an autoload() method");
+    }
+    
+    if ($enabled === true) {
+      spl_autoload_register(array($class, 'autoload'));
+    } else {
+      spl_autoload_unregister(array($class, 'autoload'));
+    }
+  }
+
+  /**
+   * Ensure that filename does not contain exploits
+   *
+   * @param  string $filename
+   * @return void
+   * @throws Zend_Exception
+   */
+  protected static function _securityCheck($filename) {
+    /**
+     * Security check
+     */
+    if (preg_match('/[^a-z0-9\\/\\\\_.-]/i', $filename)) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception('Security check: Illegal character in filename');
+    }
+  }
+
+  /**
+   * Attempt to include() the file.
+   *
+   * include() is not prefixed with the @ operator because if
+   * the file is loaded and contains a parse error, execution
+   * will halt silently and this is difficult to debug.
+   *
+   * Always set display_errors = Off on production servers!
+   *
+   * @param  string  $filespec
+   * @param  boolean $once
+   * @return boolean
+   * @deprecated Since 1.5.0; use loadFile() instead
+   */
+  protected static function _includeFile($filespec, $once = false) {
+    if ($once) {
+      return include_once $filespec;
+    } else {
+      return include $filespec;
+    }
+  }
+}
diff --git a/trunk/php/external/Zend/Loader/Exception.php b/trunk/php/external/Zend/Loader/Exception.php
new file mode 100644
index 0000000..ed4996d
--- /dev/null
+++ b/trunk/php/external/Zend/Loader/Exception.php
@@ -0,0 +1,7 @@
+<?php
+
+require_once 'external/Zend/Exception.php';
+
+class Zend_Loader_Exception extends Zend_Exception {
+
+}
diff --git a/trunk/php/external/Zend/Loader/PluginLoader.php b/trunk/php/external/Zend/Loader/PluginLoader.php
new file mode 100644
index 0000000..22046bd
--- /dev/null
+++ b/trunk/php/external/Zend/Loader/PluginLoader.php
@@ -0,0 +1,324 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @subpackage PluginLoader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/** Zend_Loader_PluginLoader_Interface */
+require_once 'external/Zend/Loader/PluginLoader/Interface.php';
+
+/** Zend_Loader */
+require_once 'external/Zend/Loader.php';
+
+/**
+ * Generic plugin class loader
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @subpackage PluginLoader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Loader_PluginLoader implements Zend_Loader_PluginLoader_Interface {
+  /**
+   * Static registry property
+   *
+   * @var array
+   */
+  protected static $_staticPrefixToPaths = array();
+  
+  /**
+   * Instance registry property
+   *
+   * @var array
+   */
+  protected $_prefixToPaths = array();
+  
+  /**
+   * Statically loaded plugins
+   *
+   * @var array
+   */
+  protected static $_staticLoadedPlugins = array();
+  
+  /**
+   * Instance loaded plugins
+   *
+   * @var array
+   */
+  protected $_loadedPlugins = array();
+  
+  /**
+   * Whether to use a statically named registry for loading plugins
+   *
+   * @var string|null
+   */
+  protected $_useStaticRegistry = null;
+
+  /**
+   * Constructor
+   *
+   * @param array $prefixToPaths
+   * @param string $staticRegistryName OPTIONAL
+   */
+  public function __construct(Array $prefixToPaths = array(), $staticRegistryName = null) {
+    if (is_string($staticRegistryName) && ! empty($staticRegistryName)) {
+      $this->_useStaticRegistry = $staticRegistryName;
+      self::$_staticPrefixToPaths[$staticRegistryName] = array();
+      self::$_staticLoadedPlugins[$staticRegistryName] = array();
+    }
+    
+    foreach ($prefixToPaths as $prefix => $path) {
+      $this->addPrefixPath($prefix, $path);
+    }
+  }
+
+  /**
+   * Format prefix for internal use
+   *
+   * @param  string $prefix
+   * @return string
+   */
+  protected function _formatPrefix($prefix) {
+    return rtrim($prefix, '_') . '_';
+  }
+
+  /**
+   * Add prefixed paths to the registry of paths
+   *
+   * @param string $prefix
+   * @param string $path
+   * @return Zend_Loader_PluginLoader
+   */
+  public function addPrefixPath($prefix, $path) {
+    if (! is_string($prefix) || ! is_string($path)) {
+      require_once 'external/Zend/Loader/PluginLoader/Exception.php';
+      throw new Zend_Loader_PluginLoader_Exception('Zend_Loader_PluginLoader::addPrefixPath() method only takes strings for prefix and path.');
+    }
+    
+    $prefix = $this->_formatPrefix($prefix);
+    $path = rtrim($path, '/\\') . '/';
+    
+    if ($this->_useStaticRegistry) {
+      self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix][] = $path;
+    } else {
+      $this->_prefixToPaths[$prefix][] = $path;
+    }
+    return $this;
+  }
+
+  /**
+   * Get path stack
+   *
+   * @param  string $prefix
+   * @return false|array False if prefix does not exist, array otherwise
+   */
+  public function getPaths($prefix = null) {
+    if ((null !== $prefix) && is_string($prefix)) {
+      $prefix = $this->_formatPrefix($prefix);
+      if ($this->_useStaticRegistry) {
+        if (isset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix])) {
+          return self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix];
+        }
+        
+        return false;
+      }
+      
+      if (isset($this->_prefixToPaths[$prefix])) {
+        return $this->_prefixToPaths[$prefix];
+      }
+      
+      return false;
+    }
+    
+    if ($this->_useStaticRegistry) {
+      return self::$_staticPrefixToPaths[$this->_useStaticRegistry];
+    }
+    
+    return $this->_prefixToPaths;
+  }
+
+  /**
+   * Clear path stack
+   *
+   * @param  string $prefix
+   * @return bool False only if $prefix does not exist
+   */
+  public function clearPaths($prefix = null) {
+    if ((null !== $prefix) && is_string($prefix)) {
+      $prefix = $this->_formatPrefix($prefix);
+      if ($this->_useStaticRegistry) {
+        if (isset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix])) {
+          unset(self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix]);
+          return true;
+        }
+        
+        return false;
+      }
+      
+      if (isset($this->_prefixToPaths[$prefix])) {
+        unset($this->_prefixToPaths[$prefix]);
+        return true;
+      }
+      
+      return false;
+    }
+    
+    if ($this->_useStaticRegistry) {
+      self::$_staticPrefixToPaths[$this->_useStaticRegistry] = array();
+    } else {
+      $this->_prefixToPaths = array();
+    }
+    
+    return true;
+  }
+
+  /**
+   * Remove a prefix (or prefixed-path) from the registry
+   *
+   * @param string $prefix
+   * @param string $path OPTIONAL
+   * @return Zend_Loader_PluginLoader
+   */
+  public function removePrefixPath($prefix, $path = null) {
+    $prefix = $this->_formatPrefix($prefix);
+    if ($this->_useStaticRegistry) {
+      $registry = & self::$_staticPrefixToPaths[$this->_useStaticRegistry];
+    } else {
+      $registry = & $this->_prefixToPaths;
+    }
+    
+    if (! isset($registry[$prefix])) {
+      require_once 'external/Zend/Loader/PluginLoader/Exception.php';
+      throw new Zend_Loader_PluginLoader_Exception('Prefix ' . $prefix . ' was not found in the PluginLoader.');
+    }
+    
+    if ($path != null) {
+      $pos = array_search($path, $registry[$prefix]);
+      if ($pos === null) {
+        throw new Zend_Loader_PluginLoader_Exception('Prefix ' . $prefix . ' / Path ' . $path . ' was not found in the PluginLoader.');
+      }
+      unset($registry[$prefix][$pos]);
+    } else {
+      unset($registry[$prefix]);
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Normalize plugin name
+   *
+   * @param  string $name
+   * @return string
+   */
+  protected function _formatName($name) {
+    return ucfirst((string)$name);
+  }
+
+  /**
+   * Whether or not a Plugin by a specific name is loaded
+   *
+   * @param string $name
+   * @return Zend_Loader_PluginLoader
+   */
+  public function isLoaded($name) {
+    $name = $this->_formatName($name);
+    if ($this->_useStaticRegistry) {
+      return isset(self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name]);
+    }
+    
+    return isset($this->_loadedPlugins[$name]);
+  }
+
+  /**
+   * Return full class name for a named plugin
+   *
+   * @param string $name
+   * @return string|false False if class not found, class name otherwise
+   */
+  public function getClassName($name) {
+    $name = $this->_formatName($name);
+    if ($this->_useStaticRegistry && isset(self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name])) {
+      return self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name];
+    } elseif (isset($this->_loadedPlugins[$name])) {
+      return $this->_loadedPlugins[$name];
+    }
+    
+    return false;
+  }
+
+  /**
+   * Load a plugin via the name provided
+   *
+   * @param  string $name
+   * @return string
+   */
+  public function load($name) {
+    $name = $this->_formatName($name);
+    if ($this->_useStaticRegistry) {
+      $registry = self::$_staticPrefixToPaths[$this->_useStaticRegistry];
+    } else {
+      $registry = $this->_prefixToPaths;
+    }
+    
+    if ($this->isLoaded($name)) {
+      return $this->getClassName($name);
+    }
+    
+    $found = false;
+    
+    $registry = array_reverse($registry, true);
+    foreach ($registry as $prefix => $paths) {
+      $paths = array_reverse($paths, true);
+      foreach ($paths as $path) {
+        
+        $classFile = str_replace('_', DIRECTORY_SEPARATOR, $name) . '.php';
+        $className = $prefix . $name;
+        
+        if (class_exists($className, false)) {
+          $found = true;
+          break 2;
+        }
+        
+        if (Zend_Loader::isReadable($path . $classFile)) {
+          include_once $path . $classFile;
+          
+          if (! class_exists($className, false)) {
+            throw new Zend_Loader_PluginLoader_Exception('File ' . $classFile . ' was loaded but class named ' . $className . ' was not found within it.');
+          }
+          
+          $found = true;
+          break 2;
+        }
+      }
+    }
+    
+    if ($found) {
+      if ($this->_useStaticRegistry) {
+        self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name] = $className;
+      } else {
+        $this->_loadedPlugins[$name] = $className;
+      }
+      return $className;
+    }
+    
+    require_once 'external/Zend/Loader/PluginLoader/Exception.php';
+    throw new Zend_Loader_PluginLoader_Exception('Plugin by name ' . $name . ' was not found in the registry.');
+  }
+}
diff --git a/trunk/php/external/Zend/Loader/PluginLoader/Exception.php b/trunk/php/external/Zend/Loader/PluginLoader/Exception.php
new file mode 100644
index 0000000..f39da62
--- /dev/null
+++ b/trunk/php/external/Zend/Loader/PluginLoader/Exception.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @subpackage PluginLoader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Loader_Exception
+ */
+require_once 'external/Zend/Loader/Exception.php';
+
+/**
+ * Plugin class loader exceptions
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @subpackage PluginLoader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Loader_PluginLoader_Exception extends Zend_Loader_Exception {
+}
diff --git a/trunk/php/external/Zend/Loader/PluginLoader/Interface.php b/trunk/php/external/Zend/Loader/PluginLoader/Interface.php
new file mode 100644
index 0000000..d6381b6
--- /dev/null
+++ b/trunk/php/external/Zend/Loader/PluginLoader/Interface.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @subpackage PluginLoader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * Plugin class loader interface
+ *
+ * @category   Zend
+ * @package    Zend_Loader
+ * @subpackage PluginLoader
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Loader_PluginLoader_Interface {
+
+  /**
+   * Add prefixed paths to the registry of paths
+   *
+   * @param string $prefix
+   * @param string $path
+   * @return Zend_Loader_PluginLoader
+   */
+  public function addPrefixPath($prefix, $path);
+
+  /**
+   * Remove a prefix (or prefixed-path) from the registry
+   *
+   * @param string $prefix
+   * @param string $path OPTIONAL
+   * @return Zend_Loader_PluginLoader
+   */
+  public function removePrefixPath($prefix, $path = null);
+
+  /**
+   * Whether or not a Helper by a specific name
+   *
+   * @param string $name
+   * @return Zend_Loader_PluginLoader
+   */
+  public function isLoaded($name);
+
+  /**
+   * Return full class name for a named helper
+   *
+   * @param string $name
+   * @return string
+   */
+  public function getClassName($name);
+
+  /**
+   * Load a helper via the name provided
+   *
+   * @param string $name
+   * @return string
+   */
+  public function load($name);
+}
diff --git a/trunk/php/external/Zend/README b/trunk/php/external/Zend/README
new file mode 100644
index 0000000..28d7639
--- /dev/null
+++ b/trunk/php/external/Zend/README
@@ -0,0 +1,6 @@
+This is a stripped copy of the Zend Framework: http://framework.zend.com/
+Which is licensed under the New BSD license: http://framework.zend.com/license
+
+To prevent having to modify the include_path, the require_once 'Zend/<file>.php' statements were rewritten using:
+# find . -type f -name "*.php" | xargs grep -l "require_once 'Zend" | xargs sed -i '' -e "s/require_once \'Zend/require_once 'external\/Zend/g"
+
diff --git a/trunk/php/external/Zend/Registry.php b/trunk/php/external/Zend/Registry.php
new file mode 100644
index 0000000..dc800de
--- /dev/null
+++ b/trunk/php/external/Zend/Registry.php
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Registry
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Registry.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * Generic storage class helps to manage global data.
+ *
+ * @category   Zend
+ * @package    Zend_Registry
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Registry extends ArrayObject {
+  /**
+   * Class name of the singleton registry object.
+   * @var string
+   */
+  private static $_registryClassName = 'Zend_Registry';
+  
+  /**
+   * Registry object provides storage for shared objects.
+   * @var Zend_Registry
+   */
+  private static $_registry = null;
+
+  /**
+   * Retrieves the default registry instance.
+   *
+   * @return Zend_Registry
+   */
+  public static function getInstance() {
+    if (self::$_registry === null) {
+      self::init();
+    }
+    
+    return self::$_registry;
+  }
+
+  /**
+   * Set the default registry instance to a specified instance.
+   *
+   * @param Zend_Registry $registry An object instance of type Zend_Registry,
+   *   or a subclass.
+   * @return void
+   * @throws Zend_Exception if registry is already initialized.
+   */
+  public static function setInstance(Zend_Registry $registry) {
+    if (self::$_registry !== null) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception('Registry is already initialized');
+    }
+    
+    self::setClassName(get_class($registry));
+    self::$_registry = $registry;
+  }
+
+  /**
+   * Initialize the default registry instance.
+   *
+   * @return void
+   */
+  protected static function init() {
+    self::setInstance(new self::$_registryClassName());
+  }
+
+  /**
+   * Set the class name to use for the default registry instance.
+   * Does not affect the currently initialized instance, it only applies
+   * for the next time you instantiate.
+   *
+   * @param string $registryClassName
+   * @return void
+   * @throws Zend_Exception if the registry is initialized or if the
+   *   class name is not valid.
+   */
+  public static function setClassName($registryClassName = 'Zend_Registry') {
+    if (self::$_registry !== null) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception('Registry is already initialized');
+    }
+    
+    if (! is_string($registryClassName)) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception("Argument is not a class name");
+    }
+    
+    /**
+     * @see Zend_Loader
+     */
+    require_once 'external/Zend/Loader.php';
+    Zend_Loader::loadClass($registryClassName);
+    
+    self::$_registryClassName = $registryClassName;
+  }
+
+  /**
+   * Unset the default registry instance.
+   * Primarily used in tearDown() in unit tests.
+   * @returns void
+   */
+  public static function _unsetInstance() {
+    self::$_registry = null;
+  }
+
+  /**
+   * getter method, basically same as offsetGet().
+   *
+   * This method can be called from an object of type Zend_Registry, or it
+   * can be called statically.  In the latter case, it uses the default
+   * static instance stored in the class.
+   *
+   * @param string $index - get the value associated with $index
+   * @return mixed
+   * @throws Zend_Exception if no entry is registerd for $index.
+   */
+  public static function get($index) {
+    $instance = self::getInstance();
+    
+    if (! $instance->offsetExists($index)) {
+      require_once 'external/Zend/Exception.php';
+      throw new Zend_Exception("No entry is registered for key '$index'");
+    }
+    
+    return $instance->offsetGet($index);
+  }
+
+  /**
+   * setter method, basically same as offsetSet().
+   *
+   * This method can be called from an object of type Zend_Registry, or it
+   * can be called statically.  In the latter case, it uses the default
+   * static instance stored in the class.
+   *
+   * @param string $index The location in the ArrayObject in which to store
+   *   the value.
+   * @param mixed $value The object to store in the ArrayObject.
+   * @return void
+   */
+  public static function set($index, $value) {
+    $instance = self::getInstance();
+    $instance->offsetSet($index, $value);
+  }
+
+  /**
+   * Returns TRUE if the $index is a named value in the registry,
+   * or FALSE if $index was not found in the registry.
+   *
+   * @param  string $index
+   * @return boolean
+   */
+  public static function isRegistered($index) {
+    if (self::$_registry === null) {
+      return false;
+    }
+    return self::$_registry->offsetExists($index);
+  }
+
+  /**
+   * @param string $index
+   * @returns mixed
+   *
+   * Workaround for http://bugs.php.net/bug.php?id=40442 (ZF-960).
+   */
+  public function offsetExists($index) {
+    return array_key_exists($index, $this);
+  }
+
+}
diff --git a/trunk/php/external/Zend/Uri.php b/trunk/php/external/Zend/Uri.php
new file mode 100644
index 0000000..8d9df50
--- /dev/null
+++ b/trunk/php/external/Zend/Uri.php
@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Uri
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Uri.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Loader
+ */
+require_once 'external/Zend/Loader.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Uri
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Uri {
+  /**
+   * Scheme of this URI (http, ftp, etc.)
+   * @var string
+   */
+  protected $_scheme = '';
+
+  /**
+   * Return a string representation of this URI.
+   *
+   * @see     getUri()
+   * @return  string
+   */
+  public function __toString() {
+    return $this->getUri();
+  }
+
+  /**
+   * Convenience function, checks that a $uri string is well-formed
+   * by validating it but not returning an object.  Returns TRUE if
+   * $uri is a well-formed URI, or FALSE otherwise.
+   *
+   * @param string $uri
+   * @return boolean
+   */
+  public static function check($uri) {
+    try {
+      $uri = self::factory($uri);
+    } catch (Exception $e) {
+      return false;
+    }
+    
+    return $uri->valid();
+  }
+
+  /**
+   * Create a new Zend_Uri object for a URI.  If building a new URI, then $uri should contain
+   * only the scheme (http, ftp, etc).  Otherwise, supply $uri with the complete URI.
+   *
+   * @param string $uri
+   * @throws Zend_Uri_Exception
+   * @return Zend_Uri
+   */
+  public static function factory($uri = 'http') {
+    /**
+     * Separate the scheme from the scheme-specific parts
+     * @link http://www.faqs.org/rfcs/rfc2396.html
+     */
+    $uri = explode(':', $uri, 2);
+    $scheme = strtolower($uri[0]);
+    $schemeSpecific = isset($uri[1]) ? $uri[1] : '';
+    
+    if (! strlen($scheme)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('An empty string was supplied for the scheme');
+    }
+    
+    // Security check: $scheme is used to load a class file, so only alphanumerics are allowed.
+    if (! ctype_alnum($scheme)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Illegal scheme supplied, only alphanumeric characters are permitted');
+    }
+    
+    /**
+     * Create a new Zend_Uri object for the $uri. If a subclass of Zend_Uri exists for the
+     * scheme, return an instance of that class. Otherwise, a Zend_Uri_Exception is thrown.
+     */
+    switch ($scheme) {
+      case 'http':
+      case 'https':
+        $className = 'Zend_Uri_Http';
+        break;
+      case 'mailto':
+      // @todo
+      default:
+        require_once 'external/Zend/Uri/Exception.php';
+        throw new Zend_Uri_Exception("Scheme \"$scheme\" is not supported");
+    }
+    Zend_Loader::loadClass($className);
+    return new $className($scheme, $schemeSpecific);
+  
+  }
+
+  /**
+   * Get the URI's scheme
+   *
+   * @return string|false Scheme or false if no scheme is set.
+   */
+  public function getScheme() {
+    if (! empty($this->_scheme)) {
+      return $this->_scheme;
+    } else {
+      return false;
+    }
+  }
+
+  /******************************************************************************
+   * Abstract Methods
+   *****************************************************************************/
+  
+  /**
+   * Zend_Uri and its subclasses cannot be instantiated directly.
+   * Use Zend_Uri::factory() to return a new Zend_Uri object.
+   */
+  abstract protected function __construct($scheme, $schemeSpecific = '');
+
+  /**
+   * Return a string representation of this URI.
+   *
+   * @return string
+   */
+  abstract public function getUri();
+
+  /**
+   * Returns TRUE if this URI is valid, or FALSE otherwise.
+   *
+   * @return boolean
+   */
+  abstract public function valid();
+}
diff --git a/trunk/php/external/Zend/Uri/Exception.php b/trunk/php/external/Zend/Uri/Exception.php
new file mode 100644
index 0000000..a25cc41
--- /dev/null
+++ b/trunk/php/external/Zend/Uri/Exception.php
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Uri
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+
+/**
+ * @see Zend_Exception
+ */
+require_once 'external/Zend/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Uri
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Uri_Exception extends Zend_Exception {
+}
+
diff --git a/trunk/php/external/Zend/Uri/Http.php b/trunk/php/external/Zend/Uri/Http.php
new file mode 100644
index 0000000..3cae9c9
--- /dev/null
+++ b/trunk/php/external/Zend/Uri/Http.php
@@ -0,0 +1,592 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Uri
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Http.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Uri
+ */
+require_once 'external/Zend/Uri.php';
+
+/**
+ * @see Zend_Validate_Hostname
+ */
+require_once 'external/Zend/Validate/Hostname.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Uri
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Uri_Http extends Zend_Uri {
+  /**
+   * URI parts are divided among these instance variables
+   */
+  protected $_username = '';
+  protected $_password = '';
+  protected $_host = '';
+  protected $_port = '';
+  protected $_path = '';
+  protected $_query = '';
+  protected $_fragment = '';
+  
+  /**
+   * Regular expression grammar rules for validation; values added by constructor
+   */
+  protected $_regex = array();
+
+  /**
+   * Constructor accepts a string $scheme (e.g., http, https) and a scheme-specific part of the URI
+   * (e.g., example.com/path/to/resource?query=param#fragment)
+   *
+   * @param string $scheme
+   * @param string $schemeSpecific
+   * @throws Zend_Uri_Exception
+   * @return void
+   */
+  protected function __construct($scheme, $schemeSpecific = '') {
+    // Set the scheme
+    $this->_scheme = $scheme;
+    
+    // Set up grammar rules for validation via regular expressions. These
+    // are to be used with slash-delimited regular expression strings.
+    $this->_regex['alphanum'] = '[^\W_]';
+    $this->_regex['escaped'] = '(?:%[\da-fA-F]{2})';
+    $this->_regex['mark'] = '[-_.!~*\'()\[\]]';
+    $this->_regex['reserved'] = '[;\/?:@&=+$,]';
+    $this->_regex['unreserved'] = '(?:' . $this->_regex['alphanum'] . '|' . $this->_regex['mark'] . ')';
+    $this->_regex['segment'] = '(?:(?:' . $this->_regex['unreserved'] . '|' . $this->_regex['escaped'] . '|[:@&=+$,;])*)';
+    $this->_regex['path'] = '(?:\/' . $this->_regex['segment'] . '?)+';
+    $this->_regex['uric'] = '(?:' . $this->_regex['reserved'] . '|' . $this->_regex['unreserved'] . '|' . $this->_regex['escaped'] . ')';
+    // If no scheme-specific part was supplied, the user intends to create
+    // a new URI with this object.  No further parsing is required.
+    if (strlen($schemeSpecific) == 0) {
+      return;
+    }
+    
+    // Parse the scheme-specific URI parts into the instance variables.
+    $this->_parseUri($schemeSpecific);
+    
+    // Validate the URI
+    if (! $this->valid()) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Invalid URI supplied');
+    }
+  }
+
+  /**
+   * Parse the scheme-specific portion of the URI and place its parts into instance variables.
+   *
+   * @param string $schemeSpecific
+   * @throws Zend_Uri_Exception
+   * @return void
+   */
+  protected function _parseUri($schemeSpecific) {
+    // High-level decomposition parser
+    $pattern = '~^((//)([^/?#]*))([^?#]*)(\?([^#]*))?(#(.*))?$~';
+    $status = @preg_match($pattern, $schemeSpecific, $matches);
+    if ($status === false) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Internal error: scheme-specific decomposition failed');
+    }
+    
+    // Failed decomposition; no further processing needed
+    if (! $status) {
+      return;
+    }
+    
+    // Save URI components that need no further decomposition
+    $this->_path = isset($matches[4]) ? $matches[4] : '';
+    $this->_query = isset($matches[6]) ? $matches[6] : '';
+    $this->_fragment = isset($matches[8]) ? $matches[8] : '';
+    
+    // Additional decomposition to get username, password, host, and port
+    $combo = isset($matches[3]) ? $matches[3] : '';
+    $pattern = '~^(([^:@]*)(:([^@]*))?@)?([^:]+)(:(.*))?$~';
+    $status = @preg_match($pattern, $combo, $matches);
+    if ($status === false) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Internal error: authority decomposition failed');
+    }
+    
+    // Failed decomposition; no further processing needed
+    if (! $status) {
+      return;
+    }
+    
+    // Save remaining URI components
+    $this->_username = isset($matches[2]) ? $matches[2] : '';
+    $this->_password = isset($matches[4]) ? $matches[4] : '';
+    $this->_host = isset($matches[5]) ? $matches[5] : '';
+    $this->_port = isset($matches[7]) ? $matches[7] : '';
+  
+  }
+
+  /**
+   * Returns a URI based on current values of the instance variables. If any
+   * part of the URI does not pass validation, then an exception is thrown.
+   *
+   * @throws Zend_Uri_Exception
+   * @return string
+   */
+  public function getUri() {
+    if (! $this->valid()) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('One or more parts of the URI are invalid');
+    }
+    $password = strlen($this->_password) ? ":$this->_password" : '';
+    $auth = strlen($this->_username) ? "$this->_username$password@" : '';
+    $port = strlen($this->_port) ? ":$this->_port" : '';
+    $query = strlen($this->_query) ? "?$this->_query" : '';
+    $fragment = strlen($this->_fragment) ? "#$this->_fragment" : '';
+    return "$this->_scheme://$auth$this->_host$port$this->_path$query$fragment";
+  }
+
+  /**
+   * Validate the current URI from the instance variables. Returns true if and only if all
+   * parts pass validation.
+   *
+   * @return boolean
+   */
+  public function valid() {
+    /**
+     * Return true if and only if all parts of the URI have passed validation
+     */
+    return $this->validateUsername() && $this->validatePassword() && $this->validateHost() && $this->validatePort() && $this->validatePath() && $this->validateQuery() && $this->validateFragment();
+  }
+
+  /**
+   * Returns the username portion of the URL, or FALSE if none.
+   *
+   * @return string
+   */
+  public function getUsername() {
+    return strlen($this->_username) ? $this->_username : false;
+  }
+
+  /**
+   * Returns true if and only if the username passes validation. If no username is passed,
+   * then the username contained in the instance variable is used.
+   *
+   * @param string $username
+   * @throws Zend_Uri_Exception
+   * @return boolean
+   */
+  public function validateUsername($username = null) {
+    if ($username === null) {
+      $username = $this->_username;
+    }
+    
+    // If the username is empty, then it is considered valid
+    if (strlen($username) == 0) {
+      return true;
+    }
+    /**
+     * Check the username against the allowed values
+     *
+     * @link http://www.faqs.org/rfcs/rfc2396.html
+     */
+    $status = @preg_match('/^(' . $this->_regex['alphanum'] . '|' . $this->_regex['mark'] . '|' . $this->_regex['escaped'] . '|[;:&=+$,])+$/', $username);
+    if ($status === false) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Internal error: username validation failed');
+    }
+    
+    return $status == 1;
+  }
+
+  /**
+   * Sets the username for the current URI, and returns the old username
+   *
+   * @param string $username
+   * @throws Zend_Uri_Exception
+   * @return string
+   */
+  public function setUsername($username) {
+    if (! $this->validateUsername($username)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception("Username \"$username\" is not a valid HTTP username");
+    }
+    $oldUsername = $this->_username;
+    $this->_username = $username;
+    return $oldUsername;
+  }
+
+  /**
+   * Returns the password portion of the URL, or FALSE if none.
+   *
+   * @return string
+   */
+  public function getPassword() {
+    return strlen($this->_password) ? $this->_password : false;
+  }
+
+  /**
+   * Returns true if and only if the password passes validation. If no password is passed,
+   * then the password contained in the instance variable is used.
+   *
+   * @param string $password
+   * @throws Zend_Uri_Exception
+   * @return boolean
+   */
+  public function validatePassword($password = null) {
+    if ($password === null) {
+      $password = $this->_password;
+    }
+    
+    // If the password is empty, then it is considered valid
+    if (strlen($password) == 0) {
+      return true;
+    }
+    
+    // If the password is nonempty, but there is no username, then it is considered invalid
+    if (strlen($password) > 0 && strlen($this->_username) == 0) {
+      return false;
+    }
+    
+    /**
+     * Check the password against the allowed values
+     *
+     * @link http://www.faqs.org/rfcs/rfc2396.html
+     */
+    $status = @preg_match('/^(' . $this->_regex['alphanum'] . '|' . $this->_regex['mark'] . '|' . $this->_regex['escaped'] . '|[;:&=+$,])+$/', $password);
+    if ($status === false) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Internal error: password validation failed.');
+    }
+    return $status == 1;
+  }
+
+  /**
+   * Sets the password for the current URI, and returns the old password
+   *
+   * @param string $password
+   * @throws Zend_Uri_Exception
+   * @return string
+   */
+  public function setPassword($password) {
+    if (! $this->validatePassword($password)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception("Password \"$password\" is not a valid HTTP password.");
+    }
+    $oldPassword = $this->_password;
+    $this->_password = $password;
+    return $oldPassword;
+  }
+
+  /**
+   * Returns the domain or host IP portion of the URL, or FALSE if none.
+   *
+   * @return string
+   */
+  public function getHost() {
+    return strlen($this->_host) ? $this->_host : false;
+  }
+
+  /**
+   * Returns true if and only if the host string passes validation. If no host is passed,
+   * then the host contained in the instance variable is used.
+   *
+   * @param string $host
+   * @return boolean
+   * @uses Zend_Filter
+   */
+  public function validateHost($host = null) {
+    if ($host === null) {
+      $host = $this->_host;
+    }
+    
+    /**
+     * If the host is empty, then it is considered invalid
+     */
+    if (strlen($host) == 0) {
+      return false;
+    }
+    
+    /**
+     * Check the host against the allowed values; delegated to Zend_Filter.
+     */
+    $validate = new Zend_Validate_Hostname(Zend_Validate_Hostname::ALLOW_ALL);
+    return $validate->isValid($host);
+  }
+
+  /**
+   * Sets the host for the current URI, and returns the old host
+   *
+   * @param string $host
+   * @throws Zend_Uri_Exception
+   * @return string
+   */
+  public function setHost($host) {
+    if (! $this->validateHost($host)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception("Host \"$host\" is not a valid HTTP host");
+    }
+    $oldHost = $this->_host;
+    $this->_host = $host;
+    return $oldHost;
+  }
+
+  /**
+   * Returns the TCP port, or FALSE if none.
+   *
+   * @return string
+   */
+  public function getPort() {
+    return strlen($this->_port) ? $this->_port : false;
+  }
+
+  /**
+   * Returns true if and only if the TCP port string passes validation. If no port is passed,
+   * then the port contained in the instance variable is used.
+   *
+   * @param string $port
+   * @return boolean
+   */
+  public function validatePort($port = null) {
+    if ($port === null) {
+      $port = $this->_port;
+    }
+    
+    // If the port is empty, then it is considered valid
+    if (! strlen($port)) {
+      return true;
+    }
+    
+    // Check the port against the allowed values
+    return ctype_digit((string)$port) && 1 <= $port && $port <= 65535;
+  }
+
+  /**
+   * Sets the port for the current URI, and returns the old port
+   *
+   * @param string $port
+   * @throws Zend_Uri_Exception
+   * @return string
+   */
+  public function setPort($port) {
+    if (! $this->validatePort($port)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception("Port \"$port\" is not a valid HTTP port.");
+    }
+    $oldPort = $this->_port;
+    $this->_port = $port;
+    return $oldPort;
+  }
+
+  /**
+   * Returns the path and filename portion of the URL, or FALSE if none.
+   *
+   * @return string
+   */
+  public function getPath() {
+    return strlen($this->_path) ? $this->_path : '/';
+  }
+
+  /**
+   * Returns true if and only if the path string passes validation. If no path is passed,
+   * then the path contained in the instance variable is used.
+   *
+   * @param string $path
+   * @throws Zend_Uri_Exception
+   * @return boolean
+   */
+  public function validatePath($path = null) {
+    if ($path === null) {
+      $path = $this->_path;
+    }
+    /**
+     * If the path is empty, then it is considered valid
+     */
+    if (strlen($path) == 0) {
+      return true;
+    }
+    /**
+     * Determine whether the path is well-formed
+     */
+    $pattern = '/^' . $this->_regex['path'] . '$/';
+    $status = @preg_match($pattern, $path);
+    if ($status === false) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Internal error: path validation failed');
+    }
+    return (boolean)$status;
+  }
+
+  /**
+   * Sets the path for the current URI, and returns the old path
+   *
+   * @param string $path
+   * @throws Zend_Uri_Exception
+   * @return string
+   */
+  public function setPath($path) {
+    if (! $this->validatePath($path)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception("Path \"$path\" is not a valid HTTP path");
+    }
+    $oldPath = $this->_path;
+    $this->_path = $path;
+    return $oldPath;
+  }
+
+  /**
+   * Returns the query portion of the URL (after ?), or FALSE if none.
+   *
+   * @return string
+   */
+  public function getQuery() {
+    return strlen($this->_query) ? $this->_query : false;
+  }
+
+  /**
+   * Returns true if and only if the query string passes validation. If no query is passed,
+   * then the query string contained in the instance variable is used.
+   *
+   * @param string $query
+   * @throws Zend_Uri_Exception
+   * @return boolean
+   */
+  public function validateQuery($query = null) {
+    if ($query === null) {
+      $query = $this->_query;
+    }
+    
+    // If query is empty, it is considered to be valid
+    if (strlen($query) == 0) {
+      return true;
+    }
+    
+    /**
+     * Determine whether the query is well-formed
+     *
+     * @link http://www.faqs.org/rfcs/rfc2396.html
+     */
+    $pattern = '/^' . $this->_regex['uric'] . '*$/';
+    $status = @preg_match($pattern, $query);
+    if ($status === false) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Internal error: query validation failed');
+    }
+    
+    return $status == 1;
+  }
+
+  /**
+   * Set the query string for the current URI, and return the old query
+   * string This method accepts both strings and arrays.
+   *
+   * @param  string|array $query The query string or array
+   * @return string              Old query string
+   */
+  public function setQuery($query) {
+    $oldQuery = $this->_query;
+    
+    // If query is empty, set an empty string
+    if (empty($query)) {
+      $this->_query = '';
+      return $oldQuery;
+    }
+    
+    // If query is an array, make a string out of it
+    if (is_array($query)) {
+      $query = http_build_query($query, '', '&');
+      
+    // If it is a string, make sure it is valid. If not parse and encode it
+    } else {
+      $query = (string)$query;
+      if (! $this->validateQuery($query)) {
+        parse_str($query, $query_array);
+        $query = http_build_query($query_array, '', '&');
+      }
+    }
+    
+    // Make sure the query is valid, and set it
+    if (! $this->validateQuery($query)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception("'$query' is not a valid query string");
+    }
+    
+    $this->_query = $query;
+    
+    return $oldQuery;
+  }
+
+  /**
+   * Returns the fragment portion of the URL (after #), or FALSE if none.
+   *
+   * @return string|false
+   */
+  public function getFragment() {
+    return strlen($this->_fragment) ? $this->_fragment : false;
+  }
+
+  /**
+   * Returns true if and only if the fragment passes validation. If no fragment is passed,
+   * then the fragment contained in the instance variable is used.
+   *
+   * @param string $fragment
+   * @throws Zend_Uri_Exception
+   * @return boolean
+   */
+  public function validateFragment($fragment = null) {
+    if ($fragment === null) {
+      $fragment = $this->_fragment;
+    }
+    
+    // If fragment is empty, it is considered to be valid
+    if (strlen($fragment) == 0) {
+      return true;
+    }
+    
+    /**
+     * Determine whether the fragment is well-formed
+     *
+     * @link http://www.faqs.org/rfcs/rfc2396.html
+     */
+    $pattern = '/^' . $this->_regex['uric'] . '*$/';
+    $status = @preg_match($pattern, $fragment);
+    if ($status === false) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception('Internal error: fragment validation failed');
+    }
+    
+    return (boolean)$status;
+  }
+
+  /**
+   * Sets the fragment for the current URI, and returns the old fragment
+   *
+   * @param string $fragment
+   * @throws Zend_Uri_Exception
+   * @return string
+   */
+  public function setFragment($fragment) {
+    if (! $this->validateFragment($fragment)) {
+      require_once 'external/Zend/Uri/Exception.php';
+      throw new Zend_Uri_Exception("Fragment \"$fragment\" is not a valid HTTP fragment");
+    }
+    $oldFragment = $this->_fragment;
+    $this->_fragment = $fragment;
+    return $oldFragment;
+  }
+}
+
diff --git a/trunk/php/external/Zend/Validate.php b/trunk/php/external/Zend/Validate.php
new file mode 100644
index 0000000..b022898
--- /dev/null
+++ b/trunk/php/external/Zend/Validate.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Validate.php 8911 2008-03-19 20:22:15Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Interface
+ */
+require_once 'external/Zend/Validate/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate implements Zend_Validate_Interface {
+  /**
+   * Validator chain
+   *
+   * @var array
+   */
+  protected $_validators = array();
+  
+  /**
+   * Array of validation failure messages
+   *
+   * @var array
+   */
+  protected $_messages = array();
+  
+  /**
+   * Array of validation failure message codes
+   *
+   * @var array
+   * @deprecated Since 1.5.0
+   */
+  protected $_errors = array();
+
+  /**
+   * Adds a validator to the end of the chain
+   *
+   * If $breakChainOnFailure is true, then if the validator fails, the next validator in the chain,
+   * if one exists, will not be executed.
+   *
+   * @param  Zend_Validate_Interface $validator
+   * @param  boolean                 $breakChainOnFailure
+   * @return Zend_Validate Provides a fluent interface
+   */
+  public function addValidator(Zend_Validate_Interface $validator, $breakChainOnFailure = false) {
+    $this->_validators[] = array('instance' => $validator, 
+        'breakChainOnFailure' => (boolean)$breakChainOnFailure);
+    return $this;
+  }
+
+  /**
+   * Returns true if and only if $value passes all validations in the chain
+   *
+   * Validators are run in the order in which they were added to the chain (FIFO).
+   *
+   * @param  mixed $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $this->_messages = array();
+    $this->_errors = array();
+    $result = true;
+    foreach ($this->_validators as $element) {
+      $validator = $element['instance'];
+      if ($validator->isValid($value)) {
+        continue;
+      }
+      $result = false;
+      $messages = $validator->getMessages();
+      $this->_messages = array_merge($this->_messages, $messages);
+      $this->_errors = array_merge($this->_errors, array_keys($messages));
+      if ($element['breakChainOnFailure']) {
+        break;
+      }
+    }
+    return $result;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns array of validation failure messages
+   *
+   * @return array
+   */
+  public function getMessages() {
+    return $this->_messages;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns array of validation failure message codes
+   *
+   * @return array
+   * @deprecated Since 1.5.0
+   */
+  public function getErrors() {
+    return $this->_errors;
+  }
+
+  /**
+   * @param  mixed    $value
+   * @param  string   $classBaseName
+   * @param  array    $args          OPTIONAL
+   * @param  mixed    $namespaces    OPTIONAL
+   * @return boolean
+   * @throws Zend_Validate_Exception
+   */
+  public static function is($value, $classBaseName, array $args = array(), $namespaces = array()) {
+    $namespaces = array_merge(array('Zend_Validate'), (array)$namespaces);
+    foreach ($namespaces as $namespace) {
+      $className = $namespace . '_' . ucfirst($classBaseName);
+      try {
+        require_once 'external/Zend/Loader.php';
+        @Zend_Loader::loadClass($className);
+        if (class_exists($className, false)) {
+          $class = new ReflectionClass($className);
+          if ($class->implementsInterface('Zend_Validate_Interface')) {
+            if ($class->hasMethod('__construct')) {
+              $object = $class->newInstanceArgs($args);
+            } else {
+              $object = $class->newInstance();
+            }
+            return $object->isValid($value);
+          }
+        }
+      } catch (Zend_Validate_Exception $ze) {
+        // if there is an exception while validating throw it
+        throw $ze;
+      } catch (Zend_Exception $ze) {  // fallthrough and continue for missing validation classes
+      }
+    }
+    require_once 'external/Zend/Validate/Exception.php';
+    throw new Zend_Validate_Exception("Validate class not found from basename '$classBaseName'");
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Abstract.php b/trunk/php/external/Zend/Validate/Abstract.php
new file mode 100644
index 0000000..c438fbb
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Abstract.php
@@ -0,0 +1,328 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Abstract.php 8113 2008-02-18 13:15:27Z matthew $
+ */
+
+/**
+ * @see Zend_Validate_Interface
+ */
+require_once 'external/Zend/Validate/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+abstract class Zend_Validate_Abstract implements Zend_Validate_Interface {
+  /**
+   * The value to be validated
+   *
+   * @var mixed
+   */
+  protected $_value;
+  
+  /**
+   * Additional variables available for validation failure messages
+   *
+   * @var array
+   */
+  protected $_messageVariables = array();
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array();
+  
+  /**
+   * Array of validation failure messages
+   *
+   * @var array
+   */
+  protected $_messages = array();
+  
+  /**
+   * Flag indidcating whether or not value should be obfuscated in error 
+   * messages
+   * @var bool
+   */
+  protected $_obscureValue = false;
+  
+  /**
+   * Array of validation failure message codes
+   *
+   * @var array
+   * @deprecated Since 1.5.0
+   */
+  protected $_errors = array();
+  
+  /**
+   * Translation object
+   * @var Zend_Translate
+   */
+  protected $_translator;
+  
+  /**
+   * Default translation object for all validate objects
+   * @var Zend_Translate
+   */
+  protected static $_defaultTranslator;
+
+  /**
+   * Returns array of validation failure messages
+   *
+   * @return array
+   */
+  public function getMessages() {
+    return $this->_messages;
+  }
+
+  /**
+   * Returns an array of the names of variables that are used in constructing validation failure messages
+   *
+   * @return array
+   */
+  public function getMessageVariables() {
+    return array_keys($this->_messageVariables);
+  }
+
+  /**
+   * Sets the validation failure message template for a particular key
+   *
+   * @param  string $messageString
+   * @param  string $messageKey     OPTIONAL
+   * @return Zend_Validate_Abstract Provides a fluent interface
+   * @throws Zend_Validate_Exception
+   */
+  public function setMessage($messageString, $messageKey = null) {
+    if ($messageKey === null) {
+      $keys = array_keys($this->_messageTemplates);
+      $messageKey = current($keys);
+    }
+    if (! isset($this->_messageTemplates[$messageKey])) {
+      require_once 'external/Zend/Validate/Exception.php';
+      throw new Zend_Validate_Exception("No message template exists for key '$messageKey'");
+    }
+    $this->_messageTemplates[$messageKey] = $messageString;
+    return $this;
+  }
+
+  /**
+   * Sets validation failure message templates given as an array, where the array keys are the message keys,
+   * and the array values are the message template strings.
+   *
+   * @param  array $messages
+   * @return Zend_Validate_Abstract
+   */
+  public function setMessages(array $messages) {
+    foreach ($messages as $key => $message) {
+      $this->setMessage($message, $key);
+    }
+    return $this;
+  }
+
+  /**
+   * Magic function returns the value of the requested property, if and only if it is the value or a
+   * message variable.
+   *
+   * @param  string $property
+   * @return mixed
+   * @throws Zend_Validate_Exception
+   */
+  public function __get($property) {
+    if ($property == 'value') {
+      return $this->_value;
+    }
+    if (array_key_exists($property, $this->_messageVariables)) {
+      return $this->{$this->_messageVariables[$property]};
+    }
+    /**
+     * @see Zend_Validate_Exception
+     */
+    require_once 'external/Zend/Validate/Exception.php';
+    throw new Zend_Validate_Exception("No property exists by the name '$property'");
+  }
+
+  /**
+   * Constructs and returns a validation failure message with the given message key and value.
+   *
+   * Returns null if and only if $messageKey does not correspond to an existing template.
+   *
+   * If a translator is available and a translation exists for $messageKey, 
+   * the translation will be used.
+   *
+   * @param  string $messageKey
+   * @param  string $value
+   * @return string
+   */
+  protected function _createMessage($messageKey, $value) {
+    if (! isset($this->_messageTemplates[$messageKey])) {
+      return null;
+    }
+    
+    $message = $this->_messageTemplates[$messageKey];
+    
+    if (null !== ($translator = $this->getTranslator())) {
+      if ($translator->isTranslated($messageKey)) {
+        $message = $translator->translate($messageKey);
+      }
+    }
+    
+    if ($this->getObscureValue()) {
+      $value = str_repeat('*', strlen($value));
+    }
+    
+    $message = str_replace('%value%', (string)$value, $message);
+    foreach ($this->_messageVariables as $ident => $property) {
+      $message = str_replace("%$ident%", $this->$property, $message);
+    }
+    return $message;
+  }
+
+  /**
+   * @param  string $messageKey OPTIONAL
+   * @param  string $value      OPTIONAL
+   * @return void
+   */
+  protected function _error($messageKey = null, $value = null) {
+    if ($messageKey === null) {
+      $keys = array_keys($this->_messageTemplates);
+      $messageKey = current($keys);
+    }
+    if ($value === null) {
+      $value = $this->_value;
+    }
+    $this->_errors[] = $messageKey;
+    $this->_messages[$messageKey] = $this->_createMessage($messageKey, $value);
+  }
+
+  /**
+   * Sets the value to be validated and clears the messages and errors arrays
+   *
+   * @param  mixed $value
+   * @return void
+   */
+  protected function _setValue($value) {
+    $this->_value = $value;
+    $this->_messages = array();
+    $this->_errors = array();
+  }
+
+  /**
+   * Returns array of validation failure message codes
+   *
+   * @return array
+   * @deprecated Since 1.5.0
+   */
+  public function getErrors() {
+    return $this->_errors;
+  }
+
+  /**
+   * Set flag indicating whether or not value should be obfuscated in messages
+   * 
+   * @param  bool $flag 
+   * @return Zend_Validate_Abstract
+   */
+  public function setObscureValue($flag) {
+    $this->_obscureValue = (bool)$flag;
+    return $this;
+  }
+
+  /**
+   * Retrieve flag indicating whether or not value should be obfuscated in 
+   * messages
+   * 
+   * @return bool
+   */
+  public function getObscureValue() {
+    return $this->_obscureValue;
+  }
+
+  /**
+   * Set translation object
+   * 
+   * @param  Zend_Translate|Zend_Translate_Adapter|null $translator 
+   * @return Zend_Validate_Abstract
+   */
+  public function setTranslator($translator = null) {
+    if ((null === $translator) || ($translator instanceof Zend_Translate_Adapter)) {
+      $this->_translator = $translator;
+    } elseif ($translator instanceof Zend_Translate) {
+      $this->_translator = $translator->getAdapter();
+    } else {
+      require_once 'external/Zend/Validate/Exception.php';
+      throw new Zend_Validate_Exception('Invalid translator specified');
+    }
+    return $this;
+  }
+
+  /**
+   * Return translation object
+   * 
+   * @return Zend_Translate_Adapter|null
+   */
+  public function getTranslator() {
+    if (null === $this->_translator) {
+      return self::getDefaultTranslator();
+    }
+    
+    return $this->_translator;
+  }
+
+  /**
+   * Set default translation object for all validate objects
+   * 
+   * @param  Zend_Translate|Zend_Translate_Adapter|null $translator 
+   * @return void
+   */
+  public static function setDefaultTranslator($translator = null) {
+    if ((null === $translator) || ($translator instanceof Zend_Translate_Adapter)) {
+      self::$_defaultTranslator = $translator;
+    } elseif ($translator instanceof Zend_Translate) {
+      self::$_defaultTranslator = $translator->getAdapter();
+    } else {
+      require_once 'external/Zend/Validate/Exception.php';
+      throw new Zend_Validate_Exception('Invalid translator specified');
+    }
+  }
+
+  /**
+   * Get default translation object for all validate objects
+   * 
+   * @return Zend_Translate_Adapter|null
+   */
+  public static function getDefaultTranslator() {
+    if (null === self::$_defaultTranslator) {
+      require_once 'external/Zend/Registry.php';
+      if (Zend_Registry::isRegistered('Zend_Translate')) {
+        $translator = Zend_Registry::get('Zend_Translate');
+        if ($translator instanceof Zend_Translate_Adapter) {
+          return $translator;
+        } elseif ($translator instanceof Zend_Translate) {
+          return $translator->getAdapter();
+        }
+      }
+    }
+    return self::$_defaultTranslator;
+  }
+}
diff --git a/trunk/php/external/Zend/Validate/Alnum.php b/trunk/php/external/Zend/Validate/Alnum.php
new file mode 100644
index 0000000..5180abe
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Alnum.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Alnum.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Alnum extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value contains non-alphabetic or non-digit characters
+   */
+  const NOT_ALNUM = 'notAlnum';
+  
+  /**
+   * Validation failure message key for when the value is an empty string
+   */
+  const STRING_EMPTY = 'stringEmpty';
+  
+  /**
+   * Whether to allow white space characters; off by default
+   *
+   * @var boolean
+   */
+  public $allowWhiteSpace;
+  
+  /**
+   * Alphanumeric filter used for validation
+   *
+   * @var Zend_Filter_Alnum
+   */
+  protected static $_filter = null;
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(
+      self::NOT_ALNUM => "'%value%' has not only alphabetic and digit characters", 
+      self::STRING_EMPTY => "'%value%' is an empty string");
+
+  /**
+   * Sets default option values for this instance
+   *
+   * @param  boolean $allowWhiteSpace
+   * @return void
+   */
+  public function __construct($allowWhiteSpace = false) {
+    $this->allowWhiteSpace = (boolean)$allowWhiteSpace;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value contains only alphabetic and digit characters
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    if ('' === $valueString) {
+      $this->_error(self::STRING_EMPTY);
+      return false;
+    }
+    
+    if (null === self::$_filter) {
+      /**
+       * @see Zend_Filter_Alnum
+       */
+      require_once 'external/Zend/Filter/Alnum.php';
+      self::$_filter = new Zend_Filter_Alnum();
+    }
+    
+    self::$_filter->allowWhiteSpace = $this->allowWhiteSpace;
+    
+    if ($valueString !== self::$_filter->filter($valueString)) {
+      $this->_error(self::NOT_ALNUM);
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Alpha.php b/trunk/php/external/Zend/Validate/Alpha.php
new file mode 100644
index 0000000..ec8a660
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Alpha.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Alpha.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Alpha extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value contains non-alphabetic characters
+   */
+  const NOT_ALPHA = 'notAlpha';
+  
+  /**
+   * Validation failure message key for when the value is an empty string
+   */
+  const STRING_EMPTY = 'stringEmpty';
+  
+  /**
+   * Whether to allow white space characters; off by default
+   *
+   * @var boolean
+   */
+  public $allowWhiteSpace;
+  
+  /**
+   * Alphabetic filter used for validation
+   *
+   * @var Zend_Filter_Alpha
+   */
+  protected static $_filter = null;
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_ALPHA => "'%value%' has not only alphabetic characters", 
+      self::STRING_EMPTY => "'%value%' is an empty string");
+
+  /**
+   * Sets default option values for this instance
+   *
+   * @param  boolean $allowWhiteSpace
+   * @return void
+   */
+  public function __construct($allowWhiteSpace = false) {
+    $this->allowWhiteSpace = (boolean)$allowWhiteSpace;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value contains only alphabetic characters
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    if ('' === $valueString) {
+      $this->_error(self::STRING_EMPTY);
+      return false;
+    }
+    
+    if (null === self::$_filter) {
+      /**
+       * @see Zend_Filter_Alpha
+       */
+      require_once 'external/Zend/Filter/Alpha.php';
+      self::$_filter = new Zend_Filter_Alpha();
+    }
+    
+    self::$_filter->allowWhiteSpace = $this->allowWhiteSpace;
+    
+    if ($valueString !== self::$_filter->filter($valueString)) {
+      $this->_error(self::NOT_ALPHA);
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Barcode.php b/trunk/php/external/Zend/Validate/Barcode.php
new file mode 100644
index 0000000..26f5354
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Barcode.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Barcode.php 8211 2008-02-20 14:29:24Z darby $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Barcode extends Zend_Validate_Abstract {
+  /**
+   * Barcode validator
+   *
+   * @var Zend_Validate_Abstract
+   */
+  protected $_barcodeValidator;
+
+  /**
+   * Generates the standard validator object
+   *
+   * @param  string $barcodeType - Barcode validator to use
+   * @return void
+   * @throws Zend_Validate_Exception
+   */
+  public function __construct($barcodeType) {
+    $this->setType($barcodeType);
+  }
+
+  /**
+   * Sets a new barcode validator
+   *
+   * @param  string $barcodeType - Barcode validator to use
+   * @return void
+   * @throws Zend_Validate_Exception
+   */
+  public function setType($barcodeType) {
+    switch (strtolower($barcodeType)) {
+      case 'upc':
+      case 'upc-a':
+        $className = 'UpcA';
+        break;
+      case 'ean13':
+      case 'ean-13':
+        $className = 'Ean13';
+        break;
+      default:
+        require_once 'external/Zend/Validate/Exception.php';
+        throw new Zend_Validate_Exception("Barcode type '$barcodeType' is not supported'");
+        break;
+    }
+    
+    require_once 'external/Zend/Validate/Barcode/' . $className . '.php';
+    
+    $class = 'Zend_Validate_Barcode_' . $className;
+    $this->_barcodeValidator = new $class();
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value contains a valid barcode
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    return call_user_func(array($this->_barcodeValidator, 'isValid'), $value);
+  }
+}
diff --git a/trunk/php/external/Zend/Validate/Barcode/Ean13.php b/trunk/php/external/Zend/Validate/Barcode/Ean13.php
new file mode 100644
index 0000000..af6dcf8
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Barcode/Ean13.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Ean13.php 8210 2008-02-20 14:09:05Z andries $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Barcode_Ean13 extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value is
+   * an invalid barcode
+   */
+  const INVALID = 'invalid';
+  
+  /**
+   * Validation failure message key for when the value is
+   * not 13 characters long
+   */
+  const INVALID_LENGTH = 'invalidLength';
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(self::INVALID => "'%value%' is an invalid EAN-13 barcode", 
+      self::INVALID_LENGTH => "'%value%' should be 13 characters");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value contains a valid barcode
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    $this->_setValue($valueString);
+    
+    if (strlen($valueString) !== 13) {
+      $this->_error(self::INVALID_LENGTH);
+      return false;
+    }
+    
+    $barcode = strrev(substr($valueString, 0, - 1));
+    $oddSum = 0;
+    $evenSum = 0;
+    
+    for ($i = 0; $i < 12; $i ++) {
+      if ($i % 2 === 0) {
+        $oddSum += $barcode[$i] * 3;
+      } elseif ($i % 2 === 1) {
+        $evenSum += $barcode[$i];
+      }
+    }
+    
+    $calculation = ($oddSum + $evenSum) % 10;
+    $checksum = ($calculation === 0) ? 0 : 10 - $calculation;
+    
+    if ($valueString[12] != $checksum) {
+      $this->_error(self::INVALID);
+      return false;
+    }
+    
+    return true;
+  }
+}
diff --git a/trunk/php/external/Zend/Validate/Barcode/UpcA.php b/trunk/php/external/Zend/Validate/Barcode/UpcA.php
new file mode 100644
index 0000000..47c7eb3
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Barcode/UpcA.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: UpcA.php 8210 2008-02-20 14:09:05Z andries $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Barcode_UpcA extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value is
+   * an invalid barcode
+   */
+  const INVALID = 'invalid';
+  
+  /**
+   * Validation failure message key for when the value is
+   * not 12 characters long
+   */
+  const INVALID_LENGTH = 'invalidLength';
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(self::INVALID => "'%value%' is an invalid UPC-A barcode", 
+      self::INVALID_LENGTH => "'%value%' should be 12 characters");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value contains a valid barcode
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    $this->_setValue($valueString);
+    
+    if (strlen($valueString) !== 12) {
+      $this->_error(self::INVALID_LENGTH);
+      return false;
+    }
+    
+    $barcode = substr($valueString, 0, - 1);
+    $oddSum = 0;
+    $evenSum = 0;
+    
+    for ($i = 0; $i < 11; $i ++) {
+      if ($i % 2 === 0) {
+        $oddSum += $barcode[$i] * 3;
+      } elseif ($i % 2 === 1) {
+        $evenSum += $barcode[$i];
+      }
+    }
+    
+    $calculation = ($oddSum + $evenSum) % 10;
+    $checksum = ($calculation === 0) ? 0 : 10 - $calculation;
+    
+    if ($valueString[11] != $checksum) {
+      $this->_error(self::INVALID);
+      return false;
+    }
+    
+    return true;
+  }
+}
diff --git a/trunk/php/external/Zend/Validate/Between.php b/trunk/php/external/Zend/Validate/Between.php
new file mode 100644
index 0000000..d50ee2f
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Between.php
@@ -0,0 +1,183 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Between.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Between extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value is not between the min and max, inclusively
+   */
+  const NOT_BETWEEN = 'notBetween';
+  
+  /**
+   * Validation failure message key for when the value is not strictly between the min and max
+   */
+  const NOT_BETWEEN_STRICT = 'notBetweenStrict';
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(
+      self::NOT_BETWEEN => "'%value%' is not between '%min%' and '%max%', inclusively", 
+      self::NOT_BETWEEN_STRICT => "'%value%' is not strictly between '%min%' and '%max%'");
+  
+  /**
+   * Additional variables available for validation failure messages
+   *
+   * @var array
+   */
+  protected $_messageVariables = array('min' => '_min', 'max' => '_max');
+  
+  /**
+   * Minimum value
+   *
+   * @var mixed
+   */
+  protected $_min;
+  
+  /**
+   * Maximum value
+   *
+   * @var mixed
+   */
+  protected $_max;
+  
+  /**
+   * Whether to do inclusive comparisons, allowing equivalence to min and/or max
+   *
+   * If false, then strict comparisons are done, and the value may equal neither
+   * the min nor max options
+   *
+   * @var boolean
+   */
+  protected $_inclusive;
+
+  /**
+   * Sets validator options
+   *
+   * @param  mixed   $min
+   * @param  mixed   $max
+   * @param  boolean $inclusive
+   * @return void
+   */
+  public function __construct($min, $max, $inclusive = true) {
+    $this->setMin($min)->setMax($max)->setInclusive($inclusive);
+  }
+
+  /**
+   * Returns the min option
+   *
+   * @return mixed
+   */
+  public function getMin() {
+    return $this->_min;
+  }
+
+  /**
+   * Sets the min option
+   *
+   * @param  mixed $min
+   * @return Zend_Validate_Between Provides a fluent interface
+   */
+  public function setMin($min) {
+    $this->_min = $min;
+    return $this;
+  }
+
+  /**
+   * Returns the max option
+   *
+   * @return mixed
+   */
+  public function getMax() {
+    return $this->_max;
+  }
+
+  /**
+   * Sets the max option
+   *
+   * @param  mixed $max
+   * @return Zend_Validate_Between Provides a fluent interface
+   */
+  public function setMax($max) {
+    $this->_max = $max;
+    return $this;
+  }
+
+  /**
+   * Returns the inclusive option
+   *
+   * @return boolean
+   */
+  public function getInclusive() {
+    return $this->_inclusive;
+  }
+
+  /**
+   * Sets the inclusive option
+   *
+   * @param  boolean $inclusive
+   * @return Zend_Validate_Between Provides a fluent interface
+   */
+  public function setInclusive($inclusive) {
+    $this->_inclusive = $inclusive;
+    return $this;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is between min and max options, inclusively
+   * if inclusive option is true.
+   *
+   * @param  mixed $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $this->_setValue($value);
+    
+    if ($this->_inclusive) {
+      if ($this->_min > $value || $value > $this->_max) {
+        $this->_error(self::NOT_BETWEEN);
+        return false;
+      }
+    } else {
+      if ($this->_min >= $value || $value >= $this->_max) {
+        $this->_error(self::NOT_BETWEEN_STRICT);
+        return false;
+      }
+    }
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Ccnum.php b/trunk/php/external/Zend/Validate/Ccnum.php
new file mode 100644
index 0000000..2ecef64
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Ccnum.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Ccnum.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Ccnum extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value is not of valid length
+   */
+  const LENGTH = 'ccnumLength';
+  
+  /**
+   * Validation failure message key for when the value fails the mod-10 checksum
+   */
+  const CHECKSUM = 'ccnumChecksum';
+  
+  /**
+   * Digits filter for input
+   *
+   * @var Zend_Filter_Digits
+   */
+  protected static $_filter = null;
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(self::LENGTH => "'%value%' must contain between 13 and 19 digits", 
+      self::CHECKSUM => "Luhn algorithm (mod-10 checksum) failed on '%value%'");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value follows the Luhn algorithm (mod-10 checksum)
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $this->_setValue($value);
+    
+    if (null === self::$_filter) {
+      /**
+       * @see Zend_Filter_Digits
+       */
+      require_once 'external/Zend/Filter/Digits.php';
+      self::$_filter = new Zend_Filter_Digits();
+    }
+    
+    $valueFiltered = self::$_filter->filter($value);
+    
+    $length = strlen($valueFiltered);
+    
+    if ($length < 13 || $length > 19) {
+      $this->_error(self::LENGTH);
+      return false;
+    }
+    
+    $sum = 0;
+    $weight = 2;
+    
+    for ($i = $length - 2; $i >= 0; $i --) {
+      $digit = $weight * $valueFiltered[$i];
+      $sum += floor($digit / 10) + $digit % 10;
+      $weight = $weight % 2 + 1;
+    }
+    
+    if ((10 - $sum % 10) % 10 != $valueFiltered[$length - 1]) {
+      $this->_error(self::CHECKSUM, $valueFiltered);
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Date.php b/trunk/php/external/Zend/Validate/Date.php
new file mode 100644
index 0000000..c9369b3
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Date.php
@@ -0,0 +1,170 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Date.php 8553 2008-03-05 16:39:50Z darby $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Date extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value does not follow the YYYY-MM-DD format
+   */
+  const NOT_YYYY_MM_DD = 'dateNotYYYY-MM-DD';
+  
+  /**
+   * Validation failure message key for when the value does not appear to be a valid date
+   */
+  const INVALID = 'dateInvalid';
+  
+  /**
+   * Validation failure message key for when the value does not fit the given dateformat or locale
+   */
+  const FALSEFORMAT = 'dateFalseFormat';
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_YYYY_MM_DD => "'%value%' is not of the format YYYY-MM-DD", 
+      self::INVALID => "'%value%' does not appear to be a valid date", 
+      self::FALSEFORMAT => "'%value%' does not fit given date format");
+  
+  /**
+   * Optional format
+   *
+   * @var string|null
+   */
+  protected $_format;
+  
+  /**
+   * Optional locale
+   *
+   * @var string|Zend_Locale|null
+   */
+  protected $_locale;
+
+  /**
+   * Sets validator options
+   *
+   * @param  string             $format OPTIONAL
+   * @param  string|Zend_Locale $locale OPTIONAL
+   * @return void
+   */
+  public function __construct($format = null, $locale = null) {
+    $this->setFormat($format);
+    $this->setLocale($locale);
+  }
+
+  /**
+   * Returns the locale option
+   *
+   * @return string|Zend_Locale|null
+   */
+  public function getLocale() {
+    return $this->_locale;
+  }
+
+  /**
+   * Sets the locale option
+   *
+   * @param  string|Zend_Locale $locale
+   * @return Zend_Validate_Date provides a fluent interface
+   */
+  public function setLocale($locale = null) {
+    if ($locale !== null) {
+      require_once 'external/Zend/Locale.php';
+      if (! Zend_Locale::isLocale($locale)) {
+        require_once 'external/Zend/Validate/Exception.php';
+        throw new Zend_Validate_Exception("The locale '$locale' is no known locale");
+      }
+    }
+    $this->_locale = $locale;
+    return $this;
+  }
+
+  /**
+   * Returns the locale option
+   *
+   * @return string|null
+   */
+  public function getFormat() {
+    return $this->_format;
+  }
+
+  /**
+   * Sets the format option
+   *
+   * @param  string $format
+   * @return Zend_Validate_Date provides a fluent interface
+   */
+  public function setFormat($format = null) {
+    $this->_format = $format;
+    return $this;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if $value is a valid date of the format YYYY-MM-DD
+   * If optional $format or $locale is set the date format is checked
+   * according to Zend_Date, see Zend_Date::isDate()
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    if (($this->_format !== null) or ($this->_locale !== null)) {
+      require_once 'external/Zend/Date.php';
+      if (! Zend_Date::isDate($value, $this->_format, $this->_locale)) {
+        $this->_error(self::FALSEFORMAT);
+        return false;
+      }
+    } else {
+      if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $valueString)) {
+        $this->_error(self::NOT_YYYY_MM_DD);
+        return false;
+      }
+      
+      list($year, $month, $day) = sscanf($valueString, '%d-%d-%d');
+      
+      if (! checkdate($month, $day, $year)) {
+        $this->_error(self::INVALID);
+        return false;
+      }
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Digits.php b/trunk/php/external/Zend/Validate/Digits.php
new file mode 100644
index 0000000..c8ddd8b
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Digits.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Digits.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Digits extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value contains non-digit characters
+   */
+  const NOT_DIGITS = 'notDigits';
+  
+  /**
+   * Validation failure message key for when the value is an empty string
+   */
+  const STRING_EMPTY = 'stringEmpty';
+  
+  /**
+   * Digits filter used for validation
+   *
+   * @var Zend_Filter_Digits
+   */
+  protected static $_filter = null;
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_DIGITS => "'%value%' contains not only digit characters", 
+      self::STRING_EMPTY => "'%value%' is an empty string");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value only contains digit characters
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    if ('' === $valueString) {
+      $this->_error(self::STRING_EMPTY);
+      return false;
+    }
+    
+    if (null === self::$_filter) {
+      /**
+       * @see Zend_Filter_Digits
+       */
+      require_once 'external/Zend/Filter/Digits.php';
+      self::$_filter = new Zend_Filter_Digits();
+    }
+    
+    if ($valueString !== self::$_filter->filter($valueString)) {
+      $this->_error(self::NOT_DIGITS);
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/EmailAddress.php b/trunk/php/external/Zend/Validate/EmailAddress.php
new file mode 100644
index 0000000..7851131
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/EmailAddress.php
@@ -0,0 +1,232 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: EmailAddress.php 8986 2008-03-21 21:38:32Z matthew $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @see Zend_Validate_Hostname
+ */
+require_once 'external/Zend/Validate/Hostname.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_EmailAddress extends Zend_Validate_Abstract {
+  
+  const INVALID = 'emailAddressInvalid';
+  const INVALID_HOSTNAME = 'emailAddressInvalidHostname';
+  const INVALID_MX_RECORD = 'emailAddressInvalidMxRecord';
+  const DOT_ATOM = 'emailAddressDotAtom';
+  const QUOTED_STRING = 'emailAddressQuotedString';
+  const INVALID_LOCAL_PART = 'emailAddressInvalidLocalPart';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(
+      self::INVALID => "'%value%' is not a valid email address in the basic format local-part@hostname", 
+      self::INVALID_HOSTNAME => "'%hostname%' is not a valid hostname for email address '%value%'", 
+      self::INVALID_MX_RECORD => "'%hostname%' does not appear to have a valid MX record for the email address '%value%'", 
+      self::DOT_ATOM => "'%localPart%' not matched against dot-atom format", 
+      self::QUOTED_STRING => "'%localPart%' not matched against quoted-string format", 
+      self::INVALID_LOCAL_PART => "'%localPart%' is not a valid local part for email address '%value%'");
+  
+  /**
+   * @var array
+   */
+  protected $_messageVariables = array('hostname' => '_hostname', 'localPart' => '_localPart');
+  
+  /**
+   * Local object for validating the hostname part of an email address
+   *
+   * @var Zend_Validate_Hostname
+   */
+  public $hostnameValidator;
+  
+  /**
+   * Whether we check for a valid MX record via DNS
+   *
+   * @var boolean
+   */
+  protected $_validateMx = false;
+  
+  /**
+   * @var string
+   */
+  protected $_hostname;
+  
+  /**
+   * @var string
+   */
+  protected $_localPart;
+
+  /**
+   * Instantiates hostname validator for local use
+   *
+   * You can pass a bitfield to determine what types of hostnames are allowed.
+   * These bitfields are defined by the ALLOW_* constants in Zend_Validate_Hostname
+   * The default is to allow DNS hostnames only
+   *
+   * @param integer                $allow             OPTIONAL
+   * @param bool                   $validateMx        OPTIONAL
+   * @param Zend_Validate_Hostname $hostnameValidator OPTIONAL
+   * @return void
+   */
+  public function __construct($allow = Zend_Validate_Hostname::ALLOW_DNS, $validateMx = false, Zend_Validate_Hostname $hostnameValidator = null) {
+    $this->setValidateMx($validateMx);
+    $this->setHostnameValidator($hostnameValidator, $allow);
+  }
+
+  /**
+   * @param Zend_Validate_Hostname $hostnameValidator OPTIONAL
+   * @param int                    $allow             OPTIONAL
+   * @return void
+   */
+  public function setHostnameValidator(Zend_Validate_Hostname $hostnameValidator = null, $allow = Zend_Validate_Hostname::ALLOW_DNS) {
+    if ($hostnameValidator === null) {
+      $hostnameValidator = new Zend_Validate_Hostname($allow);
+    }
+    $this->hostnameValidator = $hostnameValidator;
+  }
+
+  /**
+   * Whether MX checking via dns_get_mx is supported or not
+   *
+   * This currently only works on UNIX systems
+   *
+   * @return boolean
+   */
+  public function validateMxSupported() {
+    return function_exists('dns_get_mx');
+  }
+
+  /**
+   * Set whether we check for a valid MX record via DNS
+   *
+   * This only applies when DNS hostnames are validated
+   *
+   * @param boolean $allowed Set allowed to true to validate for MX records, and false to not validate them
+   */
+  public function setValidateMx($allowed) {
+    $this->_validateMx = (bool)$allowed;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is a valid email address
+   * according to RFC2822
+   *
+   * @link   http://www.ietf.org/rfc/rfc2822.txt RFC2822
+   * @link   http://www.columbia.edu/kermit/ascii.html US-ASCII characters
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    // Split email address up
+    if (! preg_match('/^(.+)@([^@]+)$/', $valueString, $matches)) {
+      $this->_error(self::INVALID);
+      return false;
+    }
+    
+    $this->_localPart = $matches[1];
+    $this->_hostname = $matches[2];
+    
+    // Match hostname part
+    $hostnameResult = $this->hostnameValidator->setTranslator($this->getTranslator())->isValid($this->_hostname);
+    if (! $hostnameResult) {
+      $this->_error(self::INVALID_HOSTNAME);
+      
+      // Get messages and errors from hostnameValidator
+      foreach ($this->hostnameValidator->getMessages() as $message) {
+        $this->_messages[] = $message;
+      }
+      foreach ($this->hostnameValidator->getErrors() as $error) {
+        $this->_errors[] = $error;
+      }
+    }
+    
+    // MX check on hostname via dns_get_record()
+    if ($this->_validateMx) {
+      if ($this->validateMxSupported()) {
+        $result = dns_get_mx($this->_hostname, $mxHosts);
+        if (count($mxHosts) < 1) {
+          $hostnameResult = false;
+          $this->_error(self::INVALID_MX_RECORD);
+        }
+      } else {
+        /**
+         * MX checks are not supported by this system
+         * @see Zend_Validate_Exception
+         */
+        require_once 'external/Zend/Validate/Exception.php';
+        throw new Zend_Validate_Exception('Internal error: MX checking not available on this system');
+      }
+    }
+    
+    // First try to match the local part on the common dot-atom format
+    $localResult = false;
+    
+    // Dot-atom characters are: 1*atext *("." 1*atext)
+    // atext: ALPHA / DIGIT / and "!", "#", "$", "%", "&", "'", "*",
+    //        "-", "/", "=", "?", "^", "_", "`", "{", "|", "}", "~"
+    $atext = 'a-zA-Z0-9\x21\x23\x24\x25\x26\x27\x2a\x2b\x2d\x2f\x3d\x3f\x5e\x5f\x60\x7b\x7c\x7d';
+    if (preg_match('/^[' . $atext . ']+(\x2e+[' . $atext . ']+)*$/', $this->_localPart)) {
+      $localResult = true;
+    } else {
+      // Try quoted string format
+      
+
+      // Quoted-string characters are: DQUOTE *([FWS] qtext/quoted-pair) [FWS] DQUOTE
+      // qtext: Non white space controls, and the rest of the US-ASCII characters not
+      //   including "\" or the quote character
+      $noWsCtl = '\x01-\x08\x0b\x0c\x0e-\x1f\x7f';
+      $qtext = $noWsCtl . '\x21\x23-\x5b\x5d-\x7e';
+      $ws = '\x20\x09';
+      if (preg_match('/^\x22([' . $ws . $qtext . '])*[$ws]?\x22$/', $this->_localPart)) {
+        $localResult = true;
+      } else {
+        $this->_error(self::DOT_ATOM);
+        $this->_error(self::QUOTED_STRING);
+        $this->_error(self::INVALID_LOCAL_PART);
+      }
+    }
+    
+    // If both parts valid, return true
+    if ($localResult && $hostnameResult) {
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Exception.php b/trunk/php/external/Zend/Validate/Exception.php
new file mode 100644
index 0000000..bc0de7e
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Exception.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Exception.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Exception
+ */
+require_once 'external/Zend/Exception.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Exception extends Zend_Exception {
+}
diff --git a/trunk/php/external/Zend/Validate/Float.php b/trunk/php/external/Zend/Validate/Float.php
new file mode 100644
index 0000000..bba1877
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Float.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Float.php 8910 2008-03-19 20:19:23Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Float extends Zend_Validate_Abstract {
+  
+  const NOT_FLOAT = 'notFloat';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_FLOAT => "'%value%' does not appear to be a float");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is a floating-point value
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    $locale = localeconv();
+    
+    $valueFiltered = str_replace($locale['thousands_sep'], '', $valueString);
+    $valueFiltered = str_replace($locale['decimal_point'], '.', $valueFiltered);
+    
+    if (strval(floatval($valueFiltered)) != $valueFiltered) {
+      $this->_error();
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/GreaterThan.php b/trunk/php/external/Zend/Validate/GreaterThan.php
new file mode 100644
index 0000000..b682c3f
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/GreaterThan.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: GreaterThan.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_GreaterThan extends Zend_Validate_Abstract {
+  
+  const NOT_GREATER = 'notGreaterThan';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_GREATER => "'%value%' is not greater than '%min%'");
+  
+  /**
+   * @var array
+   */
+  protected $_messageVariables = array('min' => '_min');
+  
+  /**
+   * Minimum value
+   *
+   * @var mixed
+   */
+  protected $_min;
+
+  /**
+   * Sets validator options
+   *
+   * @param  mixed $min
+   * @return void
+   */
+  public function __construct($min) {
+    $this->setMin($min);
+  }
+
+  /**
+   * Returns the min option
+   *
+   * @return mixed
+   */
+  public function getMin() {
+    return $this->_min;
+  }
+
+  /**
+   * Sets the min option
+   *
+   * @param  mixed $min
+   * @return Zend_Validate_GreaterThan Provides a fluent interface
+   */
+  public function setMin($min) {
+    $this->_min = $min;
+    return $this;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is greater than min option
+   *
+   * @param  mixed $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $this->_setValue($value);
+    
+    if ($this->_min >= $value) {
+      $this->_error();
+      return false;
+    }
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hex.php b/trunk/php/external/Zend/Validate/Hex.php
new file mode 100644
index 0000000..99e9897
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hex.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Hex.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hex extends Zend_Validate_Abstract {
+  /**
+   * Validation failure message key for when the value contains characters other than hexadecimal digits
+   */
+  const NOT_HEX = 'notHex';
+  
+  /**
+   * Validation failure message template definitions
+   *
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_HEX => "'%value%' has not only hexadecimal digit characters");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value contains only hexadecimal digit characters
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    if (! ctype_xdigit($valueString)) {
+      $this->_error();
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname.php b/trunk/php/external/Zend/Validate/Hostname.php
new file mode 100644
index 0000000..66ad42a
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname.php
@@ -0,0 +1,425 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Hostname.php 8986 2008-03-21 21:38:32Z matthew $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @see Zend_Loader
+ */
+require_once 'external/Zend/Loader.php';
+
+/**
+ * @see Zend_Validate_Ip
+ */
+require_once 'external/Zend/Validate/Ip.php';
+
+/**
+ * Please note there are two standalone test scripts for testing IDN characters due to problems
+ * with file encoding.
+ *
+ * The first is tests/Zend/Validate/HostnameTestStandalone.php which is designed to be run on
+ * the command line.
+ *
+ * The second is tests/Zend/Validate/HostnameTestForm.php which is designed to be run via HTML
+ * to allow users to test entering UTF-8 characters in a form.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname extends Zend_Validate_Abstract {
+  
+  const IP_ADDRESS_NOT_ALLOWED = 'hostnameIpAddressNotAllowed';
+  const UNKNOWN_TLD = 'hostnameUnknownTld';
+  const INVALID_DASH = 'hostnameDashCharacter';
+  const INVALID_HOSTNAME_SCHEMA = 'hostnameInvalidHostnameSchema';
+  const UNDECIPHERABLE_TLD = 'hostnameUndecipherableTld';
+  const INVALID_HOSTNAME = 'hostnameInvalidHostname';
+  const INVALID_LOCAL_NAME = 'hostnameInvalidLocalName';
+  const LOCAL_NAME_NOT_ALLOWED = 'hostnameLocalNameNotAllowed';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(
+      self::IP_ADDRESS_NOT_ALLOWED => "'%value%' appears to be an IP address, but IP addresses are not allowed", 
+      self::UNKNOWN_TLD => "'%value%' appears to be a DNS hostname but cannot match TLD against known list", 
+      self::INVALID_DASH => "'%value%' appears to be a DNS hostname but contains a dash (-) in an invalid position", 
+      self::INVALID_HOSTNAME_SCHEMA => "'%value%' appears to be a DNS hostname but cannot match against hostname schema for TLD '%tld%'", 
+      self::UNDECIPHERABLE_TLD => "'%value%' appears to be a DNS hostname but cannot extract TLD part", 
+      self::INVALID_HOSTNAME => "'%value%' does not match the expected structure for a DNS hostname", 
+      self::INVALID_LOCAL_NAME => "'%value%' does not appear to be a valid local network name", 
+      self::LOCAL_NAME_NOT_ALLOWED => "'%value%' appears to be a local network name but local network names are not allowed");
+  
+  /**
+   * @var array
+   */
+  protected $_messageVariables = array('tld' => '_tld');
+  
+  /**
+   * Allows Internet domain names (e.g., example.com)
+   */
+  const ALLOW_DNS = 1;
+  
+  /**
+   * Allows IP addresses
+   */
+  const ALLOW_IP = 2;
+  
+  /**
+   * Allows local network names (e.g., localhost, www.localdomain)
+   */
+  const ALLOW_LOCAL = 4;
+  
+  /**
+   * Allows all types of hostnames
+   */
+  const ALLOW_ALL = 7;
+  
+  /**
+   * Whether IDN domains are validated
+   *
+   * @var boolean
+   */
+  private $_validateIdn = true;
+  
+  /**
+   * Whether TLDs are validated against a known list
+   *
+   * @var boolean
+   */
+  private $_validateTld = true;
+  
+  /**
+   * Bit field of ALLOW constants; determines which types of hostnames are allowed
+   *
+   * @var integer
+   */
+  protected $_allow;
+  
+  /**
+   * Bit field of CHECK constants; determines what additional hostname checks to make
+   *
+   * @var unknown_type
+   */
+  // protected $_check;
+  
+
+  /**
+   * Array of valid top-level-domains
+   *
+   * @var array
+   * @see ftp://data.iana.org/TLD/tlds-alpha-by-domain.txt  List of all TLDs by domain
+   */
+  protected $_validTlds = array('ac', 'ad', 'ae', 'aero', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq', 'ar', 
+      'arpa', 'as', 'asia', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 
+      'bh', 'bi', 'biz', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 
+      'cat', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'com', 'coop', 
+      'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'edu', 'ee', 
+      'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge', 
+      'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gov', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 
+      'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'info', 'int', 
+      'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo', 'jobs', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 
+      'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 
+      'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mil', 'mk', 'ml', 'mm', 'mn', 'mo', 
+      'mobi', 'mp', 'mq', 'mr', 'ms', 'mt', 'mu', 'museum', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 
+      'name', 'nc', 'ne', 'net', 'nf', 'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 
+      'org', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl', 'pm', 'pn', 'pr', 'pro', 'ps', 'pt', 'pw', 
+      'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 
+      'sj', 'sk', 'sl', 'sm', 'sn', 'so', 'sr', 'st', 'su', 'sv', 'sy', 'sz', 'tc', 'td', 'tel', 
+      'tf', 'tg', 'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'travel', 'tt', 'tv', 
+      'tw', 'tz', 'ua', 'ug', 'uk', 'um', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 
+      'vu', 'wf', 'ws', 'ye', 'yt', 'yu', 'za', 'zm', 'zw');
+  
+  /**
+   * @var string
+   */
+  protected $_tld;
+
+  /**
+   * Sets validator options
+   *
+   * @param integer          $allow       OPTIONAL Set what types of hostname to allow (default ALLOW_DNS)
+   * @param boolean          $validateIdn OPTIONAL Set whether IDN domains are validated (default true)
+   * @param boolean          $validateTld OPTIONAL Set whether the TLD element of a hostname is validated (default true)
+   * @param Zend_Validate_Ip $ipValidator OPTIONAL
+   * @return void
+   * @see http://www.iana.org/cctld/specifications-policies-cctlds-01apr02.htm  Technical Specifications for ccTLDs
+   */
+  public function __construct($allow = self::ALLOW_DNS, $validateIdn = true, $validateTld = true, Zend_Validate_Ip $ipValidator = null) {
+    // Set allow options
+    $this->setAllow($allow);
+    
+    // Set validation options
+    $this->_validateIdn = $validateIdn;
+    $this->_validateTld = $validateTld;
+    
+    $this->setIpValidator($ipValidator);
+  }
+
+  /**
+   * @param Zend_Validate_Ip $ipValidator OPTIONAL
+   * @return void;
+   */
+  public function setIpValidator(Zend_Validate_Ip $ipValidator = null) {
+    if ($ipValidator === null) {
+      $ipValidator = new Zend_Validate_Ip();
+    }
+    $this->_ipValidator = $ipValidator;
+  }
+
+  /**
+   * Returns the allow option
+   *
+   * @return integer
+   */
+  public function getAllow() {
+    return $this->_allow;
+  }
+
+  /**
+   * Sets the allow option
+   *
+   * @param  integer $allow
+   * @return Zend_Validate_Hostname Provides a fluent interface
+   */
+  public function setAllow($allow) {
+    $this->_allow = $allow;
+    return $this;
+  }
+
+  /**
+   * Set whether IDN domains are validated
+   *
+   * This only applies when DNS hostnames are validated
+   *
+   * @param boolean $allowed Set allowed to true to validate IDNs, and false to not validate them
+   */
+  public function setValidateIdn($allowed) {
+    $this->_validateIdn = (bool)$allowed;
+  }
+
+  /**
+   * Set whether the TLD element of a hostname is validated
+   *
+   * This only applies when DNS hostnames are validated
+   *
+   * @param boolean $allowed Set allowed to true to validate TLDs, and false to not validate them
+   */
+  public function setValidateTld($allowed) {
+    $this->_validateTld = (bool)$allowed;
+  }
+
+  /**
+   * Sets the check option
+   *
+   * @param  integer $check
+   * @return Zend_Validate_Hostname Provides a fluent interface
+   */
+  /*
+    public function setCheck($check)
+    {
+        $this->_check = $check;
+        return $this;
+    }
+     */
+  
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if the $value is a valid hostname with respect to the current allow option
+   *
+   * @param  string $value
+   * @throws Zend_Validate_Exception if a fatal error occurs for validation process
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    // Check input against IP address schema
+    if ($this->_ipValidator->setTranslator($this->getTranslator())->isValid($valueString)) {
+      if (! ($this->_allow & self::ALLOW_IP)) {
+        $this->_error(self::IP_ADDRESS_NOT_ALLOWED);
+        return false;
+      } else {
+        return true;
+      }
+    }
+    
+    // Check input against DNS hostname schema
+    $domainParts = explode('.', $valueString);
+    if ((count($domainParts) > 1) && (strlen($valueString) >= 4) && (strlen($valueString) <= 254)) {
+      $status = false;
+      
+      do {
+        // First check TLD
+        if (preg_match('/([a-z]{2,10})$/i', end($domainParts), $matches)) {
+          
+          reset($domainParts);
+          
+          // Hostname characters are: *(label dot)(label dot label); max 254 chars
+          // label: id-prefix [*ldh{61} id-prefix]; max 63 chars
+          // id-prefix: alpha / digit
+          // ldh: alpha / digit / dash
+          
+
+          // Match TLD against known list
+          $this->_tld = strtolower($matches[1]);
+          if ($this->_validateTld) {
+            if (! in_array($this->_tld, $this->_validTlds)) {
+              $this->_error(self::UNKNOWN_TLD);
+              $status = false;
+              break;
+            }
+          }
+          
+          /**
+           * Match against IDN hostnames
+           * @see Zend_Validate_Hostname_Interface
+           */
+          $labelChars = 'a-z0-9';
+          $utf8 = false;
+          $classFile = 'Zend/Validate/Hostname/' . ucfirst($this->_tld) . '.php';
+          if ($this->_validateIdn) {
+            if (Zend_Loader::isReadable($classFile)) {
+              
+              // Load additional characters
+              $className = 'Zend_Validate_Hostname_' . ucfirst($this->_tld);
+              Zend_Loader::loadClass($className);
+              $labelChars .= call_user_func(array($className, 
+                  'getCharacters'));
+              $utf8 = true;
+            }
+          }
+          
+          // Keep label regex short to avoid issues with long patterns when matching IDN hostnames
+          $regexLabel = '/^[' . $labelChars . '\x2d]{1,63}$/i';
+          if ($utf8) {
+            $regexLabel .= 'u';
+          }
+          
+          // Check each hostname part
+          $valid = true;
+          foreach ($domainParts as $domainPart) {
+            
+            // Check dash (-) does not start, end or appear in 3rd and 4th positions
+            if (strpos($domainPart, '-') === 0 || (strlen($domainPart) > 2 && strpos($domainPart, '-', 2) == 2 && strpos($domainPart, '-', 3) == 3) || strrpos($domainPart, '-') === strlen($domainPart) - 1) {
+              
+              $this->_error(self::INVALID_DASH);
+              $status = false;
+              break 2;
+            }
+            
+            // Check each domain part
+            $status = @preg_match($regexLabel, $domainPart);
+            if ($status === false) {
+              /**
+               * Regex error
+               * @see Zend_Validate_Exception
+               */
+              require_once 'external/Zend/Validate/Exception.php';
+              throw new Zend_Validate_Exception('Internal error: DNS validation failed');
+            } elseif ($status === 0) {
+              $valid = false;
+            }
+          }
+          
+          // If all labels didn't match, the hostname is invalid
+          if (! $valid) {
+            $this->_error(self::INVALID_HOSTNAME_SCHEMA);
+            $status = false;
+          }
+        
+        } else {
+          // Hostname not long enough
+          $this->_error(self::UNDECIPHERABLE_TLD);
+          $status = false;
+        }
+      } while (false);
+      
+      // If the input passes as an Internet domain name, and domain names are allowed, then the hostname
+      // passes validation
+      if ($status && ($this->_allow & self::ALLOW_DNS)) {
+        return true;
+      }
+    } else {
+      $this->_error(self::INVALID_HOSTNAME);
+    }
+    
+    // Check input against local network name schema; last chance to pass validation
+    $regexLocal = '/^(([a-zA-Z0-9\x2d]{1,63}\x2e)*[a-zA-Z0-9\x2d]{1,63}){1,254}$/';
+    $status = @preg_match($regexLocal, $valueString);
+    if (false === $status) {
+      /**
+       * Regex error
+       * @see Zend_Validate_Exception
+       */
+      require_once 'external/Zend/Validate/Exception.php';
+      throw new Zend_Validate_Exception('Internal error: local network name validation failed');
+    }
+    
+    // If the input passes as a local network name, and local network names are allowed, then the
+    // hostname passes validation
+    $allowLocal = $this->_allow & self::ALLOW_LOCAL;
+    if ($status && $allowLocal) {
+      return true;
+    }
+    
+    // If the input does not pass as a local network name, add a message
+    if (! $status) {
+      $this->_error(self::INVALID_LOCAL_NAME);
+    }
+    
+    // If local network names are not allowed, add a message
+    if (! $allowLocal) {
+      $this->_error(self::LOCAL_NAME_NOT_ALLOWED);
+    }
+    
+    return false;
+  }
+
+/**
+ * Throws an exception if a regex for $type does not exist
+ *
+ * @param  string $type
+ * @throws Zend_Validate_Exception
+ * @return Zend_Validate_Hostname Provides a fluent interface
+ */
+/*
+    protected function _checkRegexType($type)
+    {
+        if (!isset($this->_regex[$type])) {
+            require_once 'external/Zend/Validate/Exception.php';
+            throw new Zend_Validate_Exception("'$type' must be one of ('" . implode(', ', array_keys($this->_regex))
+                                            . "')");
+        }
+        return $this;
+    }
+     */
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/At.php b/trunk/php/external/Zend/Validate/Hostname/At.php
new file mode 100644
index 0000000..a74a3c1
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/At.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: At.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_At implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see http://www.nic.at/en/service/technical_information/idn/charset_converter/ Austria (.AT)
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x{00EO}-\x{00F6}\x{00F8}-\x{00FF}\x{0153}\x{0161}\x{017E}';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/Ch.php b/trunk/php/external/Zend/Validate/Hostname/Ch.php
new file mode 100644
index 0000000..fbd4a95
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/Ch.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Ch.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_Ch implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see https://nic.switch.ch/reg/ocView.action?res=EF6GW2JBPVTG67DLNIQXU234MN6SC33JNQQGI7L6#anhang1 Switzerland (.CH)
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x{00EO}-\x{00F6}\x{00F8}-\x{00FF}\x{0153}';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/De.php b/trunk/php/external/Zend/Validate/Hostname/De.php
new file mode 100644
index 0000000..eafd35a
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/De.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: De.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_De implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see http://www.denic.de/en/domains/idns/liste.html Germany (.DE) alllowed characters
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x{00E1}\x{00E0}\x{0103}\x{00E2}\x{00E5}\x{00E4}\x{00E3}\x{0105}\x{0101}\x{00E6}\x{0107}' . '\x{0109}\x{010D}\x{010B}\x{00E7}\x{010F}\x{0111}\x{00E9}\x{00E8}\x{0115}\x{00EA}\x{011B}' . '\x{00EB}\x{0117}\x{0119}\x{0113}\x{011F}\x{011D}\x{0121}\x{0123}\x{0125}\x{0127}\x{00ED}' . '\x{00EC}\x{012D}\x{00EE}\x{00EF}\x{0129}\x{012F}\x{012B}\x{0131}\x{0135}\x{0137}\x{013A}' . '\x{013E}\x{013C}\x{0142}\x{0144}\x{0148}\x{00F1}\x{0146}\x{014B}\x{00F3}\x{00F2}\x{014F}' . '\x{00F4}\x{00F6}\x{0151}\x{00F5}\x{00F8}\x{014D}\x{0153}\x{0138}\x{0155}\x{0159}\x{0157}' . '\x{015B}\x{015D}\x{0161}\x{015F}\x{0165}\x{0163}\x{0167}\x{00FA}\x{00F9}\x{016D}\x{00FB}' . '\x{016F}\x{00FC}\x{0171}\x{0169}\x{0173}\x{016B}\x{0175}\x{00FD}\x{0177}\x{00FF}\x{017A}' . '\x{017E}\x{017C}\x{00F0}\x{00FE}';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/Fi.php b/trunk/php/external/Zend/Validate/Hostname/Fi.php
new file mode 100644
index 0000000..0205ae5
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/Fi.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Fi.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_Fi implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see http://www.ficora.fi/en/index/palvelut/fiverkkotunnukset/aakkostenkaytto.html Finland (.FI)
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x{00E5}\x{00E4}\x{00F6}';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/Hu.php b/trunk/php/external/Zend/Validate/Hostname/Hu.php
new file mode 100644
index 0000000..b61feb3
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/Hu.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Hu.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_Hu implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see http://www.domain.hu/domain/English/szabalyzat.html Hungary (.HU)
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x{00E1}\x{00E9}\x{00ED}\x{00F3}\x{00F6}\x{0151}\x{00FA}\x{00FC}\x{0171}';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/Interface.php b/trunk/php/external/Zend/Validate/Hostname/Interface.php
new file mode 100644
index 0000000..7691918
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/Interface.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Interface.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * UTF-8 characters should be written as four character hex codes \x{XXXX}
+   * For example é (lowercase e with acute) is represented by the hex code \x{00E9}
+   *
+   * You only need to include lower-case equivalents of characters since the hostname
+   * check is case-insensitive
+   *
+   * Please document the supported TLDs in the documentation file at:
+   * manual/en/module_specs/Zend_Validate-Hostname.xml
+   *
+   * @see http://en.wikipedia.org/wiki/Internationalized_domain_name
+   * @see http://www.iana.org/cctld/ Country-Code Top-Level Domains (TLDs)
+   * @see http://www.columbia.edu/kermit/utf8-t1.html UTF-8 characters
+   * @return string
+   */
+  static function getCharacters();
+
+}
\ No newline at end of file
diff --git a/trunk/php/external/Zend/Validate/Hostname/Li.php b/trunk/php/external/Zend/Validate/Hostname/Li.php
new file mode 100644
index 0000000..6b7da3b
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/Li.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Li.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_Li implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see https://nic.switch.ch/reg/ocView.action?res=EF6GW2JBPVTG67DLNIQXU234MN6SC33JNQQGI7L6#anhang1 Liechtenstein (.LI)
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x{00EO}-\x{00F6}\x{00F8}-\x{00FF}\x{0153}';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/No.php b/trunk/php/external/Zend/Validate/Hostname/No.php
new file mode 100644
index 0000000..d047d49
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/No.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: No.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_No implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see http://www.norid.no/domeneregistrering/idn/idn_nyetegn.en.html Norway (.NO)
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x00E1\x00E0\x00E4\x010D\x00E7\x0111\x00E9\x00E8\x00EA\x\x014B' . '\x0144\x00F1\x00F3\x00F2\x00F4\x00F6\x0161\x0167\x00FC\x017E\x00E6' . '\x00F8\x00E5';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Hostname/Se.php b/trunk/php/external/Zend/Validate/Hostname/Se.php
new file mode 100644
index 0000000..fa94803
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Hostname/Se.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Se.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Hostname_Interface
+ */
+require_once 'external/Zend/Validate/Hostname/Interface.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Hostname_Se implements Zend_Validate_Hostname_Interface {
+
+  /**
+   * Returns UTF-8 characters allowed in DNS hostnames for the specified Top-Level-Domain
+   *
+   * @see http://www.iis.se/english/IDN_campaignsite.shtml?lang=en Sweden (.SE)
+   * @return string
+   */
+  static function getCharacters() {
+    return '\x{00E5}\x{00E4}\x{00F6}\x{00FC}\x{00E9}';
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Identical.php b/trunk/php/external/Zend/Validate/Identical.php
new file mode 100644
index 0000000..f564e8b
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Identical.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Identical.php 8118 2008-02-18 16:10:32Z matthew $
+ */
+
+/** Zend_Validate_Abstract */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Identical extends Zend_Validate_Abstract {
+  /**#@+
+   * Error codes
+   * @const string
+   */
+  const NOT_SAME = 'notSame';
+  const MISSING_TOKEN = 'missingToken';
+  /**#@-*/
+  
+  /**
+   * Error messages
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_SAME => 'Tokens do not match', 
+      self::MISSING_TOKEN => 'No token was provided to match against');
+  
+  /**
+   * Original token against which to validate
+   * @var string
+   */
+  protected $_token;
+
+  /**
+   * Sets validator options
+   *
+   * @param  string $token
+   * @return void
+   */
+  public function __construct($token = null) {
+    if (null !== $token) {
+      $this->setToken($token);
+    }
+  }
+
+  /**
+   * Set token against which to compare
+   * 
+   * @param  string $token 
+   * @return Zend_Validate_Identical
+   */
+  public function setToken($token) {
+    $this->_token = (string)$token;
+    return $this;
+  }
+
+  /**
+   * Retrieve token
+   * 
+   * @return string
+   */
+  public function getToken() {
+    return $this->_token;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if a token has been set and the provided value 
+   * matches that token.
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $this->_setValue($value);
+    $token = $this->getToken();
+    
+    if (empty($token)) {
+      $this->_error(self::MISSING_TOKEN);
+      return false;
+    }
+    
+    if ($value !== $token) {
+      $this->_error(self::NOT_SAME);
+      return false;
+    }
+    
+    return true;
+  }
+}
diff --git a/trunk/php/external/Zend/Validate/InArray.php b/trunk/php/external/Zend/Validate/InArray.php
new file mode 100644
index 0000000..9bbb907
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/InArray.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: InArray.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_InArray extends Zend_Validate_Abstract {
+  
+  const NOT_IN_ARRAY = 'notInArray';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_IN_ARRAY => "'%value%' was not found in the haystack");
+  
+  /**
+   * Haystack of possible values
+   *
+   * @var array
+   */
+  protected $_haystack;
+  
+  /**
+   * Whether a strict in_array() invocation is used
+   *
+   * @var boolean
+   */
+  protected $_strict;
+
+  /**
+   * Sets validator options
+   *
+   * @param  array   $haystack
+   * @param  boolean $strict
+   * @return void
+   */
+  public function __construct(array $haystack, $strict = false) {
+    $this->setHaystack($haystack)->setStrict($strict);
+  }
+
+  /**
+   * Returns the haystack option
+   *
+   * @return mixed
+   */
+  public function getHaystack() {
+    return $this->_haystack;
+  }
+
+  /**
+   * Sets the haystack option
+   *
+   * @param  mixed $haystack
+   * @return Zend_Validate_InArray Provides a fluent interface
+   */
+  public function setHaystack(array $haystack) {
+    $this->_haystack = $haystack;
+    return $this;
+  }
+
+  /**
+   * Returns the strict option
+   *
+   * @return boolean
+   */
+  public function getStrict() {
+    return $this->_strict;
+  }
+
+  /**
+   * Sets the strict option
+   *
+   * @param  boolean $strict
+   * @return Zend_Validate_InArray Provides a fluent interface
+   */
+  public function setStrict($strict) {
+    $this->_strict = $strict;
+    return $this;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is contained in the haystack option. If the strict
+   * option is true, then the type of $value is also checked.
+   *
+   * @param  mixed $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $this->_setValue($value);
+    if (! in_array($value, $this->_haystack, $this->_strict)) {
+      $this->_error();
+      return false;
+    }
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Int.php b/trunk/php/external/Zend/Validate/Int.php
new file mode 100644
index 0000000..9ca3e36
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Int.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Int.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Int extends Zend_Validate_Abstract {
+  
+  const NOT_INT = 'notInt';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_INT => "'%value%' does not appear to be an integer");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is a valid integer
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    $locale = localeconv();
+    
+    $valueFiltered = str_replace($locale['decimal_point'], '.', $valueString);
+    $valueFiltered = str_replace($locale['thousands_sep'], '', $valueFiltered);
+    
+    if (strval(intval($valueFiltered)) != $valueFiltered) {
+      $this->_error();
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Interface.php b/trunk/php/external/Zend/Validate/Interface.php
new file mode 100644
index 0000000..935cbdd
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Interface.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Interface.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+interface Zend_Validate_Interface {
+
+  /**
+   * Returns true if and only if $value meets the validation requirements
+   *
+   * If $value fails validation, then this method returns false, and
+   * getMessages() will return an array of messages that explain why the
+   * validation failed.
+   *
+   * @param  mixed $value
+   * @return boolean
+   * @throws Zend_Valid_Exception If validation of $value is impossible
+   */
+  public function isValid($value);
+
+  /**
+   * Returns an array of messages that explain why the most recent isValid()
+   * call returned false. The array keys are validation failure message identifiers,
+   * and the array values are the corresponding human-readable message strings.
+   *
+   * If isValid() was never called or if the most recent isValid() call
+   * returned true, then this method returns an empty array.
+   *
+   * @return array
+   */
+  public function getMessages();
+
+  /**
+   * Returns an array of message codes that explain why a previous isValid() call
+   * returned false.
+   *
+   * If isValid() was never called or if the most recent isValid() call
+   * returned true, then this method returns an empty array.
+   *
+   * This is now the same as calling array_keys() on the return value from getMessages().
+   *
+   * @return array
+   * @deprecated Since 1.5.0
+   */
+  public function getErrors();
+
+}
diff --git a/trunk/php/external/Zend/Validate/Ip.php b/trunk/php/external/Zend/Validate/Ip.php
new file mode 100644
index 0000000..b60699f
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Ip.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Ip.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Ip extends Zend_Validate_Abstract {
+  
+  const NOT_IP_ADDRESS = 'notIpAddress';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(
+      self::NOT_IP_ADDRESS => "'%value%' does not appear to be a valid IP address");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is a valid IP address
+   *
+   * @param  mixed $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    if (ip2long($valueString) === false) {
+      $this->_error();
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/LessThan.php b/trunk/php/external/Zend/Validate/LessThan.php
new file mode 100644
index 0000000..d2a27b4
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/LessThan.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: LessThan.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_LessThan extends Zend_Validate_Abstract {
+  
+  const NOT_LESS = 'notLessThan';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_LESS => "'%value%' is not less than '%max%'");
+  
+  /**
+   * @var array
+   */
+  protected $_messageVariables = array('max' => '_max');
+  
+  /**
+   * Maximum value
+   *
+   * @var mixed
+   */
+  protected $_max;
+
+  /**
+   * Sets validator options
+   *
+   * @param  mixed $max
+   * @return void
+   */
+  public function __construct($max) {
+    $this->setMax($max);
+  }
+
+  /**
+   * Returns the max option
+   *
+   * @return mixed
+   */
+  public function getMax() {
+    return $this->_max;
+  }
+
+  /**
+   * Sets the max option
+   *
+   * @param  mixed $max
+   * @return Zend_Validate_LessThan Provides a fluent interface
+   */
+  public function setMax($max) {
+    $this->_max = $max;
+    return $this;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is less than max option
+   *
+   * @param  mixed $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $this->_setValue($value);
+    if ($this->_max <= $value) {
+      $this->_error();
+      return false;
+    }
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/NotEmpty.php b/trunk/php/external/Zend/Validate/NotEmpty.php
new file mode 100644
index 0000000..99862e4
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/NotEmpty.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: NotEmpty.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_NotEmpty extends Zend_Validate_Abstract {
+  
+  const IS_EMPTY = 'isEmpty';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::IS_EMPTY => "Value is empty, but a non-empty value is required");
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value is not an empty value.
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    if (empty($value)) {
+      $this->_error();
+      return false;
+    }
+    
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/Regex.php b/trunk/php/external/Zend/Validate/Regex.php
new file mode 100644
index 0000000..7c89a18
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/Regex.php
@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: Regex.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_Regex extends Zend_Validate_Abstract {
+  
+  const NOT_MATCH = 'regexNotMatch';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::NOT_MATCH => "'%value%' does not match against pattern '%pattern%'");
+  
+  /**
+   * @var array
+   */
+  protected $_messageVariables = array('pattern' => '_pattern');
+  
+  /**
+   * Regular expression pattern
+   *
+   * @var string
+   */
+  protected $_pattern;
+
+  /**
+   * Sets validator options
+   *
+   * @param  string $pattern
+   * @return void
+   */
+  public function __construct($pattern) {
+    $this->setPattern($pattern);
+  }
+
+  /**
+   * Returns the pattern option
+   *
+   * @return string
+   */
+  public function getPattern() {
+    return $this->_pattern;
+  }
+
+  /**
+   * Sets the pattern option
+   *
+   * @param  string $pattern
+   * @return Zend_Validate_Regex Provides a fluent interface
+   */
+  public function setPattern($pattern) {
+    $this->_pattern = (string)$pattern;
+    return $this;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if $value matches against the pattern option
+   *
+   * @param  string $value
+   * @throws Zend_Validate_Exception if there is a fatal error in pattern matching
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    
+    $this->_setValue($valueString);
+    
+    $status = @preg_match($this->_pattern, $valueString);
+    if (false === $status) {
+      /**
+       * @see Zend_Validate_Exception
+       */
+      require_once 'external/Zend/Validate/Exception.php';
+      throw new Zend_Validate_Exception("Internal error matching pattern '$this->_pattern' against value '$valueString'");
+    }
+    if (! $status) {
+      $this->_error();
+      return false;
+    }
+    return true;
+  }
+
+}
diff --git a/trunk/php/external/Zend/Validate/StringLength.php b/trunk/php/external/Zend/Validate/StringLength.php
new file mode 100644
index 0000000..35aa574
--- /dev/null
+++ b/trunk/php/external/Zend/Validate/StringLength.php
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * Zend Framework
+ *
+ * LICENSE
+ *
+ * This source file is subject to the new BSD license that is bundled
+ * with this package in the file LICENSE.txt.
+ * It is also available through the world-wide-web at this URL:
+ * http://framework.zend.com/license/new-bsd
+ * If you did not receive a copy of the license and are unable to
+ * obtain it through the world-wide-web, please send an email
+ * to license@zend.com so we can send you a copy immediately.
+ *
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ * @version    $Id: StringLength.php 8064 2008-02-16 10:58:39Z thomas $
+ */
+
+/**
+ * @see Zend_Validate_Abstract
+ */
+require_once 'external/Zend/Validate/Abstract.php';
+
+/**
+ * @category   Zend
+ * @package    Zend_Validate
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    http://framework.zend.com/license/new-bsd     New BSD License
+ */
+class Zend_Validate_StringLength extends Zend_Validate_Abstract {
+  
+  const TOO_SHORT = 'stringLengthTooShort';
+  const TOO_LONG = 'stringLengthTooLong';
+  
+  /**
+   * @var array
+   */
+  protected $_messageTemplates = array(self::TOO_SHORT => "'%value%' is less than %min% characters long", 
+      self::TOO_LONG => "'%value%' is greater than %max% characters long");
+  
+  /**
+   * @var array
+   */
+  protected $_messageVariables = array('min' => '_min', 'max' => '_max');
+  
+  /**
+   * Minimum length
+   *
+   * @var integer
+   */
+  protected $_min;
+  
+  /**
+   * Maximum length
+   *
+   * If null, there is no maximum length
+   *
+   * @var integer|null
+   */
+  protected $_max;
+
+  /**
+   * Sets validator options
+   *
+   * @param  integer $min
+   * @param  integer $max
+   * @return void
+   */
+  public function __construct($min = 0, $max = null) {
+    $this->setMin($min);
+    $this->setMax($max);
+  }
+
+  /**
+   * Returns the min option
+   *
+   * @return integer
+   */
+  public function getMin() {
+    return $this->_min;
+  }
+
+  /**
+   * Sets the min option
+   *
+   * @param  integer $min
+   * @throws Zend_Validate_Exception
+   * @return Zend_Validate_StringLength Provides a fluent interface
+   */
+  public function setMin($min) {
+    if (null !== $this->_max && $min > $this->_max) {
+      /**
+       * @see Zend_Validate_Exception
+       */
+      require_once 'external/Zend/Validate/Exception.php';
+      throw new Zend_Validate_Exception("The minimum must be less than or equal to the maximum length, but $min >" . " $this->_max");
+    }
+    $this->_min = max(0, (integer)$min);
+    return $this;
+  }
+
+  /**
+   * Returns the max option
+   *
+   * @return integer|null
+   */
+  public function getMax() {
+    return $this->_max;
+  }
+
+  /**
+   * Sets the max option
+   *
+   * @param  integer|null $max
+   * @throws Zend_Validate_Exception
+   * @return Zend_Validate_StringLength Provides a fluent interface
+   */
+  public function setMax($max) {
+    if (null === $max) {
+      $this->_max = null;
+    } else if ($max < $this->_min) {
+      /**
+       * @see Zend_Validate_Exception
+       */
+      require_once 'external/Zend/Validate/Exception.php';
+      throw new Zend_Validate_Exception("The maximum must be greater than or equal to the minimum length, but " . "$max < $this->_min");
+    } else {
+      $this->_max = (integer)$max;
+    }
+    
+    return $this;
+  }
+
+  /**
+   * Defined by Zend_Validate_Interface
+   *
+   * Returns true if and only if the string length of $value is at least the min option and
+   * no greater than the max option (when the max option is not null).
+   *
+   * @param  string $value
+   * @return boolean
+   */
+  public function isValid($value) {
+    $valueString = (string)$value;
+    $this->_setValue($valueString);
+    $length = iconv_strlen($valueString);
+    if ($length < $this->_min) {
+      $this->_error(self::TOO_SHORT);
+    }
+    if (null !== $this->_max && $this->_max < $length) {
+      $this->_error(self::TOO_LONG);
+    }
+    if (count($this->_messages)) {
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+}
diff --git a/trunk/php/external/dbunit.bat b/trunk/php/external/dbunit.bat
new file mode 100755
index 0000000..4cb0d14
--- /dev/null
+++ b/trunk/php/external/dbunit.bat
@@ -0,0 +1,38 @@
+@echo off
+REM PHPUnit
+REM
+REM Copyright (c) 2002-2010, Sebastian Bergmann <sb@sebastian-bergmann.de>.
+REM All rights reserved.
+REM
+REM Redistribution and use in source and binary forms, with or without
+REM modification, are permitted provided that the following conditions
+REM are met:
+REM
+REM   * Redistributions of source code must retain the above copyright
+REM     notice, this list of conditions and the following disclaimer.
+REM 
+REM   * Redistributions in binary form must reproduce the above copyright
+REM     notice, this list of conditions and the following disclaimer in
+REM     the documentation and/or other materials provided with the
+REM     distribution.
+REM
+REM   * Neither the name of Sebastian Bergmann nor the names of his
+REM     contributors may be used to endorse or promote products derived
+REM     from this software without specific prior written permission.
+REM
+REM THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+REM "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+REM LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+REM FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+REM COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+REM INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+REM BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+REM LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+REM CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRIC
+REM LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+REM ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+REM POSSIBILITY OF SUCH DAMAGE.
+REM
+
+set PHPBIN="@php_bin@"
+"@php_bin@" "@bin_dir@\dbunit" %*
diff --git a/trunk/php/external/dbunit.php b/trunk/php/external/dbunit.php
new file mode 100755
index 0000000..fd06b82
--- /dev/null
+++ b/trunk/php/external/dbunit.php
@@ -0,0 +1,63 @@
+#!/usr/bin/env php
+<?php
+/* PHPUnit
+ *
+ * Copyright (c) 2002-2010, Sebastian Bergmann <sb@sebastian-bergmann.de>.
+ * 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 Sebastian Bergmann nor the names of his
+ *     contributors may be used to endorse or promote products derived
+ *     from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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
+ * COPYRIGHT OWNER OR 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, STRIC
+ * 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.
+ */
+
+if (strpos('@php_bin@', '@php_bin') === 0) {
+    set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path());
+}
+
+if (isset($_ENV['PWD'])) {
+    chdir($_ENV['PWD']);
+}
+
+require_once 'PHPUnit/Util/Filter.php';
+
+PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');
+
+require_once 'PHPUnit/Extensions/Database/UI/Command.php';
+require_once 'PHPUnit/Extensions/Database/UI/ModeFactory.php';
+require_once 'PHPUnit/Extensions/Database/UI/Mediums/Text.php';
+require_once 'PHPUnit/Extensions/Database/UI/Context.php';
+
+$command = new PHPUnit_Extensions_Database_UI_Command(
+	new PHPUnit_Extensions_Database_UI_ModeFactory()
+);
+
+$command->main(
+	new PHPUnit_Extensions_Database_UI_Mediums_Text($_SERVER['argv']),
+	new PHPUnit_Extensions_Database_UI_Context()
+);
+?>
diff --git a/trunk/php/external/jsmin-php/jsmin.php b/trunk/php/external/jsmin-php/jsmin.php
new file mode 100644
index 0000000..7d8b039
--- /dev/null
+++ b/trunk/php/external/jsmin-php/jsmin.php
@@ -0,0 +1,288 @@
+<?php
+/*
+ * jsmin.php - PHP implementation of Douglas Crockford's JSMin.
+ *
+ * This is pretty much a direct port of jsmin.c to PHP with just a few
+ * PHP-specific performance tweaks. Also, whereas jsmin.c reads from stdin and
+ * outputs to stdout, this library accepts a string as input and returns another
+ * string as output.
+ *
+ * PHP 5 or higher is required.
+ *
+ * Permission is hereby granted to use this version of the library under the
+ * same terms as jsmin.c, which has the following license:
+ *
+ * --
+ * Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
+ *
+ * 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 shall be used for Good, not Evil.
+ *
+ * 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.
+ * --
+ *
+ * @package JSMin
+ * @author Ryan Grove <ryan@wonko.com>
+ * @copyright 2002 Douglas Crockford <douglas@crockford.com> (jsmin.c)
+ * @copyright 2008 Ryan Grove <ryan@wonko.com> (PHP port)
+ * @license http://opensource.org/licenses/mit-license.php MIT License
+ * @version 1.1.1 (2008-03-02)
+ * @link http://code.google.com/p/jsmin-php/
+ */
+
+class JsMinException extends Exception {
+}
+
+class JsMin {
+  const ORD_LF = 10;
+  const ORD_SPACE = 32;
+
+  protected $a = '';
+  protected $b = '';
+  protected $input = '';
+  protected $inputIndex = 0;
+  protected $inputLength = 0;
+  protected $lookAhead = null;
+  protected $output = '';
+
+  // -- Public Static Methods --------------------------------------------------
+
+
+  public static function minify($js) {
+    $jsmin = new JsMin($js);
+    return $jsmin->min();
+  }
+
+  // -- Public Instance Methods ------------------------------------------------
+
+
+  public function __construct($input) {
+    $this->input = str_replace("\r\n", "\n", $input);
+    $this->inputLength = strlen($this->input);
+  }
+
+  // -- Protected Instance Methods ---------------------------------------------
+
+
+  protected function action($d) {
+    switch ($d) {
+      case 1:
+        $this->output .= $this->a;
+
+      case 2:
+        $this->a = $this->b;
+
+        if ($this->a === "'" || $this->a === '"') {
+          for (;;) {
+            $this->output .= $this->a;
+            $this->a = $this->get();
+
+            if ($this->a === $this->b) {
+              break;
+            }
+
+            if (ord($this->a) <= self::ORD_LF) {
+              throw new JsMinException('Unterminated string literal.');
+            }
+
+            if ($this->a === '\\') {
+              $this->output .= $this->a;
+              $this->a = $this->get();
+            }
+          }
+        }
+
+      case 3:
+        $this->b = $this->next();
+
+        if ($this->b === '/' && ($this->a === '(' || $this->a === ',' || $this->a === '=' || $this->a === ':' || $this->a === '[' || $this->a === '!' || $this->a === '&' || $this->a === '|' || $this->a === '?')) {
+
+          $this->output .= $this->a . $this->b;
+
+          for (;;) {
+            $this->a = $this->get();
+
+            if ($this->a === '/') {
+              break;
+            } elseif ($this->a === '\\') {
+              $this->output .= $this->a;
+              $this->a = $this->get();
+            } elseif (ord($this->a) <= self::ORD_LF) {
+              throw new JsMinException('Unterminated regular expression ' . 'literal.');
+            }
+
+            $this->output .= $this->a;
+          }
+
+          $this->b = $this->next();
+        }
+    }
+  }
+
+  protected function get() {
+    $c = $this->lookAhead;
+    $this->lookAhead = null;
+
+    if ($c === null) {
+      if ($this->inputIndex < $this->inputLength) {
+        $c = $this->input[$this->inputIndex];
+        $this->inputIndex += 1;
+      } else {
+        $c = null;
+      }
+    }
+
+    if ($c === "\r") {
+      return "\n";
+    }
+
+    if ($c === null || $c === "\n" || ord($c) >= self::ORD_SPACE) {
+      return $c;
+    }
+
+    return ' ';
+  }
+
+  protected function isAlphaNum($c) {
+    return ord($c) > 126 || $c === '\\' || preg_match('/^[\w\$]$/', $c) === 1;
+  }
+
+  protected function min() {
+    $this->a = "\n";
+    $this->action(3);
+
+    while ($this->a !== null) {
+      switch ($this->a) {
+        case ' ':
+          if ($this->isAlphaNum($this->b)) {
+            $this->action(1);
+          } else {
+            $this->action(2);
+          }
+          break;
+
+        case "\n":
+          switch ($this->b) {
+            case '{':
+            case '[':
+            case '(':
+            case '+':
+            case '-':
+              $this->action(1);
+              break;
+
+            case ' ':
+              $this->action(3);
+              break;
+
+            default:
+              if ($this->isAlphaNum($this->b)) {
+                $this->action(1);
+              } else {
+                $this->action(2);
+              }
+          }
+          break;
+
+        default:
+          switch ($this->b) {
+            case ' ':
+              if ($this->isAlphaNum($this->a)) {
+                $this->action(1);
+                break;
+              }
+
+              $this->action(3);
+              break;
+
+            case "\n":
+              switch ($this->a) {
+                case '}':
+                case ']':
+                case ')':
+                case '+':
+                case '-':
+                case '"':
+                case "'":
+                  $this->action(1);
+                  break;
+
+                default:
+                  if ($this->isAlphaNum($this->a)) {
+                    $this->action(1);
+                  } else {
+                    $this->action(3);
+                  }
+              }
+              break;
+
+            default:
+              $this->action(1);
+              break;
+          }
+      }
+    }
+
+    return $this->output;
+  }
+
+  protected function next() {
+    $c = $this->get();
+
+    if ($c === '/') {
+      switch ($this->peek()) {
+        case '/':
+          for (;;) {
+            $c = $this->get();
+
+            if (ord($c) <= self::ORD_LF) {
+              return $c;
+            }
+          }
+
+        case '*':
+          $this->get();
+
+          for (;;) {
+            switch ($this->get()) {
+              case '*':
+                if ($this->peek() === '/') {
+                  $this->get();
+                  return ' ';
+                }
+                break;
+
+              case null:
+                throw new JsMinException('Unterminated comment.');
+            }
+          }
+
+        default:
+          return $c;
+      }
+    }
+
+    return $c;
+  }
+
+  protected function peek() {
+    $this->lookAhead = $this->get();
+    return $this->lookAhead;
+  }
+}
+
diff --git a/trunk/php/external/resources/com/google/caja/plugin/domita-minified.js b/trunk/php/external/resources/com/google/caja/plugin/domita-minified.js
new file mode 100644
index 0000000..453458e
--- /dev/null
+++ b/trunk/php/external/resources/com/google/caja/plugin/domita-minified.js
@@ -0,0 +1,768 @@
+{var JSON,___,attachDocumentStub,bridal,bridalMaker,cajita,css,cssparser,domitaModules,escape,html,html4,json_sans_eval,safeJSON,unicode;typeof
+Date.prototype.toJSON!=='function'&&(Date.prototype.toJSON=function(key){return isFinite(this.valueOf())?this.getUTCFullYear()+'-'+f(this.getUTCMonth()+1)+'-'+f(this.getUTCDate())+'T'+f(this.getUTCHours())+':'+f(this.getUTCMinutes())+':'+f(this.getUTCSeconds())+'Z':null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(key){return this.valueOf()}),json_sans_eval=(function(){var
+hop=Object.hasOwnProperty,EMPTY_STRING,SLASH,completeToken,cx,escapable,escapeSequence,escapes,gap,indent,meta,number,oneChar,rep,significantToken,string;function
+f(n){return n<10?'0'+n:n}cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,meta={'\b':'\\b','	':'\\t','\n':'\\n','':'\\f','\r':'\\r','\"':'\\\"','\\':'\\\\'};function
+quote(string){return escapable.lastIndex=0,escapable.test(string)?'\"'+string.replace(escapable,function(a){var
+c=meta[a];return typeof c==='string'?c:'\\u'+('0000'+a.charCodeAt(0).toString(16)).slice(-4)})+'\"':'\"'+string+'\"'}function
+str(key,holder){var mind=gap,value=holder[key],i,k,length,partial,v;value&&typeof
+value==='object'&&typeof value.toJSON==='function'&&(value=value.toJSON(key)),typeof
+rep==='function'&&(value=rep.call(holder,key,value));switch(typeof value){case'string':return quote(value);case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value)return'null';gap+=indent,partial=[];if(Object.prototype.toString.apply(value)==='[object Array]'){length=value.length;for(i=0;i<length;i+=1)partial[i]=str(i,value)||'null';return v=partial.length===0?'[]':gap?'[\n'+gap+partial.join(',\n'+gap)+'\n'+mind+']':'['+partial.join(',')+']',gap=mind,v}if(rep&&typeof
+rep==='object'){length=rep.length;for(i=0;i<length;i+=1)k=rep[i],typeof k==='string'&&(v=str(k,value),v&&partial.push(quote(k)+(gap?': ':':')+v))}else
+for(k in value)hop.call(value,k)&&(v=str(k,value),v&&partial.push(quote(k)+(gap?': ':':')+v));return v=partial.length===0?'{}':gap?'{\n'+gap+partial.join(',\n'+gap)+'\n'+mind+'}':'{'+partial.join(',')+'}',gap=mind,v}}function
+stringify(value,replacer,space){var i;gap='',indent='';if(typeof space==='number')for(i=0;i<space;i+=1)indent+=' ';else
+if(typeof space==='string')indent=space;rep=replacer;if(replacer&&typeof replacer!=='function'&&(typeof
+replacer!=='object'||typeof replacer.length!=='number'))throw new Error('json_sans_eval.stringify');return str('',{'':value})}number='(?:-?\\b(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\\b)',oneChar='(?:[^\\0-\\x08\\x0a-\\x1f\"\\\\]|\\\\(?:[\"/\\\\bfnrt]|u[0-9A-Fa-f]{4}))',string='(?:\"'+oneChar+'*\")',significantToken=new
+RegExp('(?:false|true|null|[\\{\\}\\[\\]]|'+number+'|'+string+')','g'),escapeSequence=new
+RegExp('\\\\(?:([^u])|u(.{4}))','g'),escapes={'\"':'\"','/':'/','\\':'\\','b':'\b','f':'','n':'\n','r':'\r','t':'	'};function
+unescapeOne(_,ch,hex){return ch?escapes[ch]:String.fromCharCode(parseInt(hex,16))}EMPTY_STRING=new
+String(''),SLASH='\\',completeToken=new RegExp('(?:false|true|null|[ 	\r\n]+|[\\{\\}\\[\\],:]|'+number+'|'+string+'|.)','g');function
+blank(arr,s,e){while(--e>=s)arr[e]=''}function checkSyntax(text,keyFilter){var toks=(''+text).match(completeToken),i=0,n=toks.length;checkArray();if(i<n)throw new
+Error('Trailing tokens '+toks.slice(i-1).join(''));return toks.join('');function
+checkArray(){var t;while(i<n){t=toks[i++];switch(t){case']':return;case'[':checkArray();break;case'{':checkObject()}}}function
+checkObject(){var state=0,len,t;while(i<n){t=toks[i++];switch(t.charCodeAt(0)){case
+9:case 10:case 13:case 32:continue;case 34:len=t.length;if(len===1)throw new Error(t);if(state===0){if(keyFilter&&!keyFilter(t.substring(1,len-1).replace(escapeSequence,unescapeOne)))throw new
+Error(t)}else if(state!==2)throw new Error(t);break;case 39:throw new Error(t);case
+44:if(state!==3)throw new Error(t);state=0;continue;case 58:if(state!==1)throw new
+Error(t);break;case 91:if(state!==2)throw new Error(t);checkArray();break;case 123:if(state!==2)throw new
+Error(t);checkObject();break;case 125:return;default:if(state!==2)throw new Error(t)}++state}}}function
+parse(json,opt_reviver){var toks=json.match(significantToken),tok=toks[0],cont,i,key,n,result,stack,walk;if('{'===tok)result={};else
+if('['===tok)result=[];else throw new Error(tok);stack=[result];for(i=1,n=toks.length;i<n;++i){tok=toks[i];switch(tok.charCodeAt(0)){default:cont=stack[0],cont[key||cont.length]=+tok,key=void
+0;break;case 34:tok=tok.substring(1,tok.length-1),tok.indexOf(SLASH)!==-1&&(tok=tok.replace(escapeSequence,unescapeOne)),cont=stack[0];if(!key)if(cont
+instanceof Array)key=cont.length;else{key=tok||EMPTY_STRING;break}cont[key]=tok,key=void
+0;break;case 91:cont=stack[0],stack.unshift(cont[key||cont.length]=[]),key=void 0;break;case
+93:stack.shift();break;case 102:cont=stack[0],cont[key||cont.length]=false,key=void
+0;break;case 110:cont=stack[0],cont[key||cont.length]=null,key=void 0;break;case
+116:cont=stack[0],cont[key||cont.length]=true,key=void 0;break;case 123:cont=stack[0],stack.unshift(cont[key||cont.length]={}),key=void
+0;break;case 125:stack.shift()}}if(stack.length)throw new Error;return opt_reviver&&(walk=function(holder,key){var
+value=holder[key],i,k,toDelete,v;if(value&&typeof value==='object'){toDelete=null;for(k
+in value)hop.call(value,k)&&value!==holder&&(v=walk(value,k),v!==void 0?(value[k]=v):(toDelete||(toDelete=[]),toDelete.push(k)));if(toDelete)for(i=toDelete.length;--i>=0;)delete
+value[toDelete[i]]}return opt_reviver.call(holder,key,value)},result=walk({'':result},'')),result}return{'checkSyntax':checkSyntax,'parse':parse,'stringify':stringify}})(),typeof
+JSON==='undefined'&&(JSON={}),typeof JSON.stringify!=='function'&&(JSON.stringify=json_sans_eval.stringify),typeof
+JSON.parse!=='function'&&(JSON.parse=json_sans_eval.parse),Array.typeTag___='Array',Object.typeTag___='Object',String.typeTag___='String',Boolean.typeTag___='Boolean',Number.typeTag___='Number',Date.typeTag___='Date',RegExp.typeTag___='RegExp',Error.typeTag___='Error',EvalError.typeTag___='EvalError',RangeError.typeTag___='RangeError',ReferenceError.typeTag___='ReferenceError',SyntaxError.typeTag___='SyntaxError',TypeError.typeTag___='TypeError',URIError.typeTag___='URIError',Object.prototype.proto___=null,Date.prototype.toISOString===void
+0&&typeof Date.prototype.toJSON==='function'&&(Date.prototype.toISOString=function(){return Date.prototype.toJSON.call(this)});try{(function(){}).apply({},{'length':0})}catch(ex){ex
+instanceof TypeError&&(Function.prototype.apply___=Function.prototype.apply,Function.prototype.apply=function
+applyGuard(self,args){return args&&args.CLASS___==='Arguments'&&(args=Array.slice(args,0)),this.apply___(self,args)})}Array.slice===void
+0&&(Array.slice=function(self,opt_start,opt_end){return self&&typeof self==='object'?(opt_end===void
+0&&(opt_end=self.length),Array.prototype.slice.call(self,opt_start,opt_end)):[]}),Function.prototype.bind===void
+0&&(Function.prototype.bind=function(self,var_args){var thisFunc=this,leftArgs=Array.slice(arguments,1);function
+funcBound(var_args){var args=leftArgs.concat(Array.slice(arguments,0));return thisFunc.apply(self,args)}return funcBound}),(function(global){var
+BREAK,GuardMark,GuardStamp,GuardT,MAGIC_NAME,MAGIC_NUM,MAGIC_TOKEN,NO_RESULT,PseudoFunctionProto,USELESS,attribute,endsWith__,endsWith___,endsWith_canDelete___,endsWith_canRead___,endsWith_canSet___,goodJSON,magicCount,myKeeper,myLogFunc,myNewModuleHandler,myOriginalHOP,myOriginalToString,obtainNewModule,poisonArgsCallee,poisonArgsCaller,poisonFuncArgs,poisonFuncCaller,pushMethod,registeredImports,sharedImports,stackInfoFields;function
+ToInt32(alleged_int){return alleged_int>>0}function ToUInt32(alleged_int){return alleged_int>>>0}function
+arrayIndexOf(specimen,i){var len=ToUInt32(this.length);i=ToInt32(i),i<0&&((i+=len)<0&&(i=0));for(;i<len;++i)if(i
+in this&&identical(this[i],specimen))return i;return -1}Array.prototype.indexOf=arrayIndexOf;function
+arrayLastIndexOf(specimen,i){var len=ToUInt32(this.length);if(isNaN(i))i=len-1;else{i=ToInt32(i);if(i<0){i+=len;if(i<0)return -1}else
+if(i>=len)i=len-1}for(;i>=0;--i)if(i in this&&identical(this[i],specimen))return i;return -1}Array.prototype.lastIndexOf=arrayLastIndexOf,endsWith_canDelete___=/_canDelete___$/,endsWith_canRead___=/_canRead___$/,endsWith_canSet___=/_canSet___$/,endsWith___=/___$/,endsWith__=/__$/;function
+typeOf(obj){var result=typeof obj,ctor;return result!=='function'?result:(ctor=obj.constructor,typeof
+ctor==='function'&&ctor.typeTag___==='RegExp'&&obj instanceof ctor?'object':'function')}typeof
+new RegExp('x')==='object'&&(typeOf=function fastTypeof(obj){return typeof obj}),myOriginalHOP=Object.prototype.hasOwnProperty,myOriginalToString=Object.prototype.toString;function
+hasOwnProp(obj,name){var t;return obj?(t=typeof obj,t!=='object'&&t!=='function'?false:myOriginalHOP.call(obj,name)):false}function
+identical(x,y){return x===y?x!==0||1/x===1/y:x!==x&&y!==y}function callFault(var_args){return asFunc(this).apply(USELESS,arguments)}Object.prototype.CALL___=callFault;function
+defaultLogger(str,opt_stop){}myLogFunc=markFuncFreeze(defaultLogger);function getLogFunc(){return myLogFunc}function
+setLogFunc(newLogFunc){myLogFunc=newLogFunc}function log(str){myLogFunc(String(str))}function
+fail(var_args){var message=Array.slice(arguments,0).join('');throw myLogFunc(message,true),new
+Error(message)}function enforce(test,var_args){return test||fail.apply(USELESS,Array.slice(arguments,1))}function
+enforceType(specimen,typename,opt_name){return typeOf(specimen)!==typename&&fail('expected ',typename,' instead of ',typeOf(specimen),': ',opt_name||specimen),specimen}function
+enforceNat(specimen){return enforceType(specimen,'number'),Math.floor(specimen)!==specimen&&fail('Must be integral: ',specimen),specimen<0&&fail('Must not be negative: ',specimen),Math.floor(specimen-1)!==specimen-1&&fail('Beyond precision limit: ',specimen),Math.floor(specimen-1)>=specimen&&fail('Must not be infinite: ',specimen),specimen}function
+deprecate(func,badName,advice){var warningNeeded=true;return function(){return warningNeeded&&(log('\"'+badName+'\" is deprecated.\n'+advice),warningNeeded=false),func.apply(USELESS,arguments)}}function
+debugReference(obj){var constr;switch(typeOf(obj)){case'object':return obj===null?'<null>':(constr=directConstructor(obj),'['+(constr&&constr.name||'Object')+']');default:return'('+obj+':'+typeOf(obj)+')'}}myKeeper={'toString':function
+toString(){return'<Logging Keeper>'},'handleRead':function handleRead(obj,name){return},'handleCall':function
+handleCall(obj,name,args){fail('Not callable: (',debugReference(obj),').',name)},'handleSet':function
+handleSet(obj,name,val){fail('Not writable: (',debugReference(obj),').',name)},'handleDelete':function
+handleDelete(obj,name){fail('Not deletable: (',debugReference(obj),').',name)}},Object.prototype.handleRead___=function
+handleRead___(name){var handlerName=name+'_getter___';return this[handlerName]?this[handlerName]():myKeeper.handleRead(this,name)},Object.prototype.handleCall___=function
+handleCall___(name,args){var handlerName=name+'_handler___';return this[handlerName]?this[handlerName].call(this,args):myKeeper.handleCall(this,name,args)},Object.prototype.handleSet___=function
+handleSet___(name,val){var handlerName=name+'_setter___';return this[handlerName]?this[handlerName](val):myKeeper.handleSet(this,name,val)},Object.prototype.handleDelete___=function
+handleDelete___(name){var handlerName=name+'_deleter___';return this[handlerName]?this[handlerName]():myKeeper.handleDelete(this,name)};function
+directConstructor(obj){var oldConstr,proto,result;if(obj===null)return;if(obj===void
+0)return;if(typeOf(obj)==='function')return;obj=Object(obj);if(myOriginalHOP.call(obj,'proto___')){proto=obj.proto___;if(proto===null)return;result=proto.constructor,(result.prototype!==proto||typeOf(result)!=='function')&&(result=directConstructor(proto))}else{if(!myOriginalHOP.call(obj,'constructor'))result=obj.constructor;else{oldConstr=obj.constructor;if(delete
+obj.constructor)result=obj.constructor,obj.constructor=oldConstr;else if(isPrototypical(obj))log('Guessing the directConstructor of : '+obj),result=Object;else
+return fail('Discovery of direct constructors unsupported when the ','constructor property is not deletable: ',obj,'.constructor === ',oldConstr,'(',obj===global,')')}(typeOf(result)!=='function'||!(obj
+instanceof result))&&fail('Discovery of direct constructors for foreign begotten ','objects not implemented on this platform.\n'),result.prototype.constructor===result&&(obj.proto___=result.prototype)}return result}function
+getFuncCategory(fun){return enforceType(fun,'function'),fun.typeTag___?fun.typeTag___:fun}function
+isDirectInstanceOf(obj,ctor){var constr=directConstructor(obj);return constr===void
+0?false:getFuncCategory(constr)===getFuncCategory(ctor)}function isInstanceOf(obj,ctor){return obj
+instanceof ctor?true:!!isDirectInstanceOf(obj,ctor)}function isRecord(obj){return obj?obj.RECORD___===obj?true:isDirectInstanceOf(obj,Object)?(obj.RECORD___=obj,true):false:false}function
+isArray(obj){return isDirectInstanceOf(obj,Array)}function isJSONContainer(obj){var
+constr,typeTag;return obj?obj.RECORD___===obj?true:(constr=directConstructor(obj),constr===void
+0?false:(typeTag=constr.typeTag___,typeTag!=='Object'&&typeTag!=='Array'?false:!isPrototypical(obj))):false}function
+isFrozen(obj){var t;return obj?obj.FROZEN___===obj?true:(t=typeof obj,t!=='object'&&t!=='function'):true}function
+primFreeze(obj){var badFlags,flag,i,k;if(isFrozen(obj))return obj;if(obj.SLOWFREEZE___){badFlags=[];for(k
+in obj)(endsWith_canSet___.test(k)||endsWith_canDelete___.test(k))&&(obj[k]&&badFlags.push(k));for(i=0;i<badFlags.length;++i)flag=badFlags[i],myOriginalHOP.call(obj,flag)&&(delete
+obj[flag]||fail('internal: failed delete: ',debugReference(obj),'.',flag)),obj[flag]&&(obj[flag]=false);delete
+obj.SLOWFREEZE___}return obj.FROZEN___=obj,typeOf(obj)==='function'&&(isFunc(obj)&&(grantCall(obj,'call'),grantCall(obj,'apply'),obj.CALL___=obj),obj.prototype&&primFreeze(obj.prototype)),obj}function
+freeze(obj){if(isJSONContainer(obj))return primFreeze(obj);if(typeOf(obj)==='function')return enforce(isFrozen(obj),'Internal: non-frozen function: '+obj),obj;if(isInstanceOf(obj,Error))return primFreeze(obj);fail('cajita.freeze(obj) applies only to JSON Containers, ','functions, and Errors: ',debugReference(obj))}function
+copy(obj){var result;return isJSONContainer(obj)||fail('cajita.copy(obj) applies only to JSON Containers: ',debugReference(obj)),result=isArray(obj)?[]:{},forOwnKeys(obj,markFuncFreeze(function(k,v){result[k]=v})),result}function
+snapshot(obj){return primFreeze(copy(obj))}function canRead(obj,name){return obj===void
+0||obj===null?false:!!obj[name+'_canRead___']}function canEnum(obj,name){return obj===void
+0||obj===null?false:!!obj[name+'_canEnum___']}function canCall(obj,name){return obj===void
+0||obj===null?false:obj[name+'_canCall___']?true:obj[name+'_grantCall___']?(fastpathCall(obj,name),true):false}function
+canSet(obj,name){return obj===void 0||obj===null?false:obj[name+'_canSet___']===obj?true:obj[name+'_grantSet___']===obj?(fastpathSet(obj,name),true):false}function
+canDelete(obj,name){return obj===void 0||obj===null?false:obj[name+'_canDelete___']===obj}function
+fastpathRead(obj,name){name==='toString'&&fail('internal: Can\'t fastpath .toString'),obj[name+'_canRead___']=obj}function
+fastpathEnum(obj,name){obj[name+'_canEnum___']=obj}function fastpathCall(obj,name){name==='toString'&&fail('internal: Can\'t fastpath .toString'),obj[name+'_canSet___']&&(obj[name+'_canSet___']=false),obj[name+'_grantSet___']&&(obj[name+'_grantSet___']=false),obj[name+'_canCall___']=obj}function
+fastpathSet(obj,name){name==='toString'&&fail('internal: Can\'t fastpath .toString'),isFrozen(obj)&&fail('Can\'t set .',name,' on frozen (',debugReference(obj),')'),typeOf(obj)==='function'&&fail('Can\'t make .',name,' writable on a function (',debugReference(obj),')'),fastpathEnum(obj,name),fastpathRead(obj,name),obj[name+'_canCall___']&&(obj[name+'_canCall___']=false),obj[name+'_grantCall___']&&(obj[name+'_grantCall___']=false),obj.SLOWFREEZE___=obj,obj[name+'_canSet___']=obj}function
+fastpathDelete(obj,name){name==='toString'&&fail('internal: Can\'t fastpath .toString'),isFrozen(obj)&&fail('Can\'t delete .',name,' on frozen (',debugReference(obj),')'),typeOf(obj)==='function'&&fail('Can\'t make .',name,' deletable on a function (',debugReference(obj),')'),obj.SLOWFREEZE___=obj,obj[name+'_canDelete___']=obj}function
+grantRead(obj,name){fastpathRead(obj,name)}function grantEnum(obj,name){fastpathEnum(obj,name)}function
+grantCall(obj,name){fastpathCall(obj,name),obj[name+'_grantCall___']=obj}function
+grantSet(obj,name){fastpathSet(obj,name),obj[name+'_grantSet___']=obj}function grantDelete(obj,name){fastpathDelete(obj,name)}function
+tamesTo(f,t){var ftype=typeof f,ttype;(!f||ftype!=='function'&&ftype!=='object')&&fail('Unexpected feral primitive: ',f),ttype=typeof
+t,(!t||ttype!=='function'&&ttype!=='object')&&fail('Unexpected tame primitive: ',t);if(f.TAMED_TWIN___===t&&t.FERAL_TWIN___===f)return log('multiply tamed: '+f+', '+t),void
+0;f.TAMED_TWIN___&&hasOwnProp(f,'TAMED_TWIN___')&&fail('Already tames to something: ',f),t.FERAL_TWIN___&&hasOwnProp(t,'FERAL_TWIN___')&&fail('Already untames to something: ',t),f.FERAL_TWIN___&&hasOwnProp(f,'FERAL_TWIN___')&&fail('Already tame: ',f),t.TAMED_TWIN___&&hasOwnProp(t,'TAMED_TWIN___')&&fail('Already feral: ',t),f.TAMED_TWIN___=t,t.FERAL_TWIN___=f}function
+tamesToSelf(obj){var otype=typeof obj;(!obj||otype!=='function'&&otype!=='object')&&fail('Unexpected primitive: ',obj);if(obj.TAMED_TWIN___===obj&&obj.FERAL_TWIN___===obj)return log('multiply tamed: '+obj),void
+0;obj.TAMED_TWIN___&&hasOwnProp(obj,'TAMED_TWIN___')&&fail('Already tames to something: ',obj),obj.FERAL_TWIN___&&hasOwnProp(obj,'FERAL_TWIN___')&&fail('Already untames to something: ',obj),obj.TAMED_TWIN___=obj.FERAL_TWIN___=obj}function
+tame(f){var ftype=typeof f,realFeral,t;return!f||ftype!=='function'&&ftype!=='object'?f:(t=f.TAMED_TWIN___,t&&t.FERAL_TWIN___===f?t:(realFeral=f.FERAL_TWIN___,realFeral&&realFeral.TAMED_TWIN___===f?(log('Tame-only object from feral side: '+f),f):f.AS_TAMED___?(t=f.AS_TAMED___(),t&&tamesTo(f,t),t):isRecord(f)?(t=tameRecord(f),t&&tamesTo(f,t),t):undefined))}function
+untame(t){var ttype=typeof t,f,realTame;return!t||ttype!=='function'&&ttype!=='object'?t:(f=t.FERAL_TWIN___,f&&f.TAMED_TWIN___===t?f:(realTame=t.TAMED_TWIN___,realTame&&realTame.FERAL_TWIN___===t?(log('Feral-only object from tame side: '+t),t):t.AS_FERAL___?(f=t.AS_FERAL___(),f&&tamesTo(f,t),f):isRecord(t)?(f=untameRecord(t),f&&tamesTo(f,t),f):undefined))}global.AS_TAMED___=function(){fail('global object almost leaked')},global.AS_FERAL___=function(){fail('global object leaked')};function
+tameRecord(f){var t={},changed=!isFrozen(f),fv,i,k,keys,len,tv;tamesTo(f,t);try{keys=ownKeys(f),len=keys.length;for(i=0;i<len;++i)k=keys[i],fv=f[k],tv=tame(fv),tv===void
+0&&fv!==void 0?(changed=true):(fv!==tv&&fv===fv&&(changed=true),t[k]=tv)}finally{delete
+f.TAMED_TWIN___,delete t.FERAL_TWIN___}return changed?primFreeze(t):f}function
+untameRecord(t){var f={},changed=!isFrozen(t),fv,i,k,keys,len,tv;tamesTo(f,t);try{keys=ownKeys(t),len=keys.length;for(i=0;i<len;++i)k=keys[i],tv=t[k],fv=untame(tv),fv===void
+0&&tv!==void 0?(changed=true):(tv!==fv&&tv===tv&&(changed=true),f[k]=fv)}finally{delete
+t.FERAL_TWIN___,delete f.TAMED_TWIN___}return changed?primFreeze(f):t}Array.prototype.AS_TAMED___=function
+tameArray(){var f=this,t=[],changed=!isFrozen(f),fv,i,len,tv;tamesTo(f,t);try{len=f.length;for(i=0;i<len;++i)i
+in f?(fv=f[i],tv=tame(fv),fv!==tv&&fv===fv&&(changed=true),t[i]=tv):(changed=true,t[i]=void
+0)}finally{delete f.TAMED_TWIN___,delete t.FERAL_TWIN___}return changed?primFreeze(t):f},Array.prototype.AS_FERAL___=function
+untameArray(){var t=this,f=[],changed=!isFrozen(t),fv,i,len,tv;tamesTo(f,t);try{len=t.length;for(i=0;i<len;++i)i
+in t?(tv=t[i],fv=untame(tv),tv!==fv&&tv===tv&&(changed=true),f[i]=fv):(changed=true,f[i]=void
+0)}finally{delete t.FERAL_TWIN___,delete f.TAMED_TWIN___}return changed?primFreeze(f):t},Function.prototype.AS_TAMED___=function
+defaultTameFunc(){var f=this;return isFunc(f)||isCtor(f)?f:void 0},Function.prototype.AS_FERAL___=function
+defaultUntameFunc(){var t=this;return isFunc(t)||isCtor(t)?t:void 0};function stopEscalation(val){return val===null||val===void
+0||val===global?USELESS:val}function tameXo4a(){var xo4aFunc=this,result;function
+tameApplyFuncWrapper(self,opt_args){return xo4aFunc.apply(stopEscalation(self),opt_args||[])}markFuncFreeze(tameApplyFuncWrapper);function
+tameCallFuncWrapper(self,var_args){return tameApplyFuncWrapper(self,Array.slice(arguments,1))}return markFuncFreeze(tameCallFuncWrapper),result=PseudoFunction(tameCallFuncWrapper,tameApplyFuncWrapper),result.length=xo4aFunc.length,result.toString=markFuncFreeze(xo4aFunc.toString.bind(xo4aFunc)),primFreeze(result)}function
+tameInnocent(){var feralFunc=this,result;function tameApplyFuncWrapper(self,opt_args){var
+feralThis=stopEscalation(untame(self)),feralArgs=untame(opt_args),feralResult=feralFunc.apply(feralThis,feralArgs||[]);return tame(feralResult)}markFuncFreeze(tameApplyFuncWrapper);function
+tameCallFuncWrapper(self,var_args){return tameApplyFuncWrapper(self,Array.slice(arguments,1))}return markFuncFreeze(tameCallFuncWrapper),result=PseudoFunction(tameCallFuncWrapper,tameApplyFuncWrapper),result.length=feralFunc.length,result.toString=markFuncFreeze(feralFunc.toString.bind(feralFunc)),primFreeze(result)}function
+makePoisonPill(badThing){function poisonPill(){throw new TypeError(''+badThing+' forbidden by ES5/strict')}return poisonPill}poisonArgsCallee=makePoisonPill('arguments.callee'),poisonArgsCaller=makePoisonPill('arguments.caller'),poisonFuncCaller=makePoisonPill('A function\'s .caller'),poisonFuncArgs=makePoisonPill('A function\'s .arguments');function
+args(original){var result={'length':0};return pushMethod.apply(result,original),result.CLASS___='Arguments',useGetHandler(result,'callee',poisonArgsCallee),useSetHandler(result,'callee',poisonArgsCallee),useGetHandler(result,'caller',poisonArgsCaller),useSetHandler(result,'caller',poisonArgsCaller),result}pushMethod=[].push,PseudoFunctionProto={'toString':markFuncFreeze(function(){return'pseudofunction(var_args) {\n    [some code]\n}'}),'PFUNC___':true,'CLASS___':'Function','AS_FERAL___':function
+untamePseudoFunction(){var tamePseudoFunc=this;function feralWrapper(var_args){var
+feralArgs=Array.slice(arguments,0),tamedSelf=tame(stopEscalation(this)),tamedArgs=tame(feralArgs),tameResult=callPub(tamePseudoFunc,'apply',[tamedSelf,tamedArgs]);return untame(tameResult)}return feralWrapper}},useGetHandler(PseudoFunctionProto,'caller',poisonFuncCaller),useSetHandler(PseudoFunctionProto,'caller',poisonFuncCaller),useGetHandler(PseudoFunctionProto,'arguments',poisonFuncArgs),useSetHandler(PseudoFunctionProto,'arguments',poisonFuncArgs),primFreeze(PseudoFunctionProto);function
+PseudoFunction(callFunc,opt_applyFunc){var applyFunc,result;return callFunc=asFunc(callFunc),applyFunc=opt_applyFunc?asFunc(opt_applyFunc):markFuncFreeze(function
+applyFun(self,opt_args){var args=[self];return opt_args!==void 0&&opt_args!==null&&args.push.apply(args,opt_args),callFunc.apply(USELESS,args)}),result=primBeget(PseudoFunctionProto),result.call=callFunc,result.apply=applyFunc,result.bind=markFuncFreeze(function
+bindFun(self,var_args){var args;return self=stopEscalation(self),args=[USELESS,self].concat(Array.slice(arguments,1)),markFuncFreeze(callFunc.bind.apply(callFunc,args))}),result.length=callFunc.length-1,result}function
+isCtor(constr){return constr&&!!constr.CONSTRUCTOR___}function isFunc(fun){return fun&&!!fun.FUNC___}function
+isXo4aFunc(func){return func&&!!func.XO4A___}function isPseudoFunc(fun){return fun&&fun.PFUNC___}function
+markCtor(constr,opt_Sup,opt_name){return enforceType(constr,'function',opt_name),isFunc(constr)&&fail('Simple functions can\'t be constructors: ',constr),isXo4aFunc(constr)&&fail('Exophoric functions can\'t be constructors: ',constr),constr.CONSTRUCTOR___=true,opt_Sup?derive(constr,opt_Sup):constr!==Object&&fail('Only \"Object\" has no super: ',constr),opt_name&&(constr.NAME___=String(opt_name)),constr!==Object&&constr!==Array&&(constr.prototype.AS_TAMED___=constr.prototype.AS_FERAL___=function(){return this}),constr}function
+derive(constr,sup){var proto=constr.prototype;sup=asCtor(sup),isFrozen(constr)&&fail('Derived constructor already frozen: ',constr),proto
+instanceof sup||fail('\"'+constr+'\" does not derive from \"',sup),'__proto__'in
+proto&&proto.__proto__!==sup.prototype&&fail('\"'+constr+'\" does not derive directly from \"',sup),isFrozen(proto)||(proto.proto___=sup.prototype)}function
+extend(feralCtor,someSuper,opt_name){var inert,noop;return'function'!==typeof feralCtor&&fail('Internal: Feral constructor is not a function'),someSuper=asCtor(someSuper.prototype.constructor),noop=function(){},noop.prototype=someSuper.prototype,feralCtor.prototype=new
+noop,feralCtor.prototype.proto___=someSuper.prototype,inert=function(){fail('This constructor cannot be called directly')},inert.prototype=feralCtor.prototype,feralCtor.prototype.constructor=inert,markCtor(inert,someSuper,opt_name),tamesTo(feralCtor,inert),primFreeze(inert)}function
+markXo4a(func,opt_name){return enforceType(func,'function',opt_name),isCtor(func)&&fail('Internal: Constructors can\'t be exophora: ',func),isFunc(func)&&fail('Internal: Simple functions can\'t be exophora: ',func),func.XO4A___=true,opt_name&&(func.NAME___=opt_name),func.AS_TAMED___=tameXo4a,primFreeze(func)}function
+markInnocent(func,opt_name){return enforceType(func,'function',opt_name),isCtor(func)&&fail('Internal: Constructors aren\'t innocent: ',func),isFunc(func)&&fail('Internal: Simple functions aren\'t innocent: ',func),isXo4aFunc(func)&&fail('Internal: Exophoric functions aren\'t innocent: ',func),opt_name&&(func.NAME___=opt_name),func.AS_TAMED___=tameInnocent,primFreeze(func)}function
+markFuncFreeze(fun,opt_name){return typeOf(fun)!=='function'&&fail('expected function instead of ',typeOf(fun),': ',opt_name||fun),fun.CONSTRUCTOR___&&fail('Constructors can\'t be simple functions: ',fun),fun.XO4A___&&fail('Exophoric functions can\'t be simple functions: ',fun),fun.FUNC___=opt_name?String(opt_name):true,primFreeze(fun)}function
+asCtorOnly(constr){if(isCtor(constr)||isFunc(constr))return constr;enforceType(constr,'function'),fail('Untamed functions can\'t be called as constructors: ',constr)}function
+asCtor(constr){return primFreeze(asCtorOnly(constr))}function asFunc(fun){if(fun&&fun.FUNC___)return fun.FROZEN___===fun?fun:primFreeze(fun);enforceType(fun,'function');if(isCtor(fun)){if(fun===Number||fun===String||fun===Boolean)return primFreeze(fun);fail('Constructors can\'t be called as simple functions: ',fun)}isXo4aFunc(fun)&&fail('Exophoric functions can\'t be called as simple functions: ',fun),fail('Untamed functions can\'t be called as simple functions: ',fun)}function
+toFunc(fun){return isPseudoFunc(fun)?markFuncFreeze(function applier(var_args){return callPub(fun,'apply',[USELESS,Array.slice(arguments,0)])}):asFunc(fun)}function
+isPrototypical(obj){var constr;return typeOf(obj)!=='object'?false:obj===null?false:(constr=obj.constructor,typeOf(constr)!=='function'?false:constr.prototype===obj)}function
+asFirstClass(value){switch(typeOf(value)){case'function':if(isFunc(value)||isCtor(value)){if(isFrozen(value))return value;fail('Internal: non-frozen function encountered: ',value)}else
+if(isXo4aFunc(value))fail('Internal: toxic exophora encountered: ',value);else fail('Internal: toxic function encountered: ',value);break;case'object':return value!==null&&isPrototypical(value)&&fail('Internal: prototypical object encountered: ',value),value;default:return value}}function
+canReadPub(obj,name){return typeof name==='number'&&name>=0?name in obj:(name=String(name),obj===null?false:obj===void
+0?false:obj[name+'_canRead___']?name in Object(obj):endsWith__.test(name)?false:name==='toString'?false:isJSONContainer(obj)?myOriginalHOP.call(obj,name)?(fastpathRead(obj,name),true):false:false)}function
+hasOwnPropertyOf(obj,name){return typeof name==='number'&&name>=0?hasOwnProp(obj,name):(name=String(name),obj&&obj[name+'_canRead___']===obj?true:canReadPub(obj,name)&&myOriginalHOP.call(obj,name))}function
+inPub(name,obj){var t=typeof obj;if(!obj||t!=='object'&&t!=='function')throw new
+TypeError('invalid \"in\" operand: '+obj);return obj=Object(obj),canReadPub(obj,name)?true:canCallPub(obj,name)?true:name+'_getter___'in
+obj?true:name+'_handler___'in obj}function readPub(obj,name){if(typeof name==='number'&&name>=0)return typeof
+obj==='string'?obj.charAt(name):obj[name];name=String(name);if(canReadPub(obj,name))return obj[name];if(obj===null||obj===void
+0)throw new TypeError('Can\'t read '+name+' on '+obj);return obj.handleRead___(name)}function
+readOwn(obj,name,pumpkin){if(typeof obj!=='object'||!obj){if(typeOf(obj)!=='object')return pumpkin};return typeof
+name==='number'&&name>=0?myOriginalHOP.call(obj,name)?obj[name]:pumpkin:(name=String(name),obj[name+'_canRead___']===obj?obj[name]:myOriginalHOP.call(obj,name)?endsWith__.test(name)?pumpkin:name==='toString'?pumpkin:isJSONContainer(obj)?(fastpathRead(obj,name),obj[name]):pumpkin:pumpkin)}function
+enforceStaticPath(result,permitsUsed){forOwnKeys(permitsUsed,markFuncFreeze(function(name,subPermits){enforce(isFrozen(result),'Assumed frozen: ',result);if(name==='()');else
+enforce(canReadPub(result,name),'Assumed readable: ',result,'.',name),inPub('()',subPermits)&&enforce(canCallPub(result,name),'Assumed callable: ',result,'.',name,'()'),enforceStaticPath(readPub(result,name),subPermits)}))}function
+readImport(module_imports,name,opt_permitsUsed){var pumpkin={},result=readOwn(module_imports,name,pumpkin);return result===pumpkin?(log('Linkage warning: '+name+' not importable'),void
+0):(opt_permitsUsed&&enforceStaticPath(result,opt_permitsUsed),result)}function
+canInnocentEnum(obj,name){return name=String(name),!endsWith___.test(name)}function
+canEnumPub(obj,name){return obj===null?false:obj===void 0?false:(name=String(name),obj[name+'_canEnum___']?true:endsWith__.test(name)?false:isJSONContainer(obj)?myOriginalHOP.call(obj,name)?(fastpathEnum(obj,name),name==='toString'||fastpathRead(obj,name),true):false:false)}function
+canEnumOwn(obj,name){return name=String(name),obj&&obj[name+'_canEnum___']===obj?true:canEnumPub(obj,name)&&myOriginalHOP.call(obj,name)}function
+Token(name){return name=String(name),primFreeze({'toString':markFuncFreeze(function
+tokenToString(){return name}),'throwable___':true})}markFuncFreeze(Token),BREAK=Token('BREAK'),NO_RESULT=Token('NO_RESULT');function
+forOwnKeys(obj,fn){var i,keys;fn=toFunc(fn),keys=ownKeys(obj);for(i=0;i<keys.length;++i)if(fn(keys[i],readPub(obj,keys[i]))===BREAK)return}function
+forAllKeys(obj,fn){var i,keys;fn=toFunc(fn),keys=allKeys(obj);for(i=0;i<keys.length;++i)if(fn(keys[i],readPub(obj,keys[i]))===BREAK)return}function
+ownKeys(obj){var result=[],i,k,len;if(isArray(obj)){len=obj.length;for(i=0;i<len;++i)result.push(i)}else{for(k
+in obj)canEnumOwn(obj,k)&&result.push(k);obj!==void 0&&obj!==null&&obj.handleEnum___&&(result=result.concat(obj.handleEnum___(true)))}return result}function
+allKeys(obj){var k,result;if(isArray(obj))return ownKeys(obj);result=[];for(k in
+obj)canEnumPub(obj,k)&&result.push(k);return obj!==void 0&&obj!==null&&obj.handleEnum___&&(result=result.concat(obj.handleEnum___(false))),result}function
+canCallPub(obj,name){var func;return obj===null?false:obj===void 0?false:(name=String(name),obj[name+'_canCall___']?true:obj[name+'_grantCall___']?(fastpathCall(obj,name),true):canReadPub(obj,name)?endsWith__.test(name)?false:name==='toString'?false:(func=obj[name],!isFunc(func)&&!isXo4aFunc(func)?false:(fastpathCall(obj,name),true)):false)}function
+callPub(obj,name,args){name=String(name);if(obj===null||obj===void 0)throw new TypeError('Can\'t call '+name+' on '+obj);if(obj[name+'_canCall___']||canCallPub(obj,name))return obj[name].apply(obj,args);if(obj.handleCall___)return obj.handleCall___(name,args);fail('not callable:',debugReference(obj),'.',name)}function
+canSetPub(obj,name){return name=String(name),canSet(obj,name)?true:endsWith__.test(name)?false:name==='valueOf'?false:name==='toString'?false:!isFrozen(obj)&&isJSONContainer(obj)}function
+setPub(obj,name,val){if(typeof name==='number'&&name>=0&&obj instanceof Array&&obj.FROZEN___!==obj)return obj[name]=val;name=String(name);if(obj===null||obj===void
+0)throw new TypeError('Can\'t set '+name+' on '+obj);return obj[name+'_canSet___']===obj?(obj[name]=val):canSetPub(obj,name)?(fastpathSet(obj,name),obj[name]=val):obj.handleSet___(name,val)}function
+canSetStatic(fun,staticMemberName){return staticMemberName=''+staticMemberName,typeOf(fun)!=='function'?(log('Cannot set static member of non function: '+fun),false):isFrozen(fun)?(log('Cannot set static member of frozen function: '+fun),false):isFunc(fun)?staticMemberName==='toString'?false:endsWith__.test(staticMemberName)||staticMemberName==='valueOf'?(log('Illegal static member name: '+staticMemberName),false):staticMemberName
+in fun?(log('Cannot override static member: '+staticMemberName),false):true:(log('Can only set static members on simple-functions: '+fun),false)}function
+setStatic(fun,staticMemberName,staticMemberValue){staticMemberName=''+staticMemberName,canSetStatic(fun,staticMemberName)?(fun[staticMemberName]=staticMemberValue,fastpathEnum(fun,staticMemberName),fastpathRead(fun,staticMemberName)):fun.handleSet___(staticMemberName,staticMemberValue)}function
+canDeletePub(obj,name){return name=String(name),isFrozen(obj)?false:endsWith__.test(name)?false:name==='valueOf'?false:name==='toString'?false:!!isJSONContainer(obj)}function
+deletePub(obj,name){name=String(name);if(obj===null||obj===void 0)throw new TypeError('Can\'t delete '+name+' on '+obj);return canDeletePub(obj,name)?deleteFieldEntirely(obj,name):obj.handleDelete___(name)}function
+deleteFieldEntirely(obj,name){return delete obj[name+'_canRead___'],delete obj[name+'_canEnum___'],delete
+obj[name+'_canCall___'],delete obj[name+'_grantCall___'],delete obj[name+'_grantSet___'],delete
+obj[name+'_canSet___'],delete obj[name+'_canDelete___'],delete obj[name]||(fail('not deleted: ',name),false)}USELESS=Token('USELESS');function
+manifest(ignored){}stackInfoFields=['stack','fileName','lineNumer','description','stackTrace','sourceURL','line'];function
+callStackUnsealer(ex){var i,k,numStackInfoFields,stackInfo;if(ex&&isInstanceOf(ex,Error)){stackInfo={},numStackInfoFields=stackInfoFields.length;for(i=0;i<numStackInfoFields;++i)k=stackInfoFields[i],k
+in ex&&(stackInfo[k]=ex[k]);return'cajitaStack___'in ex&&(stackInfo.cajitaStack=ex.cajitaStack___),primFreeze(stackInfo)}return}function
+tameException(ex){var name;if(ex&&ex.UNCATCHABLE___)throw ex;try{switch(typeOf(ex)){case'string':case'number':case'boolean':case'undefined':return ex;case'object':return ex===null?null:ex.throwable___?ex:isInstanceOf(ex,Error)?primFreeze(ex):''+ex;case'function':name=''+(ex.name||ex);function
+inLieuOfThrownFunction(){return'In lieu of thrown function: '+name}return markFuncFreeze(inLieuOfThrownFunction,name);default:return log('Unrecognized exception type: '+typeOf(ex)),'Unrecognized exception type: '+typeOf(ex)}}catch(_){return log('Exception during exception handling.'),'Exception during exception handling.'}}function
+primBeget(proto){var result;proto===null&&fail('Cannot beget from null.'),proto===void
+0&&fail('Cannot beget from undefined.');function F(){}return F.prototype=proto,result=new
+F,result.proto___=proto,result}function initializeMap(list){var result={},i;for(i=0;i<list.length;i+=2)setPub(result,list[i],asFirstClass(list[i+1]));return result}function
+useGetHandler(obj,name,getHandler){obj[name+'_getter___']=getHandler}function useApplyHandler(obj,name,applyHandler){obj[name+'_handler___']=applyHandler}function
+useCallHandler(obj,name,callHandler){useApplyHandler(obj,name,function callApplier(args){return callHandler.apply(this,args)})}function
+useSetHandler(obj,name,setHandler){obj[name+'_setter___']=setHandler}function useDeleteHandler(obj,name,deleteHandler){obj[name+'_deleter___']=deleteHandler}function
+grantFunc(obj,name){markFuncFreeze(obj[name],name),grantCall(obj,name),grantRead(obj,name)}function
+grantGenericMethod(proto,name){var func=markXo4a(proto[name],name),pseudoFunc;grantCall(proto,name),pseudoFunc=tame(func),useGetHandler(proto,name,function
+xo4aGetter(){return pseudoFunc})}function handleGenericMethod(obj,name,func){var
+feral=obj[name],pseudoFunc;hasOwnProp(obj,name)?hasOwnProp(feral,'TAMED_TWIN___')&&(feral=func):(feral=func),useCallHandler(obj,name,func),pseudoFunc=tameXo4a.call(func),tamesTo(feral,pseudoFunc),useGetHandler(obj,name,function
+genericGetter(){return pseudoFunc})}function grantTypedMethod(proto,name){var original=proto[name];handleGenericMethod(proto,name,function
+guardedApplier(var_args){return inheritsFrom(this,proto)||fail('Can\'t call .',name,' on a non ',directConstructor(proto),': ',this),original.apply(this,arguments)})}function
+grantMutatingMethod(proto,name){var original=proto[name];handleGenericMethod(proto,name,function
+nonMutatingApplier(var_args){return isFrozen(this)&&fail('Can\'t .',name,' a frozen object'),original.apply(this,arguments)})}function
+grantInnocentMethod(proto,name){var original=proto[name];handleGenericMethod(proto,name,function
+guardedApplier(var_args){var feralThis=stopEscalation(untame(this)),feralArgs=untame(Array.slice(arguments,0)),feralResult=original.apply(feralThis,feralArgs);return tame(feralResult)})}function
+enforceMatchable(regexp){isInstanceOf(regexp,RegExp)?isFrozen(regexp)&&fail('Can\'t match with frozen RegExp: ',regexp):enforceType(regexp,'string')}function
+all2(func2,arg1,arg2s){var len=arg2s.length,i;for(i=0;i<len;i+=1)func2(arg1,arg2s[i])}all2(grantRead,Math,['E','LN10','LN2','LOG2E','LOG10E','PI','SQRT1_2','SQRT2']),all2(grantFunc,Math,['abs','acos','asin','atan','atan2','ceil','cos','exp','floor','log','max','min','pow','random','round','sin','sqrt','tan']);function
+grantToString(proto){proto.TOSTRING___=tame(markXo4a(proto.toString,'toString'))}function
+makeToStringMethod(toStringValue){function toStringMethod(var_args){var args=Array.slice(arguments,0),result,toStringValueApply;return isFunc(toStringValue)?toStringValue.apply(this,args):(toStringValueApply=readPub(toStringValue,'apply'),isFunc(toStringValueApply)?toStringValueApply.call(toStringValue,this,args):(result=myOriginalToString.call(this),log('Not correctly printed: '+result),result))}return toStringMethod}function
+toStringGetter(){return hasOwnProp(this,'toString')&&typeOf(this.toString)==='function'&&!hasOwnProp(this,'TOSTRING___')&&grantToString(this),this.TOSTRING___}useGetHandler(Object.prototype,'toString',toStringGetter),useApplyHandler(Object.prototype,'toString',function
+toStringApplier(args){var toStringValue=toStringGetter.call(this);return makeToStringMethod(toStringValue).apply(this,args)}),useSetHandler(Object.prototype,'toString',function
+toStringSetter(toStringValue){var firstClassToStringValue;return isFrozen(this)||!isJSONContainer(this)?myKeeper.handleSet(this,'toString',toStringValue):(firstClassToStringValue=asFirstClass(toStringValue),this.TOSTRING___=firstClassToStringValue,this.toString=makeToStringMethod(firstClassToStringValue),toStringValue)}),useDeleteHandler(Object.prototype,'toString',function
+toStringDeleter(){return isFrozen(this)||!isJSONContainer(this)?myKeeper.handleDelete(this,'toString'):delete
+this.toString&&delete this.TOSTRING___}),markCtor(Object,void 0,'Object'),Object.prototype.TOSTRING___=tame(markXo4a(function(){return this.CLASS___?'[object '+this.CLASS___+']':myOriginalToString.call(this)},'toString')),all2(grantGenericMethod,Object.prototype,['toLocaleString','valueOf','isPrototypeOf']),grantRead(Object.prototype,'length'),handleGenericMethod(Object.prototype,'hasOwnProperty',function
+hasOwnPropertyHandler(name){return hasOwnPropertyOf(this,name)}),handleGenericMethod(Object.prototype,'propertyIsEnumerable',function
+propertyIsEnumerableHandler(name){return name=String(name),canEnumPub(this,name)}),useCallHandler(Object,'freeze',markFuncFreeze(freeze)),useGetHandler(Object,'freeze',function(){return freeze}),grantToString(Function.prototype),handleGenericMethod(Function.prototype,'apply',function
+applyHandler(self,opt_args){return toFunc(this).apply(USELESS,opt_args||[])}),handleGenericMethod(Function.prototype,'call',function
+callHandler(self,var_args){return toFunc(this).apply(USELESS,Array.slice(arguments,1))}),handleGenericMethod(Function.prototype,'bind',function
+bindHandler(self,var_args){var thisFunc=toFunc(this),leftArgs=Array.slice(arguments,1);function
+boundHandler(var_args){var args=leftArgs.concat(Array.slice(arguments,0));return thisFunc.apply(USELESS,args)}return markFuncFreeze(boundHandler)}),useGetHandler(Function.prototype,'caller',poisonFuncCaller),useGetHandler(Function.prototype,'arguments',poisonFuncArgs),markCtor(Array,Object,'Array'),grantFunc(Array,'slice'),grantToString(Array.prototype),all2(grantTypedMethod,Array.prototype,['toLocaleString']),all2(grantGenericMethod,Array.prototype,['concat','join','slice','indexOf','lastIndexOf']),all2(grantMutatingMethod,Array.prototype,['pop','push','reverse','shift','splice','unshift']),handleGenericMethod(Array.prototype,'sort',function
+sortHandler(comparator){return isFrozen(this)&&fail('Can\'t sort a frozen array.'),comparator?Array.prototype.sort.call(this,toFunc(comparator)):Array.prototype.sort.call(this)}),markCtor(String,Object,'String'),grantFunc(String,'fromCharCode'),grantToString(String.prototype),all2(grantTypedMethod,String.prototype,['indexOf','lastIndexOf']),all2(grantGenericMethod,String.prototype,['charAt','charCodeAt','concat','localeCompare','slice','substr','substring','toLowerCase','toLocaleLowerCase','toUpperCase','toLocaleUpperCase']),handleGenericMethod(String.prototype,'match',function
+matchHandler(regexp){return enforceMatchable(regexp),this.match(regexp)}),handleGenericMethod(String.prototype,'replace',function
+replaceHandler(searcher,replacement){return enforceMatchable(searcher),isFunc(replacement)?(replacement=asFunc(replacement)):isPseudoFunc(replacement)?(replacement=toFunc(replacement)):(replacement=''+replacement),this.replace(searcher,replacement)}),handleGenericMethod(String.prototype,'search',function
+searchHandler(regexp){return enforceMatchable(regexp),this.search(regexp)}),handleGenericMethod(String.prototype,'split',function
+splitHandler(separator,limit){return enforceMatchable(separator),this.split(separator,limit)}),markCtor(Boolean,Object,'Boolean'),grantToString(Boolean.prototype),markCtor(Number,Object,'Number'),all2(grantRead,Number,['MAX_VALUE','MIN_VALUE','NaN','NEGATIVE_INFINITY','POSITIVE_INFINITY']),grantToString(Number.prototype),all2(grantTypedMethod,Number.prototype,['toLocaleString','toFixed','toExponential','toPrecision']),markCtor(Date,Object,'Date'),grantFunc(Date,'parse'),grantFunc(Date,'UTC'),grantToString(Date.prototype),all2(grantTypedMethod,Date.prototype,['toDateString','toTimeString','toUTCString','toLocaleString','toLocaleDateString','toLocaleTimeString','toISOString','toJSON','getDay','getUTCDay','getTimezoneOffset','getTime','getFullYear','getUTCFullYear','getMonth','getUTCMonth','getDate','getUTCDate','getHours','getUTCHours','getMinutes','getUTCMinutes','getSeconds','getUTCSeconds','getMilliseconds','getUTCMilliseconds']),all2(grantMutatingMethod,Date.prototype,['setTime','setFullYear','setUTCFullYear','setMonth','setUTCMonth','setDate','setUTCDate','setHours','setUTCHours','setMinutes','setUTCMinutes','setSeconds','setUTCSeconds','setMilliseconds','setUTCMilliseconds']),markCtor(RegExp,Object,'RegExp'),grantToString(RegExp.prototype),handleGenericMethod(RegExp.prototype,'exec',function
+execHandler(specimen){return isFrozen(this)&&fail('Can\'t .exec a frozen RegExp'),specimen=String(specimen),this.exec(specimen)}),handleGenericMethod(RegExp.prototype,'test',function
+testHandler(specimen){return isFrozen(this)&&fail('Can\'t .test a frozen RegExp'),specimen=String(specimen),this.test(specimen)}),all2(grantRead,RegExp.prototype,['source','global','ignoreCase','multiline','lastIndex']),markCtor(Error,Object,'Error'),grantToString(Error.prototype),grantRead(Error.prototype,'name'),grantRead(Error.prototype,'message'),markCtor(EvalError,Error,'EvalError'),markCtor(RangeError,Error,'RangeError'),markCtor(ReferenceError,Error,'ReferenceError'),markCtor(SyntaxError,Error,'SyntaxError'),markCtor(TypeError,Error,'TypeError'),markCtor(URIError,Error,'URIError');function
+getNewModuleHandler(){return myNewModuleHandler}function setNewModuleHandler(newModuleHandler){myNewModuleHandler=newModuleHandler}obtainNewModule=freeze({'handle':markFuncFreeze(function
+handleOnly(newModule){return newModule})});function registerClosureInspector(module){this&&this.CLOSURE_INSPECTOR___&&this.CLOSURE_INSPECTOR___.supportsCajaDebugging&&this.CLOSURE_INSPECTOR___.registerCajaModule(module)}function
+makeNormalNewModuleHandler(){var imports=void 0,lastOutcome=void 0;function getImports(){return imports||(imports=copy(sharedImports)),imports}return freeze({'getImports':markFuncFreeze(getImports),'setImports':markFuncFreeze(function
+setImports(newImports){imports=newImports}),'getLastOutcome':markFuncFreeze(function
+getLastOutcome(){return lastOutcome}),'getLastValue':markFuncFreeze(function getLastValue(){return lastOutcome&&lastOutcome[0]?lastOutcome[1]:void
+0}),'handle':markFuncFreeze(function handle(newModule){var outcome,result;registerClosureInspector(newModule),outcome=void
+0;try{result=newModule.instantiate(___,getImports()),result!==NO_RESULT&&(outcome=[true,result])}catch(ex){outcome=[false,ex]}lastOutcome=outcome;if(outcome){if(outcome[0])return outcome[1];throw outcome[1]}return}),'handleUncaughtException':function
+handleUncaughtException(exception,onerror,source,lineNum){var message,shouldReport;lastOutcome=[false,exception],tameException(exception),message='unknown','object'===typeOf(exception)&&exception!==null&&(message=String(exception.message||exception.desc||message)),isPseudoFunc(onerror)&&(onerror=toFunc(onerror)),shouldReport=isFunc(onerror)?onerror.CALL___(message,String(source),String(lineNum)):onerror!==null,shouldReport!==false&&log(source+':'+lineNum+': '+message)}})}function
+prepareModule(module,load){registerClosureInspector(module);function theModule(imports){var
+completeImports=copy(sharedImports),k;completeImports.load=load;for(k in imports)hasOwnProp(imports,k)&&(completeImports[k]=imports[k]);return module.instantiate(___,primFreeze(completeImports))}return theModule.FUNC___='theModule',setStatic(theModule,'cajolerName',module.cajolerName),setStatic(theModule,'cajolerVersion',module.cajolerVersion),setStatic(theModule,'cajoledDate',module.cajoledDate),setStatic(theModule,'moduleURL',module.moduleURL),!module.includedModules||setStatic(theModule,'includedModules',___.freeze(module.includedModules)),primFreeze(theModule)}function
+loadModule(module){return freeze(module),markFuncFreeze(module.instantiate),callPub(myNewModuleHandler,'handle',[module])}registeredImports=[];function
+getId(imports){var id;return enforceType(imports,'object','imports'),'id___'in imports?(id=enforceType(imports.id___,'number','id')):(id=imports.id___=registeredImports.length),registeredImports[id]=imports,id}function
+getImports(id){var result=registeredImports[enforceType(id,'number','id')];return result===void
+0&&fail('imports#',id,' unregistered'),result}function unregister(imports){var id;enforceType(imports,'object','imports'),'id___'in
+imports&&(id=enforceType(imports.id___,'number','id'),registeredImports[id]=void
+0)}function identity(x){return x}function callWithEjector(attemptFunc,opt_failFunc){var
+failFunc=opt_failFunc||identity,disabled=false,token=new Token('ejection'),stash;token.UNCATCHABLE___=true,stash=void
+0;function ejector(result){if(disabled)cajita.fail('ejector disabled');else throw stash=result,token}markFuncFreeze(ejector);try{try{return callPub(attemptFunc,'call',[USELESS,ejector])}finally{disabled=true}}catch(e){if(e===token)return callPub(failFunc,'call',[USELESS,stash]);throw e}}function
+eject(opt_ejector,result){opt_ejector?(callPub(opt_ejector,'call',[USELESS,result]),fail('Ejector did not exit: ',opt_ejector)):fail(result)}function
+makeTrademark(typename,table){return typename=String(typename),primFreeze({'toString':markFuncFreeze(function(){return typename+'Mark'}),'stamp':primFreeze({'toString':markFuncFreeze(function(){return typename+'Stamp'}),'mark___':markFuncFreeze(function(obj){return table.set(obj,true),obj})}),'guard':{'toString':markFuncFreeze(function(){return typename+'T'}),'coerce':markFuncFreeze(function(specimen,opt_ejector){if(table.get(specimen))return specimen;eject(opt_ejector,'Specimen does not have the \"'+typename+'\" trademark')})}})}GuardMark=makeTrademark('Guard',newTable(true)),GuardT=GuardMark.guard,GuardStamp=GuardMark.stamp,primFreeze(GuardStamp.mark___(GuardT));function
+Trademark(typename){var result=makeTrademark(typename,newTable(true));return primFreeze(GuardStamp.mark___(result.guard)),result}markFuncFreeze(Trademark);function
+guard(g,specimen,opt_ejector){return g=GuardT.coerce(g),g.coerce(specimen,opt_ejector)}function
+passesGuard(g,specimen){return g=GuardT.coerce(g),callWithEjector(markFuncFreeze(function(opt_ejector){return g.coerce(specimen,opt_ejector),true}),markFuncFreeze(function(ignored){return false}))}function
+stamp(stamps,record){var i,numStamps;isRecord(record)||fail('Can only stamp records: ',record),isFrozen(record)&&fail('Can\'t stamp frozen objects: ',record),numStamps=stamps.length>>>0;for(i=0;i<numStamps;++i)'mark___'in
+stamps[i]||fail('Can\'t stamp with a non-stamp: ',stamps[i]);for(i=0;i<numStamps;++i)stamps[i].mark___(record);return freeze(record)}function
+makeSealerUnsealerPair(){var table=newTable(true),undefinedStandin={};function seal(payload){var
+box;return payload===void 0&&(payload=undefinedStandin),box=Token('(box)'),table.set(box,payload),box}function
+unseal(box){var payload=table.get(box);if(payload===void 0)fail('Sealer/Unsealer mismatch');else
+if(payload===undefinedStandin)return;else return payload}return freeze({'seal':markFuncFreeze(seal),'unseal':markFuncFreeze(unseal)})}function
+construct(ctor,args){var tmp;ctor=asCtor(ctor);switch(args.length){case 0:return new
+ctor;case 1:return new ctor(args[0]);case 2:return new ctor(args[0],args[1]);case
+3:return new ctor(args[0],args[1],args[2]);case 4:return new ctor(args[0],args[1],args[2],args[3]);case
+5:return new ctor(args[0],args[1],args[2],args[3],args[4]);case 6:return new ctor(args[0],args[1],args[2],args[3],args[4],args[5]);case
+7:return new ctor(args[0],args[1],args[2],args[3],args[4],args[5],args[6]);case 8:return new
+ctor(args[0],args[1],args[2],args[3],args[4],args[5],args[6],args[7]);case 9:return new
+ctor(args[0],args[1],args[2],args[3],args[4],args[5],args[6],args[7],args[8]);case
+10:return new ctor(args[0],args[1],args[2],args[3],args[4],args[5],args[6],args[7],args[8],args[9]);case
+11:return new ctor(args[0],args[1],args[2],args[3],args[4],args[5],args[6],args[7],args[8],args[9],args[10]);case
+12:return new ctor(args[0],args[1],args[2],args[3],args[4],args[5],args[6],args[7],args[8],args[9],args[10],args[11]);default:return ctor.typeTag___==='Array'?ctor.apply(USELESS,args):(tmp=function(args){return ctor.apply(this,args)},tmp.prototype=ctor.prototype,new
+tmp(args))}}magicCount=0,MAGIC_NUM=Math.random(),MAGIC_TOKEN=Token('MAGIC_TOKEN_FOR:'+MAGIC_NUM),MAGIC_NAME='_index;'+MAGIC_NUM+';';function
+newTable(opt_useKeyLifetime,opt_expectedSize){var myMagicIndexName,myValues;++magicCount,myMagicIndexName=MAGIC_NAME+magicCount+'___';function
+setOnKey(key,value){var ktype=typeof key,i,list;(!key||ktype!=='function'&&ktype!=='object')&&fail('Can\'t use key lifetime on primitive keys: ',key),list=key[myMagicIndexName];if(!list||list[0]!==key)key[myMagicIndexName]=[key,MAGIC_TOKEN,value];else{for(i=1;i<list.length;i+=2)if(list[i]===MAGIC_TOKEN)break;list[i]=MAGIC_TOKEN,list[i+1]=value}}function
+getOnKey(key){var ktype=typeof key,i,list;(!key||ktype!=='function'&&ktype!=='object')&&fail('Can\'t use key lifetime on primitive keys: ',key),list=key[myMagicIndexName];if(!list||list[0]!==key)return;for(i=1;i<list.length;i+=2)if(list[i]===MAGIC_TOKEN)return list[i+1];return}if(opt_useKeyLifetime)return primFreeze({'set':markFuncFreeze(setOnKey),'get':markFuncFreeze(getOnKey)});myValues=[];function
+setOnTable(key,value){var index;switch(typeof key){case'object':case'function':if(null===key)return myValues.prim_null=value,void
+0;index=getOnKey(key);if(value===void 0){if(index===void 0)return;setOnKey(key,void
+0)}else index===void 0&&(index=myValues.length,setOnKey(key,index));break;case'string':index='str_'+key;break;default:index='prim_'+key}value===void
+0?delete myValues[index]:(myValues[index]=value)}function getOnTable(key){var index;switch(typeof
+key){case'object':case'function':return null===key?myValues.prim_null:(index=getOnKey(key),void
+0===index?void 0:myValues[index]);case'string':return myValues['str_'+key];default:return myValues['prim_'+key]}}return primFreeze({'set':markFuncFreeze(setOnTable),'get':markFuncFreeze(getOnTable)})}function
+inheritsFrom(obj,allegedParent){if(null===obj)return false;if(void 0===obj)return false;if(typeOf(obj)==='function')return false;if(typeOf(allegedParent)!=='object')return false;if(null===allegedParent)return false;function
+F(){}return F.prototype=allegedParent,Object(obj)instanceof F}function getSuperCtor(func){var
+result;enforceType(func,'function');if(isCtor(func)||isFunc(func)){result=directConstructor(func.prototype);if(isCtor(result)||isFunc(result))return result}return}attribute=new
+RegExp('^([\\s\\S]*)_(?:canRead|canCall|getter|handler)___$');function getOwnPropertyNames(obj){var
+result=[],seen={},implicit=isJSONContainer(obj),base,k,match;for(k in obj)hasOwnProp(obj,k)&&(implicit&&!endsWith__.test(k)?myOriginalHOP.call(seen,k)||(seen[k]=true,result.push(k)):(match=attribute.exec(k),match!==null&&(base=match[1],myOriginalHOP.call(seen,base)||(seen[base]=true,result.push(base)))));return result}function
+getProtoPropertyNames(func){return enforceType(func,'function'),getOwnPropertyNames(func.prototype)}function
+getProtoPropertyValue(func,name){return asFirstClass(readPub(func.prototype,name))}function
+beget(parent){var result;return isRecord(parent)||fail('Can only beget() records: ',parent),result=primBeget(parent),result.RECORD___=result,result}function
+jsonParseOk(json){var x;try{return x=json.parse('{\"a\":3}'),x.a===3}catch(e){return false}}function
+jsonStringifyOk(json){var x;try{return x=json.stringify({'a':3,'b__':4},function
+replacer(k,v){return/__$/.test(k)?void 0:v}),x!=='{\"a\":3}'?false:(x=json.stringify(void
+0,'invalid'),x===void 0)}catch(e){return false}}goodJSON={},goodJSON.parse=jsonParseOk(global.JSON)?global.JSON.parse:json_sans_eval.parse,goodJSON.stringify=jsonStringifyOk(global.JSON)?global.JSON.stringify:json_sans_eval.stringify,safeJSON=primFreeze({'CLASS___':'JSON','parse':markFuncFreeze(function(text,opt_reviver){var
+reviver=void 0;return opt_reviver&&(opt_reviver=toFunc(opt_reviver),reviver=function(key,value){return opt_reviver.apply(this,arguments)}),goodJSON.parse(json_sans_eval.checkSyntax(text,function(key){return key!=='valueOf'&&key!=='toString'&&!endsWith__.test(key)}),reviver)}),'stringify':markFuncFreeze(function(obj,opt_replacer,opt_space){var
+replacer;switch(typeof opt_space){case'number':case'string':case'undefined':break;default:throw new
+TypeError('space must be a number or string')}return opt_replacer?(opt_replacer=toFunc(opt_replacer),replacer=function(key,value){return canReadPub(this,key)?opt_replacer.apply(this,arguments):void
+0}):(replacer=function(key,value){return canReadPub(this,key)?value:void 0}),goodJSON.stringify(obj,replacer,opt_space)})}),cajita={'log':log,'fail':fail,'enforce':enforce,'enforceType':enforceType,'directConstructor':directConstructor,'getFuncCategory':getFuncCategory,'isDirectInstanceOf':isDirectInstanceOf,'isInstanceOf':isInstanceOf,'isRecord':isRecord,'isArray':isArray,'isJSONContainer':isJSONContainer,'freeze':freeze,'isFrozen':isFrozen,'copy':copy,'snapshot':snapshot,'canReadPub':canReadPub,'readPub':readPub,'hasOwnPropertyOf':hasOwnPropertyOf,'readOwn':readOwn,'canEnumPub':canEnumPub,'canEnumOwn':canEnumOwn,'canInnocentEnum':canInnocentEnum,'BREAK':BREAK,'allKeys':allKeys,'forAllKeys':forAllKeys,'ownKeys':ownKeys,'forOwnKeys':forOwnKeys,'canCallPub':canCallPub,'callPub':callPub,'canSetPub':canSetPub,'setPub':setPub,'canDeletePub':canDeletePub,'deletePub':deletePub,'Token':Token,'identical':identical,'newTable':newTable,'identity':identity,'callWithEjector':callWithEjector,'eject':eject,'GuardT':GuardT,'Trademark':Trademark,'guard':guard,'passesGuard':passesGuard,'stamp':stamp,'makeSealerUnsealerPair':makeSealerUnsealerPair,'USELESS':USELESS,'manifest':manifest,'args':args,'construct':construct,'inheritsFrom':inheritsFrom,'getSuperCtor':getSuperCtor,'getOwnPropertyNames':getOwnPropertyNames,'getProtoPropertyNames':getProtoPropertyNames,'getProtoPropertyValue':getProtoPropertyValue,'beget':beget,'PseudoFunctionProto':PseudoFunctionProto,'PseudoFunction':PseudoFunction,'isPseudoFunc':isPseudoFunc,'enforceNat':deprecate(enforceNat,'___.enforceNat','Use (x === x >>> 0) instead as a UInt32 test')},forOwnKeys(cajita,markFuncFreeze(function(k,v){switch(typeOf(v)){case'object':v!==null&&primFreeze(v);break;case'function':markFuncFreeze(v)}})),sharedImports={'cajita':cajita,'null':null,'false':false,'true':true,'NaN':NaN,'Infinity':Infinity,'undefined':void
+0,'parseInt':markFuncFreeze(parseInt),'parseFloat':markFuncFreeze(parseFloat),'isNaN':markFuncFreeze(isNaN),'isFinite':markFuncFreeze(isFinite),'decodeURI':markFuncFreeze(decodeURI),'decodeURIComponent':markFuncFreeze(decodeURIComponent),'encodeURI':markFuncFreeze(encodeURI),'encodeURIComponent':markFuncFreeze(encodeURIComponent),'escape':escape?markFuncFreeze(escape):void
+0,'Math':Math,'JSON':safeJSON,'Object':Object,'Array':Array,'String':String,'Boolean':Boolean,'Number':Number,'Date':Date,'RegExp':RegExp,'Error':Error,'EvalError':EvalError,'RangeError':RangeError,'ReferenceError':ReferenceError,'SyntaxError':SyntaxError,'TypeError':TypeError,'URIError':URIError},forOwnKeys(sharedImports,markFuncFreeze(function(k,v){switch(typeOf(v)){case'object':v!==null&&primFreeze(v);break;case'function':primFreeze(v)}})),primFreeze(sharedImports),___={'getLogFunc':getLogFunc,'setLogFunc':setLogFunc,'primFreeze':primFreeze,'canRead':canRead,'grantRead':grantRead,'canEnum':canEnum,'grantEnum':grantEnum,'canCall':canCall,'canSet':canSet,'grantSet':grantSet,'canDelete':canDelete,'grantDelete':grantDelete,'readImport':readImport,'isCtor':isCtor,'isFunc':isFunc,'markCtor':markCtor,'extend':extend,'markFuncFreeze':markFuncFreeze,'markXo4a':markXo4a,'markInnocent':markInnocent,'asFunc':asFunc,'toFunc':toFunc,'inPub':inPub,'canSetStatic':canSetStatic,'setStatic':setStatic,'typeOf':typeOf,'hasOwnProp':hasOwnProp,'deleteFieldEntirely':deleteFieldEntirely,'tameException':tameException,'primBeget':primBeget,'callStackUnsealer':callStackUnsealer,'RegExp':RegExp,'GuardStamp':GuardStamp,'asFirstClass':asFirstClass,'initializeMap':initializeMap,'iM':initializeMap,'useGetHandler':useGetHandler,'useSetHandler':useSetHandler,'grantFunc':grantFunc,'grantGenericMethod':grantGenericMethod,'handleGenericMethod':handleGenericMethod,'grantTypedMethod':grantTypedMethod,'grantMutatingMethod':grantMutatingMethod,'grantInnocentMethod':grantInnocentMethod,'enforceMatchable':enforceMatchable,'all2':all2,'tamesTo':tamesTo,'tamesToSelf':tamesToSelf,'tame':tame,'untame':untame,'getNewModuleHandler':getNewModuleHandler,'setNewModuleHandler':setNewModuleHandler,'obtainNewModule':obtainNewModule,'makeNormalNewModuleHandler':makeNormalNewModuleHandler,'loadModule':loadModule,'prepareModule':prepareModule,'NO_RESULT':NO_RESULT,'getId':getId,'getImports':getImports,'unregister':unregister,'grantEnumOnly':deprecate(grantEnum,'___.grantEnumOnly','Use ___.grantEnum instead.'),'grantCall':deprecate(grantGenericMethod,'___.grantCall','Choose a method tamer (e.g., ___.grantFunc,___.grantGenericMethod,etc) according to the safety properties of calling and reading the method.'),'grantGeneric':deprecate(grantGenericMethod,'___.grantGeneric','Use ___.grantGenericMethod instead.'),'useApplyHandler':deprecate(useApplyHandler,'___.useApplyHandler','Use ___.handleGenericMethod instead.'),'useCallHandler':deprecate(useCallHandler,'___.useCallHandler','Use ___.handleGenericMethod instead.'),'handleGeneric':deprecate(useCallHandler,'___.handleGeneric','Use ___.handleGenericMethod instead.'),'grantTypedGeneric':deprecate(useCallHandler,'___.grantTypedGeneric','Use ___.grantTypedMethod instead.'),'grantMutator':deprecate(useCallHandler,'___.grantMutator','Use ___.grantMutatingMethod instead.'),'useDeleteHandler':deprecate(useDeleteHandler,'___.useDeleteHandler','Refactor to avoid needing to handle deletions.'),'isXo4aFunc':deprecate(isXo4aFunc,'___.isXo4aFunc','Refactor to avoid needing to dynamically test whether a function is marked exophoric.'),'xo4a':deprecate(markXo4a,'___.xo4a','Consider refactoring to avoid needing to explicitly mark a function as exophoric. Use one of the exophoric method tamers (e.g., ___.grantGenericMethod) instead.Otherwise, use ___.markXo4a instead.'),'ctor':deprecate(markCtor,'___.ctor','Use ___.markCtor instead.'),'func':deprecate(markFuncFreeze,'___.func','___.func should not be called from manually written code.'),'frozenFunc':deprecate(markFuncFreeze,'___.frozenFunc','Use ___.markFuncFreeze instead.'),'markFuncOnly':deprecate(markFuncFreeze,'___.markFuncOnly','___.markFuncOnly should not be called from manually written code.'),'sharedImports':sharedImports},forOwnKeys(cajita,markFuncFreeze(function(k,v){k
+in ___&&fail('internal: initialization conflict: ',k),typeOf(v)==='function'&&grantFunc(cajita,k),___[k]=v})),setNewModuleHandler(makeNormalNewModuleHandler())})(this),unicode={},unicode.BASE_CHAR='A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u0131\u0134-\u013e\u0141-\u0148\u014a-\u017e\u0180-\u01c3\u01cd-\u01f0\u01f4-\u01f5\u01fa-\u0217\u0250-\u02a8\u02bb-\u02c1\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03ce\u03d0-\u03d6\u03da\u03dc\u03de\u03e0\u03e2-\u03f3\u0401-\u040c\u040e-\u044f\u0451-\u045c\u045e-\u0481\u0490-\u04c4\u04c7-\u04c8\u04cb-\u04cc\u04d0-\u04eb\u04ee-\u04f5\u04f8-\u04f9\u0531-\u0556\u0559\u0561-\u0586\u05d0-\u05ea\u05f0-\u05f2\u0621-\u063a\u0641-\u064a\u0671-\u06b7\u06ba-\u06be\u06c0-\u06ce\u06d0-\u06d3\u06d5\u06e5-\u06e6\u0905-\u0939\u093d\u0958-\u0961\u0985-\u098c\u098f-\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09dc-\u09dd\u09df-\u09e1\u09f0-\u09f1\u0a05-\u0a0a\u0a0f-\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32-\u0a33\u0a35-\u0a36\u0a38-\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8b\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2-\u0ab3\u0ab5-\u0ab9\u0abd\u0ae0\u0b05-\u0b0c\u0b0f-\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32-\u0b33\u0b36-\u0b39\u0b3d\u0b5c-\u0b5d\u0b5f-\u0b61\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99-\u0b9a\u0b9c\u0b9e-\u0b9f\u0ba3-\u0ba4\u0ba8-\u0baa\u0bae-\u0bb5\u0bb7-\u0bb9\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c60-\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cde\u0ce0-\u0ce1\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d28\u0d2a-\u0d39\u0d60-\u0d61\u0e01-\u0e2e\u0e30\u0e32-\u0e33\u0e40-\u0e45\u0e81-\u0e82\u0e84\u0e87-\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa-\u0eab\u0ead-\u0eae\u0eb0\u0eb2-\u0eb3\u0ebd\u0ec0-\u0ec4\u0f40-\u0f47\u0f49-\u0f69\u10a0-\u10c5\u10d0-\u10f6\u1100\u1102-\u1103\u1105-\u1107\u1109\u110b-\u110c\u110e-\u1112\u113c\u113e\u1140\u114c\u114e\u1150\u1154-\u1155\u1159\u115f-\u1161\u1163\u1165\u1167\u1169\u116d-\u116e\u1172-\u1173\u1175\u119e\u11a8\u11ab\u11ae-\u11af\u11b7-\u11b8\u11ba\u11bc-\u11c2\u11eb\u11f0\u11f9\u1e00-\u1e9b\u1ea0-\u1ef9\u1f00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2126\u212a-\u212b\u212e\u2180-\u2182\u3041-\u3094\u30a1-\u30fa\u3105-\u312c\uac00-\ud7a3',unicode.IDEOGRAPHIC='\u4e00-\u9fa5\u3007\u3021-\u3029',unicode.LETTER=unicode.BASE_CHAR+unicode.IDEOGRAPHIC,unicode.COMBINING_CHAR='\u0300-\u0345\u0360-\u0361\u0483-\u0486\u0591-\u05a1\u05a3-\u05b9\u05bb-\u05bd\u05bf\u05c1-\u05c2\u05c4\u064b-\u0652\u0670\u06d6-\u06dc\u06dd-\u06df\u06e0-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0901-\u0903\u093c\u093e-\u094c\u094d\u0951-\u0954\u0962-\u0963\u0981-\u0983\u09bc\u09be\u09bf\u09c0-\u09c4\u09c7-\u09c8\u09cb-\u09cd\u09d7\u09e2-\u09e3\u0a02\u0a3c\u0a3e\u0a3f\u0a40-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a70-\u0a71\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0b01-\u0b03\u0b3c\u0b3e-\u0b43\u0b47-\u0b48\u0b4b-\u0b4d\u0b56-\u0b57\u0b82-\u0b83\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c82-\u0c83\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5-\u0cd6\u0d02-\u0d03\u0d3e-\u0d43\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86-\u0f8b\u0f90-\u0f95\u0f97\u0f99-\u0fad\u0fb1-\u0fb7\u0fb9\u20d0-\u20dc\u20e1\u302a-\u302f\u3099\u309a',unicode.DIGIT='0-9\u0660-\u0669\u06f0-\u06f9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be7-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29',unicode.EXTENDER='\xb7\u02d0\u02d1\u0387\u0640\u0e46\u0ec6\u3005\u3031-\u3035\u309d-\u309e\u30fc-\u30fe',css={'properties':(function(){var
+s=['|left|center|right','|top|center|bottom','#(?:[\\da-f]{3}){1,2}|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|rgb\\(\\s*(?:-?\\d+|0|[+\\-]?\\d+(?:\\.\\d+)?%)\\s*,\\s*(?:-?\\d+|0|[+\\-]?\\d+(?:\\.\\d+)?%)\\s*,\\s*(?:-?\\d+|0|[+\\-]?\\d+(?:\\.\\d+)?%)\\)','[+\\-]?\\d+(?:\\.\\d+)?(?:[cem]m|ex|in|p[ctx])','\\d+(?:\\.\\d+)?(?:[cem]m|ex|in|p[ctx])','none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset','[+\\-]?\\d+(?:\\.\\d+)?%','\\d+(?:\\.\\d+)?%','url\\(\"[^()\\\\\"\\r\\n]+\"\\)','repeat-x|repeat-y|(?:repeat|space|round|no-repeat)(?:\\s+(?:repeat|space|round|no-repeat)){0,2}'],c=[RegExp('^\\s*(?:\\s*(?:0|'+s[3]+'|'+s[6]+')){1,2}\\s*$','i'),RegExp('^\\s*(?:\\s*(?:0|'+s[3]+'|'+s[6]+')){1,4}(?:\\s*\\/(?:\\s*(?:0|'+s[3]+'|'+s[6]+')){1,4})?\\s*$','i'),RegExp('^\\s*(?:\\s*none|(?:(?:\\s*(?:'+s[2]+')\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*inset)?|(?:\\s*inset)?\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*(?:'+s[2]+'))?)\\s*,)*(?:\\s*(?:'+s[2]+')\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*inset)?|(?:\\s*inset)?\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*(?:'+s[2]+'))?))\\s*$','i'),RegExp('^\\s*(?:'+s[2]+'|transparent|inherit)\\s*$','i'),RegExp('^\\s*(?:'+s[5]+'|inherit)\\s*$','i'),RegExp('^\\s*(?:thin|medium|thick|0|'+s[3]+'|inherit)\\s*$','i'),RegExp('^\\s*(?:(?:thin|medium|thick|0|'+s[3]+'|'+s[5]+'|'+s[2]+'|transparent|inherit)(?:\\s+(?:thin|medium|thick|0|'+s[3]+')|\\s+(?:'+s[5]+')|\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent|\\s+inherit){0,2}|inherit)\\s*$','i'),/^\s*(?:none|inherit)\s*$/i,RegExp('^\\s*(?:'+s[8]+'|none|inherit)\\s*$','i'),RegExp('^\\s*(?:0|'+s[3]+'|'+s[6]+'|auto|inherit)\\s*$','i'),RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|none|inherit|auto)\\s*$','i'),RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|inherit|auto)\\s*$','i'),/^\s*(?:0(?:\.\d+)?|\.\d+|1(?:\.0+)?|inherit)\s*$/i,RegExp('^\\s*(?:(?:'+s[2]+'|invert|inherit|'+s[5]+'|thin|medium|thick|0|'+s[3]+')(?:\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+invert|\\s+inherit|\\s+(?:'+s[5]+'|inherit)|\\s+(?:thin|medium|thick|0|'+s[3]+'|inherit)){0,2}|inherit)\\s*$','i'),RegExp('^\\s*(?:'+s[2]+'|invert|inherit)\\s*$','i'),/^\s*(?:visible|hidden|scroll|auto|no-display|no-content)\s*$/i,RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|inherit)\\s*$','i'),/^\s*(?:auto|always|avoid|left|right|inherit)\s*$/i,RegExp('^\\s*(?:0|[+\\-]?\\d+(?:\\.\\d+)?m?s|'+s[6]+'|inherit)\\s*$','i'),/^\s*(?:0|[+\-]?\d+(?:\.\d+)?|inherit)\s*$/i,/^\s*(?:clip|ellipsis)\s*$/i,RegExp('^\\s*(?:normal|0|'+s[3]+'|inherit)\\s*$','i')];return{'-moz-border-radius':c[1],'-moz-border-radius-bottomleft':c[0],'-moz-border-radius-bottomright':c[0],'-moz-border-radius-topleft':c[0],'-moz-border-radius-topright':c[0],'-moz-box-shadow':c[2],'-moz-opacity':c[12],'-moz-outline':c[13],'-moz-outline-color':c[14],'-moz-outline-style':c[4],'-moz-outline-width':c[5],'-o-text-overflow':c[20],'-webkit-border-bottom-left-radius':c[0],'-webkit-border-bottom-right-radius':c[0],'-webkit-border-radius':c[1],'-webkit-border-radius-bottom-left':c[0],'-webkit-border-radius-bottom-right':c[0],'-webkit-border-radius-top-left':c[0],'-webkit-border-radius-top-right':c[0],'-webkit-border-top-left-radius':c[0],'-webkit-border-top-right-radius':c[0],'-webkit-box-shadow':c[2],'azimuth':/^\s*(?:0|[+\-]?\d+(?:\.\d+)?(?:g?rad|deg)|(?:left-side|far-left|left|center-left|center|center-right|right|far-right|right-side|behind)(?:\s+(?:left-side|far-left|left|center-left|center|center-right|right|far-right|right-side|behind))?|leftwards|rightwards|inherit)\s*$/i,'background':RegExp('^\\s*(?:\\s*(?:'+s[8]+'|none|(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?)(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|'+s[9]+'|scroll|fixed|local|(?:border|padding|content)-box)(?:\\s*'+s[8]+'|\\s+none|(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)){1,2})(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|\\s+repeat-x|\\s+repeat-y|(?:\\s+(?:repeat|space|round|no-repeat)){1,2}|\\s+(?:scroll|fixed|local)|\\s+(?:border|padding|content)-box){0,4}\\s*,)*\\s*(?:'+s[2]+'|transparent|inherit|'+s[8]+'|none|(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?)(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|'+s[9]+'|scroll|fixed|local|(?:border|padding|content)-box)(?:\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent|\\s+inherit|\\s*'+s[8]+'|\\s+none|(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)){1,2})(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|\\s+repeat-x|\\s+repeat-y|(?:\\s+(?:repeat|space|round|no-repeat)){1,2}|\\s+(?:scroll|fixed|local)|\\s+(?:border|padding|content)-box){0,5}\\s*$','i'),'background-attachment':/^\s*(?:scroll|fixed|local)(?:\s*,\s*(?:scroll|fixed|local))*\s*$/i,'background-color':c[3],'background-image':RegExp('^\\s*(?:'+s[8]+'|none)(?:\\s*,\\s*(?:'+s[8]+'|none))*\\s*$','i'),'background-position':RegExp('^\\s*(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?)(?:\\s*,\\s*(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?))*\\s*$','i'),'background-repeat':RegExp('^\\s*(?:'+s[9]+')(?:\\s*,\\s*(?:'+s[9]+'))*\\s*$','i'),'border':RegExp('^\\s*(?:(?:thin|medium|thick|0|'+s[3]+'|'+s[5]+'|'+s[2]+'|transparent)(?:\\s+(?:thin|medium|thick|0|'+s[3]+')|\\s+(?:'+s[5]+')|\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent){0,2}|inherit)\\s*$','i'),'border-bottom':c[6],'border-bottom-color':c[3],'border-bottom-left-radius':c[0],'border-bottom-right-radius':c[0],'border-bottom-style':c[4],'border-bottom-width':c[5],'border-collapse':/^\s*(?:collapse|separate|inherit)\s*$/i,'border-color':RegExp('^\\s*(?:(?:'+s[2]+'|transparent)(?:\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent){0,4}|inherit)\\s*$','i'),'border-left':c[6],'border-left-color':c[3],'border-left-style':c[4],'border-left-width':c[5],'border-radius':c[1],'border-right':c[6],'border-right-color':c[3],'border-right-style':c[4],'border-right-width':c[5],'border-spacing':RegExp('^\\s*(?:(?:\\s*(?:0|'+s[3]+')){1,2}|\\s*inherit)\\s*$','i'),'border-style':RegExp('^\\s*(?:(?:'+s[5]+')(?:\\s+(?:'+s[5]+')){0,4}|inherit)\\s*$','i'),'border-top':c[6],'border-top-color':c[3],'border-top-left-radius':c[0],'border-top-right-radius':c[0],'border-top-style':c[4],'border-top-width':c[5],'border-width':RegExp('^\\s*(?:(?:thin|medium|thick|0|'+s[3]+')(?:\\s+(?:thin|medium|thick|0|'+s[3]+')){0,4}|inherit)\\s*$','i'),'bottom':c[9],'box-shadow':c[2],'caption-side':/^\s*(?:top|bottom|inherit)\s*$/i,'clear':/^\s*(?:none|left|right|both|inherit)\s*$/i,'clip':RegExp('^\\s*(?:rect\\(\\s*(?:0|'+s[3]+'|auto)\\s*,\\s*(?:0|'+s[3]+'|auto)\\s*,\\s*(?:0|'+s[3]+'|auto)\\s*,\\s*(?:0|'+s[3]+'|auto)\\)|auto|inherit)\\s*$','i'),'color':RegExp('^\\s*(?:'+s[2]+'|inherit)\\s*$','i'),'counter-increment':c[7],'counter-reset':c[7],'cue':RegExp('^\\s*(?:(?:'+s[8]+'|none|inherit)(?:\\s*'+s[8]+'|\\s+none|\\s+inherit)?|inherit)\\s*$','i'),'cue-after':c[8],'cue-before':c[8],'cursor':RegExp('^\\s*(?:(?:\\s*'+s[8]+'\\s*,)*\\s*(?:auto|crosshair|default|pointer|move|e-resize|ne-resize|nw-resize|n-resize|se-resize|sw-resize|s-resize|w-resize|text|wait|help|progress|all-scroll|col-resize|hand|no-drop|not-allowed|row-resize|vertical-text)|\\s*inherit)\\s*$','i'),'direction':/^\s*(?:ltr|rtl|inherit)\s*$/i,'display':/^\s*(?:inline|block|list-item|run-in|inline-block|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|none|inherit|-moz-inline-box|-moz-inline-stack)\s*$/i,'elevation':/^\s*(?:0|[+\-]?\d+(?:\.\d+)?(?:g?rad|deg)|below|level|above|higher|lower|inherit)\s*$/i,'empty-cells':/^\s*(?:show|hide|inherit)\s*$/i,'filter':RegExp('^\\s*(?:\\s*alpha\\(\\s*opacity\\s*=\\s*(?:0|'+s[6]+'|[+\\-]?\\d+(?:\\.\\d+)?)\\))+\\s*$','i'),'float':/^\s*(?:left|right|none|inherit)\s*$/i,'font':RegExp('^\\s*(?:(?:normal|italic|oblique|inherit|small-caps|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)(?:\\s+(?:normal|italic|oblique|inherit|small-caps|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)){0,2}\\s+(?:xx-small|x-small|small|medium|large|x-large|xx-large|(?:small|larg)er|0|'+s[4]+'|'+s[7]+'|inherit)(?:\\s*\\/\\s*(?:normal|0|\\d+(?:\\.\\d+)?|'+s[4]+'|'+s[7]+'|inherit))?(?:(?:\\s*\"\\w(?:[\\w-]*\\w)(?:\\s+\\w([\\w-]*\\w))*\"|\\s+(?:serif|sans-serif|cursive|fantasy|monospace))(?:\\s*,\\s*(?:\"\\w(?:[\\w-]*\\w)(?:\\s+\\w([\\w-]*\\w))*\"|serif|sans-serif|cursive|fantasy|monospace))*|\\s+inherit)|caption|icon|menu|message-box|small-caption|status-bar|inherit)\\s*$','i'),'font-family':/^\s*(?:(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|serif|sans-serif|cursive|fantasy|monospace)(?:\s*,\s*(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|serif|sans-serif|cursive|fantasy|monospace))*|inherit)\s*$/i,'font-size':RegExp('^\\s*(?:xx-small|x-small|small|medium|large|x-large|xx-large|(?:small|larg)er|0|'+s[4]+'|'+s[7]+'|inherit)\\s*$','i'),'font-stretch':/^\s*(?:normal|wider|narrower|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)\s*$/i,'font-style':/^\s*(?:normal|italic|oblique|inherit)\s*$/i,'font-variant':/^\s*(?:normal|small-caps|inherit)\s*$/i,'font-weight':/^\s*(?:normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit)\s*$/i,'height':c[9],'left':c[9],'letter-spacing':c[21],'line-height':RegExp('^\\s*(?:normal|0|\\d+(?:\\.\\d+)?|'+s[4]+'|'+s[7]+'|inherit)\\s*$','i'),'list-style':RegExp('^\\s*(?:(?:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-latin|upper-latin|armenian|georgian|lower-alpha|upper-alpha|none|inherit|inside|outside|'+s[8]+')(?:\\s+(?:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-latin|upper-latin|armenian|georgian|lower-alpha|upper-alpha|none|inherit)|\\s+(?:inside|outside|inherit)|\\s*'+s[8]+'|\\s+none|\\s+inherit){0,2}|inherit)\\s*$','i'),'list-style-image':c[8],'list-style-position':/^\s*(?:inside|outside|inherit)\s*$/i,'list-style-type':/^\s*(?:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-latin|upper-latin|armenian|georgian|lower-alpha|upper-alpha|none|inherit)\s*$/i,'margin':RegExp('^\\s*(?:(?:0|'+s[3]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[3]+'|'+s[6]+'|auto)){0,4}|inherit)\\s*$','i'),'margin-bottom':c[9],'margin-left':c[9],'margin-right':c[9],'margin-top':c[9],'max-height':c[10],'max-width':c[10],'min-height':c[11],'min-width':c[11],'opacity':c[12],'outline':c[13],'outline-color':c[14],'outline-style':c[4],'outline-width':c[5],'overflow':/^\s*(?:visible|hidden|scroll|auto|inherit)\s*$/i,'overflow-x':c[15],'overflow-y':c[15],'padding':RegExp('^\\s*(?:(?:\\s*(?:0|'+s[4]+'|'+s[7]+')){1,4}|\\s*inherit)\\s*$','i'),'padding-bottom':c[16],'padding-left':c[16],'padding-right':c[16],'padding-top':c[16],'page-break-after':c[17],'page-break-before':c[17],'page-break-inside':/^\s*(?:avoid|auto|inherit)\s*$/i,'pause':RegExp('^\\s*(?:(?:\\s*(?:0|[+\\-]?\\d+(?:\\.\\d+)?m?s|'+s[6]+')){1,2}|\\s*inherit)\\s*$','i'),'pause-after':c[18],'pause-before':c[18],'pitch':/^\s*(?:0|\d+(?:\.\d+)?k?Hz|x-low|low|medium|high|x-high|inherit)\s*$/i,'pitch-range':c[19],'play-during':RegExp('^\\s*(?:'+s[8]+'\\s*(?:mix|repeat)(?:\\s+(?:mix|repeat))?|auto|none|inherit)\\s*$','i'),'position':/^\s*(?:static|relative|absolute|inherit)\s*$/i,'quotes':c[7],'richness':c[19],'right':c[9],'speak':/^\s*(?:normal|none|spell-out|inherit)\s*$/i,'speak-header':/^\s*(?:once|always|inherit)\s*$/i,'speak-numeral':/^\s*(?:digits|continuous|inherit)\s*$/i,'speak-punctuation':/^\s*(?:code|none|inherit)\s*$/i,'speech-rate':/^\s*(?:0|[+\-]?\d+(?:\.\d+)?|x-slow|slow|medium|fast|x-fast|faster|slower|inherit)\s*$/i,'stress':c[19],'table-layout':/^\s*(?:auto|fixed|inherit)\s*$/i,'text-align':/^\s*(?:left|right|center|justify|inherit)\s*$/i,'text-decoration':/^\s*(?:none|(?:underline|overline|line-through|blink)(?:\s+(?:underline|overline|line-through|blink)){0,3}|inherit)\s*$/i,'text-indent':RegExp('^\\s*(?:0|'+s[3]+'|'+s[6]+'|inherit)\\s*$','i'),'text-overflow':c[20],'text-shadow':c[2],'text-transform':/^\s*(?:capitalize|uppercase|lowercase|none|inherit)\s*$/i,'text-wrap':/^\s*(?:normal|unrestricted|none|suppress)\s*$/i,'top':c[9],'unicode-bidi':/^\s*(?:normal|embed|bidi-override|inherit)\s*$/i,'vertical-align':RegExp('^\\s*(?:baseline|sub|super|top|text-top|middle|bottom|text-bottom|0|'+s[6]+'|'+s[3]+'|inherit)\\s*$','i'),'visibility':/^\s*(?:visible|hidden|collapse|inherit)\s*$/i,'voice-family':/^\s*(?:(?:\s*(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|male|female|child)\s*,)*\s*(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|male|female|child)|\s*inherit)\s*$/i,'volume':RegExp('^\\s*(?:0|\\d+(?:\\.\\d+)?|'+s[7]+'|silent|x-soft|soft|medium|loud|x-loud|inherit)\\s*$','i'),'white-space':/^\s*(?:normal|pre|nowrap|pre-wrap|pre-line|inherit|-o-pre-wrap|-moz-pre-wrap|-pre-wrap)\s*$/i,'width':RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|auto|inherit)\\s*$','i'),'word-spacing':c[21],'word-wrap':/^\s*(?:normal|break-word)\s*$/i,'z-index':/^\s*(?:auto|-?\d+|inherit)\s*$/i,'zoom':RegExp('^\\s*(?:normal|0|\\d+(?:\\.\\d+)?|'+s[7]+')\\s*$','i')}})(),'alternates':{'MozBoxShadow':['boxShadow'],'WebkitBoxShadow':['boxShadow'],'float':['cssFloat','styleFloat']},'HISTORY_INSENSITIVE_STYLE_WHITELIST':{'display':true,'filter':true,'float':true,'height':true,'left':true,'opacity':true,'overflow':true,'position':true,'right':true,'top':true,'visibility':true,'width':true,'padding-left':true,'padding-right':true,'padding-top':true,'padding-bottom':true}},html4={},html4
+.atype={'NONE':0,'URI':1,'URI_FRAGMENT':11,'SCRIPT':2,'STYLE':3,'ID':4,'IDREF':5,'IDREFS':6,'GLOBAL_NAME':7,'LOCAL_NAME':8,'CLASSES':9,'FRAME_TARGET':10},html4
+.ATTRIBS={'*::class':9,'*::dir':0,'*::id':4,'*::lang':0,'*::onclick':2,'*::ondblclick':2,'*::onkeydown':2,'*::onkeypress':2,'*::onkeyup':2,'*::onload':2,'*::onmousedown':2,'*::onmousemove':2,'*::onmouseout':2,'*::onmouseover':2,'*::onmouseup':2,'*::style':3,'*::title':0,'a::accesskey':0,'a::coords':0,'a::href':1,'a::hreflang':0,'a::name':7,'a::onblur':2,'a::onfocus':2,'a::rel':0,'a::rev':0,'a::shape':0,'a::tabindex':0,'a::target':10,'a::type':0,'area::accesskey':0,'area::alt':0,'area::coords':0,'area::href':1,'area::nohref':0,'area::onblur':2,'area::onfocus':2,'area::shape':0,'area::tabindex':0,'area::target':10,'bdo::dir':0,'blockquote::cite':1,'br::clear':0,'button::accesskey':0,'button::disabled':0,'button::name':8,'button::onblur':2,'button::onfocus':2,'button::tabindex':0,'button::type':0,'button::value':0,'caption::align':0,'col::align':0,'col::char':0,'col::charoff':0,'col::span':0,'col::valign':0,'col::width':0,'colgroup::align':0,'colgroup::char':0,'colgroup::charoff':0,'colgroup::span':0,'colgroup::valign':0,'colgroup::width':0,'del::cite':1,'del::datetime':0,'dir::compact':0,'div::align':0,'dl::compact':0,'font::color':0,'font::face':0,'font::size':0,'form::accept':0,'form::action':1,'form::autocomplete':0,'form::enctype':0,'form::method':0,'form::name':7,'form::onreset':2,'form::onsubmit':2,'form::target':10,'h1::align':0,'h2::align':0,'h3::align':0,'h4::align':0,'h5::align':0,'h6::align':0,'hr::align':0,'hr::noshade':0,'hr::size':0,'hr::width':0,'iframe::align':0,'iframe::frameborder':0,'iframe::height':0,'iframe::marginheight':0,'iframe::marginwidth':0,'iframe::width':0,'img::align':0,'img::alt':0,'img::border':0,'img::height':0,'img::hspace':0,'img::ismap':0,'img::name':7,'img::src':1,'img::usemap':11,'img::vspace':0,'img::width':0,'input::accept':0,'input::accesskey':0,'input::align':0,'input::alt':0,'input::autocomplete':0,'input::checked':0,'input::disabled':0,'input::ismap':0,'input::maxlength':0,'input::name':8,'input::onblur':2,'input::onchange':2,'input::onfocus':2,'input::onselect':2,'input::readonly':0,'input::size':0,'input::src':1,'input::tabindex':0,'input::type':0,'input::usemap':11,'input::value':0,'ins::cite':1,'ins::datetime':0,'label::accesskey':0,'label::for':5,'label::onblur':2,'label::onfocus':2,'legend::accesskey':0,'legend::align':0,'li::type':0,'li::value':0,'map::name':7,'menu::compact':0,'ol::compact':0,'ol::start':0,'ol::type':0,'optgroup::disabled':0,'optgroup::label':0,'option::disabled':0,'option::label':0,'option::selected':0,'option::value':0,'p::align':0,'pre::width':0,'q::cite':1,'select::disabled':0,'select::multiple':0,'select::name':8,'select::onblur':2,'select::onchange':2,'select::onfocus':2,'select::size':0,'select::tabindex':0,'table::align':0,'table::bgcolor':0,'table::border':0,'table::cellpadding':0,'table::cellspacing':0,'table::frame':0,'table::rules':0,'table::summary':0,'table::width':0,'tbody::align':0,'tbody::char':0,'tbody::charoff':0,'tbody::valign':0,'td::abbr':0,'td::align':0,'td::axis':0,'td::bgcolor':0,'td::char':0,'td::charoff':0,'td::colspan':0,'td::headers':6,'td::height':0,'td::nowrap':0,'td::rowspan':0,'td::scope':0,'td::valign':0,'td::width':0,'textarea::accesskey':0,'textarea::cols':0,'textarea::disabled':0,'textarea::name':8,'textarea::onblur':2,'textarea::onchange':2,'textarea::onfocus':2,'textarea::onselect':2,'textarea::readonly':0,'textarea::rows':0,'textarea::tabindex':0,'tfoot::align':0,'tfoot::char':0,'tfoot::charoff':0,'tfoot::valign':0,'th::abbr':0,'th::align':0,'th::axis':0,'th::bgcolor':0,'th::char':0,'th::charoff':0,'th::colspan':0,'th::headers':6,'th::height':0,'th::nowrap':0,'th::rowspan':0,'th::scope':0,'th::valign':0,'th::width':0,'thead::align':0,'thead::char':0,'thead::charoff':0,'thead::valign':0,'tr::align':0,'tr::bgcolor':0,'tr::char':0,'tr::charoff':0,'tr::valign':0,'ul::compact':0,'ul::type':0},html4
+.eflags={'OPTIONAL_ENDTAG':1,'EMPTY':2,'CDATA':4,'RCDATA':8,'UNSAFE':16,'FOLDABLE':32,'SCRIPT':64,'STYLE':128},html4
+.ELEMENTS={'a':0,'abbr':0,'acronym':0,'address':0,'applet':16,'area':2,'b':0,'base':18,'basefont':18,'bdo':0,'big':0,'blockquote':0,'body':49,'br':2,'button':0,'caption':0,'center':0,'cite':0,'code':0,'col':2,'colgroup':1,'dd':1,'del':0,'dfn':0,'dir':0,'div':0,'dl':0,'dt':1,'em':0,'fieldset':0,'font':0,'form':0,'frame':18,'frameset':16,'h1':0,'h2':0,'h3':0,'h4':0,'h5':0,'h6':0,'head':49,'hr':2,'html':49,'i':0,'iframe':4,'img':2,'input':2,'ins':0,'isindex':18,'kbd':0,'label':0,'legend':0,'li':1,'link':18,'map':0,'menu':0,'meta':18,'noframes':20,'noscript':20,'object':16,'ol':0,'optgroup':0,'option':1,'p':1,'param':18,'pre':0,'q':0,'s':0,'samp':0,'script':84,'select':0,'small':0,'span':0,'strike':0,'strong':0,'style':148,'sub':0,'sup':0,'table':0,'tbody':1,'td':1,'textarea':8,'tfoot':1,'th':1,'thead':1,'title':24,'tr':1,'tt':0,'u':0,'ul':0,'var':0},html=(function(){var
+ENTITIES,INSIDE_TAG_TOKEN,OUTSIDE_TAG_TOKEN,ampRe,decimalEscapeRe,entityRe,eqRe,gtRe,hexEscapeRe,lcase,looseAmpRe,ltRe,nulRe,quotRe;'script'==='SCRIPT'.toLowerCase()?(lcase=function(s){return s.toLowerCase()}):(lcase=function(s){return s.replace(/[A-Z]/g,function(ch){return String.fromCharCode(ch.charCodeAt(0)|32)})}),ENTITIES={'lt':'<','gt':'>','amp':'&','nbsp':'\xa0','quot':'\"','apos':'\''},decimalEscapeRe=/^#(\d+)$/,hexEscapeRe=/^#x([0-9A-Fa-f]+)$/;function
+lookupEntity(name){var m;return name=lcase(name),ENTITIES.hasOwnProperty(name)?ENTITIES[name]:(m=name.match(decimalEscapeRe),m?String.fromCharCode(parseInt(m[1],10)):(m=name.match(hexEscapeRe))?String.fromCharCode(parseInt(m[1],16)):'')}function
+decodeOneEntity(_,name){return lookupEntity(name)}nulRe=/\0/g;function stripNULs(s){return s.replace(nulRe,'')}entityRe=/&(#\d+|#x[0-9A-Fa-f]+|\w+);/g;function
+unescapeEntities(s){return s.replace(entityRe,decodeOneEntity)}ampRe=/&/g,looseAmpRe=/&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi,ltRe=/</g,gtRe=/>/g,quotRe=/\"/g,eqRe=/\=/g;function
+escapeAttrib(s){return s.replace(ampRe,'&amp;').replace(ltRe,'&lt;').replace(gtRe,'&gt;').replace(quotRe,'&#34;').replace(eqRe,'&#61;')}function
+normalizeRCData(rcdata){return rcdata.replace(looseAmpRe,'&amp;$1').replace(ltRe,'&lt;').replace(gtRe,'&gt;')}INSIDE_TAG_TOKEN=new
+RegExp('^\\s*(?:(?:([a-z][a-z-]*)(\\s*=\\s*(\"[^\"]*\"|\'[^\']*\'|(?=[a-z][a-z-]*\\s*=)|[^>\"\'\\s]*))?)|(/?>)|.[^a-z\\s>]*)','i'),OUTSIDE_TAG_TOKEN=new
+RegExp('^(?:&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);|<!--[\\s\\S]*?-->|<!\\w[^>]*>|<\\?[^>*]*>|<(/)?([a-z][a-z0-9]*)|([^<&>]+)|([<&>]))','i');function
+makeSaxParser(handler){return function parse(htmlText,param){var attribName,attribs,dataEnd,decodedValue,eflags,encodedValue,htmlLower,inTag,m,openTag,tagName;htmlText=String(htmlText),htmlLower=null,inTag=false,attribs=[],tagName=void
+0,eflags=void 0,openTag=void 0,handler.startDoc&&handler.startDoc(param);while(htmlText){m=htmlText.match(inTag?INSIDE_TAG_TOKEN:OUTSIDE_TAG_TOKEN),htmlText=htmlText.substring(m[0].length);if(inTag){if(m[1]){attribName=lcase(m[1]);if(m[2]){encodedValue=m[3];switch(encodedValue.charCodeAt(0)){case
+34:case 39:encodedValue=encodedValue.substring(1,encodedValue.length-1)}decodedValue=unescapeEntities(stripNULs(encodedValue))}else
+decodedValue=attribName;attribs.push(attribName,decodedValue)}else if(m[4])eflags!==void
+0&&(openTag?handler.startTag&&handler.startTag(tagName,attribs,param):handler.endTag&&handler.endTag(tagName,param)),openTag&&eflags&(html4
+.eflags.CDATA|html4 .eflags.RCDATA)&&(htmlLower===null?(htmlLower=lcase(htmlText)):(htmlLower=htmlLower.substring(htmlLower.length-htmlText.length)),dataEnd=htmlLower.indexOf('</'+tagName),dataEnd<0&&(dataEnd=htmlText.length),eflags&html4
+.eflags.CDATA?handler.cdata&&handler.cdata(htmlText.substring(0,dataEnd),param):handler.rcdata&&handler.rcdata(normalizeRCData(htmlText.substring(0,dataEnd)),param),htmlText=htmlText.substring(dataEnd)),tagName=eflags=openTag=void
+0,attribs.length=0,inTag=false}else if(m[1])handler.pcdata&&handler.pcdata(m[0],param);else
+if(m[3])openTag=!m[2],inTag=true,tagName=lcase(m[3]),eflags=html4 .ELEMENTS.hasOwnProperty(tagName)?html4
+.ELEMENTS[tagName]:void 0;else if(m[4])handler.pcdata&&handler.pcdata(m[4],param);else
+if(m[5]){if(handler.pcdata)switch(m[5]){case'<':handler.pcdata('&lt;',param);break;case'>':handler.pcdata('&gt;',param);break;default:handler.pcdata('&amp;',param)}}}handler.endDoc&&handler.endDoc(param)}}return{'normalizeRCData':normalizeRCData,'escapeAttrib':escapeAttrib,'unescapeEntities':unescapeEntities,'makeSaxParser':makeSaxParser}})(),html.makeHtmlSanitizer=function(sanitizeAttributes){var
+ignoring,stack;return html.makeSaxParser({'startDoc':function(_){stack=[],ignoring=false},'startTag':function(tagName,attribs,out){var
+attribName,eflags,i,n,value;if(ignoring)return;if(!html4 .ELEMENTS.hasOwnProperty(tagName))return;eflags=html4
+.ELEMENTS[tagName];if(eflags&html4 .eflags.FOLDABLE)return;else if(eflags&html4 .eflags.UNSAFE)return ignoring=!(eflags&html4
+.eflags.EMPTY),void 0;attribs=sanitizeAttributes(tagName,attribs);if(attribs){eflags&html4
+.eflags.EMPTY||stack.push(tagName),out.push('<',tagName);for(i=0,n=attribs.length;i<n;i+=2)attribName=attribs[i],value=attribs[i+1],value!==null&&value!==void
+0&&out.push(' ',attribName,'=\"',html.escapeAttrib(value),'\"');out.push('>')}},'endTag':function(tagName,out){var
+eflags,i,index,stackEl;if(ignoring)return ignoring=false,void 0;if(!html4 .ELEMENTS.hasOwnProperty(tagName))return;eflags=html4
+.ELEMENTS[tagName];if(!(eflags&(html4 .eflags.UNSAFE|html4 .eflags.EMPTY|html4 .eflags.FOLDABLE))){if(eflags&html4
+.eflags.OPTIONAL_ENDTAG)for(index=stack.length;--index>=0;){stackEl=stack[index];if(stackEl===tagName)break;if(!(html4
+.ELEMENTS[stackEl]&html4 .eflags.OPTIONAL_ENDTAG))return}else for(index=stack.length;--index>=0;)if(stack[index]===tagName)break;if(index<0)return;for(i=stack.length;--i>index;)stackEl=stack[i],html4
+.ELEMENTS[stackEl]&html4 .eflags.OPTIONAL_ENDTAG||out.push('</',stackEl,'>');stack.length=index,out.push('</',tagName,'>')}},'pcdata':function(text,out){ignoring||out.push(text)},'rcdata':function(text,out){ignoring||out.push(text)},'cdata':function(text,out){ignoring||out.push(text)},'endDoc':function(out){var
+i;for(i=stack.length;--i>=0;)out.push('</',stack[i],'>');stack.length=0}})};function
+html_sanitize(htmlText,opt_uriPolicy,opt_nmTokenPolicy){var out=[];return html.makeHtmlSanitizer(function
+sanitizeAttribs(tagName,attribs){var attribKey,attribName,atype,i,value;for(i=0;i<attribs.length;i+=2){attribName=attribs[i],value=attribs[i+1],atype=null,((attribKey=tagName+'::'+attribName,html4
+.ATTRIBS.hasOwnProperty(attribKey))||(attribKey='*::'+attribName,html4 .ATTRIBS.hasOwnProperty(attribKey)))&&(atype=html4
+.ATTRIBS[attribKey]);if(atype!==null)switch(atype){case html4 .atype.NONE:break;case
+html4 .atype.SCRIPT:case html4 .atype.STYLE:value=null;break;case html4 .atype.ID:case
+html4 .atype.IDREF:case html4 .atype.IDREFS:case html4 .atype.GLOBAL_NAME:case html4
+.atype.LOCAL_NAME:case html4 .atype.CLASSES:value=opt_nmTokenPolicy?opt_nmTokenPolicy(value):value;break;case
+html4 .atype.URI:value=opt_uriPolicy&&opt_uriPolicy(value);break;case html4 .atype.URI_FRAGMENT:value&&'#'===value.charAt(0)?(value=opt_nmTokenPolicy?opt_nmTokenPolicy(value):value,value&&(value='#'+value)):(value=null);break;default:value=null}else
+value=null;attribs[i+1]=value}return attribs})(htmlText,out),out.join('')}function
+HtmlEmitter(base,opt_tameDocument){var arraySplice,bridal,detached,idMap,insertionPoint;if(!base)throw new
+Error('Host page error: Virtual document element was not provided');insertionPoint=base,bridal=bridalMaker(base.ownerDocument),detached=null,idMap=null,arraySplice=Array.prototype.splice;function
+buildIdMap(){var desc,descs,i,id;idMap={},descs=base.getElementsByTagName('*');for(i=0;desc=descs[i];++i)id=desc.getAttributeNode('id'),id&&id.value&&(idMap[id.value]=desc)}function
+byId(id){var node;idMap||buildIdMap(),node=idMap[id];if(node)return node;for(;node=base.ownerDocument.getElementById(id);){if(base.contains?base.contains(node):base.compareDocumentPosition(node)&16)return idMap[id]=node,node;node.id=''}return null}function
+emitStatic(htmlString){if(!base)throw new Error('Host page error: HtmlEmitter.emitStatic called after document finish()ed');if(base.firstChild)throw new
+Error('Host page error: Virtual document element is not empty');base.innerHTML=htmlString}function
+detachOnto(limit,out){var anc,child,greatAnc,next,sibling;for(child=limit.firstChild;child;child=next)next=child.nextSibling,out.push(child,limit),limit.removeChild(child);for(anc=limit;anc&&anc!==base;anc=greatAnc){greatAnc=anc.parentNode;for(sibling=anc.nextSibling;sibling;sibling=next)next=sibling.nextSibling,out.push(sibling,greatAnc),greatAnc.removeChild(sibling)}}function
+attach(id){var limit=byId(id),isLimitClosed,limitAnc,nConsumed,newDetached,parent,reattAnc,reattach;if(detached){newDetached=[0,0],detachOnto(limit,newDetached),limitAnc=limit;for(;parent=limitAnc.parentNode;)limitAnc=parent;nConsumed=0;while(nConsumed<detached.length){reattach=detached[nConsumed],reattAnc=reattach;for(;reattAnc.parentNode;reattAnc=reattAnc.parentNode);detached[nConsumed+1].appendChild(reattach),nConsumed+=2;if(reattAnc===limitAnc)break}newDetached[1]=nConsumed,arraySplice.apply(detached,newDetached)}else
+detached=[],detachOnto(limit,detached);return isLimitClosed=detached[1]!==limit,insertionPoint=isLimitClosed?limit.parentNode:limit,limit}function
+discard(placeholder){placeholder.parentNode.removeChild(placeholder)}function finish(){var
+i,n;insertionPoint=null;if(detached)for(i=0,n=detached.length;i<n;i+=2)detached[i+1].appendChild(detached[i]);return idMap=detached=base=null,this}function
+addBodyClasses(classes){base.className+=' '+classes}function signalLoaded(){var
+doc=opt_tameDocument;return doc&&doc.signalLoaded___(),this}this.byId=byId,this.attach=attach,this.discard=discard,this.emitStatic=emitStatic,this.finish=finish,this.signalLoaded=signalLoaded,this.setAttr=bridal.setAttribute,this.addBodyClasses=addBodyClasses,(function(tameDoc){var
+documentWriter,tameDocWrite,ucase;if(!tameDoc||tameDoc.write)return;function concat(items){return Array.prototype.join.call(items,'')}'script'.toUpperCase()==='SCRIPT'?(ucase=function(s){return s.toUpperCase()}):(ucase=function(s){return s.replace(/[a-z]/g,function(ch){return String.fromCharCode(ch.charCodeAt(0)&-33)})}),documentWriter={'startTag':function(tagName,attribs){var
+eltype=html4 .ELEMENTS[tagName],el;if(!html4 .ELEMENTS.hasOwnProperty(tagName)||(eltype&html4
+.eflags.UNSAFE)!==0)return;tameDoc.sanitizeAttrs___(tagName,attribs),el=bridal.createElement(tagName,attribs),eltype&html4
+.eflags.OPTIONAL_ENDTAG&&el.tagName===insertionPoint.tagName&&documentWriter.endTag(el.tagName,true),insertionPoint.appendChild(el),eltype&html4
+.eflags.EMPTY||(insertionPoint=el)},'endTag':function(tagName,optional){var anc=insertionPoint,p;tagName=ucase(tagName);while(anc!==base&&!/\bvdoc-body___\b/.test(anc.className)){p=anc.parentNode;if(anc.tagName===tagName)return insertionPoint=p,void
+0;anc=p}},'pcdata':function(text){insertionPoint.appendChild(insertionPoint.ownerDocument.createTextNode(html.unescapeEntities(text)))},'cdata':function(text){insertionPoint.appendChild(insertionPoint.ownerDocument.createTextNode(text))}},documentWriter.rcdata=documentWriter.pcdata,tameDocWrite=function
+write(html_varargs){var htmlText=concat(arguments),lexer;insertionPoint||(insertionPoint=base),lexer=html.makeSaxParser(documentWriter),lexer(htmlText)},tameDoc.write=___.markFuncFreeze(tameDocWrite,'write'),tameDoc.writeln=___.markFuncFreeze(function
+writeln(html){tameDocWrite(concat(arguments),'\n')},'writeln'),___.grantFunc(tameDoc,'write'),___.grantFunc(tameDoc,'writeln')})(opt_tameDocument)}bridalMaker=function(document){var
+isOpera=navigator.userAgent.indexOf('Opera')===0,isIE=!isOpera&&navigator.userAgent.indexOf('MSIE')!==-1,isWebkit=!isOpera&&navigator.userAgent.indexOf('WebKit')!==-1,featureAttachEvent=!!(window.attachEvent&&!window.addEventListener),featureExtendedCreateElement=(function(){try{return document.createElement('<input type=\"radio\">').type==='radio'}catch(e){return false}})(),CUSTOM_EVENT_TYPE_SUFFIX='_custom___',endsWith__,escapeAttrib;function
+tameEventType(type,opt_isCustom,opt_tagName){var tagAttr;type=String(type);if(endsWith__.test(type))throw new
+Error('Invalid event type '+type);return tagAttr=false,opt_tagName&&(tagAttr=String(opt_tagName).toLowerCase()+'::on'+type),!opt_isCustom&&(tagAttr&&html4
+.atype.SCRIPT===html4 .ATTRIBS[tagAttr]||html4 .atype.SCRIPT===html4 .ATTRIBS['*::on'+type])?type:type+CUSTOM_EVENT_TYPE_SUFFIX}function
+eventHandlerTypeFilter(handler,tameType){return function(event){if(tameType===event.eventType___)return handler.call(this,event)}}endsWith__=/__$/,escapeAttrib=html.escapeAttrib;function
+constructClone(node,deep){var attr,attrs,child,clone,cloneChild,i,tagDesc;if(node.nodeType===1&&featureExtendedCreateElement){tagDesc=node.tagName;switch(node.tagName){case'INPUT':tagDesc='<input name=\"'+escapeAttrib(node.name)+'\" type=\"'+escapeAttrib(node.type)+'\" value=\"'+escapeAttrib(node.defaultValue)+'\"'+(node.defaultChecked?' checked=\"checked\">':'>');break;case'BUTTON':tagDesc='<button name=\"'+escapeAttrib(node.name)+'\" type=\"'+escapeAttrib(node.type)+'\" value=\"'+escapeAttrib(node.value)+'\">';break;case'OPTION':tagDesc='<option '+(node.defaultSelected?' selected=\"selected\">':'>');break;case'TEXTAREA':tagDesc='<textarea value=\"'+escapeAttrib(node.defaultValue)+'\">'}clone=document.createElement(tagDesc),attrs=node.attributes;for(i=0;attr=attrs[i];++i)attr.specified&&!endsWith__.test(attr.name)&&setAttribute(clone,attr.nodeName,attr.nodeValue)}else
+clone=node.cloneNode(false);if(deep)for(child=node.firstChild;child;child=child.nextSibling)cloneChild=constructClone(child,deep),clone.appendChild(cloneChild);return clone}function
+fixupClone(node,clone){var attribs,child,cloneChild,originalAttribs;for(child=node.firstChild,cloneChild=clone.firstChild;cloneChild;child=child.nextSibling,cloneChild=cloneChild.nextSibling)fixupClone(child,cloneChild);if(node.nodeType===1)switch(node.tagName){case'INPUT':clone.value=node.value,clone.checked=node.checked;break;case'OPTION':clone.selected=node.selected,clone.value=node.value;break;case'TEXTAREA':clone.value=node.value}originalAttribs=node.attributes___,originalAttribs&&(attribs={},clone.attributes___=attribs,cajita.forOwnKeys(originalAttribs,___.markFuncFreeze(function(k,v){switch(typeof
+v){case'string':case'number':case'boolean':attribs[k]=v}})))}function getWindow(element){var
+doc=element.ownerDocument,s;return doc.parentWindow?doc.parentWindow:doc.defaultView?doc.defaultView:(s=doc.createElement('script'),s.innerHTML='document.parentWindow = window;',doc.body.appendChild(s),doc.body.removeChild(s),doc.parentWindow)}function
+untameEventType(type){var suffix=CUSTOM_EVENT_TYPE_SUFFIX,tlen=type.length,slen=suffix.length,end;return end=tlen-slen,end>=0&&suffix===type.substring(end)&&(type=type.substring(0,end)),type}function
+initEvent(event,type,bubbles,cancelable){type=tameEventType(type,true),bubbles=Boolean(bubbles),cancelable=Boolean(cancelable);if(event.initEvent)event.initEvent(type,bubbles,cancelable);else
+if(bubbles&&cancelable)event.eventType___=type;else throw new Error('Browser does not support non-bubbling/uncanceleable events')}function
+dispatchEvent(element,event){return element.dispatchEvent?Boolean(element.dispatchEvent(event)):(element.fireEvent('ondataavailable',event),Boolean(event.returnValue))}function
+addEventListener(element,type,handler,useCapture){var tameType,wrapper;return type=String(type),tameType=tameEventType(type,false,element.tagName),featureAttachEvent?type!==tameType?(wrapper=eventHandlerTypeFilter(handler,tameType),element.attachEvent('ondataavailable',wrapper),wrapper):(element.attachEvent('on'+type,handler),handler):(element.addEventListener(tameType,handler,useCapture),handler)}function
+removeEventListener(element,type,handler,useCapture){var tameType;type=String(type),tameType=tameEventType(type,false,element.tagName),featureAttachEvent?tameType!==type?element.detachEvent('ondataavailable',handler):element.detachEvent('on'+type,handler):element.removeEventListener(tameType,handler,useCapture)}function
+cloneNode(node,deep){var clone;return document.all?(clone=constructClone(node,deep)):(clone=node.cloneNode(deep)),fixupClone(node,clone),clone}function
+createElement(tagName,attribs){var el,i,n,tag;if(featureExtendedCreateElement){tag=['<',tagName];for(i=0,n=attribs.length;i<n;i+=2)tag.push(' ',attribs[i],'=\"',escapeAttrib(attribs[i+1]),'\"');return tag.push('>'),document.createElement(tag.join(''))}el=document.createElement(tagName);for(i=0,n=attribs.length;i<n;i+=2)setAttribute(el,attribs[i],attribs[i+1]);return el}function
+createStylesheet(document,cssText){var styleSheet=document.createElement('style');return styleSheet.setAttribute('type','text/css'),styleSheet.styleSheet?(styleSheet.styleSheet.cssText=cssText):styleSheet.appendChild(document.createTextNode(cssText)),styleSheet}function
+setAttribute(element,name,value){var attr;switch(name){case'style':return element.style.cssText=value,value;case'href':/^javascript:/i.test(value)?(element.stored_target___=element.target,element.target=''):element.stored_target___&&(element.target=element.stored_target___,delete
+element.stored_target___);break;case'target':if(element.href&&/^javascript:/i.test(element.href))return element.stored_target___=value,value}try{attr=element.ownerDocument.createAttribute(name),attr.value=value,element.setAttributeNode(attr)}catch(e){return element.setAttribute(name,value,0)}return value}function
+getBoundingClientRect(el){var doc=el.ownerDocument,cRect,elBoxObject,fixupLeft,fixupTop,left,op,opPosition,pageX,pageY,scrollEl,top,viewPortBoxObject,viewport;if(el.getBoundingClientRect)return cRect=el.getBoundingClientRect(),isIE&&(fixupLeft=doc.documentElement.clientLeft+doc.body.clientLeft,cRect.left-=fixupLeft,cRect.right-=fixupLeft,fixupTop=doc.documentElement.clientTop+doc.body.clientTop,cRect.top-=fixupTop,cRect.bottom-=fixupTop),{'top':+cRect.top,'left':+cRect.left,'right':+cRect.right,'bottom':+cRect.bottom};viewport=isIE&&doc.compatMode==='CSS1Compat'?doc.body:doc.documentElement,pageX=0,pageY=0;if(el===viewport);else
+if(doc.getBoxObjectFor)elBoxObject=doc.getBoxObjectFor(el),viewPortBoxObject=doc.getBoxObjectFor(viewport),pageX=elBoxObject.screenX-viewPortBoxObject.screenX,pageY=elBoxObject.screenY-viewPortBoxObject.screenY;else{for(op=el;op&&op!==el;op=op.offsetParent){pageX+=op.offsetLeft,pageY+=op.offsetTop,op!==el&&(pageX+=op.clientLeft||0,pageY+=op.clientTop||0);if(isWebkit){opPosition=doc.defaultView.getComputedStyle(op,'position'),opPosition==='fixed'&&(pageX+=doc.body.scrollLeft,pageY+=doc.body.scrollTop);break}}(isWebkit&&doc.defaultView.getComputedStyle(el,'position')==='absolute'||isOpera)&&(pageY-=doc.body.offsetTop);for(op=el;(op=op.offsetParent)&&op!==doc.body;)pageX-=op.scrollLeft,(!isOpera||op.tagName!=='TR')&&(pageY-=op.scrollTop)}return scrollEl=!isWebkit&&doc.compatMode==='CSS1Compat'?doc.documentElement:doc.body,left=pageX-scrollEl.scrollLeft,top=pageY-scrollEl.scrollTop,{'top':top,'left':left,'right':left+el.clientWidth,'bottom':top+el.clientHeight}}function
+getAttribute(element,name){var attr;if(name==='style'){if(typeof element.style.cssText==='string')return element.style.cssText};return attr=element.getAttributeNode(name),attr&&attr.specified?attr.value:null}function
+hasAttribute(element,name){var attr;return element.hasAttribute?element.hasAttribute(name):(attr=element.getAttributeNode(name),attr!==null&&attr.specified)}function
+getComputedStyle(element,pseudoElement){if(element.currentStyle&&pseudoElement===void
+0)return element.currentStyle;else if(window.getComputedStyle)return window.getComputedStyle(element,pseudoElement);throw new
+Error('Computed style not available for pseudo element '+pseudoElement)}function
+makeXhr(){var activeXClassIds,candidate,i,n;if(typeof XMLHttpRequest==='undefined'){activeXClassIds=['MSXML2.XMLHTTP.5.0','MSXML2.XMLHTTP.4.0','MSXML2.XMLHTTP.3.0','MSXML2.XMLHTTP','MICROSOFT.XMLHTTP.1.0','MICROSOFT.XMLHTTP.1','MICROSOFT.XMLHTTP'];for(i=0,n=activeXClassIds.length;i<n;++i){candidate=activeXClassIds[i];try{return new
+ActiveXObject(candidate)}catch(e){}}}return new XMLHttpRequest}return{'addEventListener':addEventListener,'removeEventListener':removeEventListener,'initEvent':initEvent,'dispatchEvent':dispatchEvent,'cloneNode':cloneNode,'createElement':createElement,'createStylesheet':createStylesheet,'setAttribute':setAttribute,'getAttribute':getAttribute,'hasAttribute':hasAttribute,'getBoundingClientRect':getBoundingClientRect,'getWindow':getWindow,'untameEventType':untameEventType,'extendedCreateElementFeature':featureExtendedCreateElement,'getComputedStyle':getComputedStyle,'makeXhr':makeXhr}},bridal=bridalMaker(document),cssparser=(function(){var
+ucaseLetter=/[A-Z]/g,BS,COMMENT,DQ,ESCAPE,HASH,HEX,IDENT,IDENT_RE,KEYWORD,LCASE,NAME,NEWLINE,NMCHAR,NMSTART,NON_ASCII,NON_HEX_ESC_RE,NUM,PROP_DECLS_TOKENS,PUNC,QUANTITY,SPACE,SPACE_RE,STRING,STRING1,STRING2,UNICODE,UNIT,URL,URL_CHARS,URL_RE,URL_SPECIAL_CHARS,WHITESPACE,unicodeEscape;function
+lcaseOne(ch){return String.fromCharCode(ch.charCodeAt(0)|32)}LCASE='i'==='I'.toLowerCase()?function(s){return s.toLowerCase()}:function(s){return s.replace(ucaseLetter,lcaseOne)},KEYWORD='(?:\\@(?:import|page|media|charset))',NEWLINE='\\n|\\r\\n|\\r|\\f',HEX='[0-9a-f]',NON_ASCII='[^\\0-\\177]',UNICODE='(?:(?:\\\\'+HEX+'{1,6})(?:\\r\\n|[ 	\\r\\n\\f])?)',ESCAPE='(?:'+UNICODE+'|\\\\[^\\r\\n\\f0-9a-f])',NMSTART='(?:[_a-z]|'+NON_ASCII+'|'+ESCAPE+')',NMCHAR='(?:[_a-z0-9-]|'+NON_ASCII+'|'+ESCAPE+')',IDENT='-?'+NMSTART+NMCHAR+'*',NAME=NMCHAR+'+',HASH='#'+NAME,STRING1='\"(?:[^\\\"\\\\]|\\\\[^])*\"',STRING2='\'(?:[^\\\'\\\\]|\\\\[^])*\'',STRING='(?:'+STRING1+'|'+STRING2+')',NUM='(?:[0-9]*\\.[0-9]+|[0-9]+)',SPACE='[ \\t\\r\\n\\f]',WHITESPACE=SPACE+'*',URL_SPECIAL_CHARS='[!#$%&*-~]',URL_CHARS='(?:'+URL_SPECIAL_CHARS+'|'+NON_ASCII+'|'+ESCAPE+')*',URL='url\\('+WHITESPACE+'(?:'+STRING+'|'+URL_CHARS+')'+WHITESPACE+'\\)',COMMENT='/\\*[^*]*\\*+(?:[^/*][^*]*\\*+)*/',UNIT='(?:em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)',QUANTITY=NUM+'(?:'+WHITESPACE+UNIT+'|'+IDENT+')?',PUNC='<!--|-->|~=|[|=\\{\\+>,:;()]',PROP_DECLS_TOKENS=new
+RegExp('(?:'+[STRING,COMMENT,QUANTITY,URL,NAME,HASH,IDENT,SPACE+'+',PUNC].join('|')+')','gi'),IDENT_RE=new
+RegExp('^(?:'+IDENT+')$','i'),URL_RE=new RegExp('^(?:'+URL+')$','i'),NON_HEX_ESC_RE=/\\(?:\r\n?|[^0-9A-Fa-f\r]|$)/g,SPACE_RE=new
+RegExp(SPACE+'+','g'),BS=/\\/g,DQ=/"/g;function normEscs(x){var out='',i,n;for(i=1,n=x.length;i<n;++i)out+='\\'+x.charCodeAt(i).toString(16)+' ';return out}function
+toCssStr(s){return'\"'+s.replace(BS,'\\5c ').replace(DQ,'\\22 ')+'\"'}function parse(cssText,handler){var
+toks=(''+cssText).match(PROP_DECLS_TOKENS),buf,i,k,n,propName,tok,url;if(!toks)return;propName=null,buf=[],k=0;for(i=0,n=toks.length;i<n;++i){tok=toks[i];switch(tok.charCodeAt(0)){case
+9:case 10:case 12:case 13:case 32:continue;case 39:tok='\"'+tok.substring(1,tok.length-1).replace(DQ,'\\22 ')+'\"';case
+34:tok=tok.replace(NON_HEX_ESC_RE,normEscs);break;case 47:if('*'===tok.charAt(1))continue;break;case
+46:case 48:case 49:case 50:case 51:case 52:case 53:case 54:case 55:case 56:case 57:tok=tok.replace(SPACE_RE,'');break;case
+58:k===1&&IDENT_RE.test(buf[0])?(propName=LCASE(buf[0])):(propName=null),k=buf.length=0;continue;case
+59:propName&&(buf.length&&handler(propName,buf.slice(0)),propName=null),k=buf.length=0;continue;case
+85:case 117:url=toUrl(tok),url!==null&&(tok='url('+toCssStr(url)+')')}buf[k++]=tok}propName&&buf.length&&handler(propName,buf.slice(0))}unicodeEscape=/\\(?:([0-9a-fA-F]{1,6})(?:\r\n?|[ \t\f\n])?|[^\r\n\f0-9a-f])/g;function
+decodeOne(_,hex){return hex?String.fromCharCode(parseInt(hex,16)):_.charAt(1)}function
+toUrl(cssToken){if(!URL_RE.test(cssToken))return null;cssToken=cssToken.replace(/^url[\s\(]+|[\s\)]+$/gi,'');switch(cssToken.charAt(0)){case'\"':case'\'':cssToken=cssToken.substring(1,cssToken.length-1)}return cssToken.replace(unicodeEscape,decodeOne)}return{'parse':parse,'toUrl':toUrl,'toCssStr':toCssStr}})(),domitaModules||(domitaModules={}),domitaModules.classUtils=function(){function
+getterSetterSuffix(name){return String.fromCharCode(name.charCodeAt(0)&-33)+name.substring(1)+'___'}function
+exportFields(object,fields){var count,field,getterName,i,setterName,suffix;for(i=fields.length;--i>=0;){field=fields[i],suffix=getterSetterSuffix(field),getterName='get'+suffix,setterName='set'+suffix,count=0,object[getterName]&&(++count,___.useGetHandler(object,field,object[getterName])),object[setterName]&&(++count,___.useSetHandler(object,field,object[setterName]));if(!count)throw new
+Error('Failed to export field '+field+' on '+object)}}function applyAccessors(object,handlers){function
+propertyOnlyHasGetter(_){throw new TypeError('setting a property that only has a getter')}___.forOwnKeys(handlers,___.markFuncFreeze(function(propertyName,def){var
+setter=def.set||propertyOnlyHasGetter;___.useGetHandler(object,propertyName,def.get),___.useSetHandler(object,propertyName,setter)}))}function
+ensureValidCallback(aCallback){if('function'!==typeof aCallback&&!('object'===typeof
+aCallback&&aCallback!==null&&___.canCallPub(aCallback,'call')))throw new Error('Expected function not '+typeof
+aCallback)}return{'exportFields':exportFields,'ensureValidCallback':ensureValidCallback,'applyAccessors':applyAccessors,'getterSetterSuffix':getterSetterSuffix}},domitaModules.XMLHttpRequestCtor=function(XMLHttpRequest,ActiveXObject){var
+activeXClassId;if(XMLHttpRequest)return XMLHttpRequest;else if(ActiveXObject)return function
+ActiveXObjectForIE(){var activeXClassIds,candidate,i,n;if(activeXClassId===void 0){activeXClassId=null,activeXClassIds=['MSXML2.XMLHTTP.5.0','MSXML2.XMLHTTP.4.0','MSXML2.XMLHTTP.3.0','MSXML2.XMLHTTP','MICROSOFT.XMLHTTP.1.0','MICROSOFT.XMLHTTP.1','MICROSOFT.XMLHTTP'];for(i=0,n=activeXClassIds.length;i<n;++i){candidate=activeXClassIds[i];try{new
+ActiveXObject(candidate),activeXClassId=candidate;break}catch(e){}}activeXClassIds=null}return new
+ActiveXObject(activeXClassId)};throw new Error('ActiveXObject not available')},domitaModules.TameXMLHttpRequest=function(xmlHttpRequestMaker,uriCallback){var
+classUtils=domitaModules.classUtils(),FROZEN,INVALID_SUFFIX,endsWith__;function TameXMLHttpRequest(){this.xhr___=new
+xmlHttpRequestMaker,classUtils.exportFields(this,['onreadystatechange','readyState','responseText','responseXML','status','statusText'])}return FROZEN='Object is frozen.',INVALID_SUFFIX='Property names may not end in \'__\'.',endsWith__=/__$/,TameXMLHttpRequest.prototype.handleRead___=function(name){var
+handlerName;return name=''+name,endsWith__.test(name)?void 0:(handlerName=name+'_getter___',this[handlerName]?this[handlerName]():___.hasOwnProp(this.xhr___.properties___,name)?this.xhr___.properties___[name]:void
+0)},TameXMLHttpRequest.prototype.handleCall___=function(name,args){var handlerName;name=''+name;if(endsWith__.test(name))throw new
+Error(INVALID_SUFFIX);handlerName=name+'_handler___';if(this[handlerName])return this[handlerName].call(this,args);if(___.hasOwnProp(this.xhr___.properties___,name))return this.xhr___.properties___[name].call(this,args);throw new
+TypeError(name+' is not a function.')},TameXMLHttpRequest.prototype.handleSet___=function(name,val){var
+handlerName;name=''+name;if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);if(___.isFrozen(this))throw new
+Error(FROZEN);return handlerName=name+'_setter___',this[handlerName]?this[handlerName](val):(this.xhr___.properties___||(this.xhr___.properties___={}),this[name+'_canEnum___']=true,this.xhr___.properties___[name]=val)},TameXMLHttpRequest.prototype.handleDelete___=function(name){var
+handlerName;name=''+name;if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);if(___.isFrozen(this))throw new
+Error(FROZEN);return handlerName=name+'_deleter___',this[handlerName]?this[handlerName]():this.xhr___.properties___?delete
+this.xhr___.properties___[name]&&delete this[name+'_canEnum___']:true},TameXMLHttpRequest.prototype.setOnreadystatechange___=function(handler){var
+self=this;this.xhr___.onreadystatechange=function(event){var evt={'target':self};return ___.callPub(handler,'call',[void
+0,evt])},this.handler___=handler},TameXMLHttpRequest.prototype.getReadyState___=function(){return Number(this.xhr___.readyState)},TameXMLHttpRequest.prototype.open=function(method,URL,opt_async,opt_userName,opt_password){var
+safeUri;method=String(method),safeUri=uriCallback.rewrite(String(URL),'*/*');if(safeUri===void
+0)throw'URI violates security policy';switch(arguments.length){case 2:this.async___=true,this.xhr___.open(method,safeUri);break;case
+3:this.async___=opt_async,this.xhr___.open(method,safeUri,Boolean(opt_async));break;case
+4:this.async___=opt_async,this.xhr___.open(method,safeUri,Boolean(opt_async),String(opt_userName));break;case
+5:this.async___=opt_async,this.xhr___.open(method,safeUri,Boolean(opt_async),String(opt_userName),String(opt_password));break;default:throw'XMLHttpRequest cannot accept '+arguments.length+' arguments'}},TameXMLHttpRequest.prototype.setRequestHeader=function(label,value){this.xhr___.setRequestHeader(String(label),String(value))},TameXMLHttpRequest.prototype.send=function(opt_data){var
+evt;arguments.length===0?this.xhr___.send(''):typeof opt_data==='string'?this.xhr___.send(opt_data):this.xhr___.send(''),this.xhr___.overrideMimeType&&(!this.async___&&this.handler___&&(evt={'target':this},___.callPub(this.handler___,'call',[void
+0,evt])))},TameXMLHttpRequest.prototype.abort=function(){this.xhr___.abort()},TameXMLHttpRequest.prototype.getAllResponseHeaders=function(){var
+result=this.xhr___.getAllResponseHeaders();return result===undefined||result===null?result:String(result)},TameXMLHttpRequest.prototype.getResponseHeader=function(headerName){var
+result=this.xhr___.getResponseHeader(String(headerName));return result===undefined||result===null?result:String(result)},TameXMLHttpRequest.prototype.getResponseText___=function(){var
+result=this.xhr___.responseText;return result===undefined||result===null?result:String(result)},TameXMLHttpRequest.prototype.getResponseXML___=function(){return{}},TameXMLHttpRequest.prototype.getStatus___=function(){var
+result=this.xhr___.status;return result===undefined||result===null?result:Number(result)},TameXMLHttpRequest.prototype.getStatusText___=function(){var
+result=this.xhr___.statusText;return result===undefined||result===null?result:String(result)},TameXMLHttpRequest.prototype.toString=function(){return'Not a real XMLHttpRequest'},___.markCtor(TameXMLHttpRequest,Object,'TameXMLHttpRequest'),___.all2(___.grantTypedMethod,TameXMLHttpRequest.prototype,['open','setRequestHeader','send','abort','getAllResponseHeaders','getResponseHeader']),TameXMLHttpRequest},domitaModules.CssPropertiesCollection=function(cssPropertyNameCollection,anElement,css){var
+canonicalStylePropertyNames={},cssPropertyNames={};return ___.forOwnKeys(cssPropertyNameCollection,___.markFuncFreeze(function(cssPropertyName){var
+baseStylePropertyName=cssPropertyName.replace(/-([a-z])/g,function(_,letter){return letter.toUpperCase()}),canonStylePropertyName=baseStylePropertyName,alts,i;cssPropertyNames[baseStylePropertyName]=cssPropertyNames[canonStylePropertyName]=cssPropertyName;if(css.alternates.hasOwnProperty(canonStylePropertyName)){alts=css.alternates[canonStylePropertyName];for(i=alts.length;--i>=0;)cssPropertyNames[alts[i]]=cssPropertyName,alts[i]in
+anElement.style&&!(canonStylePropertyName in anElement.style)&&(canonStylePropertyName=alts[i])}canonicalStylePropertyNames[cssPropertyName]=canonStylePropertyName})),{'isCanonicalProp':function(p){return cssPropertyNames.hasOwnProperty(p)},'isCssProp':function(p){return canonicalStylePropertyNames.hasOwnProperty(p)},'getCanonicalPropFromCss':function(p){return canonicalStylePropertyNames[p]},'getCssPropFromCanonical':function(p){return cssPropertyNames[p]}}},attachDocumentStub=(function(){var
+FORBIDDEN_ID_LIST_PATTERN,FORBIDDEN_ID_PATTERN,IntervalIdMark,IntervalIdT,JS_IDENT,JS_SPACE,SIMPLE_HANDLER_PATTERN,TameEventMark,TameEventT,TameNodeMark,TameNodeT,TimeoutIdMark,TimeoutIdT,VALID_ID_CHAR,VALID_ID_LIST_PATTERN,VALID_ID_PATTERN,XML_SPACE,classUtils,cssSealerUnsealerPair;function
+arrayRemove(array,from,to){var rest=array.slice((to||from)+1||array.length);return array.length=from<0?array.length+from:from,array.push.apply(array,rest)}TameNodeMark=___.Trademark('TameNode'),TameNodeT=TameNodeMark.guard,TameEventMark=___.Trademark('TameEvent'),TameEventT=TameEventMark.guard;function
+Html(htmlFragment){this.html___=String(htmlFragment||'')}Html.prototype.valueOf=Html.prototype.toString=function(){return this.html___};function
+safeHtml(htmlFragment){return htmlFragment instanceof Html?htmlFragment.html___:html.escapeAttrib(String(htmlFragment||''))}function
+blessHtml(htmlFragment){return htmlFragment instanceof Html?htmlFragment:new Html(htmlFragment)}XML_SPACE='	\n\r ',JS_SPACE='	\n\r ',JS_IDENT='(?:[a-zA-Z_][a-zA-Z0-9$_]*[a-zA-Z0-9$]|[a-zA-Z])_?',SIMPLE_HANDLER_PATTERN=new
+RegExp('^['+JS_SPACE+']*'+'(return['+JS_SPACE+']+)?'+'('+JS_IDENT+')['+JS_SPACE+']*'+'\\((?:this'+'(?:['+JS_SPACE+']*,['+JS_SPACE+']*event)?'+'['+JS_SPACE+']*)?\\)'+'['+JS_SPACE+']*(?:;?['+JS_SPACE+']*)$'),VALID_ID_CHAR=unicode.LETTER+unicode.DIGIT+'_'+'$\\-.:;=()\\[\\]'+unicode.COMBINING_CHAR+unicode.EXTENDER,VALID_ID_PATTERN=new
+RegExp('^['+VALID_ID_CHAR+']+$'),VALID_ID_LIST_PATTERN=new RegExp('^['+XML_SPACE+VALID_ID_CHAR+']*$'),FORBIDDEN_ID_PATTERN=new
+RegExp('__\\s*$'),FORBIDDEN_ID_LIST_PATTERN=new RegExp('__(?:\\s|$)');function isValidId(s){return!FORBIDDEN_ID_PATTERN.test(s)&&VALID_ID_PATTERN.test(s)}function
+isValidIdList(s){return!FORBIDDEN_ID_LIST_PATTERN.test(s)&&VALID_ID_LIST_PATTERN.test(s)}function
+trimCssSpaces(input){return input.replace(/^[ \t\r\n\f]+|[ \t\r\n\f]+$/g,'')}function
+decodeCssString(s){return s.replace(/\\(?:(\r\n?|\n|\f)|([0-9a-f]{1,6})(?:\r\n?|[ \t\n\f])?|(.))/gi,function(_,nl,hex,esc){return esc||(nl?'':String.fromCharCode(parseInt(hex,16)))})}function
+sanitizeStyleAttrValue(styleAttrValue){var sanitizedDeclarations=[];return cssparser.parse(String(styleAttrValue),function(property,value){property=property.toLowerCase(),css.properties.hasOwnProperty(property)&&css.properties[property].test(value+'')&&sanitizedDeclarations.push(property+': '+value)}),sanitizedDeclarations.join(' ; ')}function
+mimeTypeForAttr(tagName,attribName){if(attribName==='src'){if(tagName==='img')return'image/*';if(tagName==='script')return'text/javascript'}return'*/*'}function
+assert(cond){if(!cond)throw typeof console!=='undefined'&&(console.error('domita assertion failed'),console.trace()),new
+Error('Domita assertion failed')}classUtils=domitaModules.classUtils(),cssSealerUnsealerPair=___.makeSealerUnsealerPair(),TimeoutIdMark=___.Trademark('TimeoutId'),TimeoutIdT=TimeoutIdMark.guard;function
+tameSetTimeout(timeout,delayMillis){var timeoutId;if(timeout){if(typeof timeout==='string')throw new
+Error('setTimeout called with a string.  Please pass a function instead of a string of javascript');timeoutId=setTimeout(function(){___.callPub(timeout,'call',[___.USELESS])},delayMillis|0)}else
+timeoutId=NaN;return ___.stamp([TimeoutIdMark.stamp],{'timeoutId___':timeoutId})}___.markFuncFreeze(tameSetTimeout);function
+tameClearTimeout(timeoutId){var rawTimeoutId;if(timeoutId===null||timeoutId===void
+0)return;try{timeoutId=TimeoutIdT.coerce(timeoutId)}catch(e){return}rawTimeoutId=timeoutId.timeoutId___,rawTimeoutId===rawTimeoutId&&clearTimeout(rawTimeoutId)}___.markFuncFreeze(tameClearTimeout),IntervalIdMark=___.Trademark('IntervalId'),IntervalIdT=IntervalIdMark.guard;function
+tameSetInterval(interval,delayMillis){var intervalId;if(interval){if(typeof interval==='string')throw new
+Error('setInterval called with a string.  Please pass a function instead of a string of javascript');intervalId=setInterval(function(){___.callPub(interval,'call',[___.USELESS])},delayMillis|0)}else
+intervalId=NaN;return ___.stamp([IntervalIdMark.stamp],{'intervalId___':intervalId})}___.markFuncFreeze(tameSetInterval);function
+tameClearInterval(intervalId){var rawIntervalId;if(intervalId===null||intervalId===void
+0)return;try{intervalId=IntervalIdT.coerce(intervalId)}catch(e){return}rawIntervalId=intervalId.intervalId___,rawIntervalId===rawIntervalId&&clearInterval(rawIntervalId)}___.markFuncFreeze(tameClearInterval);function
+makeScrollable(element){var window=bridal.getWindow(element),overflow=null;element.currentStyle?(overflow=element.currentStyle.overflow):window.getComputedStyle?(overflow=window.getComputedStyle(element,void
+0).overflow):(overflow=null);switch(overflow&&overflow.toLowerCase()){case'visible':case'hidden':element.style.overflow='auto'}}function
+tameScrollTo(element,x,y){if(x!==+x||y!==+y||x<0||y<0)throw new Error('Cannot scroll to '+x+':'+typeof
+x+','+y+' : '+typeof y);element.scrollLeft=x,element.scrollTop=y}function tameScrollBy(element,dx,dy){if(dx!==+dx||dy!==+dy)throw new
+Error('Cannot scroll by '+dx+':'+typeof dx+', '+dy+':'+typeof dy);element.scrollLeft+=dx,element.scrollTop+=dy}function
+guessPixelsFromCss(cssStr){var m;return cssStr?(m=cssStr.match(/^([0-9]+)/),m?+m[1]:0):0}function
+tameResizeTo(element,w,h){if(w!==+w||h!==+h)throw new Error('Cannot resize to '+w+':'+typeof
+w+', '+h+':'+typeof h);element.style.width=w+'px',element.style.height=h+'px'}function
+tameResizeBy(element,dw,dh){var extraHeight,extraWidth,goalHeight,goalWidth,h,hError,style,w,wError;if(dw!==+dw||dh!==+dh)throw new
+Error('Cannot resize by '+dw+':'+typeof dw+', '+dh+':'+typeof dh);if(!dw&&!dh)return;style=element.currentStyle,style||(style=bridal.getWindow(element).getComputedStyle(element,void
+0)),extraHeight=guessPixelsFromCss(style.paddingBottom)+guessPixelsFromCss(style.paddingTop),extraWidth=guessPixelsFromCss(style.paddingLeft)+guessPixelsFromCss(style.paddingRight),goalHeight=element.clientHeight+dh,goalWidth=element.clientWidth+dw,h=goalHeight-extraHeight,w=goalWidth-extraWidth,dh&&(element.style.height=Math.max(0,h)+'px'),dw&&(element.style.width=Math.max(0,w)+'px'),dh&&element.clientHeight!==goalHeight&&(hError=element.clientHeight-goalHeight,element.style.height=Math.max(0,h-hError)+'px'),dw&&element.clientWidth!==goalWidth&&(wError=element.clientWidth-goalWidth,element.style.width=Math.max(0,w-wError)+'px')}function
+attachDocumentStub(idSuffix,uriCallback,imports,pseudoBodyNode,optPseudoWindowLocation){var
+pluginId=___.getId(imports),document=pseudoBodyNode.ownerDocument,bridal=bridalMaker(document),window=bridal.getWindow(pseudoBodyNode),ID_LIST_PARTS_PATTERN,INDEX_SIZE_ERROR,INVALID_SUFFIX,NOT_EDITABLE,PSEUDO_ELEMENT_WHITELIST,UNKNOWN_TAGNAME,UNSAFE_TAGNAME,allCssProperties,commonElementPropertyHandlers,defaultNodeClassCtor,defaultNodeClasses,editableTameNodeCache,elementPolicies,endsWith__,historyInsensitiveCssProperties,htmlSanitizer,i,idClass,idClassPattern,innerHtmlTamer,k,nodeClasses,outers,prop,readOnlyTameNodeCache,tameDefaultView,tameDocument,tameLocation,tameNavigator,tameNodeFields,tameNodePublicMembers,tameWindow,windowProps,wpLen;if(arguments.length<4)throw new
+Error('arity mismatch: '+arguments.length);optPseudoWindowLocation||(optPseudoWindowLocation={}),elementPolicies={},elementPolicies.form=function(attribs){var
+sawHandler=false,i,n;for(i=0,n=attribs.length;i<n;i+=2)attribs[i]==='onsubmit'&&(sawHandler=true);return sawHandler||attribs.push('onsubmit','return false'),attribs},elementPolicies.a=elementPolicies.area=function(attribs){return attribs.push('target','_blank'),attribs};function
+sanitizeHtml(htmlText){var out=[];return htmlSanitizer(htmlText,out),out.join('')}function
+sanitizeAttrs(tagName,attribs){var n=attribs.length,attribKey,attribName,atype,i,policy,value;for(i=0;i<n;i+=2)attribName=attribs[i],value=attribs[i+1],atype=null,(attribKey=tagName+'::'+attribName,html4
+.ATTRIBS.hasOwnProperty(attribKey))||(attribKey='*::'+attribName,html4 .ATTRIBS.hasOwnProperty(attribKey))?(atype=html4
+.ATTRIBS[attribKey],value=rewriteAttribute(tagName,attribName,atype,value)):(value=null),value!==null&&value!==void
+0?(attribs[i+1]=value):(attribs[i+1]=attribs[--n],attribs[i]=attribs[--n],i-=2);return attribs.length=n,policy=elementPolicies[tagName],policy&&elementPolicies.hasOwnProperty(tagName)?policy(attribs):attribs}htmlSanitizer=html.makeHtmlSanitizer(sanitizeAttrs);function
+unsuffix(str,suffix,fail){var n;return typeof str!=='string'?fail:(n=str.length-suffix.length,0<n&&str.substring(n)===suffix?str.substring(0,n):fail)}ID_LIST_PARTS_PATTERN=new
+RegExp('([^'+XML_SPACE+']+)(['+XML_SPACE+']+|$)','g');function virtualizeAttributeValue(attrType,realValue){realValue=String(realValue);switch(attrType){case
+html4 .atype.GLOBAL_NAME:case html4 .atype.ID:case html4 .atype.IDREF:return unsuffix(realValue,idSuffix,null);case
+html4 .atype.IDREFS:return realValue.replace(ID_LIST_PARTS_PATTERN,function(_,id,spaces){return unsuffix(id,idSuffix,'')+(spaces?' ':'')});case
+html4 .atype.URI_FRAGMENT:if(realValue&&'#'===realValue.charAt(0)){realValue=unsuffix(realValue.substring(1),idSuffix,null);return realValue?'#'+realValue:null}else{return null}default:return realValue}}function
+tameInnerHtml(htmlText){var out=[];return innerHtmlTamer(htmlText,out),out.join('')}innerHtmlTamer=html.makeSaxParser({'startTag':function(tagName,attribs,out){var
+aname,atype,i,value;out.push('<',tagName);for(i=0;i<attribs.length;i+=2)aname=attribs[i],atype=getAttributeType(tagName,aname),value=attribs[i+1],aname!=='target'&&atype!==void
+0&&(value=virtualizeAttributeValue(atype,value),typeof value==='string'&&out.push(' ',aname,'=\"',html.escapeAttrib(value),'\"'));out.push('>')},'endTag':function(name,out){out.push('</',name,'>')},'pcdata':function(text,out){out.push(text)},'rcdata':function(text,out){out.push(text)},'cdata':function(text,out){out.push(text)}});function
+rewriteAttribute(tagName,attribName,type,value){var css,cssPropertiesAndValues,doesReturn,fnName,i,match,propName,propValue,semi;switch(type){case
+html4 .atype.NONE:return String(value);case html4 .atype.CLASSES:return value=String(value),FORBIDDEN_ID_LIST_PATTERN.test(value)?null:value;case
+html4 .atype.GLOBAL_NAME:case html4 .atype.ID:case html4 .atype.IDREF:return value=String(value),value&&isValidId(value)?value+idSuffix:null;case
+html4 .atype.IDREFS:return value=String(value),value&&isValidIdList(value)?value.replace(ID_LIST_PARTS_PATTERN,function(_,id,spaces){return id+idSuffix+(spaces?' ':'')}):null;case
+html4 .atype.LOCAL_NAME:return value=String(value),value&&isValidId(value)?value:null;case
+html4 .atype.SCRIPT:return value=String(value),match=value.match(SIMPLE_HANDLER_PATTERN),match?(doesReturn=match[1],fnName=match[2],value=(doesReturn?'return ':'')+'plugin_dispatchEvent___('+'this, event, '+pluginId+', \"'+fnName+'\");',attribName==='onsubmit'&&(value='try { '+value+' } finally { return false; }'),value):null;case
+html4 .atype.URI:return value=String(value),uriCallback?uriCallback.rewrite(value,mimeTypeForAttr(tagName,attribName))||null:null;case
+html4 .atype.URI_FRAGMENT:return value=String(value),value.charAt(0)==='#'&&isValidId(value.substring(1))?'#'+value+idSuffix:null;case
+html4 .atype.STYLE:if('function'!==typeof value)return sanitizeStyleAttrValue(String(value));cssPropertiesAndValues=cssSealerUnsealerPair.unseal(value);if(!cssPropertiesAndValues)return null;css=[];for(i=0;i<cssPropertiesAndValues.length;i+=2)propName=cssPropertiesAndValues[i],propValue=cssPropertiesAndValues[i+1],semi=propName.indexOf(';'),semi>=0&&(propName=propName.substring(0,semi)),css.push(propName+' : '+propValue);return css.join(' ; ');case
+html4 .atype.FRAME_TARGET:default:return null}}function makeCache(){var cache=___.newTable(false);return cache.set(null,null),cache.set(void
+0,null),cache}editableTameNodeCache=makeCache(),readOnlyTameNodeCache=makeCache();function
+defaultTameNode(node,editable){var cache,tagName,tamed;if(node===null||node===void
+0)return null;cache=editable?editableTameNodeCache:readOnlyTameNodeCache,tamed=cache.get(node);if(tamed!==void
+0)return tamed;switch(node.nodeType){case 1:tagName=node.tagName.toLowerCase();switch(tagName){case'a':tamed=new
+TameAElement(node,editable);break;case'form':tamed=new TameFormElement(node,editable);break;case'select':case'button':case'option':case'textarea':case'input':tamed=new
+TameInputElement(node,editable);break;case'iframe':tamed=new TameIFrameElement(node,editable);break;case'img':tamed=new
+TameImageElement(node,editable);break;case'label':tamed=new TameLabelElement(node,editable);break;case'script':tamed=new
+TameScriptElement(node,editable);break;case'td':case'thead':case'tfoot':case'tbody':case'th':tamed=new
+TameTableCompElement(node,editable);break;case'tr':tamed=new TameTableRowElement(node,editable);break;case'table':tamed=new
+TameTableElement(node,editable);break;default:!html4 .ELEMENTS.hasOwnProperty(tagName)||html4
+.ELEMENTS[tagName]&html4 .eflags.UNSAFE?(tamed=new TameOpaqueNode(node,editable)):(tamed=new
+TameElement(node,editable,editable))}break;case 2:throw'Internal: Attr nodes cannot be generically wrapped';case
+3:case 4:tamed=new TameTextNode(node,editable);break;case 8:tamed=new TameCommentNode(node,editable);break;case
+11:tamed=new TameBackedNode(node,editable,editable);break;default:tamed=new TameOpaqueNode(node,editable)}return node.nodeType===1&&cache.set(node,tamed),tamed}function
+tameRelatedNode(node,editable,tameNodeCtor){var ancestor,docElem;if(node===null||node===void
+0)return null;if(node===tameDocument.body___){if(tameDocument.editable___&&!editable)throw new
+Error(NOT_EDITABLE);return tameDocument.getBody___()}try{docElem=node.ownerDocument.documentElement;for(ancestor=node;ancestor;ancestor=ancestor.parentNode)if(idClassPattern.test(ancestor.className))return tameNodeCtor(node,editable);else
+if(ancestor===docElem)return null;return tameNodeCtor(node,editable)}catch(e){}return null}function
+getNodeListLength(nodeList){var limit=nodeList.length;return limit!==+limit&&(limit=(1/0)),limit}function
+mixinNodeList(tamed,nodeList,editable,opt_tameNodeCtor){var limit=getNodeListLength(nodeList),i;if(limit>0&&!opt_tameNodeCtor)throw'Internal: Nonempty mixinNodeList() without a tameNodeCtor';for(i=0;i<limit&&nodeList[i];++i)tamed[i]=opt_tameNodeCtor(nodeList[i],editable);return nodeList=null,tamed.item=___.markFuncFreeze(function(k){k&=2147483647;if(k!==k)throw new
+Error;return tamed[k]||null}),tamed}function tameNodeList(nodeList,editable,opt_tameNodeCtor){return ___.freeze(mixinNodeList([],nodeList,editable,opt_tameNodeCtor))}function
+tameOptionsList(nodeList,editable,opt_tameNodeCtor){var nl=mixinNodeList([],nodeList,editable,opt_tameNodeCtor);return nl.selectedIndex=+nodeList.selectedIndex,___.grantRead(nl,'selectedIndex'),___.freeze(nl)}function
+fakeNodeList(array){return array.item=___.markFuncFreeze(function(i){return array[i]}),___.freeze(array)}function
+mixinHTMLCollection(tamed,nodeList,editable,opt_tameNodeCtor){var i,name,tameNode,tameNodesByName;mixinNodeList(tamed,nodeList,editable,opt_tameNodeCtor),tameNodesByName={};for(i=0;i<tamed.length&&(tameNode=tamed[i]);++i)name=tameNode.getAttribute('name'),name&&!(name.charAt(name.length-1)==='_'||name
+in tamed||name===String(name&2147483647))&&(tameNodesByName[name]||(tameNodesByName[name]=[]),tameNodesByName[name].push(tameNode));return ___.forOwnKeys(tameNodesByName,___.markFuncFreeze(function(name,tameNodes){tameNodes.length>1?(tamed[name]=fakeNodeList(tameNodes)):(tamed[name]=tameNodes[0])})),tamed.namedItem=___.markFuncFreeze(function(name){return name=String(name),name.charAt(name.length-1)==='_'?null:___.hasOwnProp(tamed,name)?___.passesGuard(TameNodeT,tamed[name])?tamed[name]:tamed[name][0]:null}),tamed}function
+tameHTMLCollection(nodeList,editable,opt_tameNodeCtor){return ___.freeze(mixinHTMLCollection([],nodeList,editable,opt_tameNodeCtor))}function
+tameGetElementsByTagName(rootNode,tagName,editable){tagName=String(tagName);if(tagName!=='*'){tagName=tagName.toLowerCase();if(!___.hasOwnProp(html4
+.ELEMENTS,tagName)||html4 .ELEMENTS[tagName]&html4 .ELEMENTS.UNSAFE)return new fakeNodeList([])}return tameNodeList(rootNode.getElementsByTagName(tagName),editable,defaultTameNode)}function
+tameGetElementsByClassName(rootNode,className,editable){var candidate,candidateClass,candidates,classes,classi,i,j,k,limit,matches,nClasses,tamed;className=String(className),classes=className.match(/[^\t\n\f\r ]+/g);for(i=classes?classes.length:0;--i>=0;)classi=classes[i],FORBIDDEN_ID_PATTERN.test(classi)&&(classes[i]=classes[classes.length-1],--classes.length);if(!classes||classes.length===0)return fakeNodeList([]);if(typeof
+rootNode.getElementsByClassName==='function')return tameNodeList(rootNode.getElementsByClassName(classes.join(' ')),editable,defaultTameNode);nClasses=classes.length;for(i=nClasses;--i>=0;)classes[i]=' '+classes[i]+' ';candidates=rootNode.getElementsByTagName('*'),matches=[],limit=candidates.length,limit!==+limit&&(limit=(1/0));a:for(j=0,k=-1;j<limit&&(candidate=candidates[j]);++j){candidateClass=' '+candidate.className+' ';for(i=nClasses;--i>=0;)if(-1===candidateClass.indexOf(classes[i]))continue a;tamed=defaultTameNode(candidate,editable),tamed&&(matches[++k]=tamed)}return fakeNodeList(matches)}function
+makeEventHandlerWrapper(thisNode,listener){classUtils.ensureValidCallback(listener);function
+wrapper(event){return plugin_dispatchEvent___(thisNode,event,___.getId(imports),listener)}return wrapper}NOT_EDITABLE='Node not editable.',INVALID_SUFFIX='Property names may not end in \'__\'.',UNSAFE_TAGNAME='Unsafe tag name.',UNKNOWN_TAGNAME='Unknown tag name.',INDEX_SIZE_ERROR='Index size error.';function
+defProperty(ctor,name,useAttrGetter,toValue,useAttrSetter,fromValue){var getterSetterSuffix=classUtils.getterSetterSuffix(name),proto=ctor.prototype;toValue&&(proto['get'+getterSetterSuffix]=useAttrGetter?function(){return toValue.call(this,this.getAttribute(name))}:function(){return toValue.call(this,this.node___[name])}),fromValue&&(proto['set'+getterSetterSuffix]=useAttrSetter?function(value){return this.setAttribute(name,fromValue.call(this,value)),value}:function(value){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return this.node___[name]=fromValue.call(this,value),value})}function
+defAttributeAlias(ctor,name,toValue,fromValue){defProperty(ctor,name,true,toValue,true,fromValue)}function
+tameAddEventListener(name,listener,useCapture){var wrappedListener;if(!this.editable___)throw new
+Error(NOT_EDITABLE);this.wrappedListeners___||(this.wrappedListeners___=[]),useCapture=Boolean(useCapture),wrappedListener=makeEventHandlerWrapper(this.node___,listener),wrappedListener=bridal.addEventListener(this.node___,name,wrappedListener,useCapture),wrappedListener.originalListener___=listener,this.wrappedListeners___.push(wrappedListener)}function
+tameRemoveEventListener(name,listener,useCapture){var i,wrappedListener;if(!this.editable___)throw new
+Error(NOT_EDITABLE);if(!this.wrappedListeners___)return;wrappedListener=null;for(i=this.wrappedListeners___.length;--i>=0;)if(this.wrappedListeners___[i].originalListener___===listener){wrappedListener=this.wrappedListeners___[i],arrayRemove(this.wrappedListeners___,i,i);break}if(!wrappedListener)return;bridal.removeEventListener(this.node___,name,wrappedListener,useCapture)}nodeClasses={};function
+inertCtor(tamedCtor,someSuper,name){return nodeClasses[name]=___.extend(tamedCtor,someSuper,name)}tameNodeFields=['nodeType','nodeValue','nodeName','firstChild','lastChild','nextSibling','previousSibling','parentNode','ownerDocument','childNodes','attributes'];function
+TameNode(editable){this.editable___=editable,TameNodeMark.stamp.mark___(this),classUtils.exportFields(this,tameNodeFields)}inertCtor(TameNode,Object,'Node'),TameNode.prototype.getOwnerDocument___=function(){if(!this.editable___&&tameDocument.editable___)throw new
+Error(NOT_EDITABLE);return tameDocument},tameNodePublicMembers=['cloneNode','appendChild','insertBefore','removeChild','replaceChild','getElementsByClassName','getElementsByTagName','dispatchEvent','hasChildNodes'];function
+TameBackedNode(node,editable,childrenEditable){if(!node)throw new Error('Creating tame node with undefined native delegate');this.node___=node,this.childrenEditable___=editable&&childrenEditable,TameNode.call(this,editable)}___.extend(TameBackedNode,TameNode),TameBackedNode.prototype.getNodeType___=function(){return this.node___.nodeType},TameBackedNode.prototype.getNodeName___=function(){return this.node___.nodeName},TameBackedNode.prototype.getNodeValue___=function(){return this.node___.nodeValue},TameBackedNode.prototype.cloneNode=function(deep){var
+clone=bridal.cloneNode(this.node___,Boolean(deep));return defaultTameNode(clone,true)},TameBackedNode.prototype.appendChild=function(child){child=TameNodeT.coerce(child);if(!this.childrenEditable___||!child.editable___)throw new
+Error(NOT_EDITABLE);return this.node___.appendChild(child.node___),child},TameBackedNode.prototype.insertBefore=function(toInsert,child){toInsert=TameNodeT.coerce(toInsert),child===void
+0&&(child=null);if(child!==null){child=TameNodeT.coerce(child);if(!child.editable___)throw new
+Error(NOT_EDITABLE)}if(!this.childrenEditable___||!toInsert.editable___)throw new
+Error(NOT_EDITABLE);return this.node___.insertBefore(toInsert.node___,child!==null?child.node___:null),toInsert},TameBackedNode.prototype.removeChild=function(child){child=TameNodeT.coerce(child);if(!this.childrenEditable___||!child.editable___)throw new
+Error(NOT_EDITABLE);return this.node___.removeChild(child.node___),child},TameBackedNode.prototype.replaceChild=function(newChild,oldChild){newChild=TameNodeT.coerce(newChild),oldChild=TameNodeT.coerce(oldChild);if(!this.childrenEditable___||!newChild.editable___||!oldChild.editable___)throw new
+Error(NOT_EDITABLE);return this.node___.replaceChild(newChild.node___,oldChild.node___),oldChild},TameBackedNode.prototype.getFirstChild___=function(){return defaultTameNode(this.node___.firstChild,this.childrenEditable___)},TameBackedNode.prototype.getLastChild___=function(){return defaultTameNode(this.node___.lastChild,this.childrenEditable___)},TameBackedNode.prototype.getNextSibling___=function(){return tameRelatedNode(this.node___.nextSibling,this.editable___,defaultTameNode)},TameBackedNode.prototype.getPreviousSibling___=function(){return tameRelatedNode(this.node___.previousSibling,this.editable___,defaultTameNode)},TameBackedNode.prototype.getParentNode___=function(){return tameRelatedNode(this.node___.parentNode,this.editable___,defaultTameNode)},TameBackedNode.prototype.getElementsByTagName=function(tagName){return tameGetElementsByTagName(this.node___,tagName,this.childrenEditable___)},TameBackedNode.prototype.getElementsByClassName=function(className){return tameGetElementsByClassName(this.node___,className,this.childrenEditable___)},TameBackedNode.prototype.getChildNodes___=function(){return tameNodeList(this.node___.childNodes,this.childrenEditable___,defaultTameNode)},TameBackedNode.prototype.getAttributes___=function(){var
+thisNode=this.node___,tameNodeCtor=function(node,editable){return new TameBackedAttributeNode(node,editable,thisNode)};return tameNodeList(this.node___.attributes,this.editable___,tameNodeCtor)},endsWith__=/__$/,TameBackedNode.prototype.handleRead___=function(name){var
+handlerName;return name=String(name),endsWith__.test(name)?void 0:(handlerName=name+'_getter___',this[handlerName]?this[handlerName]():(handlerName=handlerName.toLowerCase(),this[handlerName]?this[handlerName]():___.hasOwnProp(this.node___.properties___,name)?this.node___.properties___[name]:void
+0))},TameBackedNode.prototype.handleCall___=function(name,args){var handlerName;name=String(name);if(endsWith__.test(name))throw new
+Error(INVALID_SUFFIX);handlerName=name+'_handler___';if(this[handlerName])return this[handlerName].call(this,args);handlerName=handlerName.toLowerCase();if(this[handlerName])return this[handlerName].call(this,args);if(___.hasOwnProp(this.node___.properties___,name))return this.node___.properties___[name].call(this,args);throw new
+TypeError(name+' is not a function.')},TameBackedNode.prototype.handleSet___=function(name,val){var
+handlerName;name=String(name);if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);if(!this.editable___)throw new
+Error(NOT_EDITABLE);return handlerName=name+'_setter___',this[handlerName]?this[handlerName](val):(handlerName=handlerName.toLowerCase(),this[handlerName]?this[handlerName](val):(this.node___.properties___||(this.node___.properties___={}),this[name+'_canEnum___']=true,this.node___.properties___[name]=val))},TameBackedNode.prototype.handleDelete___=function(name){var
+handlerName;name=String(name);if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);if(!this.editable___)throw new
+Error(NOT_EDITABLE);return handlerName=name+'_deleter___',this[handlerName]?this[handlerName]():(handlerName=handlerName.toLowerCase(),this[handlerName]?this[handlerName]():this.node___.properties___?delete
+this.node___.properties___[name]&&delete this[name+'_canEnum___']:true)},TameBackedNode.prototype.handleEnum___=function(ownFlag){return this.node___.properties___?___.allKeys(this.node___.properties___):[]},TameBackedNode.prototype.hasChildNodes=function(){return!!this.node___.hasChildNodes()},TameBackedNode.prototype.dispatchEvent=function
+dispatchEvent(evt){evt=TameEventT.coerce(evt),bridal.dispatchEvent(this.node___,evt.event___)},___.all2(___.grantTypedMethod,TameBackedNode.prototype,tameNodePublicMembers),document.documentElement.contains&&(TameBackedNode.prototype.contains=function(other){var
+otherNode;return other=TameNodeT.coerce(other),otherNode=other.node___,this.node___.contains(otherNode)}),'function'===typeof
+document.documentElement.compareDocumentPosition&&(TameBackedNode.prototype.compareDocumentPosition=function(other){var
+bitmask,otherNode;return other=TameNodeT.coerce(other),otherNode=other.node___,otherNode?(bitmask=+this.node___.compareDocumentPosition(otherNode),bitmask&1&&(bitmask&=-7),bitmask&31):0},___.hasOwnProp(TameBackedNode.prototype,'contains')||(TameBackedNode.prototype.contains=function(other){var
+docPos=this.compareDocumentPosition(other);return!(!(docPos&16)&&docPos)})),___.all2(function(o,k){___.hasOwnProp(o,k)&&___.grantTypedMethod(o,k)},TameBackedNode.prototype,['contains','compareDocumentPosition']);function
+TamePseudoNode(editable){TameNode.call(this,editable),this.properties___={}}___.extend(TamePseudoNode,TameNode),TamePseudoNode.prototype.appendChild=TamePseudoNode.prototype.insertBefore=TamePseudoNode.prototype.removeChild=TamePseudoNode.prototype.replaceChild=function(){return ___.log('Node not editable; no action performed.'),void
+0},TamePseudoNode.prototype.getFirstChild___=function(){var children=this.getChildNodes___();return children.length?children[0]:null},TamePseudoNode.prototype.getLastChild___=function(){var
+children=this.getChildNodes___();return children.length?children[children.length-1]:null},TamePseudoNode.prototype.getNextSibling___=function(){var
+parentNode=this.getParentNode___(),i,siblings;if(!parentNode)return null;siblings=parentNode.getChildNodes___();for(i=siblings.length-1;--i>=0;)if(siblings[i]===this)return siblings[i+1];return null},TamePseudoNode.prototype.getPreviousSibling___=function(){var
+parentNode=this.getParentNode___(),i,siblings;if(!parentNode)return null;siblings=parentNode.getChildNodes___();for(i=siblings.length;--i>=1;)if(siblings[i]===this)return siblings[i-1];return null},TamePseudoNode.prototype.handleRead___=function(name){var
+handlerName;return name=String(name),endsWith__.test(name)?void 0:(handlerName=name+'_getter___',this[handlerName]?this[handlerName]():(handlerName=handlerName.toLowerCase(),this[handlerName]?this[handlerName]():___.hasOwnProp(this.properties___,name)?this.properties___[name]:void
+0))},TamePseudoNode.prototype.handleCall___=function(name,args){var handlerName;name=String(name);if(endsWith__.test(name))throw new
+Error(INVALID_SUFFIX);handlerName=name+'_handler___';if(this[handlerName])return this[handlerName].call(this,args);handlerName=handlerName.toLowerCase();if(this[handlerName])return this[handlerName].call(this,args);if(___.hasOwnProp(this.properties___,name))return this.properties___[name].call(this,args);throw new
+TypeError(name+' is not a function.')},TamePseudoNode.prototype.handleSet___=function(name,val){var
+handlerName;name=String(name);if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);if(!this.editable___)throw new
+Error(NOT_EDITABLE);return handlerName=name+'_setter___',this[handlerName]?this[handlerName](val):(handlerName=handlerName.toLowerCase(),this[handlerName]?this[handlerName](val):(this.properties___||(this.properties___={}),this[name+'_canEnum___']=true,this.properties___[name]=val))},TamePseudoNode.prototype.handleDelete___=function(name){var
+handlerName;name=String(name);if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);if(!this.editable___)throw new
+Error(NOT_EDITABLE);return handlerName=name+'_deleter___',this[handlerName]?this[handlerName]():(handlerName=handlerName.toLowerCase(),this[handlerName]?this[handlerName]():this.properties___?delete
+this.properties___[name]&&delete this[name+'_canEnum___']:true)},TamePseudoNode.prototype.handleEnum___=function(ownFlag){return this.properties___?___.allKeys(this.properties___):[]},TamePseudoNode.prototype.hasChildNodes=function(){return this.getFirstChild___()!=null},___.all2(___.grantTypedMethod,TamePseudoNode.prototype,tameNodePublicMembers),commonElementPropertyHandlers={'clientWidth':{'get':function(){return this.getGeometryDelegate___().clientWidth}},'clientHeight':{'get':function(){return this.getGeometryDelegate___().clientHeight}},'offsetLeft':{'get':function(){return this.getGeometryDelegate___().offsetLeft}},'offsetTop':{'get':function(){return this.getGeometryDelegate___().offsetTop}},'offsetWidth':{'get':function(){return this.getGeometryDelegate___().offsetWidth}},'offsetHeight':{'get':function(){return this.getGeometryDelegate___().offsetHeight}},'scrollLeft':{'get':function(){return this.getGeometryDelegate___().scrollLeft},'set':function(x){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return this.getGeometryDelegate___().scrollLeft=+x,x}},'scrollTop':{'get':function(){return this.getGeometryDelegate___().scrollTop},'set':function(y){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return this.getGeometryDelegate___().scrollTop=+y,y}},'scrollWidth':{'get':function(){return this.getGeometryDelegate___().scrollWidth}},'scrollHeight':{'get':function(){return this.getGeometryDelegate___().scrollHeight}}};function
+TamePseudoElement(tagName,tameDoc,childNodesGetter,parentNodeGetter,innerHTMLGetter,geometryDelegate,editable){TamePseudoNode.call(this,editable),this.tagName___=tagName,this.tameDoc___=tameDoc,this.childNodesGetter___=childNodesGetter,this.parentNodeGetter___=parentNodeGetter,this.innerHTMLGetter___=innerHTMLGetter,this.geometryDelegate___=geometryDelegate,classUtils.exportFields(this,['tagName','innerHTML']),classUtils.applyAccessors(this,commonElementPropertyHandlers)}___.extend(TamePseudoElement,TamePseudoNode),TamePseudoElement.prototype.getNodeType___=function(){return 1},TamePseudoElement.prototype.getNodeName___=TamePseudoElement.prototype.getTagName___=function(){return this.tagName___},TamePseudoElement.prototype.getNodeValue___=function(){return null},TamePseudoElement.prototype.getAttribute=function(attribName){return null},TamePseudoElement.prototype.setAttribute=function(attribName,value){},TamePseudoElement.prototype.hasAttribute=function(attribName){return false},TamePseudoElement.prototype.removeAttribute=function(attribName){},TamePseudoElement.prototype.getOwnerDocument___=function(){return this.tameDoc___},TamePseudoElement.prototype.getChildNodes___=function(){return this.childNodesGetter___()},TamePseudoElement.prototype.getAttributes___=function(){return tameNodeList([],false,undefined)},TamePseudoElement.prototype.getParentNode___=function(){return this.parentNodeGetter___()},TamePseudoElement.prototype.getInnerHTML___=function(){return this.innerHTMLGetter___()},TamePseudoElement.prototype.getElementsByTagName=function(tagName){return tagName=String(tagName).toLowerCase(),tagName===this.tagName___?fakeNodeList([]):this.getOwnerDocument___().getElementsByTagName(tagName)},TamePseudoElement.prototype.getElementsByClassName=function(className){return this.getOwnerDocument___().getElementsByClassName(className)},TamePseudoElement.prototype.getBoundingClientRect=function(){return this.geometryDelegate___.getBoundingClientRect()},TamePseudoElement.prototype.getGeometryDelegate___=function(){return this.geometryDelegate___},TamePseudoElement.prototype.toString=function(){return'<'+this.tagName___+'>'},___.all2(___.grantTypedMethod,TamePseudoElement.prototype,['getAttribute','setAttribute','hasAttribute','removeAttribute','getBoundingClientRect','getElementsByTagName']);function
+TameOpaqueNode(node,editable){TameBackedNode.call(this,node,editable,editable)}___.extend(TameOpaqueNode,TameBackedNode),TameOpaqueNode.prototype.getNodeValue___=TameBackedNode.prototype.getNodeValue___,TameOpaqueNode.prototype.getNodeType___=TameBackedNode.prototype.getNodeType___,TameOpaqueNode.prototype.getNodeName___=TameBackedNode.prototype.getNodeName___,TameOpaqueNode.prototype.getNextSibling___=TameBackedNode.prototype.getNextSibling___,TameOpaqueNode.prototype.getPreviousSibling___=TameBackedNode.prototype.getPreviousSibling___,TameOpaqueNode.prototype.getFirstChild___=TameBackedNode.prototype.getFirstChild___,TameOpaqueNode.prototype.getLastChild___=TameBackedNode.prototype.getLastChild___,TameOpaqueNode.prototype.getParentNode___=TameBackedNode.prototype.getParentNode___,TameOpaqueNode.prototype.getChildNodes___=TameBackedNode.prototype.getChildNodes___,TameOpaqueNode.prototype.getOwnerDocument___=TameBackedNode.prototype.getOwnerDocument___,TameOpaqueNode.prototype.getElementsByTagName=TameBackedNode.prototype.getElementsByTagName,TameOpaqueNode.prototype.getElementsByClassName=TameBackedNode.prototype.getElementsByClassName,TameOpaqueNode.prototype.hasChildNodes=TameBackedNode.prototype.hasChildNodes,TameOpaqueNode.prototype.getAttributes___=function(){return tameNodeList([],false,undefined)};for(i=tameNodePublicMembers.length;--i>=0;)k=tameNodePublicMembers[i],TameOpaqueNode.prototype.hasOwnProperty(k)||(TameOpaqueNode.prototype[k]=___.markFuncFreeze(function(){throw new
+Error('Node is opaque')}));___.all2(___.grantTypedMethod,TameOpaqueNode.prototype,tameNodePublicMembers);function
+TameTextNode(node,editable){var pn;assert(node.nodeType===3),pn=node.parentNode,editable&&pn&&(1===pn.nodeType&&html4
+.ELEMENTS[pn.tagName.toLowerCase()]&html4 .eflags.UNSAFE&&(editable=false)),TameBackedNode.call(this,node,editable,editable),classUtils.exportFields(this,['nodeValue','data','textContent','innerText'])}inertCtor(TameTextNode,TameBackedNode,'Text'),TameTextNode.prototype.setNodeValue___=function(value){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return this.node___.nodeValue=String(value||''),value},TameTextNode.prototype.getTextContent___=TameTextNode.prototype.getInnerText___=TameTextNode.prototype.getData___=TameTextNode.prototype.getNodeValue___,TameTextNode.prototype.setTextContent___=TameTextNode.prototype.setInnerText___=TameTextNode.prototype.setData___=TameTextNode.prototype.setNodeValue___,TameTextNode.prototype.toString=function(){return'#text'};function
+TameCommentNode(node,editable){assert(node.nodeType===8),TameBackedNode.call(this,node,editable,editable)}inertCtor(TameCommentNode,TameBackedNode,'CommentNode'),TameCommentNode.prototype.toString=function(){return'#comment'};function
+getAttributeType(tagName,attribName){var attribKey=tagName+'::'+attribName;return html4
+.ATTRIBS.hasOwnProperty(attribKey)?html4 .ATTRIBS[attribKey]:(attribKey='*::'+attribName,html4
+.ATTRIBS.hasOwnProperty(attribKey)?html4 .ATTRIBS[attribKey]:void 0)}function TameBackedAttributeNode(node,editable,ownerElement){TameBackedNode.call(this,node,editable),this.ownerElement___=ownerElement,classUtils.exportFields(this,['name','specified','value','ownerElement'])}inertCtor(TameBackedAttributeNode,TameBackedNode,'Attr'),TameBackedAttributeNode.prototype.getNodeName___=TameBackedAttributeNode.prototype.getName___=function(){return String(this.node___.name)},TameBackedAttributeNode.prototype.getSpecified___=function(){return defaultTameNode(this.ownerElement___,this.editable___).hasAttribute(this.getName___())},TameBackedAttributeNode.prototype.getNodeValue___=TameBackedAttributeNode.prototype.getValue___=function(){return defaultTameNode(this.ownerElement___,this.editable___).getAttribute(this.getName___())},TameBackedAttributeNode.prototype.setNodeValue___=TameBackedAttributeNode.prototype.setValue___=function(value){return defaultTameNode(this.ownerElement___,this.editable___).setAttribute(this.getName___(),value)},TameBackedAttributeNode.prototype.getOwnerElement___=function(){return defaultTameNode(this.ownerElement___,this.editable___)},TameBackedAttributeNode.prototype.getNodeType___=function(){return 2},TameBackedAttributeNode.prototype.cloneNode=function(deep){var
+clone=bridal.cloneNode(this.node___,Boolean(deep));return new TameBackedAttributeNode(clone,true,this.ownerElement____)},TameBackedAttributeNode.prototype.appendChild=TameBackedAttributeNode.prototype.insertBefore=TameBackedAttributeNode.prototype.removeChild=TameBackedAttributeNode.prototype.replaceChild=TameBackedAttributeNode.prototype.getFirstChild___=TameBackedAttributeNode.prototype.getLastChild___=TameBackedAttributeNode.prototype.getNextSibling___=TameBackedAttributeNode.prototype.getPreviousSibling___=TameBackedAttributeNode.prototype.getParentNode___=TameBackedAttributeNode.prototype.getElementsByTagName=TameBackedAttributeNode.prototype.getElementsByClassName=TameBackedAttributeNode.prototype.getChildNodes___=TameBackedAttributeNode.prototype.getAttributes___=function(){throw new
+Error('Not implemented.')},TameBackedAttributeNode.prototype.toString=function(){return'[Fake attribute node]'};function
+registerElementScriptAttributeHandlers(aTameElement){var attrNameRe=/::(.*)/,html4Attrib;for(html4Attrib
+in html4 .ATTRIBS)html4 .atype.SCRIPT===html4 .ATTRIBS[html4Attrib]&&(function(attribName){___.useSetHandler(aTameElement,attribName,function
+eventHandlerSetter(listener){if(!this.editable___)throw new Error(NOT_EDITABLE);return listener?(this.node___[attribName]=makeEventHandlerWrapper(this.node___,listener)):(this.node___[attribName]=null),listener})})((html4Attrib.match(attrNameRe))[1])}function
+TameElement(node,editable,childrenEditable){assert(node.nodeType===1),TameBackedNode.call(this,node,editable,childrenEditable),classUtils.exportFields(this,['className','id','innerHTML','tagName','style','offsetParent','title','dir','innerText','textContent']),classUtils.applyAccessors(this,commonElementPropertyHandlers),registerElementScriptAttributeHandlers(this)}nodeClasses.Element=inertCtor(TameElement,TameBackedNode,'HTMLElement'),TameElement.prototype.blur=function(){this.node___.blur()},TameElement.prototype.focus=function(){imports.isProcessingEvent___&&this.node___.focus()},document.documentElement.setActive&&(TameElement.prototype.setActive=function(){imports.isProcessingEvent___&&this.node___.setActive()},___.grantTypedMethod(TameElement.prototype,'setActive')),document.documentElement.hasFocus&&(TameElement.prototype.hasFocus=function(){return this.node___.hasFocus()},___.grantTypedMethod(TameElement.prototype,'hasFocus')),defAttributeAlias(TameElement,'id',defaultToEmptyStr,identity),TameElement.prototype.getAttribute=function(attribName){var
+atype,tagName,value;return attribName=String(attribName).toLowerCase(),tagName=this.node___.tagName.toLowerCase(),atype=getAttributeType(tagName,attribName),atype===void
+0?this.node___.attributes___?this.node___.attributes___[attribName]||null:null:(value=bridal.getAttribute(this.node___,attribName),'string'!==typeof
+value?value:virtualizeAttributeValue(atype,value))},TameElement.prototype.getAttributeNode=function(name){var
+hostDomNode=this.node___.getAttributeNode(name);return hostDomNode===null?null:new
+TameBackedAttributeNode(hostDomNode,this.editable___,this.node___)},TameElement.prototype.hasAttribute=function(attribName){var
+atype,tagName;return attribName=String(attribName).toLowerCase(),tagName=this.node___.tagName.toLowerCase(),atype=getAttributeType(tagName,attribName),atype===void
+0?!!(this.node___.attributes___&&___.hasOwnProp(this.node___.attributes___,attribName)):bridal.hasAttribute(this.node___,attribName)},TameElement.prototype.setAttribute=function(attribName,value){var
+atype,sanitizedValue,tagName;if(!this.editable___)throw new Error(NOT_EDITABLE);return attribName=String(attribName).toLowerCase(),tagName=this.node___.tagName.toLowerCase(),atype=getAttributeType(tagName,attribName),atype===void
+0?(this.node___.attributes___||(this.node___.attributes___={}),this.node___.attributes___[attribName]=String(value)):(sanitizedValue=rewriteAttribute(tagName,attribName,atype,value),sanitizedValue!==null&&bridal.setAttribute(this.node___,attribName,sanitizedValue)),value},TameElement.prototype.removeAttribute=function(attribName){var
+atype,tagName;if(!this.editable___)throw new Error(NOT_EDITABLE);attribName=String(attribName).toLowerCase(),tagName=this.node___.tagName.toLowerCase(),atype=getAttributeType(tagName,attribName),atype===void
+0?this.node___.attributes___&&delete this.node___.attributes___[attribName]:this.node___.removeAttribute(attribName)},TameElement.prototype.getBoundingClientRect=function(){var
+elRect=bridal.getBoundingClientRect(this.node___),vbody=bridal.getBoundingClientRect(this.getOwnerDocument___().body___),vbodyLeft=vbody.left,vbodyTop=vbody.top;return{'top':elRect.top-vbodyTop,'left':elRect.left-vbodyLeft,'right':elRect.right-vbodyLeft,'bottom':elRect.bottom-vbodyTop}},TameElement.prototype.getClassName___=function(){return this.getAttribute('class')||''},TameElement.prototype.setClassName___=function(classes){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return this.setAttribute('class',String(classes))};function
+defaultToEmptyStr(x){return x||''}defAttributeAlias(TameElement,'title',defaultToEmptyStr,String),defAttributeAlias(TameElement,'dir',defaultToEmptyStr,String);function
+innerTextOf(rawNode,out){var c,tagName;switch(rawNode.nodeType){case 1:tagName=rawNode.tagName.toLowerCase();if(html4
+.ELEMENTS.hasOwnProperty(tagName)&&!(html4 .ELEMENTS[tagName]&html4 .eflags.UNSAFE))for(c=rawNode.firstChild;c;c=c.nextSibling)innerTextOf(c,out);break;case
+3:case 4:out[out.length]=rawNode.data;break;case 11:for(c=rawNode.firstChild;c;c=c.nextSibling)innerTextOf(c,out)}}TameElement.prototype.getTextContent___=TameElement.prototype.getInnerText___=function(){var
+text=[];return innerTextOf(this.node___,text),text.join('')},TameElement.prototype.setTextContent___=TameElement.prototype.setInnerText___=function(newText){var
+c,el,newTextStr;if(!this.editable___)throw new Error(NOT_EDITABLE);newTextStr=newText!=null?String(newText):'',el=this.node___;for(;c=el.firstChild;)el.removeChild(c);return newTextStr&&el.appendChild(el.ownerDocument.createTextNode(newTextStr)),newText},TameElement.prototype.getTagName___=TameBackedNode.prototype.getNodeName___,TameElement.prototype.getInnerHTML___=function(){var
+tagName=this.node___.tagName.toLowerCase(),flags,innerHtml;return html4 .ELEMENTS.hasOwnProperty(tagName)?(flags=html4
+.ELEMENTS[tagName],innerHtml=this.node___.innerHTML,flags&html4 .eflags.CDATA?(innerHtml=html.escapeAttrib(innerHtml)):flags&html4
+.eflags.RCDATA?(innerHtml=html.normalizeRCData(innerHtml)):(innerHtml=tameInnerHtml(innerHtml)),innerHtml):''},TameElement.prototype.setInnerHTML___=function(htmlFragment){var
+flags,htmlFragmentString,isRcData,sanitizedHtml,tagName;if(!this.editable___)throw new
+Error(NOT_EDITABLE);tagName=this.node___.tagName.toLowerCase();if(!html4 .ELEMENTS.hasOwnProperty(tagName))throw new
+Error;flags=html4 .ELEMENTS[tagName];if(flags&html4 .eflags.UNSAFE)throw new Error;return isRcData=flags&html4
+.eflags.RCDATA,!isRcData&&htmlFragment instanceof Html?(htmlFragmentString=''+safeHtml(htmlFragment)):htmlFragment===null?(htmlFragmentString=''):(htmlFragmentString=''+htmlFragment),sanitizedHtml=isRcData?html.normalizeRCData(htmlFragmentString):sanitizeHtml(htmlFragmentString),this.node___.innerHTML=sanitizedHtml,htmlFragment};function
+identity(x){return x}defProperty(TameElement,'style',false,function(styleNode){return new
+TameStyle(styleNode,this.editable___,this)},true,identity),TameElement.prototype.updateStyle=function(style){var
+cssPropertiesAndValues,i,propName,propValue,semi,styleNode;if(!this.editable___)throw new
+Error(NOT_EDITABLE);cssPropertiesAndValues=cssSealerUnsealerPair.unseal(style);if(!cssPropertiesAndValues)throw new
+Error;styleNode=this.node___.style;for(i=0;i<cssPropertiesAndValues.length;i+=2)propName=cssPropertiesAndValues[i],propValue=cssPropertiesAndValues[i+1],semi=propName.indexOf(';'),semi>=0&&(propName=propName.substring(semi+1)),styleNode[propName]=propValue},TameElement.prototype.getOffsetParent___=function(){return tameRelatedNode(this.node___.offsetParent,this.editable___,defaultTameNode)},TameElement.prototype.getGeometryDelegate___=function(){return this.node___},TameElement.prototype.toString=function(){return'<'+this.node___.tagName+'>'},TameElement.prototype.addEventListener=tameAddEventListener,TameElement.prototype.removeEventListener=tameRemoveEventListener,___.all2(___.grantTypedMethod,TameElement.prototype,['addEventListener','removeEventListener','blur','focus','getAttribute','setAttribute','removeAttribute','hasAttribute','getAttributeNode','getBoundingClientRect','updateStyle']);function
+TameAElement(node,editable){TameElement.call(this,node,editable,editable),classUtils.exportFields(this,['href'])}inertCtor(TameAElement,TameElement,'HTMLAnchorElement'),defProperty(TameAElement,'href',false,identity,true,identity);function
+TameFormElement(node,editable){TameElement.call(this,node,editable,editable),this.length=node.length,classUtils.exportFields(this,['action','elements','enctype','method','target'])}inertCtor(TameFormElement,TameElement,'HTMLFormElement'),TameFormElement.prototype.handleRead___=function(name){var
+tameElements;name=String(name);if(endsWith__.test(name))return;if(___.passesGuard(TameNodeT,this)){tameElements=this.getElements___();if(___.hasOwnProp(tameElements,name))return tameElements[name]}return TameBackedNode.prototype.handleRead___.call(this,name)},TameFormElement.prototype.submit=function(){return this.node___.submit()},TameFormElement.prototype.reset=function(){return this.node___.reset()},defAttributeAlias(TameFormElement,'action',defaultToEmptyStr,String),TameFormElement.prototype.getElements___=function(){return tameHTMLCollection(this.node___.elements,this.editable___,defaultTameNode)},defAttributeAlias(TameFormElement,'enctype',defaultToEmptyStr,String),defAttributeAlias(TameFormElement,'method',defaultToEmptyStr,String),defAttributeAlias(TameFormElement,'target',defaultToEmptyStr,String),TameFormElement.prototype.reset=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);this.node___.reset()},TameFormElement.prototype.submit=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);this.node___.submit()},___.all2(___.grantTypedMethod,TameFormElement.prototype,['reset','submit']);function
+TameInputElement(node,editable){TameElement.call(this,node,editable,editable),classUtils.exportFields(this,['form','value','defaultValue','checked','disabled','readOnly','options','selected','selectedIndex','name','accessKey','tabIndex','text','defaultChecked','defaultSelected','maxLength','size','type','index','label','multiple','cols','rows'])}inertCtor(TameInputElement,TameElement,'HTMLInputElement'),defProperty(TameInputElement,'checked',false,identity,false,Boolean),defProperty(TameInputElement,'defaultChecked',false,identity,false,identity),defProperty(TameInputElement,'value',false,function(x){return x==null?null:String(x)},false,function(x){return x==null?'':''+x}),defProperty(TameInputElement,'defaultValue',false,function(x){return x==null?null:String(x)},false,function(x){return x==null?'':''+x}),TameInputElement.prototype.select=function(){this.node___.select()},TameInputElement.prototype.getForm___=function(){return tameRelatedNode(this.node___.form,this.editable___,defaultTameNode)},defProperty(TameInputElement,'disabled',false,identity,false,identity),defProperty(TameInputElement,'readOnly',false,identity,false,identity),TameInputElement.prototype.getOptions___=function(){return tameOptionsList(this.node___.options,this.editable___,defaultTameNode,'name')},defProperty(TameInputElement,'selected',false,identity,false,identity),defProperty(TameInputElement,'defaultSelected',false,identity,false,Boolean);function
+toInt(x){return x|0}defProperty(TameInputElement,'selectedIndex',false,identity,false,toInt),defProperty(TameInputElement,'name',false,identity,false,identity),defProperty(TameInputElement,'accessKey',false,identity,false,identity),defProperty(TameInputElement,'tabIndex',false,identity,false,identity),defProperty(TameInputElement,'text',false,String),defProperty(TameInputElement,'maxLength',false,identity,false,identity),defProperty(TameInputElement,'size',false,identity,false,identity),defProperty(TameInputElement,'type',false,identity,false,identity),defProperty(TameInputElement,'index',false,identity,false,identity),defProperty(TameInputElement,'label',false,identity,false,identity),defProperty(TameInputElement,'multiple',false,identity,false,identity),defProperty(TameInputElement,'cols',false,identity,false,identity),defProperty(TameInputElement,'rows',false,identity,false,identity),___.all2(___.grantTypedMethod,TameInputElement.prototype,['select']);function
+TameImageElement(node,editable){TameElement.call(this,node,editable,editable),classUtils.exportFields(this,['src','alt'])}inertCtor(TameImageElement,TameElement,'HTMLImageElement'),defProperty(TameImageElement,'src',false,identity,true,identity),defProperty(TameImageElement,'alt',false,identity,false,String);function
+TameLabelElement(node,editable){TameElement.call(this,node,editable,editable),classUtils.exportFields(this,['htmlFor'])}inertCtor(TameLabelElement,TameElement,'HTMLLabelElement'),TameLabelElement.prototype.getHtmlFor___=function(){return this.getAttribute('for')},TameLabelElement.prototype.setHtmlFor___=function(id){return this.setAttribute('for',id),id};function
+TameScriptElement(node,editable){TameElement.call(this,node,editable,false),classUtils.exportFields(this,['src'])}inertCtor(TameScriptElement,TameElement,'HTMLScriptElement'),defProperty(TameScriptElement,'src',false,identity,true,identity);function
+TameIFrameElement(node,editable){TameElement.call(this,node,editable,false),classUtils.exportFields(this,['align','frameBorder','height','width'])}inertCtor(TameIFrameElement,TameElement,'HTMLIFrameElement'),TameIFrameElement.prototype.getAlign___=function(){return this.node___.align},TameIFrameElement.prototype.setAlign___=function(alignment){if(!this.editable___)throw new
+Error(NOT_EDITABLE);alignment=String(alignment),(alignment==='left'||alignment==='right'||alignment==='center')&&(this.node___.align=alignment)},TameIFrameElement.prototype.getAttribute=function(attr){var
+attrLc=String(attr).toLowerCase();return attrLc!=='name'&&attrLc!=='src'?TameElement.prototype.getAttribute.call(this,attr):null},TameIFrameElement.prototype.setAttribute=function(attr,value){var
+attrLc=String(attr).toLowerCase();return attrLc!=='name'&&attrLc!=='src'?TameElement.prototype.setAttribute.call(this,attr,value):(___.log('Cannot set the ['+attrLc+'] attribute of an iframe.'),value)},TameIFrameElement.prototype.getFrameBorder___=function(){return this.node___.frameBorder},TameIFrameElement.prototype.setFrameBorder___=function(border){if(!this.editable___)throw new
+Error(NOT_EDITABLE);border=String(border).toLowerCase(),(border==='0'||border==='1'||border==='no'||border==='yes')&&(this.node___.frameBorder=border)},defProperty(TameIFrameElement,'height',false,identity,false,Number),defProperty(TameIFrameElement,'width',false,identity,false,Number),TameIFrameElement.prototype.handleRead___=function(name){var
+nameLc=String(name).toLowerCase();return nameLc!=='src'&&nameLc!=='name'?TameElement.prototype.handleRead___.call(this,name):undefined},TameIFrameElement.prototype.handleSet___=function(name,value){var
+nameLc=String(name).toLowerCase();return nameLc!=='src'&&nameLc!=='name'?TameElement.prototype.handleSet___.call(this,name,value):(___.log('Cannot set the ['+nameLc+'] property of an iframe.'),value)},___.all2(___.grantTypedMethod,TameIFrameElement.prototype,['getAttribute','setAttribute']);function
+TameTableCompElement(node,editable){TameElement.call(this,node,editable,editable),classUtils.exportFields(this,['colSpan','cells','cellIndex','rowSpan','rows','rowIndex','align','vAlign','nowrap','sectionRowIndex'])}___.extend(TameTableCompElement,TameElement),defProperty(TameTableCompElement,'colSpan',false,identity,false,identity),TameTableCompElement.prototype.getCells___=function(){return tameNodeList(this.node___.cells,this.editable___,defaultTameNode)},TameTableCompElement.prototype.getCellIndex___=function(){return this.node___.cellIndex},defProperty(TameTableCompElement,'rowSpan',false,identity,false,identity),TameTableCompElement.prototype.getRows___=function(){return tameNodeList(this.node___.rows,this.editable___,defaultTameNode)},TameTableCompElement.prototype.getRowIndex___=function(){return this.node___.rowIndex},TameTableCompElement.prototype.getSectionRowIndex___=function(){return this.node___.sectionRowIndex},defProperty(TameTableCompElement,'align',false,identity,false,identity),defProperty(TameTableCompElement,'vAlign',false,identity,false,identity),defProperty(TameTableCompElement,'nowrap',false,identity,false,identity),TameTableCompElement.prototype.insertRow=function(index){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return requireIntIn(index,-1,this.node___.rows.length),defaultTameNode(this.node___.insertRow(index),this.editable___)},TameTableCompElement.prototype.deleteRow=function(index){if(!this.editable___)throw new
+Error(NOT_EDITABLE);requireIntIn(index,-1,this.node___.rows.length),this.node___.deleteRow(index)},___.all2(___.grantTypedMethod,TameTableCompElement.prototype,['insertRow','deleteRow']);function
+requireIntIn(idx,min,max){if(idx!==(idx|0)||idx<min||idx>max)throw new Error(INDEX_SIZE_ERROR)}function
+TameTableRowElement(node,editable){TameTableCompElement.call(this,node,editable)}inertCtor(TameTableRowElement,TameTableCompElement,'HTMLTableRowElement'),TameTableRowElement.prototype.insertCell=function(index){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return requireIntIn(index,-1,this.node___.cells.length),defaultTameNode(this.node___.insertCell(index),this.editable___)},TameTableRowElement.prototype.deleteCell=function(index){if(!this.editable___)throw new
+Error(NOT_EDITABLE);requireIntIn(index,-1,this.node___.cells.length),this.node___.deleteCell(index)},___.all2(___.grantTypedMethod,TameTableRowElement.prototype,['insertCell','deleteCell']);function
+TameTableElement(node,editable){TameTableCompElement.call(this,node,editable),classUtils.exportFields(this,['tBodies','tHead','tFoot','cellPadding','cellSpacing','border'])}inertCtor(TameTableElement,TameTableCompElement,'HTMLTableElement'),TameTableElement.prototype.getTBodies___=function(){return tameNodeList(this.node___.tBodies,this.editable___,defaultTameNode)},TameTableElement.prototype.getTHead___=function(){return defaultTameNode(this.node___.tHead,this.editable___)},TameTableElement.prototype.getTFoot___=function(){return defaultTameNode(this.node___.tFoot,this.editable___)},TameTableElement.prototype.createTHead=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return defaultTameNode(this.node___.createTHead(),this.editable___)},TameTableElement.prototype.deleteTHead=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);this.node___.deleteTHead()},TameTableElement.prototype.createTFoot=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return defaultTameNode(this.node___.createTFoot(),this.editable___)},TameTableElement.prototype.deleteTFoot=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);this.node___.deleteTFoot()},TameTableElement.prototype.createCaption=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return defaultTameNode(this.node___.createCaption(),this.editable___)},TameTableElement.prototype.deleteCaption=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);this.node___.deleteCaption()},TameTableElement.prototype.insertRow=function(index){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return requireIntIn(index,-1,this.node___.rows.length),defaultTameNode(this.node___.insertRow(index),this.editable___)},TameTableElement.prototype.deleteRow=function(index){if(!this.editable___)throw new
+Error(NOT_EDITABLE);requireIntIn(index,-1,this.node___.rows.length),this.node___.deleteRow(index)};function
+fromInt(x){return''+(x|0)}defAttributeAlias(TameTableElement,'cellPadding',Number,fromInt),defAttributeAlias(TameTableElement,'cellSpacing',Number,fromInt),defAttributeAlias(TameTableElement,'border',Number,fromInt),___.all2(___.grantTypedMethod,TameTableElement.prototype,['createTHead','deleteTHead','createTFoot','deleteTFoot','createCaption','deleteCaption','insertRow','deleteRow']);function
+tameEvent(event){return event.tamed___?event.tamed___:(event.tamed___=new TameEvent(event))}function
+TameEvent(event){assert(!!event),this.event___=event,TameEventMark.stamp.mark___(this),classUtils.exportFields(this,['type','target','pageX','pageY','altKey','ctrlKey','metaKey','shiftKey','button','screenX','screenY','currentTarget','relatedTarget','fromElement','toElement','srcElement','clientX','clientY','keyCode','which'])}inertCtor(TameEvent,Object,'Event'),TameEvent.prototype.getType___=function(){return bridal.untameEventType(String(this.event___.type))},TameEvent.prototype.getTarget___=function(){var
+event=this.event___;return tameRelatedNode(event.target||event.srcElement,true,defaultTameNode)},TameEvent.prototype.getSrcElement___=function(){return tameRelatedNode(this.event___.srcElement,true,defaultTameNode)},TameEvent.prototype.getCurrentTarget___=function(){var
+e=this.event___;return tameRelatedNode(e.currentTarget,true,defaultTameNode)},TameEvent.prototype.getRelatedTarget___=function(){var
+e=this.event___,t=e.relatedTarget;return t||(e.type==='mouseout'?(t=e.toElement):e.type==='mouseover'&&(t=e.fromElement)),tameRelatedNode(t,true,defaultTameNode)},TameEvent.prototype.setRelatedTarget___=function(newValue){return newValue},TameEvent.prototype.getFromElement___=function(){return tameRelatedNode(this.event___.fromElement,true,defaultTameNode)},TameEvent.prototype.getToElement___=function(){return tameRelatedNode(this.event___.toElement,true,defaultTameNode)},TameEvent.prototype.getPageX___=function(){return Number(this.event___.pageX)},TameEvent.prototype.getPageY___=function(){return Number(this.event___.pageY)},TameEvent.prototype.stopPropagation=function(){this.event___.stopPropagation?this.event___.stopPropagation():(this.event___.cancelBubble=true)},TameEvent.prototype.preventDefault=function(){this.event___.preventDefault?this.event___.preventDefault():(this.event___.returnValue=false)},TameEvent.prototype.getAltKey___=function(){return Boolean(this.event___.altKey)},TameEvent.prototype.getCtrlKey___=function(){return Boolean(this.event___.ctrlKey)},TameEvent.prototype.getMetaKey___=function(){return Boolean(this.event___.metaKey)},TameEvent.prototype.getShiftKey___=function(){return Boolean(this.event___.shiftKey)},TameEvent.prototype.getButton___=function(){var
+e=this.event___;return e.button&&Number(e.button)},TameEvent.prototype.getClientX___=function(){return Number(this.event___.clientX)},TameEvent.prototype.getClientY___=function(){return Number(this.event___.clientY)},TameEvent.prototype.getScreenX___=function(){return Number(this.event___.screenX)},TameEvent.prototype.getScreenY___=function(){return Number(this.event___.screenY)},TameEvent.prototype.getWhich___=function(){var
+w=this.event___.which;return w&&Number(w)},TameEvent.prototype.getKeyCode___=function(){var
+kc=this.event___.keyCode;return kc&&Number(kc)},TameEvent.prototype.toString=function(){return'[Fake Event]'},___.all2(___.grantTypedMethod,TameEvent.prototype,['stopPropagation','preventDefault']);function
+TameCustomHTMLEvent(event){TameEvent.call(this,event),this.properties___={}}___.extend(TameCustomHTMLEvent,TameEvent),TameCustomHTMLEvent.prototype.initEvent=function(type,bubbles,cancelable){bridal.initEvent(this.event___,type,bubbles,cancelable)},TameCustomHTMLEvent.prototype.handleRead___=function(name){var
+handlerName;return name=String(name),endsWith__.test(name)?void 0:(handlerName=name+'_getter___',this[handlerName]?this[handlerName]():___.hasOwnProp(this.event___.properties___,name)?this.event___.properties___[name]:void
+0)},TameCustomHTMLEvent.prototype.handleCall___=function(name,args){var handlerName;name=String(name);if(endsWith__.test(name))throw new
+Error(INVALID_SUFFIX);handlerName=name+'_handler___';if(this[handlerName])return this[handlerName].call(this,args);if(___.hasOwnProp(this.event___.properties___,name))return this.event___.properties___[name].call(this,args);throw new
+TypeError(name+' is not a function.')},TameCustomHTMLEvent.prototype.handleSet___=function(name,val){var
+handlerName;name=String(name);if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);return handlerName=name+'_setter___',this[handlerName]?this[handlerName](val):(this.event___.properties___||(this.event___.properties___={}),this[name+'_canEnum___']=true,this.event___.properties___[name]=val)},TameCustomHTMLEvent.prototype.handleDelete___=function(name){var
+handlerName;name=String(name);if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);return handlerName=name+'_deleter___',this[handlerName]?this[handlerName]():this.event___.properties___?delete
+this.event___.properties___[name]&&delete this[name+'_canEnum___']:true},TameCustomHTMLEvent.prototype.handleEnum___=function(ownFlag){return this.event___.properties___?___.allKeys(this.event___.properties___):[]},TameCustomHTMLEvent.prototype.toString=function(){return'[Fake CustomEvent]'},___.grantTypedMethod(TameCustomHTMLEvent.prototype,'initEvent');function
+TameHTMLDocument(doc,body,domain,editable){var tameBody,tameBodyElement,tameDoc,tameHeadElement,tameHtmlElement,tameTitleElement,title;TamePseudoNode.call(this,editable),this.doc___=doc,this.body___=body,this.domain___=domain,this.onLoadListeners___=[],tameDoc=this,tameBody=defaultTameNode(body,editable),this.tameBody___=tameBody,tameBodyElement=new
+TamePseudoElement('BODY',this,function(){return tameNodeList(body.childNodes,editable,defaultTameNode)},function(){return tameHtmlElement},function(){return tameInnerHtml(body.innerHTML)},tameBody,editable),___.forOwnKeys({'appendChild':0,'removeChild':0,'insertBefore':0,'replaceChild':0},___.markFuncFreeze(function(k){tameBodyElement[k]=tameBody[k].bind(tameBody),___.grantFunc(tameBodyElement,k)})),title=doc.createTextNode(body.getAttribute('title')||''),tameTitleElement=new
+TamePseudoElement('TITLE',this,function(){return[defaultTameNode(title,false)]},function(){return tameHeadElement},function(){return html.escapeAttrib(title.nodeValue)},null,editable),tameHeadElement=new
+TamePseudoElement('HEAD',this,function(){return[tameTitleElement]},function(){return tameHtmlElement},function(){return'<title>'+tameTitleElement.getInnerHTML___()+'</title>'},null,editable),tameHtmlElement=new
+TamePseudoElement('HTML',this,function(){return[tameHeadElement,tameBodyElement]},function(){return tameDoc},function(){return'<head>'+tameHeadElement.getInnerHTML___()+'</head><body>'+tameBodyElement.getInnerHTML___()+'</body>'},tameBody,editable),body.contains&&(tameHtmlElement.contains=function(other){var
+otherNode;return other=TameNodeT.coerce(other),otherNode=other.node___,body.contains(otherNode)},___.grantFunc(tameHtmlElement,'contains')),'function'===typeof
+body.compareDocumentPosition&&(tameHtmlElement.compareDocumentPosition=function(other){var
+bitmask,otherNode;return other=TameNodeT.coerce(other),otherNode=other.node___,otherNode?(bitmask=+body.compareDocumentPosition(otherNode),bitmask&31):0},___.hasOwnProp(tameHtmlElement,'contains')||(tameHtmlElement.contains=(function(other){var
+docPos=this.compareDocumentPosition(other);return!(!(docPos&16)&&docPos)}).bind(tameHtmlElement),___.grantFunc(tameHtmlElement,'contains')),___.grantFunc(tameHtmlElement,'compareDocumentPosition')),this.documentElement___=tameHtmlElement,classUtils.exportFields(this,['documentElement','body','title','domain','forms','compatMode'])}inertCtor(TameHTMLDocument,TamePseudoNode,'HTMLDocument'),TameHTMLDocument.prototype.getNodeType___=function(){return 9},TameHTMLDocument.prototype.getNodeName___=function(){return'#document'},TameHTMLDocument.prototype.getNodeValue___=function(){return null},TameHTMLDocument.prototype.getChildNodes___=function(){return[this.documentElement___]},TameHTMLDocument.prototype.getAttributes___=function(){return[]},TameHTMLDocument.prototype.getParentNode___=function(){return null},TameHTMLDocument.prototype.getElementsByTagName=function(tagName){tagName=String(tagName).toLowerCase();switch(tagName){case'body':return fakeNodeList([this.getBody___()]);case'head':return fakeNodeList([this.getHead___()]);case'title':return fakeNodeList([this.getTitle___()]);case'html':return fakeNodeList([this.getDocumentElement___()]);default:return tameGetElementsByTagName(this.body___,tagName,this.editable___)}},TameHTMLDocument.prototype.getDocumentElement___=function(){return this.documentElement___},TameHTMLDocument.prototype.getBody___=function(){return this.documentElement___.getLastChild___()},TameHTMLDocument.prototype.getHead___=function(){return this.documentElement___.getFirstChild___()},TameHTMLDocument.prototype.getTitle___=function(){return this.getHead___().getFirstChild___()},TameHTMLDocument.prototype.getDomain___=function(){return this.domain___},TameHTMLDocument.prototype.getElementsByClassName=function(className){return tameGetElementsByClassName(this.body___,className,this.editable___)},TameHTMLDocument.prototype.addEventListener=function(name,listener,useCapture){return this.tameBody___.addEventListener(name,listener,useCapture)},TameHTMLDocument.prototype.removeEventListener=function(name,listener,useCapture){return this.tameBody___.removeEventListener(name,listener,useCapture)},TameHTMLDocument.prototype.createComment=function(text){return defaultTameNode(this.doc___.createComment(' '),true)},TameHTMLDocument.prototype.createDocumentFragment=function(){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return defaultTameNode(this.doc___.createDocumentFragment(),true)},TameHTMLDocument.prototype.createElement=function(tagName){var
+attribs,flags,i,newEl;if(!this.editable___)throw new Error(NOT_EDITABLE);tagName=String(tagName).toLowerCase();if(!html4
+.ELEMENTS.hasOwnProperty(tagName))throw new Error(UNKNOWN_TAGNAME+'['+tagName+']');flags=html4
+.ELEMENTS[tagName];if(flags&html4 .eflags.UNSAFE&&!(flags&html4 .eflags.SCRIPT))return ___.log(UNSAFE_TAGNAME+'['+tagName+']: no action performed'),null;newEl=this.doc___.createElement(tagName);if(elementPolicies.hasOwnProperty(tagName)){attribs=elementPolicies[tagName]([]);if(attribs)for(i=0;i<attribs.length;i+=2)bridal.setAttribute(newEl,attribs[i],attribs[i+1])}return defaultTameNode(newEl,true)},TameHTMLDocument.prototype.createTextNode=function(text){if(!this.editable___)throw new
+Error(NOT_EDITABLE);return defaultTameNode(this.doc___.createTextNode(text!==null&&text!==void
+0?''+text:''),true)},TameHTMLDocument.prototype.getElementById=function(id){var
+node;return id+=idSuffix,node=this.doc___.getElementById(id),defaultTameNode(node,this.editable___)},TameHTMLDocument.prototype.getForms___=function(){var
+tameForms=[],i,tameForm;for(i=0;i<this.doc___.forms.length;++i)tameForm=tameRelatedNode(this.doc___.forms.item(i),this.editable___,defaultTameNode),tameForm!==null&&tameForms.push(tameForm);return fakeNodeList(tameForms)},TameHTMLDocument.prototype.getCompatMode___=function(){return'CSS1Compat'},TameHTMLDocument.prototype.toString=function(){return'[Fake Document]'},TameHTMLDocument.prototype.createEvent=function(type){var
+document,rawEvent,tamedEvent;type=String(type);if(type!=='HTMLEvents')throw new Error('Unrecognized event type '+type);return document=this.doc___,document.createEvent?(rawEvent=document.createEvent(type)):(rawEvent=document.createEventObject(),rawEvent.eventType='ondataavailable'),tamedEvent=new
+TameCustomHTMLEvent(rawEvent),rawEvent.tamed___=tamedEvent,tamedEvent},TameHTMLDocument.prototype.getOwnerDocument___=function(){return null},TameHTMLDocument.prototype.signalLoaded___=function(){var
+onload=___.canRead(imports,'$v')&&___.canCallPub(imports.$v,'ros')&&imports.$v.ros('onload')||imports.window&&___.readPub(imports.window,'onload'),i,listeners,n;onload&&setTimeout(function(){___.callPub(onload,'call',[___.USELESS])},0),listeners=this.onLoadListeners___,this.onLoadListeners___=[];for(i=0,n=listeners.length;i<n;++i)(function(listener){setTimeout(function(){___.callPub(listener,'call',[___.USELESS])},0)})(listeners[i])},___.all2(___.grantTypedMethod,TameHTMLDocument.prototype,['addEventListener','removeEventListener','createComment','createDocumentFragment','createElement','createEvent','createTextNode','getElementById','getElementsByClassName','getElementsByTagName']),imports.handlers___=[],imports.tameNode___=defaultTameNode,imports.TameHTMLDocument___=TameHTMLDocument,imports.tameEvent___=tameEvent,imports.blessHtml___=blessHtml,imports.blessCss___=function(var_args){var
+arr=[],i,n;for(i=0,n=arguments.length;i<n;++i)arr[i]=arguments[i];return cssSealerUnsealerPair.seal(arr)},imports.htmlAttr___=function(s){return html.escapeAttrib(String(s||''))},imports.html___=safeHtml,imports.rewriteUri___=function(uri,mimeType){var
+s=rewriteAttribute(null,null,html4 .atype.URI,uri);if(!s)throw new Error;return s},imports.suffix___=function(nmtokens){var
+p=String(nmtokens).replace(/^\s+|\s+$/g,'').split(/\s+/g),out=[],i,nmtoken;for(i=0;i<p.length;++i){nmtoken=rewriteAttribute(null,null,html4
+.atype.ID,p[i]);if(!nmtoken)throw new Error(nmtokens);out.push(nmtoken)}return out.join(' ')},imports.ident___=function(nmtokens){var
+p=String(nmtokens).replace(/^\s+|\s+$/g,'').split(/\s+/g),out=[],i,nmtoken;for(i=0;i<p.length;++i){nmtoken=rewriteAttribute(null,null,html4
+.atype.CLASSES,p[i]);if(!nmtoken)throw new Error(nmtokens);out.push(nmtoken)}return out.join(' ')},allCssProperties=domitaModules.CssPropertiesCollection(css.properties,document.documentElement,css),historyInsensitiveCssProperties=domitaModules.CssPropertiesCollection(css.HISTORY_INSENSITIVE_STYLE_WHITELIST,document.documentElement,css);function
+TameStyle(style,editable,tameEl){this.style___=style,this.editable___=editable,this.tameEl___=tameEl}inertCtor(TameStyle,Object,'Style'),TameStyle.prototype.readByCanonicalName___=function(canonName){return String(this.style___[canonName]||'')},TameStyle.prototype.writeByCanonicalName___=function(canonName,val){this.style___[canonName]=val},TameStyle.prototype.allowProperty___=function(cssPropertyName){return allCssProperties.isCssProp(cssPropertyName)},TameStyle.prototype.handleRead___=function(stylePropertyName){var
+self=this,canonName,cssPropertyName;return String(stylePropertyName)==='getPropertyValue'?___.markFuncFreeze(function(args){return TameStyle.prototype.getPropertyValue.call(self,args)}):!this.style___||!allCssProperties.isCanonicalProp(stylePropertyName)?void
+0:(cssPropertyName=allCssProperties.getCssPropFromCanonical(stylePropertyName),this.allowProperty___(cssPropertyName)?(canonName=allCssProperties.getCanonicalPropFromCss(cssPropertyName),this.readByCanonicalName___(canonName)):void
+0)},TameStyle.prototype.handleCall___=function(name,args){if(String(name)==='getPropertyValue')return TameStyle.prototype.getPropertyValue.call(this,args);throw'Cannot handle method '+String(name)},TameStyle.prototype.getPropertyValue=function(cssPropertyName){var
+canonName;return cssPropertyName=String(cssPropertyName||'').toLowerCase(),this.allowProperty___(cssPropertyName)?(canonName=allCssProperties.getCanonicalPropFromCss(cssPropertyName),this.readByCanonicalName___(canonName)):''},TameStyle.prototype.handleSet___=function(stylePropertyName,value){var
+canonName,cssPropertyName,pattern,val;if(!this.editable___)throw new Error('style not editable');stylePropertyName=String(stylePropertyName);if(stylePropertyName==='cssText')return typeof
+this.style___.cssText==='string'?(this.style___.cssText=sanitizeStyleAttrValue(value)):this.tameEl___.setAttribute('style',value),value;if(!allCssProperties.isCanonicalProp(stylePropertyName))throw new
+Error('Unknown CSS property name '+stylePropertyName);cssPropertyName=allCssProperties.getCssPropFromCanonical(stylePropertyName);if(!this.allowProperty___(cssPropertyName))return;pattern=css.properties[cssPropertyName];if(!pattern)throw new
+Error('style not editable');val=''+(value||''),val=val.replace(/\burl\s*\(\s*\"([^\"]*)\"\s*\)/gi,function(_,url){var
+decodedUrl=decodeCssString(url),rewrittenUrl=uriCallback?uriCallback.rewrite(decodedUrl,'image/*'):null;return rewrittenUrl||(rewrittenUrl='about:blank'),'url(\"'+rewrittenUrl.replace(/[\"\'\{\}\(\):\\]/g,function(ch){return'\\'+ch.charCodeAt(0).toString(16)+' '})+'\")'});if(val&&!pattern.test(val+' '))throw new
+Error('bad value `'+val+'` for CSS property '+stylePropertyName);return canonName=allCssProperties.getCanonicalPropFromCss(cssPropertyName),this.writeByCanonicalName___(canonName,val),value},TameStyle.prototype.toString=function(){return'[Fake Style]'};function
+isNestedInAnchor(rawElement){for(;rawElement&&rawElement!=pseudoBodyNode;rawElement=rawElement.parentNode)if(rawElement.tagName.toLowerCase()==='a')return true;return false}function
+TameComputedStyle(rawElement,pseudoElement){TameStyle.call(this,bridal.getComputedStyle(rawElement,pseudoElement),false),this.rawElement___=rawElement,this.pseudoElement___=pseudoElement}___.extend(TameComputedStyle,TameStyle),TameComputedStyle.prototype.readByCanonicalName___=function(canonName){var
+canReturnDirectValue=historyInsensitiveCssProperties.isCanonicalProp(canonName)||!isNestedInAnchor(this.rawElement___);return canReturnDirectValue?TameStyle.prototype.readByCanonicalName___.call(this,canonName):new
+TameComputedStyle(pseudoBodyNode,this.pseudoElement___).readByCanonicalName___(canonName)},TameComputedStyle.prototype.writeByCanonicalName___=function(canonName){throw'Computed styles not editable: This code should be unreachable'},TameComputedStyle.prototype.toString=function(){return'[Fake Computed Style]'},nodeClasses.XMLHttpRequest=domitaModules.TameXMLHttpRequest(domitaModules.XMLHttpRequestCtor(window.XMLHttpRequest,window.ActiveXObject),uriCallback),imports.cssNumber___=function(num){if('number'===typeof
+num&&isFinite(num)&&!isNaN(num))return''+num;throw new Error(num)},imports.cssColor___=function(color){var
+hex;if('number'!==typeof color||color!=(color|0))throw new Error(color);return hex='0123456789abcdef','#'+hex.charAt(color>>20&15)+hex.charAt(color>>16&15)+hex.charAt(color>>12&15)+hex.charAt(color>>8&15)+hex.charAt(color>>4&15)+hex.charAt(color&15)},imports.cssUri___=function(uri,mimeType){var
+s=rewriteAttribute(null,null,html4 .atype.URI,uri);if(!s)throw new Error;return s},imports.emitCss___=function(cssText){this.getCssContainer___().appendChild(bridal.createStylesheet(document,cssText))},imports.getCssContainer___=function(){return(document.getElementsByTagName('head'))[0]};if(!/^-/.test(idSuffix))throw new
+Error('id suffix \"'+idSuffix+'\" must start with \"-\"');if(!/___$/.test(idSuffix))throw new
+Error('id suffix \"'+idSuffix+'\" must end with \"___\"');idClass=idSuffix.substring(1),idClassPattern=new
+RegExp('(?:^|\\s)'+idClass.replace(/[\.$]/g,'\\$&')+'(?:\\s|$)'),imports.getIdClass___=function(){return idClass},bridal.setAttribute(pseudoBodyNode,'class',bridal.getAttribute(pseudoBodyNode,'class')+' '+idClass+' vdoc-body___'),imports.domitaTrace___=0,imports.getDomitaTrace=___.markFuncFreeze(function(){return imports.domitaTrace___}),imports.setDomitaTrace=___.markFuncFreeze(function(x){imports.domitaTrace___=x}),imports.setTimeout=tameSetTimeout,imports.setInterval=tameSetInterval,imports.clearTimeout=tameClearTimeout,imports.clearInterval=tameClearInterval,tameDocument=new
+TameHTMLDocument(document,pseudoBodyNode,String(optPseudoWindowLocation.hostname||'nosuchhost.fake'),true),imports.document=tameDocument,tameLocation=___.primFreeze({'toString':___.markFuncFreeze(function(){return tameLocation.href}),'href':String(optPseudoWindowLocation.href||'http://nosuchhost.fake/'),'hash':String(optPseudoWindowLocation.hash||''),'host':String(optPseudoWindowLocation.host||'nosuchhost.fake'),'hostname':String(optPseudoWindowLocation.hostname||'nosuchhost.fake'),'pathname':String(optPseudoWindowLocation.pathname||'/'),'port':String(optPseudoWindowLocation.port||''),'protocol':String(optPseudoWindowLocation.protocol||'http:'),'search':String(optPseudoWindowLocation.search||'')}),tameNavigator=___.primFreeze({'appName':String(window.navigator.appName),'appVersion':String(window.navigator.appVersion),'platform':String(window.navigator.platform),'userAgent':String(window.navigator.userAgent),'cajaVersion':'1.0'}),PSEUDO_ELEMENT_WHITELIST={'first-letter':true,'first-line':true};function
+TameWindow(){this.properties___={}}function TameDefaultView(){this.document=tameDocument,assert(tameDocument.editable___),___.grantRead(this,'document')}___.forOwnKeys({'document':tameDocument,'location':tameLocation,'navigator':tameNavigator,'setTimeout':tameSetTimeout,'setInterval':tameSetInterval,'clearTimeout':tameClearTimeout,'clearInterval':tameClearInterval,'addEventListener':___.markFuncFreeze(function(name,listener,useCapture){name==='load'?(classUtils.ensureValidCallback(listener),tameDocument.onLoadListeners___.push(listener)):tameDocument.addEventListener(name,listener,useCapture)}),'removeEventListener':___.markFuncFreeze(function(name,listener,useCapture){var
+i,k,listeners,n;if(name==='load'){listeners=tameDocument.onLoadListeners___,k=0;for(i=0,n=listeners.length;i<n;++i)listeners[i-k]=listeners[i],listeners[i]===listener&&++k;listeners.length-=k}else
+tameDocument.removeEventListener(name,listener,useCapture)}),'dispatchEvent':___.markFuncFreeze(function(evt){})},___.markFuncFreeze(function(propertyName,value){TameWindow.prototype[propertyName]=value,___.grantRead(TameWindow.prototype,propertyName)})),___.forOwnKeys({'scrollBy':___.markFuncFreeze(function(dx,dy){(dx||dy)&&makeScrollable(tameDocument.body___),tameScrollBy(tameDocument.body___,dx,dy)}),'scrollTo':___.markFuncFreeze(function(x,y){makeScrollable(tameDocument.body___),tameScrollTo(tameDocument.body___,x,y)}),'resizeTo':___.markFuncFreeze(function(w,h){tameResizeTo(tameDocument.body___,w,h)}),'resizeBy':___.markFuncFreeze(function(dw,dh){tameResizeBy(tameDocument.body___,dw,dh)}),'getComputedStyle':___.markFuncFreeze(function(tameElement,pseudoElement){tameElement=TameNodeT.coerce(tameElement),pseudoElement=pseudoElement===null||pseudoElement===void
+0||''===pseudoElement?void 0:String(pseudoElement).toLowerCase();if(pseudoElement!==void
+0&&!PSEUDO_ELEMENT_WHITELIST.hasOwnProperty(pseudoElement))throw new Error('Bad pseudo element '+pseudoElement);return new
+TameComputedStyle(tameElement.node___,pseudoElement)})},___.markFuncFreeze(function(propertyName,value){TameWindow.prototype[propertyName]=value,___.grantRead(TameWindow.prototype,propertyName),TameDefaultView.prototype[propertyName]=value,___.grantRead(TameDefaultView.prototype,propertyName)})),TameWindow.prototype.handleRead___=function(name){var
+handlerName;return name=String(name),endsWith__.test(name)?void 0:(handlerName=name+'_getter___',this[handlerName]?this[handlerName]():___.hasOwnProp(this,name)?this[name]:void
+0)},TameWindow.prototype.handleSet___=function(name,val){var handlerName;name=String(name);if(endsWith__.test(name))throw new
+Error(INVALID_SUFFIX);return handlerName=name+'_setter___',this[handlerName]?this[handlerName](val):(this[name+'_canEnum___']=true,this[name+'_canRead___']=true,this[name]=val)},TameWindow.prototype.handleDelete___=function(name){var
+handlerName;name=String(name);if(endsWith__.test(name))throw new Error(INVALID_SUFFIX);return handlerName=name+'_deleter___',this[handlerName]?this[handlerName]():___.deleteFieldEntirely(this,name)},tameWindow=new
+TameWindow,tameDefaultView=new TameDefaultView(tameDocument.editable___);function
+propertyOnlyHasGetter(_){throw new TypeError('setting a property that only has a getter')}___.forOwnKeys({'clientLeft':{'get':function(){return tameDocument.body___.clientLeft}},'clientTop':{'get':function(){return tameDocument.body___.clientTop}},'clientHeight':{'get':function(){return tameDocument.body___.clientHeight}},'clientWidth':{'get':function(){return tameDocument.body___.clientWidth}},'offsetLeft':{'get':function(){return tameDocument.body___.offsetLeft}},'offsetTop':{'get':function(){return tameDocument.body___.offsetTop}},'offsetHeight':{'get':function(){return tameDocument.body___.offsetHeight}},'offsetWidth':{'get':function(){return tameDocument.body___.offsetWidth}},'pageXOffset':{'get':function(){return tameDocument.body___.scrollLeft}},'pageYOffset':{'get':function(){return tameDocument.body___.scrollTop}},'scrollLeft':{'get':function(){return tameDocument.body___.scrollLeft},'set':function(x){return tameDocument.body___.scrollLeft=+x,x}},'scrollTop':{'get':function(){return tameDocument.body___.scrollTop},'set':function(y){return tameDocument.body___.scrollTop=+y,y}},'scrollHeight':{'get':function(){return tameDocument.body___.scrollHeight}},'scrollWidth':{'get':function(){return tameDocument.body___.scrollWidth}}},___.markFuncFreeze(function(propertyName,def){var
+views=[tameWindow,tameDefaultView,tameDocument.getBody___(),tameDocument.getDocumentElement___()],setter=def.set||propertyOnlyHasGetter,getter=def.get,i,view;for(i=views.length;--i>=0;)view=views[i],___.useGetHandler(view,propertyName,getter),___.useSetHandler(view,propertyName,setter)})),___.forOwnKeys({'innerHeight':function(){return tameDocument.body___.clientHeight},'innerWidth':function(){return tameDocument.body___.clientWidth},'outerHeight':function(){return tameDocument.body___.clientHeight},'outerWidth':function(){return tameDocument.body___.clientWidth}},___.markFuncFreeze(function(propertyName,handler){___.useGetHandler(tameWindow,propertyName,handler),___.useGetHandler(tameDefaultView,propertyName,handler)})),windowProps=['top','self','opener','parent','window'],wpLen=windowProps.length;for(i=0;i<wpLen;++i)prop=windowProps[i],tameWindow[prop]=tameWindow,___.grantRead(tameWindow,prop);tameDocument.editable___&&(tameDocument.defaultView=tameDefaultView,___.grantRead(tameDocument,'defaultView'),tameDocument.sanitizeAttrs___=sanitizeAttrs),___.forOwnKeys(nodeClasses,___.markFuncFreeze(function(name,ctor){___.primFreeze(ctor),tameWindow[name]=ctor,___.grantRead(tameWindow,name)})),defaultNodeClasses=['HTMLAppletElement','HTMLAreaElement','HTMLBaseElement','HTMLBaseFontElement','HTMLBodyElement','HTMLBRElement','HTMLButtonElement','HTMLDirectoryElement','HTMLDivElement','HTMLDListElement','HTMLFieldSetElement','HTMLFontElement','HTMLFrameElement','HTMLFrameSetElement','HTMLHeadElement','HTMLHeadingElement','HTMLHRElement','HTMLHtmlElement','HTMLIFrameElement','HTMLIsIndexElement','HTMLLabelElement','HTMLLegendElement','HTMLLIElement','HTMLLinkElement','HTMLMapElement','HTMLMenuElement','HTMLMetaElement','HTMLModElement','HTMLObjectElement','HTMLOListElement','HTMLOptGroupElement','HTMLOptionElement','HTMLParagraphElement','HTMLParamElement','HTMLPreElement','HTMLQuoteElement','HTMLScriptElement','HTMLSelectElement','HTMLStyleElement','HTMLTableCaptionElement','HTMLTableCellElement','HTMLTableColElement','HTMLTableElement','HTMLTableRowElement','HTMLTableSectionElement','HTMLTextAreaElement','HTMLTitleElement','HTMLUListElement'],defaultNodeClassCtor=nodeClasses.Element;for(i=0;i<defaultNodeClasses.length;++i)tameWindow[defaultNodeClasses[i]]=defaultNodeClassCtor,___.grantRead(tameWindow,defaultNodeClasses[i]);outers=imports.outers,___.isJSONContainer(outers)?(___.forOwnKeys(outers,___.markFuncFreeze(function(k,v){k
+in tameWindow||(tameWindow[k]=v,___.grantRead(tameWindow,k))})),imports.outers=tameWindow):(imports.window=tameWindow)}return attachDocumentStub})();function
+plugin_dispatchEvent___(thisNode,event,pluginId,handler){var imports,node;return event=event||bridal.getWindow(thisNode).event,event.currentTarget||(event.currentTarget=thisNode),imports=___.getImports(pluginId),node=imports.tameNode___(thisNode,true),plugin_dispatchToHandler___(pluginId,handler,[node,imports.tameEvent___(event),node])}function
+plugin_dispatchToHandler___(pluginId,handler,args){var sig=(''+handler).match(/^function\b[^\)]*\)/),imports=___.getImports(pluginId),$v,fn,tameWin;imports.domitaTrace___&1&&___.log('Dispatch pluginId='+pluginId+', handler='+(sig?sig[0]:handler)+', args='+args);switch(typeof
+handler){case'number':handler=imports.handlers___[handler];break;case'string':fn=void
+0,tameWin=void 0,$v=___.readPub(imports,'$v'),$v&&(fn=___.callPub($v,'ros',[handler]),fn||(tameWin=___.callPub($v,'ros',['window']))),fn||(fn=___.readPub(imports,handler),fn||(tameWin||(tameWin=___.readPub(imports,'window')),tameWin&&(fn=___.readPub(tameWin,handler)))),handler=fn&&typeof
+fn.call==='function'?fn:void 0;break;case'function':case'object':break;default:throw new
+Error('Expected function as event handler, not '+typeof handler)}___.startCallerStack&&___.startCallerStack(),imports.isProcessingEvent___=true;try{return ___.callPub(handler,'call',args)}catch(ex){throw ex&&ex.cajitaStack___&&'undefined'!==typeof
+console&&console.error('Event dispatch %s: %s',handler,ex.cajitaStack___.join('\n')),ex}finally{imports.isProcessingEvent___=false}}}
\ No newline at end of file
diff --git a/trunk/php/external/resources/com/google/caja/plugin/html-sanitizer-minified.js b/trunk/php/external/resources/com/google/caja/plugin/html-sanitizer-minified.js
new file mode 100644
index 0000000..40d0d6f
--- /dev/null
+++ b/trunk/php/external/resources/com/google/caja/plugin/html-sanitizer-minified.js
@@ -0,0 +1,45 @@
+{var css={'properties':(function(){var s=['|left|center|right','|top|center|bottom','#(?:[\\da-f]{3}){1,2}|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow|rgb\\(\\s*(?:-?\\d+|0|[+\\-]?\\d+(?:\\.\\d+)?%)\\s*,\\s*(?:-?\\d+|0|[+\\-]?\\d+(?:\\.\\d+)?%)\\s*,\\s*(?:-?\\d+|0|[+\\-]?\\d+(?:\\.\\d+)?%)\\)','[+\\-]?\\d+(?:\\.\\d+)?(?:[cem]m|ex|in|p[ctx])','\\d+(?:\\.\\d+)?(?:[cem]m|ex|in|p[ctx])','none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset','[+\\-]?\\d+(?:\\.\\d+)?%','\\d+(?:\\.\\d+)?%','url\\(\"[^()\\\\\"\\r\\n]+\"\\)','repeat-x|repeat-y|(?:repeat|space|round|no-repeat)(?:\\s+(?:repeat|space|round|no-repeat)){0,2}'],c=[RegExp('^\\s*(?:\\s*(?:0|'+s[3]+'|'+s[6]+')){1,2}\\s*$','i'),RegExp('^\\s*(?:\\s*(?:0|'+s[3]+'|'+s[6]+')){1,4}(?:\\s*\\/(?:\\s*(?:0|'+s[3]+'|'+s[6]+')){1,4})?\\s*$','i'),RegExp('^\\s*(?:\\s*none|(?:(?:\\s*(?:'+s[2]+')\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*inset)?|(?:\\s*inset)?\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*(?:'+s[2]+'))?)\\s*,)*(?:\\s*(?:'+s[2]+')\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*inset)?|(?:\\s*inset)?\\s+(?:0|'+s[3]+')(?:\\s*(?:0|'+s[3]+')){1,4}(?:\\s*(?:'+s[2]+'))?))\\s*$','i'),RegExp('^\\s*(?:'+s[2]+'|transparent|inherit)\\s*$','i'),RegExp('^\\s*(?:'+s[5]+'|inherit)\\s*$','i'),RegExp('^\\s*(?:thin|medium|thick|0|'+s[3]+'|inherit)\\s*$','i'),RegExp('^\\s*(?:(?:thin|medium|thick|0|'+s[3]+'|'+s[5]+'|'+s[2]+'|transparent|inherit)(?:\\s+(?:thin|medium|thick|0|'+s[3]+')|\\s+(?:'+s[5]+')|\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent|\\s+inherit){0,2}|inherit)\\s*$','i'),/^\s*(?:none|inherit)\s*$/i,RegExp('^\\s*(?:'+s[8]+'|none|inherit)\\s*$','i'),RegExp('^\\s*(?:0|'+s[3]+'|'+s[6]+'|auto|inherit)\\s*$','i'),RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|none|inherit|auto)\\s*$','i'),RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|inherit|auto)\\s*$','i'),/^\s*(?:0(?:\.\d+)?|\.\d+|1(?:\.0+)?|inherit)\s*$/i,RegExp('^\\s*(?:(?:'+s[2]+'|invert|inherit|'+s[5]+'|thin|medium|thick|0|'+s[3]+')(?:\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+invert|\\s+inherit|\\s+(?:'+s[5]+'|inherit)|\\s+(?:thin|medium|thick|0|'+s[3]+'|inherit)){0,2}|inherit)\\s*$','i'),RegExp('^\\s*(?:'+s[2]+'|invert|inherit)\\s*$','i'),/^\s*(?:visible|hidden|scroll|auto|no-display|no-content)\s*$/i,RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|inherit)\\s*$','i'),/^\s*(?:auto|always|avoid|left|right|inherit)\s*$/i,RegExp('^\\s*(?:0|[+\\-]?\\d+(?:\\.\\d+)?m?s|'+s[6]+'|inherit)\\s*$','i'),/^\s*(?:0|[+\-]?\d+(?:\.\d+)?|inherit)\s*$/i,/^\s*(?:clip|ellipsis)\s*$/i,RegExp('^\\s*(?:normal|0|'+s[3]+'|inherit)\\s*$','i')];return{'-moz-border-radius':c[1],'-moz-border-radius-bottomleft':c[0],'-moz-border-radius-bottomright':c[0],'-moz-border-radius-topleft':c[0],'-moz-border-radius-topright':c[0],'-moz-box-shadow':c[2],'-moz-opacity':c[12],'-moz-outline':c[13],'-moz-outline-color':c[14],'-moz-outline-style':c[4],'-moz-outline-width':c[5],'-o-text-overflow':c[20],'-webkit-border-bottom-left-radius':c[0],'-webkit-border-bottom-right-radius':c[0],'-webkit-border-radius':c[1],'-webkit-border-radius-bottom-left':c[0],'-webkit-border-radius-bottom-right':c[0],'-webkit-border-radius-top-left':c[0],'-webkit-border-radius-top-right':c[0],'-webkit-border-top-left-radius':c[0],'-webkit-border-top-right-radius':c[0],'-webkit-box-shadow':c[2],'azimuth':/^\s*(?:0|[+\-]?\d+(?:\.\d+)?(?:g?rad|deg)|(?:left-side|far-left|left|center-left|center|center-right|right|far-right|right-side|behind)(?:\s+(?:left-side|far-left|left|center-left|center|center-right|right|far-right|right-side|behind))?|leftwards|rightwards|inherit)\s*$/i,'background':RegExp('^\\s*(?:\\s*(?:'+s[8]+'|none|(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?)(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|'+s[9]+'|scroll|fixed|local|(?:border|padding|content)-box)(?:\\s*'+s[8]+'|\\s+none|(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)){1,2})(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|\\s+repeat-x|\\s+repeat-y|(?:\\s+(?:repeat|space|round|no-repeat)){1,2}|\\s+(?:scroll|fixed|local)|\\s+(?:border|padding|content)-box){0,4}\\s*,)*\\s*(?:'+s[2]+'|transparent|inherit|'+s[8]+'|none|(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?)(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|'+s[9]+'|scroll|fixed|local|(?:border|padding|content)-box)(?:\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent|\\s+inherit|\\s*'+s[8]+'|\\s+none|(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)){1,2})(?:\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain))?|\\s*\\/\\s*(?:(?:0|'+s[4]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[4]+'|'+s[6]+'|auto)){0,2}|cover|contain)|\\s+repeat-x|\\s+repeat-y|(?:\\s+(?:repeat|space|round|no-repeat)){1,2}|\\s+(?:scroll|fixed|local)|\\s+(?:border|padding|content)-box){0,5}\\s*$','i'),'background-attachment':/^\s*(?:scroll|fixed|local)(?:\s*,\s*(?:scroll|fixed|local))*\s*$/i,'background-color':c[3],'background-image':RegExp('^\\s*(?:'+s[8]+'|none)(?:\\s*,\\s*(?:'+s[8]+'|none))*\\s*$','i'),'background-position':RegExp('^\\s*(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?)(?:\\s*,\\s*(?:(?:0|'+s[6]+'|'+s[3]+s[0]+')(?:\\s+(?:0|'+s[6]+'|'+s[3]+s[1]+'))?|(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?)(?:\\s+(?:center|(?:lef|righ)t(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?|(?:top|bottom)(?:\\s+(?:0|'+s[6]+'|'+s[3]+'))?))?))*\\s*$','i'),'background-repeat':RegExp('^\\s*(?:'+s[9]+')(?:\\s*,\\s*(?:'+s[9]+'))*\\s*$','i'),'border':RegExp('^\\s*(?:(?:thin|medium|thick|0|'+s[3]+'|'+s[5]+'|'+s[2]+'|transparent)(?:\\s+(?:thin|medium|thick|0|'+s[3]+')|\\s+(?:'+s[5]+')|\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent){0,2}|inherit)\\s*$','i'),'border-bottom':c[6],'border-bottom-color':c[3],'border-bottom-left-radius':c[0],'border-bottom-right-radius':c[0],'border-bottom-style':c[4],'border-bottom-width':c[5],'border-collapse':/^\s*(?:collapse|separate|inherit)\s*$/i,'border-color':RegExp('^\\s*(?:(?:'+s[2]+'|transparent)(?:\\s*#(?:[\\da-f]{3}){1,2}|\\s+aqua|\\s+black|\\s+blue|\\s+fuchsia|\\s+gray|\\s+green|\\s+lime|\\s+maroon|\\s+navy|\\s+olive|\\s+orange|\\s+purple|\\s+red|\\s+silver|\\s+teal|\\s+white|\\s+yellow|\\s+rgb\\(\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\s*,\\s*(?:-?\\d+|0|'+s[6]+')\\)|\\s+transparent){0,4}|inherit)\\s*$','i'),'border-left':c[6],'border-left-color':c[3],'border-left-style':c[4],'border-left-width':c[5],'border-radius':c[1],'border-right':c[6],'border-right-color':c[3],'border-right-style':c[4],'border-right-width':c[5],'border-spacing':RegExp('^\\s*(?:(?:\\s*(?:0|'+s[3]+')){1,2}|\\s*inherit)\\s*$','i'),'border-style':RegExp('^\\s*(?:(?:'+s[5]+')(?:\\s+(?:'+s[5]+')){0,4}|inherit)\\s*$','i'),'border-top':c[6],'border-top-color':c[3],'border-top-left-radius':c[0],'border-top-right-radius':c[0],'border-top-style':c[4],'border-top-width':c[5],'border-width':RegExp('^\\s*(?:(?:thin|medium|thick|0|'+s[3]+')(?:\\s+(?:thin|medium|thick|0|'+s[3]+')){0,4}|inherit)\\s*$','i'),'bottom':c[9],'box-shadow':c[2],'caption-side':/^\s*(?:top|bottom|inherit)\s*$/i,'clear':/^\s*(?:none|left|right|both|inherit)\s*$/i,'clip':RegExp('^\\s*(?:rect\\(\\s*(?:0|'+s[3]+'|auto)\\s*,\\s*(?:0|'+s[3]+'|auto)\\s*,\\s*(?:0|'+s[3]+'|auto)\\s*,\\s*(?:0|'+s[3]+'|auto)\\)|auto|inherit)\\s*$','i'),'color':RegExp('^\\s*(?:'+s[2]+'|inherit)\\s*$','i'),'counter-increment':c[7],'counter-reset':c[7],'cue':RegExp('^\\s*(?:(?:'+s[8]+'|none|inherit)(?:\\s*'+s[8]+'|\\s+none|\\s+inherit)?|inherit)\\s*$','i'),'cue-after':c[8],'cue-before':c[8],'cursor':RegExp('^\\s*(?:(?:\\s*'+s[8]+'\\s*,)*\\s*(?:auto|crosshair|default|pointer|move|e-resize|ne-resize|nw-resize|n-resize|se-resize|sw-resize|s-resize|w-resize|text|wait|help|progress|all-scroll|col-resize|hand|no-drop|not-allowed|row-resize|vertical-text)|\\s*inherit)\\s*$','i'),'direction':/^\s*(?:ltr|rtl|inherit)\s*$/i,'display':/^\s*(?:inline|block|list-item|run-in|inline-block|table|inline-table|table-row-group|table-header-group|table-footer-group|table-row|table-column-group|table-column|table-cell|table-caption|none|inherit|-moz-inline-box|-moz-inline-stack)\s*$/i,'elevation':/^\s*(?:0|[+\-]?\d+(?:\.\d+)?(?:g?rad|deg)|below|level|above|higher|lower|inherit)\s*$/i,'empty-cells':/^\s*(?:show|hide|inherit)\s*$/i,'filter':RegExp('^\\s*(?:\\s*alpha\\(\\s*opacity\\s*=\\s*(?:0|'+s[6]+'|[+\\-]?\\d+(?:\\.\\d+)?)\\))+\\s*$','i'),'float':/^\s*(?:left|right|none|inherit)\s*$/i,'font':RegExp('^\\s*(?:(?:normal|italic|oblique|inherit|small-caps|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)(?:\\s+(?:normal|italic|oblique|inherit|small-caps|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)){0,2}\\s+(?:xx-small|x-small|small|medium|large|x-large|xx-large|(?:small|larg)er|0|'+s[4]+'|'+s[7]+'|inherit)(?:\\s*\\/\\s*(?:normal|0|\\d+(?:\\.\\d+)?|'+s[4]+'|'+s[7]+'|inherit))?(?:(?:\\s*\"\\w(?:[\\w-]*\\w)(?:\\s+\\w([\\w-]*\\w))*\"|\\s+(?:serif|sans-serif|cursive|fantasy|monospace))(?:\\s*,\\s*(?:\"\\w(?:[\\w-]*\\w)(?:\\s+\\w([\\w-]*\\w))*\"|serif|sans-serif|cursive|fantasy|monospace))*|\\s+inherit)|caption|icon|menu|message-box|small-caption|status-bar|inherit)\\s*$','i'),'font-family':/^\s*(?:(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|serif|sans-serif|cursive|fantasy|monospace)(?:\s*,\s*(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|serif|sans-serif|cursive|fantasy|monospace))*|inherit)\s*$/i,'font-size':RegExp('^\\s*(?:xx-small|x-small|small|medium|large|x-large|xx-large|(?:small|larg)er|0|'+s[4]+'|'+s[7]+'|inherit)\\s*$','i'),'font-stretch':/^\s*(?:normal|wider|narrower|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded)\s*$/i,'font-style':/^\s*(?:normal|italic|oblique|inherit)\s*$/i,'font-variant':/^\s*(?:normal|small-caps|inherit)\s*$/i,'font-weight':/^\s*(?:normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900|inherit)\s*$/i,'height':c[9],'left':c[9],'letter-spacing':c[21],'line-height':RegExp('^\\s*(?:normal|0|\\d+(?:\\.\\d+)?|'+s[4]+'|'+s[7]+'|inherit)\\s*$','i'),'list-style':RegExp('^\\s*(?:(?:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-latin|upper-latin|armenian|georgian|lower-alpha|upper-alpha|none|inherit|inside|outside|'+s[8]+')(?:\\s+(?:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-latin|upper-latin|armenian|georgian|lower-alpha|upper-alpha|none|inherit)|\\s+(?:inside|outside|inherit)|\\s*'+s[8]+'|\\s+none|\\s+inherit){0,2}|inherit)\\s*$','i'),'list-style-image':c[8],'list-style-position':/^\s*(?:inside|outside|inherit)\s*$/i,'list-style-type':/^\s*(?:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-latin|upper-latin|armenian|georgian|lower-alpha|upper-alpha|none|inherit)\s*$/i,'margin':RegExp('^\\s*(?:(?:0|'+s[3]+'|'+s[6]+'|auto)(?:\\s+(?:0|'+s[3]+'|'+s[6]+'|auto)){0,4}|inherit)\\s*$','i'),'margin-bottom':c[9],'margin-left':c[9],'margin-right':c[9],'margin-top':c[9],'max-height':c[10],'max-width':c[10],'min-height':c[11],'min-width':c[11],'opacity':c[12],'outline':c[13],'outline-color':c[14],'outline-style':c[4],'outline-width':c[5],'overflow':/^\s*(?:visible|hidden|scroll|auto|inherit)\s*$/i,'overflow-x':c[15],'overflow-y':c[15],'padding':RegExp('^\\s*(?:(?:\\s*(?:0|'+s[4]+'|'+s[7]+')){1,4}|\\s*inherit)\\s*$','i'),'padding-bottom':c[16],'padding-left':c[16],'padding-right':c[16],'padding-top':c[16],'page-break-after':c[17],'page-break-before':c[17],'page-break-inside':/^\s*(?:avoid|auto|inherit)\s*$/i,'pause':RegExp('^\\s*(?:(?:\\s*(?:0|[+\\-]?\\d+(?:\\.\\d+)?m?s|'+s[6]+')){1,2}|\\s*inherit)\\s*$','i'),'pause-after':c[18],'pause-before':c[18],'pitch':/^\s*(?:0|\d+(?:\.\d+)?k?Hz|x-low|low|medium|high|x-high|inherit)\s*$/i,'pitch-range':c[19],'play-during':RegExp('^\\s*(?:'+s[8]+'\\s*(?:mix|repeat)(?:\\s+(?:mix|repeat))?|auto|none|inherit)\\s*$','i'),'position':/^\s*(?:static|relative|absolute|inherit)\s*$/i,'quotes':c[7],'richness':c[19],'right':c[9],'speak':/^\s*(?:normal|none|spell-out|inherit)\s*$/i,'speak-header':/^\s*(?:once|always|inherit)\s*$/i,'speak-numeral':/^\s*(?:digits|continuous|inherit)\s*$/i,'speak-punctuation':/^\s*(?:code|none|inherit)\s*$/i,'speech-rate':/^\s*(?:0|[+\-]?\d+(?:\.\d+)?|x-slow|slow|medium|fast|x-fast|faster|slower|inherit)\s*$/i,'stress':c[19],'table-layout':/^\s*(?:auto|fixed|inherit)\s*$/i,'text-align':/^\s*(?:left|right|center|justify|inherit)\s*$/i,'text-decoration':/^\s*(?:none|(?:underline|overline|line-through|blink)(?:\s+(?:underline|overline|line-through|blink)){0,3}|inherit)\s*$/i,'text-indent':RegExp('^\\s*(?:0|'+s[3]+'|'+s[6]+'|inherit)\\s*$','i'),'text-overflow':c[20],'text-shadow':c[2],'text-transform':/^\s*(?:capitalize|uppercase|lowercase|none|inherit)\s*$/i,'text-wrap':/^\s*(?:normal|unrestricted|none|suppress)\s*$/i,'top':c[9],'unicode-bidi':/^\s*(?:normal|embed|bidi-override|inherit)\s*$/i,'vertical-align':RegExp('^\\s*(?:baseline|sub|super|top|text-top|middle|bottom|text-bottom|0|'+s[6]+'|'+s[3]+'|inherit)\\s*$','i'),'visibility':/^\s*(?:visible|hidden|collapse|inherit)\s*$/i,'voice-family':/^\s*(?:(?:\s*(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|male|female|child)\s*,)*\s*(?:"\w(?:[\w-]*\w)(?:\s+\w([\w-]*\w))*"|male|female|child)|\s*inherit)\s*$/i,'volume':RegExp('^\\s*(?:0|\\d+(?:\\.\\d+)?|'+s[7]+'|silent|x-soft|soft|medium|loud|x-loud|inherit)\\s*$','i'),'white-space':/^\s*(?:normal|pre|nowrap|pre-wrap|pre-line|inherit|-o-pre-wrap|-moz-pre-wrap|-pre-wrap)\s*$/i,'width':RegExp('^\\s*(?:0|'+s[4]+'|'+s[7]+'|auto|inherit)\\s*$','i'),'word-spacing':c[21],'word-wrap':/^\s*(?:normal|break-word)\s*$/i,'z-index':/^\s*(?:auto|-?\d+|inherit)\s*$/i,'zoom':RegExp('^\\s*(?:normal|0|\\d+(?:\\.\\d+)?|'+s[7]+')\\s*$','i')}})(),'alternates':{'MozBoxShadow':['boxShadow'],'WebkitBoxShadow':['boxShadow'],'float':['cssFloat','styleFloat']},'HISTORY_INSENSITIVE_STYLE_WHITELIST':{'display':true,'filter':true,'float':true,'height':true,'left':true,'opacity':true,'overflow':true,'position':true,'right':true,'top':true,'visibility':true,'width':true,'padding-left':true,'padding-right':true,'padding-top':true,'padding-bottom':true}},html,html4;html4={},html4
+.atype={'NONE':0,'URI':1,'URI_FRAGMENT':11,'SCRIPT':2,'STYLE':3,'ID':4,'IDREF':5,'IDREFS':6,'GLOBAL_NAME':7,'LOCAL_NAME':8,'CLASSES':9,'FRAME_TARGET':10},html4
+.ATTRIBS={'*::class':9,'*::dir':0,'*::id':4,'*::lang':0,'*::onclick':2,'*::ondblclick':2,'*::onkeydown':2,'*::onkeypress':2,'*::onkeyup':2,'*::onload':2,'*::onmousedown':2,'*::onmousemove':2,'*::onmouseout':2,'*::onmouseover':2,'*::onmouseup':2,'*::style':3,'*::title':0,'a::accesskey':0,'a::coords':0,'a::href':1,'a::hreflang':0,'a::name':7,'a::onblur':2,'a::onfocus':2,'a::rel':0,'a::rev':0,'a::shape':0,'a::tabindex':0,'a::target':10,'a::type':0,'area::accesskey':0,'area::alt':0,'area::coords':0,'area::href':1,'area::nohref':0,'area::onblur':2,'area::onfocus':2,'area::shape':0,'area::tabindex':0,'area::target':10,'bdo::dir':0,'blockquote::cite':1,'br::clear':0,'button::accesskey':0,'button::disabled':0,'button::name':8,'button::onblur':2,'button::onfocus':2,'button::tabindex':0,'button::type':0,'button::value':0,'caption::align':0,'col::align':0,'col::char':0,'col::charoff':0,'col::span':0,'col::valign':0,'col::width':0,'colgroup::align':0,'colgroup::char':0,'colgroup::charoff':0,'colgroup::span':0,'colgroup::valign':0,'colgroup::width':0,'del::cite':1,'del::datetime':0,'dir::compact':0,'div::align':0,'dl::compact':0,'font::color':0,'font::face':0,'font::size':0,'form::accept':0,'form::action':1,'form::autocomplete':0,'form::enctype':0,'form::method':0,'form::name':7,'form::onreset':2,'form::onsubmit':2,'form::target':10,'h1::align':0,'h2::align':0,'h3::align':0,'h4::align':0,'h5::align':0,'h6::align':0,'hr::align':0,'hr::noshade':0,'hr::size':0,'hr::width':0,'iframe::align':0,'iframe::frameborder':0,'iframe::height':0,'iframe::marginheight':0,'iframe::marginwidth':0,'iframe::width':0,'img::align':0,'img::alt':0,'img::border':0,'img::height':0,'img::hspace':0,'img::ismap':0,'img::name':7,'img::src':1,'img::usemap':11,'img::vspace':0,'img::width':0,'input::accept':0,'input::accesskey':0,'input::align':0,'input::alt':0,'input::autocomplete':0,'input::checked':0,'input::disabled':0,'input::ismap':0,'input::maxlength':0,'input::name':8,'input::onblur':2,'input::onchange':2,'input::onfocus':2,'input::onselect':2,'input::readonly':0,'input::size':0,'input::src':1,'input::tabindex':0,'input::type':0,'input::usemap':11,'input::value':0,'ins::cite':1,'ins::datetime':0,'label::accesskey':0,'label::for':5,'label::onblur':2,'label::onfocus':2,'legend::accesskey':0,'legend::align':0,'li::type':0,'li::value':0,'map::name':7,'menu::compact':0,'ol::compact':0,'ol::start':0,'ol::type':0,'optgroup::disabled':0,'optgroup::label':0,'option::disabled':0,'option::label':0,'option::selected':0,'option::value':0,'p::align':0,'pre::width':0,'q::cite':1,'select::disabled':0,'select::multiple':0,'select::name':8,'select::onblur':2,'select::onchange':2,'select::onfocus':2,'select::size':0,'select::tabindex':0,'table::align':0,'table::bgcolor':0,'table::border':0,'table::cellpadding':0,'table::cellspacing':0,'table::frame':0,'table::rules':0,'table::summary':0,'table::width':0,'tbody::align':0,'tbody::char':0,'tbody::charoff':0,'tbody::valign':0,'td::abbr':0,'td::align':0,'td::axis':0,'td::bgcolor':0,'td::char':0,'td::charoff':0,'td::colspan':0,'td::headers':6,'td::height':0,'td::nowrap':0,'td::rowspan':0,'td::scope':0,'td::valign':0,'td::width':0,'textarea::accesskey':0,'textarea::cols':0,'textarea::disabled':0,'textarea::name':8,'textarea::onblur':2,'textarea::onchange':2,'textarea::onfocus':2,'textarea::onselect':2,'textarea::readonly':0,'textarea::rows':0,'textarea::tabindex':0,'tfoot::align':0,'tfoot::char':0,'tfoot::charoff':0,'tfoot::valign':0,'th::abbr':0,'th::align':0,'th::axis':0,'th::bgcolor':0,'th::char':0,'th::charoff':0,'th::colspan':0,'th::headers':6,'th::height':0,'th::nowrap':0,'th::rowspan':0,'th::scope':0,'th::valign':0,'th::width':0,'thead::align':0,'thead::char':0,'thead::charoff':0,'thead::valign':0,'tr::align':0,'tr::bgcolor':0,'tr::char':0,'tr::charoff':0,'tr::valign':0,'ul::compact':0,'ul::type':0},html4
+.eflags={'OPTIONAL_ENDTAG':1,'EMPTY':2,'CDATA':4,'RCDATA':8,'UNSAFE':16,'FOLDABLE':32,'SCRIPT':64,'STYLE':128},html4
+.ELEMENTS={'a':0,'abbr':0,'acronym':0,'address':0,'applet':16,'area':2,'b':0,'base':18,'basefont':18,'bdo':0,'big':0,'blockquote':0,'body':49,'br':2,'button':0,'caption':0,'center':0,'cite':0,'code':0,'col':2,'colgroup':1,'dd':1,'del':0,'dfn':0,'dir':0,'div':0,'dl':0,'dt':1,'em':0,'fieldset':0,'font':0,'form':0,'frame':18,'frameset':16,'h1':0,'h2':0,'h3':0,'h4':0,'h5':0,'h6':0,'head':49,'hr':2,'html':49,'i':0,'iframe':4,'img':2,'input':2,'ins':0,'isindex':18,'kbd':0,'label':0,'legend':0,'li':1,'link':18,'map':0,'menu':0,'meta':18,'noframes':20,'noscript':20,'object':16,'ol':0,'optgroup':0,'option':1,'p':1,'param':18,'pre':0,'q':0,'s':0,'samp':0,'script':84,'select':0,'small':0,'span':0,'strike':0,'strong':0,'style':148,'sub':0,'sup':0,'table':0,'tbody':1,'td':1,'textarea':8,'tfoot':1,'th':1,'thead':1,'title':24,'tr':1,'tt':0,'u':0,'ul':0,'var':0},html=(function(){var
+ENTITIES,INSIDE_TAG_TOKEN,OUTSIDE_TAG_TOKEN,ampRe,decimalEscapeRe,entityRe,eqRe,gtRe,hexEscapeRe,lcase,looseAmpRe,ltRe,nulRe,quotRe;'script'==='SCRIPT'.toLowerCase()?(lcase=function(s){return s.toLowerCase()}):(lcase=function(s){return s.replace(/[A-Z]/g,function(ch){return String.fromCharCode(ch.charCodeAt(0)|32)})}),ENTITIES={'lt':'<','gt':'>','amp':'&','nbsp':'\xa0','quot':'\"','apos':'\''},decimalEscapeRe=/^#(\d+)$/,hexEscapeRe=/^#x([0-9A-Fa-f]+)$/;function
+lookupEntity(name){var m;return name=lcase(name),ENTITIES.hasOwnProperty(name)?ENTITIES[name]:(m=name.match(decimalEscapeRe),m?String.fromCharCode(parseInt(m[1],10)):(m=name.match(hexEscapeRe))?String.fromCharCode(parseInt(m[1],16)):'')}function
+decodeOneEntity(_,name){return lookupEntity(name)}nulRe=/\0/g;function stripNULs(s){return s.replace(nulRe,'')}entityRe=/&(#\d+|#x[0-9A-Fa-f]+|\w+);/g;function
+unescapeEntities(s){return s.replace(entityRe,decodeOneEntity)}ampRe=/&/g,looseAmpRe=/&([^a-z#]|#(?:[^0-9x]|x(?:[^0-9a-f]|$)|$)|$)/gi,ltRe=/</g,gtRe=/>/g,quotRe=/\"/g,eqRe=/\=/g;function
+escapeAttrib(s){return s.replace(ampRe,'&amp;').replace(ltRe,'&lt;').replace(gtRe,'&gt;').replace(quotRe,'&#34;').replace(eqRe,'&#61;')}function
+normalizeRCData(rcdata){return rcdata.replace(looseAmpRe,'&amp;$1').replace(ltRe,'&lt;').replace(gtRe,'&gt;')}INSIDE_TAG_TOKEN=new
+RegExp('^\\s*(?:(?:([a-z][a-z-]*)(\\s*=\\s*(\"[^\"]*\"|\'[^\']*\'|(?=[a-z][a-z-]*\\s*=)|[^>\"\'\\s]*))?)|(/?>)|.[^a-z\\s>]*)','i'),OUTSIDE_TAG_TOKEN=new
+RegExp('^(?:&(\\#[0-9]+|\\#[x][0-9a-f]+|\\w+);|<!--[\\s\\S]*?-->|<!\\w[^>]*>|<\\?[^>*]*>|<(/)?([a-z][a-z0-9]*)|([^<&>]+)|([<&>]))','i');function
+makeSaxParser(handler){return function parse(htmlText,param){var attribName,attribs,dataEnd,decodedValue,eflags,encodedValue,htmlLower,inTag,m,openTag,tagName;htmlText=String(htmlText),htmlLower=null,inTag=false,attribs=[],tagName=void
+0,eflags=void 0,openTag=void 0,handler.startDoc&&handler.startDoc(param);while(htmlText){m=htmlText.match(inTag?INSIDE_TAG_TOKEN:OUTSIDE_TAG_TOKEN),htmlText=htmlText.substring(m[0].length);if(inTag){if(m[1]){attribName=lcase(m[1]);if(m[2]){encodedValue=m[3];switch(encodedValue.charCodeAt(0)){case
+34:case 39:encodedValue=encodedValue.substring(1,encodedValue.length-1)}decodedValue=unescapeEntities(stripNULs(encodedValue))}else
+decodedValue=attribName;attribs.push(attribName,decodedValue)}else if(m[4])eflags!==void
+0&&(openTag?handler.startTag&&handler.startTag(tagName,attribs,param):handler.endTag&&handler.endTag(tagName,param)),openTag&&eflags&(html4
+.eflags.CDATA|html4 .eflags.RCDATA)&&(htmlLower===null?(htmlLower=lcase(htmlText)):(htmlLower=htmlLower.substring(htmlLower.length-htmlText.length)),dataEnd=htmlLower.indexOf('</'+tagName),dataEnd<0&&(dataEnd=htmlText.length),eflags&html4
+.eflags.CDATA?handler.cdata&&handler.cdata(htmlText.substring(0,dataEnd),param):handler.rcdata&&handler.rcdata(normalizeRCData(htmlText.substring(0,dataEnd)),param),htmlText=htmlText.substring(dataEnd)),tagName=eflags=openTag=void
+0,attribs.length=0,inTag=false}else if(m[1])handler.pcdata&&handler.pcdata(m[0],param);else
+if(m[3])openTag=!m[2],inTag=true,tagName=lcase(m[3]),eflags=html4 .ELEMENTS.hasOwnProperty(tagName)?html4
+.ELEMENTS[tagName]:void 0;else if(m[4])handler.pcdata&&handler.pcdata(m[4],param);else
+if(m[5]){if(handler.pcdata)switch(m[5]){case'<':handler.pcdata('&lt;',param);break;case'>':handler.pcdata('&gt;',param);break;default:handler.pcdata('&amp;',param)}}}handler.endDoc&&handler.endDoc(param)}}return{'normalizeRCData':normalizeRCData,'escapeAttrib':escapeAttrib,'unescapeEntities':unescapeEntities,'makeSaxParser':makeSaxParser}})(),html.makeHtmlSanitizer=function(sanitizeAttributes){var
+ignoring,stack;return html.makeSaxParser({'startDoc':function(_){stack=[],ignoring=false},'startTag':function(tagName,attribs,out){var
+attribName,eflags,i,n,value;if(ignoring)return;if(!html4 .ELEMENTS.hasOwnProperty(tagName))return;eflags=html4
+.ELEMENTS[tagName];if(eflags&html4 .eflags.FOLDABLE)return;else if(eflags&html4 .eflags.UNSAFE)return ignoring=!(eflags&html4
+.eflags.EMPTY),void 0;attribs=sanitizeAttributes(tagName,attribs);if(attribs){eflags&html4
+.eflags.EMPTY||stack.push(tagName),out.push('<',tagName);for(i=0,n=attribs.length;i<n;i+=2)attribName=attribs[i],value=attribs[i+1],value!==null&&value!==void
+0&&out.push(' ',attribName,'=\"',html.escapeAttrib(value),'\"');out.push('>')}},'endTag':function(tagName,out){var
+eflags,i,index,stackEl;if(ignoring)return ignoring=false,void 0;if(!html4 .ELEMENTS.hasOwnProperty(tagName))return;eflags=html4
+.ELEMENTS[tagName];if(!(eflags&(html4 .eflags.UNSAFE|html4 .eflags.EMPTY|html4 .eflags.FOLDABLE))){if(eflags&html4
+.eflags.OPTIONAL_ENDTAG)for(index=stack.length;--index>=0;){stackEl=stack[index];if(stackEl===tagName)break;if(!(html4
+.ELEMENTS[stackEl]&html4 .eflags.OPTIONAL_ENDTAG))return}else for(index=stack.length;--index>=0;)if(stack[index]===tagName)break;if(index<0)return;for(i=stack.length;--i>index;)stackEl=stack[i],html4
+.ELEMENTS[stackEl]&html4 .eflags.OPTIONAL_ENDTAG||out.push('</',stackEl,'>');stack.length=index,out.push('</',tagName,'>')}},'pcdata':function(text,out){ignoring||out.push(text)},'rcdata':function(text,out){ignoring||out.push(text)},'cdata':function(text,out){ignoring||out.push(text)},'endDoc':function(out){var
+i;for(i=stack.length;--i>=0;)out.push('</',stack[i],'>');stack.length=0}})};function
+html_sanitize(htmlText,opt_uriPolicy,opt_nmTokenPolicy){var out=[];return html.makeHtmlSanitizer(function
+sanitizeAttribs(tagName,attribs){var attribKey,attribName,atype,i,value;for(i=0;i<attribs.length;i+=2){attribName=attribs[i],value=attribs[i+1],atype=null,((attribKey=tagName+'::'+attribName,html4
+.ATTRIBS.hasOwnProperty(attribKey))||(attribKey='*::'+attribName,html4 .ATTRIBS.hasOwnProperty(attribKey)))&&(atype=html4
+.ATTRIBS[attribKey]);if(atype!==null)switch(atype){case html4 .atype.NONE:break;case
+html4 .atype.SCRIPT:case html4 .atype.STYLE:value=null;break;case html4 .atype.ID:case
+html4 .atype.IDREF:case html4 .atype.IDREFS:case html4 .atype.GLOBAL_NAME:case html4
+.atype.LOCAL_NAME:case html4 .atype.CLASSES:value=opt_nmTokenPolicy?opt_nmTokenPolicy(value):value;break;case
+html4 .atype.URI:value=opt_uriPolicy&&opt_uriPolicy(value);break;case html4 .atype.URI_FRAGMENT:value&&'#'===value.charAt(0)?(value=opt_nmTokenPolicy?opt_nmTokenPolicy(value):value,value&&(value='#'+value)):(value=null);break;default:value=null}else
+value=null;attribs[i+1]=value}return attribs})(htmlText,out),out.join('')}}
\ No newline at end of file
diff --git a/trunk/php/external/resources/com/google/caja/plugin/valija.out.js b/trunk/php/external/resources/com/google/caja/plugin/valija.out.js
new file mode 100644
index 0000000..176a13a
--- /dev/null
+++ b/trunk/php/external/resources/com/google/caja/plugin/valija.out.js
@@ -0,0 +1,453 @@
+{
+  ___.loadModule({
+      'instantiate': function (___, IMPORTS___) {
+        var Array = ___.readImport(IMPORTS___, 'Array');
+        var Object = ___.readImport(IMPORTS___, 'Object');
+        var ReferenceError = ___.readImport(IMPORTS___, 'ReferenceError', {});
+        var RegExp = ___.readImport(IMPORTS___, 'RegExp', {});
+        var TypeError = ___.readImport(IMPORTS___, 'TypeError', {});
+        var cajita = ___.readImport(IMPORTS___, 'cajita', {
+            'beget': { '()': {} },
+            'freeze': { '()': {} },
+            'newTable': { '()': {} },
+            'getFuncCategory': { '()': {} },
+            'enforceType': { '()': {} },
+            'getSuperCtor': { '()': {} },
+            'getOwnPropertyNames': { '()': {} },
+            'getProtoPropertyNames': { '()': {} },
+            'getProtoPropertyValue': { '()': {} },
+            'inheritsFrom': { '()': {} },
+            'readOwn': { '()': {} },
+            'directConstructor': { '()': {} },
+            'USELESS': {},
+            'construct': { '()': {} },
+            'forAllKeys': { '()': {} },
+            'Token': { '()': {} },
+            'args': { '()': {} }
+          });
+        var loader = ___.readImport(IMPORTS___, 'loader');
+        var outers = ___.readImport(IMPORTS___, 'outers');
+        var moduleResult___, valijaMaker;
+        moduleResult___ = ___.NO_RESULT;
+        valijaMaker = (function () {
+            function valijaMaker$_var(outers) {
+              var ObjectPrototype, DisfunctionPrototype, Disfunction,
+              ObjectShadow, x0___, FuncHeader, x1___, myPOE, x2___, pumpkin,
+              x3___, x4___, x5___, t, undefIndicator;
+              function disfuncToString($dis) {
+                var callFn, printRep, match, name;
+                callFn = $dis.call_canRead___? $dis.call: ___.readPub($dis,
+                  'call');
+                if (callFn) {
+                  printRep = callFn.toString_canCall___? callFn.toString():
+                  ___.callPub(callFn, 'toString', [ ]);
+                  match = FuncHeader.exec_canCall___? FuncHeader.exec(printRep)
+                    : ___.callPub(FuncHeader, 'exec', [ printRep ]);
+                  if (null !== match) {
+                    name = $dis.name_canRead___? $dis.name: ___.readPub($dis,
+                      'name');
+                    if (name === void 0) {
+                      name = match[ 1 ];
+                    }
+                    return 'function ' + name + '(' + match[ 2 ] +
+                      ') {\n  [cajoled code]\n}';
+                  }
+                  return printRep;
+                }
+                return 'disfunction(var_args){\n   [cajoled code]\n}';
+              }
+              disfuncToString.FUNC___ = 'disfuncToString';
+              function getShadow(func) {
+                var cat, result, parentFunc, parentShadow, proto, statics, i,
+                k, meths, i, k, v;
+                cajita.enforceType(func, 'function');
+                cat = cajita.getFuncCategory(func);
+                result = myPOE.get_canCall___? myPOE.get(cat):
+                ___.callPub(myPOE, 'get', [ cat ]);
+                if (void 0 === result) {
+                  result = cajita.beget(DisfunctionPrototype);
+                  parentFunc = cajita.getSuperCtor(func);
+                  if (___.typeOf(parentFunc) === 'function') {
+                    parentShadow = getShadow.CALL___(parentFunc);
+                  } else {
+                    parentShadow = ObjectShadow;
+                  }
+                  proto = cajita.beget(parentShadow.prototype_canRead___?
+                    parentShadow.prototype: ___.readPub(parentShadow,
+                      'prototype'));
+                  result.prototype_canSet___ === result? (result.prototype =
+                    proto): ___.setPub(result, 'prototype', proto);
+                  proto.constructor_canSet___ === proto? (proto.constructor =
+                    func): ___.setPub(proto, 'constructor', func);
+                  statics = cajita.getOwnPropertyNames(func);
+                  for (i = 0; i < statics.length; i++) {
+                    k = ___.readPub(statics, i);
+                    if (k !== 'valueOf') {
+                      ___.setPub(result, k, ___.readPub(func, k));
+                    }
+                  }
+                  meths = cajita.getProtoPropertyNames(func);
+                  for (i = 0; i < meths.length; i++) {
+                    k = ___.readPub(meths, i);
+                    if (k !== 'valueOf') {
+                      v = cajita.getProtoPropertyValue(func, k);
+                      if (___.typeOf(v) === 'object' && v !== null &&
+                        ___.typeOf(v.call_canRead___? v.call: ___.readPub(v,
+                            'call')) === 'function') {
+                        v = dis.CALL___(v.call_canRead___? v.call:
+                          ___.readPub(v, 'call'), k);
+                      }
+                      ___.setPub(proto, k, v);
+                    }
+                  }
+                  myPOE.set_canCall___? myPOE.set(cat, result):
+                  ___.callPub(myPOE, 'set', [ cat, result ]);
+                }
+                return result;
+              }
+              getShadow.FUNC___ = 'getShadow';
+              function getFakeProtoOf(func) {
+                var shadow;
+                if (___.typeOf(func) === 'function') {
+                  shadow = getShadow.CALL___(func);
+                  return shadow.prototype_canRead___? shadow.prototype:
+                  ___.readPub(shadow, 'prototype');
+                } else if (___.typeOf(func) === 'object' && func !== null) {
+                  return func.prototype_canRead___? func.prototype:
+                  ___.readPub(func, 'prototype');
+                } else { return void 0; }
+              }
+              getFakeProtoOf.FUNC___ = 'getFakeProtoOf';
+              function typeOf(obj) {
+                var result;
+                result = ___.typeOf(obj);
+                if (result !== 'object') { return result; }
+                if (cajita.isPseudoFunc_canCall___? cajita.isPseudoFunc(obj):
+                  ___.callPub(cajita, 'isPseudoFunc', [ obj ])) { return 'function'; }
+                return result;
+              }
+              typeOf.FUNC___ = 'typeOf';
+              function instanceOf(obj, func) {
+                if (___.typeOf(func) === 'function' && obj instanceof func) {
+                  return true; } else {
+                  return cajita.inheritsFrom(obj, getFakeProtoOf.CALL___(func))
+                    ;
+                }
+              }
+              instanceOf.FUNC___ = 'instanceOf';
+              function read(obj, name) {
+                var result, stepParent;
+                result = cajita.readOwn(obj, name, pumpkin);
+                if (result !== pumpkin) { return result; }
+                if (___.typeOf(obj) === 'function') {
+                  return ___.readPub(getShadow.CALL___(obj), name);
+                }
+                if (obj === null || obj === void 0) {
+                  throw ___.construct(TypeError, [ 'Cannot read property \"' +
+                        name + '\" from ' + obj ]);
+                }
+                if (___.inPub(name, ___.construct(Object, [ obj ]))) {
+                  return ___.readPub(obj, name);
+                }
+                stepParent =
+                  getFakeProtoOf.CALL___(cajita.directConstructor(obj));
+                if (stepParent !== void 0 && ___.inPub(name,
+                    ___.construct(Object, [ stepParent ])) && name !==
+                  'valueOf') {
+                  return ___.readPub(stepParent, name);
+                }
+                return ___.readPub(obj, name);
+              }
+              read.FUNC___ = 'read';
+              function set(obj, name, newValue) {
+                if (___.typeOf(obj) === 'function') {
+                  ___.setPub(getShadow.CALL___(obj), name, newValue);
+                } else {
+                  ___.setPub(obj, name, newValue);
+                }
+                return newValue;
+              }
+              set.FUNC___ = 'set';
+              function callFunc(func, args) {
+                var x0___;
+                return x0___ = cajita.USELESS, func.apply_canCall___?
+                  func.apply(x0___, args): ___.callPub(func, 'apply', [ x0___,
+                    args ]);
+              }
+              callFunc.FUNC___ = 'callFunc';
+              function callMethod(obj, name, args) {
+                var m;
+                m = read.CALL___(obj, name);
+                if (!m) {
+                  throw ___.construct(TypeError, [ 'callMethod: ' + obj +
+                        ' has no method ' + name ]);
+                }
+                return m.apply_canCall___? m.apply(obj, args): ___.callPub(m,
+                  'apply', [ obj, args ]);
+              }
+              callMethod.FUNC___ = 'callMethod';
+              function construct(ctor, args) {
+                var result, altResult;
+                if (___.typeOf(ctor) === 'function') {
+                  return cajita.construct(ctor, args);
+                }
+                result = cajita.beget(ctor.prototype_canRead___?
+                  ctor.prototype: ___.readPub(ctor, 'prototype'));
+                altResult = ctor.apply_canCall___? ctor.apply(result, args):
+                ___.callPub(ctor, 'apply', [ result, args ]);
+                switch (___.typeOf(altResult)) {
+                case 'object':
+                  if (null !== altResult) { return altResult; }
+                  break;
+                case 'function':
+                  return altResult;
+                }
+                return result;
+              }
+              construct.FUNC___ = 'construct';
+              function dis(callFn, opt_name) {
+                var template, result, x0___, disproto, x1___;
+                template = cajita.PseudoFunction_canCall___?
+                  cajita.PseudoFunction(callFn): ___.callPub(cajita,
+                  'PseudoFunction', [ callFn ]);
+                result = cajita.beget(DisfunctionPrototype);
+                result.call_canSet___ === result? (result.call = callFn):
+                ___.setPub(result, 'call', callFn);
+                x0___ = template.apply_canRead___? template.apply:
+                ___.readPub(template, 'apply'), result.apply_canSet___ ===
+                  result? (result.apply = x0___): ___.setPub(result, 'apply',
+                  x0___);
+                disproto = cajita.beget(ObjectPrototype);
+                result.prototype_canSet___ === result? (result.prototype =
+                  disproto): ___.setPub(result, 'prototype', disproto);
+                disproto.constructor_canSet___ === disproto?
+                  (disproto.constructor = result): ___.setPub(disproto,
+                  'constructor', result);
+                x1___ = template.length, result.length_canSet___ === result?
+                  (result.length = x1___): ___.setPub(result, 'length', x1___);
+                if (opt_name !== void 0 && opt_name !== '') {
+                  result.name_canSet___ === result? (result.name = opt_name):
+                  ___.setPub(result, 'name', opt_name);
+                }
+                return result;
+              }
+              dis.FUNC___ = 'dis';
+              function disfuncCall($dis, self, var_args) {
+                var a___ = ___.args(arguments);
+                var x0___;
+                return x0___ = Array.slice(a___, 2), $dis.apply_canCall___?
+                  $dis.apply(self, x0___): ___.callPub($dis, 'apply', [ self,
+                    x0___ ]);
+              }
+              disfuncCall.FUNC___ = 'disfuncCall';
+              function disfuncApply($dis, self, args) {
+                return $dis.apply_canCall___? $dis.apply(self, args):
+                ___.callPub($dis, 'apply', [ self, args ]);
+              }
+              disfuncApply.FUNC___ = 'disfuncApply';
+              function disfuncBind($dis, self, var_args) {
+                var a___ = ___.args(arguments);
+                var leftArgs;
+                function disfuncBound(var_args) {
+                  var a___ = ___.args(arguments);
+                  var x0___, x1___;
+                  return x1___ = (x0___ = Array.slice(a___, 0),
+                    leftArgs.concat_canCall___? leftArgs.concat(x0___):
+                    ___.callPub(leftArgs, 'concat', [ x0___ ])),
+                  $dis.apply_canCall___? $dis.apply(self, x1___):
+                  ___.callPub($dis, 'apply', [ self, x1___ ]);
+                }
+                disfuncBound.FUNC___ = 'disfuncBound';
+                leftArgs = Array.slice(a___, 2);
+                return ___.primFreeze(disfuncBound);
+              }
+              disfuncBind.FUNC___ = 'disfuncBind';
+              function getOuters() {
+                cajita.enforceType(outers, 'object');
+                return outers;
+              }
+              getOuters.FUNC___ = 'getOuters';
+              function readOuter(name) {
+                var result;
+                result = cajita.readOwn(outers, name, pumpkin);
+                if (result !== pumpkin) { return result; }
+                if (canReadRev.CALL___(name, outers)) {
+                  return read.CALL___(outers, name);
+                } else {
+                  throw ___.construct(ReferenceError, [
+                      'Outer variable not found: ' + name ]);
+                }
+              }
+              readOuter.FUNC___ = 'readOuter';
+              function readOuterSilent(name) {
+                if (canReadRev.CALL___(name, outers)) {
+                  return read.CALL___(outers, name);
+                } else { return void 0; }
+              }
+              readOuterSilent.FUNC___ = 'readOuterSilent';
+              function setOuter(name, val) {
+                return ___.setPub(outers, name, val);
+              }
+              setOuter.FUNC___ = 'setOuter';
+              function initOuter(name) {
+                if (canReadRev.CALL___(name, outers)) { return; }
+                set.CALL___(outers, name, void 0);
+              }
+              initOuter.FUNC___ = 'initOuter';
+              function remove(obj, name) {
+                var shadow;
+                if (___.typeOf(obj) === 'function') {
+                  shadow = getShadow.CALL___(obj);
+                  return ___.deletePub(shadow, name);
+                } else {
+                  return ___.deletePub(obj, name);
+                }
+              }
+              remove.FUNC___ = 'remove';
+              function keys(obj) {
+                var result;
+                result = [ ];
+                cajita.forAllKeys(obj, ___.markFuncFreeze(function (name) {
+                      result.push_canCall___? result.push(name):
+                      ___.callPub(result, 'push', [ name ]);
+                    }));
+                cajita.forAllKeys(getSupplement.CALL___(obj),
+                  ___.markFuncFreeze(function (name) {
+                      if (!___.inPub(name, obj) && name !== 'constructor') {
+                        result.push_canCall___? result.push(name):
+                        ___.callPub(result, 'push', [ name ]);
+                      }
+                    }));
+                return result;
+              }
+              keys.FUNC___ = 'keys';
+              function canReadRev(name, obj) {
+                if (___.inPub(name, obj)) { return true; }
+                return ___.inPub(name, getSupplement.CALL___(obj));
+              }
+              canReadRev.FUNC___ = 'canReadRev';
+              function getSupplement(obj) {
+                var ctor;
+                if (___.typeOf(obj) === 'function') {
+                  return getShadow.CALL___(obj);
+                } else {
+                  ctor = cajita.directConstructor(obj);
+                  return getFakeProtoOf.CALL___(ctor);
+                }
+              }
+              getSupplement.FUNC___ = 'getSupplement';
+              function exceptionTableSet(ex) {
+                var result, x0___;
+                result = cajita.Token('' + ex);
+                x0___ = ex === void 0? undefIndicator: ex, t.set_canCall___?
+                  t.set(result, x0___): ___.callPub(t, 'set', [ result, x0___ ]
+                );
+                return result;
+              }
+              exceptionTableSet.FUNC___ = 'exceptionTableSet';
+              function exceptionTableRead(key) {
+                var v, x0___;
+                v = t.get_canCall___? t.get(key): ___.callPub(t, 'get', [ key ]
+                );
+                x0___ = void 0, t.set_canCall___? t.set(key, x0___):
+                ___.callPub(t, 'set', [ key, x0___ ]);
+                return v === void 0? key: v === undefIndicator? void 0: v;
+              }
+              exceptionTableRead.FUNC___ = 'exceptionTableRead';
+              function disArgs(original) {
+                return cajita.args(Array.slice(original, 1));
+              }
+              disArgs.FUNC___ = 'disArgs';
+              ObjectPrototype = ___.iM([ 'constructor', Object ]);
+              DisfunctionPrototype =
+                cajita.beget(cajita.PseudoFunctionProto_canRead___?
+                cajita.PseudoFunctionProto: ___.readPub(cajita,
+                  'PseudoFunctionProto'));
+              Disfunction = cajita.beget(DisfunctionPrototype);
+              Disfunction.prototype_canSet___ === Disfunction?
+                (Disfunction.prototype = DisfunctionPrototype):
+              ___.setPub(Disfunction, 'prototype', DisfunctionPrototype);
+              Disfunction.length_canSet___ === Disfunction? (Disfunction.length
+                = 1): ___.setPub(Disfunction, 'length', 1);
+              DisfunctionPrototype.constructor_canSet___ ===
+                DisfunctionPrototype? (DisfunctionPrototype.constructor =
+                Disfunction): ___.setPub(DisfunctionPrototype, 'constructor',
+                Disfunction);
+              outers.Function_canSet___ === outers? (outers.Function =
+                Disfunction): ___.setPub(outers, 'Function', Disfunction);
+              ObjectShadow = cajita.beget(DisfunctionPrototype);
+              ObjectShadow.prototype_canSet___ === ObjectShadow?
+                (ObjectShadow.prototype = ObjectPrototype):
+              ___.setPub(ObjectShadow, 'prototype', ObjectPrototype);
+              x0___ = (function () {
+                  function freeze$_meth(obj) {
+                    if (___.typeOf(obj) === 'function') {
+                      cajita.freeze(getShadow.CALL___(obj));
+                    } else {
+                      cajita.freeze(obj);
+                    }
+                    return obj;
+                  }
+                  return ___.markFuncFreeze(freeze$_meth, 'freeze$_meth');
+                })(), ObjectShadow.freeze_canSet___ === ObjectShadow?
+                (ObjectShadow.freeze = x0___): ___.setPub(ObjectShadow,
+                'freeze', x0___);
+              FuncHeader = ___.construct(RegExp, [
+                  '^\\s*function\\s*([^\\s\\(]*)\\s*\\(' + '(?:\\$dis,?\\s*)?'
+                    + '([^\\)]*)\\)' ]);
+              x1___ = dis.CALL___(___.primFreeze(disfuncToString), 'toString'),
+              DisfunctionPrototype.toString_canSet___ === DisfunctionPrototype?
+                (DisfunctionPrototype.toString = x1___):
+              ___.setPub(DisfunctionPrototype, 'toString', x1___);
+              outers.Function_canSet___ === outers? (outers.Function =
+                Disfunction): ___.setPub(outers, 'Function', Disfunction);
+              myPOE = cajita.newTable();
+              x2___ = cajita.getFuncCategory(Object), myPOE.set_canCall___?
+                myPOE.set(x2___, ObjectShadow): ___.callPub(myPOE, 'set', [
+                  x2___, ObjectShadow ]);
+              pumpkin = ___.iM([ ]);
+              x3___ = dis.CALL___(___.primFreeze(disfuncCall), 'call'),
+              DisfunctionPrototype.call_canSet___ === DisfunctionPrototype?
+                (DisfunctionPrototype.call = x3___):
+              ___.setPub(DisfunctionPrototype, 'call', x3___);
+              x4___ = dis.CALL___(___.primFreeze(disfuncApply), 'apply'),
+              DisfunctionPrototype.apply_canSet___ === DisfunctionPrototype?
+                (DisfunctionPrototype.apply = x4___):
+              ___.setPub(DisfunctionPrototype, 'apply', x4___);
+              x5___ = dis.CALL___(___.primFreeze(disfuncBind), 'bind'),
+              DisfunctionPrototype.bind_canSet___ === DisfunctionPrototype?
+                (DisfunctionPrototype.bind = x5___):
+              ___.setPub(DisfunctionPrototype, 'bind', x5___);
+              t = cajita.newTable();
+              undefIndicator = ___.iM([ ]);
+              return cajita.freeze(___.iM([ 'typeOf', ___.primFreeze(typeOf),
+                    'instanceOf', ___.primFreeze(instanceOf), 'tr',
+                    ___.primFreeze(exceptionTableRead), 'ts',
+                    ___.primFreeze(exceptionTableSet), 'r',
+                    ___.primFreeze(read), 's', ___.primFreeze(set), 'cf',
+                    ___.primFreeze(callFunc), 'cm', ___.primFreeze(callMethod),
+                    'construct', ___.primFreeze(construct), 'getOuters',
+                    ___.primFreeze(getOuters), 'ro', ___.primFreeze(readOuter),
+                    'ros', ___.primFreeze(readOuterSilent), 'so',
+                    ___.primFreeze(setOuter), 'initOuter',
+                    ___.primFreeze(initOuter), 'remove', ___.primFreeze(remove)
+                      , 'keys', ___.primFreeze(keys), 'canReadRev',
+                    ___.primFreeze(canReadRev), 'disArgs',
+                    ___.primFreeze(disArgs), 'dis', ___.primFreeze(dis) ]));
+            }
+            return ___.markFuncFreeze(valijaMaker$_var, 'valijaMaker$_var');
+          })();
+        if (___.typeOf(loader) !== 'undefined') {
+          loader.provide_canCall___? loader.provide(valijaMaker):
+          ___.callPub(loader, 'provide', [ valijaMaker ]);
+        }
+        if (___.typeOf(outers) !== 'undefined') {
+          moduleResult___ = valijaMaker.CALL___(outers);
+        }
+        return moduleResult___;
+      },
+      'cajolerName': 'com.google.caja',
+      'cajolerVersion': '4250',
+      'cajoledDate': 1282577826648
+});
+}
\ No newline at end of file
diff --git a/trunk/php/index.php b/trunk/php/index.php
new file mode 100644
index 0000000..91d1324
--- /dev/null
+++ b/trunk/php/index.php
@@ -0,0 +1,25 @@
+<?php
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+include 'src/apache/shindig/bootstrap.php';
+
+$frontController = new \apache\shindig\FrontController();
+$frontController->setLocalConfig(__DIR__ . '/config/local.php');
+$frontController->run();
\ No newline at end of file
diff --git a/trunk/php/phpunit.xml.dist b/trunk/php/phpunit.xml.dist
new file mode 100644
index 0000000..de3588c
--- /dev/null
+++ b/trunk/php/phpunit.xml.dist
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<phpunit backupGlobals="true"
+         syntaxCheck="false"
+         bootstrap="test/bootstrap.php"
+         stopOnFailure="false"
+         verbose="true">
+  <testsuites>
+    <testsuite name="shindig-php">
+      <directory>test</directory>
+    </testsuite>
+  </testsuites>
+
+
+  <logging>
+    <log type="coverage-html" target="target/site/coverage-report/"
+         title="Shindig PHP Coverage"
+         charset="UTF-8" yui="true" highlight="true"
+         lowUpperBound="34" highLowerBound="70"/>
+    <log type="coverage-clover" target="target/clover.xml"/>
+    <log type="junit" target="target/surefire-reports/phpunit-testresults.xml"/>
+  </logging>
+  <filter>
+    <blacklist>
+      <directory suffix=".php">./external/jsmin-php</directory>
+      <directory suffix=".php">./external/OAuth</directory>
+      <directory suffix=".php">./external/Zend</directory>
+      <directory suffix=".php">./test/social</directory>
+      <directory suffix=".php">./test</directory>
+    </blacklist>
+  </filter>
+</phpunit>
diff --git a/trunk/php/pom.xml b/trunk/php/pom.xml
new file mode 100644
index 0000000..25d8d5c
--- /dev/null
+++ b/trunk/php/pom.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.shindig</groupId>
+    <artifactId>shindig-project</artifactId>
+    <version>2.5.1-SNAPSHOT</version>
+    <relativePath>../pom.xml</relativePath>
+  </parent>
+
+  <artifactId>shindig-php</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Apache Shindig PHP</name>
+  <description>PHP tests</description>
+
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk/php</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk/php</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk/php</url>
+  </scm>
+
+  <!-- common params -->
+  <properties>
+    <surefire.reports>target/surefire-reports</surefire.reports>
+    <coverage.report>target/site/coverage-report</coverage.report>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <workingDirectory>target</workingDirectory>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <dependencies>
+          <dependency>
+            <!-- note that 1.8.0 has problems generating xslt output... -->
+            <groupId>org.apache.ant</groupId>
+            <artifactId>ant-trax</artifactId>
+            <version>1.7.0</version>
+          </dependency>
+          <dependency>
+            <groupId>net.sf.saxon</groupId>
+            <artifactId>saxon</artifactId>
+            <version>8.7</version>
+          </dependency>
+        </dependencies>
+        <executions>
+          <!-- run basic lint -->
+          <execution>
+            <id>phplint</id>
+            <phase>process-sources</phase>
+            <goals><goal>run</goal></goals>
+            <configuration>
+              <target>
+                <apply executable="php">
+                  <arg value="-l" />
+                  <arg value="-n" />
+                  <fileset dir="." includes="src/**/*.php" />
+                </apply>
+              </target>
+            </configuration>
+          </execution>
+          <!-- phpdocs -->
+	  <execution>
+	    <id>phpdoc</id>
+	    <phase>pre-site</phase>
+	    <goals><goal>run</goal></goals>
+	    <configuration>
+	      <target>
+		<mkdir dir="target/site/phpdoc" />
+		<exec executable="phpdoc" failonerror="true">
+		  <arg value="-d" />
+		  <arg value="src" />
+		  <arg value="-t" />
+		  <arg value="target/site/phpdoc" />
+		  <arg value="-o" />
+		  <arg value="HTML:frames:DOM/default" />
+		  <arg value="--title" />
+		  <arg value="Apache Shindig ${project.version}" />
+		</exec>
+	      </target>
+	    </configuration>
+	  </execution>
+	  
+          <!-- run phpunit -->
+          <execution>
+            <id>test</id>
+            <phase>test</phase>
+            <goals>
+              <goal>run</goal>
+            </goals>
+            <configuration>
+              <target>
+                <mkdir dir="${surefire.reports}" />
+                <mkdir dir="${coverage.report}" />
+		<mkdir dir="target/tmp" />
+                <exec executable="phpunit" dir=".">
+                  <arg value="--verbose" />
+		  <env key="TMPDIR" value="${basedir}/target/tmp" />
+                </exec>
+                <xslt in="${surefire.reports}/phpunit-testresults.xml" out="${surefire.reports}/xslt.info" style="test/config/phpunit_to_surefire.xslt">
+                  <param name="outputDir" expression="." />
+                </xslt>
+              </target>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <profiles>
+    <profile>
+      <id>reporting</id>
+      <reporting>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-report-plugin</artifactId>
+          </plugin>
+        </plugins>
+      </reporting>
+    </profile>
+  </profiles>
+</project>
+
+
diff --git a/trunk/php/src/apache/shindig/FrontController.php b/trunk/php/src/apache/shindig/FrontController.php
new file mode 100644
index 0000000..2642fc4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/FrontController.php
@@ -0,0 +1,111 @@
+<?php
+namespace apache\shindig;
+
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+class FrontController {
+
+    private $localConfig;
+
+    public function setLocalConfig($localConfig) {
+        $this->localConfig = $localConfig;
+    }
+
+    public function run() {
+        Config::loadConfig($this->localConfig);
+
+        $this->checkServerConfig();
+
+        //get servlet map and prefix the servlet paths
+        $configServletMap = Config::get('servlet_map');
+        $webPrefix = Config::get('web_prefix');
+        $servletMap = array();
+        foreach ($configServletMap as $path => $servlet) {
+            $servletMap[$webPrefix . $path] = $servlet;
+        }
+
+        // Try to match the request url to our servlet mapping
+        $servlet = false;
+        $uri = $_SERVER["REQUEST_URI"];
+        foreach ($servletMap as $url => $class) {
+            if (substr($uri, 0, strlen($url)) == $url) {
+                //FIXME temporary hack to support both /proxy and /makeRequest with the same event handler
+                // /makeRequest == /proxy?output=js
+                if ($url == $webPrefix . '/gadgets/makeRequest') {
+                    $_GET['output'] = 'js';
+                }
+                $servlet = $class;
+                break;
+            }
+        }
+
+        // If we found a correlating servlet, instance and call it. Otherwise give a 404 error
+        if ($servlet) {
+            $class = new $class();
+            $method = $_SERVER['REQUEST_METHOD'];
+            // Not all clients support the PUT, HEAD & DELETE http methods, they depend on the X-HTTP-Method-Override instead
+            if ($method == 'POST' && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
+                $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
+            }
+            $method = 'do' . ucfirst(strtolower($method));
+            if (is_callable(array($class, $method))) {
+                $class->$method();
+            } else {
+                header("HTTP/1.0 405 Method Not Allowed");
+                echo "<html><body><h1>405 Method Not Allowed</h1></body></html>";
+            }
+        } else {
+            // Unhandled event, display simple 404 error
+            header("HTTP/1.0 404 Not Found");
+            echo "<html><body><h1>404 Not Found</h1></body></html>";
+        }
+    }
+
+    private function checkServerConfig() {
+        if (!Config::get('debug')) {
+            // Basic sanity check if we have all required modules
+            $modules = array('json', 'SimpleXML', 'libxml', 'curl', 'openssl');
+            // if plain text tokens are disallowed we require mcrypt
+            if (!Config::get('allow_plaintext_token')) {
+                $modules[] = 'mcrypt';
+            }
+            // if you selected the memcache caching backend, you need the memcache extention too :)
+            if (Config::get('data_cache') == 'CacheMemcache') {
+                $modules[] = 'memcache';
+            }
+            foreach ($modules as $module) {
+                if (!extension_loaded($module)) {
+                    die("Shindig requires the {$module} extention, see <a href='http://www.php.net/{$module}'>http://www.php.net/{$module}</a> for more info");
+                }
+            }
+
+            if (get_magic_quotes_gpc()) {
+                die("Your environment has magic_quotes_gpc enabled which will interfere with Shindig.  Please set 'magic_quotes_gpc' to 'Off' in php.ini");
+            }
+
+            $populate_raw_post = strtolower(ini_get("always_populate_raw_post_data"));
+            if (!isset($populate_raw_post) || $populate_raw_post === "0" || $populate_raw_post === "Off") {
+                die("Your environment does not have always_populate_raw_post_data enabled which will interfere with Shindig.  Please set 'always_populate_raw_post_data' to 'On' in php.ini");
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/bootstrap.php b/trunk/php/src/apache/shindig/bootstrap.php
new file mode 100644
index 0000000..8f363d8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/bootstrap.php
@@ -0,0 +1,66 @@
+<?php
+namespace apache\shindig;
+
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+// Some people forget to set their timezone in their php.ini,
+// this prevents that from generating warnings
+@date_default_timezone_set(@date_default_timezone_get());
+
+require_once __DIR__ . '/../../../external/Symfony/Component/ClassLoader/UniversalClassLoader.php';
+
+$loader = new \Symfony\Component\ClassLoader\UniversalClassLoader();
+$loader->registerNamespaces(array(
+  'Symfony' => __DIR__ . '/../../../external',
+  'apache\shindig' => __DIR__ . '/../../'
+
+));
+$loader->registerPrefixes(array(
+  'Zend_' => __DIR__ . '/../../../external',
+));
+$loader->register();
+
+
+$mapperLoader = new \Symfony\Component\ClassLoader\MapClassLoader(array(
+  'JsMin' => __DIR__ . '/../../../external/jsmin-php/jsmin.php',
+  'OAuthRequest' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthSignatureMethod_RSA_SHA1' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthSignatureMethod_PLAINTEXT' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthSignatureMethod_HMAC_SHA1' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthSignatureMethodRSA_SHA1' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthUtil' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthToken' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthConsumer' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthDataStore' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthException' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+  'OAuthServer' => __DIR__ . '/../../../external/OAuth/OAuth.php',
+));
+
+$mapperLoader->register();
+
+//$extensionClassPaths = \apache\shindig\common\Config::get('extension_class_paths');
+//
+//if (! is_array($extensionClassPaths)) {
+//    $extensionClassPaths = array($extensionClassPaths);
+//}
+//
+//$loader->registerPrefixFallbacks($extensionClassPaths);
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/common/AuthenticationMode.php b/trunk/php/src/apache/shindig/common/AuthenticationMode.php
new file mode 100644
index 0000000..8c4190a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/AuthenticationMode.php
@@ -0,0 +1,51 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class AuthenticationMode {
+  /**
+   * The request has no authentication associated with it. Used for anonymous requests
+   */
+  public static $UNAUTHENTICATED = 'unauthenticated';
+
+  /**
+   * Used by rendered gadgets to authenticate calls to the container
+   */
+  public static $SECURITY_TOKEN_URL_PARAMETER = 'security_token_url_parameter';
+
+  /**
+   * A fully validated 3-legged OAuth call by a 3rd party on behalf of a user of the
+   * receiving domain. viewerid should always be available
+   */
+  public static $OAUTH = 'oauth';
+
+  /**
+   * A call by a validated 3rd party on its own behalf. Can emulate a call on behalf of a user
+   * of the receiving domain subject to ACL checking but is not required to do so. viewerid may or
+   * may not be available
+   */
+  public static $OAUTH_CONSUMER_REQUEST = 'oauth_consumer_request';
+
+  /**
+   * The request is from a logged in user of the receiving domain
+   */
+  public static $COOKIE = 'cookie';
+}
diff --git a/trunk/php/src/apache/shindig/common/BlobCrypter.php b/trunk/php/src/apache/shindig/common/BlobCrypter.php
new file mode 100644
index 0000000..475df9e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/BlobCrypter.php
@@ -0,0 +1,52 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Utility interface for managing signed, encrypted, and time stamped blobs.
+ * Blobs are made up of name/value pairs.  Time stamps are automatically
+ * included and checked.
+ *
+ */
+abstract class BlobCrypter {
+
+  /**
+   * Time stamps, encrypts, and signs a blob.
+   *
+   * @param array $in name/value pairs to encrypt
+   * @return string a base64 encoded blob
+   *
+   * @throws BlobCrypterException
+   */
+  abstract public function wrap(Array $in);
+
+  /**
+   * Unwraps a blob.
+   *
+   * @param string $in blob
+   * @param int $maxAgeSec maximum age for the blob
+   * @return array the name/value pairs, including the origin timestamp.
+   *
+   * @throws BlobExpiredException if the blob is too old to be accepted.
+   * @throws BlobCrypterException if the blob can't be decoded.
+   */
+  abstract public function unwrap($in, $maxAgeSec);
+}
diff --git a/trunk/php/src/apache/shindig/common/BlobCrypterException.php b/trunk/php/src/apache/shindig/common/BlobCrypterException.php
new file mode 100644
index 0000000..41bd0fa
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/BlobCrypterException.php
@@ -0,0 +1,27 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+class BlobCrypterException extends \Exception {
+
+}
+
diff --git a/trunk/php/src/apache/shindig/common/Cache.php b/trunk/php/src/apache/shindig/common/Cache.php
new file mode 100644
index 0000000..336e8ba
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/Cache.php
@@ -0,0 +1,172 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class Cache {
+  /**
+   * @var RequestTime
+   */
+  private $time = null;
+
+  /**
+   * @var CacheStorage
+   */
+  private $storage = null;
+
+  /**
+   * @param string $cacheClass
+   * @param string $name
+   * @param RequestTime $time
+   * @return Cache
+   */
+  static public function createCache($cacheClass, $name, RequestTime $time = null) {
+    return new Cache($cacheClass, $name, $time);
+  }
+
+  /**
+   *
+   * @param string $cacheClass
+   * @param string $name
+   * @param RequestTime $time
+   */
+  private function __construct($cacheClass, $name, RequestTime $time = null) {
+    if ($cacheClass === false) {
+      return;
+    }
+    $this->storage = new $cacheClass($name);
+    if ($time == null) {
+      $this->time = new RequestTime();
+    } else {
+      $this->time = $time;
+    }
+  }
+
+  /**
+   *
+   * @param string $key
+   * @return array or false
+   */
+  public function get($key) {
+    if (! $this->storage) {
+      return false;
+    }
+    if ($this->storage->isLocked($key)) {
+      $this->storage->waitForLock($key);
+    }
+    $data = $this->storage->fetch($key);
+    if ($data) {
+      $data = unserialize($data);
+      if ($data['valid'] && ($this->time->getRequestTime() - $data['time']) < $data['ttl']) {
+        return $data['data'];
+      }
+    }
+    return false;
+  }
+
+  /**
+   *
+   * @param string $key
+   * @return array
+   */
+  public function expiredGet($key) {
+    if (! $this->storage) {
+      return false;
+    }
+    if ($this->storage->isLocked($key)) {
+      $this->storage->waitForLock($key);
+    }
+    $data = $this->storage->fetch($key);
+    if ($data) {
+      $data = unserialize($data);
+      return array_merge(array('found' => true), $data);
+    }
+    return array('found' => false);
+  }
+
+  /**
+   *
+   * @param string $key
+   * @param mixed $value
+   * @param int $ttl optional
+   */
+  public function set($key, $value, $ttl = false) {
+    if (! $this->storage) {
+      return false;
+    }
+    if (! $ttl) {
+      $ttl = Config::Get('cache_time');
+    }
+    if ($this->storage->isLocked($key)) {
+      $this->storage->waitForLock($key);
+    }
+    $data = serialize(array('time' => $this->time->getRequestTime(), 'ttl' => $ttl, 'valid' => true, 'data' => $value));
+    $this->storage->lock($key);
+    try {
+      $this->storage->store($key, $data);
+      $this->storage->unlock($key);
+    } catch (\Exception $e) {
+      $this->storage->unlock($key);
+      throw $e;
+    }
+  }
+
+  /**
+   *
+   * @param string $key
+   */
+  public function delete($key) {
+    if (! $this->storage) {
+      return false;
+    }
+    if ($this->storage->isLocked($key)) {
+      $this->storage->waitForLock($key);
+    }
+    $this->storage->lock($key);
+    $this->storage->delete($key);
+    $this->storage->unlock($key);
+  }
+
+  /**
+   *
+   * @param string $key
+   */
+  public function invalidate($key) {
+    if (! $this->storage) {
+      return false;
+    }
+    if ($this->storage->isLocked($key)) {
+      $this->storage->waitForLock($key);
+    }
+    $this->storage->lock($key);
+    try {
+      $data = $this->storage->fetch($key);
+      if ($data) {
+        $data = unserialize($data);
+        $data = serialize(array('time' => $data['time'], 'ttl' => $data['ttl'], 'valid' => false, 'data' => $data['data']));
+        $this->storage->store($key, $data);
+      }
+      $this->storage->unlock($key);
+    } catch (\Exception $e) {
+      $this->storage->unlock($key);
+      throw $e;
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/CacheException.php b/trunk/php/src/apache/shindig/common/CacheException.php
new file mode 100644
index 0000000..b852f85
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/CacheException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class CacheException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/common/CacheStorage.php b/trunk/php/src/apache/shindig/common/CacheStorage.php
new file mode 100644
index 0000000..6637500
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/CacheStorage.php
@@ -0,0 +1,77 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+abstract class CacheStorage {
+
+  /**
+   *
+   * @param string $key
+   * @param mixed $value
+   */
+  abstract public function store($key, $value);
+
+  /**
+   *
+   * @param string $key
+   */
+  abstract public function fetch($key);
+
+  /**
+   *
+   * @param string $key
+   */
+  abstract public function delete($key);
+
+  /**
+   *
+   * @param string $key
+   */
+  abstract public function lock($key);
+
+  /**
+   *
+   * @param string $key
+   */
+  abstract public function unlock($key);
+
+  /**
+   *
+   * @param string $key
+   */
+  abstract public function isLocked($key);
+
+  /**
+   *
+   * @param string $key
+   */
+  public function waitForLock($key) {
+    $tries = 10;
+    $cnt = 0;
+    do {
+      usleep(100);
+      $cnt ++;
+    } while ($cnt <= $tries && $this->isLocked($key));
+    if ($this->isLocked($key)) {
+      $this->unlock($key);
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/Config.php b/trunk/php/src/apache/shindig/common/Config.php
new file mode 100644
index 0000000..db272eb
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/Config.php
@@ -0,0 +1,89 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Configuration class. It uses the keys/values from config/container.php
+ * and (if the file exists) config/local.php.
+ */
+class Config {
+  /**
+   *
+   * @var array
+   */
+  private static $config = false;
+
+  /**
+   *
+   * @global array $shindigConfig
+   * @param string $local
+   */
+  public static function loadConfig($localConfigPath = null) {
+    if (! self::$config) {
+      // load default configuration
+      self::$config = require realpath(dirname(__FILE__) . "/../../../../config") . '/container.php';
+      if ($localConfigPath) {
+        if (file_exists($localConfigPath)) {
+          // include local.php if it exists and merge the config arrays.
+          // the second array values overwrites the first one's
+          $localConfig = require $localConfigPath;
+          self::$config = array_merge(self::$config, $localConfig);
+        }
+      }
+    }
+  }
+
+  /**
+   * Merges the given array with the config array. It uses the keys/values from config/container.php.
+   *
+   * @param array $tconfig
+   */
+  public static function setConfig($tconfig) {
+    self::$config = array_merge(self::$config, $tconfig);
+  }
+
+  /**
+   *
+   * @param string $key
+   * @return array
+   * @throws ConfigException
+   */
+  public static function get($key) {
+    if (isset(self::$config[$key])) {
+      return self::$config[$key];
+    } else {
+      throw new ConfigException("Invalid Config Key " . $key);
+    }
+  }
+
+  /**
+   * Overrides a config value for as long as this object is loaded in memory.
+   * This allows for overriding certain configuration values for the purposes
+   * of unit tests.  Note that this does not commit a permanent change to the
+   * configuration files.
+   *
+   * @param $key string Configuration key to set the value of.
+   * @param $value string Value to assign to the specified key.
+   */
+  public static function set($key, $value) {
+    self::$config[$key] = $value;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/ConfigException.php b/trunk/php/src/apache/shindig/common/ConfigException.php
new file mode 100644
index 0000000..628de6e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ConfigException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ConfigException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/common/File.php b/trunk/php/src/apache/shindig/common/File.php
new file mode 100644
index 0000000..c2580af
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/File.php
@@ -0,0 +1,53 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+class File {
+
+  /**
+   *
+   * @param string $file
+   * @return boolean
+   */
+  public static function exists($file) {
+    // only really check if check_file_exists == true, big performance hit on production systems, but also much safer :)
+    if (Config::get('check_file_exists')) {
+      return file_exists($file);
+    } else {
+      return true;
+    }
+  }
+
+  /**
+   *
+   * @param string $file
+   * @return boolean
+   */
+  public static function readable($file) {
+    // only really check if check_file_exists == true, big performance hit on production systems, but also much safer :)
+    if (Config::get('check_file_exists')) {
+      return is_readable($file);
+    } else {
+      return true;
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/HttpServlet.php b/trunk/php/src/apache/shindig/common/HttpServlet.php
new file mode 100644
index 0000000..8e55f42
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/HttpServlet.php
@@ -0,0 +1,169 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/*
+ * This is a somewhat liberal interpretation of the HttpServlet class
+ * Mixed with some essentials to make propper http header handling
+ * happen in php.
+ */
+class HttpServlet {
+  private $lastModified = false;
+  private $contentType = 'text/html';
+  private $charset = 'UTF-8';
+  private $noCache = false;
+  private $cacheTime;
+  public $noHeaders = false;
+
+  /**
+   * Enables output buffering so we can do correct header handling in the destructor
+   *
+   */
+  public function __construct() {
+    // set our default cache time (config cache time defaults to 24 hours aka 1 day)
+    $this->cacheTime = Config::get('cache_time');
+    // to do our header magic, we need output buffering on
+    ob_start();
+  }
+
+  /**
+   * Code ran after the event handler, adds headers etc to the request
+   * If noHeaders is false, it adds all the correct http/1.1 headers to the request
+   * and deals with modified/expires/e-tags/etc. This makes the server behave more like
+   * a real http server.
+   */
+  public function __destruct() {
+    if (! $this->noHeaders) {
+      header("Content-Type: $this->contentType" . (! empty($this->charset) ? "; charset={$this->charset}" : ''));
+      header('Accept-Ranges: bytes');
+      if ($this->noCache) {
+        header("Cache-Control: no-cache, must-revalidate", true);
+        header("Expires: Mon, 26 Jul 1997 05:00:00 GMT", true);
+      } else {
+        // attempt at some propper header handling from php
+        // this departs a little from the shindig code but it should give is valid http protocol handling
+        header('Cache-Control: public,max-age=' . $this->cacheTime, true);
+        header("Expires: " . gmdate("D, d M Y H:i:s", time() + $this->cacheTime) . " GMT", true);
+        // Obey browsers (or proxy's) request to send a fresh copy if we recieve a no-cache pragma or cache-control request
+        if (! isset($_SERVER['HTTP_PRAGMA']) || ! strstr(strtolower($_SERVER['HTTP_PRAGMA']), 'no-cache') && (! isset($_SERVER['HTTP_CACHE_CONTROL']) || ! strstr(strtolower($_SERVER['HTTP_CACHE_CONTROL']), 'no-cache'))) {
+          if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $this->lastModified && ! isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
+            $if_modified_since = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
+            if ($this->lastModified <= $if_modified_since) {
+              header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $this->lastModified) . ' GMT', true);
+              header("HTTP/1.1 304 Not Modified", true);
+              header('Content-Length: 0', true);
+              ob_end_clean();
+              die();
+            }
+          }
+          header('Last-Modified: ' . gmdate('D, d M Y H:i:s', ($this->lastModified ? $this->lastModified : time())) . ' GMT', true);
+        }
+      }
+    }
+    else {
+      ob_end_flush();
+    }
+  }
+
+  public function getCharset() {
+    return $this->charset;
+  }
+
+  public function setCharset($charset) {
+    $this->charset = $charset;
+  }
+
+  /**
+   * Sets the time in seconds that the browser's cache should be
+   * considered out of date (through the Expires header)
+   *
+   * @param int $time time in seconds
+   */
+  public function setCacheTime($time) {
+    $this->cacheTime = $time;
+  }
+
+  /**
+   * Returns the time in seconds that the browser is allowed to cache the content
+   *
+   * @return int $time
+   */
+  public function getCacheTime() {
+    return $this->cacheTime;
+  }
+
+  /**
+   * Sets the content type of this request (forinstance: text/html or text/javascript, etc)
+   *
+   * @param string $type content type header to use
+   */
+  public function setContentType($type) {
+    $this->contentType = $type;
+  }
+
+  /**
+   * Returns the current content type
+   *
+   * @return string content type string
+   */
+  public function getContentType() {
+    return $this->contentType;
+  }
+
+  /**
+   * returns the current last modified time stamp
+   *
+   * @return int timestamp
+   */
+  public function getLastModified() {
+    return $this->lastModified;
+  }
+
+  /**
+   * Sets the last modified timestamp. It automaticly checks if this timestamp
+   * is larger then its current timestamp, and if not ignores the call
+   *
+   * @param int $modified timestamp
+   */
+  public function setLastModified($modified) {
+    $this->lastModified = max($this->lastModified, $modified);
+  }
+
+  /**
+   * Sets the noCache boolean. If its set to true, no-caching headers will be send
+   * (pragma no cache, expiration in the past)
+   *
+   * @param boolean $cache send no-cache headers?
+   */
+  public function setNoCache($cache = false) {
+    $this->noCache = $cache;
+  }
+
+  /**
+   * returns the noCache boolean
+   *
+   * @return boolean
+   */
+  public function getNoCache() {
+    return $this->noCache;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/IllegalArgumentException.php b/trunk/php/src/apache/shindig/common/IllegalArgumentException.php
new file mode 100644
index 0000000..20dc179
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/IllegalArgumentException.php
@@ -0,0 +1,25 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+class IllegalArgumentException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/common/Locale.php b/trunk/php/src/apache/shindig/common/Locale.php
new file mode 100644
index 0000000..ff9d678
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/Locale.php
@@ -0,0 +1,79 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * Locale class doesn't exist in php, so to allow the code base to be closer to the java and it's spec
+ * interpretation one, we created our own
+ */
+class Locale {
+  /**
+   *
+   * @var string
+   */
+  public $language;
+
+  /**
+   *
+   * @var string
+   */
+  public $country;
+
+  /**
+   *
+   * @param string $language
+   * @param string $country
+   */
+  public function __construct($language, $country) {
+    $this->language = $language;
+    $this->country = $country;
+  }
+
+  /**
+   *
+   * @param Locale $obj
+   * @return boolean
+   */
+  public function equals($obj) {
+    if (! ($obj instanceof Locale)) {
+      return false;
+    }
+    return ($obj->language == $this->language && $obj->country == $this->country);
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getLanguage() {
+    return $this->language;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getCountry() {
+    return $this->country;
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/common/OAuthLookupService.php b/trunk/php/src/apache/shindig/common/OAuthLookupService.php
new file mode 100644
index 0000000..912ee4b
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/OAuthLookupService.php
@@ -0,0 +1,37 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * Interface for handling the validation of OAuth requests.
+ */
+abstract class OAuthLookupService {
+
+  /**
+   * @var OAuthRequest $oauthRequest
+   * @var string $appUrl
+   * @var string $userId
+   * @var string $contentType
+   */
+  abstract public function getSecurityToken($oauthRequest, $appUrl, $userId, $contentType);
+}
+
diff --git a/trunk/php/src/apache/shindig/common/OpenSocialVersion.php b/trunk/php/src/apache/shindig/common/OpenSocialVersion.php
new file mode 100644
index 0000000..7a57493
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/OpenSocialVersion.php
@@ -0,0 +1,108 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Convenience class for working with OpenSocial Specification and Feature versions.
+ * Applies the rules specified in the OS specification
+ * http://opensocial-resources.googlecode.com/svn/spec/1.0/Core-Gadget.xml#Versioning
+ * 
+ */
+class OpenSocialVersion {
+    
+  public $major = null;
+  public $minor = null;
+  public $patch = null;
+  
+  /**
+   * Create a new OpenSocialVersion based upon a versionString
+   * @param string $versionString Version string
+   */
+  public function __construct($versionString = null){
+    if (! $versionString) {
+      return;
+    }
+    $versionParts = explode('.', $versionString);
+    if (isset($versionParts[0]) && is_numeric($versionParts[0])) {
+      $this->major = (int) $versionParts[0];
+    }
+    if (isset($versionParts[1]) && is_numeric($versionParts[1])) {
+      $this->minor = (int) $versionParts[1];
+    }
+    if (isset($versionParts[2]) && is_numeric($versionParts[2])) {
+      $this->patch = (int) $versionParts[2];
+    }
+  }
+
+  /**
+   * Tests if OpenSocialVersion is equivalent to the parameter version
+   * @param OpenSocialVersion $version Compare with this version
+   * @return boolean TRUE if is equivalent to version
+   */
+  public function isEquivalent(OpenSocialVersion $version){
+    if ($this->major === null || $version->major === null) {
+      return true;
+    }
+    $cmp = $version->major - $this->major;
+    if($cmp == 0 && $version->minor && $this->minor){
+      $cmp = $version->minor - $this->minor;
+    }
+    if($cmp == 0 && $version->patch && $this->patch){
+      $cmp = $version->patch - $this->patch;
+    }
+    return $cmp == 0;
+  }
+  
+  /**
+   * Tests if OpenSocialVersion is equal to or greater than parameter version
+   * @param OpenSocialVersion $version Compare with this version
+   * @return boolean TRUE if is equal or greater than version
+   */
+  public function isEqualOrGreaterThan(OpenSocialVersion $version){
+    if ($this->major === null || $version->major === null) {
+      return true;
+    }
+    $cmp = $version->major - $this->major;
+    if($cmp == 0){
+      if($version->minor && $this->minor){
+        $cmp = $version->minor - $this->minor;
+      } else {
+        $cmp = $version->minor;
+      }
+    }
+    if($cmp == 0){
+      if($version->patch && $this->patch){
+        $cmp = $version->patch - $this->patch;
+      } else {
+        $cmp = $version->patch;
+      }
+    }
+    return $cmp <= 0;
+  }
+  
+  /**
+   * @return string
+   */
+  public function __toString() {
+    return $this->major . '.' . $this->minor . '.' . $this->patch;
+  }
+}
+
diff --git a/trunk/php/src/apache/shindig/common/Options.php b/trunk/php/src/apache/shindig/common/Options.php
new file mode 100644
index 0000000..2324247
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/Options.php
@@ -0,0 +1,46 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Bag of options for making a request.
+ *
+ * This object is mutable to keep us sane. Don't mess with it once you've
+ * sent it to RemoteContentRequest or bad things might happen.
+ */
+class Options {
+  public $ignoreCache = false;
+  public $ownerSigned = true;
+  public $viewerSigned = true;
+
+  public function __construct() {}
+
+  /**
+   * Copy constructor
+   *
+   * @param Options $copyFrom
+   */
+  public function copyOptions(Options $copyFrom) {
+    $this->ignoreCache = $copyFrom->ignoreCache;
+    $this->ownerSigned = $copyFrom->ownerSigned;
+    $this->viewerSigned = $copyFrom->viewerSigned;
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/common/RemoteContent.php b/trunk/php/src/apache/shindig/common/RemoteContent.php
new file mode 100644
index 0000000..d775346
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/RemoteContent.php
@@ -0,0 +1,49 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/*
+ * remoteContent* classes, we departed from the shindig java base style a bit here
+ * We want to use curl_multi for our content fetching because we don't have any fancy
+ * worker queue's where the java variant does.
+ * So a different methodlogy which calls for a different working unfortunatly, however
+ * it's kept in the spirit of the java variant as much as possible
+ */
+abstract class RemoteContent {
+
+  /**
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  abstract public function fetch(RemoteContentRequest $request);
+
+  /**
+   * @param array $requests
+   * @return array
+   */
+  abstract public function multiFetch(Array $requests);
+
+  /**
+   * @param RemoteContentRequest $request
+   */
+  abstract public function invalidate(RemoteContentRequest $request);
+}
diff --git a/trunk/php/src/apache/shindig/common/RemoteContentException.php b/trunk/php/src/apache/shindig/common/RemoteContentException.php
new file mode 100644
index 0000000..523352d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/RemoteContentException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class RemoteContentException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/common/RemoteContentFetcher.php b/trunk/php/src/apache/shindig/common/RemoteContentFetcher.php
new file mode 100644
index 0000000..6d34e2c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/RemoteContentFetcher.php
@@ -0,0 +1,36 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+abstract class RemoteContentFetcher {
+  /**
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  abstract public function fetchRequest(RemoteContentRequest $request);
+
+  /**
+   * @param array $requests
+   * @return array
+   */
+  abstract public function multiFetchRequest(Array $requests);
+}
diff --git a/trunk/php/src/apache/shindig/common/RemoteContentRequest.php b/trunk/php/src/apache/shindig/common/RemoteContentRequest.php
new file mode 100644
index 0000000..3343dba
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/RemoteContentRequest.php
@@ -0,0 +1,411 @@
+<?php
+namespace apache\shindig\common;
+use apache\shindig\gadgets\oauth\OAuthRequestParams;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class RemoteContentRequest {
+  // these are used for making the request
+  private $uri = '';
+  // to get real url after signed requests
+  private $notSignedUri = '';
+  private $method = '';
+  private $headers = array();
+  private $postBody = false;
+  // these fields are filled in once the request has completed
+  private $responseContent = false;
+  private $responseSize = false;
+  private $responseHeaders = array();
+  private $metadata = array();
+  private $httpCode = false;
+  private $httpCodeMsg = '';
+  private $contentType = null;
+  private $created;
+  private $refreshInterval;
+  private static $SC_OK = 200; //Please, use only for testing!
+  public $handle = false;
+  public static $DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded; charset=utf-8";
+
+  /**
+   * @var Options
+   */
+  private $options;
+
+  /**
+   * @var SecurityToken
+   */
+  private $token;
+
+  /**
+   * @var string
+   */
+  private $invalidation;
+
+  public static $AUTH_NONE = 'none';
+  public static $AUTH_SIGNED = 'signed';
+  public static $AUTH_OAUTH = 'oauth';
+  public static $AUTH_OAUTH2 = 'oauth2';
+
+  /**
+   * @var string
+   */
+  private $authType;
+
+  /**
+   * @var OAuthRequestParams
+   */
+  private $oauthParams = null;
+
+  public function __construct($uri, $headers = false, $postBody = false) {
+    $this->uri = $uri;
+    $this->notSignedUri = $uri;
+    $this->headers = $headers;
+    $this->postBody = $postBody;
+    $this->created = time();
+    $this->authType = self::$AUTH_NONE;
+  }
+
+  public function createRemoteContentRequest($method, $uri, $headers, $postBody, $options) {
+    $this->method = $method;
+    $this->uri = $uri;
+    $this->options = $options;
+    // Copy the headers
+    if (! isset($headers)) {
+      $this->headers = '';
+    } else {
+      $setPragmaHeader = false;
+      $tmpHeaders = '';
+      foreach ($headers as $key => $value) {
+        // Proxies should be bypassed with the Pragma: no-cache check.
+        if ($key == "Pragma" && $options->ignoreCache) {
+          $value = "no-cache";
+          $setPragmaHeader = true;
+        }
+        $tmpHeaders .= $key . ":" . $value . "\n";
+      }
+      // Bypass caching in proxies as well.
+      if (! $setPragmaHeader && $options->ignoreCache) {
+        $tmpHeaders .= "Pragma: no-cache\n";
+      }
+      $this->headers = $tmpHeaders;
+    }
+    if (! isset($postBody)) {
+      $this->postBody = '';
+    } else {
+      $this->postBody = array_merge($postBody, $this->postBody);
+    }
+    $type = $this->getHeader("Content-Type");
+    if (! isset($type)) {
+      $this->contentType = RemoteContentRequest::$DEFAULT_CONTENT_TYPE;
+    } else {
+      $this->contentType = $type;
+    }
+  }
+
+  /**
+   * Basic GET request.
+   *
+   * @param uri
+   */
+  public function createRemoteContentRequestWithUri($uri) {
+    $this->createRemoteContentRequest("GET", $uri, null, null, RemoteContentRequest::getDefaultOptions());
+  }
+
+  /**
+   * Returns a hash code which identifies this request, used for caching, takes method, url, authType and headers
+   * into account for constructing the md5 checksum
+   * NOTE: the postBody is excluded so that the GadgetHrefRenderer can use cached requests without having to
+   *       fetch the data-pipeling social data first
+   * @return string md5 checksum
+   */
+  public function toHash() {
+    return md5($this->method . $this->uri . $this->authType . $this->headers);
+  }
+
+  public static function getDefaultOptions() {
+    return new Options();
+  }
+
+  public function getContentType() {
+    return $this->contentType;
+  }
+
+  public function getHttpCode() {
+    return $this->httpCode;
+  }
+
+  public function getHttpCodeMsg() {
+    return $this->httpCodeMsg;
+  }
+
+  public function getResponseContent() {
+    return $this->responseContent;
+  }
+
+  public function getResponseHeaders() {
+    return $this->responseHeaders;
+  }
+
+  public function getResponseSize() {
+    return $this->responseSize;
+  }
+
+  public function getHeaders() {
+    return $this->headers;
+  }
+
+  public function isPost() {
+    return ($this->postBody != false);
+  }
+
+  public function hasHeaders() {
+    return ! empty($this->headers);
+  }
+
+  public function getPostBody() {
+    return $this->postBody;
+  }
+
+  public function getUrl() {
+    return $this->uri;
+  }
+
+  public function getNotSignedUrl() {
+    return $this->notSignedUri;
+  }
+
+  public function getMethod() {
+    if ($this->method) {
+      return $this->method;
+    }
+    if ($this->postBody) {
+      return 'POST';
+    } else {
+      return 'GET';
+    }
+  }
+
+  public function setMethod($method) {
+    $this->method = $method;
+  }
+
+  /**
+   * @return Options
+   */
+  public function getOptions() {
+    if (empty($this->options)) {
+      $this->options = new Options();
+    }
+    return $this->options;
+  }
+
+  public function setContentType($type) {
+    $this->contentType = $type;
+  }
+
+  public function setHttpCode($code) {
+    $this->httpCode = intval($code);
+  }
+
+  public function setHttpCodeMsg($msg) {
+    $this->httpCodeMsg = $msg;
+  }
+
+  public function setResponseContent($content) {
+    $this->responseContent = $content;
+  }
+
+  public function setResponseHeader($headerName, $headerValue) {
+    $this->responseHeaders[$headerName] = $headerValue;
+  }
+
+  public function setResponseHeaders($headers) {
+    $this->responseHeaders = $headers;
+  }
+
+  public function setResponseSize($size) {
+    $this->responseSize = intval($size);
+  }
+
+  public function setHeaders($headers) {
+    $this->headers = $headers;
+  }
+
+  //FIXME: Find a better way to do this
+  // The headers can be an array of elements.
+  public function getHeader($headerName) {
+    $headers = explode("\n", $this->headers);
+    foreach ($headers as $header) {
+      $key = explode(":", $header, 2);
+      if (strtolower(trim($key[0])) == strtolower($headerName)) return trim($key[1]);
+    }
+    return null;
+  }
+
+  public function getResponseHeader($headerName) {
+    return isset($this->responseHeaders[$headerName]) ? $this->responseHeaders[$headerName] : null;
+  }
+
+  public function getCreated() {
+    return $this->created;
+  }
+
+  public function setPostBody($postBody) {
+    $this->postBody = $postBody;
+  }
+
+  public function setUri($uri) {
+    $this->uri = $uri;
+  }
+
+  public function setNotSignedUri($uri) {
+    $this->notSignedUri = $uri;
+  }
+
+  public function setInvalidation($invalidation) {
+    $this->invalidation = $invalidation;
+  }
+
+  public function getInvalidation() {
+    return $this->invalidation;
+  }
+
+  /**
+   * Sets the security token to use (used if the request has authorization set (signed, oauth))
+   * @param SecurityToken $token
+   */
+  public function setToken($token) {
+    $this->token = $token;
+  }
+
+  /**
+   * Returns the SecurityToken for this request
+   *
+   * @return SecurityToken
+   */
+  public function getToken() {
+    return $this->token;
+  }
+
+  public function setOAuthRequestParams(OAuthRequestParams $params) {
+    $this->oauthParams = $params;
+  }
+
+  /**
+   * @return OAuthRequestParams
+   */
+  public function getOAuthRequestParams() {
+    return $this->oauthParams;
+  }
+
+  /**
+   * Sets the authorization type for this request, can be one of
+   * - none, no signing or authorization
+   * - signed, sign the request with an oauth_signature
+   * - oauth, logges in to the remote oauth service and uses it as base for signing the requests
+   *
+   * @param string $type ('none', 'signed', 'oauth', 'oauth2')
+   */
+  public function setAuthType($type) {
+    $this->authType = $type;
+  }
+
+  /**
+   * Returns the auth type of the request
+   *
+   * @return string ('none', 'signed', 'oauth')
+   */
+  public function getAuthType() {
+    return $this->authType;
+  }
+
+  /**
+   * Sets the cache refresh interval to use for this request
+   *
+   * @param int $refreshInterval (in seconds)
+   */
+  public function setRefreshInterval($refreshInterval) {
+    $this->refreshInterval = $refreshInterval;
+  }
+
+  /**
+   * Returns the cache's refresh interval for this request
+   *
+   * @return int refreshInterval (in seconds)
+   */
+  public function getRefreshInterval() {
+    return $this->refreshInterval;
+  }
+
+  public function setMetadata($key, $value) {
+    $this->metadata[$key] = $value;
+  }
+
+  public function getMetadatas() {
+    return $this->metadata;
+  }
+
+  public function isStrictNoCache() {
+    $cacheControl = $this->getResponseHeader('Cache-Control');
+    if ($cacheControl != null) {
+      $directives = explode(',', $cacheControl);
+      foreach ($directives as $directive) {
+        if (strcasecmp($directive, 'no-cache') == 0
+            || strcasecmp($directive, 'no-store') == 0
+            || strcasecmp($directive, 'private') == 0) {
+          return true;
+        }
+      }
+    }
+    $progmas = $this->getResponseHeader('Progma');
+    if ($progmas != null) {
+      foreach ($progmas as $progma) {
+        if (strcasecmp($progma, 'no-cache') == 0) {
+          return true;
+        }
+      }
+   }
+   return false;
+ }
+
+  /**
+   * transforms a possible relative url to a absolute url from the gadget xml root
+   * @param string $url
+   * @param string $gadgetUrl
+   * @return mixed url or false
+   */
+  public static function transformRelativeUrl($url, $gadgetUrl) {
+    $parsedUri = parse_url($url);
+    if (empty($parsedUri['host'])) {
+      // relative path's in the locale spec uri
+      // check against valid chars so that we can make sure that the given
+      // relative url is valid and does not try to fetch files outside of
+      // gadget scope (e.g. /../../../usr/bin... )
+      $pattern = '%^(([a-zA-Z0-9\-_](?<!\.)){1,2}([a-zA-Z0-9\.\-_](?<!\.\.))*/?)+$%';
+      if (preg_match($pattern, $url)) {
+        $gadgetUrl = substr($gadgetUrl, 0, strrpos($gadgetUrl, '/') + 1);
+        $url = $gadgetUrl . str_replace('..', '', $url);
+      } else {
+        return false;
+      }
+    }
+    return $url;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/RequestTime.php b/trunk/php/src/apache/shindig/common/RequestTime.php
new file mode 100644
index 0000000..a4a9ed4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/RequestTime.php
@@ -0,0 +1,28 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class RequestTime {
+
+  public function getRequestTime() {
+    return $_SERVER['REQUEST_TIME'];
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/common/SecurityToken.php b/trunk/php/src/apache/shindig/common/SecurityToken.php
new file mode 100644
index 0000000..dab9220
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/SecurityToken.php
@@ -0,0 +1,98 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * An abstract representation of a signing token.
+ * Use in conjunction with @code SecurityTokenDecoder.
+ */
+abstract class SecurityToken {
+
+  static public function createFromToken($token, $maxage) {}
+
+  static public function createFromValues($owner, $viewer, $app, $domain, $appUrl, $moduleId, $containerId) {}
+
+  static public $ANONYMOUS = '-1';
+
+  /**
+   * should return the actual raw token string from get, post or header
+   * 
+   * @return string
+   */
+  static public function getTokenStringFromRequest() {}
+
+  /**
+   * is this an anonymous token? Always check this before using the owner/viewer/etc
+   *
+   * @return boolean if it's anonymous
+   */
+  abstract public function isAnonymous();
+
+  /**
+   * Serializes the token into a string. This can be the exact same as
+   * toString; using a different name here is only to force interface
+   * compliance.
+   *
+   * @return A string representation of the token.
+   */
+  abstract public function toSerialForm();
+
+  /**
+   * @return the owner from the token, or null if there is none.
+   */
+  abstract public function getOwnerId();
+
+  /**
+   * @return the viewer from the token, or null if there is none.
+   */
+  abstract public function getViewerId();
+
+  /**
+   * @return the application id from the token, or null if there is none.
+   */
+  abstract public function getAppId();
+
+  /**
+   * @return the domain from the token, or null if there is none.
+   */
+  abstract public function getDomain();
+
+  /**
+   * @return the URL of the application
+   */
+  abstract public function getAppUrl();
+
+  /**
+   * @return the module ID of the application
+   */
+  abstract public function getModuleId();
+  
+  /**
+   * @return string
+   */
+  abstract public function getAuthenticationMode();
+
+  /**
+   * @param string $mode
+   */
+  abstract public function setAuthenticationMode($mode);
+}
diff --git a/trunk/php/src/apache/shindig/common/SecurityTokenDecoder.php b/trunk/php/src/apache/shindig/common/SecurityTokenDecoder.php
new file mode 100644
index 0000000..66d3686
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/SecurityTokenDecoder.php
@@ -0,0 +1,36 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ *  Handles verification of gadget security tokens.
+ */
+abstract class SecurityTokenDecoder {
+
+  /**
+   * Decrypts and verifies a gadget security token to return a gadget token.
+   * 
+   * @param string $tokenString String representation of the token to be created.
+   * @return The token representation of the input data.
+   * @throws GadgetException If tokenString is not a valid token
+   */
+  abstract public function createToken($tokenString);
+}
diff --git a/trunk/php/src/apache/shindig/common/ShindigOAuth.php b/trunk/php/src/apache/shindig/common/ShindigOAuth.php
new file mode 100644
index 0000000..c8dc07b
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ShindigOAuth.php
@@ -0,0 +1,41 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ShindigOAuth {
+  public static $VERSION_1_0 = "1.0";
+  public static $ENCODING = "UTF-8";
+  public static $FORM_ENCODED = "application/x-www-form-urlencoded";
+  public static $OAUTH_CONSUMER_KEY = "oauth_consumer_key";
+  public static $OAUTH_TOKEN = "oauth_token";
+  public static $OAUTH_TOKEN_SECRET = "oauth_token_secret";
+  public static $OAUTH_SIGNATURE_METHOD = "oauth_signature_method";
+  public static $OAUTH_SIGNATURE = "oauth_signature";
+  public static $OAUTH_TIMESTAMP = "oauth_timestamp";
+  public static $OAUTH_NONCE = "oauth_nonce";
+  public static $OAUTH_VERIFIER = "oauth_verifier";
+  public static $OAUTH_VERSION = "oauth_version";
+  public static $HMAC_SHA1 = "HMAC_SHA1";
+  public static $RSA_SHA1 = "RSA_SHA1";
+  public static $BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
+  public static $END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
+  public static $OAUTH_PROBLEM = "oauth_problem";
+}
diff --git a/trunk/php/src/apache/shindig/common/ShindigOAuthNoDataException.php b/trunk/php/src/apache/shindig/common/ShindigOAuthNoDataException.php
new file mode 100644
index 0000000..4a21413
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ShindigOAuthNoDataException.php
@@ -0,0 +1,23 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ShindigOAuthNoDataException extends \Exception {}
diff --git a/trunk/php/src/apache/shindig/common/ShindigOAuthProblemException.php b/trunk/php/src/apache/shindig/common/ShindigOAuthProblemException.php
new file mode 100644
index 0000000..32e503e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ShindigOAuthProblemException.php
@@ -0,0 +1,23 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ShindigOAuthProblemException extends \Exception {}
diff --git a/trunk/php/src/apache/shindig/common/ShindigOAuthProtocolException.php b/trunk/php/src/apache/shindig/common/ShindigOAuthProtocolException.php
new file mode 100644
index 0000000..13e726c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ShindigOAuthProtocolException.php
@@ -0,0 +1,23 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ShindigOAuthProtocolException extends \Exception {}
diff --git a/trunk/php/src/apache/shindig/common/ShindigOAuthRequest.php b/trunk/php/src/apache/shindig/common/ShindigOAuthRequest.php
new file mode 100644
index 0000000..c82bf4d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ShindigOAuthRequest.php
@@ -0,0 +1,107 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ShindigOAuthRequest extends \OAuthRequest {
+  /**
+   * Needed so that OAuthFetcher works with the correct type of requests.
+   *
+   * @param OAuthRequest $request
+   */
+  public function __construct(\OAuthRequest $request) {
+    $this->parameters = $request->parameters;
+    $this->http_url = $request->http_url;
+    $this->http_method = $request->http_method;
+  }
+
+  /**
+   * Needed so that OAuthFetcher works with the correct type of requests.
+   * @return ShindigOAuthRequest
+   */
+  public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
+    return new ShindigOAuthRequest(\OAuthRequest::from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters));
+  }
+
+  /**
+   * Needed so that OAuthFetcher works with the correct type of requests.
+   * @return ShindigOAuthRequest
+   */
+  public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
+    return new ShindigOAuthRequest(\OAuthRequest::from_request($http_method, $http_url, $parameters));
+  }
+
+  /**
+   * Needed in OAuthFetcher.php
+   *
+   * @param array $params
+   * @return array
+   */
+  public function set_parameters($params) {
+    return $this->parameters = $params;
+  }
+
+  /**
+   * Needed in OAuthFetcher.php
+   *
+   * @param array $names
+   */
+  public function requireParameters($names) {
+    $present = $this->parameters;
+    $absent = array();
+    foreach ($names as $required) {
+      if (! in_array($required, $present)) {
+        $absent[] = $required;
+      }
+    }
+    if (count($absent) == 0) {
+      throw new ShindigOAuthProblemException("oauth_parameters_absent: " . ShindigOAuthUtil::urlencodeRFC3986($absent));
+    }
+  }
+
+  /**
+   * Needed in OAuthFetcher.php
+   *
+   * @return string
+   */
+  public function get_url() {
+    return $this->http_url;
+  }
+
+  /**
+   * Needed in SigningFetcher.php
+   *
+   * @return string
+   */
+  public static function generate_nonce() {
+    $mt = microtime();
+    $rand = mt_rand();
+    return md5($mt . $rand); // md5s look nicer than numbers
+  }
+
+  /**
+   * Needed for from_consumer_and_token
+   *
+   * @return int
+   */
+  private static function generate_timestamp() {
+    return time();
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/common/ShindigOAuthUtil.php b/trunk/php/src/apache/shindig/common/ShindigOAuthUtil.php
new file mode 100644
index 0000000..ba09fc0
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ShindigOAuthUtil.php
@@ -0,0 +1,132 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ShindigOAuthUtil extends \OAuthUtil {
+  public static $AUTH_SCHEME = "OAuth";
+  private static $AUTHORIZATION = "\ *[a-zA-Z0-9*]\ +(.*)";
+  private static $NVP = "(\\S*)\\s*\\=\\s*\"([^\"]*)\"";
+
+
+  /**
+   * Needed in OAuthFetcher.php
+   *
+   * @param array $params
+   * @return string
+   */
+  public static function getPostBodyString(Array $params) {
+    $result = '';
+    $first = true;
+    foreach ($params as $key => $val) {
+      if ($first) {
+        $first = false;
+      } else {
+        $result .= '&';
+      }
+      $result .= ShindigOAuthUtil::urlencode_rfc3986($key) . "=" . ShindigOAuthUtil::urlencode_rfc3986($val);
+    }
+    return $result;
+  }
+
+  /**
+   * Needed in OAuthFetcher.php
+   * Return true if the given Content-Type header means FORM_ENCODED.
+   *
+   * @param string $contentType
+   * @return boolean
+   */
+  public static function isFormEncoded($contentType) {
+    if (! isset($contentType)) {
+      return false;
+    }
+    $semi = strpos($contentType, ";");
+    if ($semi != false) {
+      $contentType = substr($contentType, 0, $semi);
+    }
+    return strtolower(ShindigOAuth::$FORM_ENCODED) == strtolower(trim($contentType));
+  }
+
+  /**
+   * Needed in OAuthFetcher.php
+   *
+   * @param string $url
+   * @param array $oauthParams
+   * @return string
+   */
+  public static function addParameters($url, $oauthParams) {
+    $url .= strchr($url, '?') === false ? '?' : '&';
+    foreach ($oauthParams as $key => $value) {
+      $url .= ShindigOAuthUtil::urlencode_rfc3986($key)."=".ShindigOAuthUtil::urlencode_rfc3986($value)."&";
+    }
+    return $url;
+  }
+
+  /**
+   * Needed in OAuthFetcher.php
+   *
+   * @param string $form
+   * @return array
+   */
+  public static function decodeForm($form) {
+    $parameters = array();
+    $explodedForm = explode("&", $form);
+    foreach ($explodedForm as $params) {
+      $value = explode("=", $params);
+      if (! empty($value[0]) && ! empty($value[1])) {
+        $parameters[ShindigOAuthUtil::urldecode_rfc3986($value[0])] = ShindigOAuthUtil::urldecode_rfc3986($value[1]);
+      }
+    }
+    return $parameters;
+  }
+
+  /**
+   * Needed in OAuthFetcher.php
+   *
+   * Parse the parameters from an OAuth Authorization or WWW-Authenticate
+   * header. The realm is included as a parameter. If the given header doesn't
+   * start with "OAuth ", return an empty list.
+   *
+   * @param string $authorization
+   * @return array
+   */
+  public static function decodeAuthorization($authorization) {
+    $into = array();
+    if ($authorization != null) {
+      $m = ereg(ShindigOAuthUtil::$AUTHORIZATION, $authorization);
+      if ($m !== false) {
+        if (strpos($authorization, ShindigOAuthUtil::$AUTH_SCHEME) == 0) {
+          $authorization = str_replace("OAuth ", "", $authorization);
+          $authParams = explode(", ", $authorization);
+          foreach ($authParams as $params) {
+            $m = ereg(ShindigOAuthUtil::$NVP, $params);
+            if ($m == 1) {
+              $keyValue = explode("=", $params);
+              $name = ShindigOAuthUtil::urlencode_rfc3986($keyValue[0]);
+              $value = ShindigOAuthUtil::urlencode_rfc3986(str_replace("\"", "", $keyValue[1]));
+              $into[$name] = $value;
+            }
+          }
+        }
+      }
+    }
+    return $into;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/ShindigRsaSha1SignatureMethod.php b/trunk/php/src/apache/shindig/common/ShindigRsaSha1SignatureMethod.php
new file mode 100644
index 0000000..3f5c088
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/ShindigRsaSha1SignatureMethod.php
@@ -0,0 +1,39 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ShindigRsaSha1SignatureMethod extends \OAuthSignatureMethod_RSA_SHA1 {
+  private $privateKey;
+  private $publicKey;
+
+  public function __construct($privateKey, $publicKey) {
+    $this->privateKey = $privateKey;
+    $this->publicKey = $publicKey;
+  }
+
+  protected function fetch_public_cert(&$request) {
+    return $this->publicKey;
+  }
+
+  protected function fetch_private_cert(&$request) {
+    return $this->privateKey;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/XmlError.php b/trunk/php/src/apache/shindig/common/XmlError.php
new file mode 100644
index 0000000..c226a18
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/XmlError.php
@@ -0,0 +1,94 @@
+<?php
+namespace apache\shindig\common;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * Misc class to parse and clear libxml2 based errors and returns a formatted and explanatory
+ * error string.
+ */
+class XmlError {
+
+  /**
+   *
+   * @param string $xml
+   * @return string
+   */
+  static public function getErrors($xml = false) {
+    $errors = libxml_get_errors();
+    $ret = '';
+    if ($xml) {
+      $xml = explode("\n", $xml);
+    }
+    foreach ($errors as $error) {
+      $ret .= self::parseXmlError($error, $xml);
+    }
+    libxml_clear_errors();
+    return $ret;
+  }
+
+  /**
+   *
+   * @param object $error
+   * @param array $xml
+   * @return string
+   */
+  static public function parseXmlError($error, $xml) {
+    if ($xml) {
+      $ret = $xml[$error->line - 1] . "\n";
+      $ret .= str_repeat('-', $error->column) . "^\n";
+    }
+    switch ($error->level) {
+      case LIBXML_ERR_WARNING:
+        $ret .= "Warning $error->code: ";
+        break;
+      case LIBXML_ERR_ERROR:
+        $ret .= "Error $error->code: ";
+        break;
+      case LIBXML_ERR_FATAL:
+        $ret .= "Fatal Error $error->code: ";
+        break;
+    }
+    $ret .= trim($error->message) . "\n  Line: $error->line" . "\n  Column: $error->column\n\n";
+    return $ret;
+  }
+
+  /**
+   * Generic misc debugging function to dump a node's structure as plain text xml
+   *
+   * @param DOMElement $node
+   * @param string $function
+   */
+  public function dumpNode($node, $function) {
+    $doc = new \DOMDocument(null, 'utf-8');
+    $doc->preserveWhiteSpace = true;
+    $doc->formatOutput = false;
+    $doc->strictErrorChecking = false;
+    $doc->recover = false;
+    $doc->resolveExternals = false;
+    if (! $newNode = @$doc->importNode($node, false)) {
+      echo "[Invalid node, dump failed]<br><br>";
+      return;
+    }
+    $doc->appendChild($newNode);
+    echo "<b>$function (" . get_class($node) . "):</b><br>" . htmlentities(str_replace('<?xml version="" encoding="utf-8"?>', '', $doc->saveXML()) . "\n") . "<br><br>";
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicBlobCrypter.php b/trunk/php/src/apache/shindig/common/sample/BasicBlobCrypter.php
new file mode 100644
index 0000000..c10170a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicBlobCrypter.php
@@ -0,0 +1,163 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\common\BlobCrypter;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * This class provides basic binary blob encryption and decryption, for use with the security token
+ *
+ */
+class BasicBlobCrypter extends BlobCrypter {
+  //FIXME make this compatible with the java's blobcrypter
+
+
+  // Labels for key derivation
+  protected $CIPHER_KEY_LABEL = 0;
+  protected $HMAC_KEY_LABEL = 1;
+
+  /** Key used for time stamp (in seconds) of data */
+  public $TIMESTAMP_KEY = "t";
+
+  /** minimum length of master key */
+  public $MASTER_KEY_MIN_LEN = 16;
+
+  /** allow three minutes for clock skew */
+  protected $CLOCK_SKEW_ALLOWANCE = 180;
+
+  protected $UTF8 = "UTF-8";
+
+  protected $cipherKey;
+  protected $hmacKey;
+  protected $allowPlaintextToken;
+
+  public function __construct() {
+    $this->cipherKey = Config::get('token_cipher_key');
+    $this->hmacKey = Config::get('token_hmac_key');
+    $this->allowPlaintextToken = Config::get('allow_plaintext_token');
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function wrap(Array $in) {
+    $encoded = $this->serializeAndTimestamp($in);
+    if (! function_exists('mcrypt_module_open') && $this->allowPlaintextToken) {
+      $cipherText = base64_encode($encoded);
+    } else {
+      $cipherText = Crypto::aes128cbcEncrypt($this->cipherKey, $encoded);
+    }
+    $hmac = Crypto::hmacSha1($this->hmacKey, $cipherText);
+    $b64 = base64_encode($cipherText . $hmac);
+    return $b64;
+  }
+
+  /**
+   *
+   * @param array $in
+   * @return string
+   */
+  protected function serializeAndTimestamp(Array $in) {
+    $encoded = "";
+    foreach ($in as $key => $val) {
+      $encoded .= urlencode($key) . "=" . urlencode($val) . "&";
+    }
+    $encoded .= $this->TIMESTAMP_KEY . "=" . time();
+    return $encoded;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function unwrap($in, $maxAgeSec) {
+    //TODO remove this once we have a better way to generate a fake token in the example files
+    if ($this->allowPlaintextToken && count(explode(':', $in)) >= 7) {
+      //Parses the security token in the form st=o:v:a:d:u:m:c
+      $data = $this->parseToken($in);
+      $out = array();
+      $out['o'] = $data[0];
+      $out['v'] = $data[1];
+      $out['a'] = $data[2];
+      $out['d'] = $data[3];
+      $out['u'] = $data[4];
+      $out['m'] = $data[5];
+    } else {
+      $bin = base64_decode($in);
+      if (is_callable('mb_substr')) {
+        $cipherText = mb_substr($bin, 0, - Crypto::$HMAC_SHA1_LEN, 'latin1');
+        $hmac = mb_substr($bin, mb_strlen($cipherText, 'latin1'), Crypto::$HMAC_SHA1_LEN, 'latin1');
+      } else {
+        $cipherText = substr($bin, 0, - Crypto::$HMAC_SHA1_LEN);
+        $hmac = substr($bin, strlen($cipherText));
+      }
+      Crypto::hmacSha1Verify($this->hmacKey, $cipherText, $hmac);
+      if (! function_exists('mcrypt_module_open') && $this->allowPlaintextToken) {
+        $plain = base64_decode($cipherText);
+      } else {
+        $plain = Crypto::aes128cbcDecrypt($this->cipherKey, $cipherText);
+      }
+      $out = $this->deserialize($plain);
+      $this->checkTimestamp($out, $maxAgeSec);
+    }
+    return $out;
+  }
+
+  /**
+   * Parses the security token
+   * @param string $stringToken
+   * @return array
+   */
+  protected function parseToken($stringToken) {
+    $data = explode(":", $stringToken);
+       $url_number = count($data)-6;
+
+	//get array elements conrresponding to broken url - http://host:port/gadget.xml -> ["http","//host","port/gadget.xml"]
+	$url_array = array_slice($data,4,$url_number) ;
+	$url = implode(":",$url_array);
+	array_splice($data,4,$url_number,$url);
+    return $data;
+  }
+
+  /**
+   * @param string $plain
+   * @return array
+   */
+  protected function deserialize($plain) {
+    $map = array();
+    parse_str($plain, $map);
+    return $map;
+  }
+
+  /**
+   *
+   * @param array $out
+   * @param int $maxAge
+   * @throws BlobExpiredException
+   */
+  protected function checkTimestamp(Array $out, $maxAge) {
+    $minTime = (int)$out[$this->TIMESTAMP_KEY] - $this->CLOCK_SKEW_ALLOWANCE;
+    $maxTime = (int)$out[$this->TIMESTAMP_KEY] + $maxAge + $this->CLOCK_SKEW_ALLOWANCE;
+    $now = time();
+    if (! ($minTime < $now && $now < $maxTime)) {
+      throw new BlobExpiredException("Security token expired");
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicOAuthDataStore.php b/trunk/php/src/apache/shindig/common/sample/BasicOAuthDataStore.php
new file mode 100644
index 0000000..ef4d13d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicOAuthDataStore.php
@@ -0,0 +1,57 @@
+<?php
+namespace apache\shindig\common\sample;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * Primitive OAuth backing store that doesn't do much.
+ */
+class BasicOAuthDataStore extends \OAuthDataStore {
+
+  function lookup_consumer($consumer_key) {
+    return new \OAuthConsumer($consumer_key, "fake-consumer-secret");
+  }
+
+  function lookup_token($consumer, $token_type, $token) {
+    if ($token_type == "request") {
+      return new \OAuthToken($token, "fake-request-secret");
+    } elseif ($token_type == "access") {
+      return new \OAuthToken($token, "fake-access-secret");
+    } else {
+      throw new \OAuthException("unexpected token type: $token_type");
+    }
+  }
+
+  function lookup_nonce($consumer, $token, $nonce, $timestamp) {
+    return false; // pretend we've always never seen this nonce 
+  }
+
+  function new_request_token($consumer) {
+    return new \OAuthToken("fake-request-token", "fake-request-secret");
+  }
+
+  function new_access_token($oauthToken, $consumer) {
+    return new \OAuthToken("fake-access-token", "fake-access-secret");
+  }
+
+  function authorize_request_token($token) {  // mark the given request token as having been authorized by the user
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicOAuthLookupService.php b/trunk/php/src/apache/shindig/common/sample/BasicOAuthLookupService.php
new file mode 100644
index 0000000..9f0958a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicOAuthLookupService.php
@@ -0,0 +1,46 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\social\oauth\OAuthSecurityToken;
+use apache\shindig\common\OAuthLookupService;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * Basic implementation of OAuthLookupService
+ */
+class BasicOAuthLookupService extends OAuthLookupService {
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getSecurityToken($oauthRequest, $appUrl, $userId, $contentType) {
+    return new OAuthSecurityToken($userId, $appUrl, $this->getAppId($appUrl), "samplecontainer");
+  }
+
+  /**
+   *
+   * @param string $appUrl
+   * @return int
+   */
+  private function getAppId($appUrl) {
+    return 0; // a real implementation would look this up
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicRemoteContent.php b/trunk/php/src/apache/shindig/common/sample/BasicRemoteContent.php
new file mode 100644
index 0000000..4f298be
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicRemoteContent.php
@@ -0,0 +1,278 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\social\sample\DefaultInvalidateService;
+use apache\shindig\common\RemoteContentException;
+use apache\shindig\gadgets\oauth\OAuthFetcherFactory;
+use apache\shindig\common\RemoteContent;
+use apache\shindig\common\Cache;
+use apache\shindig\common\Config;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\RemoteContentFetcher;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class BasicRemoteContent extends RemoteContent {
+  /**
+   * @var BesicRemoteContentFetcher
+   */
+  private $basicFetcher = null;
+
+  /**
+   * @var SigningFetcherFactory
+   */
+  private $signingFetcherFactory = null;
+
+  /**
+   * @var SecurityTokenDecoder
+   */
+  private $signer = null;
+
+  /**
+   * @var Cache
+   */
+  private $cache = null;
+
+  /**
+   * @var InvalidateService
+   */
+  private $invalidateService = null;
+
+  /**
+   * @var boolean cachePostRequest
+   */
+  private $cachePostRequest = false;
+
+  /**
+   * @param RemoteContentFetcher $basicFetcher
+   * @param SigningFetcherFactory $signingFetcherFactory
+   * @param SecurityTokenDecoder $signer
+   */
+  public function __construct(RemoteContentFetcher $basicFetcher = null, $signingFetcherFactory = null, $signer = null) {
+    $this->basicFetcher = $basicFetcher ? $basicFetcher : new BasicRemoteContentFetcher();
+    $this->signingFetcherFactory = $signingFetcherFactory;
+    $this->signer = $signer;
+    $this->cache = Cache::createCache(Config::get('data_cache'), 'RemoteContent');
+    $this->invalidateService = new DefaultInvalidateService($this->cache);
+  }
+
+  /**
+   *
+   * @param RemoteContentFetcher $basicFetcher 
+   */
+  public function setBasicFetcher(RemoteContentFetcher $basicFetcher) {
+    $this->basicFetcher = $basicFetcher;
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  public function fetch(RemoteContentRequest $request) {
+    $ignoreCache = $request->getOptions()->ignoreCache;
+    if (! $ignoreCache && ($this->cachePostRequest || ! $request->isPost()) && ($cachedRequest = $this->cache->get($request->toHash())) !== false && $this->invalidateService->isValid($cachedRequest)) {
+      $response = $cachedRequest;
+    } else {
+      $originalRequest = clone $request;
+      $response = $this->divertFetch($request);
+      if ($response->getHttpCode() != 200 && ! $ignoreCache && ($this->cachePostRequest || ! $originalRequest->isPost())) {
+        $cachedRequest = $this->cache->expiredGet($originalRequest->toHash());
+        if ($cachedRequest['found'] == true) {
+          return $cachedRequest['data'];
+        }
+      }
+      $this->setRequestCache($originalRequest, $response, $this->cache);
+    }
+    return $response;
+  }
+
+  /**
+   *
+   * @param array $requests
+   * @return array 
+   */
+  public function multiFetch(Array $requests) {
+    $rets = array();
+    $requestsToProc = array();
+    foreach ($requests as $request) {
+      if (! ($request instanceof RemoteContentRequest)) {
+        throw new RemoteContentException("Invalid request type in remoteContent");
+      }
+      $ignoreCache = $request->getOptions()->ignoreCache;
+      // determine which requests we can load from cache, and which we have to actually fetch
+      if (! $ignoreCache && ($this->cachePostRequest || ! $request->isPost()) && ($cachedRequest = $this->cache->get($request->toHash())) !== false && $this->invalidateService->isValid($cachedRequest)) {
+        $rets[] = $cachedRequest;
+      } else {
+        $originalRequest = clone $request;
+        $requestsToProc[] = $request;
+        $originalRequestArray[] = $originalRequest;
+      }
+    }
+    if ($requestsToProc) {
+      $normal = array();
+      $signing = array();
+      foreach ($requestsToProc as $request) {
+        switch ($request->getAuthType()) {
+          case RemoteContentRequest::$AUTH_SIGNED:
+            $signing[] = $request;
+            break;
+          case RemoteContentRequest::$AUTH_OAUTH:
+            // We do not allow multi fetch oauth content.
+            break;
+          default:
+            $normal[] = $request;
+        }
+      }
+      if ($signing) {
+        $signingFetcher = $this->signingFetcherFactory->getSigningFetcher($this->basicFetcher);
+        $signingFetcher->multiFetchRequest($signing);
+      }
+      if ($normal) {
+        $this->basicFetcher->multiFetchRequest($normal);
+      }
+      foreach ($requestsToProc as $request) {
+        list(, $originalRequest) = each($originalRequestArray);
+        $ignoreCache = $request->getOptions()->ignoreCache;
+        if ($request->getHttpCode() != 200 && ! $ignoreCache && ($this->cachePostRequest || ! $request->isPost())) {
+          $cachedRequest = $this->cache->expiredGet($request->toHash());
+          if ($cachedRequest['found'] == true) {
+            $rets[] = $cachedRequest['data'];
+          }
+        } else {
+          $this->setRequestCache($originalRequest, $request, $this->cache);
+          $rets[] = $request;
+        }
+      }
+    }
+    return $rets;
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $request 
+   */
+  public function invalidate(RemoteContentRequest $request) {
+    $this->cache->invalidate($request->toHash());
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $originalRequest
+   * @param RemoteContentRequest $request
+   * @param Cache $cache
+   */
+  private function setRequestCache(RemoteContentRequest $originalRequest, RemoteContentRequest $request, Cache $cache) {
+    if ($request->isStrictNoCache()) {
+      return;
+    }
+    $ignoreCache = $originalRequest->getOptions()->ignoreCache;
+    if (($this->cachePostRequest || ! $request->isPost()) && ! $ignoreCache) {
+      $ttl = Config::get('cache_time');
+      if ((int)$request->getHttpCode() == 200) {
+        // Got a 200 OK response, calculate the TTL to use for caching it
+        if (($expires = $request->getResponseHeader('Expires')) != null) {
+          // prefer to use the servers notion of the time since there could be a clock-skew, but otherwise use our own
+          $date = $request->getResponseHeader('Date') != null ? $request->getResponseHeader('Date') : gmdate('D, d M Y H:i:s', $_SERVER['REQUEST_TIME']) . ' GMT';
+          // convert both dates to unix epoch seconds, and calculate the TTL
+          date_default_timezone_set('GMT');
+          $date = strtotime($date);
+          $expires = strtotime($expires);
+          $ttl = $expires - $date;
+          // Don't fall for the old expiration-date-in-the-past trick, we *really* want to cache stuff since a large SNS's traffic would devastate a gadget developer's server
+          if ($expires - $date > 1) {
+            $ttl = $expires - $date;
+          }
+        }
+        // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html : The Cache-Control: max-age=<seconds> overrides the expires header, so if both are present this one will overwrite the $ttl
+        if (($cacheControl = $request->getResponseHeader('Cache-Control')) != null) {
+          $bits = explode('=', $cacheControl);
+          foreach ($bits as $key => $val) {
+            if ($val == 'max-age' && isset($bits[$key + 1])) {
+              $ttl = $bits[$key + 1];
+              break;
+            }
+          }
+        }
+      } else {
+        $ttl = 5 * 60; // cache errors for 5 minutes, takes the denial of service attack type behaviour out of having an error :)
+      }
+      $this->invalidateService->markResponse($request);
+      $this->cache->set($originalRequest->toHash(), $request, $ttl);
+    }
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  private function divertFetch(RemoteContentRequest $request) {
+    switch ($request->getAuthType()) {
+      case RemoteContentRequest::$AUTH_SIGNED:
+        $fetcher = $this->signingFetcherFactory->getSigningFetcher($this->basicFetcher);
+        return $fetcher->fetchRequest($request);
+      case RemoteContentRequest::$AUTH_OAUTH:
+      case RemoteContentRequest::$AUTH_OAUTH2:
+        $params = $request->getOAuthRequestParams();
+        $token = $request->getToken();
+        $fetcher = $this->signingFetcherFactory->getSigningFetcher($this->basicFetcher);
+        $oAuthFetcherFactory = new OAuthFetcherFactory($fetcher);
+        $oauthFetcher = $oAuthFetcherFactory->getOAuthFetcher($fetcher, $token, $params, $request->getAuthType());
+        return $oauthFetcher->fetch($request);
+      default:
+        return $this->basicFetcher->fetchRequest($request);
+    }
+  }
+
+  /**
+   * Returns the cached request, or false if there's no cached copy of this request, ignoreCache = true or if it's invalidated
+   *
+   * @param RemoteContentRequest $request
+   * @return mixed
+   */
+  public function getCachedRequest(RemoteContentRequest $request) {
+    $ignoreCache = $request->getOptions()->ignoreCache;
+    if (! $ignoreCache && ($this->cachePostRequest || ! $request->isPost()) && ($cachedRequest = $this->cache->get($request->toHash())) !== false && $this->invalidateService->isValid($cachedRequest)) {
+      return $cachedRequest;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Set wether or not POST requests should be cached, this is not something that you would usually
+   * do since it's not http spec compliant, however proxied content requests are cached even if
+   * social data is post'd to the gadget's url.
+   *
+   * @param boolean $cachePostRequest
+   */
+  public function setCachePostRequest($cachePostRequest = false) {
+    $this->cachePostRequest = $cachePostRequest;
+  }
+
+  /**
+   * Returns the current cachePostRequest value
+   *
+   * @return boolean $cachePostRequest
+   */
+  public function getCachePostRequest() {
+    return $this->cachePostRequest;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicRemoteContentFetcher.php b/trunk/php/src/apache/shindig/common/sample/BasicRemoteContentFetcher.php
new file mode 100644
index 0000000..6fef53f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicRemoteContentFetcher.php
@@ -0,0 +1,291 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\common\RemoteContentFetcher;
+use apache\shindig\common\Config;
+use apache\shindig\common\RemoteContentRequest;
+
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Basic remote content fetcher, uses curl_multi to fetch multiple resources at the same time
+ */
+class BasicRemoteContentFetcher extends RemoteContentFetcher {
+  private $requests = array();
+  private $disallowedHeaders = array('Keep-Alive', 'Host', 'Accept-Encoding', 'Set-Cookie', 'Content-Length', 'Content-Encoding', 'ETag', 'Last-Modified', 'Accept-Ranges', 'Vary', 'Expires', 'Date', 'Pragma', 'Cache-Control', 'Transfer-Encoding', 'If-Modified-Since');
+  const USER_AGENT = 'Apache Shindig';
+
+  /**
+   * Performs a single (RemoteContentRequest) request and fills in the response
+   * in the $request object
+   *
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest $request
+   */
+  public function fetchRequest(RemoteContentRequest $request) {
+    $request->handle = $this->initCurlHandle($request->getUrl());
+    $this->setHeaders($request);
+    // Execute the request
+    $content = @curl_exec($request->handle);
+    $this->parseResult($request, $content);
+    curl_close($request->handle);
+    unset($request->handle);
+    return $request;
+  }
+
+  /**
+   * Performs multiple (array of RemoteContentRequest) requests and fills in the responses
+   * in the $request objects
+   *
+   * @param Array of RemoteContentRequest's $requests
+   * @return array $requests
+   */
+  public function multiFetchRequest(Array $requests) {
+    $mh = curl_multi_init();
+    foreach ($requests as $request) {
+      $request->handle = $this->initCurlHandle($request->getUrl());
+      // Set this so the multihandler will return data
+      curl_setopt($request->handle, CURLOPT_RETURNTRANSFER, 1);
+      $this->setHeaders($request);
+      curl_multi_add_handle($mh, $request->handle);
+    }
+    $running = null;
+    do {
+      curl_multi_exec($mh, $running);
+    } while ($running > 0);
+    foreach ($requests as $request) {
+      // Execute the request
+      $content = curl_multi_getcontent($request->handle);
+      $this->parseResult($request, $content);
+      curl_multi_remove_handle($mh, $request->handle);
+      unset($request->handle);
+    }
+    curl_multi_close($mh);
+    unset($mh);
+    return $requests;
+  }
+
+  /**
+   * Parses the result content into the headers and body, and retrieves the http code and content type
+   *
+   * @param RemoteContentRequest $request
+   * @param string $content
+   */
+  protected function parseResult(RemoteContentRequest $request, $content) {
+    $headers = '';
+    $body = '';
+    $httpCode = curl_getinfo($request->handle, CURLINFO_HTTP_CODE);
+    $contentType = curl_getinfo($request->handle, CURLINFO_CONTENT_TYPE);
+    // Attempt to magically convert all text'ish responses to UTF8, especially the xml and json parsers get upset if invalid UTF8 is encountered
+    $textTypes = array('text', 'html', 'json', 'xml', 'atom');
+    $isTextType = false;
+    $isXml = false;
+    foreach ($textTypes as $textType) {
+      if (strpos($contentType, $textType) !== false) {
+        if ($textType === 'xml') {
+          $isXml = true;
+        }
+        $isTextType = true;
+    	break;
+      }
+    }
+    if ($isTextType && function_exists('mb_convert_encoding')) {
+      // try to retrieve content type out of
+      $charset = 'UTF-8';
+      $matchedCharset = array();
+      if (0 != preg_match("/charset\s*=\s*([^\"' >]*)/ix",$content, $matchedCharset) || //http header or html meta tags
+          0 != preg_match("/encoding\s*=\s*[\'\"]([^\"' >]*)/ix",$content, $matchedCharset)) { //xml declaration
+        if (trim($matchedCharset[1])) {
+   		  $charset = trim($matchedCharset[1]);
+   		  if (($pos = strpos($charset, "\n")) !== false) {
+   		    $charset = trim(substr($charset, 0, $pos));
+   		  }
+        }
+   	  }
+   	  // the xml and json parsers get very upset if there are invalid UTF8 sequences in the string, by recoding it any bad chars will be filtered out
+      $content = mb_convert_encoding($content, 'UTF-8', $charset);
+      // if original charset is not utf-8 we now try to rewrite any xml declarations
+      if ($isXml === true && strtoupper($charset) !== 'UTF-8') {
+        $pattern =  'encoding=\s*([\'"])' . $charset . '\s*\1';
+        $content = mb_ereg_replace($pattern, 'encoding="UTF-8"', $content, "i");
+      }
+  	}
+    // on redirects and such we get multiple headers back from curl it seems, we really only want the last one
+    while (substr($content, 0, strlen('HTTP')) == 'HTTP' && strpos($content, "\r\n\r\n") !== false) {
+      $headers = substr($content, 0, strpos($content, "\r\n\r\n"));
+      $content = $body = substr($content, strlen($headers) + 4);
+    }
+    $headers = explode("\n", $headers);
+    $parsedHeaders = array();
+    foreach ($headers as $header) {
+      if (strpos($header, ':')) {
+        $key = trim(substr($header, 0, strpos($header, ':')));
+        $key = str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)));
+        $val = trim(substr($header, strpos($header, ':') + 1));
+        $parsedHeaders[$key] = $val;
+      }
+    }
+    if (! $httpCode) {
+      $httpCode = '404';
+    }
+
+    if (curl_errno($request->handle)) {
+      $httpCode = '500';
+      $body = 'Curl error: ' . curl_error($request->handle);
+    }
+
+    $request->setHttpCode($httpCode);
+    $request->setHttpCodeMsg($this->resolveHttpCode($httpCode));
+    $request->setContentType($contentType);
+    $request->setResponseHeaders($parsedHeaders);
+    $request->setResponseContent($body);
+    $request->setResponseSize(strlen($content));
+  }
+
+  /**
+   * Misc function to resolve http status codes to a matching http code message
+   * since curl strips those, but we do need'm in the proxy handler
+   * @param $httpCode
+   * @return string
+   */
+  private function resolveHttpCode($httpCode) {
+    switch ((int)$httpCode) {
+      case 100: return "Continue";
+      case 101: return "Switching Protocols";
+      case 200: return "OK";
+      case 201: return "Created";
+      case 202: return "Accepted";
+      case 203: return "Non-Authoritative Information";
+      case 204: return "No Content";
+      case 205: return "Reset Content";
+      case 206: return "Partial Content";
+      case 300: return "Multiple Choices";
+      case 301: return "Moved Permanently";
+      case 302: return "Found";
+      case 303: return "See Other";
+      case 304: return "Not Modified";
+      case 305: return "Use Proxy";
+      case 306: return "(Unused)";
+      case 307: return "Temporary Redirect";
+      case 400: return "Bad Request";
+      case 401: return "Unauthorized";
+      case 402: return "Payment Required";
+      case 403: return "Forbidden";
+      case 404: return "Not Found";
+      case 405: return "Method Not Allowed";
+      case 406: return "Not Acceptable";
+      case 407: return "Proxy Authentication Required";
+      case 408: return "Request Timeout";
+      case 409: return "Conflict";
+      case 410: return "Gone";
+      case 411: return "Length Required";
+      case 412: return "Precondition Failed";
+      case 413: return "Request Entity Too Large";
+      case 414: return "Request-URI Too Long";
+      case 415: return "Unsupported Media Type";
+      case 416: return "Requested Range Not Satisfiable";
+      case 417: return "Expectation Failed";
+      case 500: return "Internal Server Error";
+      case 501: return "Not Implemented";
+      case 502: return "Bad Gateway";
+      case 503: return "Service Unavailable";
+      case 504: return "Gateway Timeout";
+      case 505: return "HTTP Version Not Supported";
+      default : return "Unknown Error";
+    }
+  }
+
+  /**
+   * Sets the headers and post body for the request if they are specified
+   *
+   * @param RemoteContentRequest $request
+   */
+  private function setHeaders(RemoteContentRequest $request) {
+    if ($request->hasHeaders()) {
+      $headers = explode("\n", $request->getHeaders());
+      $outHeaders = array();
+      foreach ($headers as $header) {
+        if (strpos($header, ':')) {
+          $key = trim(substr($header, 0, strpos($header, ':')));
+          $key = str_replace(' ', '-', ucwords(str_replace('-', ' ', $key)));
+          $val = trim(substr($header, strpos($header, ':') + 1));
+          if (! in_array($key, $this->disallowedHeaders)) {
+            $outHeaders[] = "$key: $val";
+          }
+        }
+      }
+      $outHeaders[] = "User-Agent: " . BasicRemoteContentFetcher::USER_AGENT;
+      curl_setopt($request->handle, CURLOPT_HTTPHEADER, $outHeaders);
+    }
+    $method = $request->getMethod();
+    if ($request->isPost()) {
+      curl_setopt($request->handle, CURLOPT_POST, 1);
+      curl_setopt($request->handle, CURLOPT_POSTFIELDS, $request->getPostBody());
+    } else if ($method == 'DELETE' || $method == 'HEAD' || $method == 'PUT') {
+      curl_setopt($request->handle, CURLOPT_CUSTOMREQUEST, $method);
+      if ($method == "PUT") {
+        curl_setopt($request->handle, CURLOPT_POSTFIELDS, $request->getPostBody());
+      }
+    }
+  }
+
+  /**
+   * Initializes a curl handle for making a request
+   * This will set the timeout based on the 'curl_connection_timeout configuration', and
+   * set a proxy server to use if the 'proxy' config string is not empty
+   *
+   * @param string $url
+   * @return curl handle
+   */
+  private function initCurlHandle($url) {
+    $handle = curl_init();
+    curl_setopt($handle, CURLOPT_URL, $url);
+    // CURLOPT_FOLLOWLOCATION doesn't work with PHP safemode and openbasedir turned on
+    $isOpenBasedir = false;
+    $isSafeMode = false;
+    try {
+      $isOpenBasedir = @ini_get('open_basedir');
+      $isSafeMode = @ini_get('safe_mode');
+      $isOpenBasedir = !empty($isOpenBasedir);
+      $isSafeMode = !empty($isSafeMode);
+    } catch (\Exception $e) {
+      $isOpenBasedir = false;
+      $isSafeMode = false;
+    }
+    if(!$isOpenBasedir && !$isSafeMode) {
+      curl_setopt($handle, CURLOPT_FOLLOWLOCATION, 1);
+    } else {
+      curl_setopt($handle, CURLOPT_FOLLOWLOCATION, 0);
+    }
+    curl_setopt($handle, CURLOPT_BINARYTRANSFER, 1);
+    curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
+    curl_setopt($handle, CURLOPT_AUTOREFERER, 1);
+    curl_setopt($handle, CURLOPT_MAXREDIRS, 10);
+    curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, Config::get('curl_connection_timeout'));
+    curl_setopt($handle, CURLOPT_TIMEOUT, Config::get('curl_request_timeout'));
+    curl_setopt($handle, CURLOPT_HEADER, 1);
+    curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
+    $proxy = Config::get('proxy');
+    if (! empty($proxy)) {
+      curl_setopt($handle, CURLOPT_PROXY, $proxy);
+    }
+    return $handle;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicSecurityToken.php b/trunk/php/src/apache/shindig/common/sample/BasicSecurityToken.php
new file mode 100644
index 0000000..0106331
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicSecurityToken.php
@@ -0,0 +1,227 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Primitive token implementation that uses stings as tokens.
+ */
+class BasicSecurityToken extends SecurityToken {
+  /** serialized form of the token */
+  protected $token;
+
+  /** data from the token */
+  protected $tokenData;
+
+  /** tool to use for signing and encrypting the token */
+  protected $crypter;
+
+  private $OWNER_KEY = "o";
+  private $APP_KEY = "a";
+  private $VIEWER_KEY = "v";
+  private $DOMAIN_KEY = "d";
+  private $APPURL_KEY = "u";
+  private $MODULE_KEY = "m";
+  private $CONTAINER_KEY = "c";
+
+  protected $authenticationMode;
+  static protected $rawToken;
+
+  /**
+   * {@inheritDoc}
+   */
+  public function toSerialForm() {
+    return urlencode($this->token);
+  }
+
+  /**
+   * Generates a token from an input string
+   * @param token String form of token
+   * @param maxAge max age of the token (in seconds)
+   * @throws BlobCrypterException
+   */
+  static public function createFromToken($token, $maxAge) {
+    return new BasicSecurityToken($token, $maxAge, SecurityToken::$ANONYMOUS, SecurityToken::$ANONYMOUS, null, null, null, null, null);
+  }
+
+  /**
+   * Generates a token from an input array of values
+   * @param owner owner of this gadget
+   * @param viewer viewer of this gadget
+   * @param app application id
+   * @param domain domain of the container
+   * @param appUrl url where the application lives
+   * @param moduleId module id of this gadget
+   * @return BasicSecurityToken
+   * @throws BlobCrypterException
+   */
+  static public function createFromValues($owner, $viewer, $app, $domain, $appUrl, $moduleId, $containerId) {
+    return new BasicSecurityToken(null, null, $owner, $viewer, $app, $domain, $appUrl, $moduleId, $containerId);
+  }
+
+  /**
+   * gets security token string from get, post or auth header
+   * @return string
+   */
+  static public function getTokenStringFromRequest() {
+    if (self::$rawToken) {
+      return self::$rawToken;
+    }
+
+    $headers = \OAuthUtil::get_headers();
+
+    self::$rawToken = isset($_GET['st']) ? $_GET['st'] :
+                      (isset($_POST['st']) ? $_POST['st'] :
+                          (isset($headers['Authorization']) ? self::parseAuthorization($headers['Authorization']) : ''));
+
+
+    return self::$rawToken;
+  }
+
+  /**
+   *
+   * @param string $authHeader
+   * @return string
+   */
+  static private function parseAuthorization($authHeader) {
+    if (substr($authHeader, 0, 5) != 'OAuth') {
+      return '';
+    }
+    // Ignore OAuth 1.0a
+    if (strpos($authHeader, "oauth_signature_method")) {
+      return '';
+    }
+    return trim(substr($authHeader, 6));
+  }
+
+  public function __construct($token, $maxAge, $owner, $viewer, $app, $domain, $appUrl, $moduleId, $containerId) {
+    $this->crypter = $this->getCrypter();
+    if (! empty($token)) {
+      $this->token = $token;
+      $this->tokenData = $this->crypter->unwrap($token, $maxAge);
+    } else {
+      $this->tokenData = array();
+      $this->tokenData[$this->OWNER_KEY] = $owner;
+      $this->tokenData[$this->VIEWER_KEY] = $viewer;
+      $this->tokenData[$this->APP_KEY] = $app;
+      $this->tokenData[$this->DOMAIN_KEY] = $domain;
+      $this->tokenData[$this->APPURL_KEY] = $appUrl;
+      $this->tokenData[$this->MODULE_KEY] = $moduleId;
+      $this->tokenData[$this->CONTAINER_KEY] = $containerId;
+      $this->token = $this->crypter->wrap($this->tokenData);
+    }
+  }
+
+  protected function getCrypter() {
+    return new BasicBlobCrypter();
+  }
+
+  public function isAnonymous() {
+    return ($this->tokenData[$this->OWNER_KEY] === SecurityToken::$ANONYMOUS) && ($this->tokenData[$this->VIEWER_KEY] === SecurityToken::$ANONYMOUS);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getAppId() {
+    if ($this->isAnonymous()) {
+      throw new BasicSecurityTokenException("Can't get appId from an anonymous token");
+    }
+    return $this->tokenData[$this->APP_KEY];
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getDomain() {
+    if ($this->isAnonymous()) {
+      throw new BasicSecurityTokenException("Can't get domain from an anonymous token");
+    }
+    return $this->tokenData[$this->DOMAIN_KEY];
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getOwnerId() {
+    if ($this->isAnonymous()) {
+      throw new BasicSecurityTokenException("Can't get ownerId from an anonymous token");
+    }
+    return $this->tokenData[$this->OWNER_KEY];
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getViewerId() {
+    if ($this->isAnonymous()) {
+      throw new BasicSecurityTokenException("Can't get viewerId from an anonymous token");
+    }
+    return $this->tokenData[$this->VIEWER_KEY];
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getAppUrl() {
+    if ($this->isAnonymous()) {
+      throw new BasicSecurityTokenException("Can't get appUrl from an anonymous token");
+    }
+    return urldecode($this->tokenData[$this->APPURL_KEY]);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getModuleId() {
+    if ($this->isAnonymous()) {
+      throw new BasicSecurityTokenException("Can't get moduleId from an anonymous token");
+    }
+    if (! is_numeric($this->tokenData[$this->MODULE_KEY])) {
+      throw new BasicSecurityTokenException("Module ID should be an integer");
+    }
+    return $this->tokenData[$this->MODULE_KEY];
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getContainer() {
+    if ($this->isAnonymous()) {
+      throw new BasicSecurityTokenException("Can't get container from an anonymous token");
+    }
+    return $this->tokenData[$this->CONTAINER_KEY];
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function getAuthenticationMode() {
+    return $this->authenticationMode;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function setAuthenticationMode($mode) {
+    $this->authenticationMode = $mode;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicSecurityTokenDecoder.php b/trunk/php/src/apache/shindig/common/sample/BasicSecurityTokenDecoder.php
new file mode 100644
index 0000000..1b707e9
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicSecurityTokenDecoder.php
@@ -0,0 +1,78 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\gadgets\GadgetException;
+use apache\shindig\common\SecurityTokenDecoder;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+class BasicSecurityTokenDecoder extends SecurityTokenDecoder {
+  private $OWNER_INDEX = 0;
+  private $VIEWER_INDEX = 1;
+  private $APP_ID_INDEX = 2;
+  private $DOMAIN_INDEX = 3;
+  private $APP_URL_INDEX = 4;
+  private $MODULE_ID_INDEX = 5;
+  private $CONTAINER_INDEX = 6;
+
+  /**
+   * {@inheritDoc}
+   *
+   * Returns a token with some faked out values.
+   */
+  public function createToken($stringToken) {
+    if (empty($stringToken) && ! empty($_GET['authz'])) {
+      throw new GadgetException('INVALID_GADGET_TOKEN');
+    }
+    try {
+      //TODO remove this once we have a better way to generate a fake token
+      // in the example files
+      if (Config::get('allow_plaintext_token') && count(explode(':', $stringToken)) >= 7) {
+      	//Parses the security token in the form st=o:v:a:d:u:m:c
+	    $tokens = $this->parseToken($stringToken);
+	    
+        return new BasicSecurityToken(null, null, urldecode($tokens[$this->OWNER_INDEX]), urldecode($tokens[$this->VIEWER_INDEX]), urldecode($tokens[$this->APP_ID_INDEX]), urldecode($tokens[$this->DOMAIN_INDEX]), urldecode($tokens[$this->APP_URL_INDEX]), urldecode($tokens[$this->MODULE_ID_INDEX]), urldecode($tokens[$this->CONTAINER_INDEX]));
+      } else {
+        return BasicSecurityToken::createFromToken($stringToken, Config::get('token_max_age'));
+      }
+    } catch (\Exception $e) {
+      throw new GadgetException('INVALID_GADGET_TOKEN');
+    }
+  }
+
+  /**
+   * Parses the security token
+   *
+   * @param string $stringToken
+   * @return array
+   */
+  private function parseToken($stringToken) {
+    $data = explode(":", $stringToken);
+	$url_number = count($data)-6;
+
+	//get array elements conrresponding to broken url - http://host:port/gadget.xml -> ["http","//host","port/gadget.xml"]
+	$url_array = array_slice($data,4,$url_number) ;
+	$url = implode(":",$url_array);
+	array_splice($data,4,$url_number,$url);
+    return $data;
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BasicSecurityTokenException.php b/trunk/php/src/apache/shindig/common/sample/BasicSecurityTokenException.php
new file mode 100644
index 0000000..7c01c84
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BasicSecurityTokenException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\common\sample;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class BasicSecurityTokenException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/BlobExpiredException.php b/trunk/php/src/apache/shindig/common/sample/BlobExpiredException.php
new file mode 100644
index 0000000..defc9c0
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/BlobExpiredException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\common\sample;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class BlobExpiredException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/CacheStorageApc.php b/trunk/php/src/apache/shindig/common/sample/CacheStorageApc.php
new file mode 100644
index 0000000..f79dcc4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/CacheStorageApc.php
@@ -0,0 +1,90 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\common\CacheStorage;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class CacheStorageApc extends CacheStorage {
+  private $prefix = null;
+
+  /**
+   * {@inheritDoc}
+   */
+  public function __construct($name) {
+    $this->prefix = $name;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function store($key, $value) {
+    if (($ret = @apc_fetch($key)) === false) {
+      return @apc_store($this->storageKey($key), $value);
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function fetch($key) {
+    return @apc_fetch($this->storageKey($key));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function delete($key) {
+    return @apc_delete($this->storageKey($key));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function isLocked($key) {
+    if ((@apc_fetch($this->storageKey($key) . '.lock')) === false) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function lock($key) {
+    // the interesting thing is that this could fail if the lock was created in the meantime..
+    // but we'll ignore that out of convenience
+    @apc_add($this->storageKey($key) . '.lock', '', 5);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function unlock($key) {
+    // suppress all warnings, if some other process removed it that's ok too
+    @apc_delete($this->storageKey($key) . '.lock');
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  private function storageKey($key) {
+    return $this->prefix . '_' . $key;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/CacheStorageFile.php b/trunk/php/src/apache/shindig/common/sample/CacheStorageFile.php
new file mode 100644
index 0000000..389025a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/CacheStorageFile.php
@@ -0,0 +1,129 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\common\CacheException;
+use apache\shindig\common\CacheStorage;
+use apache\shindig\common\File;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class CacheStorageFile extends CacheStorage {
+  private $prefix = null;
+
+  /**
+   * {@inheritDoc}
+   */
+  public function __construct($name) {
+    $this->prefix = $name;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function store($key, $value) {
+    $cacheDir = CacheStorageFile::getCacheDir($key);
+    $cacheFile = CacheStorageFile::getCacheFile($key);
+    if (! is_dir($cacheDir)) {
+      if (! @mkdir($cacheDir, 0755, true)) {
+        throw new CacheException("Could not create cache directory");
+      }
+    }
+
+    return file_put_contents($cacheFile, $value);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function fetch($key) {
+    $cacheFile = CacheStorageFile::getCacheFile($key);
+    if (File::exists($cacheFile) && File::readable($cacheFile)) {
+      return @file_get_contents($cacheFile);
+    }
+    return false;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function delete($key) {
+    $cacheFile = CacheStorageFile::getCacheFile($key);
+    if (! @unlink($cacheFile)) {
+      throw new CacheException("Cache file could not be deleted");
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function isLocked($key) {
+    // our lock file convention is simple: /the/file/path.lock
+    clearstatcache();
+    return file_exists($key . '.lock');
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function lock($key) {
+    $cacheDir = CacheStorageFile::getCacheDir($key);
+    $cacheFile = CacheStorageFile::getCacheFile($key);
+    if (! is_dir($cacheDir)) {
+      if (! @mkdir($cacheDir, 0755, true)) {
+        // make sure the failure isn't because of a concurency issue
+        if (! is_dir($cacheDir)) {
+          throw new CacheException("Could not create cache directory");
+        }
+      }
+    }
+    @touch($cacheFile . '.lock');
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function unlock($key) {
+    // suppress all warnings, if some other process removed it that's ok too
+    $cacheFile = CacheStorageFile::getCacheFile($key);
+    @unlink($cacheFile . '.lock');
+  }
+
+  /**
+   *
+   * @param string $key
+   * @return string
+   */
+  private function getCacheDir($key) {
+    // use the first 2 characters of the hash as a directory prefix
+    // this should prevent slowdowns due to huge directory listings
+    // and thus give some basic amount of scalability
+    return Config::get('cache_root') . '/' . $this->prefix . '/' .
+        substr($key, 0, 2);
+  }
+
+  /**
+   *
+   * @param string $key
+   * @return string
+   */
+  private function getCacheFile($key) {
+    return $this->getCacheDir($key) . '/' . $key;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/CacheStorageMemcache.php b/trunk/php/src/apache/shindig/common/sample/CacheStorageMemcache.php
new file mode 100644
index 0000000..89fbff5
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/CacheStorageMemcache.php
@@ -0,0 +1,113 @@
+<?php
+namespace apache\shindig\common\sample;
+use apache\shindig\common\CacheException;
+use apache\shindig\common\CacheStorage;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class CacheStorageMemcache extends CacheStorage {
+  /**
+   * @var Memcache
+   */
+  protected static $memcache = null;
+
+  private $prefix = null;
+
+  /**
+   * {@inheritDoc}
+   */
+  public function __construct($name) {
+    $this->prefix = $name;
+    if (!self::$memcache) {
+      self::$memcache = new \Memcache();
+      $host = Config::get('cache_host');
+      $port = Config::get('cache_port');
+      if (Config::get('cache_memcache_pconnect')) {
+        if (!@self::$memcache->pconnect($host, $port)) {
+          throw new CacheException("Couldn't connect to memcache server");
+        }
+      } else {
+        if (!@self::$memcache->connect($host, $port)) {
+          throw new CacheException("Couldn't connect to memcache server");
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function isLocked($key) {
+    if ((@self::$memcache->get($this->storageKey($key) . '.lock')) === false) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function lock($key) {
+    // the interesting thing is that this could fail if the lock was created in the meantime..
+    // but we'll ignore that out of convenience
+    @self::$memcache->add($this->storageKey($key) . '.lock', '', 0, 2);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function unlock($key) {
+    // suppress all warnings, if some other process removed it that's ok too
+    @self::$memcache->delete($this->storageKey($key) . '.lock');
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function store($key, $value) {
+    return self::$memcache->set($this->storageKey($key), $value, false, 0);
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function fetch($key) {
+    return self::$memcache->get($this->storageKey($key));
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  public function delete($key) {
+    if (!@self::$memcache->delete($this->storageKey($key))) {
+      throw new CacheException("Cache memcache could not be deleted");
+    }
+  }
+
+  /**
+   *
+   * @param string $key
+   * @return string
+   */
+  private function storageKey($key) {
+    return $this->prefix . '_' . $key;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/Crypto.php b/trunk/php/src/apache/shindig/common/sample/Crypto.php
new file mode 100644
index 0000000..269d327
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/Crypto.php
@@ -0,0 +1,122 @@
+<?php
+namespace apache\shindig\common\sample;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+final class Crypto {
+
+  /**
+   * HMAC algorithm to use
+   */
+  private static $HMAC_TYPE = "HMACSHA1";
+
+  /**
+   * minimum safe length for hmac keys (this is good practice, but not
+   * actually a requirement of the algorithm
+   */
+  private static $MIN_HMAC_KEY_LEN = 8;
+
+  /**
+   * Encryption algorithm to use
+   */
+  private static $CIPHER_TYPE = "AES/CBC/PKCS5Padding";
+
+  private static $CIPHER_KEY_TYPE = "AES";
+
+  /**
+   * Use keys of this length for encryption operations
+   */
+  public static $CIPHER_KEY_LEN = 16;
+
+  private static $CIPHER_BLOCK_SIZE = 16;
+
+  /**
+   * Length of HMAC SHA1 output
+   */
+  public static $HMAC_SHA1_LEN = 20;
+
+  private function __construct() {}
+
+  public static function hmacSha1Verify($key, $in, $expected) {
+    $hmac = Crypto::hmacSha1($key, $in);
+    if ($hmac != $expected) {
+      throw new GeneralSecurityException("HMAC verification failure");
+    }
+  }
+
+  public static function aes128cbcEncrypt($key, $text) {
+    /* Open the cipher */
+    $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
+    if (! $td) {
+      throw new GeneralSecurityException('Invalid mcrypt cipher, check your libmcrypt library and php-mcrypt extention');
+    }
+    // replaced MCRYPT_DEV_RANDOM with MCRYPT_RAND since windows doesn't have /dev/rand :)
+    srand((double)microtime() * 1000000);
+    $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND);
+    /* Intialize encryption */
+    mcrypt_generic_init($td, $key, $iv);
+    /* Encrypt data */
+    $encrypted = mcrypt_generic($td, $text);
+    /* Terminate encryption handler */
+    mcrypt_generic_deinit($td);
+    /*
+		 *  AES-128-CBC encryption.  The IV is returned as the first 16 bytes
+		 * of the cipher text.
+		 */
+    return $iv . $encrypted;
+  }
+
+  public static function aes128cbcDecrypt($key, $encrypted_text) {
+    /* Open the cipher */
+    $td = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, '');
+    if (is_callable('mb_substr')) {
+      $iv = mb_substr($encrypted_text, 0, Crypto::$CIPHER_BLOCK_SIZE, 'latin1');
+    } else {
+      $iv = substr($encrypted_text, 0, Crypto::$CIPHER_BLOCK_SIZE);
+    }
+    /* Initialize encryption module for decryption */
+    mcrypt_generic_init($td, $key, $iv);
+    /* Decrypt encrypted string */
+    if (is_callable('mb_substr')) {
+      $encrypted = mb_substr($encrypted_text, Crypto::$CIPHER_BLOCK_SIZE, mb_strlen($encrypted_text, 'latin1'), 'latin1');
+    } else {
+      $encrypted = substr($encrypted_text, Crypto::$CIPHER_BLOCK_SIZE);
+    }
+    $decrypted = mdecrypt_generic($td, $encrypted);
+    /* Terminate decryption handle and close module */
+    mcrypt_generic_deinit($td);
+    mcrypt_module_close($td);
+    /* Show string */
+    return trim($decrypted);
+  }
+
+  public static function hmacSha1($key, $data) {
+    $blocksize = 64;
+    $hashfunc = 'sha1';
+    if (strlen($key) > $blocksize) {
+      $key = pack('H*', $hashfunc($key));
+    }
+    $key = str_pad($key, $blocksize, chr(0x00));
+    $ipad = str_repeat(chr(0x36), $blocksize);
+    $opad = str_repeat(chr(0x5c), $blocksize);
+    $hmac = pack('H*', $hashfunc(($key ^ $opad) . pack('H*', $hashfunc(($key ^ $ipad) . $data))));
+    return $hmac;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/common/sample/GeneralSecurityException.php b/trunk/php/src/apache/shindig/common/sample/GeneralSecurityException.php
new file mode 100644
index 0000000..3887cbe
--- /dev/null
+++ b/trunk/php/src/apache/shindig/common/sample/GeneralSecurityException.php
@@ -0,0 +1,25 @@
+<?php
+namespace apache\shindig\common\sample;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+class GeneralSecurityException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/ContainerConfig.php b/trunk/php/src/apache/shindig/gadgets/ContainerConfig.php
new file mode 100644
index 0000000..4ff2d6a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/ContainerConfig.php
@@ -0,0 +1,155 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\File;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ContainerConfig {
+  /**
+   * @var string
+   */
+  public $default_container = 'default';
+
+  /**
+   * @var string
+   */
+  public $container_key = 'gadgets.container';
+
+  /**
+   * @var array
+   */
+  private $config = array();
+
+  /**
+   *
+   * @param string $defaultContainer
+   */
+  public function __construct($defaultContainer) {
+    if (! empty($defaultContainer)) {
+      $this->loadContainers($defaultContainer);
+    }
+  }
+
+  /**
+   *
+   * @param array $containers
+   */
+  private function loadContainers($containers) {
+    if (! File::exists($containers)) {
+      throw new \Exception("Invalid container path");
+    }
+    foreach (glob("$containers/*.js") as $file) {
+      if (! File::readable($file)) {
+        throw new \Exception("Could not read container config: $file");
+      }
+      if (is_dir($file)) {
+        // support recursive loading of sub directories
+        $this->loadContainers($file);
+      } else {
+        $this->loadFromFile($file);
+      }
+    }
+  }
+
+  /**
+   *
+   * @param string $file
+   */
+  private function loadFromFile($file) {
+    $contents = file_get_contents($file);
+    $contents = $this->removeComments($contents);
+    $config = json_decode($contents, true);
+    if ($config == $contents) {
+      throw new \Exception("Failed to json_decode the container configuration");
+    }
+    if (! isset($config[$this->container_key][0])) {
+      throw new \Exception("No gadgets.container value set for current container");
+    }
+    $container = $config[$this->container_key][0];
+    $this->config[$container] = array();
+    foreach ($config as $key => $val) {
+      $this->config[$container][$key] = $val;
+    }
+  }
+
+  /**
+   * @param string $str
+   * @return string
+   */
+  public function removeComments($str) {
+    // remove /* */ style comments
+    $str = preg_replace('@/\\*.*?\\*/@s', '', $str);
+    // remove // style comments, but keep 'http://' 'https://' and '"//'
+    // for example: "gadgets.uri.oauth.callbackTemplate" : "//%host%/gadgets/oauthcallback"
+    $str = preg_replace('/(?<!http:|https:|")\/\/.*$/m', '', $str);
+    return $str;
+  }
+
+  /**
+   *
+   * @param string $container
+   * @param string $name
+   * @return array
+   */
+  public function getConfig($container, $name) {
+    $config = array();
+    if (isset($this->config[$container]) && isset($this->config[$container][$name])) {
+      $config = $this->config[$container][$name];
+    }
+    if ($container != $this->default_container && isset($this->config[$this->default_container]) && isset($this->config[$this->default_container][$name])) {
+      $config = $this->mergeConfig($this->config[$this->default_container][$name], $config);
+    }
+    return $config;
+  }
+
+  /**
+   * Code sniplet borrowed from: http://nl.php.net/manual/en/function.array-merge-recursive.php#81409
+   * default array merge recursive doesn't overwrite values, but creates multiple elementents for that key,
+   * which is not what we want here, we want array_merge like behavior
+   *
+   * @return array
+   */
+  private function mergeConfig() // $array1, $array2, etc
+  {
+    $arrays = func_get_args();
+    $narrays = count($arrays);
+    for ($i = 0; $i < $narrays; $i ++) {
+      if (! is_array($arrays[$i])) {
+        trigger_error('Argument #' . ($i + 1) . ' is not an array - trying to merge array with scalar! Returning null!', E_USER_WARNING);
+        return null;
+      }
+    }
+    $ret = $arrays[0];
+    for ($i = 1; $i < $narrays; $i ++) {
+      foreach ($arrays[$i] as $key => $value) {
+        if (((string)$key) === ((string)intval($key))) { // integer or string as integer key - append
+          $ret[] = $value;
+        } else {
+          if (is_array($value) && isset($ret[$key])) {
+            $ret[$key] = $this->mergeConfig($ret[$key], $value);
+          } else {
+            $ret[$key] = $value;
+          }
+        }
+      }
+    }
+    return $ret;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/Gadget.php b/trunk/php/src/apache/shindig/gadgets/Gadget.php
new file mode 100644
index 0000000..9aa5c58
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/Gadget.php
@@ -0,0 +1,563 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class Gadget {
+  const DEFAULT_VIEW = 'profile';
+
+  /**
+   * @var GadgetSpec
+   */
+  public $gadgetSpec;
+
+  public $features;
+
+  /*
+   * @var Substitutions
+   */
+  public $substitutions;
+  public $rightToLeft;
+
+  /**
+   * @var GadgetContext
+   */
+  public $gadgetContext;
+
+  /**
+   *
+   * @param GadgetSpec $gadgetSpec
+   * @param GadgetContext $gadgetContext
+   */
+  public function __construct(GadgetSpec $gadgetSpec, GadgetContext $gadgetContext) {
+    $this->gadgetSpec = $gadgetSpec;
+    $this->gadgetContext = $gadgetContext;
+  }
+
+  /**
+   * returns all information about the given viewName
+   *
+   * @param string $viewName
+   * @throws GadgetException
+   * @return array
+   */
+  public function getView($viewName) {
+    if (isset($this->gadgetSpec->views[$viewName])) {
+      return $this->gadgetSpec->views[$viewName];
+    } elseif (isset($this->gadgetSpec->views[self::DEFAULT_VIEW])) {
+      return $this->gadgetSpec->views[self::DEFAULT_VIEW];
+    } else {
+      // see if there's any empty entries, we'll use that as default then (old iGoogle style)
+      foreach ($this->gadgetSpec->views as $view) {
+        if (empty($view['view'])) {
+          return $view;
+        }
+      }
+    }
+    throw new GadgetException("Invalid view specified for this gadget");
+  }
+
+  /**
+   * @return OpenSocialVersion
+   */
+  public function getSpecificationVersion() {
+    return $this->gadgetSpec->specificationVersion;
+  }
+  
+  /**
+   * Returns if the doctype attribute is set to quirksmode.  
+   * Needed to override default OpenSocial 2.0 behavior which is to render in standards mode,
+   * may not be possible to honor this attribute when inlining (caja)
+   * 
+   * @return boolean TRUE if this Gadget should be rendered in browser quirks mode
+   */
+  public function useQuirksMode() {
+    return GadgetSpec::DOCTYPE_QUIRKSMODE == $this->gadgetSpec->doctype;
+  }
+  
+  /**
+   * @return string
+   */
+  public function getDoctype() {
+    return $this->gadgetSpec->doctype;
+  }
+  
+  /**
+   * @return unknown
+   */
+  public function getAuthor() {
+    return $this->substitutions->substitute($this->gadgetSpec->author);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getAuthorAboutme() {
+    return $this->substitutions->substitute($this->gadgetSpec->authorAboutme);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getAuthorAffiliation() {
+    return $this->substitutions->substitute($this->gadgetSpec->authorAffiliation);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getAuthorEmail() {
+    return $this->substitutions->substitute($this->gadgetSpec->authorEmail);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getAuthorLink() {
+    return $this->substitutions->substitute($this->gadgetSpec->authorLink);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getAuthorLocation() {
+    return $this->substitutions->substitute($this->gadgetSpec->authorLocation);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getAuthorPhoto() {
+    return $this->substitutions->substitute($this->gadgetSpec->authorPhoto);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getAuthorQuote() {
+    return $this->substitutions->substitute($this->gadgetSpec->authorQuote);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getCategory() {
+    return $this->substitutions->substitute($this->gadgetSpec->category);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getCategory2() {
+    return $this->substitutions->substitute($this->gadgetSpec->category2);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getChecksum() {
+    return $this->gadgetSpec->checksum;
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getDescription() {
+    return $this->substitutions->substitute($this->gadgetSpec->description);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getDirectoryTitle() {
+    return $this->substitutions->substitute($this->gadgetSpec->directoryTitle);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getHeight() {
+    return $this->substitutions->substitute($this->gadgetSpec->height);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getIcon() {
+    return $this->substitutions->substitute($this->gadgetSpec->icon);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getLinks() {
+    return $this->gadgetSpec->links;
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getLocales() {
+    return $this->gadgetSpec->locales;
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getOptionalFeatures() {
+    return $this->gadgetSpec->optionalFeatures;
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getPreloads() {
+    return $this->gadgetSpec->preloads;
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getRenderInline() {
+    return $this->substitutions->substitute($this->gadgetSpec->renderInline);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getRequiredFeatures() {
+    return $this->gadgetSpec->requiredFeatures;
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getScaling() {
+    return $this->substitutions->substitute($this->gadgetSpec->scaling);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getScreenshot() {
+    return $this->substitutions->substitute($this->gadgetSpec->screenshot);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getScrolling() {
+    return $this->substitutions->substitute($this->gadgetSpec->scrolling);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getShowInDirectory() {
+    return $this->substitutions->substitute($this->gadgetSpec->showInDirectory);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getShowStats() {
+    return $this->substitutions->substitute($this->gadgetSpec->showStats);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getSingleton() {
+    return $this->substitutions->substitute($this->gadgetSpec->singleton);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getString() {
+    return $this->substitutions->substitute($this->gadgetSpec->string);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getThumbnail() {
+    return $this->substitutions->substitute($this->gadgetSpec->thumbnail);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getTitle() {
+    return $this->substitutions->substitute($this->gadgetSpec->title);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getTitleUrl() {
+    return $this->substitutions->substitute($this->gadgetSpec->titleUrl);
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getUserPrefs() {
+    return $this->gadgetSpec->userPrefs;
+  }
+
+  /**
+   * @return unknown
+   */
+  public function getWidth() {
+    return $this->substitutions->substitute($this->gadgetSpec->width);
+  }
+
+  /**
+   * @param unknown_type $author
+   */
+  public function setAuthor($author) {
+    $this->gadgetSpec->author = $author;
+  }
+
+  /**
+   * @param unknown_type $authorAboutme
+   */
+  public function setAuthorAboutme($authorAboutme) {
+    $this->gadgetSpec->authorAboutme = $authorAboutme;
+  }
+
+  /**
+   * @param unknown_type $authorAffiliation
+   */
+  public function setAuthorAffiliation($authorAffiliation) {
+    $this->gadgetSpec->authorAffiliation = $authorAffiliation;
+  }
+
+  /**
+   * @param unknown_type $authorEmail
+   */
+  public function setAuthorEmail($authorEmail) {
+    $this->gadgetSpec->authorEmail = $authorEmail;
+  }
+
+  /**
+   * @param unknown_type $authorLink
+   */
+  public function setAuthorLink($authorLink) {
+    $this->gadgetSpec->authorLink = $authorLink;
+  }
+
+  /**
+   * @param unknown_type $authorLocation
+   */
+  public function setAuthorLocation($authorLocation) {
+    $this->gadgetSpec->authorLocation = $authorLocation;
+  }
+
+  /**
+   * @param unknown_type $authorPhoto
+   */
+  public function setAuthorPhoto($authorPhoto) {
+    $this->gadgetSpec->authorPhoto = $authorPhoto;
+  }
+
+  /**
+   * @param unknown_type $authorQuote
+   */
+  public function setAuthorQuote($authorQuote) {
+    $this->gadgetSpec->authorQuote = $authorQuote;
+  }
+
+  /**
+   * @param unknown_type $category
+   */
+  public function setCategory($category) {
+    $this->gadgetSpec->category = $category;
+  }
+
+  /**
+   * @param unknown_type $category2
+   */
+  public function setCategory2($category2) {
+    $this->gadgetSpec->category2 = $category2;
+  }
+
+  /**
+   * @param unknown_type $checksum
+   */
+  public function setChecksum($checksum) {
+    $this->gadgetSpec->checksum = $checksum;
+  }
+
+  /**
+   * @param unknown_type $description
+   */
+  public function setDescription($description) {
+    $this->gadgetSpec->description = $description;
+  }
+
+  /**
+   * @param unknown_type $directoryTitle
+   */
+  public function setDirectoryTitle($directoryTitle) {
+    $this->gadgetSpec->directoryTitle = $directoryTitle;
+  }
+
+  /**
+   * @param unknown_type $height
+   */
+  public function setHeight($height) {
+    $this->gadgetSpec->height = $height;
+  }
+
+  /**
+   * @param unknown_type $icon
+   */
+  public function setIcon($icon) {
+    $this->gadgetSpec->icon = $icon;
+  }
+
+  /**
+   * @param unknown_type $links
+   */
+  public function setLinks($links) {
+    $this->gadgetSpec->links = $links;
+  }
+
+  /**
+   * @param unknown_type $locales
+   */
+  public function setLocales($locales) {
+    $this->gadgetSpec->locales = $locales;
+  }
+
+  /**
+   * @param unknown_type $optionalFeatures
+   */
+  public function setOptionalFeatures($optionalFeatures) {
+    $this->gadgetSpec->optionalFeatures = $optionalFeatures;
+  }
+
+  /**
+   * @param unknown_type $preloads
+   */
+  public function setPreloads($preloads) {
+    $this->gadgetSpec->preloads = $preloads;
+  }
+
+  /**
+   * @param unknown_type $renderInline
+   */
+  public function setRenderInline($renderInline) {
+    $this->gadgetSpec->renderInline = $renderInline;
+  }
+
+  /**
+   * @param unknown_type $requiredFeatures
+   */
+  public function setRequiredFeatures($requiredFeatures) {
+    $this->gadgetSpec->requiredFeatures = $requiredFeatures;
+  }
+
+  /**
+   * @param unknown_type $scaling
+   */
+  public function setScaling($scaling) {
+    $this->gadgetSpec->scaling = $scaling;
+  }
+
+  /**
+   * @param unknown_type $screenshot
+   */
+  public function setScreenshot($screenshot) {
+    $this->gadgetSpec->screenshot = $screenshot;
+  }
+
+  /**
+   * @param unknown_type $scrolling
+   */
+  public function setScrolling($scrolling) {
+    $this->gadgetSpec->scrolling = $scrolling;
+  }
+
+  /**
+   * @param unknown_type $showInDirectory
+   */
+  public function setShowInDirectory($showInDirectory) {
+    $this->gadgetSpec->showInDirectory = $showInDirectory;
+  }
+
+  /**
+   * @param unknown_type $showStats
+   */
+  public function setShowStats($showStats) {
+    $this->gadgetSpec->showStats = $showStats;
+  }
+
+  /**
+   * @param unknown_type $singleton
+   */
+  public function setSingleton($singleton) {
+    $this->gadgetSpec->singleton = $singleton;
+  }
+
+  /**
+   * @param unknown_type $string
+   */
+  public function setString($string) {
+    $this->gadgetSpec->string = $string;
+  }
+
+  /**
+   * @param unknown_type $thumbnail
+   */
+  public function setThumbnail($thumbnail) {
+    $this->gadgetSpec->thumbnail = $thumbnail;
+  }
+
+  /**
+   * @param unknown_type $title
+   */
+  public function setTitle($title) {
+    $this->gadgetSpec->title = $title;
+  }
+
+  /**
+   * @param unknown_type $titleUrl
+   */
+  public function setTitleUrl($titleUrl) {
+    $this->gadgetSpec->titleUrl = $titleUrl;
+  }
+
+  /**
+   * @param unknown_type $userPrefs
+   */
+  public function setUserPrefs($userPrefs) {
+    $this->gadgetSpec->userPrefs = $userPrefs;
+  }
+
+  /**
+   * @param unknown_type $width
+   */
+  public function setWidth($width) {
+    $this->gadgetSpec->width = $width;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetBlacklist.php b/trunk/php/src/apache/shindig/gadgets/GadgetBlacklist.php
new file mode 100644
index 0000000..6fdacc1
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetBlacklist.php
@@ -0,0 +1,30 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface GadgetBlacklist {
+
+  /**
+   * @param string $url
+   * @return boolean
+   */
+  function isBlacklisted($url);
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetContext.php b/trunk/php/src/apache/shindig/gadgets/GadgetContext.php
new file mode 100644
index 0000000..c63d489
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetContext.php
@@ -0,0 +1,550 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\Config;
+use apache\shindig\common\Cache;
+use apache\shindig\common\sample\BasicSecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*
+ * GadgetContext contains all contextual variables and classes that are relevant for this request,
+ * such as url, httpFetcher, feature registry, etc.
+ * Server wide variables are stored in config.php
+ */
+class GadgetContext {
+  const DEFAULT_VIEW = 'profile';
+  /**
+   *
+   * @var RemoteContentFetcher
+   */
+  protected $httpFetcher = null;
+  /**
+   *
+   * @var array
+   */
+  protected $locale = null;
+  /**
+   *
+   * @var string
+   */
+  protected $renderingContext = null;
+  /**
+   *
+   * @var GadgetFeatureRegistry
+   */
+  protected $registry = null;
+  /**
+   *
+   * @var string
+   */
+  protected $view = null;
+  /**
+   *
+   * @var string
+   */
+  protected $moduleId = null;
+  /**
+   *
+   * @var string
+   */
+  protected $url = null;
+  /**
+   *
+   * @var array
+   */
+  protected $cache = null;
+  /**
+   *
+   * @var GadgetBlacklist
+   */
+  protected $blacklist = null;
+  /**
+   *
+   * @var boolean
+   */
+  protected $ignoreCache = null;
+  /**
+   *
+   * @var string
+   */
+  protected $forcedJsLibs = null;
+  /**
+   *
+   * @var array
+   */
+  protected $containerConfig = null;
+  /**
+   *
+   * @var string
+   */
+  protected $container = null;
+
+  /**
+   *
+   * @var string
+   */
+  protected $rawXml = null;
+
+  /**
+   *
+   * @var int
+   */
+  protected $refreshInterval;
+
+  /**
+   *
+   * @param string $renderingContext
+   */
+  public function __construct($renderingContext) {
+    // Rendering context is set by the calling event handler (either GADGET or CONTAINER)
+    $this->setRenderingContext($renderingContext);
+
+    // Request variables
+    $this->setIgnoreCache($this->getIgnoreCacheParam());
+    $this->setForcedJsLibs($this->getForcedJsLibsParam());
+    $this->setUrl($this->getUrlParam());
+    $this->setRawXml($this->getRawXmlParam());
+    $this->setModuleId($this->getModuleIdParam());
+    $this->setView($this->getViewParam());
+    $this->setContainer($this->getContainerParam());
+    $this->setRefreshInterval($this->getRefreshIntervalParam());
+    //NOTE All classes are initialized when called (aka lazy loading) because we don't need all of them in every situation
+  }
+
+  /**
+   *
+   * @return int
+   */
+  protected function getRefreshIntervalParam() {
+    return isset($_GET['refresh']) ? $_GET['refresh'] : Config::get('default_refresh_interval');
+  }
+
+  /**
+   *
+   * @return string
+   */
+  protected function getContainerParam() {
+    $container = 'default';
+    if (! empty($_GET['container'])) {
+      $container = $_GET['container'];
+    } elseif (! empty($_POST['container'])) {
+      $container = $_POST['container'];
+      //FIXME The paramater used to be called 'synd' & is scheduled for removal
+    } elseif (! empty($_GET['synd'])) {
+      $container = $_GET['synd'];
+    } elseif (! empty($_POST['synd'])) {
+      $container = $_POST['synd'];
+    }
+    return $container;
+  }
+
+  /**
+   *
+   * @return boolean
+   */
+  protected function getIgnoreCacheParam() {
+    // Support both the old Orkut style &bpc and new standard style &nocache= params
+    return (isset($_GET['nocache']) && intval($_GET['nocache']) == 1) || (isset($_GET['bpc']) && intval($_GET['bpc']) == 1);
+  }
+
+  /**
+   *
+   * @return string
+   */
+  protected function getForcedJsLibsParam() {
+    return isset($_GET['libs']) ? trim($_GET['libs']) : null;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  protected function getUrlParam() {
+    if (! empty($_GET['url'])) {
+      return $_GET['url'];
+    } elseif (! empty($_POST['url'])) {
+      return $_POST['url'];
+    }
+    return null;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  protected function getRawXmlParam() {
+    if (! empty($_GET['rawxml'])) {
+      return $_GET['rawxml'];
+    } elseif (! empty($_POST['rawxml'])) {
+      return $_POST['rawxml'];
+    }
+    return null;
+  }
+
+  /**
+   *
+   * @return int
+   */
+  protected function getModuleIdParam() {
+    return isset($_GET['mid']) && is_numeric($_GET['mid']) ? intval($_GET['mid']) : 0;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  protected function getViewParam() {
+    return ! empty($_GET['view']) ? $_GET['view'] : self::DEFAULT_VIEW;
+  }
+
+  /**
+   *
+   * @return GadgetBlacklist
+   */
+  protected function instanceBlacklist() {
+    $blackListClass = Config::get('blacklist_class');
+    if (! empty($blackListClass)) {
+      return new $blackListClass();
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   *
+   * @return RemoteContentFetcher
+   */
+  protected function instanceHttpFetcher() {
+    $remoteContent = Config::get('remote_content');
+    return new $remoteContent();
+  }
+
+  /**
+   *
+   * @return GadgetFeatureRegistry 
+   */
+  protected function instanceRegistry() {
+    // feature parsing is very resource intensive so by caching the result this saves upto 30% of the processing time
+    $featureCache = Cache::createCache(Config::get('feature_cache'), 'FeatureCache');
+    $key = md5(implode(',', Config::get('features_path')));
+    if (! ($registry = $featureCache->get($key))) {
+      $registry = new GadgetFeatureRegistry(Config::get('features_path'));
+      $featureCache->set($key, $registry);
+    }
+    return $registry;
+  }
+
+  /**
+   *
+   * @return array
+   */
+  protected function instanceLocale() {
+    // Get language and country params, try the GET params first, if their not set try the POST, else use 'all' as default
+    $language = ! empty($_GET['lang']) ? $_GET['lang'] : (! empty($_POST['lang']) ? $_POST['lang'] : 'all');
+    $country = ! empty($_GET['country']) ? $_GET['country'] : (! empty($_POST['country']) ? $_POST['country'] : 'all');
+    return array('lang' => strtolower($language), 'country' => strtoupper($country));
+  }
+
+  /**
+   *
+   * @return ContainerConfig
+   */
+  protected function instanceContainerConfig() {
+    $containerConfigClass = Config::get('container_config_class');
+    return new $containerConfigClass(Config::get('container_path'));
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getContainer() {
+    return $this->container;
+  }
+
+  /**
+   *
+   * @return ContainerConfig
+   */
+  public function getContainerConfig() {
+    if ($this->containerConfig == null) {
+      $this->containerConfig = $this->instanceContainerConfig();
+    }
+    return $this->containerConfig;
+  }
+
+  /**
+   *
+   * @return int
+   */
+  public function getModuleId() {
+    return $this->moduleId;
+  }
+
+  /**
+   *
+   * @return GadgetFeatureRegistry
+   */
+  public function getRegistry() {
+    if ($this->registry == null) {
+      $this->setRegistry($this->instanceRegistry());
+    }
+    return $this->registry;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getUrl() {
+    return $this->url;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getRawXml() {
+    return $this->rawXml;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getView() {
+    return $this->view;
+  }
+
+  /**
+   *
+   * @param int $interval
+   */
+  public function setRefreshInterval($interval) {
+    $this->refreshInterval = $interval;
+  }
+
+  /**
+   *
+   * @param string $container
+   */
+  public function setContainer($container) {
+    $this->container = $container;
+  }
+
+  /**
+   *
+   * @param array $containerConfig
+   */
+  public function setContainerConfig($containerConfig) {
+    $this->containerConfig = $containerConfig;
+  }
+
+  /**
+   *
+   * @param GadgetBlacklist $blacklist
+   */
+  public function setBlacklist($blacklist) {
+    $this->blacklist = $blacklist;
+  }
+
+  /**
+   *
+   * @param array $cache
+   */
+  public function setCache($cache) {
+    $this->cache = $cache;
+  }
+
+  /**
+   *
+   * @param RemoteContentFetcher $httpFetcher
+   */
+  public function setHttpFetcher($httpFetcher) {
+    $this->httpFetcher = $httpFetcher;
+  }
+
+  /**
+   *
+   * @param array $locale
+   */
+  public function setLocale($locale) {
+    $this->locale = $locale;
+  }
+
+  /**
+   *
+   * @param int $moduleId
+   */
+  public function setModuleId($moduleId) {
+    $this->moduleId = $moduleId;
+  }
+
+  /**
+   *
+   * @param GadgetFeatureRegistry $registry
+   */
+  public function setRegistry($registry) {
+    $this->registry = $registry;
+  }
+
+  /**
+   *
+   * @param string $renderingContext
+   */
+  public function setRenderingContext($renderingContext) {
+    $this->renderingContext = $renderingContext;
+  }
+
+  /**
+   *
+   * @param string $url
+   */
+  public function setUrl($url) {
+    $this->url = $url;
+  }
+
+  /**
+   * @param string $rawXml
+   */
+  public function setRawXml($rawXml) {
+    $this->rawXml = $rawXml;
+  }
+
+  /**
+   *
+   * @param string $view
+   */
+  public function setView($view) {
+    $this->view = $view;
+  }
+
+  /**
+   *
+   * @param boolean $ignoreCache
+   */
+  public function setIgnoreCache($ignoreCache) {
+    $this->ignoreCache = $ignoreCache;
+  }
+
+  /**
+   *
+   * @param string $forcedJsLibs
+   */
+  public function setForcedJsLibs($forcedJsLibs) {
+    $this->forcedJsLibs = $forcedJsLibs;
+  }
+
+  /**
+   *
+   * @return int
+   */
+  public function getRefreshInterval() {
+    return $this->refreshInterval;
+  }
+
+  /**
+   *
+   * @return boolean
+   */
+  public function getIgnoreCache() {
+    return $this->ignoreCache;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getForcedJsLibs() {
+    return $this->forcedJsLibs;
+  }
+
+  /**
+   *
+   * @return GadgetBlacklist
+   */
+  public function getBlacklist() {
+    if ($this->blacklist == null) {
+      $this->setBlacklist($this->instanceBlacklist());
+    }
+    return $this->blacklist;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getRenderingContext() {
+    return $this->renderingContext;
+  }
+
+  /**
+   *
+   * @return RemoteContentFetcher
+   */
+  public function getHttpFetcher() {
+    if ($this->httpFetcher == null) {
+      $this->setHttpFetcher($this->instanceHttpFetcher());
+    }
+    return $this->httpFetcher;
+  }
+
+  /**
+   *
+   * @return array
+   */
+  public function getLocale() {
+    if ($this->locale == null) {
+      $this->setLocale($this->instanceLocale());
+    }
+    return $this->locale;
+  }
+
+  /**
+   * Extracts the 'st' token from the GET or POST params and calls the
+   * signer to validate the token
+   *
+   * @param SecurityTokenDecoder $signer the signer to use (configured in config.php)
+   * @return SecurityToken An object representation of the token data.
+   */
+  public function extractAndValidateToken($signer) {
+    if ($signer == null) {
+      return null;
+    }
+
+    $token = BasicSecurityToken::getTokenStringFromRequest();
+
+    return $this->validateToken($token, $signer);
+  }
+
+  /**
+   * Validates a passed-in token.
+   * 
+   * @param string $token A urlencoded base64 encoded security token.
+   * @param SecurityTokenDecoder $signer The signer to use (configured in config.php)
+   * @return SecurityToken An object representation of the token data.
+   */
+  public function validateToken($token, $signer) {
+    if (empty($token)) {
+      throw new \Exception("Missing or invalid security token");
+    }
+    return $signer->createToken($token);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetException.php b/trunk/php/src/apache/shindig/gadgets/GadgetException.php
new file mode 100644
index 0000000..eb8a1ea
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetException.php
@@ -0,0 +1,23 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetException extends \Exception {}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetFactory.php b/trunk/php/src/apache/shindig/gadgets/GadgetFactory.php
new file mode 100644
index 0000000..d62eff6
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetFactory.php
@@ -0,0 +1,411 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\sample\BasicRemoteContent;
+use apache\shindig\common\Config;
+use apache\shindig\common\XmlError;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * The Gadget Factory builds a gadget based on the current context and token and returns a fully processed
+ * gadget ready to be rendered.
+ *
+ */
+class GadgetFactory {
+  /**
+   * @var GadgetContext
+   */
+  protected $context;
+  protected $token;
+
+  public function __construct(GadgetContext $context, $token) {
+    $this->context = $context;
+    $this->token = $token;
+  }
+
+  /**
+   * Returns the processed gadget spec
+   *
+   * @return Gadget
+   */
+  public function createGadget() {
+    if (! $gadgetContent = $this->context->getRawXml()) {
+      $gadgetUrl = $this->context->getUrl();
+      if ($this->context->getBlacklist() != null && $this->context->getBlacklist()->isBlacklisted($gadgetUrl)) {
+        throw new GadgetException("The Gadget ($gadgetUrl) is blacklisted and can not be rendered");
+      }
+      // Fetch the gadget's content and create a GadgetSpec
+      $gadgetContent = $this->fetchGadget($gadgetUrl);
+    }
+    $gadgetSpecParserClass = Config::get('gadget_spec_parser');
+    $gadgetSpecParser = new $gadgetSpecParserClass();
+    $gadgetSpec = $gadgetSpecParser->parse($gadgetContent, $this->context);
+    $gadgetClass = Config::get('gadget_class');
+    $gadget = new $gadgetClass($gadgetSpec, $this->context);
+
+    // Process the gadget: fetching remote resources, processing & applying the correct translations, user prefs and feature resolving
+    $this->addSubstitutions($gadget);
+    $this->fetchResources($gadget);
+    $this->mergeLocales($gadget);
+    $this->parseUserPrefs($gadget);
+    $this->applySubstitutions($gadget);
+    $this->parseFeatures($gadget);
+    return $gadget;
+  }
+
+  /**
+   * Resolves the Required and Optional features and their dependencies into a real feature list using
+   * the GadgetFeatureRegistry, which can be used to construct the javascript for the gadget
+   *
+   * @param Gadget $gadget
+   */
+  protected function parseFeatures(Gadget &$gadget) {
+    $found = $missing = array();
+    if (! $this->context->getRegistry()->resolveFeatures(
+        $this->getNeededFeaturesForView($gadget),
+        $found, $missing)) {
+      $requiredMissing = false;
+      foreach ($missing as $featureName) {
+        if (isset($gadget->gadgetSpec->requiredFeatures[$featureName])) {
+          $requiredMissing = true;
+          break;
+        }
+      }
+      if ($requiredMissing) {
+        throw new GadgetException("Unknown features: " . implode(',', $missing));
+      }
+    }
+    $gadget->features = $found;
+  }
+
+  /**
+   * @param Gadget $gadget
+   * @return array
+   */
+  protected function getNeededFeaturesForView(Gadget &$gadget)
+  {
+    $neededFeatures = array();
+
+    $allFeatures = array_merge($gadget->gadgetSpec->requiredFeatures, $gadget->gadgetSpec->optionalFeatures);
+
+    foreach ($allFeatures as $featureName => $feature) {
+      if (! $feature['views'] || in_array($this->context->getView(), $feature['views'])) {
+        $neededFeatures[] = $featureName;
+      }
+    }
+
+    return $neededFeatures;
+  }
+
+  /**
+   * Applies the substitutions to the complex types (preloads, user prefs, etc). Simple
+   * types (author, title, etc) are translated on the fly in the gadget's getFoo() functions
+   *
+   * @param Gadget $gadget
+   */
+  protected function applySubstitutions(Gadget &$gadget) {
+    // Apply the substitutions to the UserPrefs
+    foreach ($gadget->gadgetSpec->userPrefs as $key => $pref) {
+
+      $gadget->substitutions->addSubstitution('UP', $gadget->substitutions->substitute($pref['name']), $gadget->substitutions->substitute($pref['value']));
+
+      $gadget->gadgetSpec->userPrefs[$key]['name'] = $gadget->substitutions->substitute($pref['name']);
+      $gadget->gadgetSpec->userPrefs[$key]['displayName'] = $gadget->substitutions->substitute($pref['displayName']);
+      $gadget->gadgetSpec->userPrefs[$key]['required'] = $gadget->substitutions->substitute($pref['required']);
+      $gadget->gadgetSpec->userPrefs[$key]['datatype'] = $gadget->substitutions->substitute($pref['datatype']);
+      $gadget->gadgetSpec->userPrefs[$key]['defaultValue'] = $gadget->substitutions->substitute($pref['defaultValue']);
+      $gadget->gadgetSpec->userPrefs[$key]['value'] = $gadget->substitutions->substitute($pref['value']);
+      if (isset($pref['enumValues'])) {
+        foreach ($pref['enumValues'] as $enumKey => $enumVal) {
+          $gadget->gadgetSpec->userPrefs[$key]['enumValues'][$enumKey]['value'] = $gadget->substitutions->substitute($enumVal['value']);
+          $gadget->gadgetSpec->userPrefs[$key]['enumValues'][$enumKey]['displayValue'] = $gadget->substitutions->substitute($enumVal['displayValue']);
+        }
+      }
+    }
+
+    // Apply substitutions to the preloads
+    foreach ($gadget->gadgetSpec->preloads as $key => $preload) {
+      $gadget->gadgetSpec->preloads[$key]['body'] = $gadget->substitutions->substitute($preload['body']);
+    }
+  }
+
+  /**
+   * Seeds the substitutions class with the user prefs, messages, bidi and module id
+   *
+   * @param Gadget $gadget
+   */
+  protected function addSubstitutions(Gadget &$gadget) {
+    $substiutionClass = Config::get('substitution_class');
+    $gadget->substitutions = new $substiutionClass();
+    if ($this->token) {
+      $gadget->substitutions->addSubstitution('MODULE', "ID", $this->token->getModuleId());
+    } else {
+      $gadget->substitutions->addSubstitution('MODULE', "ID", 0);
+    }
+    $gadget->substitutions->addSubstitution('BIDI', "START_EDGE", $gadget->rightToLeft ? "right" : "left");
+    $gadget->substitutions->addSubstitution('BIDI', "END_EDGE", $gadget->rightToLeft ? "left" : "right");
+    $gadget->substitutions->addSubstitution('BIDI', "DIR", $gadget->rightToLeft ? "rtl" : "ltr");
+    $gadget->substitutions->addSubstitution('BIDI', "REVERSE_DIR", $gadget->rightToLeft ? "ltr" : "rtl");
+  }
+
+  /**
+   * Process the UserPrefs values based on the current context
+   *
+   * @param Gadget $gadget
+   */
+  protected function parseUserPrefs(Gadget &$gadget) {
+    foreach ($gadget->gadgetSpec->userPrefs as $key => $pref) {
+      $queryKey = 'up_' . $pref['name'];
+      $gadget->gadgetSpec->userPrefs[$key]['value'] = isset($_GET[$queryKey]) ? trim(urldecode($_GET[$queryKey])) : $pref['defaultValue'];
+    }
+  }
+
+  /**
+   * Merges all matching Message bundles, with a full match (lang and country) having the
+   * highest priority and all/all having the lowest.
+   *
+   * This distills the locales array's back to one array of translations, which is then exposed
+   * through the $gadget->substitutions class
+   *
+   * @param Gadget $gadget
+   */
+  protected function mergeLocales(Gadget $gadget) {
+    if (count($gadget->gadgetSpec->locales)) {
+      $contextLocale = $this->context->getLocale();
+      $locales = $gadget->gadgetSpec->locales;
+      $gadget->rightToLeft = false;
+      $full = $partial = $all = array();
+      foreach ($locales as $locale) {
+        if ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == $contextLocale['country']) {
+          $full = array_merge($full, $locale['messageBundle']);
+          $gadget->rightToLeft = $locale['languageDirection'] == 'rtl';
+        } elseif ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == 'all') {
+          $partial = array_merge($partial, $locale['messageBundle']);
+        } elseif ($locale['country'] == 'all' && $locale['lang'] == 'all') {
+          $all = array_merge($all, $locale['messageBundle']);
+        }
+      }
+      $gadget->gadgetSpec->locales = array();
+      // array_merge overwrites duplicate keys from param 2 over param 1, \so $full takes precedence over partial, and it over all
+      if ($full) $gadget->gadgetSpec->locales = array_merge($full, $gadget->gadgetSpec->locales);
+      if ($partial) $gadget->gadgetSpec->locales = array_merge($partial, $gadget->gadgetSpec->locales);
+      if ($all) $gadget->gadgetSpec->locales = array_merge($all, $gadget->gadgetSpec->locales);
+      $gadget->substitutions->addSubstitutions('MSG', $gadget->gadgetSpec->locales);
+    }
+  }
+
+  /**
+   * Fetches all remote resources simultaniously using a multiFetchRequest to optimize rendering time.
+   *
+   * The preloads will be json_encoded to their gadget document injection format, and the locales will
+   * be reduced to only the GadgetContext->getLocale matching entries.
+   *
+   * @param Gadget $gadget
+   */
+  protected function fetchResources(Gadget &$gadget) {
+    $contextLocale = $this->context->getLocale();
+    $unsignedRequests = $signedRequests = array();
+    foreach ($gadget->gadgetSpec->locales as $key => $locale) {
+      // Only fetch the locales that match the current context's language and country
+      if ($locale['views'] && ! in_array($this->context->getView(), $locale['views'])) {
+        unset($gadget->gadgetSpec->locales[$key]);
+        continue;
+      }
+      if (($locale['country'] == 'all' && $locale['lang'] == 'all') || 
+          ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == 'all') ||
+          ($locale['lang'] == $contextLocale['lang'] && $locale['country'] == $contextLocale['country'])) {
+        if (! empty($locale['messages'])) {
+          $transformedUrl = RemoteContentRequest::transformRelativeUrl($locale['messages'], $this->context->getUrl());
+          if (! $transformedUrl) {
+             // remove any locales that are not applicable to this context
+             unset($gadget->gadgetSpec->locales[$key]);
+             continue;
+          } else {
+            $transformedUrl = $gadget->substitutions->substitute($transformedUrl);
+            $gadget->gadgetSpec->locales[$key]['messages'] = $transformedUrl;
+           }
+          // locale matches the current context, add it to the requests queue
+          $request = new RemoteContentRequest($gadget->gadgetSpec->locales[$key]['messages'] );
+          $request->createRemoteContentRequestWithUri($gadget->gadgetSpec->locales[$key]['messages'] );
+          $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
+          $unsignedRequests[] = $request;
+        }
+      } else {
+        // remove any locales that are not applicable to this context
+        unset($gadget->gadgetSpec->locales[$key]);
+      }
+    }
+    if (! $gadget->gadgetContext instanceof MetadataGadgetContext) {
+      // Add preloads to the request queue
+      foreach ($gadget->getPreloads() as $id => $preload) {
+        if ($preload['views'] && ! in_array($this->context->getView(), $preload['views'])) {
+          unset($gadget->gadgetSpec->preloads[$id]);
+          continue;
+        }
+        if (! empty($preload['href'])) {
+          $preload['href'] = $gadget->substitutions->substitute($preload['href']);
+          $request = new RemoteContentRequest($preload['href']);
+          if (! empty($preload['authz']) && $preload['authz'] == 'SIGNED') {
+            if ($this->token == '') {
+              throw new GadgetException("Signed preloading requested, but no valid security token set");
+            }
+            $request = new RemoteContentRequest($preload['href']);
+            $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+            $request->setNotSignedUri($preload['href']);
+            $request->setToken($this->token);
+            $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
+            if (strcasecmp($preload['signViewer'], 'false') == 0) {
+              $request->getOptions()->viewerSigned = false;
+            }
+            if (strcasecmp($preload['signOwner'], 'false') == 0) {
+              $request->getOptions()->ownerSigned = false;
+            }
+            $signedRequests[] = $request;
+          } else {
+            $request->createRemoteContentRequestWithUri($preload['href']);
+            $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
+            $unsignedRequests[] = $request;
+          }
+        }
+      }
+      // Add template libraries to the request queue
+      if ($gadget->gadgetSpec->templatesRequireLibraries) {
+        foreach ($gadget->gadgetSpec->templatesRequireLibraries as $key => $libraryUrl) {
+          $gadget->gadgetSpec->templatesRequireLibraries[$key] = $libraryUrl = $gadget->substitutions->substitute($libraryUrl);
+          $request = new RemoteContentRequest($libraryUrl);
+          $transformedUrl = RemoteContentRequest::transformRelativeUrl($libraryUrl, $this->context->getUrl());
+          if (! $transformedUrl) {
+            continue;
+          } else {
+            $gadget->gadgetSpec->templatesRequireLibraries[$key] = $transformedUrl;
+          }
+          $request->createRemoteContentRequestWithUri($gadget->gadgetSpec->templatesRequireLibraries[$key]);
+          $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
+          $unsignedRequests[] = $request;
+        }
+      }
+    }
+
+    $responses = $this->performRequests($unsignedRequests, $signedRequests);
+    
+    // assign the results to the gadget locales and preloads (using the url as the key)
+    foreach ($gadget->gadgetSpec->locales as $key => $locale) {
+      if (! empty($locale['messages']) && isset($responses[$locale['messages']]) && $responses[$locale['messages']]['rc'] == 200) {
+        $gadget->gadgetSpec->locales[$key]['messageBundle'] = $this->parseMessageBundle($responses[$locale['messages']]['body']);
+      }
+    }
+    if (! $gadget->gadgetContext instanceof MetadataGadgetContext) {
+	    $preloads = array();
+	    foreach ($gadget->gadgetSpec->preloads as $key => $preload) {
+	      if (! empty($preload['href']) && isset($responses[$preload['href']]) && $responses[$preload['href']]['rc'] == 200) {
+	        $preloads[] = array_merge(array('id' => $preload['href']), $responses[$preload['href']]);
+	      }
+	    }
+	    $gadget->gadgetSpec->preloads = $preloads;
+	    if ($gadget->gadgetSpec->templatesRequireLibraries) {
+	    	 $requiredLibraries = array();
+		    foreach ($gadget->gadgetSpec->templatesRequireLibraries as $key => $libraryUrl) {
+		    	if (isset($responses[$libraryUrl]) && $responses[$libraryUrl]['rc'] == 200) {
+		    		$requiredLibraries[$libraryUrl] = $responses[$libraryUrl]['body'];
+		    	}
+		    }
+		    $gadget->gadgetSpec->templatesRequireLibraries = $requiredLibraries;
+	    }
+    }
+  }
+
+  /**
+   * perform all requests
+   * 
+   * @param array $unsignedRequests
+   * @param array $signedRequests
+   * @return array
+   */
+  protected function performRequests($unsignedRequests, $signedRequests) {
+    // Perform the non-signed requests
+    $responses = array();
+    if (count($unsignedRequests)) {
+      $brc = new BasicRemoteContent();
+      $resps = $brc->multiFetch($unsignedRequests);
+      foreach ($resps as $response) {
+        $responses[$response->getUrl()] = array(
+            'body' => $response->getResponseContent(),
+            'rc' => $response->getHttpCode());
+      }
+    }
+
+    // Perform the signed requests
+    if (count($signedRequests)) {
+      $signingFetcherFactory = new SigningFetcherFactory(Config::get("private_key_file"));
+      $remoteFetcherClass = Config::get('remote_content_fetcher');
+      $remoteFetcher = new $remoteFetcherClass();
+      $remoteContent = new BasicRemoteContent($remoteFetcher, $signingFetcherFactory);
+      $resps = $remoteContent->multiFetch($signedRequests);
+      foreach ($resps as $response) {
+        $responses[$response->getNotSignedUrl()] = array(
+            'body' => $response->getResponseContent(),
+            'rc' => $response->getHttpCode());
+      }
+    }
+
+    return $responses;
+  }
+
+  /**
+   * Parses the (remote / fetched) message bundle xml
+   *
+   * @param string $messageBundleData
+   * @return array (MessageBundle)
+   */
+  protected function parseMessageBundle($messageBundleData) {
+    libxml_use_internal_errors(true);
+    $doc = new \DOMDocument();
+    if (! $doc->loadXML($messageBundleData, LIBXML_NOCDATA)) {
+      throw new GadgetSpecException("Error parsing gadget xml:\n" . XmlError::getErrors($messageBundleData));
+    }
+    $messageBundle = array();
+    if (($messageBundleNode = $doc->getElementsByTagName('messagebundle')) != null && $messageBundleNode->length > 0) {
+      $messageBundleNode = $messageBundleNode->item(0);
+      $messages = $messageBundleNode->getElementsByTagName('msg');
+      foreach ($messages as $msg) {
+        $messageBundle[$msg->getAttribute('name')] = trim($msg->nodeValue);
+      }
+    }
+    return $messageBundle;
+  }
+
+  /**
+   * Fetches the gadget xml for the requested URL using the http fetcher
+   *
+   * @param string $gadgetUrl
+   * @return string gadget's xml content
+   */
+  protected function fetchGadget($gadgetUrl) {
+    $request = new RemoteContentRequest($gadgetUrl);
+    $request->setToken($this->token);
+    $request->getOptions()->ignoreCache = $this->context->getIgnoreCache();
+    $xml = $this->context->getHttpFetcher()->fetch($request);
+    if ($xml->getHttpCode() != '200') {
+      throw new GadgetException("Failed to retrieve gadget content (recieved http code " . $xml->getHttpCode() . ")");
+    }
+    return $xml->getResponseContent();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetFeatureRegistry.php b/trunk/php/src/apache/shindig/gadgets/GadgetFeatureRegistry.php
new file mode 100644
index 0000000..48c9d22
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetFeatureRegistry.php
@@ -0,0 +1,376 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\Config;
+use apache\shindig\common\Cache;
+use apache\shindig\common\File;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Class that deals with the processing, loading and dep resolving of the gadget features
+ * Features are javascript libraries that provide an API, like 'opensocial' or 'settitle'
+ *
+ */
+class GadgetFeatureRegistry {
+  const FEATURE_CONTEXT_GADGET = 'gadget';
+  const FEATURE_CONTEXT_CONTAINER = 'container';
+
+  /**
+   * @var array
+   */
+  public $features = array();
+
+  /**
+   * @var array
+   */
+  private $sortedFeatures;
+
+  /**
+   *
+   * @param miexed $featurePath
+   */
+  public function __construct($featurePath) {
+    if (is_array($featurePath)) {
+      foreach ($featurePath as $path) {
+        $this->registerFeatures($path);
+      }
+    } else {
+      $this->registerFeatures($featurePath);
+    }
+    $this->processFeatures();
+  }
+
+  /**
+   *
+   * @param array $features
+   * @param GadgetContext $context
+   * @param boolean $isGadgetContext
+   * @return string
+   */
+  public function getFeaturesContent($features, GadgetContext $context, $isGadgetContext) {
+    $ret = '';
+    foreach ($features as $feature) {
+      $ret .= $this->getFeatureContent($feature, $context, $isGadgetContext);
+    }
+    return $ret;
+  }
+
+  /**
+   *
+   * @param string $feature
+   * @param GadgetContext $context
+   * @param boolean $isGadgetContext
+   * @return string
+   */
+  public function getFeatureContent($feature, GadgetContext $context, $isGadgetContext) {
+    if (empty($feature)) return '';
+    if (!isset($this->features[$feature])) {
+      throw new GadgetException("Invalid feature: ".htmlentities($feature));
+    }
+    $featureName = $feature;
+    $feature = $this->features[$feature];
+    $filesContext = $isGadgetContext ? 'gadgetJs' : 'containerJs';
+    if (!isset($feature[$filesContext])) {
+      // no javascript specified for this context
+      return '';
+    }
+    $ret = '';
+    if (Config::get('compress_javascript') && ! isset($_GET['debug'])) {
+      $featureCache = Cache::createCache(Config::get('feature_cache'), 'FeatureCache');
+      if (($featureContent = $featureCache->get(md5('features:'.$featureName.$isGadgetContext)))) {
+        return $featureContent;
+      }
+    }
+    foreach ($feature[$filesContext] as $entry) {
+      switch ($entry['type']) {
+        case 'URL':
+          $request = new RemoteContentRequest($entry['content']);
+          $request->getOptions()->ignoreCache = $context->getIgnoreCache();
+          $response = $context->getHttpFetcher()->fetch($request);
+          if ($response->getHttpCode() == '200') {
+            $ret .= $response->getResponseContent()."\n";
+          }
+          break;
+        case 'FILE':
+          $file = $feature['basePath'] . '/' . $entry['content'];
+          $ret .= file_get_contents($file). "\n";
+          break;
+        case 'INLINE':
+          $ret .= $entry['content'] . "\n";
+          break;
+      }
+    }
+    if (Config::get('compress_javascript') && ! isset($_GET['debug'])) {
+      $ret = \JsMin::minify($ret);
+      $featureCache->set(md5('features:'.$featureName.$isGadgetContext), $ret);
+    }
+    return $ret;
+  }
+
+  /**
+   *
+   * @param array $needed
+   * @param array $resultsFound
+   * @param array $resultsMissing
+   * @return boolean
+   */
+  public function resolveFeatures($needed, &$resultsFound, &$resultsMissing) {
+    $resultsFound = array();
+    $resultsMissing = array();
+    $this->addFeatureToResults($resultsFound, $this->features['core']);
+
+    foreach ($needed as $featureName) {
+      $feature = isset($this->features[$featureName]) ? $this->features[$featureName] : null;
+      if ($feature == null) {
+        $resultsMissing[] = $featureName;
+      } else {
+        $this->addFeatureToResults($resultsFound, $feature);
+      }
+    }
+    return count($resultsMissing) == 0;
+  }
+
+  /**
+   *
+   * @param array $features
+   * @param array $sortedFeatures
+   */
+  public function sortFeatures($features, &$sortedFeatures) {
+    if (empty($features)) {
+      return;
+    }
+    $sortedFeatures = array();
+    foreach ($this->sortedFeatures as $feature) {
+      if (in_array($feature, $features)) {
+        $sortedFeatures[] = $feature;
+      }
+    }
+  }
+
+  /**
+   *
+   * @param array $results
+   * @param array $feature
+   */
+  private function addFeatureToResults(&$results, $feature) {
+    if (in_array($feature['name'], $results)) {
+      return;
+    }
+    foreach ($feature['deps'] as $dep) {
+      $this->addFeatureToResults($results, $this->features[$dep]);
+    }
+    if (!in_array($feature['name'], $results)) {
+      $results[] = $feature['name'];
+    }
+  }
+
+  /**
+   * Loads the features present in the $featurePath
+   *
+   * @param string $featurePath path to scan
+   */
+  private function registerFeatures($featurePath) {
+    // Load the features from the shindig/features/features.txt file
+    $featuresFile = $featurePath . '/features.txt';
+    if (File::exists($featuresFile)) {
+      $files = explode("\n", file_get_contents($featuresFile));
+      // custom sort, else core.io seems to bubble up before core, which breaks the dep chain order
+      usort($files, array($this, 'sortFeaturesFiles'));
+      foreach ($files as $file) {
+        if (! empty($file) && strpos($file, 'feature.xml') !== false && substr($file, 0, 1) != '#' && substr($file, 0, 2) != '//') {
+          $file = realpath($featurePath . '/../' . trim($file));
+          $feature = $this->processFile($file);
+          $this->features[$feature['name']] = $feature;
+        }
+      }
+    }
+  }
+
+  /**
+   * gets core features and sorts features
+   */
+  private function processFeatures() {
+    $sortedFeatures = array();
+    // Topologically sort all features according to their dependency
+    $features = array();
+    foreach ($this->features as $feature) {
+      $features[] = $feature['name'];
+    }
+    $reverseDeps = array();
+    foreach ($features as $feature) {
+      $reverseDeps[$feature] = array();
+    }
+    $depCount = array();
+    foreach ($features as $feature) {
+      $deps = $this->features[$feature]['deps'];
+      $deps = array_uintersect($deps, $features, "strcasecmp");
+      $depCount[$feature] = count($deps);
+      foreach ($deps as $dep) {
+        $reverseDeps[$dep][] = $feature;
+      }
+    }
+    while (! empty($depCount)) {
+      $fail = true;
+      foreach ($depCount as $feature => $count) {
+        if ($count != 0) continue;
+        $fail = false;
+        $sortedFeatures[] = $feature;
+        foreach ($reverseDeps[$feature] as $reverseDep) {
+          $depCount[$reverseDep] -= 1;
+        }
+        unset($depCount[$feature]);
+      }
+      if ($fail && ! empty($depCount)) {
+        throw new GadgetException("Sorting feature dependence failed: it contains ring!");
+      }
+    }
+    $this->sortedFeatures = $sortedFeatures;
+  }
+
+  /**
+   * Loads the feature's xml content
+   *
+   * @param string $file
+   * @return array
+   */
+  private function processFile($file) {
+    $feature = null;
+    if (!empty($file) && File::exists($file)) {
+      if (($content = file_get_contents($file))) {
+        $feature = $this->parse($content, dirname($file));
+      }
+    }
+    return $feature;
+  }
+
+  /**
+   * Parses the feature's XML content
+   *
+   * @param string $content
+   * @param string $path
+   * @return array
+   */
+  protected function parse($content, $path) {
+    $doc = simplexml_load_string($content);
+    $feature = array();
+    $feature['deps'] = array();
+    $feature['basePath'] = $path;
+    if (! isset($doc->name)) {
+      throw new GadgetException('Invalid name in feature: ' . $path);
+    }
+    $feature['name'] = trim($doc->name);
+    if ($doc->gadget) {
+      foreach ($doc->gadget as $gadget) {
+        $this->processContext($feature, $gadget, self::FEATURE_CONTEXT_GADGET);
+      }
+    } else if ($doc->all) {
+      foreach ($doc->all as $container) {
+        $this->processContext($feature, $container, self::FEATURE_CONTEXT_GADGET);
+      }
+    }
+    if ($doc->container) {
+      foreach ($doc->container as $container) {
+        $this->processContext($feature, $container, self::FEATURE_CONTEXT_CONTAINER);
+      }
+    } else if ($doc->all) {
+      foreach ($doc->all as $container) {
+        $this->processContext($feature, $container, self::FEATURE_CONTEXT_CONTAINER);
+      }
+    }
+    foreach ($doc->dependency as $dependency) {
+      $feature['deps'][trim($dependency)] = trim($dependency);
+    }
+    return $feature;
+  }
+
+  /**
+   * Processes the feature's entries
+   *
+   * @param array $feature
+   * @param string $context
+   * @param string $envContext container or gadget
+   */
+  private function processContext(&$feature, $context, $envContext) {
+    foreach ($context->script as $script) {
+      $attributes = $script->attributes();
+      if (! isset($attributes['src'])) {
+        // inline content
+        $type = 'INLINE';
+        $content = (string)$script;
+      } else {
+        $content = trim($attributes['src']);
+        $url = parse_url($content);
+
+        if (! isset($url['scheme']) || ! isset($url['path']) || ! isset($url['host'])) {
+          $type = 'FILE';
+          $content = $content;
+        } else {
+          $type = false;
+          switch ($url['scheme']) {
+            case 'res':
+              $type = 'URL';
+              $scheme = (! isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https';
+              $content = $scheme . '://' . (Config::get('http_host') ? Config::get('http_host') : $_SERVER['HTTP_HOST']) . '/gadgets/resources/' . $url['host'] . $url['path'];
+              break;
+            case 'http':
+            case 'https':
+              $type = 'URL';
+              break;
+            default:
+              $type = 'FILE';
+              $content = $content;
+          }
+        }
+      }
+
+      if (! $type) {
+        continue;
+      }
+
+      $library = array('type' => $type, 'content' => $content);
+      if ($library != null) {
+        switch ($envContext) {
+          case self::FEATURE_CONTEXT_GADGET:
+            $feature['gadgetJs'][] = $library;
+            break;
+          case self::FEATURE_CONTEXT_CONTAINER:
+            $feature['containerJs'][] = $library;
+            break;
+        }
+      }
+    }
+  }
+
+  /**
+   *
+   * @param string $feature1
+   * @param string $feature2
+   * @return boolean
+   */
+  private function sortFeaturesFiles($feature1, $feature2) {
+    $feature1 = basename(str_replace('/feature.xml', '', $feature1));
+    $feature2 = basename(str_replace('/feature.xml', '', $feature2));
+    if ($feature1 == $feature2) {
+      return 0;
+    }
+    return ($feature1 < $feature2) ? - 1 : 1;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetSpec.php b/trunk/php/src/apache/shindig/gadgets/GadgetSpec.php
new file mode 100644
index 0000000..99fcf7a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetSpec.php
@@ -0,0 +1,191 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetSpec {
+  const DOCTYPE_QUIRKSMODE = "quirksmode";
+    
+  /**
+   * MD5 checksum of the xml's content
+   *
+   * @var string
+   */
+  public $checksum;
+
+  // Basic and extended ModulePrefs attributes
+  /**
+   * @var string
+   */
+  public $title;
+  /**
+   * @var string
+   */
+  public $author;
+  /**
+   * @var string
+   */
+  public $authorEmail;
+  /**
+   * @var string
+   */
+  public $description;
+  /**
+   * @var string
+   */
+  public $directoryTitle;
+  /**
+   * @var string
+   */
+  public $screenshot;
+  /**
+   * @var string
+   */
+  public $thumbnail;
+  /**
+   * @var string
+   */
+  public $titleUrl;
+  /**
+   * @var string
+   */
+  public $authorAffiliation;
+  /**
+   * @var string
+   */
+  public $authorLocation;
+  /**
+   * @var string
+   */
+  public $authorPhoto;
+  /**
+   * @var string
+   */
+  public $authorAboutme;
+  /**
+   * @var string
+   */
+  public $authorQuote;
+  /**
+   * @var string
+   */
+  public $authorLink;
+  /**
+   * @var string
+   */
+  public $showStats;
+  /**
+   * @var string
+   */
+  public $showInDirectory;
+  /**
+   * @var string
+   */
+  public $string;
+  /**
+   * @var string
+   */
+  public $width;
+  /**
+   * @var string
+   */
+  public $height;
+  /**
+   * @var string
+   */
+  public $category;
+  /**
+   * @var string
+   */
+  public $category2;
+  /**
+   * @var string
+   */
+  public $singleton;
+  /**
+   * @var string
+   */
+  public $renderInline;
+  /**
+   * @var string
+   */
+  public $scaling;
+  /**
+   * @var string
+   */
+  public $scrolling;
+  /**
+   * @var string
+   */
+  public $preloads;
+  /**
+   * @var string
+   */
+  public $locales;
+  /**
+   * @var string
+   */
+  public $icon;
+  /**
+   * @var string
+   */
+  public $optionalFeatures;
+  /**
+   * @var string
+   */
+  public $requiredFeatures;
+  /**
+   * @var string
+   */
+  public $links;
+  /**
+   * @var string
+   */
+  public $userPrefs;
+  /**
+   * @var string
+   */
+  public $rewrite = null;
+  /**
+   * @var string
+   */
+  public $oauth = null;
+
+  // used to track os-templating
+
+  /**
+   * @var boolean
+   */
+  public $templatesRequireLibraries = false;
+  /**
+   * @var boolean
+   */
+  public $templatesDisableAutoProcessing = false;
+  
+  /**
+   * @var string
+   */
+  public $doctype;
+  
+  /**
+   * @var OpenSocialVersion
+   */
+  public $specificationVersion;
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetSpecException.php b/trunk/php/src/apache/shindig/gadgets/GadgetSpecException.php
new file mode 100644
index 0000000..7532dfd
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetSpecException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetSpecException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/GadgetSpecParser.php b/trunk/php/src/apache/shindig/gadgets/GadgetSpecParser.php
new file mode 100644
index 0000000..f0294c2
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/GadgetSpecParser.php
@@ -0,0 +1,485 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\OpenSocialVersion;
+use apache\shindig\gadgets\oauth\OAuthService;
+use apache\shindig\common\XmlError;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\templates\DataPipelining;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Parses the XML content into a GadgetSpec object
+ */
+class GadgetSpecParser {
+
+  /**
+   *
+   * @var GadgetContext
+   */
+  protected $context;
+
+  /**
+   * Parses the $xmlContent into a Gadget class
+   *
+   * @param string $xmlContent
+   * @param GadgetContext $context
+   * @return GadgetSpec
+   */
+  public function parse($xmlContent, GadgetContext $context = null) {
+    $this->context = $context;
+    libxml_use_internal_errors(true);
+    $doc = new \DOMDocument();
+    if (! $doc->loadXML($xmlContent, LIBXML_NOCDATA)) {
+      throw new GadgetSpecException("Error parsing gadget xml:\n" . XmlError::getErrors($xmlContent));
+    }
+
+    //TODO: we could do a XSD schema validation here, but both the schema and most of the gadgets seem to have some form of schema
+    // violatons, so it's not really practical yet (and slow)
+    // $doc->schemaValidate('gadget.xsd');
+    $gadgetSpecClass = Config::get('gadget_spec_class');
+    $gadget = new $gadgetSpecClass();
+    $gadget->checksum = md5($xmlContent);
+    $this->parseModuleTag($doc, $gadget);
+    $this->parseModulePrefs($doc, $gadget);
+    $this->parseUserPrefs($doc, $gadget);
+    $this->parseViews($doc, $gadget);
+    //TODO: parse pipelined data
+    return $gadget;
+  }
+
+  /**
+   * Parse the gadget views
+   *
+   * @param DOMDocument $doc
+   * @param GadgetSpec $gadget
+   */
+  private function parseViews(\DOMDocument &$doc, GadgetSpec &$gadget) {
+    $views = $doc->getElementsByTagName('Content');
+    if (! $views || $views->length < 1) {
+      throw new GadgetSpecException("A gadget needs to have at least one view");
+    }
+    $gadget->views = array();
+    foreach ($views as $viewNode) {
+      if ($viewNode->getAttribute('type' == 'url') && $viewNode->getAttribute('href') == null) {
+        throw new GadgetSpecException("Malformed <Content> href value");
+      }
+      foreach (explode(',', $viewNode->getAttribute('view')) as $view) {
+        $view = trim($view);
+        $href = trim($viewNode->getAttribute('href'));
+        $type = trim(strtoupper($viewNode->getAttribute('type')));
+        if (empty($type)) {
+          $type = 'html';
+        }
+        $dataPipeliningRequests = array();
+        if (! empty($href) && $type == 'HTML') {
+          // a non empty href & type == 'HTML' means there might be data-pipelining tags in the content section
+          $dataPipeliningRequests = DataPipelining::parse($viewNode);
+        }
+        if (isset($gadget->views[$view])) {
+          $gadget->views[$view]['content'] .= $viewNode->nodeValue;
+        } else {
+          $gadget->views[$view] = array('view' => $view, 'type' => $type,
+              'href' => $href,
+              'preferedHeight' => $viewNode->getAttribute('prefered_height'),
+              'preferedWidth' => $viewNode->getAttribute('prefered_width'),
+              'quirks' => $viewNode->getAttribute('quirks'),
+              'content' => $viewNode->nodeValue,
+              'authz' => $viewNode->getAttribute('authz'),
+              'oauthServiceName' => $viewNode->getAttribute('oauth_service_name'),
+              'oauthTokenName' => $viewNode->getAttribute('oauth_token_name'),
+              'oauthRequestToken' => $viewNode->getAttribute('oauth_request_token'),
+              'oauthRequestTokenSecret' => $viewNode->getAttribute('oauth_request_token_secret'),
+              'signOwner' => $viewNode->getAttribute('sign_owner'),
+              'signViewer' => $viewNode->getAttribute('sign_viewer'),
+              'refreshInterval' => $viewNode->getAttribute('refresh_interval'),
+              'dataPipelining' => $dataPipeliningRequests);
+        }
+      }
+    }
+  }
+
+  /**
+   *
+   * @param string $attribute
+   * @return array
+   */
+  private function parseViewAttribute($attribute)
+  {
+    if (! $attribute) {
+      return array();
+    }
+    return explode(',', str_replace(' ', '', $attribute));
+  }
+
+  /**
+   * Parses the UserPref entries
+   *
+   * @param DOMDocument $doc
+   * @param GadgetSpec $gadget
+   */
+  private function parseUserPrefs(\DOMDocument &$doc, GadgetSpec &$gadget) {
+    $gadget->userPrefs = array();
+    if (($userPrefs = $doc->getElementsByTagName('UserPref')) != null) {
+      foreach ($userPrefs as $prefNode) {
+        $pref = array('name' => $prefNode->getAttribute('name'),
+            'displayName' => $prefNode->getAttribute('display_name'),
+            'datatype' => strtoupper($prefNode->getAttribute('datatype')),
+            'defaultValue' => $prefNode->getAttribute('default_value'),
+            'required' => $prefNode->getAttribute('required'));
+        if ($pref['datatype'] == 'ENUM') {
+          if (($enumValues = $prefNode->getElementsByTagName('EnumValue')) != null) {
+            $enumVals = array();
+            foreach ($enumValues as $enumNode) {
+              $enumVals[] = array(
+                  'value' => $enumNode->getAttribute('value'),
+                  'displayValue' => $enumNode->getAttribute('display_value'));
+            }
+          }
+          $pref['enumValues'] = $enumVals;
+        }
+        $gadget->userPrefs[] = $pref;
+      }
+    }
+  }
+
+  /**
+   * Parses the link spec elements
+   *
+   * @param DOMElement $modulePrefs
+   * @param GadgetSpec $gadget
+   */
+  private function parseLinks(\DOMElement &$modulePrefs, GadgetSpec &$gadget) {
+    $gadget->links = array();
+    if (($links = $modulePrefs->getElementsByTagName('Link')) != null) {
+      foreach ($links as $linkNode) {
+        $gadget->links[] = array('rel' => $linkNode->getAttribute('rel'),
+            'href' => $linkNode->getAttribute('href'),
+            'method' => strtoupper($linkNode->getAttribute('method')));
+      }
+    }
+  }
+
+  /**
+   *
+   * @param DOMDocument $doc
+   * @param GadgetSpec $gadget
+   */
+  private function parseModuleTag(\DOMDocument &$doc, GadgetSpec &$gadget) {
+    $moduleTag = $doc->getElementsByTagName("Module");
+    if ($moduleTag->length < 1) {
+      throw new GadgetSpecException("Missing Module block");
+    } elseif ($moduleTag->length > 1) {
+      throw new GadgetSpecException("More then one Module block found");
+    }
+    $moduleTag = $moduleTag->item(0);
+    $specVersion = $moduleTag->getAttribute('specificationVersion');
+    if ($specVersion) {
+        $gadget->specificationVersion = new OpenSocialVersion(str_replace(' ', '', $specVersion));
+    } else {
+        $gadget->specificationVersion = new OpenSocialVersion();
+    }
+  }
+
+  /**
+   * Parses the ModulePrefs section of the xml structure. The ModulePrefs
+   * section is required, so if it's missing or if there's 2 an GadgetSpecException will be thrown.
+   *
+   * This function also parses the ModulePref's child elements (Icon, Features, Preload and Locale)
+   *
+   * @param DOMDocument $doc
+   * @param GadgetSpec $gadget
+   */
+  private function parseModulePrefs(\DOMDocument &$doc, GadgetSpec &$gadget) {
+    $modulePrefs = $doc->getElementsByTagName("ModulePrefs");
+    if ($modulePrefs->length < 1) {
+      throw new GadgetSpecException("Missing ModulePrefs block");
+    } elseif ($modulePrefs->length > 1) {
+      throw new GadgetSpecException("More then one ModulePrefs block found");
+    }
+    $modulePrefs = $modulePrefs->item(0);
+    // parse the ModulePrefs attributes
+    $knownAttributes = array('title', 'author', 'authorEmail', 'description',
+        'directoryTitle', 'screenshot', 'thumbnail', 'titleUrl', 'authorAffiliation',
+        'authorLocation', 'authorPhoto', 'authorAboutme', 'authorQuote', 'authorLink',
+        'showStats', 'showInDirectory', 'string', 'width', 'height', 'category',
+        'category2', 'singleton', 'renderInline', 'scaling', 'scrolling', 'doctype');
+    foreach ($modulePrefs->attributes as $key => $attribute) {
+      $attrValue = trim($attribute->value);
+      // var format conversion from directory_title => directoryTitle
+      $attrKey = str_replace(' ', '', ucwords(str_replace('_', ' ', $key)));
+      $attrKey[0] = strtolower($attrKey[0]);
+      if (in_array($attrKey, $knownAttributes)) {
+        $gadget->$attrKey = $attrValue;
+      }
+    }
+    // And parse the child nodes
+    $this->parseLinks($modulePrefs, $gadget);
+    $this->parseIcon($modulePrefs, $gadget);
+    $this->parseFeatures($modulePrefs, $gadget);
+    $this->parsePreloads($modulePrefs, $gadget);
+    $this->parseLocales($modulePrefs, $gadget);
+    $this->parseOAuth($modulePrefs, $gadget);
+    $this->parseContainerSpecific($modulePrefs, $gadget);
+  }
+
+ /**
+  * Parses optional container specific moduleprefs
+  * override if needed
+  *
+  * @param DOMElement $modulePrefs
+  * @param GadgetSpec $gadget
+  */
+  protected function parseContainerSpecific(\DOMElement &$modulePrefs, GadgetSpec &$gadget) {
+
+  }
+
+  /**
+   * Parses the (optional) Icon element, returns a Icon class or null
+   *
+   * @param DOMElement $modulePrefs
+   * @param GadgetSpec $gadget
+   */
+  private function parseIcon(\DOMElement &$modulePrefs, GadgetSpec &$gadget) {
+    if (($iconNodes = $modulePrefs->getElementsByTagName('Icon')) != null) {
+      if ($iconNodes->length > 1) {
+        throw new GadgetSpecException("A gadget can only have one Icon element");
+      } elseif ($iconNodes->length == 1) {
+        $icon = $iconNodes->item(0);
+        $gadget->icon = $icon->nodeValue;
+      }
+    }
+  }
+
+  /**
+   * Parses the Required and Optional feature entries in the ModulePrefs
+   *
+   * @param DOMElement $modulePrefs
+   * @param GadgetSpec $gadget
+   */
+  private function parseFeatures(\DOMElement &$modulePrefs, GadgetSpec &$gadget) {
+    $requiredNodes = $modulePrefs->getElementsByTagName('Require');
+    $gadget->requiredFeatures = $this->parseFeatureNodes($requiredNodes, $gadget);
+    $optionalNodes = $modulePrefs->getElementsByTagName('Optional');
+    $gadget->optionalFeatures = $this->parseFeatureNodes($optionalNodes, $gadget);
+  }
+
+  /**
+   *
+   * @param DOMNodeList $nodes
+   * @param GadgetSpec $gadget
+   * @return array
+   */
+  private function parseFeatureNodes(\DOMNodeList &$nodes, GadgetSpec &$gadget) {
+    $features = array();
+    foreach ($nodes as $feature) {
+      $features[$feature->getAttribute('feature')] = array(
+          'views' => $this->parseViewAttribute($feature->getAttribute('views')),
+      );
+      // Content-rewrite is a special case since it has Params as child nodes
+      if ($feature->getAttribute('feature') == 'content-rewrite') {
+        $this->parseContentRewrite($feature, $gadget);
+      } elseif ($feature->getAttribute('feature') == 'opensocial-templates') {
+        $this->parseOpenSocialTemplates($feature, $gadget);
+      }
+    }
+    return $features;
+  }
+
+  /**
+   * Parses the gadget's OAuth entries, the OAuth entry would look something like:
+   * <OAuth>
+   *   <Service name="google">
+   *     <Access url="https://www.google.com/accounts/OAuthGetAccessToken" method="GET" />
+   *     <Request url="https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www.google.com/m8/feeds/" method="GET" />
+   *     <Authorization url="https://www.google.com/accounts/OAuthAuthorizeToken?oauth_callback=http://oauth.gmodules.com/gadgets/oauthcallback" />
+   *   </Service>
+   * </OAuth>
+   *
+   * And the resulting $gadgetSpec->oauth structure:
+   *
+   * Array (
+   *     [access] => Array (
+   *             [url] => https://www.google.com/accounts/OAuthGetAccessToken
+   *             [method] => GET
+   *         )
+   *     [request] => Array (
+   *             [url] => https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www.google.com/m8/feeds/
+   *             [method] => GET
+   *         )
+   *     [authorization] => Array (
+   *             [url] => https://www.google.com/accounts/OAuthAuthorizeToken?oauth_callback=http://oauth.gmodules.com/gadgets/oauthcallback
+   *             [method] => GET
+   *         )
+   * )
+   *
+   * @param DOMElement $modulePrefs
+   * @param GadgetSpec $gadget
+   */
+  private function parseOAuth(\DOMElement &$modulePrefs, GadgetSpec &$gadget) {
+    $this->parseOAuthNodes($modulePrefs->getElementsByTagName('OAuth'), $gadget);
+    $this->parseOAuthNodes($modulePrefs->getElementsByTagName('OAuth2'), $gadget);
+  }
+
+  /**
+   * parses the actual oauth or oauth2 DOM node
+   *
+   * @param DOMNodeList $oauthNodes
+   * @param GadgetSpec $gadget
+   */
+  private function parseOAuthNodes(\DOMNodeList $oauthNodes, GadgetSpec &$gadget) {
+    if ($oauthNodes != null) {
+      if ($oauthNodes->length > 1) {
+        throw new GadgetSpecException("A gadget can only have one OAuth element (though multiple service entries are allowed in that one OAuth element)");
+      }
+      $oauth = array();
+      if ($oauthNodes->length > 0) {
+        $oauthNode = $oauthNodes->item(0);
+        if (($serviceNodes = $oauthNode->getElementsByTagName('Service')) != null) {
+          foreach ($serviceNodes as $service) {
+            $oauthService = new OAuthService($service);
+            $oauth[$oauthService->getName()] = $oauthService;
+          }
+        }
+        $gadget->oauth = $oauth;
+      }
+    }
+  }
+
+  /**
+   * Parses the opensocial-template params (if any), supported params are:
+   * <Require feature="opensocial-templates">
+   *   <Param name="requireLibrary">http://www.example.com/templates.xml</Param>
+   *   <Param name="disableAutoProcessing">false</Param>
+   * </Require>
+   * @param DOMElement $feature
+   * @param GadgetSpec $gadget
+   */
+  private function parseOpenSocialTemplates(\DOMElement $feature, GadgetSpec &$gadget) {
+    $requireLibraries = array();
+    if (($paramNodes = $feature->getElementsByTagName('Param')) != null) {
+    	foreach ($paramNodes as $param) {
+	      $paramName = $param->getAttribute('name');
+	      $paramValue = trim($param->nodeValue);
+	      if ($paramName == 'disableAutoProcessing') {
+	      	$gadget->templatesDisableAutoProcessing = $paramValue != 'false';
+	      } elseif ($paramName == 'requireLibrary') {
+	      	$requireLibraries[] = $paramValue;
+	      }
+    	}
+    }
+    if (count($requireLibraries)) {
+    	$gadget->templatesRequireLibraries = $requireLibraries;
+    }
+  }
+
+  /**
+   * Parses the content-rewrite feature's params, possible params entries are:
+   *   <Param name="expires">86400</Param>
+   *   <Param name="include-url">*</Param>
+   *   <Param name="exclude-url">.png</Param>
+   *   <Param name="exclude-url">.tmp</Param>
+   *   <Param name="minify-css">true</Param>
+   *   <Param name="minify-js">true</Param>
+   *   <Param name="minify-html">true</Param>
+   *
+   * This sets the $gadgetSpec->rewrite to a structure like:
+   * Array (
+   *   [expires] => 86400
+   *   [include-url] => Array (
+   *     [0] => *
+   *   )
+   *   [exclude-url] => Array (
+   *     [0] => .png
+   *     [1] => .tmp
+   *   )
+   *   [minify-css] => true
+   *   [minify-js] => true
+   *   [minify-html] => true
+   * )
+   * @param DOMElement $feature
+   * @param GadgetSpec $gadget
+   */
+  private function parseContentRewrite(\DOMElement $feature, GadgetSpec &$gadget) {
+    $contentRewrite = array();
+    if (($paramNodes = $feature->getElementsByTagName('Param')) != null) {
+      foreach ($paramNodes as $param) {
+        $paramName = $param->getAttribute('name');
+        $paramValue = $param->nodeValue;
+        if ($paramName == 'include-url' || $paramName == 'exclude-url') {
+          if (! isset($contentRewrite[$paramName]) || ! is_array($contentRewrite[$paramName])) {
+            $contentRewrite[$paramName] = array();
+          }
+          $contentRewrite[$paramName][] = $paramValue;
+        } else {
+          $contentRewrite[$paramName] = $paramValue;
+        }
+      }
+    }
+    $gadget->rewrite = $contentRewrite;
+  }
+
+  /**
+   * Parses the preload elements
+   *
+   * @param DOMElement $modulePrefs
+   * @param GadgetSpec $gadget
+   */
+  private function parsePreloads(\DOMElement &$modulePrefs, GadgetSpec &$gadget) {
+    $gadget->preloads = array();
+    if (($preloadNodes = $modulePrefs->getElementsByTagName('Preload')) != null) {
+      foreach ($preloadNodes as $node) {
+        $gadget->preloads[] = array('href' => $node->getAttribute('href'),
+            'authz' => strtoupper($node->getAttribute('authz')),
+            'views' => $this->parseViewAttribute($node->getAttribute('views')),
+            'signViewer' => $node->getAttribute('sign_viewer'),
+            'signOwner' => $node->getAttribute('sign_owner'));
+      }
+    }
+  }
+
+  /**
+   * Parses the Locale (message bundle) entries
+   *
+   * @param DOMElement $modulePrefs
+   * @param GadgetSpec $gadget
+   */
+  private function parseLocales(\DOMElement &$modulePrefs, GadgetSpec &$gadget) {
+    $gadget->locales = array();
+    if (($localeNodes = $modulePrefs->getElementsByTagName('Locale')) != null) {
+      foreach ($localeNodes as $node) {
+        $messageBundle = array();
+        if (($messages = $node->getElementsByTagName('msg')) != null && $messages->length > 0) {
+          // parse inlined messages
+          foreach ($messages as $msg) {
+            $messageBundle[$msg->getAttribute('name')] = trim($msg->nodeValue);
+          }
+        }
+        $lang = $node->getAttribute('lang') == '' ? 'all' : strtolower($node->getAttribute('lang'));
+        $country = $node->getAttribute('country') == '' ? 'all' : strtoupper($node->getAttribute('country'));
+        $gadget->locales[] = array('lang' => $lang, 'country' => $country,
+            'messages' => $node->getAttribute('messages'),
+            'languageDirection' => $node->getAttribute('language_direction'),
+            'views' => $this->parseViewAttribute($node->getAttribute('views')),
+            'messageBundle' => $messageBundle);
+      }
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/MakeRequest.php b/trunk/php/src/apache/shindig/gadgets/MakeRequest.php
new file mode 100644
index 0000000..9f90696
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/MakeRequest.php
@@ -0,0 +1,435 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\sample\BasicRemoteContent;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\Config;
+use apache\shindig\common\SecurityTokenDecoder;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Common class for working with remote content requests.
+ * Example - sending a request from a gadget context:
+ * <code>
+ *   $context = new GadgetContext('GADGET');
+ *   $params = new MakeRequestOptions('http://www.example.com');
+ *   $params->setAuthz('SIGNED')
+ *          ->setNoCache(true)
+ *          ->setSignViewer(false)
+ *          ->setSecurityTokenString(BasicSecurityToken::getTokenStringFromRequest());
+ *   $result = $this->makeRequest->fetch($context, $params);
+ *   $responseCode = $result->getHttpCode();
+ *   $responseText = $result->getResponseContent();
+ * </code>
+ * More examples can be found in the
+ * {@link /php/src/test/gadgets/MakeRequestTest.php} MakeRequest unit tests.
+ */
+
+class MakeRequest {
+  /*
+   * List of disallowed request headers taken from
+   * /java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/HttpRequestHandler.java
+   */
+  static $BAD_REQUEST_HEADERS = array("HOST", "ACCEPT", "ACCEPT-ENCODING");
+  
+  /*
+   * List of disallowed response headers taken from
+   * /java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ProxyBase.java
+   */
+  static $BAD_RESPONSE_HEADERS = array(
+    "SET-COOKIE", "CONTENT-LENGTH", "CONTENT-ENCODING", "ETAG", "LAST-MODIFIED",
+    "ACCEPT-RANGES", "VARY", "EXPIRES", "DATE", "PRAGMA", "CACHE-CONTROL",
+    "TRANSFER-ENCODING", "WWW-AUTHENTICATE");
+
+  /**
+   * @var RemoteContentFetcher
+   */
+  private $remoteFetcher;
+
+  /**
+   * Constructor
+   *
+   * @param RemoteContentFetcher remoteFetcher A remote content fetcher intended
+   *     to override the default fetcher which will be loaded from the config
+   *     file.  This allows for injecting a mock into this class for testing.
+   */
+  public function __construct($remoteFetcher = null) {
+    if (isset($remoteFetcher)) {
+      $this->remoteFetcher = $remoteFetcher;
+    } else {
+      $remoteFetcherClass = Config::get('remote_content_fetcher');
+      $this->remoteFetcher = new $remoteFetcherClass();
+    }
+  }
+
+  /**
+   * Returns the remote fetcher this instance uses for remote requests.
+   *
+   * @return RemoteContentFetcher
+   */
+  public function getRemoteFetcher() {
+    return $this->remoteFetcher;
+  }
+
+  /**
+   * Makes a request for remote data.
+   *
+   * @param GadgetContext $context Gadget context used to make the request.
+   * @param MakeRequestOptions $params Parameter array used to configure the remote request.
+   * @return RemoteContentRequest A request/response which has been sent to the target server.
+   */
+  public function fetch(GadgetContext $context, MakeRequestOptions $params) {
+
+    $signingFetcherFactory = $gadgetSigner = null;
+    if ($params->getAuthz() == "SIGNED" || $params->getAuthz() == "OAUTH" || $params->getAuthz() == "OAUTH2") {
+      $gadgetSigner = Config::get('security_token_signer');
+      $gadgetSigner = new $gadgetSigner();
+      $signingFetcherFactory = new SigningFetcherFactory(Config::get("private_key_file"));
+    }
+    $basicRemoteContent = new BasicRemoteContent($this->remoteFetcher, $signingFetcherFactory, $gadgetSigner);
+    $request = $this->buildRequest($context, $params, $gadgetSigner);
+    $request->getOptions()->ignoreCache = $params->getNoCache();
+    $request->getOptions()->viewerSigned = $params->getSignViewer();
+    $request->getOptions()->ownerSigned = $params->getSignOwner();
+    $result = $basicRemoteContent->fetch($request);
+    $status = (int)$result->getHttpCode();
+    if ($status == 200) {
+      switch ($params->getResponseFormat()) {
+        case 'FEED':
+          $content = $this->parseFeed($result, $params->getHref(), $params->getNumEntries(), $params->getGetSummaries());
+          $result->setResponseContent($content);
+          break;
+      }
+    }
+    if ((strpos($result->getContentType(), 'text') !== false || strpos($result->getContentType(), 'application') !== false)
+            && strpos($result->getResponseContent(), '\u')) {
+    	$result->setResponseContent($this->decodeUtf8($result->getResponseContent()));
+    }
+
+    return $result;
+  }
+
+  /**
+   * Returns a response header array with invalid headers removed.
+   * @param array $headers An associative array of header/value pairs.
+   * The reason this cleaning is not automatic is because some consumers of
+   * MakeRequest may have need to access the stripped headers before
+   * delivering the response to a client.  The reason for removing these headers
+   * is also mostly for performance, rather than security.
+   *
+   * @return array An array with the headers defined in
+   *     MakeRequest::$BAD_RESPONSE_HEADERS removed.  The removal is
+   *     case-insensitive.
+   */
+  public function cleanResponseHeaders($headers) {
+    return $this->stripInvalidArrayKeys($headers, MakeRequest::$BAD_RESPONSE_HEADERS);
+  }
+
+  /**
+   * Builds a request to retrieve the actual content.
+   *
+   * @param GadgetContext $context The rendering context.
+   * @param MakeRequestOptions $params Options for crafting the request.
+   * @param SecurityTokenDecoder $signer A signer needed for signed requests.
+   * @return RemoteContentRequest An initialized request object.
+   */
+  public function buildRequest(GadgetContext $context, MakeRequestOptions $params, SecurityTokenDecoder $signer = null) {
+    // Check the protocol requested - curl doesn't really support file://
+    // requests but the 'error' should be handled properly
+    $protocolSplit = explode('://', $params->getHref(), 2);
+    if (count($protocolSplit) < 2) {
+      throw new \Exception("Invalid protocol specified");
+    }
+    $protocol = strtoupper($protocolSplit[0]);
+    if ($protocol != "HTTP" && $protocol != "HTTPS") {
+      throw new \Exception("Invalid protocol specified in url: " . htmlentities($protocol));
+    }
+    $method = $params->getHttpMethod();
+    if ($method == 'POST' || $method == 'PUT') {
+      // even if postData is an empty string, it will still post
+      // (since RemoteContentRquest checks if its false)
+      // so the request to POST is still honored
+      $request = new RemoteContentRequest($params->getHref(), null, $params->getRequestBody());
+    } else if ($method == 'DELETE' || $method == 'GET' || $method == 'HEAD') {
+      $request = new RemoteContentRequest($params->getHref());
+    } else {
+      throw new \Exception("Invalid HTTP method.");
+    }
+    $request->setMethod($method);
+    if ($signer) {
+      switch ($params->getAuthz()) {
+        case 'SIGNED':
+          $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+          break;
+        case 'OAUTH':
+          $request->setAuthType(RemoteContentRequest::$AUTH_OAUTH);
+          $request->setOAuthRequestParams($params->getOAuthRequestParameters());
+          break;
+        case 'OAUTH2':    
+          $request->setAuthType(RemoteContentRequest::$AUTH_OAUTH2);
+          $request->setOAuthRequestParams($params->getOAuthRequestParameters());
+          break;
+      }
+      $st = $params->getSecurityTokenString();
+      if ($st === false) {
+        throw new \Exception("A security token is required for signed requests");
+      }
+      $token = $context->validateToken($st, $signer);
+      $request->setToken($token);
+    }
+
+    // Strip invalid request headers.  This limits the utility of the
+    // MakeRequest class a little bit, but ensures that none of the invalid
+    // headers are present in any request going through this class.
+    $headers = $params->getRequestHeadersArray();
+    if ($headers !== false) {
+      $headers = $this->stripInvalidArrayKeys($headers, MakeRequest::$BAD_REQUEST_HEADERS);
+      $params->setRequestHeaders($headers);
+    }
+
+    // The request expects headers to be stored as a normal header text blob.
+    // ex: Content-Type: application/atom+xml
+    //     Accept-Language: en-us
+    $formattedHeaders = $params->getFormattedRequestHeaders();
+    if ($formattedHeaders !== false) {
+      $request->setHeaders($formattedHeaders);
+    }
+    
+    return $request;
+  }
+
+  /**
+   * Decodes UTF-8 numeric codes (&#xXXXX, or \uXXXX) from a content string.
+   * @param string $content The content string to decode.
+   * @return string A UTF-8 string where numeric codes have been converted into
+   *     their UTF character representations.
+   */
+  public function decodeUtf8($content) {
+    if (preg_match("/&#[xX][0-9a-zA-Z]{2,8};/", $content)) {
+      $content = preg_replace("/&#[xX]([0-9a-zA-Z]{2,8});/e", "'&#'.hexdec('$1').';'", $content);
+    }
+    if (preg_match("/\\\\(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})/", $content)) {
+      $content = preg_replace("/\\\\(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})/e", "'&#'.hexdec('$1').';'", $content);
+    }
+    return mb_decode_numericentity($content, array(0x0, 0xFFFF, 0, 0xFFFF), 'UTF-8');
+  }
+
+  /**
+   * Removes any invalid keys from an array in a case insensitive manner.
+   * Used for stripping invalid http headers from a request or response.
+   *
+   * @param array $target The associative array to check for invalid keys.
+   * @param array $invalidKeys An array of keys which are invalid.
+   * @return array A copy of $target with any keys matching a value in
+   *     $invalidKeys removed.
+   */
+  private function stripInvalidArrayKeys($target, $invalidKeys) {
+    $cleaned = array();
+    $upperInvalidKeys = array_map('strtoupper', $invalidKeys);
+
+    foreach ($target as $key => $value) {
+      $upperKey = strtoupper($key);
+      if (in_array($upperKey, $upperInvalidKeys) === FALSE) {
+        $cleaned[$key] = $value;
+      }
+    }
+
+    return $cleaned;
+  }
+  
+  /**
+   * Handles (RSS & Atom) Type.FEED parsing using Zend's feed parser
+   *
+   * @param RemoteContentRequest $result
+   * @param string $url
+   * @param int $numEntries
+   * @param boolean $getSummaries
+   * @return response string, either a json encoded feed structure or an error message
+   */
+  private function parseFeed($result, $url, $numEntries = 3, $getSummaries = false) {
+    $channel = array();
+    if ((int)$result->getHttpCode() == 200) {
+      $content = $result->getResponseContent();
+      try {
+        $feed = \Zend_Feed::importString($content);
+        if ($feed instanceof \Zend_Feed_Rss) {
+          // Try get author
+          if ($feed->author()) {
+            $author = $feed->author();
+          } else {
+            if ($feed->creator()) {
+              $author = $feed->creator();
+            } else {
+              $author = null;
+            }
+          }
+          // Loop over each channel item and store relevant data
+          $counter = 0;
+          $channel['Entry'] = array();
+          foreach ($feed as $item) {
+            if ($counter >= $numEntries) {
+              break;
+            }
+            $_entry = array();
+            $_entry['Title'] = $item->title();
+            $_entry['Link'] = $item->link();
+            if (!is_string($_entry['Link']) && isset($_entry['Link'][1]) && $_entry['Link'][1] instanceof \DOMElement) {
+            	$_entry['Link'] = $_entry['Link'][1]->getAttribute('href');
+            }
+            if ($getSummaries && $item->description()) {
+              $_entry['Summary'] = $item->description();
+            }
+            $date = 0;
+            if ($item->date()) {
+              $date = strtotime($item->date());
+            } else {
+              if ($item->pubDate()) {
+                $date = strtotime($item->pubDate());
+              }
+            }
+            $_entry['Date'] = $date;
+            $channel['Entry'][] = $_entry;
+            // Remember author if first found
+            if (empty($author) && $item->author()) {
+              $author = $item->author();
+            } else if ($item->creator()) {
+              $author = $item->creator();
+            }
+            $counter ++;
+          }
+          $channel['Title'] = $feed->title();
+          $channel['URL'] = $url;
+          $channel['Description'] = $feed->description();
+          if ($feed->link()) {
+            if (is_array($feed->link())) {
+              foreach ($feed->link() as $_link) {
+                if ($_link->nodeValue) $channel['Link'] = $_link->nodeValue;
+              }
+            } else {
+              $channel['Link'] = $feed->link();
+            }
+          }
+          if ($author != null) {
+            $channel['Author'] = $author;
+          }
+        } elseif ($feed instanceof \Zend_Feed_Atom) {
+          // Try get author
+          if ($feed->author()) {
+            if ($feed->author->name()) {
+              $author = $feed->author->name();
+            } else if ($feed->author->email()) {
+              $author = $feed->author->email();
+            } else {
+              $author = $feed->author();
+            }
+          } else {
+            $author = null;
+          }
+          // Loop over each entries and store relevant data
+          $counter = 0;
+          $channel['Entry'] = array();
+          foreach ($feed as $entry) {
+            if ($counter >= $numEntries) {
+              break;
+            }
+            $_entry = array();
+            $_entry['Title'] = $entry->title();
+            // get Link if rel="alternate"
+            if ($entry->link('alternate')) {
+              $_entry['Link'] = $entry->link('alternate');
+            } else {
+              // if there's no alternate, pick the one without "rel" attribtue
+              $_links = $entry->link;
+              if (is_array($_links)) {
+                foreach ($_links as $_link) {
+                  if (empty($_link['rel'])) {
+                    $_entry['Link'] = $_link['href'];
+                    break;
+                  }
+                }
+              } else {
+                $_entry['Link'] = $_links['href'];
+              }
+            }
+            if ($getSummaries && $entry->summary()) {
+              $_entry['Summary'] = $entry->summary();
+            }
+            $date = 0;
+            if ($entry->updated()) {
+              $date = strtotime($entry->updated());
+            } else {
+              if ($entry->published()) {
+                $date = strtotime($entry->published());
+              }
+            }
+            $_entry['Date'] = $date;
+            $channel['Entry'][] = $_entry;
+            // Remember author if first found
+            if (empty($author) && $entry->author()) {
+              if ($entry->author->name()) {
+                $author = $entry->author->name();
+              } else if ($entry->author->email()) {
+                $author = $entry->author->email();
+              } else {
+                $author = $entry->author();
+              }
+            } elseif (empty($author)) {
+              $author = null;
+            }
+            $counter ++;
+          }
+          $channel['Title'] = $feed->title();
+          $channel['URL'] = $url;
+          $channel['Description'] = $feed->subtitle();
+          // get Link if rel="alternate"
+          if ($feed->link('alternate')) {
+            $channel['Link'] = $feed->link('alternate');
+          } else {
+            // if there's no alternate, pick the one without "rel" attribtue
+            $_links = $feed->link;
+            if (is_array($_links)) {
+              foreach ($_links as $_link) {
+                if (empty($_link['rel'])) {
+                  $channel['Link'] = $_link['href'];
+                  break;
+                }
+              }
+            } else {
+              $channel['Link'] = $_links['href'];
+            }
+          }
+          if (! empty($author)) {
+            $channel['Author'] = $author;
+          }
+        } else {
+          throw new \Exception('Invalid feed type');
+        }
+        $resp = json_encode($channel);
+      } catch (\Zend_Feed_Exception $e) {
+        $resp = 'Error parsing feed: ' . $e->getMessage();
+      }
+    } else {
+      // feed import failed
+      $resp = "Error fetching feed, response code: " . $result->getHttpCode();
+    }
+    return $resp;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/MakeRequestHandler.php b/trunk/php/src/apache/shindig/gadgets/MakeRequestHandler.php
new file mode 100644
index 0000000..5b50e3d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/MakeRequestHandler.php
@@ -0,0 +1,69 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+// according to features/core/io.js, this is high on the list of things to scrap
+define('UNPARSEABLE_CRUFT', "throw 1; < don't be evil' >");
+
+/**
+ * Handles the gadget.io.makeRequest requests
+ */
+class MakeRequestHandler extends ProxyBase {
+
+  /**
+   * Constructor.
+   *
+   * @param GadgetContext $context Current rendering context
+   */
+  public function __construct(GadgetContext $context) {
+    $this->context = $context;
+    $makeRequestClass = Config::get('makerequest_class');
+    $this->makeRequest = new $makeRequestClass();
+  }
+
+  /**
+   * Fetches content and echoes it in JSON format
+   *
+   * @param MakeRequestOptions $params  The request configuration.
+   */
+  public function fetchJson(MakeRequestOptions $params) {
+    $result = $this->makeRequest->fetch($this->context, $params);
+    $responseArray = array(
+      'rc' => (int)$result->getHttpCode(),
+      'body' => $result->getResponseContent(),
+      'headers' => $this->makeRequest->cleanResponseHeaders($result->getResponseHeaders())
+    );
+    $responseArray = array_merge($responseArray, $result->getMetadatas());
+    $json = array($params->getHref() => $responseArray);
+    $json = json_encode($json);
+    $output = UNPARSEABLE_CRUFT . $json;
+    if ($responseArray['rc'] == 200) {
+      // only set caching headers if the result was 'OK'
+      $this->setCachingHeaders();
+    }
+    if (!Config::get('debug')) {
+      header('Content-Type: application/json; charset="UTF-8"');
+      header('Content-Disposition: attachment;filename=p.txt');
+    }
+    echo $output;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/MakeRequestOptions.php b/trunk/php/src/apache/shindig/gadgets/MakeRequestOptions.php
new file mode 100644
index 0000000..0f95c08
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/MakeRequestOptions.php
@@ -0,0 +1,764 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\gadgets\oauth\OAuthRequestParams;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\social\service\RpcRequestItem;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Class that manages the configuration of a makeRequest fetch call.
+ * Example - initializing a feed fetch manually:
+ * <code>
+ *   $context = new GadgetContext('GADGET');
+ *   $params = new MakeRequestOptions('http://www.example.com');
+ *   $params->setResponseFormat('FEED')
+ *          ->setNoCache(true)
+ *          ->setNumEntries(10)
+ *          ->setGetSummaries(true));
+ *   $result = $this->makeRequest->fetch($context, $params);
+ * </code>
+ *
+ * Additionally, this class can configure itself from the current HTTP request
+ * (useful in a servlet).  Example:
+ * <code>
+ *   $context = new GadgetContext('GADGET');
+ *   $params = MakeRequestOptions::fromCurrentRequest();
+ *   $result = $this->makeRequest->fetch($context, $params);
+ * </code>
+ */
+class MakeRequestOptions {
+  const DEFAULT_REFRESH_INTERVAL = 3600;
+  const DEFAULT_HTTP_METHOD = 'GET';
+  const DEFAULT_OUTPUT_FORMAT = 'JSON';
+  const DEFAULT_AUTHZ = 'NONE';
+  const DEFAULT_SIGN_VIEWER = true;
+  const DEFAULT_SIGN_OWNER = true;
+  const DEFAULT_OAUTH_USE_TOKEN = 'IF_AVAILABLE';
+  const DEFAULT_NUM_ENTRIES = 3;
+  const DEFAULT_GET_SUMMARIES = false;
+
+  static $VALID_HTTP_METHODS = array('GET', 'PUT', 'POST', 'HEAD', 'DELETE');
+  static $VALID_OUTPUT_FORMATS = array('TEXT', 'JSON', 'FEED', 'DOM');
+  static $VALID_AUTHZ = array('OAUTH', 'OAUTH2', 'NONE', 'SIGNED');
+  static $VALID_OAUTH_USE_TOKEN = array('NEVER', 'IF_AVAILABLE', 'ALWAYS');
+
+  private $href;
+  private $method;
+  private $body;
+  private $headers;
+  private $format;
+  private $authz;
+  private $signViewer;
+  private $signOwner;
+  private $oauthServiceName;
+  private $oauthTokenName;
+  private $oauthRequestToken;
+  private $oauthRequestTokenSecret;
+  private $oauthUseToken;
+  private $oauthClientState;
+  private $oauthReceivedCallback;
+  private $noCache;
+  private $refreshInterval;
+  private $numEntries;
+  private $getSummaries;
+  private $st;
+
+  /**
+   * Constructor.
+   *
+   * @param string $href Url to fetch.
+   */
+  public function __construct($href) {
+    $this->href = MakeRequestOptions::validateUrl($href);
+  }
+
+  /**
+   * Throws an exception if the supplied parameter is not in a set of values.
+   *
+   * @param mixed $param Parameter to check.
+   * @param array $values Valid values.
+   * @return mixed The value if the parameter exists in the array.  If a
+   *     string is passed, the string is set to uppercase before being checked
+   *     against the array and returned as an uppercase string.
+   * @throws MakeRequestParameterException If the value was not found in the
+   *     array.
+   */
+  private function assertParameterIsOneOf($param, $values) {
+    if (is_string($param)) {
+      $param = strtoupper($param);
+    }
+    if (!in_array($param, $values)) {
+      throw new MakeRequestParameterException("Got an invalid value, was expecting one of " . implode(', ', $values));
+    }
+    return $param;
+  }
+
+  /**
+   * Validates that a passed in argument is actually an url.
+   *
+   * @param string $url The parameter to check.
+   * @return string The url if it can be parsed as an url.
+   * @throws MakeRequestParameterException If the url could not be parsed.
+   */
+  public static function validateUrl($url) {
+    if (empty($url) || !@parse_url($url)) {
+      throw new MakeRequestParameterException("Invalid Url");
+    } else {
+      return $url;
+    }
+  }
+
+  /**
+   * Attempts to pull the requested parameter from the current HTTP request
+   * and return it.  If a type is specified and the requests contains a
+   * parameter with the specified name, the value of the parameter is attempted
+   * to be coerced to the requested type.  PHP's settype is used to perform
+   * the cast, although a special case is considered for the string "false"
+   * which should evaluate to boolean false in the case of http requests.
+   *
+   * @param string $name The name of the parameter to check for.  This method
+   *     examines the $_GET superglobal first, then the $_POST.
+   * @param string $optType An optional name of a type to cerce to.  Check the
+   *     documentation for PHP's <code>settype</code> function to see valid
+   *     values for this parameter.
+   * @return mixed The value of the parameter, or null if it didn't exist.
+   * @throws MakeRequestParameterException If a type was specified and the
+   *     argument could not be converted to the correct type.
+   */
+  private static function getRequestParam($name, $optType = null) {
+    $param = null;
+    if (array_key_exists($name, $_GET)) {
+      $param = $_GET[$name];
+    } else if (array_key_exists($name, $_POST)) {
+      $param = $_POST[$name];
+    }
+    if (empty($param)) {
+      $param = null;
+    }
+    if (isset($param) && isset($optType)) {
+      switch (strtolower($optType)) {
+        case 'boolean':
+        case 'bool':
+          if (($param) === "false") {
+            $param = "0";
+          }
+      }
+      if (!settype($param, $optType)) {
+        throw new MakeRequestParameterException("Parameter '$name' should be convertable to $optType.");
+      }
+    }
+    return $param;
+  }
+
+  /**
+   * Builds a MakeRequestOptions object from the current $_GET and $_POST
+   * superglobals.
+   *
+   * @return MakeRequestOptions An object initialized from the current request.
+   * @throws MakeRequestParameterException If any of the parameters were
+   *     invalid.
+   */
+  public static function fromCurrentRequest(){
+    $href = MakeRequestOptions::getRequestParam('href');
+    if (!isset($href)) {
+      $href = MakeRequestOptions::getRequestParam('url');
+    }
+
+    $options = new MakeRequestOptions($href);
+    $options->setHttpMethod(MakeRequestOptions::getRequestParam('httpMethod'))
+            ->setRequestBody(MakeRequestOptions::getRequestParam('postData'))
+            ->setFormEncodedRequestHeaders(MakeRequestOptions::getRequestParam('headers'))
+            ->setResponseFormat(MakeRequestOptions::getRequestParam('contentType'))
+            ->setAuthz(MakeRequestOptions::getRequestParam('authz'))
+            ->setSignViewer(MakeRequestOptions::getRequestParam('signViewer', 'boolean'))
+            ->setSignOwner(MakeRequestOptions::getRequestParam('signOwner', 'boolean'))
+            ->setNumEntries(MakeRequestOptions::getRequestParam('numEntries', 'integer'))
+            ->setGetSummaries(MakeRequestOptions::getRequestParam('getSummaries', 'boolean'))
+            ->setRefreshInterval(MakeRequestOptions::getRequestParam('refreshInterval', 'integer'))
+            ->setNoCache(MakeRequestOptions::getRequestParam('bypassSpecCache', 'boolean'))
+            ->setOAuthServiceName(MakeRequestOptions::getRequestParam('OAUTH_SERVICE_NAME'))
+            ->setOAuthTokenName(MakeRequestOptions::getRequestParam('OAUTH_TOKEN_NAME'))
+            ->setOAuthRequestToken(MakeRequestOptions::getRequestParam('OAUTH_REQUEST_TOKEN'))
+            ->setOAuthRequestTokenSecret(MakeRequestOptions::getRequestParam('OAUTH_REQUEST_TOKEN_SECRET'))
+            ->setOAuthUseToken(MakeRequestOptions::getRequestParam('OAUTH_USE_TOKEN'))
+            ->setOAuthReceivedCallback(MakeRequestOptions::getRequestParam('OAUTH_RECEIVED_CALLBACK'))
+            ->setOAuthClientState(MakeRequestOptions::getRequestParam('oauthState'))
+            ->setSecurityTokenString(BasicSecurityToken::getTokenStringFromRequest());
+
+    return $options;
+  }
+
+  /**
+   * Builds a MakeRequestOptions object from a RequestItem instance.  This is
+   * a helper for dealing with Handler services which need to call MakeRequest.
+   * The parameter names were taken from the osapi.http spec documents, although
+   * several parameters not in the spec are also supported to allow full
+   * functionality.
+   *
+   * @param RpcRequestItem $request The RpcRequestItem to parse.  The reason
+   *     RpcRequestItem is needed is because of the way getService() and
+   *     getMethod() are overloaded in the RequestItem subclasses.  This
+   *     function needs a reliable way to get the http method.
+   * @return MakeRequestOptions An object initialized from the current request.
+   * @throws MakeRequestParameterException If any of the parameters were
+   *     invalid.
+   */
+  public static function fromRpcRequestItem(RpcRequestItem $request) {
+    $href = $request->getParameter('href');
+    if (!isset($href)) {
+      $href = $request->getParameter('url');
+    }
+
+    $options = new MakeRequestOptions($href);
+    $options->setHttpMethod($request->getMethod())
+            ->setRequestBody($request->getParameter('body'))
+            ->setRequestHeaders($request->getParameter('headers', array()))
+            ->setResponseFormat($request->getParameter('format'))
+            ->setAuthz($request->getParameter('authz'))
+            ->setSignViewer($request->getParameter('sign_viewer'))
+            ->setSignOwner($request->getParameter('sign_owner'))
+            ->setNumEntries($request->getParameter('numEntries')) // Not in osapi.http spec, but nice to support
+            ->setGetSummaries($request->getParameter('getSummaries')) // Not in osapi.http spec, but nice to support
+            ->setRefreshInterval($request->getParameter('refreshInterval'))
+            ->setNoCache($request->getParameter('nocache')) // Not in osapi.http spec, but nice to support
+            ->setOAuthServiceName($request->getParameter('oauth_service_name'))
+            ->setOAuthTokenName($request->getParameter('oauth_token_name'))
+            ->setOAuthRequestToken($request->getParameter('oauth_request_token'))
+            ->setOAuthRequestTokenSecret($request->getParameter('oauth_request_token_secret'))
+            ->setOAuthUseToken($request->getParameter('oauth_use_token'))
+            ->setOAuthReceivedCallback($request->getParameter('oauth_received_callback'))
+            ->setOAuthClientState($request->getParameter('oauth_state')) // Not in osapi.http spec, but nice to support
+            ->setSecurityTokenString($request->getToken()->toSerialForm());
+
+   return $options;
+ }
+  /**
+   * Gets the configured URL.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getHref() {
+    return $this->href;
+  }
+
+  /**
+   * Sets the http method to use for this request.  Must be one of
+   * {@link MakeRequestOptions::$VALID_HTTP_METHODS}.
+   *
+   * @param string $method The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setHttpMethod($method) {
+    if (isset($method)) {
+      $this->method = $this->assertParameterIsOneOf($method, MakeRequestOptions::$VALID_HTTP_METHODS);
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured HTTP method.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getHttpMethod() {
+    return isset($this->method) ? $this->method : MakeRequestOptions::DEFAULT_HTTP_METHOD;
+  }
+
+  /**
+   * Sets the request body.
+   *
+   * @param string $body The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setRequestBody($body) {
+    if (isset($body)) {
+      $this->body = $body;
+    }
+    return $this;
+  }
+
+  public function getRequestBody() {
+    return isset($this->body) ? $this->body : null;
+  }
+
+  /**
+   * Sets the headers to use when making the request.
+   *
+   * @param array $headers An array of key/value pairs to use as HTTP headers.
+   *     Example:
+   *     <code>
+   *     $params->setRequestHeaders(array(
+   *         'Content-Type' => 'text/plain',
+   *         'Accept-Language' => 'en-us'
+   *     ));
+   *     </code>
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setRequestHeaders(array $headers) {
+    if (isset($headers)) {
+      $this->headers = $headers;
+    }
+    return $this;
+  }
+
+  /**
+   * Sets the headers to use when making the request.
+   *
+   * @param string $headers A form-urlencoded string of key/values to use as
+   *     HTTP headers for the request.  The OpenSocial JavaScript library
+   *     passes makeRequest headers in this format, so this is just a
+   *     convenience method.
+   *     Example:
+   *     <code>
+   *     $params->setFormEncodedRequestHeaders(
+   *       "Content-Type=text/plain&Accept-Language=en-us"
+   *     );
+   *     </code>
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setFormEncodedRequestHeaders($headers) {
+    if (isset($headers)) {
+      $headerLines = explode("&", $headers);
+      $this->headers = array();
+      foreach ($headerLines as $line) {
+        $parts = explode("=", $line);
+        if (count($parts) == 2) {
+          $this->headers[urldecode($parts[0])] = urldecode($parts[1]);
+        }
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured request headers in the HTTP header format (separated
+   * by newlines, with a key-colon-space-value format).  Example:
+   * <code>
+   *   Content-Type: text/plain
+   *   Accept-Language: en-us
+   * </code>
+   *
+   * @return string The value of this parameter.
+   */
+  public function getFormattedRequestHeaders() {
+    if (isset($this->headers)) {
+      $headerString = http_build_query($this->headers);
+      return urldecode(str_replace("&", "\n", str_replace("=", ": ", $headerString)));
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Returns the request headers as an array.
+   *
+   * @return array The request header array.
+   */
+  public function getRequestHeadersArray() {
+    if (isset($this->headers)) {
+      return $this->headers;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Sets the expected response format for this type of request.  Valid values
+   * are one of {@link MakeRequestOptions::$VALID_OUTPUT_FORMATS}.
+   *
+   * @param string $format The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setResponseFormat($format) {
+    if (isset($format)) {
+      $this->format = $this->assertParameterIsOneOf($format, MakeRequestOptions::$VALID_OUTPUT_FORMATS);
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured response format.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getResponseFormat() {
+    return isset($this->format) ? $this->format : MakeRequestOptions::DEFAULT_OUTPUT_FORMAT;
+  }
+
+  /**
+   * Sets the authorization type of the request.  Must be one of
+   * {@link MakeRequestOptions::$VALID_AUTHZ}.
+   *
+   * @param string $authz The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setAuthz($authz) {
+    if (isset($authz)) {
+      $this->authz = $this->assertParameterIsOneOf($authz, MakeRequestOptions::$VALID_AUTHZ);
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured authz parameter.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getAuthz() {
+    return isset($this->authz) ? $this->authz : MakeRequestOptions::DEFAULT_AUTHZ;
+  }
+
+  /**
+   * Sets whether to include the viewer's ID in a signed request.
+   *
+   * @param bool $signViewer True to include the viewer's ID.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setSignViewer($signViewer) {
+    if (isset($signViewer)) {
+      if (!is_bool($signViewer)) {
+        throw new MakeRequestParameterException("signViewer must be a boolean.");
+      }
+      $this->signViewer = $signViewer;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured value of whether to sign with the viewer ID or not.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getSignViewer() {
+    return isset($this->signViewer) ? $this->signViewer : MakeRequestOptions::DEFAULT_SIGN_VIEWER;
+  }
+
+  /**
+   * Sets whether to include the owner's ID in a signed request.
+   *
+   * @param bool $signOwner True to include the owner's ID.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setSignOwner($signOwner) {
+    if (isset($signOwner)) {
+      if (!is_bool($signOwner)) {
+        throw new MakeRequestParameterException("signOwner must be a boolean.");
+      }
+      $this->signOwner = $signOwner;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured value of whether to sign with the owner ID or not.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getSignOwner() {
+    return isset($this->signOwner) ? $this->signOwner : MakeRequestOptions::DEFAULT_SIGN_OWNER;
+  }
+
+  /**
+   * Sets the OAuth service name.
+   *
+   * @param string $serviceName The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setOAuthServiceName($serviceName) {
+    if (isset($serviceName)) {
+      $this->oauthServiceName = $serviceName;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured OAuth service name.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getOAuthServiceName() {
+    return isset($this->oauthServiceName) ? $this->oauthServiceName : '';
+  }
+
+  /**
+   * Sets the OAuth token name.
+   *
+   * @param string $tokenName The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setOAuthTokenName($tokenName) {
+    if (isset($tokenName)) {
+      $this->oauthTokenName = $tokenName;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured OAuth token name.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getOAuthTokenName() {
+    return isset($this->oauthTokenName) ? $this->oauthTokenName : '';
+  }
+
+  /**
+   * Sets the OAuth request token.
+   *
+   * @param string $requestToken The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setOAuthRequestToken($requestToken) {
+    if (isset($requestToken)) {
+      $this->oauthRequestToken = $requestToken;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured OAuth request token.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getOAuthRequestToken() {
+    return isset($this->oauthRequestToken) ? $this->oauthRequestToken : '';
+  }
+
+  /**
+   * Sets the OAuth request token secret.
+   *
+   * @param string $requestTokenSecret The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setOAuthRequestTokenSecret($requestTokenSecret) {
+    if (isset($requestTokenSecret)) {
+      $this->oauthRequestTokenSecret = $requestTokenSecret;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured OAuth request token secret.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getOAuthRequestTokenSecret() {
+    return isset($this->oauthRequestTokenSecret) ? $this->oauthRequestTokenSecret : '';
+  }
+
+  /**
+   * Sets whether to use an OAuth token.  Must be one of
+   * {@link MakeRequestOptions::$VALID_OAUTH_USE_TOKEN}.
+   *
+   * @param string $oauthUseToken The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setOAuthUseToken($oauthUseToken) {
+    if (isset($oauthUseToken)) {
+      $this->oauthUseToken = $this->assertParameterIsOneOf($oauthUseToken, MakeRequestOptions::$VALID_OAUTH_USE_TOKEN);
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured value of whether to use the OAuth token.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getOAuthUseToken() {
+    return isset($this->oauthUseToken) ? $this->oauthUseToken : MakeRequestOptions::DEFAULT_OAUTH_USE_TOKEN;
+  }
+
+  /**
+   * Sets the OAuth client state.
+   *
+   * @param string $oauthClientState The value to use.
+   * @return MakeRequestOptions This object (for chaining purporses).
+   */
+  public function setOAuthClientState($oauthClientState) {
+    if (isset($oauthClientState)) {
+      $this->oauthClientState = $oauthClientState;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured OAuth client state.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getOAuthClientState() {
+    return isset($this->oauthClientState) ? $this->oauthClientState : null;
+  }
+
+  public function setOAuthReceivedCallback($oauthReceivedCallback) {
+    if (isset($oauthReceivedCallback)) {
+      $this->oauthReceivedCallback = $oauthReceivedCallback;
+    }
+    return $this;
+  }
+
+  public function getOAuthReceivedCallback() {
+    return isset($this->oauthReceivedCallback) ? $this->oauthReceivedCallback : "";
+  }
+
+  /**
+   * Gets all of the configured OAuth parameters as an OAuthRequestParams
+   * object.
+   *
+   * @return OAuthRequestParams The collection of OAuth parameters.
+   */
+  public function getOAuthRequestParameters() {
+    return new OAuthRequestParams(array(
+        OAuthRequestParams::$SERVICE_PARAM => $this->getOAuthServiceName(),
+        OAuthRequestParams::$TOKEN_PARAM => $this->getOAuthTokenName(),
+        OAuthRequestParams::$REQUEST_TOKEN_PARAM => $this->getOAuthRequestToken(),
+        OAuthRequestParams::$REQUEST_TOKEN_SECRET_PARAM => $this->getOAuthRequestTokenSecret(),
+        OAuthRequestParams::$BYPASS_SPEC_CACHE_PARAM => $this->getNoCache(),
+        OAuthRequestParams::$RECEIVED_CALLBACK_PARAM => $this->getOAuthReceivedCallback(),
+        OAuthRequestParams::$CLIENT_STATE_PARAM => $this->getOAuthClientState()
+    ));
+  }
+
+  /**
+   * Sets whether to bypass the cache for this request.
+   *
+   * @param bool $noCache True if the request should bypass the cache.
+   * @return MakeRequestOptions This object (for chaining purporses)
+   */
+  public function setNoCache($noCache) {
+    if (isset($noCache)) {
+      if (!is_bool($noCache)) {
+        throw new MakeRequestParameterException("noCache must be a boolean.");
+      }
+      $this->noCache = $noCache;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured value of whether to bypass the cache.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getNoCache() {
+    return isset($this->noCache) ? $this->noCache : false;
+  }
+
+  /**
+   * Sets the refresh interval for this request.  Must be an integer equal to
+   * or greater than 0.
+   *
+   * @param int $refreshInterval The value to use
+   * @return MakeRequestOptions This object (for chaining purporses)
+   */
+  public function setRefreshInterval($refreshInterval) {
+    if (isset($refreshInterval)) {
+      if (!is_int($refreshInterval) || $refreshInterval < 0) {
+        throw new MakeRequestParameterException('Refresh interval must be greater than or equal to 0');
+      }
+      $this->refreshInterval = $refreshInterval;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured refresh interval.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getRefreshInterval() {
+    return isset($this->refreshInterval) ? $this->refreshInterval : MakeRequestOptions::DEFAULT_REFRESH_INTERVAL;
+  }
+
+  /**
+   * Sets the number of entries to return for format = FEED requests.  Must
+   * be an integer greater than 0.
+   *
+   * @param int $numEntries The value to use
+   * @return MakeRequestOptions This object (for chaining purporses)
+   */
+  public function setNumEntries($numEntries) {
+    if (isset($numEntries)) {
+      if (!is_int($numEntries) || $numEntries <= 0) {
+        throw new MakeRequestParameterException('NumEntries must be greater than 0');
+      }
+      $this->numEntries = $numEntries;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured number of entries to return for a feed request.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getNumEntries() {
+    return isset($this->numEntries) ? $this->numEntries : MakeRequestOptions::DEFAULT_NUM_ENTRIES;
+  }
+
+  /**
+   * Sets whether to fetch summaries for format = FEED requests.
+   *
+   * @param bool $getSummaries The value to use
+   * @return MakeRequestOptions This object (for chaining purporses)
+   */
+  public function setGetSummaries($getSummaries) {
+    if (isset($getSummaries)) {
+      if (!is_bool($getSummaries)) {
+        throw new MakeRequestParameterException("getSummaries must be a boolean.");
+      }
+      $this->getSummaries = $getSummaries;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured value of whether to fetch summaries for a feed request.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getGetSummaries() {
+    return isset($this->getSummaries) ? $this->getSummaries : MakeRequestOptions::DEFAULT_GET_SUMMARIES;
+  }
+
+  /**
+   * Sets a security token string.  Required for signed or OAuth requests.
+   *
+   * @param string $st The value to use
+   * @return MakeRequestOptions This object (for chaining purporses)
+   */
+  public function setSecurityTokenString($st) {
+    if (isset($st)) {
+      $this->st = $st;
+    }
+    return $this;
+  }
+
+  /**
+   * Gets the configured security token string.
+   *
+   * @return string The value of this parameter.
+   */
+  public function getSecurityTokenString() {
+    return isset($this->st) ? $this->st : false;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/MakeRequestParameterException.php b/trunk/php/src/apache/shindig/gadgets/MakeRequestParameterException.php
new file mode 100644
index 0000000..b2a5b7e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/MakeRequestParameterException.php
@@ -0,0 +1,27 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Exception class thrown when a paramter is incorrectly configured on the
+ * MakeRequestOptions class.
+ */
+class MakeRequestParameterException extends \Exception {}
diff --git a/trunk/php/src/apache/shindig/gadgets/MetadataGadgetContext.php b/trunk/php/src/apache/shindig/gadgets/MetadataGadgetContext.php
new file mode 100644
index 0000000..cdf7bbc
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/MetadataGadgetContext.php
@@ -0,0 +1,44 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MetadataGadgetContext extends GadgetContext {
+
+  /**
+   *
+   * @param object $jsonContext
+   * @param string $url
+   */
+  public function __construct($jsonContext, $url) {
+    parent::__construct('GADGET');
+    $this->url = $url;
+    $this->view = $jsonContext->view;
+    $this->locale = array('lang' => $jsonContext->language, 'country' => $jsonContext->country);
+    $this->container = $jsonContext->container;
+  }
+
+  /**
+   * @return array
+   */
+  public function getView() {
+    return $this->view;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/MetadataHandler.php b/trunk/php/src/apache/shindig/gadgets/MetadataHandler.php
new file mode 100644
index 0000000..5abcdd0
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/MetadataHandler.php
@@ -0,0 +1,149 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\Config;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MetadataHandler {
+
+  /**
+   *
+   * @param object $requests
+   * @return array
+   */
+  public function process($requests) {
+    $response = array();
+    foreach ($requests->gadgets as $gadget) {
+      try {
+        $gadgetUrl = $gadget->url;
+        $gadgetModuleId = $gadget->moduleId;
+        $context = new MetadataGadgetContext($requests->context, $gadgetUrl);
+        $token = $this->getSecurityToken();
+        $factoryClass = Config::get('gadget_factory_class');
+        $gadgetServer = new $factoryClass($context, $token);
+        $gadget = $gadgetServer->createGadget($gadgetUrl);
+        $response[] = $this->makeResponse($gadget, $gadgetModuleId, $gadgetUrl, $context);
+      } catch (\Exception $e) {
+        $response[] = array('errors' => array($e->getMessage()),
+            'moduleId' => $gadgetModuleId, 'url' => $gadgetUrl);
+      }
+    }
+    return $response;
+  }
+
+  /**
+   *
+   * @return SecurityToken
+   */
+  private function getSecurityToken() {
+    $token = BasicSecurityToken::getTokenStringFromRequest();
+    if (empty($token)) {
+      if (Config::get('allow_anonymous_token')) {
+        // no security token, continue anonymously, remeber to check
+        // for private profiles etc in your code so their not publicly
+        // accessable to anoymous users! Anonymous == owner = viewer = appId = modId = 0
+        // create token with 0 values, no gadget url, no domain and 0 duration
+        $gadgetSigner = Config::get('security_token');
+        return new $gadgetSigner(null, 0, SecurityToken::$ANONYMOUS, SecurityToken::$ANONYMOUS, 0, '', '', 0, Config::get('container_id'));
+      } else {
+        return null;
+      }
+    }
+    $gadgetSigner = Config::get('security_token_signer');
+    $gadgetSigner = new $gadgetSigner();
+    return $gadgetSigner->createToken($token);
+  }
+
+  /**
+   *
+   * @param Gadget $gadget
+   * @param GadgetContext $context
+   * @return array
+   */
+  private function getIframeURL(Gadget $gadget, GadgetContext $context) {
+    $v = $gadget->getChecksum();
+    $view = $gadget->getView($context->getView());
+    $up = '';
+    foreach ($gadget->gadgetSpec->userPrefs as $pref) {
+      $up .= '&up_' . urlencode($pref['name']) . '=' . urlencode($pref['value']);
+    }
+    $locale = $context->getLocale();
+    //Note: putting the URL last, else some browsers seem to get confused (reported by hi5)
+    return Config::get('default_iframe_prefix') . 'container=' . $context->getContainer() . ($context->getIgnoreCache() ? '&nocache=1' : '&v=' . $v) . ($context->getModuleId() != 0 ? '&mid=' . $context->getModuleId() : '') . '&lang=' . $locale['lang'] . '&country=' . $locale['country'] . '&view=' . $view['view'] . $up . '&url=' . urlencode($context->getUrl());
+  }
+
+  private function makeResponse($gadget, $gadgetModuleId, $gadgetUrl, $context) {
+    $response = array();
+    $prefs = array();
+    foreach ($gadget->gadgetSpec->userPrefs as $pref) {
+      $prefs[$pref['name']] = $pref;
+    }
+    $views = array();
+    foreach ($gadget->gadgetSpec->views as $name => $view) {
+      // we want to include all information, except for the content
+      unset($view['content']);
+      $views[$name] = $view;
+    }
+
+    $oauth = array();
+    /*
+    $oauthspec = $gadget->getOAuthSpec();
+    if (! empty($oauthspec)) {
+      foreach ($oauthspec->getServices() as $oauthservice) {
+        $oauth[$oauthservice->getName()] = array("request" => $oauthservice->getRequestUrl(), "access" => $oauthservice->getAccessUrl(), "authorization" => $oauthservice->getAuthorizationUrl());
+      }
+    }
+    */
+    $response['iframeUrl'] = $this->getIframeURL($gadget, $context);
+    $response['features'] = $gadget->features;
+    $response['links'] = $gadget->gadgetSpec->links;
+    $response['icons'] = $gadget->gadgetSpec->icon;
+    $response['views'] = $views;
+    $response['author'] = $gadget->getAuthor();
+    $response['authorEmail'] = $gadget->getAuthorEmail();
+    $response['description'] = $gadget->getDescription();
+    $response['directoryTitle'] = $gadget->getDirectoryTitle();
+    $response['screenshot'] = $gadget->getScreenShot();
+    $response['thumbnail'] = $gadget->getThumbnail();
+    $response['title'] = $gadget->getTitle();
+    $response['titleUrl'] = $gadget->getTitleUrl();
+    $response['authorAffiliation'] = $gadget->getAuthorAffiliation();
+    $response['authorLocation'] = $gadget->getAuthorLocation();
+    $response['authorPhoto'] = $gadget->getAuthorPhoto();
+    $response['authorAboutme'] = $gadget->getAuthorAboutme();
+    $response['authorQuote'] = $gadget->getAuthorQuote();
+    $response['authorLink'] = $gadget->getAuthorLink();
+    $response['showInDirectory'] = $gadget->getShowInDirectory();
+    $response['showStats'] = $gadget->getShowStats();
+    $response['width'] = $gadget->getWidth();
+    $response['height'] = $gadget->getHeight();
+    $response['categories'] = Array($gadget->getCategory(), $gadget->getCategory2());
+    $response['singleton'] = $gadget->getSingleton();
+    $response['scaling'] = $gadget->getScaling();
+    $response['scrolling'] = $gadget->getScrolling();
+    $response['moduleId'] = $gadgetModuleId;
+    $response['url'] = $gadgetUrl;
+    $response['userPrefs'] = $prefs;
+    $response['oauth'] = $oauth;
+    return $response;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/ProxyBase.php b/trunk/php/src/apache/shindig/gadgets/ProxyBase.php
new file mode 100644
index 0000000..59da990
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/ProxyBase.php
@@ -0,0 +1,102 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * This class contains the shared methods between the Proxy and makeRequest handlers
+ */
+class ProxyBase {
+  /**
+   * @var GadgetContext
+   */
+  public $context;
+
+  /**
+   *
+   * @var MakeRequest
+   */
+  protected $makeRequest;
+
+  /**
+   *
+   * @param GadgetContext $context
+   * @param MakeRequest $makeRequest
+   */
+  public function __construct(GadgetContext $context, MakeRequest $makeRequest = null) {
+    $this->context = $context;
+    if (isset($makeRequest)) {
+      $this->makeRequest = $makeRequest;
+    } else {
+      $makeRequestClass = Config::get('makerequest_class');
+      $this->makeRequest = new $makeRequestClass();
+    }
+  }
+
+  /**
+   * Sets the caching (Cache-Control & Expires) with a cache age of $lastModified
+   * or if $lastModified === false, sets Pragma: no-cache & Cache-Control: no-cache
+   *
+   * @param boolean $lastModified
+   */
+  protected function setCachingHeaders($lastModified = false) {
+    $maxAge = $this->context->getIgnoreCache() ? false : $this->context->getRefreshInterval();
+    if ($maxAge) {
+      if ($lastModified) {
+        header("Last-Modified: $lastModified");
+      }
+      // time() is a kernel call, so lets avoid it and use the request time instead
+      $time = $_SERVER['REQUEST_TIME'];
+      $expires = $maxAge !== false ? $time + $maxAge : $time - 3000;
+      $public = $maxAge ? 'public' : 'private';
+      $maxAge = $maxAge === false ? '0' : $maxAge;
+      header("Cache-Control: {$public}; max-age={$maxAge}", true);
+      header("Expires: " . gmdate("D, d M Y H:i:s", $expires) . " GMT", true);
+    } else {
+      header("Cache-Control: no-cache", true);
+      header("Pragma: no-cache", true);
+    }
+  }
+
+  /**
+   * Returns the request headers, using the apache_request_headers function if it's
+   * available, and otherwise tries to guess them from the $_SERVER superglobal
+   *
+   * @return array
+   */
+  protected function request_headers() {
+    // Try to use apache's request headers if available
+    if (function_exists("apache_request_headers")) {
+      if (($headers = apache_request_headers())) {
+        return $headers;
+      }
+    }
+    // if that failed, try to create them from the _SERVER superglobal
+    $headers = array();
+    foreach (array_keys($_SERVER) as $skey) {
+      if (substr($skey, 0, 5) == "HTTP_") {
+        $headername = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($skey, 0, 5)))));
+        $headers[$headername] = $_SERVER[$skey];
+      }
+    }
+    return $headers;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/ProxyHandler.php b/trunk/php/src/apache/shindig/gadgets/ProxyHandler.php
new file mode 100644
index 0000000..99eefce
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/ProxyHandler.php
@@ -0,0 +1,145 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\gadgets\rewrite\ContentRewriter;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * The ProxyHandler class does the actual proxy'ing work. it deals both with
+ * GET and POST based input, and peforms a request based on the input, headers and
+ * httpmethod params.
+ *
+ */
+class ProxyHandler extends ProxyBase {
+  /**
+   * Fetches the content and returns it as-is using the headers as returned
+   * by the remote host.
+   *
+   * @param string $url the url to retrieve
+   */
+  public function fetch($url) {
+    // TODO: Check to see if we can just use MakeRequestOptions::fromCurrentRequest
+    $st = BasicSecurityToken::getTokenStringFromRequest();
+    $body = isset($_GET['postData']) ? $_GET['postData'] : (isset($_POST['postData']) ? $_POST['postData'] : false);
+    $authz = isset($_GET['authz']) ? $_GET['authz'] : (isset($_POST['authz']) ? $_POST['authz'] : null);
+    $headers = isset($_GET['headers']) ? $_GET['headers'] : (isset($_POST['headers']) ? $_POST['headers'] : null);
+    $params = new MakeRequestOptions($url);
+    $params->setSecurityTokenString($st)
+      ->setAuthz($authz)
+      ->setRequestBody($body)
+      ->setHttpMethod('GET')
+      ->setFormEncodedRequestHeaders($headers)
+      ->setNoCache($this->context->getIgnoreCache());
+
+    $result = $this->makeRequest->fetch($this->context, $params);
+    $httpCode = (int)$result->getHttpCode();
+    $cleanedResponseHeaders = $this->makeRequest->cleanResponseHeaders($result->getResponseHeaders());
+    $isShockwaveFlash = false;
+    
+    foreach ($cleanedResponseHeaders as $key => $val) {
+      header("$key: $val", true);
+      if (strtoupper($key) == 'CONTENT-TYPE' && strtolower($val) == 'application/x-shockwave-flash') {
+        // We're skipping the content disposition header for flash due to an issue with Flash player 10
+        // This does make some sites a higher value phishing target, but this can be mitigated by
+        // additional referer checks.
+        $isShockwaveFlash = true;
+      }
+    }
+    if (! $isShockwaveFlash && !Config::get('debug')) {
+      header('Content-Disposition: attachment;filename=p.txt');
+    }
+    $lastModified = $result->getResponseHeader('Last-Modified') != null ? $result->getResponseHeader('Last-Modified') : gmdate('D, d M Y H:i:s', $result->getCreated()) . ' GMT';
+    $notModified = false;
+    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && $lastModified && ! isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
+      $if_modified_since = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
+      // Use the request's Last-Modified, otherwise fall back on our internal time keeping (the time the request was created)
+      $lastModified = strtotime($lastModified);
+      if ($lastModified <= $if_modified_since) {
+        $notModified = true;
+      }
+    }
+    if ($httpCode == 200) {
+      // only set caching headers if the result was 'OK'
+      $this->setCachingHeaders($lastModified);
+      // was the &gadget=<gadget url> specified in the request? if so parse it and check the rewrite settings
+      if (isset($_GET['gadget'])) {
+        $this->rewriteContent($_GET['gadget'], $result);
+      }
+    }
+    // If the cached file time is within the refreshInterval params value, return not-modified
+    if ($notModified) {
+      header('HTTP/1.0 304 Not Modified', true);
+      header('Content-Length: 0', true);
+    } else {
+      header("HTTP/1.1 $httpCode ".$result->getHttpCodeMsg());
+      // then echo the content
+      echo $result->getResponseContent();
+    }
+  }
+
+  /**
+   *
+   * @param string $gadgetUrl
+   * @param RemoteContentRequest $result
+   */
+  private function rewriteContent($gadgetUrl, RemoteContentRequest &$result) {
+    try {
+      // At the moment we're only able to rewrite CSS files, so check the content type and/or the file extension before rewriting
+      $headers = $result->getResponseHeaders();
+      $isCss = false;
+      if (isset($headers['Content-Type']) && strtolower($headers['Content-Type'] == 'text/csss')) {
+        $isCss = true;
+      } else {
+        $ext = substr($_GET['url'], strrpos($_GET['url'], '.') + 1);
+        $isCss = strtolower($ext) == 'css';
+      }
+      if ($isCss) {
+        $gadget = $this->createGadget($gadgetUrl);
+        $rewrite = $gadget->gadgetSpec->rewrite;
+        if (is_array($rewrite)) {
+          $contentRewriter = new ContentRewriter($this->context, $gadget);
+          $result->setResponseContent($contentRewriter->rewriteCSS($result->getResponseContent()));
+        }
+      }
+    } catch (\Exception $e) {
+      // ignore, not being able to rewrite anything isn't fatal
+    }
+
+  }
+
+  /**
+   * Uses the GadgetFactory to instrance the specified gadget
+   *
+   * @param string $gadgetUrl
+   * @return Gadget
+   */
+  private function createGadget($gadgetUrl) {
+    // Only include these files if appropiate, else it would slow down the entire proxy way to much
+    // make sure our context returns the gadget url and not the proxied document url
+    $this->context->setUrl($gadgetUrl);
+    // and create & return the gadget
+    $factoryClass = Config::get('gadget_factory_class');
+    $gadgetSpecFactory = new $factoryClass($this->context, null);
+    $gadget = $gadgetSpecFactory->createGadget();
+    return $gadget;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/SigningFetcher.php b/trunk/php/src/apache/shindig/gadgets/SigningFetcher.php
new file mode 100644
index 0000000..9299b59
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/SigningFetcher.php
@@ -0,0 +1,286 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\ShindigRsaSha1SignatureMethod;
+use apache\shindig\common\RemoteContentFetcher;
+use apache\shindig\common\ShindigOAuth;
+use apache\shindig\common\ShindigOAuthRequest;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * implements \signed fetch based on the OAuth request signing algorithm.
+ *
+ * Subclasses can override signMessage to use their own crypto if they don't
+ * like the oauth.net code for some reason.
+ *
+ * Instances of this class are only accessed by a single thread at a time,
+ * but instances may be created by multiple threads.
+ */
+class SigningFetcher extends RemoteContentFetcher {
+
+  protected static $OPENSOCIAL_OWNERID = "opensocial_owner_id";
+  protected static $OPENSOCIAL_VIEWERID = "opensocial_viewer_id";
+  protected static $OPENSOCIAL_APPID = "opensocial_app_id";
+  protected static $OPENSOCIAL_APPURL = "opensocial_app_url";
+  protected static $OPENSOCIAL_INSTANCEID = "opensocial_instance_id";
+  protected static $XOAUTH_PUBLIC_KEY_OLD = "xoauth_signature_publickey";
+  protected static $XOAUTH_PUBLIC_KEY_NEW = "xoauth_public_key";
+  protected static $ALLOWED_PARAM_NAME = '^[-_[:alnum:]]+$';
+
+  /**
+   * Private key we pass to the OAuth RSA_SHA1 algorithm.This can be a
+   * PrivateKey object, or a PEM formatted private key, or a DER encoded byte
+   * array for the private key.(No, really, they accept any of them.)
+   *
+   * @var resource
+   */
+  protected $privateKeyObject;
+
+  /**
+   * The name of the key, included in the fetch to help with key rotation.
+   *
+   * @var string
+   */
+  protected $keyName;
+
+  /**
+   * @var RemoteContentFetcher
+   */
+  private $fetcher;
+
+  /**
+   * Constructor based on signing with the given PrivateKey object, as returned
+   * from the openssl_pkey_get_private method.
+   *
+   * @param RemoteContentFetcher $fetcher
+   * @param string $keyName name of the key to include in the request
+   * @param resource $privateKey A key resource identifier, as returned from
+   *     openssl_pkey_get_private
+   * @return SigningFetcher
+   */
+  public static function makeFromOpenSslPrivateKey(RemoteContentFetcher $fetcher, $keyName, $privateKey) {
+    return new SigningFetcher($fetcher, $keyName, $privateKey);
+  }
+
+  /**
+   *
+   * @param RemoteContentFetcher $fetcher
+   * @param string $keyName
+   * @param resource $privateKeyObject
+   */
+  protected function __construct(RemoteContentFetcher $fetcher, $keyName, $privateKeyObject) {
+    $this->fetcher = $fetcher;
+    $this->keyName = $keyName;
+    $this->privateKeyObject = $privateKeyObject;
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  public function fetchRequest(RemoteContentRequest $request) {
+    $this->signRequest($request);
+    return $this->fetcher->fetchRequest($request);
+  }
+
+  /**
+   *
+   * @param array $requests
+   * @return array
+   */
+  public function multiFetchRequest(array $requests) {
+    foreach ($requests as $request) {
+      $this->signRequest($request);
+    }
+    return $this->fetcher->multiFetchRequest($requests);
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $request
+   */
+  private function signRequest(RemoteContentRequest $request) {
+    $url = $request->getUrl();
+    $method = $request->getMethod();
+    try {
+      // Parse the request into parameters for OAuth signing, stripping out
+      // any OAuth or OpenSocial parameters injected by the client
+      $parsedUri = parse_url($url);
+      $resource = $url;
+      $contentType = $request->getHeader('Content-Type');
+      $signBody = (stripos($contentType, 'application/x-www-form-urlencoded') !== false || $contentType == null);
+      $msgParams = array();
+      $postParams = array();
+      if ($request->getPostBody()) {
+        if ($signBody) {
+          // on normal application/x-www-form-urlencoded type post's encode and parse the post vars
+          parse_str($request->getPostBody(), $postParams);
+          $postParams = $this->sanitize($postParams);
+        } else {
+          // on any other content-type of post (application/{json,xml,xml+atom}) use the body signing hash
+          // see http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/4/spec.html for details
+          $msgParams['oauth_body_hash'] = base64_encode(sha1($request->getPostBody(), true));
+        }
+      }
+      if ($signBody && isset($postParams)) {
+        $msgParams = array_merge($msgParams, $postParams);
+      }
+      $this->addOpenSocialParams($msgParams, $request->getToken(), $request->getOptions()->ownerSigned, $request->getOptions()->viewerSigned);
+      $this->addOAuthParams($msgParams, $request->getToken());
+      $consumer = new \OAuthConsumer(NULL, NULL, NULL);
+      $signatureMethod = new ShindigRsaSha1SignatureMethod($this->privateKeyObject, null);
+      $req_req = \OAuthRequest::from_consumer_and_token($consumer, NULL, $method, $resource, $msgParams);
+      $req_req->sign_request($signatureMethod, $consumer, NULL);
+      // Rebuild the query string, including all of the parameters we added.
+      // We have to be careful not to copy POST parameters into the query.
+      // If post and query parameters share a name, they end up being removed
+      // from the query.
+      $forPost = array();
+      $postData = false;
+      if ($method == 'POST' && $signBody) {
+        foreach ($postParams as $key => $param) {
+          $forPost[$key] = $param;
+          if ($postData === false) {
+            $postData = array();
+          }
+          $postData[] = \OAuthUtil::urlencode_rfc3986($key) . "=" . \OAuthUtil::urlencode_rfc3986($param);
+        }
+        if ($postData !== false) {
+          $postData = implode("&", $postData);
+        }
+      }
+      $newQueryParts = array();
+      foreach ($req_req->get_parameters() as $key => $param) {
+        if (! isset($forPost[$key])) {
+          if (!is_array($param)) {
+            $newQueryParts[] = urlencode($key) . '=' . urlencode($param);
+          } else {
+            foreach($param as $elem) {
+              $newQueryParts[] = urlencode($key) . '=' . urlencode($elem);
+            }
+          }
+        }
+        $newQuery = implode('&', $newQueryParts);
+      }
+      // Careful here; the OAuth form encoding scheme is slightly different than
+      // the normal form encoding scheme, so we have to use the OAuth library
+      // formEncode method.
+      $url = $parsedUri['scheme'] . '://' . $parsedUri['host'] . (isset($parsedUri['port']) ? ':' . $parsedUri['port'] : '') . (isset($parsedUri['path']) ? $parsedUri['path'] : '') . '?' . $newQuery;
+      $request->setUri($url);
+      if ($signBody) {
+        $request->setPostBody($postData);
+      }
+    } catch (\Exception $e) {
+      throw new GadgetException($e);
+    }
+  }
+
+  /**
+   *
+   * @param array $msgParams
+   * @param SecurityToken $token
+   * @param boolean $signOwner
+   * @param boolean $signViewer
+   */
+  private function addOpenSocialParams(&$msgParams, SecurityToken $token, $signOwner, $signViewer) {
+    if ($signOwner) {
+      $owner = $token->getOwnerId();
+      if ($owner != null) {
+	    $msgParams[SigningFetcher::$OPENSOCIAL_OWNERID] = $owner;
+      }
+    }
+    if ($signViewer) {
+      $viewer = $token->getViewerId();
+      if ($viewer != null) {
+	    $msgParams[SigningFetcher::$OPENSOCIAL_VIEWERID] = $viewer;
+      }
+    }
+    if ($signOwner || $signViewer) {
+      $app = $token->getAppId();
+      if ($app != null) {
+	    $msgParams[SigningFetcher::$OPENSOCIAL_APPID] = $app;
+      }
+      $url = $token->getAppUrl();
+      if ($url != null) {
+	    $msgParams[SigningFetcher::$OPENSOCIAL_APPURL] = $url;
+      }
+      $moduleId = $token->getModuleId();
+      if ($moduleId != null) {
+	    $msgParams[SigningFetcher::$OPENSOCIAL_INSTANCEID] = $moduleId;
+      }
+    }
+  }
+
+  /**
+   *
+   * @param array $msgParams
+   * @param SecurityToken $token
+   */
+  private function addOAuthParams(&$msgParams, SecurityToken $token) {
+    $msgParams[ShindigOAuth::$OAUTH_TOKEN] = '';
+    $domain = $token->getDomain();
+    if ($domain != null) {
+      $msgParams[ShindigOAuth::$OAUTH_CONSUMER_KEY] = $domain;
+    }
+    if ($this->keyName != null) {
+      $msgParams[SigningFetcher::$XOAUTH_PUBLIC_KEY_OLD] = $this->keyName;
+      $msgParams[SigningFetcher::$XOAUTH_PUBLIC_KEY_NEW] = $this->keyName;
+    }
+    $nonce = ShindigOAuthRequest::generate_nonce();
+    $msgParams[ShindigOAuth::$OAUTH_NONCE] = $nonce;
+    $timestamp = time();
+    $msgParams[ShindigOAuth::$OAUTH_TIMESTAMP] = $timestamp;
+    $msgParams[ShindigOAuth::$OAUTH_SIGNATURE_METHOD] = ShindigOAuth::$RSA_SHA1;
+  }
+
+  /**
+   * Strip out any owner or viewer id passed by the client.
+   *
+   * @param array $params
+   * @return array
+   */
+  private function sanitize($params) {
+    $list = array();
+    foreach ($params as $key => $p) {
+      if ($this->allowParam($key)) {
+        $list[$key] = $p;
+      }
+    }
+    return $list;
+  }
+
+  /**
+   *
+   * @param string $paramName
+   * @return booelan
+   */
+  private function allowParam($paramName) {
+    $canonParamName = strtolower($paramName);
+    // Exclude the fields which are only used to tell the proxy what to do
+    // and the fields which should be added by signing the request later on
+    if ($canonParamName == "output" || $canonParamName == "httpmethod" || $canonParamName == "authz" || $canonParamName == "st" || $canonParamName == "headers" || $canonParamName == "url" || $canonParamName == "contenttype" || $canonParamName == "postdata" || $canonParamName == "numentries" || $canonParamName == "getsummaries" || $canonParamName == "signowner" || $canonParamName == "signviewer" || $canonParamName == "gadget" || $canonParamName == "bypassspeccache" || substr($canonParamName, 0, 5) == "oauth" || substr($canonParamName, 0, 6) == "xoauth" || substr($canonParamName, 0, 9) == "opensocial" || $canonParamName == "container") {
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/SigningFetcherFactory.php b/trunk/php/src/apache/shindig/gadgets/SigningFetcherFactory.php
new file mode 100644
index 0000000..672952f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/SigningFetcherFactory.php
@@ -0,0 +1,103 @@
+<?php
+namespace apache\shindig\gadgets;
+use apache\shindig\common\Config;
+use apache\shindig\common\File;
+use apache\shindig\common\RemoteContentFetcher;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Produces Signing content fetchers for input tokens.
+ */
+class SigningFetcherFactory {
+  private $keyName;
+  private $privateKey;
+
+  /**
+   * Produces a signing fetcher that will sign requests and delegate actual
+   * network retrieval to the {@code networkFetcher}
+   *
+   * @param RemoteContentFetcher $networkFetcher The fetcher that will be doing actual work.
+   * @return SigningFetcher
+   * @throws GadgetException
+   */
+  public function getSigningFetcher(RemoteContentFetcher $networkFetcher) {
+    return SigningFetcher::makeFromOpenSslPrivateKey($networkFetcher, $this->keyName, $this->privateKey);
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getKeyName()
+  {
+    return $this->keyName;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getPrivateKey()
+  {
+    return $this->privateKey;
+  }
+
+  /**
+   * @param keyFile The file containing your private key for signing requests.
+   */
+  public function __construct($keyFile = null) {
+    $this->keyName = 'http://' . $_SERVER["HTTP_HOST"] . Config::get('web_prefix') . '/public.cer';
+    if (! empty($keyFile)) {
+      $rsa_private_key = false;
+      $privateKey = null;
+      try {
+        if (File::exists($keyFile)) {
+          if (File::readable($keyFile)) {
+            $rsa_private_key = @file_get_contents($keyFile);
+          } else {
+            throw new \Exception("Could not read keyfile ($keyFile), check the file name and permission");
+          }
+        }
+        if (! $rsa_private_key) {
+          $rsa_private_key = '';
+        } else {
+          $phrase = Config::get('private_key_phrase') != '' ? (Config::get('private_key_phrase')) : null;
+          if (strpos($rsa_private_key, "-----BEGIN") === false) {
+            $privateKey .= "-----BEGIN PRIVATE KEY-----\n";
+            $chunks = str_split($rsa_private_key, 64);
+            foreach ($chunks as $chunk) {
+              $privateKey .= $chunk . "\n";
+            }
+            $privateKey .= "-----END PRIVATE KEY-----";
+          } else {
+            $privateKey = $rsa_private_key;
+          }
+          if (! $rsa_private_key = @openssl_pkey_get_private($privateKey, $phrase)) {
+            throw new \Exception("Could not create the key");
+          }
+        }
+      } catch (\Exception $e) {
+        throw new \Exception("Error loading private key: " . $e);
+      }
+      $this->privateKey = $rsa_private_key;
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/Substitutions.php b/trunk/php/src/apache/shindig/gadgets/Substitutions.php
new file mode 100644
index 0000000..3c29bad
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/Substitutions.php
@@ -0,0 +1,108 @@
+<?php
+namespace apache\shindig\gadgets;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class Substitutions {
+  /**
+   * @var array
+   */
+  protected $types = array('MESSAGE' => 'MSG', 'BIDI' => 'BIDI', 'USER_PREF' => 'UP', 'MODULE' => 'MODULE');
+
+  /**
+   * @var array
+   */
+  protected $substitutions = array();
+
+  public function __construct() {
+    foreach ($this->types as $type) {
+      $this->substitutions[$type] = array();
+    }
+  }
+
+  /**
+   *
+   * @param string $type
+   * @param string $key
+   * @param string $value
+   */
+  public function addSubstitution($type, $key, $value) {
+    $this->substitutions[$type]["__{$type}_{$key}__"] = $value;
+  }
+
+  /**
+   *
+   * @param string $type
+   * @param array $array
+   */
+  public function addSubstitutions($type, $array) {
+    foreach ($array as $key => $value) {
+      $this->addSubstitution($type, $key, $value);
+    }
+  }
+
+  /**
+   *
+   * @param string $input
+   * @return string
+   */
+  public function substitute($input) {
+    foreach ($this->types as $type) {
+      $input = $this->substituteType($type, $input);
+    }
+    return $input;
+  }
+
+  /**
+   *
+   * @param string $type
+   * @param string $input
+   * @return string
+   */
+  public function substituteType($type, $input) {
+    foreach ($this->substitutions[$type] as $key => $value) {
+      if (! is_array($value)) {
+        $input = str_replace($key, $value, $input);
+      }
+    }
+    return $input;
+  }
+
+  /**
+   * Substitutes a uri
+   * @param string $type The type to substitute, or null for all types.
+   * @param string $uri
+   * @return string The substituted uri, or a dummy value if the result is invalid.
+   */
+  public function substituteUri($type, $uri) {
+    if (empty($uri)) {
+      return null;
+    }
+    try {
+      if (! empty($type)) {
+        return $this->substituteType($type, $uri);
+      } else {
+        return $this->substitute($uri);
+      }
+    } catch (\Exception $e) {
+      return "";
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/AccesorInfo.php b/trunk/php/src/apache/shindig/gadgets/oauth/AccesorInfo.php
new file mode 100644
index 0000000..b189941
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/AccesorInfo.php
@@ -0,0 +1,66 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class AccesorInfo {
+  /**
+   * @var OAuthAccessor
+   */
+  public $accessor;
+  public $httpMethod;
+  public $signatureType;
+  public $paramLocation;
+
+  public function getParamLocation() {
+    return $this->paramLocation;
+  }
+
+  public function setParamLocation($paramLocation) {
+    $this->paramLocation = $paramLocation;
+  }
+
+  /**
+   * @return OAuthAccessor
+   */
+  public function getAccessor() {
+    return $this->accessor;
+  }
+
+  public function setAccessor($accessor) {
+    $this->accessor = $accessor;
+  }
+
+  public function getHttpMethod() {
+    return $this->httpMethod;
+  }
+
+  public function setHttpMethod($httpMethod) {
+    $this->httpMethod = $httpMethod;
+  }
+
+  public function getSignatureType() {
+    return $this->signatureType;
+  }
+
+  public function setSignatureType($signatureType) {
+    $this->signatureType = $signatureType;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/BasicGadgetOAuthTokenStore.php b/trunk/php/src/apache/shindig/gadgets/oauth/BasicGadgetOAuthTokenStore.php
new file mode 100644
index 0000000..c5ed624
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/BasicGadgetOAuthTokenStore.php
@@ -0,0 +1,124 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\gadgets\GadgetException;
+use apache\shindig\common\Config;
+use apache\shindig\common\Cache;
+use apache\shindig\common\ShindigOAuth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class BasicGadgetOAuthTokenStore extends GadgetOAuthTokenStore {
+
+  /** default location for consumer keys and secrets */
+  private $OAUTH_CONFIG = "oauth.json";
+  private $CONSUMER_SECRET_KEY = "consumer_secret";
+  private $CONSUMER_KEY_KEY = "consumer_key";
+  private $KEY_TYPE_KEY = "key_type";
+
+  /**
+   *
+   * @param OAuthStore $store
+   * @param BasicGadgetSpecFactory $specFadtory
+   */
+  public function __construct($store, $specFadtory) {
+    parent::__construct($store, $specFadtory);
+    $this->OAUTH_CONFIG = Config::get('container_path') . $this->OAUTH_CONFIG;
+  }
+
+  /**
+   * @param SigningFetcher $fetcher
+   */
+  public function initFromConfigFile($fetcher) {
+    // Read our consumer keys and secrets from config/oauth.js
+    // This actually involves fetching gadget specs
+    try {
+      $oauthConfigStr = file_get_contents($this->OAUTH_CONFIG);
+      // remove all comments because this confuses the json parser
+      // note: the json parser also crashes on trailing ,'s in records so please don't use them
+      $contents = preg_replace('@/\\*.*?\\*/@s', '', $oauthConfigStr);
+      $oauthConfig = json_decode($contents, true);
+      if ($oauthConfig == $contents) {
+        throw new GadgetException("OAuth configuration json failed to parse.");
+      }
+      foreach ($oauthConfig as $gadgetUri => $value) {
+        $this->storeConsumerInfos($gadgetUri, $value);
+      }
+    } catch (\Exception $e) {
+      throw new GadgetException($e);
+    }
+  }
+
+  /**
+   *
+   * @param string $gadgetUri
+   * @param array $oauthConfig
+   */
+  protected function storeConsumerInfos($gadgetUri, $oauthConfig) {
+    foreach ($oauthConfig as $key => $value) {
+      $serviceName = $key;
+      $consumerInfo = $value;
+      $this->storeConsumerInfo($gadgetUri, $serviceName, $consumerInfo);
+    }
+  }
+
+  /**
+   *
+   * @param string $gadgetUri
+   * @param string $serviceName
+   * @param array $consumerInfo
+   */
+  protected function storeConsumerInfo($gadgetUri, $serviceName, $consumerInfo) {
+    if (! isset($consumerInfo[$this->CONSUMER_SECRET_KEY]) || ! isset($consumerInfo[$this->CONSUMER_KEY_KEY]) || ! isset($consumerInfo[$this->KEY_TYPE_KEY])) {
+      throw new \Exception("Invalid configuration in oauth.json");
+    }
+    $consumerSecret = $consumerInfo[$this->CONSUMER_SECRET_KEY];
+    $consumerKey = $consumerInfo[$this->CONSUMER_KEY_KEY];
+    $keyTypeStr = $consumerInfo[$this->KEY_TYPE_KEY];
+    $keyType = 'HMAC_SYMMETRIC';
+    if ($keyTypeStr == "RSA_PRIVATE") {
+      $keyType = 'RSA_PRIVATE';
+      $cache = Cache::createCache(Config::get('data_cache'), 'OAuthToken');
+      if (($cachedRequest = $cache->get(md5("RSA_KEY_" . $serviceName))) !== false) {
+        $consumerSecret = $cachedRequest;
+      } else {
+        $key = $consumerInfo[$this->CONSUMER_SECRET_KEY];
+        if (empty($key)) {
+          throw new \Exception("Invalid key");
+        }
+        if (strpos($key, "-----BEGIN") === false) {
+          $strip_this = array(" ", "\n", "\r");
+          //removes breaklines and trim.
+          $rsa_private_key = trim(str_replace($strip_this, "", $key));
+          $consumerSecret = ShindigOAuth::$BEGIN_PRIVATE_KEY . "\n";
+          $chunks = str_split($rsa_private_key, 64);
+          foreach ($chunks as $chunk) {
+            $consumerSecret .= $chunk . "\n";
+          }
+          $consumerSecret .= ShindigOAuth::$END_PRIVATE_KEY;
+        } else {
+          $consumerSecret = $key;
+        }
+        $cache->set(md5("RSA_KEY_" . $serviceName), $consumerSecret);
+      }
+    }
+    $kas = new ConsumerKeyAndSecret($consumerKey, $consumerSecret, $keyType);
+    $this->storeConsumerKeyAndSecret($gadgetUri, $serviceName, $kas);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/BasicOAuthStore.php b/trunk/php/src/apache/shindig/gadgets/oauth/BasicOAuthStore.php
new file mode 100644
index 0000000..beeddea
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/BasicOAuthStore.php
@@ -0,0 +1,177 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\common\ShindigOAuthNoDataException;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class BasicOAuthStore implements OAuthStore {
+
+  /**
+   *
+   * @var array
+   */
+  private $consumerInfos = array();
+
+  /**
+   *
+   * @var array
+   */
+  private $tokens = array();
+
+  /**
+   *
+   * @var string
+   */
+  private $defaultConsumerKey;
+
+  /**
+   *
+   * @var string
+   */
+  private $defaultConsumerSecret;
+
+  /**
+   *
+   * @param string $consumerKey
+   * @param string $privateKey
+   */
+  public function __construct($consumerKey = null, $privateKey = null) {
+    $this->defaultConsumerKey = $consumerKey;
+    $this->defaultConsumerSecret = $privateKey;
+  }
+
+  /**
+   *
+   * @param array $consumerInfos
+   * @param array $tokens
+   */
+  public function setHashMapsForTesting($consumerInfos, $tokens) {
+    $this->consumerInfos = $consumerInfos;
+    $this->tokens = $tokens;
+  }
+
+  /**
+   *
+   * @param TokenKey $tokenKey
+   * @param ProviderInfo $provInfo
+   * @return AccesorInfo
+   */
+  public function getOAuthAccessorTokenKey(TokenKey $tokenKey, ProviderInfo $provInfo) {
+    $provKey = new ProviderKey();
+    $provKey->setGadgetUri($tokenKey->getGadgetUri());
+    $provKey->setServiceName($tokenKey->getServiceName());
+    //AccesorInfo
+    $result = $this->getOAuthAccessorProviderKey($provKey, $provInfo);
+    //TokenInfo
+    $accessToken = $this->getTokenInfo($tokenKey);
+    if ($accessToken != null) {
+      // maybe convert into methods
+      $result->getAccessor()->accessToken = $accessToken->getAccessToken();
+      $result->getAccessor()->tokenSecret = $accessToken->getTokenSecret();
+    }
+    return $result;
+  }
+
+  /**
+   *
+   * @param ProviderKey $providerKey
+   * @param ProviderInfo $provInfo
+   * @return AccesorInfo
+   */
+  public function getOAuthAccessorProviderKey(ProviderKey $providerKey, ProviderInfo $provInfo) {
+    if ($provInfo == null) {
+      throw new ShindigOAuthNoDataException("must pass non-null provider info to getOAuthAccessor");
+    }
+    //AccesorInfo
+    $result = new AccesorInfo();
+    $result->setHttpMethod($provInfo->getHttpMethod());
+    $result->setParamLocation($provInfo->getParamLocation());
+    //ConsumerKeyAndSecret
+    $key = md5(serialize($providerKey));
+    $consumerKeyAndSecret = null;
+    if (isset($this->consumerInfos[$key])) {
+      $consumerKeyAndSecret = $this->consumerInfos[$key];
+    } else {
+      throw new ShindigOAuthNoDataException("Invalid or missing consumer key, please check your oauth.json configuration.");
+    }
+    if ($consumerKeyAndSecret == null) {
+      if ($this->defaultConsumerKey == null || $this->defaultConsumerSecret == null) {
+        throw new ShindigOAuthNoDataException("ConsumerKeyAndSecret was null in oauth store");
+      } else {
+        $consumerKeyAndSecret = new ConsumerKeyAndSecret($this->defaultConsumerKey, $this->defaultConsumerSecret, OAuthStoreVars::$KeyType['RSA_PRIVATE']);
+      }
+    }
+    //OAuthServiceProvider
+    $oauthProvider = $provInfo->getProvider();
+    if (! isset($oauthProvider)) {
+      throw new ShindigOAuthNoDataException("OAuthService provider was null in provider info");
+    }
+    // Accesing the class
+    $usePublicKeyCrypto = ($consumerKeyAndSecret->getKeyType() == OAuthStoreVars::$KeyType['RSA_PRIVATE']);
+    //OAuthConsumer
+    $consumer = ($usePublicKeyCrypto) ? new \OAuthConsumer($consumerKeyAndSecret->getConsumerKey(), null, $oauthProvider) : new \OAuthConsumer($consumerKeyAndSecret->getConsumerKey(), $consumerKeyAndSecret->getConsumerSecret(), $oauthProvider);
+    if ($usePublicKeyCrypto) {
+      $consumer->setProperty(\OAuthSignatureMethod_RSA_SHA1::$PRIVATE_KEY, $consumerKeyAndSecret->getConsumerSecret());
+      $result->setSignatureType(OAuthStoreVars::$SignatureType['RSA_SHA1']);
+    } else {
+      $result->setSignatureType(OAuthStoreVars::$SignatureType['HMAC_SHA1']);
+    }
+
+    $result->setAccessor(new OAuthAccessor($consumer));
+    return $result;
+  }
+
+  /**
+   *
+   * @param ProviderKey $providerKey
+   * @param ConsumerKeyAndSecret $keyAndSecret
+   */
+  public function setOAuthConsumerKeyAndSecret($providerKey, $keyAndSecret) {
+    $key = md5(serialize($providerKey));
+    $this->consumerInfos[$key] = $keyAndSecret;
+  }
+
+  /**
+   *
+   * @param TokenKey $tokenKey
+   * @param TokenInfo $tokenInfo
+   */
+  public function setTokenAndSecret($tokenKey, $tokenInfo) {
+    $this->tokens[md5(serialize($tokenKey))] = $tokenInfo;
+  }
+
+  /**
+   *
+   * @param TokenKey $tokenKey
+   */
+  public function removeTokenAndSecret($tokenKey) {
+    unset($this->tokens[md5(serialize($tokenKey))]);
+  }
+
+  /**
+   *
+   * @param TokenKey $tokenKey
+   * @return TokenInfo
+   */
+  protected function getTokenInfo($tokenKey) {
+    $key = md5(serialize($tokenKey));
+    return isset($this->tokens[$key]) ? $this->tokens[$key] : null;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/ConsumerKeyAndSecret.php b/trunk/php/src/apache/shindig/gadgets/oauth/ConsumerKeyAndSecret.php
new file mode 100644
index 0000000..a9a8c3e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/ConsumerKeyAndSecret.php
@@ -0,0 +1,45 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ConsumerKeyAndSecret {
+  private $consumerKey;
+  private $consumerSecret;
+  private $keyType;
+
+  public function __construct($key, $secret, $type) {
+    $this->consumerKey = $key;
+    $this->consumerSecret = $secret;
+    $this->keyType = $type;
+  }
+
+  public function getConsumerKey() {
+    return $this->consumerKey;
+  }
+
+  public function getConsumerSecret() {
+    return $this->consumerSecret;
+  }
+
+  public function getKeyType() {
+    return $this->keyType;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/Endpoint.php b/trunk/php/src/apache/shindig/gadgets/oauth/Endpoint.php
new file mode 100644
index 0000000..33f81e8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/Endpoint.php
@@ -0,0 +1,53 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Description of an OAuth request token or access token URL.
+ */
+class EndPoint {
+  /**
+   * @var string
+   */
+  public $url;
+
+  /**
+   * @var string
+   */
+  public $method;
+
+  /**
+   * @var string
+   */
+  public $location;
+
+  /**
+   *
+   * @param string $url
+   * @param string $method
+   * @param string $location
+   */
+  public function __construct($url, $method, $location) {
+    $this->url = $url;
+    $this->method = $method;
+    $this->location = $location;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/GadgetInfo.php b/trunk/php/src/apache/shindig/gadgets/oauth/GadgetInfo.php
new file mode 100644
index 0000000..6c2ad63
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/GadgetInfo.php
@@ -0,0 +1,67 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetInfo {
+  /**
+   *
+   * @var string
+   */
+  private $serviceName;
+
+  /**
+   *
+   * @var ProviderInfo
+   */
+  private $providerInfo;
+
+  /**
+   *
+   * @return string
+   */
+  public function getServiceName() {
+    return $this->serviceName;
+  }
+
+  /**
+   *
+   * @param string $serviceName
+   */
+  public function setServiceName($serviceName) {
+    $this->serviceName = $serviceName;
+  }
+
+  /**
+   *
+   * @return ProviderInfo
+   */
+  public function getProviderInfo() {
+    return $this->providerInfo;
+  }
+
+  /**
+   *
+   * @param ProviderInfo $providerInfo
+   */
+  public function setProviderInfo(ProviderInfo $providerInfo) {
+    $this->providerInfo = $providerInfo;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.php b/trunk/php/src/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.php
new file mode 100644
index 0000000..6806e55
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/GadgetOAuthTokenStore.php
@@ -0,0 +1,217 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\gadgets\GadgetException;
+use apache\shindig\gadgets\GadgetSpec;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Higher-level interface that allows callers to store and retrieve
+ * OAuth-related data directly from {@code GadgetSpec}s, {@code GadgetContext}s,
+ * etc. See {@link OAuthStore} for a more detailed explanation of the OAuth
+ * Data Store.
+ */
+class GadgetOAuthTokenStore {
+
+  // we use POST if no HTTP method is specified for access and request URLs
+  // (user authorization always uses GET)
+  public static $DEFAULT_HTTP_METHOD = "POST";
+
+  /**
+   * @var OAuthStore
+   */
+  private $store;
+
+  /**
+   * @var GadgetSpec
+   */
+  private $gadgetSpec;
+
+  /**
+   * @var BasicGadgetSpecFactory
+   */
+  private $specFactory;
+
+  /**
+   * Public constructor.
+   *
+   * @param OAuthStore $store an {@link OAuthStore} that can store and retrieve OAuth
+   *              tokens, as well as information about service providers.
+   * @param BasicGadgetSpecFactory $specFactory
+   */
+  public function __construct($store, $specFactory) {
+    $this->specFactory = $specFactory;
+    $this->store = $store;
+  }
+
+  /**
+   * Stores a negotiated consumer key and secret in the gadget store.
+   * The "secret" can either be a consumer secret in the strict OAuth sense,
+   * or it can be a PKCS8-then-Base64 encoded private key that we'll be using
+   * with this service provider.
+   *
+   * @param string $gadgetUrl the URL of the gadget
+   * @param string $serviceName the service provider with whom we have negotiated a
+   *                    consumer key and secret.
+   * @param ConsumerKeyAndSecret $keyAndSecret
+   */
+  public function storeConsumerKeyAndSecret($gadgetUrl, $serviceName, $keyAndSecret) {
+    $providerKey = new ProviderKey();
+    $providerKey->setGadgetUri($gadgetUrl);
+    $providerKey->setServiceName($serviceName);
+    $this->store->setOAuthConsumerKeyAndSecret($providerKey, $keyAndSecret);
+  }
+
+  /**
+   * Stores an access token in the OAuth Data Store.
+   * @param TokenKey $tokenKey information about the Gadget storing the token.
+   * @param TokenInfo $tokenInfo the TokenInfo to be stored in the OAuth data store.
+   */
+  public function storeTokenKeyAndSecret($tokenKey, $tokenInfo) {
+    $getGadgetUri = $tokenKey->getGadgetUri();
+    if (empty($getGadgetUri)) {
+      throw new \Exception("found empty gadget URI in TokenKey");
+    }
+    $getUserId = $tokenKey->getUserId();
+    if (empty($getUserId)) {
+      throw new \Exception("found empty userId in TokenKey");
+    }
+    $this->store->setTokenAndSecret($tokenKey, $tokenInfo);
+  }
+
+  /**
+   *
+   * @param TokenKey $tokenKey
+   */
+  public function removeTokenAndSecret(TokenKey $tokenKey) {
+    $this->store->removeTokenAndSecret($tokenKey);
+  }
+
+  /**
+   * Retrieve an OAuthAccessor that is ready to sign OAuthMessages.
+   *
+   * @param TokenKey $tokenKey information about the gadget retrieving the accessor.
+   *
+   * @return OAuthAccessorInfo containing an OAuthAccessor (whic can be
+   *         passed to an OAuthMessage.sign method), as well as httpMethod and
+   *         signatureType fields.
+   */
+  public function getOAuthAccessor(TokenKey $tokenKey, $ignoreCache) {
+    $gadgetUri = $tokenKey->getGadgetUri();
+    if (empty($gadgetUri)) {
+      throw new OAuthStoreException("found empty gadget URI in TokenKey");
+    }
+    $getUserId = $tokenKey->getUserId();
+    if (empty($getUserId)) {
+      throw new OAuthStoreException("found empty userId in TokenKey");
+    }
+    $gadgetSpec = $this->specFactory->getGadgetSpecUri($gadgetUri, $ignoreCache);
+    $provInfo = $this->getProviderInfo($gadgetSpec, $tokenKey->getServiceName());
+    return $this->store->getOAuthAccessorTokenKey($tokenKey, $provInfo);
+  }
+
+  /**
+   * Reads OAuth provider information out of gadget spec.
+   * @param GadgetSpec $spec
+   * @param string $serviceName
+   * @return GadgetInfo
+   */
+  public static function getProviderInfo(GadgetSpec $spec, $serviceName) {
+    $oauthSpec = $spec->oauth;
+    if ($oauthSpec == null) {
+      $message = "gadget spec is missing /ModulePrefs/OAuth section";
+      throw new GadgetException($message);
+    }
+    $service = null;
+    if (isset($oauthSpec[$serviceName])) {
+      $service = $oauthSpec[$serviceName];
+    }
+    if ($service == null) {
+      $message = '';
+      $message .= "Spec does not contain OAuth service '";
+      $message .= $serviceName;
+      $message .= "'.  Known services: ";
+      foreach ($oauthSpec as $key => $value) {
+        $message .= "'";
+        $message .= $key;
+        $message .= "'";
+        $message .= ", ";
+      }
+      throw new GadgetException($message);
+    }
+    $provider = new OAuthServiceProvider($service->getRequestUrl(), $service->getAuthorizationUrl(), $service->getAccessUrl());
+    $httpMethod = null;
+    $paramLocation = null;
+    if ($service->getRequestUrl()) {
+      switch ($service->getRequestUrl()->method) {
+        case "GET":
+          $httpMethod = OAuthStoreVars::$HttpMethod['GET'];
+          break;
+        case "POST":
+        default:
+          $httpMethod = OAuthStoreVars::$HttpMethod['POST'];
+          break;
+      }
+
+      switch ($service->getRequestUrl()->location) {
+        case OAuthStoreVars::$OAuthParamLocation['URI_QUERY']:
+        case OAuthStoreVars::$OAuthParamLocation['POST_BODY']:
+        case OAuthStoreVars::$OAuthParamLocation['AUTH_HEADER']:
+          $paramLocation = $service->getRequestUrl()->location;
+          break;
+        default:
+          $paramLocation = OAuthStoreVars::$OAuthParamLocation['AUTH_HEADER'];
+          break;
+      }
+    }
+    $provInfo = new ProviderInfo();
+    $provInfo->setHttpMethod($httpMethod);
+    $provInfo->setParamLocation($paramLocation);
+    // TODO: for now, we'll just set the signature type to HMAC_SHA1
+    // as this will be ignored later on when retrieving consumer information.
+    // There, if we find a negotiated HMAC key, we will use HMAC_SHA1. If we
+    // find a negotiated RSA key, we will use RSA_SHA1. And if we find neither,
+    // we may use RSA_SHA1 with a default signing key.
+    $provInfo->setSignatureType(OAuthStoreVars::$SignatureType['HMAC_SHA1']);
+    $provInfo->setProvider($provider);
+    return $provInfo;
+  }
+
+  /**
+   * Extracts a single oauth-related parameter from a key-value map,
+   * throwing an exception if the parameter could not be found (unless the
+   * parameter is optional, in which case null is returned).
+   *
+   * @param array $params the key-value map from which to pull the value (parameter)
+   * @param string $ paramName the name of the parameter (key).
+   * @param boolean $isOptional if it's optional, don't throw an exception if it's not
+   *                   found.
+   * @return the value corresponding to the key (paramName)
+   * @throws GadgetException if the parameter value couldn't be found.
+   */
+  static function getOAuthParameter($params, $paramName, $isOptional) {
+    $param = @$params[$paramName];
+    if ($param == null && ! $isOptional) {
+      $message = "parameter '" . $paramName . "' missing in oauth feature section of gadget spec";
+      throw new GadgetException($message);
+    }
+    return ($param == null) ? null : trim($param);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/Location.php b/trunk/php/src/apache/shindig/gadgets/oauth/Location.php
new file mode 100644
index 0000000..3bf7503
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/Location.php
@@ -0,0 +1,31 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Location for OAuth parameters in requests to an OAuth request token,
+ * access token, or resource URL.  (Lowercase to match gadget spec schema)
+ */
+class Location {
+  public static $header = "auth-header";
+  public static $url = "url-query";
+  public static $body = "post-body";
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/Method.php b/trunk/php/src/apache/shindig/gadgets/oauth/Method.php
new file mode 100644
index 0000000..d5fac4b
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/Method.php
@@ -0,0 +1,29 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Method to use for requests to an OAuth request token or access token URL.
+ */
+class Method {
+  public static $GET = "GET";
+  public static $POST = "POST";
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuth2Fetcher.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuth2Fetcher.php
new file mode 100644
index 0000000..c31a31a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuth2Fetcher.php
@@ -0,0 +1,192 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\common\ShindigOAuthProtocolException;
+use apache\shindig\gadgets\GadgetException;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\Config;
+use apache\shindig\common\ShindigOAuth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * implements \the OAuth 2.0 dance for gadgets.
+ *
+ *
+ * This class is not thread-safe; create a new one for each request that
+ * requires OAuth signing.
+ */
+class OAuth2Fetcher extends OAuthFetcher {
+  /**
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  public function fetchRequest(RemoteContentRequest $request) {
+  	$this->realRequest = $request;
+    $this->checkCanApprove();
+    if ($this->needApproval()) {
+      $this->buildAznUrl();
+      // break out of the content fetching chain, we need permission from
+      // the user to do this
+      return $this->buildOAuthApprovalResponse();
+    } elseif ($this->needAccessToken()) {
+      $this->getAccessToken($request);
+      $this->saveAccessToken();
+      $this->buildClientAccessState();
+    }
+    return $this->fetchData();
+  }  
+  
+  /**
+   * Do we need to get the user's approval to access the data?
+   *
+   * @return boolean
+   */
+  protected function needApproval() {
+    if ($this->accessorInfo == NULL) {
+      return true;
+    } else {
+      return ($this->accessorInfo->getAccessor()->accessToken == null && ! $this->requestParams->getReceivedCallback());
+    }
+  }
+  
+  /**
+   * Do we need to exchange a request token for an access token?
+   *
+   * @return boolean
+   */
+  protected function needAccessToken() {
+    return ($this->accessorInfo->getAccessor()->accessToken == null && $this->requestParams->getReceivedCallback());
+  }
+  
+  /**
+   * Get honest-to-goodness user data.
+   *
+   * @return RemoteContentRequest
+   */
+  protected function fetchData() {
+    try {
+      $headers = 'Authorization: Bearer ' . $this->accessorInfo->getAccessor()->accessToken;
+      $this->realRequest->setHeaders($headers);
+      $remoteFetcherClass = Config::get('remote_content_fetcher');
+      $fetcher = new $remoteFetcherClass();
+      $content = $fetcher->fetchRequest($this->realRequest);
+      $statusCode = $content->getHttpCode();
+      //TODO is there a better way to detect an SP error? For example: http://wiki.oauth.net/ProblemReporting
+      if ($statusCode == 401) {
+        $tokenKey = $this->buildTokenKey();
+        $this->tokenStore->removeTokenAndSecret($tokenKey);
+      } else if ($statusCode >= 400 && $statusCode < 500) {
+        $message = $this->parseAuthHeader(null, $content);
+        if ($message->get_parameter(ShindigOAuth::$OAUTH_PROBLEM) != null) {
+          throw new ShindigOAuthProtocolException($message);
+        }
+      }
+      // Track metadata on the response
+      $this->addResponseMetadata($content);
+      return $content;
+    } catch (\Exception $e) {
+      throw new GadgetException("INTERNAL SERVER ERROR: " . $e);
+    }
+  }
+  
+  /**
+   * Builds the URL the client needs to visit to approve access.
+   */
+  protected function buildAznUrl() {
+    // At some point we can be clever and use a callback URL to improve
+    // the user experience, but that's too complex for now.
+    $accessor = $this->accessorInfo->getAccessor();
+    $azn = $accessor->consumer->callback_url->userAuthorizationURL;
+    $authUrl = $azn->url;
+    if (strstr($authUrl, "?") == FALSE) {
+      $authUrl .= "?";
+    } else {
+      $authUrl .= "&";
+    }
+    $authUrl .= "client_id=";
+    $authUrl .= urlencode($accessor->consumer->key);
+    $authUrl .= '&response_type=code';
+    $callbackState = new OAuthCallbackState($this->oauthCrypter);
+    $callbackUrl = "http://" . getenv('HTTP_HOST') . "/gadgets/oauthcallback";
+    $callbackState->setRealCallbackUrl($callbackUrl);
+    $state = $callbackState->getEncryptedState();
+    $authUrl .= "&state=" . urlencode($state);
+    $this->aznUrl = $authUrl;
+  }
+  
+  /**
+   *
+   * @param RemoteContentRequest $request
+   * @throws GadgetException
+   */
+  protected function getAccessToken(RemoteContentRequest $request) {
+    try {
+      $accessor = $this->accessorInfo->getAccessor();
+      $url = $accessor->consumer->callback_url->accessTokenURL;
+      $msgParams = array();
+      $callbackUrl = $this->requestParams->getReceivedCallback();
+      if (strlen($callbackUrl) > 0) {
+        $parsed_url = parse_url($callbackUrl);
+        parse_str($parsed_url["query"], $url_params);
+        $this->handleErrorResponse($url_params);
+        if (strlen($url_params["code"])) {
+          $msgParams['code'] = $url_params["code"];
+          $msgParams['grant_type'] = 'authorization_code';
+        } else {
+          throw new GadgetException("Invalid received callback URL: ".$callbackUrl);
+        }
+      }
+      $msgParams['client_id'] = urlencode($accessor->consumer->key);
+      $msgParams['client_secret'] = urlencode($accessor->consumer->secret);
+      $msgParams['redirect_uri'] = "http://" . getenv('HTTP_HOST') . "/gadgets/oauthcallback";
+      
+      $request = new RemoteContentRequest($url->url);
+      $request->setMethod('POST');
+      $request->setPostBody($msgParams);
+      
+      $remoteFetcherClass = Config::get('remote_content_fetcher');
+      $fetcher = new $remoteFetcherClass();
+      $content = $fetcher->fetchRequest($request);
+      $responseObject = json_decode($content->getResponseContent(), true);
+      $this->handleErrorResponse($responseObject);
+      if (! isset($responseObject['access_token'])) {
+        throw new GadgetException("invalid access token response");  
+      }
+      
+      $accessor->accessToken = $responseObject['access_token'];
+    } catch (\Exception $e) {
+      // It's unfortunate the OAuth libraries throw a generic Exception.
+      throw new GadgetException("INTERNAL SERVER ERROR: " . $e);
+    }
+  }
+  
+  /**
+   *
+   * @param array $parameters 
+   */
+  protected function handleErrorResponse(array $parameters) {
+    if (isset($parameters['error'])) {
+      throw new GadgetException('Received OAuth error ' . $parameters['error'] . 
+              (isset($parameters['error_description']) ? ' ' . $parameters['error_description'] : '') .
+              (isset($parameters['error_uri']) ? ' see: ' . $parameters['error_uri'] : ''));
+    }
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthAccessor.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthAccessor.php
new file mode 100644
index 0000000..f925be2
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthAccessor.php
@@ -0,0 +1,91 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\common\ShindigOAuthRequest;
+use apache\shindig\common\ShindigOAuth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class OAuthAccessor {
+  public $consumer;
+  public $requestToken;
+  public $accessToken;
+  public $tokenSecret;
+  private $properties = array();
+
+  /**
+   *
+   * @param OAuthConsumer $consumer 
+   */
+  public function __construct(\OAuthConsumer $consumer) {
+    $this->consumer = $consumer;
+    $this->requestToken = null;
+    $this->accessToken = null;
+    $this->tokenSecret = null;
+  }
+
+  /**
+   *
+   * @param string $name
+   * @return string
+   */
+  public function getProperty($name) {
+    return $this->properties[$name];
+  }
+
+  /**
+   *
+   * @param stirng $name
+   * @param string $value
+   */
+  public function setProperty($name, $value) {
+    $this->properties[$name] = $value;
+  }
+
+  /**
+   * @param string $method
+   * @param string $url
+   * @param string $parameters
+   * @return ShindigOAuthRequest
+   */
+  public function newRequestMessage($method, $url, $parameters) {
+    if (! isset($method)) {
+      $method = $this->getProperty("httpMethod");
+      if ($method == null) {
+        $method = $this->consumer->getProperty("httpMethod");
+        if ($method == null) {
+          $method = "GET";
+        }
+      }
+    }
+    $token = new \OAuthToken($this->accessToken, $this->tokenSecret);
+    $message = ShindigOAuthRequest::from_consumer_and_token($this->consumer, $token, $method, $url, $parameters);
+    $signatureMethod = null;
+    if ($parameters[ShindigOAuth::$OAUTH_SIGNATURE_METHOD] == ShindigOAuth::$RSA_SHA1) {
+      $signatureMethod = new \OAuthSignatureMethod_RSA_SHA1();
+    } else if ($parameters[ShindigOAuth::$OAUTH_SIGNATURE_METHOD] == ShindigOAuth::$HMAC_SHA1) {
+      $signatureMethod = new \OAuthSignatureMethod_HMAC_SHA1();
+    } else { //PLAINTEXT
+      $signatureMethod = new \OAuthSignatureMethod_PLAINTEXT();
+    }
+    $message->sign_request($signatureMethod, $this->consumer, $token);
+    return $message;
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthCallbackState.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthCallbackState.php
new file mode 100644
index 0000000..c251dc5
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthCallbackState.php
@@ -0,0 +1,90 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\common\BlobCrypterException;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Handles state passed on the OAuth callback URL.
+ */
+class OAuthCallbackState {
+  public  static $CALLBACK_STATE_MAX_AGE_SECS = 600;
+  private static $REAL_CALLBACK_URL_KEY = "u";
+
+  /**
+   *
+   * @var BlobCrypter
+   */
+  private $crypter;
+
+  /**
+   *
+   * @var array
+   */
+  private $state = array();
+
+  /**
+   *
+   * @param BlobCrypter $crypter
+   * @param string $stateBlob
+   */
+  public function __construct($crypter, $stateBlob = null) {
+    $this->crypter = $crypter;
+    if ($stateBlob != null) {
+      try {
+        $state = $crypter->unwrap($stateBlob, self::$CALLBACK_STATE_MAX_AGE_SECS);
+      } catch (BlobCrypterException $e) {
+        // Probably too old, pretend we never saw it at all.
+      }
+      if ($state != null) {
+        $this->state = $state;
+      }
+    }
+    return;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getEncryptedState() {
+    return $this->crypter->wrap($this->state);
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getRealCallbackUrl() {
+    if (isset($this->state[self::$REAL_CALLBACK_URL_KEY])) {
+      return $this->state[self::$REAL_CALLBACK_URL_KEY];
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   *
+   * @param string $callbackUrl
+   */
+  public function setRealCallbackUrl($callbackUrl) {
+    $this->state[self::$REAL_CALLBACK_URL_KEY] = $callbackUrl;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthError.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthError.php
new file mode 100644
index 0000000..2ce7ae4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthError.php
@@ -0,0 +1,37 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Error strings to be returned to gadgets as "oauthError" data.
+ */
+class OAuthError {
+  /**
+   * The request cannot be completed because the OAuth configuration for
+   * the gadget is incorrect.
+   */
+  public static $BAD_OAUTH_CONFIGURATION = "BAD_OAUTH_CONFIGURATION";
+  
+  /**
+   * The request cannot be completed for an unspecified reason.
+   */
+  public static $UNKNOWN_PROBLEM = "UNKNOWN_PROBLEM";
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthFetcher.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthFetcher.php
new file mode 100644
index 0000000..038e869
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthFetcher.php
@@ -0,0 +1,792 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\gadgets\GadgetException;
+use apache\shindig\common\ShindigOAuthProtocolException;
+use apache\shindig\common\RemoteContentFetcher;
+use apache\shindig\common\ShindigOAuth;
+use apache\shindig\common\ShindigOAuthUtil;
+use apache\shindig\common\Config;
+use apache\shindig\common\ShindigOAuthRequest;
+use apache\shindig\common\BlobCrypterException;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+// For TokenInfo
+/**
+ * implements \the OAuth dance (http://oauth.net/core/1.0/) for gadgets.
+ *
+ * Reading the example in the appendix to the OAuth spec will be helpful to
+ * those reading this code.
+ *
+ * This class is not thread-safe; create a new one for each request that
+ * requires OAuth signing.
+ */
+class OAuthFetcher extends RemoteContentFetcher {
+
+  // We store some blobs of data on the client for later reuse; the blobs
+  // contain key/value pairs, and these are the key names.
+  protected static $REQ_TOKEN_KEY = "r";
+  protected static $REQ_TOKEN_SECRET_KEY = "rs";
+  protected static $ACCESS_TOKEN_KEY = "a";
+  protected static $ACCESS_TOKEN_SECRET_KEY = "as";
+  protected static $OWNER_KEY = "o";
+
+  // names for the JSON values we return to the client
+  public static $CLIENT_STATE = "oauthState";
+  public static $APPROVAL_URL = "oauthApprovalUrl";
+  public static $ERROR_CODE = "oauthError";
+  public static $ERROR_TEXT = "oauthErrorText";
+  // names of additional OAuth parameters we include in outgoing requests
+  public static $XOAUTH_APP_URL = "xoauth_app_url";
+  public static $OAUTH_CALLBACK = "oauth_callback";
+
+  /**
+   * @var RemoteContentFetcher
+   */
+  protected $fetcher;
+
+  /**
+   * Maximum age for our client state; if this is exceeded we start over. One
+   * hour is a fairly arbitrary time limit here.
+   */
+  protected static $CLIENT_STATE_MAX_AGE_SECS = 3600;
+
+  /**
+   * The gadget security token, with info about owner/viewer/gadget.
+   */
+  protected $authToken;
+
+  /**
+   * Parameters from makeRequest
+   * @var OAuthRequestParams
+   */
+  protected $requestParams;
+
+  /**
+   * Reference to our persistent store for OAuth metadata.
+   */
+  protected $tokenStore;
+
+  /**
+   * The accessor we use for signing messages. This also holds metadata about
+   * the service provider, such as their URLs and the keys we use to access
+   * those URLs.
+   * @var AccesorInfo
+   */
+  protected $accessorInfo;
+
+  /**
+   * We use this to encrypt and sign the state we cache on the client.
+   */
+  protected $oauthCrypter;
+
+  /**
+   * State the client sent with their request.
+   */
+  protected $origClientState = array();
+
+  /**
+   * The request the client really wants to make.
+   * @var RemoteContentRequest
+   */
+  protected $realRequest;
+
+  /**
+   * State to cache on the client.
+   */
+  protected $newClientState;
+
+  /**
+   * Authorization URL for the client
+   */
+  protected $aznUrl;
+
+  /**
+   * Error code for the client
+   */
+  protected $error;
+
+  /**
+   * Error text for the client
+   */
+  protected $errorText;
+
+  /**
+   * Whether or not we're supposed to ignore the spec cache when referring
+   * to the gadget spec for information (e.g. OAuth URLs).
+   */
+  protected $bypassSpecCache;
+
+  protected $responseMetadata = array();
+
+  /**
+   *
+   * @param $tokenStore storage for long lived tokens.
+   * @param $oauthCrypter used to encrypt transient information we store on the
+   *        client.
+   * @param RemoteContentFetcher $fetcher
+   * @param $authToken user's gadget security token
+   * @param OAuthRequestParams $params OAuth fetch parameters sent from makeRequest
+   */
+  public function __construct($tokenStore, $oauthCrypter, $fetcher, $authToken, OAuthRequestParams $params) {
+    $this->fetcher = $fetcher;
+    $this->oauthCrypter = $oauthCrypter;
+    $this->authToken = $authToken;
+    $this->bypassSpecCache = $params->getBypassSpecCache();
+    $this->requestParams = $params;
+    $this->newClientState = null;
+    $this->aznUrl = null;
+    $this->error = null;
+    $this->errorText = null;
+    $origClientState = $params->getOrigClientState();
+    if ($origClientState != null && strlen($origClientState) > 0) {
+      try {
+        $this->origClientState = $this->oauthCrypter->unwrap($origClientState, self::$CLIENT_STATE_MAX_AGE_SECS);
+      } catch (BlobCrypterException $e) {  // Probably too old, pretend we never saw it at all.
+      }
+    }
+    if ($this->origClientState == null) {
+      $this->origClientState = array();
+    }
+    $this->tokenStore = $tokenStore;
+  }
+
+  /**
+   *
+   * @param Exception $e
+   * @return RemoteContentRequest
+   */
+  protected function buildErrorResponse(\Exception $e) {
+    if ($this->error == null) {
+      $this->error = OAuthError::$UNKNOWN_PROBLEM;
+    }
+    // Take a giant leap of faith and assume that the exception message
+    // will be useful to a gadget developer.  Also include the exception
+    // stack trace, in case the problem report makes it to someone who knows
+    // enough to do something useful with the stack.
+    $errorBuf = '';
+    $errorBuf .= $e->getMessage();
+    $errorBuf .= "\n\n";
+    $this->errorText = $errorBuf;
+    return $this->buildNonDataResponse();
+  }
+
+  /**
+   * @return RemoteContentRequest
+   */
+  protected function buildNonDataResponse() {
+    $response = new RemoteContentRequest($this->realRequest->getUrl());
+    $this->addResponseMetadata($response);
+    self::setStrictNoCache($response);
+    return $response;
+  }
+
+  /**
+   * Retrieves metadata from our persistent store.
+   *
+   * @throws GadgetException
+   */
+  protected function lookupOAuthMetadata() {
+    $tokenKey = $this->buildTokenKey();
+    $this->accessorInfo = $this->tokenStore->getOAuthAccessor($tokenKey, $this->bypassSpecCache);
+    // The persistent data store may be out of sync with reality; we trust
+    // the state we stored on the client to be accurate.
+    $accessor = $this->accessorInfo->getAccessor();
+    if (isset($this->origClientState[self::$REQ_TOKEN_KEY])) {
+      $accessor->requestToken = $this->origClientState[self::$REQ_TOKEN_KEY];
+      $accessor->tokenSecret = $this->origClientState[self::$REQ_TOKEN_SECRET_KEY];
+    } else if (isset($this->origClientState[self::$ACCESS_TOKEN_KEY])) {
+      $accessor->accessToken = $this->origClientState[self::$ACCESS_TOKEN_KEY];
+      $accessor->tokenSecret = $this->origClientState[self::$ACCESS_TOKEN_SECRET_KEY];
+    } else if ($accessor->accessToken == null && $this->requestParams->getRequestToken() != null) {
+      // We don't have an access token yet, but the client sent us a
+      // (hopefully) preapproved request token.
+      $accessor->requestToken = $this->requestParams->getRequestToken();
+      $accessor->tokenSecret = $this->requestParams->getRequestTokenSecret();
+    }
+  }
+
+  /**
+   *
+   * @return TokenKey
+   */
+  protected function buildTokenKey() {
+    $tokenKey = new TokenKey();
+    // need to URLDecode so when comparing with the ProviderKey it goes thought
+    $tokenKey->setGadgetUri(urldecode($this->authToken->getAppUrl()));
+    $tokenKey->setModuleId($this->authToken->getModuleId());
+    $tokenKey->setAppId($this->authToken->getAppId());
+    $tokenKey->setServiceName($this->requestParams->getServiceName());
+    $tokenKey->setTokenName($this->requestParams->getTokenName());
+    // We should always use the current viewer id as a token key. Using the owner id
+    // would mean, that a private access token (with possible write access to the api)
+    // could be accessable to other viewers that are visiting the gadget of another
+    // owner
+    $tokenKey->setUserId($this->authToken->getViewerId());
+    return $tokenKey;
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  public function fetch($request) {
+  	$this->realRequest = $request;
+    try {
+      $this->lookupOAuthMetadata();
+    } catch (\Exception $e) {
+      $this->error = OAuthError::$BAD_OAUTH_CONFIGURATION;
+      return $this->buildErrorResponse($e);
+    }
+    $response = $this->fetchRequest($request);
+    return $response;
+  }
+
+  /**
+   * @param RemoteContentRequest $request
+   * @return RemoteContentRequest
+   */
+  public function fetchRequest(RemoteContentRequest $request) {
+  	$this->realRequest = $request;
+    $this->checkCanApprove();
+    if ($this->needApproval()) {
+      // This is section 6.1 of the OAuth spec.
+      $this->fetchRequestToken($request);
+      // This is section 6.2 of the OAuth spec.
+      $this->buildClientApprovalState();
+      $this->buildAznUrl();
+      // break out of the content fetching chain, we need permission from
+      // the user to do this
+      return $this->buildOAuthApprovalResponse();
+    } elseif ($this->needAccessToken()) {
+      // This is section 6.3 of the OAuth spec
+      $this->exchangeRequestToken($request);
+      $this->saveAccessToken();
+      $this->buildClientAccessState();
+    }
+    return $this->fetchData();
+  }
+
+  /**
+   *
+   * @return RemoteContentRequest
+   */
+  protected function buildOAuthApprovalResponse() {
+    return $this->buildNonDataResponse();
+  }
+
+  /**
+   * Do we need to get the user's approval to access the data?
+   *
+   * @return boolean
+   */
+  protected function needApproval() {
+    if ($this->accessorInfo == NULL) {
+      return true;
+    } else {
+      return ($this->accessorInfo->getAccessor()->requestToken == null && $this->accessorInfo->getAccessor()->accessToken == null);
+    }
+  }
+
+  /**
+   * Make sure the user is authorized to approve access tokens.  At the moment
+   * we restrict this to page owner's viewing their own pages.
+   *
+   * @throws GadgetException
+   */
+  protected function checkCanApprove() {
+    $pageOwner = $this->authToken->getOwnerId();
+    $pageViewer = $this->authToken->getViewerId();
+    $stateOwner = @$this->origClientState[self::$OWNER_KEY];
+    if (! $pageOwner) {
+      throw new GadgetException('Unauthenticated');
+    }
+    if ($pageOwner != $pageViewer) {
+      throw new GadgetException("Only page owners can grant OAuth approval");
+    }
+    if ($stateOwner != null && $stateOwner != $pageOwner) {
+      throw new GadgetException("Client state belongs to a different person.");
+    }
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $request
+   * @throws GadgetException
+   */
+  protected function fetchRequestToken(RemoteContentRequest $request) {
+    try {
+      $accessor = $this->accessorInfo->getAccessor();
+      //TODO The implementations of oauth differs from the one in JAVA. Fix the type OAuthMessage
+      $url = $accessor->consumer->callback_url->requestTokenURL;
+      $msgParams = array();
+      self::addIdentityParams($msgParams, $request->getToken());
+      $callbackState = new OAuthCallbackState($this->oauthCrypter);
+      $callbackUrl = "http://" . getenv('HTTP_HOST') . "/gadgets/oauthcallback";
+      $callbackState->setRealCallbackUrl($callbackUrl);
+      $state = $callbackState->getEncryptedState();
+      $msgParams[self::$OAUTH_CALLBACK] = $callbackUrl . "?state=" . urlencode($state);
+      $request = $this->newRequestMessageParams($url->url, $msgParams);
+      $reply = $this->sendOAuthMessage($request);
+      $reply->requireParameters(array(ShindigOAuth::$OAUTH_TOKEN,
+          ShindigOAuth::$OAUTH_TOKEN_SECRET));
+      $accessor->requestToken = $reply->get_parameter(ShindigOAuth::$OAUTH_TOKEN);
+      $accessor->tokenSecret = $reply->get_parameter(ShindigOAuth::$OAUTH_TOKEN_SECRET);
+    } catch (\Exception $e) {
+      // It's unfortunate the OAuth libraries throw a generic Exception.
+      throw new GadgetException($e);
+    }
+  }
+
+  /**
+   * @param string $method
+   * @param string $url
+   * @param $params
+   * @return ShindigOAuthRequest
+   */
+  protected function newRequestMessageMethod($method, $url, $params) {
+    if (! isset($params)) {
+      throw new \Exception("params was null in " . "newRequestMessage " . "Use newRequesMessage if you don't have a params to pass");
+    }
+    switch ($this->accessorInfo->getSignatureType()) {
+      case ShindigOAuth::$RSA_SHA1:
+        $params[ShindigOAuth::$OAUTH_SIGNATURE_METHOD] = ShindigOAuth::$RSA_SHA1;
+        break;
+      case "PLAINTEXT":
+        $params[ShindigOAuth::$OAUTH_SIGNATURE_METHOD] = "PLAINTEXT";
+        break;
+      default:
+        $params[ShindigOAuth::$OAUTH_SIGNATURE_METHOD] = ShindigOAuth::$HMAC_SHA1;
+    }
+    $accessor = $this->accessorInfo->getAccessor();
+    return $accessor->newRequestMessage($method, $url, $params);
+  }
+
+  /*
+   * @deprecated (All outgoing messages must send additional params
+   * like XOAUTH_APP_URL, so use newRequestMessageParams instead)
+   *
+   * @param string $url
+   * @return ShindigOAuthRequest
+   */
+  protected function newRequestMessageUrlOnly($url) {
+    $params = array();
+    return $this->newRequestMessageParams($url, $params);
+  }
+
+  /**
+   * @param string $url
+   * @param string $params
+   * @return ShindigOAuthRequest
+   */
+  protected function newRequestMessageParams($url, $params) {
+    $method = "POST";
+    if ($this->accessorInfo->getHttpMethod() == OAuthStoreVars::$HttpMethod['GET']) {
+      $method = "GET";
+    }
+    return $this->newRequestMessageMethod($method, $url, $params);
+  }
+
+  /**
+   *
+   * @param string $url
+   * @param string $method
+   * @param array $params
+   * @return ShindigOAuthRequest
+   */
+  protected function newRequestMessage($url = null, $method = null, $params = null) {
+    if (isset($method) && isset($url) && isset($params)) {
+      return $this->newRequestMessageMethod($method, $url, $params);
+    } else if (isset($url) && isset($params)) {
+      return $this->newRequestMessageParams($url, $params);
+    } else if (isset($url)) {
+      return $this->newRequestMessageUrlOnly($url);
+    }
+  }
+
+  /**
+   *
+   * @param array $oauthParams
+   * @return string
+   */
+  protected function getAuthorizationHeader($oauthParams) {
+    $result = "OAuth ";
+    $first = true;
+    foreach ($oauthParams as $key => $val) {
+      if (! $first) {
+        $result .= ", ";
+      } else {
+        $first = false;
+      }
+      $result .= ShindigOAuthUtil::urlencode_rfc3986($key) . "=\"" . ShindigOAuthUtil::urlencode_rfc3986($val) . '"';
+    }
+    return $result;
+  }
+
+  /**
+   * @param array $oauthParams
+   * @param string $method
+   * @param string $url
+   * @param array $headers
+   * @param string $contentType
+   * @param string $postBody
+   * @param Options $options
+   * @return RemoteContentRequest
+   */
+  protected function createRemoteContentRequest($oauthParams, $method, $url, $headers, $contentType, $postBody, $options) {
+    $paramLocation = $this->accessorInfo->getParamLocation();
+    $newHeaders = array();
+    // paramLocation could be overriden by a run-time parameter to fetchRequest
+    switch ($paramLocation) {
+      case OAuthStoreVars::$OAuthParamLocation['AUTH_HEADER']:
+        if ($headers != null) {
+          $newHeaders = $headers;
+        }
+        $authHeader = array();
+        $authHeader = $this->getAuthorizationHeader($oauthParams);
+        $newHeaders["Authorization"] = $authHeader;
+        break;
+
+      case OAuthStoreVars::$OAuthParamLocation['POST_BODY']:
+        if (! ShindigOAuthUtil::isFormEncoded($contentType)) {
+          throw new GadgetException("Invalid param: OAuth param location can only " . "be post_body if post body if of type x-www-form-urlencoded");
+        }
+        if (! isset($postBody) || count($postBody) == 0) {
+          $postBody = ShindigOAuthUtil::getPostBodyString($oauthParams);
+        } else {
+          $postBody = $postBody . "&" . ShindigOAuthUtil::getPostBodyString($oauthParams);
+        }
+        break;
+
+      case OAuthStoreVars::$OAuthParamLocation['URI_QUERY']:
+        $url = ShindigOAuthUtil::addParameters($url, $oauthParams);
+        break;
+    }
+    $rcr = new RemoteContentRequest($url);
+    $rcr->createRemoteContentRequest($method, $url, $newHeaders, null, $options);
+    $rcr->setPostBody($postBody);
+    return $rcr;
+  }
+
+  /**
+   * Sends OAuth request token and access token messages.
+   *
+   * @param ShindigOAuthRequest $request
+   * @return ShindigOAuthRequest
+   */
+  protected function sendOAuthMessage(ShindigOAuthRequest $request) {
+    $rcr = $this->createRemoteContentRequest($this->filterOAuthParams($request), $request->get_normalized_http_method(), $request->get_url(), null, RemoteContentRequest::$DEFAULT_CONTENT_TYPE, null, RemoteContentRequest::getDefaultOptions());
+    $rcr->setToken($this->authToken);
+
+    $remoteFetcherClass = Config::get('remote_content_fetcher');
+    $fetcher = new $remoteFetcherClass();
+    $content = $fetcher->fetchRequest($rcr);
+    $reply = ShindigOAuthRequest::from_request();
+    $params = ShindigOAuthUtil::decodeForm($content->getResponseContent());
+    $reply->set_parameters($params);
+    return $reply;
+  }
+
+  /**
+   * Builds the data we'll cache on the client while we wait for approval.
+   *
+   * @throws GadgetException
+   */
+  protected function buildClientApprovalState() {
+    try {
+      $accessor = $this->accessorInfo->getAccessor();
+      $oauthState = array();
+      $oauthState[self::$REQ_TOKEN_KEY] = $accessor->requestToken;
+      $oauthState[self::$REQ_TOKEN_SECRET_KEY] = $accessor->tokenSecret;
+      $oauthState[self::$OWNER_KEY] = $this->authToken->getOwnerId();
+      $this->newClientState = $this->oauthCrypter->wrap($oauthState);
+    } catch (BlobCrypterException $e) {
+      throw new GadgetException("INTERNAL SERVER ERROR: " . $e);
+    }
+  }
+
+  /**
+   * Builds the URL the client needs to visit to approve access.
+   */
+  protected function buildAznUrl() {
+    // At some point we can be clever and use a callback URL to improve
+    // the user experience, but that's too complex for now.
+    $accessor = $this->accessorInfo->getAccessor();
+    $azn = $accessor->consumer->callback_url->userAuthorizationURL;
+    $authUrl = $azn->url;
+    if (strstr($authUrl, "?") == FALSE) {
+      $authUrl .= "?";
+    } else {
+      $authUrl .= "&";
+    }
+    $authUrl .= ShindigOAuth::$OAUTH_TOKEN;
+    $authUrl .= "=";
+    $authUrl .= ShindigOAuthUtil::urlencode_rfc3986($accessor->requestToken);
+    $this->aznUrl = $authUrl;
+  }
+
+  /**
+   * Do we need to exchange a request token for an access token?
+   *
+   * @return boolean
+   */
+  protected function needAccessToken() {
+    return ($this->accessorInfo->getAccessor()->requestToken != null && $this->accessorInfo->getAccessor()->accessToken == null);
+  }
+
+  /**
+   * implements \section 6.3 of the OAuth spec.
+   *
+   * @param RemoteContentRequest $request
+   * @throws GadgetException
+   */
+  protected function exchangeRequestToken(RemoteContentRequest $request) {
+    try {
+      $accessor = $this->accessorInfo->getAccessor();
+      $url = $accessor->consumer->callback_url->accessTokenURL;
+      $msgParams = array();
+      $msgParams[ShindigOAuth::$OAUTH_TOKEN] = $accessor->requestToken;
+      self::addIdentityParams($msgParams, $request->getToken());
+      $callbackUrl = $this->requestParams->getReceivedCallback();
+      if (strlen($callbackUrl) > 0) {
+        $parsed_url = parse_url($callbackUrl);
+        parse_str($parsed_url["query"], $url_params);
+        if (strlen($url_params["oauth_token"]) > 0 &&
+            strlen($url_params["oauth_verifier"]) > 0 &&
+            $url_params["oauth_token"] == $accessor->requestToken) {
+          $msgParams[ShindigOAuth::$OAUTH_VERIFIER] = $url_params["oauth_verifier"];
+        } else {
+          throw new GadgetException("Invalid received callback URL: ".$callbackUrl);
+        }
+      }
+      $request = $this->newRequestMessageParams($url->url, $msgParams);
+      $reply = $this->sendOAuthMessage($request);
+      $reply->requireParameters(array(ShindigOAuth::$OAUTH_TOKEN,
+          ShindigOAuth::$OAUTH_TOKEN_SECRET));
+      $accessor->accessToken = $reply->get_parameter(ShindigOAuth::$OAUTH_TOKEN);
+      $accessor->tokenSecret = $reply->get_parameter(ShindigOAuth::$OAUTH_TOKEN_SECRET);
+    } catch (\Exception $e) {
+      // It's unfortunate the OAuth libraries throw a generic Exception.
+      throw new GadgetException("INTERNAL SERVER ERROR: " . $e);
+    }
+  }
+
+  /**
+   * Save off our new token and secret to the persistent store.
+   *
+   * @throws GadgetException
+   */
+  protected function saveAccessToken() {
+    $accessor = $this->accessorInfo->getAccessor();
+    $tokenKey = $this->buildTokenKey();
+    $tokenInfo = new TokenInfo($accessor->accessToken, $accessor->tokenSecret);
+    $this->tokenStore->storeTokenKeyAndSecret($tokenKey, $tokenInfo);
+  }
+
+  /**
+   * Builds the data we'll cache on the client while we make requests.
+   *
+   * @throws GadgetException
+   */
+  protected function buildClientAccessState() {
+    try {
+      $oauthState = array();
+      $accessor = $this->accessorInfo->getAccessor();
+      $oauthState[self::$ACCESS_TOKEN_KEY] = $accessor->accessToken;
+      $oauthState[self::$ACCESS_TOKEN_SECRET_KEY] = $accessor->tokenSecret;
+      $oauthState[self::$OWNER_KEY] = $this->authToken->getOwnerId();
+      $this->newClientState = $this->oauthCrypter->wrap($oauthState);
+    } catch (BlobCrypterException $e) {
+      throw new GadgetException("INTERNAL SERVER ERROR: " . $e);
+    }
+  }
+
+  /**
+   * Get honest-to-goodness user data.
+   *
+   * @return RemoteContentRequest
+   */
+  protected function fetchData() {
+    try {
+      // TODO: it'd be better using $this->realRequest->getContentType(), but not set before hand. Temporary hack.
+      $postBody = $this->realRequest->getPostBody();
+      $url = $this->realRequest->getUrl();
+      $msgParams = array();
+      if (ShindigOAuthUtil::isFormEncoded($this->realRequest->getHeader("Content-Type")) && strlen($postBody) > 0) {
+        $entries = explode('&', $postBody);
+        foreach ($entries as $entry) {
+          $parts = explode('=', $entry);
+          if (count($parts) == 2) {
+            $msgParams[ShindigOAuthUtil::urldecode_rfc3986($parts[0])] = ShindigOAuthUtil::urldecode_rfc3986($parts[1]);
+          }
+        }
+      }
+      $method = $this->realRequest->getMethod();
+      $msgParams[self::$XOAUTH_APP_URL] = $this->authToken->getAppUrl();
+      // Build and sign the message.
+      $oauthRequest = $this->newRequestMessageMethod($method, $url, $msgParams);
+      $oauthParams = $this->filterOAuthParams($oauthRequest);
+      $newHeaders = array();
+      switch ($method) {
+        case 'POST' :
+          if (empty($postBody) || count($postBody) == 0) {
+            $postBody = ShindigOAuthUtil::getPostBodyString($oauthParams);
+          } else {
+            $postBody = $postBody . "&" . ShindigOAuthUtil::getPostBodyString($oauthParams);
+          }
+          // To avoid 417 Response from server, adding empty "Expect" header
+          $newHeaders['Expect'] = '';
+          break;
+        case 'GET' :
+          $url = ShindigOAuthUtil::addParameters($url, $oauthParams);
+          break;
+      }
+      // To choose HTTP method client requested, we don't use $this->createRemoteContentRequest() here.
+      $rcr = new RemoteContentRequest($url);
+      $rcr->createRemoteContentRequest($method, $url, $newHeaders, null, $this->realRequest->getOptions());
+      $rcr->setPostBody($postBody);
+      $remoteFetcherClass = Config::get('remote_content_fetcher');
+      $fetcher = new $remoteFetcherClass();
+      $content = $fetcher->fetchRequest($rcr);
+      $statusCode = $content->getHttpCode();
+      //TODO is there a better way to detect an SP error? For example: http://wiki.oauth.net/ProblemReporting
+      if ($statusCode == 401) {
+        $tokenKey = $this->buildTokenKey();
+        $this->tokenStore->removeTokenAndSecret($tokenKey);
+      } else if ($statusCode >= 400 && $statusCode < 500) {
+        $message = $this->parseAuthHeader(null, $content);
+        if ($message->get_parameter(ShindigOAuth::$OAUTH_PROBLEM) != null) {
+          throw new ShindigOAuthProtocolException($message);
+        }
+      }
+      // Track metadata on the response
+      $this->addResponseMetadata($content);
+      return $content;
+    } catch (\Exception $e) {
+      throw new GadgetException("INTERNAL SERVER ERROR: " . $e);
+    }
+  }
+
+  /**
+   * Parse OAuth WWW-Authenticate header and either add them to an existing
+   * message or create a new message.
+   *
+   * @param ShindigOAuthRequest $msg
+   * @param RemoteContentRequest $resp
+   * @return string the updated message.
+   */
+  protected function parseAuthHeader(ShindigOAuthRequest $msg = null, RemoteContentRequest $resp) {
+    if ($msg == null) {
+      $msg = ShindigOAuthRequest::from_request();
+    }
+    $authHeaders = $resp->getResponseHeader("WWW-Authenticate");
+    if ($authHeaders != null) {
+      $msg->set_parameters(ShindigOAuthUtil::decodeAuthorization($authHeaders));
+    }
+    return $msg;
+  }
+
+  /**
+   * Extracts only those parameters from an OAuthMessage that are OAuth-related.
+   * An OAuthMessage may hold a whole bunch of non-OAuth-related parameters
+   * because they were all needed for signing. But when constructing a request
+   * we need to be able to extract just the OAuth-related parameters because
+   * they, and only they, may have to be put into an Authorization: header or
+   * some such thing.
+   *
+   * @param string $message the OAuthMessage object, which holds non-OAuth parameters
+   * such as foo=bar (which may have been in the original URI query part, or
+   * perhaps in the POST body), as well as OAuth-related parameters (such as
+   * oauth_timestamp or oauth_signature).
+   *
+   * @return array a list that contains only the oauth_related parameters.
+   *
+   * @throws IOException
+   */
+  protected function filterOAuthParams($message) {
+    $result = array();
+    foreach ($message->get_parameters() as $key => $value) {
+      if (preg_match('/^(oauth|xoauth|opensocial)/', strtolower($key))) {
+        $result[$key] = $value;
+      }
+    }
+    return $result;
+  }
+
+  /**
+   *
+   * @return array
+   */
+  public function getResponseMetadata() {
+    return $this->responseMetadata;
+  }
+
+  /**
+   * @param RemoteContentRequest $response
+   */
+  public function addResponseMetadata(RemoteContentRequest $response) {
+    $response->setHttpCode(200);
+    if ($this->newClientState != null) {
+      $this->responseMetadata[self::$CLIENT_STATE] = $this->newClientState;
+      $response->setMetadata(self::$CLIENT_STATE, $this->newClientState);
+    }
+    if ($this->aznUrl != null) {
+      $this->responseMetadata[self::$APPROVAL_URL] = $this->aznUrl;
+      $response->setMetadata(self::$APPROVAL_URL, $this->aznUrl);
+    }
+    if ($this->error != null) {
+      $this->responseMetadata[self::$ERROR_CODE] = $this->error;
+      $response->setMetadata(self::$ERROR_CODE, $this->error);
+    }
+    if ($this->errorText != null) {
+      $this->responseMetadata[self::$ERROR_TEXT] = $this->errorText;
+      $response->setMetadata(self::$ERROR_TEXT, $this->errorText);
+    }
+  }
+
+  /**
+   * @param array $requests
+   */
+  public function multiFetchRequest(Array $requests) {// Do nothing
+  }
+
+  /**
+   * @param array $params
+   * @param SecurityToken $token
+   */
+  protected static function addIdentityParams(array & $params, SecurityToken $token) {
+    $params['opensocial_owner_id'] = $token->getOwnerId();
+    $params['opensocial_viewer_id'] = $token->getViewerId();
+    $params['opensocial_app_id'] = $token->getAppId();
+    $params['opensocial_app_url'] = $token->getAppUrl();
+  }
+
+  /**
+   *
+   * @param RemoteContentRequest $response
+   */
+  protected static function setStrictNoCache(RemoteContentRequest $response) {
+    $response->setResponseHeader('Pragma', 'no-cache');
+    $response->setResponseHeader('Cache-Control', 'no-cache');
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthFetcherFactory.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthFetcherFactory.php
new file mode 100644
index 0000000..b6972f8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthFetcherFactory.php
@@ -0,0 +1,114 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+use apache\shindig\common\sample\BasicBlobCrypter;
+use apache\shindig\gadgets\sample\BasicGadgetSpecFactory;
+use apache\shindig\common\Config;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\RemoteContentFetcher;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Produces OAuth content fetchers for input tokens.
+ */
+class OAuthFetcherFactory {
+
+  /**
+   * used to encrypt state stored on the client
+   * @var BlobCrypter
+   */
+  protected $oauthCrypter;
+
+  /**
+   * persistent storage for OAuth tokens
+   * @var OAuthStore
+   */
+  protected $tokenStore;
+
+  /**
+   *
+   * @param SigningFetcher $fetcher
+   * @param BlobCrypter $oauthCrypter
+   * @param OAuthStore $tokenStore
+   */
+  public function __construct($fetcher = null, $oauthCrypter = null, $tokenStore = null) {
+    if (isset($oauthCrypter) && isset($tokenStore)) {
+      $this->OAuthFetcherFactoryCreate($oauthCrypter, $tokenStore);
+    } elseif (isset($fetcher)) {
+      $this->OAuthFetcherFactoryInit($fetcher);
+    } else {
+      throw new \Exception('Wrong number of parameters in the OAuthFetcherFactory constuct');
+    }
+  }
+
+  /**
+   * Initialize the OAuth factory with a default implementation of
+   * BlobCrypter and consumer keys/secrets read from oauth.js
+   *
+   * @param SigningFetcher $fetcher
+   */
+  public function OAuthFetcherFactoryInit($fetcher) {
+    try {
+      $BBC = new BasicBlobCrypter();
+      $this->oauthCrypter = new BasicBlobCrypter(srand($BBC->MASTER_KEY_MIN_LEN));
+      $specFactory = new BasicGadgetSpecFactory();
+      $OAuthStore = Config::get('oauth_store');
+      $gadgetOAuthTokenStore = Config::get('gadget_oauth_token_store');
+      $basicStore = new $gadgetOAuthTokenStore(new $OAuthStore, $specFactory);
+      $basicStore->initFromConfigFile($fetcher);
+      $this->tokenStore = $basicStore;
+    } catch (\Exeption $e) {}
+  }
+
+  /**
+   * Creates an OAuthFetcherFactory based on prepared crypter and token store.
+   *
+   * @param $oauthCrypter used to wrap client side state
+   * @param OAuthStore $tokenStore used as interface to persistent token store.
+   */
+  protected function OAuthFetcherFactoryCreate($oauthCrypter, $tokenStore) {
+    $this->oauthCrypter = $oauthCrypter;
+    $this->tokenStore = $tokenStore;
+  }
+
+  /**
+   * Produces an OAuthFetcher that will sign requests and delegate actual
+   * network retrieval to the {@code fetcher}
+   *
+   * @param RemoteContentFetcher $fetcher The fetcher that will fetch real content
+   * @param SecurityToken $token The gadget token used to identity the user and gadget
+   * @param OAuthRequestParams $params The parsed parameters the gadget requested
+   * @param string $authType the oauth auth type to use, either "oauth" or "oauth2"
+   * @return OAuthFetcher
+   * @throws GadgetException
+   */
+  public function getOAuthFetcher(RemoteContentFetcher $fetcher, SecurityToken $token, OAuthRequestParams $params, $authType) {
+    switch ($authType) {
+      case RemoteContentRequest::$AUTH_OAUTH:
+    return new OAuthFetcher($this->tokenStore, $this->oauthCrypter, $fetcher, $token, $params);
+        break;
+      case RemoteContentRequest::$AUTH_OAUTH2:
+        return new OAuth2Fetcher($this->tokenStore, $this->oauthCrypter, $fetcher, $token, $params);
+        break;
+    }
+    throw new \Exception('invalid oauth authType ' . $authType);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthRequestParams.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthRequestParams.php
new file mode 100644
index 0000000..03958d1
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthRequestParams.php
@@ -0,0 +1,119 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Bundles information about a proxy request that requires OAuth
+ */
+class OAuthRequestParams {
+  public static $SERVICE_PARAM = "OAUTH_SERVICE_NAME";
+  public static $TOKEN_PARAM = "OAUTH_TOKEN_NAME";
+  public static $REQUEST_TOKEN_PARAM = "OAUTH_REQUEST_TOKEN";
+  public static $REQUEST_TOKEN_SECRET_PARAM = "OAUTH_REQUEST_TOKEN_SECRET";
+  public static $CLIENT_STATE_PARAM = "oauthState";
+  public static $RECEIVED_CALLBACK_PARAM = "OAUTH_RECEIVED_CALLBACK";
+  public static $BYPASS_SPEC_CACHE_PARAM = "bypassSpecCache";
+  protected $serviceName;
+  protected $tokenName;
+  protected $requestToken;
+  protected $requestTokenSecret;
+  protected $origClientState;
+  protected $receivedCallback;
+  protected $bypassSpecCache;
+
+  /**
+   *
+   * @param array $arguments
+   */
+  public function __construct(array $arguments) {
+    $this->serviceName = self::getParam($arguments, self::$SERVICE_PARAM, "");
+    $this->tokenName = self::getParam($arguments, self::$TOKEN_PARAM, "");
+    $this->requestToken = self::getParam($arguments, self::$REQUEST_TOKEN_PARAM, null);
+    $this->requestTokenSecret = self::getParam($arguments, self::$REQUEST_TOKEN_SECRET_PARAM, null);
+    $this->origClientState = self::getParam($arguments, self::$CLIENT_STATE_PARAM, null);
+    $this->receivedCallback = self::getParam($arguments, self::$RECEIVED_CALLBACK_PARAM, "");
+    $this->bypassSpecCache = '1' == self::getParam($arguments, self::$BYPASS_SPEC_CACHE_PARAM, null);
+  }
+
+  /**
+   *
+   * @param array $arguments
+   * @param string $name
+   * @param string $defaultValue
+   * @return array
+   */
+  private static function getParam(array $arguments, $name, $defaultValue) {
+    if (isset($arguments[$name])) {
+      return $arguments[$name];
+    } else {
+      return $defaultValue;
+    }
+  }
+
+  /**
+   * @return string
+   */
+  public function getBypassSpecCache() {
+    return $this->bypassSpecCache;
+  }
+
+  /**
+   * @return string
+   */
+  public function getRequestToken() {
+    return $this->requestToken;
+  }
+
+  /**
+   * @return string
+   */
+  public function getRequestTokenSecret() {
+    return $this->requestTokenSecret;
+  }
+
+  /**
+   * @return string
+   */
+  public function getServiceName() {
+    return $this->serviceName;
+  }
+
+  /**
+   * @return string
+   */
+  public function getTokenName() {
+    return $this->tokenName;
+  }
+
+  /**
+   * @return string
+   */
+  public function getOrigClientState() {
+    return $this->origClientState;
+  }
+
+  /**
+   * @return string
+   */
+  public function getReceivedCallback() {
+    return $this->receivedCallback;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthService.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthService.php
new file mode 100644
index 0000000..a5c0444
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthService.php
@@ -0,0 +1,153 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * The OAuth service located in the gadget xml inside ModulePrefs -> OAuth or ModulePrefs -> OAuth2.
+ *
+ * Since OAuth and OAuth2 implementation are similar we are using the same OAuthService for both implementations
+ * as well for now. The only difference is, that OAuth2 services don't need an request token endpoint
+ **/
+class OAuthService {
+
+  private static $URL_ATTR = "url";
+  private static $PARAM_LOCATION_ATTR = "param_location";
+  private static $METHOD_ATTR = "method";
+
+  /**
+   * @var string
+   */
+  private $name;
+
+  /**
+   * @var string EndPoint
+   */
+  private $requestUrl;
+
+  /**
+   * @var string EndPoint
+   */
+  private $authorizationUrl;
+
+  /**
+   * @var string EndPoint
+   */
+  private $accessUrl;
+
+  /**
+   *
+   * @param DOMElement $service
+   * @throws SpecParserException
+   */
+  public function __construct(\DOMElement $service) {
+    $this->name = (string)$service->getAttribute('name');
+    $elements = $service->getElementsByTagName('*');
+    foreach ($elements as $element) {
+      $type = $element->tagName;
+      if ($type == 'Request') {
+        if ($this->requestUrl) {
+          throw new SpecParserException("Multiple OAuth/Service/Request elements");
+        }
+        $this->requestUrl = $this->parseEndPoint($element);
+      } else if ($type == 'Authorization') {
+        if ($this->authorizationUrl) {
+          throw new SpecParserException("Multiple OAuth/Service/Authorization elements");
+        }
+        $this->authorizationUrl = $this->parseEndPoint($element);
+      } else if ($type == 'Access' || $type == 'Token') {
+        if ($this->accessUrl) {
+          throw new SpecParserException("Multiple OAuth/Service/Access elements");
+        }
+        $this->accessUrl = $this->parseEndPoint($element);
+      }
+    }
+    if ($this->accessUrl == null) {
+      throw new SpecParserException("/OAuth/Service/Access is required");
+    }
+    if ($this->authorizationUrl == null) {
+      throw new SpecParserException("/OAuth/Service/Authorization is required");
+    }
+    if ($this->requestUrl && $this->requestUrl->location != $this->accessUrl->location) {
+      throw new SpecParserException(
+          "Access@location must be identical to Request@location");
+    }
+    if ($this->requestUrl && $this->requestUrl->method != $this->accessUrl->method) {
+      throw new SpecParserException(
+          "Access@method must be identical to Request@method");
+    }
+    if ($this->requestUrl && $this->requestUrl->location == Location::$body &&
+        $this->requestUrl->method == Method::$GET) {
+      throw new SpecParserException("Incompatible parameter location, cannot" +
+          "use post-body with GET requests");
+    }
+  }
+
+  /**
+   *
+   * @param DOMElement $element
+   * @return EndPoint
+   */
+  private function parseEndPoint($element) {
+    $url = trim($element->getAttribute(OAuthService::$URL_ATTR));
+    if (empty($url)) {
+      throw new SpecParserException("Not an HTTP url");
+    }
+    $location = Location::$header;
+    $locationString = trim($element->getAttribute(OAuthService::$PARAM_LOCATION_ATTR));
+    if (! empty($locationString)) {
+      $location = $locationString;
+    }
+    $method = Method::$GET;
+    $methodString = trim($element->getAttribute(OAuthService::$METHOD_ATTR));
+    if (! empty($methodString)) {
+      $method = $methodString;
+    }
+    return new EndPoint($url, $method, $location);
+  }
+
+  /**
+   * @return string
+   */
+  public function getName() {
+    return $this->name;
+  }
+
+  /**
+   * @return string
+   */
+  public function getRequestUrl() {
+    return $this->requestUrl;
+  }
+
+  /**
+   * @return string
+   */
+  public function getAuthorizationUrl() {
+    return $this->authorizationUrl;
+  }
+
+  /**
+   * @return string
+   */
+  public function getAccessUrl() {
+    return $this->accessUrl;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthServiceProvider.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthServiceProvider.php
new file mode 100644
index 0000000..f6c2ee7
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthServiceProvider.php
@@ -0,0 +1,50 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class OAuthServiceProvider {
+  /**
+   * @var string
+   */
+  public $requestTokenURL;
+
+  /**
+   * @var string
+   */
+  public $userAuthorizationURL;
+
+  /**
+   * @var string
+   */
+  public $accessTokenURL;
+
+  /**
+   *
+   * @param string $requestTokenURL
+   * @param string $userAuthorizationURL
+   * @param string $accessTokenURL
+   */
+  public function __construct($requestTokenURL, $userAuthorizationURL, $accessTokenURL) {
+    $this->requestTokenURL = $requestTokenURL;
+    $this->userAuthorizationURL = $userAuthorizationURL;
+    $this->accessTokenURL = $accessTokenURL;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthSpec.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthSpec.php
new file mode 100644
index 0000000..c70937a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthSpec.php
@@ -0,0 +1,54 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * The OAuth preferences located in the gadget xml inside ModulePrefs.
+ * Now, it only has 1 property, but this class was designed to be reusable
+ **/
+class OAuthSpec {
+
+  /**
+   *
+   * @var array
+   */
+  private $oAuthServices = array();
+
+  /**
+   *
+   * @param array $services
+   */
+  public function __construct($services) {
+    foreach ($services as $service) {
+      $oauthService = new OAuthService($service);
+      $this->oAuthServices[$oauthService->getName()] = $oauthService;
+    }
+  }
+
+  /**
+   *
+   * @return array
+   */
+  public function getServices() {
+    return $this->oAuthServices;
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStore.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStore.php
new file mode 100644
index 0000000..d7d2f8d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStore.php
@@ -0,0 +1,56 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface OAuthStore {
+
+  public function setOAuthConsumerKeyAndSecret($providerKey, $keyAndSecret);
+
+  public function setTokenAndSecret($tokenKey, $tokenInfo);
+
+  public function removeTokenAndSecret($tokenKey);
+
+  /**
+   * Retrieve an OAuthAccessor that is ready to sign OAuthMessages for
+   * resource access.
+   * @param TokenKey $tokenKey a structure uniquely identifying the token: a userId,
+   *                 a gadgetId, a moduleId (in case there are more than one
+   *                 gadget of the same type on a page), a tokenName (which
+   *                 distinguishes this token from others that the same gadget
+   *                 might hold for the same service provider) and a serviceName
+   *                 (which is the same as the service name in the ProviderKey
+   *                 structure).
+   * @param ProviderInfo $provInfo provider information. The store combines information stored
+   *                 in the store (consumer key/secret, token, token secret,
+   *                 etc.) with the provider information (access URL, request
+   *                 URL etc.) passed in here to create an AccessorInfo object.
+   *                 If no information can be found in the
+   *                 store, it may use default keys that identify the container,
+   *                 as opposed to consumer keys and secrets that are specific
+   *                 to this gadget.
+   * @return an OAuthAccessor object than can be passed to an OAuthMessage.sign
+   *         method.
+   */
+  public function getOAuthAccessorTokenKey(TokenKey $tokenKey, ProviderInfo $provInfo);
+
+  public function getOAuthAccessorProviderKey(ProviderKey $providerKey, ProviderInfo $provInfo);
+
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStoreException.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStoreException.php
new file mode 100644
index 0000000..2e7a61c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStoreException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class OAuthStoreException extends GadgetException {
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStoreVars.php b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStoreVars.php
new file mode 100644
index 0000000..4c6768c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/OAuthStoreVars.php
@@ -0,0 +1,30 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class OAuthStoreVars {
+  public static $HttpMethod = array('GET' => 'GET', 'POST' => 'POST');
+  public static $SignatureType = array('HMAC_SHA1' => 'HMAC_SHA1', 'RSA_SHA1' => 'RSA_SHA1',
+      'PLAINTEXT' => 'PLAINTEXT');
+  public static $KeyType = array('HMAC_SYMMETRIC' => 'HMAC_SYMMETRIC', 'RSA_PRIVATE' => 'RSA_PRIVATE');
+  public static $OAuthParamLocation = array('AUTH_HEADER' => 'auth-header', 'POST_BODY' => 'post-body',
+      'URI_QUERY' => 'uri-query');
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/ProviderInfo.php b/trunk/php/src/apache/shindig/gadgets/oauth/ProviderInfo.php
new file mode 100644
index 0000000..29daa98
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/ProviderInfo.php
@@ -0,0 +1,73 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ProviderInfo {
+  private $provider;
+  private $httpMethod;
+  private $signatureType;
+  private $paramLocation;
+
+  // this can be null if we have not negotiated a consumer key and secret
+  // yet with the provider, or if we decided that we want to use a global
+  // public key
+  private $keyAndSecret;
+
+  public function getParamLocation() {
+    return $this->paramLocation;
+  }
+
+  public function setParamLocation($paramLocation) {
+    $this->paramLocation = $paramLocation;
+  }
+
+  public function getKeyAndSecret() {
+    return $this->keyAndSecret;
+  }
+
+  public function setKeyAndSecret($keyAndSecret) {
+    $this->keyAndSecret = $keyAndSecret;
+  }
+
+  public function getProvider() {
+    return $this->provider;
+  }
+
+  public function setProvider(OAuthServiceProvider $provider) {
+    $this->provider = $provider;
+  }
+
+  public function getHttpMethod() {
+    return $this->httpMethod;
+  }
+
+  public function setHttpMethod($httpMethod) {
+    $this->httpMethod = $httpMethod;
+  }
+
+  public function getSignatureType() {
+    return $this->signatureType;
+  }
+
+  public function setSignatureType($signatureType) {
+    $this->signatureType = $signatureType;
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/ProviderKey.php b/trunk/php/src/apache/shindig/gadgets/oauth/ProviderKey.php
new file mode 100644
index 0000000..66a637d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/ProviderKey.php
@@ -0,0 +1,42 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ProviderKey {
+  private $gadgetUri;
+  private $serviceName;
+
+  public function getGadgetUri() {
+    return $this->gadgetUri;
+  }
+
+  public function setGadgetUri($gadgetUri) {
+    $this->gadgetUri = $gadgetUri;
+  }
+
+  public function getServiceName() {
+    return $this->serviceName;
+  }
+
+  public function setServiceName($serviceName) {
+    $this->serviceName = $serviceName;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/SpecParserException.php b/trunk/php/src/apache/shindig/gadgets/oauth/SpecParserException.php
new file mode 100644
index 0000000..3c8058d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/SpecParserException.php
@@ -0,0 +1,26 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class SpecParserException extends \Exception
+{
+
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/TokenInfo.php b/trunk/php/src/apache/shindig/gadgets/oauth/TokenInfo.php
new file mode 100644
index 0000000..d45fd85
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/TokenInfo.php
@@ -0,0 +1,39 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class TokenInfo {
+  private $accessToken;
+  private $tokenSecret;
+
+  public function __construct($token, $secret) {
+    $this->accessToken = $token;
+    $this->tokenSecret = $secret;
+  }
+
+  public function getAccessToken() {
+    return $this->accessToken;
+  }
+
+  public function getTokenSecret() {
+    return $this->tokenSecret;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/oauth/TokenKey.php b/trunk/php/src/apache/shindig/gadgets/oauth/TokenKey.php
new file mode 100644
index 0000000..aa17fdc
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/oauth/TokenKey.php
@@ -0,0 +1,78 @@
+<?php
+namespace apache\shindig\gadgets\oauth;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class TokenKey {
+  private $userId;
+  private $gadgetUri;
+  private $moduleId;
+  private $tokenName;
+  private $serviceName;
+  private $appId;
+
+  public function getAppId() {
+    return $this->appId;
+  }
+
+  public function setAppId($appId) {
+    $this->appId = $appId;
+  }
+
+  public function getUserId() {
+    return $this->userId;
+  }
+
+  public function setUserId($userId) {
+    $this->userId = $userId;
+  }
+
+  public function getGadgetUri() {
+    return $this->gadgetUri;
+  }
+
+  public function setGadgetUri($gadgetUri) {
+    $this->gadgetUri = $gadgetUri;
+  }
+
+  public function getModuleId() {
+    return $this->moduleId;
+  }
+
+  public function setModuleId($moduleId) {
+    $this->moduleId = $moduleId;
+  }
+
+  public function getTokenName() {
+    return $this->tokenName;
+  }
+
+  public function setTokenName($tokenName) {
+    $this->tokenName = $tokenName;
+  }
+
+  public function getServiceName() {
+    return $this->serviceName;
+  }
+
+  public function setServiceName($serviceName) {
+    $this->serviceName = $serviceName;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/render/EmptyClass.php b/trunk/php/src/apache/shindig/gadgets/render/EmptyClass.php
new file mode 100644
index 0000000..e7d47e4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/render/EmptyClass.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\gadgets\render;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class EmptyClass {
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/render/GadgetBaseRenderer.php b/trunk/php/src/apache/shindig/gadgets/render/GadgetBaseRenderer.php
new file mode 100644
index 0000000..566fd5d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/render/GadgetBaseRenderer.php
@@ -0,0 +1,526 @@
+<?php
+namespace apache\shindig\gadgets\render;
+use apache\shindig\gadgets\templates\TemplateLibrary;
+use apache\shindig\gadgets\templates\ExpressionException;
+use apache\shindig\gadgets\templates\TemplateParser;
+use apache\shindig\gadgets\rewrite\GadgetRewriter;
+use apache\shindig\gadgets\templates\DataPipelining;
+use apache\shindig\common\XmlError;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\Gadget;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Base class which both the href and html renderer inherit from. This containers all the general
+ * functions to deal with rewriting, templates, script insertion, etc
+ */
+abstract class GadgetBaseRenderer extends GadgetRenderer {
+  /**
+   * @var Gadget
+   */
+  public $gadget;
+  /**
+   * @var array
+   */
+  public $dataContext = array();
+  /**
+   * @var array
+   */
+  public $unparsedTemplates = array();
+  /**
+   * @var array
+   */
+  public $dataInserts = array();
+
+  /**
+   * Sets the $this->gadget property, and populates Msg, UserPref and ViewParams dataContext
+   *
+   * @param Gadget $gadget
+   */
+  public function setGadget(Gadget $gadget) {
+    $this->gadget = $gadget;
+    $this->dataContext['UserPrefs'] = $this->dataContext['ViewParams'] = $this->dataContext['Msg'] = array();
+    if (isset($this->gadget->gadgetSpec->locales)) {
+      foreach ($this->gadget->gadgetSpec->locales as $key => $val) {
+        $this->dataContext['Msg'][$key] = $val;
+      }
+    }
+    if (isset($this->gadget->gadgetSpec->userPrefs)) {
+      foreach ($this->gadget->gadgetSpec->userPrefs as $pref) {
+        $this->dataContext['UserPrefs'][$pref['name']] = isset($pref['value']) ? $pref['value'] : '';
+      }
+    }
+    if (isset($_GET['view-params'])) {
+      $viewParams = json_decode($_GET['view-params'], true);
+      if ($viewParams != $_GET['view-params'] && $viewParams && is_array($viewParams)) {
+        foreach ($viewParams as $key => $val) {
+          $this->dataContext['ViewParams'][$key] = $val;
+        }
+      }
+    }
+  }
+
+  /**
+   * If some templates could not be parsed, we paste the back into the html document
+   * so javascript can take care of them
+   *
+   * @param string $content html to parse
+   * @return string
+   */
+  public function addTemplates($content) {
+    // If $this->gadget->gadgetSpec->templatesDisableAutoProcessing == true, unparsedTemplates will be empty, so the setting is ignored here
+    if (count($this->unparsedTemplates)) {
+      foreach ($this->unparsedTemplates as $key => $val) {
+        $content = str_replace("<template_$key></template_$key>", $val . "\n", $content);
+      }
+    }
+    return $content;
+  }
+
+  /**
+   * This function parses the os-template and os-data script tags.
+   * It's of vital importance to call this function *before* the rewriteContent function
+   * since the html/dom parser of the later breaks, mangles and otherwise destroys the
+   * os template/data script tags. So we need to expand the templates to pure html
+   * before we can proceeed to dom parse the resulting document
+   *
+   * @param string $content html to parse
+   * @return string
+   */
+  public function parseTemplates($content) {
+    $osTemplates = array();
+    $osDataRequests = array();
+    // First extract all the os-data tags, and execute those in a single combined request, saves latency
+    // and is consistent with other server implementations
+    preg_match_all('/(<script[^>]*type="text\/(os-data)"[^>]*>)(.*)(<\/script>)/imsxU', $content, $osDataRequests);
+    $osDataRequestsCombined = '';
+    foreach ($osDataRequests[0] as $match) {
+      $osDataRequestsCombined .= $match . "\n";
+      // Remove the reference from the html document
+      $content = str_replace($match, '', $content);
+    }
+    if (! empty($osDataRequestsCombined)) {
+      $this->performDataRequests($osDataRequestsCombined);
+    }
+    preg_match_all('/(<script[^>]*type="text\/(os-template)"[^>]*>)(.*)(<\/script>)/imxsU', $content, $osTemplates);
+    $templateLibrary = false;
+    if (count($osTemplates[0])) {
+      // only load the template parser if there's any templates in the gadget content
+      $templateLibrary = new TemplateLibrary($this->gadget->gadgetContext);
+      if ($this->gadget->gadgetSpec->templatesRequireLibraries) {
+        foreach ($this->gadget->gadgetSpec->templatesRequireLibraries as $library) {
+          $templateLibrary->addTemplateLibrary($library);
+        }
+      }
+      foreach ($osTemplates[0] as $match) {
+        if (! $this->gadget->gadgetSpec->templatesDisableAutoProcessing && ($renderedTemplate = $this->renderTemplate($match, $templateLibrary)) !== false) {
+          // Template was rendered, insert the rendered html into the document
+          $content = str_replace($match, $renderedTemplate, $content);
+        } else {
+          /*
+         * The template could not be rendered, this could happen because:
+         * - @require is present, and at least one of the required pieces of data is unavailable
+         * - @name is present
+         * - @autoUpdate == true
+         * - $this->gadget->gadgetSpec->templatesDisableAutoProcessing is set to true
+         * So set a magic marker (<template_$index>) that after the dom document parsing will be replaced with the original script content
+         */
+          $index = count($this->unparsedTemplates);
+          $this->unparsedTemplates[$index] = $match;
+          $content = str_replace($match, "<template_$index></template_$index>", $content);
+        }
+      }
+    }
+    return $content;
+  }
+
+  /**
+   * Parses the OpenSocial RPC format data reply into the local data context
+   *
+   * @param array $array the data to add to the context
+   */
+  public function addContextData($array) {
+    foreach ($array as $val) {
+      // we really only accept entries with a request id, otherwise it can't be referenced by context anyhow
+      if (isset($val['id'])) {
+        $key = $val['id'];
+        // Pick up only the actual data part of the response, so we can do direct variable resolution
+        if (isset($val['result']['list'])) {
+          $this->dataContext[$key] = $val['result']['list'];
+        } elseif (isset($val['result']['entry'])) {
+          $this->dataContext[$key] = $val['result']['entry'];
+        } elseif (isset($val['result'])) {
+          $this->dataContext[$key] = $val['result'];
+        } else {
+          $this->dataContext[$key] = $val;
+        }
+      }
+    }
+  }
+
+  /**
+   * Parses and performs the (combined) os-data requests
+   *
+   * @param string $osDataRequests
+   */
+  private function performDataRequests($osDataRequests) {
+    //TODO check with the java implementation guys if they do a caching strategy here (same as with data-pipelining),
+    // would result in a much higher render performance..
+    libxml_use_internal_errors(true);
+    $this->doc = new \DOMDocument(null, 'utf-8');
+    $this->doc->preserveWhiteSpace = true;
+    $this->doc->formatOutput = false;
+    $this->doc->strictErrorChecking = false;
+    $this->doc->recover = false;
+    $this->doc->resolveExternals = false;
+    if ($this->doc->loadXML($osDataRequests)) {
+      $dataPipeliningRequests = array();
+      // walk the one or multiple script tags, and build a combined request array
+      foreach ($this->doc->childNodes as $childNode) {
+        if ($childNode->tagName == 'script') {
+          $dataPipeliningRequests = array_merge($dataPipeliningRequests, DataPipelining::Parse($childNode));
+        }
+      }
+      // and perform the requests
+      if (count($dataPipeliningRequests)) {
+        $this->dataInserts = DataPipelining::fetch($dataPipeliningRequests, $this->context);
+        $this->addContextData($this->dataInserts);
+      }
+    } else {
+      echo "Error parsing os-data:\n" . XmlError::getErrors($osDataRequests);
+    }
+  }
+
+  /**
+   * Does the parsing of the template/data script content, then it hands
+   * the os-data parsing to the DataPipeling class, and os-template tags to
+   * the TemplateParser, and then returns the expanded template content (or '' on data)
+   *
+   * @param string $template
+   * @param TemplateLibrary $templateLibrary
+   * @return string
+   */
+  private function renderTemplate($template, TemplateLibrary $templateLibrary) {
+    libxml_use_internal_errors(true);
+    $this->doc = new \DOMDocument(null, 'utf-8');
+    $this->doc->preserveWhiteSpace = true;
+    $this->doc->formatOutput = false;
+    $this->doc->strictErrorChecking = false;
+    $this->doc->recover = false;
+    $this->doc->resolveExternals = false;
+    if (! $this->doc->loadXML($template)) {
+      return "Error parsing os-template:\n" . XmlError::getErrors($template);
+    }
+    if ($this->doc->childNodes->length < 1 || $this->doc->childNodes->length >> 1) {
+      return 'Invalid script block';
+    }
+    $childNode = $this->doc->childNodes->item(0);
+    if ($childNode->tagName == 'script' && $childNode->getAttribute('name') == null && $childNode->getAttribute('autoUpdate') != 'true') {
+      // If the require tag is set, check to see if we have all required data parts, and if not leave it to the client to render
+      if (($require = $childNode->getAttribute('require')) != null) {
+        $requires = explode(',', $require);
+        foreach ($requires as $val) {
+          $val = trim($val);
+          if (! isset($this->dataContext[$val])) {
+            return false;
+          }
+        }
+      }
+      // if $childNode->tag exists, add to global $templateLibraries array, else parse normally
+      $childNodeTag = $childNode->getAttribute('tag');
+      if (! empty($childNodeTag)) {
+        if (isset($this->templateLibraries[$childNode->getAttribute('tag')])) {
+          throw new ExpressionException("Template " . htmlentities($childNode->getAttribute('tag')) . " was already defined");
+        }
+        $templateLibrary->addTemplateByNode($childNode);
+      } else {
+        // Everything checked out, proceeding to render the template
+        $parser = new TemplateParser();
+        $parser->process($childNode, $this->dataContext, $templateLibrary);
+        // unwrap the output, ie we only want the script block's content and not the main <script></script> node
+        $output = new \DOMDocument(null, 'utf-8');
+        foreach ($childNode->childNodes as $node) {
+          $outNode = $output->importNode($node, true);
+          $outNode = $output->appendChild($outNode);
+        }
+        // Restore single tags to their html variant, and remove the xml header
+        $ret = str_replace(
+            array('<?xml version="" encoding="utf-8"?>', '<br/>', '<script type="text/javascript"><![CDATA[', ']]></script>'),
+            array('', '<br>', '<script type="text/javascript">', '</script>'),
+            $output->saveXML());
+        return $ret;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Rewrites the content, based on shindig's configuration (force_rewrite) and/or the gadget's
+   * spec params, it also injects the required html, css and javascript for the final gadget
+   * using the dom observer methods for the head and body
+   *
+   * @param unknown_type $content
+   * @return unknown
+   */
+  public function rewriteContent($content) {
+    // Rewrite the content, this will rewrite resource links to proxied versions (if requested), sanitize if configured, and
+    // add the various javascript tags to the document
+    $rewriter = new GadgetRewriter($this->context);
+    $rewriter->addObserver('head', $this, 'addHeadTags');
+    $rewriter->addObserver('body', $this, 'addBodyTags');
+    return $rewriter->rewrite($content, $this->gadget, true);
+  }
+
+  /**
+   * Generates the body script content
+   *
+   * @return string script
+   */
+  public function getBodyScript() {
+    return "gadgets.util.runOnLoadHandlers();";
+  }
+
+  /**
+   * Append the runOnLoadHandlers script to the gadget's document body
+   *
+   * @param DOMElement $node
+   * @param DOMDocument $doc
+   */
+  public function addBodyTags(\DOMElement &$node, \DOMDocument &$doc) {
+    $script = $this->getBodyScript();
+    $scriptNode = $doc->createElement('script');
+    $scriptNode->setAttribute('type', 'text/javascript');
+    $scriptNode->appendChild($doc->createTextNode($script));
+    $scriptNode->nodeValue = str_replace('&', '&amp;', $script);
+    $node->appendChild($scriptNode);
+  }
+
+  public function getJavaScripts() {
+    $registry = $this->context->getRegistry();
+    $forcedJsLibs = $this->getForcedJsLibs();
+    $forcedAppendJsLibs = Config::get('forcedAppendedJsLibs');
+    $externFeatures = $forcedJsLibs;
+    $inlineFeatures = array();
+    foreach ($this->gadget->features as $feature) {
+      if (! in_array($feature, $forcedJsLibs) &&
+          ! in_array($feature, $forcedAppendJsLibs)) {
+        $inlineFeatures[] = $feature;
+      }
+    }
+    $sortedExternFeatures = array();
+    $sortedInlineFeatures = array();
+    $registry->sortFeatures($externFeatures, $sortedExternFeatures);
+    $registry->sortFeatures($inlineFeatures, $sortedInlineFeatures);
+
+    // append additional js libs from config to the end of the javascript block
+    // this allows custom overloading of other javascript libraries
+    foreach ($forcedAppendJsLibs as $jsLib) {
+      $sortedInlineFeatures[] = $jsLib;
+    }
+
+    // if some of the feature libraries are externalized (through a browser cachable <script src="/gadgets/js/opensocial-0.9:settitle.js"> type url)
+    // we inject the tag and don't inline those libs (and their dependencies)
+    $scripts = array();
+    if (! empty($sortedExternFeatures)) {
+      $scripts[] = array(
+          'type' => 'extern',
+          'content' => Config::get('default_js_prefix') . $this->getJsUrl($sortedExternFeatures) . "&container=" . $this->context->getContainer()
+      );
+    }
+    $script = '';
+    foreach ($sortedInlineFeatures as $feature) {
+      $script .= $registry->getFeatureContent($feature, $this->context, true);
+    }
+    // Add the JavaScript initialization strings for the configuration, localization and preloads
+    $script .= "\n";
+    $script .= $this->appendJsConfig($this->gadget, $sortedExternFeatures, $sortedInlineFeatures);
+    $script .= $this->appendMessages($this->gadget);
+    $script .= $this->appendPreferences($this->gadget);
+    $script .= $this->appendPreloads($this->gadget);
+    if (count($this->dataInserts)) {
+      foreach ($this->dataInserts as $data) {
+        if (isset($data['result'])) {
+          $key = $data['id'];
+          $data = json_encode($data['result']);
+          $script .= "opensocial.data.DataContext.putDataSet(\"$key\", $data);\n";
+        }
+      }
+    }
+    if ($this->gadget->gadgetSpec->templatesRequireLibraries) {
+      foreach ($this->gadget->gadgetSpec->templatesRequireLibraries as $url => $library) {
+        $script .= "os.Loader.loadContent(" . json_encode($library) . ", '" . $url . "');\n";
+      }
+    }
+    if ($this->gadget->gadgetSpec->templatesDisableAutoProcessing) {
+      $script .= "opensocial.template.Container.disableAutoProcessing();\n";
+    }
+    $scripts[] = array(
+        'type' => 'inline',
+        'content' => $script
+    );
+    return $scripts;
+  }
+
+  /**
+   * Adds the various bits of javascript to the gadget's document head element
+   *
+   * @param DOMElement $node
+   * @param DOMDocument $doc
+   */
+  public function addHeadTags(\DOMElement &$node, \DOMDocument &$doc) {
+    // Inject our configured gadget document style
+    $styleNode = $doc->createElement('style');
+    $styleNode->setAttribute('type', 'text/css');
+    $styleNode->appendChild($doc->createTextNode(Config::get('gadget_css')));
+    $node->appendChild($styleNode);
+
+    // Inject the OpenSocial feature javascripts
+    $scripts = $this->getJavaScripts();
+    foreach ($scripts as $script) {
+      $scriptNode = $doc->createElement('script');
+      if ($script['type'] == 'inline') {
+        $scriptNode->setAttribute('type', 'text/javascript');
+        $scriptNode->appendChild($doc->createTextNode($script['content']));
+      } else {
+        $scriptNode->setAttribute('src', $script['content']);
+      }
+      $node->appendChild($scriptNode);
+    }
+  }
+
+  /**
+   * Retrieve the forced javascript libraries (if any), using either the &libs= from the query
+   * or if that's empty, from the config
+   *
+   * @return array contains the names of forced external javascript libs.
+   */
+  private function getForcedJsLibs() {
+    $forcedJsLibs = $this->context->getForcedJsLibs();
+    // allow the &libs=.. param to override our forced js libs configuration value
+    if (empty($forcedJsLibs)) {
+      $forcedJsLibs = Config::get('forcedJsLibs');
+    }
+    if (empty($forcedJsLibs)) {
+      return array();
+    } else {
+      $features = explode(':', $forcedJsLibs);
+      // expend features here
+      $resultsFound = array();
+      $resultsMissing = array();
+      $registry = $this->context->getRegistry();
+      $registry->resolveFeatures($features, $resultsFound, $resultsMissing);
+      return $resultsFound;
+    }
+  }
+
+  /**
+   * Appends the javascript features configuration string
+   *
+   * @param Gadget $gadget
+   * @param $externFeatures
+   * @param $inlineFeatures
+   * @return string
+   */
+  private function appendJsConfig(Gadget $gadget, $externFeatures, $inlineFeatures) {
+    $container = $this->context->getContainer();
+    $containerConfig = $this->context->getContainerConfig();
+    $gadgetConfig = array();
+    $featureConfig = $containerConfig->getConfig($container, 'gadgets.features');
+
+    // TODO some day we should parse the forcedLibs too, and include their config selectivly as well.
+    // For now we just include everything.
+    $features = array_merge($externFeatures, $inlineFeatures);
+    foreach ($features as $feature) {
+      if (! isset($gadgetConfig[$feature]) && ! empty($featureConfig[$feature])) {
+        $gadgetConfig[$feature] = $featureConfig[$feature];
+      }
+    }
+
+    // Add gadgets.util support. This is calculated dynamically based on request inputs.
+    // See java/org/apache/shindig/gadgets/render/RenderingContentRewriter.java for reference.
+    $requires = array();
+    foreach ($features as $feature) {
+      $requires[$feature] = new EmptyClass();
+    }
+    $gadgetConfig['core.util'] = $requires;
+
+    // following are some quick-fixes for osml and osapi.
+    if (isset($gadgetConfig['osml'])) {
+      unset($gadgetConfig['osml']);
+    }
+    if (! isset($gadgetConfig['osapi.services']) || count($gadgetConfig['osapi.services']) == 1) {
+      // this should really be set in config/container.js, but if not, we build a complete default set so at least most of it works out-of-the-box
+      $gadgetConfig['osapi.services'] = array(
+          'gadgets.rpc' => array('container.listMethods'),
+          'http://%host%/rpc' => array("messages.update", "albums.update",
+              "activities.delete", "activities.update",
+              "activities.supportedFields", "albums.get",
+              "activities.get", "mediaitems.update",
+              "messages.get", "appdata.get",
+              "system.listMethods", "people.supportedFields",
+              "messages.create", "mediaitems.delete",
+              "mediaitems.create", "people.get", "people.create",
+              "albums.delete", "messages.delete",
+              "appdata.update", "activities.create",
+              "mediaitems.get", "albums.create",
+              "appdata.delete", "people.update",
+              "appdata.create"),
+          'http://%host%/gadgets/api/rpc' => array('cache.invalidate',
+              'http.head', 'http.get', 'http.put',
+              'http.post', 'http.delete'));
+    }
+    $encodedConfig = json_encode($gadgetConfig);
+    $encodedConfig = str_replace('${CONTEXT_ROOT}', Config::get('web_prefix'), $encodedConfig);
+    return "gadgets.config.init(" . $encodedConfig . ");\n";
+  }
+
+  /**
+   * Injects the relevant translation message bundle into the javascript api
+   *
+   * @param Gadget $gadget
+   * @return string
+   */
+  private function appendMessages(Gadget $gadget) {
+    $msgs = json_encode($this->dataContext['Msg']);
+    return "gadgets.Prefs.setMessages_($msgs);\n";
+  }
+
+  /**
+   * Injects the pre-defined user preferences into the javascript api
+   *
+   * @param Gadget $gadget
+   * @return string
+   */
+  private function appendPreferences(Gadget $gadget) {
+    $prefs = json_encode($this->dataContext['UserPrefs']);
+    return "gadgets.Prefs.setDefaultPrefs_($prefs);\n";
+  }
+
+  /**
+   * Injects the preloaded content into the javascript api
+   *
+   * @param Gadget $gadget
+   * @return string
+   */
+  private function appendPreloads(Gadget $gadget) {
+    return "gadgets.io.preloaded_ = " . (count($gadget->gadgetSpec->preloads) ? json_encode($gadget->gadgetSpec->preloads) : "{}") . ";\n";
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/render/GadgetHrefRenderer.php b/trunk/php/src/apache/shindig/gadgets/render/GadgetHrefRenderer.php
new file mode 100644
index 0000000..58d109a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/render/GadgetHrefRenderer.php
@@ -0,0 +1,194 @@
+<?php
+namespace apache\shindig\gadgets\render;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\gadgets\SigningFetcherFactory;
+use apache\shindig\common\sample\BasicRemoteContent;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\templates\DataPipelining;
+use apache\shindig\gadgets\Gadget;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/*
+ * TODO Dynamically evaluate the limited EL subset expressions on the following tags:
+ * Any attribute on os:DataRequest other than @key and @method
+ * @userId
+ * @groupId
+ * @fields
+ * @startIndex
+ * @count
+ * @sortBy
+ * @sortOrder
+ * @filterBy
+ * @filterOp
+ * @filterValue
+ * @activityIds
+ * @href
+ * @params
+ * Example:
+ * <os:PeopleRequest key="PagedFriends" userId="@owner" groupId="@friends" startIndex="${ViewParams.first}" count="20"/>
+ * <os:HttpRequest href="http://developersite.com/api?ids=${PagedFriends.ids}"/>
+ */
+
+class GadgetHrefRenderer extends GadgetBaseRenderer {
+
+  /**
+   * Renders a 'proxied content' view, for reference see:
+   * http://opensocial-resources.googlecode.com/svn/spec/draft/OpenSocial-Data-Pipelining.xml
+   *
+   * @param Gadget $gadget
+   * @param array $view
+   */
+  public function renderGadget(Gadget $gadget, $view) {
+    $this->setGadget($gadget);
+    if (Config::get('P3P') != '') {
+      header("P3P: " . Config::get('P3P'));
+    }
+    /* TODO
+     * We should really re-add OAuth fetching support some day, uses these view atributes:
+     * $view['oauthServiceName'], $view['oauthTokenName'], $view['oauthRequestToken'], $view['oauthRequestTokenSecret'];
+    */
+    $authz = $this->getAuthz($view);
+    $refreshInterval = $this->getRefreshInterval($view);
+    $href = $this->buildHref($view, $authz, $gadget);
+    if (count($view['dataPipelining'])) {
+      $request = new RemoteContentRequest($href, "Content-type: application/json\n");
+      $request->setMethod('POST');
+      $request->getOptions()->ignoreCache = $gadget->gadgetContext->getIgnoreCache();
+    } else {
+      // no data-pipelining set, use GET and set cache/refresh interval options
+      $request = new RemoteContentRequest($href);
+      $request->setMethod('GET');
+      $request->setRefreshInterval($refreshInterval);
+      $request->getOptions()->ignoreCache = $gadget->gadgetContext->getIgnoreCache();
+    }
+    $signingFetcherFactory = $gadgetSigner = false;
+    if ($authz != 'none') {
+      $gadgetSigner = Config::get('security_token_signer');
+      $gadgetSigner = new $gadgetSigner();
+      $token = $gadget->gadgetContext->extractAndValidateToken($gadgetSigner);
+      $request->setToken($token);
+      $request->setAuthType($authz);
+      $request->getOptions()->ownerSigned = $this->getSignOwner($view);
+      $request->getOptions()->viewerSigned = $this->getSignViewer($view);
+      $signingFetcherFactory = new SigningFetcherFactory(Config::get("private_key_file"));
+    }
+    $remoteFetcherClass = Config::get('remote_content_fetcher');
+    $remoteFetcher = new $remoteFetcherClass();
+    $basicRemoteContent = new BasicRemoteContent($remoteFetcher, $signingFetcherFactory, $gadgetSigner);
+    // Cache POST's as if they were GET's, since we don't want to re-fetch and repost the social data for each view
+    $basicRemoteContent->setCachePostRequest(true);
+    if (($response = $basicRemoteContent->getCachedRequest($request)) == false) {
+      // Don't fetch the data-pipelining social data unless we don't have a cached version of the gadget's content
+      if (isset($view['dataPipelining']) && is_array($view['dataPipelining'])) {
+        foreach ($view['dataPipelining'] as &$pipeliningRequest) {
+          if (isset($pipeliningRequest['href'])) {
+            $pipeliningRequest['href'] = $gadget->substitutions->substituteUri(null, $pipeliningRequest['href']);
+          }
+        }
+        $dataPipeliningResults = DataPipelining::fetch($view['dataPipelining'], $this->context);
+        // spec stats that the proxied content data-pipelinging data is *not* available to templates (to avoid duplicate posting
+        // of the data to the gadget dev's server and once to js space), so we don't assign it to the data context, and just
+        // post the json encoded results to the remote url.
+        $request->setPostBody(json_encode($dataPipeliningResults));
+      }
+      $response = $basicRemoteContent->fetch($request);
+    }
+    if ($response->getHttpCode() != '200') {
+      // an error occured fetching the proxied content's gadget content
+      $content = '<html><body><h1>An error occured fetching the gadget content</h1><p>http error code: '.$response->getHttpCode().'</p><p>'.$response->getResponseContent().'</body></html>';
+    } else {
+      // fetched ok, build the response document and output it
+      $content = $gadget->substitutions->substitute($response->getResponseContent());
+      $content = $this->parseTemplates($content);
+      $content = $this->rewriteContent($content);
+      $content = $this->addTemplates($content);
+    }
+    echo $content;
+  }
+
+  /**
+   * Builds the outgoing URL by taking the href attribute of the view and appending
+   * the country, lang, and opensocial query params to it
+   *
+   * @param array $view
+   * @param SecurityToken $token
+   * @param Gadget $gadget
+   * @return string the url
+   */
+  private function buildHref($view, $authz, $gadget) {
+    $href = RemoteContentRequest::transformRelativeUrl($gadget->substitutions->substituteUri(null, $view['href']), $this->context->getUrl());
+    if (empty($href)) {
+      throw new \Exception("Invalid empty href in the gadget view");
+    } // add the required country and lang param to the URL
+    $lang = isset($_GET['lang']) ? $_GET['lang'] : 'en';
+    $country = isset($_GET['country']) ? $_GET['country'] : 'US';
+    $firstSeperator = strpos($href, '?') === false ? '?' : '&';
+    $href .= $firstSeperator . 'lang=' . urlencode($lang);
+    $href .= '&country=' . urlencode($country);
+    if ($authz != 'none') {
+      $href .= '&opensocial_proxied_content=1';
+    }
+    return $href;
+  }
+
+  /**
+   * Returns the requested refreshInterval (cache time) of the view, or if none is specified
+   * it will return the configured default_refresh_interval value
+   *
+   * @param array $view
+   * @return int refresh interval
+   */
+  private function getRefreshInterval($view) {
+    return ! empty($view['refreshInterval']) && is_numeric($view['refreshInterval']) ? $view['refreshInterval'] : Config::get('default_refresh_interval');
+  }
+
+  /**
+   * Returns the authz attribute of the view, can be 'none', 'signed' or 'oauth'
+   *
+   * @param array $view
+   * @return string authz attribute
+   */
+  private function getAuthz($view) {
+    return ! empty($view['authz']) ? strtolower($view['authz']) : 'none';
+  }
+
+
+  /**
+   * Returns the signOwner attribute of the view (true or false, default is true)
+   *
+   * @param array $view
+   * @return string signOwner attribute
+   */
+  private function getSignOwner($view) {
+    return ! empty($view['signOwner']) && strcasecmp($view['signOwner'], 'false') == 0 ? false : true;
+  }
+
+  /**
+   * Returns the signViewer attribute of the view (true or false, default is true)
+   *
+   * @param array $view
+   * @return string signViewer attribute
+   */
+  private function getSignViewer($view) {
+    return ! empty($view['signViewer']) && strcasecmp($view['signViewer'], 'false') == 0 ? false : true;
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/render/GadgetHtmlRenderer.php b/trunk/php/src/apache/shindig/gadgets/render/GadgetHtmlRenderer.php
new file mode 100644
index 0000000..88216b6
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/render/GadgetHtmlRenderer.php
@@ -0,0 +1,99 @@
+<?php
+namespace apache\shindig\gadgets\render;
+use apache\shindig\common\OpenSocialVersion;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\Gadget;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Renders a Gadget's Content type="html" view, inlining the content, feature javascript and javascript initialization
+ * into the gadget's content. Most of the logic is performed with in the shared GadgetBaseRender class
+ *
+ */
+class GadgetHtmlRenderer extends GadgetBaseRenderer {
+
+  /**
+   *
+   * @param Gadget $gadget
+   * @param array $view
+   */
+  public function renderGadget(Gadget $gadget, $view) {
+    $this->setGadget($gadget);
+    // Was a privacy policy header configured? if so set it
+    if (Config::get('P3P') != '') {
+      header("P3P: " . Config::get('P3P'));
+    }
+    $content = '';
+
+    // Set no doctype if quirks mode is requestet because of quirks or doctype attribute
+    if ((isset($view['quirks']) && $view['quirks']) || $gadget->useQuirksMode()) {
+    } else {
+      // Override & insert DocType if Gadget is written for OpenSocial 2.0 or greater,
+      // if quirksmode is not set
+      $version20 = new OpenSocialVersion('2.0.0');
+      if ($gadget->getDoctype()) {
+        $content .= "<!DOCTYPE " . $gadget->getDoctype() . "\n";
+      } else if ($gadget->getSpecificationVersion()->isEqualOrGreaterThan($version20)) {
+        $content .= "<!DOCTYPE HTML>\n";
+      } else { // prior to 2.0 the php version always set this doc type, when no quirks attribute was specified
+        $content .= "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n";
+      }
+    }
+
+    // Rewriting the gadget's content using the libxml library does impose some restrictions to the validity of the input html, so
+    // for the time being (until either gadgets are all fixed, or we find a more tolerant html parsing lib), we try to avoid it when we can
+    $domRewrite = false;
+    if (isset($gadget->gadgetSpec->rewrite) || Config::get('rewrite_by_default')) {
+      $domRewrite = true;
+    } elseif ((strpos($view['content'], 'text/os-data') !== false || strpos($view['content'], 'text/os-template') !== false) && ($gadget->gadgetSpec->templatesDisableAutoProcessing == false)) {
+      $domRewrite = true;
+    }
+    if (!$domRewrite) {
+      // Manually generate the html document using basic string concatinations instead of using our DOM based functions
+      $content .= "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n";
+      $content .= '<style>'.Config::get('gadget_css')."</style>\n";
+
+      $scripts = $this->getJavaScripts();
+      foreach ($scripts as $script) {
+        if ($script['type'] == 'inline') {
+          $content .= "<script type=\"text/javascript\">{$script['content']}</script>\n";
+        } else {
+          $content .= "<script type=\"text/javascript\" src=\"{$script['content']}\"></script>\n";
+        }
+      }
+
+      $content .= "</head>\n<body>\n";
+      $content .= $gadget->substitutions->substitute($view['content']);
+      $content .= '<script type="text/javascript">'.$this->getBodyScript()."</script>\n";
+      $content .= "\n</body>\n</html>\n";
+    } else {
+      // Use the (libxml2 based) DOM rewriter
+      $content .= "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/></head><body>\n";
+      // Append the content for the selected view
+      $content .= $gadget->substitutions->substitute($view['content']);
+      $content .= "\n</body>\n</html>";
+      $content = $this->parseTemplates($content);
+      $content = $this->rewriteContent($content);
+      $content = $this->addTemplates($content);
+    }
+    echo $content;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/render/GadgetRenderer.php b/trunk/php/src/apache/shindig/gadgets/render/GadgetRenderer.php
new file mode 100644
index 0000000..b21be6c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/render/GadgetRenderer.php
@@ -0,0 +1,87 @@
+<?php
+namespace apache\shindig\gadgets\render;
+use apache\shindig\common\Cache;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\Gadget;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * base class that all the rendering methods inherit from
+ *
+ */
+abstract class GadgetRenderer {
+  /**
+   * @var GadgetContext
+   */
+  protected $context;
+
+  /**
+   *
+   * @param GadgetContext $context
+   */
+  public function __construct(GadgetContext $context) {
+    $this->context = $context;
+  }
+
+  /**
+   * generates the library string (core:caja:etc.js) including a checksum of all the
+   * javascript content (?v=<md5 of js>) for cache busting
+   *
+   * @param array $features
+   * @return string the list of libraries in core:caja:etc.js?v=checksum> format
+   */
+  protected function getJsUrl($features) {
+    if (! is_array($features) || ! count($features)) {
+      return 'null';
+    }
+    $registry = $this->context->getRegistry();
+    // Given the JsServlet automatically expends the js library, we just need
+    // to include the "leaf" nodes of the features.
+    $ret = $features;
+    foreach ($features as $feature) {
+      $depFeatures = $registry->features[$feature]['deps'];
+      $ret = array_diff($ret, $depFeatures);
+    }
+    $ret = implode(':', $ret);
+    $cache = Cache::createCache(Config::get('feature_cache'), 'FeatureCache');
+    if (($md5 = $cache->get(md5('getJsUrlMD5'))) === false) {
+      $features = $registry->features;
+
+      // Build a version string from the md5() checksum of all included javascript
+      // to ensure the client always has the right version
+      $inlineJs = '';
+      foreach ($features as $feature => $content) {
+        $inlineJs .= $registry->getFeatureContent($feature, $this->context, true);
+      }
+      $md5 = md5($inlineJs);
+      $cache->set(md5('getJsUrlMD5'), $md5);
+    }
+    $ret .= ".js?v=" . $md5;
+    return $ret;
+  }
+
+  /**
+   * @param Gadget $gadget
+   * @param array $view
+   */
+  abstract function renderGadget(Gadget $gadget, $view);
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/render/GadgetUrlRenderer.php b/trunk/php/src/apache/shindig/gadgets/render/GadgetUrlRenderer.php
new file mode 100644
index 0000000..890ea3a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/render/GadgetUrlRenderer.php
@@ -0,0 +1,177 @@
+<?php
+namespace apache\shindig\gadgets\render;
+use apache\shindig\gadgets\SigningFetcherFactory;
+use apache\shindig\common\ShindigRsaSha1SignatureMethod;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\Gadget;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetUrlRenderer extends GadgetRenderer {
+
+  /**
+   * Renders an 'URL' type view (where the iframe is redirected to the specified url)
+   * This is more a legacy iGoogle support feature than something that should be actually
+   * used. Proxied content is the socially aware (and higher performance) version of this
+   * See GadgetHrefRenderer for it's implementation.
+   *
+   * @param Gadget $gadget
+   * @param Array $view
+   */
+  public function renderGadget(Gadget $gadget, $view) {
+    $redirURI = $this->getSubstitutedUrl($gadget, $view);
+    header('Location: ' . $redirURI);
+  }
+
+  /**
+   * retrieves url of content tag and substitutes it
+   *
+   * @param Gadget $gadget
+   * @param string $view
+   * @return string
+   */
+  public function getSubstitutedUrl(Gadget $gadget, $view) {
+    // Preserve existing query string parameters.
+    $redirURI = $view['href'];
+    $query = $this->getPrefsQueryString($gadget->gadgetSpec->userPrefs);
+
+    // deal with features
+    $registry = $this->context->getRegistry();
+    // since the URL mode doesn't actually have the gadget XML body, it can't inline
+    // the javascript content anyway - thus could us just ignore the 'forcedJsLibs' part.
+    $sortedFeatures = array();
+    $registry->sortFeatures($gadget->features, $sortedFeatures);
+
+    $query .= $this->appendLibsToQuery($sortedFeatures);
+    $query .= '&lang=' . urlencode(isset($_GET['lang']) ? $_GET['lang'] : 'en');
+    $query .= '&country=' . urlencode(isset($_GET['country']) ? $_GET['country'] : 'US');
+
+    $redirURI = $gadget->substitutions->substituteUri(null, $redirURI);
+    if (strpos($redirURI, '?') !== false) {
+      $redirURI = $redirURI . $query;
+    } elseif (substr($query, 0, 1) == '&') {
+      $redirURI = $redirURI . '?' . substr($query, 1);
+    } else {
+      $redirURI = $redirURI . '?' . $query;
+    }
+
+    $authz = $this->getAuthz($view);
+
+    if ($authz === 'signed') {
+      $gadgetSigner = Config::get('security_token_signer');
+      $gadgetSigner = new $gadgetSigner();
+      $token = $gadget->gadgetContext->extractAndValidateToken($gadgetSigner);
+
+      $signingFetcherFactory = new SigningFetcherFactory(Config::get("private_key_file"));
+
+      $redirURI .= '&xoauth_signature_publickey=' . urlencode($signingFetcherFactory->getKeyName());
+      $redirURI .= '&xoauth_public_key=' . urlencode($signingFetcherFactory->getKeyName());
+
+      if ($this->getSignOwner($view)) {
+        $redirURI .= '&opensocial_owner_id=' . urlencode($token->getOwnerId());
+      }
+      if ($this->getSignViewer($view)) {
+        $redirURI .= '&opensocial_viewer_id=' . urlencode($token->getViewerId());
+      }
+
+      $redirURI .= '&opensocial_app_url=' . urlencode($token->getAppUrl());
+      $redirURI .= '&opensocial_app_id=' . urlencode($token->getAppId());
+      $redirURI .= '&opensocial_instance_id=' . urlencode($token->getModuleId());
+
+      $consumer = new \OAuthConsumer(NULL, NULL, NULL);
+      $signatureMethod = new ShindigRsaSha1SignatureMethod($signingFetcherFactory->getPrivateKey(), null);
+      $req_req = \OAuthRequest::from_consumer_and_token($consumer, NULL, 'GET', $redirURI);
+      $req_req->sign_request($signatureMethod, $consumer, NULL);
+      $redirURI = $req_req->to_url();
+
+
+    }
+
+    return $redirURI;
+  }
+
+
+  /**
+   * Returns the requested libs (from getjsUrl) with the libs_param_name prepended
+   * ie: in libs=core:caja:etc.js format
+   *
+   * @param string $libs the libraries
+   * @param Gadget $gadget
+   * @return string the libs=... string to append to the redirection url
+   */
+  private function appendLibsToQuery($features) {
+    $ret = "&";
+    $ret .= Config::get('libs_param_name');
+    $ret .= "=";
+    $ret .= str_replace('?', '&', $this->getJsUrl($features));
+    return $ret;
+  }
+
+  /**
+   * Returns the user preferences in &up_<name>=<val> format
+   *
+   * @param array $libs array of features this gadget requires
+   * @param Gadget $gadget
+   * @return string the up_<name>=<val> string to use in the redirection url
+   */
+  private function getPrefsQueryString($prefs) {
+    $ret = '';
+    foreach ($prefs as $pref) {
+      $ret .= '&';
+      $ret .= Config::get('userpref_param_prefix');
+      $ret .= urlencode($pref['name']);
+      $ret .= '=';
+      $ret .= urlencode($pref['value']);
+    }
+    return $ret;
+  }
+
+  /**
+   * Returns the authz attribute of the view, can be 'none', 'signed' or 'oauth'
+   *
+   * @param array $view
+   * @return string authz attribute
+   */
+  private function getAuthz($view) {
+    return ! empty($view['authz']) ? strtolower($view['authz']) : 'none';
+  }
+
+
+  /**
+   * Returns the signOwner attribute of the view (true or false, default is true)
+   *
+   * @param array $view
+   * @return string signOwner attribute
+   */
+  private function getSignOwner($view) {
+    return ! empty($view['signOwner']) && strcasecmp($view['signOwner'], 'false') == 0 ? false : true;
+  }
+
+  /**
+   * Returns the signViewer attribute of the view (true or false, default is true)
+   *
+   * @param array $view
+   * @return string signViewer attribute
+   */
+  private function getSignViewer($view) {
+    return ! empty($view['signViewer']) && strcasecmp($view['signViewer'], 'false') == 0 ? false : true;
+  }
+}
+
diff --git a/trunk/php/src/apache/shindig/gadgets/rewrite/ContentRewriter.php b/trunk/php/src/apache/shindig/gadgets/rewrite/ContentRewriter.php
new file mode 100644
index 0000000..308812c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/rewrite/ContentRewriter.php
@@ -0,0 +1,195 @@
+<?php
+namespace apache\shindig\gadgets\rewrite;
+use apache\shindig\common\Config;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\gadgets\GadgetContext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * implements \the Content-Rewrite feature which rewrites all image, css and script
+ * links to their proxied versions, which can be quite a latency improvement, and
+ * save the gadget dev's server from melting down
+ *
+ */
+class ContentRewriter extends DomRewriter {
+  private $rewrite;
+  private $baseUrl;
+
+  /**
+   *
+   * @var array
+   */
+  private $defaultRewrite = array('include-url' => array('*'), 'exclude-url' => array(), 'refresh' => '86400');
+
+  /**
+   *
+   * @param GadgetContext $context
+   * @param Gadget $gadget
+   */
+  public function __construct(GadgetContext $context, Gadget &$gadget) {
+    parent::__construct($context, $gadget);
+    // if no rewrite params are set in the gadget but rewrite_by_default is on, use our default rules (rewrite all)
+    if (! isset($gadget->gadgetSpec->rewrite) && Config::get('rewrite_by_default')) {
+      $this->rewrite = $this->defaultRewrite;
+    } else {
+      $this->rewrite = $gadget->gadgetSpec->rewrite;
+    }
+    // the base url of the gadget is used for relative paths
+    $this->baseUrl = substr($this->context->getUrl(), 0, strrpos($this->context->getUrl(), '/') + 1);
+  }
+
+  /**
+   * Register our dom node observers
+   *
+   * @param GadgetRewriter $gadgetRewriter
+   */
+  public function register(GadgetRewriter &$gadgetRewriter) {
+    $gadgetRewriter->addObserver('img', $this, 'rewriteImage');
+    $gadgetRewriter->addObserver('style', $this, 'rewriteStyle');
+    $gadgetRewriter->addObserver('script', $this, 'rewriteScript');
+    $gadgetRewriter->addObserver('link', $this, 'rewriteStyleLink');
+  }
+
+  /**
+   * Produces the proxied version of a URL if it falls within the content-rewrite params and
+   * will append a refresh param to the proxied url based on the expires param, and the gadget
+   * url so that the proxy server knows to rewrite it's content or not
+   *
+   * @param string $url
+   * @return string
+   */
+  private function getProxyUrl($url) {
+    if (strpos(strtolower($url), 'http://') === false && strpos(strtolower($url), 'https://') === false) {
+      $url = $this->baseUrl . $url;
+    }
+    $url = Config::get('web_prefix') . '/gadgets/proxy?url=' . urlencode($url);
+    $url .= '&refresh=' . (isset($this->rewrite['expires']) && is_numeric($this->rewrite['expires']) ? $this->rewrite['expires'] : '3600');
+    $url .= '&gadget=' . urlencode($this->context->getUrl());
+    $url .= '&st=' . urlencode(BasicSecurityToken::getTokenStringFromRequest());
+    return $url;
+  }
+
+  /**
+   * Checks the URL against the include-url and exclude-url params
+   *
+   * @param string $url
+   * @param boolean
+   */
+  private function includedUrl($url) {
+    $included = $excluded = false;
+    if (isset($this->rewrite['include-url'])) {
+      foreach ($this->rewrite['include-url'] as $includeUrl) {
+        if ($includeUrl == '*' || $includeUrl == '.*' || strpos($url, $includeUrl) !== false) {
+          $included = true;
+          break;
+        }
+      }
+    }
+    if (isset($this->rewrite['exclude-url'])) {
+      foreach ($this->rewrite['exclude-url'] as $excludeUrl) {
+        if ($excludeUrl == '*' || $includeUrl == '.*' || strpos($url, $excludeUrl) !== false) {
+          $excluded = true;
+          break;
+        }
+      }
+    }
+    return ($included && ! $excluded);
+  }
+
+  /**
+   * Rewrites the src attribute of an img tag
+   *
+   * @param DOMElement $node
+   */
+  public function rewriteImage(\DOMElement &$node) {
+    if (($src = $node->getAttribute('src')) != null && $this->includedUrl($src)) {
+      $node->setAttribute('src', $this->getProxyUrl($src));
+    }
+  }
+
+  /**
+   * Uses rewriteCSS to find url(<url tag>) constructs and rewrite them to their
+   * proxied counterparts
+   *
+   * @param DOMElement $node
+   */
+  public function rewriteStyle(\DOMElement &$node) {
+    $node->nodeValue = $this->rewriteCSS($node->nodeValue);
+  }
+
+  /**
+   * Does the actual CSS rewriting, this is a seperate function so it can be called
+   * from the proxy handler too
+   *
+   * @param string $content
+   * @return string
+   */
+  public function rewriteCSS($content) {
+    $newVal = '';
+    // loop through the url elements in the content
+    while (($pos = strpos($content, 'url')) !== false) {
+      // output everything before this url tag
+      $newVal .= substr($content, 0, $pos + 3);
+      $content = substr($content, $pos + 3);
+      // low tech protection against miss-reading tags, if the open ( is to far away, this is probabbly a miss-read
+      if (($beginTag = strpos($content, '(')) < 4) {
+        $content = substr($content, $beginTag + 1);
+        $endTag = strpos($content, ')');
+        $tag = str_replace(array("'", "\""), '', trim(substr($content, 0, $endTag)));
+        // at this point $tag should be the actual url aka: http://example.org/bar/foo.gif
+        if ($this->includedUrl($tag)) {
+          $newVal .= "('" . $this->getProxyUrl($tag) . "')";
+        } else {
+          $newVal .= "('$tag')";
+        }
+        $content = substr($content, $endTag + 1);
+      }
+    }
+    // append what's left
+    $newVal .= $content;
+    return $newVal;
+  }
+
+  /**
+   * Rewrites <script src="http://example.org/foo.js" /> tags into their proxied versions
+   *
+   * @param DOMElement $node
+   */
+  public function rewriteScript(\DOMElement &$node) {
+    if (($src = $node->getAttribute('src')) != null && $this->includedUrl($src)) {
+      // make sure not to rewrite our forcedJsLibs src tag, else things break
+      if (strpos($src, '/gadgets/js') === false) {
+        $node->setAttribute('src', $this->getProxyUrl($src));
+      }
+    }
+  }
+
+  /**
+   * Rewrites <link href="http://example.org/foo.css" /> tags into their proxied versions
+   *
+   * @param DOMElement $node
+   */
+  public function rewriteStyleLink(\DOMElement &$node) {
+    if (($src = $node->getAttribute('href')) != null && $this->includedUrl($src)) {
+      $node->setAttribute('href', $this->getProxyUrl($src));
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/rewrite/DomRewriter.php b/trunk/php/src/apache/shindig/gadgets/rewrite/DomRewriter.php
new file mode 100644
index 0000000..1af6a2b
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/rewrite/DomRewriter.php
@@ -0,0 +1,59 @@
+<?php
+namespace apache\shindig\gadgets\rewrite;
+use apache\shindig\gadgets\GadgetContext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Abstract base class for DOM based rewriters. The GadgetRewriter will call the
+ *
+ *
+ */
+abstract class DomRewriter {
+  /**
+   *
+   * @var GadgetContext
+   */
+  protected $context;
+
+  /**
+   *
+   * @var Gadget
+   */
+  protected $gadget;
+
+  /**
+   *
+   * @param GadgetContext $context
+   * @param Gadget $gadget
+   */
+  public function __construct(GadgetContext $context, Gadget &$gadget) {
+    $this->context = $context;
+    $this->gadget = $gadget;
+  }
+
+  /**
+   * Function to register the element => function mappings with the GadgetRewriter.
+   * Always use lower case tag names when calling GadgetRewriter->observer
+   *
+   * @param GadgetRewriter $gadgetRewriter
+   */
+  abstract public function register(GadgetRewriter &$gadgetRewriter);
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/rewrite/GadgetRewriter.php b/trunk/php/src/apache/shindig/gadgets/rewrite/GadgetRewriter.php
new file mode 100644
index 0000000..b984be2
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/rewrite/GadgetRewriter.php
@@ -0,0 +1,163 @@
+<?php
+namespace apache\shindig\gadgets\rewrite;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\Gadget;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * The rewriter meta class, it checks the various gadget and configuration
+ * settings, and calls the appropiate classes for the registered dom element
+ * listeners
+ */
+class GadgetRewriter {
+  /**
+   *
+   * @var GadgetContext
+   */
+  private $context;
+  private $doc;
+
+  /**
+   *
+   * @var array
+   */
+  private $domObservers = array();
+
+  /**
+   *
+   * @param GadgetContext $context
+   */
+  public function __construct(GadgetContext $context) {
+    $this->context = $context;
+  }
+
+  /**
+   * Does the actual rewrite option scanning and performs the dom parsing
+   *
+   * @param string $content
+   * @param Gadget $gadget
+   * @param boolean $checkDocument
+   * @return string
+   */
+  public function rewrite($content, Gadget &$gadget, $checkDocument = false) {
+    // Check to see if the gadget requested rewriting, or if rewriting is forced in the configuration
+    if (is_array($gadget->gadgetSpec->rewrite) || Config::get('rewrite_by_default')) {
+      $contentRewriter = new ContentRewriter($this->context, $gadget);
+      $contentRewriter->register($this);
+    }
+    // Are we configured to sanitize certain views? (if so the config should be an array of view names to sanitize, iaw: array('profile', 'home'))
+    if (is_array(Config::get('sanitize_views'))) {
+      $sanitizeRewriter = new SanitizeRewriter($this->context, $gadget);
+      $sanitizeRewriter->register($this);
+    }
+    // no observers registered, return the original content, otherwise parse the DOM tree and call the observers
+    if (! count($this->domObservers)) {
+      return $content;
+    } else {
+      libxml_use_internal_errors(true);
+      $this->doc = new \DOMDocument(null, 'utf-8');
+      $this->doc->preserveWhiteSpace = true;
+      $this->doc->formatOutput = false;
+      $this->doc->strictErrorChecking = false;
+      $this->doc->recover = false;
+      $this->doc->resolveExternals = false;
+      if (! $this->doc->loadHtml($content)) {
+        //TODO parse and output libxml_get_errors();
+        libxml_clear_errors();
+        // parsing failed, return the unmodified content
+        return $content;
+      }
+
+      if ($checkDocument) {
+      	$this->checkDocument();
+      }
+
+      // find and parse all nodes in the dom document
+      $rootNodes = $this->doc->getElementsByTagName('*');
+      $this->parseNodes($rootNodes);
+      // DomDocument tries to make the document a valid html document, so added the html/body/head elements to it.. so lets strip them off before returning the content
+      $html = $this->doc->saveHTML();
+      // If the gadget specified the caja feature, cajole it
+      if (in_array('caja', $gadget->features)) {
+        //TODO : use the caja daemon to cajole the content (experimental patch is available and will be added soon)
+      }
+      return $html;
+    }
+  }
+
+  /**
+   * Proxied content documents do not always have a head tag which would cause the css & script injection
+   * to fail (since that node listeners would never be called). Do note that the html and body tags will
+   * already be automatically added by the DOMDocument->loadHtml function
+   */
+  private function checkDocument() {
+    foreach ($this->doc->childNodes as $node) {
+    	$htmlNode = false;
+    	if (isset($node->tagName) && strtolower($node->tagName) == 'html') {
+    		$htmlNode = $node;
+    		$hasHeadTag = false;
+    		foreach ($node->childNodes as $htmlChild) {
+    			if (isset($htmlChild->tagName) && $htmlChild->tagName == 'head') {
+    				$hasHeadTag = true;
+    				break;
+    			}
+    		}
+    		// If no <head> tag was found but we do have a <html> node, then add the <head> tag to it
+    		if (!$hasHeadTag && $htmlNode && $htmlNode->childNodes->length > 0) {
+    			$firstChild = $htmlNode->childNodes->item(0);
+          $htmlNode->insertBefore($this->doc->createElement('head'), $firstChild);
+    		}
+    	}
+    }
+  }
+
+  /**
+   * This function should be called from the DomRewriter implmentation class in the form of:
+   * addObserver('img', $this, 'rewriteImage')
+   *
+   * @param string $tag
+   * @param object instance $class
+   * @param string $function
+   */
+  public function addObserver($tag, $class, $function) {
+    // add the tag => function to call relationship to our $observers array
+    $this->domObservers[] = array('tag' => $tag, 'class' => $class, 'function' => $function);
+  }
+
+  /**
+   * Parses the DOMNodeList $nodes and calls the registered rewriting function on nodes
+   *
+   * @param DOMNodeList $nodes
+   */
+  private function parseNodes(\DOMNodeList &$nodes) {
+    foreach ($nodes as $node) {
+      $tagName = strtolower($node->tagName);
+      foreach ($this->domObservers as $observer) {
+        if ($observer['tag'] == $tagName) {
+          $class = $observer['class'];
+          $function = $observer['function'];
+          $class->$function($node, $this->doc);
+        }
+      }
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/rewrite/SanitizeRewriter.php b/trunk/php/src/apache/shindig/gadgets/rewrite/SanitizeRewriter.php
new file mode 100644
index 0000000..bcbac63
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/rewrite/SanitizeRewriter.php
@@ -0,0 +1,59 @@
+<?php
+namespace apache\shindig\gadgets\rewrite;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+
+/**
+ * Content sanitizer, removes any javascript from the view if this view is part of the
+ * sanitize_views array
+ */
+class SanitizeRewriter extends DomRewriter {
+
+  /**
+   * Register our dom node observers that will remove the javascript, but only
+   * if this view should be sanitized
+   *
+   * @param GadgetRewriter $gadgetRewriter
+   */
+  public function register(GadgetRewriter &$gadgetRewriter) {
+    $sanitizeViews = Config::get('sanitize_views');
+    // Only hook up our dom node observers if this view should be sanitized
+    if (in_array($this->context->getView(), $sanitizeViews)) {
+      $gadgetRewriter->addObserver('script', $this, 'rewriteScript');
+    }
+  }
+
+  /**
+   * This is a proof of concept / semi dummy content sanitizer
+   * that removes any javascript from the content block
+   *
+   * @param DOMElement $node
+   */
+  public function rewriteScript(\DOMElement &$node) {
+    if (!empty($node->nodeValue)) {
+      $node->nodeValue = '';
+    }
+    if ($node->getAttribute('src') != null) {
+      $node->setAttribute('src', '');
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/sample/BasicGadgetBlacklist.php b/trunk/php/src/apache/shindig/gadgets/sample/BasicGadgetBlacklist.php
new file mode 100644
index 0000000..ccf2f19
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/sample/BasicGadgetBlacklist.php
@@ -0,0 +1,64 @@
+<?php
+namespace apache\shindig\gadgets\sample;
+use apache\shindig\common\Config;
+use apache\shindig\common\File;
+use apache\shindig\gadgets\GadgetBlacklist;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Basic example blacklist class. This class takes a text file with regex
+ * rules against which URL's are tested.
+ * The default file location is {$base_path}/blacklist.txt
+ *
+ */
+class BasicGadgetBlacklist implements GadgetBlacklist {
+  /**
+   * @var array
+   */
+  private $rules = array();
+
+  /**
+   * @param string $file
+   */
+  public function __construct($file = false) {
+    if (! $file) {
+      $file = Config::get('base_path') . '/blacklist.txt';
+    }
+    if (File::exists($file)) {
+      $this->rules = explode("\n", @file_get_contents($file));
+    }
+  }
+
+  /**
+   * Check the URL against the blacklist rules
+   *
+   * @param string $url
+   * @return boolean is blacklisted or not?
+   */
+  function isBlacklisted($url) {
+    foreach ($this->rules as $rule) {
+      if (! empty($rule) && preg_match($rule, $url)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/sample/BasicGadgetSpecFactory.php b/trunk/php/src/apache/shindig/gadgets/sample/BasicGadgetSpecFactory.php
new file mode 100644
index 0000000..63567e8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/sample/BasicGadgetSpecFactory.php
@@ -0,0 +1,76 @@
+<?php
+namespace apache\shindig\gadgets\sample;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\sample\BasicRemoteContent;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\GadgetContext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Basic implementation of a gadget spec factory.
+ */
+class BasicGadgetSpecFactory {
+  /**
+   * @var GadgetContext
+   */
+  private $context;
+
+  /**
+   *
+   * @param GadgetContext $context
+   * @return GadgetSpec
+   */
+  public function getGadgetSpec(GadgetContext $context) {
+    $this->context = $context;
+    return $this->getGadgetSpecUri($context->getUrl(), $context->getIgnoreCache());
+  }
+
+  /**
+   * Retrieves a gadget specification from the cache or from the Internet.
+   *
+   * @param string $url
+   * @param boolean $ignoreCache
+   * @return GadgetSpec
+   */
+  public function getGadgetSpecUri($url, $ignoreCache) {
+    return $this->fetchFromWeb($url, $ignoreCache);
+  }
+
+  /**
+   * Retrieves a gadget specification from the Internet, processes its views and
+   * adds it to the cache.
+   *
+   * @param string $url
+   * @param boolean $ignoreCache
+   * @return GadgetSpec
+   */
+  private function fetchFromWeb($url, $ignoreCache) {
+    $remoteContentRequest = new RemoteContentRequest($url);
+    $remoteContentRequest->getOptions()->ignoreCache = $ignoreCache;
+    $remoteContent = new BasicRemoteContent();
+    $spec = $remoteContent->fetch($remoteContentRequest);
+
+    $gadgetSpecParserClass = Config::get('gadget_spec_parser');
+    $gadgetSpecParser = new $gadgetSpecParserClass();
+    $gadgetSpec = $gadgetSpecParser->parse($spec->getResponseContent(), $this->context);
+    return $gadgetSpec;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/CertServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/CertServlet.php
new file mode 100644
index 0000000..9c2bcb4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/CertServlet.php
@@ -0,0 +1,44 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * This class serves the public certificate, quick and dirty hack to make the certificate publicly accessible
+ * this combined with the hard coded location in SigningFetcherFactory.php : http://{host}/{prefix}/public.crt
+ * for the oauth pub key location makes a working whole
+ */
+class CertServlet extends HttpServlet {
+
+  /**
+   * Handles the get file request, only called on url = /public.crt
+   * so this function has no logic other then to output the cert
+   */
+  public function doGet() {
+    $file = Config::get('public_key_file');
+    if (! file_exists($file) || ! is_readable($file)) {
+      throw new \Exception("Invalid public key location ($file), check config and file permissions");
+    }
+    $this->setLastModified(filemtime($file));
+    readfile($file);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/ContentFilesServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/ContentFilesServlet.php
new file mode 100644
index 0000000..0fa4d37
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/ContentFilesServlet.php
@@ -0,0 +1,32 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ContentFilesServlet extends FilesServlet
+{
+  /**
+   * @return string
+   */
+  protected function getPath() {
+    return Config::get('javascript_path');
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/FilesServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/FilesServlet.php
new file mode 100644
index 0000000..2ed8647
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/FilesServlet.php
@@ -0,0 +1,91 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * This class serves files from the shindig_root/javascript directory, it was created
+ * so that the shindig examples and javascript files would work out of the box with
+ * the php version too
+ */
+abstract class FilesServlet extends HttpServlet {
+
+  /**
+   * @return string
+   */
+  abstract protected function getPath();
+
+  /**
+   * @return string
+   */
+  protected function getRequestUri() {
+    return $_SERVER['REQUEST_URI'];
+  }
+
+  /**
+   * Handles the get file request, if the file exists and is in the correct
+   * location it's echo'd to the browser (with a basic content type guessing
+   * based on the file extention, ie .js becomes text/javascript).
+   * If the file location falls outside of the shindig/javascript root a
+   * 400 Bad Request is returned, and if the file is inside of the root
+   * but doesn't exist a 404 error is returned
+   */
+  public function doGet() {
+    $file = str_replace(Config::get('web_prefix'), '', $this->getRequestUri());
+    $file = $this->getPath() . $file;
+    // make sure that the real path name is actually in the javascript_path, so people can't abuse this to read
+    // your private data from disk .. otherwise this would be a huge privacy and security issue 
+    if (substr(realpath($file), 0, strlen(realpath($this->getPath()))) != realpath($this->getPath())) {
+      header("HTTP/1.0 400 Bad Request", true);
+      echo "<html><body><h1>400 - Bad Request</h1></body></html>";
+      die();
+    }
+    // if the file doesn't exist or can't be read, give a 404 error
+    if (! file_exists($file) || ! is_readable($file) || ! is_file($file)) {
+      header("HTTP/1.0 404 Not Found", true);
+      echo "<html><body><h1>404 - Not Found</h1></body></html>";
+      die();
+    }
+    $dot = strrpos($file, '.');
+    if ($dot) {
+      $ext = strtolower(substr($file, $dot + 1));
+      if ($ext == 'html' || $ext == 'htm') {
+        $this->setContentType('text/html');
+      } elseif ($ext == 'js') {
+        $this->setContentType('text/javascript');
+      } elseif ($ext == 'css') {
+        $this->setContentType('text/css');
+      } elseif ($ext == 'xml') {
+        $this->setContentType('text/xml');
+      } elseif ($ext == 'png') {
+        $this->setContentType('image/png');
+      } elseif ($ext == 'gif') {
+        $this->setContentType('image/gif');
+      } elseif ($ext == 'jpg' || $ext == 'jpeg') {
+        $this->setContentType('image/jpeg');
+      }
+    }
+    $this->setCharset('');
+    $this->setLastModified(filemtime($file));
+    readfile($file);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/GadgetRenderingServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/GadgetRenderingServlet.php
new file mode 100644
index 0000000..b7f23dd
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/GadgetRenderingServlet.php
@@ -0,0 +1,149 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\gadgets\GadgetException;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\Gadget;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetRenderingServlet extends HttpServlet {
+  /**
+   *
+   * @var GadgetContext
+   */
+  protected $context;
+
+  /**
+   * @throws GadgetException
+   */
+  public function doGet() {
+    try {
+      if (empty($_GET['url'])) {
+        throw new GadgetException("Missing required parameter: url");
+      }
+      $contextClass = Config::get('gadget_context_class');
+      $this->context = new $contextClass('GADGET');
+      $gadgetSigner = Config::get('security_token_signer');
+      $gadgetSigner = new $gadgetSigner();
+      try {
+        $token = $this->context->extractAndValidateToken($gadgetSigner);
+      } catch (\Exception $e) {
+        // no token given, this is a fatal error if 'render_token_required' is set to true
+        if (Config::get('render_token_required')) {
+          $this->showError($e);
+        } else {
+          $token = '';
+        }
+      }
+      $factoryClass = Config::get('gadget_factory_class');
+      $gadgetSpecFactory = new $factoryClass($this->context, $token);
+      $gadget = $gadgetSpecFactory->createGadget();
+      $this->setCachingHeaders();
+      $this->renderGadget($gadget);
+    } catch (\Exception $e) {
+      $this->showError($e);
+    }
+  }
+
+  /**
+   *
+   * @param Gadget $gadget
+   * @throws GadgetException
+   */
+  protected function renderGadget(Gadget $gadget) {
+    $view = $gadget->getView($this->context->getView());
+    $renderClasses = Config::get('gadget_renderer');
+
+    foreach ($renderClasses as $renderClass => $constraints) {
+      // if current view meets the configurated renderer constraints
+      // render the gadget and stop checking
+      if ($this->checkConstraints($view, $constraints)) {
+        $gadgetRenderer = new $renderClass($this->context);
+        $gadgetRenderer->renderGadget($gadget, $view);
+        return;
+      }
+    }
+
+    throw new GadgetException("Invalid view type");   
+  }
+
+  /**
+   * checks if the current view meets the given gadget renderer constraints
+   *
+   * constraint format:
+   * 
+   * array(
+   *   attributeName => expectedValue or boolean to indicate if the attribute is 
+   *                      required or not
+   * )
+   *
+   * @param array $view
+   * @param array $constraints
+   * @return boolean
+   */
+  public function checkConstraints($view, $constraints) {
+    foreach ($constraints as $attribute => $expected) {
+      if ($expected === false && isset($view[$attribute]) && $view[$attribute]) {
+        return false;
+      } else if ($expected === true && !(isset($view[$attribute]) && $view[$attribute])) {
+        return false;
+      } else if (! is_bool($expected) && $view[$attribute] !== $expected) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * 
+   */
+  protected function setCachingHeaders() {
+    $this->setContentType("text/html; charset=UTF-8");
+    if ($this->context->getIgnoreCache()) {
+      // no cache was requested, set non-caching-headers
+      $this->setNoCache(true);
+    } elseif (isset($_GET['v'])) {
+      // version was given, cache for a long long time (a year)
+      $this->setCacheTime(365 * 24 * 60 * 60);
+    } else {
+      // no version was given, cache for 5 minutes
+      $this->setCacheTime(5 * 60);
+    }
+  }
+
+  /**
+   *
+   * @param Exception $e
+   */
+  protected function showError($e) {
+    header("HTTP/1.0 400 Bad Request", true, 400);
+    echo "<html><body>";
+    echo "<h1>Error</h1>";
+    echo $e->getMessage();
+    if (Config::get('debug')) {
+      echo "<p><b>Debug backtrace</b></p><div style='overflow:auto; height:300px; border:1px solid #000000'><pre>";
+      print_r(debug_backtrace());
+      echo "</pre></div>>";
+    }
+    echo "</body></html>";
+    die();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/JsServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/JsServlet.php
new file mode 100644
index 0000000..fe9bfe8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/JsServlet.php
@@ -0,0 +1,88 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\gadgets\GadgetFeatureRegistry;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * This event handler deals with the /js/core:caja:etc.js request which content type=url gadgets can use
+ * to retrieve our features javascript code, or used to make the most frequently used part of the feature
+ * library external, and hence cachable by the browser.
+ */
+class JsServlet extends HttpServlet {
+
+  public function doGet() {
+    $this->noHeaders = true;
+    if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+      header("HTTP/1.1 304 Not Modified");
+      header('Content-Length: 0');
+      ob_end_clean();
+      die();
+    }
+    $uri = strtolower($_SERVER["REQUEST_URI"]);
+    $uri = substr($uri, strrpos($uri, '/') + 1);
+    // remove any params that would confuse our parser
+    if (strpos($uri, '?')) {
+      $uri = substr($uri, 0, strpos($uri, '?'));
+    }
+    if (strpos($uri, '.js') !== false) {
+      $uri = substr($uri, 0, strlen($uri) - 3);
+    }
+    $needed = array();
+    if (strpos($uri, ':')) {
+      $needed = explode(':', $uri);
+    } else {
+      $needed[] = $uri;
+    }
+    $found = array();
+    $missing = array();
+    $contextClass = Config::get('gadget_context_class');
+    $context = new $contextClass('GADGET');
+    $registry = new GadgetFeatureRegistry(Config::get('features_path'));
+    if ($registry->resolveFeatures($needed, $found, $missing)) {
+      $isGadgetContext = !isset($_GET["c"]) || $_GET['c'] == 0 ? true : false;
+      $jsData = '';
+      foreach ($found as $feature) {
+        $jsData .= $registry->getFeatureContent($feature, $context, $isGadgetContext);
+      }
+      if (! strlen($jsData)) {
+        header("HTTP/1.0 404 Not Found", true);
+        die();
+      }
+      $this->setCachingHeaders();
+      header("Content-Type: text/javascript");
+      echo $jsData;
+    } else {
+      header("HTTP/1.0 404 Not Found", true);
+    }
+    die();
+  }
+
+  private function setCachingHeaders() {
+    // Expires far into the future
+    header("Expires: Tue, 01 Jan 2030 00:00:01 GMT");
+    // IE seems to need this (10 years should be enough).
+    header("Cache-Control: public,max-age=315360000");
+    // Firefox requires this for certain cases.
+    header("Last-Modified: " . gmdate('D, d M Y H:i:s', time()));
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/MakeRequestServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/MakeRequestServlet.php
new file mode 100644
index 0000000..f8a39d0
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/MakeRequestServlet.php
@@ -0,0 +1,52 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\MakeRequestOptions;
+use apache\shindig\gadgets\MakeRequestParameterException;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MakeRequestServlet extends HttpServlet {
+
+  public function doGet() {
+    try {
+      $this->noHeaders = true;
+      $contextClass = Config::get('gadget_context_class');
+      $context = new $contextClass('GADGET');
+      $makeRequestParams = MakeRequestOptions::fromCurrentRequest();
+      $makeRequestHandlerClass = Config::get('makerequest_handler');
+      $makeRequestHandler = new $makeRequestHandlerClass($context);
+      $makeRequestHandler->fetchJson($makeRequestParams);
+    } catch (MakeRequestParameterException $e) {
+      // Something was misconfigured in the request
+      header("HTTP/1.0 400 Bad Request", true);
+      echo "<html><body><h1>400 - Bad request</h1><p>" . $e->getMessage() . "</body></html>";
+    } catch (\Exception $e) {
+      // catch all exceptions and give a 500 server error
+      header("HTTP/1.0 500 Internal Server Error");
+      echo "<html><body><h1>Internal server error</h1><p>" . $e->getMessage() . "</p></body></html>";
+    }
+  }
+
+  public function doPost() {
+    $this->doGet();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/MetadataServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/MetadataServlet.php
new file mode 100644
index 0000000..48d8547
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/MetadataServlet.php
@@ -0,0 +1,65 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\gadgets\MetadataHandler;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MetadataServlet extends HttpServlet {
+
+  public function doPost() {
+    try {
+      // we support both a raw http post (without application/x-www-form-urlencoded headers) like java does
+      // and a more php / curl safe version of a form post with 'request' as the post field that holds the request json data
+      if (isset($GLOBALS['HTTP_RAW_POST_DATA']) || isset($_POST['request'])) {
+        $requestParam = urldecode(isset($_POST['request']) ? $_POST['request'] : $GLOBALS['HTTP_RAW_POST_DATA']);
+        if (get_magic_quotes_gpc()) {
+          $requestParam = stripslashes($requestParam);
+        }
+        $request = json_decode($requestParam);
+        if ($request == $requestParam) {
+          throw new \Exception("Malformed json string");
+        }
+        $handler = new MetadataHandler();
+        $response = $handler->process($request);
+        echo json_encode(array('gadgets' => $response));
+      } else {
+        throw new \Exception("No post data set");
+      }
+    } catch (\Exception $e) {
+      header("HTTP/1.0 500 Internal Server Error", true, 500);
+      echo "<html><body><h1>Internal Server Error</h1><br />";
+      if (Config::get('debug')) {
+        echo $e->getMessage() . "<br /><pre>";
+        print_r(debug_backtrace());
+        echo "</pre>";
+      }
+      echo "</body></html>";
+    }
+  }
+
+  public function doGet() {
+    header("HTTP/1.0 400 Bad Request", true, 400);
+    echo "<html><body>";
+    echo "<h1>Error</h1>";
+    echo "<body></html>";
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/OAuthCallbackServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/OAuthCallbackServlet.php
new file mode 100644
index 0000000..81b7520
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/OAuthCallbackServlet.php
@@ -0,0 +1,68 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\common\sample\BasicBlobCrypter;
+use apache\shindig\gadgets\oauth\OAuthCallbackState;
+use apache\shindig\common\HttpServlet;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class OAuthCallbackServlet extends HttpServlet {
+  public function doGet() {
+    $state = isset($_GET["state"]) ? $_GET["state"] : "";
+    $token = isset($_GET["oauth_token"]) ? $_GET["oauth_token"] : "";
+    $verifier = isset($_GET["oauth_verifier"]) ? $_GET["oauth_verifier"] : "";
+    $code = isset($_GET["code"]) ? $_GET["code"] : "";
+    if (strlen($state) > 0) {
+      $BBC = new BasicBlobCrypter();
+      $crypter = new BasicBlobCrypter(srand($BBC->MASTER_KEY_MIN_LEN));
+      $clientState = new OAuthCallbackState($crypter, $state);
+      $url = $clientState->getRealCallbackUrl();
+      $callbackUrl = "http://" . $_SERVER['HTTP_HOST'] . "/gadgets/oauthcallback";
+      if ($url = $callbackUrl) {
+        unset($_GET['state']);
+        header('Location: '.$callbackUrl.'?'.http_build_query($_GET));
+        exit;
+      }
+    } else if ((strlen($token) > 0  || strlen($code) > 0) && strlen($state) == 0 ) {
+      $this->setCacheTime(3600);
+      echo "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" " .
+      "\"http://www.w3.org/TR/html4/loose.dtd\">" .
+      "<html>" .
+      "<head>" .
+      "<title>Close this window</title>" .
+      "</head>" .
+      "<body>" .
+      "<script type=\"text/javascript\">" .
+      "try {" .
+      "  window.opener.gadgets.io.oauthReceivedCallbackUrl_ = document.location.href;" .
+      "} catch (e) {" .
+      "}" .
+      "window.close();" .
+      "</script>" .
+      "Close this window." .
+      "</body>" .
+      "</html>";
+      exit;
+    }
+    header("HTTP/1.0 400 Bad Request", true);
+    echo "<html><body><h1>" . "400 - Bad Request Error" . "</h1></body></html>";
+    die();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/ProxyServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/ProxyServlet.php
new file mode 100644
index 0000000..0b9d5d8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/ProxyServlet.php
@@ -0,0 +1,52 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ProxyServlet extends HttpServlet {
+
+  public function doGet() {
+    try {
+      // Make sure the HttpServlet doesn't overwrite our headers
+      $this->noHeaders = true;
+      $contextClass = Config::get('gadget_context_class');
+      $context = new $contextClass('GADGET');
+      $url = (isset($_GET['url']) ? $_GET['url'] : (isset($_POST['url']) ? $_POST['url'] : false));
+      $url = urldecode($url);
+      if (! $url) {
+        header("HTTP/1.0 400 Bad Request", true);
+        echo "<html><body><h1>400 - Missing url parameter</h1></body></html>";
+      }
+      $proxyHandlerClass = Config::get('proxy_handler');
+      $proxyHandler = new $proxyHandlerClass($context);
+      $proxyHandler->fetch($url);
+    } catch (\Exception $e) {
+      // catch all exceptions and give a 500 server error
+      header("HTTP/1.0 500 Internal Server Error");
+      echo "<h1>Internal server error</h1><p>" . $e->getMessage() . "</p>";
+    }
+  }
+
+  public function doPost() {
+    $this->doGet();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/servlet/ResourcesFilesServlet.php b/trunk/php/src/apache/shindig/gadgets/servlet/ResourcesFilesServlet.php
new file mode 100644
index 0000000..bbce168
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/servlet/ResourcesFilesServlet.php
@@ -0,0 +1,39 @@
+<?php
+namespace apache\shindig\gadgets\servlet;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ResourcesFilesServlet extends FilesServlet
+{
+  /**
+   * @return string
+   */
+  protected function getPath() {
+    return Config::get('resources_path');
+  }
+
+  /**
+   * @return string
+   */
+  protected function getRequestUri() {
+    return str_replace('/gadgets/resources/', '', $_SERVER["REQUEST_URI"]);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/DataPipelining.php b/trunk/php/src/apache/shindig/gadgets/templates/DataPipelining.php
new file mode 100644
index 0000000..3f207c2
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/DataPipelining.php
@@ -0,0 +1,315 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\sample\BasicRemoteContent;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\GadgetContext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+//TODO verify os:HttpRequest
+
+class DataPipelining {
+
+  /**
+   * Parses the data-pipelining tags of from a html/href view, or from a os-data script tag and returns a
+   * normalized array of requests to perform (which can be used to call DataPipelining::fetch)
+   *
+   * @param DOMElement $viewNode
+   * @return array
+   */
+  static public function parse(\DOMElement &$viewNode) {
+    $dataTags = $viewNode->getElementsByTagName('*');
+    if ($dataTags->length > 0) {
+      $dataPipeliningTags = array();
+      $namespaceErrorTags = array('httprequest', 'datarequest', 'peoplerequest', 'personappdatarequest', 'viewerrequest', 'ownerrequest', 'activitiesrequest');
+      foreach ($dataTags as $dataTag) {
+        $tag = array();
+        $tag['type'] = $dataTag->tagName;
+        $supportedDataAttributes = array('key', 'method', 'userId', 'groupId', 'fields', 'startIndex', 'count', 'sortBy', 'sortOrder', 'filterBy', 'filterOp', 'filterValue', 'activityIds', 'href', 'params');
+        foreach ($supportedDataAttributes as $dataAttribute) {
+          $val = $dataTag->getAttribute($dataAttribute);
+          if (! empty($val)) {
+            $tag[$dataAttribute] = $val;
+          }
+        }
+
+        // Make sure the proper name space decleration was used, either parsing would fail miserably
+        if (in_array(strtolower($tag['type']), $namespaceErrorTags)) {
+        	throw new ExpressionException("Invalid os-data namespace, please use xmlns:os=\"http://ns.opensocial.org/2008/markup\" in the script tag");
+        }
+
+        // normalize the methods so that os:PeopleRequest becomes a os:DataRequest with a people.get method, and os:ViewerRequest becomes a people.get with a userId = @viewer & groupId = @self, this
+        // makes it a whole lot simpler to implement the actual data fetching in the renderer
+        switch ($tag['type']) {
+          case 'os:PeopleRequest':
+            $tag['type'] = 'os:DataRequest';
+            $tag['method'] = 'people.get';
+            break;
+          case 'os:PersonAppDataRequest':
+            $tag['type'] = 'os:DataRequest';
+            $tag['method'] = 'appdata.get';
+            break;
+          case 'os:ViewerRequest':
+          case 'os:OwnerRequest':
+            $tag['method'] = 'people.get';
+            $tag['userId'] = $tag['type'] == 'os:ViewerRequest' ? '@viewer' : '@owner';
+            $tag['groupId'] = '@self';
+            $tag['type'] = 'os:DataRequest';
+            break;
+          case 'os:ActivitiesRequest':
+            $tag['type'] = 'os:DataRequest';
+            $tag['method'] = 'activities.get';
+            break;
+        }
+        $dataPipeliningTags[] = $tag;
+      }
+      return $dataPipeliningTags;
+    }
+    return null;
+  }
+
+  /**
+   * Fetches the requested data-pipeling info
+   *
+   * @param array $dataPipelining contains the parsed data-pipelining tags
+   * @param GadgetContext $context context to use for fetching
+   * @param array $dataContext the data context to use while resolving expressions in requests (it'll use the combined results + context to resolve)
+   * @return array result
+   */
+  static public function fetch($dataPipeliningRequests, GadgetContext $context, $dataContext = array()) {
+    $result = array();
+    if (is_array($dataPipeliningRequests) && count($dataPipeliningRequests)) {
+      do {
+        // See which requests we can batch together, that either don't use dynamic tags or who's tags are resolvable
+        $requestQueue = array();
+        foreach ($dataPipeliningRequests as $key => $request) {
+          if (($resolved = self::resolveRequest($request, $result)) !== false) {
+            $requestQueue[] = $resolved;
+            unset($dataPipeliningRequests[$key]);
+          }
+        }
+        if (count($requestQueue)) {
+          $returnedResults = self::performRequests($requestQueue, $context);
+          if (is_array($returnedResults)) {
+            $dataContext = self::addResultToContext($returnedResults, $dataContext);
+            $result = array_merge($returnedResults, $result);
+          }
+        }
+      } while (count($requestQueue));
+    }
+    return $result;
+  }
+
+  /**
+   * Adds the fetched results to the data context, used by the fetch() function to
+   * add the performed requests to the data context that's used to resolve expressions
+   *
+   * @param array $returnedResults
+   * @param array $dataContext
+   * @return array
+   */
+  static private function addResultToContext($returnedResults, $dataContext) {
+    foreach ($returnedResults as $val) {
+      // we really only accept entries with a request id, otherwise it can't be referenced by context anyhow
+      if (isset($val['id'])) {
+        $key = $val['id'];
+        // Pick up only the actual data part of the response, so we can do direct variable resolution
+        if (isset($val['result']['list'])) {
+          $dataContext[$key] = $val['result']['list'];
+        } elseif (isset($val['result']['entry'])) {
+          $dataContext[$key] = $val['result']['entry'];
+        } elseif (isset($val['result'])) {
+          $dataContext[$key] = $val['result'];
+        }
+      }
+    }
+    return $dataContext;
+  }
+
+  /**
+   * Peforms the actual http fetching of the data-pipelining requests, all social requests
+   * are made to $_SERVER['HTTP_HOST'] (the virtual host name of this server) / (optional) web_prefix / social / rpc, and
+   * the httpRequest's are made to $_SERVER['HTTP_HOST'] (the virtual host name of this server) / (optional) web_prefix / gadgets / makeRequest
+   * both request types use the current security token ($_GET['st']) when performing the requests so they happen in the correct context
+   *
+   * @param array $requests
+   * @param GadgetContext $context
+   * @return array response
+   */
+  static private function performRequests($requests, GadgetContext $context) {
+    $jsonRequests = array();
+    $httpRequests = array();
+    $decodedResponse = array();
+    // Using the same gadget security token for all social & http requests so everything happens in the right context
+    if (! BasicSecurityToken::getTokenStringFromRequest()) {
+    	throw new ExpressionException("No security token set, required for data-pipeling");
+    }
+    $securityToken = $_GET['st'];
+    foreach ($requests as $request) {
+      switch ($request['type']) {
+        case 'os:DataRequest':
+          // Add to the social request batch
+          $id = $request['key'];
+          $method = $request['method'];
+          // remove our internal fields so we can use the remainder as params
+          unset($request['key']);
+          unset($request['method']);
+          unset($request['type']);
+          if (isset($request['fields'])) {
+          	$request['fields'] = explode(',', $request['fields']);
+          }
+          $jsonRequests[] = array('method' => $method, 'id' => $id, 'params' => $request);
+          break;
+        case 'os:HttpRequest':
+          $id = $request['key'];
+          $url = $request['href'];
+          $format = isset($request['format']) ? $request['format'] : 'json';
+          unset($request['key']);
+          unset($request['type']);
+          unset($request['href']);
+          $httpRequests[$url] = array('id' => $id, 'url' => $url, 'format' => $format, 'queryStr' => implode('&', $request));
+          break;
+      }
+    }
+    if (count($jsonRequests)) {
+      // perform social api requests
+      $request = new RemoteContentRequest('http://'.$_SERVER['HTTP_HOST'] . Config::get('web_prefix') . '/rpc?st=' . urlencode($securityToken) . '&format=json', "Content-Type: application/json\n", json_encode($jsonRequests));
+      $request->setMethod('POST');
+      $remoteFetcherClass = Config::get('remote_content_fetcher');
+      $remoteFetcher = new $remoteFetcherClass();
+      $basicRemoteContent = new BasicRemoteContent($remoteFetcher);
+      $response = $basicRemoteContent->fetch($request);
+      $decodedResponse = json_decode($response->getResponseContent(), true);
+    }
+    if (count($httpRequests)) {
+      $requestQueue = array();
+      foreach ($httpRequests as $request) {
+        $req = new RemoteContentRequest($_SERVER['HTTP_HOST'] . Config::get('web_prefix') . '/gadgets/makeRequest?url=' . urlencode($request['url']) . '&st=' . urlencode($securityToken) . (! empty($request['queryStr']) ? '&' . $request['queryStr'] : ''));
+        $req->getOptions()->ignoreCache = $context->getIgnoreCache();
+        $req->setNotSignedUri($request['url']);
+        $requestQueue[] = $req;
+      }
+      $basicRemoteContent = new BasicRemoteContent();
+      $resps = $basicRemoteContent->multiFetch($requestQueue);
+      foreach ($resps as $response) {
+        //FIXME: this isn't completely correct yet since this picks up the status code and headers
+        // as they are returned by the makeRequest handler and not the ones from the original request
+
+        $url = $response->getNotSignedUrl();
+        $id = $httpRequests[$url]['id'];
+        // strip out the UNPARSEABLE_CRUFT (see makeRequestHandler.php) on assigning the body
+        $resp = json_decode(str_replace("throw 1; < don't be evil' >", '', $response->getResponseContent()), true);
+        if (is_array($resp)) {
+          $statusCode = $response->getHttpCode();
+          $statusCodeMessage = $response->getHttpCodeMsg();
+          $headers = $response->getHeaders();
+          if (intval($statusCode) == 200) {
+            $content = $httpRequests[$url]['format'] == 'json' ? json_decode($resp[$url]['body'], true) : $resp[$url]['body'];
+            $toAdd = array(
+              'result' => array(
+                'content' => $content,
+                'status' => $statusCode,
+                'headers' => $headers
+              )
+            );
+          } else {
+            $content = $resp[$url]['body'];
+            $toAdd = array(
+              'error' => array(
+                'code' => $statusCode,
+                'message' => $statusCodeMessage,
+                'result' => array(
+                  'content' => $content,
+                  'headers' => $headers
+                )
+              )
+            );
+          }
+          //$toAdd[$id] = array('id' => $id, 'result' => $httpRequests[$url]['format'] == 'json' ? json_decode($resp[$url]['body'], true) : $resp[$url]['body']);
+          $decodedResponse[] = array('id' => $id, 'result' => $toAdd);
+        }
+      }
+    }
+    return $decodedResponse;
+  }
+
+  /**
+   * If a request (data-pipelining tag) doesn't include any dynamic tags, it's returned as is. If
+   * however it does contain said tag, this function will attempt to resolve it using the $result
+   * array, returning the parsed request on success, or FALSE on failure to resolve.
+   *
+   * @param array $request
+   * @param array $result
+   * @return array
+   */
+  static private function resolveRequest($request, $result) {
+    $dataContext = self::makeContextData($result);
+    foreach ($request as $key => $val) {
+      $expressions = array();
+      preg_match_all('/\$\{(.*)\}/imxsU', $val, $expressions);
+      $expressionCount = count($expressions[0]);
+      if ($expressionCount) {
+        for ($i = 0; $i < $expressionCount; $i ++) {
+          $toReplace = $expressions[0][$i];
+          $expression = $expressions[1][$i];
+          try {
+            $expressionResult = ExpressionParser::evaluate($expression, $dataContext);
+            $request[$key] = str_replace($toReplace, $expressionResult, $request[$key]);
+          } catch (\Exception $e) {
+            // ignore, maybe on the next pass we can resolve this
+            return false;
+          }
+        }
+      }
+    }
+    return $request;
+  }
+
+  /**
+   * Makes a data context array out of the current data pipelining results that can be used
+   * by the expression parser to resolve the request attributes
+   *
+   * @param array $array current data pipelining results
+   * @return array $dataContext a dataContext array
+   */
+  static private function makeContextData($array) {
+    $result = array();
+    foreach ($array as $val) {
+      if (isset($val['id'])) {
+        $key = $val['id'];
+        if (isset($val['result']['list'])) {
+          $result[$key] = $val['result']['list'];
+        } elseif (isset($val['result']['entry'])) {
+          $result[$key] = $val['result']['entry'];
+        } elseif (isset($val['result'])) {
+          $result[$key] = $val['result'];
+        }
+      }
+    }
+    $dataContext = array();
+    $dataContext['Top'] = $result;
+    $dataContext['Cur'] = array();
+    $dataContext['My'] = array();
+    $dataContext['Context'] = array('UniqueId' => uniqid());
+    return $dataContext;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpLexer.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpLexer.php
new file mode 100644
index 0000000..70b85bc
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpLexer.php
@@ -0,0 +1,391 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Lexer for parsing the os-template / os-data expression language, which is based on the JSP EL syntax
+ * For reference on the language see:
+ * JSP EL: https://jsp.dev.java.net/spec/jsp-2_1-fr-spec-el.pdf
+ * OS Templates: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml
+ * OS Data pipelining: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Data-Pipelining.xml
+ *
+ * This lexer could handle special formats like floating 12.3e-10 and string 'tell "me" \'yes\\\'' correctly.
+ */
+
+class ExpLexer {
+
+  private static $BLANK = "/[\s]+/";
+  private static $FUNCTION_PATTERN = "/osx\:(parseJson|decodeBase64|urlEncode|urlDecode)/";
+  private static $TERNARY_PATTERN = "/[\?\:]/";
+  private static $PAREN_PATTERN = "/[\[\]\(\)]/";
+  private static $COMMA_PATTERN = "/[\,]/";
+  private static $OPERATOR_PATTERN = "/\>\=|\<\=|\=\=|\!\=|\&\&|\|\||\*|\/|\%|\>|\<|\!/";  // No +/-: conflict with floating
+  private static $OPERATOR_PATTERN2 = "/^(and|or|div|mod|gt|lt|ge|le|eq|ne|not|empty)$/";
+  private static $NUMBER_PATTERN = "/^(([0-9]+\.[0-9]*)|(\.[0-9]+)|([0-9]+))([eE][\+\-]?[0-9]+)?$/";
+  private static $IDENTITY_PATTERN = "/^[a-zA-Z][a-zA-Z0-9_]*$/";
+  private static $DOT_PATTERN = "/[\.]/";
+
+  private static $OPERATOR_TRANS = array('and' => '&&', 'or' => '||', 'div' => '/', 'mod' => '%', 'gt' => '>',
+      'lt' => '<', 'ge' => '>=', 'le' => '<=', 'eq' => '==', 'ne' => '!=', 'not' => '!',
+      'empty' => 'empty');
+
+  private static $PAIRS = array(')' => '(', ']' => '[', ':' => '?');
+  private static $ENDS = array(')', ']', ':');
+
+  public static function process($str) {
+    $tokenStream = ExpLexer::strToTokens($str);
+    ExpLexer::evaluateLiterals($tokenStream);
+    ExpLexer::validateTokens($tokenStream);
+    return $tokenStream;
+  }
+
+  private static function strToTokens($str) {
+    // Multi-pass segmentation
+    $node = new Token(ExpType::$RAW, $str);
+    $tokenStream = array($node);
+    $tokenStream = ExpLexer::divideString($tokenStream);
+    $tokenStream = ExpLexer::divideBlank($tokenStream);
+    $tokenStream = ExpLexer::divideKey($tokenStream, ExpLexer::$FUNCTION_PATTERN, 'functionHandler');
+    $tokenStream = ExpLexer::divideKey($tokenStream, ExpLexer::$TERNARY_PATTERN, 'ternaryHandler');
+    $tokenStream = ExpLexer::divideKey($tokenStream, ExpLexer::$PAREN_PATTERN, 'parenHandler');
+    $tokenStream = ExpLexer::divideKey($tokenStream, ExpLexer::$COMMA_PATTERN, 'commaHandler');
+    $tokenStream = ExpLexer::divideKey($tokenStream, ExpLexer::$OPERATOR_PATTERN, 'operatorHandler');
+    $tokenStream = ExpLexer::divideFloating($tokenStream);
+    $tokenStream = ExpLexer::divideKey($tokenStream, ExpLexer::$OPERATOR_PATTERN2, 'operatorHandler2');
+    $tokenStream = ExpLexer::divideDot($tokenStream);
+    ExpLexer::distinguishOperator($tokenStream);
+    return $tokenStream;
+  }
+
+  private static function isEscaped($str, $pos) {
+    $numEscaper = 0;
+    $posEscaper = $pos - 1;
+    while ($posEscaper >= 0) {
+      if ($str[$posEscaper] != '\\') break;
+      $numEscaper ++;
+      $posEscaper --;
+    }
+    return ($numEscaper % 2 == 1);
+  }
+
+  private static function divideString($tokenStream) {
+    $newTokenStream = array();
+    foreach ($tokenStream as $node) {
+      if ($node->type != ExpType::$RAW) {
+        // bypass the proceed nodes
+        array_push($newTokenStream, $node);
+        continue;
+      }
+      $str = $node->value;
+      $state = 0; // 0: seeking, 1: found "'", 2: found '"'
+      $pos = 0;
+      for ($i = 0; $i < strlen($str); $i ++) {
+        if ($str[$i] != "'" && $str[$i] != '"') continue;
+        $targetState = $str[$i] == "'" ? 1 : 2;
+        if ($state == 0) {
+          // found "'" or '"'
+          $state = $targetState;
+          if ($pos != $i) {
+            // output the previous segment
+            $newnode = new Token(ExpType::$RAW, substr($str, $pos, $i - $pos));
+            array_push($newTokenStream, $newnode);
+          }
+          $pos = $i + 1;
+        } elseif ($state == $targetState) {
+          // test whether it has been escaped
+          if (ExpLexer::isEscaped($str, $i)) continue;  // bypass the escaped ones
+          // output the string segment
+          $string = ExpType::coerceToString(new Token(ExpType::$RAW, substr($str, $pos, $i - $pos)));
+          array_push($newTokenStream, $string);
+          $pos = $i + 1;
+          $state = 0;
+        }
+      }
+      if ($state != 0) {
+        // there's open quote exists
+        throw new ExpLexerException("Unterminated string: " . $str);
+      }
+      if ($pos != strlen($str)) {
+        // output the remaining segment
+        $newnode = new Token(ExpType::$RAW, substr($str, $pos, strlen($str) - $pos));
+        array_push($newTokenStream, $newnode);
+      }
+    }
+    return $newTokenStream;
+  }
+
+  private static function divideBlank($tokenStream) {
+    $newTokenStream = array();
+    foreach ($tokenStream as $node) {
+      if ($node->type != ExpType::$RAW) {
+        // bypass the proceed nodes
+        array_push($newTokenStream, $node);
+        continue;
+      }
+      $subNodes = preg_split(ExpLexer::$BLANK, $node->value);
+      foreach ($subNodes as $subNode) {
+        $newnode = new Token(ExpType::$RAW, $subNode);
+        array_push($newTokenStream, $newnode);
+      }
+    }
+    return $newTokenStream;
+  }
+
+  private static function divideKey($tokenStream, $keyPattern, $keyHandler) {
+    $newTokenStream = array();
+    foreach ($tokenStream as $node) {
+      if ($node->type != ExpType::$RAW) {
+        // bypass the proceed nodes
+        array_push($newTokenStream, $node);
+        continue;
+      }
+      $str = $node->value;
+      preg_match_all($keyPattern, $str, $matchs, PREG_OFFSET_CAPTURE);
+      $pos = 0;
+      foreach ($matchs[0] as $match) {
+        if ($pos != $match[1]) {
+          // output the previous segment
+          $newnode = new Token(ExpType::$RAW, substr($str, $pos, $match[1] - $pos));
+          array_push($newTokenStream, $newnode);
+        }
+        // output the value.
+        array_push($newTokenStream, ExpLexer::$keyHandler($match[0]));
+        $pos = $match[1] + strlen($match[0]);
+      }
+      if ($pos != strlen($str)) {
+        // output the remaining segment
+        $newnode = new Token(ExpType::$RAW, substr($str, $pos, strlen($str) - $pos));
+        array_push($newTokenStream, $newnode);
+      }
+    }
+    return $newTokenStream;
+  }
+
+  private static function functionHandler($str) {
+    // output the opensocial function name.
+    return new Token(ExpType::$FUNCTION, $str);
+  }
+
+  private static function ternaryHandler($str) {
+    // output the ternary.
+    return new Token(ExpType::$TERNARY, $str);
+  }
+
+  private static function parenHandler($str) {
+    // output the paren.
+    return new Token(ExpType::$PAREN, $str);
+  }
+
+  private static function commaHandler($str) {
+    // output the comma.
+    return new Token(ExpType::$COMMA, $str);
+  }
+
+  private static function operatorHandler($str) {
+    // output the operator.
+    return new Token(ExpType::$RAW_OP, $str);
+  }
+
+  private static function operatorHandler2($str) {
+    // output the operator.
+    return new Token(ExpType::$RAW_OP, ExpLexer::$OPERATOR_TRANS[$str]);
+  }
+
+  private static function divideFloating($tokenStream) {
+    $newTokenStream = array();
+    $TAG_PATTERN = "/[eE][\+\-]|[eE]|[\+\-]/";
+    $FLOAT_PATTERN = "/^[0-9\.]+$/";
+    $INTEGER_PATTERN = "/^[0-9]+$/";
+    foreach ($tokenStream as $node) {
+      if ($node->type != ExpType::$RAW) {
+        // bypass the proceed nodes
+        array_push($newTokenStream, $node);
+        continue;
+      }
+      $candidateTokenStream = ExpLexer::divideKey(array($node), $TAG_PATTERN, 'tagHandler');
+      $tokenStack = array();
+      while ($token = array_shift($candidateTokenStream)) {
+        if ($token->type != 'tag') {
+          $tokenLeft = array_pop($tokenStack);
+          if (! $tokenLeft) {
+            array_push($tokenStack, $token);
+          } elseif ($tokenLeft->type == ExpType::$RAW) {
+            $tokenLeft->value = $tokenLeft->value . $token->value;
+            array_push($tokenStack, $tokenLeft);
+          } else {
+            array_push($tokenStack, $tokenLeft);
+            array_push($tokenStack, $token);
+          }
+        } else {
+          if (($token->value == '+') || ($token->value == '-')) {
+            // regular operator
+            $token->type = ExpType::$RAW_OP;
+            array_push($tokenStack, $token);
+          } else {
+            // 'e', 'e+' or 'e-' encountered.
+            $tokenLeft = array_pop($tokenStack);
+            if ($tokenLeft == null) $tokenLeft = new Token(ExpType::$RAW, '');
+            if ($tokenLeft->type != ExpType::$RAW) {
+              array_push($tokenStack, $tokenLeft); // push the token back
+              $tokenLeft = new Token(ExpType::$RAW, '');
+            }
+            if (preg_match($FLOAT_PATTERN, $tokenLeft->value) == 0) {
+              // it's '[eE]', or a normal operator '[eE][+-]'
+              $tokenLeft->value = $tokenLeft->value . $token->value[0];
+              array_push($tokenStack, $tokenLeft);
+              if (strlen($token->value) == 2) {
+                $token->type = ExpType::$RAW_OP;
+                $token->value = $token->value[1];
+                array_push($tokenStack, $token);
+              }
+            } else {
+              // might be a floating number, check the right side.
+              $tokenRight = array_shift($candidateTokenStream);
+              if ($tokenRight == null || $tokenRight->type != ExpType::$RAW || preg_match($INTEGER_PATTERN, $tokenRight->value) == 0) throw new ExpLexerException("Mal-format floating contained: " . $node->value);
+              // Okay, all tests passed.  It's a floating number.  Put all parts together.  Will evaluate its value in the latter step.
+              $tokenLeft->value = $tokenLeft->value . $token->value . $tokenRight->value;
+              array_push($tokenStack, $tokenLeft);
+            }
+          }
+        }
+      }
+      $newTokenStream = array_merge($newTokenStream, $tokenStack);
+    }
+    return $newTokenStream;
+  }
+
+  private static function tagHandler($str) {
+    // output the tag, it might be a part of floating number, or a regular operator.
+    return new Token('tag', $str);
+  }
+
+  private static function divideDot($tokenStream) {
+    $newTokenStream = array();
+    foreach ($tokenStream as $node) {
+      if ($node->type != ExpType::$RAW) {
+        // bypass the proceed nodes
+        array_push($newTokenStream, $node);
+        continue;
+      }
+      if (preg_match(ExpLexer::$NUMBER_PATTERN, $node->value) == 0) {
+        $candidateTokenStream = ExpLexer::divideKey(array($node), ExpLexer::$DOT_PATTERN, 'dotHandler');
+        $newTokenStream = array_merge($newTokenStream, $candidateTokenStream);
+      } else {
+        array_push($newTokenStream, $node);
+      }
+    }
+    return $newTokenStream;
+  }
+
+  private static function dotHandler($str) {
+    // output the tag, it might be a part of floating number, or a regular operator.
+    return new Token(ExpType::$DOT, $str);
+  }
+
+  private static function distinguishOperator(&$tokenStream) {
+    for ($i = 0; $i < count($tokenStream); $i ++) {
+      $node = $tokenStream[$i];
+      if ($node->type != ExpType::$RAW_OP) continue;
+      $str = $node->value;
+      if ($str == 'empty' || $str == '!') {
+        $node->type = ExpType::$UNARY_OP;
+      } elseif ($str != '-') {
+        $node->type = ExpType::$BINARY_OP;
+      } else {
+        // distinguish unary/binary operator '-'
+        $node->type = ExpLexer::hasUnaryLeft($tokenStream, $i) ? ExpType::$UNARY_OP : ExpType::$BINARY_OP;
+        $node->value = $node->type == ExpType::$UNARY_OP ? ' -' : ' - ';
+      }
+    }
+  }
+
+  private static function hasUnaryLeft($tokenStream, $i) {
+    $OPERATOR_LEFT_TYPE = array(ExpType::$BINARY_OP, ExpType::$UNARY_OP, ExpType::$TERNARY,
+        ExpType::$COMMA);
+    $OPERATOR_LEFT_VALUE = array('(', '[');
+    if ($i == 0) return true;
+    $node = $tokenStream[$i - 1];
+    if (in_array($node->type, $OPERATOR_LEFT_TYPE) || $node->type == ExpType::$PAREN && in_array($node->value, $OPERATOR_LEFT_VALUE)) return true;
+    return false;
+  }
+
+  private static function hasBinaryRight($tokenStream, $i) {
+    $OPERATOR_RIGHT_NO_TYPE = array(ExpType::$BINARY_OP, ExpType::$TERNARY, ExpType::$COMMA);
+    $OPERATOR_RIGHT_NO_VALUE = array(')', ']');
+    if ($i == count($tokenStream) - 1) return false;
+    $node = $tokenStream[$i + 1];
+    if (in_array($node->type, $OPERATOR_RIGHT_NO_TYPE) || $node->type == ExpType::$PAREN && in_array($node->value, $OPERATOR_RIGHT_NO_VALUE)) return false;
+    return true;
+  }
+
+  private static function evaluateLiterals(&$tokenStream) {
+    for ($i = 0; $i < count($tokenStream); $i ++) {
+      $node = $tokenStream[$i];
+      if ($node->type != ExpType::$RAW) continue;
+      $str = strtolower($node->value);
+      if ($str == 'true' || $str == 'false') {
+        $tokenStream[$i] = ExpType::coerceToBool($node);
+      } elseif ($str == 'null') {
+        $tokenStream[$i] = ExpType::coerceToNull($node);
+      } elseif (preg_match(ExpLexer::$NUMBER_PATTERN, $str) == 1) {
+        $tokenStream[$i] = ExpType::coerceToNumber($node);
+      } elseif (preg_match(ExpLexer::$IDENTITY_PATTERN, $str) == 1) {
+        // is identity
+        $tokenStream[$i]->type = ExpType::$IDENTITY;
+      } else {
+        // mal-format
+        throw new ExpLexerException("Mal-format expression segment: " . $node->value);
+      }
+    }
+  }
+
+  private static function validateTokens($tokenStream) {
+    // No return value --- throws exception when error encountered.
+    $stack = array();
+    for ($i = 0; $i < count($tokenStream); $i ++) {
+      $node = $tokenStream[$i];
+      switch ($node->type) {
+        case ExpType::$TERNARY: // drop through
+        case ExpType::$PAREN: // Check balance
+          if (in_array($node->value, ExpLexer::$ENDS)) {
+            $stackLeft = array_pop($stack);
+            if (ExpLexer::$PAIRS[$node->value] != $stackLeft) throw new ExpLexerException("Unbalanced expression");
+          } else {
+            array_push($stack, $node->value);
+          }
+          break;
+        case ExpType::$UNARY_OP:  // Check unary operator gramma
+          if (! ExpLexer::hasUnaryLeft($tokenStream, $i)) throw new ExpLexerException("Mal-format unary operator");
+          break;
+        case ExpType::$BINARY_OP:  // Check binary operator gramma
+          if (ExpLexer::hasUnaryLeft($tokenStream, $i) || ! ExpLexer::hasBinaryRight($tokenStream, $i)) throw new ExpLexerException("Mal-format binary operator");
+          break;
+        case ExpType::$DOT:  // Check dot gramma
+          if ($i == count($tokenStream) - 1 || $tokenStream[$i + 1]->type != ExpType::$IDENTITY) throw new ExpLexerException("Mal-format dot");
+          break;
+        case ExpType::$FUNCTION:  // Check function gramma
+          if ($i == count($tokenStream) - 1 || $tokenStream[$i + 1]->value != '(') throw new ExpLexerException("Mal-format function");
+          break;
+      }
+    }
+    if (count($stack) != 0) throw new ExpLexerException("Unbalanced expression");
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpLexerException.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpLexerException.php
new file mode 100644
index 0000000..38da435
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpLexerException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ExpLexerException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpParser.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpParser.php
new file mode 100644
index 0000000..f695ff0
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpParser.php
@@ -0,0 +1,136 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Lexer for parsing the os-template / os-data expression language, which is based on the JSP EL syntax
+ * For reference on the language see:
+ * JSP EL: https://jsp.dev.java.net/spec/jsp-2_1-fr-spec-el.pdf
+ * OS Templates: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml
+ * OS Data pipelining: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Data-Pipelining.xml
+ *
+ * This parser accepts the output token stream produced by ExpLexer.
+ */
+
+class ExpParser {
+
+  private static function scopePop(&$expression, &$scopes, $dataContext) {
+    $value = $expression->evaluate($dataContext);
+    $expression = array_pop($scopes);
+    $expression->append($value);
+  }
+
+  private static function scopePush(&$expression, &$scopes, $token) {
+    array_push($scopes, $expression);
+    $expression = new PrimitiveExp($token);
+  }
+
+  public static function parse($tokenStream, $dataContext) {
+    $scopes = array();
+    $expression = new PrimitiveExp(new Token('final'));
+
+    while ($token = array_shift($tokenStream)) {
+      // split non-primitive expression into primitive ones
+      switch ($token->type) {
+        case ExpType::$PAREN:
+          switch ($token->value) {
+            case '[':
+              $expression->append(new Token(ExpType::$DOT, '.'));  // drop through
+            case '(':
+              ExpParser::scopePush($expression, $scopes, $token);
+              break;
+            case ']':
+              if ($expression->reason->value != '[') throw new ExpParserException("Unbalanced [], should be detected in Lexer");
+              ExpParser::scopePop($expression, $scopes, $dataContext);
+              break;
+            case ')':
+              if ($expression->reason->value != '(') throw new ExpParserException("Unbalanced (), should be detected in Lexer");
+              ExpParser::scopePop($expression, $scopes, $dataContext);
+              // close if it's a function
+              if ($expression->reason->type == ExpType::$FUNCTION) ExpParser::scopePop($expression, $scopes, $dataContext);
+              break;
+            default:
+              throw new ExpParserException("Token error: " . print_r($token, true));
+          }
+          break;
+        case ExpType::$FUNCTION:
+          ExpParser::scopePush($expression, $scopes, $token);
+          break;
+        case ExpType::$TERNARY:
+          if ($token->value != '?') throw new ExpParserException("Ternary token error");
+          $nextTernary = ExpParser::findNextTernary($tokenStream, 1);
+          $indicator = ExpType::coerceToBool($expression->evaluate($dataContext));
+          if ($indicator->value) {
+            // parsed?todo:skip
+            $nextCloseSymbol = ExpParser::findNextCloseSymbol($tokenStream, $nextTernary + 1);
+            array_splice($tokenStream, $nextTernary, $nextCloseSymbol - $nextTernary);
+          } else {
+            // parsed?skip:todo
+            $tokenStream = array_slice($tokenStream, $nextTernary + 1);
+          }
+          $expression = new PrimitiveExp($expression->reason);
+          break;
+        case ExpType::$COMMA:
+          if ($expression->reason->value != '(') throw new ExpParserException("Unbalanced (), should be detected in Lexer");
+          ExpParser::scopePop($expression, $scopes, $dataContext);
+          ExpParser::scopePush($expression, $scopes, new Token(ExpType::$PAREN, '('));
+          break;
+        default:
+          $expression->append($token);
+      }
+    }
+    if ($expression->reason->type != 'final') throw new ExpParserException("Gramma error on the non-primitive expression");
+    return $expression->evaluate($dataContext);
+  }
+
+  private static function findNextTernary($tokenStream, $startPos) {
+    $stackDepth = 0;
+    for ($i = $startPos; $i < count($tokenStream); $i ++) {
+      $token = $tokenStream[$i];
+      if ($token->type == ExpType::$TERNARY && $token->value == '?') $stackDepth ++;
+      if ($token->type == ExpType::$TERNARY && $token->value == ':') $stackDepth --;
+      if ($stackDepth < 0) break;
+    }
+    return $i;
+  }
+
+  private static function findNextCloseSymbol($tokenStream, $startPos) {
+    $stackDepth = 0;
+    for ($i = $startPos; $i < count($tokenStream); $i ++) {
+      $token = $tokenStream[$i];
+      if ($stackDepth == 0 && ExpParser::isCloseSymbol($token, false)) break;
+      if (ExpParser::isOpenSymbol($token)) $stackDepth ++;
+      if (ExpParser::isCloseSymbol($token)) $stackDepth --;
+    }
+    return $i;
+  }
+
+  private static function isOpenSymbol($token) {
+    if ($token->type == ExpType::$PAREN && in_array($token->value, array('[', '('))) return true;
+    return false;
+  }
+
+  private static function isCloseSymbol($token, $rigid = true) {
+    if ($token->type == ExpType::$PAREN && in_array($token->value, array(']', ')'))) return true;
+    if (! $rigid && ($token->type == ExpType::$COMMA || $token->type == ExpType::$TERNARY)) return true;
+    return false;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpParserException.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpParserException.php
new file mode 100644
index 0000000..0caedd5
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpParserException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ExpParserException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpType.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpType.php
new file mode 100644
index 0000000..6d4969f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpType.php
@@ -0,0 +1,167 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Lexer for parsing the os-template / os-data expression language, which is based on the JSP EL syntax
+ * For reference on the language see:
+ * JSP EL: https://jsp.dev.java.net/spec/jsp-2_1-fr-spec-el.pdf
+ * OS Templates: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml
+ * OS Data pipelining: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Data-Pipelining.xml
+ *
+ * This ExpType handles all type identifications and conversions in ExpLexer and ExpParser.
+ */
+
+class ExpType {
+
+  // operators
+  public static $FUNCTION = 'function';
+  public static $TERNARY = 'ternary';
+  public static $PAREN = 'paren';
+  public static $COMMA = 'comma';
+  public static $RAW_OP = 'raw_operator';
+  public static $BINARY_OP = 'binary_operator';
+  public static $UNARY_OP = 'unary_operator';
+  public static $DOT = 'dot';
+
+  // values
+  public static $RAW = 'raw';
+  public static $INT = 'int';
+  public static $FLOAT = 'float';
+  public static $STRING = 'string';
+  public static $BOOL = 'bool';
+  public static $NULL = 'null';
+  public static $IDENTITY = 'identity';
+  public static $ARRAY = 'array';
+  public static $OBJECT = 'object';
+
+  private static $ESCAPE_CHARS = array('\"' => '"', "\'" => "'", '\\\\' => '\\');
+
+  /**
+   *
+   * @param mixed $value
+   * @return string
+   */
+  public static function detectType($value) {
+    if (is_int($value)) {
+      return ExpType::$INT;
+    } elseif (is_float($value)) {
+      return ExpType::$FLOAT;
+    } elseif (is_string($value)) {
+      return ExpType::$STRING;
+    } elseif (is_bool($value)) {
+      return ExpType::$BOOL;
+    } elseif (is_null($value)) {
+      return ExpType::$NULL;
+    } elseif (is_array($value)) {
+      return ExpType::$ARRAY;
+    } elseif (is_object($value)) {
+      return ExpType::$OBJECT;
+    } else {
+      throw new ExpParserException("Un-recogonized variable type of identity: " . $value);
+    }
+  }
+
+  /**
+   * @param Token $token
+   * @return Token
+   */
+  public static function coerceToNumber($token) {
+    $INTEGER_PATTERN = "/^[0-9]+$/";
+    $type = $token->type;
+    if (in_array($type, array(ExpType::$INT, ExpType::$FLOAT))) return $token;
+    if (in_array($type, array(ExpType::$BOOL, ExpType::$NULL)) || in_array($type, array(ExpType::$RAW, ExpType::$STRING)) && preg_match($INTEGER_PATTERN, $token->value) == 1) {
+      $int = new Token(ExpType::$INT, (int)($token->value));
+      return $int;
+    }
+    if (in_array($type, array(ExpType::$RAW, ExpType::$STRING))) {
+      $float = new Token(ExpType::$FLOAT, (float)($token->value));
+      return $float;
+    }
+    throw new ExpTypeException("Unable to coerce token " . print_r($token, true) . " to number");
+  }
+
+  /**
+   * @param Token $token
+   * @return Token
+   */
+  public static function coerceToString($token) {
+    $PRIMITIVE_TYPES = array(ExpType::$INT, ExpType::$FLOAT, ExpType::$STRING, ExpType::$BOOL, ExpType::$NULL);
+    $COMPOSITE_TYPES = array(ExpType::$ARRAY, ExpType::$OBJECT);
+    $type = $token->type;
+    if ($type == ExpType::$STRING) return $token;
+    $string = new Token(ExpType::$STRING);
+    if ($type == ExpType::$RAW) {
+      $string->value = strtr($token->value, ExpType::$ESCAPE_CHARS);
+    } elseif ($type == ExpType::$BOOL) {
+      $string->value = ($token->value) ? 'true' : 'false';
+    } elseif ($type == ExpType::$NULL) {
+      $string->value = 'null';
+    } elseif (in_array($type, $PRIMITIVE_TYPES)) {
+      $string->value = (string)($token->value);
+    } elseif (in_array($type, $COMPOSITE_TYPES)) {
+      $string->value = print_r($token->value, true); // maybe call .toString()?
+    } else {
+      throw new ExpTypeException("Unable to coerce token" . print_r($token, true) . " to string");
+    }
+    return $string;
+  }
+
+  /**
+   * @param Token $token
+   * @return Token
+   */
+  public static function coerceToBool($token) {
+    $PRIMITIVE_TYPES = array(ExpType::$INT, ExpType::$FLOAT, ExpType::$STRING, ExpType::$BOOL, ExpType::$NULL);
+    $COMPOSITE_TYPES = array(ExpType::$ARRAY, ExpType::$OBJECT);
+    $type = $token->type;
+    if ($type == ExpType::$BOOL) return $token;
+    $bool = new Token(ExpType::$BOOL);
+    if ($type == ExpType::$RAW) {
+      $bool->value = strtolower($token->value) == 'true' ? true : false;
+    } elseif (in_array($type, $PRIMITIVE_TYPES)) {
+      $bool->value = (bool)($token->value);
+    } elseif (in_array($type, $COMPOSITE_TYPES)) {
+      $bool->value = $token->value != null ? true : false;
+    } else {
+      throw new ExpTypeException("Unable to coerce token" . print_r($token, true) . " to bool");
+    }
+    return $bool;
+  }
+
+  /**
+   * @param Token $token
+   * @return Token
+   */
+  public static function coerceToNull($token) {
+    $COMPOSITE_TYPES = array(ExpType::$ARRAY, ExpType::$OBJECT);
+    $type = $token->type;
+    if ($type == ExpType::$NULL) return $token;
+    $null = new Token(ExpType::$NULL);
+    $value = $token->value;
+    if ($type == ExpType::$RAW && strtolower($value) == 'null' || in_array($type, $COMPOSITE_TYPES) && $token->value == null) {
+      $null->value = null;
+    } else {
+      throw new ExpTypeException("Unable to coerce token" . print_r($token, true) . " to null");
+    }
+    return $null;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpTypeException.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpTypeException.php
new file mode 100644
index 0000000..69d5fd9
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpTypeException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ExpTypeException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpressionException.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpressionException.php
new file mode 100644
index 0000000..a6a7719
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpressionException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ExpressionException extends \Exception {
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/ExpressionParser.php b/trunk/php/src/apache/shindig/gadgets/templates/ExpressionParser.php
new file mode 100644
index 0000000..ecf64d4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/ExpressionParser.php
@@ -0,0 +1,84 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Implementation of the os-template / os-data expression language, which is based on the JSP EL syntax
+ * For reference on the language see:
+ * JSP EL: https://jsp.dev.java.net/spec/jsp-2_1-fr-spec-el.pdf
+ * OS Templates: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml
+ * OS Data pipelining: http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Data-Pipelining.xml
+ *
+ */
+class ExpressionParser {
+
+  /**
+   * Evaluates the given $expression using the $dataContext as variable source.
+   *
+   * Internally the ExpressionParser uses a infix to postfix conversion to easily
+   * be able to evaluate mathematical expressions
+   *
+   * @param string $expression
+   * @param array $dataContext
+   * @return string evaluated result or an exception of failure
+   */
+  public static function evaluate($expression, $dataContext) {
+    $outputTokens = ExpLexer::process($expression);
+    $result = ExpParser::parse($outputTokens, $dataContext);
+    return $result->value;
+  }
+
+  /**
+   * Misc function to convert an array to string, the reason a plain implode() doesn't
+   * always work is because it'll complain about array to string conversions if
+   * the array contains array's as entries
+   *
+   * @param $array
+   * @return string
+   */
+  private static function arrayToString($array) {
+    foreach ($array as $key => $entry) {
+      if (is_array($entry)) {
+        $array[$key] = self::arrayToString($entry);
+      }
+    }
+    return implode(',', $array);
+  }
+
+  /**
+   * Returns the string value of the (mixed) $val, ie:
+   * on array, return "1, 2, 3, 4"
+   * on int, return "1"
+   * on string, return as is
+   *
+   * @param mixed $val
+   * @return string
+   */
+  public static function stringValue($val) {
+    if (is_array($val)) {
+      return self::arrayToString($val);
+    } elseif (is_numeric($val)) {
+      return (string)$val;
+    } else {
+      return $val;
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/PrimitiveExp.php b/trunk/php/src/apache/shindig/gadgets/templates/PrimitiveExp.php
new file mode 100644
index 0000000..858ee0f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/PrimitiveExp.php
@@ -0,0 +1,170 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class PrimitiveExp {
+
+  public $reason;
+  private $exp;
+  private $dataContext;
+
+  function __construct($reason) {
+    $this->reason = $reason;
+    $this->exp = array();
+  }
+
+  public function append($token) {
+    array_push($this->exp, $token);
+  }
+
+  public function evaluate($dataContext) {
+    $this->dataContext = $dataContext;
+    if ($this->reason->type == ExpType::$FUNCTION) {
+      $result = $this->evaluateFunction();
+    } else {
+      $result = $this->evaluateExpression();
+    }
+    return $result;
+  }
+
+  private function evaluateExpression() {
+    $OPERATOR_TYPES = array(ExpType::$UNARY_OP, ExpType::$BINARY_OP, ExpType::$DOT);
+    $PRIMITIVE_TYPES = array(ExpType::$INT, ExpType::$FLOAT, ExpType::$STRING, ExpType::$BOOL,
+        ExpType::$NULL);
+    $COMPOSITE_TYPES = array(ExpType::$ARRAY, ExpType::$OBJECT);
+    $OPERAND_TYPES = array_merge($PRIMITIVE_TYPES, $COMPOSITE_TYPES, array(ExpType::$IDENTITY));
+
+    $OPERATOR_PRECEDENCE = array('||' => 0, '&&' => 1, '==' => 2, '!=' => 2, '>' => 3,
+        '<' => 3, '>=' => 3, '<=' => 3, '+' => 4, ' - ' => 4, '*' => 5, '/' => 5, '%'  => 5,
+        ' -' => 6, '!' => 6, 'empty' => 6, '.'  => 7);
+
+    $operatorStack = array();
+    $operandStack = array();
+    foreach ($this->exp as $token) {
+      if (in_array($token->type, $OPERAND_TYPES)) {
+        array_push($operandStack, $token);
+      } elseif (in_array($token->type, $OPERATOR_TYPES)) {
+        $precedence = $OPERATOR_PRECEDENCE[$token->value];
+        while (! empty($operatorStack)) {
+          $previousPrecedence = $OPERATOR_PRECEDENCE[end($operatorStack)->value];
+          if ($precedence > $previousPrecedence || $precedence == $previousPrecedence && $token->type == ExpType::$UNARY_OP) {
+            break;
+          } else {
+            $operator = array_pop($operatorStack);
+            $operandStack = $this->compute($operandStack, $operator);
+          }
+        }
+        array_push($operatorStack, $token);
+      } else {
+        throw new ExpParserException("The expected primitive expression contains complex token: ", print_r($this->exp, true));
+      }
+    }
+    while ($operator = array_pop($operatorStack)) {
+      $operandStack = $this->compute($operandStack, $operator);
+    }
+    if (count($operandStack) != 1) {
+      throw new ExpParserException("Gramma error on the primitive expression: " . print_r($this->exp, true));
+    }
+    return $this->evaluateIdentity(array_pop($operandStack));
+  }
+
+  private function compute($operandStack, $operator) {
+    $ARITHMETIC_OPS = array('+', ' - ', '*', '/', '%'); // without unary operator ' -'
+    $RELATIONAL_OPS = array('==', '!=', '<', '>', '<=', '>=');
+    $LOGICAL_OPS = array('&&', '||'); // without '!'
+
+    $sym = $operator->value;
+    if ($operator->type == ExpType::$UNARY_OP) {
+      $operand = $this->evaluateIdentity(array_pop($operandStack));
+    } else {
+      $rhs = array_pop($operandStack);
+      $rhs = ($sym != '.') ? $this->evaluateIdentity($rhs) : $rhs;
+      $lhs = $this->evaluateIdentity(array_pop($operandStack));
+    }
+    if ($sym == '.') {
+      if ($lhs->type == ExpType::$NULL || $rhs->type == ExpType::$NULL) {  // Dealing with null type
+        $result = new Token(ExpType::$NULL, null);
+      } else {
+        if ($lhs->type == ExpType::$ARRAY) {
+          if ($rhs->type == ExpType::$INT || $rhs->type == ExpType::$STRING || $rhs->type == ExpType::$IDENTITY) $resval = isset($lhs->value[$rhs->value]) ? $lhs->value[$rhs->value] : null;
+          else throw new ExpParserException("Can't reference key typ " . print_r($rhs, true) . " on array");
+        } elseif ($lhs->type == ExpType::$OBJECT) {
+          if ($rhs->type == ExpType::$IDENTITY) eval('$resval = isset($lhs->value->' . $rhs->value . ')? $lhs->value->' . $rhs->value . ': null;');
+          else throw new ExpParserException("Can't reference key typ " . print_r($rhs, true) . " on object");
+        } else {
+          throw new ExpParserException("Can't perform ./[] operation on primitive type " . print_r($lhs, true));
+        }
+        $result = new Token(ExpType::detectType($resval), $resval);
+      }
+    } elseif ($sym == 'empty') {
+      $result = new Token(ExpType::$BOOL, empty($operand->value));
+    } elseif (in_array($sym, $ARITHMETIC_OPS)) {
+      $lhs = ExpType::coerceToNumber($lhs);
+      $rhs = ExpType::coerceToNumber($rhs);
+      eval('$resval = $lhs->value ' . $sym . '$rhs->value;');
+      $result = new Token(ExpType::detectType($resval), $resval);
+    } elseif ($sym == ' -') {  // Unary operator '-'
+      $result = ExpType::coerceToNumber($operand);
+      $result = new Token($result->type, -($result->value));
+    } elseif (in_array($sym, $RELATIONAL_OPS)) {
+      $result = new Token(ExpType::$BOOL);
+      // special case: one of the operator is null
+      if ($lhs->type == ExpType::$NULL && $rhs->type != ExpType::$NULL || $lhs->type != ExpType::$NULL && $rhs->type == ExpType::$NULL) $result->value = in_array($sym, array('<', '>', '<=', '>=', '==')) ? false : true;
+      eval('$result->value = $lhs->value ' . $sym . ' $rhs->value;');
+    } elseif (in_array($sym, $LOGICAL_OPS)) {
+      $result = new Token(ExpType::$BOOL);
+      $lhs = ExpType::coerceToBool($lhs);
+      $rhs = ExpType::coerceToBool($rhs);
+      eval('$result->value = $lhs->value ' . $sym . ' $rhs->value;');
+    } elseif ($sym == '!') {
+      $result = ExpType::coerceToBool($operand);
+      $result = new Token(ExpType::$BOOL, ! ($result->value));
+    } else {
+      throw new ExpParserException("Uncovered operator: " . $sym);
+    }
+    array_push($operandStack, $result);
+    return $operandStack;
+  }
+
+  private function evaluateIdentity($token) {
+    if ($token->type != ExpType::$IDENTITY) return $token;
+    foreach (array(false, 'Cur', 'My', 'Top') as $scope) {
+      $context = $scope ? $this->dataContext[$scope] : $this->dataContext;
+      if (isset($context[$token->value])) {
+        $val = $context[$token->value];
+        $newToken = new Token(ExpType::detectType($val), $val);
+        return $newToken;
+      }
+    }
+    throw new ExpParserException("Un-recogonized identity name in the data context: " . $token->value);
+  }
+
+  private function evaluateFunction() {
+    $FUNCTION_TRANS = array('osx:parseJson' => 'json_decode', 'osx:decodeBase64' => 'base64_decode',
+        'osx:urlEncode' => 'rawurlencode', 'osx:urlDecode' => 'rawurldecode');
+    $params = array();
+    foreach ($this->exp as $param)
+      array_push($params, $param->value);
+    $val = call_user_func_array($FUNCTION_TRANS[$this->reason->value], $params);
+    return new Token(ExpType::detectType($val), $val);
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/SwfConfig.php b/trunk/php/src/apache/shindig/gadgets/templates/SwfConfig.php
new file mode 100644
index 0000000..1efa147
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/SwfConfig.php
@@ -0,0 +1,56 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class SwfConfig {
+  public static $FLASH_VER = '9.0.115';
+  public static $PARAMS = array('loop', 'menu', 'quality', 'scale', 'salign', 'wmode', 'bgcolor',
+      'swliveconnect', 'flashvars', 'devicefont', 'allowscriptaccess', 'seamlesstabbing',
+      'allowfullscreen', 'allownetworking');
+  public static $ATTRS = array('id', 'name', 'styleclass', 'align');
+
+  /**
+   *
+   * @param array $swfConfig
+   * @param string $altContentId
+   * @param string $flashVars
+   * @return string
+   */
+  public static function buildSwfObjectCall($swfConfig, $altContentId, $flashVars = 'null') {
+    $params = SwfConfig::buildJsObj($swfConfig, SwfConfig::$PARAMS);
+    $attrs = SwfConfig::buildJsObj($swfConfig, SwfConfig::$ATTRS);
+    $flashVersion = SwfConfig::$FLASH_VER;
+    $swfObject = "swfobject.embedSWF(\"{$swfConfig['swf']}\", \"{$altContentId}\", \"{$swfConfig['width']}\", \"{$swfConfig['height']}\", \"{$flashVersion}\", null, {$flashVars}, {$params}, {$attrs});";
+    return $swfObject;
+  }
+
+  private static function buildJsObj($swfConfig, $keymap) {
+    $arr = array();
+    foreach ($swfConfig as $key => $value) {
+      if (in_array($key, $keymap)) {
+        $arr[] = "{$key}:\"{$value}\"";
+      }
+    }
+    $output = implode(",", $arr);
+    $output = '{' . $output . '}';
+    return $output;
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibrary.php b/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibrary.php
new file mode 100644
index 0000000..6dfb43f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibrary.php
@@ -0,0 +1,252 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+use apache\shindig\common\XmlError;
+use apache\shindig\common\Config;
+use apache\shindig\common\File;
+use apache\shindig\gadgets\GadgetContext;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Class that manages & loads template libraries (either inline, osml or external)
+ * See http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.13 for details
+ * Template libraries can be loaded from the gadget spec using:
+ * <Require feature="opensocial-templates">
+ *   <Param name="requireLibrary">http://www.example.com/templates.xml</Param>
+ * </Require>
+ */
+class TemplateLibrary {
+  /**
+   *
+   * @var array
+   */
+  private $osmlTags = array('os:Name', 'os:PeopleSelector', 'os:Badge');
+  /**
+   *
+   * @var array
+   */
+  private $templates = array();
+  /**
+   *
+   * @var boolean
+   */
+  private $osmlLoaded = false;
+  /**
+   *
+   * @var GadgetContext
+   */
+  private $gadgetContext;
+
+  /**
+   *
+   * @param GadgetContext $gadgetContext
+   */
+  public function __construct(GadgetContext $gadgetContext) {
+    $this->gadgetContext = $gadgetContext;
+  }
+
+  public function parseTemplate($tag, $caller) {
+    $template = $this->getTemplate($tag);
+    if ($template->dom) {
+      $templateDomCopy = new \DOMDocument(null, 'utf-8');
+      $templateDomCopy->preserveWhiteSpace = true;
+      $templateDomCopy->formatOutput = false;
+      $templateDomCopy->strictErrorChecking = false;
+      $templateDomCopy->recover = false;
+      $templateDomCopy->resolveExternals = false;
+      // If this template pulls in any new style and/or javascript, add those to the document
+      if ($style = $template->getStyle()) {
+        $styleNode = $templateDomCopy->createElement('style');
+        $styleNode->appendChild($templateDomCopy->createTextNode($style));
+        $templateDomCopy->appendChild($styleNode);
+      }
+      if ($script = $template->getScript()) {
+        $scriptNode = $templateDomCopy->createElement('script');
+        $scriptNode->setAttribute('type', 'text/javascript');
+        $scriptNode->appendChild($templateDomCopy->createCDATASection($script));
+        $templateDomCopy->appendChild($scriptNode);
+      }
+      // Copy the DOM structure since parseNode() modifies the DOM structure directly
+      $removeNodes = array();
+      foreach ($template->dom->childNodes as $node) {
+        $newNode = $templateDomCopy->importNode($node, true);
+        $newNode = $templateDomCopy->appendChild($newNode);
+        // Parse the template's DOM using our current data context (which includes the My context for templates)
+        if (($removeNode = $caller->parseNode($newNode)) !== false) {
+          $removeNodes[] = $removeNode;
+        }
+      }
+      foreach ($removeNodes as $removeNode) {
+        $removeNode->parentNode->removeChild($removeNode);
+      }
+      return $templateDomCopy;
+    }
+    return false;
+  }
+
+  /**
+   * Add template by DOMElement node, this function is primary called from
+   * the GadgetBaseRenderer class when it comes accross a script block with
+   * type=os-template & tag="some:name"
+   *
+   * @param DOMElement $node
+   */
+  public function addTemplateByNode(\DOMElement &$node, $scripts = false, $styles = false) {
+    $tag = $node->getAttribute('tag');
+    $template = new TemplateLibraryEntry($node);
+    if ($scripts) {
+      foreach ($scripts as $script) {
+        $template->addScript($script);
+      }
+    }
+    if ($styles) {
+      foreach ($styles as $style) {
+        $template->addstyle($style);
+      }
+    }
+    $this->templates[$tag] = $template;
+  }
+
+  /**
+   *
+   * @param DOMElement $node
+   * @param TemplateLibraryContent $globalScript
+   * @param TemplateLibraryContent $globalStyle
+   */
+  private function addTemplateDef(\DOMElement &$node, $globalScript, $globalStyle) {
+    $tag = $node->getAttribute('tag');
+    if (empty($tag)) {
+      throw new ExpressionException("Missing tag attribute on TemplateDef element");
+    }
+    $templateNodes = array();
+    foreach ($node->childNodes as $childNode) {
+      if (isset($childNode->tagName)) {
+        switch ($childNode->tagName) {
+          case 'Template':
+            $templateNodes[] = $childNode;
+            break;
+          case 'JavaScript':
+            $globalScript[] = new TemplateLibraryContent($childNode->nodeValue);
+            break;
+          case 'Style':
+            $globalStyle[] = new TemplateLibraryContent($childNode->nodeValue);
+            break;
+        }
+      }
+    }
+    // Initialize the templates after scanning the entire structure so that all scripts and styles will be included with each template
+    foreach ($templateNodes as $templateNode) {
+      $templateNode->setAttribute('tag', $tag);
+      $this->addTemplateByNode($templateNode, $globalScript, $globalStyle);
+    }
+  }
+
+  /**
+   * Add a template library set, for details see:
+   * http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.13
+   *
+   * @param string $library
+   */
+  public function addTemplateLibrary($library) {
+    libxml_use_internal_errors(true);
+    $doc = new \DOMDocument(null, 'utf-8');
+    $doc->preserveWhiteSpace = true;
+    $doc->formatOutput = false;
+    $doc->strictErrorChecking = false;
+    $doc->recover = false;
+    $doc->resolveExternals = false;
+    if (! $doc->loadXML($library)) {
+      throw new ExpressionException("Error parsing template library:\n" . XmlError::getErrors($library));
+    }
+    // Theoretically this could support multiple <Templates> root nodes, which isn't quite spec, but owell
+    foreach ($doc->childNodes as $rootNode) {
+      $templateNodes = array();
+      $globalScript = array();
+      $globalStyle = array();
+      if (isset($rootNode->tagName) && $rootNode->tagName == 'Templates') {
+        foreach ($rootNode->childNodes as $childNode) {
+          if (isset($childNode->tagName)) {
+            switch ($childNode->tagName) {
+              case 'TemplateDef':
+                $this->addTemplateDef($childNode, $globalScript, $globalStyle);
+                break;
+              case 'Template':
+                $templateNodes[] = $childNode;
+                break;
+              case 'JavaScript':
+                $globalScript[] = new TemplateLibraryContent($childNode->nodeValue);
+                break;
+              case 'Style':
+                $globalStyle[] = new TemplateLibraryContent($childNode->nodeValue);
+                break;
+            }
+          }
+        }
+      }
+      // Initialize the templates after scanning the entire structure so that all scripts and styles will be included with each template
+      foreach ($templateNodes as $templateNode) {
+        $this->addTemplateByNode($templateNode, $globalScript, $globalStyle);
+      }
+    }
+  }
+
+  /**
+   * Check to see if a template with name $tag exists
+   *
+   * @param string $tag
+   * @return boolean
+   */
+  public function hasTemplate($tag) {
+    if (in_array($tag, $this->osmlTags)) {
+      if (! $this->osmlLoaded) {
+        $this->loadOsmlLibrary();
+      }
+      return true;
+    }
+    return isset($this->templates[$tag]);
+  }
+
+  public function getTemplate($tag) {
+    if (! $this->hasTemplate($tag)) {
+      throw new ExpressionException("Invalid template tag");
+    }
+    return $this->templates[$tag];
+  }
+
+  /*
+   *
+   */
+  private function loadOsmlLibrary() {
+    $container = $this->gadgetContext->getContainer();
+    $containerConfig = $this->gadgetContext->getContainerConfig();
+    $gadgetsFeatures = $containerConfig->getConfig($container, 'gadgets.features');
+    if (! isset($gadgetsFeatures['osml'])) {
+      throw new ExpressionException("Missing OSML configuration key in config/config.js");
+    } elseif (! isset($gadgetsFeatures['osml']['library'])) {
+      throw new ExpressionException("Missing OSML.Library configuration key in config/config.js");
+    }
+    $osmlLibrary = Config::get('container_path') . str_replace('config/', '', $gadgetsFeatures['osml']['library']);
+    if (! File::exists($osmlLibrary)) {
+      throw new ExpressionException("Missing OSML Library ($osmlLibrary)");
+    }
+    $this->addTemplateLibrary(file_get_contents($osmlLibrary));
+    $this->osmlLoaded = true;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibraryContent.php b/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibraryContent.php
new file mode 100644
index 0000000..1dfcb5c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibraryContent.php
@@ -0,0 +1,35 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Scripts can be global per library set, so we assign the global script to each actual template
+ * and on calling it, the TemplateLibraryEntry will check to see if the content was already output
+ */
+class TemplateLibraryContent {
+  public $content;
+  public $included;
+
+  public function __construct($content) {
+    $this->content = $content;
+    $this->included = false;
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibraryEntry.php b/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibraryEntry.php
new file mode 100644
index 0000000..1b0c361
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/TemplateLibraryEntry.php
@@ -0,0 +1,88 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Misc class that holds the template information, an inline template
+ * will only contain a text blob (stored as parsed $dom node), however
+ * external and OSML library templates can also container script and
+ * style blocks
+ *
+ */
+class TemplateLibraryEntry {
+  public $dom;
+  public $style = array();
+  public $script = array();
+
+  public function __construct($dom = false) {
+    $this->dom = $dom;
+  }
+
+  /**
+   * Adds a javascript blob to this template
+   *
+   * @param unknown_type $script
+   */
+  public function addScript(TemplateLibraryContent $script) {
+    $this->script[] = $script;
+  }
+
+  /**
+   * Adds a style blob to this template
+   *
+   * @param unknown_type $style
+   */
+  public function addStyle(TemplateLibraryContent $style) {
+    $this->style[] = $style;
+  }
+
+  /**
+   * Returns the (combined, in inclusion  order) script text blob, or
+   * false if there's no javascript for this template
+   *
+   * @return javascript string or false
+   */
+  public function getScript() {
+    $ret = '';
+    foreach ($this->script as $script) {
+      if (! $script->included) {
+        $ret .= $script->content . "\n";
+      }
+    }
+    return ! empty($ret) ? $ret : false;
+  }
+
+  /**
+   * Returns the (combined, in inclusion  order) stylesheet text blob, or
+   * false if there's no style sheet associated with this template
+   *
+   * @return javascript string or false
+   */
+  public function getStyle() {
+    $ret = '';
+    foreach ($this->style as $style) {
+      if (! $style->included) {
+        $ret .= $style->content . "\n";
+      }
+    }
+    return ! empty($ret) ? $ret : false;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/TemplateParser.php b/trunk/php/src/apache/shindig/gadgets/templates/TemplateParser.php
new file mode 100644
index 0000000..dcdf4c4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/TemplateParser.php
@@ -0,0 +1,598 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\Config;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+//TODO support repeat tags on OSML tags, ie this should work: <os:Html repeat="${Bar}" />
+//TODO remove the os-templates javascript if all the templates are rendered on the server (saves many Kb's in gadget size)
+
+
+class TemplateParser {
+  /**
+   * @var array
+   */
+  private $dataContext;
+  /**
+   * @var TemplateLibrary
+   */
+  private $templateLibrary;
+
+  /**
+   * dumps a node to stdout
+   *
+   * @param DOMnode $node
+   * @param string $function
+   */
+  public function dumpNode($node, $function) {
+    $doc = new \DOMDocument(null, 'utf-8');
+    $doc->preserveWhiteSpace = true;
+    $doc->formatOutput = false;
+    $doc->strictErrorChecking = false;
+    $doc->recover = false;
+    $doc->resolveExternals = false;
+    if (! $newNode = @$doc->importNode($node, false)) {
+      echo "[Invalid node, dump failed]<br><br>";
+      return;
+    }
+    $doc->appendChild($newNode);
+    echo "<b>$function (" . get_class($node) . "):</b><br>" . htmlentities(str_replace('<?xml version="" encoding="utf-8"?>', '', $doc->saveXML()) . "\n") . "<br><br>";
+  }
+
+  /**
+   * Processes an os-template
+   *
+   * @param DOMnode $osTemplate
+   * @param array $dataContext
+   * @param TemplateLibrary $templateLibrary
+   */
+  public function process(\DOMnode &$osTemplate, $dataContext, TemplateLibrary $templateLibrary) {
+    $this->setDataContext($dataContext);
+    $this->templateLibrary = $templateLibrary;
+    if ($osTemplate instanceof \DOMElement) {
+      if (($removeNode = $this->parseNode($osTemplate)) !== false) {
+        $removeNode->parentNode->removeChild($removeNode);
+      }
+    }
+  }
+
+  /**
+   * Sets and initializes the data context to use while processing the template
+   *
+   * @param array $dataContext
+   */
+  private function setDataContext($dataContext) {
+    $this->dataContext = array();
+    $this->dataContext['Top'] = $dataContext;
+    $this->dataContext['Cur'] = array();
+    $this->dataContext['My'] = array();
+    $this->dataContext['Context'] = array('UniqueId' => uniqid());
+  }
+
+  /**
+   * returns the current datacontext, used mainly for testing purposes
+   *
+   * @return array
+   */
+  public function getDataContext() {
+    return $this->dataContext;
+  }
+
+  /**
+   * @param DOMNode $node
+   * @return boolean
+   */
+  public function parseNode(\DOMNode &$node) {
+    $removeNode = false;
+    if ($node instanceof \DOMText) {
+      if (! $node->isWhitespaceInElementContent() && ! empty($node->nodeValue)) {
+        $this->parseNodeText($node);
+      }
+    } else {
+      $tagName = isset($node->tagName) ? $node->tagName : '';
+      if (substr($tagName, 0, 3) == 'os:' || substr($tagName, 0, 4) == 'osx:') {
+        $removeNode = $this->parseOsmlNode($node);
+      } elseif ($this->templateLibrary->hasTemplate($tagName)) {
+        // the tag name refers to an existing template (myapp:EmployeeCard type naming)
+        // the extra check on the : character is to make sure this is a name spaced custom tag and not some one trying to override basic html tags (br, img, etc)
+        $this->parseLibrary($tagName, $node);
+      } else {
+        $removeNode = $this->parseNodeAttributes($node);
+      }
+    }
+    return is_object($removeNode) ? $removeNode : false;
+  }
+
+  /**
+   * Misc function that maps the node's attributes to a key => value array
+   * and results any expressions to actual values
+   *
+   * @param DOMElement $node
+   * @return array
+   */
+  private function nodeAttributesToScope(\DOMElement &$node) {
+    $myContext = array();
+    if ($node->hasAttributes()) {
+      foreach ($node->attributes as $attr) {
+        if (strpos($attr->value, '${') !== false) {
+          // attribute value contains an expression
+          $expressions = array();
+          preg_match_all('/(\$\{)(.*)(\})/imsxU', $attr->value, $expressions);
+          for ($i = 0; $i < count($expressions[0]); $i ++) {
+            $expression = $expressions[2][$i];
+            $myContext[$attr->name] = ExpressionParser::evaluate($expression, $this->dataContext);
+          }
+        } else {
+          // plain old string
+          $myContext[$attr->name] = trim($attr->value);
+        }
+      }
+    }
+    return $myContext;
+  }
+
+  /**
+   * Parses the specified template library
+   *
+   * @param string $tagName
+   * @param DOMNode $node
+   */
+  private function parseLibrary($tagName, \DOMNode &$node) {
+    // Set the My context based on the node's attributes
+    $myContext = $this->nodeAttributesToScope($node);
+
+    // Template call has child nodes, those are params that can be used in a os:Render call, store them
+    $oldNodeContext = isset($this->dataContext['_os_render_nodes']) ? $this->dataContext['_os_render_nodes'] : array();
+    $this->dataContext['_os_render_nodes'] = array();
+    if ($node->childNodes->length) {
+      foreach ($node->childNodes as $childNode) {
+        if (isset($childNode->tagName) && ! empty($childNode->tagName)) {
+          $nodeParam = ($pos = strpos($childNode->tagName, ':')) ? trim(substr($childNode->tagName, $pos + 1)) : trim($childNode->tagName);
+          $this->dataContext['_os_render_nodes'][$nodeParam] = $childNode;
+          $myContext[$nodeParam] = $this->nodeAttributesToScope($childNode);
+        }
+      }
+    }
+    // Parse the template library (store the My scope since this could be a nested call)
+    $previousMy = $this->dataContext['My'];
+    $this->dataContext['My'] = $myContext;
+    $ret = $this->templateLibrary->parseTemplate($tagName, $this);
+    $this->dataContext['My'] = $previousMy;
+    $this->dataContext['_os_render_nodes'] = $oldNodeContext;
+    if ($ret) {
+      // And replace the node with the parsed output
+      $ownerDocument = $node->ownerDocument;
+      foreach ($ret->childNodes as $childNode) {
+        $importedNode = $ownerDocument->importNode($childNode, true);
+        $importedNode = $node->parentNode->insertBefore($importedNode, $node);
+      }
+    }
+  }
+
+  /**
+   *
+   * @param DOMText $node
+   */
+  private function parseNodeText(\DOMText &$node) {
+    if (strpos($node->nodeValue, '${') !== false) {
+      $expressions = array();
+      preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->wholeText, $expressions);
+      for ($i = 0; $i < count($expressions[0]); $i ++) {
+        $toReplace = $expressions[0][$i];
+        $expression = $expressions[2][$i];
+        $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
+        $stringVal = htmlentities(ExpressionParser::stringValue($expressionResult), ENT_QUOTES, 'UTF-8');
+        $node->nodeValue = str_replace($toReplace, $stringVal, $node->nodeValue);
+      }
+    }
+  }
+
+  /**
+   *
+   * @param DOMNode $node
+   * @return DOMNode or false
+   */
+  private function parseNodeAttributes(\DOMNode &$node) {
+    if ($node->hasAttributes()) {
+      foreach ($node->attributes as $attr) {
+        if (strpos($attr->value, '${') !== false) {
+          $expressions = array();
+          preg_match_all('/(\$\{)(.*)(\})/imsxU', $attr->value, $expressions);
+          for ($i = 0; $i < count($expressions[0]); $i ++) {
+            $toReplace = $expressions[0][$i];
+            $expression = $expressions[2][$i];
+            $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
+            switch (strtolower($attr->name)) {
+
+              case 'repeat':
+                // Can only loop if the result of the expression was an array
+                if (! is_array($expressionResult)) {
+                  throw new ExpressionException("Can't repeat on a singular var");
+                }
+                // Make sure the repeat variable doesn't show up in the cloned nodes (otherwise it would infinit recurse on this->parseNode())
+                $node->removeAttribute('repeat');
+                // Is a named var requested?
+                $variableName = $node->getAttribute('var') ? trim($node->getAttribute('var')) : false;
+                // Store the current 'Cur', index and count state, we might be in a nested repeat loop
+                $previousCount = isset($this->dataContext['Context']['Count']) ? $this->dataContext['Context']['Count'] : null;
+                $previousIndex = isset($this->dataContext['Context']['Index']) ? $this->dataContext['Context']['Index'] : null;
+                $previousCur = $this->dataContext['Cur'];
+                // For information on the loop context, see http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.10.1
+                $this->dataContext['Context']['Count'] = count($expressionResult);
+                foreach ($expressionResult as $index => $entry) {
+                  if ($variableName) {
+                    // this is cheating a little since we're not putting it on the top level scope, the variable resolver will check 'Cur' first though so myVar.Something will still resolve correctly
+                    $this->dataContext['Cur'][$variableName] = $entry;
+                  }
+                  $this->dataContext['Cur'] = $entry;
+                  $this->dataContext['Context']['Index'] = $index;
+                  // Clone this node and it's children
+                  $newNode = $node->cloneNode(true);
+                  // Append the parsed & expanded node to the parent
+                  $newNode = $node->parentNode->insertBefore($newNode, $node);
+                  // And parse it (using the global + loop context)
+                  $this->parseNode($newNode, true);
+                }
+                // Restore our previous data context state
+                $this->dataContext['Cur'] = $previousCur;
+                if ($previousCount) {
+                  $this->dataContext['Context']['Count'] = $previousCount;
+                } else {
+                  unset($this->dataContext['Context']['Count']);
+                }
+                if ($previousIndex) {
+                  $this->dataContext['Context']['Index'] = $previousIndex;
+                } else {
+                  unset($this->dataContext['Context']['Index']);
+                }
+                return $node;
+                break;
+
+              case 'if':
+                if (! $expressionResult) {
+                  return $node;
+                } else {
+                  $node->removeAttribute('if');
+                }
+                break;
+
+              // These special cases that only apply for certain tag types
+              case 'selected':
+                if ($node->tagName == 'option') {
+                  if ($expressionResult) {
+                    $node->setAttribute('selected', 'selected');
+                  } else {
+                    $node->removeAttribute('selected');
+                  }
+                } else {
+                  throw new ExpressionException("Can only use selected on an option tag");
+                }
+                break;
+
+              case 'checked':
+                if ($node->tagName == 'input') {
+                  if ($expressionResult) {
+                    $node->setAttribute('checked', 'checked');
+                  } else {
+                    $node->removeAttribute('checked');
+                  }
+                } else {
+                  throw new ExpressionException("Can only use checked on an input tag");
+                }
+                break;
+
+              case 'disabled':
+                $disabledTags = array('input', 'button',
+                    'select', 'textarea');
+                if (in_array($node->tagName, $disabledTags)) {
+                  if ($expressionResult) {
+                    $node->setAttribute('disabled', 'disabled');
+                  } else {
+                    $node->removeAttribute('disabled');
+                  }
+                } else {
+                  throw new ExpressionException("Can only use disabled on input, button, select and textarea tags");
+                }
+                break;
+
+              default:
+                // On non os-template spec attributes, do a simple str_replace with the evaluated value
+                $stringVal = htmlentities(ExpressionParser::stringValue($expressionResult), ENT_QUOTES, 'UTF-8');
+                $newAttrVal = str_replace($toReplace, $stringVal, $attr->value);
+                $node->setAttribute($attr->name, $newAttrVal);
+                break;
+            }
+          }
+        }
+      }
+    }
+
+    // if a repeat attribute was found, don't recurse on it's child nodes, the repeat handling already did that
+    if (isset($node->childNodes) && $node->childNodes->length > 0) {
+      $removeNodes = array();
+      // recursive loop to all this node's children
+      foreach ($node->childNodes as $childNode) {
+        if (($removeNode = $this->parseNode($childNode)) !== false) {
+          $removeNodes[] = $removeNode;
+        }
+      }
+      if (count($removeNodes)) {
+        foreach ($removeNodes as $removeNode) {
+          $removeNode->parentNode->removeChild($removeNode);
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Function that handles the os: and osx: tags
+   *
+   * @param DOMNode $node
+   * @return DOMNode or false
+   */
+  private function parseOsmlNode(\DOMNode &$node) {
+    $tagName = strtolower($node->tagName);
+    if (! $this->checkIf($node)) {
+      // If the OSML tag contains an if attribute and the expression evaluates to false
+      // flag it for removal and don't process it
+      return $node;
+    }
+    switch ($tagName) {
+
+      /****** Control statements ******/
+
+      case 'os:repeat':
+        if (! $node->getAttribute('expression')) {
+          throw new ExpressionException("Invalid os:Repeat tag, missing expression attribute");
+        }
+        $expressions = array();
+        preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('expression'), $expressions);
+        $expression = $expressions[2][0];
+        $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
+        if (! is_array($expressionResult)) {
+          throw new ExpressionException("Can't repeat on a singular var");
+        }
+        // Store the current 'Cur', index and count state, we might be in a nested repeat loop
+        $previousCount = isset($this->dataContext['Context']['Count']) ? $this->dataContext['Context']['Count'] : null;
+        $previousIndex = isset($this->dataContext['Context']['Index']) ? $this->dataContext['Context']['Index'] : null;
+        $previousCur = $this->dataContext['Cur'];
+        // Is a named var requested?
+        $variableName = $node->getAttribute('var') ? trim($node->getAttribute('var')) : false;
+        // For information on the loop context, see http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Templating.xml#rfc.section.10.1
+        $this->dataContext['Context']['Count'] = count($expressionResult);
+        foreach ($expressionResult as $index => $entry) {
+          if ($variableName) {
+            // this is cheating a little since we're not putting it on the top level scope, the variable resolver will check 'Cur' first though so myVar.Something will still resolve correctly
+            $this->dataContext['Cur'][$variableName] = $entry;
+          }
+          $this->dataContext['Cur'] = $entry;
+          $this->dataContext['Context']['Index'] = $index;
+          foreach ($node->childNodes as $childNode) {
+            $newNode = $childNode->cloneNode(true);
+            $newNode = $node->parentNode->insertBefore($newNode, $node);
+            $this->parseNode($newNode);
+          }
+        }
+        // Restore our previous data context state
+        $this->dataContext['Cur'] = $previousCur;
+        if ($previousCount) {
+          $this->dataContext['Context']['Count'] = $previousCount;
+        } else {
+          unset($this->dataContext['Context']['Count']);
+        }
+        if ($previousIndex) {
+          $this->dataContext['Context']['Index'] = $previousIndex;
+        } else {
+          unset($this->dataContext['Context']['Index']);
+        }
+        return $node;
+        break;
+
+      case 'os:if':
+        $expressions = array();
+        if (! $node->getAttribute('condition')) {
+          throw new ExpressionException("Invalid os:If tag, missing condition attribute");
+        }
+        preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('condition'), $expressions);
+        if (! count($expressions[2])) {
+          throw new ExpressionException("Invalid os:If tag, missing condition expression");
+        }
+        $expression = $expressions[2][0];
+        $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
+        if ($expressionResult) {
+          foreach ($node->childNodes as $childNode) {
+            $newNode = $childNode->cloneNode(true);
+            $this->parseNode($newNode);
+            $newNode = $node->parentNode->insertBefore($newNode, $node);
+          }
+        }
+        return $node;
+        break;
+
+      /****** OSML tags (os: name space) ******/
+
+      case 'os:name':
+        $this->parseLibrary('os:Name', $node);
+        return $node;
+        break;
+
+      case 'os:badge':
+        $this->parseLibrary('os:Badge', $node);
+        return $node;
+        break;
+
+      case 'os:peopleselector':
+        $this->parseLibrary('os:PeopleSelector', $node);
+        return $node;
+        break;
+
+      case 'os:html':
+        if (! $node->getAttribute('code')) {
+          throw new ExpressionException("Invalid os:Html tag, missing code attribute");
+        }
+        preg_match_all('/(\$\{)(.*)(\})/imsxU', $node->getAttribute('code'), $expressions);
+        if (count($expressions[2])) {
+          $expression = $expressions[2][0];
+          $code = ExpressionParser::evaluate($expression, $this->dataContext);
+        } else {
+          $code = $node->getAttribute('code');
+        }
+        $node->parentNode->insertBefore($node->ownerDocument->createTextNode($code), $node);
+
+        return $node;
+        break;
+
+      case 'os:render':
+        if (! ($content = $node->getAttribute('content'))) {
+          throw new ExpressionException("os:Render missing attribute: content");
+        }
+        $content = $node->getAttribute('content');
+        if (! isset($this->dataContext['_os_render_nodes'][$content])) {
+          throw new ExpressionException("os:Render, Unknown entry: " . htmlentities($content));
+        }
+        $nodes = $this->dataContext['_os_render_nodes'][$content];
+        $ownerDocument = $node->ownerDocument;
+        // Only parse the child nodes of the dom tree and not the (myapp:foo) top level element
+        foreach ($nodes->childNodes as $childNode) {
+          $importedNode = $ownerDocument->importNode($childNode, true);
+          $importedNode = $node->parentNode->insertBefore($importedNode, $node);
+          $this->parseNode($importedNode);
+        }
+        return $node;
+        break;
+
+      /****** Extension - Tags ******/
+
+      case 'os:flash':
+        // handle expressions
+        $this->parseNodeAttributes($node);
+
+        // read swf config from attributes
+        $swfConfig = array('width' => '100px',
+            'height' => '100px', 'play' => 'immediate');
+        foreach ($node->attributes as $attr) {
+          $swfConfig[$attr->name] = $attr->value;
+        }
+
+        // attach security token in the flash var
+        $st = 'st=' . BasicSecurityToken::getTokenStringFromRequest();
+        if (array_key_exists('flashvars', $swfConfig)) {
+          $swfConfig['flashvars'] = $swfConfig['flashvars'] . '&' . $st;
+        } else {
+          $swfConfig['flashvars'] = $st;
+        }
+
+        // Restrict the content if sanitization is enabled
+        $sanitizationEnabled = Config::get('sanitize_views');
+        if ($sanitizationEnabled) {
+          $swfConfig['allowscriptaccess'] = 'never';
+          $swfConfig['swliveconnect'] = 'false';
+          $swfConfig['allownetworking'] = 'internal';
+        }
+
+        // Generate unique id for this swf
+        $ALT_CONTENT_PREFIX = 'os_Flash_alt_';
+        $altContentId = uniqid($ALT_CONTENT_PREFIX);
+
+        // Create a div wrapper around the provided alternate content, and add the alternate content to the holder
+        $altHolder = $node->ownerDocument->createElement('div');
+        $altHolder->setAttribute('id', $altContentId);
+        foreach ($node->childNodes as $childNode) {
+          $altHolder->appendChild($childNode);
+        }
+        $node->parentNode->insertBefore($altHolder, $node);
+
+        // Create the call to swfobject in header
+        $scriptCode = SwfConfig::buildSwfObjectCall($swfConfig, $altContentId);
+        $scriptBlock = $node->ownerDocument->createElement('script');
+        $scriptBlock->setAttribute('type', 'text/javascript');
+        $node->parentNode->insertBefore($scriptBlock, $node);
+        if ($swfConfig['play'] != 'immediate') {
+          // Add onclick handler to trigger call to swfobject
+          $scriptCode = "function {$altContentId}()\{{$scriptCode};\}";
+          $altHolder->setAttribute('onclick', "{$altContentId}()");
+        }
+        $scriptCodeNode = $node->ownerDocument->createTextNode($scriptCode);
+        $scriptBlock->appendChild($scriptCodeNode);
+        return $node;
+        break;
+      case 'os:var':
+        // handle expressions
+        $this->parseNodeAttributes($node);
+
+        if (! ($key = $node->getAttribute('key'))) {
+          throw new ExpressionException("os:Var missing attribute: key");
+        }
+
+        // either get value from attribute
+        if (! ($value = $node->getAttribute('value'))) {
+          $value = '';
+        }
+
+        // or from inner text of node
+        if (! $value && $node->textContent) {
+          $value = $node->textContent;
+        }
+
+        // try to decode if the value is a valid json object
+        $parsedValue = json_decode($value, true);
+
+        if ($parsedValue) {
+          $value = $parsedValue;
+        }
+
+        $this->dataContext['Top'][$key] = $value;
+
+        return $node;
+        break;
+      case 'osx:navigatetoapp':
+        break;
+
+      case 'osx:navigatetoperson':
+        break;
+    }
+    return false;
+  }
+
+  /**
+   * Misc function that checks if the OSML tag $node has an if attribute, returns
+   * true if the expression is true or no if attribute is set
+   *
+   * @param DOMElement $node
+   * @return boolean
+   */
+  private function checkIf(\DOMElement &$node) {
+    if (($if = $node->getAttribute('if'))) {
+      $expressions = array();
+      preg_match_all('/(\$\{)(.*)(\})/imsxU', $if, $expressions);
+      if (! count($expressions[2])) {
+        throw new ExpressionException("Invalid os:If tag, missing condition expression");
+      }
+      $expression = $expressions[2][0];
+      $expressionResult = ExpressionParser::evaluate($expression, $this->dataContext);
+      return $expressionResult ? true : false;
+    }
+    return true;
+  }
+}
+
+
diff --git a/trunk/php/src/apache/shindig/gadgets/templates/Token.php b/trunk/php/src/apache/shindig/gadgets/templates/Token.php
new file mode 100644
index 0000000..affbb13
--- /dev/null
+++ b/trunk/php/src/apache/shindig/gadgets/templates/Token.php
@@ -0,0 +1,43 @@
+<?php
+namespace apache\shindig\gadgets\templates;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class Token {
+  /**
+   * @var string
+   */
+  public $type;
+
+  /**
+   * @var mixed
+   */
+  public $value;
+
+  /**
+   *
+   * @param string $type
+   * @param mixed $value
+   */
+  function __construct($type, $value = null) {
+    $this->type = $type;
+    $this->value = $value;
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/social/converters/InputActivitiesConverter.php b/trunk/php/src/apache/shindig/social/converters/InputActivitiesConverter.php
new file mode 100644
index 0000000..afa0b5e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputActivitiesConverter.php
@@ -0,0 +1,42 @@
+<?php
+namespace apache\shindig\social\converters;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InputActivitiesConverter extends InputConverter
+{
+    public function convertAtom($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertActivities($xml, $xml->content->activity);
+    }
+
+    public function convertJson($requestParam) {
+        $ret = json_decode($requestParam, true);
+        if ($ret == $requestParam) {
+          throw new \Exception("Mallformed activity json string");
+        }
+        return $ret;
+    }
+
+    public function convertXml($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertActivities($xml, $xml->activity);
+    }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputAlbumsConverter.php b/trunk/php/src/apache/shindig/social/converters/InputAlbumsConverter.php
new file mode 100644
index 0000000..6b08bff
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputAlbumsConverter.php
@@ -0,0 +1,42 @@
+<?php
+namespace apache\shindig\social\converters;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InputAlbumsConverter extends InputConverter
+{
+    public function convertAtom($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertAlbums($xml, $xml->content->album);
+    }
+
+    public function convertJson($requestParam) {
+        $ret = json_decode($requestParam, true);
+        if ($ret == $requestParam) {
+          throw new \Exception("Mallformed album json string. " . $requestParam);
+        }
+        return $ret;
+    }
+
+    public function convertXml($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertAlbums($xml, $xml);
+    }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputAppDataConverter.php b/trunk/php/src/apache/shindig/social/converters/InputAppDataConverter.php
new file mode 100644
index 0000000..5394f27
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputAppDataConverter.php
@@ -0,0 +1,58 @@
+<?php
+namespace apache\shindig\social\converters;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InputAppDataConverter extends InputConverter
+{
+    public function convertAtom($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        if (! isset($xml->content) || ! isset($xml->content->appdata)) {
+          throw new \Exception("Mallformed AppData xml");
+        }
+        $data = array();
+        foreach (get_object_vars($xml->content->appdata) as $key => $val) {
+          $data[trim($key)] = trim($val);
+        }
+        return $data;
+    }
+
+    public function convertJson($requestParam) {
+        $ret = json_decode($requestParam, true);
+        if ($ret == $requestParam) {
+          throw new \Exception("Mallformed app data json string");
+        }
+        return $ret;
+    }
+
+    public function convertXml($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        if (! isset($xml->entry)) {
+          throw new \Exception("Mallformed AppData xml");
+        }
+        $data = array();
+        foreach ($xml->entry as $entry) {
+          $key = trim($entry->key);
+          $val = isset($entry->value) ? trim($entry->value) : null;
+          $data[$key] = $val;
+        }
+        return $data;
+    }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputBasicXmlConverter.php b/trunk/php/src/apache/shindig/social/converters/InputBasicXmlConverter.php
new file mode 100644
index 0000000..7e4a6c9
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputBasicXmlConverter.php
@@ -0,0 +1,199 @@
+<?php
+namespace apache\shindig\social\converters;
+use apache\shindig\social\model\MediaItem;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Basic methods for converting Atom and XML
+ */
+class InputBasicXmlConverter {
+
+  public static function loadString($requestParam, $namespace = null) {
+    return simplexml_load_string($requestParam, 'SimpleXMLElement', LIBXML_NOCDATA, $namespace);
+  }
+
+  public static function convertActivities($xml, $activityXml) {
+    $activity = array();
+    if (! isset($xml->title)) {
+      throw new \Exception("Mallformed activity xml");
+    }
+    // remember to either type cast to (string) or trim() the string so we don't get 
+    // SimpleXMLString types in the internal data representation. I often prefer
+    // using trim() since it cleans up the data too
+    $activity['id'] = isset($xml->id) ? trim($xml->id) : '';
+    $activity['title'] = trim($xml->title);
+    $activity['body'] = isset($xml->summary) ? trim($xml->summary) : '';
+    $activity['streamTitle'] = isset($activityXml->streamTitle) ? trim($activityXml->streamTitle) : '';
+    $activity['streamId'] = isset($activityXml->streamId) ? trim($activityXml->streamId) : '';
+    $activity['updated'] = isset($xml->updated) ? trim($xml->updated) : '';
+    if (isset($activityXml->mediaItems)) {
+      $activity['mediaItems'] = array();
+      foreach ($activityXml->mediaItems->MediaItem as $mediaItem) {
+        $item = array();
+        if (! isset($mediaItem->type) || ! isset($mediaItem->mimeType) || ! isset($mediaItem->url)) {
+          throw new \Exception("Invalid media item in activity xml");
+        }
+        $item['type'] = trim($mediaItem->type);
+        $item['mimeType'] = trim($mediaItem->mimeType);
+        $item['url'] = trim($mediaItem->url);
+        $activity['mediaItems'][] = $item;
+      }
+    }
+    return $activity;
+  }
+  
+  public static function convertAlbums($xml, $albumXml) {
+    $fields = array('id', 'description', 'mediaItemCount', 'thumbnailUrl', 'ownerId', 'mediaMimeType');
+    $album = self::copyFields($albumXml, $fields);
+    if (isset($xml->title) && !empty($xml->title)) {
+      $album['title'] = trim($xml->title);
+    } else if (isset($albumXml->caption)) {
+      $album['title'] = trim($albumXml->caption); 
+    }
+    if (isset($albumXml->mediaType) && in_array(strtoupper(trim($albumXml->mediaType)), MediaItem::$TYPES)) {
+      $album['mediaType'] = strtoupper(trim($albumXml->mediaType));
+    }
+    if (isset($albumXml->location)) {
+      $address = self::convertAddresses($albumXml->location);      
+      if ($address) {
+        $album['location'] = $address;
+      }
+    }
+    return $album;
+  }
+  
+  public static function convertMediaItems($xml, $mediaItemXml) {
+    $fields = array('albumId', 'created', 'description', 'duration', 'fileSize', 'id', 'language',
+      'lastUpdated', 'mimeType', 'numComments', 'numViews', 'numVotes', 'rating',
+      'startTime', 'taggedPeople', 'tags', 'thumbnailUrl', 'url');
+    $mediaItem = self::copyFields($mediaItemXml, $fields);
+    if (isset($xml->title) && !empty($xml->title)) {
+      $mediaItem['title'] = trim($xml->title);
+    } else if (isset($mediaItemXml->caption)) {
+      $mediaItem['title'] = trim($mediaItemXml->caption);
+    }
+    if (isset($mediaItemXml->type) && in_array(strtoupper(trim($mediaItemXml->type)), MediaItem::$TYPES)) {
+      $mediaItem['type'] = strtoupper(trim($mediaItemXml->type));
+    }
+    if (isset($mediaItemXml->location)) {
+      $address = self::convertAddresses($mediaItemXml->location);      
+      if ($address) {
+        $mediaItem['location'] = $address;
+      }
+    }
+    return $mediaItem;
+  }
+  
+  public static function convertAddresses($xml) {
+    $fields = array('country', 'extendedAddress', 'latitude', 'locality', 'longitude', 'poBox',
+      'postalCode', 'region', 'streetAddress', 'type', 'unstructuredAddress', 'formatted');
+    return self::copyFields($xml, $fields);
+  }
+  
+  public static function copyFields($xml, $fields) {
+    $object = array();
+    if (!is_array($fields)) {
+      $fields = array($fields);
+    }
+    foreach ($fields as $field) {
+      if ($xml && isset($xml->$field)) {
+        $object[$field] = trim($xml->$field);
+      }
+    }
+    return $object;
+  }
+  
+  public static function convertMessages($requestParam, $xml, $content) {
+    // As only message handler has the context to know whether it's a message or a message
+    // collection request. All the fields for both the Message and the MessageCollection
+    // classes are converted here. Message handler has the responsibility to validate the
+    // params.
+    $message = array();
+    if (isset($xml->id)) {
+      $message['id'] = trim($xml->id);
+    }
+    if (isset($xml->title)) {
+      $message['title'] = trim($xml->title);
+    }
+    if (!empty($content)) {
+      $message['body'] = trim($content);
+    }
+    if (isset($xml->bodyId)) {
+      $meesage['bodyId'] = trim($xml->bodyId);
+    }
+    if (isset($xml->titleId)) {
+      $message['titleId'] = trim($xml->titleId);
+    }
+    if (isset($xml->appUrl)) {
+      $message['appUrl'] = trim($xml->appUrl);
+    }
+    if (isset($xml->status)) {
+      $message['status'] = trim($xml->status);
+    }
+    if (isset($xml->timeSent)) {
+      $message['timeSent'] = trim($xml->timeSent);
+    }
+    if (isset($xml->type)) {
+      $message['type'] = trim($xml->type);
+    }
+    if (isset($xml->updated)) {
+      $message['updated'] = trim($xml->updated);
+    }
+    if (isset($xml->senderId)) {
+      $message['senderId'] = trim($xml->senderId);
+    }
+    if (isset($xml->appUrl)) {
+      $message['appUrl'] = trim($xml->appUrl);
+    }
+    if (isset($xml->collectionIds)) {
+      $message['collectionIds'] = array();
+      foreach ($xml->collectionIds as $collectionId) {
+        $message['collectionIds'][] = trim($collectionId);
+      }
+    }
+    
+    // Tries to retrieve recipients by looking at the osapi name space first then
+    // the default namespace.
+    $recipientXml = self::loadString($requestParam, "http://opensocial.org/2008/opensocialapi");
+    if (empty($recipientXml) || !isset($recipientXml->recipient)) {
+      $recipientXml = $xml;
+    }
+    
+    if (isset($recipientXml->recipient)) {
+      $message['recipients'] = array();
+      foreach ($recipientXml->recipient as $recipient) {
+        $message['recipients'][] = trim($recipient);
+      }
+    }
+    
+    // TODO: Parses the inReplyTo, replies and urls fields.
+    
+    // MessageCollection specified fiedls.
+    if (isset($xml->total)) {
+      $message['total'] = trim($xml->total);
+    }
+    if (isset($xml->unread)) {
+      $message['unread'] = trim($xml->unread);
+    }
+    
+    return $message;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputConverter.php b/trunk/php/src/apache/shindig/social/converters/InputConverter.php
new file mode 100644
index 0000000..e4bb8a9
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputConverter.php
@@ -0,0 +1,38 @@
+<?php
+namespace apache\shindig\social\converters;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Abstract class for the conversion of the RESTful API input
+ * Since the data layout between json and atom is completely
+ * different (since the structure in atom has a atom meaning
+ * and a social data meaning), we have the need to put the
+ * hoisting rules somewhere..
+ */
+abstract class InputConverter {
+
+  abstract public function convertAtom($requestParam);
+
+  abstract public function convertJson($requestParam);
+
+  abstract public function convertXml($requestParam);
+
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputInvalidateConverter.php b/trunk/php/src/apache/shindig/social/converters/InputInvalidateConverter.php
new file mode 100644
index 0000000..3efa5c4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputInvalidateConverter.php
@@ -0,0 +1,37 @@
+<?php
+namespace apache\shindig\social\converters;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InputInvalidateConverter extends InputConverter
+{
+    public function convertAtom($requestParam) {
+        return $this->convertJson($requestParam);
+    }
+
+    public function convertJson($requestParam) {
+        // parameters for invalidate route are always in json format
+        return json_decode($requestParam, true);
+    }
+
+    public function convertXml($requestParam) {
+        return $this->convertJson($requestParam);
+    }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputMediaItemsConverter.php b/trunk/php/src/apache/shindig/social/converters/InputMediaItemsConverter.php
new file mode 100644
index 0000000..8a63894
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputMediaItemsConverter.php
@@ -0,0 +1,44 @@
+<?php
+namespace apache\shindig\social\converters;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InputMediaItemsConverter extends InputConverter
+{
+    public function convertAtom($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertMediaItems($xml, $xml->content->mediaItem);
+    }
+
+    public function convertJson($requestParam) {
+        $ret = json_decode($requestParam, true);
+        if ($ret == $requestParam) {
+            // The content upload specification allows the content-type in the post
+            // body to be the binary data of the content.
+            return null;
+        }
+        return $ret;
+    }
+
+    public function convertXml($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertMediaItems($xml, $xml);
+    }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputMessagesConverter.php b/trunk/php/src/apache/shindig/social/converters/InputMessagesConverter.php
new file mode 100644
index 0000000..d5eee66
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputMessagesConverter.php
@@ -0,0 +1,42 @@
+<?php
+namespace apache\shindig\social\converters;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InputMessagesConverter extends InputConverter
+{
+    public function convertAtom($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertMessages($requestParam, $xml, $xml->content);
+    }
+
+    public function convertJson($requestParam) {
+        $ret = json_decode($requestParam, true);
+        if ($ret == $requestParam) {
+          throw new \Exception("Mallformed message string");
+        }
+        return $ret;
+    }
+
+    public function convertXml($requestParam) {
+        $xml = InputBasicXmlConverter::loadString($requestParam);
+        return InputBasicXmlConverter::convertMessages($requestParam, $xml, $xml->body);
+    }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/InputPeopleConverter.php b/trunk/php/src/apache/shindig/social/converters/InputPeopleConverter.php
new file mode 100644
index 0000000..6b08ecc
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/InputPeopleConverter.php
@@ -0,0 +1,38 @@
+<?php
+namespace apache\shindig\social\converters;
+use apache\shindig\social\service\SocialSpiException;
+use apache\shindig\social\service\ResponseError;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InputPeopleConverter extends InputConverter
+{
+    public function convertAtom($requestParam) {
+        throw new SocialSpiException("Operation not supported", ResponseError::$NOT_IMPLEMENTED);
+    }
+
+    public function convertJson($requestParam) {
+        throw new SocialSpiException("Operation not supported", ResponseError::$NOT_IMPLEMENTED);
+    }
+
+    public function convertXml($requestParam) {
+        throw new SocialSpiException("Operation not supported", ResponseError::$NOT_IMPLEMENTED);
+    }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/OutputAtomConverter.php b/trunk/php/src/apache/shindig/social/converters/OutputAtomConverter.php
new file mode 100644
index 0000000..648a310
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/OutputAtomConverter.php
@@ -0,0 +1,229 @@
+<?php
+namespace apache\shindig\social\converters;
+use apache\shindig\social\service\RestRequestItem;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\social\spi\RestfulCollection;
+use apache\shindig\social\model\Activity;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Format = atom output converter, for format definition see:
+ * http://sites.google.com/a/opensocial.org/opensocial/Technical-Resources/opensocial-spec-v08/restful-api-specification
+ */
+class OutputAtomConverter extends OutputConverter {
+  private static $nameSpace = 'http://www.w3.org/2005/Atom';
+  private static $osNameSpace = 'http://ns.opensocial.org/2008/opensocial';
+  private static $xmlVersion = '1.0';
+  private static $charSet = 'UTF-8';
+  private static $formatOutput = true;
+  // this maps the REST url to the atom content type
+  private static $entryTypes = array('people' => 'entry', 'appdata' => 'entry',
+      'activities' => 'entry', 'messages' => 'entry');
+  private $doc;
+
+  /**
+   *
+   * @param ResponseItem $responseItem
+   * @param RestRequestItem $requestItem
+   */
+  function outputResponse(ResponseItem $responseItem, RestRequestItem $requestItem) {
+    $doc = $this->createAtomDoc();
+    $requestType = $this->getRequestType($requestItem);
+    $data = $responseItem->getResponse();
+    $params = $requestItem->getParameters();
+    $userId = isset($params['userId']) ? $params['userId'][0] : '';
+    $guid = 'urn:guid:' . $userId;
+    $authorName = $_SERVER['HTTP_HOST'] . ':' . $userId;
+    $updatedAtom = date(DATE_ATOM);
+
+    // Check to see if this is a single entry, or a collection, and construct either an atom
+    // feed (collection) or an entry (single)
+    if ($data instanceof RestfulCollection) {
+      $totalResults = $data->getTotalResults();
+      $itemsPerPage = $requestItem->getCount();
+      $startIndex = $requestItem->getStartIndex();
+
+      // The root Feed element
+      $entry = $this->addNode($doc, 'feed', '', false, self::$nameSpace);
+
+      // Required Atom fields
+      $endPos = ($startIndex + $itemsPerPage) > $totalResults ? $totalResults : ($startIndex + $itemsPerPage);
+      $this->addNode($entry, 'title', $requestType . ' feed for id ' . $authorName . ' (' . $startIndex . ' - ' . ($endPos - 1) . ' of ' . $totalResults . ')');
+      $author = $this->addNode($entry, 'author');
+      $this->addNode($author, 'uri', $guid);
+      $this->addNode($author, 'name', $authorName);
+      $this->addNode($entry, 'updated', $updatedAtom);
+      $this->addNode($entry, 'id', $guid);
+      $this->addNode($entry, 'link', '', array('rel' => 'self',
+          'href' => 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']));
+      // Add osearch & next link to the entry
+      $this->addPagingFields($entry, $startIndex, $itemsPerPage, $totalResults);
+      // Add response entries to feed
+      $responses = $data->getEntry();
+      foreach ($responses as $response) {
+        // Attempt to have a real ID field, otherwise we fall back on the idSpec id
+        $idField = is_object($response) && isset($response->id) ? $response->id : (is_array($response) && isset($response['id']) ? $response['id'] : $requestItem->getUser()->getUserId($requestItem->getToken()));
+        // construct <entry> blocks this record
+        $feedEntry = $this->addNode($entry, 'entry');
+        $content = $this->addNode($feedEntry, 'content', '', array(
+            'type' => 'application/xml'));
+        // Author node
+        $author = $this->addNode($feedEntry, 'author');
+        $this->addNode($author, 'uri', $guid);
+        $this->addNode($author, 'name', $authorName);
+        // Special hoisting rules for activities
+        if ($response instanceof Activity) {
+          $this->addNode($feedEntry, 'category', '', array('term' => 'status'));
+          $this->addNode($feedEntry, 'updated', date(DATE_ATOM, $response->postedTime));
+          $this->addNode($feedEntry, 'id', 'urn:guid:' . $response->id);
+          //FIXME should add a link field but don't have URL's available yet:
+          // <link rel="self" type="application/atom+xml" href="http://api.example.org/activity/feeds/.../af3778"/>
+          $this->addNode($feedEntry, 'title', strip_tags($response->title));
+          $this->addNode($feedEntry, 'summary', $response->body);
+          // Unset them so addData doesn't include them again
+          unset($response->postedTime);
+          unset($response->id);
+          unset($response->title);
+          unset($response->body);
+        } else {
+          $this->addNode($feedEntry, 'id', 'urn:guid:' . $idField);
+          $this->addNode($feedEntry, 'title', $requestType . ' feed entry for id ' . $idField);
+          $this->addNode($feedEntry, 'updated', $updatedAtom);
+        }
+
+        // recursively add responseItem data to the xml structure
+        $this->addData($content, $requestType, $response, self::$osNameSpace);
+      }
+    } else {
+      // Single entry = Atom:Entry
+      $entry = $doc->appendChild($doc->createElementNS(self::$nameSpace, "entry"));
+      // Atom fields
+      $this->addNode($entry, 'title', $requestType . ' entry for ' . $authorName);
+      $author = $this->addNode($entry, 'author');
+      $this->addNode($author, 'uri', $guid);
+      $this->addNode($author, 'name', $authorName);
+      $this->addNode($entry, 'id', $guid);
+      $this->addNode($entry, 'updated', $updatedAtom);
+      $content = $this->addNode($entry, 'content', '', array('type' => 'application/xml'));
+      // addData loops through the responseItem data recursively creating a matching XML structure
+      $this->addData($content, $requestType, $data['entry'], self::$osNameSpace);
+    }
+    $xml = $doc->saveXML();
+    if ($responseItem->getResponse() instanceof RestfulCollection) {
+      //FIXME dirty hack until i find a way to add multiple name spaces using DomXML functions
+      $xml = str_replace('<feed xmlns="http://www.w3.org/2005/Atom">', '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:osearch="http://a9.com/-/spec/opensearch/1.1">', $xml);
+    }
+    echo $xml;
+  }
+
+  /**
+   *
+   * @param array $responses
+   * @param SecurityToken $token
+   * @throws Exception
+   */
+  function outputBatch(Array $responses, SecurityToken $token) {
+    throw new \Exception("Atom batch not supported");
+  }
+
+  /**
+   * Easy shortcut for creating & appending XML nodes
+   *
+   * @param DOMElement $node node to append the new child node too
+   * @param string $name name of the new element
+   * @param string $value value of the element, if empty no text node is created
+   * @param array $attributes optional array of attributes, false by default. If set attributes are added to the node using the key => val pairs
+   * @param string $nameSpace optional namespace to use when creating node
+   * @return DOMElement node
+   */
+  private function addNode($node, $name, $value = '', $attributes = false, $nameSpace = false) {
+    return OutputBasicXmlConverter::addNode($this->doc, $node, $name, $value, $attributes, $nameSpace);
+  }
+
+  /**
+   * Adds the osearch fields & generates a next link if result set > itemsPerPage
+   *
+   * @param DOMElement $entry the entry DOMElement to append the links too
+   * @param int $startIndex
+   * @param int $itemsPerPage
+   * @param int $totalResults
+   */
+  private function addPagingFields($entry, $startIndex, $itemsPerPage, $totalResults) {
+    $this->addNode($entry, 'osearch:totalResults', $totalResults);
+    $this->addNode($entry, 'osearch:startIndex', $startIndex ? $startIndex : '0');
+    $this->addNode($entry, 'osearch:itemsPerPage', $itemsPerPage);
+    // Create a 'next' link based on our current url if this is a pageable collection & there is more to display
+    if (($startIndex + $itemsPerPage) < $totalResults) {
+      $nextStartIndex = ($startIndex + $itemsPerPage) - 1;
+      if (($uri = $_SERVER['REQUEST_URI']) === false) {
+        throw new \Exception("Could not parse URI : {$_SERVER['REQUEST_URI']}");
+      }
+      $uri = parse_url($uri);
+      $params = array();
+      if (isset($uri['query'])) {
+        parse_str($uri['query'], $params);
+      }
+      $params[RestRequestItem::$START_INDEX] = $nextStartIndex;
+      $params[RestRequestItem::$COUNT] = $itemsPerPage;
+      foreach ($params as $paramKey => $paramVal) {
+        $outParams[] = $paramKey . '=' . $paramVal;
+      }
+      $outParams = '?' . implode('&', $outParams);
+      $nextUri = 'http://' . $_SERVER['HTTP_HOST'] . $uri['path'] . $outParams;
+      $this->addNode($entry, 'link', '', array('rel' => 'next', 'href' => $nextUri));
+    }
+  }
+
+  /**
+   * Creates the root document using our xml version & charset
+   *
+   * @return DOMDocument
+   */
+  private function createAtomDoc() {
+    $this->doc = new \DOMDocument(self::$xmlVersion, self::$charSet);
+    $this->doc->formatOutput = self::$formatOutput;
+    return $this->doc;
+  }
+
+  /**
+   * Extracts the Atom entity name from the request url
+   *
+   * @param RequestItem $requestItem the request item
+   * @return string the request type
+   */
+  private function getRequestType($requestItem) {
+    return OutputBasicXmlConverter::getRequestType($requestItem, self::$entryTypes);
+  }
+
+  /**
+   * Recursive function that maps an data array or object to it's xml represantation
+   *
+   * @param DOMElement $element the element to append the new \node(s) to
+   * @param string $name the name of the to be created node
+   * @param array or object $data the data to map to xml
+   * @param string $nameSpace if specified, the node is created using this namespace
+   * @return DOMElement returns newly created element
+   */
+  private function addData(\DOMElement $element, $name, $data, $nameSpace = false) {
+    return OutputBasicXmlConverter::addData($this->doc, $element, $name, $data, $nameSpace);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/OutputBasicXmlConverter.php b/trunk/php/src/apache/shindig/social/converters/OutputBasicXmlConverter.php
new file mode 100644
index 0000000..2fc762a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/OutputBasicXmlConverter.php
@@ -0,0 +1,146 @@
+<?php
+namespace apache\shindig\social\converters;
+use apache\shindig\social\model\Enum;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Basic methods for OutputAtomConverter and OutputXmlConverter.
+ */
+class OutputBasicXmlConverter {
+
+  /**
+   * Extracts the Xml entity name from the request url
+   *
+   * @param RequestItem $requestItem the request item
+   * @param array $entryTypes the map of entries
+   * @return string the request type
+   */
+  public static function getRequestType($requestItem, $entryTypes) {
+    // map the Request URL to the content type to use  
+    $params = $requestItem->getParameters();
+    if (! is_array($params)) {
+      throw new \Exception("Unsupported request type");
+    }
+    $type = false;
+    foreach ($params as $key => $val) {
+      if (isset($entryTypes[$key])) {
+        $type = $entryTypes[$key];
+        break;
+      }
+    }
+    if (! $type) {
+      throw new \Exception("Unsupported request type");
+    }
+    return $type;
+  }
+
+  /**
+   * Easy shortcut for creating & appending XML nodes
+   *
+   * @param DOMDocument $doc document the root document
+   * @param DOMElement $node node to append the new child node to
+   * @param string $name name of the new element
+   * @param string $value value of the element, if empty no text node is created
+   * @param array $attributes optional array of attributes, false by default. If set attributes are added to the node using the key => val pairs
+   * @param string $nameSpace optional namespace to use when creating node
+   * @return DOMElement node
+   */
+  public static function addNode(\DOMDocument $doc, $node, $name, $value = '', $attributes = false, $nameSpace = false) {
+    if ($nameSpace) {
+      $childNode = $node->appendChild($doc->createElementNS($nameSpace, $name));
+    } else {
+      $childNode = $node->appendChild($doc->createElement($name));
+    }
+    if (! empty($value) || $value == '0') {
+      $childNode->appendChild($doc->createTextNode($value));
+    }
+    if ($attributes && is_array($attributes)) {
+      foreach ($attributes as $attrName => $attrVal) {
+        $childNodeAttr = $childNode->appendChild($doc->createAttribute($attrName));
+        if (! empty($attrVal)) {
+          $childNodeAttr->appendChild($doc->createTextNode($attrVal));
+        }
+      }
+    }
+    return $childNode;
+  }
+
+  /**
+   * Recursive function that maps an data array or object to it's xml represantation 
+   *
+   * @param DOMDocument $doc the root document
+   * @param DOMElement $element the element to append the new \node(s) to
+   * @param string $name the name of the to be created node
+   * @param array or object $data the data to map to xml
+   * @param string $nameSpace if specified, the node is created using this namespace
+   * @return DOMElement returns newly created element
+   */
+  public static function addData(\DOMDocument $doc, \DOMElement $element, $name, $data, $nameSpace = false) {
+    if ($nameSpace) {
+      $newElement = $element->appendChild($doc->createElementNS($nameSpace, $name));
+    } else {
+      $newElement = $element->appendChild($doc->createElement($name));
+    }
+    if (is_array($data)) {
+      foreach ($data as $key => $val) {
+        if (is_array($val) || is_object($val)) {
+          // prevent invalid names.. try to guess a good one :)
+          if (is_numeric($key)) {
+            $key = is_object($val) ? get_class($val) : $key = $name;
+          }
+          self::addData($doc, $newElement, $key, $val);
+        } else {
+          if (is_numeric($key)) {
+            $key = is_object($val) ? get_class($val) : $key = $name;
+          }
+          $elm = $newElement->appendChild($doc->createElement($key));
+          $elm->appendChild($doc->createTextNode($val));
+        }
+      }
+    } elseif (is_object($data)) {
+      if ($data instanceof Enum) {
+        if (isset($data->key)) {
+          // enums are output as : <NAME key="$key">$displayValue</NAME> 
+          $keyEntry = $newElement->appendChild($doc->createAttribute('key'));
+          $keyEntry->appendChild($doc->createTextNode($data->key));
+          $newElement->appendChild($doc->createTextNode($data->getDisplayValue()));
+        }
+      } else {
+        $vars = get_object_vars($data);
+        foreach ($vars as $key => $val) {
+          if (is_array($val) || is_object($val)) {
+            // prevent invalid names.. try to guess a good one :)
+            if (is_numeric($key)) {
+              $key = is_object($val) ? get_class($val) : $key = $name;
+            }
+            self::addData($doc, $newElement, $key, $val);
+          } else {
+            $elm = $newElement->appendChild($doc->createElement($key));
+            $elm->appendChild($doc->createTextNode($val));
+          }
+        }
+      }
+    } else {
+      $newElement->appendChild($doc->createTextNode($data));
+    }
+    return $newElement;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/OutputConverter.php b/trunk/php/src/apache/shindig/social/converters/OutputConverter.php
new file mode 100644
index 0000000..fd21083
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/OutputConverter.php
@@ -0,0 +1,42 @@
+<?php
+namespace apache\shindig\social\converters;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\social\service\RestRequestItem;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Abstract class for the Output conversion of the RESTful API
+ *
+ */
+abstract class OutputConverter {
+  /**
+   * @param ResponseItem $responseItem
+   * @param RestRequestItem $requestItem
+   */
+  abstract function outputResponse(ResponseItem $responseItem, RestRequestItem $requestItem);
+
+  /**
+   * @param array $responses
+   * @param SecurityToken $token
+   */
+  abstract function outputBatch(Array $responses, SecurityToken $token);
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/OutputJsonConverter.php b/trunk/php/src/apache/shindig/social/converters/OutputJsonConverter.php
new file mode 100644
index 0000000..a9d8197
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/OutputJsonConverter.php
@@ -0,0 +1,161 @@
+<?php
+namespace apache\shindig\social\converters;
+use apache\shindig\common\Config;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\social\service\RestRequestItem;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\social\spi\RestfulCollection;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Format = json output converter
+ *
+ */
+class OutputJsonConverter extends OutputConverter {
+
+  /**
+   *
+   * @param ResponseItem $responseItem
+   * @param RestRequestItem $requestItem
+   */
+  function outputResponse(ResponseItem $responseItem, RestRequestItem $requestItem) {
+    $response = $responseItem->getResponse();
+    if ($response instanceof RestfulCollection) {
+      $itemsPerPage = $requestItem->getCount();
+      if ($itemsPerPage > 0) $response->itemsPerPage = $itemsPerPage;
+    }
+    // several service calls return a null value
+    if (! is_null($response)) {
+        $this->encodeAndSendResponse($response);
+    }
+  }
+
+  /**
+   *
+   * @param array $responses
+   * @param SecurityToken $token
+   */
+  function outputBatch(Array $responses, SecurityToken $token) {
+    $this->boundryHeaders();
+    foreach ($responses as $response) {
+      $request = $response['request'];
+      $response = $response['response'];
+      $part = json_encode($response);
+      $this->outputPart($part, $response->getError());
+    }
+  }
+
+  /**
+   *
+   * @param array $responses
+   * @param SecurityToken $token
+   */
+  function outputJsonBatch(Array $responses, SecurityToken $token) {
+    $this->encodeAndSendResponse(array("responses" => $responses, "error" => false));
+  }
+
+  /**
+   * encodes data to json, adds jsonp callback if requested and sends response
+   * to client
+   *
+   * @param array $data
+   */
+  private function encodeAndSendResponse($data) {
+    if (isset($_GET['callback']) && preg_match('/^[a-zA-Z0-9\_\.]*$/', $_GET['callback'])) {
+        echo $_GET['callback'] . '(' . json_encode($data) . ')';
+        return;
+    }
+    if (Config::get('debug')) {
+        echo self::json_format(json_encode($data)); // TODO: add a query option to pretty-print json output
+    } else {
+        echo json_encode($data);
+    }
+  }
+
+  /**
+   * Generate a pretty-printed representation of a JSON object.
+   * 
+   * Taken from php comments for json_encode.
+   *
+   * @param string $json  JSON string
+   * @return string|false The pretty version, false if JSON was invalid
+   */
+  static function json_format($json) {
+    $tab = "  ";
+    $new_json = "";
+    $indent_level = 0;
+    $in_string = false;
+    $json_obj = json_decode($json);
+    if (! $json_obj) {
+      return false;
+    }
+    $json = json_encode($json_obj);
+    $len = strlen($json);
+    for ($c = 0; $c < $len; $c ++) {
+      $char = $json[$c];
+      switch ($char) {
+        case '{':
+        case '[':
+          if (! $in_string) {
+            $new_json .= $char . "\n" . str_repeat($tab, $indent_level + 1);
+            $indent_level ++;
+          } else {
+            $new_json .= $char;
+          }
+          break;
+        
+        case '}':
+        case ']':
+          if (! $in_string) {
+            $indent_level --;
+            $new_json .= "\n" . str_repeat($tab, $indent_level) . $char;
+          } else {
+            $new_json .= $char;
+          }
+          break;
+        
+        case ',':
+          if (! $in_string) {
+            $new_json .= ",\n" . str_repeat($tab, $indent_level);
+          } else {
+            $new_json .= $char;
+          }
+          break;
+        
+        case ':':
+          if (! $in_string) {
+            $new_json .= ":";
+          } else {
+            $new_json .= $char;
+          }
+          break;
+        
+        case '"':
+          $in_string = ! $in_string;
+        
+        default:
+          $new_json .= $char;
+          break;
+      }
+    }
+    return $new_json;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/converters/OutputXmlConverter.php b/trunk/php/src/apache/shindig/social/converters/OutputXmlConverter.php
new file mode 100644
index 0000000..7e15476
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/converters/OutputXmlConverter.php
@@ -0,0 +1,134 @@
+<?php
+namespace apache\shindig\social\converters;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\social\service\RestRequestItem;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\social\spi\RestfulCollection;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Format = xml output converter, for format definition see:
+ * http://docs.google.com/View?docid=dcc2jvzt_37hdzwkmf8
+ */
+class OutputXmlConverter extends OutputConverter {
+  private static $xmlVersion = '1.0';
+  private static $charSet = 'UTF-8';
+  private static $formatOutput = true;
+  
+  // this maps the REST url to the xml tags
+  private static $entryTypes = array('people' => 'entry', 'appdata' => 'entry', 
+      'activities' => 'entry', 'messages' => 'entry');
+  private $doc;
+
+  /**
+   *
+   * @param ResponseItem $responseItem
+   * @param RestRequestItem $requestItem
+   */
+  function outputResponse(ResponseItem $responseItem, RestRequestItem $requestItem) {
+    $doc = $this->createXmlDoc();
+    $requestType = $this->getRequestType($requestItem);
+    $data = $responseItem->getResponse();
+    
+    // Check to see if this is a single entry, or a collection, and construct either an xml 
+    // feed (collection) or an entry (single)		
+    if ($data instanceof RestfulCollection) {
+      $totalResults = $data->getTotalResults();
+      $itemsPerPage = $requestItem->getCount();
+      $startIndex = $requestItem->getStartIndex();
+      
+      // The root Feed element
+      $entry = $this->addNode($doc, 'response', '');
+      
+      // Required Xml fields
+      $this->addNode($entry, 'startIndex', $startIndex);
+      $this->addNode($entry, 'itemsPerPage', $itemsPerPage);
+      $this->addNode($entry, 'totalResults', $totalResults);
+      $responses = $data->getEntry();
+      foreach ($responses as $response) {
+        // recursively add responseItem data to the xml structure
+        $this->addData($entry, $requestType, $response);
+      }
+    } else {
+      // Single entry = Xml:Entry	
+      $entry = $this->addNode($doc, 'response', '');
+      // addData loops through the responseItem data recursively creating a matching XML structure
+      $this->addData($entry, 'entry', $data['entry']);
+    }
+    $xml = $doc->saveXML();
+    echo $xml;
+  }
+
+  /**
+   *
+   * @param array $responses
+   * @param SecurityToken $token 
+   */
+  function outputBatch(Array $responses, SecurityToken $token) {
+    throw new \Exception("XML batch not supported");
+  }
+
+  /**
+   * Easy shortcut for creating & appending XML nodes
+   *
+   * @param DOMElement $node node to append the new child node too
+   * @param string $name name of the new element
+   * @param string $value value of the element, if empty no text node is created
+   * @param array $attributes optional array of attributes, false by default. If set attributes are added to the node using the key => val pairs
+   * @return DOMElement node
+   */
+  private function addNode($node, $name, $value = '', $attributes = false) {
+    return OutputBasicXmlConverter::addNode($this->doc, $node, $name, $value, $attributes);
+  }
+
+  /**
+   * Creates the root document using our xml version & charset
+   *
+   * @return DOMDocument
+   */
+  private function createXmlDoc() {
+    $this->doc = new \DOMDocument(self::$xmlVersion, self::$charSet);
+    $this->doc->formatOutput = self::$formatOutput;
+    return $this->doc;
+  }
+
+  /**
+   * Extracts the Xml entity name from the request url
+   *
+   * @param RequestItem $requestItem the request item
+   * @return string the request type
+   */
+  private function getRequestType($requestItem) {
+    return OutputBasicXmlConverter::getRequestType($requestItem, self::$entryTypes);
+  }
+
+  /**
+   * Recursive function that maps an data array or object to it's xml represantation 
+   *
+   * @param DOMElement $element the element to append the new \node(s) to
+   * @param string $name the name of the to be created node
+   * @param array or object $data the data to map to xml
+   * @return DOMElement returns newly created element
+   */
+  private function addData(\DOMElement $element, $name, $data) {
+    return OutputBasicXmlConverter::addData($this->doc, $element, $name, $data);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Account.php b/trunk/php/src/apache/shindig/social/model/Account.php
new file mode 100644
index 0000000..46cb55f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Account.php
@@ -0,0 +1,75 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ *
+ */
+class Account implements ComplexField {
+  public $domain;
+  public $userid;
+  public $username;
+  public $primary;
+
+  public function __construct($domain, $userid, $username, $primary = null) {
+    $this->domain = $domain;
+    $this->userid = $userid;
+    $this->username = $username;
+    $this->primary = $primary;
+  }
+
+  public function getDomain() {
+    return $this->domain;
+  }
+
+  public function setDomain($domain) {
+    $this->domain = $domain;
+  }
+
+  public function getUserid() {
+    return $this->userid;
+  }
+
+  public function setUserid($userid) {
+    $this->userid = $userid;
+  }
+
+  public function getUsername() {
+    return $this->username;
+  }
+
+  public function setUsername($username) {
+    $this->username = $username;
+  }
+
+  public function getPrimary() {
+    return $this->primary;
+  }
+
+  public function setPrimary($primary) {
+    $this->primary = $primary;
+  }
+
+  public function getPrimarySubValue() {
+    return $this->getDomain();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Activity.php b/trunk/php/src/apache/shindig/social/model/Activity.php
new file mode 100644
index 0000000..42c6fc0
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Activity.php
@@ -0,0 +1,187 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Activity
+ */
+class Activity {
+  public $appId;
+  public $body;
+  public $bodyId;
+  public $externalId;
+  public $id;
+  public $mediaItems;
+  public $postedTime;
+  public $priority;
+  public $streamFaviconUrl;
+  public $streamSourceUrl;
+  public $streamTitle;
+  public $streamUrl;
+  public $templateParams;
+  public $title;
+  public $titleId;
+  public $url;
+  public $userId;
+
+  public function __construct($id, $userId) {
+    $this->id = $id;
+    $this->userId = $userId;
+  }
+
+  public function getAppId() {
+    return $this->appId;
+  }
+
+  public function setAppId($appId) {
+    $this->appId = $appId;
+  }
+
+  public function getBody() {
+    return $this->body;
+  }
+
+  public function setBody($body) {
+    $this->body = $body;
+  }
+
+  public function getBodyId() {
+    return $this->bodyId;
+  }
+
+  public function setBodyId($bodyId) {
+    $this->bodyId = $bodyId;
+  }
+
+  public function getExternalId() {
+    return $this->externalId;
+  }
+
+  public function setExternalId($externalId) {
+    $this->externalId = $externalId;
+  }
+
+  public function getId() {
+    return $this->id;
+  }
+
+  public function setId($id) {
+    $this->id = $id;
+  }
+
+  public function getMediaItems() {
+    return $this->mediaItems;
+  }
+
+  public function setMediaItems($mediaItems) {
+    $this->mediaItems = $mediaItems;
+  }
+
+  public function getPostedTime() {
+    return $this->postedTime;
+  }
+
+  public function setPostedTime($postedTime) {
+    $this->postedTime = $postedTime;
+  }
+
+  public function getPriority() {
+    return $this->priority;
+  }
+
+  public function setPriority($priority) {
+    $this->priority = $priority;
+  }
+
+  public function getStreamFaviconUrl() {
+    return $this->streamFaviconUrl;
+  }
+
+  public function setStreamFaviconUrl($streamFaviconUrl) {
+    $this->streamFaviconUrl = $streamFaviconUrl;
+  }
+
+  public function getStreamSourceUrl() {
+    return $this->streamSourceUrl;
+  }
+
+  public function setStreamSourceUrl($streamSourceUrl) {
+    $this->streamSourceUrl = $streamSourceUrl;
+  }
+
+  public function getStreamTitle() {
+    return $this->streamTitle;
+  }
+
+  public function setStreamTitle($streamTitle) {
+    $this->streamTitle = $streamTitle;
+  }
+
+  public function getStreamUrl() {
+    return $this->streamUrl;
+  }
+
+  public function setStreamUrl($streamUrl) {
+    $this->streamUrl = $streamUrl;
+  }
+
+  public function getTemplateParams() {
+    return $this->templateParams;
+  }
+
+  public function setTemplateParams($templateParams) {
+    $this->templateParams = $templateParams;
+  }
+
+  public function getTitle() {
+    return $this->title;
+  }
+
+  public function setTitle($title) {
+    $this->title = strip_tags($title, '<b><i><a><span><img>');
+  }
+
+  public function getTitleId() {
+    return $this->titleId;
+  }
+
+  public function setTitleId($titleId) {
+    $this->titleId = $titleId;
+  }
+
+  public function getUrl() {
+    return $this->url;
+  }
+
+  public function setUrl($url) {
+    $this->url = $url;
+  }
+
+  public function getUserId() {
+    return $this->userId;
+  }
+
+  public function setUserId($userId) {
+    $this->userId = $userId;
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Address.php b/trunk/php/src/apache/shindig/social/model/Address.php
new file mode 100644
index 0000000..8c2c0df
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Address.php
@@ -0,0 +1,155 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Address
+ *
+ */
+class Address implements ComplexField {
+  public $country;
+  public $latitude;
+  public $longitude;
+  public $locality;
+  public $postalCode;
+  public $region;
+  public $streetAddress;
+  public $type;
+  public $formatted;
+  public $primary;
+  public $poBox;
+  public $extendedAddress;
+  public $unstructuredAddress;
+
+  public function __construct($formatted, $primary = null) {
+    $this->formatted = $formatted;
+    $this->primary = $primary;
+  }
+
+  public function getPoBox() {
+    return $this->poBox;
+  }
+  
+  public function setPoBox($poBox) {
+    $this->poBox = $poBox;
+  }
+  
+  public function getExtendedAddress() {
+    return $this->extendedAddress;
+  }
+  
+  public function setExtendedAddress($extendedAddress) {
+    $this->extendedAddress = $extendedAddress;
+  }
+  
+  public function getUnstructuredAddress() {
+    return $this->unstructuredAddress;
+  }
+  
+  public function setUnstructuredAddress($unstructuredAddress) {
+    $this->unstructuredAddress = $unstructuredAddress;
+  }
+  
+  public function getCountry() {
+    return $this->country;
+  }
+
+  public function setCountry($country) {
+    $this->country = $country;
+  }
+
+  public function getLatitude() {
+    return $this->latitude;
+  }
+
+  public function setLatitude($latitude) {
+    $this->latitude = $latitude;
+  }
+
+  public function getLocality() {
+    return $this->locality;
+  }
+
+  public function setLocality($locality) {
+    $this->locality = $locality;
+  }
+
+  public function getLongitude() {
+    return $this->longitude;
+  }
+
+  public function setLongitude($longitude) {
+    $this->longitude = $longitude;
+  }
+
+  public function getPostalCode() {
+    return $this->postalCode;
+  }
+
+  public function setPostalCode($postalCode) {
+    $this->postalCode = $postalCode;
+  }
+
+  public function getRegion() {
+    return $this->region;
+  }
+
+  public function setRegion($region) {
+    $this->region = $region;
+  }
+
+  public function getStreetAddress() {
+    return $this->streetAddress;
+  }
+
+  public function setStreetAddress($streetAddress) {
+    $this->streetAddress = $streetAddress;
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+
+  public function setType($type) {
+    $this->type = $type;
+  }
+
+  public function getFormatted() {
+    return $this->formatted;
+  }
+
+  public function setFormatted($formatted) {
+    $this->formatted = $formatted;
+  }
+
+  public function getPrimary() {
+    return $this->primary;
+  }
+
+  public function setPrimary($primary) {
+    $this->primary = $primary;
+  }
+
+  public function getPrimarySubValue() {
+    return $this->getFormatted();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Album.php b/trunk/php/src/apache/shindig/social/model/Album.php
new file mode 100644
index 0000000..7485041
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Album.php
@@ -0,0 +1,117 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Specification.xml#opensocial.Album
+ */
+class Album {
+
+  public $id;
+  public $title;
+  public $description;
+  public $location;
+  public $mediaItemCount;
+  public $ownerId;
+  public $thumbnailUrl;
+  public $mediaMimeType;
+  public $mediaType;
+  
+  public function __construct($id, $ownerId) {
+    $this->setId($id);
+    $this->setOwnerId($ownerId);
+  }
+  
+  public function getId() {
+    return $this->id;
+  }
+  
+  public function setId($id) {
+    $this->id = $id;
+  }
+  
+  public function getTitle() {
+    return $this->title;
+  }
+  
+  public function setTitle($title) {
+    $this->title = $title;
+  }
+  
+  public function getDescription() {
+    return $this->description;
+  }
+  
+  public function setDescription($description) {
+    $this->description = $description;
+  }
+  
+  public function getLocation() {
+    return $this->location;
+  }
+  
+  public function setLocation($location) {
+    $this->location = $location;
+  }
+  
+  public function getMediaItemCount() {
+    return $this->mediaItemCount;
+  }
+  
+  public function setMediaItemCount($mediaItemCount) {
+    $this->mediaItemCount = $mediaItemCount > 0 ? $mediaItemCount : 0;
+  }
+  
+  public function getOwnerId() {
+    return $this->ownerId;
+  }
+  
+  public function setOwnerId($ownerId) {
+    $this->ownerId = $ownerId;
+  }
+  
+  public function getThumbnailUrl() {
+    return $this->thumbnailUrl;
+  }
+  
+  public function setThumbnailUrl($thumbnailUrl) {
+    $this->thumbnailUrl = $thumbnailUrl;
+  }
+  
+  public function getMediaMimeType() {
+    return $this->mediaMimeType;
+  }
+  
+  public function setMediaMimeType($mediaMimeType) {
+    $this->mediaMimeType = $mediaMimeType;
+  }
+  
+  public function getMediaType() {
+    return $this->mediaType;
+  }
+  
+  public function setMediaType($mediaType) {
+    if (!in_array($mediaType, MediaItem::$TYPES)) {
+      throw new \Exception("Invalid Media type");
+    }
+    $this->mediaType = $mediaType;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/ApiCollection.php b/trunk/php/src/apache/shindig/social/model/ApiCollection.php
new file mode 100644
index 0000000..aa8c820
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/ApiCollection.php
@@ -0,0 +1,61 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Collection
+ */
+class ApiCollection {
+  public $items = array();
+  public $offset;
+  public $totalSize;
+
+  public function __construct($items, $offset = false, $totalSize = false) {
+    $this->items = $items;
+    $this->offset = $offset;
+    $this->totalSize = $totalSize;
+  }
+
+  public function getItems() {
+    return $this->items;
+  }
+
+  public function setItems($items) {
+    $this->items = $items;
+  }
+
+  public function getOffset() {
+    return $this->offset;
+  }
+
+  public function setOffset($offset) {
+    $this->offset = $offset;
+  }
+
+  public function getTotalSize() {
+    return $this->totalSize;
+  }
+
+  public function setTotalSize($totalSize) {
+    $this->totalSize = $totalSize;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/BodyType.php b/trunk/php/src/apache/shindig/social/model/BodyType.php
new file mode 100644
index 0000000..9cde1d8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/BodyType.php
@@ -0,0 +1,78 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.BodyType
+ */
+class BodyType implements ComplexField {
+  public $build;
+  public $eyeColor;
+  public $hairColor;
+  public $height;
+  public $weight;
+
+  public function getBuild() {
+    return $this->build;
+  }
+
+  public function setBuild($build) {
+    $this->build = $build;
+  }
+
+  public function getEyeColor() {
+    return $this->eyeColor;
+  }
+
+  public function setEyeColor($eyeColor) {
+    $this->eyeColor = $eyeColor;
+  }
+
+  public function getHairColor() {
+    return $this->hairColor;
+  }
+
+  public function setHairColor($hairColor) {
+    $this->hairColor = $hairColor;
+  }
+
+  public function getHeight() {
+    return $this->height;
+  }
+
+  public function setHeight($height) {
+    $this->height = $height;
+  }
+
+  public function getWeight() {
+    return $this->weight;
+  }
+
+  public function setWeight($weight) {
+    $this->weight = $weight;
+  }
+
+  public function getPrimarySubValue() {
+    // FIXME: is primary sub-field specified for bodyType in the spec??
+    return $this->getBuild();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/ComplexField.php b/trunk/php/src/apache/shindig/social/model/ComplexField.php
new file mode 100644
index 0000000..789d4c5
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/ComplexField.php
@@ -0,0 +1,30 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface ComplexField {
+
+  /**
+   * Returns the value of the primary sub-field for this complex field.
+   * This is usually the "value" sub-field, and it's the value used for sorting/filtering.
+   */
+  function getPrimarySubValue();
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Email.php b/trunk/php/src/apache/shindig/social/model/Email.php
new file mode 100644
index 0000000..64e9c88
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Email.php
@@ -0,0 +1,29 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Email
+ *
+ */
+class Email extends ListField {
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Enum.php b/trunk/php/src/apache/shindig/social/model/Enum.php
new file mode 100644
index 0000000..e5a1e80
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Enum.php
@@ -0,0 +1,67 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Enum
+ *
+ * Base class for all Enum objects. This class allows containers to use constants
+ * for fields that have a common set of values.
+ *
+ */
+
+abstract class Enum implements ComplexField {
+  public $displayValue;
+  public $key;
+  public $values = array();
+
+  public function __construct($key, $displayValue = '') {
+    if (! empty($key) && ! isset($this->values[$key])) {
+      if (in_array($key, $this->values)) {
+        // case of mixing key <> display value, correct it
+        $key = array_search($key, $this->values);
+      } else {
+        $this->displayValue = $displayValue;
+        //throw new \Exception("Invalid Enum key: $key\n". print_r(debug_backtrace(), true));
+      }
+    }
+    $this->key = $key;
+    $this->displayValue = ! empty($displayValue) ? $displayValue : (isset($this->values[$key]) ? $this->values[$key] : '');
+    unset($this->values);
+  }
+
+  public function getDisplayValue() {
+    return $this->displayValue;
+  }
+
+  public function setDisplayValue($displayValue) {
+    $this->displayValue = $displayValue;
+  }
+
+  public function toString() {
+    return $this->jsonString;
+  }
+
+  public function getPrimarySubValue() {
+    return $this->key;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/EnumDrinker.php b/trunk/php/src/apache/shindig/social/model/EnumDrinker.php
new file mode 100644
index 0000000..4196d84
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/EnumDrinker.php
@@ -0,0 +1,30 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * public Enum for opensocial.Enum.Drinker
+ */
+class EnumDrinker extends Enum {
+  public $values = array('HEAVILY' => "Heavily", 'NO' => "No", 'OCCASIONALLY' => "Occasionally",
+      'QUIT' => "Quit", 'QUITTING' => "Quitting", 'REGULARLY' => "Regularly",
+      'SOCIALLY' => "Socially", 'YES' => "Yes");
+}
diff --git a/trunk/php/src/apache/shindig/social/model/EnumGender.php b/trunk/php/src/apache/shindig/social/model/EnumGender.php
new file mode 100644
index 0000000..2a12932
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/EnumGender.php
@@ -0,0 +1,28 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * public Enum for opensocial.Enum.Gender
+ */
+class EnumGender extends Enum {
+  public $values = array('FEMALE' => "Female", 'MALE' => "Male");
+}
\ No newline at end of file
diff --git a/trunk/php/src/apache/shindig/social/model/EnumLookingFor.php b/trunk/php/src/apache/shindig/social/model/EnumLookingFor.php
new file mode 100644
index 0000000..366fc2e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/EnumLookingFor.php
@@ -0,0 +1,30 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * public Enum for opensocial.Enum.LookingFor
+ */
+class EnumLookingFor extends Enum {
+  public $values = array('ACTIVITY_PARTNERS' => 'Activity Partners', 'DATING' => 'Dating',
+      'FRIENDS' => 'Friends', 'NETWORKING' => 'Networking', 'RANDOM' => 'Random',
+      'RELATIONSHIP' => 'Relationship');
+}
diff --git a/trunk/php/src/apache/shindig/social/model/EnumPresence.php b/trunk/php/src/apache/shindig/social/model/EnumPresence.php
new file mode 100644
index 0000000..1bd5197
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/EnumPresence.php
@@ -0,0 +1,29 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * public Enum for opensocial.Enum.Presence
+ */
+class EnumPresence extends Enum {
+  public $values = array('AWAY' => "Away", 'CHAT' => "Chat", 'DND' => "Do Not Disturb", 'OFFLINE' => "Offline",
+      'ONLINE' => "Online", 'XA' => "Extended Away");
+}
diff --git a/trunk/php/src/apache/shindig/social/model/EnumSmoker.php b/trunk/php/src/apache/shindig/social/model/EnumSmoker.php
new file mode 100644
index 0000000..3ab280b
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/EnumSmoker.php
@@ -0,0 +1,30 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * public Enum for opensocial.Enum.Smoker
+ */
+class EnumSmoker extends Enum {
+  public $values = array('HEAVILY' => "Heavily", 'NO' => "No", 'OCCASIONALLY' => "Ocasionally", 'QUIT' => "Quit",
+      'QUITTING' => "Quitting", 'REGULARLY' => "Regularly", 'SOCIALLY' => "Socially",
+      'YES' => "Yes");
+}
diff --git a/trunk/php/src/apache/shindig/social/model/IdSpec.php b/trunk/php/src/apache/shindig/social/model/IdSpec.php
new file mode 100644
index 0000000..699b81d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/IdSpec.php
@@ -0,0 +1,65 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class IdSpec {
+  public static $types = array('VIEWER', 'OWNER', 'VIEWER_FRIENDS', 'OWNER_FRIENDS', 'USER_IDS');
+
+  public $jsonSpec;
+  public $type;
+
+  public function __construct($jsonSpec, $type) {
+    $this->jsonSpec = $jsonSpec;
+    $this->type = $type;
+  }
+
+  static public function fromJson($jsonIdSpec) {
+    if (! empty($jsonIdSpec) && in_array((string)$jsonIdSpec, self::$types)) {
+      $idSpecEnum = (string)$jsonIdSpec;
+    } elseif (! empty($jsonIdSpec)) {
+      $idSpecEnum = 'USER_IDS';
+    } else {
+      throw new \Exception("The json request had a bad idSpec");
+    }
+    return new IdSpec($jsonIdSpec, $idSpecEnum);
+  }
+
+  /**
+   * Only valid for IdSpecs of type USER_IDS
+   * @return A list of the user ids in the id spec
+   *
+   */
+  public function fetchUserIds() {
+    $userIdArray = $this->jsonSpec;
+    if (! is_array($userIdArray)) {
+      $userIdArray = array($userIdArray);
+    }
+    $userIds = array();
+    foreach ($userIdArray as $id) {
+      $userIds[] = (string)$id;
+    }
+    return $userIds;
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Im.php b/trunk/php/src/apache/shindig/social/model/Im.php
new file mode 100644
index 0000000..09daf1a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Im.php
@@ -0,0 +1,29 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * WRONG URL -> http://code.google.com/apis/opensocial/docs/0.7/reference/opensocial.Im.Field.html 
+ *
+ */
+class Im extends ListField {
+}
diff --git a/trunk/php/src/apache/shindig/social/model/ListField.php b/trunk/php/src/apache/shindig/social/model/ListField.php
new file mode 100644
index 0000000..370fb27
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/ListField.php
@@ -0,0 +1,64 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Base class for plural fields, defining the standard value/type/primary sub-fields.
+ */
+class ListField implements ComplexField {
+  public $value;
+  public $type;
+  public $primary;
+
+  public function __construct($value, $type, $primary = null) {
+    $this->value = $value;
+    $this->type = $type;
+    $this->primary = $primary;
+  }
+
+  public function getValue() {
+    return $this->value;
+  }
+
+  public function setValue($value) {
+    $this->value = $value;
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+
+  public function setType($type) {
+    $this->type = $type;
+  }
+
+  public function getPrimary() {
+    return $this->primary;
+  }
+
+  public function setPrimary($primary) {
+    $this->primary = $primary ? true : null;
+  }
+
+  public function getPrimarySubValue() {
+    return $this->getValue();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/MediaItem.php b/trunk/php/src/apache/shindig/social/model/MediaItem.php
new file mode 100644
index 0000000..a4377db
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/MediaItem.php
@@ -0,0 +1,228 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * http://opensocial-resources.googlecode.com/svn/spec/0.9/OpenSocial-Specification.xml#opensocial.MediaItem
+ */
+class MediaItem {
+  
+  public $albumId;
+  public $created;
+  public $description;
+  public $duration;
+  public $fileSize;
+  public $id;
+  public $language;
+  public $lastUpdated;
+  public $location;
+  public $mimeType;
+  public $numComments;
+  public $numViews;
+  public $numVotes;
+  public $rating;
+  public $startTime;
+  public $taggedPeople;
+  public $tags;
+  public $thumbnailUrl;
+  public $title;
+  public $type;
+  public $url;
+  
+  public static $TYPES = array('AUDIO', 'VIDEO', 'IMAGE');
+
+  public function __construct($mimeType, $type, $url) {
+    $this->setMimeType($mimeType);
+    $this->setType($type);
+    $this->setUrl($url);
+  }
+
+  public function getId() {
+    return $this->id;
+  }
+
+  public function setId($id) {
+    $this->id = $id;
+  }
+  
+  public function getAlbumId() {
+    return $this->albumId;
+  }
+
+  public function setAlbumId($albumId) {
+    $this->albumId = $albumId;
+  }
+  
+  public function getCreated() {
+    return $this->created;
+  }
+
+  public function setCreated($created) {
+    $this->created = $created;
+  }
+
+  public function getDescription() {
+    return $this->$description;
+  }
+
+  public function setDescription($description) {
+    $this->description = $description;
+  }
+  
+  public function getDuration() {
+    return $this->duration;
+  }
+
+  public function setDuration($duration) {
+    $this->duration = $duration;
+  }
+  
+  public function getFileSize() {
+    return $this->fileSize;
+  }
+
+  public function setFileSize($fileSize) {
+    $this->fileSize = $fileSize;
+  }
+  
+  public function getLanguage() {
+    return $this->language;
+  }
+
+  public function setLanguage($language) {
+    $this->language = $language;
+  }
+  
+  public function getLastUpdated() {
+    return $this->lastUpdated;
+  }
+
+  public function setLastUpdated($lastUpdated) {
+    $this->lastUpdated = $lastUpdated;
+  }
+  
+  public function getLocation() {
+    return $this->location;
+  }
+
+  public function setLocation($location) {
+    $this->location = $location;
+  }
+  
+  public function getNumComments() {
+    return $this->numComments;
+  }
+
+  public function setNumComments($numComments) {
+    $this->numComments = $numComments;
+  }
+  
+  public function getNumViews() {
+    return $this->numViews;
+  }
+
+  public function setNumViews($numViews) {
+    $this->numViews = $numViews;
+  }
+  
+  public function getNumVotes() {
+    return $this->numVotes;
+  }
+
+  public function setNumVotes($numVotes) {
+    $this->numVotes = $numVotes;
+  }
+  
+  public function getRating() {
+    return $this->rating;
+  }
+
+  public function setRating($rating) {
+    $this->rating = $rating;
+  }
+  
+  public function getStartTime() {
+    return $this->startTime;
+  }
+
+  public function setStartTime($startTime) {
+    $this->startTime = $startTime;
+  }
+  
+  public function getTaggedPeople() {
+    return $this->taggedPeople;
+  }
+
+  public function setTaggedPeople($taggedPeople) {
+    $this->taggedPeople = $taggedPeople;
+  }
+  
+  public function getTags() {
+    return $this->tags;
+  }
+
+  public function setTags($tags) {
+    $this->tags = $tags;
+  }
+  
+  public function getThumbnailUrl() {
+    return $this->thumbnailUrl;
+  }
+
+  public function setThumbnailUrl($thumbnailUrl) {
+    $this->thumbnailUrl = $thumbnailUrl;
+  }
+  
+  public function getTitle() {
+    return $this->title;
+  }
+
+  public function setTitle($title) {
+    $this->title = $title;
+  }
+  
+  public function getMimeType() {
+    return $this->mimeType;
+  }
+
+  public function setMimeType($mimeType) {
+    $this->mimeType = $mimeType;
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+
+  public function setType($type) {
+    if (! in_array($type, self::$TYPES)) {
+      throw new \Exception("Invalid Media type");
+    }
+    $this->type = $type;
+  }
+
+  public function getUrl() {
+    return $this->url;
+  }
+
+  public function setUrl($url) {
+    $this->url = $url;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Message.php b/trunk/php/src/apache/shindig/social/model/Message.php
new file mode 100644
index 0000000..8a25c3f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Message.php
@@ -0,0 +1,214 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://opensocial-resources.googlecode.com/svn/spec/draft/OpenSocial-Specification.xml#opensocial.Message.Field
+ */
+class Message {
+  // These fields should be referenced via getters and setters. 'public' only for json_encode. 
+  public $appUrl;
+  public $body;
+  public $bodyId;
+  public $collectionIds;
+  public $id;
+  public $inReplyTo;
+  public $recipients;
+  public $replies;
+  public $senderId;
+  public $status;
+  public $timeSent;
+  public $title;
+  public $titleId;
+  public $type;
+  public $updated;
+  public $urls;
+  
+  public static $DEFAULT_FIELDS = array('appUrl', 'body', 'bodyId',
+      'collectionIds', 'id', 'inReplyTo', 'recipients', 'replies',
+      'senderId', 'status', 'timeSent', 'title', 'titleId', 'type',
+      'updated', 'urls');
+  
+  public static $TYPES = array(
+      /* An email */
+      'EMAIL',
+      /* A short private message */
+      'NOTIFICATION',
+      /* A message to a specific user that can be seen only by that user */
+      'PRIVATE_MESSAGE',
+      /* A message to a specific user that can be seen by more than that user */
+      'PUBLIC_MESSAGE');
+  
+  public static $STATUS = array('NEW', 'READ', 'DELETED');
+
+  public function __construct($id, $title) {
+    $this->setId($id);
+    $this->setTitle($title);
+  }
+
+  public function getAppUrl() {
+    return $this->appUrl;
+  }
+  
+  public function setAppUrl($url) {
+    $this->url = $url;
+  }
+
+  public function getBody() {
+    return $this->body;
+  }
+
+  public function setBody($body) {
+    $this->body = $body;
+  }
+
+  public function getBodyId() {
+    return $this->bodyId;
+  }
+
+  public function setBodyId($bodyId) {
+    $this->bodyId = $bodyId;
+  }
+
+  public function getCollectionIds() {
+    return $this->collectionIds;
+  }
+
+  public function setCollectionIds($collectionIds) {
+    $this->$collectionIds = $collectionIds;
+  }
+
+  public function getId() {
+    return $this->id;
+  }
+
+  public function setId($id) {
+    $this->id = $id;
+  }
+
+  public function getInReplyTo() {
+    return $this->inReplyTo;
+  }
+
+  public function setInReplyTo($inReplyTo) {
+    $this->inReplyTo = $inReplyTo;
+  }
+
+  public function getRecipients() {
+    return $this->recipients;
+  }
+
+  public function setRecipients($recipients) {
+    $this->recipients = $recipients;
+  }
+  
+  public function getReplies() {
+    return $this->replies;
+  }
+  
+  public function setReplies($replies) {
+    $this->replies = $replies;
+  }
+
+  public function getStatus() {
+    return $this->status;
+  }
+
+  public function setStatus($status) {
+    $this->status = $status;
+  }
+
+  public function getSenderId() {
+    return $this->senderId;
+  }
+  
+  public function setSenderId($senderId) {
+    $this->senderId = $senderId;
+  }
+
+  public function getTimeSent() {
+    return $this->timeSent;
+  }
+
+  public function setTimeSent($timeSent) {
+    $this->timeSent = $timeSent;
+  }
+
+  public function getTitle() {
+    return $this->title;
+  }
+
+  public function setTitle($title) {
+    $this->title = $title;
+  }
+
+  public function getTitleId() {
+    return $this->titleId;
+  }
+
+  public function setTitleId($titleId) {
+    $this->titleId = $titleId;
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+
+  public function setType($type) {
+    $this->type = $type;
+  }
+  
+  public function getUpdated() {
+    return $this->updated;
+  }
+
+  public function setUpdated($updated) {
+    $this->updated = $updated;
+  }
+
+  /**
+   * Gets the URLs related to the message
+   * @return the URLs related to the person, their webpages, or feeds
+   */
+  public function getUrls() {
+    return $this->urls;
+  }
+
+  /**
+   * Sets the URLs related to the message
+   * @param urls the URLs related to the person, their webpages, or feeds
+   */
+  public function setUrls($urls) {
+    $this->urls = $urls;
+  }
+  
+  /**
+   * TODO implement either a standard 'sanitizing' facility or
+   * define an interface that can be set on this class so
+   * others can plug in their own.
+   * @param htmlStr String to be sanitized.
+   * @return the sanitized HTML String
+   */
+  public function sanitizeHTML($htmlStr) {
+    return $htmlStr;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/MessageCollection.php b/trunk/php/src/apache/shindig/social/model/MessageCollection.php
new file mode 100644
index 0000000..ff83504
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/MessageCollection.php
@@ -0,0 +1,100 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://opensocial-resources.googlecode.com/svn/spec/draft/OpenSocial-Specification.xml#opensocial.MessageCollection.Field
+ *
+ */
+class MessageCollection {
+  // Indicates the collection of all messages sent to the user
+  public static $INBOX = '@inbox';
+  // Indicates the collection of all messages sent by the user
+  // and used as a special endpoint for posting outbound messages.
+  public static $OUTBOX = '@outbox';
+  // All the messages both sent from and to the user.
+  public static $ALL = '@all';
+
+  // These fileds should be referenced via getters and setters. 'public' only for json_encode. 
+  public $id;
+  public $title;
+  public $total;
+  public $unread;
+  public $updated;
+  public $urls = array();
+
+  public static $DEFAULT_FIELDS = array('id', 'title', 'total', 'unread', 'updated', 'urls');
+
+  public function __construct($id, $title) {
+    $this->setId($id);
+    $this->setTitle($title);
+  }
+
+  public function getId() {
+    return $this->id;
+  }
+
+  public function setId($id) {
+    $this->id = $id;
+  }
+
+  public function getTitle() {
+    return $this->title;
+  }
+
+  public function setTitle($title) {
+    $this->title = $title;
+  }
+
+  public function getTotal() {
+    return $this->total;
+  }
+
+  public function setTotal($total) {
+    $this->total = $total;
+  }
+
+  public function getUnread() {
+    return $this->unread;
+  }
+
+  public function setUnread($unread) {
+    $this->unread = $unread;
+  }
+
+  public function getUpdated() {
+    return $this->updated;
+  }
+
+  public function setUpdated($updated) {
+    $this->updated = $updated;
+  }
+
+  public function getUrls() {
+    return $this->urls;
+  }
+
+  public function setUrls($urls) {
+    $this->urls = $urls;
+  }
+}
+
diff --git a/trunk/php/src/apache/shindig/social/model/Name.php b/trunk/php/src/apache/shindig/social/model/Name.php
new file mode 100644
index 0000000..8ac297a
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Name.php
@@ -0,0 +1,91 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Name
+ *
+ */
+class Name implements ComplexField {
+  public $additionalName;
+  public $familyName;
+  public $givenName;
+  public $honorificPrefix;
+  public $honorificSuffix;
+  public $formatted;
+
+  public function __construct($formatted) {
+    $this->formatted = $formatted;
+  }
+
+  public function getFormatted() {
+    return $this->formatted;
+  }
+
+  public function setFormatted($formatted) {
+    $this->formatted = $formatted;
+  }
+
+  public function getAdditionalName() {
+    return $this->additionalName;
+  }
+
+  public function setAdditionalName($additionalName) {
+    $this->additionalName = $additionalName;
+  }
+
+  public function getFamilyName() {
+    return $this->familyName;
+  }
+
+  public function setFamilyName($familyName) {
+    $this->familyName = $familyName;
+  }
+
+  public function getGivenName() {
+    return $this->givenName;
+  }
+
+  public function setGivenName($givenName) {
+    $this->givenName = $givenName;
+  }
+
+  public function getHonorificPrefix() {
+    return $this->honorificPrefix;
+  }
+
+  public function setHonorificPrefix($honorificPrefix) {
+    $this->honorificPrefix = $honorificPrefix;
+  }
+
+  public function getHonorificSuffix() {
+    return $this->honorificSuffix;
+  }
+
+  public function setHonorificSuffix($honorificSuffix) {
+    $this->honorificSuffix = $honorificSuffix;
+  }
+
+  public function getPrimarySubValue() {
+    return $this->getFormatted();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Organization.php b/trunk/php/src/apache/shindig/social/model/Organization.php
new file mode 100644
index 0000000..c12b19d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Organization.php
@@ -0,0 +1,146 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Organization
+ *
+ */
+class Organization implements ComplexField {
+  public $address;
+  public $description;
+  public $endDate;
+  public $field;
+  public $name;
+  public $primary;
+  public $salary;
+  public $startDate;
+  public $subField;
+  public $title;
+  public $type;
+  public $webpage;
+
+  public function __construct($name, $primary = null) {
+    $this->name = $name;
+    $this->primary = $primary;
+  }
+
+  public function getAddress() {
+    return $this->address;
+  }
+
+  public function setAddress($address) {
+    $this->address = $address;
+  }
+
+  public function getDescription() {
+    return $this->description;
+  }
+
+  public function setDescription($description) {
+    $this->description = $description;
+  }
+
+  public function getEndDate() {
+    return $this->endDate;
+  }
+
+  public function setEndDate($endDate) {
+    $this->endDate = $endDate;
+  }
+
+  public function getField() {
+    return $this->field;
+  }
+
+  public function setField($field) {
+    $this->field = $field;
+  }
+
+  public function getName() {
+    return $this->name;
+  }
+
+  public function setName($name) {
+    $this->name = $name;
+  }
+
+  public function getSalary() {
+    return $this->salary;
+  }
+
+  public function setSalary($salary) {
+    $this->salary = $salary;
+  }
+
+  public function getStartDate() {
+    return $this->startDate;
+  }
+
+  public function setStartDate($startDate) {
+    $this->startDate = $startDate;
+  }
+
+  public function getSubField() {
+    return $this->subField;
+  }
+
+  public function setSubField($subField) {
+    $this->subField = $subField;
+  }
+
+  public function getTitle() {
+    return $this->title;
+  }
+
+  public function setTitle($title) {
+    $this->title = $title;
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+
+  public function setType($type) {
+    $this->type = $type;
+  }
+
+  public function getWebpage() {
+    return $this->webpage;
+  }
+
+  public function setWebpage($webpage) {
+    $this->webpage = $webpage;
+  }
+
+  public function getPrimary() {
+    return $this->primary;
+  }
+
+  public function setPrimary($primary) {
+    $this->primary = $primary;
+  }
+
+  public function getPrimarySubValue() {
+    return $this->getName();
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Person.php b/trunk/php/src/apache/shindig/social/model/Person.php
new file mode 100644
index 0000000..e75a13f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Person.php
@@ -0,0 +1,601 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Person
+ *
+ */
+class Person {
+  public $aboutMe;
+  public $accounts;
+  public $activities;
+  public $addresses;
+  public $age;
+  public $birthday;
+  public $bodyType;
+  public $books;
+  public $cars;
+  public $children;
+  public $currentLocation;
+  public $displayName;
+  public $drinker;
+  public $emails;
+  public $ethnicity;
+  public $fashion;
+  public $food;
+  public $gender;
+  public $happiestWhen;
+  public $hasApp;
+  public $heroes;
+  public $humor;
+  public $id;
+  public $ims;
+  public $interests;
+  public $jobInterests;
+  public $languagesSpoken;
+  public $livingArrangement;
+  public $lookingFor;
+  public $movies;
+  public $music;
+  public $organizations;
+  public $name;
+  public $networkPresence;
+  public $nickname;
+  public $pets;
+  public $phoneNumbers;
+  public $photos;
+  public $politicalViews;
+  public $profileSong;
+  public $profileUrl;
+  public $profileVideo;
+  public $quotes;
+  public $relationshipStatus;
+  public $religion;
+  public $romance;
+  public $scaredOf;
+  public $sexualOrientation;
+  public $smoker;
+  public $sports;
+  public $status;
+  public $tags;
+  public $thumbnailUrl;
+  public $utcOffset;
+  public $turnOffs;
+  public $turnOns;
+  public $tvShows;
+  public $urls;
+
+  // Note: Not in the opensocial js person object directly
+  public $isOwner = false;
+  public $isViewer = false;
+
+  public function __construct($id, $name) {
+    $this->id = $id;
+    $this->name = $name;
+  }
+
+  private function setFieldImpl($fieldName, $value) {
+    // treat empty singular/plural fields as null so they don't pollute the output
+    if ($value === '' || (is_array($value) && ! count($value))) {
+      $value = null;
+    }
+    $this->$fieldName = $value;
+  }
+
+  /**
+   * Returns the field value for the given fieldName, if present.
+   * @param $fieldName name of the contact field, e.g. "displayName"
+   */
+  public function getFieldByName($fieldName) {
+    if (isset($this->$fieldName)) {
+      return $this->$fieldName;
+    }
+    return null;
+  }
+
+  public function getAboutMe() {
+    return $this->aboutMe;
+  }
+
+  public function setAboutMe($aboutMe) {
+    $this->setFieldImpl('aboutMe', $aboutMe);
+  }
+
+  public function getAccounts() {
+    return $this->accounts;
+  }
+
+  public function setAccounts($accounts) {
+    $this->setFieldImpl('accounts', $accounts);
+  }
+
+  public function getActivities() {
+    return $this->activities;
+  }
+
+  public function setActivities($activities) {
+    $this->setFieldImpl('activities', $activities);
+  }
+
+  public function getAddresses() {
+    return $this->addresses;
+  }
+
+  public function setAddresses($addresses) {
+    $this->setFieldImpl('addresses', $addresses);
+  }
+
+  public function getAge() {
+    return $this->age;
+  }
+
+  public function setAge($age) {
+    $this->setFieldImpl('age', $age);
+  }
+
+  public function getBirthday() {
+    return $this->birthday;
+  }
+
+  public function setBirthday($birthday) {
+    $birthday = date('Y-m-d', strtotime($birthday));
+    $this->setFieldImpl('birthday', $birthday);
+  }
+
+  public function getBodyType() {
+    return $this->bodyType;
+  }
+
+  public function setBodyType($bodyType) {
+    $this->setFieldImpl('bodyType', $bodyType);
+  }
+
+  public function getBooks() {
+    return $this->books;
+  }
+
+  public function setBooks($books) {
+    $this->setFieldImpl('books', $books);
+  }
+
+  public function getCars() {
+    return $this->cars;
+  }
+
+  public function setCars($cars) {
+    $this->setFieldImpl('cars', $cars);
+  }
+
+  public function getChildren() {
+    return $this->children;
+  }
+
+  public function setChildren($children) {
+    $this->setFieldImpl('children', $children);
+  }
+
+  public function getCurrentLocation() {
+    return $this->currentLocation;
+  }
+
+  public function setCurrentLocation($currentLocation) {
+    $this->setFieldImpl('currentLocation', $currentLocation);
+  }
+
+  public function getDisplayName() {
+    return $this->displayName;
+  }
+
+  public function setDisplayName($displayName) {
+    $this->setFieldImpl('displayName', $displayName);
+  }
+
+  public function getDrinker() {
+    return $this->drinker;
+  }
+
+  public function setDrinker($drinker) {
+    $this->setFieldImpl('drinker', $drinker);
+  }
+
+  public function getEmails() {
+    return $this->emails;
+  }
+
+  public function setEmails($emails) {
+    $this->setFieldImpl('emails', $emails);
+  }
+
+  public function getEthnicity() {
+    return $this->ethnicity;
+  }
+
+  public function setEthnicity($ethnicity) {
+    $this->setFieldImpl('ethnicity', $ethnicity);
+  }
+
+  public function getFashion() {
+    return $this->fashion;
+  }
+
+  public function setFashion($fashion) {
+    $this->setFieldImpl('fashion', $fashion);
+  }
+
+  public function getFood() {
+    return $this->food;
+  }
+
+  public function setFood($food) {
+    $this->setFieldImpl('food', $food);
+  }
+
+  public function getGender() {
+    return $this->gender;
+  }
+
+  public function setGender($gender) {
+    $this->setFieldImpl('gender', $gender);
+  }
+
+  public function getHappiestWhen() {
+    return $this->happiestWhen;
+  }
+
+  public function setHappiestWhen($happiestWhen) {
+    $this->setFieldImpl('happiestWhen', $happiestWhen);
+  }
+
+  public function getHeroes() {
+    return $this->heroes;
+  }
+
+  public function setHeroes($heroes) {
+    $this->setFieldImpl('heroes', $heroes);
+  }
+
+  public function getHasApp() {
+    return $this->hasApp;
+  }
+
+  public function setHasApp($hasApp) {
+    $this->setFieldImpl('hasApp', $hasApp);
+  }
+
+  public function getHumor() {
+    return $this->humor;
+  }
+
+  public function setHumor($humor) {
+    $this->setFieldImpl('humor', $humor);
+  }
+
+  public function getId() {
+    return $this->id;
+  }
+
+  public function setId($id) {
+    $this->setFieldImpl('id', $id);
+  }
+
+  public function getIms() {
+    return $this->ims;
+  }
+
+  public function setIms($ims) {
+    $this->setFieldImpl('ims', $ims);
+  }
+
+  public function getInterests() {
+    return $this->interests;
+  }
+
+  public function setInterests($interests) {
+    $this->setFieldImpl('interests', $interests);
+  }
+
+  public function getJobInterests() {
+    return $this->jobInterests;
+  }
+
+  public function setJobInterests($jobInterests) {
+    $this->setFieldImpl('jobInterests', $jobInterests);
+  }
+
+  public function getLanguagesSpoken() {
+    return $this->languagesSpoken;
+  }
+
+  public function setLanguagesSpoken($languagesSpoken) {
+    $this->setFieldImpl('languagesSpoken', $languagesSpoken);
+  }
+
+  public function getLivingArrangement() {
+    return $this->livingArrangement;
+  }
+
+  public function setLivingArrangement($livingArrangement) {
+    $this->setFieldImpl('livingArrangement', $livingArrangement);
+  }
+
+  public function getLookingFor() {
+    return $this->lookingFor;
+  }
+
+  public function setLookingFor($lookingFor) {
+    $this->setFieldImpl('lookingFor', new EnumLookingFor($lookingFor));
+  }
+
+  public function getMovies() {
+    return $this->movies;
+  }
+
+  public function setMovies($movies) {
+    $this->setFieldImpl('movies', $movies);
+  }
+
+  public function getMusic() {
+    return $this->music;
+  }
+
+  public function setMusic($music) {
+    $this->setFieldImpl('music', $music);
+  }
+
+  public function getName() {
+    return $this->name;
+  }
+
+  public function setName($name) {
+    $this->setFieldImpl('name', $name);
+  }
+
+  public function getNetworkPresence() {
+    return $this->networkPresence;
+  }
+
+  public function setNetworkPresence($networkPresence) {
+    $this->setFieldImpl('networkPresence', new EnumPresence($networkPresence));
+  }
+
+  public function getNickname() {
+    return $this->nickname;
+  }
+
+  public function setNickname($nickname) {
+    $this->nickname = $nickname;
+    $this->setFieldImpl('nickname', $nickname);
+  }
+
+  public function getOrganizations() {
+    return $this->organizations;
+  }
+
+  public function setOrganizations($organizations) {
+    $this->setFieldImpl('organizations', $organizations);
+  }
+
+  public function getPets() {
+    return $this->pets;
+  }
+
+  public function setPets($pets) {
+    $this->setFieldImpl('pets', $pets);
+  }
+
+  public function getPhoneNumbers() {
+    return $this->phoneNumbers;
+  }
+
+  public function setPhoneNumbers($phoneNumbers) {
+    $this->setFieldImpl('phoneNumbers', $phoneNumbers);
+  }
+
+  public function getPhotos() {
+    return $this->photos;
+  }
+
+  public function setPhotos($photos) {
+    $this->setFieldImpl('photos', $photos);
+  }
+
+  public function getPoliticalViews() {
+    return $this->politicalViews;
+  }
+
+  public function setPoliticalViews($politicalViews) {
+    $this->setFieldImpl('politicalViews', $politicalViews);
+  }
+
+  public function getProfileSong() {
+    return $this->profileSong;
+  }
+
+  public function setProfileSong($profileSong) {
+    $this->setFieldImpl('profileSong', $profileSong);
+  }
+
+  public function getProfileUrl() {
+    return $this->profileUrl;
+  }
+
+  public function setProfileUrl($profileUrl) {
+    $this->setFieldImpl('profileUrl', $profileUrl);
+  }
+
+  public function getProfileVideo() {
+    return $this->profileVideo;
+  }
+
+  public function setProfileVideo($profileVideo) {
+    $this->setFieldImpl('profileVideo', $profileVideo);
+  }
+
+  public function getQuotes() {
+    return $this->quotes;
+  }
+
+  public function setQuotes($quotes) {
+    $this->setFieldImpl('quotes', $quotes);
+  }
+
+  public function getRelationshipStatus() {
+    return $this->relationshipStatus;
+  }
+
+  public function setRelationshipStatus($relationshipStatus) {
+    $this->setFieldImpl('relationshipStatus', $relationshipStatus);
+  }
+
+  public function getReligion() {
+    return $this->religion;
+  }
+
+  public function setReligion($religion) {
+    $this->religion = $religion;
+  }
+
+  public function getRomance() {
+    return $this->romance;
+  }
+
+  public function setRomance($romance) {
+    $this->setFieldImpl('romance', $romance);
+  }
+
+  public function getScaredOf() {
+    return $this->scaredOf;
+  }
+
+  public function setScaredOf($scaredOf) {
+    $this->setFieldImpl('scaredOf', $scaredOf);
+  }
+
+  public function getSexualOrientation() {
+    return $this->sexualOrientation;
+  }
+
+  public function setSexualOrientation($sexualOrientation) {
+    $this->setFieldImpl('sexualOrientation', $sexualOrientation);
+  }
+
+  public function getSmoker() {
+    return $this->smoker;
+  }
+
+  public function setSmoker($smoker) {
+    $this->setFieldImpl('smoker', new EnumSmoker($smoker));
+  }
+
+  public function getSports() {
+    return $this->sports;
+  }
+
+  public function setSports($sports) {
+    $this->setFieldImpl('sports', $sports);
+  }
+
+  public function getStatus() {
+    return $this->status;
+  }
+
+  public function setStatus($status) {
+    $this->setFieldImpl('status', $status);
+  }
+
+  public function getTags() {
+    return $this->tags;
+  }
+
+  public function setTags($tags) {
+    $this->setFieldImpl('tags', $tags);
+  }
+
+  public function getThumbnailUrl() {
+    return $this->thumbnailUrl;
+  }
+
+  public function setThumbnailUrl($thumbnailUrl) {
+    $this->setFieldImpl('thumbnailUrl', $thumbnailUrl);
+  }
+
+  public function getUtcOffset() {
+    return $this->utcOffset;
+  }
+
+  public function setUtcOffset($utcOffset) {
+    // TODO: validate +00:00 format here?
+    $sign = ($utcOffset >= 0) ? "+" : "-";
+    $utcOffset = date('h:i', strtotime($utcOffset));
+    $utcOffset = $sign . $utcOffset;
+    $this->setFieldImpl('utcOffset', $utcOffset);
+  }
+
+  public function getTurnOffs() {
+    return $this->turnOffs;
+  }
+
+  public function setTurnOffs($turnOffs) {
+    $this->setFieldImpl('turnOffs', $turnOffs);
+  }
+
+  public function getTurnOns() {
+    return $this->turnOns;
+  }
+
+  public function setTurnOns($turnOns) {
+    $this->setFieldImpl('turnOns', $turnOns);
+  }
+
+  public function getTvShows() {
+    return $this->tvShows;
+  }
+
+  public function setTvShows($tvShows) {
+    $this->setFieldImpl('tvShows', $tvShows);
+  }
+
+  public function getUrls() {
+    return $this->urls;
+  }
+
+  public function setUrls($urls) {
+    $this->setFieldImpl('urls', $urls);
+  }
+
+  public function getIsOwner() {
+    return $this->isOwner;
+  }
+
+  public function setIsOwner($isOwner) {
+    $this->setFieldImpl('isOwner', $isOwner);
+  }
+
+  public function getIsViewer() {
+    return $this->isViewer;
+  }
+
+  public function setIsViewer($isViewer) {
+    $this->setFieldImpl('isViewer', $isViewer);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Phone.php b/trunk/php/src/apache/shindig/social/model/Phone.php
new file mode 100644
index 0000000..99716ab
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Phone.php
@@ -0,0 +1,29 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Phone
+ *
+ */
+class Phone extends ListField {
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Photo.php b/trunk/php/src/apache/shindig/social/model/Photo.php
new file mode 100644
index 0000000..f24bb12
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Photo.php
@@ -0,0 +1,29 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * WRONG URL -> http://code.google.com/apis/opensocial/docs/0.7/reference/opensocial.Photo.Field.html
+ *
+ */
+class Photo extends ListField {
+}
diff --git a/trunk/php/src/apache/shindig/social/model/Url.php b/trunk/php/src/apache/shindig/social/model/Url.php
new file mode 100644
index 0000000..2295c41
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/model/Url.php
@@ -0,0 +1,47 @@
+<?php
+namespace apache\shindig\social\model;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * see
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v081/opensocial-reference#opensocial.Url
+ */
+class Url extends ListField {
+  public $value;
+  public $linkText;
+  public $type;
+  public $primary;
+
+  public function __construct($value, $type, $linkText, $primary = null) {
+    $this->value = $value;
+    $this->type = $type;
+    $this->linkText = $linkText;
+    $this->primary = $primary;
+  }
+
+  public function getLinkText() {
+    return $this->linkText;
+  }
+
+  public function setLinkText($linkText) {
+    $this->linkText = $linkText;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/oauth/OAuthSecurityToken.php b/trunk/php/src/apache/shindig/social/oauth/OAuthSecurityToken.php
new file mode 100644
index 0000000..0eb048f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/oauth/OAuthSecurityToken.php
@@ -0,0 +1,81 @@
+<?php
+namespace apache\shindig\social\oauth;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * SecurityToken derived from a successful OAuth validation.
+ */
+class OAuthSecurityToken extends SecurityToken {
+  private $userId;
+  private $appUrl;
+  private $appId;
+  private $domain;
+  
+  private $authenticationMode;
+
+  public function __construct($userId, $appUrl, $appId, $domain) {
+    $this->userId = $userId;
+    $this->appUrl = $appUrl;
+    $this->appId = $appId;
+    $this->domain = $domain;
+  }
+
+  public function isAnonymous() {
+    return ($this->userId == null);
+  }
+
+  public function getOwnerId() {
+    return $this->userId;
+  }
+
+  public function getViewerId() {
+    return $this->userId;
+  }
+
+  public function getAppId() {
+    return $this->appId;
+  }
+
+  public function getDomain() {
+    return $this->domain;
+  }
+
+  public function getAppUrl() {
+    return $this->appUrl;
+  }
+
+  public function getModuleId() {
+    return null;
+  }
+
+  public function toSerialForm() {
+    return "OAuthSecurityToken[userId=$userId,appUrl=$appUrl,appId=$appId,domain=$domain]";
+  }
+  
+  public function getAuthenticationMode() {
+    return $this->authenticationMode;
+  }
+  
+  public function setAuthenticationMode($mode) {
+    $this->authenticationMode = $mode;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/sample/DefaultInvalidateService.php b/trunk/php/src/apache/shindig/social/sample/DefaultInvalidateService.php
new file mode 100644
index 0000000..585535e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/sample/DefaultInvalidateService.php
@@ -0,0 +1,153 @@
+<?php
+namespace apache\shindig\social\sample;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\Cache;
+use apache\shindig\common\Config;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\social\spi\InvalidateService;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class DefaultInvalidateService implements InvalidateService {
+
+  /**
+   * @var Cache
+   */
+  private $invalidationEntry;
+  
+  /**
+   * @var Cache
+   */
+  private $cache;
+  
+  private static $marker = null;
+  
+  /**
+   * @var Cache
+   */
+  private static $makerCache = null;
+  
+  private static $TOKEN_PREFIX = 'INV_TOK_';
+  
+  public function __construct(Cache $cache) {
+    $this->cache = $cache;
+    $this->invalidationEntry = Cache::createCache(Config::get('data_cache'), 'InvalidationEntry');
+    if (self::$makerCache == null) {
+      self::$makerCache = Cache::createCache(Config::get('data_cache'), 'MarkerCache');
+      $value = self::$makerCache->expiredGet('marker');
+      if ($value['found']) {
+        self::$marker = $value['data'];
+      } else {
+        self::$marker = 0;
+        self::$makerCache->set('marker', self::$marker);
+      }
+    }
+  }
+  /**
+   * Invalidate a set of cached resources that are part of the application specification itself.
+   * This includes gadget specs, manifests and message bundles
+   * @param uris of content to invalidate
+   * @param token identifying the calling application
+   */
+  function invalidateApplicationResources(Array $uris, SecurityToken $token) {
+    foreach($uris as $uri) {
+      $request = new RemoteContentRequest($uri);
+      $this->cache->invalidate($request->toHash());
+    }
+  }
+
+  /**
+   * Invalidate all cached resources where the specified user ids were used as either the
+   * owner or viewer id when a signed or OAuth request was made for the content by the application
+   * identified in the security token.
+   * @param opensocialIds Set of user ids to invalidate authenticated/signed content for
+   * @param token identifying the calling application
+   */
+  function invalidateUserResources(Array $opensocialIds, SecurityToken $token) {
+    foreach($opensocialIds as $opensocialId) {
+      ++self::$marker;
+      self::$makerCache->set('marker', self::$marker);
+      $this->invalidationEntry->set($this->getKey($opensocialId, $token), self::$marker);
+    }
+  }
+
+  /**
+   * Is the specified request still valid. If the request is signed or authenticated
+   * has its content been invalidated by a call to invalidateUserResource subsequent to the
+   * response being cached.
+   */
+  function isValid(RemoteContentRequest $request) {
+    if ($request->getAuthType() == RemoteContentRequest::$AUTH_NONE) {
+      return true;
+    }
+    return $request->getInvalidation() == $this->getInvalidationMark($request);
+  }
+
+  /**
+   * Mark the request prior to caching it so that subsequent calls to isValid can detect
+   * if it has been invalidated.
+   */
+  function markResponse(RemoteContentRequest $request) {
+    $mark = $this->getInvalidationMark($request);
+    if ($mark) {
+      $request->setInvalidation($mark);
+    }
+  }
+  
+  /**
+   * @return string
+   */
+  private function getKey($userId, SecurityToken $token) {
+    $pos = strrpos($userId, ':');
+    if ($pos !== false) {
+      $userId = substr($userId, $pos + 1);
+    }
+    
+    if ($token->getAppId()) {
+      return DefaultInvalidateService::$TOKEN_PREFIX . $token->getAppId() . '_' . $userId;
+    }
+    return DefaultInvalidateService::$TOKEN_PREFIX . $token->getAppUrl() . '_' . $userId;
+  }
+  
+  private function getInvalidationMark(RemoteContentRequest $request) {
+    $token = $request->getToken();
+    if (!$token) {
+      return null;
+    }
+    $currentInvalidation = '';
+    if ($token->getOwnerId()) {
+      $ownerKey = $this->getKey($token->getOwnerId(), $token);
+      $cached = $this->invalidationEntry->expiredGet($ownerKey);
+      $ownerStamp = $cached['found'] ? $cached['data'] : false;
+    }
+    if ($token->getViewerId()) {
+      $viewerKey = $this->getKey($token->getViewerId(), $token);
+      $cached = $this->invalidationEntry->expiredGet($viewerKey);
+      $viewerStamp = $cached['found'] ? $cached['data'] : false;
+    }
+    if (isset($ownerStamp)) {
+      $currentInvalidation = $currentInvalidation . 'o=' . $ownerStamp . ';'; 
+    }
+    if (isset($viewerStamp)) {
+      $currentInvalidation = $currentInvalidation . 'v=' . $viewerStamp . ';'; 
+    }
+    return $currentInvalidation;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/sample/JsonDbOpensocialService.php b/trunk/php/src/apache/shindig/social/sample/JsonDbOpensocialService.php
new file mode 100644
index 0000000..15c832f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/sample/JsonDbOpensocialService.php
@@ -0,0 +1,1025 @@
+<?php
+namespace apache\shindig\social\sample;
+use apache\shindig\social\service\SocialSpiException;
+use apache\shindig\social\spi\CollectionOptions;
+use apache\shindig\social\spi\RestfulCollection;
+use apache\shindig\social\spi\DataCollection;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\social\service\ResponseError;
+use apache\shindig\common\Config;
+use apache\shindig\social\model\MediaItem;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\social\spi\GroupId;
+use apache\shindig\social\spi\UserId;
+use apache\shindig\social\spi\ActivityService;
+use apache\shindig\social\spi\PersonService;
+use apache\shindig\social\spi\AppDataService;
+use apache\shindig\social\spi\GroupService;
+use apache\shindig\social\spi\MessagesService;
+use apache\shindig\social\spi\AlbumService;
+use apache\shindig\social\spi\MediaItemService;
+
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Implementation of supported services backed by a JSON DB
+ */
+class JsonDbOpensocialService implements ActivityService, PersonService, AppDataService, GroupService, MessagesService, AlbumService, MediaItemService {
+
+  /**
+   * The DB
+   */
+  private $db;
+
+  /**
+   * db["activities"] -> Array<Person>
+   */
+  private static $PEOPLE_TABLE = "people";
+
+  /**
+   * db["people"] -> Map<Person.Id, Array<Activity>>
+   */
+  private static $ACTIVITIES_TABLE = "activities";
+
+  /**
+   * db["messages"] : Map<Person.Id, MessageCollection>
+   */
+  private static $MESSAGES_TABLE = "messages";
+
+  /**
+   * db["albums"] -> Map<Person.Id, Map<Album.Id, Album>>
+   */
+  private static $ALBUMS_TABLE = "albums";
+
+  /**
+   * db["mediaItems"] -> Map<Album.Id, Map<MediaItem.Id, MediaItem>>
+   */
+  private static $MEDIA_ITEMS_TABLE = "mediaItems";
+
+  /**
+   * db["data"] -> Map<Person.Id, Map<String, String>>
+   */
+  private static $DATA_TABLE = "data";
+
+  /**
+   * db["groups"] -> Map<Person.Id, Array<Group>>
+   */
+  private static $GROUPS_TABLE = "groups";
+
+  /**
+   * db["friendLinks"] -> Map<Person.Id, Array<Person.Id>>
+   */
+  private static $FRIEND_LINK_TABLE = "friendLinks";
+
+  /**
+   * db["userApplications"] -> Map<Person.Id, Array<Application Ids>>
+   */
+  private static $USER_APPLICATIONS_TABLE = "userApplications";
+
+  private $allPeople = null;
+
+  private $allData = null;
+
+  private $allGroups = null;
+  
+  private $allActivities = null;
+
+  private $allMessageCollections = null;
+
+  protected function getDbFilename() {
+    return sys_get_temp_dir() . '/' . 'ShindigDb' . getenv('BUILD_TAG') . '.json';
+  }
+
+  public function getDb() {
+    try {
+      $fileName = $this->getDbFilename();
+      if (file_exists($fileName)) {
+        if (! is_readable($fileName)) {
+          throw new SocialSpiException("Could not read temp json db file: $fileName, check permissions", ResponseError::$INTERNAL_ERROR);
+        }
+        $cachedDb = file_get_contents($fileName);
+        $jsonDecoded = json_decode($cachedDb, true);
+        if ($jsonDecoded == $cachedDb || $jsonDecoded == null) {
+          throw new SocialSpiException("Failed to decode the json db", ResponseError::$INTERNAL_ERROR);
+        }
+        return $jsonDecoded;
+      } else {
+        $jsonDb = Config::get('jsondb_path');
+        if (! file_exists($jsonDb) || ! is_readable($jsonDb)) {
+          throw new SocialSpiException("Could not read json db file: $jsonDb, check if the file exists & has proper permissions", ResponseError::$INTERNAL_ERROR);
+        }
+        $dbConfig = @file_get_contents($jsonDb);
+        $contents = preg_replace('/(?<!http:|https:)\/\/.*$/m', '', preg_replace('@/\\*.*?\\*/@s', '', $dbConfig));
+        $jsonDecoded = json_decode($contents, true);
+        if ($jsonDecoded == $contents || $jsonDecoded == null) {
+          throw new SocialSpiException("Failed to decode the json db", ResponseError::$INTERNAL_ERROR);
+        }
+        $this->saveDb($jsonDecoded);
+        return $jsonDecoded;
+      }
+    } catch (\Exception $e) {
+      throw new SocialSpiException("An error occured while reading/writing the json db: " . $e->getMessage(), ResponseError::$INTERNAL_ERROR);
+    }
+  }
+
+  private function saveDb($db) {
+    if (! @file_put_contents($this->getDbFilename(), json_encode($db))) {
+      throw new \Exception("Could not save json db: " . $this->getDbFileName());
+    }
+  }
+
+  public function resetDb() {
+    @unlink($this->getDbFilename());
+  }
+
+  private function getAllPeople() {
+    $db = $this->getDb();
+    $peopleTable = $db[self::$PEOPLE_TABLE];
+    foreach ($peopleTable as $people) {
+      $this->allPeople[$people['id']] = $people;
+    }
+    $db[self::$PEOPLE_TABLE] = $this->allPeople;
+    return $this->allPeople;
+  }
+
+  private function getAllData() {
+    $db = $this->getDb();
+    $dataTable = $db[self::$DATA_TABLE];
+    foreach ($dataTable as $key => $value) {
+      $this->allData[$key] = $value;
+    }
+    $db[self::$DATA_TABLE] = $this->allData;
+    return $this->allData;
+  }
+
+  private function getAllGroups() {
+    $db = $this->getDb();
+    $this->allGroups = $db[self::$GROUPS_TABLE];
+    return $this->allGroups;
+  }
+
+  private function getAllActivities() {
+    $db = $this->getDb();
+    $activitiesTable = $db[self::$ACTIVITIES_TABLE];
+    foreach ($activitiesTable as $key => $value) {
+      $this->allActivities[$key] = $value;
+    }
+    $db[self::$ACTIVITIES_TABLE] = $this->allActivities;
+    return $this->allActivities;
+  }
+
+  private function getAllMessageCollections() {
+    $db = $this->getDb();
+    $messagesTable = $db[self::$MESSAGES_TABLE];
+    foreach ($messagesTable as $key => $value) {
+      $this->allMessageCollections[$key] = $value;
+    }
+    $db[self::$MESSAGES_TABLE] = $this->allMessageCollections;
+    return $this->allMessageCollections;
+  }
+
+  private function getAllAlbums() {
+    $db = $this->getDb();
+    $albumTable = isset($db[self::$ALBUMS_TABLE]) ? $db[self::$ALBUMS_TABLE] : array();
+    $allAlbums = array();
+    foreach ($albumTable as $key => $value) {
+      $allAlbums[$key] = $value;
+    }
+    return $allAlbums;
+  }
+
+  private function getAllMediaItems() {
+    $db = $this->getDb();
+    $mediaItemsTable = isset($db[self::$MEDIA_ITEMS_TABLE]) ? $db[self::$MEDIA_ITEMS_TABLE] : array();
+    $allMediaItems = array();
+    foreach ($mediaItemsTable as $key => $value) {
+      $allMediaItems[$key] = $value;
+    }
+    return $allMediaItems;
+  }
+
+  private function getPeopleWithApp($appId) {
+    $peopleWithApp = array();
+    $db = $this->getDb();
+    $userApplicationsTable = $db[self::$USER_APPLICATIONS_TABLE];
+    foreach ($userApplicationsTable as $key => $value) {
+      if (in_array($appId, $userApplicationsTable[$key])) {
+        $peopleWithApp[] = $key;
+      }
+    }
+    return $peopleWithApp;
+  }
+
+  public function getPerson($userId, $groupId, $fields, SecurityToken $token) {
+    if (! is_object($groupId)) {
+      throw new SocialSpiException("Not Implemented", ResponseError::$NOT_IMPLEMENTED);
+    }
+    $person = $this->getPeople($userId, $groupId, new CollectionOptions(), $fields, $token);
+    if (is_array($person->getEntry())) {
+      $person = $person->getEntry();
+      if (is_array($person) && count($person) == 1) {
+        return array_pop($person);
+      }
+    }
+    throw new SocialSpiException("Person not found", ResponseError::$BAD_REQUEST);
+  }
+
+  private function getMutualFriends($ids, $friendId) {
+    $db = $this->getDb();
+    $friendsTable = $db[self::$FRIEND_LINK_TABLE];
+    if (is_array($friendsTable) && count($friendsTable) && isset($friendsTable[$friendId])) {
+      $friendIds = $friendsTable[$friendId];
+      $mutualFriends = array_intersect($ids, $friendIds);
+    }
+    return $mutualFriends;
+  }
+
+  public function getPeople($userId, $groupId, CollectionOptions $options, $fields, SecurityToken $token) {
+    $sortOrder = $options->getSortOrder();
+    $filter = $options->getFilterBy();
+    $filterOp = $options->getFilterOperation();
+    $filterValue = $options->getFilterValue();
+    $first = $options->getStartIndex();
+    $max = $options->getCount();
+    $networkDistance = $options->getNetworkDistance();
+    $ids = $this->getIdSet($userId, $groupId, $token);
+    $allPeople = $this->getAllPeople();
+    if ($filter == "@friends" && $filterOp == "contains" && isset($filterValue)) {
+      if ($options->getFilterValue() == '@viewer') {
+        $filterValue = $token->getViewerId();
+      } elseif ($options->getFilterValue() == '@owner') {
+        $filterValue = $token->getOwnerId();
+      }
+      $ids = $this->getMutualFriends($ids, $filterValue);
+    }
+    if (! $token->isAnonymous() && $filter == "hasApp") {
+      $appId = $token->getAppId();
+      $peopleWithApp = $this->getPeopleWithApp($appId);
+    }
+    $people = array();
+    foreach ($ids as $id) {
+      if ($filter == "hasApp" && ! in_array($id, $peopleWithApp)) {
+        continue;
+      }
+      $person = null;
+      if (is_array($allPeople) && isset($allPeople[$id])) {
+        $person = $allPeople[$id];
+        if (! $token->isAnonymous() && $id == $token->getViewerId()) {
+          $person['isViewer'] = true;
+        }
+        if (! $token->isAnonymous() && $id == $token->getOwnerId()) {
+          $person['isOwner'] = true;
+        }   
+
+        $people[] = $person;
+      }
+    }
+    if ($sortOrder == 'name') {
+      usort($people, array($this, 'comparator'));
+    }
+
+    try {
+      $people = $this->filterResults($people, $options);
+    } catch (\Exception $e) {
+      $people['filtered'] = 'false';
+    }
+    
+   if ($fields) { 
+        $people = self::adjustFields($people, $fields);
+    }
+
+    //TODO: The samplecontainer doesn't support any filters yet. We should fix this.
+    $totalSize = count($people);
+    $collection = new RestfulCollection($people, $options->getStartIndex(), $totalSize);
+    $collection->setItemsPerPage($options->getCount());
+    return $collection;
+  }
+
+  private function filterResults($results, $options) {
+    if (! $options->getFilterBy()) {
+      return $results; // no filtering specified
+    }
+    $filterBy = $options->getFilterBy();
+    $op = $options->getFilterOperation();
+    if (! $op) {
+      $op = CollectionOptions::FILTER_OP_EQUALS; // use this container-specific default
+    }
+    $value = $options->getFilterValue();
+    $filteredResults = array();
+    $numFilteredResults = 0;
+    foreach ($results as $id => $person) {
+      if ($this->passesFilter($person, $filterBy, $op, $value)) {
+        $filteredResults[$id] = $person;
+        $numFilteredResults ++;
+      }
+    }
+    return $filteredResults;
+  }
+
+  private function passesFilter($entity, $filterBy, $op, $value) {
+    $fieldValue = $entity[$filterBy];
+    if (! $fieldValue || (is_array($fieldValue) && ! count($fieldValue))) {
+      return false; // person is missing the field being filtered for
+    }
+    if ($op == CollectionOptions::FILTER_OP_PRESENT) {
+      return true; // person has a non-empty value for the requested field
+    }
+    if (! $value) {
+      return false; // can't do an equals/startswith/contains filter on an empty filter value
+    }
+    // grab string value for comparison
+    if (is_array($fieldValue)) {
+      // plural fields match if any instance of that field matches
+      foreach ($fieldValue as $field) {
+        if ($this->passesStringFilter($field, $op, $value)) {
+          return true;
+        }
+      }
+    } else {
+      return $this->passesStringFilter($fieldValue, $op, $value);
+    }
+
+    return false;
+  }
+
+  public function getPersonData($userId, GroupId $groupId, $appId, $fields, SecurityToken $token) {
+    if (! isset($fields[0])) {
+      $fields[0] = '@all';
+    }
+    $db = $this->getDb();
+    $allData = $this->getAllData();
+    $friendsTable = $db[self::$FRIEND_LINK_TABLE];
+    $data = array();
+    $ids = $this->getIdSet($userId, $groupId, $token);
+    foreach ($ids as $id) {
+      if (isset($allData[$id])) {
+        $allPersonData = $allData[$id];
+        $personData = array();
+        foreach (array_keys($allPersonData) as $key) {
+          if (in_array($key, $fields) || in_array("@all", $fields)) {
+            $personData[$key] = $allPersonData[$key];
+          }
+        }
+        $data[$id] = $personData;
+      }
+    }
+    return new DataCollection($data);
+  }
+
+  public function updatePersonData(UserId $userId, GroupId $groupId, $appId, $fields, $values, SecurityToken $token) {
+    $db = $this->getDb();
+    foreach ($fields as $key => $present) {
+      if (! $this->isValidKey($present)) {
+        throw new SocialSpiException("The person app data key had invalid characters", ResponseError::$BAD_REQUEST);
+      }
+    }
+    $allData = $this->getAllData();
+    $tmpUserId = $userId->getUserId($token);
+    $person = isset($allData[$tmpUserId]) ? $allData[$tmpUserId] : array();
+    switch ($groupId->getType()) {
+      case 'self':
+        foreach ($fields as $key => $present) {
+          $value = isset($values[$present]) ? @$values[$present] : null;
+          $person[$present] = $value;
+        }
+        break;
+      default:
+        throw new SocialSpiException("We don't support updating data in batches yet", ResponseError::$NOT_IMPLEMENTED);
+        break;
+    }
+    $allData[$userId->getUserId($token)] = $person;
+    $db[self::$DATA_TABLE] = $allData;
+    $this->saveDb($db);
+    return null;
+  }
+
+  public function deletePersonData($userId, GroupId $groupId, $appId, $fields, SecurityToken $token) {
+    $db = $this->getDb();
+    $allData = $this->getAllData();
+    if ($fields == null || (isset($fields[0]) && $fields[0] == '*')) {
+      $allData[$userId->getUserId($token)] = null;
+      $db[self::$DATA_TABLE] = $allData;
+      $this->saveDb($db);
+      return null;
+    }
+    foreach ($fields as $key => $present) {
+      if (! $this->isValidKey($key)) {
+        throw new SocialSpiException("The person app data key had invalid characters", ResponseError::$BAD_REQUEST);
+      }
+    }
+    switch ($groupId->getType()) {
+      case 'self':
+        foreach ($fields as $key => $present) {
+          $value = isset($values[$key]) ? null : @$values[$key];
+          $person[$key] = $value;
+        }
+        $allData[$userId->getUserId($token)] = $person;
+        $db[self::$DATA_TABLE] = $allData;
+        $this->saveDb($db);
+        break;
+      default:
+        throw new SocialSpiException("We don't support updating data in batches yet", ResponseError::$NOT_IMPLEMENTED);
+        break;
+    }
+    return null;
+  }
+
+  public function getPersonGroups($userId, GroupId $groupId, SecurityToken $token) {
+    $allGroups = $this->getAllGroups();
+    $ids = $this->getIdSet($userId, $groupId, $token);
+    $output = array();
+    foreach ($ids as $id) {
+      if (isset($allGroups[$id])) {
+        $output[$id] = $allGroups[$id];
+      }
+    }
+    return $output;
+  }
+
+
+  public function getActivity($userId, $groupId, $appdId, $fields, $activityId, SecurityToken $token) {
+    $activities = $this->getActivities($userId, $groupId, $appdId, null, null, null, null, $fields, array(
+        $activityId), $token);
+    if ($activities instanceof RestfulCollection) {
+      $activities = $activities->getEntry();
+      foreach ($activities as $activity) {
+        if ($activity->getId() == $activityId) {
+          return $activity;
+        }
+      }
+    }
+    throw new SocialSpiException("Activity not found", ResponseError::$NOT_FOUND);
+  }
+
+  public function getActivities($userIds, $groupId, $appId, $sortBy, $filterBy, $filterOp, $filterValue, $startIndex, $count, $fields, $activityIds, $token) {
+    $db = $this->getDb();
+    $friendsTable = $db[self::$FRIEND_LINK_TABLE];
+    $ids = array();
+    $ids = $this->getIdSet($userIds, $groupId, $token);
+    $allActivities = $this->getAllActivities();
+    $activities = array();
+    foreach ($ids as $id) {
+      if (isset($allActivities[$id])) {
+        $personsActivities = $allActivities[$id];
+        $activities = array_merge($activities, $personsActivities);
+        if ($fields) {
+          $newPersonsActivities = array();
+          foreach ($personsActivities as $activity) {
+            $newActivity = array();
+            foreach ($fields as $field => $present) {
+              $newActivity[$present] = $activity[$present];
+            }
+            $newPersonsActivities[] = $newActivity;
+          }
+          $personsActivities = $newPersonsActivities;
+          $activities = $personsActivities;
+        }
+        if ($filterBy && $filterValue) {
+          $newActivities = array();
+          foreach ($activities as $activity) {
+            if (array_key_exists($filterBy, $activity)) {
+              if ($this->passesStringFilter($activity[$filterBy], $filterOp, $filterValue)) {
+                $newActivities[] = $activity;
+              }
+            } else {
+              throw new SocialSpiException("Invalid filterby parameter", ResponseError::$NOT_FOUND);
+            }
+          }
+          $activities = $newActivities;
+        }
+      }
+    }
+    $totalResults = count($activities);
+    if (! $totalResults) {
+      throw new SocialSpiException("Activity not found", ResponseError::$NOT_FOUND);
+    }
+    $activities = array_slice($activities, $startIndex, $count);
+    $ret = new RestfulCollection($activities, $startIndex, $totalResults);
+    $ret->setItemsPerPage($count);
+    return $ret;
+  }
+
+  /*
+   * to check the activity against filter
+   */
+  private function passesStringFilter($fieldValue, $filterOp, $filterValue) {
+    switch ($filterOp) {
+      case CollectionOptions::FILTER_OP_EQUALS:
+        return $fieldValue == $filterValue;
+      case CollectionOptions::FILTER_OP_CONTAINS:
+        return strpos($fieldValue, $filterValue) !== false;
+      case CollectionOptions::FILTER_OP_STARTSWITH:
+        return strpos($fieldValue, $filterValue) === 0;
+      default:
+        throw new \Exception('unrecognized filterOp');
+    }
+  }
+
+  public function createActivity($userId, $groupId, $appId, $fields, $activity, SecurityToken $token) {
+    $db = $this->getDb();
+    $activitiesTable = $this->getAllActivities();
+    $activity['appId'] = $token->getAppId();
+    try {
+      if (! isset($activitiesTable[$userId->getUserId($token)])) {
+        $activitiesTable[$userId->getUserId($token)] = array();
+      }
+      $activity['id'] = count($activitiesTable[$userId->getUserId($token)]) + 1;
+      array_push($activitiesTable[$userId->getUserId($token)], $activity);
+      $db[self::$ACTIVITIES_TABLE] = $activitiesTable;
+      $this->saveDb($db);
+      // Should this return something to show success?
+    } catch (\Exception $e) {
+      throw new SocialSpiException("Activity can't be created: " . $e->getMessage(), ResponseError::$INTERNAL_ERROR);
+    }
+  }
+
+  public function deleteActivities($userId, $groupId, $appId, $activityIds, SecurityToken $token) {
+    $db = $this->getDb();
+    $activitiesTable = $this->getAllActivities();
+    if (! isset($activitiesTable[$userId->getUserId($token)])) {
+      throw new SocialSpiException("Activity not found.", ResponseError::$BAD_REQUEST);
+    }
+    $newActivities = array();
+    foreach ($activitiesTable[$userId->getUserId($token)] as $activity) {
+      $found = false;
+      foreach ($activityIds as $id) {
+        if ($activity['id'] == $id) {
+          $found = true;
+        }
+      }
+      if (! $found) {
+        array_push($newActivities, $activity);
+      }
+    }
+    if (count($newActivities) == count($activitiesTable[$userId->getUserId($token)])) {
+      throw new SocialSpiException("Activities not found.", ResponseError::$BAD_REQUEST);
+    }
+    $activitiesTable[$userId->getUserId($token)] = $newActivities;
+    $db[self::$ACTIVITIES_TABLE] = $activitiesTable;
+    $this->saveDb($db);
+  }
+
+  public function createMessage($userId, $msgCollId, $message, $token) {
+    $db = $this->getDb();
+    $messagesTable = $this->getAllMessageCollections();
+    if ($msgCollId == '@outbox') {
+      $msgCollId = 'privateMessage';
+    }
+    if (! isset($messagesTable[$userId->getUserId($token)]) || ! isset($messagesTable[$userId->getUserId($token)][$msgCollId])) {
+      throw new SocialSpiException("Message collection not found.", ResponseError::$BAD_REQUEST);
+    }
+    $msgColl = $messagesTable[$userId->getUserId($token)][$msgCollId];
+    if (! isset($msgColl['messages'])) {
+      $msgColl['messages'] = array();
+    }
+    $message['id'] = count($msgColl['messages']) + 1;
+    $msgColl['messages'][$message['id']] = $message;
+    if (isset($msgColl['total'])) {
+      ++ $msgColl['total'];
+    } else {
+      $msgColl['total'] = 1;
+    }
+    if (isset($msgColl['unread'])) {
+      ++ $msgColl['unread'];
+    } else {
+      $msgColl['unread'] = 1;
+    }
+    $messagesTable[$userId->getUserId($token)][$msgCollId] = $msgColl;
+    $db[self::$MESSAGES_TABLE] = $messagesTable;
+    $this->saveDb($db);
+  }
+
+  public function updateMessage($userId, $msgCollId, $message, $token) {
+    throw new SocialSpiException("Not implemented", ResponseError::$NOT_IMPLEMENTED);
+  }
+
+  public function deleteMessages($userId, $msgCollId, $messageIds, $token) {
+    $db = $this->getDb();
+    $messagesTable = $this->getAllMessageCollections();
+    if ($msgCollId == '@inbox' || $msgCollId == '@outbox') {
+      $msgCollId = 'privateMessage';
+    }
+    if (! isset($messagesTable[$userId->getUserId($token)]) || ! isset($messagesTable[$userId->getUserId($token)][$msgCollId])) {
+      throw new SocialSpiException("Message collection not found.", ResponseError::$BAD_REQUEST);
+    }
+    $msgColl = $messagesTable[$userId->getUserId($token)][$msgCollId];
+    foreach ($messageIds as $id) {
+      if (! isset($msgColl['messages']) || ! isset($msgColl['messages'][$id])) {
+        throw new SocialSpiException("Message not found.", ResponseError::$BAD_REQUEST);
+      }
+    }
+    foreach ($messageIds as $id) {
+      unset($msgColl['messages'][$id]);
+    }
+    if (isset($msgColl['total'])) {
+      $msgColl['total'] -= count($messageIds);
+    }
+    $messagesTable[$userId->getUserId($token)][$msgCollId] = $msgColl;
+    $db[self::$MESSAGES_TABLE] = $messagesTable;
+    $this->saveDb($db);
+  }
+
+  public function getMessages($userId, $msgCollId, $fields, $msgIds, $options, $token) {
+    $collections = $this->getAllMessageCollections();
+    $results = array();
+    // TODO: Handles @inbox and @outbox.
+    if ($msgCollId == '@outbox' || $msgCollId == '@inbox') {
+      $msgCollId = 'privateMessage';
+    }
+    if (isset($collections[$userId->getUserId($token)]) && isset($collections[$userId->getUserId($token)][$msgCollId])) {
+      $msgColl = $collections[$userId->getUserId($token)][$msgCollId];
+      if (! isset($msgColl['messages'])) {
+        $msgColl['messages'] = array();
+      }
+      if (empty($msgIds)) {
+        $results = $msgColl['messages'];
+      } else {
+        foreach ($msgColl['messages'] as $message) {
+          if (in_array($message['id'], $msgIds)) {
+            $results[] = $message;
+          }
+        }
+      }
+      if ($options) {
+        $results = $this->filterResults($results, $options);
+      }
+      if ($fields) {
+        $results = self::adjustFields($results, $fields);
+      }
+      return self::paginateResults($results, $options);
+    } else {
+      throw new SocialSpiException("Message collections not found", ResponseError::$NOT_FOUND);
+    }
+  }
+
+  public function createMessageCollection($userId, $msgCollection, $token) {
+    $db = $this->getDb();
+    $messagesTable = $this->getAllMessageCollections();
+    try {
+      if (! isset($messagesTable[$userId->getUserId($token)])) {
+        $messagesTable[$userId->getUserId($token)] = array();
+      } else if (isset($msgCollection['id']) && isset($messagesTable[$userId->getUserId($token)][$msgCollection['id']])) {
+        throw new SocialSpiException("Message collection already exists.", ResponseError::$BAD_REQUEST);
+      }
+      $msgCollection['total'] = 0;
+      $msgCollection['unread'] = 0;
+      $msgCollection['updated'] = time();
+      $id = count($messagesTable[$userId->getUserId($token)]);
+      $msgCollection['id'] = $id;
+      $messagesTable[$userId->getUserId($token)][$id] = $msgCollection;
+      $db[self::$MESSAGES_TABLE] = $messagesTable;
+      $this->saveDb($db);
+      return $msgCollection;
+    } catch (\Exception $e) {
+      throw new SocialSpiException("Message collection can't be created: " . $e->getMessage(), ResponseError::$INTERNAL_ERROR);
+    }
+  }
+
+  public function updateMessageCollection($userId, $msgCollection, $token) {
+    $db = $this->getDb();
+    $messagesTable = $this->getAllMessageCollections();
+    if (! isset($messagesTable[$userId->getUserId($token)]) || ! isset($messagesTable[$userId->getUserId($token)][$msgCollection['id']])) {
+      throw new SocialSpiException("Message collection not found.", ResponseError::$BAD_REQUEST);
+    }
+    // The total number of messages in the collection shouldn't be updated.
+    $msgCollection['total'] = $messagesTable[$userId->getUserId($token)][$msgCollection['id']]['total'];
+    $msgCollection['updated'] = time();
+    $messagesTable[$userId->getUserId($token)][$msgCollection['id']] = $msgCollection;
+    $db[self::$MESSAGES_TABLE] = $messagesTable;
+    $this->saveDb($db);
+  }
+
+  public function deleteMessageCollection($userId, $msgCollId, $token) {
+    $db = $this->getDb();
+    $messagesTable = $this->getAllMessageCollections();
+    try {
+      if (! isset($messagesTable[$userId->getUserId($token)]) || ! isset($messagesTable[$userId->getUserId($token)][$msgCollId])) {
+        throw new SocialSpiException("Message collection not found.", ResponseError::$NOT_FOUND);
+      } else {
+        unset($messagesTable[$userId->getUserId($token)][$msgCollId]);
+      }
+      $db[self::$MESSAGES_TABLE] = $messagesTable;
+      $this->saveDb($db);
+    } catch (\Exception $e) {
+      throw new SocialSpiException("Message collection can't be created: " . $e->getMessage(), ResponseError::$INTERNAL_ERROR);
+    }
+  }
+
+  public function getMessageCollections($userId, $fields, $options, $token) {
+    $all = $this->getAllMessageCollections();
+    $results = array();
+    if (isset($all[$userId->getUserId($token)])) {
+      $results = $all[$userId->getUserId($token)];
+    } else {
+      return RestfulCollection::createFromEntry(array());
+    }
+    if ($options) {
+      $results = $this->filterResults($results, $options);
+    }
+    if (empty($results)) {
+      throw new SocialSpiException("Message collections not found", ResponseError::$NOT_FOUND);
+    }
+    foreach ($results as $id => $messageCollection) {
+      if (! isset($results[$id]["id"])) {
+        $results[$id]["id"] = $id;
+      }
+      $results[$id]["total"] = isset($results[$id]["messages"]) ? count($results[$id]["messages"]) : 0;
+      $results[$id]["unread"] = $results[$id]["total"];
+    }
+    if ($fields) {
+      $results = self::adjustFields($results, $fields);
+    }
+    return self::paginateResults($results, $options);
+  }
+
+  public function getAlbums($userId, $groupId, $albumIds, $options, $fields, $token) {
+    $all = $this->getAllAlbums();
+    $allMediaItems = $this->getAllMediaItems();
+    $results = array();
+    if (! isset($all[$userId->getUserId($token)])) {
+      return RestfulCollection::createFromEntry(array());
+    }
+    $albumIds = array_unique($albumIds);
+    foreach ($all[$userId->getUserId($token)] as $id => $album) {
+      if (empty($albumIds) || in_array($id, $albumIds)) {
+        $results[] = $album;
+        $album['mediaItemCount'] = isset($allMediaItems[$id]) ? count($allMediaItems[$id]) : 0;
+      }
+    }
+    if ($options) {
+      $results = $this->filterResults($results, $options);
+    }
+    if ($fields) {
+      $results = self::adjustFields($results, $fields);
+    }
+    return self::paginateResults($results, $options);
+  }
+
+  public function createAlbum($userId, $groupId, $album, $token) {
+    $all = $this->getAllAlbums();
+    $cnt = 0;
+    foreach ($all as $key => $value) {
+      $cnt += count($value);
+    }
+    $id = 'testIdPrefix' . $cnt;
+    $album['id'] = $id;
+    $album['ownerId'] = $userId->getUserId($token);
+    if (isset($album['mediaType'])) {
+      $album['mediaType'] = strtoupper($album['mediaType']);
+      if (! in_array($album['mediaType'], MediaItem::$TYPES)) {
+        unset($album['mediaType']);
+      }
+    }
+    if (! isset($all[$userId->getUserId($token)])) {
+      $all[$userId->getUserId($token)] = array();
+    }
+    $all[$userId->getUserId($token)][$id] = $album;
+    $db = $this->getDb();
+    $db[self::$ALBUMS_TABLE] = $all;
+    $this->saveDb($db);
+    return $album;
+  }
+
+  public function updateAlbum($userId, $groupId, $album, $token) {
+    $all = $this->getAllAlbums();
+    if (! $album['id'] || ! $all[$userId->getUserId($token)] || ! $all[$userId->getUserId($token)][$album['id']]) {
+      throw new SocialSpiException("Album not found.", ResponseError::$BAD_REQUEST);
+    }
+    $origin = $all[$userId->getUserId($token)][$album['id']];
+    if ($origin['ownerId'] != $userId->getUserId($token)) {
+      throw new SocialSpiException("Not the owner.", ResponseError::$UNAUTHORIZED);
+    }
+    $album['ownerId'] = $origin['ownerId'];
+    if (isset($album['mediaType'])) {
+      $album['mediaType'] = strtoupper($album['mediaType']);
+      if (! in_array($album['mediaType'], MediaItem::$TYPES)) {
+        unset($album['mediaType']);
+      }
+    }
+    $all[$userId->getUserId($token)][$album['id']] = $album;
+
+    $db = $this->getDb();
+    $db[self::$ALBUMS_TABLE] = $all;
+    $this->saveDb($db);
+  }
+
+  public function deleteAlbum($userId, $groupId, $albumId, $token) {
+    $all = $this->getAllAlbums();
+    $albumId = $albumId[0];
+    if (! $albumId || ! $all[$userId->getUserId($token)] || ! $all[$userId->getUserId($token)][$albumId]) {
+      throw new SocialSpiException("Album not found.", ResponseError::$BAD_REQUEST);
+    }
+    if ($all[$userId->getUserId($token)][$albumId]['ownerId'] != $userId->getUserId($token)) {
+      throw new SocialSpiException("Not the owner.", ResponseError::$UNAUTHORIZED);
+    }
+    unset($all[$userId->getUserId($token)][$albumId]);
+    $db = $this->getDb();
+    $db[self::$ALBUMS_TABLE] = $all;
+    $this->saveDb($db);
+  }
+
+  public function getMediaItems($userId, $groupId, $albumId, $mediaItemIds, $options, $fields, $token) {
+    $all = $this->getAllMediaItems();
+    $results = array();
+    if (! isset($all[$albumId])) {
+      return RestfulCollection::createFromEntry(array());
+    }
+    $mediaItemIds = array_unique($mediaItemIds);
+    foreach ($all[$albumId] as $id => $mediaItem) {
+      if (empty($mediaItemIds) || in_array($id, $mediaItemIds)) {
+        $results[] = $mediaItem;
+      }
+    }
+    if ($options) {
+      $results = $this->filterResults($results, $options);
+    }
+    if ($fields) {
+      $results = self::adjustFields($results, $fields);
+    }
+    return self::paginateResults($results, $options);
+  }
+
+  public function createMediaItem($userId, $groupId, $mediaItem, $file, $token) {
+    $all = $this->getAllMediaItems();
+    $albumId = $mediaItem['albumId'];
+    $id = isset($all[$albumId]) ? (count($all[$albumId]) + 1) : 0;
+    $mediaItem['id'] = $id;
+    $mediaItem['lastUpdated'] = time();
+    $mediaItem['created'] = $mediaItem['lastUpdated'];
+    $mediaItem['numComments'] = 0;
+    if (isset($mediaItem['type'])) {
+      $mediaItem['type'] = strtoupper($mediaItem['type']);
+      if (! in_array($mediaItem['type'], MediaItem::$TYPES)) {
+        unset($mediaItem['type']);
+      }
+    }
+    if (isset($all[$albumId]) && (! $all[$albumId])) {
+      $all[$albumId] = array();
+    }
+    $all[$albumId][$id] = $mediaItem;
+    $db = $this->getDb();
+    $db[self::$MEDIA_ITEMS_TABLE] = $all;
+    $this->saveDb($db);
+    return $mediaItem;
+  }
+
+  public function updateMediaItem($userId, $groupId, $mediaItem, $token) {
+    $all = $this->getAllMediaItems();
+    if (! $all[$mediaItem['albumId']] || ! $all[$mediaItem['albumId']][$mediaItem['id']]) {
+      throw new SocialSpiException("MediaItem not found.", ResponseError::$BAD_REQUEST);
+    }
+
+    $origin = $all[$mediaItem['albumId']][$mediaItem['id']];
+    $mediaItem['lastUpdated'] = time();
+    $mediaItem['created'] = $origin['created'];
+    $mediaItem['numComments'] = $origin['numComments'];
+    if (isset($mediaItem['type'])) {
+      $mediaItem['type'] = strtoupper($mediaItem['type']);
+      if (! in_array($mediaItem['type'], MediaItem::$TYPES)) {
+        unset($mediaItem['type']);
+      }
+    }
+
+    $all[$mediaItem['albumId']][$mediaItem['id']] = $mediaItem;
+    $db = $this->getDb();
+    $db[self::$MEDIA_ITEMS_TABLE] = $all;
+    $this->saveDb($db);
+  }
+
+  public function deleteMediaItems($userId, $groupId, $albumId, $mediaItemIds, $token) {
+    $all = $this->getAllMediaItems();
+    if (! $all[$albumId]) {
+      throw new SocialSpiException("MediaItem not found.", ResponseError::$BAD_REQUEST);
+    }
+    foreach ($mediaItemIds as $id) {
+      if (! $all[$albumId][$id]) {
+        throw new SocialSpiException("MediaItem not found.", ResponseError::$BAD_REQUEST);
+      }
+    }
+    foreach ($mediaItemIds as $id) {
+      unset($all[$albumId][$id]);
+    }
+    $db = $this->getDb();
+    $db[self::$MEDIA_ITEMS_TABLE] = $all;
+    $this->saveDb($db);
+  }
+
+  /**
+   * Paginates the results set according to the critera specified by the options.
+   */
+  private static function paginateResults($results, $options) {
+    if (! $options) {
+      return RestfulCollection::createFromEntry($results);
+    } else {
+      $startIndex = $options->getStartIndex();
+      $count = $options->getCount();
+      $totalResults = count($results);
+      // NOTE: Assumes the index is 0 based.
+      $results = array_slice($results, $startIndex, $count);
+      $ret = new RestfulCollection($results, $startIndex, $totalResults);
+      $ret->setItemsPerPage($count);
+      return $ret;
+    }
+  }
+
+  /**
+   * Removes the unnecessary fields by sets the requested fiedls only.
+   */
+  private static function adjustFields($results, $fields) {
+    if (empty($fields) || empty($results) || in_array('@all', $fields)) {
+      return $results;
+    }
+    $newResults = array();
+    foreach ($results as $entity) {
+      $newEntity = array();
+      foreach ($fields as $field) {
+        $newEntity[$field] = isset($entity[$field]) ? $entity[$field] : null;
+      }
+      $newResults[] = $newEntity;
+    }
+    return $newResults;
+  }
+
+  /**
+   * Determines whether the input is a valid key.
+   *
+   * @param key the key to validate.
+   * @return true if the key is a valid appdata key, false otherwise.
+   */
+  public static function isValidKey($key) {
+    if (empty($key)) {
+      return false;
+    }
+    for ($i = 0; $i < strlen($key); ++ $i) {
+      $c = substr($key, $i, 1);
+      if (($c >= 'a' && $c <= 'z') || ($c >= 'A' && $c <= 'Z') || ($c >= '0' && $c <= '9') || ($c == '-') || ($c == '_') || ($c == '.')) {
+        continue;
+      }
+      return false;
+    }
+    return true;
+  }
+
+  private function comparator($person, $person1) {
+    $name = $person['name']['unstructured'];
+    $name1 = $person1['name']['unstructured'];
+    if ($name == $name1) {
+      return 0;
+    }
+    return ($name < $name1) ? - 1 : 1;
+  }
+
+  /**
+   * Get the set of user id's from a user or collection of users, and group
+   * Code taken from http://code.google.com/p/partuza/source/browse/trunk/Shindig/PartuzaService.php
+   */
+  private function getIdSet($user, GroupId $group, SecurityToken $token) {
+    $ids = array();
+    $db = $this->getDb();
+    $friendsTable = $db[self::$FRIEND_LINK_TABLE];
+    if ($user instanceof UserId) {
+      $userId = $user->getUserId($token);
+      if ($group == null) {
+        return array($userId);
+      }
+      switch ($group->getType()) {
+        case 'self':
+          $ids[] = $userId;
+          break;
+        case 'all':
+        case 'friends':
+          if (is_array($friendsTable) && count($friendsTable) && isset($friendsTable[$userId])) {
+            $ids = $friendsTable[$userId];
+          }
+          break;
+        default:
+          return new ResponseItem(NOT_IMPLEMENTED, "We don't support fetching data in batches yet", null);
+          break;
+      }
+    } elseif (is_array($user)) {
+      $ids = array();
+      foreach ($user as $id) {
+        $ids = array_merge($ids, $this->getIdSet($id, $group, $token));
+      }
+    }
+    return $ids;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/ActivityHandler.php b/trunk/php/src/apache/shindig/social/service/ActivityHandler.php
new file mode 100644
index 0000000..3946549
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/ActivityHandler.php
@@ -0,0 +1,129 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\IllegalArgumentException;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ActivityHandler extends DataRequestHandler {
+  
+  private static $ACTIVITY_ID_PATH = "/activities/{userId}/{groupId}/appId/{activityId}";
+
+  public function __construct() {
+    parent::__construct('activity_service');
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleDelete(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$ACTIVITY_ID_PATH);
+    $userIds = $requestItem->getUsers();
+    $activityIds = $requestItem->getListParameter("activityId");
+    if (empty($userIds)) {
+      throw new \InvalidArgumentException("No userId specified");
+    } elseif (count($userIds) > 1) {
+      throw new \InvalidArgumentException("Multiple userIds not supported");
+    }
+    return $this->service->deleteActivities($userIds[0], $requestItem->getGroup(), $requestItem->getAppId(), $activityIds, $requestItem->getToken());
+  }
+
+  /**
+   * /activities/{userId}/{groupId}/{optionalActvityId}
+   *
+   * examples:
+   * /activities/john.doe/@self/1
+   * /activities/john.doe/@self
+   * /activities/john.doe/@friends
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleGet(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$ACTIVITY_ID_PATH);
+    $userIds = $requestItem->getUsers();
+    $optionalActivityIds = $requestItem->getListParameter("activityId");
+    // Preconditions
+    if (empty($userIds)) {
+      throw new \InvalidArgumentException("No userId specified");
+    } elseif (count($userIds) > 1 && ! empty($optionalActivityIds)) {
+      throw new IllegalArgumentException("Cannot fetch same activityIds for multiple userIds");
+    }
+    if (! empty($optionalActivityIds)) {
+      if (count($optionalActivityIds) == 1) {
+        return $this->service->getActivity($userIds[0], $requestItem->getGroup(), $requestItem->getAppId(), $requestItem->getFields(), $optionalActivityIds[0], $requestItem->getToken());
+      } else {
+        return $this->service->getActivities($userIds[0], $requestItem->getGroup(), $requestItem->getAppId(), $requestItem->getSortBy(), $requestItem->getFilterBy(), $requestItem->getFilterOperation(), $requestItem->getFilterValue(), $requestItem->getStartIndex(), $requestItem->getCount(), $requestItem->getFields(), $optionalActivityIds, $requestItem->getToken());
+      }
+    }
+    return $this->service->getActivities($userIds, $requestItem->getGroup(), $requestItem->getAppId(), $requestItem->getSortBy(), $requestItem->getFilterBy(), $requestItem->getFilterOperation(), $requestItem->getFilterValue(), $requestItem->getStartIndex(), $requestItem->getCount(), $requestItem->getFields(), null, $requestItem->getToken());
+  }
+
+  /**
+   * /activities/{userId}/@self
+   *
+   * examples:
+   * /activities/@viewer/@self/@app
+   * /activities/john.doe/@self
+   * - postBody is an activity object
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handlePost(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$ACTIVITY_ID_PATH);
+    $userIds = $requestItem->getUsers();
+    $activityIds = $requestItem->getListParameter("activityId");
+    if (empty($userIds)) {
+      throw new \InvalidArgumentException("No userId specified");
+    } elseif (count($userIds) > 1) {
+      throw new \InvalidArgumentException("Multiple userIds not supported");
+    }
+    // TODO This seems reasonable to allow on PUT but we don't have an update verb.
+    if (! empty($activityIds)) {
+      throw new \InvalidArgumentException("Cannot specify activityId in create");
+    }
+    /*
+     * Note, on just about all types of social networks you would only allow activities to be created when the owner == viewer, and the userId == viewer as well, in code this would mean:
+     *  if ($token->getOwnerId() != $token->getViewerId() || $token->getViewerId() != $userId->getUserId($token)) {
+     *    throw new SocialSpiException("Create activity permission denied.", ResponseError::$UNAUTHORIZED);
+     *  }
+     */
+    return $this->service->createActivity($userIds[0], $requestItem->getGroup(), $requestItem->getAppId(), $requestItem->getFields(), $requestItem->getParameter("activity"), $requestItem->getToken());
+  }
+
+  /**
+   * /activities/{userId}/@self
+   *
+   * examples:
+   * /activities/john.doe/@self
+   * - postBody is an activity object
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handlePut(RequestItem $requestItem) {
+    return $this->handlePost($requestItem);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/AlbumHandler.php b/trunk/php/src/apache/shindig/social/service/AlbumHandler.php
new file mode 100644
index 0000000..7b6228e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/AlbumHandler.php
@@ -0,0 +1,123 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\social\spi\CollectionOptions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * AlbumHandler checks POST/PUT/GET/DELETE requests params and call the album service.
+ */
+class AlbumHandler extends DataRequestHandler {
+  private static $ALBUM_PATH = "/albums/{userId}/{groupId}/{albumId}";
+
+  public function __construct() {
+    parent::__construct('album_service');
+  }
+
+  /**
+   * Deletes the album. The URI structure: /{userId}/{groupId}/{albumId}
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleDelete(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$ALBUM_PATH);
+
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+    $albumIds = $requestItem->getListParameter('albumId');
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be singular value.");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+    HandlerPreconditions::requireNotEmpty($albumIds, "albumId must be specified.");
+
+    $this->service->deleteAlbum($userIds[0], $groupId, $albumIds, $requestItem->getToken());
+  }
+
+  /**
+   * Gets the albums. The URI structure: /{userId}/{groupId}/{albumId}+.
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleGet(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$ALBUM_PATH);
+
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be singular value.");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+
+    $options = new CollectionOptions($requestItem);
+    $fields = $requestItem->getFields();
+
+    $albumIds = $requestItem->getListParameter('albumId');
+
+    return $this->service->getAlbums($userIds[0], $groupId, $albumIds, $options, $fields, $requestItem->getToken());
+  }
+
+  /**
+   * Creates an album. The URI structure: /{userId}/{groupId}.
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handlePost(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$ALBUM_PATH);
+
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+    $album = $requestItem->getParameter('album');
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be of singular value");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+    HandlerPreconditions::requireNotEmpty($album, "album must be specified.");
+
+    return $this->service->createAlbum($userIds[0], $groupId, $album, $requestItem->getToken());
+  }
+
+  /**
+   * Updates the album. The URI structure: /{userId}/{groupId}/{albumId}
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handlePut(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$ALBUM_PATH);
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+    $albumIds = $requestItem->getListParameter('albumId');
+    $album = $requestItem->getParameter('album');
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be singular value");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+    HandlerPreconditions::requireSingular($albumIds, "albumId must be singular value.");
+    HandlerPreconditions::requireNotEmpty($album, "album must be specified.");
+
+    $album['id'] = $albumIds[0];
+
+    return $this->service->updateAlbum($userIds[0], $groupId, $album, $requestItem->getToken());
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/AppDataHandler.php b/trunk/php/src/apache/shindig/social/service/AppDataHandler.php
new file mode 100644
index 0000000..0635906
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/AppDataHandler.php
@@ -0,0 +1,149 @@
+<?php
+namespace apache\shindig\social\service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class AppDataHandler extends DataRequestHandler {
+  private static $APP_DATA_PATH = "/appdata/{userId}/{groupId}/appId";
+
+  public function __construct() {
+    parent::__construct('app_data_service');
+  }
+
+  /**
+   * /people/{userId}/{groupId}/{appId}
+   * - fields={field1, field2}
+   *
+   * examples:
+   * /appdata/john.doe/@friends/app?fields=count
+   * /appdata/john.doe/@self/app
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleDelete(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$APP_DATA_PATH);
+    $userIds = $requestItem->getUsers();
+    if (count($userIds) < 1) {
+      throw new \InvalidArgumentException("No userId specified");
+    } elseif (count($userIds) > 1) {
+      throw new \InvalidArgumentException("Multiple userIds not supported");
+    }
+    return $this->service->deletePersonData($userIds[0], $requestItem->getGroup(), $requestItem->getAppId(), $requestItem->getFields(), $requestItem->getToken());
+  }
+
+  /**
+   * /appdata/{userId}/{groupId}/{appId}
+   * - fields={field1, field2}
+   *
+   * examples:
+   * /appdata/john.doe/@friends/app?fields=count
+   * /appdata/john.doe/@self/app
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleGet(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$APP_DATA_PATH);
+    $userIds = $requestItem->getUsers();
+    if (count($userIds) < 1) {
+      throw new \InvalidArgumentException("No userId(s) specified");
+    }
+    return $this->service->getPersonData($userIds[0], $requestItem->getGroup(), $requestItem->getAppId(), $requestItem->getFields(), $requestItem->getToken());
+  }
+
+  /**
+   * /appdata/{userId}/{groupId}/{appId}
+   * - fields={field1, field2}
+   *
+   * examples:
+   * /appdata/john.doe/@friends/app?fields=count
+   * /appdata/john.doe/@self/app
+   *
+   * The post data should be a regular json object. All of the fields vars will
+   * be pulled from the values and set on the person object. If there are no
+   * fields vars then all of the data will be overridden.
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handlePost(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$APP_DATA_PATH);
+    $userIds = $requestItem->getUsers();
+    if (count($userIds) < 1) {
+      throw new \InvalidArgumentException("No userId specified");
+    } elseif (count($userIds) > 1) {
+      throw new \InvalidArgumentException("Multiple userIds not supported");
+    }
+    $values = $requestItem->getParameter("data");
+    // this used to be $requestItem->getFields() instead of using the fields, but that makes no sense to me
+    // better to detect the fields depending on input right?
+    $fields = array();
+    foreach (array_keys($values) as $key) {
+      $fields[] = $key;
+      if (! $this->isValidKey($key)) {
+        throw new SocialSpiException("One or more of the app data keys are invalid: " . $key, ResponseError::$BAD_REQUEST);
+      }
+    }
+    $this->service->updatePersonData($userIds[0], $requestItem->getGroup(), $requestItem->getAppId(), $fields, $values, $requestItem->getToken());
+  }
+
+  /**
+   * /appdata/{userId}/{groupId}/{appId}
+   * - fields={field1, field2}
+   *
+   * examples:
+   * /appdata/john.doe/@friends/app?fields=count
+   * /appdata/john.doe/@self/app
+   *
+   * The post data should be a regular json object. All of the fields vars will
+   * be pulled from the values and set on the person object. If there are no
+   * fields vars then all of the data will be overridden.
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handlePut(RequestItem $requestItem) {
+    return $this->handlePost($requestItem);
+  }
+
+  /**
+   * Determines whether the input is a valid key.
+   *
+   * @param string $key the key to validate.
+   * @return boolean true if the key is a valid appdata key, false otherwise.
+   */
+  public static function isValidKey($key) {
+    if (empty($key)) {
+      return false;
+    }
+    for ($i = 0; $i < strlen($key); ++ $i) {
+      $c = substr($key, $i, 1);
+      if (($c >= 'a' && $c <= 'z') || ($c >= 'A' && $c <= 'Z') || ($c >= '0' && $c <= '9') || ($c == '-') || ($c == '_') || ($c == '.')) {
+        continue;
+      }
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/DataRequestHandler.php b/trunk/php/src/apache/shindig/social/service/DataRequestHandler.php
new file mode 100644
index 0000000..792a344
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/DataRequestHandler.php
@@ -0,0 +1,191 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\Config;
+use apache\shindig\common\ConfigException;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+abstract class DataRequestHandler {
+  protected $service;
+
+  /**
+   *
+   * @param string $serviceName
+   */
+  public function __construct($serviceName) {
+    try {
+      $service = trim(Config::get($serviceName));
+      if (!empty($service)) {
+        $this->service = new $service();
+      }
+    } catch (ConfigException $e) {
+      // Do nothing. If service name is not specified in the config file.
+      // All the requests to the handler will throw not implemented exception.
+      // The handler function should invoke checkService method before serving. 
+    }
+  }
+  
+  private static $GET_SYNONYMS = array("get");
+  private static $CREATE_SYNONYMS = array("post", "create");
+  private static $UPDATE_SYNONYMS = array("put", "update");
+  private static $DELETE_SYNONYMS = array("delete");
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem 
+   */
+  public function handleItem(RequestItem $requestItem) {
+    try {
+      $token = $requestItem->getToken();
+      $method = strtolower($requestItem->getMethod());
+      if ($token->isAnonymous() && ! in_array($method, self::$GET_SYNONYMS)) {
+        // Anonymous requests are only allowed to GET data (not create/edit/delete)
+        throw new SocialSpiException("[$method] not allowed for anonymous users", ResponseError::$BAD_REQUEST);
+      } elseif (in_array($method, self::$GET_SYNONYMS)) {
+        $parameters = $requestItem->getParameters();
+        if (in_array("@supportedFields", $parameters, true)) {
+          $response = $this->getSupportedFields($parameters);
+        } else {
+          $response = $this->handleGet($requestItem);
+        }
+      } elseif (in_array($method, self::$UPDATE_SYNONYMS)) {
+        $response = $this->handlePut($requestItem);
+      } elseif (in_array($method, self::$DELETE_SYNONYMS)) {
+        $response = $this->handleDelete($requestItem);
+      } elseif (in_array($method, self::$CREATE_SYNONYMS)) {
+        $response = $this->handlePost($requestItem);
+      } else {
+        throw new SocialSpiException("Unserviced Http method type", ResponseError::$BAD_REQUEST);
+      }
+    } catch (SocialSpiException $e) {
+      $response = new ResponseItem($e->getCode(), $e->getMessage());
+    } catch (\Exception $e) {
+      $response = new ResponseItem(ResponseError::$INTERNAL_ERROR, "Internal error: " . $e->getMessage());
+    }
+    return $response;
+  }
+
+  /**
+   *
+   * @param int $appId
+   * @param SecurityToken $token
+   * @return int
+   */
+  static public function getAppId($appId, SecurityToken $token) {
+    if ($appId == '@app') {
+      return $token->getAppId();
+    } else {
+      return $appId;
+    }
+  }
+
+  /**
+   *
+   * @param string $string
+   * @return object
+   */
+  static public function convertToObject($string) {
+    //TODO should detect if it's atom/xml or json here really. assuming json for now
+    $decoded = json_decode($string);
+    if ($decoded == $string) {
+      throw new \Exception("Invalid JSON syntax");
+    }
+    return $decoded;
+  }
+
+  /**
+   *  To support people/@supportedFields and activity/@supportedFields
+   *
+   * @param array $parameters url parameters to get request type(people/activity)
+   * @return ResponseItem
+   */
+  public function getSupportedFields($parameters) {
+    $contextClass = Config::get('gadget_context_class');
+    $context = new $contextClass('GADGET');
+    $container = $context->getContainer();
+    $containerConfigClass = Config::get('container_config_class');
+    $containerConfig = new $containerConfigClass(Config::get('container_path'));
+    $config = $containerConfig->getConfig($container, 'gadgets.features');
+    $version = $this->getOpenSocialVersion($config);
+    $supportedFields = $config[$version]['supportedFields'];
+    if (in_array('people', $parameters)) {
+      $ret = $supportedFields['person'];
+    } else {
+      $ret = $supportedFields['activity'];
+    }
+    return new ResponseItem(null, null, $ret);
+  }
+
+  /**
+   *  To get OpenSocial version for getting supportedFields
+   *
+   * @param array $config configuration values from container's js files
+   * @return string
+   */
+  private function getOpenSocialVersion($config) {
+    $str = "opensocial-";
+    $version = array();
+    foreach ($config as $key => $value) {
+      if (substr($str, 0, strlen($key)) == $str) {
+        $version[] = $key;
+      }
+    }
+    if (! count($version)) {
+      throw new \Exception("Invalid container configuration, opensocial-x.y key not found");
+    }
+    rsort($version);
+    return $version[0];
+  }
+  
+  /**
+   * Checks whether the service is initialized.
+   */
+  protected function checkService() {
+    if (!$this->service) {
+      throw new SocialSpiException("Not Implemented.", ResponseError::$NOT_IMPLEMENTED);
+    }
+  }
+
+  /**
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  abstract public function handleDelete(RequestItem $requestItem);
+
+  /**
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  abstract public function handleGet(RequestItem $requestItem);
+
+  /**
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  abstract public function handlePost(RequestItem $requestItem);
+
+  /**
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  abstract public function handlePut(RequestItem $requestItem);
+}
diff --git a/trunk/php/src/apache/shindig/social/service/GroupHandler.php b/trunk/php/src/apache/shindig/social/service/GroupHandler.php
new file mode 100644
index 0000000..b30b304
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/GroupHandler.php
@@ -0,0 +1,77 @@
+<?php
+namespace apache\shindig\social\service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GroupHandler extends DataRequestHandler {
+  private static $GROUPS_PATH = "/groups/{userId}";
+
+  public function __construct() {
+    parent::__construct('group_service');
+  }
+
+  /**
+   * /groups/{userId}
+   *
+   * examples:
+   * /groups/john.doe?fields=count
+   * /groups/@me
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleGet(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$GROUPS_PATH);
+    $userIds = $requestItem->getUsers();
+    if (count($userIds) < 1) {
+      throw new \InvalidArgumentException("No userId(s) specified");
+    }
+    return $this->service->getPersonGroups($userIds[0], $requestItem->getGroup(), $requestItem->getToken());
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @throws SocialSpiException
+   */
+  public function handleDelete(RequestItem $requestItem) {
+    throw new SocialSpiException("You can't delete groups.", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @throws SocialSpiException
+   */
+  public function handlePut(RequestItem $requestItem) {
+    throw new SocialSpiException("You can't update groups.", ResponseError::$NOT_IMPLEMENTED);
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @throws SocialSpiException
+   */
+  public function handlePost(RequestItem $requestItem) {
+    throw new SocialSpiException("You can't add groups.", ResponseError::$NOT_IMPLEMENTED);
+  }
+
+}
diff --git a/trunk/php/src/apache/shindig/social/service/HandlerPreconditions.php b/trunk/php/src/apache/shindig/social/service/HandlerPreconditions.php
new file mode 100644
index 0000000..67b16fa
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/HandlerPreconditions.php
@@ -0,0 +1,86 @@
+<?php
+namespace apache\shindig\social\service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Preconditions for handlers.
+ */
+class HandlerPreconditions {
+
+  private function __construct() {}
+
+  /**
+   *
+   * @param mixed $item
+   * @param string $message
+   */
+  public static function requireNotEmpty($item, $message) {
+    if (empty($item)) {
+      throw new \InvalidArgumentException($message);
+    }
+  }
+
+  /**
+   *
+   * @param mixed $item
+   * @param string $message
+   */
+  public static function requireEmpty($item, $message) {
+    if (! empty($item)) {
+      throw new \InvalidArgumentException($message);
+    }
+  }
+
+  /**
+   *
+   * @param mixed $item
+   * @param string $message
+   */
+  public static function requireSingular($item, $message) {
+    self::requireNotEmpty($item, $message);
+    if (count($item) != 1) {
+      throw new \InvalidArgumentException($message);
+    }
+  }
+
+  /**
+   *
+   * @param mixed $item
+   * @param string $message
+   */
+  public static function requirePlural($item, $message) {
+    self::requireNotEmpty($item, $message);
+    if (count($item) <= 1) {
+      throw new \InvalidArgumentException($message);
+    }
+  }
+
+  /**
+   *
+   * @param mixed $item
+   * @param string $message
+   */
+  public static function requireCondition($cond, $message) {
+    if (! $cond) {
+      throw new \InvalidArgumentException($message);
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/HttpHandler.php b/trunk/php/src/apache/shindig/social/service/HttpHandler.php
new file mode 100644
index 0000000..3739f47
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/HttpHandler.php
@@ -0,0 +1,110 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\gadgets\MakeRequestOptions;
+use apache\shindig\common\Config;
+
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Handler for http.* requests.  This is required to support osapi.http, which
+ * has an implicit RPC service dependency.  More information about what we're
+ * supporting here is available at:
+ * http://www.opensocial.org/Technical-Resources/opensocial-spec-v09/OpenSocial-Specification.html#osapi.http
+ */
+class HttpHandler extends DataRequestHandler {
+
+  /**
+   * Yet another do nothing constructor.
+   */
+  public function __construct() {
+    // Nothing to see here.
+  }
+
+  /**
+   * Processes an RPC request for http data.
+   *
+   * @param RequestItem $requestItem The request parameters.
+   * @return array An array of content, status code, and headers from the
+   *     response.  The expected structure is undocumented in the spec, sadly.
+   *     TODO: Filter some/most headers from the response (waste of bandwidth).
+   */
+  public function handleItem(RequestItem $requestItem) {
+    try {
+      // We should only get RPC requests at this point.  There's a class cast
+      // here from RequestItem->RpcRequestItem, but PHP doesn't seem to
+      // complain.  
+      $options = MakeRequestOptions::fromRpcRequestItem($requestItem);
+      $makeRequestClass = Config::get('makerequest_class');
+      $makeRequest = new $makeRequestClass();
+      $contextClass = Config::get('gadget_context_class');
+      $context = new $contextClass('GADGET');
+      $response = $makeRequest->fetch($context, $options);
+
+      // try to decode json object here since in order
+      // to not break gadgets.io.makeRequest functionality
+      // $response->getResponseContent() has to return a string
+      $content = json_decode($response->getResponseContent(), true);
+
+      $result = array(
+        'content' => $content ? $content : $response->getResponseContent(),
+        'status' => $response->getHttpCode(),
+        'headers' => $response->getResponseHeaders()
+      );
+    } catch (SocialSpiException $e) {
+      $result = new ResponseItem($e->getCode(), $e->getMessage());
+    } catch (\Exception $e) {
+      $result = new ResponseItem(ResponseError::$INTERNAL_ERROR, "Internal error: " . $e->getMessage());
+    }
+    return $result;
+  }
+
+  /**
+   * Only RPC operations supported.
+   * @param RequestItem $request The request item.
+   */
+  public function handleDelete(RequestItem $request) {
+    throw new SocialSpiException("DELETE not allowed for http service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   * Only RPC operations supported.
+   * @param RequestItem $request The request item.
+   */
+  public function handlePut(RequestItem $request) {
+    throw new SocialSpiException("PUT not allowed for http service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   * Only RPC operations supported.
+   * @param RequestItem $request The request item.
+   */
+  public function handlePost(RequestItem $request) {
+    throw new SocialSpiException("POST not allowed for http service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   * Only RPC operations supported.
+   * @param RequestItem $request The request item.
+   */
+  public function handleGet(RequestItem $request) {
+    throw new SocialSpiException("GET not allowed for http service", ResponseError::$BAD_REQUEST);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/InvalidateHandler.php b/trunk/php/src/apache/shindig/social/service/InvalidateHandler.php
new file mode 100644
index 0000000..89a0bd5
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/InvalidateHandler.php
@@ -0,0 +1,134 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\Config;
+use apache\shindig\common\Cache;
+use apache\shindig\common\AuthenticationMode;
+use apache\shindig\common\ConfigException;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class InvalidateHandler extends DataRequestHandler {
+
+  private static $INVALIDATE_PATH = "/cache/invalidate";
+
+  private static $KEYS_PARAM = "invalidationKeys";
+
+  public function __construct() {
+    try {
+      $service = trim(Config::get('invalidate_service'));
+      if (!empty($service)) {
+        $cache = Cache::createCache(Config::get('data_cache'), 'RemoteContent');
+        $this->service = new $service($cache);
+      }
+    } catch (ConfigException $e) {
+      // Do nothing. If invalidate service is not specified in the config file.
+      // All the requests to the handler will throw not implemented exception.
+    }
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleItem(RequestItem $requestItem) {
+    try {
+      $method = strtolower($requestItem->getMethod());
+      $method = 'handle' . ucfirst($method);
+      $this->$method($requestItem);
+    } catch (SocialSpiException $e) {
+      $response = new ResponseItem($e->getCode(), $e->getMessage());
+    } catch (\Exception $e) {
+      $response = new ResponseItem(ResponseError::$INTERNAL_ERROR, "Internal error: " . $e->getMessage());
+    }
+    return $response;
+  }
+
+  /**
+   *
+   * @param RequestItem $request
+   * @throws SocialSpiException
+   */
+  public function handleDelete(RequestItem $request) {
+    throw new SocialSpiException("Http delete not allowed for invalidation service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   *
+   * @param RequestItem $request
+   * @throws SocialSpiException
+   */
+  public function handlePut(RequestItem $request) {
+    throw new SocialSpiException("Http put not allowed for invalidation service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   */
+  public function handlePost(RequestItem $request) {
+    $this->handleInvalidate($request);
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   */
+  public function handleGet(RequestItem $request) {
+    $this->handleInvalidate($request);
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   */
+  public function handleInvalidate(RequestItem $request) {
+    $this->checkService();
+    if (!$request->getToken()->getAppId() && !$request->getToken()->getAppUrl()) {
+      throw new SocialSpiException("Can't invalidate content without specifying application", ResponseError::$BAD_REQUEST);
+    }
+
+    $isBackendInvalidation = AuthenticationMode::$OAUTH_CONSUMER_REQUEST == $request->getToken()->getAuthenticationMode();
+    $invalidationKeys = $request->getListParameter('invalidationKeys');
+    $resources = array();
+    $userIds = array();
+    if ($request->getToken()->getViewerId()) {
+      $userIds[] = $request->getToken()->getViewerId();
+    }
+    foreach($invalidationKeys as $key) {
+      if (strpos($key, 'http') !== false) {
+        if (!$isBackendInvalidation) {
+          throw new SocialSpiException('Cannot flush application resources from a gadget. Must use OAuth consumer request');
+        }
+        $resources[] = $key;
+      } else {
+        if ($key == '@viewer') {
+          continue;
+        }
+        if (!$isBackendInvalidation) {
+          throw new SocialSpiException('Cannot invalidate the content for a user other than the viewer from a gadget.');
+        }
+        $userIds[] = $key;
+      }
+    }
+    $this->service->invalidateApplicationResources($resources, $request->getToken());
+    $this->service->invalidateUserResources($userIds, $request->getToken());
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/MediaItemHandler.php b/trunk/php/src/apache/shindig/social/service/MediaItemHandler.php
new file mode 100644
index 0000000..3e42685
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/MediaItemHandler.php
@@ -0,0 +1,245 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\social\spi\CollectionOptions;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\sample\BasicRemoteContent;
+use apache\shindig\social\model\MediaItem;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MediaItemHandler extends DataRequestHandler {
+  private static $MEDIA_ITEM_PATH = "/mediaitems/{userId}/{groupId}/{albumId}/{mediaItemId}";
+
+  public function __construct() {
+    parent::__construct('media_item_service');
+  }
+
+  /**
+   * Deletes the media items. The URI structure: /{userId}/{groupId}/{albumId}/{mediaItemId}+
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handleDelete(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MEDIA_ITEM_PATH);
+
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+    $albumIds = $requestItem->getListParameter('albumId');
+    $mediaItemIds = $requestItem->getListParameter('mediaItemId');
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be singular value.");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+    HandlerPreconditions::requireSingular($albumIds, "albumId must be singular value.");
+
+    $this->service->deleteMediaItems($userIds[0], $groupId, $albumIds[0], $mediaItemIds, $requestItem->getToken());
+  }
+
+  /**
+   * Gets the media items. The URI structure: /{userId}/{groupId}/{albumId}/{mediaItemId}+
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handleGet(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MEDIA_ITEM_PATH);
+
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+    $albumIds = $requestItem->getListParameter("albumId");
+    $mediaItemIds = $requestItem->getListParameter("mediaItemId");
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be singular value.");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+    HandlerPreconditions::requireSingular($albumIds, "albumId must be singular value.");
+
+    $options = new CollectionOptions($requestItem);
+    $fields = $requestItem->getFields();
+
+    return $this->service->getMediaItems($userIds[0], $groupId, $albumIds[0], $mediaItemIds, $options, $fields, $requestItem->getToken());
+  }
+
+  /**
+   * Creates the media item. The URI structure: /{userId}/{groupId}/{albumId}.
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handlePost(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MEDIA_ITEM_PATH);
+
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+    $albumIds = $requestItem->getListParameter('albumId');
+    $mediaItem = $requestItem->getParameter('mediaItem');
+    if (! isset($mediaItem)) {
+      // For the content upload REST api. The param is mediaType in the spec now. As there is no mediaType
+      // field in MediaItem. It should be 'type'.
+      $type = $requestItem->getParameter('mediaType');
+      if (! isset($type)) {
+        $type = $requestItem->getParameter('type');
+      }
+      if (in_array($type, MediaItem::$TYPES)) {
+        $mediaItem = array('type' => $type);
+        // Only support title and description for now.
+        $mediaItem['title'] = $requestItem->getParameter('title');
+        $mediaItem['description'] = $requestItem->getParameter('description');
+      }
+    }
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be of singular value");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+    HandlerPreconditions::requireSingular($albumIds, "albumId must be sigular value.");
+    HandlerPreconditions::requireNotEmpty($mediaItem, "mediaItem must be specified.");
+    $mediaItem['albumId'] = $albumIds[0];
+    $file = array();
+    if (isset($mediaItem['url']) && substr($mediaItem['url'], 0, strlen('@field:')) != '@field:') {
+      $file = $this->processRemoteContent($mediaItem['url']);
+    } else {
+      $file = $this->processUploadedContent();
+    }
+
+    $ret = $this->service->createMediaItem($userIds[0], $groupId, $mediaItem, $file, $requestItem->getToken());
+    if (isset($file['tmp_name']) && file_exists($file['tmp_name'])) {
+      @unlink($file['tmp_name']);
+    }
+    return $ret;
+  }
+
+  /**
+   * Fetches the remote media content and saves it as a temporary file. Returns the meta data of the file.
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  private function processRemoteContent($uri) {
+    $request = new RemoteContentRequest($uri);
+    $request->createRemoteContentRequestWithUri($uri);
+    $brc = new BasicRemoteContent();
+    $response = $brc->fetch($request);
+    if ($response->getHttpCode() != 200) {
+      throw new SocialSpiException("Failed to fetch the content from $uri code: " . $response->getHttpCode(), ResponseError::$BAD_REQUEST);
+    }
+    if (!$this->isValidContentType($response->getContentType())) {
+      throw new SocialSpiException("The content type " . $response->getContentType() .
+        " fetched from $uri is not valid.", ResponseError::$BAD_REQUEST);
+    }
+    $content = $response->getResponseContent();
+    return $this->writeBinaryContent($content, $response->getContentType());
+  }
+
+  /**
+   * Checks the $_FILES and HTTP_RAW_POST_DATA variables to write the user uploaded content as a temporary file.
+   * Returns the meta data of the file.
+   *
+   * @return array
+   */
+  private function processUploadedContent() {
+    $file = array();
+    if (! empty($_FILES)) {
+      // The RPC api supports to post the file using the content type 'multipart/form-data'.
+      $uploadedFile = current($_FILES);
+      if ($uploadedFile['error'] != UPLOAD_ERR_OK) {
+        if ($uploadedFile['error'] == UPLOAD_ERR_INI_SIZE || $uploadedFile == UPLOAD_ERR_FORM_SIZE) {
+          throw new SocialSpiException("The uploaded file is too large.", ResponseError::$REQUEST_TOO_LARGE);
+        } else {
+          throw new SocialSpiException("Failed to upload the file.", ResponseError::$BAD_REQUEST);
+        }
+      }
+      if (!$this->isValidContentType($uploadedFile['type'])) {
+        throw new SocialSpiException("The content type of the uploaded file " . $uploadedFile['type'] . " is not valid.", ResponseError::$BAD_REQUEST); 
+      }
+      $tmpName = tempnam('', 'shindig');
+      if (!move_uploaded_file($uploadedFile['tmp_name'], $tmpName)) {
+        throw new SocialSpiException("Failed to move the uploaded file.", ResponseError::$INTERNAL_ERROR);
+      }
+      $file['tmp_name'] = $tmpName;
+      $file['size'] = $uploadedFile['size'];
+      $file['type'] = $uploadedFile['type'];
+      $file['name'] = $uploadedFile['name'];
+    } else if (isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
+      // The REST api supports to post the file using the content type 'image/*', 'video/*' or 'audio/*'.
+      if ($this->isValidContentType($_SERVER['CONTENT_TYPE'])) {
+        $postData = $GLOBALS['HTTP_RAW_POST_DATA'];
+        $file = $this->writeBinaryContent($postData, $_SERVER['CONTENT_TYPE']);
+      }
+    }
+    return $file;
+  }
+  
+  /**
+   * Writes the binary content to a temporary file and returns the meta data of the file.
+   *
+   * @param string $rawData
+   * @param string $contentType
+   * @return array
+   */
+  private function writeBinaryContent(&$rawData, $contentType) {
+    $tmpName = tempnam('', 'shindig');
+    $fp = fopen($tmpName, 'w');
+    if (!fwrite($fp, $rawData)) {
+      throw new SocialSpiException("Failed to write the uploaded file.", ResponseError::$INTERNAL_ERROR);
+    }
+    fclose($fp);
+    return array('tmp_name' => $tmpName, 'size' => filesize($tmpName), 'name' => basename($tmpName), 'type' => $contentType);
+  }
+  
+  /**
+   * Returns true if the given content type is valid.
+   *
+   * @param string $contentType
+   * @return boolean
+   */
+  private function isValidContentType($contentType) {
+    $acceptedMediaPrefixes = array('image', 'video', 'audio');
+    $prefix = substr($contentType, 0, strpos($contentType, '/'));
+    return in_array($prefix, $acceptedMediaPrefixes);
+  }
+
+  /**
+   * Updates the mediaItem. The URI structure: /{userId}/{groupId}/{albumId}/{mediaItemId}
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handlePut(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MEDIA_ITEM_PATH);
+
+    $userIds = $requestItem->getUsers();
+    $groupId = $requestItem->getGroup();
+    $albumIds = $requestItem->getListParameter('albumId');
+    $mediaItemIds = $requestItem->getListParameter('mediaItemId');
+    $mediaItem = $requestItem->getParameter('mediaItem');
+
+    HandlerPreconditions::requireSingular($userIds, "userId must be singular value.");
+    HandlerPreconditions::requireNotEmpty($groupId, "groupId must be specified.");
+    HandlerPreconditions::requireSingular($albumIds, "albumId must be sigular value.");
+    HandlerPreconditions::requireSingular($mediaItemIds, "mediaItemId must be sigular value.");
+    HandlerPreconditions::requireNotEmpty($mediaItem, "mediaItem must be specified.");
+
+    $mediaItem['id'] = $mediaItemIds[0];
+    $mediaItem['albumId'] = $albumIds[0];
+    return $this->service->updateMediaItem($userIds[0], $groupId, $mediaItem, $requestItem->getToken());
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/MessagesHandler.php b/trunk/php/src/apache/shindig/social/service/MessagesHandler.php
new file mode 100644
index 0000000..16907a5
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/MessagesHandler.php
@@ -0,0 +1,173 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\social\spi\CollectionOptions;
+use apache\shindig\social\model\MessageCollection;
+use apache\shindig\social\model\Message;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MessagesHandler extends DataRequestHandler {
+
+  private static $MESSAGES_PATH = "/messages/{userId}/msgCollId/{messageId}";
+
+  public function __construct() {
+    parent::__construct('messages_service');
+  }
+
+  /**
+   * Deletes the message collection or the messages.
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handleDelete(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MESSAGES_PATH);
+
+    $userIds = $requestItem->getUsers();
+    HandlerPreconditions::requireSingular($userIds, "UserId can only be singular.");
+    $msgCollId = $requestItem->getParameter("msgCollId");
+    HandlerPreconditions::requireNotEmpty($msgCollId, "A message collection id is required");
+
+    $token = $requestItem->getToken();
+    $messageIds = $requestItem->getListParameter("messageId");
+    if (empty($messageIds)) {
+      $this->service->deleteMessageCollection($userIds[0], $msgCollId, $token);
+    } else {
+      $this->service->deleteMessages($userIds[0], $msgCollId, $messageIds, $token);
+    }
+  }
+
+  /**
+   * Returns a list of message collections or messages.
+   * Examples:
+   * /messages/john.doe
+   * /messages/john.doe/notification
+   * /messages/john.doe/notification/1,2,3
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handleGet(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MESSAGES_PATH);
+
+    $userIds = $requestItem->getUsers();
+    HandlerPreconditions::requireSingular($userIds, "UserId is not singular.");
+
+    $options = new CollectionOptions($requestItem);
+    $msgCollId = $requestItem->getParameter("msgCollId");
+
+    $token = $requestItem->getToken();
+    if (empty($msgCollId)) {
+      // Gets the message collections.
+      return $this->service->getMessageCollections($userIds[0], $requestItem->getFields(MessageCollection::$DEFAULT_FIELDS), $options, $token);
+    }
+
+    $messageIds = $requestItem->getListParameter("messageId");
+    if (empty($messageIds)) {
+      $messageIds = array();
+    }
+    return $this->service->getMessages($userIds[0], $msgCollId, $requestItem->getFields(Message::$DEFAULT_FIELDS), $messageIds, $options, $token);
+  }
+
+  /**
+   * Creates a new message collection or message.
+   * Exapmples:
+   * /messages/john.doe
+   * /messages/john.doe/notification
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handlePost(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MESSAGES_PATH);
+
+    $userIds = $requestItem->getUsers();
+    HandlerPreconditions::requireSingular($userIds, "UserId is not singular.");
+
+    $msgCollId = $requestItem->getParameter("msgCollId");
+    $entity = $requestItem->getParameter("entity");
+
+    // If the parameters contain 'message' the request is from the old api(< 0.9).
+    if ($requestItem->getParameter("message")) {
+      $entity = $requestItem->getParameter("message");
+      $msgCollId = "@outbox";
+    }
+
+    if (empty($msgCollId)) {
+      // Creates a message collection.
+      $messageCollection = $entity;
+      HandlerPreconditions::requireNotEmpty($messageCollection, "Filed entity not specified.");
+      $title = isset($messageCollection['title']) ? trim($messageCollection['title']) : '';
+      HandlerPreconditions::requireCondition(strlen($title) > 0, "Title must be specified.");
+      return $this->service->createMessageCollection($userIds[0], $messageCollection, $requestItem->getToken());
+    } else {
+      // Creates a message.
+      $messageIds = $requestItem->getListParameter("messageId");
+      HandlerPreconditions::requireEmpty($messageIds, "messageId cannot be specified in create method.");
+      $message = $entity;
+      HandlerPreconditions::requireNotEmpty($message, "Filed entity not specified.");
+      HandlerPreconditions::requireEmpty($messageIds, "messageId cannot be specified in create method.");
+
+      // Message fields validation.
+      $title = isset($message['title']) ? trim($message['title']) : '';
+      HandlerPreconditions::requireCondition(strlen($title) > 0, "Title must be specified.");
+      HandlerPreconditions::requireNotEmpty($message['recipients'], "Field recipients is required.");
+      HandlerPreconditions::requireCondition(is_array($message['recipients']), "recipients must be array.");
+
+      return $this->service->createMessage($userIds[0], $msgCollId, $message, $requestItem->getToken());
+    }
+  }
+
+  /**
+   * Updates a message or a message collection.
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handlePut(RequestItem $requestItem) {
+    $this->checkService();
+    $requestItem->applyUrlTemplate(self::$MESSAGES_PATH);
+
+    $userIds = $requestItem->getUsers();
+    HandlerPreconditions::requireSingular($userIds, "UserId is not singular.");
+
+    $msgCollId = $requestItem->getParameter("msgCollId");
+    HandlerPreconditions::requireNotEmpty($msgCollId, "msgCollId is required.");
+
+    $messageIds = $requestItem->getListParameter("messageId");
+    if (empty($messageIds)) {
+      // Updates message collection.
+      $messageCollection = $requestItem->getParameter("entity");
+      $messageCollection['id'] = $msgCollId;
+      HandlerPreconditions::requireNotEmpty($messageCollection, "Can't parse message collection.");
+      return $this->service->updateMessageCollection($userIds[0], $messageCollection, $requestItem->getToken());
+    } else {
+      // Updates a message.
+      HandlerPreconditions::requireSingular($messageIds, "Message id is not singular.");
+      $message = $requestItem->getParameter("entity");
+      $message['id'] = $messageIds[0];
+      HandlerPreconditions::requireNotEmpty($message, "Can't parse message.");
+      return $this->service->updateMessage($userIds[0], $msgCollId, $message, $requestItem->getToken());
+    }
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/PersonHandler.php b/trunk/php/src/apache/shindig/social/service/PersonHandler.php
new file mode 100644
index 0000000..ef7a369
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/PersonHandler.php
@@ -0,0 +1,169 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\IllegalArgumentException;
+use apache\shindig\social\spi\CollectionOptions;
+use apache\shindig\social\spi\UserId;
+use apache\shindig\social\spi\GroupId;
+use apache\shindig\social\spi\RestfulCollection;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class PersonHandler extends DataRequestHandler {
+
+  protected static $PEOPLE_PATH = "/people/{userId}/{groupId}/{personId}";
+  protected static $DEFAULT_FIELDS = array('id', 'displayName', 'gender', 'thumbnailUrl');
+
+  protected static $ANONYMOUS_ID_TYPE = array('viewer', 'me');
+  protected static $ANONYMOUS_VIEWER = array(
+      'isOwner' => false,
+      'isViewer' => true,
+      'name' => 'anonymous_user',
+      'displayName' => 'Guest'
+  );
+
+  public function __construct() {
+    parent::__construct('person_service');
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @throws SocialSpiException
+   */
+  public function handleDelete(RequestItem $request) {
+    throw new SocialSpiException("You can't delete people.", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @throws SocialSpiException
+   */
+  public function handlePut(RequestItem $request) {
+    throw new SocialSpiException("You can't update right now.", ResponseError::$NOT_IMPLEMENTED);
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @throws SocialSpiException
+   */
+  public function handlePost(RequestItem $request) {
+    throw new SocialSpiException("You can't add people right now.", ResponseError::$NOT_IMPLEMENTED);
+  }
+
+  /**
+   * Allowed end-points /people/{userId}+/{groupId} /people/{userId}/{groupId}/{optionalPersonId}+
+   *
+   * examples: /people/john.doe/@all /people/john.doe/@friends /people/john.doe/@self
+   *
+   * @param RequestItem $request
+   * @return ResponseItem
+   */
+  public function handleGet(RequestItem $request) {
+    $this->checkService();
+    $request->applyUrlTemplate(self::$PEOPLE_PATH);
+
+    $groupId = $request->getGroup();
+    $optionalPersonId = $request->getListParameter("personId");
+    $fields = $request->getFields(self::$DEFAULT_FIELDS);
+    $userIds = $request->getUsers();
+
+    // Preconditions
+    if (count($userIds) < 1) {
+      throw new IllegalArgumentException("No userId specified");
+    } elseif (count($userIds) > 1 && count($optionalPersonId) != 0) {
+      throw new IllegalArgumentException("Cannot fetch personIds for multiple userIds");
+    }
+
+    $options = new CollectionOptions();
+    $options->setSortBy($request->getSortBy());
+    $options->setSortOrder($request->getSortOrder());
+    $options->setFilterBy($request->getFilterBy());
+    $options->setFilterOperation($request->getFilterOperation());
+    $options->setFilterValue($request->getFilterValue());
+    $options->setStartIndex($request->getStartIndex());
+    $options->setCount($request->getCount());
+
+    $token = $request->getToken();
+    $groupType = $groupId->getType();
+    // handle Anonymous Viewer exceptions
+    $containAnonymousUser = false;
+    if ($token->isAnonymous()) {
+      // Find out whether userIds contains
+      // a) @viewer, b) @me, c) SecurityToken::$ANONYMOUS
+      foreach ($userIds as $key=>$id) {
+        if (in_array($id->getType(), self::$ANONYMOUS_ID_TYPE) ||
+            (($id->getType() == 'userId') && ($id->getUserId($token) == SecurityToken::$ANONYMOUS))) {
+          $containAnonymousUser = true;
+          unset($userIds[$key]);
+        }
+      }
+      if ($containAnonymousUser) {
+        $userIds = array_values($userIds);
+        // Skip any requests if groupId is not @self or @all, since anonymous viewer won't have friends.
+        if (($groupType != 'self') && ($groupType != 'all')) {
+          throw new \Exception("Can't get friend from an anonymous viewer.");
+        }
+      }
+    }
+    if ($containAnonymousUser && (count($userIds) == 0)) {
+      return self::$ANONYMOUS_VIEWER;
+    }
+    $service = $this->service;
+    $ret = null;
+    if (count($userIds) == 1) {
+      if (count($optionalPersonId) == 0) {
+        if ($groupType == 'self') {
+          $ret = $service->getPerson($userIds[0], $groupId, $fields, $token);
+        } else {
+          $ret = $service->getPeople($userIds, $groupId, $options, $fields, $token);
+        }
+      } elseif (count($optionalPersonId) == 1) {
+        $ret = $service->getPerson($optionalPersonId[0], $groupId, $fields, $token);
+      } else {
+        $personIds = array();
+        foreach ($optionalPersonId as $pid) {
+          $personIds[] = new UserId('userId', $pid);
+        }
+        // Every other case is a collection response of optional person ids
+        $ret = $service->getPeople($personIds, new GroupId('self', null), $options, $fields, $token);
+      }
+    } else {
+      // Every other case is a collection response.
+      $ret = $service->getPeople($userIds, $groupId, $options, $fields, $token);
+    }
+    // Append anonymous viewer
+    if ($containAnonymousUser) {
+      if (is_array($ret)) {
+        // Single user
+        $people = array($ret, self::$ANONYMOUS_VIEWER);
+        $ret = new RestfulCollection($people, $options->getStartIndex(), 2);
+        $ret->setItemsPerPage($options->getCount());
+      } else {
+        // Multiple users
+        $ret->entry[] = self::$ANONYMOUS_VIEWER;
+        $ret->totalResults += 1;
+      }
+    }
+    return $ret;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/RequestItem.php b/trunk/php/src/apache/shindig/social/service/RequestItem.php
new file mode 100644
index 0000000..e7a8334
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/RequestItem.php
@@ -0,0 +1,265 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\IllegalArgumentException;
+use apache\shindig\social\spi\UserId;
+use apache\shindig\social\spi\GroupId;
+use apache\shindig\social\spi\CollectionOptions;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Abstract base type for social API requests.
+ */
+abstract class RequestItem {
+
+  // Common OpenSocial API fields
+  public static $APP_ID = "appId";
+
+  public static $USER_ID = "userId";
+
+  public static $GROUP_ID = "groupId";
+
+  public static $START_INDEX = "startIndex";
+
+  public static $COUNT = "count";
+
+  public static $SORT_BY = "sortBy";
+  public static $SORT_ORDER = "sortOrder";
+
+  public static $FILTER_BY = "filterBy";
+  public static $FILTER_OPERATION = "filterOp";
+  public static $FILTER_VALUE = "filterValue";
+
+  public static $FIELDS = "fields";
+
+  // Opensocial defaults
+  public static $DEFAULT_START_INDEX = 0;
+
+  public static $DEFAULT_COUNT = 20;
+
+  public static $APP_SUBSTITUTION_TOKEN = "@app";
+
+  /**
+   * @var SecurityToken
+   */
+  protected $token;
+
+  protected $operation;
+
+  protected $service;
+
+  /**
+   *
+   * @param object $service
+   * @param string $operation
+   * @param SecurityToken $token
+   */
+  public function __construct($service, $operation, SecurityToken $token) {
+    $this->service = $service;
+    $this->operation = $operation;
+    $this->token = $token;
+  }
+
+  /**
+   *
+   * @return int
+   */
+  public function getAppId() {
+    $appId = $this->getParameter(self::$APP_ID);
+    if ($appId != null && $appId == self::$APP_SUBSTITUTION_TOKEN) {
+      return $this->token->getAppId();
+    } else {
+      return $appId;
+    }
+  }
+
+  /**
+   *
+   * @return array
+   */
+  public function getUsers() {
+    $ids = $this->getListParameter(self::$USER_ID);
+    if (empty($ids)) {
+      if ($this->token->getViewerId() != null) {
+        // Assume @me
+        $ids = array("@me");
+      } else {
+        throw new IllegalArgumentException("No userId provided and viewer not available");
+      }
+    }
+    $userIds = array();
+    foreach ($ids as $id) {
+      $userIds[] = UserId::fromJson($id);
+    }
+    return $userIds;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getGroup() {
+    return GroupId::fromJson($this->getParameter(self::$GROUP_ID, "@self"));
+  }
+
+  /**
+   *
+   * @return int
+   */
+  public function getStartIndex() {
+    $startIndex = $this->getParameter(self::$START_INDEX);
+    if ($startIndex == null) {
+      return self::$DEFAULT_START_INDEX;
+    } elseif (is_numeric($startIndex)) {
+      return intval($startIndex);
+    } else {
+      throw new SocialSpiException("Parameter " . self::$START_INDEX . " (" . $startIndex . ") is not a number.", ResponseError::$BAD_REQUEST);
+    }
+  }
+
+  /**
+   *
+   * @return int
+   */
+  public function getCount() {
+    $count = $this->getParameter(self::$COUNT);
+    if ($count == null) {
+      return self::$DEFAULT_COUNT;
+    } elseif (is_numeric($count)) {
+      return intval($count);
+    } else {
+      throw new SocialSpiException("Parameter " . self::$COUNT . " (" . $count . ") is not a number.", ResponseError::$BAD_REQUEST);
+    }
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getSortBy() {
+    $sortBy = $this->getParameter(self::$SORT_BY);
+    return $sortBy == null ? CollectionOptions::TOP_FRIENDS_SORT : $sortBy;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getSortOrder() {
+    $sortOrder = $this->getParameter(self::$SORT_ORDER);
+    if (empty($sortOrder)) {
+      return CollectionOptions::SORT_ORDER_ASCENDING;
+    } elseif ($sortOrder == CollectionOptions::SORT_ORDER_ASCENDING || $sortOrder == CollectionOptions::SORT_ORDER_DESCENDING) {
+      return $sortOrder;
+    } else {
+      throw new SocialSpiException("Parameter " . self::$SORT_ORDER . " (" . $sortOrder . ") is not valid.", ResponseError::$BAD_REQUEST);
+    }
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getFilterBy() {
+    return $this->getParameter(self::$FILTER_BY);
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getFilterOperation() {
+    $filterOp = $this->getParameter(self::$FILTER_OPERATION);
+    if (empty($filterOp)) {
+      return CollectionOptions::FILTER_OP_CONTAINS;
+    } elseif ($filterOp == CollectionOptions::FILTER_OP_EQUALS || $filterOp == CollectionOptions::FILTER_OP_CONTAINS || $filterOp == CollectionOptions::FILTER_OP_STARTSWITH || $filterOp == CollectionOptions::FILTER_OP_PRESENT) {
+      return $filterOp;
+    } else {
+      throw new SocialSpiException("Parameter " . self::$FILTER_OPERATION . " (" . $filterOp . ") is not valid.", ResponseError::$BAD_REQUEST);
+    }
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getFilterValue() {
+    $filterValue = $this->getParameter(self::$FILTER_VALUE);
+    return empty($filterValue) ? "" : $filterValue;
+  }
+
+  /**
+   *
+   * @param array $defaultValue
+   * @return array
+   */
+  public function getFields(Array $defaultValue = array()) {
+    $result = array();
+    $fields = $this->getListParameter(self::$FIELDS);
+    if (is_array($fields)) {
+      $result = $fields;
+    }
+    if (! count($result)) {
+      return $defaultValue;
+    } else {
+      // often we get duplicate fields, remove'm
+      $cleanResult = array();
+      foreach ($result as $field) {
+        if (! in_array($field, $cleanResult)) {
+          $cleanResult[urldecode($field)] = urldecode($field);
+        }
+      }
+      $result = $cleanResult;
+    }
+    return $result;
+  }
+
+  /**
+   *
+   * @param string $rpcMethod
+   * @return string
+   */
+  public function getOperation($rpcMethod = null) {
+    return $this->operation;
+  }
+
+  /**
+   *
+   * @param string $rpcMethod
+   * @return object
+   */
+  public function getService($rpcMethod = null) {
+    return $this->service;
+  }
+
+  /**
+   * @return SecurityToken
+   */
+  public function getToken() {
+    return $this->token;
+  }
+
+  public abstract function applyUrlTemplate($urlTemplate);
+
+  public abstract function getParameter($paramName, $defaultValue = null);
+
+  public abstract function getListParameter($paramName);
+}
diff --git a/trunk/php/src/apache/shindig/social/service/ResponseError.php b/trunk/php/src/apache/shindig/social/service/ResponseError.php
new file mode 100644
index 0000000..e594041
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/ResponseError.php
@@ -0,0 +1,123 @@
+<?php
+namespace apache\shindig\social\service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * An Enumeration for holding all the responses emitted by the social API.
+ */
+class ResponseError {
+  /** value representing NOT IMPLEMENTED. */
+  public static $NOT_IMPLEMENTED = 501;
+  /** value representing UNAUTHORIZED. */
+  public static $UNAUTHORIZED = 401;
+  /** value representing FORBIDDEN. */
+  public static $FORBIDDEN = 403;
+  /** value representing BAD REQUEST. */
+  public static $BAD_REQUEST = 400;
+  /** value representing NOT FOUND. */
+  public static $NOT_FOUND = 404;
+  /** value representing content uploading exceeds the quota.*/
+  public static $REQUEST_TOO_LARGE = 413;
+  /** value representing INTERNAL SERVER ERROR. */
+  public static $INTERNAL_ERROR = 500;
+  /** value representing EXPECTATION FAILED. */
+  public static $LIMIT_EXCEEDED = 409;
+
+  /**
+   * The json value of the error.
+   */
+  private $jsonValue;
+  /**
+   * The http error code associated with the error.
+   */
+  private $httpErrorCode;
+
+  /**
+   * The HTTP response header
+   */
+  private $httpErrorMsg;
+
+  /**
+   * Construct a Response Error from the jsonValue as a string and the Http Error Code.
+   * @param string $jsonValue the json String representation of the error code.
+   */
+  public function __construct($jsonValue) {
+    $this->jsonValue = $jsonValue;
+    switch ($this->jsonValue) {
+      case self::$BAD_REQUEST:
+        $this->httpErrorMsg = '400 Bad Request';
+        $this->httpErrorcode = 400;
+        break;
+      case self::$UNAUTHORIZED:
+        $this->httpErrorMsg = '401 Unauthorized';
+        $this->httpErrorcode = 401;
+        break;
+      case self::$FORBIDDEN:
+        $this->httpErrorMsg = '403 Forbidden';
+        $this->httpErrorcode = 403;
+        break;
+      case self::$NOT_FOUND:
+        $this->httpErrorMsg = '404 Not Found';
+        $this->httpErrorcode = 404;
+        break;
+      case self::$NOT_IMPLEMENTED:
+        $this->httpErrorMsg = '501 Not Implemented';
+        $this->httpErrorcode = 501;
+        break;
+      case self::$LIMIT_EXCEEDED:
+        //FIXME or should this be a 507 Insufficient Storage (WebDAV, RFC 4918) ?
+        $this->httpErrorMsg = '509 Limit Exceeeded';
+        $this->httpErrorcode = 509;
+        break;
+      case self::$INTERNAL_ERROR:
+      default:
+        $this->httpErrorMsg = '500 Internal Server Error';
+        $this->httpErrorcode = 500;
+        break;
+    }
+  }
+
+  /**
+   *
+   * Converts the ResponseError to a String representation
+   *
+   * @return string
+   */
+  public function toString() {
+    return $this->jsonValue;
+  }
+
+  /**
+   * Get the HTTP error code.
+   * @return string the Http Error code.
+   */
+  public function getHttpErrorCode() {
+    return $this->httpErrorCode;
+  }
+
+  /**
+   * Get the HTTP error response header.
+   * @return string the Http response header.
+   */
+  public function getHttpErrorMsg() {
+    return $this->httpErrorMsg;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/ResponseItem.php b/trunk/php/src/apache/shindig/social/service/ResponseItem.php
new file mode 100644
index 0000000..1c82167
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/ResponseItem.php
@@ -0,0 +1,125 @@
+<?php
+namespace apache\shindig\social\service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Represents the response items that get handed back as json within the
+ * DataResponse
+ */
+class ResponseItem {
+  public $error;
+  public $errorMessage;
+  public $response;
+
+  /**
+   *
+   * @param string $error
+   * @param string $errorMessage
+   * @param mixed $response
+   */
+  public function __construct($error = null, $errorMessage = null, $response = null) {
+    $this->error = $error;
+    $this->errorMessage = $errorMessage;
+    $this->response = $this->trimResponse($response);
+    if ($this->error === null && $this->errorMessage === null) {
+      // trim null values of self too
+      unset($this->error);
+      unset($this->errorMessage);
+    }
+  }
+
+  /**
+   * the json_encode function does not trim null values,
+   * so we do this manually
+   *
+   * @param mixed $object
+   */
+  private function trimResponse(&$object) {
+    if (is_array($object)) {
+      foreach ($object as $key => $val) {
+        // binary compare, otherwise false == 0 == null too
+        if ($val === null) {
+          unset($object[$key]);
+        } elseif (is_array($val) || is_object($val)) {
+          $object[$key] = $this->trimResponse($val);
+        }
+      }
+    } elseif (is_object($object)) {
+      $vars = get_object_vars($object);
+      foreach ($vars as $key => $val) {
+        if ($val === null) {
+          unset($object->$key);
+        } elseif (is_array($val) || is_object($val)) {
+          $object->$key = $this->trimResponse($val);
+        }
+      }
+    }
+    return $object;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getError() {
+    return isset($this->error) ? $this->error : null;
+  }
+
+  /**
+   *
+   * @param string $error
+   */
+  public function setError($error) {
+    $this->error = $error;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  public function getErrorMessage() {
+    return $this->errorMessage;
+  }
+
+  /**
+   *
+   * @param string $errorMessage
+   */
+  public function setErrorMessage($errorMessage) {
+    $this->errorMessage = $errorMessage;
+  }
+
+  /**
+   *
+   * @return mixed
+   */
+  public function getResponse() {
+    return $this->response;
+  }
+
+  /**
+   *
+   * @param mixed $response
+   */
+  public function setResponse($response) {
+    $this->response = $response;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/RestRequestItem.php b/trunk/php/src/apache/shindig/social/service/RestRequestItem.php
new file mode 100644
index 0000000..5141e9e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/RestRequestItem.php
@@ -0,0 +1,256 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\IllegalArgumentException;
+use apache\shindig\common\Config;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Represents the request items that come from the restful request.
+ */
+class RestRequestItem extends RequestItem {
+  /**
+   *
+   * @var string
+   */
+  private $url;
+  /**
+   *
+   * @var array
+   */
+  private $params;
+  
+  /**
+   * @var string
+   */
+  private $inputConverterMethod;
+  
+  /**
+   * @var OutputConverter
+   */
+  private $outputConverter;
+
+  /**
+   *
+   * @var string
+   */
+  private $postData;
+
+  /**
+   *
+   * @param object $service
+   * @param string $method
+   * @param SecurityToken $token
+   * @param string $inputConverterMethod
+   * @param OutputConverter $outputConverter
+   */
+  public function __construct($service, $method, SecurityToken $token, $inputConverterMethod, $outputConverter) {
+    parent::__construct($service, $method, $token);
+    $this->inputConverterMethod = $inputConverterMethod;
+    $this->outputConverter = $outputConverter;
+  }
+
+  /**
+   * @param $servletRequest
+   * @param SecurityToken $token
+   * @param string $inputConverterMethod
+   * @param OutputConverter $outputConverter
+   * @return RestRequestItem
+   */
+  public static function createWithRequest($servletRequest, $token, $inputConverterMethod, $outputConverter) {
+    $restfulRequestItem = new RestRequestItem(self::getServiceFromPath($servletRequest['url']), self::getMethod(), $token, $inputConverterMethod, $outputConverter);
+    $restfulRequestItem->setUrl($servletRequest['url']);
+    if (isset($servletRequest['params'])) {
+      $restfulRequestItem->setParams($servletRequest['params']);
+    } else {
+      $paramPieces = parse_url($restfulRequestItem->url);
+      if (isset($paramPieces['query'])) {
+        $params = array();
+        parse_str($paramPieces['query'], $params);
+        $restfulRequestItem->setParams($params);
+      }
+    }
+    if (isset($servletRequest['postData'])) {
+      $restfulRequestItem->setPostData($servletRequest['postData']);
+    }
+    return $restfulRequestItem;
+  }
+
+  /**
+   *
+   * @param string $url
+   */
+  public function setUrl($url) {
+    $this->url = $url;
+  }
+
+  /**
+   *
+   * @param array $params
+   */
+  public function setParams($params) {
+    $this->params = $params;
+  }
+
+  /**
+   *
+   * @param string $postData
+   */
+  public function setPostData($postData) {
+    $this->postData = $postData;
+    $service = $this->getServiceFromPath($this->url);
+    $inputConverterConfig = Config::get('service_input_converter');
+    if (isset($inputConverterConfig[$service])) {
+      $class = $inputConverterConfig[$service]['class'];
+      $inputConverter = new $class();
+      $data   = $inputConverter->{$this->inputConverterMethod}($this->postData);
+      $targetField = $inputConverterConfig[$service]['targetField'];
+      if ($targetField !== false && isset($data)) {
+        if ($targetField !== null) {
+          $this->params[$targetField] = $data;
+        } else {
+          $this->params = $data;
+        }
+        }
+    } else {
+        throw new \Exception("Invalid or unknown service endpoint: $service");
+    }
+  
+  }
+  
+  /**
+   * '/people/@me/@self' => 'people'
+   * '/invalidate?invalidationKey=1' => 'invalidate'
+   *
+   * @param string $pathInfo
+   * @return $pathInfo
+   */
+  static function getServiceFromPath($pathInfo) {
+    $pathInfo = substr($pathInfo, 1);
+    $indexOfNextPathSeparator = strpos($pathInfo, '/');
+    $indexOfNextQuestionMark = strpos($pathInfo, '?');
+    if ($indexOfNextPathSeparator !== false && $indexOfNextQuestionMark !== false) {
+      return substr($pathInfo, 0, min($indexOfNextPathSeparator, $indexOfNextQuestionMark));
+    }
+    if ($indexOfNextPathSeparator !== false) {
+      return substr($pathInfo, 0, $indexOfNextPathSeparator);
+    }
+    if ($indexOfNextQuestionMark !== false) {
+      return substr($pathInfo, 0, $indexOfNextQuestionMark);
+    }
+    return $pathInfo;
+  }
+
+  /**
+   *
+   * @return string
+   */
+  static function getMethod() {
+    if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
+      return $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
+    } else {
+      return $_SERVER['REQUEST_METHOD'];
+    }
+  }
+
+  /**
+   * This could definitely be cleaner..
+   * TODO: Come up with a cleaner way to handle all of this code.
+   *
+   * @param urlTemplate The template the url follows
+   */
+  public function applyUrlTemplate($urlTemplate) {
+    $paramPieces = @parse_url($this->url);
+    $actualUrl = explode("/", $paramPieces['path']);
+    $expectedUrl = explode("/", $urlTemplate);
+    for ($i = 1; $i < count($actualUrl); $i ++) {
+      $actualPart = isset($actualUrl[$i]) ? $actualUrl[$i] : null;
+      $expectedPart = isset($expectedUrl[$i]) ? $expectedUrl[$i] : null;
+      if (strpos($expectedPart, "{") !== false) {
+        $this->params[preg_replace('/\{|\}/', '', $expectedPart)] = explode(',', $actualPart);
+      } elseif (strpos($actualPart, ',') !== false) {
+        throw new IllegalArgumentException("Cannot expect plural value " + $actualPart + " for singular field " + $expectedPart + " in " + $this->url);
+      } else {
+        $this->params[$expectedPart] = $actualPart;
+      }
+    }
+  }
+
+  /**
+   *
+   * @return aray
+   */
+  public function getParameters() {
+    return $this->params;
+  }
+
+  /**
+   *
+   * @param string $paramName
+   * @param string $paramValue
+   */
+  public function setParameter($paramName, $paramValue) {
+    // Ignore nulls
+    if ($paramValue == null) {
+      return;
+    }
+    $this->params[$paramName] = $paramValue;
+  }
+
+  /**
+   * Return a single param value
+   *
+   * @param string $paramName
+   * @param string $defaultValue
+   * @return string
+   */
+  public function getParameter($paramName, $defaultValue = null) {
+    $paramValue = isset($this->params[$paramName]) ? $this->params[$paramName] : null;
+    if ($paramValue != null && ! empty($paramValue)) {
+      return $paramValue;
+    }
+    return $defaultValue;
+  }
+
+  /**
+   * Return a list param value
+   *
+   * @param string $paramName
+   * @return array
+   */
+  public function getListParameter($paramName) {
+    $stringList = isset($this->params[$paramName]) ? $this->params[$paramName] : null;
+    if ($stringList == null) {
+      return array();
+    } elseif (is_array($stringList)) {
+      // already converted to array, return straight away
+      return $stringList;
+    }
+    if (strpos($stringList, ',') !== false) {
+      $stringList = explode(',', $stringList);
+    } else {
+      // Allow up-conversion of non-array to array params.
+      $stringList = array($stringList);
+    }
+    $this->params[$paramName] = $stringList;
+    return $stringList;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/RpcRequestItem.php b/trunk/php/src/apache/shindig/social/service/RpcRequestItem.php
new file mode 100644
index 0000000..a19eafa
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/RpcRequestItem.php
@@ -0,0 +1,136 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * A JSON-RPC specific implementation of RequestItem
+ */
+class RpcRequestItem extends RequestItem {
+
+  private $data;
+
+  /**
+   *
+   * @param array $rpc
+   * @param SecurityToken $token
+   */
+  public function __construct($rpc, SecurityToken $token) {
+    if (empty($rpc['method'])) {
+      throw new SocialSpiException("Missing method in RPC call");
+    }
+    parent::__construct($rpc['method'], $rpc['method'], $token);
+    if (isset($rpc['params'])) {
+      $this->data = $rpc['params'];
+    } else {
+      $this->data = array();
+    }
+  }
+
+  /**
+   *
+   * @param string $rpcMethod
+   * @return string
+   */
+  public function getService($rpcMethod = null) {
+    $service = null;
+    if ($rpcMethod != null) {
+      $service = substr($rpcMethod, 0, strpos($rpcMethod, '.'));
+    } else {
+      $service = substr($this->service, 0, strpos($this->service, '.'));
+    }
+    // Accepts both 'mediaItems' and 'mediaitems'.
+    if ($service == 'mediaItems') {
+        $service = 'mediaitems';
+    }
+    return $service;
+  }
+
+  /**
+   *
+   * @param string $rpcMethod
+   * @return string
+   */
+  public function getOperation($rpcMethod = null) {
+    if ($rpcMethod != null) {
+      $op = substr($rpcMethod, strpos($rpcMethod, '.') + 1);
+    } else {
+      $op = substr($this->operation, strpos($this->operation, '.') + 1);
+    }
+    return $op;
+  }
+
+  /**
+   *
+   * @param string $rpcMethod
+   * @return string
+   */
+  public function getMethod($rpcMethod = null) {
+    return $this->getOperation($rpcMethod);
+  }
+
+  /**
+   *
+   * @return array
+   */
+  public function getParameters() {
+    return $this->data;
+  }
+
+  /**
+   *
+   * @param string $paramName
+   * @param string $defaultValue
+   * @return string
+   */
+  public function getParameter($paramName, $defaultValue = null) {
+    if (isset($this->data[$paramName])) {
+      return $this->data[$paramName];
+    } else {
+      return $defaultValue;
+    }
+  }
+
+  /**
+   *
+   * @param string $paramName
+   * @return array
+   */
+  public function getListParameter($paramName) {
+    if (isset($this->data[$paramName])) {
+      if (is_array($this->data[$paramName])) {
+        return $this->data[$paramName];
+      } else {
+        // Allow up-conversion of non-array to array params.
+        return array($this->data[$paramName]);
+      }
+    } else {
+      return array();
+    }
+  }
+
+  /**
+   *
+   * @param string $urlTemplate
+   */
+  public function applyUrlTemplate($urlTemplate) {  // No params in the URL
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/service/SocialSpiException.php b/trunk/php/src/apache/shindig/social/service/SocialSpiException.php
new file mode 100644
index 0000000..a601182
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/SocialSpiException.php
@@ -0,0 +1,24 @@
+<?php
+namespace apache\shindig\social\service;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class SocialSpiException extends \Exception {
+}
diff --git a/trunk/php/src/apache/shindig/social/service/SystemHandler.php b/trunk/php/src/apache/shindig/social/service/SystemHandler.php
new file mode 100644
index 0000000..5355c2d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/service/SystemHandler.php
@@ -0,0 +1,113 @@
+<?php
+namespace apache\shindig\social\service;
+use apache\shindig\common\Config;
+
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class SystemHandler extends DataRequestHandler {
+
+  public function __construct() {  // do nothing, listMethods doesn't have a service implementation since it depends on the container.js configuration
+  }
+
+  /**
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  public function handleItem(RequestItem $requestItem) {
+    try {
+      $method = strtolower($requestItem->getMethod());
+      $method = 'handle' . ucfirst($method);
+      $response = $this->$method($requestItem);
+    } catch (SocialSpiException $e) {
+      $response = new ResponseItem($e->getCode(), $e->getMessage());
+    } catch (\Exception $e) {
+      $response = new ResponseItem(ResponseError::$INTERNAL_ERROR, "Internal error: " . $e->getMessage());
+    }
+    return $response;
+  }
+
+  /**
+   *
+   * @param RequestItem $request
+   */
+  public function handleDelete(RequestItem $request) {
+    throw new SocialSpiException("Http delete not allowed for invalidation service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   *
+   * @param RequestItem $request
+   */
+  public function handlePut(RequestItem $request) {
+    throw new SocialSpiException("Http put not allowed for invalidation service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   *
+   * @param RequestItem $request
+   */
+  public function handlePost(RequestItem $request) {
+    throw new SocialSpiException("Http put not allowed for invalidation service", ResponseError::$BAD_REQUEST);
+  }
+
+  /**
+   *
+   * @param RequestItem $request
+   * @return array
+   */
+  public function handleGet(RequestItem $request) {
+    return $this->handleListMethods($request);
+  }
+
+  /**
+   *
+   * @param RequestItem $request
+   * @return array
+   */
+  public function handleListMethods(RequestItem $request) {
+    $containerConfigClass = Config::get('container_config_class');
+    $containerConfig = new $containerConfigClass(Config::get('container_path'));
+    $gadgetConfig = $containerConfig->getConfig('default', 'gadgets.features');
+    if (! isset($gadgetConfig['osapi.services']) || count($gadgetConfig['osapi.services']) == 1) {
+      // this should really be set in config/container.js, but if not, we build a complete default set so at least most of it works out-of-the-box
+      $gadgetConfig['osapi.services'] = array(
+          'gadgets.rpc' => array('container.listMethods'),
+          'http://%host%/social/rpc' => array("messages.update", "albums.update",
+              "activities.delete", "activities.update",
+              "activities.supportedFields", "albums.get",
+              "activities.get", "mediaitems.update",
+              "messages.get", "appdata.get",
+              "system.listMethods", "people.supportedFields",
+              "messages.create", "mediaitems.delete",
+              "mediaitems.create", "people.get", "people.create",
+              "albums.delete", "messages.delete",
+              "appdata.update", "activities.create",
+              "mediaitems.get", "albums.create",
+              "appdata.delete", "people.update",
+              "appdata.create"),
+          'http://%host%/gadgets/api/rpc' => array('cache.invalidate',
+              'http.head', 'http.get', 'http.put',
+              'http.post', 'http.delete'));
+    }
+    return $gadgetConfig['osapi.services'];
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/servlet/ApiServlet.php b/trunk/php/src/apache/shindig/social/servlet/ApiServlet.php
new file mode 100644
index 0000000..0cbef0f
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/servlet/ApiServlet.php
@@ -0,0 +1,178 @@
+<?php
+namespace apache\shindig\social\servlet;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\social\service\SocialSpiException;
+use apache\shindig\common\HttpServlet;
+use apache\shindig\common\Config;
+use apache\shindig\common\AuthenticationMode;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\social\service\ResponseError;
+use apache\shindig\social\service\RequestItem;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Common base class for API servlets.
+ */
+abstract class ApiServlet extends HttpServlet {
+  public $handlers = array();
+
+  protected static $DEFAULT_ENCODING = "UTF-8";
+
+  public function __construct() {
+    parent::__construct();
+    $this->setNoCache(true);
+    if (isset($_SERVER['CONTENT_TYPE']) && (strtolower($_SERVER['CONTENT_TYPE']) != $_SERVER['CONTENT_TYPE'])) {
+      // make sure the content type is in all lower case since that's what we'll check for in the handlers
+      $_SERVER['CONTENT_TYPE'] = strtolower($_SERVER['CONTENT_TYPE']);
+    }
+    $acceptedContentTypes = array('application/atom+xml', 'application/xml', 'application/json', 'application/json-rpc', 'application/jsonrequest', 'application/javascript');
+    if (isset($_SERVER['CONTENT_TYPE'])) {
+      // normalize things like "application/json; charset=utf-8" to application/json
+      foreach ($acceptedContentTypes as $contentType) {
+        if (strpos($_SERVER['CONTENT_TYPE'], $contentType) !== false) {
+          $_SERVER['CONTENT_TYPE'] = $contentType;
+          $this->setContentType($contentType);
+          break;
+        }
+      }
+    }
+    if (isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
+      if (! isset($_SERVER['CONTENT_TYPE']) || ! in_array($_SERVER['CONTENT_TYPE'], $acceptedContentTypes)) {
+        $prefix = substr($_SERVER['CONTENT_TYPE'], 0, strpos($_SERVER['CONTENT_TYPE'], '/'));
+        $acceptedMediaPrefixes = array('image', 'video', 'audio');
+        if (! in_array($prefix, $acceptedMediaPrefixes)) {
+          throw new \Exception("When posting to the social end-point you have to specify a content type,
+              supported content types are: 'application/json', 'application/xml' and 'application/atom+xml'.
+              For content upload, content type can be 'image/*', 'audio/*' and 'video/*'");
+        }
+      }
+    }
+  }
+
+  /**
+   *
+   * @return SecurityToken
+   */
+  public function getSecurityToken() {
+    // Support a configurable host name ('http_host' key) so that OAuth signatures don't fail in reverse-proxy type situations
+    $scheme = (! isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https';
+    $http_url = $scheme . '://' . (Config::get('http_host') ? Config::get('http_host') : $_SERVER['HTTP_HOST']) . $_SERVER['REQUEST_URI'];
+    // see if we have an OAuth request
+    $request = \OAuthRequest::from_request(null, $http_url, null);
+    $appUrl = $request->get_parameter('oauth_consumer_key');
+    $userId = $request->get_parameter('xoauth_requestor_id'); // from Consumer Request extension (2-legged OAuth)
+    $signature = $request->get_parameter('oauth_signature');
+    if ($appUrl && $signature) {
+      //if ($appUrl && $signature && $userId) {
+      // look up the user and perms for this oauth request
+      $oauthLookupService = Config::get('oauth_lookup_service');
+      $oauthLookupService = new $oauthLookupService();
+      $token = $oauthLookupService->getSecurityToken($request, $appUrl, $userId, $this->getContentType());
+      if ($token) {
+        $token->setAuthenticationMode(AuthenticationMode::$OAUTH_CONSUMER_REQUEST);
+        return $token;
+      } else {
+        return null; // invalid oauth request, or 3rd party doesn't have access to this user
+      }
+    } // else, not a valid oauth request, so don't bother
+
+
+    // look for encrypted security token
+    $token = BasicSecurityToken::getTokenStringFromRequest();
+    if (empty($token)) {
+      if (Config::get('allow_anonymous_token')) {
+        // no security token, continue anonymously, remeber to check
+        // for private profiles etc in your code so their not publicly
+        // accessable to anoymous users! Anonymous == owner = viewer = appId = modId = 0
+        // create token with 0 values, no gadget url, no domain and 0 duration
+        $gadgetSigner = Config::get('security_token');
+        return new $gadgetSigner(null, 0, SecurityToken::$ANONYMOUS, SecurityToken::$ANONYMOUS, 0, '', '', 0, Config::get('container_id'));
+      } else {
+        return null;
+      }
+    }
+    $gadgetSigner = Config::get('security_token_signer');
+    $gadgetSigner = new $gadgetSigner();
+    return $gadgetSigner->createToken($token);
+  }
+
+  /**
+   * @param ResponseItem $responseItem
+   */
+  protected abstract function sendError(ResponseItem $responseItem);
+
+  protected function sendSecurityError() {
+    $this->sendError(new ResponseItem(ResponseError::$UNAUTHORIZED, "The request did not have a proper security token nor oauth message and unauthenticated requests are not allowed"));
+  }
+
+  /**
+   * Delivers a request item to the appropriate DataRequestHandler.
+   *
+   * @param RequestItem $requestItem
+   * @return ResponseItem
+   */
+  protected function handleRequestItem(RequestItem $requestItem) {
+    // lazy initialization of the service handlers, no need to instance them all for each request
+ 
+    $service = $requestItem->getService();
+ 
+    if (! isset($this->handlers[$service])) {
+ 
+      $handlerClasses = Config::get('service_handler');
+ 
+      if (isset($handlerClasses[$service])) {
+          $handlerClass = $handlerClasses[$service];
+          $this->handlers[$service] = new $handlerClass();
+      } else {
+          throw new SocialSpiException("The service " . $service . " is not implemented", ResponseError::$NOT_IMPLEMENTED);
+      }
+ 
+    }
+    $handler = $this->handlers[$service];
+    return $handler->handleItem($requestItem);
+  }
+
+  /**
+   *
+   * @param mixed $result
+   * @return ResponseItem 
+   */
+  protected function getResponseItem($result) {
+    if ($result instanceof ResponseItem) {
+      return $result;
+    } else {
+      return new ResponseItem(null, null, $result);
+    }
+  }
+
+  /**
+   *
+   * @param Exception $e
+   * @return ResponseItem
+   */
+  protected function responseItemFromException($e) {
+    if ($e instanceof SocialSpiException) {
+      return new ResponseItem($e->getCode(), $e->getMessage(), null);
+    }
+    return new ResponseItem(ResponseError::$INTERNAL_ERROR, $e->getMessage());
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/servlet/CompatibilityJsonRpcServlet.php b/trunk/php/src/apache/shindig/social/servlet/CompatibilityJsonRpcServlet.php
new file mode 100644
index 0000000..57a0a51
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/servlet/CompatibilityJsonRpcServlet.php
@@ -0,0 +1,28 @@
+<?php
+namespace apache\shindig\social\servlet;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * JSON-RPC handler servlet. for older clients going to /social/rpc endpoint
+ */
+class CompatibilityJsonRpcServlet extends JsonRpcServlet {
+    protected $resultKey = 'data';
+}
diff --git a/trunk/php/src/apache/shindig/social/servlet/DataServiceServlet.php b/trunk/php/src/apache/shindig/social/servlet/DataServiceServlet.php
new file mode 100644
index 0000000..a4ef4e1
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/servlet/DataServiceServlet.php
@@ -0,0 +1,253 @@
+<?php
+namespace apache\shindig\social\servlet;
+use apache\shindig\social\converters\OutputXmlConverter;
+use apache\shindig\social\converters\OutputAtomConverter;
+use apache\shindig\social\converters\OutputJsonConverter;
+use apache\shindig\common\Config;
+use apache\shindig\social\service\ResponseError;
+use apache\shindig\social\service\RestRequestItem;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\social\spi\DataCollection;
+use apache\shindig\social\spi\RestfulCollection;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class DataServiceServlet extends ApiServlet {
+
+  protected static $FORMAT_PARAM = "format";
+  protected static $ATOM_FORMAT = "atom";
+  protected static $XML_FORMAT = "xml";
+
+  public function doGet() {
+    $this->doPost();
+  }
+
+  public function doPut() {
+    $this->doPost();
+  }
+
+  public function doDelete() {
+    $this->doPost();
+  }
+
+  public function doPost() {
+    $xrdsLocation = Config::get('xrds_location');
+    if ($xrdsLocation) {
+      header("X-XRDS-Location: $xrdsLocation", false);
+    }
+    try {
+      $token = $this->getSecurityToken();
+      if ($token == null) {
+        $this->sendSecurityError();
+        return;
+      }
+      $inputConverterMethod = $this->getInputConverterMethodForRequest();
+      $outputConverter = $this->getOutputConverterForRequest();
+      $this->handleSingleRequest($token, $inputConverterMethod, $outputConverter);
+    } catch (\Exception $e) {
+      $code = '500 Internal Server Error';
+      header("HTTP/1.0 $code", true);
+      echo "<h1>$code - Internal Server Error</h1>\n" . $e->getMessage();
+      if (Config::get('debug')) {
+        echo "\n\n<br>\nDebug backtrace:\n<br>\n<pre>\n";
+        echo $e->getTraceAsString();
+        echo "\n</pre>\n";
+      }
+    }
+  }
+
+  /**
+   *
+   * @param ResponseItem $responseItem 
+   */
+  public function sendError(ResponseItem $responseItem) {
+    $unauthorized = false;
+    $errorMessage = $responseItem->getErrorMessage();
+    $errorCode = $responseItem->getError();
+    switch ($errorCode) {
+      case ResponseError::$BAD_REQUEST:
+        $code = '400 Bad Request';
+        break;
+      case ResponseError::$UNAUTHORIZED:
+        $code = '401 Unauthorized';
+        $unauthorized = true;
+        break;
+      case ResponseError::$FORBIDDEN:
+        $code = '403 Forbidden';
+        break;
+      case ResponseError::$NOT_FOUND:
+        $code = '404 Not Found';
+        break;
+      case ResponseError::$NOT_IMPLEMENTED:
+        $code = '501 Not Implemented';
+        break;
+      case ResponseError::$INTERNAL_ERROR:
+      default:
+        $code = '500 Internal Server Error';
+        break;
+    }
+    @header("HTTP/1.0 $code", true);
+    if ($unauthorized) {
+      header("WWW-Authenticate: OAuth realm", true);
+    }
+    echo "$code - $errorMessage";
+    die();
+  }
+
+  /**
+   * Handler for non-batch requests (REST only has non-batch requests)
+   *
+   * @param SecurityToken $token
+   * @param string $inputConverterMethod
+   * @param OutputConverter $outputConverter
+   */
+  private function handleSingleRequest(SecurityToken $token, $inputConverterMethod, $outputConverter) {
+    //uri example: /social/rest/people/@self   /gadgets/api/rest/cache/invalidate
+    $servletRequest = array(
+        'url' => substr($_SERVER["REQUEST_URI"], strpos($_SERVER["REQUEST_URI"], '/rest') + 5));
+    // Php version 5.2.9(linux) doesn't set HTTP_RAW_POST_DATA properly.
+    if (!isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
+      $tmp = file_get_contents('php://input');
+      if (!empty($tmp)) {
+        $GLOBALS['HTTP_RAW_POST_DATA'] = $tmp;
+      }
+    }
+    if (isset($GLOBALS['HTTP_RAW_POST_DATA'])) {
+      $servletRequest['postData'] = $GLOBALS['HTTP_RAW_POST_DATA'];
+      if (get_magic_quotes_gpc()) {
+        $servletRequest['postData'] = stripslashes($servletRequest['postData']);
+      }
+    }
+    $servletRequest['params'] = array_merge($_GET, $_POST);
+    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, $inputConverterMethod, $outputConverter);
+    $responseItem = $this->getResponseItem($this->handleRequestItem($requestItem));
+    if ($responseItem->getError() == null) {
+      $response = $responseItem->getResponse();
+      if (! ($response instanceof DataCollection) && ! ($response instanceof RestfulCollection) && count($response)) {
+        $response = array("entry" => $response);
+        $responseItem->setResponse($response);
+      }
+      $outputConverter->outputResponse($responseItem, $requestItem);
+    } else {
+      $this->sendError($responseItem);
+    }
+  }
+
+  /**
+   * Returns the output converter to use
+   *
+   * @return OutputConverter
+   */
+  private function getOutputConverterForRequest() {
+    $outputFormat = strtolower(trim(! empty($_POST[self::$FORMAT_PARAM]) ? $_POST[self::$FORMAT_PARAM] : (! empty($_GET[self::$FORMAT_PARAM]) ? $_GET[self::$FORMAT_PARAM] : 'json')));
+    switch ($outputFormat) {
+      case 'xml':
+        if (!Config::get('debug')) $this->setContentType('application/xml');
+        return new OutputXmlConverter();
+      case 'atom':
+        if (!Config::get('debug')) $this->setContentType('application/atom+xml');
+        return new OutputAtomConverter();
+      case 'json':
+        if (!Config::get('debug')) $this->setContentType('application/json');
+        return new OutputJsonConverter();
+      default:
+        // if no output format is set, see if we can match an input format header
+        // if not, default to json
+        if (isset($_SERVER['CONTENT_TYPE'])) {
+          switch ($_SERVER['CONTENT_TYPE']) {
+            case 'application/atom+xml':
+              if (!Config::get('debug')) $this->setContentType('application/atom+xml');
+              return new OutputAtomConverter();
+            case 'application/xml':
+              if (!Config::get('debug')) $this->setContentType('application/xml');
+              return new OutputXmlConverter();
+            default:
+            case 'application/json':
+              if (!Config::get('debug')) $this->setContentType('application/json');
+              return new OutputJsonConverter();
+          }
+        }
+        break;
+    }
+    // just to satisfy the code scanner, code is actually unreachable
+    return null;
+  }
+
+  /**
+   * Returns the input converter method to use
+   *
+   * @return string
+   */
+  private function getInputConverterMethodForRequest() {
+    $inputFormat = $this->getInputRequestFormat();
+    switch ($inputFormat) {
+      case 'xml':
+        return 'convertXml';
+      case 'atom':
+        return 'convertAtom';
+      case 'json':
+        return 'convertJson';
+      default:
+        throw new \Exception("Unknown format param: $inputFormat");
+    }
+  }
+
+  /**
+   * Tries to guess the input format based on the Content-Type
+   * header, of if none is set, the format query param
+   *
+   * @return string request format to use
+   */
+  private function getInputRequestFormat() {
+    // input format is defined by the Content-Type header
+    // if that isn't set we use the &format= param
+    // if that isn't set, we default to json
+    if (isset($_SERVER['CONTENT_TYPE'])) {
+      switch ($_SERVER['CONTENT_TYPE']) {
+        case 'application/atom+xml':
+          return 'atom';
+        case 'application/xml':
+          return 'xml';
+        case 'application/json':
+        default:
+          return 'json';
+      }
+    } else {
+      // if no Content-Type header is set, we assume the input format will be the same as the &format=<foo> param
+      // if that isn't set either, we assume json
+      return strtolower(trim(! empty($_POST[self::$FORMAT_PARAM]) ? $_POST[self::$FORMAT_PARAM] : (! empty($_GET[self::$FORMAT_PARAM]) ? $_GET[self::$FORMAT_PARAM] : 'json')));
+    }
+    // just to satisfy the code scanner, code is actually unreachable
+    return null;
+  }
+
+  /**
+   * Returns the route to use (activities, people, appdata, messages)
+   *
+   * @param string $pathInfo
+   * @return string the route name
+   */
+  private function getRouteFromParameter($pathInfo) {
+    $pathInfo = substr($pathInfo, 1);
+    $indexOfNextPathSeparator = strpos($pathInfo, "/");
+    return $indexOfNextPathSeparator !== false ? substr($pathInfo, 0, $indexOfNextPathSeparator) : $pathInfo;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/servlet/JsonRpcServlet.php b/trunk/php/src/apache/shindig/social/servlet/JsonRpcServlet.php
new file mode 100644
index 0000000..8435460
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/servlet/JsonRpcServlet.php
@@ -0,0 +1,269 @@
+<?php
+namespace apache\shindig\social\servlet;
+use apache\shindig\social\service\RpcRequestItem;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\social\spi\RestfulCollection;
+use apache\shindig\social\spi\DataCollection;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * JSON-RPC handler servlet.
+ */
+class JsonRpcServlet extends ApiServlet {
+
+  protected $resultKey = 'result';
+
+  /**
+   * Single request through GET
+   * http://api.example.org/rpc?method=people.get&id=myself&params.userId=@me&params.groupId=@self
+   */
+  public function doGet() {
+    $token = $this->getSecurityToken();
+    if ($token == null) {
+      $this->sendSecurityError();
+      return;
+    }
+
+    $request = $this->parseGetRequest($_SERVER['QUERY_STRING']);
+
+    $this->dispatch($request, $token);
+  }
+
+  /**
+   * parses all $_GET parameters according to rpc spec
+   * @see http://opensocial-resources.googlecode.com/svn/spec/1.1/Core-API-Server.xml#urlAddressing
+   *
+   * @param string $parameters should be $_GET on production
+   * @return array
+   */
+  public function parseGetRequest($parameterString)
+  {
+    // we have to parse the query parameters by hand because parse_str or the built in
+    // $_GET replace '.' with '_' in parameter keys
+    $parameters = array();
+    $pairs = explode('&', $parameterString);
+    foreach ($pairs as $pair) {
+      if (strpos($pair, '=') !== false) {
+        list($key, $value) = explode('=', $pair);
+        $parameters[$key] = urldecode($value);
+      }
+    }
+    $request = array();
+    foreach($parameters as $key => $value) {
+      // parse value lists like field=1,2,3,4,5
+      if (strpos($value, ',') !== false) {
+        $parsedValue = explode(',', $value);
+      } else {
+        $parsedValue = $value;
+      }
+      // handle multidimensional nested keys like field.nested=value
+      if (strpos($key, '.') > 0) {
+        $keyParts = explode('.', $key);
+        $request = $this->getMultiDimensionalArray($request, $keyParts, $parsedValue);
+      } else {
+        $request = $this->getMultiDimensionalArray($request, array($key), $parsedValue);
+      }
+    }
+    return $request;
+  }
+
+  /**
+   * parses a multidimensional parameter
+   * e.g. params.foo=bar to 'params' => array('foo' => 'bar')
+   *
+   * @param array $request
+   * @param array $keyParts
+   * @param mixed $value
+   * @return array
+   */
+  private function getMultiDimensionalArray($request, $keyParts, $value)
+  {
+    if (! $keyParts) {
+      return $value;
+    }
+    $key = array_shift($keyParts);
+
+    $matches = array();
+
+    // handle something like field(0).nested1=value1&field(1).nested2=value2
+    if (preg_match('/^([a-zA-Z0-9]*)\(([0-9]*)\)$/', $key, $matches)) {
+      $key = $matches[1];
+      array_unshift($keyParts, $matches[2]);
+    }
+
+    if (! isset($request[$key])) {
+      $request[$key] = array();
+    }
+    $value = $this->getMultiDimensionalArray($request[$key], $keyParts, $value);
+
+    if (is_array($value)) {
+      $request[$key] = $value + $request[$key];
+    } else {
+      $request[$key] = $value;
+    }
+
+    return $request;
+  }
+
+  /**
+   * RPC Post request
+   */
+  public function doPost() {
+    $token = $this->getSecurityToken();
+    if ($token == null || $token == false) {
+      $this->sendSecurityError();
+      return;
+    }
+    if (isset($GLOBALS['HTTP_RAW_POST_DATA']) || isset($_POST['request'])) {
+      $requestParam = isset($GLOBALS['HTTP_RAW_POST_DATA']) ? $GLOBALS['HTTP_RAW_POST_DATA'] : (get_magic_quotes_gpc() ? stripslashes($_POST['request']) : $_POST['request']);
+      $request = json_decode($requestParam, true);
+      if ($request == $requestParam) {
+        throw new \InvalidArgumentException("Malformed json string");
+      }
+    } else {
+      throw new \InvalidArgumentException("Missing POST data");
+    }
+    if ((strpos($requestParam, '[') !== false) && strpos($requestParam, '[') < strpos($requestParam, '{')) {
+      // Is a batch
+      $this->dispatchBatch($request, $token);
+    } else {
+      $this->dispatch($request, $token);
+    }
+  }
+
+  /**
+   *
+   * @param array $batch
+   * @param SecurityToken $token
+   */
+  public function dispatchBatch($batch, $token) {
+    $responses = array();
+    // Gather all Futures.  We do this up front so that
+    // the first call to get() comes after all futures are created,
+    // which allows for implementations that batch multiple Futures
+    // into single requests.
+    for ($i = 0; $i < count($batch); $i ++) {
+      $batchObj = $batch[$i];
+      $requestItem = new RpcRequestItem($batchObj, $token);
+      $responses[$i] = $this->handleRequestItem($requestItem);
+    }
+    // Resolve each Future into a response.
+    // TODO: should use shared deadline across each request
+    $result = array();
+    for ($i = 0; $i < count($batch); $i ++) {
+      $batchObj = $batch[$i];
+      $key = isset($batchObj["id"]) ? $batchObj["id"] : null;
+      $responseItem = $this->getJSONResponse($key, $this->getResponseItem($responses[$i]));
+      $result[] = $responseItem;
+    }
+    $this->encodeAndSendResponse($result);
+  }
+
+  /**
+   *
+   * @param array $request
+   * @param SecurityToken $token
+   */
+  public function dispatch($request, $token) {
+    $key = null;
+    if (isset($request["id"])) {
+      $key = $request["id"];
+    }
+    $requestItem = new RpcRequestItem($request, $token);
+    // Resolve each Future into a response.
+    // TODO: should use shared deadline across each request
+    $a = $this->handleRequestItem($requestItem);
+    $response = $this->getResponseItem($a);
+    $result = $this->getJSONResponse($key, $response);
+    $this->encodeAndSendResponse($result);
+  }
+
+  /**
+   *
+   * @param string $key
+   * @param ResponseItem $responseItem
+   * @return array
+   */
+  private function getJSONResponse($key, ResponseItem $responseItem) {
+    $result = array();
+    if ($key != null) {
+      $result["id"] = $key;
+    }
+    if ($responseItem->getError() != null) {
+      $result["error"] = $this->getErrorJson($responseItem);
+    } else {
+      $response = $responseItem->getResponse();
+      $converted = $response;
+      if ($response instanceof RestfulCollection) {
+        // FIXME this is a little hacky because of the field names in the RestfulCollection
+        $converted->list = $converted->entry;
+        unset($converted->entry);
+        $result[$this->resultKey] = $converted;
+      } elseif ($response instanceof DataCollection) {
+        $result[$this->resultKey] = $converted->getEntry();
+      } else {
+        $result[$this->resultKey] = $converted;
+      }
+    }
+    return $result;
+  }
+
+  // TODO(doll): Refactor the responseItem so that the fields on it line up with this format.
+  // Then we can use the general converter to output the response to the client and we won't
+  // be harcoded to json.
+  /**
+   *
+   * @param ResponseItem $responseItem
+   * @return array
+   */
+  private function getErrorJson(ResponseItem $responseItem) {
+    $error = array();
+    $error["code"] = $responseItem->getError();
+    $error["message"] = $responseItem->getErrorMessage();
+    return $error;
+  }
+
+  /**
+   * encodes data to json, adds jsonp callback if requested and sends response
+   * to client
+   *
+   * @param array $data
+   * @return string
+   */
+  private function encodeAndSendResponse($data) {
+    // TODO: Refactor this class to use the OutputJsonConverter, so that we do not have to duplicate
+    // encoding and JSONP handling here
+    if (isset($_GET['callback']) && preg_match('/^[a-zA-Z0-9\_\.]*$/', $_GET['callback'])) {
+        echo $_GET['callback'] . '(' . json_encode($data) . ')';
+        return;
+    }
+    echo json_encode($data);
+  }
+
+  /**
+   *
+   * @param ResponseItem $responseItem
+   */
+  public function sendError(ResponseItem $responseItem) {
+    $error = $this->getErrorJson($responseItem);
+    $this->encodeAndSendResponse($error);
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/ActivityService.php b/trunk/php/src/apache/shindig/social/spi/ActivityService.php
new file mode 100644
index 0000000..d59167c
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/ActivityService.php
@@ -0,0 +1,40 @@
+<?php
+namespace apache\shindig\social\spi;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface ActivityService {
+
+  /**
+   * Returns a list of activities that correspond to the passed in person ids.
+   */
+  public function getActivities($userIds, $groupId, $appId, $sortBy, $filterBy, $filterOp, $filterValue, $startIndex, $count, $fields, $activityIds, $token);
+
+  public function getActivity($userId, $groupId, $appdId, $fields, $activityId, SecurityToken $token);
+
+  public function deleteActivities($userId, $groupId, $appId, $activityIds, SecurityToken $token);
+
+  /**
+   * Creates the passed in activity for the given user. Once createActivity is
+   * called, getActivities will be able to return the Activity.
+   */
+  public function createActivity($userId, $groupId, $appId, $fields, $activity, SecurityToken $token);
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/AlbumService.php b/trunk/php/src/apache/shindig/social/spi/AlbumService.php
new file mode 100644
index 0000000..ab0407e
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/AlbumService.php
@@ -0,0 +1,75 @@
+<?php
+namespace apache\shindig\social\spi;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * The interface to access albums.
+ */
+interface AlbumService {
+
+  /**
+   * Returns a list of Albums that correspond to the passed in User/GroupId
+   *
+   * @param userId ID of the user to indicate the requestor
+   * @param groupId Albums for all of the people in the specific group.
+   * @param albumIds album Ids to fetch. Fetch all albums if this is empty
+   * @param collectionOptions options for sorting, pagination etc
+   * @param fields fields to fetch
+   * @param token The gadget token
+   * @return a list of albums
+   */
+  public function getAlbums($userId, $groupId, $albumIds, $collectionOptions, $fields, $token);
+
+  /**
+   * Creates an album for a user. An Album ID is created and provided back in
+   * the returned album.
+   *
+   * @param userId id of the user for whom an album is to be created
+   * @param groupId group id for this request
+   * @param album album with fields set for a create request. id field is ignored.
+   * @param token security token to authorize this request
+   * @return the created album with album id set in it
+   */
+  public function createAlbum($userId, $groupId, $album, $token);
+
+  /**
+   * Updates an album for the fields set in album.
+   *
+   * @param userId id of user whose album is to be updated
+   * @param groupId group id for this request
+   * @param album album with id and fields to be updated.
+   * @param token security token to authorize this request
+   * @return updated album
+   */
+  public function updateAlbum($userId, $groupId, $album, $token);
+
+  /**
+   * Deletes an album.
+   *
+   * @param userId id of owner of album
+   * @param groupId group id of owner of album
+   * @param albumId id of album to be deleted
+   * @param token security token to authorize this request
+   * @return void on completion
+   */
+  public function deleteAlbum($userId, $groupId, $albumId, $token);
+
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/AppDataService.php b/trunk/php/src/apache/shindig/social/spi/AppDataService.php
new file mode 100644
index 0000000..cf11bd2
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/AppDataService.php
@@ -0,0 +1,50 @@
+<?php
+namespace apache\shindig\social\spi;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface AppDataService {
+
+  /**
+   * Fetch data for a list of ids.
+   * @param UserId The user id to perform the action for
+   * @param GroupId optional grouping ID
+   * @param fields The list of fields to fetch
+   * @param token The SecurityToken for this request
+   * @return ResponseItem a response item with the error code set if
+   *     there was a problem
+   */
+  function getPersonData($userId, GroupId $groupId, $appId, $fields, SecurityToken $token);
+
+  function deletePersonData($userId, GroupId $groupId, $appId, $fields, SecurityToken $token);
+
+  /**
+   * Updates the data key for the given person with the new value.
+   *
+   * @param id The person the data is for.
+   * @param key The key of the data.
+   * @param value The new value of the data.
+   * @param token The SecurityToken for this request
+   * @return ResponseItem a response item with the error code set if
+   *     there was a problem
+   */
+  function updatePersonData(UserId $userId, GroupId $groupId, $appId, $fields, $values, SecurityToken $token);
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/CollectionOptions.php b/trunk/php/src/apache/shindig/social/spi/CollectionOptions.php
new file mode 100644
index 0000000..85933f8
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/CollectionOptions.php
@@ -0,0 +1,139 @@
+<?php
+namespace apache\shindig\social\spi;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Represents the request options for sorting/filtering/paging.
+ */
+class CollectionOptions {
+  private $sortBy;
+  private $sortOrder;
+  const SORT_ORDER_ASCENDING = 'ascending';
+  const SORT_ORDER_DESCENDING = 'descending';
+
+  private $filterBy;
+  private $filterOp;
+  private $filterValue;
+
+  const FILTER_OP_EQUALS = 'equals';
+  const FILTER_OP_CONTAINS = 'contains';
+  const FILTER_OP_STARTSWITH = 'startswith';
+  const FILTER_OP_PRESENT = 'present';
+
+  const TOP_FRIENDS_SORT = "topFriends";
+  const TOP_FRIENDS_FILTER = "topFriends";
+  const HAS_APP_FILTER = "hasApp";
+
+  private $updatedSince;
+
+  private $networkDistance;
+
+  private $startIndex;
+  private $count;
+
+  public function __construct($requestItem = NULL) {
+    if (empty($requestItem)) {
+      $this->startIndex = 0;
+      $this->count = 0;
+      $this->sortOrder = CollectionOptions::SORT_ORDER_ASCENDING;
+    } else {
+      $this->setSortBy($requestItem->getSortBy());
+      $this->setSortOrder($requestItem->getSortOrder());
+      $this->setFilterBy($requestItem->getFilterBy());
+      $this->setFilterOperation($requestItem->getFilterOperation());
+      $this->setFilterValue($requestItem->getFilterValue());
+      $this->setStartIndex($requestItem->getStartIndex());
+      $this->setCount($requestItem->getCount());
+    }
+  }
+
+  public function getSortBy() {
+    return $this->sortBy;
+  }
+
+  public function setSortBy($sortBy) {
+    $this->sortBy = $sortBy;
+  }
+
+  public function getSortOrder() {
+    return $this->sortOrder;
+  }
+
+  public function setSortOrder($sortOrder) {
+    $this->sortOrder = $sortOrder;
+  }
+
+  public function getFilterBy() {
+    return $this->filterBy;
+  }
+
+  public function setFilterBy($filterBy) {
+    $this->filterBy = $filterBy;
+  }
+
+  public function getFilterOperation() {
+    return $this->filterOp;
+  }
+
+  public function setFilterOperation($filterOp) {
+    $this->filterOp = $filterOp;
+  }
+
+  public function getFilterValue() {
+    return $this->filterValue;
+  }
+
+  public function setFilterValue($filterValue) {
+    $this->filterValue = $filterValue;
+  }
+
+  public function getUpdatedSince() {
+    return $this->updatedSince;
+  }
+
+  public function setUpdatedSince($updatedSince) {
+    $this->updatedSince = $updatedSince;
+  }
+
+  public function getNetworkDistance() {
+    return $this->networkDistance;
+  }
+
+  public function setNetworkDistance($networkDistance) {
+    $this->networkDistance = $networkDistance;
+  }
+
+  public function getStartIndex() {
+    return $this->startIndex;
+  }
+
+  public function setStartIndex($startIndex) {
+    $this->startIndex = $startIndex;
+  }
+
+  public function getCount() {
+    return $this->count;
+  }
+
+  public function setCount($count) {
+    $this->count = $count;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/DataCollection.php b/trunk/php/src/apache/shindig/social/spi/DataCollection.php
new file mode 100644
index 0000000..fd2adbe
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/DataCollection.php
@@ -0,0 +1,37 @@
+<?php
+namespace apache\shindig\social\spi;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class DataCollection {
+  public $entry;
+
+  public function __construct($entry) {
+    $this->entry = $entry;
+  }
+
+  public function getEntry() {
+    return $this->entry;
+  }
+
+  public function setEntry($entry) {
+    $this->entry = $entry;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/GroupId.php b/trunk/php/src/apache/shindig/social/spi/GroupId.php
new file mode 100644
index 0000000..085e6f2
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/GroupId.php
@@ -0,0 +1,53 @@
+<?php
+namespace apache\shindig\social\spi;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GroupId {
+  public static $types = array('all', 'friends', 'self', 'groupId');
+  private $type;
+  private $groupId;
+
+  public function __construct($type, $groupId) {
+    $this->type = $type;
+    $this->groupId = $groupId;
+  }
+
+  static public function fromJson($jsonId) {
+    if (is_array($jsonId)) {
+      if (in_array(substr($jsonId[0], 1), GroupId::$types)) {
+        return new GroupId(substr($jsonId[0], 1), null);
+      } else {
+        return new GroupId('groupId', $jsonId);
+      }
+    } elseif (in_array(substr($jsonId, 1), GroupId::$types)) {
+      return new GroupId(substr($jsonId, 1), null);
+    }
+    return new GroupId('groupId', $jsonId);
+  }
+
+  public function getGroupId() {
+    return $this->groupId;
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/GroupService.php b/trunk/php/src/apache/shindig/social/spi/GroupService.php
new file mode 100644
index 0000000..743f596
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/GroupService.php
@@ -0,0 +1,36 @@
+<?php
+namespace apache\shindig\social\spi;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface GroupService {
+
+  /**
+   * Fetch groups for a list of ids.
+   * @param UserId The user id to perform the action for
+   * @param GroupId optional grouping ID
+   * @param token The SecurityToken for this request
+   * @return ResponseItem a response item with the error code set if
+   *     there was a problem
+   */
+  function getPersonGroups($userId, GroupId $groupId, SecurityToken $token);
+
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/InvalidateService.php b/trunk/php/src/apache/shindig/social/spi/InvalidateService.php
new file mode 100644
index 0000000..7d5278d
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/InvalidateService.php
@@ -0,0 +1,56 @@
+<?php
+namespace apache\shindig\social\spi;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\common\RemoteContentRequest;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface InvalidateService {
+  /**
+   * Invalidate a set of cached resources that are part of the application specification itself.
+   * This includes gadget specs, manifests and message bundles
+   * @param uris of content to invalidate
+   * @param token identifying the calling application
+   */
+  function invalidateApplicationResources(Array $uris, SecurityToken $token);
+
+  /**
+   * Invalidate all cached resources where the specified user ids were used as either the
+   * owner or viewer id when a signed or OAuth request was made for the content by the application
+   * identified in the security token.
+   * @param opensocialIds Set of user ids to invalidate authenticated/signed content for
+   * @param token identifying the calling application
+   */
+  function invalidateUserResources(Array $opensocialIds, SecurityToken $token);
+
+  /**
+   * Is the specified request still valid. If the request is signed or authenticated
+   * has its content been invalidated by a call to invalidateUserResource subsequent to the
+   * response being cached.
+   */
+  function isValid(RemoteContentRequest $request);
+
+  /**
+   * Mark the request prior to caching it so that subsequent calls to isValid can detect
+   * if it has been invalidated.
+   */
+  function markResponse(RemoteContentRequest $request);
+}
+
diff --git a/trunk/php/src/apache/shindig/social/spi/MediaItemService.php b/trunk/php/src/apache/shindig/social/spi/MediaItemService.php
new file mode 100644
index 0000000..95b4c41
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/MediaItemService.php
@@ -0,0 +1,85 @@
+<?php
+namespace apache\shindig\social\spi;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Media Service exposes an interface to access the media items.
+ */
+interface MediaItemService {
+  
+  /**
+   * Returns mediaItems from an album
+   *
+   * @param userId The id of the person whose album to fetch
+   * @param groupId The group Id
+   * @param albumId The id of the album to fetch
+   * @param mediaItemIds MediaItemIds to fetch. Fetch all mediaItems if this is empty
+   * @param collectionOptions options for sorting, pagination etc
+   * @param fields fields to fetch
+   * @param token The gadget token
+   * @return a list of media items
+   */
+  public function getMediaItems($userId, $groupId, $albumId, $mediaItemIds, $collectionOptions, $fields, $token);
+
+  /**
+   * Creates a media item in a specified album. The albumId is taken from the
+   * mediaItem object. id of the media item object should not be set. A file may
+   * be uploaded with the content type 'multipart/form-data', 'image/*', 'video/*'
+   * or 'audio/*'. The uploaded file is moved to a temporary location. The file info
+   * is stored in the 'file' param. After this method is invoked the file is deleted.
+   *
+   * @param userId id of the user for whom a media item is to be created
+   * @param groupId group id
+   * @param mediaItem specifies album-id and media item fields
+   * @param An associative array that describes the uploaded file. The array is empty if
+   *     there is no uploaded file. It has 'name', 'tmp_name', 'type' and 'size' fields.
+   *     i.e. [tmp_name] => /tmp/upload//tmp/php/php1h4j1o, [type] => image/png,
+   *     [size] => 123, [name] = user_file_name.png.  
+   *     The file is a regular file and should not be moved by the move_uploaded_file method.
+   * @param token security token to authorize this request
+   * @return the created media item
+   */
+  public function createMediaItem($userId, $groupId, $mediaItem, $file, $token);
+
+  /**
+   * Updates a media item in an album. Album id and media item id is taken in
+   * from albumMediaItem.
+   *
+   * @param userId id of user whose media item is to be updated
+   * @param groupId group id
+   * @param mediaItem specifies album id, media-item id, fields to update
+   * @param token security token
+   * @return updated album media item
+   */
+  public function updateMediaItem($userId, $groupId, $mediaItem, $token);
+
+  /**
+   * Deletes an album media item.
+   *
+   * @param id id of user whose media item is to be deleted
+   * @param groupId group id
+   * @param albumId id of album to update
+   * @param mediaItemIds ids of media item to update
+   * @param token security token to authorize this update request
+   * @return void on successful completion
+   */
+  public function deleteMediaItems($userId, $groupId, $albumId, $mediaItemIds, $token);  
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/MessagesService.php b/trunk/php/src/apache/shindig/social/spi/MessagesService.php
new file mode 100644
index 0000000..cff36f4
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/MessagesService.php
@@ -0,0 +1,103 @@
+<?php
+namespace apache\shindig\social\spi;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface MessagesService {
+
+  /**
+   * Creates a new message collection for the given arguments.
+   * NOTE: It'd better not to use 0 or "0" as message collection id
+   * to prevent pertential problems.
+   * @param userId  The userId to create the message collection for
+   * @param msgCollection A message collection that is to be created
+   * @param token  A security token for this request
+   */
+  public function createMessageCollection($userId, $msgCollection, $token);
+
+  /**
+   * Updates a message collection for the given arguments
+   * @param userId  The userId to update the message collection for
+   * @param msgCollection Data for the message collection to be updated
+   * @param token  A security token for this request
+   */
+  public function updateMessageCollection($userId, $msgCollection, $token);
+
+  /**
+   * Deletes a message collection for the given arguments
+   * @param userId  The userId to create the message collection for
+   * @param msgCollId The message collection id to be deleted
+   * @param token  A security token for this request
+   */
+  public function deleteMessageCollection($userId, $msgCollId, $token);
+
+  /**
+   * Returns a list of message collections corresponding to the given user
+   * @param userId   The User to fetch for
+   * @param fields   The fields that the returned message collections contain.
+   * @param options  Filter criteria, pagination, etc.
+   * @param token    Given security token for this request
+   * @return a collection of message collections.
+   */
+  public function getMessageCollections($userId, $fields, $options, $token);
+
+  /**
+   * Posts a message to the user's specified message collection and sends the
+   * message to the set of recipients specified in the message.
+   * @param userId      The user posting the message.
+   * @param msgCollId   The message collection Id to post to
+   * @param message     The message to post
+   * @param token       A valid security token
+   */
+  public function createMessage($userId, $msgCollId, $message, $token);
+
+  /**
+   * Updates a specific message with new data
+   * @param userId      The User to modify for
+   * @param msgCollId   The Message Collection ID to update from
+   * @param message     The message details to modify
+   * @param token       Given Security Token for this request
+   */
+  public function updateMessage($userId, $msgCollId, $message, $token);
+
+  /**
+   * Deletes a set of messages for a given user/message collection
+   * @param userId      The User to delete for
+   * @param msgCollId   The Message Collection ID to delete from
+   * @param messageIds  List of IDs to delete
+   * @param token       Given Security Token for this request
+   */
+  public function deleteMessages($userId, $msgCollId, $messageIds, $token);
+
+  /**
+   * Returns a collection of messages that correspond to the passed in criteria.
+   * The container implementation can get appId from the token and translate
+   * the appId to appUrl in the response.
+   * @param userId     The user id to fetch for
+   * @param msgCollId  A message collection id. Supports @inbox and @outbox defined in MessageCollection class.
+   * @param fields     The fields to fetch for the messages
+   * @param msgIds     An explicit set of message ids to fetch. Empty means all the messages that fulfills the given criterias.
+   * @param options    Options to control the fetch.
+   * @param token      Given security token for this request
+   * @return a collection of messages
+   */
+  public function getMessages($userId, $msgCollId, $fields, $msgIds, $options, $token);
+
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/PersonService.php b/trunk/php/src/apache/shindig/social/spi/PersonService.php
new file mode 100644
index 0000000..1d8e8a9
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/PersonService.php
@@ -0,0 +1,44 @@
+<?php
+namespace apache\shindig\social\spi;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+interface PersonService {
+
+  /**
+   * Returns a Person object for person with $id or false on not found
+   *
+   * @param container specific id $id
+   * @param fields set of contact fields to return, as array('fieldName' => 1)
+   * @param security token $token
+   */
+  function getPerson($userId, $groupId, $fields, SecurityToken $token);
+
+  /**
+   * Returns a list of people that correspond to the passed in person ids.
+   * @param ids The ids of the people to fetch.
+   * @param options Request options for filtering/sorting/paging
+   * @param fields set of contact fields to return, as array('fieldName' => 1)
+   * @return a list of people.
+   */
+  function getPeople($userId, $groupId, CollectionOptions $options, $fields, SecurityToken $token);
+}
+
diff --git a/trunk/php/src/apache/shindig/social/spi/RestfulCollection.php b/trunk/php/src/apache/shindig/social/spi/RestfulCollection.php
new file mode 100644
index 0000000..702fda2
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/RestfulCollection.php
@@ -0,0 +1,103 @@
+<?php
+namespace apache\shindig\social\spi;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * This class represents a RESTful social data response
+ */
+class RestfulCollection {
+  
+  public $entry;
+  public $startIndex;
+  public $totalResults;
+  public $itemsPerPage;
+  
+  // boolean flags to indicate whether the requested operations were performed or declined
+  public $filtered;
+  public $sorted;
+  public $updatedSince;
+
+  public static function createFromEntry($entry) {
+    return new RestfulCollection($entry, 0, count($entry));
+  }
+
+  public function __construct($entry, $startIndex, $totalResults) {
+    $this->entry = $entry;
+    $this->startIndex = $startIndex;
+    $this->totalResults = $totalResults;
+  }
+
+  public function getEntry() {
+    return $this->entry;
+  }
+
+  public function setEntry($entry) {
+    $this->entry = $entry;
+  }
+
+  public function getStartIndex() {
+    return $this->startIndex;
+  }
+
+  public function setStartIndex($startIndex) {
+    $this->startIndex = $startIndex;
+  }
+
+  public function getItemsPerPage() {
+    return $this->itemsPerPage;
+  }
+
+  public function setItemsPerPage($itemsPerPage) {
+    $this->itemsPerPage = $itemsPerPage;
+  }
+
+  public function getTotalResults() {
+    return $this->totalResults;
+  }
+
+  public function setTotalResults($totalResults) {
+    $this->totalResults = $totalResults;
+  }
+
+  public function getFiltered($filtered) {
+    $this->filtered = $filtered;
+  }
+
+  public function setFiltered($filtered) {
+    $this->filtered = $filtered;
+  }
+
+  public function getSorted($sorted) {
+    $this->sorted = $sorted;
+  }
+
+  public function setSorted($sorted) {
+    $this->sorted = $sorted;
+  }
+
+  public function getUpdatedSince($updatedSince) {
+    $this->updatedSince = $updatedSince;
+  }
+
+  public function setUpdatedSince($updatedSince) {
+    $this->updatedSince = $updatedSince;
+  }
+}
diff --git a/trunk/php/src/apache/shindig/social/spi/UserId.php b/trunk/php/src/apache/shindig/social/spi/UserId.php
new file mode 100644
index 0000000..da8e785
--- /dev/null
+++ b/trunk/php/src/apache/shindig/social/spi/UserId.php
@@ -0,0 +1,62 @@
+<?php
+namespace apache\shindig\social\spi;
+use apache\shindig\common\SecurityToken;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class UserId {
+  public static $types = array('me', 'viewer', 'owner', 'userId');
+  private $type;
+  private $userId;
+
+  public function __construct($type, $userId) {
+    $this->type = $type;
+    $this->userId = $userId;
+  }
+
+  static public function fromJson($jsonId) {
+    if (in_array(substr($jsonId, 1), UserId::$types)) {
+      return new UserId(substr($jsonId, 1), null);
+    }
+    return new UserId('userId', $jsonId);
+  }
+
+  public function getUserId(SecurityToken $token) {
+    switch ($this->type) {
+      case 'viewer':
+      case 'me':
+        return $token->getViewerId();
+        break;
+      case 'owner':
+        return $token->getOwnerId();
+        break;
+      case 'userId':
+        return $this->userId;
+        break;
+      default:
+        throw new \Exception("The type field is not a valid enum: {$this->type}");
+        break;
+    }
+  }
+
+  public function getType() {
+    return $this->type;
+  }
+}
diff --git a/trunk/php/test/bootstrap.php b/trunk/php/test/bootstrap.php
new file mode 100644
index 0000000..de3c1b2
--- /dev/null
+++ b/trunk/php/test/bootstrap.php
@@ -0,0 +1,27 @@
+<?php
+namespace apache\shindig\test;
+
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+include __DIR__ . "/../src/apache/shindig/bootstrap.php";
+
+// load the test config instead of the production one
+Config::loadConfig(__DIR__. '/../config/test.php');
\ No newline at end of file
diff --git a/trunk/php/test/certs/README b/trunk/php/test/certs/README
new file mode 100644
index 0000000..8de5508
--- /dev/null
+++ b/trunk/php/test/certs/README
@@ -0,0 +1,5 @@
+These keys are just used for unit test and should never be used in production.
+
+Goto the certs directory shindig/php/certs and create your own public and private keys
+with your own password there.
+
diff --git a/trunk/php/test/certs/private.key b/trunk/php/test/certs/private.key
new file mode 100644
index 0000000..eaa9e5a
--- /dev/null
+++ b/trunk/php/test/certs/private.key
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,FA1FDD216DED3C69
+
+BFekZznDqWZAAtEryw/br7Yh3U8b0NWxgWQXPlaxlE3gQQYA8PJB4Jh/cjw7V4kQ
+PcZddQSZ3KcN9a915aVoLdOJfp3RTOXk8wIRYBQ2o17ap7T/ylJlT0/qQdWp/3bh
+s3FnY19ZaX2dAfh2ETCqWFoLMn+RuT8xgju3uMM9N9NmaQ080n/KGvl09Ljb9ul3
+91J3SrDT5IpfbAGicpcmyAV3WsQQC1FQsP3oJyADp/63oZeIuVFsrfAgRfR2xUwd
+sS0mawHHEIDsZBPc/9tiiSA+a9QZrrcrra3RtOg2gFF6oErZuO0Ot7lI+Q6QbKPk
+tNrMitIwMd6qQdTT3gBHbAzhk1D6db2k5gWOki74bxJMyWVDX01i5ncU8jn34p2g
+6yDrjiEGdVACS/xHKeGJk/mBL8y/0wMq4yENHeEnQbFbYBo9NUOCsMc+Ja9DuLKs
+o+dUWBHrGk6wyWBHi6I0Jox3hBQqrE8a5bVj+jvlck5Z+/ncOvVu8K7eL57yhQzr
+RVW43qx/rqg5Diz3HEajo/I9qBEOidIBtijPXPkR9lSjXZlQrF6ItIA7HV14grCW
+ZvycEttN5Z9OlnujbHddYHbTNb7ncLG+XfemWBVMWzDGLEEq8uYaLnFRTtbOdTT4
+EBARGw2MF6JRxYmEkf1yYGq61Hp1LAduF+UHodgH4tAg82KxLEKBC1ZPdSSF8pNq
+3iaspbPSJo/ZGvI9uWUY+lyv4tsCa4HC62cU/TMjxNB6ll+HC5CYNyi96pdryeEW
++tPkpYxX6Oyv20gzwaqyv3MaYuHz+nHsJt87bW6CrGX4v2fzP2/maA==
+-----END RSA PRIVATE KEY-----
diff --git a/trunk/php/test/certs/public.crt b/trunk/php/test/certs/public.crt
new file mode 100644
index 0000000..0cff664
--- /dev/null
+++ b/trunk/php/test/certs/public.crt
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICkjCCAfugAwIBAgIJAI4XlVLWTYStMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
+BAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRcwFQYDVQQKEw5BcGFjaGUgU2hp
+bmRpZzAeFw0xMDA4MTUxNjAyMDZaFw0xMTA4MTUxNjAyMDZaMDsxCzAJBgNVBAYT
+AlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRcwFQYDVQQKEw5BcGFjaGUgU2hpbmRp
+ZzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAxXdIKgzLkMp1xlHyP+qwlvZX
+Doz2kr/uC3UCoJHEG5bIvO3JZ5YQBDhQeNq2HyWzyi9645HHtU3WsnonJTL0WHYo
+zXCwTesJrgRVDFxoQfvRu2E9p78rX1QtaLSdDgQGJkNO2MFMz7d2V+ADMPEEBJYg
+E2qj2eOwjY5qJv356skCAwEAAaOBnTCBmjAdBgNVHQ4EFgQU/NfE6kopCs7uO3Ni
+602hPU4GWGswawYDVR0jBGQwYoAU/NfE6kopCs7uO3Ni602hPU4GWGuhP6Q9MDsx
+CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpTb21lLVN0YXRlMRcwFQYDVQQKEw5BcGFj
+aGUgU2hpbmRpZ4IJAI4XlVLWTYStMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEF
+BQADgYEAj/QV42ys1V1TCi1C56BUD9e1oJVArEmlKITjlFQk4MNE55zgdvUSySHF
+rhfkqnQYuczJRL52r5SvzM06bjpNumDJYFwpKOlwYz05LFTCrOLhJF4YmtOPoHJp
+xvYLtrT6xJb192cTHDyiw8aqGa/f0An5C2fD9P9NCmiHZQazg6U=
+-----END CERTIFICATE-----
diff --git a/trunk/php/test/common/BasicBlobCrypterTest.php b/trunk/php/test/common/BasicBlobCrypterTest.php
new file mode 100644
index 0000000..de81251
--- /dev/null
+++ b/trunk/php/test/common/BasicBlobCrypterTest.php
@@ -0,0 +1,110 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\sample\BasicBlobCrypter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * BasicBlobCrypter test case.
+ */
+class BasicBlobCrypterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var BasicBlobCrypter
+   */
+  private $BasicBlobCrypter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->BasicBlobCrypter = new BasicBlobCrypter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->BasicBlobCrypter = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests BasicBlobCrypter->__construct()
+   */
+  public function test__construct() {
+    $this->BasicBlobCrypter->__construct();
+  }
+
+  /**
+   * Tests BasicBlobCrypter->wrap()
+   */
+  public function testWrap() {
+    $test = array();
+    $test['o'] = 'o';
+    $test['v'] = 'v';
+    $test['a'] = 'a';
+    $test['d'] = 'd';
+    $test['u'] = 'u';
+    $test['m'] = 'm';
+    $wrapped = $this->BasicBlobCrypter->wrap($test);
+    $unwrapped = $this->BasicBlobCrypter->unwrap($wrapped, 3600);
+    $this->assertEquals($unwrapped['o'], 'o');
+    $this->assertEquals($unwrapped['v'], 'v');
+    $this->assertEquals($unwrapped['a'], 'a');
+    $this->assertEquals($unwrapped['d'], 'd');
+    $this->assertEquals($unwrapped['u'], 'u');
+    $this->assertEquals($unwrapped['m'], 'm');
+  }
+
+  /**
+   * Tests BasicBlobCrypter->wrap() exception
+   */
+  public function testWrapException() {
+    $this->setExpectedException('apache\shindig\common\sample\BlobExpiredException');
+    $test = array();
+    $test['o'] = 'o';
+    $test['v'] = 'v';
+    $test['a'] = 'a';
+    $test['d'] = 'd';
+    $test['u'] = 'u';
+    $test['m'] = 'm';
+    $wrapped = $this->BasicBlobCrypter->wrap($test);
+    /* there is a 180 seconds clock skew allowed, so this way we make sure it's expired */
+    $this->BasicBlobCrypter->unwrap($wrapped, - 4000);
+  }
+
+  /**
+   * Tests BasicBlobCrypter->unwrap() with plaintext token
+   */
+  public function testUnwrapPlaintextToken() {
+    $token = "o:v:a:d:http://host:80/gadget.xml:m:c";
+    $unwrapped = $this->BasicBlobCrypter->unwrap($token, null);
+    $this->assertEquals($unwrapped['o'], 'o');
+    $this->assertEquals($unwrapped['v'], 'v');
+    $this->assertEquals($unwrapped['a'], 'a');
+    $this->assertEquals($unwrapped['d'], 'd');
+    $this->assertEquals($unwrapped['u'], 'http://host:80/gadget.xml');
+    $this->assertEquals($unwrapped['m'], 'm');
+  }
+
+}
+
diff --git a/trunk/php/test/common/BasicRemoteContentTest.php b/trunk/php/test/common/BasicRemoteContentTest.php
new file mode 100644
index 0000000..75176cd
--- /dev/null
+++ b/trunk/php/test/common/BasicRemoteContentTest.php
@@ -0,0 +1,373 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\sample\BasicRemoteContent;
+use apache\shindig\common\sample\BasicSecurityTokenDecoder;
+use apache\shindig\common\sample\BasicRemoteContentFetcher;
+use apache\shindig\gadgets\SigningFetcherFactory;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\RemoteContentFetcher;
+use apache\shindig\gadgets\SigningFetcher;
+use apache\shindig\common\Config;
+use apache\shindig\common\sample\BasicSecurityToken;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockSigningFetcherFactory {
+  private $keyName;
+  private $privateKey;
+
+  /**
+   * Produces a signing fetcher that will sign requests and delegate actual
+   * network retrieval to the {@code networkFetcher}
+   *
+   * @param RemoteContentFetcher $networkFetcher The fetcher that will be doing actual work.
+   * @return SigningFetcher
+   * @throws GadgetException
+   */
+  public function getSigningFetcher(RemoteContentFetcher $networkFetcher) {
+    return SigningFetcher::makeFromOpenSslPrivateKey($networkFetcher, $this->keyName, $this->privateKey);
+  }
+
+  /**
+   * @here will create a private key.
+   */
+  public function __construct() {
+    $_SERVER["HTTP_HOST"] = 'localhost';
+    $privkey = openssl_pkey_new();
+    $phrase = Config::get('private_key_phrase') != '' ? (Config::get('private_key_phrase')) : null;
+    openssl_pkey_export($privkey, $rsa_private_key, $phrase);
+    
+    if (! $rsa_private_key = @openssl_pkey_get_private($rsa_private_key, $phrase)) {
+      throw new \Exception("Could not create the key");
+    }
+    $this->privateKey = $rsa_private_key;
+    $this->keyName = 'http://' . $_SERVER["HTTP_HOST"] . Config::get('web_prefix') . '/public.cer';
+  }
+}
+
+class MockRemoteContentFetcher extends RemoteContentFetcher {
+  private $expectedRequest = array();
+
+  private $expectedMultiRequest = array();
+
+  private $actualRequest = array();
+
+  private $actualMultiRequest = array();
+  
+  private $valid = array(true, true, true, true);
+
+  public function fetchRequest(RemoteContentRequest $request) {
+    $this->actualRequest[] = $request;
+    $this->fetch($request);
+    return $request;
+  }
+
+  public function multiFetchRequest(Array $requests) {
+    $this->actualMultiRequest[] = $requests;
+    foreach ($requests as $request) {
+      $this->fetch($request);
+    }
+    return $requests;
+  }
+
+  public function expectFetchRequest(RemoteContentRequest $request) {
+    $this->expectedRequest[] = $request;
+  }
+
+  public function expectMultiFetchRequest(Array $requests) {
+    $this->expectedMultiRequest[] = $requests;
+  }
+
+  public function verify() {
+    $result = ($this->expectedRequest == $this->actualRequest) &&
+              ($this->expectedMultiRequest == $this->actualMultiRequest);
+    $this->clean();
+    return $result;
+  }
+
+  public function clean() {
+    $this->actualRequest = array();
+    $this->actualMultiRequest = array();
+    $this->expectedRequest = array();
+    $this->expectedMultiRequest = array();
+  }
+
+  private function fetch(RemoteContentRequest $request) {
+    if ($request->getUrl() == 'http://test.chabotc.com/ok.html') {
+      $request->setHttpCode(200);
+      $request->setContentType('text/html; charset=UTF-8');
+      $request->setResponseContent('OK');
+    } else if ($request->getUrl() == 'http://test.chabotc.com/fail.html') {
+      $request->setHttpCode(404);
+    } else if (preg_match('/http:\/\/test\.chabotc\.com\/valid(\d)\.html/',
+                          $request->getUrl(), $matches) > 0) {
+      if ($this->valid[intval($matches[1])]) {
+        $this->valid[intval($matches[1])] = false;
+        $request->setHttpCode(200);
+        $request->setContentType('text/html; charset=UTF-8');
+        $request->setResponseContent('OK');
+      } else {
+        $request->setHttpCode(404);
+      }
+    } else if (strpos($request->getUrl(), 'http://test.chabotc.com/signing.html') == 0) {
+      $url = parse_url($request->getUrl());
+      $query = array();
+      parse_str($url['query'], $query);
+      $request->setHttpCode(200);
+      $request->setContentType('text/html; charset=UTF-8');
+      if ($query['xoauth_signature_publickey'] && $query['oauth_signature']) {
+        $request->setResponseContent('OK');
+      } else {
+        $request->setResponseContent('FAILED');
+      }
+    }
+  }
+}
+
+/**
+ * BasicRemoteContent test case.
+ */
+class BasicRemoteContentTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var BasicRemoteContent
+   */
+  private $basicRemoteContent = null;
+
+  /**
+   * @var MockRemoteContentFetcher
+   */
+  private $fetcher = null;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->fetcher = new MockRemoteContentFetcher();
+    $signingFetcherFactory = new MockSigningFetcherFactory();
+    $this->basicRemoteContent = new BasicRemoteContent($this->fetcher, $signingFetcherFactory, new BasicSecurityTokenDecoder());
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->basicRemoteContent = null;
+    $this->fetcher = null;
+    parent::tearDown();
+  }
+  
+  /**
+   * Tests BasicRemoteContent->__construct()
+   */
+  public function testConstruct() {
+    $basic = new BasicRemoteContent(new BasicRemoteContentFetcher(), null, false);
+    $signing = new BasicRemoteContent(new BasicRemoteContentFetcher(), new SigningFetcherFactory(), new BasicSecurityTokenDecoder());
+  }
+
+  /**
+   * Tests BasicRemoteContent->fetch()
+   */
+  public function testFetch() {
+    $request = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $ret = $this->basicRemoteContent->fetch($request);
+    $content = $ret->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+  }
+
+  /**
+   * Tests BasicRemoteContent->fetch() 404 response
+   */
+  public function testFetch404() {
+    $request = new RemoteContentRequest('http://test.chabotc.com/fail.html');
+    $ret = $this->basicRemoteContent->fetch($request);
+    $this->assertEquals('404', $ret->getHttpCode());
+  }
+  
+  /**
+   * Tests BasicRemoteContent->fetch() with different response
+   */
+  public function testFetchValid() {
+    $this->fetcher->clean();
+    $request = new RemoteContentRequest('http://test.chabotc.com/valid0.html');
+    $this->basicRemoteContent->invalidate($request);
+    $this->fetcher->expectFetchRequest($request);
+    $ret = $this->basicRemoteContent->fetch($request);
+    $this->assertTrue($this->fetcher->verify());
+    $content = $ret->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+    
+    $request = new RemoteContentRequest('http://test.chabotc.com/valid0.html');
+    $this->basicRemoteContent->invalidate($request);
+    $this->fetcher->expectFetchRequest($request);
+    $ret = $this->basicRemoteContent->fetch($request);
+    $this->assertTrue($this->fetcher->verify());
+    $content = $ret->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+  }
+  
+  /**
+   * Tests BasicRemoteContent->multiFetch() with different response
+   */
+  public function testmultiFetchValid() {
+    $this->fetcher->clean();
+    $requests = array();
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/valid1.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/valid2.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/valid3.html');
+    $this->basicRemoteContent->invalidate($requests[0]);
+    $this->basicRemoteContent->invalidate($requests[1]);
+    $this->basicRemoteContent->invalidate($requests[2]);
+    $this->fetcher->expectMultiFetchRequest($requests);
+    $rets = $this->basicRemoteContent->multiFetch($requests);
+    $this->assertTrue($this->fetcher->verify());
+    $content_0 = $rets[0]->getResponseContent();
+    $content_1 = $rets[1]->getResponseContent();
+    $content_2 = $rets[2]->getResponseContent();
+    $this->assertEquals("OK", trim($content_0));
+    $this->assertEquals("OK", trim($content_1));
+    $this->assertEquals("OK", trim($content_2));
+    $this->assertEquals('200', $rets[0]->getHttpCode());
+    $this->assertEquals('200', $rets[1]->getHttpCode());
+    $this->assertEquals('200', $rets[2]->getHttpCode());
+    
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/valid1.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/valid2.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/valid3.html');
+    $this->basicRemoteContent->invalidate($requests[0]);
+    $this->basicRemoteContent->invalidate($requests[1]);
+    $this->basicRemoteContent->invalidate($requests[2]);
+    $this->fetcher->expectMultiFetchRequest($requests);
+    $rets = $this->basicRemoteContent->multiFetch($requests);
+    $this->assertTrue($this->fetcher->verify());
+    $content_0 = $rets[0]->getResponseContent();
+    $content_1 = $rets[1]->getResponseContent();
+    $content_2 = $rets[2]->getResponseContent();
+    $this->assertEquals("OK", trim($content_0));
+    $this->assertEquals("OK", trim($content_1));
+    $this->assertEquals("OK", trim($content_2));
+    $this->assertEquals('200', $rets[0]->getHttpCode());
+    $this->assertEquals('200', $rets[1]->getHttpCode());
+    $this->assertEquals('200', $rets[2]->getHttpCode());
+  }
+
+  /**
+   * Tests BasicRemoteContent->fetch() 200, 200 and 200 responses
+   */
+  public function testMultiFetch() {
+    $requests = array();
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+
+    $rets = $this->basicRemoteContent->multiFetch($requests);
+    $content_0 = $rets[0]->getResponseContent();
+    $content_1 = $rets[1]->getResponseContent();
+    $content_2 = $rets[2]->getResponseContent();
+    $this->assertEquals("OK", trim($content_0));
+    $this->assertEquals("OK", trim($content_1));
+    $this->assertEquals("OK", trim($content_2));
+    $this->assertEquals('200', $rets[0]->getHttpCode());
+    $this->assertEquals('200', $rets[1]->getHttpCode());
+    $this->assertEquals('200', $rets[2]->getHttpCode());
+  }
+
+  /**
+   * Tests BasicRemoteContent->Multifetch() 200, 200 and 404 responses
+   */
+  public function testMultiFetchMix() {
+    $requests = array();
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/fail.html');
+
+    $rets = $this->basicRemoteContent->multiFetch($requests);
+    $content_0 = $rets[0]->getResponseContent();
+    $content_1 = $rets[1]->getResponseContent();
+    $this->assertEquals("OK", trim($content_0));
+    $this->assertEquals("OK", trim($content_1));
+    $this->assertEquals('200', $rets[0]->getHttpCode());
+    $this->assertEquals('200', $rets[1]->getHttpCode());
+    $this->assertEquals('404', $rets[2]->getHttpCode());
+  }
+
+  /**
+   * Tests BasicRemoteContent->Multifetch() 404, 404 and 404 responses
+   */
+  public function testMultiFetch404() {
+    $requests = array();
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/fail.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/fail.html');
+    $requests[] = new RemoteContentRequest('http://test.chabotc.com/fail.html');
+    $rets = $this->basicRemoteContent->multiFetch($requests);
+    $this->assertEquals('404', $rets[0]->getHttpCode());
+    $this->assertEquals('404', $rets[1]->getHttpCode());
+    $this->assertEquals('404', $rets[2]->getHttpCode());
+  }
+
+  /**
+   * Tests BasicRemoteContent->invalidate()
+   */
+  public function testInvalidate() {
+    // Fetches url for the first time.
+    $request = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $ret = $this->basicRemoteContent->fetch($request);
+    $this->fetcher->clean();
+    $content = $ret->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+
+    // Fetches url again and $this->fetcher->fetchRequest will not be called.
+    $request = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $ret = $this->basicRemoteContent->fetch($request);
+    $this->assertTrue($this->fetcher->verify());
+    $content = $ret->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+
+    // Invalidates cache and fetches url.
+    // $this->fetcher->fetchRequest will be called.
+    $request = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $this->fetcher->expectFetchRequest($request);
+    $this->basicRemoteContent->invalidate($request);
+    $ret = $this->basicRemoteContent->fetch($request);
+    $this->assertTrue($this->fetcher->verify());
+    $content = $ret->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+  }
+  
+  /**
+   * Tests through SigningFetcher
+   */
+  public function testSigningFetch() {
+    $request1 = new RemoteContentRequest('http://test.chabotc.com/signing.html');
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $request1->setToken($token);
+    $request1->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+    $request2 = new RemoteContentRequest('http://test.chabotc.com/ok.html');
+    $this->basicRemoteContent->invalidate($request1);
+    $this->basicRemoteContent->invalidate($request2);
+    $requests = array($request1, $request2);
+    $this->basicRemoteContent->multiFetch($requests);
+    $content = $request1->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+    $content = $request2->getResponseContent();
+    $this->assertEquals("OK", trim($content));
+  }
+}
diff --git a/trunk/php/test/common/BasicSecurityTokenTest.php b/trunk/php/test/common/BasicSecurityTokenTest.php
new file mode 100644
index 0000000..5f3105d
--- /dev/null
+++ b/trunk/php/test/common/BasicSecurityTokenTest.php
@@ -0,0 +1,179 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\SecurityToken;
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * BasicSecurityToken test case.
+ */
+class BasicSecurityTokenTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var BasicSecurityToken
+   */
+  private $BasicSecurityToken;
+  
+  /**
+   * @var BasicSecurityToken
+   */
+  private $anonymousToken;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->BasicSecurityToken = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $this->anonymousToken = BasicSecurityToken::createFromValues(SecurityToken::$ANONYMOUS, SecurityToken::$ANONYMOUS, 'app', 'domain', 'appUrl', '1', 'default');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->BasicSecurityToken = null;
+    $this->anonymousToken = null;
+    TestBasicSecurityToken::resetRawToken();
+    unset($_SERVER['HTTP_AUTHORIZATION']);
+    unset($_POST['st']);
+    unset($_GET['st']);
+    parent::tearDown();
+  }
+
+  /**
+   * Tests BasicSecurityToken::createFromValues(), toSerialForm() and createFromToken() 
+   */
+  public function testCreateFromValues() {
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $this->assertEquals('owner', $token->getOwnerId());
+    $this->assertEquals('viewer', $token->getViewerId());
+    $this->assertEquals('app', $token->getAppId());
+    $this->assertEquals('domain', $token->getDomain());
+    $this->assertEquals('appUrl', $token->getAppUrl());
+    $this->assertEquals('1', $token->getModuleId());
+    
+    $stringToken = urldecode($token->toSerialForm());
+    $duplicatedToken = BasicSecurityToken::createFromToken($stringToken, Config::get('token_max_age'));
+    $this->assertEquals('owner', $duplicatedToken->getOwnerId());
+    $this->assertEquals('viewer', $duplicatedToken->getViewerId());
+    $this->assertEquals('app', $duplicatedToken->getAppId());
+    $this->assertEquals('domain', $duplicatedToken->getDomain());
+    $this->assertEquals('appUrl', $duplicatedToken->getAppUrl());
+    $this->assertEquals('1', $duplicatedToken->getModuleId());
+  }
+
+  /**
+   * Tests BasicSecurityToken->getAppId()
+   */
+  public function testGetAppId() {
+    $this->assertEquals('app', $this->BasicSecurityToken->getAppId());
+    $this->setExpectedException('apache\shindig\common\sample\BasicSecurityTokenException');
+    $this->anonymousToken->getAppId();
+  }
+
+  /**
+   * Tests BasicSecurityToken->getAppUrl()
+   */
+  public function testGetAppUrl() {
+    $this->assertEquals('appUrl', $this->BasicSecurityToken->getAppUrl());
+    $this->setExpectedException('apache\shindig\common\sample\BasicSecurityTokenException');
+    $this->anonymousToken->getAppUrl();
+  }
+
+  /**
+   * Tests BasicSecurityToken->getDomain()
+   */
+  public function testGetDomain() {
+    $this->assertEquals('domain', $this->BasicSecurityToken->getDomain());
+    $this->setExpectedException('apache\shindig\common\sample\BasicSecurityTokenException');
+    $this->anonymousToken->getDomain();
+  }
+
+  /**
+   * Tests BasicSecurityToken->getModuleId()
+   */
+  public function testGetModuleId() {
+    $this->assertEquals(1, $this->BasicSecurityToken->getModuleId());
+    $this->setExpectedException('apache\shindig\common\sample\BasicSecurityTokenException');
+    $this->anonymousToken->getModuleId();
+  }
+
+  /**
+   * Tests BasicSecurityToken->getOwnerId()
+   */
+  public function testGetOwnerId() {
+    $this->assertEquals('owner', $this->BasicSecurityToken->getOwnerId());
+    $this->setExpectedException('apache\shindig\common\sample\BasicSecurityTokenException');
+    $this->anonymousToken->getOwnerId();
+  }
+
+  /**
+   * Tests BasicSecurityToken->getViewerId()
+   */
+  public function testGetViewerId() {
+    $this->assertEquals('viewer', $this->BasicSecurityToken->getViewerId());
+    $this->setExpectedException('apache\shindig\common\sample\BasicSecurityTokenException');
+    $this->anonymousToken->getViewerId();
+  }
+
+  /**
+   * Tests BasicSecurityToken->isAnonymous()
+   */
+  public function testIsAnonymous() {
+    $this->assertFalse($this->BasicSecurityToken->isAnonymous());
+  }
+
+  public function testGetRawToken() {
+      $_GET['st'] = 'abc';
+
+      $this->assertEquals('abc', BasicSecurityToken::getTokenStringFromRequest());
+      TestBasicSecurityToken::resetRawToken();
+
+      $_POST['st'] = 'def';
+      $_SERVER['HTTP_AUTHORIZATION'] = 'OAuth ghi';
+      $this->assertEquals('abc', BasicSecurityToken::getTokenStringFromRequest());
+
+      unset($_GET['st']);
+
+      // test if runtime cache works
+      $this->assertEquals('abc', BasicSecurityToken::getTokenStringFromRequest());
+      TestBasicSecurityToken::resetRawToken();
+      //should use post now
+      $this->assertEquals('def', BasicSecurityToken::getTokenStringFromRequest());
+      TestBasicSecurityToken::resetRawToken();
+
+      unset($_POST['st']);
+
+      // get token from OAuth header
+      $this->assertEquals('ghi', BasicSecurityToken::getTokenStringFromRequest());
+  }
+}
+
+class TestBasicSecurityToken extends BasicSecurityToken
+{
+    static public function resetRawToken()
+    {
+        parent::$rawToken = null;
+    }
+
+}
diff --git a/trunk/php/test/common/CacheFileTest.php b/trunk/php/test/common/CacheFileTest.php
new file mode 100644
index 0000000..5c696d4
--- /dev/null
+++ b/trunk/php/test/common/CacheFileTest.php
@@ -0,0 +1,168 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\RequestTime;
+use apache\shindig\common\Cache;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockRequestTime extends RequestTime {
+  private $time = 0;
+
+  public function getRequestTime() {
+    return $this->time;
+  }
+
+  public function sleep($second) {
+    $this->time += $second;
+  }
+}
+
+/**
+ * CacheFile test case.
+ */
+class CacheFileTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var Cache
+   */
+  private $cache;
+
+  /**
+   * @var MockRequestTime
+   */
+  private $time;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->time = new MockRequestTime();
+    $this->cache = Cache::createCache('apache\shindig\common\sample\CacheStorageFile', 'TestCache', $this->time);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->cache = null;
+    $this->time = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Cache::createCache()
+   */
+  public function testCreateCache() {
+    $cache = Cache::createCache('apache\shindig\common\sample\CacheStorageFile', 'TestCache');
+  }
+
+  /**
+   * Tests cache->delete()
+   */
+  public function testDelete() {
+    @rmdir(sys_get_temp_dir() . "/shindig/TestCache/te");
+    $this->cache->set("test", "testing");
+    $this->assertTrue(false != $this->cache->get("test"));
+    $this->cache->delete("test");
+    $this->assertFalse($this->cache->get("test"));
+  }
+
+  /**
+   * Tests cache->delete()
+   */
+  public function testDeleteException() {
+    $this->setExpectedException("apache\shindig\common\CacheException");
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->get()
+   */
+  public function testGet() {
+    $this->cache->set("test", "testing");
+    $this->assertEquals("testing", $this->cache->get("test"));
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->expiredGet()
+   */
+  public function testExpiredGet() {
+    $this->cache->set("test", "testing", 1);
+    $this->time->sleep(100);
+    $this->assertFalse($this->cache->get("test"));
+    $expected = array("found" => true, "ttl" => 1,
+                      "valid" => true, "data" => "testing");
+    $output = $this->cache->expiredGet("test");
+    $expected["time"] = $output["time"];
+    $this->assertEquals($expected, $output);
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->set()
+   */
+  public function testSet() {
+    $this->cache->set("test", "testing");
+    $this->assertEquals("testing", $this->cache->get("test"));
+    $expected = array("found" => true,
+                      "valid" => true, "data" => "testing");
+    $output = $this->cache->expiredGet("test");
+    $expected["time"] = $output["time"];
+    $expected["ttl"] = $output["ttl"];
+    $this->assertEquals($expected, $output);
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->set()
+   */
+  public function testSetException() {
+    @rmdir(sys_get_temp_dir() . "/shindig/TestCache/te");
+    $this->assertTrue(touch(sys_get_temp_dir() . "/shindig/TestCache/te"));
+    $this->setExpectedException("apache\shindig\common\CacheException");
+    try {
+      $this->cache->set("test", "testing");
+    } catch (\Exception $e) {
+      $this->assertTrue(unlink(sys_get_temp_dir() . "/shindig/TestCache/te"));
+      throw $e;
+    }
+    unlink(sys_get_temp_dir() . "/shindig/TestCache/te");
+  }
+
+  /**
+   * Tests cache->invalidate()
+   */
+  public function testInvalidation() {
+    @unlink(sys_get_temp_dir() . "/shindig/TestCache/te/test");
+    @rmdir(sys_get_temp_dir() . "/shindig/TestCache/te");
+    $this->cache->set("test", "testing");
+    $this->cache->invalidate("test");
+    $this->assertEquals(false, $this->cache->get("test"));
+    $expected = array("found" => true,
+                      "valid" => false, "data" => "testing");
+    $output = $this->cache->expiredGet("test");
+    $expected["time"] = $output["time"];
+    $expected["ttl"] = $output["ttl"];
+    $this->assertEquals($expected, $output);
+    $this->cache->delete("test");
+  }
+}
diff --git a/trunk/php/test/common/CacheMemcacheTest.php b/trunk/php/test/common/CacheMemcacheTest.php
new file mode 100644
index 0000000..ac97eb8
--- /dev/null
+++ b/trunk/php/test/common/CacheMemcacheTest.php
@@ -0,0 +1,153 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\RequestTime;
+use apache\shindig\common\Cache;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockRequestTimeMc extends RequestTime {
+  private $time = 0;
+
+  public function getRequestTime() {
+    return $this->time;
+  }
+
+  public function sleep($second) {
+    $this->time += $second;
+  }
+}
+/**
+ * CacheMemcache test case.
+ */
+class CacheMemcacheTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var Cache
+   */
+  private $cache;
+
+  /**
+   * @var MockRequestTime
+   */
+  private $time;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    if (!extension_loaded('memcache')) {
+      $message = 'memcache requires the memcache extention';
+      $this->markTestSkipped($message);
+    }
+    parent::setUp();
+    $this->time = new MockRequestTimeMc();
+    try {
+      $this->cache = Cache::createCache('apache\shindig\common\sample\CacheStorageMemcache', 'TestCache', $this->time);
+    } catch (\Exception $e) {
+      $message = 'memcache server can not connect';
+      $this->markTestSkipped($message);
+    }
+    if (! is_resource($this->cache)) {
+      $message = 'memcache server can not connect';
+      $this->markTestSkipped($message);
+    }
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->cache = null;
+    $this->time = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests cache->delete()
+   */
+  public function testDelete() {
+    $this->cache->set("test", "testing");
+    $this->assertTrue(false != $this->cache->get("test"));
+    $this->cache->delete("test");
+    $this->assertFalse($this->cache->get("test"));
+  }
+
+  /**
+   * Tests cache->delete()
+   */
+  public function testDeleteException() {
+    $this->setExpectedException("CacheException");
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->get()
+   */
+  public function testGet() {
+    $this->cache->set("test", "testing");
+    $this->assertEquals("testing", $this->cache->get("test"));
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->get()
+   */
+  public function testExpiredGet() {
+    $this->cache->set("test", "testing", 1);
+    $this->time->sleep(100);
+    $this->assertFalse($this->cache->get("test"));
+    $expected = array("found" => true, "ttl" => 1,
+                      "valid" => true, "data" => "testing");
+    $output = $this->cache->expiredGet("test");
+    $expected["time"] = $output["time"];
+    $this->assertEquals($expected, $output);
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->set()
+   */
+  public function testSet() {
+    $this->cache->set("test", "testing");
+    $this->assertEquals("testing", $this->cache->get("test"));
+    $expected = array("found" => true,
+                      "valid" => true, "data" => "testing");
+    $output = $this->cache->expiredGet("test");
+    $expected["time"] = $output["time"];
+    $expected["ttl"] = $output["ttl"];
+    $this->assertEquals($expected, $output);
+    $this->cache->delete("test");
+  }
+
+  /**
+   * Tests cache->invalidate()
+   */
+  public function testInvalidation() {
+    $this->cache->set("test", "testing");
+    $this->cache->invalidate("test");
+    $this->assertEquals(false, $this->cache->get("test"));
+    $expected = array("found" => true,
+                      "valid" => false, "data" => "testing");
+    $output = $this->cache->expiredGet("test");
+    $expected["time"] = $output["time"];
+    $expected["ttl"] = $output["ttl"];
+    $this->assertEquals($expected, $output);
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/test/common/CryptoTest.php b/trunk/php/test/common/CryptoTest.php
new file mode 100644
index 0000000..1bc4a12
--- /dev/null
+++ b/trunk/php/test/common/CryptoTest.php
@@ -0,0 +1,80 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\sample\GeneralSecurityException;
+use apache\shindig\common\sample\Crypto;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Crypto test case.
+ */
+class CryptoTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * Tests Crypto::aes128cbcEncrypt()
+   */
+  public function testAes128() {
+    $string = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit';
+    $key = 'Aliquam erat volutpat';
+    $encrypted = Crypto::aes128cbcEncrypt($key, $string);
+    $decrypted = Crypto::aes128cbcDecrypt($key, $encrypted);
+    $this->assertEquals($decrypted, $string);
+  }
+
+  /**
+   * Tests Crypto::hmacSha1()
+   */
+  public function testHmacSha1() {
+    $string = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit';
+    $key = 'Aliquam erat volutpat';
+    $expected = '%16%E7%E0E%22%08%5C%2B48%85d%FE%DE%C7%3A%C3%0D%11c';
+    $hmac = urlencode(Crypto::hmacSha1($key, $string));
+    $this->assertEquals($expected, $hmac);
+  }
+
+  /**
+   * Tests Crypto::hmacSha1Verify()
+   */
+  public function testHmacSha1VerifyException() {
+    $string = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit';
+    $key = 'Aliquam erat volutpat';
+    $expected = 'foo';
+    $this->setExpectedException('apache\shindig\common\sample\GeneralSecurityException');
+    Crypto::hmacSha1Verify($key, $string, $expected);
+  }
+
+  /**
+   * Tests Crypto::hmacSha1Verify()
+   */
+  public function testHmacSha1Verify() {
+    $string = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit';
+    $key = 'Aliquam erat volutpat';
+    $expected = '%16%E7%E0E%22%08%5C%2B48%85d%FE%DE%C7%3A%C3%0D%11c';
+    try {
+      Crypto::hmacSha1Verify($key, $string, urldecode($expected));
+      $success = true;
+    } catch (GeneralSecurityException $e) {
+      $success = false;
+    }
+    $this->assertTrue($success);
+  }
+
+}
+
diff --git a/trunk/php/test/common/HttpServletTest.php b/trunk/php/test/common/HttpServletTest.php
new file mode 100644
index 0000000..3390a58
--- /dev/null
+++ b/trunk/php/test/common/HttpServletTest.php
@@ -0,0 +1,124 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\HttpServlet;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * HttpServlet test case.
+ */
+class HttpServletTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var HttpServlet
+   */
+  private $HttpServlet;
+  
+  private $cacheTime = 60;
+  private $contentType1 = "text/html";
+  private $contentType2 = "text/javascript";
+  private $lastModified = 500;
+  private $noCache = false;
+  public $contentType = 'utf-8';
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->HttpServlet = new HttpServlet(/* parameters */);
+    
+    $this->HttpServlet->setLastModified($this->lastModified);
+    $this->HttpServlet->setNoCache($this->noCache);
+    $this->HttpServlet->setContentType($this->contentType);
+    $this->HttpServlet->setCacheTime($this->cacheTime);
+    $this->HttpServlet->noHeaders = true;
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  
+  protected function tearDown() {
+    $this->HttpServlet = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests HttpServlet->getCacheTime()
+   */
+  public function testGetCacheTime() {
+    $this->assertEquals($this->cacheTime, $this->HttpServlet->getCacheTime());
+  }
+
+  /**
+   * Tests HttpServlet->getContentType()
+   */
+  public function testGetContentType() {
+    $this->assertEquals($this->contentType, $this->HttpServlet->getContentType());
+  }
+
+  /**
+   * Tests HttpServlet->getLastModified()
+   */
+  public function testGetLastModified() {
+    $this->assertEquals($this->lastModified, $this->HttpServlet->getLastModified());
+  }
+
+  /**
+   * Tests HttpServlet->getNoCache()
+   */
+  public function testGetNoCache() {
+    $this->assertEquals($this->noCache, $this->HttpServlet->getNoCache());
+  }
+
+  /**
+   * Tests HttpServlet->setCacheTime()
+   */
+  public function testSetCacheTime() {
+    $this->HttpServlet->setCacheTime($this->cacheTime + 100);
+    $this->assertEquals($this->cacheTime + 100, $this->HttpServlet->getCacheTime());
+  }
+
+  /**
+   * Tests HttpServlet->setContentType()
+   */
+  public function testSetContentType() {
+    $this->HttpServlet->setContentType($this->contentType2);
+    $this->assertNotEquals($this->contentType1, $this->HttpServlet->getContentType());
+  }
+
+  /**
+   * Tests HttpServlet->setLastModified()
+   */
+  public function testSetLastModified() {
+    $this->HttpServlet->setLastModified($this->lastModified + 100);
+    $this->assertEquals($this->lastModified + 100, $this->HttpServlet->getLastModified());
+  }
+
+  /**
+   * Tests HttpServlet->setNoCache()
+   */
+  public function testSetNoCache() {
+    $this->HttpServlet->setNoCache(! $this->noCache);
+    $this->assertNotEquals($this->noCache, $this->HttpServlet->getNoCache());
+  }
+
+}
diff --git a/trunk/php/test/common/LocaleTest.php b/trunk/php/test/common/LocaleTest.php
new file mode 100644
index 0000000..f82eeb4
--- /dev/null
+++ b/trunk/php/test/common/LocaleTest.php
@@ -0,0 +1,83 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\Locale;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Locale test case.
+ */
+class LocaleTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Locale
+   */
+  private $Locale;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Locale = new Locale('EN', 'US');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Locale = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Constructs the test case.
+   */
+  public function __construct() {}
+
+  /**
+   * Tests Locale->__construct()
+   */
+  public function test__construct() {
+    $this->Locale->__construct('EN', 'US');
+  }
+
+  /**
+   * Tests Locale->equals()
+   */
+  public function testEquals() {
+    $locale = new Locale('EN', 'US');
+    $this->assertTrue($this->Locale->equals($locale));
+  }
+
+  /**
+   * Tests Locale->getCountry()
+   */
+  public function testGetCountry() {
+    $this->assertEquals('US', $this->Locale->getCountry());
+  }
+
+  /**
+   * Tests Locale->getLanguage()
+   */
+  public function testGetLanguage() {
+    $this->assertEquals('EN', $this->Locale->getLanguage());
+  }
+}
diff --git a/trunk/php/test/common/OpenSocialVersionTest.php b/trunk/php/test/common/OpenSocialVersionTest.php
new file mode 100644
index 0000000..9d6c7de
--- /dev/null
+++ b/trunk/php/test/common/OpenSocialVersionTest.php
@@ -0,0 +1,83 @@
+<?php
+namespace apache\shindig\test\common;
+use apache\shindig\common\OpenSocialVersion;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * OpenSocialVersion test case.
+ */
+class OpenSocialVersionTest extends \PHPUnit_Framework_TestCase {
+    
+    public function testTwoVersionsAreEqual() {
+        $version1 = new OpenSocialVersion('1.1.0');
+        $version2 = new OpenSocialVersion('1.1.0');
+        
+        $this->assertTrue($version1->isEquivalent($version2));
+    }
+    
+    public function testToString() {
+        $this->assertEquals('1.2.3', (string) (new OpenSocialVersion('1.2.3')));
+    }
+    
+    public function testTwoVersionsAreNotEqual() {
+        $version1 = new OpenSocialVersion('1.1.0');
+        $version2 = new OpenSocialVersion('1.2.0');
+        $version3 = new OpenSocialVersion('2.1.0');
+        $version4 = new OpenSocialVersion('1.1.2');
+        
+        $this->assertFalse($version1->isEquivalent($version2));
+       // $this->assertFalse($version1->isEquivalent($version3));
+       // $this->assertFalse($version1->isEquivalent($version4));
+    }
+    
+    public function testVersionIsEqualOrGreater() {
+        $version1 = new OpenSocialVersion('1.1.0');
+        $version2 = new OpenSocialVersion('1.1.0');
+        $version3 = new OpenSocialVersion('1.1.1');
+        $version4 = new OpenSocialVersion('1.2.0');
+        $version5 = new OpenSocialVersion('2.2.0');
+        
+        $this->assertTrue($version2->isEqualOrGreaterThan($version1));
+        $this->assertTrue($version3->isEqualOrGreaterThan($version1));
+        $this->assertTrue($version4->isEqualOrGreaterThan($version1));
+        $this->assertTrue($version5->isEqualOrGreaterThan($version1));
+    }
+    
+    public function testVersionIsNotEqualOrGreater() {
+        $version1 = new OpenSocialVersion('1.1.1');
+        $version2 = new OpenSocialVersion('1.0.9');
+        $version3 = new OpenSocialVersion('1.1.0');
+        $version4 = new OpenSocialVersion('0.2.0');
+        $version5 = new OpenSocialVersion('0.9.9');
+        
+        $this->assertFalse($version2->isEqualOrGreaterThan($version1));
+        $this->assertFalse($version3->isEqualOrGreaterThan($version1));
+        $this->assertFalse($version4->isEqualOrGreaterThan($version1));
+        $this->assertFalse($version5->isEqualOrGreaterThan($version1));
+    }
+    
+    public function testEmptyOpenSocialVersion() {
+        $version1 = new OpenSocialVersion('2.0.0');
+        $version2 = new OpenSocialVersion();
+        
+        $this->assertTrue($version2->isEqualOrGreaterThan($version1));
+    }
+}
\ No newline at end of file
diff --git a/trunk/php/test/config/phpunit_to_surefire.xslt b/trunk/php/test/config/phpunit_to_surefire.xslt
new file mode 100644
index 0000000..40e5b73
--- /dev/null
+++ b/trunk/php/test/config/phpunit_to_surefire.xslt
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+ -->
+
+<xsl:stylesheet version="2.0"
+	xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema"
+	xmlns:fn="http://www.w3.org/2005/xpath-functions">
+	<xsl:output method="xml" version="1.0" encoding="UTF-8"
+		indent="yes" />
+	<xsl:param name="outputDir">.</xsl:param>
+
+	<xsl:template match="testsuites">
+		<xsl:apply-templates select="testsuite" />
+	</xsl:template>
+
+	<xsl:template match="testsuite">
+		<xsl:if test="testcase">
+			<xsl:variable name="outputName" select="./@name" />
+			<xsl:result-document href="{$outputDir}/TEST-{$outputName}.xml" method="xml">
+				<xsl:copy-of select="." />
+			</xsl:result-document>
+		</xsl:if>
+
+		<xsl:apply-templates select="testsuite" />
+	</xsl:template>
+</xsl:stylesheet>
diff --git a/trunk/php/test/gadgets/BasicGadgetBlacklistTest.php b/trunk/php/test/gadgets/BasicGadgetBlacklistTest.php
new file mode 100644
index 0000000..6b40888
--- /dev/null
+++ b/trunk/php/test/gadgets/BasicGadgetBlacklistTest.php
@@ -0,0 +1,62 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\sample\BasicGadgetBlacklist;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * BasicGadgetBlacklist test case.
+ */
+class BasicGadgetBlacklistTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var BasicGadgetBlacklist
+   */
+  private $BasicGadgetBlacklist;
+  
+  private $tmpFile;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    // sys_get_temp_dir() requires php >= 5.2.1
+    $this->tmpFile = tempnam(sys_get_temp_dir(), 'test-blacklist-');
+    file_put_contents($this->tmpFile, "/www/i\n");
+    $this->BasicGadgetBlacklist = new BasicGadgetBlacklist($this->tmpFile);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    @unlink($this->tmpFile);
+    $this->BasicGadgetBlacklist = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests BasicGadgetBlacklist->isBlacklisted()
+   */
+  public function testIsBlacklisted() {
+    $this->assertTrue($this->BasicGadgetBlacklist->isBlacklisted('http://www.foo.com/bar.xml'));
+  }
+}
diff --git a/trunk/php/test/gadgets/ContainerConfigTest.php b/trunk/php/test/gadgets/ContainerConfigTest.php
new file mode 100644
index 0000000..33fac1c
--- /dev/null
+++ b/trunk/php/test/gadgets/ContainerConfigTest.php
@@ -0,0 +1,85 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\ContainerConfig;
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * ContainerConfig test case.
+ */
+class ContainerConfigTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+   }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    parent::tearDown();
+  }
+
+  /**
+   * Tests ContainerConfig->getConfig()
+   */
+  public function testGetConfig() {
+    $containerConfig = new ContainerConfig(Config::get('container_path'));
+    $config = $containerConfig->getConfig('default', 'gadgets.features');
+    $this->assertArrayHasKey('core.io', $config);
+    $this->assertArrayHasKey('views', $config);
+    $this->assertArrayHasKey('rpc', $config);
+    $this->assertArrayHasKey('skins', $config);
+    $this->assertArrayHasKey('opensocial', $config);
+    $this->assertArrayHasKey('path', $config['opensocial']);
+  }
+  
+  /**
+   * Tests ContainerConfig::removeComments()
+   */
+  public function testRemoveComments() {
+    $jsFile = <<<EOD
+/*
+ * Comments
+ */
+
+// Comments
+{"gadgets.container" : ["default"],
+"gadgets.parent" : null,
+"gadgets.uri.iframe.lockedDomainSuffix" : "-a.example.com:8080",
+"gadgets.iframeBaseUri" : "/gadgets/ifr",
+"gadgets.uri.oauth.callbackTemplate" : "//%host%/gadgets/oauthcallback"
+}
+EOD;
+    $containerConfig = new ContainerConfig(Config::get('container_path'));
+    $uncommented = $containerConfig->removeComments($jsFile);
+    $jsonObj = json_decode($uncommented, true);
+    $this->assertNotEquals($uncommented, $jsonObj);
+    $this->assertEquals(array("default"), $jsonObj["gadgets.container"]);
+    $this->assertEquals(null, $jsonObj["gadgets.parent"]);
+    $this->assertEquals("-a.example.com:8080", $jsonObj["gadgets.uri.iframe.lockedDomainSuffix"]);
+    $this->assertEquals("/gadgets/ifr", $jsonObj["gadgets.iframeBaseUri"]);
+    $this->assertEquals("//%host%/gadgets/oauthcallback", $jsonObj["gadgets.uri.oauth.callbackTemplate"]);
+  }
+}
diff --git a/trunk/php/test/gadgets/DataPipeliningTest.php b/trunk/php/test/gadgets/DataPipeliningTest.php
new file mode 100644
index 0000000..851f90c
--- /dev/null
+++ b/trunk/php/test/gadgets/DataPipeliningTest.php
@@ -0,0 +1,109 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\templates\DataPipelining;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * ContainerConfig test case.
+ */
+class DataPipeliningTest extends \PHPUnit_Framework_TestCase {
+  /**
+   * @var Gadget
+   */
+  private $viewNode = '<?xml version="1.0" encoding="UTF-8" ?>
+    <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-data">
+        <os:PeopleRequest key="viewer" userId="@viewer" groupId="@self"/>
+        <os:PeopleRequest key="viewerFriends" userId="@viewer" groupId="@friends" foo="bar"/>
+        <os:ViewerRequest />
+        <os:OwnerRequest />
+        <os:PersonAppDataRequest key="appdata" userId="@viewer" fields="field" />
+        <os:PersonAppDataRequest key="appdataFriends" userId="@viewer" groupId="@friends" fields="field" />
+        <os:ActivitiesRequest />
+        <os:HttpRequest href="http://example.com" />
+    </script>
+    ';
+
+  public function testParse() {
+      $doc = new \DomDocument();
+      $doc->loadXml($this->viewNode);
+      $contentBlocks = $doc->getElementsByTagName('script');
+      $tags = array();
+      foreach ($contentBlocks as $content) {
+        $tags[] = DataPipelining::parse($content);
+      }
+      $this->assertEquals(1, count($tags));
+
+      $expected = array(
+          array(
+              'type' => 'os:DataRequest',
+              'key' => 'viewer',
+              'userId' => '@viewer',
+              'groupId' => '@self',
+              'method' => 'people.get',
+          ),
+          array(
+              'type' => 'os:DataRequest',
+              'key' => 'viewerFriends',
+              'userId' => '@viewer',
+              'groupId' => '@friends',
+              'method' => 'people.get',
+          ),
+          array(
+              'type' => 'os:DataRequest',
+              'method' => 'people.get',
+              'userId' => '@viewer',
+              'groupId' => '@self',
+          ),
+          array(
+              'type' => 'os:DataRequest',
+              'method' => 'people.get',
+              'userId' => '@owner',
+              'groupId' => '@self',
+          ),
+          array(
+              'type' => 'os:DataRequest',
+              'key' => 'appdata',
+              'userId' => '@viewer',
+              'fields' => 'field',
+              'method' => 'appdata.get',
+          ),
+          array(
+              'type' => 'os:DataRequest',
+              'key' => 'appdataFriends',
+              'userId' => '@viewer',
+              'groupId' => '@friends',
+              'fields' => 'field',
+              'method' => 'appdata.get',
+          ),
+          array(
+              'type' => 'os:DataRequest',
+              'method' => 'activities.get',
+          ),
+          array(
+              'type' => 'os:HttpRequest',
+              'href' => 'http://example.com',
+          ),
+      );
+
+      $this->assertEquals($expected, $tags[0]);
+      
+  }
+}
diff --git a/trunk/php/test/gadgets/ExpTypeTest.php b/trunk/php/test/gadgets/ExpTypeTest.php
new file mode 100644
index 0000000..16e1470
--- /dev/null
+++ b/trunk/php/test/gadgets/ExpTypeTest.php
@@ -0,0 +1,107 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\templates\Token;
+use apache\shindig\gadgets\templates\ExpType;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * ExpType test case.
+ */
+class ExpTypeTest extends \PHPUnit_Framework_TestCase {
+
+  private $tokens;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    ExpType::$RAW;  // dummy here, for loading ExpType.php
+    $int = new Token(ExpType::$INT, 1);
+    $float = new Token(ExpType::$FLOAT, 1.0);
+    $string = new Token(ExpType::$STRING, 'Jacky Wang');
+    $bool = new Token(ExpType::$BOOL, true);
+    $null = new Token(ExpType::$NULL, null);
+    $array = new Token(ExpType::$ARRAY, array());
+    $object = new Token(ExpType::$OBJECT, (object)("it's object"));
+    $this->tokens = array('int' => $int, 'float' => $float, 'string' => $string, 'bool' => $bool, 'null' => $null, 'array' => $array, 'object' => $object);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    parent::tearDown();
+  }
+  
+  /**
+   * Tests ExpType::detectType
+   */
+  public function testDetectType() {
+    foreach ($this->tokens as $token) {
+      $this->assertEquals($token->type, ExpType::detectType($token->value));
+    }
+  }
+
+  /**
+   * Tests ExpType::coerce
+   */
+  public function testCoerce() {
+    // coerce number
+    $this->assertEquals($this->tokens['int'], ExpType::coerceToNumber($this->tokens['int']));
+    $this->assertEquals($this->tokens['float'], ExpType::coerceToNumber($this->tokens['float']));
+    $this->assertEquals(new Token(ExpType::$INT, 2), ExpType::coerceToNumber(new Token(ExpType::$RAW, '2')));
+    $this->assertEquals(new Token(ExpType::$INT, 2), ExpType::coerceToNumber(new Token(ExpType::$STRING, '2')));
+    $this->assertEquals(new Token(ExpType::$INT, 1), ExpType::coerceToNumber(new Token(ExpType::$BOOL, true)));
+    $this->assertEquals(new Token(ExpType::$INT, 0), ExpType::coerceToNumber(new Token(ExpType::$BOOL, false)));
+    $this->assertEquals(new Token(ExpType::$INT, 0), ExpType::coerceToNumber(new Token(ExpType::$NULL, null)));
+    $this->assertEquals(new Token(ExpType::$FLOAT, 1.0), ExpType::coerceToNumber(new Token(ExpType::$RAW, '1.0')));
+    $this->assertEquals(new Token(ExpType::$FLOAT, 1.0), ExpType::coerceToNumber(new Token(ExpType::$STRING, '1.0')));
+    
+    // coerce string
+    $this->assertEquals($this->tokens['string'], ExpType::coerceToString($this->tokens['string']));
+    $this->assertEquals(new Token(ExpType::$STRING, '2'), ExpType::coerceToString(new Token(ExpType::$RAW, '2')));
+    $this->assertEquals(new Token(ExpType::$STRING, '2'), ExpType::coerceToString(new Token(ExpType::$INT, 2)));
+    $this->assertEquals(new Token(ExpType::$STRING, '2'), ExpType::coerceToString(new Token(ExpType::$FLOAT, 2.0)));
+    $this->assertEquals(new Token(ExpType::$STRING, '2.5'), ExpType::coerceToString(new Token(ExpType::$FLOAT, 2.5)));
+    $this->assertEquals(new Token(ExpType::$STRING, 'true'), ExpType::coerceToString(new Token(ExpType::$BOOL, true)));
+    $this->assertEquals(new Token(ExpType::$STRING, 'false'), ExpType::coerceToString(new Token(ExpType::$BOOL, false)));
+    $this->assertEquals(new Token(ExpType::$STRING, 'null'), ExpType::coerceToString(new Token(ExpType::$NULL, null)));
+    
+    // coerce bool
+    $this->assertEquals($this->tokens['bool'], ExpType::coerceToBool($this->tokens['bool']));
+    $this->assertEquals(new Token(ExpType::$BOOL, true), ExpType::coerceToBool(new Token(ExpType::$RAW, 'True')));
+    $this->assertEquals(new Token(ExpType::$BOOL, true), ExpType::coerceToBool(new Token(ExpType::$RAW, 'true')));
+    $this->assertEquals(new Token(ExpType::$BOOL, false), ExpType::coerceToBool(new Token(ExpType::$RAW, 'False')));
+    $this->assertEquals(new Token(ExpType::$BOOL, false), ExpType::coerceToBool(new Token(ExpType::$RAW, 'false')));
+    $this->assertEquals(new Token(ExpType::$BOOL, true), ExpType::coerceToBool(new Token(ExpType::$STRING, 'false')));
+    $this->assertEquals(new Token(ExpType::$BOOL, false), ExpType::coerceToBool(new Token(ExpType::$STRING, '')));
+    $this->assertEquals(new Token(ExpType::$BOOL, true), ExpType::coerceToBool(new Token(ExpType::$INT, 2)));
+    $this->assertEquals(new Token(ExpType::$BOOL, false), ExpType::coerceToBool(new Token(ExpType::$INT, 0)));
+    $this->assertEquals(new Token(ExpType::$BOOL, true), ExpType::coerceToBool(new Token(ExpType::$FLOAT, 2.0)));
+    $this->assertEquals(new Token(ExpType::$BOOL, false), ExpType::coerceToBool(new Token(ExpType::$FLOAT, 0.0)));
+    $this->assertEquals(new Token(ExpType::$BOOL, false), ExpType::coerceToBool(new Token(ExpType::$NULL, null)));
+    
+    // coerce null
+    $this->assertEquals($this->tokens['null'], ExpType::coerceToNull($this->tokens['null']));
+    $this->assertEquals($this->tokens['null'], ExpType::coerceToNull(new Token(ExpType::$RAW, 'null')));
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/test/gadgets/ExpressionParserTest.php b/trunk/php/test/gadgets/ExpressionParserTest.php
new file mode 100644
index 0000000..552b8ef
--- /dev/null
+++ b/trunk/php/test/gadgets/ExpressionParserTest.php
@@ -0,0 +1,184 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\templates\Token;
+use apache\shindig\gadgets\templates\ExpType;
+use apache\shindig\gadgets\templates\ExpLexer;
+use apache\shindig\gadgets\templates\ExpParser;
+use apache\shindig\gadgets\templates\ExpressionParser;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class ObjEe {
+  public $Ee;
+}
+
+/**
+ * ExpressionParser test case.
+ */
+class ExpressionParserTest extends \PHPUnit_Framework_TestCase {
+
+  private $input;
+  private $tokenStream;
+  private $dataContext;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $encoded_json = base64_encode('{"array_attr": [0, 1]}');
+    $attr_equ = "no_prefix_id.Ee+---((2-1)*(4.0-3)-1.0/2+5e-1)>-.5e+1+4 or not empty cur_id['empty_str']&&!top_id.null_attr";
+    $this->input = 'osx:parseJson(osx:urlDecode(osx:urlEncode(osx:decodeBase64("' . $encoded_json . '"))))'
+        . '.array_attr['
+        . $attr_equ . "?0:1"
+        . "]==1?(true?'no_prefix_id.Ee <= 0':"
+        . '"\'should never be here\'	\\\\\\""'
+        . "):'no_prefix_id.Ee > 0'";
+    
+    $this->tokenStream = array(
+        new Token(ExpType::$FUNCTION, 'osx:parseJson'),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$FUNCTION, 'osx:urlDecode'),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$FUNCTION, 'osx:urlEncode'),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$FUNCTION, 'osx:decodeBase64'),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$STRING, 'eyJhcnJheV9hdHRyIjogWzAsIDFdfQ=='),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$DOT, '.'),
+        new Token(ExpType::$IDENTITY, 'array_attr'),
+        new Token(ExpType::$PAREN, '['),
+        new Token(ExpType::$IDENTITY, 'no_prefix_id'),
+        new Token(ExpType::$DOT, '.'),
+        new Token(ExpType::$IDENTITY, 'Ee'),
+        new Token(ExpType::$BINARY_OP, '+'),
+        new Token(ExpType::$UNARY_OP, ' -'),
+        new Token(ExpType::$UNARY_OP, ' -'),
+        new Token(ExpType::$UNARY_OP, ' -'),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$INT, 2),
+        new Token(ExpType::$BINARY_OP, ' - '),
+        new Token(ExpType::$INT, 1),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$BINARY_OP, '*'),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$FLOAT, 4.0),
+        new Token(ExpType::$BINARY_OP, ' - '),
+        new Token(ExpType::$INT, 3),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$BINARY_OP, ' - '),
+        new Token(ExpType::$FLOAT, 1.0),
+        new Token(ExpType::$BINARY_OP, '/'),
+        new Token(ExpType::$INT, 2),
+        new Token(ExpType::$BINARY_OP, '+'),
+        new Token(ExpType::$FLOAT, 0.5),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$BINARY_OP, '>'),
+        new Token(ExpType::$UNARY_OP, ' -'),
+        new Token(ExpType::$FLOAT, 5.0),
+        new Token(ExpType::$BINARY_OP, '+'),
+        new Token(ExpType::$INT, 4),
+        new Token(ExpType::$BINARY_OP, '||'),
+        new Token(ExpType::$UNARY_OP, '!'),
+        new Token(ExpType::$UNARY_OP, 'empty'),
+        new Token(ExpType::$IDENTITY, 'cur_id'),
+        new Token(ExpType::$PAREN, '['),
+        new Token(ExpType::$STRING, 'empty_str'),
+        new Token(ExpType::$PAREN, ']'),
+        new Token(ExpType::$BINARY_OP, '&&'),
+        new Token(ExpType::$UNARY_OP, '!'),
+        new Token(ExpType::$IDENTITY, 'top_id'),
+        new Token(ExpType::$DOT, '.'),
+        new Token(ExpType::$IDENTITY, 'null_attr'),
+        new Token(ExpType::$TERNARY, '?'),
+        new Token(ExpType::$INT, 0),
+        new Token(ExpType::$TERNARY, ':'),
+        new Token(ExpType::$INT, 1),
+        new Token(ExpType::$PAREN, ']'),
+        new Token(ExpType::$BINARY_OP, '=='),
+        new Token(ExpType::$INT, 1),
+        new Token(ExpType::$TERNARY, '?'),
+        new Token(ExpType::$PAREN, '('),
+        new Token(ExpType::$BOOL, true),
+        new Token(ExpType::$TERNARY, '?'),
+        new Token(ExpType::$STRING, 'no_prefix_id.Ee <= 0'),
+        new Token(ExpType::$TERNARY, ':'),
+        new Token(ExpType::$STRING, '\'should never be here\'	\\"'),
+        new Token(ExpType::$PAREN, ')'),
+        new Token(ExpType::$TERNARY, ':'),
+        new Token(ExpType::$STRING, 'no_prefix_id.Ee > 0')
+    );
+    
+    $no_prefix_id = new ObjEe();
+    $no_prefix_id->Ee = 1;  // change this number to see the difference
+    $cur_id = array('empty_str' => '');
+    $top_id = (object)'empty_object';
+    $this->dataContext = array(
+        'no_prefix_id' => $no_prefix_id,
+        'Cur' => array('cur_id' => $cur_id),
+        'My' => array(),
+        'Top' => array('top_id' => $top_id)
+    );
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    parent::tearDown();
+  }
+  
+  /**
+   * Tests ExpLexer::process
+   */
+  public function testProcess() {
+    $actualTokenStream = ExpLexer::process($this->input);
+    $this->assertEquals($this->tokenStream, $actualTokenStream);
+  }
+
+  /**
+   * Tests ExpParser::parse
+   */
+  public function testParse() {
+    $this->dataContext['no_prefix_id']->Ee = 1;
+    $actualResult = ExpParser::parse($this->tokenStream, $this->dataContext);
+    $this->assertEquals(new Token(ExpType::$STRING, 'no_prefix_id.Ee > 0'), $actualResult);
+    
+    $this->dataContext['no_prefix_id']->Ee = -1;
+    $actualResult = ExpParser::parse($this->tokenStream, $this->dataContext);
+    $this->assertEquals(new Token(ExpType::$STRING, 'no_prefix_id.Ee <= 0'), $actualResult);
+  }
+
+  /**
+   * Tests ExpressionParser::evaluate
+   */
+  public function testEvaluate() {
+    $actualOutput = ExpressionParser::evaluate($this->input, $this->dataContext);
+    
+    // Expected result
+    $this->assertEquals('no_prefix_id.Ee > 0', $actualOutput);
+  }
+
+}
diff --git a/trunk/php/test/gadgets/FilesServletTest.php b/trunk/php/test/gadgets/FilesServletTest.php
new file mode 100644
index 0000000..085c83f
--- /dev/null
+++ b/trunk/php/test/gadgets/FilesServletTest.php
@@ -0,0 +1,108 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\servlet\ResourcesFilesServlet;
+use apache\shindig\gadgets\servlet\ContentFilesServlet;
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockResourcesFilesServlet extends ResourcesFilesServlet
+{
+    public $noHeaders = true;
+    public $uri;
+
+    protected function getRequestUri() {
+      return $this->uri;
+    }
+}
+
+class MockContentFilesServlet extends ContentFilesServlet
+{
+    public $noHeaders = true;
+    public $uri;
+
+    protected function getRequestUri() {
+      return $this->uri;
+    }
+}
+
+class FilesServletTest extends \PHPUnit_Framework_TestCase
+{
+    public function testResources() {
+        $servlet = new MockResourcesFilesServlet();
+        $servlet->uri = 'com/google/caja/plugin/domita-minified.js';
+        ob_start();
+        $servlet->doGet();
+        $servletContent = ob_get_clean();
+        $fileContent = file_get_contents(Config::get('resources_path') . $servlet->uri);
+        $this->assertEquals($fileContent, $servletContent);
+    }
+
+    public function testContentHtml() {
+        $servlet = new MockContentFilesServlet();
+        $servlet->uri = 'container/rpc_relay.html';
+        ob_start();
+        $servlet->doGet();
+        $servletContent = ob_get_clean();
+        $fileContent = file_get_contents(Config::get('javascript_path') . $servlet->uri);
+        $this->assertEquals($fileContent, $servletContent);
+    }
+
+    public function testContentCss() {
+        $servlet = new MockContentFilesServlet();
+        $servlet->uri = 'container/gadgets.css';
+        ob_start();
+        $servlet->doGet();
+        $servletContent = ob_get_clean();
+        $fileContent = file_get_contents(Config::get('javascript_path') . $servlet->uri);
+        $this->assertEquals($fileContent, $servletContent);
+    }
+
+    public function testContentFlash() {
+        $servlet = new MockContentFilesServlet();
+        $servlet->uri = 'container/Bridge.swf';
+        ob_start();
+        $servlet->doGet();
+        $servletContent = ob_get_clean();
+        $fileContent = file_get_contents(Config::get('javascript_path') . $servlet->uri);
+        $this->assertEquals($fileContent, $servletContent);
+    }
+
+    public function testContentGif() {
+        $servlet = new MockContentFilesServlet();
+        $servlet->uri = 'images/new.gif';
+        ob_start();
+        $servlet->doGet();
+        $servletContent = ob_get_clean();
+        $fileContent = file_get_contents(Config::get('javascript_path') . $servlet->uri);
+        $this->assertEquals($fileContent, $servletContent);
+    }
+
+    public function testContentPng() {
+        $servlet = new MockContentFilesServlet();
+        $servlet->uri = 'images/icon.png';
+        ob_start();
+        $servlet->doGet();
+        $servletContent = ob_get_clean();
+        $fileContent = file_get_contents(Config::get('javascript_path') . $servlet->uri);
+        $this->assertEquals($fileContent, $servletContent);
+    }
+}
+
diff --git a/trunk/php/test/gadgets/GadgetContextTest.php b/trunk/php/test/gadgets/GadgetContextTest.php
new file mode 100644
index 0000000..61c2ff9
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetContextTest.php
@@ -0,0 +1,202 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetContext;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * GadgetContext test case.
+ */
+class GadgetContextTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var GadgetContext
+   */
+  private $GadgetContext;
+  
+  /**
+   * @var testData
+   */
+  private $testData = array('url' => 'http://www.google.com/gadget', 'libs' => '', 'synd' => 'default', 
+      'nocache' => '', 'rawxml' => '<foo></foo>', 'container' => 'default', 'view' => 'default', 'mid' => '123',
+      'bcp' => '');
+  
+  /**
+   * @var gadgetRenderingContext
+   */
+  private $gadgetRenderingContext = 'GADGET';
+
+  private $orgGet;
+  private $orgPost;
+  private $orgServer;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->orgGet = $_GET;
+    $this->orgPost = $_POST;
+    $this->orgServer = $_SERVER;
+    
+    $_GET = $this->testData;
+    
+    $_SERVER['HTTP_HOST'] = 'localhost';
+    
+    $this->GadgetContext = new GadgetContext($this->gadgetRenderingContext);
+  
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->GadgetContext = null;
+
+    $_GET = $this->orgGet;
+    $_POST = $this->orgPost;
+    $_SERVER = $this->orgServer;
+    
+    unset($_SERVER['HTTP_HOST']);
+    
+    parent::tearDown();
+  }
+
+  /**
+   * Tests GadgetContext->getBlacklist()
+   */
+  public function testGetBlacklist() {
+    $this->assertTrue(is_object($this->GadgetContext->getBlackList()));
+  
+  }
+
+  /**
+   * Tests GadgetContext->getContainer()
+   */
+  public function testGetContainer() {
+    $this->assertEquals($this->testData['container'], $this->GadgetContext->getContainer());
+  
+  }
+
+  /**
+   * Tests GadgetContext->getForcedJsLibs()
+   */
+  public function testGetForcedJsLibs() {
+    $this->assertEquals($this->testData['libs'], $this->GadgetContext->getForcedJsLibs());
+  
+  }
+
+  /**
+   * Tests GadgetContext->getHttpFetcher()
+   */
+  public function testGetHttpFetcher() {
+    $this->assertNotNull($this->GadgetContext->getHttpFetcher());
+  
+  }
+
+  /**
+   * Tests GadgetContext->getLocale()
+   */
+  public function testGetLocale() {
+    $this->assertNotNull($this->GadgetContext->getLocale());
+  
+  }
+
+  /**
+   * Tests GadgetContext->getModuleId()
+   */
+  public function testGetModuleId() {
+    $this->assertEquals($this->testData['mid'], $this->GadgetContext->getModuleId());
+  
+  }
+
+  /**
+   * Tests GadgetContext->getRegistry()
+   */
+  public function testGetRegistry() {
+    $this->assertNotNull($this->GadgetContext->getRegistry());
+  
+  }
+
+  /**
+   * Tests GadgetContext->getRenderingContext()
+   */
+  public function testGetRenderingContext() {
+    $this->assertEquals($this->gadgetRenderingContext, $this->GadgetContext->getRenderingContext());
+  
+  }
+
+  /**
+   * Tests GadgetContext->getUrl()
+   */
+  public function testGetUrl() {
+    $this->assertEquals($this->testData['url'], $this->GadgetContext->getUrl());
+  
+  }
+
+  public function testGetRawXml() {
+    $this->assertEquals($this->testData['rawxml'], $this->GadgetContext->getRawXml());
+  }
+
+  /**
+   * Tests GadgetContext->getView()
+   */
+  public function testGetView() {
+    $this->assertEquals($this->testData['view'], $this->GadgetContext->getView());
+  
+  }
+
+  /**
+   * Tests GadgetContext->setRenderingContext()
+   */
+  public function testSetRenderingContext() {
+    $redering_context = 'Dummie_rendering_context';
+    $this->GadgetContext->setRenderingContext($redering_context);
+    $this->assertEquals($redering_context, $this->GadgetContext->getRenderingContext());
+  
+  }
+
+  /**
+   * Tests GadgetContext->setUrl()
+   */
+  public function testSetUrl() {
+    $url = 'Dummie_url';
+    $this->GadgetContext->setUrl($url);
+    $this->assertEquals($url, $this->GadgetContext->getUrl());
+  }
+
+  public function testSetRawXml() {
+    $xml = 'Dummie_xml';
+    $this->GadgetContext->setRawXml($xml);
+    $this->assertEquals($xml, $this->GadgetContext->getRawXml());
+  }
+
+  /**
+   * Tests GadgetContext->setView()
+   */
+  public function testSetView() {
+    $view = 'Dummie_view';
+    $this->GadgetContext->setView($view);
+    $this->assertEquals($view, $this->GadgetContext->getView());
+  
+  }
+
+}
diff --git a/trunk/php/test/gadgets/GadgetFactoryTest.php b/trunk/php/test/gadgets/GadgetFactoryTest.php
new file mode 100644
index 0000000..d773c58
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetFactoryTest.php
@@ -0,0 +1,246 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\GadgetFactory;
+use apache\shindig\common\sample\BasicSecurityToken;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetFactoryTest extends \PHPUnit_Framework_TestCase {
+    private $oldGet;
+    private $oldPost;
+    private $token;
+    public function setUp()
+    {
+        $_SERVER['HTTP_HOST'] = 'localhost';
+        $this->oldGet = $_GET;
+        $this->oldPost = $_POST;
+        $this->token = BasicSecurityToken::createFromValues(1, 1, 1, 'example.com', 'http://example.com/gadget', 1, 1);
+    }
+
+    public function tearDown()
+    {
+        unset($_SERVER['HTTP_HOST']);
+        $_GET = $this->oldGet;
+        $_POST = $this->oldPost;
+    }
+
+    public function testCreateGadgetFromRawXml()
+    {
+        $_GET = array(
+            'rawxml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Require feature="dynamic-height" />
+    <Require feature="flash" />
+    <Require feature="minimessage" />
+  </ModulePrefs>
+  <Content type="html" view="home">
+  <![CDATA[
+    <h1>Hello, world!</h1>
+  ]]>
+  </Content>
+</Module>'
+        );
+        $_POST = array();
+        $context = new GadgetContext('GADGET');
+        $gadgetFactory = new GadgetFactory($context, $this->token);
+        $gadget = $gadgetFactory->createGadget();
+
+        $this->assertEquals('title', $gadget->gadgetSpec->title);
+        $this->assertEquals('<h1>Hello, world!</h1>', trim($gadget->gadgetSpec->views['home']['content']));
+    }
+
+    public function testCreateGadgetFromRawXmlInPost()
+    {
+        $_POST = array(
+            'rawxml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Require feature="dynamic-height" />
+    <Require feature="flash" />
+    <Require feature="minimessage" />
+  </ModulePrefs>
+  <Content type="html" view="home">
+  <![CDATA[
+    <h1>Hello, world!</h1>
+  ]]>
+  </Content>
+</Module>'
+        );
+        $_GET = array();
+        $context = new GadgetContext('GADGET');
+        $gadgetFactory = new GadgetFactory($context, $this->token);
+        $gadget = $gadgetFactory->createGadget();
+
+        $this->assertEquals('title', $gadget->gadgetSpec->title);
+        $this->assertEquals('<h1>Hello, world!</h1>', trim($gadget->gadgetSpec->views['home']['content']));
+    }
+
+    public function testParseFeaturesDependentOnCurrentView() {
+        $_POST = array(
+            'rawxml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Require feature="pubsub" views="canvas" />
+    <Require feature="flash" views="canvas,profile" />
+    <Optional feature="minimessage" />
+    <Optional feature="invalid" />
+    <Optional feature="pubsub2" views="canvas" />
+    <Optional feature="opensocial-data" views="canvas, profile" />
+  </ModulePrefs>
+  <Content type="html" view="home">
+  </Content>
+</Module>'
+        );
+        $_GET = array();
+        $context = new GadgetContext('GADGET');
+        $context->setView('profile');
+        $gadgetFactory = new GadgetFactory($context, $this->token);
+        $gadget = $gadgetFactory->createGadget();
+
+        $this->assertTrue(in_array('flash', $gadget->features));
+        $this->assertTrue(in_array('minimessage', $gadget->features));
+        $this->assertTrue(in_array('opensocial-data', $gadget->features));
+
+        $this->assertFalse(in_array('pubsub', $gadget->features));
+        $this->assertFalse(in_array('pubsub2', $gadget->features));
+    }
+
+    public function testRequiringInvalidFeatureThrowsException() {
+        $this->setExpectedException('apache\shindig\gadgets\GadgetException', 'Unknown features: invalid');
+        $_POST = array(
+            'rawxml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Require feature="invalid" />
+  </ModulePrefs>
+  <Content type="html" view="home">
+  </Content>
+</Module>'
+        );
+        $_GET = array();
+        $context = new GadgetContext('GADGET');
+        $context->setView('profile');
+        $gadgetFactory = new GadgetFactory($context, $this->token);
+        $gadget = $gadgetFactory->createGadget();
+    }
+
+    public function testParsePreloadsDependentOnCurrentView() {
+        $_POST = array(
+            'rawxml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Preload href="http://www.example.com/one" />
+    <Preload href="http://www.example.com/two" views="canvas"/>
+    <Preload href="http://www.example.com/three" views="canvas,profile"/>
+  </ModulePrefs>
+  <Content type="html" view="home">
+  </Content>
+</Module>'
+        );
+        $_GET = array();
+        $context = new GadgetContext('GADGET');
+        $context->setView('profile');
+        $gadgetFactory = new TestGadgetFactory($context, $this->token);
+        $gadget = $gadgetFactory->createGadget();
+
+        $this->assertEquals('http://www.example.com/one', $gadget->gadgetSpec->preloads[0]['id']);
+        $this->assertEquals('http://www.example.com/three', $gadget->gadgetSpec->preloads[1]['id']);
+        $this->assertEquals(2, count($gadget->gadgetSpec->preloads));
+    }
+
+    public function testParseLocalsDependentOnCurrentView() {
+        $_POST = array(
+            'rawxml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+     <Locale messages="http://example.com/helloOne/en_ALL.xml"/>
+     <Locale messages="http://example.com/helloTwo/en_ALL.xml" views="canvas"/>
+     <Locale messages="http://example.com/helloThree/en_ALL.xml" views="canvas,profile"/>
+  </ModulePrefs>
+  <Content type="html" view="home">
+  </Content>
+</Module>'
+        );
+        $_GET = array();
+        $context = new GadgetContext('GADGET');
+        $context->setView('profile');
+        $gadgetFactory = new TestGadgetFactory($context, $this->token);
+        $gadget = $gadgetFactory->createGadget();
+        $this->assertEquals(array('greetingOne' => 'Hello', 'greetingThree' => 'Hello'), $gadget->gadgetSpec->locales);
+    }
+}
+
+class TestGadgetFactory extends GadgetFactory
+{
+    private $responses = array(
+        'http://www.example.com/one' => 'preloadOne',
+        'http://www.example.com/two' => 'preloadTwo',
+        'http://www.example.com/three' => 'preloadThree',
+        'http://example.com/helloOne/en_ALL.xml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<messagebundle>
+  <msg name="greetingOne">
+    Hello
+  </msg>
+</messagebundle>',
+        'http://example.com/helloTwo/en_ALL.xml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<messagebundle>
+  <msg name="greetingTwo">
+    Hello
+  </msg>
+</messagebundle>',
+        'http://example.com/helloThree/en_ALL.xml' => '<?xml version="1.0" encoding="UTF-8" ?>
+<messagebundle>
+  <msg name="greetingThree">
+    Hello
+  </msg>
+</messagebundle>',
+    );
+    /**
+     * mock request sending
+     *
+     * @param array $unsignedRequests
+     * @param array $signedRequests
+     * @return array
+     */
+    protected function performRequests($unsignedRequests, $signedRequests) {
+        // Perform the non-signed requests
+        $responses = array();
+        if (count($unsignedRequests)) {
+            foreach ($unsignedRequests as $request) {
+                $responses[$request->getUrl()] = array(
+                    'body' => $this->responses[$request->getUrl()],
+                    'rc' => 200);
+            }
+        }
+
+        // Perform the signed requests
+        if (count($signedRequests)) {
+            foreach ($signedRequests as $request) {
+                $responses[$request->getUrl()] = array(
+                    'body' => $this->responses[$request->getUrl()],
+                    'rc' => 200);
+            }
+        }
+
+        return $responses;
+    }
+}
\ No newline at end of file
diff --git a/trunk/php/test/gadgets/GadgetFeatureRegistryTest.php b/trunk/php/test/gadgets/GadgetFeatureRegistryTest.php
new file mode 100644
index 0000000..d8ed72b
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetFeatureRegistryTest.php
@@ -0,0 +1,251 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetFeatureRegistry;
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * GadgetFeatureRegistry test case.
+ */
+class GadgetFeatureRegistryTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var GadgetFeatureRegistry
+   */
+  private $GadgetFeatureRegistry;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    $_SERVER['HTTP_HOST'] = 'localhost';
+    parent::setUp();
+    $this->GadgetFeatureRegistry = new TestGadgetFeatureRegistry(Config::get('features_path'));
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    unset($_SERVER['HTTP_HOST']);
+    $this->GadgetFeatureRegistry = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests GadgetFeatureRegistry->__construct()
+   */
+  public function test__construct() {
+    $this->GadgetFeatureRegistry->__construct(Config::get('features_path'));
+  }
+
+  public function testParseFeatureFileWithContainerGadgetAndAll() {
+    $content = '<?xml version="1.0"?>
+<feature>
+  <name>featureName</name>
+  <dependency>dependency1</dependency>
+  <dependency>dependency2</dependency>
+  <gadget>
+    <script src="gadgetFile1.js"/>
+    <script src="gadgetFile2.js"/>
+    <script>alert(1);</script>
+    <script src="res://example.com/file.js" />
+  </gadget>
+  <container>
+    <script src="containerFile1.js"/>
+    <script src="containerFile2.js"/>
+    <script src="http://example.com/file.js" />
+  </container>
+  <all>
+    <script src="file1.js"/>
+    <script src="file2.js"/>
+    <script src="https://example.com/file.js" />
+  </all>
+</feature>';
+    $basePath = '/path';
+    $feature = $this->GadgetFeatureRegistry->_parse($content, $basePath);
+
+    $expected = array(
+        'deps' => array(
+            'dependency1' => 'dependency1',
+            'dependency2' => 'dependency2',
+        ),
+        'basePath' => '/path',
+        'name' => 'featureName',
+        'gadgetJs' => array(
+            array(
+                'type' => 'FILE',
+                'content' => 'gadgetFile1.js',
+            ),
+            array(
+                'type' => 'FILE',
+                'content' => 'gadgetFile2.js',
+            ),
+            array(
+                'type' => 'INLINE',
+                'content' => 'alert(1);',
+            ),
+            array(
+                'type' => 'URL',
+                'content' => 'http://localhost/gadgets/resources/example.com/file.js',
+            ),
+        ),
+        'containerJs' => array(
+            array(
+                'type' => 'FILE',
+                'content' => 'containerFile1.js',
+            ),
+            array(
+                'type' => 'FILE',
+                'content' => 'containerFile2.js',
+            ),
+            array(
+                'type' => 'URL',
+                'content' => 'http://example.com/file.js',
+            ),
+        )
+    );
+
+    $this->assertEquals($expected, $feature);
+  }
+
+  public function testParseFeatureFileWithContainerAndAllBlock() {
+    $content = '<?xml version="1.0"?>
+<feature>
+  <name>featureName</name>
+  <dependency>dependency1</dependency>
+  <dependency>dependency2</dependency>
+  <all>
+    <script src="file1.js"/>
+    <script src="file2.js"/>
+    <script src="https://example.com/file.js" />
+  </all>
+</feature>';
+    $basePath = '/path';
+    $feature = $this->GadgetFeatureRegistry->_parse($content, $basePath);
+
+    $expected = array(
+        'deps' => array(
+            'dependency1' => 'dependency1',
+            'dependency2' => 'dependency2',
+        ),
+        'basePath' => '/path',
+        'name' => 'featureName',
+        'gadgetJs' => array(
+            array(
+                'type' => 'FILE',
+                'content' => 'file1.js',
+            ),
+            array(
+                'type' => 'FILE',
+                'content' => 'file2.js',
+            ),
+            array(
+                'type' => 'URL',
+                'content' => 'https://example.com/file.js',
+            ),
+        ),
+        'containerJs' => array(
+            array(
+                'type' => 'FILE',
+                'content' => 'file1.js',
+            ),
+            array(
+                'type' => 'FILE',
+                'content' => 'file2.js',
+            ),
+            array(
+                'type' => 'URL',
+                'content' => 'https://example.com/file.js',
+            ),
+        )
+    );
+
+    $this->assertEquals($expected, $feature);
+  }
+
+  public function testParseFeatureFileWithAllBlock() {
+    $content = '<?xml version="1.0"?>
+<feature>
+  <name>featureName</name>
+  <dependency>dependency1</dependency>
+  <dependency>dependency2</dependency>
+  <container>
+    <script src="containerFile1.js"/>
+    <script src="containerFile2.js"/>
+    <script src="http://example.com/file.js" />
+  </container>
+  <all>
+    <script src="file1.js"/>
+    <script src="file2.js"/>
+    <script src="https://example.com/file.js" />
+  </all>
+</feature>';
+    $basePath = '/path';
+    $feature = $this->GadgetFeatureRegistry->_parse($content, $basePath);
+
+    $expected = array(
+        'deps' => array(
+            'dependency1' => 'dependency1',
+            'dependency2' => 'dependency2',
+        ),
+        'basePath' => '/path',
+        'name' => 'featureName',
+        'gadgetJs' => array(
+            array(
+                'type' => 'FILE',
+                'content' => 'file1.js',
+            ),
+            array(
+                'type' => 'FILE',
+                'content' => 'file2.js',
+            ),
+            array(
+                'type' => 'URL',
+                'content' => 'https://example.com/file.js',
+            ),
+        ),
+        'containerJs' => array(
+            array(
+                'type' => 'FILE',
+                'content' => 'containerFile1.js',
+            ),
+            array(
+                'type' => 'FILE',
+                'content' => 'containerFile2.js',
+            ),
+            array(
+                'type' => 'URL',
+                'content' => 'http://example.com/file.js',
+            ),
+        )
+    );
+
+    $this->assertEquals($expected, $feature);
+  }
+}
+
+class TestGadgetFeatureRegistry extends GadgetFeatureRegistry
+{
+    public function _parse($content, $basePath) {
+        return $this->parse($content, $basePath);
+    }
+}
diff --git a/trunk/php/test/gadgets/GadgetHtmlRendererTest.php b/trunk/php/test/gadgets/GadgetHtmlRendererTest.php
new file mode 100644
index 0000000..df76a36
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetHtmlRendererTest.php
@@ -0,0 +1,270 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\render\GadgetHtmlRenderer;
+use apache\shindig\common\OpenSocialVersion;
+use apache\shindig\gadgets\GadgetFactory;
+use apache\shindig\common\Cache;
+use apache\shindig\common\Config;
+use apache\shindig\gadgets\GadgetSpec;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockHtmlGadgetFactory extends GadgetFactory {
+  public function __construct(GadgetContext $context, $token) {
+    parent::__construct($context, $token);
+  }
+
+  protected function fetchGadget($gadgetUrl) {
+    return '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Require feature="dynamic-height" />
+    <Require feature="flash" />
+    <Require feature="minimessage" />
+  </ModulePrefs>
+  <Content type="html" view="home">
+  <![CDATA[
+    <h1>Hello, world!</h1>
+  ]]>
+  </Content>
+</Module>';
+  }
+}
+
+/**
+ * GadgetRendererTest test case.
+ */
+class GadgetHtmlRendererTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var Gadget
+   */
+  private $gadget;
+
+  /**
+   * @var GadgetContext
+   */
+  private $gadgetContext;
+
+  /**
+   * @var GadgetHtmlRender
+   */
+  private $gadgetHtmlRenderer;
+
+  /**
+   * @var view
+   */
+  private $view;
+
+  /**
+   * @var DomElement
+   */
+  private $domElement;
+
+  /**
+   * @var DomDocument
+   */
+  private $domDocument;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    $_SERVER['HTTP_HOST'] = 'localhost';
+    $featureCache = Cache::createCache(Config::get('feature_cache'), 'FeatureCache');
+    $key = md5(implode(',', Config::get('features_path')));
+    $featureCache->delete($key);
+    parent::setUp();
+
+    $this->gadgetContext = new GadgetContext('GADGET');
+    $gadgetSpecFactory = new MockHtmlGadgetFactory($this->gadgetContext, null);
+    $gadgetSpecFactory->fetchGadget = null;
+    $this->gadget = $gadgetSpecFactory->createGadget();
+    $this->view = $this->gadget->gadgetSpec->views['home'];
+    // init gadgetRenderer;
+    $this->gadgetHtmlRenderer = new GadgetHtmlRenderer($this->gadgetContext);
+
+    // init $this->doc
+    $this->domDocument = new \DOMDocument(null, 'utf-8');
+    $this->domDocument->preserveWhiteSpace = true;
+    $this->domDocument->formatOutput = false;
+    $this->domDocument->strictErrorChecking = false;
+    $this->domDocument->recover = false;
+
+    // init $this->element
+    $this->domElement = $this->domDocument->createElement('test');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    unset($_SERVER['HTTP_HOST']);
+    $this->gadget = null;
+    $this->gadgetContext = null;
+    $this->gadgetHtmlRenderer = null;
+    $this->view = null;
+    $this->domDocument = null;
+    $this->domElement = null;
+
+    parent::tearDown();
+  }
+
+  public function testTest() {
+    $this->assertTrue(true);
+  }
+
+  public function testGetJavaScriptsExternal() {
+    $oldForcedJsLibs = Config::get('forcedJsLibs');
+    $oldForcedAppendJsLibs = Config::get('forcedAppendedJsLibs');
+    Config::set('forcedJsLibs', 'dynamic-height:views');
+    Config::set('forcedAppendedJsLibs', array('flash'));
+    $this->gadgetHtmlRenderer->dataContext = array(
+        'Msg' => array(
+            'message1' => 'one',
+            'message2' => 'two',
+         ),
+        'UserPrefs' => array(
+            'key1' => 'value1',
+            'key2' => 'value2',
+         ),
+    );
+    $this->gadgetHtmlRenderer->gadget = $this->gadget;
+    $javaScripts = $this->gadgetHtmlRenderer->getJavaScripts();
+//    Config::set('forcedJsLibs', $oldForcedJsLibs);
+//    Config::set('forcedAppendedJsLibs', $oldForcedAppendJsLibs);
+//    $hasExtern = false;
+//    $hasInline = false;
+//    foreach ($javaScripts as $script) {
+//        switch ($script['type']) {
+//            case 'extern':
+//                if ($hasExtern) {
+//                    $this->fail('two entries with script type extern');
+//                }
+//                $hasExtern = true;
+//                $this->assertEquals(0, strpos($script['content'], '/gadgets/js/dynamic-height:views:core.js?'), 'could not find string "/gadgets/js/dynamic-height:views:core.js?" in: '.  PHP_EOL . $script['content']);
+//                break;
+//            case 'inline':
+//                if ($hasInline) {
+//                    $this->fail('two entries with script type inline');
+//                }
+//                //this is from dynamic height and should not be included
+//                $this->assertFalse(strpos($script['content'], 'gadgets.window = gadgets.window || {};'));
+//                //minimessage should be included
+//                $miniMessagePos = strpos($script['content'], 'gadgets.MiniMessage = function');
+//                $this->assertTrue($miniMessagePos > 0);
+//                //we force flash to be appended, so it should be after minimessage
+//                $this->assertTrue(strpos($script['content'], 'gadgets.flash = gadgets.flash || {};') > $miniMessagePos);
+//                $hasInline = true;
+//                break;
+//            default:
+//                $this->fail('invalid script type ' . $script['type']);
+//        }
+//    }
+//    $this->assertTrue($hasExtern);
+//    $this->assertTrue($hasInline);
+  }
+//
+//  /**
+//   * Tests GadgetHtmlRenderer->renderGadget()
+//   */
+//  public function testRenderGadgetDefaultDoctype() {
+//    Config::set('P3P', ''); // prevents "modify header information" errors
+//    ob_start();
+//    $this->gadgetHtmlRenderer->renderGadget($this->gadget, $this->view);
+//    $content = ob_get_clean();
+//    $this->assertTrue(strpos($content, '!DOCTYPE HTML>') > 0, $content);
+//  }
+//
+//  public function testLegacyDoctypeBecauseOfOldOpenSocialVersion() {
+//    Config::set('P3P', ''); // prevents "modify header information" errors
+//    $this->gadget->gadgetSpec->specificationVersion = new OpenSocialVersion('1.0.0');
+//    ob_start();
+//    $this->gadgetHtmlRenderer->renderGadget($this->gadget, $this->view);
+//    $content = ob_get_clean();
+//    $this->assertTrue(strpos($content, '!DOCTYPE HTML PUBLIC') > 0);
+//  }
+//
+//  public function testCustomDoctypeDoctype() {
+//    Config::set('P3P', ''); // prevents "modify header information" errors
+//    $this->gadget->gadgetSpec->doctype = 'CUSTOM';
+//    ob_start();
+//    $this->gadgetHtmlRenderer->renderGadget($this->gadget, $this->view);
+//    $content = ob_get_clean();
+//    $this->assertTrue(strpos($content, '!DOCTYPE CUSTOM') > 0);
+//  }
+//
+//  public function testQuirksModeBecauseOfQuirksDoctype() {
+//    Config::set('P3P', ''); // prevents "modify header information" errors
+//    $this->gadget->gadgetSpec->doctype = GadgetSpec::DOCTYPE_QUIRKSMODE;
+//    ob_start();
+//    $this->gadgetHtmlRenderer->renderGadget($this->gadget, $this->view);
+//    $content = ob_get_clean();
+//    $this->assertTrue(strpos($content, '!DOCTYPE') === false);
+//  }
+//
+//  public function testQuirksModeBecauseOfContentBlockAttribute() {
+//    Config::set('P3P', ''); // prevents "modify header information" errors
+//    $this->view['quirks'] = true;
+//    ob_start();
+//    $this->gadgetHtmlRenderer->renderGadget($this->gadget, $this->view);
+//    $content = ob_get_clean();
+//    $this->assertTrue(strpos($content, '!DOCTYPE') === false);
+//  }
+//
+//  /**
+//   * Tests GadgetHtmlRenderer->addBodyTags()
+//   */
+//  public function testAddBodyTags() {
+//    $this->gadgetHtmlRenderer->addBodyTags($this->domElement, $this->domDocument);
+//    $tmpNodeList = $this->domElement->getElementsByTagName("script");
+//    foreach($tmpNodeList as $tmpNode) {
+//      $this->assertEquals('gadgets.util.runOnLoadHandlers();', $tmpNode->nodeValue);
+//    }
+//  }
+//
+//  /**
+//   * Tests GadgetHtmlRenderer->addHeadTags()
+//   */
+//  public function testAddHeadTags() {
+//    ob_start();
+//    $this->gadgetHtmlRenderer->renderGadget($this->gadget, $this->view);
+//    ob_end_clean();
+//    $this->gadgetHtmlRenderer->addHeadTags($this->domElement, $this->domDocument);
+//
+//    // TODO: currently we just test the script part
+//    $tmpNodeList = $this->domElement->getElementsByTagName("script");
+//    $scripts = $this->gadgetHtmlRenderer->getJavaScripts();
+//
+//    $idx = 0;
+//    foreach($tmpNodeList as $tmpNode) {
+//      $script = $scripts[$idx++];
+//      if ($script['type'] == 'inline') {
+//        $this->assertEquals('text/javascript', $tmpNode->getAttribute('type'));
+//        $this->assertEquals(trim($script['content']), trim($tmpNode->nodeValue));
+//      } else {
+//        $this->assertEquals($script['content'], $tmpNode->getAttribute('src'));
+//      }
+//    }
+//  }
+
+}
+
diff --git a/trunk/php/test/gadgets/GadgetRenderingServletTest.php b/trunk/php/test/gadgets/GadgetRenderingServletTest.php
new file mode 100644
index 0000000..769775a
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetRenderingServletTest.php
@@ -0,0 +1,68 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\servlet\GadgetRenderingServlet;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class GadgetRenderingServletTest extends \PHPUnit_Framework_TestCase {
+    public function testCheckConstraints() {
+        $servlet = new GadgetRenderingServlet();
+        $servlet->noHeaders = true;
+
+        $constraints = array('type' => 'HTML', 'href' => false);
+
+        $view = array('type' => 'HTML', 'foo' => 'bar');
+        $this->assertTrue($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'HTML', 'foo' => 'bar', 'href' => '');
+        $this->assertTrue($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'HTML', 'foo' => 'bar', 'href' => 'blub');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'URL', 'foo' => 'bar', 'href' => 'blub');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'URL', 'foo' => 'bar');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+
+        $constraints = array('type' => 'HTML', 'href' => true);
+
+        $view = array('type' => 'HTML', 'foo' => 'bar');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'HTML', 'foo' => 'bar', 'href' => '');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'HTML', 'foo' => 'bar', 'href' => 'blub');
+        $this->assertTrue($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'URL', 'foo' => 'bar', 'href' => 'blub');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'URL', 'foo' => 'bar');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+
+        $constraints = array('type' => 'URL');
+
+        $view = array('type' => 'HTML', 'foo' => 'bar');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'HTML', 'foo' => 'bar', 'href' => '');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'HTML', 'foo' => 'bar', 'href' => 'blub');
+        $this->assertFalse($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'URL', 'foo' => 'bar', 'href' => 'blub');
+        $this->assertTrue($servlet->checkConstraints($view, $constraints));
+        $view = array('type' => 'URL', 'foo' => 'bar');
+        $this->assertTrue($servlet->checkConstraints($view, $constraints));
+    }
+}
diff --git a/trunk/php/test/gadgets/GadgetRewriterTest.php b/trunk/php/test/gadgets/GadgetRewriterTest.php
new file mode 100644
index 0000000..6a90c1c
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetRewriterTest.php
@@ -0,0 +1,111 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\rewrite\GadgetRewriter;
+use apache\shindig\gadgets\GadgetFactory;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockRewriterGadgetFactory extends GadgetFactory {
+  public function __construct(GadgetContext $context, $token) {
+    parent::__construct($context, $token);
+  }
+
+  protected function fetchGadget($gadgetUrl) {
+    return <<<EOD
+<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Require feature="dynamic-height" />
+  </ModulePrefs>
+  <Content type="html" view="profile">
+  <![CDATA[
+    <script>var test='<b>BIG WORDS</b>'</script>
+    <h1>Hello, world!</h1>
+  ]]>
+  </Content>
+</Module>
+EOD;
+  }
+}
+
+/**
+ * GadgetRendererTest test case.
+ */
+class GadgetRewriterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var Gadget
+   */
+  private $gadget;
+
+  /**
+   * @var GadgetContext
+   */
+  private $gadgetContext;
+
+  /**
+   * @var GadgetRewriter
+   */
+  private $gadgetRewriter;
+
+  /**
+   * @var view
+   */
+  private $view;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    $_SERVER['HTTP_HOST'] = 'localhost';
+    parent::setUp();
+
+    $this->gadgetContext = new GadgetContext('GADGET');
+    $gadgetSpecFactory = new MockRewriterGadgetFactory($this->gadgetContext, null);
+    $gadgetSpecFactory->fetchGadget = null;
+    $this->gadget = $gadgetSpecFactory->createGadget();
+    $this->gadgetRewriter = new GadgetRewriter($this->gadgetContext);
+    $this->view = $this->gadget->getView($this->gadgetContext->getView());
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    unset($_SERVER['HTTP_HOST']);
+    $this->gadget = null;
+    $this->gadgetContext = null;
+    $this->gadgetRewriter = null;
+    $this->view = null;
+
+    parent::tearDown();
+  }
+
+  /**
+   * Tests GadgetHtmlRenderer->renderGadget()
+   */
+  public function testRewrite() {
+    preg_match_all('|<script>(.*?)</script>|', $this->gadgetRewriter->rewrite($this->view["content"], $this->gadget), $tmp, PREG_SET_ORDER);
+    $desc_string = $tmp[0][1];
+    $source_string = "var test='<b>BIG WORDS</b>'";
+    $this->assertEquals($source_string, $desc_string);
+  }
+}
diff --git a/trunk/php/test/gadgets/GadgetSpecParserTest.php b/trunk/php/test/gadgets/GadgetSpecParserTest.php
new file mode 100644
index 0000000..400fd84
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetSpecParserTest.php
@@ -0,0 +1,91 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetSpecParser;
+use apache\shindig\gadgets\GadgetContext;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * GadgetSpecParser test case.
+ */
+class GadgetSpecParserTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var GadgetSpecParser
+   */
+  private $GadgetSpecParser;
+
+  /**
+   * @var Gadget
+   */
+  private $Gadget = '<?xml version="1.0" encoding="UTF-8" ?>
+<Module specificationVersion="2.0.0">
+  <ModulePrefs title="Test" doctype="html" />
+  <Content type="html" view="home">
+  <![CDATA[
+    <h1>Hello, world!</h1>
+  ]]>
+  </Content>
+</Module>';
+
+  /**
+   * @var Context
+   */
+  private $Context;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->GadgetSpecParser = new GadgetSpecParser();
+    $this->Context = new GadgetContext('GADGET');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->GadgetSpecParser = null;
+
+    parent::tearDown();
+  }
+
+  /**
+   * Tests GadgetSpecParser->parse() exception
+   */
+  public function testParseExeption() {
+    $this->setExpectedException('apache\shindig\gadgets\GadgetSpecException');
+    $this->assertTrue($this->GadgetSpecParser->parse('<', $this->Context));
+  }
+
+  /**
+   * Tests GadgetSpecParser->parse()
+   */
+  public function testParse() {
+    $gadgetParsed = $this->GadgetSpecParser->parse($this->Gadget, $this->Context);
+    $view = $gadgetParsed->views['home'];
+    $this->assertEquals('<h1>Hello, world!</h1>', trim($view['content']));
+    $this->assertEquals('2.0.0', (string) $gadgetParsed->specificationVersion);
+    $this->assertEquals('html', $gadgetParsed->doctype);
+  }
+}
+
diff --git a/trunk/php/test/gadgets/GadgetTest.php b/trunk/php/test/gadgets/GadgetTest.php
new file mode 100644
index 0000000..65db062
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetTest.php
@@ -0,0 +1,293 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\GadgetFactory;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockGadgetFactory extends GadgetFactory {
+  public function __construct(GadgetContext $context, $token) {
+  	parent::__construct($context, $token);
+  }
+  
+	protected function fetchGadget($gadgetUrl) {
+		return '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title" author="authorTest"
+   author_aboutme="authorAboutMeTest" author_affiliation="authorAffiliation"
+   author_email="authorEmail" author_link="authorLink"
+   author_location="authorLocation" author_photo="authorPhoto"
+   author_quote="authorQuote" category="category" category2="category2"
+   description="description" directory_title="directoryTitle" height="100"
+   width="100" screenshot="screenshot" singleton="true" thumbnail="thumbnail"
+   string="string" title_url="titleUrl" render_inline="never" scaling="true"
+   scrolling="true" show_in_directory="true" show_stats="false"
+  >
+      <Require feature="opensocial" />
+      <Require feature="pubsub" views="default, canvas"/>
+      <Require feature="flash" views="mobile"/>
+      <Optional feature="views" />
+      <Optional feature="opensocial-data" views="profile,canvas" />
+  </ModulePrefs>
+  <UserPref name="name1" default_value="0" datatype="hidden"/>
+  <UserPref name="name2" default_value="value" datatype="hidden"/>
+  <Content type="html" view="home">
+  <![CDATA[
+    <h1>Hello, world!</h1>
+  ]]>
+  </Content>
+</Module>';
+	}
+}
+
+/**
+ * Gadget test case.
+ */
+class GadgetTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Gadget
+   */
+  private $gadget;
+  private $context;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    
+    $this->context = new GadgetContext('GADGET');
+    $gadgetSpecFactory = new MockGadgetFactory($this->context, null);
+    $gadgetSpecFactory->fetchGadget = null;
+    $this->gadget = $gadgetSpecFactory->createGadget();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    
+    $this->gadget = null;
+    
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Gadget->getAuthor()
+   */
+  public function testGetAuthor() {
+    $this->assertEquals('authorTest', $this->gadget->getAuthor());
+  }
+
+  /**
+   * Tests Gadget->getAuthorAboutme()
+   */
+  public function testGetAuthorAboutme() {
+    $this->assertEquals('authorAboutMeTest', $this->gadget->getAuthorAboutme());
+  }
+
+  /**
+   * Tests Gadget->getAuthorAffiliation()
+   */
+  public function testGetAuthorAffiliation() {
+    $this->assertEquals('authorAffiliation', $this->gadget->getAuthorAffiliation());
+  }
+
+  /**
+   * Tests Gadget->getAuthorEmail()
+   */
+  public function testGetAuthorEmail() {
+    $this->assertEquals('authorEmail', $this->gadget->getAuthorEmail());
+  }
+
+  /**
+   * Tests Gadget->getAuthorLink()
+   */
+  public function testGetAuthorLink() {
+    $this->assertEquals('authorLink', $this->gadget->getAuthorLink());
+  }
+
+  /**
+   * Tests Gadget->getAuthorLocation()
+   */
+  public function testGetAuthorLocation() {
+    $this->assertEquals('authorLocation', $this->gadget->getAuthorLocation());
+  }
+
+  /**
+   * Tests Gadget->getAuthorPhoto()
+   */
+  public function testGetAuthorPhoto() {
+    $this->assertEquals('authorPhoto', $this->gadget->getAuthorPhoto());
+  }
+
+  /**
+   * Tests Gadget->getAuthorQuote()
+   */
+  public function testGetAuthorQuote() {
+    $this->assertEquals('authorQuote', $this->gadget->getAuthorQuote());
+  }
+
+  /**
+   * Tests Gadget->getCategory()
+   */
+  public function testGetCategory() {
+    $this->assertEquals('category', $this->gadget->getCategory());
+  }
+
+  /**
+   * Tests Gadget->getCategory2()
+   */
+  public function testGetCategory2() {
+    $this->assertEquals('category2', $this->gadget->getCategory2());
+  }
+
+  /**
+   * Tests Gadget->getDescription()
+   */
+  public function testGetDescription() {
+    $this->assertEquals('description', $this->gadget->getDescription());
+  }
+
+  /**
+   * Tests Gadget->getDirectoryTitle()
+   */
+  public function testGetDirectoryTitle() {
+    $this->assertEquals('directoryTitle', $this->gadget->getDirectoryTitle());
+  }
+
+  /**
+   * Tests Gadget->getHeight()
+   */
+  public function testGetHeight() {
+    $this->assertEquals('100', $this->gadget->getHeight());
+  }
+
+  /**
+   * Tests Gadget->getRenderInline()
+   */
+  public function testGetRenderInline() {
+    $this->assertEquals("never", $this->gadget->getRenderInline());
+  }
+
+  /**
+   * Tests Gadget->getScaling()
+   */
+  public function testGetScaling() {
+    $this->assertEquals("true", $this->gadget->getScaling());
+  }
+
+  /**
+   * Tests Gadget->getScreenshot()
+   */
+  public function testGetScreenshot() {
+    $this->assertEquals('screenshot', $this->gadget->getScreenshot());
+  }
+
+  /**
+   * Tests Gadget->getScrolling()
+   */
+  public function testGetScrolling() {
+    $this->assertEquals("true", $this->gadget->getScrolling());
+  }
+
+  /**
+   * Tests Gadget->getShowInDirectory()
+   */
+  public function testGetShowInDirectory() {
+    $this->assertEquals("true", $this->gadget->getShowInDirectory());
+  }
+
+  /**
+   * Tests Gadget->getShowStats()
+   */
+  public function testGetShowStats() {
+    $this->assertEquals("false", $this->gadget->getShowStats());
+  }
+
+  /**
+   * Tests Gadget->getSingleton()
+   */
+  public function testGetSingleton() {
+    $this->assertEquals('true', $this->gadget->getSingleton());
+  }
+
+  /**
+   * Tests Gadget->getString()
+   */
+  public function testGetString() {
+    $this->assertEquals('string', $this->gadget->getString());
+  }
+
+  /**
+   * Tests Gadget->getThumbnail()
+   */
+  public function testGetThumbnail() {
+    $this->assertEquals('thumbnail', $this->gadget->getThumbnail());
+  }
+
+  /**
+   * Tests Gadget->getTitle()
+   */
+  public function testGetTitle() {
+    $this->assertEquals('title', $this->gadget->getTitle());
+  }
+
+  /**
+   * Tests Gadget->getTitleUrl()
+   */
+  public function testGetTitleUrl() {
+    $this->assertEquals('titleUrl', $this->gadget->getTitleUrl());
+  }
+
+  public function testGetRequiredFeatures() {
+    $this->assertEquals(array(
+        'opensocial' => array('views' => array()),
+        'pubsub' => array('views' => array('default', 'canvas')),
+        'flash' => array('views' => array('mobile'))), $this->gadget->getRequiredFeatures());
+  }
+
+  public function testGetOptionalFeatures() {
+    $this->assertEquals(array(
+        'views' => array('views' => array()),
+        'opensocial-data' => array('views' => array('profile', 'canvas'))), $this->gadget->getOptionalFeatures());
+  }
+
+  /**
+   * Tests Gadget->getUserPrefs()
+   */
+  public function testGetUserPrefs() {
+    $userPrefs = $this->gadget->getUserPrefs();
+    $this->assertEquals("name1", $userPrefs[0]['name']);
+    $this->assertEquals("0", $userPrefs[0]['defaultValue']);
+    $this->assertEquals("0", $userPrefs[0]['value']);
+    $this->assertEquals("name2", $userPrefs[1]['name']);
+    $this->assertEquals("value", $userPrefs[1]['defaultValue']);
+    $this->assertEquals("value", $userPrefs[1]['value']);
+  }
+
+  /**
+   * Tests Gadget->getWidth()
+   */
+  public function testGetWidth() {
+    $this->assertEquals("100", $this->gadget->getWidth());
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/test/gadgets/GadgetUrlRendererTest.php b/trunk/php/test/gadgets/GadgetUrlRendererTest.php
new file mode 100644
index 0000000..9323ef5
--- /dev/null
+++ b/trunk/php/test/gadgets/GadgetUrlRendererTest.php
@@ -0,0 +1,95 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\render\GadgetUrlRenderer;
+use apache\shindig\gadgets\GadgetFactory;
+use apache\shindig\common\Cache;
+use apache\shindig\common\Config;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockUrlGadgetFactory extends GadgetFactory {
+  public function __construct(GadgetContext $context, $token) {
+    parent::__construct($context, $token);
+  }
+
+  protected function fetchGadget($gadgetUrl) {
+    return '<?xml version="1.0" encoding="UTF-8" ?>
+<Module>
+  <ModulePrefs title="title">
+    <Require feature="dynamic-height" />
+  </ModulePrefs>
+  <UserPref name="key" datatype="string" default_value="value" />
+  <Content type="url" url="http://example.com/gadget.php" view="home">
+  </Content>
+</Module>';
+  }
+}
+
+/**
+ * GadgetUrlRendererTest test case.
+ */
+class GadgetUrlRendererTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var Gadget
+   */
+  private $gadget;
+
+  /**
+   * @var GadgetContext
+   */
+  private $gadgetContext;
+
+  /**
+   * @var GadgetHtmlRender
+   */
+  private $gadgetUrlRenderer;
+
+
+  protected function setUp() {
+    $_SERVER['HTTP_HOST'] = 'localhost';
+    $featureCache = Cache::createCache(Config::get('feature_cache'), 'FeatureCache');
+    $key = md5(implode(',', Config::get('features_path')));
+    $featureCache->delete($key);
+    parent::setUp();
+    $this->gadgetContext = new GadgetContext('GADGET');
+    $gadgetSpecFactory = new MockUrlGadgetFactory($this->gadgetContext, null);
+    $gadgetSpecFactory->fetchGadget = null;
+    $this->gadget = $gadgetSpecFactory->createGadget();
+    $this->gadgetUrlRenderer = new GadgetUrlRenderer($this->gadgetContext);
+  }
+
+  public function testTest() {
+    $this->assertTrue(true);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    unset($_SERVER['HTTP_HOST']);
+    $this->gadget = null;
+    $this->gadgetContext = null;
+    $this->gadgetUrlRenderer = null;
+
+    parent::tearDown();
+  }
+}
diff --git a/trunk/php/test/gadgets/MakeRequestTest.php b/trunk/php/test/gadgets/MakeRequestTest.php
new file mode 100644
index 0000000..5e8948d
--- /dev/null
+++ b/trunk/php/test/gadgets/MakeRequestTest.php
@@ -0,0 +1,337 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\MakeRequest;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\gadgets\MakeRequestOptions;
+use apache\shindig\common\RemoteContentFetcher;
+use apache\shindig\common\sample\BasicSecurityToken;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Provides an implementation of RemoteContentFetcher which can be controlled
+ * by unit tests and does not actually make any external requests.
+ * RemoteContentRequest objects added to this class are added to an internal
+ * FIFO queue.  Every time a request is sent to fetchRequest, the next
+ * RemoteContentRequest object in the queue is returned as a response.
+ * Requests sent to fetchRequest are also stored in a queue for later retrieval
+ * and examination.
+ */
+class MockMakeRequestFetcher extends RemoteContentFetcher {
+  private $responses;
+  private $requests;
+
+  /**
+   * Constructor.
+   */
+  public function __construct() {
+    $_SERVER["HTTP_HOST"] = 'localhost';
+    date_default_timezone_set('GMT');
+    $this->responses = array();
+    $this->requests = array();
+  }
+
+  /**
+   * Adds a response object to an internal queue of responses.
+   * @param RemoteContentRequest $response The response to return.
+   */
+  public function enqueueResponse(RemoteContentRequest $response) {
+    $this->responses[] = $response;
+  }
+
+  /**
+   * Returns a request object that was sent to fetchRequest.  If multiple
+   * requests have been sent to fetchRequest, they are returned by this
+   * method in the order they were requested.
+   * @return RemoteContentRequest
+   */
+  public function dequeueRequest() {
+    return array_shift($this->requests);
+  }
+
+  /**
+   * Fakes a content request to a remote server.
+   * @param RemoteContentRequest $request  The external request information.
+   * @return RemoteContentRequest The next response which was enqueued by
+   *     calling enqueueResponse.
+   */
+  public function fetchRequest(RemoteContentRequest $request) {
+    $this->requests[] = $request;
+    return array_shift($this->responses);
+  }
+
+  /**
+   * Fakes multiple requests to a remote server.  Calls fetchRequest for
+   * each request passed to this method.
+   * @param array $requests An array of RemoteContentRequests.
+   * @return An array of RemoteContentRequests corresponging to the responses
+   *    returned for each of the request inputs.
+   */
+  public function multiFetchRequest(Array $requests) {
+    $responses = array();
+    foreach ($requests as $key => $request) {
+      $responses[$key] = $this->fetchRequest($request);
+    }
+    return $responses;
+  }
+}
+
+/**
+ * Unit tests for the MakeRequest class.
+ */
+class MakeRequestTest extends \PHPUnit_Framework_TestCase {
+  private $fetcher;
+  private $makeRequest;
+  private $context;
+  private $response;
+  
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->fetcher = new MockMakeRequestFetcher();
+    $this->makeRequest = new MakeRequest($this->fetcher);
+    $this->context = new GadgetContext('GADGET');
+
+    $this->response = new RemoteContentRequest('http://www.example.com');
+    $this->response->setHttpCode(200);
+    $this->response->setResponseContent("Basic response");
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    parent::tearDown();
+  }
+
+  /**
+   * Executes a makeRequest call and returns the request object which would
+   * have been sent externally (as opposed to the response).
+   *
+   * @param MakeRequestOptions $params
+   * @param RemoteContentRequest $response The response to return for this
+   *     request.
+   * @return RemoteContentRequest The request object.
+   */
+  protected function catchRequest(MakeRequestOptions $params, RemoteContentRequest $response) {
+    $this->fetcher->enqueueResponse($response);
+    $result = $this->makeRequest->fetch($this->context, $params);
+    return $this->fetcher->dequeueRequest();
+  }
+
+  /**
+   * Tests that makeRequest calls with an invalid url throw an exception.
+   */
+  public function testInvalidUrl() {
+    try {
+      $params = new MakeRequestOptions('invalidurl');
+      $this->makeRequest->fetch($params);
+      $this->fail("Calling makeRequest with an invalid url should throw an exception.");
+    } catch (\Exception $ex) { }
+  }
+
+  /**
+   * Tests that normal requests specify a GET to the supplied URL.
+   */
+  public function testBasicRequest() {
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setNoCache(true);
+
+    $request = $this->catchRequest($params, $this->response);
+    $this->assertContains($request->getUrl(), 'http://www.example.com');
+    $this->assertEquals('GET', $request->getMethod());
+  }
+
+  /**
+   * Tests that signed requests generate appropriate oauth_ and opensocial_
+   * parameters.
+   */
+  public function testSignedRequest() {
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setAuthz('SIGNED')
+           ->setNoCache(true)
+           ->setSecurityTokenString(urldecode($token->toSerialForm()));
+
+    $request = $this->catchRequest($params, $this->response);
+
+    $this->assertContains('oauth_signature', $request->getUrl());
+    $this->assertContains('oauth_signature_method=RSA-SHA1', $request->getUrl());
+    $this->assertContains('opensocial_app_url=appUrl', $request->getUrl());
+    $this->assertContains('opensocial_viewer_id=viewer', $request->getUrl());
+    $this->assertContains('opensocial_owner_id=owner', $request->getUrl());
+    $this->assertEquals('GET', $request->getMethod());
+  }
+
+  /**
+   * Tests that setting "sign_viewer" = false does not include viewer
+   * information in the request.
+   */
+  public function testSignedNoViewerRequest() {
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setAuthz('SIGNED')
+           ->setNoCache(true)
+           ->setSignViewer(false)
+           ->setSecurityTokenString(urldecode($token->toSerialForm()));
+
+    $request = $this->catchRequest($params, $this->response);
+
+    $this->assertContains('oauth_signature', $request->getUrl());
+    $this->assertNotContains('opensocial_viewer_id=viewer', $request->getUrl());
+    $this->assertContains('opensocial_owner_id=owner', $request->getUrl());
+  }
+
+  /**
+   * Tests that setting "format" = "FEED" parses an atom feed into a JSON
+   * structure.
+   */
+  public function testFeedRequest() {
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setResponseFormat('FEED')
+           ->setNoCache(true)
+           ->setNumEntries(2);
+
+    $sampleAtomPath = realpath(dirname(__FILE__) . "/../misc/sampleAtomFeed.xml");
+    $sampleAtom = file_get_contents($sampleAtomPath);
+    $this->response->setResponseContent($sampleAtom);
+    $this->fetcher->enqueueResponse($this->response);
+    $result = $this->makeRequest->fetch($this->context, $params);
+    $feedJson = json_decode($result->getResponseContent(), true);
+
+    $this->assertArrayHasKey('Entry', $feedJson);
+    $this->assertEquals(2, count($feedJson['Entry']));
+    $this->assertArrayHasKey('Title', $feedJson['Entry'][0]);
+    $this->assertEquals("Atom-Powered Robots Run Amok", $feedJson['Entry'][0]['Title']);
+  }
+
+  /**
+   * Tests that setting request headers are passed in the outgoing request.
+   */
+  public function testRequestHeaders(){
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setRequestHeaders(array(
+      "Content-Type" => "application/json",
+      "Accept-Language" => "en-us"
+    ));
+    $params->setNoCache(true);
+
+    $request = $this->catchRequest($params, $this->response);
+    $this->assertTrue($request->hasHeaders());
+    $this->assertEquals('application/json', $request->getHeader('Content-Type'));
+    $this->assertEquals('en-us', $request->getHeader('Accept-Language'));
+  }
+
+  /**
+   * Tests that setting invalid request headers are not passed in the outgoing
+   * request.
+   */
+  public function testInvalidRequestHeaders(){
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setRequestHeaders(array(
+      "Content-Type" => "application/json",
+      "Accept-Language" => "en-us",
+      "Host" => "http://www.evil.com",
+      "host" => "http://www.evil.com",
+      "HOST" => "http://www.evil.com",
+      "Accept" => "blah",
+      "Accept-Encoding" => "blah"
+    ));
+    $params->setNoCache(true);
+
+    $request = $this->catchRequest($params, $this->response);
+    $this->assertTrue($request->hasHeaders());
+    $this->assertEquals('application/json', $request->getHeader('Content-Type'));
+    $this->assertEquals('en-us', $request->getHeader('Accept-Language'));
+
+    $this->assertNull($request->getHeader('Host'));
+    $this->assertNull($request->getHeader('Accept'));
+    $this->assertNull($request->getHeader('Accept-Encoding'));
+  }
+
+  /**
+   * Tests that setting request headers in a form urlencoded way are passed in the outgoing request.
+   */
+  public function testFormEncodedRequestHeaders(){
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setFormEncodedRequestHeaders("Content-Type=application%2Fx-www-form-urlencoded&Accept-Language=en-us");
+    $params->setNoCache(true);
+
+    $request = $this->catchRequest($params, $this->response);
+    $this->assertTrue($request->hasHeaders());
+    $this->assertEquals('application/x-www-form-urlencoded', $request->getHeader('Content-Type'));
+  }
+
+  public function testResponseHeaders() {
+    $params = new MakeRequestOptions('http://www.example.com');
+    $params->setNoCache(true);
+
+    $headers = array(
+      'Content-Type' => 'text/plain'
+    );
+    $this->response->setResponseHeaders($headers);
+    $this->fetcher->enqueueResponse($this->response);
+
+    $result = $this->makeRequest->fetch($this->context, $params);
+    $response_headers = $result->getResponseHeaders();
+
+    $this->assertArrayHasKey('Content-Type', $response_headers);
+    $this->assertEquals('text/plain', $response_headers['Content-Type']);
+  }
+
+  public function testCleanResponseHeaders() {
+    $response_headers = array(
+      'Content-Type' => 'text/plain',
+      'Set-Cookie' => 'blah',
+      'set-cookie' => 'blah',
+      'SET-COOKIE' => 'blah',
+      'sEt-cOoKiE' => 'blah',
+      'Accept-Ranges' => 'blah',
+      'Vary' => 'blah',
+      'Expires' => 'blah',
+      'Date' => 'blah',
+      'Pragma' => 'blah',
+      'Cache-Control' => 'blah',
+      'Transfer-Encoding' => 'blah',
+      'WWW-Authenticate' => 'blah'
+    );
+    
+    $cleaned_headers = $this->makeRequest->cleanResponseHeaders($response_headers);
+
+    $this->assertArrayHasKey('Content-Type', $cleaned_headers);
+    $this->assertEquals('text/plain', $cleaned_headers['Content-Type']);
+    $this->assertArrayNotHasKey('Set-Cookie', $cleaned_headers);
+    $this->assertArrayNotHasKey('set-cookie', $cleaned_headers);
+    $this->assertArrayNotHasKey('SET-COOKIE', $cleaned_headers);
+    $this->assertArrayNotHasKey('sEt-cOoKiE', $cleaned_headers);
+    $this->assertArrayNotHasKey('Accept-Ranges', $cleaned_headers);
+    $this->assertArrayNotHasKey('Vary', $cleaned_headers);
+    $this->assertArrayNotHasKey('Expires', $cleaned_headers);
+    $this->assertArrayNotHasKey('Date', $cleaned_headers);
+    $this->assertArrayNotHasKey('Pragma', $cleaned_headers);
+    $this->assertArrayNotHasKey('Cache-Control', $cleaned_headers);
+    $this->assertArrayNotHasKey('Transfer-Encoding', $cleaned_headers);
+    $this->assertArrayNotHasKey('WWW-Authenticate', $cleaned_headers);
+  }
+}
+
diff --git a/trunk/php/test/gadgets/SigningFetcherTest.php b/trunk/php/test/gadgets/SigningFetcherTest.php
new file mode 100644
index 0000000..30983a6
--- /dev/null
+++ b/trunk/php/test/gadgets/SigningFetcherTest.php
@@ -0,0 +1,167 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\gadgets\SigningFetcher;
+use apache\shindig\common\sample\BasicSecurityToken;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class MockSignatureMethod extends \OAuthSignatureMethod_RSA_SHA1 {
+  protected function fetch_public_cert(&$request) {
+    return <<<EOD
+-----BEGIN CERTIFICATE-----
+MIICsDCCAhmgAwIBAgIJALlpyqPEjwvvMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV
+BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
+aWRnaXRzIFB0eSBMdGQwHhcNMDkwNDAxMDgzMzQzWhcNMTIwMzMxMDgzMzQzWjBF
+MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50
+ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB
+gQD8pipiScqep1T8e531ieuseKR1GPaVWmduMBXzrIhMYfD2x+hWy6ocGkcNxVIE
+dopIo238YtSde/T3JiSE/Ho5uQ/os4mzVM+uZSkNyknZkzEmCkIg+kz6P91SMF5j
+ioxdRcT0rg7d+DvsUd2Gt3UPdMf1GtcBGd8bxfjuNQQtyQIDAQABo4GnMIGkMB0G
+A1UdDgQWBBQNTYnsqvzJ192fs03xJhjwlIVOQTB1BgNVHSMEbjBsgBQNTYnsqvzJ
+192fs03xJhjwlIVOQaFJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUt
+U3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJALlpyqPE
+jwvvMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEA2HUzlfAvZ1ELSa1V
+k1QBQQWEnXI7ST7jtsqflyErJW2SekMu0ReLAeVqYkVfeJG/7FZ18i7/LMOEV6uY
+3k3kOKRcgbfa/k1j3siRbpNdyD3qzGxo3ggtE32P7l8IdWLkWcMvkAqfROXhay5W
+nbpJMipy62GBW7yBbG+ypSasgI0=
+-----END CERTIFICATE-----
+EOD;
+  }
+  protected function fetch_private_cert(&$request) {
+    ;
+  }
+}
+
+/**
+ * SigningFetcher test case.
+ */
+class SigningFetcherTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var SigningFetcher
+   */
+  private $signingFetcher;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $private_key = <<<EOD
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,2BB1348F45867303
+
+9+e/kJCKUTnJLrNYY1iSjX+e6IVPo31dN20ab3O1BknT5c28PLjJbQkJz479VCX8
+zJen/OyugesHXiQe5skPaG6+xwWGnztIxjHCLT5WtRE755UT3K83IeDde1zsK9xy
+Iy8aRZbfBKCkgriIRNgD496gaVgEOGljEhCCIBLWERNZntcGmaBmN6CUdg75uuTI
+HMX+2cA68yzRx31cU6EYdzB2vN93aLNuPI1u2ebFe7kuNYhW3d9Bc5MJh7iQdOfO
+Yf94Xuic+2vIvwxi30Htz0wTBmTdEolDsSWzuyj7pjtUa0zZqaawCwLMYJFtz8lm
+M2c5PXv8VvLBFIsTXWdy5+qDWMeROl1PaSDQ7HfAq8BtwNqV2yMKLE6cwHIWbYr/
+lyIcBEhAZ8jfM81AWCgyAyeGSi4xGoCljxptExEwVzBJGjH93Ly6M7tjLBLmEQJM
+nGmcY/3lmSMQIbxHV4ktXukPMrYYaTu5DW9jE+sNUHj+iUN/jJMTdOGh8zUtOQTs
+qGuZBJbmjxdfSogCBL3f+JqOtRYUIIsZWEgb/AC10PC4pBit+9Cs9Z1LDMynFjKH
+kGX/qgro2rPLiqR8o2dI/wCIa5sJhUT5vFC5N+Jn0jyhROK+eom4yEF0xX3DxSZY
+iiclKgIOL/iB7FYEYFO17kUjFj8g53QWKh4tML/UG4GTIetNjD2u8wbobE7SxzZf
+HHJXc4OblK/6GVpLn7yxZ5/EG7vtX/R4aPA70VFSkJYUd0xHWjUihss+9/TSIj/K
+Cgpm3sdinamuC5b40tVhFhrfZyfUlqmssjU1nOsbnS+EqFgQJimbDg==
+-----END RSA PRIVATE KEY-----
+EOD;
+    $rsa_private_key = @openssl_pkey_get_private($private_key, 'shindig');
+    $basicFetcher = $this->getMock('apache\shindig\common\RemoteContentFetcher');
+    $this->signingFetcher = SigningFetcher::makeFromOpenSslPrivateKey($basicFetcher, 'http://shindig/public.cer', $rsa_private_key);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Substitutions = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests SigningFetcher->fetchRequest
+   */
+  public function testFetchRequest() {
+    $request = new RemoteContentRequest('http://example.org/signed');
+    $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+    $request->setToken(BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default'));
+    $request->setPostBody('key=value&anotherkey=value');
+    $this->signingFetcher->fetchRequest($request);
+    $this->verifySignedRequest($request);
+  }
+
+  /**
+   * Tests SigningFetcher->fetchRequest
+   */
+  public function testFetchRequestForBodyHash() {
+    $request = new RemoteContentRequest('http://example.org/signed');
+    $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+    $request->setToken(BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default'));
+    $request->setPostBody('Hello World!');
+    $request->setHeaders('Content-Type: text/plain');
+    $this->signingFetcher->fetchRequest($request);
+    $this->verifySignedRequest($request);
+    $url = parse_url($request->getUrl());
+    $query = array();
+    parse_str($url['query'], $query);
+    // test example 'Hello World!' and 'Lve95gjOVATpfV8EL5X4nxwjKHE=' are from
+    // OAuth Request Body Hash 1.0 Draft 4 Example
+    $this->assertEquals('Lve95gjOVATpfV8EL5X4nxwjKHE=', $query['oauth_body_hash']);
+  }
+
+  /**
+   * Tests SigningFetcher->fetchRequest
+   */
+  public function testFetchRequestWithEmptyPath() {
+    $request = new RemoteContentRequest('http://example.org');
+    $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+    $request->setToken(BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default'));
+    $request->setPostBody('key=value&anotherkey=value');
+    $this->signingFetcher->fetchRequest($request);
+    $this->verifySignedRequest($request);
+  }
+
+  private function verifySignedRequest(RemoteContentRequest $request) {
+    $url = parse_url($request->getUrl());
+    $query = array();
+    parse_str($url['query'], $query);
+    $post = array();
+    $contentType = $request->getHeader('Content-Type');
+    if ((stripos($contentType, 'application/x-www-form-urlencoded') !== false || $contentType == null)) {
+      parse_str($request->getPostBody(), $post);
+    } else {
+      $this->assertEquals(base64_encode(sha1($request->getPostBody(), true)), $query['oauth_body_hash']);
+    }
+    $this->assertEquals('owner', $query['opensocial_owner_id']);
+    $this->assertEquals('viewer', $query['opensocial_viewer_id']);
+    $this->assertEquals('app', $query['opensocial_app_id']);
+    $this->assertEquals('appUrl', $query['opensocial_app_url']);
+    $this->assertEquals('1', $query['opensocial_instance_id']);
+    $this->assertEquals($query['xoauth_signature_publickey'], $query['xoauth_public_key']);
+    $oauthRequest = \OAuthRequest::from_request($request->getMethod(), $request->getUrl(), array_merge($query, $post));
+    $signature_method = new MockSignatureMethod();
+    $signature_valid = $signature_method->check_signature($oauthRequest, null, null, $query['oauth_signature']);
+    $this->assertTrue($signature_valid);
+  }
+}
+
diff --git a/trunk/php/test/gadgets/SubstitutionsTest.php b/trunk/php/test/gadgets/SubstitutionsTest.php
new file mode 100644
index 0000000..fac7632
--- /dev/null
+++ b/trunk/php/test/gadgets/SubstitutionsTest.php
@@ -0,0 +1,67 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\Substitutions;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Substitutions test case.
+ */
+class SubstitutionsTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Substitutions
+   */
+  private $Substitutions;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Substitutions = new Substitutions();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Substitutions = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Substitutions->substitute()
+   * Substitutions->addSubstitution()
+   * Substitutions->substituteType()
+   */
+  public function testSubstitute() {
+    
+    $type = 'MSG';
+    $key = 'DMSG';
+    $value = 'success';
+    $input = 'Test: __MSG_DMSG__';
+    $this->Substitutions->addSubstitution($type, $key, $value);
+    $this->assertEquals('Test: success', $this->Substitutions->substitute($input));
+  
+  }
+
+}
+
diff --git a/trunk/php/test/gadgets/TemplateParserTest.php b/trunk/php/test/gadgets/TemplateParserTest.php
new file mode 100644
index 0000000..cb29ee1
--- /dev/null
+++ b/trunk/php/test/gadgets/TemplateParserTest.php
@@ -0,0 +1,57 @@
+<?php
+namespace apache\shindig\test\gadgets;
+use apache\shindig\gadgets\templates\TemplateLibrary;
+use apache\shindig\gadgets\GadgetContext;
+use apache\shindig\gadgets\templates\TemplateParser;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class TemplateParserTest extends \PHPUnit_Framework_TestCase {
+  public function testOsVar() {
+    $viewNode = '<?xml version="1.0" encoding="UTF-8" ?>
+        <script xmlns:os="http://ns.opensocial.org/2008/markup" type="text/os-template">
+          <os:Var key="counter" value="1" />
+          <os:Var key="counter2" value="${counter + 1}" />
+          <os:Var key="array" value="[1,3,5,7]" />
+          <os:Var key="object">
+            {"key" : "value"}
+          </os:Var>
+        </script>';
+
+    $dataContext = array();
+    $doc = new \DomDocument();
+    $doc->loadXml($viewNode);
+    $contentBlocks = $doc->getElementsByTagName('script');
+    $library = new TemplateLibrary(new GadgetContext('GADGET'));
+    $parser = new TemplateParser();
+    $tags = array();
+    foreach ($contentBlocks as $content) {
+      $tags[] = $parser->process($content, $dataContext, $library);
+    }
+    $this->assertEquals(1, count($tags));
+
+    $dataContext = $parser->getDataContext();
+
+    $this->assertEquals(1, $dataContext['Top']['counter']);
+    $this->assertEquals(2, $dataContext['Top']['counter2']);
+    $this->assertEquals(array(1,3,5,7), $dataContext['Top']['array']);
+    $this->assertEquals(array('key' => 'value'), $dataContext['Top']['object']);
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/test/misc/activity/activity.xml b/trunk/php/test/misc/activity/activity.xml
new file mode 100644
index 0000000..44ea36b
--- /dev/null
+++ b/trunk/php/test/misc/activity/activity.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="ACTIVITY" author_email="lipengage@gmail.com">
+    <Require feature="opensocial-1.0"></Require>
+    <Require feature="dynamic-height"></Require>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+    <style>
+      div {border: solid 1px green; margin:2px;}
+    </style>
+    <script type="text/javascript">
+var viewer;
+var owner;
+
+function addFinished(activity) {
+  var div = document.getElementById("add_activities");
+  var html = ['title:', activity.title, '<br/>', 'body:', activity.body, '<br/>', div.innerHTML].join('');
+  div.innerHTML = html;
+  gadgets.window.adjustHeight();
+}
+
+function addMoreActivity() {
+  var today = new Date();
+  var activityStamp = today.getTime();
+  var title = Math.floor(Math.random()*100);
+  var body = 'body_' + activityStamp;
+  var templateParams = {};
+  templateParams['Song'] = 'With or without you';
+  templateParams['Artist'] = 'U2';
+  templateParams['Viewer'] = viewer;
+  templateParams['Owner'] = owner;
+  var params = {
+    'titleId': 'allFields titleId: ' + today + ' ' + activityStamp,
+    'title': title,
+    'templateParams': templateParams,
+    'url': 'url_' + activityStamp,
+    'bodyId': 'bodyId_' + activityStamp,
+    'body': body,
+    'externalId': 'externalId_' + activityStamp,
+    'streamTitle': 'streamTitle_' + activityStamp,
+    'streamUrl': 'streamUrl_' + activityStamp,
+    'streamSourceUrl': 'streamSourceUrl_' + activityStamp,
+    'streamFaviconUrl': 'streamFaviconUrl_' + activityStamp,
+    'priority': '1',
+    'id': 'id_' + activityStamp,
+    'userId': 'userId_' + activityStamp,
+    'appId': 'appId_' + activityStamp,
+    'postedTime': activityStamp
+  };
+  var activity = opensocial.newActivity(params);
+  opensocial.requestCreateActivity(
+    activity,
+    opensocial.CreateActivityPriority.HIGH,
+    function() {addFinished({'title':title,'body':body})}
+  );
+}
+function displayGetActivities(activities, div_id) {
+  var div = document.getElementById(div_id);
+  var harr = ['size:'];
+  harr.push(activities.size());
+  harr.push(' total:');
+  harr.push(activities.getTotalSize());
+  harr.push('<br/>');
+  activities.each(
+    function(activity) {
+      harr.push('title:');
+      harr.push(activity.getField('title'));
+      harr.push('<br/>');
+      harr.push('body:');
+      harr.push(activity.getField('body'));
+      harr.push('<br/>');
+    }
+  );
+  div.innerHTML = harr.join('');
+  gadgets.window.adjustHeight();
+}
+
+function getMoreActivities(params, div_id) {
+  var req = opensocial.newDataRequest();
+  var opt_params = params;
+  var idSpec = opensocial.newIdSpec({"userId" : 'OWNER', "groupId" : '@self'});
+  req.add(req.newFetchActivitiesRequest(idSpec, opt_params), 'activities');
+  req.send(function(data) {
+    var activities = data.get('activities').getData();
+    displayGetActivities(activities, div_id);
+  });
+}
+
+function default_activities(div_id) {
+  var params = {}
+  getMoreActivities(params, div_id);
+}
+
+function first_max_activities(div_id) {
+  var params = {};
+  params[opensocial.DataRequest.ActivityRequestFields.FIRST] = document.getElementById('first').value;
+  params[opensocial.DataRequest.ActivityRequestFields.MAX] = document.getElementById('max').value;
+  getMoreActivities(params, div_id);
+}
+
+function _rest(url, div_id) {
+  gadgets.io.makeRequest(
+    url, 
+    function(data) {
+      var tmp = gadgets.json.parse(data.text);
+      var jsonActivities = [];
+      for(var i = 0; i < tmp.entry.length; i++) {
+        jsonActivities.push(new JsonActivity(tmp.entry[i]));
+      }
+      var activities = new opensocial.Collection(jsonActivities, tmp.startIndex, tmp.itemsPerPage);
+      displayGetActivities(activities, div_id);
+    }, 
+    null
+  );
+}
+
+function rest(div_id) {
+  //url format /activities/{userId}/{groupId}/appId/{activityId};
+
+  var content = {};
+  content.type = 'activity';
+  content.first = document.getElementById("_first").value;
+  content.max = document.getElementById("_max").value;
+  content.url = document.getElementById("restUrl").value+'/activities/'+owner.getId()+'/@self';
+  content.key = document.getElementById("consumerKey").value;
+  content.secret = document.getElementById("consumerSecret").value;
+  var params = {};
+  params[gadgets.io.RequestParameters.POST_DATA] = "data=" + gadgets.json.stringify(content);
+  params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST;
+  signUrl = document.getElementById("signUrl").value;
+  gadgets.io.makeRequest(
+    signUrl, 
+    function(data){
+      var tmp = gadgets.json.parse(data.text);
+      _rest(tmp.url, div_id);
+    }, 
+    params
+  );
+}
+
+function initData() {
+  var req = opensocial.newDataRequest();
+  req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.OWNER), 'o');
+  req.add(req.newFetchPersonRequest(opensocial.IdSpec.PersonId.VIEWER), 'v');
+  req.send(function(data) {
+    viewer = data.get('v').getData();
+    owner = data.get('o').getData();
+  });
+  gadgets.window.adjustHeight();
+}
+gadgets.util.registerOnLoadHandler(initData);
+    </script>
+    <div>
+      <input type="button" value="add one activity" onclick="addMoreActivity();"/><br/>
+      <div id="add_activities">
+      </div>
+    </div>
+    <div>
+      <input type="button" value="default activities" onclick="default_activities('default_activities')" /><br/>
+      <div id="default_activities">
+      </div>
+    </div>
+    <div>
+      <input type="button" value="first max activities" onclick="first_max_activities('first_max_activities')" /><br/>
+      first: <input type="text" id="first" style="width:40px;" />
+      max: <input type="text" id="max" style="width:40px;" /><br/>
+      <div id="first_max_activities">
+      </div>
+    </div>
+    <div>
+      <input type="button" value="rest" onclick="rest('rest_first_max_activities')"/><br/>
+      first: <input type="text" id="_first" style="width:50px;" />
+      max: <input type="text" id="_max" style="width:50px;" /><br/>
+      rest url: <br/>
+      <input style="width:400px;" id="restUrl" type="text" value="http://shindig/social/rest"/><br/>
+      sign url: <br/>
+      <input style="width:400px;" id="signUrl" type="text" value="http://shindig/test/misc/sign.php"/><br/>
+      oauth consumer key: <br/>
+      <input style="width:400px;" id="consumerKey" type="text" value=""/><br/>
+      oauth consumer secret: <br/>
+      <input style="width:400px;" id="consumerSecret" type="text" value=""/><br/>
+      <div id="rest_first_max_activities">
+      </div>
+    </div>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/php/test/misc/album/album.xml b/trunk/php/test/misc/album/album.xml
new file mode 100644
index 0000000..5b809ef
--- /dev/null
+++ b/trunk/php/test/misc/album/album.xml
@@ -0,0 +1,186 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Album test">
+    <Require feature="dynamic-height"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+    <script type="text/javascript">
+
+$ = function(id) {
+  return document.getElementById(id);
+}
+
+
+function sendRequest(url, method, data) {
+  var xhr = new window.XMLHttpRequest();
+  xhr.open(method, url, true);
+  xhr.setRequestHeader("Content-type", "application/json");
+  var body = data ? gadgets.json.stringify(data) : null;
+  xhr.send(body);
+}
+
+function restCreate() {
+  var title = $('create-title');
+  var desc = $('create-description');
+  var data = {};
+  data['title'] = title.value;
+  data['description'] = desc.value;
+  data['mediaType'] = 'IMAGE';
+  var url = $('rest-url').value + '?' + $('param').value;
+  sendRequest(url, 'POST', data);
+}
+
+function jsonCreate() {
+  var title = $('create-title');
+  var desc = $('create-description');
+  var data = {};
+  data['method'] = 'albums.create';
+  data['params'] = {'album' : {'title': title.value, 'description': desc.value, 'mediaType': 'IMAGE'}};
+  data['params']['userId'] = '@me';
+  data['params']['groupId'] = '@self';
+  data['id'] = 'createAlbum';
+  var url = $('json-rpc-url').value + '?' + $('param').value;
+  sendRequest(url, 'POST', data);
+}
+
+function restGet() {
+  var albumId = $('get-album-id').value;
+  var url = '';
+  if (albumId) {
+    url = $('rest-url').value + '/' + albumId + '?' + $('param').value;
+  } else {
+    url = $('rest-url').value + '?' + $('param').value;
+  }
+  var startIndex = $('get-start-index').value;
+  if (startIndex) {
+    url += '&startIndex=' + startIndex;
+  }
+  var count = $('get-count').value;
+  if (count) {
+    url += '&count=' + count;
+  }
+  sendRequest(url, 'GET', null);
+}
+
+function jsonGet() {
+  var data = {'method': 'albums.get', 'id': 'getAlbum'};
+  data['params'] = {'userId': '@me', 'groupId': '@self'};
+  var albumId = $('get-album-id').value;
+  var url = $('json-rpc-url').value + '?' + $('param').value;
+  var id = [];
+  if (albumId) {
+    id = albumId.split(',');
+  }
+  data['params']['albumId'] = id;
+  var startIndex = $('get-start-index').value;
+  if (startIndex) {
+    data['params']['startIndex'] = startIndex;
+  }
+  var count = $('get-count').value;
+  if (count) {
+    data['params']['count'] = count;
+  }
+  sendRequest(url, 'POST', data);
+}
+
+function restDelete() {
+  var albumId = $('delete-album-id').value;
+  var url = $('rest-url').value + '/' + albumId + '?' + $('param').value;
+  sendRequest(url, 'DELETE', null);
+}
+
+function jsonDelete() {
+  var albumId = $('delete-album-id').value;
+  var ids = albumId.split(',');
+  var data = {};
+  data['method'] = 'albums.delete';
+  data['params'] = {'albumId': ids};
+  var url = $('json-rpc-url').value + '?' + $('param').value;
+  sendRequest(url, 'POST', data);
+}
+
+function restUpdate() {
+  var title = $('update-title');
+  var desc = $('update-description');
+  var data = {'title': title.value, 'description': desc.value, 'mediaType': 'IMAGE', 'location':{'latitude':100, 'longitude':200}};
+  var albumId = $('update-album-id').value;
+  var url = $('rest-url').value + '/' + albumId + '?' + $('param').value;
+  sendRequest(url, 'PUT', data);
+}
+
+function jsonUpdate() {
+  var title = $('update-title');
+  var desc = $('update-description');
+  var data = {};
+  data['method'] = 'albums.update';
+  data['params'] = {'album' : {'title': title.value, 'description': desc.value, 'mediaType': 'IMAGE', 'location':{'latitude':100, 'longitude':200}}};
+  data['params']['userId'] = '@me';
+  data['params']['groupId'] = '@self';
+  data['id'] = 'updateAlbum';
+  data['params']['albumId'] = $('update-album-id').value;
+  var url = $('json-rpc-url').value + '?' + $('param').value;
+  sendRequest(url, 'POST', data);
+}
+
+function init() {
+  gadgets.window.adjustHeight();
+}
+
+gadgets.util.registerOnLoadHandler(init);
+    </script>
+      <p>The gadget is used to test the create/update/delete/get albums
+        functionality via the REST and JSON-RPC api.<br/> Please use the firebug to
+        check the request and the response. </p>
+
+      <div>
+        REST URL: <input id="rest-url" style="margin-left:40px" type="text" size=60 value="http://shindig/social/rest/albums/@me/@self"><br/>
+          JSON-RPC URL: <input id="json-rpc-url" type="text" size=60 value="http://shindig/social/rpc"/><br/>
+        Param: <input id="param" style="margin-left:70px" type="text" size=60 value="st=1:1:1:partuza:test.com:1:0"/>
+      </div>
+      <p><b>Create the album</b></p>
+      Title:<input id="create-title" type="text" style="margin-left:50px" size=60 value="default album title"/><br/>
+      Description:<input id="create-description" type="text" size=60 value="the description of the create album"/><br/>
+      <input type="submit" value="REST" onclick=restCreate() />
+      <input type="submit" value="JSON-RPC" onclick=jsonCreate() />
+
+      <p><b>Get the album.</b> </p>
+      Album Id:<input id="get-album-id" size=3 type="text"/>
+      startIndex:<input id="get-start-index" size=3 type="text"/>
+      count:<input id="get-count" size=3 type="text"/>
+
+      <input type="submit" value="REST" onclick=restGet() />
+      <input type="submit" value="JSON-RPC" onclick=jsonGet() /><br/>
+
+      <p><b>Update the album</b></p>
+      Album Id:<input id="update-album-id" size=5 type="text"/><br/>
+      Title:<input id="update-title" type="text" style="margin-left:50px" size=60 value="updated album title"/><br/>
+      Description:<input id="update-description" type="text" size=60 value="updated description"/><br/>
+      <input type="submit" value="REST" onclick=restUpdate() />
+      <input type="submit" value="JSON-RPC" onclick=jsonUpdate() />
+
+      <p><b>Delete the album</b><p>
+      Album Id: <input id="delete-album-id" size=5 type="text"/><br/>
+      <input type="submit" value="REST" onclick=restDelete() />
+      <input type="submit" value="JSON-RPC" onclick=jsonDelete() />
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/php/test/misc/feather.png b/trunk/php/test/misc/feather.png
new file mode 100644
index 0000000..6569333
--- /dev/null
+++ b/trunk/php/test/misc/feather.png
Binary files differ
diff --git a/trunk/php/test/misc/invalidation/count.php b/trunk/php/test/misc/invalidation/count.php
new file mode 100644
index 0000000..c4e14a2
--- /dev/null
+++ b/trunk/php/test/misc/invalidation/count.php
@@ -0,0 +1,36 @@
+<?php
+namespace apache\shindig\test\misc\invalidation;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+@date_default_timezone_set(@date_default_timezone_get());
+
+$filename = sys_get_temp_dir() . '/shindig_test_misc_invalidation_count';
+
+if (file_exists($filename)) {
+  $count = file_get_contents($filename);
+} else {
+  touch($filename);
+  $count = 0;
+}
+
+$count += 1;
+echo "Count: $count at time: " . date('Y-m-d H:i:s');
+
+file_put_contents($filename, $count);
diff --git a/trunk/php/test/misc/invalidation/invalidation.xml b/trunk/php/test/misc/invalidation/invalidation.xml
new file mode 100644
index 0000000..0a216a9
--- /dev/null
+++ b/trunk/php/test/misc/invalidation/invalidation.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="invalidation" author_email="panjie.pan@gmail.com">
+    <Require feature="dynamic-height"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+      <script type="text/javascript">
+
+function pre_rest() {
+  var content = {}
+  content.type = 'invalidation';
+  content.url = document.getElementById("restUrl").value;
+  content.postdata = stringify();
+  content.key = document.getElementById("consumerKey").value;
+  content.secret = document.getElementById("consumerSecret").value;
+  var params = {};
+  params[gadgets.io.RequestParameters.POST_DATA] = "data=" + gadgets.json.stringify(content);
+  params[gadgets.io.RequestParameters.METHOD] = gadgets.io.MethodType.POST;
+  signUrl = document.getElementById("signUrl").value;
+  gadgets.io.makeRequest(signUrl, rest, params);
+}
+
+function rest(obj) {
+  var postdata = stringify();
+  var xhr = new window.XMLHttpRequest();
+  var data = gadgets.json.parse(obj.data);
+  var url = data.url;
+  xhr.open("POST", url, true);
+  xhr.setRequestHeader("Content-type", "application/json");
+  xhr.send(postdata);
+}
+
+function jsonrpc() {
+  var invalidationKeys = "@viewer";
+  var postdata = stringify();
+  var st = shindig.auth.getSecurityToken();
+  var xhr = new window.XMLHttpRequest();
+  var url = [document.getElementById("rpcUrl").value];
+  url.push("?st=");
+  url.push(encodeURIComponent(st));
+  xhr.open("POST", url.join(""), true);
+  xhr.setRequestHeader("Content-type", "application/json");
+  var jsonRpc = {}
+  jsonRpc.method = "cache.invalidate";
+  jsonRpc.params = {}
+  jsonRpc.params.invalidationKeys = invalidationKeys.split("\n");
+  xhr.send(gadgets.json.stringify(jsonRpc));
+}
+
+function stringify() {
+  var invalidationKeys = document.getElementById("invalidationKeys").value;
+  var result = {};
+  result.invalidationKeys = invalidationKeys.split("\n");
+  return gadgets.json.stringify(result);
+}
+
+function makeRequest() {
+  var url = document.getElementById("makeRequestUrl").value;
+  var params = {};
+  params[gadgets.io.RequestParameters.AUTHORIZATION] = gadgets.io.AuthorizationType.SIGNED;
+  gadgets.io.makeRequest(url, makeRequestCallback, params);
+}
+
+function makeRequestCallback(obj) {
+  document.getElementById("makeRequestResult").value = obj.data;
+}
+
+function init() {
+  gadgets.window.adjustHeight();
+}
+
+gadgets.util.registerOnLoadHandler(init);
+      </script>
+    <div>
+    makeRequest url:<br>
+    <textarea style="width:600px;" id="makeRequestUrl">http://shindig/test/misc/invalidation/count.php</textarea><br>
+    <input type="button" value="makeRequest" onClick="makeRequest()"><br>
+    makeRequest result:<br>
+    <textarea style="width:600px;" id="makeRequestResult"></textarea><br>
+    <br><br><br>
+
+    rest url:<br>
+    <textarea style="width:600px;" id="restUrl">http://shindig/gadgets/api/rest/cache/invalidate</textarea><br>
+    rpc url:<br>
+    <textarea style="width:600px;" id="rpcUrl">http://shindig/gadgets/api/rpc</textarea><br>
+    invalidationKeys: <br>
+    <textarea style="width:600px;" id="invalidationKeys"></textarea><br>
+    sign url: <br>
+    <textarea style="width:600px;" id="signUrl">http://shindig/test/misc/sign.php</textarea><br>
+    oauth consumer key: <br>
+    <textarea style="width:600px;" id="consumerKey"></textarea><br>
+    oauth consumer secret: <br>
+    <textarea style="width:600px;" id="consumerSecret"></textarea><br>
+    </div>
+    <input type="button" value="rest invalidate" onClick="pre_rest()">
+    <input type="button" value="jsonrpc invalidate" onClick="jsonrpc()">
+    <div id="main">
+        <div id="message"></div>
+        <div id="logging"></div>
+    </div>
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/php/test/misc/ok.html b/trunk/php/test/misc/ok.html
new file mode 100644
index 0000000..d86bac9
--- /dev/null
+++ b/trunk/php/test/misc/ok.html
@@ -0,0 +1 @@
+OK
diff --git a/trunk/php/test/misc/rewriter1.css b/trunk/php/test/misc/rewriter1.css
new file mode 100644
index 0000000..07a05f5
--- /dev/null
+++ b/trunk/php/test/misc/rewriter1.css
@@ -0,0 +1,6 @@
+#backgrdiv {
+  background-image: url("feather.png");
+  background-repeat: no-repeat;
+  background-position: 20%;
+  border: 2px red solid;
+}
diff --git a/trunk/php/test/misc/rewriter1.js b/trunk/php/test/misc/rewriter1.js
new file mode 100644
index 0000000..d23cf72
--- /dev/null
+++ b/trunk/php/test/misc/rewriter1.js
@@ -0,0 +1 @@
+document.getElementById('jstarget1').innerHTML = "This content was loaded from rewriter1.js";
\ No newline at end of file
diff --git a/trunk/php/test/misc/rewriter2.css b/trunk/php/test/misc/rewriter2.css
new file mode 100644
index 0000000..516a333
--- /dev/null
+++ b/trunk/php/test/misc/rewriter2.css
@@ -0,0 +1,6 @@
+#backgrdiv2 {
+  background-image: url("feather.png");
+  background-repeat: no-repeat;
+  background-position: 20%;
+  border: 2px blue solid;
+}
diff --git a/trunk/php/test/misc/rewriter2.js b/trunk/php/test/misc/rewriter2.js
new file mode 100644
index 0000000..fe2d767
--- /dev/null
+++ b/trunk/php/test/misc/rewriter2.js
@@ -0,0 +1 @@
+document.getElementById('jstarget2').innerHTML= "This content was loaded from rewriter2.js";
\ No newline at end of file
diff --git a/trunk/php/test/misc/sampleAtomFeed.xml b/trunk/php/test/misc/sampleAtomFeed.xml
new file mode 100644
index 0000000..a5807a0
--- /dev/null
+++ b/trunk/php/test/misc/sampleAtomFeed.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+  <title>Example Feed</title>
+  <subtitle>A subtitle.</subtitle>
+  <link href="http://example.org/feed/" rel="self" />
+  <link href="http://example.org/" />
+  <id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
+  <updated>2009-01-03T18:30:02Z</updated>
+  <author>
+    <name>John Doe</name>
+    <email>johndoe@example.com</email>
+  </author>
+  <entry>
+    <title>Atom-Powered Robots Run Amok</title>
+    <link href="http://example.org/entry1" />
+    <id>urn:uuid:11111111-1111-1111-1111-111111111111</id>
+    <updated>2009-01-01T18:30:02Z</updated>
+    <summary>This is the first sample atom entry.</summary>
+  </entry>
+  <entry>
+    <title>Atom-Powered Robots Now OK</title>
+    <link href="http://example.org/entry2" />
+    <id>urn:uuid:22222222-2222-2222-2222-222222222222</id>
+    <updated>2009-01-02T18:30:02Z</updated>
+    <summary>This is the second sample atom entry.</summary>
+  </entry>
+  <entry>
+    <title>Atom Feed Samples Interesting, Scientists Say</title>
+    <link href="http://example.org/entry3" />
+    <id>urn:uuid:333333333-3333-3333-3333-333333333333</id>
+    <updated>2009-01-03T18:30:02Z</updated>
+    <summary>This is the third sample atom entry.</summary>
+  </entry>
+</feed>
diff --git a/trunk/php/test/misc/sign.php b/trunk/php/test/misc/sign.php
new file mode 100644
index 0000000..5fe4823
--- /dev/null
+++ b/trunk/php/test/misc/sign.php
@@ -0,0 +1,53 @@
+<?php
+namespace apache\shindig\test\misc;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+$data = json_decode(stripslashes(stripslashes($_POST['data'])));
+
+switch ($data->type) {
+  case 'activity':
+    $consumer = new \OAuthConsumer($data->key, $data->secret);
+    $signature_method = new \OAuthSignatureMethod_HMAC_SHA1();
+    $params = array();
+    $params['oauth_consumer_key'] = $data->key;
+    $params['startIndex'] = $data->first;
+    $params['count'] = $data->max;
+    $http_query = http_build_query($params);
+    $oauth_request = \OAuthRequest::from_consumer_and_token($consumer, null, 'GET', $data->url, $params);
+    $oauth_request->sign_request($signature_method, $consumer, null);
+
+    $result = $oauth_request->to_url();
+    header('ContentType: application/json');
+    echo '{"url" : "' . $result . '"}';
+  break;
+  case 'invalidation':
+    $consumer = new \OAuthConsumer($data->key, $data->secret);
+    $signature_method = new \OAuthSignatureMethod_HMAC_SHA1();
+    $params = array();
+    $params['oauth_body_hash'] = base64_encode(sha1(stripslashes($data->postdata), true));
+    $params['oauth_consumer_key'] = $data->key;
+    $oauth_request = \OAuthRequest::from_consumer_and_token($consumer, null, 'POST', $data->url, $params);
+    $oauth_request->sign_request($signature_method, $consumer, null);
+
+    $result = $oauth_request->to_url();
+    header('ContentType: application/json');
+    echo '{"url" : "' . $result . '"}';
+  break;
+}
diff --git a/trunk/php/test/misc/testGadget.xml b/trunk/php/test/misc/testGadget.xml
new file mode 100644
index 0000000..6db952c
--- /dev/null
+++ b/trunk/php/test/misc/testGadget.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+	<ModulePrefs title="example title" author="example author"
+		author_email="test@example.org" description="description"
+		directory_title="example directory title" screenshot="http://example.org/screenshot.gif"
+		thumbnail="http://example.org/thumbnail.gif" title_url="http://example.org"
+		author_affiliation="example org" author_location="example location"
+		author_photo="example photo" author_aboutme="example about me"
+		author_quote="example quote" author_link="example link" show_stats="true"
+		show_in_directory="true" width="200" height="100" category="example category"
+		category2="example category2" singleton="true" render_inline="true"
+		scaling="true" scrolling="false">
+		
+		<Icon>http://example.org/favicon.ico</Icon>
+		
+		<Require feature="dynamic-height" />
+		<Require feature="views" />
+		<Require feature="tabs" />
+		<Require feature="flash" />
+		<Require feature="setprefs" />
+		<Optional feature="missing-feature" />
+
+        <Optional feature="content-rewrite">
+	        <Param name="expires">86400</Param>
+	        <Param name="include-url">*</Param>
+	        <Param name="exclude-url">.jpeg</Param>
+	        <Param name="minify-css">true</Param>
+	        <Param name="minify-js">true</Param>
+	        <Param name="minify-html">true</Param>
+        </Optional>
+
+	    <OAuth>
+	      <Service name="google">
+	        <Access url="https://www.google.com/accounts/OAuthGetAccessToken" method="GET" /> 
+	        <Request url="https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www.google.com/m8/feeds/" method="GET" /> 
+	        <Authorization url="https://www.google.com/accounts/OAuthAuthorizeToken?oauth_callback=http://oauth.gmodules.com/gadgets/oauthcallback" /> 
+	      </Service>
+	    </OAuth>
+
+		<Locale messages="http://www.labpixies.com/campaigns/todo/i19/all_all.xml"/>
+		<Locale lang="nl" messages="http://www.labpixies.com/campaigns/todo/i19/nl_all.xml"/>
+		<Locale lang="nl" country="BE" messages="http://www.labpixies.com/campaigns/todo/i19/nl_be.xml"/>
+		<Locale lang="en" county="US">
+			<messagebundle>
+				<msg name="foo">foo</msg>
+				<msg name="bar">bar</msg>
+			</messagebundle>
+		</Locale>
+		
+		<Preload href='http://test.chabotc.com/ok.html'/>
+		
+	</ModulePrefs>
+	
+	<UserPref name='testEnum' display_name='testEnum' datatype='enum' default_value='foo'>
+		<EnumValue value='foo' display_value='foo' />
+		<EnumValue value='bar' display_value='bar' />
+	</UserPref>
+	<UserPref name='testString' display_name='testString' datatype='string' default_value='testString' required='true' />
+	<UserPref name='testBool' display_name='testBool' datatype='bool' default_value='false' />
+	<UserPref name='testHidden' display_name='testHidden' datatype='hidden' default_value='testHidden' />
+	<UserPref name="__MSG_title__" display_name="__MSG_title__" datatype="string" default_value="__MSG_title__" />
+	
+	<link rel="event" href="http://www.example.com/pingme" method="POST" />
+	<link rel="event.addapp" href="http://www.example.com/add" />
+	<link rel="event.removeapp" href="http://www.example.com/remove" />
+	
+	<Content type="html" view="profile" prefered_height="400" prefered_width="300" quirks="true"><![CDATA[
+<style type="text/css"> @import url( rewriter1.css ); </style>
+<link rel="stylesheet" type="text/css" href="rewriter2.css"/>
+<p>A simple gadget to demonstrate the content rewriter</p>
+<div>
+  This is a URL in content that was not rewritten http://www.notrewritten.com
+</div>
+<div id="backgrdiv">
+  This div has a background <br/> image from imported CSS
+</div>
+<div id="backgrdiv2">
+  This div has a background <br/> image from linked CSS
+</div>
+<p> This <img id="rewriteimg" src="feather.png" alt="If you can read this there is a problem"/> is an image tag that was rewritten</p>
+<p id="jstarget1">If you can read this there is a problem</p>
+<p id="jstarget2">If you can read this there is a problem</p>
+<script type="text/javascript" src="rewriter1.js"></script>
+<script type="text/javascript" src="rewriter2.js"></script>
+<script>
+gadgets.window.adjustHeight();
+</script>
+]]></Content>
+	
+	<Content type="url" view="canvas" href="http://example.com/foo.html" prefered_height="400" prefered_width="300" quirks="true"></Content>
+	
+	<Content view="home" href="http://www.example.com/myapp/home/index.html"></Content>
+	
+</Module>
diff --git a/trunk/php/test/misc/testRest.php b/trunk/php/test/misc/testRest.php
new file mode 100644
index 0000000..e1b0004
--- /dev/null
+++ b/trunk/php/test/misc/testRest.php
@@ -0,0 +1,336 @@
+#!/usr/bin/php -Cq
+<?php
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Quick and dirty REST API test script that excersises the basic REST calls.
+ */
+
+/* Modify this if you want to test in a different social graph then partuza (user id 1 = me on in my local db).
+ * the security token only works if ALLOW_PLAINTEXT_TOKEN is set to true in the php-shindig config
+ * format of the plain text token is owner:viewer:appid:container:url:modid
+ */
+$securityToken = '1:1:1:partuza:test.com:0';
+// The server to test against
+$restUrl = 'http://shindig/social/rest';
+
+function curlRest($url, $postData, $contentType, $method = 'POST') {
+  global $securityToken, $restUrl;
+  $ch = curl_init();
+  if (substr($url, 0, 1) != '/') {
+    $url = '/' . $url;
+  }
+  $sep = strpos($url, '?') !== false ? '&' : '?';
+  curl_setopt($ch, CURLOPT_URL, $restUrl . $url . $sep . 'st=' . $securityToken);
+  curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: $contentType"));
+  curl_setopt($ch, CURLOPT_HEADER, 0);
+  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
+  curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
+  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+  $ret = curl_exec($ch);
+  curl_close($ch);
+  return $ret;
+}
+
+/* -- NOT_IMPLEMENTED in partuza and the sample json db, so skipping this by default
+// ************** Set App Data using XML post payload ********************** //
+$postData = '<entry xmlns="http://www.w3.org/2005/Atom"
+         xmlns:osapi="http://opensocial.org/2008/opensocialapi">
+  <osapi:recipient>1</osapi:recipient>
+  <osapi:recipient>2</osapi:recipient>
+  <osapi:recipient>3</osapi:recipient>
+  <title>You have an invitation from Joe</title>
+  <id>{msgid}</id>
+  <link rel="alternate" href="http://app.example.org/invites/{msgid}"/>
+  <content>Click <a href="http://app.example.org/invites/{msgid}">here</a> to review your invitation.</content>
+</entry>';
+echo "Sending a message via POST.. ";
+$ret = curlRest('/messages/1/outbox', $postData, 'application/atom+xml', 'POST');
+if (! empty($ret)) {
+	echo "FAILURE:\n[$ret]\n";
+	die();
+} else {
+	echo "OK\n";
+}
+
+*/
+
+// ************** Set App Data using XML post payload ********************** //
+$postData = '<appdata xmlns="http://ns.opensocial.org/2008/opensocial">
+  <entry>
+    <key>pokes</key>
+    <value>1</value>
+  </entry>
+  <entry>
+    <key>last_poke</key>
+    <value>2008-02-13T18:30:02Z</value>
+  </entry>
+</appdata>';
+echo "Setting pokes and last_poke app data using XML.. ";
+$ret = curlRest('/appdata/1/@self/1', $postData, 'application/xml');
+if (! empty($ret)) {
+  echo "FAILURE:\n[$ret]\n";
+  die();
+} else {
+  echo "OK\n";
+  // verify data was written correctly
+  echo "Verifying XML set app data.. ";
+  $ret = curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'GET');
+  $retDecoded = json_decode($ret, true);
+  if ($ret == $retDecoded) {
+    die("Invalid json string in return: $ret\n");
+  }
+  if (isset($retDecoded['entry']) && isset($retDecoded['entry'][1]) && isset($retDecoded['entry'][1]['last_poke']) && isset($retDecoded['entry'][1]['pokes']) && $retDecoded['entry'][1]['last_poke'] == '2008-02-13T18:30:02Z' && $retDecoded['entry'][1]['pokes'] == '1') {
+    echo "OK\n";
+  } else {
+    echo "FAILURE, unexpected return value: $ret\n";
+    die();
+  }
+}
+
+// ************** Set App Data using ATOM post payload ********************** //
+$postData = '<entry xmlns="http://www.w3.org/2005/Atom">
+  <content type="text/xml">
+    <appdata xmlns="http://opensocial.org/2008/opensocial">  
+        <pokes>2</pokes>
+        <last_poke>2003-12-14T18:30:02Z</last_poke>
+      </appdata>
+  </content>
+  <title/>
+  <updated>2003-12-14T18:30:02Z</updated>
+  <author><url>urn:guid:example.org:34KJDCSKJN2HHF0DW20394</url></author>
+  <id>urn:guid:example.org:34KJDCSKJN2HHF0DW20394</id>
+</entry>';
+echo "Setting pokes and last_poke app data using Atom.. ";
+$ret = curlRest('/appdata/1/@self/1', $postData, 'application/atom+xml');
+if (! empty($ret)) {
+  echo "FAILURE:\n$ret\n\n";
+  die();
+} else {
+  echo "OK\n";
+  // verify data was written correctly
+  echo "Verifying Atom set app data.. ";
+  $ret = curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'GET');
+  $retDecoded = json_decode($ret, true);
+  if ($ret == $retDecoded) {
+    die("Invalid json string in return: $ret\n");
+  }
+  if (isset($retDecoded['entry']) && isset($retDecoded['entry'][1]) && isset($retDecoded['entry'][1]['last_poke']) && isset($retDecoded['entry'][1]['pokes']) && $retDecoded['entry'][1]['last_poke'] == '2003-12-14T18:30:02Z' && $retDecoded['entry'][1]['pokes'] == '2') {
+    echo "OK\n";
+  } else {
+    echo "FAILURE, unexpected return value: $ret\n";
+    die();
+  }
+}
+
+// ************** Set App Data using JSON post payload ********************** //
+$postData = '{
+  "pokes" : 4,
+  "last_poke" : "2008-06-13T18:30:02Z"
+}';
+echo "Setting pokes and last_poke app data using JSON.. ";
+$ret = curlRest('/appdata/1/@self/1', $postData, 'application/json');
+if (! empty($ret)) {
+  echo "FAILURE:\n$ret\n\n";
+  die();
+} else {
+  echo "OK\n";
+  // verify data was written correctly
+  echo "Verifying Atom set app data.. ";
+  $ret = curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'GET');
+  $retDecoded = json_decode($ret, true);
+  if ($ret == $retDecoded) {
+    die("Invalid json string in return: $ret\n");
+  }
+  if (isset($retDecoded['entry']) && isset($retDecoded['entry'][1]) && isset($retDecoded['entry'][1]['last_poke']) && isset($retDecoded['entry'][1]['pokes']) && $retDecoded['entry'][1]['last_poke'] == '2008-06-13T18:30:02Z' && $retDecoded['entry'][1]['pokes'] == '4') {
+    echo "OK\n";
+  } else {
+    echo "FAILURE, unexpected return value: $ret\n";
+    die();
+  }
+}
+
+// ************** Delete app data ********************** //
+echo "Deleting app data.. ";
+$ret = curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'DELETE');
+if (! empty($ret)) {
+  echo "FAILURE:\n$ret\n";
+  die();
+} else {
+  echo "OK\n";
+}
+
+// ************** Create Activity using JSON post payload ********************** //
+echo "Creating activity using JSON.. ";
+$randomTitle = "[" . rand(0, 2048) . "] test activity";
+$postData = '{
+  "id" : "http://example.org/activities/example.org:87ead8dead6beef/self/af3778",
+  "title" : "' . $randomTitle . '",
+  "updated" : "2008-02-20T23:35:37.266Z",
+  "body" : "Some details for some activity",
+  "bodyId" : "383777272",
+  "url" : "http://api.example.org/activity/feeds/.../af3778",
+  "userId" : "example.org:34KJDCSKJN2HHF0DW20394"
+}';
+$ret = curlRest('/activities/1/@self', $postData, 'application/json');
+if (! empty($ret)) {
+  echo "FAILURE:\n$ret";
+  die();
+} else {
+  echo "OK\n";
+  // verify data was written correctly
+  echo "Verifying JSON created activity.. ";
+  $ret = curlRest('/activities/1/@self?count=20', '', 'application/json', 'GET');
+  $retDecoded = json_decode($ret, true);
+  if ($ret == $retDecoded) {
+    die("Invalid json string in return: $ret\n");
+  }
+  $found = false;
+  // see if we can find our just created activity
+  if (isset($retDecoded['entry'])) {
+    foreach ($retDecoded['entry'] as $entry) {
+      if ($entry['title'] == $randomTitle) {
+        $found = true;
+        $activityId = $entry['id'];
+        break;
+      }
+    }
+    echo "OK\n";
+  }
+  if (! $found) {
+    echo "FAILURE, couldn't find activity, or unexpected return value: $ret\n";
+    die();
+  } else {
+    echo "Deleting created activity..";
+    $ret = curlRest("/activities/1/@self/@app/$activityId", '', 'application/json', 'DELETE');
+    if (! empty($ret)) {
+      die("FAILED\n");
+    } else {
+      echo "OK\n";
+    }
+  }
+}
+
+// ************** Create Activity using XML post payload ********************** //
+echo "Creating activity using XML.. ";
+$randomTitle = "[" . rand(0, 2048) . "] test activity";
+$postData = '<activity xmlns="http://ns.opensocial.org/2008/opensocial">
+  <id>http://example.org/activities/example.org:87ead8dead6beef/self/af3778</id>
+  <title>' . $randomTitle . '</title>
+  <updated>2008-02-20T23:35:37.266Z</updated>
+  <body>Some details for some activity</body>
+  <bodyId>383777272</bodyId>
+  <url>http://api.example.org/activity/feeds/.../af3778</url>
+  <userId>example.org:34KJDCSKJN2HHF0DW20394</userId>
+</activity>';
+$ret = curlRest('/activities/1/@self', $postData, 'application/xml');
+if (! empty($ret)) {
+  echo "FAILURE:\n$ret";
+  die();
+} else {
+  echo "OK\n";
+  // verify data was written correctly
+  echo "Verifying XML created activity.. ";
+  $ret = curlRest('/activities/1/@self?count=4', '', 'application/json', 'GET');
+  $retDecoded = json_decode($ret, true);
+  if ($ret == $retDecoded) {
+    die("Invalid json string in return: $ret\n");
+  }
+  $found = false;
+  // see if we can find our just created activity
+  if (isset($retDecoded['entry'])) {
+    foreach ($retDecoded['entry'] as $entry) {
+      if ($entry['title'] == $randomTitle) {
+        $found = true;
+        $activityId = $entry['id'];
+        break;
+      }
+    }
+    echo "OK\n";
+  }
+  if (! $found) {
+    echo "FAILURE, couldn't find activity, or unexpected return value: $ret\n";
+    die();
+  } else {
+    echo "Deleting created activity..";
+    $ret = curlRest("/activities/1/@self/@app/$activityId", '', 'application/json', 'DELETE');
+    if (! empty($ret)) {
+      die("FAILED\n");
+    } else {
+      echo "OK\n";
+    }
+  }
+}
+
+// ************** Create Activity using Atom post payload ********************** //
+echo "Creating activity using Atom.. ";
+$randomTitle = "[" . rand(0, 2048) . "] test activity";
+$postData = '<entry xmlns="http://www.w3.org/2005/Atom">
+  <category term="status"/>
+  <id>http://example.org/activities/example.org:87ead8dead6beef/self/af3778</id>
+  <title>' . $randomTitle . '</title>
+  <summary>Some details for some activity</summary>
+  <updated>2008-02-20T23:35:37.266Z</updated>
+  <link rel="self" type="application/atom+xml" href="http://api.example.org/activity/feeds/.../af3778"/>
+  <author><uri>urn:guid:example.org:34KJDCSKJN2HHF0DW20394</uri></author>
+  <content>
+    <activity xmlns="http://ns.opensocial.org/2008/opensocial">
+      <bodyId>383777272</bodyId>
+    </activity>
+  </content>
+</entry>';
+$ret = curlRest('/activities/1/@self', $postData, 'application/atom+xml');
+if (! empty($ret)) {
+  echo "FAILURE:\n$ret";
+  die();
+} else {
+  echo "OK\n";
+  // verify data was written correctly
+  echo "Verifying Atom created activity.. ";
+  $ret = curlRest('/activities/1/@self?count=4', '', 'application/json', 'GET');
+  $retDecoded = json_decode($ret, true);
+  if ($ret == $retDecoded) {
+    die("Invalid json string in return: $ret\n");
+  }
+  $found = false;
+  // see if we can find our just created activity
+  if (isset($retDecoded['entry'])) {
+    foreach ($retDecoded['entry'] as $entry) {
+      if ($entry['title'] == $randomTitle) {
+        $found = true;
+        $activityId = $entry['id'];
+        break;
+      }
+    }
+    echo "OK\n";
+  }
+  if (! $found) {
+    echo "FAILURE, couldn't find activity, or unexpected return value: $ret\n";
+    die();
+  } else {
+    echo "Deleting created activity..";
+    $ret = curlRest("/activities/1/@self/@app/$activityId", '', 'application/json', 'DELETE');
+    if (! empty($ret)) {
+      die("FAILED: $ret\n");
+    } else {
+      echo "OK\n";
+    }
+  }
+}
diff --git a/trunk/php/test/misc/upload/upload.php b/trunk/php/test/misc/upload/upload.php
new file mode 100644
index 0000000..bbe0046
--- /dev/null
+++ b/trunk/php/test/misc/upload/upload.php
@@ -0,0 +1,51 @@
+<?php
+namespace apache\shindig\test\misc\upload;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Post the file to the specified REST api endpoint. It's used to test the REST 
+ * api for content upload.
+ */
+function curlRest($url, $fileName, $contentType) {
+  $fp = fopen($fileName, 'r');
+  $fileSize = filesize($fileName);
+
+  $ch = curl_init();
+  curl_setopt($ch, CURLOPT_URL, $url);
+  curl_setopt($ch, CURLOPT_HTTPHEADER, array("Content-Type: $contentType", "Expect:"));
+  curl_setopt($ch, CURLOPT_HEADER, 0);
+  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
+  curl_setopt($ch, CURLOPT_UPLOAD, 1);
+
+  curl_setopt($ch, CURLOPT_INFILESIZE, $fileSize);
+  curl_setopt($ch, CURLOPT_INFILE, $fp);
+  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
+  $ret = curl_exec($ch);
+  curl_close($ch);
+  return $ret;
+}
+
+// The title is 'opensocial' and the description is 'icon'.
+$url = "http://shindig/social/rest/mediaitems/@me/@self/1?st=1:1:1:partuza:test.com:1:0&mediaType=IMAGE&title=opensocial&description=icon";
+// Create a media item with a image file uploaded. 
+$ret = curlRest($url, "icon.jpg", "image/jpg");
+
+var_dump($ret);
diff --git a/trunk/php/test/misc/upload/upload.xml b/trunk/php/test/misc/upload/upload.xml
new file mode 100644
index 0000000..617cc6d
--- /dev/null
+++ b/trunk/php/test/misc/upload/upload.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<Module>
+  <ModulePrefs title="Content upload">
+    <Require feature="dynamic-height"/>
+  </ModulePrefs>
+  <Content type="html">
+    <![CDATA[
+    <script type="text/javascript">
+
+function initData() {
+  var jsonInput = document.getElementById("json-input");
+  jsonInput.value = ' [ { "method":"mediaitems.create", "params": {"albumId":"1", "mediaItem": { "id" : "11223344", "thumbnailUrl" : "http://www.libpng.org/pub/png/img_png/pngnow.png", "mimeType" : "image/png", "type" : "image", "url" : "@field:1.jpg", "albumId" : "1" } } } ]';
+  gadgets.window.adjustHeight();
+}
+
+
+function restDelete() {
+  var restUrl = document.getElementById("rest-delete-url");
+  var xhr = new window.XMLHttpRequest();
+  xhr.open("DELETE", restUrl.value, true);
+  xhr.setRequestHeader("Content-type", "application/json");
+  xhr.send(null);
+}
+
+function restGet() {
+  var restUrl = document.getElementById("rest-get-url");
+  var xhr = new window.XMLHttpRequest();
+  xhr.open("GET", restUrl.value, true);
+  xhr.setRequestHeader("Content-type", "application/json");
+  xhr.send(null);
+}
+
+gadgets.util.registerOnLoadHandler(initData);
+    </script>
+
+    please use firebug to see the requests and the responses and copy the created url to browser address bar to fetch the created image.
+    <p>The JSON RPC method.</p>
+    The JSON data that will be posted.
+    <form enctype="multipart/form-data"
+      action="http://shindig/social/rpc?st=1:1:1:partuza:test.com:1:0" method="POST">
+      <textarea id="json-input" cols=100 rows=4 name="request" ></textarea><br/>
+      The image file to upload:
+      <input name="uploadedfile" type="file" /><br />
+      <input type="submit" value="JsonRpc create media item." />
+    </form>
+    <br/>
+    <p> The REST method.</p>
+    <form enctype="multipart/form-data" action="http://shindig/social/rest/mediaitems/@me/@self/1?st=1:1:1:partuza:test.com:1:0&mediaType=IMAGE&title=mobile&description=g1" method="POST">
+      Choose an image file to upload:
+      <input type="file" name="uploadedfile" />
+      <input type="submit" value="Rest create media item." />
+    </form>
+    <p> REST delete </p>
+    The url: <input id="rest-delete-url" size=60 type="text" value="http://shindig/social/rest/mediaitems/@me/@self/1/64?st=1:1:1:partuza:test.com:1:0" />
+    <input type="button" onclick="restDelete()" value="delete"/>
+
+    <p> REST get </p>
+    The url: <input id="rest-get-url" size=60 type="text" value="http://shindig/social/rest/mediaitems/@me/@self/1/64?st=1:1:1:partuza:test.com:1:0" />
+    <input type="button" onclick="restGet()" value="get"/>
+
+    ]]>
+  </Content>
+</Module>
diff --git a/trunk/php/test/social/ActivityRestTest.php b/trunk/php/test/social/ActivityRestTest.php
new file mode 100644
index 0000000..d507e9f
--- /dev/null
+++ b/trunk/php/test/social/ActivityRestTest.php
@@ -0,0 +1,96 @@
+<?php
+namespace apache\shindig\test\social;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+require_once 'RestBase.php';
+
+class ActivityRestTest extends RestBase {
+
+  private function verifyLifeCycle($postData, $postDataFormat, $randomTitle) {
+    $url = '/activities/1/@self';
+    $ret = $this->curlRest($url, $postData, $postDataFormat);
+    $this->assertTrue(empty($ret), "Create activity failed. Response: $ret");
+
+    // Verifyies data was written correctly
+    $ret = $this->curlRest($url . '?count=20', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json string in return: $ret");
+    // Sees if we can find our just created activity
+    $activityId = null;
+    foreach ($retDecoded['entry'] as $entry) {
+      if ($entry['title'] == $randomTitle) {
+        $activityId = $entry['id'];
+        break;
+      }
+    }
+    $this->assertNotNull($activityId, "Couldn't find created activity.");
+    $ret = $this->curlRest($url . "/@app/$activityId", '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete activity failed. Repsonse: $ret");
+  }
+
+  public function testLifeCycleInJson() {
+    $randomTitle = "[" . rand(0, 2048) . "] test activity";
+    $postData = '{
+      "id" : "http://example.org/activities/example.org:87ead8dead6beef/self/af3778",
+      "title" : "' . $randomTitle . '",
+      "updated" : "2008-02-20T23:35:37.266Z",
+      "body" : "Some details for some activity",
+      "bodyId" : "383777272",
+      "url" : "http://api.example.org/activity/feeds/.../af3778",
+      "userId" : "example.org:34KJDCSKJN2HHF0DW20394"
+    }';
+    $this->verifyLifeCycle($postData, 'application/json', $randomTitle);
+  }
+
+  public function testLifeCycleInAtom() {
+    $randomTitle = "[" . rand(0, 2048) . "] test activity";
+    $postData = '<entry xmlns="http://www.w3.org/2005/Atom">
+        <category term="status"/>
+        <id>http://example.org/activities/example.org:87ead8dead6beef/self/af3778</id>
+        <title>' . $randomTitle . '</title>
+        <summary>Some details for some activity</summary>
+        <updated>2008-02-20T23:35:37.266Z</updated>
+        <link rel="self" type="application/atom+xml" href="http://api.example.org/activity/feeds/.../af3778"/>
+        <author><uri>urn:guid:example.org:34KJDCSKJN2HHF0DW20394</uri></author>
+        <content>
+          <activity xmlns="http://ns.opensocial.org/2008/opensocial">
+            <bodyId>383777272</bodyId>
+          </activity>
+        </content>
+      </entry>';
+    $this->verifyLifeCycle($postData, 'application/atom+xml', $randomTitle);
+  }
+
+  public function testLifeCycleInXml() {
+    $randomTitle = "[" . rand(0, 2048) . "] test activity";
+    $postData = '<activity xmlns="http://ns.opensocial.org/2008/opensocial">
+      <id>http://example.org/activities/example.org:87ead8dead6beef/self/af3778</id>
+      <title>' . $randomTitle . '</title>
+      <updated>2008-02-20T23:35:37.266Z</updated>
+      <body>Some details for some activity</body>
+      <bodyId>383777272</bodyId>
+      <url>http://api.example.org/activity/feeds/.../af3778</url>
+      <userId>example.org:34KJDCSKJN2HHF0DW20394</userId>
+      </activity>';
+    $this->verifyLifeCycle($postData, 'application/xml', $randomTitle);
+  }
+}
+
diff --git a/trunk/php/test/social/ActivityTest.php b/trunk/php/test/social/ActivityTest.php
new file mode 100644
index 0000000..a76cfa9
--- /dev/null
+++ b/trunk/php/test/social/ActivityTest.php
@@ -0,0 +1,209 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Activity;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Activity test case.
+ */
+class ActivityTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Activity
+   */
+  private $Activity;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Activity = new Activity(1, 1);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Activity = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Constructs the test case.
+   */
+  public function __construct() {}
+
+  /**
+   * Tests Activity->__construct()
+   */
+  public function test__construct() {
+    $this->Activity->__construct(1, 1);
+  }
+
+  /**
+   * Tests Activity->getAppId()
+   */
+  public function testGetAppId() {
+    $this->Activity->setAppId(1);
+    $this->assertEquals(1, $this->Activity->getAppId());
+  }
+
+  /**
+   * Tests Activity->getBody()
+   */
+  public function testGetBody() {
+    $testStr = '<b>test <i>me</i></b>';
+    $this->Activity->setBody($testStr);
+    $this->assertEquals($testStr, $this->Activity->getBody());
+  }
+
+  /**
+   * Tests Activity->getBodyId()
+   */
+  public function testGetBodyId() {
+    $bodyId = '123';
+    $this->Activity->setBodyId($bodyId);
+    $this->assertEquals($bodyId, $this->Activity->getBodyId());
+  }
+
+  /**
+   * Tests Activity->getExternalId()
+   */
+  public function testGetExternalId() {
+    $extId = '456';
+    $this->Activity->setExternalId($extId);
+    $this->assertEquals($extId, $this->Activity->getExternalId());
+  }
+
+  /**
+   * Tests Activity->getId()
+   */
+  public function testGetId() {
+    $this->assertEquals(1, $this->Activity->getId());
+  }
+
+  /**
+   * Tests Activity->getMediaItems()
+   */
+  public function testGetMediaItems() {
+    $mediaItems = array('foo' => 'bar');
+    $this->Activity->setMediaItems($mediaItems);
+    $this->assertEquals($mediaItems, $this->Activity->getMediaItems());
+  }
+
+  /**
+   * Tests Activity->getPostedTime()
+   */
+  public function testGetPostedTime() {
+    $time = time();
+    $this->Activity->setPostedTime($time);
+    $this->assertEquals($time, $this->Activity->getPostedTime());
+  }
+
+  /**
+   * Tests Activity->getPriority()
+   */
+  public function testGetPriority() {
+    $priority = 1;
+    $this->Activity->setPriority($priority);
+    $this->assertEquals($priority, $this->Activity->getPriority());
+  }
+
+  /**
+   * Tests Activity->getStreamFaviconUrl()
+   */
+  public function testGetStreamFaviconUrl() {
+    $url = 'http://www.google.com/ig/modules/horoscope_content/virgo.gif';
+    $this->Activity->setStreamFaviconUrl($url);
+    $this->assertEquals($url, $this->Activity->getStreamFaviconUrl());
+  }
+
+  /**
+   * Tests Activity->getStreamSourceUrl()
+   */
+  public function testGetStreamSourceUrl() {
+    $url = 'http://api.example.org/activity/foo/1';
+    $this->Activity->setStreamSourceUrl($url);
+    $this->assertEquals($url, $this->Activity->getStreamSourceUrl());
+  }
+
+  /**
+   * Tests Activity->getStreamTitle()
+   */
+  public function testGetStreamTitle() {
+    $title = 'Foo Activity';
+    $this->Activity->setStreamTitle($title);
+    $this->assertEquals($title, $this->Activity->getStreamTitle());
+  }
+
+  /**
+   * Tests Activity->getStreamUrl()
+   */
+  public function testGetStreamUrl() {
+    $streamUrl = 'http://api.example.org/activityStream/foo/1';
+    $this->Activity->setStreamUrl($streamUrl);
+    $this->assertEquals($streamUrl, $this->Activity->getStreamUrl());
+  }
+
+  /**
+   * Tests Activity->getTemplateParams()
+   */
+  public function testGetTemplateParams() {
+    $params = array('fooParam' => 'barParam');
+    $this->Activity->setTemplateParams($params);
+    $this->assertEquals($params, $this->Activity->getTemplateParams());
+  }
+
+  /**
+   * Tests Activity->getTitle()
+   */
+  public function testGetTitle() {
+    $title = 'Foo Activity Title';
+    $this->Activity->setTitle($title);
+    $this->assertEquals($title, $this->Activity->getTitle());
+  }
+
+  /**
+   * Tests Activity->getTitleId()
+   */
+  public function testGetTitleId() {
+    $titleId = '976';
+    $this->Activity->setTitleId($titleId);
+    $this->assertEquals($titleId, $this->Activity->getTitleId());
+  }
+
+  /**
+   * Tests Activity->getUrl()
+   */
+  public function testGetUrl() {
+    $url = 'http://api.example.org/url';
+    $this->Activity->setUrl($url);
+    $this->assertEquals($url, $this->Activity->getUrl());
+  }
+
+  /**
+   * Tests Activity->getUserId()
+   */
+  public function testGetUserId() {
+    $this->assertEquals(1, $this->Activity->getUserId());
+  }
+}
diff --git a/trunk/php/test/social/AddressTest.php b/trunk/php/test/social/AddressTest.php
new file mode 100644
index 0000000..95fbb38
--- /dev/null
+++ b/trunk/php/test/social/AddressTest.php
@@ -0,0 +1,194 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Address;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Address test case.
+ */
+class AddressTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Address
+   */
+  private $Address;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Address = new Address('UNSTRUCTUREDADDRESS');
+    $this->Address->country = 'COUNTRY';
+    $this->Address->extendedAddress = 'EXTENDEDADDRESS';
+    $this->Address->latitude = 'LATITUDE';
+    $this->Address->longitude = 'LONGITUDE';
+    $this->Address->locality = 'LOCALITY';
+    $this->Address->poBox = 'POBOX';
+    $this->Address->postalCode = 'POSTALCODE';
+    $this->Address->region = 'REGION';
+    $this->Address->streetAddress = 'STREETADDRESS';
+    $this->Address->type = 'TYPE';
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Address = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Address->getCountry()
+   */
+  public function testGetCountry() {
+    $this->assertEquals('COUNTRY', $this->Address->getCountry());
+  }
+
+  /**
+   * Tests Address->getLatitude()
+   */
+  public function testGetLatitude() {
+    $this->assertEquals('LATITUDE', $this->Address->getLatitude());
+  }
+
+  /**
+   * Tests Address->getLocality()
+   */
+  public function testGetLocality() {
+    $this->assertEquals('LOCALITY', $this->Address->getLocality());
+  }
+
+  /**
+   * Tests Address->getLongitude()
+   */
+  public function testGetLongitude() {
+    $this->assertEquals('LONGITUDE', $this->Address->getLongitude());
+  }
+
+  /**
+   * Tests Address->getPostalCode()
+   */
+  public function testGetPostalCode() {
+    $this->assertEquals('POSTALCODE', $this->Address->getPostalCode());
+  }
+
+  /**
+   * Tests Address->getRegion()
+   */
+  public function testGetRegion() {
+    $this->assertEquals('REGION', $this->Address->getRegion());
+  }
+
+  /**
+   * Tests Address->getStreetAddress()
+   */
+  public function testGetStreetAddress() {
+    $this->assertEquals('STREETADDRESS', $this->Address->getStreetAddress());
+  }
+
+  /**
+   * Tests Address->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('TYPE', $this->Address->getType());
+  }
+
+  /**
+   * Tests Address->getFormatted()
+   */
+  public function testGetFormatted() {
+    $this->assertEquals('UNSTRUCTUREDADDRESS', $this->Address->getFormatted());
+  }
+
+  /**
+   * Tests Address->setCountry()
+   */
+  public function testSetCountry() {
+    $this->Address->setCountry('country');
+    $this->assertEquals('country', $this->Address->getCountry());
+  }
+
+  /**
+   * Tests Address->setLatitude()
+   */
+  public function testSetLatitude() {
+    $this->Address->setLatitude('latitude');
+    $this->assertEquals('latitude', $this->Address->getLatitude());
+  }
+
+  /**
+   * Tests Address->setLocality()
+   */
+  public function testSetLocality() {
+    $this->Address->setLocality('locality');
+    $this->assertEquals('locality', $this->Address->getLocality());
+  }
+
+  /**
+   * Tests Address->setLongitude()
+   */
+  public function testSetLongitude() {
+    $this->Address->setLongitude('longitude');
+    $this->assertEquals('longitude', $this->Address->getLongitude());
+  }
+
+  /**
+   * Tests Address->setPostalCode()
+   */
+  public function testSetPostalCode() {
+    $this->Address->setPostalCode('postalcode');
+    $this->assertEquals('postalcode', $this->Address->getPostalCode());
+  }
+
+  /**
+   * Tests Address->setRegion()
+   */
+  public function testSetRegion() {
+    $this->Address->setRegion('religion');
+    $this->assertEquals('religion', $this->Address->getRegion());
+  }
+
+  /**
+   * Tests Address->setStreetAddress()
+   */
+  public function testSetStreetAddress() {
+    $this->Address->setStreetAddress('streetaddress');
+    $this->assertEquals('streetaddress', $this->Address->getStreetAddress());
+  }
+
+  /**
+   * Tests Address->setType()
+   */
+  public function testSetType() {
+    $this->Address->setType('type');
+    $this->assertEquals('type', $this->Address->getType());
+  }
+
+  /**
+   * Tests Address->setFormatted()
+   */
+  public function testSetFormatted() {
+    $this->Address->setFormatted('unstructuredaddress');
+    $this->assertEquals('unstructuredaddress', $this->Address->getFormatted());
+  }
+}
diff --git a/trunk/php/test/social/AlbumRestTest.php b/trunk/php/test/social/AlbumRestTest.php
new file mode 100644
index 0000000..69949d3
--- /dev/null
+++ b/trunk/php/test/social/AlbumRestTest.php
@@ -0,0 +1,113 @@
+<?php
+namespace apache\shindig\test\social;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+require_once 'RestBase.php';
+
+class AlbumRestTest extends RestBase {
+
+  private function verifyLifeCycle($postData, $postDataFormat) {
+    $url = '/albums/1/@self';
+    $ret = $this->curlRest($url, $postData, $postDataFormat);
+    $album = json_decode($ret, true);
+    $album = $album['entry'];
+
+    $ret = $this->curlRest($url . '/' . urlencode($album['id']), '', 'application/json', 'GET');
+    $this->assertFalse(empty($ret));
+    $fetched = json_decode($ret, true);
+    $fetched = $fetched['entry'][0];
+    $this->assertEquals('Example Album', $fetched['title'], "Title should be same.");
+    $this->assertEquals('This is an example album, and this text is an example description', $fetched['description'], "Description should be same.");
+    $this->assertEquals('VIDEO', $fetched['mediaType'], "mediaType should be same.");
+
+    $fetched['thumbnailUrl'] = 'http://changed.com/tn.png';
+    $ret = $this->curlRest($url . '/' . urlencode($album['id']), json_encode($fetched), 'application/json', 'PUT');
+    $ret = $this->curlRest($url . '/' . urlencode($album['id']), '', 'application/json', 'GET');
+    $this->assertFalse(empty($ret));
+    $fetched = json_decode($ret, true);
+    $fetched = $fetched['entry'][0];
+    $this->assertEquals('http://changed.com/tn.png', $fetched['thumbnailUrl'], "thumbnailUrl should be same.");
+    $this->assertEquals('Example Album', $fetched['title'], "Title should be same.");
+    $this->assertEquals('This is an example album, and this text is an example description', $fetched['description'], "Description should be same.");
+    $this->assertEquals('VIDEO', $fetched['mediaType'], "mediaType should be same.");
+
+    $ret = $this->curlRest($url . '/' . urlencode($album['id']), '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete the created album failed. Response: $ret");
+
+    $ret = $this->curlRest($url . '/' . urlencode($album['id']), '', 'application/json', 'GET');
+    $fetched = json_decode($ret, true);
+    $fetched = $fetched['entry'];
+    $this->assertTrue(empty($fetched));
+  }
+
+  public function testLifeCycleInJson() {
+    $postData = '{ "id" : "44332211",
+       "thumbnailUrl" : "http://pages.example.org/albums/4433221-tn.png",
+       "title" : "Example Album",
+       "description" : "This is an example album, and this text is an example description",
+       "location" : { "latitude": 0, "longitude": 0 },
+       "ownerId" : "example.org:55443322",
+       "mediaType" : "VIDEO"
+    }';
+
+    $this->verifyLifeCycle($postData, 'application/json');
+  }
+
+  public function testLifeCycleInXml() {
+    $postData = '<album xmlns="http://ns.opensocial.org/2008/opensocial">
+                   <id>44332211</id>
+                   <thumbnailUrl>http://pages.example.org/albums/4433221-tn.png</thumbnailUrl>
+                   <caption>Example Album</caption>
+                   <description>This is an example album, and this text is an example description</description>
+                   <location>
+                     <latitude>0</latitude>
+                     <longitude>0</longitude>
+                   </location>
+                   <ownerId>example.org:55443322</ownerId>
+                   <mediaType>VIDEO</mediaType>
+                 </album>';
+    $this->verifyLifeCycle($postData, 'application/xml');
+  }
+
+  public function testLifeCycleInAtom() {
+    $postData = '<entry xmlns="http://www.w3.org/2005/Atom">
+                 <content type="application/xml">
+                   <album xmlns="http://ns.opensocial.org/2008/opensocial">
+                     <id>44332211</id>
+                     <thumbnailUrl>http://pages.example.org/albums/4433221-tn.png</thumbnailUrl>
+                     <caption>Example Album</caption>
+                     <description>This is an example album, and this text is an example description</description>
+                     <location>
+                       <latitude>0</latitude>
+                       <longitude>0</longitude>
+                     </location>
+                     <ownerId>example.org:55443322</ownerId>
+                     <mediaType>VIDEO</mediaType>
+                   </album>
+                 </content>
+                 <title/>
+                 <updated>2003-12-13T18:30:02Z</updated>
+                 <author><url>example.org:55443322</url></author>
+                 <id>urn:guid:example.org:44332211</id>
+                 </entry>';
+    $this->verifyLifeCycle($postData, 'application/atom+xml');
+  }
+}
diff --git a/trunk/php/test/social/ApiCollectionTest.php b/trunk/php/test/social/ApiCollectionTest.php
new file mode 100644
index 0000000..bcd47e4
--- /dev/null
+++ b/trunk/php/test/social/ApiCollectionTest.php
@@ -0,0 +1,104 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\ApiCollection;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * ApiCollection test case.
+ */
+class ApiCollectionTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var ApiCollection
+   */
+  private $ApiCollection;
+  private $items;
+  private $offset;
+  private $totalSize;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->items = array('A', 'B', 'C');
+    $this->offset = true;
+    $this->totalSize = true;
+    $this->ApiCollection = new ApiCollection($this->items, $this->offset, $this->totalSize);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->ApiCollection = null;
+    $this->items = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests ApiCollection->getItems()
+   */
+  public function testGetItems() {
+    $this->assertEquals($this->items, $this->ApiCollection->getItems());
+  }
+
+  /**
+   * Tests ApiCollection->getOffset()
+   */
+  public function testGetOffset() {
+    $this->assertTrue($this->ApiCollection->getOffset());
+  }
+
+  /**
+   * Tests ApiCollection->getTotalSize()
+   */
+  public function testGetTotalSize() {
+    $this->assertTrue($this->ApiCollection->getTotalSize());
+  }
+
+  /**
+   * Tests ApiCollection->setItems()
+   */
+  public function testSetItems() {
+    $itemsToTestSetItems = array('a', 'b', 'c');
+    $this->ApiCollection->setItems($itemsToTestSetItems);
+    $this->assertEquals($itemsToTestSetItems, $this->ApiCollection->items);
+  }
+
+  /**
+   * Tests ApiCollection->setOffset()
+   */
+  public function testSetOffset() {
+    $offset = ! $this->ApiCollection->offset;
+    $this->ApiCollection->setOffset($offset);
+    $this->assertEquals($offset, $this->ApiCollection->offset);
+  }
+
+  /**
+   * Tests ApiCollection->setTotalSize()
+   */
+  public function testSetTotalSize() {
+    $totalSize = ! $this->ApiCollection->totalSize;
+    $this->ApiCollection->setTotalSize($totalSize);
+    $this->assertEquals($totalSize, $this->ApiCollection->totalSize);
+  }
+}
diff --git a/trunk/php/test/social/AppDataRestTest.php b/trunk/php/test/social/AppDataRestTest.php
new file mode 100644
index 0000000..33e41d5
--- /dev/null
+++ b/trunk/php/test/social/AppDataRestTest.php
@@ -0,0 +1,128 @@
+<?php
+namespace apache\shindig\test\social;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+require_once 'RestBase.php';
+
+class AppDataRestTest extends RestBase {
+
+  public function testAppDataLifeCycleInJson() {
+    $postData = '{
+      "pokes" : 4,
+      "last_poke" : "2008-06-13T18:30:02Z"
+    }';
+    // Creates the app data.
+    $ret = $this->curlRest('/appdata/1/@self/1', $postData, 'application/json');
+    $this->assertTrue(empty($ret), "Create app data failed. $ret.");
+    // Verifies data was written correctly
+    $ret = $this->curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json string in return: $ret.");
+    $this->assertTrue(isset($retDecoded['entry']) && isset($retDecoded['entry'][1])
+        && isset($retDecoded['entry'][1]['last_poke']) && isset($retDecoded['entry'][1]['pokes'])
+        && $retDecoded['entry'][1]['last_poke'] == '2008-06-13T18:30:02Z'
+        && $retDecoded['entry'][1]['pokes'] == '4', "Unexpected return value: $ret.");
+    // Deletes the app data.
+    $ret = $this->curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete app data failed. $ret");
+  }
+
+  public function testAppDataLifeCycleInXml() {
+    $postData = '<appdata xmlns="http://ns.opensocial.org/2008/opensocial">
+        <entry>
+          <key>pokes</key>
+          <value>1</value>
+        </entry>
+        <entry>
+          <key>last_poke</key>
+          <value>2008-02-13T18:30:02Z</value>
+        </entry>
+      </appdata>';
+    // Creates or update the app data.
+    $ret = $this->curlRest('/appdata/1/@self/1', $postData, 'application/xml');
+    $this->assertTrue(empty($ret), "Create app data failed. $ret");
+
+    // Verifies data was written correctly.
+    $ret = $this->curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json string in return: $ret.");
+    $this->assertTrue(isset($retDecoded['entry']) && isset($retDecoded['entry'][1])
+        && isset($retDecoded['entry'][1]['last_poke']) && isset($retDecoded['entry'][1]['pokes'])
+        && $retDecoded['entry'][1]['last_poke'] == '2008-02-13T18:30:02Z'
+        && $retDecoded['entry'][1]['pokes'] == '1', "Unexpected return value: $ret.");
+
+    // Updates the app data.
+    $updateData = '<appdata xmlns="http://ns.opensocial.org/2008/opensocial">
+        <entry>
+          <key>pokes</key>
+          <value>100</value>
+        </entry>
+        <entry>
+          <key>last_poke</key>
+          <value>2009-02-13T18:30:02Z</value>
+        </entry>
+      </appdata>';
+    $ret = $this->curlRest('/appdata/1/@self/1', $updateData, 'application/xml');
+    $this->assertTrue(empty($ret), "Update app data failed. $ret");
+
+    // Verifies data was written correctly.
+    $ret = $this->curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json string in return: $ret.");
+    $this->assertTrue(isset($retDecoded['entry']) && isset($retDecoded['entry'][1])
+        && isset($retDecoded['entry'][1]['last_poke']) && isset($retDecoded['entry'][1]['pokes'])
+        && $retDecoded['entry'][1]['last_poke'] == '2009-02-13T18:30:02Z'
+        && $retDecoded['entry'][1]['pokes'] == '100', "Unexpected return value: $ret.");
+
+    // Deletes the app data.
+    $ret = $this->curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete app data failed. $ret");
+  }
+
+  public function testAppDataLifeCycleInAtom() {
+    $postData = '<entry xmlns="http://www.w3.org/2005/Atom">
+      <content type="text/xml">
+        <appdata xmlns="http://opensocial.org/2008/opensocial">
+            <pokes>2</pokes>
+            <last_poke>2003-12-14T18:30:02Z</last_poke>
+          </appdata>
+      </content>
+      <title/>
+      <updated>2003-12-14T18:30:02Z</updated>
+      <author><url>urn:guid:example.org:34KJDCSKJN2HHF0DW20394</url></author>
+      <id>urn:guid:example.org:34KJDCSKJN2HHF0DW20394</id>
+    </entry>';
+    // Creates the app data.
+    $ret = $this->curlRest('/appdata/1/@self/1', $postData, 'application/atom+xml');
+    $this->assertTrue(empty($ret), "Create app data failed. $ret");
+    // Verifies data was written correctly
+    $ret = $this->curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json string in return: $ret.");
+    $this->assertTrue(isset($retDecoded['entry']) && isset($retDecoded['entry'][1])
+        && isset($retDecoded['entry'][1]['last_poke']) && isset($retDecoded['entry'][1]['pokes'])
+        && $retDecoded['entry'][1]['last_poke'] == '2003-12-14T18:30:02Z'
+        && $retDecoded['entry'][1]['pokes'] == '2', "Unexpected return value: $ret\n");
+    // Deletes the app data.
+    $ret = $this->curlRest('/appdata/1/@self/1?fields=pokes,last_poke', '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete app data failed. $ret");
+  }
+}
diff --git a/trunk/php/test/social/BodyTypeTest.php b/trunk/php/test/social/BodyTypeTest.php
new file mode 100644
index 0000000..da6d597
--- /dev/null
+++ b/trunk/php/test/social/BodyTypeTest.php
@@ -0,0 +1,129 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\BodyType;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * BodyType test case.
+ */
+class BodyTypeTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var BodyType
+   */
+  private $BodyType;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->BodyType = new BodyType();
+    $this->BodyType->build = 'BUILD';
+    $this->BodyType->eyeColor = 'EYECOLOR';
+    $this->BodyType->hairColor = 'HAIRCOLOR';
+    $this->BodyType->height = 'HEIGHT';
+    $this->BodyType->weight = 'WEIGHT';
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->BodyType = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests BodyType->getBuild()
+   */
+  public function testGetBuild() {
+    $this->assertEquals('BUILD', $this->BodyType->getBuild());
+  }
+
+  /**
+   * Tests BodyType->getEyeColor()
+   */
+  public function testGetEyeColor() {
+    $this->assertEquals('EYECOLOR', $this->BodyType->getEyeColor());
+  }
+
+  /**
+   * Tests BodyType->getHairColor()
+   */
+  public function testGetHairColor() {
+    $this->assertEquals('HAIRCOLOR', $this->BodyType->getHairColor());
+  }
+
+  /**
+   * Tests BodyType->getHeight()
+   */
+  public function testGetHeight() {
+    $this->assertEquals('HEIGHT', $this->BodyType->getHeight());
+  }
+
+  /**
+   * Tests BodyType->getWeight()
+   */
+  public function testGetWeight() {
+    $this->assertEquals('WEIGHT', $this->BodyType->getWeight());
+  }
+
+  /**
+   * Tests BodyType->setBuild()
+   */
+  public function testSetBuild() {
+    $this->BodyType->setBuild('build');
+    $this->assertEquals('build', $this->BodyType->getBuild());
+  }
+
+  /**
+   * Tests BodyType->setEyeColor()
+   */
+  public function testSetEyeColor() {
+    $this->BodyType->setEyeColor('eyecolor');
+    $this->assertEquals('eyecolor', $this->BodyType->getEyeColor());
+  }
+
+  /**
+   * Tests BodyType->setHairColor()
+   */
+  public function testSetHairColor() {
+    $this->BodyType->setHairColor('haircolor');
+    $this->assertEquals('haircolor', $this->BodyType->getHairColor());
+  }
+
+  /**
+   * Tests BodyType->setHeight()
+   */
+  public function testSetHeight() {
+    $this->BodyType->setHeight('height');
+    $this->assertEquals('height', $this->BodyType->getHeight());
+  }
+
+  /**
+   * Tests BodyType->setWeight()
+   */
+  public function testSetWeight() {
+    $this->BodyType->setWeight('weight');
+    $this->assertEquals('weight', $this->BodyType->getWeight());
+  }
+}
diff --git a/trunk/php/test/social/DefaultInvalidateServiceTest.php b/trunk/php/test/social/DefaultInvalidateServiceTest.php
new file mode 100644
index 0000000..e425b12
--- /dev/null
+++ b/trunk/php/test/social/DefaultInvalidateServiceTest.php
@@ -0,0 +1,108 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\sample\DefaultInvalidateService;
+use apache\shindig\common\RemoteContentRequest;
+use apache\shindig\common\Cache;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\AuthenticationMode;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * DefaultInvalidateService test case.
+ */
+class DefaultInvalidateServiceTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var DefaultInvalidateService
+   */
+  private $service;
+
+  /**
+   * @var Cache
+   */
+  private $cache;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->cache = Cache::createCache('apache\shindig\common\sample\CacheStorageFile', 'TestCache');
+    $this->service = new DefaultInvalidateService($this->cache);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->service = null;
+    $this->cache = null;
+    parent::tearDown();
+  }
+
+  public function testInvalidateApplicationResources() {
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $request1 = new RemoteContentRequest('http://url1');
+    $request1->setToken($token);
+    $request2 = new RemoteContentRequest('http://url2');
+    $request2->setToken($token);
+    $this->service->markResponse($request1);
+    $this->service->markResponse($request2);
+    $this->cache->set($request1->toHash(), $request1);
+    $this->cache->set($request2->toHash(), $request2);
+    $this->assertTrue($this->service->isValid($request1));
+    $this->assertTrue($this->service->isValid($request2));
+    $this->assertEquals($request1, $this->cache->get($request1->toHash()));
+    $this->assertEquals($request2, $this->cache->get($request2->toHash()));
+    $resource = array('http://url1', 'http://url2');
+    $this->service->invalidateApplicationResources($resource, $token);
+    $this->assertFalse($this->cache->get($request1->toHash()));
+    $this->assertFalse($this->cache->get($request2->toHash()));
+  }
+
+  public function testInvalidateUserResources() {
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $token->setAuthenticationMode(AuthenticationMode::$OAUTH_CONSUMER_REQUEST);
+    $request = new RemoteContentRequest('http://url');
+    $request->setToken($token);
+    $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+    $this->service->markResponse($request);
+    $opensocialIds = array('owner');
+    $this->service->invalidateUserResources($opensocialIds, $token);
+    $this->assertFalse($this->service->isValid($request));
+    $this->service->markResponse($request);
+    $this->assertTrue($this->service->isValid($request));
+  }
+
+  public function testInvalidateUserResourcesWithEmptyAppId() {
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', null, 'domain', 'appUrl', '1', 'default');
+    $token->setAuthenticationMode(AuthenticationMode::$OAUTH_CONSUMER_REQUEST);
+    $request = new RemoteContentRequest('http://url');
+    $request->setToken($token);
+    $request->setAuthType(RemoteContentRequest::$AUTH_SIGNED);
+    $this->service->markResponse($request);
+    $opensocialIds = array('owner');
+    $this->service->invalidateUserResources($opensocialIds, $token);
+    $this->assertFalse($this->service->isValid($request));
+    $this->service->markResponse($request);
+    $this->assertTrue($this->service->isValid($request));
+  }
+}
diff --git a/trunk/php/test/social/EmailTest.php b/trunk/php/test/social/EmailTest.php
new file mode 100644
index 0000000..caab949
--- /dev/null
+++ b/trunk/php/test/social/EmailTest.php
@@ -0,0 +1,72 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Email;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Email test case.
+ */
+class EmailTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Email
+   */
+  private $Email;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Email = new Email('ADDRESS', 'TYPE');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Email = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Email->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('TYPE', $this->Email->getType());
+  }
+
+  /**
+   * Tests Email->setAddress()
+   */
+  public function testSetAddress() {
+    $this->Email->setValue('address');
+    $this->assertEquals('address', $this->Email->getValue());
+  }
+
+  /**
+   * Tests Email->setType()
+   */
+  public function testSetType() {
+    $this->Email->setType('type');
+    $this->assertEquals('type', $this->Email->getType());
+  }
+}
diff --git a/trunk/php/test/social/GroupIdTest.php b/trunk/php/test/social/GroupIdTest.php
new file mode 100644
index 0000000..dfe1a72
--- /dev/null
+++ b/trunk/php/test/social/GroupIdTest.php
@@ -0,0 +1,86 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\spi\GroupId;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * GroupId test case.
+ */
+class GroupIdTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var GroupId
+   */
+  private $GroupId;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->GroupId = new GroupId('all', 1);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->GroupId = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Constructs the test case.
+   */
+  public function __construct() {}
+
+  /**
+   * Tests GroupId->__construct()
+   */
+  public function test__construct() {
+    $this->GroupId->__construct('all', 1);
+  }
+
+  /**
+   * Tests GroupId->getGroupId()
+   */
+  public function testGetGroupId() {
+    $this->assertEquals(1, $this->GroupId->getGroupId());
+  }
+
+  /**
+   * Tests GroupId->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('all', $this->GroupId->getType());
+  }
+
+  /**
+   * Tests GroupId->fromJson()
+   */
+  public function testFromJson() {
+    $json = 'jsonid';
+    $fromJson = $this->GroupId->fromJson($json);
+    $this->assertEquals('groupId', $fromJson->getType());
+    $this->assertEquals('jsonid', $fromJson->getGroupId());
+  }
+
+}
diff --git a/trunk/php/test/social/GroupsRestTest.php b/trunk/php/test/social/GroupsRestTest.php
new file mode 100644
index 0000000..91f71cd
--- /dev/null
+++ b/trunk/php/test/social/GroupsRestTest.php
@@ -0,0 +1,37 @@
+<?php
+namespace apache\shindig\test\social;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+require_once 'RestBase.php';
+
+class GroupsRestTest extends RestBase {
+
+
+  public function testGroupsLifeCycleInJson() {
+    // Get the groups for the user.
+    $ret = $this->curlRest('/groups/john.doe', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json string in return: $ret.");
+    $this->assertTrue(isset($retDecoded['entry']) && isset($retDecoded['entry']["john.doe"])
+        && isset($retDecoded['entry']["john.doe"][0])
+        && $retDecoded['entry']["john.doe"][0] == '1', "Unexpected return value: $ret.");
+  }
+}
diff --git a/trunk/php/test/social/IdSpecTest.php b/trunk/php/test/social/IdSpecTest.php
new file mode 100644
index 0000000..f46618b
--- /dev/null
+++ b/trunk/php/test/social/IdSpecTest.php
@@ -0,0 +1,77 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\IdSpec;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * IdSpec test case.
+ */
+class IdSpecTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var IdSpec
+   */
+  private $IdSpec;
+  private $jsonspec;
+  private $type;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->jsonspec = array(1, 2, 3, 4, 5, 6, 7, 8, 9);
+    $this->type = 'VIEWER';
+    $this->IdSpec = new IdSpec($this->jsonspec, $this->type);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->IdSpec = null;
+    $this->jsonspec = null;
+    $this->type = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests IdSpec->fetchUserIds()
+   */
+  public function testFetchUserIds() {
+    $this->assertEquals($this->jsonspec, $this->IdSpec->fetchUserIds());
+  }
+
+  /**
+   * Tests IdSpec::fromJson()
+   */
+  public function testFromJson() {
+    $result = IdSpec::fromJson('OWNER');
+    $this->assertTrue($result instanceof IdSpec);
+  }
+
+  /**
+   * Tests IdSpec->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('VIEWER', $this->IdSpec->getType());
+  }
+}
diff --git a/trunk/php/test/social/InputActivitiesConverterTest.php b/trunk/php/test/social/InputActivitiesConverterTest.php
new file mode 100644
index 0000000..1ab04b9
--- /dev/null
+++ b/trunk/php/test/social/InputActivitiesConverterTest.php
@@ -0,0 +1,155 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\InputActivitiesConverter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * InputActivitiesConverter test case.
+ */
+class InputActivitiesConverterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var InputActivitiesConverter
+   */
+  private $inputConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->inputConverter = new InputActivitiesConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->inputConverter = null;
+    parent::tearDown();
+  }
+
+  public function testConvertAtom() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+<entry>
+  <content type="application/xml">
+    <activity xmlns="http://ns.opensocial.org/2008/opensocial">
+        <mediaItems>
+          <MediaItem>
+            <mimeType>IMAGE</mimeType>
+            <type>image</type>
+            <url>http://cdn.davesdaily.com/pictures/784-awesome-hands.jpg</url>
+            <types>
+              <AUDIO>audio</AUDIO>
+              <VIDEO>video</VIDEO>
+              <IMAGE>image</IMAGE>
+            </types>
+          </MediaItem>
+        </mediaItems>
+      <streamTitle>activities</streamTitle>
+      <streamId>1</streamId>
+      <userId>1</userId>
+    </activity>
+  </content>
+  <author>
+    <uri>urn:guid:1</uri>
+    <name>api.example.org:1</name>
+  </author>
+  <category term="status"/>
+  <updated>2008-08-05T10:31:04+02:00</updated>
+  <id>urn:guid:220</id>
+  <title>example title</title>
+  <summary>example summary</summary>
+</entry>
+';
+    $activity = $this->inputConverter->convertAtom($xml);
+    $this->assertEquals('urn:guid:220', $activity['id']);
+    $this->assertEquals('example title', $activity['title']);
+    $this->assertEquals('example summary', $activity['body']);
+    $this->assertEquals('1', $activity['streamId']);
+    $this->assertEquals('activities', $activity['streamTitle']);
+    $this->assertEquals('2008-08-05T10:31:04+02:00', $activity['updated']);
+    $this->assertEquals('image', $activity['mediaItems'][0]['type']);
+    $this->assertEquals('IMAGE', $activity['mediaItems'][0]['mimeType']);
+    $this->assertEquals('http://cdn.davesdaily.com/pictures/784-awesome-hands.jpg', $activity['mediaItems'][0]['url']);
+  }
+
+  public function testConvertJson() {
+    $json = '{
+		"body":"write back!",
+		"id":"202",
+		"mediaItems":[{"mimeType":"image","type":"image","url":"http:\/\/cdn.davesdaily.com\/pictures\/784-awesome-hands.jpg"}],
+		"postedTime":"1217886794",
+		"streamTitle":"activities",
+		"title":"test title",
+		"userId":"1"
+		}';
+    $activity = $this->inputConverter->convertJson($json);
+    $this->assertEquals('write back!', $activity['body']);
+    $this->assertEquals('202', $activity['id']);
+    $this->assertEquals('image', $activity['mediaItems'][0]['mimeType']);
+    $this->assertEquals('image', $activity['mediaItems'][0]['type']);
+    $this->assertEquals('http://cdn.davesdaily.com/pictures/784-awesome-hands.jpg', $activity['mediaItems'][0]['url']);
+    $this->assertEquals('1217886794', $activity['postedTime']);
+    $this->assertEquals('activities', $activity['streamTitle']);
+    $this->assertEquals('test title', $activity['title']);
+    $this->assertEquals('1', $activity['userId']);
+  }
+
+  public function testConvertXml() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+<response>
+  <activity xmlns="http://ns.opensocial.org/2008/opensocial">
+      <mediaItems>
+        <MediaItem>
+          <mimeType>IMAGE</mimeType>
+          <type>image</type>
+          <url>http://cdn.davesdaily.com/pictures/784-awesome-hands.jpg</url>
+          <types>
+            <AUDIO>audio</AUDIO>
+            <VIDEO>video</VIDEO>
+            <IMAGE>image</IMAGE>
+          </types>
+        </MediaItem>
+      </mediaItems>
+    <streamTitle>activities</streamTitle>
+    <streamId>1</streamId>
+    <userId>1</userId>
+  </activity>
+  <category term="status"/>
+  <updated>2008-08-05T10:31:04+02:00</updated>
+  <id>urn:guid:220</id>
+  <title>example title</title>
+  <summary>example summary</summary>
+</response>
+';
+    $activity = $this->inputConverter->convertXml($xml);
+    $this->assertEquals('urn:guid:220', $activity['id']);
+    $this->assertEquals('example title', $activity['title']);
+    $this->assertEquals('example summary', $activity['body']);
+    $this->assertEquals('1', $activity['streamId']);
+    $this->assertEquals('activities', $activity['streamTitle']);
+    $this->assertEquals('2008-08-05T10:31:04+02:00', $activity['updated']);
+    $this->assertEquals('image', $activity['mediaItems'][0]['type']);
+    $this->assertEquals('IMAGE', $activity['mediaItems'][0]['mimeType']);
+    $this->assertEquals('http://cdn.davesdaily.com/pictures/784-awesome-hands.jpg', $activity['mediaItems'][0]['url']);
+  }
+}
diff --git a/trunk/php/test/social/InputAlbumsConverterTest.php b/trunk/php/test/social/InputAlbumsConverterTest.php
new file mode 100644
index 0000000..4fdedac
--- /dev/null
+++ b/trunk/php/test/social/InputAlbumsConverterTest.php
@@ -0,0 +1,117 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\InputAlbumsConverter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * InputAlbumsConverter test case.
+ */
+class InputAlbumsConverterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var InputAlbumsConverter
+   */
+  private $inputConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->inputConverter = new InputAlbumsConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->inputConverter = null;
+    parent::tearDown();
+  }
+
+  public function testConvertAtom() {
+    $xml = '<entry xmlns="http://www.w3.org/2005/Atom">
+            <content type="application/xml">
+              <album xmlns="http://ns.opensocial.org/2008/opensocial">
+                <id>44332211</id>
+                <thumbnailUrl>http://www.libpng.org/pub/png/img_png/pngnow.png</thumbnailUrl>
+                <caption>Example Album</caption>
+                <description>This is an example album, and this text is an example description</description>
+                <location>
+                  <latitude>0</latitude>
+                  <longitude>0</longitude>
+                </location>
+                <ownerId>example.org:55443322</ownerId>
+              </album>
+            </content>
+            <title/>
+            <updated>2003-12-13T18:30:02Z</updated>
+            <author><url>example.org:55443322</url></author>
+            <id>urn:guid:example.org:44332211</id>
+            </entry>';
+    $album = $this->inputConverter->convertAtom($xml);
+    $this->assertEquals('44332211', $album['id']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $album['thumbnailUrl']);
+    $this->assertEquals('This is an example album, and this text is an example description', $album['description']);
+    $this->assertEquals('Example Album', $album['title']);
+    $this->assertEquals('example.org:55443322', $album['ownerId']);
+    $this->assertFalse(empty($album['location']));
+    $this->assertEquals(0, $album['location']['latitude']);
+    $this->assertEquals(0, $album['location']['longitude']);
+  }
+
+  public function testConvertJson() {
+    $json = '{ "id": "albumId",
+               "title": "The album title.",
+               "location": {"latitude": 100.0, "longitude": 200.0}
+    }';
+    $album = $this->inputConverter->convertJson($json);
+    $this->assertEquals('albumId', $album['id']);
+    $this->assertEquals('The album title.', $album['title']);
+    $this->assertFalse(empty($album['location']));
+    $this->assertEquals(100.0, $album['location']['latitude']);
+    $this->assertEquals(200.0, $album['location']['longitude']);
+  }
+
+  public function testConvertXml() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+            <album xmlns="http://ns.opensocial.org/2008/opensocial">
+            <id>44332211</id>
+            <thumbnailUrl>http://www.libpng.org/pub/png/img_png/pngnow.png</thumbnailUrl>
+            <caption>Example Album</caption>
+            <description>This is an example album, and this text is an example description</description>
+            <location>
+               <latitude>0</latitude>
+               <longitude>0</longitude>
+            </location>
+            <ownerId>example.org:55443322</ownerId>
+            </album>';
+    $album = $this->inputConverter->convertXml($xml);
+    $this->assertEquals('44332211', $album['id']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $album['thumbnailUrl']);
+    $this->assertEquals('This is an example album, and this text is an example description', $album['description']);
+    $this->assertEquals('Example Album', $album['title']);
+    $this->assertEquals('example.org:55443322', $album['ownerId']);
+    $this->assertFalse(empty($album['location']));
+    $this->assertEquals(0, $album['location']['latitude']);
+    $this->assertEquals(0, $album['location']['longitude']);
+  }
+}
diff --git a/trunk/php/test/social/InputAppDataConverterTest.php b/trunk/php/test/social/InputAppDataConverterTest.php
new file mode 100644
index 0000000..fff615c
--- /dev/null
+++ b/trunk/php/test/social/InputAppDataConverterTest.php
@@ -0,0 +1,93 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\InputAppDataConverter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * InputAppDataConverter test case.
+ */
+class InputAppDataConverterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var InputAppDataConverter
+   */
+  private $inputConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->inputConverter = new InputAppDataConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->inputConverter = null;
+    parent::tearDown();
+  }
+
+  public function testConvertAtom() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+<entry>
+    <content type="application/xml">
+      <appdata xmlns="http://ns.opensocial.org/2008/opensocial">
+        <sign>Virgo</sign>
+      </appdata>
+    </content>
+    <author>
+      <uri>urn:guid:1</uri>
+      <name>api.example.org:1</name>
+    </author>
+    <id>urn:guid:1</id>
+    <title>appdata id 1</title>
+    <updated>2008-08-06T22:36:20+02:00</updated>
+  </entry>';
+    $appdata = $this->inputConverter->convertAtom($xml);
+    $expect = array('sign' => 'Virgo');
+    $this->assertEquals($expect, $appdata);
+  }
+
+  public function testConvertJson() {
+    $json = '{
+ 		"pokes" : 3,
+		"last_poke" : "2008-02-13T18:30:02Z"
+		}';
+    $appData = $this->inputConverter->convertJson($json);
+    $this->assertEquals('3', $appData['pokes']);
+    $this->assertEquals('2008-02-13T18:30:02Z', $appData['last_poke']);
+  }
+
+  public function testConvertXml() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+<response>
+  <entry>
+    <key>sign</key>
+    <value>Virgo</value>
+  </entry>
+</response>';
+    $appdata = $this->inputConverter->convertXml($xml);
+    $expect = array('sign' => 'Virgo');
+    $this->assertEquals($expect, $appdata);
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/test/social/InputInvalidateConverterTest.php b/trunk/php/test/social/InputInvalidateConverterTest.php
new file mode 100644
index 0000000..2cd86b0
--- /dev/null
+++ b/trunk/php/test/social/InputInvalidateConverterTest.php
@@ -0,0 +1,68 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\InputInvalidateConverter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * InputAtomConverter test case.
+ */
+class InputInvalidateConverterTest extends \PHPUnit_Framework_TestCase {
+  /**
+   * @var InputInvalidateConverter
+   */
+  private $inputConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->inputConverter = new InputInvalidateConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->inputConverter = null;
+    parent::tearDown();
+  }
+
+  public function testConvertJson() {
+    $testArray = array(
+        'foo' => 'bar',
+        'bla' => 'blub',
+    );
+
+    $converted = $this->inputConverter->convertJson(json_encode($testArray));
+
+    $this->assertEquals($testArray, $converted);
+  }
+
+  public function testConvertAtom() {
+      $this->testConvertJson();
+  }
+
+  public function testConvertXml() {
+      $this->testConvertJson();
+  }
+
+}
diff --git a/trunk/php/test/social/InputMediaItemsConverterTest.php b/trunk/php/test/social/InputMediaItemsConverterTest.php
new file mode 100644
index 0000000..92bdc21
--- /dev/null
+++ b/trunk/php/test/social/InputMediaItemsConverterTest.php
@@ -0,0 +1,108 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\InputMediaItemsConverter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * InputAtomConverter test case.
+ */
+class InputMediaItemsConverterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var InputMediaItemsConverter
+   */
+  private $inputConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->inputConverter = new InputMediaItemsConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->inputConverter = null;
+    parent::tearDown();
+  }
+
+  public function testConvertAtom() {
+    $xml = '<entry xmlns="http://www.w3.org/2005/Atom">
+              <content type="application/xml">
+                <mediaItem xmlns="http://ns.opensocial.org/2008/opensocial">
+                  <id>11223344</id>
+                  <thumbnailUrl>http://www.libpng.org/pub/png/img_png/pngnow.png</thumbnailUrl>
+                  <mimeType>image/png</mimeType>
+                  <type>image</type>
+                  <url>http://www.libpng.org/pub/png/img_png/pngnow.png</url>
+                  <albumId>44332211</albumId>
+                </mediaItem>
+              </content>
+              <title/>
+              <updated>2003-12-13T18:30:02Z</updated>
+              <author><url>example.org:55443322</url></author>
+              <id>urn:guid:example.org:11223344</id>
+            </entry>';
+    $mediaItem = $this->inputConverter->convertAtom($xml);
+    $this->assertEquals('11223344', $mediaItem['id']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $mediaItem['thumbnailUrl']);
+    $this->assertEquals('44332211', $mediaItem['albumId']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $mediaItem['url']);
+    $this->assertEquals('image/png', $mediaItem['mimeType']);
+  }
+
+  public function testConvertJson() {
+    $json = '{ "id" : "11223344",
+               "thumbnailUrl" : "http://www.libpng.org/pub/png/img_png/pngnow.png",
+               "mimeType" : "image/png",
+               "type" : "image",
+               "url" : "http://www.libpng.org/pub/png/img_png/pngnow.png",
+               "albumId" : "44332211"
+             }';
+    $mediaItem = $this->inputConverter->convertJson($json);
+    $this->assertEquals('11223344', $mediaItem['id']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $mediaItem['thumbnailUrl']);
+    $this->assertEquals('44332211', $mediaItem['albumId']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $mediaItem['url']);
+    $this->assertEquals('image/png', $mediaItem['mimeType']);
+  }
+
+  public function testConvertXml() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+            <mediaItem xmlns="http://ns.opensocial.org/2008/opensocial">
+              <id>11223344</id>
+              <thumbnailUrl>http://www.libpng.org/pub/png/img_png/pngnow.png</thumbnailUrl>
+              <mimeType>image/png</mimeType>
+              <type>image</type>
+              <url>http://www.libpng.org/pub/png/img_png/pngnow.png</url>
+              <albumId>44332211</albumId>
+            </mediaItem>';
+    $mediaItem = $this->inputConverter->convertXml($xml);
+    $this->assertEquals('11223344', $mediaItem['id']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $mediaItem['thumbnailUrl']);
+    $this->assertEquals('44332211', $mediaItem['albumId']);
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $mediaItem['url']);
+    $this->assertEquals('image/png', $mediaItem['mimeType']);
+  }
+}
diff --git a/trunk/php/test/social/InputMessagesConverterTest.php b/trunk/php/test/social/InputMessagesConverterTest.php
new file mode 100644
index 0000000..f1c1f67
--- /dev/null
+++ b/trunk/php/test/social/InputMessagesConverterTest.php
@@ -0,0 +1,98 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\InputMessagesConverter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * InputAtomConverter test case.
+ */
+class InputMessagesConverterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var InputMessagesConverter
+   */
+  private $inputConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->inputConverter = new InputMessagesConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->inputConverter = null;
+    parent::tearDown();
+  }
+
+  public function testConvertAtom() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="http://www.w3.org/2005/Atom"
+         xmlns:osapi="http://opensocial.org/2008/opensocialapi">
+  <osapi:recipient>example.org:AD38B3886625AAF</osapi:recipient>
+  <osapi:recipient>example.org:997638BAA6F25AD</osapi:recipient>
+  <title>You have an invitation from Joe</title>
+  <id>{msgid}</id>
+  <link rel="alternate" href="http://app.example.org/invites/{msgid}"/>
+  <content>Click &lt;a href="http://app.example.org/invites/{msgid}"&gt;here&lt;/a&gt; to review your invitation.</content>
+</entry>';
+    $message = $this->inputConverter->convertAtom($xml);
+    $this->assertEquals('{msgid}', $message['id']);
+    $this->assertEquals('You have an invitation from Joe', $message['title']);
+    $this->assertEquals('Click <a href="http://app.example.org/invites/{msgid}">here</a> to review your invitation.', $message['body']);
+    $this->assertEquals('example.org:AD38B3886625AAF', $message['recipients'][0]);
+    $this->assertEquals('example.org:997638BAA6F25AD', $message['recipients'][1]);
+  }
+
+  public function testConvertJson() {
+    $json = '{
+ 		"id" : "msgid",
+		"title" : "You have an invitation from Joe",
+		"body" : "Click here to review your invitation"
+		}';
+    $message = $this->inputConverter->convertJson($json);
+    file_put_contents(sys_get_temp_dir() . '/message.txt', print_r($json, true));
+    $this->assertEquals('msgid', $message['id']);
+    $this->assertEquals('You have an invitation from Joe', $message['title']);
+    $this->assertEquals('Click here to review your invitation', $message['body']);
+  }
+
+  public function testConvertXml() {
+    $xml = '<?xml version="1.0" encoding="UTF-8"?>
+<response xmlns:osapi="http://opensocial.org/2008/opensocialapi">
+  <osapi:recipient>example.org:AD38B3886625AAF</osapi:recipient>
+  <osapi:recipient>example.org:997638BAA6F25AD</osapi:recipient>
+  <title>You have an invitation from Joe</title>
+  <id>{msgid}</id>
+  <body>Click &lt;a href="http://app.example.org/invites/{msgid}"&gt;here&lt;/a&gt; to review your invitation.</body>
+</response>';
+    $message = $this->inputConverter->convertXml($xml);
+    $this->assertEquals('{msgid}', $message['id']);
+    $this->assertEquals('You have an invitation from Joe', $message['title']);
+    $this->assertEquals('Click <a href="http://app.example.org/invites/{msgid}">here</a> to review your invitation.', $message['body']);
+    $this->assertEquals('example.org:AD38B3886625AAF', $message['recipients'][0]);
+    $this->assertEquals('example.org:997638BAA6F25AD', $message['recipients'][1]);
+  }
+}
diff --git a/trunk/php/test/social/InputPeopleConverterTest.php b/trunk/php/test/social/InputPeopleConverterTest.php
new file mode 100644
index 0000000..806038b
--- /dev/null
+++ b/trunk/php/test/social/InputPeopleConverterTest.php
@@ -0,0 +1,64 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\InputPeopleConverter;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * InputAtomConverter test case.
+ */
+class InputPeopleConverterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var InputPeopleConverter
+   */
+  private $inputConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->inputConverter = new InputPeopleConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->inputConverter = null;
+    parent::tearDown();
+  }
+
+  public function testConvertAtom() {
+    $this->setExpectedException('apache\shindig\social\service\SocialSpiException');
+    $this->inputConverter->convertAtom('');
+  }
+
+  public function testConvertJson() {
+    $this->setExpectedException('apache\shindig\social\service\SocialSpiException');
+    $this->inputConverter->convertJson('');
+  }
+
+  public function testConvertXml() {
+    $this->setExpectedException('apache\shindig\social\service\SocialSpiException');
+    $this->inputConverter->convertXml('');
+  }
+}
diff --git a/trunk/php/test/social/JsonDbOpensocialServiceTest.php b/trunk/php/test/social/JsonDbOpensocialServiceTest.php
new file mode 100644
index 0000000..4ada472
--- /dev/null
+++ b/trunk/php/test/social/JsonDbOpensocialServiceTest.php
@@ -0,0 +1,209 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\sample\JsonDbOpensocialService;
+use apache\shindig\social\spi\UserId;
+use apache\shindig\social\spi\GroupId;
+use apache\shindig\social\spi\CollectionOptions;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\social\model\Message;
+use apache\shindig\social\model\MessageCollection;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * JsonDbOpensocialService test case.
+ */
+class JsonDbOpensocialServiceTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var JsonDbOpensocialService
+   */
+  private $service;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->service = new JsonDbOpensocialService();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->service = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Constructs the test case.
+   */
+  public function __construct() {}
+
+  /**
+   * Tests JsonDbOpensocialService->getActivities() with paging.
+   */
+  public function testGetActivities() {
+    $token = BasicSecurityToken::createFromValues('jane.doe', 'jane.doe', 1, 1, 1, 1, 'default');
+    $userId = new UserId('owner', null);
+    $userIds = array($userId);
+    $groupId = new GroupId('self', null);
+    $startIndex = 1;
+    $count = 1;
+    
+    $ret = $this->service->getActivities($userIds, $groupId, 1, null, null, null, null, $startIndex, $count, null, 1, $token);
+    $this->assertEquals($startIndex, $ret->startIndex);
+    $this->assertEquals($count, count($ret->entry));
+    $this->assertEquals(2, $ret->totalResults);
+    $this->assertEquals('2', $ret->entry[0]['id']);
+    $this->assertEquals('Jane says George likes yoda!', $ret->entry[0]['title']);
+    $this->assertEquals('or is it you?', $ret->entry[0]['body']);
+  }
+  
+  public function testActivityLifeCycle() {
+    $token = BasicSecurityToken::createFromValues('jane.doe', 'jane.doe', 1, 1, 1, 1, 'default');
+    $userId = new UserId('owner', null);
+    $userIds = array($userId);
+    $groupId = new GroupId('self', null);
+    $title = 'activity life cycle unit test title';
+    $activity = array('id' => '1', 'userId' => 'userId', 'title' => $title);
+    $ret = $this->service->getActivities($userIds, $groupId, 1, null, null, null, null, 0, 4, null, 1, $token);
+    $this->assertEquals(2, count($ret->entry));
+    
+    $this->service->createActivity($userId, $groupId, $token->getAppId(), null, $activity, $token);
+    $ret = $this->service->getActivities($userIds, $groupId, 1, null, null, null, null, 0, 4, null, 1, $token);
+    $this->assertEquals(3, count($ret->entry));
+    $id = null;
+    foreach ($ret->entry as $entity) {
+      if ($entity['title'] == $title) {
+        $id = $entity['id'];
+      }
+    }
+    $this->assertNotNull($id);
+    
+    $this->service->deleteActivities($userId, $groupId, $token->getAppId(), array($id), $token);
+    
+    $ret = $this->service->getActivities($userIds, $groupId, 1, null, null, null, null, 0, 4, null, 1, $token);
+    $this->assertEquals(2, count($ret->entry));
+  }
+
+  public function testMessageLifeCycle() {
+    $token = BasicSecurityToken::createFromValues('john.doe', 'canonical', 1, 1, 1, 1, 'default');
+    $userId = new UserId('viewer', null);
+    $body = 'message unit test body';
+    $title = 'message unit test title';
+    $message = array('id' => '1', 'body' => $body, 'title' => $title, 'type' => 'NOTIFICATION');
+    $ret = $this->service->getMessages($userId, 'notification', null, null, null, $token);
+    $this->assertEquals(3, count($ret->entry));
+    
+    $this->service->createMessage($userId, 'notification', $message, $token);
+    $ret = $this->service->getMessages($userId, 'notification', null, null, null, $token);
+    $this->assertEquals(4, count($ret->entry));
+    
+    $fetchedMessage = null;
+    foreach ($ret->entry as $message) {
+      if ($message['title'] == $title) {
+        $fetchedMessage = $message;
+      }
+    }
+    $this->assertEquals($body, $fetchedMessage['body']);
+    
+    $this->service->deleteMessages($userId, 'notification', array($fetchedMessage['id']), $token);
+    $ret = $this->service->getMessages($userId, 'notification', null, null, null, $token);
+    $this->assertEquals(3, count($ret->entry));
+  }
+  
+  public function testGetMessages() {
+    $token = BasicSecurityToken::createFromValues('canonical', 'canonical', 1, 1, 1, 1, 'default');
+    $userId = new UserId('viewer', null);
+    $options = new CollectionOptions();
+    $options->setCount(2);
+    $options->setStartIndex(1);
+    $ret = $this->service->getMessages($userId, 'notification', Message::$DEFAULT_FIELDS, array('1', '2', '3'), $options, $token);
+    
+    $this->assertEquals(2, count($ret->entry));
+    $this->assertEquals('2', $ret->entry[0]['id']);
+    $this->assertEquals('notification', $ret->entry[0]['type']);
+    $this->assertEquals('play checkers', $ret->entry[0]['title']);
+    
+    $this->assertEquals('3', $ret->entry[1]['id']);
+    
+    $this->assertEquals('you won!', $ret->entry[1]['title']);
+  }
+  
+  public function testMessageCollectionLifeCycle() {
+    // NOTE: If this method failes after the creation of the market collection.
+    // There will be a market collection in the cached json file /tmp/ShindigDb.json
+    // that prevents the test from passing. Change the test case and remove that
+    // file then run again.
+    $token = BasicSecurityToken::createFromValues('john.doe', 'john.doe', 1, 1, 1, 1, 'default');
+    $title = 'created for message collection unit test';
+    $userId = new UserId('owner', null);
+    $msgColl = array('id' => '1', 'title' => $title);
+    $this->service->createMessageCollection($userId, $msgColl, $token);
+    
+    $msgColls = $this->service->getMessageCollections($userId, null, null, $token)->entry;
+    $this->assertEquals(4, count($msgColls));
+    $fetchedMsgColl = null;
+    foreach ($msgColls as $coll) {
+      if ($coll['title'] == $title) {
+        $fetchedMsgColl = $coll;
+      }
+    }
+    $this->assertNotNull($fetchedMsgColl);
+    
+    $newTitle = 'new title for unit test';
+    $msgColl['title'] = $newTitle;
+    $msgColl['id'] = $fetchedMsgColl['id'];
+    $this->service->updateMessageCollection($userId, $msgColl, $token);
+    
+    $msgColls = $this->service->getMessageCollections($userId, null, null, $token)->entry;
+    $this->assertEquals(4, count($msgColls));
+    foreach ($msgColls as $coll) {
+      if ($coll['id'] == $fetchedMsgColl['id']) {
+        $fetchedMsgColl = $coll;
+      }
+    }
+    $this->assertEquals($newTitle, $fetchedMsgColl['title']);
+    
+    $this->service->deleteMessageCollection($userId, $fetchedMsgColl['id'], $token);
+    $msgColls = $this->service->getMessageCollections($userId, null, null, $token)->entry;
+    $this->assertEquals(3, count($msgColls));    
+  }
+  
+  public function testGetMessageCollections() {
+    $token = BasicSecurityToken::createFromValues('john.doe', 'john.doe', 1, 1, 1, 1, 'default');
+    $userId = new UserId('owner', null);
+    $ret = $this->service->getMessageCollections($userId, MessageCollection::$DEFAULT_FIELDS, null, $token);
+    $this->assertEquals('Notifications', $ret->entry[0]['title']);
+    $this->assertEquals('notification', $ret->entry[0]['id']);
+    $this->assertEquals(2, $ret->entry[0]['total']);
+    
+    $this->assertEquals('Inbox', $ret->entry[1]['title']);
+    $this->assertEquals('privateMessage', $ret->entry[1]['id']);
+    $this->assertEquals(0, $ret->entry[1]['total']);
+    
+    $this->assertEquals('Inbox', $ret->entry[2]['title']);
+    $this->assertEquals('publicMessage', $ret->entry[2]['id']);
+    $this->assertEquals(0, $ret->entry[2]['total']);
+  }
+  
+}
diff --git a/trunk/php/test/social/JsonRpcServletTest.php b/trunk/php/test/social/JsonRpcServletTest.php
new file mode 100644
index 0000000..8472f80
--- /dev/null
+++ b/trunk/php/test/social/JsonRpcServletTest.php
@@ -0,0 +1,76 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\servlet\JsonRpcServlet;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+class JsonRpcServletTest extends \PHPUnit_Framework_TestCase {
+
+    public function testParseRPCGetParameters()
+    {
+        $servlet = new JsonRpcServlet();
+        $servlet->noHeaders = true;
+        $parameters = 'oauth_token=abcdef&method=people.get&id=req&params.userId=@me&params.groupId=@self&field=1,2,3&fieldtwo(0).nested1=value1&fieldtwo(1).nested2.blub(0)=value2&fieldtwo(1).nested3=value3&f.a.c=foo&f.a.d=bar';
+
+        $result = $servlet->parseGetRequest($parameters);
+
+        $expected = array(
+            'method' => 'people.get',
+            'id' => 'req',
+            'params' => array(
+                'userId' => '@me',
+                'groupId' => '@self',
+            ),
+            'field' => array(1,2,3),
+            'fieldtwo' => array(
+                0 => array(
+                    'nested1' => 'value1',
+                ),
+                1 => array(
+                    'nested2' => array(
+                        'blub' => array(
+                            0 => 'value2',
+                        ),
+                    ),
+                    'nested3' => 'value3',
+                ),
+            ),
+            'f' => array(
+                'a' => array(
+                    'c' => 'foo',
+                    'd' => 'bar',
+                )
+            ),
+            'oauth_token' => 'abcdef',
+        );
+
+        $this->assertEquals($expected, $result);
+    }
+
+
+    public function testParseRPCGetWithEmptyParameters()
+    {
+        $servlet = new JsonRpcServlet();
+        $servlet->noHeaders = true;
+        $result = $servlet->parseGetRequest('');
+
+        $this->assertEquals(array(), $result);
+    }
+}
diff --git a/trunk/php/test/social/MediaItemRestTest.php b/trunk/php/test/social/MediaItemRestTest.php
new file mode 100644
index 0000000..20c7611
--- /dev/null
+++ b/trunk/php/test/social/MediaItemRestTest.php
@@ -0,0 +1,179 @@
+<?php
+namespace apache\shindig\test\social;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+require_once 'RestBase.php';
+
+class MediaItemRestTest extends RestBase {
+
+  protected function setUp() {
+    $postData = '{ "id" : "44332211",
+       "thumbnailUrl" : "http://www.libpng.org/pub/png/img_png/pngnow.png",
+       "title" : "Example Album",
+       "description" : "This is an example album, and this text is an example description",
+       "location" : { "latitude": 0, "longitude": 0 },
+       "ownerId" : "example.org:55443322",
+       "mediaType" : "VIDEO"
+    }';
+
+    $url = '/albums/1/@self';
+    $ret = $this->curlRest($url, $postData, 'application/json');
+    $this->assertFalse(empty($ret));
+    $album = json_decode($ret, true);
+    $this->album = $album['entry'];
+  }
+
+  protected function tearDown() {
+//    $url = '/albums/1/@self';
+//    $ret = $this->curlRest($url . '/' . urlencode($this->album['id']), '', 'application/json', 'DELETE');
+//    $this->assertTrue(empty($ret), "Delete the created album failed. Response: $ret");
+  }
+
+  private function verifyLifeCycle($postData, $postDataFormat) {
+    $url = '/mediaitems/1/@self/' . $this->album['id'];
+    $ret = $this->curlRest($url, $postData, $postDataFormat);
+    $mediaItem = json_decode($ret, true);
+    $mediaItem = $mediaItem['entry'];
+
+    $ret = $this->curlRest($url . '/' . urlencode($mediaItem['id']), '', 'application/json', 'GET');
+    $this->assertFalse(empty($ret));
+    $fetched = json_decode($ret, true);
+    $fetched = $fetched['entry'][0];
+    $this->assertEquals('http://www.libpng.org/pub/png/img_png/pngnow.png', $fetched['thumbnailUrl'], "thumbnailUrl should be same.");
+    $this->assertEquals('image/png', $fetched['mimeType'], "mimeType should be same.");
+    $this->assertEquals('IMAGE', $fetched['type'], "type should be same.");
+    $fetched['thumbnailUrl'] = 'http://changed.com/tn.png';
+    $ret = $this->curlRest($url . '/' . urlencode($mediaItem['id']), json_encode($fetched), 'application/json', 'PUT');
+    $ret = $this->curlRest($url . '/' . urlencode($mediaItem['id']), '', 'application/json', 'GET');
+    $this->assertFalse(empty($ret));
+    $fetched = json_decode($ret, true);
+    $fetched = $fetched['entry'][0];
+    $this->assertEquals('http://changed.com/tn.png', $fetched['thumbnailUrl'], "thumbnailUrl should be same.");
+    $this->assertEquals('image/png', $fetched['mimeType'], "mimeType should be same.");
+    $this->assertEquals('IMAGE', $fetched['type'], "type should be same.");
+
+    $ret = $this->curlRest($url . '/' . urlencode($mediaItem['id']), '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete the created mediaItem failed. Response: $ret");
+
+    $ret = $this->curlRest($url . '/' . urlencode($mediaItem['id']), '', 'application/json', 'GET');
+    $fetched = json_decode($ret, true);
+    $fetched = $fetched['entry'];
+    $this->assertTrue(empty($fetched));
+  }
+
+  public function testLifeCycleInJson() {
+    $postData = '{ "id" : "11223344",
+                   "thumbnailUrl" : "http://www.libpng.org/pub/png/img_png/pngnow.png",
+                   "mimeType" : "image/png",
+                   "type" : "image",
+                   "url" : "http://www.google.com/intl/en_ALL/images/logo.gif",
+                   "albumId" : "' . $this->album['id'] . '"
+                 }';
+    $this->verifyLifeCycle($postData, 'application/json');
+  }
+
+  public function testLifeCycleInXml() {
+    $postData = '<?xml version="1.0" encoding="UTF-8"?>
+                 <mediaItem xmlns="http://ns.opensocial.org/2008/opensocial">
+                   <id>11223344</id>
+                   <thumbnailUrl>http://www.libpng.org/pub/png/img_png/pngnow.png</thumbnailUrl>
+                   <mimeType>image/png</mimeType>
+                   <type>image</type>
+                   <url>http://www.google.com/intl/en_ALL/images/logo.gif</url>
+                   <albumId>' . $this->album['id'] . '</albumId>
+                 </mediaItem>';
+    $this->verifyLifeCycle($postData, 'application/xml');
+  }
+
+  public function testLifeCycleInAtom() {
+    $postData = '<entry xmlns="http://www.w3.org/2005/Atom">
+                   <content type="application/xml">
+                     <mediaItem xmlns="http://ns.opensocial.org/2008/opensocial">
+                       <id>11223344</id>
+                       <thumbnailUrl>http://www.libpng.org/pub/png/img_png/pngnow.png</thumbnailUrl>
+                       <mimeType>image/png</mimeType>
+                       <type>image</type>
+                       <url>http://www.google.com/intl/en_ALL/images/logo.gif</url>
+                       <albumId>' . $this->album['id'] . '</albumId>
+                     </mediaItem>
+                   </content>
+                   <title/>
+                   <updated>2003-12-13T18:30:02Z</updated>
+                   <author><url>example.org:55443322</url></author>
+                   <id>urn:guid:example.org:11223344</id>
+                 </entry>';
+    $this->verifyLifeCycle($postData, 'application/atom+xml');
+  }
+
+  public function testLifeCycleWithActivity() {
+    // Creates the media item.
+    $postData = '{ "id" : "11223344",
+               "thumbnailUrl" : "http://www.libpng.org/pub/png/img_png/pngnow.png",
+               "mimeType" : "image/png",
+               "type" : "image",
+               "url" : "http://www.libpng.org/pub/png/img_png/pngnow.png",
+               "albumId" : "' . $this->album['id'] . '"
+             }';
+    $url = '/mediaitems/1/@self/' . $this->album['id'];
+    $ret = $this->curlRest($url, $postData, 'application/json');
+    $mediaItem = json_decode($ret, true);
+    $mediaItem = $mediaItem['entry'];
+    // Creates the activity.
+    $activityUrl = '/activities/1/@self';
+    $randomTitle = "[" . rand(0, 2048) . "] test activity";
+    $postData = '{
+      "id" : "http://example.org/activities/example.org:87ead8dead6beef/self/af3778",
+      "title" : "' . $randomTitle . '",
+      "updated" : "2008-02-20T23:35:37.266Z",
+      "body" : "Some details for some activity",
+      "bodyId" : "383777272",
+      "url" : "http://api.example.org/activity/feeds/.../af3778",
+      "userId" : "example.org:34KJDCSKJN2HHF0DW20394",
+      "mediaItems" : [ {
+          "id": ' . $mediaItem['id'] . ',
+          "albumId": "' . $mediaItem['albumId'] . '"
+        }
+      ]
+    }';
+    $ret = $this->curlRest($activityUrl, $postData, 'application/json');
+    $this->assertTrue(empty($ret), "Create activity failed. Response: $ret");
+    // Verifyies data was written correctly
+    $ret = $this->curlRest($activityUrl . '?count=20', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json string in return: $ret");
+    // Sees if we can find our just created activity
+    $activityId = null;
+    foreach ($retDecoded['entry'] as $entry) {
+      if ($entry['title'] == $randomTitle) {
+        $activityId = $entry['id'];
+        break;
+      }
+    }
+    $this->assertNotNull($activityId, "Couldn't find created activity.");
+
+
+    $ret = $this->curlRest($activityUrl . "/@app/$activityId", '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete activity failed. Repsonse: $ret");
+
+    $ret = $this->curlRest($url . '/' . urlencode($mediaItem['id']), '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete the created mediaItem failed. Response: $ret");
+  }
+}
\ No newline at end of file
diff --git a/trunk/php/test/social/MediaItemTest.php b/trunk/php/test/social/MediaItemTest.php
new file mode 100644
index 0000000..6f2c7bd
--- /dev/null
+++ b/trunk/php/test/social/MediaItemTest.php
@@ -0,0 +1,94 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\MediaItem;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * MediaItem test case.
+ */
+class MediaItemTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var MediaItem
+   */
+  private $MediaItem;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->MediaItem = new MediaItem('MIMETYPE', 'AUDIO', 'URL');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->MediaItem = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests MediaItem->getMimeType()
+   */
+  public function testGetMimeType() {
+    $this->assertEquals('MIMETYPE', $this->MediaItem->getMimeType());
+  }
+
+  /**
+   * Tests MediaItem->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('AUDIO', $this->MediaItem->getType());
+  }
+
+  /**
+   * Tests MediaItem->getUrl()
+   */
+  public function testGetUrl() {
+    $this->assertEquals('URL', $this->MediaItem->getUrl());
+  }
+
+  /**
+   * Tests MediaItem->setMimeType()
+   */
+  public function testSetMimeType() {
+    $this->MediaItem->setMimeType('mimetype');
+    $this->assertEquals('mimetype', $this->MediaItem->mimeType);
+  }
+
+  /**
+   * Tests MediaItem->setType()
+   */
+  public function testSetType() {
+    $this->MediaItem->setType('VIDEO');
+    $this->assertEquals('VIDEO', $this->MediaItem->type);
+  }
+
+  /**
+   * Tests MediaItem->setUrl()
+   */
+  public function testSetUrl() {
+    $this->MediaItem->setUrl('url');
+    $this->assertEquals('url', $this->MediaItem->url);
+  }
+}
diff --git a/trunk/php/test/social/MessageRestTest.php b/trunk/php/test/social/MessageRestTest.php
new file mode 100644
index 0000000..44225bf
--- /dev/null
+++ b/trunk/php/test/social/MessageRestTest.php
@@ -0,0 +1,157 @@
+<?php
+namespace apache\shindig\test\social;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+require_once 'RestBase.php';
+
+/**
+ * It is an integration test. Since it's may be used by the specified container implementation.
+ * It'd better not to depends on the data in the json DB sample.
+ */
+class MessageRestTest extends RestBase {
+
+  private function getAllEntities($url) {
+    $sep = strpos($url, '?') !== false ? '&' : '?';
+    $ret = $this->curlRest($url . $sep . 'startIndex=0&count=1000000', '', 'application/json', 'GET');
+    $retDecoded = json_decode($ret, true);
+    $this->assertTrue($ret != $retDecoded && $ret != null, "Invalid json response: $retDecoded");
+    return $retDecoded['entry'];
+  }
+
+  /**
+   * NOTE: If there are lots of messages in the storage this test may take a long time as
+   * it retrieves all the message.
+   */
+  private function verifyLifeCycle($postData, $postDataFormat, $randomTitle) {
+    $url = '/messages/1/@outbox';
+
+    $cnt = count($this->getAllEntities($url));
+
+    // Creates the message.
+    $ret = $this->curlRest($url, $postData, $postDataFormat, 'POST');
+    $this->assertTrue(empty($ret), "Create message failed. Response: $ret");
+
+    // Gets the message.
+    $messages = $this->getAllEntities($url);
+    $this->assertEquals($cnt + 1, count($messages), "Size of the messages is not right.");
+    $fetchedMessage = null;
+    foreach ($messages as $m) {
+      if ($m['title'] == $randomTitle) {
+        $fetchedMessage = $m;
+      }
+    }
+    $this->assertNotNull($fetchedMessage, "Couldn't find the created message with title $randomTitle");
+
+    // Deletes the message.
+    $ret = $this->curlRest($url . '/' . urlencode($fetchedMessage['id']), '', 'application/json', 'DELETE');
+    $this->assertTrue(empty($ret), "Delete the created message failed. Response: $ret");
+
+    $messages = $this->getAllEntities($url, $randomTitle);
+    $this->assertEquals($cnt, count($messages), "Size of the messages is not right after deletion.");
+  }
+
+  public function testLifeCycleInJson() {
+    $randomTitle = "[" . rand(0, 2048) . "] message test title.";
+    $postData = '{
+      "id" : "msgid",
+      "recipients" : [2,3],
+      "title" : "' . $randomTitle . '",
+      "titleId" : "541141091700",
+      "body" : "Short message from Joe to some friends",
+      "bodyId" : "5491155811231",
+      "type" : "privateMessage",
+      "status" : "unread"
+    }';
+    $this->verifyLifeCycle($postData, 'application/json', $randomTitle);
+  }
+
+  public function testLifeCycleInXml() {
+    $randomTitle = "[" . rand(0, 2048) . "] message test title.";
+    $postData = '<message xmlns="http://ns.opensocial.org/2008/opensocial">
+      <recipient>2</recipient>
+      <recipient>3</recipient>
+      <title>' . $randomTitle . '</title>
+      <id>msgid</id>
+      <body>Click <a href="http://app.example.org/invites/{msgid}">here</a> to review your invitation.</body>
+    </message>';
+    $this->verifyLifeCycle($postData, 'application/xml', $randomTitle);
+  }
+
+  public function testLifeCycleInAtom() {
+    $randomTitle = "[" . rand(0, 2048) . "] message test title.";
+    $postData = '<entry xmlns="http://www.w3.org/2005/Atom"
+             xmlns:osapi="http://opensocial.org/2008/opensocialapi">
+      <osapi:recipient>2</osapi:recipient>
+      <osapi:recipient>3</osapi:recipient>
+      <title>' . $randomTitle . '</title>
+      <id>{msgid}</id>
+      <link rel="alternate" href="http://app.example.org/invites/{msgid}"/>
+      <content>Click <a href="http://app.example.org/invites/{msgid}">here</a> to review your invitation.</content>
+    </entry>';
+    $this->verifyLifeCycle($postData, 'application/atom+xml', $randomTitle);
+  }
+
+  public function testMessageCollectionLifeCycle() {
+    $url = '/messages/1';
+    // Gets number of message collections in the repository.
+    $cnt = count($this->getAllEntities($url));
+
+    // Creates a message collection.
+    $createData = array();
+    $createData['title'] = "[" . rand(0, 2048) . "] message collection test title.";
+    $createData['urls'] = array("http://abc.com/abc", "http://xyz.com/xyz");
+    $ret = $this->curlRest($url, json_encode($createData), 'application/json', 'POST');
+
+    // Verifies that whether the message collection is created.
+    $retDecoded = json_decode($ret, true);
+    $id = $retDecoded['entry']['id'];
+    $this->assertEquals($cnt + 1, count($this->getAllEntities($url)), "Wrong size of the collections. $ret");
+
+    // Updates the created message collection.
+    $newUrls = array("http://123.com/123");
+    $newTitle = 'new title';
+    $updateData = array();
+    $updateData['id'] = $id;
+    $updateData['title'] = $newTitle;
+    $updateData['urls'] = $newUrls;
+
+    $ret = $this->curlRest($url . "/$id", json_encode($updateData), 'application/json', 'PUT');
+    $this->assertTrue(empty($ret), "Update should return empty. $ret <$id>");
+
+    $collections = $this->getAllEntities($url);
+    $this->assertEquals($cnt + 1, count($collections), "Wrong size of the collections.");
+    $found = false;
+    foreach ($collections as $collection) {
+      if ($collection['id'] == $id) {
+        $this->assertEquals($newTitle, $collection['title']);
+        $this->assertEquals($newUrls, $collection['urls']);
+        $found = true;
+      }
+    }
+    $this->assertTrue($found, "Created message not found.");
+
+    // Deletes the message collection.
+    $ret = $this->curlRest($url . "/$id", '', 'application/json', 'DELETE');
+
+    // Verifies that the message collection is deleted.
+    $this->assertEquals($cnt, count($this->getAllEntities($url)), "Wrong size of the collections. $ret");
+  }
+}
diff --git a/trunk/php/test/social/MessageTest.php b/trunk/php/test/social/MessageTest.php
new file mode 100644
index 0000000..f56d9a1
--- /dev/null
+++ b/trunk/php/test/social/MessageTest.php
@@ -0,0 +1,103 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Message;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Message test case.
+ */
+class MessageTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Message
+   */
+  private $message;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->message = new Message(1, 'TITLE');
+    $this->message->setBody('BODY');
+    $this->message->setType('NOTIFICATION');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->message = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Message->getBody()
+   */
+  public function testGetBody() {
+    $this->assertEquals('BODY', $this->message->getBody());
+  }
+
+  /**
+   * Tests Message->getTitle()
+   */
+  public function testGetTitle() {
+    $this->assertEquals('TITLE', $this->message->getTitle());
+  }
+
+  /**
+   * Tests Message->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('NOTIFICATION', $this->message->getType());
+  }
+
+  /**
+   * Tests Message->sanitizeHTML()
+   */
+  public function testSanitizeHTML() {
+    $this->assertEquals('ABC', $this->message->sanitizeHTML('ABC'));
+  }
+
+  /**
+   * Tests Message->setBody()
+   */
+  public function testSetBody() {
+    $this->message->setBody('body');
+    $this->assertEquals('body', $this->message->getBody());
+  }
+
+  /**
+   * Tests Message->setTitle()
+   */
+  public function testSetTitle() {
+    $this->message->setTitle('title');
+    $this->assertEquals('title', $this->message->getTitle());
+  }
+
+  /**
+   * Tests Message->setType()
+   */
+  public function testSetType() {
+    $this->message->setType('EMAIL');
+    $this->assertEquals('EMAIL', $this->message->getType());
+  }
+}
diff --git a/trunk/php/test/social/NameTest.php b/trunk/php/test/social/NameTest.php
new file mode 100644
index 0000000..9c012e4
--- /dev/null
+++ b/trunk/php/test/social/NameTest.php
@@ -0,0 +1,178 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Name;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Name test case.
+ */
+class NameTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Name
+   */
+  private $Name;
+  
+  /**
+   * @var additionalName
+   */
+  public $additionalName;
+  
+  /**
+   * @var familyName
+   */
+  public $familyName;
+  
+  /**
+   * @var givenName
+   */
+  public $givenName;
+  
+  /**
+   * @var honorificPrefix
+   */
+  public $honorificPrefix;
+  
+  /**
+   * @var honorificSuffix
+   */
+  public $honorificSuffix;
+  
+  /**
+   * @var unstructured
+   */
+  public $unstructured = '';
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Name = new Name($this->unstructured);
+  
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Name = null;
+    
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Name->getAdditionalName()
+   */
+  public function testGetAdditionalName() {
+    $this->Name->additionalName = $this->additionalName;
+    $this->assertEquals($this->Name->getAdditionalName(), $this->additionalName);
+  }
+
+  /**
+   * Tests Name->getFamilyName()
+   */
+  public function testGetFamilyName() {
+    $this->Name->familyName = $this->familyName;
+    $this->assertEquals($this->Name->getFamilyName(), $this->familyName);
+  }
+
+  /**
+   * Tests Name->getGivenName()
+   */
+  public function testGetGivenName() {
+    $this->Name->givenName = $this->givenName;
+    $this->assertEquals($this->Name->getGivenName(), $this->givenName);
+  }
+
+  /**
+   * Tests Name->getHonorificPrefix()
+   */
+  public function testGetHonorificPrefix() {
+    $this->Name->honorificPrefix = $this->honorificPrefix;
+    $this->assertEquals($this->Name->getHonorificPrefix(), $this->honorificPrefix);
+  }
+
+  /**
+   * Tests Name->getHonorificSuffix()
+   */
+  public function testGetHonorificSuffix() {
+    $this->Name->honorificSuffix = $this->honorificSuffix;
+    $this->assertEquals($this->Name->getHonorificSuffix(), $this->honorificSuffix);
+  }
+
+  /**
+   * Tests Name->getUnstructured()
+   */
+  public function testGetUnstructured() {
+    $this->Name->unstructured = $this->unstructured;
+    $this->assertEquals($this->Name->getFormatted(), $this->unstructured);
+  }
+
+  /**
+   * Tests Name->setAdditionalName()
+   */
+  public function testSetAdditionalName() {
+    $this->Name->setAdditionalName($this->additionalName);
+    $this->assertEquals($this->Name->getAdditionalName(), $this->additionalName);
+  }
+
+  /**
+   * Tests Name->setFamilyName()
+   */
+  public function testSetFamilyName() {
+    $this->Name->setFamilyName($this->familyName);
+    $this->assertEquals($this->Name->getFamilyName(), $this->familyName);
+  }
+
+  /**
+   * Tests Name->setGivenName()
+   */
+  public function testSetGivenName() {
+    $this->Name->setGivenName($this->givenName);
+    $this->assertEquals($this->Name->getGivenName(), $this->givenName);
+  }
+
+  /**
+   * Tests Name->setHonorificPrefix()
+   */
+  public function testSetHonorificPrefix() {
+    $this->Name->setHonorificPrefix($this->honorificPrefix);
+    $this->assertEquals($this->Name->getHonorificPrefix(), $this->honorificPrefix);
+  
+  }
+
+  /**
+   * Tests Name->setHonorificSuffix()
+   */
+  public function testSetHonorificSuffix() {
+    $this->Name->setHonorificSuffix($this->honorificSuffix);
+    $this->assertEquals($this->Name->getHonorificSuffix(), $this->honorificSuffix);
+  }
+
+  /**
+   * Tests Name->setUnstructured()
+   */
+  public function testSetUnstructured() {
+    $this->Name->setFormatted($this->unstructured);
+    $this->assertEquals($this->Name->getFormatted(), $this->unstructured);
+  }
+}
diff --git a/trunk/php/test/social/OrganizationTest.php b/trunk/php/test/social/OrganizationTest.php
new file mode 100644
index 0000000..3733d57
--- /dev/null
+++ b/trunk/php/test/social/OrganizationTest.php
@@ -0,0 +1,210 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Organization;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Organization test case.
+ */
+class OrganizationTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Organization
+   */
+  private $Organization;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    
+    $this->Organization = new Organization('NAME');
+    $this->Organization->address = 'ADDRESS';
+    $this->Organization->description = 'DESCRIPTION';
+    $this->Organization->endDate = 'ENDDATE';
+    $this->Organization->field = 'FIELD';
+    $this->Organization->name = 'NAME';
+    $this->Organization->salary = 'SALARY';
+    $this->Organization->startDate = 'STARTDATE';
+    $this->Organization->subField = 'SUBFIELD';
+    $this->Organization->title = 'TITLE';
+    $this->Organization->webpage = 'WEBPAGE';
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Organization = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Organization->getAddress()
+   */
+  public function testGetAddress() {
+    $this->assertEquals('ADDRESS', $this->Organization->getAddress());
+  }
+
+  /**
+   * Tests Organization->getDescription()
+   */
+  public function testGetDescription() {
+    $this->assertEquals('DESCRIPTION', $this->Organization->getDescription());
+  }
+
+  /**
+   * Tests Organization->getEndDate()
+   */
+  public function testGetEndDate() {
+    $this->assertEquals('ENDDATE', $this->Organization->getEndDate());
+  }
+
+  /**
+   * Tests Organization->getField()
+   */
+  public function testGetField() {
+    $this->assertEquals('FIELD', $this->Organization->getField());
+  }
+
+  /**
+   * Tests Organization->getName()
+   */
+  public function testGetName() {
+    $this->assertEquals('NAME', $this->Organization->getName());
+  }
+
+  /**
+   * Tests Organization->getSalary()
+   */
+  public function testGetSalary() {
+    $this->assertEquals('SALARY', $this->Organization->getSalary());
+  }
+
+  /**
+   * Tests Organization->getStartDate()
+   */
+  public function testGetStartDate() {
+    $this->assertEquals('STARTDATE', $this->Organization->getStartDate());
+  }
+
+  /**
+   * Tests Organization->getSubField()
+   */
+  public function testGetSubField() {
+    $this->assertEquals('SUBFIELD', $this->Organization->getSubField());
+  }
+
+  /**
+   * Tests Organization->getTitle()
+   */
+  public function testGetTitle() {
+    $this->assertEquals('TITLE', $this->Organization->getTitle());
+  }
+
+  /**
+   * Tests Organization->getWebpage()
+   */
+  public function testGetWebpage() {
+    $this->assertEquals('WEBPAGE', $this->Organization->getWebpage());
+  }
+
+  /**
+   * Tests Organization->setAddress()
+   */
+  public function testSetAddress() {
+    $this->Organization->setAddress('address');
+    $this->assertEquals('address', $this->Organization->address);
+  }
+
+  /**
+   * Tests Organization->setDescription()
+   */
+  public function testSetDescription() {
+    $this->Organization->setDescription('description');
+    $this->assertEquals('description', $this->Organization->description);
+  }
+
+  /**
+   * Tests Organization->setEndDate()
+   */
+  public function testSetEndDate() {
+    $this->Organization->setEndDate('enddate');
+    $this->assertEquals('enddate', $this->Organization->endDate);
+  }
+
+  /**
+   * Tests Organization->setField()
+   */
+  public function testSetField() {
+    $this->Organization->setField('field');
+    $this->assertEquals('field', $this->Organization->field);
+  }
+
+  /**
+   * Tests Organization->setName()
+   */
+  public function testSetName() {
+    $this->Organization->setName('name');
+    $this->assertEquals('name', $this->Organization->name);
+  }
+
+  /**
+   * Tests Organization->setSalary()
+   */
+  public function testSetSalary() {
+    $this->Organization->setSalary('salary');
+    $this->assertEquals('salary', $this->Organization->salary);
+  }
+
+  /**
+   * Tests Organization->setStartDate()
+   */
+  public function testSetStartDate() {
+    $this->Organization->setStartDate('startdate');
+    $this->assertEquals('startdate', $this->Organization->startDate);
+  }
+
+  /**
+   * Tests Organization->setSubField()
+   */
+  public function testSetSubField() {
+    $this->Organization->setSubField('subfield');
+    $this->assertEquals('subfield', $this->Organization->subField);
+  }
+
+  /**
+   * Tests Organization->setTitle()
+   */
+  public function testSetTitle() {
+    $this->Organization->setTitle('title');
+    $this->assertEquals('title', $this->Organization->title);
+  }
+
+  /**
+   * Tests Organization->setWebpage()
+   */
+  public function testSetWebpage() {
+    $this->Organization->setWebpage('webpage');
+    $this->assertEquals('webpage', $this->Organization->webpage);
+  }
+}
diff --git a/trunk/php/test/social/OutputAtomConverterTest.php b/trunk/php/test/social/OutputAtomConverterTest.php
new file mode 100644
index 0000000..717163c
--- /dev/null
+++ b/trunk/php/test/social/OutputAtomConverterTest.php
@@ -0,0 +1,99 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\OutputAtomConverter;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\social\service\RestRequestItem;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * OutputAtomConverter test case.
+ */
+class OutputAtomConverterTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var OutputAtomConverter
+   */
+  private $OutputAtomConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    $_SERVER['REQUEST_METHOD'] = 'GET';
+    $_SERVER["HTTP_HOST"] = 'localhost';
+    parent::setUp();
+    $this->OutputAtomConverter = new OutputAtomConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->OutputAtomConverter = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests OutputAtomConverter->outputResponse()
+   */
+  public function testOutputResponse() {
+    $outputConverter = new OutputAtomConverter();
+    $servletRequest = array('url' => '/people/1/@self');
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, 'convertAtom', $outputConverter);
+    $requestItem->applyUrlTemplate("/people/{userId}/{groupId}/{personId}");
+    $entry = array('isOwner' => false, 'isViewer' => false,
+                   'displayName' => '1 1', 'id' => '1');
+    $response = array('entry' => $entry);
+    $responseItem = new ResponseItem(null, null, $response);
+    ob_start();
+    $outputConverter->outputResponse($responseItem, $requestItem);
+    $output = ob_get_clean();
+    $expected = '<entry xmlns="http://www.w3.org/2005/Atom">
+  <title>person entry for shindig:1</title>
+  <author>
+    <uri>urn:guid:1</uri>
+    <name>shindig:1</name>
+  </author>
+  <id>urn:guid:1</id>
+  <updated>2008-12-11T19:58:31+01:00</updated>
+  <content type="application/xml">
+    <entry xmlns="http://ns.opensocial.org/2008/opensocial">
+      <isOwner></isOwner>
+      <isViewer></isViewer>
+      <displayName>1 1</displayName>
+      <id>1</id>
+    </entry>
+  </content>
+</entry>
+';
+    $outputXml = simplexml_load_string($output);
+    $expectedXml = simplexml_load_string($expected);
+    $expectedXml->updated = $outputXml->updated;
+    // Prefix may be 'shindig' or something else.
+    $expectedXml->title = $outputXml->title; 
+    $expectedXml->author->name = $outputXml->author->name;
+    $this->assertEquals($expectedXml, $outputXml);
+  }
+
+}
+
diff --git a/trunk/php/test/social/OutputJsonConverterTest.php b/trunk/php/test/social/OutputJsonConverterTest.php
new file mode 100644
index 0000000..0bdbe8c
--- /dev/null
+++ b/trunk/php/test/social/OutputJsonConverterTest.php
@@ -0,0 +1,123 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\OutputJsonConverter;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\social\service\RestRequestItem;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * OutputJsonConverter test case.
+ */
+class OutputJsonConverterTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var OutputJsonConverter
+   */
+  private $OutputJsonConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    $_SERVER['REQUEST_METHOD'] = 'GET';
+    parent::setUp();
+    $this->OutputJsonConverter = new OutputJsonConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->OutputJsonConverter = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests OutputJsonConverter->outputResponse()
+   */
+  public function testOutputResponse() {
+    $outputConverter = new OutputJsonConverter();
+    $servletRequest = array('url' => '/people/1/@self');
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, 'convertJson', $outputConverter);
+    $requestItem->applyUrlTemplate("/people/{userId}/{groupId}/{personId}");
+    $response = array(
+        'entry' => array('isOwner' => false, 'isViewer' => false, 'displayName' => '1 1', 
+            'id' => '1'));
+    $responseItem = new ResponseItem(null, null, $response);
+    ob_start();
+    $outputConverter->outputResponse($responseItem, $requestItem);
+    $output = ob_get_clean();
+    $expected = '{
+        "entry": {
+          "isOwner": false,
+          "isViewer": false,
+          "displayName": "1 1",
+          "id": "1"
+        }
+    }';
+    $outputJson = json_decode($output);
+    $expectedJson = json_decode($expected);
+    $this->assertEquals($expectedJson, $outputJson);
+  }
+
+  public function testOutputJsonPResponse() {
+    $_GET['callback'] = 'cb';
+    $outputConverter = new OutputJsonConverter();
+    $servletRequest = array('url' => '/people/1/@self');
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, 'convertJson', $outputConverter);
+    $requestItem->applyUrlTemplate("/people/{userId}/{groupId}/{personId}");
+    $response = array(
+        'entry' => array('isOwner' => false, 'isViewer' => false, 'displayName' => '1 1',
+            'id' => '1'));
+    $responseItem = new ResponseItem(null, null, $response);
+    ob_start();
+    $outputConverter->outputResponse($responseItem, $requestItem);
+    $output = ob_get_clean();
+    $expected = 'cb({"entry":{"isOwner":false,"isViewer":false,"displayName":"1 1","id":"1"}})';
+    unset($_GET['callback']);
+    $this->assertEquals($expected, $output);
+  }
+
+  public function testOutputJsonPResponseWithInvalidCallback() {
+    $_GET['callback'] = 'alert(1);cb';
+    $outputConverter = new OutputJsonConverter();
+    $servletRequest = array('url' => '/people/1/@self');
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, 'convertJson', $outputConverter);
+    $requestItem->applyUrlTemplate("/people/{userId}/{groupId}/{personId}");
+    $response = array(
+        'entry' => array('isOwner' => false, 'isViewer' => false, 'displayName' => '1 1',
+            'id' => '1'));
+    $responseItem = new ResponseItem(null, null, $response);
+    ob_start();
+    $outputConverter->outputResponse($responseItem, $requestItem);
+    $output = ob_get_clean();
+    $expected = '{"entry":{"isOwner":false,"isViewer":false,"displayName":"1 1","id":"1"}}';
+    unset($_GET['callback']);
+    $outputJson = json_decode($output);
+    $expectedJson = json_decode($expected);
+    $this->assertEquals($expectedJson, $outputJson);
+  }
+}
+
diff --git a/trunk/php/test/social/OutputXmlConverterTest.php b/trunk/php/test/social/OutputXmlConverterTest.php
new file mode 100644
index 0000000..8123e67
--- /dev/null
+++ b/trunk/php/test/social/OutputXmlConverterTest.php
@@ -0,0 +1,86 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\OutputXmlConverter;
+use apache\shindig\social\service\ResponseItem;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\social\service\RestRequestItem;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * OutputXmlConverter test case.
+ */
+class OutputXmlConverterTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var OutputXmlConverter
+   */
+  private $OutputXmlConverter;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    $_SERVER['REQUEST_METHOD'] = 'GET';
+    parent::setUp();
+    $this->OutputXmlConverter = new OutputXmlConverter();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->OutputXmlConverter = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests OutputXmlConverter->outputResponse()
+   */
+  public function testOutputResponse() {
+    $outputConverter = new OutputXmlConverter();
+    $servletRequest = array('url' => '/people/1/@self');
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, 'convertXml', $outputConverter);
+    $requestItem->applyUrlTemplate("/people/{userId}/{groupId}/{personId}");
+    $entry = array('isOwner' => false, 'isViewer' => false,
+                   'displayName' => '1 1', 'id' => '1');
+    $response = array('entry' => $entry);
+    $responseItem = new ResponseItem(null, null, $response);
+    ob_start();
+    $outputConverter->outputResponse($responseItem, $requestItem);
+    $output = ob_get_clean();
+    $expected = '<?xml version="1.0" encoding="UTF-8"?>
+<response>
+  <entry>
+    <isOwner></isOwner>
+    <isViewer></isViewer>
+    <displayName>1 1</displayName>
+    <id>1</id>
+  </entry>
+</response>
+';
+    $outputXml = simplexml_load_string($output);
+    $expectedXml = simplexml_load_string($expected);
+    $this->assertEquals($expectedXml, $outputXml);
+  }
+
+}
+
diff --git a/trunk/php/test/social/PersonTest.php b/trunk/php/test/social/PersonTest.php
new file mode 100644
index 0000000..7924a75
--- /dev/null
+++ b/trunk/php/test/social/PersonTest.php
@@ -0,0 +1,901 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Person;
+use apache\shindig\social\model\EnumLookingFor;
+use apache\shindig\social\model\EnumDrinker;
+use apache\shindig\social\model\EnumPresence;
+use apache\shindig\social\model\EnumSmoker;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Person test case.
+ */
+class PersonTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Person
+   */
+  private $Person;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Person = new Person('ID', 'NAME');
+    $this->Person->aboutMe = 'ABOUTME';
+    $this->Person->activities = 'ACTIVITIES';
+    $this->Person->addresses = 'ADDRESSES';
+    $this->Person->age = 'AGE';
+    $this->Person->bodyType = 'BODYTYPE';
+    $this->Person->books = 'BOOKS';
+    $this->Person->cars = 'CARS';
+    $this->Person->children = 'CHILDREN';
+    $this->Person->currentLocation = 'CURRENTLOCATION';
+    $this->Person->dateOfBirth = 'DATEOFBIRTH';
+    $this->Person->drinker = 'HEAVILY';
+    $this->Person->emails = 'EMAILS';
+    $this->Person->ethnicity = 'ETHNICITY';
+    $this->Person->fashion = 'FASHION';
+    $this->Person->food = 'FOOD';
+    $this->Person->gender = 'GENDER';
+    $this->Person->happiestWhen = 'HAPPIESTWHEN';
+    $this->Person->hasApp = 'HASAPP';
+    $this->Person->heroes = 'HEROES';
+    $this->Person->humor = 'HUMOR';
+    $this->Person->interests = 'INTERESTS';
+    $this->Person->jobInterests = 'JOBINTERESTS';
+    $this->Person->jobs = 'JOBS';
+    $this->Person->languagesSpoken = 'LANGUAGESSPOKEN';
+    $this->Person->livingArrangement = 'LIVINGARRANGEMENT';
+    //$this->Person->lookingFor = new EnumLookingFor('FRIENDS');
+    $this->Person->movies = 'MOVIES';
+    $this->Person->music = 'MUSIC';
+    $this->Person->networkPresence = 'NETWORKPRESENCE';
+    $this->Person->nickname = 'NICKNAME';
+    $this->Person->pets = 'PETS';
+    $this->Person->phoneNumbers = 'PHONENUMBERS';
+    $this->Person->politicalViews = 'POLITICALVIEWS';
+    $this->Person->profileSong = 'PROFILESONG';
+    $this->Person->profileUrl = 'PROFILEURL';
+    $this->Person->profileVideo = 'PROFILEVIDEO';
+    $this->Person->quotes = 'QUOTES';
+    $this->Person->relationshipStatus = 'RELATIONSHIPSTATUS';
+    $this->Person->religion = 'RELIGION';
+    $this->Person->romance = 'ROMANCE';
+    $this->Person->scaredOf = 'SCAREDOF';
+    $this->Person->schools = 'SCHOOLS';
+    $this->Person->sexualOrientation = 'SEXUALORIENTATION';
+    $this->Person->smoker = 'SMOKER';
+    $this->Person->sports = 'SPORTS';
+    $this->Person->status = 'STATUS';
+    $this->Person->tags = 'TAGS';
+    $this->Person->thumbnailUrl = 'THUMBNAILSURL';
+    $this->Person->timeZone = 'TIMEZONE';
+    $this->Person->turnOffs = 'TURNOFFS';
+    $this->Person->turnOns = 'TURNONS';
+    $this->Person->tvShows = 'TVSHOWS';
+    $this->Person->urls = 'URLS';
+    $this->Person->isOwner = 'ISOWNER';
+    $this->Person->isViewer = 'ISVIEWER';
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Person = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Person->getAboutMe()
+   */
+  public function testGetAboutMe() {
+    $this->assertEquals('ABOUTME', $this->Person->getAboutMe());
+  }
+
+  /**
+   * Tests Person->getActivities()
+   */
+  public function testGetActivities() {
+    $this->assertEquals('ACTIVITIES', $this->Person->getActivities());
+  }
+
+  /**
+   * Tests Person->getAddresses()
+   */
+  public function testGetAddresses() {
+    $this->assertEquals('ADDRESSES', $this->Person->getAddresses());
+  }
+
+  /**
+   * Tests Person->getAge()
+   */
+  public function testGetAge() {
+    $this->assertEquals('AGE', $this->Person->getAge());
+  }
+
+  /**
+   * Tests Person->getBodyType()
+   */
+  public function testGetBodyType() {
+    $this->assertEquals('BODYTYPE', $this->Person->getBodyType());
+  }
+
+  /**
+   * Tests Person->getBooks()
+   */
+  public function testGetBooks() {
+    $this->assertEquals('BOOKS', $this->Person->getBooks());
+  }
+
+  /**
+   * Tests Person->getCars()
+   */
+  public function testGetCars() {
+    $this->assertEquals('CARS', $this->Person->getCars());
+  
+  }
+
+  /**
+   * Tests Person->getChildren()
+   */
+  public function testGetChildren() {
+    $this->assertEquals('CHILDREN', $this->Person->getChildren());
+  }
+
+  /**
+   * Tests Person->getCurrentLocation()
+   */
+  public function testGetCurrentLocation() {
+    $this->assertEquals('CURRENTLOCATION', $this->Person->getCurrentLocation());
+  }
+
+  /**
+   * Tests Person->getDateOfBirth()
+   */
+  public function testGetDateOfBirth() {
+    $this->Person->setBirthday('10/10/2010');
+    $this->assertEquals('2010-10-10', $this->Person->getBirthday());
+  }
+
+  /**
+   * Tests Person->getDrinker()
+   */
+  public function testGetDrinker() {
+    //$drinker = new EnumDrinker('HEAVILY');
+    $this->Person->setDrinker('HEAVILY');
+    $this->assertEquals('HEAVILY', $this->Person->getDrinker());
+  }
+
+  /**
+   * Tests Person->getEmails()
+   */
+  public function testGetEmails() {
+    $this->assertEquals('EMAILS', $this->Person->getEmails());
+  
+  }
+
+  /**
+   * Tests Person->getEthnicity()
+   */
+  public function testGetEthnicity() {
+    $this->assertEquals('ETHNICITY', $this->Person->getEthnicity());
+  }
+
+  /**
+   * Tests Person->getFashion()
+   */
+  public function testGetFashion() {
+    $this->assertEquals('FASHION', $this->Person->getFashion());
+  
+  }
+
+  /**
+   * Tests Person->getFood()
+   */
+  public function testGetFood() {
+    $this->assertEquals('FOOD', $this->Person->getFood());
+  }
+
+  /**
+   * Tests Person->getGender()
+   */
+  public function testGetGender() {
+    $this->Person->setGender('FEMALE');
+    $this->assertEquals('FEMALE', $this->Person->getGender());
+  }
+
+  /**
+   * Tests Person->getHappiestWhen()
+   */
+  public function testGetHappiestWhen() {
+    $this->assertEquals('HAPPIESTWHEN', $this->Person->getHappiestWhen());
+  }
+
+  /**
+   * Tests Person->getHasApp()
+   */
+  public function testGetHasApp() {
+    $this->assertEquals('HASAPP', $this->Person->getHasApp());
+  }
+
+  /**
+   * Tests Person->getHeroes()
+   */
+  public function testGetHeroes() {
+    $this->assertEquals('HEROES', $this->Person->getHeroes());
+  }
+
+  /**
+   * Tests Person->getHumor()
+   */
+  public function testGetHumor() {
+    $this->assertEquals('HUMOR', $this->Person->getHumor());
+  }
+
+  /**
+   * Tests Person->getId()
+   */
+  public function testGetId() {
+    $this->assertEquals('ID', $this->Person->getId());
+  }
+
+  /**
+   * Tests Person->getInterests()
+   */
+  public function testGetInterests() {
+    $this->assertEquals('INTERESTS', $this->Person->getInterests());
+  }
+
+  /**
+   * Tests Person->getIsOwner()
+   */
+  public function testGetIsOwner() {
+    $this->assertEquals('ISOWNER', $this->Person->getIsOwner());
+  
+  }
+
+  /**
+   * Tests Person->getIsViewer()
+   */
+  public function testGetIsViewer() {
+    $this->assertEquals('ISVIEWER', $this->Person->getIsViewer());
+  }
+
+  /**
+   * Tests Person->getJobInterests()
+   */
+  public function testGetJobInterests() {
+    $this->assertEquals('JOBINTERESTS', $this->Person->getJobInterests());
+  }
+
+  /**
+   * Tests Person->getLanguagesSpoken()
+   */
+  public function testGetLanguagesSpoken() {
+    $this->assertEquals('LANGUAGESSPOKEN', $this->Person->getLanguagesSpoken());
+  }
+
+  /**
+   * Tests Person->getLivingArrangement()
+   */
+  public function testGetLivingArrangement() {
+    $this->assertEquals('LIVINGARRANGEMENT', $this->Person->getLivingArrangement());
+  }
+
+  /**
+   * Tests Person->getMovies()
+   */
+  public function testGetMovies() {
+    $this->assertEquals('MOVIES', $this->Person->getMovies());
+  }
+
+  /**
+   * Tests Person->getMusic()
+   */
+  public function testGetMusic() {
+    $this->assertEquals('MUSIC', $this->Person->getMusic());
+  }
+
+  /**
+   * Tests Person->getName()
+   */
+  public function testGetName() {
+    $this->assertEquals('NAME', $this->Person->getName());
+  }
+
+  /**
+   * Tests Person->getNetworkPresence()
+   */
+  public function testGetNetworkPresence() {
+    $presence = new EnumPresence('DND');
+    $this->Person->setNetworkPresence('DND');
+    $this->assertEquals($presence, $this->Person->getNetworkPresence());
+  }
+
+  /**
+   * Tests Person->getNickname()
+   */
+  public function testGetNickname() {
+    $this->assertEquals('NICKNAME', $this->Person->getNickname());
+  
+  }
+
+  /**
+   * Tests Person->getPets()
+   */
+  public function testGetPets() {
+    $this->assertEquals('PETS', $this->Person->getPets());
+  }
+
+  /**
+   * Tests Person->getPhoneNumbers()
+   */
+  public function testGetPhoneNumbers() {
+    $this->assertEquals('PHONENUMBERS', $this->Person->getPhoneNumbers());
+  }
+
+  /**
+   * Tests Person->getPoliticalViews()
+   */
+  public function testGetPoliticalViews() {
+    $this->assertEquals('POLITICALVIEWS', $this->Person->getPoliticalViews());
+  }
+
+  /**
+   * Tests Person->getProfileSong()
+   */
+  public function testGetProfileSong() {
+    $this->assertEquals('PROFILESONG', $this->Person->getProfileSong());
+  }
+
+  /**
+   * Tests Person->getProfileUrl()
+   */
+  public function testGetProfileUrl() {
+    $this->assertEquals('PROFILEURL', $this->Person->getProfileUrl());
+  }
+
+  /**
+   * Tests Person->getProfileVideo()
+   */
+  public function testGetProfileVideo() {
+    $this->assertEquals('PROFILEVIDEO', $this->Person->getProfileVideo());
+  }
+
+  /**
+   * Tests Person->getQuotes()
+   */
+  public function testGetQuotes() {
+    $this->assertEquals('QUOTES', $this->Person->getQuotes());
+  }
+
+  /**
+   * Tests Person->getRelationshipStatus()
+   */
+  public function testGetRelationshipStatus() {
+    $this->assertEquals('RELATIONSHIPSTATUS', $this->Person->getRelationshipStatus());
+  }
+
+  /**
+   * Tests Person->getReligion()
+   */
+  public function testGetReligion() {
+    $this->assertEquals('RELIGION', $this->Person->getReligion());
+  }
+
+  /**
+   * Tests Person->getRomance()
+   */
+  public function testGetRomance() {
+    $this->assertEquals('ROMANCE', $this->Person->getRomance());
+  }
+
+  /**
+   * Tests Person->getScaredOf()
+   */
+  public function testGetScaredOf() {
+    $this->assertEquals('SCAREDOF', $this->Person->getScaredOf());
+  }
+
+  /**
+   * Tests Person->getSexualOrientation()
+   */
+  public function testGetSexualOrientation() {
+    $this->assertEquals('SEXUALORIENTATION', $this->Person->getSexualOrientation());
+  }
+
+  /**
+   * Tests Person->getSmoker()
+   */
+  public function testGetSmoker() {
+    $smoker = new EnumSmoker('OCCASIONALLY');
+    $this->Person->setSmoker('OCCASIONALLY');
+    $this->assertEquals($smoker, $this->Person->getSmoker());
+  }
+
+  /**
+   * Tests Person->getSports()
+   */
+  public function testGetSports() {
+    $this->assertEquals('SPORTS', $this->Person->getSports());
+  }
+
+  /**
+   * Tests Person->getStatus()
+   */
+  public function testGetStatus() {
+    $this->assertEquals('STATUS', $this->Person->getStatus());
+  }
+
+  /**
+   * Tests Person->getTags()
+   */
+  public function testGetTags() {
+    $this->assertEquals('TAGS', $this->Person->getTags());
+  }
+
+  /**
+   * Tests Person->getThumbnailUrl()
+   */
+  public function testGetThumbnailUrl() {
+    $this->assertEquals('THUMBNAILSURL', $this->Person->getThumbnailUrl());
+  }
+
+  /**
+   * Tests Person->getTurnOffs()
+   */
+  public function testGetTurnOffs() {
+    $this->assertEquals('TURNOFFS', $this->Person->getTurnOffs());
+  }
+
+  /**
+   * Tests Person->getTurnOns()
+   */
+  public function testGetTurnOns() {
+    $this->assertEquals('TURNONS', $this->Person->getTurnOns());
+  }
+
+  /**
+   * Tests Person->getTvShows()
+   */
+  public function testGetTvShows() {
+    $this->assertEquals('TVSHOWS', $this->Person->getTvShows());
+  }
+
+  /**
+   * Tests Person->getUrls()
+   */
+  public function testGetUrls() {
+    $this->assertEquals('URLS', $this->Person->getUrls());
+  }
+
+  /**
+   * Tests Person->setAboutMe()
+   */
+  public function testSetAboutMe() {
+    $this->Person->setAboutMe('aboutme');
+    $this->assertEquals('aboutme', $this->Person->aboutMe);
+  }
+
+  /**
+   * Tests Person->setActivities()
+   */
+  public function testSetActivities() {
+    $this->Person->setActivities('activities');
+    $this->assertEquals('activities', $this->Person->activities);
+  }
+
+  /**
+   * Tests Person->setAddresses()
+   */
+  public function testSetAddresses() {
+    $this->Person->setAddresses('addresses');
+    $this->assertEquals('addresses', $this->Person->addresses);
+  }
+
+  /**
+   * Tests Person->setAge()
+   */
+  public function testSetAge() {
+    $this->Person->setAge('age');
+    $this->assertEquals('age', $this->Person->age);
+  }
+
+  /**
+   * Tests Person->setBodyType()
+   */
+  public function testSetBodyType() {
+    $this->Person->setBodyType('bodytype');
+    $this->assertEquals('bodytype', $this->Person->bodyType);
+  }
+
+  /**
+   * Tests Person->setBooks()
+   */
+  public function testSetBooks() {
+    $this->Person->setBooks('books');
+    $this->assertEquals('books', $this->Person->books);
+  }
+
+  /**
+   * Tests Person->setCars()
+   */
+  public function testSetCars() {
+    $this->Person->setCars('cars');
+    $this->assertEquals('cars', $this->Person->cars);
+  }
+
+  /**
+   * Tests Person->setChildren()
+   */
+  public function testSetChildren() {
+    $this->Person->setChildren('children');
+    $this->assertEquals('children', $this->Person->children);
+  }
+
+  /**
+   * Tests Person->setCurrentLocation()
+   */
+  public function testSetCurrentLocation() {
+    $this->Person->setCurrentLocation('currentlocation');
+    $this->assertEquals('currentlocation', $this->Person->currentLocation);
+  }
+
+  /**
+   * Tests Person->setDateOfBirth()
+   */
+  public function testSetDateOfBirth() {
+    $this->Person->setBirthday('10/10/2010');
+    $this->assertEquals('2010-10-10', $this->Person->getBirthday());
+  }
+
+  /**
+   * Tests Person->setEmails()
+   */
+  public function testSetEmails() {
+    $this->Person->setEmails('emails');
+    $this->assertEquals('emails', $this->Person->emails);
+  }
+
+  /**
+   * Tests Person->setEthnicity()
+   */
+  public function testSetEthnicity() {
+    $this->Person->setEthnicity('ethnicity');
+    $this->assertEquals('ethnicity', $this->Person->ethnicity);
+  }
+
+  /**
+   * Tests Person->setFashion()
+   */
+  public function testSetFashion() {
+    $this->Person->setFashion('fashion');
+    $this->assertEquals('fashion', $this->Person->fashion);
+  }
+
+  /**
+   * Tests Person->setFood()
+   */
+  public function testSetFood() {
+    $this->Person->setFood('food');
+    $this->assertEquals('food', $this->Person->food);
+  }
+
+  /**
+   * Tests Person->setGender()
+   */
+  public function testSetGender() {
+    $this->Person->setGender('MALE');
+    $this->assertEquals('MALE', $this->Person->gender);
+  }
+
+  /**
+   * Tests Person->setHappiestWhen()
+   */
+  public function testSetHappiestWhen() {
+    $this->Person->setHappiestWhen('happiestwhen');
+    $this->assertEquals('happiestwhen', $this->Person->happiestWhen);
+  }
+
+  /**
+   * Tests Person->setHasApp()
+   */
+  public function testSetHasApp() {
+    $this->Person->setHasApp('hasapp');
+    $this->assertEquals('hasapp', $this->Person->hasApp);
+  }
+
+  /**
+   * Tests Person->setHeroes()
+   */
+  public function testSetHeroes() {
+    $this->Person->setHeroes('heroes');
+    $this->assertEquals('heroes', $this->Person->heroes);
+  }
+
+  /**
+   * Tests Person->setHumor()
+   */
+  public function testSetHumor() {
+    $this->Person->setHumor('humor');
+    $this->assertEquals('humor', $this->Person->humor);
+  }
+
+  /**
+   * Tests Person->setId()
+   */
+  public function testSetId() {
+    $this->Person->setId('id');
+    $this->assertEquals('id', $this->Person->id);
+  }
+
+  /**
+   * Tests Person->setInterests()
+   */
+  public function testSetInterests() {
+    $this->Person->setInterests('interests');
+    $this->assertEquals('interests', $this->Person->interests);
+  }
+
+  /**
+   * Tests Person->setIsOwner()
+   */
+  public function testSetIsOwner() {
+    $this->Person->setIsOwner('isowner');
+    $this->assertEquals('isowner', $this->Person->isOwner);
+  }
+
+  /**
+   * Tests Person->setIsViewer()
+   */
+  public function testSetIsViewer() {
+    $this->Person->setIsViewer('isviewer');
+    $this->assertEquals('isviewer', $this->Person->isViewer);
+  }
+
+  /**
+   * Tests Person->setJobInterests()
+   */
+  public function testSetJobInterests() {
+    $this->Person->setJobInterests('jobinterests');
+    $this->assertEquals('jobinterests', $this->Person->jobInterests);
+  }
+
+  /**
+   * Tests Person->setLanguagesSpoken()
+   */
+  public function testSetLanguagesSpoken() {
+    $this->Person->setLanguagesSpoken('languagesspoken');
+    $this->assertEquals('languagesspoken', $this->Person->languagesSpoken);
+  }
+
+  /**
+   * Tests Person->setLivingArrangement()
+   */
+  public function testSetLivingArrangement() {
+    $this->Person->setLivingArrangement('livingarrangement');
+    $this->assertEquals('livingarrangement', $this->Person->livingArrangement);
+  }
+
+  /**
+   * Tests Person->setLookingFor()
+   */
+  public function testSetLookingFor() {
+    $lookingFor = new EnumLookingFor('FRIENDS');
+    $this->Person->setLookingFor('FRIENDS');
+    $this->assertEquals($lookingFor, $this->Person->getLookingFor());
+  }
+
+  /**
+   * Tests Person->setMovies()
+   */
+  public function testSetMovies() {
+    $this->Person->setMovies('movies');
+    $this->assertEquals('movies', $this->Person->movies);
+  }
+
+  /**
+   * Tests Person->setMusic()
+   */
+  public function testSetMusic() {
+    $this->Person->setMusic('music');
+    $this->assertEquals('music', $this->Person->music);
+  }
+
+  /**
+   * Tests Person->setName()
+   */
+  public function testSetName() {
+    $this->Person->setName('name');
+    $this->assertEquals('name', $this->Person->name);
+  }
+
+  /**
+   * Tests Person->setNickname()
+   */
+  public function testSetNickname() {
+    $this->Person->setNickname('nickname');
+    $this->assertEquals('nickname', $this->Person->nickname);
+  }
+
+  /**
+   * Tests Person->setPets()
+   */
+  public function testSetPets() {
+    $this->Person->setPets('pets');
+    $this->assertEquals('pets', $this->Person->pets);
+  }
+
+  /**
+   * Tests Person->setPhoneNumbers()
+   */
+  public function testSetPhoneNumbers() {
+    $this->Person->setPhoneNumbers('phonenumbers');
+    $this->assertEquals('phonenumbers', $this->Person->phoneNumbers);
+  }
+
+  /**
+   * Tests Person->setPoliticalViews()
+   */
+  public function testSetPoliticalViews() {
+    $this->Person->setPoliticalViews('politicalviews');
+    $this->assertEquals('politicalviews', $this->Person->politicalViews);
+  }
+
+  /**
+   * Tests Person->setProfileSong()
+   */
+  public function testSetProfileSong() {
+    $this->Person->setProfileSong('profilesong');
+    $this->assertEquals('profilesong', $this->Person->profileSong);
+  }
+
+  /**
+   * Tests Person->setProfileUrl()
+   */
+  public function testSetProfileUrl() {
+    $this->Person->setProfileUrl('profileurl');
+    $this->assertEquals('profileurl', $this->Person->profileUrl);
+  }
+
+  /**
+   * Tests Person->setProfileVideo()
+   */
+  public function testSetProfileVideo() {
+    $this->Person->setProfileVideo('profilevideo');
+    $this->assertEquals('profilevideo', $this->Person->profileVideo);
+  }
+
+  /**
+   * Tests Person->setQuotes()
+   */
+  public function testSetQuotes() {
+    $this->Person->setQuotes('quotes');
+    $this->assertEquals('quotes', $this->Person->quotes);
+  }
+
+  /**
+   * Tests Person->setRelationshipStatus()
+   */
+  public function testSetRelationshipStatus() {
+    $this->Person->setRelationshipStatus('relationshipstatus');
+    $this->assertEquals('relationshipstatus', $this->Person->relationshipStatus);
+  }
+
+  /**
+   * Tests Person->setReligion()
+   */
+  public function testSetReligion() {
+    $this->Person->setReligion('religion');
+    $this->assertEquals('religion', $this->Person->religion);
+  }
+
+  /**
+   * Tests Person->setRomance()
+   */
+  public function testSetRomance() {
+    $this->Person->setRomance('romance');
+    $this->assertEquals('romance', $this->Person->romance);
+  }
+
+  /**
+   * Tests Person->setScaredOf()
+   */
+  public function testSetScaredOf() {
+    $this->Person->setScaredOf('scaredof');
+    $this->assertEquals('scaredof', $this->Person->scaredOf);
+  }
+
+  /**
+   * Tests Person->setSexualOrientation()
+   */
+  public function testSetSexualOrientation() {
+    $this->Person->setSexualOrientation('sexualorientation');
+    $this->assertEquals('sexualorientation', $this->Person->sexualOrientation);
+  }
+
+  /**
+   * Tests Person->setSports()
+   */
+  public function testSetSports() {
+    $this->Person->setSports('sports');
+    $this->assertEquals('sports', $this->Person->sports);
+  }
+
+  /**
+   * Tests Person->setStatus()
+   */
+  public function testSetStatus() {
+    $this->Person->setStatus('status');
+    $this->assertEquals('status', $this->Person->status);
+  }
+
+  /**
+   * Tests Person->setTags()
+   */
+  public function testSetTags() {
+    $this->Person->setTags('tags');
+    $this->assertEquals('tags', $this->Person->tags);
+  }
+
+  /**
+   * Tests Person->setThumbnailUrl()
+   */
+  public function testSetThumbnailUrl() {
+    $this->Person->setThumbnailUrl('thumbnailurl');
+    $this->assertEquals('thumbnailurl', $this->Person->thumbnailUrl);
+  }
+
+  /**
+   * Tests Person->setTurnOffs()
+   */
+  public function testSetTurnOffs() {
+    $this->Person->setTurnOffs('turnoffs');
+    $this->assertEquals('turnoffs', $this->Person->turnOffs);
+  }
+
+  /**
+   * Tests Person->setTurnOns()
+   */
+  public function testSetTurnOns() {
+    $this->Person->setTurnOns('turnons');
+    $this->assertEquals('turnons', $this->Person->turnOns);
+  }
+
+  /**
+   * Tests Person->setTvShows()
+   */
+  public function testSetTvShows() {
+    $this->Person->setTvShows('tvshows');
+    $this->assertEquals('tvshows', $this->Person->tvShows);
+  }
+
+  /**
+   * Tests Person->setUrls()
+   */
+  public function testSetUrls() {
+    $this->Person->setUrls('urls');
+    $this->assertEquals('urls', $this->Person->urls);
+  }
+}
diff --git a/trunk/php/test/social/PhoneTest.php b/trunk/php/test/social/PhoneTest.php
new file mode 100644
index 0000000..04ad284
--- /dev/null
+++ b/trunk/php/test/social/PhoneTest.php
@@ -0,0 +1,79 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Phone;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Phone test case.
+ */
+class PhoneTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Phone
+   */
+  private $Phone;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Phone = new Phone('number', 'type');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Phone = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Phone->getNumber()
+   */
+  public function testGetNumber() {
+    $this->assertEquals('number', $this->Phone->getValue());
+  }
+
+  /**
+   * Tests Phone->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('type', $this->Phone->getType());
+  }
+
+  /**
+   * Tests Phone->setNumber()
+   */
+  public function testSetNumber() {
+    $this->Phone->setValue('NUMBER');
+    $this->assertEquals('NUMBER', $this->Phone->getValue());
+  }
+
+  /**
+   * Tests Phone->setType()
+   */
+  public function testSetType() {
+    $this->Phone->setType('TYPE');
+    $this->assertEquals('TYPE', $this->Phone->type);
+  }
+}
diff --git a/trunk/php/test/social/ResponseItemTest.php b/trunk/php/test/social/ResponseItemTest.php
new file mode 100644
index 0000000..806dd6b
--- /dev/null
+++ b/trunk/php/test/social/ResponseItemTest.php
@@ -0,0 +1,104 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\service\ResponseItem;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * ResponseItem test case.
+ */
+class ResponseItemTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var ResponseItem
+   */
+  private $ResponseItem;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->ResponseItem = new ResponseItem('error', 'errorMessage', array('foo' => null, 'bar' => 1));
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->ResponseItem = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests ResponseItem->__construct()
+   */
+  public function test__construct() {
+    $this->ResponseItem->__construct('error', 'errorMessage', array('foo' => null, 'bar' => 1));
+  }
+
+  /**
+   * Tests ResponseItem->getError()
+   */
+  public function testGetError() {
+    $this->assertEquals('error', $this->ResponseItem->getError());
+  }
+
+  /**
+   * Tests ResponseItem->getErrorMessage()
+   */
+  public function testGetErrorMessage() {
+    $this->assertEquals('errorMessage', $this->ResponseItem->getErrorMessage());
+  
+  }
+
+  /**
+   * Tests ResponseItem->getResponse()
+   */
+  public function testGetResponse() {
+    $expected = array('bar' => 1);
+    $this->assertEquals($expected, $this->ResponseItem->getResponse());
+  }
+
+  /**
+   * Tests ResponseItem->setError()
+   */
+  public function testSetError() {
+    $this->ResponseItem->setError('seterror');
+    $this->assertEquals('seterror', $this->ResponseItem->getError());
+  }
+
+  /**
+   * Tests ResponseItem->setErrorMessage()
+   */
+  public function testSetErrorMessage() {
+    $this->ResponseItem->setErrorMessage('seterrormessage');
+    $this->assertEquals('seterrormessage', $this->ResponseItem->getErrorMessage());
+  }
+
+  /**
+   * Tests ResponseItem->setResponse()
+   */
+  public function testSetResponse() {
+    $expected = array('bar' => 2);
+    $this->ResponseItem->setResponse($expected);
+    $this->assertEquals($expected, $this->ResponseItem->getResponse());
+  }
+}
diff --git a/trunk/php/test/social/RestBase.php b/trunk/php/test/social/RestBase.php
new file mode 100644
index 0000000..890d86c
--- /dev/null
+++ b/trunk/php/test/social/RestBase.php
@@ -0,0 +1,83 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\sample\JsonDbOpensocialService;
+use apache\shindig\social\servlet\DataServiceServlet;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\common\Config;
+
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Base class for the REST api integration tests.
+ */
+class RestBase extends \PHPUnit_Framework_TestCase {
+  
+  private $securityToken;
+  // The server to test against. You may need to add shindig to 127.0.0.1 mapping in /etc/hosts.
+  private $restUrl = '';
+
+  public function __construct() {
+    $db = new JsonDbOpensocialService();
+    $db->resetDb();
+    $this->securityToken = BasicSecurityToken::createFromValues(1, 1, 1, 'partuza', 'test.com', 1, 0)->toSerialForm();
+    $this->securityToken = urldecode($this->securityToken);
+    $this->restUrl = 'http://localhost' . Config::get('web_prefix') . '/social/rest';
+  }
+
+  protected function curlRest($url, $postData, $contentType = 'application/json', $method = 'POST') {
+    $_SERVER['CONTENT_TYPE'] = $contentType;
+    $sep = strpos($url, '?') !== false ? '&' : '?';
+    $_SERVER["REQUEST_URI"] = $this->restUrl . $url . $sep . 'st=' . $this->securityToken;
+    $parsedUrl = parse_url($_SERVER["REQUEST_URI"]);
+    $GLOBALS['HTTP_RAW_POST_DATA'] = $postData ? $postData : null;
+    $_SERVER['REQUEST_METHOD'] = $method;
+    $_SERVER['QUERY_STRING'] = $parsedUrl['query'];
+    $_SERVER['HTTP_HOST'] = $parsedUrl['host'];
+    $_GET = array('st' => $this->securityToken);
+    $servlet = new DataServiceServlet();
+    $servletMethod = 'do' . ucfirst(strtolower($method));
+    $servlet->noHeaders = true; // prevents "modify header information" errors
+    ob_start();
+    $servlet->$servletMethod();
+    $ret = ob_get_clean();
+    //var_dump($ret);
+    return $ret;
+  }
+
+  protected function getSecurityToken() {
+    return $this->securityToken;
+  }
+  
+  protected function setSecurityToken($token) {
+    $this->securityToken = $token;
+  }
+  
+  protected function getRestUrl() {
+    return $this->restUrl;
+  }
+  
+  protected function setRestUrl($url) {
+    $this->restUrl = $url; 
+  }
+}
+
+
+
diff --git a/trunk/php/test/social/RestFulCollectionTest.php b/trunk/php/test/social/RestFulCollectionTest.php
new file mode 100644
index 0000000..61afdfe
--- /dev/null
+++ b/trunk/php/test/social/RestFulCollectionTest.php
@@ -0,0 +1,99 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\spi\RestfulCollection;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * RestfulCollection test case.
+ */
+class RestfulCollectionTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @var RestfulCollection
+   */
+  private $RestfulCollection;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $entry = array('Entry');
+    $this->restfulCollection = new RestfulCollection($entry, 1, 1);
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->restfulCollection = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Constructs the test case.
+   */
+  public function __construct() {}
+
+  /**
+   * Tests RestfulCollection->__construct()
+   */
+  public function test__construct() {
+    $entry = array('Entry');
+    $this->restfulCollection->__construct($entry, 1, 1);
+  }
+
+  /**
+   * Tests RestfulCollection->getEntry()
+   */
+  public function testGetEntry() {
+    $entry = array('Entry');
+    $this->restfulCollection->setEntry($entry);
+    $this->assertEquals($entry, $this->restfulCollection->getEntry());
+  }
+
+  /**
+   * Tests RestfulCollection->getStartIndex()
+   */
+  public function testGetStartIndex() {
+    $this->restfulCollection->setStartIndex(1);
+    $this->assertEquals(1, $this->restfulCollection->getStartIndex());
+  }
+
+  /**
+   * Tests RestfulCollection->getTotalResults()
+   */
+  public function testGetTotalResults() {
+    $this->restfulCollection->setTotalResults(1);
+    $this->assertEquals(1, $this->restfulCollection->getTotalResults());
+  }
+
+  /**
+   * Tests RestfulCollection->createFromEntry()
+   */
+  public function testCreateFromEntry() {
+    $entry = array('Entry');
+    $restfulCollection = RestfulCollection::createFromEntry($entry);
+    $this->assertEquals(1, $restfulCollection->getTotalResults());
+    $this->assertEquals($entry, $restfulCollection->getEntry());
+    $this->assertEquals(0, $restfulCollection->getStartIndex());
+  }
+}
diff --git a/trunk/php/test/social/RestRequestItemTest.php b/trunk/php/test/social/RestRequestItemTest.php
new file mode 100644
index 0000000..c7ec54f
--- /dev/null
+++ b/trunk/php/test/social/RestRequestItemTest.php
@@ -0,0 +1,69 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\converters\OutputJsonConverter;
+use apache\shindig\common\sample\BasicSecurityToken;
+use apache\shindig\social\service\RestRequestItem;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * RestRequestItem test case.
+ */
+class RestRequestItemTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    $_SERVER['REQUEST_METHOD'] = 'GET';
+    parent::setUp();
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    parent::tearDown();
+  }
+
+  /**
+   * Tests RestRequestItem->createWithRequest()
+   */
+  public function testCreateWithRequest() {
+    $expectedParams = array('oauth_nonce' => '10075052d8a3cd0087d11346edba8f1f',
+                            'oauth_timestamp' => '1242011332',
+                            'oauth_consumer_key' => 'consumerKey',
+                            'fields' => 'gender,name',
+                            'oauth_signature_method' => 'HMAC-SHA1',
+                            'oauth_signature' => 'wDcyXTBqhxW70G+ddZtw7zPVGyE=');
+    $urlencodedParams = array();
+    foreach ($expectedParams as $key => $value) {
+      $urlencodedParams[] = $key . '=' . urlencode($value);
+    }
+    $url = '/people/1/@self?' . join('&', $urlencodedParams);
+    $outputConverter = new OutputJsonConverter();
+    $servletRequest = array('url' => $url);
+    $token = BasicSecurityToken::createFromValues('owner', 'viewer', 'app', 'domain', 'appUrl', '1', 'default');
+    $requestItem = RestRequestItem::createWithRequest($servletRequest, $token, 'convertJson', $outputConverter);
+    $params = $requestItem->getParameters();
+    $this->assertEquals($expectedParams, $params);
+  }
+
+}
diff --git a/trunk/php/test/social/UrlTest.php b/trunk/php/test/social/UrlTest.php
new file mode 100644
index 0000000..a155267
--- /dev/null
+++ b/trunk/php/test/social/UrlTest.php
@@ -0,0 +1,94 @@
+<?php
+namespace apache\shindig\test\social;
+use apache\shindig\social\model\Url;
+
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+/**
+ * Url test case.
+ */
+class UrlTest extends \PHPUnit_Framework_TestCase {
+  
+  /**
+   * @var Url
+   */
+  private $Url;
+
+  /**
+   * Prepares the environment before running a test.
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->Url = new Url('A', 'T', 'L');
+  }
+
+  /**
+   * Cleans up the environment after running a test.
+   */
+  protected function tearDown() {
+    $this->Url = null;
+    parent::tearDown();
+  }
+
+  /**
+   * Tests Url->getAddress()
+   */
+  public function testGetAddress() {
+    $this->assertEquals('A', $this->Url->getValue());
+  }
+
+  /**
+   * Tests Url->getLinkText()
+   */
+  public function testGetLinkText() {
+    $this->assertEquals('L', $this->Url->getLinkText());
+  }
+
+  /**
+   * Tests Url->getType()
+   */
+  public function testGetType() {
+    $this->assertEquals('T', $this->Url->getType());
+  }
+
+  /**
+   * Tests Url->setAddress()
+   */
+  public function testSetAddress() {
+    $this->Url->setValue('a');
+    $this->assertEquals('a', $this->Url->getValue());
+  }
+
+  /**
+   * Tests Url->setLinkText()
+   */
+  public function testSetLinkText() {
+    $this->Url->setLinkText('l');
+    $this->assertEquals('l', $this->Url->getLinkText());
+  }
+
+  /**
+   * Tests Url->setType()
+   */
+  public function testSetType() {
+    $this->Url->setType('t');
+    $this->assertEquals('t', $this->Url->type);
+  }
+}
\ No newline at end of file
diff --git a/trunk/pom.xml b/trunk/pom.xml
new file mode 100644
index 0000000..17632a4
--- /dev/null
+++ b/trunk/pom.xml
@@ -0,0 +1,1788 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache</groupId>
+    <artifactId>apache</artifactId>
+    <version>12</version>
+  </parent>
+
+  <groupId>org.apache.shindig</groupId>
+  <artifactId>shindig-project</artifactId>
+  <version>2.5.1-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Apache Shindig Project</name>
+  <description>
+    Shindig is a JavaScript container and implementations of the
+    backend APIs and proxy required for hosting OpenSocial
+    applications.
+  </description>
+  <url>http://shindig.apache.org</url>
+  <inceptionYear>2007</inceptionYear>
+
+  <properties>
+    <!-- What version of the API are we targeting for compatible -->
+    <shindig.api.compatible>2.5.0</shindig.api.compatible>
+    <!-- What version of the API did we ship last -->
+    <shindig.api.previous>2.0.0</shindig.api.previous>
+
+    <!-- properties that help us sort through the java 1.5/1.6 mess -->
+    <shindig.jdk.version>1.6</shindig.jdk.version>
+    <shindig.jdk.javadoc>1.6.0</shindig.jdk.javadoc>
+    <shindig.jdk.classifier />
+  </properties>
+
+  <prerequisites>
+    <maven>2.0.8</maven>
+  </prerequisites>
+
+  <!-- ====================================================================== -->
+  <!-- S C M                                                                  -->
+  <!-- ====================================================================== -->
+  <scm>
+    <connection>scm:svn:http://svn.apache.org/repos/asf/shindig/trunk</connection>
+    <developerConnection>scm:svn:https://svn.apache.org/repos/asf/shindig/trunk</developerConnection>
+    <url>http://svn.apache.org/viewvc/shindig/trunk</url>
+  </scm>
+
+  <!-- ====================================================================== -->
+  <!-- I S S U E  M A N A G E M E N T                                         -->
+  <!-- ====================================================================== -->
+  <issueManagement>
+    <system>jira</system>
+    <url>http://issues.apache.org/jira/browse/SHINDIG</url>
+  </issueManagement>
+
+  <!-- ====================================================================== -->
+  <!-- C I  M A N A G E M E N T                                               -->
+  <!-- ====================================================================== -->
+  <ciManagement>
+    <system>Jenkins</system>
+    <url>https://builds.apache.org/view/S-Z/view/Shindig/</url>
+  </ciManagement>
+
+  <!-- ====================================================================== -->
+  <!-- D I S T R I B U T I O N  M A N A G E M E N T                           -->
+  <!-- ====================================================================== -->
+  <distributionManagement>
+    <!-- This is the technical website for Shindig 2.0.x -->
+    <site>
+      <id>apache.website</id>
+      <url>scp://people.apache.org/www/shindig.apache.org/shindig-3.0.x</url>
+    </site>
+  </distributionManagement>
+
+  <!-- ====================================================================== -->
+  <!-- M A I L I N G   L I S T S                                              -->
+  <!-- ====================================================================== -->
+  <mailingLists>
+    <mailingList>
+      <name>Shindig Dev List</name>
+      <subscribe>dev-subscribe@shindig.apache.org</subscribe>
+      <unsubscribe>dev-unsubscribe@shindig.apache.org</unsubscribe>
+      <post>dev@shindig.apache.org</post>
+      <archive>http://mail-archives.apache.org/mod_mbox/shindig-dev/</archive>
+      <otherArchives>
+        <otherArchive>http://shindig-dev.markmail.org/</otherArchive>
+      </otherArchives>
+    </mailingList>
+    <mailingList>
+      <name>Shindig Users List</name>
+      <subscribe>users-subscribe@shindig.apache.org</subscribe>
+      <unsubscribe>users-unsubscribe@shindig.apache.org</unsubscribe>
+      <archive>http://mail-archives.apache.org/mod_mbox/shindig-users/</archive>
+    </mailingList>
+    <mailingList>
+      <name>Shindig Commit List</name>
+      <subscribe>commits-subscribe@shindig.apache.org</subscribe>
+      <unsubscribe>commits-unsubscribe@shindig.apache.org</unsubscribe>
+      <archive>http://mail-archives.apache.org/mod_mbox/shindig-commits/</archive>
+    </mailingList>
+    <mailingList>
+      <name>Shindig Issues List</name>
+      <subscribe>issues-subscribe@shindig.apache.org</subscribe>
+      <unsubscribe>issues-unsubscribe@shindig.apache.org</unsubscribe>
+      <archive>http://mail-archives.apache.org/mod_mbox/shindig-issues/</archive>
+    </mailingList>
+  </mailingLists>
+
+  <!-- ====================================================================== -->
+  <!-- P E O P L E                                                            -->
+  <!-- ====================================================================== -->
+  <!-- Developers listed by PMC Chair, PMC, Mentors, Committers, Contributers, all alphabetical-->
+  <developers>
+    <!-- PMC Chair -->
+    <developer>
+      <id>lindner</id>
+      <name>Paul Lindner</name>
+      <email>lindner@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Chair</role>
+      </roles>
+    </developer>
+    <!-- PMC -->
+    <developer>
+      <id>agektmr</id>
+      <name>Eiji Kitamura</name>
+      <email>agektmr@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>bhofmann</id>
+      <name>Bastian Hofmann</name>
+      <email>bhofmann@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>brianm</id>
+      <name>Brian McCallister</name>
+      <email>brianm@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+        <role>Mentor</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>evan</id>
+      <name>Evan Gilbert</name>
+      <email>evan@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>jasvir</id>
+      <name>Jasvir Nagra</name>
+      <email>jasvir@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>johnh</id>
+      <name>John Hjelmstad</name>
+      <email>johnh@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>jyang</id>
+      <name>Jun Yang</name>
+      <email>jyang@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>zhen</id>
+      <name>Zhen Wang</name>
+      <email>zhen@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>chico</id>
+      <name>Chico Charlesworth</name>
+      <email>chico@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>chirag</id>
+      <name>Chirag Shah</name>
+      <email>chirag@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>hsaputra</id>
+      <name>Henry Saputra</name>
+      <email>hsaputra@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>chaowang</id>
+      <name>Jacky Wang</name>
+      <email>chaowang@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>zhoresh</id>
+      <name>Ziv Horesh</name>
+      <email>zhoresh@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>gagan</id>
+      <name>Gagandeep Singh</name>
+      <email>gagan@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>hnguy</id>
+      <name>Han Nguyen</name>
+      <email>hnguy@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>felix8a</id>
+      <name>Felix Lee</name>
+      <email>felix8a@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>rbaxter85</id>
+      <name>Ryan Baxter</name>
+      <email>rbaxter85@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>woodser</id>
+      <name>Eric Woods</name>
+      <email>woodser@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>lixu</id>
+      <name>Li Xu</name>
+      <email>lixu@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>ddumont</id>
+      <name>Dan Dumont</name>
+      <email>ddumont@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>jcian</id>
+      <name>Jesse Ciancetta</name>
+      <email>jcian@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>PMC Member</role>
+      </roles>
+    </developer>
+
+    <!-- Mentors -->
+    <developer>
+      <id>fitz</id>
+      <name>Brian Fitzpatrick</name>
+      <email>fitz@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Mentor</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>sylvain</id>
+      <name>Sylvain Wallez</name>
+      <email>sylvain@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Mentor</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>tomdz</id>
+      <name>Thomas Dudziak</name>
+      <email>tomdz@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Mentor</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>upayavira</id>
+      <name>Upayavira</name>
+      <email>upayavira@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Mentor</role>
+      </roles>
+    </developer>
+    <!--Committers-->
+
+    <!--Emeritus-->
+    <developer>
+      <id>gstein</id>
+      <name>Greg Stein</name>
+      <email>gstein@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus Mentor</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>sgala</id>
+      <name>Santiago Gala</name>
+      <email>sgala@hisitech.com</email>
+      <organization>ASF</organization>
+      <roles>
+       <role>Emeritus Mentor</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>ieb</id>
+      <name>Ian Boston</name>
+      <email>ieb@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus Mentor</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>awiner</id>
+      <name>Adam Winer</name>
+      <email>awiner@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>henning</id>
+      <name>Henning Schmiedehausen</name>
+      <email>henning@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>chabotc</id>
+      <name>Chris Chabot</name>
+      <email>chabotc@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>dbentley</id>
+      <name>Daniel Bentley</name>
+      <email>dbentley@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>doll</id>
+      <name>Cassie Doll</name>
+      <email>doll@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>dpeterson</id>
+      <name>Dan Peterson</name>
+      <email>dpeterson@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>etnu</id>
+      <name>Kevin Brown</name>
+      <email>etnu@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>lryan</id>
+      <name>Louis Ryan</name>
+      <email>lryan@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>martint</id>
+      <name>Martin Traverso</name>
+      <email>martint@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>vsiveton</id>
+      <name>Vincent Siveton</name>
+      <email>vsiveton@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+    <developer>
+      <id>dharkness</id>
+      <name>David Harkness</name>
+      <email>dharkness@apache.org</email>
+      <organization>ASF</organization>
+      <roles>
+        <role>Emeritus</role>
+      </roles>
+    </developer>
+  </developers>
+
+  <modules>
+    <module>features</module>
+    <module>java/common</module>
+    <module>java/gadgets</module>
+    <module>java/social-api</module>
+    <module>java/sample-container</module>
+    <module>java/sample-maven-archetype</module>
+    <module>java/server-resources</module>
+    <module>java/server-dependencies</module>
+    <module>java/server</module>
+    <module>extras</module>
+  </modules>
+
+
+  <!-- ====================================================================== -->
+  <!-- P R O F I L E S                                                        -->
+  <!-- ====================================================================== -->
+  <profiles>
+
+    <!-- stub for all -->
+    <profile>
+      <id>all</id>
+    </profile>
+
+    <!-- profile to run an embedded jetty instance -->
+    <profile>
+      <id>run</id>
+      <dependencies>
+      </dependencies>
+
+      <build>
+        <defaultGoal>jetty:run-war</defaultGoal>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-antrun-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>create-work</id>
+                <phase>compile</phase>
+                <configuration>
+                  <tasks>
+                    <mkdir dir="target" />
+                  </tasks>
+                </configuration>
+                <goals>
+                  <goal>run</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.mortbay.jetty</groupId>
+            <artifactId>maven-jetty-plugin</artifactId>
+            <configuration>
+              <tempDirectory>${basedir}/java/server/target/work</tempDirectory>
+              <webApp>${basedir}/java/server/target/shindig-server-${project.version}.war</webApp>
+              <contextPath>/</contextPath>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <!-- generate reports -->
+    <profile>
+      <id>reporting</id>
+      <modules>
+        <module>features</module>
+        <module>java/common</module>
+        <module>java/gadgets</module>
+        <module>java/social-api</module>
+        <module>java/sample-container</module>
+        <module>java/server-resources</module>
+        <module>java/server-dependencies</module>
+        <module>java/server</module>
+        <module>extras</module>
+      </modules>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>cobertura-maven-plugin</artifactId>
+            <configuration>
+              <check>
+                <haltOnFailure>false</haltOnFailure>
+                <regexes>
+                  <regex>
+                    <pattern>org.apache.shindig.*</pattern>
+                    <branchRate>90</branchRate>
+                    <lineRate>90</lineRate>
+                  </regex>
+                </regexes>
+              </check>
+              <instrumentation>
+                <includes>
+                  <include>org/apache/shindig/**/*.class</include>
+                </includes>
+              </instrumentation>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-site-plugin</artifactId>
+            <configuration>
+              <stagingSiteURL>scp://people.apache.org/www/shindig.apache.org/shindig-2.0.x/${project.version}</stagingSiteURL>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.rat</groupId>
+            <artifactId>apache-rat-plugin</artifactId>
+            <executions>
+              <execution>
+                <phase>verify</phase>
+                <goals>
+                  <goal>check</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-checkstyle-plugin</artifactId>
+            <version>2.8</version>
+            <configuration>
+              <configLocation>etc/checkstyle/checkstyle.xml</configLocation>
+              <headerLocation>etc/checkstyle/java.header</headerLocation>
+              <encoding>${project.build.sourceEncoding}</encoding>
+            </configuration>
+          </plugin>
+        </plugins>
+      </build>
+
+      <reporting>
+        <!-- ordered alphabetically by owner -->
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-checkstyle-plugin</artifactId>
+            <version>2.8</version>
+            <configuration>
+              <configLocation>etc/checkstyle/checkstyle.xml</configLocation>
+              <headerLocation>etc/checkstyle/java.header</headerLocation>
+              <encoding>${project.build.sourceEncoding}</encoding>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <version>2.7</version>
+            <configuration>
+              <source>${shindig.jdk.version}</source>
+              <target>${shindig.jdk.version}</target>
+              <encoding>${project.build.sourceEncoding}</encoding>
+              <links>
+                <link>http://java.sun.com/j2se/${shindig.jdk.javadoc}/docs/api</link>
+                <link>http://java.sun.com/products/servlet/2.5/docs/servlet-2_5-mr2/</link>
+                <link>http://www.json.org/javadoc/</link>
+                <link>http://junit.sourceforge.net/javadoc/</link>
+              </links>
+              <detectLinks>true</detectLinks>
+            </configuration>
+            <reportSets>
+              <reportSet>
+                <id>non-aggregate</id>
+                <reports>
+                  <report>javadoc</report>
+                  <report>test-javadoc</report>
+                </reports>
+              </reportSet>
+              <reportSet>
+                <id>aggregate</id>
+                <reports>
+                  <report>aggregate</report>
+                  <report>test-aggregate</report>
+                </reports>
+              </reportSet>
+            </reportSets>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-jxr-plugin</artifactId>
+            <version>2.2</version>
+            <configuration>
+              <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
+              <outputEncoding>${project.build.sourceEncoding}</outputEncoding>
+            </configuration>
+            <reportSets>
+              <reportSet>
+                <id>non-aggregate</id>
+                <configuration>
+                  <aggregate>false</aggregate>
+                </configuration>
+                <reports>
+                  <report>jxr</report>
+                  <report>test-jxr</report>
+                </reports>
+              </reportSet>
+              <reportSet>
+                <id>aggregate</id>
+                <configuration>
+                  <aggregate>true</aggregate>
+                </configuration>
+                <reports>
+                  <report>jxr</report>
+                  <report>test-jxr</report>
+                </reports>
+              </reportSet>
+            </reportSets>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-pmd-plugin</artifactId>
+            <version>2.5</version>
+            <configuration>
+              <targetJdk>${shindig.jdk.version}</targetJdk>
+              <sourceEncoding>${project.build.sourceEncoding}</sourceEncoding>
+            </configuration>
+            <reportSets>
+              <reportSet>
+                <id>non-aggregate</id>
+                <configuration>
+                  <aggregate>false</aggregate>
+                </configuration>
+                <reports>
+                  <report>cpd</report>
+                  <report>pmd</report>
+                </reports>
+              </reportSet>
+              <reportSet>
+                <id>aggregate</id>
+                <configuration>
+                  <aggregate>true</aggregate>
+                </configuration>
+                <reports>
+                  <report>cpd</report>
+                  <report>pmd</report>
+                </reports>
+              </reportSet>
+            </reportSets>
+          </plugin>
+
+          <plugin>
+            <groupId>org.apache.rat</groupId>
+            <artifactId>apache-rat-plugin</artifactId>
+            <version>0.8</version>
+            <configuration>
+              <excludeSubProjects>false</excludeSubProjects>
+              <excludes>
+                <exclude>**/*.iml</exclude>
+                <exclude>.gitignore</exclude>
+                <exclude>.reviewboardrc</exclude>
+                <exclude>release.properties</exclude>
+                <exclude>**/.git/**/*</exclude>
+                <exclude>**/README*</exclude>
+                <exclude>**/target/**</exclude>
+                <exclude>**/external/**</exclude>
+                <exclude>**/features-extras/swfobject/swfobject.js</exclude>
+                <exclude>**/features-extras/swfobject/swfobject.opt.js</exclude>
+                <exclude>etc/svn-ignores</exclude>
+                <exclude>etc/svn-props</exclude>
+                <exclude>etc/eclipse/shindig.importorder</exclude>
+                <exclude>JsMin.php</exclude>
+                <exclude>**/content/**/*.json</exclude>
+                <exclude>**/content/**/*.swf</exclude>
+                <exclude>**/test/misc/rewriter*.css</exclude>
+                <exclude>**/test/misc/rewriter*.js</exclude>
+                <exclude>**/test/misc/*.html</exclude>
+                <exclude>**/test/misc/*.xml</exclude>
+                <exclude>**/test/certs/*</exclude>
+                <exclude>**/site/src/site/resources/**</exclude>
+                <exclude>**/site/src/site/site.vm</exclude>
+                <exclude>**/site/generated_site/**</exclude>
+                <exclude>**/site/cms/trunk/content/bootstrap/**</exclude>
+                <exclude>**/site/cms/trunk/content/jquery/**</exclude>
+                <exclude>phpunit.xml.dist</exclude>
+                <exclude>BUILD-JAVA</exclude>
+                <exclude>COMMITTERS</exclude>
+                <exclude>UPGRADING</exclude>
+                <exclude>**/MANIFEST.MF</exclude>
+                <exclude>**/src/test/**/*.html</exclude>
+                <exclude>**/src/test/**/*.css</exclude>
+                <exclude>**/src/test/**/*.json</exclude>
+                <exclude>**/src/test/**/*.xml</exclude>
+                <exclude>**/create.sql</exclude>
+                <exclude>**/drop.sql</exclude>
+                <exclude>**/derby.log</exclude>
+                <exclude>**/dependency-reduced-pom.xml</exclude>
+              </excludes>
+            </configuration>
+          </plugin>
+
+          <!-- Mojo -->
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>clirr-maven-plugin</artifactId>
+            <version>2.2.3</version>
+            <configuration>
+              <comparisonVersion>${shindig.api.previous}</comparisonVersion>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>cobertura-maven-plugin</artifactId>
+            <version>2.4</version>
+            <configuration>
+              <formats>
+                <format>html</format>
+                <format>xml</format>
+              </formats>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>findbugs-maven-plugin</artifactId>
+            <version>2.3.1</version>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>jdepend-maven-plugin</artifactId>
+            <version>2.0-beta-2</version>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-surefire-report-plugin</artifactId>
+            <version>2.8</version>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>taglist-maven-plugin</artifactId>
+            <version>2.4</version>
+            <configuration>
+              <tagListOptions>
+                <tagClasses>
+                  <tagClass>
+                    <displayName>Todo Work</displayName>
+                    <tags>
+                      <tag>
+                        <matchString>todo</matchString>
+                        <matchType>ignoreCase</matchType>
+                      </tag>
+                      <tag>
+                        <matchString>FIXME</matchString>
+                        <matchType>exact</matchType>
+                      </tag>
+                    </tags>
+                  </tagClass>
+                  <tagClass>
+                    <displayName>Deprecated</displayName>
+                    <tags>
+                      <tag>
+                        <matchString>@deprecated</matchString>
+                        <matchType>exact</matchType>
+                      </tag>
+                    </tags>
+                  </tagClass>
+                </tagClasses>
+              </tagListOptions>
+              <encoding>${project.build.sourceEncoding}</encoding>
+            </configuration>
+            <reportSets>
+              <reportSet>
+                <id>non-aggregate</id>
+                <configuration>
+                  <aggregate>false</aggregate>
+                </configuration>
+                <reports>
+                  <report>taglist</report>
+                </reports>
+              </reportSet>
+              <reportSet>
+                <id>aggregate</id>
+                <configuration>
+                  <aggregate>true</aggregate>
+                </configuration>
+                <reports>
+                  <report>taglist</report>
+                </reports>
+              </reportSet>
+            </reportSets>
+          </plugin>
+        </plugins>
+      </reporting>
+    </profile>
+
+    <!-- Apache Release Profile -->
+    <profile>
+      <id>apache-release</id>
+      <modules>
+        <module>features</module>
+        <module>java/common</module>
+        <module>java/gadgets</module>
+        <module>java/social-api</module>
+        <module>java/sample-container</module>
+        <module>java/server-resources</module>
+        <module>java/server-dependencies</module>
+        <module>java/server</module>
+        <module>java/uber</module>
+        <module>extras</module>
+        <module>php</module>
+        <module>assembly</module>
+      </modules>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-source-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>attach-sources</id>
+                <goals>
+                  <goal>jar</goal>
+                  <goal>test-jar</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>attach-javadocs</id>
+                <goals>
+                  <goal>jar</goal>
+                  <goal>test-jar</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.rat</groupId>
+            <artifactId>apache-rat-plugin</artifactId>
+            <executions>
+              <execution>
+                <phase>verify</phase>
+                <goals>
+                  <goal>check</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+
+    <profile>
+      <id>php</id>
+      <modules>
+        <module>features</module>
+        <module>php</module>
+      </modules>
+    </profile>
+
+    <!-- assemble the source releases -->
+    <profile>
+      <id>assemble</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-source-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>attach-sources</id>
+                <goals>
+                  <goal>jar</goal>
+                  <goal>test-jar</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-javadoc-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>attach-javadocs</id>
+                <goals>
+                  <goal>jar</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+      <modules>
+        <module>features</module>
+        <module>java/common</module>
+        <module>java/gadgets</module>
+        <module>java/social-api</module>
+        <module>java/sample-container</module>
+        <module>java/server-resources</module>
+        <module>java/server-dependencies</module>
+        <module>java/server</module>
+        <module>extras</module>
+        <module>assembly</module>
+      </modules>
+    </profile>
+
+    <profile>
+      <id>to-committers</id>
+      <build>
+        <defaultGoal>xslt:transform</defaultGoal>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.mojo</groupId>
+            <artifactId>xslt-maven-plugin</artifactId>
+            <configuration>
+              <xslFile>${basedir}/etc/to-committers.xsl</xslFile>
+              <srcIncludes>pom.xml</srcIncludes>
+              <srcDir>${basedir}</srcDir>
+              <destDir>${project.build.directory}</destDir>
+              <fileNameRegex>pom.xml</fileNameRegex>
+              <fileNameReplacement>COMMITTERS</fileNameReplacement>
+            </configuration>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>transform</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+  <!-- ====================================================================== -->
+  <!-- B U I L D                                                              -->
+  <!-- ====================================================================== -->
+
+  <build>
+    <defaultGoal>install</defaultGoal>
+
+    <pluginManagement>
+      <!-- set versions/conf of common plugins for reproducibility, ordered alphabetically by owner -->
+      <plugins>
+        <!-- Maven -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-antrun-plugin</artifactId>
+          <version>1.6</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>2.3.2</version>
+          <configuration>
+            <source>${shindig.jdk.version}</source>
+            <target>${shindig.jdk.version}</target>
+            <showDeprecation>true</showDeprecation>
+            <compilerArgument>-Xlint:unchecked,deprecation,fallthrough,finally</compilerArgument>
+            <fork>true</fork>
+            <encoding>${project.build.sourceEncoding}</encoding>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-dependency-plugin</artifactId>
+          <version>2.1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins </groupId>
+          <artifactId>maven-eclipse-plugin</artifactId>
+          <!-- stay at 2.6, 2.8 has problems with our poms -->
+          <version>2.6</version>
+          <configuration>
+            <addVersionToProjectName>true</addVersionToProjectName>
+            <workspaceCodeStylesURL>http://svn.apache.org/repos/asf/shindig/trunk/etc/eclipse/shindig-eclipse-codestyle_2.xml</workspaceCodeStylesURL>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-enforcer-plugin</artifactId>
+          <version>1.0</version>
+            <configuration>
+              <rules>
+                <requireJavaVersion>
+                  <version>[1.5,)</version>
+                </requireJavaVersion>
+              </rules>
+            </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-gpg-plugin</artifactId>
+          <version>1.4</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-install-plugin</artifactId>
+          <version>2.3.1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <version>2.7</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <version>2.3.1</version>
+          <executions>
+           <execution>
+             <goals>
+               <goal>test-jar</goal>
+             </goals>
+           </execution>
+          </executions>
+          <configuration>
+            <archive>
+              <manifest>
+                <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
+                <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+              </manifest>
+            </archive>
+            <classifier>${shindig.jdk.classifier}</classifier>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>2.7</version>
+          <configuration>
+            <source>${shindig.jdk.version}</source>
+            <target>${shindig.jdk.version}</target>
+            <encoding>${project.build.sourceEncoding}</encoding>
+            <links>
+              <link>http://java.sun.com/j2se/${shindig.jdk.javadoc}/docs/api</link>
+              <link>http://java.sun.com/products/servlet/2.5/docs/servlet-2_5-mr2/</link>
+              <link>http://www.json.org/javadoc/</link>
+              <link>http://junit.sourceforge.net/javadoc/</link>
+            </links>
+            <fixTags>since,param,return,throws</fixTags>
+            <fixMethodComment>true</fixMethodComment>
+            <fixClassComment>true</fixClassComment>
+            <fixFieldComment>false</fixFieldComment>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jxr-plugin</artifactId>
+          <version>2.2</version>
+          <configuration>
+            <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-release-plugin</artifactId>
+          <version>2.2.1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-remote-resources-plugin</artifactId>
+          <version>1.2</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-resources-plugin</artifactId>
+          <version>2.5</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-site-plugin</artifactId>
+          <version>3.0</version>
+          <configuration>
+            <inputEncoding>${project.build.sourceEncoding}</inputEncoding>
+            <outputEncoding>${project.build.sourceEncoding}</outputEncoding>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>2.1.2</version>
+          <executions>
+            <execution>
+              <id>attach-sources</id>
+              <goals>
+                <goal>jar</goal>
+                <goal>test-jar</goal>
+              </goals>
+            </execution>
+          </executions>
+          <configuration>
+            <excludeResources>false</excludeResources>
+            <attach>true</attach>
+            <classifier>${shindig.jdk.classifier}</classifier>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>2.7.2</version>
+          <configuration>
+            <parallel>both</parallel>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-war-plugin</artifactId>
+          <version>2.1.1</version>
+        </plugin>
+
+        <!-- Mojo -->
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>clirr-maven-plugin</artifactId>
+          <version>2.2.3</version>
+        </plugin>
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>cobertura-maven-plugin</artifactId>
+          <version>2.4</version>
+          <configuration>
+	        <formats>
+	          <format>html</format>
+	          <format>xml</format>
+	        </formats>
+          </configuration>
+        </plugin>
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>findbugs-maven-plugin</artifactId>
+          <version>2.3.1</version>
+        </plugin>
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>jdepend-maven-plugin</artifactId>
+          <version>2.0-beta-2</version>
+        </plugin>
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>taglist-maven-plugin</artifactId>
+          <version>2.4</version>
+        </plugin>
+        <plugin>
+          <groupId>org.codehaus.mojo</groupId>
+          <artifactId>xslt-maven-plugin</artifactId>
+          <version>1.0</version>
+        </plugin>
+
+        <!-- Misc -->
+        <plugin>
+          <groupId>net.alchim31.maven</groupId>
+          <artifactId>yuicompressor-maven-plugin</artifactId>
+          <version>1.1</version>
+        </plugin>
+
+        <plugin>
+          <groupId>org.mortbay.jetty</groupId>
+          <artifactId>maven-jetty-plugin</artifactId>
+          <version>6.1.26</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.geronimo.genesis.plugins</groupId>
+          <artifactId>tools-maven-plugin</artifactId>
+          <version>1.4</version>
+          <executions>
+            <execution>
+              <id>verify-legal-files</id>
+              <phase>verify</phase>
+              <goals>
+                <goal>verify-legal-files</goal>
+              </goals>
+              <configuration>
+                <strict>true</strict>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.rat</groupId>
+          <artifactId>apache-rat-plugin</artifactId>
+          <version>0.8</version>
+          <configuration>
+            <excludeSubProjects>false</excludeSubProjects>
+            <excludes>
+              <exclude>**/*.iml</exclude>
+              <exclude>.gitignore</exclude>
+              <exclude>.reviewboardrc</exclude>
+              <exclude>release.properties</exclude>
+              <exclude>**/.git/**/*</exclude>
+              <exclude>**/README*</exclude>
+              <exclude>**/target/**</exclude>
+              <exclude>**/external/**</exclude>
+              <exclude>**/features-extras/swfobject/swfobject.js</exclude>
+              <exclude>**/features-extras/swfobject/swfobject.opt.js</exclude>
+              <exclude>etc/svn-ignores</exclude>
+              <exclude>etc/svn-props</exclude>
+              <exclude>etc/eclipse/shindig.importorder</exclude>
+              <exclude>JsMin.php</exclude>
+              <exclude>**/content/**/*.json</exclude>
+              <exclude>**/content/**/*.swf</exclude>
+              <exclude>**/test/misc/rewriter*.css</exclude>
+              <exclude>**/test/misc/rewriter*.js</exclude>
+              <exclude>**/test/misc/*.html</exclude>
+              <exclude>**/test/misc/*.xml</exclude>
+              <exclude>**/test/certs/*</exclude>
+              <exclude>**/site/src/site/resources/**</exclude>
+              <exclude>**/site/src/site/site.vm</exclude>
+              <exclude>**/site/generated_site/**</exclude>
+              <exclude>**/site/cms/trunk/content/bootstrap/**</exclude>
+              <exclude>**/site/cms/trunk/content/jquery/**</exclude>
+              <exclude>phpunit.xml.dist</exclude>
+              <exclude>BUILD-JAVA</exclude>
+              <exclude>COMMITTERS</exclude>
+              <exclude>UPGRADING</exclude>
+              <exclude>**/MANIFEST.MF</exclude>
+              <exclude>**/src/test/**/*.html</exclude>
+              <exclude>**/src/test/**/*.css</exclude>
+              <exclude>**/src/test/**/*.json</exclude>
+              <exclude>**/src/test/**/*.xml</exclude>
+              <exclude>**/create.sql</exclude>
+              <exclude>**/drop.sql</exclude>
+              <exclude>**/derby.log</exclude>
+              <exclude>**/dependency-reduced-pom.xml</exclude>
+              <exclude>**/*.classpath</exclude>
+              <exclude>**/.project</exclude>
+              <exclude>**/.settings/*</exclude>
+              <exclude>**/src/test/resources/**</exclude>
+            </excludes>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+
+    <!-- ordered alphabetically by owner -->
+    <plugins>
+      <!-- Maven -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-eclipse-plugin</artifactId>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>enforce-java</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <!-- We want to package up license resources in the JARs produced -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-remote-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>process</goal>
+            </goals>
+            <configuration>
+              <resourceBundles>
+                <resourceBundle>org.apache:apache-jar-resource-bundle:1.4</resourceBundle>
+              </resourceBundles>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-resources-plugin</artifactId>
+        <configuration>
+          <encoding>${project.build.sourceEncoding}</encoding>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <excludeResources>false</excludeResources>
+          <attach>true</attach>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-site-plugin</artifactId>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.rat</groupId>
+        <artifactId>apache-rat-plugin</artifactId>
+      </plugin>
+
+      <!-- Misc -->
+      <plugin>
+        <groupId>org.apache.geronimo.genesis.plugins</groupId>
+        <artifactId>tools-maven-plugin</artifactId>
+      </plugin>
+    </plugins>
+  </build>
+
+  <!-- ====================================================================== -->
+  <!-- R E P O R T I N G                                                      -->
+  <!-- ====================================================================== -->
+  <reporting>
+    <!-- ordered alphabetically by owner -->
+    <plugins>
+      <!-- Maven -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-project-info-reports-plugin</artifactId>
+        <version>2.4</version>
+      </plugin>
+    </plugins>
+  </reporting>
+
+  <!-- ====================================================================== -->
+  <!-- R E P O S I T O R I E S                                                -->
+  <!-- ====================================================================== -->
+  <repositories>
+    <repository>
+      <id>central</id>
+      <name>Maven Repository Switchboard</name>
+      <layout>default</layout>
+      <url>http://repo1.maven.org/maven2</url>
+      <snapshots>
+        <enabled>false</enabled>
+      </snapshots>
+    </repository>
+    <repository>
+      <id>diff_match_patch</id>
+      <url>http://google-diff-match-patch.googlecode.com/svn/trunk/maven</url>
+    </repository>
+    <repository>
+      <id>caja</id>
+      <url>http://google-caja.googlecode.com/svn/maven</url>
+    </repository>
+    <repository>
+      <id>oauth</id>
+      <url>http://oauth.googlecode.com/svn/code/maven</url>
+    </repository>
+    <repository>
+      <id>com.google.javascript</id>
+      <url>http://oss.sonatype.org/content/groups/staging</url>
+    </repository>
+    <!-- for jstl-1.2 for now.. -->
+    <repository>
+      <id>java.net</id>
+      <url>http://download.java.net/maven/2/</url>
+    </repository>
+  </repositories>
+
+  <!-- ====================================================================== -->
+  <!-- D E P E N D E N C I E S                                                -->
+  <!-- ====================================================================== -->
+  <dependencies>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>servlet-api</artifactId>
+      <version>2.5</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.11</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit-addons</groupId>
+      <artifactId>junit-addons</artifactId>
+      <version>1.4</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <version>3.1</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>xmlunit</groupId>
+      <artifactId>xmlunit</artifactId>
+      <version>1.3</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <dependencyManagement>
+    <dependencies>
+      <!-- project dependencies -->
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-features</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-common</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-gadgets</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-sample-container</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-server-resources</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+        <type>war</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-server-dependencies</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+        <type>pom</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-server</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+        <type>war</type>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-social-api</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-extras</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <classifier>${shindig.jdk.classifier}</classifier>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shindig</groupId>
+        <artifactId>shindig-common</artifactId>
+        <version>2.5.1-SNAPSHOT</version>
+        <type>test-jar</type>
+        <scope>test</scope>
+      </dependency>
+
+      <dependency>
+        <groupId>com.google.inject</groupId>
+        <artifactId>guice</artifactId>
+        <version>3.0</version>
+      </dependency>
+
+      <dependency>
+        <groupId>com.google.inject.extensions</groupId>
+        <artifactId>guice-multibindings</artifactId>
+        <version>3.0</version>
+      </dependency>
+
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>14.0.1</version>
+      </dependency>
+
+      <dependency>
+        <groupId>commons-codec</groupId>
+        <artifactId>commons-codec</artifactId>
+        <version>1.7</version>
+      </dependency>
+      <dependency>
+        <groupId>commons-fileupload</groupId>
+        <artifactId>commons-fileupload</artifactId>
+        <version>1.2.2</version>
+      </dependency>
+      <dependency>
+        <groupId>org.json</groupId>
+        <artifactId>json</artifactId>
+        <version>20070829</version>
+      </dependency>
+     <dependency>
+       <groupId>caja</groupId>
+       <artifactId>htmlparser</artifactId>
+       <version>r4209</version>
+       <scope>compile</scope>
+       <exclusions>
+         <!-- force use of xml-apis until caja fixes their pom -->
+         <exclusion>
+           <groupId>xerces</groupId>
+           <artifactId>xmlParserAPIs</artifactId>
+         </exclusion>
+       </exclusions>
+     </dependency>
+     <dependency>
+       <groupId>caja</groupId>
+       <artifactId>caja</artifactId>
+       <version>r5054</version>
+       <scope>compile</scope>
+       <exclusions>
+         <!-- force use of xml-apis until caja fixes their pom -->
+         <exclusion>
+           <groupId>xerces</groupId>
+           <artifactId>xmlParserAPIs</artifactId>
+         </exclusion>
+       </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>net.oauth.core</groupId>
+        <artifactId>oauth</artifactId>
+        <version>20100527</version>
+        <scope>compile</scope>
+      </dependency>
+      <dependency>
+        <groupId>net.oauth.core</groupId>
+        <artifactId>oauth-provider</artifactId>
+        <version>20100527</version>
+        <scope>compile</scope>
+      </dependency>
+      <dependency>
+        <groupId>net.oauth.core</groupId>
+        <artifactId>oauth-httpclient4</artifactId>
+        <version>20090913</version>
+        <scope>compile</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.commons</groupId>
+        <artifactId>commons-lang3</artifactId>
+        <version>3.1</version>
+        <scope>compile</scope>
+      </dependency>
+      <dependency>
+        <groupId>commons-io</groupId>
+        <artifactId>commons-io</artifactId>
+        <version>2.4</version>
+        <scope>compile</scope>
+      </dependency>
+      <dependency>
+        <artifactId>commons-collections</artifactId>
+        <groupId>commons-collections</groupId>
+        <version>3.2.1</version>
+      </dependency>
+      <dependency>
+        <groupId>diff_match_patch</groupId>
+        <artifactId>diff_match_patch</artifactId>
+        <version>current</version>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.mortbay.jetty</groupId>
+        <artifactId>jetty</artifactId>
+        <version>6.1.26</version>
+      </dependency>
+      <dependency>
+        <groupId>joda-time</groupId>
+        <artifactId>joda-time</artifactId>
+        <version>2.1</version>
+      </dependency>
+      <dependency>
+        <groupId>rome</groupId>
+        <artifactId>rome</artifactId>
+        <version>1.0</version>
+      </dependency>
+      <dependency>
+        <groupId>rome</groupId>
+        <artifactId>modules</artifactId>
+        <version>0.3.2</version>
+      </dependency>
+      <dependency>
+        <groupId>com.ibm.icu</groupId>
+        <artifactId>icu4j</artifactId>
+        <version>4.8.1.1</version>
+      </dependency>
+      <dependency>
+        <groupId>net.sourceforge.htmlunit</groupId>
+        <artifactId>htmlunit</artifactId>
+        <version>2.9</version>
+      </dependency>
+      <dependency>
+        <groupId>log4j</groupId>
+        <artifactId>log4j</artifactId>
+        <version>1.2.17</version>
+      </dependency>
+      <dependency>
+        <groupId>rhino</groupId>
+        <artifactId>js</artifactId>
+        <version>1.7R2</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.geronimo.specs</groupId>
+        <artifactId>geronimo-stax-api_1.0_spec</artifactId>
+        <version>1.0.1</version>
+      </dependency>
+      <dependency>
+        <groupId>net.sf.ehcache</groupId>
+        <artifactId>ehcache-core</artifactId>
+        <version>2.5.2</version>
+      </dependency>
+      <dependency>
+        <groupId>com.thoughtworks.xstream</groupId>
+        <artifactId>xstream</artifactId>
+        <version>1.4.3</version>
+      </dependency>
+      <dependency>
+        <groupId>xpp3</groupId>
+        <artifactId>xpp3_min</artifactId>
+        <version>1.1.4c</version>
+      </dependency>
+      <dependency>
+        <groupId>net.sourceforge.nekohtml</groupId>
+        <artifactId>nekohtml</artifactId>
+        <version>1.9.17</version>
+      </dependency>
+      <dependency>
+        <groupId>xerces</groupId>
+        <artifactId>xercesImpl</artifactId>
+        <version>2.9.1</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.sanselan</groupId>
+        <artifactId>sanselan</artifactId>
+        <version>0.97-incubator</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.tomcat</groupId>
+        <artifactId>el-api</artifactId>
+        <version>6.0.36</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.tomcat</groupId>
+        <artifactId>jasper-el</artifactId>
+        <version>6.0.36</version>
+      </dependency>
+      <dependency>
+        <groupId>de.odysseus.juel</groupId>
+        <artifactId>juel-impl</artifactId>
+        <version>2.2.5</version>
+        <exclusions>
+          <exclusion>
+            <groupId>de.odysseus.juel</groupId>
+            <artifactId>juel-api</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
+        <groupId>xml-apis</groupId>
+        <artifactId>xml-apis</artifactId>
+        <version>1.3.04</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.httpcomponents</groupId>
+        <artifactId>httpclient</artifactId>
+        <version>4.1.2</version>
+      </dependency>
+      <dependency>
+        <groupId>org.apache.shiro</groupId>
+        <artifactId>shiro-web</artifactId>
+        <version>1.1.0</version>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>slf4j-jdk14</artifactId>
+        <version>1.6.1</version>
+      </dependency>
+      <dependency>
+        <groupId>javax.servlet</groupId>
+        <artifactId>jstl</artifactId>
+        <version>1.2</version>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+</project>